Xóa khỏi BST
Sau bài này bạn sẽ làm được
- Phân biệt ba trường hợp xóa
- Cài xóa đúng cho cả ba trường hợp
- Giải thích vì sao dùng nút nhỏ nhất của cây con phải
- Kiểm tra lại tính chất BST sau khi xóa
Xóa là thao tác khó nhất trên BST. Nút không con và nút một con đều dễ, nhưng nút hai con thì không có chỗ nào để nối vào, nên phải mượn một nút khác tới thế chỗ.
#Ba trường hợp
| Trường hợp | Số con | Cách xử lý | Độ khó |
|---|---|---|---|
| 1 | 0 | Giải phóng nút, trả về NULL cho cha | Dễ nhất |
| 2 | 1 | Nối con duy nhất lên thế chỗ, giải phóng nút | Dễ |
| 3 | 2 | Chép giá trị của nút kế tiếp lên, rồi xóa nút kế tiếp ở cây con phải | Khó, và là chỗ mọi lỗi nằm |
#Cài đặt
#include <stdlib.h>
#include "bst.h"
/* Nút nhỏ nhất của cây con gốc r. r phải khác NULL. */
static TNode *nho_nhat(TNode *r)
{
while (r->left != NULL) r = r->left;
return r;
}
/* Xóa nút có giá trị v khỏi cây gốc r.
Trả về gốc MỚI của cây con đó, theo đúng mẫu ở Bài 24.4.
Không tìm thấy v thì cây không đổi. */
TNode *bst_xoa(TNode *r, int v)
{
if (r == NULL) return NULL;
if (v < r->data) r->left = bst_xoa(r->left, v);
else if (v > r->data) r->right = bst_xoa(r->right, v);
else {
/* Đây là nút cần xóa */
/* TH1: không con */
if (r->left == NULL && r->right == NULL) {
free(r);
return NULL;
}
/* TH2: chỉ có con phải */
if (r->left == NULL) {
TNode *con = r->right;
free(r);
return con;
}
/* TH2: chỉ có con trái */
if (r->right == NULL) {
TNode *con = r->left;
free(r);
return con;
}
/* TH3: hai con.
Lấy nút kế tiếp, tức nút nhỏ nhất của cây con PHẢI.
Chép giá trị của nó lên, rồi xóa nó khỏi cây con phải. */
TNode *ke = nho_nhat(r->right);
r->data = ke->data;
r->right = bst_xoa(r->right, ke->data);
}
return r;
}Đi xuống tìm nút cần xóa
Giống hệt hàm tìm, nhưng mỗi tầng gán kết quả trở lại
r->lefthoặcr->right. Ở hầu hết các tầng phép gán này không đổi gì.Tới nút cần xóa, phân loại theo số con
Ba trường hợp không con, một con, hai con, xử lý riêng từng cái.
Với hai con, chép giá trị nút kế tiếp lên
Nút kế tiếp là nút nhỏ nhất của cây con phải. Nó lớn hơn mọi giá trị bên trái và nhỏ hơn mọi giá trị còn lại bên phải, nên đặt nó vào chỗ này giữ nguyên tính chất BST.
Rồi xóa nút kế tiếp khỏi cây con phải
Lời gọi đệ quy này chắc chắn rơi vào trường hợp 1 hoặc 2, vì nút nhỏ nhất không bao giờ có con trái. Nên không có đệ quy vô hạn.
Chương trình thử
#include <stdio.h>
#include "bst.h"
int main(void)
{
TNode *goc = NULL;
int nguon[] = { 50, 30, 70, 20, 40, 60, 80 };
for (size_t i = 0; i < sizeof nguon / sizeof nguon[0]; ++i)
goc = bst_chen(goc, nguon[i]);
printf("ban dau : "); trung_thu_tu(goc); printf("\n");
goc = bst_xoa(goc, 20); /* TH1: la */
printf("sau xoa 20 : "); trung_thu_tu(goc); printf("\n");
printf("van la BST : %d\n", la_bst(goc));
goc = bst_xoa(goc, 30); /* TH2: mot con (40) */
printf("sau xoa 30 : "); trung_thu_tu(goc); printf("\n");
printf("van la BST : %d\n", la_bst(goc));
goc = bst_xoa(goc, 50); /* TH3: hai con */
printf("sau xoa 50 : "); trung_thu_tu(goc); printf("\n");
printf("van la BST : %d\n", la_bst(goc));
printf("goc moi : %d\n", goc->data);
goc = bst_xoa(goc, 99); /* khong co */
printf("sau xoa 99 : "); trung_thu_tu(goc); printf("\n");
printf("cay:\n");
in_khung(goc, "", 1, 0);
tree_huy(goc);
return 0;
}ban dau : 20 30 40 50 60 70 80
sau xoa 20 : 30 40 50 60 70 80
van la BST : 1
sau xoa 30 : 40 50 60 70 80
van la BST : 1
sau xoa 50 : 40 60 70 80
van la BST : 1
goc moi : 60
sau xoa 99 : 40 60 70 80
cay:
,-- 80
,-- 70
60
'-- 40All heap blocks were freed -- no leaks are possible ERROR SUMMARY: 0 errors from 0 contexts
#Chọn nút thế mạng
/* Lựa chọn A: nút kế tiếp, nhỏ nhất của cây con PHẢI */
TNode *ke = nho_nhat(r->right);
r->data = ke->data;
r->right = bst_xoa(r->right, ke->data);
/* Lựa chọn B: nút liền trước, lớn nhất của cây con TRÁI */
TNode *truoc = lon_nhat(r->left);
r->data = truoc->data;
r->left = bst_xoa(r->left, truoc->data);#Chép dữ liệu hay nối lại con trỏ
Bản ở trên chép giá trị của nút kế tiếp lên nút bị xóa. Có một cách khác: gỡ hẳn nút kế tiếp ra rồi đặt nó vào đúng vị trí của nút bị xóa, tức nối lại con trỏ.
/* Không chép data, mà thật sự thay nút. Dài hơn nhưng cần thiết
khi có con trỏ từ bên ngoài trỏ vào nút. */
TNode *bst_xoa_noi(TNode *r, int v)
{
if (r == NULL) return NULL;
if (v < r->data) { r->left = bst_xoa_noi(r->left, v); return r; }
else if (v > r->data) { r->right = bst_xoa_noi(r->right, v); return r; }
if (r->left == NULL) { TNode *c = r->right; free(r); return c; }
if (r->right == NULL) { TNode *c = r->left; free(r); return c; }
/* Hai con: gỡ nút kế tiếp ra khỏi cây con phải */
TNode *cha_ke = r;
TNode *ke = r->right;
while (ke->left != NULL) { cha_ke = ke; ke = ke->left; }
if (cha_ke != r) {
cha_ke->left = ke->right; /* ke không có con trái */
ke->right = r->right;
}
ke->left = r->left; /* ke nhận cả hai cây con của r */
free(r);
return ke; /* ke thành gốc mới của cây con này */
}| Chép dữ liệu | Nối lại con trỏ | |
|---|---|---|
| Số dòng | Ít hơn | Nhiều hơn |
| Dễ viết đúng | Rất | Dễ sai ở chỗ cha_ke != r |
| Con trỏ từ ngoài vào nút còn dùng được không | Không, nút vẫn đó nhưng giá trị đổi | Có, mỗi nút giữ nguyên giá trị suốt đời |
| Hợp khi data là struct lớn | Kém, phải chép cả struct | Tốt, chỉ đổi con trỏ |
| Hợp khi data sở hữu bộ nhớ | Phải cẩn thận, dễ rò rỉ hoặc giải phóng hai lần | Tốt, không đụng vào data |
#Xóa lặp lại làm cây lệch
#include <stdio.h>
#include <stdlib.h>
#include "bst.h"
/* Chèn n khóa ngẫu nhiên, rồi lặp lại: xóa một khóa ngẫu nhiên
và chèn một khóa ngẫu nhiên khác. Đo chiều cao theo thời gian. */
int main(void)
{
const long n = 10000;
const long lan = 200000;
srand(777);
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();
goc = bst_chen(goc, khoa[i]);
}
printf("ban dau : chieu cao %d\n", tree_chieu_cao(goc));
for (long k = 0; k < lan; ++k) {
long i = rand() % n;
goc = bst_xoa(goc, khoa[i]);
khoa[i] = rand();
goc = bst_chen(goc, khoa[i]);
if ((k + 1) % 50000 == 0)
printf("sau %6ld luot: chieu cao %d\n", k + 1, tree_chieu_cao(goc));
}
free(khoa);
tree_huy(goc);
return 0;
}ban dau : chieu cao 31 sau 50000 luot: chieu cao 42 sau 100000 luot: chieu cao 51 sau 150000 luot: chieu cao 58 sau 200000 luot: chieu cao 63
ban dau : chieu cao 31 sau 200000 luot: chieu cao 39
ban dau : chieu cao 16 sau 200000 luot: chieu cao 16
Tự làm thử
- Cài
bst_xoađầy đủ và thử đủ ba trường hợp, gọila_bstsau mỗi lần xóa. - Viết bản sai
free(ke)thay vì gọi lại hàm xóa, chạy dưới-fsanitize=addressvà đọc báo cáo. - Cài bản nối lại con trỏ, rồi kiểm rằng con trỏ tới một nút vẫn dùng được sau khi xóa một nút khác.
- Cài bản luân phiên hai lựa chọn nút thế mạng và đo chiều cao sau hai trăm nghìn lượt chèn xóa.
- Xóa hết mọi nút của một cây bằng cách lặp xóa gốc, kiểm rằng cuối cùng cây rỗng và valgrind không báo rò rỉ.
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
- Ba trường hợp xóa phân biệt theo số con: không con thì giải phóng, một con thì nối con lên, hai con thì mượn nút thế mạng.
- Nút thế mạng chỉ có thể là nút kế tiếp hoặc nút liền trước, vì chỉ hai giá trị đó giữ được dãy trung thứ tự tăng dần.
- Sau khi chép giá trị nút thế mạng lên, phải gọi lại hàm xóa trên cây con tương ứng chứ không được
freethẳng. - Chép dữ liệu ngắn hơn nhưng nguy hiểm khi nút sở hữu bộ nhớ. Nối lại con trỏ an toàn hơn và giữ được con trỏ từ bên ngoài.
- Chèn và xóa lặp lại làm BST lệch dần, chiều cao tăng theo thời gian dù dữ liệu ngẫu nhiên. Chỉ cây tự cân bằng chặn được điều đó.