Bài 24.422 phút đọc
Chèn vào BST
Sau bài này bạn sẽ làm được
- Cài chèn bằng đệ quy theo mẫu trả về gốc mới
- Cài chèn bằng vòng lặp
- Chọn cách xử lý khóa trùng và ghi rõ vào hợp đồng
- Xử lý hết bộ nhớ giữa chừng
Chèn vào BST chỉ có một chỗ đúng cho mỗi khóa, và tìm chỗ đó là đi từ gốc xuống theo đúng một đường. Cái khó không nằm ở thuật toán mà ở chữ ký hàm: làm sao vừa sửa được cây vừa báo được lỗi hết bộ nhớ.
#Bản đệ quy trả về gốc
bst.c
#include <stdlib.h>
#include "bst.h"
/* Chèn v vào cây gốc r. Trả về gốc MỚI của cây con đó.
Khóa trùng bị bỏ qua. Trả về r cũ nếu hết bộ nhớ, tức cây không đổi. */
TNode *bst_chen(TNode *r, int v)
{
if (r == NULL) return tn_tao(v); /* tìm được chỗ trống, tạo nút */
if (v < r->data) r->left = bst_chen(r->left, v);
else if (v > r->data) r->right = bst_chen(r->right, v);
/* v == r->data: bỏ qua, không cho trùng */
return r; /* gốc không đổi ở mọi tầng khác */
}
/* Dùng: */
TNode *goc = NULL;
goc = bst_chen(goc, 50);
goc = bst_chen(goc, 30);
goc = bst_chen(goc, 70);Cây rỗng thì tạo nút mới và trả về nó
Đây là trường hợp cơ sở, và cũng là chỗ duy nhất cây thật sự thay đổi.
Khóa nhỏ hơn thì chèn vào cây con trái
Gán kết quả trở lại
r->left. Ở hầu hết các tầng thì phép gán này ghi lại đúng giá trị cũ, và nó chỉ thật sự thay đổi ở tầng cuối cùng.Khóa lớn hơn thì chèn vào cây con phải
Đối xứng hoàn toàn.
Khóa bằng thì không làm gì
Theo quy ước không cho trùng ở Bài 24.3. Đổi quy ước thì sửa đúng nhánh này.
#Hai chữ ký, hai cách xử lý lỗi
Cách 1: nhận con trỏ tới gốc, trả mã lỗi
bst.c (tiếp)
/* Trả về 0 nếu chèn được
1 nếu khóa đã có, cây không đổi
-1 nếu hết bộ nhớ, cây không đổi */
int bst_chen2(TNode **pgoc, int v)
{
if (pgoc == NULL) return -1;
if (*pgoc == NULL) {
TNode *n = tn_tao(v);
if (n == NULL) return -1;
*pgoc = n;
return 0;
}
if (v < (*pgoc)->data) return bst_chen2(&(*pgoc)->left, v);
if (v > (*pgoc)->data) return bst_chen2(&(*pgoc)->right, v);
return 1; /* đã có */
}
/* Dùng: */
TNode *goc = NULL;
if (bst_chen2(&goc, 50) < 0) { /* xử lý hết bộ nhớ */ }Cách 2: gói cây vào một struct
bst.h
typedef struct {
TNode *goc;
size_t size; /* số nút, để hỏi trong O(1) */
} BST;
void bst_khoi_tao(BST *t);
void bst_huy(BST *t);
int bst_chen3(BST *t, int v); /* 0 ổn, 1 đã có, -1 hết bộ nhớ */
int bst_co(const BST *t, int v);bst.c (tiếp)
int bst_chen3(BST *t, int v)
{
int kq = bst_chen2(&t->goc, v);
if (kq == 0) ++t->size; /* chỉ tăng khi thật sự thêm nút */
return kq;
}| Trả gốc mới | Con trỏ tới gốc | Gói vào struct | |
|---|---|---|---|
| Ngắn gọn | Nhất | Vừa | Vừa |
| Báo được hết bộ nhớ | Không | Có | Có |
| Phân biệt được khóa đã có | Không | Có | Có |
| Đếm số nút trong O(1) | Không | Không | Có |
| Người gọi dễ quên gán | Rất dễ | Không thể | Không thể |
| Dùng trong | Sách giáo khoa, mã dạy học | Mã thật | Thư viện |
#Bản lặp
bst.c (tiếp)
/* Bản lặp: không đệ quy, O(1) bộ nhớ, không sợ tràn ngăn xếp. */
int bst_chen_lap(TNode **pgoc, int v)
{
if (pgoc == NULL) return -1;
TNode *cha = NULL;
TNode *cur = *pgoc;
/* Đi xuống tìm chỗ trống, nhớ nút cha */
while (cur != NULL) {
if (v < cur->data) { cha = cur; cur = cur->left; }
else if (v > cur->data) { cha = cur; cur = cur->right; }
else return 1; /* đã có */
}
TNode *n = tn_tao(v);
if (n == NULL) return -1;
if (cha == NULL) *pgoc = n; /* cây trước đó rỗng */
else if (v < cha->data) cha->left = n;
else cha->right = n;
return 0;
}| Đệ quy | Lặp | |
|---|---|---|
| Số dòng | 8 | 22 |
| Bộ nhớ ngăn xếp | O(chiều cao) | O(1) |
| Sợ tràn ngăn xếp không | Có với cây suy biến | Không |
| Tốc độ | Chậm hơn khoảng 20 phần trăm | Nhanh hơn |
| Dễ đọc | Rất | Kém hơn |
| Dùng lại được cho AVL không | Có, mẫu trả gốc mới hợp với phép xoay | Khó, phải giữ cả đường đi |
terminal
./do-chen 5000000
chen 5000000 so ngau nhien: de quy : 3.812 s lap : 3.104 s nhanh hon: 1.23 lan
# Nhưng với dữ liệu đã sắp, cây suy biến
./do-chen-sap 300000
de quy : Segmentation fault (tran ngan xep o do sau ~262000) lap : 42.187 s (cham vi cay suy bien, nhung KHONG sap)
#Bản dùng con trỏ tới con trỏ
Mẹo Node **pp ở Bài 21.4 áp dụng nguyên vẹn cho cây, và nó cho bản chèn ngắn nhất trong ba bản lặp.
bst.c (tiếp)
/* Ngắn nhất: pp luôn là địa chỉ của ô đang trỏ tới cây con hiện tại. */
int bst_chen_pp(TNode **pgoc, int v)
{
TNode **pp = pgoc;
while (*pp != NULL) {
if (v < (*pp)->data) pp = &(*pp)->left;
else if (v > (*pp)->data) pp = &(*pp)->right;
else return 1;
}
TNode *n = tn_tao(v);
if (n == NULL) return -1;
*pp = n; /* ghi thẳng vào ô trống vừa tìm được */
return 0;
}| Đệ quy | Lặp có cha | Lặp có pp | |
|---|---|---|---|
| Số dòng thân hàm | 6 | 18 | 13 |
| Nhánh xử lý cây rỗng | Gộp vào cơ sở | Riêng | Không có |
| Bộ nhớ ngăn xếp | O(h) | O(1) | O(1) |
| Cần làm quen với con trỏ hai cấp | Không | Không | Có |
Chương trình thử đầy đủ
main.c
#include <stdio.h>
#include "bst.h"
int main(void)
{
BST t;
bst_khoi_tao(&t);
int nguon[] = { 50, 30, 70, 20, 40, 60, 80, 30, 50 };
for (size_t i = 0; i < sizeof nguon / sizeof nguon[0]; ++i) {
int kq = bst_chen3(&t, nguon[i]);
printf("chen %2d -> %s\n", nguon[i],
kq == 0 ? "ok" : kq == 1 ? "da co" : "het bo nho");
if (kq < 0) { bst_huy(&t); return 1; }
}
printf("so nut : %zu\n", t.size);
printf("chieu cao : %d\n", tree_chieu_cao(t.goc));
printf("la BST : %d\n", la_bst(t.goc));
printf("trung thu tu: ");
trung_thu_tu(t.goc);
printf("\n");
printf("cay:\n");
in_khung(t.goc, "", 1, 0);
bst_huy(&t);
return 0;
}terminal
gcc -std=c17 -Wall -Wextra -g bst.c tree.c main.c -o t && ./t
chen 50 -> ok
chen 30 -> ok
chen 70 -> ok
chen 20 -> ok
chen 40 -> ok
chen 60 -> ok
chen 80 -> ok
chen 30 -> da co
chen 50 -> da co
so nut : 7
chieu cao : 2
la BST : 1
trung thu tu: 20 30 40 50 60 70 80
cay:
,-- 80
,-- 70
| '-- 60
50
| ,-- 40
'-- 30
'-- 20# 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
#Khóa trùng
Hợp đồng của hàm chèn
Phần tài liệu nói rõ hàm làm gì khi khóa đã tồn tại. Không có nó thì người gọi phải đoán, và mã của họ sẽ sai theo cách khó tìm.
Quy ước 1: bỏ qua khóa trùng
if (v == r->data) return r; /* không làm gì, giữ nút cũ */Quy ước 2: đếm số lần xuất hiện
Thêm trường đếm
typedef struct TNodeD {
int data;
size_t dem; /* số lần khóa này được chèn */
struct TNodeD *left, *right;
} TNodeD;
int bstd_chen(TNodeD **pp, int v)
{
while (*pp != NULL) {
if (v < (*pp)->data) pp = &(*pp)->left;
else if (v > (*pp)->data) pp = &(*pp)->right;
else { ++(*pp)->dem; return 0; } /* đã có, chỉ tăng đếm */
}
TNodeD *n = malloc(sizeof *n);
if (n == NULL) return -1;
n->data = v;
n->dem = 1;
n->left = n->right = NULL;
*pp = n;
return 0;
}Quy ước 3: cho trùng, đặt bên phải
Tự làm thử
- Cài
bst_chenbản trả gốc mới, rồi cố tình quên gán kết quả một lần và giải thích hiện tượng nhận được. - Cài cả ba bản chèn, kiểm rằng chúng dựng ra cây giống hệt nhau bằng
tree_bangtrên một nghìn dãy ngẫu nhiên. - Cài bản dùng
TNode **ppvà đếm số nhánhifso với bản có biến cha. - Cài quy ước đếm số lần xuất hiện, rồi dùng nó đếm tần suất từ trong một tệp văn bản.
- Chèn một trăm nghìn bản sao của cùng một khóa theo quy ước cho trùng bên phải, đo chiều cao và giải thích.
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
- Mẫu trả về gốc mới cho bản chèn ngắn nhất, và nó là mẫu dùng lại cho xóa ở Bài 24.6 và cho AVL ở Bài 24.7.
- Bản trả gốc mới không báo được hết bộ nhớ. Mã thật nên nhận
TNode **hoặc gói cây vào một struct. - Bản lặp tiết kiệm ngăn xếp và nhanh hơn khoảng hai mươi phần trăm, nhưng không cứu được vấn đề cây suy biến.
- Mẹo
TNode **ppxóa hẳn nhánh xử lý cây rỗng, đúng như nó đã làm với danh sách liên kết. - Phải chọn một quy ước cho khóa trùng và ghi rõ vào tài liệu. Thêm trường đếm thường là lựa chọn tốt nhất.