Spis treści 1 Drzewa 1.1 Drzewa binarne 1.1.1 Zadanie 1.1.2 Drzewo BST (Binary Search Tree) 1.1.2.1 Zadanie 1 1.1.2.2 Zadanie 2 1.1.2.3 Zadanie 3 1.1.2.4 Usuwanie węzła w drzewie BST 1.1.2.5 Zadanie 4 1.1.2.6 Równoważenie drzwa (zadanie domowe) 2 obiegi drzew Drzewa Przypomnij sobie krótki wstęp do teorii grafów przedstawiony na początku semestru. Drzewo jest strukturą składającą się z węzłów i krawędzi. Przykład drzewa. Węzłem wyróżnionym jest korzeń drzewa (na rysunku Figure 1 korzeniem jest węzeł o wartości 2). W drzewie nie występują cykle, czyli ścieżki zaczynające się i kończące na tym samym wierzchołku bez powtórzeń krawędzi. Drzewa są też spójne, co oznacza, że między dowolną parą wierzchołków istnieje ścieżka. Stopniem wierzchołka nazywamy liczbę krawędzi z niego wychodzących. W drzewach wprowadza się następujące pojęcia: liść węzeł o stopniu równym jeden, co oznacza, że liście są węzłami połączonymi tylko ze
swoim rodzicem. Na rysunku Figure 1 liśćmi są węzły o wartościach 9, 3, 4, 5, 11. Synowie węzły połączone z danym i leżące poniżej. Na rysunku Figure 1 synami są wszystkie węzły oprócz węzła o wartości 2, który jest korzeniem drzewa (ang. root). Ścieżka ciąg krawędzi łączących dwa wierzchołki. Od węzła 4 do węzła 5 mamy następującą ścieżkę krawędź 4-8, krawędź 8-2, krawędź 2-7, krawędź 7-6, krawędź 6-5. Poziom wierzchołka odległość wierzchołka od korzenia. Przykładowo wierzchołek 6 ma poziom 3. Wysokość drzewa maksymalny poziom drzewa. Wysokość drzewa na rysunku Figure 1 to 4. Stopień wierzchołka liczba krawędzi z nim sąsiadujących. Na rysunku Figure 1 węzeł o kluczu 2 ma stopień 3. Stopień drzewa maksymalny stopień wierzchołka. Drzewo przedstawione na rysunku Figure 2 ma stopień 2. Węzeł wewnętrzny węzeł nie będący liściem na rysunku Figure 1 węzłami wewnętrznymi są węzły o kluczach 6,7,2, i 8. drzewo można formalnie zdefiniować rekurencyjnie jako drzewo puste lub korzeń z listą drzew Przykład drzewa. Drzewa binarne Drzewo binarne jest drzewem, w którym stopień każdego wierzchołka (liczba połączeń danego wierzchołka) nie jest wyższy niż 3. Drzewo takie przedstawione jest na rysunku Figure 3. Drzewo takie można wygodnie reprezentować w tablicy jeżeli nadamy wierzchołkowi i-ty element tablicy, jego lewy syn będzie miał indeks 2i+1, a prawy 2i+2 w przypadku języków, w których wektory numerowane są od zera.
Przykład drzewa binarnego Reprezentacja drzewa binarnego przedstawionego na rysunku Figure 3, postaci tablicy jest następująca: indeks klucz 0 2 1 7 2 5 3 2 4 6 5 6 9 7 8 9 5 10 11 12 13 14 4 15 Zadanie <korzen litera="d"> <lewy litera="b"> <lewy litera="a"> </lewy> <prawy litera="c"> </prawy> </lewy>
<prawy litera="f"> <lewy litera="e"> </lewy> <prawy litera="g"> </prawy> </prawy> </korzen> Napisz funkcję zapisująca drzewo binarne do tablicy (albo listy) zw porządku prefiksowym (do tablicy/listy wpisz artybut/y). Przetestuj swój program na powyższym drzewie. W ramach zabawy spróbuj także zapisać drzewo w porządku infiksowym i postfiksowym. Drzewo BST (Binary Search Tree) Tutaj można obejrzeć symulację działania drzewa BST, jak również różnego innego rodzaju drzewiastych struktur danych. Drzewo BST (wyszukiwania binarnego) ma następującą własność dla każdego węzła wszystkie wartości znajdujące się w poddrzewie którego korzeniem jest jego prawy syn mają wartości większe niż on, a w poddrzewie którego korzeniem jest lewy syn mniejsze (albo na odwrót, ważna jest konsekwencja implementacji). Struktura ta jest bardzo pomocna przy wyszukiwaniu. Załóżmy, że chcemy znaleźć liczbę k. Jeśli k ma wartość większą niż klucz korzenia to rekurencyjnie szukamy w lewym poddrzewie, jeśli mniejszą to w prawym, jeśli równą to znaleźliśmy. Algorytm ten przedstawiony jest na rys. Figure 7. Drzewo BST wypisane infixowo daje ciąg uporządkowany. class Wezel(object): def init (self, klucz): self.lewysyn = None self.prawysyn = None self.klucz = klucz Figure 4: Inicjalizowanie klasy węzeł. class Wezel(object):... def sprawdz(self, klucz, rodzic=none): if klucz > self.klucz: if self.lewysyn is None: return None, None return self.lewysyn.sprawdz(klucz, self) elif klucz < self.klucz: if self.prawysyn is None: return None, None
return self.prawysyn.sprawdz(klucz, self) return self, rodzic Figure 5: Sprawdzenie czy wartość jest w drzewie BST. class Wezel(object): def wstaw(self, klucz): if klucz < self.klucz: if self.lewysyn is None: self.lewysyn = Wezel(klucz) self.lewysyn.wstaw(klucz) if self.prawysyn is None: self.prawysyn = Wezel(klucz) self.prawysyn.wstaw(klucz) Figure 6: Wstawianie do drzewa BST. def przeszukaj_drzewo_binarne(wezel, klucz): if wezel is None: return None # nie znaleziono klucza if klucz < wezel.klucz: return przeszukaj_drzewo_binarne(wezel.lewysyn, klucz) elif klucz > wezel.klucz: return przeszukaj_drzewo_binarne(wezel.prawysyn, klucz) # gdy szukany klucz jest rowny kluczowi danego wezla return wezel.klucz Figure 7: Przeszukiwanie drzewa binarnego. Zadanie 1 Algorytmy i fragmenty struktur danych opisane na przedstawione na rysunkach Figure 4, Figure 6, Figure 5, Figure 7 złoż w klasę Wezel. Sprawdź, czy wszystkie metody są poprawnie zapisane. Dopisz do klasy Wezel metodę wypisującą drzewo. Zadanie 2 Do klasy Wezel dodaj metodę wypisującą liczbę synów danego węzła. Zadanie 3 Porównanie dwóch drzew powinno zachodzić rekurencyjnie. Jeżeli natrafi na jeden różny węzeł w dwóch drzewach powinno zwracać False. Różny węzeł może znaczyć także brakujący liść. Jak
argument powinien być przekazywany korzeń drugiego drzewa. Do klasy Wezel dodaj taką metodę. Usuwanie węzła w drzewie BST Usuwanięcie węzła może wymagać reorganizacji struktury drzewa w celu zachowania własności BST. Należy rozważyć trzy przypadki usuwany węzeł nie ma synów (jest liściem): usunięcie przebiega bez reorganizacji drzewa, wskaźnik do węzła w jego ojcu zastępowany jest wskaźnikiem do węzła pustego usuwany węzeł ma jednego syna: dany węzeł usuwamy, a jego syna podstawiamy w miejsce usuniętego węzła usuwany węzeł ma dwóch synów: po jego usunięciu wstawiamy w jego miejsce węzeł, który jest jego następnikiem lub poprzednikiem (do wyboru albo implementacja z następnikiem albo z poprzednikiem). Następnik i poprzednik to odpowiednio najbardziej lewy węzeł w prawym podrzewie, lub najbardziej prawy węzeł węzeł w prawym poddrzewie. rodzic = wezel nastepnik = wezel.prawysyn while nastepnik.lewysyn: rodzic = nastepnik nastepnik = nastepnik.lewysyn wezel.klucz = nastepnik.klucz if rodzic.lewysyn == nastepnik: rodzic.lewysyn = nastepnik.prawysyn rodzic.prawysyn = nastepnik.prawysyn Zadanie 4 Dodaj do klasy metodę usuwającą węzeł o zadanym kluczy z drzewa BST. Równoważenie drzwa (zadanie domowe) Przeczytaj artykuł opisujący algorytm równoważenia drzewa DSW. Napisz własną implementację tego algorytmu w Pythonie, używając klasy Wezel. obiegi drzew możliwe są następujące obiegi drzew i kolejności odwiedzania: prefixowylp - my, lewy syn, prawy syn prefixowypl - my, prawy syn, lewy syn infixowylp - lewy syn, my, prawy syn infixowypl - prawy syn, my, lewy syn postfixowylp - lewy syn, prawy syn, my
postfixowypl - prawy syn, lewy syn, my obiegi te są powiązane ciekawymi twierdzeniami: prefixowylp = postfixowypl^{-1} prefixowypl = postfixowylp^{-1} infoxowylp = infixowypl^{-1} Materiały zostały przygotowane na postawie m.in. Binary Search Tree library na blogu Laurenta Luce'a