Cây tìm kiếm nhị phân (BST)
Cập nhật lần cuối
Cây tìm kiếm nhị phân (binary search tree) giữ các giá trị theo thứ tự: với mỗi nút, mọi giá trị trong cây con trái đều nhỏ hơn và mọi giá trị trong cây con phải đều lớn hơn. Để chèn hoặc tìm một giá trị, bạn bắt đầu từ gốc rồi liên tục đi sang trái hoặc phải tùy kết quả so sánh, nên mỗi bước loại đi một nửa phạm vi tìm kiếm. Nhấn Phát ở trên để xem các giá trị được đặt vào nhờ so sánh và một lượt tìm kiếm đi dần xuống cây.
Trên cây cân bằng, các thao tác này chạy trong thời gian O(log n). Điểm yếu là: chèn dữ liệu đã sắp xếp sẽ khiến cây suy biến thành danh sách liên kết với các thao tác O(n), và đó chính là lý do các biến thể tự cân bằng như cây AVL và cây đỏ đen ra đời.
Độ phức tạp thời gian và không gian
| Thao tác | Cân bằng | Xấu nhất (cây lệch) |
|---|---|---|
| Tìm kiếm | O(log n) | O(n) |
| Chèn | O(log n) | O(n) |
| Xóa | O(log n) | O(n) |
| Không gian | O(n) | O(n) |
Từng bước thực hiện (chèn)
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Nếu cây rỗng, giá trị mới trở thành gốc. |
| 2 | Nếu không, bắt đầu từ gốc. |
| 3 | Nếu giá trị nhỏ hơn, đi sang con trái; nếu lớn hơn, đi sang phải. |
| 4 | Lặp lại cho đến khi gặp một vị trí trống. |
| 5 | Gắn giá trị mới vào đó như một nút lá. |
Ví dụ minh họa
Chèn lần lượt từng giá trị của [5, 3, 8, 1, 4] vào một cây rỗng:
| Chèn | Đường đi | Thao tác |
|---|---|---|
5 | - | Cây rỗng, nên 5 trở thành gốc. |
3 | 5 | 3 < 5, đi sang trái; vị trí còn trống, gắn 3 làm con trái của 5. |
8 | 5 | 8 > 5, đi sang phải; vị trí còn trống, gắn 8 làm con phải của 5. |
1 | 5 -> 3 | 1 < 5 đi sang trái, rồi 1 < 3 đi sang trái; gắn 1 làm con trái của 3. |
4 | 5 -> 3 | 4 < 5 đi sang trái, rồi 4 > 3 đi sang phải; gắn 4 làm con phải của 3. |
Khi nào nên dùng cây tìm kiếm nhị phân
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn cần cả thứ tự sắp xếp lẫn tra cứu nhanh, và dữ liệu được chèn theo thứ tự ngẫu nhiên. | Dữ liệu đến theo thứ tự đã sắp xếp: một BST không cân bằng suy biến thành O(n) mỗi thao tác. |
| Bạn muốn phép duyệt trung thứ tự tự động cho ra các giá trị đã sắp xếp. | Bạn chỉ cần kiểm tra phần tử có tồn tại hay không, không cần thứ tự: bảng băm cho tra cứu trung bình O(1). |
| Bạn cần truy vấn theo khoảng hoặc tìm phần tử liền sau/liền trước của một khóa. | Bạn cần bảo đảm cận O(log n): hãy dùng cây AVL hoặc cây đỏ đen tự cân bằng. |
| Tập dữ liệu thay đổi thường xuyên và việc cập nhật một mảng tĩnh đã sắp xếp sẽ tốn kém. | Tập dữ liệu cố định và chỉ đọc: mảng đã sắp xếp cùng tìm kiếm nhị phân đơn giản hơn và thân thiện với bộ nhớ đệm. |
Code Binary Search Tree
Một cách cài đặt Binary Search Tree gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Binary Search Tree bằng Python
1class Node:2 def __init__(self, key):3 self.key = key4 self.left = None5 self.right = None6
7
8def insert(node, key):9 if node is None:10 return Node(key)11 if key < node.key:12 node.left = insert(node.left, key)13 elif key > node.key:14 node.right = insert(node.right, key)15 return node # duplicates are ignored16
17
18def search(node, key):19 if node is None:20 return False21 if key == node.key:22 return True23 if key < node.key:24 return search(node.left, key)25 return search(node.right, key)26
27
28def inorder(node):29 if node is None:30 return []31 return inorder(node.left) + [node.key] + inorder(node.right)32
33
34root = None35for key in [8, 3, 10, 1, 6, 14, 4, 7]:36 root = insert(root, key)37
38print("Inorder (sorted):", inorder(root))39print("search(6): ", search(root, 6))40print("search(5): ", search(root, 5))Code Binary Search Tree bằng JavaScript
1class Node {2 constructor(value) {3 this.value = value;4 this.left = null;5 this.right = null;6 }7}8
9class BinarySearchTree {10 constructor() {11 this.root = null;12 }13
14 insert(value) {15 const attach = (node) => {16 if (!node) return new Node(value);17 if (value < node.value) node.left = attach(node.left);18 else node.right = attach(node.right);19 return node;20 };21 this.root = attach(this.root);22 }23
24 search(value) {25 let current = this.root;26 while (current) {27 if (value === current.value) return true;28 current = value < current.value ? current.left : current.right;29 }30 return false;31 }32
33 // Inorder traversal of a BST visits values in sorted order34 inorder(node = this.root, out = []) {35 if (!node) return out;36 this.inorder(node.left, out);37 out.push(node.value);38 this.inorder(node.right, out);39 return out;40 }41}42
43const bst = new BinarySearchTree();44for (const value of [8, 3, 10, 1, 6, 14, 4]) bst.insert(value);45console.log("Inorder:", bst.inorder().join(" "));46console.log("search(6):", bst.search(6));47console.log("search(7):", bst.search(7));Code Binary Search Tree bằng Java
1public class Main {2 static class Node {3 int key;4 Node left, right;5 Node(int key) { this.key = key; }6 }7
8 static Node insert(Node node, int key) {9 if (node == null) return new Node(key);10 // Smaller keys go left, larger keys go right11 if (key < node.key) node.left = insert(node.left, key);12 else if (key > node.key) node.right = insert(node.right, key);13 return node;14 }15
16 static boolean search(Node node, int key) {17 if (node == null) return false;18 if (key == node.key) return true;19 return key < node.key ? search(node.left, key) : search(node.right, key);20 }21
22 static void inorder(Node node, StringBuilder sb) {23 if (node == null) return;24 inorder(node.left, sb);25 sb.append(node.key).append(" ");26 inorder(node.right, sb);27 }28
29 public static void main(String[] args) {30 Node root = null;31 int[] keys = {8, 3, 10, 1, 6, 14, 4};32 for (int k : keys) root = insert(root, k);33
34 StringBuilder sb = new StringBuilder();35 inorder(root, sb);36 System.out.println("Inorder (sorted): " + sb.toString().trim());37 System.out.println("search 6: " + search(root, 6));38 System.out.println("search 7: " + search(root, 7));39 }40}Code Binary Search Tree bằng C++
1#include <iostream>2
3struct Node {4 int value;5 Node* left = nullptr;6 Node* right = nullptr;7 explicit Node(int v) : value(v) {}8};9
10Node* insert(Node* root, int value) {11 if (root == nullptr) return new Node(value);12 if (value < root->value) root->left = insert(root->left, value);13 else if (value > root->value) root->right = insert(root->right, value);14 return root; // duplicates are ignored15}16
17bool contains(const Node* root, int value) {18 // Walk down: smaller goes left, larger goes right19 while (root != nullptr) {20 if (value == root->value) return true;21 root = value < root->value ? root->left : root->right;22 }23 return false;24}25
26void inorder(const Node* node) {27 if (node == nullptr) return;28 inorder(node->left);29 std::cout << node->value << " ";30 inorder(node->right);31}32
33int main() {34 Node* root = nullptr;35 for (int value : {50, 30, 70, 20, 40, 60, 80}) {36 root = insert(root, value);37 }38 std::cout << "Sorted: ";39 inorder(root);40 std::cout << "\n" << std::boolalpha;41 std::cout << "contains(40): " << contains(root, 40) << "\n";42 std::cout << "contains(45): " << contains(root, 45) << "\n";43 return 0;44}Code Binary Search Tree bằng C
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4
5typedef struct Node {6 int value;7 struct Node* left;8 struct Node* right;9} Node;10
11Node* newNode(int value) {12 Node* n = malloc(sizeof(Node));13 n->value = value;14 n->left = n->right = NULL;15 return n;16}17
18Node* insert(Node* root, int value) {19 if (root == NULL) return newNode(value);20 if (value < root->value) root->left = insert(root->left, value);21 else if (value > root->value) root->right = insert(root->right, value);22 return root; // duplicates are ignored23}24
25bool contains(const Node* root, int value) {26 // Walk down: smaller goes left, larger goes right27 while (root != NULL) {28 if (value == root->value) return true;29 root = value < root->value ? root->left : root->right;30 }31 return false;32}33
34void inorder(const Node* node) {35 if (node == NULL) return;36 inorder(node->left);37 printf("%d ", node->value);38 inorder(node->right);39}40
41int main(void) {42 int values[] = {50, 30, 70, 20, 40, 60, 80};43 Node* root = NULL;44 for (int i = 0; i < 7; i++) root = insert(root, values[i]);45 printf("Sorted: ");46 inorder(root);47 printf("\n");48 printf("contains(40): %s\n", contains(root, 40) ? "true" : "false");49 printf("contains(45): %s\n", contains(root, 45) ? "true" : "false");50 return 0;51}Câu hỏi thường gặp về cây tìm kiếm nhị phân
Độ phức tạp thời gian của cây tìm kiếm nhị phân là bao nhiêu?
O(log n) trên cây cân bằng, vì mỗi lần so sánh loại bỏ một nửa số nút còn lại. Trong trường hợp xấu nhất, khi cây bị lệch thành một chuỗi do chèn dữ liệu đã sắp xếp, các thao tác này suy biến thành O(n).Cây nhị phân và cây tìm kiếm nhị phân khác nhau như thế nào?
Cây nhị phân là bất kỳ cây nào mà mỗi nút có tối đa hai con, không có quy tắc thứ tự nào. Cây tìm kiếm nhị phân thêm bất biến: giá trị ở cây con trái nhỏ hơn và giá trị ở cây con phải lớn hơn nút đó, và chính điều này giúp tìm kiếm nhanh.
Vì sao cây tìm kiếm nhị phân có thể trở nên chậm?
O(n) mỗi thao tác. Các cây tự cân bằng (AVL, đỏ đen) xoay các nút để tránh điều này.Cây tìm kiếm nhị phân và bảng băm khác nhau như thế nào?
O(1) nhưng không lưu khóa theo thứ tự nào, nên không trả lời được truy vấn theo khoảng hay tìm phần tử liền sau. Cây tìm kiếm nhị phân chậm hơn một chút với O(log n) nhưng giữ khóa theo thứ tự, cho phép duyệt theo thứ tự đã sắp xếp và tìm giá trị gần nhất. Chọn BST khi thứ tự quan trọng, chọn bảng băm khi bạn chỉ cần kiểm tra sự tồn tại.Khi nào nên dùng cây tự cân bằng thay cho BST thông thường?
O(log n). BST thông thường phù hợp để học, cho tập dữ liệu nhỏ, hoặc khi khóa đến theo thứ tự ngẫu nhiên, nhưng không có gì bảo vệ bạn khỏi trường hợp xấu nhất khi cây bị lệch.Duyệt trung thứ tự một BST có trả về các giá trị đã sắp xếp không?
Có. Duyệt cây con trái, rồi đến nút, rồi đến cây con phải sẽ cho ra các khóa theo thứ tự tăng dần, đây là hệ quả trực tiếp của bất biến BST. Một sai lầm phổ biến là mong đợi điều tương tự từ một cây nhị phân thông thường: không có quy tắc thứ tự, duyệt trung thứ tự không tạo ra dãy có ý nghĩa nào.