STRUKTURY DANYCH I ZŁOŻONOŚĆ OBLICZENIOWA Część 3 Drzewa Przeszukiwanie drzew 1 / 24
DRZEWA (ang.: trees) Drzewo struktura danych o typie podstawowym T definiowana rekurencyjnie jako: - struktura pusta, lub - węzeł typu T ze skończoną liczbą dowiązanych rozłącznych struktur drzewiastych o typie podstawowym T (zwanych także poddrzewami). Korzeń (root) - jedyny węzeł, który nie należy do poddrzewa dowiązanego do jakiegokolwiek węzła drzewa (pod warunkiem, że drzewo nie jest strukturą pustą, ani nie jest drzewem swobodnym). Dygresja W implementacjach referencja do korzenia jest zazwyczaj wykorzystywana jednocześnie jako referencja do całej struktury. Gałąź/krawędź (branch/edge) abstrakcyjny fakt potwierdzający powiązanie dwóch węzłów drzewa. Liść/węzeł końcowy (leaf) - węzeł, do którego nie są dowiązane żadne poddrzewa. Węzeł wewnętrzny (internal node) węzeł nie będący ani korzeniem, ani liściem. Poziom (level) zbiór określający lokalizację węzła w drzewie w sposób następujący: korzeń należy do poziomu pierwszego; węzły dowiązane bezpośrednio do korzenia należą do poziomu drugiego, itd. 2 / 24
Jeżeli węzeł X na poziomie (i+1)-szym jest dowiązany bezpośrednio do węzła Y na poziomie i-tym, wówczas węzeł Y nazywamy przodkiem (rodzicem, parent) węzła X, zaś węzeł X - potomkiem (dzieckiem, child) węzła Y. Wysokość (głębokość, height, depth) drzewa maksymalny spośród poziomów wszystkich węzłów drzewa. Stopień węzła - liczba bezpośrednich potomków węzła (stopień węzła będącego liściem wynosi zero ). Stopień drzewa maksymalny spośród stopni wszystkich węzłów drzewa. Dygresja Istnieją struktury drzewiaste, dla których dopuszczalny stopień węzła jest z góry ograniczony przez pewną liczbę naturalną (tzw. drzewa wielokierunkowe stopnia n), ale istnieją również takie, w których dopuszczalna liczba węzłów potomnych nie jest określona jawnie, lecz wynika z wzajemnych relacji wszystkich węzłów w drzewie (np. drzewa dwumianowe). Długość drogi węzła X - liczba gałęzi, przez które należy przejść w drodze od korzenia do węzła X (długość drogi dla korzenia wynosi 1; długość drogi węzła X należącego do poziomu i-tego wynosi i. Długość drogi drzewa (długość drogi wewnętrznej) - suma długości dróg jego wszystkich składowych (węzłów). 3 / 24
Dla powyższego drzewa: korzeniem jest węzeł A; liśćmi są węzły C, G, I, J, K, L, N i O; pozostałe węzły są węzłami wewnętrznymi; głębokość (wysokość) drzewa wynosi 5; długość drogi dla węzła J wynosi 4; długość drogi wewnętrznej drzewa wynosi 48; E jest przodkiem I, J i K; M i N są potomkami H; stopień węzła C wynosi 0; stopień węzła M wynosi 1; stopień drzewa 3; 4 / 24
Drzewa uporządkowane (ordered trees) - drzewa, w których gałęzie każdego z węzłów są liniowo uporządkowane. Maksymalna liczba węzłów drzewa wielokierunkowego stopnia d o wysokości h wynosi: Drzewo dokładnie wyważone (perfectly balanced tree) - drzewo, którego węzły (przy ustalonej ich liczbie) zajmują najmniejszą możliwą liczbę poziomów, a ponadto liście tego drzewa znajdują się wyłącznie na ostatnim lub przedostatnim poziomie. Drzewo binarne (binary tree) drzewo wielokierunkowe, którego stopień wynosi 2. Maksymalna liczba węzłów drzewa binarnego o wysokości h wynosi: N 2 ( h ) = 1 + 2 + 2 2 + + 2 h-1 = 2 h - 1 Minimalna liczba poziomów (wysokość) drzewa binarnego zawierającego N węzłów: h log 2 (N+1) 5 / 24
UPORZĄDKOWANE DRZEWA BINARNE(ang.: ordered binary trees) Uporządkowane) drzewo binarne - skończony zbiór elementów (węzłów), który jest albo pusty, albo zawiera korzeń (węzeł) z dwoma rozłącznymi binarnymi drzewami, zwanymi lewym i prawym poddrzewem korzenia. Zazwyczaj (chociaż nie zawsze) do bezpośredniego lub pośredniego ustanawiania porządku w obrębie drzewa wykorzystywana jest wyróżniona składowa węzła, zwana kluczem (key). Struktura danych dla węzła: PASCAL: type node_rec = record key : To; left, right : node_rec; data : Ti; end; C,C++: struct node_rec eltype key; struct node_rec *left, *right; datatype data; ; 6 / 24
Przykłady sposobów uporządkowania drzew binarnych Drzewo poszukiwań binarnych (ang.: binary search tree, BST): klucz lewego potomka < klucz przodka < klucz prawego potomka Kopiec binarny zorientowany (uporządkowany ze względu) na maksimum (ang.: max. binary heap): klucz lewego potomka klucz przodka i klucz prawego potomka klucz przodka W wersji alternatywnej, zwanej kopcem binarnym zorientowanym na minimum: klucz lewego potomka klucz przodka i klucz prawego potomka klucz przodka Dla pewnych drzew binarnych uporządkowanie wynika z mechanizmów konstrukcji drzewa specyficznych dla dziedziny zastosowań (np. drzewa binarne powstające w wyniku analizy fraz kodu źródłowego sformułowanego w określonym języku programowania). 7 / 24
ODWIEDZANIE WSZYSTKICH WĘZŁÓW UPORZĄDKOWANEGO DRZEWA BINARNEGO Procedura przeszukiwania wzdłużnego scan_preorder (node) if(node NULL) P(node); scan_preorder(node left); scan_preorder(node right); return; (a + b / c) * (d - e * f) P(node) wydruk znaku * + a / b c - d * e f 8 / 24
Procedura przeszukiwania poprzecznego scan_inorder (node) if(node NULL) scan_inorder(node left); P(node); scan_inorder(node right); return; (a + b / c) * (d - e * f) a + b / c * d e * f 9 / 24
Procedura przeszukiwania wstecznego scan_postorder (node) if(node NULL) scan_postorder(node left); scan_postorder(node right); P(node); return; (a + b / c) * (d - e * f) a b c / + d e f * - * 10 / 24
DRZEWA BINARNE A ODWROTNA NOTACJA POLSKA Odwrotna Notacja Polska ONP (ang.: Reverse Polish Notation RPN) sposób beznawiasowego zapisu wyrażeń formalnych (np. wyrażeń arytmetycznych) wymyślony przez Jana Łukasiewicza (1878-1956) i często wykorzystywany do translacji wyrażeń arytmetycznych lub zdań w języku programowania wysokiego poziomu. Przykład zastosowania: translacja wyrażeń arytmetycznych Zmienne reprezentowane przez pojedyncze symbole literowe (a, b, c,, z) Nawiasy reprezentowane przez symbole ( i ) Operacje arytmetyczne reprezentowane przez symbole +,, *, /,, i Symbol Priorytet a, b, c,, z -1 ( 0 ) 1 +, 2 *, /,, (negacja) 3 (potęgowanie) 4 11 / 24
Faza pierwsza eliminacja nawiasów (algorytm) Z wyrażenia wejściowego pobierane są kolejne symbole i umieszczane w stosie wynikowym lub umieszczane i zdejmowane ze stosu pomocniczego zgodnie z następującymi regułami: - jeżeli symbolem jest litera (symbole od a do z), to umieść go w stosie wynikowym (wraz z priorytetem); - jeżeli symbolem jest (, to umieść go w stosie pomocniczym (wraz z priorytetem); - jeżeli symbolem jest ), to pobierz ze stosu pomocniczego i umieść w stosie wynikowym wszystkie kolejne elementy o priorytecie 1, po pobraniu ze stosu pomocniczego symbolu ( (o priorytecie 0) przejdź do pobrania kolejnego symbolu; - jeżeli priorytet symbolu > 1, to dopóki nie umieścisz symbolu w stosie pomocniczym (wraz z priorytetem), postępuj następująco: - jeżeli stos pomocniczy jest pusty, lub na jego szczycie znajduje się element o priorytecie niższym od priorytetu pobranego symbolu, to umieść ten symbol (wraz z priorytetem) w stosie pomocniczym; - w przeciwnym przypadku pobierz element ze stosu pomocniczego i umieść go w stosie wynikowym; - jeżeli pobrane są wszystkie symbole, to kolejno pobierz elementy ze stosu pomocniczego i umieść je w stosie wynikowym. 12 / 24
Faza pierwsza eliminacja nawiasów (przykład) Wyrażenie arytmetyczne: ( a + b / c ) * ( d - e * f ) Symbol Priorytet Symbol Priorytet / 3 + 2 ( 0 Stos pomocniczy c -1 b -1 a -1 Stos wynikowy 13 / 24
Faza pierwsza eliminacja nawiasów (przykład) Wyrażenie arytmetyczne: ( a + b / c ) * ( d - e * f ) Symbol Priorytet Symbol Priorytet + 2 / 3 / 3 + 2 * ( 03 Stos pomocniczy c -1 b -1 a -1 Stos wynikowy 14 / 24
Faza pierwsza eliminacja nawiasów (przykład) Wyrażenie arytmetyczne: ( a + b / c ) * ( d - e * f ) Symbol Priorytet Symbol Priorytet f -1 e -1 d -1 + 2 * 3-2 ( 0 * 3 Stos pomocniczy / 3 c -1 b -1 a ( -1 0 Stos pomocniczy wynikowy 15 / 24
Faza pierwsza eliminacja nawiasów (przykład) Wyrażenie arytmetyczne: ( a + b / c ) * ( d - e * f ) Symbol Priorytet Symbol Priorytet * 3-2 * 3 f -1 e -1 d -1 + 2 * 3-2 ( 0 * 3 Stos pomocniczy / 3 c -1 b -1 a -1 Stos wynikowy 16 / 24
Faza druga tworzenie uporządkowanego drzewa binarnego (algorytm) Pobierz element ze szczytu stosu wynikowego i uczyń go korzeniem drzewa oraz węzłem bieżącym. Dopóki stos wynikowy nie będzie pusty postępuj zgodnie z następującymi regułami: - pobierz element ze stosu i utwórz z niego nowy węzeł; - jeżeli węzeł bieżący nie ma prawego potomka, to uczyń nim nowy węzeł; jeżeli nowy węzeł ma priorytet > 0, to uczyń go węzłem bieżącym; - w przeciwnym przypadku (jeżeli węzeł bieżący ma już prawego potomka) uczyń nowy węzeł lewym potomkiem; jeżeli nowy węzeł ma priorytet > 0, to uczyń go węzłem bieżącym; - jeżeli węzeł bieżący ma już obu potomków, to uczyń węzłem bieżącym jego rodzica. 17 / 24
Faza druga tworzenie uporządkowanego drzewa binarnego (przykład) Czerwonym kolorem wyróżniono węzeł bieżący Symbol Priorytet * 3-2 * 3 f -1 e -1 d -1 + 2 / 3 c -1 b -1 a -1 Stos wynikowy * + d Drzewo binarne - e * f 18 / 24
19 / 24 STRUKTURY DANYCH I ZŁOŻONOŚĆ OBLICZENIOWA Obliczanie wartości wyrażenia arytmetycznego - wykorzystanie procedury przeszukiwania wstecznego (pseudokod) stack_type stack; tree_type x; //referencja do korzenia char y; //chodzi o ideę, w rzeczywistości argumenty są skojarzone przez nazwy //zmiennych z odpowiednimi wartościami liczbowymi Initialize(stack); scan_postorder(x); y pop(stack); P(tree_type *node, stack_type *stack) // char arg1, arg2, z; if (node priority < 0) push(node op, stack); else if (node op = * ) arg2 pop(stack); arg1 pop(stack); z arg1 * arg2; push(z, stack); else if (node op = + ) else if (node op = - ).
JESZCZE O PRZESZUKIWANIU DRZEWA BINARNEGO W celu uniknięcia rekurencji w funkcji przeszukiwania można posłużyć się stosem w sposób jawny: scan_preorder_with_stack(node) initialize(stack); if(node NULL) push(node, stack); while(~empty(stack)) node pop(stack); P(node); if(node right NULL) push(node right, stack); if(node left NULL) push(node left, stack); return; 20 / 24
Dla przeszukiwania poprzecznego sytuacja nieco się komplikuje : scan_inorder_with_stack (node) initialize(stack); while(node NULL) while(node NULL) if(node right NULL) push(node right, stack); push(node, stack); node node left); node pop(stack); while(~empty(stack) & (node right = NULL)) P(node); node pop(stack); P(node); if(~empty(stack)) node pop(stack); else node NULL; return; 21 / 24
DRZEWA Z ŁAŃCUCHAMI DOWIĄZAŃ (ang.: threaded trees) Element typu podstawowego: struct node_rec eltype key; BOOLEAN rthread; BOOLEAN visit_switch; struct node_rec *left, *right; datatype Ti; ; Czerwonymi łukami oznaczono prawe dowiązania (right threads). Składowa rthread = TRUE dla węzłów bez prawych potomków (oprócz skrajnego prawego węzła, który jest ostatnim odwiedzanym w trybie przeszukiwania poprzecznego); ma ona istotne znaczenie dla wyszukiwania, wstawiania i usuwania węzłów. Składowa visit_switch jest jedną z możliwych form zapobiegania powtórnym odwiedzinom w węźle w procesie przeszukiwania drzewa; przed rozpoczęciem przeszukiwania poprzecznego wszystkie węzły muszą mieć identyczną wartość tej składowej (TRUE albo FALSE). 22 / 24
Dzięki dowiązaniom przeszukiwanie poprzeczne nie odwołuje się do stosu, ale wymaga utworzenia na potrzeby przeszukiwania listy już odwiedzonych węzłów, albo innego mechanizmu zapobiegającego zapętleniom (np. dynamicznej składowej logicznej; tu: visit_switch). threaded_scan_inorder (node) // p referencja na węzeł bieżący; p node; if(p = NULL) return; //drzewo jest puste cur_switch ~ (p visit_switch); while(p NULL) while((p left NULL) & (p left visit_switch cur_switch)) p p left; P(p); p visit_switch cur_switch; p p right; return; 23 / 24
Koniec części 3 24 / 24