Part III · Data Structures Chapter 10 Extra depth

Elementary Data Structures

Arrays, stacks, queues, linked lists, and rooted trees — the five shapes that everything else in the book is built out of.

Part III is where CLRS stops solving problems and starts building the containers that solve them. Chapter 10 is the foundation layer: contiguous storage and pointer-linked storage, and the four access disciplines you can impose on them. Nothing here is difficult. What matters is having the cost of every operation and the reason for that cost available without thinking, because chapters 11 through 13, every graph algorithm in Part VI, and most of the design decisions you make in real code are downstream of exactly these trade-offs.

4th edition note. Restructured. Section 10.1 is now “Simple array-based data structures” and covers arrays and matrices explicitly before stacks and queues — new material on memory layout that the 3rd edition assumed. The 3rd edition’s §10.4 on implementing pointers and objects in languages without them has been dropped. Linked lists and rooted trees are otherwise the same.

Contents

  1. Dynamic sets: the vocabulary
  2. Arrays
  3. Matrices and memory layout
  4. Stacks
  5. Queues
  6. Linked lists
  7. Sentinels
  8. Representing rooted trees
  9. The complete cost table
  10. Recap

Dynamic sets: the vocabulary

Part III opens by naming what all these structures are implementations of.

A dynamic set is a collection that can grow, shrink, and change over time. Elements are objects with a key field, usually drawn from a totally ordered set, plus satellite data that the structure carries but never inspects. Operations divide into queries, which return information, and modifying operations, which change the set.
OperationKindMeaning
SEARCH(S, k)QueryReturn a pointer to an element with key k, or NIL
INSERT(S, x)ModifyAdd the element pointed to by x
DELETE(S, x)ModifyRemove the element pointed to by x — note it takes a pointer, not a key
MINIMUM(S), MAXIMUM(S)QueryThe extreme element, for ordered sets
SUCCESSOR(S, x), PREDECESSOR(S, x)QueryThe next or previous element in sorted order
Why DELETE takes a pointer, not a key. This detail causes real confusion. CLRS defines deletion as taking a pointer to the element already located. If you only have a key, you must call SEARCH first and pay for that separately. The distinction matters enormously for linked lists, where deletion given a pointer is O(1) but finding the pointer is O(n). Quoting “linked list deletion is O(1)” without that qualification is the single most common data-structures error in interviews.

Arrays

An array is a block of contiguous memory holding n equal-sized elements. Its one superpower follows directly from that layout:

address of A[i] = base_address + (i - 1) · element_size // 1-indexed

One multiply and one add, independent of i and of n. That is why array access is Θ(1), and it is the only reason.

OperationCostWhy
Access A[i]Θ(1)Address arithmetic
Search unsortedΘ(n)Linear scan
Search sortedΘ(lg n)Binary search
Insert or delete at the endΘ(1)Nothing moves
Insert or delete in the middleΘ(n)Everything after it shifts
Dynamic arrays. A fixed array cannot grow. The standard fix — used by every vector, ArrayList, and Python list — is to allocate extra capacity and double it when full, copying everything to the new block. A single append can therefore cost Θ(n), but the doublings are rare enough that the average cost over a sequence of appends is O(1). That is amortized analysis, and Chapter 16 proves it. Doubling matters: growing by a fixed increment instead would make appends Θ(n) amortized.

Matrices and memory layout

New in the 4th edition, and worth having because it explains performance differences that the RAM model cannot.

Memory is one-dimensional. A two-dimensional matrix has to be flattened, and there are two conventions:

the matrix a b c d e f g h i flattened into linear memory row-major a b c d e f g h i C, Python, Go column-major a d g b e h c f i Fortran, MATLAB, R Row-major address: A[i][j] at base + ((i-1)·n + (j-1))·size for an m×n matrix. Consequence: in row-major storage, iterating for i { for j { A[i][j] } } walks memory sequentially and is fast; swapping the loops strides across memory and can be several times slower on the same asymptotic work.
Figure 10.1 — Row-major versus column-major. Both are Θ(1) access in the RAM model; on real hardware the loop order that matches the layout wins by a large constant.

CLRS also mentions blocked layouts, where the matrix is stored as a grid of small submatrices each laid out contiguously. That is what makes tuned matrix-multiplication libraries fast, and it is the practical reason Strassen from Chapter 4 struggles to compete.

Stacks

A stack implements a LIFO policy: last in, first out. The element removed is the one most recently inserted. Insert is called PUSH and delete is called POP, and neither takes an argument saying which element — the policy decides.

An array of size n plus one index S.top implements it exactly.

STACK-EMPTY(S) 1 if S.top == 0 2 return TRUE 3 else return FALSE PUSH(S, x) 1 if S.top == S.size 2 error "overflow" 3 S.top = S.top + 1 4 S[S.top] = x POP(S) 1 if STACK-EMPTY(S) 2 error "underflow" 3 S.top = S.top - 1 4 return S[S.top + 1]

All three are Θ(1). Note POP does not erase the slot — it just moves the index. The value is still physically there until overwritten.

Underflow is popping an empty stack, a program error. Overflow is pushing onto a full one, which a dynamic array avoids by reallocating.

Where stacks show up in this book: the recursion stack itself (every recursive algorithm is implicitly using one), iterative depth-first search in Chapter 20, and the parenthesis structure of DFS discovery and finish times. Outside it: expression evaluation, undo, backtracking, and the call stack your debugger prints.

Queues

A queue implements a FIFO policy: first in, first out. Insert is ENQUEUE and adds at the tail; delete is DEQUEUE and removes from the head.

The array implementation is subtler than the stack, because both ends move. If you never reused space, the queue would crawl off the end of the array. The fix is to wrap around, treating the array as circular.

15 6 9 8 4 1234 5678 9101112 Q.head = 4 Q.tail = 9 tail wraps from slot 12 back to slot 1 Empty when head == tail. Full when head == tail + 1 (mod n). Both conditions look identical unless you sacrifice one slot or keep a separate count — the classic off-by-one.
Figure 10.2 — A circular queue. Q.head points at the next element to leave; Q.tail at the next free slot.
ENQUEUE(Q, x) 1 if Q.size == Q.length // would overflow 2 error "overflow" 3 Q[Q.tail] = x 4 if Q.tail == Q.length 5 Q.tail = 1 // wrap around 6 else Q.tail = Q.tail + 1 7 Q.size = Q.size + 1 DEQUEUE(Q) 1 if Q.size == 0 2 error "underflow" 3 x = Q[Q.head] 4 if Q.head == Q.length 5 Q.head = 1 // wrap around 6 else Q.head = Q.head + 1 7 Q.size = Q.size - 1 8 return x

Both Θ(1). Keeping Q.size explicitly is the cleanest way to distinguish empty from full; the alternative is to leave one slot permanently unused.

Deques. A double-ended queue allows insertion and deletion at both ends, all in Θ(1). It generalises both the stack and the queue, and CLRS leaves it as an exercise.

Where queues show up: breadth-first search in Chapter 20 is defined by its queue — swap the queue for a stack and BFS becomes DFS. Also task scheduling, buffering, and every producer-consumer pipeline.

Linked lists

A linked list stores elements in objects arranged in a linear order determined by pointers rather than by memory addresses. Each node x has x.key, x.next, and in a doubly linked list x.prev. The list handle L has L.head, and L.head = NIL means the list is empty.
VariantProperty
Singly linkedOnly next. Half the pointer overhead, but you cannot walk backwards, which makes deletion given a pointer Θ(n).
Doubly linkedBoth next and prev. Deletion given a pointer is Θ(1).
SortedLinear order matches key order, so the minimum is the head. Insertion becomes Θ(n).
CircularThe last node’s next points at the head and the head’s prev at the last node. No NIL ends.
LIST-SEARCH(L, k) 1 x = L.head 2 while x ≠ NIL and x.key ≠ k 3 x = x.next 4 return x // Θ(n) worst case LIST-PREPEND(L, x) 1 x.next = L.head 2 x.prev = NIL 3 if L.head ≠ NIL 4 L.head.prev = x 5 L.head = x // Θ(1) LIST-DELETE(L, x) 1 if x.prev ≠ NIL 2 x.prev.next = x.next 3 else L.head = x.next 4 if x.next ≠ NIL 5 x.next.prev = x.prev // Θ(1) — given the pointer x

Count the NIL tests. Four of the twelve lines exist only to handle the two boundary cases, head and tail. That noise is what sentinels remove.

The Θ(1) deletion claim, precisely. LIST-DELETE(L, x) is Θ(1) because it is handed the pointer. Deleting by key means LIST-SEARCH first, so the real cost is Θ(n). And in a singly linked list even pointer deletion is Θ(n), because you must scan from the head to find the predecessor whose next needs rewriting.

Sentinels

A sentinel is a dummy object that lets you treat a boundary condition as an ordinary case. For a doubly linked list, add one node L.nil and make the list circular around it: L.nil.next is the head, L.nil.prev is the tail, and the real ends point back at L.nil instead of holding NIL.

L.nil 9 16 4 tail.next = L.nil, and L.nil.next = head — the ring closes With the sentinel, LIST-DELETE is two lines and LIST-INSERT is four — no NIL tests at all, because there is no longer any such thing as “the first” or “the last” node needing special care.
Figure 10.3 — A circular doubly linked list with sentinel. L.nil holds no key; it exists purely to eliminate boundary cases.
LIST-DELETE'(x) // with sentinel 1 x.prev.next = x.next 2 x.next.prev = x.prev LIST-INSERT'(L, x) // insert at the head 1 x.next = L.nil.next 2 L.nil.next.prev = x 3 L.nil.next = x 4 x.prev = L.nil

Compare with the versions above. Every NIL test is gone, because x.prev and x.next are never NIL — at worst they are the sentinel.

Sentinels do not improve asymptotics. They buy clarity with a constant amount of space, and clarity is where correctness comes from. The pattern recurs: the T.nil node in red-black trees (Chapter 13) is exactly the same idea and is what makes the rebalancing cases tractable to write down.
When not to use one. If you keep many short lists — hash table chains, adjacency lists for a sparse graph — one sentinel object per list can dominate the memory. CLRS says this explicitly. Use sentinels for a few long lists, not for thousands of tiny ones.

Representing rooted trees

Linked structures generalise beyond lines. The question is how many children a node may have.

Binary trees

Three pointers per node: x.p (parent), x.left, x.right. The tree handle T holds T.root, and T.root.p = NIL. A missing child is NIL. This is the representation used for binary search trees and red-black trees in the next chapters.

Unbounded branching: left-child, right-sibling

If a node can have any number of children you cannot pre-declare a fixed set of pointers, and one pointer per child would waste space when the count varies. The standard trick uses exactly two pointers per node regardless of the number of children:

the tree A B C D E F A has 3 children, B has 2 left-child, right-sibling A B C D E F left-child right-sibling 2 pointers per node, any branching factor
Figure 10.4 — The same tree, two representations. Children of a node form a linked list hanging off its left-child pointer.
The space cost is O(n) pointers for n nodes no matter how the branching varies. The price is that reaching the kth child takes k steps instead of one, and there is no random access among siblings. This representation is used for the disjoint-set forests of Chapter 19 and, in spirit, for adjacency lists in Chapter 20.

CLRS notes that other schemes exist and the right one depends on the application — for example, storing children in an array or a hash table per node when random access among siblings matters. Chapter 18’s B-trees do exactly that, with a fixed-capacity array of children per node.

The complete cost table

This is the table worth having memorised. Every entry is derivable from the layout, and knowing why beats knowing what.

OperationUnsorted arraySorted arraySingly linkedDoubly linkedStack / Queue
Access by indexΘ(1)Θ(1)Θ(n)Θ(n)
Search by keyΘ(n)Θ(lg n)Θ(n)Θ(n)
Insert at frontΘ(n)Θ(n)Θ(1)Θ(1)Θ(1)
Insert at endΘ(1)*Θ(n)Θ(n)Θ(n)Θ(1)
Delete given a pointerΘ(n)Θ(n)Θ(n)Θ(1)Θ(1)
MinimumΘ(n)Θ(1)Θ(n)Θ(n)
Successor of an elementΘ(n)Θ(1)Θ(1)Θ(1)
Memory overheadNoneNone1 pointer/node2 pointers/nodeSmall

* amortized, with a doubling dynamic array. † Θ(1) if you maintain a tail pointer. ‡ in list order, which equals key order only if the list is sorted.

The one sentence that summarises Part III’s opening. Contiguous storage buys you random access and pays with expensive structural change. Pointer-linked storage buys you cheap structural change and pays with no random access and worse cache behaviour. Every structure in the next three chapters is an attempt to get some of both — hash tables by computing the index instead of searching for it, search trees by making the structure itself carry the order.
What the RAM model hides here. The table says array search and linked-list search are both Θ(n). On real hardware the array wins by a large factor, because a linear scan of contiguous memory is prefetched perfectly while a pointer chase stalls on cache misses. For small n a linear scan of an array frequently beats a “better” structure outright. Measure before believing the table.

Recap

The nine things to carry forward

Where this goes next

Both structures so far force a choice between fast lookup and fast modification. Chapter 11 refuses the choice: a hash table computes an array index directly from the key, giving Θ(1) expected search, insert, and delete. The cost is that all ordering information is destroyed — no minimum, no successor, no range queries. Chapter 12 then takes the opposite route with binary search trees, which keep order and accept O(h) per operation, and Chapter 13 makes h provably O(lg n).


Ch 9 — Medians and Order Statistics Ch 11 — Hash Tables