====== Kopce binarne, drzewa AVL i czerwono-czarne ======
===== Teoria do przygotowania =====
- Kopce binarne:
* Sposób organizacji danych.
* Implementacja tablicowa kopca.
* Operacje na kopcu:
* Wstawianie elementu.
* Usuwanie elementu ze szczytu kopca.
* Budowanie kopca z wektora nieuporządkowanych danych.
* Sortowanie przez kopcowanie.
- Drzewa AVL:
* Zasada organizacji danych.
* Operacje na drzewach AVL:
* Wstawianie elementu.
* Usuwanie elementu.
* Wyszukiwanie elementu.
* Wyważanie drzewa:
* Przypadki źle wywarzonych drzew
* Rotacje
- Drzewa Czerwono-Czarne:
* Opis idei drzew czerwono-czarnych:
* wymagania (5 reguł),
* długość najkrótszej i najdłuższej ścieżki od korzenia do liścia.
* Operacje na drzewach czerwono-czarnych:
* Wstawianie elementu.
* Usuwanie elementu.
* Wyszukiwanie elementu.
* Możliwe przypadki przy wykonywaniu ww. operacji i sposoby radzenia sobie z nimi.
**Część ćwiczeń będzie wymagała użycia komputerów, więc prosimy o ich przyniesienie.**
===== Ćwiczenia implementacyjne =====
==== Pliki do ćwiczeń ====
W archiwum {{:pl:dydaktyka:asd:cwiczenia:heapsavl.zip}} znajdują się pliki potrzebne do wykonania ćwiczeń.
W archiwum znajdują się następujące pliki:
* ''binary-heap.cpp'' - plik zawierający definicje metod klasy reprezentującej kopiec binarny maximum.
* ''binary-heap.h'' - plik nagłówkowy dla powyższej klasy, zawierający jej definicje.
* ''avlnode.cpp'' - plik zawierający definicję metod klasy reprezentującej drzewo AVL.
* ''avlnode.h'' - plik nagłówkowy dla powyższej klasy, zawierający jej definicje.
* ''avl.cpp'' - plik zawierający definicję metod klasy reprezentującej drzewo AVL.
* ''avl.h'' - oraz plik nagłówkowy.
* ''main*.cpp'' - pliki pokazujące przykładowe wykorzystanie klas.
==== Ćwiczenie: kopce binarne ====
W archiwum znajduje się plik ''binary-heap.cpp'', który posiada dwie nie uzupełnione metody:
void binary_heap::buildHeap(const int* __array, int __size);
int* binary_heap::sort(void);
gdzie:
* ''buildHeap(const int* __array, int __size)'' przyjmuje dwa parametry:
- ''__array'' wskaźnik do nieposortowanej tablicy
- ''__size'' - rozmiar tej tablicy
* ''sort'' nie przyjmuje żadnych parametrów nastomiast zwraca wskaźnik do posortowanej tablicy.
Zadaniem jest napisanie programu który wykorzysta kopiec do realizacji algorytmu sortowania (oczywiście przez kopcowanie).
\\ Do tego celu należy:
- Uzupełnić te dwie metody, przy czym w przypadku metody ''buildHeap'' nie można korzystać z metody ''insert'' - rozwiązanie nieoptymalne.
- Napisać funkcję ''main'' w której stworzona zostanie nieposortowana tablica,
- następnie na jej podstawie zbudowany zostanie kopiec i w końcu
- za pomocą metody sort, otrzymamy tablicę posortowaną (kopiec nie może ulec zniszczeniu).
==== Ćwiczenie: drzewa AVL ====
W archiwum jest plik ''avl.cpp'' który ma nie zdefiniowaną metodę
avlnode* avl::fixBalance(avlnode* __node)
która jako argument przyjmuje wskaźnik na węzeł względem którego ma odbyć się rotacja i zwraca wskaźnik na węzeł który po rotacji jest wstawiony na pierwotny.
Zadaniem jest napisanie tej metody. \\
Realizacja zadania jest o tyle prosta że nie trzeba się bawić w poprawianie wag poszczególnych węzłów - robi się to automatycznie dzięki takiej a nie innej implementacji metod (''leftWeight'' oraz ''rightWeight'') klasy ''avlnode''. Oczywiście jest to nie optymalne rozwiązanie ale za to dużo czytelniejsze i przez to lepiej nadaje się do celów prezentacyjnych. Jak widać docelowe rozwiązanie jest krótkie i polega na sprawdzeniu wyżej wymienionych warunków.
===== Uzupełnienie teoretyczne =====
==== Drzewa czerwono-czarne ====
//Przedstawione materiały i przykłady pochodzą z: Wikipedia EN, Wikipedia PL.//
//Wymagana znajomość odpowiednich rozdziałów z: Cormen - "Wprowadzenie do Algorytmów".//
{{:pl:dydaktyka:asd:priv:labs2012:rbt-example.svg?600}}
Drzewa czerwono-czarne (red-black trees, RB trees) to kolejna po drzewach AVL próba rozwiązania problemu drzew samorównoważących. Drzewa RB są nowsze o kilka lat od AVL, operacje wstawiania i usuwania są tu nieco szybsze, ale z uwagi na to że równoważenie nie jest tak skrupulatne jak w AVL, operacje wyszukiwania mogą być nieco wolniejsze. Niezłe zachowanie //worst-case//, dzięki czemu szeroko stosowane w praktyce, m.in. do harmonogramowania w aktualnych wersjach jądra Linuksa.
Główne różnice w stosunku do klasycznych BST to
* to, że liście nie zawierają danych. W praktyce implementujemy je jako puste elementy, jako wskaźniki do jednego elementu (tzw. //sentinel node//, aby oszczędzić pamięć), lub wskaźniki do NIL (ale to nieco komplikuje implementację operacji na drzewie),
* każdy węzeł drzewa posiada dodatkowy atrybut, //kolor//, który może przyjmować wartości //czerwony// lub //czarny//.
Drzewo RB musi spełniać pięć reguł:
- Każdy węzeł jest czerwony lub czarny.
- Korzeń jest czarny.
- Każdy liść jest czarny.
- Jeśli węzeł jest czerwony, to jego synowie muszą być czarni.
- Każda ścieżka z ustalonego węzła do liścia liczy tyle samo czarnych węzłów.
W efekcie, najdłuższa ścieżka od korzenia do liścia jest co najwyżej dwa razy dłuższa niż ścieżka najkrótsza. Wynika to z własności, które z kolei wynikają z ww. reguł:
- Zgodnie z własnością 4, żadna ścieżka nie zawiera dwóch czerwonych węzłów pod rząd, jednak może zawierać czarne. Stąd najkrótsza ścieżka od węzła X zawiera wyłącznie n czarnych węzłów.
- Zgodnie z własnością 5, druga ścieżka wychodząca z węzła X musi zawierać także n czarnych węzłów. Jedynym sposobem, aby miała ona inną łączną długość, jest umieszczenie pomiędzy każdą parą węzłów czarnych węzła czerwonego.
- Zgodnie z własnością 3, liść kończący obie ścieżki musi być czarny. Jeżeli węzeł X jest czarny, wtedy w ścieżce możemy rozmieścić co najwyżej n-1 węzłów czerwonych, w przeciwnym zaś razie będziemy mieli w niej n czerwonych węzłów (wliczając w to sam X).
=== Wstawianie ===
Operacja wstawiania składa się z trzech etapów:
- Umieszczenie elementu za pomocą standardowego algorytmu dla drzew poszukiwań binarnych.
- Pokolorowanie nowego węzła na czerwono.
- Ewentualne przywrócenie własności czerwono-czarnych (rozpatrzenie możliwych przypadków).
Po wykonaniu kroków 1 i 2 możemy napotkać na 5 możliwych przypadków (rozpatrujemy je kolejno):
- Wstawiony węzeł **N** jest korzeniem drzewa.
- Rodzic (parent) wstawionego węzła **P** jest czarny (a więc reguły nie zostały naruszone).
- Rodzic **P** jest czerwony, i brat ojca (wujek - uncle) węzła **U** jest czerwony.
- Rodzic **P** jest czerwony, wujek **U** jest czarny i **N** jest prawym synem.
- Rodzic **P** jest czerwony, wujek **U** jest czarny i **N** jest lewym synem.
**UWAGA:** Przypadki 3-5 występują w dwóch "lustrzanych" wariantach:
* **P** (rodzic wstawionego węzła) jest //lewym// synem swojego ojca (**G**),
* **P** (rodzic wstawionego węzła) jest //prawym// synem swojego ojca (**G**).
Komentarz i ilustracje zakładają, że **P** (rodzic wstawionego węzła) jest //lewym// synem swojego ojca (**G**), natomiast kod rozważa oba przypadki.
Pomonicze funkcje do ekstrakcji dziadka i wuja (nice!) wyglądają tak:
struct node *grandparent(struct node *n)
{
if ((n != NULL) && (n->parent != NULL))
return n->parent->parent;
else
return NULL;
}
struct node *uncle(struct node *n)
{
struct node *g = grandparent(n);
if (g == NULL)
return NULL; // No grandparent means no uncle
if (n->parent == g->left)
return g->right;
else
return g->left;
}
== Przypadek 1 ==
Wstawiony węzeł **N** jest korzeniem drzewa.
Malujemy **N** na czarno, zgodnie z zasadą nr 2 korzeń ma być czarny, a ponieważ jest to korzeń, to dodaje jeden czarny węzeł do każdej ze ścieżek. Kończymy procedurę.
void insert_case1(struct node *n)
{
if (n->parent == NULL)
n->color = BLACK;
else
insert_case2(n);
}
== Przypadek 2 ==
Rodzic (parent) wstawionego węzła **P** jest czarny.
Zasada nr 4 jest nienaruszona (bo nie mamy dwóch czerwonych pod rząd w żadnej ścieżce). Zasada nr 5 również, bo nowy czerwony (który posiada dwójkę czarnych dzieci) zastąpił czarny liść, więc liczba czarnych w każdej ścieżce pozostaje niezmieniona.
void insert_case2(struct node *n)
{
if (n->parent->color == BLACK)
return; /* Tree is still valid */
else
insert_case3(n);
}
== Przypadek 3 ==
Rodzic **P** jest czerwony, i brat ojca (wujek - uncle) węzła **U** jest czerwony.
{{:pl:dydaktyka:asd:priv:labs2012:rbt-insert-case3.png}}
Przemalowujemy **P** i **U** na czarno, a dziadka **G** na czerwono. Liczba czarnych na każdej ścieżce jest zachowana. Dziadek może teraz jednak naruszać zasadę nr 2 (jeżeli jest korzeniem) lub 4 (jeżeli ma czerwonego ojca - pradziadka). Dlatego musimy całą procedurę (od przypadku nr 1) uruchomić rekurencyjnie dla węzła **G** (rekurencja jest niegroźna, bo ogonowa, więc można zrobić iteracyjnie).
void insert_case3(struct node *n)
{
struct node *u = uncle(n), *g;
if ((u != NULL) && (u->color == RED)) {
n->parent->color = BLACK;
u->color = BLACK;
g = grandparent(n);
g->color = RED;
insert_case1(g);
} else {
insert_case4(n);
}
}
== Przypadek 4 ==
Rodzic **P** jest czerwony, wujek **U** jest czarny i **N** jest prawym synem.
{{:pl:dydaktyka:asd:priv:labs2012:rbt-insert-case4.png}}
Wykonujemy rotację w lewo, która zamienia role **P** i **N**, a ponieważ dalej mamy czerwonego rodzica i dziecko, przetwarzamy dawnego rodzica **P** (po zamianie oznaczeń P<->N) według procedury dla [[#przypadek_5|przypadku 5]].
Na skutek powyższej rotacji, niektóre ścieżki które nie przechodziły przez **N** teraz mogą przez ten węzeł przechodzić, a niektóre które przechodziły przez **P** mogą już przez niego nie przechodzić. Oba węzły są czerwone, więc zasada nr 5 nie jest naruszona.
void insert_case4(struct node *n)
{
struct node *g = grandparent(n);
if ((n == n->parent->right) && (n->parent == g->left)) {
rotate_left(n->parent);
n = n->left;
} else if ((n == n->parent->left) && (n->parent == g->right)) {
rotate_right(n->parent);
n = n->right;
}
insert_case5(n);
}
== Przypadek 5 ==
Rodzic **P** jest czerwony, wujek **U** jest czarny i **N** jest lewym synem.
{{:pl:dydaktyka:asd:priv:labs2012:rbt-insert-case5.png}}
Wykonujemy rotację w prawo względem dziadka **G**. W efekcie dawny ojciec **P** jest rodzicem zarówno dawnego syna **N**, jak i dawnego dziadka **G**. Wiemy, że **G** musiał być czarny, więc jeżeli zamienimy kolory **P** oraz **G**, mamy znów spełnioną zasadę nr 4. Zasada 5 jest również spełniona, gdyż wszystkie ścieżki które przechodziły przez czarny **G** teraz przechodzą przez (obecnie czarny) **P**.
void insert_case5(struct node *n)
{
struct node *g = grandparent(n);
n->parent->color = BLACK;
g->color = RED;
if (n == n->parent->left)
rotate_right(g);
else
rotate_left(g);
}
=== Usuwanie ===
Usuwanie węzła posiadającego dwoje dzieci "nie-liściowych" (w nomenklaturze drzew RB) z klasycznego drzewa BST polega na wybraniu maksymalnego elementu z jego lewego poddrzewa (albo minimalnego z prawego) i przeniesieniu do usuwanego węzła. Kopiowanie wartości nie narusza własności drzew RB, więc rozpatrywany tu będzie tylko przypadek usuwania węzła posiadającego //co najwyżej// jeden węzeł "nie-liściowy".
Przez **M** oznaczamy węzeł do usuniecia, przez **C** - jego wybrane dziecko. Jeżeli jedno z dzieci posiada wartość, przypisujemy to dziecko do **C**, jeżeli oba dzieci są NIL (liściami), przypisujemy dowolne.
Możliwe przypadki:
- Jeżeli **M** jest czerwony, podmieniamy go przez **C**, który musi być czarny (zasada nr 4) - ten przypadek może mieć miejsce tylko jeżeli **M** ma dwoje dzieci NIL. Usunęliśmy węzeł czerwony, więc liczba czarnych węzłów w ścieżkach nie może się zmienić (zasada nr 5), a że rodzic czerwonego musiał być czarny, zasada nr 4 również jest spełniona.
- Jeżeli **M** jest czarny, a **C** czerwony, usuwamy **M** a **C** malujemy na czarno - zasady nr 4 i 5 są spełnione.
- Gorzej, jeżeli zarówno **M** jak i **C** są czarne. Podmieniamy **M** przez **C**, zmieniamy nazwę **C** na **N**, a nazwę jego nowego rodzeństwa (czyli drugiego dziecka rodzica usuniętego węzła **M**) na **S**. Rodzica usuniętego węzła (czyli nowego rodzica **N**) oznaczamy jako **P**, a dzieci węzła **S** - odpowiednio **SL** i **SR**.
Funkcja pomocnicza do szukania rodzeństwa:
struct node *sibling(struct node *n)
{
if (n == n->parent->left)
return n->parent->right;
else
return n->parent->left;
}
Powyższe kroki wykonuje następujący kod:
void delete_one_child(struct node *n)
{
/*
* Precondition: n has at most one non-null child.
*/
struct node *child = is_leaf(n->right) ? n->left : n->right;
replace_node(n, child);
if (n->color == BLACK) {
if (child->color == RED)
child->color = BLACK;
else
delete_case1(child);
}
free(n);
}
Jeżeli **N** i jego pierwotny rodzic są czarni, mamy do rozważenia kilka przypadków.
== Usuwanie - przypadek 1 ==
**N** jest nowym korzeniem. Usunęliśmy jeden węzeł z każdej ścieżki, a nowy korzeń jest czarny, więc własności są zachowane.
void delete_case1(struct node *n)
{
if (n->parent != NULL)
delete_case2(n);
}
== Usuwanie - przypadek 2 ==
{{:pl:dydaktyka:asd:priv:labs2012:rbt-delete-case2.png}}
**S** jest czerwony. Zamieniamy kolory **P** i **S** i wykonujemy rotację w lewo na **P**, przez co **S** staje się dziadkiem **N**.
void delete_case2(struct node *n)
{
struct node *s = sibling(n);
if (s->color == RED) {
n->parent->color = RED;
s->color = BLACK;
if (n == n->parent->left)
rotate_left(n->parent);
else
rotate_right(n->parent);
}
delete_case3(n);
}
== Usuwanie - przypadek 3 ==
{{:pl:dydaktyka:asd:priv:labs2012:rbt-delete-case3.png}}
**P**, **S** i dzieci **S** są czarne. Malujemy **S** na czerwono. Teraz wszystkie ścieżki przechodzące przez **S**, czyli dokładnie te które nie przechodzą przez **N**, mają o jeden czarny węzeł mniej. Ponieważ usunęliśmy rodzica **P** który był czarny, liczniki po lewej i prawej stronie tego poddrzewa się zgadzają. Niestety, ścieżki prowadzące przez **P** mają o jeden czarny węzeł mniej niż te które nie przechodzą przez **P**. Dlatego uruchamiamy procedurę rebalansowania począwszy od **P**.
void delete_case3(struct node *n)
{
struct node *s = sibling(n);
if ((n->parent->color == BLACK) &&
(s->color == BLACK) &&
(s->left->color == BLACK) &&
(s->right->color == BLACK)) {
s->color = RED;
delete_case1(n->parent);
} else
delete_case4(n);
}
== Usuwanie - przypadek 4 ==
{{:pl:dydaktyka:asd:priv:labs2012:rbt-delete-case4.png}}
**S** i jego dzieci są czarni, ale **P** jest czerwony. Zamieniamy więc kolory **P** i **S**. Dodaje to jeden czarny węzeł do ścieżek prowadzących przez **N**, co kompensuje usunięty węzeł.
void delete_case4(struct node *n)
{
struct node *s = sibling(n);
if ((n->parent->color == RED) &&
(s->color == BLACK) &&
(s->left->color == BLACK) &&
(s->right->color == BLACK)) {
s->color = RED;
n->parent->color = BLACK;
} else
delete_case5(n);
}
== Usuwanie - przypadek 5 ==
{{:pl:dydaktyka:asd:priv:labs2012:rbt-delete-case5.png}}
**S** jest czarny, **SL** czerwony, **SR** czarny, a **N** jest lewym dzieckiem. Wykonujemy rotację w prawo w **S**, tak że **SL** staje się rodzicem **S** i nowym rodzeństwem **N**. Teraz zamieniamy kolor **S** i jego rodzica.
void delete_case5(struct node *n)
{
struct node *s = sibling(n);
if (s->color == BLACK) { /* this if statement is trivial,
due to case 2 (even though case 2 changed the sibling to a sibling's child,
the sibling's child can't be red, since no red parent can have a red child). */
/* the following statements just force the red to be on the left of the left of the parent,
or right of the right, so case six will rotate correctly. */
if ((n == n->parent->left) &&
(s->right->color == BLACK) &&
(s->left->color == RED)) { /* this last test is trivial too due to cases 2-4. */
s->color = RED;
s->left->color = BLACK;
rotate_right(s);
} else if ((n == n->parent->right) &&
(s->left->color == BLACK) &&
(s->right->color == RED)) {/* this last test is trivial too due to cases 2-4. */
s->color = RED;
s->right->color = BLACK;
rotate_left(s);
}
}
delete_case6(n);
}
== Usuwanie - przypadek 6 ==
{{:pl:dydaktyka:asd:priv:labs2012:rbt-delete-case6.png}}
**S** jest czarny, **SR** czerwony, a **N** jest lewym dzieckiem. Wykonujemy rotację w lewo w **P**, tak że **S** staje się rodzicem **P** i **SR**. **N** zyskuje dodatkowego czarnego przodka: albo **P** stał się czarny, albo był czarny a **S** został dodany jako czarny dziadek. Ścieżki prowadzące przez **N** mają więc teraz dodatkowy czarny węzeł.
Dla ścieżek nie prowadzących przez **N** mamy możliwości:
* Prowadzą przez nowe rodzeństwo **N**. Takie ścieżki muszą prowadzić przez **P** i **S** (wcześniej i teraz), więc liczba czarnych węzłów się nie zmienia.
* Prowadzą przez nowego wuja **N**, **SR**. Wcześniej takie ścieżki musiały prowadzić przez **S**, rodzica **S** i **SR** (który był czerwony), ale teraz prowadzą tylko przez **S**, który przyjął kolor swojego dawnego rodzica, oraz **SR**, który został przemalowany z czerwonego na czarny.
Oba przypadki oznaczają, że liczba czarnych węzłów na tych ścieżkach nie uległa zmianie. Zasady nr 4 i 5 są więc znów spełnione.
void delete_case6(struct node *n)
{
struct node *s = sibling(n);
s->color = n->parent->color;
n->parent->color = BLACK;
if (n == n->parent->left) {
s->right->color = BLACK;
rotate_left(n->parent);
} else {
s->left->color = BLACK;
rotate_right(n->parent);
}
}