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-কে সেই পতন ঠেকাতে দেয়।
আগে এটা বুঝি
গত লেসনের শেষ পর্যবেক্ষণ: “বাঁয়ে ছোট, ডানে বড়” বসালে 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 | সরাসরি মুছুন |
| ২ | এক child | child উঠে আসে |
| ৩ | দুই child | successor (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 |
|---|---|---|
| LL | z-এর left-এর left | rot_right(z) |
| RR | z-এর right-এর right | rot_left(z) |
| LR | z-এর left-এর right (zig-zag) | rot_left(z.left) → rot_right(z) |
| RL | z-এর right-এর left | rot_right(z.right) → rot_left(z) |
Straight-line ভার এক rotation-এ মেটে; zig-zag-এ আগে বাঁক সোজা, তারপর single rotation।
Red-Black — রাজনৈতিক সমঝোতা
Red-black (Guibas & Sedgewick 1978): প্রতি node এক bit — red/black — আর পাঁচ নিয়ম:
- প্রতিটা node red বা black
- Root black
- প্রতিটা NIL/leaf sentinel black
- Red node-এর children black (দুই red পরপর অসম্ভব)
- Root→NIL প্রতিটা path-এ black node-সংখ্যা সমান (black-height)
এই পাঁচেই height ≤ 2 × log₂(n+1) (প্রমাণ check-এ)। Insert-এর
ভাবনা: নতুন node সবসময় red; parent-ও red হলে recolor-এ সমস্যা
উপরে ঠেলা, বা uncle-র রঙ দেখে rotation।
AVL বনাম Red-Black — সিদ্ধান্তের টেবিল
| দিক | AVL | Red-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 fix | O(log n) rotation পর্যন্ত | ≤ ৩ rotation |
| Node overhead | height field (৪ byte) | ১ color bit |
| প্রিয় বাড়ি | read-heavy index | kernel, 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 ✓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।
নিজে চালিয়ে দেখুন
Degenerate প্রমাণ — একই ১৫টা key, দুইটা ভাগ্য
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-এ — ফারাকটা সংখ্যায় দাঁড়িয়ে যায়।
নিজে বানান
Working AVL tree — insert + rotation + height tracking + invariant validator
- node-এ height field; structural change-এর পরে নিচ থেকে উপরে update
- rotate_left/right — তিন pointer বদল + height refresh
- insert recursive, ফেরার পথে rebalance — LL/LR/RR/RL
- validate(): BST-order + height-correctness + balance — recursive audit
- 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-caseO(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 না。
বুঝেছেন কি না দেখুন
1BST-তে কোন traversal sorted ক্রম দেয়, কেন?
স্মরণ
In-order। Induction: subtree-তে ক্রম = বাঁ (ছোট) → node → ডান (বড়); invariant থাকলে সংযোগে ক্রম ভাঙে না। Delete-এর case-3-এ successor-ই বেছে নেওয়ার কারণও এটা — in-order-এর পরের element, জায়গা নিলে ক্রম অটুট।
2Keys 10, 20, 30, 25, 27 ক্রমে AVL-তে ঢুকলে কোন rotation কোথায়?
চূড়ান্ত root?
প্রয়োগ
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।
3Red-black নিয়ম ৫ (সব path-এ black-সংখ্যা সমান) থেকে প্রমাণ করুন
height ≤ 2 × log₂(n+1)।
যুক্তি
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 তবু এটাই।
5std::map বনাম std::unordered_map — ordered requirement ছাড়া কি
unordered সবসময় জেতে?
যুক্তি
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 উত্তর