RB-alberi (Red-Black trees) Proprietà degli RB-alberi Rotazioni Inserimento Rimozione Proprietà degli RB-alberi • Un RB-albero (Red-black tree) è un albero binario di ricerca dove ogni nodo ha in aggiunta un campo per memorizzare il suo colore: RED (rosso) o BLACK (nero). • La colorazione avviene mediante regole precise, che assicurano che nessun cammino dalla radice ad una foglia risulti lungo più del doppio di qualsiasi altro. • Si ottiene così che l’albero è abbastanza bilanciato. • Ciascun nodo dell’albero contiene i campi color, key, left, right, e p. Proprietà degli RB-alberi • Un RB-albero (red-black tree) soddisfa le seguenti proprietà: 1. Ciascun nodo è rosso o nero. 2. Ciascuna foglia (NIL) è nera. 3. Se un nodo è rosso allora entrambi i suoi figli sono neri. 4. Ogni cammino da un nodo ad una foglia sua discendente contiene lo stesso numero di nodi neri. Dalla proprietà 4 si definisce la b-altezza di un nodo x. bh(x) = numero di nodi neri su un cammino da un nodo x, non incluso, ad una foglia sua discendente. Nota: Un nodo nero può avere figli rossi o neri. Tutti i nodi interni hanno due figli. Red-Black Albero root[T] 9 5 15 4 7 12 19 NIL 2 NIL NIL 8 6 NIL NIL NIL NIL 11 NIL NIL 13 NIL NIL NIL NIL Altezza di RB-albero Un RB-albero con n nodi interni ha un’altezza di al più 2 lg(n+1). Dim.: Si ha che ogni sottoalbero con radice in x contiene almeno 2bh(x)-1 nodi interni. Dimostrazione per induzione: Se bh(x)=0, x è una foglia e si ha 20-1=1-1=0. Supponiamo vera per bh(x)=k-1, allora consideriamo un nodo x con bh(x)=k e k>0. x è un nodo interno (k>0) e ha due figli con b-altezza bh(x) o bh(x)-1. Allora i nodi interni nel sottoalbero con radice in x sono almeno (2bh(x)-1-1)+(2bh(x)-1-1)+1= 2bh(x)-1. Altezza di RB-albero Dim. (prosegue): Sia h l’altezza del RB-albero. Per la proprietà 3 almeno metà dei nodi sono neri. Quindi la b-altezza della radice è almeno h/2. Dunque, i nodi interni n sono almeno 2h/2-1. 2h/2-1 ≤ n h ≤ 2 lg(n+1). Operazioni su un RB-albero •Le operazioni di SEARCH, PREDECESSOR, MINIMUM e MAXIMUM sono quelle viste per un albero binario di ricerca. Possono essere eseguite in un RB-albero con un tempo pari a O(h) = O(lg(n)). •Le operazioni di INSERT e DELETE si complicano, poiché devono tener conto delle proprietà aggiuntive del RB-albero. Più precisamente si devono effettuare dei cambiamenti in modo da ripristinare le regole di colorazione dei nodi. •Tuttavia, RB-INSERT() e RB-DELETE() possono essere eseguite in tempo O(lg(n)). Rotazioni • Le rotazioni sono delle operazioni che cambiano la struttura dei puntatori di due nodi (padre e figlio). • E’ un’operazione locale dell’albero di ricerca che non modifica l’ordinamento delle chiavi. • Questa operazione sono utilizzate da INSERT e DELETE per ripristinare le proprietà violate degli RB-alberi mantenendo l’albero sempre un albero binario di ricerca. Rotazioni y x c a b x RIGHT-ROTATE(T,y) LEFT-ROTATE(T,x) a≤x≤b≤y≤c y a b c I due nodi interni x e y potrebbero essere ovunque nell’albero. Le lettere a, b e c rappresentano sottoalberi arbitrari. Un’operazione di rotazione mantiene l’ordinamento delle chiavi secondo la visita inorder. Rotazioni LEFT-ROTATE(T,x) 1. y ← right[x] 2. right[x] ← left[y] 3. if left[y] ≠ NIL 4. then p[left[y]] ← x 5. p[y] ← p[x] 6. if p[x] = NIL 7. then root[T] ← y 8. else if x = left[p[x]] 9. then left[p[x]] ← y 10. else right[p[x]] ← y 11. left[y] ← x 12. p[x] ← y // inizializzazione di y // inizio rotazione: sposta b // metti y al posto di x // se x era la radice // modifica puntatore di p[x] // metti x a sinistra di y Si opera solo sui puntatori lasciando inalterati gli altri campi. RIGHT-ROTATE() è un procedura simmetrica. Entrambe le procedure sono eseguite in un tempo costante, quindi O(1). Inserimento RB-INSERT(T,x) 1. TREE-INSERT(T,x) 2. color[x] ← RED // ins. come albero b. di ricerca // la proprietà 3 potrebbe essere violata p[x] = RED Si usa la procedura TREE-INSERT per inserire il nodo x nel RB-albero, visto che è un albero binario. Il nodo x viene colorato di rosso (i due figli NIL sono neri). Proprietà 1 e 2 sono soddisfatte: il nuovo nodo è rosso e ha due figli NIL neri. Proprietà 4 si mantiene il nuovo nodo è rosso e non aumenta la b-altezza fino ai suoi due figli neri. Potrebbe essere violata la proprietà 3, che dice che un nodo rosso non può avere figli rossi. Inserimento 3. while x ≠ root[T] and color[p[x]] = RED // finché prop. 3 violata 4. do if p[x] = left[p[p[x]]] // 3 casi: p[x] sinistra di p[p[x]] 5. then y ← right[p[p[x]]] 6. if color[y] = RED // caso 1: p[x] e fratello y RED 7. then color[p[x]] ← BLACK // y “zio” di x 8. color[y] ← BLACK 9. color[p[p[x]]] ← RED 10. x ← p[p[x]] Il ciclo while continua ad essere eseguita finché la proprietà 3 risulta invariata: x e p[x] sono entrambi RED. Lo scopo è quello di far “risalire” nell’albero questa violazione, mentre tutte le altre proprietà rimangono valide. Dopo ogni iterazione: o il puntatore x risale l’albero o viene eseguita qualche rotazione ed il ciclo termina. Inserimento Ci sono 3 casi + altri 3 simmetrici (la linea 4 li suddivide). Caso 1: y il fratello di p[x] è esso stesso RED. Quindi p[p[x]] è BLACK. Possibile nuova violazione tra p[p[x]] e suo padre: se entrambi RED! p[p[x]] p[x] x p[p[x]] y p[x] x y Inserimento 11. 12. 13. else if x = right[p[x]] // caso 2: zio y BLACK then x ← p[x] // e x a destra di p[x] LEFT-ROTATE(T,x) Caso 2: lo zio di x è BLACK e x è figlio destro di p[x]. Quindi, p[p[x]] è BLACK (unica violazione: tra x e p[x]!). bh(a) = bh(b) = bh(c) p[p[x]] Caso 3 x ← p[x] y p[x] x LEFT-ROTATE(T,x) y x y a c x b c a b Inserimento color[p[x]] ← BLACK // caso 3: zio y BLACK color[p[p[x]]] ← RED // e x a sinistra di p[x] RIGHT-ROTATE(T,p[p[x]]) 14. 15. 16. Caso 3: lo zio y di x è BLACK e x (RED) è figlio sinistro di p[x] (RED). Quindi, p[p[x]] è BLACK (unica violazione: tra x e p[x]!). p[p[x]] color[p[x]] ← B p[p[x]] color[p[p[x]]] ← R p[x] RIGHT-ROTATE(T,p[p[x]]) p[x] y p[x] p[p[x]] y x c x a b c x a a b c b bh(a) = bh(b) = bh(c) = bh(y) y Inserimento Il Caso 2 si trasforma nel Caso 3. La proprietà 4 si preserva. A questo punto (Caso 3) si deve effettuare alcuni cambiamenti di colore ed una rotazione destra che preserva la proprietà 4. Dopo il Caso 3 il ciclo while non è più ripetuto, in quanto p[x] è nero. A questo punto tutte le proprietà sono rispettate. Inserimento 17. else (altri 3 Casi simmetrici: scambia “right” e “left”) 18. color[root[T]] ← BLACK // nel caso x risalga fino alla radice La linea 17 è l’inizio degli altri 3 Casi simmetrici. Il codice risulta uguale, basta scambiare “right” con “left” e viceversa. L’ultima riga è importante: la radice risulta sempre nera. Se x non è la radice e p[x] risulta rosso, allora p[x] non è esso stesso la radice dell’albero e, quindi, esiste p[p[x]]. Questa condizione è fondamentale per il corretto funzionamento dell’algoritmo. Inserimento RB-INSERT(T,x) 1. TREE-INSERT(T,x) // ins. come albero b. di ricerca 2. color[x] ← RED // la proprietà 3 violata? 3. while x ≠ root[T] and color[p[x]] = RED // finché prop. 3 violata 4. do if p[x] = left[p[p[x]]] // 3 casi: p[x] sinistra di p[p[x]] 5. then y ← right[p[p[x]]] 6. if color[y] = RED // caso 1: p[x] e fratello y RED 7. then color[p[x]] ← BLACK // y “zio” di x 8. color[y] ← BLACK 9. color[p[p[x]]] ← RED 10. x ← p[p[x]] 11. else if x = right[p[x]] // caso 2: zio y BLACK 12. then x ← p[x] // e x a destra di p[x] 13. LEFT-ROTATE(T,x) 14. color[p[x]] ← BLACK // caso 2: zio y BLACK 15. color[p[p[x]]] ← RED // e x a sinistra di p[x] 16. RIGHT-ROTATE(T,p[p[x]]) 17. else (altri 3 Casi simmetrici: scambia “right” e “left”) 18. color[root[T]] ← BLACK // nel caso x risalga fino alla radice Inserimento Analisi del tempo di esecuzione: • Dato che l’altezza di un RB-albero di n nodi è O(lg(n)), la chiamata TREE-INSERT() costa tempo O(lg(n)). • Il ciclo while è ripetuto solo se si esegue il Caso 1 con il conseguente spostamento verso la radice di x. • Per cui il numero massimo di volte che il ciclo while può essere ripetuto è O(lg(n)). • Ogni ciclo while è eseguito in tempo costante. • Quindi, RB-INSERT() impiega un tempo totale pari a O(lg(n)). Inserimento Un esempio pratico root[T] RB-INSERT(T,x) key[x] = 5 11 4 5 15 3 13 NIL 2 NIL 7 8 6 NIL NIL NIL NIL NIL NIL 19 NIL NIL NIL Inserimento Un esempio pratico root[T] Caso 1 11 4 15 3 Proprietà 3 violata 13 y NIL 2 NIL 7 8 6 NIL x 5 NIL NIL NIL NIL NIL NIL 19 NIL NIL NIL Inserimento Un esempio pratico root[T] Caso 2 11 y 4 Proprietà 3 violata x 3 7 13 NIL 2 NIL 15 8 6 NIL 5 NIL NIL NIL NIL NIL NIL 19 NIL NIL NIL Inserimento Un esempio pratico root[T] Caso 3 Proprietà 3 violata 11 y 7 15 x 8 4 3 6 NIL 5 2 NIL NIL NIL NIL NIL NIL 13 NIL NIL 19 NIL NIL NIL Inserimento Un esempio pratico Nota: l’albero è più bilanciato! root[T] 7 4 11 3 NIL 2 NIL 6 NIL 5 NIL 8 NIL NIL NIL 15 13 NIL NIL 19 NIL NIL NIL Rimozione La rimozione di un nodo da un RB-albero è un po’ più complicata dell’inserimento. Per semplificare il codice si fa uso di una sentinella nil[T] al posto di ogni foglia NIL. Il colore di nil[T] è BLACK, mentre gli altri campi (p, left, right, key) sono arbitrari. Tutti i puntatori a NIL sono sostituiti con puntatori a nil[T]. Il vantaggio è quello di poter trattare nil[T] come un nodo e poter quindi assegnare il valore p[nil[T]] necessario per il corretto funzionamento dell’algoritmo. Rimozione RB-DELETE(T,z) 1. if left[z] = nil[T] o right[z] = nil[T] 2. then y ← z // z ha 0 o 1 figlio 3. else y ← TREE-SUCCESSOR(z) // z ha due figli, trova succ(z) 4. if left[y] ≠ nil[T] // x punta ad eventuale 5. then x ← left[y] // unico figlio di y, altrimenti a nil[T] 6. else x ← right[z] 7. p[x] ← p[y] // taglia fuori y 8. if p[y] = nil[T] 9. then root[T] ← x // se y è la radice 10. else if y = left[p[y]] // altrimenti 11. then left[p[y]] ← x // completa eliminazione di y 12. else right[p[y]] ← x 13. if y ≠ z // se y è il successore 14. then key[z] ← key[y] // copia y in z 15. copia anche altri attributi di y in z 16. if color[y] = BLACK // chiama fixup se proprietà 4 17. then RB-DELETE-FIXUP(T,x) // risulta violata 18. return y Inserimento Un esempio pratico root[T] RB-DELETE(T,x) con key[x] = 2 7 4 11 3 z=y 2 6 8 5 15 13 NIL 19 Inserimento Un esempio pratico root[T] RB-DELETE(T,x) con key[x] = 2 7 4 11 3 6 8 5 15 13 NIL 19 Rimozione La rimozione di un nodo da un RB-albero è una semplice modifica della procedura TREE-DELETE. Dopo aver rimosso il nodo y, si chiama RB-DELETE-FIXUP se color[y] è BLACK. Questo perché viene eliminato un nodo interno nero e la proprietà 4 sulla b-altezza non è più valida. In RB-DELETE-FIXUP(T,x) si considera che x abbia un colore nero “doppio”, cioè pari a 2. La proprietà 4 risulta cosi soddisfatta. Lo scopo è di spostare il colore nero di troppo verso la radice in modo da eliminarlo. Rimozione RB-DELETE-FIXUP(T,x) 1. while x ≠ root[T] and color[x] = BLACK 2. 3. 4. 5. 6. 7. 8. // spingi su BLACK extra verso radice // 4 casi se x = left[p[x]] do if x = left[p[x]] then w ← right[p[x]] if color[w] = RED then color[w] ← BLACK color[p[x]] ← RED LEFT-ROTATE(T, p[x]) w ← right[p[x]] Nero per color[w] Caso 1 B x c C C A f a E w x E d e D B D b Caso 2, 3 o 4 Nero doppio w A a Nero doppio // caso 1 // fratello RED b c e d f Rimozione 9. 10. 11. if color[left[w]] = BLACK and color[right[w]] = BLACK then color [w] ← RED // caso 2: w BLACK x ← p[x] // figli di w BLACK Se il nuovo p[x] è RED il ciclo while termina. Per esempio, se si passa dal Caso 1 al Caso 2, il nuovo x (p[x]) è RED. Aggiungi colore nero Caso 2 B D A b Nero doppio c B w x a nuovo x C E d D A e a f b c C E d e f Rimozione 12. 13. 14. 15. 16. else if color [right[w]] = BLACK // caso 3: w BLACK, left[w] RED then color[left[w]] ← BLACK // right[w] BLACK color[w] ← RED RIGHT-ROTATE(T,w) w ←right[p[x]] Si è trasformato il Caso 3 nel Caso 4. Caso 4 Caso 3 B b Nero doppio c a C E d e C A D A nuovo w x w x a B Nero doppio f b c D d E e f Rimozione color[w] ← color[p[x]] // caso 4: w BLACK color[p[x]] ← BLACK // right[w] RED color[p[x]] ← BLACK LEFT-ROTATE(T,p[x]) x ← root[T] // per terminare il ciclo e assicurarsi che root sia BLACK 17. 18. 19. 20. 21. Caso 4 cpx = color[p[x]] B D w x a b Nero doppio c C e E A E d w B D A color ← cpx due neri f a C b c e d f nuovo x = root[T] Rimozione RB-DELETE-FIXUP(T,x) 1. while x ≠ root[T] and color[x] = BLACK // spingi su BLACK extra verso radice // 4 casi se x = left[p[x]] 2. do if x = left[p[x]] 3. then w ← right[p[x]] 4. if color[w] = RED // caso 1: w RED 5. then color[w] ← BLACK // fratello RED 6. color[p[x]] ← RED 7. LEFT-ROTATE(T, p[x]) 8. w ← right[p[x]] 9. if color[left[w]] = BLACK and color[right[w]] = BLACK 10. then color [w] ← RED // caso 2: w BLACK 11. x ← p[x] // figli di w BLACK Rimozione 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. else if color [right[w]] = BLACK // caso 3: w BLACK then color[left[w]] ← BLACK // right[w] BLACK color[w] ← RED RIGHT-ROTATE(T,w) w ←right[p[x]] color[w] ← color[p[x]] // caso 4: w BLACK color[p[x]] ← BLACK // right[w] RED color[p[x]] ← BLACK LEFT-ROTATE(T,p[x]) x ← root[T] // per terminare il ciclo e assicurarsi che root sia BLACK 22. else (analogo con “right” e “left” scambiati) 23. color[x] ← BLACK Rimozione In tutti i quattro casi (+ gli i 4 simmetrici) la proprietà 4 si mantiene dopo le modifiche, ossia il numero di nodi neri dalla radice a ognuno dei sottoalberi a, b, c ,d , e ed f rimane identico dopo la trasformazione. Tempo di esecuzione: Data l’altezza dell’albero è pari a O(lg(n)), la chiamata di RBDELETE() senza considerare RB-DELETE-FIXUP() ha un costo di O(lg(n)) (vedi rimozione in un albero binario). In RB-DELETE-FIXUP(), il Caso 1 si riconduce ai Casi 2, 3 o 4 in tempo costante. I Casi 3, e 4 fanno terminare la procedura dopo l’esecuzione di un numero costante di passi. Rimozione Il Caso 2 è l’unico per il quale si potrebbe ripetere l’esecuzione del ciclo while. Questo può avvenire al più O(lg(n)) volte. Quindi la procedura RB-DELETE-FIXUP() impegna un tempo O(lg(n)) ed esegue al più tre rotazioni. Il tempo di esecuzione complessivo della procedura RBDELETE() risulta O(lg(n)). Su un RB-albero le operazioni di SEARCH, PREDECESSOR, MINIMUM, MAXIMUM, INSERT e DELETE sono tutte O(lg(n)).