How it works
The explanation, with examples you can run.
Amortised growth
A dynamic array keeps two numbers: its length (elements in use) and its capacity (slots allocated). push writes into the next free slot, which is O(1). When length reaches capacity, it allocates a larger block, copies every element across, and frees the old one, which is O(n) for that one push. The trick is to grow by a factor, not a fixed amount:
Press Run to see what your code prints, in order.
Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.
A million pushes cause 1,048,575 element copies in total, about one per push. The expensive pushes happen at sizes 1, 2, 4, 8 … and their costs add up to less than twice the final size. That is what amortised O(1) means: any one push may be slow, but n pushes cost O(n) altogether.
Real implementations choose different factors to trade memory for copying. CPython's list grows by roughly one eighth plus a little, about 1.125x; Java's ArrayList documentation promises only that adding an element "has constant amortized time cost". Any factor above 1 gives the same Big-O. If you know the final size, pre-size the array (new Array(n) then fill it, ArrayList(capacity), [None] * n) to skip the copies entirely.
The cost of the front
Inserting at index 0 means moving every existing element one slot to the right. Removing from the front means moving them all left. Each is O(n), so doing it n times is O(n²):
Press Run to see what your code prints, in order.
Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.
In Node 22 on a laptop this took about 2 ms for the pushes and between 650 and 950 ms for the unshifts, depending on the run. Inserting in the middle (splice) has the same cost. When you need cheap operations at both ends, use a ring buffer: a fixed block with a head and tail index that wrap around, which is how Python's collections.deque and Java's ArrayDeque avoid shifting.
Why arrays usually win
Even for operations where another structure has a better Big-O, arrays often win in practice: no per-element allocation, no pointers to follow, and the hardware is built to stream through consecutive memory. Reach for something else when you need cheap insertion in the middle, cheap removal at the front, or lookups by key rather than by position.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.