Architectural Synthesis: LLRB Rebalancing + Subtree Size Invariant
Let us trace how sz (subtree node count) interacts with the three LLRB primitive transformations:
rotateLeft(h)(promotes $x = h.\text{right}$ above $h$): Before rotation, $h$ was the root of the local subtree; after rotation, $x$ is the root of the exact same set of nodes! Therefore, we setx->sz = h->szand recomputeh->sz = 1 + size(h->left) + size(h->right)in $O(1)$ time.rotateRight(h)(promotes $x = h.\text{left}$ above $h$): Symmetric—setx->sz = h->szand recomputeh->sz = 1 + size(h->left) + size(h->right).flipColors(h): Only flips boolean color bits without changing any parent-child links, so allszfields remain untouched!
With sz maintained at every node, we unlock three $O(\log N)$ analytical queries:
rank(key): Returns the exact number of elements in the set strictly less thankey($|\{x \in S : x < \text{key}\}|$).select(k)($0 \le k < N$): Returns the unique key $x \in S$ whoserank(x) == k.rangeCount(lo, hi)($lo \le hi$): Returns the number of keys in the closed interval $[lo, hi]$ in $O(\log N)$ time viarank(hi) - rank(lo) + (contains(hi) ? 1 : 0)!
| Order-Statistic Method | LLRB Order-Statistic Tree | Unaugmented std::set / TreeMap |
Sorted std::vector |
|---|---|---|---|
insert(key) / erase(key) |
O(log N) | O(log N) | O(N) Shift |
rank(key) (Count $< \text{key}$) |
O(log N) | O(N) std::distance |
O(log N) lower_bound |
select(k) ($k$-th Smallest) |
O(log N) | O(N) std::advance |
O(1) vec[k] |
rangeCount(lo, hi) |
O(log N) | O(N) | O(log N) |
Exact Dual Identity Between rank and select
For any Order-Statistic Tree $T$ storing $N$ distinct keys:
$$\forall k \in \{0, 1, \dots, N-1\}: \quad \text{rank}(\text{select}(k)) == k$$
$$\forall x \in T: \quad \text{select}(\text{rank}(x)) == x$$
Furthermore, for any query key $q$ (even if $q \notin T$), $\text{rank}(q)$ satisfies the recursive invariant at node $u$:
$$\text{rank}(u, q) = \begin{cases} 0 & \text{if } u == \text{null} \\ \text{rank}(u.\text{left}, q) & \text{if } q < u.\text{key} \\ \text{size}(u.\text{left}) & \text{if } q == u.\text{key} \\ 1 + \text{size}(u.\text{left}) + \text{rank}(u.\text{right}, q) & \text{if } q > u.\text{key} \end{cases}$$
Trace how AVL LL/LR/RR/RL rotations and Sedgewick Left-Leaning Red-Black (LLRB) local transformations (rotateLeft, rotateRight, flipColors) preserve in-order sequence and subtree sizes while bounding tree height.
Invariant & State
Hardware Reality: Iterative rank and select Without Recursion
While LLRB insert uses bottom-up recursion along the $O(\log N)$ path so rotations can unwind cleanly, rank(key), select(k), and contains(key) are read-only downward walks! Writing rank, select, and contains as tight while (u != nullptr) loops avoids all stack-frame push/pop overhead and executes in $\le 2 \log_2 N$ branch-predicted pointer hops.
Production Reference Implementation: Complete LLRB Order-Statistic Tree
// C++20: Complete Left-Leaning Red-Black (LLRB) Order-Statistic Tree
#include <cstdint>
#include <cassert>
template <typename Key>
class LLRBOrderStatisticTree {
struct Node {
Key key;
int32_t sz = 1;
bool red = true;
Node *left = nullptr, *right = nullptr;
explicit Node(Key k) : key(k) {}
};
Node* root = nullptr;
static bool isRed(const Node* u) noexcept { return u != nullptr && u->red; }
static int32_t size(const Node* u) noexcept { return u ? u->sz : 0; }
static void pull(Node* u) noexcept { u->sz = 1 + size(u->left) + size(u->right); }
static Node* rotateLeft(Node* h) noexcept {
Node* x = h->right;
h->right = x->left;
x->left = h;
x->red = h->red;
h->red = true;
x->sz = h->sz; // x replaces h as root of this exact subtree
pull(h); // Recompute demoted node h
return x;
}
static Node* rotateRight(Node* h) noexcept {
Node* x = h->left;
h->left = x->right;
x->right = h;
x->red = h->red;
h->red = true;
x->sz = h->sz;
pull(h);
return x;
}
static void flipColors(Node* h) noexcept {
h->red = !h->red;
h->left->red = !h->left->red;
h->right->red = !h->right->red;
}
static Node* insertRec(Node* h, const Key& key) {
if (!h) return new Node(key);
if (key < h->key) h->left = insertRec(h->left, key);
else if (h->key < key) h->right = insertRec(h->right, key);
if (isRed(h->right) && !isRed(h->left)) h = rotateLeft(h);
if (isRed(h->left) && isRed(h->left->left)) h = rotateRight(h);
if (isRed(h->left) && isRed(h->right)) flipColors(h);
pull(h);
return h;
}
public:
int32_t size() const noexcept { return size(root); }
void insert(const Key& key) {
root = insertRec(root, key);
root->red = false; // Root is always Black
}
bool contains(const Key& key) const noexcept {
const Node* u = root;
while (u) {
if (key < u->key) u = u->left;
else if (u->key < key) u = u->right;
else return true;
}
return false;
}
// Number of keys strictly less than 'key' in O(log N) time
int32_t rank(const Key& key) const noexcept {
const Node* u = root;
int32_t count = 0;
while (u) {
if (key < u->key) {
u = u->left;
} else if (u->key < key) {
count += 1 + size(u->left);
u = u->right;
} else {
return count + size(u->left);
}
}
return count;
}
// 0-indexed k-th smallest key in O(log N) time
const Key& select(int32_t k) const noexcept {
assert(k >= 0 && k < size(root));
const Node* u = root;
while (u) {
int32_t leftSz = size(u->left);
if (k == leftSz) return u->key;
if (k < leftSz) {
u = u->left;
} else {
k -= (leftSz + 1);
u = u->right;
}
}
return root->key;
}
// Count of keys in closed interval [lo, hi] in O(log N) time
int32_t rangeCount(const Key& lo, const Key& hi) const noexcept {
if (hi < lo) return 0;
return rank(hi) - rank(lo) + (contains(hi) ? 1 : 0);
}
};
Failure Modes & Adversarial Pitfalls
- Unconditionally Incrementing
h->sz++on Descent When Inserting a Duplicate Key: Ifinsert(key)is called with akeythat already exists in the set, no new leaf is created! Incrementingszon the way down corrupts every ancestor's size whenever a duplicate is inserted. Always recomputeh->sz = 1 + size(h->left) + size(h->right)bottom-up on the way back up (pull(h)). - Off-By-One in
rangeCount(lo, hi): Becauserank(key)counts elements strictly less thankey,rank(hi) - rank(lo)counts keys in the half-open interval $[lo, hi)$. To count the closed interval $[lo, hi]$, you must add $+1$ ifcontains(hi)is true (or computerank(hi_successor) - rank(lo)).