Cây AVL: Xoay, chèn, xóa bằng C++ Ví dụ

⚡ Tóm tắt thông minh

Cây AVL là cây tìm kiếm nhị phân tự cân bằng, trong đó sự khác biệt về chiều cao giữa cây con bên trái và bên phải của mỗi nút nằm trong khoảng -1, 0 hoặc +1, đảm bảo hiệu suất tìm kiếm O(log n).

  • 🌲 Định nghĩa: Cây tìm kiếm nhị phân trong đó hệ số cân bằng của mỗi nút nằm trong tập {-1, 0, +1}, được đặt tên theo các nhà phát minh Adelson-Velsky và Landis.
  • ⚖️ Yếu tố cân bằng: Được tính bằng chiều cao(trái) − chiều cao(phải); các giá trị nằm ngoài {-1, 0, +1} sẽ kích hoạt phép quay để khôi phục trạng thái cân bằng.
  • 🔄 Vòng quay: Bốn trường hợp — LL, RR, LR và RL — sắp xếp lại các nút sau khi chèn hoặc xóa không cân bằng để giữ cho chiều cao của cây có dạng logarit.
  • Chèn: Chèn BST tiêu chuẩn, tiếp theo là bước đi hướng lên để tính toán lại các yếu tố cân bằng và thực hiện tối đa một vòng quay đơn hoặc kép.
  • Xóa: Tương tự như việc xóa cây tìm kiếm nhị phân (BST) nhưng có thể gây ra nhiều vòng xoay liên tiếp trên cây vì chiều cao của cây con có thể giảm ở mỗi tổ tiên.
  • 🚀 Ứng dụng Cơ sở dữ liệu, chỉ mục trong bộ nhớ, siêu dữ liệu hệ thống tệp và cấu trúc tìm kiếm AI sử dụng cây AVL để tra cứu có thứ tự nhanh chóng.

Cây AVL

Cây AVL là gì?

Cây AVL Cây tìm kiếm nhị phân (BST) là loại cây mà sự khác biệt về chiều cao giữa cây con bên trái và bên phải của mỗi nút là -1, 0 hoặc +1. Chúng là các BST tự cân bằng duy trì thời gian tìm kiếm theo logarit, được đặt tên theo những người phát minh ra chúng là Adelson-Velsky và Landis (AVL).

Cây AVL hoạt động như thế nào?

Để hiểu lý do tại sao cây AVL tồn tại, hãy xem điều gì xảy ra sai với một cây thông thường. Cây tìm kiếm nhị phânHãy xem xét các khóa này được chèn theo thứ tự đã cho:

Cây AVL hoạt động

Trực quan hóa cây AVL

Cây phát triển tuyến tính khi các khóa được chèn vào theo thứ tự tăng dần, làm suy giảm độ phức tạp tìm kiếm xuống O(n). Điều đó làm mất đi mục đích của cây tìm kiếm nhị phân (BST) — chỉ có cây cân bằng mới giữ cho quá trình tìm kiếm có độ phức tạp logarit. Bây giờ hãy xem xét cùng các khóa đó được chèn theo thứ tự khác.

Cây AVL hoạt động

Cùng một khóa, nhưng thứ tự chèn khác nhau sẽ tạo ra hình dạng nông hơn, do đó mọi tìm kiếm đều chạy trong O(log n). Cây AVL đảm bảo hình dạng đó bằng cách theo dõi chiều cao trong mỗi lần chèn và điều chỉnh sự mất cân bằng mà không phá vỡ thứ tự của cây tìm kiếm nhị phân (BST).

Hệ số cân bằng trong cây AVL

Hệ số cân bằng (BF) tracks là chiều cao của mỗi nút để cây có thể tự cân bằng một cách linh hoạt.

Thuộc tính của yếu tố cân bằng

Hệ số cân bằng trong cây AVL

Cây AVL hệ số cân bằng

  • Hệ số cân bằng là hiệu số giữa chiều cao của cây con bên trái và chiều cao của cây con bên phải.
  • Balance factor(node) = height(node->left) − height(node->right)
  • Các giá trị được cho phép duy nhất là -1, 0 và +1.
  • Giá trị -1 có nghĩa là cây con bên phải chứa thêm một cấp độ — nút này nặng về phía bên phải.
  • Giá trị +1 có nghĩa là cây con bên trái chứa thêm một cấp độ — nút này nặng về phía bên trái.
  • Giá trị bằng 0 có nghĩa là cả hai phía có chiều cao bằng nhau — nút này hoàn toàn cân bằng.

Xoay AVL

Các phép quay được thực hiện bất cứ khi nào việc chèn hoặc xóa phá vỡ quy tắc hệ số cân bằng. Bốn trường hợp đó là LL, RR, LR và RL.

Xoay trái – Xoay trái

Việc xoay này được thực hiện khi một nút mới được chèn vào nút con bên trái của cây con bên trái.

Cây AVL Trái – Xoay Trái

Cây AVL Trái – Xoay Trái

Một phép quay phải duy nhất được thực hiện. Trường hợp này xảy ra khi một nút có BF +2 và nút con bên trái của nó có BF +1.

Phải – Xoay Phải

Việc xoay này được thực hiện khi một nút mới được chèn vào con bên phải của cây con bên phải.

Cây AVL phải – Xoay phải

Một phép quay trái duy nhất được thực hiện. Trường hợp này xảy ra khi một nút có BF −2 và nút con bên phải của nó có BF −1.

Xoay phải – trái

Việc xoay này được thực hiện khi một nút mới được chèn vào con trái của cây con bên phải.

Cây AVL Phải – Xoay Trái

Sự kiện này được kích hoạt khi BF(node) = −2 và BF(right-child) = +1. Xoay phải nút con, sau đó xoay trái nút chính.

Xoay Trái – Phải

Việc xoay này được thực hiện khi một nút mới được chèn vào nút con bên phải của cây con bên trái.

Cây AVL Xoay Trái – Phải

Sự kiện này được kích hoạt khi BF(node) = +2 và BF(left-child) = −1. Xoay trái nút con, sau đó xoay phải nút chính.

Chèn vào cây AVL

Thao tác chèn gần như giống hệt với thao tác chèn trong cây tìm kiếm nhị phân (BST). Sau mỗi lần chèn, cây sẽ di chuyển lên trên và cân bằng lại. Thao tác chèn mất thời gian O(log n) trong trường hợp xấu nhất.

Chèn vào cây AVL

Triển khai chèn cây AVL

Bước 1: Chèn nút bằng thuật toán cây tìm kiếm nhị phân (BST) tiêu chuẩn. Trong ví dụ trên, hãy chèn 160.

Bước 2: Cập nhật hệ số cân bằng của mỗi tổ tiên dọc theo đường dẫn chèn.

Bước 3: Nếu bất kỳ tổ tiên nào vi phạm phạm vi hệ số cân bằng, hãy thực hiện phép quay tương ứng. Trong ví dụ này, hệ số cân bằng của nút 350 bị vi phạm, vì vậy phép quay LL sẽ khôi phục lại sự cân bằng.

  1. If BF(node) = +2BF(left-child) = +1Thực hiện xoay LL.
  2. If BF(node) = −2BF(right-child) = −1Thực hiện xoay RR.
  3. If BF(node) = −2BF(right-child) = +1Thực hiện phép quay RL.
  4. If BF(node) = +2BF(left-child) = −1Thực hiện phép quay LR.

Xóa trong cây AVL

Việc xóa tuân theo cùng một logic như cây tìm kiếm nhị phân thông thường và sẽ cân bằng lại sau đó.

Bước 1: Tìm phần tử trong cây.

Bước 2: Xóa nút bằng cách sử dụng phương pháp xóa cây tìm kiếm nhị phân chuẩn.

Bước 3: Có hai trường hợp có thể xảy ra.

Trường hợp 1: Xóa từ cây con bên phải.

  • 1A. If BF(node) = +2BF(left-child) = +1Thực hiện xoay LL.
  • Năm 1B. If BF(node) = +2BF(left-child) = −1Thực hiện phép quay LR.
  • 1C. If BF(node) = +2BF(left-child) = 0Thực hiện xoay LL.

Xóa trong cây AVL

Trường hợp 2: Xóa phần tử từ cây con bên trái.

  • 2A. If BF(node) = −2BF(right-child) = −1Thực hiện xoay RR.
  • Năm 2B. If BF(node) = −2BF(right-child) = +1Thực hiện phép quay RL.
  • 2C. If BF(node) = −2BF(right-child) = 0Thực hiện xoay RR.

Xóa trong cây AVL

C++ Ví dụ về cây AVL

Dưới đây là một C++ chương trình triển khai cây AVL:

#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;

struct node {
    struct node *left;
    int data;
    int height;
    struct node *right;
};

class AVL {
public:
    struct node *root;

    AVL() {
        this->root = NULL;
    }

    int calheight(struct node *p) {
        if (p->left && p->right) {
            if (p->left->height < p->right->height)
                return p->right->height + 1;
            else
                return p->left->height + 1;
        }
        else if (p->left && p->right == NULL) {
            return p->left->height + 1;
        }
        else if (p->left == NULL && p->right) {
            return p->right->height + 1;
        }
        return 0;
    }

    int bf(struct node *n) {
        if (n->left && n->right)
            return n->left->height - n->right->height;
        else if (n->left && n->right == NULL)
            return n->left->height;
        else if (n->left == NULL && n->right)
            return -n->right->height;
        return 0;
    }

    struct node *llrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->left;
        p->left = tp->right;
        tp->right = p;
        return tp;
    }

    struct node *rrrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->right;
        p->right = tp->left;
        tp->left = p;
        return tp;
    }

    struct node *rlrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->right;
        struct node *tp2 = p->right->left;
        p->right = tp2->left;
        tp->left = tp2->right;
        tp2->left = p;
        tp2->right = tp;
        return tp2;
    }

    struct node *lrrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->left;
        struct node *tp2 = p->left->right;
        p->left = tp2->right;
        tp->right = tp2->left;
        tp2->right = p;
        tp2->left = tp;
        return tp2;
    }

    struct node *insert(struct node *r, int data) {
        if (r == NULL) {
            r = new struct node;
            r->data = data;
            r->left = r->right = NULL;
            r->height = 1;
            return r;
        }
        if (data < r->data)
            r->left = insert(r->left, data);
        else
            r->right = insert(r->right, data);

        r->height = calheight(r);

        if (bf(r) == 2 && bf(r->left) == 1)       r = llrotation(r);
        else if (bf(r) == -2 && bf(r->right) == -1) r = rrrotation(r);
        else if (bf(r) == -2 && bf(r->right) == 1)  r = rlrotation(r);
        else if (bf(r) == 2 && bf(r->left) == -1)   r = lrrotation(r);

        return r;
    }

    void levelorder_newline() {
        if (this->root == NULL) {
            cout << "\nEmpty tree\n";
            return;
        }
        levelorder_newline(this->root);
    }

    void levelorder_newline(struct node *v) {
        queue<struct node *> q;
        struct node *cur;
        q.push(v);
        q.push(NULL);
        while (!q.empty()) {
            cur = q.front();
            q.pop();
            if (cur == NULL && q.size() != 0) {
                cout << "\n";
                q.push(NULL);
                continue;
            }
            if (cur != NULL) {
                cout << " " << cur->data;
                if (cur->left != NULL)  q.push(cur->left);
                if (cur->right != NULL) q.push(cur->right);
            }
        }
    }

    struct node *deleteNode(struct node *p, int data) {
        if (p->left == NULL && p->right == NULL) {
            if (p == this->root) this->root = NULL;
            delete p;
            return NULL;
        }
        struct node *q;
        if (p->data < data)      p->right = deleteNode(p->right, data);
        else if (p->data > data) p->left  = deleteNode(p->left, data);
        else {
            if (p->left != NULL) {
                q = inpre(p->left);
                p->data = q->data;
                p->left = deleteNode(p->left, q->data);
            } else {
                q = insuc(p->right);
                p->data = q->data;
                p->right = deleteNode(p->right, q->data);
            }
        }

        if (bf(p) == 2 && bf(p->left) == 1)         p = llrotation(p);
        else if (bf(p) == 2 && bf(p->left) == -1)    p = lrrotation(p);
        else if (bf(p) == 2 && bf(p->left) == 0)     p = llrotation(p);
        else if (bf(p) == -2 && bf(p->right) == -1)  p = rrrotation(p);
        else if (bf(p) == -2 && bf(p->right) == 1)   p = rlrotation(p);
        else if (bf(p) == -2 && bf(p->right) == 0)   p = rrrotation(p);

        return p;
    }

    struct node *inpre(struct node *p) {
        while (p->right != NULL) p = p->right;
        return p;
    }

    struct node *insuc(struct node *p) {
        while (p->left != NULL) p = p->left;
        return p;
    }

    ~AVL() {}
};

int main() {
    AVL b;
    int c, x;
    do {
        cout << "\n1.Display levelorder on newline";
        cout << "\n2.Insert";
        cout << "\n3.Delete\n";
        cout << "\n0.Exit\n";
        cout << "\nChoice: ";
        cin >> c;
        switch (c) {
        case 1: b.levelorder_newline(); break;
        case 2:
            cout << "\nEnter no. "; cin >> x;
            b.root = b.insert(b.root, x);
            break;
        case 3:
            cout << "\nWhat to delete? "; cin >> x;
            b.root = b.deleteNode(b.root, x);
            break;
        case 0: break;
        }
    } while (c != 0);
}

Ví dụ về cách chạy đoạn mã trên:

  1. Sao chép đoạn mã trên và lưu vào một tệp có tên là... avl.cpp.
  2. Biên dịch mã:
g++ avl.cpp -o run
  1. Chạy mã.
./run

C++ Ví dụ về cây AVL

Ưu điểm của cây AVL

  • Chiều cao của cây AVL luôn cân bằng và không bao giờ vượt quá log N.
  • Quá trình tìm kiếm nhanh hơn so với cây tìm kiếm nhị phân thông thường vì cây này không thể bị thoái hóa.
  • Quá trình tự cân bằng diễn ra tự động — không cần bước thiết lập lại.
  • Hiệu năng xác định phù hợp với các hệ thống thời gian thực và các chỉ mục trong bộ nhớ.

Câu Hỏi Thường Gặp

Cây AVL là một cây tìm kiếm nhị phân tự cân bằng, trong đó hệ số cân bằng của mỗi nút luôn nằm trong tập hợp {-1, 0, +1}. Các phép xoay khôi phục lại tính bất biến này sau mỗi lần chèn hoặc xóa.ping Tìm kiếm, chèn và xóa với độ phức tạp O(log n).

Hệ số cân bằng của một nút bằng chiều cao (cây con bên trái) trừ đi chiều cao (cây con bên phải). Giá trị phải nằm trong khoảng {-1, 0, +1}. Hệ số cân bằng +2 hoặc -2 báo hiệu rằng thao tác chèn hoặc xóa đã làm mất cân bằng nút đó và cần phải xoay nút.

Bốn phép quay đó là LL, RR, LR và RL. LL sử dụng một phép quay đơn sang phải, RR sử dụng một phép quay đơn sang trái, còn LR và RL là các phép quay kép kết hợp một phép quay trên nút con với một phép quay ngược chiều trên nút gốc.

Việc chèn tuân theo quy tắc cây tìm kiếm nhị phân (BST) tiêu chuẩn, sau đó cây sẽ duyệt ngược lên để cập nhật chiều cao. Nếu bất kỳ tổ tiên nào vi phạm quy tắc cân bằng, một phép xoay đơn hoặc xoay kép sẽ khôi phục lại sự cân bằng. Thông thường chỉ cần tối đa một phép xoay cho mỗi lần chèn.

Cây AVL được cân bằng nghiêm ngặt với hệ số cân bằng tối đa là một, giúp tra cứu nhanh hơn. Cây Đỏ-Đen cho phép cân bằng lỏng lẻo hơn, giúp thao tác chèn và xóa rẻ hơn nhưng tìm kiếm chậm hơn một chút. Cơ sở dữ liệu ưu tiên cây đỏ-đen cho các tác vụ ghi dữ liệu nặng.

Cây AVL hỗ trợ các chỉ mục cơ sở dữ liệu trong bộ nhớ, siêu dữ liệu hệ thống tệp, hàng đợi ưu tiên, tra cứu danh bạ điện thoại, kiểm tra chính tả và bất kỳ khối lượng công việc nào cần tìm kiếm xác định O(log n) cộng với duyệt theo thứ tự cho các truy vấn phạm vi.

Đúng vậy. Các hệ thống AI sử dụng cây AVL cho bảng ký hiệu, kho lưu trữ thuộc tính được sắp xếp, cân bằng cây kd và tra cứu lân cận gần nhất trên dữ liệu có cấu trúc. Chúng cũng là nền tảng của các chỉ mục truy xuất được xếp hạng trong các quy trình tìm kiếm thông minh.

Đúng vậy. GitHub Copilot và các trợ lý AI tương tự hỗ trợ các thao tác chèn, xóa và xoay trong GitHub. C++, Java, hoặc là Pythonvà tạo các bài kiểm tra đơn vị để xác minh tính bất biến của hệ số cân bằng trong mọi thao tác.

Tóm tắt bài viết này với: