CHUYÊN ĐỀ COMPETITIVE PROGRAMMING — TUẦN 8

Recursion & Tree

Đệ 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.

10 worked examples · 3 knowledge checks · 12 bài về nhà

Phần 1. Nền tảng Recursion

Cách tư duy đúng đắn về đệ quy — không phải "tự gọi mình"
1.1

1.1 Recursion là gì?

Định nghĩa:
Hàm đệ quy giải bài toán bằng cách quy bài toán hiện tại về một hoặc nhiều bài toán nhỏ hơn của cùng cấu trúc, cho đến khi chạm một base case không cần đệ quy nữa.

Hai thành phần bắt buộc

  1. Base case — điều kiện dừng. Ví dụ: n == 0, node is None.
  2. Recursive case — quy về bài nhỏ hơn. Ví dụ: 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.

Ví dụ kinh điể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.

1.2

1.2 Cách suy nghĩ đúng — "Leap of Faith"

Đ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.

Triết lý đúng:
Tin tưởng rằng lời gọi đệ quy con sẽ trả về kết quả đúng. Việc của em chỉ là:
(1) Viết base case đúng.
(2) Giả định lời gọi con đã đúng — kết hợp kết quả của chúng để giải bài hiện tại. Nếu cả hai đúng, đệ quy tự động đúng.

Ví dụ — Tổng node trong cây

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ễ.

1.3

1.3 Recursion vs Iteration

AspectRecursionIteration
Đọc / viếtTự nhiên cho cấu trúc cây, đệ quyTự nhiên cho mảng, vòng lặp tuần tự
SpaceO(depth) — call stackO(1) thường
PerformanceCó overhead gọi hàmNhanh hơn ~2-3 lần thường
Stack overflowCó thể với đệ quy sâu (n > 10⁴)Không bao giờ
Tail call optimisationPython KHÔNG có (khác Scheme/Haskell)

Khi nào ưu tiên cái nào?

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.

1.4

1.4 Bốn bẫy đệ quy thường gặp

  1. Quên / sai base case. Chương trình chạy mãi → RecursionError. Luôn xử lý None, mảng rỗng, n = 0/1.
  2. Lời gọi không "nhỏ hơn".
    # SAI — n không nhỏ đi
    def f(n):
        if n == 0: return 0
        return f(n) + 1   # vô tận!
  3. Đệ quy ngây thơ với lời gọi lặp. Fibonacci kiểu O(2ⁿ) → cực chậm. Cần memoisation (Tuần 12).
  4. Mutate biến shared.
    # 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.

Phần 2. Nền tảng Tree

TreeNode, ba phép duyệt, level-order
2.1

2.1 TreeNode trong Python

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

Các loại tree quan trọng

Lưu ý:
BST cho phép operations O(log n) trung bình — nhưng worst case O(n) nếu cây bị skewed. Để đảm bảo O(log n) mọi trường hợp, cần balanced BST (AVL, Red-Black). LeetCode hiếm khi yêu cầu cài balanced từ đầu.
2.2

2.2 Ba phép duyệt — Trực quan

1 2 3 4 5 6
Phép duyệtThứ tựKết quả trên ví dụ
PreorderRoot → Left → Right1, 2, 4, 5, 3, 6
InorderLeft → Root → Right4, 2, 5, 1, 3, 6
PostorderLeft → Right → Root4, 5, 2, 6, 3, 1
Level-orderTheo 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.

2.3

2.3 Code ba phép duyệt

Recursive — gọn 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

Iterative Inorder — dùng stack (kinh điển)

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.

2.4

2.4 Level-order Traversal — BFS với Queue

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
Mẹo "snapshot len(q)":
Vòng 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.

Phần 3. DFS Patterns trên Tree

Top-down vs Bottom-up — hai cách tư duy đệ quy
3.1

3.1 Top-down vs Bottom-up DFS

AspectTop-downBottom-up
Hướng truyềnRoot → 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ápHàm phụ với tham số tích luỹReturn value từ đệ quy
Ví dụPath Sum, Same Tree, Validate BSTMaximum Depth, Diameter, LCA

Top-down — mẫu

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)

Bottom-up — mẫu

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)
3.2

3.2 Worked Example 1 — Maximum Depth (Bottom-up)

Đề (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

Trace với cây ở slide 2.2

Time O(n), Space O(h).

Tinh thần leap of faith:
Khi tính 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.
3.3

3.3 Worked Example 2 — Same Tree (Top-down)

Đề (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).

Đặc trưng top-down:
Hàm không trả về thông tin tổng hợptruyền cặp node song song từ trên xuống. Đây là pattern chuẩn cho bài "so sánh hai cây": Same Tree, Symmetric Tree, Subtree of Another Tree.

Cách viết khác — gộp base case

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)
3.4

3.4 Worked Example 3 — Symmetric Tree

Đề (LeetCode #101): Trả True nếu cây đối xứng qua trục giữa.

Quan sát

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).

3.5

3.5 Worked Example 4 — Invert Binary Tree

Đề (LeetCode #226): Đổi leftright 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).

Phân tích — Tinh thần "leap of faith" tột bậc

Một dòng để invert. Tại sao đúng?

Bài học:
Khi tin tưởng đệ quy, bài Invert Tree từ "khó" trở thành 1 dòng. Nếu cố theo dấu thì rất rối. Đây là minh chứng tinh thần "leap of faith" mạnh nhất.
3.Q

Knowledge Check — DFS Patterns

Q1: Pattern nào phù hợp khi cần "tổng hợp thông tin từ các cây con để tính cho node hiện tại"?
Bottom-up: gọi đệ quy con trước, lấy kết quả, kết hợp tại node hiện tại. Maximum Depth, Diameter, sum tổng đều theo pattern này. Top-down ngược lại — truyền thông tin xuống.
Q2: Tính chất nào của Inorder Traversal trên BST quan trọng?
Vì BST có "trái < root < phải", inorder (Left → Root → Right) cho dãy tăng dần. Tính chất này dùng để verify BST, tìm phần tử thứ k nhỏ nhất, hoặc convert BST sang sorted list.
Q3: Trong Level-order BFS, vì sao cần for _ in range(len(q))?
Trong khi xử lý level k, queue được thêm các node level k+1. Nếu không snapshot, vòng lặp sẽ ăn lan sang level sau → mất ranh giới level. Pattern for _ in range(len(q)) giải quyết.

Phần 4. Information Aggregation Pattern

Đệ quy trả về thông tin tổng hợp — pattern then chốt cho bài tree khó
4.1

4.1 Pattern: Đệ quy trả về thông tin tổng hợp

Mô hình tổng quát:
Khi bài cần tính một thuộc tính phức tạp tại mỗi node, đệ quy trả về nhiều giá trị: (thông tin chính cần truyền lên, kết quả đáp án phụ tới điểm này).

Khi nào áp dụng?

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.

4.2

4.2 Worked Example 5 — Diameter of Binary Tree

Đề (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.

Quan sát then chốt

Đườ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).

4.3

4.3 Diameter — Vì sao "split" tại node?

Bài này minh hoạ rõ pattern "thông tin trả lên ≠ ans":

Vì sao không thể trả "diameter" và truyền lên?

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.

Mẫu chuẩn:
  1. Đệ quy trả về thông tin cha cần để tính tiếp.
  2. Trong thân đệ quy, cập nhật ans như "ứng viên qua node hiện tại".
  3. Ans cuối = max/min qua mọi node — dùng nonlocal hoặc instance variable.
Pattern này áp dụng cho 80% bài tree Medium/Hard.
4.4

4.4 Worked Example 6 — Path Sum I & II

Đề 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.

Path Sum II (#113) — liệt kê mọi đườ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.

4.5

4.5 Worked Example 7 — Lowest Common Ancestor

Đề (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).

Phân tích 4 case

  1. Cả p và q ở cây con trái → left ≠ None, right = None → trả left.
  2. Cả p và q ở cây con phải → tương tự → trả right.
  3. Một ở trái, một ở phải → cả leftright ≠ None → root chính là LCA.
  4. Root chính là p hoặc q → base case trả ngay.
4.6

4.6 LCA — Tinh thần và bẫy

Vì sao base case 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.

Biến thể BST — đơn giản hơn

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.

4.7

4.7 Worked Example 8 — Validate Binary Search Tree

Đề (LeetCode #98): Cho root. Trả True nếu cây là BST hợp lệ.

Bẫy phổ biến

# 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

Đúng — truyền biên (min, max)

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.

Cách 2 — In-order check

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.

4.Q

Knowledge Check — Information Aggregation

Q1: Trong Diameter of Binary Tree, vì sao hàm depth trả về chiều sâu, nhưng đáp án là diameter?
Pattern "thông tin truyền lên ≠ ans". Cha cần depth để tính chuỗi sâu nhất. Đáp án = max của left + right tại mỗi node — không thể "ghép" được nên cập nhật như side effect.
Q2: Bài Validate BST cần truyền biên (low, high) thay vì chỉ check con trực tiếp vì sao?
Định nghĩa BST: TẤT CẢ giá trị bên trái < root.val. Check con trực tiếp bỏ sót cháu, chắt. Truyền biên đảm bảo mọi node trong cây con tuân thủ ràng buộc tích luỹ từ tổ tiên.
Q3: Trong Path Sum II, dòng res.append(path[:]) có vai trò gì?
path là list mutable shared qua các đệ quy. Sau khi backtrack (path.pop()), path rỗng dần. Append 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).

Phần 5. Divide & Conquer

Tinh thần đệ quy mở rộng cho mảng — Merge Sort, Quick Sort
5.1

5.1 Pattern Divide & Conquer

Ba bước:
  1. Divide: chia bài toán thành các bài con cùng cấu trúc, kích thước nhỏ hơn.
  2. Conquer: giải đệ quy các bài con (tin tưởng).
  3. Combine: ghép kết quả con lại để có đáp án cho bài lớn.

Master Theorem (sơ lược)

Với T(n) = a·T(n/b) + O(n^c):

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).

5.2

5.2 Worked Example 9 — Merge Sort

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).

Tính chất

Đây là thuật toán sorted trong Python (Timsort = Merge Sort cải tiến).

5.3

5.3 Worked Example 10 — Quick Sort

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

Tính chất

Mở rộng: Quick Select — tìm phần tử thứ k trong O(n) trung bình

Á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).

So sánh Merge vs Quick:
Merge: stable, đảm bảo O(n log n), tốn space. Quick: in-place, average tốt nhưng worst kém. Trong phỏng vấn, biết khi nào ưu tiên cái nào là dấu hiệu chiều sâu.
5.Q

Knowledge Check — D&C

Q1: Vì sao Merge Sort luôn O(n log n) bất kể input?
Cấu trúc đệ quy của Merge Sort không phụ thuộc giá trị: chia đôi → 2 đệ quy → merge O(n). Cây đệ quy có chiều cao log n, mỗi tầng O(n) công việc → O(n log n).
Q2: Quick Sort worst case O(n²) xảy ra khi nào?
Pivot kém → một bên rỗng, bên kia size n−1. Cây đệ quy biến thành "linked list" độ sâu n. Tổng công việc 1+2+...+n = O(n²). Random pivot hoặc median-of-three giúp tránh worst case.
6.0

6.0 Decision Tree — Bài Tree

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)

Quy trình code bài tree

  1. Vẽ cây mẫu trên giấy.
  2. Tự hỏi: "Đệ quy con cần trả gì để giải bài hiện tại?"
  3. Viết base case (None → ?).
  4. Viết recursive case dựa trên "leap of faith".
6.1

Tổng kết Tuần 8

Đã học

Bài về nhà (12 bài)

  1. LeetCode #104 — Maximum Depth of Binary Tree
  2. LeetCode #100 — Same Tree
  3. LeetCode #101 — Symmetric Tree
  4. LeetCode #226 — Invert Binary Tree
  5. LeetCode #543 — Diameter of Binary Tree
  6. LeetCode #112 — Path Sum & #113 Path Sum II
  7. LeetCode #236 — Lowest Common Ancestor of Binary Tree
  8. LeetCode #235 — LCA of BST (variant)
  9. LeetCode #98 — Validate Binary Search Tree
  10. LeetCode #102 — Binary Tree Level Order Traversal
  11. LeetCode #199 — Binary Tree Right Side View
  12. LeetCode #124 — Binary Tree Maximum Path Sum (Hard)

Tuần 9 sẽ học gì?

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.

Hết Tuần 8 — Cảm ơn các em.

Mục lục

Nhấn T hoặc Esc để đóng