Professional Documents
Culture Documents
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à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.
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)
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:
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;
}
}
Đị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ướ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)
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);
}
}
}
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
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á 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
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
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)