Self-Balancing BSTs: AVL & Red-Black

AVL ও Red-Black Tree

Read: ~45 min Advanced 6 practice problems Live code runner

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.

Naive BST-তে যদি sorted order-এ insert করেন, তবে এটি একটি linked list-এ পরিণত হয় — search হয়ে যায় O(n)। তাই প্রতিটি insertion / deletion-এর পর rotation দিয়ে balance ঠিক রাখতে হয়।
Naive (h=4) 1 2 3 4 Balanced (h=2) 3 2 4 1 5 Figure 18.1 — Same keys, different shapes. Self-balancing BSTs guarantee the right side.

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.

The four rotation cases LL: left-heavy, left child also left-heavy → single right rotate. RR: mirror. LR: left-heavy, left child right-heavy → left-rotate child, then right-rotate self. RL: mirror.
avl.cpp
#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:

  1. Every node is red or black.
  2. The root is black.
  3. All NIL leaves are black.
  4. A red node's children are both black (no two reds in a row).
  5. 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

PropertyAVLRed-Black
Height bound≤ 1.44 log₂(n)≤ 2 log₂(n)
SearchFaster (tighter)Slightly slower
Insert / Delete rotationsup to O(log n)O(1) amortised
Best fitRead-heavy workloadsWrite-heavy workloads
Used bySome DB indexesstd::map, TreeMap, Linux kernel

5. Practice Problems

  1. 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.

  2. 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
    }
  3. 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.

  4. 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.

  5. 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
    }
  6. 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.

AVL ও RB — দুটিই O(log n) গ্যারান্টি দেয়। AVL strict, RB ঢিলেঢালা কিন্তু write-friendly। std::map হলো RB।

Next Module → B-Trees, B+ Trees & Segment Trees Intro — disk-friendly trees।