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à: Ciascun nodo è rosso o nero. Ciascuna foglia (NIL) è nera. Se un nodo è rosso allora entrambi i suoi figli sono neri. 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 2 6 8 11 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 RIGHT-ROTATE(T,y) x x y LEFT-ROTATE(T,x) c a a b b c a ≤ x ≤ b ≤ y ≤ 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) y ← right[x] // inizializzazione di y right[x] ← left[y] // inizio rotazione: sposta b if left[y] ≠ NIL then p[left[y]] ← x p[y] ← p[x] // metti y al posto di x if p[x] = NIL then root[T] ← y // se x era la radice else if x = left[p[x]] // modifica puntatore di p[x] then left[p[x]] ← y else right[p[x]] ← y left[y] ← x // metti x a sinistra di y p[x] ← 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) TREE-INSERT(T,x) // ins. come albero b. di ricerca color[x] ← RED // 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 while x ≠ root[T] and color[p[x]] = RED // finché prop. 3 violata do if p[x] = left[p[p[x]]] // 3 casi: p[x] sinistra di p[p[x]] then y ← right[p[p[x]]] if color[y] = RED // caso 1: p[x] e fratello y RED then color[p[x]] ← BLACK // y “zio” di x color[y] ← BLACK color[p[p[x]]] ← RED 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[p[x]] p[x] y p[x] y x x
Inserimento 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] LEFT-ROTATE(T,x) p[x] y x y y x a x c b c a b
bh(a) = bh(b) = bh(c) = bh(y) 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]]) 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]!). color[p[x]] ← B color[p[p[x]]] ← R p[p[x]] p[p[x]] p[x] RIGHT-ROTATE(T,p[p[x]]) p[p[x]] p[x] y p[x] y x x c x c a b c y a b a b bh(a) = bh(b) = bh(c) = bh(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 else (altri 3 Casi simmetrici: scambia “right” e “left”) 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) TREE-INSERT(T,x) // ins. come albero b. di ricerca color[x] ← RED // la proprietà 3 violata? while x ≠ root[T] and color[p[x]] = RED // finché prop. 3 violata do if p[x] = left[p[p[x]]] // 3 casi: p[x] sinistra di p[p[x]] then y ← right[p[p[x]]] if color[y] = RED // caso 1: p[x] e fratello y RED then color[p[x]] ← BLACK // y “zio” di x color[y] ← BLACK color[p[p[x]]] ← RED x ← p[p[x]] 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) color[p[x]] ← BLACK // caso 2: zio y BLACK color[p[p[x]]] ← RED // e x a sinistra di p[x] RIGHT-ROTATE(T,p[p[x]]) else (altri 3 Casi simmetrici: scambia “right” e “left”) 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 RB-INSERT(T,x) key[x] = 5 root[T] 11 5 4 15 3 7 13 19 NIL 2 8 NIL NIL NIL NIL 6 NIL NIL NIL NIL NIL NIL
Inserimento Un esempio pratico Caso 1 root[T] 11 4 15 3 7 13 19 y 2 8 NIL 2 8 NIL NIL NIL NIL 6 x NIL NIL 5 NIL NIL NIL Proprietà 3 violata NIL NIL
Inserimento Un esempio pratico Caso 2 root[T] 11 y 4 15 Proprietà 3 violata x 3 7 13 19 NIL 2 8 NIL NIL NIL NIL 6 NIL NIL 5 NIL NIL NIL NIL NIL
Inserimento Un esempio pratico Caso 3 root[T] Proprietà 3 violata 11 y 7 15 x 8 4 13 19 NIL NIL NIL NIL NIL NIL 3 6 NIL 5 NIL 2 NIL NIL NIL NIL
Inserimento Un esempio pratico Nota: l’albero è più bilanciato! root[T] Nota: l’albero è più bilanciato! 7 4 11 3 6 8 15 2 NIL 5 NIL NIL NIL 13 19 NIL NIL NIL NIL NIL 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) if left[z] = nil[T] o right[z] = nil[T] then y ← z // z ha 0 o 1 figlio else y ← TREE-SUCCESSOR(z) // z ha due figli, trova succ(z) if left[y] ≠ nil[T] // x punta ad eventuale then x ← left[y] // unico figlio di y, altrimenti a nil[T] else x ← right[z] p[x] ← p[y] // taglia fuori y if p[y] = nil[T] then root[T] ← x // se y è la radice else if y = left[p[y]] // altrimenti then left[p[y]] ← x // completa eliminazione di y else right[p[y]] ← x if y ≠ z // se y è il successore then key[z] ← key[y] // copia y in z copia anche altri attributi di y in z if color[y] = BLACK // chiama fixup se proprietà 4 then RB-DELETE-FIXUP(T,x) // risulta violata return y
Inserimento Un esempio pratico RB-DELETE(T,x) con key[x] = 2 root[T] 7 4 11 3 6 8 15 z = y 2 5 13 19 NIL
Inserimento Un esempio pratico RB-DELETE(T,x) con key[x] = 2 root[T] 7 4 11 3 6 8 15 5 13 19 NIL
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) while x ≠ root[T] and color[x] = BLACK // spingi su BLACK extra verso radice do if x = left[p[x]] // 4 casi se x = left[p[x]] then w ← right[p[x]] if color[w] = RED // caso 1 then color[w] ← BLACK // fratello RED color[p[x]] ← RED LEFT-ROTATE(T, p[x]) w ← right[p[x]] Caso 2, 3 o 4 Nero per color[w] Nero doppio Caso 1 B D x w A D B E w x A C a b e f C E Nero doppio a b c d c d e f
Rimozione 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 nuovo x B B w x A D A D a b C E a b C E Nero doppio c d e f c d e f
Rimozione Si è trasformato il Caso 3 nel Caso 4. 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 3 Caso 4 B nuovo w x B A C w x A D c a b D a b C E Nero doppio d E Nero doppio c d e f 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 Caso 4 due neri color ← cpx cpx = color[p[x]] D B w w x B E A D A C e f a b C E nuovo x = root[T] Nero doppio a b c d c d e f
Rimozione RB-DELETE-FIXUP(T,x) while x ≠ root[T] and color[x] = BLACK // spingi su BLACK extra verso radice do if x = left[p[x]] // 4 casi se x = left[p[x]] then w ← right[p[x]] if color[w] = RED // caso 1: w RED then color[w] ← BLACK // fratello RED color[p[x]] ← RED LEFT-ROTATE(T, p[x]) w ← right[p[x]] 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
Rimozione 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 else (analogo con “right” e “left” scambiati) 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 RB-DELETE() 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 RB-DELETE() risulta O(lg(n)). Su un RB-albero le operazioni di SEARCH, PREDECESSOR, MINIMUM, MAXIMUM, INSERT e DELETE sono tutte O(lg(n)).