Sept 19, 2022
An Array of n items of type T:
Challenge: Operations that modify the array size
require copying the array.
Solution Reserve extra space in the array!
An ArrayBuffer of type T:
class ArrayBuffer[T] extends Buffer[T]
{
var used = 0
var data = Array[Option[T]].fill(INITIAL_SIZE) { None }
def length = used
def apply(i: Int): T =
{
if(i < 0 || i >= used){ throw new IndexOutOfBoundsException(i) }
return data(i).get
}
/* ... */
}
What the heck is Option[T]?
val x = functionThatCanReturnNull()
x.frobulate()
java.lang.NullPointerException (in production)
val x = functionThatCanReturnNull()
if(x == null) { handle this case }
else { x.frobulate() }
Problem: It's easy to miss this test
(and bring down a million-dollar server)!
val x = functionThatReturnsOption()
x.frobulate()
error: value frobulate is not a member of Option[MyClass]
At compile time.
Bonus: an Option[T] is a Seq[T]
Digression over!
def remove(target: Int): T =
{
/* Sanity-check inputs */
if(target < 0 || target >= used){
throw new IndexOutOfBoundsException(target) }
/* Shift elements left */
for(i <- target until (used-1)){
data(i) = data(i+1)
}
/* Update metadata */
data(used-1) = None
used -= 1
}
What is the complexity?
O(data.size) (i.e., O(n)) or Θ(used−target)
Tremove(n) is O(n) and Ω(1)
(these bounds are "tight")
We usually parameterize runtime complexity by datastructure size, we can measure runtime in terms of other parameters (e.g., used and i).
def append(elem: T): Unit =
{
if(used == data.size){ /* 🙁 case */
/* assume newLength > data.size, but pick it later */
val newData = Array.copyOf(original = data, newLength = ???)
/* Array.copyOf doesn't init elements, so we have to */
for(i <- data.size until newData.size){ newData(i) = None }
}
/* Append element, update data and metadata */
newData(used) = Some(elem)
data = newData
used += 1
}
What is the complexity?
O(data.size) (i.e., O(n)) ... but ...
Tappend(n) is O(n) and Ω(1)
(these bounds are also "tight", so no Θ-bound)
How often do we hit the 🙁 case?
For n appends into an empty buffer...
While used≤INITIAL_SIZE: ∑ISi=0Θ(1)
And after: ∑ni=IS+1Θ(i)
Total for n insertions: Θ(n2)
For n appends into an empty buffer...
While used≤INITIAL_SIZE: ∑ISi=0Θ(1)
And after: n∑i=IS+1{Θ(i)if i=ISmod10Θ(1)otherwise
... or ... (n∑i=IS+1Θ(1))+((n−IS+1)10∑j=0Θ((IS+1+j)⋅10))
Total for n insertions: Θ(n2)
For n appends into an empty buffer...
While used≤INITIAL_SIZE: ∑ISi=0Θ(1)
And after... n∑i=IS+1{Θ(i)if i=IS⋅2k (for any k∈N)Θ(1)otherwise
How many boxes for n inserts? Θ(log(n))
How much work for box j?
How much work for n inserts? Θ(log(n))∑j=0Θ(2j)
Total for n insertions: Θ(n)
append(elem) is O(n)
n calls to append(elem) are O(n)
The cost of n calls is guaranteed.
(It would be nice if we had a name for this...)
If n calls to a function take O(T(n))...
We say the Amortized Runtime is O(T(n)n)
e.g., the amortized runtime of append is O(nn)=O(1)
(even though the worst-case runtime is O(n))