LABORATORIO DI ALGORITMI
E STRUTTURE DATI A-L
Ingegneria e scienze informatiche – Cesena
A.A: 2016/2017
Docente: Greta Sasso
Alberi binari di ricerca
 Albero binario in cui le chiavi dei nodi sono ordinate secondo i
seguenti principi:
 Le chiavi del sottoalbero sinistro di un nodo X sono tutte
strettamente minori della chiave del nodo X
 Le chiavi nel sottoalbero destro di un
nodo X sono tutte strettamente
maggiori della chiave del nodo X
 Non esistono chiavi duplicate
nell’albero
 Queste proprietà hanno lo scopo di
agevolare la ricerca di un elemento.
Alberi binari di ricerca
typedef struct BINARYTREE {
int key;
struct BT * left;
struct BT * right;
}BT;
BT * createNode( int e){
BT * node= (BT *)malloc(sizeof (BT));
if (node!=NULL){
node->key=e;
node->left=NULL;
node->right=NULL;
}
return node;
}
Creazione di un BST da una lista di interi
Creiamo l’albero binario di ricerca, visitando sequenzialmente la
lista di interi in input.
Lista di partenza:
Creazione a partire da un vettore
BT* createTree( int * vet , int dim) {
BT* T = NULL;
int i = 0;
if (dim > 0) {
T = nodeAlloc(vet[0]);
for (i = 1; i < dim; i++) {
insertKey(T, vet[i]);
}
}
return T;
}
void insertKey(BT* T, int key) {
if (key < T-> elem) { // se minore della radice lo
inserisco nel sottoalbero di sinistra
if (T->left == NULL)
T->left = nodeAlloc(key);
else
insertKey(T->left, key);
}else if (key > T->elem) {//altrimenti se maggiore
della radice lo inserisco nel sottoalbero di sinistra
if (T->right == NULL)
T->right = nodeAlloc(key);
else
insertKey(T->right, key);
}
}
Visita di un albero binario di ricerca
 Si possono applicare le tre tipologie di visita in profondità viste per
gli alberi binari
 La visita in-order restituisce la lista ordinata delle chiavi di un albero
binario di ricerca:
 Visita del sottoalbero di sinistra
 Visita della radice
 Visita del sottoalbero di destra
Ricerca di un elemento
 Ricerca in un albero binario:
8<15?
Si
per cercare un elemento scendo
ricorsivamente nei sottoalberi
di sinistra e di destra fino a giungere
ai casi base: albero terminato,
o elemento trovato.
No
15==15
Trovato!
if (tree == NULL)return 0;
if (tree->elem == s) return 1;
return treeSearch(tree->left,s) +
treeSearch(tree->right,s);
18<15?
Elem da cercare
 Ricerca in un albero binario di
ricerca:
Possiamo scegliere quale dei due
sottoalberi analizzare per
continuare la ricerca dell’elemento
dato.
15
Ricerca di un elemento
 Si confronta l’elemento da cercare con il valore contenuto nella
radice:
 Se è minore allora si scende nel sottoalbero di sinistra
 Se è uguale la ricerca ha successo
 Se è maggiore si scende nel sottoalbero di destra
Il procedimento si ripete finché la rimanente parte da analizzare corrisponde a
un albero vuoto o finché non si trova l’elemento( caso base della ricorsione)
I nodi incontrati durante la ricorsione
Percorrono un cammino verso le foglie.
Pertanto Il tempo di esecuzione è
O(height(T))
search(x,k)
if x==NIL or k==x.key
return x
if key< x.key
return search(x.left,k)
else
return search(x.right,k)
Ricerca del massimo
Che caratteristiche ha il massimo
assoluto rispetto al massimo
di un sottoalbero?
 15 è un massimo locale
 18 è il massimo assoluto
 il massimo assoluto
non ha il figlio di destra
perché non ci sono elementi
maggiori
Massimo
Assoluto
Massimo nel
sottoalbero
con chiave 18
max(T,e)
While(x.right!=NIL)
x=x.right
Return x
Quanto costano queste operazioni?  Nel caso pessimo, visitano il percorso
più lungo radice-foglia: O(Height[T])
Predecessore
 Predecessore di un
dato nodo: cerca il
nodo con chiave
maggiore tra tutti i nodi
con chiave
strettamente minore di
quella del nodo dato.
 Due i casi da analizzare
1) Il nodo ha un figlio sinistro  il predecessore è il massimo nel
sottoalbero sinistra. Il massimo non ha un figlio alla sua
destra infatti.
2) Il nodo non ha un figlio sinistro  il predecessore è
l’antenato più vicino che contiene il nodo in questione nel
suo sottoalbero destro ( non esiste se e solo se il nodo è il
minimo assoluto)
• Per semplificare l’implementazione delle funzioni
predecessore e successore abbiamo la necessità di
modificare la struttura dati.
• È necessario introdurre un nuovo puntatore che permetta di
accedere velocemente al padre di un nodo.
• Occorre modificare la funzione di inserimento di un nodo
nell’albero tenendo conto di questo nuovo fattore.
• Le altre funzioni di base come la ricerca di una chiave , o la
cancellazione dell’albero non necessitano di modifiche
struct BINARYTREE {
int key;
struct NODO* left;
struct NODO* right;
struct NODO* up;
};
void insertKey2(BT2* T, int key) {
if (key < T->key) {
if (T->left == NULL) {
T->left = nodeAlloc(key);
T->left->up = T;
}
else
insertKey(T->left, key);
}
else if (key > T->key) {
if (T->right == NULL){
T->right = nodeAlloc(key);
T->right->up = T;
}
else
insertKey(T->right, key);
}
}
• Aggiorniamo quindi la procedura di inserimento per gestire il puntatore
al padre
Predecessore
pseudocodice iterativo
function
SearchPredecessor(T)
if T = NIL then
return NIL
end if
if T.left != NIL then //CASO 1: ha un figlio sinistro
return SearchMax(T.left)
else //CASO 2: cerco l’antenato più vicino che contiene
l’attuale nodo nel suo sottoalbero di destra
X=T
y = T.up //antenato
while y != NULL and x == y.left do
x = y
y = x.up
end while
return y
end if
end function
2) Il nodo non ha un figlio sinistro  il predecessore è
l’antenato più vicino che contiene il nodo in questione nel
suo sottoalbero destro ( non esiste se e solo se il nodo è il
minimo assoluto)
X == Y.left?
NO: return Y(8) X=18
Y= X.up =8
X == Y.left?
X=15
Y= X.up =18
X == Y.left?
Pred(9)
X=9
Y= X.up =15
Successore
 Successore di un
dato nodo: cerca il
nodo con chiave
minore tra tutti i nodi
con chiave
strettamente
maggiore di quella
del nodo dato
 Due i casi da
analizzare
1) Il nodo ha un figlio destro  il successore è il minimo del
sottoalbero destro. Il minimo non ha un figlio sinistro.
2) Il nodo non ha un figlio destro  il successore è l’antenato
più vicino che contiene il nodo in questione nel suo
sottoalbero sinistro ( non esiste se e solo se il nodo è il
massimo assoluto)
function SearchSuccessor(T)
if T = NIL then
return NIL
else
if T.right= NIL then
return SearchMin(T.right)
else
x = T
y = x.up
while y != NULL and x = y.right do
x = y
y = x.right
end while
return y
end if
end if
end function
• Quanto costa cercare il successore di un nodo? Caso pessimo:
O(Height[T]). Attraversiamo tutti i nodi da foglia a radice.
• Quanto costa cercare successore di un nodo senza puntatore al padre?
Caso pessimo: O(Height[T]). Attraversiamo tutti i nodi da radice a foglia.
Cancellazione di un nodo
 Vogliamo rimuovere un nodo dall’albero mantenendo le
proprietà di un albero binario di ricerca.
 Tre i casi possibili  il nodo da rimuovere può
1) Essere una foglia
2) Avere un solo figlio
3) Avere due figli
1) Il nodo da rimuovere è una foglia: il nodo viene rimosso
2) Il nodo da rimuovere ha un solo figlio: rimuoviamo il nodo e
lo sostituiamo col figlio
3) Il nodo da rimuovere ha due figli: sostituiamo la chiave del
nodo con la chiave del suo successore e rimuoviamo il
successore. Il successore si trova nel sottoalbero a destra.
La rimozione del successore ricade in uno dei due casi
precedenti ( 1 o 2 )
DelNode(T)
if IsLeaf(T) then
DelKey(T; key)
if IsLeftChild(T) then
x <- SearchKey(T; key)
Left[Up[T]] <- NULL
if x != NIL then
else if IsRightChild(T) then
DelNode(x)
Right[Up[T]] <- NULL
end if
end if
/*cerco la chiave, se
Delete(T)
la trovo, elimino il
else if HasOneChild(T) then
nodo */
tmp <- Child(T)
if IsLeftChild(T) then
Left[Up[T]] <- tmp
else if IsRightChild(T) then
Right[Up[T]] <- tmp
end if
Up[tmp] <- Up[T]
Delete(T)
else if HasTwoChildren(T) then
tmp <- SearchSuccessor(T)
Key[T] <Key[tmp]
DelNode(tmp)
end if
Cancellazione di un nodo – Costo computazionale
• Quando costa DelNode?
• I due casi, rimozione di una figlio o di un nodo con un figlio unico,
vengono gestiti in modo costante.
• Il caso di rimozione di un nodo con due figli costa quando la ricerca
del successore SearchSuccessor
• Caso pessimo: O(Height[T]). Attraversiamo tutti i nodi da foglia a
radice.
• Quando costa DelKey?
• Richiama prima la funzione SearchKey e poi DelNode:
• Costo pessimo di SearchKey: O(Height[T]).
• Costo pessimo di DelNode : O(Height[T]).
• Caso pessimo di DelKey: O(Height[T]) + O(Height[T]) = O(Height[T]).
 Implementare la funzione per la ricerca di un elemento, del massimo
e del minimo all’interno di un albero binario di ricerca
[ Esercizio proposto 5.1 ]
 Fornire un algoritmo, e implementare la soluzione, ( con il costo
computazionale più basso nel caso pessimo) che dati in input un
albero binario di ricerca T e due numeri interi positivi a e b , con a <=
b ritorni il numero di elementi compresi all'intervallo [a, b]
Esempio:
Intervallo : [ 6,15 ]
Output: 4
[ Esame 5.2 ]
Scrivere una funzione che restituisca il
predecessore di un elemento in un albero binario
di ricerca. Usare tale funzione per ordinare
l’albero intero.
[ Esame 13/07/2011 ]
Come cambia l’esercizio dell’esame 13/07/2011
per ottenere un array ordinato in modo
crescente usando il predecessore? E se al posto
del predecessore fosse richiesto di utilizzare il
successore?
Definire un algoritmo per determinare se un
albero binario T in input è un albero binario di
ricerca.
Dati due alberi binari di ricerca T1 e T2 tali che le
chiavi in T1 sono tutte minori delle chiavi in T2,
scrivere una procedura che restituisce un albero
di ricerca contenente tutte le chiavi in tempo
O(h)
[ Esercizi aggiuntivi 5.2 ]
Domande e discussione
 Domande?