Part V · Advanced Data Structures Chapter 17 Extra depth

Augmenting Data Structures

Store one extra field per node in a red-black tree and get a whole new operation, for free — provided the field can be recomputed from a node’s children.

You rarely invent a data structure from scratch. Far more often you take a textbook structure and hang extra information on it to support an operation it did not originally have. Chapter 17 makes that into a method: a four-step procedure, plus a theorem that tells you when the augmentation is asymptotically free. Two worked examples — order-statistic trees and interval trees — both start from the red-black tree of Chapter 13 and both keep all operations at O(lg n).

4th edition note. Numbering shifted from 14 to 17, and the chapter moved into Part V. Content is unchanged.

Contents

  1. Order-statistic trees
  2. OS-SELECT and OS-RANK
  3. Maintaining the size field
  4. The four-step method
  5. Theorem 17.1
  6. Interval trees
  7. Recap

Order-statistic trees

Chapter 9 found the ith smallest element of an unsorted array in O(n). Suppose instead you maintain a dynamic set and want the ith smallest at any moment, plus the reverse question: given an element, what is its rank?

An order-statistic tree is a red-black tree with one extra attribute per node: x.size, the number of internal nodes in the subtree rooted at x, including x itself. Define T.nil.size = 0, which gives the identity
x.size = x.left.size + x.right.size + 1
26 17 41 14 21 47 size 6size 3size 2 111 6 = 3 + 2 + 1 — the identity holds at every node
Figure 17.1 — An order-statistic tree. The size field is the only addition; keys and colours are exactly as in Chapter 13.

OS-SELECT and OS-RANK

OS-SELECT(x, i) // the ith smallest in the subtree at x 1 r = x.left.size + 1 // rank of x within its own subtree 2 if i == r 3 return x 4 elseif i < r 5 return OS-SELECT(x.left, i) 6 else 7 return OS-SELECT(x.right, i - r) // discount what is on the left

O(lg n). The shape is binary search, but on rank rather than on key. Line 7 subtracts r because the right subtree’s local ranks start over at 1.

OS-RANK(T, x) // position of x in the sorted order 1 r = x.left.size + 1 2 y = x 3 while y ≠ T.root 4 if y == y.p.right // y is a right child 5 r = r + y.p.left.size + 1 // count the parent and its left subtree 6 y = y.p 7 return r

O(lg n). Walk up to the root, and every time you come up from a right child, add that parent plus everything in its left subtree, since all of those precede x.

Maintaining the size field

The augmentation is only useful if size can be kept correct without slowing insertion and deletion down.

After LEFT-ROTATE(T, x), where y was x.right: y.size = x.size // y took x's whole subtree x.size = x.left.size + x.right.size + 1 // recompute from children

Two assignments per rotation, and insertion does at most 2 rotations and deletion at most 3. So the augmentation adds O(1) to each and everything stays O(lg n).

This is the whole trick. size is computable from a node’s own children, so a local change requires only a local repair. Any field with that property is cheap to maintain; any field that depends on distant parts of the tree is not.

The four-step method

1. Choose an underlying data structure. 2. Determine additional information to maintain in it. 3. Verify that the additional information can be maintained by the basic modifying operations. 4. Develop new operations that use the information.

CLRS stresses these are a guide, not a rigid order — in practice you iterate, and step 4 often reveals that step 2 chose the wrong field. Step 3 is where designs die.

Theorem 17.1

Theorem 17.1. Let f be an attribute that augments a red-black tree of n nodes, and suppose that for any node x, the value of f(x) can be computed using only the information in x, x.left, and x.right — including x.left.f and x.right.f. Then we can maintain the values of f in all nodes during insertion and deletion without asymptotically affecting the O(lg n) performance of those operations.

The reason is a two-part argument you should be able to reconstruct. A change to x.f propagates only to x’s ancestors, and there are O(lg n) of them on a red-black tree. And a rotation touches only two nodes, whose f values are recomputed locally in O(1); since there are O(1) rotations per update, the total extra work is O(lg n).

Candidate fieldComputable from children?Cheap to maintain?
Subtree sizeYes — sum of children plus 1Yes
Subtree min / max keyYesYes
Subtree sum of valuesYesYes
Black-heightYes — from a child’s black-height and colourYes
Depth of a nodeNo — depends on ancestors, not descendantsNo. One rotation near the root changes the depth of Θ(n) nodes.
That last row is the counterexample to keep. Depth looks as innocent as size, but it flows down the tree rather than up, so a single rotation invalidates an entire subtree’s worth of values. Theorem 17.1 does not apply, and there is no cheap fix.

Interval trees

The second example, and a more interesting one because the augmented field is used in a non-obvious way.

Each node now represents a closed interval i = [i.low, i.high]. The tree is keyed on i.low. The augmentation is x.max: the maximum high endpoint of any interval in the subtree rooted at x.

x.max = max( x.int.high, x.left.max, x.right.max )

Computable from children, so Theorem 17.1 applies and max is free to maintain.

The new operation: INTERVAL-SEARCH(T, i) returns any interval in the tree that overlaps i, or T.nil.

INTERVAL-SEARCH(T, i) 1 x = T.root 2 while x ≠ T.nil and i does not overlap x.int 3 if x.left ≠ T.nil and x.left.max ≥ i.low 4 x = x.left 5 else x = x.right 6 return x

O(lg n) — a single root-to-leaf descent with no backtracking.

Why one descent suffices. The loop never has to try both children, and the justification is a small theorem. If the search goes right (because x.left.max < i.low), then every interval in the left subtree ends before i begins, so no overlap can exist there. If it goes left, then some interval in the left subtree has high ≥ i.low; CLRS proves that if no overlap exists in the left subtree, none exists in the right either — so going left is safe. Either way one child can be discarded outright.

Two intervals i and j overlap exactly when i.low ≤ j.high and j.low ≤ i.high. The interval trichotomy the chapter states is that exactly one of three holds: they overlap, i is entirely left of j, or i is entirely right of j.

StructureBaseAugmentationNew operationCost
Order-statistic treeRed-black treesizeOS-SELECT, OS-RANKO(lg n)
Interval treeRed-black treemax endpointINTERVAL-SEARCHO(lg n)
Where this shows up. Order-statistic trees answer “what is my rank on the leaderboard?” and “who is in 500th place?” in O(lg n), and they compute the number of inversions in a sequence. Interval trees back calendar conflict detection, IP range and firewall rule matching, and genome annotation lookups. Both are one field away from a structure you already have.

Recap

The seven things to carry forward

Where this goes next

Chapter 18 changes the assumption underneath every tree so far. B-trees are balanced search trees designed for data that lives on disk, where a single access costs far more than any amount of computation. The answer is nodes with hundreds of children, so the tree is only a few levels deep and a search costs a handful of disk reads.


Ch 16 — Amortized Analysis Ch 18 — B-Trees