Algorytmy i struktury danych

Podobne dokumenty
Drzewo. Drzewo uporządkowane ma ponumerowanych (oznaczonych) następników. Drzewo uporządkowane składa się z węzłów, które zawierają następujące pola:

Wykład 2. Drzewa zbalansowane AVL i 2-3-4

Wysokość drzewa Głębokość węzła

ZASADY PROGRAMOWANIA KOMPUTERÓW ZAP zima 2014/2015. Drzewa BST c.d., równoważenie drzew, kopce.

Drzewa binarne. Drzewo binarne to dowolny obiekt powstały zgodnie z regułami: jest drzewem binarnym Jeśli T 0. jest drzewem binarnym Np.

Podstawy Informatyki. Metody dostępu do danych

Porządek symetryczny: right(x)

Wykład 8. Drzewa AVL i 2-3-4

Drzewa czerwono-czarne.

Wykład 2. Drzewa poszukiwań binarnych (BST)

Struktury Danych i Złożoność Obliczeniowa

WSTĘP DO INFORMATYKI. Drzewa i struktury drzewiaste

Przypomnij sobie krótki wstęp do teorii grafów przedstawiony na początku semestru.

Algorytmy i Struktury Danych

Algorytmy i Struktury Danych

Wykład 6. Drzewa poszukiwań binarnych (BST)

Algorytmy i struktury danych. wykład 5

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Algorytmy i struktury danych Laboratorium 7. 2 Drzewa poszukiwań binarnych

Koszt zamortyzowany. Potencjał - Fundusz Ubezpieczeń Kosztów Algorytmicznych

Każdy węzeł w drzewie posiada 3 pola: klucz, adres prawego potomka i adres lewego potomka. Pola zawierające adresy mogą być puste.

Algorytmy i Struktury Danych.

Podstawy programowania 2. Temat: Drzewa binarne. Przygotował: mgr inż. Tomasz Michno

Drzewa poszukiwań binarnych

Algorytmy i Struktury Danych, 9. ćwiczenia

ALGORYTMY I STRUKTURY DANYCH

Drzewa BST i AVL. Drzewa poszukiwań binarnych (BST)

Wykład 3. Drzewa czerwono-czarne

Kolejka priorytetowa. Często rozważa się kolejki priorytetowe, w których poszukuje się elementu minimalnego zamiast maksymalnego.

Algorytmy i Struktury Danych

Sortowanie bąbelkowe

Wykład 8 - Drzewa i algorytmy ich przetwarzania

Listy, kolejki, stosy

Wykład X. Programowanie. dr inż. Janusz Słupik. Gliwice, Wydział Matematyki Stosowanej Politechniki Śląskiej. c Copyright 2016 Janusz Słupik

Tadeusz Pankowski

Wyszukiwanie w BST Minimalny i maksymalny klucz. Wyszukiwanie w BST Minimalny klucz. Wyszukiwanie w BST - minimalny klucz Wersja rekurencyjna

Algorytmy i. Wykład 5: Drzewa. Dr inż. Paweł Kasprowski

Drzewa poszukiwań binarnych

Algorytmy i struktury danych. Drzewa: BST, kopce. Letnie Warsztaty Matematyczno-Informatyczne

< K (2) = ( Adams, John ), P (2) = adres bloku 2 > < K (1) = ( Aaron, Ed ), P (1) = adres bloku 1 >

. Podstawy Programowania 2. Drzewa bst - część druga. Arkadiusz Chrobot. 12 maja 2019

prowadzący dr ADRIAN HORZYK /~horzyk tel.: Konsultacje paw. D-13/325

Teoretyczne podstawy informatyki

Drzewa wyszukiwań binarnych (BST)

Algorytmy i Struktury Danych. (c) Marcin Sydow. Słownik. Tablica mieszająca. Słowniki. Słownik uporządkowany. Drzewo BST.

Drzewa AVL definicje

Wstęp do programowania. Drzewa podstawowe techniki. Piotr Chrząstowski-Wachtel

Analiza algorytmów zadania podstawowe

Zadanie 1 Przygotuj algorytm programu - sortowanie przez wstawianie.

Teoretyczne podstawy informatyki

Programowanie obiektowe

Podstawy Informatyki Metody dostępu do danych

Algorytmy i struktury danych Struktury danych IS/IO, WIMiIP

Programowanie obiektowe

Algorytmy i struktury danych Struktury danych - drzewa IS/IO, WIMiIP

Lista liniowa dwukierunkowa

Sortowanie. Bartman Jacek Algorytmy i struktury

ALGORYTMY I STRUKTURY DANYCH

Drzewa podstawowe poj

Algorytmy i Struktury Danych. Co dziś? Drzewo decyzyjne. Wykład IV Sortowania cd. Elementarne struktury danych

liniowa - elementy następują jeden za drugim. Graficznie możemy przedstawić to tak:

Wykład 3. Złożoność i realizowalność algorytmów Elementarne struktury danych: stosy, kolejki, listy

Definicja pliku kratowego

AiSD zadanie drugie. Gliwiński Jarosław Marek Kruczyński Konrad Marek Grupa dziekańska I5. 10 kwietnia 2008

. Podstawy Programowania 2. Drzewa bst - część pierwsza. Arkadiusz Chrobot. 22 maja 2016

PLAN WYKŁADU BAZY DANYCH INDEKSY - DEFINICJE. Indeksy jednopoziomowe Indeksy wielopoziomowe Indeksy z użyciem B-drzew i B + -drzew

Algorytmy i struktury danych. Wykład 4 Tablice nieporządkowane i uporządkowane

ALGORYTMY I STRUKTURY DANYCH

Wykład 8. Drzewo rozpinające (minimum spanning tree)

Wstęp do programowania. Drzewa. Piotr Chrząstowski-Wachtel

Sortowanie. Kolejki priorytetowe i algorytm Heapsort Dynamiczny problem sortowania:

Modelowanie hierarchicznych struktur w relacyjnych bazach danych

Indeksy. Wprowadzenie. Indeksy jednopoziomowe indeks podstawowy indeks zgrupowany indeks wtórny. Indeksy wielopoziomowe

Algorytmy i struktury danych

Kompresja danych Streszczenie Studia Dzienne Wykład 10,

Struktury danych: stos, kolejka, lista, drzewo


STRUKTURY DANYCH I ZŁOŻONOŚĆ OBLICZENIOWA STRUKTURY DANYCH I ZŁOŻONOŚĆ OBLICZENIOWA. Część 3. Drzewa Przeszukiwanie drzew

Stos LIFO Last In First Out

Przykładowe B+ drzewo

Strategia "dziel i zwyciężaj"

Drzewo binarne BST. LABORKA Piotr Ciskowski

Wykład 5. Sortowanie w czasie liniowologarytmicznym

Ogólne wiadomości o drzewach

BAZY DANYCH. Microsoft Access. Adrian Horzyk OPTYMALIZACJA BAZY DANYCH I TWORZENIE INDEKSÓW. Akademia Górniczo-Hutnicza

Programowanie obiektowe i C++ dla matematyków

Bazy danych - BD. Indeksy. Wykład przygotował: Robert Wrembel. BD wykład 7 (1)

PODSTAWY INFORMATYKI wykład 6.

dodatkowe operacje dla kopca binarnego: typu min oraz typu max:

Wstęp do programowania

Grafy (3): drzewa. Wykłady z matematyki dyskretnej dla informatyków i teleinformatyków. UTP Bydgoszcz

Złożoność obliczeniowa algorytmu ilość zasobów komputera jakiej potrzebuje dany algorytm. Pojęcie to

Sztuczna Inteligencja i Systemy Doradcze

TEORETYCZNE PODSTAWY INFORMATYKI

OSTASZEWSKI Paweł (55566) PAWLICKI Piotr (55567) Algorytmy i Struktury Danych PIŁA

Matematyka dyskretna - 7.Drzewa

Algorytm obejścia drzewa poszukiwań i zadanie o hetmanach szachowych

Bazy danych. Andrzej Łachwa, UJ, /15

Wykład 7 Abstrakcyjne typy danych słownik (lista symboli)

Wstęp do programowania

Distributed Hash Tables i ich zastosowania

Transkrypt:

Algorytmy i struktury danych Wykład 9 - Drzewa i algorytmy ich przetwarzania (ciąg dalszy) Janusz Szwabiński Plan wykładu: Binarne drzewo poszukiwań (BST) Zrównoważone binarne drzewa poszukiwań (AVL) Implementacja tablicy asocjacyjnej (mapy) - podsumowanie Drzewa czerwono-czarne Źródła: większość ilustracji i przykładów pochodzi z "Problem Solving with Algorithms and Data Structures using Python", http://interactivepython.org/runestone/static/pythonds/index.html Binarne drzewo poszukiwań (ang. binary search tree, BST) drzewo binarne, w którym: lewe poddrzewo każdego węzła zawiera wyłącznie elementy o kluczach nie większych niż klucz węzła prawe poddrzewo każdego węzła zawiera wyłącznie elementy o kluczach nie mniejszych niż klucz węzła węzły, oprócz klucza, przechowują wskaźniki na swojego lewego i prawego syna oraz na swojego ojca przechodząc drzewo metodą inorder uzyskamy ciąg kluczy posortowanych niemalejąco koszty operacji (wstawianie, wyszukiwanie, usuwanie): proporcjonalny do wysokości (liczby poziomów) drzewa h w przypadku drzew zrównoważonych h log 2 n, gdzie n to liczba węzłów optymistyczny koszt operacji to O(logn) w przypadku drzew skrajnie niezrównoważonych może być nawet h n pesymistyczny koszt wzrasta do O(n) można wykorzystać do implementacji tablic asocjacyjnych (czyli par klucz-wartość) Interfejs abstrakcyjnej tablicy asocjacyjnej Map() - tworzy pustą mapę (tablicę asocjacyjną) put(key,val) - dodaje nową parę klucz-wartość do tablicy. Jeśli klucz jest już w tablicy, odpowiadająca mu wartość jest zmieniana na nową get(key) - odczytuje wartość odpowiadającą kluczowi del map[key] - usuwa parę klucz-wartość z tablicy len() - liczba par klucz-wartość zapisana w tablicy in - operator przynależności, zwraca True jeżeli klucz znajduje się w tablicy Implementacja rozważmy najpierw proces tworzenia drzewa dla następującej listy kluczy: 70,31,93,94,14,23,73 pierwszy element listy, czyli 70 staje się korzeniem 31 jest mniejsze od 70, więc staje się jego lewym dzieckiem 93 jest większe od 70, więc staje się prawym dzieckiem korzenia 94 jest większe od 70 i od 93, więc staje się prawym dzieckiem elementu 93

14 jest mniejsze od 70 i od 31, więc staje się lewym dzieckiem elementu 31 23 jest mniejsze od 70 i 31, ale większe od 14, więc staje się prawym dzieckiem elementu 14 zaimplementujemy BST przy pomocy dwóch klas: TreeNode - węzeł drzewa, zawiera referencje do dzieci i rodzica oraz szereg funkcji pomocniczych, które pozwalają sklasyfikować węzeł na podstawie jego położenia BinarySearchTree - właściwe drzewo, zawiera referencję do korzenia drzewa, która jest równa None (drzewo puste) lub wskazuje na konkret klasy TreeNode Podstawowe klasy In [2]: class TreeNode: def init (self,key,val,left=none,right=none, parent=none): self.key = key self.payload = val self.leftchild = left self.rightchild = right self.parent = parent def hasleftchild(self): return self.leftchild def hasrightchild(self): return self.rightchild def isleftchild(self): return self.parent and self.parent.leftchild == self def isrightchild(self): return self.parent and self.parent.rightchild == self def isroot(self): return not self.parent def isleaf(self): return not (self.rightchild or self.leftchild) def hasanychildren(self): return self.rightchild or self.leftchild def hasbothchildren(self): return self.rightchild and self.leftchild def replacenodedata(self,key,value,lc,rc): self.key = key self.payload = value self.leftchild = lc self.rightchild = rc if self.hasleftchild(): self.leftchild.parent = self if self.hasrightchild(): self.rightchild.parent = self szablon właściwej klasy może wyglądać tak:

In [1]: class BinarySearchTree: def init (self): self.root = None self.size = 0 def length(self): def len (self): def iter (self): return self.root. iter () Wstawianie elementów do drzewa metoda put jeśli drzewo jest puste, tworzymy nową instancję TreeNode i wstawiamy ją w miejsce korzenia jeśli drzewo ma już korzeń, wówczas: zaczynając od korzenia, porównujemy wartość nowego klucza z kluczem aktualnego węzła: jeśli jest mniejsza, przechodzimy do lewego poddrzewa jeśli jest większa, przechodzimy do prawego poddrzewa jeśli nie ma poddrzew, znaleźliśmy pozycję do wstawienia nowego klucza tworzymy nową instancję TreeNode i wstawiamy ją na znalezioną pozycję

In [3]: class BinarySearchTree: def init (self): self.root = None self.size = 0 def length(self): def len (self): def iter (self): return self.root. iter () def put(self,key,val): if self.root: self._put(key,val,self.root) #_put is a helper function self.root = TreeNode(key,val) self.size = self.size + 1 def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) dodatkowo możemy przeładować jeszcze operator [], co pozwoli na korzystanie z nowej struktury jak ze słownika: In [4]: class BinarySearchTree: def init (self): self.root = None self.size = 0 def length(self): def len (self): def iter (self): return self.root. iter () def put(self,key,val): if self.root: self._put(key,val,self.root) #_put is a helper function self.root = TreeNode(key,val) self.size = self.size + 1 def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) def setitem (self,k,v): #overloading of [] operator self.put(k,v)

Odczytywanie wartości metoda get rekursywne przeszukiwanie drzewa do momentu: znalezienia podanego klucza dojścia do liścia, którego klucz ma inną wartość (klucza nie ma w drzewie) dodatkowo znowu przeładujemy operator [], aby za jego pomocą można było odczytywać wartości In [5]: class BinarySearchTree: def init (self): self.root = None self.size = 0 def length(self): def len (self): def iter (self): return self.root. iter () def put(self,key,val): if self.root: self._put(key,val,self.root) #_put is a helper function self.root = TreeNode(key,val) self.size = self.size + 1 def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) def setitem (self,k,v): #overloading of [] operator self.put(k,v) def get(self,key): if self.root: res = self._get(key,self.root) if res: return res.payload return None return None def _get(self,key,currentnode): if not currentnode: return None elif currentnode.key == key: return currentnode elif key < currentnode.key: return self._get(key,currentnode.leftchild) return self._get(key,currentnode.rightchild) def getitem (self,key): #overloading of [] operator return self.get(key) Sprawdzanie przynależności metoda get pozwala na natychmiastowe zaimplementowanie operatora in

In [ ]: class BinarySearchTree: def init (self): self.root = None self.size = 0 def length(self): def len (self): def iter (self): return self.root. iter () def put(self,key,val): if self.root: self._put(key,val,self.root) #_put is a helper function self.root = TreeNode(key,val) self.size = self.size + 1 def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) def setitem (self,k,v): #overloading of [] operator self.put(k,v) def get(self,key): if self.root: res = self._get(key,self.root) if res: return res.payload return None return None def _get(self,key,currentnode): if not currentnode: return None elif currentnode.key == key: return currentnode elif key < currentnode.key: return self._get(key,currentnode.leftchild) return self._get(key,currentnode.rightchild) def getitem (self,key): #overloading of [] operator return self.get(key) def contains (self,key): # overloading of in operator if self._get(key,self.root): return True return False Usuwanie elementów metoda delete i przeciążony operator del najtrudniejsza z metod kroki: znalezienie węzła z podanym kluczem metodą _get trzy możliwości - znaleziony węzeł: jest liściem (nie ma dzieci) najprostsza możliwość

usuwamy element i referencję do niego w węźle rodzicu ma jedno dziecko przesuwamy dziecko w miejsce usuwanego rodzica ma dwoje dzieci przeszukujemy poddrzewo usuwanego węzła, aby znaleźć kandydata na jego miejsce, czyli następnik (ang. successor) będzie to węzeł o najmniejszym kluczu większym od klucza węzła usuwanego powinien on mieć tylko prawe dziecko usuwamy go jak w poprzednim przypadku wstawiamy na miejsce usuwanego węzła In [6]: class BinarySearchTree: def init (self): self.root = None self.size = 0 def length(self):

def length(self): def len (self): def iter (self): return self.root. iter () def put(self,key,val): if self.root: self._put(key,val,self.root) #_put is a helper function self.root = TreeNode(key,val) self.size = self.size + 1 def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) def setitem (self,k,v): #overloading of [] operator self.put(k,v) def get(self,key): if self.root: res = self._get(key,self.root) if res: return res.payload return None return None def _get(self,key,currentnode): if not currentnode: return None elif currentnode.key == key: return currentnode elif key < currentnode.key: return self._get(key,currentnode.leftchild) return self._get(key,currentnode.rightchild) def getitem (self,key): #overloading of [] operator return self.get(key) def contains (self,key): # overloading of in operator if self._get(key,self.root): return True return False def delete(self,key): if self.size > 1: nodetoremove = self._get(key,self.root) if nodetoremove: self.remove(nodetoremove) self.size = self.size-1 raise KeyError('Error, key not in tree') elif self.size == 1 and self.root.key == key: self.root = None self.size = self.size - 1 raise KeyError('Error, key not in tree') def delitem (self,key): #overloading of del operator self.delete(key) def spliceout(self):

def spliceout(self): if self.isleaf(): if self.isleftchild(): self.parent.leftchild = None self.parent.rightchild = None elif self.hasanychildren(): if self.hasleftchild(): if self.isleftchild(): self.parent.leftchild = self.leftchild self.parent.rightchild = self.leftchild self.leftchild.parent = self.parent if self.isleftchild(): self.parent.leftchild = self.rightchild self.parent.rightchild = self.rightchild self.rightchild.parent = self.parent def findsuccessor(self): succ = None if self.hasrightchild(): succ = self.rightchild.findmin() if self.parent: if self.isleftchild(): succ = self.parent self.parent.rightchild = None succ = self.parent.findsuccessor() self.parent.rightchild = self return succ def findmin(self): current = self while current.hasleftchild(): current = current.leftchild return current def remove(self,currentnode): if currentnode.isleaf(): #leaf if currentnode == currentnode.parent.leftchild: currentnode.parent.leftchild = None currentnode.parent.rightchild = None elif currentnode.hasbothchildren(): #interior succ = currentnode.findsuccessor() succ.spliceout() currentnode.key = succ.key currentnode.payload = succ.payload # this node has one child if currentnode.hasleftchild(): if currentnode.isleftchild(): currentnode.leftchild.parent = currentnode.parent currentnode.parent.leftchild = currentnode.leftchild elif currentnode.isrightchild(): currentnode.leftchild.parent = currentnode.parent currentnode.parent.rightchild = currentnode.leftchild currentnode.replacenodedata(currentnode.leftchild.key, currentnode.leftchild.payload, currentnode.leftchild.leftchild, currentnode.leftchild.rightchild) if currentnode.isleftchild(): currentnode.rightchild.parent = currentnode.parent currentnode.parent.leftchild = currentnode.rightchild elif currentnode.isrightchild(): currentnode.rightchild.parent = currentnode.parent currentnode.parent.rightchild = currentnode.rightchild currentnode.replacenodedata(currentnode.rightchild.key, currentnode.rightchild.payload, currentnode.rightchild.leftchild, currentnode.rightchild.rightchild)

In [7]: mytree = BinarySearchTree() mytree[3]="red" mytree[4]="blue" mytree[6]="yellow" mytree[2]="at" print(mytree[6]) print(mytree[2]) yellow at Analiza binarnych drzew wyszukiwania szukając miejsca do wstawienia nowej pary klucz-wartość, w najgorszym wypadku wykonamy liczbę porównań równą wysokości drzewa wysokość to nic innego jak liczba krawędzi między korzeniem i najgłębiej położonym liściem wydajność metody put ograniczona jest wysokością drzewa jeśli wartości wstawiane są do drzewa w losowym porządku kluczy, wysokość h log 2 n ponieważ klucze są losowo uporządkowane, mniej więcej połowa z nich będzie mniejsza od korzenia, a druga połowa - większa w drzewie binarnym mamy jeden element na poziomie pierwszym (korzeń), maksymalnie dwa elementy na kolejnym itd. na poziomie d mamy zatem 2 d elementów jeżeli drzewo jest idealnie zrównoważone, całkowita liczba węzłów wynosi n = 2 h + 1 1 idealnie zrównoważone drzewo ma tę samą liczbę węzłów w każdym poddrzewie rzeczywiście mamy więc h = log 2 n wysokość jest ograniczeniem wydajności - put jest klasy O(logn) jeśli wstawiane wartości wstawiane są według posortowanych kluczy drzewo ma wysokość h = n w tym wypadku put jest klasy O(n) metody get, in i del mają podobne ograniczenia wydajności Zrównoważone binarne drzewa poszukiwań (AVL) Definicja zrównoważone binarne drzewo poszukiwań skrót AVL pochodzi od nazwisk rosyjskich matematyków: Adelsona-Velskiego oraz Landisa (właściwie: Gieorgij Adelson-Wielskij i Jewgienij Łandis) rozwiązuje problem utrzymania dobrej wydajności operacji na drzewie (czyli jego dobrej struktury) każdemu węzłowi przypisuje się współczynnik wyważenia (ang. balance factor): balancefactor = height(leftsubtree) height(rightsubtree)

drzewo jest zrównoważone, jeśli współczynnik wyważenia wynosi 0, +1 lub -1 wstawiając lub usuwając węzły tak, aby zachować własności drzewa BST, modyfikuje się również współczynnik wyważenia gdy współczynnik przyjmuje niedozwoloną wartość, wykonuje się operację rotacji węzłów w celu przywrócenia zrównoważenia Wydajność motywacja dla drzew AVL jest taka, że utrzymując współczynnik zrównoważenia w dopuszczalnych granicach poprawimy złożoność najważniejszych opreacji rozważmy 3 drzewa o wysokościach 0, 1, 2 i 3 w ich najmniej zrównoważonej wersji zachowującej poprawne wartości współczynników wyważenia

liczba węzłów w funkcji wysokości w powyższym przykładzie: N 0 = 1 N 1 = 1 + 1 = 2 N 2 = 1 + 1 + 2 = 4 N 3 = 1 + 2 + 4 = 7 ogólnie otrzymaliśmy zależność zależność ta bardzo przypomina ciąg Fibonacciego: N h = 1 + N h 1 + N h 2 F 0 = 0 F 1 = 1 F i = F i 1 + F i 2 for all i 2 wiemy, że lim gdzie \Phi to złoty podział wyraźmy liczbę węzłów w drzewie AVL przez wyrazy ciągu Fibonacciego: N_h = F_{h+2} - 1, h \ge 1 ponadto załóżmy, że: F_i = \Phi^i/\sqrt{5} wówczas N_h = \frac{\phi^{h+2}}{\sqrt{5}} - 1 \begin{eqnarray} \log{n_h+1} & = & (h+2)\log{\phi} - \frac{1}{2} \log{5} \\ h & = & \frac{\log{n_h+1} - 2 \log{\phi} + \frac{1}{2} \log{5}}{\log{\phi}} \\ h & = & 1.44 \log{n_h} \end{eqnarray} drzewa AVL również w najgorszym przypadku mają wysokość równą pewnej stałej pomnożonej przez logarytm z liczby węzłów wydajność podstawowych operacji pozostaje na poziomie O(\log N)! Implementacja możemy zaimplementować drzewo AVL jako klasę pochodną drzew BST musimy nadpisać część definicji metod pomocniczych na takie, które zachowują własności drzew AVL Wstawianie elementów nowe klucze są wstawiane do drzewa jako liście ich współczynnik wyważenia wynosi 0 musimy jednak zaktualizować współczynnik rodzica następnie rekursywnie wyliczamy na nowo współczynniki wszystkich przodków dwa przypadki bazowe: doszliśmy do korzenia rodzic po aktualizacji ma współczynnik wyważenia równy 0 (jeśli jakieś poddrzewo ma współczynnik 0, to współczynnik przodka nie ulega zmianie musimy nadpisać metodę _put i dopisać nową funkcję pomocniczą do aktualizowania współczynnika wyważenia

In [8]: class AVLTree(BinarySearchTree): def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) self.updatebalance(currentnode.leftchild) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) self.updatebalance(currentnode.rightchild) def updatebalance(self,node): if node.balancefactor > 1 or node.balancefactor < -1: self.rebalance(node) return if node.parent!= None: if node.isleftchild(): node.parent.balancefactor += 1 elif node.isrightchild(): node.parent.balancefactor -= 1 if node.parent.balancefactor!= 0: self.updatebalance(node.parent) aby ponownie zrównoważyć drzewo, konieczna może być rotacja węzłów rotacja w lewo odbywa się w następujący sposób: prawe dziecko B staje się korzeniem poddrzewa dawny korzeń jest teraz nowym lewym dzieckiem jeśli węzeł B miał prawe dziecko, pozostaw je w kolejnym przykładzie wymagana jest rotacja w prawo przesuń C w miejsce korzenia poprzedni korzeń E staje się prawym dzieckiem nowego korzenia jeśli C miał prawe dziecko (D), staje się ono lewym dzieckiem nowego prawego dziecka (E)

In [9]: class AVLTree(BinarySearchTree): def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) self.updatebalance(currentnode.leftchild) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) self.updatebalance(currentnode.rightchild) def updatebalance(self,node): if node.balancefactor > 1 or node.balancefactor < -1: self.rebalance(node) #to be implemented return if node.parent!= None: if node.isleftchild(): node.parent.balancefactor += 1 elif node.isrightchild(): node.parent.balancefactor -= 1 if node.parent.balancefactor!= 0: self.updatebalance(node.parent) def rotateleft(self,rotroot): newroot = rotroot.rightchild #keep track of the new root rotroot.rightchild = newroot.leftchild # right child of the old root replaced with the left child of the new if newroot.leftchild!= None: newroot.leftchild.parent = rotroot newroot.parent = rotroot.parent if rotroot.isroot(): self.root = newroot if rotroot.isleftchild(): # if the old root is a left child then we change the pa rent rotroot.parent.leftchild = newroot # of the left child to point to the new root; otherwise we # change the parent of the right child to point rotroot.parent.rightchild = newroot # to the new root newroot.leftchild = rotroot rotroot.parent = newroot rotroot.balancefactor = rotroot.balancefactor + 1 - min(newroot.balancefactor, 0) #aktualizacja wspó łczynnika newroot.balancefactor = newroot.balancefactor + 1 + max(rotroot.balancefactor, 0) w ostatnich dwóch liniach metody rotateleft zmieniliśmy współczynniki zrównoważenia nowego i poprzedniego korzenia ponieważ pozostałe ruchy przesuwają całe poddrzewa, pozostałe współczynniki nie ulegają zmianie rozważmy rotację w lewo jak na powyższym rysunku B i D to rotowane elementy, reszta to ich poddrzewa niech h_x opisuje wysokość poddrzewa o korzeniu w węźle x z definicji mamy \begin{eqnarray} newbal(b) & = & h_a - h_c \\ oldbal(b) & = & h_a - h_d \end{eqnarray} wysokość węzła to dłuższa z wysokości jego poddrzew zwiększona o 1: h_d = 1 + \max (h_c,h_e) oldbal(b) = h_a - (1 + \max(h_c,h_e)) odejmując równania na nowy i stary współczynnik dla węzła B otrzymamy \begin{split}newbal(b) - oldbal(b) = h_a - h_c - (h_a - (1 + \max(h_c,h_e))) \\ newbal(b) - oldbal(b) = h_a - h_c - h_a + (1 + \max(h_c,h_e)) \\

newbal(b) - oldbal(b) = h_a - h_a + 1 + \max(h_c,h_e) - h_c \\ newbal(b) - oldbal(b) = 1 + \max(h_c,h_e) - h_c\end{split} korzystamy z własności \max (a,b) - c = \max(a-c,b-c) ostatecznie otrzymamy \begin{split}newbal(b) = oldbal(b) + 1 + \max(0, -oldbal(d)) \\ newbal(b) = oldbal(b) + 1 - \min(0, oldbal(d)) \\\end{split} innymi słowy nie musimy wyliczać wysokości poddrzew, żeby zaktualizować współczynniki wyważenia węzła B podobny wywód można przeprowadzić dla węzła D z rotacją związany jest jeszcze jeden problem: współczynnik węzła wynosi -2, więc powinniśmy rotować w lewo wtedy otrzymamy jednak potrzebujemy dodatkowych warunków: jeśli poddrzewo wymaga rotacji w lewo (współczynnik korzenia mniejszy od 0), sprawdź współczynnik prawego dziecka: jeśli dziecko ma współczynnik większy od zera (dłuższa lewa gałąź), rotuj w prawo względem prawego dziecka, a następnie w lewo względem korzenia jeśli poddrzewo wymaga rotacji w prawo (współczynnik korzenia większy od 0), sprawdź współczynnik lewego dziecka: jeśli dziecko ma współczynnik mniejszy od zera (dłuższa prawa gałąź), rotuj w lewo względem lewego dziecka, a następnie w prawo względem korzenia

In [10]: class AVLTree(BinarySearchTree): def _put(self,key,val,currentnode): if key < currentnode.key: if currentnode.hasleftchild(): self._put(key,val,currentnode.leftchild) currentnode.leftchild = TreeNode(key,val,parent=currentNode) self.updatebalance(currentnode.leftchild) if currentnode.hasrightchild(): self._put(key,val,currentnode.rightchild) currentnode.rightchild = TreeNode(key,val,parent=currentNode) self.updatebalance(currentnode.rightchild) def updatebalance(self,node): if node.balancefactor > 1 or node.balancefactor < -1: self.rebalance(node) return if node.parent!= None: if node.isleftchild(): node.parent.balancefactor += 1 elif node.isrightchild(): node.parent.balancefactor -= 1 if node.parent.balancefactor!= 0: self.updatebalance(node.parent) def rotateleft(self,rotroot): newroot = rotroot.rightchild #keep track of the new root rotroot.rightchild = newroot.leftchild # right child of the old root replaced with the left child of the new if newroot.leftchild!= None: newroot.leftchild.parent = rotroot newroot.parent = rotroot.parent if rotroot.isroot(): self.root = newroot if rotroot.isleftchild(): # if the old root is a left child then we change the pa rent rotroot.parent.leftchild = newroot # of the left child to point to the new root; otherwise we # change the parent of the right child to point rotroot.parent.rightchild = newroot # to the new root newroot.leftchild = rotroot rotroot.parent = newroot rotroot.balancefactor = rotroot.balancefactor + 1 - min(newroot.balancefactor, 0) #aktualizacja wspó łczynnika newroot.balancefactor = newroot.balancefactor + 1 + max(rotroot.balancefactor, 0) def rebalance(self,node): if node.balancefactor < 0: if node.rightchild.balancefactor > 0: self.rotateright(node.rightchild) self.rotateleft(node) self.rotateleft(node) elif node.balancefactor > 0: if node.leftchild.balancefactor < 0: self.rotateleft(node.leftchild) self.rotateright(node) self.rotateright(node)

po wstawieniu nowego elementu jako liścia aktualizacja współczynników wszystkich przodków to co najwyżej log_2(n) operacji (jedna na każdym poziomie) co najwyżej dwie rotacje, aby przywrócić zrównoważenie rotacje są klasy O(1) \Rightarrow put pozostaje klasy O(\log_2 n) metody get i in są takie same, jak dla drzew BST metoda del również będzie wymagała rotacji, jednak podobnie jak w przypadku put jej złożoność czasowa nie zmieni się w stosunku do drzew BST Implementacja tablicy asocjacyjnej (mapy) - podsumowanie Operacja Lista posortowana Tablica hashująca Drzewa binarne Drzewa AVL put O(n) O(1) O(n) O(log_2 n) get O(log_2n) O(1) O(n) O(log_2n) in O(log_2n) O(1) O(n) O(log_2n) del O(n) O(1) O(n) O(log_2n)

Drzewa czerwono-czarne samoorganizujące się drzewo binarne poszukiwań wynalezione przez Rudolfa Bayera w 1972 r (jako symetryczne binarne B-drzewa) używane najczęściej do implementacji tablic asocjacyjnych skomplikowane w implementacji niska złożoność obliczeniowa elementarnych operacji z każdym węzłem powiązany jest dodatkowy atrybut - kolor, który może być czerwony lub czarny oprócz typowych własności drzew BST wprowadzono kolejne wymagania: 1. każdy węzeł jest czerwony albo czarny 2. korzeń jest czarny 3. każdy liść jest czarny (można traktować nil jako liść) 4. jeśli węzeł jest czerwony, to jego synowie muszą być czarni 5. każda ścieżka z ustalonego węzła do liścia liczy tyle samo czarnych węzłów wymagania te gwarantują, że najdłuższa ścieżka od korzenia do liścia będzie co najwyżej dwukrotnie dłuższa niż najkrótsza: 1. Zgodnie z własnością 4, żadna ścieżka nie zawiera dwóch czerwonych węzłów z rzędu, jednak może zawierać czarne. Stąd najkrótsza ścieżka od węzła X zawiera wyłącznie n czarnych węzłów. 2. Zgodnie z własnością 5, druga ścieżka wychodząca z węzła X musi zawierać także n czarnych węzłów. Jedynym sposobem, aby miała ona inną łączną długość, jest umieszczenie pomiędzy każdą parą węzłów czarnych węzła czerwonego. 3. Zgodnie z własnością 3, liść kończący obie ścieżki musi być czarny. Jeżeli węzeł X jest czarny, wtedy w ścieżce możemy rozmieścić co najwyżej n-1 węzłów czerwonych, w przeciwnym zaś razie będziemy mieli w niej n czerwonych węzłów (wliczając w to sam X). dla n węzłów głębokość drzewa czerwono-czarnego h wyniesie najwyżej 2 \log (n+1), przez co elementarne operacje będą wykonywać się w czasie O(\log n) podobnie, jak drzewa AVL wymagają rotacji węzłów celem przywrócenia własności pesymistyczna złożonośc czasowa jest taka sama jak drzew AVL, jednak drzewa AVL są bardziej wydajne przy powtarzających się wyszukiwaniach (http://web.stanford.edu/~blp/papers/libavl.pdf)