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łnym2 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 ilości 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 ilości 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ę:
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 między innymi
drzewa AVL4, drzewa, 2-3-drzewa5 oraz drzewa
czerwono
czarne, które zostaną tutaj szerzej
omówione.
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
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.
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
1.
każdy węzeł drzewa jest albo czerwony, albo czarny,
2.
każdy liść jest czarny6 jest czarny,
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ę
1
2
3
4
5
6
czarne:
kolor. Liczbę czarny węzłów od ustalonego węzła (ale bez wliczania go) do liścia
Pozostałe operacje na strukturze drzewiastej mają taką samą złożoność czasową, gdyż są zależne od operacji wyszukiwania.
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
nazywamy czarną wysokością tego węzła. Czarną wysokością drzew nazywamy czarną wysokość jego korzenia. Drzewa czerwono czarne o
„n” węzłach wewnętrznych mają wysokość 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
x
A
A
y
Rotacja w lewo
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
12
else x^.parent^.right_child:=y;
12
else x^.parent^.left_child:=y;
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
7
Dla procedury „right_rotate” zakładamy, że istnieje lewy potomek węzła „x”.
2
Podstawy Programowania
semestr drugi
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ę „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);
2 var
1 function tree_insert(var r:ptree; a:byte):ptree;
3 x,y:ptree;
4 begin
5 x:=tree_insert(r,a);
6 x^.color:=red;
7 while (x<>r) and (x^.parent^.color=red) do
8
9
10
11
12
if x^.parent = x^.parent^.parent^.left_child then
begin
y:=x^.parent^.parent^.right_child;
if y^.color=red then
begin
13
x^.parent^.color:=black;
14
y^.color:=black;
15
x^.parent^.parent^.color:=red;
16
x:=x^.parent^.parent;
17
18
19
end
else
begin
20
if x=x^.parent^.right_child then
21
begin
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
2 var
3 x,y,z:ptree;
end
else
begin
42
if x=x^.parent^.left_child then
43
begin
4 begin
5 y:=nil;
6 x:=r;
7 while x<>nil do
8 begin
9
10
y:=x;
if a < x^.key then x:=x^.left_child else x:=x^.right_child;
11 end;
12 new(z);
13 z^.parent:=y;
14 z^.key:=a;
15 z^.left_child:=nil;
16 z^.right_child:=nil;
17 if y=nil then r:=z
18 else if z^.key < y^.key then y^.left_child:=z else y^.right_child:=z;
19 tree_insert:=z;
20 end;
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
3
Podstawy Programowania
semestr drugi
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
45
right_rotate(r,x);
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
46
end;
(„dziadek”) nowego węzła jest czarny, ale zarówno prawy i lewy potomek „dziadka” oraz
47
x^.parent^.color:=black;
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
48
x^.parent^.parent^.color:=red;
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
49
left_rotate(r,x^.parent^.parent);
„x” zawierającego adres „dziadka” nowego węzła (wiersz 16). Przypadek drugi i trzeci
50
end;
zachodzą gdy „brat” węzła rodzicielskiego ma kolor czarny. Różnica między nimi polega na
tym, że w drugim przypadku nowy węzeł jest prawym, a w trzecim lewym potomkiem
51
end;
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 nowy węzeł są czerwone
52
r^.color:=black;
nie narusza to własności czwartej drzewa czerwono czarnego. Przypadek trzeci nie musi
53 end;
być poprzedzony przypadkiem drugim. Jeśli on występuje, to kolor „brata” rodzica nowego
węzła musi być czarny 8. 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.
44
x:=x^.parent;
11
2
14
1
7
15
5
8
4
„y”
„x”
Przypadek 1
11
2
14
1
„y”
„x”
7
15
5
8
4
Przypadek 2
11
„y”
7
„x”
14
2
8
15
5
1
4
Przypadek 3
7
11
„x”
2
14
8
1
15
5
4
8
Inaczej wystąpiłby przypadek pierwszy.
4

Podobne dokumenty