Back to blog

Trees & Binary Search Trees: Cấu Trúc Dữ Liệu Phân Cấp

algorithmsdata-structurestreesbinary-search-treeinterview-prepdsa
Trees & Binary Search Trees: Cấu Trúc Dữ Liệu Phân Cấp

Giới Thiệu

Trees là cấu trúc dữ liệu phi tuyến tính quan trọng nhất trong khoa học máy tính. Khác với arrays, linked lists, stacks, và queues — tất cả đều tuần tự — trees tổ chức dữ liệu theo cấu trúc phân cấp cho phép thực hiện các thao tác hoàn toàn khác biệt.

Mỗi khi bạn duyệt file system, parse HTML, đánh giá biểu thức, hay dùng autocomplete — bạn đang tương tác với trees. Trong coding interviews, các bài toán về tree nằm trong số phổ biến nhất, xuất hiện trong gần 40% các buổi phỏng vấn kỹ thuật tại các công ty tech hàng đầu.

Bài viết này bắt đầu Phase 3: Cấu Trúc Dữ Liệu Phi Tuyến Tính trong series DSA. Các cấu trúc tuyến tính bạn đã học trước đó (arrays, linked lists, stacks, queues) sẽ là nền tảng — stacks hỗ trợ duyệt DFS, queues hỗ trợ duyệt BFS, và arrays/linked lists được dùng để cài đặt trees.

Bạn Sẽ Học Được Gì

✅ Hiểu thuật ngữ và tính chất của tree
✅ Cài đặt Binary Trees và Binary Search Trees từ đầu
✅ Nắm vững cả bốn cách duyệt tree (Inorder, Preorder, Postorder, Level-Order)
✅ Thực hiện các thao tác BST: insert, search, delete
✅ Hiểu balanced vs skewed trees và tại sao nó quan trọng
✅ Giải 8 bài toán phỏng vấn kinh điển về tree từng bước

Yêu Cầu Trước


1. Nền Tảng Về Trees

1.1 Tree Là Gì?

Tree là cấu trúc dữ liệu phân cấp gồm các nodes được kết nối bởi edges. Nó có một root node duy nhất ở trên cùng, và mọi node khác đều có đúng một parent.

1.2 Thuật Ngữ Về Tree

Thuật ngữĐịnh nghĩa
RootNode trên cùng, không có parent
ParentNode ngay phía trên một node cho trước
ChildNode ngay phía dưới một node cho trước
LeafNode không có children
EdgeKết nối giữa parent và child
HeightĐường dài nhất từ root đến leaf (height root = 0 hoặc 1, tùy convention)
DepthKhoảng cách từ root đến một node
LevelTất cả nodes ở cùng một depth
SubtreeMột node và tất cả con cháu của nó
DegreeSố children mà một node có

Tính chất quan trọng:

  • Tree có N nodes thì có đúng N - 1 edges
  • Có đúng một đường đi giữa hai nodes bất kỳ
  • Tree có height h có tối đa 2^(h+1) - 1 nodes (với binary trees)

1.3 Các Loại Trees

Trong bài này chúng ta sẽ tập trung chủ yếu vào Binary Trees và Binary Search Trees.


2. Binary Trees

Binary tree là tree mà mỗi node có tối đa 2 children, gọi là left child và right child.

2.1 Các Loại Binary Tree

Full Binary Tree — Mỗi node có 0 hoặc 2 children (không có node chỉ có 1 child):

Complete Binary Tree — Tất cả levels đều đầy trừ level cuối, được điền từ trái sang phải:

Perfect Binary Tree — Tất cả internal nodes có 2 children, tất cả leaves ở cùng level:

  • Level 0: 1 node, Level 1: 2 nodes, Level 2: 4 nodes...
  • Tổng nodes tại height h: 2^(h+1) - 1

Balanced Binary Tree — Chênh lệch height giữa subtree trái và phải của mọi node tối đa là 1.

Degenerate (Skewed) Tree — Mỗi node chỉ có một child — thực chất là linked list với các thao tác O(n):

2.2 Cài Đặt Binary Tree Node

// TypeScript - Binary Tree Node
class TreeNode<T> {
  val: T;
  left: TreeNode<T> | null;
  right: TreeNode<T> | null;
 
  constructor(val: T, left: TreeNode<T> | null = null, right: TreeNode<T> | null = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}
 
// Xây dựng tree thủ công
const root = new TreeNode(10);
root.left = new TreeNode(5);
root.right = new TreeNode(15);
root.left.left = new TreeNode(3);
root.left.right = new TreeNode(7);
root.right.left = new TreeNode(12);
root.right.right = new TreeNode(20);
# Python - Binary Tree Node
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
 
# Xây dựng tree thủ công
root = TreeNode(10)
root.left = TreeNode(5)
root.right = TreeNode(15)
root.left.left = TreeNode(3)
root.left.right = TreeNode(7)
root.right.left = TreeNode(12)
root.right.right = TreeNode(20)

3. Duyệt Tree — DFS và BFS

Duyệt tree là quá trình ghé thăm mỗi node đúng một lần. Có hai cách tiếp cận cơ bản:

  • Depth-First Search (DFS) — Đi sâu nhất có thể rồi mới quay lại (dùng stack)
  • Breadth-First Search (BFS) — Ghé thăm tất cả nodes ở level hiện tại trước khi đi sâu hơn (dùng queue)

3.1 DFS: Duyệt Inorder (Trái → Root → Phải)

Với BST, duyệt inorder sẽ ghé thăm nodes theo thứ tự tăng dần. Đây là cách duyệt quan trọng nhất cho BSTs.

Thứ tự ghé thăm: 3 → 5 → 7 → 10 → 12 → 15 → 20 (đã sắp xếp!)

// TypeScript - Duyệt Inorder
 
// Đệ quy (sạch sẽ và trực quan)
function inorderRecursive(root: TreeNode<number> | null): number[] {
  const result: number[] = [];
 
  function dfs(node: TreeNode<number> | null): void {
    if (!node) return;
    dfs(node.left);        // Trái
    result.push(node.val); // Root
    dfs(node.right);       // Phải
  }
 
  dfs(root);
  return result;
}
 
// Vòng lặp (dùng stack tường minh — quan trọng cho phỏng vấn)
function inorderIterative(root: TreeNode<number> | null): number[] {
  const result: number[] = [];
  const stack: TreeNode<number>[] = [];
  let current = root;
 
  while (current || stack.length > 0) {
    // Đi sang trái xa nhất có thể
    while (current) {
      stack.push(current);
      current = current.left;
    }
    // Xử lý node hiện tại
    current = stack.pop()!;
    result.push(current.val);
    // Chuyển sang subtree phải
    current = current.right;
  }
 
  return result;
}
# Python - Duyệt Inorder
 
# Đệ quy
def inorder_recursive(root: TreeNode | None) -> list[int]:
    result = []
 
    def dfs(node: TreeNode | None):
        if not node:
            return
        dfs(node.left)           # Trái
        result.append(node.val)  # Root
        dfs(node.right)          # Phải
 
    dfs(root)
    return result
 
# Vòng lặp
def inorder_iterative(root: TreeNode | None) -> list[int]:
    result = []
    stack = []
    current = root
 
    while current or stack:
        while current:
            stack.append(current)
            current = current.left
        current = stack.pop()
        result.append(current.val)
        current = current.right
 
    return result

3.2 DFS: Duyệt Preorder (Root → Trái → Phải)

Preorder ghé thăm root trước, rồi subtree trái, rồi subtree phải. Hữu ích cho việc sao chép/serialize trees.

Thứ tự ghé thăm: 10 → 5 → 3 → 7 → 15 → 12 → 20

// TypeScript - Duyệt Preorder
 
// Đệ quy
function preorderRecursive(root: TreeNode<number> | null): number[] {
  const result: number[] = [];
 
  function dfs(node: TreeNode<number> | null): void {
    if (!node) return;
    result.push(node.val); // Root
    dfs(node.left);        // Trái
    dfs(node.right);       // Phải
  }
 
  dfs(root);
  return result;
}
 
// Vòng lặp
function preorderIterative(root: TreeNode<number> | null): number[] {
  if (!root) return [];
  const result: number[] = [];
  const stack: TreeNode<number>[] = [root];
 
  while (stack.length > 0) {
    const node = stack.pop()!;
    result.push(node.val);
    // Push phải trước để trái được xử lý trước (stack là LIFO)
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }
 
  return result;
}
# Python - Duyệt Preorder
 
# Đệ quy
def preorder_recursive(root: TreeNode | None) -> list[int]:
    result = []
 
    def dfs(node: TreeNode | None):
        if not node:
            return
        result.append(node.val)  # Root
        dfs(node.left)           # Trái
        dfs(node.right)          # Phải
 
    dfs(root)
    return result
 
# Vòng lặp
def preorder_iterative(root: TreeNode | None) -> list[int]:
    if not root:
        return []
    result = []
    stack = [root]
 
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
 
    return result

3.3 DFS: Duyệt Postorder (Trái → Phải → Root)

Postorder ghé thăm root cuối cùng — xử lý children trước parent. Hữu ích cho việc xóa trees và đánh giá expression trees.

Thứ tự ghé thăm: 3 → 7 → 5 → 12 → 20 → 15 → 10

// TypeScript - Duyệt Postorder
 
// Đệ quy
function postorderRecursive(root: TreeNode<number> | null): number[] {
  const result: number[] = [];
 
  function dfs(node: TreeNode<number> | null): void {
    if (!node) return;
    dfs(node.left);        // Trái
    dfs(node.right);       // Phải
    result.push(node.val); // Root
  }
 
  dfs(root);
  return result;
}
 
// Vòng lặp (preorder biến đổi + đảo ngược)
function postorderIterative(root: TreeNode<number> | null): number[] {
  if (!root) return [];
  const result: number[] = [];
  const stack: TreeNode<number>[] = [root];
 
  while (stack.length > 0) {
    const node = stack.pop()!;
    result.push(node.val);
    // Push trái trước, rồi phải (ngược với preorder)
    if (node.left) stack.push(node.left);
    if (node.right) stack.push(node.right);
  }
 
  return result.reverse(); // Đảo ngược cho ra postorder
}
# Python - Duyệt Postorder
 
# Đệ quy
def postorder_recursive(root: TreeNode | None) -> list[int]:
    result = []
 
    def dfs(node: TreeNode | None):
        if not node:
            return
        dfs(node.left)           # Trái
        dfs(node.right)          # Phải
        result.append(node.val)  # Root
 
    dfs(root)
    return result
 
# Vòng lặp (preorder biến đổi + đảo ngược)
def postorder_iterative(root: TreeNode | None) -> list[int]:
    if not root:
        return []
    result = []
    stack = [root]
 
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.left:
            stack.append(node.left)
        if node.right:
            stack.append(node.right)
 
    return result[::-1]

3.4 BFS: Duyệt Level-Order

Ghé thăm nodes theo từng level, từ trái sang phải. Dùng queue (đúng cái mình đã học ở bài trước!).

Thứ tự ghé thăm: [10] → [5, 15] → [3, 7, 12, 20]

// TypeScript - Duyệt Level-Order (BFS)
function levelOrder(root: TreeNode<number> | null): number[][] {
  if (!root) return [];
  const result: number[][] = [];
  const queue: TreeNode<number>[] = [root];
 
  while (queue.length > 0) {
    const levelSize = queue.length;
    const currentLevel: number[] = [];
 
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift()!;
      currentLevel.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
 
    result.push(currentLevel);
  }
 
  return result;
  // Trả về: [[10], [5, 15], [3, 7, 12, 20]]
}
# Python - Duyệt Level-Order (BFS)
from collections import deque
 
def level_order(root: TreeNode | None) -> list[list[int]]:
    if not root:
        return []
    result = []
    queue = deque([root])
 
    while queue:
        level_size = len(queue)
        current_level = []
 
        for _ in range(level_size):
            node = queue.popleft()
            current_level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
 
        result.append(current_level)
 
    return result
    # Trả về: [[10], [5, 15], [3, 7, 12, 20]]

3.5 Tổng Kết Các Cách Duyệt

Cách duyệtThứ tựUse CaseCấu trúc dữ liệu
InorderTrái → Root → PhảiLấy thứ tự sắp xếp từ BSTStack (DFS)
PreorderRoot → Trái → PhảiSao chép/serialize treeStack (DFS)
PostorderTrái → Phải → RootXóa tree, đánh giá expressionStack (DFS)
Level-OrderTheo từng levelIn levels, tìm min depthQueue (BFS)

Mẹo nhớ: Tên cho biết khi nào bạn ghé root — preorder (root đầu tiên), inorder (root ở giữa), postorder (root cuối cùng).


4. Binary Search Trees (BST)

Binary Search Tree là binary tree với một tính chất then chốt:

Với mọi node: tất cả giá trị trong subtree trái < giá trị node < tất cả giá trị trong subtree phải

Tính chất này cho phép các thao tác search, insert, và delete trong O(log n) (khi tree balanced).

4.1 Tìm Kiếm Trong BST

Bắt đầu từ root. Nếu target < node hiện tại, đi sang trái. Nếu target > node hiện tại, đi sang phải. Lặp lại cho đến khi tìm thấy hoặc null.

// TypeScript - Tìm kiếm BST
 
// Đệ quy
function searchBST(root: TreeNode<number> | null, target: number): TreeNode<number> | null {
  if (!root) return null;
  if (target === root.val) return root;
  if (target < root.val) return searchBST(root.left, target);
  return searchBST(root.right, target);
}
 
// Vòng lặp (ưu tiên — không có risk stack overflow)
function searchBSTIterative(root: TreeNode<number> | null, target: number): TreeNode<number> | null {
  let current = root;
  while (current) {
    if (target === current.val) return current;
    if (target < current.val) current = current.left;
    else current = current.right;
  }
  return null;
}
# Python - Tìm kiếm BST
 
# Đệ quy
def search_bst(root: TreeNode | None, target: int) -> TreeNode | None:
    if not root:
        return None
    if target == root.val:
        return root
    if target < root.val:
        return search_bst(root.left, target)
    return search_bst(root.right, target)
 
# Vòng lặp
def search_bst_iterative(root: TreeNode | None, target: int) -> TreeNode | None:
    current = root
    while current:
        if target == current.val:
            return current
        elif target < current.val:
            current = current.left
        else:
            current = current.right
    return None

Độ phức tạp thời gian: O(h) với h là height của tree. O(log n) cho BST balanced, O(n) cho BST skewed.

4.2 Chèn Vào BST

Tìm vị trí đúng (cùng logic như search), rồi gắn node mới như leaf.

// TypeScript - Chèn BST
function insertBST(root: TreeNode<number> | null, val: number): TreeNode<number> {
  if (!root) return new TreeNode(val);
 
  if (val < root.val) {
    root.left = insertBST(root.left, val);
  } else if (val > root.val) {
    root.right = insertBST(root.right, val);
  }
  // Nếu val === root.val, bỏ qua duplicate
 
  return root;
}
# Python - Chèn BST
def insert_bst(root: TreeNode | None, val: int) -> TreeNode:
    if not root:
        return TreeNode(val)
 
    if val < root.val:
        root.left = insert_bst(root.left, val)
    elif val > root.val:
        root.right = insert_bst(root.right, val)
 
    return root

4.3 Xóa Trong BST

Thao tác BST phức tạp nhất. Có ba trường hợp cần xử lý:

Trường hợp 1: Node là leaf — Đơn giản xóa nó.

Trường hợp 2: Node có một child — Thay node bằng child của nó.

Trường hợp 3: Node có hai children — Tìm inorder successor (node nhỏ nhất trong subtree phải), sao chép giá trị vào node hiện tại, rồi xóa successor.

Sau khi xóa: Thay 5 bằng 6 (inorder successor), rồi xóa 6 gốc.

// TypeScript - Xóa BST
function deleteBST(root: TreeNode<number> | null, val: number): TreeNode<number> | null {
  if (!root) return null;
 
  if (val < root.val) {
    root.left = deleteBST(root.left, val);
  } else if (val > root.val) {
    root.right = deleteBST(root.right, val);
  } else {
    // Tìm thấy node cần xóa
 
    // Trường hợp 1 & 2: Không có child hoặc một child
    if (!root.left) return root.right;
    if (!root.right) return root.left;
 
    // Trường hợp 3: Hai children — tìm inorder successor
    let successor = root.right;
    while (successor.left) {
      successor = successor.left;
    }
    root.val = successor.val;
    root.right = deleteBST(root.right, successor.val);
  }
 
  return root;
}
# Python - Xóa BST
def delete_bst(root: TreeNode | None, val: int) -> TreeNode | None:
    if not root:
        return None
 
    if val < root.val:
        root.left = delete_bst(root.left, val)
    elif val > root.val:
        root.right = delete_bst(root.right, val)
    else:
        # Tìm thấy node cần xóa
 
        # Trường hợp 1 & 2: Không có child hoặc một child
        if not root.left:
            return root.right
        if not root.right:
            return root.left
 
        # Trường hợp 3: Hai children — tìm inorder successor
        successor = root.right
        while successor.left:
            successor = successor.left
        root.val = successor.val
        root.right = delete_bst(root.right, successor.val)
 
    return root

4.4 Tổng Kết Độ Phức Tạp BST

Thao tácTrung bình (Balanced)Tệ nhất (Skewed)
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)
Duyệt inorderO(n)O(n)
Không gian (đệ quy)O(log n) stackO(n) stack

Vấn đề cân bằng: Nếu bạn chèn dữ liệu đã sắp xếp [1, 2, 3, 4, 5] vào BST, bạn sẽ có skewed tree (linked list). Tất cả thao tác thoái hóa thành O(n). Đây là lý do balanced BSTs (AVL, Red-Black) được phát minh.


5. Balanced BSTs — Tổng Quan Ngắn

Balanced BSTs tự động duy trì height O(log n) thông qua rotations sau các thao tác insert/delete.

5.1 AVL Trees

  • Cân bằng nghiêm ngặt: Chênh lệch height giữa subtree trái và phải ≤ 1 cho mọi node
  • Dùng rotations (left, right, left-right, right-left) để tái cân bằng
  • Tra cứu nhanh hơn Red-Black trees (cân bằng nghiêm ngặt hơn)
  • Chèn/xóa chậm hơn (nhiều rotations hơn)

5.2 Red-Black Trees

  • Cân bằng nới lỏng: Không đường đi nào từ root đến leaf dài hơn gấp đôi đường đi khác
  • Mỗi node được tô đỏ hoặc đen, với các quy tắc đảm bảo cân bằng
  • Được dùng trong hầu hết standard libraries:
    • Java: TreeMap, TreeSet
    • C++: std::map, std::set
    • Linux kernel: Process scheduling, memory management

5.3 Khi Nào Dùng Gì

Cấu trúcLookupInsert/DeleteUse Case
BST (không balanced)O(log n) avg, O(n) worstO(log n) avg, O(n) worstHọc tập, trường hợp đơn giản
AVL TreeO(log n) đảm bảoO(log n) với nhiều rotationsWorkloads đọc nhiều
Red-Black TreeO(log n) đảm bảoO(log n) với ít rotationsĐa mục đích (standard libs)
Hash MapO(1) trung bìnhO(1) trung bìnhKhi thứ tự không quan trọng

Mẹo phỏng vấn: Bạn hiếm khi cần cài đặt AVL hay Red-Black trees từ đầu. Nhưng bạn nên hiểu tại sao chúng tồn tại và trade-offs của chúng.


6. Trie (Prefix Tree) — Giới Thiệu

Trie là cấu trúc dạng tree dùng cho việc tìm kiếm chuỗi và khớp prefix hiệu quả. Mỗi node đại diện cho một ký tự, và các đường đi từ root đến các node được đánh dấu tạo thành các từ hoàn chỉnh.

Các từ lưu trữ: car, cat, dog

// TypeScript - Cài Đặt Trie Cơ Bản
class TrieNode {
  children: Map<string, TrieNode> = new Map();
  isEnd: boolean = false;
}
 
class Trie {
  root: TrieNode = new TrieNode();
 
  insert(word: string): void {
    let node = this.root;
    for (const char of word) {
      if (!node.children.has(char)) {
        node.children.set(char, new TrieNode());
      }
      node = node.children.get(char)!;
    }
    node.isEnd = true;
  }
 
  search(word: string): boolean {
    const node = this.findNode(word);
    return node !== null && node.isEnd;
  }
 
  startsWith(prefix: string): boolean {
    return this.findNode(prefix) !== null;
  }
 
  private findNode(prefix: string): TrieNode | null {
    let node = this.root;
    for (const char of prefix) {
      if (!node.children.has(char)) return null;
      node = node.children.get(char)!;
    }
    return node;
  }
}
 
// Sử dụng
const trie = new Trie();
trie.insert("car");
trie.insert("cat");
trie.insert("dog");
console.log(trie.search("car"));      // true
console.log(trie.search("ca"));       // false
console.log(trie.startsWith("ca"));   // true
# Python - Cài Đặt Trie Cơ Bản
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False
 
class Trie:
    def __init__(self):
        self.root = TrieNode()
 
    def insert(self, word: str) -> None:
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_end = True
 
    def search(self, word: str) -> bool:
        node = self._find_node(word)
        return node is not None and node.is_end
 
    def starts_with(self, prefix: str) -> bool:
        return self._find_node(prefix) is not None
 
    def _find_node(self, prefix: str) -> TrieNode | None:
        node = self.root
        for char in prefix:
            if char not in node.children:
                return None
            node = node.children[char]
        return node
Thao tácĐộ phức tạp thời gianKhông gian
InsertO(m) với m = độ dài từO(m) mỗi từ
SearchO(m)O(1)
Prefix searchO(m)O(1)

Use cases: Autocomplete, kiểm tra chính tả, bảng routing IP, tra cứu từ điển.


7. Các Bài Toán Phỏng Vấn Kinh Điển

Bài 1: Độ Sâu Tối Đa Của Binary Tree

Cho binary tree, tìm độ sâu tối đa (số nodes trên đường đi dài nhất từ root đến leaf xa nhất).

Cách tiếp cận: DFS đệ quy — độ sâu của tree là 1 + max(độ sâu trái, độ sâu phải).

// TypeScript - Độ Sâu Tối Đa
function maxDepth(root: TreeNode<number> | null): number {
  if (!root) return 0;
  return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// Tree: [10, 5, 15, 3, 7] → maxDepth = 3
# Python - Độ Sâu Tối Đa
def max_depth(root: TreeNode | None) -> int:
    if not root:
        return 0
    return 1 + max(max_depth(root.left), max_depth(root.right))

Thời gian: O(n) — ghé thăm mọi node. Không gian: O(h) — recursion stack.


Bài 2: Lật Binary Tree

Lật binary tree (đổi children trái và phải ở mọi node).

Bài toán nổi tiếng mà tác giả Homebrew Max Howell không giải được ở Google.

// TypeScript - Lật Binary Tree
function invertTree(root: TreeNode<number> | null): TreeNode<number> | null {
  if (!root) return null;
 
  // Đổi children
  const temp = root.left;
  root.left = root.right;
  root.right = temp;
 
  // Đệ quy
  invertTree(root.left);
  invertTree(root.right);
 
  return root;
}
# Python - Lật Binary Tree
def invert_tree(root: TreeNode | None) -> TreeNode | None:
    if not root:
        return None
 
    root.left, root.right = root.right, root.left
 
    invert_tree(root.left)
    invert_tree(root.right)
 
    return root

Thời gian: O(n). Không gian: O(h).


Bài 3: Kiểm Tra Binary Search Tree Hợp Lệ

Xác định xem binary tree có phải BST hợp lệ không.

Insight quan trọng: Đừng chỉ kiểm tra left.val < root.val < right.val — bạn phải đảm bảo TẤT CẢ nodes trong subtree trái nhỏ hơn root, và TẤT CẢ ở phải lớn hơn. Truyền khoảng giá trị hợp lệ xuống.

// TypeScript - Kiểm Tra BST
function isValidBST(root: TreeNode<number> | null): boolean {
  function validate(
    node: TreeNode<number> | null,
    min: number,
    max: number
  ): boolean {
    if (!node) return true;
    if (node.val <= min || node.val >= max) return false;
    return (
      validate(node.left, min, node.val) &&
      validate(node.right, node.val, max)
    );
  }
 
  return validate(root, -Infinity, Infinity);
}
# Python - Kiểm Tra BST
def is_valid_bst(root: TreeNode | None) -> bool:
    def validate(node: TreeNode | None, min_val: float, max_val: float) -> bool:
        if not node:
            return True
        if node.val <= min_val or node.val >= max_val:
            return False
        return (
            validate(node.left, min_val, node.val) and
            validate(node.right, node.val, max_val)
        )
 
    return validate(root, float('-inf'), float('inf'))

Cách khác: Duyệt inorder phải cho ra dãy tăng nghiêm ngặt.

Thời gian: O(n). Không gian: O(h).


Bài 4: Tổ Tiên Chung Gần Nhất (LCA)

Cho hai nodes p và q trong binary tree, tìm tổ tiên chung gần nhất của chúng.

Định nghĩa LCA: Node sâu nhất mà là tổ tiên của cả p và q.

// TypeScript - LCA của Binary Tree
function lowestCommonAncestor(
  root: TreeNode<number> | null,
  p: TreeNode<number>,
  q: TreeNode<number>
): TreeNode<number> | null {
  if (!root || root === p || root === q) return root;
 
  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);
 
  // Nếu cả hai bên trả về non-null, root là LCA
  if (left && right) return root;
  // Ngược lại, LCA ở bên trả về non-null
  return left ?? right;
}
 
// Cho BST — tận dụng tính chất sắp xếp
function lcaBST(
  root: TreeNode<number> | null,
  p: TreeNode<number>,
  q: TreeNode<number>
): TreeNode<number> | null {
  if (!root) return null;
 
  if (p.val < root.val && q.val < root.val) {
    return lcaBST(root.left, p, q);    // Cả hai bên trái
  }
  if (p.val > root.val && q.val > root.val) {
    return lcaBST(root.right, p, q);   // Cả hai bên phải
  }
  return root; // Điểm chia — đây là LCA
}
# Python - LCA của Binary Tree
def lowest_common_ancestor(root, p, q):
    if not root or root == p or root == q:
        return root
 
    left = lowest_common_ancestor(root.left, p, q)
    right = lowest_common_ancestor(root.right, p, q)
 
    if left and right:
        return root
    return left or right
 
# Cho BST
def lca_bst(root, p, q):
    if not root:
        return None
    if p.val < root.val and q.val < root.val:
        return lca_bst(root.left, p, q)
    if p.val > root.val and q.val > root.val:
        return lca_bst(root.right, p, q)
    return root

Thời gian: O(n) cho binary tree, O(h) cho BST. Không gian: O(h).


Bài 5: Kiểm Tra Tree Có Balanced Không

Tree balanced nếu chênh lệch height giữa subtree trái và phải của mọi node tối đa là 1.

// TypeScript - Kiểm Tra Balanced
function isBalanced(root: TreeNode<number> | null): boolean {
  function height(node: TreeNode<number> | null): number {
    if (!node) return 0;
 
    const leftHeight = height(node.left);
    if (leftHeight === -1) return -1; // Subtree trái không balanced
 
    const rightHeight = height(node.right);
    if (rightHeight === -1) return -1; // Subtree phải không balanced
 
    if (Math.abs(leftHeight - rightHeight) > 1) return -1; // Node hiện tại không balanced
 
    return 1 + Math.max(leftHeight, rightHeight);
  }
 
  return height(root) !== -1;
}
# Python - Kiểm Tra Balanced
def is_balanced(root: TreeNode | None) -> bool:
    def height(node: TreeNode | None) -> int:
        if not node:
            return 0
 
        left_h = height(node.left)
        if left_h == -1:
            return -1
 
        right_h = height(node.right)
        if right_h == -1:
            return -1
 
        if abs(left_h - right_h) > 1:
            return -1
 
        return 1 + max(left_h, right_h)
 
    return height(root) != -1

Mẹo: Trả về -1 làm sentinel cho "không balanced" để short-circuit sớm.

Thời gian: O(n). Không gian: O(h).


Bài 6: Path Sum

Cho binary tree và target sum, xác định xem tree có đường đi root-to-leaf mà tổng tất cả giá trị bằng target sum không.

// TypeScript - Path Sum
function hasPathSum(root: TreeNode<number> | null, targetSum: number): boolean {
  if (!root) return false;
 
  // Leaf node — kiểm tra tổng còn lại có bằng giá trị node này không
  if (!root.left && !root.right) {
    return targetSum === root.val;
  }
 
  const remaining = targetSum - root.val;
  return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining);
}
# Python - Path Sum
def has_path_sum(root: TreeNode | None, target_sum: int) -> bool:
    if not root:
        return False
 
    if not root.left and not root.right:
        return target_sum == root.val
 
    remaining = target_sum - root.val
    return has_path_sum(root.left, remaining) or has_path_sum(root.right, remaining)

Thời gian: O(n). Không gian: O(h).


Bài 7: Serialize và Deserialize Binary Tree

Thiết kế thuật toán để serialize binary tree thành chuỗi và deserialize ngược lại.

// TypeScript - Serialize/Deserialize (Preorder)
function serialize(root: TreeNode<number> | null): string {
  const result: string[] = [];
 
  function dfs(node: TreeNode<number> | null): void {
    if (!node) {
      result.push("null");
      return;
    }
    result.push(String(node.val));
    dfs(node.left);
    dfs(node.right);
  }
 
  dfs(root);
  return result.join(",");
}
 
function deserialize(data: string): TreeNode<number> | null {
  const values = data.split(",");
  let index = 0;
 
  function dfs(): TreeNode<number> | null {
    if (values[index] === "null") {
      index++;
      return null;
    }
 
    const node = new TreeNode(Number(values[index]));
    index++;
    node.left = dfs();
    node.right = dfs();
    return node;
  }
 
  return dfs();
}
# Python - Serialize/Deserialize (Preorder)
def serialize(root: TreeNode | None) -> str:
    result = []
 
    def dfs(node):
        if not node:
            result.append("null")
            return
        result.append(str(node.val))
        dfs(node.left)
        dfs(node.right)
 
    dfs(root)
    return ",".join(result)
 
def deserialize(data: str) -> TreeNode | None:
    values = iter(data.split(","))
 
    def dfs():
        val = next(values)
        if val == "null":
            return None
        node = TreeNode(int(val))
        node.left = dfs()
        node.right = dfs()
        return node
 
    return dfs()

Thời gian: O(n) cho cả hai thao tác. Không gian: O(n).


Bài 8: Xây Tree Từ Inorder và Preorder

Cho mảng inorder và preorder traversal, xây dựng lại binary tree.

Insight quan trọng: Phần tử đầu tiên của preorder luôn là root. Tìm root đó trong inorder — mọi thứ bên trái là subtree trái, mọi thứ bên phải là subtree phải.

// TypeScript - Xây Tree từ Preorder + Inorder
function buildTree(preorder: number[], inorder: number[]): TreeNode<number> | null {
  const inorderMap = new Map<number, number>();
  inorder.forEach((val, idx) => inorderMap.set(val, idx));
 
  let preIdx = 0;
 
  function build(left: number, right: number): TreeNode<number> | null {
    if (left > right) return null;
 
    const rootVal = preorder[preIdx++];
    const root = new TreeNode(rootVal);
    const mid = inorderMap.get(rootVal)!;
 
    root.left = build(left, mid - 1);
    root.right = build(mid + 1, right);
 
    return root;
  }
 
  return build(0, inorder.length - 1);
}
# Python - Xây Tree từ Preorder + Inorder
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
    inorder_map = {val: idx for idx, val in enumerate(inorder)}
    pre_idx = [0]  # Dùng list để cho phép thay đổi trong nested function
 
    def build(left: int, right: int) -> TreeNode | None:
        if left > right:
            return None
 
        root_val = preorder[pre_idx[0]]
        pre_idx[0] += 1
        root = TreeNode(root_val)
        mid = inorder_map[root_val]
 
        root.left = build(left, mid - 1)
        root.right = build(mid + 1, right)
 
        return root
 
    return build(0, len(inorder) - 1)

Thời gian: O(n). Không gian: O(n) cho hash map.


8. Hướng Dẫn Nhận Diện Pattern

Khi gặp bài toán tree, nhận diện các pattern sau:

PatternKhi nào dùngBài toán ví dụ
DFS đệ quyHầu hết bài toán treeMax depth, invert, validate BST
BFS (level-order)Xử lý theo từng levelLevel order traversal, min depth, right side view
Top-down DFSTruyền thông tin từ parent xuống childPath sum, validate BST với range
Bottom-up DFSTính toán thông tin từ children lên parentMax depth, balanced check, diameter
Tính chất BSTDữ liệu có thứ tự, tìm kiếm theo giá trịSearch, insert, delete, LCA trong BST
Serialize/DeserializeLưu trữ/truyền cấu trúc treeMã hóa preorder, mã hóa level-order
Hai cách duyệtXây dựng lại treePreorder + inorder, postorder + inorder

Phương pháp phỏng vấn:

  • "Tìm depth/height" → Bottom-up DFS (trả height từ leaves lên)
  • "Tìm đường đi" → Top-down DFS (tích lũy path từ root xuống)
  • "Theo từng level" → BFS với queue
  • "Thứ tự sắp xếp" → BST inorder traversal
  • "Kiểm tra cấu trúc" → Đệ quy với ràng buộc (truyền khoảng min/max)

9. Bảng Tham Chiếu Độ Phức Tạp

Độ Phức Tạp Thời Gian Theo Thao Tác

Thao tácBST (avg)BST (worst)Balanced BSTTrie
SearchO(log n)O(n)O(log n)O(m)
InsertO(log n)O(n)O(log n)O(m)
DeleteO(log n)O(n)O(log n)O(m)
TraversalO(n)O(n)O(n)—
Min/MaxO(log n)O(n)O(log n)—

n = số nodes, m = độ dài key/word

Độ Phức Tạp Không Gian

Cấu trúcKhông gianRecursive Stack
Binary TreeO(n)O(h) mỗi lần gọi
BSTO(n)O(h) mỗi lần gọi
TrieO(ALPHABET × m × n)O(m) mỗi lần gọi

h = height: O(log n) balanced, O(n) skewed


10. Bài Tập Luyện

Easy (Bắt Đầu Ở Đây)

#Bài toánConcept chính
1Maximum Depth of Binary TreeBottom-up DFS
2Invert Binary TreeRecursive swap
3Same TreeParallel DFS
4Symmetric TreeMirror comparison
5Path SumTop-down DFS

Medium (Xây Dựng Sự Tự Tin)

#Bài toánConcept chính
6Validate BSTRange-based DFS
7Level Order TraversalBFS với queue
8Lowest Common AncestorRecursive split
9BST IteratorControlled inorder
10Build Tree (Preorder + Inorder)Divide and conquer

Hard (Sẵn Sàng Phỏng Vấn)

#Bài toánConcept chính
11Serialize/DeserializePreorder encoding
12Binary Tree Maximum Path SumBottom-up với global max
13Implement TriePrefix tree design
14Word Search IITrie + backtracking
15Count Nodes in Complete TreeBinary search on levels

Cách tiếp cận khuyến nghị: Giải tất cả bài Easy trước, rồi Medium. Chỉ thử Hard khi bạn đã thoải mái với các pattern.


Tóm Tắt và Những Điểm Chính

Trees:
✅ Cấu trúc dữ liệu phân cấp với root, quan hệ parent-child, và leaves
✅ Binary tree = tối đa 2 children mỗi node; BST thêm tính chất sắp xếp
✅ Height quyết định performance: balanced O(log n) vs skewed O(n)

Các cách duyệt:
✅ Inorder (Trái-Root-Phải) cho thứ tự sắp xếp trong BST — cách duyệt phổ biến nhất
✅ Preorder (Root-Trái-Phải) cho serialization, Postorder (Trái-Phải-Root) cho deletion
✅ Level-order (BFS) dùng queue để xử lý theo từng level

Các thao tác BST:
✅ Search, insert đơn giản — đi trái/phải dựa trên so sánh
✅ Delete có 3 trường hợp — leaf, một child, hai children (dùng inorder successor)
✅ Tất cả O(h) — balanced trees đảm bảo O(log n)

Interview Patterns:
✅ Hầu hết bài tree dùng DFS đệ quy — bắt đầu với base case if (!node) return
✅ Top-down = truyền thông tin xuống (path sum), Bottom-up = trả thông tin lên (max depth)
✅ Bài BST tận dụng tính chất sắp xếp cho thao tác O(log n)
✅ Trie cho bài prefix chuỗi, cài đặt với hash map children


Tiếp Theo Là Gì?

Với trees đã nắm vững, bạn sẵn sàng chinh phục graphs — dạng tổng quát nhất của cấu trúc dữ liệu phi tuyến tính. Trees thực ra là trường hợp đặc biệt của graphs (liên thông, không chu trình, có root).

Tiếp tục series DSA:

Tiếp theo: Graphs & Graph Algorithms — BFS, DFS, đường đi ngắn nhất, topological sort
Trước đó: Stacks & Queues — Cấu trúc dữ liệu LIFO và FIFO
Tổng quan series: Data Structures & Algorithms Roadmap

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.