Podstawy Programowania – semestr drugi Wyk ad ósmy ł 1. Czas

Transkrypt

Podstawy Programowania – semestr drugi Wyk ad ósmy ł 1. Czas
Podstawy Programowania – semestr drugi
Wykład ósmy
1.
Czas wyszukiwania w drzewie BST
W podsumowaniu poprzedniego wykładu znalazła się informacja, że czas wyszukiwania elementu1 w drzewie BST, które jest drzewem pełnym 2 lub zbliżonym do
pełnego jest proporcjonalny do wysokości tego drzewa. Pełnym drzewem binarnym nazywamy takie drzewo, którego każdy węzeł ma stopień 2, a wszystkie liście
znajdują się na tym samym poziomie. Jeśli w drzewie BST umieszczamy węzły o losowych wartościach, które nie tworzą ciągów uporządkowanych, to drzewo takie może
być w pewnym stopniu zbliżone do drzewa pełnego i czas wyszukiwania elementów dla takiej struktury jest proporcjonalny do logarytmu przy podstawie dwa z liczby
węzłów tego drzewa. Niestety, jeśli wstawiamy do drzewa elementy uporządkowane malejąco lub rosnąco to otrzymujemy drzewo zdegenerowane, które jest listą
liniową3. Czas wyszukiwania w takiej liście jest proporcjonalny do ilości jej elementów, a więc w tym przypadku drzewo BST nie jest tak wydajną strukturą, jakbyśmy
tego sobie życzyli.
2.
Drzewa zrównoważone
Istnieje pewna klasa drzew, dla których czas wyszukiwania jest zawsze proporcjonalny do ich wysokości. Te drzewa nazywamy drzewami zrównoważonymi. Wśród tych
drzew można wyróżnić drzewa doskonale zrównoważone. Drzewo jest doskonale zrównoważone jeśli dla każdego jego węzła liczby węzłów w jego prawym i lewym
poddrzewie różnią się co najwyżej o jeden. Poniżej znajduje się funkcja, która tworzy taką przykładową strukturę:
Funkcja ta zwraca wskaźnik na korzeń drzewa doskonale
zrównoważonego. Przez pierwszy jej parametr przekazywana jest
ilość węzłów, które będą umieszczone w drzewie, natomiast drugi
parametr został dodany po to, aby każdemu węzłowi w drzewie
nadać wartość odpowiadającą jego poziomowi. Funkcja ta jest
funkcją rekurencyjną. Ciąg jej wywołań kończy się wtedy, kiedy
parametr „n” osiągnie wartość zero. Jeśli jego wartość jest różna
od zera wówczas określana jest ilość elementów w lewym
i prawym poddrzewie (wiersze 13 i 14) tworzonego węzła,
przydzielana jest pamięć na ten węzeł, oraz nadawana jest
wartość polom „dana”, „left” i „right” tego węzła. W przypadku
dwóch ostatnich pól ich zawartość jest zależna od tego, co zwrócą
kolejne rekurencyjne wywołania tej funkcji.
1 function create_perfectly_balanced_tree(n:word; x:integer):wskaznik;
2 var
3 nowy:wskaznik;
4 nl,np:word;
5 begin
6 if n=0 then
7
begin
8
create_perfectly_balanced_tree:=nil;
9
exit;
10
end
Niestety, operacja wstawiania, która dla losowych wartości
elementów utrzymywałaby doskonale zrównoważoną strukturę
takiego drzewa jest skomplikowana i nie jest stosowana
w praktyce, ze względu na czas jej działania. Na szczęście
drzewa doskonale zrównoważone są tylko podzbiorem
ogólniejszego zbioru drzew zrównoważonych, w których takie
operacje jak wstawianie, lokalizowanie i usuwanie węzła mają
czas wykonania zależny od ich wysokości. Do takich drzew
zalicza się między innymi drzewa AVL4, drzewa 2-3-drzewa5 oraz
drzewa czerwono – czarne, które zostaną tutaj szerzej opisane.
11 else
12
begin
13
nl:=n div 2;
14
np:=n-nl-1;
15
new(nowy);
16
nowy^.dana:=x;
17
nowy^.left:=create_perfectly_balanced_tree(nl,x+1);
18
nowy^.right:=create_perfectly_balanced_tree(np,x+1);
19
end;
20 create_perfectly_balanced_tree:=nowy;
21 end;
3.
Drzewa czerwono – czarne
Drzewa czerwono – czarne są drzewami BST, które spełnia następujące własności czerwono – czarne:
1.
każdy węzeł drzewa jest albo czerwony, albo czarny,
2.
każdy liść jest czarny6,
3.
jeśli węzeł jest czerwony, to jego obaj potomkowie są czarni,
4.
każda prosta ścieżka z ustalonego węzła do liścia ma tyle samo czarnych węzłów.
Każdy węzeł w drzewie ma określoną dodatkową cechę – kolor. Liczbę czarny węzłów od ustalonego węzła (ale bez wliczania go) do liścia nazywamy czarną
wysokością tego węzła. Czarną wysokością drzewa nazywamy czarną wysokość jego korzenia. Drzewa czerwono – czarne o „n” węzłach wewnętrznych mają wysokość
1
2
3
4
5
6
Złożoność czasowa operacji wstawiania lub usuwania elementu w drzewie jest stała, tak jak w listach, ale takie operacje są zawsze poprzedzone wyszukiwaniem.
Ten rodzaj drzewa nazywany jest również drzewem zupełnym.
Listę liniową można więc definiować jako drzewo zdegenerowane.
Nazwa pochodzi od pierwszych liter nazwisk odkrywców tej struktury – dwóch rosyjskich matematyków G. M. Adelsona-Wielskiego i J. M. Łandisa.
Te drzewa nie są drzewami binarnymi.
Jako liście w drzewie czerwono – czarnym są traktowane dowiązania o wartości „NIL”.
1
Podstawy Programowania – semestr drugi
co najwyżej 2lg(n+1). Typ określający węzeł takiego drzewa można zdefiniować następująco:
1 type
2 ptree = ^tree;
3 colour = (red, black);
4 tree = record
5
color:colour;
6
key:byte;
7
parent:ptree;
8
left_child:ptree;
9
right_child:ptree;
10 end;
Podstawowymi operacjami na tym drzewie są operacje rotacji w lewo i w prawo, które wykorzystywane są do przywrócenia własności drzewa czerwono – czarnego,
których mogło zostać pozbawione na skutek wstawienia lub usunięcie węzła. Operacje rotacji mają na celu przywrócenie uporządkowania elementów drzewa ze względu
na wartości ich kluczy. Działanie tych operacji można zilustrować następującym rysunkiem:
Rotacja w prawo
y
x
C
A
y
x
Rotacja w lewo
A
B
B
C
Lewą rotację można wykonać na węźle „x” wtedy i tylko wtedy, kiedy istnieje jego prawy potomek „y”. Rotację tą można traktować jako obrót wokół krawędzi
poprowadzonej między węzłami „x” i „y”. W wyniku jej wykonania węzeł „y” staje się nowym korzeniem poddrzewa, a „x” staje się jego lewym potomkiem. Lewy potomek
węzła „y” zostaje prawym synem węzła „x”. Rotacja w prawo jest symetryczna względem rotacji w lewo. Oto kod procedur, które wykonują obie operacje:
1 procedure left_rotate(var r:ptree; x:ptree);
1 procedure right_rotate(var r:ptree; x:ptree);
2 var
2 var
3 y:ptree;
3 y:ptree;
4 begin
4 begin
5 y:=x^.right_child;
5 y:=x^.left_child;
6 x^.right_child:=y^.left_child;
6 x^.left_child:=y^.right_child;
7 if y^.left_child<>nil then y^.left_child^.parent:=x;
7 if y^.right_child<>nil then y^.right_child^.parent:=x;
8 y^.parent:=x^.parent;
8 y^.parent:=x^.parent;
9 if x^.parent=nil then r:=y
9 if x^.parent=nil then r:=y
10 else
10 else
11
if x=x^.parent^.left_child then x^.parent^.left_child:=y
11
if x=x^.parent^.right_child then x^.parent^.right_child:=y
else x^.parent^.right_child:=y;
12
else x^.parent^.left_child:=y;
12
13 y^.left_child:=x;
13 y^.right_child:=x;
14 x^.parent:=y;
14 x^.parent:=y;
15 end;
15 end;
Ponieważ obie procedury są do siebie podobne zostanie omówiona tylko procedura „left_rotate”. Posiada ona dwa parametry, pierwszym jest wskaźnik na korzeń
drzewa, a drugim wskaźnik na węzeł, dla którego zostanie wykonana rotacja. Odpowiada on węzłowi „x” po prawej stronie rysunku umieszczonego wyżej. Wywołując ją
zakładamy, że prawy potomek tego węzła istnieje7. W wierszy 5 tej procedury, w zmiennej lokalnej „y” zapamiętywany jest wskaźnik na prawego potomka węzła „x”.
W kolejnym wierszu w polu „right_child” węzła „x” zapamiętywany jest adres lewego potomka węzła „y”. Jeśli ten potomek istnieje, to jego rodzicem staje się węzeł „x”,
a rodzicem węzła „y” dotychczasowy rodzic węzła „x”. Jeśli „x” nie miał rodzica, to oznacza, że był korzeniem drzewa i teraz węzeł „y” powinien nim zostać (wiersz 9).
W przeciwnym przypadku, jeśli „x” był lewym potomkiem swojego rodzica, to teraz powinien nim zostać „y” (wiersz 11). Jeśli jednak „x” był prawym potomkiem rodzica,
to teraz tym potomkiem powinien zostać „y” (wiersz 12). W wierszu 13 „x” zostaje lewym potomkiem „y”, a w wierszu 14 „y” staje się rodzicem „x”. Procedurę
7
Dla procedury „right_rotate” zakładamy, że istnieje lewy potomek węzła „x”.
2
Podstawy Programowania – semestr drugi
„right_rotate” otrzymuje się z procedury „left_rotate” poprzez zamianę nazwy pola „right_child” na „left_child” i odwrotnie. Obie te procedury wykonują się w czasie
O(1). Właściwe wstawienie elementu do drzewa czerwono – czarnego odbywa się poprzez wywołanie funkcji „tree_insert”, której kod jest następujący:
1 procedure rb_insert(var r:ptree; a:byte);
1 function tree_insert(var r:ptree; a:byte):ptree;
2 var
2 var
3 x,y:ptree;
3 x,y,z:ptree;
4 begin
4 begin
5 x:=tree_insert(r,a);
5 y:=nil;
6 x^.color:=red;
6 x:=r;
7 while (x<>r) and (x^.parent^.color=red) do
7 while x<>nil do
8
if x^.parent = x^.parent^.parent^.left_child then
8 begin
9
begin
9
10
10
11
y:=x;
y:=x^.parent^.parent^.right_child;
if a < x^.key then x:=x^.left_child else x:=x^.right_child;
if y^.color=red then
11 end;
12
begin
12 new(z);
13
x^.parent^.color:=black;
14
y^.color:=black;
15
x^.parent^.parent^.color:=red;
16
x:=x^.parent^.parent;
13 z^.parent:=y;
14 z^.key:=a;
15 z^.left_child:=nil;
16 z^.right_child:=nil;
17
end
17 if y=nil then r:=z
18
else
18 else if z^.key < y^.key then y^.left_child:=z else y^.right_child:=z;
19
begin
19 tree_insert:=z;
20
if x=x^.parent^.right_child then
21
begin
20 end;
22
x:=x^.parent;
23
left_rotate(r,x);
24
end;
25
x^.parent^.color:=black;
26
x^.parent^.parent^.color:=red;
27
28
29
30
31
right_rotate(r,x^.parent^.parent);
end;
end
else
begin
32
y:=x^.parent^.parent^.left_child;
33
if y^.color=red then
34
begin
35
x^.parent^.color:=black;
36
y^.color:=black;
37
x^.parent^.parent^.color:=red;
38
x:=x^.parent^.parent;
39
40
41
end
else
begin
42
if x=x^.parent^.left_child then
43
begin
Funkcja ta jest iteracyjną wersją procedur rekurencyjnych, które wstawiały węzeł do
drzewa BST. W pętli „while” przeszukiwane jest całe drzewo celem znalezienia miejsca,
gdzie należy wstawić nowy węzeł. Po jej zakończeniu zmienna lokalna „y” zawiera adres
rodzica węzła, który zostanie utworzony. Następnie przydzielana jest pamięć na ten węzeł
i inicjalizowane są jego pola. Jeśli zmienna „y” miała wartość „nil”, to nowy węzeł, którego
adres jest przechowywany w zmiennej „z”, powinien zostać korzeniem drzewa.
W przeciwnym przypadku porównywana jest wartość zapisana w polu „key” nowego
węzła, z odpowiednim polem jego przyszłego rodzica, którego adres jest umieszczony
w zmiennej „y”. Jeśli ta wartość jest mniejsza, to nowy węzeł staje się lewym potomkiem
węzła rodzicielskiego, a jeśli większa, to powinien zostać prawym potomkiem tego węzła.
Na koniec funkcja zwraca adres nowego węzła. Nie jest to koniec operacji wstawiania
węzła do drzewa czerwono – czarnego. Jak zostało to wcześniej wspomniane, taka
operacja może spowodować, że drzewo przestanie spełniać wymienione cztery własności
czerwono – czarne. Zadanie ich przywrócenia wykonuje procedura „rb_insert”, a właściwie
ta część kodu, która pozostaje po wywołaniu funkcji „tree_inset”. Zanim przystąpimy do
omówienia działania tego kodu, zastanówmy się jakie własności mogą zostać naruszone
po dodaniu nowego węzła do istniejącego drzewa. Okazuje się, że jedyną taką własnością
jest własność trzecia. Nowy węzeł jest wstępnie kolorowany na czerwono (wiersz 6) . Jeśli
jego rodzic też ma kolor czerwony, to zostaje naruszona wcześniej wspomniana własność.
W pętli „while” procedura „rb_insert” przesuwa to zaburzenie w górę drzewa, jednocześnie
dbając, aby zachowana była własność czwarta drzew czerwono – czarnych. Pętla „while”
kończy się kiedy, po wykonaniu kilku rotacji zaburzenie zostanie zlikwidowane lub kiedy
zaburzenie, które po każdej iteracji jest przesuwane w „górę” drzewa, „dojdzie” do
korzenia. Jeśli korzeń i któryś z jego potomków będzie czerwony, to aby przywrócić
własność drzew czerwono – czarnych należy zmienić kolor korzenia na czarny. W pętli
„while” rozpatrywanych jest sześć przypadków. My ograniczymy się do rozpatrzenia
połowy z nich, gdyż druga połowa jest symetryczna i kod je rozpatrujący można uzyskać
poprzez skopiowanie kodu dotyczącego wcześniejszych przypadków i zamianę nazw pól
„left_child” na „right_child” i vice versa. Decyzja, która połowa tych przypadków będzie
rozpatrywana podejmowana jest w wierszu 8, gdzie badane jest, czy rodzic węzła
wstawionego jest lewym, czy prawym potomkiem swojego rodzica. Poniżej zostaną
rozpatrzone czynności dla przypadku, kiedy jest on lewym potomkiem. Ponieważ korzeń
drzewa czerwono – czarnego jest zawsze czarny, to rodzic nowego węzła nie może nim być
i możemy przyjąć, że jego rodzic istnieje. W wierszu 10 zmiennej „y” przypisujemy adres
węzła, który wraz z węzłem rodzicielskim nowego węzła tworzy rodzeństwo. W kolejnym
wierszu sprawdzamy kolor tego węzła. Jeśli jest on czerwony, to zachodzi wówczas
pierwszy przypadek, czyli rodzic rodzica („dziadek”) nowego węzła jest czarny, ale
3
Podstawy Programowania – semestr drugi
44
x:=x^.parent;
45
right_rotate(r,x);
46
end;
47
x^.parent^.color:=black;
48
x^.parent^.parent^.color:=red;
49
left_rotate(r,x^.parent^.parent);
50
zarówno prawy i lewy potomek „dziadka” oraz nowy węzeł są czerwone. Problem ten jest rozwiązywany
przez zmianę koloru rodzica i jego „brata” na czarny, a „dziadka” na czerwony (aby nie naruszyć
własności czwartej). W wyniku tej operacji albo otrzymujemy drzewo czerwono – czarne spełniające
wszystkie warunki, albo zaburzenie przenosi się na wyższy poziom drzewa i należy powtórzyć opisane
czynności dla „x” zawierającego adres „dziadka” nowego węzła (wiersz 16). Przypadek drugi i trzeci
zachodzą gdy „brat” rodzica węzła wskazywanego przez „x” ma kolor czarny. Różnica między nimi polega
na tym, że w drugim przypadku węzeł wskazywany przez „x” jest prawym, a w trzecim lewym potomkiem
swojego rodzica. Drugi przypadek można rozwiązać stosując rotację w lewo, sprowadzając w ten sposób
przypadek drugi do trzeciego. Ponieważ węzeł rodzica i węzeł „x” są czerwone nie narusza to własności
czwartej drzewa czerwono – czarnego. Przypadek trzeci nie musi być poprzedzony przypadkiem drugim.
Jeśli on występuje, to kolor „brata” rodzica nowego węzła musi być czarny8. Aby przywrócić naruszoną
własność drzewa czerwono – czarnego zmieniamy kolor rodzica na czarny, a „dziadka” na czerwony
i wykonujemy rotację w prawo, która zachowuje własność czwartą i przywraca własność trzecią. Poniżej
umieszczono ilustrację działania tej procedury dla przykładowego drzewa.
end;
51
end;
52
r^.color:=black;
53 end;
11
2
14
1
7
15
5
8
„y”
4
„x”
Przypadek 1
11
2
„y”
14
1
„x”
7
15
5
8
4
Przypadek 2
11
„y”
7
„x”
14
2
8
15
5
1
4
Przypadek 3
7
„x”
11
2
14
8
1
5
15
4
8
Inaczej wystąpiłby przypadek pierwszy.
4