Binary Tree — hierarchy-র সবচেয়ে সরল রূপ
Binary Trees and Traversals
Binary tree হলো hierarchy-র সবচেয়ে সরল রূপ — প্রতিটা node-এর হাতে মাত্র দুইটা pointer, আর traversal মানে সেই tree-এর প্রতিটা node-কে ঠিক একবার করে ঘুরে দেখা, চারটা ভিন্ন নিয়মে।
আগে এটা বুঝি
গত লেসনে stack, queue, deque — সব linear: প্রতি node-এ মাত্র
একটা next pointer, একটা রাস্তা।
কিন্তু বাস্তবের বেশিরভাগ তথ্য শাখায় বিভক্ত: directory-র নিচে
directory, tag-এর ভেতরে tag, manager-এর নিচে team — এমনকি 2 + 3 * 4
expression-ও ভেতরে ভেতরে শাখাওয়ালা。
এই hierarchy-র সবচেয়ে সরল, বহুল ব্যবহৃত ধারক tree — আজকের
বিষয় তার প্রথম রূপ binary tree: প্রতি node-এ সর্বোচ্চ দুইটা
pointer (left, right)। linked list-এর চেয়ে ঠিক একটা বেশি — অথচ
এই একটা pointer সম্ভাবনার জগৎ বদলে দেয়।
আজকের প্রশ্ন দুইটা: (১) tree বর্ণনার ভাষা — depth, height, আকৃতি? (২) প্রতিটা node ঘোরার নিয়ম — কারণ tree-তে “প্রথম থেকে শেষ” বলে একটাই ক্রম নেই; নিয়ম ঠিক করলেই চার traversal জন্মায়।
মূল ধারণা
Node, left, right — পুরো vocabulary একটা ছবিতে
A ← root
/ \
B C
/ \ / \
D E F G ← এরা সবাই leaf
| শব্দ | অর্থ |
|---|---|
| node | value + left + right pointer |
| root / leaf | সবার উপরের node (parent নেই) / child-হীন node (D..G) |
| parent / child | B হলো D-এর parent; D হলো B-র left child |
| subtree | কোনো node + তার descendants — নিজেই পূর্ণ tree |
Python-এ একটা node মাত্র তিনটা field: value, left, right —
দুই pointer শুরুতে None। (Build অংশে পূর্ণ class আছে।)
Height, depth, level — একই রকম শোনায়, এক না
- Depth (of a node) — root থেকে সেই node পর্যন্ত edge সংখ্যা;
root-এর depth = 0, উপরের ছবিতে
D-এর depth = 2। - Height (of a node) — node থেকে সবচেয়ে দূরের descendant leaf পর্যন্ত edge; leaf-এর height = 0।
- Height (of the tree) — root-এর height; উপরের ছবিতে = 2।
- Level — একই depth-এর সব node; root বসে level 1-এ (level = depth + 1)।
চারটা আকৃতি — আর পারফরম্যান্স মানে
Full: প্রতি node-এ 0 বা 2 child Complete: শেষ level ছাড়া সব ভরা,
শেষটা বাঁ দিকে জমা
o o o o
/ \ / \ / \ / \
o o o o o o o o
/ \ / /
o o o o
Perfect: সব leaf একই depth Degenerate: সবার এক child — linked list!
o o
/ \ \
o o o
\
oনামের জন্য না — পারফরম্যান্সের জন্য: perfect-এর height
log₂(n+1) − 1, degenerate-এর n − 1 — একই node, আকাশ-পাতাল গভীরতা।
আর এক level নামা = এক pointer-hop = সম্ভাব্য cache trip; হিসাবটা
Under the Hood-এ পাকা হবে。
| আকৃতি (n node) | Height | মন্তব্য |
|---|---|---|
| Perfect | log₂(n+1) − 1 | search-এর স্বপ্ন |
| Complete | ⌊log₂ n⌋ | Heap এখানেই বসে |
| Full | … n−1 পর্যন্ত | নিয়ম আছে, balance নেই |
| Degenerate | n − 1 | linked-list ছদ্মবেশ |
ভেতরে কী ঘটছে
Traversal — কেন একটাই উত্তর নেই
প্রতিটা মোড়ে দুইটা রাস্তা — তাই প্রশ্ন: node-এর নিজের কাজ (print, compare, free) কখন হবে — বাঁয়ে যাওয়ার আগে, নাকি দুই পাশ ঘুরে ফিরে এসে?
- Pre-ordernode আগে, তারপর বাঁয়া subtree, তারপর ডানা
- In-orderবাঁয়া, তারপর node, তারপর ডানা — node মাঝখানে (in)
- Post-orderবাঁয়া, ডানা, শেষে node — কাজ সবার পরে (post)
- Level-orderউপর থেকে নিচে, বাঁ থেকে ডানে — DFS নয়, BFS
Recursive কোডে তিনটা DFS traversal একটার ভাই — শুধু একটা লাইনের জায়গা বদলায়:
def inorder(node): # left → node → right
if node is None:
return
inorder(node.left)
visit(node) # pre-order-এ এই লাইন সবার আগে,
inorder(node.right) # post-order-এ সবার পরে — ব্যস!
# pre / in / post = root-এর কাজটা কখন হয়; বাঁ-আগে-ডান ক্রম সবগুলোতেই এক।Call stack-ই তো একটা stack
উপরের কোডে আমরা stack লিখিনি — অথচ ব্যবহার হচ্ছেই। প্রতিটা pending call মানে recursion-এর call stack-এ একটা frame: কোন node-এ আছি, ফিরে কী করতে হবে। Recursion-এর সুবিধা — bookkeeping compiler করে দেয়; অসুবিধা — stack-টা আপনার হাতে নেই।
Degenerate tree-তে (height n−1) recursive inorder n frame খায় —
Python-এর limit ~১০০০ ছাড়ালেই RecursionError, নিচের level-এ আসল
stack overflow। সমাধান: explicit stack — নিজের list-এ কাজ-তালিকা,
loop। Build-এ চারটাই recursion-ছাড়া লিখব।
Level-order? “যে node আগে পেলাম তার children-ও আগে” — FIFO:
stacks-queues-deques লেসনের deque-ই হাতিয়ার। Stack দিলে শেষে-pushed
child আগে বেরোয় — গভীরে চলে যাবেন, level-ক্রম নষ্ট। (queue-vs-stack-
এর এই ফারাক graph-এ BFS vs DFS হয়ে ফিরবে。)
Memory-তে এই tree আসলে কেমন
Node-রা heap-এ আলাদা আলাদা allocate হয় — পাশাপাশি থাকার নিশ্চয়তা
নেই। এক level নামা = এক dereference; cache-এর বাইরে গেলে পূর্ণ
cache miss। এটাই locality
of reference-এর শত্রু নং ১: pointer chasing। Perfect খোঁজার
log₂ n hop দুর্দান্ত asymptotic, কিন্তু প্রতি hop সম্ভাব্য
cache line fetch। Complete tree হলে
pointer-ই লাগে না — পুরোটা array-তে; জাদুটা heaps-লেসনে।
Expression tree — operator ভেতরে, operand পাতায়
(2 + 3) * 4:
* pre-order: * + 2 3 4 (prefix)
/ \ in-order: 2 + 3 * 4 (infix — parens ছাড়া ambiguous!)
+ 4 post-order: 2 3 + 4 * (postfix / RPN)
/ \
2 3Postfix-এ operator-এর দরকারের আগেই দুই operand হাতে — stack-machine ভালোবাসে। আর এই tree-টাই compiler-এর AST-এর নমুনা: parser source থেকে tree, optimizer সেখানে rewrite, code generator থেকে machine code — compilers module-এর ast-design প্রসঙ্গ এখান থেকেই শুরু।
উদাহরণ
একটা tree, চারটা traversal — হাতে করে
A
/ \
B C
/ \ / \
D E F GIn-order ধাপে ধাপে (বাঁয়া পুরো, তারপর node, তারপর ডানা):
inorder(A)
├─ inorder(B) → D, B, E (D leaf; B-এর কাজ মাঝে; E leaf)
└─ A প্রিন্ট
└─ inorder(C) → F, C, G
| Traversal | Output | চেনার দাগ |
|---|---|---|
| Pre-order | A B D E C F G | প্রথম element-ই root |
| In-order | D B E A F C G | root মাঝে পড়ে |
| Post-order | D E B F G C A | শেষ element root |
| Level-order | A B C D E F G | উপর→নিচ, বাঁ→ডান |
আর in-order output D B E A F C G — বর্ণানুক্রমে সাজানো! কৈফিয়ত
না — letters “বাঁয়ে ছোট, ডানে বড়” মেনে বসানো। সেই নিয়মই BST —
পরের লেসনের hero; এই পর্যবেক্ষণই সেখানকার স্ট্রাকচারের ভিত্তি।
ছবিতে
একই tree-এর তিনটা চেহারা
depth: 0 1 2 (level: 1, 2, 3)
A
/ \
B C ← A-র height = 2
/ \ / \
D E F G ← leaf-দের height = 0
tree height = 2; nodes = 7 = 2³−1; edges = 6 = nodes−1
nested text: (A (B (D) (E)) (C (F) (G)))
পড়ার নিয়ম: প্রতি বন্ধনী-জোড়া = node; ভেতরে বাঁ subtree, ডান subtree।
JSON, Lisp source, বহু config format এই ভাষাতেই কথা বলে।
traversal strips:
pre: A B D E C F G in: D B E A F C G post: D E B F G C Aনিজে চালিয়ে দেখুন
Tree বানান, ঘোরান, আর Catalan-এর গোয়েন্দা হোন
from collections import deque
class Node:
__slots__ = ('value', 'left', 'right')
def __init__(self, value):
self.value, self.left, self.right = value, None, None
def build_sample():
d, e, f, g = Node('D'), Node('E'), Node('F'), Node('G')
b, c = Node('B'), Node('C')
b.left, b.right, c.left, c.right = d, e, f, g
a = Node('A'); a.left, a.right = b, c
return a
def preorder(n): return [] if not n else [n.value] + preorder(n.left) + preorder(n.right)
def inorder(n): return [] if not n else inorder(n.left) + [n.value] + inorder(n.right)
def postorder(n): return [] if not n else postorder(n.left) + postorder(n.right) + [n.value]
def levelorder(n):
out, q = [], deque([n] if n else [])
while q:
x = q.popleft()
out.append(x.value)
if x.left: q.append(x.left)
if x.right: q.append(x.right)
return out
def nested_text(n):
if not n: return ''
if not n.left and not n.right: return f'{n.value}'
return f'({n.value} {nested_text(n.left)} {nested_text(n.right)})'
t = build_sample()
for name, fn in (('pre', preorder), ('in', inorder),
('post', postorder), ('lvl', levelorder)):
print(f'{name:4s}:', ' '.join(fn(t)))
print('nested:', nested_text(t))
# --- Catalan teaser: n node-এ কত ভিন্ন আকৃতি? ---
from functools import lru_cache
import math
@lru_cache(maxsize=None)
def shapes(n):
"""n node-এর structurally distinct binary tree"""
if n \<= 1: return 1
return sum(shapes(l) * shapes(n - 1 - l) for l in range(n))
for n in range(1, 11):
assert shapes(n) == math.comb(2 * n, n) // (n + 1) # closed form
print('Catalan:', [shapes(i) for i in range(1, 11)])প্রত্যাশিত output:
pre : A B D E C F G
in : D B E A F C G
post: D E B F G C A
lvl : A B C D E F G
nested: (A (B D E) (C F G))
Catalan: [1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796]যাচাই করুন: চার output টেবিলের সাথে মিলছে? আর Catalan সংখ্যা — মাত্র ১০ node-এ ১৬,৭৯৬ আকৃতি। একই value ভিন্ন ক্রমে ঢুকলেই ভিন্ন আকৃতি, ভিন্ন height, ভিন্ন গতি — পরের লেসনের নাটকের seed।
একই tree চার traversal-এ চার ক্রম দেয়; n node দিয়ে বানানো যায় এমন ভিন্ন আকৃতির সংখ্যা Catalan(n) — exponential ভাবে ফুঁসে ওঠে, তাই 'আকৃতি কেমন হবে' ঠিক করাটাই performance-এর আসল লেগো।
নিজে বানান
BinaryTree class — চারটা traversal, কোনো recursion ছাড়া
- insert_level_order দিয়ে list থেকে complete-ish tree (queue ব্যবহার)
- pre/in-order iterative — একটা stack
- post-order iterative — two-stack trick
- level-order — queue (BFS)
- draw(): ঘুরানো ASCII pretty-printer + recursive ফলাফলের সাথে assert
মূল চ্যালেঞ্জ recursion-ছাড়া post-order: two-stack trick — modified pre-order (node→right→left) চালিয়ে ফল উল্টে দিন।
from collections import deque
class Node:
__slots__ = ('value', 'left', 'right')
def __init__(self, value):
self.value = value
self.left = None
self.right = None
class BinaryTree:
def __init__(self):
self.root = None
def insert_level_order(self, values):
if not values:
return
self.root = Node(values[0])
q, i = deque([self.root]), 1
while q and i \< len(values):
node = q.popleft()
if i \< len(values):
node.left = Node(values[i]); i += 1
q.append(node.left)
if i \< len(values):
node.right = Node(values[i]); i += 1
q.append(node.right)
def preorder(self):
out, stack = [], [self.root] if self.root else []
while stack:
n = stack.pop()
out.append(n.value)
if n.right: stack.append(n.right) # ডান আগে push —
if n.left: stack.append(n.left) # তাই বাঁ আগে pop
return out
def inorder(self):
out, stack, cur = [], [], self.root
while cur or stack:
while cur: # বাঁয়ের দেয়াল ধরে নামো
stack.append(cur)
cur = cur.left
cur = stack.pop()
out.append(cur.value) # node-এর কাজ
cur = cur.right # এবার ডান subtree
return out
def postorder(self):
if not self.root:
return []
s1, s2 = [self.root], []
while s1:
n = s1.pop()
s2.append(n)
if n.left: s1.append(n.left)
if n.right: s1.append(n.right)
return [n.value for n in reversed(s2)]
def levelorder(self):
out, q = [], deque([self.root] if self.root else [])
while q:
n = q.popleft()
out.append(n.value)
if n.left: q.append(n.left)
if n.right: q.append(n.right)
return out
def draw(self):
lines = []
def rec(n, depth): # শুধু printer-এ recursion মাফ
if not n: return
rec(n.right, depth + 1)
lines.append(' ' * depth + str(n.value))
rec(n.left, depth + 1)
rec(self.root, 0)
return '\n'.join(lines)
if __name__ == '__main__':
t = BinaryTree()
t.insert_level_order(['A', 'B', 'C', 'D', 'E', 'F', 'G'])
print(t.draw(), end='\n\n')
# recursion-এর সাথে মিলিয়ে দেখা — দুই পথ, এক উত্তর
assert t.preorder() == ['A', 'B', 'D', 'E', 'C', 'F', 'G']
assert t.inorder() == ['D', 'B', 'E', 'A', 'F', 'C', 'G']
assert t.postorder() == ['D', 'E', 'B', 'F', 'G', 'C', 'A']
print('pre :', t.preorder())
print('in :', t.inorder())
print('post:', t.postorder())
print('lvl :', t.levelorder())
print('সব assert পাস — iterative == recursive ✓')প্রত্যাশিত output:
G
C
F
A
E
B
D
pre : ['A', 'B', 'D', 'E', 'C', 'F', 'G']
in : ['D', 'B', 'E', 'A', 'F', 'C', 'G']
post: ['D', 'E', 'B', 'F', 'G', 'C', 'A']
lvl : ['A', 'B', 'C', 'D', 'E', 'F', 'G']
সব assert পাস — iterative == recursive ✓(ছবিটা ৯০° ঘুরানো — head বাঁয়ে হেলান দিলেই চেনা tree।)
নিজে বাড়ান: size()/height() iterative; nested-text serialize/
deserialize; ৫০০০-node chain-এ recursive inorder-এর RecursionError vs
আপনার iterative ভার্সন।
বাস্তব সিস্টেমে
যেখানে যেখানে binary tree লুকিয়ে আছে
- DOM ও render engines: webpage-এর গঠন tree; CSS matching, event bubbling, প্রতি frame-এ reconciliation — সব tree-walk।
- Compiler-এর AST: parser expression tree বানায়, optimizer post-order-এ ভেতর-থেকে-বাইরে rewrite করে — child প্রসেস না হলে parent-এর কাজ শুরুই হয় না; post-order-এর প্রাণবন্ত ব্যবহার।
- Serialization + Huffman coding: pre-order + null marker-এ tree wire-এ; Huffman-এ character = root-to-leaf path — ZIP/JPEG-এর ভেতরে।
- Graphics: ray tracing-এ bounding-volume hierarchy — GPU প্রতি pixel-এ BVH-descent।
দুইটা প্রশ্ন সবখানে: কোন ক্রমে ঘুরব, আর কতটা চ্যাপ্টা — প্রথমটা আজ, দ্বিতীয়টা পরের দুই লেসন।
যে ভুলগুলো সবাই করে
“Height আর depth একই জিনিসের দুই নাম।”
Depth: root থেকে node-এ — root-এর depth সবসময় 0। Height: node থেকে গভীরতম leaf — leaf-এর height 0; root-এর height-ই tree-র height। Perfect tree-তে depth + subtree-height = tree height — যাচাই করলে আর গুলাবেন না。
“In-order traversal সবসময় sorted output দেয়।”
Sorting in-order-র সংজ্ঞায় নেই — A-র বাঁয়ে Z বসালেই output
Z A ...। Sorted হয় শুধু BST invariant (“বাঁয়ে ছোট, ডানে বড়”)
থাকলে; invariant ছাড়া in-order এলোমেলো ক্রম মাত্র।
“Level-order-ও DFS-এরই রকম — stack বদলে queue বসালেই হয়ে যায়।”
Level-order depth-first family-র সদস্যই না — এটা BFS, ভিন্ন ভ্রমণ-দর্শন। Queue-ই natural ইঞ্জিন (FIFO-ই level-ধারাবাহিকতা); stack দিয়ে করতে গেলে per-level batching-এর জটিলতা। Tree→graph-এ গেলে এই পার্থক্য আরো জরুরি — shortest path BFS-এর জন্মসিদ্ধি, DFS ভুল দেয়।
“'Complete' আর 'full'-এ ব্যাপার একই।”
Full = প্রতি node-এ 0 বা ২ child; Complete = উপরের levels ভরা + শেষ level বাঁ থেকে ভরা (position-নিয়ম); Perfect = দুটোই + সব leaf এক depth-এ। Complete-এর বিশেষ দাম: একমাত্র এই আকৃতিতেই tree array-index arithmetic-এ encode হয় — pointer-খরচ শূন্য。
বুঝেছেন কি না দেখুন
1Tree-টার জন্য লিখুন: (ক) প্রতিটা node-এর depth, (খ) tree height,
(গ) চার traversal-এর output।
1
/ \
2 3
\ \
4 5
স্মরণ
1
/ \
2 3
\ \
4 5(ক) depth: 1→0; 2,3→1; 4,5→2। (খ) height = 2।
(গ) pre: 1 2 4 3 5; in: 2 4 1 3 5 (2-র left নেই, তাই 2 আগে);
post: 4 2 5 3 1; level: 1 2 3 4 5।
(Tree-টা full না, complete-ও না — 4 বসেছে 2-এর ডানে — degenerate-ও না। বাস্তবের tree কোনো পরিচ্ছন্ন বিভাগে পড়েই না।)
2Pre-order A B D C, in-order B D A C — tree পুনর্গঠন করুন।
শুধু pre+post দিলে কি সবসময় সম্ভব?
যুক্তি
A B D C, in-order B D A C — tree পুনর্গঠন করুন।
শুধু pre+post দিলে কি সবসময় সম্ভব?Root = pre-এর প্রথম A; in-order-তে বাঁয়ে B D, ডানে C। বাঁ
subtree-র pre B D, in B D: root B, বাঁয়ে কিছু নেই → D হলো
B-র ডান child। ডান subtree: node C একা। ফল: A-র বাঁয়ে B
(ডানে D), ডানে C।
Pre+post দিয়ে না — single-child chain-এ child বাঁয়ে নাকি ডানে, দুই তালিকাই ধরতে পারে না। Ambiguity কাটে in-order জড়ালে — সেটাই বাঁ-ডান সীমানা স্পষ্ট করে।
3Tree-এর সব node free() করতে চান। কোন traversal, কেন? ভুলটা করলে
ঠিক কী বিপদ?
প্রয়োগ
Post-order: free-র আগে child-রা — তাদের address parent-এর pointer-এ! Pre/in-order-তে parent মাঝপথে free হলে use-after-free + leak। Compiler shutdown-path-এ ঠিক এই সিদ্ধান্তই থাকে。
4Perfect tree-র height h (edge convention) হলে মোট node ও leaf কত?
h=10 হিসাব করুন। উল্টো: ১০ লাখ node-এর perfect tree-র height?
যুক্তি
Level i-তে 2^i node; মোট 2^(h+1) − 1, leaf 2^h। h=10: মোট
2047, leaf 1024। উল্টোদিকে 2^(h+1) − 1 = 10⁶ → h ≈ 19।
বিস্ময়টাই tree-র অর্থনীতি: ২০ level-এ ১০ লাখ node — linear scan-এর ১০ লাখ touch-এর বিপরীতে ~২০ pointer-touch। (প্রতি hop cache hit না — সেই হিসাব পরের লেসনগুলোর খাবার।)
5Expression tree-র post-order output 8 2 ^ 3 + (^ = power)। Infix
কী? In-order দিয়ে পুনর্গঠন কেন বিপজ্জনক?
ডিজাইন
8 2 ^ 3 + (^ = power)। Infix
কী? In-order দিয়ে পুনর্গঠন কেন বিপজ্জনক?Stack দিয়ে: 8,2 push; ^ → 8^2 push; 3 push; + →
(8^2)+3। Infix: (8 ^ 2) + 3 = 67। In-order দিত 8 ^ 2 + 3
— এখানে ঠিক (precedence মিলেছে), কিন্তু (8−2)*3 tree-তে দিত
8 − 2 * 3 — মান ২, সঠিক ১৮। Grouping তথ্য হারায় বলে IR-এ postfix
(RPN) প্রিয় — parenthesis-ই লাগে না।
এরপর কী
এরপর কী
আজ tree-র ভাষা: node, height, depth, চার আকৃতি, চার traversal, Catalan-এর exponential তালিকা। পরের লেসনে “বাঁয়ে ছোট, ডানে বড়” বসলে tree → Binary Search Tree — খোঁজা, ঢোকানো, মোছা log₂ n-এ। তারপর মোচড়: sorted input-এ degenerate হয়ে linked list — security attack পর্যন্ত। ঠেকাতে rotation, তার ওপর AVL ও red-black-এর রাজ্য। আজকের build-এর iterative traversal-গুলোই সেখানে validator হবে — ফেলে দেবেন না।
আরও পড়ুন
- Introduction to Algorithms, 3rd ed., Section 12.1 — What is a binary search tree? — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · Binary tree-এর formal definition আর traversal-এর theorem — এই লেসনের in-order অংশের ভিত্তি
- Algorithms, 4th ed., Section 3.2 — Binary Search Trees — Robert Sedgewick, Kevin Wayne · Traversal implementation-এর ব্যবহারিক রীতি আর iterator ডিজাইন
- The Art of Computer Programming, Vol. 1, Section 2.3 — Trees — Donald E. Knuth · Tree-এর vocabulary, binary ও general tree-র সম্পর্ক, আর Catalan number-এর ক্লাসিক আলোচনা