Algorytmy sortowania wewnętrznego
Transkrypt
Algorytmy sortowania wewnętrznego
Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych Algorytmy i struktury danych Laboratorium Nr 3 Algorytmy sortowania wewnętrznego 1 Cel ćwiczenia Ćwiczenie ma na celu zapoznanie studentów z wybranymi metodami sortowania wewnętrznego. Są to: sortowanie przez proste wstawianie, sortowanie przez proste wybieranie (przez selekcję), sortowanie bąbelkowe, sortowanie szybkie (quicksort), sortowanie stogowe i sortowanie przez scalanie. 2 Wstęp Metoda sortowania jest nazywana wewnętrzną, jeśli dane uporządkowane są w postaci tablicy (zatem mieszczą się w pamięci operacyjnej RAM) oraz w procesie sortowania nie korzysta się z tablic pomocniczych. Dobra metoda sortowania powinna charakteryzować się stabilnością. Metoda sortowania jest stabilna, jeśli zachowuje względną kolejność elementów ze zdublowanymi kluczami. Przykładowo, jeśli ułożoną alfabetycznie listę osób posortuje się po roku urodzenia, metoda stabilna da w efekcie listę, w której osoby urodzone w tym samym roku będą nadal ułożone w kolejności alfabetycznej, natomiast metoda niestabilna nie gwarantuje tego. 3 3.1 Metody sortowania wewnętrznego Sortowanie przez proste wstawianie Metodę tę można porównać do porządkowania kart na ręku: należy brać kolejne, jeszcze nie uporządkowane karty, i wstawiać na odpowiednie miejsce pomiędzy karty już uporządkowane. Zakładając, że ciąg do posortowania oznaczony jest przez a i składa się on z N elementów, sortowanie przez proste wstawianie odbywa się w następujący sposób: dla każdego i = 2, 3, . . . , N trzeba powtarzać wstawianie elementu ai w już uporządkowaną część ciągu a1 ≤ · · · ≤ ai−1 . W metodzie tej obiekty podzielone są umownie na dwa ciągi: ciąg wynikowy a1 , a2 , . . . , ai−1 oraz ciąg źródłowy ai . . . an . W każdym kroku, zaczynając od i = 2, i-ty element ciągu źródłowego przenoszony jest do ciągu wynikowego i wstawiany w odpowiednie miejsce, po czym wartość i jest inkrementowana. Pętla zaczyna się od i = 2, gdyż zakładamy, że element a1 już znajduje się na właściwym miejscu. Ponieważ przy przenoszeniu elementów w lewo istnieje możliwość przekroczenia zakresu tablicy, najczęściej stosuje się wartownika. Dla uproszczenia kodu zakłada się, że wartownik jest umieszczany pod indeksem 0. Wartownik musi być równy przenoszonemu elementowi, co zapewni, że nie da się wyjść poza tablicę. Wstawienie elementu w odpowiednie miejsce ciągu ilustruje rys. 1. Kolorem jasnoniebieskim zaznaczono elementy już posortowane, niebieskim - element do wstawienia. W zaprezentowanym przykładzie wartownikiem jest element a0 . Pełni on także rolę bufora, w którym przechowywany jest aktualny element na czas przesunięcia innych elementów. Nie można wykroczyć się poza pierwszy element tego ciągu, gdyż element a0 ciągu, będący wartownikiem, jest zawsze elementem mniejszym lub równym od wstawianego, więc pętla zawsze zakończy się prawidłowo. Przykład opisuje wstawienie litery R pomiędzy elementy O i S. W pierwszym kroku kopiujemy literę R na miejsce a0 , i staje się ona wartownikiem. Po skopiowaniu miejsce a4 , w którym dotychczas była litera R, możemy potraktować jako „wolne” i wpisać tam inny element ciągu. Kopiujemy zatem literę a3 1 Wartownik O Ciąg S T R 1 R O S O S T 2 T 3 O 4 O Wynik R S T S T Rys. 1: Wstawianie elementu do uporządkowanej tablicy (litera T ) do a4 - teraz „wolne” miejsce to a3 . Powtarzając kopiowanie do momentu natrafienia na literę większą lub równą przenoszonej, czyli jak w przykładzie literze O, otrzymamy wolne miejsce, w które należy wstawić przenoszoną literę R. Wstaw element ai na miejsce o indeksie 0, otrzymując wartownika; Dopóki element a0 jest mniejszy od ai−1 wykonuj przypisz elementowi ai element ai−1 zmniejsz i Przypisz elementowi ai element a0 Przypisujemy do zmiennej a0 element ai , następnie dopóki a0 < ai wpisujemy do i-tego elementu ciągu element i − 1, zmniejszając jednocześnie i. Na zakończenie elementowi i-temu trzeba przypisać wartość a0 . Algorytm sortowania przez proste wstawianie działa stosunkowo wolno, jest on bowiem algorytmem klasy O(N 2 ). Oznacza to, że w praktyce algorytm ten nie jest stosowany do sortowania długich ciągów. Jednocześnie zaletą algorytmu jest jego prostota. Aby posortować całą tablicę, należy po prostu wykonać algorytm wstawiania elementów do uporządkowanej tablicy dla elementów od indeksu 2 do N . Rozpatrzmy działanie algorytmu na przykładowym ciągu liter SORTOWANIE. Na rys. 2, który obrazuje tę operację, elementy posortowane oznaczono kolorem jasnoniebieskim. Wartownik Ciąg S O R T O W A N I E I1 O S O R T O W A N I E I2 R O S R T O W A N I E I3 T O R S T O W A N I E I4 O O R S T O W A N I E I5 W O O R S T W A N I E I6 A O O R S T W A N I E I7 N A O O R S T W N I E I8 I A N O O R S T W I E I9 E A I N O O R S T W E A E I N O O R S T W Wynik Rys. 2: Sortowanie przez proste wstawianie Na początku rozważana jest druga litera ciągu - litera O (pierwsza litera ciągu - litera S - znajduje się już na właściwym miejscu). Litera O zostaje skopiowana na miejsce wartownika (a0 ). Następnie powinna ona zostać wstawiona na odpowiednie miejsce w ciągu, czyli należy przenieść ją przed literę S. Warto zauważyć, że w tej iteracji przed wyjściem z lewej strony poza zakres tablicy chroni wartownik. W iteracji nr 3 na właściwym miejscu, pomiędzy literami O i S, zostaje umieszczona litera R. 2 W kolejnych iteracjach należy postępować analogicznie. W niektórych iteracjach (np iteracja nr 3) przestawienie nie jest w ogóle konieczne, w pozostałych na właściwych miejscach zostają umieszczone odpowiednie litery. 3.1.1 Sortowanie przez wstawianie połówkowe Ta metoda sortowania jest ulepszeniem sortowania przez proste wstawianie. Modyfikacja opiera się na prostej obserwacji: ponieważ wstawiamy elementy do posortowanej tablicy, można łatwo znaleźć miejsce, w które należy wstawić element dzięki metodzie przeszukiwania binarnego. Poza tą zmianą algorytm działa na identycznej zasadzie, jak zwykłe sortowanie przez proste wstawianie. Dzięki takiej modyfikacji zmniejsza się ilość porównań, liczba przesunięć pozostaje niezmieniona. Ponieważ jednak przesuwanie sortowanych elementów jest zwykle kosztowniejsze niż ich porównywanie, usprawnienie to nie jest znaczące. Dany jest ciąg o elementach al . . . ap oraz element do wstawienia el. Poszukujemy miejsca w ciągu, w które powinien zostać wstawiony element el za pomocą algorytmu przeszukiwania binarnego. Można zapisać ten algorytm w następującej postaci: Dopóki l ≤ p wykonuj: Oblicz indeks elementu środkowego m ze wzoru m = b (l + p)/2 c; Jeśli element eljest mniejszy od elementu am : Odetnij prawą część tablicy przypisując p = m − 1; W przeciwnym wypadku: Odetnij lewą część tablicy, przypisując l = m + 1; L=1 M=5 A E I N O A E I N O P=10 O R L=6 R L=M=6 P=7 R E I N O O A E I N O O A E I N O O T M=8 O A S W P P=10 S T W P S T W P R S T W P P=7 L=8 R S T W L=P=M=7 Rys. 3: Przeszukiwanie binarne Poszukujemy miejsca, w które należy wstawić literę P w ciągu AEIN OORST W (indeksowany od 1 do 10). Na początku granice przeszukiwania określone są następująco - lewa l = 1, a prawa p = 10, gdyż przeszukujemy cały ciąg. Szukamy indeksu środkowego elementu ciągu - jest to m = b(l + p)/2c = 5, porównujemy zatem literę P ze środkową literą ciągu (litera O). Ponieważ litera P jest większa od O, a wiadomo, że ciąg jest uporządkowany, to możemy odrzucić lewą część ciągu (na lewo od litery O włącznie z samą literą O). Robimy to poprzez przesunięcie lewego indeksu poszukiwań - teraz do l należy wstawić m + 1 = 6. Na rysunku odrzucona część tablicy wyróżniona jest kolorem szarym. Następnie czynności powtarzamy: szukamy środka ciągu wg opisanej wyżej metody, otrzymując 8. Ze sprawdzenia (P < S) wynika, że tym razem litera w tablicy jest większa od szukanej, a więc można przesunąć prawą granicę przeszukania na m − 1 = 7. Kontynuując w ten sposób do momentu, gdy lewa granica jest mniejsza lub równa od prawej, otrzymamy l = 8 i p = 7. Ponieważ L ≥ R, przeszukiwanie kończy się. Indeks L jest miejscem, w które należy wstawić literę P . 3.2 Sortowanie przez proste wybieranie (przez selekcję) Jest to kolejny prosty algorytm, który można opisać w następujący sposób: należy znaleźć najmniejszy element ciągu N -elementowego i zamienić go miejscami z pierwszym elementem tego ciągu. Następnie znaleźć najmniejszy element w ciągu N − 1-elementowym (pierwszy element jest już posortowany, więc szukać najmniejszego elementu należy od indeksu 2) i zamienić go miejscami z drugim elementem ciągu. Kontynuując w ten sam sposób, ciąg zostanie posortowany. 3 Oznaczając ciąg symbolem a i zakładając, że ma on N elementów, sortowanie przez wybieranie polega na wyznaczeniu najmniejszego elementu ciągu ai . . . aN dla każdego i = 1, 2, . . . , N − 1 i zamienieniu go elementem ai . Operację tę powtarza się do czasu posortowania całego ciągu. W większości implementacji liczba zamian w tym algorytmie wynosi N −1, liczba porównań natomiast jest proporcjonalna do N 2 . Cechy te predestynują sortowanie przez proste wybieranie do obróbki zbiorów z dużymi polami danych i małymi kluczami, gdyż w takich zastosowaniach koszt przemieszczania danych dominuje nad kosztem porównań, a żaden inny algorytm nie dokonuje mniejszej ilości przestawień podczas sortowania. Wadą algorytmu sortowania przez wybieranie jest fakt, iż czas jego działania praktycznie nie zależy od stopnia uporządkowania zbioru sortowanego - oznacza to, że algorytm ten sortuje już posortowany ciąg lub ciąg zawierający te same liczby w takim samym czasie, jak ciąg całkowicie losowy. Ciąg S O R T O W A N I E I1 S O R T O W A N I E I2 A O R T O W S N I E I3 A E R T O W S N I O I4 A E I T O W S N R O I5 A E I N O W S T R O I6 A E I N O W S T R O I7 A E I N O O S T R W I8 A E I N O O R T S W I9 A E I N O O R S T W Wynik A E I N O O R S T W Rys. 4: Sortowanie przez proste wybieranie Sam algorytm ma następującą postać: Wykonuj co następuje od indeksu i = 1 do i = N − 1: Wskaż najmniejszy element spośród ai ... aN ; Zamień ten element z elementem ai . Przebieg sortowania ciągu SORTOWANIE obrazuje rys. 4. Ciąg posortowany oznaczony jest kolorem niebieskim. W pierwszym przebiegu pętli najmniejszym elementem jest A, co zostało zaznaczone kolorem ciemnoniebieskim. Element ten jest zamieniany z pierwszym elementem ciągu (litera S). W drugiej iteracji najmniejsze jest E, dlatego też jest ono zamieniane z drugim elementem ciągu (pierwszym nieposortowanym) - literą O. W każdej kolejnej iteracji wyznaczane jest minimalny element w nieposortowanej części ciągu (elementy już posortowane oznaczane są kolorem jasnoniebieskim). Następnie, jeśli element minimalny nie znajduje się na właściwym miejscu, zamienia się go z pierwszym elementem nieposortowanym. Zdarza się, że element najmniejszy znajduje się już w ciągu posortowanym (tak, jak np w iteracji 5) - w takim przebiegu pętli nie zmienia się kolejności elementów w ciągu. 3.3 Sortowanie bąbelkowe Metoda ta polega na zamienianiu miejscami dwóch sąsiadujących ze sobą elementów do czasu uzyskania zbioru uporządkowanego. Nazwa algorytmu pochodzi od analogii do pęcherzyków powietrza, ulatujących w górę tuby wypełnionej wodą. Zakładamy, że ciąg ”przeczesywany” będzie zawsze od prawej do lewej strony. Trafiając na najmniejszy element ciągu, zamieniany on jest z kolejnymi elementami aż trafi na pierwsze miejsce ciągu. W drugim przebiegu postępuje się podobnie z drugim najmniejszym elementem, co prowadzi do umieszczenia tego elementu na drugim miejscu. Kontynuując otrzyma się ciąg posortowany. Algorytm ma następującą postać: 4 Wykonuj co następuje N − 1 razy od indeksu i = N − 1 do i = 1: Wskaż na ostatni element; Wykonaj co następuje i razy od indeksu j = 1: Porównaj element j-ty z elementem j + 1; Jeśli porównywane elementy są w niewłaściwej kolejności (element j > j + 1), zamień je miejscami. Ciąg S O R T O W A N I E I1 A S O R T O W E N I I2 A E S O R T O W I N I3 A E I S O R T O W N I4 A E I N S O R T O W I5 A E I N O S O R T W I6 A E I N O O S R T W I7 A E I N O O R S T W I8 A E I N O O R S T W I9 A E I N O O R S T W Wynik A E I N O O R S T W Rys. 5: Sortowanie bąbelkowe Rozważmy działanie algorytmu na przykładowym ciągu SORT OW AN IE (rys. 5). I1 S O R T O W A N I E S O R T O W A N E I S O R T O W A E N I S O R T O W A E N I O W E N I … A S O R T Rys. 6: Sortowanie bąbelkowe - pierwsza iteracja W pierwszej iteracji (rys. 6) ostatnia litera E jest zamieniana kolejno z literami I oraz N (kolorem ciemnoniebieskim zaznaczono litery mniejsze - jeśli litera mniejsza znajduje się na dalszym miejscu ciągu, konieczna jest jej zamiana z literą poprzedzającą); zamiana zatrzymuje się na literze A. Następnie to litera A będzie zamieniana z każdą literą ją poprzedzającą i przesunie się na początek ciągu (litera A jest najmniejsza w ciągu). W kolejnych iteracjach, przy analogicznym postępowaniu, pojedyncze litery trafiają na właściwe miejsce w ciągu. Główną zaletą metody sortowania bąbelkowego jest łatwość jej implementacji. Algorytm ten ma natomiast wiele wad: zdarzają się ”puste przebiegi” ciągu bez zamian, bo elementy są już posortowane, dodatkowo algorytm ten jest bardzo wrażliwy na konfigurację danych. Przykładowo, poniższe ciągi, prawie nie różniące się od siebie, wymagają różnej ilości zamian: w pierwszym przypadku jednej, a w drugim aż sześciu: Ciąg A Ciąg B 4 4 2 6 6 18 5 18 20 20 39 39 40 40 2 3.4 Sortowanie szybkie Algorytm sortowania szybkiego (quicksort) jest najczęściej używanym algorytmem spośród wszystkich algorytmów sortowania. Podstawowa wersja tego algorytmu została wynaleziona w 1960 r. przez C.A.R. Hoare’a. Popularność algorytmu sortowania szybkiego jest spowodowana jego zaletami: • do posortowania n elementów wymaga średnio czasu proporcjonalnego do O(n log n); • czas działania algorytmu dla rzeczywistych przypadków jest najczęściej zbliżony do czasu średniego, a w wielu przypadkach czas ten jest nawet liniowy; • jest dość prosty w implementacji, biorąc pod uwagę szybkość działania. Algorytm ten ma również wady: • jest niestabilny, tzn. nie ma gwarancji zachowania kolejności takich samych elementów w tablicy posortowanej; np mając tablicę rekordów zawierających nazwiska oraz wiek, posortowaną po nazwiskach, sortując tę tablicę po wieku nie ma gwarancji, że osoby w tym samym wieku będą umieszczone alfabetycznie w tablicy posortowanej; • pesymistyczna złożoność tego algorytmu wynosi O(n2 ). Algorytm sortowania szybkiego jest przykładem wykorzystania techniki ”dziel-i-rządź”. Technika ta polega na wykorzystaniu cechy rekurencji: dekompozycji problemu na pewną ilość skończonych podproblemów tego samego typu, a następnie połączeniu otrzymanych częściowych rozwiązań w celu otrzymania rozwiązania globalnego. W wielu przypadkach technika ta pozwala na zmianę klasy algorytmu (np z O(n2 ) do O(n log2 n)). Algorytm sortowania szybkiego działa na zasadzie podziału ciągu na dwie części i niezależnego, rekurencyjnego sortowania każdej z tych części. Najważniejszą częścią algorytmu jest proces podziału, który działa następująco: • Wybierany jest element osiowy - może to być dowolny element ciągu; • Dokonuje się zamiany elementów ciągu tak, aby uzyskać dwie podtablice, pierwsza o elementach mniejszych lub równych elementowi osiowemu i druga o elementach większych lub równych elementowi osiowemu. Po dokonaniu podziału następuje rekurencyjne wywołanie algorytmu osobno dla części lewej i prawej. Można łatwo wykazać przez indukcję, że takie postępowanie prowadzi do posortowania danych, gdyż zawsze w wyniku podziału przynajmniej jeden element (osiowy) trafia na właściwą pozycję. Przykładowy schemat postępowania przy podziale ciągu jest następujący: • dokonujemy wyboru elementu osiowego. Zwykle wybiera się pierwszy lub ostatni element ciągu; • przeglądamy ciąg od jego lewego końca do momentu znalezienia elementu większego lub równego elementowi osiowemu; • przeglądamy ciąg od prawego końca, do czasu znalezienia elementu mniejszego lub równego elementowi osiowemu; • jeśli indeksy powyższych przeszukiwań nie minęły się, zamieniamy miejscami oba znalezione elementy; • kontynuujemy przeglądanie ciągu z lewej i prawej strony oraz zamianę miejscami elementów do czasu, gdy indeksy miną się. Przykład działania procesu podziału obrazuje rys. 7. Zakłada się, że elementem osiowym jest ostatni element ciągu (w przykładzie jest to litera E), co zostało na rysunku oznaczone kolorem jasnoniebieskim. Idąc od początku ciągu szukamy pierwszego elementu większego lub równego od osiowego - warunek ten spełnia litera S. Następnie idąc od prawej strony szukamy elementów mniejszych lub równych od osiowego - jak łatwo się domyślić, trafiamy na sam element osiowy, czyli literę E. Zamieniamy miejscami litery S oraz E. Kontynuując, z lewej strony elementem większym od osiowego jest O, natomiast z prawej strony jest to dopiero litera A (litery N oraz I są większe od elementu osiowego - litery E). Zamieniamy miejscami litery O oraz A. W kolejnej iteracji z lewej strony dochodzimy do elementu R, a z prawej - do elementu A, który jednak leży przed elementem R. Oznacza to zakończenie „przeczesywania” tablicy. 6 S O R T O W A N I E S O R T O W A N I E E O R T O W A N I S E A R T O W O N I S Rys. 7: Sortowanie szybkie - proces podziału Po dokonaniu podziału ciągu na dwa podciągi, wystarczy tylko wywołać rekurencyjnie funkcję wykonującą szybkie sortowanie dla lewego oraz dla prawego podciągu. Podział na podciągi przebiega w miejscu minięcia się indeksów (w przykładzie - na literze R, czyli dla indeksu równego 3). Przykład działania algorytmu szybkiego sortowania dla ciągu SORTOWANIE obrazuje rys. 8. Poszczególne linie przedstawiają postać ciągu już po wykonaniu podziału (zakłada się, że element osiowy jest ostatnim elementem ciągu - w każdej iteracji został on oznaczony kolorem jasnoniebieskim), miejsce podziału oznaczono pionową kreską. Ciąg S O R T O W A N I E I1 E A R T O W O N I S I2 A E I3 R S O I O N W T I4 N I O S O R I5 I N I6 O R O S I7 O R O O R T W T W I8 I9 Wynik A E I N O O R S Rys. 8: Sortowanie szybkie W pierwszej iteracji elementem osiowym jest E. Po dokonaniu podziału, wywoływana jest rekurencyjnie procedura sortująca na części lewej (ciąg EA) oraz prawej (ciąg RT OW ON IS). Dalsza część algorytmu wykonywana jest na identycznej zasadzie. Podczas implementowania algorytmu sortowania szybkiego należy pamiętać o: • zapewnieniu zakończenia programu rekurencyjnego, czyli: – procedura nie może wywołać sama siebie dla ciągów o długości 1 lub mniejszej – procedura będzie wywoływać sama siebie dla ciągów o liczności mniejszej niż ilość elementów, z którymi została ona wywołana. • Częstym błędzie implementacji polegającym na zapomnieniu o tym, by za każdym razem przynajmniej jeden element był wstawiany na właściwą pozycję. Błąd taki kończy się nieskończoną pętlą rekurencyjną, gdy element osiowy okazuje się być najmniejszym lub największym elementem ciągu. 3.5 Sortowanie stogowe Aby omówić sortowanie stogowe, niezbędne jest wprowadzenie kilku podstawowych pojęć. Drzewo binarne jest drzewem, dla którego każdy ojciec ma co najwyżej dwóch bezpośrednich synów. Strukturę taką przedstawia rys. 9. Każdy element drzewa, posiadający elementy dołączone na niższym 7 21 9 15 3 2 11 11 Rys. 9: Przykładowe drzewo binarne, uporządkowane stogowo poziomie, jest ich poprzednikiem (ojcem), natomiast same elementy dołączone są jego następnikami (zwanymi również potomkami lub synami). Na przykład, na rys. 9 elementy „9” i „15” są bezpośrednimi następnikami (synami) elementu „21”. Element „9” jest także bezpośrednim poprzednikiem (ojcem) elementów „3” i „2”. Korzeń drzewa to element, nie posiadający poprzedników (znajdujący się na samej górze struktury - w przykładzie to element „21”). W drzewie uporządkowanym stogowo każdy poprzednik (ojciec) jest większy lub równy od wszystkich swoich następników (synów). Oznacza to, że korzeń drzewa jest jego największym elementem. Drzewo z rys. 9 jest uporządkowane stogowo. Stóg jest zbiorem elementów, ułożonych w pełne drzewo binarne, uporządkowane stogowo i przedstawione w formie tablicy. Można przyjąć następującą zasadę: element na pozycji i ma następniki na pozycjach 2 ∗ i oraz 2 ∗ i + 1; analogicznie element na pozycji i ma poprzednika na pozycji bi/2c. Następniki elementu są mniejsze od samego elementu. Pośrednio można wnioskować, że na podstawie tej zasady żaden węzeł drzewa uporządkowanego stogowo nie jest większy od korzenia tego drzewa. A zatem ciąg kluczy: hl , hl+1 , . . . , hp jest stogiem, jeśli hi ≤ h2∗i ∧ hi ≤ h2∗i+1 . Tablicowa reprezentacja drzewiasta tablicy H może przedstawiać się następująco: h1 h2 h3 h4 h5 h6 h7 h8 h9 h10 h11 h12 h13 h14 h1 h2 h3 h4 h8 h5 h9 h10 h6 h11 h12 h7 h13 h14 Rys. 10: Tablica H oraz drzewo binarne Rozpoczynając od pierwszego elementu tablicy do jej ostatniego elementu, dla wybranego elementu o indeksie i traktowanego jako poprzednik definujemy dwa następniki jako elementy o indeksach 2 ∗ i i 2 ∗ i + 1, np. dla elementu h tablicy H o indeksie 1 (h1 ) jego następnikami będą elementy tablicy H o indeksach 2 (h2 ) i 3 (h3 ). Analogicznie dla elementu h13 jego następnikami, zgodnie z opisaną zależnością, będą elementy h26 oraz h27 . Dla każdego następnika można równie łatwo znaleźć jego poprzednika: w tablicy wystarczy odnaleźć element o indeksie bi/2c - np dla elementu h5 jego poprzednikiem będzie element h2 . Korzeniem drzewa jest element h1 (o indeksie 1). Oczywiście muszą zachodzić zależności stogu, tzn. dla każdego i-tego elementu tablicy jego następniki na pozycjach 2 ∗ i oraz 2 ∗ i + 1 muszą być mniejsze od tego elementu. Należy zwrócić uwagę, że w podanym przykładzie element h7 ma tylko jednego następnika - h14 . Wynika to z faktu, że w tablicy H jest tylko 14 elementów. Mamy tutaj 7 poprzedników i jednocześnie 13 następników, czyli elementy h1 , h2 , . . . , h7 są poprzednikami (są elementami z przynajmniej jednym następnikiem); jednocześnie elementy h2 , h3 , . . . , h13 są następnikami (są elementami z przypisanym poprzednikiem). A zatem w stogu część elementów jest jednocześnie poprzednikiem i następnikiem. 3.5.1 Algorytm tworzenia stogu z tablicy - przesiewanie stogu Jak wspomniano, stóg ma właściwość taką, że każdy i-ty element jest większy od swoich następników (elementów o indeksach 2 ∗ i oraz 2 ∗ i + 1). Zatem aby sprawdzić, czy tablica jest uporządkowana stogowo, należy przeanalizować wszystkie elementy będące poprzednikami i sprawdzić, czy ich następniki są mniejsze od tych elementów, a jeśli nie, zamienić miejscami badany element z następnikiem, który 8 jest od niego większy. Po dokonaniu zamiany należy jeszcze sprawdzić, czy przesiany w dół element nie zakłócił porządku stogowego. Aby zmniejszyć ilość porównań, porównuje się następniki elementu, a sam element porównuje się z większym z jego następników. Przykład działania procedury przesiewania Dana jest tablica oraz reprezentowane przez nią drzewo binarne: 7 4 5 8 3 9 2 6 7 4 8 5 3 9 2 6 Rys. 11: Tablica H oraz reprezentowane przez nią drzewo binarne 9 1 2 7 4 8 7 5 3 9 4 2 8 6 5 3 9 2 6 3A 3B 7 4 8 7 9 3 5 8 2 4 6 9 3 5 2 6 4A 4B 7 8 6 9 9 3 5 8 2 6 4 7 3 4 9 8 6 7 3 5 2 4 9 8 7 6 3 5 2 4 Rys. 12: Działanie procedury przesiewania 10 5 2 Teraz zaczynając od ostatniego elementu, będącego poprzednikiem wprowadzamy porządek stogowy w drzewie (rys. 12). • Krok 1. Obliczamy indeks ostatniego elementu będącego poprzednikiem poprzez obliczenie (m = bN/2c, gdzie N jest rozmiarem tablicy) - jest to wartość 4, a zatem indeks wskazuje na element 8. Element ten ma jeden następnik, który jest mniejszy od niego, a zatem pierwsze „poddrzewo”, którego korzeniem jest element 4, spełnia własności stogu. • Krok 2. Przechodzimy do elementu równego 5 o indeksie 3. Ten element ma dwóch następników, większym z nich jest 9 - a zatem poddrzewo nie jest stogiem (gdyż potomek 9 jest większy od swojego poprzednika) Aby ustanowić porządek stogowy, zamieniamy miejscami element 5 z elementem 9. Ponieważ żaden z następników nowego korzenia poddrzewa (elementu 9) nie jest ma swoich następników, po dokonaniu zamiany całe poddrzewo jest stogiem. • Krok 3A . Element równy 4 o indeksie 2. Ten element ma dwóch potomków - większym z nich jest 8, więc poddrzewo również nie jest stogiem (element 8 jest większy od poprzednika, elementu 4). Dokonujemy zatem zamiany miejscami elementu 4 i 8. • Krok 3B . Teraz należy sprawdzić, czy całe poddrzewo jest już uporządkowane stogowo, sprawdzamy zatem, czy zamieniony element 4 jest większy od swoich następników (ma tylko jednego). Okazuje się, że nie - więc konieczna jest kolejna zamiana elementów (element 4 o nowym indeksie 4 z elementem 6 o indeksie 8). Dopiero po przeczesaniu całego poddrzewa można uznać, że spełnia ono cechy stogu. • Krok 4A . Porównujemy element równy 1 o indeksie 1 z większym z jego następników (elementy o indeksach 2 i 3) i dokonujemy niezbędnej zamiany (element 3 z elementem 1). • Krok 4B . Sprawdzamy poddrzewo, którego korzeniem jest zamieniony moment wcześniej element 2. Zamiana nie jest wymagana, drzewo spełnia warunek stogu. • Krok 5. Kończymy przesiewanie - całe drzewo jest uporządkowane stogowo. Otrzymujemy tablicę, przedstawioną w dolnej części rys. 12. Sam algorytm ma postać: Wykonuj co następuje od indeksu i = b N/2 c do i = 1: Wskaż na i-ty element tablicy; Wybierz większego z następników tego elementu (jeśli ma jednego następnika, wybierz go); Jeśli następnik jest większy od badanego elementu wykonuj Zamień miejscami badany element i-ty z jego większym potomkiem; Przypisz do i indeks elementu, który został przesiany w dół. 3.5.2 Sortowanie stogowe Sortowanie stogowe polega na wielokrotnym (n − 1-krotnym, gdzie n oznacza rozmiar tablicy do posortowania) wykonaniu następującej sekwencji poleceń (zakładamy, że w tablicy jest już zaprowadzony porządek stogowy): Wykonuj co następuje od indeksu i = N do i = 2: Zamień i-ty element drzewa z korzeniem tego drzewa (elementem nr 1); Przesiej drzewo, zawierające elementy od 1 do i − 1; 3.6 Sortowanie przez scalanie Działanie algorytmu sortowania przez scalanie można podzielić na dwie fazy: najpierw następuje podział tablicy na dwie równe części (lub prawie równe, gdy ilość elementów w tablicy jest nieparzysta), następnie każda z tych części jest nadal dzielona na pół itd. Proces podziału kończy się w momencie, gdy przetwarzana część tablicy zawiera tylko jeden element. Wtedy rozpoczyna się proces scalania poszczególnych fragmentów tablicy. Zasadę działania algorytmu sortowania przez scalanie przedstawia rys. 13. Uwaga! W algorytmie sortowania przez scalanie, zgodnie z definicją algorytmu sortowania wewnętrznego, nie wolno używać dodatkowej tablicy!. Zakładając, że sortowana tablica zawiera N elementów, algorytm sortowania można przedstawić następująco: 11 2 L=1 2 L=R=1 1 2 1 2 R=2 L=1 1 9 5 9 L=R=3 L=R=4 2 R=3 L=1 0 1 22 5 0 5 5 9 R=5 L=4 0 9 R=6 L=R=6 PODZIAŁ L=R=5 5 22 0 5 9 L=4 2 0 9 L=R=2 1 2 R=3 2 1 2 L=1 2 1 SCALANIE 5 9 R=6 Rys. 13: Sortowanie przez scalanie Wykonuj co następuje dopóki tablica ma więcej niż jeden element: Podziel tablicę na dwie (prawie) równe części; Wywołaj rekurencyjnie funkcję sortującą na lewej części tablicy; Wywołaj rekurencyjnie funkcję sortującą na prawej części tablicy; Dokonaj scalenia obu części tablicy. A zatem ciąg jest dzielony w każdym kroku na dwa niemal równoliczne podciągi, które rekurencyjnie porządkowane są tą sama metodą. Warunkiem zakończenia rekurencji w tym algorytmie jest sytuacja, kiedy ciąg ma tylko jeden element. Zatem powrót z wywołań rekurencyjnych rozpoczyna się z ciągami złożonymi z pojedynczych elementów, które są następnie scalane w ciągi o długości dwa, a następnie w ciągi o długości trzy lub cztery elementy itd. Sam proces sortowania wymaga pamiętania lewej i prawej granicy „podtablic”, na które ciąg jest dzielony. Należy pamiętać, że podział na „podtablice” jest umowny, a wszystkie dane cały czas znajdują się w jednej tablicy, więc granice oznaczają tylko położenie „podtablic” w tablicy danych. Granice wszystkich „podtablic” zostały oznaczone na rys. 13, np w drugiej linii są dwie „podtablice”: 212 oraz 950, ich granicami są odpowiednio 1 i 3 oraz 4 i 6. Po zakończeniu rekurencyjnego dzielenia tablic, rozpoczyna się proces scalania. Analizując ten proces łatwo zauważyć, że zarówno lewa, jak i prawa część scalanej tablicy są zawsze posortowane prawidłowo, wystarczy więc elementy z prawej części tablicy „wkomponować” w część lewą. Proces ten przypomina nieco sortowanie przez proste wstawianie, jednakże w tej sytuacji nie ma wartownika, należy więc kontrolować, czy podczas zamiany elementów indeksy cały czas wskazują na elementy tablicy. Fragment procesu scalania dwóch „podtablic” przedstawia rys. 14. Elementy drugiego ciągu są wyróżnione kolorem ciemnoniebieskim. Rysunek prezentuje wstawienie na właściwe miejsce tylko pierwszej cyfry drugiego ciągu (0), z kolejnymi cyframi drugiego ciągu należy postępować analogicznie. Ciąg 1 2 2 1 2 2 0 5 9 5 9 0 1 1 2 2 5 9 2 2 5 9 1 2 2 5 9 1 2 2 5 9 Bufor 2 1 3 4 Wynik 0 5 Rys. 14: Proces scalania dwóch ciągów - dla pierwszego elementu drugiego ciągu Algorytm scalania dwóch podtablic (l i r oznaczają odpowiednio lewy i prawy koniec tablicy) można zapisać następująco: Wykonuj co następuje od indeksu i = d(l + r)/2e do końca tablicy 12 Skopiuj i − ty element tablicy do bufora tymczasowego; Ustaw indeks j na elemencie i − 1; Dopóki j-ty element tablicy jest większy od bufora tymczasowego wykonuj: Przenieś element j-ty na miejsce j + 1; Zmniejsz j o 1; Wstaw zawartość bufora tymczasowego w j + 1 miejsce tablicy. 4 Zadania do wykonania 1. Podać wynik działania każdego algorytmu dla ciągu PRZYKLADSORTOWANIA. 2. Które z poznanych algorytmów sortowania są stabilne? Literatura [1] L.Banachowski i in. Algorytmy i struktury danych; WNT, 1996. [2] T.H.Cormen i in. Wprowadzenie do algorytmów, WNT, 2000 [3] A.Drozdek Struktury danych w jezyku C, WNT, 1996 [4] D.E.Knuth Sztuka programowania, WNT, 2002 [5] R.Sedgewick Algorytmy w C++, Wydawnictwo RM, 1999 [6] P.Wróblewski, Algorytmy, struktury danych i techniki programowania, HELION, 1996 [7] N. Wirth, Algorytmy + struktury danych = programy, WNT, 2001 13