সরাসরি পর্বে যান
পর্ব ২৬ · ৭২-এর মধ্যে · মডিউল ৪

Tree — পরিভাষা, উপস্থাপন আর traversal

Trees — Terminology, Binary Tree Types, Representations, Preorder/Inorder/Postorder/Level-Order Traversal & Classic Tree Problems

Taught in Bangla, this lesson introduces trees: root, parent, leaf, depth and height, full, complete, perfect and degenerate binary trees, the node-count and height bounds, four memory representations, recursive and stack-based preorder, inorder and postorder traversals, queue-based level order, rebuilding a tree from preorder and inorder, and classic problems like height, diameter, LCA and expression trees in C++ and Python.

এই পর্বে যা শিখবেন

  • পরিবার, ফোল্ডার, সরকারি দপ্তর আর ওয়েবপেজের মধ্যে tree চেনা, আর tree-র প্রতিটি পরিভাষা নিখুঁতভাবে বলা
  • Full, complete, perfect আর degenerate binary tree আলাদা করা; node-সংখ্যা আর উচ্চতার সম্পর্ক প্রমাণ করা
  • Tree মেমরিতে রাখার চারটি উপায় আর কোনটি কখন ভালো, তা বোঝা
  • চারটি traversal হাতে trace করা — recursion দিয়ে, stack দিয়ে আর queue দিয়ে — আর তাদের সময় ও জায়গার হিসাব দেওয়া
  • Preorder + inorder থেকে tree বানানো, tree serialize করা, আর height, diameter, LCA, expression tree-র মতো সমস্যা O(n)-এ সমাধান করা

১ · Tree কী — পরিবার, ফোল্ডার আর সরকারি দপ্তরের ক্রমবিন্যাস

এ পর্যন্ত এই কোর্সে যত ডেটা স্ট্রাকচার দেখেছেন — array, linked list, stack, queue — সবই লাইনের মতো: প্রতিটি উপাদানের আগে একজন, পরে একজন। কিন্তু পৃথিবীর অনেক তথ্য লাইনে সাজানো থাকে না, থাকে ধাপে ধাপে ভাগ হয়ে। দাদার কয়েকজন ছেলেমেয়ে, তাঁদের প্রত্যেকের আবার ছেলেমেয়ে — একটি বংশতালিকা (family tree). আপনার কম্পিউটারের D: ড্রাইভে কয়েকটি ফোল্ডার, প্রতিটির ভেতরে আবার ফোল্ডার আর ফাইল। এ রকম “ওপর থেকে নিচে ভাগ হওয়া” কাঠামোকে বলে ক্রমবিন্যাস বা hierarchy, আর এর ডেটা স্ট্রাকচারের নাম tree (ট্রি).

একটি সরকারি উদাহরণ ভাবুন। প্রাথমিক ও গণশিক্ষা মন্ত্রণালয়ের অধীনে প্রাথমিক শিক্ষা অধিদপ্তর; অধিদপ্তরের অধীনে প্রতিটি জেলায় জেলা প্রাথমিক শিক্ষা অফিস; আর প্রতিটি জেলা অফিসের অধীনে কয়েকটি উপজেলা শিক্ষা অফিস। একটি নির্দেশ মন্ত্রণালয় থেকে নিচে নামে, আর প্রতিবেদন উপজেলা থেকে ওপরে ওঠে — কিন্তু কোনো উপজেলা অফিস একসাথে দুটি জেলা অফিসের অধীনে থাকে না। এই “প্রত্যেকের ঠিক একজন ঊর্ধ্বতন” নিয়মটিই tree-র প্রাণ।

প্রাথমিক ও গণশিক্ষা মন্ত্রণালয়                ← root
└── প্রাথমিক শিক্ষা অধিদপ্তর
    ├── জেলা প্রাথমিক শিক্ষা অফিস, রাজশাহী
    │   ├── উপজেলা শিক্ষা অফিস, পবা           ← leaf
    │   └── উপজেলা শিক্ষা অফিস, গোদাগাড়ী      ← leaf
    └── জেলা প্রাথমিক শিক্ষা অফিস, সিলেট
        └── উপজেলা শিক্ষা অফিস, জৈন্তাপুর       ← leaf

প্রোগ্রামিংয়ের জগতে tree প্রায় সবখানে। Windows Explorer-এর বাঁ পাশের ফোল্ডার-গাছ একটি tree. একটি ওয়েবপেজ খুললে ব্রাউজার HTML পড়ে একটি tree বানায়, যার নাম DOM (Document Object Model): <html>-এর নিচে <head> আর <body>, <body>-এর নিচে <div>, তার নিচে <p>. JSON ফাইল, কম্পাইলারের কাছে আপনার কোড (syntax tree), ডেটাবেসের index (পর্ব ৩০-এর B-tree), এমনকি পর্ব ০৭-এর recursion tree — সবই tree.

সংজ্ঞা — দুইভাবে

Recursive সংজ্ঞা (প্রোগ্রামারের প্রিয়): একটি tree হয় খালি, নয়তো একটি বিশেষ node — root — আর তার নিচে শূন্য বা একাধিক আলাদা আলাদা tree, যাদের বলে subtree. লক্ষ করুন, সংজ্ঞার ভেতরেই “tree” শব্দটি আছে: বড় tree ছোট tree দিয়ে তৈরি। এজন্যই tree-র প্রায় সব অ্যালগরিদম recursion দিয়ে দুই-তিন লাইনে লেখা যায় (পর্ব ০৭).

গ্রাফের সংজ্ঞা (গণিতবিদের প্রিয়, পর্ব ৩৯-এ বিস্তারিত): tree হলো এমন একটি graph যা connected (যেকোনো দুই node-এর মধ্যে পথ আছে) আর যাতে কোনো চক্র (cycle) নেই। দুই node-কে জোড়া রেখার নাম edge. যেকোনো একটি node-কে root বেছে “ঝুলিয়ে” দিলেই rooted tree পাওয়া যায় — আর এই পর্বের সব tree rooted.

মূল তথ্য

n টি node-এর যেকোনো tree-তে edge ঠিক n − 1 টি। প্রমাণ এক লাইনের: root ছাড়া প্রতিটি node-এর ঠিক একটি parent, আর প্রতিটি edge ঠিক একজন সন্তানকে তার parent-এর সাথে জোড়ে। তাই edge আর “root নয় এমন node” — দুটি এক-এক মিলে যায়, সংখ্যা n − 1. একটি edge কম হলে graph ভেঙে দুই টুকরো হতো; একটি বেশি হলে কোথাও চক্র তৈরি হতো। কনটেস্টে “n টি শহর, n − 1 টি রাস্তা, সব শহর যুক্ত” পড়লেই বুঝবেন — এটি একটি tree.

২ · Tree-এর পরিভাষা — root থেকে leaf পর্যন্ত

Tree-র শব্দগুলো এসেছে দুই জায়গা থেকে: গাছ (root, leaf, branch) আর পরিবার (parent, child, sibling, ancestor). মজার ব্যাপার, কম্পিউটার বিজ্ঞানে গাছটি উল্টো আঁকা হয় — শিকড় ওপরে, পাতা নিচে। এই পর্ব জুড়ে নিচের tree-টি আমাদের সঙ্গী থাকবে:

        1
      /   \
    2       3
   / \       \
  4   5       6
     / \
    7   8
এই পর্বের প্রধান উদাহরণ: ৮টি node, ৭টি edge, উচ্চতা ৩।
শব্দমানেওপরের tree-তে
Rootসবার ওপরের node, যার কোনো parent নেই1
Parent / childএকটি edge-এর ওপরের প্রান্ত parent, নিচেরটি child (সন্তান)5-এর parent 2; 5-এর সন্তান 7, 8
Siblingএকই parent-এর সন্তানরা — ভাইবোন4 আর 5; 7 আর 8
Ancestor / descendantroot-এর দিকে যাওয়ার পথে যারা পড়ে তারা পূর্বপুরুষ; নিচের দিকে যারা, তারা উত্তরসূরি7-এর ancestor 5, 2, 1; 2-এর descendant 4, 5, 7, 8
Leafযার কোনো সন্তান নেই4, 7, 8, 6
Internal nodeঅন্তত একটি সন্তান আছে এমন node1, 2, 3, 5
Pathedge ধরে ধরে একটি node থেকে আরেকটিতে যাওয়ার ক্রম; দৈর্ঘ্য = edge-এর সংখ্যা4 → 2 → 5 → 7, দৈর্ঘ্য 3
Depthroot থেকে ওই node পর্যন্ত পথের দৈর্ঘ্যdepth(1) = 0, depth(6) = 2, depth(7) = 3
Heightওই node থেকে নিচের সবচেয়ে গভীর leaf পর্যন্ত পথের দৈর্ঘ্য; root-এর height = tree-র heightheight(5) = 1, height(3) = 1, height(1) = 3
Levelএকই depth-এর সব node মিলে একটি স্তরlevel 2: 4, 5, 6
Degreeএকটি node-এর সন্তান-সংখ্যাdegree(1) = 2, degree(3) = 1, leaf-এর 0

নিচের ডায়াগ্রামে এই শব্দগুলো একে একে tree-র ওপর দেখুন। “পরে” চাপুন:

প্রধান উদাহরণের treeসবুজ = এই ধাপের node

            
এই ধাপে
শব্দ
উত্তর
  1. Root. 1 সবার ওপরে, তার কোনো parent নেই। root থেকে নিজের দূরত্ব শূন্য, তাই depth(1) = 0. প্রতিটি tree-তে root ঠিক একটি।
  2. Parent, child, sibling. 5-কে ধরুন। তার ঠিক ওপরে 2 — সে 5-এর parent. নিচে 7 আর 8 — 5-এর সন্তান। 4-এর parent-ও 2, তাই 4 আর 5 ভাইবোন (sibling). লক্ষ করুন, 5-এর parent একটিই, কিন্তু সন্তান একাধিক হতে পারে।
  3. Ancestor আর depth. 6 থেকে ওপরে উঠুন: 3, তারপর 1. এরা 6-এর ancestor. পথ 1 → 3 → 6-এ edge দুটি, তাই depth(6) = 2. Depth মাপা হয় ওপর থেকে নিচে, root থেকে।
  4. Subtree আর descendant. 2-কে root ধরে তার নিচের সবাইকে নিলে পাওয়া যায় subtree(2) = {2, 4, 5, 7, 8} — নিজেই একটি পূর্ণ tree. 2 বাদে বাকিরা 2-এর descendant. Recursion-এর মূল কথা এখানেই: বড় tree-র প্রশ্ন ভাঙে ছোট subtree-র প্রশ্নে।
  5. Leaf আর internal node. 4, 7, 8, 6-এর কোনো সন্তান নেই — এরা leaf. বাকি 1, 2, 3, 5 internal node. ফোল্ডারের উপমায়: leaf হলো ফাইল বা খালি ফোল্ডার।
  6. Level. একই depth-এর node মিলে একটি level: level 0-এ শুধু 1, level 1-এ 2 আর 3, level 2-এ 4, 5, 6, আর level 3-এ 7, 8. কেউ কেউ level গোনেন 1 থেকে — বই পড়ার সময় সংজ্ঞাটি দেখে নিন।
  7. Height আর edge-এর হিসাব. root থেকে সবচেয়ে গভীর leaf (7 বা 8) পর্যন্ত পথ 1 → 2 → 5 → 7, দৈর্ঘ্য 3 — তাই tree-র height 3. Height মাপা হয় নিচ থেকে ওপরে: leaf-এর height 0, height(5) = 1, height(2) = 2. আর মোট edge 8 − 1 = 7, অংশ ১-এর সূত্রমতো।

Height আর depth গুলিয়ে ফেলবেন না, আর “edge গুনছি না node” আগে ঠিক করুন। এই কোর্সে height আর depth দুটোই edge-এর সংখ্যা: একটি node-এর tree-র height 0, আর খালি tree-র height −1 ধরা হয়। কিছু বই আর অনেক অনলাইন judge height মাপে node-এর সংখ্যায় — তখন একটি node-এর tree-র height 1. দুটির পার্থক্য সবসময় ঠিক ১, কিন্তু পরীক্ষায় বা কনটেস্টে ভুল সংজ্ঞা নিলে পুরো উত্তর এক ঘর সরে যায়। প্রশ্নে সংজ্ঞা না থাকলে নিজের ধরে নেওয়া সংজ্ঞাটি লিখে দিন।

৩ · Binary tree আর তার প্রকারভেদ

সাধারণ tree-তে একটি node-এর যত খুশি সন্তান থাকতে পারে — একটি জেলায় যতগুলো উপজেলা। কিন্তু অ্যালগরিদমে সবচেয়ে বেশি ব্যবহৃত হয় binary tree: প্রতিটি node-এর সর্বোচ্চ দুটি সন্তান, আর তাদের নাম আলাদা — বাঁ সন্তান আর ডান সন্তান. “বাঁ” আর “ডান” এখানে শুধু ছবির ব্যাপার নয়, সংজ্ঞার অংশ: শুধু বাঁ সন্তানওয়ালা 1 → 2 আর শুধু ডান সন্তানওয়ালা 1 → 2 দুটি আলাদা binary tree. প্রধান উদাহরণের tree-টিও একটি binary tree; সেখানে 3-এর শুধু ডান সন্তান আছে।

Full (proper)

প্রতিটি node-এর সন্তান হয় 0, নয় 2 — কখনো একটি নয়। Expression tree (অংশ ৯) এমন: প্রতিটি operator-এর দুটি operand.

Complete

শেষ level ছাড়া সব level পুরো ভরা, আর শেষ level বাঁ থেকে ফাঁক ছাড়া ভরা। পর্ব ২৩-এর heap ঠিক এটি।

Perfect

সব internal node-এর দুটি সন্তান আর সব leaf একই level-এ। উচ্চতা h হলে node ঠিক 2h+1 − 1 টি।

Degenerate (skewed)

প্রতিটি internal node-এর একটিই সন্তান — tree আসলে একটি linked list. উচ্চতা n − 1, সবচেয়ে খারাপ অবস্থা।

   Full           Complete         Perfect          Degenerate
    o                o                o              o
   / \             /   \            /   \             \
  o   o           o     o          o     o             o
     / \         / \   /          / \   / \           /
    o   o       o   o o          o   o o   o         o
                                                      \
 (0 বা 2 সন্তান)  (শেষ level বাঁয়ে) (সব level ভরা)       o

প্রতিটি perfect tree একই সাথে full আর complete; কিন্তু উল্টোটা সত্য নয়। ওপরের “Full” ছবিটি complete নয় (level 1-এর বাঁ node-এর সন্তান নেই অথচ ডানের আছে), আর “Complete” ছবিটি full নয় (একটি node-এর একটিই সন্তান)। প্রধান উদাহরণের tree কোনোটিই নয়: 3-এর একটি সন্তান, তাই full নয়; level 2-এ 3-এর বাঁ ঘর ফাঁকা, তাই complete-ও নয়।

নামের গোলমাল আছে। কিছু পুরোনো বইয়ে “full binary tree” বলতে perfect tree বোঝায়, আবার কোথাও “complete” বলতে perfect. এই কোর্সের সংজ্ঞাগুলো এখনকার বেশিরভাগ পাঠ্যবই আর অনলাইন judge-এর সাথে মেলে। পরীক্ষায় শব্দটির পাশে এক লাইনে সংজ্ঞাটি লিখে দিলে আর ভুল বোঝার সুযোগ থাকে না।

কতগুলো node, কত উচ্চতা — তিনটি প্রমাণ

(ক) level d-তে সর্বোচ্চ 2d টি node. Induction (পর্ব ০৩): level 0-তে শুধু root, 20 = 1. level d-তে সর্বোচ্চ 2d টি থাকলে, প্রত্যেকের সর্বোচ্চ দুটি সন্তান, তাই level d + 1-এ সর্বোচ্চ 2 · 2d = 2d+1.

(খ) উচ্চতা h হলে মোট node সর্বোচ্চ 2h+1 − 1. সব level যোগ করুন: 1 + 2 + 4 + … + 2h = 2h+1 − 1 (পর্ব ০৬-এর geometric series). সমতা হয় ঠিক তখন, যখন প্রতিটি level পুরো ভরা — অর্থাৎ perfect tree-তে। উচ্চতা 3-এর binary tree-তে তাই বড়জোর 15টি node; আমাদের উদাহরণে আছে ৮টি।

(গ) n টি node-এর binary tree-র উচ্চতা অন্তত ⌊log₂ n⌋. (খ) থেকে n ≤ 2h+1 − 1, অর্থাৎ 2h+1 ≥ n + 1, তাই h ≥ log₂(n + 1) − 1; h পূর্ণসংখ্যা বলে h ≥ ⌈log₂(n + 1)⌉ − 1, যা ঠিক ⌊log₂ n⌋-এর সমান। উদাহরণে n = 8: ⌊log₂ 8⌋ = 3 — আমাদের tree-র উচ্চতা ঠিক এই সর্বনিম্ন মানেই। আর সবচেয়ে বেশি উচ্চতা? degenerate tree-তে n − 1. অর্থাৎ একই n টি node-এ উচ্চতা log n থেকে n পর্যন্ত যেকোনো কিছু হতে পারে. দশ লাখ node-এ এটি ১৯ বনাম ৯,৯৯,৯৯৯ — পর্ব ২৭-এর BST কেন মাঝে মাঝে ধীর হয়ে যায়, আর পর্ব ২৮–২৯-এ কেন ভারসাম্য রাখার এত আয়োজন, তার মূল কারণ এটিই।

আরেকটি সুন্দর তথ্য: full binary tree-তে leaf-এর সংখ্যা = internal node-এর সংখ্যা + 1. I টি internal node থাকলে প্রত্যেকের ঠিক দুটি সন্তান, তাই edge = 2I. আবার edge = n − 1 = I + L − 1. দুটি মিলিয়ে 2I = I + L − 1, অর্থাৎ L = I + 1. ক্রিকেটের নকআউট টুর্নামেন্টে ১৬টি দল (leaf) থাকলে ম্যাচ (internal node) হয় ঠিক ১৫টি — প্রতিটি ম্যাচে একটি দল বাদ পড়ে, আর শেষে একটি দল টিকে থাকে।

৪ · Tree মেমরিতে রাখা — চারটি উপস্থাপন

কাগজে tree আঁকা সহজ; কিন্তু কম্পিউটারের মেমরি তো একটি লম্বা সারি (পর্ব ১০). সেই সারিতে “কে কার সন্তান” কীভাবে রাখবেন? চারটি প্রচলিত উপায় আছে, আর প্রতিটির শক্তি আলাদা।

(১) Linked node — প্রতিটি node-এ দুটি pointer

পর্ব ১২-এর linked list-এ প্রতিটি node-এ ছিল একটি next pointer; binary tree-তে থাকে দুটি — left আর right. সন্তান না থাকলে pointer খালি (nullptr বা None). দরকার হলে ওপরে ওঠার জন্য তৃতীয় একটি parent pointer-ও রাখা যায়।

struct Node {
    int val;
    Node* left  = nullptr;     // left child, or nullptr
    Node* right = nullptr;     // right child, or nullptr
    // Node* parent = nullptr; // optional: lets you walk upwards
};
class Node:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

root = Node(1, Node(2, Node(4), Node(5, Node(7), Node(8))), Node(3, None, Node(6)))

এটিই সবচেয়ে নমনীয়: যেকোনো আকারের tree, node যোগ-বাদ O(1)-এ (জায়গা জানা থাকলে). খরচ হলো প্রতি node-এ দুটি pointer (৬৪-বিট মেশিনে ১৬ byte) আর মেমরিতে ছড়িয়ে থাকা node — cache-এর জন্য খারাপ (পর্ব ১০, ১২). আর একটি মজার হিসাব: n টি node-এ মোট 2n টি pointer-ঘর, কিন্তু edge মাত্র n − 1 টি — তাই n + 1 টি pointer সবসময় খালি. এই অব্যবহৃত ঘরগুলো কাজে লাগানোর বুদ্ধি থেকেই এসেছে threaded tree আর Morris traversal (অংশ ৬).

(২) Array — heap-এর মতো index দিয়ে

পর্ব ২৩-এ দেখেছেন: complete binary tree-কে level ধরে বাঁ থেকে ডানে একটি array-তে রাখলে কোনো pointer লাগে না — 0-based index i-এর সন্তান 2i + 1 আর 2i + 2, parent (i − 1)/2. যেকোনো binary tree-ও এভাবে রাখা যায়, শুধু অনুপস্থিত node-এর ঘর খালি থাকে। আমাদের উদাহরণে 6 আছে 3-এর ডানে, তাই index 2·2 + 2 = 6; আর 7 আছে 5-এর (index 4) বাঁয়ে, তাই index 9:

index:  0  1  2  3  4  5  6  7  8  9  10
value:  1  2  3  4  5  -  6  -  -  7  8      (- = খালি ঘর)

১১টি ঘরে ৮টি node — চলে। কিন্তু degenerate tree-তে, যেখানে প্রতিটি node শুধু ডান সন্তান, depth d-এর node বসে index 2d+1 − 2-এ; n টি node-এর জন্য array লাগে 2n − 1 ঘরের! তাই এই পদ্ধতি শুধু complete বা প্রায়-complete tree-র জন্য — heap, আর পর্ব ৩৪-এর segment tree.

(৩) Parent array — প্রত্যেকে শুধু তার parent-কে চেনে

node-গুলোর নম্বর 1 থেকে n হলে একটি array par[v] রাখুন — v-এর parent কে (root-এর জন্য 0 বা −1). উদাহরণে: par[2] = 1, par[3] = 1, par[4] = 2, par[5] = 2, par[6] = 3, par[7] = 5, par[8] = 5. মাত্র n টি সংখ্যা! ওপরে ওঠা O(1), তাই “এই উপজেলা অফিস কোন মন্ত্রণালয়ের অধীনে” জাতীয় প্রশ্নে চমৎকার — depth বের করা, দুই node-এর সাধারণ পূর্বপুরুষ খোঁজা (অংশ ৯), আর পর্ব ৩৩-এর union-find পুরোটাই এই উপস্থাপনে চলে। দুর্বলতা: কারও সন্তান কারা, জানতে পুরো array দেখতে হয় — O(n). আর বাঁ-ডানের পার্থক্যও এতে নেই।

(৪) Child list আর left-child right-sibling — যেকোনো সংখ্যক সন্তান

সাধারণ tree-তে (জেলার অধীনে যতগুলো উপজেলা) প্রতিটি node-এর জন্য তার সন্তানদের একটি list রাখুন: vector<int> children[v]. কনটেস্টে tree সাধারণত আসে n − 1 টি edge হিসেবে (“u আর v যুক্ত”); তখন প্রতিটি node-এর প্রতিবেশীদের list (adjacency list, পর্ব ৩৯) বানিয়ে root থেকে DFS করলেই parent আর সন্তান আলাদা হয়ে যায়।

আরেকটি চতুর উপায় left-child right-sibling: প্রতিটি node-এ মাত্র দুটি pointer — firstChild (প্রথম সন্তান) আর nextSibling (পরের ভাই/বোন)। মন্ত্রণালয়ের উদাহরণে অধিদপ্তরের firstChild রাজশাহী জেলা অফিস, আর রাজশাহীর nextSibling সিলেট। যেকোনো সংখ্যক সন্তানের tree এভাবে একটি binary tree হয়ে যায় — Donald Knuth-এর The Art of Computer Programming-এ এই রূপান্তরকে বলা হয়েছে tree আর binary tree-র “স্বাভাবিক সম্পর্ক”. ফাইল সিস্টেম আর পুরোনো অনেক কম্পাইলারের ভেতরে এই কাঠামো পাওয়া যায়।

উপস্থাপনজায়গাসন্তান খোঁজাparent খোঁজাকখন ভালো
Linked node (left, right)Θ(n), প্রতি node-এ ২ pointerO(1)O(1) শুধু parent pointer রাখলেসাধারণ binary tree, BST, ইন্টারভিউ
Array (2i + 1, 2i + 2)Θ(n) complete হলে, 2n পর্যন্ত skewed হলেO(1)O(1)heap, segment tree
Parent arrayΘ(n), প্রতি node-এ ১ সংখ্যাO(n)O(1)ওপরে ওঠা, union-find, LCA
Child list / LCRSΘ(n)degree অনুযায়ীআলাদা রাখলে O(1)সাধারণ tree, কনটেস্টের ইনপুট

৫ · Depth-first traversal — preorder, inorder, postorder

Array-তে সব উপাদান দেখার উপায় একটিই: বাঁ থেকে ডানে। Tree-তে কোনো স্বাভাবিক “বাঁ থেকে ডান” নেই — তাই প্রতিটি node ঠিক একবার দেখার (visit করার) নিয়মটি আমাদেরই ঠিক করতে হয়। এই কাজের নাম tree traversal. সবচেয়ে প্রচলিত তিনটি নিয়ম depth-first: একটি শাখা ধরে যতদূর যাওয়া যায় যাওয়া, তারপর ফিরে এসে পরের শাখা। তিনটির পার্থক্য শুধু একটি প্রশ্নে — root-কে কখন দেখবেন?

নামক্রমপ্রধান উদাহরণে ফলকোথায় কাজে লাগে
Preorderroot → বাঁ → ডান1 2 4 5 7 8 3 6tree কপি করা, serialize করা, ফোল্ডারের সূচি ছাপা
Inorderবাঁ → root → ডান4 2 7 5 8 1 3 6BST-তে সাজানো ক্রম (পর্ব ২৭), expression-এর সাধারণ রূপ
Postorderবাঁ → ডান → root4 7 8 5 2 6 3 1height, size, ফোল্ডারের মোট আকার, tree মুছে ফেলা

নামগুলো মনে রাখার কৌশল: “pre” মানে আগে, “in” মানে মাঝে, “post” মানে পরে — root-এর অবস্থান অনুযায়ী। আর বাঁ subtree সবসময় ডানের আগে। কোডে তিনটি প্রায় হুবহু এক; শুধু “visit” লাইনটি জায়গা বদলায়:

Pseudocode — recursive depth-first traversals (t = a node, NIL = empty)

preorder(t):    if t == NIL: return
                visit(t);  preorder(t.left);  preorder(t.right)

inorder(t):     if t == NIL: return
                inorder(t.left);  visit(t);  inorder(t.right)

postorder(t):   if t == NIL: return
                postorder(t.left);  postorder(t.right);  visit(t)

উপমা — tree-র চারপাশে হাঁটা. ধরুন tree-টি একটি দেয়ালের নকশা, আর আপনি root-এর বাঁ পাশ থেকে শুরু করে দেয়ালে আঙুল রেখে পুরো নকশার চারপাশে ঘড়ির কাঁটার উল্টো দিকে হাঁটছেন — প্রথমে বাঁ দিকে নিচে নেমে। প্রতিটি node-এর পাশ দিয়ে আপনি তিনবার যান: একবার বাঁ পাশ দিয়ে নামার সময়, একবার নিচ দিয়ে (বাঁ subtree শেষ করে ডানে যাওয়ার সময়), আর একবার ডান পাশ দিয়ে ওঠার সময়। প্রথম দেখায় নাম লিখলে preorder, দ্বিতীয়বারে inorder, তৃতীয়বারে postorder. একটিই হাঁটা, তিনটি ক্রম।

এই হাঁটা ঠিক recursion-এর ডাক আর ফেরা। নিচে একটি ছোট tree-তে (A-এর সন্তান B, C; B-এর সন্তান D, E) দেখুন কোন মুহূর্তে কোন ক্রমে কে যুক্ত হয়, আর call stack-এ তখন কে কে আছে:

ছোট treeসবুজ = এখন যে node-এ আছি

            
প্রতিটি ধাপের পর
ঘটনা
call stack (নিচ → ওপর)
preorder
inorder
postorder
  1. A-তে প্রথমবার. preorder(A) ডাকা হলো। root-এর কাজ সবার আগে, তাই preorder-এ A লেখা হলো। inorder আর postorder-কে অপেক্ষা করতে হবে — তাদের আগে বাঁ subtree শেষ হওয়া চাই।
  2. B-তে প্রথমবার. A তার বাঁ সন্তান B-কে ডাকল; stack-এ এখন A আর B. preorder-এ B যুক্ত হলো — preorder হলো “প্রথম দেখাতেই লিখে ফেলা”।
  3. D, একটি leaf. D-র দুই সন্তানই খালি, তাই তার তিনটি মুহূর্ত পরপর আসে: preorder-এ D, (খালি বাঁ শেষ) inorder-এ D, (খালি ডান শেষ) postorder-এ D. তাই সব leaf তিন ক্রমেই একই আপেক্ষিক ক্রমে (বাঁ থেকে ডানে) আসে।
  4. B-তে দ্বিতীয়বার. D শেষ করে B-তে ফিরলাম — বাঁ subtree শেষ, তাই এখন B-এর inorder মুহূর্ত: inorder = D B. Postorder-এ B এখনো নেই, কারণ ডান subtree বাকি।
  5. E, আরেকটি leaf. B ডাকল E-কে। preorder A B D E, inorder D B E, postorder D E. লক্ষ করুন, stack-এ D আর নেই — সে কাজ শেষ করে ফিরে গেছে; stack-এ একসাথে থাকে শুধু root থেকে বর্তমান node পর্যন্ত পথ।
  6. B-তে তৃতীয়বার. B-এর দুই subtree-ই শেষ, তাই postorder-এ B: D E B. এখন B-এর পুরো subtree শেষ, তার frame stack থেকে সরে গেল।
  7. A-তে দ্বিতীয়বার. বাঁ subtree (B-এর পুরো গোষ্ঠী) শেষ, তাই A-এর inorder মুহূর্ত: D B E A. inorder-এ A ঠিক বাঁ গোষ্ঠী আর ডান গোষ্ঠীর মাঝখানে বসে — অংশ ৮-এ এই তথ্যটিই tree ফেরত আনবে।
  8. C, শেষ leaf. A ডাকল C-কে, তিন ক্রমেই C যুক্ত হলো। preorder সম্পূর্ণ: A B D E C. inorder সম্পূর্ণ: D B E A C.
  9. A-তে তৃতীয়বার. শেষে postorder-এ A: D E B C A. Root postorder-এ সবসময় শেষে, preorder-এ সবসময় প্রথমে। মোট ৫টি node, প্রতিটি ঠিক তিনবার ছোঁয়া — কাজ Θ(n); stack-এর সর্বোচ্চ গভীরতা ৩ = height + 1.

বিশ্লেষণ. প্রতিটি node-এর জন্য ফাংশনটি ঠিক একবার ডাকা হয়, আর প্রতিটি খালি সন্তানের জন্যও একবার (সেই n + 1 টি nullptr) — মোট 2n + 1 টি ডাক, প্রতিটিতে O(1) কাজ। তাই সময় Θ(n). বাড়তি জায়গা হলো call stack-এর সর্বোচ্চ গভীরতা, যা height + 1 — অর্থাৎ O(h): ভারসাম্যপূর্ণ tree-তে O(log n), কিন্তু degenerate tree-তে Θ(n). শেষের কথাটি বাস্তবে গুরুত্বপূর্ণ — পরের অংশে দেখবেন কেন।

৬ · Recursion ছাড়া — explicit stack দিয়ে traversal

ধরুন একটি কনটেস্টের ইনপুটে ২,০০,০০০ node-এর tree, আর সেটি আসলে একটি লম্বা শিকল (degenerate). Recursive inorder-এ call stack ২,০০,০০০ frame গভীর হবে। Python-এ ডিফল্ট recursion limit মাত্র ১০০০ — RecursionError. C++-এ সিস্টেমের stack সীমিত (প্রায়ই কয়েক মেগাবাইট), তাই প্রোগ্রাম হঠাৎ crash করতে পারে — পর্ব ০৭-এর stack overflow. সমাধান: recursion যে stack লুকিয়ে ব্যবহার করত, সেটি নিজে হাতে রাখুন। নিজের stack থাকে heap memory-তে, যার সীমা অনেক বড়।

Iterative inorder-এর ধারণা: একটি pointer cur নিয়ে যতদূর সম্ভব বাঁয়ে নামুন, পথের প্রতিটি node stack-এ রাখুন (এরা “বাঁ subtree শেষ হওয়ার অপেক্ষায়”). বাঁয়ে আর যাওয়া না গেলে stack-এর ওপরেরটি তুলুন — তার বাঁ দিক শেষ, তাই এখন তাকে visit করুন — তারপর তার ডান সন্তানে গিয়ে আবার একই কাজ।

vector<int> inorderIter(Node* root) {
    vector<int> out; stack<Node*> st; Node* cur = root;
    while (cur || !st.empty()) {
        while (cur) { st.push(cur); cur = cur->left; }   // go left as far as possible
        cur = st.top(); st.pop();                         // its left side is finished
        out.push_back(cur->val);                          // visit
        cur = cur->right;                                 // now do its right subtree
    }
    return out;
}
vector<int> preorderIter(Node* root) {
    vector<int> out; stack<Node*> st;
    if (root) st.push(root);
    while (!st.empty()) {
        Node* u = st.top(); st.pop(); out.push_back(u->val);
        if (u->right) st.push(u->right);                 // right first, so left pops first
        if (u->left)  st.push(u->left);
    }
    return out;
}
def inorder_iter(root):
    out, st, cur = [], [], root
    while cur or st:
        while cur:                  # go left as far as possible
            st.append(cur); cur = cur.left
        cur = st.pop()              # its left side is finished
        out.append(cur.val)         # visit
        cur = cur.right             # now do its right subtree
    return out

def preorder_iter(root):
    out, st = [], [root] if root else []
    while st:
        u = st.pop(); out.append(u.val)
        if u.right: st.append(u.right)   # right first, so left pops first
        if u.left:  st.append(u.left)
    return out

প্রধান উদাহরণের tree-তে iterative inorder ধাপে ধাপে দেখুন:

প্রধান উদাহরণের treeসবুজ = এই ধাপে push বা visit

            
প্রতিটি ধাপের পর
এই ধাপের কাজ
stack (নিচ → ওপর)
cur
output
  1. বাঁয়ে নামা. cur = 1 থেকে শুরু। 1, তারপর 2, তারপর 4 — প্রত্যেককে stack-এ রেখে বাঁয়ে নামলাম। 4-এর বাঁ সন্তান নেই, cur খালি। stack-এ 1 2 4: এরা সবাই “বাঁ দিক শেষ হলে আমাকে দেখো” বলে অপেক্ষায়।
  2. প্রথম visit. ওপরের 4 তুললাম। তার বাঁ দিক (খালি) শেষ, তাই visit: output 4. তারপর cur = 4-এর ডান = খালি।
  3. 2-এর পালা. cur খালি, তাই ভেতরের লুপ কিছু করে না। stack থেকে 2 — visit. output 4 2. এবার cur = 2-এর ডান সন্তান 5.
  4. আবার বাঁয়ে নামা. 5-কে push করে বাঁয়ে 7, তাকেও push; 7-এর বাঁ নেই। stack থেকে 7 তুলে visit — output 4 2 7. stack-এ এখন 1 5.
  5. 5. 7-এর ডান খালি, তাই stack থেকে 5 — visit. output 4 2 7 5. cur = 5-এর ডান সন্তান 8.
  6. 8. 8-কে push, বাঁয়ে কেউ নেই, আবার pop — visit 8. output 4 2 7 5 8. পুরো বাঁ subtree শেষ, stack-এ শুধু 1.
  7. Root. stack থেকে 1 — visit. inorder-এ root বসে বাঁ subtree-র সবার পরে, ডানের সবার আগে। stack এখন খালি, কিন্তু cur = 3 — তাই লুপ চলবে।
  8. 3. 3-কে push, তার বাঁ নেই, pop করে visit. cur = 3-এর ডান 6.
  9. শেষ. 6-কে push, pop, visit. cur খালি আর stack খালি — লুপ শেষ। output 4 2 7 5 8 1 3 6, recursive inorder-এর সাথে হুবহু এক। stack-এ একসাথে সর্বোচ্চ ৩টি node ছিল — সীমা height + 1 = 4-এর মধ্যে।

কেন ঠিক কাজ করে — invariant. প্রতিবার বাইরের লুপের শুরুতে একটি কথা সত্য: “যা এখনো visit হয়নি, তার সঠিক inorder ক্রম হলো — আগে cur-এর পুরো subtree-র inorder, তারপর stack-এর ওপর থেকে নিচে প্রতিটি node u-এর জন্য: u নিজে, তারপর u-এর ডান subtree-র inorder.” শুরুতে cur = root আর stack খালি — সত্য। ভেতরের লুপ cur-এর বাঁ-পথ stack-এ তোলে, যা এই ক্রম বদলায় না (একটি node-এর inorder মানে বাঁ subtree, নিজে, ডান subtree). তারপর stack-এর ওপরের node-টিই ক্রমের প্রথম জন, তাই তাকে visit করা ঠিক, আর cur = তার ডান সন্তান করলে কথাটি আবার সত্য হয়। cur আর stack দুটোই খালি হলে বাকি কিছু নেই — সব ঠিক ক্রমে visit হয়েছে। প্রতিটি node ঠিক একবার push আর একবার pop হয় — সময় Θ(n), stack-এ সর্বোচ্চ h + 1 টি node — জায়গা O(h), recursion-এর সমান। পার্থক্য শুধু এই যে stack এখন আপনার নিয়ন্ত্রণে।

Iterative preorder আরও সহজ (ওপরের কোড): root push; তারপর বারবার pop করে visit, আর প্রথমে ডান সন্তান, পরে বাঁ সন্তান push — stack উল্টো ক্রমে বের করে বলে বাঁ আগে আসে। Iterative postorder-এর সবচেয়ে সহজ কৌশল: “root → ডান → বাঁ” ক্রমে একটি preorder চালান (preorderIter-এ দুই push-এর ক্রম বদলে), তারপর ফলটি উল্টে দিন — পাওয়া যায় “বাঁ → ডান → root”, অর্থাৎ postorder. এতে Θ(n) বাড়তি জায়গা লাগে; একটি stack আর “শেষে কাকে visit করেছি” pointer দিয়েও লেখা যায়, তবে কোডটি জটিল।

আরও গভীরে — Morris traversal, O(1) জায়গায়: অংশ ৪-এ দেখেছেন, n + 1 টি pointer সবসময় খালি। ১৯৭৯ সালে Joseph M. Morris দেখান, এগুলো সাময়িক “সুতা” (thread) হিসেবে ব্যবহার করে কোনো stack ছাড়াই inorder করা যায়। cur-এর বাঁ subtree থাকলে তার সবচেয়ে ডানের node — inorder-এ cur-এর ঠিক আগের node, predecessor — খুঁজুন। তার right খালি হলে সেখানে cur-এর দিকে একটি সুতা বাঁধুন আর বাঁয়ে নামুন; আর সুতা আগেই থাকলে বুঝবেন বাঁ দিক শেষ — সুতা খুলুন, cur visit করুন, ডানে যান। প্রতিটি edge সর্বোচ্চ কয়েকবার হাঁটা হয়, তাই সময় এখনো O(n), কিন্তু বাড়তি জায়গা O(1). মূল্য: চলার সময় tree সাময়িকভাবে বদলে যায় — একই tree অন্য thread একসাথে পড়লে বা মাঝপথে থামলে বিপদ। এই সুতার ধারণা আরও পুরোনো: A. J. Perlis আর C. Thornton ১৯৬০ সালে স্থায়ী সুতাওয়ালা threaded binary tree প্রস্তাব করেছিলেন; Morris traversal সেটিকে সাময়িক করে।

৭ · Level-order — queue দিয়ে স্তরে স্তরে

চতুর্থ traversal-টি অন্য রকম। সরকারি নির্দেশ যেভাবে ছড়ায় — প্রথমে মন্ত্রণালয়, তারপর সব অধিদপ্তর, তারপর সব জেলা অফিস, শেষে সব উপজেলা অফিস — সেভাবে tree পড়া: level 0, তারপর level 1-এর সবাই বাঁ থেকে ডানে, তারপর level 2. একে বলে level-order traversal, আর এটিই tree-তে breadth-first search (BFS, পর্ব ৪০-এ গ্রাফে বিস্তারিত)।

এখানে stack চলবে না, কারণ stack সবচেয়ে নতুনটিকে আগে দেয় — তাতে গভীরে নেমে যাওয়া হয়। দরকার “আগে এলে আগে” — পর্ব ১৪-এর queue. নিয়ম: root-কে queue-তে রাখুন; তারপর যতক্ষণ queue খালি নয়, সামনের node বের করে visit করুন আর তার সন্তানদের (বাঁ, তারপর ডান) পেছনে ঢোকান। টিকিটের লাইনের মতো: যে আগে লাইনে দাঁড়িয়েছে, তার কাজ আগে হয়; আর নতুন যারা এল, তারা লাইনের শেষে।

প্রধান উদাহরণের treeসবুজ = এই ধাপে queue থেকে বের হলো

            
প্রতিটি ধাপের পর
এই ধাপের কাজ
queue (সামনে → পেছনে)
output
  1. শুরু. queue-তে শুধু root 1. এখনো কিছু visit হয়নি।
  2. Level 0. সামনের 1 বের করে visit; তার সন্তান 2 আর 3 queue-র পেছনে। queue-তে এখন পুরো level 1 — আর কেউ নয়।
  3. Level 1 শুরু. 2 বের হলো, visit; তার সন্তান 4, 5 পেছনে ঢুকল। কিন্তু সামনে এখনো 3 আছে — তাই 4, 5-এর আগে 3-এর পালা আসবে। FIFO-ই স্তরের ক্রম রক্ষা করে।
  4. Level 1 শেষ. 3 বের হলো; তার শুধু ডান সন্তান 6 ঢুকল। queue 4 5 6 — ঠিক level 2. প্রতিটি মুহূর্তে queue-তে থাকে বড়জোর দুটি পাশাপাশি level-এর node.
  5. 4. 4 leaf, কেউ ঢুকল না। output 1 2 3 4.
  6. 5. 5 বের হলো, ঢুকল 7 আর 8. queue 6 7 8 — level 2-এর শেষজন আর level 3-এর দুজন।
  7. 6. 6 leaf. queue-তে বাকি 7 8 — পুরো level 3.
  8. শেষ. 7 আর 8 বের হলো, দুজনেই leaf. queue খালি — শেষ। output 1 2 3 4 5 6 7 8: level ধরে বাঁ থেকে ডানে। queue-র সর্বোচ্চ আকার ছিল ৩।

বিশ্লেষণ. প্রতিটি node একবার ঢোকে, একবার বের হয় — সময় Θ(n). জায়গা queue-র সর্বোচ্চ আকার, যা tree-র সবচেয়ে চওড়া level-এর কাছাকাছি — O(w), যেখানে w = সর্বোচ্চ চওড়া। এখানে DFS-এর সাথে একটি সুন্দর বৈপরীত্য: perfect tree-তে শেষ level-এ প্রায় n/2 টি node, তাই level-order-এর জায়গা Θ(n) কিন্তু DFS-এর মাত্র Θ(log n); আর degenerate tree-তে উল্টো — DFS-এর Θ(n), level-order-এর O(1). কোন traversal ব্যবহার করবেন, তা অনেক সময় tree-র আকৃতির ওপর নির্ভর করে।

Level আলাদা করে দরকার হলে (যেমন “প্রতিটি level আলাদা লাইনে ছাপুন” বা “প্রতিটি level-এর ডানের node”), লুপের শুরুতে queue-র আকার k মনে রাখুন আর ঠিক k টি node বের করুন — এই k টি-ই একটি পুরো level. অনুশীলন ৪-এ এটি লাগবে।

কোডে — চারটি traversal, height আর diameter একসাথে

এবার প্রধান উদাহরণের tree বানিয়ে চারটি traversal ছাপুন, আর সাথে height আর diameter (tree-র যেকোনো দুই node-এর মধ্যে সবচেয়ে লম্বা পথ, edge-এ মাপা) — অংশ ৯-এ diameter-এর যুক্তি বিস্তারিত। C++-এ node-গুলো একটি vector-এ রাখা হয়েছে, যাতে new/delete ছাড়াই pointer দিয়ে জোড়া যায়:

#include <algorithm>
#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;
struct Node { int val = 0; Node* left = nullptr; Node* right = nullptr; };
using V = vector<int>;
void pre(Node* t, V& o)  { if (t) { o.push_back(t->val); pre(t->left, o); pre(t->right, o); } }
void in(Node* t, V& o)   { if (t) { in(t->left, o); o.push_back(t->val); in(t->right, o); } }
void post(Node* t, V& o) { if (t) { post(t->left, o); post(t->right, o); o.push_back(t->val); } }
V level(Node* t) {
    V o; queue<Node*> q; if (t) q.push(t);
    while (!q.empty()) {
        Node* u = q.front(); q.pop(); o.push_back(u->val);
        if (u->left) q.push(u->left);
        if (u->right) q.push(u->right);
    }
    return o;
}
pair<int, int> heightDiam(Node* t) {      // (height, diameter) in edges; empty tree = (-1, 0)
    if (!t) return {-1, 0};
    auto [hl, dl] = heightDiam(t->left); auto [hr, dr] = heightDiam(t->right);
    return {1 + max(hl, hr), max({dl, dr, hl + hr + 2})};   // hl + hr + 2 = longest path bending at t
}
void show(const char* name, const V& o) { cout << name; for (int x : o) cout << ' ' << x; cout << '\n'; }
int main() {
    vector<Node> pool(9);                 // pool[v] is the node that holds value v
    for (int v = 1; v <= 8; ++v) pool[v].val = v;
    auto link = [&](int p, int l, int r) { pool[p].left = l ? &pool[l] : nullptr; pool[p].right = r ? &pool[r] : nullptr; };
    link(1, 2, 3); link(2, 4, 5); link(3, 0, 6); link(5, 7, 8); Node* root = &pool[1];
    V a, b, c; pre(root, a); in(root, b); post(root, c);
    show("preorder: ", a); show("inorder:  ", b); show("postorder:", c); show("level:    ", level(root));
    auto [h, d] = heightDiam(root); cout << "height: " << h << "  diameter: " << d << '\n';
}

আউটপুট:

preorder:  1 2 4 5 7 8 3 6
inorder:   4 2 7 5 8 1 3 6
postorder: 4 7 8 5 2 6 3 1
level:     1 2 3 4 5 6 7 8
height: 3  diameter: 5

চারটি ক্রম অংশ ৫ আর ৭-এর ডায়াগ্রামের সাথে মেলে। তিনটি recursive ফাংশনের পার্থক্য শুধু o.push_back(t->val) কোথায় বসেছে। heightDiam একটি postorder: আগে দুই সন্তানের (height, diameter) জোড়া আনে, তারপর নিজেরটি বানায়। খালি tree-র height −1 ধরায় leaf-এর height 1 + max(−1, −1) = 0, আর “t-তে বাঁক নেওয়া পথ” hl + hr + 2 leaf-এ 0 — আলাদা কোনো শর্ত লাগে না। Diameter 5 হলো পথ 7 → 5 → 2 → 1 → 3 → 6. max({...}) তিনটি মানের সর্বোচ্চ দেয় (initializer list থেকে)। সময় প্রতিটি ফাংশনে Θ(n), জায়গা O(h).

from collections import deque

class Node:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def pre(t, o):
    if t: o.append(t.val); pre(t.left, o); pre(t.right, o)
def ino(t, o):
    if t: ino(t.left, o); o.append(t.val); ino(t.right, o)
def post(t, o):
    if t: post(t.left, o); post(t.right, o); o.append(t.val)
def level(t):
    o, q = [], deque([t] if t else [])
    while q:
        u = q.popleft(); o.append(u.val)
        if u.left: q.append(u.left)
        if u.right: q.append(u.right)
    return o
def height_diam(t):                      # (height, diameter) in edges; empty tree = (-1, 0)
    if t is None: return -1, 0
    hl, dl = height_diam(t.left)
    hr, dr = height_diam(t.right)
    return 1 + max(hl, hr), max(dl, dr, hl + hr + 2)   # hl + hr + 2 = longest path bending at t

root = Node(1, Node(2, Node(4), Node(5, Node(7), Node(8))), Node(3, None, Node(6)))
for name, f in [("preorder: ", pre), ("inorder:  ", ino), ("postorder:", post)]:
    o = []; f(root, o); print(name, *o)
print("level:    ", *level(root))
h, d = height_diam(root)
print(f"height: {h}  diameter: {d}")

আউটপুট C++-এর সাথে হুবহু এক:

preorder:  1 2 4 5 7 8 3 6
inorder:   4 2 7 5 8 1 3 6
postorder: 4 7 8 5 2 6 3 1
level:     1 2 3 4 5 6 7 8
height: 3  diameter: 5

Python-এ tree বানানো এক লাইনে: Node(1, Node(2, …), Node(3, None, Node(6))) — বন্ধনীর গঠনই tree-র গঠন। deque-এর popleft() O(1); সাধারণ list-এর pop(0) O(n), তাই level-order-এ কখনো list-কে queue বানাবেন না (পর্ব ১৪). print(name, *o) list-এর মানগুলো ফাঁকা দিয়ে ছাপে। একটি সতর্কতা: এই recursive ফাংশনগুলো ১০০০-এর বেশি গভীর tree-তে RecursionError দেবে — বড় ইনপুটে অংশ ৬-এর iterative রূপ ব্যবহার করুন।

৮ · Traversal থেকে tree ফেরত — preorder আর inorder

উল্টো প্রশ্ন: শুধু traversal-এর ফল দেওয়া আছে, tree-টি কী ছিল? একটি traversal একা যথেষ্ট নয়। Preorder 1 2 3 দিয়ে ঠিক পাঁচটি আলাদা আকারের tree হয় — 1-এর বাঁয়ে 2, তার বাঁয়ে 3; বা 1-এর দুই পাশে 2 আর 3; ইত্যাদি। কিন্তু preorder আর inorder একসাথে দিলে (সব মান আলাদা হলে) tree ঠিক একটিই। কৌশলটি দুটি পর্যবেক্ষণে:

  • Preorder-এর প্রথম মানটি root — preorder root দিয়েই শুরু হয়।
  • Inorder-এ root-এর বাঁয়ে যারা, তারা ঠিক বাঁ subtree; ডানে যারা, ডান subtree — অংশ ৫-এর ডায়াগ্রামে দেখেছেন, inorder-এ root বসে দুই গোষ্ঠীর মাঝখানে।

বাঁ subtree-তে k টি node থাকলে preorder-এ root-এর পরের k টি মান ঠিক বাঁ subtree-র preorder (কারণ preorder প্রথমে পুরো বাঁ subtree শেষ করে, তারপর ডানে যায়), আর বাকিগুলো ডান subtree-র preorder. দুই ভাগেই আবার একই সমস্যা, ছোট আকারে — recursion. প্রধান উদাহরণের দুই traversal থেকে tree ফেরত আনুন:

যতটুকু tree বানানো হয়েছে[ ] = এখনো না-বানানো অংশ (inorder ক্রমে)

            
এই ধাপের subproblem
preorder অংশ
inorder অংশ
নতুন root
inorder-এ ভাগ
  1. Root খোঁজা. preorder-এর প্রথম মান 1 — এটিই root. inorder-এ 1 আছে index 5-এ: তার বাঁয়ে 4 2 7 5 8 (৫টি), ডানে 3 6. তাই preorder-এ 1-এর পরের ৫টি — 2 4 5 7 8 — বাঁ subtree-র preorder, আর 3 6 ডানের।
  2. বাঁ subtree. এখন সমস্যা preorder 2 4 5 7 8, inorder 4 2 7 5 8. root 2; inorder-এ 2-এর বাঁয়ে শুধু 4, ডানে 7 5 8. preorder-এ 2-এর পরের ১টি (4) বাঁয়ের, বাকি 5 7 8 ডানের।
  3. আরও নিচে. {4} একা — একটি leaf. ডান দিকে preorder 5 7 8, inorder 7 5 8: root 5, বাঁয়ে 7, ডানে 8 — দুজনেই leaf. বাঁ subtree সম্পূর্ণ।
  4. ডান subtree. preorder 3 6, inorder 3 6. root 3; inorder-এ 3-এর বাঁয়ে কেউ নেই, ডানে 6. তাই 3-এর বাঁ সন্তান নেই — এই তথ্যটি শুধু inorder-ই দিতে পারে।
  5. শেষ. {6} একটি leaf, 3-এর ডানে বসল। tree সম্পূর্ণ — প্রধান উদাহরণের tree-টিই। মোট ৮টি root বাছাই, প্রতিটির inorder-অবস্থান একটি hash map থেকে O(1)-এ — পুরো কাজ O(n).

কেন tree ঠিক একটিই — induction-এ প্রমাণ. n-এর ওপর strong induction. n = 0 হলে খালি tree, একটিই। ধরুন n-এর চেয়ে কম node-এর যেকোনো (preorder, inorder) জোড়া থেকে tree অনন্য। n টি node-এর যেকোনো tree T, যার traversal দুটি দেওয়া জোড়া, তার root অবশ্যই preorder-এর প্রথম মান r — অন্য কিছু হতে পারে না। সব মান আলাদা বলে r inorder-এ ঠিক এক জায়গায়, তাই T-এর বাঁ subtree-র মানের সেট (r-এর বাঁয়ের সবাই) আর আকার k বাধ্যতামূলকভাবে নির্দিষ্ট। তাহলে বাঁ subtree-র preorder আর inorder-ও নির্দিষ্ট (preorder-এর পরের k টি, inorder-এর প্রথম k টি), আর ডানের-ও। দুটিই n-এর চেয়ে ছোট, তাই induction অনুযায়ী দুই subtree অনন্য — ফলে T-ও অনন্য। ∎

একই যুক্তিতে postorder + inorder-ও যথেষ্ট (postorder-এর শেষ মান root), আর level-order + inorder-ও। কিন্তু preorder + postorder যথেষ্ট নয়: 1-এর বাঁয়ে 2 আর 1-এর ডানে 2 — দুটি tree-রই preorder 1 2, postorder 2 1. একমাত্র সন্তান বাঁয়ে না ডানে, তা জানার উপায় নেই। (Full binary tree-তে একমাত্র সন্তান থাকে না, তাই সেখানে এই জোড়াও যথেষ্ট।) আর মান পুনরাবৃত্তি হলে preorder + inorder-ও ব্যর্থ হতে পারে: দুটি node-এর মান 1 হলে 1 1 preorder আর 1 1 inorder — বাঁ না ডান, বলা যায় না।

Serialize — null চিহ্ন দিলে একটি traversal-ই যথেষ্ট

একটি tree ফাইলে লিখে রাখা বা নেটওয়ার্কে পাঠানোর কাজকে বলে serialization; ফেরত আনাকে deserialization. Preorder + inorder দুটি পাঠানো ঝামেলার, আর মান আলাদা হওয়ার শর্ত লাগে। সহজ উপায়: preorder লিখুন, আর প্রতিটি খালি সন্তানের জায়গায় একটি চিহ্ন, যেমন #. প্রধান উদাহরণ হয়ে যায়:

1 2 4 # # 5 7 # # 8 # # 3 # 6 # #

৮টি মান আর ৯টি # (সেই n + 1 টি খালি pointer) — মোট 2n + 1 টি token. ফেরত আনা এক নিঃশ্বাসে: একটি token পড়ুন; # হলে খালি tree ফেরত দিন; নইলে সেই মান দিয়ে node বানান, তারপর recursion দিয়ে আগে বাঁ, পরে ডান subtree পড়ুন। #-গুলো বলে দেয় কোথায় একটি subtree শেষ, তাই এখানে মান পুনরাবৃত্তি হলেও কোনো সমস্যা নেই — দুই দিকেই সময় Θ(n).

নিচের প্রোগ্রাম preorder + inorder থেকে tree বানায়, তারপর নতুন tree-র traversal আবার বের করে মিলিয়ে দেখে, আর serialize করে:

#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
struct Node { int val; Node* left; Node* right; };
vector<Node> pool;                        // node storage (reserved, so pointers stay valid)
unordered_map<int, int> pos; size_t nxt = 0;   // value -> inorder index; next unused preorder position
Node* build(const vector<int>& pre, int lo, int hi) {   // this subtree owns inorder[lo..hi]
    if (lo > hi) return nullptr;
    int v = pre[nxt++], m = pos[v];       // next preorder value is this subtree's root
    pool.push_back({v, nullptr, nullptr}); Node* t = &pool.back();
    t->left = build(pre, lo, m - 1);      // left first: it comes first in preorder
    t->right = build(pre, m + 1, hi); return t;
}
void walk(Node* t, int order, vector<int>& o) {         // order: 0 = pre, 1 = in, 2 = post
    if (!t) return;
    if (order == 0) o.push_back(t->val);
    walk(t->left, order, o);
    if (order == 1) o.push_back(t->val);
    walk(t->right, order, o);
    if (order == 2) o.push_back(t->val);
}
string serialize(Node* t) {               // preorder with '#' for every empty child
    return t ? to_string(t->val) + " " + serialize(t->left) + " " + serialize(t->right) : "#";
}
int main() {
    vector<int> pre{1, 2, 4, 5, 7, 8, 3, 6}, in{4, 2, 7, 5, 8, 1, 3, 6};
    for (int i = 0; i < (int)in.size(); ++i) pos[in[i]] = i;
    pool.reserve(pre.size()); Node* t = build(pre, 0, (int)in.size() - 1);
    vector<int> p, q, r; walk(t, 0, p); walk(t, 1, q); walk(t, 2, r);
    cout << "postorder:"; for (int x : r) cout << ' ' << x;
    cout << "\npreorder matches: " << (p == pre ? "yes" : "no") << "\ninorder matches:  " << (q == in ? "yes" : "no");
    cout << "\nserialized: " << serialize(t) << '\n';
}

আউটপুট:

postorder: 4 7 8 5 2 6 3 1
preorder matches: yes
inorder matches:  yes
serialized: 1 2 4 # # 5 7 # # 8 # # 3 # 6 # #

বানানো tree-র postorder 4 7 8 5 2 6 3 1 — আগের প্রোগ্রামের সাথে মেলে, অর্থাৎ ঠিক সেই tree-ই ফিরে এসেছে; আর দুই traversal আবার বের করে ইনপুটের সাথে মিলিয়ে দেখা হয়েছে। nxt preorder-এর ভেতরে একটি চলন্ত pointer: প্রতিটি build ডাক তার root হিসেবে পরের মানটি নেয়। এজন্যই বাঁ subtree আগে বানাতে হয় — ক্রম উল্টালে preorder-এর মান ভুল subtree-তে চলে যায়। pos map না রেখে প্রতিবার inorder-এ root খুঁজলে degenerate tree-তে কাজ হয় 1 + 2 + … + n = Θ(n²). pool.reserve জরুরি: reserve ছাড়া vector বড় হওয়ার সময় সব node নতুন জায়গায় সরে যায়, আর আগের pointer-গুলো ঝুলন্ত (dangling) হয়ে পড়ে (পর্ব ১০).

class Node:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def build_tree(pre, ino):                # values must be distinct
    pos = {v: i for i, v in enumerate(ino)}
    nxt = iter(pre)                      # preorder is consumed left to right
    def build(lo, hi):                   # this subtree owns ino[lo..hi]
        if lo > hi: return None
        v = next(nxt); m = pos[v]        # next preorder value is this subtree's root
        left = build(lo, m - 1)          # build left first: it comes first in preorder
        return Node(v, left, build(m + 1, hi))
    return build(0, len(ino) - 1)

def walk(t, order, o):                   # order: 0 = pre, 1 = in, 2 = post
    if t is None: return o
    if order == 0: o.append(t.val)
    walk(t.left, order, o)
    if order == 1: o.append(t.val)
    walk(t.right, order, o)
    if order == 2: o.append(t.val)
    return o

def serialize(t):                        # preorder with '#' for every empty child
    return "#" if t is None else f"{t.val} {serialize(t.left)} {serialize(t.right)}"

pre = [1, 2, 4, 5, 7, 8, 3, 6]
ino = [4, 2, 7, 5, 8, 1, 3, 6]
t = build_tree(pre, ino)
print("postorder:", *walk(t, 2, []))
print("preorder matches:", "yes" if walk(t, 0, []) == pre else "no")
print("inorder matches: ", "yes" if walk(t, 1, []) == ino else "no")
print("serialized:", serialize(t))

আউটপুট C++-এর সাথে হুবহু এক:

postorder: 4 7 8 5 2 6 3 1
preorder matches: yes
inorder matches:  yes
serialized: 1 2 4 # # 5 7 # # 8 # # 3 # 6 # #

Python-এ চলন্ত pointer-এর কাজ করছে একটি iterator: next(nxt) প্রতিবার preorder-এর পরের মানটি দেয়। left = build(...) আলাদা লাইনে আগে লেখা হয়েছে, যাতে ডান subtree-র আগে বাঁয়েরটি নিশ্চিতভাবে বানানো হয়। serialize নিজেই একটি preorder: খালি হলে "#", নইলে “মান, বাঁয়ের serialize, ডানের serialize”. f-string-এর জোড়া দেওয়ার কারণে এই সংস্করণটি বড় tree-তে O(n · h) সময় নিতে পারে; বাস্তবে একটি list-এ token জমিয়ে শেষে " ".join করলে Θ(n).

৯ · Tree-এর ক্লাসিক সমস্যা — একই ছাঁচ, ভিন্ন প্রশ্ন

Tree-র বেশিরভাগ ইন্টারভিউ সমস্যা একটি প্রশ্নে নামে: “দুই সন্তানের উত্তর জানা থাকলে নিজের উত্তর কীভাবে বানাব?” এটিই postorder-এর ছাঁচ, আর এর শুদ্ধতা প্রমাণ হয় structural induction-এ — খালি tree-তে ঠিক, আর দুই subtree-তে ঠিক হলে পুরোটায় ঠিক। প্রতিটি node-এ O(1) কাজ হলে সময় Θ(n), জায়গা O(h).

সমস্যাখালি tree-তেদুই সন্তানের উত্তর থেকে নিজের
node গোনা01 + count(L) + count(R)
leaf গোনা0দুই সন্তানই খালি হলে 1, নইলে leaves(L) + leaves(R)
height (edge-এ)−11 + max(height(L), height(R))
সব মানের যোগফল0val + sum(L) + sum(R)
দুটি tree হুবহু এক?দুটিই খালি → হ্যাঁval সমান আর same(L₁, L₂) আর same(R₁, R₂)

Diameter — বাঁক কোথায়, তা খুঁজুন

Tree-র যেকোনো সবচেয়ে লম্বা পথ কোথাও না কোথাও “সবচেয়ে উঁচু” একটি node-এ বাঁক নেয় — পথটি সেই node থেকে বাঁ subtree-তে নামে আর ডান subtree-তে নামে (একদিকে শূন্যও হতে পারে)। যে node t-তে বাঁক, সেখানে সবচেয়ে লম্বা সম্ভাব্য পথ = (বাঁ subtree-র height + 1) + (ডানের height + 1) = hl + hr + 2. তাই diameter = সব node-এ hl + hr + 2-এর সর্বোচ্চ। প্রতিটি node-এ height তো postorder-এ এমনিতেই বের হচ্ছে — একই হাঁটায় সর্বোচ্চটিও রাখা যায়। কোডের heightDiam ঠিক এটিই করে; নিচে নিচ থেকে ওপরে হিসাবটি দেখুন:

প্রধান উদাহরণের treeসবুজ = এই ধাপে হিসাব হলো

            
খালি সন্তানের height = −1
node (hl, hr)
height = 1 + max
বাঁকের পথ hl + hr + 2
এখন পর্যন্ত diameter
  1. Leaf-গুলো. postorder-এ প্রথমে আসে 4, 7, 8. প্রত্যেকের দুই সন্তান খালি (height −1), তাই height 1 + (−1) = 0, আর বাঁকের পথ −1 + −1 + 2 = 0 — একা একটি node, দৈর্ঘ্য শূন্য।
  2. 5. বাঁয়ে 7 (height 0), ডানে 8 (height 0). height(5) = 1. 5-এ বাঁক নেওয়া পথ 0 + 0 + 2 = 2: 7 → 5 → 8. diameter এখন 2.
  3. 2. বাঁয়ে 4 (0), ডানে 5 (1). height(2) = 2. বাঁকের পথ 0 + 1 + 2 = 3: 4 → 2 → 5 → 7. diameter 3.
  4. ডান দিক. 6 একটি leaf — height 0. 3-এর বাঁ খালি (−1), ডানে 6 (0): height(3) = 1, বাঁকের পথ −1 + 0 + 2 = 1 (3 → 6). diameter বদলায়নি, 3-ই আছে।
  5. Root. বাঁয়ে 2 (height 2), ডানে 3 (height 1). height(1) = 3, বাঁকের পথ 2 + 1 + 2 = 5: 7 → 5 → 2 → 1 → 3 → 6. diameter 5 — প্রোগ্রামের আউটপুটের সাথে মেলে। প্রতিটি node একবার, মোট Θ(n).

সাধারণ ভুল ১: “diameter = height(L) + height(R) + 2, শুধু root-এ” — সবচেয়ে লম্বা পথ root দিয়ে না-ও যেতে পারে (অনুশীলন ৩). সাধারণ ভুল ২: প্রতিটি node-এ আলাদা করে height() ডাকা — প্রতিটি ডাক O(n), তাই degenerate tree-তে মোট Θ(n²). একই postorder-এ দুটি মান একসাথে ফেরত দিলে Θ(n). পর্ব ৪৮-এ সাধারণ (binary নয় এমন) tree-তে দুইবার BFS/DFS দিয়ে diameter বের করার আরেকটি সুন্দর পদ্ধতি দেখবেন।

Symmetric কি না — আয়নার প্রশ্ন

একটি tree symmetric, যদি root-এর মাঝ দিয়ে একটি আয়না রাখলে বাঁ অর্ধেক ডান অর্ধেকের প্রতিবিম্ব হয়। প্রশ্নটি দুটি subtree-র ওপর: mirror(a, b) সত্য হয় যদি দুটিই খালি; অথবা দুটিই আছে, মান সমান, আর a-এর বাঁ b-এর ডানের আয়না এবং a-এর ডান b-এর বাঁয়ের আয়না। উত্তর = mirror(root.left, root.right). প্রতিটি node একবার তুলনা হয় — Θ(n). প্রধান উদাহরণ symmetric নয়: root-এর বাঁয়ে 2, ডানে 3 — মানই আলাদা।

Lowest common ancestor (LCA)

দুটি node p আর q-এর lowest common ancestor হলো সেই node, যে দুজনেরই ancestor (একজন নিজেও হতে পারে) আর যে root থেকে সবচেয়ে দূরে — বংশতালিকায় দুই চাচাতো ভাইয়ের “সবচেয়ে কাছের সাধারণ দাদা”. প্রধান উদাহরণে LCA(7, 4) = 2, LCA(7, 6) = 1, LCA(5, 8) = 5. Binary tree-তে একটি সরল recursive সমাধান:

// returns p, q, their LCA, or nullptr, depending on what this subtree contains
Node* lca(Node* t, Node* p, Node* q) {
    if (!t || t == p || t == q) return t;
    Node* L = lca(t->left, p, q);
    Node* R = lca(t->right, p, q);
    if (L && R) return t;          // p on one side, q on the other: t is the split point
    return L ? L : R;              // both (or the only one found) are on one side
}

যুক্তি: ফাংশনটি জানায় t-এর subtree-তে p বা q-এর কোনটি পাওয়া গেছে। দুই দিক থেকেই কিছু এলে p আর q দুই আলাদা subtree-তে — t-ই সেই প্রথম বিন্দু যেখানে তাদের পথ আলাদা হয়, তাই LCA. শুধু এক দিক থেকে এলে উত্তর সেদিকেই। (এখানে ধরে নেওয়া হয়েছে p আর q দুজনেই tree-তে আছে।) প্রতিটি প্রশ্নে O(n). Parent array থাকলে আরেক উপায়: দুজনের depth সমান করে একসাথে ওপরে উঠুন — O(h). লাখো প্রশ্ন থাকলে পর্ব ৪৮-এর binary lifting O(n log n) প্রস্তুতির পর প্রতিটি প্রশ্নে O(log n) দেয়।

Expression tree — postfix-এর গোপন রূপ

গণিতের রাশি (3 + 4) * (5 - 2) একটি expression tree: প্রতিটি internal node একটি operator, প্রতিটি leaf একটি সংখ্যা — আর এটি একটি full binary tree. বন্ধনীর আর দরকার নেই, কারণ গঠনই বলে দেয় কী আগে হবে।

          *
        /   \
      +       -
     / \     / \
    3   4   5   2

preorder  : * + 3 4 - 5 2      (prefix, Polish notation)
inorder   : 3 + 4 * 5 - 2      (বন্ধনী হারিয়ে গেছে — অর্থ বদলে যায়!)
postorder : 3 4 + 5 2 - *      (postfix — পর্ব ১৩-এর stack-এ মান বের করা যায়)

লক্ষ করুন: postorder হুবহু পর্ব ১৩-এর postfix রূপ। Tree-র মান বের করাও একটি postorder: দুই সন্তানের মান আগে, তারপর operator প্রয়োগ — (3 + 4) = 7, (5 − 2) = 3, 7 × 3 = 21. আর উল্টো দিকে, postfix থেকে tree বানানো যায় পর্ব ১৩-এর stack দিয়েই: সংখ্যা পেলে একটি leaf push; operator পেলে দুটি pop (প্রথমটি ডান, দ্বিতীয়টি বাঁ সন্তান), নতুন node বানিয়ে push. শেষে stack-এ একটিই node — root. Inorder থেকে সঠিক রাশি পেতে প্রতিটি operator-এর subtree-কে বন্ধনীতে মুড়তে হয়: ((3 + 4) * (5 - 2)). কম্পাইলার ঠিক এভাবে আপনার কোডকে একটি syntax tree বানায়, আর postorder ধরে মেশিনের নির্দেশ তৈরি করে।

পরীক্ষা ও ইন্টারভিউতে:

  • n টি node-এর binary tree-তে কয়টি null pointer? — 2n ঘরের মধ্যে n − 1 টি edge, তাই n + 1 টি।
  • Preorder আর inorder দেওয়া আছে, postorder বের করুন। — preorder-এর প্রথমটি root, inorder-এ তাকে খুঁজে বাঁ-ডান ভাগ, প্রতিটি ভাগে একই কাজ; শেষে বাঁ, ডান, root ক্রমে লিখুন।
  • Recursion ছাড়া inorder লিখুন। — নিজের stack: বাঁয়ে নামতে নামতে push, তারপর pop-visit-ডানে যাওয়া; O(n) সময়, O(h) জায়গা।
  • Binary tree-র diameter O(n)-এ কীভাবে? — একটি postorder, যা height ফেরত দেয় আর প্রতিটি node-এ hl + hr + 2-এর সর্বোচ্চ রাখে।
  • কোন traversal জোড়া tree অনন্যভাবে নির্ধারণ করে না? — preorder + postorder (একমাত্র সন্তান বাঁয়ে না ডানে, বোঝা যায় না); inorder-সহ জোড়াগুলো করে।

১০ · AI-এর যুগে tree — কোড ছোট, ভুল সূক্ষ্ম

Tree-র কোড সাধারণত পাঁচ-দশ লাইনের, আর AI এগুলো নির্ভুল দেখতে লেখে। কিন্তু ভুল লুকিয়ে থাকে সংজ্ঞায় (height edge-এ না node-এ?), ছোট পরীক্ষায় ধরা পড়ে না এমন জায়গায় (diameter root দিয়ে যায় না), আর বড় ইনপুটে (১,০০,০০০ গভীরতার শিকলে recursion ভেঙে পড়ে)। AI প্রায়ই আত্মবিশ্বাসের সাথে বলে “preorder + postorder থেকে tree অনন্য”, বা প্রতিটি node-এ height() ডেকে O(n²) সমাধানকে O(n) বলে দেয়।

Tree-র কোডে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
height বা diameter node-এ গোনা, অথচ প্রশ্ন চায় edge-এ (বা উল্টো)একটি node-এর tree আর দুই node-এর শিকলে হাতে উত্তর বের করে মিলিয়ে নিন
diameter শুধু root-এ hl + hr + 2অনুশীলন ৩-এর মতো একটি tree দিন, যার লম্বা পথ root এড়িয়ে যায়
প্রতিটি node-এ আলাদা height() ডেকে “O(n)” দাবিdegenerate tree-তে ডাক গুনুন; অথবা n = 105 শিকলে সময় মাপুন
preorder + inorder থেকে বানাতে প্রতিবার linear search, বা ডান subtree আগে বানানোskewed tree-তে Θ(n²) দেখুন; বানানো tree-র traversal আবার বের করে ইনপুটের সাথে মেলান
“preorder + postorder যথেষ্ট” বা পুনরাবৃত্ত মানেও অনন্য বলা1-এর বাঁয়ে 2 বনাম ডানে 2 — দুটির traversal লিখে দেখান
লম্বা tree-তে recursion; Python-এ ১০০০-এর সীমা ভুলে যাওয়া১,০০,০০০ node-এর শিকল দিয়ে চালান; iterative রূপ চান
LCA-তে BST-এর নিয়ম (মান তুলনা) সাধারণ binary tree-তে প্রয়োগমান সাজানো নয় এমন tree-তে চালিয়ে দেখুন

AI-কে সমাধান লিখে দিতে বলার চেয়ে আপনার trace আর যুক্তি পরীক্ষা করতে বলুন, আর প্রতিটি complexity-র দাবির কারণ চান:

আমি DSA শিখছি, এখন binary tree traversal আর diameter. নিচে আমার কোড আর একটি tree-তে আমার হাতে করা trace দিলাম: [এখানে কোড আর trace পেস্ট করুন] পুরো সমাধান নতুন করে লিখে দিও না। শুধু এগুলো করো: ১. আমার height আর diameter edge-এ গোনা না node-এ, কোড দেখে বলো — একটি node-এর tree-তে কী ফেরত আসে? ২. এমন একটি ছোট tree দাও, যার diameter root দিয়ে যায় না, আর দেখাও আমার কোড সেখানে ঠিক উত্তর দেয় কি না। ৩. আমার কোডের time complexity degenerate (শিকলের মতো) tree-তে কত? ধাপে ধাপে ডাকের সংখ্যা গুনে দেখাও। ৪. ১,০০,০০০ গভীরতার tree-তে আমার কোড কেন বা কীভাবে ভেঙে পড়তে পারে, আর iterative করতে কোন stack লাগবে — শুধু ইঙ্গিত দাও।

Tree-র কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Tree-র ধারণা কম্পিউটারের অনেক আগে গণিতে এসেছিল — বিদ্যুতের তার আর রসায়নের অণু থেকে কীভাবে, আর কেন কম্পিউটার বিজ্ঞানে গাছটি উল্টো আঁকা হয়?

Tree প্রথম গাণিতিক বস্তু হিসেবে গুরুত্ব পায় উনিশ শতকে, দুটি একদম ভিন্ন প্রশ্ন থেকে। ১৮৪৭ সালে জার্মান পদার্থবিদ Gustav Kirchhoff বৈদ্যুতিক বর্তনীর (circuit) সমীকরণ সমাধান করতে গিয়ে লক্ষ করেন, একটি তারের জাল থেকে চক্রহীন কিন্তু সবকিছু যুক্ত রাখে এমন উপ-জাল — আজকের ভাষায় spanning tree — বেছে নিলে স্বাধীন সমীকরণগুলো সহজে পাওয়া যায়। তাঁর সেই কাজ থেকেই এসেছে বিখ্যাত matrix-tree theorem, যা একটি graph-এর spanning tree-র সংখ্যা গোনে। পর্ব ৪৫-এর minimum spanning tree সেই ধারণারই উত্তরসূরি।

দ্বিতীয় উৎস ব্রিটিশ গণিতবিদ Arthur Cayley. ১৮৫৭ সালে তিনি “tree” নামটি ব্যবহার করে একটি প্রবন্ধ লেখেন — rooted tree গোনার প্রশ্নে, যা এসেছিল differential operator-এর হিসাব থেকে। পরে, ১৮৭০-এর দশকে, তিনি একই কৌশলে রসায়নের একটি বাস্তব প্রশ্নে হাত দেন: CnH2n+2 সূত্রের হাইড্রোকার্বনের (alkane) কয়টি আলাদা গঠন (isomer) সম্ভব? কার্বন পরমাণুগুলোর কঙ্কাল আসলে একটি tree — চক্র নেই, সব যুক্ত। পরে, ১৮৮৯ সালে, Cayley প্রকাশ করেন যে n টি নামাঙ্কিত node-এ আলাদা tree ঠিক nn−2 টি — যা আজ Cayley-র সূত্র নামে পরিচিত, যদিও Carl Borchardt আগেই প্রায় একই ফল পেয়েছিলেন।

কম্পিউটার বিজ্ঞানে tree আসে ১৯৫০-এর দশকে — সূচি, অভিধান খোঁজা, আর কম্পাইলারের জন্য রাশি পার্স করতে। তখন থেকেই প্রথা দাঁড়ায় root ওপরে আঁকা, কারণ আমরা পড়ি ওপর থেকে নিচে, আর অ্যালগরিদম সাধারণত root থেকেই শুরু হয়। বংশতালিকাও সাধারণত এভাবেই আঁকা হয় — পূর্বপুরুষ ওপরে। Donald Knuth তাঁর The Art of Computer Programming-এর প্রথম খণ্ডে (১৯৬৮) এই প্রথা মেনেই tree-র পরিভাষা, traversal আর উপস্থাপনগুলোকে একটি সুসংগঠিত রূপ দেন, যা আজও পাঠ্যবইয়ে চলে।

লক্ষণীয় হলো, তারের জাল, অণুর গঠন, বংশতালিকা আর ফাইল সিস্টেম — একেবারে আলাদা জগৎ, কিন্তু গাণিতিক কঙ্কাল একই: connected, চক্রহীন, n টি node-এ n − 1 টি edge. একটি ভালো abstraction-এর শক্তি এখানেই — একবার প্রমাণ করা তথ্য সব জায়গায় খাটে।

মূল উপলব্ধি: Tree কম্পিউটারের আবিষ্কার নয়, বরং গণিতের পুরোনো একটি কাঠামো, যা বিদ্যুৎ আর রসায়নের প্রশ্ন থেকে জন্মেছে। কম্পিউটার বিজ্ঞান শুধু একে উল্টো করে আঁকে — আর এর ওপর অসংখ্য দ্রুত অ্যালগরিদম বানায়।

প্র ০২Recursion, নিজের stack আর Morris traversal — তিনটিই inorder করে। বাস্তবে কোনটি কখন বেছে নেবেন, আর stack overflow কেন শুধু তাত্ত্বিক ভয় নয়?

তিনটি পদ্ধতির সময় একই — Θ(n). পার্থক্য জায়গায়, নিরাপত্তায় আর কোডের সরলতায়। Recursive রূপ সবচেয়ে ছোট আর পড়তে সহজ; ইন্টারভিউতে প্রথমে এটিই লেখা উচিত। কিন্তু এর জায়গা লুকিয়ে থাকে call stack-এ, আর সেই stack-এর আকার প্রোগ্রাম নিজে ঠিক করে না — অপারেটিং সিস্টেম বা ভাষার runtime ঠিক করে। Linux-এ একটি প্রোগ্রামের মূল thread-এর stack প্রায়ই ৮ MB, Windows-এ প্রায়ই মাত্র ১ MB; প্রতিটি frame যদি কয়েক ডজন byte হয়, তাহলে Windows-এ কয়েক দশ হাজার আর Linux-এ এক-দুই লাখ গভীরতাতেই সীমা ছাড়ায়। Python আরও কড়া: ডিফল্ট recursion limit ১০০০, আর sys.setrecursionlimit বাড়ালে গভীরতার সাথে মেমরির খরচ বাড়ে, আর C-তে লেখা ফাংশনের ভেতর দিয়ে recursion হলে আসল stack ফুরিয়ে interpreter crash-ও করতে পারে।

এটি তাত্ত্বিক নয়। কনটেস্টে tree প্রায়ই আসে “parent অ্যারে” বা edge-তালিকা হিসেবে, আর পরীক্ষকেরা ইচ্ছা করেই একটি লম্বা শিকল (১, ২, ৩, … , ২,০০,০০০) দেন — ঠিক recursive DFS-কে ফেলে দেওয়ার জন্য। বাস্তব সফটওয়্যারেও একই: একটি JSON বা XML parser যদি recursion দিয়ে লেখা হয়, তাহলে আক্রমণকারী হাজার হাজার স্তরের নেস্টেড বন্ধনী পাঠিয়ে সার্ভার crash করাতে পারে। এজন্য অনেক parser-এ সর্বোচ্চ গভীরতার সীমা থাকে।

নিজের stack এই সমস্যা মেটায়: stack থাকে heap memory-তে, যার সীমা গিগাবাইটের ঘরে, আর গভীরতা কত হলো তা আপনি দেখতে ও সামলাতে পারেন। মূল্য হলো কোড একটু লম্বা, বিশেষ করে postorder-এ। Morris traversal (১৯৭৯) আরেক ধাপ এগিয়ে বাড়তি জায়গা O(1)-এ নামায় — খালি right pointer-গুলোতে সাময়িক সুতা বেঁধে। এর ধারণা এসেছে Perlis আর Thornton-এর ১৯৬০ সালের threaded tree থেকে, যেখানে সুতাগুলো স্থায়ী থাকত। কিন্তু Morris চলার সময় tree বদলায়: একাধিক thread একসাথে tree পড়লে, বা কোনো exception-এ মাঝপথে থামলে, tree ভাঙা অবস্থায় থেকে যায়। আর প্রতিটি predecessor খুঁজতে বাড়তি হাঁটা লাগে বলে ধ্রুবক বড়।

তাই বাস্তব নিয়ম মোটামুটি এ রকম: tree-র উচ্চতা যদি নিশ্চিতভাবে ছোট হয় (ভারসাম্যপূর্ণ BST, যেমন পর্ব ২৮–২৯), তাহলে recursion; উচ্চতা অজানা বা বড় হতে পারলে নিজের stack; আর মেমরি সত্যিই অত্যন্ত সীমিত (embedded সিস্টেম) এবং tree শুধু একজন পড়ছে — তখন Morris.

মূল উপলব্ধি: Big-O একই হলেও “জায়গা কোথায় খরচ হচ্ছে” প্রশ্নটি বাস্তবে প্রোগ্রাম চলবে কি না তা ঠিক করে। Recursion-এর stack অদৃশ্য, কিন্তু সীমাহীন নয়।

প্র ০৩n টি node দিয়ে কয়টি আলাদা আকারের binary tree হয়? এই সংখ্যা থেকে বোঝা যায় কেন একটি traversal একা tree ফেরত আনতে পারে না, আর একটি tree রাখতে কম করে কত bit লাগে।

n টি node-এর binary tree-র আলাদা আকার (shape) গুনুন — মান বাদ দিয়ে শুধু গঠন। n = 0-তে ১টি (খালি), n = 1-এ ১টি, n = 2-এ ২টি (সন্তান বাঁয়ে বা ডানে), n = 3-এ ৫টি, n = 4-এ ১৪টি। এগুলো Catalan সংখ্যা: Cn = (2n)! / ((n + 1)! · n!), যার নাম বেলজিয়ান গণিতবিদ Eugène Catalan-এর নামে। গোনার যুক্তি সরল: root-এর বাঁয়ে k টি node হলে ডানে n − 1 − k টি, তাই Cn = Σ Ck · Cn−1−k. সংখ্যাটি প্রায় 4n / (n3/2 √π) হারে বাড়ে — অর্থাৎ সূচকীয়ভাবে।

এখন একটি preorder ক্রম, যেমন 1 2 3 4, নিন। যেকোনো আকারের tree নিয়ে তার node-গুলোতে preorder ক্রমে 1, 2, 3, 4 বসালেই এই preorder পাওয়া যায় — তাই একই preorder-এর tree ঠিক C4 = ১৪টি। একটি traversal শুধু মানগুলোর একটি ক্রম দেয়, আকারের কোনো তথ্য দেয় না। Inorder যোগ করলে সেই হারানো তথ্য ফেরত আসে: preorder + inorder জোড়া সব মিলিয়ে ঠিক একটি tree নির্দেশ করে (অংশ ৮-এর প্রমাণ)। অন্যদিকে preorder + postorder-এর ক্ষেত্রে, প্রতিটি একমাত্র-সন্তানের বাঁ-ডান সিদ্ধান্ত হারিয়ে যায়; k টি এমন node থাকলে একই জোড়ার 2k টি tree হয়।

তথ্যতত্ত্বের দিক থেকে: Cn টি সম্ভাবনার মধ্যে একটি আলাদা করতে অন্তত log₂ Cn ≈ 2n bit লাগে। অথচ সাধারণ linked উপস্থাপনে প্রতিটি node-এ দুটি ৬৪-বিট pointer — 128n bit! আকারটুকু আসলে 2n + 1 bit-এই রাখা যায়: অংশ ৮-এর serialization-এ প্রতিটি মান বা #-কে শুধু ১ বা ০ ধরলেই হয়। ১৯৮৯ সালে Guy Jacobson দেখান, এমন প্রায়-সর্বনিম্ন জায়গার (succinct) উপস্থাপনেও parent, সন্তান খোঁজার মতো কাজ দ্রুত করা যায়। আজ বিশাল DNA index, search engine আর মোবাইলের অভিধানে এমন succinct tree ব্যবহার হয়, যেখানে কোটি কোটি node মেমরিতে ধরাতে হয়।

একই Catalan সংখ্যা অবাক করা অনেক জায়গায় আসে: n জোড়া বন্ধনীর সঠিক বিন্যাস, (n + 2)-বাহুর বহুভুজকে ত্রিভুজে ভাগ, আর n টি গুণের ক্রম ঠিক করার উপায় — পর্ব ৫৪-এর matrix chain সমস্যার সম্ভাব্য বন্ধনী-বিন্যাস।

মূল উপলব্ধি: একটি traversal মানের ক্রম দেয়, আকার দেয় না; আকারের জন্য প্রায় 2n bit বাড়তি তথ্য লাগে — inorder, null-চিহ্ন বা অন্য কিছু দিয়ে সেটি দিতেই হবে।

প্র ০৪ফোল্ডার মুছতে কেন postorder, কপি করতে কেন preorder লাগে — আর ব্রাউজার DOM tree-তে প্রতি মুহূর্তে কী কী traversal চালায়?

Windows বা Linux-এর ফাইল সিস্টেম একটি tree: ফোল্ডার হলো internal node, ফাইল leaf. এখানে traversal-এর ক্রম শুধু রুচির ব্যাপার নয় — ভুল ক্রমে কাজই হয় না। ফোল্ডার মুছতে (rm -r) ভেতরের সবকিছু আগে মুছতে হয়, কারণ অপারেটিং সিস্টেম একটি অ-খালি ফোল্ডার সরাসরি মুছতে দেয় না — সন্তান আগে, parent পরে: postorder. ফোল্ডারের মোট আকার বের করতেও (Linux-এর du) আগে প্রতিটি সাব-ফোল্ডারের আকার জানতে হয় — আবার postorder, অংশ ৯-এর “সন্তানের উত্তর থেকে নিজের” ছাঁচ। উল্টো দিকে ফোল্ডার কপি করতে গন্তব্যে আগে ফোল্ডারটি বানাতে হয়, তারপর ভেতরের জিনিস — parent আগে: preorder. আর tree কমান্ড বা Explorer-এর ফোল্ডার-সূচি ছাপাও preorder.

ওয়েব ব্রাউজারে DOM tree-র ওপর traversal চলে প্রতি মুহূর্তে। HTML পার্স করার সময় ব্রাউজার একটি stack ব্যবহার করে খোলা ট্যাগগুলো মনে রাখে — পর্ব ১৩-এর বন্ধনী মেলানোর মতো — আর tree বানায়। পেজ আঁকার সময় (layout) একটি node-এর আকার প্রায়ই তার সন্তানদের আকারের ওপর নির্ভর করে, তাই অনেক হিসাব postorder ধাঁচের; আর রঙ বা ফন্টের মতো উত্তরাধিকারসূত্রে পাওয়া style ওপর থেকে নিচে নামে — preorder ধাঁচ। document.querySelectorAll নথির ক্রমে ফল দেয়, যা আসলে DOM-এর preorder. CSS selector মেলানোর সময় ব্রাউজারগুলো সাধারণত ডান থেকে বাঁয়ে পড়ে: div p-এর জন্য প্রতিটি p থেকে ancestor-দের দিকে উঠে div খোঁজে — অর্থাৎ parent pointer ধরে ওপরে হাঁটা।

React-এর মতো লাইব্রেরি আরও এক ধাপ যায়: পুরোনো আর নতুন দুটি UI tree মিলিয়ে কী বদলেছে বের করে। দুটি সাধারণ tree-র মধ্যে সবচেয়ে কম পরিবর্তন খোঁজার পরিচিত অ্যালগরিদমগুলো O(n³) সময় নেয় — এক হাজার element-এ প্রায় একশো কোটি তুলনা। React-এর নিজস্ব নথিতে বলা আছে, তারা দুটি অনুমানের (ভিন্ন ধরনের element মানে ভিন্ন subtree, আর তালিকার সন্তানদের জন্য key) ওপর ভর করে কাজটি O(n)-এ নামায় — একটি সরল level-by-level তুলনা।

মূল উপলব্ধি: Preorder মানে “parent-কে আগে তৈরি বা জানানো”, postorder মানে “সন্তানদের ফল নিয়ে parent-এর হিসাব”. প্রশ্নটি দেখে ঠিক করুন তথ্য ওপর থেকে নিচে যাবে, না নিচ থেকে ওপরে — traversal আপনাআপনি বেছে নেওয়া হয়ে যাবে।

প্র ০৫Expression tree, Polish notation আর কম্পাইলার — postorder কেন মেশিনের ভাষার এত কাছের?

১৯২০-এর দশকে পোলিশ যুক্তিবিদ Jan Łukasiewicz যুক্তির রাশি বন্ধনী ছাড়া লেখার একটি উপায় দেন: operator আগে, operand পরে — + 3 4. একে বলে Polish notation বা prefix, আর এটি হুবহু expression tree-র preorder. উল্টো রূপ, operator পরে — 3 4 + — হলো reverse Polish notation (RPN) বা postfix, অর্থাৎ postorder. দুই রূপেই বন্ধনী লাগে না, কারণ tree-র গঠন ক্রমের মধ্যেই ধরা থাকে। শুধু inorder-এ বন্ধনী লাগে — এজন্যই আমরা মানুষেরা যে রূপে লিখি, সেটিই যন্ত্রের জন্য সবচেয়ে কঠিন।

Postfix-এর বড় সুবিধা: একটি stack দিয়ে বাঁ থেকে ডানে একবার পড়েই মান বের হয় (পর্ব ১৩) — সংখ্যা পেলে push, operator পেলে দুটি pop, ফল push. এজন্য ১৯৭২ সালের HP-35-সহ Hewlett-Packard-এর অনেক বৈজ্ঞানিক ক্যালকুলেটর RPN-এ চলত: ব্যবহারকারী 3 ENTER 4 + চাপতেন, বন্ধনীর ঝামেলা নেই। আর ১৯৬১ সালে Edsger Dijkstra-র shunting-yard অ্যালগরিদম সাধারণ infix রাশিকে একটি stack দিয়ে postfix-এ রূপান্তর করে — কম্পাইলারের শুরুর দিনের একটি মূল কৌশল।

আধুনিক কম্পাইলার আপনার কোড পড়ে প্রথমে একটি syntax tree বানায় — a = b * (c + d) হয়ে যায় একটি tree, যার root-এ =. তারপর অনেক ধাপ এই tree-র ওপর traversal: নামগুলো ঠিক আছে কি না যাচাই, type মেলানো, আর optimisation — যেমন 2 * 3-এর মতো দুটি ধ্রুবকের subtree-কে কম্পাইলের সময়ই 6-এ বদলে দেওয়া (constant folding), যা একটি postorder. শেষে কোড তৈরিতেও postorder: Java-র JVM, Python-এর CPython আর WebAssembly — তিনটিই stack machine, অর্থাৎ তাদের নির্দেশগুলো মূলত postfix. b * (c + d) হয় মোটামুটি “load b, load c, load d, add, multiply” — ঠিক tree-র postorder.

অর্থাৎ যে postorder এই পর্বে শুধু সংখ্যার একটি ক্রম মনে হচ্ছিল, সেটি আসলে প্রতিদিন কোটি কোটি ডিভাইসে চলা কোডের আকার। আর expression tree একটি full binary tree বলে অংশ ৩-এর সূত্রও খাটে: n টি operator-এর রাশিতে ঠিক n + 1 টি operand.

মূল উপলব্ধি: Postorder হলো “আগে উপকরণ, পরে কাজ” — যন্ত্রের stack যেভাবে চিন্তা করে। মানুষের infix আর যন্ত্রের postfix-এর মাঝে সেতু একটি tree.

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে trace করুন: নিচের tree-র preorder, inorder, postorder আর level-order লিখুন; সাথে height (edge-এ) আর leaf-এর সংখ্যা।
              F
            /   \
          B       G
         / \       \
        A   D       I
           / \     /
          C   E   H

    Preorder (root, বাঁ, ডান): F B A D C E G I H. Inorder (বাঁ, root, ডান): A B C D E F G H I — অক্ষরগুলো বর্ণমালার ক্রমে এল, কারণ এই tree-টি আসলে একটি BST (পর্ব ২৭). Postorder (বাঁ, ডান, root): A C E D B H I G F. Level-order: F B G A D I C E H.

    Height 3: F → B → D → C (বা F → G → I → H). Leaf চারটি: A, C, E, H. যাচাই: ৯টি node, তাই edge ৮টি।

  2. Tree ফেরত আনুন: একটি binary tree-র preorder 10 20 40 50 30 60 70 আর inorder 40 20 50 10 60 70 30. Tree-টি আঁকুন, তার postorder লিখুন, আর বলুন height কত।

    Root 10; inorder-এ তার বাঁয়ে 40 20 50 (৩টি), ডানে 60 70 30. বাঁ: preorder 20 40 50 → root 20, বাঁয়ে 40, ডানে 50. ডান: preorder 30 60 70, inorder 60 70 30 → root 30, যার বাঁয়ে 60 70 আর ডানে কেউ নেই। আবার preorder 60 70, inorder 60 70 → root 60, ডানে 70.

            10
           /  \
         20    30
        /  \   /
      40   50 60
                \
                70

    Postorder: 40 50 20 70 60 30 10. Height 3 (10 → 30 → 60 → 70). লক্ষ করুন, 30-এর একমাত্র সন্তান বাঁয়ে আর 60-এর একমাত্র সন্তান ডানে — শুধু preorder + postorder দিলে এই দুটি সিদ্ধান্ত হারিয়ে যেত।

  3. ভেবে বলুন: একটি binary tree আঁকুন, যার diameter root দিয়ে যায় না। তারপর ব্যাখ্যা করুন, শুধু root-এ “height(বাঁ) + height(ডান) + 2” হিসাব করা কোড সেখানে কী উত্তর দেবে, আর সঠিক উত্তর কত।
            1
           /
          2
         / \
        3   5
       /     \
      4       6

    Root 1-এর শুধু বাঁ সন্তান। ভুল কোড: height(বাঁ) = height(2) = 2, ডান খালি (−1), তাই 2 + (−1) + 2 = 3 — অর্থাৎ পথ 1 → 2 → 3 → 4. কিন্তু সবচেয়ে লম্বা পথ 4 → 3 → 2 → 5 → 6, দৈর্ঘ্য 4, যার বাঁক 2-এ — root 1 পথেই নেই। সঠিক অ্যালগরিদম প্রতিটি node-এ বাঁকের পথ মাপে: node 2-এ 1 + 1 + 2 = 4. তাই diameter 4.

  4. কোড লিখুন: একটি binary tree-র সর্বোচ্চ চওড়া (সবচেয়ে বেশি node আছে এমন level-এর node-সংখ্যা) বের করার ফাংশন লিখুন level-order দিয়ে। Time ও space complexity বলুন। প্রধান উদাহরণের tree-তে উত্তর কত?

    প্রতিটি level শুরুর সময় queue-তে ঠিক সেই level-এর সব node থাকে, তাই queue-র আকারই level-এর চওড়া:

    int maxWidth(Node* root) {
        if (!root) return 0;
        queue<Node*> q; q.push(root);
        int best = 0;
        while (!q.empty()) {
            int k = (int)q.size();                 // exactly one whole level is in the queue
            best = max(best, k);
            for (int i = 0; i < k; ++i) {          // pop that level, push the next one
                Node* u = q.front(); q.pop();
                if (u->left)  q.push(u->left);
                if (u->right) q.push(u->right);
            }
        }
        return best;
    }

    প্রধান উদাহরণে level-এর আকার 1, 2, 3, 2 — উত্তর 3. প্রতিটি node একবার ঢোকে ও বের হয়: সময় Θ(n), জায়গা O(w) — w নিজেই উত্তর। (কিছু প্রশ্নে “চওড়া” মানে একই level-এর দুই প্রান্তের node-এর মাঝের ফাঁকা ঘরও গোনা; তখন অংশ ৪-এর array index 2i + 1, 2i + 2 সাথে রাখতে হয়।)

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমসব মান আলাদা এমন একটি binary tree-র preorder আর inorder থেকে tree বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. preorder-এর প্রথম মানটিকে root ধরা
  2. inorder-এ root-এর অবস্থান খুঁজে তার বাঁয়ের আর ডানের মানগুলো আলাদা করা
  3. বাঁ ভাগের আকার k গুনে preorder-এ root-এর পরের k টি মানকে বাঁ subtree-র preorder ধরা
  4. বাঁ subtree-র preorder আর inorder দিয়ে একই নিয়মে বাঁ subtree বানানো
  5. বাকি মানগুলো দিয়ে ডান subtree বানিয়ে দুটিকে root-এর সাথে জোড়া

৫০টি node-এর একটি tree-তে কয়টি edge আছে?

  • 50
  • 49
  • 51
  • 100

উচ্চতা 4 (edge-এ মাপা, অর্থাৎ root থেকে সবচেয়ে গভীর leaf পর্যন্ত ৪টি edge) এমন একটি binary tree-তে সর্বোচ্চ কয়টি node থাকতে পারে?

  • 16
  • 15
  • 31
  • 32

একটি binary tree-র preorder A B D E C F আর inorder D B E A F C. এর postorder কী?

  • D E B F C A
  • D B E F C A
  • E D B C F A
  • A B C D E F

একটি binary tree-র level-order traversal (স্তরে স্তরে, বাঁ থেকে ডানে) করতে সাধারণত কোন ডেটা স্ট্রাকচার লাগে?

  • Stack
  • Queue
  • Min-heap
  • Hash table

n টি node-এর একটি degenerate (প্রতিটি node-এর একটিই সন্তান) binary tree-তে recursive inorder traversal চালালে বাড়তি জায়গা (call stack) কত লাগে?

  • O(1)
  • Θ(log n)
  • Θ(n)
  • Θ(n log n)

সব মান আলাদা — এমন binary tree-র ক্ষেত্রে কোন traversal-জোড়া সবসময় tree-টিকে অনন্যভাবে নির্ধারণ করে না?

  • Preorder + inorder
  • Postorder + inorder
  • Preorder + postorder
  • Level-order + inorder

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Tree = connected, চক্রহীন; root ছাড়া প্রত্যেকের ঠিক একটি parent, তাই n টি node-এ n − 1 টি edge. Depth ওপর থেকে, height নিচ থেকে — দুটোই edge-এ।
  2. Binary tree-তে উচ্চতা h হলে node ≤ 2h+1 − 1, তাই উচ্চতা ⌊log₂ n⌋ থেকে n − 1. Full, complete, perfect, degenerate — সংজ্ঞাগুলো আলাদা।
  3. উপস্থাপন: linked node (নমনীয়), array 2i + 1/2i + 2 (complete tree-র জন্য), parent array (ওপরে ওঠা), child list/LCRS (যেকোনো সংখ্যক সন্তান)।
  4. Preorder root আগে, inorder মাঝে, postorder শেষে — recursion বা stack-এ, Θ(n) সময়, O(h) জায়গা। Level-order queue-তে, O(w) জায়গা।
  5. Preorder + inorder → tree অনন্য (preorder + postorder নয়)। Height, diameter, symmetric, LCA — সবই “সন্তানের উত্তর থেকে নিজের”, এক হাঁটায় Θ(n).

পরের পর্বে: Inorder-এ যদি সবসময় সাজানো ক্রম আসত? Binary search tree — বাঁয়ে ছোট, ডানে বড় — যেখানে খোঁজা, যোগ করা আর মুছে ফেলা চলে tree-র উচ্চতা ধরে, আর কেন সেই উচ্চতাই সবকিছু।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান