Arrays and dynamic arrays
An array is a structure where elements of the same type sit contiguously in memory. That one sentence explains every property arrays have.
Because elements are adjacent, the address of element i is computed by a formula: base + i × elementSize. No searching is involved — which is why reading and writing by index is O(1), however large n gets.
The second, less-discussed benefit of contiguity is cache locality: the CPU reads a whole block (a cache line) at a time, so walking an array sequentially is far faster than chasing pointers, for the same number of operations. Real measurements can differ by 5-10x — even though both are O(n).
| Operation | Complexity | Why |
|---|---|---|
| Read/write by index `arr[i]` | O(1) | The address is computed by a formula |
| Append (`push`) | amortized O(1) | Usually writes into free capacity; growing costs O(n) when full |
| Remove from the end (`pop`) | O(1) | Only the length shrinks, nothing shifts |
| Insert at the front (`unshift`) | O(n) | Every element shifts one slot right |
| Remove from the middle (`splice`) | O(n) | Everything to the right shifts left so no hole remains |
| Search in an unsorted array | O(n) | Worst case you must look at every element |
| Binary search in a sorted array | O(log n) | The range halves at each step |
| Sorting (`sort`) | O(n log n) | The theoretical bound for comparison-based sorting |
Why dynamic arrays exist. A classic array's size is fixed at creation, because a contiguous block is reserved and the neighbouring addresses may belong to someone else. JS Array, Kotlin ArrayList, Dart List are all dynamic arrays: internally a fixed-size buffer plus a separate length counter.
The growth mechanism:
- When
length == capacity, a new buffer — usually twice as large — is allocated - All elements are copied into it — that is
O(n) - The old buffer becomes garbage
Because capacity grows geometrically (1, 2, 4, 8, 16...), n appends cost O(n) in total, i.e. amortized O(1) each. If capacity grew by a constant step (say +10 each time), the total would be O(n²) — making that comparison in an interview is a strong signal.
When the element count is known up front, pre-allocating capacity removes the copying entirely (new Array(n), ArrayList(n), List.filled(n, 0)).
When an array is the wrong choice:
- Frequent insertion/deletion at the front or middle — every operation is an
O(n)shift. If you need queue behaviour, reach for anArrayDeque/circular buffer; if you delete from the middle a lot, consider a linked list. - Lookup by key —
arr.find(u => u.id === id)isO(n). Called inside a loop it becomesO(n²). Fix: aMap/HashMapgivesO(1). - Membership tests —
arr.includes(x)isO(n); aSetmakes itO(1). - Very sparse data — if 100 of a million slots are filled, an array wastes memory; a
Mapfits better.
Conversely, an array is almost always right for: sequential iteration, index access, fixed-size data, and small collections.
Interview tip. The most common question: "What's the difference between an array and a linked list?" Start from contiguous memory vs pointers, then list the consequences: array — O(1) indexing, poor insertion; linked list — O(1) insertion (given the node), no indexing. Mentioning cache locality sets you apart from most candidates.
The second classic: "Why is `push` O(1) if the array has to grow?" — answer with the amortized argument (doubling, geometric series).
The most common mistakes: calling includes/indexOf/find inside a loop, creating O(n²) without noticing; assuming splice is O(1); and mutating the array (splice) while looping over it, shifting the indices — the classic "skips every other element" bug.
📚 Sources and documentation
- Arrayofficialdeveloper.mozilla.org