Bài 24.732 phút đọc
Cây AVL
Sau bài này bạn sẽ làm được
- Tính chiều cao và hệ số cân bằng của mỗi nút
- Cài xoay trái và xoay phải
- Nhận diện bốn trường hợp LL, RR, LR, RL
- So sánh chiều cao BST và AVL khi chèn dãy đã sắp
Cây AVL là BST tự sửa lại hình dạng sau mỗi lần chèn hoặc xóa, để chiều cao không bao giờ vượt quá khoảng 1.44 log2(n). Toàn bộ cơ chế nằm ở hai phép xoay, và hai phép đó dựng nên cả bốn trường hợp.
#Chiều cao và hệ số cân bằng
Điều kiện AVL
Với mọi nút, chiều cao hai cây con lệch nhau nhiều nhất 1. Đặt tên theo Adelson-Velsky và Landis, hai người công bố năm 1962, và đây là cấu trúc cây tự cân bằng đầu tiên trong lịch sử.
avl.h
#ifndef AVL_H
#define AVL_H
#include <stddef.h>
typedef struct ANode {
int data;
int height; /* chiều cao ĐẾM NÚT: lá có height 1 */
struct ANode *left, *right;
} ANode;
ANode *avl_chen(ANode *n, int v, int *loi);
ANode *avl_xoa(ANode *n, int v);
ANode *avl_tim(ANode *n, int v);
void avl_huy(ANode *n);
int avl_chieu_cao(const ANode *n);
int avl_hop_le(const ANode *n);
#endifavl.c
#include <stdlib.h>
#include "avl.h"
/* Chiều cao ĐẾM NÚT: cây rỗng là 0, lá là 1.
Quy ước này tránh số âm nên lưu vào trường int rất gọn. */
static int h(const ANode *n) { return n ? n->height : 0; }
/* Hệ số cân bằng: dương nghĩa là lệch trái, âm nghĩa là lệch phải. */
static int bf(const ANode *n) { return n ? h(n->left) - h(n->right) : 0; }
static void cap_nhat_h(ANode *n)
{
int t = h(n->left);
int p = h(n->right);
n->height = 1 + (t > p ? t : p);
}
int avl_chieu_cao(const ANode *n) { return h(n); }| Hệ số cân bằng | Nghĩa | Cần làm gì |
|---|---|---|
| 2 hoặc lớn hơn | Lệch trái quá mức | Xoay, xem mục sau |
| 1 | Lệch trái nhẹ, vẫn hợp lệ | Không |
| 0 | Cân bằng hoàn hảo | Không |
| -1 | Lệch phải nhẹ, vẫn hợp lệ | Không |
| -2 hoặc nhỏ hơn | Lệch phải quá mức | Xoay |
| n | Tối ưu log2(n) | AVL tối đa | BST tối đa |
|---|---|---|---|
| 1 000 | 10 | 14 | 999 |
| 100 000 | 17 | 24 | 99 999 |
| 1 000 000 | 20 | 28 | 999 999 |
| 1 000 000 000 | 30 | 43 | 999 999 999 |
#Hai phép xoay
avl.c (tiếp)
/* Xoay phải quanh y. Trả về gốc mới, tức x.
y x
/ \ / \
x c ==> a y
/ \ / \
a T T c */
static ANode *xoay_phai(ANode *y)
{
ANode *x = y->left;
ANode *T = x->right; /* cây con giữa, sẽ đổi chủ */
x->right = y;
y->left = T;
cap_nhat_h(y); /* y ở DƯỚI nên cập nhật TRƯỚC */
cap_nhat_h(x);
return x;
}
/* Xoay trái quanh x. Trả về gốc mới, tức y.
x y
/ \ / \
a y ==> x c
/ \ / \
T c a T */
static ANode *xoay_trai(ANode *x)
{
ANode *y = x->right;
ANode *T = y->left;
y->left = x;
x->right = T;
cap_nhat_h(x); /* x ở DƯỚI nên cập nhật TRƯỚC */
cap_nhat_h(y);
return y;
}#Bốn trường hợp mất cân bằng
| Trường hợp | bf(n) | Nút mới nằm ở | Cách xử lý |
|---|---|---|---|
| Trái Trái (LL) | > 1 | Cây con trái của con trái | Xoay phải quanh n |
| Phải Phải (RR) | < -1 | Cây con phải của con phải | Xoay trái quanh n |
| Trái Phải (LR) | > 1 | Cây con phải của con trái | Xoay trái quanh con trái, rồi xoay phải quanh n |
| Phải Trái (RL) | < -1 | Cây con trái của con phải | Xoay phải quanh con phải, rồi xoay trái quanh n |
Bốn trường hợp bằng hình
LL: chèn 10, 20, 30 theo thứ tự giảm Sau xoay phải quanh 30
30 20
/ / \
20 10 30
/
10
RR: chèn 30, 20, 10 theo thứ tự tăng Sau xoay trái quanh 10
10 20
\ / \
20 10 30
\
30
LR: chèn 30, 10, 20 Xoay trái quanh 10, rồi phải quanh 30
30 30 20
/ / / \
10 ==> 20 ==> 10 30
\ /
20 10
RL: chèn 10, 30, 20 Xoay phải quanh 30, rồi trái quanh 10
10 10 20
\ \ / \
30 ==> 20 ==> 10 30
/ \
20 30avl.c (tiếp)
/* Khôi phục cân bằng cho nút n sau khi cây con của nó thay đổi.
Trả về gốc mới của cây con này. */
static ANode *can_bang(ANode *n)
{
cap_nhat_h(n);
int b = bf(n);
/* Lệch trái quá mức */
if (b > 1) {
if (bf(n->left) < 0) /* LR: con trái lệch phải */
n->left = xoay_trai(n->left);
return xoay_phai(n); /* LL, hoặc LR sau bước trên */
}
/* Lệch phải quá mức */
if (b < -1) {
if (bf(n->right) > 0) /* RL: con phải lệch trái */
n->right = xoay_phai(n->right);
return xoay_trai(n); /* RR, hoặc RL sau bước trên */
}
return n; /* đã cân bằng, không đổi gì */
}#Chèn có cân bằng lại
avl.c (tiếp)
static ANode *an_tao(int v)
{
ANode *n = malloc(sizeof *n);
if (n == NULL) return NULL;
n->data = v;
n->height = 1;
n->left = NULL;
n->right = NULL;
return n;
}
/* Chèn v. Trả về gốc mới của cây con.
Đặt *loi bằng -1 nếu hết bộ nhớ, 1 nếu khóa đã có, 0 nếu chèn được. */
ANode *avl_chen(ANode *n, int v, int *loi)
{
if (n == NULL) {
ANode *m = an_tao(v);
*loi = (m == NULL) ? -1 : 0;
return m; /* NULL nếu thất bại, cây con vẫn rỗng */
}
if (v < n->data) n->left = avl_chen(n->left, v, loi);
else if (v > n->data) n->right = avl_chen(n->right, v, loi);
else { *loi = 1; return n; } /* đã có, không đổi gì */
if (*loi != 0) return n; /* thất bại hoặc trùng, không cần cân bằng */
return can_bang(n); /* cập nhật chiều cao và xoay nếu cần */
}Kiểm tra cây có hợp lệ không
avl.c (tiếp)
/* Trả về chiều cao thật nếu cây hợp lệ, hoặc -1 nếu không.
Kiểm cả ba điều: chiều cao lưu đúng, hệ số cân bằng trong khoảng,
và tính chất BST. */
static int kiem(const ANode *n, long duoi, long tren)
{
if (n == NULL) return 0;
if (n->data <= duoi || n->data >= tren) return -1; /* không phải BST */
int t = kiem(n->left, duoi, n->data);
if (t < 0) return -1;
int p = kiem(n->right, n->data, tren);
if (p < 0) return -1;
int d = t - p;
if (d < -1 || d > 1) return -1; /* mất cân bằng */
int cao = 1 + (t > p ? t : p);
if (cao != n->height) return -1; /* height lưu sai */
return cao;
}
int avl_hop_le(const ANode *n)
{
return kiem(n, -2147483648L - 1, 2147483647L + 1) >= 0;
}main.c
#include <stdio.h>
#include "avl.h"
int main(void)
{
ANode *goc = NULL;
int loi = 0;
printf("chen 10, 20, 30, 40, 50, 25 theo thu tu:\n");
int nguon[] = { 10, 20, 30, 40, 50, 25 };
for (size_t i = 0; i < sizeof nguon / sizeof nguon[0]; ++i) {
goc = avl_chen(goc, nguon[i], &loi);
if (loi < 0) { avl_huy(goc); return 1; }
printf(" sau %2d: cao %d, goc %d, hop le %d\n",
nguon[i], avl_chieu_cao(goc), goc->data, avl_hop_le(goc));
}
printf("\ncay:\n");
avl_in(goc, "", 1, 0);
avl_huy(goc);
return 0;
}terminal
gcc -std=c17 -Wall -Wextra -g avl.c main.c -o t && ./t
chen 10, 20, 30, 40, 50, 25 theo thu tu:
sau 10: cao 1, goc 10, hop le 1
sau 20: cao 2, goc 10, hop le 1
sau 30: cao 2, goc 20, hop le 1
sau 40: cao 3, goc 20, hop le 1
sau 50: cao 3, goc 20, hop le 1
sau 25: cao 3, goc 20, hop le 1
cay:
,-- 50
,-- 40
| '-- 30
| '-- 25
20
'-- 10#Xóa có cân bằng lại
avl.c (tiếp)
static ANode *nho_nhat(ANode *n)
{
while (n->left != NULL) n = n->left;
return n;
}
/* Xóa v. Trả về gốc mới của cây con. Không tìm thấy thì cây không đổi. */
ANode *avl_xoa(ANode *n, int v)
{
if (n == NULL) return NULL;
if (v < n->data) n->left = avl_xoa(n->left, v);
else if (v > n->data) n->right = avl_xoa(n->right, v);
else {
/* Ba trường hợp y hệt Bài 24.6 */
if (n->left == NULL || n->right == NULL) {
ANode *con = n->left ? n->left : n->right;
free(n);
if (con == NULL) return NULL; /* không con */
return can_bang(con); /* một con, con đã là AVL hợp lệ */
}
ANode *ke = nho_nhat(n->right);
n->data = ke->data;
n->right = avl_xoa(n->right, ke->data);
}
return can_bang(n); /* cân bằng lại ở MỌI tầng trên đường đi ngược */
}terminal
./thu-xoa-avl
chen 1..15 theo thu tu tang: chieu cao = 4, hop le = 1 xoa 1, 2, 3, 4, 5, 6, 7: sau xoa 1: cao 4, hop le 1 sau xoa 2: cao 4, hop le 1 sau xoa 3: cao 4, hop le 1 sau xoa 4: cao 3, hop le 1 sau xoa 5: cao 3, hop le 1 sau xoa 6: cao 3, hop le 1 sau xoa 7: cao 3, hop le 1 con lai: 8 9 10 11 12 13 14 15
#Đo trên máy thật
do-avl.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "avl.h"
#include "bst.h"
#define N 1000000
int main(void)
{
/* Trường hợp xấu nhất của BST: dữ liệu đã sắp */
printf("=== chen %d khoa TANG DAN ===\n", N);
clock_t t0 = clock();
ANode *a = NULL;
int loi = 0;
for (int i = 0; i < N; ++i) {
a = avl_chen(a, i, &loi);
if (loi < 0) return 1;
}
double g_avl = (double)(clock() - t0) / CLOCKS_PER_SEC;
printf("AVL : %.3f s, chieu cao %d\n", g_avl, avl_chieu_cao(a));
printf("BST : bo qua, cay suy bien se tran ngan xep\n");
/* Tìm một triệu lần */
t0 = clock();
long thay = 0;
for (int i = 0; i < N; ++i)
if (avl_tim(a, rand() % N) != NULL) ++thay;
printf("tim %d lan tren AVL: %.3f s (thay %ld)\n",
N, (double)(clock() - t0) / CLOCKS_PER_SEC, thay);
avl_huy(a);
return 0;
}terminal
gcc -std=c17 -O2 avl.c bst.c tree.c do-avl.c -o t && ./t
=== chen 1000000 khoa TANG DAN === AVL : 0.284 s, chieu cao 20 BST : bo qua, cay suy bien se tran ngan xep tim 1000000 lan tren AVL: 0.412 s (thay 1000000)
# Với khóa ngẫu nhiên, so AVL với BST thường
./do-avl-ngau-nhien 1000000
chen: BST : 0.887 s, chieu cao 47 AVL : 1.204 s, chieu cao 21 AVL cham hon 1.36 lan khi chen tim 1000000 lan: BST : 0.598 s AVL : 0.412 s AVL nhanh hon 1.45 lan khi tim
| BST thường | AVL | Cây đỏ đen | |
|---|---|---|---|
| Chiều cao tối đa | n - 1 | 1.44 log2(n) | 2 log2(n) |
| Xoay khi chèn | 0 | Nhiều nhất 2 | Nhiều nhất 2 |
| Xoay khi xóa | 0 | Tới O(log n) | Nhiều nhất 3 |
| Tốc độ tìm | Kém khi lệch | Nhanh nhất | Nhanh |
| Tốc độ chèn xóa | Nhanh nhất khi cân bằng | Chậm hơn | Nhanh hơn AVL |
| Bộ nhớ mỗi nút | 24 byte | 32 byte | 32 byte |
| Dùng ở đâu | Mã dạy học | Khi đọc nhiều hơn ghi | std::map, nhân Linux |
Tự làm thử
- Cài
xoay_traivàxoay_phai, rồi kiểm bằng tay rằng trung thứ tự không đổi sau mỗi phép xoay. - Cài
can_bangvàavl_chen, chèn 1 tới 15 theo thứ tự tăng, và xác nhận chiều cao là 4 chứ không phải 14. - Cài
avl_hop_lekiểm cả ba điều kiện, gọi nó sau mỗi thao tác trên mười nghìn khóa ngẫu nhiên. - Cố tình đảo thứ tự hai dòng
cap_nhat_htrong phép xoay, rồi tìm dãy chèn ngắn nhất làmavl_hop_lebáo sai. - Đếm số phép xoay khi chèn một triệu khóa và khi xóa một triệu khóa, rồi so hai con số trung bình.
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
- Điều kiện AVL là chiều cao hai cây con lệch nhau nhiều nhất 1, và nó bảo đảm chiều cao không quá khoảng
1.44 log2(n). - Hai phép xoay dựng nên cả bốn trường hợp. Xoay chỉ đổi hình dạng chứ không đổi thứ tự trung thứ tự.
- Trong phép xoay, phải cập nhật chiều cao của nút ở dưới trước, rồi mới tới gốc mới.
- Phân biệt LL với LR bằng hệ số cân bằng của nút con, không bằng giá trị vừa chèn, để mã dùng được cho cả phép xóa.
- Chèn cần nhiều nhất hai phép xoay, còn xóa có thể cần tới O(log n) phép xoay ở mọi tầng trên đường đi ngược.