Sortowanie tablic jednowymiarowych
Transkrypt
Sortowanie tablic jednowymiarowych
Podstawy Programowania Wykład siódmy: Sortowanie tablic. 1. Sortowanie Proces porządkowania danych nazywa się w programowaniu sortowaniem. Porządkowanie może oznaczać uszeregowanie danych różnowartościowych w porządku rosnącym lub malejącym. Jeśli dane powtarzają się to można je uszeregować niemalejąco lub nierosnąco. Sortować można zarówno dane numeryczne jak i nienumeryczne, np.: można porządkować litery według ich kolejności występowania w alfabecie. Sposób sortowania zależy między innymi od struktury danych, w której zapisane zostały informacje podlegające porządkowaniu. Ten wykład będzie dotyczył sortowania tablic jednowymiarowych. Zapoznamy się z czterema algorytmami realizującymi tą czynność. W ostatniej części wykładu zostaną opisane operacje znajdowania maksimum i minimum, które ulegają znacznemu uproszczeniu jeśli tablica jest posortowana. Zostanie także zaprezentowany algorytm wyszukiwania1 dla tablic posortowanych, który jest wydajniejszy od tych, które są stosowane dla tablic nieposortowanych. 2. Algorytmy sortowania tablic Wszystkie opisane tu algorytmy mają pewne cechy wspólne. Po pierwsze są one algorytmami sortowania wewnętrznego, tzn. żadne dane nie są zapisywane na dysku podczas wykonywania operacji porządkowania tablicy. Dodatkowo sortownie wykonywane przez te algorytmy jest sortowaniem stabilnym – jeśli w tablicy znajdą się dwie jednakowe wartości, to ich początkowa kolejność względem siebie nie ulega zmianie po wykonaniu sortowania. Ta cecha może mieć znaczenie w niektórych zastosowaniach. Ostatnią cechą wspólną tych algorytmów, jest to że wykonują sortowanie w miejscu, tzn. niezależnie od tego ile elementów będzie miała sortowana tablica, to będą one potrzebowały stałej ilości pamięci2 do wykonania tej operacji. Wystarcza im tylko pamięć przydzielona na porządkowaną tablicę i kilka zmiennych. 3. Sortowanie przez wybór (ang. selection sort) Algorytm sortowania przez wybór jest „spokrewniony” z algorytmami wyszukiwania maksimum i minimum w nieposortowanej tablicy. Załóżmy, że tablica, zawiera 15 elementów typu byte i że wartości tych elementów są nieposortowane, a my je chcemy uporządkować niemalejąco (rosnąco). Zauważmy, że w tablicy posortowanej, w pierwszym elemencie musi zawsze znajdować się wartość najmniejsza. Należy więc znaleźć element o wartości minimalnej 1 Operacje wyszukiwani i sortowania, według D.E.Knutha są najczęściej wykonywanymi operacjami przez współczesne komputery. 2 Algorytmy posiadające tę cechę często nazywa się algorytmami in situ. 2 spośród pozostałych czternastu elementów tablicy. Jeśli wartość tego elementu będzie mniejsza od wartości pierwszego elementu, to należy te wartości zamienić miejscami. Tę samą czynność wykonujemy dla drugiego elementu, ale wykluczamy z naszych poszukiwań pierwszy element (jego wartość jest już na swoim miejscu) i poszukujemy minimum wśród trzynastu pozostałych elementów. Opisane czynność powtarzamy, do momentu, kiedy wartości wszystkich elementów tablicy będą uporządkowane. Oto kod źródłowy procedury sortowania przez wybór, wraz z ilustracją obrazującą sposób jej działania: procedure selection_sort(var t:tablica); var i,j,k,tmp:byte; begin for i:=low(t) to high(t)-1 do begin k:=i; for j:=i+1 to high(t) do if t[k] > t[j] then k:=j; if i<>k then begin tmp:=t[k]; t[k]:=t[i]; t[i]:=tmp; end; end; end; 5 6 4 8 7 9 1 1 6 4 8 7 9 5 1 4 6 8 7 9 5 Kolorem czerwonym oznaczono elementy, których wartości nie zostały jeszcze uporządkowane, kolor zielony oznacza elementy należące do posortowanego ciągu, czyli krótko – posortowane. Tablica jest przekazywana do procedury przez zmienną. Chcemy bowiem, aby wszystkie zmiany dokonane przez procedurę w tablicy były zachowane po jej wykonaniu. Zmienną lokalna tmp będziemy posługiwać się podczas zamiany wartości dwóch elementów tablicy. Zmienna k służy do zapamiętania indeksu elementu o najmniejszej wartości. W procedurze znajdują się dwie pętle for. Licznikiem zewnętrznej pętli jest zmienna i. Wyznacza ona kolejne elementy tablicy, których wartości będziemy porządkować. Licznikiem wewnętrznej pętli for jest zmienna j. Jest ona inicjalizowana wartością o jeden większą od bieżącej wartości zmiennej i. W wewnętrznej pętli for wyszukujemy spośród wszystkich elementów tablicy znajdujących się za elementem wskazywanym przez i, ten element który ma najmniejszą wartość. Jego indeks zapamiętujemy w zmiennej k. Po wykonaniu pętli wewnętrznej sprawdzamy, czy wartość zmiennej k jest różna od wartości zmiennej i, czyli czy nie wskazują one na ten sam element. Jeśli 3 tak, to wartość elementu wskazywanego przez i zamieniamy z wartością elementu wskazywanego przez k. Wymiana ta przebiega w trzech etapach. Najpierw zapamiętujemy wartość elementu wskazywanego przez k w zmiennej tmp, potem przypisujemy temu elementowi wartość elementu tablicy wskazywanego przez i, a na końcu elementowi wskazywanemu przez i przypisujemy wartość zmiennej tmp. Procedura kończy się z chwilą zakończenia pętli zewnętrznej. Algorytm sortowania przez wybór może być także użyty do posortowania tablicy w porządku malejącym (nierosnącym). Wystarczy zmienić na przeciwny znak w warunku, znajdującym się w instrukcji if, wykonywanej w wewnętrznej pętli for. 4. Sortowanie przez wstawianie (ang. insertion sort) Algorytm sortowania przez wstawianie jest innym rozwiązaniem rozważanego problemu. W pierwszym kroku tego algorytmu sprawdzamy wartości pierwszej pary elementów tablicy. Najpierw zapamiętujemy wartość elementu drugiego w osobnej zmiennej, następnie sprawdzamy, czy jest ona mniejsza od wartości elementu pierwszego. Jeśli tak, to wartość elementu pierwszego jest przesuwana o jedno miejsce w prawo, do elementu drugiego, a w elemencie pierwszym zapisujemy wartość zapamiętaną w osobnej zmiennej. Dalej zapamiętujemy w osobnej zmiennej wartość trzeciego elementu. Podobnie jak poprzednio porównujemy ją z wartością drugiego elementu. Jeśli ta ostatnia jest większa, to przesuwamy ją o jedno miejsce w prawo (do trzeciego elementu). Następnie porównujemy wartość zapisaną w osobnej zmiennej z wartością pierwszego elementu tablicy. Jeśli i ona jest większa, to też przesuwamy ją o jedno miejsce w prawo, a wartość ze zmiennej zapisujemy w pierwszym elemencie. Oczywiście podczas tego przeszukiwania może się okazać, że jakiś element będzie miał wartość mniejszą od wartości zapamiętanej w osobnej zmiennej. Wówczas trzeba wstawić wartość z tej zmiennej po prawej stronie tego elementu. Nie trzeba też sprawdzać elementów znajdujących się po jego lewej stronie, na pewno będą miały wartości jeszcze mniejsze. Wykonanie algorytmu kończymy, gdy wszystkie elementy tablicy zostaną uporządkowane. To sortowanie można również opisać krócej: szukamy elementu po prawej stronie tablicy o wartości mniejszej lub równej elementowi, który bieżąco rozważamy. Wartości wszystkich elementów, które nie spełniają tego warunku przesuwamy w prawo i wstawiamy bieżącą wartość bezpośrednio za znalezionym elementem. Czynności te powtarzamy, aż tablica zostanie posortowana. W życiu codziennym ten algorytm jest wykonywany przez osoby grające w różne gry karciane. Zazwyczaj pule kart takich osób są uporządkowane według koloru i figury. Porządkowanie to jest wykonywane właśnie według algorytmu sortowania przez wstawianie. Gracz trzyma uporządkowane karty w jednej ręce. Po dobraniu nowej karty, 4 przesuwa pozostałe, tak aby znaleźć odpowiednie miejsce dla nowej karty. Można zauważyć, że algorytm sortowania przez wstawianie jest w pewnym stopniu przeciwieństwem algorytmu sortowania przez wybór. W tym ostatnim mamy miejsce w tablicy (element) i szukamy dla niego wartości, natomiast w algorytmie sortowania przez wstawianie wybieramy wartość i szukamy dla niej miejsca. Oto kod źródłowy procedury realizującej sortowanie przez wstawianie wraz z ilustracją obrazującą jej działanie (kolory oznaczają to samo co poprzednio): procedure insertion_sort(var t:tablica); var i,j,key:byte; begin for j:=low(t)+1 to high(t) do begin key:=t[j]; i:=j-1; while (i>low(t)) and (t[i]>key) do begin t[i+1]:=t[i]; i:=i-1; end; t[i+1]:=key; end; end; 5 6 4 8 7 9 1 4 5 6 8 7 91 4 5 6 7 8 9 1 1 4 5 6 7 8 9 W procedurze znajdują się dwie pętle. Licznik zewnętrznej pętli for wyznacza kolejne wartości elementów, które procedura będzie porządkowała. Będą one zapamiętywane w zmiennej key. W pętli while rozpatrujemy wszystkie elementy znajdujące się przed elementem wskazywanym przez zmienną j (ten element zawiera porządkowaną wartość). Kolejne z tych elementów będą wewnątrz opisywanej pętli wskazywane przez zmienną i. Jeśli wartość elementu określanego przez tę zmienną jest większa od wartości zapamiętanej w zmiennej key, to przesuwamy ją o jedno miejsce w prawo (kopiujemy do najbliższego elementu po prawej stronie). Pętla while może się zakończyć na dwa sposoby: albo znajdziemy element o wartości mniejszej od tej która jest zapamiętana w zmiennej key, albo „wyjdziemy” poza pierwszy element tablicy (np. zmienna i będzie miała wartość 0, a indeks pierwszego elementu tablicy ma wartość 1). W pierwszym przypadku wartość ze zmiennej key powinna zostać zapisana do elementu znajdującego się bezpośrednio po prawej stronie znalezionego elementu. W drugim przypadku wartość ta powinna zostać zapisana 5 w pierwszym elemencie. Obie te czynności dają się zapisać w postaci instrukcji t[i+1]:=key; która znajduje się bezpośrednio za pętlą while. Procedura sortowania przez wstawianie może również posortować tablicę malejąco (nierosnąco). Wystarczy zmienić znak w warunku t[i]>key na przeciwny. 5. Sortowanie bąbelkowe3 (ang. bubble sort) W większości przypadków edukację początkujących programistów w zakresie sortowania zaczyna się od omówienia właśnie tego algorytmu. Jest to chyba najprostszy do zapisania w postaci programu algorytm. Idea jego działania jest trochę odmienna od wcześniej tu przedstawionych. W tym algorytmie przeglądamy tablicę kilka razy. Za pierwszym razem przeglądamy elementy tablicy zaczynając od ostatniego i przesuwając się do przodu, aż napotkamy drugi element. Podczas tego przeglądania sprawdzamy parami wartości sąsiadujących elementów. Jeśli wartość elementu poprzedzającego jest mniejsza od wartości elementu bieżącego, to zamieniamy je miejscami. Jeśli nie, to przesuwamy się o jeden element do przodu i powtarzamy porównanie elementów. Ponowne przeglądanie elementów tablicy zaczynamy też od jej ostatniego elementu, ale kończymy na trzecim elemencie. Algorytm kończy się w chwili, kiedy uporządkujemy ostatnią parę elementów. Oto kod źródłowy procedur realizującej sortowanie bąbelkowe4 (z ilustracją działania obok): procedure bubble_sort(var t:tablica); var i,j,tmp:byte; begin for i:=low(t) to high(t)-1 do for j:=high(t) downto i+1 do if t[j-1] > t[j] then begin tmp:=t[j]; t[j]:=t[j-1]; t[j-1]:=tmp; end; end; 5 6 4 8 7 9 1 1 5 6 4 8 7 9 1 5 6 4 7 8 9 Zewnętrzna pętla for „ogranicza” zakres działania wewnętrznej pętli for, która porównuje wartości sąsiednich elementów, poczynając od ostatniego w tablicy. Jeśli te wartości nie znajdują się we właściwym porządku, to zostają za3 Algorytm ten jest nazywany również algorytmem sortowania przez zamianę. 4 Według N.Wirtha ta nazwa tego algorytmu pochodzi od pewnego skojarzenia, otóż gdybyśmy narysowali tablicę pionowo i obserwowali działanie tego algorytmu na wartościach tej tablicy, to mniejsze wartości „unosiłyby się” w górę tablicy „jak bąbelki powietrza w cieczy”. 6 mienione miejscami. Wykonanie kończy się wraz z uporządkowaniem ostatniej pary elementów w tablicy. Również za pomocą tego algorytmu możemy uporządkować wartości elementów malejąco (nierosnąco) zmieniając znak w warunku instrukcji if. 6. Sortowanie mieszane5 (ang. shake sort) Algorytm sortowania mieszanego jest pewną modyfikacją algorytmu sortowania bąbelkowego. W tym algorytmie zapamiętujemy indeks ostatniego elementu, którego wartość uległa wymianie podczas bieżącego przeglądania tablicy. Dodatkowo tablicę przeglądamy dwukierunkowo, tzn. naprzemiennie przeglądamy tablicę raz od końca, raz od początku. Oto kod procedury sortującej według tego algorytmu (z ilustracją działania obok): procedure shake_sort(var t:tablica); var j,k,u,d,key:byte; begin d:=low(t)+1; u:=high(t); repeat for j:=u downto d do if t[j-1] > t[j] then begin key:=t[j-1]; t[j-1]:=t[j]; t[j]:=key; k:=j; end; d:=k+1; for j:=d to u do if t[j-1]>t[j] then begin key:=t[j-1]; t[j-1]:=t[j]; t[j]:=key; k:=j; end; u:=k-1; until d>u; end; 5 6 4 8 7 9 1 1 5 6 4 8 7 9 1 5 4 6 8 7 9 W zmiennej k zapamiętywany jest indeks elementu tablicy, którego wartość jako ostatnia uległa zmianie przy przeglądaniu tablicy „w górę” lub „w dół”. 5 Nazywane również dwukierunkowym sortowaniem bąbelkowym (ang. bidirectional bubble sort). 7 W zmiennych d i u zapamiętujemy indeksy brzegowych elementów obszaru nieuporządkowanego tablicy (taki obszar w przypadku sortowania mieszanego znajduje się „w środku” tablicy, a „brzegi” są uporządkowane). W pętli repeat wykonujemy dwie pętle for. Pierwsza pętla przegląda elementy tablicy od elementu o indeksie u, w kierunku początku tablicy, dokonując wymian. Po jej zakończeniu w zmiennej d zapisany jest indeks pierwszego elementu, począwszy od początku tablicy, który jeszcze nie jest uporządkowany. Druga pętla for rozpoczyna przegląd tablicy w przeciwnym kierunku, zaczynając od elementu, którego indeks jest zapisany w zmiennej d, a kończąc na elemencie, którego indeks jest zapisany w zmiennej u. Jeśli przeanalizujemy uważnie działanie tego algorytmu, to odkryjemy, że z każdą iteracją pętli repeat zakres elementów przeglądanych przez obie pętle for jest zawężany w kierunku środka tablicy. Wykonanie procedury kończy się, kiedy wartość zmiennej u będzie mniejsza lub równa wartości zmiennej d. Podobnie jak poprzednio, jeśli chcemy, aby procedura sortowała malejąco (nierosnąco) wystarczy zmienić znaki warunków w obu instrukcjach warunkowych. 7. Wydajność algorytmów sortowania Możemy stwierdzić, że szybkość działania tych algorytmów będzie uzależniona głównie od liczby przeprowadzanych wymian wartości elementów6. Im jest ich mniej, tym algorytm jest szybszy. Z prezentowanych algorytmów najlepiej pod tym względem wypada algorytm sortowania przez wybór. Drugie miejsce przypada algorytmowi sortowania przez wstawianie. Wydajność algorytmów sortowania mieszanego i bąbelkowego jest porównywalna, choć minimalnie lepszy jest ten pierwszy. 8. Znajdowanie wartości minimalnej i maksymalnej w tablicy posortowanej Algorytmy wyszukiwania wartości maksymalnej i minimalnej w tablicy posortowanej niemalejąco stają się trywialne. Aby znaleźć wartość minimalną wystarczy sięgnąć do pierwszego elementu tablicy, natomiast wartość maksymalną znajdziemy w ostatnim elemencie tablicy. 9. Wyszukiwanie binarne (ang. binary search) Dla tablic posortowanych istnieje bardzo szybki algorytm znajdowania szukanej wartości, nazywany algorytmem wyszukiwania binarnego. Szukanie danej wartości rozpoczynamy od wyznaczenia środkowego elementu tablicy. Jeśli jego 6 Dosyć często trzy instrukcje dokonujące wymiany są zapisywane przez programistów w postaci osobnego podprogramu. 8 wartość jest wartością szukaną, to kończymy jego wykonanie. Jeśli nie, to sprawdzamy, czy szukana wartość jest mniejsza, czy większa od jego wartości. Jeśli mniejsza, to będziemy dalej rozpatrywać część tablicy leżącą na lewo od niego, jeśli większa, to na prawo od niego. Po ustaleniu, którą część tablicy będziemy rozpatrywać, wykonujemy dla niej wyżej opisane czynności. Algorytm kończy się kiedy znajdziemy szukaną wartość lub kiedy nie będziemy mogli dalej „dzielić” tablicy. Wystąpienie ostatniego przypadku oznacza, że nie ma w niej szukanej wartości. Mimo, że opis słowny tego algorytmu po raz pierwszy ukazał się w roku 1946, to jego poprawna wersja ukazała się dopiero w roku 1962. Okazuje się, że pisząc program lub podprogram, którego działanie jest oparte o ten algorytm należy uwzględnić kilka szczegółów, które nie są wprost zawarte w opisie słownym. Oto procedura, która realizuje wyszukiwanie według tego algorytmu: function binary_search(const t:tablica; value:byte):byte; {Zakładamy, że tablica jest posortowana niemalejąco. Jeśli element znajduje się w tablicy, to funkcja zwróci indeks miejsca, gdzie ona występuje, jeśli nie, to zwróci zero.} var g,d,s:byte; begin binary_search:=0; d:=low(tablica); g:=high(tablica); while d<=g do begin s:=(d+g) div 2; if t[s]<value then d:=s+1; if t[s]=value then begin binary_search:=s; break; end; if t[s]>value then g:=s-1; end; end; Zmienne g i d służą do zapamiętania odpowiednio indeksów górnego i dolnego elementu rozpatrywanej części tablicy (początkowo brana jest pod uwagę cała tablica). Zmienna s służy do zapamiętania indeksu elementu środkowego w danej części tablicy. Pętla while kończy się, kiedy wartość indeksu górnego jest mniejsza od wartości indeksu dolnego (nie znaleziono wartości). Po uważnej analizie kodu dowiemy się, że nie jest to jedyny sposób zakończenia tej pętli. 9 Wewnątrz pętli wyznaczany jest indeks środkowego elementu rozpatrywanej części tablicy. Następnie badana jest wartość tego elementu. Jeśli jest to wartość, którą szukamy, to zwracamy indeks tego elementu i kończymy wykonanie pętli. Jeśli ta wartość jest mniejsza od szukanej to modyfikujemy wartość indeksu elementu dolnego rozpatrywanej części. Nie wolno temu indeksowi przypisać wartości indeksu elementu środkowego, bo ten element już odrzuciliśmy, a więc musimy przypisać mu wartość o jeden większą. Jeśli wartość elementu środkowego jest większa od szukanej to modyfikujemy indeks wskazujący element górny badanej części tablicy. Z takiego samego powodu jak poprzednio przypisujemy mu wartość o jeden mniejszą od wartości indeksu elementu środkowego. Jeśli pętla nie zostanie przerwana, tylko sama się zakończy będzie to oznaczało, że szukana wartość nie została znaleziona w tablicy. Oto ilustracja działania tego algorytmu. Kolor szary oznacza odrzuconą część tablicy: 1 4 5 6 7 8 9 1 4 5 6 7 8 9 szukana: 4 znaleziono ! 10