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án height(left) - height(right). Giá trị này cho biết mức độ "lệch" của cây con tại nút N. Nếu abs(balance) > 1, cây con mất cân bằng.
  • rightRotate(Node *y)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 (yx 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 trong rightRotate) 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ếu abs(balance) > 1, cây con tại nút này mất cân bằng. Dựa vào dấu của balance và mối quan hệ giữa key 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àm insert phải trả về gốc mới của cây con sau khi có thể đã thực hiện xoay.
  • inorderTraversalprintTree: 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ảng keys và gọi insert 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àm deleteNode để 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.
    • 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ếu abs(balance) > 1, cây con tại nút này mất cân bằng. Dựa vào dấu của balancebalanceFactor 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 tra balanceFactor 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.

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âncây AVL hay không.

Một cây là cây AVL hợp lệ khi:

  1. 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.
  2. 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ần key, 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ủa node phải có giá trị nằm trong khoảng [min_key, max_key].
    • Trường hợp cơ sở: Nếu nodenullptr, chiều cao là 0, trả về 0.
    • Kiểm tra tính chất BST: So sánh node->key với min_keymax_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ật max_key cho cây con trái thành node->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ật min_key cho cây con phải thành node->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ếu abs(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.
  • isValidAVL(Node* root): Hàm public mà người dùng gọi. Nó chỉ đơn giản gọi hàm đệ quy checkAVLAndGetHeightWithBST bắt đầu từ root với giới hạn min_keymax_key là giá trị nhỏ nhất và lớn nhất có thể của kiểu int. 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ệ.
  • createUnbalancedAVLTreecreateNonBSTTree: 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ọi isValidAVL để 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âncâ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âncây AVL:

  • Hiểu rõ quá trình chènxoá 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í: BSTcân bằng AVL.

Comments

There are no comments at the moment.