STRUTTURA DATI: ALBERO BINARIO

L’Albero Binario è una struttura dati gerarchica formata da nodi, dove ciascun nodo può avere al massimo due figli:
➡ un figlio sinistro e un figlio destro.

È ideale per rappresentare dati con relazioni gerarchiche e per implementare altre strutture complesse (es. BST, Heap, Alberi Bilanciati).

TERMINOLOGIA

  • Nodo → elemento dell’albero contenente un valore e puntatori ai figli.
  • Radice (Root) → il nodo principale, punto di partenza di tutte le operazioni.
  • Foglia (Leaf) → nodo senza figli.
  • Padre (Parent) → nodo che ha almeno un figlio.
  • Figlio (Child) → nodo collegato a un padre.
  • Fratelli (Siblings) → nodi che condividono lo stesso padre.
  • Sottoalbero (Subtree) → albero derivato da un nodo specifico.
  • Albero Vuoto → struttura senza nodi (radice nulla).
  • Profondità (Depth) → distanza di un nodo dalla radice.
  • Altezza di un Nodo → distanza fino alla foglia più lontana.
  • Livello (Level) → insieme dei nodi alla stessa profondità.
  • Altezza dell’Albero → percorso più lungo radice → foglia.
  • Dimensione (Size) → numero totale di nodi.

OPERAZIONI DI VISITA

  • Pre-Ordine → Nodo → Sottoalbero Sinistro → Sottoalbero Destro.
  • In-Ordine → Sottoalbero Sinistro → Nodo → Sottoalbero Destro.
  • Post-Ordine → Sottoalbero Sinistro → Sottoalbero Destro → Nodo.
  • Per Livelli (Level-Order) → attraversamento livello per livello (con una coda).

OPERAZIONI PRINCIPALI

  • inserisci(valore) → aggiunge un nuovo nodo nell’albero.
  • cerca(valore) → verifica se un valore è presente.
  • elimina(valore) → rimuove un nodo con valore specifico.
  • visitaInOrdine() → restituisce i valori in ordine simmetrico.
  • visitaPreOrdine() → attraversa prima il nodo, poi i sottoalberi.
  • visitaPostOrdine() → attraversa prima i sottoalberi, poi il nodo.
  • altezza() → calcola la profondità massima.
  • contaNodi() → restituisce il numero totale di nodi.

PRESTAZIONI

  • Alberi bilanciati:
    • Inserimento → O(log n)
    • Ricerca → O(log n)
    • Eliminazione → O(log n)
  • Alberi sbilanciati:
    • Tutte le operazioni possono degradare a O(n).

➡ L’efficienza dipende dal bilanciamento dell’albero.

NOTE

  • È la base per strutture come Binary Search Tree (BST), AVL, Heap.
  • Per evitare perdita di riferimenti, conviene usare copie dei nodi nelle funzioni ricorsive → si preserva un backup al nodo originale.
  • Le modifiche si propagano grazie agli effetti collaterali, mantenendo la coerenza della struttura.

CODICES

// Classe Nodo
class Nodo<E> {
    valore: E;
    sinistro: Nodo<E> | null;
    destro: Nodo<E> | null;

    /**
     * 
     * @param valore 
     */
    constructor(valore: E) {
        this.valore = valore; // Valore del nodo
        this.sinistro = null; // Puntatore al figlio sinistro
        this.destro = null;   // Puntatore al figlio destro
    }
}

// Classe Albero
class AlberoBinario<E> {
    radice: Nodo<E> | null;

    constructor() {
        this.radice = null; // Puntatore al nodo radice
    }

    /**
     * Inserimento di un valore nell'albero
     * @param valore > valore del nodo da inserire
     */
    inserisci(valore: E): void {
        const nuovoNodo = new Nodo(valore);
        if (!this.radice) {
            this.radice = nuovoNodo;
        } else {
            this.inserisciNodo(this.radice, nuovoNodo);
        }
    }

    // inserisciNodo(nodoCorrente, nuovoNodo) --> 
    /**
     * Funzione ausiliaria per l'inserimento del nodo
     * @param nodoCorrente 
     * @param nuovoNodo 
     */
    inserisciNodo(nodoCorrente: Nodo<E>, nuovoNodo: Nodo<E>): void {
        if (nuovoNodo.valore < nodoCorrente.valore) {
            if (!nodoCorrente.sinistro) {
                nodoCorrente.sinistro = nuovoNodo;
            } else {
                this.inserisciNodo(nodoCorrente.sinistro, nuovoNodo);
            }
        } else {
            if (!nodoCorrente.destro) {
                nodoCorrente.destro = nuovoNodo;
            } else {
                this.inserisciNodo(nodoCorrente.destro, nuovoNodo);
            }
        }
    }

    /**
     * Ricerca di un valore nell'albero
     * @param valore 
     * @returns true se è stato trovato, false altrimenti
     */
    cerca(valore: E): boolean | null {
        return this.cercaNodo(this.radice, valore);
    }

    /**
     * Funzione ausiliaria per cercare un nodo all'interno di un albero
     * @param nodoCorrente 
     * @param valore 
     * @returns true se è stato trovato, false altrimenti
     */
    private cercaNodo(nodoCorrente: Nodo<E> | null, valore: E): boolean {
        if (!nodoCorrente) return false;
        if (nodoCorrente.valore === valore) return true;
        return valore < nodoCorrente.valore
            ? this.cercaNodo(nodoCorrente.sinistro, valore)
            : this.cercaNodo(nodoCorrente.destro, valore);
    }

    /**
     * Visita simmetrica(sx - r - dx)
     * @returns array con gli elmenti in ordine in base alla visita effettuata
     */
    visitaInOrdine(): E[] {
        if (this.radice) {
            const visita: E[] = [];
            this.visitaInOrdineNodo(this.radice, visita);
            return visita;
        } else {
            return [];
        }

    }

    /**
     * Funzione ausiliaria per visita simmetrica
     * @param nodoCorrente 
     * @param visita 
     */
    private visitaInOrdineNodo(nodoCorrente: Nodo<E>, visita: E[]): void {
        if (nodoCorrente && nodoCorrente.sinistro) {
            this.visitaInOrdineNodo(nodoCorrente.sinistro, visita);
        }
        if (nodoCorrente) {
            visita.push(nodoCorrente.valore);
        }
        if (nodoCorrente && nodoCorrente.destro) {
            this.visitaInOrdineNodo(nodoCorrente.destro, visita);
        }
    }

    /**
     * Visita in pre ordine(radice - sx - dx)
     * @returns array con gli elmenti in ordine in base alla visita effettuata
     */
    visitaPreOrdine(): E[] {
        if (this.radice) {
            const visita: E[] = [];
            this.visitaPreOrdineNodo(this.radice, visita);
            return visita;
        } else {
            return [];
        }
    }

    /**
     * Funzione ausiliaria per visita in pre ordine
     * @param nodoCorrente 
     * @param visita 
     */
    private visitaPreOrdineNodo(nodoCorrente: Nodo<E>, visita: E[]) {
        if (nodoCorrente) {
            visita.push(nodoCorrente.valore);
        }
        if (nodoCorrente && nodoCorrente.sinistro) {
            this.visitaPreOrdineNodo(nodoCorrente.sinistro, visita);
        }
        if (nodoCorrente && nodoCorrente.destro) {
            this.visitaPreOrdineNodo(nodoCorrente.destro, visita);
        }
    }

    /**
     * Visita in post ordine(sx - dx - radice)
     * @returns array con gli elmenti in ordine in base alla visita effettuata
     */
    visitaPostOrdine() {
        if (this.radice) {
            const visita: E[] = [];
            this.visitaPostOrdineNodo(this.radice, visita);
            return visita;
        } else {
            return [];
        }
    }

    /**
     * Funzione ausiliaria per visita in post ordine
     * @param nodoCorrente 
     * @param visita 
     */
    private visitaPostOrdineNodo(nodoCorrente: Nodo<E>, visita: E[]) {
        if (nodoCorrente && nodoCorrente.sinistro) {
            this.visitaPostOrdineNodo(nodoCorrente.sinistro, visita);
        }
        if (nodoCorrente && nodoCorrente.destro) {
            this.visitaPostOrdineNodo(nodoCorrente.destro, visita);
        }
        if (nodoCorrente) {
            visita.push(nodoCorrente.valore);
        }
    }

    /**
     * Eliminazione del primo nodo in base al suo valore
     * @param valore 
     */
    elimina(valore: E): void {
        if (this.radice) {
            this.radice = this.eliminaNodo(this.radice, valore);
        }
    }

    /**
     * Funzione ausiliaria per cancellare uno specifico nodo dall'albero
     * @param nodoCorrente 
     * @param valore 
     * @returns nuova radice
     */
    private eliminaNodo(nodoCorrente: Nodo<E> | null, valore: E): Nodo<E> | null {
        if (!nodoCorrente) return null;

        // 1) Pulisci prima i figli (post-order: sinistro, destro, poi corrente)
        nodoCorrente.sinistro = this.eliminaNodo(nodoCorrente.sinistro, valore);
        nodoCorrente.destro = this.eliminaNodo(nodoCorrente.destro, valore);

        // 2) Se il nodo corrente ha il valore da eliminare, ricollega i sottoalberi
        if (nodoCorrente.valore === valore) {
            // caso 0 figli
            if (!nodoCorrente.sinistro && !nodoCorrente.destro) return null;

            // caso 1 figlio
            if (!nodoCorrente.sinistro) return nodoCorrente.destro;
            if (!nodoCorrente.destro) return nodoCorrente.sinistro;

            // caso 2 figli (strategia: attacca il sottoalbero destro
            // al nodo più a destra del sottoalbero sinistro e ritorna il sinistro)
            let rightmost = nodoCorrente.sinistro;
            while (rightmost.destro) rightmost = rightmost.destro;
            rightmost.destro = nodoCorrente.destro;
            return nodoCorrente.sinistro;
        }

        // 3) Nessuna eliminazione qui, restituisci il nodo (eventualmente con figli aggiornati)
        return nodoCorrente;
    }

    /**
     * Trovare il nodo con il valore minimo nell'albero
     * @param nodo 
     * @returns nodo minimo, null se l'albero è vuoto
     */
    private trovaMinimo(nodo: Nodo<E> | null): E | null {
        if (!nodo) return null;

        // Valore corrente
        let min: E = nodo.valore;

        // Ricerca nel sottoalbero sinistro
        const minSinistro = this.trovaMinimo(nodo.sinistro);
        if (minSinistro !== null && minSinistro && minSinistro < min) {
            min = minSinistro;
        }

        // Ricerca nel sottoalbero destro
        const minDestro = this.trovaMinimo(nodo.destro);
        if (minDestro !== null && minDestro && minDestro < min) {
            min = minDestro;
        }

        return min;
    }

    /**
     * calcola l'altezza dell'albero
     * @returns 
     */
    altezza(): number {
        if (this.radice) {
            return this.calcolaAltezza(this.radice);
        } else {
            return 0;
        }
    }

    /**
     * Funzione ausiliaria per calcolare l'altezza dell'albero
     * @param nodoCorrente 
     * @returns altezza dell'albero
     */
    private calcolaAltezza(nodoCorrente: Nodo<E>): number {
        if (!nodoCorrente) return 0;
        const altezzaSinistro = nodoCorrente.sinistro ? this.calcolaAltezza(nodoCorrente.sinistro) : 0;
        const altezzaDestro = nodoCorrente.destro ? this.calcolaAltezza(nodoCorrente.destro) : 0;
        return Math.max(altezzaSinistro, altezzaDestro) + 1; // Aggiungi 1 per il nodo corrente
    }

    /**
     * Calcola il numero di nodi di cui è composto l'albero
     * @returns numero di nodi 
     */
    contaNodi(): number {
        if (this.radice) {
            return this.contaNodiRicorsivo(this.radice);
        } else {
            return 0;
        }
    }

    /**
     * Funzione ausiliaria per contare il numero di nodi di cui è composto l'albero
     * @param nodoCorrente 
     * @returns numero di nodi dell'albero
     */
    private contaNodiRicorsivo(nodoCorrente: Nodo<E>): number {
        if (!nodoCorrente) return 0;
        const contaSinistro = nodoCorrente.sinistro ? this.contaNodiRicorsivo(nodoCorrente.sinistro) : 0;
        const contaDestro = nodoCorrente.destro ? this.contaNodiRicorsivo(nodoCorrente.destro) : 0;
        return contaSinistro + contaDestro + 1; // Conta il nodo corrente
    }
}

// test della struttura dati
const albero = new AlberoBinario();

console.log("Inserimento di valori nell'albero...");
albero.inserisci(50);
albero.inserisci(30);
albero.inserisci(70);
albero.inserisci(20);
albero.inserisci(40);
albero.inserisci(60);
albero.inserisci(80);

console.log("Visita in ordine (In-Order Traversal):", albero.visitaInOrdine());

console.log("Ricerca di valori:");
console.log("Cerca 40:", albero.cerca(40));
console.log("Cerca 100:", albero.cerca(100));

console.log("Eliminazione di un nodo (30)");
albero.elimina(30);
console.log("Visita in ordine dopo eliminazione:", albero.visitaInOrdine());

console.log("Visita Pre-Ordine:", albero.visitaPreOrdine());
console.log("Visita Post-Ordine:", albero.visitaPostOrdine());
console.log("Altezza dell'albero:", albero.altezza());
console.log("Numero totale di nodi:", albero.contaNodi());