Bài 24.228 phút đọc
Bốn kiểu duyệt cây
Sau bài này bạn sẽ làm được
- Viết cả bốn kiểu duyệt và đọc được kết quả
- Chọn đúng kiểu duyệt cho từng việc
- Giải thích vì sao hủy cây bắt buộc dùng hậu thứ tự
- Cài duyệt theo tầng bằng hàng đợi
Ba kiểu duyệt theo chiều sâu khác nhau đúng một chỗ: nút hiện tại được xử lý trước, giữa, hay sau hai cây con. Một dòng đổi chỗ, và ba thuật toán hoàn toàn khác nhau cho ba mục đích khác nhau.
#Ba kiểu duyệt theo chiều sâu
duyet.c
#include <stdio.h>
#include "tree.h"
/* TIỀN thứ tự: gốc, trái, phải */
void tien_thu_tu(const TNode *r)
{
if (r == NULL) return;
printf("%d ", r->data); /* xử lý TRƯỚC hai cây con */
tien_thu_tu(r->left);
tien_thu_tu(r->right);
}
/* TRUNG thứ tự: trái, gốc, phải */
void trung_thu_tu(const TNode *r)
{
if (r == NULL) return;
trung_thu_tu(r->left);
printf("%d ", r->data); /* xử lý GIỮA hai cây con */
trung_thu_tu(r->right);
}
/* HẬU thứ tự: trái, phải, gốc */
void hau_thu_tu(const TNode *r)
{
if (r == NULL) return;
hau_thu_tu(r->left);
hau_thu_tu(r->right);
printf("%d ", r->data); /* xử lý SAU hai cây con */
}terminal
gcc -std=c17 -Wall -Wextra -g tree.c duyet.c main.c -o t && ./t
tien thu tu : 50 30 20 40 70 60 80 trung thu tu: 20 30 40 50 60 70 80 hau thu tu : 20 40 30 60 80 70 50 theo tang : 50 30 70 20 40 60 80
#Chọn kiểu nào cho việc gì
| Kiểu duyệt | Dùng khi | Ví dụ cụ thể |
|---|---|---|
| Tiền thứ tự | Cần xử lý nút trước khi biết gì về con của nó | Sao chép cây, in cấu trúc cây, tuần tự hóa ra tệp |
| Trung thứ tự | Cần thứ tự tăng dần trên cây tìm kiếm | In dãy đã sắp, tìm phần tử thứ k, kiểm tra BST hợp lệ |
| Hậu thứ tự | Cần kết quả của hai cây con trước khi xử lý nút | Hủy cây, tính chiều cao, tính kích thước cây con, tính biểu thức |
| Theo tầng | Cần xử lý theo khoảng cách từ gốc | In cây theo tầng, tìm đường ngắn nhất, tuần tự hóa gọn |
Sao chép cây cần tiền thứ tự
sao-chep.c
/* Tạo nút mới TRƯỚC, rồi gắn hai cây con vào.
Trả về NULL nếu hết bộ nhớ, và dọn sạch phần đã chép. */
TNode *tree_sao_chep(const TNode *r)
{
if (r == NULL) return NULL;
TNode *n = tn_tao(r->data); /* xử lý gốc TRƯỚC */
if (n == NULL) return NULL;
n->left = tree_sao_chep(r->left);
if (r->left != NULL && n->left == NULL) { tree_huy(n); return NULL; }
n->right = tree_sao_chep(r->right);
if (r->right != NULL && n->right == NULL) { tree_huy(n); return NULL; }
return n;
}Tính biểu thức cần hậu thứ tự
tinh-bieu-thuc.c
/* Cây biểu thức: lá là số, nút trong là toán tử.
Phải có giá trị của hai cây con TRƯỚC khi tính được nút này. */
typedef struct BTNode {
char op; /* 0 nếu là số */
double so;
struct BTNode *left, *right;
} BTNode;
typedef enum { BT_OK, BT_CHIA_KHONG, BT_LOI } MaBT;
MaBT bt_tinh(const BTNode *r, double *ra)
{
if (r == NULL) return BT_LOI;
if (r->op == 0) { *ra = r->so; return BT_OK; } /* lá */
double a, b;
MaBT m;
m = bt_tinh(r->left, &a); if (m != BT_OK) return m; /* trái trước */
m = bt_tinh(r->right, &b); if (m != BT_OK) return m; /* phải sau */
switch (r->op) { /* rồi mới gốc */
case '+': *ra = a + b; return BT_OK;
case '-': *ra = a - b; return BT_OK;
case '*': *ra = a * b; return BT_OK;
case '/':
if (b == 0.0) return BT_CHIA_KHONG;
*ra = a / b;
return BT_OK;
default: return BT_LOI;
}
}terminal
./cay-bieu-thuc '(3 + 4) * 2'
cay:
*
/ \
+ 2
/ \
3 4
hau thu tu: 3 4 + 2 *
ket qua : 14#Vì sao hủy cây bắt buộc hậu thứ tự
Tiền thứ tự
void tree_huy_sai(TNode *r)
{
if (r == NULL) return;
free(r); /* giải phóng gốc TRƯỚC */
tree_huy_sai(r->left); /* r đã treo, đọc r->left là UB */
tree_huy_sai(r->right);
}Hậu thứ tự
void tree_huy(TNode *r)
{
if (r == NULL) return;
tree_huy(r->left); /* hủy hết con TRƯỚC */
tree_huy(r->right);
free(r); /* gốc SAU CÙNG */
}Sau free(r) thì r->left là đọc vùng đã giải phóng. Đây đúng là lỗi ở Bài 14.11, chỉ đổi bối cảnh. Điều tệ nhất là bản sai thường vẫn chạy đúng với cây nhỏ, vì bộ cấp phát chưa kịp ghi đè vùng đó.
terminal
gcc -std=c17 -g huy-sai.c -o t && ./t
so nut da huy: 7 (chay binh thuong, khong bao loi gi)
# Nhưng trình dò lỗi thấy ngay
gcc -std=c17 -g -fsanitize=address huy-sai.c -o t-asan && ./t-asan
ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000018
READ of size 8 at 0x602000000018 thread T0
#0 in tree_huy_sai huy-sai.c:6
0x602000000018 is located 8 bytes inside of 24-byte region
freed by thread T0 here:
#0 in free
#1 in tree_huy_sai huy-sai.c:5# Với cây lớn hơn thì nó rò rỉ thật, vì nhánh con không bao giờ được thăm
valgrind --leak-check=full ./t-lon
==4321== 23976 bytes in 999 blocks are definitely lost in loss record 1 of 1
#Duyệt theo tầng bằng hàng đợi
theo-tang.c
#include <stdio.h>
#include "queue-ptr.h" /* hàng đợi chứa TNode *, xem Bài 23.1 */
#include "tree.h"
/* In mọi nút theo thứ tự tầng, trái sang phải. */
int theo_tang(const TNode *goc)
{
if (goc == NULL) return 0;
QueueP q;
queue_khoi_tao(&q);
if (queue_enqueue(&q, goc) != 0) return -1;
while (!queue_rong(&q)) {
const TNode *n;
queue_dequeue(&q, &n);
printf("%d ", n->data);
if (n->left != NULL && queue_enqueue(&q, n->left) != 0) goto loi;
if (n->right != NULL && queue_enqueue(&q, n->right) != 0) goto loi;
}
queue_huy(&q);
return 0;
loi:
queue_huy(&q);
return -1;
}
/* Bản có xuống dòng giữa các tầng và trả về số tầng. */
int theo_tang_co_dong(const TNode *goc, size_t *so_tang)
{
if (goc == NULL) { *so_tang = 0; return 0; }
QueueP q;
size_t tang = 0;
queue_khoi_tao(&q);
if (queue_enqueue(&q, goc) != 0) return -1;
while (!queue_rong(&q)) {
size_t n_tang = queue_so_phan_tu(&q); /* CHỐT trước vòng trong */
for (size_t i = 0; i < n_tang; ++i) {
const TNode *n;
queue_dequeue(&q, &n);
printf("%d ", n->data);
if (n->left != NULL && queue_enqueue(&q, n->left) != 0) goto loi;
if (n->right != NULL && queue_enqueue(&q, n->right) != 0) goto loi;
}
printf("\n");
++tang;
}
queue_huy(&q);
*so_tang = tang;
return 0;
loi:
queue_huy(&q);
return -1;
}terminal
./theo-tang
theo tang : 50 30 70 20 40 60 80 co xuong dong : 50 30 70 20 40 60 80 so tang = 3
#Bản không đệ quy
Bài 22.3 đã nói: đổi hàng đợi thành ngăn xếp là đổi từ duyệt theo tầng sang duyệt theo chiều sâu. Đây là mã cụ thể.
lap.c
/* Tiền thứ tự bằng ngăn xếp. Đẩy PHẢI trước để TRÁI ra trước. */
int tien_thu_tu_lap(const TNode *goc)
{
if (goc == NULL) return 0;
StackP st;
stack_khoi_tao(&st);
if (stack_push(&st, goc) != 0) return -1;
while (!stack_rong(&st)) {
const TNode *n;
stack_pop(&st, &n);
printf("%d ", n->data);
if (n->right != NULL && stack_push(&st, n->right) != 0) goto loi;
if (n->left != NULL && stack_push(&st, n->left) != 0) goto loi;
}
stack_huy(&st);
return 0;
loi:
stack_huy(&st);
return -1;
}
/* Trung thứ tự bằng ngăn xếp. Khó hơn hẳn: phải đi hết nhánh trái
rồi mới xử lý, nên cần một con trỏ chạy song song với ngăn xếp. */
int trung_thu_tu_lap(const TNode *goc)
{
StackP st;
const TNode *cur = goc;
stack_khoi_tao(&st);
while (cur != NULL || !stack_rong(&st)) {
while (cur != NULL) { /* đi hết xuống nhánh trái */
if (stack_push(&st, cur) != 0) { stack_huy(&st); return -1; }
cur = cur->left;
}
stack_pop(&st, &cur); /* lấy nút trái nhất chưa xử lý */
printf("%d ", cur->data);
cur = cur->right; /* rồi sang nhánh phải của nó */
}
stack_huy(&st);
return 0;
}| Kiểu duyệt | Đệ quy | Lặp |
|---|---|---|
| Tiền thứ tự | 3 dòng | Dễ, một ngăn xếp |
| Trung thứ tự | 3 dòng | Khó hơn, cần con trỏ chạy song song |
| Hậu thứ tự | 3 dòng | Khó nhất, hoặc dùng mẹo hai ngăn xếp |
| Theo tầng | Không tự nhiên | Dễ, một hàng đợi |
#In cây ra màn hình
Xoay cây chín mươi độ thì in được bằng một hàm sáu dòng. Đây là công cụ gỡ lỗi hữu ích nhất khi làm việc với cây.
in-cay.c
#include <stdio.h>
#include "tree.h"
/* In cây xoay 90 độ: gốc ở bên trái, cây con phải ở phía trên.
Duyệt phải, gốc, trái, tức TRUNG thứ tự đảo ngược. */
void in_cay(const TNode *r, int muc)
{
if (r == NULL) return;
in_cay(r->right, muc + 1);
for (int i = 0; i < muc; ++i) printf(" ");
printf("%d\n", r->data);
in_cay(r->left, muc + 1);
}terminal
./in-cay
80
70
60
50
40
30
20Bản có khung nối, dễ đọc hơn
in-cay-khung.c
/* tien_to là chuỗi thụt lề tích lũy.
la_goc cho biết đây có phải nút gốc không, vì gốc không có cha
nên không cần ký hiệu nối.
la_phai cho biết nút này là con phải hay con trái của cha nó. */
void in_khung(const TNode *r, const char *tien_to, int la_goc, int la_phai)
{
if (r == NULL) return;
char moi[256];
/* Cây con phải in TRƯỚC, nên nó nằm phía trên */
snprintf(moi, sizeof moi, "%s%s", tien_to,
la_goc ? "" : (la_phai ? " " : "| "));
in_khung(r->right, moi, 0, 1);
printf("%s%s%d\n", tien_to,
la_goc ? "" : (la_phai ? ",-- " : "'-- "), r->data);
snprintf(moi, sizeof moi, "%s%s", tien_to,
la_goc ? "" : (la_phai ? "| " : " "));
in_khung(r->left, moi, 0, 0);
}
/* Gọi: in_khung(goc, "", 1, 0); */terminal
./in-cay-khung
,-- 80
,-- 70
| '-- 60
50
| ,-- 40
'-- 30
'-- 20Tự làm thử
- Cài cả bốn kiểu duyệt và xác nhận kết quả khớp với bảng trong bài.
- Viết
tree_huytheo tiền thứ tự sai, chạy dưới-fsanitize=addressvà dưới valgrind, rồi so hai báo cáo. - Cài duyệt theo tầng có xuống dòng, và tìm ra dãy sai khi không chốt số phần tử trước vòng trong.
- Cài trung thứ tự bản lặp, rồi so kết quả với bản đệ quy trên một trăm cây ngẫu nhiên.
- Cài hàm in cây có khung nối, và dùng nó để kiểm tra bằng mắt các phép chèn ở Bài 24.4.
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 kiểu duyệt theo chiều sâu chỉ khác nhau ở vị trí của dòng xử lý nút so với hai lời gọi đệ quy.
- Trung thứ tự trên cây tìm kiếm cho dãy tăng dần. Hậu thứ tự của cây biểu thức cho dạng hậu tố.
- Hủy cây bắt buộc hậu thứ tự, vì sau
freethì không đọc đượcleftvàrightnữa. - Duyệt theo tầng dùng hàng đợi và tốn O(n) bộ nhớ. Duyệt theo chiều sâu dùng ngăn xếp và tốn O(chiều cao).
- Khi xử lý theo lô từng tầng, phải chốt số phần tử của hàng đợi trước vòng lặp trong.