Materiały pochodzą z Platformy Edukacyjnej Portalu www.szkolnictwo.pl Wszelkie treści i zasoby edukacyjne publikowane na łamach Portalu www.szkolnictwo.pl mogą być wykorzystywane przez jego Użytkowników wyłącznie w zakresie własnego użytku osobistego oraz do użytku w szkołach podczas zajęć dydaktycznych. Kopiowanie, wprowadzanie zmian, przesyłanie, publiczne odtwarzanie i wszelkie wykorzystywanie tych treści do celów komercyjnych jest niedozwolone. Plik można dowolnie modernizować na potrzeby własne oraz do wykorzystania w szkołach podczas zajęć dydaktycznych. Algorytmy sortujące Drzewa decyzyjne. Sortowanie przez kopcowanie Drzewo (tree) W informatyce drzewa są strukturami danych reprezentującymi drzewa matematyczne. W naturalny sposób reprezentują hierarchię danych (obiektów fizycznych i abstrakcyjnych, pojęć, itp.), dlatego głównie do tego celu są stosowane. Drzewa ułatwiają i przyspieszają wyszukiwanie, a także pozwalają w łatwy sposób operować na posortowanych danych. Posiadają one bardzo duże znaczenie, są stosowane praktycznie w każdej dziedzinie informatyki (np. bazy danych, grafika komputerowa, przetwarzanie tekstu, telekomunikacja). Drzewo (ang. tree) jest dynamiczną strukturą zbudowaną z węzłów N lub wierzchołków (ang. node). Każdy z węzłów może posiadać jednego rodzica (przodka lub węzła nadrzędnego) (ang. parent node) i kilku potomków (dzieci lub węzły potomne) (ang. child node). Węzeł, który nie posiada rodzica nazywamy węzłem głównym lub korzeniem drzewa R (ang. root node). Węzeł, który nie posiada potomka to węzeł terminalny – liść L (ang. leaf). Drzewo może posiadać wiele liści lecz tylko jeden korzeń. R N L L L N L Drzewo binarne (binary tree) Drzewo binarne jest hierarchiczną strukturą danych. W hierarchii liniowej każdy element może posiadać co najwyżej jeden element następnego ciągu tzw. następnik. Każdy węzeł może posiadać dwa następniki (dzieci) – stąd nazwa drzewa – binarne to znaczy dwójkowe – zawierające dwa elementy. Węzły są połączone krawędziami symbolizującymi następstwo kolejnych elementów w strukturze drzewa binarnego. Według rysunku, po prawej stronie węzeł A posiada dwa węzły potomne: B i C. Węzeł B nosi nazwę lewego potomka (ang. left child node), a węzeł C nosi nazwę prawego potomka (ang. right child node). Z kolei węzeł B posiada węzły potomne D i E, a węzeł C ma węzły potomne F i G. Natomiast liściami są węzły terminalne D, E, F i G. A B D C E F Przykład drzewa binarnego G Drzewo binarne a tablica Aby móc przetwarzać za pomocą komputera struktury drzew binarnych musimy zapisać, przedstawić je w pamięci. W tym celu stosujemy nelementową tablicę (rys. 1) – każdy jej element reprezentuje jeden węzeł drzewa binarnego. Dla określenia związku pomiędzy poszczególnymi indeksami elementów w tablicy a położeniem tych elementów w strukturze drzewa binarnego określimy: • element d[1] będzie korzeniem drzewa • i-ty poziom drzewa binarnego wymaga 2i-1 węzłów, które będą kolejno pobierane z tablicy (rys. 2) W celu określenia k-tego węzła (rys. 3) zostaną wprowadzone następujące oznaczenia – dla węzłów potomnych: • 2k – lewy potomek • 2k+1 – prawy potomek • węzeł nadrzędny ma indeks równy [k / 2] (dzielenie całkowitoliczbowe) Rys. 1 Tablica Rys. 2 Drzewo binarne d[k/2] d[k] d[2k] d[2k+1] Rys. 3 Drzewo binarne dla k-tego węzła Przykład 1 Jako przykład zostanie przedstawiony sposób w jaki tworzymy drzewo binarne dla zbioru liczb [8 6 9 2 5 7 1] W celu utworzenia drzewa binarnego bierzemy pierwszy element zbioru, w naszym przypadku 8 – będzie to korzeń 8 8692571 8 6 Kolejnym etapem jest dołączenie do korzenia dwóch węzłów potomnych – elementów, które w zbiorze znajdują się obok siebie. Są to 6 i 9. 9 8692571 8 6 Następnie, kolejne dwa elementy ze zbioru dołączamy do lewego węzła potomnego (6) – będą to jego węzły potomne – liczby 2 i 5. 9 2 5 8692571 8 6 2 Ostatnim etapem jest dołączenie kolejnych elementów tj. 7 i 1 do prawego węzła. Drzewo jest kompletne. 9 5 7 8692571 1 Ścieżką nazywamy ciąg węzłów drzewa binarnego spełniających warunek, iż każdy węzeł poprzedni jest rodzicem węzła następnego. Jeśli ścieżka składa się z k węzłów, to długością ścieżki jest liczba k - 1. W celu zrozumienia zawiłej definicji zilustrujemy to przykładem: d[1] d[2] d[4] d[8] d[3] d[5] d[9] d[10] d[6] d[11] d[12] d[7] d[13] d[14] d[15] Na rysunku została przedstawiona ścieżka biegnąca przez węzły {d[1], d[2], d[5], d[10]} zawiera ona cztery węzły, ma zatem długość równą 3. Wysokością drzewa binarnego nazwiemy długość najdłuższej ścieżki od korzenia do liścia. W powyższym przykładzie taka ścieżka ma długość równą 3 – zatem wysokość przedstawionego drzewa ma wysokość równą 3. Dla n węzłów zrównoważone drzewo binarne ma wysokość równą: h = [log2 n] Kopiec Kopiec jest drzewem binarnym, które musi spełnić następujący warunek, tzw. warunek kopca: Węzeł nadrzędny jest większy lub równy węzłom potomnym (w porządku malejącym relacja jest odwrotna - mniejszy lub równy). Własności kopca: • korzeń zawsze jest największym (w porządku malejącym najmniejszym) elementem z całego drzewa binarnego • uporządkowanie - wartość każdego wierzchołka (rodzica) jest większa niż wartości jego potomków. • kształt - synowie znajdują się na jednym lub więcej poziomach, a te na najniższym poziomie (liście) są przesunięte jak najbardziej w lewo. Wynika z tego, że jeżeli drzewo zawiera n wierzchołków, to żaden z nich nie jest bardziej oddalony od korzenia niż o (log n) węzłów. Własności te są warunkami na tyle silnymi, aby umożliwić szybkie odnalezienie elementu największego lub najmniejszego w zbiorze, a jednocześnie umożliwiają szybką reorganizację struktury kopca po dodaniu lub usunięciu z niego elementu. Przykład 2 Utwórzmy kopiec dla elementów danego zbioru liczb [7 6 9 3 4 8 11] 7 7 6 9 3 4 8 11 7 Podobnie jak w przykładzie 1, budowę kopca rozpoczynamy od pierwszego elementu zbioru, w naszym przypadku liczba 7 będzie korzeniem. Do korzenia dołączamy kolejny element zbioru. Warunek kopca jest spełniony 6 7 6 9 3 4 7 11 7 6 9 7 6 9 3 4 7 11 7 6 9 Ten krok ma na celu przywrócenie warunku kopca – dlatego za nowy węzeł nadrzędny wybieramy nowo dodany węzeł. Zamieniamy ze sobą miejscami węzeł nadrzędny z węzłem dodanym – czyli 7 z 9. Po zamianie węzła 7 z 9 warunek kopca jest spełniony, możemy przejść do dodawania kolejnych elementów zbioru. 9 6 Dodajemy kolejny element ze zbioru. Sprawdzamy warunek kopca. W tym przypadku dodany element 9 jest większy od elementu go poprzedzającego – warunek kopca nie jest spełniony dlatego wykonujemy kolejną operację. 7 Przykład 2 cd. 9 6 3 8 4 7 11 Dołączenie ostatniego elementu znów narusza warunek kopca. Zamieniamy miejscami węzeł 8 z węzłem 11. 7 6 9 3 4 8 11 9 6 3 11 4 7 8 Po wymianie węzłów 8 i 11 warunek kopca został przywrócony, ale tylko na tym poziomie. Widzimy, że węzeł 11 stał się dzieckiem węzła 9 – na tym poziomie warunek kopca został naruszony. Musimy dokonać zamiany miejsc węzła 9 i 11 aby przywrócić warunek kopca. 11 6 3 9 4 7 8 Wymiana węzłów 9 i 11 to ostatni krok w tworzeniu danego kopca. W całym drzewie spełniony jest warunek kopca – zadanie zostało rozwiązane. Specyfikacja algorytmu konstrukcji kopca Dane wejściowe : d [ ] - Zbiór zawierający elementy do wstawienia do kopca. Numeracja elementów rozpoczyna się od 1 n - Ilość elementów w zbiorze, nN Dane wyjściowe : d [ ] - zbiór zawierający kopiec Zmienne pomocnicze : i - zmienna licznikowa pętli umieszczającej kolejne elementy zbioru w kopcu, i N, I {2,3,...,n} j,k - indeksy elementów leżących na ścieżce od wstawianego elementu do korzenia, j,k C x - zmienna pomocnicza przechowująca tymczasowo element wstawiany do kopca Lista kroków : K01: Dla i = 2, ..., n: wykonuj kroki 2...5 K02: j := i; k := j div 2; K03: x := d [ i ]; K04: Dopóki ( k > 0 ) and ( d [k] < x ) wykonuj : d [ j ] := d [ k ]; j := k; k := j div 2; K05: d [ j ] := x; K06: Koniec START Schemat blokowy Przedstawiony algorytm ma klasę złożoności O(n). Kopiec jest tworzony w tym samym zbiorze wejściowym d[ ]. W pętli 1 kolejne elementy zbioru są wstawiane do kopca. Rozpoczynamy od elementu drugiego, ponieważ pierwszy i tak zostaje na swoim miejscu. Inicjujemy także następujące zmienne: j – pozycja wstawianego elementu (liścia), k – pozycja rodzica (elementu nadrzędnego), x – zapamiętująca wstawiany element. Zadaniem pętli 2 jest znalezienie w kopcu miejsca do wstawienia zapamiętanego elementu w zmiennej x. Pętla jest wykonywana do momentu k=0 – osiągnięcia korzenia lub znalezienia przodka większego od zapamiętanego elementu – złamania warunku kopca. W takim przypadku następuje zamiana miejsc elementów tak aby warunek kopca został spełniony. Po zakończeniu pętli w zmiennej j znajduje się numer pozycji w zbiorze d[ ], na której należy umieścić element w x. Po zakończeniu pętli nr 1 w zbiorze zostaje utworzona struktura kopca. i:=2 i<=n NIE KONIEC TAK j:=i k:=j div 2 x:=d[i] 1 k>0 2 NIE TAK d[k]<x NIE TAK d[j]:=x d[j]:=d[k] j:=k k:=j div 2 i:=i+1 Rozbiór kopca Rozbiór kopca polega na : • usunięciu korzenia kopca i wstawieniu w jego miejsce ostatniego elementu kopca • sprawdzeniu, czy warunek kopca jest spełniony, jeżeli nie, należy tak ustawić elementy kopca, by ten warunek został spełniony 8 • rozbioru dokonujemy tak długo, aż kopiec będzie pusty 6 5 Zilustrujmy to na przykładzie rozbioru następującego kopca: 2 8 8 1 6 2 5 5 7 6 1 5 1 2 5 5 7 5 6 7 5 6 2 1 5 7 7 1 Rozbiór rozpoczynamy od usunięcia korzenia i wstawiamy na jego miejsce ostatni element kopca. Krok ten powoduje zaburzenie struktury kopca. 5 2 5 7 6 2 7 5 1 6 2 5 5 1 W kolejnych krokach sprawdzamy czy warunek kopca został spełniony na poszczególnych poziomach struktury. Dokonujemy zamiany węzłów do momentu spełnienia warunku. Rozbiór kopca cd. 78 7 1 6 2 5 5 6 1 5 2 5 1 6 2 Ponownie usuwamy korzeń i wstawiamy na jego miejsce ostatni element kopca. 6 5 6 1 5 5 2 5 5 5 2 1 Struktura kopca została naruszona. Dokonujemy wymiany węzłów między sobą tak aby w drzewie spełniony został warunek kopca. 678 6 5 2 1 5 5 1 2 1 5 2 Usuwamy kolejny korzeń i wstawiamy na jego miejsce ostatni liść. Warunek kopca zostaje naruszony. 5 5 5 1 2 5 5 2 1 5 Ponownie wymieniamy elementy kopca tak aby warunek kopca został spełniony. Rozbiór kopca cd. 5678 5 2 1 5 2 5 Usuwamy korzeń i wstawiamy na jego miejsce ostatni element kopca. 1 5 1 2 2 5 1 Sprawdzamy czy struktura kopca została naruszona. Dokonujemy wymiany węzłów między sobą tak aby w drzewie spełniony został warunek kopca. 55678 5 2 1 1 2 2 55678 1 2 Usuwamy kolejny korzeń i wstawiamy na jego miejsce ostatni element drzewa. Teraz zostaje nam już tylko jedna wymiana. 2 1 1 1 2 55678 Wymieniliśmy ostatnie elementy drzewa. Zaczynając od korzenia dodajemy pozostałe elementy do posortowanego zbioru. Kopiec został rozebrany. Specyfikacja algorytmu rozbioru kopca Dane wejściowe : d [ ] - Zbiór zawierający poprawną strukturę kopca. Numeracja elementów rozpoczyna się od 1 n - Ilość elementów w zbiorze, nN Dane wyjściowe : d [ ] - zbiór zawierający pobrane z kopca ułożone w porządku rosnącym Zmienne pomocnicze : i - zmienna licznikowa pętli pobierającej kolejne elementy z kopca, i N, i {n, n-1,...,2} j,k - indeksy elementów leżących na ścieżce w dół od korzenia, j,k N m - indeks większego z dwóch elementów potomnych, m N Lista kroków: K01: Dla i = n, n - 1, ..., 2: wykonuj K02...K08 K02: d[1] ↔ d[i] K03: j ← 1; k ← 2 K04: Dopóki (k < i) wykonuj K05...K08 K05: Jeżeli (k + 1 < i) ∧ (d[k + 1] > d[k]), to m ← k + 1 inaczej m ← k K06: Jeżeli d[m] ≤ d[j], to wyjdź z pętli K04 i kontynuuj następny obieg K01 K07: d[j] ↔ d[m] K08: j ← m; k ← j + j K09: Koniec START Schemat blokowy Celem pętli nr 1 jest zamiana miejscami kolejnych ostatnich kiści z korzeniem drzewa, natomiast pętli 2 - przywrócenie strukturze warunku kopca. Pętla nr 1 przegląda elementy zbioru od końca od n do 2. Element i-ty zostaje wymieniony z korzeniem kopca. Po tej zamianie rozpoczynamy w pętli 2 przeglądanie drzewa od korzenia w dół. Zmienna j przechowuje indeksy przodka, zmienna k przechowuje indeks lewego potomka. Koniec pętli nr 2 następuje gdy węzeł j-ty nie ma już potomka. Zmienna m wyznacza nam indeks większego potomka z dwóch węzłów dzieci. Następnie sprawdzamy czy zachowany jest warunek kopca, jeśli tak to kończymy pętlę, jeśli nie to zmieniamy miejscami węzeł nadrzędny j-ty z jego największym potomkiem m-tym i jako nowy węzeł nadrzędny przyjmujemy węzeł m-ty. W zmiennej k wyznaczamy indeks lewego potomka i kontynuujemy pętlę 2. Każde wyjście z pętli 2 zmniejsza zmienną i o 1, ponownie przenosimy się do ostatniego liścia i kontynuujemy pętlę 1. in 1 i>1 KONIEC TAK d[1] d[i] j 1 k 2 2 k<i TAK k+1<i TAK d[k+1]>d[k] TAK m k+1 d[m] ≤ d[j] d[j] d[m] j m k j+j m k TAK i i – 1 Na podstawie przedstawionego algorytmu możemy ustalić klasę złożoności obliczeniowej rozbioru kopca. Przeanalizujmy schemat blokowy rozbioru kopca. Pętla zewnętrzna nr 1 wykona się n - 1 razy, a w każdym obiegu tej pętli pętla wewnętrzna wykona się maksymalnie log2n razy. Dlatego, gdy dokonamy górnego oszacowania otrzymamy klasę złożoności obliczeniowej rzędu O(n log n) w przypadku pesymistycznym. Przypadek optymistyczny wystąpi tylko wtedy, gdy zbiór d[ ]będzie zbudowany z ciągu tych samych elementów. Dla tego przypadku klasa złożoności obliczeniowej będzie wynosić O(n). Sortowanie przez kopcowanie (heap sort) W tej lekcji poznaliście już w jaki sposób tworzyć kopiec i jak dokonać jego rozbioru. Sortowanie przez kopcowanie (sortowanie kopcem) to nic innego jak połączenie tych dwóch elementów. W zbiorze tworzymy kopiec, a następnie dokonujemy jego rozbioru. W wyniku tego zbiór zostanie posortowany. Sortowanie to jest dość szybkim algorytmem i nie pochlania zbyt wiele zasobów pamięci. Jego asymptotyczna złożoność czasowa to O(n log n). W praktyce algorytm ten jest nieco wolniejszy od sortowania szybkiego, ale ma lepszą pesymistyczną złożoność czasową - O(n log n), podczas gdy dla quicksort jest to O(n2) co jest nie do przyjęcia dla dużych zbiorów danych. Sortowanie przez kopcowanie jest niestabilne, co może być czasami uznawane za wadę. Algorytm sortuje w miejscu. Sortowanie kopcem – specyfikacja problemu Dane wejściowe d[ ] - zbiór zawierający elementy do posortowania, które są numerowane począwszy od 1. n - Ilość elementów w zbiorze, n N Dane wyjściowe d[ ] - zbiór zawierający elementy posortowane rosnąco Zmienne pomocnicze i funkcje Twórz_kopiec - procedura budująca kopiec z elementów zbioru d[ ]. Kopiec powstaje w zbiorze d[ ]. Rozbierz_kopiec - procedura dokonująca rozbioru kopca utworzonego w zbiorze d[ ]. Wynik rozbioru trafia z powrotem do zbioru d[ ] START Lista kroków K01: Twórz_kopiec Twórz_kopiec K02: Rozbierz_kopiec K03: Koniec Rozbierz_kopiec KONIEC Bibliografia • • • • • • • • Thomas H. Cormen, Leiserson C. E., Rivest R. L.: Wprowadzenie do algorytmów, WNT 2001 Wirth N.: Algorytmy + struktury danych = program, WNT, Warszawa 2002 Sysło M.: Algorytmy, WSiP, Warszawa 1997 Wróblewski P.: Algorytmy, struktury danych i techniki programowania, Wyd. Helion, 2003 www.pl.wikipedia.org www.algorytm.org www.encyklopedia.pwn.pl www.edu.i-lo.tarnow.pl