Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ৮/২৪অ্যাডভান্সড১ ঘণ্টা ৫ মিনিট

BST, AVL, Red-Black — unbalanced হলে কী হয়

BST, AVL and Red-Black Trees

BST-র log₂ n গ্যারান্টি শুধু balance থাকলেই সত্য — sorted input তা ভেঙে linked list বানায়, আর rotation নামের O(1) pointer-অস্ত্র AVL ও red-black tree-কে সেই পতন ঠেকাতে দেয়।

এই লেসন শেষে আপনি পারবেন

  • BST invariant ('বাঁয়ে ছোট, ডানে বড়') থেকে search, insert, delete-এর যুক্তি বের করে হাতে trace করতে পারবেন
  • sorted input ঢুকলে BST কীভাবে degenerate হয়ে linked list হয় — এমনকি attack vector হয়ে ওঠে — তা আকৃতিতে দেখিয়ে প্রমাণ করতে পারবেন
  • single ও double rotation-এর pointer-স্তরের mechanics সম্পূর্ণভাবে সম্পাদন করে দেখাতে পারবেন — কোন case-এ কোনটা, কেন
  • AVL-এর strict balance আর red-black-এর loose balance-এর trade-off মাপ দিয়ে তুলনা করে বলতে পারবেন কোন workload-এ কোনটা
  • নিজের লেখা AVL tree-তে প্রতিটা operation-এর পরে invariant validate করে ভুল-প্রবণ জায়গাগুলো ধরতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনের শেষ পর্যবেক্ষণ: “বাঁয়ে ছোট, ডানে বড়” বসালে in-order sorted। আজকের hero Binary Search Tree (BST): খোঁজা এবং ঢোকানো দুটোই O(log₂ n) — যদি… tree-টা চ্যাপ্টা থাকে।

সেই “যদি”-ই নাটক: BST আকৃতির গ্যারান্টি দেয় না। Sorted input-এ (১, ২, …, ১৫) জন্মায় সোজা chain — linked list-এর ছদ্মবেশ, খোঁজা O(n)। আর attacker ইচ্ছে করেই এমন key পাঠাতে পারে (complexity attack)。

আজকের প্রশ্ন: tree স্বয়ংক্রিয়ভাবে চ্যাপ্টা রাখার সস্তা কৌশল? উত্তর rotation — O(1) pointer-সার্জারি — আর তার ওপর দাঁড়ানো দুই legendary structure: AVL (কড়া ভারসাম্য, দ্রুত lookup) আর red-black (ঢিলা ভারসাম্য, কম ঝামেলা) — যথাক্রমে গবেষণা-আর-kernel- এর প্রিয়।

মূল ধারণা

BST invariant — এক বাক্যের শক্তি

প্রতিটা node x-এর জন্য:

x-এর left subtree-র সব key \< x.key; right subtree-র সব > x.key — নিয়মটা প্রতিটা subtree-তে বারবার (শুধু direct child না — এই পার্থক্যই বড় ফাঁদ, misconception-এ ফিরবে)।

def search(node, key):
    while node and node.key != key:
        node = node.left if key \< node.key else node.right
    return node                      # None = পাওয়া যায়নি

def insert(root, key):               # নতুন key সবসময় leaf-এ!
    if not root:
        return Node(key)
    if key \< root.key:  root.left  = insert(root.left, key)
    else:               root.right = insert(root.right, key)
    return root

খোঁজার খরচ = depth = height-সমানুপাতিক — height ঠিক করে আকৃতি, আকৃতি ঠিক করে ইনপুট-ক্রম — এই শৃঙ্খলই আজকের সূত্রধর।

Delete — তিনটা case

Caseঅবস্থাকাজ
১Leafসরাসরি মুছুন
২এক childchild উঠে আসে
৩দুই childsuccessor (right-subtree min)-এর key copy → successor মুছুন (case ১/২)

Case 3-র সৌন্দর্য: successor = “in-order-তে ঠিক পরের” — জায়গা নিলে in-order sorted থাকে, invariant অটুট।

Degenerate — যখন tree হারিয়ে যায়

insert 1..7 (sorted):   1→2→3→...→7     height 6, খোঁজা ৭ ধাপ
insert 4,2,6,1,3,5,7:        4          height 2, খোঁজা ৩ ধাপ
                           2     6
                          1 3   5 7

একই key, একই class — ঢোকানোর ক্রম বদলালেই height 6 vs 2। n টা sorted key-তে height n−1: প্রতিটা op O(n) — কষ্ট করে বানানো linked list, যার সুবিধা শূন্য, অথচ প্রতি node-এ বাড়তি একটা None pointer।

Rotation — সর্বজনীন rebalancing primitive

Invariant না ভেঙে আকৃতি বদলানোর একটাই move:

Right rotation (Left = mirror):

      y                x        in-order আগে-পরে:
     / \              / \       A, x, B, y, C — অভিন্ন!
    x   C    ──▶     A   y
   / \                  / \
  A   B                B   C

তিন pointer বদল — ব্যস। In-order A x B y C দুই অবস্থাতেই এক, তাই invariant ভাঙে না — শুধু উচ্চতা বদলায়। Double rotation = দুই single পরপর (zig-zag-এ)।

ভেতরে কী ঘটছে

AVL — কড়া শাসন

Adelson-Velskii ও Landis, ১৯৬২ — প্রথম self-balancing tree। নিয়ম: প্রতি node-এ balance factor = height(left) − height(right), থাকতে হবে −1/0/+1। Insert/delete-এর পরে পথে height update করে উপরে ওঠা হয়; প্রথম unbalanced node-এ rotate। চার case:

Caseভার কোথায় (z = unbalanced node)Fix
LLz-এর left-এর leftrot_right(z)
RRz-এর right-এর rightrot_left(z)
LRz-এর left-এর right (zig-zag)rot_left(z.left) → rot_right(z)
RLz-এর right-এর leftrot_right(z.right) → rot_left(z)

Straight-line ভার এক rotation-এ মেটে; zig-zag-এ আগে বাঁক সোজা, তারপর single rotation।

Red-Black — রাজনৈতিক সমঝোতা

Red-black (Guibas & Sedgewick 1978): প্রতি node এক bit — red/black — আর পাঁচ নিয়ম:

  1. প্রতিটা node red বা black
  2. Root black
  3. প্রতিটা NIL/leaf sentinel black
  4. Red node-এর children black (দুই red পরপর অসম্ভব)
  5. Root→NIL প্রতিটা path-এ black node-সংখ্যা সমান (black-height)

এই পাঁচেই height ≤ 2 × log₂(n+1) (প্রমাণ check-এ)। Insert-এর ভাবনা: নতুন node সবসময় red; parent-ও red হলে recolor-এ সমস্যা উপরে ঠেলা, বা uncle-র রঙ দেখে rotation।

AVL বনাম Red-Black — সিদ্ধান্তের টেবিল

দিকAVLRed-Black
Balance নিয়মBF −1/0/+1 প্রতি node৫টি coloring নিয়ম
Height bound≤ 1.44 log₂ n≤ 2 log₂(n+1)
Lookupদ্রুততমভালো; মাঝে AVL-এর দ্বিগুণ গভীর
Insert fix≤ ২ rotationগড়ে O(1) rotation + recolor chain
Delete fixO(log n) rotation পর্যন্ত≤ ৩ rotation
Node overheadheight field (৪ byte)১ color bit
প্রিয় বাড়িread-heavy indexkernel, stdlib, general-purpose

মোদ্দা: দুটোই ঠিক — read/write ratio, memory budget আর latency guarantee ঠিক করে। “Which is better?” → “depends” + এই টেবিল।

উদাহরণ

Insert path-এর পূর্ণ জীবনচক্র — AVL-এ 10, 20, 30, 40, 50

[১] insert 10:        10                [২] insert 20:   10
                                                           \
                                                            20

[৩] insert 30 → 10 BF=−2, RR:  rot_left(10):     20
                                                                /  \
                                                              10    30

[৪] insert 40 → 10 BF=−2, RR:                    20
                                                /    \
                                              10      30
                                                        \
                                                        40

[৫] insert 50 → 30 BF=−2, RR:                    20
                                                /    \
                                              10      40
                                                    /  \
                                                  30    50

লক্ষণ: (১) fix হয় নিচুতম unbalanced ancestor-এ, এক rotation-ই subtree সুস্থ; (২) plain BST-তে এই input-এর height 4 হতো, AVL-এ 2।

Zig-zag input 10, 30, 20? ভার right-child-এর left-এ — RL case: rot_right(30) (বাঁক সোজা), তারপর rot_left(10)।

ছবিতে

আগে (y left-ভারী):            পরে (rot_right(y)):

      y ◀─parent                  x ◀─parent (একই!)
     / \                         / \
    x   C                       A   y
   / \                             / \
  A   B                           B   C

pointer: y.left=x.right;  x.right=y;  parent→x
(height/balance নিচ থেকে উপরে refresh)
in-order আগে/পরে: A→x→B→y→C ✓
Rotation-এর pointer-anatomy — মাত্র তিন pointer-বদল; in-order A x B y C দুই অবস্থাতেই অভিন্ন।
plain BST, insert 1..15:          AVL, insert 1..15:

1                                  8
 \                               /   \
  2                             4     12
   \                           / \    /  \
    3                         2   6  10  14
    ..                       /\ /\  /\  /\
     \                      1 3579 11 13 15
     15       (chain!)          height 14  vs  3

ছবি tie-breaking-এ ভিন্ন valid আকৃতি হতে পারে — logarithmic height আর balance-নিয়ম অবশ্যই; validator assert করে। Root ৮-এ = median-ঘেঁষা root।

একই ১৫টা key — sorted-input plain BST বনাম AVL; height 14 বনাম 3।

নিজে চালিয়ে দেখুন

EXPERIMENT

Degenerate প্রমাণ — একই ১৫টা key, দুইটা ভাগ্য

Python 3.8+· ১৫ মিনিট
from collections import deque


# ---------- plain BST ----------
class BN:
    __slots__ = ('key', 'left', 'right')
    def __init__(self, k):
        self.key, self.left, self.right = k, None, None

def bst_insert(root, key):
    if not root:
        return BN(key)
    if key \< root.key: root.left  = bst_insert(root.left, key)
    else:               root.right = bst_insert(root.right, key)
    return root


# ---------- AVL ----------
class AN:
    __slots__ = ('key', 'left', 'right', 'h')
    def __init__(self, k):
        self.key, self.left, self.right, self.h = k, None, None, 1

def H(x): return x.h if x else 0
def upd(x): x.h = 1 + max(H(x.left), H(x.right))
def BF(x): return H(x.left) - H(x.right)

def rot_r(y):
    x = y.left; y.left = x.right; x.right = y
    upd(y); upd(x); return x

def rot_l(x):
    y = x.right; x.right = y.left; y.left = x
    upd(x); upd(y); return y

def avl_ins(x, key):
    if not x: return AN(key)
    if key \< x.key:   x.left  = avl_ins(x.left, key)
    elif key > x.key: x.right = avl_ins(x.right, key)
    else: return x
    upd(x)
    b = BF(x)
    if b > 1:
        if BF(x.left) \< 0: x.left = rot_l(x.left)
        return rot_r(x)
    if b \< -1:
        if BF(x.right) > 0: x.right = rot_r(x.right)
        return rot_l(x)
    return x


def stats(root):
    """(max_depth, avg_depth) — successful search-এর খরচের আদল"""
    q, total, cnt, mx = deque([(root, 0)]), 0, 0, 0
    while q:
        n, d = q.popleft()
        total += d; cnt += 1; mx = max(mx, d)
        if n.left:  q.append((n.left, d + 1))
        if n.right: q.append((n.right, d + 1))
    return mx, total / cnt


keys = list(range(1, 16))            # sorted input — আজকের ভিলেন
bst_root = None
for k in keys: bst_root = bst_insert(bst_root, k)
avl_root = None
for k in keys: avl_root = avl_ins(avl_root, k)

for name, r in (('plain BST', bst_root), ('AVL', avl_root)):
    mx, avg = stats(r)
    print(f'{name:10s} height={mx:2d}  avg search depth={avg:.2f}')
print(f'theoretical best height = {len(keys).bit_length() - 1}')

প্রত্যাশিত output:

plain BST  height=14  avg search depth=7.00
AVL        height= 3  avg search depth=2.27
theoretical best height = 3

একই key — গড় গভীরতা 7 (worst 14) vs 2.27 (worst 3); n বাড়লে linear-vs-logarithmic — ১০ হাজার sorted key-তে ~৫০০০ comparison vs ~১৩।

নিজে বাড়ান: random shuffle-এ plain BST-র height ~2–3× log₂ n (randomness বাঁচায়), sorted-এ আবার chain — বিপদ adversarial ক্রমে।

এটা কী প্রমাণ করে

Sorted input plain BST-কে height n−1-এর chain-এ ফেলে গড় খোঁজা-গভীরতা O(n) করে; AVL একই input-এ auto-balance করে theoretical-best height-এ — ফারাকটা সংখ্যায় দাঁড়িয়ে যায়।

নিজে বানান

BUILD IT

Working AVL tree — insert + rotation + height tracking + invariant validator

Python · ●●●○○
  1. node-এ height field; structural change-এর পরে নিচ থেকে উপরে update
  2. rotate_left/right — তিন pointer বদল + height refresh
  3. insert recursive, ফেরার পথে rebalance — LL/LR/RR/RL
  4. validate(): BST-order + height-correctness + balance — recursive audit
  5. harness: shuffled ও sorted input-এ প্রতি insert-এর পরে assert(validate)
import random


class AVLNode:
    __slots__ = ('key', 'left', 'right', 'h')

    def __init__(self, key):
        self.key, self.left, self.right, self.h = key, None, None, 1


def height(x):
    return x.h if x else 0

def update(x):
    x.h = 1 + max(height(x.left), height(x.right))

def bf(x):
    return height(x.left) - height(x.right)


def rotate_right(y):
    x = y.left
    y.left = x.right          # x-এর ডান subtree y-এর বাঁয়ে
    x.right = y               # y নামল x-এর ডানে
    update(y); update(x)      # নিচেরটা আগে!
    return x

def rotate_left(x):
    y = x.right
    x.right = y.left
    y.left = x
    update(x); update(y)
    return y


def rebalance(x):
    """ফেরার পথে; প্রথম imbalance-এ fix, নতুন subtree-root ফেরত"""
    update(x)
    b = bf(x)
    if b > 1:                                # বাঁয়ে ভারী
        if bf(x.left) \< 0:                  # LR: আগে বাঁক সোজা
            x.left = rotate_left(x.left)
        return rotate_right(x)                # LL
    if b \< -1:                               # ডানে ভারী
        if bf(x.right) > 0:                  # RL
            x.right = rotate_right(x.right)
        return rotate_left(x)                 # RR
    return x                                  # সুস্থ — ছোঁয়াইনি


def avl_insert(node, key):
    if not node:
        return AVLNode(key)
    if key \< node.key:
        node.left = avl_insert(node.left, key)
    elif key > node.key:
        node.right = avl_insert(node.right, key)
    else:
        return node                            # duplicate — কিছুই না
    return rebalance(node)


# ---------- validator: safety net ----------
def validate(node, lo=float('-inf'), hi=float('inf')):
    """subtree height ফেরত; পথে সব invariant assert"""
    if not node:
        return 0
    assert lo \< node.key \< hi, f'BST order ভাঙল @ {node.key}'
    hl = validate(node.left, lo, node.key)
    hr = validate(node.right, node.key, hi)
    assert node.h == 1 + max(hl, hr), f'height field ভুল @ {node.key}'
    assert abs(hl - hr) \<= 1, f'balance ভাঙল @ {node.key} ({hl}/{hr})'
    return node.h


if __name__ == '__main__':
    # --- টেস্ট ১: adversarial sorted input ---
    root = None
    for k in range(1, 16):
        root = avl_insert(root, k)
        validate(root)                         # প্রতি step-এ পূর্ণ audit
    print('sorted 1..15 OK, height =', height(root),
          '(perfect-এর কাছাকাছি: best =', len(range(1,16)).bit_length() - 2, ')')

    # --- টেস্ট ২: random ৫০০ insert ---
    root = None
    for k in random.sample(range(10_000), 500):
        root = avl_insert(root, k)
        validate(root)
    print('random 500 OK, height =', height(root),
          '(bound:', round(1.44 * 9), ')')

    # --- টেস্ট ৩: duplicate ---
    before = height(root)
    root = avl_insert(root, root.key)
    validate(root)
    assert height(root) == before
    print('duplicate insert OK — কোনো পরিবর্তন নেই')

প্রত্যাশিত output (random height seedভেদে ৯–১১):

sorted 1..15 OK, height = 4 (perfect-এর কাছাকাছি: best = 3 )
random 500 OK, height = 10 (bound: 13)
duplicate insert OK — কোনো পরিবর্তন নেই

(build-এর height field leaf-কে 1 গোনে — node-counting; experiment- এর stats edge-counting — তাই sorted tree এখানে ‘4’, ওখানে ‘3’।)

নিজে বাড়ান: (১) delete(key) — successor-swap + ফেরার পথে rebalance, rotation-count counter-সহ; (২) sorted vs random rotation- তুলনা; (৩) update() comment-out করে validator-কে ভাঙুন।

বাস্তব সিস্টেমে

ব্যালান্সড tree যেখানে এই মুহূর্তেও চলছে

  • Linux kernel: struct rb_root সর্বত্র — CFS scheduler, VM area map, timers; busy server-এ প্রতি second-এ লাখ traversal, নিঃশব্দে।
  • Ordered maps: std::map, TreeMap — sorted iteration + worst-case O(log n)।
  • DB in-memory side + RT/embedded: symbol table, metadata cache; কড়া deadline-এ rbtree-র bounded fix-cost-ই বিক্রির পয়েন্ট।

উল্টো দিকটাও: plain BST-র production-ব্যবহার প্রায় শূন্য — সে শেখার সিঁড়ি।

যে ভুলগুলো সবাই করে

“BST invariant মানে প্রতিটা node-এর বাঁ child ছোট, ডান child বড়।”

অসম্পূর্ণ — নিয়ম পুরো subtree-র সাপেক্ষে। Direct child ঠিক থেকেও দূরের descendant ভাঙতে পারে: root-এর ডান child-এর বাঁ subtree-তে ছোট key ঢুকলে প্রতি-level তুলনা ঠিক দেখায়, অথচ BST invalid। এজন্যই validation-এ descent-এ min/max bound — build-এর validate() তা-ই।

“AVL সবসময় red-black-এর চেয়ে ভালো — বেশি strictly balanced।”

Lookup-only-তে হ্যাঁ। Write-heavy-তে উল্টো: AVL fix-এ ঘন rotation, rbtree violation recolor-এ মেটায় — pointer ছোঁয়ই না; delete-এ ≤৩ rotation ceiling worst-case latency-র সোনা। “Better for this pattern” — hood-এর টেবিল দেখুন।

“Rotation একটা O(log n) অপারেশন — rebalancing ধীর।”

Rotation O(1) — তিন pointer, দুই height update। Logarithmic হলো কোথায় imbalance, তা path-ধরে খোঁজা। AVL-এ প্রায়ই নিচেই মেটে; rbtree- এ recolor-chain ছড়ায় কিন্তু rotation গড়ে একটা। Amortized খরচ এত কম যে log-guarantee practical-এ টেরই পাওয়া যায় না。

“Self-balancing মানে root সবসময় median, দুই পাশ হুবহু অর্ধেক।”

Perfect split প্রতি insertion-এ O(n) — sorted-array insertion-ই! চালাকি: প্রতি node-এ local শৃঙ্খলা রেখে global logarithmic height। Root median থেকে দূরে থাকতেই পারে, তবু সুস্থ — metric গভীরতা, root না。

বুঝেছেন কি না দেখুন

1

BST-তে কোন traversal sorted ক্রম দেয়, কেন?

স্মরণ

In-order। Induction: subtree-তে ক্রম = বাঁ (ছোট) → node → ডান (বড়); invariant থাকলে সংযোগে ক্রম ভাঙে না। Delete-এর case-3-এ successor-ই বেছে নেওয়ার কারণও এটা — in-order-এর পরের element, জায়গা নিলে ক্রম অটুট।

2

Keys 10, 20, 30, 25, 27 ক্রমে AVL-তে ঢুকলে কোন rotation কোথায়? চূড়ান্ত root?

প্রয়োগ
  • 10, 20 সোজা; 30 → 10 BF=−2 straight-right → RR, rot_left(10) → root 20, বাঁ 10, ডান 30।
  • 25 → 30-এর বাঁয়ে; 20 BF=−2 + right-child BF=+1 → RL: rot_right(30) তারপর rot_left(20) → root 25।
  • 27 → 30-এর বাঁয়ে; 25-এর BF=−1 — violation নেই।

Root: 25, height 2, মোট ৩ single rotation।

3

Red-black নিয়ম ৫ (সব path-এ black-সংখ্যা সমান) থেকে প্রমাণ করুন height ≤ 2 × log₂(n+1)।

যুক্তি

ধরি black-height = bh। Claim: subtree-র nodes ≥ 2^bh − 1। Induction: leaf ✓; ভেতরের node-এর child-দের bh ≥ bh−1 (red-child সমান, black-child এক কম — red-chain ≤ ১) → nodes ≥ 2^bh − 1 ✓।

নিয়ম ৪: path-এ অর্ধেকের বেশি red না → h ≤ 2·bh; আর bh ≤ log₂(n+1) → h ≤ 2 log₂(n+1) ∎ — AVL-এর 1.44 vs 2-এর ফারাকের জন্ম এখানেই。

4

৮ MB RAM-এর embedded sensor logger: প্রতি ms-এ নতুন reading-key ঢোকে, মাঝে মাঝে range query। Plain BST / AVL / red-black — কোনটা, কীসের ভিত্তিতে?

ডিজাইন

Red-black: (১) plain BST বাদ — logger timestamp monotonic = sorted-input scenario; (২) memory — AVL height-field ৪ byte vs rbtree ১ bit, কোটি node-এ ~৩০ MB ফারাক; (৩) write-dominated — rbtree-র amortized-সস্তা insert-fix ঠিক এই জায়গায়। Range query দুটোতেই successor-walk। Disk-flush হলে B-tree — in-memory side তবু এটাই।

5

std::map বনাম std::unordered_map — ordered requirement ছাড়া কি unordered সবসময় জেতে?

যুক্তি

map = rbtree: sorted iteration, lower_bound, guaranteed O(log n)। unordered_map = hash: average O(1), adversarial collision-এ O(n), ordering শূন্য। Exact-only lookup-এ hash সাধারণত জেতে — কিন্তু hash-flooding attack, string-key hashing O(len), bucket pointer- chase-এর cache-behavior — ছোট collection-এ rbtree constant-factor-এ জেতে। Measure — average-এর সাথে worst-case-ও।

এরপর কী

এরপর কী

BST-র promise ইনপুট-ক্রমে জিম্মি; rotation O(1) অস্ত্র; AVL-red-black তার কড়া-ঢিলা দুই দর্শন।

অক্ষত ধরে-নেওয়া: প্রতি node = আলাদা object, প্রতি ধাপ = pointer-hop। Disk-এ Disk-এ এক random read microsecond-শত — অর্থনীতি উল্টে যায়। পরের hero B-tree: node page-সমান চওড়া, fanout হাজারে, height ৩-৪; B+ variant leaf-link-এ range scan sequential। Database-index আর filesystem-metadata এখন থেকে অন্য চোখে দেখবেন。

আরও পড়ুন

  • Introduction to Algorithms, 3rd ed., Chapter 12–13 — Binary Search Trees; Red-Black Trees — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · BST delete-এর তিন case, rotation-এর formal definition, red-black-এর ৫ property ও height bound-এর প্রমাণ
  • "An algorithm for the organization of information" (1962) — G. M. Adelson-Velskii, E. M. Landis · AVL tree-এর মূল পেপার — history-র প্রথম self-balancing search tree
  • Performance Analysis of BSTs in System Software (Stanford Tech Report, 2004) — Ben Pfaff · Kernel-grade benchmark — system software red-black কেন পছন্দ করে, measurement-based উত্তর