Salta al contenuto
Note per Studenti Alberi e heap in C

Alberi e heap in C

In questa pagina 3

Realizzazione in C degli ADT visti in Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →, Alberi binari di ricercaAlbero binario di ricerca come albero binario proprio con entry nei nodi interni e foglie vuote; inorder crescente; TreeSearch; get, put e remove (due casi, con predecessore inorder) in Theta(h); altezza fino a n-1; esempi di inserimenti; alberi aumentati con campi size e max e algoritmi di conteggio e interrogazione in O(h).Alberi binari di ricerca → e HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →. Premessa: Puntatori, struct e memoria dinamica in CPuntatori e passaggio per riferimento in C, array e aritmetica dei puntatori, stringhe, struct e typedef con l'operatore ->, malloc e free, puntatore a puntatore per modificare una testa; compilazione con Makefile; errori tipici (puntatori pendenti, perdite di memoria, off-by-one).Puntatori, struct e memoria dinamica in C → e Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C →; per le basi del C si vedano Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →, Strutture (struct) in Cstruct per raggruppare campi di tipo diverso, typedef, accesso con punto e freccia, struct come parametri e valori di ritorno, array di struct, struct allocate dinamicamente e struct autoreferenziali.Strutture (struct) in C → e Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →, e per la ricorsione RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione → e Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →.

Albero binario con nodi collegati

c
typedef struct nodo {
    int val;
    struct nodo *left;
    struct nodo *right;
} Nodo;

Nodo *nuovoNodo(int v) {
    Nodo *n = malloc(sizeof(Nodo));
    n->val = v;
    n->left = n->right = NULL;
    return n;
}

Definizione (albero binario in C come puntatore alla radice). L'albero è il puntatore alla radice (NULL = albero vuoto). Gli algoritmi ricorsivi seguono lo schema delle visite: caso base r == NULL, poi i due sottoalberi.

Esempio. Con Nodo *r = nuovoNodo(5); r->left = nuovoNodo(3); si ha contaNodi(r) == 2, altezza(r) == 1 e inorder(r) stampa 3 5.

Traccia di contaNodi(r) sull'albero con radice 55 e figlio sinistro 33: contaNodi(5) =1+=1+contaNodi(3)++contaNodi(NULL); contaNodi(3) =1+0+0=1=1+0+0=1 (i suoi figli sono NULL, caso base); quindi contaNodi(5) =1+1+0=2=1+1+0=2. Per altezza: foglie 00, albero vuoto −1-1 (così una foglia vale 1+max⁡(−1,−1)=01+\max(-1,-1)=0, coerente con la definizione di altezza in AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →); altezza(5) =1+max⁡(altezza(3),altezza(NULL))=1+max⁡(0,−1)=1=1+\max(\text{altezza}(3),\text{altezza}(\text{NULL}))=1+\max(0,-1)=1. La profondità della ricorsione, e quindi lo stack (Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →, Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM →), è pari all'altezza dell'albero più uno.

c
int contaNodi(const Nodo *r) {
    return r == NULL ? 0 : 1 + contaNodi(r->left) + contaNodi(r->right);
}

int altezza(const Nodo *r) {              /* albero vuoto: -1, foglia: 0 */
    if (r == NULL) return -1;
    int hl = altezza(r->left), hr = altezza(r->right);
    return 1 + (hl > hr ? hl : hr);
}

void inorder(const Nodo *r) {
    if (r == NULL) return;
    inorder(r->left);
    printf("%d ", r->val);
    inorder(r->right);
}

void liberaAlbero(Nodo *r) {              /* postorder: figli prima, poi il nodo */
    if (r == NULL) return;
    liberaAlbero(r->left);
    liberaAlbero(r->right);
    free(r);
}

Tutte visitano ogni nodo una volta: Θ(n)\Theta(n). liberaAlbero deve essere un postorder: liberando la radice per prima si perdono i puntatori ai figli (dopo free(r) leggere r->left sarebbe un uso di memoria rilasciata, un puntatore pendente). Sull'albero 5,35,3 l'ordine delle free è prima il nodo 33, poi il nodo 55.

Definizione (inserimento in un albero binario di ricerca con doppio puntatore). La radice può cambiare (primo inserimento), quindi si passa Nodo **:

void addBST(Nodo **r, int v) {
    if (*r == NULL)
        *r = nuovoNodo(v);
    else if (v < (*r)->val)
        addBST(&(*r)->left, v);
    else if (v > (*r)->val)
        addBST(&(*r)->right, v);          /* chiave uguale: ignorata */
}

Esempio. Con addBST(&r, 30); addBST(&r, 40); addBST(&r, 24); la visita inorder stampa 24 30 40. Passo per passo: r vale NULL, quindi addBST(&r, 30) crea la radice 3030 (e la scrive in r: per questo serve Nodo **, come nelle liste, Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C →). Per 4040: *r non è NULL, 40>3040>30, si chiama addBST(&(*r)->right, 40), dove *r ora è il campo right della radice, ancora NULL: si crea lì il nodo. Per 2424: 24<3024<30, si scende a sinistra e si crea il nodo nel campo left. È la ricerca di TreeSearch (Alberi binari di ricercaAlbero binario di ricerca come albero binario proprio con entry nei nodi interni e foglie vuote; inorder crescente; TreeSearch; get, put e remove (due casi, con predecessore inorder) in Theta(h); altezza fino a n-1; esempi di inserimenti; alberi aumentati con campi size e max e algoritmi di conteggio e interrogazione in O(h).Alberi binari di ricerca →) con l'espansione della foglia, dove la "foglia" è un campo NULL.

Costo Θ(h)\Theta(h), con hh altezza dell'albero. Inserendo 30,40,24,58,48,26,11,1330, 40, 24, 58, 48, 26, 11, 13 la visita inorder stampa 11 13 24 26 30 40 48 58 (ordine crescente) e l'altezza è 33 (verificato eseguendo il codice).

Per uno struct con più campi (ad esempio una entry chiave-valore) si definisce una struct Entry e il nodo contiene un campo Entry data; il confronto avviene sulla chiave. Per un campo aggiuntivo che dipende dai sottoalberi (es. il massimo dei valori del sottoalbero, vedi Alberi binari di ricercaAlbero binario di ricerca come albero binario proprio con entry nei nodi interni e foglie vuote; inorder crescente; TreeSearch; get, put e remove (due casi, con predecessore inorder) in Theta(h); altezza fino a n-1; esempi di inserimenti; alberi aumentati con campi size e max e algoritmi di conteggio e interrogazione in O(h).Alberi binari di ricerca →) lo si aggiorna risalendo dopo l'inserimento ricorsivo.

Heap su array

Definizione (min-heap su array con indici da 1). Un min-heap (HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →) è un array h con indici da 1 (h[0] non si usa): figli di i in 2i e 2i+1, padre in i/2. last è il numero di entry.

Esempio. Con last = 7, i figli di i = 3 sono h[6] e h[7], e il padre di h[7] è h[3]. In C la divisione tra interi positivi tronca, quindi 7 / 2 == 3 coincide con ⌊7/2⌋\lfloor 7/2\rfloor (Tipi, operatori e controllo del flusso in CTipi interi e reali del C con dimensioni e limiti, conversioni implicite e cast, divisione intera, operatori di incremento, logici e bit a bit; if, switch, while, do-while, for, break e continue.Tipi, operatori e controllo del flusso in C →); per questo h[i / 2] è il padre senza chiamare altre funzioni. Perché gli indici partono da 11 si veda HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap → (con indici da 00 le formule si complicano).

c
void insert(int h[], int *last, int v) {
    int i = ++(*last);
    h[i] = v;
    while (i > 1 && h[i / 2] > h[i]) {        /* up-heap bubbling */
        int t = h[i]; h[i] = h[i / 2]; h[i / 2] = t;
        i /= 2;
    }
}

int indexMinChild(const int h[], int last, int i) {   /* richiede 2*i <= last */
    int res = 2 * i;
    if (res + 1 <= last && h[res + 1] < h[res]) res++;
    return res;
}

void downHeap(int h[], int last, int i) {
    while (2 * i <= last) {
        int j = indexMinChild(h, last, i);
        if (h[i] <= h[j]) break;
        int t = h[i]; h[i] = h[j]; h[j] = t;
        i = j;
    }
}

int removeMin(int h[], int *last) {
    int m = h[1];
    h[1] = h[(*last)--];                      /* l'ultima entry va in radice */
    downHeap(h, *last, 1);
    return m;
}

void bottomUp(int h[], int last) {            /* costruzione in O(n) */
    for (int k = last / 2; k >= 1; k--)
        downHeap(h, last, k);
}

Proprietà (costi di insert, removeMin e bottomUp in C). insert e removeMin costano Θ(log⁡n)\Theta(\log n), bottomUp Θ(n)\Theta(n) (vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →). Prova: da 11 9 7 13 3 4, bottomUp produce 3 9 4 13 11 7; dopo insert(2) i due removeMin restituiscono 22 e 33, e resta 4 9 7 13 11.

Esempio. Da 11 9 7 13 3 4, bottomUp produce 3 9 4 13 11 7; dopo insert(2), due removeMin restituiscono 2 e 3 e resta 4 9 7 13 11.

Traccia, con last = 6 e h = [3, 9, 4, 13, 11, 7] (indici da 11):

operazione array dopo passi
insert(2) [3, 9, 4, 13, 11, 7, 2] poi [3, 9, 2, 13, 11, 7, 4] poi [2, 9, 3, 13, 11, 7, 4] last diventa 77; 22 in posizione 77; h[3]=4>2h[3]=4>2 scambio; h[1]=3>2h[1]=3>2 scambio; fine
removeMin() →2\to 2 [4, 9, 3, 13, 11, 7] poi [3, 9, 4, 13, 11, 7] h[1] = h[last--] porta 44 in radice e last = 6; figlio minore tra 99 e 33 è 33 (indice 33): scambio; in posizione 33 il figlio è h[6]=7≥4h[6]=7\ge4: stop
removeMin() →3\to 3 [7, 9, 4, 13, 11] poi [4, 9, 7, 13, 11] 77 in radice e last = 5; figli 99 e 44: scambio con 44; in posizione 33 non ci sono figli (2⋅3=6>52\cdot3=6>5)

h[(*last)--] usa il valore corrente di *last come indice e dopo lo decrementa: l'ultima entry viene letta prima che last scenda. Il costo è quello descritto in HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →: ogni funzione sale o scende lungo un cammino, al più ⌊log⁡2n⌋\lfloor\log_2 n\rfloor passi.

Se l'array è passato senza h[0] libero (indici da 0) le formule diventano figli 2i+1, 2i+2 e padre (i-1)/2: va deciso una volta e mantenuto in tutte le funzioni. Un max-heap si ottiene invertendo i confronti; per un heap di struct con chiave e valore si confronta il campo giusto.

Errori comuni

  • Passare last per valore a insert/removeMin: la dimensione non si aggiorna.
  • In indexMinChild leggere h[2i+1] quando 2i+1 > last.
  • liberaAlbero in preorder (libera il nodo e poi legge i figli).
  • Usare Nodo * invece di Nodo ** nell'inserimento: il primo nodo non viene mai collegato.
  • Mescolare indici da 0 e da 1 nello stesso heap.

Versione ripasso

Esercizi su questo argomento

Teoria collegata