Lists §
Lists are flat, sequential data structures that store elements in a fixed order. They are relevant when the access pattern is positional (by index) or when the collection is small enough that a linear scan is acceptable. They form the foundation upon which all other search structures build: a hash table is an array of buckets, a B-tree is a tree of arrays, and a graph is a set of adjacency lists.
The progression through this page follows the natural evolution of the idea: arrays are the primitive (fixed-size, contiguous); vectors add dynamic sizing on top of arrays; linked lists trade cache locality for splice at arbitrary positions; stacks, queues, and deques restrict the access pattern to the ends of any of the above; and Bloom filters repurpose a bit array as a probabilistic membership oracle.
The trade-off triangle. Choosing among these structures is a three-way tension: random-access speed (favors arrays), insertion/deletion speed (favors linked lists), and space overhead (favors arrays). No single structure wins all three corners, so the right choice is dictated by which corner your access pattern stresses most.
Authoritative sources
Arrays §
An array of elements in contiguous memory supports:
- Random access by index:
- Search by value:
- Insertion / deletion at position : (shift)
Details
How it works. An array is a block of bytes (where is the element width) allocated contiguously. Element lives at address , so the CPU computes the address with a single multiply-and-add. No pointer chasing is needed.
Worked example. Consider an array of 8 int32 values (4 bytes each) starting at address 0x1000:
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| Address | 0x1000 | 0x1004 | 0x1008 | 0x100C | 0x1010 | 0x1014 | 0x1018 | 0x101C |
| Value | 42 | 7 | 99 | 1 | 55 | 3 | 88 | 12 |
Accessing index 5: address = 0x1000 + 5 × 4 = 0x1014 → value 3. One memory access, .
Cache locality. A cache line (typically 64 bytes) loads 16 int32 elements at once. Sequential iteration over an array achieves near-peak bandwidth because each access hits the same cache line. This is why arrays outperform linked lists for iteration despite insertion. See Memory Hierarchy for the full treatment of cache lines, spatial and temporal locality, and the memory wall.
Lookup tables. A precomputed array indexed by the key itself (e.g., a 256-entry table for byte values). access, space. Used for character classification, base64 decoding, and small integer key maps. See Knuth, TAOCP Vol. 1: Fundamental Algorithms for the classical treatment of arrays and sequential access.
Example problem.
Problem
You have an array of 1000 int64 values. How many cache lines are touched by a single full sequential scan (assuming 64-byte cache lines and no prefetching)?
Solution
Each int64 is 8 bytes. 1000 × 8 = 8000 bytes total. 8000 ÷ 64 = 125 cache lines. A sequential scan touches exactly 125 cache lines (one miss per line, assuming cold cache).
Vectors (growable arrays) §
A vector amortizes append by growing capacity geometrically. Over insertions, the underlying array is reallocated times; total work is (amortized per insertion).
Details
How it works. A vector maintains two numbers: size (number of elements) and capacity (number of allocated slots). When size == capacity, the vector allocates a new block of bytes, copies all elements, and frees the old block. The amortized cost per insertion is element-copies.
Growth factor. Any constant factor gives amortized per insertion. For example, gives amortized cost 3 per insertion; gives amortized cost 2. The factor minimizes the number of reallocations (), while smaller factors reduce peak memory waste. Peak memory usage is at most elements (just before a shrink, if supported).
Worked example. Start with capacity 1, growth factor 2:
| Insertion # | Size | Capacity | Reallocation? | Copies |
|---|---|---|---|---|
| 1 | 1 | 1 | No | 0 |
| 2 | 2 | 2 | Yes (1→2) | 1 |
| 3 | 3 | 4 | Yes (2→4) | 2 |
| 4 | 4 | 4 | No | 0 |
| 5 | 5 | 8 | Yes (4→8) | 4 |
| 6 | 6 | 8 | No | 0 |
| 7 | 7 | 8 | No | 0 |
| 8 | 8 | 8 | No | 0 |
| 9 | 9 | 16 | Yes (8→16) | 8 |
Total copies for 9 insertions: 1+2+4+8 = 15 < 2×9. Amortized ≈ 1.67 per insertion.
Language implementations. In C++, std::vector uses geometric growth (typically doubling). In Rust, Vec uses a similar strategy. In Python, list over-allocates by per growth step for large lists; for small lists the capacity roughly doubles. See CLRS, Ch. 10 (Elementary Data Structures) for the amortized-analysis treatment of dynamic arrays.
Vectors and sorting. A sorted vector supports binary search.
Example problem.
Problem
A vector starts with capacity 1 and doubles on overflow. After inserting 1024 elements, how many total element-copies have occurred?
Solution
Reallocations happen at sizes 1, 2, 4, 8, …, 512. Copies: 1+2+4+…+512 = 1023. Total copies = 1023 ≈ for (a power of 2).
Linked lists §
A singly linked list stores elements in nodes with a next pointer. A doubly linked list adds a prev pointer.
- Access by index:
- Insertion / deletion at a known node:
- Space: + pointer overhead per node (8 bytes for
next, 16 bytes fornext+prevon 64-bit)
Details
How it works. Each node is a separate heap allocation containing the element and one or two pointers. To insert after node , allocate a new node, set its next to , then set to the new node. No existing elements move.
Worked example. Insert 42 after the node holding 7 in the list 3 → 7 → 99:
- Allocate new node:
new = Node(42, next = NULL) new.next = 7.next→new.nextpoints to node(99)7.next = new→ node(7).next now points to node(42)
Result: 3 → 7 → 42 → 99. Three pointer writes, .
Cache locality. Each node is a separate heap allocation, so iteration causes a cache miss per node (unless the allocator happens to place them adjacently). This is the fundamental trade-off: linked lists buy splice at the cost of spatial locality. They are preferred when:
- Frequent insertion/deletion at arbitrary positions (e.g., an editor’s undo buffer).
- The collection is small and the pointer overhead is negligible.
- A memory pool or arena allocator is used to improve locality.
In practice, most general-purpose containers (Java ArrayList, C++ std::vector) prefer arrays over linked lists. See Memory Management for the heap allocation model that makes linked lists expensive, and CLRS, Ch. 10 (Elementary Data Structures) for the pointer-relinking analysis.
Example problem.
Problem
You have a singly linked list of 1000 nodes. What is the cost of deleting the node at position 500? What about a doubly linked list?
Solution
Singly: — you must walk from the head to position 499 to get the predecessor, then relink. Cost ≈ 500 pointer dereferences.
Doubly: still to find position 500 (no random access), but once you have the node, deletion is (relink prev.next and next.prev). The savings are only in the deletion step itself, not the search.
Stacks, Queues, and Deques §
These are restricted access patterns layered on top of any of the structures above. They expose only the ends of the sequence, giving for every operation regardless of the underlying representation.
| Structure | Access pattern | Push / Enqueue | Pop / Dequeue | Peek |
|---|---|---|---|---|
| Stack | LIFO (last-in, first-out) | O(1) | O(1) | O(1) |
| Queue | FIFO (first-in, first-out) | O(1) | O(1) | O(1) |
| Deque | Both ends | O(1) | O(1) | O(1) |
Details
Stack (LIFO). The most recent element is the first removed. Implementations: an array with a top index (push = write at top, pop = read and decrement), or a singly linked list where the head is the top. Typical uses: call-stack management, expression evaluation, DFS traversal (see Graphs), undo/redo buffers.
Worked example (stack). Push 3, 7, 1; pop; push 9; pop:
| Step | Operation | Stack (top → bottom) |
|---|---|---|
| 1 | push 3 | [3] |
| 2 | push 7 | [7, 3] |
| 3 | push 1 | [1, 7, 3] |
| 4 | pop → 1 | [7, 3] |
| 5 | push 9 | [9, 7, 3] |
| 6 | pop → 9 | [7, 3] |
Queue (FIFO). The oldest element is the first removed. Implementations: a circular buffer (array with head and tail indices that wrap around), or two stacks (enqueue on one, dequeue from the other, amortized ). Typical uses: BFS traversal (see Graphs), task scheduling, I/O buffering, message passing.
Worked example (queue, circular buffer of size 4).
| Step | Operation | Buffer [0..3] | head | tail |
|---|---|---|---|---|
| 1 | enqueue 10 | [10, _, _, _] | 0 | 1 |
| 2 | enqueue 20 | [10, 20, _, _] | 0 | 2 |
| 3 | dequeue → 10 | [_, 20, _, _] | 1 | 2 |
| 4 | enqueue 30 | [_, 20, 30, _] | 1 | 3 |
| 5 | dequeue → 20 | [_, _, 30, _] | 2 | 3 |
| 6 | enqueue 40 | [_, _, 30, 40] | 2 | 0 (wrap) |
Deque (double-ended queue). Supports push/pop at both ends in . Implementations: a circular buffer with room for one extra slot (to distinguish full from empty), or a block list (array of fixed-size blocks, each pointing to the next/previous block). Typical uses: sliding-window maximum, BFS with backtracking, undo with both directions.
Relationship to other structures. A stack is a vector with only push_back/pop_back exposed. A queue is a vector (or circular buffer) with push_back/pop_front. A deque is a vector with both ends exposed. The restriction is purely on the interface, not the implementation. See CLRS, Ch. 10 (Elementary Data Structures) for the standard stack/queue/deque definitions.
Example problem.
Problem
Design a queue using two stacks such that each element is moved at most twice over its lifetime. What is the amortized cost of enqueue and dequeue?
Solution
Maintain in_stack and out_stack. enqueue(x): push onto in_stack. dequeue(): if out_stack is empty, pop all elements from in_stack and push them onto out_stack (reversing order). Then pop from out_stack. Each element is pushed/popped at most twice (once into in_stack, once into out_stack), so the amortized cost per operation is .
Bloom filters §
A Bloom filter is a space-efficient probabilistic set-membership test built on a bit array (a list of bits). It answers “is element in the set?” with no false negatives but a tunable false-positive rate.
A Bloom filter repurposes the simplest list — a bit array — as a probabilistic oracle, illustrating how the array primitive underlies even non-sequential data structures.
- independent hash functions map each element to bits in a bit array of size .
- Insert: set bits. .
- Query: all bits set → “maybe present”; any bit clear → “definitely absent”. .
- False positive rate: .
- No false negatives. No deletion (unless using a counting Bloom filter).
Details
How it works. Each element is hashed times, producing bit positions in the array. Insertion sets those bits. A query checks whether all positions are set. As more elements are inserted, bits collide and the false-positive rate rises.
Worked example. Bit array of bits, hash functions. Insert elements A, B, C:
| Element | h₁ | h₂ | h₃ | Bits set |
|---|---|---|---|---|
| A | 2 | 7 | 11 | positions 2, 7, 11 |
| B | 4 | 7 | 13 | positions 4, 7, 13 |
| C | 2 | 9 | 13 | positions 2, 9, 13 |
Array after inserts: [0,0,1,0,1,0,0,1,0,1,0,1,0,1,0,0]
Query D (hashes to positions 2, 7, 14): position 14 is 0 → definitely absent. ✓ Query E (hashes to positions 2, 7, 13): all set → maybe present (false positive if E was never inserted).
Optimal parameters. For elements and target FPR :
For 10M elements at 1% FPR: bits per element, total million bits MB.
Uses. Cache filtering (avoid disk lookups for missing keys), deduplication (web crawlers), distributed systems (Cassandra’s bloom filter on SSTables — see Key-Value Stores), and network packet filtering.
Counting Bloom filters replace bits with small counters (typically 4 bits), enabling deletion at the cost of 4× space.
Authoritative source
Example problem.
Problem
You need a membership test for 100 million URLs (average 64 bytes) with at most 1% false positive rate and no false negatives. What is the minimum memory footprint?
Solution
A Bloom filter with and :
Total: ≈ 120 MB for 100M URLs, versus 6.4 GB for storing the full URLs. The 1% FPR means 1M false positives per 100M queries — acceptable for a web crawler’s “already seen” check.
Comparison §
| Structure | Random access | Search by value | Insert at pos i | Delete at pos i | Space overhead | Cache behavior |
|---|---|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) | None | Excellent (contiguous) |
| Vector | O(1) | O(n) / O(log n) sorted | O(n) | O(n) | ≤ c·n (capacity) | Excellent (contiguous) |
| Linked list | O(n) | O(n) | O(1) at known node | O(1) at known node | 8–16 B/node (pointers) | Poor (pointer chasing) |
| Stack | O(1) top only | O(n) | O(1) top | O(1) top | Depends on backing | Depends on backing |
| Queue | O(1) ends only | O(n) | O(1) tail | O(1) head | Depends on backing | Depends on backing |
| Deque | O(1) ends only | O(n) | O(1) either end | O(1) either end | Depends on backing | Depends on backing |
| Bloom filter | O(1) per bit | O(k) membership | O(k) | Not supported (counting: O(k)) | m bits (no element storage) | Good (bit array) |
Interactive simulation §
Quizz §
Example problem
Problem. You are building a text editor with an undo/redo system. The user can type (push), undo (pop), and redo (re-push). You also need to support “jump to line 500” (random access). Which combination of data structures would you use, and why?
Solution
Use a vector for the document text (random access to line 500 in , and amortized append for typing). Maintain a stack of undo operations (each entry records what to revert) and a stack of redo operations. Typing pushes onto the undo stack and clears the redo stack. Undo pops from the undo stack and pushes onto the redo stack. Redo does the reverse.
Why not a linked list for the document? Random access to line 500 would be , and the pointer overhead per character/word would be prohibitive. Why not a plain array? The document grows and shrinks; a vector handles that with amortized append.
The undo/redo stacks could be backed by the same vector (using a top index) or by a separate vector of operation records. The key insight: the access pattern (LIFO for undo/redo, random access for the document) dictates the structure choice.