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