Alberi AVL: rotazioni, inserimento, eliminazione con C++ Esempio

⚡ Riepilogo intelligente

Gli alberi AVL sono alberi di ricerca binari autobilancianti in cui la differenza di altezza tra i sottoalberi sinistro e destro di ogni nodo rimane entro -1, 0 o +1, garantendo prestazioni di ricerca O(log n).

  • 🌲 Definizione: Un albero di ricerca binario in cui il fattore di bilanciamento di ogni nodo è compreso tra {-1, 0, +1}, che prende il nome dagli inventori Adelson-Velsky e Landis.
  • Fattore di equilibrio: Calcolato come altezza(sinistra) − altezza(destra); i valori al di fuori di {-1, 0, +1} attivano una rotazione per ripristinare l'equilibrio.
  • 🔄 Rotazioni: Quattro casi — LL, RR, LR e RL — riallineano i nodi dopo inserimenti o cancellazioni sbilanciati per mantenere l'altezza dell'albero logaritmica.
  • Inserimento: Inserimento BST standard seguito da una camminata verso l'alto che ricalcola i fattori di bilanciamento ed esegue al massimo una rotazione singola o doppia.
  • Soppressione: Simile alla cancellazione BST, ma può innescare una cascata di rotazioni multiple lungo l'albero perché l'altezza del sottoalbero può ridursi a ogni antenato.
  • 🚀 applicazioni: I database, gli indici in memoria, i metadati del file system e le strutture di ricerca dell'IA utilizzano gli alberi AVL per ricerche ordinate e veloci.

Alberi AVL

Cosa sono gli alberi AVL?

Alberi AVL Gli alberi di ricerca binaria (BST) sono alberi di ricerca binaria in cui la differenza di altezza tra il sottoalbero sinistro e quello destro di ogni nodo è -1, 0 o +1. Si tratta di BST autobilancianti che mantengono un tempo di ricerca logaritmico e prendono il nome dai loro inventori Adelson-Velsky e Landis (AVL).

Come funziona AVL Tree?

Per capire perché esistono gli alberi AVL, guarda cosa va storto con un semplice Albero di ricerca binarioConsidera questi tasti inseriti nell'ordine indicato:

AVL Lavoro sugli alberi

Visualizzazione dell'albero AVL

L'albero cresce linearmente quando le chiavi arrivano in ordine crescente, degenerando la ricerca in O(n). Questo vanifica lo scopo di un BST: solo un albero bilanciato mantiene la ricerca logaritmica. Ora osserviamo le stesse chiavi inserite in un ordine diverso.

AVL Lavoro sugli alberi

Le stesse chiavi, ma un diverso ordine di inserimento, producono una forma meno profonda, quindi ogni ricerca viene eseguita in O(log n). Gli alberi AVL impongono tale forma monitorando l'altezza ad ogni inserimento e correggendo lo squilibrio senza rompere l'ordinamento dell'albero di ricerca binario.

Fattore di equilibrio negli alberi AVL

Il fattore di bilanciamento (BF) traccalcola l'altezza di ciascun nodo in modo che l'albero possa autobilanciarsi al volo.

Proprietà del fattore di equilibrio

Fattore di equilibrio negli alberi AVL

Albero del fattore di equilibrio AVL

  • Il fattore di bilanciamento è la differenza tra l'altezza del sottoalbero sinistro e l'altezza del sottoalbero destro.
  • Balance factor(node) = height(node->left) − height(node->right)
  • Gli unici valori consentiti sono −1, 0 e +1.
  • Un valore di -1 significa che il sottoalbero destro contiene un livello extra: il nodo è sbilanciato a destra.
  • Un valore di +1 significa che il sottoalbero sinistro contiene un livello extra: il nodo è sbilanciato a sinistra.
  • Un valore pari a 0 significa che entrambi i lati hanno la stessa altezza: il nodo è perfettamente bilanciato.

Rotazioni AVL

Le rotazioni vengono eseguite ogni volta che un inserimento o una cancellazione viola la regola del fattore di bilanciamento. I quattro casi sono LL, RR, LR e RL.

Sinistra – Rotazione sinistra

Questa rotazione viene eseguita quando un nuovo nodo viene inserito nel figlio sinistro del sottoalbero sinistro.

AVL Albero a sinistra – Rotazione a sinistra

AVL Albero a sinistra – Rotazione a sinistra

Viene eseguita una singola rotazione a destra. Questo caso si verifica quando un nodo ha BF +2 e il suo figlio sinistro ha BF +1.

Destra – Rotazione a destra

Questa rotazione viene eseguita quando un nuovo nodo viene inserito nel figlio destro del sottoalbero destro.

AVL Albero a destra – Rotazione a destra

Viene eseguita una singola rotazione a sinistra. Questo caso si verifica quando un nodo ha BF −2 e il suo figlio destro ha BF −1.

Rotazione destra – sinistra

Questa rotazione viene eseguita quando un nuovo nodo viene inserito nel figlio sinistro del sottoalbero destro.

Rotazione albero AVL destra – sinistra

Si attiva quando BF(nodo) = −2 e BF(figlio destro) = +1. Ruota a destra il figlio destro, quindi ruota a sinistra il nodo.

Rotazione sinistra-destra

Questa rotazione viene eseguita quando un nuovo nodo viene inserito nel figlio destro del sottoalbero sinistro.

Rotazione albero AVL da sinistra a destra

Si attiva quando BF(nodo) = +2 e BF(figlio sinistro) = −1. Ruota a sinistra il figlio sinistro, quindi ruota a destra il nodo.

Inserimento negli alberi AVL

L'inserimento è quasi identico a un semplice inserimento in un albero di ricerca binario (BST). Dopo ogni inserimento, l'albero risale e si ribilancia. L'inserimento ha una complessità temporale di O(log n) nel caso peggiore.

Inserimento negli alberi AVL

Implementazione dell'inserimento dell'albero AVL

Passo 1: Inserisci il nodo utilizzando l'algoritmo BST standard. Nell'esempio precedente, inserisci 160.

Passo 2: Aggiorna il fattore di bilanciamento di ogni antenato lungo il percorso di inserimento.

Passo 3: Se un antenato viola l'intervallo del fattore di bilanciamento, esegui la rotazione corrispondente. Nell'esempio, il fattore di bilanciamento del nodo 350 viene violato, quindi una rotazione LL ripristina l'equilibrio.

  1. If BF(node) = +2 and BF(left-child) = +1, eseguire la rotazione LL.
  2. If BF(node) = −2 and BF(right-child) = −1, eseguire la rotazione RR.
  3. If BF(node) = −2 and BF(right-child) = +1, eseguire la rotazione RL.
  4. If BF(node) = +2 and BF(left-child) = −1, eseguire la rotazione LR.

Cancellazione negli alberi AVL

La cancellazione segue la stessa logica di un semplice albero di ricerca binario e il successivo ribilanciamento.

Passo 1: Trova l'elemento nell'albero.

Passo 2: Elimina il nodo utilizzando la procedura standard di eliminazione degli alberi di ricerca binaria (BST).

Passo 3: Sono possibili due casi.

Caso 1: Eliminazione dal sottoalbero destro.

  • 1A. If BF(node) = +2 and BF(left-child) = +1, eseguire la rotazione LL.
  • 1B. If BF(node) = +2 and BF(left-child) = −1, eseguire la rotazione LR.
  • 1C. If BF(node) = +2 and BF(left-child) = 0, eseguire la rotazione LL.

Cancellazione negli alberi AVL

Caso 2: Eliminazione dal sottoalbero sinistro.

  • 2A. If BF(node) = −2 and BF(right-child) = −1, eseguire la rotazione RR.
  • 2B. If BF(node) = −2 and BF(right-child) = +1, eseguire la rotazione RL.
  • 2C. If BF(node) = −2 and BF(right-child) = 0, eseguire la rotazione RR.

Cancellazione negli alberi AVL

C++ Esempio di alberi AVL

Di seguito è riportato un C++ Programma che implementa gli alberi 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);
}

Esempio pratico del codice sopra riportato:

  1. Copia il codice sopra e salvalo in un file chiamato avl.cpp.
  2. Compila il codice:
g++ avl.cpp -o run
  1. Esegui il codice.
./run

C++ Esempio di alberi AVL

Vantaggi degli alberi AVL

  • L'altezza dell'albero AVL è sempre bilanciata e non cresce mai oltre log N.
  • La ricerca è più veloce di un semplice albero di ricerca binario perché l'albero non può degenerare.
  • Il bilanciamento automatico è automatico: non è necessario alcun passaggio di ricostruzione.
  • Le prestazioni deterministiche sono adatte ai sistemi in tempo reale e agli indici in memoria.

DOMANDE FREQUENTI

Un albero AVL è un albero di ricerca binario autobilanciante in cui il fattore di bilanciamento di ogni nodo rimane in {-1, 0, +1}. Le rotazioni ripristinano questo invariante ad ogni inserimento o cancellazione, mantenendoloping ricerca, inserimento ed eliminazione in O(log n).

Il fattore di bilanciamento di un nodo è pari all'altezza del sottoalbero sinistro meno l'altezza del sottoalbero destro. I valori devono essere compresi tra {-1, 0, +1}. Un fattore di bilanciamento di +2 o -2 indica che un'inserzione o una cancellazione ha sbilanciato il nodo e che è necessaria una rotazione.

Le quattro rotazioni sono LL, RR, LR e RL. LL utilizza una singola rotazione a destra, RR utilizza una singola rotazione a sinistra, mentre LR e RL sono doppie rotazioni che combinano una rotazione sul figlio con una rotazione opposta sul nodo.

L'inserimento segue la regola BST standard, dopodiché l'albero risale aggiornando le altezze. Se un antenato viola la regola di equilibrio, una singola o doppia rotazione ripristina l'equilibrio. È necessaria al massimo una rotazione per ogni inserimento.

Gli alberi AVL sono rigorosamente bilanciati con un fattore di bilanciamento massimo pari a uno, il che consente ricerche più veloci. Gli alberi rosso-neri consentono un bilanciamento meno rigido, il che rende le operazioni di inserimento e cancellazione più economiche, ma le ricerche leggermente più lente. I database preferiscono gli alberi rosso-neri per carichi di lavoro con un elevato numero di scritture.

Gli alberi AVL alimentano gli indici dei database in memoria, i metadati del filesystem, le code di priorità, le ricerche nella rubrica telefonica, i correttori ortografici e qualsiasi carico di lavoro che richieda una ricerca deterministica O(log n) e un attraversamento in ordine per le query di intervallo.

Sì. I sistemi di intelligenza artificiale utilizzano gli alberi AVL per le tabelle dei simboli, gli archivi di caratteristiche ordinate, il bilanciamento degli alberi kd e le ricerche del vicino più prossimo su dati strutturati. Sono inoltre alla base degli indici di recupero classificati nelle pipeline di ricerca intelligenti.

Sì. GitHub Copilot e assistenti IA simili generano routine di inserimento, eliminazione e rotazione in C++, Java, o Pythone generare test unitari che verificano l'invarianza del fattore di bilanciamento su ogni operazione.

Riassumi questo post con: