Đệ quy là ngôn ngữ tự nhiên của cây. Học cách "tin tưởng" vào lời gọi đệ quy con (leap of faith), thiết kế trả về thông tin tổng hợp, và áp dụng Divide & Conquer.
n == 0, node is None.fib(n-1) + fib(n-2).Nếu thiếu base case → vô tận, stack overflow. Nếu recursive case không thực sự nhỏ hơn → cũng vô tận.
def factorial(n):
if n <= 1: return 1 # base case
return n * factorial(n - 1) # recursive case — gọi với n nhỏ hơn
Hai dòng — toàn bộ định nghĩa giai thừa.
Đa số sinh viên thất bại với đệ quy vì cố theo dấu mọi lời gọi trong đầu. Đó là sai lầm.
def total(root):
if not root: return 0 # base
return root.val + total(root.left) + total(root.right)
# Tin rằng total(root.left) trả tổng cây con trái — không cần lo cách nó tính
Đừng cố hình dung "total đi đến đâu rồi quay lại". Hãy nghĩ: "Nếu hai cây con có tổng đúng, tổng cả cây = root + tổng trái + tổng phải".
Đây là kỹ năng quan trọng nhất của tuần. Khi nắm được, mọi bài tree trở nên dễ.
| Aspect | Recursion | Iteration |
|---|---|---|
| Đọc / viết | Tự nhiên cho cấu trúc cây, đệ quy | Tự nhiên cho mảng, vòng lặp tuần tự |
| Space | O(depth) — call stack | O(1) thường |
| Performance | Có overhead gọi hàm | Nhanh hơn ~2-3 lần thường |
| Stack overflow | Có thể với đệ quy sâu (n > 10⁴) | Không bao giờ |
| Tail call optimisation | Python KHÔNG có (khác Scheme/Haskell) | — |
Trong phỏng vấn, nếu được hỏi cả hai cách (như Reverse Linked List ở Tuần 5), nên biết để đáp ứng.
None, mảng rỗng, n = 0/1.# SAI — n không nhỏ đi
def f(n):
if n == 0: return 0
return f(n) + 1 # vô tận!# Bài Path Sum II — backtracking với list
def dfs(node, path):
path.append(node.val)
...
# QUÊN path.pop() ở cuối → các nhánh sau bị "ô nhiễm"
Sẽ đào sâu ở Tuần 11 (Backtracking).Python mặc định giới hạn đệ quy ~1000. Tăng bằng sys.setrecursionlimit(10000) nếu cần.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
| Phép duyệt | Thứ tự | Kết quả trên ví dụ |
|---|---|---|
| Preorder | Root → Left → Right | 1, 2, 4, 5, 3, 6 |
| Inorder | Left → Root → Right | 4, 2, 5, 1, 3, 6 |
| Postorder | Left → Right → Root | 4, 5, 2, 6, 3, 1 |
| Level-order | Theo mức (BFS) | 1, 2, 3, 4, 5, 6 |
Tính chất quan trọng: Inorder của BST cho ra sequence sort tăng dần — dùng để verify BST hoặc tìm phần tử thứ k nhỏ nhất.
def preorder(root):
if not root: return
print(root.val) # xử lý ROOT
preorder(root.left)
preorder(root.right)
def inorder(root):
if not root: return
inorder(root.left)
print(root.val) # xử lý ROOT giữa
inorder(root.right)
def postorder(root):
if not root: return
postorder(root.left)
postorder(root.right)
print(root.val) # xử lý ROOT cuối
def inorder(root):
stack, cur = [], root
while cur or stack:
while cur:
stack.append(cur) # đi sâu nhất bên trái
cur = cur.left
cur = stack.pop()
print(cur.val)
cur = cur.right
Time O(n) cho mọi phép duyệt; Space O(h) với h = chiều cao.
Thay vì DFS, đôi khi cần duyệt theo từng tầng (level). Dùng deque như queue:
from collections import deque
def levelOrder(root):
if not root: return []
res = []
q = deque([root])
while q:
level = []
for _ in range(len(q)): # snapshot kích thước hiện tại
node = q.popleft()
level.append(node.val)
if node.left: q.append(node.left)
if node.right: q.append(node.right)
res.append(level)
return res
for _ in range(len(q)) đóng băng kích thước queue trước khi vòng lặp bắt đầu →
đảm bảo chỉ xử lý các node cùng level. Nếu không snapshot, queue sẽ tăng lên trong khi lặp → sai logic.
Pattern này xuất hiện ở mọi bài "level-by-level".
Áp dụng: Right Side View, Zigzag Traversal, Average of Levels, Connect Next Right Pointers.
| Aspect | Top-down | Bottom-up |
|---|---|---|
| Hướng truyền | Root → Leaf (truyền tham số xuống) | Leaf → Root (trả giá trị lên) |
| Khi nào dùng? | Cần biết "đường đi từ root đến node hiện tại" | Cần tổng hợp thông tin từ cây con |
| Cú pháp | Hàm phụ với tham số tích luỹ | Return value từ đệ quy |
| Ví dụ | Path Sum, Same Tree, Validate BST | Maximum Depth, Diameter, LCA |
def helper(node, accumulated_info):
if not node: return
new_info = combine(accumulated_info, node.val)
helper(node.left, new_info)
helper(node.right, new_info)
def helper(node):
if not node: return base_value
left_info = helper(node.left)
right_info = helper(node.right)
return combine(node.val, left_info, right_info)
Đề (LeetCode #104): Cho root của cây nhị phân. Tìm chiều sâu lớn nhất (số node trên đường dài nhất từ root đến leaf).
def maxDepth(root):
if not root: return 0 # base — cây rỗng có chiều sâu 0
left = maxDepth(root.left) # tin: trả đúng chiều sâu cây con trái
right = maxDepth(root.right)
return 1 + max(left, right) # 1 (cho root hiện tại) + chiều sâu lớn hơn của 2 con
maxDepth(4) = 1 + max(0, 0) = 1maxDepth(5) = 1 + max(0, 0) = 1maxDepth(6) = 1 + max(0, 0) = 1maxDepth(2) = 1 + max(1, 1) = 2maxDepth(3) = 1 + max(0, 1) = 2maxDepth(1) = 1 + max(2, 2) = 3 ✓Time O(n), Space O(h).
maxDepth(1), ta không cần đi vào chi tiết maxDepth(2) hay maxDepth(3) hoạt động như thế nào.
Tin rằng chúng đúng — và ráp lại theo công thức.
Đề (LeetCode #100): Cho hai cây p, q. Trả True nếu cấu trúc và giá trị giống nhau.
def isSameTree(p, q):
if not p and not q: return True # cả hai rỗng → đúng
if not p or not q: return False # một rỗng → sai
if p.val != q.val: return False # giá trị khác → sai
return (isSameTree(p.left, q.left)
and isSameTree(p.right, q.right))
Time O(n) với n = số node của cây nhỏ hơn (dừng sớm khi khác); Space O(h).
def isSameTree(p, q):
if not p or not q: return p is q # hai cùng None → True; một None → False
return p.val == q.val and \
isSameTree(p.left, q.left) and \
isSameTree(p.right, q.right)
Đề (LeetCode #101): Trả True nếu cây đối xứng qua trục giữa.
Cây đối xứng ⟺ cây con trái và cây con phải là phản chiếu của nhau:
def isSymmetric(root):
def mirror(a, b):
if not a and not b: return True
if not a or not b: return False
return (a.val == b.val
and mirror(a.left, b.right)
and mirror(a.right, b.left))
return mirror(root.left, root.right) if root else True
Cùng tinh thần Same Tree, nhưng so sánh chéo thay vì cùng phía.
Time O(n), Space O(h).
Đề (LeetCode #226): Đổi left và right tại mọi node.
def invertTree(root):
if not root: return None
root.left, root.right = invertTree(root.right), invertTree(root.left)
return root
Time O(n), Space O(h).
Một dòng để invert. Tại sao đúng?
invertTree(root.left) trả về cây con trái đã được đảo.invertTree(root.right) trả về cây con phải đã được đảo.for _ in range(len(q))?for _ in range(len(q)) giải quyết.Pattern này xuất hiện ở: Diameter, Max Path Sum, Validate BST, House Robber III. Đây là pattern đặc trưng của bài tree Medium/Hard.
Đề (LeetCode #543): Diameter = số cạnh trên đường dài nhất giữa hai node bất kỳ. Đường có thể không đi qua root.
Đường dài nhất qua node X có cấu trúc: nhánh trái sâu nhất + node X + nhánh phải sâu nhất = depth(X.left) + depth(X.right).
Đáp án = max của tổng này qua mọi node.
def diameterOfBinaryTree(root):
diameter = 0
def depth(node):
nonlocal diameter
if not node: return 0
left = depth(node.left)
right = depth(node.right)
diameter = max(diameter, left + right) # cập nhật ans
return 1 + max(left, right) # trả depth lên cha
depth(root)
return diameter
Time O(n), Space O(h).
Bài này minh hoạ rõ pattern "thông tin trả lên ≠ ans":
depth(node) trả về chiều sâu — để cha dùng.diameter được cập nhật như side effect — đáp án thực sự.Vì cha không thể "ghép" diameter của con trái với diameter của con phải để ra diameter của cha. Hai diameter của con có thể là đường không đi qua node hiện tại.
Cha chỉ cần biết "nhánh sâu nhất xuyên qua mỗi con" để tính đường mới qua chính cha.
nonlocal hoặc instance variable.Đề Path Sum (LeetCode #112): Trả True nếu có đường từ root đến leaf có tổng = target.
def hasPathSum(root, target):
if not root: return False
if not root.left and not root.right: # leaf
return root.val == target
remain = target - root.val
return (hasPathSum(root.left, remain)
or hasPathSum(root.right, remain))
Top-down — truyền remain xuống.
def pathSum(root, target):
res = []
def dfs(node, remain, path):
if not node: return
path.append(node.val)
if not node.left and not node.right and remain == node.val:
res.append(path[:]) # COPY path!
dfs(node.left, remain - node.val, path)
dfs(node.right, remain - node.val, path)
path.pop() # backtrack
dfs(root, target, [])
return res
Time O(n²) worst (mỗi đường có thể dài O(n), copy O(n)). Bẫy: quên path.pop()
hoặc quên path[:] để copy. Sẽ học sâu ở Tuần 11.
Đề (LeetCode #236): Cho cây và hai node p, q. Tìm tổ tiên chung thấp nhất.
def lowestCommonAncestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowestCommonAncestor(root.left, p, q)
right = lowestCommonAncestor(root.right, p, q)
if left and right: return root # p và q ở hai phía
return left if left else right # cả hai ở một phía
Time O(n), Space O(h).
left ≠ None, right = None → trả left.right.left và right ≠ None → root chính là LCA.root == p or root == q đủ?Khi gặp p (hoặc q), trả về luôn — không cần tiếp tục đi sâu. Vì:
Đề bảo đảm p và q tồn tại — đó là điều kiện then chốt cho trick này hoạt động.
def lowestCommonAncestorBST(root, p, q):
while root:
if p.val < root.val and q.val < root.val:
root = root.left
elif p.val > root.val and q.val > root.val:
root = root.right
else:
return root # phân kỳ → root là LCA
Tận dụng tính chất BST: O(h) thay vì O(n). h = log n nếu cây balanced.
Đề (LeetCode #98): Cho root. Trả True nếu cây là BST hợp lệ.
# SAI — chỉ check con trực tiếp, không check toàn cây con
def isValidBST_WRONG(root):
if not root: return True
if root.left and root.left.val >= root.val: return False
if root.right and root.right.val <= root.val: return False
return isValidBST_WRONG(root.left) and isValidBST_WRONG(root.right)
# Sai trên cây [5, 1, 4, null, null, 3, 6] — node 3 nhỏ hơn 5 nhưng nằm bên phải
def isValidBST(root):
def valid(node, low, high):
if not node: return True
if node.val <= low or node.val >= high:
return False
return (valid(node.left, low, node.val)
and valid(node.right, node.val, high))
return valid(root, float('-inf'), float('inf'))
Top-down: truyền biên xuống. Mỗi khi đi trái, biên trên = root.val; đi phải, biên dưới = root.val.
Inorder của BST sort tăng → kiểm tra "phần tử trước < phần tử hiện tại" qua từng bước inorder.
depth trả về chiều sâu, nhưng đáp án là diameter?left + right tại mỗi node — không thể "ghép" được nên cập nhật như side effect.res.append(path[:]) có vai trò gì?path[:] tạo snapshot tại thời điểm hiện tại; append path lưu reference, sau cùng mọi entry đều trỏ tới list cuối (thường rỗng).Với T(n) = a·T(n/b) + O(n^c):
c > log_b(a): T(n) = O(n^c).c = log_b(a): T(n) = O(n^c · log n).c < log_b(a): T(n) = O(n^(log_b a)).Ví dụ Merge Sort: T(n) = 2·T(n/2) + O(n) → a=2, b=2, c=1, log₂(2) = 1 → case giữa → O(n log n).
Sinh viên không cần học thuộc Master Theorem — chỉ cần hiểu để giải thích tại sao Merge Sort là O(n log n).
def mergeSort(arr):
if len(arr) <= 1: return arr # base
mid = len(arr) // 2
left = mergeSort(arr[:mid]) # divide + conquer
right = mergeSort(arr[mid:])
return merge(left, right) # combine
def merge(a, b):
res = []
i = j = 0
while i < len(a) and j < len(b):
if a[i] <= b[j]:
res.append(a[i]); i += 1
else:
res.append(b[j]); j += 1
res.extend(a[i:]); res.extend(b[j:])
return res
Time O(n log n), Space O(n).
Đây là thuật toán sorted trong Python (Timsort = Merge Sort cải tiến).
def quickSort(arr, lo=0, hi=None):
if hi is None: hi = len(arr) - 1
if lo < hi:
p = partition(arr, lo, hi)
quickSort(arr, lo, p - 1)
quickSort(arr, p + 1, hi)
def partition(arr, lo, hi):
pivot = arr[hi]
i = lo - 1
for j in range(lo, hi):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i+1], arr[hi] = arr[hi], arr[i+1]
return i + 1
Áp dụng partition + đệ quy 1 phía. Là pattern quan trọng cho Top K Frequent (Tuần 9 — Heap có cách khác).
| Tín hiệu trong đề | Pattern |
|---|---|
| "Chiều sâu", "depth" | Bottom-up DFS (Maximum Depth) |
| "So sánh hai cây" | Top-down DFS song song (Same/Symmetric Tree) |
| "Đường dài nhất / max path sum" | Information Aggregation (Diameter) |
| "Đường từ root đến leaf" | Top-down DFS truyền tích luỹ (Path Sum) |
| "LCA / Common Ancestor" | Bottom-up DFS với 4-case logic |
| "Validate BST" | Top-down truyền biên (low, high) |
| "Theo level / right view / zigzag" | Level-order BFS với snapshot len(q) |
| "Sort O(n log n) đảm bảo" | Merge Sort |
| "Tìm phần tử thứ k trong array" | Quick Select (partition + đệ quy 1 phía) |
for _ in range(len(q)).BST & Heap — Top-K Patterns và K-way Merge
Hai cấu trúc dữ liệu nâng cao. Heap (priority queue) qua module heapq giải hiệu quả các bài Top-K, K-way Merge,
Median Stream. Đào sâu BST với operations cơ bản. Áp dụng Quick Select đã học để giải Top K Frequent với time O(n) trung bình.
Tuần 9 sẽ tổng hợp toàn bộ kỹ năng đã học để giải các bài "boss" như Find Median from Data Stream, Merge K Sorted Lists.
Nhấn T hoặc Esc để đóng