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

Cây nhị phân

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

  • Dùng đúng các thuật ngữ gốc, lá, cha, con, độ sâu, chiều cao
  • Tính số nút tối đa của cây có chiều cao cho trước
  • Cài đếm nút, đếm lá và tính chiều cao
  • Phân biệt cây đầy, cây hoàn chỉnh và cây cân bằng

Cây là cấu trúc đầu tiên trong khóa học mà đệ quy không phải một lựa chọn mà là cách tự nhiên nhất. Lý do rất đơn giản: định nghĩa của cây vốn đã đệ quy, và mọi hàm trên nó chỉ là dịch định nghĩa đó sang mã.

#Thuật ngữ

Cây bảy nút với gốc 50. Đây là cây mẫu dùng lại xuyên suốt Chương 24.
Thuật ngữNghĩaTrong cây trên
GốcNút duy nhất không có cha50
LáNút không có con nào20, 40, 60, 80
Nút trongNút có ít nhất một con50, 30, 70
Cha của xNút có x làm conCha của 20 là 30
Anh emHai nút có cùng cha20 và 40 là anh em
Độ sâu của xSố cạnh từ gốc tới xĐộ sâu của 20 là 2
Chiều cao của xSố cạnh từ x xuống lá xa nhất bên dưới nóChiều cao của 30 là 1
Chiều cao của câyChiều cao của gốc2
Cây con gốc xNút x cùng toàn bộ con cháu của nóCây con gốc 30 gồm 30, 20, 40

#Khai báo và dựng cây

tree.h
#ifndef TREE_H
#define TREE_H

#include <stddef.h>

typedef struct TNode {
    int           data;
    struct TNode *left;
    struct TNode *right;
} TNode;

TNode *tn_tao(int data);
void   tree_huy(TNode *r);
size_t tree_dem_nut(const TNode *r);
size_t tree_dem_la(const TNode *r);
int    tree_chieu_cao(const TNode *r);
size_t tree_dem_nut_trong(const TNode *r);

#endif
tree.c
#include <stdlib.h>

#include "tree.h"

TNode *tn_tao(int data)
{
    TNode *n = malloc(sizeof *n);

    if (n == NULL) return NULL;

    n->data  = data;
    n->left  = NULL;
    n->right = NULL;

    return n;
}
main.c
#include <stdio.h>

#include "tree.h"

/* Dựng bằng tay cây mẫu của chương.
   Trả về NULL nếu hết bộ nhớ giữa chừng, và dọn sạch phần đã dựng. */
static TNode *dung_cay_mau(void)
{
    TNode *r = tn_tao(50);

    if (r == NULL) return NULL;

    r->left  = tn_tao(30);
    r->right = tn_tao(70);

    if (r->left == NULL || r->right == NULL) { tree_huy(r); return NULL; }

    r->left->left   = tn_tao(20);
    r->left->right  = tn_tao(40);
    r->right->left  = tn_tao(60);
    r->right->right = tn_tao(80);

    if (r->left->left  == NULL || r->left->right  == NULL ||
        r->right->left == NULL || r->right->right == NULL) {
        tree_huy(r);

        return NULL;
    }

    return r;
}

int main(void)
{
    TNode *goc = dung_cay_mau();

    if (goc == NULL) { fprintf(stderr, "Het bo nho\n"); return 1; }

    printf("so nut       : %zu\n", tree_dem_nut(goc));
    printf("so la        : %zu\n", tree_dem_la(goc));
    printf("so nut trong : %zu\n", tree_dem_nut_trong(goc));
    printf("chieu cao    : %d\n",  tree_chieu_cao(goc));

    tree_huy(goc);

    return 0;
}

#Số nút tối đa và chiều cao tối thiểu

Chiều cao hSố nút tối đaSố nút tối thiểuSố nút ở tầng h
0111
1322
2734
31548
h2^(h+1) - 1h + 12^h
do-chieu-cao.c
#include <stdio.h>
#include <math.h>

int main(void)
{
    printf("%12s %12s %12s\n", "so nut", "cao toi thieu", "cao toi da");

    for (long n = 10; n <= 10000000; n *= 10)
        printf("%12ld %12.0f %12ld\n",
               n, ceil(log2((double)n + 1.0)) - 1.0, n - 1);

    return 0;
}
terminal
gcc -std=c17 do-chieu-cao.c -o t -lm && ./t
      so nut cao toi thieu   cao toi da
          10            3            9
         100            6           99
        1000            9          999
       10000           13         9999
      100000           16        99999
     1000000           19       999999
    10000000           23      9999999

#Bốn hàm đệ quy cơ bản

Cả bốn hàm dưới đây có chung một khuôn: xử lý trường hợp cây rỗng, rồi gộp kết quả của hai cây con. Nhận ra khuôn này là nhận ra cách viết hầu hết mọi hàm trên cây.

tree.c (tiếp)
/* Đếm số nút. Cây rỗng có 0 nút. */
size_t tree_dem_nut(const TNode *r)
{
    if (r == NULL) return 0;

    return 1 + tree_dem_nut(r->left) + tree_dem_nut(r->right);
}

/* Đếm số lá. Lá là nút không có con nào. */
size_t tree_dem_la(const TNode *r)
{
    if (r == NULL) return 0;

    if (r->left == NULL && r->right == NULL) return 1;

    return tree_dem_la(r->left) + tree_dem_la(r->right);
}

/* Đếm số nút trong, tức nút có ít nhất một con. */
size_t tree_dem_nut_trong(const TNode *r)
{
    if (r == NULL) return 0;

    if (r->left == NULL && r->right == NULL) return 0;

    return 1 + tree_dem_nut_trong(r->left) + tree_dem_nut_trong(r->right);
}

/* Chiều cao tính theo cạnh. Cây rỗng có chiều cao -1. */
int tree_chieu_cao(const TNode *r)
{
    if (r == NULL) return -1;

    int t = tree_chieu_cao(r->left);
    int p = tree_chieu_cao(r->right);

    return 1 + (t > p ? t : p);
}

/* Hủy cây. Con TRƯỚC, gốc SAU. Bài 24.2 nói kỹ vì sao. */
void tree_huy(TNode *r)
{
    if (r == NULL) return;

    tree_huy(r->left);
    tree_huy(r->right);
    free(r);
}
terminal
gcc -std=c17 -Wall -Wextra -g tree.c main.c -o t && ./t
so nut       : 7
so la        : 4
so nut trong : 3
chieu cao    : 2
# Valgrind trên bản không bật sanitizer
valgrind --leak-check=full ./t
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts

Vài hàm nữa theo cùng khuôn

tree.c (tiếp)
#include <limits.h>

/* Giá trị lớn nhất trong cây. Trả về INT_MIN nếu cây rỗng. */
int tree_lon_nhat(const TNode *r)
{
    if (r == NULL) return INT_MIN;

    int t  = tree_lon_nhat(r->left);
    int p  = tree_lon_nhat(r->right);
    int kq = r->data;

    if (t > kq) kq = t;
    if (p > kq) kq = p;

    return kq;
}

/* Tổng mọi giá trị. */
long long tree_tong(const TNode *r)
{
    if (r == NULL) return 0;

    return r->data + tree_tong(r->left) + tree_tong(r->right);
}

/* Hai cây có giống hệt nhau không, cả cấu trúc lẫn giá trị. */
int tree_bang(const TNode *a, const TNode *b)
{
    if (a == NULL && b == NULL) return 1;
    if (a == NULL || b == NULL) return 0;

    return a->data == b->data
        && tree_bang(a->left,  b->left)
        && tree_bang(a->right, b->right);
}

/* Cây gương: đảo trái phải ở mọi nút. Sửa tại chỗ. */
void tree_lat(TNode *r)
{
    if (r == NULL) return;

    TNode *t = r->left;

    r->left  = r->right;
    r->right = t;

    tree_lat(r->left);
    tree_lat(r->right);
}
terminal
./them-ham
lon nhat: 80
tong    : 350
ban sao bang goc: 1
sau khi lat, con bang goc: 0

#Cây đầy, hoàn chỉnh, cân bằng

Loại câyĐịnh nghĩaDùng ở đâu
Cây đầyMọi nút có 0 hoặc 2 con, không nút nào có đúng 1 conCây Huffman ở Bài 28.2
Cây hoàn chỉnhMọi tầng đầy, trừ tầng cuối lấp từ trái sang phảiHeap ở Bài 23.4
Cây hoàn hảoMọi tầng đều đầy kín, tức đúng 2^(h+1) - 1 nútTrường hợp lý tưởng, hiếm gặp thật
Cây cân bằng chiều caoỞ mọi nút, chiều cao hai cây con lệch nhau nhiều nhất 1Cây AVL ở Bài 24.7
phan-loai.c
/* Cây đầy: mọi nút có 0 hoặc 2 con. */
int tree_day(const TNode *r)
{
    if (r == NULL) return 1;

    if ((r->left == NULL) != (r->right == NULL)) return 0;   /* đúng một con */

    return tree_day(r->left) && tree_day(r->right);
}

/* Cây cân bằng chiều cao. Bản ngây thơ tốn O(n bình phương). */
int tree_can_bang_cham(const TNode *r)
{
    if (r == NULL) return 1;

    int t = tree_chieu_cao(r->left);       /* mỗi lần lại duyệt cả cây con */
    int p = tree_chieu_cao(r->right);
    int d = t - p;

    if (d < -1 || d > 1) return 0;

    return tree_can_bang_cham(r->left) && tree_can_bang_cham(r->right);
}

/* Bản O(n): trả về chiều cao, hoặc -2 làm tín hiệu "không cân bằng". */
static int cao_hoac_bao_loi(const TNode *r)
{
    if (r == NULL) return -1;

    int t = cao_hoac_bao_loi(r->left);

    if (t == -2) return -2;

    int p = cao_hoac_bao_loi(r->right);

    if (p == -2) return -2;

    int d = t - p;

    if (d < -1 || d > 1) return -2;

    return 1 + (t > p ? t : p);
}

int tree_can_bang(const TNode *r)
{
    return cao_hoac_bao_loi(r) != -2;
}

Tự làm thử

  1. Cài TNode, hàm tạo nút, hàm hủy cây, rồi dựng cây mẫu và chạy dưới valgrind.
  2. Cài đủ bốn hàm đếm và tính chiều cao, kiểm với cây mẫu, cây rỗng, cây một nút và cây suy biến.
  3. Viết tree_dem_la bản sai coi NULL là lá, rồi chạy trên cây mẫu và giải thích con số nhận được.
  4. Cài tree_lat rồi xác nhận lật hai lần thì cây trở về như cũ, dùng tree_bang để kiểm.
  5. Cài cả hai bản kiểm cân bằng và đo thời gian trên cây suy biến hai mươi nghìn 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

  • Chiều cao đếm theo cạnh hay theo nút đều được, miễn nhất quán trong một chương trình.
  • Cây chiều cao h có nhiều nhất 2 mũ (h+1) trừ 1 nút. Đảo lại, cây n nút có chiều cao ít nhất khoảng log2(n).
  • Mọi hàm đệ quy trên cây theo cùng một khuôn: trường hợp cây rỗng, trường hợp lá nếu cần, giải hai cây con, rồi gộp.
  • Độ sâu đệ quy bằng chiều cao cây. Với cây cân bằng thì luôn an toàn, với cây suy biến thì tràn ngăn xếp.
  • Cây hoàn chỉnh là điều kiện của heap, cây cân bằng chiều cao là điều kiện của AVL, và hai khái niệm đó khác nhau.