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)