AA strom
AA strom (Arne Andersson strom) je červeno-černý strom s jedním přídavným pravidlem. Na rozdíl od červeno-černého stromu mohou být červené uzly vkládány pouze do pravého potomka, což znamená, že žádný červený uzel nemůže být levý potomek. Takto získáme strom izomorfní s B-stromem třetí úrovně (2-3 strom). Značně se tak usnadní implementace stromových operací.
Pro správné vyvážení červeno-černých stromů musí vyvažovací algoritmy uvažovat sedm různých tvarů:
Díky omezení, že pouze pravé hrany mohou být červené, stačí u AA stromů brát v potaz jen dva případy:
Podobným způsobem k zjednodušení úlohy přistupují tzv. left-leaning červeno-černé stromy, které zakazují červené pravé potomky určitého vrcholu s tou výjimkou, kdy je červený zároveň i levý potomek toho vrcholu. Tak vzniká struktura izomorfní s 2-3-4-stromem.
Pro další úvahy budeme předpokládat, že je v každém uzlu uložena proměnná úroveň, uchovávající počet levých hran, které musíme sledovat, než narazíme na nulový ukazatel. Listy tedy budou mít úroveň jedna.
Původní strom:
4 / \ 2 10 / \ / \ 1 3 6 12 / \ / \ 5 8 11 13 / \ 7 9
strom, ve kterém jsou uloženy úrovně:
4,3 / \ 2,2 10,3 / \ / \ 1,1 3,1 6,2 12,2 / \ / \ 5,1 8,2 11,1 13,1 / \ 7,1 9,1
a strom zaznamenaný v notaci červeno-černých stromů (B – černá, R – červená):
B(4) / \ B(2) R(10) / \ / \ B(1) R(3) B(6) B(12) / \ / \ B(5) R(8) B(11) R(13) / \ B(7) B(9)
Pro AA strom platí následující pravidla:
- Levý potomek nesmí mít stejnou úroveň jako jeho rodič
- Nejsou povoleny dva praví potomci se stejnou úrovní jako rodič
Pokud bychom chtěli tato pravidla zapsat v terminologii červeno-černých stromů, zněla by následovně:
- Červené levé hrany nejsou povoleny
- Dvě po sobě jdoucí červené hrany nejsou povoleny
Díky vlastnostem AA stromu, kdy může být pouze pravý potomek červený, jsou dva výše zmíněné zápisy pravidel ekvivalentní a budeme je v článku různě střídat.
Split a skew
Na vyvažování AA stromu stačí pouze dvě operace: split a skew. Skew je pravá rotace, pokud vkládání nebo mazání vytvoří levou červenou hranu. Split je levá rotace, když vkládání nebo mazání vytvoří dvě po sobě jdoucí červené hrany.
void Skew (struct node *oldparent)
{
struct node *newp = oldparent->left;
if (oldparent->parent->left == oldparent) oldparent->parent->left = newp;
else oldparent->parent->right = newp;
newp->parent = oldparent->parent;
oldparent->parent = newp;
oldparent->left = newp->right;
if (oldparent->left) oldparent->left->parent = oldparent;
newp->right = oldparent;
oldparent->level = oldparent->left ? oldparent->left->level + 1 : 1;
}
int Split (struct node *oldparent)
{
struct node *newp = oldparent->right;
if (newp && newp->right && newp->right->level == oldparent->level) {
if (oldparent->parent->left == oldparent) oldparent->parent->left = newp;
else oldparent->parent->right = newp;
newp->parent = oldparent->parent;
oldparent->parent = newp;
oldparent->right = newp->left;
if (oldparent->right) oldparent->right->parent = oldparent;
newp->left = oldparent;
newp->level = oldparent->level + 1;
return 1;
}
return 0;
}
AA stromy se snadněji implementují, pokud se k označení konce stromu použijí uzly se zarážkami, které se lépe testují, místo nulových ukazatelů. Je relativně snadné převést kód se zarážkami na kód s ukončováním pomocí nulových ukazatelů. Jediné v čem bude rozdíl, je implementace operací split a skew.
Vkládání
Po vkládání je stejně jako u ostatních binárních vyvážených stromů nutné zkontrolovat vyvážení stromu. Pokud se zvýší úroveň pravého potomka (protože nový uzel byl vložen napravo, nebo se zvýšila úroveň pravého potomka, nebo přibyl nový levý potomek s vyšší úrovní), mohou se objevit dva po sobě jdoucí červené uzly, které musí být ošetřeny operací split, která ovšem může způsobit zvýšení úrovně nového rodiče.
Pokud se zvýší úroveň uzlu, je potřeba vykonat operaci skew. Může se stát, že předchozí rodič uzlu (nyní potomek) a nebo jeho předchozí potomek (nyní prapotomek) jsou červení. Tím pádem jsme opět v situace, kdy musíme provést split.
Jako názornou ukázku vybudujeme AA strom. Nejdříve vložíme 0 do prázdného stromu, tím neporušíme žádné pravidlo a úroveň tohoto uzlu bude 1. Následně vložíme uzel 1 a opět nejsou porušena žádná pravidla. Po vložení uzlu 2 už musíme použít operaci split, neboť je porušeno druhé pravidlo. Split provede levou rotaci a zvýší úroveň uzlu 1 o 1.
0,1 1,2 \ / \ 1,1 → 0,1 2,1 \ 2,1
Přidáním uzlu 3 se žádné pravidlo neporuší. Přidáním uzlu 4 se opět poruší druhé pravidlo tentokrát vzhledem k uzlu 2 a musíme použít operaci split:
1,2 1,2 / \ / \ 0,1 2,1 0,1 3,2 \ → / \ 3,1 2,1 4,1 \ 4,1
Přidání 5 projde bez komplikací, ale 6 znovu poruší druhé pravidlo a je potřeba využít split. Tentokrát však jeden split nebude stačit, neboť rozdělení 4, 5 a 6 způsobí, že se zvýší úroveň 5 a dojde k porušení druhého pravidla uzly 1, 2 a 5:
1,2 1,2 / \ / \ 0,1 3,2 0,1 3,2 / \ / \ 2,1 4,1 → 2,1 5,2 \ / \ 5,1 4,1 6,1 \ 6,1
Pro vyvážení stromu musíme provést split na uzlu 1.
1,2 3,3 / \ / \ 0,1 3,2 1,2 5,2 / \ → / \ / \ 2,1 5,2 0,1 2,1 4,1 6,1 / \ 4,1 6,1
Tento příklad je umělý, uzly jsme přidávali ve vzestupném pořadí a nebylo potřeba využít operace skew. V dalším příkladu budeme vkládat uzly sestupně. Do prázdného stromu vložíme 6, není potřeba žádného zásahu a hned přidáme 5. Tímto jsme porušili první pravidlo, neboť levý potomek nesmí mít stejnou úroveň jako jeho předek. Problém spravíme pomocí skew, která provede pravou rotaci a udělá z levé červené hrany pravou červenou:
6,1 5,1 / → \ 5,1 6,1
Po vložení 4 vznikne další červená levá hrana a znovu použijeme skew. Skew však vytvoří dvě po sobě jdoucí červené hrany, které musí být rozděleny pomocí split.
5,1 4,1 5,2 / \ → \ → / \ 4,1 6,1 5,1 4,1 6,1 \ 6,1
Dále vložíme 3 provedeme vyvážení pomocí skew.
5,2 5,2 / \ → / \ 4,1 6,1 3,1 6,1 / \ 3,1 4,1
Po vložení 2 musíme použít skew i split.
5,2 5,2 5,2 / \ / \ / \ 3,1 6,1 → 2,1 6,1 → 3,2 6,1 / \ \ / \ 2,1 4,1 3,1 2,1 4,1 \ 4,1
Tato situace, ale porušuje první pravidlo, neboť 3 je levá hrana 5 a má stejnou úroveň. Strom znovu vyvážíme použitím operace skew.
5,2 3,2 / \ / \ 3,2 6,1 → 2,1 5,2 / \ / \ 2,1 4,1 4,1 6,1
Na vyvážení stromu po vkládání může použít následující proceduru:
void RebalanceAfterLeafAdd (struct node *n)
{ // n is a node that has just been inserted and is now a leaf node.
n->level = 1;
n->left = NULL;
n->right = NULL;
for (n = n->parent; n != &root; n = n->parent) {
if (n->level != (n->left ? n->left->level + 1 : 1)) {
Skew (n);
if (!n->right || n->level != n->right->level) n = n->parent;
}
if (!Split (n->parent)) break;
}
}
Odstranění
Odstranění uzlu v AA stromu je postavené na jednoduchém principu. Pokud odstraňovaný uzel není list, najdeme takový list, který je v pořadí těsně před nebo za odstraňovaným uzlem. Vlastnosti AA stromu nám zaručují, že takový list skutečně existuje. Následně tyto dva uzly vyměníme a odstraníme list. U rodiče levého potomka musíme snížit úroveň a u pravého potomka se musíme ujistit, zda dva po sobě jdoucí uzly nejsou červené.
Algoritmus odstranění si předvedeme na následujícím vyváženém stromu.
3,3 / \ 1,2 5,2 / \ / \ 0,1 2,1 4,1 6,1
Po odstranění 0 musíme snížit úroveň uzlů 1 a 3 o jedna. Obejdeme se bez operací skew a split.
3,2 / \ 1,1 5,2 \ / \ 2,1 4,1 6,1
Následně odstraníme 3.
2,2 / \ 1,1 5,2 / \ 4,1 6,1
3 je nahrazena svým in-order následníkem 2. V tomto případě se neporuší pravidla a není potřeba provádět vyvážení. Pokud však odstraníme 1, způsobíme nevyvážení a u listu 2 a 5 musí být snížena úroveň.
2,2 2,1 \ \ 5,2 → 5,1 / \ / \ 4,1 6,1 4,1 6,1
Pro vyvážení musíme nyní použít skew následovaný split.
2,1 2,1 4,2 \ \ / \ 5,1 → 4,1 → 2,1 5,1 / \ \ \ 4,1 6,1 5,1 6,1 \ 6,1
Kompletní algoritmus odstranění v jazyce C vypadá následovně:
void Remove (struct node *n)
{
struct node *leaf = n, *tmp;
if (n->left) {
for (leaf = n->left; leaf->right; leaf = leaf->right) {}
}
else if (n->right) leaf = n->right;
tmp = leaf->parent == n ? leaf : leaf->parent;
if (leaf->parent->left == leaf) leaf->parent->left = NULL;
else leaf->parent->right = NULL;
if (n != leaf) {
if (n->parent->left == n) n->parent->left = leaf;
else n->parent->right = leaf;
leaf->parent = n->parent;
if (n->left) n->left->parent = leaf;
leaf->left = n->left;
if (n->right) n->right->parent = leaf;
leaf->right = n->right;
leaf->level = n->level;
}
for (; tmp != &root; tmp = tmp->parent) {
if (tmp->level > (tmp->left ? tmp->left->level + 1 : 1)) {
tmp->level--;
if (Split (tmp)) {
if (Split (tmp)) Skew (tmp->parent->parent);
break;
}
}
else if (tmp->level <= (tmp->right ? tmp->right->level + 1 : 1)) break;
else {
Skew (tmp);
if (tmp->level > tmp->parent->level) {
Skew (tmp);
Split (tmp->parent->parent);
break;
}
tmp = tmp->parent;
}
}
}
Výkon
Výkon AA stromu je ekvivalentní s výkonem červeno-černého stromu. Přestože AA provádí více rotací, jednodušší algoritmy bývají rychlejší a vyrovnají tak ztrátu výkonu. Červeno-černý strom podává vyrovnanější výsledky, ale AA strom je často plošší, což znamená rychlejší hledání.