Sortowanie szybkie Sortowanie szybkie (quick sort) jest obecnie
Transkrypt
Sortowanie szybkie Sortowanie szybkie (quick sort) jest obecnie
Antoni M. Zajączkowski: Algorytmy i podstawy programowania – sortowanie szybkie 20 maja 2015 SORTOWANIE SZYBKIE Sortowanie szybkie (quick sort) jest obecnie uważane za najefektywniejszy algorytm sortowania wewnętrznego (Aho, Hopcroft, Ullman, 2003). Algorytm ten został opracowany przez Hoare’a (1962), który zastosował zasadę „dziel dziel i zwyciężaj” zwyciężaj (divide and conquer). W przypadku sortowania pewnej niepustej tablicy jednowymiarowej T wybierany jest najpierw jeden jej element, nazywany elementem dzielącym (pivot), a pozostałe elementy grupowane są w dwóch podtablicach tak, że w jednej umieszczane są elementy o kluczach nie większych od klucza elementu dzielącego, a w drugiej umieszczane są elementy o kluczach większych lub równych kluczowi elementu dzielącego. Następnie, każda z podtablic jest sortowana przez rekurencyjne wywołania opisanego algorytmu. Ponieważ zasadniczym krokiem w tym algorytmie sortowania jest podział sortowanej tablicy, Wirth (2001) nazywa sortowanie szybkie, sortowaniem przez podział. podział 1. Opis algorytmu Niech T(L .. R) oznacza tablicę jednowymiarową, której pierwszy i ostatni indeks oznaczamy odpowiednio przez L i R . Tablicę tę należy posortować w porządku rosnącym wg kluczy jej elementów. Jak wspomnieliśmy, najważniejszą częścią algorytmu sortowania szybkiego jest procedura realizująca podział tablicy wejściowej, przy czym podział ten musi spełniać warunki (Sedgewick, 1983): 1. Element T(P) zostaje umieszczony w końcowym położeniu – nie będzie już przesuwany, przy czym P ∈ L .. R oznacza indeks tego elementu, nazywanego elementem dzielącym. 2. Wszystkie elementy podtablicy T(L ..P − 1) mają klucze mniejsze, lub równe kluczowi elementu T(P). 3. Wszystkie elementy podtablicy T(P + 1.. R) mają klucze większe, lub równe kluczowi elementu T(P). Po wykonaniu takiego podziału sortowane są podtablice T(L ..P − 1) i T(P + 1.. R) przez rekurencyjne wywołanie algorytmu. Prototyp algorytmu sortowania szybkiego możemy zapisać w postaci (Sedgewick, 1983): procedure Recursive_Quick_Sort (List : in out List_Type; Left_End : in Index_Type; Right_End : in Index_Type) is Pivot_Index : Index_Type; begin if Right_End > Left_End then Partition(List, Left_End, Right_End_End, Pivot_Index); Recursive_Quick_Sort(List, Left_End, Pivot_Index - 1); Recursive_Quick_Sort(List, Pivot_Index + 1, Right_End_End); end if; end Recursive_Quick_Sort; przy czym procedura Partition dokonuje podziału tablicy wejściowej i wyznacza indeks elementu dzielącego w jego docelowym położeniu. Zwróćmy uwagę na to, że tablica jest sortowana w miejscu i dlatego parametr List jest rodzaju in out. out 1 Antoni M. Zajączkowski: Algorytmy i podstawy programowania – sortowanie szybkie 20 maja 2015 2. Działanie algorytmu Działanie algorytmu wyjaśnimy sortując tablicę, której kluczami elementów są wielkie litery, a indeksami są liczby całkowite, przy czym wprowadzimy odpowiednie deklaracje i omówimy wybrane instrukcje tak, że później łatwo będzie napisać program w Adzie implementujący algorytm sortowania szybkiego. Zgodnie z tym, wprowadzamy deklaracje subtype Key_Type is Character range 'A'..'Z'; type Element is record Key : Key_Type; -- Data : Data_Type; end record; type List_Type is array (Integer range <>) of Element; Nagłówek procedury Recursive_Quick_Sort przyjmuje postać procedure Recursive_Quick_Sort (List : in out List_Type; Left_End : in Integer; Right_End : in Integer); Weźmy pod uwagę tablicę, której elementy tworzą początkowo następujący ciąg (Sedgewick, 1983): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A S O R T I N G E X A M P L E Zaczynając podział musimy wybrać pewien element dzielący. W naszej implementacji przyjmiemy, że jest nim ostatni (w niebieskiej ramce) element tablicy, który jest zmienną lokalną Pivot : Element; Znając początkowe położenie elementu dzielącego Pivot := List(Right_End); -- List(Right_End) = 'E' przeglądamy tablicę w prawo, zaczynając od pierwszego elementu, aż do momentu, gdy znajdziemy element o kluczu większym lub równym kluczowi elementu dzielącego. W tym celu stosujemy zmienną lokalną Left_Index : Integer := Left_End – 1; -- Left_Index = 0 Następnie, zaczynając od ostatniego elementu, przeglądamy tablicę w lewo, aż znajdziemy element o kluczu mniejszym lub równym kluczowi elementu dzielącego. Stosujemy tu zmienną lokalną Right_Index : Integer := Right_End; -- Right_Index = 15 Przeglądanie (skanowanie) tablicy możemy zorganizować następująco: loop -- scan from the left end Left_Index := Left_Index + 1; exit when List (Left_Index).Key >= Pivot.Key; end loop; loop -- scan from the right end Right_Index := Right_Index - 1; exit when List (Right_Index).Key <= Pivot.Key; end loop; W naszym przykładzie wykonanie tych pętli daje 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A S O R T I N G E X A M P L E przy czym klucze elementów znalezionych przy przeglądaniu w prawo i lewo zaznaczono odpowiednio czerwoną i zieloną ramką. 2 Antoni M. Zajączkowski: Algorytmy i podstawy programowania – sortowanie szybkie 20 maja 2015 Pętle zatrzymują się, gdy odpowiednio Left_Index = 2 i Right_Index = 11. Łatwo zauważyć, że znalezione elementy nie są we właściwych położeniach względem siebie, a więc należy zamienić ich pozycje. Temp := List (Left_Index); -- Temp = List(2) List (Left_Index) := List (Right_Index); List (Right_Index) := Temp; Po tej zamianie dostajemy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A A O R T I N G E X S M P L E Zaczynając od wyznaczonych w poprzednio indeksów, ponownie przeglądamy tablicę 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A A O R T I N G E X S M P L E i zamieniamy znalezione elementy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A A E R T I N G O X S M P L E -- Temp = List(3) Kolejne przeglądanie zatrzymuje się, gdy Left_Index = 4 i Right_Index = 3 1 2 3 4 A A E R 5 T 6 I 7 N 8 G 9 10 11 12 13 14 15 O X S M P L E W tym przypadku indeksy lewy i prawy spełniają warunek Right_Index <= Left_Index, który jest warunkiem zakończenia pętli zewnętrznej sterującej procesem dwustronnego przeglądania tablicy. Pętla ta może mieć postać loop -- Dwustronne_Skanowanie; exit when Right_Index <= Left_Index end loop; Po wyjściu z pętli zewnętrznej nasza tablica ma postać 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A A R E T I N G O X S M P L E Teraz należy już tylko zamienić element przechowywany w lokalnej zmiennej Temp z elementem dzielącym List (Right_Index) := List (Left_Index); List (Left_Index) := List (Right_Index); List (Right_End) := Temp; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A A E E T I N G O X S M P L R Zauważmy, że po zakończeniu opisanego podziału, element dzielący znalazł się w docelowym położeniu, a podtablice List(1..3) i List(5..15) mogą być sortowane niezależnie przez rekurencyjne wywołania procedury Recursive_Quick_Sort: Recursive_Quick_Sort (List, Left_End, Left_Index-1); Recursive_Quick_Sort (List, Left_Index + 1, Right_End); Zadanie obliczeniowe. obliczeniowe Napisać, uruchomić i przetestować program w Adzie implementujący algorytm sortowania szybkiego. Algorytm powinna realizować procedura z odpowiednio zaprojektowanym interfejsem (parametrami formalnymi). Procedura ta powinna wyznaczać liczbę przesunięć (zamian) elementów oraz prezentować stan sortowanej ta- 3 Antoni M. Zajączkowski: Algorytmy i podstawy programowania – sortowanie szybkie 20 maja 2015 blicy po wykonaniu każdego etapu algorytmu. Wykonać też obliczenia dla przypadku posortowanej tablicy i tablicy posortowanej wg porządku malejącego. Program. Program. Test_Recursive_Quick_Sort LITERATURA Aho, A.V, J.E. Hopcroft, J.D. Ullman. (2003). Algorytmy i struktury danych. Helion, Gliwice, Polska (tłum. z ang.). Hoare, C.A.R. (1962). Quicksort. Computer Journal. 5, 10 - 15. Sedgewick, R. (1983). Algorithms. Addison-Wesley, Reading, Mass. Wirth, N. (2001). Algorytmy + struktury danych = programy. WNT, Warszawa (tłum. z ang.). 4