Skip to content
Back to Home

Dynamic Arrays

3 practice problemsInteractive visual guide

The core idea

Fixed storage underneath. Flexible size on top.

Fixed-size array

size 4 · capacity 4

Full
4
0
7
1
9
2
12
3

Direct index access stays O(1), but there is no room to append.

append(15)full → resize ×2

Dynamic array

size 5 · capacity 8

Appended
4
0
7
1
9
2
12
3
15
4
·
5
·
6
·
7

Copy once, append the value, then reuse the spare capacity for future appends.

Why dynamic arrays matter:Interview readinessReusable amortizationIntuitive Big O

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 →
4
100
7
101
9
102
12
103
busy
104

The neighboring address belongs to something else.

allocate elsewherecopy 4 values

2 · Larger block

capacity 8
4
220
7
221
9
222
12
223
·
224
·
225
·
226
·
227

A new contiguous block has room for future appends.

Copying n existing values makes a resize O(n).

If we increased capacity by only 1 slot, nearly every append would trigger this O(n)O(n) copy. That is too slow.

Doubling is the standard interview choice. More generally, any fixed multiplier greater than 11 preserves amortized O(1)O(1) 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.
python
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 true
Size and capacity0 ≤ size ≤ capacity
Valid index0 ≤ i < size
Next appendwrite at index size
After resizingorder is preserved

Optional Shrinking

Removing the last element normally just decreases size, so pop_back() is O(1)O(1). 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 O(1)O(1) amortized, although the occasional shrink itself costs O(n)O(n).

Amortized Time Analysis

What does "amortized" mean?

Appending to a dynamic array has poor worst-case performance (when resizing happens, it takes O(n)O(n)), but excellent amortized performance (O(1)O(1)).

Geometric growth

Each resize pays for many future appends

12

copy 1 element

24

copy 2 elements

48

copy 4 elements

816

copy 8 elements

1 + 2 + 4 + 8 = 15total 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 11, 22, and 44, so we copy 1+2+4=71 + 2 + 4 = 7 existing elements. The final size and capacity are both 88.

Extra Operations

It's important to distinguish between operations at the end of the array vs. arbitrary indices.

  1. pop(i): Removes element at index i.

    • Time: O(n)O(n). We must shift all elements after i one slot to the left to close the gap.
    • Note: If you need to pop from both ends often, consider a Deque (Double-Ended Queue).
  2. insert(i, x): Inserts x at index i.

    • Time: O(n)O(n). We must shift all elements from i onwards one slot to the right.
  3. contains(x): Checks if x exists.

    • Time: O(n)O(n). 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

OperationTime ComplexityNotes
get(i)O(1)O(1)Constant access time
set(i, x)O(1)O(1)Constant update time
append(x)O(1)O(1) (amortized)O(n)O(n) worst case (resize)
pop_back()O(1)O(1)Reduces size; optional shrinking is implementation-dependent
pop(i)O(n)O(n)Requires shifting elements
insert(i, x)O(n)O(n)Requires shifting elements
contains(x)O(n)O(n)Linear scan required