Salta al contenuto
Note per Studenti Alberi binari di ricerca

Alberi binari di ricerca

In questa pagina 6

Un albero binario di ricerca (ABR, binary search tree) implementa una mappa ordinata (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) su chiavi di un universo ordinato. È un albero binario proprio (vedi Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →) i cui nodi interni memorizzano entry e le cui foglie sono vuote (sentinelle esterne), tale che per ogni nodo interno vv con chiave kk:

  • le chiavi del sottoalbero sinistro di vv sono <k< k;
  • le chiavi del sottoalbero destro di vv sono >k> k.

Conseguenza: la visita inorder tocca le entry in ordine crescente di chiave. L'albero con nn entry ha nn nodi interni e n+1n + 1 foglie (vedi la relazione m=n−m+1m = n - m + 1 in Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →).

Ricerca

Algoritmo TreeSearch(k, v)
Input: chiave k, nodo v     Output: nodo di T_v con chiave k, o la foglia in cui k andrebbe
if T.isExternal(v) OR v.getElement().getKey() = k then return v
if k < v.getElement().getKey() then return TreeSearch(k, T.left(v))
else return TreeSearch(k, T.right(v))

Per cercare in tutto l'albero si parte da T.root(). La correttezza è immediata dalla definizione. Complessità (albero della ricorsione, vedi Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →): le chiamate sono sui nodi di un cammino di lunghezza ≤h\le h, una per nodo, ciascuna Θ(1)\Theta(1); esistono istanze in cui il cammino arriva alla foglia più profonda: Θ(h)\Theta(h) (intendendo Θ(h+1)\Theta(h + 1) per includere h=0h = 0).

Metodi della mappa

  • get(k): w←w \leftarrow TreeSearch(k, root); se ww è esterno restituisce null, altrimenti il valore di ww. Θ(h)\Theta(h).

  • put(k, x): w←w \leftarrow TreeSearch. Se ww è interno si sostituisce il valore e si restituisce il vecchio; altrimenti ww è una foglia e si espande: diventa nodo interno con la entry (k,x)(k, x) e due nuove foglie come figli; si incrementa il numero di entry e si restituisce null. Θ(h)\Theta(h).

  • remove(k): w←w \leftarrow TreeSearch; se è esterno restituisce null. Altrimenti due casi:

    1. ww ha almeno un figlio foglia: si elimina ww e la foglia e si collega l'altro figlio uu al padre di ww (se ww era la radice, uu diventa radice);
    2. ww ha due figli interni: si prende yy = predecessore inorder di ww (il nodo più a destra del sottoalbero sinistro: entry con la chiave immediatamente minore) e si copia la sua entry in ww; yy non ha figlio destro interno, quindi si elimina yy con il caso 1. L'albero resta di ricerca: la nuova chiave in ww è maggiore di tutte le chiavi a sinistra e minore di quelle a destra.

    Costo: Θ(h)\Theta(h) per la ricerca, O(h)O(h) per trovare il predecessore, O(1)O(1) per aggiornare i nodi: Θ(h)\Theta(h).

Esempi

Inserimenti (albero inizialmente vuoto) delle chiavi 30,40,24,58,48,26,11,1330, 40, 24, 58, 48, 26, 11, 13: 3030 radice; 4040 a destra; 2424 a sinistra; 5858 figlio destro di 4040; 4848 figlio sinistro di 5858; 2626 figlio destro di 2424; 1111 figlio sinistro di 2424; 1313 figlio destro di 1111. Preorder 30,24,11,13,26,40,58,4830, 24, 11, 13, 26, 40, 58, 48, inorder 11,13,24,26,30,40,48,5811, 13, 24, 26, 30, 40, 48, 58, altezza 33.

L'ordine di inserimento conta. Le stesse chiavi inserite nell'ordine 13,11,24,26,30,40,48,5813, 11, 24, 26, 30, 40, 48, 58 danno un albero diverso (preorder 13,11,24,26,30,40,48,5813, 11, 24, 26, 30, 40, 48, 58, altezza 66, tutta la parte destra è un cammino).

Altri due casi d'esame: inserendo 7,3,10,1,5,9,127, 3, 10, 1, 5, 9, 12 si ottiene un albero perfetto di altezza 22, con inorder 1,3,5,7,9,10,121, 3, 5, 7, 9, 10, 12; inserendo 5,2,8,4,7,9,35, 2, 8, 4, 7, 9, 3 la radice è 55, a sinistra 22 con figlio destro 44 che ha figlio sinistro 33, a destra 88 con figli 77 e 99 (inorder 2,3,4,5,7,8,92, 3, 4, 5, 7, 8, 9, altezza 33).

Rimozioni dall'albero del primo esempio: remove(26) (caso 1: 2626 ha solo foglie): inorder 11,13,24,30,40,48,5811, 13, 24, 30, 40, 48, 58; poi remove(24) (2424 ha solo il figlio sinistro 1111: caso 1, 1111 prende il suo posto); poi remove(30) sulla radice (caso 2: predecessore 1313, che sostituisce la radice): preorder 13,11,40,58,4813, 11, 40, 58, 48.

Altezza e caso pessimo

Le tre operazioni costano Θ(h)\Theta(h) e hh può arrivare a n−1n - 1 (chiavi inserite in ordine crescente o decrescente: l'albero degenera in una lista), per cui al caso pessimo sono Θ(n)\Theta(n); in un albero bilanciato h=Θ(log⁡n)h = \Theta(\log n). Per ottenere Θ(log⁡n)\Theta(\log n) garantito: alberi che si ristrutturano (AVL, rosso-neri) oppure nodi più capienti, gli alberi (2,4) (vedi Multi-way search tree e alberi (2,4)Multi-way search tree (nodi con più entry, d figli e d-1 chiavi ordinate), ricerca in O(d_max h), un MWS tree con n entry ha n+1 foglie; albero (2,4): nodi con 2-4 figli e tutte le foglie alla stessa profondità, altezza Theta(log n) con dimostrazione, operazioni in Theta(log n); idea di overflow e underflow; tabella di confronto con gli alberi binari di ricerca.Multi-way search tree e alberi (2,4) →).

Alberi aumentati

Spesso gli esercizi aggiungono a ogni nodo vv un campo che riassume il sottoalbero TvT_v e che si aggiorna, risalendo, a ogni inserimento o rimozione (O(h)O(h) nodi coinvolti).

Contare le entry con chiave ≤k\le k con v.size = numero di entry di TvT_v:

Algoritmo contaMinoriUguali(v, k)
Input: nodo v, chiave k    Output: numero di entry in T_v con chiave <= k
if T.isExternal(v) then return 0
if k < v.key then return contaMinoriUguali(T.left(v), k)
else return size(T.left(v)) + 1 + contaMinoriUguali(T.right(v), k)

(con size di una foglia uguale a 00). Se k<v.keyk < v.key tutto vv e il sottoalbero destro sono scartati; altrimenti vv e tutto il sottoalbero sinistro contano e si prosegue a destra. Si scende lungo un solo cammino: Θ(h)\Theta(h).

Interrogazione con v.max (esame sui terremoti): TT è una mappa tempo t↦t \mapsto intensità iti_t e ogni nodo ha v.max = massima intensità in TvT_v. Si chiede se esiste una entry con chiave ≥t\ge t e valore ≥i\ge i.

Algoritmo TFR(v, t, i)
Input: nodo v, tempo t, intensità i     Output: yes/no
if T.isExternal(v) OR v.max < i then return no       (nessuna entry in T_v può bastare)
if v.key >= t then
    if v.value >= i then return yes
    if TFR(T.left(v), t, i) = yes then return yes     (a sinistra ci sono chiavi < v.key, forse >= t)
return TFR(T.right(v), t, i)

Se v.key<tv.key < t tutto il sottoalbero sinistro ha chiavi <t< t e si va solo a destra. Se v.key≥tv.key \ge t tutto il sottoalbero destro ha chiavi ≥t\ge t, quindi per esso conta solo max.

Complessità O(h)O(h). I nodi su cui l'algoritmo prosegue "a cavallo" della soglia tt formano un unico cammino radice-foglia. Da ciascuno di essi parte al più una seconda chiamata, sul sottoalbero interamente nell'intervallo: se il suo max è <i< i termina subito con nono in O(1)O(1); altrimenti esiste una entry valida, e la discesa lungo il cammino dei nodi con max ≥i\ge i la trova restituendo yesyes dopo al più hh passi (e a quel punto tutto finisce). In totale O(h)+O(h)=O(h)O(h) + O(h) = O(h). Il codice è stato confrontato con la ricerca esaustiva su 500 alberi casuali.

Errori comuni

  • Credere che h=Θ(log⁡n)h = \Theta(\log n): vale solo se l'albero è bilanciato; il caso pessimo è Θ(n)\Theta(n).
  • Nel caso 2 della rimozione, eliminare ww senza sostituirlo con il predecessore (o con il successore): l'albero perde la proprietà di ricerca.
  • Supporre che inserire le stesse chiavi in ordine diverso dia lo stesso albero.
  • Dimenticare di aggiornare i campi aumentati (size, max) lungo il cammino dopo put e remove.

Versione ripasso

Esercizi su questo argomento

Teoria collegata