Alberi di ricerc 161 3 5 2 4 8 10 11 28 30 31 45 47 2 3 4 5 8 10 11 15 25 28 30 31 45 47 50 Figur 6.20 Esempio di lbero 2-3. I nodi interni con due figli mntengono il mssimo del sottolbero sinistro (S); i nodi interni con tre figli mntengono il mssimo del sottolbero sinistro e di quello centrle (S ed M). 6.4 Alberi 2-3 Nei Prgrfi 6.2 e 6.3 bbimo visto come usre rotzioni per implementre le operzioni su dizionri in tempo logritmico (nel cso peggiore o mmortizzto). In questo prgrfo studieremo un tecnic lterntiv lle rotzioni, bst sull ide di permettere un mggiore flessibilità nel grdo dei nodi. Se il grdo non è vincolto d essere 2, il bilncimento può inftti essere mntenuto trmite opportune seprzioni (split) e fusioni (fuse) di nodi. Definizione 6.6 (Albero 2-3) Un lbero 2-3 è un lbero che in cui ogni nodo interno h 2 o 3 figli e tutti i cmmini rdice-fogli hnno l stess lunghezz. Dimostrimo innnzitutto un limitzione sull ltezz degli lberi 2-3. Lemm 6.5 Si T un lbero 2-3 con n nodi, f foglie ed ltezz h. Le seguenti disuglinze sono soddisftte d n, f e h: 2 h+1 1 n (3 h+1 1)/2 e 2 h f 3 h. Dimostrzione. Mostrimo l limitzioni contempornemente usndo induzione su h. Se h = 0, l lbero consiste di un singolo nodo, che è nche fogli, e le disuguglinze sono bnlmente verificte. Supponimo or che l ipotesi induttiv si verifict sino d ltezz h e considerimo un lbero 2-3 T di ltezz h+1. Si T ottenuto d T eliminndo l ultimo livello e sino n e f il numero di nodi e di foglie di T, rispettivmente. Per ipotesi induttiv, 2 h+1 1 n (3 h+1 1)/2 e 2 h f 3 h. Poiché ogni fogli di T h lmeno due d l più tre figli in T, vremo 2 2 h f 3 3 h, ovvero 2 h+1 f 3 h+1. Poiché n = n + f, è fcile completre l dimostrzione sfruttndo l ipotesi induttiv su n. Usndo l limitzione sul numero di nodi dimostrt nel Lemm 6.5, e pssndo l logritmo, ottenimo che l ltezz di un lbero 2-3 è Θ(log n). Le chivi e gli elementi del dizionrio sono ssegnti lle foglie dell lbero, in modo che le chivi ppino in ordine crescente d sinistr verso destr. Ogni nodo interno v mntiene invece due informzioni supplementri: S[v] e M[v]. S[v] è l mssim chive nel sottolbero rdicto nel figlio sinistro di v, e M[v] è
162 Cpitolo 6 lgoritmo serch(rdice r di un lbero 2-3, chive x) elem 1. if ( r è un fogli ) then 2. if ( x = chive(r) ) then return elem(r) 3. else return null 4. v i i-esimo figlio di r 5. if ( x S[r] ) then return serch(v 1, x) 6. else if ( r h due figli oppure x M[r] ) then return serch(v 2, x) 7. else return serch(v 3, x) Figur 6.21 Implementzione dell operzione serch in un lbero 2-3. l mssim chive nel sottolbero rdicto nel figlio centrle di v. Un esempio è mostrto in Figur 6.20. Grzie lle informzioni S ed M, un ricerc può essere implementt in un mnier simile gli lberi binri di ricerc clssici. serch(chive k) elem Confrontimo l chive cerct k si con S[v] che con M[v]. Se k S[v] proseguimo nel sottolbero sinistro; se S[v] < k M[v] proseguimo nel sottolbero centrle; ltrimenti proseguimo nel sottolbero destro. Lo pseudocodice è mostrto in Figur 6.21. Il tempo richiesto d serch è proporzionle ll ltezz dell lbero, che è Θ(log n) per il Lemm 6.5. 6.4.1 Fusioni e seprzioni di nodi In questo prgrfo mostreremo come sfruttre le possibili vrizioni di grdo per ggiornre un lbero 2-3 fronte di inserimenti e cncellzioni di elementi. insert(elem e, chive k) Creimo un nuovo nodo u con elemento e e chive k. Loclizzimo l corrett posizione per l inserimento di u ricercndo l chive k nell lbero. Identifichimo così un nodo v, sul penultimo livello, che dovrebbe diventre genitore di u. Abbimo or due csi: v h due figli: possimo ggiungere u come nuovo figlio di v, inserendolo opportunmente come figlio sinistro, centrle, o destro in modo d mntenere l ordinmento crescente delle chivi. Questo può comportre dover ggiornre i vlori S ed M del nodo v e dei suoi ntenti. v h tre figli: non potendo ggiungere un ulteriore figlio v, seprimo il nodo in due, con un operzione chimt split. Creimo un nuovo nodo w e clcolimo l posizione corrett che u dovrebbe vere rispetto i tre figli di v. Rendimo le due foglie con chivi minime figlie di w e le rimnenti due figlie di v. Attcchimo poi w come figlio del pdre di v immeditmente precedente v. Se il pdre di w vev due figli, possimo fermrci. Altrimenti, dobbimo
Alberi di ricerc 163 11 28 3 5 2 4 8 10 2 3 4 5 8 9 10 11 15 25 28 30 31 45 47 30 31 45 47 50 3 5 11 28 2 4 8 10 30 31 45 47 2 3 4 5 8 9 10 11 15 25 28 30 31 45 47 50 3 9 11 28 2 4 8 10 30 31 45 47 2 3 4 5 8 9 10 11 15 25 28 30 31 45 47 50 5 3 9 11 28 2 4 8 10 30 31 45 47 2 3 4 5 8 9 10 11 15 25 28 30 31 45 47 50 Figur 6.22 Inserimento del nodo con chive 9 in un lbero 2-3. eseguire un nuovo split sul pdre di v, procedendo nell lbero verso l lto. Nel cso peggiore, qundo tutti i nodi lungo il cmmino vevno già tre figli, ggiungeremo ll lbero un nuov rdice. I vlori S e M dei nodi incontrti lungo l rislit vnno nch essi ggiornti opportunmente. Un esempio di inserimento è illustrto in Figur 6.22. Lo pseudocodice dell procedur split, richimt su un nodo v con 4 figli, è inoltre mostrto in Figur 6. (lo pseudocodice non mostr come ggiornre i cmpi S ed M di ciscun nodo: ggiungere le opportune istruzioni può essere un utile esercizio). Poiché ciscuno split richiede tempo costnte e l ltezz dell lbero è Θ(log n), nel cso peggiore l inserimento richiede tempo O(log n).
164 Cpitolo 6 lgoritmo split(nodo v) 1. cre un nuovo nodo w 2. si v i l i-esimo figlio di v in T, 1 i 4 3. rendi v 1 e v 2 figli sinistro e destro di w 4. if ( prent[v] = null ) then 5. cre un nuovo nodo r 6. rendi w e v figli sinistro e destro di r 7. else 8. ggiungi w come figlio di prent[v] immeditmente precedente v 9. if ( prent[v] h quttro figli ) then split(prent[v]) Figur 6. Implementzione dell procedur usiliri split ust per implementre l inserimento in un lbero 2-3. delete(elem e) L operzione di cncellzione è simmetric quell di inserimento. Si v il nodo contenente l elemento e d eliminre. Abbimo tre csi: v è l rdice: bst rimuoverl ottenendo un lbero vuoto. Il pdre di v h tre figli: è possibile rimuovere v, ggiornndo eventulmente i cmpi S e M di v e dei suoi ntenti (vedi Figur 6.24). Il pdre di v h due figli: se il pdre di v è l rdice, bst eliminre v e suo pdre, lscindo l ltro figlio come rdice. Altrimenti, eseguimo un operzione simmetric llo split, che chimeremo fuse. Si w il pdre di v; ssumimo che w bbi un frtello l ll su sinistr (nessun nodo può inftti essere figlio unico, e il cso in cui w bbi un unico frtello destr viene trttto in modo b b c b () c d d (b) b c d e c d e c c (c) b c d c d Figur 6.24 Cncellzione d un lbero 2-3: vri csi.
Alberi di ricerc 165 28 38 15 25 28 30 31 35 45 47 30 31 35 38 45 47 50 28 28 38 28 30 31 35 45 47 30 31 35 38 45 47 50 28 38 30 31 35 45 47 28 30 31 35 38 45 47 50 Figur 6.25 Cncellzione del nodo con chive d un lbero 2-3. simile). Se l h tre figli, spostimo il figlio destro di l come figlio sinistro di w e cncellimo v (vedi Figur 6.24b). Altrimenti, dopo ver rimosso v, ttcchimo l unico figlio rimnente di w come figlio destro di l e richimimo ricorsivmente l procedur per cncellre w (vedi Figur 6.24c). Un esempio di cncellzione è mostrto in Figur 6.25. Anche in questo cso, poiché ciscun fuse richiede tempo costnte e l ltezz dell lbero è Θ(log n), nel cso peggiore l cncellzione richiede tempo O(log n). Rissumimo i dettgli sull implementzione dell clsse Albero in Figur 6.26 e le prestzioni degli lberi 2-3 nel seguente teorem. Teorem 6.5 Un lbero 2-3 con n nodi support operzioni serch, insert e delete in tempo O(log n) nel cso peggiore. 6.5 B-lberi In questo prgrfo ffronteremo il problem di come relizzre un dizionrio in memori secondri. Come bbimo visto nel Prgrfo 2.8 del Cpitolo 2, l
166 Cpitolo 6 clsse Albero implement Dizionrio: dti: S(n) = O(n) un lbero 2-3 T con n nodi: le foglie mntengono le chivi e gli elementi del dizionrio, mentre i nodi interni mntengono le informzioni supplementri S e M. operzioni: serch(chive k) elem T (n) = O(log n) trcci un cmmino nell lbero usndo l proprietà di ricerc e le informzioni S e M per decidere se proseguire nel sottolbero sinistro, centrle (se esiste) o destro. insert(elem e, chive k) T (n) = O(log n) cre un nuovo nodo v con elemento e e chive k, e lo ggiunge ll lbero come fogli mntenendo l proprietà di ricerc. Se il pdre di v vev già tre figli, sepr il nodo in due trmite uno split e propg le seprzioni verso l lto, fino l primo nodo con due figli o fino ll crezione di un nuov rdice. delete(elem e) T (n) = O(log n) elimin il nodo v con elemento e. Se il pdre w di v vev solo due figli, uniscilo l frtello trmite un operzione fuse e propg le fusioni verso l lto, fino l primo nodo con tre figli o fino ll cncellzione dell rdice. Figur 6.26 Dizionrio relizzto medinte lberi 2-3. memori secondri è tipicmente molto più grnde di quell principle, m ccedervi è molto più costoso in termini di tempo. È quindi importnte minimizzre il numero di letture e scritture su memori estern (I/O). Per mmortizzre il tempo speso in ogni ccesso, i dti sono gestiti in blocchi di dimensione B: per poter sfruttre questo ftto, è importnte che un lgoritmo o un struttur dti esibisc loclità nell ccesso i dti. Le implementzioni dei dizionri proposte nei prgrfi precedenti non esibiscono buon loclità: intuitivmente, non offrono nessun grnzi che i nodi su un certo cmmino di ricerc risiedno nello stesso blocco, e quindi si potrebbero vere Θ(log n) I/O nel cso peggiore. Come vedremo, è possibile fre molto meglio. L ide è di umentre l informzione crico di ciscun nodo ed il grdo del nodo rendendoli proporzionli B. In questo modo, potremo usre tutt l informzione contenut in ogni blocco che porteremo in memori principle, e, poiché diminuiremo considerevolmente l ltezz dell lbero, vremo un numero minore di ccessi memori secondri. I B-lberi che presentimo in questo prgrfo sono dovuti Byer e McCreight [4] e sono un estensione nturle degli lberi 2-3 che bbimo descritto nel Prgrfo 6.4.
Alberi di ricerc 167 6.5.1 Definizioni e proprietà Si t un intero fissto 2, detto grdo minimo e tipicmente scelto in modo proporzionle ll dimensione B di un blocco. Definizione 6.7 (B-lbero) Un B-lbero non vuoto di grdo minimo t è un lbero rdicto con le seguenti proprietà: 1. tutte le foglie hnno l stess profondità; 2. ogni nodo v diverso dll rdice mntiene k(v) chivi ordinte, chive 1 (v) chive 2 (v)... chive k(v) (v), tli che t 1 k(v) 2t 1; 3. l rdice mntiene lmeno 1 ed l più 2t 1 chivi ordinte; 4. ogni nodo interno v h k(v) + 1 figli; 5. le chivi chive i (v) seprno gli intervlli di chivi memorizzti in ciscun sottolbero: se c i è un qulunque chive nell i-esimo sottolbero di un nodo v, llor c 1 chive 1 (v) c 2 chive 2 (v)... c k(v) chive k(v) (v) c k(v)+1. Diremo che un nodo è pieno se contiene esttmente (2t 1) chivi e qusi vuoto se ne contiene (t 1). Dlle Proprietà 2 e 4 segue che il grdo di ogni nodo interno diverso dll rdice è compreso tr t e 2t (il grdo minimo dell rdice può invece scendere fino 2 in bse ll Proprietà 3). Esminimo or l ltezz di un B-lbero. Lemm 6.6 Ogni B-lbero di ltezz h con n chivi soddisf h log t n+1 2. Dimostrzione. L ltezz mssim, prità di numero dei nodi, si rggiunge qundo ogni nodo h grdo minimo. Assumimo quindi che l rdice bbi 2 figli e che ogni ltro nodo interno ne bbi t. Avremo un nodo profondità 0 (l rdice), due nodi profondità 1 (i figli dell rdice), 2t nodi profondità 2, 2t 2 nodi profondità 3, e così vi. In generle, il numero dei nodi l livello i è t volte il numero dei nodi l livello i 1, ed è pri 2t i 1. Poiché le chivi sono n ed ogni nodo ne contiene t 1, l seguente disuguglinz deve essere soddisftt: 1 + (t 1) h 2t i 1 n i=1 Usndo l serie geometric, sppimo che h h 1 t i 1 = t i = th 1 t 1 i=1 i=0 d cui si ottiene: 1 + 2(t 1) th 1 t 1 n
168 Cpitolo 6 lgoritmo serch(rdice v di un B-lbero, chive x) elem 1. i 1 2. while ( i k(v) e x > chive i (v) ) do i i + 1 3. if ( i k(v) e chive i (v) = x ) then return elem i (v) 4. if ( v è fogli ) then return null 5. else return serch(i-esimo f iglio di v, x) Figur 6.27 Implementzione dell operzione serch in un B-lbero. e quindi t h n 1 + 1 = n + 1 2 2 Pssndo l logritmo in bse t, ottenimo l limitzione nell enuncito. serch(chive k) elem Possimo implementre l operzione serch su un B-lbero bsndoci sul ftto che l Proprietà 5 dell Definizione 6.7 è un generlizzzione dell proprietà di ricerc introdott nel Prgrfo 6.1: invece di prendere un decisione binri, prenderemo d ogni psso un decisione Θ(t)-ri, poichè ogni nodo h Θ(t) figli. Al generico psso, trovndoci su un nodo v, identifichimo l più piccol chive di v mggiore dell elemento cercto x: se non esiste, proseguimo sull ultimo figlio di v, ltrimenti proseguimo sul figlio immeditmente precedente l chive individut. Lo pseudocodice è mostrto in Figur 6.27. Nel cso peggiore, poiché d ogni chimt si scende di un livello, il numero di chimte ricorsive è pri ll ltezz dell lbero, e quindi O(log t n). Se ogni nodo ttrversto entrsse intermente in un blocco, il numero di I/O srebbe quindi log t n, che può essere sostnzilmente inferiore rispetto log 2 n. Il vntggio mggiore si ottiene proprio scegliendo t proporzionle B, nel qul cso vremo log B n I/O. Considerimo or il numero di operzioni. Osservimo che su ogni nodo, nell implementzione dt, spedimo tempo O(t). Essendo le chivi ordinte, però, potremmo effetture un ricerc binri ll interno del nodo, il che richiederebbe tempo O(log t). Con questo ccorgimento, eseguiremmo O(log t log t n) operzioni elementri. Usndo le regole del cmbimento di bse dei logritmi, è fcile vedere che quest quntità è O(log n), esttmente come negli lberi AVL o 2-3. 6.5.2 Inserimenti e cncellzioni di chivi In questo prgrfo mostreremo come inserire nuovi elementi e come cncellre elementi esistenti ripristinndo le proprietà dei B-lberi nell Definizione 6.7.
Alberi di ricerc 169 f d f i k d g i k g Figur 6.28 Split in un B-lbero con grdo minimo t = 3. insert(elem e, chive k) Per inserire chivi fremo uso di un operzione split nlog quell vist nel Prgrfo 6.4.1 per gli lberi 2-3. In prticolre, un split viene eseguit ogni qulvolt si richiede di ggiungere un nuov chive d un nodo pieno (ovvero già contenente 2t 1 chivi). Il nodo verrebbe inftti contenere 2t chivi, violndo l Proprietà 2. È però possibile dividere il nodo in due soddisfcendo loclmente i vincoli ed eventulmente propgndo il problem verso l lto. Lo split è mostrto in Figur 6.28: il nodo sinistro contiene le t 1 chivi minime e quello destro le t chivi mssime, mentre l chive t viene spint verso l lto per seprre i due nodi. Osservimo che l operzione split può essere implementt eseguendo O(1) ccessi disco e in tempo Θ(t). Dt l operzione split, l operzione insert oper come segue. Dopo ver identificto, trmite un ricerc che termin con un insuccesso, l fogli f in cui l chive k e l elemento e ndrebbero ggiunti, si possono verificre due csi: se l fogli non è pien, si inserisce l chive nell opportun posizione e l lgoritmo termin; se l fogli è pien si esegue uno split promuovendo l t-esim chive, dicimo x, l livello superiore. L chive x viene ggiunt l pdre dell fogli f nell posizione opportun, con i punttori sinistr e destr che puntno i due nuovi nodi creti. Potrebbe ccdere che nche il pdre di f si pieno, e in tl cso si eseguirà uno split fcendo rislire un chive x l livello superiore. Nel cso peggiore, qundo tutti i nodi lungo il cmmino erno già pieni, dovremo ggiungere ll lbero un nuov rdice contenente un sol chive. In quest implementzione vengono effettute due psste sui nodi dell lbero, un dll lto verso il bsso per l ricerc dell fogli, e l ltr dl bsso verso l lto per eventuli split. Poiché l ltezz dell lbero è O(log t n) e su ogni nodo vengono eseguiti solo O(1) I/O, il numero totle di I/O è O(log t n). Diversmente dll operzione serch, il tempo di esecuzione è invece O(t log t n) cus delle operzioni split. In prtic, è possibile implementre l operzione insert in modo d diminuire di un fttore moltiplictivo costnte il numero di I/O: nell implementzione più efficiente viene effettut un sol psst nell lbero dll lto verso il bsso, e viene eseguit un split su tutti i nodi pieni incontrti.
170 Cpitolo 6 clsse BAlbero implement Dizionrio: dti: S(n) = O(n) un intero t 2 e un B-lbero T di grdo minimo t con n nodi. operzioni: serch(chive k) elem O(log t n) I/O, T (n) = O(log n) trcci un cmmino nell lbero prendendo un decisione Θ(t)-ri su ogni nodo, in bse ll Proprietà 5 nell definizione di B-lbero. insert(elem e, chive k) O(log t n) I/O, T (n) = O(t log t n) identific trmite un ricerc l fogli f in cui l elemento v inserito, in modo d mntenere l proprietà di ricerc. Se l fogli è pien, esegui uno split promuovendo l t-esim chive verso l lto. Se tutti i nodi nel cmmino d f ll rdice sono pieni, cre un nuov rdice. delete(elem e) O(log t n) I/O, T (n) = O(t log t n) si v il nodo contenente l elemento d eliminre. Se v non è un fogli, elimin il predecessore, dopo ver copito chive ed elemento di quest ultimo in v. Altrimenti elimin v: se il pdre w di v er qusi vuoto, esegui un operzione fuse e propg le fusioni verso l lto, fino l primo nodo con più di t figli o fino ll cncellzione dell rdice. Figur 6.29 Dizionrio relizzto medinte B-lberi di grdo minimo t. delete(elem e) Per cncellre chivi fremo uso di un operzione fuse nlog quell vist nel Prgrfo 6.4.1 per gli lberi 2-3. Si u il nodo contenente l elemento e d cncellre. Se u non è un fogli, trovimo il suo predecessore y, rimpizzimo chive(u) ed elem(u) con chive(y) ed elem(y), rispettivmente, e ricorsivmente cncellimo y, che è necessrimente contenuto in un fogli. Vedimo quindi come cncellre un elemento d un fogli: se l fogli non è qusi vuot (ovvero contiene lmeno t chivi), è sufficiente cncellre l elemento; se l fogli è qusi vuot, esminimo l situzione del frtello sinistro e destro: se uno dei due frtelli non è qusi vuoto, operimo un redistribuzione degli elementi tr frtelli in modo d soddisfre i vincoli di pienezz, modificndo nche l chive seprtrice contenut nel pdre; se entrmbi i frtelli sono qusi vuoti, operimo un operzione di fuse: ggiungimo l frtello, mntenendo l ordine, si le chivi dell fogli si l chive seprtrice. Eliminimo poi l fogli. In tl modo il numero di figli del pdre, così come le sue chivi, diminuisce di 1. Se il pdre er qusi vuoto, e quindi or h t 2 chivi, dobbimo propgre le redistribuzioni di chivi o fusioni verso l lto. Nel cso peggiore, qundo tutti i nodi lungo il cmmino
Alberi di ricerc 171 ed i loro frtelli erno qusi vuoti pieni, l rdice dell lbero srà elimint e l ltezz diminuirà di 1. Anche in questo cso si può dimostrre che il numero di I/O ed il tempo di esecuzione sono rispettivmente O(log t n) ed O(t log t n). Anlogmente ll operzione insert, è possibile fornire un implementzione dell operzione delete che esegue un sol psst nell lbero dll lto verso il bsso. Rissumimo l implementzione dell clsse BAlbero in Figur 6.29 e le prestzioni dei B-lberi nel seguente teorem: Teorem 6.6 Un B-lbero con grdo minimo t ed n nodi è in grdo di supportre le operzioni serch, insert e delete con O(log t n) I/O. L operzione serch h tempo di esecuzione O(log n), mentre insert e delete hnno tempo di esecuzione O(t log t n). 6.6 Alberi 2-3-4 e lberi rosso-neri Nei precedenti prgrfi bbimo descritto due diverse tipologie di lberi di ricerc. Gli lberi AVL e gli lberi uto-ggiustnti sono lberi di ricerc binri, e mntengono il bilncimento durnte operzioni insert e delete trmite opportune rotzioni. Gli lberi 2-3 ed i B-lberi sono invece crtterizzti dll vere nodi con grdo vribile, e proprio l vribilità del grdo viene sfruttt per dividere o fondere nodi mntenendo il bilncimento. In questo prgrfo mostreremo che esiste un strett relzione tr i due pprocci, presentndo un prospettiv unifict l problem di mntenere il bilncimento dell lbero. A tl fine, descriveremo due nuovi tipi di lberi di ricerc, gli lberi 2-3-4 e gli lberi rosso-neri, mostrndo un modo per trsformre gli uni negli ltri. Alberi 2-3-4. B-lbero. Un lbero 2-3-4 può essere fcilmente definito prtire d un Definizione 6.8 (Albero 2-3-4) Un lbero 2-3-4 è un B-lbero vente grdo minimo 2. In bse lle Definizioni 6.7 e 6.8, in un lbero 2-3-4 il grdo di ogni nodo diverso dll rdice può quindi vrire d 2 4, ed il nodo può contenere d 1 3 chivi. Le operzioni serch, insert e delete sono implementte esttmente come nei B-lberi, usndo come sottoprocedure le operzioni split e fuse. Rimndimo l Prgrfo 6.5 per mggiori dettgli.