Menu
CoddyTech

Cây AVL

Cập nhật lần cuối

Cây AVL (AVL tree) là một cây tìm kiếm nhị phân tự cân bằng. Nó hoạt động như một BST thông thường, nhưng sau mỗi lần chèn, cây kiểm tra hệ số cân bằng (balance factor) của từng nút tổ tiên, tức là chênh lệch chiều cao giữa cây con trái và cây con phải. Nếu có nút nào mất cân bằng (chênh lệch lớn hơn 1), cây thực hiện các phép xoay để khôi phục cân bằng. Nhấn Phát ở trên để xem từng giá trị được chèn vào và cây tự xoay về đúng hình dạng.

Vì không bao giờ để cây lệch quá một chút, cây AVL bảo đảm chiều cao O(log n), nên tìm kiếm, chèn và xóa luôn là O(log n), kể cả với dữ liệu đã sắp xếp vốn sẽ làm hỏng một BST thông thường. Cái giá phải trả là các phép xoay bổ sung và việc cập nhật chiều cao sau mỗi lần chèn.

Độ phức tạp thời gian và không gian

Thao tácĐộ phức tạpGhi chú
Tìm kiếmO(log n)Chiều cao luôn khoảng ~1,44 log n
ChènO(log n)Cộng thêm O(1) phép xoay
XóaO(log n)Cộng thêm O(log n) phép xoay
Không gianO(n)Mỗi nút lưu thêm một trường chiều cao

Bốn trường hợp xoay

Trường hợpMất cân bằngCách sửa
Left-LeftLệch về bên trái của con tráiMột phép xoay phải
Right-RightLệch về bên phải của con phảiMột phép xoay trái
Left-RightLệch về bên phải của con tráiXoay trái, rồi xoay phải
Right-LeftLệch về bên trái của con phảiXoay phải, rồi xoay trái

Ví dụ minh họa

Chèn lần lượt từng giá trị của [10, 20, 30, 40, 50, 25]:

BướcCấu trúcThao tác
Chèn 1010Nút đầu tiên trở thành gốc; cây cân bằng
Chèn 2010(_, 20)Đi sang phải 10; vẫn cân bằng
Chèn 3020(10, 30)Right-Right tại 10, nên một phép xoay trái đưa 20 lên làm gốc
Chèn 4020(10, 30(_, 40))Đi sang phải 30; mọi hệ số cân bằng vẫn nằm trong ±1
Chèn 5020(10, 40(30, 50))Right-Right tại 30, nên phép xoay trái tại 30 đưa 40 lên
Chèn 2530(20(10, 25), 40(_, 50))Right-Left tại 20: xoay phải cây con của 40, rồi xoay trái 20

Khi nào nên dùng cây AVL

Nên dùng khiTránh dùng khi
Số lần tra cứu nhiều hơn hẳn số lần chèn và bạn muốn chiều cao cây thấp nhất có thểChèn và xóa chiếm phần lớn: các phép xoay và cân bằng lại tốn kém hơn so với cây đỏ đen
Bạn cần bảo đảm trường hợp xấu nhất O(log n), kể cả với dữ liệu cố tình gây bất lợi hoặc đã sắp xếpMột bảng băm thông thường là đủ: bạn không cần duyệt theo thứ tự hay truy vấn theo khoảng
Bạn cần các thao tác có thứ tự: duyệt trung thứ tự, tìm phần tử liền trước/liền sau, truy vấn theo khoảngTập dữ liệu rất nhỏ: một BST thông thường hoặc mảng đã sắp xếp đơn giản hơn và đủ nhanh
Dữ liệu có thể đến theo thứ tự đã sắp xếp, điều sẽ biến một BST không cân bằng thành danh sách liên kếtBộ nhớ quá eo hẹp đến mức không thể chịu thêm trường chiều cao cho mỗi nút

Code AVL Tree

Một cách cài đặt AVL Tree gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.

Code AVL Tree bằng Python

Python
1class Node:2    def __init__(self, key):3        self.key = key4        self.left = None5        self.right = None6        self.height = 17
8
9def height(node):10    return node.height if node else 011
12
13def update(node):14    node.height = 1 + max(height(node.left), height(node.right))15
16
17def rotate_right(y):18    x = y.left19    y.left = x.right20    x.right = y21    update(y)22    update(x)23    return x24
25
26def rotate_left(x):27    y = x.right28    x.right = y.left29    y.left = x30    update(x)31    update(y)32    return y33
34
35def insert(node, key):36    if node is None:37        return Node(key)38    if key < node.key:39        node.left = insert(node.left, key)40    else:41        node.right = insert(node.right, key)42    update(node)43    balance = height(node.left) - height(node.right)44    # Four imbalance cases: LL, RR, LR, RL45    if balance > 1 and key < node.left.key:46        return rotate_right(node)47    if balance < -1 and key > node.right.key:48        return rotate_left(node)49    if balance > 1:50        node.left = rotate_left(node.left)51        return rotate_right(node)52    if balance < -1:53        node.right = rotate_right(node.right)54        return rotate_left(node)55    return node56
57
58def inorder(node):59    if node is None:60        return []61    return inorder(node.left) + [node.key] + inorder(node.right)62
63
64root = None65for key in [10, 20, 30, 40, 50, 25]:66    root = insert(root, key)67
68print("Inorder:", inorder(root))69print("Root:", root.key, "| tree height:", root.height)
Chạy code này trong Playground Python

Câu hỏi thường gặp về cây AVL

Hệ số cân bằng trong cây AVL là gì?

Hệ số cân bằng của một nút là chiều cao cây con trái trừ đi chiều cao cây con phải. Cây AVL giữ hệ số cân bằng của mọi nút ở mức -1, 0 hoặc +1; nếu một lần chèn đẩy giá trị này ra ngoài khoảng đó, một phép xoay sẽ khôi phục cân bằng.

Cây AVL và cây đỏ đen khác nhau như thế nào?
Cả hai đều là BST tự cân bằng với các thao tác O(log n). Cây AVL được cân bằng chặt chẽ hơn nên tra cứu nhanh hơn một chút, nhưng có thể phải xoay nhiều hơn khi chèn/xóa. Cây đỏ đen cân bằng lỏng hơn và ưu tiên ít phép xoay hơn, vì vậy nhiều thư viện chuẩn dùng nó cho map và set.
Vì sao nên dùng cây AVL thay cho cây tìm kiếm nhị phân thông thường?
Một BST thông thường có thể suy biến thành các thao tác O(n) nếu giá trị được chèn theo thứ tự đã sắp xếp, tạo thành một chuỗi lệch hẳn về một bên. Cây AVL tự xoay để giữ cân bằng, bảo đảm chiều cao và các thao tác luôn là O(log n) bất kể thứ tự chèn.
Cây AVL có tốt hơn cây đỏ đen cho chỉ mục cơ sở dữ liệu không?

Còn tùy vào khối lượng công việc. Cây AVL cân bằng cứng nhắc hơn, nên có lợi thế khi thao tác đọc chiếm phần lớn và bạn muốn đường tìm kiếm ngắn nhất có thể. Tuy nhiên, phần lớn cơ sở dữ liệu và thư viện của các ngôn ngữ chọn cây đỏ đen (hoặc B-tree) vì chúng cân bằng lại với ít phép xoay hơn khi ghi nhiều, điều quan trọng hơn khi chèn và xóa diễn ra thường xuyên.

Một lần chèn vào cây AVL cần bao nhiêu phép xoay?
Nhiều nhất là một lần xoay, có thể là xoay đơn hoặc xoay kép (hai bước), để cân bằng lại cây sau khi chèn một giá trị. Lý do là một lần chèn chỉ tăng chiều cao của một cây con thêm tối đa 1, nên chỉ cần sửa một lần tại nút tổ tiên mất cân bằng thấp nhất. Xóa thì khác: có thể cần tới O(log n) phép xoay vì việc cân bằng lại lan dần lên phía gốc.
Có cần cập nhật chiều cao các nút sau mỗi phép xoay không?

Có. Một bug phổ biến là xoay con trỏ đúng nhưng quên tính lại chiều cao của hai nút liên quan. Sau mỗi phép xoay, bạn phải cập nhật chiều cao của nút bị hạ xuống trước, rồi đến nút được đưa lên, vì chiều cao của nút được đưa lên phụ thuộc vào các nút con mới của nó. Bỏ qua bước này sẽ để lại hệ số cân bằng sai, làm hỏng các quyết định cân bằng lại về sau.

Hình minh họa các ngôn ngữ lập trình của Coddy

Thành thạo thuật toán cùng Coddy

Bắt đầu