DSA DSA In Depth
0%
lesson augmented-bstorder-statisticsinterval-tree

Augmenting BSTs: Order-Statistic Trees (select/rank) & Interval Trees

7 min read Module 3.2: Self-Balancing, Augmented, Disk-Backed & Persistent Trees Updated Oct 23, 2026
📄 Cheat Sheet
Interval Search Trees & Augmented BST Invariants (Robert Sedgewick)
Osiris Salazar (Princeton Algorithms) Interval Search Trees & Augmented BST Invariants (Robert Sedgewick) Watch on YouTube ↗

Robert Sedgewick explains how augmenting each BST node with the maximum right endpoint in its subtree enables O(log N) interval overlap queries.

0:00 / --:--

The CLRS Augmentation Theorem & Two Canonical Augmented Trees

Why do rotations preserve local subtree summaries in $O(1)$ time? Because a rotation (rotateLeft or rotateRight) on edge $(x, y)$ only changes the descendant sets of two nodes: $x$ and $y$! The entire subtree rooted at the new top node contains the exact same set of nodes as before the rotation, so no ancestor above the rotated pair needs its associative summary recomputed from scratch (though ancestor updates along the $O(\log N)$ insertion/deletion path take $O(\log N)$ total time anyway).

1. Order-Statistic Tree (size[u] Augmentation)

Augment each node $u$ with its subtree size $\text{size}(u) = 1 + \text{size}(u.\text{left}) + \text{size}(u.\text{right})$ (where $\text{size}(\text{null}) = 0$). Then:

  • select(u, k) (0-indexed $k$-th smallest key): Let $L = \text{size}(u.\text{left})$ be the number of nodes in $u$'s left subtree. If $k == L$, return $u$; if $k < L$, recurse into select(u.left, k); if $k > L$, subtract the $L + 1$ skipped elements and recurse into select(u.right, k - L - 1)!
  • rank(root, key) (count of keys $< \text{key}$): Walk down from the root; every time we branch right (because $u.\text{key} < \text{key}$), add $\text{size}(u.\text{left}) + 1$ to our running accumulator.

2. Interval Tree (maxEnd[u] Augmentation for 1D Overlap Search)

Suppose we store closed intervals $I_i = [lo_i, hi_i]$ ordered primarily by their low endpoint $lo_i$ as the BST key, and augment each node $u$ with the maximum high endpoint anywhere in $u$'s subtree:

$$\text{maxEnd}(u) = \max\big(u.hi, \; \text{maxEnd}(u.\text{left}), \; \text{maxEnd}(u.\text{right})\big)$$

To find any interval in the tree that overlaps query $Q = [q_{lo}, q_{hi}]$ (recall two closed intervals overlap iff $u.lo \le q_{hi}$ AND $q_{lo} \le u.hi$), we inspect a **single root-to-leaf path** in $O(\log N)$ time:

  1. If $u$ overlaps $Q$, return $u$.
  2. If $u.\text{left} \ne \text{null}$ and $\text{maxEnd}(u.\text{left}) \ge q_{lo}$, go left (u = u.left).
  3. Otherwise, go right (u = u.right).
Augmented Structure & Operation Stored Node Field $f(u)$ Local Maintenance $g(\cdot)$ Query / Update Time
Order-Statistic select(k) / rank(x) size (subtree count) $1 + \text{size}(L) + \text{size}(R)$ O(log N)
Interval Tree findAnyOverlap(Q) maxEnd (subtree $\max hi$) $\max(u.hi, \text{maxEnd}(L), \text{maxEnd}(R))$ O(log N)
Subtree Affine / Hash / Monoid Tree agg (associative monoid) $\text{agg}(L) \oplus \text{val}(u) \oplus \text{agg}(R)$ O(log N)

Correctness Proof: Why Interval Tree Search Never Needs to Backtrack When Going Left!

Students always ask: "In Step 2, if $\text{maxEnd}(u.\text{left}) \ge q_{lo}$ and we go left, what if the interval in $u.\text{left}$ that achieved $\text{maxEnd} \ge q_{lo}$ has its low endpoint $> q_{hi}$, so the left subtree has NO overlap while the right subtree DOES have an overlap?"

Proof that this is impossible: Let $I^* = [lo^*, hi^*] \in u.\text{left}$ be the interval in $u$'s left subtree that achieves $hi^* = \text{maxEnd}(u.\text{left}) \ge q_{lo}$.

  • Case A: If $lo^* \le q_{hi}$, then since we already know $hi^* \ge q_{lo}$, $I^*$ itself overlaps $Q = [q_{lo}, q_{hi}]$! So an overlapping interval genuinely exists in $u.\text{left}$.
  • Case B: Suppose $lo^* > q_{hi}$. Because $I^*$ lies in the left subtree of $u$ and the BST is ordered by low endpoints, every interval $J = [lo_J, hi_J]$ in $u$'s right subtree satisfies: $$lo_J \ge u.lo \ge lo^* > q_{hi}!$$ Thus no interval in $u.\text{right}$ can possibly overlap $Q$ either! Going left when $\text{maxEnd}(u.\text{left}) \ge q_{lo}$ is 100% safe and never misses a valid match in $u.\text{right}$.
Augmented Order-Statistic (rank/select) & Interval Overlap Tree
O(log N) Augmented Descent · O(1) Rotation Fixup

Explore the Augmentation Theorem: maintain subtree maxEnd = max(hi, L.maxEnd, R.maxEnd) for O(log N) interval overlap search and Linux VMA gap allocation, or subtree size sz for O(log N) select(k) / rank(x).

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Deterministic Single-Path Theorem: If u.left.maxEnd ≥ q.lo (23 ≥ 14), we are guaranteed to either find an overlap in u.left OR prove no overlap can exist in u.right!
[17,19] max=30 [5,8] L.max=23 ≥ 14 [21,24] max=30 Query [14, 16]: Root [17,19] misses, but u.left.maxEnd (23) ≥ 14 ⟹ Go Left ONLY!
Search Step
searchOverlap([14, 16]) at Root
Pruning Predicate
u.left.maxEnd (23) ≥ q.lo (14)
Guarantee
Hit [15, 23] inside Left Subtree in O(log N)
[Step 1] Deterministic Single-Path Theorem: If u.left.maxEnd ≥ q.lo (23 ≥ 14), we are guaranteed to either find an overlap in u.left OR prove no overlap can exist in u.right!
Interval Tree [lo, hi] | maxEnd (BST keyed by lo): [16,21] max=30 [8,9] max=23 [25,30] max=30 [5,8] m=8 [15,23] m=23 Query Q = [22, 24] — Single-Path O(log N) Trace: 1. At root [16,21]: no overlap (21 < 22). Check left.maxEnd = 23 ≥ q_lo (22) ⇒ GO LEFT! 2. At [8,9]: no overlap (9 < 22). Check left.maxEnd = 8 < 22 ⇒ GO RIGHT! 3. At [15,23]: 15 ≤ 24 AND 22 ≤ 23 ⇒ MATCH FOUND!
flowchart TD Start["searchOverlap(u, qLo, qHi)"] --> NullCheck{"u == null ?"} NullCheck -- "Yes" --> None["return null"] NullCheck -- "No" --> Overlap{"u.lo <= qHi AND qLo <= u.hi ?"} Overlap -- "Yes" --> Hit["return u"] Overlap -- "No" --> LeftMax{"u.left != null AND u.left.maxEnd >= qLo ?"} LeftMax -- "Yes (Guaranteed Safe)" --> GoL["return searchOverlap(u.left, qLo, qHi)"] LeftMax -- "No (Left cannot overlap)" --> GoR["return searchOverlap(u.right, qLo, qHi)"]

Hardware Reality: Linux Kernel VMAs & GCC Policy-Based Data Structures

Where do Augmented BSTs appear in production? First, the Linux kernel's Virtual Memory Area manager (mm/mmap.c) uses an augmented Red-Black Interval Tree (rb_subtree_gap) to locate free virtual address holes for mmap() in $O(\log N)$ time. Second, in C++ competitive and systems programming, GNU libstdc++ provides __gnu_pbds::tree with tree_order_statistics_node_update, giving a drop-in Red-Black Order-Statistic Tree supporting find_by_order(k) (select) and order_of_key(x) (rank) in $O(\log N)$ time.

Implementation: Interval Tree Overlap Search & Order-Statistic Select/Rank

// C++20: Augmented BST Primitives — Interval Overlap Search + Order-Statistic Select & Rank
#include <algorithm>
#include <cstdint>

struct AugmentedNode {
    int64_t lo, hi;      // Interval [lo, hi]; 'lo' acts as the primary BST key
    int64_t maxEnd;      // Max 'hi' in subtree
    int32_t sz = 1;      // Subtree node count for Order-Statistic rank/select
    AugmentedNode *left = nullptr, *right = nullptr;

    AugmentedNode(int64_t l, int64_t h) : lo(l), hi(h), maxEnd(h) {}
};

// O(1) Local Pull called after every child modification or rotation
inline void pullAugmented(AugmentedNode* u) noexcept {
    if (!u) return;
    u->sz = 1 + (u->left ? u->left->sz : 0) + (u->right ? u->right->sz : 0);
    int64_t m = u->hi;
    if (u->left)  m = std::max(m, u->left->maxEnd);
    if (u->right) m = std::max(m, u->right->maxEnd);
    u->maxEnd = m;
}

// 1. O(log N) Interval Overlap Search (Single root-to-leaf walk, zero backtracking!)
const AugmentedNode* findOverlappingInterval(const AugmentedNode* u, int64_t qLo, int64_t qHi) noexcept {
    while (u != nullptr) {
        if (u->lo <= qHi && qLo <= u->hi) return u; // Overlap found!
        if (u->left != nullptr && u->left->maxEnd >= qLo) {
            u = u->left;
        } else {
            u = u->right;
        }
    }
    return nullptr;
}

// 2. O(log N) Order-Statistic Select (0-indexed k-th smallest by 'lo')
const AugmentedNode* selectKth(const AugmentedNode* u, int32_t k) noexcept {
    while (u != nullptr) {
        int32_t leftSz = u->left ? u->left->sz : 0;
        if (k == leftSz) return u;
        if (k < leftSz) {
            u = u->left;
        } else {
            k -= (leftSz + 1);
            u = u->right;
        }
    }
    return nullptr;
}

Failure Modes & Adversarial Pitfalls

  • Augmenting with Global Rank Instead of Local Subtree Size Breaks $O(\log N)$ Insertion: If you store each node's global sorted index globalRank[u] inside the node, inserting a new minimum element increments the global rank of all $N$ nodes, forcing $\Theta(N)$ updates! By storing only subtree-local size[u], inserting a node only increments size on its $O(\log N)$ ancestors.
  • Forgetting to Call pullAugmented Inside Rotations: Any rotation rotateLeft(x) or rotateRight(x) changes the children of both x and y. Omitting pullAugmented(x); pullAugmented(y); inside the rotation helper leaves stale size and maxEnd values that cause silent out-of-bounds select or missed interval overlaps.
Check yourself Question 1 of 4
Concept Check The Augmentation Theorem for Balanced BSTs recall

According to the CLRS Augmentation Theorem, when can an auxiliary field $f(x)$ stored at every node $x$ of a balanced BST be maintained in $O(\log N)$ time during insertions, deletions, and rotations without changing the asymptotic complexity?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why does storing `u.globalRank` violate the Augmentation Theorem and degrade insertion/deletion to $\Omega(N)$?

Key Takeaways

By the CLRS Augmentation Theorem, any subtree property f(u) = g(u.val, f(u.left), f(u.right)) computable in O(1) from children can be maintained across balanced BST updates and rotations in O(log N) time.
Order-Statistic Trees augment nodes with subtree size sz(u) = 1 + sz(u.left) + sz(u.right) (never global rank!) to answer select(k) and rank(key) in O(log N) time.
Interval Trees order intervals by low endpoint lo and augment each node with maxEnd(u) = max(u.hi, maxEnd(L), maxEnd(R)).
Searching for any overlapping interval follows a single O(log N) path: if maxEnd(u.left) >= q_lo, going left is guaranteed safe because if the left witness lo* > q_hi, then every interval in u.right has lo >= lo* > q_hi and cannot overlap either.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue