Technopedia

The technologist’s encyclopedia

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 O(1)O(1) 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.

Random-access speed favors arrays Insertion / deletion favors linked lists Space overhead favors arrays

Authoritative sources

Arrays

An array of nn elements in contiguous memory supports:

Details

How it works. An array is a block of n×wn \times w bytes (where ww is the element width) allocated contiguously. Element ii lives at address base+i⋅w\text{base} + i \cdot w, so the CPU computes the address with a single multiply-and-add. No pointer chasing is needed.

0 4 8 12 15 0x1000 0x103C one 64-byte cache line = 16 × int32, loaded together

Worked example. Consider an array of 8 int32 values (4 bytes each) starting at address 0x1000:

Index01234567
Address0x10000x10040x10080x100C0x10100x10140x10180x101C
Value4279915538812

Accessing index 5: address = 0x1000 + 5 × 4 = 0x1014 → value 3. One memory access, O(1)O(1).

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 O(n)O(n) 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). O(1)O(1) access, O(∣K∣)O(|K|) 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 O(1)O(1) append by growing capacity geometrically. Over nn insertions, the underlying array is reallocated O(log⁡n)O(\log n) times; total work is O(n)O(n) (amortized O(1)O(1) 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 c×capacityc \times \text{capacity} bytes, copies all elements, and frees the old block. The amortized cost per insertion is 11−1/c\frac{1}{1 - 1/c} element-copies.

Growth factor. Any constant factor c>1c > 1 gives O(1)O(1) amortized per insertion. For example, c=1.5c = 1.5 gives amortized cost 3 per insertion; c=2c = 2 gives amortized cost 2. The factor c=2c = 2 minimizes the number of reallocations (log⁡2n\log_2 n), while smaller factors reduce peak memory waste. Peak memory usage is at most c⋅nc \cdot n elements (just before a shrink, if supported).

Worked example. Start with capacity 1, growth factor 2:

Insertion #SizeCapacityReallocation?Copies
111No0
222Yes (1→2)1
334Yes (2→4)2
444No0
558Yes (4→8)4
668No0
778No0
888No0
9916Yes (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 ∼12.5%\sim 12.5\% 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 O(log⁡n)O(\log n) 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 ≈ n−1n - 1 for n=1024n = 1024 (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.

Details

How it works. Each node is a separate heap allocation containing the element and one or two pointers. To insert after node pp, allocate a new node, set its next to p.nextp.\text{next}, then set p.nextp.\text{next} to the new node. No existing elements move.

3 7 99 next next prev prev head NULL

Worked example. Insert 42 after the node holding 7 in the list 3 → 7 → 99:

  1. Allocate new node: new = Node(42, next = NULL)
  2. new.next = 7.next → new.next points to node(99)
  3. 7.next = new → node(7).next now points to node(42)

Result: 3 → 7 → 42 → 99. Three pointer writes, O(1)O(1).

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 O(1)O(1) splice at the cost of spatial locality. They are preferred when:

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: O(n)O(n) — you must walk from the head to position 499 to get the predecessor, then relink. Cost ≈ 500 pointer dereferences.

Doubly: still O(n)O(n) to find position 500 (no random access), but once you have the node, deletion is O(1)O(1) (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 O(1)O(1) for every operation regardless of the underlying representation.

StructureAccess patternPush / EnqueuePop / DequeuePeek
StackLIFO (last-in, first-out)O(1)O(1)O(1)
QueueFIFO (first-in, first-out)O(1)O(1)O(1)
DequeBoth endsO(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:

StepOperationStack (top → bottom)
1push 3[3]
2push 7[7, 3]
3push 1[1, 7, 3]
4pop → 1[7, 3]
5push 9[9, 7, 3]
6pop → 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 O(1)O(1)). Typical uses: BFS traversal (see Graphs), task scheduling, I/O buffering, message passing.

Worked example (queue, circular buffer of size 4).

StepOperationBuffer [0..3]headtail
1enqueue 10[10, _, _, _]01
2enqueue 20[10, 20, _, _]02
3dequeue → 10[_, 20, _, _]12
4enqueue 30[_, 20, 30, _]13
5dequeue → 20[_, _, 30, _]23
6enqueue 40[_, _, 30, 40]20 (wrap)

Deque (double-ended queue). Supports push/pop at both ends in O(1)O(1). 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 O(1)O(1).

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

Details

How it works. Each element is hashed kk times, producing kk bit positions in the array. Insertion sets those bits. A query checks whether all kk positions are set. As more elements are inserted, bits collide and the false-positive rate rises.

A B C 0 2 4 7 9 11 13 15

Worked example. Bit array of m=16m = 16 bits, k=3k = 3 hash functions. Insert elements A, B, C:

Elementh₁h₂h₃Bits set
A2711positions 2, 7, 11
B4713positions 4, 7, 13
C2913positions 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 nn elements and target FPR ε\varepsilon:

m=−nln⁡ε(ln⁡2)2≈1.44 nlog⁡2(1/ε)m = \frac{-n \ln \varepsilon}{(\ln 2)^2} \approx 1.44 \, n \log_2(1/\varepsilon) k=mnln⁡2k = \frac{m}{n} \ln 2

For 10M elements at 1% FPR: m≈9.6m \approx 9.6 bits per element, total ≈96\approx 96 million bits ≈12\approx 12 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 n=108n = 10^8 and ε=0.01\varepsilon = 0.01:

m=−nln⁡ε(ln⁡2)2=−108×(−4.605)0.480≈9.59×108 bits≈120 MBm = \frac{-n \ln \varepsilon}{(\ln 2)^2} = \frac{-10^8 \times (-4.605)}{0.480} \approx 9.59 \times 10^8 \text{ bits} \approx 120 \text{ MB} k=mnln⁡2≈9.59×0.693≈6.6  ⇒  k=7k = \frac{m}{n} \ln 2 \approx 9.59 \times 0.693 \approx 6.6 \;\Rightarrow\; k = 7

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

StructureRandom accessSearch by valueInsert at pos iDelete at pos iSpace overheadCache behavior
ArrayO(1)O(n)O(n)O(n)NoneExcellent (contiguous)
VectorO(1)O(n) / O(log n) sortedO(n)O(n)≤ c·n (capacity)Excellent (contiguous)
Linked listO(n)O(n)O(1) at known nodeO(1) at known node8–16 B/node (pointers)Poor (pointer chasing)
StackO(1) top onlyO(n)O(1) topO(1) topDepends on backingDepends on backing
QueueO(1) ends onlyO(n)O(1) tailO(1) headDepends on backingDepends on backing
DequeO(1) ends onlyO(n)O(1) either endO(1) either endDepends on backingDepends on backing
Bloom filterO(1) per bitO(k) membershipO(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 O(1)O(1), and O(1)O(1) 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 O(n)O(n), 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 O(1)O(1) 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.