Esercizi di Algoritmi e strutture dati II: B-Alberi. A cura di: Teresa E. NARDIELLO Esercizio 19.1-2 Per quali valori di t l albero in figura è un B-Albero legale? M D H Q T X B C F G J K L N P R S V W Y Z Per t=2 abbiamo: Se x=root 1 <= n(x) <= 3 ok! 2 <= child(x) <= 4 ok! Se x è diverso da root 1 <= n(x) <= 3 ok! 2 <= child(x) <= 4 ok! Per t=3 abbiamo: Se x=root 1 <= n(x) <= 5 ok! 2 <= child(x) <= 6 ok! Se x è diverso da root 2 <= n(x) <= 5 ok! 3 <= child(x) <= 6 ok! Per t=4 abbiamo: Se x=root 1 <= n(x) <= 7 ok! 2 <= child(x) <= 8 ok! Se x è diverso da root 3 <= n(x) <= 7 no! In questo caso c è una violazione della proprietà per cui t-1<=n(x)<=2t-1.
Esercizio 19.1-3 Si mostrino tutti i B-Alberi legali con grado minimo uguale a 2 che rappresentano l insieme {1,2,3,4,5}. Se t=2 allora 1<= n(x)<=3 quindi avremo i seguenti alberi: 2 3 4 1 3 4 5 1 2 4 5 1 2 3 5 2 4 1 3 5 Tali alberi sono tutti legali nel senso che rispettano la proprietà espressa sopra. Se proviamo a mettere 1 oppure 5 nella radice abbiamo: 1 5? 2 3 4 5 1 2 3 4? In questi due casi non riusciamo a costruire i B-Alberi.
Esercizio 19.1-4 Si derivi un limite superiore stretto del numero delle chiavi che possono essere memorizzate in un B-Albero di altezza h in funzione del grado minimo t. Abbiamo un B-Albero di altezza h, con grado minimo t, quello che vogliamo sapere è come possiamo limitare n, ovvero, n<=f(t)? Dalle proprietà dei B-Alberi sappiamo che: quando x è diverso dalla radice vale la seguenti proprietà t-1< n(x) <= 2t-1 quando x è uguale alla radice vale la seguente proprietà 1 <= n(root) <= 2t-1 Il numero massimo di chiavi per qualsiasi nodo è sempre 2t-1, e il numero massimo di figli per nodo è 2t. Un B-Albero massimale è allora il seguente: 2t 2t 2t 2t Come possiamo osservare dalla figura abbiamo: PROFONDITA NUMERO NODI 0 1
1 2t 2 (2t) 2 3 (2t) 3 Proseguendo in questo modo a profondità h avremo un numero di nodi pari a (2t) h. L albero risultante è un albero massimale in cui avremo: h Numero nodi totale = (2t) i = (2t) h+1-1/(2t-1) i=0 Numero chiavi totale = numero dei nodi totale * (2t-1) = (2t) h+1-1. Infine siamo giunti alla conclusione che n<=(2t) h+1-1. Esercizio 19.1-5 Si descriva quale tipo di struttura di dati si otterrebbe se ogni nodo nero di un albero RB incorporasse i suoi figli rossi, fondendo i loro figli con i figli del nodo rosso. Consideriamo un esempio: 26 17 41 14 21 30 47 10 16 19 23 28 38 7 12 15 20 35 39 3
In tale struttura abbiamo omesso le foglie e abbiamo distinto i nodi rossi etichettandoli con la chiave in rosso, analogamente per i nodi neri. Vediamo cosa accade se ogni nodo nero dell albero RB incorpora i suoi figli rossi e fonde con i suoi figli i figli del nodo rosso: 17 26 10 14 21 30 41 3 7 12 15 16 19 20 23 28 35 38 39 47 Alla fine otteniamo un B-Albero di grado t=2. Vediamo se sono verificate le proprietà dei B-Alberi: 1) Ogni nodo x è caratterizzato dai seguenti campi o attributi: a. n[x] è il numero delle chiavi memorizzate nel nodo x b. le n[x] chiavi sono memorizzate in ordine non decrescente: key 1 [x]<=key 2 [x]<=.<=key n[x] [x] c. leaf[x] è un valore di verità che è uguale a TRUE se il nodo x è una foglia, è uguale a FALSE altrimenti. Verifichiamola: 1.a: Ogni nodo x dell albero risultante dalla fusione di nodi rossi e neri contiene all inizio sicuramente una sola chiave che è quella che era presente nell albero RB di partenza, quindi n[x]=1; tale valore viene poi incrementato ogni qual volta un nodo nero incorpora i suoi figli rossi. 1.b: Possiamo facilmente avere la seconda proprietà sfruttando il fatto che un albero RB è anche un albero binario di ricerca e quindi se il nodo y(che viene incorporato da un nodo nero) apparteneva al sottoalbero sinistro del nodo x(nodo nero) allora key[y]<=key[x]. Viceversa se il nodo y apparteneva al sottoalbero destro per cui key[x]<=key[y]. Utilizzando tali proprietà degli alberi RB possiamo incorporare i nodi rossi seguendo l ordinamento delle chiavi e quindi inserendole in modo non decrescente.
1.c: Quando un nodo nero incorpora un nodo rosso andiamo a considerare il puntatore al figlio, se il figlio non esiste allora impostiamo il valore di leaf[nodo]=true altrimenti sarà leaf[nodo]=false. 2) Un nodo interno x contiene n[x]+1 puntatori c 1 [x],..,c n[x]+1 [x], ai suoi figli. Le foglie non hanno figli e quindi i campi c i delle foglie sono sempre indefiniti. Verifichiamola: Poiché ogni nodo di un albero RB ha al massimo due figli rossi di cui uno appartenente al sottoalbero di sinistra e l altro al sottoalbero di destra allora nel caso in cui incorporiamo due nodi rossi uno sarà indicato dal puntatore c 1 [x] e l altro da c 3 [x]. Nel caso in cui assorbiamo un solo nodo rosso allora sarà il valore della sua chiave a dirci in quale posizione sarà inserito e quindi quale indice i assumerà il puntatore c i [x]. 3) I campi key i [x] definiscono gli intervalli delle chiavi memorizzate in ciascun sottoalbero: se k i è una qualunque chiave memorizzata nel sottoalbero di radice c i [x] allora vale che k 1 <=key 1 [x]<=...<=key n[x] [x]<=key n[x]+1 [x]. Verifichiamola: Questa proprietà è una diretta conseguenza della proprietà di ordinamento parziale presente negli alberi RB che viene mantenuta e rispettata quando incorporiamo dei nodi rossi così come descritto in precedenza. 4) Tutte le foglie hanno la stessa profondità che coincide con l altezza dell albero. Verifichiamola: Ricordiamo che in ogni albero RB ogni cammino semplice da un nodo ad una foglia sua discendente ha lo stesso numero di nodi. Quindi i nodi foglia avranno tutti la stessa profondità che coincide con l altezza nera dell albero RB di partenza. 5) Il numero delle chiavi che può essere memorizzato in un nodo è limitato sia inferiormente che superiormente. Questi limiti possono essere espressi in termini di un intero t>=2 chiamato il grado minimo del B-Albero: a. Ogni nodo, con la sola eccezione della radice, deve contenere almeno t-1 chiavi. Ogni nodo interno, con la sola eccezione della radice,
deve contenere almeno t figli. Se l albero non è vuoto, la radice deve avere almeno una chiave. b. Ogni nodo può contenere al massimo 2t-1 chiavi. Quindi un nodo interno può avere al massimo 2t figli. Diciamo che un nodo è pieno se contiene esattamente 2t-1 chiavi. Verifichiamola: 5.a: Se il nodo x è la radice dell albero allora al massimo può avere incorporato due figli rossi, quindi avrà un numero di chiavi n[x] che sarà limitato da 1<=n[x]<=3 (1<=n[x]<=2t-1 in questo caso t=2 ok!). Se il nodo x non è la radice dell albero allora come prima abbiamo la limitazione 1<=n[x]<=3 (t-1<=n[x]<=2t-1 ok!). Tutto questo dimostra la proprietà 5.a. 5.b: Per quanto riguarda i figli di un nodo x come sopra distinguiamo il caso in cui si tratti della radice oppure no. Se il nodo x è la radice dell albero allora abbiamo la limitazione 2<= child(x) <=2t. Come prima la radice può aver assorbito uno o due figli rossi quindi avrà un numero di puntatori ai figli che sarà pari a n[x]+1, nel nostro caso 2<= child(x) <=4 in cui t=2 ok!. Se il nodo x non è la radice dell albero il discorso è analogo solo che questa volta la limitazione è t<= child(x) <=2t e nel nostro caso abbiamo 2<= child(x) <=4 ok! Anche la proprietà 5.b è rispettata. Esercizio 19.1-5 Si spieghi come trovare la chiave più piccola presente in un B-Albero e come trovare il predecessore di una determinata chiave memorizzata in un B-Albero. Per trovare la chiave più piccola in un B-Albero scendiamo sempre a sinistra e nell ultimo nodo consideriamo la prima chiave a sinistra. Per trovare il predecessore di una determinata chiave useremo come esempio l albero dell esercizio precedente. Supponiamo di avere come parametri della procedura PRED(x,i) in cui la variabile x indica il nodo in cui si trova la chiave k di cui vogliamo conoscere il predecessore e la variabile i che indica la posizione della chiave k all interno del nodo x in modo tale che key(x)=k. Distinguiamo vari casi:
1)Se leaf(x)=f vuol dire che il nodo x non è una foglia, allora per trovare il predecessore di k, avendo PRED(x,i) faremo max(c i (x)), ovvero, andiamo a trovare il massimo del sottoalbero di sinistra. 2)Se leaf(x)=t vuol dire che il nodo x è una foglia e dobbiamo andare a distinguere vari sottocasi: Se i è diverso da 1 allora il predecessore sarà la chiave key i-1 (x). (Esempio:nell albero precedente il predecessore di 38 è 35) Se i è uguale a 1, allora il nodo x sarà figlio di qualche nodo y tale che x=c j (y), quindi esaminiamo il valore di j. 1. Se j>1 allora il predecessore sarà nella chiave key j-1 (y). (Esempio:nell albero precedente il predecessore di 35 è 30 infatti i=1, j=2 => j>i allora Pred(35)=key j-1 (y)=key 1 (y)=30) 2. Se j è uguale a 1 come prima andiamo a vedere nel padre del nodo x se troviamo il predecessore. Quando i=1 e j=1 allora il predecessore si trova risalendo nell albero fino ad incontrare un nodo y tale che c k (y) abbia un indice k diverso da 1 in modo tale che key k-1 (y) è il predecessore cercato. (Esempio: nell albero di prima il predecessore di 3 non esiste, in questo caso i=1, j=1 ma x è l ultimo nodo a sinistra, il che significa che la prima chiave è il minimo dell albero.) (Esempio: il predecessore di 28 nell albero di prima si trova risalendo fino alla radice che contiene la chiave 26). Esercizio 19.2-6 Supponiamo che la procedura B-TREE-SEARCH sia realizzata in modo tale da utilizzare in ogni nodo la ricerca binaria invece della ricerca lineare. Si mostri che questa modifica permette di ottenere un tempo di CPU di O(lg n) indipendentemente dalla scelta di t in funzione del valore di n. Nel caso della ricerca binaria avremo che il tempo di CPU è: O(log t n * log 2 t) = O(log 2 n) perché log t n = log 2 n/log 2 t.
Esercizio 19.3-1 Si mostri il risultato dell eliminazione delle chiavi C,P,V dal seguente albero: E L P T X A C J K N O Q R S U V Y Z x Si osservi che t=3 e si ricordi che la funzione DELETE viene chiamata su un nodo solo se il numero delle chiavi è almeno uguale al grado minimo. DELETE(root,C) Poiché il nodo x a cui appartiene la chiave C ha t-1 chiavi e nessun fratello gliele può prestare allora eseguiamo una fusione. Per prima cosa facciamo scendere la chiave E nel nodo x, effettuiamo una fusione col nodo fratello di x e poi invochiamo la funzione DELETE(x,C). L P T X A E J K N O Q R S z U V Y Z DELETE(root,P) Il nodo in cui è presente il successore di P ha un numero di chiavi sufficienti per poter chiamare la procedura quindi chiamiamo la funzione DELETE sul nodo z a cui appartiene la chiave Q=SUCC(P); in seguito metteremo nel nodo root al posto della chiave P la chiave Q.
L Q T X A E J K N O R S U V Y Z DELETE(root,V) Nel nodo a cui appartiene V non ci sono abbastanza chiavi e nessuno dei fratelli del nodo è in grado di prestargliele quindi facciamo una fusione tra i nodi RS e UV; facciamo scendere la chiave T dalla radice nel nodo in cui è presente V ed infine invochiamo la procedura DELETE(y,V) dove y è il nuovo nodo ottenuto dalla fusione. Fusione tra i nodi RS e UV con relativo inserimento della chiave T: L Q X A E J K N O R S T U V Y Z y DELETE(y,V) L Q X A E J K N O R S T U Y Z