CHƯƠNG 09 · HIERARCHICAL DS · ~150 phút

Tree
Binary Tree, BST, Trie

Tree xuất hiện khắp nơi: file system, DOM, JSON, database index (B-tree), AST của parser. Phỏng vấn rất hay hỏi traversal và BST. Học chương này, bạn nắm: 4 loại traversal (recursive + iterative), BST operations, Trie cho autocomplete, và nguyên lý balance tree.

9.1 Terminology — thuật ngữ cần thuộc

           [10]              ← root (gốc)
          /    \
        [5]    [15]            ← internal nodes
        / \      \
      [3] [7]   [20]           ← leaves (lá) — không có con

- Root:   node trên cùng, không có parent
- Leaf:   node không có con (cả trái lẫn phải đều null)
- Parent: node "cha" (level trên)
- Child:  node "con" (level dưới)
- Sibling: cùng parent
- Depth:  khoảng cách từ root đến node (root depth = 0)
- Height: khoảng cách từ node đến leaf xa nhất
- Level:  depth + 1 (đôi khi cùng nghĩa)
- Subtree: cây con bắt nguồn từ một node

Phân loại Binary Tree

  • Full: mọi node có 0 hoặc 2 con (không có node có 1 con duy nhất)
  • Complete: tất cả các level đều đầy, ngoại trừ level cuối có thể chưa đầy nhưng được fill từ trái sang phải
  • Perfect: full + tất cả leaves cùng level
  • Balanced: chiều cao giữa nhánh trái và phải của mỗi node chênh nhau ≤ 1 → cây "thấp", thao tác hiệu quả
  • Skewed: như linked list — mọi node chỉ có 1 con

9.2 Binary Tree — cài đặt cơ bản

class TreeNode {
  constructor(val = 0, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

// Tạo cây thủ công:
//        1
//       / \
//      2   3
//     / \
//    4   5
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

Tính chiều cao (height) của cây

// Height = số cạnh dài nhất từ node đến leaf
function height(root) {
  if (!root) return -1;  // hoặc 0, tuỳ định nghĩa
  return 1 + Math.max(height(root.left), height(root.right));
}

// Hoặc đếm theo số node (depth-based):
function maxDepth(root) {
  if (!root) return 0;
  return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}

Đếm số node

function countNodes(root) {
  if (!root) return 0;
  return 1 + countNodes(root.left) + countNodes(root.right);
}

9.3 Traversal (DFS) — 3 thứ tự cần thuộc

3 cách duyệt cây theo DFS, khác nhau ở thứ tự thăm node:

  • Pre-order: Node → Trái → Phải
  • In-order: Trái → Node → Phải
  • Post-order: Trái → Phải → Node
           1
          / \
         2   3
        / \
       4   5

Pre-order:  1, 2, 4, 5, 3
In-order:   4, 2, 5, 1, 3
Post-order: 4, 5, 2, 3, 1

9.3.1 Recursive — đẹp và dễ hiểu

function preOrder(root, result = []) {
  if (!root) return result;
  result.push(root.val);
  preOrder(root.left, result);
  preOrder(root.right, result);
  return result;
}

function inOrder(root, result = []) {
  if (!root) return result;
  inOrder(root.left, result);
  result.push(root.val);
  inOrder(root.right, result);
  return result;
}

function postOrder(root, result = []) {
  if (!root) return result;
  postOrder(root.left, result);
  postOrder(root.right, result);
  result.push(root.val);
  return result;
}

9.3.2 Iterative với Stack — bắt buộc biết

// Pre-order iterative
function preOrderIter(root) {
  if (!root) return [];
  const result = [], stack = [root];
  while (stack.length) {
    const node = stack.pop();
    result.push(node.val);
    if (node.right) stack.push(node.right);  // right trước vì stack LIFO
    if (node.left)  stack.push(node.left);
  }
  return result;
}

// In-order iterative — tricky hơn
function inOrderIter(root) {
  const result = [], stack = [];
  let curr = root;
  while (curr || stack.length) {
    while (curr) {
      stack.push(curr);
      curr = curr.left;
    }
    curr = stack.pop();
    result.push(curr.val);
    curr = curr.right;
  }
  return result;
}

// Post-order iterative — khó nhất
function postOrderIter(root) {
  if (!root) return [];
  const result = [], stack = [root];
  while (stack.length) {
    const node = stack.pop();
    result.unshift(node.val);  // chèn đầu (đảo thứ tự pre-order)
    if (node.left)  stack.push(node.left);
    if (node.right) stack.push(node.right);
  }
  return result;
}
// Trick: post-order = đảo ngược của (Node → Right → Left)

Khi nào dùng cái nào?

LoạiKhi nào
Pre-orderCopy/clone cây, serialize cây, evaluate prefix expression
In-orderBST → cho ra sorted output (Tính chất quan trọng nhất của BST!)
Post-orderXoá cây (xoá con trước cha), tính kích thước subtree, evaluate postfix

9.4 Level-order Traversal (BFS) — duyệt theo lớp

Thăm node theo từng level — root → tất cả depth=1 → tất cả depth=2 → ...

function levelOrder(root) {
  if (!root) return [];
  const result = [], queue = [root];
  while (queue.length) {
    const level = [];
    const sz = queue.length;  // số node ở level hiện tại
    for (let i = 0; i < sz; i++) {
      const node = queue.shift();  // (Lưu ý: shift O(n), demo thôi)
      level.push(node.val);
      if (node.left)  queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    result.push(level);
  }
  return result;
}

//        1
//       / \
//      2   3
//     / \
//    4   5
// Output: [[1], [2,3], [4,5]]
⚠️ Đừng dùng arr.shift() làm queue trong production
Đã nhắc ở Chương 4. Dùng index thủ công hoặc cài Queue class. Demo trên dùng shift để code ngắn gọn.

Variants của Level-order

  • Zigzag: level chẵn trái-phải, level lẻ phải-trái
  • Right Side View: trả về phần tử phải nhất của mỗi level
  • Average of Levels: tính trung bình mỗi level
// Right Side View
function rightSideView(root) {
  if (!root) return [];
  const result = [], queue = [root];
  while (queue.length) {
    const sz = queue.length;
    for (let i = 0; i < sz; i++) {
      const node = queue.shift();
      if (i === sz - 1) result.push(node.val);  // cuối level
      if (node.left)  queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
  }
  return result;
}

9.5 Binary Search Tree (BST) — cây tìm kiếm nhị phân

Định nghĩa BST: với mỗi node:

  • Mọi node trong subtree trái có giá trị < node hiện tại
  • Mọi node trong subtree phải có giá trị > node hiện tại
  • (Tuỳ định nghĩa, có thể cho phép = hoặc không — thường BST chuẩn không cho phép trùng)
           [8]
          /   \
        [3]   [10]
        / \      \
      [1] [6]   [14]
          / \    /
        [4] [7] [13]

BST là cây "tự sort": in-order traversal của BST cho ra mảng tăng dần.

Tính chất BẤT ĐẲNG QUAN TRỌNG

📐 Validate BST
Sai lầm phổ biến: chỉ kiểm tra node.left.val < node.val < node.right.val. Phải kiểm tra toàn bộ subtree nằm trong khoảng (min, max).
// ❌ SAI — chỉ check 1 cấp
function isValidBSTBad(root) {
  if (!root) return true;
  if (root.left && root.left.val >= root.val) return false;
  if (root.right && root.right.val <= root.val) return false;
  return isValidBSTBad(root.left) && isValidBSTBad(root.right);
}
// Counter-example:
//      5
//     / \
//    3   8
//       /
//      2     ← 2 < 5 nhưng nằm trong right subtree → vi phạm!

// ✅ ĐÚNG — truyền (min, max) xuống
function isValidBST(root, min = -Infinity, max = Infinity) {
  if (!root) return true;
  if (root.val <= min || root.val >= max) return false;
  return isValidBST(root.left, min, root.val) &&
         isValidBST(root.right, root.val, max);
}

// ✅ ĐÚNG — dùng in-order traversal (cho ra phải sort tăng)
function isValidBSTInorder(root) {
  let prev = -Infinity;
  function inorder(node) {
    if (!node) return true;
    if (!inorder(node.left)) return false;
    if (node.val <= prev) return false;
    prev = node.val;
    return inorder(node.right);
  }
  return inorder(root);
}

9.6 BST Operations — search, insert, delete

Search — O(h) với h là height

function search(root, target) {
  if (!root) return null;
  if (root.val === target) return root;
  if (target < root.val) return search(root.left, target);
  return search(root.right, target);
}

Insert

function insert(root, val) {
  if (!root) return new TreeNode(val);
  if (val < root.val) root.left = insert(root.left, val);
  else if (val > root.val) root.right = insert(root.right, val);
  // val === root.val: bỏ qua (BST không cho trùng)
  return root;
}

Delete — phức tạp nhất, 3 trường hợp

function deleteNode(root, key) {
  if (!root) return null;

  if (key < root.val) {
    root.left = deleteNode(root.left, key);
  } else if (key > root.val) {
    root.right = deleteNode(root.right, key);
  } else {
    // Tìm thấy node cần xoá
    // Case 1: leaf — chỉ cần xoá
    if (!root.left && !root.right) return null;
    // Case 2: chỉ có 1 con — thay bằng con đó
    if (!root.left)  return root.right;
    if (!root.right) return root.left;
    // Case 3: có 2 con — thay bằng successor (nhỏ nhất ở right subtree)
    let succ = root.right;
    while (succ.left) succ = succ.left;
    root.val = succ.val;
    root.right = deleteNode(root.right, succ.val);
  }
  return root;
}

Tại sao chọn successor? Vì successor là phần tử vừa lớn hơn node bị xoá → giữ tính chất BST.

Big-O của BST operations

OperationAverageWorst
Search/Insert/DeleteO(log n)O(n)

Worst O(n) xảy ra khi BST suy biến thành linked list (vd insert 1, 2, 3, 4, 5 theo thứ tự). Để tránh, ta cần self-balancing BST.

9.7 Balanced BST — AVL, Red-Black

Self-balancing BST tự động giữ height ~O(log n) bằng cách xoay (rotation) sau insert/delete. Bạn không cần cài chi tiết, nhưng phải biết tên + nguyên lý.

AVL Tree (1962)

  • Mỗi node lưu "balance factor" = height(left) - height(right)
  • Yêu cầu balance factor luôn ∈ {-1, 0, 1}
  • Khi insert/delete, nếu vi phạm → rotate (LL, LR, RL, RR)
  • Strict balance → search nhanh nhất, nhưng nhiều rotation

Red-Black Tree

  • Mỗi node có "màu" đỏ hoặc đen, với 5 invariants
  • Balance "lỏng" hơn AVL — chiều cao có thể lệch nhưng không vượt 2x
  • Ít rotation hơn → insert/delete nhanh hơn
  • Java TreeMap, C++ std::map, Linux kernel scheduler đều dùng

B-tree / B+ tree

  • Cây có nhiều con (không chỉ 2) — phù hợp disk-based storage
  • Database index (MySQL InnoDB B+ tree), filesystem (NTFS, HFS+)
  • Giảm số lần đọc disk vì 1 page chứa nhiều keys
💡 Trong phỏng vấn
Bạn không cần code AVL/RB tree từ đầu. Chỉ cần biết: (1) chúng tồn tại, (2) cho O(log n) đảm bảo, (3) dùng ở đâu trong industry. Nếu interviewer hỏi sâu, vẽ ý tưởng rotation đơn giản.

9.8 Trie (Prefix Tree) — vũ khí cho string

Trie (đọc là "try", từ "retrieval") là cây mà mỗi cạnh đại diện 1 ký tự, mỗi đường từ root đến leaf tạo thành 1 từ. Cực kỳ tốt cho:

  • Autocomplete
  • Spell check
  • IP routing (longest prefix match)
  • Bài "search by prefix"
            [root]
           /   |   \
          a    b    c
         / \   |
        p   t  e
        |   |  |
        p   ●  e
        |      |
        l      ●
        |
        e
        |
        ●

(các ● = end of word: "app", "at", "be", "bee")

Cài đặt Trie

class TrieNode {
  constructor() {
    this.children = {};   // hoặc Map(), hoặc Array(26) cho a-z
    this.isEnd = false;
  }
}

class Trie {
  constructor() { this.root = new TrieNode(); }

  // O(L) với L = độ dài từ
  insert(word) {
    let node = this.root;
    for (const ch of word) {
      if (!node.children[ch]) node.children[ch] = new TrieNode();
      node = node.children[ch];
    }
    node.isEnd = true;
  }

  search(word) {
    const node = this._traverse(word);
    return node !== null && node.isEnd;
  }

  startsWith(prefix) {
    return this._traverse(prefix) !== null;
  }

  _traverse(s) {
    let node = this.root;
    for (const ch of s) {
      if (!node.children[ch]) return null;
      node = node.children[ch];
    }
    return node;
  }
}

// Demo
const t = new Trie();
t.insert("apple");
t.insert("app");
console.log(t.search("apple"));    // true
console.log(t.search("app"));      // true
console.log(t.search("ap"));       // false (không phải end)
console.log(t.startsWith("ap"));   // true

Big-O của Trie

  • Insert / Search / startsWith: O(L) với L = độ dài word/prefix
  • Space: O(N · L) tệ nhất, nhưng tiết kiệm khi nhiều từ chia sẻ prefix

Ưu điểm so với HashSet<String>

  • HashSet: O(L) search nhưng không hỗ trợ prefix
  • Trie: O(L) search + O(L) prefix search → autocomplete cực nhanh

9.9 LCA & Diameter — bài kinh điển

9.9.1 Lowest Common Ancestor

Tìm ancestor (tổ tiên) thấp nhất của 2 node p, q.

// Cho cây bất kỳ (không cần BST)
function lowestCommonAncestor(root, p, q) {
  if (!root || root === p || root === q) return root;
  const left  = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);
  if (left && right) return root;  // p và q ở 2 subtree khác nhau
  return left || right;
}

// Trong BST có thể tận dụng tính chất sort
function lcaBST(root, p, q) {
  if (p.val < root.val && q.val < root.val) return lcaBST(root.left, p, q);
  if (p.val > root.val && q.val > root.val) return lcaBST(root.right, p, q);
  return root;  // p và q nằm hai bên hoặc chính là root
}

9.9.2 Diameter of Binary Tree

Đường đi dài nhất giữa 2 node bất kỳ (không nhất thiết qua root).

function diameter(root) {
  let max = 0;
  function depth(node) {
    if (!node) return 0;
    const l = depth(node.left);
    const r = depth(node.right);
    max = Math.max(max, l + r);  // đường đi qua node = l + r
    return 1 + Math.max(l, r);
  }
  depth(root);
  return max;
}

9.9.3 Serialize and Deserialize Tree

// Serialize: dùng pre-order với marker null
function serialize(root) {
  const result = [];
  function dfs(node) {
    if (!node) { result.push('#'); return; }
    result.push(node.val);
    dfs(node.left);
    dfs(node.right);
  }
  dfs(root);
  return result.join(',');
}

function deserialize(data) {
  const tokens = data.split(',');
  let i = 0;
  function build() {
    const t = tokens[i++];
    if (t === '#') return null;
    const node = new TreeNode(Number(t));
    node.left  = build();
    node.right = build();
    return node;
  }
  return build();
}

Bài tập

Bài 1 — Maximum Depth of Binary Tree

Tính độ sâu max của cây. Recursive 1 dòng.

Bài 2 — Same Tree

Hai cây có giống hệt không (cấu trúc + giá trị)?

Bài 3 — Symmetric Tree

Cây có đối xứng qua trục giữa không? Hint: viết hàm mirror(left, right).

Bài 4 — Binary Tree Level Order Traversal

Đã có ở 9.4. Tự cài đầy đủ.

Bài 5 — Validate Binary Search Tree

Đã có ở 9.5. Tự cài cả 2 cách (min-max và in-order).

Bài 6 — Lowest Common Ancestor

Đã có ở 9.9.1. Cài cho cả Binary Tree và BST.

Bài 7 — Diameter of Binary Tree

Đã có ở 9.9.2.

Bài 8 — Implement Trie

LeetCode 208. Đã có ở 9.8.

Bài 9 — Word Search II (Hard)

Cho board ký tự và mảng words. Trả về các word có trong board. Hint: build Trie + DFS từ mỗi ô.

Bài 10 — Serialize and Deserialize Binary Tree (Hard)

Đã có ở 9.9.3.

🧪 Quiz cuối chương

Câu 1. In-order traversal của BST cho ra?

  • Mảng ngẫu nhiên
  • Mảng giảm dần
  • Mảng tăng dần (sorted)
  • Theo thứ tự insert

Đáp án: Mảng tăng dần. Đây là tính chất quan trọng nhất của BST. Vì với mỗi node, left subtree < node < right subtree → in-order (Left → Node → Right) cho output sort.

Câu 2. Big-O search trong BST balanced?

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

Đáp án: O(log n). Mỗi bước, ta chọn 1 trong 2 nửa con → height = log n. Worst case O(n) khi cây skewed.

Câu 3. BFS trên cây dùng cấu trúc gì?

  • Stack
  • Heap
  • Queue
  • Hash Map

Đáp án: Queue. BFS thăm theo lớp → FIFO. DFS dùng Stack hoặc đệ quy.

Câu 4. Khi delete node có 2 con trong BST, ta thay bằng?

  • In-order successor (min của right subtree)
  • Bất kỳ leaf nào
  • Root
  • Không xoá được

Đáp án: In-order successor. Hoặc predecessor (max của left subtree). Cả hai giữ tính chất BST.

Câu 5. BST suy biến thành linked list khi?

  • Khi có quá nhiều node
  • Khi insert theo thứ tự đã sort (vd 1,2,3,4,5)
  • Khi delete root
  • Không thể xảy ra

Đáp án: Insert sorted input. Mỗi node mới đi về 1 phía → cây "lệch" (skewed) thành linked list. Để tránh, dùng AVL/Red-Black tree.

Câu 6. Big-O insert trong Trie với word độ dài L?

  • O(L)
  • O(N)
  • O(N · L)
  • O(log N)

Đáp án: O(L). Đi qua L ký tự, mỗi ký tự O(1) lookup trong children map. Không phụ thuộc số word đã insert.

Câu 7. Pre-order = ? In-order = ? Post-order = ?

  • Trái-Node-Phải; Node-Trái-Phải; Trái-Phải-Node
  • Node-Trái-Phải; Phải-Node-Trái; Trái-Phải-Node
  • Node-Trái-Phải; Trái-Node-Phải; Trái-Phải-Node
  • Trái-Phải-Node; Trái-Node-Phải; Node-Trái-Phải

Đáp án: Pre = Node-Trái-Phải, In = Trái-Node-Phải, Post = Trái-Phải-Node. "Pre/In/Post" chỉ vị trí thăm Node so với 2 con.

Câu 8. Database index thường dùng cấu trúc nào?

  • Trie
  • B-tree / B+ tree (cây nhiều con cho disk)
  • Linked list
  • Hash table không sort

Đáp án: B-tree/B+ tree. Mỗi node lưu nhiều key → giảm số lần đọc disk. MySQL InnoDB dùng B+ tree.

Tổng kết chương 9

  • ✅ Tree terminology: root, leaf, depth, height, level
  • ✅ 4 traversal: Pre/In/Post-order (DFS) và Level-order (BFS)
  • ✅ Iterative traversal dùng Stack (DFS) hoặc Queue (BFS) — phải biết cả recursive lẫn iterative
  • BST: in-order = sorted; search/insert/delete O(log n) avg, O(n) worst
  • ✅ Validate BST phải kiểm tra với min/max bound, không chỉ 1 cấp
  • ✅ Delete node 2 con: thay bằng successor
  • AVL / Red-Black tree = self-balancing BST cho O(log n) đảm bảo
  • Trie: O(L) insert/search/prefix — vũ khí autocomplete
  • ✅ Bài kinh điển: LCA, Diameter, Serialize tree
← Chương trước Chương 08: Searching Chương kế tiếp Chương 10: Heap →