Elementary Data Structures
Every container is built one of two ways: contiguous in an array, or linked through pointers. We trade cache-friendly random access against splicing, derive the **amortized append of a doubling dynamic array, and assemble the two ordered access disciplines — the LIFO stack and the FIFO queue (with its generalization, the deque**) — on top of both.
╌╌╌╌
Every data structure in this course (every tree, heap, hash table, and graph) is ultimately stored one of two ways, and the choice affects everything built on top of it. Elements are laid out contiguously in one block of memory, or scattered and joined by pointers. This first lesson works out the two strategies and the four ordered containers (array, linked list, stack, queue) that the rest of the module builds on.
Two ways to store a sequence
A contiguous structure stores its elements in a single block of memory, one after another. An array of elements each of size occupies one run of bytes, so element lives at a known offset from the start. A linked structure stores each element in its own separately-allocated node and uses a pointer in each node to find the next; the nodes may sit anywhere in memory.1
The contrast is sharp, and it drives every later choice:
- Random access. Contiguous wins outright. Element of an array is at address , computed with one multiply-add, so indexing is . In a linked list there is no address arithmetic; to reach the -th node you must follow pointers, which is .
- Splicing. Linked wins outright. To insert or delete an element given a pointer to its node, a linked list rewires a constant number of pointers in ; an array must shift every later element to keep the block contiguous, which is .
- Cache locality. Contiguous wins. A modern CPU reads memory in cache lines and prefetches sequentially, so a linear scan of an array is far faster than chasing pointers across scattered nodes, even though both are comparisons. Constant factors, not asymptotics, yet they are large.
- Space overhead. Linked pays per-element: every node carries one or two pointers besides its key. An array pays nothing per element but may reserve unused capacity (below).
Arrays and dynamic arrays
A fixed-size array is the contiguous structure in its purest form: allocate slots up front, index any of them in . Its limitation is that is fixed at allocation. Real programs rarely know the final size in advance, so we want a structure that grows.
A dynamic array (C++ vector, Python list, Java ArrayList) keeps a
contiguous backing block of some capacity the current size, and
appends into the spare room. When the block fills, it allocates a larger block,
copies the elements over, and frees the old one. The design decision that matters is
how much larger, and the answer is to double the capacity.
- 1if then
- 2
- 3allocate new block of capacity
- 4copy intothe resize
- 5free old block; ;
- 6
- 7
A single append is usually , writing into spare room and bumping the size, but the appends that trigger a resize cost because they copy the whole array. The worst case of one operation is therefore . Yet the average cost over a sequence of appends is , and this is worth proving.
The geometric growth is what makes this work: each resize is twice as expensive as the last but happens half as often, so the costs telescope into a constant per operation. Growing by a fixed increment instead of doubling would make the same appends cost . Amortized has two costs: an occasional latency spike on the doubling step, and up to wasted capacity right after a resize.2
The trace, append by append
The lemma's algebra is worth checking on a concrete trace. Start from an empty array with capacity and run appends, counting one unit per element written or copied:
| append | capacity before | resize? | copies | cost |
|---|---|---|---|---|
| no | ||||
| grow to | ||||
| grow to | ||||
| no | ||||
| grow to | ||||
| – | no | each | ||
| grow to | ||||
| – | no | each |
The total is writes plus copies: units for appends, under per operation, matching the aggregate bound. The spikes at appends (and next at ) double in height each time but arrive half as often, which is the telescoping made visible.
The aggregate proof above sums costs after the fact. The accounting method explains the same bound as a budget you could enforce up front: charge every append credits. One credit pays for writing the new element. The other two are banked on the element itself. When a resize hits at capacity , the elements appended since the previous resize each hold banked credits, enough to pay for copying themselves and one element from the older half of the array, which spent its own credits at an earlier resize. Every copy is prepaid, so no operation ever draws on future income, and credits cover any appends.2
Doubling is essential, not incidental. Suppose the array instead grew by a fixed increment each time it filled. Resizes would then occur at sizes , and the resize at size copies elements, so appends cost
which is per append for any constant . Concretely, appends with perform about copy operations where doubling performs under . Any geometric factor works ( trades a smaller memory overshoot for more frequent copies); arithmetic growth does not.
The same discipline runs in reverse for a shrinking array. Popping elements should eventually release memory, but halving the block the instant the array is half full invites thrashing: alternating push/pop at the boundary would resize on every operation. The standard fix is hysteresis, halving only when the array falls to a quarter full. After any resize, in either direction, the array is exactly half full, so at least cheap operations must pass before the next resize, and the amortized bound survives deletion too.
Linked lists
A linked list threads elements through pointers. In a singly linked list each node stores a and a pointer to its successor; a pointer names the first node and the last node's is . A doubly linked list adds a pointer, so the list can be traversed in both directions and a node can be removed knowing only itself.
The complexities follow directly from the pointer structure:
- Insert / delete given the node. . To delete node from a doubly linked list, set and , a constant number of pointer writes, no shifting. This is the linked list's signature advantage over an array.
- Search by key, or index by position. . There is no address arithmetic; you must walk the chain.
The boundary cases (deleting the head, deleting the tail, operating on an empty
list) force nil checks that clutter the code. A standard trick removes them: a
sentinel is a dummy node that is always present and never holds real data. Wrap
the list into a ring around one sentinel , with the first real
node and the last; now every node has a real predecessor and
successor, and delete needs no special cases.3
- 1
- 2
With a sentinel there are no nil guards: even at the ends, and
point at real nodes (possibly the sentinel itself), so the two
assignments always make sense.
The splice, pointer by pointer
Watch the delete on a concrete list. Take and delete the node holding ; call it . The first assignment, , rewrites the field of the -node to point at the -node. The second, , rewrites the field of the -node to point back at the -node. Two writes and the list reads in both directions. Nothing was shifted, nothing else was touched, and the cost is the same whether the list holds three nodes or three million: that locality is the whole case for linked storage.
Insertion is the same idea with four writes instead of two. To splice a new node in immediately after a node :
- 1learns its successor first
- 2
- 3old successor points back at
- 4finally lets go of the old link
The order of the writes is the classic pitfall. The first line reads , so the last line, which overwrites , must come after it: swap them and 's successor becomes itself, quietly turning the tail of the list into a self-loop. Run the trace on , inserting after the -node: line 1 points at the -node, line 2 points at the -node, line 3 rewrites the -node's to , and line 4 rewrites the -node's to . The list now reads , again in regardless of length. Deleting the head or splicing at the tail is still the same code under a sentinel, which is why the sentinel is worth its one node of overhead.
| operation | array | linked list |
|---|---|---|
| index / random access | ||
| search (unsorted) | ||
| insert/delete at known position | shift | splice |
| insert/delete at end | amortized | |
| cache locality | excellent | poor |
| extra space per element | none | 1–2 pointers |
Neither structure dominates: choose contiguous when you index and scan, linked when you splice in the middle and never need the -th element by number.
Stacks: last in, first out
A stack restricts access to one discipline: LIFO, last in, first out. Only the most recently inserted element is reachable. The operations are (add to the top), (remove the top), and (read the top without removing) — all .
A stack is trivial to back with a dynamic array: keep a index, push by writing and incrementing, pop by decrementing. (Amortized if it must grow.) Equally, a singly linked list with push/pop at the head is a stack with worst-case operations and no resize spikes.
Stacks appear wherever computation is nested: the call stack that holds function activation records, depth-first search, evaluating arithmetic expressions, and matching brackets. For brackets, push each opener, then pop and check on each closer, accepting iff the stack ends empty. That last pattern is the Valid Parentheses problem.
Queues and deques: first in, first out
A queue enforces the opposite discipline: FIFO, first in, first out, like a line at a counter. adds at the tail; removes from the head. Both are .
Backing a queue with an array needs care: if we always dequeued from index we would shift the whole array each time, . The fix is a circular buffer. Keep a fixed array of capacity and two indices, and ; enqueue writes and advances , dequeue reads and advances . The indices chase each other around the ring, reusing freed slots, so both operations stay with no shifting and no wasted scanning.4 (When the buffer fills, resize and re-lay-out into a larger ring at amortized , exactly as for the dynamic array.)
Wraparound, index by index
The modular arithmetic deserves one full trace. Take capacity and start empty with . Enqueue through : each write lands at and advances , leaving in slots – with , . Dequeue four times: the reads return in insertion order while advances to ; slots – still contain the old values, but they are logically free and are never erased. Now enqueue , , :
| operation | write | update | state after |
|---|---|---|---|
| enqueue | , | ||
| enqueue | , | ||
| enqueue | , |
The enqueue of is the wrap: steps off the right end of the array and the folds it back to slot , where the next write overwrites the stale . The queue now holds , physically split across slots – and but logically contiguous around the ring. Dequeues would keep reading in FIFO order, with making the same wrap three steps later.
One boundary case needs a decision. With only the two indices, describes both the empty queue and the full one, since a full ring's has lapped all the way around to . Either keep an explicit element count alongside the indices, or declare the buffer full at elements so the two states stay distinguishable; both choices are and both appear in production code. Forgetting the ambiguity entirely is the classic circular-buffer bug: the full buffer reports empty and silently drops a lap of data.
A deque (double-ended queue, pronounced deck
) generalizes both: it supports
insert and delete at both ends. A deque used at one end only is a stack;
used to push at one end and pop at the other, it is a queue, so the deque
subsumes everything in this lesson. A doubly linked list with a head and tail
sentinel implements a deque directly, and a circular buffer with both indices
movable in either direction does too; the Design Circular Deque problem asks for
the latter.
Elementary structures in practice
The textbook trade-off, contiguous versus linked, is only the starting point; real systems adjust it in several ways.
Growth factors. The amortized argument
works for any geometric factor, and standard libraries pick different ones for
different reasons. Microsoft's and most C++ std::vector implementations double;
GCC's libstdc++ also doubles, but Facebook's folly::fbvector grows by
precisely because doubling can never reuse the freed blocks. With a
factor below the golden ratio , the sum of all previous
block sizes eventually exceeds the next block, so an allocator can place the new
array in the coalesced space the old ones left behind; doubling can never do
this. The choice is a genuine trade of memory footprint against copy frequency,
and both live in production.
Bulk nodes for cache locality. A plain linked list's one-node-per-
element layout has poor cache behavior, so practical linked
structures store
many elements per node. An unrolled linked list keeps a small array (say
to elements) in each node, recovering most of an array's locality while
keeping splicing at node boundaries; this is the shape of many production
rope
and gap buffer
text structures. A B-tree or its cache-oblivious
cousins push the same idea to a full tree, which is why they
dominate on disk.
Standard-library deques. Python's collections.deque and Java's
ArrayDeque are not linked lists but blocked circular buffers, arrays of
fixed-size blocks, giving push/pop at both ends and cache-friendly
iteration. And in immutable/functional languages, the everyday list
is a
persistent singly linked list whose shared tails make prepend and
structural sharing cheap, a different sweet spot from the mutable dynamic array
that dominates imperative code.5
Takeaways
- Every container is either contiguous (an array — random access by address arithmetic, cache-friendly, to splice) or linked (nodes joined by pointers — splice given the node, to index, a pointer of overhead per element). The choice is a trade, not a winner.
- A dynamic array appends in amortized by doubling capacity on overflow: the aggregate copy cost over appends is , or by the accounting method, prepaid credits per append cover every copy. Fixed-increment growth costs instead; shrinking halves only at one-quarter full to avoid thrashing. The worst-case single append is still on the resize step.
- A doubly linked list inserts and deletes in given the node: delete is two pointer writes, insert-after is four, with write order mattering (read before overwriting it). A sentinel node erases the boundary cases.
- A stack is LIFO (, all ) and underlies the call stack, DFS, expression evaluation, and bracket matching.
- A queue is FIFO, implemented as a circular buffer with and indices advanced for ends. Since means both empty and full, keep a count (or cap at elements) to tell them apart. The deque generalizes both stack and queue to operations at either end.
Footnotes
- Skiena, §3.1–3.2, Contiguous vs. Linked Structures: the array-vs-pointer trade-off and its consequences for access, splicing, and locality. ↩
- CLRS, Ch. 10, Elementary Data Structures (with the amortized analysis of Ch. 16): geometric doubling gives amortized table append. ↩ ↩2
- CLRS, Ch. 10, Elementary Data Structures (§10.2): doubly linked lists and the sentinel that removes boundary cases from insert/delete. ↩
- CLRS, Ch. 10, Elementary Data Structures (§10.1): stacks and the circular-array queue with head/tail indices taken . ↩
- On sub- growth factors reusing freed memory, see the folly
fbvectordesign notes; on unrolled lists, Shao & Reps,Unrolling lists
(1994); persistent lists are standard in Okasaki, Purely Functional Data Structures (1998). ↩
╌╌ END ╌╌