Albero binario
Da Wikipedia, l'enciclopedia libera.
In programmazione un albero binario è una struttura dati formata da nodi collegati tra loro da archi.
Ogni nodo è strutturato in modo particolarmente semplice: ha come componenti una chiave, il suo contenuto, e due puntatori con ruoli distinti, un puntatore al cosiddetto figlio destro e uno al figlio sinistro; ciascuno dei due figli può mancare. Si tratta quindi di un caso particolare di struttura ad albero, struttura relazionale per la quale non si pongono limiti al numero dei figli.
I vantaggi di questa struttura dati stanno principalmente nel fatto che consentono di implementare liste che possono essere visitate da procedure molto efficienti.
Indice |
[modifica] Implementare gli alberi binari
In questa sezione trattiamo l'implementazione degli alberi binari dal punto di vista teorico, facendo ricorso a strutture di programmazione generiche; sarà poi compito del programmatore decidere come implementare queste strutture sulla base del linguaggio di programmazione che si troverà ad usare.
Esistono vari sistemi, ma il più semplice risulta essere quello che utilizza un array che contiene i nodi dell'albero secondo questa logica: la radice occupa la prima posizione dell'array e i figli di questa radice sono a loro volta nell'array e occupano come posizione (2i) e (2i+1) dove i è la posizione della radice, del padre, dei due figli.
Si fa notare che questa implementazione è ottimale se l'albero è completo cioè se tutti gli elementi che costituiscono l'albero hanno esattamente due figli, tranne ovviamente le foglie, altrimenti è necessario un flag booleano, in realtà un array di accompagnamento, che ci indica se la posizione è valida o meno.
Interpretiamola: in A, posizione 1, c'è sempre la radice, in posizione 2 e 3 troviamo i figli B e C. Prendiamo il figlio B, posizione 2, i suoi figli sono in 4 e 5, ma, la colonna dei flag ci dice che le posizioni 4 e 5 sono disattivate, infatti B non ha figli, al contrario, C posizione 3, in 6 e 7 ha proprio i valori cercati e cioè i suoi due figli D, E.
I vantaggi dell'utilizzo di questa implementazione sono la semplicità di accesso e la semplicità di gestione degli elementi della lista, al contrario, le operazioni di inserimento e in generale la dimensione considerevole di un array di un grande albero giocano a sfavore di questa implementazione che risulta essere di conseguenza sconveniente da usare.
Un'altra implementazione che prevede l'uso di array è quello della rappresentazione collegata con array, in cui, in una tabella a tre colonne abbiamo, rispettivamente per riga, in quella centrale il valore, in quella sinistra l'indirizzo del figlio sinistro e in quella destra l'indirizzo di quello destro. È necessario aggiungere una variabile inizio per indicare il punto in cui dobbiamo iniziare l'analisi, al contrario, se un indirizzo è a zero è da considerarsi NULL.
Iniziando da 1 , A ha figli che sono in 2 e 3 , il figlio B non ha discendenti, quello C invece ha come discendenti 4 a sinistra e 5 a destra, cioè D ed E.
Lo stesso risultato si può ottenere prendendo in considerazione anziché un array, un semplice nodo strutturato a tre campi, etichetta, figlio sinistro, figlio destro e con un puntatore al primo nodo, e di fatto ci ricolleghiamo all'immagine di accompagnamento alle due tabelle precedenti.
Profondità di un albero: La radice r ha profondità 0, i suoi figli sinistro e destro, hanno profondità 1, i nipoti profondità 2 e cosi via. Quindi se la profondità di un nodo è p i suoi figli non vuoti hanno profondità p+1
Per quanto riguarda l'altezza h di un albero binario è data dalla massima profondità raggiunta dalle sue foglie. Quindi, l'altezza misura la massima distanza di una foglia dalla radice dell'albero, in termini di numero di archi attraversati. Poiché la definizione di alberi si applica anche ai sotto alberi, è più efficiente e semplice trovare l'altezza di un albero binario osservando che l'albero composto da un solo nodo ha altezza pari a 0, mentre un albero con almeno due nodi ha altezza pari all'altezza del suo sottoalbero più alto, incrementata di uno in quanto la radice introduce un ulteriore livello(da cui deriviamo che l'albero vuoto ha altezza pari a -1)
Nel caso dell'albero nella figura qui sopra, l'altezza h è 0(foglie)+1(figli radice)+1(radice)=2.
Esistono alcune formule per calcolare le caratteristiche degli alberi:
| Log2(n + 1) − 1 | Altezza di un albero binario bilanciato pieno di n nodi |
| 2h + 1 − 1 | Numero massimo di nodi in un albero binario di altezza h |
| h | Altezza o numero minimo di nodi di un albero con altezza h |
| 2l | Numero massimo di nodi ad un livello l (elle) |
[modifica] Implementare gli alberi di ricerca binari su array
Se non è necessario effettuare frequentemente operazioni di inserimento e cancellazioni o non è affatto necessario effettuarle e non si vuole usare troppa memoria è possibile implementare un albero di ricerca binario su un array ordinato, con la restrizione che il numero degli elementi sia 2n − 1 con
.
L'immagine sopra mostra un albero di ricerca binario implementato su un array ordinato di 15 elementi, si comincia ponendo il centro dell'array come radice dell'albero e come sue foglie rispettivamente il centro della parte destra dell'array e il centro della parte sinistra dell'array, si continua applicando ricorsivamente il procedimento fino a che non sono stati coperti tutti gli elementi. Si ottiene quindi l'equivalente dell'albero
la pseudo-algoritmo per la ricerca di una chiave è
Ricerca di una chiave
N := numero di elementi dell'albero (2^k-1)
A := array delle N chiavi ordinate in ordine crescente, A[0], A[1] .. A[N - 1]
key := chiave da cercare
jump := (N + 1)/4
i: = (N - 1)/2
while jump > 0 do
if key == A[i] then
ricerca finita
else if key < A[i] then
i = i - jump
else if key > A[i] then
i = i + jump
endif
jump = jump / 2
done
nessun risultato
.
QUESTO MODO DI VEDERE UN ALBERO BINARIO SFRUTTA LA DEFINIZIONE DI SKIP-LIST,OSSIA DI UN ALBERO BINARIO IN CUI GLI ELEMENTI SONO ORDINATI E SI SFRUTTA UN ALGORITMO RANDOMIZZATO. In una skip list noi per cercare un elemento non facciamo altro che creare nuove liste a partire da quella di partenza L0,ma ogni volta dimezziamo gli elementi seguendo la regola (n/2^i)=2 ossia sappiamo che possiamo creare Li liste fino a che non arriviamo alla lista con il minor numero di elementi,ossia 2.La skip list è molto utile se vogliamo trovare un elemento in quanto nella ricerca dobbiamo partire dalla lista L(lgn) e scendere cercando sempre il primo elemento pù grande del valore cecato x;è un buon algoritmo anche se è costoso in termini di spazio, ma è più difficile aggiungere o cancellare un elemento.
[modifica] Implementare gli alberi binari tramite puntatori
Un modo semplice per implementare gli alberi binari di ricerca è quello di usare i puntatori. Nell'implementazione classica ogni nodo dell'albero oltre al suo valore ha un puntatore al figlio destro ed uno al figlio sinistro, in questo modo e' possibile, partendo dalla radice, discendere nell'albero fino ad arrivare alle foglie. Tutti i nodi sono uguali, l'unica differenza e' che nessun nodo puntera' alla radice (infatti la radice non e' né un figlio destro, né un figlio sinistro), e le foglie non avendo figli non punteranno a nulla (nil, NULL value).
[modifica] Una semplice implementazione in C
/************************************************************************************************************* * In questo file verranno incluse le funzioni necessarie a costruire un albero binario, * e gestirlo tramite * i puntatori che ogni radice ha verso il figlio destro e il figlio sinistro. * L'interfaccia e' abbastanza semplice, una volta avviato si arriva ad un menu. * Si consiglia per compilarlo sotto *nix di usare "gcc -o btree binarytree.c" , * Per avviarlo invece, come suggerisce l'ERRORE di parametri * sara' utile seguire la forma ./btree <nomefile>, in cui nomefile puo' anche contenere il path completo. * i dati presenti in nomefile devono essere degli interi separati da spazi. **************************************************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <stdbool.h> FILE *intfile; typedef struct T{ // comincio a definire la struttura che mi servira' int value; // come posso notare ho il valore attuale struct T *T_l, *T_r; // e due puntatori uno al figlio sinistro // l'altro al figlio destro. }*tree, dim; tree mergetree(int el, tree t1, tree t2){ // mergetree unisce due alberi tree t0 = (tree)malloc(sizeof(dim)); t0->T_l = t1; t0->T_r = t2; t0->value = el; return(t0); } tree createleaf(int el){ return mergetree(el, NULL, NULL); // ogni foglia e' formata da un valore // e due puntatori nulli. } int isvoidtree(tree t){ // verifico che un albero sia vuoto if (t == NULL) // se non c'e' nulla e' ovvio che e' // un albero vuoto.. return true; else return false; } tree leftchild(tree t){ // restituisce il figlio sinistro, // accedendo alla struttura tree. return t->T_l; } tree rightchild(tree t){ // restituisce il figlio destro, // accedendo alla struttura tree. return t->T_r; } int root(tree t){ // restituisce la radice, sempre facendo // accesso alla struttura. return t->value; } tree insert(int el, tree t){ // si inserisce un intero el, nell'albero t if(isvoidtree(t)) // se l'albero e' vuoto, allora verra' // creata una foglia return createleaf(el); if (root(t) >= el) // altrimenti si procede da direttive, ovvero // se il valore della radice e' >= dell'elemento return mergetree(root(t), insert(el,leftchild(t)), rightchild(t)); //andra' a sinistra if (root(t) < el) // se la radice e' invece minore // dell'elemento verra' inserita a destra. return mergetree(root(t), leftchild(t), insert(el, rightchild(t))); else return t; } int mintree(tree t){ // Trovo il minimo per dicotomia: sapendo che piu' // mi muovo in basso if(isvoidtree(leftchild(t))) return root(t); // ed a sinistra, piu' ho un numero piccolo. else return mintree(leftchild(t)); // ripeto la procedura ricorsivamente. } int maxtree(tree t){ if(isvoidtree(rightchild(t))) return root(t); // Come per il minimo, solo che in questo caso // mi muovo in basso a destra. else return maxtree(rightchild(t)); } void showtree(tree t){ int i; if (isvoidtree(t) == false){ showtree(leftchild(t)); printf("%d ", root(t)); showtree(rightchild(t)); } } int main(int argc, char *argv[]){ if(argc<2){ // controllo che ci siano tutti i parametri printf("ERRORE: Per avviare il programma la sintassi e' ./btree <file>\n"); return(1); } if ((intfile = fopen(argv[1],"r")) == NULL){ // apro il file che mi serve printf("ERRORE: Non riesco ad aprire il file %s\n",argv[1]); return(2); } printf("*Ho aperto il file %s.\n",argv[1]); int num; // scansiono il file di interi tree albero = NULL; // inizializzo l'albero vuoto while(fscanf(intfile,"%d", &num) != EOF){ albero = insert(num,albero); } printf("*Ho costruito l'albero binario\n\nCosa vuoi fare adesso?\n[s]tampare l'albero.\ncercare il [m]inimo.\ncercare il [M]assimo\n[u]scire.\n\n"); char tmp; printf(">"); while((tmp = getc(stdin)) != 'u'){ switch (tmp){ case 's': // serve a mostrare l'albero showtree(albero); printf("\n"); break; case 'm': // stampa a video il valore minimo printf("Il valore minimo dell'albero binario e' %d\n",mintree(albero)); break; case 'M': // stampa a video il valore massimo printf("Il valore massimo dell'albero binario e' %d\n",maxtree(albero)); break; default: printf(">"); break; } } close(intfile); return(0); }



