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. Те ​​са самобалансиращи се BST, които поддържат логаритмично време за търсене, кръстени на изобретателите Аделсън-Велски и Ландис (AVL).

Как работи AVL Tree?

За да разберете защо съществуват AVL дървета, вижте какво се обърква с обикновена Двоично дърво за търсенеДа разгледаме тези ключове, поставени в дадения ред:

AVL Tree работа

AVL дървовидна визуализация

Дървото расте линейно, когато ключовете пристигат във възходящ ред, дегенерирайки търсенето до O(n). Това обезсмисля целта на BST - само балансирано дърво поддържа търсенето логаритмично. Сега разгледайте същите ключове, вмъкнати в различен ред.

AVL Tree работа

Същите ключове, различен ред на вмъкване, водят до по-плитка форма, така че всяко търсене се изпълнява за 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 Tree Left – Ляво завъртане

AVL Tree Left – Ляво завъртане

Извършва се еднократно завъртане надясно. Този случай се задейства, когато възелът има BF +2, а лявото му дете има BF +1.

Надясно – завъртане надясно

Това завъртане се извършва, когато се вмъкне нов възел в дясното дете на дясното поддърво.

AVL Tree Right – дясно завъртане

Извършва се еднократно завъртане наляво. Този случай се задейства, когато възелът има BF −2, а десният му наследник има BF −1.

Завъртане надясно – наляво

Това завъртане се извършва, когато се вмъкне нов възел в лявото дете на дясното поддърво.

Дърво на AVL дясно – ляво завъртане

Задейства се, когато BF(възел) = −2 и BF(десен-дете) = +1. Завъртете надясно десния дете, след това завъртете наляво възела.

Завъртане наляво – надясно

Това завъртане се извършва, когато се вмъкне нов възел в дясното дете на лявото поддърво.

AVL Tree Left – Right Rotation

Задейства се, когато 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 дървета

По-долу е показан a 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 и подобни AI асистенти поддържат процедури за вмъкване, изтриване и ротация в C++, Java или Pythonи генериране на единични тестове, които проверяват инвариантността на коефициента на баланс при всяка операция.

Обобщете тази публикация с: