Di ricerca
Un albero si dice binario di ricerca se e soddisfa la proprietà di ricerca:
Ogni
y
nel sottoalbero sinistro dix
hay.info <= x.info
e ogniy
nel destroy.info >= x.info
che genera una stampa in ordine crescente con la visita in-order.
Operazioni
Dato che la maggior parte delle operazioni sono dove è l'altezza, mantenere l'albero bilanciato permette di ottenere al meglio invece che nel caso peggiore.
-
Ricerca
search(Node x, Elem k) -> Node | NIL if x == NIL or x.info == k return x else if x.info > k return tree_search(x.left, k) else return tree_search(x.right, k)
oppure iterativamente:
search(Node x, Elem k) -> Node | NIL while x != NIL and x.info != k if k < x.info x = x.left else x = x.right return x
con perchè nel caso peggiore fa il cammino più lungo dell'albero, cioè l'altezza .
-
Massimo e minimo
maximum(Node x) -> Node while x.right != NIL x = x.right return x minimum(Node x) -> Node while x.left != NIL x = x.left return x
anche in questo caso entrambi con .
-
Successore o predecessore
Il successore del nodo
x
in un albero di ricerca sarà il minimo del sottoalbero destro se ne possiede uno, altrimenti sarà l'antenato dix
che lo contiene nel suo sottoalbero sinistro:successor(Node x) if x.right != NIL return minimum(x.right) else y = x.parent while y != NIL and y.right == x x = y y = y.parent return y
con perchè sia
minimum
che il cammino per la radice sono al più .Il predecessore invece, è la versione specchiata del successore:
predecessor(Node x) if x.left != NIL return maximum(x.left) else y = x.parent while y != NIL and y.left == x x = y y = y.parent return y
sempre con .
-
Inserimento
insert(Tree T, Node z) y = NIL x = T.root while x != NIL // Al più h volte y = x if z.info < x.info x = x.left else x = x.right z.parent = y if y == NIL // Anche T.root è NIL T.root = z else if z.info < y.info y.left = z else y.right = z
con .
-
Cancellazione
Si può dimostrare che il successore di un nodo non ha figlio sinistro, dato che nella visita in-order precedono i nodi del sottoalbero sinistro di , poi e poi i nodi del sottoalbero destro.
Per assurdo se , i.e. il successore di , avesse un figlio sinistro , nella visita si avrebbe che che è una contraddizione dato che allora sarebbe il successore di e non .
Questo permetterà di formare un algoritmo corretto per la cancellazione, che richiederà però:
transplant(Tree T, Node u, Node v) // Rimpiazza u con v if u.parent == NIL T.root = v else if u.parent.left == u u.parent.left = v else u.parent.right = v if v != NIL v.parent = u.parent
con , con cui si ottiene:
delete(Tree T, Node z) if z.left == NIL transplant(T, z, z.right) // Rimpiazza z con z.right dato che z.left è vuoto else if z.right == NIL transplant(T, z, z.left) else // Trova il successore in tempo O(h) per metterlo al posto di z y = minimum(z.right) // Se z non è il padre di y si rimpiazza y con y.right così che z.right // sia di ricerca e si possa spostare y al posto di z if y.parent != z transplant(T, y, y.right) y.right = z.right z.right.parent = y // Si sostituisce z con y anche se z è padre di y perchè essendo successore // y non ha y.left e quindi come figli di y si mettono z.left e z.right transplant(T, z, y) y.left = z.left z.left.parent = y
che avrà complessità per via di
minimum
. -
Costruzione
build(Array A) -> Tree Tree T = {root = NIL} for i = 1 to A.length // n volte Node u = {key = A[i], left = NIL, right = NIL} insert(T, u) // O(h) con h = i nel caso peggiore in cui A è ordinato return T
con .
Se sappiamo però che
A
è dato ordinato, allora si può ottimizzare:build_ord(Array A) -> Tree Tree T = {root = build_ord_aux(A, 1, A.length, NIL)} return T build_ord_aux(Array A, int inf, int sup, Node parent) -> Node if inf > sup return NIL else med = floor((inf + sup)/2) Node r = {key = A[med], parent = parent} r.left = build_ord_aux(A, inf, med-1, r) r.right = build_ord_aux(A, med+1, sup, r) return r
con perchè se per il teorema master.
Esempi
-
Restituire il numero massimo di ripetizioni in un albero binario di ricerca in .
int massimo_ripetizioni(Tree t) { if (t.root == nullptr) { return 0; } int max = 1, count = 1; PNode iter = minimum(t.root); // O(h) int value = iter->key; iter = successor(iter); while (iter != nullptr) { if (iter->key == value) { count++; } else { if (count > max) { max = count; } count = 1; value = iter->key; } iter = successor(iter); // O(h) } if (count > max) { max = count; } return max; }
con perchè chiamando
minimum
e poi voltesuccessor
si ha una visita in-order. -
Verificare che un albero sia di ricerca.
bool is_bst(PNode r) { if (r == nullptr) { return true; } int min, max; return is_bst_aux(r, min, max); } bool is_bst_aux(PNode u, int& min, int& max) { int min_sx, min_dx, max_sx, max_dx; bool is_sx, is_dx; if (u->left == nullptr) { is_sx = true; min_sx = max_sx = u->key; } else { is_sx = is_bst_aux(u->left, min_sx, max_sx); } if (u->right == nullptr) { is_dx = true; min_dx = max_dx = u->key; } else { is_dx = is_bst_aux(u->right, min_dx, max_dx); } min = min_sx; max = max_dx; return is_sx && is_dx && max_sx <= u->key && min_dx >= u->key; }
con per la decomposizione.
-
Verificare che dato un albero binario di ricerca .
bool check(Tree t) { if (t.root == nullptr) { return true; } PNode iter = minimum(t.root), succ; bool valid = true; while (iter != nullptr && valid) { succ = successor(iter); if (succ != nullptr && succ->key >= iter->key+2) { valid = false; } else { iter = succ; } } return valid; }
con dato che la visita in-order può terminare in anticipo se l'albero non è valido.