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

Tìm trong BST

Sau bài này bạn sẽ làm được

  • Cài tìm bằng vòng lặp thay vì đệ quy
  • Tìm nút nhỏ nhất và lớn nhất
  • Tìm nút kế tiếp theo thứ tự trung thứ tự
  • Đếm số phép so sánh thực tế

Tìm trong BST là đi từ gốc xuống theo đúng một đường, mỗi bước loại bỏ một nửa cây còn lại. Cùng ý tưởng đó cho tìm nhỏ nhất, lớn nhất, và nút kế tiếp theo thứ tự, tất cả trong O(chiều cao).

#Tìm bằng vòng lặp

Tìm 60 trong cây mẫu: so với 50 đi phải, so với 70 đi trái, gặp 60. Đúng ba phép so sánh.
bst.c
/* Tìm nút có data bằng v. Trả về NULL nếu không có.
   Bản LẶP: O(1) bộ nhớ, không sợ tràn ngăn xếp. */
TNode *bst_tim(TNode *r, int v)
{
    while (r != NULL) {
        if      (v == r->data) return r;
        else if (v <  r->data) r = r->left;
        else                   r = r->right;
    }

    return NULL;
}

/* Bản đệ quy, để so sánh. Cùng độ phức tạp nhưng tốn ngăn xếp. */
TNode *bst_tim_de_quy(TNode *r, int v)
{
    if (r == NULL || v == r->data) return r;

    return v < r->data ? bst_tim_de_quy(r->left,  v)
                       : bst_tim_de_quy(r->right, v);
}
Tìm giá trịĐường điSố phép so sánh
50501
3050 -> 302
6050 -> 70 -> 603
2050 -> 30 -> 203
4550 -> 30 -> 40 -> NULL4, không thấy
10050 -> 70 -> 80 -> NULL4, không thấy

#Nhỏ nhất và lớn nhất

bst.c (tiếp)
/* Nút nhỏ nhất: đi hết sang trái. Trả về NULL nếu cây rỗng. */
TNode *bst_nho_nhat(TNode *r)
{
    if (r == NULL) return NULL;

    while (r->left != NULL) r = r->left;

    return r;
}

/* Nút lớn nhất: đi hết sang phải. */
TNode *bst_lon_nhat(TNode *r)
{
    if (r == NULL) return NULL;

    while (r->right != NULL) r = r->right;

    return r;
}

#Nút kế tiếp và nút liền trước

Nút kế tiếp của x là nút nhỏ nhất trong số các nút lớn hơn x, tức nút đứng ngay sau x trong duyệt trung thứ tự. Không có con trỏ cha thì phải tìm từ gốc.

bst.c (tiếp)
/* Nút kế tiếp của giá trị v theo thứ tự tăng dần.
   Không cần v phải có trong cây. Trả về NULL nếu không có nút nào lớn hơn v. */
TNode *bst_ke_tiep(TNode *r, int v)
{
    TNode *ung_vien = NULL;

    while (r != NULL) {
        if (r->data > v) {
            ung_vien = r;        /* ứng viên tốt nhất tới lúc này */
            r        = r->left;  /* thử tìm ứng viên nhỏ hơn nữa bên trái */
        } else {
            r = r->right;        /* r->data <= v, mọi thứ bên trái đều loại */
        }
    }

    return ung_vien;
}

/* Nút liền trước, đối xứng hoàn toàn. */
TNode *bst_lien_truoc(TNode *r, int v)
{
    TNode *ung_vien = NULL;

    while (r != NULL) {
        if (r->data < v) {
            ung_vien = r;
            r        = r->right;
        } else {
            r = r->left;
        }
    }

    return ung_vien;
}
terminal
./ke-tiep
cay: 20 30 40 50 60 70 80

ke tiep cua 20 -> 30
ke tiep cua 45 -> 50
ke tiep cua 50 -> 60
ke tiep cua 80 -> khong co

lien truoc cua 20 -> khong co
lien truoc cua 45 -> 40
lien truoc cua 50 -> 40
lien truoc cua 100 -> 80

Bản có con trỏ cha

Khi nút biết cha của nó
typedef struct TNodeP {
    int             data;
    struct TNodeP  *left, *right, *cha;
} TNodeP;

/* Nút kế tiếp của một nút CỤ THỂ, không phải của một giá trị. */
TNodeP *ke_tiep_cua_nut(TNodeP *x)
{
    if (x == NULL) return NULL;

    /* Trường hợp 1: có cây con phải thì kế tiếp là nút nhỏ nhất trong đó */
    if (x->right != NULL) {
        x = x->right;

        while (x->left != NULL) x = x->left;

        return x;
    }

    /* Trường hợp 2: không có cây con phải thì đi lên cho tới khi
       gặp một tổ tiên mà x nằm trong cây con TRÁI của nó */
    TNodeP *p = x->cha;

    while (p != NULL && x == p->right) {
        x = p;
        p = p->cha;
    }

    return p;
}

#Truy vấn theo khoảng

Đây là thứ bảng băm ở Chương 25 hoàn toàn không làm được, và là lý do chính để chọn cây thay vì bảng băm.

bst.c (tiếp)
/* Gọi ham cho mọi giá trị trong khoảng [thap, cao], theo thứ tự tăng dần.
   Cắt tỉa: không đi vào cây con chắc chắn nằm ngoài khoảng. */
void bst_duyet_khoang(const TNode *r, int thap, int cao,
                      void (*ham)(int, void *), void *ctx)
{
    if (r == NULL) return;

    /* Chỉ đi trái nếu còn giá trị nào trong đó có thể lớn hơn hoặc bằng thap */
    if (r->data > thap)
        bst_duyet_khoang(r->left, thap, cao, ham, ctx);

    if (r->data >= thap && r->data <= cao)
        ham(r->data, ctx);

    /* Chỉ đi phải nếu còn giá trị nào trong đó có thể nhỏ hơn hoặc bằng cao */
    if (r->data < cao)
        bst_duyet_khoang(r->right, thap, cao, ham, ctx);
}

/* Đếm số nút trong khoảng, cùng cách cắt tỉa. */
size_t bst_dem_khoang(const TNode *r, int thap, int cao)
{
    if (r == NULL) return 0;

    size_t d = 0;

    if (r->data > thap) d += bst_dem_khoang(r->left, thap, cao);
    if (r->data >= thap && r->data <= cao) ++d;
    if (r->data < cao)  d += bst_dem_khoang(r->right, thap, cao);

    return d;
}

#Đếm số phép so sánh thực tế

dem-so-sanh.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include "bst.h"

static long tong_so_sanh;

static TNode *tim_dem(TNode *r, int v)
{
    while (r != NULL) {
        ++tong_so_sanh;

        if      (v == r->data) return r;
        else if (v <  r->data) r = r->left;
        else                   r = r->right;
    }

    return NULL;
}

int main(void)
{
    srand(2024);

    printf("%10s %10s %12s %12s\n",
           "n", "chieu cao", "so sanh TB", "log2(n)");

    for (long n = 1000; n <= 1000000; n *= 10) {
        TNode *goc = NULL;
        int   *khoa = malloc((size_t)n * sizeof *khoa);

        if (khoa == NULL) return 1;

        for (long i = 0; i < n; ++i) {
            khoa[i] = rand();

            if (bst_chen2(&goc, khoa[i]) < 0) { tree_huy(goc); return 1; }
        }

        tong_so_sanh = 0;

        long lan = 100000;

        for (long i = 0; i < lan; ++i)
            tim_dem(goc, khoa[rand() % n]);

        printf("%10ld %10d %12.1f %12.1f\n",
               n, tree_chieu_cao(goc),
               (double)tong_so_sanh / lan, log2((double)n));

        free(khoa);
        tree_huy(goc);
    }

    return 0;
}
terminal
gcc -std=c17 -O2 bst.c tree.c dem-so-sanh.c -o t -lm && ./t
         n  chieu cao   so sanh TB      log2(n)
      1000         21         12.4         10.0
     10000         30         16.1         13.3
    100000         38         19.9         16.6
   1000000         47         23.8         19.9

Tự làm thử

  1. Cài bst_tim bản lặp và bản đệ quy, xác nhận chúng cho cùng kết quả trên một nghìn cây ngẫu nhiên.
  2. Cài bst_nho_nhat và bst_lon_nhat, kiểm với cây rỗng, cây một nút, và cây chỉ có nhánh phải.
  3. Cài bst_ke_tiep theo mẫu ứng viên tốt nhất, rồi dùng nó duyệt cả cây theo thứ tự tăng dần mà không đệ quy.
  4. Cài bst_duyet_khoang có cắt tỉa và bản không cắt tỉa, đo thời gian trên cây một triệu nút.
  5. Chạy dem-so-sanh.c trên máy bạn, rồi làm lại với khóa chèn theo thứ tự tăng dần và so hai bảng.

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ìm trong BST là đi một đường từ gốc xuống, mỗi bước loại bỏ một nhánh. Chi phí là O(chiều cao).
  • Hàm tìm nên viết bản lặp, vì bản đệ quy là đệ quy đuôi thuần túy và chuẩn C không hứa tối ưu nó.
  • Nhỏ nhất là đi hết sang trái, lớn nhất là đi hết sang phải. Nút nhỏ nhất không nhất thiết là lá.
  • Nút kế tiếp tìm bằng mẫu ứng viên tốt nhất, cùng mẫu với lower_bound ở Bài 26.2.
  • Truy vấn theo khoảng có cắt tỉa tốn O(chiều cao cộng số kết quả). Đây là thứ bảng băm không làm được.