Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 24.322 phút đọc

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

Cây tìm kiếm nhị phân
Với mọi nút 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.
Cây mẫu của chương thỏa tính chất BST. Trung thứ tự của nó là dãy tăng dần.

Xử lý khóa trùng

Quy ướcƯu điểmNhượ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ợpKhông đếm được số lần xuất hiện
Cho trùng, đặt bên phảiGiữ được mọi bản saoXóa và tìm phức tạp hơn, cây dễ lệch
Thêm trường đếm vào nútGọn, cây không phình, đếm đượcPhả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

kiem-bst.c
#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.

suy-bien.c
/* 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ènChiều caoTìm tệ nhấtĐánh giá
Tăng dần 1..nn - 1n phép so sánhTệ nhất có thể
Giảm dần n..1n - 1n phép so sánhTệ như trên, lệch trái
Ngẫu nhiênkhoả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ốnglog2(n) + 1Tối ưu
terminal
./do-suy-bien 100000
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.

do-chieu-cao-ngau-nhien.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;
}
terminal
gcc -std=c17 -O2 bst.c do-chieu-cao-ngau-nhien.c -o t -lm && ./t
         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ắpBảng bămCây cân bằng
Tìm một khóaO(log n)O(1) trung bìnhO(log n)
ChènO(n)O(1) trung bìnhO(log n)
XóaO(n)O(1) trung bìnhO(log n)
Duyệt theo thứ tựO(n), rất nhanhKhông làm đượcO(n)
Tìm theo khoảngO(log n + k)Không làm đượcO(log n + k)
Tìm nhỏ nhất và lớn nhấtO(1)O(n)O(log n)
Tìm phần tử kế tiếpO(1)Không làm đượcO(log n)
Trường hợp xấu nhấtỔn địnhO(n)Ổn định

Cây tự cân bằng trong thực tế

Cấu trúcDùng ở đâuĐặc điểm
Cây AVLCơ sở dữ liệu trong bộ nhớ, Bài 24.7Câ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 đỏ đenstd::map của C++, bộ lập lịch của nhân LinuxCâ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ệpNhiều con mỗi nút, tối ưu cho đọc ghi theo khối trên đĩa
TreapThư viện thuật toán thi đấuDùng ngẫu nhiên thay vì xoay theo quy tắc, cài rất ngắn

Tự làm thử

  1. 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ệ.
  2. Cài la_bst theo 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.
  3. 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.
  4. Cài tu_mang_sap dự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ằng log2(n) làm tròn xuống.
  5. Đ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.