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