Un albero binario è ordinato, e viene anche chiamato albero binario di ricerca, binary search tree (BST), quando il campo chiave di ogni nodo è minore del campo chiave di ogni nodo del suo sottoalbero destro ed è maggiore del campo chiave di ogni nodo del suo sottoalbero sinistro.
Un albero si dice bilanciato se il livello di tutte le foglie è uguale all'altezza dell'albero o a questa stessa altezza meno 1.
Se un albero è bilanciato allora per fare una ricerca di una chiave in esso contenuta si esplora in un numero di passi inferiore a quello necessario per esplorare un albero non bilanciato.
Come si può osservare nell’esempio degli alberi mostrati nella figura successiva pur essendo tutti alberi BST la ricerca della chiave Riccardo viene fatta in un numero di passi decisamente diverso a seconda se l’albero è o non bilanciato.
Se un albero bilanciato ha M livelli, il numero di nodi di cui è formato può variare tra
2M e 2(M+1) -1
In un albero bilanciato il numero di passi massimo di ricerca è di O(log2 (N)) dove N è il numero di nodi.
Da questa immagine si ricava facilmente che il numero di nodi N di un albero bilanciato, nota l’altezza dell’albero pari a M (o M+1) è data: (vedi figura).
Per trovare, quindi, una chiave in un dato BST, se esso è bilanciato il numero massimo di passi è dato dall’altezza dell’albero M. Poichè abbiamo mostrato che 2M≤N≤2M+1-1.
Da questa espressione per ricavare N, supposto M abbastanza grande, e quindi -1 trascurabile abbiamo:
N ≅ 2M da cui M ≅ log2N
Quindi per un albero bilanciato, con N nodi, il numero di passi massimo per trovare una chiave è pari O(log2N), nel caso l’albero non fosse bilanciato il massimo numero di passi sarà pari a O(N).
Nella figura è illustrato il caso peggiore per il quale la ricerca può essere, nel caso peggiore fatta in O(N) passi.
Si può dimostrare che un albero di ricerca binario costruito in maniera casuale, quindi non necessariamente bilanciato, effettua in media un numero di passi per effettuare la ricerca pari a 1.36 * O(log2(N)).
Per avere un’idea della differenza che intercorre tra i valori O(N), log2(N) e 1.36* log(N) riportiamo nella tabella che segue i valori delle tra funzioni corrispondenti a diversi valori di N.
Di seguito ci rifaremo alla seguente struct per definire un nodo di un albero:
struct RTnodo
{
int key;
RTnodo *left, *right;
};
typedef RTnodo* Tnodo;
Per esplorare un albero vi sono tre diverse modalità.
INORDER. Visitare tutti i nodi di un BST di interi, attraversando:
PREORDER. Visitare tutti i nodi di un BST di interi, attraversando:
POSTORDER. Visitare tutti i nodi di un BST di interi, attraversando:
Dato l’albero di figura (foto in alto)
un esempio di output è riprodotto nella foto in basso.
Allegato: visite
Per utilizzare il codice visite è necessario costruire un file di nome TREE1 .txt in cui vanno inserite le chiavi dei singoli nodi dell’albero che si vuole costruire.
Poiché ogni nodo di un BST punta ad altri due nodi, ciascuno dei quali è una variabile dinamica di tipo struct avremo che una variabile di tipo RTnodo, cioè un puntatore alla radice di un BST, è a sua volta una variabile BST, e può quindi essere passata da un blocco ad un altro.
In altre parole dato un nodo di un BST, questo è radice per i suoi sottoalberi e i nodi a cui punta sono a loro volta radici di altri sottoalberi.
Pertanto possiamo adoperare ricorsivamente queste variabili.
Le principali operazioni che ci interessa definire sono: