Professional Documents
Culture Documents
PRZYKADOWY ROZDZIA
SPIS TRECI
KATALOG KSIEK
KATALOG ONLINE
Algorytmy. Od podstaw
Autorzy: Simon Harris, James Ross
Tumaczenie: Andrzej Grayski
ISBN: 83-246-0372-7
Tytu oryginau: Beginning Algorithms
Format: B5, stron: 610
TWJ KOSZYK
DODAJ DO KOSZYKA
CENNIK I INFORMACJE
ZAMW INFORMACJE
O NOWOCIACH
ZAMW CENNIK
CZYTELNIA
FRAGMENTY KSIEK ONLINE
Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: helion@helion.pl
O autorach ................................................................................................................................................... 9
Podzikowania ............................................................................................................................................11
Wprowadzenie ...........................................................................................................................................13
Rozdzia 1. Zaczynamy .............................................................................................................................. 23
Czym s algorytmy? ..................................................................................................... 23
Co to jest zoono algorytmu? .................................................................................... 26
Porwnywanie zoonoci i notacja duego O ............................................................... 27
Zoono staa O(1) ........................................................................................... 29
Zoono liniowa O(N) ........................................................................................ 29
Zoono kwadratowa O(N2) ............................................................................... 30
Zoono logarytmiczna O(log N) i O(N log N) ....................................................... 31
Zoono rzdu silni O(N!) .................................................................................. 32
Testowanie moduw .................................................................................................... 32
Czym jest testowanie moduw? .............................................................................. 33
Dlaczego testowanie moduw jest wane? ............................................................... 35
Biblioteka JUnit i jej wykorzystywanie ........................................................................ 35
Programowanie sterowane testami ........................................................................... 38
Podsumowanie ............................................................................................................ 39
Algorytmy. Od podstaw
Rozdzia 3. Listy .........................................................................................................................................71
Czym s listy? ............................................................................................................. 71
Testowanie list ............................................................................................................ 74
Implementowanie list ................................................................................................... 86
Lista tablicowa ....................................................................................................... 87
Lista wizana ......................................................................................................... 95
Podsumowanie .......................................................................................................... 104
wiczenia .................................................................................................................. 104
Spis treci
Algorytmy. Od podstaw
Rozdzia 11. Haszowanie ..........................................................................................................................305
Podstawy haszowania ................................................................................................. 305
Praktyczna realizacja haszowania ................................................................................ 311
Prbkowanie liniowe ............................................................................................. 314
Porcjowanie .......................................................................................................... 321
Ocena efektywnoci tablic haszowanych ...................................................................... 326
Podsumowanie .......................................................................................................... 332
wiczenia .................................................................................................................. 332
Spis treci
Opisywane w poprzednich rozdziaach struktury danych peni fundamentaln rol w tworzonych aplikacjach jako rodki organizujce przetwarzanie ogromnych iloci danych. W szczeglnoci sortowanie danych wedug pewnego kryterium stanowi nieodczny element wielu
algorytmw, w tym algorytmw opisywanych w dalszych rozdziaach niniejszej ksiki.
Jednoczenie staje si ono czsto wskim gardem wydajnoci aplikacji, nic wic dziwnego,
e sortowanie danych rozmaitych typw byo przedmiotem intensywnych bada w ostatnich dziesicioleciach i nadal stanowi jeden z kluczowych punktw zainteresowa informatyki. W niniejszym rozdziale omawiamy trzy proste algorytmy sortowania, atwe w implementacji, lecz przydatne raczej dla niewielkich iloci danych, a to ze wzgldu na zoono
kwadratow O(N2) (N jest liczb elementw w sortowanym zestawie). Bardziej wydajnym
i jednoczenie bardziej skomplikowanym algorytmom sortowania powicony bdzie
rozdzia 7.
W niniejszym rozdziale omawiamy:
n
Przegldajc na co dzie ksik telefoniczn, spis teleadresowy itp., najczciej nie uwiadamiamy sobie, i wykorzystujemy fakt ich posortowania. Szukajc okrelonego nazwiska
czy firmy, po prostu prbujemy zgadn, w ktrym miejscu spisu moemy si go spodziewa,
152
Algorytmy. Od podstaw
i ju po kilku takich prbach trafiamy na dan stron, na ktrej w cigu kilku sekund odnajdujemy to, czego szukamy (bd stwierdzamy, e taki to a taki abonent w spisie teleadresowym nie figuruje). Wyobramy sobie teraz, e taki spis teleadresowy nie jest posortowany abonenci wystpuj w nim w kolejnoci przypadkowej1. Trzeba mie duo
dobrej woli i determinacji, by w ogle podj si prby znalezienia w nim czegokolwiek
lub kogokolwiek prby raczej z gry skazanej na niepowodzenie. Wiele zbiorw danych
byoby zupenie bezuytecznych, gdyby nie zostay posortowane wedug pewnego uytecznego kryterium dotyczy to nie tylko nazwisk czy nazw w spisie teleadresowym, lecz take
np. ksiek na pkach bibliotecznych. Jako e czsto zbiory te posortowane s a priori,
uwaamy to za co naturalnego i w ogle o sortowaniu nie mylimy. W przypadku komputerowego przetwarzania danych jest zupenie inaczej: trudno oczekiwa, e uytkownik
aplikacji dostarcza bdzie dane w kolejnoci posortowanej, a w kadym razie byoby czym
kuriozalnym wymaga od niego czegokolwiek, co znacznie efektywniej moe za niego wykona komputer. Sortowanie rozmaitych danych staje si wic nieodczn czynnoci
wielu aplikacji, a dobra znajomo rnych metod sortowania jest warunkiem wykonywania tej czynnoci w sposb efektywny.
Zalety i wady danego algorytmu sortowania wynikaj przede wszystkim z tego, ile wymienionych wyej operacji naley wykona w celu posortowania okrelonego zbioru danych i jak
efektywna jest kada z tych operacji. Porwnywanie elementw jest czynnoci znacznie
mniej oczywist, ni mogoby si to w pierwszej chwili wydawa; w kolejnym podrozdziale
omawiamy wynikajc z niego koncepcj komparatora. Dokadny opis wykorzystywanych
operacji listowych get(), set(), insert() i delete() znajdzie Czytelnik w rozdziale 3.
153
Operacje komparatora
Komparator wykonuje tylko jedn operacj jest ni okrelenie wzgldnej kolejnoci
dwch porwnywanych obiektw. Zalenie od tego, czy pierwszy z wymienionych obiektw jest mniejszy, rwny lub wikszy od drugiego (w sensie przyjtego kryterium porwnywania), wynikiem tej operacji jest (odpowiednio) warto ujemna, zero lub warto dodatnia. Jeli typ ktregokolwiek z wymienionych obiektw wyklucza moliwo porwnywania
go z innymi obiektami, prba wykonania porwnania powoduje wystpienie wyjtku ClassCastException.
154
Algorytmy. Od podstaw
Interfejs komparatora
Jedyna operacja komparatora przekada si na jedyn metod interfejsu Comparator, okrelajc wzgldn relacj (porzdek) midzy obiektami okrelonymi przez jej argumenty:
public interface Comparator {
public int compare(Object left, Object right);
}
Argumenty metody nieprzypadkowo okrelone zostay jako lewy (left) i prawy (right),
mog by bowiem utosamiane z (odpowiednio) lewym i prawym argumentem operatora
porwnania metoda compare() w istocie stanowi uoglnienie operatora porwnania dla
typw podstawowych jzyka. Zalenie od tego, czy obiekt left jest (w sensie przyjtego
kryterium porwnywania) mniejszy od obiektu right, rwny mu lub od niego wikszy,
metoda zwraca (odpowiednio) warto ujemn (zwykle 1, cho niekoniecznie), zero (koniecznie) lub warto dodatni (zwykle 1, cho niekoniecznie).
Komparator naturalny
W wielu typach danych, szczeglnie typach podstawowych, jak acuchy czy liczby cakowite, zdefiniowane jest a priori uporzdkowanie naturalne: 1 poprzedza 2, A poprzedza B, B
poprzedza C itp. Komparator narzucajcy taki wanie naturalny porzdek nazywamy (jakeby inaczej) komparatorem naturalnym. Jak pokaemy za chwil, dla danych okrelonego
typu zdefiniowa mona ich naturalne uporzdkowanie, bazujc na konwencjach obowizujcych w jzyku Java umoliwiajcym to rodkiem jest interfejs Comparable.
Interfejs Comparable
Interfejs Comparable posiada tylko jedn metod:
public interface Comparable {
public int compareTo(Object other);
}
155
Jest wic jasne, e aby zdefiniowa naturalny porzdek w stosunku do wartoci danej klasy,
naley zaimplementowa w tej klasie interfejs Comparable. Przykadowo dla rekordw zawierajce dane pracownikw za uporzdkowanie naturalne mona przyj uporzdkowanie
wedug nazwiska i imienia. Koncepcja ta stanowi uoglnienie operatorw <, = i > na zoone
typy danych i faktycznie wiele powszechnie uywanych klas z pakietu java.lang implementuje interfejs Comparable.
Gdy wyobrazimy sobie funkcjonowanie komparatora naturalnego (ktrego klas nazwiemy
NaturalComparator), szybko stanie si jasne, i naley go przetestowa w kontekcie trzech
Jak to dziaa?
Dla kadego z trzech moliwych przypadkw relacji midzy porwnywanymi obiektami
(mniejszy, rwny, wikszy) zdefiniowano osobny przypadek testowy, wykonujcy oczywiste
porwnanie acuchw jednoznakowych. Wszystkie przypadki testowe bazuj na zaoeniu, e istnieje tylko jedna statyczna instancja klasy NaturalComparator i nie naley tworzy innych jej instancji.
156
Algorytmy. Od podstaw
public final class NaturalComparator implements Comparator {
/** jedyna, publicznie dostpna instancja komparatora */
public static final NaturalComparator INSTANCE = new NaturalComparator();
/**
* konstruktor prywatny, nie jest moliwe samodzielne tworzenie instancji
*/
private NaturalComparator() {
}
...
Po upewnieniu si, e lewy argument nie jest argumentem pustym, nastpuje jego rzutowanie na instancj interfejsu Comparable i wywoanie metody compareTo() tego interfejsu z prawym obiektem jako argumentem.
Rzutujc obiekt left na instancj interfejsu Comparable nie sprawdzamy, czy rzutowanie to
jest wykonalne, tzn. czy typ tego obiektu nie wyklucza wykonywania jego porwna z innymi obiektami. Sprawdzenie takie jest niepotrzebne, bowiem interfejs Comparator dopuszcza wystpowanie wyjtku ClassCastException w sytuacji, gdy wymieniony wyej warunek
nie jest speniony.
Jak to dziaa?
Klasa NaturalComparator skonstruowana zostaa w celu porwnywania dwch obiektw
implementujcych interfejs Comparable. Interfejs ten implementowany jest standardowo
przez wiele klas Javy i oczywicie mona go implementowa ad hoc w klasach definiowanych samodzielnie. Implementacja taka polega kadorazowo na zrzutowaniu lewego argumentu na instancj interfejsu Comparable i wywoaniu na jego rzecz metody compareTo()
z prawym argumentem jako parametrem. Wszelka logika porwnywania nie jest w tej
implementacji widoczna, skrywa si bowiem cakowicie w implementacji metody compareTo().
Komparator odwrotny
Zdarza si, e majc zdefiniowane pewne uporzdkowanie wartoci jakiego typu, chcielibymy posortowa te wartoci w kolejnoci dokadnie odwrotnej ni wynikajca z tego
uporzdkowania, na przykad wypisa nazwy plikw pewnego katalogu w kolejnoci od-
157
Po upewnieniu si, e lewy argument nie jest argumentem pustym, nastpuje wywoanie
jego metody compareTo() z prawym argumentem jako parametrem wywoania.
Mimo i to dorane rozwizanie sprawdza si niele w tym szczeglnym przypadku, jest
mao uniwersalne, bowiem dla bardziej zoonych struktur danych, jak lista plikw czy lista
danych pracowniczych, wymaga definiowania dwch komparatorw, po jednym dla kadego
kierunku uporzdkowania.
Znacznie bardziej eleganckie rozwizanie, ktre za chwil zaprezentujemy, polega na odwrceniu kierunku wskazanego komparatora poprzez jego udekorowanie (otoczenie) innym
komparatorem, w wyniku czego otrzymuje si komparator zwany komparatorem odwrotnym.
Dla kadego typu danych wystarczy wwczas zdefiniowa tylko jeden komparator i w razie
potrzeby odwraca wyznaczane przez niego uporzdkowanie w sposb uniwersalny, za
pomoc opisanego odwracania.
Analogicznie, jeli lewy argument komparatora odwrotnego jest wikszy ni prawy, metoda
compare() tego komparatora powinna zwrci warto ujemn:
public void testGreaterThanBecomesLessThan() {
ReverseComparator comparator =
new ReverseComparator(NaturalComparator.INSTANCE);
assertTrue(comparator.compare("B", "A") < 0);
}
158
Algorytmy. Od podstaw
W przypadku porwnywania identycznych argumentw nic si nie zmienia, zarwno dla
komparatora oryginalnego, jak i odwrotnego metoda compare() powinna zwrci 0:
public void testEqualsRemainstnchanged() {
ReverseComparator comparator =
new ReverseComparator(NaturalComparator.INSTANCE);
assertTrue(comparator.compare("A", "A") == 0);
}
Jak to dziaa?
Na bazie komparatora naturalnego NaturalComparator tworzony jest komparator odwrotny
ReverseComparator, ktrego dziaanie (czyli wyniki porwna) powinno by dokadnie odwrotne w stosunku do pierwowzoru. W szczeglnoci acuch A powinien by uznany za
wikszy od acucha B i vice versa acuch B powinien zosta uznany za mniejszy od acucha A. W przypadku porwnywania dwch identycznych obiektw komparator odwrotny take powinien uzna je za identyczne.
Jak to dziaa?
Na pierwszy rzut oka wyglda to ma zwyke delegowanie wywoania do metody compare()
komparatora oryginalnego; jeli jednak spojrze uwanie po raz drugi, atwo mona zauway,
e delegowaniu temu towarzyszy zmiana kolejnoci argumentw: przykadowo wywoanie
159
compare("A", "B") w interfejsie odwrotnym (ReverseComparator) delegowane jest do interfejsu oryginalnego jako wywoanie compare("B", "A"). Zamiana kolejnoci argumentw
160
Algorytmy. Od podstaw
Rysunek 6.2.
Po pierwszej
zamianie miejsc
Rysunek 6.3.
Po wykonaniu
pierwszego kroku
najstarsza osoba
znajduje si ju
na swoim miejscu,
czyli na skrajnej
prawej pozycji
Cho senior rodu zajmuje ju waciw pozycj, kolejno, w jakiej ustawione s pozostae
osoby, nadal pozostawia wiele do yczenia, mimo e wykonalimy ju kilka porwna i przestawie. Na razie musimy si pogodzi z tak nieefektywnym sortowaniem, w nastpnym
rozdziale poznamy jego efektywniejsze algorytmy.
Kolejny krok sortowania bbelkowego przebiega identycznie jak pierwszy z t jednak rnic, e skrajna prawa pozycja jest ju waciwie obsadzona i moemy j pomin w porwnaniach. Ostatecznie krok ten doprowadza do tego, e druga co do starszestwa osoba
trafia na przeznaczon dla niej pozycj, jak na rysunku 6.4.
Rysunek 6.4.
Po wykonaniu
drugiego kroku
sortowania
dwie najstarsze
osoby stoj ju na
swoich miejscach
Wykonujc jeszcze dwa kroki sortowania, z udziaem najpierw trzech, a potem dwch osb,
otrzymamy ostatecznie podany ukad widoczny na rysunku 6.5.
Rysunek 6.5.
Rodzina prawidowo
ustawiona wedug
starszestwa
161
Interfejs ListSorter
Jak wiele interfejsw interfejs ListSorter jest skrajnie prosty, zawiera bowiem tylko jedn
metod, odpowiedzialn za posortowanie listy.
Metoda sort() otrzymuje list jako argument wejciowy i zwraca jako wynik jej posortowan wersj. Zalenie od implementacji lista wynikowa moe by list oryginaln, w ktrej
poprzestawiano elementy (sortowanie w miejscu) lub list nowo utworzon, zawierajc
kopie elementw pierwszej listy.
public interface ListSorter {
public List sort(List list);
}
162
Algorytmy. Od podstaw
_unsortedList = new LinkedList();
_unsortedList.add("programowanie");
_unsortedList.add("sterowane");
_unsortedList.add("testami");
_unsortedList.add("to");
_unsortedList.add("may");
_unsortedList.add("krok");
_unsortedList.add("dla");
_unsortedList.add("programisty");
_unsortedList.add("lecz");
_unsortedList.add("olbrzymi");
_unsortedList.add("skok");
_unsortedList.add("w");
_unsortedList.add("dziejach");
_unsortedList.add("programowania");
_sortedList = new LinkedList();
_sortedList.add("dla");
_sortedList.add("dziejach");
_sortedList.add("krok");
_sortedList.add("lecz");
_sortedList.add("may");
_sortedList.add("olbrzymi");
_sortedList.add("programisty");
_sortedList.add("programowania");
_sortedList.add("programowanie");
_sortedList.add("skok");
_sortedList.add("sterowane");
_sortedList.add("testami");
_sortedList.add("to");
_sortedList.add("w");
}
Musimy jeszcze zadeklarowa abstrakcyjn metod tworzc instancj implementujc interfejs ListSorter:
protected abstract ListSorter createListSorter(Comparator comparator);
163
expected.next();
actual.next();
Jak to dziaa?
W pierwszym wierszu tworzona jest instancja klasy realizujcej okrelony algorytm sortowania; sortowanie odbywa si w naturalnej kolejnoci alfabetycznej acuchw specyfikowanym komparatorem jest bowiem komparator naturalny. W drugim wierszu wspomniany algorytm jest fizycznie realizowany w testowej licie _unsortedList. Po zakoczeniu
sortowania jego wynik porwnywany jest ze wzorcem: w stosunku do obydwu list wynikowej i wzorcowej najpierw porwnywane s ich rozmiary, a nastpnie przy uyciu
iteratorw porwnywane s kolejne pary odpowiadajcych sobie elementw. Identyczno
obydwu list jest warunkiem, ktry spenia musi dowolna implementacja algorytmu sortowania, jeeli w ogle zamierzamy jej uy do posortowania czegokolwiek!
Przechodzc od ogu do szczegw, zajmijmy si testowaniem sortowania bbelkowego.
Z kompilacj powyszego kodu musimy jednak poczeka, a zdefiniujemy klas BubblesortListSorter uczynimy to niebawem.
Jak to dziaa?
Klasa BubbleListSorterTest, mimo i jej zdefiniowanie sprowadzao si do zdefiniowania
jednej metody, dziedziczy po klasie bazowej AbstractListSorterTest zestaw danych testowych oraz metod testListSorterCanSortSampleList() zawierajc ca logik testow.
Konkretyzuje ona jedyny abstrakcyjny element tej logiki metod createListSorter()
tworzc instancj klasy reprezentujcej algorytm sortujcy.
164
Algorytmy. Od podstaw
/**
* Konstruktor
* parametr: komparator okrelaj cy uporz dkowanie elementrw
*/
public BubblesortListSorter(Comparator comparator) {
assert comparator != null : "nie okrelono komparatora";
_comparator = comparator;
}
...
Teraz przed nami najwaniejsze implementacja samego algorytmu sortowania bbelkowego. Jak pamitamy, algorytm ten wymaga wielu przej przez sortowan list; w wyniku
kadego przejcia kolejny element w pobliu koca listy ustawiany jest na swej waciwej
pozycji. Wynika std, e dla N-elementowej listy po wykonaniu N1 krokw na swych docelowych pozycjach znajdzie si N1 kocowych elementw, a wic take i element pocztkowy, ergo liczba krokw potrzebnych do posortowania dowolnej listy jest o jeden
mniejsza od liczby elementw zawartych w tej licie. Kod odpowiedzialny za powtarzanie
wspomnianych krokw nazwiemy ptl zewntrzn (outer loop).
W kadym kroku porwnywane s pary ssiadujcych elementw; jeeli wzgldna kolejno elementw pary nie jest zgodna z kryterium okrelonym przez komparator, elementy
zamieniane s miejscami ten cykl nazwiemy ptl wewntrzn (inner loop). Poniewa
w kadym kroku kolejny element kocowy lduje na swej pozycji docelowej, liczba elementw porwnywanych w kolejnych krokach systematycznie si zmniejsza: w pierwszym
kroku musimy wykona N1 porwna, w drugim N2 itd. Wyjania to warunek kontynuowania ptli wewntrznej left < (size - pass).
public List sort(List list) {
assert list != null : "nie okrelono listy wejciowej";
int size = list.size();
for (int pass = 1; pass < size; ++pass) {
// ptla zewntrzna
}
}
165
return list;
Jak przed chwil wspomnielimy, jeli kolejno ssiadujcych elementw nie jest zgodna
z kryterium okrelonym przez komparator, elementy te zamieniane s miejscami. Musimy
wic dysponowa metod zamieniajc miejscami wartoci elementw o wskazanych indeksach.
private void swap(List list, int left, int right) {
Object temp = list.get(left);
list.set(left, list.get(right));
list.set(right, temp);
}
Wyobra sobie ksiki o rnej wysokoci, przypadkowo uoono na pce, jak przedstawia
to rysunek 6.6. Wanie spodziewasz si odwiedzin mamy i chcesz jej zaimponowa swoich zamiowaniem do porzdku domowego, postanawiasz wic poukada ksiki wedug
malejcej wysokoci od lewej do prawej.
Rysunek 6.6.
Pka z losowo
ustawionymi
ksikami
166
Algorytmy. Od podstaw
Sortowanie bbelkowe raczej si do tego nie nada, bo przestawianie ssiednich par byoby
strat czasu zamiana miejscami dwch ksiek trwa bowiem znacznie duej ni porwnanie ich wysokoci. Zdecydowanie lepsz metod na uzyskanie danego uoenia ksiek
bdzie sortowanie przez wybieranie, zwane take sortowaniem przez selekcj (selectionsort).
Znajd na pce najwysz ksik i zdejmij j z pki. Powiniene j ustawi jako pierwsz od lewej; zamiast przesuwa w prawo by moe du liczb innych ksiek, po prostu
zamie j z t, ktra aktualnie znajduje si najbardziej na lewo (nie unikniesz cakowicie
przesuwania ksiek, bowiem zapewne rni si one od siebie gruboci, ten szczeg nie
ma jednak znaczenia w sytuacji, gdy zamiast ksiek sortowane s elementy listy). Opisana
zamiana ksiek, zamiast przesuwania caej ich grupy, pozbawia sortowanie pewnej wasnoci zwanej stabilnoci; zajmiemy si ni w rozdziale 7., na razie jest ona bez znaczenia. Ukad ksiek po pierwszej zamianie przedstawiony jest na rysunku 6.7.
Rysunek 6.7.
Najwysza ksika
znajduje si
ju na skrajnej
lewej pozycji
Jak atwo si domyli, w kolejnym kroku naley odszuka najwysz z pozostaych ksiek i zamieni j miejscami z t, ktra aktualnie zajmuje pozycj drug od lewej. Efekt tej
zamiany przedstawiony jest na rysunku 6.8.
Rysunek 6.8.
Druga co do
wysokoci ksika
znajduje si na
waciwej pozycji
167
168
Algorytmy. Od podstaw
Oczywicie moe si tak zdarzy, e w ktrym stadium sortowania ksika bdzie ju
znajdowa si na swej pozycji docelowej i adne przestawianie nie bdzie wwczas konieczne. Tak czy inaczej nie zmienia to podstawowej wasnoci sortowania przez wybr
tej mianowicie, e grupa elementw jeszcze nieposortowanych, pocztkowo obejmujca
wszystkie elementy, zmniejsza si systematyczne, rozrasta si natomiast grupa elementw
ju posortowanych, pocztkowo pusta, a w kocu obejmujca wszystkie elementy. Co wicej, wybierana ksika od razu trafia na sw docelow pozycj, w przeciwiestwie do sortowania bbelkowego, gdzie elementy stopniowo przesuwane s maymi krokami.
Znaczna cz kodu testowego stworzonego przy okazji sortowania bbelkowego moe by
wykorzystana przy okazji sortowania przez wybieranie. Rozpoczniemy od stworzenia zestawu testowego, po czym zajmiemy si samym algorytmem sortowania.
Jak to dziaa?
Klasa testowa SelectionSortListSorterTest dziedziczy po swej klasie bazowej AbstractListSorterTest wszystkie dane testowe i ca logik testow. Jedynym elementem specyficznym dla sortowania przez wybieranie jest zaimplementowana metoda createListSorter(), dostarczajca instancji klasy realizujcej algorytm sortowania.
169
Jak to dziaa?
Implementacja sortowania przez wybieranie ma posta dwch zagniedonych ptli zewntrznej i wewntrznej podobnie jak w przypadku sortowania bbelkowego. Jest jednak kilka istotnych rnic, nie od razu zauwaalnych. Po pierwsze, ptla zewntrzna przebiega indeksy od 0 do N2, a nie od 1 do N1. Liczba krokw pozostaje ta sama, lecz
zmienna sterujca ptli rwna jest pozycji docelowej, na ktrej umieszczany jest kolejny
element w pierwszym kroku jest to pozycja 0, w drugim pozycja 1 itd. Po wykonaniu
N1 krokw ostatni, N-ty element samoczynnie znajduje si ju na waciwej pozycji.
Po drugie, w ptli wewntrznej nie dokonuje si adnych przestawie, a jedynie wyszukuje
(w grupie nieposortowanych jeszcze elementw) element o najmniejszej wartoci. Co prawda
jest to sytuacja odwrotna do przykadu z ksikami, gdzie sortowanie nastpowao wedug
malejcej wysokoci, lecz dla algorytmu jako takiego nie ma to wikszego znaczenia
w razie potrzeby zawsze mona uy komparatora odwrotnego.
public List sort(List list) {
assert list != null : "nie okrelono listy";
int size = list.size();
for (int slot = 0; slot < size - 1; ++slot) {
int smallest = slot;
for (int check = slot + 1; check < size; ++check) {
if (_comparator.compare(list.get(check), list.get(smallest)) < 0) {
smallest = check;
}
}
swap(list, smallest, slot);
}
return list;
}
Po trzecie, istnieje pewna drobna, lecz istotna rnica w procedurze przestawiajcej elementy. Moe si ot zdarzy, e kolejny element bdzie si ju znajdowa na swoim miejscu i przestawianie go (z samym sob) bdzie niepotrzebne (w sortowaniu bbelkowym
sytuacja taka nie moga si zdarzy, bowiem przestawianie dotyczyo zawsze ssiadujcych
elementw). Metoda swap() sprawdza wic kadorazowo, czy elementy specyfikowane do
przestawienia s istotnie rne:
private void swap(List list, int left, int right) {
if (left == right) {
// czy istotnie chodzi o rrne elementy
return;
// nie, nic nie rrb.
}
170
Algorytmy. Od podstaw
Object temp = list.get(left);
list.set(left, list.get(right));
list.set(right, temp);
}
Sortowanie przez wstawianie (insertionsort) charakterystyczne jest dla ukadania trzymanych w rku kart w kolejnoci wzrastajcej wanoci. Zamy, e ley przed Tob pi
odwrconych kart (rys. 6.10), ktre chciaby posortowa wedug nastpujcego kryterium:
n
najpierw piki (
), potem trefle (
), potem kara (
), a na kocu kiery (
w ramach danego koloru as (A), 2, 3, , 10, walet (J), dama (Q) i krl (K).
),
Rysunek 6.10.
Rka karciana
pi nieznanych
jeszcze kart
Odkrywamy pierwsz kart; nie ma nic prostszego jak posortowanie jednego elementu,
wic po prostu odkadamy kart do grupy elementw posortowanych. W sytuacji na rysunku
6.11 odkryt kart jest sidemka karo.
Rysunek 6.11.
Pojedyncza karta
jest zawsze
posortowana
Niech druga odkryta karta bdzie waletem pik (rysunek 6.12). Wedug przyjtego kryterium poprzedza ona sidemk karo, wstawiamy j wic na pierwsz pozycj.
Trzecia karta okazuje si by asem trefl i wedug przyjtej kolejnoci plasuje si midzy
dwiema ju odkrytymi (rysunek 6.13).
171
Rysunek 6.12.
Druga karta
zostaje wstawiona
przed pierwsz
Rysunek 6.13.
Trzecia karta
zostaje wstawiona
midzy dwie
pozostae
Jak wic widzimy, sortowanie przez wstawianie polega na podziale sortowanych elementw na dwie grupy: posortowan (pocztkowo pust) i nieposortowan (obejmujc pocztkowo wszystkie elementy). W kadym z kolejnych krokw z grupy nieposortowanej
brany jest kolejny element i wstawiany na odpowiednie miejsce do grupy posortowanej
tak by pozostaa ona nadal posortowana. W ten sposb grupa nieposortowana stopniowo si
zmniejsza, a grupa posortowana powiksza si, by w kocu obj wszystkie elementy
jak na rysunku 6.14, po odkryciu wszystkich piciu kart.
Rysunek 6.14.
Odkrycie
przedostatniej
i ostatniej karty
172
Algorytmy. Od podstaw
package com.wrox.algorithms.sorting;
public class InsertionSortListSorterTest extends AbstractListSorterTest {
protected ListSorter createListSorter(Comparator comparator) {
return new InsertionSortListSorter(comparator);
}
}
Jak to dziaa?
Tak jak poprzednio klasa testowa (InsertionSortListSorterTest) dziedziczy po swej klasie bazowej AbstractListSorterTest wszystkie dane testowe i ca logik testow. Jedynym elementem specyficznym dla sortowania przez wstawianie jest zaimplementowana metoda createListSorter(), dostarczajca instancji klasy realizujcej algorytm sortowania.
Metoda sort() klasy InsertionSortListSorter rni si zasadniczo od tej implementowanej w klasach BubbleSortListSorter i SelectionSortListSorter pod jednym wzgldem:
zamiast sortowania zawartoci listy w miejscu tworzymy now, pust list wynikow i sukcesywnie wstawiamy do niej (na waciw pozycj) elementy pobierane kolejno z listy wejciowej.
public List sort(List list) {
assert list != null : "nie okrelono listy wejciowej";
final List result = new LinkedList();
173
Iterator it = list.iterator();
for (it.first(); !it.isDone(); it.next()) {
// ptla zewntrzna
int slot = result.size();
while (slot > 0) {
// ptla wewntrzna
if (_comparator.compare(it.current(), result.get(slot - 1)) >= 0) {
break;
}
--slot;
}
result.insert(slot, it.current());
}
return result;
}
Jak to dziaa?
W zewntrznej ptli for za pomoc iteratora pobierane s kolejne elementy listy wejciowej; uycie iteratora jest rozwizaniem bardziej uniwersalnym ni bezporedni dostp do
elementw na podstawie ich indeksw. W ptli wewntrznej ktra nie jest ptl for, lecz
ptl while w (stopniowo zapenianej) licie wynikowej poszukiwana jest pozycja, na
ktr naley wstawi element pobrany z listy wejciowej. W przeciwiestwie do listy wejciowej, ktrej implementacja jest bez znaczenia, lista wynikowa jest list wizan LinkedList,
a dostp do jej elementw odbywa si w sposb bezporedni. Wybralimy list wizan ze
wzgldu na efektywno, z jak mona wstawia do niej elementy. Lista wynikowa pozostaje
cay czas posortowana, a po wstawieniu do niej ostatniego elementu sortowanie si koczy.
Zwrmy ponadto uwag, e poszukiwanie (w ptli wewntrznej) waciwej pozycji w licie wynikowej rozpoczyna si od jej koca. Mimo i nie wpywa to na wydajno sortowania przecitnej listy, to jednak drastycznie poprawia t wydajno w przypadku, gdy lista
wejciowa jest ju posortowana (lub prawie posortowana) wstawienie elementu (a waciwie jego doczenie) odbywa si ju po wykonaniu jednego porwnania. Powrcimy do
tej kwestii przy okazji porwnywania prostych algorytmw sortowania w dalszej czci niniejszego rozdziau. Kierunek przegldania posortowanej listy wynikowej nie jest natomiast obojtny z punktu widzenia stabilnoci sortowania.
174
Algorytmy. Od podstaw
Nazwisko
Albert
Smith
Brian
Jackson
David
Barnes
John
Smith
John
Wilson
Mary
Smith
Tom
Barnes
Vince
De Marco
Walter
Clarke
Nazwisko
David
Barnes
Tom
Barnes
Walter
Clarke
Vince
De Marco
Brian
Jackson
Albert
Smith
John
Smith
Mary
Smith
John
Wilson
Nazwisko
David
Barnes
Tom
Barnes
Walter
Clarke
Vince
De Marco
Brian
Jackson
Albert
Smith
Mary
Smith
John
Smith
John
Wilson
175
Spord trzech opisanych dotd algorytmw sortowania algorytmem stabilnym jest sortowanie bbelkowe. To, czy stabilne jest sortowanie przez wstawianie, zalene jest od kolejnoci pobierania elementw z listy wejciowej i sposobu ich wstawiania do listy wynikowej;
prezentowana przez nas implementacja jest implementacj stabiln. Podobnie stabilno
sortowania przez wybieranie zaley od szczegw jego implementacji. Omawiane w nastpnym rozdziale bardziej zaawansowane algorytmy sortowania, cho cechuj si znaczco lepsz efektywnoci, nie s algorytmami stabilnymi i jest to jedna z ich wad w porwnaniu z prostymi algorytmami sortowania, o czym trzeba pamita przy tworzeniu aplikacji
o konkretnych wymaganiach.
Po zapoznaniu si z trzema prostymi algorytmami sortowania bbelkowego, przez wybieranie i przez wstawianie nie sposb nie zastanawia si, ktry z nich okae si najlepszy w danym zastosowaniu, a dokadniej jakimi kryteriami naley si kierowa przy
dokonywaniu jego wyboru. W niniejszym podrozdziale dokonamy porwnania wymienionych algorytmw; nie bdzie to formalne porwnanie matematyczne, lecz porwnanie
praktyczne oparte na obserwacji sortowania rzeczywistych danych. Nie jest naszym zadaniem definitywne sformuowanie kryteriw wyboru konkretnego algorytmu, lecz raczej pokazanie, jak wspomniana analiza porwnawcza moe dokonanie takiego wyboru uatwi.
Na pocztku tego rozdziau informowalimy, ze istot kadego sortowania jest intensywne
wykonywanie dwch operacji: porwnywania elementw i ich przestawiania. Nasza analiza porwnawcza koncentrowa si bdzie na pierwszej z tych operacji, a uywane na jej
potrzeby zestawy danych bd znacznie wiksze ni w zestawach testowych weryfikujcych poprawno implementacji algorytmw; jest to konieczne z tego wzgldu, e prawdziwy charakter kadego algorytmu, odzwierciedlany gwnie przez jego zachowanie asymptotyczne wyraone w notacji duego O, uwidacznia si dopiero przy rozwizywaniu problemw
o duych rozmiarach. Ponadto, poniewa konkretne dane wejciowe algorytmu maj zwykle wpyw na jego efektywno, analiz nasz przeprowadzimy w oparciu o trzy szczeglne
rodzaje danych wejciowych:
n
list ju posortowan,
176
Algorytmy. Od podstaw
CallCountingListComparator
Poniewa za wszystkie porwnania, jakie wykonywane s w ramach algorytmu sortowania,
odpowiedzialny jest komparator, a dokadniej jego metoda compare(), najprostszym sposobem zliczania porwna wydaje si przechwycenie wywoania tej metody, czyli wzbogacenie jej o fragment kodu dokonujcy zliczania wszystkich wywoa. Mona by te posun
si jeszcze dalej i wyposay w taki mechanizm zliczania w jak klas bazow, z ktrej
wyprowadzane byby wszystkie zliczajce komparatory. Wymagaoby to jednak ponownego zaimplementowania od podstaw tych komparatorw, ktre chcemy uczyni zliczajcymi. Chcc wykorzysta w jak najwikszym stopniu istniejcy kod, postpimy wic inaczej i funkcj zliczajc komparatora zrealizujemy za pomoc jego otoczki (dekoratora),
podobnie jak czynilimy to w przypadku odwracania uporzdkowania za pomoc klasy
ReverseComparator.
public final class CallCountingComparator implements Comparator {
/** komparator oryginalny, ktrry wyposaamy w funkcj zliczania */
private final Comparator _comparator;
/** zmienna przechowuj ca liczb zarejestrowanych wywoa
private int _callCount;
komparatora */
/**
* Konstruktor.
* Parametr: oryginalny komparator
*/
public CallCountingComparator(Comparator comparator) {
assert comparator != null : "nie okrelono komparatora";
_comparator = comparator;
_callCount = 0;
Podobnie jak komparator odwrotny ReverseComparator, tak i komparator zliczajcy CallCountingComparator definiowany jest na bazie dowolnego komparatora przekazywanego jako
parametr wywoania konstruktora. Wywoanie metody compare() komparatora zliczajcego
jest rejestrowane poprzez zwikszenie wartoci zmiennej _callCount, po czym delegowane
jest do metody compare() komparatora oryginalnego. Warto zmiennej _callCount, rwna
liczbie dokonanych wywoa, dostpna jest za porednictwem metody getCallCount().
Majc do dyspozycji komparator zliczajcy, moemy tworzy zestawy testowe badajce
zachowanie si poszczeglnych algorytmw sortowania w odniesieniu do danych o rnym
charakterze.
177
ListSorterCallCountingTest
Mimo i tym razem nie zamierzamy testowa poprawnoci zachowania si kodu, lecz mierzy liczb porwna wykonywanych przez algorytmy sortowania, skorzystamy z biblioteki
Jtnit, bowiem podobnie jak w przypadku testw moduw bdziemy musieli wykona kilka dyskretnych scenariuszy dla kadego algorytmu poprzedzonych pewnymi czynnociami
przygotowawczymi (setup). Zdefiniujemy wic klas testow, a w ramach niej sta okrelajc rozmiar sortowanej listy, trzy listy o charakterystykach wczeniej wymienionych
(posortowan, posortowan odwrotnie i nieposortowan) oraz instancj komparatora zliczajcego.
package com.wrox.algorithms.sorting;
import com.wrox.algorithms.lists.ArrayList;
import com.wrox.algorithms.lists.List;
import junit.framework.TestCase;
public class ListSorterCallCountingTest extends TestCase {
private static final int TEST_SIZE = 1000;
// lista posortowana
private final List _sortedArrayList = new ArrayList(TEST_SIZE);
// lista posortowana odwrotnie
private final List _reverseArrayList = new ArrayList(TEST_SIZE);
// lista o przypadkowej kolejnoci elementrw
private final List _randomArrayList = new ArrayList(TEST_SIZE);
private CallCountingComparator _comparator;
...
}
178
Algorytmy. Od podstaw
}
for (int i = 1; i < TEST_SIZE; ++i) {
_randomArrayList.add(new Integer((int)(TEST_SIZE * Math.random())));
}
}
Wyniki obserwacji kadego z sortowa, czyli informacja o liczbie wywoa metody compare() odnonego komparatora, wywietlane s za pomoc metody reportCalls(), ktr opiszemy za chwil. W podobny sposb przeprowadzimy obserwacj dla listy posortowanej
w danej kolejnoci
public void testDirectCaseBubblesort () {
new BubblesortListSorter(_comparator).sort(_sortedArrayList);
reportCalls();
}
public void testDirectCaseSelectionSort () {
new SelectionSortListSorter(_comparator).sort(_sortedArrayList);
reportCalls();
}
public void testDirectCaseInsertionSort () {
new InsertionSortListSorter(_comparator).sort(_sortedArrayList);
reportCalls();
}
179
Wspomniana wczeniej metoda reportCalls() odczytuje za pomoc metody callCount() licznik dokonanych porwna i wyprowadza jego warto poprzedzon nazw klasy
testowej:
private void reportCalls() {
System.out.println(getName() + ": " + _comparator.getCallCount() + " wywoa ");
}
Nazwa klasy testowej jak atwo si zorientowa udostpniana jest przez metod getName(), ktra jest metod klasy bazowej TestCase biblioteki Jtnit. Oto przykadowy raport
dla listy posortowanej odwrotnie:
testReverseCaseBubblesort: 499500 wywoa
testReverseCaseSelectionSort: 499500 wywoa
testReverseCaseInsertionSort: 499500 wywoa
Jak wida, wszystkie trzy algorytmy sortowania wykonay tak sam liczb porwna dla
listy wyglda na to, e jest ona jednakowo trudnym przypadkiem dla kadego z nich.
Nie naley jednak przyjmowa tego jako reguy, a w przypadku danych o charakterze wycznie empirycznym (jak tutaj) naley wystrzega si formuowania pochopnych, by moe z gruntu faszywych wnioskw, cho oczywicie nie mona nie zastanawia si nad
przyczynami obserwowanych faktw.
Wyniki analogicznej analizy dla listy ju posortowanej wygldaj zgoa odmiennie:
testDirectCaseBubblesort: 49t501 wywoa
testDirectCaseSelectionSort: 49t501 wywoa
testDirectCaseInsertionSort: 99t wywoa
Tak dua wraliwo sortowania przez wstawianie na fakt posortowania listy wejciowej
nie powinna by zaskoczeniem. Jej przyczyn wyjanialimy wczeniej jest ni szczeglny
sposb przeszukiwania listy wynikowej, poczwszy od jej koca, nie pocztku.
Na koniec pozostaje porwnanie zachowania si algorytmw sortowania dla typowej, nieuporzdkowanej listy:
testRandomCaseBubblesort: 49t501 wywoa
testRandomCaseSelectionSort: 49t501 wywoa
testRandomCaseInsertionSort: 262095 wywoa
180
Algorytmy. Od podstaw
Algorytm sortowania przez wstawianie wykonuje, jak wida, dwukrotnie mniej porwna
ni kady z dwch pozostaych algorytmw.
By moe najwaniejszym wnioskiem podsumowujcym analiz jest niewraliwo sortowania bbelkowego i sortowania przez wybieranie na charakter sortowanych danych. W przeciwiestwie do nich sortowanie przez wstawianie wykazuje due zdolnoci adaptacyjne: jeli
mona posortowa dane mniejszym nakadem pracy, algorytm istotnie wykorzystuje t moliwo. Jest to gwn przyczyn faworyzowania w praktyce sortowania przez wstawianie
w stosunku do sortowania bbelkowego i sortowania przez wybieranie.
W niniejszym rozdziale:
n
181
Lektura niniejszego rozdziau z pewnoci pozwoli Czytelnikom lepiej zrozumie znaczenie sortowania dla innych czynnoci algorytmicznych, na przykad wyszukiwania. Tre
rozdziau jest ponadto dowodem na to, e rozmaite problemy algorytmiczne mog by rozwizywane na rne sposoby w szczeglnoci istnieje kilka rnych metod porzdkowania elementw w zadanej kolejnoci. W nastpnym rozdziale poznamy inne, bardziej
zoone metody sortowania, ktre dla bardzo duych rozmiarw danych wejciowych okazuj si znacznie efektywniejsze od metod dotychczas opisanych.