Alberi
Gli alberi (finiti, ipotesi d’ora in avanti sottintesa) si possono vedere come una generalizzazione
delle sequenze lineari (vettori o liste) nel senso che, mentre queste in ultime ciascun elemento
possiede al più un successore immediato, in un albero i successori possono esser più di uno.
lista
…
albero
Il primo elemento di questa struttura prende il nome di radice; i sui successori immediati sono
altrettante radici di alberi contenuti all’interno dell’albero dato o, come si dice, sottoalberi.
Definizione induttiva di albero
Fissato un insieme A di valori, l’insieme Tree(A) egli alberi etichettati con valori in A è definito
induttivamente:
1) ∅ ∈ Tree( A)
2) a ∈ A, k ≥ 0, t1 ,..., t k ∈ Tree( A) ⇒ (a, t1 ,..., t k ) ∈ Tree( A)
Osserviamo che (a, t1 ,..., t k ) è una sequenza ordinata, in cui a è l’etichetta della radice, e
t1 ,K, t k sono i sottoalberi immediati della radice; non sempre è necessario che vi sia un ordine tra i
sottoalberi immediati della radice, nel qual caso nella seconda clausola della definizione si utilizza
un insieme {a, t1 ,K, t k }.
Esempi: albero genealogico, albero del file system, alberi sintattici, albero delle chiamate ricorsive.
Gli alberi sono particolari grafi. Un grafo è una struttura G = (V, A), dove V è l’insieme dei vertici
(o nodi), mentre A ⊆ V × V è una relazione binaria tra vertici: quando uAv (che abbrevia (u, v) ∈ A )
si dice che esiste un arco incidente in u e v (in tal caso u e v si dicono adiacenti); in generale gli
archi (u, v) e (v, u) sono distinti, e il grafo si dice sia orientato; quando la relazione A è simmetrica,
gli archi (u, v) e (v, u) vengono identificati (alternativamente si definisce A come insieme di coppie
non ordinate di vertici), e il grafo non è orientato. Graficamente si rappresentano vertici ed archi
come qui di seguito illustrato:
u
v
Arco orientato da u a v
u
v
Arco non orientato tra u e v
Gli archi si possono comporre a formare un cammino, purché i vertici in cui gli archi incidono
corrispondano: la relazione “esiste un cammino da u a v” (ovvero tra u e v nel caso non orientato) è
la chiusura transitiva A + di A:
u
w
x
v
Cammino (orientato) da u a v
Quando esiste un vertice v tale che vA+ v , ossia un cammino da un vertice a se stesso, si dice che
esiste un ciclo (nel caso non orientato si parla talora di circuito ovvero di cricca, intendendo con
quest’ultimo termine l’insieme dei vertici in cui incidono gli archi ciclo).
Definizione degli alberi come grafi: alberi liberi
Un albero libero è un grafo G = (V, A) non orientato tale che:
1) G è connesso, ovvero per ogni coppia di vertici u, v ∈V con u ≠ v sia uA + v , ossia esiste un
cammino tra u e v;
2) G è aciclico, ovvero in G non vi sono cicli.
Comunque in un albero libero si scelga un vertice r ∈V , resta determinato un albero nel senso della
definizione induttiva di albero; in effetti l’insieme V − {r} risulta allora ripartito in sottoinsiemi
disgiunti di vertici, che sono altrettanti alberi; inoltre in ciascuno di essi la scelta di r induce la
scelta di una radice: l’unico vertice adiacente ad r. Assumendo che V sia finito, le due nozioni di
albero sono allora equivalenti. Nelle figure seguenti si illustra un albero libero con due delle
possibili scelte di una radice (il vertice scelto come radice è cerchiato con un tratto spesso e l’albero
disegnato in modo da evidenziare la relazione padre-figlio con segmenti dall’alto al basso):
e
a
a
c
b
d
d
e
c
d
Albero libero
b
c
b
Albero libero con radice in c
a
e
Albero libero con radice in b
Rappresentazione degli alberi in memoria
Il grado di un albero è il massimo numero di figli di uno stesso vertice. Gli alberi binari, ossia di
grado 2, sono rappresentabili in memoria con una generalizzazione diretta della rappresentazione
delle liste semplici; si utilizzano cioè record con tre campi anziché due, di cui uno è destinato a
contenere l’informazione sull’etichetta (label), e gli altri due (left e right) sono puntatori al primo e
secondo sottoalbero della struttura che ha radice nel vertice rappresentato dal record.
r
label
left
a
right
c
r
b
d
a
c
b
d
Rappresentazione in memoria di un albero binario con record e puntatori
Anche la codifica in C++ di questa struttura dati è simile a quella delle liste: un possibilità è quella
qui di seguito riportata (caso delle etichette come interi):
typedef struct BTnode* BTree;
struct BTnode {
int label;
BTree left, right;
};
BTree NewNode (int lbl, BTree lft, BTree rgt)
{
BTree t = new BTnode;
Label(t) = lbl;
Left(t) = lft;
Right(t) = rgt;
return t;
}
int& Label (BTree t)
{
return t->label;
}
BTree& Left (BTree t)
{
return t->left;
}
BTree& Right (BTree t)
{
return t->right; }
Il caso degli alberi generali, ovvero di grado superiore a 2 pone un evidente problema per la
definizione dei record che rappresentano i vertici, non essendovi a priori limiti al numero dei figli di
un vertice. Naturalmente è possibile prevedere dei campi vettore per i figli, allocati dinamicamente,
ma questo presuppone che l’albero sia dato per così dire tutto insieme, e non, come invece
normalmente accade, costruito per successivi inserimenti di nuovi vertici o di interi sottoalberi in
una struttura preesistente. Per questo motivo si adotta una soluzione basata sulla rappresentazione
degli alberi binari e sul fatto che un albero di qualunque grado può essere trasformato in albero
binario in modo invertibile, ossia potendosi ricostruire la struttura originaria a partire da quella in
forma binaria. Il metodo si basa su una diversa interpretazione dei due sottoalberi di un vertice
binario: la radice del sottoalbero di sinistra rappresenta infatti il primo figlio del vertice considerato,
mentre il ramo destro (ossia la sequenza dei discendenti a destra dal vertice dato) rappresenta i
fratelli successivi in un dato ordinamento:
a
a
b
e
c
b
d
f
g
c
e
d
f
g
Trasformazione di un albero di grado 3 in un albero binario
La struttura del record che rappresenta un vertice è dunque la stessa che nel caso degli alberi binari,
salvo che, per rendere il codice più perspicuo, si adotta una diversa scelta dei nomi dei campi:
typedef struct Tnode* Tree;
struct Tnode {
int label;
Tree child, sibling;
};
L’approccio illustrato è preferibile quando ci si aspetta di esplorare gli alberi dalla radice verso le
foglie, cioè verso quei vertici privi di discendenti. Gli alberi orientati in questo modo si dicono
alberi sorgente. Se invece l’esplorazione procede dalle foglie (o da qualunque altro vertice interno)
verso la radice, si preferisce una realizzazione in cui ciascun vertice diverso dalla radice rinvia al
vertice padre: è il caso dei cosiddetti alberi pozzo.
Albero sorgente
Albero pozzo
A differenza della relazione padre-figlio, quella inversa figlio-padre è univoca in un albero;
considerando dunque l’albero come un grafo (V, A), la relazione A −1 è una funzione finita, che si
può facilmente rappresentare con un vettore di record a due campi: uno per l’etichetta (label) e
l’altro (parent) per l’indice del record che rappresenta il padre nel vettore (la radice, non avendo
padre, conterrà un valore non significativo come – 1, oppure il proprio stesso indice).
a
c
b
d
e
f
e
g
c
d
a
f
b
6
2
4
6
-1
6
4
0
1
2
3
4
5
6
g
Albero rappresentato con il “vettore dei padri”
Il vettore che rappresenta questa relazione è chiamato “vettore dei padri”, ed il suo uso non è
limitato alla rappresentazione di un singolo albero: in effetti questa codifica viene utilizzata per
rappresentare un insieme finito di alberi (foresta) che a loro volta rappresentano partizioni
dell’insieme delle etichette che occorrono nei vertici in cui ciascuna parte, che nella struttura è un
albero, ha per rappresentante la radice di questo albero.
x
b
r
h
f
a
x b w f a h r
-1 0 5 0 5 -1 0
w
0
1 2
3 4
5
6
Rappresentazione della partizione {{ x, b, r , f }, {h, a, w}}
Visite di alberi
Pressoché tutti gli algoritmi sugli alberi comprendono una parte di navigazione attraverso la
struttura, che viene detta nel caso degli alberi (e dei grafi in generale) visita. Il caso più evidente è
quello della ricerca di un valore dato tra le etichette di un albero. Restringendo l’attenzione agli
alberi binari, possiamo affrontare il problema estendendo a questa struttura l’algoritmo di ricerca in
una lista (semplice). Delle due opzioni però che abbiamo nel caso delle liste, quella iterativa non è
direttamente applicabile agli alberi, non essendo chiaro come si possa recuperare l’informazione su
quale sia la radice del sottoalbero destro se nella visita si sia scelto di esplorare prima il sottoalbero
sinistro (e viceversa). La versione ricorsiva invece si adatta perfettamente:
bool SearchList (int n, List l)
{
if (IsEmptyList(l)) return false;
else if (n == Head(l)) return true;
else return SearchList (n, Tail(l));
}
diventa infatti:
bool SearchBTree (int n, Btree t)
{
if (IsEmptyTRee(t)) return false;
else if (n == Label(t)) return true;
else return SearchBTree (n, Left(t)) ||
SearchBTree (n, Right(t));
}
Questa ricerca si basa su un’induzione sull’altezza dell’albero: se l’albero è vuoto la risposta è
negativa; se l’etichetta della radice è n allora la risposta è positiva; altrimenti n occorre in t se e solo
se occorre nei sottoalberi sinistro oppure destro: ma essendo questi di altezza inferiore a quella di t
la funzione SearchBTree applicata ad essi è definita per ipotesi induttiva.
Se si tiene conto della sequenza dei vertici dell’albero nell’ordine in cui questa funzione li
ispeziona, allora ci si accorge che questa corrisponde ad una visita in profondità (DFS: Depth First
Search), vale a dire ad un’esplorazione dell’albero in cui si procede dalla radice percorrendo i rami
sino alle foglie e ritornando indietro all’ultima diramazione non esplorata per proseguire allo stesso
modo la visita dei vertici sugli altri rami.
Ad esempio una DFS dell’albero nella figura qui sotto, eseguita allo stesso modo della funzione
SearchBTree,
0
12
2
9
3
4
darebbe la sequenza:
0, 2, 9, 3, 12, 4.
Si tratta più specificatamente di una visita in preordine, ossia che procede a visitare la radice, e
quindi con lo stesso criterio il sottolabero sinistro e quindi quello destro. Se la sequenza fosse stata:
sinistro, radice, destro, si sarebbe parlato di visita in ordinata; nel caso della sequenza sinistro,
destro, radice la visita si dice in postordine.
Quelli che seguono sono altri esempi di visite DFS:
int CardBTree (Btree t)
// post: ritorna la cardinalità, ossia il numero dei vertici, di t
{
if (IsEmptyBTree(t)) return 0;
else return CardBTree (Left(t)) + CardBTree (Right(t)) + 1;
}
Si dice altezza di un albero non vuoto la massima distanza tra la radice e le foglie, misurata
contando il numero degli archi che formano un cammino tra la radice ed una foglia, che si dice
ramo.
int Height (Btree t)
// pre: t non è vuoto
// post: ritorna l’altezza di t
{
if (IsEmptyBTree(Left(t)) && IsEmptyBTree(Right(t)))
// t è una foglia
return 0;
else if (IsEmptyBTree(Right(t))) // Left(t) non è vuoto
return Height (Left(t)) + 1;
else if (IsEmptyBTree(Left(t))) // Right(t) non è vuoto
return Height (Right(t)) + 1;
else // né Left(t) né Right(t) è vuoto
return Height (Left(t)) + Height (Right(t)) + 1
}
Una volta compreso come costruire una DFS nel caso degli alberi binari, la sua estensione agli
alberi generali non richiede modifiche sostanziali; quello che segue è un algoritmo che stampa la
sequenza delle etichette di un albero (rappresentato con record e puntatori) visitato in profondità:
void DFS (Tree t)
{
if (!IsEmptyTree (t))
{
cout << Label(t);
// l’iterazione che segue considera tutti
// i vertici che sono figli di quello appena vistato
Tree child = Child(t);
while (child != NULL)
{
DFS (child);
child = Sibling(child);
}
}
}
Una semplice induzione sull’altezza dell’albero mostra che una DFS ha complessità temporale
O(n), dove la dimensione dell’ingresso è la cardinalità dell’albero.
Un’alternativa alla DFS è la BFS (Breadth First Search), visita in ampiezza o per livelli. Un livello è
formato da vertici equidistanti dalla radice; i livelli sono numerati in dipendenza di tale distanza:
Livello 0
Livello 1
Livello 2
Un primo approccio al problema consiste nello sviluppare una funzione che visita il livello k ≥ 0
dell’albero (binario):
void Level (BTree t, int k)
// stampa le etichette del livello k dell’albero t
{
if (!IsEmpty(t))
{
if (k == 0) cout << Label(t);
else
{
Level(Left(t), k – 1);
Level(Right(t), k – 1);
}
}
}
A questo punto, per ottenere una BFS sarà sufficiente iterare la funzione Level da 0 all’altezza:
void BFS (BTree t)
{
int h = Height (t);
for (int k = 0; k <= h; k++)
Level(t, k);
}
Qual è la complessità di questa funzione? Dipende chiaramente da quella di Level, che a sua volta è
una DFS dell’albero che si ottiene da t eliminando tutti i vertici che distino più di k dalla radice: il
caso peggiore si ottiene quando il rapporto tra k e la cardinalità di quet’albero è prossimo ad 1, e
cioè quando l’albero sia degenere:
Alberi degenere sinistro e destro
Se ne deduce che Level è O(k), e quindi che, sempre nel caso peggiore, la complessità di BFS
n −1
risulta O(∑k =0 k ) = O(n 2 ).
Ci chiediamo allora se esista un metodo per costruire una BFS in modo che risulti della stessa
complessità della DFS, cioè O(n). La soluzione cercata si basa sull’uso di una coda, vale a dire di
una struttura sequenziale cui si accede solamente agli estremi: con l’operazione Enqueue si
aggiunge un elemento come ultimo della coda; con l’operazione Dequeue si elimina il primo
elemento dalla coda e lo si restituisce. L’effetto degli accodamenti e delle eliminazioni sugli
elementi di una coda è chiamato regola FIFO, First In First Out, ossia il primo entrato è il primo ad
uscire, ed in generale nell’estrazione viene rispettata la sequenza cronologica degli inserimenti.
Dal momento che il tipo dei valori che sono sulla coda non incide sulla natura delle operazioni che
gestiscono la coda, alcuni linguaggi tra cui il C++, consentono di implementare questa ed altre
simili strutture dati come classi parametriche rispetto al tipo. Il costrutto utilizzato è
template <class T> class … { … };
Una classe di questo genere la si può pensare come un funtore che, dato un tipo, restituisce una
classe, completa di tutti i suoi membri, quale sarebbe stata definita sostituendo nel codice che segue
la dichiarazione template il tipo dato alla variabile di tipo T (per maggiori informazioni si
consulti il capitolo 16 del testo di Schildt).
template <class T>
class Queue {
public:
Queue() { front = rear = NULL; }
bool IsEmpty() const { return front == NULL; }
void Enqueue (T t); // post: aggiunge t in fondo
T Dequeue (); // pre : coda non vuota
// post: elimina e ritorna il primo el.
private:
QElement<T> *front, *rear;
};
La realizzazione di questa classe si basa sull’uso di una lista di record concatenati di cui mantiene
un puntatore al primo elemento, front, ed un all’ultimo, rear.
front
…
rear
Il tipo QElement dei record che formano la lista è anch’esso realizzato con una struttura
parametrica in T:
template <class T>
struct QElement {
T elem;
QElement* next;
};
Le operazioni Enqueue e Dequeue sono ordinarie operazioni di inserimento davanti e al fondo di
una lista semplice, in cui occorre però distinguere quando l’inserimento avvenga in una coda vuota,
ovvero quando l’estrazione produca una coda vuota: in queste situazioni il comportamento di
Enqueue e Dequeue deve infatti coinvolgere sia front che rear, là dove normalmente
coinvolge solo uno dei due puntatori (rear nel caso di Enqueue, front in quello di Dequeue).
template <class T>
void Queue<T>::Enqueue (T t)
{
QElement<T>* newEl = new QElement<T>;
newEl->elem = t;
newEl->next = NULL;
if (rear == NULL)
{ front = newEl; rear = newEl; }
else
{
rear->next = newEl; rear = rear->next;
}
template <class T>
T Queue<T>::Dequeue ()
{
T t = front->elem;
QElement<T>* tobedeleted = front;
front = front->next;
if (front == NULL) rear = NULL;
delete tobedeleted;
return t;
}
}
Osserviamo che, con questa implementazione, le operazioni di accodamento ed estrazione
richiedono sempre tempo costante.
Tornando al problema di implementare una BFS in tempo lineare, possiamo finalmente scrivere:
void BFS (BTree t)
{
if (!IsEmptyTree(t))
{ Queue<Tree> q;
// qui la classe Queue si specializza al tipo BTree
q.Enqueue(t);
while (!q.IsEmpty())
{
t = q.Dequeue();
cout << Label(t);
if (!IsEmptyBTree(Left (t))) q.Enqueue(Left (t));
if (!IsEmptyBTree(Right(t))) q.Enqueue(Right(t));
}
}
}
L’invariante di ciclo di questo algoritmo è che la sequenza delle etichette degli elementi sulla coda
costituisce sempre una sottosequenza della sequenza in uscita, ossia della visita per livelli. Quanto
alla complessità, poiché ogni vertice dell’albero entra nella coda esattamente una volta, e la visita di
ciascuno di essi richiede tempo O(1), essa è chiaramente O(n) nella cardinalità dell’albero, come
richiesto. Si lascia come esercizio l’estensione di questo algoritmo al caso generale degli alberi di
grado qualunque.
Alberi binari di ricerca
Tra gli impieghi più frequenti degli alberi vi è quello per la realizzazione di collezioni di elementi
su cui si eseguono fondamentalmente tre operazioni: la ricerca di un elemento, l’inserimento e la
cancellazione. Tali tipi di dato sono chiamati Insiemi dinamici o Dizionari. Supponiamo che i valori
che etichettano i vertici degli alberi siano ordinabili in modo lineare (cosicché due elementi sono
sempre confrontabili): se non esiste alcuna relazione tra l’ordine relativo delle etichette e la
posizione dei vertici nell’albero, per la ricerca non vi è alternativa alla funzione SearchBTree
vista sopra (costo O(n)). Supponiamo invece di ricercare valori (diciamo interi) su alberi binari che
soddisfino la seguente definizione:
Definizione degli alberi binari di ricerca (Binary Search Tree – BST)
Un albero binario è di ricerca se, essendo etichettato con valori da un dominio totalmente ordinato,
è vuoto oppure:
1) i sui sottoalberi sinistro e destro sono entrambi di ricerca,
2) l’etichetta della radice è maggiore di tutte le etichette del sottoalbero sinistro e minore di
tutte quelle del sottoalbero destro.
8
3
10
1
6
4
13
È chiaro che se t è un BST ed il valore cercato è minore (maggiore) di quello in radice, la ricerca
può limitarsi al sottoalbero sinistro (destro), e così via ricorsivamente. Nel caso peggiore
l’algoritmo esplora il ramo più lungo ed è dunque O(h) dove h è l’altezza dell’albero:
bool BST_Search (Btree t, int k)
// pre: t è un BST con etichette intere
// post: ritorna true se e solo se k occorre in t
{
if (IsEmptyBTree(t)) return false;
else if (k == Label(t)) return true;
else if (k < Label(t)) return BST_Search(Left(t));
else return BST_Search(Right(t));
}
Naturalmente, poiché h nel caso peggiore è n – 1, dove n è la cardinalità dell’insieme, non vi è in
generale nessun vantaggio ad usare un BST in luogo di una struttura lineare come un vettore
ordinato, il quale consente comunque una ricerca O(log n) con l’algoritmo della ricerca dicotomica.
La ragione della scelta di queste strutture dati per la rappresentazione dei dizionari risiede in due
fatti distinti:
1) tutte le tre operazioni fondamentali su un BST sono O(h), come verrà mostrato di seguito; al
contrario le operazioni di inserimento e cancellazione su un vettore ordinato sono Ω(n), a
causa della necessità di effettuare traslazioni degli altri valori presenti sul vettore;
2) se la struttura dell’albero è sufficientemente simile a quella di un albero completo, allora
h ∈ O(log n) e l’albero si dice bilanciato: sono note tecniche che, all’atto di inserire ovvero
eliminare un elemento da un albero bilanciato impediscono che questo perda tale
caratteristica, e lo fanno con complessità ancora O(h): il risultato è una struttura dati che
consente di effettuare tutte le operazioni fondamentali in tempo O(log n).
Trascurando l’aspetto del bilanciamento (per cui si rinvia il lettore interessato ad un testo di
algoritmi che tratti ad esempio i cosiddetti alberi rosso-neri, oppure gli alberi AVL), di seguito si
illustrano gli algoritmi di inserimento e cancellazione su un BST.
BTree Insert (int k, BTree t)
// pre: t è un BST
// post: ritorna il BST ottenuto inserendo k in t;
//
se k è presente in t, ritorna t senza modifiche
{
if (!IsEmptyBTree(t)) return NewNode(k, NULL, NULL);
else if (Label(t) == k) return t;
else if (k < Label(t))
{
Left(t) = Insert(k, Left (t));
return t;
}
else {
Right(t) = Insert(k, Right(t));
return t;
}
}
L’algoritmo è in sostanza una ricerca che, nel caso in cui il valore k non sia presente, aggiunge un
nuovo nodo contenente k il quale risulterà una foglia dell’albero così modificato. Si osservi come la
funzione ritorni, e quindi assegni nuovamente, tutti i puntatori dei vertici sul ramo attraversato nella
ricerca: sebbene questo possa sembrare dispendioso e soprattutto inutile, non altera la complessità,
che rimane O(h), ma rende semplice il codice, perché nell’aggiungere il nuovo nodo non abbiamo
bisogno di sapere se l’inserimento avvenga a sinistra o a destra di un nodo preesistente, oppure se il
nuovo nodo sia la radice del BST modificato (che allora conterrà solo quel vertice).
L’eliminazione di un vertice contenete un determinato valore da un BST pone un problema:
concentrandoci infatti sul caso in cui il vertice da eliminare occorra in radice (diversamente ci
penserà la ricorsione a svolgere il lavoro) si possono presentare quattro casi qui di seguito
raffigurati, a seconda della presenza o meno di sottoalberi non vuoti della radice:
k
k
k
left
k
right
left
right
Il primo caso è banale poiché l’albero risultante è vuoto; il secondo ed il terzo sono semplici,
essendo sufficiente ritornare il sottoalbero sinistro e destro rispettivamente; il quarto caso richiede
maggiore attenzione, non essendo chiaro come si possano fondere tra loro i due alberi risultanti
dall’eliminazione della radice. Tuttavia si può osservare che se il massimo del sottoalbero sinistro,
oppure il minimo del destro rimpiazzasse la radice, il risultato sarebbe ancora un BST. Bisogna
tenere conto del fatto che in questo processo di rimpiazzamento il vertice che diventerà la nuova
radice andrà comunque eliminato dal sottoalbero in cui si trova; ma il massimo (minimo) in un BST
non può avere un sottoalbero destro (sinistro), dunque la sua eliminazione ricade in uno dei primi
due casi (ovvero del primo e terzo nel caso del minimo).
8
3
6
10
1
6
3
13
1
10
4
13
4
L’eliminazione di 8 dall’albero sopra a sinistra comporta la migrazione di 6 in radice, ma anche la
sua eliminazione dal sottoalbero sinistro, ossia, ricorsivamente, dal sottoalbero con radice 6, che
viene rimpiazzato dal suo sottoalbero sinistro, nell’esempio costituito dal solo vertice 4. L’albero
che ne risulta è raffigurato sopra a destra.
BTree DelMax (BTree t, BTree& m)
// pre: t non è vuoto
// post: ritorna l’albero che si ottiene da t rimuovendo il massimo
//
il cui indirizzo (puntatore) è assegnato ad m (parametro
//
passato per riferimento)
{
if (IsEmpty(Right(t)) // il massimo è in radice
{
m = t;
return Left(t);
}
else // il massimo di t è il massimo di Right(t)
{
Right(t) = DelMin (Right(t), m);
return t;
}
}
BTree Delete (int k, Btree t)
// pre: t è un BST
// post: ritorna il BST ottenuto eliminando k da t;
//
se k non è presente in t, ritorna t senza modifiche
{
if (IsEmptyBTree(t)) return NULL;
else if (k == Label(t)) // elimina la radice: distingue 4 casi
{
BTree p;
if (IsEmptyBTree(Left(t)) && IsEmptyBTree(Right(t)))
{
delete t; return NULL; }
else if (IsEmptyBTree(Right(t)))
{
p = Left(t);
delete t;
return p;
}
else if (IsEmptyBTree(Left(t)))
{
p = Right(t);
delete t;
return p;
}
else // la radice ha due sottoalb. non vuoti
{
Left(t) = DelMax (Left(t), p);
Left (p) = Left(t);
Right(p) = Right(t);
delete t;
return p;
}
}
else if (k < Label (t))
{
Left (t) = Delete(k, Left (t));
return t;
}
else {
Right(t) = Delete(k, Right(t));
return t;
}
}