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