You are on page 1of 14

AVL TREE (written by trungvokhanh)

1)Định nghĩa

Định nghĩa: Cây AVL là một cây nhị phân tìm kiếm trong đó chiều cao cây con trái và
chiều cao cây con phải của nút gốc hơn kém nhau không quá 1, và cả hai cây con trái và
phải này đều phải là cây AVL.

1 - Đối tượng cất giữ là thông tin lưu ở mỗi nút của cây AVL

2 - Các phép toán cơ bản trên cây AVL


a) Thêm một phần tử vào cây AVL
b) Huỷ một phần tử trên cây AVL
c) Cân bằng lại một cây vừa bị mất cân bằng

2) Cài đặt

Trong bài viết này chúng ta sẽ sử dụng ngôn ngữ lập trình C++ để mô tả các cấu trúc
dữ liệu và thực hiện các phép toán.

2.1) Cấu trúc dữ liệu cho cây AVL:

Chỉ số cân bằng của một nút: Chỉ số cân bằng của một nút là hiệu của chiều cao cây con phải
và cây con trái của nó. Đối với một cây cân bằng, chỉ số cân bằng (CSCB) của mỗi nút chỉ có
thể nhận một trong ba giá trị sau đây:

CSCB(p) = 0 <=> Độ cao cây trái (p) = Độ cao cây phải (p)
CSCB(p) = 1 <=> Độ cao cây trái (p) > Độ cao cây phải (p)
CSCB(p) =-1 <=> Độ cao cây trái (p) < Độ cao cây phải (p)

Xét nút P, ta dùng các ký hiệu sau:

Chỉ số cân bằng của P: balance = hleft - hright

Độ cao cây trái P ký hiệu là hleft


Độ cao cây phải P ký hiệu là hright
Để khảo sát cây cân bằng, ta cần lưu thêm thông tin về chỉ số cân bằng tại mỗi nút. Lúc đó, cây
cân bằng có thể được khai báo như sau:
#include <iostream.h>
template <class Data> // Mẫu dữ liệu
using namespace std;
#define LH -1 //Cây con trái cao hơn
#define EH -0 //Hai cây con bằng nhau
#define RH 1 //Cây con phải cao hơn
typedef struct AVL {
int balance; // Chỉ số cân bằng
int high; // Độ cao của nút
Data info; // Thông tin chứa trong một nút
struct AVL* parent; // Nút cha
struct AVL* Left; // Nút con trái
struct AVL* Right; // Nút con phải
} AVLNode;
typedef AVLNode *AVLTree;

AVLTree T; // khai báo T là một cây AVL

Chú ý: Khi sử dụng kiểu dữ liệu tự định nghĩa, trong quá trình thao tác trên kiểu dữ liệu
này có thể sử dụng các phép toán +, -, *, /, >, < ... do đó bạn phải tự định nghĩa các toán
tử này bằng operator với cấu trúc như sau:

type operator sign (Data A, Data B);


Ví dụ:
typedef struct {int x, y;} Vector; // Định nghĩa một vector
bool operator < (Vector A, Vector B) // Định nghĩa so sánh độ dài 2
vector
{
return A.x*A.x + A.y*A.y < B.x*B.x + B.y*B.y;
}

2.2) Các thao tác trên cây AVL


Để dễ hiểu hơn ta sẽ trình bày các thao tác theo thứ tự sau:
i) Cân bằng lại một cây vừa bị mất cân bằng.
ii) Thêm một nút vào cây AVL
iii) Xoá một nút khỏi cây AVL

i) Cân bằng lại một cây vừa bị mất cân bằng:


Sau khi thêm hoặc xoá một nút trên cây cân bằng cây có thể bị mất cân bằng do đó ta
phải thao tác để cây trở lại cân bằng.
Trước hết xin được trình bày sơ lược về phép toán quan trọng là:
1) Tính chiều cao và hệ số cân bằng tại một nút u.
2) Phép quay trên cây nhị phân.

i.1) Tính chiều cao và hệ số cân bằng tại một nút u.


SOURCE CODE
int max(int i, int j) // Hàm trả về số lớn nhất trong 2 số nguyên i, j
{
return i>j ? i:j;
}

void cal_balance(AVLTree& U)
{
if (U->Left == NULL && U->Right == NULL) // Nút U là lá
{ U->high = 1;
U->balance = 0;
} else
if (U->Left == NULL && U->Right != NULL) // Nút U chỉ có cây
con phải
{U->high = (U->Right)->high + 1;
U->balance = -(U->Right)->high;
} else
if (U->Right == NULL && U->Left == NULL) // Nút U chỉ có cây
con trái
{U->high = (U->Left)->high + 1;
U->balance = (U->Left)->high;
} else // Nút U có cả cây con trái lẫn cây con phải
{U->high = max((U->Left)->high , (U->Right)->high) + 1;
U->balance = (U->Left)->high - (U->Right)->high;
}
}

i.2) Phép quay trên cây nhị phân

Định nghĩa: Phép quay trên các cây nhị phân là một phép biến đổi làm thay đổi vai trò
cha con giữa 2 nút trên cây. Có hai phép quay là quay phải hoặc quay trái:
- Phép quay phải chuyển một nút cha thành con phải của nút con bên trái.
- Phép quay trái chuyển một nút cha thành con trái của nút con phải.
Đồng thời, với sự thay đổi đó, một sự điều chỉnh cho các nút con trước đây của nút mới
chuyển thành nút cha. Phép quay bảo toàn thứ tự giữa của các nút trên cây, nghĩa là
trước và sau một hoặc nhiều phép quay, danh sách duyệt các đỉnh theo thứ tự giữa (trung
thứ tự) không thay đổi. Nhờ vậy nếu một cây nhị phân là cây tìm kiếm nhị phân của một
dãy khóa thì sau khi quay nó vẫn là cây tìm kiếm nhị phân của dãy khóa đó.

a) Phép quay phải: Chuyển một nút cha thành con phải của nút con bên trái.
Giả sử T là nút gốc của cây và có nút con trái là L. Phép quay phải chuyển L thành
gốc của cây và gốc R cũ trở thành con phải của cây L và cây con phải của L lại trở thành
cây con trái của T. Cập nhật lại quan hệ cho con giữa các nút và tính lại hệ số cân bằng,
chiều cao

SOURCE CODE
Quay phải tại đỉnh U trong cây nhị phân.
void right_rotate(AVLTree& U)
{ AVLTree L = U->Left; // L là con trái của U
if (L == NULL) return; // Không quay được
if (L->Right != NULL)
{ U->Left = L->Right;
(L->Right)->parent = U;
} else U->Left = NULL;
L->Right = U;
if (U->info < (U->parent)->info) // U là con trái của U-
>parent
{ (U->parent)->Left = L;
L->parent = U->parent;
} else //U là con phải của U->parent
{ (U->parent)->Right = L;
L->parent = U->parent;
}
U->parent = L;
// Tính lại sự cân bằng của cây
cal_balance(U);
cal_balance(L);
cal_balance(L->parent);
}

b) Phép quay trái: chuyển một nút cha thành con trái của nút con phải .
Giả sử T là nút gốc của cây và có nút con phải là R. Phép quay trái chuyển R thành
gốc của cây và gốc T cũ trở thành con trái của cây R và cây con trái của R lại trở thành
cây con phải của T. Cập nhật lại quan hệ cho con giữa các nút và tính lại hệ số cân bằng,
chiều cao.

SOURCE CODE
Quay trái tại đỉnh U trong cây nhị phân gốc T.

void left_rotate(AVLTree& U)
{ AVLTree R = U->Right;
if (R == NULL) return;
if (R->Left != NULL)
{ U->Right = R->Left;
(R->Left)->parent = U;
} else U->Right = NULL;
R->Left = U;
if (U->info < (U->parent)->info)
{ (U->parent)->Left = R;
R->parent = U->parent;
} else
{ (U->parent)->Right = R;
R->parent = U->parent;
}
U->parent = R;
cal_balance(U);
cal_balance(R);
cal_balance(R->parent);
}
Trở về với vấn đề chính là làm cách nào khôi phục sự cân bằng cho cây AVL khi nó mất
cân bằng?
Nhận xét:
Chỉ số cân bằng của cây AVL tại mỗi nút đều có giá trị tuyệt đối nhỏ hơn hoặc bằng 1. Do đó
khi chèn hay xoá 1 nút ra khỏi cây cân bằng thì hệ số này thay đổi không quá 1. Như vậy, khi
mất cân bằng, độ lệch chiều cao giữa 2 cây con sẽ là 2.

Khi tính cân bằng AVL tại u bị phá vỡ, cần một hoặc hai phép quay để tái cân bằng
AVL cây con gốc u và biến đổi cây T trở thành cân bằng AVL.

i.3) Các trường hợp mất cân bằng và cách tái cân bằng

Trường hợp 1 (LL)

Thực hiện phép quay phải tại nút D.

Trường hợp 2 (LR)

Trước hết thực hiện phép quay trái tại nút B để đưa về trường hợp 1 (LL) sau đó thực
hiện phép quay phải tại nút D.
Trường hợp 3 (RR)

Thực hiện phép quay trái tại nút màu gạch.

Trường hợp 4 (RL)

Trước hết thực hiện phép quay phải tại nút màu xanh để đưa về trường hợp 3 (RR) sau đó
thực hiện phép quay trái tại nút màu gạch..

SOURCE CODE
Tái cân bằng tại cây con gốc U

void rebalance(AVLTree U)
{
if (U->balance > 1) // Cây con U lệch trái
{
if ((U->Left)->balance > 0) right_rotate(U); // Chèn thêm làm cây
con trái U lệch trái
else // Chèn thêm làm cây con trái U lệch phải
{ left_rotate(U->Left);
right_rotate(U);
}
} else // Cây con U lệch phải
if (U->balance < -1)
{
if ((U->Right)->balance < 0) left_rotate(U); // Chèn thêm làm cây
con phải U lệch phải
else // Chèn thêm làm cây con phải U lệch trái
{ right_rotate(U->Right);
left_rotate(U);
}
}
}

ii) Thêm một nút vào cây AVL


Chúng ta giả thiết rằng thông tin của các nút đôi một khác nhau hoặc, khi một thông tin
đã có trong cây thì không cần thiết phải thêm vào nữa

Bắt đầu di chuyển tại nút gốc. Xét tại một nút, nếu nó là rỗng thì thay thế nó bằng nút
cần chèn. Ngược lại: nếu nút cần chèn có giá trị lớn hơn nút đang xét thì ta đi về phía cây
con phải của nút này. Nếu nhỏ hơn thì ta đi về phía cây con trái. Tuy nhiên, sau khi thêm
xong, nếu chiều cao của cây thay đổi, tử vị trí thêm vào, ta phải tìm ngược lên gốc để
kiểm tra các nút bị mất cân bằng không. Nếu có ta phải cân bằng lại ở nút này bằng
phương pháp đã nói ở trên.

Thuật toán: Giả sử cần thêm vào một nút mang thông tin X ( gọi tắt là nút X)
1. Tìm kiếm vị trí thích hợp để thêm nút X
2. Thêm nút X vào cây
3. Cân bằng lại cây

SOURCE CODE

int abs(int i) // Hàm trả về giá trị tuyệt đối của i


{
return i > 0 ? i : -i;
}
// Chèn nút mang thông tin X vào cây r
void inserted(Data x, AVLTree& r)
{AVLTree t;
if (r->info == x) return; // nút X đã có trong cây AVL không xét nữa
if (r->info > x) // thêm vào con trái của r
{
if (r->Left == NULL)
{ t = new AVLNode;
t->info = x;
t->high = 1;
t->parent = r;
t->Left = t->Right = NULL;
t->balance = 0;
r->Left = t;
// chỉnh lại sự cân bằng cho nút cha của t
while (t->parent != NULL) // trong khi t chưa phải là nút gốc
{
t = t->parent;
cal_balance(t);
if (abs(t->balance) > 1)
{ rebalance(t);
break;
}
}
} else inserted(x, r->Left);
} else // thêm vào con phải của r
{ if (r->Right == NULL)
{ t = new AVLNode;
t->info = x;
t->high = 1;
t->parent = r;
t->Left = t->Right = NULL;
t->balance = 0;
r->Left = t;
while (t->parent != NULL)
{ t = t->parent;
cal_balance(t);
if (abs(t->balance) > 1) { rebalance(t);
break;
}
}
} else inserted(x, r->Right);
}
}

iii) Xoá một nút khỏi cây AVL

Việc xoá một nút khỏi cây AVL cũng gồm 3 thao tác như khi thêm 1 nút vào:
Thuật toán: Xoá,một nút mang thông tin X trên cây AVL
1. Tìm vị trí nút cần xoá
2. Xoá nút đó ra khỏi cây
3. Cân bằng lại cây

Mô tả thuật toán: Bắt đầu di chuyển tại nút gốc. Nếu như khoá X nhỏ hơn khoá chứa
trong nút đang xét ta di chuyển về phía cây con trái, nếu khoá X lớn hơn khoá đang xét
thì ta di chuyển về phía phải. Kết thúc di chuyển khi khoá của nút đang xét đúng bằng X.
Có thể có các trường hợp sau:
a) Thông tin X không chứa trong cây AVL.
b) Nút chứa thông tin X là nút lá, ta chỉ cần xoá nút này đi và cân bằng lại cây.
c) Nút chứa thông tin X chỉ có cây con trái hoặc cây con phải. Ta tiến hành loại bỏ thông
tin chứa trong nút này và thay thế nút đó bằng con của nó đồng thu hồi bộ nhớ đã cấp
phát cho nó.
d) Nút chứa thông tin X có cả cây con trái lẫn cây con phải. Đây là trường hợp phức tạp
nhất cần xử lý. Ta tìm nút trái nhất của cây con phải, sau đó thay thế thông tin chứa ở nút
đang xét với nút này và loại bỏ nó ( đưa về trường hợp b)

SOURCE CODE
// Hàm trả về t là nút con trái nhất của cây con gốc r.
void L_left(AVLTree r, AVLTree& t)
{
t = r;
if (t == NULL) return;
while (t->Left != NULL) t = t->Left;
}

// Xoá nút r ra khỏi cây AVL


void del_leaf(AVLTree& r)
{AVLTree rp;
if (r->Left == NULL && r->Right == NULL) // r là nút lá
{ rp = r->parent;
if (rp->Right == r) rp->Right = NULL; // r là nút con phải
else rp->Left = NULL; // r là nút con trái
delete r;
while (rp->parent != NULL) // r không phải là nút gốc của cây AVL
{ cal_balance(rp);
if (abs(rp->balance) > 1)
{ rebalance(rp);
break;
}
rp = rp->parent;
}
return;
}
// Trường hợp r chỉ có cây con trái hoặc cây con phải
rp = r->parent;
if (r->Left != NULL) // r chỉ có cây con trái
{ if (rp->Right == r)
{ rp->Right = r->Left;
(r->Left)->parent = rp;
} else
{ rp->Left = r->Left;
(r->Left)->parent = rp;
}
} else // r chỉ có cây con phải
if (rp->Right == r)
{ rp->Right = r->Right;
(rp->Right)->parent = rp;
} else { rp->Left = r->Right;
(r->Right)->parent = rp;
}
delete r; // xoá nút r
while (rp->parent != NULL)
{ cal_balance(rp);
if (abs(rp->balance) > 1) { rebalance(rp); break; }
rp = rp->parent;
}
}

// Xoá một nút mang thông tin X ra khỏi cây AVL gốc r
void deleteNode(Data x, AVLTree& r)
{AVLTree t;
if (r == NULL) return;
if (r->info < x) { deleteNode(x, r->Right); return; }
if (r->info > x) { deleteNode(x, r->Left); return; }
if (r->Left == NULL || r->Right == NULL) del_leaf(r);
else { L_left(r->Right, t);
r->info = t->info;
del_leaf(t); // Xoá nút t
}
}

3) Ứng dụng

Bài toán:
Hãy xây dựng một tập hợp các số nguyên và các phép toán:
1. Khởi tạo một tập hợp rỗng
2. Thêm một phần tử vào tập hợp nếu phần tử này chưa có trong tập hợp
3. Xoá một phần tử nếu nó chứa trong tập hợp này
4. Tìm phần tử có giá trị lớn nhất trong tập hợp này
5. Tìm phần tử có giá trị bé nhất trong tập hợp này
6. Kiểm tra xem 1 phần tử X có thuộc tập hợp hay không (found/not found)
7. Kiểm tra xem tập hợp có rỗng hay không (no / yes)
8. Tìm phần tử bé nhất không bé hơn X hoặc thông báo là không tìm thấy (not found)
9. Tìm phần tử lớn nhất không lớn hơn X hoặc thông báo là không tìm thấy (not found)

Input
Dòng 1: Ghi số nguyên M ( là số thao tác trên tập hợp )
M dòng tiếp theo: Mỗi dòng gồm số nguyên i là mã lệnh, nếu i bằng 2, 3, 6, 8, 9 thì có
thêm một số nguyên X , i = 1, ...,9 tương ứng với các phép toán đã nói ở trên (Chỉ có duy
nhất 1 lệnh khởi tạo 1 tập hợp ngay ở dòng đầu tiên)

Output
Gồm một số dòng: Mỗi dòng chứa 1 phần tử là kết quả trả về tương ứng với yêu cầu ở
Input

Example
SET.INP SET.OUT
1 20
2 20 2
2 5 not found
2 15 6
2 9 2
2 13 false
4
2 2
2 6
2 12
2 14
5
3 20
3 12
3 15
6 22
8 5
9 5
3 14
3 9
7

Giới hạn:
1 ≤ M ≤ 10000.
các số trong tập hợp nằm trong phạm vi long

Cấu trúc dữ liệu:


Để giải quyết bài toán này ta sử dụng cấu trúc cây AVL với các thao tác chèn, tìm kiếm
đã trình bày ở trên. Tổ chức dữ liệu cho cây AVL như sau:

typedef struct AVL{int info;


int balance;
int high;
struct AVL *Left;
struct AVL *Right;
struct AVL *parent;
}AVLNode;
typedef AVLNode *AVLTree; ;

Thuật toán:
1. Khởi tạo tập hợp rỗng: gán cho nút gốc bằng NULL thao tác này chỉ mất O(1)
2. Thêm 1 phần tử vào tập hợp: sử dụng phép chèn đã trình bày ở trên
3. Xoá 1 phần tử ra khỏi tập hợp: sử dụng phép xoá đã trình bày ở trên
4. Tìm phần tử có giá trị lớn nhất: ta tìm phần tử phải nhất của cây AVL
5. Tìm phần tử có giá trị bé nhất: ta tìm phần tử trái nhất của cây AVL
6. Kiểm tra xem 1 phần tử X có thuộc tập hợp hay không: Giống như tìm kiếm trên cây
nhị phân
7. Kiểm tra xem tập hợp có rỗng hay không: kiểm tra xem cây AVL có rỗng hay không
8. Tìm phần tử bé nhất không bé hơn X: ta tìm phần từ trái nhất của cây con phải nút này
9. Tìm phần tử lớn nhất không lớn hơn X: ta tìm phần tử phải nhất của cây con phải nút
này

Đánh giá độ phức tạp của thuật toán:


Trước hết cần có một đánh giá chính xác về chiều cao của cây AVL, ta xét bài toán:
cây AVL có chiều cao h sẽ phải có tối thiểu bao nhiêu nút ?
Gọi N(h) là số nút tối thiểu của cây AVL có chiều cao h.
Ta có: N(0) = 0
N(1) = 1 và N(2) = 2.
Cây AVL có chiều cao h sẽ có 1 cây con AVL chiều cao h-1 và 1 cây con AVL chiều
cao h-2.
Do đó:
N(h) = 1 + N(h-1) + N(h-2) (1)
Ta lại có: N(h-1) > N(h-2)
Nên từ (1) suy ra:
N(h) > 2N(h-2)
N(h) > 22N(h-4)
…......................
N(h) > 2iN(h-2i)
i = h/2
N(h) > 2h/2
h < 2log2(N(h))
Như vậy, cây AVL có chiều cao O(log2(N)) = O(log N)

Ta thấy các thao tác ở trên là duyệt trên cây AVL, đi từ nút gốc đến 1 nút và mỗi nút
được đi qua không quá 1 lần, suy ra số bước di chuyển tối đa không quá chiều cao của
cây AVL. Do đó có thể nhận thấy rằng tất cả các thao tác ở trên có độ phức tạp là O(log
N). Nếu có M phép toán trên tập hợp thì độ phức tạp của thuật toán này sẽ là O(M log N)

Giải thích ví dụ và các thao tác trên cây AVL:

Sau khi chèn 20..


20
Sau khi chèn 5..
20
/
5
Sau khi chèn 15..
15
/ \
5 20
Sau khi chèn các phần tử 9, 13..
15
/ \
9 20
/ \
5 13

Tìm kiếm phần tử lớn nhất..


15
/ \
9 20
/ \
5 13
Sau khi chèn các phần tử 2, 6, 12, 14, ..
9
/ \
/ \
/ \
5 15
/ \ / \
2 6 / \
13 20
/ \
/ \
12 14

Tìm kiếm phần tử bé nhất...


9
/ \
/ \
/ \
5 15
/ \ / \
2 6 / \
13 20
/ \
/ \
12 14

Sau khi xoá 20..


9
/ \
/ \
/ \
5 13
/ \ / \
2 6 / \
12 15
/
/
14

Sau khi xoá 12..


9
/ \
/ \
/ \
5 14
/ \ / \
2 6 / \
13 15

Sau khi xoá 15..


9
/ \
/ \
/ \
5 14
/ \ /
2 6 /
13

Tìm phần tử nhỏ nhất không nhỏ hơn 5..


9
/ \
/ \
/ \
5 14
/ \ /
2 6 /
13

Tìm phần tử lớn nhất không lớn hơn 5..


9
/ \
/ \
/ \
5 14
/ \ /
2 6 /
13

Sau khi xoá 14..


9
/ \
/ \
/ \
5 13
/ \
2 6

Sau khi xoá 9..


5
/ \
/ \
/ \
2 13
/
6

Tập hợp chưa rỗng, chứa các phần tử 2, 5, 6, 13

Bài viết có sử dụng một số tài liệu tham khảo:


1. Cấu trúc dữ liệu thuật toán Prof. Nguyễn Đức Nghĩa
2. Introduction to Algorithms Thomas H. Cormen, Charles E. Leiserson, Ronald L.
Rivest, Clifford
3. www.en.wikipedia.org/wiki/AVL_tree

You might also like