Cây tìm kiếm nhị phân
Sau bài này bạn sẽ làm được
- Phát biểu chính xác tính chất BST
- Kiểm tra một cây có phải BST hợp lệ
- Giải thích vì sao chèn dãy đã sắp làm cây suy biến
- Ước lượng chiều cao thật với dữ liệu ngẫu nhiên
Thêm một ràng buộc vào cây nhị phân thì tìm kiếm thành O(log n). Ràng buộc đó nghe đơn giản tới mức người ta hay phát biểu sai, và phát biểu sai dẫn thẳng tới một hàm kiểm tra sai.
#Tính chất BST, phát biểu cho đúng
x: mọi giá trị trong toàn bộ cây con trái của x đều nhỏ hơn x, và mọi giá trị trong toàn bộ cây con phải đều lớn hơn x.Xử lý khóa trùng
| Quy ước | Ưu điểm | Nhược điểm |
|---|---|---|
| Không cho trùng, chèn khóa đã có thì bỏ qua | Đơn giản nhất, dùng làm tập hợp | Không đếm được số lần xuất hiện |
| Cho trùng, đặt bên phải | Giữ được mọi bản sao | Xóa và tìm phức tạp hơn, cây dễ lệch |
| Thêm trường đếm vào nút | Gọn, cây không phình, đếm được | Phải sửa mọi hàm để tôn trọng trường đếm |
Sách này chọn quy ước thứ nhất: không cho khóa trùng. Nó làm mọi hàm ngắn hơn và đủ cho hầu hết ví dụ. Nhưng dù chọn cái nào, hãy ghi rõ vào tài liệu của hàm chèn, vì người gọi phải biết.
#Kiểm tra một cây có phải BST
#include <limits.h>
#include <stdio.h>
#include "tree.h"
/* Cách 1: truyền khoảng hợp lệ xuống. O(n), một lượt duyệt. */
static int trong_khoang(const TNode *r, long can_duoi, long can_tren)
{
if (r == NULL) return 1;
if (r->data <= can_duoi || r->data >= can_tren) return 0;
return trong_khoang(r->left, can_duoi, r->data)
&& trong_khoang(r->right, r->data, can_tren);
}
int la_bst(const TNode *r)
{
/* Dùng long để cận rộng hơn kiểu int, tránh vấn đề với INT_MIN và INT_MAX */
return trong_khoang(r, (long)INT_MIN - 1, (long)INT_MAX + 1);
}
/* Cách 2: duyệt trung thứ tự, kiểm dãy có tăng nghiêm ngặt không.
truoc giữ giá trị nút vừa thăm, khoi_tao cho biết đã thăm nút nào chưa. */
static int trung_tang(const TNode *r, long *truoc, int *khoi_tao)
{
if (r == NULL) return 1;
if (!trung_tang(r->left, truoc, khoi_tao)) return 0;
if (*khoi_tao && r->data <= *truoc) return 0;
*truoc = r->data;
*khoi_tao = 1;
return trung_tang(r->right, truoc, khoi_tao);
}
int la_bst_2(const TNode *r)
{
long truoc = 0;
int khoi_tao = 0;
return trung_tang(r, &truoc, &khoi_tao);
}#Cây suy biến
Toàn bộ giá trị của BST nằm ở giả thiết cây cân bằng. Chèn dữ liệu đã sắp xếp phá vỡ giả thiết đó hoàn toàn.
/* Chèn 1, 2, 3, 4, 5 vào một BST rỗng:
1 Mỗi số mới đều lớn hơn tất cả số cũ,
\ nên nó luôn đi sang phải và thành con
2 phải của nút cuối cùng.
\
3 Kết quả: cây thành danh sách liên kết,
\ chiều cao bằng n trừ 1.
4
\ Mọi thao tác trở thành O(n).
5 */| Thứ tự chèn | Chiều cao | Tìm tệ nhất | Đánh giá |
|---|---|---|---|
| Tăng dần 1..n | n - 1 | n phép so sánh | Tệ nhất có thể |
| Giảm dần n..1 | n - 1 | n phép so sánh | Tệ như trên, lệch trái |
| Ngẫu nhiên | khoảng 3 log2(n) | khoảng 3 log2(n) | Tốt, cùng bậc với tối ưu |
| Theo thứ tự cân bằng có chủ ý | log2(n) làm tròn xuống | log2(n) + 1 | Tối ưu |
chen 100000 so tang dan: chieu cao = 99999 tim 100000 lan: 21.483 s chen 100000 so ngau nhien: chieu cao = 39 tim 100000 lan: 0.019 s cham hon: 1130.7 lan
#Chiều cao với dữ liệu ngẫu nhiên
Với n khóa chèn theo thứ tự ngẫu nhiên đều, chiều cao trung bình của BST tiệm cận 4.31 ln(n). Đổi sang cơ số 2 thì đó là khoảng 2.99 log2(n), tức gấp ba lần tối ưu. Vẫn là O(log n), nên vẫn dùng được.
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "bst.h"
int main(void)
{
srand(12345);
printf("%10s %10s %10s %10s\n", "n", "cao TB", "log2(n)", "ty le");
for (long n = 1000; n <= 1000000; n *= 10) {
double tong = 0.0;
int lan = 20;
for (int k = 0; k < lan; ++k) {
TNode *goc = NULL;
for (long i = 0; i < n; ++i)
if (bst_chen(&goc, rand()) != 0) { tree_huy(goc); return 1; }
tong += tree_chieu_cao(goc);
tree_huy(goc);
}
double tb = tong / lan;
double lg = log2((double)n);
printf("%10ld %10.1f %10.1f %10.2f\n", n, tb, lg, tb / lg);
}
return 0;
} n cao TB log2(n) ty le
1000 21.4 10.0 2.15
10000 29.7 13.3 2.24
100000 38.1 16.6 2.29
1000000 46.8 19.9 2.35#Khi nào nên dùng BST
| Mảng đã sắp | Bảng băm | Cây cân bằng | |
|---|---|---|---|
| Tìm một khóa | O(log n) | O(1) trung bình | O(log n) |
| Chèn | O(n) | O(1) trung bình | O(log n) |
| Xóa | O(n) | O(1) trung bình | O(log n) |
| Duyệt theo thứ tự | O(n), rất nhanh | Không làm được | O(n) |
| Tìm theo khoảng | O(log n + k) | Không làm được | O(log n + k) |
| Tìm nhỏ nhất và lớn nhất | O(1) | O(n) | O(log n) |
| Tìm phần tử kế tiếp | O(1) | Không làm được | O(log n) |
| Trường hợp xấu nhất | Ổn định | O(n) | Ổn định |
Cây tự cân bằng trong thực tế
| Cấu trúc | Dùng ở đâu | Đặc điểm |
|---|---|---|
| Cây AVL | Cơ sở dữ liệu trong bộ nhớ, Bài 24.7 | Cân bằng chặt nhất, tìm nhanh nhất, chèn xóa tốn nhiều phép xoay hơn |
| Cây đỏ đen | std::map của C++, bộ lập lịch của nhân Linux | Cân bằng lỏng hơn, chèn xóa ít xoay hơn, cài đặt phức tạp |
| Cây B và B+ | Chỉ mục cơ sở dữ liệu, hệ thống tệp | Nhiều con mỗi nút, tối ưu cho đọc ghi theo khối trên đĩa |
| Treap | Thư viện thuật toán thi đấu | Dùng ngẫu nhiên thay vì xoay theo quy tắc, cài rất ngắn |
Tự làm thử
- Dựng bằng tay cây phản ví dụ có 60 nằm dưới 30, rồi chạy cả ba hàm kiểm và xác nhận bản sai nói nó hợp lệ.
- Cài
la_bsttheo cả hai cách, khoảng và trung thứ tự, rồi so kết quả trên một nghìn cây ngẫu nhiên. - Chèn một trăm nghìn số tăng dần vào BST và đo chiều cao. Làm lại với số ngẫu nhiên rồi so.
- Cài
tu_mang_sapdựng cây cân bằng hoàn hảo từ mảng đã sắp, rồi xác nhận chiều cao đúng bằnglog2(n)làm tròn xuống. - Đo thời gian tìm một trăm nghìn khóa trên cây suy biến và trên cây ngẫu nhiên cùng số nút.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Tính chất BST nói về toàn bộ cây con, không chỉ hai con trực tiếp. Phát biểu sai dẫn thẳng tới hàm kiểm tra sai.
- Ba cách phát biểu tương đương: theo cây con, theo khoảng hợp lệ, và theo trung thứ tự tăng nghiêm ngặt.
- Chèn dữ liệu đã sắp làm cây suy biến thành danh sách liên kết, và mọi thao tác thành O(n).
- Dữ liệu đã sắp là chuyện xảy ra hằng ngày, nên BST thường không dùng được trong mã sản xuất. Phải dùng cây tự cân bằng.
- Cây thắng bảng băm khi cần thứ tự, truy vấn theo khoảng, hoặc bảo đảm ở trường hợp xấu nhất. Ngoài ra bảng băm nhanh hơn.