024 — Serialize and Deserialize Binary Tree¶
Source¶
- Original source: Classic "turn a tree into a string and back" exercise
- Platform: LeetCode — https://leetcode.com/problems/serialize-and-deserialize-binary-tree/
- Topic: trees
- Difficulty: Hard
- Pattern: Preorder construction
- Basic idea inherited from: Preorder + None markers
1. Problem in simple words¶
তোমাকে দুটো function লিখতে হবে। serialize(root) একটা binary tree-কে একটা string-এ রূপান্তর করে (যাতে ফাইলে রাখা বা network-এ পাঠানো যায়)। deserialize(data) সেই string থেকে হুবহু একই tree আবার বানিয়ে ফেরত দেয়। শর্ত: deserialize(serialize(root)) করলে গঠন আর value-সহ মূল tree-টাই ফিরে পেতে হবে।
মূল চ্যালেঞ্জ: গঠনটাও বাঁচাতে হবে — শুধু value-গুলো জানা যথেষ্ট নয়, কোন node-এর কোন দিকে কে আছে, কোথায় ফাঁকা — সব string-এ encode করতে হবে।
1 serialize → "1,2,3,#,#,4,5,#,#,#,#"
/ \ (# = None marker, preorder ক্রমে)
2 3
/ \
4 5 deserialize → ঠিক এই tree-ই ফিরিয়ে আনে
2. Which basic idea does this inherit from?¶
ভিত্তি preorder traversal (Node → Left → Right) — কিন্তু একটা গুরুত্বপূর্ণ যোগ: None marker (#)। সাধারণ preorder শুধু value দেয়, যা থেকে গঠন অদ্বিতীয়ভাবে ফেরানো যায় না। কিন্তু প্রতিটা ফাঁকা child-এর জায়গায় একটা marker রাখলে preorder-টা অদ্বিতীয় হয়ে যায় — তখন একই preorder ক্রমে পড়তে পড়তে tree-টা সরাসরি গড়ে তোলা যায় (Construct-from-preorder #21-এর জ্ঞাতি, কিন্তু এখানে inorder লাগে না, marker-ই যথেষ্ট)।
3. What is the hidden math or logic?¶
লুকানো logic হলো marker-সহ preorder-এর অদ্বিতীয়তা:
serialize(node):
node None হলে → "#" লেখো
নইলে → node.val লেখো, তারপর serialize(left), তারপর serialize(right)
deserialize(tokens): # একই preorder ক্রমে পড়ো
পরের token নাও
"#" হলে → None ফেরত
নইলে → node বানাও, node.left = deserialize(...), node.right = deserialize(...)
মূল বুদ্ধি: serialize আর deserialize ঠিক একই ক্রমে (preorder) node ছোঁয়, তাই একটা চলন্ত pointer দিয়ে token-গুলো পরপর খেয়ে গেলেই গঠন নিখুঁতভাবে পুনর্গঠিত হয়।
4. Which data structure is needed?¶
serialize-এ একটা list/string-builder (token জমায়, শেষে join), deserialize-এ token-গুলোর একটা iterator বা deque (পরের token O(1)-তে তুলতে) আর recursion call stack। দুই দিকেই tree-র self-similarity-র উপর recursion চলে; marker # দিয়ে ফাঁকা স্থান চিহ্নিত।
5. Why this data structure fits¶
preorder এমন একটা ক্রম যেখানে root আগে আসে — তাই deserialize পড়ার সময় আগে parent বানিয়ে তারপর child বসাতে পারে, ঠিক যেভাবে serialize লিখেছিল। একটা deque/iterator token-গুলো একই ক্রমে এক-এক করে দেয়, কোনো index-হিসাব ছাড়াই। আর marker # ছাড়া preorder ambiguous হতো (দুটো ভিন্ন tree একই value-ক্রম দিতে পারে) — marker সেই ফাঁক বন্ধ করে গঠন অদ্বিতীয় করে।
6. Real-life analogy¶
ভাবো তুমি একটা পরিবার-গাছ ফোনে কাউকে মুখে বলে দিচ্ছ। তুমি বলবে: "আগে নিজের নাম, তারপর বাঁ সন্তানের পুরো শাখা, তারপর ডান সন্তানের পুরো শাখা। যেখানে কেউ নেই, সেখানে বলবে 'খালি'।" ওপ্রান্তের লোক ঠিক একই ক্রমে শুনে শুনে কাগজে গাছটা আঁকবে — 'খালি' শুনলে সেদিকে কিছু আঁকবে না। 'খালি' শব্দটাই (marker) নিশ্চিত করে দু'জনের আঁকা গাছ হুবহু এক হবে।
7. Visual explanation¶
preorder ক্রমে value আর # (None) লেখা হয়; deserialize সেই tokens একই ক্রমে খায়:
8. Example input and output¶
Input : root = [1,2,3,null,null,4,5]
serialize -> "1,2,#,#,3,4,#,#,5,#,#"
deserialize-> ওই string থেকে হুবহু আগের tree
Edge : root = [] -> serialize "#" -> deserialize None
Edge : root = [1] -> serialize "1,#,#" -> একটামাত্র node 1
Edge : root = [1,2] -> serialize "1,2,#,#,#"-> 1, বাঁ child 2
মূল চুক্তি: যেকোনো tree-র জন্য deserialize(serialize(t)) == t (গঠন+value)
9. Brute force thinking¶
প্রথম সরল চিন্তা — marker ছাড়া: শুধু preorder value-গুলো জমাই, আর deserialize-এ অনুমান করি কোথায় ভাঙব।
1) serialize: শুধু value preorder-এ লেখো → "1,2,3,4,5"
2) deserialize: প্রথমটা root, তারপর... কোনগুলো বাঁ, কোনগুলো ডান?
3) → অনুমান করার কোনো নিশ্চিত উপায় নেই (গঠন হারিয়ে গেছে)
10. Why brute force is slow¶
এটা ধীর নয় — এটা সরাসরি ভুল। marker ছাড়া একই value-sequence একাধিক ভিন্ন গঠনের tree থেকে আসতে পারে (যেমন [1,2]-তে 2 বাঁ না ডান child তা বোঝা যায় না)। তাই deserialize অদ্বিতীয় উত্তর দিতে পারে না। সমাধান: প্রতিটা ফাঁকা child-এর জায়গায় একটা None marker যোগ করা — সামান্য বেশি জায়গা, কিন্তু গঠন সম্পূর্ণ পুনরুদ্ধারযোগ্য।
11. Key observation¶
মূল observation: প্রতিটা ফাঁকা child-এও একটা marker রাখলে preorder অদ্বিতীয় হয়ে যায়। তখন deserialize-এর আর "কোথায় ভাঙব" ভাবতে হয় না — সে শুধু token-গুলো একই preorder ক্রমে খায়: value পেলে node বানায় (আগে বাঁ, পরে ডান recurse), # পেলে None ফেরত দেয়।
12. Optimized intuition¶
serialize একটা preorder DFS: None হলে #, নইলে value তারপর left তারপর right — সব token একটা list-এ, শেষে কমা দিয়ে join। deserialize একটা iterator বানায় token-গুলোর উপর; একটা helper পরের token নেয় — # হলে None, নইলে node বানিয়ে node.left = build(), node.right = build()। যেহেতু serialize ও deserialize একই ক্রমে চলে, pointer নিজে থেকেই ঠিক জায়গায় থাকে। দুই দিকই O(n)।
13. Thinking tweak¶
মোচড়: "string-টা কীভাবে parse করব" নিয়ে আটকে যেও না। মনে রাখো — serialize আর deserialize একই recursion, একই preorder ক্রম; একটা লেখে, আরেকটা পড়ে। মূল চাবি None marker: সেটা থাকলে গঠন অদ্বিতীয়, আর একটা চলন্ত iterator/pointer-ই বাকি কাজ করে দেয়। আলাদা index গোনা বা inorder লাগে না (#21-এর চেয়ে এখানে সহজ)।
14. Step-by-step dry run¶
deserialize tokens ["1","2","#","#","3","4","#","#","5","#","#"], iterator সামনে থেকে খায়:
build(): token "1" → node 1। এখনnode.left = build()।build(): token "2" → node 2।node.left = build()→ token "#" → None।node.right = build()→ "#" → None। node 2 (leaf) ফেরত। (1-এর বাঁ = 2)- ফিরে 1-এর
node.right = build(): token "3" → node 3। - 3-এর বাঁ:
build()→ "4" → node 4; তার দুই child "#","#" → None,None (leaf 4)। - 3-এর ডান:
build()→ "5" → node 5; দুই child "#","#" → None,None (leaf 5)। - গঠন: 1 → (2, 3), 3 → (4, 5)। ঠিক মূল tree।
15. Python solution¶
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
"""level-order list (None = missing child) থেকে tree বানায় (test setup-এর জন্য)।"""
if not values:
return None
root = TreeNode(values[0])
q = deque([root])
i = 1
while q and i < len(values):
node = q.popleft()
if i < len(values) and values[i] is not None:
node.left = TreeNode(values[i])
q.append(node.left)
i += 1
if i < len(values) and values[i] is not None:
node.right = TreeNode(values[i])
q.append(node.right)
i += 1
return root
def serialize(root):
out = []
def dfs(node):
if node is None:
out.append("#") # None marker
return
out.append(str(node.val)) # preorder: value আগে
dfs(node.left) # তারপর বাঁ
dfs(node.right) # তারপর ডান
dfs(root)
return ",".join(out)
def deserialize(data):
tokens = iter(data.split(",")) # একই preorder ক্রমে পড়ব
def build():
val = next(tokens)
if val == "#": # None marker → ফাঁকা
return None
node = TreeNode(int(val))
node.left = build() # serialize-এর মতোই বাঁ আগে
node.right = build() # তারপর ডান
return node
return build()
def same_tree(a, b):
"""round-trip যাচাইয়ের helper: দুই tree হুবহু এক কি না।"""
if a is None and b is None:
return True
if a is None or b is None or a.val != b.val:
return False
return same_tree(a.left, b.left) and same_tree(a.right, b.right)
# ---- tests ----
t = build_tree([1, 2, 3, None, None, 4, 5])
assert serialize(t) == "1,2,#,#,3,4,#,#,5,#,#"
assert same_tree(deserialize(serialize(t)), t) # round-trip ঠিক
assert serialize(build_tree([])) == "#" # খালি tree
assert deserialize("#") is None
assert same_tree(deserialize(serialize(build_tree([1]))), build_tree([1]))
assert same_tree(deserialize(serialize(build_tree([1, 2]))), build_tree([1, 2]))
neg = build_tree([-7, -3, 9]) # ঋণাত্মক value-ও টেকে
assert same_tree(deserialize(serialize(neg)), neg)
print("সব test pass!")
16. Line-by-line code explanation¶
serialize.dfs:Noneহলে"#"(marker), নইলে value লিখে আগে বাঁ তারপর ডান — খাঁটি preorder।",".join(out)— সব token কমা দিয়ে একটা string-এ; এটাই serialize-এর আউটপুট।tokens = iter(data.split(","))— string ভেঙে token, তার উপর একটা iterator;next()পরের token O(1)-তে দেয়।build()-এnext(tokens)— serialize যে ক্রমে লিখেছিল, ঠিক সেই ক্রমে পড়ছি, তাই pointer নিজেই সঠিক জায়গায়।if val == "#": return None— marker পেলে এই দিক ফাঁকা।node.left = build(); node.right = build()— serialize-এর "বাঁ আগে, ডান পরে" ক্রম হুবহু মিলিয়ে recurse; এই মিল না থাকলে গঠন এলোমেলো হয়।
17. Output walkthrough¶
build_tree([1,2,3,None,None,4,5]) থেকে serialize দেয় "1,2,#,#,3,4,#,#,5,#,#" — assert pass। সেই string deserialize করে আবার মূল tree (round-trip same_tree pass)। খালি tree দেয় "#" → None, একক node আর [1,2] round-trip ঠিক, ঋণাত্মক value-ও টেকে — সব pass। শেষে print: সব test pass!।
18. Time complexity¶
O(n) দুই দিকেই — serialize প্রতিটা node (আর প্রতিটা None-প্রান্ত) একবার লেখে; deserialize প্রতিটা token একবার পড়ে node বানায়। token-সংখ্যা node-সংখ্যার রৈখিক গুণিতক (প্রতিটা None একটা marker)।
19. Space complexity¶
O(n) — serialize-এর output string আর token list O(n); recursion call stack tree-র height পর্যন্ত (skewed হলে O(n), balanced O(log n))। deserialize-এও একই — token list O(n) + height-সমান stack।
20. Common mistakes¶
- None marker বাদ দেওয়া — তখন preorder ambiguous, deserialize গঠন ফেরাতে পারে না।
- serialize "বাঁ আগে" কিন্তু deserialize "ডান আগে" পড়া (বা উল্টো) — ক্রম না মিললে tree আয়না-উল্টে যায়।
- ঋণাত্মক value বা multi-digit value-কে marker
#-এর সাথে গুলিয়ে ফেলা — তাই সংখ্যা আর marker আলাদা token (কমা-বিভক্ত) রাখা ভালো। - deserialize-এ index-counter ভুল manage করা — iterator/deque ব্যবহার করলে এই ভুল এড়ানো যায়।
21. Which basic problem this inherits from¶
ভিত্তি: traversal-patterns.md-র preorder, আর Construct-from-preorder-and-inorder (#21)-এর "একটা traversal থেকে tree গড়া"। পার্থক্য — এখানে inorder লাগে না, কারণ None marker-ই preorder-কে অদ্বিতীয় করে দেয়, তাই একটা traversal-ই যথেষ্ট।
22. Similar harder problems¶
- Construct Binary Tree from Preorder and Inorder (marker ছাড়া দুই traversal দিয়ে গঠন) — https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/ (এই tracker-এর #21)
- Serialize and Deserialize BST (BST property কাজে লাগিয়ে marker ছাড়াই সম্ভব) — https://leetcode.com/problems/serialize-and-deserialize-bst/
- Serialize and Deserialize N-ary Tree (general tree, child-count বা শেষ-marker সহ) — https://leetcode.com/problems/serialize-and-deserialize-n-ary-tree/
23. Pattern learned¶
Marker-augmented preorder round-trip: serialize-এ preorder DFS চালাও, ফাঁকা child-এ একটা None marker লেখো; deserialize-এ একই ক্রমে token খেয়ে গাছ গড়ো (marker → None, value → node + বাঁ + ডান)। গঠনসহ tree-কে string-এ-string-থেকে আনার সব problem-এর কঙ্কাল এটাই।
24. Final summary¶
ভবিষ্যতের আমাকে: "tree ↔ string = marker-সহ preorder; serialize লেখে, deserialize একই ক্রমে token খেয়ে গাছ ফেরায়।" None marker আর serialize/deserialize-এর হুবহু এক ক্রম — এই দুটোই মূল। 'গঠনসহ tree সংরক্ষণ/পুনরুদ্ধার' টাইপ problem দেখলেই এই marker-preorder চালটা মনে করবে।
25. JavaScript solution (with test cases)¶
const assert = (cond, msg = "") => { if (!cond) throw new Error("FAIL " + msg); };
class TreeNode {
constructor(val = 0, left = null, right = null) {
this.val = val; this.left = left; this.right = right;
}
}
function buildTree(values) {
if (!values || values.length === 0) return null;
const root = new TreeNode(values[0]);
const q = [root];
let i = 1, head = 0;
while (head < q.length && i < values.length) {
const node = q[head++];
if (i < values.length && values[i] !== null) { node.left = new TreeNode(values[i]); q.push(node.left); }
i++;
if (i < values.length && values[i] !== null) { node.right = new TreeNode(values[i]); q.push(node.right); }
i++;
}
return root;
}
function serialize(root) {
const out = [];
const dfs = (node) => {
if (node === null) { out.push("#"); return; } // None marker
out.push(String(node.val)); // preorder: value আগে
dfs(node.left); // তারপর বাঁ
dfs(node.right); // তারপর ডান
};
dfs(root);
return out.join(",");
}
function deserialize(data) {
const tokens = data.split(",");
let idx = 0; // একই preorder ক্রমে পড়ার pointer
const build = () => {
const val = tokens[idx++];
if (val === "#") return null; // None marker → ফাঁকা
const node = new TreeNode(Number(val));
node.left = build(); // serialize-এর মতোই বাঁ আগে
node.right = build(); // তারপর ডান
return node;
};
return build();
}
// round-trip যাচাইয়ের helper: দুই tree হুবহু এক কি না
function sameTree(a, b) {
if (a === null && b === null) return true;
if (a === null || b === null || a.val !== b.val) return false;
return sameTree(a.left, b.left) && sameTree(a.right, b.right);
}
// ---- test cases ----
const t = buildTree([1, 2, 3, null, null, 4, 5]);
assert(serialize(t) === "1,2,#,#,3,4,#,#,5,#,#", "serialize string");
assert(sameTree(deserialize(serialize(t)), t), "round-trip");
assert(serialize(buildTree([])) === "#", "খালি tree");
assert(deserialize("#") === null, "deserialize #");
assert(sameTree(deserialize(serialize(buildTree([1]))), buildTree([1])), "একক node");
const neg = buildTree([-7, -3, 9]);
assert(sameTree(deserialize(serialize(neg)), neg), "ঋণাত্মক value");
console.log("সব JS test pass!");
JS টীকা: Python-এর
iter()+next()-এর সমতুল্য এখানে একটা plainidxcounter দিয়ে token-গুলো পরপর খাওয়া — যেহেতু serialize আর deserialize একই preorder ক্রমে চলে, এই চলন্ত index নিজে থেকেই সঠিক জায়গায় থাকে।Number(val)string token-কে আবার সংখ্যায় ফেরায় (ঋণাত্মক/multi-digit সহ); marker#আর সংখ্যা আলাদা token হওয়ায় গুলিয়ে যায় না।
26. TypeScript solution (with test cases)¶
interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; }
function node(val: number, left: TreeNode | null = null, right: TreeNode | null = null): TreeNode {
return { val, left, right };
}
function buildTree(values: (number | null)[]): TreeNode | null {
if (values.length === 0) return null;
const root = node(values[0] as number);
const q: TreeNode[] = [root];
let i = 1, head = 0;
while (head < q.length && i < values.length) {
const cur = q[head++];
if (i < values.length && values[i] !== null) { cur.left = node(values[i] as number); q.push(cur.left); }
i++;
if (i < values.length && values[i] !== null) { cur.right = node(values[i] as number); q.push(cur.right); }
i++;
}
return root;
}
function serialize(root: TreeNode | null): string {
const out: string[] = [];
const dfs = (n: TreeNode | null): void => {
if (n === null) { out.push("#"); return; }
out.push(String(n.val));
dfs(n.left);
dfs(n.right);
};
dfs(root);
return out.join(",");
}
function deserialize(data: string): TreeNode | null {
const tokens = data.split(",");
let idx = 0;
const build = (): TreeNode | null => {
const val = tokens[idx++];
if (val === "#") return null;
const cur = node(Number(val));
cur.left = build();
cur.right = build();
return cur;
};
return build();
}
function sameTree(a: TreeNode | null, b: TreeNode | null): boolean {
if (a === null && b === null) return true;
if (a === null || b === null || a.val !== b.val) return false;
return sameTree(a.left, b.left) && sameTree(a.right, b.right);
}
const expect = (cond: boolean, msg = ""): void => { if (!cond) throw new Error("FAIL " + msg); };
const t = buildTree([1, 2, 3, null, null, 4, 5]);
expect(serialize(t) === "1,2,#,#,3,4,#,#,5,#,#", "serialize");
expect(sameTree(deserialize(serialize(t)), t), "round-trip");
expect(serialize(buildTree([])) === "#", "খালি");
expect(deserialize("#") === null, "null");
const neg = buildTree([-7, -3, 9]);
expect(sameTree(deserialize(serialize(neg)), neg), "neg");
console.log("সব TS test pass!");
TS টীকা:
serialize-এর return typestringআরdeserialize-এরTreeNode | null— এই signature জোড়া round-trip চুক্তিটা type-এই লিখে রাখে: tree → string → tree।build-এর return typeTreeNode | nullবাধ্য করে marker-case (null) সামলাতে।string[]ওString(...)নিশ্চিত করে token সবসময় string, তাইNumber(val)দিয়ে নিরাপদে আবার সংখ্যায় ফেরানো যায়।
27. কোথায় কাজে লাগে (real-world use)¶
- Caching / persistence: একটা in-memory tree (যেমন parsed config বা index) ডিস্কে save করে পরে হুবহু restore করা — serialize/deserialize-ই এর ভিত্তি।
- Network transmission: একটা tree structure এক সার্ভার থেকে আরেক সার্ভারে পাঠাতে string/byte-stream-এ encode করা (এই problem JSON/protobuf-এর সরলীকৃত রূপ)।
- Undo/redo snapshots: একটা document tree-র অবস্থা serialize করে রেখে পরে সেই snapshot-এ ফেরা।
- Inter-process / clipboard copy: একটা hierarchical data structure এক প্রসেস থেকে আরেক প্রসেসে কপি করতে flatten করা ও আবার গড়া।
- Test fixtures / golden files: একটা পরিচিত tree serialize করে golden string হিসেবে রাখা, পরে regression test-এ মিলিয়ে দেখা।
মূল সুর: গঠনসহ কোনো tree-কে সংরক্ষণ বা পাঠাতে হলে marker-augmented preorder দিয়ে এক ক্রমে লিখে, একই ক্রমে পড়ে নিখুঁত round-trip পাওয়া যায়।
মনে রাখার নিয়ম (legal): সব নিজের ভাষায় লেখা; problem statement copy করা হয়নি; official link শুধু attribution-এর জন্য; "asked by Google/Amazon" দাবি নেই — এটা একটা common interview pattern।