Self-Balancing BSTs: AVL & Red-Black
AVL ও Red-Black Tree
1. Why Balance? The Naive BST Problem
Insert 1, 2, 3, 4, 5 into a plain BST in order. The result is a right-leaning chain of
height 5. Search becomes O(n), not O(log n). The fix: after every insertion (or deletion),
restore balance by performing rotations.
2. AVL Trees — Strict Balance
Invariant: for every node, |height(left) − height(right)| ≤ 1. After each insert, walk up the path; whenever the balance factor exceeds ±1, perform one of four rotations: LL, RR, LR, RL. Trees stay tighter than Red-Black, at the cost of more rotations.
#include <bits/stdc++.h>
using namespace std;
struct Node {
int key, h;
Node *l, *r;
Node(int k) : key(k), h(1), l(nullptr), r(nullptr) {}
};
int H(Node* n) { return n ? n->h : 0; }
int bf(Node* n) { return n ? H(n->l) - H(n->r) : 0; }
void upd(Node* n) { n->h = 1 + max(H(n->l), H(n->r)); }
Node* rotR(Node* y) {
Node* x = y->l; y->l = x->r; x->r = y;
upd(y); upd(x); return x;
}
Node* rotL(Node* x) {
Node* y = x->r; x->r = y->l; y->l = x;
upd(x); upd(y); return y;
}
Node* insert(Node* n, int k) {
if (!n) return new Node(k);
if (k < n->key) n->l = insert(n->l, k);
else if (k > n->key) n->r = insert(n->r, k);
else return n;
upd(n);
int b = bf(n);
if (b > 1 && k < n->l->key) return rotR(n); // LL
if (b < -1 && k > n->r->key) return rotL(n); // RR
if (b > 1 && k > n->l->key) { n->l = rotL(n->l); return rotR(n); } // LR
if (b < -1 && k < n->r->key) { n->r = rotR(n->r); return rotL(n); } // RL
return n;
}
void inorder(Node* n) {
if (!n) return;
inorder(n->l);
cout << n->key << "(h=" << n->h << ") ";
inorder(n->r);
}
int main() {
Node* root = nullptr;
for (int x : {10, 20, 30, 40, 50, 25}) root = insert(root, x);
inorder(root);
cout << "\nroot height = " << root->h;
}
Without rotations the tree height after these 6 inserts would be 5. With AVL it stays at 3.
3. Red-Black Trees — Looser Balance, Fewer Rotations
Each node is RED or BLACK. Five invariants:
- Every node is red or black.
- The root is black.
- All NIL leaves are black.
- A red node's children are both black (no two reds in a row).
- Every root-to-leaf path has the same number of black nodes (the black-height).
These together force height ≤ 2·log₂(n+1). Insert/delete need at most 2-3 rotations and some
recolouring — fewer rotations than AVL on average, which is why std::map,
std::set, Java's TreeMap, and the Linux kernel's CFS scheduler all
use Red-Black trees.
std::map, std::set, Java-র TreeMap, এমনকি Linux CFS scheduler — সবই RB tree। কারণ AVL-এর তুলনায় RB-তে গড়ে কম rotation লাগে, যদিও tree একটু লম্বা হয়।
4. AVL vs Red-Black
| Property | AVL | Red-Black |
|---|---|---|
| Height bound | ≤ 1.44 log₂(n) | ≤ 2 log₂(n) |
| Search | Faster (tighter) | Slightly slower |
| Insert / Delete rotations | up to O(log n) | O(1) amortised |
| Best fit | Read-heavy workloads | Write-heavy workloads |
| Used by | Some DB indexes | std::map, TreeMap, Linux kernel |
5. Practice Problems
-
Given the AVL tree state, predict the rotation type that fires when 25 is inserted into a tree containing 10, 20, 30 (root 20).Tree {20 →(10, 30)} -তে 25 insert করলে কোন rotation চালু হবে?
✨ Show Answer (উত্তর দেখুন)
Answer: 25 lands as left child of 30. balance(20) = 0; balance(30) = +1; tree still balanced. No rotation. Now insert 23: it lands left of 25; balance(30) becomes +2 with 25 left-heavy → LL rotation at 30.
-
Insert 1, 2, 3, 4, 5, 6, 7 in order into the AVL above and print inorder + heights. Confirm height grows like log n.1..7 insert করুন এবং inorder + height প্রিন্ট করুন।
✨ Show Answer (উত্তর দেখুন)
a2.cpp// Reuse the AVL code from §2 — just feed 1..7 int main() { Node* root = nullptr; for (int x = 1; x <= 7; x++) root = insert(root, x); inorder(root); cout << "\nheight = " << root->h; // 3, not 7 } -
Why does an AVL tree of height h have at least F(h+2) − 1 nodes (where F is Fibonacci)?AVL height h হলে নোডের সংখ্যা ≥ F(h+2) − 1 — কেন?
✨ Show Answer (উত্তর দেখুন)
Sketch: let N(h) be the minimum nodes in any AVL of height h. Worst case: one subtree is height h−1, the other h−2. So N(h) = 1 + N(h−1) + N(h−2), with N(0) = 0, N(1) = 1. This is Fibonacci shifted: N(h) = F(h+2) − 1. Inverting: h ≤ 1.44 log₂ n.
-
Identify the violations in the following Red-Black tree: root R, root→B(red), root→R(red).দেওয়া RB tree-তে কোন invariant ভাঙছে চিহ্নিত করুন।
✨ Show Answer (উত্তর দেখুন)
Answer: (a) Root must be BLACK — but it is red. (b) Two red nodes in a row (root → red child) violates "no two reds adjacent". Fixing requires recolouring root to BLACK; the children's colours then need to be checked for the black-height invariant.
-
Use C++
std::map(a Red-Black tree) to keep a running set of integers and support: insert, erase, kth smallest. (Hint: pair with order statistics is non-trivial in plain map; use__gnu_pbds::tree.)Order statistics tree দিয়ে kth smallest সমর্থন করুন।✨ Show Answer (উত্তর দেখুন)
a5.cpp#include <bits/stdc++.h> #include <ext/pb_ds/assoc_container.hpp> using namespace std; using namespace __gnu_pbds; typedef tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update> OST; int main() { OST t; for (int x : {5, 2, 8, 1, 9, 3}) t.insert(x); cout << "3rd smallest = " << *t.find_by_order(2); // 0-indexed } -
Argue why std::map prefers Red-Black over AVL when most workloads are write-heavy.Write-heavy workload-এ std::map RB tree বেছে নেয় কেন?
✨ Show Answer (উত্তর দেখুন)
Answer: AVL needs up to log n rotations on insert/delete to keep |bf| ≤ 1; RB needs O(1) amortised rotations because the tighter recolouring trick spreads the work. Slightly taller tree (≤ 2 log n vs 1.44 log n) is acceptable for the constant-time write win — and most C++ programs do mixed reads/writes, not pure reads.
Summary — Module 18
AVL keeps |bf| ≤ 1 with up to log n rotations — fastest reads. Red-Black uses colours to bound height by 2·log n with O(1) amortised rotations — fastest writes. Both guarantee O(log n) for everything. Real-world standard libraries pick Red-Black; AVL is more common in heavy-read database indexes.