Sparround

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).

OperationComplexityWhy
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 arrayO(n)Worst case you must look at every element
Binary search in a sorted arrayO(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 an ArrayDeque/circular buffer; if you delete from the middle a lot, consider a linked list.
  • Lookup by keyarr.find(u => u.id === id) is O(n). Called inside a loop it becomes O(n²). Fix: a Map/HashMap gives O(1).
  • Membership testsarr.includes(x) is O(n); a Set makes it O(1).
  • Very sparse data — if 100 of a million slots are filled, an array wastes memory; a Map fits 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