Salta al contenuto
Note per Studenti Esercizio 8 · indici del level numbering

Esercizio 8indici del level numbering

Esame
In questa pagina 5

Testo (esempio di tema d'esame, seconda parte, esercizio 1, 5 punti). Dato un albero binario proprio TT, si definisce il level numbering dei nodi di TT come segue:

  • se vv è radice di TT, allora l'indice ℓ(v)\ell(v) di vv nel level numbering è 11;
  • se vv non è radice, sia uu il padre di vv: allora ℓ(v)=2 ℓ(u)\ell(v) = 2\,\ell(u) se vv è figlio sinistro di uu, e ℓ(v)=2 ℓ(u)+1\ell(v) = 2\,\ell(u) + 1 se vv è figlio destro di uu.

Si vuole progettare un algoritmo ricorsivo che salvi in un campo v.indexLN l'indice ℓ(v)\ell(v) di vv nel level numbering, per ogni nodo v∈Tv \in T.

(a) Si fornisca lo pseudocodice per l'algoritmo richiesto. (b) Si analizzi la complessità dell'algoritmo.

Per ricevere il massimo dei punti l'algoritmo deve avere complessità in tempo O(n)O(n), dove nn è il numero di nodi di TT.


Idea

L'indice di un nodo dipende solo dall'indice del padre e dal lato. Il legame con la scrittura binaria (Sistemi di numerazione posizionaliNotazione posizionale in base b; conversioni tra base 10, 2, 8 e 16 per interi (divisioni successive) e per parti frazionarie (moltiplicazioni successive); numeri periodici in binario.Sistemi di numerazione posizionali →) è utile: moltiplicare per 22 aggiunge uno zero a destra, moltiplicare per 22 e sommare 11 aggiunge un uno. Quindi ℓ(v)\ell(v) è il numero binario formato da un 11 iniziale seguito, nell'ordine, dai passi dalla radice a vv (00 = sinistra, 11 = destra). Quindi il padre deve conoscere il proprio indice prima di passarlo ai figli: serve una visita in preorder in cui l'indice viene passato come parametro (non lo si ricalcola risalendo dai nodi: costerebbe Θ(depth)\Theta(\text{depth}) per nodo, quindi fino a Θ(n2)\Theta(n^2) in totale; vedi Visite di alberiVisite in preorder e postorder come schemi generali (template) da adattare; complessità Theta(n + somma dei costi di visita) perché la somma dei figli è n-1; esempi (indice di un libro, spazio occupato in un file system); profondità con il preorder, altezza con il postorder; antenato comune più basso.Visite di alberi →).

(a) Pseudocodice

Algoritmo levelNumbering(T, v, idx)
Input: albero binario proprio T, nodo v di T, intero idx = indice che v deve avere
Output: nessuno; side effect: v.indexLN = idx e lo stesso per ogni discendente di v
v.indexLN <- idx
if T.isInternal(v) then
    levelNumbering(T, T.left(v), 2 * idx)
    levelNumbering(T, T.right(v), 2 * idx + 1)

Chiamata iniziale: levelNumbering(T, T.root(), 1).

Correttezza (induzione sull'altezza di vv). Se vv è una foglia imposta il proprio indice e termina: corretto. Se vv è interno, imposta idx (corretto per ipotesi sul parametro) e per l'ipotesi induttiva le chiamate sui figli assegnano correttamente i sottoalberi con i parametri 2 idx2\,idx e 2 idx+12\,idx + 1, che sono per definizione gli indici dei figli.

(b) Complessità

L'albero della ricorsione ha un nodo per ogni nodo di TT (una chiamata per nodo, esattamente una) e ogni chiamata esegue un numero costante di operazioni, escluse le chiamate figlie: due moltiplicazioni, un'addizione, un'assegnazione e il test. Quindi

t(n)∈Θ(n).t(n) \in \Theta(n).

(Si assume che le operazioni su interi costino Θ(1)\Theta(1): con alberi molto profondi gli indici crescono come 2depth2^{\text{depth}} e superano la parola di macchina.)

Esempio

Albero con radice RR, figlio sinistro XX (interno, con foglie aa e bb) e figlio destro YY (foglia): ℓ(R)=1\ell(R) = 1, ℓ(X)=2\ell(X) = 2, ℓ(Y)=3\ell(Y) = 3, ℓ(a)=4\ell(a) = 4, ℓ(b)=5\ell(b) = 5.

chiamata idx ricevuto v.indexLN assegnato chiamate figlie
levelNumbering(T, R, 1) 1 1 X con 2⋅1=22\cdot1=2, Y con 2⋅1+1=32\cdot1+1=3
levelNumbering(T, X, 2) 2 2 a con 44, b con 55
levelNumbering(T, a, 4) 4 4 nessuna (foglia)
levelNumbering(T, b, 5) 5 5 nessuna
levelNumbering(T, Y, 3) 3 3 nessuna

In binario: ℓ(b)=5=1012\ell(b)=5=101_2, cioè 11 (radice), poi 00 (sinistra, verso XX), poi 11 (destra, verso bb). Gli indici sono tutti distinti perché cammini diversi danno sequenze di bit diverse. Il caso estremo, un cammino tutto a destra di altezza hh, dà 1,3,7,…,2h+1−11, 3, 7, \dots, 2^{h+1}-1 (ogni passo a destra applica x↦2x+1x \mapsto 2x+1, e 1→3→7→151\to3\to7\to15 sono i numeri 2k−12^k-1). Si riconosce la numerazione per livelli usata per l'array di uno heap (vedi HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →): i figli di ℓ\ell sono 2ℓ2\ell e 2ℓ+12\ell + 1. In un albero completo gli indici coincidono con le posizioni in P[1..n]P[1..n]; in un albero qualsiasi possono arrivare a 2h+1−12^{h+1} - 1 (cammino tutto a destra), il che mostra perché l'array è adatto solo agli alberi completi. (Verificato con alberi casuali: gli indici sono tutti distinti.)

Errori comuni

  • Calcolare ℓ(v)\ell(v) risalendo dai nodi al padre: Θ(n⋅h)\Theta(n \cdot h) invece di Θ(n)\Theta(n).
  • Passare il valore del padre senza il fattore 22 (o senza il +1+1 per il figlio destro).
  • Dimenticare la chiamata iniziale con indice 11 (non 00).
  • Chiamare ricorsivamente sui figli anche per una foglia (accede a figli inesistenti).

Versione ripasso

Testo. Albero binario proprio; ℓ(radice)=1\ell(\text{radice}) = 1, figlio sinistro 2ℓ(u)2\ell(u), destro 2ℓ(u)+12\ell(u) + 1. Algoritmo ricorsivo che salva v.indexLN in tutti i nodi, in O(n)O(n).

Esercizi su questo argomento

Teoria collegata