Bài 32.5. Bài tập tổng hợp về cây nhị phân và AVL

Bài 32.5. Bài tập tổng hợp về cây nhị phân và AVL
Chào mừng các bạn đã quay trở lại với series blog về Cấu trúc dữ liệu và Giải thuật!
Chúng ta đã cùng nhau đi qua hành trình khám phá thế giới của cây nhị phân, hiểu về các phép duyệt, tìm kiếm, thêm, xoá cơ bản. Đặc biệt, chúng ta đã tìm hiểu sâu về cây AVL - một biến thể "cao cấp" hơn, đảm bảo tính cân bằng để tối ưu hiệu suất các thao tác.
Lý thuyết là một chuyện, nhưng để thực sự nắm vững và cảm nhận được sức mạnh cũng như sự phức tạp của những cấu trúc này, không có cách nào tốt hơn là thực hành thông qua các bài tập tổng hợp. Bài blog hôm nay sẽ là sân chơi để chúng ta cùng nhau áp dụng những kiến thức đã học vào việc giải quyết các vấn đề cụ thể, minh hoạ bằng ngôn ngữ C++.
Hãy cùng bắt tay vào các thử thách nhé!
Thử thách 1: Xây dựng cây AVL từ một dãy số
Bài tập đầu tiên khá cơ bản nhưng cực kỳ quan trọng: xây dựng một cây AVL bằng cách chèn tuần tự các phần tử từ một mảng cho trước. Thử thách ở đây không chỉ là chèn đúng vị trí theo quy tắc của cây tìm kiếm nhị phân, mà còn là kiểm tra và thực hiện các phép xoay (rotation) cần thiết để cây luôn giữ được tính cân bằng AVL sau mỗi lần chèn.
Ví dụ: Xây dựng cây AVL từ dãy số: [10, 20, 15, 25, 5]
- Chèn 10: Cây chỉ có gốc là 10. Cân bằng.
- Chèn 20: 20 > 10, chèn vào phải 10. Cây: 10 -> (null, 20). Cân bằng.
- Chèn 15: 15 > 10, vào phải 10. 15 < 20, vào trái 20. Cây: 10 -> (null, 20 -> (15, null)). Nút 20 có hệ số cân bằng là 1. Nút 10 có hệ số cân bằng là -2 (cao(phải)=2, cao(trái)=0). Mất cân bằng tại 10!. Đây là trường hợp LR (Left-Right) đối với gốc 10 (10 -> 20 là Right, 20 -> 15 là Left). Cần thực hiện xoay:
- Xoay phải 20 và 15 (tại nút 20): Cây con dưới 20 thành 15 -> (10, 20).
- Xoay trái 10 và 15 (tại nút 10): Gốc mới là 15. Cây thành: 15 -> (10, 20). Cân bằng.
- Chèn 25: 25 > 15, vào phải 15. 25 > 20, vào phải 20. Cây: 15 -> (10, 20 -> (null, 25)). Cân bằng.
- Chèn 5: 5 < 15, vào trái 15. 5 < 10, vào trái 10. Cây: 15 -> (10 -> (5, null), 20 -> (null, 25)). Cân bằng.
Cây cuối cùng (sau khi chèn 5):
15
/ \
10 20
/ \
5 25
Tất cả các nút đều có hệ số cân bằng là 0.
Bây giờ, hãy xem đoạn code C++ minh hoạ cách thực hiện phép chèn và tự động cân bằng này.
#include <iostream>
#include <algorithm> // For std::max
// Định nghĩa cấu trúc Node cho cây AVL
struct Node {
int key;
Node *left;
Node *right;
int height; // Lưu trữ độ cao của cây con gốc là node này
Node(int k) : key(k), left(nullptr), right(nullptr), height(1) {}
};
// Hàm lấy độ cao của một nút
int height(Node *N) {
if (N == nullptr)
return 0;
return N->height;
}
// Hàm lấy giá trị lớn nhất (dùng cho tính độ cao)
int max(int a, int b) {
return (a > b) ? a : b;
}
// Hàm lấy hệ số cân bằng (Balance Factor) của một nút
// BF = height(left) - height(right)
int getBalanceFactor(Node *N) {
if (N == nullptr)
return 0;
return height(N->left) - height(N->right);
}
// Hàm thực hiện xoay phải (Right Rotation)
// y x
// / \ Right Rotation (y) / \
// x T3 ------------------> T1 y
// / \ / \
//T1 T2 T2 T3
Node *rightRotate(Node *y) {
Node *x = y->left;
Node *T2 = x->right;
// Thực hiện xoay
x->right = y;
y->left = T2;
// Cập nhật độ cao sau khi xoay
y->height = max(height(y->left), height(y->right)) + 1;
x->height = max(height(x->left), height(x->right)) + 1;
// Trả về gốc mới
return x;
}
// Hàm thực hiện xoay trái (Left Rotation)
// x y
// / \ Left Rotation (x) / \
// T1 y -----------------> x T3
// / \ / \
// T2 T3 T1 T2
Node *leftRotate(Node *x) {
Node *y = x->right;
Node *T2 = y->left;
// Thực hiện xoay
y->left = x;
x->right = T2;
// Cập nhật độ cao sau khi xoay
x->height = max(height(x->left), height(x->right)) + 1;
y->height = max(height(y->left), height(y->right)) + 1;
// Trả về gốc mới
return y;
}
// Hàm chèn một nút mới vào cây AVL
Node *insert(Node *node, int key) {
// 1. Thực hiện chèn như cây tìm kiếm nhị phân thông thường
if (node == nullptr)
return new Node(key);
if (key < node->key)
node->left = insert(node->left, key);
else if (key > node->key)
node->right = insert(node->right, key);
else // Các khóa trùng lặp không được phép trong ví dụ này
return node;
// 2. Cập nhật độ cao của nút hiện tại (sau khi đệ quy trở về)
node->height = 1 + max(height(node->left), height(node->right));
// 3. Lấy hệ số cân bằng để kiểm tra
int balance = getBalanceFactor(node);
// 4. Nếu nút này mất cân bằng, có 4 trường hợp cần xử lý
// Trường hợp Left-Left (LL): Chèn vào cây con trái của cây con trái
if (balance > 1 && key < node->left->key)
return rightRotate(node);
// Trường hợp Right-Right (RR): Chèn vào cây con phải của cây con phải
if (balance < -1 && key > node->right->key)
return leftRotate(node);
// Trường hợp Left-Right (LR): Chèn vào cây con phải của cây con trái
if (balance > 1 && key > node->left->key) {
node->left = leftRotate(node->left); // Xoay trái cây con trái
return rightRotate(node); // Sau đó xoay phải nút hiện tại
}
// Trường hợp Right-Left (RL): Chèn vào cây con trái của cây con phải
if (balance < -1 && key < node->right->key) {
node->right = rightRotate(node->right); // Xoay phải cây con phải
return leftRotate(node); // Sau đó xoay trái nút hiện tại
}
// Nếu không mất cân bằng, trả về nút (không thay đổi)
return node;
}
// Hàm duyệt cây theo thứ tự giữa (Inorder Traversal) để kiểm tra
void inorderTraversal(Node *root) {
if (root != nullptr) {
inorderTraversal(root->left);
std::cout << root->key << " (BF=" << getBalanceFactor(root) << ", H=" << root->height << ") ";
inorderTraversal(root->right);
}
}
// Hàm hiển thị cây theo cấu trúc hơi trực quan (dùng cho debug)
void printTree(Node* root, int space = 0, int height = 10) {
if (root == nullptr) return;
space += height;
printTree(root->right, space);
std::cout << std::endl;
for (int i = height; i < space; i++) std::cout << " ";
std::cout << root->key << "(h=" << root->height << ", bf=" << getBalanceFactor(root) << ")\n";
printTree(root->left, space);
}
int main() {
Node *root = nullptr;
int keys[] = {10, 20, 15, 25, 5};
int n = sizeof(keys) / sizeof(keys[0]);
for (int key : keys) {
root = insert(root, key);
std::cout << "\n--- Sau khi chen " << key << " ---" << std::endl;
// inorderTraversal(root); // Có thể dùng inorder để kiểm tra thứ tự
printTree(root); // Hoặc dùng printTree để xem cấu trúc
std::cout << std::endl;
}
std::cout << "\n--- Cay AVL cuoi cung (Inorder Traversal) ---" << std::endl;
inorderTraversal(root);
std::cout << std::endl;
// Cần thêm hàm giải phóng bộ nhớ nếu chạy lâu hoặc với nhiều dữ liệu
// (Ví dụ này đơn giản nên bỏ qua phần giải phóng)
return 0;
}
Giải thích code:
struct Node
: Mỗi nút lưu giá trịkey
, con trỏ đến nút con trái (left
), nút con phải (right
) vàheight
(độ cao của cây con gốc tại nút đó).height
là thông tin cực kỳ quan trọng cho việc tính toán hệ số cân bằng.height(Node *N)
: Hàm trợ giúp đơn giản trả về độ cao của một nút (trả về 0 nếu nút lànullptr
).getBalanceFactor(Node *N)
: Tính toánheight(left) - height(right)
. Giá trị này cho biết mức độ "lệch" của cây con tại nútN
. Nếuabs(balance) > 1
, cây con mất cân bằng.rightRotate(Node *y)
vàleftRotate(Node *x)
: Đây là các hàm cốt lõi thực hiện phép xoay. Mỗi hàm nhận vào nút gốc của cây con cần xoay (y
cho xoay phải,x
cho xoay trái) và trả về nút gốc mới của cây con sau khi xoay. Quan trọng là phải cập nhật lại độ cao của các nút bị ảnh hưởng (y
vàx
trong cả hai hàm) sau khi thực hiện liên kết lại các con trỏ. Thứ tự cập nhật độ cao là quan trọng: cập nhật các nút "ở dưới" trước (ví dụy->height
trongrightRotate
) rồi mới đến nút "ở trên" (x->height
).insert(Node *node, int key)
: Đây là hàm đệ quy chính để chèn.- Bước 1: Giống như chèn trong cây tìm kiếm nhị phân thông thường. Tìm vị trí thích hợp và tạo nút mới.
- Bước 2: Sau khi đệ quy trở về, cập nhật lại
height
của nút hiện tại. Độ cao mới bằng 1 cộng với độ cao lớn nhất của hai cây con. - Bước 3: Tính
balanceFactor
cho nút hiện tại. - Bước 4: Kiểm tra
balanceFactor
. Nếuabs(balance) > 1
, cây con tại nút này mất cân bằng. Dựa vào dấu củabalance
và mối quan hệ giữakey
mới chèn với khóa của nút con (trái hoặc phải), xác định 1 trong 4 trường hợp (LL, RR, LR, RL) và gọi hàm xoay thích hợp. Hàminsert
phải trả về gốc mới của cây con sau khi có thể đã thực hiện xoay.
inorderTraversal
vàprintTree
: Các hàm trợ giúp để hiển thị cây, giúp kiểm tra xem các phần tử có đúng thứ tự sau khi chèn không và cấu trúc cây trông như thế nào.printTree
cố gắng hiển thị theo chiều ngang để dễ nhìn hơn.main
: Khởi tạo cây rỗng (root = nullptr
), duyệt qua mảngkeys
và gọiinsert
cho từng phần tử. Sau mỗi lần chèn, in ra trạng thái cây để theo dõi quá trình cân bằng.
Bài tập này giúp chúng ta củng cố lại cách chèn trong BST, cách tính độ cao và hệ số cân bằng, và quan trọng nhất là cách áp dụng các phép xoay (đơn và kép) để duy trì tính cân bằng AVL.
Thử thách 2: Xóa một nút trong cây AVL
Xóa một nút trong cây AVL thường phức tạp hơn chèn một chút. Ngoài việc tìm nút cần xóa và xử lý các trường hợp (nút lá, nút có một con, nút có hai con) như trong cây tìm kiếm nhị phân thông thường, chúng ta còn phải kiểm tra và tái cân bằng cây trên đường đi từ vị trí xóa (hoặc vị trí của nút thay thế, ví dụ như nút nhỏ nhất trong cây con phải) trở ngược lên gốc.
Ví dụ: Xóa nút có giá trị 20 từ cây AVL cuối cùng của Thử thách 1:
Cây ban đầu:
15
/ \
10 20
/ \
5 25
Nút cần xóa là 20. Nút này có một con (25). Theo quy tắc xóa trong BST, thay thế 20 bằng con của nó (25), sau đó xóa nút 25 ban đầu.
Cây sau khi xóa 20:
15
/ \
10 25
/
5
Kiểm tra cân bằng:
- Nút 5: BF=0.
- Nút 10: BF = height(5) - height(null) = 1 - 0 = 1. Cân bằng.
- Nút 25: BF = height(null) - height(null) = 0. Cân bằng.
- Nút 15: BF = height(10) - height(25) = 2 - 1 = 1. Cân bằng.
Cây vẫn cân bằng. Trong trường hợp này, không cần xoay.
Ví dụ phức tạp hơn: Xóa nút 10 từ cây trên.
Cây hiện tại:
15
/ \
10 25
/
5
Nút cần xóa là 10. Nó có một con (5). Thay thế 10 bằng 5, xóa nút 5 ban đầu.
Cây sau khi xóa 10:
15
/ \
5 25
Kiểm tra cân bằng:
- Nút 5: BF=0.
- Nút 25: BF=0.
- Nút 15: BF = height(5) - height(25) = 1 - 1 = 0. Cân bằng.
Cây vẫn cân bằng.
Bây giờ, hãy xem code C++ cho phép xóa trong cây AVL. Phần này sẽ tái sử dụng các hàm trợ giúp đã viết (height, getBalanceFactor, rotate...).
// ... (Cần include các hàm và cấu trúc Node từ ví dụ trên) ...
// Hàm tìm nút có giá trị nhỏ nhất trong một cây con (dùng khi xóa nút có 2 con)
Node *minValueNode(Node *node) {
Node *current = node;
// Duyệt xuống tận cùng bên trái để tìm nút nhỏ nhất
while (current && current->left != nullptr)
current = current->left;
return current;
}
// Hàm xóa một nút có key cho trước khỏi cây AVL
Node *deleteNode(Node *root, int key) {
// 1. Thực hiện xóa như cây tìm kiếm nhị phân thông thường
if (root == nullptr)
return root;
if (key < root->key)
root->left = deleteNode(root->left, key);
else if (key > root->key)
root->right = deleteNode(root->right, key);
else { // Tìm thấy nút cần xóa (key == root->key)
// Trường hợp 1: Nút lá hoặc chỉ có một con
if (root->left == nullptr || root->right == nullptr) {
Node *temp = root->left ? root->left : root->right;
// Trường hợp không có con (là nút lá)
if (temp == nullptr) {
temp = root;
root = nullptr; // Nút gốc trở thành null
} else // Trường hợp có một con
*root = *temp; // Copy nội dung của con vào nút hiện tại
delete temp; // Giải phóng bộ nhớ của nút đã xóa / nút con cũ
} else { // Trường hợp 2: Nút có hai con
// Tìm nút nhỏ nhất trong cây con phải (hoặc lớn nhất trong cây con trái)
Node *temp = minValueNode(root->right);
// Copy nội dung (key) của nút nhỏ nhất vào nút hiện tại
root->key = temp->key;
// Xóa nút nhỏ nhất trong cây con phải (nó chắc chắn là nút lá hoặc có 1 con)
root->right = deleteNode(root->right, temp->key);
}
}
// Nếu cây chỉ còn 1 nút (hoặc rỗng) sau khi xóa, không cần cân bằng
if (root == nullptr)
return root;
// 2. Cập nhật độ cao của nút hiện tại
root->height = 1 + max(height(root->left), height(root->right));
// 3. Lấy hệ số cân bằng
int balance = getBalanceFactor(root);
// 4. Nếu nút này mất cân bằng, thực hiện các phép xoay
// Trường hợp Left-Left (LL) - Cây con trái bị nặng và cân bằng hoặc nặng về trái
if (balance > 1 && getBalanceFactor(root->left) >= 0)
return rightRotate(root);
// Trường hợp Left-Right (LR) - Cây con trái bị nặng và nặng về phải
if (balance > 1 && getBalanceFactor(root->left) < 0) {
root->left = leftRotate(root->left);
return rightRotate(root);
}
// Trường hợp Right-Right (RR) - Cây con phải bị nặng và cân bằng hoặc nặng về phải
if (balance < -1 && getBalanceFactor(root->right) <= 0)
return leftRotate(root);
// Trường hợp Right-Left (RL) - Cây con phải bị nặng và nặng về trái
if (balance < -1 && getBalanceFactor(root->right) > 0) {
root->right = rightRotate(root->right);
return leftRotate(root);
}
// Nếu không mất cân bằng, trả về gốc
return root;
}
// Hàm duyệt cây theo thứ tự giữa (Inorder Traversal) để kiểm tra (copy lại cho đầy đủ)
void inorderTraversal(Node *root) {
if (root != nullptr) {
inorderTraversal(root->left);
std::cout << root->key << " (BF=" << getBalanceFactor(root) << ", H=" << root->height << ") ";
inorderTraversal(root->right);
}
}
// Hàm hiển thị cây theo cấu trúc hơi trực quan (dùng cho debug) (copy lại cho đầy đủ)
void printTree(Node* root, int space = 0, int height = 10) {
if (root == nullptr) return;
space += height;
printTree(root->right, space);
std::cout << std::endl;
for (int i = height; i < space; i++) std::cout << " ";
std::cout << root->key << "(h=" << root->height << ", bf=" << getBalanceFactor(root) << ")\n";
printTree(root->left, space);
}
int main() {
Node *root = nullptr;
int keys_insert[] = {9, 5, 10, 0, 6, 11, -1, 1, 2};
int n_insert = sizeof(keys_insert) / sizeof(keys_insert[0]);
for (int key : keys_insert) {
root = insert(root, key);
}
std::cout << "--- Cay AVL sau khi chen tat ca ---" << std::endl;
printTree(root);
std::cout << "\n";
int keys_delete[] = {10, 5, 9}; // Cac khoa can xoa
for (int key : keys_delete) {
std::cout << "\n--- Xoa nut " << key << " ---" << std::endl;
root = deleteNode(root, key);
printTree(root);
std::cout << "\n";
}
std::cout << "\n--- Cay AVL cuoi cung sau khi xoa (Inorder Traversal) ---" << std::endl;
inorderTraversal(root);
std::cout << std::endl;
// Can them ham giai phong bo nho
return 0;
}
Giải thích code:
minValueNode(Node *node)
: Hàm này là một tiện ích tiêu chuẩn cho việc xóa trong BST. Nó tìm nút có khóa nhỏ nhất trong cây con có gốc lànode
(bằng cách đi xuống nhánh trái nhất). Nút này được sử dụng để thay thế nút bị xóa nếu nút đó có hai con.deleteNode(Node *root, int key)
: Đây là hàm đệ quy để xóa.- Bước 1: Tìm nút cần xóa. Quá trình tìm kiếm giống như trong BST. Khi tìm thấy nút (
key == root->key
), xử lý các trường hợp:- Nút lá hoặc 1 con: Dễ nhất, chỉ cần liên kết nút cha với con của nút bị xóa (hoặc đặt con trỏ về
nullptr
nếu là nút lá), sau đó giải phóng bộ nhớ của nút bị xóa. - Nút 2 con: Tìm nút thay thế (ở đây là nút nhỏ nhất trong cây con phải dùng
minValueNode
), sao chép giá trị của nút thay thế vào nút hiện tại, sau đó đệ quy gọi lại hàmdeleteNode
để xóa nút thay thế gốc khỏi cây con phải. Quan trọng là lúc này chúng ta xóa nút thay thế (đã được copy giá trị), nó chỉ có tối đa 1 con nên việc xóa tiếp theo sẽ đơn giản hơn.
- Nút lá hoặc 1 con: Dễ nhất, chỉ cần liên kết nút cha với con của nút bị xóa (hoặc đặt con trỏ về
- Bước 2: Sau khi thực hiện xóa và đệ quy trở về từ cây con bị ảnh hưởng, cập nhật lại
height
của nút hiện tại (nút gốc của cây con mà chúng ta đang xử lý trong lần gọi đệ quy này). - Bước 3: Tính
balanceFactor
của nút hiện tại. - Bước 4: Kiểm tra
balanceFactor
. Tương tự như chèn, nếuabs(balance) > 1
, cây con tại nút này mất cân bằng. Dựa vào dấu củabalance
vàbalanceFactor
của nút con (trái hoặc phải), xác định 1 trong 4 trường hợp (LL, LR, RR, RL) và gọi hàm xoay thích hợp. Lưu ý các điều kiện kiểm trabalanceFactor
của nút con hơi khác so với lúc chèn, vì việc mất cân bằng có thể do nút con bị "hụt" đi một nhánh.
- Bước 1: Tìm nút cần xóa. Quá trình tìm kiếm giống như trong BST. Khi tìm thấy nút (
Xóa trong AVL đòi hỏi sự cẩn thận khi xử lý các trường hợp xóa khác nhau và đặc biệt là quá trình tái cân bằng đi ngược lên cây sau khi nút đã bị loại bỏ.
Thử thách 3: Kiểm tra xem một cây nhị phân có phải là cây AVL hợp lệ hay không?
Đôi khi chúng ta được cung cấp một cấu trúc cây nhị phân có sẵn và cần kiểm tra xem nó có tuân thủ cả hai quy tắc của cây tìm kiếm nhị phân và cây AVL hay không.
Một cây là cây AVL hợp lệ khi:
- Nó là một cây tìm kiếm nhị phân (BST): Với mọi nút, tất cả các khóa trong cây con bên trái đều nhỏ hơn khóa của nút, và tất cả các khóa trong cây con bên phải đều lớn hơn khóa của nút.
- Nó tuân thủ tính chất cân bằng AVL: Với mọi nút, sự khác biệt về chiều cao giữa cây con bên trái và cây con bên phải của nó (hệ số cân bằng) không vượt quá 1 (tức là -1, 0, hoặc 1).
Chúng ta có thể kiểm tra hai điều kiện này riêng biệt hoặc kết hợp lại để tăng hiệu quả. Cách hiệu quả nhất là kết hợp: duyệt cây, trong quá trình duyệt, kiểm tra cả tính chất BST và tính chất cân bằng cùng lúc.
Ý tưởng kết hợp: Viết một hàm đệ quy trả về chiều cao của cây con tại nút hiện tại. Trong khi tính chiều cao, kiểm tra:
- Tính chất BST: Khóa của nút hiện tại có lớn hơn giá trị lớn nhất trong cây con trái và nhỏ hơn giá trị nhỏ nhất trong cây con phải không? (Cần truyền thêm giới hạn min/max khi duyệt).
- Tính chất cân bằng: Hệ số cân bằng của nút hiện tại có hợp lệ không? Nếu không, cây không phải AVL. Nếu có, dùng chiều cao này để tính cho nút cha.
Nếu bất kỳ lúc nào kiểm tra thất bại, toàn bộ cây không phải là AVL hợp lệ.
Dưới đây là cách triển khai hàm kiểm tra tính hợp lệ của cây AVL. Chúng ta sẽ dùng một hàm đệ quy trả về chiều cao, đồng thời sử dụng một biến boolean được truyền theo tham chiếu (hoặc giá trị trả về đặc biệt) để báo hiệu nếu tìm thấy sự mất cân bằng hoặc vi phạm BST.
#include <iostream>
#include <algorithm> // For std::max
#include <limits> // For numeric_limits
// Định nghĩa cấu trúc Node (giống như trên, bỏ qua phần height và xoay vì chỉ kiểm tra)
struct Node {
int key;
Node *left;
Node *right;
Node(int k) : key(k), left(nullptr), right(nullptr) {}
};
// Hàm trợ giúp đệ quy kiểm tra đồng thời tính BST và tính cân bằng AVL
// Hàm này trả về chiều cao của cây con gốc là node,
// hoặc trả về một giá trị đặc biệt (ví dụ: -2) nếu cây con không hợp lệ (vi phạm BST hoặc AVL)
int checkAVLAndGetHeight(Node* node) {
// Trường hợp cơ sở: nút rỗng
if (node == nullptr) {
return 0; // Chiều cao của cây rỗng là 0
}
// Đệ quy kiểm tra cây con trái
int leftHeight = checkAVLAndGetHeight(node->left);
// Nếu cây con trái không hợp lệ, trả về ngay
if (leftHeight == -2) {
return -2;
}
// Đệ quy kiểm tra cây con phải
int rightHeight = checkAVLAndGetHeight(node->right);
// Nếu cây con phải không hợp lệ, trả về ngay
if (rightHeight == -2) {
return -2;
}
// Kiểm tra tính chất BST cục bộ:
// Nút trái lớn nhất <= nút hiện tại < nút phải nhỏ nhất
// (Cần cẩn thận với cách triển khai này, một cách tốt hơn là truyền min/max bound)
// Thay vào đó, chúng ta có thể dùng duyệt inorder và kiểm tra thứ tự tăng dần
// HOẶC kết hợp kiểm tra BST vào hàm này bằng cách truyền min/max allowed key.
// Cách truyền min/max bound hiệu quả hơn:
/*
int checkAVLAndGetHeight(Node* node, int min_key, int max_key) {
if (node == nullptr) return 0;
// Check BST property
if (node->key < min_key || node->key > max_key) return -2; // Vi pham BST
int leftHeight = checkAVLAndGetHeight(node->left, min_key, node->key -1); // Cap nhat max_key cho nhanh trai
if (leftHeight == -2) return -2;
int rightHeight = checkAVLAndGetHeight(node->right, node->key + 1, max_key); // Cap nhat min_key cho nhanh phai
if (rightHeight == -2) return -2;
// Check AVL balance property
if (std::abs(leftHeight - rightHeight) > 1) return -2; // Vi pham AVL balance
// Return height if valid
return std::max(leftHeight, rightHeight) + 1;
}
*/
// Cách đơn giản hơn (nhưng kém hiệu quả hơn) là kiểm tra BST riêng, hoặc giả định BST đúng và chỉ check balance
// Nhưng yêu cầu là "tổng hợp", nên phải check cả hai.
// Chúng ta sẽ dùng lại phương pháp truyền min/max cho chuẩn BST.
// Kiểm tra tính chất cân bằng AVL tại nút hiện tại
if (std::abs(leftHeight - rightHeight) > 1) {
return -2; // Vi phạm tính chất cân bằng AVL
}
// Nếu cả hai cây con hợp lệ và nút hiện tại cân bằng, trả về chiều cao của nút hiện tại
return std::max(leftHeight, rightHeight) + 1;
}
// Hàm kiểm tra xem một cây nhị phân có phải là cây tìm kiếm nhị phân hợp lệ hay không (phần riêng biệt nếu không dùng cách kết hợp trên)
// bool isBST(Node* node, int min_key, int max_key) {
// if (node == nullptr) return true;
// if (node->key < min_key || node->key > max_key) return false;
// return isBST(node->left, min_key, node->key - 1) &&
// isBST(node->right, node->key + 1, max_key);
// }
// Hàm kiểm tra xem một cây nhị phân có phải là cây AVL hợp lệ hoàn chỉnh hay không
bool isValidAVL(Node* root) {
// Cách 1: Kiểm tra BST riêng, sau đó kiểm tra cân bằng riêng
// Requires isBST and a separate checkHeight function that returns height or error
// Cách 2: Kết hợp cả hai vào một hàm đệ quy trả về chiều cao hoặc báo lỗi
// Chúng ta cần một hàm checkAVLAndGetHeightWithBSTCheck truyền min/max bound
// Để đơn giản hóa ví dụ code trong blog, chúng ta sẽ sử dụng hàm checkAVLAndGetHeight
// và NGẦM HIỂU rằng tính chất BST đã được đảm bảo bởi cấu trúc cây ban đầu
// HOẶC coi đây là bài tập chỉ kiểm tra tính cân bằng AVL dựa trên chiều cao giả định đúng.
// Tuy nhiên, bài tập tổng hợp nên cần check cả hai.
// Let's implement the combined check with min/max bounds.
// Hàm trợ giúp đệ quy kiểm tra đồng thời tính BST và tính cân bằng AVL (phiên bản đầy đủ)
// Trả về chiều cao hoặc -2 nếu không hợp lệ
auto checkAVLAndGetHeightWithBST =
[](auto self, Node* node, int min_key, int max_key) -> int {
// Trường hợp cơ sở: nút rỗng
if (node == nullptr) {
return 0; // Chiều cao của cây rỗng là 0
}
// Kiểm tra tính chất BST tại nút hiện tại
if (node->key < min_key || node->key > max_key) {
// std::cout << "Vi pham BST tai nut " << node->key << std::endl; // Debug
return -2; // Vi phạm tính chất BST
}
// Đệ quy kiểm tra cây con trái
int leftHeight = self(self, node->left, min_key, node->key - 1);
// Nếu cây con trái không hợp lệ, trả về ngay
if (leftHeight == -2) {
return -2;
}
// Đệ quy kiểm tra cây con phải
int rightHeight = self(self, node->right, node->key + 1, max_key);
// Nếu cây con phải không hợp lệ, trả về ngay
if (rightHeight == -2) {
return -2;
}
// Kiểm tra tính chất cân bằng AVL tại nút hiện tại
if (std::abs(leftHeight - rightHeight) > 1) {
// std::cout << "Vi pham AVL balance tai nut " << node->key << ". BF = " << (leftHeight - rightHeight) << std::endl; // Debug
return -2; // Vi phạm tính chất cân bằng AVL
}
// Nếu cả hai cây con hợp lệ và nút hiện tại cân bằng, trả về chiều cao của nút hiện tại
return std::max(leftHeight, rightHeight) + 1;
};
// Bắt đầu kiểm tra từ gốc với giới hạn min/max ban đầu là vô cực
return checkAVLAndGetHeightWithBST(checkAVLAndGetHeightWithBST, root, std::numeric_limits<int>::min(), std::numeric_limits<int>::max()) != -2;
}
// Hàm tạo một cây nhị phân MẤT CÂN BẰNG AVL (nhưng vẫn là BST) để kiểm tra
Node* createUnbalancedAVLTree() {
Node* root = new Node(20);
root->left = new Node(10);
root->right = new Node(30);
root->left->left = new Node(5);
root->left->right = new Node(15);
root->left->left->left = new Node(3); // Nút 3 gây mất cân bằng tại 10, rồi tại 20
// Cây này là BST, nhưng nút 10 có balance factor 2, nút 20 có balance factor 2
return root;
}
// Hàm tạo một cây nhị phân KHÔNG PHẢI BST (nhưng có thể trông cân bằng) để kiểm tra
Node* createNonBSTTree() {
Node* root = new Node(10);
root->left = new Node(5);
root->right = new Node(15);
root->left->left = new Node(3);
root->left->right = new Node(12); // Lỗi BST: 12 ở cây con trái của 10 nhưng lớn hơn 10
return root;
}
int main() {
// Test case 1: Cây AVL hợp lệ (từ ví dụ chèn ban đầu)
Node *avl_root = nullptr;
int keys_insert[] = {10, 20, 15, 25, 5};
for (int key : keys_insert) {
// Cần sử dụng hàm insert từ ví dụ 1 để đảm bảo nó là AVL
// Để test hàm isValidAVL, chúng ta sẽ tạo cây thủ công hoặc dùng hàm insert đầy đủ
// Tạm dùng hàm insert từ ví dụ 1 (cần copy toàn bộ code struct Node và hàm insert)
// Giả sử hàm insert đã có ở đây:
// avl_root = insert(avl_root, key);
}
// Tạo cây AVL hợp lệ thủ công cho đơn giản ví dụ này
Node* valid_avl = new Node(15);
valid_avl->left = new Node(10);
valid_avl->right = new Node(20);
valid_avl->left->left = new Node(5);
valid_avl->right->right = new Node(25);
std::cout << "--- Kiem tra Cay AVL hop le 1 ---" << std::endl;
if (isValidAVL(valid_avl)) {
std::cout << "-> CAY HOP LE!" << std::endl;
} else {
std::cout << "-> CAY KHONG HOP LE!" << std::endl;
}
// Test case 2: Cây không phải AVL (mất cân bằng)
Node* unbalanced_avl = createUnbalancedAVLTree();
std::cout << "\n--- Kiem tra Cay mat can bang AVL ---" << std::endl;
if (isValidAVL(unbalanced_avl)) {
std::cout << "-> CAY HOP LE!" << std::endl;
} else {
std::cout << "-> CAY KHONG HOP LE! (Vi pham can bang AVL)" << std::endl;
}
// Test case 3: Cây không phải BST
Node* non_bst = createNonBSTTree();
std::cout << "\n--- Kiem tra Cay khong phai BST ---" << std::endl;
if (isValidAVL(non_bst)) {
std::cout << "-> CAY HOP LE!" << std::endl;
} else {
std::cout << "-> CAY KHONG HOP LE! (Vi pham tinh chat BST)" << std::endl;
}
// Can them ham giai phong bo nho cho tat ca cac cay da tao
return 0;
}
Giải thích code:
struct Node
: Cấu trúc nút đơn giản chỉ cầnkey
,left
,right
. Chiều cao không lưu trong nút nữa mà được tính toán trong hàm kiểm tra.checkAVLAndGetHeightWithBST
(lambda function): Đây là hàm đệ quy chính.- Nó nhận con trỏ
node
hiện tại và hai tham sốmin_key
,max_key
để kiểm tra tính chất BST. Mọi nút trong cây con củanode
phải có giá trị nằm trong khoảng[min_key, max_key]
. - Trường hợp cơ sở: Nếu
node
lànullptr
, chiều cao là 0, trả về 0. - Kiểm tra tính chất BST: So sánh
node->key
vớimin_key
vàmax_key
. Nếu vi phạm, trả về-2
(một giá trị đặc biệt báo hiệu lỗi). - Gọi đệ quy cho cây con trái:
self(self, node->left, min_key, node->key - 1)
. Lưu ý cập nhậtmax_key
cho cây con trái thànhnode->key - 1
. - Gọi đệ quy cho cây con phải:
self(self, node->right, node->key + 1, max_key)
. Lưu ý cập nhậtmin_key
cho cây con phải thànhnode->key + 1
. - Nếu một trong hai lời gọi đệ quy trả về
-2
, có nghĩa là có lỗi ở cây con, ta trả về-2
ngay lập tức. - Nếu cả hai cây con đều hợp lệ, kiểm tra tính chất cân bằng AVL: Tính hiệu chiều cao giữa cây con trái (
leftHeight
) và cây con phải (rightHeight
). Nếuabs(leftHeight - rightHeight) > 1
, trả về-2
. - Nếu tất cả các kiểm tra đều vượt qua, nút hiện tại hợp lệ và cân bằng. Trả về chiều cao của cây con gốc là nút này:
max(leftHeight, rightHeight) + 1
.
- Nó nhận con trỏ
isValidAVL(Node* root)
: Hàm public mà người dùng gọi. Nó chỉ đơn giản gọi hàm đệ quycheckAVLAndGetHeightWithBST
bắt đầu từroot
với giới hạnmin_key
vàmax_key
là giá trị nhỏ nhất và lớn nhất có thể của kiểuint
. Nếu kết quả trả về khác-2
, tức là không có lỗi nào được phát hiện trong quá trình duyệt, cây là AVL hợp lệ.createUnbalancedAVLTree
vàcreateNonBSTTree
: Các hàm tiện ích để tạo ra các cây mẫu dùng cho việc kiểm tra các trường hợp không hợp lệ.main
: Tạo các cây mẫu và gọiisValidAVL
để kiểm tra, in kết quả ra màn hình.
Thử thách này cho thấy cách kết hợp các tính chất của cây tìm kiếm nhị phân và cây AVL vào một thuật toán kiểm tra hiệu quả.
Tổng kết các bài tập
Qua ba thử thách vừa rồi, chúng ta đã ôn lại và thực hành các kỹ năng quan trọng khi làm việc với cây nhị phân và cây AVL:
- Hiểu rõ quá trình chèn và xoá một nút trong cây tìm kiếm nhị phân.
- Nắm vững khái niệm độ cao, hệ số cân bằng và cách tính toán chúng.
- Hiểu và áp dụng thành thạo các phép xoay (đơn và kép) để duy trì tính cân bằng AVL sau khi chèn/xoá.
- Biết cách kiểm tra tính hợp lệ của một cấu trúc cây nhị phân theo cả hai tiêu chí: BST và cân bằng AVL.
Comments