Algorytmy i struktury danych Równoważenie drzew Drzewa czerwono-czarne Drzewa kontekstowe B-drzewa
Drzewa zrównoważone Czas operacji na BST jest proporcjonalny do wysokości drzewa Drzewo doskonale zrównoważone – dla dowolnego wierzchołka rozmiar lewego i prawego poddrzewa różnią się najwyżej o 1. Drzewo zrównoważone – długość dowolnej scieżki z węzła do liści różni się od wysokości tego węzła najwyżej o 1. Drzewo w przybliżeniu zrównoważone – długość dowolnej scieżki z węzła do liści różni się od wysokości tego węzła najwyżej 2 razy.
Przykłady drzew zrównoważonych Drzewa AVL Drzewa czerwono czarne B-drzewa
Drzewo czerwono-czarne Każdy węzeł jest czerwony lub czarny Każdy NULL jest czarny Jeżeli węzeł jest czerwony to obaj jego synowie są czarni Każda ścieżka z ustalonego węzła do liścia-NULL ma tyle samo czarnych węzłów (czarna wysokość)
Drzewo czerwono-czarne (RBT) 30 25 41 15 28 35 45 NULL 8 18 27 29 33 37 4 10 36 40 26 20 1 Wysokość drzewa RBT 2lg(n+1)
Równowaga w RBT Każda ścieżka z ustalonego wezła do liścia-NULL ma tyle samo czarnych węzłów NIE może być węzła, który nie ma obu potomków i w poddrzewie ma czarne węzły NULL NULL
Właściwości RBT Wysokość drzewa RBT jest 2lg(n+1) Search O(log(n)) Min O(log(n)) Max O(log(n)) Succesor O(log(n)) Predecesor O(log(n))
Definicja węzła class NODE : data = None color = BLACK left = None right = None parent = none
Operacja rotacji y x A B C RightRotate(T,y) x y A B C LeftRotate(T,x)
LeftRotate x y A B C def LeftRotate(root, x): # zmiana na right y = x.right # x.left if (y==None): return x.right = y.left # x.right if (y.left!=None): y.left.parent = x # y.right y.parent = x.parent if x.parent==None: root = y elif x==x.parent.left: x.parent.left = y else: x.parent.right = y y.left = x # y.right x.parent = y return root y x A B C
Wstawianie węzła Nie narusza długości czarnych ścieżek Narusza długość czarnych ścieżek Nie narusza długości czarnych ścieżek Ale może naruszyć zasadę 3
Wstawianie węzła Wstaw węzeł x do drzewa (liść) Pokoloruj x na czerwono Uporządkuj drzewo (naruszona może być własność 3):
Porządkowanie RBT po INS - 1 15 4 20 8 1 25 Przypadek 1 6 10 15 stryj 20 5 4 x nowy x 8 1 25 6 10 5 Jeżeli stryj(x) jest czerwony przemaluj wierzchołki dziadek(x), ojciec(x), stryj(x) wznów operację od dziadka, tj. podstaw x = dziadek(x)
Porządkowanie RBT po INS - 2 15 stryj 20 15 4 stryj 25 14 x 8 nowy x 8 1 4 15 10 6 10 Przypadek 2 1 6 5 5 Jeżeli x jest synem z tej strony co stryj(x) (kierunek ) Przyjmij x = ojciec(x) obróć drzewo w przeciwną, tj. ’ względem nowego x Uwaga po tej operacji x znajduje się po przeciwnej stronie swego ojca niż stryj swojego
Porządkowanie RBT po INS - 3 11 stryj 14 7 x 2 15 8 7 x 11 14 1 5 2 5 8 4 Przypadek 3 15 1 4 Jeżeli x jest synem z przeciwnej strony niż stryj(x) Przemaluj wierzchołki ojciec(x) i dziadek(x), a następnie, obróć drzewo względem dziadek(x) w stronę stryja tj.
Porządkowanie drzewa po INS Z: korzeń drzewa jest czarny (czemu nie ?) while x_nie_jest_korzeniem_i_ojciec(x)_jest_czerwony: if stryj(x)_jest_czerwony: przemaluj_wierzchołki_dziadek(x)_ojciec(x)_stryj(x) #1 x = dziadek(x) # wznów operację od dziadka else: if x_jest_synem_z_tej_strony_co_stryj(x): #(kierunek ) x = ojciec(x) #2 obróc_drzewo_w_przeciwną_strone_względem_x # tj w strone ’ względem nowego x przemaluj_ojciec(x)_i_dziadek(x) #3 obróc_drzewo_względem_dziadek(x)_w_stronę_stryj (x) #tj. pomaluj_korzeń_drzewa_na_czarny pomaluj_korzeń_drzewa_na_czarny # dla drzewa jedoelementowego
Usuwanie węzła NULL NULL Przenosimy nadmiarowy kolor czarny na syna usuwanego wierzchołka Problem: wierzchołek mógł nie mieć syna Czy mogl mieć czarnego syna(ów) ?
Wstawianie węzła – implem. def RBTInsertNode(root, x): root = BSTInsertNode(root, x) x.color = Color.RED # zalozenie: root.color == BLACK while x != root and x.parent.color == Color.RED: if x.parent == x.parent.parent.left: #Porządkowanie dla ojca po lewej else: #Porządkowanie dla ojca po prawej root.color = Color.BLACK root.color = Color.BLACK # dla drzewa jednoelem. return root
Porządkowanie dla ojca po lewej uncle = x.parent.parent.right if GetColor(uncle) == Color.RED : x.parent.color = Color.BLACK # przypadek 1 uncle.color = Color.BLACK x.parent.parent.color = Color.RED x = x.parent.parent else: if x == x.parent.right: x = x.parent # przypadek 2 root = LeftRotate(root, x) x.parent.color = Color.BLACK # przypadek 3 root = RightRotate(root, x.parent.parent)
Pobieranie koloru - NULL def GetColor(node): if node != None: return node.color else: return Color.BLACK
Usuwanie węzła Usuń węzeł podobnie jak dla zwykłego drzewa Jeżeli usuwany wierzchołek był koloru czarnego należy wykonac porządkowanie drzewa (naruszona może być własność 3 lub 4):
Porządkowanie RBT po DEL - 1 2 x brat 10 1 A A B 7 15 C D E F Przypadek 1 10 2 15 brat (nowy) x 7 1 E F Jeżeli brat jest czerwony przemaluj wierzchołki brat, ojciec(x), obróć drzewo wokół wierzchołka ojciec(x) w kierunku syna x (tj. ) i zaktualizuj brat Uwaga: po tym kroku brata jest czarny A A B C D
Porządkowanie RBT po DEL - 2 x brother 7 1 A B 5 9 Przypadek 2 nowe x C D E F 2 1 7 A B 5 9 C D E F Jeżeli obaj synowie brat-a są czarni przemaluj brat-a i ustaw rozpocznij od ojciec(x)
Porządkowanie RBT po DEL - 3 2 x brother 7 1 Przypadek 3 2 A B x 5 9 C D E F brother (nowy) 1 5 A B C 7 D 9 Jeżeli przynajmniej jeden syn brat-a jest czerwony jeżeli dalszy syn brat-a (w kierunku ’) jest czarny drugiego syna brat-a pomaluj na czarno, brat-a na czerwono i obróć drzewo wokoł wierzchołka brat w kierunku ’, zaktualizuj brat E F Uwaga: po kroku trzecim dalszy syn brata bedzie czerwony
Porządkowanie RBT po DEL - 4 2 x brother 5 1 5 A B 3 Przypadek 4 7 C D E F 2 7 E F x 3 1 C D A B STOP: np x = proot Jeżeli dalszy syn brat-a jest czerwony przemaluj brat-a na kolor taki jak ojciec(x), wierzchołki ojciec(x) oraz dalszego syna brat-a (w kierunku ’) na czarno, obróć drzewo wokół ojca w kierunku x (tj. )
Porządkowanie po DEL while x_nie_jest_korzeniem_i_x_jest_czarny if brat(x)_jest_czerwony : przemaluj_wierzchołki_brat(x)_oraz_ojciec(x) #1 obróć_drzewo_wokół_wierzchołka_ojciec(x)_w_kierunku_x # (tj. ) po obrocie x ma nowego brata elif obaj_synowie_brat(x)_sa_czarni : przemaluj_brat(x) #2 x = ojciec(x) # rozpocznij od ojciec(x) else: if dalszy_syn_brat(x)_jest_czarny # dalszy tj. w kierunku ’ pomaluj_drugiego_syna_brat(x)_na_czarno #3 pomaluj_brat(x)_na_czerwono obróc_drzewo_wokół_wierzchołka_brat(x)_w_przeciwnym_do_x # tj. w kierunku ’ . po obrocie x ma nowego brat a przemaluj_brat(x)_na_kolor_taki_jak_ojciec(x) #4 przemaluj_ojciec(x)_i_dalszy_syna_brat(x)_na_czarno, obróc_drzewo_wokół_ojciec(x)_w_kierunku_x # tj. x = root # zakończ porządkowanie Porządkowanie po DEL
Uwagi implementacyjne Aby uniknąć w kodzie sprawdzania warunków czy syn(owie) != None przed pobraniem koloru / sprawdzeniem typu / odwolaniem sie do ojca można: dodać funkcje realizujące odpowiednie testy (por. GetColor) dodać wartownika np NIL – specjalny węzeł który ma wszystkie wskaźniki == None i na który pokazują wszystkie wskaźniki dawniej równe None Przypisanie: son.parent = todel.parent można wykonać bezwarunkowo (syn moze być ew. wartownikiem, ale istnieje) - w takim przypadku NIL.parent == todel.parent.
RBT wzbogacone o statystyki poz. Aktualizacja rozmiarów: def LeftRotate (root, x): ..... y.size = getsize(y); x.size = getsize(x->left) + getsize(x->right) +1 return root y 93 19 42 19 x y T=RightRotate(T,y) x 42 11 7 93 12 6 T=LeftRotate(T,x) 6 4 4 7
B-drzewo . M . n.keys[1] n.keys[0] . D . H . . Q . T . X . n.sons[0] B C F G J K L N P R S V W Y Z Wszystkie klucze dla i-tego syna jego potomków są wieksze lib równe od i-tego klucza i mniejsze lub równe od i+1 Węzeł o i synach ma i-1 kluczy Wezły różne od korzenia zawierają co najmniej T-1 kluczy (stąd węzły wewnętrzne maja conajmniej t synów) Węzły zawierają conajwyżej 2T-1 kluczy (stąd węzły wewnętrzne maja conajwyżej 2T synów -> węzły pełne)
Minimalne B-drzewo o h=3 root 1 1 2 2t 2t2 T - 1 T T - 1 T T - 1 T T - 1 T T - 1 T T - 1 T T - 1 T - 1 T - 1 T - 1 T - 1 T - 1 T - 1 T - 1 Dla T = 2 otrzymujemy tzw. 2-3-4 drzewo
Właściwości B-drzewa B-drzewo jest zrównoważone Zmienna liczba kluczy i synów Wszystkie liście są na tej samej głębokości Mała głębokość drzewa Zaprojektowane do minimalizacja dostepów np. do dysku – korzeń wczytuje się pamięci od razu
Definicja węzła T = 5 class BNODE: isLeaf=true cntKey=0 keys = Array(2*T-1, None) sons = Array(2*T, None) #pozycja na dysku biezacego wezla thisNodeDiscPos = None #pozycje na dysku dla danych odpowiadających #poszczegolnym kluczom dataDiscPos = Array(2*T-1, None) def Array(size, initVal=None): return map(lambda x: initVal, range(0,size)) class DISCPOS:...
FunkcjePomocnicze def LoadNode(nodeDiscPos) # alokacja w pamięci i odczyt def WriteNodeToDisc(node) # zapis na dysk pod pode. thisNodeDiscPos AllocateNode() # alokacja w pamięci i na dysku, # zapis struktury na dysk p = BNODE() #p.isLeaf = true, p.cntKey = 0 p.thisNodeDiscPos = AllocateSpaceOnDisc() WriteNodeToDisc(p) return p
Wyszukiwanie w B-drzewie BTreeFind(p,k): if p_zawiera_szukany_klucz k: return p elif p_jest_liściem: return None else: # p nie jest liściem i nie zawiera k s = wytypuj_poddrzewo_p_które_może_zwierać_k ptmp = LoadNode(s) ret = BTreeFind(ptmp,k) #zadbaj o zwolnienie ptmp jeśli ret!=ptmp return ret
Rozbijanie węzła T = 4 keys[i-1] keys[i] p N . W sons[i] w . P . Q . R . S . T . U . V . keys[i] keys[i-1] keys[i+] p N . S . W sons[i] sons[i+1] w y . P . Q . R . . T . U . V .
Rozbijanie korzenia T=4 w . P . Q . R . S . T . U . V . root keys[0] p . S . sons[0] sons[1] w y . P . Q . R . . T . U . V .
Rozbijanie węzła w B-drzewie rozbijamy pełen węzeł w będący i-tym synem węzła p środkowy z 2*T-1 kluczy w w wstawiamy do węzła p (przed element na pozycji i) wskaźnik na nowy węzeł z wstawiamy do węzła p (przed element na pozycji i) T-1 kluczy z w przepisujemy do z T wskaźników z w przepisujemy do z zwracamy nowy węzeł (jesli trzeba odbiorca powinien zwolnić pamiec po uzyciu wezla)
Rozbijanie węzła w B-drzewie BTreeSplit(p, i, w): #Zalozenie: p!=w jeśli mamy rozbic korzeń najpierw #należy dodac nowy wezel(powyzej korzenia) z = AllocateNode() z.isLeaf = w.isLeaf z.cntKeys, w.cntKeys = T-1, T-1 for j in range(p.cntKey-1,i,-1): p.keys[j]=p.keys[j-1] #p.data[j]=p.data[j-1] for j in range(p.cntKey, i,-1): p.sons[j]=p.sons[j-1] p.keys[i] = w.keys[T-1] #p.data[i]=w.data[T-1] p.sons[i] = z p.cntSons = p.cntSons +1 for j in range(0, T-1): z.keys[j] = w.keys[T+j] #z.data[j]=w.data[T-1+j] for i in range(0,T): z.sons[j] = w.sons[T+j] WriteNodeToDisc(p) WriteNodeToDisc(w) WriteNodeToDisc(z) return z
T=3 . G . M . P . X . A C D E J K N O R S T U V Y Z +B A B C D E J K N O R S T U V Y Z +Q . G . M . P . T . X . A B C D E J K N O Q R S U V Y Z
T=3 . G . M . P . T . X . A B C D E J K N O Q R S U V Y Z +L . P . J K L N O Q R S U V Y Z +F . P . . C . G . M . . T . X . A B D E F J K L N O Q R S U V Y Z
Wstawianie klucza do B-drzewa #(1) if korzeń_jest_pelen: dodaj_nowy_korzen_i_rozbij_dotychczasowy_na_dwa w = korzeń #(2) s = syn_w_mogacy_zawierać_nowy_klucz_k if s_jest_pelen: rozbij_s_na_dwa_wezly s = syn_w_mogący_zawierać_nowy_klucz_k if s_jest_liściem: dodaj_klucz_k_do_wezla_s else: wykonaj_rekurencyjnie_(2)_dla_wezla_s
T=3 . P . . C . G . M . . T . X . A B D E F J K L N O Q R S U V Y Z -F . C . G . L . . T . X . A B D E J K N O Q R S U V Y Z
T=3 . P . . C . G . L . . T . X . A B D E J K N O Q R S U V Y Z . L . . P . T . X . A B D E J K N O Q R S U V Y Z -S . L . . C . G . . P . T . X . A B D E J K N O Q R U V Y Z
T=3 . P . . C . G . L . . T . X . A B D E J K N O Q R S U V Y Z -G . C . L . . T . X . A B D E J K N O Q R S U V Y Z -D . C . L . P . T . X . A B E J K N O Q R S U V Y Z
Usuwanie klucza z B-drzewa #Nie wchodzimy na węzły minimalne! w = root if w_jest_liściem_i_w_zawiera_klucz_k: usuń_klucz_k_z_węzła_w elif w_nie_jest_liściem_i_w_zawiera_klucz_k: p = syn_porzedzający_k n = syn_nastepujący_po_k if p_ma_co_najmniej_T_kluczy: k1 = wyznacz_poprzednik_k_w_poddrzewie_p rekurencyjnie_usuń_k1_i_zastąp_k_przez_k1 elif n_ma_co_najmniej_T_kluczy : k1 = wyznacz_następnik_k_w_poddrzewie_n rekurencyjnie_usuń_k1_i_zastąp_k_przez_k1 else: nowy = scal_p_oraz_n_przenosząc_k_do_nowego_węzła rekurencyjnie_usuń_k_z_węzła_nowy
Usuwanie klucza z B-drzewa elif w_nie_jest_liściem_i_w_nie_zawiera_klucza_k: p = wyznacz_poddrzewo_w_które_może_zwierać_k if p_zawiera_T-1_kluczy: if jeden_z_braci_p_ma_T_kluczy: przesuń_odpowiedni_klucz_z_brata_p_do_w_do_p else: połącz_p_z_jednym_z_braci_pobierając_klucz_z_w kontynuuj_usuwanie_dla_p else: # tj w jest liściem i w nie zawiera klucza k return
Usuwanie klucza z B-drzewa Uwagi: – większość węzłów znajduje sie w liściach stąd zwykle usunięcie będzie polegało na pojedynczym zejściu w dół drzewa – w przypadku wezłów wewnętrznych konieczne może być rekurencyjne usuwanie co wymagac może przejścia w dół i powrotu