Дерева AVL: обертання, вставка, видалення з C++ Приклад

⚡ Розумний підсумок

AVL-дерева — це самобалансуючі бінарні дерева пошуку, де різниця висоти між лівим і правим піддеревами кожного вузла залишається в межах -1, 0 або +1, що гарантує продуктивність пошуку O(log n).

  • 🌲 Визначення: Бінарне дерево пошуку, в якому коефіцієнт балансу кожного вузла лежить в межах {-1, 0, +1}, назване на честь винахідників Адельсона-Вельського та Лендіса.
  • 🇧🇷 Фактор балансу: Обчислюється як height(left) − height(right); значення поза межами {-1, 0, +1} запускають обертання для відновлення балансу.
  • 🔄 Обертання: Чотири випадки — LL, RR, LR та RL — перевирівнюють вузли після незбалансованих вставок або видалень, щоб зберегти логарифмічну висоту дерева.
  • Вставка: Стандартна вставка BST, а потім висхідний рух, який перераховує коефіцієнти балансу та виконує максимум одне одинарне або подвійне обертання.
  • Видалення: Те саме, що й видалення BST, але може каскадувати кілька обертань угору по дереву, оскільки висота піддерева може зменшуватися у кожного предка.
  • ???? Область застосування: Бази даних, індекси в пам'яті, метадані файлової системи та структури пошуку штучного інтелекту використовують AVL-дерева для швидкого впорядкованого пошуку.

Дерева AVL

Що таке дерева AVL?

Дерева AVL — це бінарні дерева пошуку, в яких різниця висот між лівим і правим піддеревом кожного вузла становить -1, 0 або +1. Вони є самобалансуючими BST, що підтримують логарифмічний час пошуку, названими на честь винахідників Адельсона-Вельського та Лендіса (AVL).

Як працює AVL Tree?

Щоб зрозуміти, чому існують AVL-дерева, розглянемо, що не так з рівниною Бінарне дерево пошукуРозглянемо ці ключі, вставлені у вказаному порядку:

Робота з деревом AVL

Візуалізація дерева AVL

Дерево зростає лінійно, коли ключі надходять у порядку зростання, що зводить пошук до O(n). Це суперечить меті BST — лише збалансоване дерево зберігає логарифмічний пошук. Тепер розглянемо ті ж ключі, вставлені в іншому порядку.

Робота з деревом AVL

Ті самі ключі, але з різним порядком вставки, створюють менш поверхневу форму, тому кожен пошук виконується за O(log n). AVL-дерева забезпечують дотримання цієї форми, спостерігаючи за висотою при кожній вставці та виправляючи дисбаланс без порушення BST-порядку.

Фактор балансу в деревах AVL

Коефіцієнт балансу (BF) tracks висота кожного вузла, щоб дерево могло самобалансуватися на льоту.

Властивості фактора балансу

Фактор балансу в деревах AVL

Коефіцієнт балансу дерева AVL

  • Коефіцієнт балансу – це різниця між висотою лівого піддерева та висотою правого піддерева.
  • Balance factor(node) = height(node->left) − height(node->right)
  • Єдині дозволені значення - −1, 0 та +1.
  • Значення -1 означає, що праве піддерево містить один додатковий рівень — вузол має переважну праву частину.
  • Значення +1 означає, що ліве піддерево містить один додатковий рівень — вузол має переважну кількість лівих вузлів.
  • Значення 0 означає, що обидві сторони мають однакову висоту — вузол ідеально збалансований.

Обертання AVL

Ротації виконуються щоразу, коли вставка або видалення порушує правило коефіцієнта балансу. Чотири випадки: LL, RR, LR та RL.

Вліво – обертання вліво

Це обертання виконується, коли новий вузол вставляється в лівий дочірній елемент лівого піддерева.

Дерево AVL вліво – обертання вліво

Дерево AVL вліво – обертання вліво

Виконується одне обертання праворуч. Цей випадок спрацьовує, коли вузол має BF +2, а його лівий дочірній вузол має BF +1.

Вправо – Обертання вправо

Це обертання виконується, коли новий вузол вставляється в праву дочірню частину правого піддерева.

AVL Tree Right – праворуч

Виконується одне обертання ліворуч. Цей випадок спрацьовує, коли вузол має BF −2, а його правий дочірній вузол має BF −1.

Обертання вправо – вліво

Це обертання виконується, коли новий вузол вставляється в лівий дочірній елемент правого піддерева.

Дерево AVL Обертання вправо – вліво

Спрацьовує, коли BF(вузол) = −2 та BF(правий-дочірній вузол) = +1. Поверніть праворуч правий дочірній вузол, потім поверніть ліворуч вузол.

Обертання вліво – вправо

Це обертання виконується, коли новий вузол вставляється в праву дочірню частину лівого піддерева.

Дерево AVL Обертання вліво – вправо

Спрацьовує, коли BF(вузол) = +2 та BF(лівий-дочірній вузол) = −1. Повернути лівий дочірній вузол ліворуч, потім повернути вузол праворуч.

Вставка в дерева AVL

Вставка майже ідентична звичайній вставці BST. Після кожної вставки дерево піднімається вгору та перебалансовується. Вставка виконується за час O(log n) у найгіршому випадку.

Вставка в дерева AVL

Реалізація вставки дерева AVL

Крок 1: Вставте вузол, використовуючи стандартний алгоритм BST. У наведеному вище прикладі вставте 160.

Крок 2: Оновити коефіцієнт балансу кожного предка вздовж шляху вставки.

Крок 3: Якщо будь-який предок порушує діапазон коефіцієнта балансу, виконайте відповідну ротацію. У прикладі коефіцієнт балансу вузла 350 порушено, тому ротація LL відновлює баланс.

  1. If BF(node) = +2 та BF(left-child) = +1, виконайте обертання LL.
  2. If BF(node) = −2 та BF(right-child) = −1, виконайте обертання RR.
  3. If BF(node) = −2 та BF(right-child) = +1, виконайте обертання RL.
  4. If BF(node) = +2 та BF(left-child) = −1, виконайте обертання LR.

Видалення в деревах AVL

Видалення відбувається за тією ж логікою, що й звичайний BST, а потім відбувається повторне балансування.

Крок 1: Знайдіть елемент у дереві.

Крок 2: Видаліть вузол за допомогою стандартного видалення BST.

Крок 3: Можливі два випадки.

Справа 1: Видалення з правого піддерева.

  • 1A. If BF(node) = +2 та BF(left-child) = +1, виконайте обертання LL.
  • 1B. If BF(node) = +2 та BF(left-child) = −1, виконайте обертання LR.
  • 1C. If BF(node) = +2 та BF(left-child) = 0, виконайте обертання LL.

Видалення в деревах AVL

Справа 2: Видалення з лівого піддерева.

  • 2A. If BF(node) = −2 та BF(right-child) = −1, виконайте обертання RR.
  • 2B. If BF(node) = −2 та BF(right-child) = +1, виконайте обертання RL.
  • 2C. If BF(node) = −2 та BF(right-child) = 0, виконайте обертання RR.

Видалення в деревах AVL

C++ Приклад дерев AVL

Нижче C++ програма, що реалізує 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);
}

Приклад виконання наведеного вище коду:

  1. Скопіюйте наведений вище код та збережіть його у файлі з назвою avl.cpp.
  2. Скомпілюйте код:
g++ avl.cpp -o run
  1. Запустіть код.
./run

C++ Приклад дерев AVL

Переваги дерев AVL

  • Висота AVL-дерева завжди збалансована і ніколи не перевищує log N.
  • Пошук швидший, ніж звичайне бінарне дерево пошуку, оскільки дерево не може вироджуватися.
  • Самобалансування відбувається автоматично — жодного етапу перебудови не потрібно.
  • Детермінована продуктивність підходить для систем реального часу та індексів у пам'яті.

Поширені запитання

AVL-дерево — це самобалансуюче бінарне дерево пошуку, де коефіцієнт балансу кожного вузла залишається в межах {-1, 0, +1}. Ротації відновлюють цей інваріант при кожній вставці або видаленні, зберігаючи...ping пошук, вставка та видалення виконуються за O(log n).

Коефіцієнт балансу вузла дорівнює висоті (ліве піддерево) мінус висота (праве піддерево). Значення повинні лежати в діапазоні {-1, 0, +1}. Коефіцієнт балансу +2 або -2 сигналізує про те, що вставка або видалення порушило баланс цього вузла, і потрібна ротація.

Чотири обертання - це LL, RR, LR та RL. LL використовує одне обертання праворуч, RR - одне обертання ліворуч, а LR та RL - це подвійні обертання, які поєднують одне обертання на дочірньому вузлі з протилежним обертанням на вузлі.

Вставка виконується за стандартним правилом BST, після чого дерево повертається вгору, оновлюючи висоту. Якщо будь-який предок порушує правило балансу, один або два оберти відновлюють баланс. На одну вставку потрібен максимум один оберт.

Дерева AVL суворо збалансовані з коефіцієнтом балансу не більше одиниці, що забезпечує швидший пошук. Червоно-чорні дерева дозволяють слабший баланс, що робить вставку та видалення дешевшими, але пошук трохи повільнішим. Бази даних надають перевагу червоно-чорним деревам для навантажень з великим обсягом запису.

AVL-дерева забезпечують роботу з індексами баз даних в оперативній пам'яті, метаданими файлової системи, чергами пріоритетів, пошуком у телефонній книзі, перевіркою орфографії та будь-яким робочим навантаженням, яке потребує детермінованого пошуку O(log n) плюс обхід у порядку для запитів діапазону.

Так. Системи штучного інтелекту використовують AVL-дерева для таблиць символів, упорядкованих сховищ ознак, балансування kd-дерев та пошуку найближчих сусідів у структурованих даних. Вони також лежать в основі ранжованих індексів пошуку в інтелектуальних конвеєрах пошуку.

Так. GitHub Copilot та подібні помічники зі штучним інтелектом створюють процедури вставки, видалення та обертання в C++, Javaабо Python, та генерувати модульні тести, які перевіряють інваріантність коефіцієнта балансу для кожної операції.

Підсумуйте цей пост за допомогою: