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(\texttt{data.size})$ (i.e., $O(n)$) or $\Theta(used-target)$
$T_{remove}(n)$ is $O(n)$ and $\Omega(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(\texttt{data.size})$ (i.e., $O(n)$) ... but ...
$T_{append}(n)$ is $O(n)$ and $\Omega(1)$
(these bounds are also "tight", so no $\Theta$-bound)
How often do we hit the 🙁 case?
For $n$ appends into an empty buffer...
While $\texttt{used} \leq \texttt{INITIAL_SIZE}$: $\sum_{i = 0}^{\texttt{IS}} \Theta(1)$
And after: $\sum_{i = \texttt{IS}+1}^{n} \Theta(i)$
Total for $n$ insertions: $\Theta(n^2)$
For $n$ appends into an empty buffer...
While $\texttt{used} \leq \texttt{INITIAL_SIZE}$: $\sum_{i = 0}^{\texttt{IS}} \Theta(1)$
And after: $$\sum_{i = \texttt{IS}+1}^{n} \begin{cases} \Theta(i) & \textbf{if } i = \texttt{IS} \mod 10\\ \Theta(1) & \textbf{otherwise} \end{cases}$$
... or ... $$ \left(\sum_{i = \texttt{IS}+1}^{n} \Theta(1)\right) + \left(\sum_{j = 0}^{\frac{(n - \texttt{IS}+1)}{10}} \Theta((\texttt{IS}+1+j)\cdot 10) \right) $$
Total for $n$ insertions: $\Theta(n^2)$
For $n$ appends into an empty buffer...
While $\texttt{used} \leq \texttt{INITIAL_SIZE}$: $\sum_{i = 0}^{\texttt{IS}} \Theta(1)$
And after... $$\sum_{i = IS+1}^{n} \begin{cases} \Theta(i) & \textbf{if } i = \texttt{IS} \cdot 2^k \textbf{ (for any $k \in \mathbb N$)}\\ \Theta(1) & \textbf{otherwise} \end{cases}$$
How many boxes for $n$ inserts? $\Theta(\log(n))$
How much work for box $j$?
How much work for $n$ inserts? $$\sum_{j = 0}^{\Theta(\log(n))}\Theta(2^j)$$
Total for $n$ insertions: $\Theta(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\left(\frac{T(n)}{n}\right)$
e.g., the amortized runtime of append is $O(\frac{n}{n}) = O(1)$
(even though the worst-case runtime is $O(n)$)