Dynamic Array
An array that grows automatically by doubling its capacity, giving amortized O(1) append with O(1) indexed access.
Definition
A dynamic array wraps a plain Array with two numbers: size (elements in use) and capacity (slots allocated). Appends write into spare capacity; when it runs out, the structure allocates a new block twice as large, copies everything over, and continues.
Doubling makes the expensive copies rare enough that the total work of n appends is O(n) — amortized `O(1)` per append — while indexed access stays O(1). This is std::vector in C++, ArrayList in Java, list in Python, [] in JavaScript, and slices in Go.
It is the most used container in practice because it keeps the cache-friendliness of arrays while removing the fixed-size constraint. Its remaining weakness is the same as a static array: inserting or deleting in the middle costs O(n).
Intuition
A mental model before the formal terms.
Imagine packing books into a box. When the box is full you do not buy a box one book bigger — you buy one twice the size and move everything once. As the box grows you move books less and less often, so on average each book is handled only about two or three times over its lifetime, no matter how many books you own.
Compare that with growing by a fixed 10 slots each time: you would re-pack every 10 books, and the total moves would add up to n²/20 — quadratic. Doubling (or any constant factor > 1) is what keeps the total linear.
How it works
- Keep
data(a raw array),sizeandcapacity. Initiallycapacityis small (e.g. 4 or 8) andsize = 0. push(v): ifsize == capacity, callgrow(); thendata[size] = v; size++.grow(): allocate a new array of2 * capacity(or 1 if capacity was 0), copydata[0..size-1]into it, replacedataand updatecapacity.pop():size--and returndata[size]. Optionally shrink whensize <= capacity / 4to keep memory proportional tosizewithout thrashing.get(i)/set(i, v): bounds-check0 <= i < size, then direct index.insertAt/deleteAtshift elements exactly like a static array.
Why it works
Amortized analysis: with doubling, the k-th resize copies 2^k elements. Reaching size n triggers resizes copying 1 + 2 + 4 + … + n ≤ 2n elements in total. Dividing by n appends gives at most 2 extra copies per element — a constant.
The accounting view: charge each append 3 units — one to write the element, two saved for a future copy of itself and of an older element. When a resize copies c elements, the c/2 newest each paid 2 units, exactly covering all c moves.
Shrinking at 1/4 (not 1/2) full avoids oscillation: after shrinking to half capacity the array is exactly half full, so it needs size/2 pushes before growing again or size/4 pops before shrinking again — both Θ(size), so each resize is still paid for.
Operations
| Operation | Description | Cost |
|---|---|---|
| get(i) / set(i, v) | Direct indexed access with bounds check 0 <= i < size. | O(1) |
| push(v) | Append at data[size]; doubles capacity first if full. | O(1) amortized, O(n) worst |
| pop() | Remove and return the last element; optionally shrink at 1/4 occupancy. | O(1) amortized |
| insertAt(i, v) | Grow if needed, shift data[i..size-1] right, write v. | O(n) |
| deleteAt(i) | Shift data[i+1..size-1] left and decrement size. | O(n) |
| search(v) | Linear scan; O(log n) with binary search if kept sorted. | O(n) |
| grow() | Allocate 2 * capacity, copy size elements. | O(n) |
Recognition
How to tell a problem wants this.
- You need to collect results whose count is unknown in advance (filtered output, BFS frontier, path reconstruction).
- The language's built-in list/vector is being used as a stack (
push/popat the end). - The problem mentions amortized complexity, "why is
appendfast?", or asks you to implementArrayList/vector.
Interactive demo
Play, step, change the input. ← → and space work too.
No interactive visualization for this topic yet
Related visualizations are linked under Related.
Pseudocode
1class DynamicArray:2 data = allocate(4); size = 0; capacity = 43 push(v):4 if size == capacity: grow()5 data[size] = v; size += 16 grow():7 bigger = allocate(2 * capacity)8 copy data[0..size-1] into bigger9 data = bigger; capacity *= 210 pop():11 size -= 1; v = data[size]12 if size <= capacity / 4 and capacity > 4: shrink to capacity / 213 return v14 get(i): assert 0 <= i < size; return data[i]Implementation
1from typing import Generic, TypeVar2 3T = TypeVar("T")4 5 6class DynamicArray(Generic[T]):7 """Growable array; Python's built-in list already does exactly this."""8 91 · Storage, size and capacity10 def __init__(self, initial: int = 4) -> None:11 self._data: list[T | None] = [None] * initial12 self._n = 013 self._cap = initial14 15 def __len__(self) -> int:16 return self._n17 18 @property19 def capacity(self) -> int:20 return self._cap21 222 · Resize by copying into a new block23 def _resize(self, new_cap: int) -> None:24 bigger: list[T | None] = [None] * new_cap25 for i in range(self._n):26 bigger[i] = self._data[i]27 self._data = bigger28 self._cap = new_cap29 303 · Push (amortized O(1))31 def push(self, value: T) -> None:32 if self._n == self._cap:33 self._resize(self._cap * 2)34 self._data[self._n] = value35 self._n += 136 374 · Pop with shrink38 def pop(self) -> T:39 if self._n == 0:40 raise IndexError("pop from empty array")41 self._n -= 142 value = self._data[self._n]43 self._data[self._n] = None44 if self._cap > 4 and self._n <= self._cap // 4:45 self._resize(self._cap // 2)46 return value # type: ignore[return-value]47 485 · Bounds-checked access49 def get(self, i: int) -> T:50 if not 0 <= i < self._n:51 raise IndexError(i)52 return self._data[i] # type: ignore[return-value]53 54 def set(self, i: int, value: T) -> None:55 if not 0 <= i < self._n:56 raise IndexError(i)57 self._data[i] = value[None] * initialis the backing block;_nand_captrack logical and allocated size._resizecopies live slots into a bigger (or smaller) list — CPython does this in C insidelist_resize.pushdoubles when full, then stores at_n; this is whatlist.appenddoes with a ~1.125x growth factor.popclears the slot and halves capacity at 25% usage; CPython also shrinks when a list drops below half.get/setreject negative indices, unlikelist, to keep the teaching model simple.
CPython over-allocates by about 12.5%, so more reallocations happen than with doubling, but each is a fast C memcpy of pointers.
- Python
listis a dynamic array of object pointers;append/pop()are amortized O(1),insert(0, x)/pop(0)are O(n). sys.getsizeof(lst)shows the over-allocation growing in steps.- For a queue use
collections.deque; for dense numbers usearray.arrayor NumPy.
- Using
lst.pop(0)in a loop — O(n) per call. - Building a list with
lst = lst + [x]in a loop — copies every time, O(n^2). - Assuming a list comprehension pre-sizes the result; it still grows incrementally (though faster than append).
- Growth factor: libstdc++
std::vectordoubles, MSVC uses 1.5x, V8 uses ~1.5x + 16, CPythonlistuses ~1.125x — all are amortized O(1) per append. - Only C++ exposes
capacity()/reserve()/shrink_to_fit(); JS and Python hide the backing store entirely. - Reallocation invalidates C++ iterators and references; JS and Python references stay valid because they point at objects, not slots.
- Popping from empty: C++ is undefined behaviour, JS returns
undefined, Python raisesIndexError.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(1) | O(1) | |
| Search | O(n) | O(n) | |
| Insert | O(n) | O(n) | Middle insert shifts; see Push for the end. |
| Delete | O(n) | O(n) | O(1) amortized at the end (Pop). |
| Update | O(1) | O(1) | |
| Push (append) | O(1) amortized | O(n) | Worst case is the resize copy. |
| Pop | O(1) amortized | O(n) | O(n) only when shrinking. |
| Resize | O(n) | O(n) | |
| Space | O(n) | Capacity is at most 2n right after growth and at least n/4 with shrinking; memory is Θ(n). | |
Advantages & disadvantages
- Amortized
O(1)append withO(1)random access — the best of arrays without a fixed size. - Contiguous memory: cache-friendly iteration and low overhead (no per-element pointers, unlike Linked List).
- Ubiquitous: every mainstream language provides one as the default sequence type.
- Doubles as an efficient Stack.
- Individual appends can be
O(n)when a resize happens — unacceptable in hard real-time code. - Up to 50% of allocated memory may be unused right after a resize (with doubling).
- Insert/delete in the middle is
O(n); insert at the front isO(n)(use a Deque for that). - Resizing invalidates raw pointers/iterators into the old buffer (C++), a classic source of bugs.
Use cases
- Default collection for building up results, adjacency lists (Adjacency List), and DP tables of unknown length.
- Stack implementation (
push/popat the back). - Backing storage for a Binary Heap / Priority Queue and for Hash Table bucket arrays that rehash on growth.
- String builders that accumulate characters before a final join.
- Default choice for any sequence whose length is not known in advance.
- Stack-like access (push/pop at the end) with occasional random access.
- You want array performance (cache locality,
O(1)index) without managing capacity yourself.
- Frequent insert/delete at the front or middle — use a Deque (front) or a Linked List / balanced tree (middle by position).
- Strict per-operation latency bounds — a single append may trigger an
O(n)copy. - Keyed lookups — use a Hash Map; sorted-order queries — use a Binary Search Tree or keep the array sorted with Binary Search inserts.
Alternatives
Common mistakes
- Growing by a constant amount (e.g. +10) instead of a constant factor — total append cost becomes
O(n²). - Shrinking at 1/2 occupancy — a push/pop sequence at the boundary then resizes on every operation (thrashing).
- Holding a pointer/iterator across a
pushin C++ and reading freed memory after a resize. - Calling
insert(0, x)in a loop and expectingO(n)total; it isO(n²). - Quoting
O(1)append without the word "amortized" when asked about worst case.
Interview patterns
- "Implement ArrayList/vector" — write push with doubling and explain the amortized bound.
- Using a list as a stack for parentheses matching, monotonic stacks and DFS.
- Building an Adjacency List as an array of dynamic arrays.
O(1)delete by swapping with the last element and popping (insert/delete/getRandom in O(1)).
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Hash map or array?Beginner
- Stack versus queueBeginner
- Two SumBeginner
- Longest Substring Without Repeating CharactersIntermediate
- Daily TemperaturesIntermediate