Matematyka Dyskretna Andrzej Szepietowski 25 czerwca 2002 roku Rozdział 1 Struktury danych 1.1 Listy, stosy i kolejki Lista to uporza̧dkowany cia̧g elementów. Przykładami list sa̧ wektory lub tablice jednowymiarowe. W wektorach mamy dostȩp do dowolnego elementu, poprzez podanie indeksu tego elementu. Przykład 1.1 W jȩzyku Pascal przykładem typu tablicy jednowymiarowej jest array[1..N] of integer. Jeżeli mamy zmienna̧ tego typu a:array[1..N] of integer, to tablica a zawiera N elementów a[1], a[2], ... ,a[N]. W programie możemy odwoływać siȩ do całej tablicy, na przykład w instrukcji przypisania a:=b, lub do pojedynczych elementów: a[i]:=a[i+1]. Możemy także używać tablic dwu lub wiȩcej wymiarowych. Przykładem tablicy dwuwymiarowej jest typ array[1..N,1..M] of real. Zmienna c:array[1..N,1..M] of real zawiera elementów. Dla każdej pary liczb spełniaja̧cej warunki c[i,j] zawiera liczbȩ typu real. , element , Czasami wygodniej posługiwać siȩ listami bez używania indeksów. Przykładami list, których można używać bez konieczności odwoływania siȩ do indeksów poszczególnych elementów, sa̧ kolejki i stosy. 3 4 Rozdział 1. Struktury danych Definicja 1.2 Kolejka jest lista̧ z trzema operacjami: dodawania nowego elementu na koniec kolejki, zdejmowania pierwszego elementu z pocza̧tku kolejki, sprawdzania, czy kolejka jest pusta. Taki sposób dodawania i odejmowania elementów jest określany angielskim skrótem FIFO (first in first out, czyli pierwszy wszedł — pierwszy wyjdzie). Przykłady kolejek spotykamy w sklepach, gdzie klienci czekaja̧cy na obsłużenie tworza̧ kolejki. Definicja 1.3 Stos jest lista̧ z trzema operacjami: dodawania elementu na wierzch stosu, zdejmowania elementu z wierzchu stosu, sprawdzania, czy stos jest pusty. Na stosie dodajemy i odejmujemy elementy z tego samego ko ńca, podobnie jak w stosie talerzy spiȩtrzonym na stole. Talerze dokładane sa̧ na wierzch stosu i zdejmowane z wierzchu stosu. Taka organizacja obsługi listy określana jest angielskim skrótem LIFO (last in first out, czyli ostatni wszedł – pierwszy wyjdzie). Niektórzy w ten sposób organizuja̧ pracȩ na biurku. Przychodza̧ce listy układaja̧ na stosie i jak maja̧ czas, to zdejmuja̧ jeden list i odpowiadaja̧ na niego. Przyjrzyjmy siȩ zastosowaniu kolejki lub stosu do szukania. Przypuśćmy, że szukamy przez telefon pewnej informacji (na przykład chcielibyśmy siȩ dowiedzieć, kto z naszych znajomych ma pewna̧ ksia̧żkȩ). Algorytm szukania ksia̧żki wśród znajomych tworzymy STOS, który na pocza̧tku jest pusty, wkładamy na STOS numer telefonu swojego znajomego, powtarzamy dopóki na stosie sa̧ jakieś numery: zdejmujemy z wierzchu STOSU jeden numer telefonu, dzwonimy pod ten numer, jeżeli osoba, do której siȩ dodzwoniliśmy, posiada szukana̧ ksia̧żkȩ, to koniec poszukiwań, jeżeli nie posiada ksia̧żki, to pytamy ja̧ o numery telefonów jej znajomych, którzy moga̧ mieć ksia̧żkȩ (lub znać kogoś kto ja̧ ma); każdy nowy numer zostaje dopisany do STOSU. W powyższym algorytmie zamiast stosu może być użyta kolejka. 1.2. Drzewa binarne 5 1.2 Drzewa binarne Drzewo jest hierarchiczna̧ struktura̧ danych. Jeden element drzewa, zwany korzeniem, jest wyróżniony. Inne elementy drzewa sa̧ jego potomstwem lub potomstwem jego potomstwa itd. Terminologia używana do opisu drzew jest mieszanina̧ terminów z teorii grafów, botaniki i stosunków rodzinnych. Elementy drzewa nazywa siȩ wierzchołkami lub wȩzłami. Liście to wierzchołki nie maja̧ce potomstwa. Drzewa czȩsto przedstawia siȩ w formie grafu, gdzie każdy wierzchołek jest poła̧czony krawȩdzia̧ ze swoim ojcem i ze swoimi dziećmi (swoim potomstwem). Dla każdego elementu w drzewie istnieje dokładnie jedna ścieżka prowadza̧ca od korzenia do tego wierzchołka. Drzewa binarne to takie drzewa, w których każdy wierzchołek ma co najwyżej dwóch synów. Do oznaczania wierzchołków w drzewie binarnym wygodnie jest używa ć cia̧gów oznacza zbiór wszystkich skończonych cia̧gów zer i jedyzer i jedynek. Niech nek. Zbiór ten zawiera cia̧g pusty (długości 0), oznaczany przez . Wierzchołki drzewa oznaczamy w nastȩpuja̧cy sposób: korzeń drzewa oznaczamy przez — pusty cia̧g, jeżeli jakiś wierzchołek jest oznaczony przez , to jego synowie oznaczeni sa̧ przez i . Rysunek 1.1: Przykład drzewa binarnego Przy takim oznaczeniu wierzchołków drzewa binarnego nazwa wierzchołka mówi nam, jaka ścieżka prowadzi od korzenia do . Na przykład, aby dojść od korzenia do wierzchołka nalezy: pójść w prawo do , potem znowu w prawo do , a na końcu w lewo do . 6 Rozdział 1. Struktury danych Jeżeli mamy drzewo binarne , to z każdym wierzchołkiem możemy skojarzyć poddrzewo złożone z wierzchołka i wszystkich jego potomków. Na przykład w drzewie przedstawionym na rysunku 1.1 wierzchołek wyznacza poddrzewo przedstawione na rysunku 1.2. Rysunek 1.2: Poddrzewo Mówimy też, że drzewo składa siȩ z korzenia (wierzchołka ), z lewego poddrzewa i z prawego poddrzewa . Wysokościa̧ drzewa nazywamy długość (liczb˛e kraw˛edzi) najdłuższej ścieżki w drzewie prowadza̧cej od korzenia do liścia. Na przykład drzewo z rysunku 1.1 jest wysokości 3. 1.3 Drzewa wyrażeń arytmetycznych Przykładem zastosowania drzew binarnych sa̧ drzewa wyraże ń arytmetycznych. Najpierw przykład. Na rysunku 1.3 przedstawiono drzewo wyrażenia . W drzewie tym każdy wierzchołek ma etykietȩ. Liście etykietowane sa̧ stałymi albo zmiennymi. Wierzchołki nie bȩda̧ce liśćmi etykietowane sa̧ operacjami arytmetyczymi. Każdemu wierzchołkowi w drzewie możemy przypisać wyrażenie arytmetyczne według nastȩpuja̧cej zasady: dla liści wyrażeniami sa̧ etykiety tych liści (stałe lub zmienne), jeżeli wierzchołek wyrażenia ma etykietȩ , a jego synom przypisano , to wierzchołkowi przypisujemy wyrażenie !i. Przykład 1.4 W drzewie z rysunku1.3 wierzchołkowi z etykieta˛ odpowiada wyrażenie , wierzchołkowi z etykieta˛ wyrażenie " , a korzeniowi wyrażenie # $%" !'& Wyrażenie to zawiera wiȩcej nawiasów, niż to siȩ zwykle stosuje. Normalnie to samo wyrażenie przedstawiamy bez nawiasów w postaci () . 1.3. Drzewa wyrażeń arytmetycznych Rysunek 1.3: Drzewo wyrażenia 7 ) Opuszczenie nawiasów może prowadzić do niejednoznaczności lub może zmienić sens wyrażenia. Na przykład wyrażenie % po opuszczeniu nawiasów stanie siȩ identyczne z wyrażeniem "$" i zmieni sens. Drzewo, które odpowiada wyrażeniu % , przedstawiono na rysunku 1.4. Rysunek 1.4: Drzewo wyrażenia () Drzewo wyrażenia arytmetycznego oddaje logiczna̧ strukturȩ i sposób obliczania tego wyrażenia. 8 Rozdział 1. Struktury danych Istnieje sposób przedstawiania wyrażeń arytmetycznych nie wymagaja̧cy nawiasów. Jest to tak zwana notacja polska lub Łukasiewicza. Jest ona też nazywana notacja̧ postfixowa̧, ponieważ znak operacji stoi na końcu wyrażenia, za argumentami, czyli wyrażenie w notacji postfixowej ma postać: pierwszy argument — drugi argument — operacja. Notacja, do jakiej jesteśmy przyzwyczajeni, nazywa siȩ infixowa, ponieważ operacja znajduje siȩ pomiȩdzy argumentami, czyli wyrażenie w notacji infixowej ma postać: pierwszy argument — operacja — drugi argument. Przykład 1.5 Wyrażenie w postaci postfixowej ! ma w postaci infixowej postać ! a wyrażenie jest postfixowa̧ postacia̧ wyrażenia () & W wyrażeniach w postaci postfixowej nie potrzeba nawiasów. Wartość wyrażenia można w sposób jednoznaczny odtworzyć z samego wyrażenia za pomoca̧ nast˛epujacego ˛ algorytmu.: Algorytm obliczania wartości wyrażenia w postaci postfixowej. Dla kolejnych elementów zapisu wyrażenia: jeżeli element jest stała̧ lub zmienna̧, to wkładamy jego wartość na stos, jeżeli element jest znakiem operacji, to: zdejmujemy dwie wartości z wierzchu stosu, wykonujemy operacjȩ na tych wartościach, obliczona̧ wartość wkładamy na wierzch stosu, po przejściu całego wyrażenia jego wartość znajduje siȩ na stosie. " Przykład 1.6 Zademonstrujmy ten algorytm na przykładzie wyrażenia: Załóżmy, że zmienne maja̧ nastȩpuja̧ce wartości: , , , , . Poniższa tabela przedstawia zawartość stosu po przeczytaniu kolejnych elementów wyrażenia. 1.4. Przeszukiwanie drzew binarnych czytany element a b c d e 9 stos 3, 3, 2, 3, 2, 1, 3, 3, 9, 9, 4, 9, 4, 2, 9, 2, 11. 1.4 Przeszukiwanie drzew binarnych Zajmiemy siȩ teraz dwoma algorytmami przeszukiwania drzew (binarnych): przeszukiwanie w gła̧b i wszerz. Różnia̧ siȩ one rodzajem użytych struktur danych. W algorytmie przeszukiwania w gła̧b użyjemy stosu, a w algorytmie przeszukiwania wszerz użyjemy kolejki. 1.4.1 Przeszukiwanie drzewa w głab ˛ Algorytm przeszukiwania drzewa w gła̧b. Dane wejściowe: drzewo . odwiedzamy korzeń i wkładamy go na STOS; zaznaczamy jako wierzchołek odwiedzony, dopóki STOS nie jest pusty, powtarzamy: jeżeli jest wierzchołkiem na wierzchu STOSU, to sprawdzamy, czy istnieje syn wierzchołka , który nie był jeszcze odwiedzony, najpierw sprawdzamy , a potem . jeżeli takie siȩ znajdzie, to odwiedzamy , wkładamy go na wierzch STOSU i zaznaczamy jako wierzchołek odwiedzony, jeżeli takiego nie ma, to zdejmujemy chołka bȩda̧cego na stosie pod spodem. ze STOSU i cofamy siȩ do wierz- Przykład 1.7 Poniższa tabela pokazuje jaki wierzchołek jest odwiedzany i jaka jest zawartość stosu po każdej kolejnej iteracji pȩtli algorytmu, gdy przeszukiwane jest drzewo z rysunku 1.1. 10 Rozdział 1. Struktury danych Wierzchołek STOS ,0 0 00 0 01 0 ,0,00 ,0 ,0,01 ,0 ,1 1 10 1 11 110 11 111 11 1 ,1,10 ,1 ,1,11 ,1,11,110 ,1,11 ,1,11,111 ,1,11 ,1 W metodzie przeszukiwania w gła̧b po każdym kroku algorytmu wierzchołki znajduja̧ce siȩ na stosie tworza̧ ścieżkȩ od wierzchołka wejściowego do wierzchołka aktualnie odwiedzanego. Zauważmy, że nazwa każdego wierzchołka na stosie jest prefiksem (przedrostkiem) nazwy nastȩpnego wierzchołka. Dlatego wystarczy przechowywać ostatnie bity wierzchołków na stosie. Nie jest też konieczne zaznaczanie, które wierzchołki były już odwiedzone, wystarczy zauważyć, że: jeżeli przyszliśmy do wierzchołka od jego ojca, to żaden z synów nie był jeszcze odwiedzany, jeżeli przyszliśmy do wierzchołka od lewego syna odwiedzony był tylko lewy syn, jeżeli przyszliśmy do wierzchołka od prawego syna odwiedzeni już byli obaj synowie. (po zdjȩciu Algorytm przeszukiwania drzewa w gła̧b (druga wersja). Dane wejściowe: drzewo . odwiedzamy korzeń i wkładamy go na STOS, dopóki STOS nie jest pusty, powtarzamy: Jeżeli jest aktualnie odwiedzanym wierzchołkiem i Jeżeli Jeżeli ostatnia̧ operacja̧ na stosi było włożenie nowego elementu, to: Jeżeli , to przejdź do ale i włóż 1 na stos, i włóż 0 na stos, , to przejdź do ze stosu), to (po zdjȩciu ze stosu), to Oto prostsza wersja algorytmu przeszukiwania w gła̧b: Jeżeli 1.4. Przeszukiwanie drzew binarnych oraz ojca wierzchołka . Jeżeli 11 , to zdejmij ostatni element ze stosu i przejdź do i włóż 1 na stos, Jeżeli ostatnia̧ operacja̧ na stosie było zdjȩcie 0 to: Jeżeli chołka . , to przejdź do , to zdejmij ostatni element ze stosu i przejdź do ojca wierz- Jeżeli ostatnia̧ operacja̧ na stosie było zdjȩcie 1 to: zdejmij ostatni element ze stosu i przejdź do ojca wierzchołka . Przykład 1.8 Poniższa tabela pokazuje jaki wierzchołek jest odwiedzany i jaka jest zawartość stosu po każdej kolejnej iteracji pȩtli drugiego algorytmu, gdy przeszukiwane jest drzewo z rysunku 1.1. Wierzchołek STOS ,0 0 00 0 01 0 1 10 1 11 110 11 111 11 1 ,0,0 ,0 ,0,1 ,0 ,1 ,1,0 ,1 ,1,1 ,1,1,0 ,1,1 ,1,1,1 ,1,1 ,1 Zauważmy, że etykiety na stosie zła̧czone razem tworza̧ nazwȩ aktualnie odwiedzanego wierzchołka. 1.4.2 Przeszukiwanie drzewa wszerz Nastȩpny algorytm przeszukiwania drzew używa kolejki jako pomocniczej struktury danych. Algorytm przeszukiwania wszerz. Dane wejściowe: drzewo . odwiedzamy korzeń drzewa i wkładamy go do KOLEJKI. 12 Rozdział 1. Struktury danych dopóki KOLEJKA nie jest pusta, powtarzamy: bierzemy jeden wierzchołek z pocza̧tku KOLEJKI, odwiedzamy wszystkiech synów wierzchołka kolejki. i wkładamy je na koniec Poniżej przedstawiono odwiedzane wierzchołki oraz zawartość kolejki po każdej kolejnej iteracji pȩtli algorytmu przeszukiwania wszerz drzewa przedstawionego na rysunku 1.1. wierzchołki KOLEJKA 0,1 00,01 10,11 110,111 - 0,1 1,00,01 00,01,10,11 01,10,11 10,11 11 110,111 111 - W metodzie przeszukiwania wszerz wierzchołki sa̧ przeszukiwane w kolejności od wierzchołków bȩda̧cych najbliżej wierzchołka pocza̧tkowego do wierzchołków bȩda̧cych dalej. 1.4.3 Rekurencyjne algorytmy przeszukiwania drzew Istnieje prosty i ciekawy sposób uzyskiwania postaci postfixowej wyrażenia arytmetycznego z drzewa tego wyrażenia. Aby uzyskać postać postfixowa̧ wyrażenia, należy przeszukać drzewo tego wyrażenia w pewien określony sposób, zwany przeszukiwaniem postorder. Przeszukiwanie postorder. Aby przeszukać (pod)drzewo maja̧ce swój korzeń w wierzchołku : przeszukujemy jego lewe poddrzewo (z korzeniem w przeszukujemy jego prawe poddrzewo (z korzeniem w ), ), odwiedzamy wierzchołek (korzeń drzewa). Algorytm ten możemy krótko przedstawić w schemacie: lewe poddrzewo — prawe poddrzewo — korzeń. Przykład 1.9 Jeżeli przeszukamy drzewo z rysunku 1.4 i wypiszemy po kolei etykiety odwiedzanych wierzchołków, to otrzymamy cia̧g: który jest postacia̧ postfixowa̧ wyrażenia () . 1.5. Drzewa poszukiwań binarnych 13 Istnieja̧ jeszcze dwie inne pokrewne metody przeszukiwania drzew binarnych: inorder i preorder: Przeszukiwanie inorder. Aby przeszukać (pod)drzewo maja̧ce swój korzeń w wierzchołku : przeszukujemy jego lewe poddrzewo (z korzeniem w odwiedzamy wierzchołek (korzeń drzewa), ), ). przeszukujemy jego prawe poddrzewo (z korzeniem w Przeszukiwanie preorder. Aby przeszukać (pod)drzewo maja̧ce swój korzeń w wierzchołku : odwiedzamy wierzchołek (korzeń drzewa), przeszukujemy jego lewe poddrzewo (z korzeniem w przeszukujemy jego prawe poddrzewo (z korzeniem w ), ). Przykład 1.10 Jeżeli przeszukamy drzewo z rysunku 1.4 metoda̧ inorder, to etykiety utworza̧ cia̧g: czyli wyrażenie w postaci infixowej, ale bez nawiasów. Przeszukanie tego samego drzewa metoda̧ preorder da cia̧g etykiet: ) Jest to tak zwana postać prefixowa wyrażenia. Znak operacji wystȩpuje w niej przed argumentami. Podobne jak w postaci postfixowej, postać prefixowa da siȩ jednoznacznie rozkładać i nie wymaga nawiasów. 1.5 Drzewa poszukiwań binarnych Drzewa sa̧ podstawowa̧ struktura̧ przy budowie dużych baz danych. Jeda̧ z najprostszych takich struktur sa̧ drzewa poszukiwań binarnych. Aby utworzyć drzewo poszukiwań binarnych, zaczynamy od pustego drzewa, a nastȩpnie wstawiamy po kolei elementy, które maja̧ być przechowywane w drzewie. Wstawiane elementy powinny być z jakiegoś uporza̧dkowanego zbioru. Poniżej przedstawiamy algorytmu wstawiania elementów do drzewa. oznacza wartość przechowywana̧ w wierzchołku . Pamiȩtajmy, że oznacza poddrzewo o korzeniu w wierzchołku . Algorytm wstawiania elementu do drzewa poszukiwań binarnych. Aby wstawić element do drzewa : jeżeli drzewo jest puste, to (wstaw do korzenia ), 14 Rozdział 1. Struktury danych w przeciwnym razie porównaj z zawartościa̧ korzenia : jeżeli jeżeli , to wstaw do poddrzewa , , to wstaw do poddrzewa . Przykład 1.11 Przypuśćmy, że mamy cia̧g liczb naturalnych: # ! & Utworzymy dla tego cia̧gu drzewo poszukiwań binarnych. Rysunek 1.5: Drzewo poszukiwań po wstawieniu elementów: 128, 76, 106, 402 Po wstawieniu pierwszych czterech elementów cia̧gu otrzymamy drzewo, które jest przedstawione na rysunku 1.5, a po wstawieniu całego cia̧gu otrzymamy drzewo, które jest przedstawione na rysunku 1.6. Jeżeli teraz przeszukamy to drzewo metoda̧ inorder, to otrzymamy ten sam cia̧g, ale uporza̧dkowany: ! & ! Jeżeli mamy już drzewo poszukiwań binarnych , to dla każdego wierzchołka zachodzi dla każdego , dla każdego , , . Czyli wszystkie wierzchołki w lewym poddrzewie zawieraja˛ wartości mniejsze od wartości w , a wszystkie wierzchołki w prawym poddrzewie zawieraja˛ wartości mniejsze od wartości w . Aby stwierdzić, czy jakiś element znajduje siȩ na tym drzewie. Postȩpujemy podobnie jak przy wstawianiu elementów. Zaczynamy od korzenia drzewa i szukamy elementu za pomoca̧ poniższego algorytmu. 1.6. Zadania 15 Rysunek 1.6: Drzewo dla cia̧gu: 128,76,106,402,100,46,354,1018,112,28, 396,35 Algorytm szukania elementu na drzewie . Aby stwierdzić, czy element znajduje siȩ na drzewie : jeżeli jest puste, to koniec, elementu nie ma na drzewie, jeżeli nie jest puste, to porównujemy z wartościa̧ : jeżeli ( jeżeli jeżeli , to koniec, znaleźliśmy element na drzewie, , , to szukamy w prawym poddrzewie . , to szukamy w lewym poddrzewie W drzewie poszukiwań binarnych czas wyszukiwania lub wstawiania elementu jest , gdzie jest wysokościa̧ drzewa. W obu algorytmach tylko raz przechodzimy od korzenia w dół do liścia. Najlepiej by było, gdyby wysokość drzewa była rzȩdu logarytm od liczby wierzchołków, ale nie w każdym drzewie poszukiwa ń binarnych tak musi być. 1.6 Zadania 1. Ile wierzchołków może mieć drzewo binarne wysokości ? 2. Przeszukaj metoda̧ „w gła̧b” („wszerz”) drzewo z rysunku 1.7. 3. Narysuj drzewo dla wyrażenie postfixowej i prefixowej. . Przedstaw to wyrażenie w postaci 16 Rozdział 1. Struktury danych 4. Narysuj drzewo dla wyrażenie . Przedstaw to wyrażenie w postaci infixowej i prefixowej. Oblicz wartość tego wyrażenia. Przedstaw to wyrażenie w postaci infixowej i prefixowej. 5. Wypisz w postaci infixowej, prefixowej i postfixowej wyrażenie przedstawione na rysunku 7. Rysunek 1.7: Drzewo wyrażenia 6. Narysuj drzewo poszukiwań binarnych dla nastȩpuja̧cego cia̧gu liczb: 30, 43, 13, 8, 50, 40, 20, 19, 22. 7. Narysuj drzewo poszukiwań binarnych dla nastȩpuja̧cego cia̧gu słów: słowik, wróbel, kos, jaskółka, kogut, dziȩcioł, gil, kukułka, szczygieł, sowa, kruk, czubatka. [Fragment wiersza Ptasie radio Juliana Tuwima] krawȩdzi. liściach ma wierzchołków 8. Udowodnij, że każde drzewo o werzchołkach ma 9. Udowodnij, że każde pełne drzewo binarne o wewnȩtrznych. Wskazówka. Drzewo binarne nazywa siȩ pełne, jeżeli każdy jego wierzchołek ma albo dwóch synów, albo nie ma synów wcale (jest liściem).