Mục lục bài họcĐang ở d05-b3
Recursive Algorithms on Trees and Lists
Why trees and recursion belong together. A binary tree is defined recursively: it is either empty, or a node with a left subtree and a right subtree, each of which is itself a binary tree. Any algorithm on a tree therefore has the same shape as the definition. Handle the empty case (the base case), then do something with the node and call yourself on the two subtrees. Iteration can do the same work only by managing its own stack of nodes still to visit, which is longer and more error-prone; recursion lets the call stack do that bookkeeping for free.
The template and its variations. HEIGHT(NODE): if NODE is null return 0; otherwise return 1 + the larger of HEIGHT(left) and HEIGHT(right). Almost every tree question changes only the last line. COUNT: 1 + COUNT(left) + COUNT(right). SUM: value + SUM(left) + SUM(right). LEAVES: return 0 for null, 1 when both children are null, otherwise LEAVES(left) + LEAVES(right). Searching a binary search tree is the one case that visits only one subtree: compare the target with the node's value, and recurse left if smaller, right if larger, stopping on a match or on null. The three traversals — pre-order, in-order, post-order — are the same recursion with the visit placed before, between or after the two recursive calls, and in-order on a binary search tree outputs values in sorted order.
Order of execution and the call stack. Recursive calls go down to the leaves first; values come back up afterwards, and a parent cannot return until both children have. The call stack is as deep as the tree is high: a balanced tree of 1 000 nodes is about 10 levels deep, a degenerate tree (values inserted in sorted order) is 1 000 deep, and a tree deep enough overflows the stack — which is why the same recursive sum that is elegant on a balanced tree can crash on a sorted-input tree. Recursion on a linked list has the same shape: the base case is the null pointer, and each call handles one node then recurses on the rest; but because a list of N items gives N nested calls, a loop is usually preferred for lists and recursion is reserved for trees.
Tracing recursion in the exam. Write each call on its own line with its argument, indent nested calls, and write the returned value beside the call once it is known. Never trace only the final answer: marks are for showing the base cases returning and the values combining on the way up. Two errors account for most lost marks: forgetting the base case (the function calls itself forever) and combining the results wrongly (adding heights instead of taking the maximum).
Vì sao cây và đệ quy đi cùng nhau. Cây nhị phân được định nghĩa đệ quy: hoặc rỗng, hoặc là một nút có cây con trái và cây con phải, mỗi cây con lại là một cây nhị phân. Vì vậy mọi thuật toán trên cây có cùng hình dạng với định nghĩa. Xử lý trường hợp rỗng (điều kiện dừng), rồi làm gì đó với nút và gọi chính mình trên hai cây con. Vòng lặp chỉ làm được cùng việc bằng cách tự quản lý một ngăn xếp các nút còn phải thăm, dài hơn và dễ sai hơn; đệ quy để ngăn xếp gọi làm việc sổ sách đó miễn phí.
Khung mẫu và các biến thể. HEIGHT(NODE): nếu NODE rỗng trả về 0; ngược lại trả về 1 cộng giá trị lớn hơn trong HEIGHT(trái) và HEIGHT(phải). Gần như mọi câu hỏi về cây chỉ đổi dòng cuối. COUNT: 1 + COUNT(trái) + COUNT(phải). SUM: giá trị + SUM(trái) + SUM(phải). LEAVES: trả 0 cho rỗng, 1 khi cả hai con rỗng, ngược lại LEAVES(trái) + LEAVES(phải). Tìm kiếm trong cây tìm kiếm nhị phân là trường hợp duy nhất chỉ thăm một cây con: so mục tiêu với giá trị nút, đi trái nếu nhỏ hơn, đi phải nếu lớn hơn, dừng khi khớp hoặc gặp rỗng. Ba phép duyệt — tiền thứ tự, trung thứ tự, hậu thứ tự — là cùng một đệ quy với thao tác thăm đặt trước, giữa hoặc sau hai lời gọi, và trung thứ tự trên cây tìm kiếm nhị phân cho ra giá trị theo thứ tự tăng.
Thứ tự thực thi và ngăn xếp gọi. Các lời gọi đệ quy đi xuống tận lá trước; giá trị trở về sau, và nút cha không thể trả về khi cả hai con chưa xong. Ngăn xếp gọi sâu bằng chiều cao cây: cây cân bằng 1 000 nút sâu khoảng 10 tầng, cây suy biến (giá trị chèn theo thứ tự đã sắp) sâu 1 000, và cây đủ sâu làm tràn ngăn xếp — đó là lý do cùng một hàm tính tổng đệ quy thanh lịch trên cây cân bằng có thể sập trên cây từ dữ liệu đã sắp. Đệ quy trên danh sách liên kết cùng hình dạng: điều kiện dừng là con trỏ rỗng, mỗi lời gọi xử lý một nút rồi đệ quy trên phần còn lại; nhưng vì danh sách N phần tử cho N lời gọi lồng nhau, vòng lặp thường được ưu tiên cho danh sách và đệ quy dành cho cây.
Lần vết đệ quy trong phòng thi. Viết mỗi lời gọi trên một dòng riêng kèm đối số, thụt lề các lời gọi lồng, và viết giá trị trả về cạnh lời gọi khi đã biết. Đừng bao giờ chỉ ghi đáp số cuối: điểm nằm ở việc cho thấy các điều kiện dừng trả về và các giá trị kết hợp trên đường lên. Hai lỗi chiếm phần lớn điểm mất: quên điều kiện dừng (hàm gọi mình mãi) và kết hợp kết quả sai (cộng hai chiều cao thay vì lấy giá trị lớn hơn).
Với cây trong hình (gốc 50; 30 và 70 là con; 20, 40 là con của 30; 80 là con phải của 70; 10 là con trái của 20), hãy viết hàm LEAVES(NODE) trả về số lá, lần vết đầy đủ, và giải thích vì sao không thể viết return LEAVES(left) + LEAVES(right) + 1.
Hàm. if NODE = null then return 0; if NODE.left = null and NODE.right = null then return 1; return LEAVES(NODE.left) + LEAVES(NODE.right). Hai điều kiện dừng: rỗng cho 0 vì không phải nút, lá cho 1 vì chính nó là thứ cần đếm; nút trong không được cộng 1 vì nó không phải lá.
Lần vết. LEAVES(50) = LEAVES(30) + LEAVES(70).
LEAVES(30) = LEAVES(20) + LEAVES(40).
LEAVES(20) = LEAVES(10) + LEAVES(null); LEAVES(10) là lá → 1; LEAVES(null) → 0; nên LEAVES(20) = 1.
LEAVES(40) là lá → 1.
nên LEAVES(30) = 1 + 1 = 2.
LEAVES(70) = LEAVES(null) + LEAVES(80) = 0 + 1 = 1.
LEAVES(50) = 2 + 1 = 3. Ba lá là 10, 40, 80 — kiểm bằng mắt khớp.
Vì sao không cộng 1 ở mọi nút. return LEAVES(left) + LEAVES(right) + 1 đếm mọi nút không rỗng, tức chính là COUNT, và cho 7 thay vì 3. Sự khác biệt giữa đếm nút và đếm lá nằm hoàn toàn ở chỗ số 1 được cộng ở đâu: ở mọi nút hay chỉ ở nút không có con. Đây là ví dụ rõ nhất cho việc chỉ dòng cuối thay đổi trong khung đệ quy trên cây, và cũng là câu hỏi giám khảo dùng để phân biệt người hiểu với người thuộc.
Hàm SUM(NODE) = if null then 0 else value + SUM(left) + SUM(right). Chạy trên hai cây tìm kiếm nhị phân cùng chứa các số 1 tới 1 000: cây A chèn theo thứ tự ngẫu nhiên, cây B chèn theo thứ tự 1, 2, 3, ... Hãy so sánh số lời gọi, độ sâu ngăn xếp lớn nhất, và rủi ro, rồi nêu cách khắc phục.
Số lời gọi — bằng nhau. Hàm thăm mọi nút đúng một lần và gọi thêm một lần cho mỗi con rỗng, nên cả hai cây đều có 1 000 lời gọi cho nút cộng 1 001 lời gọi cho con rỗng — tổng như nhau, và kết quả 500 500 như nhau. Khối lượng tính toán là O(n) trên cả hai.
Độ sâu ngăn xếp — rất khác. Cây A chèn ngẫu nhiên có chiều cao khoảng log₂ của 1 000, tức xấp xỉ 10 tới 20 tầng, nên ngăn xếp gọi không bao giờ sâu hơn 20 khung. Cây B chèn theo thứ tự tăng: mỗi giá trị mới lớn hơn mọi giá trị cũ nên luôn thành con phải, và cây là một chuỗi thẳng 1 000 nút — chiều cao 1 000. SUM(1) gọi SUM(2) gọi SUM(3) ... tới SUM(1000) trước khi bất kỳ lời gọi nào trả về, nên ngăn xếp sâu 1 000 khung cùng lúc.
Rủi ro. Với 1 000 phần tử có thể vẫn chạy được, nhưng với 100 000 phần tử chèn theo thứ tự, ngăn xếp tràn và chương trình sập trên cây B trong khi cây A vẫn chỉ sâu khoảng 20. Cùng một hàm đúng, cùng dữ liệu, kết cục khác nhau chỉ vì thứ tự chèn — và dữ liệu đã sắp là thứ rất hay gặp trong thực tế (mã tăng dần, dấu thời gian).
Khắc phục. Không sửa hàm mà sửa cây: dùng cây tự cân bằng để chiều cao luôn là O(log n) bất kể thứ tự chèn, hoặc xáo trộn dữ liệu trước khi chèn nếu biết nó đã sắp. Phương án cuối là viết lại bằng vòng lặp với ngăn xếp tự quản lý trên vùng nhớ động, nơi giới hạn lớn hơn ngăn xếp gọi nhiều. Bài đánh giá tốt phân biệt được thuật toán đúng với thuật toán an toàn trên dữ liệu thật.
Bẫy 2 — lần vết chỉ ghi đáp số. Điểm nằm ở việc cho thấy điều kiện dừng trả về và giá trị kết hợp trên đường lên.
Bẫy 3 — coi hàm đệ quy đúng là an toàn. Cây suy biến từ dữ liệu đã sắp làm ngăn xếp sâu bằng n và tràn.
Đọc xong rồi — làm thử ngay
Bài tập của chương Abstract Data Structures (HL) gồm 14 câu trắc nghiệm và 5 đề tự luận. Đáp án hiện ngay khi chọn, miễn phí.