Деревья AVL: ротация, вставка, удаление с помощью C++ Пример

⚡ Умное резюме

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

  • ???? Определение: Бинарное дерево поиска, в котором коэффициент баланса каждого узла находится в диапазоне {-1, 0, +1}, названное в честь изобретателей Адельсона-Вельски и Ландиса.
  • Коэффициент баланса: Вычисляется как высота(слева) − высота(справа); значения вне диапазона {-1, 0, +1} запускают вращение для восстановления равновесия.
  • 🔄 Повороты: В четырех случаях — LL, RR, LR и RL — узлы перестраиваются после несбалансированных вставок или удалений, чтобы сохранить логарифмическую высоту дерева.
  • Вставка: Стандартная процедура BST с последующим подъемом вверх, в ходе которой пересчитываются факторы равновесия и выполняется максимум один одинарный или двойной оборот.
  • Удаление: Аналогично удалению BST, но может вызвать каскадное повторение нескольких операций вверх по дереву, поскольку высота поддерева может уменьшаться у каждого предка.
  • 🚀 Области применения: Базы данных, индексы в оперативной памяти, метаданные файловых систем и структуры поиска в системах искусственного интеллекта используют AVL-деревья для быстрого упорядоченного поиска.

AVL Деревья

Что такое деревья AVL?

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

Как работает дерево AVL?

Чтобы понять, зачем существуют AVL-деревья, давайте посмотрим, что не так с обычным деревом. Двоичное дерево поискаРассмотрим следующие клавиши, вставленные в указанном порядке:

АВЛ Работа с деревом

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

Дерево растет линейно, когда ключи поступают в порядке возрастания, что приводит к вырождению поиска до O(n). Это противоречит цели бинарного дерева поиска — только сбалансированное дерево поддерживает логарифмический поиск. Теперь рассмотрим те же ключи, вставленные в другом порядке.

АВЛ Работа с деревом

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

Коэффициент баланса в деревьях AVL

Коэффициент баланса (BF) tracзадает высоту каждого узла, чтобы дерево могло автоматически балансироваться в режиме реального времени.

Свойства фактора баланса

Коэффициент баланса в деревьях AVL

Коэффициент баланса AVL-дерева

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

Ротации АВЛ

Вращение выполняется всякий раз, когда вставка или удаление нарушает правило балансового коэффициента. Четыре случая: LL, RR, LR и RL.

Влево – вращение влево

Этот поворот выполняется, когда новый узел вставляется в левый дочерний элемент левого поддерева.

Дерево AVL слева – вращение влево

Дерево AVL слева – вращение влево

Выполняется однократное вращение вправо. Этот случай срабатывает, когда у узла значение BF + 2, а у его левого дочернего узла — BF + 1.

Вправо – правое вращение

Этот поворот выполняется, когда новый узел вставляется в правый дочерний элемент правого поддерева.

Дерево AVL вправо – вращение вправо

Выполняется однократное вращение влево. Этот случай срабатывает, когда у узла значение BF −2, а у его правого дочернего узла — BF −1.

Право-левое вращение

Этот поворот выполняется, когда новый узел вставляется в левый дочерний элемент правого поддерева.

Дерево AVL, вращение вправо – влево

Срабатывает, когда BF(узел) = −2 и BF(правый дочерний узел) = +1. Поворачивает правый дочерний узел вправо, затем поворачивает узел влево.

Вращение влево-вправо

Этот поворот выполняется, когда новый узел вставляется в правый дочерний элемент левого поддерева.

Дерево AVL, вращение влево-вправо

Срабатывает, когда BF(узел) = +2 и BF(левый дочерний узел) = −1. Поворачивает левый дочерний узел влево, затем узел вправо.

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

Процесс вставки практически идентичен вставке в обычное дерево поиска. После каждой вставки дерево поднимается вверх и выполняет перебалансировку. Вставка выполняется за время 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, выполнить вращение влево-вправо.

Удаление в деревьях 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, выполнить вращение влево-вправо.
  • 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.
  • Поиск выполняется быстрее, чем поиск по простому бинарному дереву поиска, потому что дерево не может вырождаться.
  • Самобалансировка происходит автоматически — перенастройка не требуется.
  • Детерминированная производительность подходит для систем реального времени и индексов, хранящихся в оперативной памяти.

Часто задаваемые вопросы (FAQ)

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и сгенерировать модульные тесты, которые проверяют, что коэффициент баланса инвариантен для каждой операции.

Подведем итог этой публикации следующим образом: