Dynamic Arrays
The core idea
Fixed storage underneath. Flexible size on top.
Fixed-size array
size 4 · capacity 4
Direct index access stays O(1), but there is no room to append.
Dynamic array
size 5 · capacity 8
Copy once, append the value, then reuse the spare capacity for future appends.
Implementation Details
Let's look at how we might implement a dynamic array using only fixed-size arrays.
The Challenge: Resizing
The challenging case is when the underlying fixed-size array is full. We call this process resizing.
When a full dynamic array resizes, it usually creates a larger contiguous block elsewhere in memory, then copies the existing elements into it.
Why resizing requires a copy
An array needs one contiguous block. If the next address is occupied, it cannot grow in place.
1 · Full block
cannot extend →The neighboring address belongs to something else.
2 · Larger block
capacity 8A new contiguous block has room for future appends.
If we increased capacity by only 1 slot, nearly every append would trigger this copy. That is too slow.
Doubling is the standard interview choice. More generally, any fixed multiplier greater than preserves amortized append; production libraries may choose a different factor.
Code Structure
We track two key properties:
size: The number of actual elements.capacity: The number of "slots" in the underlying fixed-size array.
class DynamicArray:
def __init__(self):
self.capacity = 10
self.size = 0
self.fixed_array = [None] * self.capacity
def append(self, x):
if self.size == self.capacity:
self._resize(self.capacity * 2)
self.fixed_array[self.size] = x
self.size += 1
def _resize(self, new_capacity):
new_fixed_array = [None] * new_capacity
for i in range(self.size):
new_fixed_array[i] = self.fixed_array[i]
self.fixed_array = new_fixed_array
self.capacity = new_capacity
Implementation invariants
Keep these true0 ≤ size ≤ capacity0 ≤ i < sizewrite at index sizeorder is preservedOptional Shrinking
Removing the last element normally just decreases size, so pop_back() is . Some implementations optionally shrink the backing array to reclaim memory; others keep the capacity until an explicit trim.
If you implement automatic shrinking, leave a gap between the grow and shrink thresholds:
- Shrink at 50%? Bad idea. If we hover around 50%, we might constantly resize and shrink (thrashing).
- Shrink at 25%? Good idea. Halve the capacity when the array is 25% full. After shrinking, it is 50% full—enough room to avoid repeated resizing without wasting too much space.
With this gap, pop_back() remains amortized, although the occasional shrink itself costs .
Amortized Time Analysis
What does "amortized" mean?
Appending to a dynamic array has poor worst-case performance (when resizing happens, it takes ), but excellent amortized performance ().
Geometric growth
Each resize pays for many future appends
copy 1 element
copy 2 elements
copy 4 elements
copy 8 elements
1 + 2 + 4 + 8 = 15→total copying grows linearly, so each append averages O(1).Quick check: starting with capacity 1, append 8 values. How many existing elements are copied?
Track the resize points before checking the answer.
Solution
Resizes happen at capacities , , and , so we copy existing elements. The final size and capacity are both .
Extra Operations
It's important to distinguish between operations at the end of the array vs. arbitrary indices.
-
pop(i): Removes element at indexi.- Time: . We must shift all elements after
ione slot to the left to close the gap. - Note: If you need to pop from both ends often, consider a Deque (Double-Ended Queue).
- Time: . We must shift all elements after
-
insert(i, x): Insertsxat indexi.- Time: . We must shift all elements from
ionwards one slot to the right.
- Time: . We must shift all elements from
-
contains(x): Checks ifxexists.- Time: . In an unordered array, we must scan linearly.
When to Use It
When to use a dynamic array
Good fit
- Fast indexing and cache-friendly iteration
- Mostly append or remove at the end
- Compact storage matters
Consider another structure
- Frequent insertions or removals near the front
- Both ends must be efficient—use a deque
- References must remain stable after growth
If the final size is roughly known, reserve capacity up front to avoid repeated reallocations. In low-level languages, a resize may invalidate existing references or iterators.
Key Takeaways
| Operation | Time Complexity | Notes |
|---|---|---|
get(i) | Constant access time | |
set(i, x) | Constant update time | |
append(x) | (amortized) | worst case (resize) |
pop_back() | Reduces size; optional shrinking is implementation-dependent | |
pop(i) | Requires shifting elements | |
insert(i, x) | Requires shifting elements | |
contains(x) | Linear scan required |
Practice Problems
Design Dynamic Array
Design a Dynamic Array (aka a resizable array) class, such as an ArrayList in Java or a vector in C++.
Remove Element
Given nums and val, remove every val in-place and return the new length k. Only the first k positions matter after the operation.
Concatenation of Array
Given nums of length n, return an array of length 2n containing nums twice: [nums, nums].