Bài 5 · Nâng cao · 24 phút
Cây nhị phân tìm kiếm
Biên soạn bởi Nguyễn Anh Tuấn
Cây nhị phân tìm kiếm (BST): lưu dữ liệu có thứ tự để tìm/chèn/xoá trung bình O(log n); vì sao cây lệch thành O(n) và ý tưởng cây cân bằng.
Bài Sắp xếp & tìm kiếm kết lại với một nỗi phiền: tìm nhị phân rất nhanh, nhưng chỉ chạy trên mảng đã sắp - mà cứ thêm hay xoá một phần tử là dãy lại lệch, phải sắp lại. Có cách nào vừa giữ thứ tự, vừa cho thêm/xoá nhanh không?
Có: cây nhị phân tìm kiếm (Binary Search Tree, BST). Hãy coi nó là “tìm nhị phân được làm thành một cấu trúc sống”: mỗi node đặt ra một câu hỏi nhỏ hơn tôi thì rẽ trái, lớn hơn thì rẽ phải, và quy tắc đó đúng ở mọi node trong cây.
- ▸BST giữ dữ liệu có thứ tự mà vẫn cho tìm, chèn, xoá nhanh.
- ▸Mỗi node là một bước rẽ trái/phải - giống mỗi bước chia đôi của tìm nhị phân.
- ▸Nó được dựng từ node + con trỏ, đúng các viên gạch ở bài Mảng & danh sách liên kết.
Mỗi node có ba phần: value (giá trị), con left và con right. Điều làm nên BST là bất biến (invariant): với mọi node, toàn bộ cây con bên TRÁI nhỏ hơn nó, toàn bộ cây con bên PHẢI lớn hơn nó. Ví dụ chèn 5, 3, 8, 1, 4, 7, 9:
- ▸node = { value, left, right } - đúng kiểu node + con trỏ đã quen.
- ▸Bất biến: trái < node < phải, đúng ở MỌI node (không chỉ con trực tiếp).
- ▸Node trên cùng là gốc (root); node không có con là lá (leaf).
Vì sao bất biến lại quý
Tìm bắt đầu từ gốc: bằng thì xong; nhỏ hơn thì sang con trái; lớn hơn sang con phải - mỗi bước bỏ đi một nửa cây. Chèn đi y như tìm, tới chỗ trống thì gắn node mới vào. Và một món quà: duyệt in-order (trái → gốc → phải) cho ra đúng dãy đã sắp.
bst.ts · chèn, duyệt in-order, và tìm có ghi đường đi
type Node = { value: number; left: Node | null; right: Node | null };
function insert(root: Node | null, value: number): Node {
if (!root) return { value, left: null, right: null };
if (value < root.value) return { ...root, left: insert(root.left, value) };
if (value > root.value) return { ...root, right: insert(root.right, value) };
return root; // trung thi bo qua
}
function inorder(root: Node | null, out: number[] = []): number[] {
if (!root) return out;
inorder(root.left, out); out.push(root.value); inorder(root.right, out);
return out;
}
function timKiem(root: Node | null, x: number) {
const path: number[] = [];
let node = root;
while (node) {
path.push(node.value);
if (x === node.value) return { found: true, path };
node = x < node.value ? node.left : node.right; // re mot nua
}
return { found: false, path };
}
let cay: Node | null = null;
for (const v of [5, 3, 8, 1, 4, 7, 9]) cay = insert(cay, v);
console.log(inorder(cay).join(' ')); // in-order -> da sap
console.log(timKiem(cay, 7)); // tim 7
console.log(timKiem(cay, 6)); // tim 6 (khong co) Kết quả khi chạy
1 3 4 5 7 8 9
{ found: true, path: [ 5, 8, 7 ] }
{ found: false, path: [ 5, 8, 7 ] }- ▸Tìm/chèn: đi từ gốc, mỗi bước so sánh rồi rẽ trái hoặc phải.
- ▸Số bước = chiều cao cây - cây càng thấp càng nhanh.
- ▸Duyệt in-order một BST luôn cho dãy đã sắp tăng dần.
Nhập một số rồi bấm Chèn để cây lớn dần; bấm Tìm để xem đường đi tô màu từ gốc xuống. Để ý ô Chiều cao so với lý tưởng:
Chèn vài số rồi bấm Tìm để xem đường đi tô màu. So chiều cao thật với lý tưởng: bấm Ví dụ lệch (chèn dãy đã sắp) để thấy cây tụt thành đường thẳng, tìm kiếm rơi về O(n).
- ▸Đường tìm luôn đi từ gốc xuống, mỗi tầng một bước.
- ▸Cây cân đối: chiều cao gần bằng “lý tưởng” log n.
- ▸Bấm “Ví dụ lệch” để thấy cây suy biến thành đường thẳng.
Mọi chi phí của BST đều bằng chiều cao cây. Cây cân đối có chiều cao quanh log n, nên tìm/ chèn/xoá đều O(log n): một triệu node chỉ cao chừng 20 tầng.
Trung thực
- ▸Chi phí tìm/chèn/xoá = chiều cao cây.
- ▸Cân đối: O(log n). Suy biến (chèn dãy đã sắp): O(n).
- ▸Cây tự cân bằng (AVL, Red-Black) giữ chiều cao ~log n sau mỗi thay đổi.
BST cho ta điều tìm nhị phân thiếu: một cấu trúc vừa có thứ tự, vừa thay đổi nhanh - miễn là giữ cây cân đối. Nó tuyệt khi bạn cần dữ liệu theo thứ tự (liệt kê tăng dần, tìm khoảng, lấy nhỏ nhất/lớn nhất).
Bước tiếp theo
Câu hỏi thường gặp
Tìm nhị phân chạy trên một MẢNG đã sắp cố định; thêm/xoá một phần tử là phải sắp lại tốn kém. BST là “tìm nhị phân động”: nó là một CẤU TRÚC luôn giữ thứ tự, nên vừa tìm O(log n) vừa chèn/xoá O(log n) mà không phải sắp lại từ đầu.
Khó hơn chèn một chút vì có ba trường hợp: node lá (xoá thẳng), node có một con (nối con lên thay), node có hai con (thay bằng phần tử nhỏ nhất của cây con phải, rồi xoá phần tử đó). Cả ba đều giữ được bất biến BST và vẫn O(log n) trên cây cân đối.
Vì mỗi số mới đều lớn hơn tất cả số trước, nên luôn rẽ phải - cây tụt thành một đường thẳng chỉ toàn con phải, y hệt một danh sách liên kết. Lúc đó tìm kiếm phải đi qua từng node, thành O(n) thay vì O(log n).
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Bất biến (invariant) của cây nhị phân tìm kiếm là gì?
- 1
Vẽ cây từ dãy
Chèn lần lượt
8, 3, 10, 1, 6, 14, 4, 7vào một BST rỗng và vẽ cây kết quả.Hoàn thành khi: Gốc là
8; bên trái có3(con1và6, dưới6là4và7); bên phải có10rồi14. Mọi node trái nhỏ hơn, phải lớn hơn. - 2
Lần theo đường tìm
Trên cây vừa vẽ, viết đường đi (dãy node ghé qua) khi tìm
7và khi tìm5.Hoàn thành khi: Tìm
7:8 → 3 → 6 → 7(thấy). Tìm5:8 → 3 → 6 → 4rồi hết đường (không có). - 3
In-order ra dãy sắp
Duyệt in-order (trái → gốc → phải) cây ở bài 1 và viết dãy thu được. Nó có tính chất gì?
Hoàn thành khi: Ra
1, 3, 4, 6, 7, 8, 10, 14- một dãy đã sắp tăng dần. Đó là bất biến của BST. - 4
Cao bao nhiêu là tốt
Một BST cân đối chứa 1 000 000 node thì chiều cao xấp xỉ bao nhiêu? Tìm một phần tử tốn tối đa bao nhiêu bước?
Hoàn thành khi: Chiều cao ~
log₂(1 000 000)≈ 20; tìm tối đa ~20 bước (mỗi bước xuống một tầng). - 5
Khi cây thành đường thẳng
Chèn
1, 2, 3, 4, 5theo thứ tự đó vào BST rỗng. Cây trông thế nào và tìm5tốn mấy bước?Hoàn thành khi: Cây tụt thành đường thẳng toàn con phải (như danh sách liên kết); tìm
5tốn 5 bước -O(n). - 6
Tự code chèn
Viết hàm
chen(root, x)bằng TypeScript chènxvào BST và trả về gốc; bỏ qua nếuxđã có.Hoàn thành khi: Nếu cây rỗng tạo node mới;
xnhỏ hơn thì chèn vào con trái, lớn hơn vào con phải, bằng thì bỏ qua; chạy đúng trên vài ca thử.