W y d a n i e IV
R O B E R T S E D G E W I C K K E V I N W A Y N E
b . 7 ii
Helion
SPIS TREŚCI
P rzedm ow a .................................................................................................... 8
1 P odstaw y................................................................................................. 14
1.1 Podstawowy m odel program ow ania 20
1.2 Abstrakcja danych 76
1.3 Wielozbiory, kolejki i stosy 132
1.4 Analizy algorytm ów 184
1.5 Studium przypadku — problem U nion-F ind 228
2 Sortowanie............................................................................................254
2.1 Podstawowe m etody sortow ania 256
2.2 Sortowanie przez scalanie 282
2.3 Sortowanie szybkie 300
2.4 Kolejki priorytetow e 320
2.5 Zastosow ania 348
3 W yszukiw anie......................................................................................372
3.1 Tablice sym boli 374
3.2 Drzewa wyszukiwań binarnych 408
3.3 Zbalansow ane drzewa wyszukiwań 436
3.4 Tablice z haszow aniem 470
3.5 Zastosow ania 498
4 G ra fy ...................................................................................................... 526
4.1 Grafy nieskierow ane 530
4.2 Grafy skierowane 578
4.3 M inim alne drzewa rozpinające 616
4.4 Najkrótsze ścieżki 650
5 Łańcuchy zn a k ó w ................................................................................ 706
5.1 Sortow anie łańcuchów znaków 714
5.2 Drzewa trie 742
5.3 W yszukiwanie podłańcuchów 770
5.4 W yrażenia regularne 800
5.5 Kompresja danych 822
6 K o n te k s t................................................................................................864
A lgorytm y................................................................................................... 944
K l i e n t y ...................................................................................................... 945
Skorowidz...................................................................................................946
7
puzedm ow a
siążka ta ma stanowić przegląd najważniejszych stosowanych obecnie algo
K rytmów komputerowych i pozwolić poznać podstawowe techniki osobom,
które powinny je rozumieć. Napisano ją jako podręcznik na drugi kurs nauk
komputerowych, prowadzony po zdobyciu przez studentów podstawowych umiejęt
ności programistycznych i zaznajomieniu się z systemami komputerowymi. Może być
przydatna także do samodzielnej nauki lub jako źródło wiedzy dla osób zajmujących
się rozwijaniem systemów komputerowych lub aplikacji, ponieważ zawiera implemen
tacje użytecznych algorytmów oraz szczegółowe informacje o ich wydajności i klien
tach. Szeroka perspektywa sprawia, że książka jest odpowiednim wprowadzeniem do
dziedziny algorytmów.
i s t r u k t u r d a n y c h jest podstawą w każdym programie
p o z n a w a n ie a l g o r y t m ó w
nauk komputerowych, jednak dziedzina ta jest przeznaczona nie tylko dla progra
mistów i studentów nauk komputerowych. Każdy, kto używa komputera, chce, aby
maszyna działała szybciej lub rozwiązywała większe problemy. Algorytmy w książce
reprezentują niezbędną wiedzę opracowaną w ciągu ostatnich 50 lat. Od symulacji fi
zycznych problemów ruchu N ciał po problemy sekwencjonowania genomu z biologii
molekularnej — opisane tu podstawowe metody stały się kluczowe w badaniach na
ukowych. W obszarach od systemów modelowania architektonicznego po symulacje
lotu stały się niezbędnymi narzędziami dla inżynierów. W dziedzinach od systemów
baz danych do wyszukiwarek internetowych stały się podstawowymi elementami
współczesnych systemów oprogramowania. A to dopiero kilka przykładów. Wraz ze
zwiększaniem się zakresu zastosowań komputerów rośnie też wpływ podstawowych
m etod omówionych w książce.
Przed przedstawieniem podstawowego sposobu badania algorytmów opracowano
typy danych dla stosów, kolejek i innych niskopoziomowych abstrakcji używanych
w książce. Następnie omówiono podstawowe algorytmy sortowania i wyszukiwa
nia oraz do przetwarzania grafów i łańcuchów znaków. Ostatni rozdział to przegląd,
w którym pozostały materiał z książki przedstawiono w szerszym kontekście.
Cechy charakterystyczne Książka ma pozwolić poznać algorytmy często sto
sowane w praktyce. Przedstawiono tu bardzo zróżnicowane algorytmy i struktury
danych oraz wystarczającą ilość wiedzy na ich temat, aby m ożna było je swobodnie
zaimplementować i zdiagnozować, a także zastosować w dowolnym środowisku
obliczeniowym. Oto zastosowane podejścia:
A lg o rytm y Opisy algorytmów oparte są na kompletnych implementacjach i om ó
wieniu działania programów na podstawie spójnego zbioru przykładów. Zamiast
przedstawiać pseudokod, zaprezentowano rzeczywisty kod, dlatego programy można
szybko wykorzystać w praktyce. Programy napisano w Javie, jednak w taki sposób,
że większość kodu m ożna ponownie wykorzystać do opracowania implementacji
w innych współczesnych językach programowania.
Typy danych Zastosowano współczesny styl programowania oparty na abstrakcji
danych, dlatego algorytmy i ich struktury danych są hermetyzowane razem.
Z astosow ania Każdy rozdział obejmuje szczegółowy opis zastosowań, w których
algorytmy odgrywają kluczową rolę. Są to zastosowania od dziedzin fizyki i biologii
molekularnej przez obszary inżynierii komputerów i systemów po popularne zada
nia, takie jak kompresja danych i wyszukiwanie informacji w internecie.
Podejście naukow e W książce położono nacisk na opracowywanie modeli m atem a
tycznych do opisu wydajności algorytmów, używanie modeli do tworzenia hipotez
na tem at wydajności i testowanie hipotez przez urucham ianie algorytmów w reali
stycznym kontekście.
Szeroki zakres Uwzględniono podstawowe abstrakcyjne typy danych, algorytmy
sortowania, algorytmy wyszukiwania, przetwarzanie grafów i przetwarzanie łań
cuchów znaków. Materiał opisywany jest w kontekście algorytmów — omówiono
struktury danych, paradygmaty projektowania algorytmów, redukcję i modele roz
wiązywania problemów. Przedstawiono klasyczne metody, uczone od lat 60. ubiegłe
go wieku, a także nowe rozwiązania, wynalezione w ostatnich latach.
Naszym głównym celem jest przedstawienie najważniejszych używanych obecnie
algorytmów jak najszerszemu gronu odbiorców. Opisywane algorytmy są prze
ważnie pomysłowymi rozwiązaniami, które — co zaskakujące — m ożna zapisać
w kilkunastu lub kilkudziesięciu wierszach kodu. Algorytmy te razem umożliwiają
rozwiązanie niezwykle dużej grupy problemów. Pozwalają tworzyć niemożliwe bez
ich użycia struktury obliczeniowe, rozwiązania problem ów naukowych i aplikacje
komercyjne.
Witryna poświęcona książce Ważną cechą książki jest jej powiązanie z wi
tryną [Link]. W itryna jest dostępna bezpłatnie i zawiera wiele m a
teriałów na tem at algorytmów oraz struktur danych dla wykładowców, studentów
i programistów. Oto wybrane materiały:
Elektroniczne streszczenie Tekst jest streszczony w witrynie. Streszczenie ma tę
samą ogólną strukturę, co książka, ale obejmuje odnośniki pozwalające łatwo poru
szać się po materiale.
Pełne im plem entacje W witrynie dostępny jest cały kod z książki. Ma on postać
odpowiednią do rozwijania programów. Dostępnych jest też wiele innych implemen
tacji, w tym zaawansowane implementacje i usprawnienia opisane w książce, roz
wiązania wybranych ćwiczeń i kod kliencki różnych aplikacji. Nacisk położono na
umożliwienie testowania algorytmów w kontekście sensownych aplikacji.
Ćwiczenia i odpow iedzi W witrynie rozwinięto ćwiczenia z książki przez dodanie
zadań powtórkowych (odpowiedzi dostępne są po kliknięciu), licznych przykładów
pokazujących zakres tematyczny materiału, ćwiczeń programistycznych z kodem
rozwiązań, a także trudnych problemów.
D ynam iczne w izualizacje W drukowanej książce nie da się przedstawić dynamicz
nych symulacji, jednak witryna zawiera implementacje z klasami do obsługi grafiki,
stanowiące atrakcyjne wizualne demonstracje zastosowań algorytmów.
M ateriały do kursu Kompletny zbiór slajdów z wykładów jest bezpośrednio powią
zany z materiałem z książki i witryny. Dołączono też kompletny zestaw zadań pro
gramistycznych z listami kontrolnymi, danymi testowymi i materiałami potrzebnymi
do przygotowań.
O dnośniki do pow iązanych m ateriałów Setki odnośników prowadzą studentów
do pomocniczych informacji na tem at zastosowań i do źródeł przydatnych przy po
znawaniu algorytmów
Celem przy tworzeniu materiałów z witryny było udostępnienie informacji uzu
pełniających omawiane zagadnienia. Ogólnie w czasie poznawania konkretnych
algorytmów lub przy próbie uzyskania ogólnego obrazu należy przeczytać książkę,
a z witryny korzystać jak ze źródła wiedzy w trakcie programowania lub jako punktu
wyjścia do szukania bardziej szczegółowych informacji w internecie.
10
Wykorzystanie w programie nauczania Książka ma być podręcznikiem
do drugiego kursu w programie nauk komputerowych. Obejmuje cały podstawowy
materiał i jest doskonałym narzędziem umożliwiającym studentom zyskanie do
świadczenia oraz dojrzałości w programowaniu, wnioskowaniu ilościowym i rozwią
zywaniu problemów. Zwykle wystarczającym wymogiem wstępnym jest ukończenie
jednego kursu z nauk komputerowych. Książka jest przeznaczona dla każdego, kto
zna jeden ze współczesnych języków programowania i podstawowe funkcje współ
czesnych systemów komputerowych.
Algorytmy i struktury danych są zapisane w Javie, ale w stylu przystępnym dla
osób znających inne współczesne języki. Zastosowano nowoczesne abstrakcje z Javy
(w tym typy generyczne), ale pominięto wyrafinowane mechanizmy języka.
Materiały matematyczne związane z analizami można przeważnie zrozumieć bez
dodatkowej wiedzy (w przeciwnym razie opisano je jako wykraczające poza zakres
książki), dlatego w większości książki specyficzne przygotowanie matematyczne jest
potrzebne w niewielkim zakresie, choć — oczywiście — bywa pomocne. Opisane
zastosowania oparto na materiałach dla początkujących z dziedziny nauk przyrodni
czych i też nie wymagają dodatkowej wiedzy.
Przedstawiony m ateriał stanowi niezbędną podstawę dla każdego studenta nauk
komputerowych, inżynierii elektrycznej lub badań operacyjnych. Jest też w artoś
ciowy dla studentów interesujących się naukam i przyrodniczymi, matem atyką lub
inżynierią.
Kontekst Książka ma stanowić kontynuację tekstu dla początkujących, An Intro
duction to Programming in Java: An Interdisciplinary Approach, który jest ogólnym
wprowadzeniem do omawianych zagadnień. Te dwie książki razem można wykorzy
stać w dwu- lub trzysemestralnym wprowadzeniu do nauk komputerowych, które za
pewni studentom wiedzę potrzebną do skutecznego stosowania metod obliczeniowych
w dowolnej dziedzinie nauk komputerowych, inżynierii lub nauk społecznych.
Punktem wyjścia przy pisaniu dużej części książki była seria podręczników
Algorithms Sedgewicka. Niniejsza pozycja duchem najbardziej przypomina wydanie
pierwsze i drugie, natomiast wykorzystano tu dziesięciolecia doświadczeń w naucza
niu i poznawaniu opisanego materiału. Nowsza książka Sedgewicka, Algorithms in
C/C++/Java, Third Edition, jest bardziej źródłem wiedzy lub podręcznikiem do kursu
dla zaawansowanych. Niniejszą książkę zaprojektowano specjalnie jako podręcznik
na jednosemestralny kurs dla studentów pierwszego lub drugiego roku oraz jako
współczesne wprowadzenie do podstaw i źródło wiedzy dla programistów.
11
P o d z i ę k o w a n ia Książka ta jest wydawana prawie od 40 lat, dlatego wymienienie
wszystkich osób, które się do tego przyczyniły, jest po prostu niemożliwe. We wcześ
niejszych wydaniach wymieniono dziesiątki osób. Oto niektóre z nich (w porząd
ku alfabetycznym): Andrew Appel, Trina Avery, Marc Brown, Lyn Dupre, Philippe
Flajolet, Tom Freeman, Dave Hanson, Janet Incerpi, Mike Schidlowsky, Steve Summit
i Chris Van Wyk. Wszystkie te osoby zasługują na podziękowania, nawet jeśli wnio
sły wkład w książkę kilkadziesiąt lat temu. W czwartym wydaniu dziękujemy set
kom studentów z Princeton i kilku innych jednostek, którzy musieli „zmagać się”
ze wstępnymi wersjami książki, a także czytelnikom z całego świata za nadsyłanie
komentarzy i poprawek przez witrynę.
Jesteśmy wdzięczni Uniwersytetowi Princeton za wsparcie oraz niezachwiane za
angażowanie w doskonalenie nauczania i uczenia się, co zapewniło podstawy do po
wstania tej książki.
Peter Gordon służył nam m ądrym i radam i niemal od początku prac. Między in
nymi delikatnie zaproponował podejście „powrót do podstaw”, na którym oparliśmy
to wydanie. W kontekście czwartego wydania jesteśmy wdzięczni Barbarze Wood
za staranną i profesjonalną edycję, Julie Nahil za zarządzanie produkcją oraz wielu
innym osobom z wydawnictwa Pearson za ich pracę przy powstawaniu i marketingu
książki. Wszystkie te osoby doskonale dostosowały się do dość wymagającego har
monogramu, nie idąc przy tym na żadne kompromisy w obszarze jakości.
Robert Sedgewick
Kevin Wayne
Princeton, NJ
Styczeń 2011
ROZDZIAŁ 1
mli Podstawy
1.1 Podstawowy model p ro g ra m o w a n ia ......................... 20
1.2 Abstrakcja d a n y c h .......................................................... 76
1.3 Wielozbiory, kolejki i s t o s y .......................................... 132
1.4 Analizy a lg o ry tm ó w ..................................................... 184
1.5 Studium przypadku — problem Union-Find. . . . 228
siążka ta ma służyć do nauki bardzo zróżnicowanego zestawu ważnych i przy
K datnych algorytmów — metod rozwiązywania problemów możliwych do za
implementowania w komputerach. Algorytmy są powiązane ze strukturami
danych — sposobami porządkowania danych umożliwiającymi wydajne przetwarzanie
tych ostatnich przez algorytm. W tym rozdziale przedstawiono podstawowe narzędzia
potrzebne do poznawania algorytmów i struktur danych.
Najpierw wprowadzono podstawowy model programowania. Wszystkie programy
w książce zaimplementowano za pom ocą małego podzbioru języka programowania
Java oraz kilku opracowanych przez nas bibliotek wejścia-wyjścia i do obliczeń staty
stycznych. p o d r o z d z i a ł i . i jest podsumowaniem konstrukcji, mechanizmów i bi
bliotek języka używanych w książce.
Następnie omówiono abstrakcję danych i zdefiniowano abstrakcyjne typy danych
(ang. abstract data type — ADT) używane w programowaniu modularnym. W p o d
r o z d z i a l e 1 . 2 przedstawiono proces implementowania typów ADT w Javie. Najpierw
należy określić interfejs API (ang. applications programming interface), a następnie za
stosować klasy Javy do utworzenia implementacji używanej w kodzie klienckim.
Dalej omówiono trzy ważne i przydatne podstawowe typy ADT — wielozbiory,
kolejki i stosy, p o d r o z d z i a ł 1.3 to opis interfejsów API i implementacji wielozbio-
rów, kolejek oraz stosów za pomocą tablic, tablic o zmiennej długości i list powią
zanych. Zagadnienia te posłużą za modele i punkty wyjścia przy implementowaniu
algorytmów w dalszej części książki.
Przy badaniu algorytmów podstawową kwestią jest wydajność. W p o d r o z d z i a l e 1.4
opisano stosowany tu sposób analizy wydajności algorytmów. Podstawą jest metoda
naukowa. Należy opracować hipotezy na tem at wydajności, utworzyć modele m ate
matyczne i przeprowadzić eksperymenty w celu ich przetestowania. W razie potrze
by proces trzeba powtórzyć.
Rozdział kończy się studium przypadku. Opisano w nim rozwiązania problemu
określania połączeń (ang. connectivity problem), w których wykorzystano algoryt
my i struktury danych do zaimplementowania klasycznej struktury ADT o nazwie
Union-Find.
15
RO ZD ZIA Ł 1 n Podstawy
Algorytmy W czasie pisania program u komputerowego programista zwykle
implementuje metodę wymyśloną wcześniej w celu rozwiązania pewnego proble
mu. M etoda jest często niezależna od używanego języka programowania. Zwykle
działa równie dobrze na wielu komputerach i w licznych językach programowania.
To metoda, a nie sam program komputerowy określa kroki potrzebne do rozwiązania
problemu. Pojęcie algorytm w naukach komputerowych określa skończoną, determ i
nistyczną i skuteczną metodę rozwiązywania problemu możliwą do zaimplemento
wania w postaci programu komputerowego. Algorytmy są istotą nauk kom putero
wych i głównym obiektem badań w tej dziedzinie.
Algorytm m ożna zdefiniować, opisując w języku naturalnym procedurę rozwią
zywania problemu lub pisząc program komputerowy z implementacją tej procedury,
co pokazano po prawej dla algorytmu Euklidesa służącego do znajdowania najwięk
szego wspólnego dzielnika dwóch
liczb (wersję algorytmu wymyślono Opis w języku polskim
Oblicz największy wspólny dzielnik
ponad 2300 lat temu). Jeśli nie znasz
dwóch nieujemnych liczb całkowitych p
algorytmu Euklidesa, zachęcamy do i q w następujący sposób: jeśli q równa się 0,
wykonania ć w i c z e ń 1 .1.24 i 1 .1 .2 5 , odpowiedzią jest p. W przeciwnym razie należy
na przykład po przeczytaniu p o d podzielić p przez q i wykorzystać resztę r.
Odpowiedź to największy wspólny dzielnik q i r.
r o z d z ia ł u 1 . 1 . W tej książce do
opisu algorytmów służą programy Opis w języku Java
komputerowe. Ważną przyczyną za
p ub lic s t a t i c i n t gc d ( in t p, i n t q)
stosowania tego podejścia jest to, że
I
ułatwia ono sprawdzenie, czy algo i f (q == 0) return p;
rytm jest skończony, deterministycz i n t r = p % q;
return gcd(q, r ) ;
ny i skuteczny. Ważne jest jednak, }
aby pamiętać, że program w konkret Algorytm Euklidesa
nym języku to tylko jeden ze sposo
bów na zapisanie algorytmu. To, że wiele algorytmów z tej książki w kilku ostatnich
dziesięcioleciach przedstawiono w różnych językach programowania, pozwala przy
puszczać, iż każdy algorytm jest metodą, którą m ożna zaimplementować na każdym
komputerze w dowolnym języku programowania.
Większość wartych zainteresowania algorytmów wymaga uporządkowania danych
używanych w obliczeniach. Takie uporządkowanie prowadzi do powstania struktur
danych, które także są podstawowym obiektem badań w naukach komputerowych.
Algorytmy i struktury danych są ze sobą powiązane. W książce przyjęto podejście,
że struktury danych są produktem ubocznym lub produktem końcowym rozwijania
algorytmów, dlatego trzeba je poznać, aby móc zrozumieć algorytmy. Proste algo
rytmy mogą wykorzystywać skomplikowane struktury danych i na odwrót — skom
plikowane algorytmy mogą używać prostych struktur danych. W książce omówiono
cechy wielu struktur danych (równie dobrze mogliśmy zatytułować ją Algorytmy
i struktury danych).
R O ZD ZIA Ł 1 □ Podstawy
Przy używaniu komputera do rozwiązywania problemu zwykle m ożna zastosować
wiele podejść. Jeśli problem jest mały, użyte podejście prawie nie ma znaczenia, o ile
pozwala znaleźć poprawne rozwiązanie. Jednak dla dużych problemów (lub kiedy
trzeba rozwiązać dużą liczbę małych problemów) warto opracować metody, które
wydajnie wykorzystują czas i pamięć.
Podstawową przyczyną badania algorytmów jest to, że dziedzina ta pozwala uzy
skać duże oszczędności, a nawet umożliwia realizowanie zadań niewykonalnych
bez odpowiednich algorytmów. Jeśli aplikacja przetwarza miliony obiektów, nie jest
niczym niezwykłym przyspieszenie jej działania o milion razy przez zastosowanie
odpowiednio zaprojektowanego algorytmu. W książce przedstawiono wiele takich
przykładów. Z kolei inwestowanie dodatkowych środków lub czasu w zakup i insta
lację nowych komputerów umożliwia przyspieszenie program u tylko o 10 lub 100
razy. Staranne projektowanie algorytmu to, niezależnie od dziedziny, niezwykle waż
ny aspekt procesu rozwiązywania dużych problemów.
W czasie rozwijania dużych lub złożonych programów komputerowych trzeba
poświęcić wiele wysiłku na zrozumienie i zdefiniowanie rozwiązywanego problemu,
opanowanie jego złożoności i podzielenie go na mniejsze podzadania, dla których
m ożna łatwo utworzyć implementację. Często implementacja wielu algorytmów po
trzebnych po podziale jest bardzo prosta. Jednak w wielu sytuacjach istnieje kilka
algorytmów, które trzeba starannie dobrać, ponieważ większość zasobów systemu
zużywana jest na ich wykonywanie. W książce koncentrujemy się na algorytmach
tego rodzaju. Badamy podstawowe algorytmy przydatne do rozwiązywania trudnych
problemów w różnorodnych obszarach.
Współużytkowanie programów w systemach komputerowych jest coraz częstsze,
dlatego choć prawdopodobnie w użyciu będzie wiele algorytmów z tej książki, zaim
plementować trzeba będzie tylko ich małą część. Na przykład biblioteki Javy obejmują
implementacje wielu podstawowych algorytmów. Jednak zaimplementowanie pro
stych wersji podstawowych algorytmów pomaga lepiej je zrozumieć (a przez to spraw
niej z nich korzystać) i dopracować zaawansowane wersje z biblioteki. Co ważniejsze,
często możliwa jest zmiana implementacji podstawowych algorytmów. Jest to po
trzebne przede wszystkim z uwagi na to, że zbyt często programiści stykają się z cał
kowicie nowym środowiskiem obliczeniowym (sprzętem lub oprogramowaniem)
0 nowych mechanizmach, z których dawne implementacje nie korzystają w optymal
ny sposób. W książce koncentrujemy się na najprostszych sensownych implementa
cjach najlepszych algorytmów. Przykładamy szczególną wagę do kodu kluczowych
części algorytmów i pokazujemy, w których miejscach najbardziej przydatne mogą
okazać się niskopoziomowe optymalizacje.
D obór najlepszego algorytmu do konkretnego zadania może być skomplikowany
1wymagać złożonych analiz matematycznych. Gałąź nauk komputerowych zajmująca
się takim i zagadnieniami to analiza algorytmów. Dla wielu omawianych algorytmów
przez analizę wykazano doskonałą wydajność teoretyczną. O tym, że inne działają
RO ZD ZIA Ł 1 □ Podstawy
dobrze, wiadomo dzięki doświadczeniu. Głównym celem jest tu przedstawienie sen
sownych algorytmów do wykonywania ważnych zadań. Zwrócono przy tym uwagę
na porównanie wydajności metod. Nie należy używać algorytmu bez wiedzy o tym,
z jakich zasobów korzysta. Dlatego warto znać oczekiwaną wydajność algorytmów.
Podsumowanie zagadnień W ramach przeglądu opisano w tym miejscu głów
ne części książki. Przedstawiono konkretne tematy i ogólne podejście do materiału.
Omawiane zagadnienia dobrano tak, aby uwzględnić jak najwięcej podstawowych
algorytmów. Niektóre kwestie dotyczą kluczowych obszarów nauk komputerowych.
Omówiono je szczegółowo, aby przedstawić podstawowe algorytmy o wielu zasto
sowaniach. Inne opisywane algorytmy pochodzą z zaawansowanych obszarów nauk
komputerowych i powiązanych dziedzin. Rozważane algorytmy są efektem dziesię
cioleci badań i rozwoju oraz odgrywają kluczową rolę w ciągle rosnącym świecie
zastosowań obliczeń komputerowych.
Podstaw y ( r o z d z i a ł i .) W kontekście tej książki to podstawowe zasady i m eto
dyka używane do implementowania, analizowania oraz porównywania algorytmów.
Omówiono tu model programowania w Javie, abstrakcję danych, podstawowe struk
tury danych, abstrakcyjne typy danych dla kolekcji, m etody analizowania wydajności
algorytmów i studium przypadku.
Sortowanie ( r o z d z i a ł 2 .) Służą do porządkowania tablic i są niezwykle istotne.
Rozważono tu szczegółowo różnorodne algorytmy, w tym sortowanie przez wstawia
nie, sortowanie przez wybieranie, sortowanie Shella, sortowanie szyblde, sortowanie
przez scalanie i sortowanie przez kopcowanie. Przedstawiono też algorytmy dla kilku
powiązanych problemów, dotyczące kolejek priorytetowych, pobierania i scalania.
Wiele algorytmów z tego fragmentu stanowi podstawę algorytmów omawianych
w dalszej części książki.
W yszukiw anie ( r o z d z i a ł 3 .) Służące do znajdowania konkretnych elementów
w ich dużych kolekcjach, także mają podstawowe znaczenie. Omówiono tu podsta
wowe i zaawansowane m etody wyszukiwania, w tym binarne drzewa wyszukiwań,
zbalansowane drzewa wyszukiwań i haszowanie. Uwzględniono zależności między
tymi technikami i porównano ich wydajność.
Grafy ( r o z d z i a ł 4 .) To zbiory obiektów i połączeń, często z wagami i kierunkiem.
Grafy to użyteczne modele dla wielu trudnych i ważnych problemów. Projektowanie
algorytmów do przetwarzania grafów jest ważną dziedziną badań. Omówiono prze
szukiwanie w głąb, przeszukiwanie wszerz, problem określania połączeń i kilka al
gorytmów oraz aplikacji, w tym algorytmy Kruskala i Prima do wyszukiwania m ini
malnego drzewa rozpinającego oraz algorytmy Dijkstry i Bellmana-Forda do rozwią
zania problemu wyszukiwania najkrótszej ścieżki.
RO ZD ZIA Ł 1 h Podstawy
Łańcuchy zna kó w ( r o z d z i a ł 5 .) To podstawowy typ danych we współczesnych
aplikacjach. Rozważono tu wiele m etod przetwarzania ciągów znaków. Rozpoczęto
od szybszych algorytmów sortowania i wyszukiwania dla kluczy w postaci łańcu
chów znaków. Następnie rozważono wyszukiwanie podłańcuchów, dopasowy
wanie do wzorca w postaci wyrażenia regularnego i algorytmy kompresji danych.
W prowadzeniem do zaawansowanych zagadnień jest omówienie podstawowych
problemów, które są ważne same w sobie.
K ontekst ( r o z d z i a ł 6 .) Pomaga powiązać opisany w książce materiał z kilkoma
innymi zaawansowanymi obszarami badań, w tym obliczeniami naukowymi, ba
daniami operacyjnymi i teorią programowania. Omówiono symulacje oparte na
zdarzeniach, drzewa zbalansowane, tablice sufiksowe, przepływ maksymalny i inne
zaawansowane tematy. Przedstawiono je w formie przystępnej dla początkujących,
aby pozwolić docenić ciekawe zaawansowane obszary badań, w których algorytmy
odgrywają kluczową rolę. W końcowej części opisano problemy związane z wyszu
kiwaniem, redukcję i problemy NP-zupełne, aby przedstawić teoretyczne podstawy
badań algorytmów i ich związki z materiałem omówionym w książce.
s ą c i e k a w e i e k s c y t u j ą c e , ponieważ jest to nowa
b a d a n ia n a d a lg o r y t m a m i
dziedzina (prawie wszystkie analizowane algorytmy mają mniej niż 50 lat, a niektó
re wymyślono w ostatnich latach), jednak o bogatej tradycji (część algorytmów jest
znana od setek lat). Wciąż pojawiają się nowe odkrycia, przy czym nieliczne algo
rytmy są w pełni przebadane. W książce omówiono zawiłe, skomplikowane i trudne
algorytmy, a także te eleganckie, proste i łatwe. Zadanie polega na zrozumieniu tych
pierwszych i docenieniu tych ostatnich w kontekście zastosowań naukowych oraz
komercyjnych. Przy okazji omówiono różnorodne przydatne narzędzia i opraco
wano sposób myślenia algorytmicznego, który będzie pom ocny przy rozwiązywaniu
przyszłych problemów.
p r z e d s t a w i o n e t u a n a l i z y a l g o r y t m ó w są oparte na ich implementacji w postaci
programów napisanych w Javie. Podejście to zastosowano z kilku powodów:
■ Programy są zwięzłymi, eleganckimi i kompletnymi opisami algorytmów.
■ Można uruchomić programy, aby zbadać cechy algorytmów.
■ Można natychmiast wykorzystać algorytmy w aplikacjach.
Są to ważne korzyści w porównaniu ze stosowaniem opisów algorytmów w języku
polskim.
Potencjalną wadą tego podejścia jest to, że trzeba użyć specyficznego języka pro
gramowania, co może utrudnić oddzielenie istoty algorytmu od szczegółów imple
mentacji. Implementacje z książki zaprojektowano tak, aby zniwelować ten problem.
W tym celu użyto konstrukcji programistycznych dostępnych w wielu współczes
nych językach i potrzebnych do właściwego opisu algorytmu.
Zastosowano tylko mały podzbiór Javy. Choć podzbiór ten nie jest formalnie
zdefiniowany, można zauważyć, że użyto stosunkowo niewielu konstrukcji z Javy.
Skoncentrowano się za to na mechanizmach dostępnych w wielu współczesnych ję
zykach programowania. Przedstawiony kod jest kompletny. Spodziewamy się, że p o
bierzesz go i uruchomisz, wykorzystując udostępnione lub własne dane testowe.
Konstrukcje programistyczne, biblioteld oprogramowania i mechanizmy systemu
operacyjnego używane do implementowania oraz opisywania algorytmów nazwa
no modelem programowania. W tym podrozdziale i w p o d r o z d z i a l e 1.2 dokładnie
opisano ten model. Omówienie modelu jest samodzielną częścią książki, a m a służyć
przede wszystkim jako dokumentacja i źródło wiedzy pomagające zrozumieć przed
stawiony tu kod. Opisywany model zastosowano też w książce An Introduction to
Programming in Java: An Interdisciplinary Approach, gdzie material przedstawiono
w mniej skondensowanej formie.
Punktem odniesienia jest rysunek na następnej stronie, na którym przedstawio
no kompletny program w Javie, obejmujący wiele podstawowych elementów modelu
programowania. Kod ten posłuży jako przykład przy omawianiu mechanizmów ję
zyka, jednak jego szczegółowy opis znajduje się na stronie 58 (kod ten to im plemen
tacja klasycznego algorytmu, wyszukiwania binarnego, i test wykorzystujący go do
filtrowania na podstawie białej listy). Zakładamy, że prawdopodobnie rozpoznajesz
wiele mechanizmów użytych w kodzie. W uwagach znajdują się odwołania do stron,
pomagające znaleźć odpowiedzi na potencjalne pytania. Ponieważ kod napisano
w określonym stylu (przy czym starano się konsekwentnie stosować różne idiomy
i konstrukcje Javy), nawet doświadczeni programiści Javy powinni zapoznać się z in
formacjami z podrozdziału.
20
1.1 a Podstawowy model program owania
System przekazuje do mai n O wartość
argumentu - "w hi t e l i s t . t x t "
Wiersz poleceń
(strona 48 ) ' N azw ap lik u fa rgs [0 ] J
ł
% j a v a B in a r y S e a r c h l a r g e w . t x t < l a r g e T . t x t
Standardowe
wyjście - -499569 t
(strona 49) 984875 PM przekierowany
ze standardowego
wejścia (strona 52)
Anatomia programu w Javie i sposób wywoływania go z poziomu wiersza poleceń
RO ZD ZIA Ł 1 ■ Podstawy
Podstawowa struktura programu Javy Program (klasa) Javy to albo biblio
teka metod statycznych (funkcji), albo definicja typu danych. Aby utworzyć bibliotekę
m etod statycznych i definicji typu danych, należy użyć pięciu opisanych dalej ele
mentów, stanowiących podstawę programowania w Javie i wielu innych współczes
nych językach programowania. Oto te elementy:
■ Podstawowe typy danych, precyzyjnie określające znaczenie pojęć liczba całko
wita, liczba rzeczywista, wartość logiczna i innych w programie komputerowym.
Definicje obejmują zbiór możliwych wartości i operacji na nich. Operacje można
łączyć w wyrażenia, takie jak określające wartości wyrażenia matematyczne.
■ Instrukcje umożliwiają definiowanie obliczeń przez tworzenie i przypisywanie
wartości do zmiennych, kontrolowanie przebiegu wykonania lub powodowanie
efektów ubocznych. Używanych jest sześć rodzajów instrukcji: deklaracje, przy
pisania, instrukcje warunkowe, pętle, wywołania i instrukcje return.
■ Tablice umożliwiają używanie wielu wartości tego samego typu.
■ Metody statyczne pozwalają hermetyzować i ponownie wykorzystywać kod oraz
rozwijać programy jako zbiory niezależnych modułów.
■ Łańcuchy znaków to ciągi znaków. W Javie wbudowane są pewne operacje na
łańcuchach znaków.
■ Wejście i wyjście służy do komunikacji między program am i oraz ze światem
zewnętrznym.
■ Abstrakcja danych to rozwinięcie hermetyzacji i wielokrotnego użytku, um oż
liwiające definiowanie złożonych typów danych i ułatwiające programowanie
obiektowe.
W tym podrozdziale omówiono po kolei pięć pierwszych elementów Abstrakcja da
nych to temat następnego podrozdziału.
Uruchomienie program u Javy wymaga interakcji z systemem operacyjnym lub
środowiskiem programistycznym. Z uwagi na przejrzystość i zwięzłość nazywamy
takie elementy terminalem wirtualnym, w którym można komunikować się z progra
mami przez wpisywanie poleceń dla systemu. W witrynie można znaleźć inform a
cje o korzystaniu z term inala wirtualnego w używanym systemie lub o stosowaniu
jednego z wielu bardziej zaawansowanych środowisk programistycznych dostępnych
dla współczesnych systemów.
Program BinarySearch obejmuje dwie m etody statyczne — rank() i main().
Pierwsza, rank (), zawiera cztery instrukcje: dwie deklaracje, pętlę (która sama obej
muje przypisanie i dwie instrukcje warunkowe) oraz instrukcję return. Druga metoda,
mai n (), składa się z trzech instrukcji: deklaracji, wywołania i pętli (obejmującej przy
pisanie oraz instrukcję warunkową).
Aby wywołać program Javy, należy najpierw skompilować go za pomocą polecenia
ja vac, a następnie uruchomić, używając polecenia java. Przykładowo, aby uruchomić
program Bi narySearch, trzeba najpierw wprowadzić polecenie javac Bi narySearch.
java. Powoduje ono utworzenie pliku [Link], zawierającego niskopozio-
mową wersję programu w kodzie bajtowym Javy w pliku [Link]. Następnie
1.1 n Podstawowy model program owania
należy wpisać polecenie java Bi narySearch (po którym następuje nazwa pliku z białą
listą), aby przekazać sterowanie do wersji programu w kodzie bajtowym. Aby zrozu
mieć skutki tych działań, warto szczegółowo rozważyć proste typy danych, wyrażenia,
różnego rodzaju instrukcje Javy, tablice, metody statyczne, łańcuchy znaków i operacje
wejścia-wyjścia.
Proste typy danych i wyrażenia Typ danych to zbiór wartości i operacji na
tych wartościach. Zacznijmy od przyjrzenia się czterem poniższym prostym typom
danych, stanowiącym podstawę języka Java:
■ Liczby całkowite i operacje arytmetyczne (i nt).
■ Liczby rzeczywiste i operacje arytmetyczne (doubl e).
■ Wartości logiczne — zbiór wartości { true, false } z operacjam i logicznymi
(bool ean).
■ Znaki — znaki alfanumeryczne i wprowadzane symbole (char).
Rozważmy teraz mechanizmy podawania wartości i operacje dla tych typów.
Program Javy manipuluje zmiennymi, których nazwy to identyfikatory. Każda
zmienna jest określonego typu danych i przechowuje jedną z wartości dozwolonych
dla danego typu. W kodzie Javy wyrażenia (podobne do wyrażeń matematycznych)
służą do stosowania operacji powiązanych z każdym typem. Dla typów prostych do
wskazywania zmiennych służą identyfikatory, do określania operacji służą symbole
operatorów, na przykład +, -, * i /, wartościami są literały, takie jak 1 lub 3.14, a ope
racjami na wartościach — wyrażenia w rodzaju (x + 2.236) /2. Wyrażenie definiuje
jedną z wartości typu danych.
Pojęcie Przykłady Definicja
Zbiór wartości i operacji na nich
-jnt d o u b le boole an c h a r
typ danych (wbudowany w język Java)
Ciąg liter, cyfr i symboli _ oraz $,
Identyfikator a abc Ab$ a b abl23 lo hi przy czym pierwszym znakiem nie
może być cyfra
Zmienna [dowolny identyfikator] Nazwy wartości typu danych
Operator + - * / Nazwa operacji dla typu danych
int 1 0 -42
double 2.0 1.0e-15 3.14 Reprezentacja wartości w kodzie
Literał boolean true f a l s e źródłowym
char ' a ' ' + ' ' 9 ' 1\ n 1
Wyrażenie int lo + (hi - lo )/2 Literał, zmienna lub określający
double 1.0e-15 * t
boolean
wartość ciąg operacji na literałach
lo <= hi
i (lub) zmiennych
Podstawowe cegiełki programów Javy
RO ZD ZIA Ł 1 n Podstawy
Aby zdefiniować typ danych, trzeba tylko określić wartości i zbiór wykonywanych
na nich operacji. Informacje te podsumowano w tabeli dla typów i nt, doubl e, bool ean
i char Javy. Typy te są podobne do prostych typów danych z wielu języków progra
mowania. Dla typów i nt i doubl e operacjami są standardowe operacje arytmetyczne.
Dla typu bool ean są to znane operacje logiczne. Należy zauważyć, że operatory +, -,
* i / są przeciążone. Ten sam symbol, w zależności od kontekstu, określa operacje
dla wielu różnych typów. Kluczową cechą podstawowych operacji jest to, że operacja
obejmująca wartości danego typu daje wartość tego samego typu. Reguła ta podkreśla
fakt, że często używane są wartości przybliżone, ponieważ dokładna wartość wyraże
nia nie należy do danego typu. Na przykład 5/3 ma wartość 1, a 5 .0 /3 .0 — wartość
bardzo zbliżoną do 1.66666666666667, natomiast żadne z tych wyrażeń nie jest rów
ne 5/3. Tabela jest bardzo niekompletna. W pytaniach i odpowiedziach w końcowej
części podrozdziału przedstawiono pewne dodatkowe operatory i różne wyjątkowe
sytuacje, które czasem trzeba uwzględnić.
Typowe wyrażenia
Typ Zbiór wartości Operatory
Wyrażenie Wartość
Liczby całkowite + (dodawanie)
5 + 3 8
od - 231 do +231 - 1 - (odejmowanie) 5 - 3 2
int (32-bitowe * (mnożenie) 5 * 3 15
z uzupełnieniem 5 /3 1
/ (dzielenie) 9.
3E 'o
9^ 0Q L
dwójkowym) % (reszta)
Liczby rzeczywiste + (dodawanie) 3.141 - .03 3.111
o podwójnej precyzji - (odejmowanie) 2.0 - 2.0e-7 1.9999998
double
(64-bitowe zgodne ze * (mnożenie) 100 * .015 1.5
6.02e23 / 2.0 3.01e23
standardem IEEE 754) / (dzielenie)
&& (i) true & & false false
II (lub) f a l s e || true tru e
tru e lub fal se !f a l s e true
! (nie)
tru e ~ tru e fal se
A (xor)
char Znaki (16-bitowe) [operacje arytmetyczne, rzadko stosowane]
Proste typy danych w Javie
1.1 ■ Podstawowy m odel program owania
W yrażenia Jak przedstawiono to w ostatniej tabeli, typowe wyrażenia są infiksowe.
Obejmują literał, po którym następuje operator i inny literał (lub kolejne wyrażenie).
Jeśli wyrażenie zawiera więcej niż jeden operator, często znaczenie ma kolejność ich
występowania, dlatego opisane dalej priorytety operatorów są częścią specyfikacji
Javy. O peratory * i / mają wyższy priorytet (są stosowane wcześniej) niż + i W śród
operatorów logicznych najwyższy priorytet ma !, a następnie &&i 11. Operatory o ta
kim samym priorytecie są zwykle stosowane od lewej do prawej. Podobnie jak w stan
dardowych wyrażeniach arytmetycznych m ożna użyć nawiasów do zmodyfikowania
reguł. Ponieważ priorytety są nieco inne w poszczególnych językach, w książce uży
wamy nawiasów i staramy się unikać zależności od reguł.
Konwersja typów Liczby są automatycznie przekształcane na pojemniejszy typ,
jeśli nie prowadzi to do utraty informacji. Na przykład w wyrażeniu 1 + 2.5 war
tość 1 jest przekształcana na liczbę typu doubl e, 1 . 0, a wynik wyrażenia to wartość
3.5 typu doubl e. Rzutowanie polega na podaniu w wyrażeniu nazwy typu w nawia
sach. Jest to żądanie przekształcenia podanej dalej wartości na wartość danego typu.
Na przykład (i nt ) 3.7 to 3, a (double) 3 to 3.0. Zauważmy, że rzutowanie na typ i nt
powoduje odcięcie części ułamkowej, a nie zaokrąglenie wartości. Reguły rzutowa
nia w skomplikowanych wyrażeniach bywają złożone. Rzutowanie należy stosować
rzadko i ostrożnie. Najlepiej jest stosować wyrażenia obejmujące literały lub zmienne
jednego typu.
Porów nania Podane tu operatory porównują dwie wartości tego samego typu i zwra
cają wartość typu bool ean. Oto te operatory: równość (==), nierówność (! =), mniejszy
niż (<), mniejszy lub równy (<=), większy niż (>) i większy lub równy (>=). Są to tak
zwane operatory typu mieszanego, ponieważ ich wartość jest typu bool ean, a nie typu
porównywanych wartości. Wyrażenie o wartości typu bool ean to wyrażenie logiczne.
Takie wyrażenia są najczęściej elementami instrukcji warunkowych lub pętli.
Inne typ y proste Typ i nt w Javie ma 232 wartości, dlatego można go reprezentować
w maszynach ze słowami 32-bitowymi (wiele maszyn ma obecnie słowa 64-bitowe,
jednak wciąż stosuje się 32-bitowy typ i nt). Standardowo typ doubl e ma reprezen
tację 64-bitową. Rozmiary tych typów danych są odpowiednie dla typowych aplika
cji korzystających z liczb całkowitych i rzeczywistych. Aby zapewnić elastyczność,
w Javie udostępniono pięć dodatkowych prostych typów danych:
■ 64-bitowe liczby całkowite z operacjami arytmetycznymi (1 ong),
■ 16-bitowe liczby całkowite z operacjami arytmetycznymi (short),
■ 16-bitowe znaki z operacjami arytmetycznymi (char),
■ 8 -bitowe liczby całkowite z operacjami arytmetycznymi (byte),
■ 32-bitowe liczby rzeczywiste o pojedynczej precyzji z operacjami arytmetycz
nymi (float).
W książce najczęściej używane są operacje arytmetyczne typów i nt i doubl e, dlatego
nie omówiono szczegółowo pozostałych (bardzo podobnych) typów.
RO ZD ZIA Ł 1 o Podstawy
Instrukcje Program Javy składa się z instrukcji, w których można zdefiniować
obliczenia przez tworzenie zmiennych i manipulowanie nimi, przypisywanie do nich
wartości określonych typów danych i kontrolowanie wykonywania takich operacji.
Instrukcje są często uporządkowane w bloki, czyli ciągi instrukcji w nawiasach klam
rowych.
■ Deklaracje tworzą zmienne danego typu i określają ich nazwę w postaci iden
tyfikatora.
° Przypisania łączą wartość określonego typu (zdefiniowaną w wyrażeniu) ze
zmienną. Java obsługuje też kilka idiomów przypisania niejawnego, służących
do modyfikowania wartości typu danych względem ich bieżącego stanu, na
przykład przez zwiększenie wartości zmiennej całkowitoliczbowej.
■ Instrukcje warunkowe umożliwiają prostą zmianę przebiegu wykonania pro
gramu — uruchomienie instrukcji z jednego z dwóch bloków w zależności od
podanego warunku.
■ Pętle służą do bardziej rozbudowanej zmiany przebiegu wykonania programu
— uruchamiania instrukcji z bloku dopóty, dopóki dany warunek jest spełniony.
D Wywołania i instrukcje retu rn dotyczą m etod statycznych (strona 34), które sta
nowią inny sposób modyfikowania przebiegu wykonania program u i porząd
kowania kodu.
Program to ciąg instrukcji — deklaracji, przypisań, instrukcji warunkowych, pęt
li, wywołań i instrukcji return. Programy mają zwykle strukturę zagnieżdżoną.
Instrukcja w bloku w instrukcji warunkowej lub pętli sama może być instrukcją wa
runkową lub pętlą. Na przykład pętla while w metodzie rank() obejmuje instrukcję
i f. Dalej omówiono po kolei każdy rodzaj instrukcji.
Deklaracje Deklaracja łączy nazwę zmiennej z typem na etapie kompilacji. Java wy
maga stosowania deklaracji do określania nazw i typów zmiennych. W ten sposób
można bezpośrednio opisać obliczenia. Java jest językiem ze ścisłą kontrolą typów,
ponieważ kompilator Javy sprawdza ich zgodność (na przykład nie zezwala na p o
mnożenie przez siebie wartości typów bool ean i doubl e). Deklaracje mogą występo
wać w dowolnym miejscu przed pierwszym użyciem zmiennej. Najczęściej podaje się
je w miejscu pierwszego użycia. Zasięg zmiennej to fragment programu, w którym
zmienna jest zdefiniowana. Ogólnie zasięg zmiennej to instrukcje następujące po jej
deklaracji w bloku z tą deklaracją.
Przypisania Instrukcja przypisania łączy wartość typu danych (zdefiniowaną za
pomocą wyrażenia) ze zmienną. Zapis c = a + b w Javie nie oznacza równości m a
tematycznej, ale działanie — ustawienie wartości zmiennej c na wartość zmiennej
a plus wartość zmiennej b. To prawda, że c bezpośrednio po wykonaniu przypisa
nia matematycznie równa się a + b, jednak celem instrukcji jest zmiana wartości c
(jeśli jest to konieczne). Po lewej stronie instrukcji przypisania musi znajdować się
pojedyncza zmienna. Po prawej stronie m ożna umieścić dowolne wyrażenie dające
wartość danego typu.
1.1 0 Podstawowy model program owania
Instrukcje w arunkow e Większość obliczeń wymaga podjęcia różnych działań
w zależności od danych wejściowych. Jednym ze sposobów na wyrażenie tych różnic
w Javie jest instrukcja i f :
i f (<wyrażenie logiczne>) { < instrukcje w bloku> }
Zastosowano tu formalny zapis nazywany szablonem, używany w niektórych m iej
scach do przedstawiania formatu konstrukcji Javy. W nawiasach ostrych, <>, umiesz
czona jest zdefiniowana już konstrukcja; m ożna wykorzystać jej dowolny egzemplarz.
Tu <wyrażeni e 1ogi czne> to wyrażenie o wartości logicznej, na przykład obejmujące
porównanie, a <i n stru k cje w bl oku> to ciąg instrukcji Javy. Można formalnie zde
finiować <wyrażenie logiczne> i < in stru k cje w bl oku>, jednak unikamy aż tak
szczegółowych opisów. Znaczenie instrukcji i f jest oczywiste — instrukcje w blo
ku są wykonywane wtedy i tylko wtedy, jeśli wartość wyrażenia logicznego to true.
Instrukcja i f-e l se:
i f («wyrażenie logiczne>) { « in stru k cje w bloku> }
else { « in stru k cje w bloku> }
umożliwia wybór między dwoma różnymi blokami instrukcji.
Pętle Wiele obliczeń z natury się powtarza. Podstawowa konstrukcja Javy służąca do
obsługi takich obliczeń ma następujący format:
while («wyrażenie logiczne>) ( « in stru k cje w bloku> }
Instrukcja whi 1e ma tę samą postać, co instrukcja i f (jedyną różnicą jest użycie słowa
kluczowego whi 1e zamiast i f), ale odm ienne znaczenie. Jest to instrukcja informują
ca komputer, aby działał w następujący sposób: jeśli wyrażenie logiczne ma wartość
fal se, nie trzeba nic robić. Jeżeli wyrażenie to ma wartość true, trzeba wykonać ciąg
instrukcji z bloku (podobnie jak dla i f ), a następnie ponownie sprawdzić wyrażenie
logiczne. Jeśli ma wartość true, należy jeszcze raz uruchomić ciąg instrukcji z blo
ku i kontynuować ten proces dopóty, dopóki wyrażenie logiczne ma wartość true.
Instrukcje w bloku pętli nazywane są ciałem pętli.
Instrukcje break i continue W niektórych sytuacjach potrzebne jest nieco bardziej
skomplikowane sterowanie wykonaniem, niż umożliwiają to instrukcje i f i while.
Java udostępnia dwie dodatkowe instrukcje do użytku w pętlach whi 1e:
■ Instrukcję break, która powoduje natychmiastowe wyjście z pętli.
■ Instrukcję conti nue, która natychmiast rozpoczyna kolejną iterację pętli.
W kodzie z książki rzadko korzystamy z tych instrukcji (wielu programistów nigdy
ich nie używa), jednak w pewnych sytuacjach pozwalają one znacznie uprościć kod.
RO ZD ZIA Ł 1 a Podstawy
Zapis skrócony Istnieje kilka sposobów na wyrażenie pewnych obliczeń. Celem
jest napisanie przejrzystego, eleganckiego i wydajnego kodu. W takim kodzie często
występują powszechnie stosowane skróty (są one dostępne w wielu językach, nie tyl
ko w Javie).
Deklaracje inicjujące Można połączyć deklarację z przypisaniem, aby zainicjować
zmienną w miejscu jej deklaracji (tworzenia). Na przykład kod in t i = 1; two
rzy zmienną i nt o nazwie i oraz przypisuje jej początkową wartość 1. Najlepszym
rozwiązaniem jest stosowanie tego mechanizmu blisko miejsca pierwszego użycia
zmiennej (w celu ograniczenia jej zasięgu).
P rzypisania niejaw ne Jeśli celem jest zmiana wartości zmiennej względem jej obec
nej wartości, m ożna stosować następujące skróty:
■ Operatory inkrementacji i dekrementacji: i ++ oznacza to samo, co i = i + 1 ,
oraz ma wartość i w wyrażeniu. Podobnie i -- to odpowiednik wyrażenia i =
i - 1. Instrukcje ++i oraz - - i działają tak samo, jednak w wyrażeniu używana
jest wartość po inkrementacji lub dekrementacji, a nie sprzed tych operacji.
■ Inne operacje złożone: dodanie operatora binarnego przed = w przypisaniu to
odpowiednik użycia zmiennej podanej po lewej stronie jako pierwszego ope-
randu. Na przykład instrukcja i /=2; to odpowiednik kodu i = i/2 ;. Warto
zauważyć, że i += 1 ; ma ten sam efekt, co i = i+ 1 ; (oraz i++).
Bloki z jed n ą instrukcją Jeśli blok w instrukcji warunkowej lub pętli obejmuje tylko
jedną instrukcję, można pominąć nawiasy Idamrowe.
N otacja fo r Wiele pętli działa tak: zmienna indeksująca inicjowana jest pewną war
tością, a następnie pętla while sprawdza oparty na zmiennej warunek kontynuacji
pętli, przy czym ostatnia instrukcja w pętli whi 1e zwiększa wartość zmiennej. Taką
pętlę można zapisać zwięźle za pomocą notacji fo r Javy:
fo r (<inicjowanie>; «wyrażenie logiczne>; <inkrementacja>)
{
« in stru k cje w bloku>
}
Kod ten jest — z nielicznymi wyjątkami — odpowiednikiem poniższego:
<inicjowanie>;
while («wyrażenie logiczne>)
{
« in stru k cje w bloku>
<i nkrementacja>;
}
Pętle fo r wykorzystano tu w idiomie programistycznym „zainicjuj i inkrem entuj”.
1.1 a Podstaw ow y model program owania
Instrukcja Przykłady Definicja
Deklaracja i nt i ; Tworzenie zmiennej określonego
double c;
typu, nazwanej za pomocą
podanego identyfikatora
Przypisanie a = b + 3; Przypisywanie wartości typu
dis crim in a n t = b*b - 4.0*c;
danych do zmiennej
Deklaracja i n t i = 1; Deklaracja, która ponadto
inicjująca double c = 3.141592625;
powoduje przypisanie wartości
początkowej
Przypisanie i++; i = i + 1;
niejawne i += 1;
Instrukcja i f (x < 0) x = -x; Wykonywanie instrukcji
warunkowa w zależności od wartości
if
wyrażenia logicznego
Instrukcja i f (x > y) max = x; Wykonywanie jednej lub drugiej
warunkowa else max = y;
instrukcji w zależności od
if-e lse
wartości wyrażenia logicznego
Pętla whi 1e in t v = 0; Wykonywanie instrukcji dopóty,
while (v <= N)
dopóki wyrażenie logicznie nie
v = 2*v;
double t = c; przyjmie wartości fal se
while ([Link] - c/t) > le -1 5 *t )
t = (c/t + t) / 2.0;
Pętla f o r f o r (i n t 1 = 1; i <= N; 1++) Zwięzła wersja instrukcji whi 1 e
sum += 1 . 0 / i ;
f o r ( i n t i = 0 ; i <= N; i++)
[Link](2*M [Link]*i/N );
Wywołanie i n t key = S t d l n . r e a d l n t ( ) ; Wywoływanie innych metod
(strona 34)
Instrukcja return f a l s e ; Zwracanie wartości przez metodę
return (strona 36)
Instrukcje języka Java
RO ZD ZIA Ł 1 n Podstawy
Tablice Tablica przechowuje kolekcję wartości tego samego typu. Służy nie tylko
do przechowywania wartości, ale też zapewnia dostęp do każdej z nich. Aby możliwe
było wskazywanie poszczególnych wartości w tablicy, są one numerowane. Następnie
można użyć ich indeksu. Jeśli istnieje N wartości, m ożna przyjąć, że mają num ery od
0 do N - 1. W kodzie Javy można jednoznacznie określić dowolną wartość, używając
zapisu a [i] , aby wskazać i-tą wartość dla dowolnego i z przedziału od 0 do N-l.
Ta konstrukcja Javy to tablica jednowymiarowa.
Tworzenie i inicjowanie tablic Przygotowanie tablicy w programie w Javie wymaga
wykonania trzech odrębnych operacji:
° zadeklarowania nazwy i typu tablicy,
a utworzenia tablicy,
° zainicjowania wartości tablicy.
Aby zadeklarować tablicę, należy podać jej nazwę i typ przechowywanych danych.
W celu utworzenia tablicy trzeba określić jej długość (liczbę wartości). Na przykład
długi zapis widoczny po prawej stro
Długi zapis _
nie tworzy tablicę N liczb typu do Deklaracja
d o u b le [] a; " Tworzenie
uble, a każda z nich inicjowana jest
a = new double[N ];
wartością 0.0. Pierwsza instrukcja to
f o r ( i n t i = 0 ; i < N; i++)
deklaracja tablicy. Przypom ina ona a [i] = 0 .0 ;
deklarację zmiennej tego samego Inicjowanie
Krótki zapis
typu prostego, jednak różni się n a
wiasami kwadratowymi występują d o u b le j] a = new do u b le[N ];
cymi po nazwie typu. Oznaczają one,
że jest to deklaracja tablicy. Słowo Deklaracja inicjująca
kluczowe new w drugiej instrukcji to i n t [ ] a = { 1, 1, 2, 3, 5, 8 };
dyrektywa Javy służąca do tworzenia Deklarowanie, tworzenie i inicjowanie tablicy
tablic. Tablice trzeba bezpośrednio
tworzyć w czasie wykonywania program u, ponieważ w czasie kompilacji kom pi
lator Javy nie wie, ile miejsca ma zarezerwować na tablicę (co jest możliwe dla
wartości typów prostych). Instrukcja fo r inicjuje N wartości tablicy. Kod ustawia
wszystkie elementy tablicy na wartość 0.0. Przy pisaniu kodu używającego tablicy
trzeba mieć pewność, że ją zadeklarowano, utworzono i zainicjowano. Pominięcie
jednego z tych kroków jest częstym błędem programistycznym.
K rótki zapis Aby pisać bardziej zwięzły kod, często wykorzystuje się domyślny
sposób inicjowania tablic w Javie, który łączy wszystkie trzy kroki w jednej in
strukcji, tak jak w krótkim zapisie w przykładzie. Kod po lewej stronie znaku rów
ności stanowi deklarację. Kod po prawej tworzy tablicę. Pętla fo r jest tu zbędna,
ponieważ domyślną wartością początkową zmiennych typu double w tablicach
Javy jest 0.0. Pętla jest natom iast potrzebna, jeśli pożądana jest wartość niezerowa.
Domyślna wartość początkowa wynosi 0 dla typów liczbowych i fa ls e dla typu
1.1 Ei Podstawowy model program owania
boolean. Trzecia możliwość przedstawiona w przykładzie polega na inicjowaniu
wartości w czasie kompilacji. W tym celu należy podać rozdzielone przecinkam i
literały w nawiasach klamrowych.
Używanie tablicy Typowy kod do przetwarzania tablic pokazano na stronie 33.
Po zadeklarowaniu i utworzeniu tablicy można wskazać dowolną wartość wszędzie
tam, gdzie dozwolona jest nazwa zmiennej. W tym celu po nazwie tablicy należy po
dać indeks całkowitoliczbowy w nawiasach klamrowych. Tablica po utworzeniu ma
stały rozmiar. W programie m ożna sprawdzić długość tablicy a [] za pomocą kodu
[Link] g th . Ostatni element tablicy a[] to zawsze a [a .le n g th - l] . Java przeprowadza
automatyczne sprawdzanie zakresu. Jeśli programista utworzył tablicę o rozmiarze
N i używa indeksu o wartości mniejszej niż 0 lub większej niż N-l, program zgłasza
wyjątek czasu wykonania ArrayOutOfBoundsExcepti on.
Utożsam ianie nazw (ang. aliasing) Warto zapamiętać, że nazwa tablicy dotyczy jej
całej. Przypisanie jednej nazwy tablicy do drugiej powoduje, że obie zmienne będą
oznaczać tę samą tablicę, co przedstawiono w poniższym fragmencie kodu.
i nt [] a = new i nt [N];
a [ i ] = 1234;
i nt [] b = a;
b [i] = 5678; / / a [ i ] j e s t tera z równe 5678.
To zjawisko to utożsamianie nazw i może prowadzić do trudnych do wykrycia błę
dów. Jeśli programista chce utworzyć kopię tablicy, musi zadeklarować, utworzyć
i zainicjować nową tablicę, a następnie skopiować wszystkie elementy z pierwotnej
tablicy do nowej, tak jak w trzecim przykładzie na stronie 33.
Tablice dw uw ym iarow e Tablica dwuwymiarowa w Javie to tablica tablic jednowy
miarowych. Tablica dwuwymiarowa może być nierówna (tablice w niej mogą mieć
różną długość). Jednak najczęściej stosuje się — dla odpowiednich param etrów M
i N — tablice dwuwymiarowe M na N, składające się z M wierszy, z których każ
dy jest tablicą o długości N (dlatego można stwierdzić, że tablica ma N kolumn).
Rozbudowanie tablic Javy tak, aby obsługiwały tablice dwuwymiarowe, jest proste.
W celu wskazania elementu w wierszu i oraz kolumnie j dwuwymiarowej tablicy
a[] [] należy użyć zapisu a [i] [ j ] . Zadeklarowanie tablicy dwuwymiarowej wymaga
dodania następnej pary nawiasów kwadratowych, a przy tworzeniu takiej tablicy po
nazwie typu należy określić liczbę wierszy, a następnie liczbę kolumn w nawiasach
kwadratowych:
d o u b l e j ] [] a = new d o u b le [M ] [ N ] ;
RO ZD ZIA Ł 1 s Podstawy
Jest to tablica M na N. Tradycyjnie pierwszy wymiar określa liczbę wierszy, a drugi
— liczbę kolumn. Podobnie jak w tablicach jednowymiarowych Java inicjuje wszyst
kie elementy typów liczbowych zerem, a elementy typu bool ean — wartością fal se.
Domyślne inicjowanie tablic dwuwymiarowych jest przydatne, ponieważ pozwala
pominąć więcej kodu niż przy tablicach jednowymiarowych. Poniższy kod to odpo
wiednik opisanego wcześniej jednowierszowego idiomu „utwórz i zainicjuj”:
doublet] [] a;
a = new double[M] [N];
fo r (in t i = 0 ; i < M; i++)
fo r (in t j = 0; j < N; j++)
a [ i] [ j ] = 0.0;
Ten kod jest zbędny przy inicjowaniu zerem, zagnieżdżone pętle fo r są jednak p o
trzebne przy inicjowaniu innymi wartościami.
1.1 ° Podstawowy m odel program owania
Zadanie Implementacja (fragment kodu)
Wyszukiwanie maksymalnej doubl e max = a [0];
wartości w tablicy f o r (i n t i = 1; i < [Link] ngth; i++)
i f (a [i] > max) max = a [i ];
Obliczanie średniej i n t N = [Link];
dla wartości z tablicy double sum = 0.0;
f o r ( i n t i = 0; i < N; 1++)
sum += a [ i ] ;
double average = sum / N;
Kopiowanie do innej tablicy i n t N = [Link];
doublet] b = new double[N];
f o r (i n t i = 0; i < N; i++)
b [ i ] = a [i ] ;
Odwracanie kolejności i n t N = [Link] h;
elementów w tablicy f o r (i n t i = 0; i < N/2; 1++)
{
double temp = a [i ] ;
a [i] = a [ N - l - i ];
a [ N - i -1] = temp;
}
Mnożenie macierzy i n t N = [Link] h;
kwadratowych doublet] [] c = new double[N] [N ];
f o r ( i n t i = 0; i < N; i++ )
a [ ][ ]* b [][ ] - c[][] f o r (i n t j = 0 ; j < N; j++)
{ // O b licza nie iloc zy n u skalarnego wiersza
i oraz
// kolumny j .
f o r (i n t k = 0; k < N; k++)
c [ i ] [ j ] + - a [ i ] [ k ]*b [k ] [ j ] ;
}
Typowy kod do przetwarzania tablic
34 R O ZD ZIA Ł 1 ■ Podstawy
Metody statyczne Każdy program Javy w tej książce jest albo definicję typu da
nych (opisano je szczegółowo w p o d r o z d z ia l e 1 .2 ), albo bibliotekę metod statycz
nych (przedstawionych w tym miejscu). Metody statyczne w wielu językach są nazy
wane funkcjami, ponieważ mogą działać jak funkcje matematyczne, co omówiono
dalej. Każda metoda statyczna to ciąg instrukcji wykonywanych jedna po drugiej po
wywołaniu metody statycznej. Określenie statyczne pozwala odróżnić te metody od
metod egzemplarza (ang. instance method), przedstawionych w p o d r o z d z ia l e 1 .2 .
Słowo metoda bez dookreślenia jest używane przy opisie cech wspólnych dla metod
obu rodzajów.
Definiowanie m etody statycznej W metodzie ukrywa się obliczenia zdefiniowane za
pomocą ciągu instrukcji. Metoda przyjmuje argumenty (wartości określonych typów
danych) i na ich podstawie albo oblicza wartość zwracaną (przypomina to obliczanie
wartości za pomocą funkcji matematycznej), albo powoduje efekty uboczne (na przykład
wyświetla wartość). Metoda statyczna
Svanatura Typ zwracanej Argument rank() w programie BinarySearch to
Sygnatura wartości Nazwa Typ a 1
metody argumentu ^ przykład metody pierwszego rodzaju;
mai n () to metoda drugiego typu. Każda
p u b l i c s t a t i c Id o u b le lls a r t l ( Id o u b le cTTj metoda statyczna składa się z sygnatury
{ _____________________________ (słów kluczowych public s ta tic , po
i f (c < 0) return [Link];
Zmienne których następuje typ zwracanej war
d ou b le e r r = le - 1 5 ;
lokalne — —------- , tości, nazwa metody i ciąg argumentów
I d o u b l e 1 1= c;_______________
C ia ło w h i l e | ( M a t h . a b s ( t - c/ t)| > e r r * t ) — każdy z zadeklarowanym typem)
metody t = (c/ t + t ) / 2 .0; i ciała (bloku z instrukcjami — ciągu
r e t u r n tT] \
instrukcji umieszczonych w nawiasach
} Wywołanie innej metody
Instrukcja return
klamrowych). Przykładowe metody
statyczne przedstawiono w tabeli na
Anatomia metody statycznej
stronie obok.
W yw oływ anie m etod statycznych Wywołanie m etody wymaga podania jej nazwy
i wyrażeń z rozdzielonymi przecinkami wartościami argumentów w nawiasach. Jeśli
wywołana metoda jest częścią wyrażenia, oblicza wartość, która jest używana w wy
rażeniu w miejscu wywołania. Na przykład wywołanie m etody rank() w programie
BinarySearch powoduje zwrócenie wartości typu in t. Samo wywołanie metody,
po którym następuje średnik, to instrukcja. Zwykle powoduje ona efekty uboczne.
Na przykład wywołanie A rray [Link] rt() w metodzie main() programu BinarySearch
to wywołanie m etody systemowej A rrays. s o rt (), której efektem ubocznym jest po
sortowanie elementów tablicy. Przy wywoływaniu m etody argumenty są inicjowa
ne wartościami z odpowiedniego wyrażenia z wywołania. Instrukcja return kończy
działanie metody statycznej i powoduje zwrócenie sterowania do jednostki wywołu
jącej. Jeśli m etoda statyczna oblicza wartość, trzeba ją podać w instrukcji retu rn
(jeżeli metoda statyczna może dojść do końca ciągu instrukcji bez napotkania in
strukcji return, kompilator zgłasza błąd).
1.1 a Podstawowy model program owania
Zadanie Implementacja
Wartość bezwzględna p ublic s t a t i c in t a b s ( i n t x)
dla typu i nt {
i f (x < 0) return -x;
else retu rn x;
}
Wartość bezwzględna pub lic s t a t i c double abs(double x)
dla typu double (
i f (x < 0.0) return -x;
else return x;
}
Sprawdzanie, czy liczba pub lic s t a t i c boolean i s P r i m e ( i n t N)
jest pierwsza {
i f (N < 2) re tu rn f a l s e ;
f o r ( i n t i = 2; i * i <= N; i++)
i f (N % i == 0) return f a l s e ;
retu rn true;
Obliczanie pierwiastka p ub lic s t a t i c double s q rt(d ouble c)
kwadratowego {
i f (c > 0) re tu rn [Link];
(metodą Newtona)
double e r r » le -15;
double t = c;
while ([Link](t - c/t) > e r r * t)
t = (c/t + t) / 2.0;
return t;
}
Przeciwprostokątna p ublic s t a t i c double hypotenuse(double a, double b)
trójkąta prostokątnego { return M a th .sq rt(a *a + b*b) ; }
Liczby harmoniczne p ub lic s t a t i c double H (in t N)
(strona 197) {
double sum = 0.0;
f o r (i n t i = 1; i <= N; 1++)
sum += 1.0 / i ;
retu rn sum;
Typowe implementacje metod statycznych
RO ZD ZIA Ł 1 a Podstawy
Cechy m etod Kompletny, szczegółowy opis cech m etod wykracza poza zakres książ
ki, warto jednak zwrócić uwagę na następujące kwestie:
■ Argumenty są przekazywane przez wartość. Argumentów m ożna używać w do
wolnym miejscu w ciele m etody w taki sam sposób, jak zmiennych lokalnych.
Jedyna różnica między argumentami a zmiennymi lokalnymi polega na tym, że
argumenty są inicjowane wartościami podanymi w wywołaniu. M etoda działa
na podstawie wartości argumentów, a nie przy użyciu ich samych. Jedną z kon
sekwencji takiego stanu rzeczy jest to, że zmiana wartości argumentu w m eto
dzie statycznej nie m a wpływu na kod wywołujący metodę. Ogólnie w kodzie
w książce wartość argumentów nie jest modyfikowana. Przekazywanie przez
wartość powoduje, że argumenty w postaci tablicy są używane jak nazwa za
stępcza (strona 31). M etoda używa argumentu do wskazywania tablicy z miejsca
wywołania i może zmienić jej zawartość (choć nie modyfikuje samej tablicy).
Na przykład wywołanie A rrays. s o rt () zmienia zawartość tablicy przekazanej
jako argument — powoduje uporządkowanie jej elementów.
■ Nazwy metod można przeciążać. W bibliotece Math Javy podejście to zastoso
wano do udostępnienia implementacji metod M [Link](), [Link]() i Math,
max () dla wszystkich prostych typów liczbowych. Innym częstym zastosowa
niem przeciążania jest definiowanie dwóch różnych wersji funkcji, z których
jedna przyjmuje argument, a druga używa wartości domyślnej argumentu.
■ Metoda zwraca jedną wartość, ale może zawierać wiele instrukcji return. Metoda
Javy może zwracać tylko jedną wartość, o typie zadeklarowanym w sygnatu
rze. Sterowanie wraca do programu wywołującego bezpośrednio po dojściu do
pierwszej instrukcji return w metodzie statycznej. Instrukcje retu rn można
umieścić w dowolnym miejscu. Choć czasem istnieje wiele instrukcji return,
każda metoda statyczna w każdym wywołaniu zwraca tylko jedną wartość —
podaną po pierwszej napotkanej instrukcji return.
■ Metody mogą powodować efekty uboczne. W metodzie można podać słowo
kluczowe void jako typ zwracanej wartości. Oznacza to, że m etoda nie zwraca
wartości. W metodach statycznych tego rodzaju nie trzeba bezpośrednio uży
wać instrukcji return. Sterowanie wraca do miejsca wywołania po ostatniej in
strukcji. Metoda statyczna typu voi d powoduje efekty uboczne (przyjmuje dane
wejściowe, generuje dane wyjściowe, modyfikuje elementy tablicy lub w inny
sposób zmienia stan systemu). Na przykład typ zwracanej wartości w metodzie
statycznej mai n () w programach w książce to voi d, ponieważ jej zadaniem jest
generowanie danych wyjściowych. Technicznie m etody typu void nie działają
jak funkcje matematyczne (to samo dotyczy funkcji [Link](), która nie
przyjmuje argumentów, ale generuje zwracaną wartość).
Metody egzemplarza, będące tematem p o d r o z d z i a ł u 2 . 1 , też mają te cechy, choć
znacznie różnią się w obszarze efektów ubocznych.
1.1 E Podstawowy m odel program owania
Rekurencja Metoda może wywoływać samą siebie (jeśli nie znasz dobrze techniki
rekurencji, zachęcamy do wykonania ć w i c z e ń od 1 . 1.16 do 1 . 1 .22 ). Kod w dolnej czę
ści tej strony to odm ienna implementacja metody rank() z program u Bi narySearch.
Często stosujemy rekurencyjne implementacje metod, ponieważ pozwala to tworzyć
zwięzły, elegancki kod, łatwiejszy do zrozumienia niż równoważna implementacja,
w której nie wykorzystano rekurencji. W komentarzach do wspomnianej implemen
tacji znajduje się krótki opis działania kodu. Na podstawie tego komentarza m oż
na — za pom ocą indukcji matematycznej — udowodnić, że kod działa poprawnie.
W p o d r o z d z i a l e 3.1 zagadnienie to rozwinięto i przedstawiono odpowiedni dowód
dla wyszukiwania binarnego. Istnieją trzy ważne reguły rozwijania programów reku-
rencyjnych:
■ W rekurencji występuje przypadek podstawowy. Jako pierwsza w programie za
wsze występuje instrukcja warunkowa z instrukcją return.
■ Wywołania rekurencyjne muszą rozwiązywać podproblemy, które w pewnym
sensie są mniejsze, dlatego wywołania rekurencyjne prowadzą do przypadku
podstawowego. W przedstawionym dalej kodzie różnica między wartościami
czwartego i trzeciego argumentu zawsze się zmniejsza.
■ Wywołania rekurencyjne nie powinny rozwiązywać nakładających się pod-
problemów. W kodzie fragmenty tablicy uwzględniane w obu podproblemach
są rozłączne.
Naruszenie jednej z reguł często prowadzi do uzyskania nieprawidłowych wyni
ków lub powstania niezwykle niewydajnych programów (zobacz ć w i c z e n i a 1 . 1.19
i 1 .1 .2 7 ). Przestrzeganie zasad zwykle pozwala utworzyć przejrzysty i poprawny pro
gram, którego wydajność łatwo jest określić. Innym powodem stosowania metod
rekurencyjnych jest to, że prowadzą do modeli matematycznych, które można wyko
rzystać do zrozumienia wydajności. Zagadnienie to omówiono w p o d r o z d z i a l e 3.2
(dla wyszukiwania binarnego) i w kilku innych miejscach książki.
p u b lic s t a t i c in t ra n k (in t key, i n t [] a)
{ return rank(key, a, 0, a .length - 1); }
p ub lic s t a t i c i n t ra n k (in t key, i n t [] a, i n t lo , i n t hi)
( // Indeks klucza w a [ ] , j e ś l i i s t n i e j e , j e s t nie mniejszy niż lo
//i nie większy n iż h i .
i f (lo > hi) return -1;
i n t mid = l o + (hi - lo ) / 2;
if (key < ajmid]) return rank(key, a, lo , mid - 1);
e lse i f (key > a[mid]) return rank(key, a, mid + 1, h i ) ;
e lse return mid;
Rekurencyjna implementacja wyszukiwania binarnego
RO ZD ZIA Ł 1 H Podstawy
Podstawowy model programowania Biblioteka metod statycznych to zbiór metod sta
tycznych zdefiniowanych w klasie Javy. Klasa znajduje się w pliku ze słowami kluczowymi
publ i c cl ass, po których następuje nazwa klasy i — w nawiasach klamrowych — meto
dy statyczne. Plik ma nazwę taką samą jak klasa i rozszerzenie .java. Podstawowy model
programowania w Javie polega na opracowaniu programu rozwiązującego konkretne za
dania obliczeniowe za pomocą biblioteki metod statycznych, z których jedna nosi nazwę
mai n(). Wpisanie słowa java i nazwy klasy, po której następuje seria łańcuchów znaków,
powoduje wywołanie metody mai n () z tej klasy, a jej argumentem jest tablica podanych
łańcuchów znaków. Po wykonaniu ostatniej instrukcji metody main () program kończy
działanie. W tej książce program Javy wykonujący zadanie to kod utworzony w opisany
tu sposób (program może też obejmować definicję typu danych, co opisano w p o d r o z
d z i a l e 1 .2 ). Na przykład BinarySearch to program Javy składający się z dwóch metod
statycznych, rank () i mai n (), wyświetlający liczby ze strumienia wejścia, które nie znaj
dują się w pliku z białą listą podanym jako argument w wierszu poleceń.
Programowanie m odularne Kluczowe znaczenie w tym m odelu ma to, że biblio
teki metod statycznych umożliwiają programowanie modularne. Podejście to polega
na budowaniu bibliotek m etod statycznych (modułów), przy czym m etody statyczne
z jednej biblioteki mogą wywoływać metody statyczne zdefiniowane w innych biblio
tekach. Technika ta ma wiele istotnych zalet. Umożliwia:
■ używanie modułów o rozsądnej wielkości (nawet w programach obejmujących
dużą ilość kodu);
■ współużytkowanie i wielokrotne wykorzystanie kodu bez konieczności ponow
nego implementowania go;
* łatwe podstawianie poprawionych implementacji za dawne;
■ rozwijanie odpowiednich abstrakcyjnych modeli do rozwiązywania problemów
programistycznych;
■ diagnozowanie mniejszych fragmentów kodu (zobacz dalszy akapit na temat
testów jednostkowych).
W programie BinarySearch wykorzystano trzy inne niezależnie opracowane biblio
teki — napisane przez nas biblioteki Stdln i In oraz bibliotekę Arrays Javy. Każda
z nich także korzysta z kilku innych bibliotek.
Testy jednostkow e Zalecaną praktyką w programowaniu w Javie jest umieszcza
nie w każdej bibliotece m etod statycznych metody main() służącej do testowania
m etod z biblioteki (w niektórych innych językach programowania używanie kilku
m etod main() jest niedozwolone, dlatego nie m ożna zastosować tego podejścia).
Przygotowanie poprawnych testów jednostkowych samo w sobie może być poważ
nym wyzwaniem programistycznym. Minimalnym wymogiem jest to, aby każdy
m oduł zawierał metodę main() sprawdzającą kod m odułu i zapewniającą, że kod
ten działa. W dojrzałym m odule często można dopracować metodę main(), tak aby
stała się klientem wspomagającym tworzenie aplikacji (ang. development client), który
pomaga przeprowadzać bardziej szczegółowe testy w trakcie rozwijania kodu, lub
klientem testowym, sprawdzającym dokładnie cały kod. Kiedy klient staje się bardziej
skomplikowany, można umieścić go w niezależnym module. W tej książce używamy
1.1 * Podstawowy m odel program ow ania 39
m etody mai n (), aby pomóc lepiej zrozumieć przeznaczenie każdego modułu, i pozo
stawiamy opracowanie klientów testowych jako ćwiczenia.
Biblioteki zew nętrzne Można używać m etod statycznych z czterech rodzajów bi
bliotek, z których każda wymaga (nieco) innych procedur w celu wielokrotnego wy
korzystania kodu. Większość bibliotek obejmuje m etody statyczne, przy czym nie
które biblioteki to definicje typów danych, które też zawierają m etody statyczne. Oto
rodzaje bibliotek:
" Standardowe biblioteki systemowe j ava. 1ang. *. Należą do nich: Standardowe
biblioteki systemowe
Math, zawierająca metody dla często używanych funkcji matem a
Math
tycznych; Integer i Doubl e, które służą do przekształcania między
Int e g e r'
łańcuchami znaków a wartościami typów in t i double; String
Double*
i S tringB uilder, omówione szczegółowo w dalszej części p o d
S t r i ng'
rozdziału i w r o z d z ia l e 5 .; a także dziesiątki innych bibliotek,
StringBuilder
których nie wykorzystano w tej książce.
■ Im portowane biblioteki systemowe, na przykład j a v a .u t i l . System
Arrays. W standardowym wydaniu Javy istnieją tysiące takich bi Importowane
biblioteki systemowe
bliotek, jednak rzadko są one stosowane w tej książce. Aby użyć
[Link].A rrays
takiej biblioteki, na początku program u trzeba umieścić instruk
Biblioteki standardowe
cję import. opracowane przez nas
■ Inne biblioteki z książki. Metodę rank() z programu BinarySearch Std ln
można wykorzystać w innym programie. Aby to zrobić, należy po StdOut
brać kod źródłowy z witryny do katalogu roboczego. StdDraw
■ Biblioteki standardowe Std* opracowane do użytku w tej książ StdRandom
ce (i w podręczniku dla początkujących An Introduction to Prog St d Stats
ramming in Java: An Interdisciplinary Approach). Biblioteki te opi In'
sano na kilku następnych stronach. Kod źródłowy i instrukcje do
Out*
tyczące ich pobierania znajdują się w witrynie.
'Definicje typów danych
Aby wywołać metodę z innej biblioteki (która znajduje się w tym sa obejmujące metody statyczne
mym lub określonym katalogu, jest standardową biblioteką systemo
Biblioteki z metodami
wą lub biblioteką systemową podaną w instrukcji import przed defi statycznymi używane
nicją klasy), należy w każdym wywołaniu umieścić nazwę biblioteki w tej książce
przed nazwą metody. Na przykład w metodzie main() w programie
BinarySearch wywołano metodę s o rt() biblioteki systemowej ja v a .uti 1 .Arrays,
metodę re a d ln ts() z opracowanej przez nas biblioteki In oraz metodę p rin tln ()
z opracowanej przez nas biblioteki StdOut.
modular
b ib l io t e k i m eto d z a im p le m e n t o w a n e s a m o d z ie l n ie i p r z e z in n y c h w
nym środowisku programowania pozwalają znacznie rozwinąć model programowania.
Oprócz wszystkich bibliotek dostępnych w standardowych wydaniach Javy w inter-
necie można znaleźć tysiące innych bibliotek przeznaczonych do rozmaitych zastoso
wań. Aby ograniczyć zakres modelu programowania do akceptowalnego rozmiaru, co
pozwoli skoncentrować się na algorytmach, używamy tylko bibliotek wymienionych
w tabeli po prawej stronie i podzbioru ich metod wymienionych w interfejsach API.
RO ZD ZIA Ł 1 Q Podstawy
Interfejsy API Kluczowym elementem programowania m odularnego jest doku
mentacja, wyjaśniająca działanie metod biblioteki przeznaczonych do użytku przez
inne osoby. Metody bibliotek używane w tej książce konsekwentnie przedstawiamy
w interfejsach API, które obejmują listy bibliotek oraz sygnatury i krótkie opisy każdej
stosowanej metody. Nazwa klient oznacza program wywołujący metodę z innej bi
blioteki, a implementacja to kod Javy wykonujący m etody podane w interfejsie API.
P rzykład Przykład ten, interfejs API z często używanymi m etodam i statycznymi ze
standardowej biblioteki Math z pakietu j ava. 1ang, stanowi ilustrację konwencji zwią
zanych z interfejsami API:
p ub lic c l a s s M a t h
s t a t i c double abs(double a) Wartość bezwzględna a
s t a t i c double max(double a, double b) Maksim um spośród a i b
s t a t i c double min(double a, double b) M inim um spośród a i b
Uwaga 1. Metody abs () , max () i mi n () są zdefiniowane także dla typów i n t , longrfloat.
s t a t i c double s i n (double theta) Funkcja sinus
s t a t i c double cos (double theta) Funkcja cosinus
s t a t i c double tan(double theta) Funkcja tangens
Uwaga 2. Kąty są wyrażane w radianach. Do przekształcania służą metody toDegrees() i toRadians ().
Uwaga 3. Metody a s i n ( ) , acos() la t a n ( ) obliczają funkcje odwrotne.
s t a t i c double exp(double a) Funkcja wykładnicza (et')
s t a t i c double log(d ouble a) Logarytm naturalny (logt a lub In a)
s t a t i c double pow(double a, double b) Podnoszenie a do potęgi b-tej (ab)
s t a t i c double random() Liczba losowa z przedziału [0,1)
s t a t i c double sqrt(dou b le a) Pierwiastek kwadratowy z a
s t a t i c double E Wartość e (stała)
s t a t i c double PI Wartość Ti (stała)
Inne dostępne funkcje można znaleźć w witrynie
Interfejs API matematycznej biblioteki Javy (fragmenty)
1.1 o Podstawowy model program ow ania
Metody te to implementacje funkcji matematycznych. Argumenty m etod służą do
obliczania wartości określonego typu (wyjątkiem jest metoda random() — nie jest
ona implementacją funkcji matematycznej, ponieważ nie przyjmuje argumentu).
Ponieważ wszystkie działają na wartościach typu doubl e i zwracają wynik tego typu,
można uznać je za rozwinięcie typu danych doubl e. Rozszerzalność tego rodzaju jest
jedną z charakterystycznych cech współczesnych języków programowania. Każda
metoda jest opisana w interfejsie API w wierszu z informacjami potrzebnymi do
stosowania danej metody. Biblioteka Math zawiera też definicje dokładnych wartości
stałych PI (dla liczby n) i E (dla liczby e), dlatego można stosować te nazwy w celu
określenia stałych w programach. Na przykład wartość wyrażenia Math, sin (Math.
PI/2) wynosi 1.0, tak samo jak wartość wyrażenia Math, log (Math. E) (ponieważ
Math, sin () przyjmuje argumenty w radianach, a [Link] to implementacja funk
cji logarytm naturalny).
Biblioteki Javy Obszerny elektroniczny opis tysięcy bibliotek jest częścią każdego
wydania Javy. Tu przedstawiono tylko część używanych w książce metod, aby jasno
określić model programowania. Na przykład w programie Bi narySearch użyto metody
s o rt() z biblioteki Arrays Javy. Oto dokumentacja tej metody:
pub lic c l a s s Arrays
s t a t i c void so rt ( i nt [] a) Sortowanie tablicy w porządku rosnącym
Uwaga: metoda ta jest zdefiniowana także dla innych typów prostych i dla typu Object.
Fragment biblioteki Arrays Javy ( ja v a .u t il .A rrays)
Biblioteka Arrays nie należy do pakietu j ava. 1ang, dlatego korzystanie z niej wymaga
użycia instrukcji import, tak jak zrobiono to w programie Bi narySearch. r o z d z ia ł 2.
książki dotyczy implementacji metody so rt () dla tablic. Opisano między innymi al
gorytmy sortowania przez scalanie i sortowania szybkiego zaimplementowane w m e
todzie A rrays. s o rt (). Wiele podstawowych algorytmów omówionych w książce jest
zaimplementowanych w Javie i licznych innych środowiskach programistycznych.
Biblioteka Arrays obejmuje też na przykład implementację wyszukiwania binarnego.
Aby uniknąć niejasności, zwykle korzystamy z opracowanych przez nas implemen
tacji, natomiast nie m a niczego złego w stosowaniu dopracowanych implementacji
z bibliotek, jeśli programista rozumie dany algorytm.
RO ZD ZIA Ł 1 o Podstaw y
Opracowane p rze z nas biblioteki standardow e Opracowaliśmy szereg bibliotek
udostępniających funkcje przydatne przy programowaniu w Javie na podstawowym
poziomie, w aplikacjach naukowych, a także przy rozwijaniu, analizowaniu i stoso
waniu algorytmów. Większość bibliotek dotyczy wejścia i wyjścia. Korzystamy też
z dwóch przedstawionych dalej bibliotek do testowania i analizowania implemen
tacji. Pierwsza stanowi rozwinięcie m etody [Link]() i umożliwia pobieranie
losowych wartości z różnych przedziałów. Druga obsługuje obliczenia statystyczne.
p ub lic c l a s s StdRandom
static void i n i t i a l i z e ( l o n g seed) Inicjowanie
static double random() Liczba rzeczywista z przedziału 0 - 1
static i n t uniform (int N) Liczba całkowita z przedziału 0 - N-l
static in t uniform (int lo , i n t hi) Liczba całkowita z przedziału! o — h i -1
static double uniform(double lo, double hi) Liczba rzeczywista z przedziału 1o - hi
s t a t i c boolean b e r n o u lli( d o u b le p) Zwraca true z prawdopodobieństwem p
static double g a ussian () Rozkład normalny, średnia 0, odch. st. 1
static double ga ussian(d ouble m, double s) Rozkład normalny, średnia m, odch. st. s
static in t dis c re te (d o u b le [ ] a) Zwraca i z prawdopodobieństwem a [ i ]
static void shuffle (doublet] a) Losowo porządkuje elementy tablicy a []
Uwaga: dostępne są przeciążone implementacje metody shuffle() dla innych typów prostych i dla
typu Object.
Interfejs API dla opracowanej przez nas biblioteki metod statycznych
do zwracania liczb losowych
p ub lic c l a s s StdRandom
s t a t i c double max (doublet] a) Największa wartość
s t a t i c double min(double[] a) Najmniejsza wartość
s t a t i c double var (d ou ble[] a) Wariancja dla próbki
s t a t i c double stddev(double[] a) Odchylenie standardowe dla próbki
s t a t i c double median (doublet] a) Mediana
Interfejs API dla opracowanej przez nas biblioteki metod statycznych do analizy danych
1.1 B Podstawowy m odel program owania
M etoda I n itia l ize() z biblioteki StdRandom umożliwia określenie ziarna dla gene
ratora liczb losowych, dzięki czemu można powtórzyć eksperymenty z wykorzysta
niem takich liczb. Z implementacjami wielu spośród tych m etod m ożna zapoznać się
na stronie 44. Niektóre m etody są bardzo proste w implementacji. Po co umieszczać
je w bibliotece? Oto standardowe odpowiedzi dotyczące dobrze zaprojektowanych
bibliotek:
n Takie m etody zapewniają poziom abstrakcji, co pozwala skoncentrować się na
implementowaniu i testowaniu omawianych w książce algorytmów zamiast na
generowaniu losowych obiektów lub obliczaniu statystyk. Kod kliencki, w któ
rym wykorzystano te metody, jest bardziej przejrzysty i łatwiejszy do zrozumie
nia niż samodzielnie napisany kod, wykonujący te same obliczenia.
■ Implementacje z biblioteki wykrywają wyjątkowe warunki, uwzględniają rzad
kie sytuacje i są gruntownie przetestowane, dlatego m ożna zakładać, że działa
ją w oczekiwany sposób. Implementacje tego rodzaju mogą obejmować dużą
ilość kodu. Często potrzebne są implementacje dla różnych typów danych.
Na przykład biblioteka Arrays Javy obejmuje wiele przeciążonych implementa
cji m etody s o rt () — po jednej dla każdego typu danych, który może wymagać
sortowania.
Są to podstawy programowania m odularnego w Javie, jednak w tym kontekście
stwierdzenia te m ożna uznać za nieco przesadne. Choć metody w obu bibliotekach są
samodokumentujące się, a implementacja wielu z nich jest prosta, niektóre stanowią
ciekawe ćwiczenia algorytmiczne. Dlatego zachęcamy do tego, aby zarówno przeana
lizować kod z bibliotek StdRandom. java i S td S ta ts . java z witryny, jak i korzystać
z tych sprawdzonych implementacji. Najłatwiejszy sposób na wykorzystanie biblio
tek (i sprawdzenie kodu) polega na pobraniu kodu źródłowego z witryny i umiesz
czeniu go w katalogu roboczym. W witrynie opisano też różne zależne od systemu
mechanizmy używania bibliotek bez konieczności tworzenia wielu kopii.
W łasne biblioteki Warto traktować każdy napisany przez siebie program jak imple
mentację biblioteki do powtórnego użytku w przyszłości. W tym celu należy:
° Napisać kod klienta — implementację wysokiego poziomu, dzielącą obliczenia
na części o rozsądnej wielkości.
D Określić interfejs API dla biblioteki (lub kilka interfejsów API dla kilku biblio
tek) metod statycznych i uwzględnić w nim każdą część.
■ Opracować implementację interfejsu API, obejmującą metodę mai n (), która te
stuje metody niezależnie od klienta.
To podejście nie tylko prowadzi do powstania wartościowego oprogramowania, które
można powtórnie zastosować; wykorzystanie programowania modularnego w ten spo
sób jest też kluczem do udanego rozwiązywania złożonych zadań programistycznych.
RO ZD ZIA Ł 1 □ Podstawy
Oczekiwany efekt Implementacja
Losowa wartość typu doubl e p ub lic s t a t i c double uniform(double a, double b)
z przedziału [a, b) { return a + [Link]() * (b-a); )
Losowa wartość typu i nt p ub lic s t a t i c in t u n iform (int N)
z przedziału [0. .N) { return ( i n t ) ([Link]() * N ) ;
Losowa wartość typu i nt p ub lic s t a t i c in t u n iform (int lo, i n t hi)
z przedziału [ l o . . h i ) { return lo + [Link](hi - lo ) ;
p ub lic s t a t i c in t d i s c re te (d ou ble[] a)
{ // Elementy z a[] muszą s i ę sumować do 1.
double r = [Link]();
double sum = 0.0;
Losowa wartość typu i nt for (int i = 0; i < [Link] ngth; i++)
z rozkładu dyskretnego (
(i z prawdopodobieństwem a [ i ] j sum = sum + a [ i ] ;
i f (sum >= r) return i ;
}
return -1;
p ub lic s t a t i c void shuffle(double[] a)
{
in t N = [Link];
f o r ( i n t i = 0; i < N; i++)
Losowo zmienia pozycje elementów { // Przestawia a [ i ] i losowy element z a [ i .. N - l ] .
w tablicy wartości typu doubl e i n t r = i + [Link](N-i);
(zobacz ćwiczenie 1.1.36) double temp = a [ i ] ;
a[i] = a [ r ] ;
a [r] = temp;
}
)
Implementacje metod statycznych z biblioteki StdRandom
1.1 Q Podstawowy m odel program owania
p r z e z n a c z e n i e m i n t e r f e j s u A P I jest oddzielenie ldienta od implementacji. Klient
nie powinien posiadać na tem at implementacji żadnych informacji oprócz tych udo
stępnionych w interfejsie API, a w implementacji nie należy uwzględniać cech żad
nego konkretnego klienta. Interfejsy API umożliwiają niezależne rozwijanie kodu
o różnym przeznaczeniu i jego późniejszy wielokrotny użytek na dużą skalę. Żadna
biblioteka Javy nie może zawierać wszystkich m etod potrzebnych w danych oblicze
niach, dlatego opisane podejście to kluczowy krok w pracy nad skomplikowanymi
aplikacjami. Programiści zwykle traktują interfejs API jak kontrakt między klientem
a implementacją, będący dokładną specyfikacją tego, co każda metoda ma robić.
Często zadanie można wykonać na wiele sposobów, a oddzielenie kodu klienta od
kodu implementacji pozwala zastosować nowe i ulepszone implementacje. W dzie
dzinie badań nad algorytmami omawiane podejście jest ważnym czynnikiem um oż
liwiającym zrozumienie wpływu opracowanych usprawnień algorytmu.
RO ZD ZIA Ł 1 h Podstawy
Łańcuchy znaków Typ S tri ng to ciąg znaków (wartości typu char). Literał typu
S tri ng to ciąg znaków w cudzysłowach, na przykład "Wi t a j , świ eci e ! St ri ng to
typ danych Javy, jednak nie jest typem prostym. Opisano go w tym miejscu, ponieważ
jest kluczowym typem danych, używanym w niemal każdym programie Javy.
Z łączanie Java ma wbudowany operator złączania (+) dla typu S tri ng, podobny do
operatorów wbudowanych dla typów prostych. Pozwala to dodać wiersz z poniższej
tabeli do tabeli typów prostych ze strony 24. Wynik złączenia dwóch wartości typu
S tring to jedna taka wartość, w której po pierwszym łańcuchu znaków następuje
drugi.
Typowe Typowe wyrażenie
Typ Zbiór wartości . O p e r a t o r y -------------------------;---------------------—------ —------
literały Wyrażenie Wartość
“AB" "W ita j, " + "O lu " "W ita j, Olu"
String Ciągi znaków "W ita j" + (złączanie) "12" + "34" "1234"
"2.5" "1 " + "+ " + "2 " "1+2"
Typ danych St ri ng Javy
Konwersja Dwa podstawowe zastosowania łańcuchów znaków to przekształcanie
wartości wpisywanych za pom ocą klawiatury na wartości typów danych i przekształ
canie wartości typów danych na wyświetlane wartości. Java udostępnia wbudowane
operacje na typie S tring ułatwiające wykonywanie tych zadań. Język obejmuje bi
blioteki In teger i Double, zawierające metody statyczne do przekształcania między
wartościami typów S tri ng i i nt oraz wartościami typów S tri ng i doubl e.
p ub lic c l a s s Integer______________________________________________________________
static in t p a rs e ln t ( S t r i n g s) Przekształcanie s na wartość typu i nt
s ta tic Strin g t o S tr in g (in t i) Przekształcanie i na wartość typu S t r i n g
pub lic c l a s s Double_______________________________________________________________
s t a t i c doubl e parseDoubl e (S t r i ng s) Przekształcanie s na wartość typu doubl e
s t a t i c S t r i n g t o S tr in g (d o u b le x) Przekształcanie x na wartość typu S t r i n g
Interfejsy API do przekształcania między liczbami a wartościami typu String
1.1 Ei Podstawowy model program owania
Konwersja autom atyczna Opisane metody to S tri ng () rzadko stosuje się w bezpo
średni sposób, ponieważ Java posiada wbudowany mechanizm umożliwiający prze
kształcenie wartości dowolnego typu na typ S tring za pom ocą złączania. Jeśli jednym
z argumentów operatora + jest wartość typu S tri ng, Java automatycznie przekształca
drugi argument na ten typ (jeśli nie jest to wartość tego typu). Oprócz zastosowań
w rodzaju "Pierw iastek kwadratowy z 2.0 to " + M [Link](2.0) mechanizm
ten umożliwia przekształcanie wartości dowolnego typu danych na typ S tri ng przez
złączenie wartości z pustym łańcuchem znaków "".
A rg u m en ty wiersza poleceń Ważnym zastosowaniem łańcuchów znaków w pro
gramowaniu w Javie jest obsługa prostego mechanizmu przekazywania informacji
z wiersza poleceń do programu. Po wpisaniu przez użytkownika polecenia java
i nazwy biblioteki wraz z ciągiem łańcuchów znaków system Javy wywołuje metodę
main() biblioteki z tablicę łańcuchów znaków jako argumentem. Obejmuje ona łań
cuchy znaków wpisane po nazwie biblioteki. Na przykład m etoda mai n () w progra
mie Bi narySearch przyjmuje jeden argument wiersza poleceń, dlatego system tworzy
tablicę o jednej wartości. Program używa tej wartości, args [0], do określenia nazwy
pliku z białą listą, używaną jako argument m etody In . read ln ts (). Inny typowy pa
radygmat często używany w kodzie w książce dotyczy sytuacji, w której argument
wiersza poleceń ma przedstawiać liczbę. Używamy wtedy m etody p arseln tQ do
przekształcenia argumentu na wartość typu i nt lub m etody parseDoubl e() do prze
kształcenia na wartość typu doubl e.
PRZETWARZANIE Z WYKORZYSTANIEM ŁAŃCUCHÓW ZNAKÓW tO kluCZOWy aspekt
współczesnej inform atyki. Na razie używ am y typu S tring tylko do przekształcania
m iędzy zewnętrzną reprezentacją liczb w postaci ciągów znaków a wewnętrzną re
prezentacją w artości liczbowych typów danych. W p o d r o z d z ia l e 1.2 pokazano, że
Java obsługuje o wiele więcej stosowanych w książce operacji na wartościach typu
String. W p o d r o z d z ia l e 1.4 om ówiono wewnętrzną reprezentację w artości typu
String. W r o z d z ia l e 5 . szczegółowo opisano algorytm y do przetwarzania danych
tego typu. Są to jedne z najciekawszych, najbardziej złożonych i najważniejszych m e
tod rozważanych w książce.
48 RO ZD ZIA Ł 1 □ Podstawy
Wejście i wyjście Podstawową funkcją opracowanych przez nas bibliotek stan
dardowych do obsługi wejścia, wyjścia i rysowania jest obsługa prostego modelu
interakcji programów Javy ze światem zewnętrznym. Biblioteki te zbudowano na
podstawie rozbudowanych możliwości bibliotek Javy, które są jednak zwykle dużo
bardziej skomplikowane oraz trudniejsze do nauczenia się i użytku. Rozpoczynamy
od krótkiego przeglądu modelu.
W modelu program Javy przyjmuje war
Argumenty
wiersza poleceń tości wejściowe z argumentów wiersza pole
ceń lub z abstrakcyjnego strum ienia znaków
(standardowego strumienia wejścia) i zapisu
je dane w innym abstrakcyjnym strum ieniu
znaków (standardowym strumieniu wyjścia).
Konieczne jest uwzględnienie interfejsu
między Javą a systemem operacyjnym, dla
Plikowe operacje
tego trzeba pokrótce omówić podstawowe
wejścia-wyjścia mechanizmy udostępniane przez większość
Standardowe
rysowanie współczesnych systemów operacyjnych
i środowisk programistycznych. Więcej in
Program Javy „z lotu ptaka"
formacji o konkretnych systemach znajduje
się w witrynie. Domyślnie argumenty wiersza poleceń, standardowe wejście i stan
dardowe wyjście są powiązane z aplikacją przyjmującą polecenia, obsługiwaną albo
przez system operacyjny, albo przez środowisko programistyczne. Używamy tu ogól
nej nazwy okno terminala do określenia okna tej aplikacji, służącego do wpisywania
i odczytywania tekstu. Od czasu wczesnych systemów uniksowych z lat 70. ubiegłego
wieku model ten był wygodnym i bezpośrednim sposobem interakcji z programami
oraz danymi. Do klasycznego modelu dodaliśmy mechanizmy standardowego ryso
wania, umożliwiające tworzenie wizualnych reprezentacji przy analizach danych.
Polecenia i argum enty W oknie terminala widoczny jest znak zachęty. Przy nim wpi
sywane są polecenia do systemu operacyjnego, które mogą przyjmować argumenty.
W książce używamy tylko kilku poleceń, przedstawionych w tabeli poniżej. Najczęściej
stosujemy polecenie java, służące do uruchamiania programów. Na stronie 47 wspo
mniano, że klasy Javy mają metodę statyczną mai n (), przyjmującą jako argument tabli
cę args [] z wartościami typu S tri ng. Tablica ta to ciąg wpisanych argumentów wiersza
poleceń udostępnionych Javie przez system operacyjny. Zgodnie z konwencją Java
i system operacyjny przetwarzają argumenty jak łańcuchy znaków. Jeśli argument ma
być liczbą, należy
Polecenie A rgumenty______________________ Przeznaczenie _________użyć m etody W ro
javac Nazwa pliku .java Kompilacja program u Javy dzaju In te g e [Link]-
java Nazwa pliku .class (bez rozszerzenia) U ruchamianie program u Javy s e ln t() do przek
i argum enty wiersza poleceń ształcenia wartości
more Nazwa dowolnego pliku tekstowego Wyświetlanie zawartości pliku z typu String na
Typowe polecenia systemu operacyjnego właściwy typ.
1.1 a Podstawowy model program ow ania 49
Standardow e wyjście Opracowana przez nas Wywołanie metody statycznej
Znak m a in () z programu Random Seq
biblioteka StdOut zapewnia obsługę standardo zachęty
wego wyjścia. Domyślnie system łączy standardo
V
we wyjście z oknem terminala. M etoda p r i n t () % j a v a Random Seq 5 1 0 0 .0 2 0 0 .0
- \~
umieszcza argument w standardowym wyjściu. a r g s [0]
Wywołanie środowiska
Metoda p rin tln () dodaje nowy wiersz, a me uruchomieniowego Javy
a r g s [1]
a r g s [2]
toda p r in tf ( ) obsługuje sformatowane wyjście
w opisany dalej sposób. Java udostępnia podobną Struktura polecenia
metodę w bibliotece [Link]. Tu używamy bi
blioteki StdOut, aby traktować standardowe wej
ście i wyjście w jednolity sposób (i zapewnić kilka
technicznych usprawnień).
p ub lic c l a s s StdOut
s t a t i c void p r i n t ( S t r i n g s) Wyświetla s
s t a t i c void p r i n t l n ( S t r i n g s) Wyświetla s i nowy wiersz
s t a t i c void p r i n t l n () Wyświetla nowy wiersz
s t a t i c void p r in tf (String f, ...) Wyświetla sformatowane dane
Uwaga: istnieją też przeciążone implementacje dla typów podstawowych i typu Object.
Interfejs API opracowanej przez nas biblioteki metod statycznych
do obsługi standardowego wyjścia
Aby używać tych metod, na
public c la s s RandomSeq
leży pobrać do katalogu robo
{
czego plik [Link] z wi public s t a t i c void main(String[] args)
tryny i stosować kod w rodza { // Wyświetlanie N losowych wartości z przedziału (lo, hi),
i nt N = In t e g e r.p a r s e ln t ( a r g s [0]);
ju StdO [Link] n t1n ( "Wi t a j , double lo = [Link] Double(args[l]);
św iecie!"J; do ich wywoły double hi = [Link](args[2]);
wania. Po prawej przedsta for (in t i = 0; i < N; i++)
{
wiono prostego klienta. double x = [Link](lo, h i);
Std O u t. p rin t f ("% .2 f \ n ", x);
Sformatowane dane wyjścio
1
we W najprostszej postaci 1
metoda pri n tf () przyjmuje
dwa argumenty. Pierwszy to Przykładowy klient biblioteki StdOut
łańcuch formatujący, który
określa, jak należy przekształ
cić drugi argument na łańcuch znaków w celu jego wyświet % ja va RandomSeq 5 100.0 200.0
lenia. Najprostszy rodzaj łańcucha formatującego zaczyna 123.43
153.13
się od znaku %, a kończy jednoliterowym kodem konwersji.
144.38
Najczęściej używane tu kody konwersji to: d (dla wartości 155.18
dziesiętnych opartych na typach całkowitoliczbowych Javy), 104.02
f (dla wartości zmiennoprzecinkowych) i s (dla wartości
RO ZD ZIA Ł 1 a Podstawy
typu S tri ng). Między % a kodem konwersji znajduje się wartość całkowitoliczbowa,
określająca szerokość pola z przekształconą wartością (liczbę znaków w przekształco
nym łańcuchu wyjściowym). Domyślnie po lewej dodawane są odstępy, przez co dłu
gość przekształconych danych wyjściowych jest równa szerokości pola. Aby odstępy
pojawiły się po prawej stronie, należy wstawić znak minus przed szerokością pola. Jeśli
przekształcony łańcuch wyjściowy jest dłuższy niż szerokość pola, jej wartość jest ig
norowana. Po szerokości można podać kropkę i liczbę cyfr podawanych po kropce
dla wartości typu double (precyzję) albo liczbę znaków pobieranych z początku łań
cucha dla wartości typu S tri ng. Najważniejszą rzeczą do zapamiętania na temat meto
dy p rin tf () jest to, że kod konwersji w łańcuchu formatującym musi pasować do typu
powiązanego argumentu. Java musi móc przekształcić typ argumentu na typ żądany
w kodzie konwersji. Pierwszy argument metody pri n tf () ma typ S tri ng i może obej
mować znaki, które nie należą do formatującego łańcucha znaków. Każda część argu
mentu, która nie wchodzi w skład formatującego łańcucha znaków, trafia do danych
wyjściowych, natomiast łańcuch formatujący jest zastępowany wartością argumentu
(przekształconą w odpowiedni sposób na typ S tri ng). Na przykład instrukcja:
S td O [Link]("P I to około % .2f\n", Math.P I);
wyświetla wiersz:
PI to około 3.14
Zauważmy, że trzeba bezpośrednio dodać symbol nowego wiersza, \n, aby za pom o
cą metody pri n tf () wyświetlić nowy wiersz. Metoda ta może przyjmować więcej niż
dwa argumenty. Wtedy łańcuch formatujący obejmuje określenia sposobu formato
wania dla każdego dodatkowego argumentu. Czasem kody rozdzielone są innymi
znakami przekazywanymi na wyjście. Można też użyć metody statycznej S trin g ,
format () z argumentami opisanymi dla m etody pri n tf (), aby uzyskać sformatowa
ny łańcuch znaków bez wyświetlania go. Wyświetlanie sformatowanych danych to
wygodny mechanizm, umożliwiający pisanie zwięzłego kodu, który generuje dane
z eksperymentów uporządkowane w formie tabeli (jest to podstawowe zastosowanie
tego mechanizmu w książce).
Przykładowe Wartość łańcucha przekształcona
Typ Kod Typowy literał
łańcuchy formatujące na dane wyjściowe
512
in t d 512
512
% 14.2f 1595.17
f
double 1595.1680010754388 "% .7 f" 1595.168001111
e
'% 14.4e 1.5952e+03
"%1 4s" Witaj, wiosno
String s "W itaj, wiosno "%-14s" Witaj, wiosno
V 1 4 .5 S Wi taj
Sposoby formatowania za pomocą metody p r in t f ()
(w witrynie opisano o wiele więcej możliwości)
1.1 h Podstawowy model program ow ania
Standardowe wejście Opracowana p ub lic c l a s s Average
przez nas biblioteka Stdln przyjmu {
p ub lic s t a t i c void m a in ( S tr in g [] args)
je dane ze standardowego strum ie
{ // Średnia dla l i c z b ze standardowego wejścia,
nia wejścia. Może być on pusty lub double sum = 0.0;
zawierać ciąg wartości oddzielonych in t cnt = 0;
while ( ¡ S t d ln . i s E m p t y O )
białymi znakami (odstępami, tabu
{ // Wczytywanie li c z b y i dodawanie je j do sumy.
lacjami, znakami nowego wiersza sum += St d In . re a d D o u b le ( );
itd.). Domyślnie system wiąże stan cnt++;
dardowe wyjście z oknem terminala )
double avg = sum / cnt;
— wprowadzone dane są strumie S t d O u t . p r i n t f ( “Średnia wynosi % . 5 f \ n " , avg);
niem wejścia (w zależności od apli }
kacji okna terminala zakończonym 1
sekwencją <Ctrl-d> lu b <ctrl-Z>). Przykładowy klient biblioteki Stdln
Każda wartość ma typ String lub
jeden z typów prostych Javy. Jedną z kluczowych cech
% java Average
standardowego strumienia wejścia jest to, że program 1.23456
używa wartości po ich wczytaniu. Po pobraniu wartości 2.34567
3.45678
nie można się cofnąć i wczytać ich ponownie. Powoduje
4.56789
to pewne ograniczenia, jednak odzwierciedla fizyczne < c t r l -d>
cechy niektórych urządzeń wejścia i upraszcza imple Średnia wynosi 2.90123
mentację abstrakcji. Metody statyczne z omawianej bi
blioteki dotyczące modelu strumienia wejścia są zwykle
łatwe do zrozumienia (sygnatury dobrze je opisują).
p ub lic c l a s s Stdln
s t a t i c boolean isEmptyO t r u e, jeśli nie ma więcej wartości; w innej sytuacji fal se
static in t re ad lnt() Wczytuje wartość typu i nt
static double readDoubleO Wczytuje wartość typu double
static float re adFloat( ) Wczytuje wartość typu float
static long readLong() Wczytuje wartość typu 1ong
s t a t i c boolean readBoolean() Wczytuje wartość typu bool ean
static char readChar() Wczytuje wartość typu char
static byte readByte() Wczytuje wartość typu byte
static S t r i n g re a d S tr in gf) Wczytuje wartość typu S t r i ng
s t a t i c boolean hasNextLine() Czy w strumieniu wejścia istnieje następny wiersz?
static S t r i n g re adLineO Wczytuje pozostałą część wiersza
static S t r i n g re a d A ll( ) Wczytuje pozostałą część strumienia wejścia
Interfejs API opracowanej przez nas biblioteki metod statycznych
do obsługi standardowego wejścia
52 RO ZD ZIA Ł 1 o Podstawy
Przekierowywanie i potoki Standardowe wejście i wyjście umożliwiają wykorzysta
nie rozszerzenia wiersza poleceń, obsługiwanego w wielu systemach operacyjnych.
Przez dodanie prostej dyrektywy do polecenia wywołującego program można prze-
kierować standardowe wyjście do pliku — albo w celu trwałego zapisania danych,
albo po to, aby wykorzystać je później jako wejście innego programu:
% java RandomSeq 1000 100.0 200.0 > d a ta .tx t
To polecenie określa, że standardowego strumienia wyjścia nie należy wyświetlać w ok
nie terminala, tylko trzeba zapisać go w pliku tekstowym [Link]. Każde wywołanie
metody StdOut.p r in t() lub S tdO [Link]() powoduje dołączenie tekstu do końco
wej części pliku. W przykładzie ostatecznie powstaje plik zawierający 1000 losowych
wartości. Żadne dane wyjściowe nie pojawiają się w oknie terminala — trafiają za to
bezpośrednio do pliku o nazwie podanej po symbolu >. Dlatego można zapisać in
formacje w celu ich później-
Przekierowywanie z pliku do standardowego wejścia szego pobrania. Zauważmy,
% ja v a A ve ra ge < d a t a .t x t że nie trzeba w żaden spo
d a t a .t x t sób zmieniać programu
RandomSeq. Korzysta on
z abstrakcji standardowego
wyjścia i nie jest zależny od
Przekierowywanie standardowego wyjścia do pliku zastosowania różnych im
plementacji tej abstrakcji.
% j a v a RandomSeq 1000 1 0 0 .0 2 0 0 .0 > d a t a . t x t
Podobnie można przekie-
rować standardowe wejście,
tak aby biblioteka Stdln
wczytywała dane z pliku,
a nie z aplikacji terminala:
Potokowe przekazywanie wyjścia z jednego programu do wejścia drugiego % java Average < d a ta .tx t
% j a v a RandomSeq 1000 1 0 0 .0 2 0 0 .0 | ja v a A ve ra ge To polecenie wczytuje ciąg
liczb z pliku [Link] i ob
licza ich średnią wartość.
Symbol < to dyrektywa, któ
ra nakazuje systemowi ope
racyjnemu zastosowanie
standardowego strumienia
Przekierowywanie ¡ potokowe przekazywanie w wierszu poleceń wejścia przez wczytanie
danych z pliku tekstowego
[Link] zamiast oczekiwania na wpisanie przez użytkownika danych w oknie ter
minala. Kiedy program wywołuje metodę [Link] ouble(), system operacyjny
wczytuje wartość z pliku. Połączenie obu technik w celu przekierowania wyjścia
z jednego programu do wejścia drugiego to przekazywanie potokowe:
java RandomSeq 1000 100.0 200.0 | java Average
1.1 n Podstawowy m odel program owania
To polecenie określa, że standardowe wyjście programu RandomSeq i standardowe wej
ście program u Average to ten sam strumień. Efekt jest taki, jakby program RandomSeq
wprowadzał wygenerowane liczby w oknie term inala w czasie działania programu
Average. Różnica między tym podejściem a innymi technikami jest bardzo istotna,
ponieważ tu można pominąć ograniczenie rozmiaru przetwarzanych strumieni wej
ścia i wyjścia. Można na przykład zastąpić 1000 w przykładzie liczbą 1000000000,
nawet jeśli w komputerze nie ma miejsca na zapisanie miliarda liczb (potrzebny jest
jednak czas na ich przetworzenie). Po wywołaniu przez program RandomSeq metody
StdOut. pri n tl n () na koniec strumienia dodawany jest łańcuch znaków. Wywołanie
m etody St d I n . read I nt () w programie Average powoduje usunięcie łańcucha znaków
z początku strumienia. Dokładny czas realizowania tych operacji zależy od systemu
operacyjnego. System może wykonywać program RandomSeq do czasu wygenerowa
nia danych wyjściowych, a następnie uruchomić program Average, aby wykorzystać
dane wejściowe. Może też wykonywać program Average do momentu, w którym p o
trzebne będą dane wejściowe, i wtedy uruchomić program RandomSeq do m om en
tu wygenerowania potrzebnych danych wyjściowych. Efekt końcowy jest taki sam,
natomiast w programach nie trzeba przejmować się takim i szczegółami, ponieważ
programy używają wyłącznie abstrakcji standardowego wejścia i wyjścia.
D ane wejściowe i wyjściowe z p liku Opracowane przez nas biblioteki In i Out udo
stępniają m etody statyczne zapewniające abstrakcję odczytu z pliku i zapisu w nim
zawartości tablicy wartości typu prostego (lub typu String). Do odczytu i zapisu
służą m etody re a d ln ts(), readDoubles() i readS trings() z biblioteki In oraz wri-
te l n t s ( ) , writeDoubles() i w riteS trin g s() zbiblioteki Out. Podanym argumentem
może być plik lub strona internetowa. Pozwala to na przykład użyć pliku i standardo
wego wejścia do dwóch różnych celów w jednym programie, tak jak w Bi narySearch.
Biblioteki In i Out obejmują też implementacje typów danych z m etodam i egzempla
rza, oferującymi bardziej uniwersalne możliwości w zakresie traktowania wielu pli
ków jak strum ieni wejścia i wyjścia oraz stron internetowych jak strum ieni wejścia.
Biblioteki te ponownie opisano w p o d r o z d z ia l e 1.2.
p u b lic c l a s s In
static i n t [ ] r e a d l n t s ( S t r i n g name) Wczytuje wartości typu i nt
static double[] re adDouble s(Str ing name) Wczytuje wartości typu doubl e
static S t r i n g [ ] r e a d S t r i n g ( S t r i n g name) Wczytuje wartości typu S t r i n g
p u b lic c l a s s Out
sta tic void w r i t e ( i n t [ ] a, S t r i n g name) Zapisuje wartości typu in t
static void w rite(double[] a, S t r i n g name) Zapisuje wartości typu double
sta tic void w r i t e ( S t r i n g [ ] a, S t r i n g name) Zapisuje wartości typu S t r i n g
Uwaga 1. Obsługiwane są też inne typy proste.
Uwaga 2. Obsługiwane są też Stdln i StdOut (należy pominąć argument name).
Interfejs API opracowanych przez nas metod statycznych do odczytu i zapisu tablic
RO ZD ZIA Ł 1 ■ Podstawy
Standardow e rysowanie (podstawowe m etody) Do S t d D ra w .p oin t(xO , y O ) ;
St d D ra w .1 in e (x0 , yO, x l , y l ) ;
tego miejsca opracowane przez nas abstrakcje wejścia-
wyjścia dotyczyły wyłącznie tekstu. Teraz wprowadzamy
abstrakcję do generowania danych wyjściowych w for
mie rysunków. Biblioteka jest łatwa w użyciu i um oż
liwia wykorzystanie wizualnych środków wyrazu do
przedstawienia o wiele większej ilości informacji, niż to
możliwe za pomocą samego tekstu. Podobnie jak stan
dardowe wejście i wyjście abstrakcja do standardowego
rysowania jest zaimplementowana w bibliotece. Jest to
biblioteka StdDraw, której m ożna używać po pobraniu
pliku [Link] z witryny do katalogu robocze
go. Standardowe rysowanie jest bardzo proste. Można
wyobrazić sobie abstrakcyjne narzędzie do rysowania,
które potrafi generować linie i punkty w dwuwymiaro
wej przestrzeni. Narzędzie reaguje na polecenia naryso
wania podstawowych kształtów geometrycznych, które
programy wydają za pom ocą wywołań m etod statycz S t d D r a w .s q u a r e ( x , y , r) ;
nych z biblioteki StdDraw. Metody te służą między in
nymi do rysowania linii, punktów, łańcuchów znaków,
okręgów, prostokątów i wielokątów. Metody te, podob
nie jak m etody standardowego wejścia i wyjścia, prawie
nie wymagają opisu. StdDraw. 1i ne () rysuje prostą linię
łączącą punkty (xQ, y g) i (xl, y ) , których współrzędne (x,y)
podawane są jako argumenty. [Link]() rysu
je punkt o środku (x, y), którego współrzędne podano
jako argument, i tak dalej, co pokazano na rysunkach d o u b le [ ] x = {x 0, x l , x2, x3 };
d o u b ie [] y = {yO, y l , y2, y 3 } ;
po prawej stronie. Figury geometryczne można wypeł
StdDra w.p oiyg onCx, y) ;
nić (domyślnie kolorem czarnym). Domyślną miarą jest
jednostka kwadratowa (wszystkie współrzędne mają
wartości między 0 a 1). Standardowa implementacja
wyświetla rysunek w oknie na ekranie komputera. Linie
i punkty są czarne, a tło — białe.
Przykłady zastosowania
biblioteki StdDraw
1.1 ■ Podstawowy m odel program owania
p ub lic c l a s s StdDraw
s t a t i c void lin e (d o u b le x0, double yO, double x l , double y l )
s t a t i c void point(double x, double y)
s t a t i c void text(d ouble x, double y, S t r i n g s)
s t a t i c void c i r c le (d o u b le x, double y, double r)
s t a t i c void fille d C ircle (d ou b le x, double y, double r)
s t a t i c void el 1ipse(double x, double y, double rw, double rh)
s t a t i c void f i lle d E l1ip se(d ouble x, double y, double rw, double rh)
s t a t i c void square(double x, double y, double r)
s t a t i c void filledSquare(double x, double y, double r)
s t a t i c void rectan gle(d ouble x, double y, double rw, double rh)
s t a t i c void fil 1edRectangle(double x, double y, double rw, double rh)
s t a t i c void polygon(double[] x, doublet] y)
s t a t i c void filledPolygo n(double[] x, doublet] y)
Interfejs API opracowanej przez nas biblioteki metod statycznych
do standardow ego rysowania (m etody do rysowania)
Standardowe rysow anie (m etody pom ocnicze) Biblioteka obejmuje też m etody do
zmiany skali i rozmiaru płótna, koloru i szerokości linii, czcionki tekstu oraz czasu
rysowania (do wykorzystania w animacjach). Jako argument m etody setPenColor()
można zastosować jeden ze zdefiniowanych kolorów: BLACK, BLUE, CYAN, DARK_GRAY,
GRAY, GREEN, LIGHT_GRAY, MAGENTA, ORANGE, PINK, RED, B00K_RED, WHITE i YELLOW. Są one
zdefiniowane jako stałe w bibliotece StdDraw (dlatego do ich określania służy kod
w rodzaju [Link]). Okno obejmuje też opcje m enu służące do zapisywania
rysunku w pliku w formacie odpowiednim do publikowania w internecie.
p ub lic c l a s s StdDraw
s t a t i c void s etXscale (d ouble x0, double 1) Ustawia przedział dla x na (xg, x )
s t a t i c void setYscale (d ouble yO, double 1) Ustawia przedział dla y na (yg, y )
s t a t i c void setPenRadius(double r) Ustawia szerokość pióra na r
s t a t i c void setPenColor( Color c) Ustawia kolor pióra na c
s t a t i c void setFon t(Font f) Ustawia czcionkę tekstu n a f
s t a t i c void set C a n v a s S iz e ( in t w, i n t h) Ustawia płótno na okno o wymiarach w n a h
s t a t i c void c l e a r ( C o lo r c) Czyści zawartość płótna i zapełniają kolorem c
s t a t i c void show(int dt) Wyświetla wszystko; wstrzymuje pracę na dt
milisekund
Interfejs API opracowanej przez las biblioteki metod statycznych
do standardowego rysów nia (metody pomocnicze)
R O ZD ZIA Ł 1 a Podstaw y
w t e j k s i ą ż c e używamy biblioteki StdDraw do analizowania danych i tworzenia
wizualnej reprezentacji algorytmów. W tabeli na następnej stronie przedstawiono
pewne możliwości. Wiele innych przykładów opisano w tekście i w ćwiczeniach
w książce. Biblioteka obsługuje też animacje. Temat ten, co oczywiste, poruszono
głównie w witrynie.
1.1 o Podstawowy m odel program owania
Dane Implementacja rysowania (fragment kodu) Efekt
i n t N = 100;
[Link] Xscale(0, N) ;
StdDra [Link] Ysc ale(0 , N*N);
[Link](.Ol) ;
Wartości f o r ( i n t i = 1; i <= N; i++)
funkcji {
S t d D ra w .p o in t( i, i ) ;
S t d D ra w .p o in t( i, i * i ) ;
S t d D ra w .p o in t( i, i * M a t h . l o g ( i )) ;
}
i n t N = 50;
double[] a = new double[N] ;
f o r (i n t i = 0 ; i < N; i++)
a [ i ] = [Link];
f o r ( i n t 1 = 0; i < N; i++ )
Tablica
losowych
{
double x = 1.0*i/N;
wartości double y = a [ i ] / 2 . 0 ;
double rw = 0.5/N;
double rh = a [ i ] / 2 . 0 ;
[Link](x, y, rw, rh);
)
i n t N = 50;
double[] a = new double[N] ;
f o r (i n t i = 0; i < N; i++ )
a [i ] = [Link];
A rrays.s o r t ( a ) ;
Posortowana f o r ( i n t i = 0; i < N; i++ )
tablica losowych (
wartości double x = 1.0*i/N;
double y = a [ i ] / 2 . 0 ;
double rw = 0.5/N;
double rh = a [ i ] / 2 . 0 ;
[Link](x, y, rw, rh);
Przykłady rysowania za pomocą StdDraw
58 R O ZD ZIA Ł 1 ■ Podstawy
Wyszukiwanie binarne Przykładow y program Javy, od którego zaczęliśmy,
przedstaw iony na następnej stronie, o party jest na znanym , skutecznym i pow szech
nie stosow anym algorytm ie w yszukiwania binarnego. N a przykładzie tego program u
pokazano, ja k analizow ane są nowe algorytm y z książki. Podobnie jak dla wszyst
kich om aw ianych program ów , dostępna jest zarów no dokładna definicja metody, jak
i kom pletna im plem entacja w Javie, którą m ożna pobrać z witryny.
W yszukiw anie binarne Algorytm wyszukiwania binarnego przeanalizowano szczegóło
wo w p o d r o z d z i a l e 3 .2 , tu jednak warto podać krótki opis. Algorytm zaimplementowa
no w metodzie statycznej ran k (). Przyjmuje ona
Udane wyszukiw anie wartości 23
lo mid hi jako argumenty klucz w postaci liczby całkowi
I I jr
10 1112 16 18 23 29 33 48 54 57 68 77 84 98 tej i posortowaną tablicę wartości typu i nt oraz
lo mid hi zwraca indeks klucza, jeśli znajduje się w tablicy,
ł i ł lub — w przeciwnym razie — wartość -1. W tym
10 11 12 16 18 23 29
l o mid hi
celu m etoda przechowuje zmienne 1 o i h i, takie
ł ł ł
10 11 12 16 18 23 29 33 48 54 57 68 77 8-1 9 że klucz znajduje się w a [1 o . . h i] , jeśli istnieje
Nieudane w yszukiw anie wartości 50
w tablicy. Dalej rozpoczyna się pętla, w której
lo mid hi sprawdzany jest środkowy element przedziału
+ ł ł (o indeksie mi d). Jeśli klucz jest równy a[mid],
10 1112 16 18 23 29 33 48 54 57 68 77 84 98
1o mi d hi zwracana wartość to mid. W przeciwnym razie
i i + m etoda dzieli przedział mniej więcej na połowę
10 11 i.2 U I 48 54 57 68 77 84 98
l o mid hi i przeszukuje lewą część, jeśli klucz m a wartość
I I I
10 11 12 16 18 23 29 3: 48 54 57 68 77 84 98 mniejszą niż a [mi d ] , lub prawą część, jeżeli klucz
l o mid hi
jest większy niż a [mid]. Proces kończy się po
\+ /
10 11 12 16 18 23 29 33 48 54 57 68 77 84 98 znalezieniu klucza lub opróżnieniu przedziału.
hi l o
Wyszukiwanie binarne jest skuteczne, ponieważ
+ ł
10 11 12 16 18 23 29 33 48 54 57 68 77 84 98 znalezienie klucza (lub ustalenie, że nie m a go
Wyszukiwanie binarne w posortowanej tablicy w tablicy) wymaga sprawdzenia niewielu ele
m entów tablicy (w stosunku do jej wielkości).
tin y w .tx t tin yT .txt
K lie n t w spo m a g a ją cy tw o rze n ie aplikacji D la każdej
84 23
im plem entacji algorytm u zam ieszczam y w spom agają 48 50
cego tw orzenie aplikacji klienta main(), którego m ożna 68 10 *
użyć razem z przykładow ym , zam ieszczonym w książce 10 99
18 18'
i w itrynie plikiem wejściowym, aby poznać algorytm 98 23
12 98 Nie występują
i sprawdzić jego wydajność. W przykładzie klient wczy
23 84 w tin y W .tx t
tuje liczby całkowite z pliku podanego w w ierszu poleceń, 54 11
a następnie wyświetla w standardow ym wyjściu liczby 57 10
48 48
całkowite, które nie w ystępują w pliku. Krótkie pliki te 33 77)
stowe, takie jak pokazany po prawej, służą tu do d e m o n 16 13
77 54
stracji pracy p rogram u oraz jako podstaw a do śledzenia 11 98
działania kodu i w przykładach. Do m odelow ania p ra 29 77
cy rzeczywistych aplikacji i testow ania w ydajności służą 77
68
duże pliki testowe (zobacz stronę 60). Krótki plik testowy dla klienta
testowego programu B in a r y S e a r c h
1.1 Podstawowy model program ow ania 59
Wyszukiwanie binarne
import ja v a . u t il . A r r a y s ;
public c la s s BinarySearch
{
public s t a t ic in t ran k(in t key, in t [] a)
{ // Tablica, musi być posortowana,
in t lo = 0;
in t hi = [Link] - 1;
while (lo <= hi)
{ // Klucz znajduje s ię wa [ l o . . h i ] lub nie ma go w ta b lic y ,
in t mid = lo + (hi - lo) / 2;
if (key < a [mid]) hi = mid - 1;
else i f (key > a [mid]) lo = mid + 1;
else return mid;
}
return -1;
}
public s t a t ic void m ain(String[] args)
{
in t [ ] w h i t e li s t = I n . r e a d l n t s ( a r g s [0]);
A rra y [Link] rt(w h ite list);
while (IS t d ln .isE m p t y O )
{ // Wczytywanie klucza i wyświetlanie go, j e ś l i nie znajduje się
// na b iałej 1 iś c ie .
in t key = S t d l n . r e a d l n t Q ;
i f (rank(key, w h i t e li s t ) < 0)
S td O u t .p rin t ln (k e y );
}
}
}
Program przyjmuje jako argument nazwę pliku z białą listą (z ciągiem liczb całkowitych)
i odfiltrowuje wszystkie wartości ze standardowego wejścia, które znajdują się na białej liście.
Pozostawia tylko liczby nieznajdujące się na liście. W celu wydajnego wykonania zadania
wykorzystano algorytm wyszukiwania binarnego zaimplementowany w metodzie statycznej
rank (). Pełne omówienie, dowód popraw
ności, analizy wydajności i zastosowania Java B in arySearch tinyW .txt < t in y T . t x t
algorytmu wyszukiwania binarnego przed- gg
stawiono w p o d r o z d z i a l e 3 .1 . 13
RO ZD ZIA Ł 1 o Podstawy
Stosowanie białych list Kiedy to możliwe, opisywane klienty wspomagające tworze
nie aplikacji odzwierciedlają praktyczne sytuacje i ilustrują potrzebę stosowania da
nego algorytmu. Tu związana jest ona z procesem stosowania białych list. Wyobraźmy
sobie operatora kart kredytowych, który musi sprawdzać, czy transakcje użytkowni
ka dotyczą prawidłowego konta. W tym celu można:
■ Przechowywać num ery kont użytkowników w pliku nazywanym białą listą.
■ Przekazywać num er konta powiązany z każdą transakcją do standardowego
strumienia wejścia.
* Korzystać z klienta testowego do umieszczania w standardowym wyjściu n u
merów, które nie są powiązane z żadnym użytkownikiem. Operator prawdopo
dobnie zechce odrzucić takie transakcje.
Możliwe, że duża firma z milionami użytkowników będzie musiała przetwarzać milio
ny transakcji. Aby zamodelować tę sytuację, w witrynie udostępniliśmy pliki [Link]
(z m ilio n e m liczb całkowitych) i [Link] (z 10 milionami liczb całkowitych).
W ydajność Często nie wystarczy utworzyć działający program. Na przykład dużo
prostsza implementacja algorytmu rank(), która nie wymaga nawet sortowania tab
licy, polega na sprawdzaniu każdego elementu:
public s t a t i c in t ra n k (in t key, in t[] a)
{
fo r (in t i = 0 ; i < a .le n g th ; i++)
i f ( a [ i] == key) return i ;
return - 1 ;
}
Skoro istnieje to proste i zrozumiałe rozwiązanie, po co stosować sortowanie przez
scalanie i wyszukiwanie binarne? W ć w ic z e n iu 1 . 1.38 okazuje się, że kom puter jest
zbyt wolny, aby m ożna użyć tak prymitywnej implementacji metody ran k () dla dużej
liczby danych wyjściowych (na przykład miliona elementów białej listy i 10 milionów
transakcji). Rozwiązanie problemu białych list dla dużej liczby danych wyjściowych jest
niemożliwe bez wydajnych algorytmów, takich jak wyszukiwanie binarne i sortowanie
przez scalanie. Wysoka wydajność ma często kluczowe znaczenie, dlatego w p o d r o z
d z ia l e 1.4 przedstawiono podstawy badania wydajności. Ponadto dla każdego om a
wianego algorytmu (w tym dla wyszukiwania binarnego, p o d r o z d z ia ł 3 . 1 , i sorto
wania przez scalanie, p o d r o z d z ia ł 2 .2 ) opisano cechy z obszaru wydajności.
w t y m m ie js c u celem dokładnego nakreślenia modelu programowania jest zagwa
rantowanie, że zdołasz na swoim komputerze uruchomić kod w rodzaju programu
Bi narySearch, użyć kodu do przetestowania danych podobnych do użytych w roz
dziale i zmodyfikować program pod kątem różnych sytuacji (takich j ale opisane w ćwi
czeniach w końcowej części podrozdziału), aby możliwie dobrze zrozumieć jego za
stosowania. Zarysowany model programowania ma ułatwiać wykonywanie talach
czynności. Są one kluczowe w przedstawionym podejściu do badania algorytmów.
1.1 H Podstawowy model program owania
la r g e w . t x t largeT .txt
489910
18940
774392
490636
125544
407391
115771
992663
923282
176914
217904
571222
519039
395667
Nie występują
w la r g e w .t x t
1 000 000
wartości
typu i n t
10 000 000
wartości
typu i n t
% ja v a B in a rySe a rch la r g e w .t x t < la r g e T . t x t
499569
984875
295754
207807
140925
161828
t
3 675 966
wartości
typu i n t
Duże pliki dla klienta testowego
programu B in a r y S e a r c h
RO ZD ZIA Ł 1 * Podstawy
Perspektywa W tym miejscu opisano elegancki i kompletny model program o
wania, który służył (i nadal służy) licznym programistom przez wiele dziesięcioleci.
Jednak we współczesnym programowaniu posunięto się o krok dalej. Ten następ
ny poziom to abstrakcja danych, czasem nazywana programowaniem obiektowym.
Jest to temat następnego podrozdziału. Ujmijmy to prosto — abstrakcja danych ma
umożliwiać definiowanie w programach typów danych (zbiorów wartości i zbiorów
operacji na nich), a nie tylko m etod statycznych działających na wbudowanych
typach danych.
Programowanie obiektowe w ostatnich dziesięcioleciach zyskało dużą popularność,
a abstrakcja danych jest kluczowa we współczesnym programowaniu. Abstrakcję da
nych uwzględniamy w tej książce z trzech głównych powodów.
■ Pozwala zwiększyć zakres wielokrotnego użytku kodu przez programowanie
modularne. Na przykład techniki sortowania z r o z d z i a ł u 2 . oraz wyszukiwa
nie binarne i inne algorytmy z r o z d z i a ł u 3 . umożliwiają klientom korzystanie
z tego samego kodu dla dowolnego typu danych (nie tylko dla liczb całkowi
tych), w tym typu zdefiniowanego w kliencie.
■ Zapewnia wygodny mechanizm do budowania tak zwanych powiązanych
struktur danych, które zapewniają większą elastyczność niż tablice i w wielu
sytuacjach są podstawą wydajnych algorytmów.
■ Umożliwia precyzyjne zdefiniowanie napotkanych problemów algorytmicz
nych. Na przykład algorytmy dla problemu Union-Find z p o d r o z d z i a ł u 1 . 5 ,
algorytmy kolejki priorytetowej z p o d r o z d z i a ł u 2.4 i algorytmy tablicy sym
boli z r o z d z i a ł u 3. mają pozwalać definiować struktury danych, które um oż
liwiają wydajne zaimplementowanie zbioru operacji. Abstrakcja danych dosko
nale nadaje się do rozwiązania tego problemu.
Mimo wagi tych kwestii koncentrujemy się na badaniu algorytmów. Dlatego dalej
omawiamy kluczowe cechy programowania obiektowego związane z tym zadaniem.
1.1 a Podstawowy m odel program owania
^ Pytania i odpowiedzi
P. Czym jest kod bajtowy Javy?
O. Jest to niskopoziomowa wersja program u działająca w maszynie wirtualnej Javy.
Ten poziom abstrakcji ułatwia programistom Javy zagwarantowanie, że programy
będą działać na różnorodnych urządzeniach.
P. Wydaje się błędem, że Java umożliwia przepełnienie wartości typu i nt i zwrócenie
błędnych wartości. Czy Java nie powinna automatycznie wykrywać przepełnienia?
O. Kwestia ta rodzi spory wśród programistów. Oto krótka odpowiedź — brak spraw
dzania to jeden z powodów, dla których pewne typy danych są nazywane prostymi.
Wiedza pozwala w dużym stopniu uniknąć takich problemów. Należy stosować typ
in t dla małych liczb (mających mniej niż 10 cyfr w zapisie dziesiętnym), a typ long
dla wartości na poziomie miliardów lub większych.
P. Jaka jest wartość wyrażenia Math. abs (-2147483648) ?
O. -2147483648. Ten dziwny (ale prawdziwy) wynik to typowy efekt przepełnienia
liczby całkowitej.
P. Jak można zainicjować zmienną typu doubl e nieskończonością?
O. Java udostępnia w tym celu wbudowane stałe: Double.POSITIVE_INFINITY
i [Link].
P. Czy m ożna porównywać wartości typów doubl e i i nt?
O. Wymaga to konwersji typu, warto jednak pamiętać, że Java zwykle automatycznie
przeprowadza wymaganą konwersję. Na przykład jeśli x to zmienna typu i nt o war
tości 3, wyrażenie (x < 3.1) ma wartość true. Java przed porównaniem przekształca
x na typ doubl e (ponieważ 3 . 1 to literał tego typu).
P. Co się stanie, jeśli użyję zmiennej przed jej zainicjowaniem?
O. Jeżeli w kodzie istnieje ścieżka prowadząca do użycia niezainicjowanej zmiennej,
Java zgłosi błąd czasu kompilacji.
P. Jakie wartości mają w Javie wyrażenia 1/0 i 1.0/0.0?
O. Pierwsze spowoduje wyjątek czasu wykonania związany z dzieleniem przez zero
(powoduje to zatrzymanie programu, ponieważ wartość jest niezdefiniowana).
Drugie ma wartość Infini ty.
RO ZD ZIA Ł 1 o Podstawy
Pytania i odpowiedzi (ciąg dalszy)
P. Czy można używać symboli < i > do porównywania zmiennych typu S tri ng?
O. Nie. Operatory te są zdefiniowane tylko dla typów prostych. Zobacz tekst na stro
nie 92.
P. Jaki jest wynik dzielenia i reszta dla ujemnych liczb całkowitych?
O. Iloraz a/b zaokrąglany jest w kierunku [Link] z operacji a %b jest definio
wana tak: (a / b) * b + a % b jest zawsze równe a. Na przykład-14/3 i 14/-3 t o -4,
natomiast -14 % 3 to -2, a 14 % -3 to 2.
P. Dlaczego piszemy (a && b), a nie (a & b)?
O. Operatory &, | i ^ to bitowe operatory logiczne dla typów całkowitoliczbowych,
obliczające część wspólną, różnicę i różnicę symetryczną dla bitów z każdej pozycji.
Tak więc wartość 10&6 to 14, a 10^6 to 12. W książce rzadko stosujemy te operatory.
&& i | | są poprawne tylko dla wyrażeń logicznych, analizowanych osobno z uwagi
na przetwarzanie skrócone. Przetwarzanie wyrażenia odbywa się od lewej do prawej
i kończy się, kiedy wartość jest znana.
P. Czy dwuznaczność w zagnieżdżonych instrukcjach i f stanowi problem?
O. Tak. W Javie kod:
i f <wyrl> i f <wyr2> <instA> e ls e <instB>
jest równoważny poniższemu:
i f <wyrl> { i f <wyr2> <instA> else <instB> }
choć m ożna by sądzić, że odpowiada zapisowi:
i f <wyrl> { i f <wyr2> <instA> } e ls e <instB>
Korzystanie z nawiasów to dobry sposób na uniknięcie problemu wiszącej instrukcji
el se.
P. Jaka jest różnica między pętlą fo r a pętlą whi 1 e?
O. Kod w nagłówku pętli fo r jest traktowany tak, jakby znajdował się w tym samym
bloku, co ciało pętli. W typowej pętli fo r zmienna używana do inkrementacji nie jest
dostępna w instrukcjach poza pętlą, natomiast w pętli while jest. To rozróżnienie
często prowadzi do stosowania pętli whi 1 e zamiast for.
P. Niektórzy programiści używają do deklarowania tablic zapisu in t a[] zamiast
i nt [] a. Czym różnią się te formy?
1.1 Ei Podstawowy model program ow ania
O. W Javie obie wersje są poprawne i równoważne. Pierwsza odpowiada sposobo
wi deklarowania tablic w języku C. Druga jest preferowana w Javie, ponieważ typ
zmiennej i nt [] bardziej jednoznacznie określa, że jest to tablica liczb całkowitych.
P. Dlaczego indeksy tablic zaczynają się od 0, a nie od 1?
O. Zwyczaj ten pochodzi z programowania w języku maszynowym, gdzie adres
elementu tablicy obliczano przez dodanie indeksu do adresu początku tablicy.
Rozpoczynanie indeksów od 1 powodowało marnowanie pamięci na początku tabli
cy lub czasu na odejmowanie 1 .
P. Jeśli a [] to tablica, dlaczego instrukcja StdOut. pri ntl n (a) wyświetla szesnastko
wą liczbę całkowitą, na przykład @f62373, zamiast elementów tablicy?
O. Dobre pytanie. Instrukcja wyświetla adres zajmowany przez tablicę w pamięci,
który — niestety — rzadko jest tym, czego programista potrzebuje.
P. Dlaczego w książce nie są używane standardowe biblioteki Javy dla wejścia i gra
fiki?
O. Są używane, ale wolimy korzystać z prostszych abstrakcyjnych modeli. Biblioteki
Javy, na których oparto Stdln i StrDraw, zbudowano na potrzeby pisania kodu pro
dukcyjnego, dlatego same biblioteki i ich interfejsy API są nieco chaotyczne. Aby się
o tym przekonać, warto przyjrzeć się kodowi bibliotek Stdln. java i StdDraw. java.
P. Czy program może ponownie wczytać dane ze standardowego wejścia?
O. Nie. Dane można wczytać tylko raz (podobnie nie można wycofać instrukcji
p rin tln (J).
P. Co się stanie, jeśli program spróbuje wczytać dane po wyczerpaniu zawartości
wejścia standardowego?
O. Zgłoszony zostanie błąd. Instrukcja [Link] pty() pozwala uniknąć takiego
błędu przez sprawdzenie, czy dane wejściowe są dostępne.
P. Co oznacza poniższy komunikat o błędzie?
Exception in thread "main" j a v a . [Link]: Stdln
O. Prawdopodobnie zapomniano umieścić biblioteki Stdln. java w katalogu robo
czym.
P. Czy w Javie m etoda statyczna może przyjmować inną m etodę statyczną jako
argument?
O. Nie. To dobre pytanie, ponieważ wiele innych języków to umożliwia.
66 R O ZD ZIA Ł 1 a Podstawy
| ĆWICZENIA
1.1.1. Podaj wartość każdego z poniższych wyrażeń:
a. ( 0 + 15 ) / 2
b. 2.0e-6 * 100000000.1
c. true && f a ls e || true && true
1.1.2. Podaj typ i wartość każdego z poniższych wyrażeń:
a. (1 + 2 .2 3 6 )¡2
b. 1 + 2 + 3 + 4.0
c. 4.1 >= 4
d. 1 + 2 + "3"
1.1.3. Napisz program, który przyjmuje trzy całkowitoliczbowe argumenty z wier
sza poleceń i wyświetla słowo równe, jeśli wartości są takie same, i ni erówne w prze
ciwnym przypadku.
1.1.4. Jakie błędy (i czy w ogóle) znajdują się w poniższych instrukcjach?
a. if (a > b) then c = 0;
b. ifa>b {c = 0 ; }
c. i f (a > b) c = 0 ;
d. i f (a > b) c = 0 else b = 0 ;
1.1.5. Napisz fragment kodu, który wyświetla wartość true, jeśli zmienne x i y typu
doubl e znajdują się w przedziale od 0 do 1, a w przeciwnym razie wyświetla wartość
fal se.
1.1. 6 . Jakie dane wyświetli poniższy program?
in t f = 0;
in t g = 1;
for (in t i = 0; i <= 15; i++)
(
S t d O u t . p r in t ln ( f ) ;
f = f + g;
g = f - g;
}
1.1 ■ Podstawowy m odel program ow ania 67
1.1.7. Podaj wartość wyświetlaną przez każdy z poniższych fragmentów kodu:
a. double t = 9.0;
while ([Link](t - 9.0/t) > .001)
t = (9.0/t + t) / 2.0;
S t d 0 u t . p r in t f ( " % . 5 f \ n " , t ) ;
b. in t sum = 0;
fo r (in t i = 1; i < 1000; i++)
fo r (in t j = 0; j < i ; j++)
sum++;
Std O u t.p rin tln (su m );
c. in t sum = 0;
f o r ( in t i = 1; i < 1000; i *= 2)
f o r (in t j = 0; j < 1000; j++)
sum++;
Std O u t.p rin tln (su m );
1 .1. 8 . Co wyświetla każda z poniższych instrukcji?
a. System.o u t . p r i n t l n ( ' b ' ) ;
b. S y s t e m .o u t .p r in t ln ('b ' + 1c ' ) ;
c. [Link] .p rin tl n( (char) ( ' a' + 4 ) ) ;
Wyjaśnij wszystkie skutki.
1.1.9. Napisz fragment kodu, który umieszcza binarną reprezentację dodatniej licz
by całkowitej Nw zmiennej s typu S t r i ng.
Rozwiązanie: Java udostępnia wbudowaną metodę In t e g e r . toBi naryStri ng (N), któ
ra wykonuje potrzebną operację, jednak celem ćwiczenia jest pokazanie, jak zaimple
mentować taką metodę. Oto wyjątkowo zwięzłe rozwiązanie:
String s =
f o r ( i n t n = N; n > 0; n /= 2)
s = (n % 2) + s;
RO ZD ZIA Ł 1 n Podstawy
ĆWICZENIA (ciąg dalszy)
1.1.10. Jaki błąd znajduje się w poniższym fragmencie kodu?
i n t [] a;
fo r (in t i = 0; i < 10; i++)
a [i] = i * i ;
Rozwiązanie: nie przydzielono tu pamięci dla a[] za pom ocą new. Kod ten prowadzi
do błędu czasu kompilacji v a riab le a might not have been i n i t i a l i z e d (zmienna
a mogła nie zostać zainicjowana).
1.1.11. Napisz fragment kodu, który wyświetla zawartość dwuwymiarowej tablicy
wartości logicznych. Użyj * do reprezentowania wartości tru e i odstępu do reprezen
towania fal se. Dodaj num ery wierszy i kolumn.
1.1.12. Co wyświetla poniższy kod?
i nt [] a = new in t [10];
f o r (in t i = 0 ; i < 1 0 ; i++)
a [i] = 9 - i;
fo r (in t i = 0; i < 10; i++)
a [i] = a [ a [ i ] ] ;
f o r (in t i = 0; i < 10; i++)
System, out. p rin t In ( i );
1.1.13. Napisz fragment kodu do wyświetlania transpozycji (tablicy z przestawiony
mi wierszami i kolumnami) dwuwymiarowej tablicy o M wierszach i N kolumnach.
1.1.14. Napisz metodę statyczną lg (), która przyjmuje jako argument wartość N
typu i nt i zwraca największą wartość typu i nt nie większą niż logarytm o podstawie
2 dla N. Nie używaj biblioteki Math.
1.1.15. Napisz m etodę statyczną histogram(), przyjmującą jako argumenty tablicę
a [] wartości typu i nt i liczbę całkowitą Moraz zwracającą tablicę o długości M, której
i-ty element to liczba wystąpień liczby całkowitej i w tablicy podanej jako argument.
Jeśli wszystkie wartości w a[] znajdują się w przedziale od 0 do M-l, suma wartości
w zwróconej tablicy powinna być równa a . 1ength.
1.1.16. Podaj wartość wywołania exRl(6):
public s t a t i c S t r in g e x R l(in t n)
{
i f (n <= 0) return
return exRl(n-3) + n + exRl(n-2) + n;
1.1 e Podstawowy model program owania
1.1.17. Podaj wady poniższej funkcji rekurencyjnej:
public s t a t i c S tring exR2(int n)
{
S t r in g s = exR2(n-3) + n + exR2(n-2) + n;
i f (n <= 0) return
return s;
}
Odpowiedź: funkcja nigdy nie dojdzie do przypadku podstawowego. Wywołanie
exR2(3) spowoduje wywołania exR2(0), exR2(-3), exR3(-6) i tak dalej do czasu wy
stąpienia błędu StackOverflowError.
1.1.18. Rozważ poniższą funkcję rekurencyjną:
public s t a t ic in t mystery(int a, in t b)
{
i f (b == 0) return 0;
i f (b % 2 == 0) return mystery(a+a, b/2);
return mystery(a+a, b/2) + a;
}
Jakie wartości mają wywołania mystery (2, 25) imystery(3, 11)? Opisz, jaką wartość
obliczy funkcja mystery (a, b) dla dodatnich liczb całkowitych a i b. Odpowiedz na to
samo pytanie, ale zastąp + znakiem *, a instrukcję return 0 — wywołaniem return 1.
1.1.19. Uruchom na komputerze następujący program:
public c lass Fibonacci
{
public s t a t ic long F (in t N)
{
i f (N == 0) return 0;
i f (N == 1) return 1;
return F(N-l) + F (N -2 );
}
public s t a t i c void m ain(String[] args)
{
for (in t N = 0; N < 100; N++)
S td 0 u [Link] tln (N + " " + F(N));
}
R O ZD ZIA Ł 1 ■ Podstawy
ĆWICZENIA (ciąg dalszy)
Jaka jest największa wartość N, przy której program obliczy wartość F(N) w mniej
niż godzinę? Opracuj lepszą implementację F(N), która zapisuje obliczone wartości
w tablicy.
1.1.20. Napisz rekurencyjną metodę statyczną obliczającą wartość ln(Nl).
1.1.21. Napisz program, który wczytuje wiersze z wejścia standardowego, przy czym
każdy wiersz obejmuje nazwisko i dwie liczby całkowite. Program ma następnie za
pomocą m etody pri n tf () wyświetlać tabelę z kolum ną z nazwiskiem, liczbami cał
kowitymi i wynikiem dzielenia pierwszej liczby przez drugą z dokładnością do trzech
miejsc po przecinku. Programu tego typu można użyć do wyświetlenia w tabeli śred
nich wybić dla baseballistów lub średnich ocen dla studentów.
1.1.22. Napisz wersję programu Bi narySearch, która używa rekurencyjnej m eto
dy rank() przedstawionej na stronie 37 i rejestruje wywołania metod. Przy każdym
wywołaniu metody rekurencyjnej należy wyświetlić wartości argumentów 1 o i hi
z wcięciem określającym głębokość rekurencji. Wskazówka: dodaj do m etody reku
rencyjnej argument określający głębokość.
1.1.23. Dodaj do klienta testowego program u Bi narySearch mechanizm reagowa
nia na drugi argument, którym może być + (nakazuje wyświetlanie liczb ze standar
dowego wejścia, które nie znajdują się na białej liście) lub - (powoduje wyświetlanie
liczb znajdujących się na białej liście).
1.1.24. Podaj ciąg wartości p i ą uzyskanych przy stosowaniu algorytmu Euklidesa
do obliczania największego wspólnego dzielnika liczb 105 i 24. Rozwiń kod ze stro
ny 16, aby utworzyć program Eucl i d, który pobiera dwie liczby całkowite z wiersza
poleceń, oblicza największy wspólny dzielnik i wyświetla dwa argumenty przy każ
dym wywołaniu m etody rekurencyjnej. Użyj programu do obliczenia największego
wspólnego dzielnika liczb 1111111 i 1234567.
1.1.25. Użyj indukcji matematycznej do udowodnienia, że algorytm Euklidesa obli
cza największy wspólny dzielnik dowolnej pary nieujemnych liczb całkowitych p i q.
1.1 n Podstawowy m odel program owania
[j p r o b l e m y d o r o z w ią z a n ia
1.1.26. Sortow anie trzech liczb. Załóżmy, że zmienne a, b, c i t są tego samego licz
bowego typu prostego. Wykaż, że poniższy kod porządkuje a, b i c w kolejności ros
nącej:
if (a > b) { t = a; a = b; b = t ; }
if (a > c) { t = a; a = c ; c=t; }
if (b>c) { t = b ; b=c; c=t; }
1.1.27. R ozkład dw u m ian ow y. Oszacuj liczbę rekurencyjnych wywołań używanych
przez kod:
public s t a t ic double binomial (in t N, in t k, double p)
{
i f (N == 0 && k == 0) return 1.0;
i f (N < 0 || k < 0) return 0.0;
return (1.0 - p )*b in om ia l(N -l, k, p) + p*bin om ial(N-l, k-1, p ) ;
}
do obliczenia wyrażenia binomial (100, 50). Opracuj lepszą implementację opartą
na zapisywaniu obliczonych wartości w tablicy.
1.1.28. Usuwanie duplikatów . Zmodyfikuj klienta testowego dla programu
Bi narySearch, tak aby po sortowaniu usuwał powtarzające się klucze z białej listy.
1.1.29. Rów ne klucze. Dodaj do programu BinarySearch metodę statyczną rank(),
która przyjmuje jako argumenty klucz i posortowaną tablicę wartości typu i nt (nie
które z nich mogą być sobie równe), a zwraca liczbę elementów mniejszych niż klucz.
Dodaj też podobną metodę count (), zwracającą liczbę elementów równych kluczowi.
Uwaga: jeśli i oraz j to wartości zwrócone przez m etody ran k(key, a) icount(key,
a ), to a [ i . . i +j - 1 ] są wartościami w tablicy równymi atrybutowi key.
1.1.30. Ć w iczenie dotyczące tablic. Napisz fragment kodu, który tworzy tablicę war
tości logicznych, a [] [], o wymiarach N n a N. W tablicy element a [i] [j] ma wartość
true, jeśli i oraz j to liczby względnie pierwsze (nie mają wspólnych dzielników).
W przeciwnym razie element ma wartość fal se.
1.1.31. Losowe połączen ia. Napisz program, który przyjmuje argumenty z wiersza
poleceń (liczbę całkowitą N i wartość p typu double mieszczącą się w przedziale
0 - 1), rysuje w równomiernych odstępach N kropek o wielkości .05 na obwodzie
okręgu, a następnie, z prawdopodobieństwem p dla każdej pary punktów, łączy je
szarą linią.
R O ZD ZIA Ł 1 a Podstaw y
PROBLEMY DO ROZW IĄZANIA (ciąg dalszy)
1.1.32. H istogram . Załóżmy, że standardowy strum ień wejścia zawiera ciąg wartości
typu doubl e. Napisz program, który pobiera z wiersza poleceń liczbę całkowitą N
i dwie wartości typu doubl e, l oraz r, a następnie używa biblioteki StdDraw do naryso
wania histogramu z liczbą wartości ze standardowego strumienia wejścia mieszczą
cych się w każdym z N przedziałów wyznaczonych przez podział zbioru (/, r) na N
fragmentów o równej wielkości.
1.1.33. Biblioteka Matri x. Napisz bibliotekę Matri x z implementacją poniższego in
terfejsu API:
p ub lic c l a s s M atr ix
static double dot(dou ble[] x, double[] y) Iloczyn wektorowy
static doublet] [] mult (d ou b le [] [] a, doublet] [] b) Iloczyn macierzy
s t a t i c doublet] [] transpose (d ouble [] t] a) Transpozycja
static doublet] m ult (d ouble [] [] a, doublet] x) Iloczyn macierz-wektor
static doublet] mult(double[] y, doublet] □ a) Iloczyn wektor-macierz
Utwórz klienta testowego, który wczytuje wartości ze standardowego wejścia i testuje
wszystkie metody.
1.1.34. Filtrowanie. Która z poniższych operacji w ym aga zapisania wszystkich war
tości ze standardowego wejścia (na przykład w tablicy), a którą m ożna zaimplemen
tować jako filtr, używając jedynie stałej liczby zmiennych i tablic o stałym rozmiarze
(niezależnym od N)? Dla każdej operacji dane wejściowe pochodzą ze standardowe
go wejścia i składają się z N liczb rzeczywistych z przedziału od 0 do 1 .
■ Wyświetlanie wartości maksymalnej i minimalnej.
■ Wyświetlanie mediany liczb.
■ Wyświetlanie k -tej najmniejszej wartości dla k mniejszego niż 100.
■ Wyświetlanie sumy kwadratów liczb.
■ Wyświetlanie średniej N liczb.
■ Wyświetlanie procentu liczb większych od średniej.
■ Wyświetlanie N liczb w porządku rosnącym.
■ Wyświetlanie N liczb w losowej kolejności.
1.1 a Podstaw ow y model program owania
[ eksperym en ty
1.1.35. Sym ulowanie rzutu kostką. Poniższy kod oblicza rozkład prawdopodobień
stwa sumy oczek na dwóch kostkach:
in t SIDES = 6;
double[] d is t = new double[2*SIDES+1];
fo r (in t i = 1; i <= SIDES; i++)
fo r (in t j = 1; j <= SIDES; j++)
d ist[i+ j] += 1.0;
fo r (in t k = 2; k <= 2*SIDES; k++)
d is t[ k ] /= 36.0;
Wartość d ist[k ] to prawdopodobieństwo, że suma oczek na kostkach to k.
Przeprowadź eksperymenty, aby potwierdzić poprawność tych obliczeń. Zasymuluj
N rzutów kostkami i zachowaj częstotliwość wystąpień każdej wartości przy oblicza
niu sum dwóch losowych liczb całkowitych z przedziału od 1 do 6 . Jak duże musi być
N, aby empiryczne wyniki pasowały do precyzyjnych rezultatów z dokładnością do
trzech miejsc po przecinku?
1.1.36. Em piryczne spraw dzanie przetasow ania. Przeprowadź eksperymenty, aby
sprawdzić, że przedstawiony na stronie 44 kod do przetasowywania wartości dzia
ła w opisany sposób. Napisz program ShuffleTest, który pobiera z wiersza poleceń
argumenty M i N , N razy przetasowuje elementy tablicy o rozmiarze M inicjowanej
przed każdym przestawianiem wartościami a [i] = i i wyświetla tablicę M na M,
w której wiersz i zawiera liczbę wystąpień wartości i na pozycji j dla wszystkich
możliwych j . Wszystkie wartości w tablicy powinny być bliskie N/ M.
1.1.37. N iepopraw ne przetasow anie. Załóżmy, że w kodzie do przetasowywania ele
mentów wybieramy losową liczbę całkowitą z przedziału od 0 do N- 1 zamiast z prze
działu od i do N-l. Pokaż, że wynikowa kolejność nie jest z równym prawdopodo
bieństwem jedną z N! możliwości. Przeprowadź dla tej wersji test z poprzedniego
ćwiczenia.
1.1.38. W yszukiwanie binarne a w yszu kiw an ie m eto d ę ataku siłowego. Napisz pro
gram BruteForceSearch z wykorzystaniem wyszukiwania metodą ataku siłowego, co
opisano na stronie 60. Porównaj czas działania tego program u na plikach largeW .txt
i [Link] z czasem pracy programu Bi narySearch.
R O ZD ZIA Ł 1 a Podstawy
EKSPERYMENTY (ciąg dalszy)
1.1.39. Losowe dopasowywanie. Napisz klienta program u BinarySearch, który p o
biera z wiersza poleceń wartość T typu i nt i urucham ia T prób opisanego dalej ekspe
rym entu dla N = 103,1 0 4,1 0 5 i 106. Program ma tworzyć dwie tablice N losowo gene
rowanych dodatnich sześciocyfrowych wartości typu i nt i znajdować liczbę wartości
występujących w obu tablicach, a następnie wyświetlać tablicę ze średnim poziomem
tej liczby dla T prób dla każdej wartości N.
1.2. A B S T R A K C JA D A N Y C H
t y p dan ych to zbiór wartości i operacji na tych wartościach. Wcześniej omówiono
szczegółowo proste typy danych Javy. Przykładowo, w artości prostego typu danych
in t wynoszą od - 2 31 do 231 - 1. O peracje na typie in t obejmują +, *, -, /, %, < i >.
W zasadzie wszystkie programy z tej książki m ożna napisać za pomocą samych wbu
dowanych typów prostych, jednak dużo wygodniej jest rozwijać kod na wyższym po
ziomie abstrakcji. W tym podrozdziale skoncentrowano się na procesie definiowania
i używania typów danych. Proces ten to abstrakcja danych (uzupełnia on abstrakcję
funkcji, będącą tematem p o d r o z d z ia ł u i . i ).
Programowanie w Javie w dużym stopniu oparte jest na budowaniu za pomocą
znanej instrukcji c la ss Javy typów danych nazywanych typ a m i referencyjnym i (ina
czej w skaźnikow ym i). Ten styl programowania to program ow anie obiektowe, związane
z pojęciem obiektu (jest to jednostka przechowująca wartość typu danych). Typy pro
ste Javy służą głównie do tworzenia programów działających na liczbach, natomiast
za pom ocą typów referencyjnych można pisać programy manipulujące łańcuchami
znaków, obrazami, dźwiękami i setkami innych abstrakcji dostępnych w bibliotekach
standardowych Javy lub w kodzie z witryny. Jeszcze ważniejsze niż biblioteki wbudo
wanych typów danych jest to, że zbiór typów danych w Javie jest otwarty, ponieważ
m ożna definiować w łasne ty p y danych, aby zaimplementować dowolną abstrakcję.
Abstrakcyjny typ danych (ang. abstract d ata type — ADT) to typ danych, które
go reprezentacja jest ukryta przed klientem. Implementowanie typu ADT jako klasy
Javy przebiega podobnie jak implementowanie biblioteki funkcji jako zbioru m etod
statycznych. Główna różnica polega na tym, że należy powiązać dane z implemen
tacją funkcji i ukryć reprezentację danych przed klientem. Przy korzystaniu z typów
ADT najważniejsze są operacje określone w interfejsie API. Nie trzeba zwracać uwagi
na reprezentację danych. W trakcie im plem entow ania typu ADT należy skoncentro
wać się na danych, a następnie zaimplementować operacje na nich.
Abstrakcyjne typy danych są ważne, ponieważ umożliwiają hermetyzację w pro
jekcie programu. W książce typy tego rodzaju służą do:
■ precyzyjnego ujmowania problemów w formie interfejsów API przeznaczonych
do użytku przez różne klienty;
■ opisywania algorytmów i struktur danych w implementacjach interfejsów API.
Główną przyczyną analizowania różnych algorytmów wykonujących to samo zada
nie jest ich odm ienna charakterystyka związana z wydajnością. Abstrakcyjne typy
danych zapewniają odpowiedni schemat do analizowania algorytmów, ponieważ
umożliwiają natychmiastowe wykorzystanie wiedzy o wydajności algorytmu — m oż
na zastąpić jeden algorytm innym, aby poprawić wydajność wszystkich klientów bez
zmiany kodu choćby jednego z nich.
1.2 b Abstrakcja danych
K orzystan ie z ab strak cyjn ych ty p ó w d an ych N ie trzeba zn a ć im plem enta
cji typu danych, aby m óc go stosować, dlatego rozpoczynamy od opisania programów
korzystających z nieskomplikowanego typu danych Counter. Jego wartości to nazwa
i nieujemna liczba całkowita, a operacje to tw orzen ie i inicjowanie zerem , inkrem en-
tacja o je d en i spraw dzanie obecnej wartości. Abstrakcja ta jest przydatna w wielu
kontekstach. Można na przykład użyć jej w oprogramowaniu do obsługi głosowania,
aby zagwarantować, że dana osoba może tylko zwiększyć liczbę głosów o jeden. Typ
można też zastosować do śledzenia podstawowych operacji w ramach analizowania
wydajności algorytmów. Aby móc używać typu Counter, trzeba poznać sposób wy
woływania operacji zdefiniowanych w typie danych oraz mechanizmy Javy służące
do tworzenia wartości typu danych i manipulowania nimi. Takie mechanizmy są
niezwykle istotne we współczesnym programowaniu. Korzystamy z nich w książce,
dlatego warto starannie przyjrzeć się pierwszemu przykładowi.
In terfejs A P I a b stra k c y jn e g o ty p u d a n y c h Do określania działania abstrakcyjnego
typu danych służy interfejs A P I (ang. application program m in g interface). Jest to lista
konstruktorów i m etod egzem plarza (operacji) wraz z nieformalnym opisem efektów
ich działania. Oto interfejs API typu Counter:
p ub lic c l a s s Counter
Coun te r(Stri ng i d) Tworzy licznik o nazwie id
void increment() Zwiększa wartość licznika o jeden
i n t tal 1y () Liczba inkrementacji od czasu utworzenia
S t r i ng t o S t r i ng() Reprezentacja w postaci łańcucha znaków
Interfejs API licznika
Choć podstawą definicji typu danych jest zbiór wartości, jego rola nie jest widoczna
w interfejsie API. Interfejs API obejmuje tylko operacje na wartościach. Dlatego de
finicja typu ADT pod wieloma względami przypomina bibliotekę m etod statycznych
(zobacz stronę 36).
n Oba elementy są implementowane jako klasy Javy.
■ Metody egzemplarza przyjmują zero lub więcej argumentów określonego typu,
rozdzielonych przecinkami i umieszczonych w nawiasach.
° Metody mogą zwracać wartość określonego typu lub w ogóle jej nie zwracać
(metody typu void).
Istnieją też trzy ważne różnice:
0 Niektóre elementy w interfejsach API mają nazwę taką samą jak klasa, ale nie
mają typu zwracanej wartości. Są to tak zwane konstruktory. Odgrywają one
specjalną rolę. Tu typ Counter ma konstruktor przyjmujący argument typu
S t r i ng.
RO ZD ZIA Ł 1 0 Podstaw y
■ Metody egzemplarza nie mają modyfikatora s t a t i c. N ie są to m etody statyczne
— służą do działania na wartościach typu danych.
■ Niektóre metody egzemplarza są tworzone ze względu na konwencje Javy.
Nazywamy je m etodam i o d ziedziczo n ym i i oznaczamy szarym kolorem w in
terfejsach API.
Interfejs API abstrakcyjnego typu danych, podobnie jak interfejs API bibliotek m e
tod statycznych, to kontrakt z wszystkimi klientami, a tym samym punkt wyjścia do
rozwijania kodu klienta i implementacji typu danych. Tu interfejs API informuje, że
przy stosowaniu typu Counter m ożna używać konstruktora Counter(), m etod eg
zemplarza increm ento i ta l 1y () oraz odziedziczonej m etody to S tri ng().
M etody odziedziczone Różne konwencje Javy umożliwiają wykorzystanie w budo
wanych mechanizmów języka w typie danych przez umieszczenie w interfejsie API
specyficznych metod. Na przykład wszystkie typy danych Javy d zied ziczą metodę
to S tri ng (), która na podstawie wartości typu danych zwraca reprezentację typu
String. Java wywołuje tę metodę, kiedy wartość typu danych jest złączana z wartoś
cią typu S tring za pom ocą operatora +. Implementacja domyślna nie jest specjal
nie przydatna (zwraca łańcuch znaków z adresem wartości typu danych w pam ię
ci), dlatego często udostępniamy implementację zastępującą domyślną. Wtedy m e
todę to S tri ng () umieszczamy w interfejsie API. Oto inne przykłady takich metod:
equals (), compárelo() ihashCode() (zobacz stronę 113).
K od klienta Podobnie jak w programowaniu m odularnym opartym na metodach
statycznych, tak i tu interfejs API pozwala pisać kod klienta bez wiedzy o szcze
gółach implementacji (a także pisać kod implementacji bez dokładnej znajomości
konkretnych klientów). Przedstawione na stronie 40 mechanizmy porządkowania
programów jako niezależnych modułów są przydatne we wszystkich klasach Javy,
dlatego można ich użyć do programowania m odularnego za pom ocą typów ADT,
jak i przy użyciu bibliotek m etod statycznych. Dlatego m ożna używać typów ADT
w dowolnym programie, jeśli kod źródłowy typu znajduje się w pliku .java w tym
samym katalogu lub w standardowej bibliotece Javy albo jest dostępny z uwagi na
instrukcję import lub inny opisany w książce mechanizm określania ścieżki do klasy.
Stosowanie typu ADT zapewnia wszystkie korzyści programowania modularnego.
Ukrycie całego kodu z implementacją typu danych w jednej klasie Javy umożliwia
rozwijanie kodu klienta na wyższym poziomie abstrakcji. Do tworzenia kodu klienta
potrzebna jest możliwość deklarowania zm iennych, tw orzen ia obiektów przechowu
jących wartości typu danych i zapew n iania dostępu do wartości działającym na nich
metodom egzemplarza. Czynności te różnią się od ich odpowiedników dla typów
prostych, choć m ożna zauważyć wiele podobieństw.
1.2 H Abstrakcja danych 79
Obiekty Zm ienną heads powiązaną z typem danych Counter można, oczywiście, za
deklarować za pom ocą kodu:
Counter heads;
Jak jednak można przypisywać wartości lub urucham iać operacje? Odpowiedź
na to pytanie związana jest z zagadnieniami podstawowymi w abstrakcji danych.
Obiekt jest jednostką, która przyjmuje wartość typu danych. Obiekty mają trzy klu
czowe cechy: stan, tożsamość i działanie. Stan obiektu to wartość jego typu danych.
Za tożsamość obiektu m ożna uznać miejsce, w którym wartość jest przechowywana
w pamięci. Działanie obiektu zależy od operacji typu da Jed e n o b ie k t ty p u C o u n te r
nych. Implementacja odpowiada za zachowanie tożsamości
obiektu, dlatego kod klienta może korzystać z typu danych
niezależnie od reprezentacji jego stanu, zachowując zgod
ność z interfejsem API opisującym działanie obiektu. Stan heads
obiektu m ożna wykorzystać do zwrócenia informacji klien
towi lub wywołania efektów ubocznych. Stan m ożna też
zmienić za pom ocą jednej z operacji typu danych. Jednak 460
szczegóły reprezentacji wartości typu danych nie mają w ko
dzie klienta znaczenia. Referencja to mechanizm służący do
dostępu do obiektu. W słownictwie związanym z Javą typy
proste (w których zmienne powiązane są z wartościami) są
wyraźnie odróżniane od tych opisywanych w tym miejscu, Dwa o b ie k ty ty p u C o u n te r
nazywanych typami referencyjnymi. Szczegóły implementa
cji referencji zależą od implementacji Javy. Można jednak
traktować referencję jak adres w pamięci, co pokazano po
heads
prawej (z uwagi na zwięzłość na rysunku użyto adresów
ta ils
trzycyfrowych).
Tożsamość
Tworzenie obiektów Każda wartość typu danych jest prze y * obiektu
460 X heads
chowywana w obiekcie. Aby utworzyć obiekt (inaczej: utwo
rzyć egzemplarz typu), należy wywołać konstruktor, używa
jąc słowa kluczowego new, po którym następuje nazwa klasy Tożsamość
obiektu
i () (lub lista wartości argumentów w nawiasach, jeśli kon 612 ta ils
struktor przyjmuje argumenty). Konstruktor nie ma typu
zwracanej wartości, ponieważ zawsze zwraca referencję do
obiektu określonego typu danych. Przy każdym wywołaniu
new() w kodzie klienta system:
Reprezentacja obiektu
■ Przydziela w pamięci obszar na obiekt.
" Wywołuje konstruktor, aby zainicjować wartość obiektu.
0 Zwraca referencję do obiektu.
W kodzie klienta obiekty zwykle tworzy się za pomocą deklaracji inicjującej, która łączy
zmienną z obiektem (często podobną technikę stosuje się dla zmiennych typów prostych).
W odróżnieniu od typów prostych zmienne są wiązane z referencjami do obiektów, a nie
80 RO ZD ZIA Ł 1 a Podstawy
z samymi wartościami typu da Deklaracja łącząca zmienną Wywołanie konstruktora
z referencją do obiektu w celu utworzenia obiektu
nych. Można utworzyć dowolną
liczbę obiektów tej samej klasy.
Każdy z nich ma odrębną toż C o u n te r heads new C o u n t e r ( " h e a d s " ) ;
samość i może (ale nie musi) Tworzenie obiektu
przechowywać tę samą war
tość, co inny obiekt tego typu.
Na przykład kod:
Counter heads = new C o u n t e r ( " o r ły " ) ;
Counter t a i l s = new C o u n te r("re s z k i") ;
tworzy dwa różne obiekty typu Counter. W abstrakcyjnym typie danych szczegó
ły reprezentacji wartości są ukryte przed kodem klienta. Można założyć, że wartość
powiązana z każdym obiektem Counter to nazwa typu S t r i ng i licznik typu i nt, nie
można jednak pisać kodu zależnego od konkretnej reprezentacji (a nawet stwierdzić,
czy założenie jest prawdziwe — możliwe, że licznik to wartość typu long).
W ywoływ anie m etod egzemplarza Metody egzemplarza służą do działania na war
tościach typu danych, dlatego język Java udostępnia specjalny mechanizm do wywo
ływania takich metod, w którym podkreślone jest powiązanie z obiektem. Metodę eg
zemplarza można wywołać, podając nazwę zmiennej powiązanej z obiektem, kropkę,
nazwę metody egzemplarza i 0 lub więcej argumentów umieszczonych w nawiasach
•Deklaracja
i rozdzielonych przecinkami. Metoda egzem
c o u n t e r h e a d s; ' plarza może zmieniać wartość typu danych lub
Za p o m o cą new (konstruktor) tylko ją sprawdzać. Metody egzemplarza mają
he ad s = new c o u n t e r ( " h e a d s " ) ; wszystkie cechy metod statycznych wymienio
t ne na stronie 36. Argumenty są przekazywane
Wywołanie konstruktora (tworzenie obiektu) przez wartość, nazwy metod można przeciążać,
Jako instrukcja (w artość zw racana v o id ) metody mogą mieć wartość zwracaną i powodo
|heads|. i n c r e m e n t Q : wać efekty uboczne. Mają też dodatkową, cha
T
Nazwa obiektu \ rakterystyczną dla nich cechę: każde wywołanie
Wywołanie metody egzemplarza jest powiązane z obiektem. Na przykład kod:
zmieniającej wartość obiektu
[Link] entO ;
Jako w yrażenie
[ h e a d s ] . t a l ly ( ) - t a i l s . t a l l y Q wywołuje metodę egzemplarza i ncrement () dzia
r łającą na obiekcie heads typu Counter (tu operacja
Nazwa obiektu \
Wywołanie metody egzemplarza, polega na zwiększeniu wartości licznika). Kod:
która daje dostęp do wartości obiektu
h e a d s .ta lly () - ta i 1 s .t a l l y ( ) ;
Poprzez au to m a ty cz n ą konw ersję ty p u ( to S tr in g O )
wywołuje metodę egzemplarza ta l 1 y () dwu
s t d O u t . p r i n t l n ( Iheadsl ) ;
krotnie — raz na obiekcie heads typu Counter
t
Wywołanie h e a d s .t o S t r in g Q i raz na obiekcie ta i 1 s tego samego typu (tu ope
racja powoduje zwrócenie wartości licznika jako
Wywoływanie metod egzemplarza
liczby typu i nt). Jak pokazano w przykładach,
1.2 ■ Abstrakcja danych 81
w kodzie klienta można używać wywołań metod egzemplarza w taki sam sposób, jak
wywołań metod statycznych — jako instrukcji (metody voi d) lub wartości w wyraże
niach (metody zwracające wartość). Metody statyczne to przede wszystkim implemen
tacje funkcji. Metody niestatyczne (egzemplarza) służą głównie do implementowania
operacji typów danych. W kodzie klienta mogą występować metody obu rodzajów,
przy czym łatwo je odróżnić
Metoda egzemplarza Metoda statyczna
od siebie, ponieważ wywołania
metod statycznych rozpoczy Przykładowe
[Link] ent() M a th .sq rt(2 .0 )
wywołanie
nają się od nazwy klasy (trady
cyjnie zaczynającej się wielką Wywoływana
Nazwa obiektu Nazwa klasy
za pomocą
literą), a wywołania metod
niestatycznych — od nazwy Referencja do obiektu
Parametry Argumenty
i argumenty
obiektu (tradycyjnie zaczyna
Główne Sprawdzanie lub Obliczanie
jącej się małą literą). Różnice
przeznaczenie zmienianie wartości obiektu zwracanej wartości
te podsumowano w tabeli po
Metody egzem plarza a metody statyczne
prawej stronie.
Korzystanie z obiektów Deklaracje tworzą nazwy zmiennych powiązanych z obiek
tami. Nazw można używać w kodzie nie tylko do tworzenia obiektów i wywoływania
metod egzemplarza, ale też w taki sam sposób, jak nazw zmiennych dla liczb całkowi
tych, liczb zmiennoprzecinkowych i innych typów prostych. Aby utworzyć kod klienta
z wykorzystaniem określonego typu danych, należy:
° Zadeklarować zmienne tego typu, służące do wskazywania obiektów.
° Użyć słowa kluczowego new, aby wywołać konstruktor tworzący obiekty tego typu.
° Użyć nazwy obiektu w celu wywołania m etod egzemplarza — albo jako instruk
cji, albo w wyrażeniach.
Na przykład klasa FI i ps przedstawiona na początku następnej strony to klient typu
danych Counter, pobierający argument T z wiersza poleceń i symulujący T rzutów
monetą (FI ips jest też klientem biblioteki StdRandom). Oprócz takich bezpośrednich
zastosowań zmienne powiązane z obiektami m ożna wykorzystać w taki sam sposób,
jak zmienne powiązane z wartościami typów prostych:
■ w instrukcjach przypisania;
° do przekazywania lub zwracania obiektów w metodach;
D do tworzenia i używania tablic obiektów.
Zrozumienie każdego rodzaju zastosowania wymaga myślenia w kategoriach refe
rencji, a nie wartości. Jest to wyraźnie widoczne w dalszym omówieniu wszystkich
rodzajów zastosowań.
Instrukcje przypisania Instrukcja przypisania dla typu referencyjnego tworzy ko
pię referencji. Taka instrukcja nie tworzy nowego obiektu, a jedynie nową referencję
do istniejącego. Jest to tak zwane utożsamianie nazw (ang. aliasing). W wyniku tego
procesu obie zmienne zaczynają wskazywać ten sam obiekt. Efekt utożsamiania nazw
jest tu nieco nieoczekiwany, ponieważ różni się od skutków specyficznych dla zm ien
nych z wartościami typów prostych. Należy koniecznie zrozumieć tę różnicę.
RO ZD ZIA Ł 1 0 Podstawy
p ub lic c l a s s F l i p s
{
p ub lic s t a t i c void main(String[] args)
I % ja va F I i p s 10
i n t T = In t e g e r . p a r s e ln t ( a r g s [ 0 ] ); 5 o r íy
Counter heads = new C o u n t e r ( " o r t y " ) ; 5 reszki
Counter t a i l s = new C o u n t e r ( " r e s z k i '') ; różn ica: 0
f o r ( i n t t = 0; t < T; t++)
i f ([Link](0.5)) % java FI i p s 10
[Link](); 8 orty
e lse t a i l s . i n c r e m e n t ( ) ; 2 re szki
S td O u t. p rin t ln (h e a d s); ró żn ica: 6
Std O u t.p rintln (tails);
i n t d = h e a d s .t a lly ( ) - tai 1s . t a l l y (); % java FI ip s 1000000
S t d O u t . p r i n t ln ( “ro znica: " + M a th .a b s (d )); 499710 o r ty
} 500290 resz ki
ró żnica: 580
Klient typu danych Counter symulujący T rzutów monetą
Jeśli x i y to zmienne typu prostego, przypisanie Counter c l;
x = y powoduje skopiowanie wartości y do x. Dla c l = new C o u n t e r ( " o n e s " ) ;
c [Link] c r e m e n t O ;
typów referencyjnych kopiowane są referencje, C o u n t e r c2 = c l ;
a nie wartości. Utożsamianie nazw to częste źród c2 . i n c r e m e n t O ;
ło błędów w programach Javy, co przedstawiono
w poniższym przykładzie:
Counter c l = new C ounter("jedynki") ;
>
Referencje do tego
c l . in c re m e n to ;
samego obiektu
Counter c2 = c l;
[Link]();
S td O u t .p rin t ln (c l) ;
Dla typowej implementacji metody toS tri ng () kod
wyświetli łańcuch znaków "2 jedynki". Może, ale
nie musi to być zgodne z oczekiwaniami, a począt
Referencja
kowo wydaje się niezgodne z intuicją. Takie błędy 811 do „ je d y n k i”
często występują w programach pisanych przez
osoby o niewielkim doświadczeniu w korzystaniu
z obiektów (może to dotyczyć także Ciebie, dlatego
zwróć na to uwagę!). Zmiana stanu obiektu wpływa
na cały kod, w którym używane są zmienne o róż
nych nazwach powiązane z danym obiektem. Dwie Utożsamianie nazw
różne zmienne typu prostego zwykle traktuje się
jako niezależne, jednak intuicja ta nie jest prawdzi
wa dla zmiennych typów referencyjnych.
1.2 o Abstrakcja danych 83
O biekty ja k o argum enty Obiekty można przekazywać jako argumenty do metod.
Zwykle pozwala to uprościć kod klienta. Na przykład użycie obiektu typu Counter
jako argumentu powoduje przekazanie nazwy i licznika za pomocą jednej zmien
nej. W Javie wywołanie m etody z argumentami m a taki efekt, jakby każdą wartość
argumentu umieszczono po prawej stronie instrukcji przypisania z odpowiednią na
zwą argumentu po lewej. Oznacza to, że Java przekazuje kopię wartości argumentu
z programu wywołującego do metody. Jest to tak zwane przekazywanie przez wartość
(zobacz stronę 36). Ważną konsekwencją tego podejścia jest to, że m etoda nie może
zmienić wartości zmiennej z programu wywołującego. W przypadku typów prostych
zasada ta działa w oczekiwany sposób (dwie zmienne są niezależne), jednak przy każ
dym użyciu typu referencyjnego jako argumentu m etody powstaje nazwa zastępcza,
dlatego trzeba zachować ostrożność. Ujmijmy to inaczej — zwyczajowo referencje są
przekazywane przez wartość (powstaje ich kopia), natomiast obiekty — przez refe
rencję. Przykładowo, jeśli do m etody przekazano referencję do obiektu typu Counter,
metoda nie może zmienić pierwotnej referencji (sprawić, aby wskazywała na inny
obiekt typu Counter), natomiast może zmienić wartość obiektu, na przykład używając
referencji w celu wywołania metody i ncrement ().
Obiekty ja ko zwracane wartości Oczywiście, można też używać obiektów jako war
tości zwracanych przez metodę. Metoda może zwrócić obiekt przekazany do niej jako
argument, tak jak w dalszym przykładzie, lub utworzyć obiekt i zwrócić referencję do
niego. Jest to ważna możli
wość, ponieważ Java dopusz p ub lic c l a s s FlipsMax
cza tylko jedną zwracaną war {
p ub lic s t a t i c Counter max(Counter x, Counter y)
tość. Zastosowanie obiektów
{
pozwala napisać kod, który i f ( x . t a l l y ( ) > y . t a l l y O ) return x;
zwraca kilka wartości. e lse return y;
1
p ublic s t a t i c void m a in ( S tr in g [] args )
{
in t T = I n t e g e r . p a r s e l n t ( a r g s [0 ]);
Counter heads = new C o u n t e r ( " o r t y " ) ;
Counter t a i l s = new C o u n t e r ( " r e s z k i " ) ;
f o r (i n t t = 0; t < T; t++)
i f (S tdRan dom .bernoulli(0.5 ))
head [Link]();
e lse t a i I s . i n c r e m e n t ();
i f (hea [Link] 1y () == t a i l s . t a l l y O )
S t d O u t . p r i n t l n C 'R e m i s " ) ;
e lse St dOut. println(max(hea ds, t a i l s ) + " wygrywają");
% java FlipsMax 1000000
500281 resz ki wygrywają
Przykładowa metoda statyczna z argumentami i zwracanymi wartościami
w postaci obiektów
RO ZD ZIA Ł 1 B Podstawy
Tablice to obiekty W Javie każda wartość typu innego niż typ prosty jest obiek
tem. Obiektami są na przykład tablice. Podobnie jak dla łańcuchów znaków, język
zapewnia specjalną obsługę pewnych operacji na tablicach: deklarowania, inicjowa
nia i indeksowania. Podobnie jak w przypadku innych obiektów, przekazanie tablicy
do m etody lub użycie reprezentującej tablicę zmiennej po prawej stronie instrukcji
przypisania powoduje utworzenie kopii referencji do tablicy, a nie kopii samej tabli
cy. To podejście dobrze nadaje się dla typowego przypadku, kiedy programista ocze
kuje, że m etoda będzie mogła zmodyfikować tablicę, zmieniając uporządkowanie jej
elementów (za pom ocą m etody jav a, ú til .A rra y [Link] rt() lub opisanej na stronie 44
metody shuffle()).
Tablice obiektów Elementy tablicy mogą być dowolnego typu, jak już to przedsta
wiono. Parametr args [] w napisanej przez nas implementacji m etody mai n () to tab
lica obiektów typu S tri ng. Tablicę obiektów można utworzyć w dwóch krokach:
■ utworzenie tablicy za pomocą składni z nawiasami kwadratowymi używanej
dla konstruktorów tablic;
■ utworzenie każdego obiektu w tablicy przy użyciu standardowego konstruktora
dla poszczególnych obiektów.
Poniższy kod to symulacja rzutu kostką. Wykorzystano tu tablicę obiektów typu
Counter do rejestrowania liczby wystąpień każdej możliwej wartości. Tablica obiek
tów w Javie to tablica referencji do nich — nie zawiera samych obiektów. Jeśli obiek
ty są duże, pozwala to zwiększyć wydajność, ponieważ nie trzeba przenosić samych
obiektów (wystarczy przenieść referencje). Przy małych obiektach może nastąpić
spadek wydajności z uwagi na konieczność podążania za referencją za każdym ra
zem, kiedy potrzebne są informacje.
pub lic c la ss R o lls
(
p u b lic s t a t i c void m a in (S tr in g [] args)
{
in t T = In t e g e r.p a rs e ln t (a rg s[0 ]);
i n t S I D E S = 6;
C ounte rJ] r o l l s = new C o u n t e r [ S I D E S + l ] ;
f o r ( i n t i = 1; i <= S I D E S ; 1++)
ro llsji] = new C o u n t e r ( "w y s t ą p ie ń " + i ) ;
f o r ( i n t t = 0; t < T; t++ )
{
i n t r e s u l t = S t d R a n d o m .u n if o r m (l , S I D E S + 1 ) ; % j a v a R o l l s 1000000
r o lls jr e s u lt ] .increm ento; 167308 w y st ą p ie ń 1
1 166540 w y s t ą p ie ń 2
f o r ( i n t i = 1; i <= S I D E S ; i + + ) 166087 w y st ą p ie ń 3
St dOut . pri n t 1n (rol 1s [ i ] ); 167051 w y st ą p ie ń 4
1 166422 w y st ą p ie ń 5
1 166592 w y st ą p ie ń 6
Klient typu Counter symulujący T rzutów kostką
1.2 ■ Abstrakcja danych
z u w a g i n a k o n c e n t r a c j ę n a o b i e k t a c h pisanie kodu z wykorzystaniem abstrakcji
danych (definiowanie i używanie typów danych, z wartościami typu danych przecho
wywanymi w obiektach) powszechnie nazywane jest programowaniem obiektowym.
Opisane wcześniej podstawowe zagadnienia to punkt wyjścia do programowania
obiektowego, dlatego warto je pokrótce podsumować. Typ danych to zbiór warto
ści i operacji zdefiniowanych na tych wartościach. Typy danych implementowane są
w niezależnych modułach Javy (oznaczanych słowem class), po czym można pisać
programy klienckie używające tych typów. Obiekt to jednostka, która może przyj
mować wartości typu danych (jest egzemplarzem typu danych). Obiekty mają trzy
podstawowe cechy: stan, tożsamość i działanie. Implementacja typu danych zapewnia
obsługę klientów typu w następujący sposób:
° Kod klienta może tworzyć obiekty (określać ich tożsamość) za pom ocą słowa
new. Słowo to powoduje wywołanie konstruktora, który tworzy obiekt, inicjuje
jego zmienne egzemplarza i zwraca referencję do obiektu.
° Kod klienta może manipulować wartościami typu danych (kontrolować działa
nie obiektu, na przykład zmieniając jego stan) przez użycie powiązanej z obiek
tem zmiennej do wywołania metody egzemplarza, która działa na zmiennych
egzemplarza obiektu.
■ Kod klienta może manipulować obiektami, tworząc tablice obiektów i przekazu
jąc je oraz zwracając do m etod w prawie taki sam sposób, jak wartości prostych
typów danych. Różnicą jest to, że zmienne to referencje do wartości, a nie same
wartości.
Te możliwości to podstawa elastycznego, współczesnego i bardzo przydatnego stylu
programowania używanego przy omawianiu algorytmów w tej książce.
RO ZD ZIA Ł 1 a Podstawy
Przykładowe abstrakcyjne typy danych W Javie istnieją tysiące wbudowa
nych typów ADT. Ponadto zdefiniowaliśmy wiele innych typów ADT ułatwiających
naukę algorytmów. Każdy napisany przez nas program Javy jest implementacją typu
danych (lub biblioteką m etod statycznych). Aby nie komplikować pracy, przedstawia
my interfejsy API wszystkich typów ADT używanych w książce (nie jest ich wiele).
W tym punkcie przedstawiono kilka przykładowych typów danych wraz z przy
kładami kodu klienta. W niektórych sytuacjach zaprezentowano fragmenty interfej
sów API zawierających dziesiątki m etod egzemplarza. Omawiamy takie interfejsy
API, aby zaprezentować praktyczne przykłady, wskazać metody egzemplarza uży
wane w książce i podkreślić, że nie trzeba znać szczegółów implementacji typu ADT,
aby móc go używać.
Na następnej stronie jako podręczne źródło wiedzy zaprezentowano typy danych
używane i rozwijane w książce. Typy te należą do kilku kategorii:
■ Standardowe systemowe typy ADT z bibliotek java. lang*. Można ich używać
w każdym programie Javy.
■ Typy ADT Javy z bibliotek w rodzaju java, awt, java, net i java. io. Także te typy
można stosować w każdym programie Javy, ale trzeba użyć instrukcji import.
■ Opracowane przez nas typy ADT dla wejścia-wyjścia, umożliwiające korzysta
nie z wielu strum ieni wejścia-wyjścia podobnych do Stdln i StdOut.
■ Oparte na danych typy ADT, służące głównie do ułatwiania porządkowania
i przetwarzania danych przez ukrycie ich reprezentacji. W dalszej części punktu
opisano kilka przykładów zastosowań tych typów w obszarze geometrii obli
czeniowej i przetwarzania informacji. Ponadto typy te wykorzystano w kodzie
klientów.
■ Typy ADT dla kolekcji. Mają one przede wszystkim ułatwiać manipulowanie
kolekcjami danych tego samego typu. W p o d r o z d z i a l e 1.3 opisano podsta
wowe typy Bag, Stack i Queue, w r o z d z i a l e 2 . — typy PQ, a w r o z d z i a ł a c h 3 .
i 5 . — typy ST i SET.
■ Typy ADT oparte na operacjach, używane do analizowania algorytmów, co opi
sano w p o d r o z d z i a ł a c h 1.4 i 1 .5 .
D Typy ADT dla algorytmów działających na grafach, w tym typy ADT oparte na
danych, głównie ukrywające różnego rodzaju reprezentacje grafów, i typy ADT
oparte na operacjach, przede wszystkim zapewniające specyfikacje algorytmów
przetwarzania grafów.
Lista ta nie obejmuje dziesiątków typów uwzględnianych w ćwiczeniach (typy te
mogą występować w indeksie). Ponadto, co opisano na stronie 102, często wyróżnia
my różne implementacje typów ADT opisowymi przedrostkami. Jako grupa używane
typy ADT pokazują, że uporządkowanie i zrozumienie stosowanych typów danych
jest ważnym czynnikiem we współczesnym programowaniu.
W typowej aplikacji używanych jest czasem tylko od pięciu do dziesięciu typów
ADT. Głównym celem przy rozwijaniu i porządkowaniu typów ADT w tej książce
jest umożliwienie programistom łatwego korzystania ze stosunkowo małego zbioru
typów w pracy nad kodem klienta.
1.2 o Abstrakcja danych 87
Standardowe systemowe typy Javy z [Link] Typy dla kolekcji
In t e g e r Nakładka na typ i nt S tac k Stos
Double Nakładka na typ doubl e Queue Kolejka FIFO
Indeksowane wartości
S trin g Bag Wielozbiór
typu ch a r
Typ do budowania
S trin gB u ilde r MinPQ MaxPQ Kolejki priorytetowe
łańcuchów znaków
IndexMinPQ Indeksowana kolejka
Inne typy Javy
IndexMaxPQ priorytetowa
j a v a . a w t. C o l o r Kolory ST Tablica symboli
[Link] [Link] Czcionki SET Zbiór
Tablica symboli z kluczami
j a v a . n e t . URL Adresy URL StringST
w postaci łańcuchów znaków
j a v a , i o. F i l e Pliki Oparte na danych typy dla grafów
Opracowane przez nas standardowe
Graph Graf
typy do obsługi wejścia-wyjścia
In Strumień wejścia Dig r a p h Graf skierowany
Out Strumień wyjścia Edge Krawędź (ważona)
Draw Rysowanie EdgeWeightedGraph G raf (ważony)
Oparte na danych typy Krawędź
Di recte dEd ge
dla przykładowych klientów (skierowana i ważona)
Po int2D P unkt w przestrzeni EdgeWi eght edDi gr aph Graf (skierowany i ważony)
Przedział
I n t e r v a l ID Oparte na operacjach typy dla grafów
jednowymiarowy
Przedział D ynamiczne sprawdzanie
I n t e r v a l 2D UF
dwuwymiarowy połączeń
Wyszukiwanie ścieżki
Date Data DepthFi r s t P a t h s
metodą DFS
Transaction Transakcja CC Połączone komponenty
Wyszukiwanie ścieżki
Typy do analizowania algorytmów Bread thF irstP aths
metodą BFS
Wyszukiwanie ścieżki w grafie
Counter Licznik Di rec tedDF S
skierowanym metodą DFS
Wyszukiwanie ścieżki w grafie
A cc um ula tor Akumulator D ire c t e d B F S
skierowanym metodą BFS
.. , . Wersja wizualna
V is u a l A c c u m u l a t o r T r a n s i t i veCl o s u r e Wszystkie ścieżki
akumulatora
Stopwatch Stoper Topological Porządek topologiczny
DepthFi r s t O r d e r Porządek DFS
D irectedC ycle Wyszukiwanie cykli
SCC Silnie spójne składowe
Minimalne drzewo
MST
rozpinające
SP Najkrótsze ścieżki
Wybrane typy ADT używane w książce
88 RO ZD ZIA Ł 1 B Podstawy
O biekty geom etryczne Naturalnym przykładem programowania obiektowego jest
projektowanie typów danych dla obiektów geometrycznych. Interfejsy API podane
na następnej stronie odpowiadają typom danych dla trzech często używanych obiek
tów geometrycznych: Point2D (punktów
p u b li c s t a t i c v o i d m a i n ( S t r i n g [ ] a r g s ) w przestrzeni), IntervallD (przedziałów
{ na linii) i Interval 2D (dwuwymiarowych
dou ble x l o = D o u b l e . p a r s e D o u b l e ( a r g s [ 0 ] ) ;
dou b le xhi = D o u b l e . p a r s e D o u b l e ( a r g s [ l ] ); przedziałów w przestrzeni lub prostokątów
double y l o = D o u b l e . p a r s e D o u b l e ( a r g s [ 2 ] ) ; przylegających do osi). Interfejsy API, jak
double y h i = D o u b l e . p a r s e D o u b l e ( a r g s [ 3 ] ) ; zwykle, w zasadzie nie wymagają opisu i po
int T = Integer.p a rse ln t (a rg s[4 ]);
zwalają tworzyć łatwy do zrozumienia kod
I n t e r v a l l D x = new I n t e r v a l l D ( x l o , x h i ) ; klienta, taki jak w przykładzie pokazanym
I n t e r v a l ID y = new I n t e r v a l l D ( y l o , y h i ) ; po lewej stronie. Program wczytuje z wier
I n t e r v a l 2 D box = new I n t e r v a l 2 D ( x , y);
sza poleceń granice obiektu Interval 2D
[Link] ();
i liczbę całkowitą T, generuje T losowych
Count er c = new C o u n t e r ( " t r a f i e n i a " ) ; punktów w jednostce kwadratowej i zlicza,
f o r ( i n t t := 0; t < T; t++ )
ile punktów znajduje się w przedziale (po
{
double x = [Link];
zwala to oszacować obszar prostokąta). Aby
double y = [Link]; efekt był ciekawszy, klient ponadto rysuje
P o i n t p : new P o in t(x, y ) ; przedział i punkty znajdujące się poza nim.
i f (b [Link] ntains(p)) c. i ncrem entO;
else [Link]();
Przykład ten to model metody, która redu
kuje problem obliczania powierzchni i ob
jętości do ustalania, czy punkt znajduje się
StdO ut.p rintJn(c);
w danym kształcie, czy nie (jest to prostsze,
S td O u t.p rin tln (b o x .are a());
ale niebanalne zadanie). Oczywiście, m oż
na zdefiniować interfejsy API dla innych
Klient testowy typu lnterval2D obiektów geometrycznych, takich jak frag
menty linii, trójkąty, wielokąty, okręgi i tak dalej, choć
zaimplementowanie operacji dla nich może okazać się
trudne. Kilka przykładów poruszono w ćwiczeniach
w końcowej części podrozdziału.
PROGRAMY PRZETWARZAJĄCE OBIEKTY GEOMETRYCZNE
mają wiele zastosowań w przetwarzaniu z wykorzy
staniem modeli ze świata naturalnego, obliczeniach
naukowych, grach komputerowych, filmach i wielu
innych obszarach. Rozwijanie i badanie takich progra
mów oraz aplikacji doprowadziło do rozkwitu ważnej
dziedziny badań nazywanej geometrią obliczeniową. Jest
to bogate źródło przykładów zastosowań algorytmów
% java Interval 2D .2 .5 .5 .6 10000
297 trafienia
omawianych w tej książce. W kontekście tego podroz-
.03
1.2 0 Abstrakcja danych
p u b lic c la s s Point2D
Point2D(double x, double y) Tworzenie punktu
double x() Współrzędna x
double y () Współrzędna y
double r( ) Promień (współrzędne biegunowe)
double theta() Kąt (współrzędne biegunowe)
double distTo (Poin t2D that) Odległość euklidesowo od danego punktu do punktu that
void draw() Rysowanie punktu na StdDraw
Interfejs API dla punktów w przestrzeni
public c l a s s I n t e r v a llD
IntervallD(double lo, double hi) Tworzenie przedziału
double length() Długość przedziału
boolean conta ins(d ouble x) Czy przedział obejmuje x?
boolean i n t e r s e c t s ( I n t e r v a l I D that) Czy przedział ma część wspólną z t ha t?
void draw() Rysowanie przedziału na StdDraw
Interfejs API dla przedziałów na linii
public c l a s s Interval2 D
I n t e r v a l2 D ( I n t e r v a lID x, In t e r v a llD y) Tworzenie przedziału dwuwymiarowego
double area() Powierzchnia przedziału dwuwymiarowego
boolean c o n t a i n s (Point2D x) Czy przedział dwuwymiarowy zawiera p?
boolean i n t e r s e c t s ( I n t e r v a l 2 D that) Czy przedział dwuwymiarowy
ma część wspólną z t h a t?
void draw() Rysowanie przedziału dwuwymiarowego
na StdDraw
Interfejs API dla dwuwymiarowych przedziałów w przestrzeni
działu chcemy pokazać, że abstrakcyjne typy danych bezpośrednio reprezentujące
abstrakcje geometryczne nie są trudne do zdefiniowania oraz mogą prowadzić do
prostego i przejrzystego kodu klienta. Dowodem na to stwierdzenie jest kilka ćwi
czeń z końcowej części podrozdziału i z witryny.
RO ZD ZIA Ł 1 a Podstawy
Przetw arzanie informacji Niezależnie od tego, czy chodzi o bank przetwarzają
cy miliony transakcji kartami kredytowymi, czy o firmę z obszaru analizy danych
internetowych przetwarzającą miliardy kliknięć touchpada, czy o grupę badawczą
przetwarzającą miliony obserwacji z eksperymentów, wiele zastosowań dotyczy
przetwarzania i porządkowania informacji. Abstrakcyjne typy danych są naturalnym
narzędziem do porządkowania informacji. Nie wdawajmy się w szczegóły — dwa in
terfejsy API widoczne na następnej stronie reprezentują typowe podejście stosowane
w praktyce. Celem jest zdefiniowanie typów danych umożliwiających przechowy
wanie w obiektach informacji odpowiadających zjawiskom z rzeczywistego świata.
Data to dzień, miesiąc i rok, a transakcja to klient, data i wartość. To tylko dwa przy
kłady — można też definiować typy danych przechowujące szczegółowe inform a
cje o klientach, godzinach, miejscach, produktach, usługach itd. Każdy typ danych
udostępnia konstruktory tworzące obiekty, które obejmują dane i m etody używane
w kodzie klienta do dostępu do tych danych. Aby uprościć kod klienta, dla każdego
typu udostępniono dwa konstruktory. Jeden przedstawia dane w odpowiednim ty
pie, a drugi przetwarza łańcuch znaków w celu pobrania danych (szczegółowy opis
zawiera ć w i c z e n i e 1 .2 . 1 9 ). Jak zwykle w kodzie klienta nie trzeba znać reprezentacji
danych. Dane zazwyczaj porządkowane są w ten sposób, aby można było traktować
dane powiązane z obiektem jak jeden element. Można tworzyć tablice wartości typu
Transaction, używać wartości typu Date jako argumentu lub wartości zwracanej
przez metodę itd. Typy danych tego rodzaju mają ukrywać dane i umożliwiać rozwi
janie kodu klienta, który nie zależy od reprezentacji danych. Nie rozwodzimy się tu
nad uporządkowaniem informacji. Warto jednak zauważyć, że zastosowane podejście
i odziedziczone metody to S tri ng(), compareTo(), equal s() i hashCode() umożliwiają
wykorzystanie implementacji algorytmów, które potrafią działać dla dowolnego typu
danych. Odziedziczone metody omówiono dokładniej na stronie 112. Zwróciliśmy
już uwagę na stosowaną w Javie konwencję, która umożliwia klientom wyświetlanie
reprezentacji w postaci łańcucha znaków dla każdej wartości, jeśli w implementacji
typu danych znajduje się metoda to S tri ng(). W p o d r o z d z i a ł a c h 1 .3 , 2 . 5 , 3.4 i 3.5
opisano konwencje związane z innymi odziedziczonymi metodami, używając jako
przykładów typów Date i Transaction, p o d r o z d z i a ł 1.3 zawiera klasyczne przy
kłady typów danych i mechanizmu Javy nazywanego typami sparametryzowanymi
(inaczej ogólnymi lub generycznymi), w którym wykorzystano owe konwencje. Także
w r o z d z i a ł a c h 2 . i 3 . omówiono korzystanie z typów generycznych i odziedziczo
nych m etod do rozwijania implementacji algorytmów sortowania oraz wyszukiwania
działających dla dowolnych typów danych.
które są logicznie powiązane, warto zastanowić
je ś l i is t n ie ją d a n e r ó ż n y c h t y p ó w ,
się nad zdefiniowaniem typu ADT, tak jak zrobiono to w przykładach. Rozwiązanie
to pomaga uporządkować dane i znacznie uprościć kod klienta w typowych zastoso
waniach, a także jest ważnym krokiem na drodze do abstrakcji danych.
1.2 h Abstrakcja danych
p u b lic c la s s Date implements Comparable<Date>
D a t e ( i n t month, i n t day, i n t y e a r ) Tworzy datę
D a t e ( S t r i n g date) Tworzy datę (konstruktor z przetwarzaniem)
i n t month() Miesiąc
i n t day( ) Dzień
in t year() Rok
S trin g t o S t r in g O Reprezentacja w postaci łańcucha znaków
boolean e q u a l s ( O b j e c t t h a t ) Czy data jest taka sama ja k w t h a t ?
i n t compareTo(Date t h a t ) Porównywanie daty z t h a t
i n t hashCode() Kod skrótu
p u b l i c c l a s s T r a n s a c t i o n implements C o mp a r a b le < T r a n s a ct io n >
T r a n s a c t i o n ( S t r i n g who, Date when, double amount)
T ran sa c tio n (Strin g transaction) Tworzy transakcjç
(konstruktor z przetwarzaniem)
S t r i n g who() Nazwisko klienta
Date when() Data
dou ble amount() Wartość
Strin g t o S tr in g O Reprezentacja w postaci łańcucha znaków
boolean e q u a l s ( O b j e c t t h a t ) Czy transakcja jest taka sama ja k w t h a t ?
i n t co m p a re T o ( T ra n s a c tio n t h a t ) Porównywanie transakcji z t h a t
i n t hashCode( ) Kod skrótu
Przykładowe interfejsy API dla aplikacji komercyjnych (daty i transakcje)
RO ZD ZIA Ł 1 □ Podstawy
Łańcuchy znaków S tring to w Javie ważny i przydatny typ ADT. Typ String to
indeksowany ciąg wartości typu char, mający dziesiątki m etod egzemplarza, w tym
poniższe:
p u b lic c la s s S trin g
S t r i n g () Tworzenie pustego łańcucha znaków
i n t le n g t h () Długość łańcucha znaków
i n t c h a r A t (in t i ) i -ty znak
in t i ndexOf(S t r i n g p) Pierwsze wystąpienie p (-1, jeśli nie ma p)
i n t indexO f( S t r i n g p, i n t i ) Pierwsze wystąpienie p po i -tym znaku (-1, jeśli nie ma p)
S t r i n g co n c a t( S t rin g t) Łańcuch z dołączonym t
S t r i n g s u b s t r i n g ( i n t i , i n t j) Podłańcuch danego łańcucha (znaki od i-tego do j - 1 )
S t r i ng [] s p l i t ( S t r i n g del im) Łańcuchy znaków między wystąpieniami delim
in t compareTo(String t) Porównywanie łańcuchów znaków
boolean e q u a l s ( S t r i n g t) Czy wartość danego łańcucha jest taka sama ja k t.?
i n t hashCode() Kod skrótu
Interfejs API typu String J avy (wybrane metody)
Wartości typu S tri ng przypominają tablice znaków, jednak nie są nimi. Tablice mają
wbudowaną składnię Javy umożliwiającą dostęp do znaku. Typ S tri ng posiada metody
egzemplarza zapewniające dostęp indeksowany, określające długość itd. Dla tego typu
język udostępnia specjalną obsługę inicjowania i złączania. Zamiast tworzyć i inicjo
wać łańcuch znaków za pomocą konstruktora, można użyć literału. Zamiast wywoły
wać metodę concat (), wystarczy użyć operatora +. Omawianie szczegółów implemen
tacji nie jest tu ważne, jednak — co okaże się w r o z d z i a l e 5 . — przy rozwijaniu algo
rytmów przetwarzających łańcuchy warto zrozumieć aspekty związane z wydajnością
niektórych metod. Dlaczego nie używamy prostych tablic znaków zamiast wartości
typu String? Odpowiedź jest taka sama jak dla innych typów ADT — aby uprościć
kod klienta i zwiększyć jego przejrzystość. Za po
S t r i n g a = " i mamy "
mocą typu S tri ng można pisać przejrzysty i prosty S t r i n g b = " j u ż cz as 5
kod klienta z wykorzystaniem wielu wygodnych S t r i n g c = "n a "
metod egzemplarza bez uwzględniania sposobu
Wywołanie Wartość
reprezentowania łańcuchów znaków (zobacz na
[Link] th() 7
stępną stronę). Nawet ta krótka lista obejmuje roz
[Link](4) m
budowane operacje, wymagające zaawansowanych
[Link](c) " i mamy na"
algorytmów, takich jak opisane w r o z d z i a l e 5 . Na
a. in d e x O f( "m a m y") 2
przykład argumentem metody spl i t () może być
a . s u b s t r i n g ( 2 , 5) "mam"
wyrażenie regularne (zobacz p o d r o z d z i a ł 5 .4 ).
a . s p l i t ( " " ) [ 0]
II.j I
W przykładzie zastosowania metody s p l i t( ) na
stronie 93 użyto argumentu "\\s+ ", co oznacza a . s p l i t ( " " ) [ 1 ] "mamy"
„jedno lub więcej wystąpień tabulacji, odstępów, b. e q u a l s ( c ) fa l se
nowych wierszy lub powrotów karetki”. Przykładowe operacje
na łańcuchach znaków
1.2 o Abstrakcja danych
Zadanie Implementacja
p u b l i c s t a t i c boolean i s P a l i n d r o m e ( S t r i n g s)
{
int N = s .le n g t h ( ) ;
Czy łańcuch znaków f o r ( i n t i = 0; i < N/2; i+ + )
jest palindromem? i f ( s . c h a r A t ( i ) != s . c h a r A t ( N - l - i ) )
return f a l s e ;
return true;
Strin g s = args[0];
Pobieranie nazwy pliku
in t dot = s .in d e x O f ( " . " ) ;
i rozszerzenia z argumentu
S t r i n g ba se = s . s u b s t r i n g ( 0 , dot);
z wiersza poleceń S t r i n g exten sio n = s . s u b s t r i n g ( d o t + 1, s . l e n g t h ( ) ) ;
Wyświetlanie wszystkich S t r i n g q ue ry = a r g s [ 0 ] ;
w hile ( IS t d l n . is E m p t y O )
wierszy ze standardowego
wejścia, zawierających
(
Strin g s = Std ln .re ad Lin e();
łańcuch znaków podany i f (s .c o n t a in s (q u e ry ) ) S t d O u t . p r i n t l n ( s ) ;
w wierszu poleceń )
Tworzenie tablicy
łańcuchów znaków S t r i n g in p u t = S t d l n . r e a d A l 1 ( ) ;
ze S t d l n ograniczonych Strin g[] words = i n p u t . s p l i t ( " \ \ s + " ) ;
białymi znakam i
p u b l i c bo ole an i s S o r t e d ( S t r i n g [ ] a)
(
f o r ( i n t i = 1; i < a . l e n g t h ; i++)
Sprawdzanie,
czy łańcuchy znaków {
if ( a [ f -1] . c o m p a r e T o ( a [ i ] ) > 0)
w tablicy są uporządkowane return f a ls e ;
alfabetycznie }
return true;
Typowy kod do przetwarzania łańcuchów znaków
94 RO ZD ZIA Ł 1 a Podstawy
Ponownie o wejściu i wyjściu Wadą bibliotek standardowych [Link] i StdDraw
opisanych w p o d r o z d z i a l e i . i jest to, że w danym programie umożliwiają pracę
z jednym tylko plikiem wejściowym, jednym plikiem wyjściowym i jednym rysun
kiem. Programowanie obiektowe umożliwia zdefiniowanie podobnych mechanizmów
pozwalających korzystać z wielu strum ieni wejścia, strum ieni wyjścia i rysunków
w jednym programie. Opracowana przez nas biblioteka standardowa obejmuje typy
danych In, Out i Draw o interfejsach API pokazanych na następnej stronie. Wywołanie
dla typu In lub Out konstruktora z argumentem w postaci łańcucha znaków powo
duje najpierw próbę znalezienia w bieżącym katalogu pliku o podanej nazwie. Jeśli
plik nie istnieje, należy założyć, że argument to nazwa witryny, i spróbować nawiązać
z nią połączenie (jeżeli taka witryna nie istnieje, zgłaszany jest wyjątek czasu wykona
nia). W obu sytua-
p u b l i c c l a s s Cat cjach plik lub witry
( na staje się źródłem
p u b l i c s t a t i c v oid m a i n ( S t r i n g [ ] ar g s )
{ // Kopiowanie pl ik ó w wejściowych do o bie k tu out ( o s t a t n i argument). wejścia lub docelo
Out out = new O u t ( a r g s [ a r g s . l e n g t h - l ] ); wą lokalizacją wyj
f o r ( i n t i = 0; i < a r g s . l e n g th - 1; 1++)
ścia dla tworzonego
{ // Kopiowanie p l i k u wejściowego o nazwie podanej j a k o i - t y
// argument do o bie k tu out.
obiektu strumienia.
In in = new I n ( a r g s [ i ] ) ; Metody read*()
S trin g s = i n . r e a d A ll(); i pri nt*() będą do
o u t.p rin tln (s);
in .c lo se ();
tyczyć danego pliku
} lub określonej witry
o u [Link] O ; ny. Przy korzystaniu
z konstruktora bez
argumentów otrzy
Przykładowy klient typów In i Out mywane są standar
dowe strumienie. Rozwiązanie to umożliwia jednem u
programowi przetwarzanie wielu plików i rysunków.
% more i n l . t x t
Można też przypisać takie obiekty do zmiennych, prze
This i s kazać je jako argumenty lub wartości zwracane m e
tod, tworzyć z nich tablice i manipulować nimi w taki
% more i n 2 . t x t
sposób, jak obiektami dowolnego typu. Przedstawiony
a tiny
test. po lewej stronie program Cat to przykładowy klient
typów In i Out korzystający z wielu strumieni wejścia
% j a v a Cat i n l . t x t i n 2 . t x t o u t . t x t
w celu połączenia kilku plików wejściowych w jeden
% more o u t . t x t wyjściowy. Klasy In i Out obejmują ponadto metody
This i s statyczne do odczytu plików zawierających same war
a tiny tości typu i nt, doubl e lub S tri ng i zapisu ich w tablicy
test.
(zobacz stronę 138 i ć w i c z e n i e 1 .2 . 1 5 ).
1.2 ■ Abstrakcja danych
p u b lic c la s s In
In () Tworzenie strumienia wejścia na podstawie standardowego wejścia
I n (S tring name) Tworzenie strumienia wejścia na podstawie pliku lub witryny
boolean isEmpty () tru e, jeśli nie ma ju ż danych wejściowych, i fal se w przeciwnym
razie
i n t re adlnt() Wczytywanie wartości typu int
double readDouble() Wczytywanie wartości typu double
void c lo se () Zamykanie strumienia wejścia
Uwaga: wszystkie operacje obsługiwane przez Stdln są też obsługiwane dla obiektów typu In
Interfejs API opracowanego przez nas typu danych dla strumieni wejścia
public c l a s s Out
Tworzenie strumienia wyjścia prowadzącego
Out()
do wyjścia standardowego
Out ( S t r i n g name)1 Tworzenie strumienia wyjścia prowadzącego do pliku
void p r i n t ( S t r i n g s) Dołączanie s do strumienia wyjścia
void p r i n t l n ( S t r i n g <;) Dołączanie s i nowego wiersza do strumienia wyjścia
void p r i n t l n ( ) Dołączanie nowego wiersza do strumienia wyjścia
void p r i n t f ( S t r i n g f,, . . . ) Formatowane wyświetlanie w strumieniu wyjścia
void clo se () Zamykanie strumienia wyjścia
Uwaga: wszystkie operacje obsługiwane przez StdO ut są też obsługiwane dla obiektów typu Out
Interfejs API opracowanego przez nas typu danych dla strumieni wyjścia
public c l a s s Draw
Draw()
void lin e (d o u b le xO, double yO, double x l , double y l )
void point(double x, double y)
Uwaga: wszystkie operacje obsługiwane przez StdDraw są też obsługiwane dla obiektów typu Draw
Interfejs API opracowanego przez nas typu danych dla rysunków
RO ZD ZIA Ł 1 n Podstawy
Implementowanie abstrakcyjnych typów danych Typy ADT, podobnie
jak biblioteki m etod statycznych, implementuje się za pom ocą klas Javy (słowo klu
czowe class). Należy umieścić kod w pliku o nazwie takiej samej jak nazwa klasy
i rozszerzeniu .java. Pierwsze instrukcje w pliku to deklaracje zmiennych egzempla
rza, które określają wartości typu danych. Po zmiennych egzemplarza znajduje się
konstruktor i metody egzemplarza, będące implementacją operacji na wartościach
typu danych. Metody egzemplarza mogą być publiczne (określone w interfejsie API)
lub prywatne (służą do porządkowania obliczeń i są niedostępne dla klientów).
Definicja typu danych może obejmować wiele konstruktorów, a także definicje metod
statycznych. Metoda mai n () klienta do testów jednostkowych zwykle służy do testo
wania i diagnozowania. Jako pierwszy przykład rozważmy implementację typu ADT
Counter zdefiniowanego na stronie 77. Na następnej stronie znajduje się kompletna
implementacja z komentarzami. Jest to źródło informacji przydatne przy omawianiu
fragmentów kodu. Implementacja każdego typu ADT m a te same podstawowe ele
menty, co w tym prostym przykładzie.
Z m ienne egzem plarza Aby zdefi- p ub lic c l a s s Counter
niować wartości typu danych (stan Deklaracje final s t r i n g name.
każdego obiektu), należy zadeklaro- im ienny ci private -¡nt count;
, . . ,, egzemplarza
wac zmienne egzemplarza w sposob j-
podobny, jak wcześniej deklarowano
zmienne lokalne. Istnieje kluczowa Zmienne egzemplarza w typach ADT są prywatne
różnica między zmiennymi egzem
plarza a zmiennymi lokalnymi w metodzie statycznej lub bloku. Każdej zmiennej
lokalnej w danym momencie odpowiada tylko jedna wartość, natomiast istnieje wiele
wartości powiązanych z każdą zmienną egzemplarza (po jednej na każdy obiekt bę
dący egzemplarzem typu danych). W rozwiązaniu tym nie ma wieloznaczności, po
nieważ każdy dostęp do zmiennej egzemplarza odbywa się za pom ocą nazwy obiektu.
Nazwa określa obiekt, którego wartość jest używana. Ponadto każda deklaracja ma
kwalifikator w postaci modyfikatora widoczności. W implementacjach typów ADT
występuje modyfikator private. Powoduje to wykorzystanie mechanizmu Javy do
wymuszania tego, że reprezentacja typu ADT ma być ukryta przed klientem. Jeśli
wartość ma pozostać niezmienna po jej zainicjowaniu, należy też użyć modyfikatora
final. Typ Counter posiada dwie zmienne egzemplarza: name typu S tri ng i count typu
i nt. Gdyby użyto zmiennych egzemplarza z modyfikatorem public (co jest dozwolo
ne w Javie), typ danych z definicji nie byłby abstrakcyjny, dlatego nie stosujemy tego
rozwiązania.
K onstruktory Każda klasa Javy ma przynajmniej jeden konstruktor, który ustala toż
samość obiektu. Konstruktor przypomina metodę statyczną, może jednak bezpośred
nio korzystać ze zmiennych egzemplarza i nie ma wartości zwracanej. Ogólnie zada
niem konstruktora jest inicjowanie zmiennych egzemplarza. Każdy konstruktor tworzy
obiekt i udostępnia klientowi referencję do obiektu. Konstruktory zawsze mają tę samą
nazwę, co klasa. Można przeciążyć tę nazwę i utworzyć wiele konstruktorów o różnych
1.2 o Abstrakcja danych
p u b lic c l a s s Counter
{
Nazwa klasy
Zmienne p r i v a t e f i n a l S t r i n g name;|
egzemplarza p r i v a t e i n t count;
p u b l i c c o u n t e r ( S t r i ng i d )
Konstruktor -
{ name = i d ; }
p u b lic vo id in c re m e n to
{ count++; }
Metody p u b lic in t t a l l y O
egzemplarza { re tu rn count; }
Nazwa zmiennej
egzemplarza
p u b lic S t rin g t o S t r in g O
{ r e t u r n c o u n t + " " + name; }
Klient testowy — p u b lic s t a t i c vo id m a in (S t rin g [] args)
{
Tworzenie — - c o u n t e r h e a ds = new C o u n t e r ( " o r ł y " ) ;
i inicjowanie ■
C o u n t e r t a i l s = new [ c o u n t e r ( " r e s z k i " ) ; |
obiektów
^ Wywołanie
h eads . i n c r e m e n t ( ) ; konstruktora
h e a ds.i ncrem entO ;
t a i 1s .i ncrem entO ; Automatyczne wywołanie
metody t o s t n n g ( ) Nazwa
obiektu
S td O u t.p rin tln (h e a d s + " " + t a i l s )
S t d o u t . p r i n t l n ( h e a d s . t a l l y O + |t a i l s . t a l l y O |) ;
\
Wywołanie metody
Struktura klasy z definicją typu danych
98 R O ZD ZIA Ł 1 o Podstawy
sygnaturach (tak samo, jak w przypadku metod). Jeśli programista nie zdefiniuje żad
nego innego konstruktora, automatycznie używany jest konstruktor domyślny. Nie ma
on argumentów i inicjuje zmienne egzemplarza domyślnymi wartościami. Wartości do
myślne zmiennych egzemplarza to 0 dla prostych typów liczbowych, f al se dla typu boo-
1ean i nul 1 dla typów referencyjnych. Wartości te można zmienić, używając deklaracji
inicjującej dla zmiennych egzem
plarza. Java automatycznie wywo- p u b li c c l a s s Counter
luje konstruktor, kiedy w kliencie
private f in a l s t r in g
występuje słowo new. Przeciążone p r i v a t e i n t count;
konstruktory zwykle służą do ini
cjowania zmiennych egzemplarza BEZ typu
Modyfikator zwracanej Nazwa konstruktora (taka
podanymi przez klienta wartoś widoczności wartości sama, jak nazwa klasy) / Parametr
ciami innymi niż domyślne. Na
przykład typ Counter posiada
\
p u b !i c | Counter| ( | s t r i n g id )
konstruktor jednoargumentowy, { name == “id; |} \
który inicjuje zmienną egzempla Sygnatura
rza name wartością podaną jako Kod inicjujący zmienne egzemplarza (zmienna
count jest domyślnie inicjowana wartością 0)
argument (zmienna egzemplarza
count jest inicjowana wartością
S tru k tu ra k o n s tru k to ra
domyślną 0).
M etody egzem plarza Aby zaimplementować metody egzemplarza typu danych
(określić działanie każdego obiektu), należy umieścić w metodach egzemplarza kod
taki sam, jaki poznano w p o d r o z d z i a l e i . i przy implementowaniu metod statycz
nych (funkcji). Każda metoda egzemplarza ma typ zwracanej wartości, sygnaturę
(określającą nazwę metody oraz typy i nazwy parametrów) i ciało (składające się
z ciągu instrukcji, w tym instruk-
cji return z wartością zwracaną Modyfikatorwidoczności
Typ zwracanej Nazwa
wartości metody
- Sygnatura
do klienta). Kiedy klient wywołuje _ ł _______ ł _ __ ł _
metodę, wartości parametrów (je |pub!i c||void ||lncrement()|
śli te ostatnie istnieją) są inicjowa { |count|r+; }
ne wartościami podanym i przez \
klienta, instrukcje są wykonywane Nazwa zmiennej egzemplarza
do m om entu napotkania zwraca Struktura metody egzemplarza
nej wartości, po czym wartość jest
zwracana klientowi. Ma to ten sam efekt, co zastąpienie wywołania m etody w klien
cie uzyskaną wartością. Wszystkie działania są takie same jak w metodach statycz
nych, m etody egzemplarza posiadają jednak pewną kluczową cechę — maję dostęp
do zmiennych egzemplarza i mogą wykonywać na nich operacje. Jak można określić,
z którego obiektu zmienne egzemplarza mają zostać zastosowane? Wystarczy za
stanowić się nad tym pytaniem, aby dostrzec logiczną odpowiedź — referencja do
zmiennej w metodzie egzemplarza prowadzi do wartości w obiekcie użytym do wy-
1.2 a Abstrakcja danych
wołania metody. W wywołaniu [Link] ento kod metody increm ento używa
zmiennych egzemplarza obiektu heads. Ujmijmy to inaczej — programowanie obiek
towe wzbogaca używanie zmiennych w programach Javy o niezwykle istotny aspekt:
■ wywoływanie metod egzemplarza działających na wartościach danego obiektu.
Różnica w porównaniu z używaniem samych m etod statycznych jest czysto sem an
tyczna (zobacz „Pytania i odpowiedzi”), jednak w wielu sytuacjach zmienia sposób
myślenia o rozwijaniu kodu przez współczesnych programistów. Jak się okaże, podej
ście to dobrze nadaje się do badania algorytmów i struktur danych.
Zasięg W skrócie m ożna stwierdzić, że w kodzie Javy pisanym przy implementowa
niu metod egzemplarza używane są trzy rodzaje zmiennych:
» zmienne dla parametrów,
° zmienne lokalne,
B zmienne egzemplarza.
Pierwsze dwa rodzaje są takie same jak dla m etod statycznych. Zmienne dla param e
trów są określane w sygnaturze metody i inicjowane wartościami podanym i w wywo
łaniu metody w kliencie. Zmienne lokalne są deklarowane i inicjowane w ciele m e
tody. Zasięgiem zmiennych parametrów jest cała metoda. Dla zmiennych lokalnych
zasięg to dalsze instrukcje w bloku z definicją zmiennej. Zmienne egzemplarza są zu
pełnie odmienne. Przechowują wartości typu danych dla obiektów określonej klasy,
a zasięgiem jest cała klasa (jeśli występuje wieloznaczność, można użyć przedrostka
thi s do wskazania zmiennych egzemplarza). Zrozumienie różnic między trzema ro
dzajami zmiennych w metodach egzemplarza to klucz do skutecznego program owa
nia obiektowego.
r ..L 1 - - -n 1 - Zmienna egzemplarza
p r iv a t e in t v a r;
p r i v a t e v o id m e t h o d l( )
{
Zmienna ^ in t v a r; Dotj
Dotyczy zmiennej lokalnej,
lokalna a NIE zmiennej egzemplarza
var
t h is .v a r
Dotyczy zmiennej egzemplarza
}
p r i v a t e v o id m e tho d2 ( )
{
var
} Dotyczy zmiennej egzemplarza
}
Z a się g z m ie n n y c h e g z e m p la rz a i lo k a ln y c h w m e to d z ie e g z e m p la rz a
100 R O Z D Z IA Ł ! a Podstawy
Interfejs API, klienty i im plem entacje Są to podstawowe elementy, które trzeba zro
zumieć, aby móc tworzyć i stosować abstrakcyjne typy danych w Javie. Implementacja
każdego omawianego typu ADT to klasa Javy z prywatnymi zmiennymi egzemplarza,
konstruktorami, m etodam i egzemplarza i klientem. Do pełnego zrozumienia typu
danych potrzeba interfejsu API, kodu typowego klienta i implementacji. Informacje
te dla typu Counter pokazano na następnej stronie. Aby podkreślić oddzielenie klien
ta od implementacji, zwykle każdego klienta przedstawiamy jako odrębną klasę
z metodą statyczną main(), a metodę main() klienta testowego w definicji typu da
nych rezerwujemy na podstawowe testy jednostkowe i na potrzeby programowania
(każda m etoda egzemplarza wywoływana jest w niej przynajmniej raz). W każdym
rozwijanym typie danych wykonujemy te same zadania. Zamiast myśleć o operacjach
potrzebnych do zrealizowania zadania obliczeniowego (jak postępowano przy nauce
programowania), zastanawiamy się nad potrzebami klienta, a następnie uwzględnia
my je w typie ADT, wykonując trzy poniższe kroki:
■ Określanie interfejsu API. Interfejs API służy do oddzielania klientów od imple
mentacji, co umożliwia programowanie modularne. Przy określaniu interfejsu
API ważne są dwa cele. Po pierwsze, należy umożliwić pisanie przejrzystego
i poprawnego kodu klienta. Dobrym pomysłem jest napisanie kodu klien
ta przed zakończeniem tworzenia interfejsu API. Pozwala to upewnić się, że
określone operacje typu danych to te potrzebne klientom. Po drugie, możliwe
powinno być zaimplementowanie operacji. Nie ma sensu określać operacji, jeśli
nie wiadomo, jak je zaimplementować.
■ Implementowanie klasy Javy zgodnej ze specyfikacją interfejsu API. Najpierw
należy wybrać zmienne egzemplarza, a następnie napisać konstruktory i m eto
dy egzemplarza.
■ Opracowanie wielu klientów testowych w celu potwierdzenia poprawności de
cyzji projektowych podjętych w dwóch pierwszych krokach.
Jakie operacje są potrzebne klientom i jakie wartości typu danych w największym
stopniu ułatwiają wykonywanie tych operacji? Podejmowanie takich podstawowych
decyzji jest istotą rozwijania każdej implementacji.
1.2 a Abstrakcja danych 101
I n t e r f e j s API p u b lic c la s s Counter
C o u n t e r ( S t r i ng i d) Tworzenie licznika o nazwie i d
v o i d in c r e m e n t () Zwiększanie wartości licznika
in t tal ly ( ) Liczba inkrementacji od czasu utworzenia licznika
String to String O Reprezentacja w postaci łańcucha znaków
Typow y klient pub lic c la s s F lip s
{
p u b l i c s t a t i c v o id m a i n ( S t r i n g [ ] args)
{
i n t T = I n t e g e r . p a r s e l n t ( a r g s [ 0 ] );
Co unte r heads = new C o u n t e r ( " o r t y " ) ;
Co unte r t a i l s = new C o u n t e r ( " r e s z k i " ) ;
f o r ( i n t t = 0; t < T; t++ )
if (StdR and om .bernoulli(0.5))
[Link] c r e m e n t o ;
e ls e t a i l [Link] c re m e n t();
StdO u t.p rin tln (h e ad s);
S t d O u t . p r i n t l n ( t a i 1s ) ;
in t d = h e a d [Link] lly () - t a i 1 s . t a l 1y ( ) ;
StdOut.p r in t ln ( "r ó ż n ic a : 11 + M a t h . a b s ( d ) ) ;
}
Zastoso w an ie Im p le m e n ta c ja
p u b l i c c l a s s C o un te r % j a v a F l i p s 1000000
{ 500172 o r t y
p r i v a t e final S t r i n g name; 499828 r e s z k i
p r i v a t e i n t count; r ó ż n i c a : 344
p u b l i c C o u n t e r ( S t r i n g id )
{ ñame = id; }
p u b l i c v o id i n c r e m e n t o
{ co un t++; }
pub lic in t t a l l y ( )
{ r e t u r n co unt; )
pub lic S t r in g t o S t r in g O
{ r e t u r n count + 11 11 + name; )
Abstrakcyjny typ danych dla prostego licznika
1 02 R O Z D Z IA L I □ Podstawy
Więcej implementacji typów ADT Tak jak przy każdym zagadnieniu pro
gramistycznym, tak i tu najlepszym sposobem na zrozumienie wartości oraz przy
datności typów ADT jest staranne przyjrzenie się większej liczbie przykładów i im
plementacji. Będzie to możliwe, ponieważ duża część książki dotyczy implementacji
typów ADT, natomiast kilka dodatkowych przykładów w tym miejscu pozwala uzy
skać podstawową wiedzę.
D a te Na następnej stronie przedstawiono dwie implementacje typu ADT Date opisa
nego na stronie 91. Aby zwiększyć przejrzystość, pominięto konstruktor przetwarzają
cy dane (opisany w ć w i c z e n i u 1 .2 . 1 9 ) i odziedziczone metody equal s ( ) (zobacz stro
nę 115), compareTo() (zobacz stronę 259) i hashCodeQ (zobacz ć w i c z e n i e 3 .4 .2 2 ).
W prostej implementacji widocznej po lewej stronie dzień, miesiąc i rok przecho
wywane są jako zmienne egzemplarza, dlatego metody egzemplarza jedynie zwracają
odpowiednią wartość. Wydajniejsza ze względu na pamięć implementacja przedsta
wiona po prawej stronie wykorzystuje jedną wartość typu i nt do zapisu daty. Użyto tu
wartości o mieszanej podstawie, która reprezentuje datę dla dnia d, miesiąca m i roku
y jako 512y + 32m + d. Dla klienta jedną z istotnych różnic między tymi implemen
tacjami jest możliwość naruszenia niejawnych założeń. Kod oparty na drugiej imple
mentacji wymaga do poprawnego działania, aby dzień zawierał się w przedziale 0 -3 1 ,
miesiąc w przedziale 0 - 1 2 , a rok był dodatni (w praktyce w obu implementacjach
należy sprawdzać, czy miesiące znajdują się w przedziale 1 - 1 2 , a dni w przedziale
1 - 3 1 ; ponadto daty w rodzaju 31 czerwca 2009 i 29 lutego 2009 są niedozwolone,
choć sprawdzenie tego wymaga więcej pracy). Przykład ten podkreśla fakt, że w in
terfejsie API rzadko w pełni określamy wymagania dotyczące implementacji (zwykle
staramy się, jak możemy; tu mogliśmy postarać się bardziej). W kliencie można też
odczuć różnicę między implementacjami w obszarze wydajności. Implementacja po
prawej stronie wymaga mniej pamięci do przechowywania wartości typu danych, jed
nak dzieje się to kosztem dłuższego czasu udostępniania ich klientowi w uzgodnionej
postaci (potrzeba jednej lub dwóch operacji arytmetycznych). Różne zyski i koszty
tego rodzaju są czymś powszechnym. W jednym kliencie preferowana może być jedna
implementacja, a w innym — druga, dlatego należy uwzględnić obie. Jednym z po
wtarzających się zagadnień poruszanych w książce jest konieczność zrozumienia wy
mogów z zakresu pamięci i czasu specyficznych dla różnych implementacji. Trzeba
też uwzględnić dopasowanie implementacji do różnych ldientów. Jedną z kluczowych
zalet stosowania w implementacjach abstrakcji danych jest to, że zwykle można zmie
nić jedną implementację na inną bez modyfikowania kodu klientów.
U trzy m y w a n ie w ielu im p le m e n ta c ji Duża liczba implementacji tego samego in
terfejsu API może prowadzić do problemów z konserwacją i nazwami. W niektórych
przypadkach korzystne jest zastąpienie dawnej implementacji nową, usprawnioną.
W innych sytuacjach potrzebne może być utrzymywanie dwóch implementacji —
jednej odpowiedniej dla jednych ldientów i drugiej, właściwej dla innych. Głównym
celem tej książki jest szczegółowe omówienie kilku implementacji każdego z wie
lu podstawowych typów ADT. Implementacje te zwykle mają inne cechy związane
1.2 0 Abstrakcja danych 103
In te rfe js API public c la s s Date
D ate(int month, in t day, in t year) Tworzenie daty
i n t month() Miesiąc
i n t da y() Dzień
i n t ye a r() p 0k
String to Strin g O Reprezentacja w postaci łańcucha znaków
Klient testowy Zastosowanie
public s t a t i c void m a in ( Str in g [] args) % java Date 12 31 1999
( 12/31/1999
in t m = I n t e g e r . p a r s e l n t ( a r g s [0])
int d = Integer.p a rs e ln t( a r g s [l])
i n t y = I n t e g e r . p a r s e l n t ( a r g s [2])
Date date = new Date(m, d, y ) ;
S t d O u t . p r i n t ln ( d a t e ) ;
Implementacja Inna Implementacja
public c l a s s Date p ub lic c l a s s Date
{ f
p riv ate final i n t month; p riv a t e final i n t value;
p riv ate final i n t day;
private final i n t year; p ub lic D a t e ( in t m, i n t d, i n t y)
{ value = y*512 + m*32 + d; }
p ublic D ate(int m, i n t d, in t y)
{ month = m; day = d; yea r = y; } p ub lic in t month()
{ return (value / 32) % 16; )
p ub lic i n t month()
{ return month; } p ub lic i n t day()
{ return value % 32; }
public i n t day()
{ return day; ) p ub lic i n t yea r()
{ return value / 512; }
public i n t yea r()
{ return day; } p ub lic S t r i n g t o S t r i n g O
{ return month() + "/ " + day()
public S t r i n g t o S t r i n g O + "/" + y e a r ( ) ; }
{ return month() + "/ " + day() }
+ "/ " + y e a r ( ) ; )
)
Dwie im plem entacje abstrakcyjnego typu danych służącego do herm etyzacji dat
104 R O ZD ZIA Ł 1 » Podstawy
z wydajnością. W książce często porównywana jest wydajność jednego klienta przy
korzystaniu z dwóch różnych implementacji tego samego interfejsu API. Dlatego
zwykle stosujemy nieformalne konwencje nazewnicze o następujących cechach:
■ Różne implementacje tego samego interfejsu API są określone za pom ocą opi
sowego przedrostka. Na przykład implementacje typu Date z poprzedniej stro
ny można nazwać Basi cDate i Smal 1Date. Można też opracować implementację
SmartDate, sprawdzającą, czy data jest poprawna.
■ Utrzymywanie podstawowej implementacji bez przedrostka, obejmujące roz
wiązania odpowiednie dla większości klientów. Oznacza to, że w większości
klientów należy użyć prostego typu Date.
W dużych systemach opisane rozwiązanie nie jest idealne, ponieważ może wyma
gać modyfikowania kodu klienta. Na przykład po opracowaniu nowej implementa
cji ExtraSmal 1Date jedyną możliwością jest zmiana kodu klienta lub utworzenie tej
implementacji jako podstawowej, używanej przez wszystkie klienty. Java ma wiele
różnych mechanizmów do utrzymywania wielu implementacji bez konieczności
modyfikowania kodu klienta, jednak rzadko korzystamy z tych rozwiązań, ponieważ
nawet dla ekspertów stosowanie ich jest skomplikowane (a nawet kontrowersyjne),
przede wszystkim w połączeniu z innymi, cenionymi przez nas mechanizmami ję
zyka (takimi jak typy generyczne i iteratory). Kwestie te są ważne (ich pominięcie
doprowadziło do słynnego problemu roku 2000 na przełomie tysiącleci, ponieważ
w wielu programach korzystano z niestandardowych implementacji abstrakcji daty,
w których nie uwzględniono dwóch pierwszych cyfr roku), jednak szczegółowe om a
wianie ich odwiodłoby nas od badania algorytmów.
A ku m u la to r Interfejs API akumulatora przedstawiony na następnej stronie to defi
nicja abstrakcyjnego typu danych, umożliwiającego klientom przechowywanie śred
nich wartości danych. Ten typ danych jest często używany w książce do przetwarza
nia wyników eksperymentów (zobacz p o d r o z d z ia ł 1 .4 ). Implementacja jest prosta
— program przechowuje w zmiennej egzemplarza typu i nt liczbę napotkanych war
tości danych, a w zmiennej egzemplarza typu doubl e — sumę tych wartości. W celu
obliczenia średniej program dzieli sumę przez liczbę. Zauważmy, że implementacja
nie zapisuje wartości danych. Można ją wykorzystać dla bardzo dużej liczby wartości
(nawet w urządzeniu, które nie umożliwia ich zapisania). Ponadto w dużym systemie
można zastosować znaczną liczbę akumulatorów. Są to zaawansowane cechy z obsza
ru wydajności, które można opisać w interfejsie API (implementacja, która zapisuje
wartości, może doprowadzić do wyczerpania pamięci przez aplikację).
1.2 ■ Abstrakcja danych 105
I n t e r f e j s API p u b lic c la s s Accum ulator
AccumulatorO Tworzenie akumulatora
void addDataValue(double val) Dodawanie nowej wartości
double mean() Średnia wszystkich wartości
String to S tr in g O Reprezentacja w postaci łańcucha znaków
Klient testowy p ub lic c l a s s TestAccumulator
{
p ub lic s t a t i c void m a in ( Str in g [] args )
(
int T = In t e g e r.p a rse ln t(a rg s[0 ]);
Accumulator a = new AccumulatorO;
f o r (i n t t = 0; t < T; t++)
[Link]([Link]());
Std O u t.p rintln (a);
1
Zastosowanie
% java TestAccumulator 1000
Średnia (l i c z b a wartości = 1000): 0.51829
% java TestAccumulator 1000000
Średnia (l i c z b a wartości = 1000000): 0.49948
% java TestAccumulator 1000000
Średnia (l i c z b a wartości = 1000000): 0.50014
Implementacja
pub lic c l a s s Accumulator
{
p riv a t e double t o t a l ;
p riv a t e in t N;
pub lic void addDataValue(double val)
(
N++;
t otal += v a l ;
1
p ub lic double mean()
{ retu rn t o t a l /N; }
p ub lic S t r i n g t o S t r i n g O
( return “Średnia (l i c z b a wartości = " + N + " ) : 11
+ S t r i n g . f o r m a t ( " % 7 . 5 f " , mean()); }
Abstrakcyjny typ danych do akum ulowania wartości
10 6 R O Z D Z IA L I a Podstawy
W izualny akum ulator Implementacja wizualnego akumulatora przedstawiona
na następnej stronie to rozwinięcie typu Accumul ator, pozwalające zaprezentować
przydatny efekt uboczny. Wersja ta rysuje na StdDraw wszystkie dane (szarym ko
lorem) i bieżącą średnią (na czerwono).
Wysokość N-tej czerwonej kropki
Najłatwiejszy sposób na uzyskanie tego od lewej to średnia wysokości
lewych N szarych kropek
efektu to dodanie konstruktora, któ
ry określa liczbę rysowanych punktów
i maksymalną wartość (używaną do
zmiany skali). Visual Accumul ato r pod
względem technicznym nie jest imple
Wysokość szarej
mentacją interfejsu API Accumul ato r kropki to wartość
punktu danych
(konstruktor ma tu inną sygnaturę
i powoduje inny efekt uboczny). Ogólnie Rysunek wygenerowany przez wizualny akumulator
staramy się tworzyć pełne specyfikacje
interfejsów API i unikamy wprowadzania jakichkolwiek zmian po określeniu inter
fejsu, ponieważ może to wymagać modyfikacji w kodzie nieznanej liczby ldientów
(i w implementacji). Czasem jednak m ożna uzasadnić dodanie konstruktora w celu
wzbogacenia możliwości, ponieważ wymaga to zmiany w kodzie klienta tego samego
wiersza, który my zmieniamy przy modyfikowaniu nazwy klasy. W tym przykładzie
jeśli zbudowano klienta, który używa typu Accumul ato r i prawdopodobnie obejmu
je liczne wywołania m etod addDataVal ue() i avg(), m ożna wykorzystać zalety typu
Vi sual Accumul ator, zmieniając jeden wiersz kodu.
Zastosowanie
% j a v a T e s t V i s u a l A c c u m u l a t o r 2000
Ś r e d n i a ( l i c z b a w a r t o ś c i = 2 0 0 0 ) : 0 .5 0 9 7 8 9
1.2 b Abstrakcja danych 1 07
I n t e r f e j s API pub lic c l a s s V i sualAccumulator
Vis ua lA ccu m u la tor(int t r i a l s , double max)
void addDataVal ue (double val) Dodawanie nowej wartości
double avg() Średnia wszystkich wartości
String to Strin g O Reprezentacja w postaci łańcucha znaków
K lien t t e s t o w y p ub lic c l a s s TestVi sual Accumul ator
{
p ub lic s t a t i c void m a i n ( S t r i n g [] args)
1
i n t T = I n t e g e r . p a r s e l n t ( a r g s [0]) ;
Visua l Accumulator a = new V i s u a l Accumulator(T, 1.0);
f o r ( i n t t = 0; t < T; t++)
[Link]([Link]());
StdO u t.p rintln (a);
}
)
Implementacja p ub lic c l a s s Vi sual Accumul ato r
{
p riv a t e double t o t a l;
p riv a t e i n t N;
p ub lic V is ua lA ccu m u la tor(int t r i a l s , double max)
{
St dDra [Link] Xscale(0, t r i a l s ) ;
[Link](0, max);
[Link] Pen Radius(. 005);
p ub lic void addDataValue(double val)
(
N++;
t otal += v a l ;
[Link](StdDraw.DARK_GRAY);
[Link](N, v a l) ;
[Link]([Link]);
[Link](N, t o ta l/N );
}
p ub lic double mean()
p ub lic S t r i n g t o S t r i n g O
// Tak samo, jak w ty p ie Accumulator.
Abstrakcyjny typ danych do akum ulowania wartości (wersja wizualna)
RO ZD ZIA Ł 1 □ Podstawy
Projektowanie typu danych Abstrakcyjny typ danych to typ, którego reprezen
tacja jest ukryta przed klientem. Podejście to wywarło bardzo duży wpływ na współ
czesne programowanie. Różne omówione przykłady zapewniają słownictwo potrzeb
ne do poznawania zaawansowanych cech typów ADT i ich implementacji w formie
klas Javy. Wiele zagadnień pozornie nie dotyczy badań nad algorytmami, dlatego
można pominąć ten fragment i wrócić do niego później, w kontekście konkretnych
problemów z obszaru implementacji. Celem jest tu umieszczenie w jednym miejscu
ważnych informacji związanych z projektowaniem typów danych i zapewnienie pod
staw dla implementacji pojawiających się w książce.
H erm etyzacja Kluczową cechą programowania obiektowego jest to, że umożliwia
hermetyzowanie typów danych w ich implementacjach, co ułatwia oddzielne rozwi
janie klientów i implementacji typów danych. Hermetyzacja umożliwia program o
wanie modularne, pozwalające na:
■ Niezależne rozwijanie kodu klienta i implementacji.
■ Podstawianie usprawnionych implementacji bez wpływu na klienta.
■ Zapewnianie obsługi nienapisanych jeszcze programów (interfejs API zawiera
wskazówki pom ocne w rozwijaniu nowych klientów).
Hermetyzacja powoduje też odizolowanie operacji na typie danych, co ma następu
jące skutki:
■ Zmniejsza prawdopodobieństwo wystąpienia błędów.
■ Pozwala dodać do implementacji testy spójności i inne mechanizmy diagno
styczne.
* Zwiększa przejrzystość kodu klienta.
Hermetycznego typu danych może używać każdy klient, dlatego takie typy stanowią
rozszerzenie Javy. Zalecany przez nas styl programowania jest oparty na podziale dużych
programów na małe moduły, które można rozwijać i diagnozować niezależnie. Podejście
to zwiększa odporność oprogramowania, ponieważ ogranicza efekty zmian i wyznacza
zakres takich skutków. Ponadto sprzyja wielokrotnemu wykorzystaniu kodu z uwagi na
możliwość podstawienia nowych implementacji typu danych w celu zwiększenia wydaj
ności i dokładności lub poprawy wykorzystania pamięci. To samo podejście sprawdza
się w wielu kontekstach. Często zalety hermetyzacji można wykorzystać przy używaniu
bibliotek systemowych. Nowe wersje Javy nieraz obejmują nowe implementacje różnych
typów danych lub bibliotek metod statycznych, jednak interfejsy API się nie zmieniają.
W kontekście badania algorytmów i struktur danych istnieje mocna oraz stała moty
wacja do rozwijania lepszych algorytmów, ponieważ pozwala to zwiększyć wydajność
wszystkich klientów przez podstawienie usprawnionej implementacji typu ADT. Nie
wymaga to jednocześnie zmiany kodu żadnego klienta. Kluczem do sukcesu w progra
mowaniu modularnym jest zachowanie niezależności między modułami. Dlatego ważne
jest, aby interfejs API byi jedynym obszarem zależności między klientem a implemen
tacją. Aby używać typu danych, nie trzeba wiedzieć, jak jest zaimplementowany. Ponadto
w trakcie implementowania typu danych można założyć, że klient zna tylko jego interfejs
API. Hermetyzacja to klucz do uzyskania obu tych korzyści.
1.2 n Abstrakcja danych 109
Projektow anie interfejsów A P I Jednym z najważniejszych i najtrudniejszych kro
ków przy budowaniu współczesnego oprogramowania jest projektowanie interfejsów
API- Zadanie to wymaga praktyki, starannego przemyślenia i wielu podejść, jednak
czas poświęcony na zaprojektowanie dobrego interfejsu API z pewnością zwróci się
z uwagi na oszczędność czasu przy diagnozowaniu oraz w wyniku wielokrotnego wy
korzystania kodu. Tworzenie interfejsu API może wydawać się przesadą przy pisaniu
krótkiego programu, warto jednak rozwijać każdy program z myślą o tym, że któregoś
dnia konieczne będzie powtórne wykorzystanie kodu. W idealnych warunkach inter
fejs API jasno określa działanie kodu (w tym efekty uboczne) dla wszystkich możli
wych danych wejściowych, a ponadto dostępne jest oprogramowanie sprawdzające,
czy implementacje są zgodne ze specyfikacją. Niestety, jeden z podstawowych efek
tów z obszaru teoretycznych nauk komputerowych, problem specyfikacji, wskazuje
na to, że cel ten (sprawdzenie zgodności ze specyfikacją) jest nieosiągalny. Omówmy
pokrótce ten efekt — specyfikacja musiałaby być napisana w języku formalnym, ta
kim jak język programowania, a z matematyki wiadomo, że problem ustalenia, czy
dwa programy wykonują te same obliczenia, jest nierozstrzygalny. Dlatego używane
w książce interfejsy API to krótkie opisy w języku polskim, określające zbiór warto
ści powiązanego abstrakcyjnego typu danych, a także listy konstruktorów i metod
egzemplarza, także z krótkim i opisami (po polsku) ich przeznaczenia z uwzględnie
niem efektów ubocznych. Aby stwierdzić poprawność projektu, zawsze zamieszcza
my przykładowy kod klienta w tekście blisko interfejsu API. W tym ogólnym sche
macie istnieje wiele pułapek zagrażających projektowi każdego interfejsu API:
n Interfejs API może być zbyt trudny do zaimplementowania, ponieważ niezbęd
na implementacja jest trudna lub niemożliwa do napisania.
0 Interfejs API może być zbyt trudny w użyciu, co prowadzi do powstawania kodu
klienta, który jest bardziej skomplikowany niż bez interfejsu API.
° Interfejs API może być zbyt wąski i nie zawierać m etod potrzebnych klientom.
D Interfejs API może być zbyt szeroki i obejmować wiele m etod niepotrzebnych
żadnemu klientowi. Ten problem występuje prawdopodobnie najczęściej i naj
trudniej jest go uniknąć. Wielkość interfejsu API zwykle rośnie z czasem, p o
nieważ nietrudno jest dodawać m etody do istniejącego interfejsu, natomiast
trudno jest je usunąć bez naruszania istniejących klientów.
° Interfejs API może być zbyt ogólny i nie udostępniać przydatnych abstrakcji.
0 Interfejs API może być zbyt specyficzny i udostępniać abstrakcje tak szczegóło
we lub niezrozumiałe, że staje się bezużyteczny.
D Interfejs API może być zbyt zależny od konkretnej implementacji, przez co nie
umożliwia swobodnego wyboru reprezentacji w kodzie klienta. Także tej pu
łapki trudno jest uniknąć, ponieważ reprezentacja jest, oczywiście, kluczowym
aspektem rozwijania implementacji.
Te rozważania można ująć w kolejnym stwierdzeniu — udostępniaj klientom tylko te
metody, których potrzebują, i żadnych innych.
110 R O Z D Z IA L I O Podstawy
A lgorytm y i abstrakcyjne typy danych Abstrakcja danych w naturalny sposób uła
twia badanie algorytmów, ponieważ pomaga utworzyć schemat, w którym można
precyzyjnie określić zarówno to, co algorytm ma robić, jak i sposób korzystania z nie
go przez klienta. W tej książce algorytm jest zwykle implementacją m etody egzem
plarza z abstrakcyjnego typu danych. Przykładowo, program do sprawdzania białych
list, opisany w początkowej części rozdziału, można przedstawić jako klienta typu
ADT, wykorzystując następujące operacje:
■ Tworzenie obiektu SET na podstawie tablicy wartości.
■ Określanie, czy dana wartość znajduje się w zbiorze.
Operacje te ukryto w typie ADT StaticSEToflnts, przedstawionym na następnej
stronie wraz z typowym klientem — programem Whi te l i st. StaticSEToflnts to spe
cjalny przypadek ogólniejszego i przydatniejszego typu ADT, tablicy symbolicznej,
która jest tematem r o z d z i a ł u 3 . Wyszukiwanie binarne to jeden z kilku omawianych
algorytmów odpowiednich do implementowania typów ADT tego rodzaju. W po
równaniu z implementacją programu BinarySearch ze strony 59 ta implementacja
prowadzi do bardziej przejrzystego i użytecznego kodu klienta. Typ S ta ti cSETof Ints
wymusza na przykład sortowanie tablicy przed wywołaniem m etody rank(). Za po
mocą tego abstrakcyjnego typu danych można oddzielić klienta od implementacji,
co ułatwia każdemu klientowi wykorzystanie zalet algorytmu wyszukiwania binar
nego przez samo stosowanie się do interfejsu API (w klientach używających metody
rank() z program u BinarySearch trzeba pamiętać o wcześniejszym posortowaniu
tablicy). Program do sprawdzania białych list to jeden z wielu klientów, które mogą
wykorzystać wyszukiwanie binarne.
to zbiór metod Zastosowanie
K A Ż D Y PR O G R A M JA V Y
statycznych i (lub) implementacja typu % java Whi t e l i st la [Link] < la rg e T . tx t
danych. W tej książce skoncentrowano 499569
się głównie na implementacjach abs 984875
295754
trakcyjnych typów danych, takich jak 207807
StaticSEToflnts, gdzie najważniejsze są 140925
operacje, a reprezentacja danych jest ukry 161828
wana przed klientem. Jak pokazano w tym
przykładzie, abstrakcja danych umożliwia:
■ Precyzyjne określanie, co algorytmy dają klientom.
■ Oddzielanie implementacji algorytmów od kodu klienta.
■ Rozwijanie warstw abstrakcji, co pozwala zastosować dobrze znane algorytmy
do tworzenia innych algorytmów.
Są to pożądane cechy każdej techniki opisywania algorytmów, niezależnie od tego,
czy służy do tego język polski czy pseudokod. Zastosowanie mechanizmu cl ass Javy
do uzyskania abstrakcji danych ma mało wad, a wiele zalet. Mechanizm ten pozwala
uzyskać działający kod, który można przetestować i wykorzystać do porównania wy
dajności różnych klientów.
1.2 ta Abstrakcja danych 111
I n t e r f e j s API p ublic c la s s S ta ticSE T o fln ts
S t a t i cSETof I n t s (i nt [] a) Tworzenie zbioru na podstawie wartości z a []
boolean c on ta ins! i nt key) Czy key znajduje się w zbiorze?
Typow y k lie n t pUbl i c c l a s s Whi tel i st
{
p ub lic s t a t i c void m a in ( Str in g [] args )
{
i nt [] w = l n . r e a d l n t s ( a r g s [ 0 ] ) ;
S t a t i c S E T o f ln t s set = new S t a t i c S E T o f l n t s ( w ) ;
while ( ! S t d l n . isEmpty())
{ // Wczytywanie klucza i wyświetlanie go, j e ś l i nie występuje
na b iałe j l i ś c i e ,
i n t key = S t d l n . r e a d l n t ( ) ;
i f ( ! s e t .c o n ta in s (k e y ) )
S t d O u t . p r i n t ln ( k e y ) ;
}
}
Im p le m e n ta c ja import j a v a . u t i l . A r r a y s ;
p ub lic c l a s s S t a t i c S E T o f ln t s
(
p riv ate i n t [] a;
p ub lic S t a t i c S E T o f I n t s ( i n t [ ] keys)
{
a = new i n t [ k e y s . l e n g t h ] ;
fo r ( i n t i = 0; i < k [Link]; i++)
a [ i ] = k e y s [ i ] ; // Kopia zabezpieczająca.
A [Link](a);
)
p ub lic boolean c o n t a i n s ( i n t key)
{ return rank(key) != -1 ; )
p riv a t e i n t ra n k (in t key)
{ // Wyszukiwanie binarne,
i n t lo = 0;
in t hi = a .length - 1;
while (lo <= hi)
{ // Klucz znajduje s i ę w przed ziale a [1 o . .hi]
// lub w ogóle nie i s t n i e j e ,
i n t mid = lo +(hi - lo) / 2;
if (key <a [mid]) hi = mid - 1;
e lse i f (key > a [mid]) lo =mid+ 1;
e lse return mid;
}
return -1;
}
}
Wyszukiwanie binarne przekształcone na program obiektowy
(typ ADT do wyszukiwania elementów w zbiorze liczb całkowitych)
112 R O Z D Z IA L I a Podstawy
Dziedziczenie interfejsu Java obsługuje dziedziczenie, co umożliwia definiowanie
związków między obiektami. Mechanizmy dziedziczenia są powszechnie stosowane
przez programistów, dlatego jeśli bierzesz udział w kursie z inżynierii oprogramowa
nia, z pewnością szczegółowo się z nimi zapoznasz. Pierwszy omawiany tu mechanizm
dziedziczenia to tworzenie podtypów. Za pomocą tej techniki można określić zależno
ści między niepowiązanymi klasami, określając w interfejsie zbiór wspólnych metod,
które musi obejmować każda klasa z implementacją tego interfejsu. Interfejs jest ni
czym więcej jak listą m etod egzemplarza. Przykładowo, zamiast używać stosowanych
w książce nieformalnych interfejsów API, można opisać interfejs typu Date tak:
public interface Datable
{
in t month();
in t day() ;
in t y e a r();
}
Następnie w kodzie implementacji można wskazać interfejs:
public c la s s Date implements Datable
{
// Kod im p le m e n t a c ji (taki sam, j a k w c z e ś n i e j ) .
Kompilator Javy sprawdzi wtedy, czy kod pasuje do interfejsu. Dodanie fragmen
tu implements Datable do klasy z implementacją operacji month(), day() i year()
stanowi gwarancję dla klientów, że obiekt danej klasy pozwala wywołać te metody.
Ta technika to dziedziczenie interfejsu. Klasa z implementacją dziedziczy interfejs.
Dziedziczenie interfejsu pozwala pisać programy klienckie, które mogą manipulować
obiektami dowolnego typu z implementacją danego interfejsu (nawet typu, który jesz
cze nie istnieje), wywołując m etody z interfejsu. Mogliśmy zastosować dziedziczenie
interfejsów zamiast mniej formalnych interfejsów API, jednak zdecydowaliśmy się
na inne rozwiązanie, aby uniknąć zależności od specyficznych, wysokopoziomowych
mechanizmów języka, które
Interfejs Metody Podrozdział nie są kluczowe do zrozumie
nia algorytmów. Zastosowane
j [Link] compareToO 2.1
Porównywanie podejście pozwala też unik
ja va .u til.C om p a ra tor compare() 2.5 nąć dodatkowego obciążenia
[Link] iterator() 1.3 w postaci plików interfejsu.
Jednak w niektórych sytu
Iterowanie hasNext()
acjach mechanizmy Javy
j a v a . u t i l . Ite ra to r next() 1.3
remove() sprawiły, że uznaliśmy, iż
warto wykorzystać interfejsy.
Interfejsy Javy używane 1w książce
1.2 s Abstrakcja danych 1 13
Stosujemy je do porównywania i iterowania, co opisano w tabeli w dolnej części po
przedniej strony. Interfejsy opisano bardziej szczegółowo przy omawianiu zagadnień
w y m ien io n y ch w tabeli.
Dziedziczenie im plem entacji Java obsługuje też inny mechanizm dziedziczenia
— tworzenie podklas. Technika ta daje duże możliwości i pozwala programistom
modyfikować operacje oraz dodawać funkcje bez pisania całej klasy od podstaw.
Podejście polega na definiowaniu nowej klasy (podklasy lub klasy pochodnej) dzie
dziczącej metody egzemplarza oraz zmienne egzemplarza po innej klasie (nadklasie
lub klasie bazowej). Podklasa zawiera więcej m etod niż nadldasa. Ponadto w podkla-
sie można przedefiniować lub przesłonić metody z nadklasy. Tworzenie podklas jest
powszechnie stosowane przez programistów systemów przy rozwijaniu tak zwanych
bibliotek rozszerzalnych. Jeden programista (także Ty) może dodać m etody do bi
blioteki zbudowanej przez kogoś innego (lub przez zespół programistów systemów),
ponownie wykorzystując kod biblioteki (z czasem może stać się ona bardzo duża).
To podejście jest często stosowane na przykład przez programistów interfejsów GUI,
co pozwala ponownie wykorzystać dużą ilość kodu potrzebnego do udostępnienia
wszystkich mechanizmów oczekiwanych przez użytkowników (menu rozwijanych,
obsługi wycinania i wklejania, dostępu do plików itd.). Tworzenie podklas jest uzna
wane za kontrowersyjne wśród programistów systemów i aplikacji (zalety tej tech
niki w porównaniu z dziedziczeniem interfejsu są dyskusyjne). W tej książce uni
kamy tej techniki, ponieważ zwykle utrudnia hermetyzację. Pewne elementy tego
podejścia są wbudowane w Javę, dlatego nie da się ich uniknąć. Na przykład każda
klasa jest podklasą klasy Object Javy. Ta struktura umożliwia istnienie „konwencji”,
zgodnie z którą każda klasa obejmuje implementację m etod getC lass(), to S tri ng(),
equal s(), hashCode() i kilku innych, których nie używamy w tej książce. W rzeczy
wistości każda ldasa dziedziczy te m etody po Masie Obj ect jako jej podldasa, dlatego
każdy Mient może korzystać z tych m etod dla dowolnego obiektu. ZwyMe w nowych
Masach przesłania się metody to S tri ng(), equal s() i hashCode(), ponieważ domyśl
na implementacja Masy Object zazwyczaj nie działa w pożądany sposób. Metody to
Stri ng () i equal s() opisano w tym miejscu. Omówienie m etody hashCode() znajdu
je Się W PODROZDZIALE 3 .4 .
Metoda Przeznaczenie Podrozdział
C la ss g e t C la s s () Jakiej klasy jest dany obiekt? 1.2
S t r i ng t o S t r i ng () Reprezentacja obiektu w postaci łańcucha znaków 1.1
boolean equals (Object that) Czy dany obiekt jest równy that? 1.2
int hashCode() Kod skrótu dla obiektu 3.4
Używane w książce metody odziedziczone po klasie Object
114 RO ZD ZIA Ł 1 o Podstawy
Przekształcanie łańcuchów znaków Zgodnie z konwencją każdy typ Javy dziedziczy
po klasie Object metodę to S trin g O , dlatego każdy klient może wywołać ją dla do
wolnego obiektu. Ta konwencja stanowi podstawę stosowanego w Javie automatycz
nego przekształcania jednego operandu operatora łączenia + na typ S tri ng, jeśli dru
gi operand ma ten typ. Jeżeli typ danych obiektu nie obejmuje implementacji metody
to S trin g O , wywoływana jest domyślna implementacja z klasy Object. Przeważnie
nie jest ona przydatna, ponieważ zwykle zwraca łańcuch znaków z adresem obiektu
w pamięci. Dlatego zazwyczaj w każdej rozwijanej klasie dodajemy implementację
m etody to S tri ng (), przesłaniającą wersję domyślną, tak jak w klasie Date na następ
nej stronie. Jak pokazano to w kodzie, implementacje metody to S trin g O są często
dość proste. Pośrednio (przez operator +) wykorzystano w nich metodę to S tri ng ()
dla każdej zmiennej egzemplarza.
Typy nakładkowe Java udostępnia wbudowane typy referencyjne (nazywane typami
nakładkowymi) — po jednym dla każdego z typów prostych. Typy te to Bool ean, Byte,
Character, Double, Float, Integer, Long i Short (odpowiadają one typom boolean,
byte, char, double, float, in t, long oraz short). Składają się głównie z metod statycz
nych w rodzaju p a rse ln t(), a ponadto obejmują odziedziczone metody egzemplarza
to S trin g O , compareTo(), equal s() i hashCodeQ. Jeśli jest to uzasadnione, Java auto
matycznie przekształca dane z typów prostych na nakładkowe, co opisano na stronie
135. Na przykład przy łączeniu wartości typu i nt z wartością typu S tri ng ta pierwsza
jest przekształcana na typ Integer, dla którego można wywołać metodę to S tri ng ().
Równość Co oznacza, że dwa obiekty są sobie równe? Sprawdzanie równości za pom o
cą wyrażenia (a == b), gdzie a i b to zmienne referencyjne tego samego typu, pozwala
określić, czy obiekty mają tę samą tożsamość (czy ich referencje są takie same). W typo
wych klientach ważniejsze jest sprawdzanie, czy wartości typu danych (stan obiektu) są
identyczne, lub zaimplementowanie pewnych reguł specyficznych dla typu. Java zapew
nia dobry początek, ponieważ udostępnia implementacje na potrzeby obu tych zadań
dla typów standardowych, takich jak Integer, Doubl e i S tri ng, a także bardziej skom
plikowanych, na przykład Fi 1e i URL. Przy stosowaniu tych typów danych można użyć
wbudowanych implementacji. Przykładowo, jeśli x i y to wartości typu S tri ng, wyraże
nie x .equal s (y) ma wartość true wtedy i tylko wtedy, jeśli x i y mają tę samą długość
oraz są identyczne na każdej pozycji. Przy definiowaniu własnych typów danych, takich
jak Date lub Transaction, trzeba przesłonić metodę equal s(). Zgodnie z konwencjami
Javy metoda equal s () musi wyznaczać relację równoważności. Musi więc być:
• zwrotna — x. equal s(x) to true;
n symetryczna — x . equal s (y) to tru e wtedy i tylko wtedy, jeśli y . equal s (x);
n przechodnia — jeśli [Link](y) i [Link] u als(z) mają wartość true, to
x .equals (z ).
Ponadto metoda musi przyjmować Object jako argument i mieć następujące cechy:
■ spójność — kolejne wywołania x . equal s (y) powinny zwracać tę samą wartość
(jeśli żadnego obiektu nie zmodyfikowano);
■ wykrywać nierówność dla nuli — x .e q u a ls (n u ll) ma zwracać fal se.
1.2 a Abstrakcja danych 11
Są to naturalne definicje, jednak spełnienie podanych wymogów, stosowanie się do
konwencji Javy i uniknięcie zbędnej pracy przy implementowaniu może być trudne,
co pokazano na przykładzie typu Date. Zastosowano tu opisane krok po kroku po
dejście:
□ Jeśli referencja do obiektu jest taka sama, jak referencja do argumentu, na
leży zwrócić true. Pozwala to uniknąć sprawdzania wszystkich pozostałych
warunków.
o Jeżeli argument to nuli, należy zwrócić fa lse , aby przestrzegać konwencji
(i uniknąć podążania za pustą referencją w dalszym kodzie).
o Jeśli obiekty nie są tej samej klasy, należy zwrócić fa lse . Do ustalania klasy
obiektu służy metoda getC lass(). Zauważmy, że m ożna tu użyć operatora ==
do określenia, czy dwa
obiekty typu Cl ass są sobie
p ub lic c l a s s Date
równe, ponieważ metoda
getC lass() zwraca tę samą p riv a t e final i n t month;
referencję dla wszystkich p riv a t e final i n t day;
p riv a t e final in t year;
obiektów danej ldasy.
D Rzutowanie argumentu z ty p ub lic D ate(int m, in t d, i n t y)
pu Obj ect na Date (z uwagi { month = m; day = d; year = y; }
na wcześniejszy test rzuto
p ub lic i n t month()
wanie musi się powieść). { return month; }
■ Zwrócenie fal se, jeśli któ
p ub lic in t day()
reś ze zmiennych egzem
{ return day; }
plarza nie pasują do siebie.
Dla innych Mas odpowied publ i c in t yearf)
nie mogą być inne defini { return year; }
cje równości. Na przyMad p ublic S t r i n g t o S t r i n g O
dwa obiekty typu Counter { return month0 + " / " + day() + "/ " + y e a r ( ) ; }
można traktować jako
pub lic boolean equals(Object x)
równe, jeśli ich zmienne
egzemplarza count mają tę i f ( t h i s == x) return true;
samą wartość. i f (x == n u ll ) return f a l s e ;
i f ( t h i s . g e t C l a s s O != x . g e t C l a s s ()) return f a l s e ;
Implementacja ta to model, któ
Date that = (Date) x;
rego można użyć do zaimple i f (t h i s . d a y != [Link]) return f a l s e ;
mentowania metody eq ua 1 s () i f ([Link] != [Link]) return f a l s e ;
dla dowolnego typu. Po zaimple i f ( t h i s . y e a r != [Link]) return f a l s e ;
return true;
mentowaniu jednej taMej m eto
dy zaimplementowanie innych
nie powinno sprawiać trudności.
Przesłanianie metod toStringO i equals!) w definicji typu danych
116 R O Z D Z IA L I b Podstawy
Z arządzanie pam ięcią Możliwość przypisania nowej wartości do zmiennej refe
rencyjnej powoduje, że program może utworzyć obiekt, do którego nie da się uzyskać
dostępu. Rozważmy trzy instrukcje przypisania z rysunku po lewej stronie. Po trze
ciej instrukcji przypisania a i b prowadzą do tego samego obiektu Date (1/1/2011),
a ponadto nie istnieje referencja do obiektu Date,
Date a = new D a te (12 , 3 1 , 19 9 9 ); , , , , . . . . TJ
Date b = new D a t e ( l, 1 , 2 0 1 1 ) ; który wykorzystano do zainicjowania a. Jedyna re
b = a; ferencja do tego obiektu znajdowała się w zmiennej
a. Referencję tę nadpisano w wyniku przypisania,
dlatego nie ma możliwości dotarcia do obiektu. Taki
obiekt nazywa się osieroconym. Obiekty stają się osie
8 11 , Referencje do tego rocone także po wyjściu z zasięgu. Programy Javy
8 11 samego obiektu tworzą bardzo dużą liczbę obiektów (i zmiennych
przechowujący wartości prostych typów danych),
jednak w danym momencie potrzebna jest ich nie
wielka część. Dlatego w językach programowania
Obiekt
osierocony i systemach potrzebne są mechanizmy alokowania
655 12 pamięci na wartości typu danych na czas, kiedy są
_ Ostatni dzień
656 31 potrzebne, oraz zwalniania pamięci, kiedy wartość
1999 roku
657 1999 nie jest już przydatna (lub po osieroceniu obiektu).
Zarządzanie pamięcią jest łatwiejsze dla typów pro
stych, ponieważ wszystkie informacje potrzebne do
8 11 alokacji pamięci są dostępne na etapie kompilacji.
Pierwszy dzień
812 2011 roku
Java (i większość innych systemów) odpowiada za re
813 2011 zerwowanie pamięci na zmienne w momencie ich de
klarowania i zwalnianie pamięci, kiedy zmienna wy
chodzi z zasięgu. Zarządzanie pamięcią dla obiektów
jest trudniejsze. System może zaalokować pamięć na
Osierocony obiekt obiekt w momencie jego tworzenia, jednak nie wie,
kiedy dokładnie ma ją zwolnić, ponieważ to sposób
działania programu wyznacza czas osierocenia obiektu. W wielu językach (takich jak
C i C++) to programista odpowiada za alokowanie i zwalnianie pamięci. Podejście
to jest żm udne i podatne na błędy. Jedną z najważniejszych cech Javy jest możliwość
automatycznego zarządzania pamięcią. Ma to zwolnić programistów z obowiązku za
rządzania pamięcią. Java śledzi osierocone obiekty i zwalnia zajmowaną przez nie
pamięć, zwracając ją do puli wolnej pamięci. Odzyskiwanie pamięci w ten sposób
nazywane jest przywracaniem pamięci. Jedną z cech charakterystycznych Javy jest re
guła uniemożliwiająca modyfikowanie referencji. Zasada ta umożliwia Javie wydajne
automatyczne przywracanie pamięci. Programiści wciąż spierają się o to, czy wygo
da wynikająca z tego, że nie trzeba zajmować się zarządzaniem pamięcią, uzasadnia
koszty automatycznego przywracania pamięci.
1.2 a Abstrakcja danych 1 17
N iezm ienność Niezmienny (ang. immutable) typ danych, taki jak Date, cechuje się
tym, że wartość obiektu po jego utworzeniu nigdy się nie zmienia. Z kolei zmienne
typy danych, na przykład Counter lub Accumul ator, manipulują wartościami obiektu
przeznaczonymi do modyfikowania. W Javie do wymuszania niezmienności służy
modyfikator finał. Zadeklarowanie zmiennej przy jego użyciu oznacza, że wartość
zostanie przypisana do niej tylko raz — albo przy inicjowaniu, albo w konstruktorze.
Kod modyfikujący wartość zmiennej z modyfikatorem finał powoduje błąd czasu
kompilacji. W przedstawionym kodzie użyto modyfikatora finał dla zmiennych eg
zemplarza, których wartość nigdy się nie zmienia. To podejście pozwala udokum en
tować, że wartość się nie zmienia, zapobiega przypadkowym modyfikacjom i ułatwia
diagnozowanie programów. Przykładowo, wartości z modyfikatorem finał nie trzeba
dodawać do śladu, ponieważ wiadomo, że jest niezmienna. Typ danych w rodzaju
typu Data, w którym wszystkie zmienne egzemplarza są typu prostego i mają m ody
fikator finał, to typ niezmienny (w kodzie, w którym — tak jak w tej książce — nie
stosuje się dziedziczenia implementacji). Ustalenie, czy typ danych ma być niezm ien
ny, jest ważną decyzją projektową, specyficzną dla aplikacji. Abstrakcja w typach da
nych w rodzaju typu Date ma służyć ukrywaniu wartości, które się nie zmieniają, co
pozwala stosować je w instrukcjach przypisania, jako argumenty i wartości zwracane
przez funkcje w taki sam sposób, jak używa się typów prostych (bez obaw o możli
wość zmiany wartości). Programista implementujący klienta
_ , , , , , ,, j , , , Zm ienne Niezmienne
typu Date może napisać kod d = dO dla dwóch zmiennych
typu Date, podobnie jak dla wartości typu double lub in t. Counter Data
Jednak gdyby typ Date był zmienny, a wartość d zmieniłaby się Tablice Javy S t r i ng
po przypisaniu d = dO, modyfikacji uległaby także wartość dO Przykłady typów
(obie zmienne to referencje do tego samego obiektu)! Z dru- zm iennych i niezmiennych
giej strony, w typach danych w rodzaju Counter i Accumuł a to r
celem abstrakcji jest ukrywanie modyfikowanych wartości. Zetknąłeś się już z tą
różnicą jako programista programów klienckich przy stosowaniu tablic Javy (typ
zmienny) i typu danych S tri ng Javy (typ niezmienny). Przy przekazywaniu wartości
typu S tri ng do m etody nie trzeba martwić się tym, że m etoda zmieni układ znaków
w łańcuchu. M etoda może natomiast zmodyfikować zawartość tablicy. Obiekty typu
String są niezmienne, ponieważ zwykle nie chcemy, aby ich wartość się zmieniała.
Tablice Javy są zmienne, gdyż zazwyczaj chcemy modyfikować ich wartość. W pew
nych sytuacjach przydatne są zmienne łańcuchy znaków (do ich tworzenia służy klasa
S tri ngBui 1der Javy) i niezmienne tablice (tak działa klasa Vector opisana w dalszej
części podrozdziału). Ogólnie typy niezmienne są łatwiejsze w użyciu i trudniej po
pełnić błąd przy ich stosowaniu niż przy korzystaniu z typów zmiennych, ponieważ
zasięg kodu, w którym m ożna modyfikować te pierwsze, jest dużo mniejszy. Łatwiej
jest diagnozować kod, w którym używane są typy niezmienne, ponieważ prościej jest
zagwarantować, że zmienne tego typu w kodzie klienta zachowają spójny stan. Przy
korzystaniu z typów zmiennych zawsze trzeba uważać, gdzie i kiedy modyfikowa
ne są wartości. Wadą niezmienności jest konieczność tworzenia nowego obiektu dla
118 RO ZDZIAŁ 1 B Podstawy
każdej wartości. Koszty te są zwykle akceptowalne, ponieważ mechanizm przywra
cania pamięci w Javie jest przeważnie zoptymalizowany pod kątem tej operacji. Inna
wada niezmienności wynika z tego, że modyfikator final gwarantuje niezmienność
tylko wtedy, kiedy zmienne egzemplarza są typu prostego, a nie referencyjnego. Jeżeli
zmienna egzemplarza typu referencyjnego ma modyfikator final, wartość tej zmien
nej (referencja do obiektu) nigdy się nie zmienia i zawsze prowadzi do tego samego
obiektu, natomiast wartość samego obiektu można zmodyfikować. Przykładowo, po
niższy kod nie jest implementacją typu niezmiennego:
public c la s s Vector
{
private final double[] coords;
public Vector(double[] a)
{ coords = a; }
}
Program kliencki może utworzyć obiekt typu Vector, podając elementy tablicy, a na
stępnie (z pominięciem interfejsu API) zmodyfikować je po utworzeniu:
double[] a = ( 3.0, 4.0 };
Vector vector = new Vector(a);
a [0] = 0.0; // Pominięcie publicznego in t e r fe js u API.
Zmienna egzemplarza coords [] ma modyfikatory pri vate i final, jednak typ Vector
jest zmienny, ponieważ klient przechowuje referencję do danych. Nad niezm iennoś
cią należy zastanowić się przy projektowaniu każdego typu danych. W interfejsie API
należy też określić, czy typ danych jest niezmienny, tak aby programiści klientów
wiedzieli, że wartości obiektu się nie zmieniają. W tej książce niezmienność jest przy
datna głównie do sprawdzania poprawności algorytmów. Na przykład gdyby typ da
nych używany w algorytmie wyszukiwania binarnego był zmienny, działanie klien
tów mogłoby być niezgodne z założeniem, że tablica jest posortowana pod kątem
tego algorytmu.
1.2 □ Abstrakcja danych
Projektowanie kontraktowe Na zakończenie pokrótce omawiamy mechanizmy
javy umożliwiające sprawdzanie założeń na temat program u w czasie jego działania.
Używamy do tego dwóch mechanizmów Javy:
■ Wyjątków, służących ogólnie do obsługi nieprzewidzianych błędów poza kon
trolą programisty.
■ Asercji, które pozwalają sprawdzać założenia poczynione w rozwijanym kodzie.
Używanie wielu wyjątków i asercji to dobry zwyczaj programistyczny. W tej książce
z uwagi na zwięzłość stosujemy je rzadko, jednak m ożna je znaleźć w kodzie dostęp
nym w witrynie. Kod ten jest zgodny z bogatymi komentarzami na tem at algoryt
mów, dotyczącymi wyjątkowych warunków i zakładanych niezmienników.
Wyjątki i błędy Wyjątki i błędy to zakłócające pracę zdarzenia zachodzące w trak
cie działania programu, często sygnalizujące usterkę. Podejmowane działanie to tak
zwane zgłoszenie wyjątku lub zgłoszenie błędu. W omówieniu podstawowych m e
chanizmów Javy przedstawiono już wyjątki zgłaszane przez metody systemowe Javy,
takie jak: StackOverflowError, ArithmeticException, ArraylndexOutOfBoundsExcept
ion, OutOfMemoryError i Nul 1PointerException. Można też tworzyć własne wyjątki.
Najprostszy ich typ to Runti meExcepti on. Wyjątki tego rodzaju kończą działanie pro
gramu i powodują wyświetlenie kom unikatu o błędzie:
throw new RuntimeException("Tu komunikat o b łę d z ie . ") ;
Zgodnie z ogólnym podejściem o nazwie programowanie z szybkim przerywaniem
działania (ang. fail fast programming) błędy można łatwiej zlokalizować, jeśli program
zgłasza wyjątek bezpośrednio po wykryciu usterki (przeciwna technika polega na igno
rowaniu błędu i odraczaniu zgłaszania wyjątku do pewnego momentu w przyszłości).
Asercje Asercja to wyrażenie logiczne, które w danym miejscu programu powin
no mieć wartość true. Jeśli wyrażenie ma wartość fal se, program kończy działanie
i zgłasza komunikat o błędzie. Asercje służą zarówno do potwierdzania poprawno
ści programu, jak i do dokumentowania jego przeznaczenia. Załóżmy na przykład,
że program oblicza wartość używaną jako indeks tablicy. Jeśli wartość jest ujemna,
może później spowodować wyjątek ArraylndexOutOfBoundsException. Jednak kod
assert index >= 0; pozwala zlokalizować miejsce wystąpienia błędu. Ponadto m oż
na dodać opcjonalny, szczegółowy komunikat, na przykład:
assert index >= 0 : "Ujemny indeks w metodzie X. " ;
Pomaga to zlokalizować błąd. Asercje domyślnie są wyłączone. Można włączyć je
w wierszu poleceń, używając flagi-enabl e asserti ons (skrócony zapis to-ea). Asercje
służą do diagnozowania. Nie należy opierać działania program u na asercjach, ponie
waż mogą zostać wyłączone. W czasie kursu z programowania systemów nauczysz
się stosować asercje do zapewniania, że kod nigdy nie zakończy działania zgłosze
niem błędu systemowego lub wejściem w pętlę nieskończoną. Podejściu temu odpo
wiada jeden z modeli programowania — projektowanie kontraktowe. Projektant typu
120 R O Z D Z IA L I ■ Podstawy
danych określa warunek wstępny (warunek, który klient musi spełniać w momencie
wywołania metody), warunek końcowy (implementacja gwarantuje jego spełnienie
po zwróceniu sterowania z metody) i efekty uboczne (inne zmiany stanu, które m e
toda może powodować). W czasie programowania warunki te m ożna sprawdzać za
pomocą asercji.
Podsum ow anie Mechanizmy języka opisane w tym podrozdziale pokazują, że pro
jektowanie efektywnych typów danych związane jest z niebanalnymi problemami,
które niełatwo jest rozwiązać. Eksperci wciąż dyskutują na temat najlepszych spo
sobów radzenia sobie z pewnymi omówionymi tu zagadnieniami projektowymi.
Dlaczego Java nie dopuszcza stosowania funkcji jako argumentów? Dlaczego Matlab
kopiuje tablice przekazywane jako argumenty do funkcji? Na początku r o z d z i a ł u i .
wspomniano, że narzekanie na mechanizmy języka programowania często prowadzi
do wejścia na trudną drogę projektowania języków programowania. Jeśli nie planujesz
tego robić, najlepszym podejściem jest stosowanie popularnych języków. Większość
systemów udostępnia bogate biblioteld, z których, oczywiście, należy korzystać w od
powiednich sytuacjach. Często jednak m ożna uprościć kod klientów i zabezpieczyć
się, budując abstrakcje, które można łatwo przenieść do innych języków. Głównym
celem jest utworzenie typów danych w taki sposób, aby większość zadań można było
wykonać na poziomie abstrakcji odpowiednim do rozwiązywanego problemu.
Tabela na następnej stronie zawiera podsumowanie różnych rodzajów omówio
nych klas Javy.
1.2 h Abstrakcja danych 121
Rodzaj klasy Przykłady Cechy
Metody statyczne Math Stdln StdOut Brak zmiennych egzemplarza
Wszystkie zmienne egzemplarza
mają modyfikator pri vate
Wszystkie zmienne egzemplarza
Niezmienne
Date Tran sact ion mają modyfikator finał
abstrakcyjne
S t r i n g Integer
typy danych Kopiowanie zabezpieczające
typów referencyjnych
Uwaga: cechy te są konieczne,
ale niewystarczające
Wszystkie zmienne egzemplarza
Zmienne
mają modyfikator pri vate
abstrakcyjne Counter Accumulator
Nie wszystkie zmienne egzemplarza
typy danych
mają modyfikator finał
Abstrakcyjne Wszystkie zmienne egzemplarza
typy danych V isua l Accumulator mają modyfikator pri vate
z efektami ubocznymi In Out Draw Metody egzemplarza wykonują
dla wejścia-wyjścia operacje wejścia-wyjścia
Klasy Javy (implementacje typów danych)
122 R O Z D Z IA L I ■ Podstawy
| Pytania i odpowiedzi
P. Po co stosować abstrakcję danych?
O. Ponieważ pomaga tworzyć niezawodny i poprawny kod. Na przykład w wyborach
prezydenckich w 2000 roku Al Gore otrzymał -16 022 głosy według elektronicznej
maszyny do głosowania w hrabstwie Volusia na Florydzie. Licznik najwyraźniej nie
był poprawnie zahermetyzowany w oprogramowaniu maszyny!
P. Po co stosować podział na typy proste i referencyjne? Czy nie lepiej używać sa
mych typów referencyjnych?
O. Ważna jest wydajność. Java udostępnia odpowiadające typom prostym typy re
ferencyjne Integer, Doubl e itd. Mogą z nich korzystać programiści, którzy chcą zig
norować wspomniany podział. Typy proste są bliższe typom danych obsługiwanym
przez sprzęt komputera, dlatego używające ich programy zwykle działają szybciej niż
programy, w których wykorzystano powiązane typy referencyjne.
P. Czy typy danych muszą być abstrakcyjne?
O. Nie. Java udostępnia modyfikatory pub! i c i protected, umożliwiające niektórym
klientom bezpośrednie wskazywanie zmiennych egzemplarza. Jak opisano w tekście,
zalety płynące z zapewnienia klientom bezpośredniego dostępu do danych są znacz
nie mniejsze niż wady związane z zależnością od konkretnej reprezentacji. Dlatego
w pisanym przez nas kodzie wszystkie zmienne egzemplarza mają modyfikator pri -
vate. Ponadto w niektórych miejscach zastosowano metody egzemplarza z takim
modyfikatorem (metody publiczne współużytkują ich kod).
P. Co się stanie, jeśli zapomnę użyć słowa new przy tworzeniu obiektu?
O. Java potraktuje to tak, jakbyś chciał wywołać metodę statyczną, która zwraca
wartość o typie danego obiektu. Ponieważ nie zdefiniowano takiej metody, kom uni
kat o błędzie będzie taki sam, jak przy każdym użyciu niezdefiniowanego symbolu.
Próba kompilacji poniższego kodu:
Counter c = C o u n te r(" te st");
powoduje wyświetlenie kom unikatu o błędzie:
cannot find symbol
symbol : method Counter(String)
Ten sam komunikat o błędzie pojawi się po podaniu złej liczby argumentów w kon
struktorze.
1.2 ■ Abstrakcja danych 123
P. Co się stanie, kiedy zapomnę użyć słowa new przy tworzeniu tablicy obiektów?
O. Słowo new trzeba podać przy tworzeniu każdego obiektu, dlatego tworząc tablicę
N obiektów, należy użyć go N +1 razy — raz dla tablicy i po jednym razie dla każdego
obiektu. Jeśli zapomnisz utworzyć tablicę:
CounterJ] a;
a [0] = new C o u n t e r ( " t e s t " ) ;
zobaczysz ten sam kom unikat o błędzie, co przy próbie przypisania wartości do nie-
zainicjowanej zmiennej:
v a riab le a might not have been i n i t i a l i z e d
a [0] = new C o u n t e r ( " t e s t " ) ;
/\
Jeżeli jednak zapomnisz słowa new przy tworzeniu obiektu w tablicy, a następnie
spróbujesz użyć obiektu do wywołania metody:
CounterJ] a = new Counter[2];
a [0] .in c re m e n t o ;
zgłoszony zostanie wyjątek Nul 1Poi nterExcepti on.
P. Dlaczego nie należy pisać instrukcji StdOut .pri ntl n (x .to S trin g O ) do wyświet
lania obiektów?
O. Ten kod działa poprawnie, jednak Java pozwala pom inąć jego fragment, gdyż au
tomatycznie wywołuje metodę to S tri ng () dla każdego obiektu, ponieważ pri ntl n ()
ma wersję przyjmującą argument typu Object.
P. Czym jest wskaźniki
O. Dobre pytanie. Podany wcześniej wyjątek, Nul 1 Poi nterExcepti on (czyli wyjątek
pustego wskaźnika), powinien nosić nazwę NullReferenceException (czyli wyją
tek pustej referencji). Wskaźnik, podobnie jak referencję Javy, m ożna traktować jak
adres w pamięci. W wielu językach programowania wskaźnik to prosty typ danych,
którym programiści mogą manipulować na wiele sposobów. Jednak programowanie
z wykorzystaniem wskaźników jest narażone na błędy, dlatego operacje na wskaź
nikach trzeba starannie projektować, aby pom óc program istom uniknąć błędów.
W Javie podejście to zastosowano w skrajnym stopniu (jest to rozwiązanie prefero
wane przez wielu współczesnych projektantów języków programowania). Istnieje tu
tylko jeden sposób na utworzenie referencji (new) i tylko jeden sposób na jej zm o
dyfikowanie (za pom ocą instrukcji przypisania). Oznacza to, że jedyną rzeczą, jaką
programista może zrobić z referencją, jest jej utworzenie i skopiowanie. W żargo
nie związanym z językami programowania referencje Javy to tak zwane bezpieczne
124 R O Z D Z IA L I a Podstawy
Pytania i odpowiedzi (ciąg dalszy)
wskaźniki, ponieważ Java gwarantuje, że każda referencja prowadzi do obiektu okre
ślonego typu (a także potrafi określić — na potrzeby przywracania pamięci — które
obiekty nie są używane). Programiści przyzwyczajeni do pisania kodu, który bez
pośrednio m anipuluje wskaźnikami, uważają, że Java w ogóle nie posiada wskaźni
ków, jednak cały czas trwają dyskusje, czy stosowanie niebezpiecznych wskaźników
w ogóle jest pożądane.
P. Gdzie można znaleźć więcej informacji o tym, w jaki sposób w Javie zaimplemen
towane są referencje i jak język obsługuje przywracanie pamięci?
O. Jeden system Javy może zupełnie różnić się od drugiego. Przykładowo, natural
nym rozwiązaniem jest używanie wskaźników (adresów w pamięci) lub uchwytów
(wskaźników do wskaźników). Pierwsze podejście zapewnia szybszy dostęp do da
nych; drugie — lepsze przywracanie pamięci.
P. Co dokładnie daje importowanie nazwy?
O. Niewiele — pozwala zaoszczędzić pisania. Zamiast używać instrukcji import,
można na przykład wszędzie używać nazwy ja v a .u til .Arrays w miejsce nazwy
Arrays.
P. Jakie problemy powoduje dziedziczenie implementacji?
O. Tworzenie podklas utrudnia programowanie m odularne z dwóch powodów. Po
pierwsze, każda zmiana w nadklasie wpływa na wszystkie podklasy. Podklasy nie
można rozwijać niezależnie od nadklasy. Podklasa jest całkowicie zależna od nadlda-
sy. Jest to tak zwany problem wrażliwej klasy bazowej. Po drugie, kod podklasy ma
dostęp do zmiennych egzemplarza, dlatego może być niezgodny z intencjami autora
kodu nadklasy. Przykładowo, projektant klasy Counter dla systemu do obsługi gło
sowania może włożyć dużo pracy w to, aby w klasie Counter można było zwiększać
licznik tylko o jeden (przypomnijmy problem Ala Gorea). Jednak podklasa, mająca
pełny dostęp do zmiennych egzemplarza, może ustawić dowolną wartość licznika.
P. Jak sprawić, aby klasa była niezmienna?
O. W celu zapewnienia niezmienności typu danych, który obejmuje zmienną eg
zemplarza zmiennego typu, trzeba utworzyć lokalną kopię, tak zwaną kopię zabezpie
czającą. Nawet to może nie wystarczyć. Utworzenie kopii to jeden problem; innym
jest zagwarantowanie, że żadna z metod egzemplarza nie zmienia wartości.
P. Czym jest nul 1?
1.2 □ Abstrakcja danych 1 25
O. Jest to literał oznaczający brak obiektu. Wywołanie m etody przy użyciu referen
cji nul l nie m a sensu i prowadzi do zgłoszenia wyjątku Nul l Poi nterExcepti on. Jeśli
napotkasz taki komunikat o błędzie, upewnij się, czy konstruktor poprawnie inicjuje
wszystkie zmienne egzemplarza.
P. Czy w ldasie z implementacją typu danych można umieścić metodę statyczną?
O. Oczywiście. Na przyMad we wszystMch pisanych przez nas Masach znajduje się
metoda mai n ( ) . Ponadto warto rozważyć dodanie m etod statycznych dla operacji na
wielu obiektach, lciedy żaden z nich nie jest w naturalny sposób tym, który powinien
wywoływać tę metodę. PrzyMadowo, w Masie Poi nt można zdefiniować metodę sta
tyczną podobną do tej:
public s t a t ic double distance(Point a, Point b)
{
return a .distT o(b );
}
Dołączenie takiej m etody często pozwala zwiększyć przejrzystość kodu Mienta.
P. Czy istnieją inne rodzaje zmiennych oprócz zmiennych dla parametrów, lokal
nych i egzemplarza?
O. Jeśli zastosujesz słowo Muczowe s ta ti c w deldaracji Masy (poza typami), powsta
nie zupełnie odm ienny rodzaj zmiennej — zmienna statyczna. Zmienne statyczne,
podobnie jak zmienne egzemplarza, są dostępne w każdej metodzie z Masy, jednak
nie są powiązane z żadnym obiektem. W starszych językach programowania nazy
wano taMe zmienne globalnymi z uwagi na ich globalny zasięg. We współczesnym
programowaniu ważne jest ograniczanie zasięgu, dlatego z taMch zmiennych korzy
sta się rzadko. W miejscach, w których takie zmienne są potrzebne, zwracamy na nie
uwagę.
P. Czym jest przestarzała metoda?
O. Jest to metoda, która nie jest w pełni obsługiwana, ale zachowano ją w interfejsie
API w celu zapewnienia zgodności. Java zawierała Medyś metodę C haracter. i sSpa-
ce(), a programiści pisali całe programy, wykorzystując działanie tej metody. Kiedy
programiści Javy chcieli później dodać obsługę innych białych znaków z kodowania
Unicode, nie mogli zmienić działania m etody i sSpace(), nie uszkadzając przy tym
programów Mientów, dlatego zamiast tego dodali nową metodę, C h ara [Link]-
teSpace(), a dawną uznali za przestarzałą. Z czasem podejście to, oczywiście, kom
plikuje interfejsy API. Nieraz za przestarzałe zostają uznane całe Masy. PrzyMadowo,
w Javie uznano za przestarzałą Masę ja v a .u til .Date, aby zapewnić lepszą obsługę
umiędzynarodowiania.
R O ZD ZIA Ł 1 ■ Podstawy
| ĆWICZENIA
1.2.1. Napisz klienta typu Poi nt2D. Klient ma pobierać z wiersza poleceń liczbę cał
kowitą N, generować N losowych punktów w jednostce kwadratowej i obliczać odle
głość między parę najbliższych punktów.
1.2.2. Napisz klienta typu Interval ID. Klient ma pobierać z wiersza poleceń war
tość N typu i nt, wczytywać ze standardowego wejścia N przedziałów (każdy zdefi
niowany za pom ocą pary wartości typu doubl e) i wyświetlać wszystkie pary mające
część wspólną.
1.2.3. Napisz klienta typu Interval 2D. Klient ma pobierać z wiersza poleceń argu
m enty N, mi n i max oraz generować N losowych dwuwymiarowych przedziałów, któ
rych wysokość i szerokość podzielono na równe fragmenty między mi n i max w jed
nostce kwadratowej. Narysuj przedziały na StdDraw i wyświetl liczbę par przedzia
łów mających część wspólną oraz liczbę par przedziałów, z których jeden zawiera się
w drugim.
1.2.4. Co wyświetla poniższy fragment kodu?
S t r in g s t r i n g l = "w it a j";
S t r in g s trin g 2 = s t r i n g l ;
s t r i n g l = "św ie cie ";
S td O u t.p rin tln (strin g l);
Std 0 u t.p rin tln (strin g 2 );
1.2.5. Co wyświetla poniższy fragment kodu?
S t r in g s = "Witaj, świecie";
[Link]();
s .s u b s t r in g (7 , 14);
S t d O u t . p r in t ln ( s ) ;
Odpowiedź: "W itaj, świecie". Obiekty typu S tring są niezmienne. Jego metody
zwracają nowy obiekt typu S tring o odpowiedniej wartości, jednak nie zmieniają
wartości obiektu, dla którego je wywołano. Powyższy kod pomija zwrócone obiekty
i wyświetla pierwotny łańcuch znaków. Aby wyświetlić słowo "ŚWIECIE", należy użyć
instrukcji s = [Link]() i s = [Link] b s trin g (7 , 14).
1.2.6. Łańcuch znaków s jest przesunięciem cyklicznym (ang. circular rotation) łań
cucha t, jeśli pasuje do niego po cyklicznym przestawieniu znaków o dowolną liczbę
pozycji. Na przykład ACTGACGto przesunięcie cykliczne łańcucha TGACGAC i na odwrót.
1.2 o Abstrakcja danych 127
Wykrycie tego warunku jest ważne w badaniach nad sekwencjami genomu. Napisz
program, który sprawdza, czy dwa łańcuchy znaków s i t są dla siebie przesunięciem
cyklicznym. Wskazówka: rozwiązaniem jest jeden wiersz z m etodam i indexOf(),
1 ength () i łączeniem łańcuchów znaków.
1.2.7. Co zwraca poniższa funkcja rekurencyjna?
public s t a t i c S tring m ystery(String s)
{
in t N = s . le n g t h ();
i f (N <= 1) return s;
S tring a = s .su b strin g (0 , N/2);
S tring b = [Link] b strin g (N /2 , N ) ;
retu rn mystery(b) + m ystery(a);
}
1.2.8. Załóżmy, że a [] i b [] to tablice łańcuchów znaków składające się z milionów
liczb całkowitych. Jak działa poniższy kod? Czy jego wydajność jest zadowalająca?
in t[] t = a; a = b; b = t ;
Odpowiedź: kod zamienia zawartość tablic. Jest maksymalnie wydajny, ponieważ robi
to przez kopiowanie referencji, dlatego nie trzeba kopiować milionów elementów.
1.2.9. Zmodyfikuj program Bi narySearch (strona 59), tak aby używał klasy Counter
do zliczania kluczy sprawdzanych we wszystkich wyszukiwaniach i wyświetlał liczbę
kluczy po zakończeniu poszukiwań. Wskazówka: utwórz obiekt klasy Counter w m e
todzie mai n () i przekaż go jako argument do metody rank ().
1.2.10 Utwórz klasę Vi sual Counter z obsługą inkrementacji i dekrementacji.
Konstruktor m a przyjmować dwa argumenty — Ni max. Nto maksymalna liczba ope
racji, a max to maksymalna wartość bezwzględna licznika. Jako efekt uboczny obiekt
ma generować rysunek z wartością licznika po każdej jej zmianie.
1.2.11. Napisz implementację typu SmartDate na podstawie interfejsu API typu
Date. Implementacja ma zgłaszać wyjątek, jeśli data jest nieprawidłowa.
1.2.12. Dodaj do typu SmartDate metodę dayOfTheWeek(), zwracającą wartość typu
String z odpowiednim dniem tygodnia (Poniedziałek, Wtorek, Środa, Czwartek,
Piątek, Sobota, N iedziela) dla danej daty. Możesz przyjąć, że data pochodzi z XXI
wieku.
128 R O Z D Z IA L I n Podstawy
ĆWICZENIA (ciąg dalszy)
1.2.13. Napisz implementację typu Transact i on, biorąc za model opracowaną przez
nas implementację typu Date (strona 103).
1.2.14. Napisz implementację metody equals() dla typu Transaction, biorąc za
model opracowaną przez nas implementację metody equals() dla typu Date (stro
na 115).
1.2 a Abstrakcja danych 129
[I PROBLEMY DO ROZWIĄZANIA
1.2.15* Dane wejściowe z pliku. Napisz implementację m etody statycznej readl nts ()
z biblioteki I n (metody tej używamy w różnych klientach testowych, na przykład do
wyszukiwania binarnego na stronie 59). Implementacja ma być oparta na metodzie
s p lit( ) typu String.
Rozwiązanie-.
public s t a t i c i n t [] re ad In ts(S trin g name)
{
In in = new In(name);
S trin g input = S td ln .rea d A ll();
S t r i n g Q words = [Link] i t ( " \ \ s + " ) ;
i n t [] in t s = new int[w [Link];
f o r in t i = 0 ; i < [Link]; i++)
i n t s [ i ] = In t e g e r. p a rs e In t( w o rd s [i]);
return in t s ;
}
Inną implementację omówiono w p o d r o z d z ia l e 1.3 (zobacz stronę 138).
1.2.16. Liczby wymierne. Zaimplementuj niezmienny typ danych Rational dla liczb
wymiernych. Typ ma obsługiwać dodawanie, odejmowanie, mnożenie i dzielenie.
p ub lic c la s s Rational
R a t io n a l( in t numerator, in t denominator)
Rational p lu s(R a tio n a l b) Suma danej liczby i b
Rational m inus(R ational b) Różnica między daną liczbą i b
Rational tim es(R ationa l b) Iloczyn danej liczby i b
Rational d iv id e s(R a tio n a l b) Iloraz danej liczby i b
boolean equals (R ational that) Czy dana liczba jest równa that?
S t r in g t o S t r in g ( ) Reprezentacja w postaci łańcucha znaków
Nie przejmuj się sprawdzaniem przepełnienia (zobacz ć w i c z e n i e 1 .2 . 1 7 ), jednak
jako zmienne egzemplarza zastosuj dwie wartości typu 1 ong reprezentujące licznik
i mianownik. Zmniejsza to prawdopodobieństwo przepełnienia. Użyj algorytmu
Euklidesa (zobacz stronę 16), aby zagwarantować, że licznik i mianownik nie mają
wspólnego dzielnika. Dodaj klienta testowego sprawdzającego wszystkie metody.
130 R O Z D Z IA L I □ Podstawy
PROBLEMY DO ROZW IĄZANIA (ciąg dalszy)
1.2.17. Odporna na błędy implementacja liczb wymiernych. Zastosuj asercje do
opracowania implementacji typu Rational (zobacz ć w i c z e n i e 1 .2 . 1 6 ) odpornej na
przepełnienie.
1.2.18. Wariancja dla akumulatora. Sprawdź poprawność poniższego kodu, w któ
rym do klasy Accumulator dodano m etody var() i stddev(), obliczające wariancję
oraz średnią dla liczb podanych jako argumenty m etody addDataVal ue():
public c la s s Accumulator
{
private double m;
private double s;
private in t N;
public void addDataValue(double x)
{
N++;
s = s + 1.0 * (N -l) / N * (x - m) * (x - m);
m = m + (x - m) / N;
1
public double mean()
{ return m; }
public double var()
{ return s/(N - 1); }
public double stddevQ
{ return M a t h . s q r t ( t h i s . v a r ( ) ) ; }
}
Ta implementacja jest w mniejszym stopniu narażona na błędy przy zaokrąglaniu niż
prosta implementacja oparta na zapisywaniu sumy kwadratów liczb.
1.2 e Abstrakcja danych 131
1.2.19. Parsowanie. Napisz konstruktory z przetwarzaniem (ang. parse constructor)
dla implementacji typów Date i Transaction z ć w i c z e n i a 1 .2 .1 3 . Konstruktory mają
przyjmować jeden argument typu S t r i ng określający wartości używane do inicjowa
nia typów. Wykorzystaj formaty przedstawione w tabeli.
Częściowe rozwiązanie:
public Date(String date)
{
S t r in g [ ] fields = [Link] i t ( " / " ) ;
month = In te g er.p arse ln t(fields[0 ]) ;
day = [Link](fiel d s [1] ) ;
year = In t e g e r.p a rs e ln t(fie ld s [2 ]);
}
Typ Format Przykład
Liczby całkowite , ,
Date • 5/22/1939
oddzielone ukośnikami
Klient, data i wartość , ,
T ran sa ction Tu rin g 5/22/1939 11.99
rozdzielone odstępami
F o r m a ty u ż y w a n e p rz y p r z e tw a r z a n iu
1.3. W IE LO Z B IO R Y , K O L E JK I I S T O S Y
k il k a po d st aw o w yc h typó w przechowuje kolekcje obiektów. Kolekcja
d a n yc h
obiektów jest grupą wartości, a operacje dotyczą tu dodawania, usuwania lub spraw
dzania obiektów w kolekcji. W tym podrozdziale omawiamy trzy typy danych tego
rodzaju — wielozbiory, kolejki i stosy. Różnią się one tym, który obiekt ma być usu
wany lub sprawdzany jako następny.
Wielozbiory, kolejki i stosy to podstawowe typy o wielu zastosowaniach. Używamy
ich w implementacjach w całej książce. Ponadto kod klienta i implementacji typów
z tego podrozdziału stanowi wprowadzenie do ogólnego, stosowanego przez nas spo
sobu rozwijania struktur danych i algorytmów.
Jednym z celów w tym podrozdziale jest podkreślenie faktu, że sposób reprezen
towania obiektów w kolekcji bezpośrednio wpływa na wydajność różnych operacji.
Dla kolekcji projektujemy struktury danych reprezentujące grupy obiektów i umożli
wiające wydajne zaimplementowanie potrzebnych operacji.
Drugim celem jest przedstawienie typów generycznych i iteracji — podstawowych
elementów Javy, pozwalających znacznie uprościć kod klienta. Są to zaawansowane
mechanizmy języka programowania, które nie są niezbędne do zrozumienia algoryt
mów, natomiast pozwalają tworzyć kod klienta (i implementacje algorytmów) w bar
dziej przejrzysty, zwięzły i elegancki sposób.
Trzecim celem w podrozdziale jest wprowadzenie powiązanych struktur danych
i pokazanie ich znaczenia. Klasyczna struktura danych, lista powiązana, pozwala za
implementować wielozbiory, kolejki i stosy w bardzo wydajny sposób, nieosiągalny
innymi metodami. Zrozumienie list powiązanych to kluczowy pierwszy krok na dro
dze do poznawania algorytmów i struktur danych.
Dla każdego z trzech wymienionych typów omawiamy interfejsy API i przykła
dowe programy klienckie, a następnie analizujemy możliwe reprezentacje wartości
typu danych oraz implementacje operacji typu. Scenariusz ten (w kontekście bardziej
skomplikowanych struktur danych) powtarza się w książce. Implementacje z tego
miejsca są modelem dla późniejszych implementacji, dlatego warto je starannie prze
studiować.
132
-
1.3 * Wielozbiory, kolejki i stosy 133
Interfejsy API Analizy abstrakcyjnych typów danych rozpoczynamy, jak zwykle,
od zdefiniowania interfejsów API, które przedstawiono poniżej. Każdy typ obejmu
je konstruktor nieprzyjmujący argumentów, metodę do dodawania elementów do
kolekcji, metodę do sprawdzania, czy kolekcja jest pusta, i metodę zwracającą roz
miar kolekcji. Typy Stack i Queue mają m etody do usuwania określonego elementu
z kolekcji. Oprócz tych podstawowych elementów interfejsy API obejmują dwa m e
chanizmy Javy opisane na kilku kolejnych stronach: typy generyczne i kolekcje z m oż
liwością iterowania.
W ie lo zb ió r
p u b lic c la s s Bag<Item> implements Ite rable<Ite m >
Bag () Tworzenie pustego wielozbioru
void add(Item item) Dodawanie elementu
boolean isEm ptyO Czy wielozbiór jest pusty?
in t s iz e ( ) Liczba elementów w wielozbiorze
K olejka FIFO
p u b lic c la s s Queue<Item> implements Iterable<Item >
Queued Tworzenie pustej kolejki
void enqueue (Item item) Dodawanie elementu
Item dequeued Usuwanie elementu dodanego najdawniej
boolean isEm ptyO Czy, kolejka jest pusta?
in t s i z e d Liczba elementów w kolejce
S to s (k o le jk a LIFO)
p u b lic c la s s Stack<Item > implements Ite rable<Ite m >
Stack() Tworzenie pustego stosu
void push(Item item) Dodawanie elementu
Item pop() Usuwanie ostatnio dodanego elementu
boolean isEm ptyO Czy stos jest pusty?
in t s i z e d Liczba elementów na stosie
In te rf e js y API p o d s ta w o w y c h g e n e r y c z n y c h k o le k c ji z m o ż liw o ś c ią ¡te ro w a n ia
134 R O Z D Z IA L I * Podstawy
Typy generyczne Kluczową cechą typów ADT dla kolekcji jest to, że możliwe po
winno być używanie ich dla dowolnego typu danych. Umożliwia to specyficzny m e
chanizm Javy — typy generyczne (inaczej typy sparametryzowane). Wpływ typów ge
nerycznych na język programowania jest na tyle duży, że w wielu językach (także we
wczesnych wersjach Javy) typy te nie występują. Jednak sposób, w jaki je stosujemy,
wymaga tylko niewielkiej ilości dodatkowej składni Javy i jest łatwy do zrozumienia.
Zapis <Item> po nazwie klasy w każdym interfejsie API określa, że nazwa Item to pa
rametr typu. Jest to symboliczny zastępnik, za który można podstawić konkretny typ
używany w kliencie. Kod Stack<Item> można przeczytać jako „stos elementów”. Przy
implementowaniu typu Stack konkretny typ podstawiany za Item nie jest znany, jed
nak w kliencie można użyć stosu dla dowolnego typu danych, w tym dla typów napi
sanych długo po opracowaniu implementacji stosu. Kod klienta określa konkretny typ
w momencie tworzenia stosu. Można zastąpić Item nazwą dowolnego typu referencyj
nego (należy zrobić to konsekwentnie, w miejscu każdego wystąpienia nazwy Item).
Jest to dokładnie to, czego potrzebujemy. Można na przykład napisać taki kod:
Stack<String> stack = new S ta c k < S t r in g > ( ) ;
stack.p ush("T est");
String next = s ta c k . pop ();
aby użyć stosu dla obiektów typu S tri ng. Poniższy kod:
Queue<Date> queue = new Queue<Date>();
[Link](new Date(12, 31, 1999));
Date next = [Link]();
powoduje użycie kolejki dla obiektów Date. Próba dodania obiektu typu Date (lub da
nych dowolnego typu różnego od String) do stack lub obiektu typu String (lub da
nych dowolnego typu różnego od Date) do queue powoduje błąd czasu kompilacji.
Bez typów generycznych konieczne byłoby definiowanie (i implementowanie) róż
nych interfejsów API dla każdego typu danych, który trzeba umieszczać w kolekcji.
Typy generyczne pozwalają zastosować jeden interfejs API (i jedną implementację)
dla wszystkich typów danych — nawet dla typów implementowanych w przyszłości.
Jak się wkrótce okaże, typy generyczne prowadzą do tworzenia przejrzystego kodu
klienta. Kod ten jest łatwy do zrozumienia i w diagnozowaniu, dlatego używamy ta
kich typów w książce.
A utoboxing Jako param etr typu trzeba podać typ referencyjny, dlatego Java udostęp
nia specjalny mechanizm, umożliwiający stosowanie generycznego kodu dla typów
prostych. Przypomnijmy, że typy nakładkowe Javy to typy referencyjne odpowiada
jące typom prostym. Typy Boolean, Byte, Character, Double, Float, Integer, Long
i Short odpowiadają typom bool ean, byte, char, doubl e, float, i nt, 1ong i short. Java
automatycznie dokonuje konwersji między wymienionymi typami referencyjnymi
1.3 o Wielozbiory, kolejki i stosy 135
a powiązanymi typami prostymi w przypisaniach, argumentach m etod i wyrażeniach
arytmetycznych oraz logicznych. W kontekście omawianych zagadnień konwersja
jest pomocna, ponieważ umożliwia stosowanie generycznego kodu dla typów pro
stych, tak jak poniżej:
Stack<Integer> stack = new S ta c k < In te g e r> ();
[Link](17) ; // Autoboxing (in t -> I n t e g e r ) .
in t i = [Link] (); / / Autounboxing (Integer -> in t ) .
Automatyczne rzutowanie z typu prostego na nakładkowy to tak zwany autoboxing, a au
tomatyczne rzutowanie z typu nakładkowego na prosty to autounboxing. W przykładzie
Java automatycznie rzutuje (autoboxing) wartość typu prostego 17 na typ Integer przy
przekazywaniu jej do metody push(). Metoda pop ( ) zwraca wartość typu Integer, którą
Java przed przypisaniem do zmiennej i rzutuje (autounboxing) na typ i nt.
Kolekcje z możliwością iterowania W wielu aplikacjach klient musi jedynie prze
tworzyć wszystkie elementy, iterując (czyli przechodząc) po elementach kolekcji.
Technika ta jest tak ważna, że stanowi jeden z podstawowych elementów Javy i wielu
innych współczesnych języków (sam język programowania posiada mechanizm ob
sługi tej techniki — nie służą do tego biblioteki). Iterowanie pozwala pisać przejrzysty
i zwięzły kod, wolny od zależności od szczegółów implementacji kolekcji. Załóżmy
na przykład, że klient przechowuje kolekcję transakcji w obiekcie Queue:
Queue<Transaction> co lle c tio n = new Queue<Transaction>();
Jeśli kolekcja umożliwia iterowanie, w kliencie można wyświetlić listę transakcji za
pomocą jednej instrukcji:
fo r (Transaction t : co lle c tio n )
( S td O u t.p rin tln (t) ; }
Technika ta jest też nazywana instrukcją foreach. Instrukcję fo r można czytać tak:
dla każdej transakcji t z kolekcji wykonaj następujący blok kodu. Kod klienta nie musi
znać reprezentacji ani implementacji kolekcji. Musi jedynie przetworzyć każdy z jej
elementów. Ta sama pętla fo r zadziała dla kolekcji Bag z transakcjami lub dowolnej
innej kolekcji z możliwością iterowania. Trudno wyobrazić sobie bardziej przejrzysty
i zwięzły kod klienta. Jak się okaże, zapewnienie obsługi tego mechanizmu wymaga
dodatkowej pracy przy implementowaniu, jednak efekt jest tego wart.
w arto zau w ażyć , że jedyne różnice między interfejsami API typów Stack i Queue to
ich nazwy oraz nazwy metod. To spostrzeżenie dowodzi, że nie można łatwo określić
wszystkich cech typu danych na liście sygnatur metod. Tu rzeczywista specyfikacja
obejmuje opisy w języku polskim, określające reguły wybierania elementu — usu
wanego lub przetwarzanego w instrukcji foreach. Różnice między tymi regułami są
znaczące, stanowią część interfejsu API i, oczywiście, mają kluczowe znaczenie przy
rozwijaniu kodu klienta.
136 R O Z D Z IA L I ■ Podstawy
W ielozbiory Wielozbiór to kolekcja, która nie obsługuje usuwania elementów. Jej
funkcją jest umożliwienie klientom zapisywania elementów i przechodzenia po nich.
W kliencie można też sprawdzić, czy wielozbiór jest pusty, oraz określić liczbę ele
mentów. Kolejność iterowania jest nieokreślona i nie powinna mieć dla klienta zna
czenia. Aby docenić tę kolekcję, wyobraźmy sobie zbieracza szklanych kulek, który
umieszcza kulki po jednej w wielozbiorze i od czasu do czasu sprawdza wszystkie
kulki, szukając jednej, mającej określone cechy. Za
Wielozbiór
pomocą przedstawionego interfejsu API typu Bag z kulkami
klient może dodawać elementy do wielozbioru
i w odpowiednim momencie przetwarzać je wszyst
kie za pom ocą instrukcji foreach. W takim kliencie
można użyć stosu lub kolejki, jednak jednym ze spo
sobów na podkreślenie, że kolejność przetwarzania
elementów nie ma znaczenia, jest zastosowanie typu ad d (#)
Bag. Klasa S ta ts ilustruje typowego klienta typu Bag.
Zadanie polega na obliczeniu średniej i odchylenia
standardowego dla wartości typu doubl e ze standar
dowego wejścia. Jeśli w standardowym wejściu jest
N liczb, ich średnią należy obliczyć, dodając liczby
do siebie i dzieląc je przez N. Odchylenie standardo
we obliczane jest przez dodanie kwadratów różnic add( )
między każdą liczbą a średnią, podzielenie wyniku
przez N - 1 i obliczenie pierwiastka kwadratowego
z rezultatu. Kolejność sprawdzania liczb nie jest
istotna w żadnej z tych operacji, dlatego zapisujemy
wartości w kolekcji Bag i używamy techniki fo re
ach do obliczenia każdej sumy. Uwaga: odchyle f o r (Marbłe m : bag)
nie standardowe można obliczyć bez zapisywania
@ • • • •
wszystkich liczb (tak jak przy obliczaniu średniej
w typie Accumulator — zobacz ć w i c z e n i e 1 .2 . 1 8 ).
Zapisanie wszystkich wartości w kolekcji Bag jest Przetwarzanie każdej kulki m
(w dowolnej kolejności)
jednak konieczne przy obliczaniu bardziej skompli
O p e ra c je n a w ie lo z b io rz e
kowanych statystyk.
1.3 ° Wielozbiory, kolejki i stosy 137
T y p o w y k lie n t p u b lic c la s s Sta ts
ty p u B ag {
p u b lic s t a t ic void m a in (S trin g [] a rgs)
(
Bag<Double> numbers = new Bag<D ouble>();
w hile (IS td ln .is E m p t y O )
numbers.a d d (S td ln . readDouble( ) ) ;
in t N = n u m b e [Link] e ();
double sum = 0.0;
fo r (double x : numbers)
sum += x;
double mean = sum/N;
sum = 0.0;
f o r (double x : numbers)
sum += (x - mean)*(x - mean);
double std = M a th .sq rt(su m / (N -1 ));
S t d O u t.p rin t f("S re d n ia : % .2 f \n ", mean);
S td 0 u t.p rin t f("0 d c h . S t .: % .2 f \n ", std );
}
}
Z a s to s o w a n ie % java Sta ts
100
99
101
120
98
107
109
81
101
90
Średn ia: 100.60
Odch. S t .: 10.51
138 R O Z D Z IA Ł! 0 Podstawy
Kolejki FIFO Kolejka FIFO (lub po prostu kolejka) to kolekcja oparta na zasadzie
pierwszy na wejściu, pierwszy na wyjściu (ang. first-in-jirst-out — FIFO). Zasada wy
konywania zadań w kolejności ich nadcho
Serwer
Kolejka klientów dzenia jest często spotykana w codziennym
{
Tc
życiu — od osób czekających w kolejce po
m m m bilet do teatru, przez samochody stoją
N o w y element
ce przed budką poboru opłat, po zadania
Dodaw anie trafia na koniec oczekujące na wykonanie przez aplikację
do kolejki ł w komputerze. Podstawą wszystkich reguł
m Ta
mmmm obsługi jest uczciwość. Większość osób
N o w y element za uczciwe rozwiązanie uznaje to, że jed
trafia na koniec
Dodaw anie nostka oczekująca najdłużej powinna zo
do kolejki ł
CD Tm m m m m stać obsłużona jako pierwsza. Tak właśnie
działa kolejka FIFO. Kolejki są naturalnym
Pierwszy element modelem wielu codziennych zjawisk i od
opuszcza
Usuwanie kolejkę grywają kluczową rolę w wielu aplikacjach.
z kolejki I Kiedy klient przechodzi po elementach ko
m m Tmmmm lejki za pom ocą techniki fo r each, elementy
Następny element są przetwarzane w kolejności ich dodawa
opuszcza nia do kolejki. Kolejki w aplikacjach stosu
Usuwanie kolejkę
je się głównie po to, aby zapisać elementy
z kolejki ł
m CD Tm m m w kolekcji, zachowując przy tym ich względ
ną kolejność. Elementy opuszczają kolejkę
T ypow a kolejka FIFO w tej samej kolejności, w jakiej je do niej
dodano. Przykładowo, przedstawiony dalej
klient to implementacja m etody statycznej r e a d D o u b le s () z opracowanej przez nas
klasy In. M etoda ta pozwala klientowi pobierać liczby z pliku do tablicy, bez uprzed
niej znajomości rozmiaru pliku. Metoda dodaje do kolejki liczby z pliku, używa m eto
dy s i z e () typu Queue do określenia
rozmiaru tablicy, tworzy ją, a na p u b lic s t a t ic i n t [] r e a d ln t s ( S t r in g name)
stępnie usuwa z kolejki liczby, aby {
In in = new In(nam e);
przenieść je do tablicy. Kolejka jest
Queue<Integer> q = new Q ueu e< Intege r> ();
odpowiednia, ponieważ powoduje w hile (lin . is E m p t y O )
umieszczanie liczb w tablicy w ko q .e n q u e u e (in .re a d ln t ());
lejności, w jakiej występują w pliku
in t N = q . s i z e ( ) ;
(jeśli kolejność jest nieistotna, m oż i nt [] a = new i nt [N ];
na użyć typu Bag). W kodzie wyko f o r ( in t i = 0; i < N; i++)
rzystano autoboxing i autounboxing a [i ] = [Link] ;
re tu rn a;
do przekształcania między typem
prostym doubl e z kodu klienta a ty
pem nakładkowym D o u b le używa Przykładowy klient typu Queue
nym w kolejce.
1.3 Q Wielozbiory, kolejki i stosy 139
Stosy Stos to kolekcja oparta na zasadzie Stos
ostatni na wejściu, pierwszy na wyjściu (ang. dokumentów
last-in-first-out— LIFO). Przy przechowywa
niu poczty na stercie na biurku używasz stosu.
Nowe wiadomości umieszczasz na wierzchu,
Nowy (szary)
a kiedy masz na to czas, czytasz pierwszy list jest dokładany
pushC
od góry. Obecnie nie używamy tylu doku na wierzch
mentów, co kiedyś, jednak ta sama zasada sta
nowi podstawę kilku regularnie używanych
aplikacji. Przykładowo, wiele osób porząd Nowy (czarny)
jest dokładany
kuje pocztę elektroniczną za pomocą stosu. p u sh ( .
na wierzch
Dodają (ang. push) otrzymaną wiadomość na
wierzch i zdejmują (ang. pop) ją, aby się z nią
zapoznać, przy czym zaczynają od najnow Zdejmowanie
szych listów (ostatni na wejściu, pierwszy na czarnego
po p C)
z wierzchu
wyjściu). Zaletą tej strategii jest to, że można
zapoznać się z ciekawą wiadomością zaraz
po jej otrzymaniu. Wada polega na tym, że
niektóre starsze listy nigdy nie zostaną prze Zdejmowanie
czytane, jeśli stos nigdy nie jest opróżniany. szarego
' = popQ
z wierzchu
Prawdopodobnie znasz też inny przykłado
wy stos, który powstaje w czasie poruszania £
się po sieci WWW. W momencie kliknięcia
Operacje na stosie
odnośnika przeglądarka wyświetla nową stro
nę (i umieszcza ją na stosie). Można wciąż
klikać odnośniki, aby przechodzić do nowych stron, jednak zawsze m ożna wrócić
do poprzedniej, klikając przycisk Wstecz (zdejmując stronę ze stosu). Reguła LIFO
obowiązująca dla stosu zapewnia właśnie takie działanie. Kiedy klient przechodzi
po elementach stosu za pom ocą techniki foreach, elementy są przetwarzane w ko
lejności odwrotnej do ich dokładania.
Typowym powodem stosowania ite- p u b lic c la s s Reverse
ratora dla stosu w aplikacji jest chęć 1
zachowania elementów kolekcji z jed p u b lic s t a t ic void m a in (S trin g [] a rgs)
noczesnym odwróceniem ich względ 1
Stac k< In te ge r> stack;
nej kolejności. Przykładowo, klient stack = new S t a c k < In t e g e r> ();
Reverse, widoczny po prawej stronie, w h ile (IS td ln .is E m p t y O )
s t a c k . p u s h ( S t d ln . r e a d ln t O ) ;
odwraca kolejność liczb całkowitych ze
standardowego wejścia, przy czym nie f o r ( in t i ; stack)
trzeba z góry wiedzieć, ile ich jest. Stosy S t d O u t . p r in t ln ( i) ;
mają podstawowe i istotne znaczenie ^
w przetwarzaniu, czego dowodzi oma- ^
wiany dalej szczegółowy przykład. Przykładowy klient typu Stack
I
14 0 R O Z D Z IA L I ■ Podstawy
P rzetw arzanie w yrażeń arytm etycznych Rozważmy inny, klasyczny przykład
klienta używającego stosu (pokazano tu też przydatność typów generycznych).
Niektóre z pierwszych programów omawianych w p o d r o z d z i a l e i . i obejmowały
obliczanie wartości wyrażeń arytmetycznych podobnych do poniższego:
( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )
Pomnożenie 4 przez 5, dodanie 2 do 3, pomnożenie wyników i dodanie 1 daje war
tość 101. Jak jednak system Javy przeprowadza te obliczenia? Nie wdając się w szcze
góły działania systemu Javy, m ożna przedstawić kluczowe zagadnienia przez napi
sanie w Javie programu, który przyjmuje jako dane wejściowe łańcuch znaków (wy
rażenie) i zwraca jako dane wyjściowe liczbę reprezentowaną przez wyrażenie. Dla
uproszczenia zacznijmy od rekurencyjnej definicji: wyrażenie arytmetyczne to albo
liczba, albo lewy nawias, po którym następuje wyrażenie arytmetyczne, operator, ko
lejne wyrażenie arytmetyczne i prawy nawias. Z uwagi na uproszczenie jest to defi
nicja wyrażenia arytmetycznego w notacji nawiasowej, dokładnie określającej, które
operatory dotyczą poszczególnych operandów. Możliwe, że lepiej znasz wyrażenia
w rodzaju 1 + 2 * 3 , które często oparte są na priorytetach operatorów, a nie na
nawiasach. Omawiane podstawowe mechanizmy pozwalają uwzględniać priorytety,
jednak tu pomijamy tę komplikację. Obsługiwane są tu znane operatory binarne, *,
+, - i /, a także operator pierwiastka kwadratowego, sq rt, przyjmujący tylko jeden
argument. Łatwo m ożna dodać więcej operatorów i nowe ich rodzaje, aby uwzględ
nić dużą klasę znanych wyrażeń matematycznych (w tym funkcji trygonom etrycz
nych, wykładniczych i logarytmicznych). Koncentrujemy się tu na zrozumieniu, jak
interpretować łańcuch nawiasów, operatorów i liczb, aby umożliwić wykonywanie
we właściwej kolejności niskopoziomowych operacji arytmetycznych dostępnych na
każdym komputerze. W jaki dokładnie sposób odbywa się przekształcanie wyraże
nia arytmetycznego — łańcucha znaków — na reprezentowaną wartość? Niezwykle
prosty algorytm opracowany przez E. W. Dijkstrę w latach 60. ubiegłego wieku wy
maga do wykonania tego zadania dwóch stosów (jednego na operandy, drugiego
na operatory). Wyrażenie składa się z nawiasów, operatorów i operandów (liczb).
Przetwarzając dane od lewej do prawej i pobierając elementy po jednym, m ożna m a
nipulować stosami na cztery podstawowe sposoby:
■ umieszczanie operandów na stosie operandów;
■ umieszczanie operatorów na stosie operatorów;
■ ignorowanie lewych nawiasów;
n po napotkaniu prawego nawiasu należy zdjąć operator, zdjąć odpowiednią licz
bę operandów i umieścić na stosie operandów wynik zastosowania operatora
do operandów.
Po przetworzeniu ostatniego prawego nawiasu na stosie znajduje się tylko jedna war
tość. Jest to wartość wyrażenia. Metoda ta początkowo może wydawać się zagadko
wa, jednak m ożna łatwo się przekonać, że daje poprawną wartość. Kiedy algorytm
1.3
Oparty na dwóch stosach algorytm Dijkstry do obliczania wartości wyrażeń
public c la s s Evaluate
{
p u b l i c s t a t i c void m a i n ( S t r i n g [ ] args)
Stack<String> ops = new S t a c k < S t r i n g > ( ) ;
St a c k < Do u b l e> v a l s = new S t a c k < D o u b l e > ( ) ;
while (IS td ln .isE m p ty O )
{ / / Wczytywanie symbol u; j e ś l i t o o p e r a t o r n a l e ż y u mi e ś c i ć go na s t o s i e .
String s = S td ln .re a d S trin g O ;
if ([Link] u a ls("("))
else i f ([Link]("+")) [Link](s)
else i f ([Link]("-")) [Link](s)
else i f ([Link]("*")) [Link](s)
else i f ([Link]("/")) [Link](s)
else i f ([Link]("sqrt")) [Link](s)
else i f ([Link])"))
{ / / Jeśli symbol t o należy z dją ć elementy obi i c z y ć wyni k
// i u m i e ś c i ć go na s t o s i e .
S t r i n g op = o p s . p o p ( ) ;
double v = v a l s . p o p O ;
if ([Link]("+")) = [Link] + v;
else i f ([Link]("-")) = [Link] - v;
else i f ([Link]("*")) = [Link] * v;
else i f ([Link]("/")) = [Link] / v;
else i f ([Link]("sqrt")) = M [Link](v);
[Link](v);
} / / Symbol n i e j e s t o p e r a t o r e m a ni na wi as em.
// N a l e ż y u m i e ś c i ć na s t o s i e w a r t o ś ć t y p u d o u b l e ,
e lse v a ls .p u sh([Link](s));
}
[Link](v a ls.p o p O );
}
Przedstawiony klient typu Stack używa dwóch stosów do obliczania wyrażeń arytmetycznych.
Jest to ilustracja podstawowego procesu z dziedziny przetwarzania — interpretowania łańcu
cha znaków jako programu i wykonywania go w celu obliczenia pożądanego wyniku. Dzięki
typom generycznym można użyć kodu z jednej implemen
tacji typu Stack do zaimplementowania stosu wartości %ja va Evaluate
typu S tri ng i stosu wartości typu Doubl e. Dla uproszczę- ( l + ( ( Z + 3 ) * ( 4 * 5 ) ) )
nia w kodzie przyjęto, że wyrażenie zapisano w notacji na
wiasowej, a liczby i znaki są oddzielone odstępami. % java Evaluate
( ( 1 + sq rt ( 5.0 ) ) / 2.0 )
1.618033988749895
142 ROZDZIALI a Podstawy
napotka podwyrażenie składające się z dwóch operandów rozdzielonych operatorem
(wszystkie te elementy znajdują się w nawiasach), umieszcza wynik wykonania ope
racji na stosie operandów. Efekt jest taki sam, jakby w danych wejściowych wartość
pojawiła się zamiast podwyrażenia. Dlatego m ożna zastąpić podwyrażenie wartoś
cią, aby uzyskać wyrażenie, które daje ten sam wynik. Metodę tę m ożna stosować
wielokrotnie do m om entu uzyskania pojedynczej wartości. Przykładowo, algorytm
oblicza tę samą wartość dla wszystkich poniższych wyrażeń:
( 1+ ( ( 2 + 3 ) * ( 4 * 5 ) ) )
( 1+ ( 5 * ( 4 * 5 ) ) )
( 1+ ( 5 *20 ) )
( 1+100 )
101
Klasa Evaluate, przedstawiona na poprzedniej stronie, zawiera implementację tego
algorytmu. Pokazany kod to prosty przykład interpretera, czyli programu interpre
tującego obliczenia podane w formie łańcucha znaków i wykonującego te obliczenia
w celu uzyskania wyniku.
1.3 a Wielozbiory, kolejki i stosy 143
^ Lewy naw ias - ignorow any
( l + ( ( 2 + 3 ) * ( 4 * 5 ) ) )
y O perand - um ieszczany n a stosie operandów
Stos jI, . .,
operandów ~''x_ l + ( ( 2 + 3 ) * ( 4 * 5 ) ) )
Stos
fcd
p j ---------
^ Operator - um ieszczany na stosie operatorów
+ ( ( 2 + 3 ) * ( 4 * 5 ) ) )
operatorów ' sx | + 1
( ( 2 + 3) * ( 4 * 5 ) ) )
11
( 2 + 3 ) * ( 4 * 5 ) ) )
2 + 3 ) * (4 * 5 ) ) )
+ 3 ) * ( 4 * 5) ) )
1 2
++
3 ) * ( 4 * 5 ) ) )
Praw y naw ias - zdejm ow anie operatora
j / i operandów oraz um ieszczanie w yniku na stosie
) * C4 * 5 ) ) )
* ( 4 * 5 ) ) )
[U
( 4*5) ))
4*5)))
11 5 4
II + * 1
* 5) ) )
Il *5 *4
1+
5) ) )
II1 5* 4* 5:
II*
) ) )
u 5 20 ;
L+. A |
) )
11 100
1+
)
| 101 i
Ślad działania opartego na dwóch stosach algorytmu
Dijkstry do obliczania wyrażeń arytmetycznych
144 R O Z D Z IA Ł ! n Podstawy
Implementowanie kolekcji Omawianie implementacji typów Bag, Stack i Queue
zaczynamy od prostej, klasycznej implementacji, a następnie przedstawiamy uspraw
nienia prowadzące do implementacji interfejsów API opisanych na stronie 133.
Stos o stałej pojemności W ramach wstępu rozważmy abstrakcyjny typ danych dla sto
su o stałej długości, przechowującego łańcuchy znaków (kod pokazano na następnej
stronie). Interfejs API różni się tu od interfejsu API opracowanego przez nas typu Stack.
Nowy typ działa tylko dla wartości typu S tri ng, wymaga określenia pojemności przez
klienta i nie obsługuje iterowania. Główna decyzja przy tworzeniu implementacji inter
fejsu API dotyczy wyboru reprezentacji danych. Dla typu Fi xedCapaci tyStackOfStri ngs
oczywistym rozwiązaniem jest użycie tablicy wartości typu S tri ng. Wybór ten prowadzi
do utworzenia implementacji przedstawionej w dolnej części następnej strony. Trudno
utworzyć prostszą implementację — każda metoda ma tu jeden wiersz. Zmienne eg
zemplarza to tablica a [], przechowująca elementy stosu, i liczba całkowita N, służąca do
zliczania elementów na stosie. Aby usunąć element, należy zmniejszyć wartość N, a na
stępnie zwrócić a [N]. W celu wstawienia nowego elementu trzeba ustawić a [N] na nowy
element i zwiększyć wartość N. Operacje te pozwalają zachować następujące cechy:
■ Kolejność elementów w tablicy odpowiada kolejności ich wstawiania.
■ Stos jest pusty, jeśli Njest równe 0.
■ Wierzchołek stosu (jeśli stos nie jest pusty) to element a [N-1].
Myślenie w kategoriach niezmienników tego rodzaju jest, jak zwykle, najprostszym spo
sobem na sprawdzenie, czy implementacja działa w oczekiwany sposób. Należy w peł
ni zrozumieć implementację. Najlepszy sposób to sprawdzenie śladu zawartości stosu
dla ciągu operacji, co pokazano po lewej stronie dla klienta testowego. Klient wczytuje
łańcuchy znaków ze standardowego wejścia
Std ln StdOut N a[n]
(dodaj) (zdejmij)
i umieszcza każdy łańcuch na stosie, chyba że
0 1 2 3 4
jego wartość to — wtedy klient zdejmuje
0
element ze stosu i wyświetla wynik. Główną
to 1 to
cechą z obszaru wydajności tej implementa
be 2 to be
cji jest to, że operacje dodaj i zdejmij zajmują
or 3 to be or
tyle samo czasu niezależnie od wielkości sto
not 4 to be or not
su. Implementacja ta jest stosowana w wielu
to 5 to be or not to
aplikacjach ze względu na jej prostotę. Ma
- to 4 to be or not to
jednak kilka wad, które ograniczają jej zasto
be 5 to be or not be
sowanie jako narzędzia do ogólnego użytku.
- be 4 to be or not be
Takie narzędzie opisujemy dalej. Wkładając
- not 3 to be or not be
w to trochę pracy (i korzystając z mechani
that - 4 to be or that be
zmów Javy), można opracować implemen
- that 3 to be or that be
tację przydatną w większej liczbie sytuacji.
- or 2 to be or that be
Wysiłek jest tego wart, ponieważ opracowana
- be 1 to be o r that be
tu implementacja posłuży za model dla im
is 2 to is or not to
plementacji innych, bardziej rozbudowanych
Ślad działania klienta testowego
abstrakcyjnych typów danych w tej książce.
FixedCapacityStackOfStrings
1.3 ® Wielozbiory, kolejki i stosy 145
I n t e r f e j s API p u blic c la s s FixedCapacityStackO fStrings
Fi xedCapaci ty S ta c k O fS tri ngs (i nt cap) Tworzenie pustego stosu o pojemności cap
void push (S t r in g item) Dodawanie łańcucha znaków
S t rin g pop() Usuwanie ostatnio dodanego łańcucha znaków
boolean isEm p tyO Czy stos jest pusty?
in t s iz e ( ) Liczba łańcuchów znaków na stosie
K lien t te s t o w y p u b lic s t a t ic void m a in (S trin g [] args)
{
F ix e d C ap a city Sta ck O fStrin gs s;
s = new F ix e d C a p a c ity Sta c k O fStrin g s(lO O );
w h ile ( I S t d l n . isEm p tyO )
{
S t r in g item = S t d ln .r e a d S t r in g O ;
i f ( lit e m . e q u a ls ( "- ") )
s .p u sh (ite m );
e lse i f ( Is .is E m p t y O ) Std O u t.p rin t(s.p o p () + 11 " ) ;
}
Std O u t.p rin tln ("(e le m e n ty na s t o s ie : " + s . s i z e ( ) + " ) " ) ;
Z a s to s o w a n ie % more to b e .txt
to be or not t o - b e - - that - - - i s
% java Fix e d C ap a city Sta ck O fStrin gs < to b e .txt
to be not that or be (elementy na s t o s ie : 2)
I m p le m e n ta c ja p u b lic c la s s Fix e d C ap a citySta ck O fStrin gs
f
p riv a te S t r in g [] a; // Elementy stosu,
p riv a te in t N; // Rozmiar.
p u b lic F ix e d C a p a c ity S ta c k O fS trin g s(in t cap)
{ a = new S t rin g [cap]; }
p u b lic boolean isEm ptyO f return N == 0; }
p u b lic in t s iz e ( ) { return N; )
p u b lic void p u sh (S trin g item)
{ a[N++] = item; }
p u b lic S t rin g pop()
{ return a [ — N ]; }
A b s tra k c y jn y t y p d a n y c h d la s to s u o s ta łe j d łu g o ś c i
n a ła ń c u c h y z n a k ó w
146 R O Z D Z IA L I ■ Podstawy
Typy generyczne Pierwszą wadą typu FixedCapacityStackOfStrings jest to, że
działa tylko dla obiektów typu String. Aby utworzyć stos wartości typu double,
trzeba opracować inną klasę o podobnym kodzie, co w zasadzie sprowadza się do
zastąpienia w każdym miejscu nazwy S tring nazwą double. Jest to łatwe, ale staje
się uciążliwe, kiedy trzeba zbudować stosy wartości typu Transaction, Data itd. Jak
opisano to na stronie 134, typy sparametryzowane (generyczne) Javy zaprojektowano
specjalnie pod kątem tej sytuacji. Przedstawiono już kilka przykładów kodu klienta
(na stronach 137, 138, 139 i 141). Jak jednak m ożna zaimplementować generyczny
stos? Szczegóły pokazano w kodzie na następnej stronie. Zaimplementowano tam
klasę FixedCapacityStack. Różni się ona od klasy FixedCapacityStackOfStrings
tylko wyróżnionym kodem. Każde wystąpienie typu S tri ng zastąpiono słowem Item
(z jednym opisanym dalej wyjątkiem). Klasa zadeklarowana jest za pom ocą poniż
szego pierwszego wiersza kodu:
public class FixedCapacityStack<Item>
Nazwa Item to parametr typu, czyli symboliczny zastępnik, zamiast którego m oż
na podać konkretny typ używany w kliencie. Fragment FixedCapacityStack<Item>
można czytać jako stos elementów. Dokładnie tego potrzebujemy. Na etapie imple
mentowania typu FixedCapacityStack typ podstawiany za Item nie jest znany, jed
nak klient może używać stosu dla dowolnego typu danych, podając konkretny typ
w czasie tworzenia stosu. Konkretny typ musi tu być typem referencyjnym, jednak
w klientach m ożna wykorzystać autoboxing do konwersji typów prostych na powią
zane typy nakładkowe. Java używa param etru typu Item do wykrywania błędów nie
dopasowania. Choć konkretny typ nie jest jeszcze znany, do zmiennych typu Item
przypisane muszą być wartości typu Item itd. Występuje tu jednak pewna trudność.
W implementacji konstruktora typu Fi xedCapaci tyStack chcielibyśmy użyć kodu:
a = new Item jcap];
Wymaga on utworzenia generycznej tablicy. Z przyczyn historycznych i technicz
nych, których omawianie wykracza poza zakres książki, tworzenie tablic genetycz
nych jest w Javie niedozwolone. Zamiast tego należy zastosować rzutowanie:
a = (Item[J) new Object [cap];
Kod ten prowadzi do pożądanych efektów, choć kompilator Javy zgłasza ostrzeżenie,
które jednak można bezpiecznie zignorować. Stosujemy ten idiom w książce (użyto
go też w implementacjach bibliotek systemowych Javy dla podobnych abstrakcyj
nych typów danych).
1.3 a Wielozbiory, kolejki i stosy 1 47
I n t e r f e js API p u b lic c la s s Fixed C apacityStack<Item >
F ix e d C a p a city Sta ck (in t cap) Tworzenie pustego stosu o pojemności cap
void push(Item item) Dodawanie elementu
Item pop() Usuwanie ostatnio dodanego elementu
boolean isEm ptyO Czy stos jest pusty?
in t s iz e ( ) Liczba elementów na stosie
Klient testowy p u b lic s t a t ic void m a in (S trin g [] a rgs)
{
Fix e d C ap a citySta ck <Strin g> s;
s = new F ix e d C a p a c ity Sta c k < S trin g > (1 0 0 );
w hile (IS td ln .is E m p t y O )
{
S t r in g item = S t d ln . r e a d S t r in g O ;
i f ( lit e m . e q u a ls ( "- ") )
s.p u sh (ite m );
e lse i f ( Is .is E m p t y O ) S td O u t.p rin t(s.p o p () + " " ) ;
1
Std O u t.p rin tln ("(e le m e n ty na s t o s ie : " + s . s i z e ( ) + " ) " ) ;
1
Zastosowanie
% more to b e .txt
to be or not t o - b e - - that - - - i s
% Java FixedC apacityStack < to b e .txt
to be not that or be (na s t o s ie : 2)
Implementacja p u b lic c la s s FixedCapacityStack<Item >
f
p riv a te Item[] a; // Elementy stosu,
p riv a te in t N; // Rozmiar.
p u b lic F ix e d C a p a city Sta ck (in t cap)
( a = (Ite m G ) new Object [cap]; )
p u b lic boolean isEm ptyO { return N == 0; }
p u b lic in t s iz e ( ) { return N; }
p u b lic void push(Item item)
{ a[N++] = item; }
p u b lic Item pop()
{ return a [ — N ]; }
1
Abstrakcyjny typ danych dla generycznego stosu
o stałej pojem ności
14 8 R O Z D Z IA L I b Podstawy
Z m iana wielkości tablicy Reprezentowanie zawartości stosu za pomocą tablicy p o
woduje, że w klientach trzeba z góry oszacować maksymalny rozmiar stosu. W Javie
nie m ożna zmienić wielkości tablicy po jej utworzeniu, dlatego stos zawsze zaj
muje pamięć równą jego maksymalnemu rozmiarowi. Ustawienie w kliencie d u
żej pojemności grozi m arnowaniem dużej ilości pamięci, kiedy kolekcja jest pusta
(lub prawie pusta). Przykładowo, system transakcyjny może obejmować miliardy
elementów i tysiące kolekcji na nie. W takim kliencie trzeba umożliwić zapisanie
w każdej kolekcji wszystkich elementów, choć typowym ograniczeniem w systemach
tego rodzaju jest to, że każdy element może występować w jednej tylko kolekcji.
Ponadto w klientach występuje zagrożenie przepełnieniem, kiedy kolekcja staje się
większa od tablicy. Dlatego w metodzie push () trzeba sprawdzać, czy stos jest pełny.
W interfejsie API należy też udostępnić metodę isFul 1 (), umożliwiającą klientom
sprawdzanie tego warunku. Pomijamy ten kod, ponieważ chcemy, co odzwierciedla
pierwotny interfejs API typu Stack, zwolnić klienty z konieczności radzenia sobie
z sytuacją zapełnienia stosu. Zamiast tego modyfikujemy implementację tablicy, tak
aby dynamicznie dostosowywać rozmiar tablicy a [], dzięki czemu będzie wystar
czająco duża, żeby pomieścić wszystkie elementy, a przy tym na tyle mała, że ilość
marnowanej pamięci nie będzie zbyt duża. Realizacja tych celów okazuje się zaska
kująco łatwa. Po pierwsze, trzeba zaimplementować metodę, która przenosi stos do
tablicy o innej wielkości:
private void r e s iz e ( in t max)
{ / / Przenoszenie stosu o rozmiarze N <= max do nowej t a b l ic y
// o wielkości max.
Item [] temp = (Item[]) new Object [max];
f o r (in t i = 0; i < N; i++)
temp[i] = a [ i ] ;
a = temp;
}
Teraz w metodzie push () należy sprawdzać, czy tablica nie jest zbyt mała. Konkretnie
należy określić, czy w tablicy dostępne jest miejsce na nowy element. Wymaga to
sprawdzenia, czy wielkość stosu, N, jest równa długości tablicy, a . 1ength. Jeśli nie ma
wolnego miejsca, kod podwaja wielkość tablicy. Następnie wystarczy, tak jak wcześ
niej, wstawić nowy element za pomocą instrukcji a [N++] = item:
public void p u sh (Strin g item)
{ // Umieszczanie elementu na wierzchu stosu,
i f (N == [Link]) r e s i z e ( 2 * a . le n g t h );
a[N++] = item;
}
Podobnie w metodzie pop() należy rozpocząć od usunięcia elementu, a następnie
zmniejszyć wielkość tablicy o połowę, jeśli jest zbyt duża. Po analizie widać, że od
powiednim testem jest sprawdzanie, czy wielkość stosu nie spadła poniżej jednej
1.3 h Wlelozbiory, kolejki i stosy 149
czwartej rozm iaru tablicy. Po zmniejszeniu wielkości tablicy o połowę będzie ona
w połowie pełna, co pozwoli wykonać wiele operacji push ( ) i pop ( ) , zanim niezbęd
na będzie ponowna zmiana rozmiaru.
public S t r in g pop()
{ // Zdejmowanie elementu z wierzchu stosu.
S t r in g i tern = a [--N ];
a[N] = n u li; // Likwidowanie zbędnych referencji
// (zobacz opis w te k ście ),
i f (N > 0 && N == [Link]/4) r e s iz e ( a . le n g t h / 2 );
return i tern;
}
W tej implementacji stos nigdy nie zostaje przepełniony i nigdy nie jest zajęty w mniej
niż jednej czwartej (o ile nie jest pusty — wtedy rozmiar tablicy to 1). Szczegółowe
analizy dotyczące wydajności tego podejścia przedstawiono w p o d r o z d z i a l e 1 .4 .
Zbędne referencje Reguły przywracania pamięci w Javie powodują odzyskiwanie
pamięci powiązanej z obiektami, do których dostęp jest niemożliwy. W przedstawio
nych tu implementacjach m etody pop () referencja do pobranego elementu pozostaje
w tablicy. Element jest w zasadzie osierocony — kod nigdy już nie będzie z niego
korzystał — jednak mechanizm przywracania pamięci nie potrafi tego stwierdzić do
czasu nadpisania elementu. Nawet kiedy klient skończy korzystać z elementu, refe
rencja w tablicy może sprawić, że element nie zostanie usunięty. Przechowywanie
referencji do niepotrzebnego elementu prowadzi do powstawania zbędnych referencji
(ang. loitering). Tu można łatwo uniknąć tego zjawiska, ustawiając odpowiadający
zdjętemu elementowi wpis w tablicy na nuli. Powoduje to nadpisanie nieużywanej
referencji i umożliwia systemowi przywrócenie pamięci powiązanej ze zdjętym ele
mentem, kiedy klient skończy z niego korzystać.
push() pop() N a .le ngth a [n]
0 1 2 3 4 5 6 7
0 1 null
to 1 to
be 2 2 be
or 3 4 or null
not 4 not
to 5 8 to null null null
- to 4 null
be 5 be
- be 4 null
- not 3 null
that - 4 that
- that 3 null
- or 2 4 null null
- be 1 3 null
is 2 is
Ślad procesu zmieniania wielkości tablicy przy wykonywaniu serii operacji push() i pop()
150 R O Z D Z IA L I a Podstawy
Iterowanie Jak wspomniano we wcześniejszej części podrozdziału, jedną z podsta
wowych operacji na kolekcjach jest przetwarzanie każdego elementu w czasie itero-
wania po kolekcji, używając instrukcji foreach Javy. Technika ta pozwala tworzyć
przejrzysty i zwięzły kod, wolny od zależności od szczegółów implementacji kolek
cji. Omówienie implementowania iteracji rozpoczynamy od fragmentu kodu klien
ta, który wyświetla wszystkie elementy kolekcji łańcuchów znaków (po jednym na
wiersz):
Stack<String> c o lle c tio n = new S tack<S tring>();
fo r (S tring s : co llec tio n )
S td O u t.p rin tln (s);
Ta instrukcja foreach jest skrótem dla struktury whi 1e (podobnie jak sama instrukcja
for); stanowi odpowiednik poniższej instrukcji whi 1 e:
Iterato r< S trin g > i = col l e c tio n .i t e r a t o r ( ) ;
while (i .hasNext())
{
S tring s = i .n e x t( ) ;
S td O u t.p rin tln (s);
}
Ten kod obejmuje elementy potrzebne do zaimplementowania dowolnej kolekcji
z możliwością iterowania:
■ Kolekcja musi obejmować implementację m etody ite r a to r ( ) zwracającej
obiekt typu Ite ra to r.
■ Klasa Ite r a to r musi obejmować dwie metody: hasNext () (zwracającą wartość
typu bool ean) i next () (zwraca element generyczny z kolekcji).
W Javie do określania, że klasa zawiera implementację konkretnej metody, służy sło
wo i n te rf ace (zobacz stronę 112). W kolekcjach z możliwością iterowania niezbędne
interfejsy są już zdefiniowane w Javie. Aby umożliwić iterowanie po klasie, najpierw
trzeba dodać do jej deklaracji fragment implements Iterable<Item >, odpowiadający
interfejsowi:
public in te rfa c e Iterable<Item >
{
Iterator<Item > i t e r a to r ( ) ;
}
Interfejs ten znajduje się w bibliotece jav a. 1an g .Ite ra b le . Do klasy trzeba też do
dać metodę ite r a to r ( ) zwracającą obiekt Iterator<Item >. Iteratory są generyczne,
dlatego można używać sparametryzowanego typu Item, aby umożliwić klientom ite
rowanie po obiektach niezależnie od podanego typu. W używanej tu reprezentacji
1.3 o Wielozbiory, kolejki i stosy 151
w postaci tablicy trzeba iterować po tablicy w kolejności odwrotnej. Dlatego nazwa
liśmy iterator ReverseArray I te r a to r i dodaliśmy poniższą metodę:
public Iterator<Item> it e ra to r()
{ return new R e v e rs e A rra y It e ra to r(); }
Czym jest iterator? Jest to obiekt klasy z implementacją m etod hasNext() i next(),
co określa poniższy interfejs (znajduje się on w bibliotece j ava. uti 1 . Iterator):
public interface Iterator<Item>
{
boolean hasNext();
Item next() ;
void remove();
}
Choć w interfejsie określono metodę remove(), w tej książce zawsze jest ona pusta,
ponieważ najlepiej jest unikać łączenia iteracji z operacjami modyfikującymi struktu
rę danych. W iteratorze ReverseArraylterator wszystkie metody mają jeden wiersz
i są zaimplementowane w klasie zagnieżdżonej w ldasie stosu:
private c la s s ReverseArraylterator implements Iterator<Item>
{
private in t i = N;
public boolean hasNext() ( return i > 0; }
public Item next() { return a [ - - i ] ; }
public voici remove() { }
}
Warto zauważyć, że zagnieżdżona klasa ma dostęp do zmiennych egzemplarza klasy
zewnętrznej, którymi tu są a [] i N (ta możliwość to jedna z podstawowych przyczyn
tworzenia iteratorów jako klas zagnieżdżonych). Technicznie, aby zachować zgod
ność ze specyfikacją interfejsu Iterator, należy w dwóch sytuacjach zgłaszać wyjąt
ki: wyjątek UnsupportedOperati onExcepti on, jeśli klient wywołuje metodę remove (),
i wyjątek NoSuchEl ementExcepti on, kiedy klient wywołuje next() przy i równym 0.
Ponieważ iteratory są tu używane tylko w strukturze foreach, gdzie sytuacje te nie
występują, pomijamy kod wyjątków. Pozostaje jeden ważny szczegół — na początku
programu trzeba dołączyć instrukcję:
import j a v a . u t i l .Ite ra t o r;
Wynika to z tego, że z przyczyn historycznych It e r a t o r nie jest częścią biblioteki
[Link] (choć Iterab le do niej należy). Teraz klient używający instrukcji foreach
dla danej klasy uzyska efekt podobny do korzystania z typowej pętli fo r dla tablic,
jednak nie musi wiedzieć, że dane zapisane są w tablicy (jest to szczegół implemen-
152 R O Z D Z IA L I □ Podstawy
tacji). To rozwiązanie ma kluczowe znaczenie w implementacjach podstawowych
typów danych, takich jak kolekcje omawiane w książce i dostępne w bibliotekach
Javy. Dzięki tej technice można zastosować kompletnie odm ienną reprezentację bez
konieczności modyfikowania kodu klientów. Co ważniejsze, w klientach można stoso
wać iterację bez znajomości szczegółów implementacji klasy.
alg o rytm i . i to implementacja interfejsu API opracowanego przez nas typu Stack.
Implementacja zmienia wielkość tablicy, umożliwia klientom tworzenie stosów dla
dowolnego typu danych i pozwala na stosowanie instrukcji foreach do iterowania po
elementach stosu w porządku LIFO. Implementację oparto na mechanizmach Javy,
w tym interfejsach I te r a to r i Ite ra b le , nie trzeba jednak szczegółowo ich pozna
wać, ponieważ sam kod jest prosty i można go wykorzystać jako szablon dla innych
implementacji kolekcji.
Przykładowo, m ożna zaimplementować interfejs API typu Queue, przechowując
dwa indeksy jako zmienne egzemplarza, zmienną head określającą początek kolejki
i zmienną ta i 1 wyznaczającą jej koniec. Aby usunąć element, należy użyć zmiennej
head w celu uzyskania dostępu do elementu, a następnie zwiększyć wartość tej zm ien
nej. Aby wstawić element, należy wykorzystać zmienną ta i 1 do jego zapisania, a na
stępnie zwiększyć tę zmienną. Jeśli inkrementacja indeksu prowadzi do wyjścia poza
koniec tablicy, należy ustawić indeks na 0. Opracowanie szczegółów sprawdzania,
czy kolejka jest pusta i czy tablica jest pełna, co wymaga jej rozszerzenia, to ciekawe
i warte wykonania ćwiczenie programistyczne (zobacz ć w ic z e n ie 1 .3 . 1 4 ).
Std ln StdOut a[]
N head fLa
a i1l1
(dodaj) (usuń) 0 1 2 3 4 5 6 7
5 0 5 to be or not to
- to 4 1 5 to be or not to
be - 5 1 6 to be or not to be
- be 4 2 6 to be or not to be
_ or 3 3 6 to be or that to be
Ślad działania klienta testowego klasy ResizingArrayQueue
W kontekście badań nad algorytm am i a l g o r y t m 1. 1 m a duże znaczenie, ponieważ
prawie (choć nie do końca) pozwala zrealizować dla dowolnej im plem entacji kolekcji
cele z zakresu wydajności:
■ Ilość czasu wymagana przez operację pow inna być niezależna od rozm iaru
kolekcji.
■ Ilość zajmowanej pamięci powinna rosnąć liniowo wraz z wielkością pamięci.
Wadą klasy Resi zi ngArrayStack jest to, że niektóre operacje dodaj i zdejmij wymagają
zmiany wielkości, co zajmuje czas proporcjonalnie do rozmiaru stosu. Dalej omówiono
sposób rozwiązania tego problemu przez zupełnie inne uporządkowanie danych.
1.3 Wielozbiory, kolejki i stosy 153
ALGORYTM 1.1. Stos (LIFO) — implementacja ze zmianą wielkości tablicy
import j a v a . u t i l . I t e r a t o r ;
public c la s s ResizingArrayStack<Item> implements Iterable<Item>
{
p rivate Item [] a = ( Item [ ] ) new Object[1]; // E l e m e n t y s t o s u ,
private in t N = 0; // L i c z b a e l e m e n t ó w .
public boolean isEmptyQ { return N == 0; }
public in t s iz e () { return N; }
private void re siz e (in t max)
{ // P r z e n o s z e n i e s t o s u do nowej t a b l i c y o w i e l k o ś c i max.
Item[] temp = ( I tem []) new Object [max];
f o r (in t i = 0; i < N; i++)
temp[i] = a [ i ];
a = temp;
}
public void push(Item item)
{ // Doda wa nie e l e m e n t u na w i e r z c h s t o s u ,
i f (N == a . length) r e s i z e ( 2 * a . le n g t h );
a[N++] = item;
}
public Item pop()
{ // Zdejmowanie e le m e n t u z w i e r z c h u s t o s u .
Item item = a [ - - N ] ;
a[N] = n u li; // U n i k a n i e z b ę d n y c h r e f e r e n c j i ( z o b a c z o p i s w t e k ś c i e ) ,
i f (N > 0 && N == [Link]/4) re s iz e([Link] ng th /2);
return item;
}
public Iterator<Item> it e r a t o r ( )
{ return new R e ve rs e A rra y It e ra to r(); )
private c la s s ReverseArraylterator implements Iterator<Item>
( // Obsługa i t e r a c j i w p o r z ą d k u LIFO,
p rivate in t i = N;
public boolean hasNextQ ( return i > 0; }
public Item next() ( return a [ - - i]; }
public void remove() ( }
}
}
Ta generyczna implementacja z możliwością iterowania dla interfejsu API typu Stack jest
modelem dla typów ADT kolekcji przechowujących elementy w tablicy. Implementacja
zmienia wielkość tablicy, aby była zależna liniowo od rozmiaru stosu.
R O ZD ZIA Ł 1 b Podstawy
Listy powiązane Rozważmy teraz podstawową strukturę danych, która jest od
powiednia do reprezentowania danych w implementacjach typów ADT dla kolekcji.
Jest to pierwszy przykład, w którym pokazano budowanie struktury danych nieob-
sługiwanej bezpośrednio przez Javę. Przedstawiona implementacja jest modelem
kodu używanego do budowania w książce bardziej skomplikowanych struktur da
nych, dlatego powinieneś starannie zapoznać się z tym fragmentem, nawet jeśli masz
doświadczenie w stosowaniu list powiązanych.
Definicja. Lista powiązana to rekurencyjna struktura danych, która jest albo pusta
(nul 1 ), albo jest referencją do węzła zawierającego generyczny element i referencję
do listy powiązanej.
Węzeł w tej definicji to abstrakcyjna jednostka, która może przechowywać dane dowol
nego rodzaju, a także referencję do węzła, określającą rolę elementu w liście powiązanej.
Rekurencyjna struktura danych, podobnie jak program rekurencyjny, początkowo może
być trudna do zrozumienia, ma jednak bardzo dużą wartość z uwagi na jej prostotę.
Rekord w ęzła W programowaniu obiektowym implementowanie list powiązanych
nie jest trudne. Zaczynamy od klasy zagnieżdżonej z definicją abstrakcyjnego węzła:
private c la s s Node
{
Item item;
Node next;
}
Klasa Node ma dwie zmienne egzemplarza — Item (typ sparametryzowany) i Node.
Klasę Node należy zdefiniować w klasie, w której będzie używana, i poprzedzić m ody
fikatorem private, ponieważ klienty nie będą z niej korzystać. Obiekt typu Node, tak
jak każdego innego typu danych, można tworzyć przez wywołanie konstruktora bez
argumentów — new Node (). Powstaje w ten sposób referencja do obiektu typu Node,
którego obie zmienne egzemplarza są zainicjowane wartością null. Item to miejsce
na dane porządkowane za pomocą listy powiązanej (używamy typów generycznych
Javy, aby można było zastosować dowolny typ referencyjny). Zmienna egzemplarza
typu Node pozwala powiązać omawianą strukturę danych. Aby podkreślić, że klasa
Node służy tylko do strukturyzowania danych, nie definiujemy dla niej żadnych metod,
a w kodzie stosujemy bezpośrednio zmienne egzemplarza. Jeśli first to zmienna typu
Node, zmienne egzemplarza można wskazywać za pomocą kodu f irs t . i tem i fir s t . next.
Klasy tego rodzaju czasem nazywa się rekordami. Nie są one implementacjami abstrak
cyjnych typów danych, ponieważ bezpośrednio wskazujemy ich zmienne egzemplarza.
Jednak we wszystkich omawianych tu implementacjach typ Node i kod klienta tego typu
znajdują się w tej samej klasie, a obiekt typu Node nie jest dostępny dla klientów tej klasy,
dlatego nadal można czerpać korzyści ze stosowania abstrakcji danych.
1.3 s Wielozbiory, kolejki i stosy 155
B udow anie listy pow iązanej Na podstawie rekurencyjnej definicji można przed
stawić listę powiązaną za pomocą zmiennej typu Node. Należy zapewnić, że wartość
zmiennej to albo nuli, albo referencja do obiektu typu Node, którego pole next jest re
ferencją do listy powiązanej. Przykładowo, aby zbudować listę powiązaną obejmującą
elementy to, be i or, m ożna utworzyć obiekt typu Node dla każdego elementu:
Node first = new Node();
Node second = new Node();
Node th ird = new NodeQ;
Następnie w każdym węźle trzeba ustawić pole Node f i r s t = new Node();
item na pożądaną wartość (dla uproszczenia f i r s t .i tem = " t o " ;
załóżmy, że Item to S tri ng): fi rst
first, item = "t o "; to
nuli
[Link] = "be";
th ird .ite m = " o r ";
Node second = new NodeO ;
oraz ustawić pola next na listę powiązaną: s e c o n d .i tem = " b e " ;
f i r s t . n e x t = second;
[Link] = second;
[Link] = th ird ;
Zauważmy, że t h i r d . next nadal ma wartość
null, zainicjowaną w czasie tworzenia obiek
tu. W efekcie thi rd to lista powiązana (jest to
Node t h i r d = new Node();
referencja do węzła z referencją do nuli, czyli th ird .ite m = "o r";
do pustej listy powiązanej), podobnie jak se seco n d .n e xt = t h i r d ;
cond (jest to referencja do węzła z referencją fi rst second
do thi rd, czyli do listy powiązanej) i first (jest thi rd
to referencja do węzła z referencją do second,
czyli do listy powiązanej). Analizowany kod
wykonuje przypisania w innej kolejności, co
pokazano na rysunku na tej stronie. wiązanie listy
elementów. W opisanym przykładzie first
l is t a p o w ią z a n a r e p r e z e n t u j e c ią g
reprezentuje ciąg to be or. Ciąg elementów m ożna też zapisać jako tablicę. Na przy
kład można użyć kodu:
S tri ng [] s = { " t o " , "be", "o r " };
do przedstawienia tego samego ciągu łańcuchów znaków. Różnica polega na tym,
że łatwiej jest wstawiać i usuwać elementy przy korzystaniu z listy powiązanej. Dalej
omówiono kod wykonujący te zadania.
156 R O Z D Z IA L I o Podstawy
W czasie śledzenia kodu opartego na listach i innych powiązanych strukturach ko
rzystamy z wizualnej reprezentacji, w której:
■ Prostokąty reprezentują obiekty.
■ W prostokątach znajdują się wartości zmiennych egzemplarza.
■ Strzałki reprezentują referencje i prowadzą do wskazywanych obiektów.
Ta wizualna reprezentacja ujmuje kluczowe cechy list powiązanych. Z uwagi na
zwięzłość referencje do węzłów nazywamy odnośnikami. Jeśli wartości elementów
to łańcuchy znaków (tak jak w przykładach), dla uproszczenia umieszczamy łań
cuch w prostokącie obiektu, zamiast stosować precyzyjniejszą grafikę, reprezentu
jącą obiekt łańcucha znaków i tablicę znaków, jak opisano to w p o d r o z d z i a l e 1 .2 .
Zastosowana wizualna reprezentacja umożliwia skupienie się na odnośnikach.
W staw ianie na początek Najpierw załóżmy, że należy wstawić nowy węzeł do listy
powiązanej. Najłatwiej zrobić to na początku listy. Przykładowo, aby wstawić łańcuch
znaków not na początek listy powiązanej, której pierwszy węzeł to first, należy zapi
sać first w ol dfirst, przypisać do first nowy obiekt typu Node i przypisać do pola i tem
wartość not, a do pola next — element ol dfirst. Kod wstawiający węzeł na początek
listy powiązanej obejmuje tylko kilka instrukcji przypisania, dlatego czas tej operacji
jest niezależny od długości listy.
Z apisyw anie o d n o śn ik a do listy
Node o l d f i r s t = f i r s t ;
o l d fi r s t
Tw orzenie n o w eg o w ęzła p o czątkow ego
f i r s t = new Node() ;
o ld f i r s t
U staw ianie zm iennych eg zem p larza w now ym w ęźle
f i r s t . item = "not";
f i r s t . next = o l d f i rs t ;
Wstawianie nowego węzła na początek listy powiązanej
1.3 ■ Wielozbiory, kolejki i stosy 1 57
U suw anie z p o c z ą tk u Teraz załóżmy, że należy f i r s t = f i r s t . n e x t ;
usunąć pierwszy węzeł z listy. Operacja ta jest jesz
cze prostsza — wystarczy przypisać do first war to
be
or
tość first.n ex t. Zwykle przed przypisaniem należy null
pobrać wartość elementu (przez zapisanie jej do
zmiennej typu Item), ponieważ po zmianie wartości fir s t
zmiennej first dostęp do wskazywanego przez nią
wcześniej węzła może być niemożliwy. Standardowo
obiekt węzła staje się osierocony, a system zarządza U su w a n ie p ie rw s z e g o w ę z ła listy p o w ią z a n e j
nia pamięcią Javy odzyskuje zajmowaną przez obiekt
pamięć. Opisana operacja obejmuje tylko jedną instrukcję przypisania, dlatego czas
jej wykonania nie zależy od długości listy.
W sta w ia n ie n a ko n iec Jak można dodać węzeł na koniec listy powiązanej ? Potrzebny
jest do tego odnośnik do ostatniego węzła listy, ponieważ odnośnik tego węzła trzeba
zmienić, tak aby wskazywał nowy węzeł, zawierający wstawiany element. Pisząc kod
listy powiązanej, warto przemyśleć przechowywanie dodatkowego odnośnika, ponie
waż każda m etoda modyfikująca listę musi sprawdzać, czy zmienna z tym odnośni
kiem nie wymaga modyfikacji, a także wprowadzać niezbędne zmiany. Przykładowo,
opisany wcześniej kod do usuwania pierwszego węzła listy może wymagać zmiany
referencji do ostatniego węzła, ponie
Z apisyw anie odnośnika d o o s ta tn ie g o w ęzła
waż kiedy lista zawiera tylko jeden
Node o l d l a s t = l a s t ;
węzeł, jest on jednocześnie pierwszym
i ostatnim! Ponadto kod nie zadziała
(podąży za pustym odnośnikiem), jeśli
lista jest pusta. Szczegóły tego rodzaju
sprawiają, że diagnozowanie kodu list
powiązanych jest zawsze trudne. Tw orzenie n o w ego o s ta tn ie g o w ęzła
Node l a s t = new N o d e O ;
W sta w ia n ie i u su w a n ie n a innych
la [Link] m = " n o t " ;
p o zycja ch W skrócie pokazano, że
poniższe operacje na liście powiązanej
m ożna zaimplementować za pomocą
tylko kilku instrukcji, pod warunkiem
że istnieje dostęp do odnośnika first D ołączanie no w ego w ęzła na koniec listy
(do pierwszego elementu) i la s t (do o l d l a s t . next = l a s t ;
ostatniego elementu). Oto te operacje;
" wstawianie na początek,
■ usuwanie z początku,
■ wstawianie na koniec.
W sta w ia n ie n o w e g o w ę zła na k o n ie c listy p o w ią z a n e j
1 58 RO ZD ZIA Ł 1 n Podstawy
Inne operacje, takie jak poniższe, nie są tak proste:
■ usuwanie danego węzła,
■ wstawianie nowego węzła przed dany.
Przykładowo, jak m ożna usunąć ostatni węzeł listy? Odnośnik 1a s t nie jest pomocny,
ponieważ trzeba ustawić odnośnik w poprzednim węźle listy (o tej samej wartości,
co 1ast) na nul 1. Jeśli nie ma innych informacji, jedyne rozwiązanie polega na przej
ściu po całej liście w celu znalezienia węzła z odnośnikiem do 1 a st (zobacz dalszy
tekst i ć w i c z e n i e 1 .3 . 1 9 ). Takie podejście jest niepożądane, ponieważ czas operacji
jest proporcjonalny do długości listy. Standardowe rozwiązanie umożliwiające arbi
tralne wstawianie i usuwanie danych polega na użyciu listy podwójnie powiązanej,
w której każdy węzeł obejmuje dwa odnośniki — po jednym w każdym kierunku.
Opracowanie kodu wspomnianych operacji pozostawiamy jako ćwiczenie (zobacz
ć w i c z e n i e 1 .3 .3 1 ). W implementacjach z tej książki listy podwójnie powiązane nie
są potrzebne.
Przechodzenie Do sprawdzania każdego elementu tablicy służy znany kod, taki jak
poniższa pętla do przetwarzania elementów tablicy a []:
fo r ( in t i = 0; i < N; i++)
{
// Przetwarzanie a [ i ].
}
Istnieje podobny idiom do sprawdzania elementów z listy powiązanej. Należy zai
nicjować zmienną indeksującą pętli, x, referencją do pierwszego obiektu Node na li
ście powiązanej. Następnie trzeba znaleźć element powiązany z x, pobierając wartość
[Link], a potem zmodyfikować x, żeby prowadziła do następnego obiektu Node listy
powiązanej (do zmiennej należy przypisać wartość [Link]). Proces ten jest powta
rzany dopóty, dopóki x ma wartość różną od nul 1. Wartość nul 1 oznacza dojście do
końca listy powiązanej. Proces ten to przechodzenie po liście. Można go zwięźle za
pisać za pomocą kodu podobnego do poniższej pętli, przetwarzającej elementy listy
powiązanej, w której do pierwszego elementu prowadzi zmienna first:
fo r (Node x = first; x != n u li; x = [Link])
{
// Przetwarzanie [Link].
}
Idiom ten jest tak naturalny, jak standardowy idiom do iterowania po elementach
tablicy. W implementacjach w tej książce używamy go jako podstawy dla iteratorów,
aby umożliwić w kodzie klienta iterowanie po elementach bez znajomości szczegó
łów implementacji listy powiązanej.
1.3 a Wielozbiory, kolejki i stosy 159
Implementacja stosu Opracowanie implementacji interfejsu API typu Stack z wyko
rzystaniem tych wstępnych informacji jest proste, co zademonstrowano w a l g o r y t m i e
1.2 na stronie 161. Algorytm przechowuje stos w postaci listy powiązanej. Wierzchołek
stosu znajduje się na początku listy i prowadzi do niego zmienna egzemplarza first.
Dlatego aby umieścić element na stosie (metoda push()), należy dodać go na począ
tek listy, używając kodu opisanego na stronie 156. Zdjęcie elementu (metoda pop())
wymaga usunięcia go z początku listy za pomocą kodu omówionego na stronie 157.
Metoda s i ze () wymaga śledzenia liczby elementów w zmiennej egzemplarza N, zwięk
szania jej w momencie dodawania elementu i zmniejszania w trakcie zdejmowania.
W implementacji metody i sEmpty () trzeba sprawdzić, czy zmienna first m a wartość
nuli (można też sprawdzić, czy N jest równe 0). W implementacji użyto typu gene-
rycznego Item. Fragment <Item> po nazwie klasy oznacza, że każde wystąpienie Item
w implementacji jest zastępowane nazwą typu danych podaną w kliencie (zobacz stro
nę 146). Na razie pomijamy kod do obsługi iterowania — omawiamy go na stronie
167. Na następnej stronie pokazano ślad działania klienta testowego. Zastosowanie list
powiązanych pozwala tu zrealizować optymalne cele projektowe:
° Rozwiązania można używać dla dowolnego typu danych.
° Ilość zajmowanej pamięci jest zawsze proporcjonalna do wielkości kolekcji.
D Czas wykonywania operacji jest zawsze niezależny od wielkości kolekcji.
Przedstawiona implementacja jest prototypem implementacji wielu omawianych algo
rytmów. Zdefiniowano tu strukturę danych w postaci listy powiązanej i zaimplemento
wano metody dla klientów, push () i pop (), w których pożądane efekty udało się osiąg
nąć za pomocą kilku wierszy kodu. Algorytmy i struktury danych są ze sobą powiąza
ne. Tu kod implementacji algorytmu jest dość prosty, jednak cechy struktury danych
są bardziej skomplikowane i wymagały wyjaśnienia na kilku wcześniejszych stronach.
Zależność między definicją struktury danych i implementacją algorytmu jest typowa.
Koncentrujemy się na niej w implementacjach typów ADT w tej książce.
K lie n t testowy dla typu Stack
p u b lic s t a t ic void main ( S t r in g [] args)
{ // Tworzenie sto su i dodawanie/zdejmowanie łańcuchów znaków
// zgodnie z instrukcjam i ze Std ln .
Sta c k < S trin g > s = new S t a c k < S t r in g > ( );
w h ile ( IS td ln .is E m p ty ())
f
S t r in g item = S t d ln . r e a d S t r in g f ) ;
i f (lite m .e q u a lsC 1- 1') )
s .p u sh (ite m );
e lse i f (![Link] m p ty ()) Std O u t.p rin t(s.p o p () + " ") ;
}
S t d O u t . p r in t ln ( "(elementy na s t o s ie : " + s . s i z e ( ) +
}
Klient te s to w y d la ty p u S tack
160 RO ZD ZIA Ł 1 □ Podstawy
Ślad działania wspomagającego rozwijanie aplikacji klienta klasy Stack
1.3 Wielozbiory, kolejki i stosy 161
A L G O R Y T M 1.2. S t o s — im p le m e n ta c ja z w y k o rz y s ta n ie m listy p o w ią za n e j
public c la s s Stack<Item> implements Iterable<Item>
p rivate Node first; // Wierzch stosu (ostatnio dodany węzeł),
p rivate in t N; // Liczba elementów.
private c la s s Node
{ // Klasa zagnieżdżona z definicją węzłów.
Item item;
Node next;
)
public boolean isEmptyQ { return first == n u ll; } // Lub N == 0.
public in t s iz e ( ) { return N; }
p ublic void push(Item item)
( // Umieszczanie elementu na wierzchu stosu.
Node oldfirst = first;
first = new Node();
first, item = item;
[Link] = oldfirst;
N++;
}
public Item pop()
( // Zdejmowanie elementu z wierzchu stosu.
Item item = first, i tern;
first = [Link];
N—;
return item;
}
// Implementacja metody i t e r a t o r Q znajduje s ię na stro n ie 167.
// K lie n t testowy main() znajduje się na stro n ie 159.
}
Ta generyczna implementacja klasy Stack oparta jest na liście powiązanej. Implementacja
ta pozwala tworzyć stosy z danymi dowolnego typu. Aby zapewnić obsługę iteracji, należy
dodać wyróżniony kod, opisany dla typu
Bag n a stronie 167. % more [Link]
to be or not to - be - - that - - - is
% java Stack < to b e .txt
to be not that or be (elementy na s t o s ie : 2)
1 62 R O Z D Z IA L I □ Podstawy
Im plem entacja kolejki Implementacja interfejsu API opracowanego przez nas typu
Queue oparta na liście powiązanej także jest prosta, co pokazano w a l g o r y t m i e 1.3
na następnej stronie. Kolejka jest przechowywana na liście powiązanej w kolejności
od najdawniej do ostatnio dodanego elementu. Do początku kolejki prowadzi zmien
na egzemplarza first, a do końca — zmienna egzemplarza la s t. Dlatego aby dodać
element do kolejki (m etoda e n qu e u e ()), należy umieścić go na końcu listy, używa
jąc kodu opisanego na stronie 157, rozwiniętego tak, aby ustawiał first i la s t na
nowy węzeł, jeśli lista jest pusta. W celu usunięcia elementu (metoda dequeue ()) na
leży skasować go z początku listy, używając tego samego kodu, co dla m etody pop ()
w klasie Stack, wzbogaconego o aktualizację zmiennej 1a st, kiedy lista staje się pusta.
Implementacje m etod si ze () i isEmptyO są takie same jak w klasie Stack. Tu, po
dobnie jak w implementacji klasy Stack, użyto generycznego param etru typu Item
i pominięto kod do obsługi iterowania, omówiony w ramach implementacji klasy Bag
na stronie 167. Dalej pokazano klienta wspomagającego tworzenie aplikacji podob
nego do tego dla klasy Stack. Na następnej stronie znajduje się ślad działania klienta.
W implementacji wykorzystano tę samą strukturę danych, co w klasie Stack (listę
powiązaną), jednak zaimplementowano inne algorytmy do dodawania i usuwania
elementów, co z perspektywy klienta robi różnicę między porządkiem LIFO i FIFO.
Także tu zastosowanie listy powiązanej pozwala zrealizować cele projektowe — roz
wiązanie m ożna wykorzystać dla dowolnego typu danych, ilość potrzebnej pamięci
jest proporcjonalna do liczby elementów w kolekcji, a czas potrzebny na wykonanie
operacji jest zawsze niezależny od rozmiaru kolekcji.
p u b lic s t a t ic void m a in (S trin g [] a rgs)
( // Tworzenie k o le jk i oraz dodawanie do nie j i usuwanie z n ie j łańcuchów znaków.
Queue<String> q = new Q u e u e < Strin g > ();
w hile (IS td ln .is E m p t y O )
{
S t r in g item = S t d ln . r e a d S t r in g O ;
i f (lite m .e q u a ls ( " - " ) )
[Link](item );
e lse i f (!q .isE m p ty O ) Std O u t.p rin t(q .d e q u e u e d + " " ) ;
1
Std O u t.p rin tln ("(e le m e n ty w kolejce: " + q . s iz e Q +
1
Klient testowy dla klasy Queue
% more to b e .txt
to be o r not t o - b e - - that - - - i s
% java Queue < to b e .txt
to be o r not to be (elementy w kolejce: 2)
1.3 Wielozbiory, kolejki i stosy 163
ALGORYTM 1.3. Kolejka FIFO
public c la s s Queue<Item> implements Iterable<Item>
{
private Node first; // Odnośnik do najdawniej dodanego węzła,
p rivate Node la s t ; // Odnośnik do osta tn io dodanego węzła,
p rivate in t N; // Liczba elementów w kolejce.
private c la ss Node
{ // Klasa zagnieżdżona z definicją węzłów.
Item item;
Node next;
}
public boolean isEmptyO { return first == n u ll; } // Lub N == 0.
public in t s iz e ( ) { return N; }
public void enqueue(Item item)
{ // Dodawanie elementu na koniec l i s t y .
Node o ld la s t = la s t ;
1ast = new Node();
l a s t . i tern = item;
la s t .n e x t = n u l l ;
i f (isEmptyO) first = la s t ;
else old la st .n e x t = la s t ;
N++;
}
public Item dequeue()
{ // Usuwanie elementu z początku l i s t y .
Item item = first, i tern;
first = [Link];
i f (isEmptyO) la s t = n u ll;
N— ;
return item;
}
// Implementacja metody it e r a t o r ( ) znajduje s ię na stro n ie 167.
// K lie n t testowy main() znajduje s ię na stro n ie 162.
}
Ta generyczna implementacja klasy Queue oparta jest na liście powiązanej. Implementacji
można używać do tworzenia kolejek zawierających dane dowolnego typu. Aby zapewnić ob
sługę iteracji, należy dodać wyróżniony kod, opisany dla klasy Bag na stronie 167.
164 RO ZD ZIA Ł 1 □ Podstawy
Ślad działania wspomagającego tworzenie aplikacji klienta klasy Queue
1.3 n Wielozbiory, kolejki i stosy
p o w i ą z a n e s ą g ł ó w n ą a l t e r n a t y w ą dla tablic przy określaniu struktu
l is t y
ry kolekcji danych. Możliwość ta jest dostępna dla programistów od dziesięcioleci.
Ważnym m om entem w historii języków programowania było opracowanie przez
Johna McCarthyego w latach 50. ubiegłego wieku języka LISP. W języku tym listy po
wiązane były podstawowymi strukturam i dla programów i danych. Programowanie
z wykorzystaniem list powiązanych rodzi wiele problemów i powoduje powstawanie
kodu trudnego do zdiagnozowania, czego dowodzą ćwiczenia. We współczesnym
kodzie bezpieczne wskaźniki, automatyczne przywracanie pamięci (zobacz stro
nę 123) i typy ADT umożliwiają ukrycie kodu do przetwarzania list w kilku ldasach
podobnych do tych opisanych w tym miejscu.
166 R O Z D Z IA L I a Podstawy
Im plem entacja w ielozbiorów Implementacja interfejsu API typu Bag za pomocą
listy powiązanej wymaga tylko zmiany nazwy m etody push () z klasy Stack na add ()
i usunięcia implementacji m etody pop (), co pokazano w a l g o r y t m i e 1.4 na na
stępnej stronie (zastosowanie tego samego podejścia do klasy Queue też jest możliwe,
ale wymaga więcej kodu). W implementacji wyróżniono kod umożliwiający iterowa-
nie po klasach Stack, Queue i Bag przez przechodzenie po liście. W klasie Stack lista
ma porządek LIFO. Dla klasy Queue zastosowano porządek FIFO. Dla wielozbiorów
obowiązuje porządek LIFO, ale nie ma to znaczenia. Jak pokazano w kodzie wyróż
nionym w a l g o r y t m i e 1 .4 , przy implementowaniu iterowania po kolekcji pierwszy
krok polega na dołączeniu fragmentu:
import j a v a . u t i l . I t e r a t o r ;
Dzięki temu w kodzie m ożna używać interfejsu I te r a to r Javy. Drugi krok to dodanie
do deklaracji klasy kodu:
implements Iterable<Item>
Jest to „obietnica” udostępnienia metody i te r a to r (). Metoda ta zwraca obiekt klasy
z implementacją interfejsu Ite ra to r:
public Iterator<Item> it e r a t o r ! )
{ return new L i s t I t e r a t o r ( ) ; }
Kod ten to zapewnienie, że zaimplementowana zostanie klasa z m etodam i hasNext (),
next() i remove() wywoływanymi, kiedy klient używa techniki foreach. Aby m oż
na było zaimplementować te metody, w klasie zagnieżdżonej Li s t I te r a to r w a l g o
r y t m i e 1.4 umieszczono zmienną egzemplarza current, zawierającą bieżący węzeł
listy. M etoda hasNext() sprawdza, czy zmienna current ma wartość n u li, a metoda
next() zapisuje referencję do aktualnego elementu, aktualizuje zmienną current tak,
aby wskazywała na następny węzeł listy, i zwraca zapisaną referencję.
1.3 Wielozbiory, kolejki i stosy 1 67
ALGORYTM 1.4. Wielozbiór
import j a v a . u t i l . I t e r a t o r ;
public c la s s Bag<Item> implements Iterable<Item>
{
p rivate Node first; // Pierwszy węzeł na l i ś c i e .
p rivate c la s s Node
{
Item item;
Node next;
}
p ublic void add(Item item)
{ // To samo, co w metodzie push() k lasy Stack.
Node oldfirst = first;
first = new N odeQ ;
[Link] = item;
[Link] = oldfirst;
}
public Iterator<Item> i t e r a t o r ()
{ return new L i s t I t e r a t o r ( ) ; }
private c la s s L i s t l t e r a t o r implements Iterator<Item>
{
p rivate Node current = first;
p ublic boolean hasNext()
{ return current != n u ll; }
p ublic void remove() { )
p ublic Item next()
(
Item item = [Link];
current = [Link];
return item;
}
}
}
W tej implementacji klasy Bag przechowywana jest lista powiązana elementów podanych
w wywołaniach metody add(). Kod metod isEmpty() i s iz e () jest taki sam, jak w kla
sie Stack, dlatego go pominięto. Iterator przechodzi po liście, zachowując aktualny węzeł
w zmiennej current. Można umożliwić przechodzenie po klasach Stack i Queue, dodając
wyróżniony kod do a l g o r y t m ó w i . i i 1 .2 , ponieważ w klasach tych użyto tej samej struk
tury danych, przy czym lista przechowywana jest w kolejności LIFO i FIFO.
168 R O Z D Z IA L I o Podstawy
Przegląd Opisane w tym podrozdziale implementacje wielozbiorów, kolejek i sto
sów z obsługą typów generycznych oraz iterowania zapewniają poziom abstrakcji
umożliwiający pisanie zwięzłych programów klienckich do manipulowania kolek
cjami obiektów. Szczegółowe zrozumienie omówionych typów ADT jest ważne jako
wprowadzenie do analiz algorytmów i struktur danych. Wynika to z trzech przyczyn.
Po pierwsze, przedstawione typy danych służą w tej książce jako cegiełki do budowa
nia struktur danych wyższego poziomu. Po drugie, ilustrują zależności między struk
turam i danych i algorytmami oraz trudności z realizacją naturalnych celów związa
nych z wydajnością, które czasem są sprzeczne. Po trzecie, w kilku implementacjach
skoncentrowano się na typach ADT obsługujących bardziej rozbudowane operacje
na kolekcjach obiektów. Także przy tworzeniu tych typów jako punkt wyjścia wyko
rzystano opisane tu implementacje.
S tru ktu ry danych Przedstawiono już dwa sposoby reprezentowania kolekcji obiek
tów — za pomocą tablic i list powiązanych. Tablice są wbudowane w Javę, a listy po
wiązane można łatwo zbudować za pom ocą standardowych rekordów Javy. Te dwie
możliwości, czasem nazywane przydziałem sekwencyjnym i przydziałem listowym,
to podstawowe techniki. W dalszej części książki opracowano implementacje typów
ADT, w których na różne sposoby połączono i rozwinięto te podstawowe struktu
ry. Ważnym rozszerzeniem jest stosowanie wielu odnośników dla struktur danych.
Przykładowo, w p o d r o z d z i a ł a c h 3.2 i 3.3 skoncentrowano się na drzewach binar
nych, zbudowanych z węzłów, z których każdy posiada dwa odnośniki. Innym waż
nym rozwinięciem jest kompozycja struktur danych. Można utworzyć wielozbiór sto
sów, kolejkę tablic itd. W r o z d z i a l e 4 . skoncentrowano się na przykład na grafach,
które przedstawiono jako tablice wielozbiorów. W ten sposób m ożna bardzo łatwo
definiować struktury danych o dowolnej złożoności. Ważnym powodem koncentro
wania się na abstrakcyjnych typach danych jest próba kontrolowania tej złożoności.
Struktura danych Zalety Wady
Indeks zapewnia natychmiastowy Trzeba znać wielkość
Tablica
dostęp do każdego elementu w czasie inicjowania
Ilość zajmowanej pamięci jest Dostęp do elementu
Lista powiązana
proporcjonalna do wielkości wymaga referencji
P o d s ta w o w e s tr u k tu r y d a n y c h
1.3 b Wielozbiory, kolejki i stosy 169
SPOSÓB PRZEDSTAW IEN IA WIELOZBIORÓW , KOLEJEK I STOSÓW W tym podrozdzia-
le jest prototypowym przykładem podejścia stosowanego w książce do opisywania
struktur danych i algorytmów. Omawiając nowy obszar zastosowań, przedstawiamy
trudności z dziedziny przetwarzania i używamy abstrakcji danych do ich przezwycię
żenia. Wykonujemy przy tym następujące kroki:
o Określenie interfejsu API.
■ Opracowanie kodu klienta na podstawie konkretnych zastosowań.
■ Opisanie struktury danych (reprezentacji zbioru wartości), która posłuży jako
podstawa dla zmiennych egzemplarza w klasie z implementacją typu ADT zgod
ną ze specyfikacją interfejsu API.
■ Opisanie algorytmów (sposobów implementowania zbiorów operacji), które
posłużą jako podstawa do zaimplementowania w klasie metod egzemplarza.
■ Analiza cech algorytmów w obszarze wydajności.
W następnym podrozdziale omówiono szczegółowo ostatni krok, ponieważ często
wyznacza on, które algorytmy i implementacje są najbardziej przydatne w praktycz
nych zastosowaniach.
Struktur danych Podrozdział Typ ADT Reprezentacja
Drzewo z odnośnikiem
1.5 Uni onFi nd Tablica liczb całkowitych
do rodzica
Binarne drzewo
3.2, 3.3 BST Dwa odnośniki na węzeł
wyszukiwań
Tablica, przesunięcie
Łańcuch znaków 5.1 S t rin g
i długość
Sterta binarna 2.4 PQ Tablica obiektów
Tablica z haszowaniem
3.4 SeperateChai ni ngHashST Tablica list powiązanych
(metoda łańcuchowa)
Tablica z haszowaniem
3.4 Li nearProbi ngHashST Dwie tablice obiektów
(badanie liniowe)
Listy sąsiedztwa Tablica obiektów
4.1, 4.2 Graph
dla grafów typu Bag
Węzeł z tablicą
Drzewo trie 5.2 TrieST
odnośników
Trójkowe drzewo trie 5.3 TST Trzy odnośniki na węzeł
Przykładowe struktury danych rozwijane w książce
17 0 R O Z D Z IA L I o Podstawy
PYTANIA I ODPOW IEDZI
P. Nie wszystkie języki programowania (w tym wczesne wersje Javy) udostępniają
typy generyczne. Jakie są inne możliwości?
O. Jedną z możliwości jest utrzymywanie różnych implementacji każdego typu da
nych, o czym wspom niano w tekście. Inne podejście to zbudowanie stosu wartości
typu Object i rzutowanie na odpowiedni typ w kodzie klienta w miejscu wywołania
m etody pop ( ) . Problem z tym podejściem polega na tym, że błędy niedopasowania
typów m ożna wykryć dopiero w czasie wykonywania programu. Natomiast przy
stosowaniu typów generycznych kod umieszczający na stosie obiekt złego typu,
na przykład:
Stack<Apple> stack = new Stack<Apple>();
Apple a = new A p p le ( ) ;
Orange b = new Orange();
[Link]( a ) ;
s ta c k .p u sh (b); // Błąd czasu kompilacji.
spowoduje błąd czasu kompilacji:
push(Apple) in Stack<Apple> cannot be applied to (Orange)
Możliwość wykrywania talach błędów w czasie kompilacji to wystarczający powód
do stosowania typów generycznych.
P. Dlaczego w Javie niedozwolone są generyczne tablice?
O. Eksperci wciąż dyskutują nad tą kwestią. Możliwe, że będziesz musiał zostać jed
nym z nich, aby to zrozumieć! Początkujący powinni zapoznać się z tablicami kowa-
riantnymi (ang. covariance array) i wymazywaniem typów (ang. type erasure).
P. Jak m ożna utworzyć tablicę stosów łańcuchów znaków?
O. Należy zastosować rzutowanie, takie jak poniżej:
Stack<String>[] a = (S t a c k < S trin g > []) new StackjN];
Ostrzeżenie: takie rzutowanie w kodzie klienta różni się od tego opisanego na stro
nie 146. Możliwe, że spodziewałeś się użycia typu Object zamiast Stack. Przy sto
sowaniu typów generycznych Java sprawdza bezpieczeństwo typów na etapie kom
pilacji, jednak w czasie wykonania programu nie korzysta z uzyskanych informacji,
dlatego dostępna jest struktura Stack<Object>[] (w skrócie Stack[]), którą trzeba
zrzutować na typ Stack<String>[].
1.3 Q Wielozbiory, kolejki i stosy 171
P. Co się dzieje, kiedy program wywołuje pop () dla pustego stosu?
O. Zależy to od implementacji. W opracowanej przez nas implementacji ze strony
161 zgłoszony zostanie wyjątek Nul 1Poi nterExcepti on. W implementacjach z witry
ny zgłaszany jest wyjątek czasu wykonania, pomagający użytkownikom zlokalizować
błąd. Ogólnie w kodzie, który m a być używany przez wiele osób, warto stosować tyle
testów, ile to możliwe.
P. Po co przejmować się zmienianiem wielkości tablicy, skoro można użyć list po
wiązanych?
O. Dalej opisano kilka implementacji typów ADT, w których trzeba użyć tablic do
wykonania operacji, jakich nie da się łatwo zrealizować za pom ocą list powiązanych.
Klasa ResizingArrayStack to model kontrolowania wykorzystania pamięci zajmo
wanej przez tablice.
P, Dlaczego Node zadeklarowano jako klasę zagnieżdżoną z modyfikatorem pri vate?
O. Zadeklarowanie klasy zagnieżdżonej Node jako prywatnej (private) sprawia, że
dostęp do metod i zmiennych egzemplarza ma tylko klasa zewnętrzna. Jedną z cech
prywatnych klas zagnieżdżonych jest to, że ze zmiennych egzemplarza można bez
pośrednio korzystać w klasie zewnętrznej, ale już nigdzie indziej. Dlatego nie trzeba
deklarować zmiennych egzemplarza za pom ocą modyfikatorów publ i c lub pri vate.
Uwaga dla ekspertów: klasy zagnieżdżone, które nie są statyczne, to klasy wewnętrzne.
Dlatego technicznie klasy Node są wewnętrzne, choć klasy, które nie są generyczne,
mogą być statyczne.
P. Po wpisaniu instrukcji ja vac Stack, java w celu uruchomienia a l g o r y t m u 1 . 2
i podobnych programów powstają pliki [Link] i Stack$[Link]. Jakie jest prze
znaczenie tego drugiego?
O. Jest to plik klasy wewnętrznej Node. Konwencje nazewnicze Javy wymagają użycia
znaku $ do rozdzielenia nazw klas — zewnętrznej i wewnętrznej.
P. Czy istnieją biblioteki Javy dla stosów i kolejek?
O. I tak, i nie. Java posiada wbudowaną bibliotekę o nazwie ja va. ut i 1. Stack, jednak
należy jej unikać, jeśli potrzebny jest stos. Posiada ona kilka dodatkowych operacji
(na przykład pobieranie i-tego elementu), które zwykle nie są powiązane ze stosem.
Ponadto umożliwia umieszczenie elementu na dole stosu (zamiast na wierzchu), dla
tego można zaimplementować w ten sposób kolejkę! Choć dodatkowe operacje mogą
wydawać się korzystne, w rzeczywistości są wadą. Stosujemy typy danych nie tylko
jako biblioteki wszystkich możliwych operacji, ale też jako mechanizm do precyzyj
nego określania potrzebnych operacji. Podstawową zaletą takiego podejścia jest to,
17 2 R O Z D Z IA L I □ Podstawy
PYTANIA I ODPOWIEDZI (ciągdalszy)
że system może zapobiec wykonaniu niepotrzebnych operacji. Interfejs API biblio
teki ja v a .u til .Stack to przykład szerokiego interfejsu. Zwykle staramy się unikać
interfejsów tego rodzaju.
P. Czy należy umożliwiać klientom wstawianie elementów nul 1 do stosu lub kolejki?
O. To pytanie często pojawia się przy implementowaniu kolekcji w Javie. W imple
mentacjach z tej książki (i w bibliotekach Javy do obsługi stosów oraz kolejek) wsta
wianie wartości nul 1 jest dozwolone.
P. Jak powinien działać iterator klasy Stack, kiedy klient wywoła push () lub pop()
w trakcie iterowania?
O. Należy zgłosić wyjątek ja v a .u til .ConcurrentModificationException, aby utwo
rzyć iterator z szybkim przeryw a n iem działania. Zobacz ćwiczenie 1.3.50.
P. Czy można używać pętli foreach dla tablic?
O. Tak, choć tablice nie obejmują implementacji interfejsu Ite ra b le . Poniższy jed-
nowierszowy kod wyświetla argumenty z wiersza poleceń:
public s t a t i c void m ain(String[] args)
{ fo r (S trin g s : args) S t d O u t . p r in t ln ( s ) ; }
P. Czy można używać pętli foreach dla łańcuchów znaków?
O. Nie, klasa S tring nie zawiera implementacji interfejsu Iterab le.
P. Dlaczego nie warto tworzyć jednego typu danych Col 1e c ti on, który obejmował
by implementację m etod do dodawania elementów, usuwania ostatnio wstawionego,
usuwania najdawniej wstawionego, usuwania dowolnego, zwracania liczby elemen
tów w kolekcji i wykonywania wszystkich innych potrzebnych operacji? Pozwoliłoby
to zaimplementować wszystkie m etody w jednej klasie używanej w wielu klientach.
O. To także przykład szerokiego interfejsu. Java udostępnia implementacje tego ro
dzaju w klasach j a v a . u t il . A r r a y L is t i j a v a . u t il .LinkedList. Jedną z przyczyn
unikania tego podejścia jest to, że nie ma pewności, iż wszystkie operacje są wy
dajnie zaimplementowane. W książce używamy interfejsów API jako punktu wyj
ścia do projektowania wydajnych algorytmów i struktur danych. Zdecydowanie
łatwiej uzyskać ten efekt dla interfejsów, które obejmują nieliczne operacje. Innym
powodem stosowania wąskich interfejsów jest wymuszanie przez nie pewnej dy
scypliny w program ach klienckich. Znacznie ułatwia to zrozum ienie kodu klienta.
Jeśli jeden klient używa typu S tac k< Stri ng>, a inny — typu Queue<Transaction>,
wiadomo, że w pierwszym potrzebny jest porządek LIFO, a w drugim — porządek
FIFO.
1.3 h Wielozbiory, kolejki i stosy 173
I ĆWICZENIA
1.3.1. Dodaj metodę i sFu 11 () (czyli „jest pełny”) do klasy FixedCapacityStackOf
Strings.
1.3.2. Podaj dane wyjściowe wyświetlane przez instrukcję java Stack dla danych
wejściowych:
i t was - the best - of times - - - i t was - the - -
1 .3.3. Załóżmy, że klient wykonuje ciąg wymieszanych operacji dodaj i zdejmij na
stosie. Operacje dodaj powodują dodawanie do stosu kolejno liczb całkowitych od 0
do 9. Operacje zdejmij wyświetlają zwracane wartości. Który z poniższych ciągów nie
jest możliwy?
a. 4 3 2 1 0 9 8 7 6 5
b. 4 6 8 7 5 3 2 9 0 1
c. 2 5 6 7 4 8 9 3 1 0
d. 4 3 2 1 0 5 6 7 8 9
e. 1 2 3 4 5 6 9 8 7 0
f. 0 4 6 5 3 8 1 7 2 9
g. 1 4 7 9 8 6 5 3 0 2
h. 2 1 4 3 6 5 8 7 9 0
1.3.4. Napisz klienta Parantheses dla stosu. Klient ma wczytywać strum ień teks
towy ze standardowego wejścia i używać stosu do określenia, czy nawiasy są odpo
wiednio zagnieżdżone. Przykładowo, program powinien wyświetlić tru e dla ciągu
[()]{ } { [()()]()} i fa lse dla [(]).
1.3.5. Co wyświetli poniższy fragment kodu dla Nrównego 50? Podaj wysokopozio-
mowy opis działania kodu dla dodatniej liczby całkowitej N.
Stack<Integer> stack = new Stack< Integer> ();
while (N > 0)
{
[Link](N % 2 );
N = N / 2;
}
fo r (in t d : stack) S td O u t.p rin t(d );
S td 0 u t.p rin tln ( );
Odpowiedź: kod wyświetla binarny zapis N (110010 dla Nrównego 50).
RO ZD ZIA Ł 1 Q Podstawy
ĆWICZENIA (ciąg dalszy)
1.3.6. Jaki efekt dla kolejki q ma wykonanie poniższego kodu?
Stack<String> stack = new S tack<String>();
while ([Link] ptyO )
[Link]([Link]!)) ;
while (¡[Link] m ptyO )
[Link]([Link]( ) );
1.3.7. Dodaj do klasy Stack metodę peek() zwracającą (bez zdejmowania) element
ostatnio wstawiony do stosu.
1.3.8. Podaj zawartość i wielkość tablicy typu Doubl i ngStackOfStrings po wprowa
dzeniu następujących danych wejściowych:
i t was - the best - of times - - - i t was - the - -
1.3.9. Napisz program, który pobiera ze standardowego wejścia wyrażenie bez le
wych nawiasów i wyświetla równoważne wyrażenie infiksowe ze wstawionymi na
wiasami. Na przykład dla danych wejściowych:
1 + 2) * 3 - 4 ) * 5 - 6 ) ) )
program ma wyświetlić:
( ( 1 + 2 ) * ( ( 3 - 4 ) * ( 5 - 6 ) )
1.3.10. Napisz filtr InfixToPostfix przekształcający wyrażenia arytmetyczne z posta
ci infiksowej na postfiksową.
1.3.11. Napisz program Eval uatePostfix, który przyjmuje ze standardowego wejścia
wyrażenie postfiksowe, oblicza je i wyświetla wartość. Połączenie potokowe danych
wyjściowych z programu z poprzedniego ćwiczenia z tym programem daje działanie
podobne do programu Eval uate.
1.3.12. Napisz klienta klasy Stack z możliwością iterowania. Klient ma zawierać
metodę statyczną copy(), która przyjmuje jako argument stos łańcuchów znaków
i zwraca kopię stosu. Uwaga: jest to dobry przykład tego, jak wartościowy jest itera
tor, który tu umożliwia utworzenie potrzebnej m etody bez zmian w podstawowym
interfejsie API.
1.3 a Wielozbiory, kolejki i stosy 175
1.3.13. Załóżmy, że klient wykonuje ciąg wymieszanych operacji dodawania do ko
lejki i usuwania z kolejki. Operacje dodawania do kolejki umieszczają w kolejce ko
lejne liczby całkowite od 0 do 9. Operacje usuwania z kolejki wyświetlają zwracane
wartości. Które z poniższych ciągów nie są możliwe?
a. 0 1 2 3 4 5 6 7 8 9
b. 4 6 8 7 5 3 2 9 0 1
c. 2 5 6 7 4 8 9 3 1 0
d. 4 3 2 1 0 5 6 7 8 9
1.3.14. Napisz klasę Resi zi ngArrayQueueOfStrings, w której abstrakcję kolejki za
implementowano za pomocą tablicy o stałej długości. Następnie rozwiń implemen
tację o możliwość zmiany rozmiaru kolejki, aby zlikwidować ograniczenie jej wiel
kości.
1.3.15. Napisz klienta klasy Queue. Klient ma pobierać argument k z wiersza poleceń
i wyświetlać k-ty od końca łańcuch znaków znaleziony w standardowym wejściu (za
kładamy, że standardowe wejście zawiera k lub więcej łańcuchów znaków).
1.3.16. Używając jako modelu metody re ad ln ts () ze strony 138, napisz metodę sta
tyczną readDates () dla typu Date. Metoda ma wczytywać ze standardowego wejścia
daty w formacie określonym w tabeli na stronie 131 i zwracać zawierającą je tablicę.
1.3.17. Wykonaj ć w ic z e n ie 1 .3.16 dla typu Transaction.
176 R O Z D Z IA L I Q Podstawy
ĆWICZENIA DOTYCZĄCE LIST POWIĄZANYCH
Ćwiczenia z tej listy mają pomóc zdobyć doświadczenie w korzystaniu z list powią
zanych. Oto sugestia — rysunki wykonaj za pomocą wizualnej reprezentacji opisanej
w tekście.
1.3.1 8 . Załóżmy, że x to węzeł listy powiązanej, który nie znajduje się na jej końcu.
Jaki efekt ma wywołanie poniższego fragmentu kodu:
[Link] = x .n ex t.n ex t;
Odpowiedź: kod powoduje usunięcie z listy węzła znajdującego się bezpośrednio po x.
1.3.19. Napisz fragment kodu usuwający ostatni węzeł z listy powiązanej, której
pierwszy węzeł to first.
1.3.20. Napisz metodę del e te (), która przyjmuje argument k typu i nt i usuwa k-ty
element listy powiązanej, jeśli taki istnieje.
1.3.21. Napisz metodę find ( ), która przyjmuje jako argumenty listę powiązaną i łań
cuch znaków key oraz zwraca true, jeśli pole i tem jednego z węzłów listy ma wartość
równą key. W przeciwnym razie m etoda ma zwracać fal se.
1.3.22. Załóżmy, że x to węzeł listy powiązanej. Jak działa poniższy fragment
kodu?
t.n e x t = [Link];
[Link] = t ;
Odpowiedź: wstawia węzeł t bezpośrednio za węzłem x.
1.3.23. Dlaczego poniższy fragment kodu nie działa tak samo, jak kod z poprzed
niego ćwiczenia?
[Link] = t ;
t.n e x t = [Link];
Odpowiedź: w czasie aktualizowania t.n e x t pole [Link] nie prowadzi do węzła znaj
dującego się pierwotnie po x, ale do samego t!
1.3.24. Napisz metodę removeAfter(), która przyjmuje jako argument węzeł listy
powiązanej i usuwa węzeł znajdujący się po podanym (lub nie podejmuje żadnych
działań, jeśli sam argument lub pole next węzła podanego w argumencie to nul 1 ).
1.3.25. Napisz metodę i n sertA fter (), która przyjmuje jako argumenty dwa węzły
listy powiązanej i wstawia drugi po pierwszym na liście tego ostatniego (lub nie wy
konuje żadnych operacji, jeśli choć jeden argument to nul 1 ).
1.3 o Wielozbiory, kolejki i stosy 177
1.3.26. Napisz metodę remove () przyjmującą jako argumenty listę powiązaną i łań
cuch znaków key oraz usuwającą z listy wszystkie węzły, w których pole i tem ma
wartość key.
1.3.27. Napisz metodę max (), która przyjmuje jako argument referencję do pierw
szego węzła listy powiązanej i zwraca wartość maksymalnego klucza z listy. Załóżmy,
że wszystkie klucze to dodatnie liczby całkowite. Jeśli lista jest pusta, m etoda ma
zwracać 0.
1.3.28. Napisz rekurencyjne rozwiązanie poprzedniego ćwiczenia.
1.3.29. Napisz implementację klasy Queue opartą na cyklicznej liście powiązanej,
która wygląda tak samo, jak zwykła lista powiązana, jednak żaden odnośnik nie ma
wartości nul 1 , a kiedy lista nie jest pusta, pole 1 a s t . next prowadzi do węzła first.
Użyj tylko jednej zmiennej egzemplarza typu Node (1 ast).
1.3.30. Napisz funkcję, która jako argument przyjmuje pierwszy węzeł listy powiąza
nej, odwraca kolejność listy (niszcząc ją samą) i zwraca jako wynik pierwszy węzeł.
Rozwiązanie iteracyjne: aby wykonać to zadanie, należy zapisać referencje do trzech
kolejnych węzłów listy powiązanej — reverse, first i second. W każdej iteracji trzeba
pobrać węzeł first z pierwotnej listy powiązanej i wstawić go na początek odwróco
nej listy. Zachowywany jest przy tym niezmiennik, zgodnie z którym first to pierw
szy węzeł z pozostałej części pierwotnej listy, second to drugi węzeł tej listy, a reverse
to pierwszy węzeł wynikowej odwróconej listy.
public Node reverse(Node x)
{
Node first = x;
Node reverse = nul 1;
while (first != null)
{
Node second = first.n e x t;
first.n e x t = reverse;
reverse = first;
first = second;
}
retu rn reverse;
}
178 R O Z D Z IA L I a Podstawy
ĆWICZENIA DOTYCZĄCE LIST POWIĄZANYCH (ciąg dalszy)
W czasie pisania kodu z wykorzystaniem list powiązanych zawsze trzeba uważać,
aby poprawnie obsługiwać sytuacje wyjątkowe (kiedy lista powiązana jest pusta, kie
dy składa się z jednego lub dwóch węzłów) i brzegowe (zarządzanie pierwszym lub
ostatnim elementem). Zwykle jest to dużo trudniejsze od obsługi normalnych przy
padków.
Rozwiązanie rekurencyjne: przy założeniu, że lista ma N węzłów, m ożna rekurencyj-
nie odwrócić ostatnich N - 1 węzłów, a następnie starannie dołączyć pierwszy węzeł
na koniec.
public Node reverse(Node first)
{
i f (first == n u ll) return n u ll;
i f ([Link] == n u ll) return first;
Node second = [Link];
Node rest = reverse(second);
[Link] = first;
[Link] = n u l l ;
return rest;
}
1.3.31. Zaimplementuj klasę zagnieżdżoną Doubl eNode, przeznaczoną do tworzenia
list podwójnie powiązanych, w których każdy węzeł zawiera referencje do poprzed
niego i następnego elementu (jeśli jeden z nich nie istnieje, referencja ma wartość
nuli). Następnie zaimplementuj metody statyczne do wykonywania następujących
zadań: wstawiania na początek, wstawiania na koniec, usuwania z początku, usuwa
nia z końca, wstawiania przed danym węzłem, wstawiania za danym węzłem i usu
wania danego węzła.
1.3 o Wielozbiory, kolejki i stosy 1 79
I PROBLEMY DO ROZWIĄZANIA
1.3.32. Steque. Steque, czyli kolejka zakończona stosem, to typ danych obsługujący
operacje push, pop i enqueue. Określ interfejs API dla takiego typu ADT. Opracuj
implementację opartą na liście powiązanej.
1.3.33. Deque. Deque, czyli kolejka o dwóch końcach, przypomina stos lub kolejkę,
ale umożliwia dodawanie i usuwanie elementów po obu stronach. Deque przechowu
je kolekcję elementów i obsługuje następujący interfejs API:
p u b lic c la s s Deque<Item> implements Ite rable<Ite m >
Deque() Tworzenie pustej deque
boolean isEm ptyO Czy deque jest pusta?
in t s iz e ( ) Liczba elementów w deque
void pushLeft(Item item) Dodawanie elementu po lewej stronie
v oi d pushRi ght (Item i tern) Dodawanie elementu po prawej stronie
Item popLeft() Usuwanie elementu po lewej stronie
Item popR ight() Usuwanie elementu po prawej stronie
Interfejs API dla generycznej kolejki o dwóch końcach
Napisz klasę Deque, używając listy podwójnie powiązanej do zaimplementowania
przedstawionego interfejsu API. Napisz klasę Resi zi ngArrayDeque opartą na techni
ce zmiany wielkości tablicy.
1.3.34. Wielozbiór z dostępem losowym. Wielozbiór z dostępem losowym przechowu
je kolekcję elementów i obsługuje następujący interfejs API:
p u b lic c la s s RandomBag<Item> implements Ite rab le<Ite m >
RandomBag () Tworzenie pustego wielozbioru z dostępem losowym
boolean isEm ptyO Czy wielozbiór jest pusty?
in t s i z e () Liczba elementów w wielozbiorze
void add (Item item) Dodawanie elementu
Interfejs API generycznego wielozbioru z dostępem losowym
Napisz klasę RandomBag z implementacją tego interfejsu API. Zauważ, że interfejs jest
niemal taki sam, jak dla klasy Bag. Różnicą jest słowo random (czyli losowy), oznacza
jące, że iterator powinien zwracać elementy w losowej kolejności (każda z N! permu-
tacji powinna być równie prawdopodobna). Wskazówka: umieść elementy w tablicy
i określ dla nich losową kolejność w konstruktorze iteratora.
180 R O Z D Z IA L I n Podstawy
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
1.3.35. Kolejka z dostępem losowym. Kolejka z dostępem losowym przechowuje ko
lekcję elementów i obsługuje następujący interfejs API:
p u b lic c la s s RandomQueue<Item>
RandomQueue() Tworzenie pustej kolejki z dostępem losowym
boolean isEm pty() Czy kolejka jest pusta?
void enqueue(Item item) Dodawanie elementu
Item dequeue() Usuwanie i zwracanie losowego elementu
(pobieranie bez zwracania do kolejki)
Item sample!) Zwracanie losowego elementu bez usuwania go
(pobieranie ze zwracaniem do kolejki)
Interfejs API generycznej kolejki z dostępem losowym
Napisz klasę RandomQueue z implementacją przedstawionego interfejsu API.
Wskazówka: użyj tablicy (ze zmianą wielkości). Aby usunąć element, należy zamie
nić miejscami element z losowej pozycji (o indeksie między 0 a N-l) i z ostatniej
pozycji (indeks N-l). Następnie trzeba usunąć i zwrócić ostatni obiekt, tak jak w kla
sie ResizingArrayStack. Napisz za pom ocą klasy RandomQueue<Card> klienta, który
rozdaje karty do brydża (po 13 kart dla gracza).
1.3.36. Iterator z dostępem losowym. Napisz iterator dla klasy RandomQueue<Item>
z poprzedniego ćwiczenia. Iterator ma zwracać elementy w losowej kolejności.
1.3.37. Problem Józefa Flawiusza. W pochodzącym z czasów antycznych problemie
Józefa Flawiusza N osób znajduje się w trudnym położeniu i zgadza się zastosować
opisaną dalej strategię w celu zmniejszenia liczebności grupy. Ludzie siadają w kole
(na pozycjach o num erach od 0 do N -l), po czym usuwana jest co M -ta osoba do m o
mentu, w którym pozostaje tylko jedna. Według legendy Józef Flawiusz odkrył, gdzie
powinien usiąść, aby go nie wyeliminowano. Napisz klienta Josephus klasy Queue.
Klient ma pobierać z wiersza poleceń wartości N i M oraz wyświetlać kolejność eli
minowania osób (co pokazuje, gdzie Józef Flawiusz powinien usiąść).
% java Josephus 7 2
1 3 5 0 4 2 6
1.3 a Wiehzbiory, kolejki i stosy
1.3.38. Usuwanie k-tego elementu. Zaimplementuj klasę obsługującą poniższy in
terfejs API:
p ub lic c la s s General i zedQueue<Item>
General i zedQueue () Tworzenie pustej kolejki
boolean isEm pty() Czy kolejka jest pusta?
void in s e rt(Ite m x) Dodawanie elementu
Usuwanie i zwracanie k-tego elementu
Item d e le t e (in t k) „ , , 6
(licząc od najstarszego)
API dla generycznej ogólnej kolejki
Najpierw opracuj implementację opartą na tablicy, a następnie — opartą na liście
powiązanej. Uwaga: algorytmy i struktury danych przedstawione w r o z d z i a l e 3 .
umożliwiają utworzenie implementacji, która gwarantuje, że m etody i n se rt () i de
le te () działają w czasie proporcjonalnym do logarytmu liczby elementów kolejki
(zobacz ć w i c z e n i e 3 . 5 .27 ).
1.3.39. Bufor cykliczny. Bufor cykliczny (inaczej kolejka cykliczna) to struktura da
nych typu FIFO o stałej wielkości N. Jest przydatna do przenoszenia danych między
asynchronicznymi procesami lub do zapisywania plików dziennika. Kiedy bufor jest
pusty, konsum ent czeka do czasu dostarczenia danych. Jeśli bufor jest pełny, produ
cent czeka na możliwość dodania danych. Opracuj interfejs API klasy RingBuffer
oraz implementację opartą na tablicy (z cyklicznym „zawijaniem”).
1.3.40. Przenoszenie na początek. Program ma wczytywać ciąg znaków ze standar
dowego wejścia i zapisywać znaki na liście powiązanej, usuwając przy tym powtórze
nia. Po wczytaniu danego znaku po raz pierwszy należy wstawić go na początek listy.
Po wczytaniu powtarzającego się symbolu należy usunąć go z listy i wstawić na po
czątek. Nazwij program MoveToFront. Jest to implementacja dobrze znanej strategii
przenoszenia na początek, przydatnej do obsługi pamięci podręcznej, kompresowania
danych oraz w wielu innych zastosowaniach, jeśli ostatnio używane elementy z naj
większym prawdopodobieństwem zostaną ponownie użyte.
1.3.41. Kopiowanie kolejki. Utwórz nowy konstruktor, tak aby instrukcja:
Queue<Item> r = new Queue<Item>(q);
sprawiała, że r wskazuje na nową i niezależną kopię kolejki q. Możliwe ma być doda
wanie i usuwanie elementów kolejek q oraz r bez wpływu na drugą z nich. Wskazówka:
usuń wszystkie elementy z q oraz dodaj je zarówno do q, jak i do r.
R O ZD ZIA Ł 1 a Podstawy
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
1.3.42. Kopiowanie stosu. Utwórz nowy konstruktor dla implementacji klasy Stack
opartej na liście powiązanej. Instrukcja:
Stack<Item> t = new Stack<Item >(s);
ma tworzyć t jako nową i niezależną kopię stosu s.
1.3.43. Wyświetlanie list plików. Katalog to lista plików i katalogów. Napisz program,
który pobiera jako argument z wiersza poleceń nazwę katalogu i wyświetla wszystkie
znajdujące się w nim pliki, a ponadto rekurencyjnie zawartość każdego katalogu pod
jego nazwą. Wskazówka: użyj kolejki i biblioteki jav a, i o. Fi le.
1.3.44. Bufor edytora tekstu. Opracuj typ danych dla bufora w edytorze tekstu i za
implementuj następujący interfejs API:
p u b lic c la s s B u ffe r
B u ffe r() Tworzenie pustego bufora
void in s e r t( c h a r c) Wstawianie znaku c na pozycji kursora
char d e le te !) Usuwanie i zwracanie znaku na pozycji kursora
void 1e f t ( in t k) Przenoszenie kursora o k pozycji w lewo
void r i g h t ( i n t k) Przenoszenie kursora o k pozycji w prawo
in t s iz e ! ) Liczba znaków w buforze
In te rf e js API d la b u fo r a n a t e k s t
Wskazówka: zastosuj dwa stosy.
1.3.45. Ogólność stosu. Załóżmy, że wywołano ciąg wymieszanych operacji dodaj
i zdejmij, takich jak w kliencie testowym. Liczby całkowite 0, 1, ..., N-l w tejże ko
lejności (instrukcje dodaj) są wymieszane z N znakami minus (instrukcje zdejmij).
Wymyśl algorytm, który określa, czy ciąg wymieszanych instrukcji powoduje próbę
odczytu z pustego stosu. Ilość dostępnej pamięci jest niezależna od N — nie możesz
zapisywać liczb całkowitych w strukturze danych. Opracuj działający w czasie linio
wym algorytm do określania, czy dana permutacja może zostać wygenerowana jako
dane wyjściowe przez klienta testowego (w zależności od miejsc wywołania instruk
cji zdejmij).
1.3 □ Wielozbiory, kolejki i stosy 183
Rozwiązanie: próba odczytu z pustego stosu nie ma miejsca, o ile nie istnieje liczba k,
taka że pierwszych k operacji zdejmowania następuje przed pierwszymi k operacjami
dodawania. Jeśli daną permutację można wygenerować, powstaje zawsze w następu
jący sposób: jeżeli następna liczba całkowita w wyjściowej permutacji znajduje się na
wierzchu stosu, należy ją zdjąć; w przeciwnym razie należy umieścić ją na stosie.
1.3.46. Niedozwolone trójki. Udowodnij, że permutację na podstawie stosu można
wygenerować (jak opisano to w poprzednim ćwiczeniu) wtedy i tylko wtedy, jeśli nie
obejmuje ona żadnej niedozwolonej trójki (a, b, c), takiej że a < b < c i c jest pierw
sze, a drugie, natomiast b — trzecie (między c i a oraz a i b mogą znajdować się inne
wartości).
Częściowe rozwiązanie: załóżmy, że w perm utacji występuje niedozwolona trójka
(a, b, c). Element c jest zdejmowany przed a i b, ale a i b umieszczono na stosie
przed c. Dlatego w momencie umieszczania na stosie c zarówno a, jak i b już się na
n im znajdują. Dlatego a nie można zdjąć przed b.
1.3.47. Złączalne kolejki, stosy i steque. Dodaj n o w ą o p e ra c ję złączania, k tó ra —
n is z c z ą c p i e r w o tn e s t r u k t u r y — z łą c z a d w ie k o le jk i, d w a s to s y lu b d w ie s t r u k t u r y
s te q u e (z o b a c z ć w ic z e n ie 1 .3 .3 2 ). Wskazówka: u ż y j c y k l i c z n e j l i s t y p o w i ą z a n e j ,
o b e jm u ją c e j w s k a ź n ik d o o s ta tn ie g o e le m e n tu .
1.3.48. Dwa stosy oparte na strukturze deque. Zaimplementuj dwa stosy za pomocą
jednej struktury deque, tak aby każda operacja wymagała stałej liczby operacji na
strukturze deque (zobacz ć w i c z e n i e 1 .3 .3 3 ).
1.3.49. Kolejka oparta na stałej liczbie stosów. Zaimplementuj kolejkę za pomocą
stałej liczby stosów, tak aby każda operacja na kolejce wymagała stałej (dla najgorsze
go przypadku) liczby operacji na stosach. Ostrzeżenie: bardzo trudne.
1.3.50. Iterator z szybkim przerywaniem działania. Zmodyfikuj kod iteratora w kla
sie Stack, tak aby natychmiast zgłaszał wyjątek ja v a .u til .ConcurrentModificatio
nException, kiedy klient modyfikuje kolekcję (przez wywołanie push() lub pop())
w trakcie iterowania.
Rozwiązanie: należy przechowywać licznik dla liczby operacji push () i pop (). W cza
sie tworzenia iteratora wartość tę trzeba zapisać jako zmienną egzemplarza z kla
sy Ite ra to r. Przed każdym wywołaniem hasNext() i next() należy sprawdzić, czy
wartość nie zmieniła się od czasu utworzenia iteratora. Jeśli została zmodyfikowana,
należy zgłosić wyjątek.
ludzie Z a
W R A Z Z NA B Y W A N IEM D O Ś W IA D C Z E N IA W KOR ZY STA N IU Z K O M P U T E R Ó W
czynają używać ich do rozwiązywania trudnych problemów lub przetwarzania du
żych ilości danych. Niezmiennie prowadzi to do pytań w rodzaju:
Jak długo zajmie wykonywanie programu?
Dlaczego programowi zabrakło pamięci?
Z pewnością zadawałeś sobie te pytania, na przykład w trakcie przebudowywania
biblioteki utworów muzycznych lub zdjęć, instalowania aplikacji, pracy nad dużym
dokumentem lub używania dużej ilości danych z eksperymentów. Pytania te są zbyt
ogólne, aby m ożna było na nie precyzyjnie odpowiedzieć. Odpowiedzi zależą od wie
lu czynników, takich jak cechy używanego komputera, przetwarzane dane i program
wykonujący operacje (z implementacją pewnego algorytmu). Wszystkie te czynniki
sprawiają, że trzeba przeanalizować olbrzymią ilość informacji.
Mimo tych trudności droga do uzyskania przydatnych odpowiedzi na podstawo
we pytania jest często niezwykle prosta, co pokazano w tym podrozdziale. Proces
oparty jest na metodzie naukowej — powszechnie przyjętym zestawie technik uży
wanych przez naukowców do zbierania wiedzy o świecie. Stosujemy analizę mate
matyczną do opracowywania zwięzłych modeli kosztów i przeprowadzamy badania
eksperymentalne w celu potwierdzenia tych modeli.
Metoda naukowa Podejście stosowane przez naukowców do poznawania świata
jest też skuteczne do badania czasu działania programów. Oto etapy procesu:
■ Zaobserwowanie pewnej cechy świata, zwykle na podstawie precyzyjnych po
miarów.
* Zaproponowanie hipotetycznego modelu spójnego z obserwacjami.
■ Prognozowanie zdarzeń na podstawie hipotez.
■ Weryfikacja prognoz na podstawie dalszych obserwacji.
■ Walidacja przez powtarzanie procesu do momentu, w którym hipotezy i obser
wacje są zgodne.
Jedną z kluczowych cech metody naukowej jest to, że projektowane eksperymenty
muszą być powtarzalne, tak aby inni mogli samodzielnie przekonać się o słuszno
ści hipotez. Hipotezy muszą być falsyfikowalne, aby m ożna było stwierdzić, że dana
hipoteza jest błędna i wymaga modyfikacji. Zgodnie z powiedzeniem przypisanym
Einsteinowi, „żadna liczba eksperymentów nie dowiedzie, iż mam rację, natomiast
wystarczy jeden, aby wykazać, że się mylę”, nigdy nie wiadomo, że hipoteza jest w peł
ni poprawna. Można tylko stwierdzić, że jest spójna z obserwacjami.
1.4 h Analizy algorytm ów 185
O b se r w a c je Pierwszą trudnością jest ustalenie, jak przeprowadzić ilościowe
pomiary czasu działania programów. Zadanie to jest zdecydowanie prostsze niż
w naukach przyrodniczych. Nie trzeba w tym celu wysyłać rakiety na Marsa, zabijać
zwierząt laboratoryjnych lub rozszczepiać atomu — wystarczy uruchom ić program.
Co więcej, każde uruchomienie programu to eksperyment naukowy, który łączy pro
gram ze światem i pozwala odpowiedzieć na podstawowe pytanie: Jak długo potrwa
wykonywanie programu?
Pierwsza ilościowa obserwacja na tem at większości programów dotyczy tego,
że rozmiar problemu wyznacza trudność zadania obliczeniowego. Zwykle rozmiar
problemu odpowiada albo wielkości danych wyjściowych, albo wartości argumentu
z wiersza poleceń. Intuicyjnie można stwierdzić, że czas działania powinien rosnąć
wraz z rozmiarem problemu. Jednak przy rozwijaniu i urucham ianiu program u za
wsze pojawia się pytanie o to, jak duży jest ten wzrost.
Inna ilościowa obserwacja na temat wielu programów dotyczy tego, że czas działa
nia jest stosunkowo niezależny od samych danych wejściowych — ważny jest przede
wszystkim rozmiar problemu. Jeśli ten związek nie jest zachowany, należy zwiększyć
poziom zrozumienia, a prawdopodobnie też i kontroli nad zależnością czasu działa
nia od danych wejściowych. Jednak wspomniany związek często występuje, dlatego
dalej koncentrujemy się na lepszym opisie zależności między rozmiarem problemu
a czasem działania.
P rzykład Podstawowym przykładem jest przedstawiony tu program ThreeSum.
Oblicza on liczbę trójek z pliku z N liczbami całkowitymi sumujących się do 0 (zakła
damy, że przepełnienie nie ma tu znaczenia). Operacje te mogą wydawać się sztuczne,
jednak są głęboko powiązane z wieloma
podstawowymi zadaniami obliczenio p u b lic c la s s ThreeSum
wymi (zobacz na przykład ć w i c z e n i e {
1 .4 .26 ). Jako danych wejściowych użyj p u b lic s t a t ic in t count (i nt [] a)
{ // Z lic z a n ie tró je k sumujących s ię do 0.
pliku lM [Link] z witryny. Plik zawiera in t N = a .le n gth ;
milion losowo wygenerowanych war in t cnt = 0;
tości typu in t. Druga, ósma i dziesiąta f o r ( in t i = 0; i < N; i++)
f o r ( in t j = i+ 1 ; j < N; j++)
wartość pliku lM [Link] sumują się do
f o r ( in t k = j+ 1; k < N; k++)
0. Ile jeszcze takich trójek znajduje się i f (a [ i] + a [j] + a[k] == 0)
w pliku? Program ThreeSum potrafi to cnt++;
return cnt;
obliczyć, ale czy robi to w rozsądnym
}
czasie? Jaka jest zależność między roz
miarem problem u (N) a czasem działa p u b lic s t a t ic void m a in (S trin g [] args)
nia programu? W ramach pierwszego 1
in t [ ] a = ln . r e a d ln t s ( a r g s [ 0 ] ) ;
eksperymentu uruchom na komputerze S t d O u t. p rin t ln (c o u n t(a ));
program ThreeSum dla plików [Link], 1
[Link], [Link] i [Link] z witry-
Jak długo potrwa wykonywanie programu dla danego N?
186 R O Z D Z IA L I o Podstawy
% morę lM in t s . t x t
ny (zawierają one 1000, 2000, 4000 i 8000 liczb całkowitych z pliku
324110 lM [Link]). Można szybko określić, że w pliku lK [Link] znajduje się
-442472 70 trójek sumujących się do 0, a w pliku [Link] — 528 takich trój
626686
-157678
ek. Programowi znacznie więcej czasu zajmuje ustalenie, że w pliku
508681 [Link] jest 4039 trójek sumujących się do 0, a w czasie oczekiwania
123414 na zakończenie sprawdzania pliku [Link] zadasz sobie pytanie: Jak
-77867
długo potrwa wykonywanie programu? Jak się okaże, uzyskanie odpo
155091
129801 wiedzi na to pytanie dla omawia
% j a v a Threesum l K i n t s . t x t
287381 nego kodu jest łatwe. Dość często
604242
m ożna poczynić całkiem precy
686904 tik tik tik
-247109
zyjne prognozy w czasie działania
77867 programu.
982455 70
-210707 Stoper W iarygodny pom iar do
% j a v a Threesum 2 l C i n t s . t x t
-922943 kładnego czasu działania progra
-738817
mu może być trudny. Na szczęś tik tik tik tik tik tik tik tik
85168 tik tik tik tik tik tik tik tik
855430 cie, zwykle wystarczą szacunki. tik tik tik tik tik tik tik tik
Chcemy móc odróżnić programy
kończące pracę w kilka sekund lub 528
m inut od tych, których działanie % j a v a Threesum 4 K i n t s . t x t
0
k tik tik tik tik tik tik tik
zajmuje kilka dni, miesięcy, a nawet więcej czasu. k tik tik tik tik tik tik tik
Chcemy też wiedzieć, kiedy jeden program jest k tik tik tik tik tik tik tik
k tik tik tik tik tik tik tik
dwa razy szybszy od innego w wykonywaniu da k tik tik tik tik tik tik tik
nego zadania. Nadal trzeba dokonać dokładnych k tik tik tik tik tik tik tik
k tik tik tik tik tik tik tik
pomiarów w celu wygenerowania danych eks k tik tik tik tik tik tik tik
perymentalnych, które m ożna wykorzystać do k tik tik tik tik tik tik tik
k tik tik tik tik tik tik tik
sformułowania i walidacji hipotez dotyczących k tik tik tik tik tik tik tik
zależności między czasem działania a rozmia k tik tik tik tik tik tik tik
k tik tik tik tik tik tik tik
rem problemu. Do pomiarów posłuży typ danych k tik tik tik tik tik tik tik
Stopwatch przedstawiony na następnej stronie. k tik tik tik tik tik tik tik
k tik tik tik tik tik tik tik
Metoda elapsedTime() zwraca czas (w sekun k tik tik tik tik tik tik tik
dach), który upłynął od czasu utworzenia obiek k tik tik tik tik tik tik tik
k tik tik tik tik tik tik tik
tu. Implementacja oparta jest na metodzie syste k tik tik tik tik tik tik tik
mowej currentTimeMi 11 i s () Javy, zwracającej k tik tik tik tik tik tik tik
k tik tik tik tik tik tik tik
aktualny czas w milisekundach. M etoda ta w m o k tik tik tik tik tik tik tik
mencie wywołania konstruktora zapisuje czas, k tik tik tik tik tik tik tik
4039
a w chwili wywołania metody el apsedTime()
jest ponownie używana do obliczenia czasu, jaki Obserwowanie czasu działania programu
upłynął.
1.4 a Analizy algorytmów 1 87
I n t e r f e j s API public cla ss Stopwatch
Stopwatch () Tworzenie stopem
double elapsedTim e() Zwracanie czasu, ja ki upłynął
od czasu utworzenia obiektu
Klient testowy
p u b lic s t a t ic void m a in (S trin g [] args)
{
in t N = In t e g e r .p a r s e ln t ( a r g s [0 ]);
i nt [] a = new i nt [N ];
fo r (in t i = 0; i < N; i++)
a [ i] = [Link](-1000000, 1000000);
Stopwatch tim er = new Stopwatch();
in t cnt = ThreeSum .count(a);
double time = tim [Link] p sed T im e ();
StdOut.p r i n t l n ( " 1 iczba tró je k ; " + cnt + " + tim e);
Zastosowanie % java Stopwatch 1000
lic z b a tró je k : 51; 0.488 seconds
% java Stopwatch 2000
lic z b a tró je k ; 516; 3.855 seconds
Implementacja p u b lic c la s s Stopwatch
{
p riv a te final long s t a r t ;
p u b lic Stopwatch()
{ s t a r t = S y ste m .c u rre n tT im e M illisO ; }
p u b lic double elapsedTim e()
{
long now = S y st e m .c u rre n t T im e M illis O ;
return (now - s t a r t ) / 1000.0;
Abstrakcyjny typ danych dla stopera
18 8 R O Z D Z IA Ł ! b Podstawy
A n a lizy danych eksperym entalnych Program DoublingTest pokazany na następ
nej stronie to bardziej złożony klient program u Stopwatch, generujący dane ekspe
rymentalne dla program u ThreeSum. Klient generuje ciąg losowych tablic wejścio
wych, w każdym kroku podwaja wielkość tablicy i wyświetla czas działania metody
[Link]() dla danych wejściowych o każdej wielkości. Eksperymenty te są,
oczywiście, powtarzalne. Można uruchomić je na własnym komputerze dowolną
liczbę razy. Uruchomienie programu DoublingTest prowadzi do cyklu prognozy-
weryfikacja. Oczywiście, ponieważ Twój kom puter różni się od naszego, czas wy
konania będzie prawdopodobnie inny od pokazanego w tym miejscu. Gdyby Twój
kom puter był dwukrotnie szybszy od naszego, czas działania wyniósłby na nim mniej
więcej połowę czasu pracy programu na naszej maszynie. Bezpośrednio prowadzi
to do dobrze ugruntowanej hipotezy, zgodnie z którą czas wykonania na poszcze
gólnych komputerach różni się o stały czynnik. Można jednak zadać sobie bardziej
szczegółowe pytanie: Jak długo potrwa działanie programu wyrażone jako funkcja od
wielkości danych wejściowych? Aby pomóc odpowiedzieć na to pytanie, generujemy
dane w formie graficznej. Diagramy w dolnej części następnej strony przedstawiają
efekt tego procesu w skali normalnej i logarytmicznej. Na osi x pokazano wielkość
problemu, N, a na osi y — czas działania T(N). Diagram w skali logarytmicznej na
tychmiast prowadzi do hipotez na tem at czasu wykonania — dane pasują do prostej
o nachyleniu 3. Równanie dla takiej linii to:
l g (T(N)) = 3 Ig /V + Ig a
(gdzie a to stała), co jest odpowiednikiem:
T[N) = a (V3
dla czasu wykonania jako funkcji od rozmiaru danych wejściowych — takiej właśnie
funkcji potrzebujemy. Można użyć otrzymanych punktów danych do obliczenia a.
Na przykład T(8000) = 51,1 = a 80 003, tak więc a = 9,98xl0‘n , a następnie zastosować
równanie:
r((V) = 9,98xl0-u (V3
do prognozowania czasu wykonania program u dla dużych N. Nieformalnie testu
jemy hipotezę, że punkty danych na diagramie logarytmicznym znajdą się blisko
opisywanej linii. Dostępne są metody statystyczne do przeprowadzania dokładniej
szych analiz w celu znalezienia szacowanej wartości a i wykładnika b, jednak te krót
kie obliczenia wystarczą do oszacowania czasu działania w większości zastosowań.
Przykładowo, można oszacować czas wykonania programu na naszym komputerze
dlaiV= 16 000 na około 9,98x10'“ 160003 = 408,8 sekundy lub około 6,8 m inuty (rze
czywisty czas wyniósł 409,3 sekundy). W czasie oczekiwania na wyświetlenie przez
program Doubl i ngTest wiersza dla N = 16 000 możesz użyć tej m etody do oszacowa
nia, kiedy program zakończy działanie, a następnie sprawdzić wynik i zobaczyć, czy
prognozy były trafne.
1.4 □ Analizy algorytmów 1 89
program do przeprowadzania eksperym entów
public c la s s D oublingTest % java DoublingTest
250 0.0
(
p ub lic s t a t i c double t im e T r ia l( in t N) 500 0.0
{ // Czas d z ia ła n ia metody [Link]() dla N 1000 0 . 1
// losowych 6-cyfrow ych lic z b całkow itych, 2000 0 .8
in t MAX = 1000000; 4000 6.4
i nt [] a = new i nt [N ]; 8000 51.1
fo r (in t i = 0 ; i < N ; i++ )
a [ i ] = [Link](-MAX, MAX);
Stopwatch tim er = new Stopw atch();
in t cnt = ThreeSum .count(a);
return tim [Link] p sed T im e ();
}
p u b lic s t a t ic void m a in (S trin g [] args)
{ // W yśw ietlanie t a b e li z czasami wykonania,
fo r ( in t N = 250; true ; N += N)
{ // W yśw ietlanie czasu dla problemu o rozm iarze N.
double time = t im e T r ia l(N );
S t d 0 u t.p rin t f("% 7 d % 5 .1 f\n ", N, tim e);
)
Prosta
Diagram 5 1 ,2 -
o nachyleniu 3 .
logarytm iczny
25,6
1 2 ,8 -
6 ,4 -
3 ,2 “
5
tOl i'6“
0 ,8 -
0,4
0,2
0,1
1 2 4 8
R o zm ia r p ro b le m u - N (w tysiącach) Ig W (w tysiącach)
Analizy danych eksperymentalnych (czas wykonania metody ThreeSum,countQ)
190 ROZDZIAŁ 1 o Podstawy
Do tej pory proces ten odzwierciedla proces stosowany przez naukowców przy
próbie zrozumienia cech świata rzeczywistego. Prosta na diagramie logarytmicznym
pozwala zaproponować hipotezę, zgodnie z którą dane pasują do równania T(N) =
a Nk Taki sposób dopasowania to zależność potęgowa. Zależności potęgowe opisują
bardzo wiele zjawisk naturalnych i sztucznych. Można w uzasadniony sposób p o
stawić hipotezę, że opisują też czas wykonania programu. Na potrzeby analiz algo
rytmów istnieją modele matematyczne, które zapewniają solidne podstawy dla tej
i podobnych hipotez. Przyjrzyjmy się takim modelom.
Modele matematyczne W początkowym okresie istnienia nauk kom putero
wych D.E. Knuth stwierdził, że mimo czynników komplikujących określenie czasu
wykonania programu, zasadniczo można zbudować model matematyczny opisujący
czas pracy każdego programu. Podstawowa myśl Knutha jest prosta — łączny czas
wykonania programu zależy od dwóch głównych czynników:
■ kosztu wykonania każdej instrukcji;
■ liczby wywołań każdej instrukcji.
Pierwszy czynnik zależy od komputera, kompilatora Javy i systemu operacyjnego.
Drugi jest cechą program u i danych wejściowych. Jeśli znane są oba czynniki dla
wszystkich instrukcji programu, można pomnożyć wartości i zsumować wyniki, aby
uzyskać czas wykonania.
Największą trudność sprawia ustalenie liczby wywołań instrukcji. Analiza nie
których instrukcji jest łatwa. Przykładowo, instrukcja ustawiająca zmienną cnt na 0
w metodzie [Link]() jest wywoływana dokładnie raz. Inne instrukcje wy
magają zastanowienia. Instrukcja i f w metodzie ThreeSum. count () jest wykonywana
dokładnie:
N(N-l) (A/-2)/6
razy (jest to liczba sposobów wyboru trzech różnych liczb z tablicy wejściowej; zo
bacz ć w i c z e n i e 1 .4 . 1 ). Inne obliczenia zależą od danych wejściowych. Przykładowo,
liczba wywołań instrukcji cnt++ w metodzie [Link] () to liczba występują
cych w danych wejściowych trójek sumujących się do 0. Liczba ta może wynosić od
0 do liczby wszystkich trójek. W metodzie Doubl i ngTest, która losowo generuje war
tości, m ożna przeprowadzić analizy probabilistyczne w celu ustalenia oczekiwanej
liczby (zobacz ć w i c z e n i e 1 .4 .40 ).
Przybliżenia z tyldę Analizy częstotliwości mogą wymagać skomplikowanych i dłu
gich wyrażeń matematycznych. Rozważmy na przykład omówioną wcześniej liczbę
wywołań instrukcji i f w programie ThreeSum:
N (/V—1) (/V—2) / 6 = /V3/ 6 - /V2/ 2 + N/3
1.4 o Analizy algorytmów 191
193
w wyrażeniu tym, co typowe dla wyra N 3/6
żeń tego rodzaju, wyrazy po najstarszym
mają stosunkowo małą wartość (na przy
kład dla N = 1000 wyraz - N 72 + N/3 ~
-499 667, co jest nieistotne w porównaniu
z AP/6 = 166 666 667). Aby umożliwić po
minięcie nieznaczących wyrazów, a tym
samym znacznie uprościć używane wzory
matematyczne, używa się narzędzia m ate
matycznego — notacji tyldy (~). Notacja
Przybliżenie oparte na najstarszym wyrazie wielomianu
ta umożliwia stosowanie przybliżeń z tyl
dę, w których pomija się wyrazy o niskich
Przybliżenie Tempo
potęgach, komplikujące wzory i mające Funkcja
z tyldą wzrostu
niewielki wpływ na potrzebną wartość:
N 76 - N 2/2 + N/3 ■N76 N3
N72 + NI2 ~ N 2/2 N2
Definicja. Zapis ~/(N) to reprezen
lg N + 1 ~ lg N lg N
tacja dowolnej funkcji, dla której wy
nik dzielenia przez/(N ) zbliża się do 1 3 ~3 1
wraz z rosnącym N. g(N) ~ f{N ) ozna Typowe przybliżenia z tyldą
cza, że g(N )/f{N ) zbliża się do 1 wraz
z rosnącym N.
Tempo wzrostu
Przykładowo, przybliżenie ~ bP/6 opisuje liczbę wy
wołań instrukcji i f w program ie ThreeSum, ponieważ
Opis Funkcja
NV6 - AP/2 + N/3 podzielone przez N 76 zbliża się do 1
Stałe 1 wraz ze wzrostem N. Najczęściej używane są przybliże
nia z tyldą w postaci g(N) ~af{N), gdzie/(N ) = Nb(log
Logarytmiczne log N N)c dla stałych a, b i c. f(N ) jest tu tempem wzrostu
g(N). Przy używaniu logarytmów do określania tem
Liniowe N pa wzrostu zwykle nie podaje się podstawy, ponieważ
szczegół ten można uwzględnić w a. Dotyczy stosun
Liniowo- N lo g N kowo nielicznych funkcji, często spotykanych przy
logarytmiczne analizowaniu tempa wzrostu czasu wykonania progra
m u i pokazanych w tabeli po lewej stronie (wyjątkiem
Kwadratowe N2 jest funkcja wykładnicza, którą omówiono w rozdziale
„ k o n t e k s t ” ) . Bardziej szczegółowy opis tych funkcji
Sześcienne N3 i krótkie wyjaśnienie, dlaczego używa się ich w anali
2n
zach algorytmów, znajduje się po omówieniu progra
Wykładnicze
mu ThreeSum.
Często spotykane
funkcje tempa wzrostu
19 2 R O Z D Z IA Ł ! □ Podstawy
Przybliżony czas w ykonania Aby zastosować sposób Knutha na tworzenie wyrażeń
matematycznych określających łączny czas wykonania programu Javy, można (teore
tycznie) zbadać kompilator Javy, żeby ustalić liczbę instrukcji maszynowych odpowia
dających każdej instrukcji Javy, i przeanalizować specyfikację komputera w celu ustale
nia czasu wykonywania każdej instrukcji maszynowej. W ten sposób uzyskamy łączny
czas. Proces ten dla programu Thr ee Sum pokrótce podsumowano na następnej stronie.
Skategoryzowano bloki instrukcji Javy według liczby wywołań, określono oparte na
najstarszym wyrazie przybliżenia liczby wywołań, ustalono koszt każdej instrukcji i ob
liczono sumę. Zauważmy, że liczba wywołań niektórych instrukcji zależy od danych
wejściowych. Tu liczba uruchomień instrukcji cnt++ zależy, oczywiście, od danych. Jest
to liczba trójek sumujących się do 0 i może wynosić od 0 do ~AP/6 . Nie przedstawiamy
tu szczegółów (wartości stałych) dla konkretnego systemu. Warto jednak zaznaczyć, że
stosując stałe tQ, t:, i,,... dla czasu działania bloków instrukcji, zakładamy, iż każdy blok
instrukcji Javy odpowiada instrukcjom maszynowym, które działają przez stały czas.
Kluczowym spostrzeżeniem w tym przykładzie jest to, że tylko najczęściej wykony
wane instrukcje mają wpływ na łączny czas. Instrukcje te nazywamy pętlę wewnętrznę
programu. Dla programu Thr ee Su m pętlą wewnętrzną są instrukcje zwiększające war
tość k i sprawdzające, czy jest ona mniejsza niż N, a także instrukcje określające, czy
suma trzech liczb jest równa 0. Ponadto, w zależności od danych wejściowych, ważne
mogą być też instrukcje obsługujące licznik. Jest to typowa sytuacja — czas wykonania
wielu programów zależy tylko od małego podzbioru instrukcji.
H ipotezy dotyczące tem pa wzrostu Oto podsumowanie — eksperymenty opisane
na stronie 189 i model matematyczny przedstawiony na stronie 193 są podstawą dla
następującej hipotezy:
Cecha A. Tempo wzrostu czasu wykonania program u ThreeSum (określa liczbę
sumujących się do 0 trójek wśród N liczb) wynosi N 3.
Dowód. Niech T{N) będzie czasem wykonania program u T h r ee S u m dla N liczb.
Opisany model matematyczny wskazuje, że T(N) ~ aN 3 dla pewnej zależnej od
maszyny stałej a. Eksperymenty przeprowadzone na wielu komputerach (w tym
Twoim i naszym) potwierdzają prawdziwość przybliżenia.
W książce używamy nazwy cecha na określenie hipotezy, którą trzeba poddać walida
cji przez eksperymenty. Wynik końcowy analiz matematycznych jest dokładnie taki
sam, jak efekt analiz eksperymentalnych. Czas wykonania program u T h r e e S u m wyno
si ~ aN3 dla zależnej od maszyny stałej a. Ta spójność dowodzi poprawności zarówno
eksperymentów, jak i m odelu matematycznego, a ponadto pozwala lepiej zrozumieć
program, ponieważ nie trzeba przeprowadzać eksperymentów w celu ustalenia wy
kładnika. Tyle samo pracy wymaga walidacja wartości a w konkretnym systemie,
choć zadanie to zwykle jest wykonywane przez ekspertów w sytuacjach, w których
wydajność ma kluczowe znaczenie.
1.4 a Analizy algorytmów 193
public class ThreeSum
{
public static int count(int[] a)
{
int N = [Link];
i n t c n t = 0;
for (int i = 0 ; li < N: i++|l
g B
c for (int j = i+1; |j < N; j++|)
ii C for (int k = j+l;|k < n ; k++|^ ~N2/2 |
if Ca [i ] + a [ j ] + a [k] == 0) -NV6 I
cnt++;
return cnt;
\
}
public static void mainCstring[] args) \
{ Pętla
int[] a = [Link](args[0]);
[Link](count(a));
}
Struktura liczby wywołań instrukcji programu
Blok Czas
Liczba wywołań Łączny czas
instrukcji w sekundach
E 0 x (zależy od danych wejściowych)
D i N3/ 6 - A P/2 + NI 3 i, (7^/6 - AP/2 + AT/3)
C 2
N2/2 + NI 2 t2 (AP/2 + NI2)
B 3
N t3N
A 4
1
(i,/6) AP
+ {t 12 - 116) N2
Łączna suma 2 1
+ (f,/3 - tJ2 + i3) N
+ K + fox
Przybliżenie z tyldą ~ (tJ / 6 ) iSP (dla małego X)
Tempo wzrostu AP
Przykładowa analiza czasu w ykonania program u
194 R O Z D Z IA L I ■ Podstawy
A n a lizy algorytm ów Hipotezy podobne do CECHA A są ważne, ponieważ łączą
abstrakcyjny świat program u Javy z rzeczywistym światem komputera, na którym
program uruchomiono. Tempo wzrostu pozwala posunąć się o krok dalej i oddzielić
program od użytego do jego zaimplementowania algorytmu. Chodzi o to, że tem
po wzrostu czasu wykonania program u ThreeSum wynosi N 3 niezależnie od tego,
czy program zaimplementowano w Javie i czy działa on na laptopie, telefonie ko
mórkowym czy superkomputerze. Ważne jest to, że program sprawdza wszystkie
trójki liczb z danych wejściowych. Tempo wzrostu jest wyznaczane przez używany
algorytm (a czasem i model danych wejściowych). Oddzielenie algorytmu od imple
mentacji na konkretnym komputerze to ważna technika, ponieważ pozwala rozwijać
wiedzę o wydajności algorytmów, a następnie stosować ją dla dowolnego komputera.
Przykładowo, m ożna stwierdzić, że ThreeSum to implementacja algorytmu opartego
na ataku siłowym: „Oblicz sumę wszystkich różnych trójek i policz te, dla których
suma wynosi 0”. Oczekujemy, że implementacja tego algorytmu w dowolnym języku
programowania na dowolnym komputerze będzie
działać w czasie proporcjonalnym do N3. W prak Model kosztów dla sum
tyce dużą część wiedzy na temat wydajności kla trójek. Przy badaniu al
sycznych algorytmów opracowano wiele lat temu, gorytmów rozwiązujących
jednak wiedza ta jest adekwatna także w kontekście problem sum trójek zli
współczesnych komputerów. czane są dostępy do tablicy
M odel kosztów Skoncentrujmy się na właściwoś (liczba operacji dostępu
ciach algorytmów. Określmy w tym celu model do tablicy w celu odczytu
kosztów, który wyznacza podstawowe operacje wy lub zapisu).
konywane w analizowanym algorytmie przy roz
wiązywaniu problemu. Przykładowo, model kosz
tów odpowiedni dla problemu sum trójek, opisany po prawej, oparty jest na liczbie
operacji dostępu do elementów tablicy. W m odelu kosztów można podać precyzyjne
matematyczne stwierdzenia na tem at cech algorytmu, a nie tylko konkretnej imple
mentacji. Wygląda to tak:
Twierdzenie B. Algorytm ataku siłowego do obliczania sum trójek uzyskuje
dostęp do tablicy ~ N 3/2 razy w celu ustalenia liczby trójek sumujących się do 0
dla N liczb.
Dowód. Algorytm uzyskuje dostęp do każdej z trzech liczb z ~ N 3/6 trójek.
1.4 b Analizy algorytm ów 195
Twierdzenie to matematyczna prawda na tem at algorytmu, wyrażona w kategoriach
modelu kosztów. W książce analizujemy algorytmy w kontekście konkretnego m o
delu kosztów. Celem jest wyrażenie modelu kosztów w taki sposób, aby tempo wzro
stu czasu wykonania dla danej implementacji było takie samo, jak tempo wzrostu
kosztów działania algorytmu (model kosztów powinien więc obejmować operacje
z pętli wewnętrznej). Poszukujemy precyzyjnych matematycznych danych na temat
algorytmów (twierdzeń), a także przedstawiamy hipotezy dotyczące wydajności im
plementacji (cechy), które można sprawdzić za pomocą eksperymentów. Tu t w i e r
d z e n i e b to matematyczna prawda zgodna z hipotezą podaną jako c e c h a a , udo
wodnioną eksperymentalnie według m etody naukowej.
i
196 RO ZD ZIA Ł 1 0 Podstawy
Podsum ow anie Dla wielu programów opracowanie matematycznego modelu czasu
wykonania sprowadza się do następujących kroków:
■ Opracowania modelu danych wejściowych, w tym definicji rozmiaru problemu.
* Określenia pętli wewnętrznej.
■ Zdefiniowania modelu kosztów obejmującego operacje z pętli wewnętrznej.
■ Ustalenia liczby wywołań tych operacji dla dostępnych danych wejściowych.
Może to wymagać analiz matematycznych. W dalszej części rozdziału omówio
no pewne przykłady w kontekście specyficznych podstawowych algorytmów.
Jeśli program jest zdefiniowany za pom ocą wielu metod, zwykle opisujemy je osob
no. Rozważmy przykładowy program z p o d r o z d z ia ł u i . i , Bi narySearch.
Wyszukiwanie binarne. Model danych wejściowych to tablica a [] o rozmiarze N.
Pętla wewnętrzna to instrukcje w jednej pętli while. Model kosztów obejmuje ope
rację porównywania (porównanie wartości dwóch elementów tablicy). Analizy, opi
sane w p o d r o z d z ia le i . i i przedstawione szczegółowo w t w ie r d z e n iu b w p o d
r o z d z ia le 3 . 1 , pokazują, że liczba porównań wynosi najwyżej lg N+ 1.
Białe listy. Model danych wejściowych to N liczb na białej liście i M liczb w stan
dardowym wejściu (przy założeniu, że M » N). Pętla wewnętrzna to instrukcje
w pętli whi 1e. Model kosztów to operacja porównywania (tak samo jak w wyszuki
waniu binarnym). Analizy są dostępne natychmiast na podstawie analiz wyszuki
wania binarnego. Liczba porównań wynosi najwyżej M (lg N + 1).
Dochodzimy więc do wniosku, że tempo wzrostu czasu wykonania dla sprawdzania
białej listy wynosi najwyżej M lg N, przy czym należy uwzględnić następujące kwestie:
■ Dla małych N najważniejsze mogą być koszty operacji wejścia-wyjścia.
■ Liczba porównań zależy od danych wejściowych. Wynosi między ~M a ~M lg
N w zależności od tego, ile liczb ze standardowego wejścia znajduje się na białej
liście i po jakim czasie wyszukiwanie binarne pozwoli znaleźć te wartości (zwy
kle czas wynosi ~M lg N).
■ Zakładamy, że koszt operacji A rray [Link] rt() jest niski w porównaniu do M lg
N. Operacja ta jest zaimplementowana za pom ocą algorytmu sortowania przez
scalanie. W p o d r o z d z ia le 2.2 okaże się, że tempo wzrostu czasu wykonania
tego algorytmu to N log N (zobacz t w i e r d z e n ie g w r o z d z i a l e 2.), dlatego
założenie jest uzasadnione.
Tak więc model jest zgodny z hipotezami z podrozdziału 1 . 1 , zgodnie z którymi al
gorytm wyszukiwania binarnego umożliwia przeprowadzenie obliczeń dla dużych M
i N. Po podwojeniu długości standardowego strum ienia wejścia można oczekiwać
podwojenia czasu wykonania programu, natomiast podwojenie wielkości białej listy
prowadzi do nieznacznego wydłużenia czasu wykonania.
1.4 n Analizy algorytm ów 197
t w o r z e n i e m o d e l i m a t e m a t y c z n y c h na potrzeby analiz algorytmów to płodna
dziedzina badań, wykraczająca nieco poza zakres tej książki. Jednak, jak okaże się
przy omawianiu wyszukiwania binarnego, sortowania przez scalanie i wielu innych
algorytmów, poznanie pewnych modeli matematycznych jest niezbędne do zrozu
mienia wydajności podstawowych algorytmów. Dlatego często przedstawiamy szcze
góły i (lub) wyniki klasycznych badań. Napotykamy przy tym różne funkcje i przy
bliżenia powszechnie stosowane w analizach matematycznych. W tabelach poniżej
przedstawiono wybrane informacje:
Opis Zapis Definicja
Dolne ograniczenie Ld Największa liczba całkowita nie większa niż x
Górne ograniczenie W Najmniejsza liczba całkowita nie mniejsza niż x
Logarytm naturalny ln N log N (x, takie żeex = N)
Logarytm binarny lg N log,N (x, takie że 2X= N)
Całkowitoliczbowy LlgNj Największa liczba całkowita nie większa niż lg N; (liczba
logarytm binarny bitów w reprezentacji binarnej N) - 1
Liczby harmoniczne 1 + Vi+ 1/3 + V4 + ... + 1/N
Silnia N! Ix 2 x 3 x 4 x ... xN
Funkcje często stosowane w analizach algorytm ów
Opis Przybliżenie
Suma częściowa
Hw= 1 + Vi + 1/3 + 'A + ... + 1/N ~ ln N
szeregu harmonicznego
Liczba trójkątna 1 + 2 + 3 + 4 + ... + N ~ N 2/2
Suma częściowa
szeregu geometrycznego
1 + 2 + 4 + 8 + ... + N = 2N - 1 ~ 2N, jeśli N = 2"
Przybliżenie Stirlinga lg N\ = lg 1 + lg 2 + lg 3 + Ig 4 + ... + lg N ~ N lg N
Współczynnik Newtona ( k ) ~ Nk/k\, gdzie k to mała stała
Funkcja wykładnicza (1 - \lx)x ~ \ le
Przybliżenia przydatne przy analizowaniu algorytm ów
198 R O Z D Z IA L I h Podstawy
Kategorie tempa wzrostu W implementacjach algorytmów używamy tylko
kilku podstawowych elementów (instrukcji, instrukcji warunkowych, pętli, zagnież
dżania i wywołań metod), dlatego bardzo często tempo wzrostu dla kosztów to jed
na z kilku funkcji od rozmiaru problemu (N). Funkcje te podsum owano w tabeli
na następnej stronie. Podano tam też nazwy funkcji, typowy powiązany z nimi kod
i przykłady.
Stale Program, dla którego tempo wzrostu dla czasu wykonania jest stałe, wykonuje
określoną liczbę operacji w celu zakończenia pracy. Dlatego czas wykonania nie zale
ży od N. Większość operacji Javy działa w ten sposób.
Logarytm iczne Program, dla którego tempo wzrostu dla czasu wykonania jest loga
rytmiczne, działa tylko nieco wolniej od programu o stałym czasie pracy. Klasycznym
przykładem programu z czasem wykonania rosnącym logarytmicznie względem roz
miaru problemu jest wyszukiwanie binarne (zobacz program BinarySearch na stro
nie 59). Podstawa logarytmu nie ma znaczenia w kontekście tempa wzrostu (ponie
waż wszystkie logarytmy o tej samej podstawie różnią się o stały czynnik), dlatego
używamy tu log N.
Liniowe Programy przetwarzające każdy fragment danych wejściowych stałą ilość
czasu lub oparte na jednej pętli fo r występują dość często. Tempo wzrostu dla takich
programów jest liniowe. Czas ich wykonania jest proporcjonalny do N.
Liniow o-logarytm iczne Nazwy liniowo-logarytmiczne używamy do opisywania pro
gramów, dla których czas wykonania dla problemu o wielkości N ma tempo wzrostu
równe N log N. Także tu podstawa logarytmu nie ma znaczenia w kontekście tem
pa wzrostu. Typowym przykładem algorytmów liniowo-logarytmicznych są Merge.
s o rt() (zobacz a l g o r y t m 2 .4 ) iQ u ic k .so rt() (zobacz a l g o r y t m 2 . 5 ).
Kwadratowe Typowy program o tempie wzrostu czasu wykonania równym AP ma
dwie zagnieżdżone pętle for, służące do obliczeń obejmujących wszystkie pary N ele
mentów. Podstawowe algorytmy sortowania, Sel ecti on. so rt () (zobacz a l g o r y t m 2 .1 )
i In se rti on. so rt () (zobacz a l g o r y t m 2.2), to przykładowe programy tego rodzaju.
Sześcienne Typowy program o tempie wzrostu czasu wykonania równym N 3 ma trzy
zagnieżdżone pętle for, służące do obliczeń obejmujących wszystkie trójki spośród N
elementów. Prototypem jest przykład z tego podrozdziału — program ThreeSum.
W ykładnicze W r o z d z i a l e 6 . (ale nie wcześniej!) omówiono programy, których
czas wykonania jest proporcjonalny do 2N lub większej wartości. Ogólnie nazwa wy
kładniczy dotyczy algorytmów, dla których tempo wzrostu wynosi bN dla dowolnej
stałej b > 1 , nawet jeśli różne wartości b prowadzą do zupełnie innych czasów wyko
nania. Algorytmy wykładnicze są niezwykle powolne. Dla dużych problemów nigdy
nie są wykonywane do końca. Mimo to algorytmy wykładnicze odgrywają kluczową
rolę w teorii algorytmów, ponieważ istnieje duża klasa problemów, dla których algo
rytm wykładniczy jest najlepszym możliwym rozwiązaniem.
1.4 o Analizy algorytmów 199
Tempo
Nazwa Typow y kod Opis Przykład
wzrostu
Dodawanie
Stałe a = b + c; Instrukcja
dwóch liczb
Dzielenie Wyszukiwanie
Logarytmiczne log N [zobacz stronę 59]
na pół binarne
double max = a [ 0 ] ;
Znajdowanie
Liniowe N for (int i = 1; i < N; i++) Pętla
if ( a [ i] > max) max = a [ i ] ; maksimum
Liniowo- „Dziel Sortowanie
N log N [.zobacz ALGORYTM 2.4]
logarytmiczne i zwyciężaj” przez scalanie
f o r ( in t i = 0 ; i < N ; i++ )
fo r ( in t j = i+ 1 ; j < N; j++) Podwójna Sprawdzanie
Kwadratowe N1 i f ( a [ i] + a [j] == 0) pętla wszystkich par
cnt++;
f o r (in t i = 0; i < N; i++ )
f o r (in t j = 1+1; j < N; j++)
Potrójna Sprawdzanie
Sześcienne N} f o r (in t k = j+1; k < N; k++)
i f ( a [ i] + a [j] + a[k] == 0) pętla wszystkich trójek
cnt++;
Sprawdzanie
Wyszukiwanie
Wykładnicze 2N [zobacz r o zd z ia ł 6.] wszystkich
wyczerpujące
podzbiorów
Podsum owanie popularnych hipotez dotyczących tempa wzrostu
200 RO ZD ZIA Ł 1 o Podstawy
t e k a t e g o r i e s ą s p o t y k a n e n a j c z ę ś c i e j , ale, oczywiście, nie są to wszystkie m oż
liwości. Tempo wzrostu kosztów algorytmu może wynosić N 2 log N lub N 3'2 albo
być równe innej podobnej funkcji. Szczegółowe analizy algorytmów mogą wymagać
pełnej gamy rozwijanych przez wieki narzędzi matematycznych.
Wiele omawianych algorytmów ma
S ta n d a r d o w y d ia g r a m
prostą w opisie wydajność, którą można
precyzyjnie określić za pomocą jednego
z przedstawionych temp wzrostu. Dlatego
przeważnie można w modelu kosztów po
dać specyficzne twierdzenia, na przykład:
sortowanie przez scalanie wymaga między
Y z N lg N a N lg Nporównań. Bezpośrednio
wynika z tego hipoteza (cecha): tempo
wzrostu dla czasu wykonania sortowania
przez scalanie jest liniowo-logarytmicz-
ne. Z uwagi na zwięzłość skracamy to do
stwierdzenia, że sortowanie przez scalanie
jest liniowo-logarytmiczne.
Diagramy po lewej stronie pokazują,
Rozmiar p ro b lem u (w tysiącach)
jak ważne jest tem po wzrostu w prakty
ce. Oś x określa rozmiar problemu, a oś y
W y k re s lo g a r y tm ic z n y — czas wykonania. Diagramy pokazują,
że algorytmy kwadratowe i sześcienne są
nieakceptowalne dla dużych problemów.
Okazuje się, że kilka ważnych problemów
ma proste rozwiązania kwadratowe, na
tomiast istnieją też sprytne algorytmy
liniowo-logarytmiczne. Te ostatnie algo
rytmy (w tym sortowanie przez scalanie)
mają bardzo duże praktyczne znaczenie,
ponieważ umożliwiają rozwiązywanie
dużo większych problemów niż przy uży
ciu algorytmów kwadratowych. Dlatego
w książce tej koncentrujemy się na rozwi
janiu logarytmicznych, liniowych i linio-
i------1---- 1--- 1---- 1---- 1---- 1 i 1 r~
1 2 4 8 512 wo-logarytmicznych algorytmów dla pod
Rozmiar p ro b lem u (w tysiącach) stawowych problemów.
T y p o w e t e m p o w z ro stu
1.4 □ Analizy algorytmów 201
P r o je k to w a n ie sz y b s z y c h a lg o r y t m ó w Jednym z głównych powodów ba
dania tempa wzrostu dla programu jest ułatwienie zaprojektowania szybszego al
gorytmu rozwiązującego ten sam problem. Aby to zilustrować, rozważmy szybszy
algorytm dla problemów sum trójek. Jak m ożna opracować szybszy algorytm nawet
przed zagłębieniem się w badania algorytmów? Oto odpowiedź na pytanie — już
wcześniej omówiono i zastosowano dwa klasyczne algorytmy, sortowanie przez scala
nie oraz wyszukiwanie binarne, przy czym ten pierwszy jest liniowo-logarytmiczny,
a drugi — logarytmiczny. Jak można wykorzystać te algorytmy do rozwiązania prob
lemu sum trójek?
Rozgrzewka: sum y p a r Rozważmy łatwiejszy problem — określanie liczby par liczb
całkowitych z pliku wejściowego dających sumę 0. Aby uprościć omówienie, załóżmy
ponadto, że liczby są niepowtarzalne. Problem można łatwo rozwiązać w czasie kwa
dratowym, usuwając z m etody ThreeSum. count () pętlę k i tablicę a [k]. Pozostaje wte
dy pętla podwójna sprawdzająca wszystkie pary, co pokazano w wierszu Kwadratowe
w tabeli ze strony 199 (implementację tę nazwijmy TwoSum). W poniższej implemen
tacji pokazano, jak wykorzystać sortowanie przez scalanie i wyszukiwanie binarne
(zobacz stronę 59) do utworzenia liniowo-logarytmicznego rozwiązania problemu
sum par. Ulepszony algorytm oparto na tym, że element a [i ] należy do dającej sumę
0 pary wtedy i tylko wtedy, jeśli w tablicy znajduje się wartość -a [i ] (jeżeli a [i ] nie
jest zerem). Aby rozwiązać problem, należy posortować tablicę (co umożliwia wyszu
kiwanie binarne), a następnie dla każdego elementu a [i ] tablicy znaleźć -a [i ] za p o
mocą wyszukiwania binarnego (używając m etody rank () z programu Bi narySearch).
Jeśli wynik to indeks j, a j > i, należy zwiększyć licznik. Ten krótki test obejmuje
trzy przypadki:
0 Nieudane wyszukiwanie binarne import j a v a .u t il .A rrays;
zwraca wartość - 1 , dlatego licznik public cUss TwoSumFast
nie jest zwiększany.
■ Jeśli wyszukiwanie binarne zwraca j p u b lic s t a t ic in t count( i n t [] a)
{ // O k re śla n ie lic z b y par dających sumę 0.
> i, a [i] + a [j] = 0, dlatego należy A rra y s.s o r t (a );
zwiększyć licznik. in t N = a .le n gth ;
° Jeżeli wyszukiwanie binarne zwraca in t cnt = 0;
f o r (in t i = 0 ; i < N; 1++)
j z przedziału od 0 do i , także otrzy
i f ( B in a r y S e a r c h .r a n k (- a [ i], a) > i)
mujemy a [i] + a [j] = 0, ale nie cnt++;
należy zwiększać licznika, aby nie return cnt;
zliczać par dwukrotnie.
Wynik obliczeń jest dokładnie taki sam p u b lic s t a t ic void main ( S t r in g ! ] args)
jak w algorytmie kwadratowym, ale roz 1
wiązanie działa znacznie szybciej. Czas i n t [] a = ln . r e a d ln t s ( a r g s [ 0 ] ) ;
S t d O u t. p rin t ln (c o u n t(a ));
wykonania sortowania przez scalanie jest
1
proporcjonalny do N log N, a N wyszuki
Liniowo-logarytmiczne rozwiązanie problemu sumy par
202 RO ZD ZIA Ł 1 a Podstawy
wań binarnych zajmuje czas proporcjonalnie do log N, dlatego czas działania całego
algorytmu jest proporcjonalny do N log N. Opracowanie szybszego algorytmu nie
jest tylko akademickim ćwiczeniem. Szybszy algorytm umożliwia rozwiązywanie
dużo większych problemów. Przykładowo, prawdopodobnie możliwe będzie rozwią
zanie na Twoim komputerze w rozsądnym czasie problemu sum par dla miliona liczb
całkowitych (plik lM [Link]), jednak przy stosowaniu algorytmu kwadratowego za
danie to zajęłoby dużo czasu (zobacz ć w i c z e n i e 1 .4 .4 1 ).
Szybki algorytm dla sum trójek To samo podejście jest skuteczne dla problemu
sum trójek. Także tu zakładamy, że liczby całkowite są niepowtarzalne. Para a [i]
i a [j] to część sumującej się do 0 trójki wtedy i tylko wtedy, jeśli wartość - ( a [ i ] +
a [ j ] ) znajduje się w tablicy (oraz jest różna od a [i ] lub a [ j ] ). Kod poniżej sortu
je tablicę, a następnie wykonuje N ( N -1)/2 wyszukiwań binarnych, z których każde
zajmuje czas proporcjonalny do log N. W sumie daje to czas proporcjonalny do N 2
log N. Zauważmy, że w tym przypadku koszt sortowania jest nieznaczący. Także to
rozwiązanie umożliwia rozwiązanie dużo większych problemów (zobacz ć w i c z e n i e
1 .4 .4 2 ). Diagramy na rysunku w dolnej części następnej strony pokazują rozbież
ność w kosztach pracy czterech algorytmów dla rozważanych rozmiarów problemu.
Różnice te z pewnością stanowią motywację do szukania szybszych algorytmów.
Dolne ograniczenia W tabeli na stronie 203 znajduje się podsumowanie dyskusji
z tego podrozdziału. Natychmiast powstaje ciekawe pytanie: Czy można znaleźć al
gorytmy dla problemów sum par i trójek działające wyraźnie szybciej niż TwoSumFast
i ThreeSumFast? Czy istnieje al
import ja v a .u t i 1 .A r r a y s ; gorytm liniowy dla sum par lub
algorytm liniowo-logarytmiczny
p ub lic c la s s ThreeSumFast
dla sum trójek? Odpowiedzi na te
p u b lic s t a t ic in t c o u n t{in t[] a) pytania brzmią: nie dla sum par
{ // Z lic z a t r ó j k i sumujące s ię do 0. (w modelu, w którym uwzględ
A rra y [Link] rt(a );
niane są tylko porównania funk
in t N = a .le n gth ;
in t cnt = 0; cji liniowych lub kwadratowych
f o r ( in t i = 0; i < N; i++) funkcji liczb) i nie wiadomo dla
f o r (in t j = i+1 ; j < N; j++)
sum trójek, choć eksperci sądzą,
i f ( B in a r y S e a r c h . r a n k ( - a [ i] - a [ j ] , a) > j)
cnt++; że najlepszy możliwy algorytm
return cnt; dla sum trójek jest kwadratowy.
Dolne ograniczenie tempa wzro
p u b lic s t a t ic void m a in (S trin g [] args)
stu czasu wykonania dla najgor
szego przypadku dla wszystldch
i n t [] a = ln . r e a d ln t s ( a r g s [ 0 ] ) ; możliwych algorytmów rozwią
S t d O u t. p rin t ln (c o u n t(a ));
zujących dany problem ma bar
dzo duże znaczenie. Zagadnienie
to szczegółowo omówiono w pod-
Rozwiązanie o złożoności N2 Ig N dla problemu sum trójek
1.4 a Analizy algorytm ów 203
rozdziale 2.2 w kontekście sortowania. Niebanalne dolne Tempo wzrostu
Algorytm
ograniczenia trudno jest ustalić, są jednak bardzo przy czasu wykonania
datne w poszukiwaniu wydajnych algorytmów. TwoSum N2
TwoSumFast N log N
PRZY K ŁA D Y Z T E G O P O D R O Z D Z IA Ł U SĄ PODSTAW Ą d o
omawiania algorytmów w tej książce. Zastosowano opisa ThreeSum N3
ną poniżej strategię rozwiązywania nowych problemów.
ThreeSumFast N 2 log N
B Implementowanie i analizowanie prostego rozwią
Podsumowanie czasów wykonania
zania problemu. Zwykle takie rozwiązania, podob
ne do ThreeSum i TwoSum, nazywamy rozwiązaniami
przez atak siłowy.
° Sprawdzanie usprawnień algorytmów (takich jak TwoSumFast i ThreeSumFast),
zwykle zaprojektowanych w celu zmniejszenia tem pa wzrostu czasu wykona
nia.
n Przeprowadzanie eksperymentów w celu sprawdzenia poprawności hipotezy,
zgodnie z którą nowe algorytmy są szybsze.
W wielu przypadkach analizowanych jest kilka algorytmów rozwiązujących ten sam
problem, ponieważ czas wykonania to tylko jeden aspekt ważny przy wyborze algo
rytmu. Zagadnienie to szczegółowo omówiono w książce w kontekście podstawo
wych problemów.
Rozmiar problemu (A/) (w tysiącach) Rozmiar problemu (N ) (w tysiącach)
Koszty alg o ry tm ó w rozw iązujących problem y sum par i trójek
204 RO ZD ZIA Ł 1 n Podstawy
Eksperymenty ze stosunkiem czasu wykonania dla podwojonych
danych Poniżej opisano prosty i skuteczny krótki sposób na przewidywanie wy
dajności oraz określanie przybliżonego tem pa wzrostu czasu wykonania dowolnego
programu.
■ Opracuj generator danych wejściowych generujący wartości, które odpowiada
ją danym oczekiwanym w praktyce (tak jak losowe liczby całkowite w metodzie
tim eTrial () programu Doubl i ngTest).
■ Uruchom przedstawiony dalej program DoublingRatio. Jest to modyfikacja
program u Doubl i ngTest, obliczająca stosunek danego czasu wykonania do p o
przedniego.
■ Uruchamiaj program dopóty, dopóki stosunek czasów wykonania nie dojdzie
do granicy 2 b.
Test ten nie jest skuteczny, jeśli stosunek nie zbliża się do granicy. Jednak w bardzo
wielu programach można uzyskać taki efekt, co prowadzi do następujących wnio
sków:
■ Tempo wzrostu czasu wykonania wynosi w przybliżeniu Nb.
■ Aby przewidzieć czas wykonania, należy pomnożyć ostatni zaobserwowany
czas wykonania przez 2b i podwoić N. Proces ten m ożna kontynuować dowol
nie długo. Aby uzyskać prognozy dla danych wejściowych o wielkości różnej
niż 2 do M-tej potęgi, m ożna wybrać inny stosunek (zobacz ć w i c z e n i e 1 .4 .9 ).
Jak pokazano dalej, stosunek czasów wykonania dla programu Th reeSum wynosi oko
ło 8 . Można przewidzieć, że czas wykonania dla N = 16 000, 32 000 i 64 000 wyniesie
408,8,3270,4 i 26 163,2 sekundy. Wystarczy w tym celu kilkakrotnie pomnożyć ostat
ni czas dla 8 000 (51,1) przez 8 .
Program do wykonywania eksperymentów
p u b lic c la s s D oub lingR atio
Wyniki eksperymentów
1
p u b lic s t a t ic double t im e T r ia l( in t N)
% ja va D oublingR atio
// Tak samo, jak w programie D oublingTest (stron a 189)
250 0.0 2.7
500 0.0 4.8
p u b lic s t a t ic void m a in (S trin g [] args)
1000 0.1 6.9
{ 2000 0.8 7.7
double prev = t im e T r ia l(125); 6.4 8.0
4000
fo r (in t N = 250; true ; N + s N)
8000 51.1 8.0
1
double time = t im e T r ia l( N );
S td 0 u t.p rin tf("% 6 d % 7.1f ", N, tim e);
S t d 0 u t . p r in t f ( "% 5 . 1 f \n ", tim e/p rev);
prev = time; Prognozy
1
16000 408.8 8.0
1
32000 3270.4 8.0
) 64000 26163.2 8.0
1.4 Q Analizy algorytm ów 205
Test ten jest w przybliżeniu równoznaczny procesowi opisanemu na stronie 188
(przeprowadzanie eksperymentów, rysowanie wartości na diagramie logarytmicznym
w celu ustalenia hipotezy, że czas wykonania to aNb, określanie wartości b na podsta
wie nachylenia linii i obliczanie a), jednak łatwiej go zastosować. Uruchamiając pro
gram Doubl i ngRati o, można ręcznie trafnie przewidzieć wydajność. Kiedy stosunek
zbliża się do przyjętej granicy, wystarczy pomnożyć czas przez stosunek, aby uzupeł
nić kolejne pola w tabeli. Przybliżony model tempa wzrostu to zależność potęgowa,
przy czym potęgą jest tu logarytm binarny stosunku.
Dlaczego stosunek zbliża się do stałej? Proste obliczenia matematyczne pokazują,
że jest tak dla wszystkich często spotykanych temp wzrostu (za wyjątkiem wykład
niczego):
Twierdzenie C (stosunek dla podwojonych danych). Jeśli T (N) ~ aN 1’ lg N, to
T(2N)/T(N) ~ 2h.
Dowód. Natychmiast wynika z poniższych obliczeń:
T(2N)/T(N) = a(2N)hlg (2N)/aNhlg N
= 2 fc(1 + lg 2 / l g N)
~2b
Zwykle nie m ożna ignorować czynnika logarytmicznego przy tworzeniu modelu m a
tematycznego, jednak odgrywa on mniejszą rolę w prognozowaniu wydajności za
pomocą hipotez dla podwajania rozmiaru danych.
należy rozw ażyć przeprowadzenie eksperymentów ze stosunkiem czasu dla
podwojonych danych dla każdego programu, którego wydajność m a znaczenie. Jest
to bardzo prosty sposób na oszacowanie tem pa wzrostu czasu wykonania. Można
dzięki temu wykryć błąd związany z wydajnością, sprawiający, że program jest mniej
wydajny, niż oczekiwano. Ujmijmy to bardziej ogólnie — można stosować hipotezy
na temat tem pa wzrostu czasu wykonania programów, aby przewidywać wydajność
na jeden z opisanych dalej sposobów.
Szacowanie możliwości rozw iązania dużych problem ów Możliwe musi być udzie
lenie odpowiedzi na podstawowe pytanie na temat każdego pisanego programu: Czy
program potrafi przetworzyć określone dane wejściowe w rozsądnym czasie? Aby od
powiedzieć na takie pytanie dla dużych ilości danych, należy dokonać ekstrapolacji
za pomocą czynnika dużo większego niż podwajanie, równego na przykład 10 , co
pokazano w czwartej kolumnie tabeli w dolnej części następnej strony. Dla bankiera
inwestycyjnego tworzącego codziennie modele finansowe, naukowca urucham iają
cego program do analizy danych eksperymentalnych, inżyniera przeprowadzającego
symulacje w celu przetestowania projektu i dla innych osób nie jest niczym niezwy-
206 RO ZD ZIA Ł 1 o Podstawy
kłym regularne uruchamianie programów, których wykonanie trwa kilka godzin.
W tabeli uwzględniono takie sytuacje. Znajomość tem pa wzrostu czasu wykonania
dla algorytmu zapewnia informacje potrzebne do zrozumienia ograniczeń rozmia
ru problemów, jakie m ożna rozwiązać. Zdobywanie takiej wiedzy jest najważniejszą
przyczyną analizowania wydajności. Bez takich informacji prawdopodobnie nie bę
dziesz wiedział, ile czasu zajmie wykonanie programu, natomiast dzięki nim zdołasz
na serwetce obliczyć szacunkowe koszty i podjąć odpowiednie działania.
Szacowanie korzyści z zastosow ania szybszego kom putera Od czasu do czasu
możesz natrafić na inne podstawowe pytanie: O ile szybciej można rozwiązać problem
za pomocą lepszego komputera? Zwykle jeśli nowy kom puter jest x razy szybszy od
starego, m ożna skrócić czas wykonania x razy. Jednak przeważnie nowy komputer
pozwala rozwiązać większe problemy. Jak wpływa to na czas wykonania? Aby odpo
wiedzieć na to pytanie, trzeba znać tempo wzrostu.
z g o d n i e z e s ł y n n ą p r a k t y c z n ą r e g u ł ą znaną jako prawo Moored m ożna oczeki
wać, że za 18 miesięcy pojawi się kom puter o dwukrotnie większej szybkości i z dwa
razy większą ilością pamięci, a za około 5 lat — maszyna 10-krotnie szybsza z 10-
krotnie większą pamięcią. W tabeli poniżej pokazano, że prawo M oorea nie pozwala
„nadążyć” za wzrostem ilości danych, jeśli algorytm jest kwadratowy lub sześcienny.
Rodzaj algorytmu można szybko określić, przeprowadzając test podwajania i spraw
dzając, czy stosunek podwojenia czasu wykonania przy podwojonej wielkości da
nych wejściowych dochodzi do 2, a nie do 4 lub 8 .
Dla p r o g r a m u d z ia ła ją c e g o k ilk a g o d z in
T e m p o w z ro s tu c z a s u .. , , ... ,
r d la d a n y c h w e jś c io w y c h o w ie lk o śc i N
P ro g n o z o w a n y P r o g n o z o w a n y c z a s d la 1
O p is F u n k c ja C z y n n ik 2 C z y n n ik 10
c z a s d la 10/V n a 10 ra z y s z y b s z y m k o m p u
Liniowe N 2 10 D zień K ilka g o d z in
Liniowo-
N lo g N 2 10 D zień K ilka g o d zin
logarytmiczne
Kwadratowe N2 4 100 K ilka ty g o d n i D zie ń
Sześcienne N2 8 1000 K ilka m ie się cy K ilka ty g o d n i
2 n 2 9N
Wykładnicze 2n N ig d y N ig d y
P ro g n o z y n a p o d s ta w ie f u n k c ji t e m p a w z ro s tu
1.4 n Analizy algorytmów 207
Z a s tr z e ż e n ia Jest wiele powodów, z których w czasie szczegółowego analizowania
wydajności program u wyniki mogą być niespójne lub mylące. Wszystkie przyczyny
wynikają z nieprawidłowych założeń będących podstawą hipotez. Można przedsta
wić nowe hipotezy na podstawie nowych założeń, jednak im więcej szczegółów trze
ba uwzględnić, tym więcej staranności wymagają analizy.
D uże stałe Przy przybliżeniach opartych na pierwszym wyrazie ignorowane są stałe
współczynniki w dalszych wyrazach. Nie zawsze jest to uzasadnione. Przykładowo,
w przybliżeniu dla funkcji 2 N 2 + c N szacowanym na ~2 N 2 zakładamy, że c jest małe.
Jeśli jest inaczej (eto na przykład 103lub 106), przybliżenie jest mylące. Dlatego trzeba
uważać na duże stałe.
Pętla w ew nętrzna, która nie dom inuje Założenie, że pętla wewnętrzna dominuje,
nie zawsze jest poprawne. Model kosztów może nie uwzględniać rzeczywistej pętli
wewnętrznej. Ponadto rozmiar problemu (N) może nie być wystarczający, aby pierw
szy wyraz w matematycznym opisie liczby wywołań instrukcji w pętli wewnętrznej
był o tyle większy od dalszych, żeby m ożna pominąć te ostatnie. W niektórych pro
gramach poza pętlą wewnętrzną znajduje się na tyle dużo kodu, że trzeba go uwzględ
nić. Wymaga to dopracowania modelu kosztów.
Czas w ykonania instrukcji Założenie, że każda instrukcja zajmuje tyle samo cza
su, nie zawsze jest poprawne. Przykładowo, w większości współczesnych systemów
komputerowych stosuje się buforowanie przy porządkowaniu pamięci, dlatego do
stęp do elementów z dużej tablicy może zajmować dużo czasu, jeśli nie są one blisko
siebie. Można zaobserwować efekt buforowania dla programu ThreeSum, pozwalając
na dłuższe działanie programu Doubl i ngTest. Stosunek czasów wykonania najpierw
zbliża się do 8, a potem — z uwagi na buforowanie — może nagle wzrosnąć dla du
żych tablic.
Uwzględnianie system u Zwykle w komputerze wykonywanych jest wiele operacji.
Java to jedna z wielu aplikacji współzawodniczących o zasoby. Sama Java ma wiele
opcji i mechanizmów kontrolnych wpływających na wydajność. Mechanizm przy
wracania pamięci, kompilator działający w trybie JIT lub pobieranie danych z inter-
netu mogą w istotny sposób zakłócać wyniki eksperymentów. Kwestie te wpływają na
podstawową zasadę metody naukowej, zgodnie z którą eksperymenty powinny być
powtarzalne — to, co dzieje się w danym momencie na komputerze, nigdy się nie po
wtórzy. Wszystkie inne operacje wykonywane przez kom puter powinny zasadniczo
być pomijalne i kontrolowalne.
Z b y t m ałe różnice Często przy porównywaniu dwóch różnych programów wy
konujących to samo zadanie jeden jest w pewnych sytuacjach szybszy, a w innych
— wolniejszy. Może to wynikać z jednej lub kilku wspomnianych wcześniej kwestii.
Naturalna dla niektórych programistów (i studentów) jest tendencja do poświęcania
dużej ilości energii na przeprowadzenie wyścigów w celu znalezienia najlepszej im
plementacji. Zadanie to najlepiej pozostawić ekspertom.
208 R O ZD ZIA Ł 1 □ Podstawy
D u ża zależność od danych wejściowych Jednym z pierwszych założeń przy okre
ślaniu tem pa wzrostu czasu wykonania jest to, że czas powinien być względnie nieza
leżny od danych wejściowych. Jeśli jest inaczej, wyniki mogą być niespójne, a hipote
za — niemożliwa do sprawdzenia. Załóżmy na przykład, że zmodyfikowano program
ThreeSum w celu udzielenia odpowiedzi na pytanie: Czy dane wejściowe zawierają
trójkę sumującą się do 0? Wartość zwracana ma tu typ bool ean, zamiast c n t + + wystę
puje instrukcja r e t u r n tru e , a ostatnią instrukcją jest r e t u r n fal se. Tempo wzrostu
czasu wykonania programu jest stałe, jeśli trzy pierwsze liczby całkowite sumują się
do 0 , i sześcienne, jeżeli w danych wejściowych w ogóle nie m a takich trójek.
Problemy o wielu param etrach Koncentrowaliśmy się na pomiarze wydajności
jako funkcji od jednego param etru, którym zwykle jest wartość argumentu z wiersza
poleceń lub wielkość danych wejściowych. Jednak czasem param etrów jest więcej.
Typowy przykład to sytuacja, w której algorytm wymaga utworzenia struktury da
nych, a następnie wykonuje ciąg operacji, wykorzystując tę strukturę. Parametrami
dla takich aplikacji jest zarówno wielkość struktury danych, jak i liczba operacji.
Przedstawiono już przykład takiej sytuacji w analizach problemu sprawdzania bia
łej listy z wykorzystaniem wyszukiwania binarnego. Biała lista zawiera tu N liczb,
a standardowe wejście — M liczb. Typowy czas wykonania jest proporcjonalny do
M log N.
Mimo tych wszystkich zastrzeżeń zrozumienie tempa wzrostu czasu wykonania pro
gramu jest cenne dla każdego programisty, a opisane tu m etody dają dużo możliwości
i działają w wielu okolicznościach. Według przemyśleń Knutha m etody te można teo
retycznie stosować w najdrobniejszych detalach, aby uzyskać szczegółowe, precyzyjne
prognozy. Typowe systemy komputerowe są niezwykle złożone, dlatego ścisłe analizy
najlepiej pozostawić ekspertom, jednak te same m etody są skuteczne do określania
przybliżonych szacunków czasu wykonania dowolnego programu. Konstruktor ra
kiet musi móc określić, czy lot testowy zakończy się w oceanie czy w mieście. Badacz
z dziedziny medycyny musi wiedzieć, czy testowany lek zabije pacjentów czy ich wy
leczy. Każdy naukowiec lub inżynier korzystający z program u komputerowego musi
mieć pojęcie, czy potrwa to sekundę czy rok.
1.4 o Analizy algorytmów
R a d z e n ie s o b ie z z a le ż n o ś c ią o d d a n y c h w e jś c io w y c h W wielu proble
mach jednym z najważniejszych zastrzeżeń jest zależność od danych wejściowych,
ponieważ czas wykonania może wtedy znacznie się wahać. Czas wykonania zmody
fikowanej wersji program u ThreeSum wspomnianej na poprzedniej stronie waha się
(w zależności od danych) od stałego do sześciennego, dlatego prognozy wydajności
wymagają dokładniejszych analiz. Pokrótce opisano tu niektóre skuteczne podejścia.
Zastosowano je do niektórych algorytmów w dalszej części książki.
M odele danych wejściowych Jedno z podejść polega na staranniejszym określeniu
w modelu rodzaju danych wejściowych przetwarzanych w rozwiązywanych prob
lemach. Przykładowo, m ożna przyjąć, że liczby w danych wejściowych programu
ThreeSum to losowe wartości typu in t. Podejście to rodzi problemy z dwóch przy
czyn:
D Model może być nierealistyczny.
D Analizy są czasem niezwykle skomplikowane i wymagają umiejętności m ate
matycznych wykraczających poza te dostępne przeciętnemu studentowi lub
programiście.
Pierwszy problem ma większe znaczenie. Często dzieje się tak dlatego, że celem ob
liczeń jest odkrycie cech danych wejściowych. Przykładowo, jak dla program u prze
twarzającego genom można oszacować wydajność dla różnych genomów? Dobry
model opisujący genomy występujące w naturze to właśnie to, czego naukowcy szu
kają, dlatego oszacowanie czasu wykonania program u na danych istniejących w n a
turze sprowadza się do opracowania części tego modelu! Drugi problem prowadzi
do koncentrowania się na wynikach matematycznych tylko dla najważniejszych al
gorytmów. W książce przedstawiono kilka przykładów, w których prosty i wygodny
w użytku m odel danych wejściowych w połączeniu z klasyczną analizą matematycz
ną pomaga przewidzieć wydajność.
Gwarancje wydajności dla najgorszego p rzyp a d ku W niektórych aplikacjach wy
magane jest, aby czas wykonania programu, niezależnie od danych wejściowych, był
krótszy od pewnego limitu. Aby zapewnić tego rodzaju gwarancje wydajności, teore
tycy stosują niezwykle pesymistyczne podejście do wydajności algorytmu i ustalają,
ile wyniesie czas wykonania dla najgorszego przypadku. Takie konserwatywne nasta
wienie może być odpowiednie dla oprogramowania sterującego reaktorem atom o
wym, tem pom atem lub hamulcami samochodu. Należy zagwarantować, że oprogra
mowanie wykona zadanie w określonym czasie, ponieważ jeśli tego nie zrobi, może
dojść do katastrofy. Naukowcy zwykle nie zastanawiają się nad najgorszym przy
padkiem, badając świat. W biologii najgorszym przypadkiem może być wymarcie
rodzaju ludzkiego; w fizyce — koniec wszechświata. Jednak w dziedzinie systemów
komputerowych najgorszy przypadek jest czasem bardzo rzeczywistym problemem,
ponieważ dane mogą być generowane przez innego (potencjalnie złośliwego) użyt
kownika, a nie przez naturę. Na przykład witryny, w których nie stosuje się algoryt
mów z gwarancjami wydajności, są podatne na ataki odmowy usługi (ang. denial of
RO ZD ZIA Ł 1 H Podstawy
service), kiedy hakerzy zgłoszą wiele szkodliwych żądań, co prowadzi do znacznego
spadku wydajności witryny. Dlatego wiele z zaprojektowanych tu algorytmów posia
da gwarancje wydajności. Oto przykłady:
Twierdzenie D. W opartych na liście powiązanej implementacjach typów Bag
( a l g o r y t m 1 .4 ), Stack ( a l g o r y t m 1 .2 ) i Queue ( a l g o r y t m 1 .3 ) wszystkie ope
racje w najgorszym przypadku zajmują stały czas.
Dowód. Wynika bezpośrednio z kodu. Liczba instrukcji wykonywanych dla
każdej operacji jest ograniczona przez niską stałą. Zastrzeżenie: dowód opar
ty jest na (sensownym) założeniu, zgodnie z którym system Javy tworzy nowy
obiekt Node w stałym czasie.
Algorytm y z randomizacją Ważnym sposobem na zapewnienie gwarancji wydaj
ności jest randomizacja (czyli wprowadzenie losowości). Przykładowo, algorytm sor
towania szybkiego opisany w p o d r o z d z i a l e 2.3 (jest to prawdopodobnie najczęściej
stosowany algorytm sortowania) jest w najgorszym przypadku kwadratowy, jednak
losowe uporządkowanie danych wejściowych daje gwarancję probabilistyczną, że czas
wykonania będzie liniowo-logarytmiczny. Przy każdym uruchomieniu algorytmu jego
wykonanie zajmie inną ilość czasu, jednak prawdopodobieństwo, że czas nie będzie
liniowo-logarytmiczny, jest tak małe, iż można je pominąć. Podobnie algorytmy ha-
szujące dla tablicy symboli omówione w p o d r o z d z i a l e 3.4 (jest to prawdopodobnie
najczęściej stosowane rozwiązanie) są w najgorszym przypadku liniowe, natomiast
przy gwarancji probabilistycznej działają w stałym czasie. Gwarancje probabilistyczne
nie są bezwzględne, jednak prawdopodobieństwo ich niedotrzymania jest mniejsze niż
tego, że komputer zostanie trafiony przez błyskawicę. Dlatego gwarancje tego rodzaju
są w praktyce przydatne jako gwarancje dla najgorszego przypadku.
Ciągi operacji W wielu aplikacjach „dane wejściowe” algorytmu to nie tylko dane, ale
też ciągi operacji wykonywanych przez klienta. Stos, którego klient najpierw dodaje N
wartości, a następnie zdejmuje je wszystkie, może mieć wydajność inną niż w sytua
cji, kiedy klient na zmianę wykonuje N operacji dokładania i zdejmowania elementów.
W analizach trzeba uwzględnić obie sytuacje lub dodać sensowny model ciągu operacji.
A n a lizy z uwzględnieniem am ortyzacji Inny sposób na zapewnienie gwarancji
wydajności polega na amortyzacji kosztów. Technika ta polega na rejestrowaniu
łącznych kosztów wszystkich operacji i dzieleniu sumy przez liczbę operacji. W tym
podejściu m ożna zezwolić na kosztowne operacje, zachowując niski średni koszt.
Prototypowym przykładem analiz tego rodzaju są badania nad tablicą o zmiennej
wielkości dla typu Stack przedstawione w p o d r o z d z i a l e 1.3 ( a l g o r y t m 1 . 1 ze stro
ny 153). Dla uproszczenia załóżmy, że N jest potęgą dwójki. Zaczynamy od pustej
struktury. Ile elementów tablicy zostanie użytych przy N kolejnych wywołaniach me
tody pus h () ? Łatwo jest ustalić tę wartość. Liczba dostępów do tablicy wynosi:
1.4 a Analizy algorytm ów 211
N + 4 + 8 + 1 6 + ... + 2 N = 5 N - 4 2 56-
Pierwszy wyraz określa liczbę dostępów
Jedna szara kropka 128
do tablicy w każdym z N wywołań metody 5 ~ dla każdej operacji /
TT .>
push(). Dalsze wyrazy dotyczą dostępów 64
potrzebnych przy inicjowaniu struktury N T
5 >/ Czerwone kropki określają
O średnią skum ulowaną 5
danych przy każdym podwajaniu jej wiel
\
kości. Tak więc średnia liczba dostępów do
Liczba operacji add() 128
tablicy na operację jest stała, choć ostatnia
operacja zajmuje czas liniowy. Są to analizy Zamortyzowane koszty dodawania
elementów do kolekcji RandomBag
z uwzględnieniem amortyzacji, ponieważ
koszt niewielu długich operacji rozdzielo
no, przypisując jego część do każdej z wielu krótkich operacji. Klasa Vi sual Accumu 1ato r
umożliwia łatwe przedstawienie tego procesu, co pokazano powyżej.
Twierdzenie E. W implementacji klasy Stack ( a l g o r y t m i . i ) opartej na tabli
cy o zmiennej wielkości średnia liczba dostępów do tablicy dla dowolnego ciągu
operacji przy początkowo pustej strukturze danych jest w najgorszym przypadku
stała.
Zarys dowodu. Dla każdego wywołania push() powodującego powiększenie
tablicy (na przykład z rozmiaru N do 2 N) należy rozważyć N I2 - 1 operacji pus h (),
które ostatnio spowodowały zwiększenie tablicy do A: (dla A: równego między N I2
+ 2 a N). Uśredniając 4N dostępów do tablicy potrzebnych do jej powiększenia
z N/2 dostępami do tablicy (po jednym dla każdego wywołania push ()), można
uzyskać średni koszt dziewięciu dostępów do tablicy na operację. Udowodnienie,
że liczba dostępów do tablicy dla dowolnego ciągu M operacji jest proporcjonal
na do Ai, to trudniejsze zadanie (zobacz ć w i c z e n i e 1 .4 .3 2 ).
Analizy tego rodzaju mają wiele zastosowań. Tablice o zmiennej wielkości zastosowano
jako struktury danych dla kilku algorytmów omówionych w dalszej części książki.
z a d a n ie m jest odkrycie tylu ważnych informacji o algo
a n a l it y k a a lg o r y t m ó w
rytmie, jak to możliwe. Programista aplikacji odpowiada za zastosowanie tej wiedzy
do rozwijania programów skutecznie rozwiązujących problemy. W idealnych w arun
kach algorytmy powinny prowadzić do przejrzystego i zwięzłego kodu, zapewniają
cego dobre gwarancje i wysoką wydajność dla ważnych danych. Z uwagi na te cechy
wiele klasycznych algorytmów omówionych w tym rozdziale ma znaczenie w wielu
kontekstach. Stosując te algorytmy jako model, można samodzielnie rozwijać dobre
rozwiązania typowych problemów napotykanych w trakcie programowania.
212 R O ZD ZIA Ł 1 n Podstawy
Pamięć Wykorzystanie pamięci przez program, podobnie jak czas wykonania, wiąże
się bezpośrednio ze światem fizycznym. Duża część układów komputera umożliwia pro
gramom zapisywanie wartości i późniejsze ich pobieranie. Im więcej wartości program
musi przechowywać w danym momencie, tym więcej układów potrzebuje. Zapewne
znasz ograniczenia ilości pamięci na swoim komputerze (nawet lepiej niż limity związa
ne z czasem), ponieważ zapłaciłeś dodatkowe pieniądze za większą ilość pamięci.
Wykorzystanie pamięci przez Javę jest dobrze zdefiniowane dla danego kom pu
tera (każda wartość wymaga dokładnie tej samej ilości pamięci przy każdym u ru
chomieniu programu), jednak Java jest zaimplementowana dla bardzo różnorodnych
urządzeń obliczeniowych, a ilość zajmowanej pamięci jest zależna od implementacji.
W celu zachowania zwięzłości używamy słowa typowe do określenia, że wartości są
zależne od maszyny.
Jednym z najważniejszych mechanizmów Javy jest system aloka-
Typ Bajty cji pamięci. Ma on zwolnić programistów z konieczności zajmowania
boolean 1
się pamięcią. Oczywiście, w odpowiednich sytuacjach warto korzystać
z tego mechanizmu. Jednak trzeba wiedzieć (przynajmniej w przybliże
byte 1 niu), kiedy pamięciowe wymagania program u uniemożliwią rozwiąza
char 2 nie danego problemu.
Analizowanie wykorzystania pamięci jest dużo łatwiejsze od anali
in t 4
zowania czasu wykonania — przede wszystkim dlatego, że nie trzeba
float 4 uwzględniać tak wielu instrukcji program u (ważne są tylko deklaracje),
long
a analizy pozwalają zredukować złożone obiekty do typów prostych,
8
dla których wykorzystanie pamięci jest dobrze zdefiniowane i łatwe do
double 8 zrozumienia (wystarczy określić liczbę zmiennych i pomnożyć ją przez
Typowe w ym agania odpowiednią dla typu liczbę bajtów). Ponieważ w Javie typ danych i nt
pamięciowe dla to zbiór wartości z przedziału od -2 147 483 648 do 2 147 483 647, co
typów prostych
daje w sumie 2 32 różnych wartości, w typowych implementacjach Javy
do reprezentowania wartości typu i nt służą 32 bity. Podobne rozważa
nia dotyczą innych typów prostych. W typowych implementacjach Javy używane są
8 -bitowe bajty, wartości char reprezentowane są za pom ocą 2 bajtów (16 bitów), każ
da wartość doubl e i 1ong zajmuje 8 bajtów (64 bity), a wartość typu bool ean — 1 bajt
(ponieważ komputery zwykle korzystają z pamięci po jednym bajcie). W połączeniu
z wiedzą o ilości dostępnej pamięci na podstawie tych wartości m ożna obliczyć ogra
niczenia. Przykładowo, jeśli w komputerze dostępny jest gigabajt pamięci (miliard
bajtów), nie można w niej jednocześnie pomieścić więcej niż około 32 miliony war
tości typu i nt lub 16 milionów wartości typu doubl e.
Z drugiej strony, analizowanie wykorzystania pamięci jest zależne od rozmaitych róż
nic w sprzęcie i implementacjach Javy, dlatego przedstawione tu specyficzne przykłady
należy traktować jako wskazówki na temat określania zużycia pamięci, a nie ostateczne
informacje dotyczące Twojego komputera. Przykładowo, wiele struktur danych obej
muje reprezentację adresów maszynowych, a ilość pamięci potrzebnej na takie adresy
jest różna w zależności od maszyny. Dla spójności przyjmijmy, że do reprezentowania
1.4 □ Analizy algorytmów 213
adresów służy 8 bajtów, co jest typowe dla Obiekt nakładkowy dla liczb całkowitych 24 bajty
p u b lic c l a s s In t e g e r
powszechnie używanych obecnie architek { Narzut
p r iv a t e i n t x;
tur 64-bitowych. Warto jednak pamiętać, że dla
obiektu
w wielu starszych maszynach używano ar } Wartość
typu i n t
chitektury 32-bitowej, która wymagała tylko Dopełnienie
4 bajtów na adres maszynowy.
Obiekt dla daty 32 bajty
Obiekty Aby określić, ile pamięci zajmuje p u b l i c c l a s s D a te
{
obiekt, należy dodać ilość pamięci zajmo p r iv a t e i n t day; Narzut
p r i v a t e i n t m onth; dla
waną przez każdą zmienną egzemplarza p r iv a t e in t ye a r; obiektu
do narzutu powiązanego z każdym obiek day
month
Wartości
tem (zwykle narzut ten wynosi 16 bajtów). typu i n t
year
Narzut obejmuje referencję do klasy obiek Dopełnienie
tu, informacje na potrzeby przywracania
pamięci i informacje związane z synchroni Obiekt licznika 32 bajty
p u b lic c l a s s C ounter
zacją. Ponadto pamięć jest zwykle dopełnia {
p r i v a t e S t r i n g name; Narzut
na do wielokrotności 8 bajtów (jest to słowo p r iv a t e i n t co u n t; dla Referencja
obiektu
maszynowe w maszynach 64-bitowych). Na do obiektu
}" ^ s trin g
przykład obiekt typu Integer zajmuje 24
Wartość
bajty (16 bajtów narzutu, 4 bajty na zmienną cou nt
typu i n t
Dopełnienie
egzemplarza typu i nt i 4 bajty dopełnienia).
Obiekt typu Date (strona 103) zajmuje 32 Obiekt węzła (klasa wewnętrzna) 40 bajtów
bajty — 16 bajtów narzutu, 4 bajty na każ p u b l i c c l a s s Node
dą zmienną egzemplarza typu i nt i 4 baj { Narzut
p r i v a t e Ite m ite m ;
dla
p r i v a t e Node n e x t ;
ty dopełnienia. Referencja do obiektu jest obiektu
zwykle adresem pamięci, dlatego zajmuje } Dodatkowy
narzut
8 bajtów. Na przykład obiekt typu Counter
(strona 101) zajmuje 32 bajty — 16 bajtów
Referencje
narzutu, 8 bajtów na zmienną egzemplarza
typu S tri ng (referencję), 4 bajty na zmienną
egzemplarza typu i nt i 4 bajty dopełnienia. Typowe wymagania pamięciowe obiektów
Przy obliczaniu pamięci na referencję pa
mięć na sam obiekt uwzględniana jest osob
no, dlatego tu pamięci zajmowanej przez wartość typu S tri ng nie wzięto pod uwagę.
Listy p o w iązane Zagnieżdżona niestatyczna (wewnętrzna) klasa, taka jak klasa
Node (strona 154), wymaga dodatkowych 8 bajtów narzutu (na referencję do m a
cierzystego egzemplarza). Dlatego obiekt typu Node zajmuje 40 bajtów (16 bajtów
narzutu dla obiektu, po 8 bajtów na referencje do obiektów typu Item i Node oraz
8 bajtów dodatkowego narzutu). Obiekt typu Integer zajmuje 24 bajty, dlatego stos
z Nliczb całkowitych oparty na liście powiązanej ( a l g o r y t m 1 .2 ) wymaga 32 + 64N
bajtów — standardowo 16 na narzut dla obiektu typu Stack, 8 na zmienną egzempla
rza w postaci referencji, 4 na zmienną egzemplarza typu i nt, 4 na dopełnienie i 64 dla
każdego elementu (40 na obiekt typu Node i 24 na obiekt typu Integer).
214 R O Z D Z IA L I n Podstawy
Tablice Typowe wymogi pamięciowe dla różnych rodzajów tablic Javy przedsta
wiono na diagramach na następnej stronie. Tablice w Javie są implementowane jako
obiekty i zwykle wymagają dodatkowego narzutu na długość. Tablica wartości typu
prostego zazwyczaj wymaga 24 bajtów informacji nagłówkowych (16 bajtów narzutu
dla obiektu, 4 bajtów na długość i 4 bajtów dopełnienia) plus pamięci na zapisanie
wartości. Na przykład tablica N wartości typu i nt zajmuje 24 + 4N bajtów (w za
okrągleniu w górę do wielokrotności liczby 8 ), a tablica N wartości typu doubl e — 24
+ 8N bajtów. Tablica obiektów to tablica referencji do obiektów, dlatego trzeba do
dać pamięć na referencje do pamięci potrzebnej na obiekty. Na przykład tablica N
obiektów typu Date (strona 103) zajmuje 24 bajty (narzut dla tablicy) plus 8N bajtów
(referencje) plus 32 bajty na każdy obiekt i 4 bajty dopełnienia, co w sumie daje 24 +
40Nbajtów. Tablica dwuwymiarowa to tablica tablic (każda tablica jest obiektem). Na
przykład dwuwymiarowa ta b lic a M n a N z wartościami typu doubl e zajmuje 2 4 bajty
(narzut dla tablicy tablic) plus 8M bajtów (referencje do wierszy tablicy) plus M razy
16 bajtów (narzut dla wierszy tablicy) plus M razy N razy 8 bajtów (dla N wartości
typu doubl e w każdym z M wierszy), co w sumie daje 8N M + 32M + 24 ~ 8N M baj
tów. Jeśli elementy tablicy to obiekty, podobne rachunki prowadzą do sumy 8N M +
32M + 24 ~ 8N M bajtów dla tablicy tablic wypełnionej referencjami do obiektów
(plus pamięć na same obiekty).
O biekty typu String Pamięć dla obiektów typu S t r i ng Javy obliczana jest w taki sam
sposób, jak dla innych obiektów, przy czym dla łańcuchów znaków typowe jest utoż
samianie nazw. Standardowa implementacja typu S t r in g obejmuje cztery zmienne
egzemplarza: referencję do tablicy łańcuchów znaków (8 bajtów) i trzy wartości typu
in t (po 4 bajty). Pierwsza wartość typu in t to pozycja w tablicy znaków; druga to
długość łańcucha znaków. W kategoriach nazw zmiennych egzemplarza z rysunku
na następnej stronie łańcuch znaków składa się ze znaków od val ue [offset] do
value [o ffse t + count - 1], Trzecia wartość in t w obiektach typu S t r i ng to skrót,
który pozwala w pewnych warunkach (nie mają one tu znaczenia) uniknąć powta
rzania obliczeń. Dlatego każdy obiekt typu S t r i ng zajmuje łącznie 40 bajtów (16 baj
tów narzutu dla obiektu plus 4 bajty na każdą zmienną egzemplarza typu i nt plus
8 bajtów na referencję do tablicy plus 4 bajty dopełnienia). Jest to pamięć potrzebna
oprócz pamięci na same znalu, znajdujące się w tablicy. Pamięć na znaki liczona jest
osobno, ponieważ tablice elementów typu char często są współużytkowane przez
różne łańcuchy znaków. Ponieważ obiekty typu S trin g są niezmienne, rozwiązanie
to pozwala w implementacji na zaoszczędzenie pamięci, jeśli obiekty tego typu mają
tę samą tablicę val ue [].
Wartości typu String ipo d ła ń cu ch y Obiekt typu S trin g o długości N zwykle zaj
muje 40 bajtów (na obiekt typu S tri ng) plus 24 + 2N bajtów (na tablicę ze znakami),
co w sumie daje 64 + 2 N bajtów. Jednak przy przetwarzaniu łańcuchów znaków typowe
jest korzystanie z podłańcuchów, a reprezentacja łańcuchów znaków w Javie um oż
liwia stosowanie podłańcuchów bez konieczności tworzenia kopii znaków łańcucha.
1.4 o Analizy algorytmów 215
T a b lic a w a rto ści ty p u i n t Tablica w a rto ści ty p u d o u b l e
in t [] a = new i n t [ N ] ; d o u b le t ] c = new d o u b le [ N ] ;
Narzut 16 bajtów Narzut 16 bajtów
dla dla
obiektu obiektu
Wartość Wartość
N N
typu i n t - typu i n t -
Dopełnienie Dopełnienie
(A bajty) (4 bajty)
. A/ wartości
; typu i n t N wartości
' (4A/ bajtów) typu d o u b le 24 + 8 N bajtów
y / (8N bajtów)
Łącznie: 24 + 4 N
(dla parzystego N)
Łącznie: 24 + 8 N
Tablica o b ie k tó w 32 bajty Tablica ta b lic (tab lica d w u w y m ia ro w a ) N wartości
typu d o u b le
d o u b le f ] t] t ; j S / (8N bajtów)
t = new d o u b le fM ] [N] ;
16 bajtów
M referencji
(8M bajtów)
Date[] ~40/V Łącznie: 24 + 8 M + M x (24 + 8N) = 24 + 32 M + 3 MN
doublet] [] ~8NM
Typow e w y m o g i p a m ię cio w e d la ta b lic z w a rto ścia m i ty p u i nt i doubl e, o b ie k ta m i i ta b lic am i
216 R O Z D Z IA L I □ Podstawy
O b ie k t ty p u s t r i n g (z b ib lio te k i Javy) Za pomocą metody substring () można utwo-
40bajtów rzyć nowy obiekt typu S tring (40 bajtów), ko
p u b lic c l a s s S t r in g
rzystając jednak z tej samej tablicy value[],
Narzut
{ dla dlatego podlańcuch istniejącego łańcucha zna
p r iv a t e c h a r [ ] v a lu e ;
p r iv a t e in t o ffs e t; obiektu
p r iv a t e i n t c o u n t;
ków zajmuje tylko 40 bajtów. Tablica znaków
p r iv a t e i n t h a sh ; v a lu e - Referencja zawierająca pierwotny łańcuch znaków otrzy
}” o ffse t
■" Wartości muje nową nazwę w obiekcie podłańcucha. Pola
' typu i n t z pozycją i długością wyznaczają podłańcuch.
h a sh
Dopełnienie Ujmijmy to inaczej — podłańcuch zajmuje stałą
P rz y k ła d o w y p o d ła ń c u c h ilość dodatkowej pamięci, a utworzenie go zajmu
S t r i n g genome = CGCCTGGCGTCTGTAC"; je stały czas, nawet jeśli długości łańcucha i pod
S t r i n g cod on = g e n o m e . s u b s t r in g ( 6 , 3 ) ;
genome
łańcucha są bardzo duże. Naiwna reprezentacja
oparta na kopiowaniu znaków przy tworzeniu
podłańcucha wymaga czasu i pamięci rosnących
liniowo. Możliwość tworzenia podłańcuchów za
pomocą pamięci (i czasu) w ilości niezależnej od
długości podłańcucha jest kluczem do wydajne
go działania wielu podstawowych algorytmów
do przetwarzania łańcuchów znaków.
p o d s t a w o w e m e c h a n i z m y są przydatne
te
do szacowania wykorzystania pamięci w bar
dzo licznych programach, istnieje jednak wie
le czynników, które utrudniają to zadanie.
W spom niano już o potencjalnym efekcie utoż
samiania nazw. Ponadto wykorzystanie pa
mięci jest skomplikowanym i dynamicznym
procesem, jeśli należy uwzględnić wywołania
funkcji, ponieważ mechanizm alokacji pam ię
ci systemowej odgrywa wtedy ważniejszą rolę
z uwagi na specyfikę każdego systemu. Przykładowo, kiedy program wywołuje m eto
dę, system alokuje potrzebną jej pamięć (na zmienne lokalne) ze specjalnego obszaru
nazywanego stosem (jest to stos systemowy). Kiedy metoda zwraca sterowanie do
miejsca wywołania, pamięć jest zwracana na stos. Dlatego tworzenie tablic lub in
nych dużych obiektów w programach rekurencyjnych jest niebezpieczne, ponieważ
każde rekurencyjne wywołanie powoduje zajęcie dużej ilości pamięci. Przy tworzeniu
obiektu za pom ocą słowa new system alokuje potrzebną na obiekt pamięć z innego
specjalnego obszaru pamięci, ze sterty (nie jest to sterta binarna omówiona w p o d
r o z d z i a l e 2 .4 ). Trzeba pamiętać, że każdy obiekt istnieje tak długo, jak referencje do
niego. Po usunięciu referencji proces systemowy {mechanizm przywracania pamięci)
odzyskuje pamięć na stercie. Ta dynamika może utrudnić precyzyjne oszacowanie
wykorzystania pamięci.
1.4 ■ Analizy algorytmów 217
P e r s p e k ty w a Wysoka wydajność jest ważna. Niezwykle wolny program jest pra
wie tak bezużyteczny, jak program niepoprawny, dlatego z pewnością warto zwrócić
uwagę na koszty, aby wiedzieć, jakiego rodzaju problemy są możliwe do rozwiązania.
Zawsze warto mieć pojęcie zwłaszcza o tym, który kod stanowi wewnętrzną pętlę
programów.
Prawdopodobnie najczęstszym błędem w programowaniu jest zwracanie nad
miernej uwagi na cechy związane z wydajnością. Priorytetem jest pisanie przejrzy
stego i prawidłowego kodu. Modyfikowanie program u wyłącznie w celu przyspie
szenia go najlepiej pozostawić ekspertom. Zresztą, takie zmiany często dają efekty
przeciwne do zamierzonych, ponieważ powstaje wtedy skomplikowany i trudny do
zrozumienia kod. C.A.R. Hoare (twórca algorytmu sortowania szybldego oraz zna
ny zwolennik pisania przejrzystego i poprawnego kodu) kiedyś streścił to podejście,
stwierdzając, że: „Przedwczesna optymalizacja jest źródłem wszelkiego zła”. Knuth
dookreślił to: „(a przynajmniej większości) w programowaniu”. Oprócz tego popra
wa czasu wykonania nie jest warta zachodu, jeśli możliwe korzyści są nieistotne.
Przykładowo, 10-krotna poprawa czasu wykonania w programie, w którym ten czas
jest stały, nie m a znaczenia. Nawet jeśli program działa kilka minut, łączny czas p o
trzebny na zaimplementowanie i zdiagnozowanie ulepszonego algorytmu może być
znacząco dłuższy niż czas pracy nieco wolniejszej wersji. Lepiej pozwolić wtedy na
wykonanie pracy komputerowi. Co gorsza, możesz poświęcić dużo czasu i wysiłku
na zaimplementowanie rozwiązań, które w teorii powinny usprawnić program, ale
w praktyce tego nie robią.
Prawdopodobnie drugim najczęstszym błędem w programowaniu jest ignorowa
nie cech związanych z wydajnością. Szybsze algorytmy są często bardziej skompliko
wane od algorytmów opartych na ataku siłowym, dlatego kusząca jest myśl o zaak
ceptowaniu wolniejszego algorytmu, aby uniknąć zmagań z bardziej skomplikowa
nym kodem. Jednak czasem już kilka wierszy dobrego kodu pozwala uzyskać znacz
ne korzyści. Użytkownicy zaskakująco wielu systemów komputerowych tracą dużo
czasu w oczekiwaniu na zakończenie rozwiązywania problemu przez oparte na ataku
siłowym algorytmy o złożoności kwadratowej, choć dostępne są algorytmy liniowe
lub liniowo-logarytmiczne, które zakończyłyby pracę w o wiele krótszym czasie. Jeśli
rozmiar problemu jest bardzo duży, często nie ma innej możliwości niż poszukanie
lepszych algorytmów.
Zwykle stosujemy opisaną w tym podrozdziale metodykę do szacowania wyko
rzystania pamięci i formułowania hipotez na tem at tem pa wzrostu czasu wykonania
na podstawie przybliżeń z tyldą uzyskanych przez przeprowadzenie analiz m atem a
tycznych opartych na modelu kosztów. Hipotezy te sprawdzamy eksperymentalnie.
Ulepszenie program u tak, aby był bardziej przejrzysty, wydajny i elegancki, zawsze
powinno być celem pracy nad nim. Jeśli w trakcie rozwijania program u cały czas
zwracasz uwagę na koszty, będziesz mógł czerpać z tego korzyści przy każdym jego
uruchomieniu.
RO ZD ZIA Ł 1 ■ Podstawy
Pytania i odpowiedzi
P. Dlaczego użyto pliku lM [Link], zamiast generować losowe wartości za pomocą
biblioteki StdRandom?
O. Dzięki tem u łatwiej jest diagnozować rozwijany kod i powtarzać eksperymen
ty. Biblioteka StdRandom przy każdym uruchom ieniu generuje różne wartości, dla
tego wywołanie programu po rozwiązaniu błędu czasem nie pozwala przetestować
poprawki! Można użyć m etody i ni t i al i ze () z biblioteki StdRandom, aby rozwiązać
ten problem, jednak pliki w rodzaju lM [Link] ułatwiają dodawanie przypadków
testowych w trakcie diagnozowania. Ponadto programiści mogą porównać wydaj
ność kodu na różnych komputerach bez uwzględniania m odelu danych wejściowych.
Po zakończeniu diagnozowania program u i kiedy wiesz już, jaką ma wydajność,
z pewnością warto przetestować go na losowych danych. Podejście to zastosowano
w programach Doubl i ngTest i Doubl ingRatio.
P. Uruchomiłem program Doubl i ngRati o na moim komputerze, ale wyniki nie były
spójne z tymi z książki. Niektóre stosunki nie były bliskie 8 . Dlaczego?
O. Dlatego przedstawiono „zastrzeżenia” na stronie 207. Prawdopodobnie system
operacyjny Twojego komputera w czasie eksperymentów wykonywa! inne operacje.
Jednym ze sposobów na złagodzenie takich problemów jest poświęcenie dodatko
wego czasu i przeprowadzenie większej liczby eksperymentów. Możesz na przykład
zmodyfikować program Doubl i ngTest tak, aby przeprowadził eksperymenty 1000
razy dla każdego N. Da to dużo dokładniejsze szacunki czasu wykonania dla każdej
wielkości danych (zobacz ć w i c z e n i e 1 .4 .39 ).
P. Co dokładnie oznacza „wraz z rosnącym N ” w definicji notacji tyldy?
O. Formalna definicja/(N) ~ g(N) to N^^fibTj/giN) = 1.
P. Widziałem inne notacje opisujące tempo wzrostu. O co w tym chodzi?
O. Powszechnie stosuje się notację dużego O. Mówimy, że/(iV) m a złożoność 0(g(N)),
jeśli istnieją stałe c i N 0, takie że \f(N)\ < cg(N) dla wszystkich N > N 0. Notacja ta jest
bardzo przydatna do określania górnego ograniczenia asymptotycznego dla wydaj
ności algorytmów. Ma to znaczenie w teorii algorytmów, jednak nie jest przydatne do
prognozowania wydajności lub porównywania algorytmów.
P. Dlaczego nie?
O. Głównym powodem jest to, że notacja opisuje tylko górne ograniczenie czasu wy
konania. Rzeczywista wydajność może być znacznie wyższa. Czas wykonania algo
rytm u może wynosić zarówno O(isF), jak i ~ a AMog N. Dlatego notacji dużego O nie
można wykorzystać do uzasadnienia technik w rodzaju testów podwajania (zobacz
t w i e r d z e n i e c na stronie 205).
1.4 n Analizy algorytmów 219
P. Dlaczego więc notacja dużego O jest tak powszechnie stosowana?
O. Ponieważ ułatwia określanie ograniczeń tem pa wzrostu nawet dla skomplikowa
nych algorytmów, dla których dokładniejsze analizy mogą być niemożliwe. Ponadto
jest zgodna z notacjami dużej O i dużej ©, których teoretycy z dziedziny nauk kom
puterowych używają do kategoryzowania algorytmów przez określanie ograniczenia
ich wydajności dla najgorszego przypadku. Mówimy, żef(N ) jest Cl(g(N)), jeśli istnie
ją stałe c i N 0, takie że [/(N)| > c g W dla N > N(). Jeżeli f(N ) jest O (g(N)) i Q(g(N)),
mówimy, że/(N ) jest ©(g-(NJ). Notację dużej O stosuje się zwykle do opisywania dol
nego ograniczenia dla najgorszego przypadku, a notacja dużej © służy zazwyczaj do
opisu wydajności algorytmów optymalnych (w tym sensie, że nie istnieje algorytm
o lepszym asymptotycznym tempie wzrostu dla najgorszego przypadku). Algorytmy
optymalne oczywiście warto rozważać w zastosowaniach praktycznych, jednak — jak
się okaże — trzeba uwzględnić także wiele innych kwestii.
P. Czy asymptotyczne górne ograniczenie wydajności nie jest ważne?
O. Tak, ale wolimy omawiać dokładne wyniki w kategoriach liczby wywołań instruk
cji w kontekście modelu kosztów. To podejście zapewnia więcej informacji na temat
wydajności algorytmu, a dla opisywanych algorytmów można uzyskać tego typu wy
niki. Mówimy na przykład, że „program ThreeSum uzyskuje dostęp do tablicy -AP/2
razy” lub „liczba wywołań cnt++ w programie ThreeSum dla najgorszego przypadku
wynosi ~N}/6”. Jest to dłuższe, ale i dużo bogatsze w informacje stwierdzenie niż
„czas wykonania program u ThreeSum wynosi O iN 3)”.
P. Kiedy tempo wzrostu czasu wykonania algorytmu wynosi N log N, test podwa
jania prowadzi do hipotezy, że czas wykonania wynosi ~ a N dla stałej a. Czy nie
stanowi to problemu?
O. Trzeba zachować ostrożność i nie tworzyć konkretnych modeli matematycznych
na podstawie danych eksperymentalnych. Jednak przy prognozowaniu wydajno
ści wspomniana sytuacja nie stanowi problemu. Przykładowo, jeśli N m a wartość
między 16 000 a 32 000, punkty dla I4N i N lg N znajdują się bardzo blisko siebie.
Dane pasują do obu krzywych. Wraz ze wzrostem N krzywe stają się jeszcze bliższe.
Eksperymentalne sprawdzenie hipotezy, że czas wykonania algorytmu jest liniowo-
logarytmiczny, a nie liniowy, wymaga dokładności.
P. Czy instrukcja i nt [] a = new i nt[N] liczona jest jako N dostępów do tablicy
(potrzebnych do zainicjowania elementów wartościami 0 )?
O. Zwykle tak, dlatego w książce stosujemy takie założenie, choć kompilator o za
awansowanej implementacji może próbować uniknąć ponoszenia takich kosztów dla
dużych rzadkich tablic.
ROZDZIAŁ 1 o Podstawy
j ĆWICZENIA
1.4.1. Wykaż, że liczba różnych trójek, które m ożna wybrać z N elementów, wynosi
dokładnie N (N -l){N -2 )/6 . Wskazówka: zastosuj indukcję matematyczną lub twier
dzenie o zliczaniu (ang. counting argument).
1.4.2. Zmodyfikuj program ThreeSum, tak aby działał poprawnie nawet dla tak du
żych wartości typu i nt, że dodanie dwóch z nich może powodować przepełnienie.
1.4.3. Zmodyfikuj program Doubl i ngTest, aby używał biblioteki StdDraw do gene
rowania rysunków w rodzaju wykresów standardowych lub logarytmicznych z teks
tu. W razie potrzeby należy stosować zmianę skali, żeby rysunek zawsze zajmował
dużą część okna.
1.4.4. Utwórz dla program u TwoSum tabelę podobną do tej ze strony 193.
1.4.5. Podaj przybliżenia z tyldą dla poniższych wartości:
a. N + 1
b. 1 + 1/N
c. (1 + 1/N)(1 + 2IN)
d. 2N 3 - 15N2 + N
e. lg(2N)/lg N
f lg ( N W l) /lg W
g. N 100 / 2 N
1.4.6. Podaj tempo wzrostu czasu wykonania (jako funkcję od N) dla każdego z po
niższych fragmentów kodu:
a) in t sum = 0 ;
fo r (in t n = N; n > 0; n /= 2)
fo r (in t i = 0 ; i < n; i++)
sum++;
b) in t sum = 0 ;
fo r (in t i = 1; i < N; i * = 2 )
fo r (in t j = 0;j < i ; j++)
sum++;
1.4 a Analizy algorytmów 221
c) in t sum = 0 ;
fo r (in t i = 1; i < N; i *= 2)
fo r (in t j = 0; j < N; j++)
sum++;
1.4.7. Przeanalizuj program ThreeSum w m odelu kosztów, w którym uwzględniane
są operacje arytmetyczne (i porównania) na wejściowych liczbach.
1.4.8. Napisz program do określania liczby par równych sobie wartości z pliku wej
ściowego. Jeśli pierwszy zaprojektowany algorytm jest kwadratowy, pomyśl ponow
nie i użyj m etody A rrays. s o rt () do opracowania rozwiązania liniowo-logarytmicz-
nego.
1.4.9. Podaj wzór na prognozowanie czasu wykonania program u dla problemu
0 rozmiarze N, jeśli eksperymenty z podwajaniem wykazały, że czynnik to 2 b, a czas
wykonania dla problemu o wielkości NQwynosi T.
1.4.10. Zmodyfikuj wyszukiwanie binarne tak, aby zawsze zwracało element o naj
mniejszym indeksie pasujący do szukanego elementu (czas wykonania nadal ma być
logarytmiczny).
1.4.11. Dodaj do typu StaticSEToflnts (strona 111) metodę egzemplarza howMa-
ny(), znajdującą liczbę wystąpień danego klucza w czasie proporcjonalnym do log N
(dla najgorszego przypadku).
1.4.12. Napisz program, który pobiera dwie posortowane tablice N wartości typu i nt
1wyświetla wszystkie elementy (posortowane) występujące w obu tablicach. Czas wy
konania programu powinien być proporcjonalny do N (dla najgorszego przypadku).
1.4.13. Na podstawie założeń przedstawionych w tekście podaj ilość pamięci po
trzebnej na przedstawienie obiektów poniższych typów:
a. Accumulator
b. Transaction
c. FixedCapacityStackOfStrings o pojemności C i N elementach
d. Point2D
e. In terval ID
f. Interval2D
g. Double
RO ZD ZIA Ł 1 ■ Podstawy
U PROBLEMY DO ROZWIĄZANIA
1.4.14. Sumy czwórek. Opracuj algorytm rozwiązujący problem sum czwórek.
1.4.15. Szybszy algorytm dla sum trójek. Jako wstęp opracuj implementację
TwoSumFaster z wykorzystaniem liniowego algorytmu zliczającego pary sumujące się
do zera dla posortowanej tablicy (zamiast opartego na wyszukiwaniu binarnym algo
rytm u liniowo-logarytmicznego). Następnie zastosuj podobne podejście do utworze
nia kwadratowego algorytmu dla problemu sum trójek.
1.4.1 6 . Najbliższa para (w jednym wymiarze). Napisz program, który w tablicy a []
z N wartościami typu doubl e wyszukuje najbliższą parę, czyli dwie wartości różniące
się o nie więcej niż dowolna inna para. Czas wykonania programu powinien być dla
najgorszego przypadku liniowo-logarytmiczny.
1.4.17. Najdalsza para (w jednym wymiarze). Napisz program, który w tablicy a[]
z N wartościami typu doubl e wyszukuje najdalszą parę, czyli dwie wartości różniące
się o nie mniej niż dowolna inna para. Czas wykonania program u powinien być dla
najgorszego przypadku liniowy.
1.4.18. M inimum lokalne tablicy. Napisz program, który w tablicy a [] z N różnymi
liczbami całkowitymi znajduje minimum lokalne — indeks i , taki że a [i ] < a [i - 1 ]
i a [i ] < a [i +1]. Program dla najgorszego przypadku powinien przeprowadzać ~2lg
N porównań.
Odpowiedź: sprawdź środkową wartość, a [N/2], i dwie wartości sąsiednie — a [N/2
- 1] i a [N/2 + 1], Jeśli a [N/2] jest m inim um lokalnym, należy zakończyć wyszukiwa
nie. W przeciwnym razie należy szukać w połowie zawierającej mniejszego sąsiada.
1.4.19. M inimum lokalne macierzy. Dla tablicy a[] o wymiarach N na N, zawiera
jącej N 2 różnych liczb całkowitych, zaprojektuj algorytm, który działa w czasie pro
porcjonalnym do N i wyszukuje minimum lokalne — parę indeksów i oraz j, takich
że a [i] [j] < a [i +1 ] [j], a [i] [j] < a [i] [j+ 1 ], a [i] [j] < a [ i - 1 ] [j] i a [i] [j] <
a [i] [j-1 ] • Czas wykonania programu powinien być proporcjonalny do N (dla naj
gorszego przypadku).
1.4.20. Wyszukiwanie w ciągu bitonicznym. Tablica jest bitoniczna, jeśli składa się
z ciągu rosnących liczb całkowitych, po którym bezpośrednio następuje ciąg male
jących liczb całkowitych1. Napisz program, który dla bitonicznej tablicy N różnych
wartości typu i nt określa, czy dana liczba całkowita znajduje się w tablicy. Program
powinien dla najgorszego przypadku przeprowadzać ~3lg N porównań.
1 To pewna nieścisłość; ciąg bitoniczny składa się z elementów niemałejących, a następ
nie nierosnących lub na odwrót — z elementów nierosnących, a następnie niemałejących
— przyp. tłum.
1.4 * Analizy algorytmów
1 .4.21 • Wyszukiwanie binarne dla niepowtarzalnych wartości. Opracuj implementa
cję wyszukiwania binarnego dla typu S ta ti cSEToflnts (zobacz stronę 110), w której
gwarantowany czas wykonania metody contains() wynosi ~lg R, gdzie R to liczba
różnych liczb całkowitych w tablicy podanej jako argument konstruktora.
1 .4.22. Wyszukiwanie binarne z samym dodawaniem i odejmowaniem [autor: Mihai
Patrascu]. Napisz program, który pobiera tablicę N uporządkowanych rosnąco róż
nych wartości typu in t i określa, czy dana liczba całkowita znajduje się w tablicy.
Możesz stosować tylko dodawanie i odejmowanie oraz stałą ilość dodatkowej pam ię
ci. Czas wykonania program u dla najgorszego przypadku powinien być proporcjo
nalny do log N.
Odpowiedź: zamiast wyszukiwać na podstawie potęg dwójki (wyszukiwanie binar
ne), należy zastosować liczby Fibonacciego, które także rosną wykładniczo. Należy
przechowywać aktualnie przeszukiwany przedział jako [i, i + Fk] oraz zapisywać Fk
i Fk-1 w dwóch zmiennych. W każdym kroku trzeba obliczyć przez odejmowanie
Fk-2 i zmienić bieżący przedział na [z, i + Fk-2] lub [i + Fk-2, i + Fk-2 + Fk-1],
1.4.23. Wyszukiwanie binarne ułamków. Opracuj metodę, która za pomocą loga
rytmicznej liczby zapytań w postaci: Czy liczba jest mniejsza niż x? znajduje licz
bę wymierną p/q, taką że 0 < p < q < N. Wskazówka: dwa ułamki o mianownikach
mniejszych niż N nie mogą się różnić o więcej niż 1/N 2.
1.4.24. Zrzucanie jajek z budynku. Załóżmy, że istnieje N-piętrowy budynek i m nó
stwo jajek. Przyjmijmy, że jajko zostaje stłuczone, jeśli zrzucić je z piętra F lub wyż
szego, a w przeciwnym razie pozostaje nietknięte. Najpierw opracuj strategię określa
nia wartości F, tak aby liczba zniszczonych jajek wynosiła ~lg N przy ~lg N rzutach.
Następnie znajdź sposób na zmniejszenie kosztów do ~2 lg F.
1.4.25. Zrzucanie dwóch jajek z budynku. Rozważ poprzednie pytanie, ale tym ra
zem przyjmij, że są tylko dwa jajka, a model kosztów oparty jest na liczbie rzutów.
Opracuj strategię określania F, dla którego liczba rzutów wynosi co najwyżej 2 ~Jn .
Następnie znajdź sposób na zmniejszenie kosztu do ~c . Jest to odpowiednik sy
tuacji, w której przy wyszukiwaniu trafienia (nieuszkodzone jajka) są dużo mniej
kosztowne niż pominięcia (zniszczone jajka).
1.4.26. Współliniowość trójek. Załóżmy, że istnieje algorytm, który pobiera N róż
nych punktów w przestrzeni i zwraca liczbę trójek znajdujących się na jednej linii.
Wykaż, że m ożna wykorzystać ten algorytm do rozwiązania problemu sum trójek.
Duża podpowiedz: użyj algebry, aby wykazać, że (a, a3), (b, b3) i (c, c3) są współliniowe
wtedy i tylko wtedy, jeśli a + b + c = 0 .
224 R O ZD ZIA Ł 1 o Podstawy
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
1.4.27. Kolejka oparta na dwóch stosach. Zaimplementuj kolejkę za pom ocą dwóch
stosów, tak aby każda operacja na kolejce zajmowała stałą (po amortyzacji) liczbę
operacji na stosie. Wskazówka: jeśli umieścisz elementy na stosie, a następnie zdej
miesz je wszystkie, będą miały odwrotną kolejność. Powtórzenie tego procesu spo
woduje przywrócenie pierwotnej kolejności.
1.4.28. Stos oparty na kolejce. Zaimplementuj stos za pomocą jednej kolejki, tak aby
każda operacja na stosie wymagała liniowej liczby operacji na kolejce. Wskazówka:
aby usunąć element, pobierz wszystkie elementy kolejki jeden po drugim i umieść
je na końcu za wyjątkiem jednego, który należy zwrócić i usunąć (przyznajemy, że
rozwiązanie to jest bardzo mało wydajne).
1.4.29. Steque oparta na dwóch stosach. Zaimplementuj strukturę steque za pomocą
dwóch stosów, tak aby każda operacja na steque (zobacz ć w i c z e n i e 1 .3 .3 2 ) wyma
gała stałej (po amortyzacji) liczby operacji na stosie.
1.4.30. Deque oparta na stosie i steque. Zaimplementuj strukturę deque za pomocą
stosu i struktury steque (zobacz ć w i c z e n i e 1 .3 .3 2 ), tak aby każda operacja na deque
wymagała stałej (po amortyzacji) liczby operacji na stosie i steque.
1.4.31. Deque oparta na trzech stosach. Zaimplementuj strukturę deque za pomocą
trzech stosów, tak aby każda operacja na niej wymagała stałej (po amortyzacji) liczby
operacji na stosie.
1.4.32. Analizy z uwzględnieniem amortyzacji. Udowodnij, że jeśli zaczynamy od
pustego stosu, liczba dostępów do tablicy dla dowolnego ciągu M operacji (przy im
plementacji klasy Stack opartej na tablicy o zmiennej wielkości) jest proporcjonalna
do M.
1.4.33. Wymagania pamięciowe na maszynie 32-bitowej. Podaj wymagania pam ię
ciowe dla typów Integer, Date, Counter, i n t [], doublet], doublet] []. String, Node
i Stack (dla implementacji opartej na liście powiązanej) na maszynie 32-bitowej.
Przyjmij, że referencje zajmują po 4 bajty, narzut dla obiektu wynosi 8 bajtów, a do
pełnienie odbywa się do wielokrotności liczby 4.
1.4.34. Zimno - ciepło. Celem jest odgadnięcie tajnej liczby całkowitej z przedziału
od 1 do N. Gracz wielokrotnie zgaduje liczby całkowite z tego przedziału. Po każdej
próbie dowiaduje się, czy podana liczba jest równa szukanej. Jeśli tak, gra się kończy;
w przeciwnym razie gracz otrzymuje informację o tym, czy jest bliżej („ciepło”) czy
dalej od szukanej liczby („zimno”) niż w poprzedniej próbie. Zaprojektuj algorytm,
który znajduje tajną liczbę w co najwyżej ~2 lg N próbach. Następnie opracuj algo
rytm, który robi to w co najwyżej ~1 lg N próbach.
1.4 ■ Analizy algorytm ów 225
1 .4.35. Koszty czasowe dla stosów. Wyjaśnij wartości z poniższej tabeli, w której po
kazano typowe koszty czasowe dla różnych implementacji stosu. Wykorzystaj model
kosztów, w którym liczone są zarówno referencje do danych (referencje do danych
umieszczanych na stosie — albo referencje do tablicy, albo do zmiennej egzemplarza
obiektu), jak i tworzone obiekty.
Koszt umieszczenia N wartości typu int
Struktura danych Typ elementu --------------------------------------------------------------------
Referencje do danych Tworzone obiekty
i nt 2N N
Lista powiązana In teger 3N 2N
Tablica o zmiennej i nt -5 N lg N
wielkości In teger -5 N ~N
Koszty czasowe dla stosów (różne implementacje)
1.4.36. Wykorzystanie pamięci w stosach. Wyjaśnij wartości w poniższej tabeli.
Przedstawiono w niej typowe wykorzystanie pamięci dla różnych implementacji sto
sów. Zastosuj statyczną klasę zagnieżdżoną dla węzłów listy powiązanej, aby uniknąć
narzutu dla niestatycznej klasy zagnieżdżonej.
Pamięć potrzebna dla N
Struktura danych Typ elementu
wartości typu int (w bajtach)
Lista powiązana in t -32 N
In te g e r
-56 N
Tablica in t Od -4 N do -16 N
In teger
o zmiennej wielkości Od -32 N do -56 N
W ykorzystanie pamięci w stosach (różne implementacje)
226 R O ZD ZIA Ł 1 n Podstawy
j] EKSPERYMENTY
1.4.37. Spadek wydajności z uwagi na autoboxing. Przeprowadź eksperymenty, aby
ustalić spadek wydajności na Twoim komputerze w wyniku stosowania autoboxin-
gu i autounboxingu. Opracuj implementację typu FixedCapacityStackOfInts i użyj
klienta podobnego do programu Doubl i ngRati o do porównania jej wydajności z ge-
nerycznym typem FixedCapacityStack<Integer> dla dużej liczby operacji push()
i popi)•
1.4.38. Naiwna implementacja obliczania sum trójek. Przeprowadź eksperymenty,
aby ocenić poniższą implementację wewnętrznej pętli program u ThreeSum:
fo r ( in t i = 0; i < N; i++)
fo r ( i nt j = 0; j < N; j++)
fo r ( int k = 0; k < N; k++)
i f (i < j && j < k)
i f (a [i] + a [j] + a[k] == 0)
cnt++;
W tym celu opracuj wersję programu Doubl ingTest, która oblicza stosunek czasów
wykonania nowego program u i programu ThreeSum.
1.4.39. Zwiększanie precyzji testów podwajania. Zmodyfikuj program Doubl i ngRati o
tak, aby pobierał z wiersza poleceń drugi argument, określający liczbę wywołań m e
tody timeTri al () dla każdej wartości N. Uruchom program dla 10, 100 i 1000 prób.
Omów dokładność wyników.
1.4.40. Sumy trójek dla losowych wartości. Sformułuj i sprawdź hipotezę opisującą
liczbę sumujących się do 0 trójek wśród N losowych wartości typu i nt. Jeśli znasz się
na analizach matematycznych, opracuj odpowiedni model matematyczny dla tego
problemu, w którym wartości mają rozkład równomierny między - M a M, a M nie
jest małe.
1.4.41. Czasy wykonania. Oszacuj ilość czasu potrzebnego na wykonanie na Twoim
komputerze programów TwoSumFast, TwoSum, ThreeSumFast i ThreeSum w celu rozwią
zania problem u dla pliku zawierającego milion liczb. Wykorzystaj do tego program
Doubl i ngRati o.
1.4 h Analizy algorytmów 227
1.4.42. Rozmiary problemu. Oszacuj największą wartość P, dla której na Twoim kom
puterze m ożna uruchomić programy TwoSumFast, TwoSum, ThreeSumFast i ThreeSum,
aby rozwiązać problemy dla pliku zawierającego 2P tysięcy liczb. Wykorzystaj pro
gram Doubl i ngRatio.
1 .4.43. Tablice o zmiennej wielkości a listy powiązane. Przeprowadź eksperymenty,
aby sprawdzić hipotezę, zgodnie z którą tablice o zmiennej wielkości są wydajniejsze
dla stosów niż listy powiązane (zobacz ć w i c z e n i a 1 .4.35 i 1 .4 .36 ). W t y m celu opra
cuj wersję program u Doubl i ngRati o, która oblicza stosunek czasów wykonania obu
programów.
1.4.44. Problem urodzin. Napisz program, który pobiera z wiersza poleceń liczbę
całkowitą N i używa metody StdRandom. uni form() do wygenerowania losowego cią
gu liczb całkowitych z przedziału od 0 do N - 1. Przeprowadź eksperymenty, aby
sprawdzić hipotezę, zgodnie z którą liczba wartości całkowitych wygenerowanych
przed powtórzeniem się jednej z nich wynosi ~.
1.4.45. Problem kolekcjonera kuponów. Na podstawie liczb całkowitych wygenero
wanych tak jak w poprzednim przykładzie przeprowadź eksperymenty, aby spraw
dzić hipotezę, zgodnie z którą liczba liczb całkowitych wygenerowanych przed uzy
skaniem wszystkich możliwych wartości wynosi ~NHN.
u
a b y p r z e d s t a w i ć podstawowe podejście do rozwijania i analizowania algorytmów,
omawiamy tu szczegółowo pewien przykład. Celem jest podkreślenie następujących
kwestii:
■ Od dobrych algorytmów może zależeć, czy dany praktyczny problem da się
rozwiązać czy nie.
■ Wydajny algorytm może być tak prosty do napisania jak algorytm niewydajny.
■ Zrozumienie cech implementacji związanych z wydajnością jest ciekawym i da
jącym satysfakcję zadaniem intelektualnym.
■ M etoda naukowa to ważne narzędzie pomagające wybrać jedną z różnych me
tod rozwiązania tego samego problemu.
■ Proces iteracyjnego ulepszania może prowadzić do powstawania coraz wydaj
niejszych algorytmów.
Do zagadnień tych wracamy w książce. Opisany tu prototypowy przykład stanowi
podstawę do stosowania tej samej ogólnej metodologii do wielu innych problemów.
Problem omawiany w tym miejscu nie jest sztuczny. Dotyczy podstawowego zada
nia obliczeniowego, a opracowane rozwiązanie jest używane w wielu zastosowaniach
— od badania przesiąkania w chemii fizycznej po łączność w sieciach komunikacyj
nych. Zaczynamy od prostego rozwiązania, a następnie staramy się zrozumieć jego
cechy związane z wydajnością, co pomaga ustalić, jak usprawnić algorytm.
Dynamiczne określanie połączeń Zaczynamy od specyfikacji problemu
— dane wejściowe to ciąg par liczb całkowitych, w których każda liczba reprezentuje
obiekt pewnego typu. Para p q oznacza „p jest połączone z q”. Zakładamy, że „jest
połączone z” to relacja równoważności, co oznacza, że jest ona:
■ zwrotna: p jest połączone z p;
■ symetryczna: jeśli p jest połączone z q, to q jest połączone z p;
■ przechodnia: jeśli p jest połączone z q, a q jest połączone z r, to p jest połączone z r.
Relacja równoważności dzieli obiekty na klasy równoważności. Tu dwa obiekty należą
do tej samej klasy równoważności wtedy i tylko wtedy, jeśli są połączone. Celem jest
napisanie programu, który odfiltrowuje z ciągu nadmiarowe pary (w których oba
obiekty należą do tej samej klasy równoważności). Ujmijmy to inaczej — kiedy pro
gram wczyta z wejścia parę p q, powinien dodać ją do danych wyjściowych wtedy
i tylko wtedy, jeśli z par napotkanych do tej pory nie wynika, że p jest połączone z q.
Jeżeli z wcześniejszych par wynika, że p jest połączone z q, program powinien pom i
nąć tę parę i wczytać następną. Na rysunku na następnej stronie pokazano przykład
działania procesu. Aby osiągnąć zamierzony cel, trzeba zaprojektować strukturę da
nych, która zapamiętuje wystarczającą ilość informacji o napotkanych parach, aby
móc zdecydować, czy obiekty z nowej pary są połączone. Zadanie zaprojektowania
228
1.5 n Studium przypadku — problem Union-Find 229
takiej metody nieformalnie nazwaliśmy problemem dynamicznego określania połą
czeń. Oto przykładowe zastosowania rozwiązania tego problemu.
Sieci Liczby całkowite mogą reprezentować kom putery w dużej sieci, a pary — po
łączenia w tej sieci. Program określa, czy trzeba nawiązać nowe bezpośrednie p o
łączenie dla p i q, aby maszyny mogły się komunikować, czym ożna wykorzystać
istniejące połączenia do utworzenia ścieżki komunikacyjnej. Liczby całkowite mogą
też reprezentować elementy w obwodzie elektrycznym, a pary — przewody łączące
te elementy. Ponadto liczby całkowite mogą reprezentować osoby w sieci społecznoś-
ciowej, a pary — znajomych. W takich zastosowaniach czasem trzeba przetwarzać
miliony obiektów i miliardy połączeń.
Rów noznaczność nazw zm iennych W niektórych 0. 1. 2. 3. 4.
środowiskach programistycznych można zadeklarować 5. 6. 7m 8# 9#
dwie zmienne jako równoznaczne (są wtedy referen • 0— 0
cjami do tego samego obiektu). Po serii takich dekla 4 3
racji system musi mieć możliwość określenia, czy dane
dwie nazwy są równoznaczne. Jest to jedno z wczes 3 8 • ro •
•
nych zastosowań (związane z językiem programowania
FORTRAN), które doprowadziło do opracowania oma
6 5
:
wianych dalej algorytmów.
Zbiory m atem atyczne Na bardziej abstrakcyjnym po
9 4
u :n
ziomie m ożna traktować liczby całkowite jak wartości
zbiorów matematycznych. Przy przetwarzaniu pary p q
2 1
n
5 9
należy sprawdzić, czy elementy należą do tego samego
zbioru. Jeśli nie, należy połączyć zbiory zawierające p
5 0
i q, umieszczając je w jednym zbiorze.
Nie należy
7 2 wyświetlać par,
a b y u j e d n o l i c i ć o p i s , w dalszej części podrozdziału które już
sq połączone
stosujemy terminologię z obszaru sieci. Obiekty na 6 1
zywamy punktami, pary połączeniami, a klasy rów
noważności — połączonymi składowymi (lub, krótko, 1 0
składowymi). Dla uproszczenia zakładamy, że istnieje
N punktów o nazwach w postaci liczb całkowitych od
0 do N-l. Nie powoduje to utraty ogólności, ponieważ Dwie sk ła d o w e
w r o z d z i a l e 3 . rozważamy wiele algorytmów, które
pozwalają w wydajny sposób powiązać dowolne nazwy Przykład dynamicznego określania połączeń
z całkowitoliczbowymi identyfikatorami.
Na początku następnej strony pokazano większy przykład, który ukazuje trudność
problemu określania połączeń. Można szybko zidentyfikować składową obejmującą
jeden punkt w środkowej części po lewej stronie diagramu i składową obejmującą
pięć punktów w lewym dolnym rogu. Jednak zweryfikowanie, czy wszystkie pozosta-
230 RO ZD ZIA Ł 1 ■ Podstawy
LI
Połączona ,
składowa
Ś re d n ie j w ielk ości p rz y k ła d d la o k re ś la n ia p o łą c z e ń (625 p u n k tó w , 9 0 0 k ra w ę d zi, 3 p o łą c z o n e s k ład o w e )
łe punkty są ze sobą połączone, może okazać się trudne. Dla program u jest to jeszcze
trudniejsze, ponieważ używa tylko nazw punktów i połączeń, natomiast nie ma do
stępu do geometrycznego układu punktów na diagramie. Jak można szybko określić,
czy dane dwa punkty w takiej sieci są połączone?
Pierwsze zadanie, z którym trzeba się zmierzyć przy rozwijaniu algorytmu, polega
na precyzyjnym ujęciu problemu. Można oczekiwać, że im większe są wymagania
wobec algorytmu, tym więcej czasu i pamięci będzie on potrzebował do wykonania
pracy. Nie da się z góry ująć tej zależności w formie liczbowej. Ponadto często spe
cyfikacja problemu zmienia się po stwierdzeniu, że jego rozwiązanie jest trudne lub
kosztowne (lub — w szczęśliwych okolicznościach — po ustaleniu, że algorytm udo
stępnia informacje bardziej przydatne od wymaganych w pierwotnej specyfikacji).
Specyfikacja problemu określania połączeń wymaga tylko tego, aby program ustalał,
1.5 ■ Studium przyp ad ku — problem Union-Find 231
czy dana para p q jest połączona czy nie. Program nie musi podawać zbioru połączeń
dla danej pary. Ten ostatni wymóg zwiększa poziom trudności problemu i prowadzi
do innej rodziny algorytmów, opisanej w p o d r o z d z i a l e 4 . 1 .
Aby ustalić specyfikację problemu, opracowano interfejs API z podstawowymi
potrzebnymi operacjami: inicjowaniem, dodawaniem połączenia między dwoma
punktami, identyfikowaniem składowej obejmującej dany punkt, określaniem, czy
dwa punkty należą do tej samej składowej, i zliczaniem składowych. Interfejs API
wygląda więc tak:
p ub lic c la s s UF
UF ( i nt N) Inicjowanie N p u n któ w nazwami w postaci liczb
całkowitych (od 0 do N-lJ
void u n io n (in t p, in t q) Dodawanie połączenia między p a ą
in t find (i nt p) Identyfikator składowej dla p (od 0 do N -l)
, Zwraca true, jeśli p i q znajdują się w tej samej
boolean connected(int p, in t q) 1 r j x i 1
składowej
in t count () Liczba składowych
Interfejs API na potrzeby problem u Union-Find
Operacja union() scala dwie składowe, jeśli dwa punkty znajdują się w różnych
składowych. Operacja find () zwraca całkowitoliczbowy identyfikator składowej dla
danego punktu. Operacja connected () określa, czy dwa punkty należą do tej samej
składowej. M etoda count () zwraca liczbę składowych. Zaczynamy od Nskładowych,
a każda operacja uni on () scalająca dwie różne składowe powoduje zmniejszenie ich
liczby o 1 .
Jak się wkrótce okaże, opracowanie rozwiązania algorytmicznego do dynamiczne
go określania połączeń sprowadza się do utworzenia implementacji przedstawionego
interfejsu API. W każdej implementacji trzeba:
° zdefiniować strukturę danych reprezentującą znane połączenia;
■ utworzyć wydajne implementacje operacji union(), find() , connected!) i co
unt!) oparte na tej strukturze danych.
Jak zwykle natura struktury danych m a bezpośredni wpływ na wydajność algoryt
mów, dlatego projektowanie struktury i algorytmu jest powiązane. Interfejs API
określa konwencję, zgodnie z którą zarówno punkty, jak i składowe są identyfikowa
ne za pomocą wartości typu i nt z przedziału od 0 do N-l, dlatego uzasadnione jest
stosowanie indeksowanej punktam i tablicy i d [] jako podstawowej struktury danych
reprezentującej składowe. Identyfikatorem składowej jest zawsze nazwa jednego
z należących do niej z punktów. Dlatego można uznać, że każda składowa jest repre
zentowana przez jeden z jej punktów. Początkowo jest N składowych (każdy punkt
stanowi składową), dlatego należy zainicjować id [i] wartością i dla wszystkich i od
232 RO ZD ZIA Ł 1 ta Podstawy
0 do N-l. Dla każdego punktu i w id [i] przechowywane są informacje potrzebne
w metodzie find() do ustalenia składowej zawierającej i. Do ustalania służą różne
strategie zależne od algorytmu. We wszystkich implementacjach użyto jednowier-
szowej implementacji m etody connected(), find(p) == find(q), zwracającej wartość
typu boolean.
punktem wyjścia jest a l g o r y t m 1.5 z na
p o d s u m u jm y —
% morę tin y U F .tx t
10 stępnej strony. Przechowywane są dwie zmienne egzemplarza:
4 3 liczba składowych i tablica i d []. Implementacje m etod find()
3 8 i uni on () są tematem pozostałej części podrozdziału.
6 5
9 4
Aby przetestować przydatność interfejsu API i przygoto
2 1 wać podstawy do pisania kodu, w metodzie main() umieści
8 9 liśmy klienta, który za pom ocą interfejsu rozwiązuje problem
5 0
dynamicznego określania połączeń. Klient wczytuje wartość N
7 2
6 1 i ciąg par liczb całkowitych (każda z przedziału od 0 do N-l),
1 0 wywołując metodę find () dla każdej pary. Jeśli dwa punkty
6 7
z pary są już połączone, program przechodzi do kolejnej pary.
% more [Link] Jeżeli punkty nie są połączone, program wywołuje metodę
625 union() i wyświetla parę. Przed przejściem do im plementa
528 503 cji warto wspomnieć, że przygotowaliśmy także dane testo
548 523
we. Plik [Link] zawiera 11 połączeń między 10 punktami,
[lic z b a połączeń: 900] użyte w krótkim przykładzie przedstawionym na stronie 229;
plik [Link] obejmuje 900 połączeń między 625 punk
% more la rg e U F .txt
tami, co pokazano na stronie 230; plik [Link] to przykład
1000000
786321 134521 z dwoma milionami połączeń dla miliona punktów. Celem jest
696834 98245 umożliwienie obsługi danych wejściowych w rodzaju pliku
[Link] w rozsądnym czasie.
[lic z b a połączeń: 2000000] 5 ,
W ramach analizowania algorytmów koncentrujemy się na
liczbie dostępów do elementów tablicy. Pośrednio formułujemy
w ten sposób hipotezę,
zgodnie z którą czasy wykonania algorytmów Model kosztów dla problemu
na konkretnej maszynie są stałe dla danej licz Union-Find. Przy badaniu al
by dostępów. Hipoteza ta wynika bezpośred gorytmów będących implemen
nio z kodu, nietrudno sprawdzić jej popraw tacją interfejsu API dla proble
ność poprzez eksperymenty, a ponadto — jak m u Union-Find liczone są dostę
się okaże — stanowi użyteczny punkt wyjścia py do tablicy (liczba dostępów do
do porównywania algorytmów. elementów tablicy w celu odczy
tu lub zapisu).
1.5 Studium przypadku — problem Union-Find 233
ALGORYTM 1.5. Implementacja problemu Union-Find
p u b l i c c l a s s UF
{
private int[] id; // D o s t ę p do i d e n t y f i k a t o r ó w s k ł a d o w y c h
// (w t a b l i c y i n d e k s o w a n e j p u n k t a m i ) ,
private i n t count; // Liczba składowych.
p u b l i c U F ( i n t N)
{ / / I n i c j o w a n ie t a b l i c y identyfikatorów składowych,
c o u n t = N;
i d = new i n t [ N ] ;
f o r ( i n t i = 0 ; i < N; i + + ) % j ava uf < t in y U F . tx t
id [i] = i ; 4 3
} 3 8
6 5
p ublic i n t count() 9 4
( re tu rn count; } 2 1
5 0
p u b l i c boolean c o n n e c t e d ( i n t p, int q) 72
{ r e t u r n f i n d ( p ) == f i n d ( q ) ; } 1
lic z b a składowych: 2
p u b l i c i n t find ( i n t p)
p u b l i c v o i d u n i o n ( i n t p , i n t q)
/ / Zobacz s t r o n ę 234 ( s z y b k a met o da f i nd) , s t r o n ę 236 ( s z y b k a met oda u n i o n )
/ / i s t r o n ę 240 ( w e r s j a z w a g a m i ) .
p u b lic s t a t i c void m ain (S trin g [] args)
( / / Rozwiązywanie problemu dynamicznego o k r e ś l a n i a
/ / p o ł ą c z e ń d l a danych ze S t d l n .
i n t N = S t d l n . r e a d l n t ( ) ; / / Wczytywanie l i c z b y punktów.
UF u f = new UF ( N) ; / / I n i c j o w a n ie N składowych,
while ([Link] m ptyO )
{
int p = [Link] ;
int q = S td ln .re ad ln tO ; / / W c z y t y w a n i e p u n k t ó w do
/ / połączenia.
i f ( u f .c o n n e c te d (p , q)) c o n tin u e ; / / Ignorowanie, j e ś l i i s t n i e j e
/ / połączenie.
[Link](p, q ) ; / / Ł ąc ze n ie składowych
StdO [Link](p + " " + q ) ; / / i wyświetlanie połączenia.
}
StdO [Link]("liczba składowych: " + [Link]());
Omawiana implementacja klasy UF oparta jest na powyższym kodzie. Przechowywana jest tu
tablica liczb całkowitych i d [], na podstawie której metoda find () zwraca tę samą liczbę cał
kowitą dla każdego punktu należącego do danej składowej. Metoda uni on () musi zapewniać
zachowanie tego niezmiennika.
234 RO ZD ZIA Ł 1 □ Podstawy
Implementacje Opisano tu trzy różne implementacje. We wszystkich do spraw
dzania, czy dwa punkty znajdują się w tej samej składowej, służy indeksowana miej
scami tablica i d [].
Szybka m etoda fin d Jednym z rozwiązań jest utrzymywanie niezmiennika, zgodnie
z którym p i q są połączone wtedy i tylko wtedy, jeśli i d [p] jest równe i d [q]. Ujmijmy
to inaczej — wszystkie punkty składowej muszą mieć tę samą wartość w tablicy i d [].
Jest to technika z szybkę metodę fin d (ang. ąuick-find), ponieważ metoda find(p) jedy
nie zwraca id[p], z czego bezpośrednio wynika, że metodę connected(p, q) można
zredukować do testu id[p] == id[q]
(metoda ta zwraca true wtedy i tylko Metoda find sprawdza i d [5] /' i d [9]
wtedy, jeśli p i q należą do tego same p q 0 1 2 3 4 5 6 7 8 9
go komponentu). Aby zachować nie 59 1 1 1 8 8 1 1 1 8 8
zmiennik w wywołaniu union(p, q),
Metoda union musi zmienić wszystkie jedynki na ósemki
najpierw należy sprawdzić, czy punkty
p q 0 1 2 3 4 5 6 7 8 9
należą do tej samej składowej. Jeśli tak
jest, nie trzeba nic robić. W przeciw 59 1 1 1 8 8 1 1 1 8 8
nym razie jest tak, że wszystkie elemen 8 8 8 8 8 8 8 8 8 8
ty tablicy i d [] odpowiadające punktom Przegląd techniki z szybką metodą find
ze składowej, do której należy p, mają
jedną wartość, a wszystkie elementy powiązane z punktami ze składowej obejmującej
q posiadają inną wartość. Aby połączyć obie składowe w jedną, trzeba ustawić wszyst
kie elementy tablicy i d [] odpowiadające obu zbiorom punktów na tę samą wartość,
co pokazano w przykładzie po prawej. W tym celu trzeba przejść po tablicy i zmienić
wszystkie elementy o wartościach równych i d [p] na i d [q]. Można też zmodyfikować
wszystkie elementy równe i d [q] na wartość i d [p] — nie stanowi to różnicy. Oparty na
tych opisach kod metod find () i uni on () jest prosty. Przedstawiono go po lewej stronie.
Na następnej stronie pokazano pełny ślad działania wspomagającego tworzenie aplika
cji klienta dla przykładowych danych testowych z pliku [Link].
p u b lic in t find (in t p)
{ return i d [ p ] ; }
p u b lic void u n io n (in t p, in t q)
{ // Umieszczanie p i q w jednej składowej,
in t pID = find(p);
in t qID = find(q);
// Nie trzeba n ic ro b ić , j e ś l i p i q znajdują s ię ju ż
// w jednej składowej,
i f (pID == qID) re turn;
// Zmiana nazwy składowej d la p na nazwę składowej, do której nale ży q.
fo r ( in t i = 0; i < id .le n g th ; i++)
i f ( i d [ i ] == pID) i d [ i ] = q ID ;
cou n t--;
Technika z szybką metodą find
1.5 ■ Studium przypadku — problem Union-Find 235
A n a lizy techniki z szybką m etodą fin d Operacja find () z pewnością jest szybka,
ponieważ zakończenie jej działania wymaga tylko jednego dostępu do tablicy i d [].
Jednak rozwiązanie to zwykle nie nadaje się dla dużych problemów, ponieważ m eto
da uni on () musi przejść przez całą tablicę i d [] dla każdej pary wejściowej.
Założenie F. W algorytmie z szybką metodą find () potrzebny jest jeden dostęp
do tablicy na każde wywołanie metody find () i od N + 3 do 2N + 1 dostępów do
tablicy na każde wywołanie metody uni on () łączącej obie składowe.
Dowód. Wynika bezpośrednio z kodu. Każde wywołanie m etody connected()
wymaga przetestowania dwóch elementów tablicy i d [] — po jednym na każde
z dwóch wywołań metody find (). Każde wywołanie m etody uni on () łączące dwie
składowe obejmuje dwa wywołania metody find (), sprawdzenie każdego z N ele
mentów tablicy i d [] i zmianę od 1 do N - 1 z nich.
i d []
Załóżmy, że technikę z szybką metodą find () zastosowa
p q 0 1 2 3 4 5 6 7 8 9
no do problemu dynamicznego określania połączeń. Jeśli
4 3 0 1 3 4 5 6 7 8 9
istnieje tylko jedna składowa, potrzebnych jest N - 1 wy
0 1 2 3 3 5 6 7 8 9
■> wołań metody uni on () i, co z tego wynika, (N+3)(N-1) ~
3 8 0 1 2 3 5 6 7 8 9
N 2 dostępów do tablicy. Od razu prowadzi to do hipotezy,
0 1 2 8 8 5 6 7 8 9
że dynamiczne określanie połączeń za pomocą techniki
6 5 0 1 2 8 8 5 6 7 8 9
zszybkąmetodąfind () może być procesem, w którym czas
0 1 2 8 8 5 5 7 8 9
T rośnie kwadratowo. Analizy te można uogólnić i stwier
9 4 0 1 8 8 5 5 7 8 9
0 1 2 8 8 5 5 7 8 8
dzić, że technika z szybką metodą find () jest kwadratowa
2 1 0 1 2 8 S 5 5 7 8 8
dla typowych zastosowań, w których ostatecznie liczba
0 1 1 8 8 5 5 7 8 8 składowych jest niewielka. Za pomocą testu podwajania
8 9 0 1 1 8 8 5 5 7 8 8 można łatwo sprawdzić tę hipotezę na własnym kompu
5 0 0 1 1 8 8 5 5 7 8 8 terze (instruktażowy przykład przedstawiono w ć w i c z e
0 1 1 8 8 0 0 7 8 8 n i u 1 .5 .23 ). Współczesne komputery wykonują miliony
7 2 0 1 1 8 8 0 0 7 8 8 lub miliardy instrukcji na sekundę, dlatego koszt jest
0 1 1 8 8 0 0 1 8 8 niezauważalny przy małych N, jednak we współczesnej
6 1 0 1^ 1 8 8 0 0 1 8 8 aplikacji czasem trzeba przetworzyć miliony lub miliar
1 1 8 8 1 1 8 8 dy miejsc, co przedstawiono za pomocą pliku testowego
1 0 1 1 1 o\ 8 11' 1 1 S 8 [Link]. Jeśli nadal nie jesteś przekonany i uważasz, że
6 7 L 1 8 8n y 1 1 8 8 posiadasz wyjątkowo wydajny komputer,
i d [p] / i d[q] mają różną wartość, spróbuj użyć techniki z szybką metodą
dlatego metoda u n io n O zmienia find() do określenia liczby składowych
wartość elementów równych
id [ p ] n o id [q ] (wyróżnione) dla par z pliku [Link]. Nieunikniony
i d [p] i i d [q] są takie same, wniosek jest taki, że nie można rozwiązać
dlatego zmiany nie są potrzebne takiego problemu za pomocą algorytmu
Ślad działania techniki z szybką metodą find z szybką metodą find(), trzeba więc po
szukać lepszych algorytmów.
236 R O ZD ZIA Ł 1 o Podstawy
Technika z szybkę m etodę union Następny rozważany algorytm to uzupełniająca
technika,w której skoncentrowanosięnaprzyspieszeniuoperacjiuni on () .Rozwiązanie
to oparto na tej samej strukturze danych — tablicy i d [] indeksowanej punktami.
Tu jednak interpretujemy wartości w inny sposób, definiując bardziej skomplikowa
ne struktury. Element tablicy i d [] dla każdego punktu to nazwa innego punktu w tej
samej składowej (a czasem tego samego punktu). To połączenie nazywamy odnośni
kiem. W implementacji metody find () zaczynamy od danego punktu, przechodzimy
za pomocą odnośnika do na
stępnego i tak dalej, aż do m o Technika z szybką metodą union
m entu dotarcia do korzenia — p riv a te in t find (i nt p)
punktu, który posiada odnoś { // Wyszukiwanie nazwy sktadowej.
w hile (p != i d [ p ] ) p = i d [ p ] ;
nik do samego siebie (jak się return p;
okaże, program zawsze docho 1
dzi do korzenia). Dwa punkty
p u b lic void u n io n (in t p, in t q)
znajdują się w jednej składowej
{ // Przypisyw anie tego samego korzenia do p i q.
wtedy i tylko wtedy, jeśli pro in t pRoot = find(p);
ces prowadzi do tego samego in t qRoot = find(q);
i f (pRoot == qRoot) re turn;
korzenia. Aby proces był po
prawny, metoda union(p, q) id [pRoot] = qRoot;
musi zachowywać opisany nie
zmiennik. Można łatwo osiąg count— ;
1
nąć ten efekt. Należy podążać
za odnośnikami, aby znaleźć
korzenie powiązane z p i q, a następnie zmienić nazwę jednej ze składowych, łącząc
jeden z korzeni z innym. Stąd nazwa — technika z szybkę metodę union(). Także tu
można dowolnie wybrać, czy zmienić nazwę składowej zawierającej p czy obejmują
cej q. Przedstawiona imple
id [ ] to reprezentacja lasu drzew mentacja zmienia nazwę
Metoda f i nd O musi przechodzić
z odnośnikami do rodzica składowej obejmującej p.
do korzenia za pomocą odnośników
Rysunek na następnej stro
p q 0 1 2 3 4 5 6 7 8 9
nie przedstawia ślad dzia
59 1 1 1 8 3 0 5 1 8 8
łania algorytmu z szybką
t t
f i n d (5 ) to f i n d (9 ) to m etodą union() na pliku
i d [i d [i d [5 ]]] id [ id [ 9 ] ] [Link]. Ślad działania
najłatwiej zrozumieć na
Metoda u n io n () zmienia tylko
podstawie graficznej re
jeden odnośnik
prezentacji przedstawionej
p q 0 1 2 3 4 5 6 7 8 9
po lewej stronie, co opisa
59 1 1 1 8 3 0 5 1 8 8 no dalej.
1 8 1 8 3 0 5 1 8 8
Technika z szybką metodą union()
1.5 ■ Studium p rzyp adku— problem Union-Find 237
Reprezentacja lasu drzew Kod szybkiej m etody union () jest krótki, ale dość skom
plikowany. Przedstawienie punktów jako węzłów (kółka z cyframi), a odnośników
jako strzałek między węzłami pozwala utworzyć graficzną reprezentację struktu
ry danych, która pozwala na stosunkowo łatwe zrozumienie działania algorytmu.
Wynikowe struktury to drzewa. W ujęciu technicznym tablica i d[] to reprezentacja
lasu (zbioru) drzew oparta
id[]
na odnośnikach do rodzica. ® © @ © © © © ® ® ®
p q 0 1 2 3 4 5 6 7 8 9
Aby uprościć diagramy, czę
4 3 0 1 2 3 4 5 6 7 8 9 ® ® © @ © © ® ® ®
sto pomijamy zarówno gro 0 1 2 3 3 5 6 7 8 9 ©
ty strzałek w odnośnikach 3 8 0 1 2 3 3 5 6 7 8 9 ® © © © © ® ® ®
(ponieważ wszystkie strzał 0 1 2 8 3 5 6 7 8 9 @
ki są skierowane w górę), 2)
jak i odnośniki z korzenia 65 0 1 2 8 3 5 6 7 8 9
® © © O O ® S
do niego samego. Lasy od 0 1 2 8 3 5 5 7 8 9 © O)
powiadające tablicy i d [] ©
dla pliku [Link] p o 9 4 0 1 2 8 3 5 5 7 8 9
® ® © ® ® (S
kazano po prawej stronie. 0 1 2 8 3 5 5 7 8 8 ® © OD
Program zaczyna od węzła
odpowiadającego dowol 2 1 0 1 2 8 3 5 5 7 8 8
®
nemu punktowi i podąża 0 1 1 8 3 5 5 7 8 8 (?) (?)
za odnośnikami, ostatecz
nie dochodząc do korzenia 8 9 0 1 1 8 3 5 5 7 8 8
drzewa zawierającego dany 5 0 0 1 1 8 3 5 5 7 8 8
węzeł. To ostatnie stwier 0 1 1 8 3 0 5 7 8 8
dzenie m ożna udowodnić
przez indukcję. Prawdą jest, 72 0 1 1 8 3 0 5 7 8 8
że po zainicjowaniu tablicy 0 1 1 8 3 0 5 1 8 8
każdy węzeł posiada odnoś
nik do samego siebie. Jeśli 6 1 0 1 1 8 3 0 5 1 8 S
1 1 1 8 3 0 5 1 8 S
jest to prawdą przed opera
cją uni on (), jest tak też po
10 1 1 1 8 3 0 5 1 8 8
niej. Dlatego m etoda find()
67 1 1 1 8 3 0 5 1 8 8
ze strony 236 zwraca nazwę
punktu, który jest korze
Ślad działania techniki z szybką metodą u n io n O (z powiązanymi lasami drzew)
niem (co pozwala metodzie
connected() sprawdzić, czy
dwa punkty znajdują się w tym samym drzewie). Opisana reprezentacja jest przy
datna w tym problemie, ponieważ węzły odpowiadające dwóm punktom należą do
jednego drzewa wtedy i tylko wtedy, jeśli znajdują się w tej samej składowej. Ponadto
budowanie drzew nie jest trudne. Implementacja metody uni on () przedstawiona na
stronie 236 łączy dwa drzewa w jedno za pomocą jednej instrukcji, ustawiając korzeń
jednego drzewa jako rodzica drugiego.
238 RO ZD ZIA Ł 1 b Podstawy
A naliza techniki z szybką m etodą union() Algorytm z szybką metodą union () wy
daje się szybszy od algorytmu z szybką metodą find ( ) , ponieważ nie musi przechodzić
przez całą tablicę dla każdej pary wejścio
i d[]
® © (D © wej. Jednak o ile jest szybszy? Analizowanie
lo
p q 1 2 3 4 ...
kosztów techniki z szybką metodą u n io n ()
1O
i 0 1 3 4 ... ( p © © (?)
jest dużo trudniejsze niż dla szybkiej metody
1 1 2 3 4 ...
find (), ponieważ koszty w większym stop
0 2 0 1 2 D3 4 . . . © ©
niu zależą od natury danych wejściowych.
1 2 3 4 ...
W najlepszym przypadku metoda find () po
trzebuje jednego dostępu do tablicy w celu
0 3 (4)
znalezienia identyfikatora punktu (tak jak
w szybkiej metodzie find ()). W najgorszym
przypadku potrzeba 2N + 1 dostępów do tab
licy, tak jak dla 0 w przykładzie po lewej stro
0 4 0
nie (są to konserwatywne obliczenia, ponie
waż skompilowany kod zwykle nie wymaga
dostępu do tablicy przy drugim użyciu i d [p]
w pętli while). Nietrudno więc utworzyć
Głębokość = 4 - dane wejściowe dla najlepszego przypadku,
Najgorszy przypadek dla techniki z szybką metodą u ni on O dla których czas wykonania w kliencie do
dynamicznego określania połączeń jest linio
wy. Z drugiej strony, nietrudno też przygotować dane dla najgorszego przypadku, a wte
dy czas wykonania jest kwadratowy (zobacz rysunek po lewej stronie i t w i e r d z e n i e g
dalej). Na szczęście, nie trzeba mierzyć się z problemem analizowania szybkiej metody
uni on ( ) oraz porównywania wydajności technik z szybkimi metodami find ( ) i uni on (),
ponieważ dalej omówiono inną wersję, dużo wydajniejszą od obu opisanych do tej pory.
Na razie można traktować technikę z szybką metodą u n io n () jako usprawnienie tech
niki z szybką metodą find (), ponieważ zlikwidowano tu największą wadę tej ostatniej
(liniowy czas działania metody uni on ()). Różnica ta z pewnością zapewnia poprawę
dla typowych danych, jednak technika z szybką metodą uni on () nadal ma wadę — nie
można zagwarantować, że w każdym przypadku będzie znacząco szybsza od techniki
z szybką metodą find () (dla niektórych danych ta pierwsza jest szybsza).
Definicja. Wielkość drzewa to liczba jego węzłów. Głębokość węzła w drzewie to
liczba odnośników na ścieżce od węzła do korzenia. Wysokość drzewa to maksy
malna głębokość dla jego węzłów.
Twierdzenie G. Liczba dostępów do tablicy w metodzie find () w technice z szybką
metodą un i on () to 1 plus dwukrotność głębokości węzła dla danego punktu. Liczba
dostępów do tablicy w metodach uni on () i connected() to koszt dwóch operacji
find () (plus 1 dla metody uni on (), jeśli punkty znajdują się w różnych drzewach).
Dowód. Wynika bezpośrednio z kodu.
1.5 ■ Studium przypadku — problem Union-Find 239
Ponownie załóżmy, że stosujemy technikę z szybką m etodą union() do problemu
dynamicznego określania połączeń i powstaje jedna składowa. Bezpośrednim wnio
skiem z t w i e r d z e n i a G jest to, że czas wykonania dla najgorszego przypadku jest
kwadratowy. Przyjmijmy, że pary wejściowe pojawiają się w kolejności 0-1, 0-2, 0-3
itd. Po N - 1 takich parach uzyskujemy N punktów w jednym zbiorze. Drzewo utwo
rzone przez algorytm z szybką metodą union() ma wysokość N - 1.0 prowadzi do
1 połączonej z 2, która jest połączona z 3 i tak dalej (zobacz rysunek na poprzedniej
stronie). Według t w i e r d z e n i a g liczba dostępów do tablicy dla operacji union()
dla pary 0 i wynosi dokładnie 2 i + 2 (punkt 0 jest na głębokości i, a punkt i — na
głębokości 0). Dlatego łączna liczba dostępów do tablicy dla operacji find () dla N par
to 2 (1 + 2 + ... + N) ~ N 2.
Szybka m etoda union() z w agami Na Szybka m e to d a u n io n O
szczęście, istnieje łatwa modyfikacja
szybkiej m etody uni on (), pozwala
jąca zagwarantować, że niekorzystne
Mniejsze , / większe \
przypadki podobne do opisanego się v drzewo ) ( drzewo 1
nie zdarzą. Zamiast arbitralnie łączyć Może umieścić
większe drzewo niżej
w metodzie uni on () drugie drzewo
z pierwszym, należy śledzić wielkość Z w agam i
Zawsze wybiera
każdego drzewa i zawsze łączyć m niej lepsze rozwiązanie
sze z większym. Wersja ta wymaga nieco
więcej kodu i nowej tablicy na przecho < sr
Większe \ / Mniejsze f Mniejsze Większe
wywanie liczby węzłów, co pokazano na drzewo ) V drzewo V drzewo drzewo J
stronie 240, jednak zapewnia znaczną
poprawę wydajności. Jest to algorytm S z y b k a m e to d a u n i o n O z w a g a m i
z szybki} metodę uni on () z wagami. Las
drzew utworzony przez ten algorytm dla pliku [Link] pokazano na rysunku w le
wej górnej części strony 241. Nawet w tym krótkim przykładzie wysokość drzewa jest
wyraźnie mniejsza niż jego wysokość w wersji bez wag.
A naliza szybkiej m etody union() z w agami Na rysunku w prawej górnej części
strony 241 pokazano najgorszy przypadek dla szybkiej metody uni on () z wagami,
kiedy to wielkość drzew scalanych w metodzie
uni on () jest zawsze równa (i jest potęgą dwój % java WeightedQuickUnionUF < [Link]
ki). Przedstawione struktury drzewiaste wy 528 503
548 523
glądają na skomplikowane, jednak mają prostą
cechę — wysokość drzewa o 2 n węzłach wyno lic z b a składowych: 3
si n. Ponadto przy scalaniu dwóch drzew o 2 n
% java WeightedQuickUnionUF < la rg eU [Link]
węzłach uzyskujemy drzewo o 2 n+1 węzłach, 785321 134521
a jego wysokość rośnie do n+1. Można uogól 696834 98245
nić tę obserwację, aby utworzyć dowód na to,
lic z b a składowych: 6
że algorytm z wagami gwarantuje wydajność
logarytmiczną.
240 RO ZD ZIA Ł 1 Podstaw y
ALGORYTM 1.5 (ciąg dalszy).
Implementacja dla problemu Union-Find (szybka metoda unlon() z wagami)
public c la s s WeightedQuickUnionUF
{
p rivate i n t [ ] id;
// Odnośniki do rodziców (w t a b l ic y indeksowanej
// punktami).
private i n t [] sz; // Wielkości składowych określonych za pomocą korzeni
// (w t a b l ic y indeksowanej punktami),
private in t count; // Liczba składowych.
public WeightedQuickUnionUF(int N)
{
count = N;
id = new i n t [ N ] ;
fo r (in t i = 0 ; i < N; i++) id [i] = i;
sz = new i n t [ N ];
fo r (in t i = 0 ; i < N; i++) sz [i ] = 1;
}
public in t count()
{ return count; }
public boolean connected(int p, in t q)
{ return find(p) == find(q); }
private in t find (i nt p)
{ // Podążanie za odnośnikami w celu znalezienia korzenia,
while (p ! = i d [ p ] ) p = i d [ p ] ;
return p;
}
public void u n ion (in t p, in t q)
{
in t i = find(p);
in t j = find(q);
i f (i == j) return;
// Ustawianie mniejszego korzenia, aby prowadził do większego,
i f (sz [i ] < s z [j ]) { id [i] = j; sz [j] += sz [ i ] ; }
else ( id [ j ] = i; sz [i] += sz [ j ] ; }
count--;
}
_ } ___________________________________________________________________
Kod ten najłatwiej zrozumieć w kategoriach opisanej w tekście reprezentacji w postaci lasu
drzew. Dodano zmienną egzemplarzasz [](tablica indeksowana punktami), aby metoda
union() mogła powiązać korzeń mniejszego drzewa z korzeniem większego. Ten dodatek
umożliwia rozwiązywanie dużych problemów.
1.5 h Studium przypadku — problem Union-Find 241
Przykładow e dane wejściow e D ane w ejściow e dla n ajg o rszeg o przypadku
p q
® ® @ © © © © ® ® ®
p q
®©®©®®®0
4 3 @ © © ( 4 ) © © ® ® ® o i ® © © © © © ®
® ©
®©© ©©®® 2 3 ® (|) © © © ®
6 5 ® © © © © ® ® 4 5
© ® (?)
9 4 ® © © JS i © ® 6 7
& ® © ©
2 1 ® © jS l © ® 0 2
© © ® ©) ©
8 9
5 0 4 6
7 2
0 4
6 1
1 0
6 7
Ślad d z ia ła n ia te c h n ik i z s z y b k ą m e to d ą u n i o n O z w a g a m i (lasy d rzew )
Twierdzenie H. Głębokość dowolnego węzła w lesie zbudowanym za pomocą
szybkiej m etody union () z wagami dla N miejsc wynosi najwyżej lg N.
Dowód. Udowodnijmy bardziej ogólne stwierdzenie za pom ocą (zupełnej) in
dukcji — wysokość dowolnego drzewa wielkości k w lesie wynosi najwyżej lg k.
Podstawowy przypadek oparty jest na tym, że dla k równego 1 wysokość drze
wa wynosi 0. Zgodnie z hipotezą indukcyjną zakładamy, że wysokość drzewa
o wielkości i wynosi najwyżej lg i dla wszystkich i < k. Po połączeniu drzewa
0 wielkości i z drzewem o wielkości j przy i < j oraz i + j = k głębokość każdego
węzła w mniejszym zbiorze zwiększana jest o 1 , jednak teraz węzły znajdują się
w drzewie o wielkości = k, tak więc właściwość zachowano z uwagi na to, że
1 + Igi = lg(i + i) < lg(i +j) = lgk.
C. -
242 R O ZD ZIA Ł 1 □ Podstawy
Szybka metoda unionO
Z wagami
A^
® JK o o o o o o o
Ą
Średnia głębokość -1,52
Szybka m e to d a u n io n O i szybka m e to d a u n io n O z w agam i (100 punktów , 88 operacji u n io n O )
Wniosek. Przy stosowaniu szybkiej metody union() z wagami dla N punktów
tempo wzrostu dla najgorszego przypadku wynosi dla metod find (), connected()
i unionO logN.
Dowód. Każda operacja wykonuje najwyżej stałą liczbę dostępów do tablicy dla
każdego węzła ze ścieżki z węzła do korzenia w lesie.
W kontekście dynamicznego określania połączeń praktyczne implikacje płynące
z t w i e r d z e n i a H i wniosku są takie, że szybka metoda unionO z wagami to jedyny
z trzech algorytmów, który można z powodzeniem zastosować dla dużych problemów.
Algorytm oparty na szybkiej metodzie uni on() z wagami wymaga najwyżej c M ig N
dostępów do tablicy w celu przetworzenia M połączeń między N punktami (c to nie
wielka stała). Wynik ten jest zdecydowanie inny niż w technice z szybką metodą find (),
która zawsze (natomiast szybka metoda union() czasem) wymaga, przynajmniej M N
dostępów do tablicy. Dlatego szybka metoda union() z wagami pozwala zagwaranto
wać, że duże praktyczne problemy dynamicznego określania połączeń można rozwią
zać w sensownym czasie. Za cenę kilku dodatkowych wierszy kodu uzyskujemy pro
gram, który dla dużych problemów dynamicznego określania połączeń, które czasem
występują w praktyce, może być miliony razy szybszy od prostszych algorytmów.
Na początku tej strony pokazano przykład ze 100 punktami. Na rysunku wyraźnie
widać, że przy stosowaniu szybkiej m etody union() z wagami stosunkowo niewiele
węzłów znajduje się daleko od korzenia. Program często scala jednowęzłowe drze
wo z większym, dlatego węzeł oddalony jest od korzenia o tylko jeden odnośnik.
W empirycznych badaniach nad dużymi problemami wykazano, że szybka metoda
union() z wagami zwykle rozwiązuje praktyczne problemy w stałym czasie na opera
cję. Trudno oczekiwać bardziej wydajnego algorytmu.
1.5 0 Studium przypadku — problem Union-Find 243
Algorytm Tempo wzrostu dla N p u n k tó w (dla n ajgo rsze go przypadku)
Konstruktor union() find()
Szybka metoda find() N N 1
Szybka metoda union() N Wysokość drzewa Wysokość drzewa
Szybka metoda find() z wagami N lgN Ig N
Szybka metoda find() N Bardzo, bardzo blisko 1 (z amortyzacją);
z wagami i kompresję ścieżek zobacz ć w i c z e n i e 1 .5.13
Niemożliwe N 1 1
Cechy dotyczące wydajności algorytm ów dla problemu Union-Find
O ptym alne algorytm y Czy istnieje algorytm, który gwarantuje stały czas na opera
cję? Jest to niezwykle trudne pytanie, nad którym badacze zastanawiają się od wielu
lat. W poszukiwaniu odpowiedzi zbadano wiele odm ian technik z szybką metodą
un ion () i szybką metodą u n io n () z wagami. Opisana dalej przykładowa metoda,
kompresja ścieżek, jest łatwa w implementacji. W idealnym rozwiązaniu każdy węzeł
powinien prowadzić bezpośrednio do korzenia drzewa, jednak należy unikać kosz
tów zmiany dużej liczby odnośników, co było potrzebne w algorytmie z szybką m eto
dą find ( ) . Można zbliżyć się do optimum, ustawiając wszystkie sprawdzane węzły tak,
aby prowadziły bezpośrednio do korzenia. Na pozór wydaje się, że to ekstremalne
rozwiązanie, jednak łatwo je zaimplementować. Nie ma nic specjalnego w strukturze
omawianych drzew. Jeśli można je zmodyfikować w celu zwiększenia wydajności al
gorytmu, należy to zrobić. Aby zaimplementować kompresję ścieżek, wystarczy dodać
do metody find() nową pętlę, która ustawia element id[] dla każdego napotkanego
węzła na odnośnik prowadzący bezpośrednio do korzenia. W efekcie drzewa zostają
prawie całkowicie spłaszczone, co pozwala zbliżyć się do ideału osiągniętego w algo
rytmie z szybką m etodą find ( ) . Technika ta jest prosta i skuteczna, jednak w prak
tycznych zastosowaniach prawdopodobnie nie da się zauważyć poprawy względem
szybkiej m etody uni on () z wagami (zobacz ć w i c z e n i e 1 . 5 .24 ). Teoretyczne wyniki
dotyczące tego zagadnienia są niezwykle skomplikowane i dość istotne. Szybka me
toda uni on () z wagami i kompresję ścieżek jest optymalna, ale nie zapewnia stałego
czasu na operację. Oznacza to nie tylko tyle, że opisana technika nie działa w stałym
czasie na operację (po amortyzacji), ale też to, że nie istnieje algorytm, który gwaran
tuje wykonanie każdej operacji Union-Find w stałym czasie (z amortyzacją) w bar
dzo ogólnym modelu obliczeń opartym na dostępie do komórek (ang. celi probe).
Szybka m etoda u n io n () z wagami i kompresją ścieżek jest bardzo bliska optym alne
mu rozwiązaniu problemu.
244 RO ZD ZIA Ł 1 ■ Podstawy
Wykresy kosztów z am ortyzacją Warto tu, tak jak dla implementacji każdego typu
danych, przeprowadzić eksperymenty w celu sprawdzenia poprawności hipotez doty
czących wydajności dla typowych klientów,
Szybka m e to d a f i n d O
jak opisano to w p o d r o z d z i a l e 1 .4 . Na ry
1300- sunku po lewej stronie pokazano szczegóło
wo wydajność algorytmów wspomagających
tworzenie aplikacji klienta do dynamicz
Jedna szara kropka dla nego określania połączeń dla przykła
każdego połączenia
przetworzonego przez klienta
du z 625 punktami (plik mediumUF.
txt). Tworzenie takich wykresów jest
łatwe (zobacz ć w i c z e n i e 1 .5 .1 6 ). Dla
Operacje metody u n io n ( ) wymagają
przynajmniej 625 dostępów ¿-tego przetwarzanego połączenia należy
zapisać zmienną cost, która określa liczbę
dostępów do tablicy (i d [] lub sz []). Należy
też utworzyć zmienną t ot al , która zawiera
Czerwone 458 sumę dotychczasowych dostępów do tablicy.
kropki oznaczają
skumulowaną średnią Następnie wystarczy narysować szarą kropkę
w punkcie ( i , cost) i czerwoną w punkcie
( i, t o t a l / i ) . Czerwone kropki określają
średni koszt na operację (po amortyzacji).
Wykresy pozwalają dobrze zrozumieć dzia
Operacje metody c o n n e c te d ( ) wymagają łanie algorytmu. W technice z szybką me
dokładnie dwóch dostępów do tablicy
todą find() każda operacja union() wymaga
\ ______ przynajmniej 625 dostępów (plus jeden na
Liczba p o łączeń
“n
900 każdą scalaną składową, aż do kolejnych 625
dostępów), a każda operacja connected () —
Szybka m e to d a u n io n O dwóch dostępów. Początkowo większość po
1 0 0 —1
Operacje f i n d O łączeń wymaga wywołania metody union(),
stają się kosztowne
dlatego skumulowana średnia jest bliska 625.
OJ Później większość połączeń prowadzi do wy
wołań connected (), pozwalających pominąć
Szybka m e to d a u n io n O z w agam i wywołania union(), dlatego średnia skumu
Brak kosztownych operacji
lowana spada, choć wciąż pozostaje stosun
20 n
0 - 1-
\ ______ kowo wysoka. Dane wejściowe, dla których
duża liczba wywołań connected() prowadzi
Koszt wszystkich operacji (dla 625 punktów)
do pominięcia wywołania union(), zapew
niają wyraźnie lepszą wydajność — zobacz na przykład ć w i c z e n i e 1 .5 .2 3 . W technice
z szybką metodą uni on () wszystkie operacje wymagają początkowo tylko kilku dostę
pów do tablicy. Ostatecznie wysokość drzewa zaczyna odgrywać ważną rolę, a koszty
po amortyzacji znacznie rosną. W technice z szybką metodą uni on () z wagami wyso
kość drzewa pozostaje mała, dlatego żadna z operacji nie jest kosztowna, a koszty po
amortyzacji są niskie. Eksperymenty są potwierdzeniem wniosków, zgodnie z którymi
warto zaimplementować szybką metodę un i on () z wagami. Technika ta nie pozostawia
dużo miejsca na poprawę w kontekście praktycznych problemów.
1.5 ■ Studium przypadku— problem Union-Find 245
P e r s p e k ty w y Intuicyjnie widać, że każda z opisanych implementacji klasy UF jest
usprawnieniem w porównaniu z poprzednią wersją, jednak proces zmian jest sztucz
nie płynny, ponieważ mamy możliwość przyjrzenia się po fakcie modyfikacjom algo
rytmów badanych przez naukowców przez wiele lat. Przedstawione implementacje
są proste, a problem — dobrze określony, dlatego m ożna ocenić różne algorytmy
bezpośrednio, przeprowadzając empiryczne badania. Ponadto można wykorzystać
badania do sprawdzenia matematycznych obliczeń, które pozwalają ilościowo okre
ślić wydajność algorytmów. Kiedy to możliwe, dla najważniejszych problemów stosu
jemy w książce te same podstawowe kroki, co dla algorytmów Union-Find opisanych
w tym podrozdziale. Niektóre etapy wymieniono na poniższej liście.
n Tworzenie kompletnego i specyficznego opisu problemu, w tym określenie
podstawowych abstrakcyjnych operacji charakterystycznych dla problemu i in
terfejsu API.
B Staranne opracowanie krótkiej implementacji prostego algorytmu z wykorzy
staniem dobrze przemyślanego klienta wspomagającego tworzenie aplikacji
i realistycznych danych wejściowych.
0 Ustalenie, w jakich warunkach implementacja nie pozwala na rozwiązanie proble
mów o wymaganym rozmiarze, co wymaga jej usprawnienia lub rezygnacji z niej.
° Opracowanie usprawnionych implementacji w procesie stopniowego ulepsza
nia i potwierdzenie skuteczności usprawnień poprzez analizy empiryczne, m a
tematyczne lub obu rodzajów.
■ Znalezienie abstrakcyjnych wysokopoziomowych reprezentacji struktur da
nych lub algorytmów, które umożliwią skuteczne zaprojektowanie usprawnio
nych wersji na ogólnym poziomie.
■ Próby zapewnienia gwarancji wydajności dla najgorszego przypadku, przy
czym należy zaakceptować wysoką wydajność dla typowych danych, jeśli m oż
na ją uzyskać.
■ Ustalenie, kiedy pozostawić wprowadzanie dalszych usprawnień przez szczegółowe,
dogłębne badania doświadczonym naukowcom i przejść do następnego problemu.
Możliwość uzyskania spektakularnej poprawy wydajności dla praktycznych proble
mów, co pokazano na przykładzie problemu Union-Find, sprawia, że projektowanie
algorytmów jest tak atrakcyjną dziedziną badań. Jakie inne obszary projektowania
pozwalają potencjalnie uzyskać oszczędności rzędu milionów lub miliardów razy
(a nawet większe)?
Opracowanie wydajnego algorytmu jest intelektualnie satysfakcjonującą czynnością,
która może przynieść bezpośrednie praktyczne korzyści. Jak pokazano to na problemie
dynamicznego określania połączeń, opisany w prosty sposób problem może wymagać
analizy wielu algorytmów, które są nie tylko przydatne i interesujące, ale też wyrafino
wane i trudne do zrozumienia. Można natrafić na liczne pomysłowe algorytmy, opra
cowane przez lata na potrzeby wielu praktycznych problemów. Wraz z poszerzaniem
się zastosowań technik obliczeniowych do rozwiązywania naukowych i komercyjnych
problemów rośnie też znaczenie umiejętności stosowania wydajnych algorytmów do
znanych zadań oraz opracowywania wydajnych rozwiązań nowych problemów.
246 ROZDZIAŁ 1 ■ Podstawy
| Pytania i odpowiedzi
P. Chciałbym dodać do interfejsu API metodę del ete (), która umożliwi klientom
usuwanie połączeń. Macie jakieś wskazówki?
O. Nikt nie zaprojektował algorytmu do usuwania połączeń, który byłby tak prosty
i wydajny, jak rozwiązania przedstawione w tym podrozdziale. Zagadnienie to po
wtarza się w książce. Niektóre z omawianych struktur mają tę cechę, że usuwanie
z nich danych jest dużo trudniejsze niż ich dodawanie.
P. Czym jest model oparty na dostępie do komórek?
O. Jest to model obliczeń, w którym uwzględniane są tylko dostępy do pamięci o do
stępie swobodnym na tyle dużej, że mieści całe dane wejściowe. Wszystkie pozostałe
operacje są uznawane za bezkosztowe.
1.5 a Studium przyp ad ku — problem Union-Find 247
ĆWICZENIA
1.5.1 . Wyświetl zawartość tablicy i d [] i liczbę dostępów do tablicy dla każdej pary
wejściowej w algorytmie z szybką m etodą find () użytym dla ciągu: 9-0 3-4 5-8 7-2
2-1 5-7 0-3 4-2.
1.5.2. Wykonaj ć w i c z e n i e 1 .5 .1 , ale dla szybkiej m etody u n i o n ( ) (strona 236).
Ponadto narysuj las drzew reprezentowany przez tablicę i d [] po przetworzeniu każ
dej pary wejściowej.
1.5.3. Wykonaj ć w ic z e n ie 1 .5 . 1 , użyj jednak szybkiej metody u n i o n ( ) z wagami
(strona 240).
1.5.4. Wyświetl zawartość tablic sz [] i i d [] oraz liczbę dostępów do tablicy dla każdej
pary wejściowej z przedstawionych w tekście przykładów dla szybkiej metody union ()
z wagami (zarówno dla przykładowych danych, jak i dla najgorszego przypadku).
1.5.5. Oszacuj m inim alną ilość czasu (w dniach) potrzebną na rozwiązanie szybką
metodą find() problemu dynamicznego określania połączeń dla 109 punktów i 106
par wejściowych. Przyjmij, że kom puter wykonuje 109 instrukcji na sekundę, a każda
iteracja wewnętrznej pętli for wymaga wykonania 10 instrukcji maszynowych.
1 .5.6. Wykonaj ĆWICZENIE 1.5.5 dla szybkiej metody uni on () z wagami.
1 .5.7. Opracuj klasy Qui ckUni onllF i Qui ckFi ndUF, będące — odpowiednio — imple
mentacjami technik z szybką metodą union() oraz szybką m etodą find().
1.5.8. Podaj kontrprzykład pokazujący, dlaczego intuicyjna implementacja metody
uni on () z techniki z szybką m etodą find () jest nieprawidłowa:
public void u n ion(int p, in t q)
(
i f (connected(p, q ) ) return;
// Zmiana nazwy składowej obejmującej p na nazwę składowej
// zawierającej q.
fo r (in t i = 0 ; i < id .length; i++)
i f (id [i] ==i d [p] ) id [i] = i d [q] ;
count--;
}
1.5.9. Narysuj drzewo odpowiadające tabli
cy i d [] przedstawionej po prawej stronie. Czy i 0 1 2 3 4 5 6 7 8 9
może być ona efektem działania szybkiej metody —— ------------------------------------------------
uni on () z wagami? Wyjaśnij, dlaczego jest to nie- id [i] 1 1 3 1 5 6 1 3 4 5
możliwe, lub podaj ciąg operacji prowadzący do
otrzymania takiej tablicy.
248 ROZDZIAŁ 1 □ Podstawy
ĆWICZENIA (ciąg dalszy)
1.5.10. Załóżmy, że w algorytmie z szybką metodą union() z wagami ustawiono
i d [find (p)] na q zamiast na i d [find ( q ) ] . Czy uzyskany algorytm będzie poprawny?
Odpowiedź: tak, ale wysokość drzewa będzie w nim większa, dlatego gwarancje wy
dajności nie będą obowiązywać.
1.5.1 1. Zaimplementuj technikę z szybkę metodą find() z wagami, w której elementy
mniejszej składowej są zawsze ustawiane w tablicy i d [] na identyfikator większej
składowej. Jak taka zmiana wpłynie na wydajność?
1.5 n Studium przypadku — problem Union-Find 249
i PROBLEMY DO ROZWIĄZANIA
1.5.12. Szybka metoda union() z kompresję ścieżek. Zmodyfikuj szybką metodę
uni on () (strona 236), wbudowując w nią kompresję ścieżek przez dodanie do metody
union() pętli, która łączy każdy punkt na ścieżkach od p i q do korzeni ich drzew
z korzeniem nowego drzewa. Podaj ciąg par wejściowych, dla których technika daje
ścieżkę o długości 4. Uwaga: zamortyzowany koszt na operację w tym algorytmie jest
logarytmiczny.
1.5.13. Szybka metoda union() z wagami i kompresję ścieżek. Zmodyfikuj szybką
metodę un io n () z wagami ( a l g o r y t m 1 .5 ), aby zaimplementować kompresję ście
żek, co opisano w ć w i c z e n i u 1 .5 . 1 2 . Podaj ciąg par wejściowych, dla którego metoda
tworzy drzewo o wysokości 4. Uwaga: zamortyzowany koszt na operację w tym algo
rytmie jest ograniczony pewną funkcją (odwrotnościę funkcji Ackermanna) i wynosi
poniżej 5 dla dowolnej stosowanej w praktyce wartości N.
1.5.14. Szybka metoda union() z wagami opartymi na wysokości. Opracuj imple
mentację klasy UF. Wykorzystaj tę samą podstawową strategię, co w szybkiej metodzie
uni on () z wagami, ale program ma śledzić wysokość drzewa i zawsze dołączać niższe
do wyższego. Udowodnij, że dla algorytmu górne ograniczenie wysokości drzew dla
N punktów jest logarytmiczne.
1.5.15. Drzewa dwumianowe. Wykaż, że dla najgorszego przypadku liczba węzłów
drzewa na każdym poziomie w technice z szybką metodą union() z wagami odpo
wiada współczynnikom dwumianowym. Oblicz średnią głębokość węzła w drzewie
dla najgorszego przypadku dla N = 2n węzłów.
1.5.16. Wykresy amortyzowanych kosztów. Dopracuj implementacje z ć w i c z e n i a
1 .5 .7 , aby generowały zamortyzowane wykresy kosztów podobne do tych z tekstu.
1.5.17. Losowe połęczenia. Opracuj klienta ErdosRenyi dla klasy UF. Klient m a p o
bierać z wiersza poleceń liczbę całkowitą N, generować losowe pary liczb całkowitych
z przedziału od 0 do N-l, wywoływać metodę connected () w celu ustalenia, czy pary
są połączone, a następnie wywoływać metodę union(), jeśli połączenie nie istnieje
(tak jak w kliencie wspomagającym tworzenie aplikacji). Program ma działać w pętli
do czasu połączenia wszystkich punktów i wyświetlać liczbę utworzonych połączeń.
Program ma obejmować metodę statyczną count(), która jako argument przyjmuje
N i zwraca liczbę połączeń, oraz metodę main(), przyjmującą N z wiersza poleceń,
wywołującą count () i wyświetlającą zwróconą wartość.
250 ROZDZIAŁ 1 ■ Podstawy
PROBLEMY DO ROZW IĄZANIA (ciąg dalszy)
1.5.18. Generator losowych tabel. Napisz program RandomGrid, który przyjmuje
z wiersza poleceń wartość N typu i nt, generuje wszystkie połączenia w tabeli N na N,
umieszcza połączenia w losowej kolejności, ustawia elementy par w losowym porząd
ku (tak aby pary p q i q p były równie prawdopodobne) i wyświetla wynik w standar
dowym wyjściu. Do losowego uporządkowania połączeń użyj klasy RandomBag (zo
bacz ć w i c z e n i e 1 .3.34 na stronie 179). W celu hermetyzacji p i q w jednym obiekcie
zastosuj pokazaną dalej klasę zagnieżdżoną Connection. Zapisz program jako dwie
metody statyczne: generate ( ) , która jako argument przyjmuje N i zwraca tablicę po
łączeń, oraz mai n(), pobierającą N z wiersza poleceń, wywołującą metodę generate()
i przechodzącą po zwróconej tablicy w celu wyświetlenia połączeń.
1.5.19. Animacje. Napisz klienta klasy RandomGrid (zobacz ć w i c z e n i e 1 .5 . 1 8 ), uży
wającego klasy Uni onFi nd do sprawdzania połączeń (tak jak w kliencie wspomagają
cym tworzenie aplikacji) i biblioteki StdDraw do wyświetlania połączeń w czasie ich
przetwarzania.
1.5.20. Dynamiczny wzrost. Za pomocą list powiązanych lub tablic o zmiennej
wielkości opracuj implementację techniki z szybką m etodą union() z wagami, tak
aby nie trzeba było z góry określać liczby obiektów. Do interfejsu A P I dodaj metodę
NewSi te ( ) , zwracającą identyfikator typu i nt.
p riv a t e c l a s s Connection
1
i n t p;
i n t q;
p ub lic C onnectionfint p, i n t q)
( t h i s . p = p; t h i s . q = q; }
)
Rekord do hermetyzacji połączeń
1.5 ■ Studium przypadku — problem Unlon-Find 251
EKSPERYMENTY
1.5.21. Model Erdósa-Renyiego. Wykorzystaj klienta z ć w ic z e n ia 1 .5.17 do prze
testowania hipotezy, wedle której liczba par wygenerowanych do czasu powstania
jednej składowej wynosi ~ 'A N ln N.
1.5.22. Test podwajania dla modelu Erdósa-Renyiego. Opracuj klienta do testowania
wydajności, który pobiera z wiersza poleceń wartość T typu i nt i wykonuje T prób
opisanego dalej eksperymentu. Użyj klienta z ć w i c z e n i a 1 .5.17 do wygenerowania
losowych połączeń, używającego klasy UnionFind do sprawdzania połączeń (tak jak
w kliencie wspomagającym tworzenie aplikacji). Program ma działać w pętli do cza
su połączenia wszystkich punktów. Dla każdego Nwyświetl wartość N, średnią liczbę
przetworzonych połączeń i stosunek czasu wykonania do poprzedniego takiego cza
su. Użyj program u do sprawdzenia hipotez z tekstu, zgodnie z którymi czasy wyko
nania dla technik z szybką metodą find () i szybką metodą union() są kwadratowe,
a szybka metoda uni on () z wagami działa w czasie bliskim liniowemu.
1.5.23. Porównaj techniki z szybkę metodę find() i szybkę metodę union() w modelu
Erdósa-Renyiego. Opracuj klienta do testowania wydajności, który pobiera z wiersza
poleceń wartość T typu i nt i wykonuje T prób opisanego dalej eksperymentu. Użyj
klienta z ć w i c z e n i a 1 .5.17 do wygenerowania losowych połączeń. Zapisz połącze
nia, tak aby można było użyć zarówno szybkiej m etody union(), jak i szybkiej m e
tody find () do sprawdzenia połączeń (tak jak w kliencie wspomagającym tworzenie
aplikacji). Program ma działać w pętli do czasu połączenia wszystkich punktów. Dla
każdego Nwyświetl wartość Ni stosunek dwóch czasów wykonania.
1.5.24. Szybkie algorytmy w modelu Erdósa-Renyiego. Do testów z ć w i c z e n i a 1 . 5.23
dodaj szybką metodę union() i szybką metodę union() z wagami i kompresją ścieżek.
Czy widzisz różnicę między tymi dwoma algorytmami?
1.5.25. Test podwajania dla losowych tabel. Opracuj klienta do testowania wydaj
ności, który pobiera z wiersza poleceń wartość T typu i nt i wykonuje T powtórzeń
opisanego dalej eksperymentu. Użyj klienta z ć w i c z e n i a 1 . 5.18 do wygenerowania
połączeń (z losową kolejnością par i przypadkowym porządkiem elementów w pa
rach) w kwadratowej tabeli N na N, a następnie zastosuj klasę UnionFind do spraw
dzenia połączeń, tak jak w kliencie wspomagającym tworzenie aplikacji. Program
ma działać w pętli do czasu połączenia wszystkich punktów. Dla każdego Nwyświetl
wartość N, średnią liczbę przetworzonych połączeń i stosunek czasu wykonania do
wcześniejszego takiego czasu. Za pomocą program u sprawdź hipotezy, wedle których
czasy wykonania dla technik z szybkimi metodami find() i union() są kwadratowe,
a szybka m etoda union() z wagami działa prawie liniowo. Uwaga: wraz z podwaja
niem Nliczba pól w tabeli rośnie czterokrotnie, dlatego czynnik podwajania powinien
wynieść 16 dla technik kwadratowych i 4 dla liniowych.
252 ROZDZIAŁ 1 □ Podstawy
EKSPERYMENTY (ciąg dalszy)
1.5.26. Wykresy zamortyzowanych kosztów w modelu Erdósa-Renyiego. Opracuj
klienta, który przyjmuje z wiersza poleceń wartość Ntypu i nt i tworzy wykres zamor
tyzowanych kosztów wszystkich operacji (podobny do rysunków z tekstu). Program
ma generować losowe pary liczb całkowitych z przedziału od 0 do N-l, wywoływać
metodę connected() w celu ustalenia, czy punkty są połączone, anastępnie union(),
jeśli nie są (tak jak w kliencie wspomagającym tworzenie aplikacji). Program ma
działać w pętli do czasu połączenia wszystkich punktów.
ROZDZIAŁ 2
m li Sortowanie
ortowanie to proces porządkowania obiektów w logiczny sposób. Przykładowo,
S na wydruku dla użytkownika karty kredytowej transakcje są uporządkowane
chronologicznie. Kolejność ta została prawdopodobnie wyznaczona przez algo
rytm sortowania. W początkowym okresie rozwoju informatyki szacowano, że do 30%
wszystkich cykli procesora poświęcanych jest na sortowanie. To, że obecnie odsetek
ten jest niższy, wynika z tego, iż algorytmy sortowania są stosunkowo wydajne, a nie ze
zmniejszenia znaczenia tej operacji. Wszechobecność komputerów sprawia, że dostęp
nych jest mnóstwo danych, a pierwszym krokiem przy ich organizowaniu jest często
sortowanie. We wszystkich systemach komputerowych istnieją implementacje algoryt
mów sortowania dostępne dla systemu i użytkowników.
Są trzy praktyczne powody, dla których warto poznać algorytmy sortowania
(mimo że m ożna zastosować sortowanie systemowe).
■ Analiza algorytmów sortowania jest solidnym wprowadzeniem do podejścia
używanego przy porównywaniu wydajności algorytmów w tej książce.
° Podobne techniki są skuteczne w rozwiązywaniu innych problemów.
° Algorytmy sortowania często służą za punkt wyjścia przy rozwiązywaniu in
nych problemów.
Ważniejsze od tych praktycznych powodów jest to, że algorytmy sortowania są ele
ganckie, klasyczne i skuteczne.
Sortowanie odgrywa kluczową rolę w komercyjnym przetwarzaniu danych
i współczesnych obliczeniach naukowych. Istnieje wiele zastosowań takich algoryt
mów w obszarze przetwarzania transakcji, optymalizacji kombinatorycznej, astro
fizyki, dynamiki molekularnej, lingwistyki, badań nad genomem, prognozowania
pogody itd. Jeden z algorytmów sortowania (sortowanie szybkie, opisane w p o d
r o z d z i a l e 2 .3 ) został uznany za jeden z 10 najważniejszych algorytmów XX wieku
w dziedzinie nauki i inżynierii.
W tym rozdziale omówiono kilka klasycznych m etod sortowania i wydajną imple
mentację ważnego typu danych — kolejki priorytetowej. Opisano teoretyczne pod
stawy porównywania algorytmów sortowania, a rozdział zakończono analizą zasto
sowań sortowania i kolejek priorytetowych.
255
w r a m a c h p i e r w s z e j W YPRA W Y do krainy algorytmów sortowania analizujemy
dwie podstawowe m etody sortowania i odmianę jednej z nich. Oto niektóre powody
do zapoznania się z tymi stosunkowo prostymi algorytmami. Po pierwsze, zapewnia
ją one kontekst, w którym m ożna poznać terminologię i podstawowe mechanizmy.
Po drugie, te proste algorytmy są w niektórych zastosowaniach wydajniejsze od za
awansowanych algorytmów omówionych dalej. Po trzecie, jak się okaże, pozwalają
poprawić wydajność bardziej skomplikowanych rozwiązań.
Reguły Zajmujemy się przede wszystkim algorytmami do zmiany kolejności
w tablicach elementów, w których każdy element posiada klucz. Zadaniem algorytmu
sortowania jest zmiana kolejności elementów, tak aby klucze były uporządkowane
według dobrze zdefiniowanej reguły (zwykle w porządku liczbowym lub alfabetycz
nym). Należy uporządkować tablicę, żeby klucz każdego elementu był nie mniejszy
niż klucz na każdej pozycji o niższym indeksie i nie większy niż klucz w elementach
o większych indeksach. Specyficzne cechy kluczy i elementów mogą być bardzo róż
ne w poszczególnych zastosowaniach. W Javie elementy są obiektami, a abstrakcyjne
pojęcie „klucz” jest ujęte we wbudowanym mechanizmie — opisanym na stronie 259
interfejsie Comparabl e.
Klasa Example, przedstawiona na następnej stronie, to ilustracja zastosowanych
konwencji. Kod sortujący umieszczono w metodzie s o rt() w tej samej klasie, co
prywatne funkcje pomocnicze 1 e s s () i e x c h ( ) (a czasem także kilka innych) oraz
przykładowego klienta mai n ( ) . W klasie Exampl e znajduje się też kod, który może być
przydatny przy wstępnym diagnozowaniu. Klient testowy m a in () sortuje łańcuchy
znaków ze standardowego wejścia i używa prywatnej metody show() do wyświet
lenia zawartości tablicy. W dalszej części rozdziału zbadano różne ldienty testowe,
służące do porównywania algorytmów i analizowania ich wydajności. Aby rozróżnić
metody sortowania, różnym klasom nadano inne nazwy. W klientach można wy
woływać różne implementacje za pomocą specyficznych nazw: I n s e r t i o n . s o rt() ,
M e r g e . s o r t ( ) , Q u i c k . s o r t Q itd.
Kod sortujący przeważnie korzysta z danych za pomocą tylko dwóch operacji:
metody 1 e s s (), która porównuje elementy, oraz metody exch(), zamieniającej je
miejscami. Implementowanie metody exch() jest łatwe, a interfejs Comparable uła
twia implementowanie m etody 1e s s (). Ponieważ dostęp do danych mają tylko te
dwie operacje, kod jest czytelny i przenośny, a ponadto łatwo jest sprawdzać popraw
ność algorytmów, badać ich wydajności oraz porównywać je. Przed przejściem do
implementacji sortowania omówiono liczne ważne kwestie, które trzeba starannie
przemyśleć dla każdej techniki sortowania.
2.1 Podstawowe metody sortowania 257
Szablon klas sortujących
p u b lic c la s s Example
{
p u b l i c s t a t i c v o i d s o r t ( C o m p a r a b l e [ ] a)
{ / * Zobacz a l g o r y t m y 2 . 1 , 2 . 2 , 2 . 3 , 2 . 4 , 2 . 5 l u b 2 . 7 . * / }
p r i v a t e s t a t i c b o o le a n l e s s (C om parable v, Com parable w)
( r e t u r n v . c o m p á r e l o (w) < 0; }
p r i v a t e s t a t i c v o i d e x c h (C o m p a r a b le [] a, i n t i , in t j)
{ C om parable t = a [ i ] ; a [i ] = a [ j ] ; a [ j ] = t; }
p r i v a t e s t a t i c v o i d sh o w (C o m p a ra b le [] a)
{ // W y ś w i e t la t a b l i c ę w jednym w i e r s z u ,
f o r ( i n t i = 0; i < a . l e n g t h ; i + + )
Std O u t.p rin t(a [i] + " ");
S td O u t.p rin tln ();
}
p u b l i c s t a t i c b o o le a n i s S o r t e d ( C o m p a r a b l e [ ] a)
{ // Sp raw d za, c z y e lem enty t a b l i c y mają o d p o w ie d n ią k o l e j n o ś ć ,
fo r ( i n t i = 1; i < a . l e n g t h ; i + + )
i f (1 e s s (a [i ] , a [ i - 1 ] ) ) r e t u r n f a l s e ;
re tu rn true;
}
p u b lic s t a t i c vo id m a in ( S t r in g [ ] a rgs)
{ // W c z y t u je ł a ń c u c h y znaków ze sta n d ard o w e go w e j ś c i a ,
// s o r t u j e j e i w y ś w i e t l a .
Strin g [] a = In .re a d S trin g s();
sort(a);
a sse rt isS o rte d (a );
show (a);
}
}
% more t i n y . t x t
S 0 R T E X A M P L E
W klasie tej przedstawiono konwencje używane
dalej do implementowania technik sortowania tab % j a v a Example < t i n y . t x t
lic. Dla każdego algorytmu sortowania pokazano A E E L M O P R S T X
metodę s o rt() z podobnej klasy, przy czym nazwę
Example zmieniono na nazwę odpowiednią dla al % more w o r d s 3 . t x t
gorytmu. Klient testowy sortuje łańcuchy znaków bed bug dad y e s zoo . . . a l l bad y e t
ze standardowego wejścia, jednak metody sortowa
% j a v a Example < w o r d s . t x t
nia zadziałają dla dowolnego typu danych imple
all bad bed bug dad . . . y e s y e t zoo
mentującego interfejs Comparabl e.
258 RO ZD ZIA Ł 2 ■ Sortowanie
Spraw dzanie popraw ności Czy implementacja sortowania zawsze umieszcza ele
menty tablicy we właściwej kolejności, niezależnie od ich początkowego uporząd
kowania? Stosujemy konserwatywne podejście i umieszczamy w kliencie testowym
instrukcję a s s e rt i sSorted ( a ) a b y sprawdzić, czy elementy tablicy są po sortowa
niu odpowiednio uporządkowane. Warto umieścić tę instrukcję w każdej implemen
tacji sortowania, choć zwykle testujemy kod i opracowujemy matematyczne dowody
poprawności algorytmów. Warto zauważyć, że test jest wystarczający tylko wtedy,
jeśli do zmiany pozycji elementów tablicy używamy wyłącznie m etody exch (). Przy
stosowaniu kodu zapisującego wartości bezpośrednio w tablicy test nie gwarantuje
poprawności (za prawidłowy uznany zostanie na przykład kod niszczący pierwotną
tablicę wejściową przez ustawienie wszystkich elementów na tę samą wartość).
Czas w ykonania Testujemy też wydajność algorytmów.
Zaczynamy od udowodnienia faktów na temat liczby pod-
Model kosztów dla sorto- stawowych operacji (porównań i przestawień oraz czasem
wania. Przy analizowaniu liczby dostępów tablicy w celu odczytu lub zapisu), któ-
algorytmów sortowania li- re różne algorytmy sortowania wykonują dla rozmaitych
czone są porównania i prze- naturalnych modeli danych wejściowych. Następnie uży-
stawienia. Dla algorytmów, wamy tych faktów do opracowania hipotez dotyczących
które nie przestawiają ele- względnej wydajności algorytmów. Prezentujemy też
mentów, liczone są dostępy narzędzia do eksperymentalnego sprawdzania hipotez.
do tablicy. Używamy spójnego stylu kodowania, aby ułatwić tworze
nie prawidłowych hipotez na tem at wydajności, prawdzi
wych dla typowych implementacji.
D odatkow a pam ięć Ilość dodatkowej pamięci używanej przez algorytm sortowania
jest często równie ważnym czynnikiem jak czas wykonania. Algorytmy sortowania
dzielą się na dwa podstawowe rodzaje — sortujące w miejscu, które nie potrzebują
dodatkowej pamięci (za wyjątkiem małego stosu wywołań funkcji lub stałej liczby
zmiennych egzemplarza), oraz algorytmy wymagające dodatkowej pamięci na drugą
kopię sortowanej tablicy.
Typy danych Kod sortujący działa dla elementów każdego typu obsługującego
interfejs Comparable. Stosowanie się do konwencji Javy jest tu wygodne, ponieważ
wiele typów danych obsługuje ten interfejs. Dotyczy to na przykład nakładkowych
typów numerycznych Javy, takich jak Integer i Doubl e, a także typu S tring i różnych
zaawansowanych typów w rodzaju F ile lub URL. Wystarczy wywołać jedną z m e
tod sortowania, podając jako argument tablicę wartości dowolnego z tych typów.
Przykładowo, w kodzie po prawej stronie użyto
s o r to w a n ia sz y b k ie g o ( z o b a c z p o d r o z d z i a ł
‘ & v
2. 3 ) . m r.n
Double a [] = new Double[N];
do posortowania N losowych wartości typu f o r ( i n t i = 0; i < N; i+ + )
Double. Przy samodzielnym tworzeniu typów a [i]= S t d R a n d o m . u n if o r m O ;
można umożliwić w kodzie klienta sortowanie Quick.s o r t ( a ) ;
danych określonego typu, implementując inter- Sortowanie tablicy losowych wartości
2.1 □ Podstawowe metody sortowania 259
fejs Comparabl e. W tym celu wystarczy zaimplementować metodę compareTo (), która
wyznacza uporządkowanie obiektów typu w tak zwanym porządku naturalnym, co
pokazano tu dla typu danych Date (zobacz stronę 103). Zgodnie z konwencjami Javy
wywołanie v . compareTo (w) zwraca licz
bę całkowitą — ujem ną (zwykle - 1 ) dla p u b l i c c l a s s Date implements Comparable<Date>
v<w, zero dla v=w i dodatnią (zwykle +1 ) {
p r i v a t e final i n t day;
dla v>w. Z uwagi na zwięzłość w dalszej p r i v a t e final i n t month;
części akapitu używamy standardowe p r i v a t e final i n t y e a r ;
go zapisu w rodzaju v>w jako skrótu dla
p u b l i c D a t e ( i n t d, i n t m, i n t y)
kodu v. compareTo (w) >0. Wywołanie
{ day = d; month = m; y e a r = y ; }
v. compareTo (w) powoduje wyjątek, je
śli v i w mają niezgodne typy lub jedna p u b l i c i n t d a y() { r e t u r n day; }
p u b l i c i n t month() { r e t u r n month; }
z tych wartości to nul 1. Ponadto m eto
pu b lic in t year() { return year; )
da compareTo() musi wyznaczać porzą
dek liniowy. Musi więc być: p u b l i c i n t compareTo(Date t h a t )
0 zwrotna (v=v dla każdego v), 1
i f ( t h i s . y e a r > t h a t . y e a r ) r e t u r n +1;
0 antysymetryczna (dla wszystkich i f ( t h i s . y e a r < t h a t . y e a r ) r e t u r n -1;
v i w jeśli v<w, to w>v, a jeżeli v=w, i f ( t h is . m o n t h > t h at. m o nth ) r e t u r n +1;
to w=v), i f ( t h is . m o n t h < th at .m o nth ) r e t u r n - 1 ;
i f ( t h i s . d a y > t h a t . d a y ) r e t u r n +1;
° przechodnia (dla wszystkich v,
i f ( t h is . d a y < t h a t.d a y ) return -1;
w i x jeśli v<=w i w<=x, to v<=x). r e t u r n 0;
W matematyce reguły te są intuicyjne 1
i standardowe. N ietrudno się do nich
pub lic S tr in g t o S t r in g ()
dostosować. Ujmijmy to krótko — m e { r e t u r n month + " / " + day + " / " + y e a r ; }
toda compareTo() to implementacja }
abstrakcyjnego klucza. Definiuje upo
rządkowanie sortowanych elementów Definiowanie typu umożliwiającego porównyw anie
(obiektów), które mogą być dowolnego
typu obsługującego interfejs Comparabl e. Zauważmy, że w metodzie compareTo () nie
trzeba używać wszystkich zmiennych egzemplarza. Klucz może być małą częścią każ
dego elementu.
w d a l s z e j części r o z d z i a ł u omówiono liczne algorytmy do sortowania tablic obiek
tów mających porządek naturalny. Aby porównać algorytmy i przedstawić różnice
między nimi, zbadano wiele ich cech, w tym liczbę porównań i przestawień dla róż
nego rodzaju danych wejściowych oraz ilość potrzebnej dodatkowej pamięci. Cechy
te prowadzą do opracowania hipotez na temat wydajności. Wiele właściwości algoryt
mów sprawdzono w ostatnich dziesięcioleciach na niezliczonych komputerach. Zawsze
trzeba badać specyficzne implementacje, dlatego omówiono służące do tego narzędzia.
Po rozważeniu klasycznego sortowania przez wybieranie, sortowania przez wstawianie,
sortowania Shella, sortowania przez scalanie, sortowania szybkiego i sortowania przez
kopcowanie, w p o d r o z d z i a l e 2.5 omówiono praktyczne zagadnienia i zastosowania.
260 RO ZD ZIA Ł 2 Q Sortowanie
Sortowanie przez wybieranie Jeden z najprostszych algorytmów sortowa
nia działa tak — najpierw należy znaleźć najmniejszy element tablicy i przestawić
go z pierwszym elementem (z nim samym, jeśli to obiekt na pierwszej pozycji jest
najmniejszy). Następnie trzeba znaleźć kolejny najmniejszy element i przestawić go
z drugim elementem. Proces jest kontynuowany do m om entu posortowania całej
tablicy. M etoda ta nosi nazwę sortowanie przez wybieranie, ponieważ oparta jest na
wielokrotnym wybieraniu najmniejszego z pozostałych elementów.
Jak widać na podstawie implementacji w a l g o r y t m i e 2 .1 , pętla wewnętrzna
w sortowaniu przez wybieranie jedynie porównuje bieżący element z najmniejszym
ze znalezionych do tej pory (dodatkowy kod zwiększa bieżący indeks i sprawdza, czy
jego wartość nie wyszła poza granice tablicy). Trudno napisać prostszy kod. Operacja
przenoszenia elementów znajduje się poza pętlą wewnętrzną. Każde przestawienie
prowadzi do umieszczenia elementu na ostatecznej pozycji, dlatego liczba przesta
wień wynosi N. Tak więc czas wykonania jest zależny od liczby porównań.
Twierdzenie A, Sortowanie przez wybieranie wymaga - N 2/ 2 porównań i N
przestawień.
Dowód. M ożna to udowodnić, analizując ślad działania algorytmu. Jest nim ta
bela o wymiarach W naiV,w której litery w kolorze innym niż szary odpowiadają
porównaniom. Około połowa elementów tablicy (te na przekątnej i nad nią) jest
w kolorze innym niż szary. Każdy element na przekątnej odpowiada przestawie
niu. Ujmijmy to dokładniej — na podstawie analizy kodu m ożna stwierdzić, że
dla każdego i między 0 a N - 1 potrzeba jednego przestawienia i N - l - i porów
nań, co daje w sumie N przestawień i ( N - 1) + (N - 2) + ... + 2 + 1+ 0 = N ( N ~ 1)
/ 2 ~ N 2 / 2 porównań.
p o d s u m u j m y — sortowanie przez wybieranie to prosta m etoda sortowania, łatwa do
zrozumienia i zaimplementowania. Oto dwie specyficzne dla niej cechy.
Czas w ykonania jest niezależny od danych wejściowych Proces wyszukiwania
najmniejszego elementu w jednym przejściu przez tablicę nie zapewnia informacji
o tym, gdzie może znajdować się najmniejszy element w następnym przejściu. Cecha
ta w niektórych sytuacjach jest wadą. Przykładowo, osoba używająca klienta do sor
towania może być zaskoczona, kiedy stwierdzi, że sortowanie przez wybieranie działa
równie długo dla już uporządkowanej tablicy lub dla tablicy, w której wszystkie klu
cze są takie same, jak dla losowo uporządkowanej tablicy! Jak się okaże, inne algoryt
my lepiej wykorzystują początkowe uporządkowanie danych wejściowych.
Potrzebna je st m inim alna liczba przestaw ień Każde z N przestawień zmienia war
tość dwóch elementów tablicy, dlatego sortowanie przez wybieranie wymaga N prze
stawień. Liczba dostępów do tablicy rośnie liniowo wraz z wielkością tablicy. Żaden
inny z omawianych algorytmów sortowania nie posiada tej cechy (w większości
wzrost jest liniowo-logarytmiczny lub kwadratowy).
2.1 Podstawowe metody sortowania 261
ALGORYTM 2.1. Sortowanie przez wybieranie
public c la ss Sélection
{
public s t a t i c void sort(Comparable[] a)
{ / / Sortowanie a [] w porządku rosnącym,
in t N = a .le n g th ; / / Długość ta b lic y ,
fo r (in t i = 0; i < N; i++)
( / / Przestaw ianie a [i ] z najmniejszym elementem z a [ i+ l...N ) .
in t min = i ; / / Indeks minimalnego elementu,
fo r (in t j = i+1; j < N; j++)
i f ( l e s s ( a [ j ] , a[min])) min = j ;
exch(a, i , m in);
}
}
/ / Metody le s s () , exch(), isSortedQ i main() przedstawiono na stro n ie 257.
)
Dla każdego i implementacja umieszcza i -ty najmniejszy element w a [i ]. Elementy na lewo
od i to i najmniejszych elementów. Nie są one ponownie sprawdzane.
a []
i min 0 1 2 3 4 5 6 7 8 9 10 Czarne elementy są
■ sprawdzane w celu
S O R T E X A M p L E
znalezienia minimum
0 6 S 0 R T E X A M p L E
1 4 A O R T E X S M p L E Czerwone elementy
" foa[min]
2 10 A E R T O X s M p L E
3 9 A E E T 0 X s M p L R
4 7 A E E L 0 X s M p T R
5 7 A E E L M X s 0 p T R
6 8 A E E L M 0 s X p T R
7 10 A E E L M 0 p X S T R
8 8 A E E L M 0 p R S T X Szare elementy
9 9 A E E L M 0 p R s T X znajdują się na
10 10 A E E L M 0 p R s T X ostatecznej pozycji
A E E L M O p R s T X
Ślad d z ia ła n ia s o rto w a n ia p rz e z w y b ie ra n ie (z a w a rto ść ta b lic y p o k a ż d y m p rz e sta w ie n iu )
262 RO ZD ZIA Ł 2 o Sortowanie
Sortowanie przez wstawianie Algorytm często stosowany do sortowania kart
w czasie gry w brydża polega na sprawdzaniu kolejnych kart i umieszczaniu ich w od
powiednim miejscu wśród wcześniej ułożonych (przy zachowaniu uporządkowania
w tej grupie). W implementacji komputerowej trzeba zrobić miejsce na wstawienie
bieżącego elementu, przenosząc większe elementy o jedno miejsce w prawo przed
wstawieniem danego na wolną pozycję, a l g o r y t m 2 .2 to implementacja tej techniki,
nazywanej sortowaniem przez wstawianie.
Tu, podobnie jak w sortowaniu przez wybieranie, elementy na lewo od bieżącego
indeksu są posortowane, jednak nie znajdują się na ostatecznej pozycji, ponieważ
konieczne może być ich przeniesienie w celu zrobienia miejsca na mniejsze, później
napotkane elementy. Jednak po dojściu indeksu do prawego końca tablica jest w peł
ni posortowana.
Czas wykonania sortowania przez wstawianie zależy od początkowego układu
elementów w danych wejściowych (inaczej niż w sortowaniu przez wybieranie).
Przykładowo, jeśli tablica jest duża, a elementy są już uporządkowane (lub prawie
posortowane), sortowanie jest dużo szybsze niż dla elementów rozmieszczonych
losowo albo w odwrotnej kolejności.
Twierdzenie B. Sortowanie przez wstawianie wymaga średnio ~N 2/4 porównań
i -ATM przestawień dla losowo uporządkowanej tablicy o długości N i niepowta
rzalnych kluczach. W najgorszym przypadku potrzeba - N 2/ 2 porównań i ~ W i2
przestawień, a w najlepszym przypadku jest to N - 1 porównań i 0 przestawień.
Dowód. Podobnie jak w t w i e r d z e n i u a , tak i tu liczbę porównań i przestawień
łatwo jest zwizualizować w tabeli o wymiarach N n a N używanej do ilustrowania
sortowania. Należy policzyć elementy pod przekątną. W najgorszym przypadku
należy uwzględnić wszystkie elementy, a w najlepszym zbiór nie obejmuje żad
nego elementu. Dla losowo uporządkowanych tablic można oczekiwać, że każdy
element trzeba średnio przesunąć o mniej więcej połowę, dlatego uwzględniamy
połowę elementów pod przekątną.
Liczba porównań to liczba przestawień plus dodatkowa wartość równa N
minus liczba sytuacji, w których wstawiany element jest najmniejszy spośród
dotychczas znalezionych. W najgorszym przypadku (tablica w odwrotnej kolej
ności) wartość ta jest nieistotna w stosunku do łącznej liczby porównań. W naj
lepszym przypadku (tablica posortowana) porównań jest N - 1.
Sortowanie przez wstawianie działa dobrze dla pewnego rodzaju nielosowych tablic, któ
re często powstają w praktyce (nawet jeśli tablice są bardzo duże). Rozważmy na przykład,
co się stanie po zastosowaniu sortowania przez wstawianie dla już posortowanej tablicy.
Algorytm natychmiast stwierdzi, że każdy element znajduje się we właściwym miejscu
tablicy, a łączny czas wykonania rośnie liniowo (czas wykonania sortowania przez wy
bieranie dla takich tablic jest kwadratowy). To samo dotyczy tablic, w których wszystkie
klucze są równe (stąd warunek niepowtarzalności kluczy w t w i e r d z e n i u b ).
2.1 Podstawowe metody sortowania 263
ALGORYTM 2.2. Sortowanie przez wstawianie
p ublic c la s s In se rtio n
{
public s t a t ic void sort(Comparable[] a)
{ // Sortowanie a[] w porządku rosnącym,
in t N = [Link];
f o r (in t i = 1; i < N; i++)
{ // Wstawianie a [ i] między a [ i - l ] , a [ i - 2 ] , a [ i -3] itd.
fo r (in t j = i ; j > 0 && l e s s ( a [ j ] , a [ j - l ] ); j — )
exch(a, j, j - l ) ;
}
}
// Metody l e s s ( ) , exch(), isSorted() i main () przedstawiono na stronie 257.
Dla każdego i z przedziału od 0 do N-l należy przestawić a [i] z mniejszymi elementami
z przedziału od a [0] do a [i -1]. Przy przesuwaniu indeksu i od lewej do prawej elementy
po lewej stronie są posortowane, dlatego po dotarciu i do prawego końca tablica jest posor
towana.
a[]
i j 0 1 2 3 4 5 6 7 8 9 10
S 0 R T E X A M P L E Szare elementy
1 0 0 s R T E X A M P L E pozostają w miejscu
2 1 0 R S T E X A M P L E
3 3 0 R S T E X A M P L E
Czerwony element
4 0 E 0 R S T X A M P L E foa[j]
5 5 E 0 R S T X A M P L E
6 0 A E 0 R S T X M P L E
7 2 A E M 0 R s T X P L E Czarne elementy
należy przenieść
8 4 A E M 0 P R S T X L E
o jedno miejsce w prawo
9 2 A E L M 0 P R S T X E przy wstawianiu
10 2 A E E L M 0 P R S T X
A E E L M 0 P R s T X
Ślad działania sortowania przez wstawianie (zawartość tablicy po każdym wstawianiu)
RO ZD ZIA Ł 2 ■ Sortowanie
Rozważmy bardziej ogólne zagadnienie, związane z częściowo posortowanymi tabli
cami. Inwersja to para elementów tablicy uporządkowanych w niewłaściwy sposób.
W słowie E X A M P L E występuje 11 inwersji: E-A, X-A, X-M, X-P, X-L, X-E, M-L, M-E,
P-L, P-E i L-E. Jeśli liczba inwersji w tablicy jest mniejsza niż pewna stała wielokrot
ność wielkości tablicy, mówimy, że tablica jest częściowo posortowana. Oto typowe
przykłady częściowo posortowanych tablic:
■ Tablica, w której każdy element znajduje się niedaleko ostatecznej pozycji.
■ Krótka tablica dołączona do długiej posortowanej tablicy.
■ Tablica, w której niewielka liczba elementów znajduje się nie na swoim miejscu.
Sortowanie przez wstawianie (w przeciwieństwie do sortowania przez wybieranie)
jest wydajną metodą dla takich tablic. Jeśli liczba inwersji jest niska, sortowanie przez
wstawianie jest często szybsze niż jakakolwiek inna m etoda sortowania omówiona
w rozdziale.
Twierdzenie C. Liczba przestawień w sortowaniu przez wstawianie jest równa
liczbie inwersji w tablicy, a liczba porównań wynosi przynajmniej liczbę inwersji,
a najwyżej liczbę inwersji plus wielkość tablicy minus 1 .
Dowód. Każde przestawienie dotyczy dwóch przyległych elementów ustawio
nych w złej kolejności, a tym samym zmniejsza liczbę inwersji o jeden, a tablica
jest posortowana, kiedy liczba inwersji dochodzi do zera. Każde przestawienie
wymaga porównania. Ponadto mogą mieć miejsce dodatkowe porównania dla
każdej wartości i z przedziału od 1 do N-l (jeśli a [ i] nie dociera do lewego
końca tablicy).
Można łatwo znacznie przyspieszyć sortowanie przez wstawianie, skracając we
wnętrzną pętlę tak, aby przenosiła większe elementy o jedną pozycję w prawo, za
miast wykonywać pełne przestawianie (pozwala to zmniejszyć liczbę dostępów do
tablicy o połowę). Wprowadzenie tego usprawnienia pozostawiamy jako ćwiczenie
(zobacz ć w i c z e n i e 2 . 1 .2 5 ).
— sortowanie przez wstawianie to doskonała m etoda dla częściowo
p o d s u m o w a n ie
posortowanych tablic. Jest też dobrą techniką dla krótkich tablic. Ma to znaczenie
nie tylko z uwagi na to, że takie tablice często występują w praktyce, ale też dlatego,
iż tablice obu rodzajów powstają na etapach pośrednich w zaawansowanych algo
rytm ach sortujących. Dlatego sortowanie przez wstawianie omówiono ponownie
w kontekście takich algorytmów.
2.1 o Podstawowe metody sortowania 265
W iz u a liz a c ja d z ia ła n ia a lg o r y t m ó w s o r tu ją c y c h W tym rozdziale używa
my prostej reprezentacji wizualnej do opisywania algorytmów sortujących. Zamiast
śledzić postępy sortowania za pomocą
wartości kluczy, na przyldad liter, liczb [Link] ll ■■■11lol |l | | | | |
lub słów, używamy pionowych słupków
■I [Link]
sortowanych według wysokości. Zaletą
takiej reprezentacji jest to, że pozwala Dl .nillllllillll
zrozumieć działanie metody. i “ r nillllllillll
Po prawej stronie, w wizualnych śla ml .■[Link]
dach działania, od razu widać, że w sor ml ilIlDll UlD ll
towaniu przez wstawianie elementy na
ll dI I bOddoibD 00
prawo od indeksu nie są uwzględniane,
natomiast w sortowaniu przez wybiera I l n u m i II
nie nie są sprawdzane elementy na lewo m liii lililim l II
od indeksu. Ponadto wyraźnie widać, że illl l ll l i i l l BO
sortowanie przez wstawianie nie wyma ■ ■m m iii 111:110001
ga przenoszenia elementów mniejszych
III lIlDlIlll
od wstawianego i wykonuje średnio
około połowy porównań potrzebnych ollllllllll IllIlDlI
w sortowaniu przez wybieranie. ..iimiil lllliill
Za pom ocą opracowanej przez nas O lllll Czerne elementy Olllll
sq porów nyw ane
biblioteki StdDraw tworzenie wizualne 111111111 DIlDB
go śladu nie jest trudniejsze od genero
11111III! Illl
wania zwykłego śladu. Należy posorto
wać wartości typu Doubl e, dopracować im m i 1 101
algorytm tak, aby wywoływał metodę Olllll ll
show() w odpowiedni sposób (tak jak n illllllillll ....■■nillllllillll
dla standardowego śladu), i opracować S ortow anie przez w staw ianie Sortow anie przez w ybieranie
wersję m etody show(), żeby korzysta
W izu aln y śla d d z ia ła n ia p o d s ta w o w y c h a lg o ry tm ó w s o rtu ją c y c h
ła z biblioteki StdDraw do rysowania
słupków, zamiast wyświetlać wyniki.
Najbardziej skomplikowanym zadaniem jest określenie skali dla osi y tak, aby kolejne
rysunki pojawiły się w oczekiwanej kolejności. Zachęcamy do wykonania ć w i c z e
n i a 2 . 1 . 1 8 . Pozwoli to docenić wartość wizualnego śladu i ułatwi jego tworzenie.
Jeszcze łatwiejszym zadaniem jest utworzenie animacji na podstawie śladu dzia
łania, co pozwoli zobaczyć dynamiczne sortowanie tablicy. Animacja oparta jest na
procesie opisanym w poprzednim akapicie, jednak nie trzeba tu martwić się o oś y
(wystarczy za każdym razem wyczyścić zawartość okna i ponownie wyświetlić słupki).
Choć nie m ożna tego pokazać na kartach książki, animowane reprezentacje także
pomagają zrozumieć działanie algorytmów. Zachęcamy do wykonania ć w i c z e n i a
2 . 1 . 1 7 , co pozwoli się o tym przekonać.
266 RO ZD ZIA Ł 2 a Sortowanie
Porównywanie dwóch algorytmów sortujących Mamy już dwie imple
mentacje i oczywiście ciekawe jest, która z nich jest szybsza — sortowanie przez wy
bieranie ( a l g o r y t m 2 .1 ) czy sortowanie przez wstawianie ( a l g o r y t m 2 .2 ). Pytania
tego rodzaju pojawiają się wielokrotnie w czasie badań algorytmów i są głównym
tematem tej książki. Pewne podstawowe kwestie omówiono w r o z d z i a l e i . , jednak
ten pierwszy przykład wykorzystamy do przedstawienia podstawowego podejścia do
udzielania odpowiedzi na podobne pytania. Ogólnie, stosując podejście wprowadzo
ne w p o d r o z d z i a l e 1 .4 , porównujemy algorytmy przez:
■ ich zaimplementowanie i zdiagnozowanie,
■ przeanalizowanie podstawowych cech,
■ sformułowanie hipotez na tem at względnej wydajności,
■ przeprowadzenie eksperymentów w celu sprawdzenia hipotez.
Kroki te są ni mniej, nie więcej jak sprawdzoną metodą naukową zastosowaną do
badania algorytmów.
W tym kontekście a l g o r y t m 2 . 1 i a l g o r y t m 2.2 dotyczą pierwszego kroku.
t w i e r d z e n i a a , b i c stanowią drugi krok. c e c h a d ze strony 2 6 7 to krok trzeci,
a klasa SortCompare ze strony 2 6 8 umożliwia wykonanie czwartego kroku. Wszystkie
etapy są ze sobą powiązane.
Krótkie opisy powodują, że nie widać dużej ilości pracy potrzebnej do popraw
nego zaimplementowania, przeanalizowania i przetestowania algorytmów. Każdy
programista wie, że kod jest efektem długiego diagnozowania i usprawniania; każdy
matematyk zdaje sobie sprawę, iż poprawne analizy bywają bardzo skomplikowane;
każdy naukowiec wie, że formułowanie hipotez oraz projektowanie i wykonywanie
eksperymentów w celu ich sprawdzenia wymaga olbrzymiej staranności. Kompletne
opracowanie wyników pozostawiamy ekspertom badającym najważniejsze algoryt
my, jednak każdy programista stosujący algorytm powinien znać naukowy kontekst,
który pozwolił ustalić cechy algorytmu w obszarze wydajności.
Po opracowaniu implementacji następny krok polega na ustaleniu odpowiedniego
modelu danych wejściowych. Dla sortowania naturalnym modelem, który wykorzy
stano w t w i e r d z e n i a c h a , b i c , jest uznanie, że tablice są losowo uporządkowane
oraz że wartości kluczy są niepowtarzalne. W zastosowaniach, w których pojawia
się duża liczba kluczy o tej samej wartości, potrzebny jest bardziej skomplikowany
model.
Jak można sformułować hipotezę dotyczącą czasu wykonania sortowania przez
wstawianie i wybieranie dla losowo uporządkowanych tablic? Z analizy a l g o r y t m ó w
2 . 1 i 2 .2 oraz t w i e r d z e ń a i b bezpośrednio wynika, że dla losowych danych czas
wykonania obu algorytmów powinien być kwadratowy. Oznacza to, że czas sortowa
nia przez wstawianie jest proporcjonalny do małej stałej razy N 2, a sortowania przez
wybieranie — do innej małej stałej razy N 2. Wartości obu stałych zależą od kosztów
porównań i przestawień na danym komputerze. Dla wielu typów danych i standar
dowych komputerów sensowne jest założenie, że koszty te są zbliżone (choć istnieje
kilka ważnych wyjątków). Bezpośrednio wynikają z tego następujące hipotezy.
2.1 ■ Podstawowe metody sortowania 267
Cecha D. Dla losowo uporządkowanych tablic niepowtarzalnych wartości czas
sortowania przez wstawianie i sortowania przez wybieranie jest kwadratowy,
a szybkość tych algorytmów różni się o niewielką stałą.
Dowód. Stwierdzenie to przez ostatnie pół wieku potwierdzono na wielu kom
puterach. Sortowanie przez wstawianie było około dwukrotnie szybsze od sorto
wania przez wybieranie w czasie pisania pierwszego wydania tej książki (w roku
1980) i nadal tak jest, choć wtedy posortowanie 100 000 elementów za pomocą
tych algorytmów zajmowało kilka godzin, a obecnie dzieje się to w kilka sekund.
Czy na Twoim komputerze sortowanie przez wstawianie jest nieco szybsze od
sortowania przez wybieranie? Aby to sprawdzić, możesz użyć klasy SortCompare
z następnej strony. W klasie używana jest m etoda s o r t ( ) z klas o nazwach po
danych jako argumenty wiersza poleceń do wykonania określonej liczby ekspe
rymentów (sortowania tablic o danym rozmiarze). Program wyświetla stosunek
odnotowanych czasów wykonania algorytmów.
Aby sprawdzić hipotezę, przeprowadzono eksperymenty za pom ocą klasy SortCompare
(zobacz stronę 268). Jak zwykle do ustalenia czasu wykonania służy klasa Stopwatch.
Pokazana tu implementacja m etody tim e() działa dla podstawowych technik sorto
wania opisanych w rozdziale. Metoda timeRandomInput() z klasy SortCompare dzia
ła zgodnie z modelem losowo uporządkowanych danych wejściowych — generuje
losowe wartości typu Double, sortuje je i zwraca łączny czas sortowania dla okre
ślonej liczby prób. Wykorzystanie losowych wartości typu Double z przedziału od
0.0 do 1.0 jest dużo prostsze niż
użycie funkcji bibliotecznej w ro p ub lic s t a t i c double t im e (S tr in g a lg , Comparable!] a)
dzaju [Link](). Jest to {
Stopwatch tim er = new StopwatchQ;
też skuteczne podejście, ponieważ i f (a lg .e q u als("In se rtio n ")) In s e r tio n .s o r t (a );
wystąpienie kluczy o równej war i f (alg .eq uals("Se lec tion")) Se le c tio n .so rt(a );
tości jest bardzo mało prawdopo i f ([Link]("She!1")) She!1 . s o r t ( a ) ;
i f ( a lg . e q u a l s ( "N e r g e ") ) [Link](a);
dobne (zobacz ć w i c z e n i e 2 .5 .3 1 ).
i f ( a lg . e q u a l s ( " Q u i c k " ) ) Q [Link](a);
Jak opisano to w r o z d z i a l e 1 ., i f (a lg .e q u a l s ( "H e a p ")) H e a p .s o r t (a );
liczba prób jest pobierana jako ar return tim er. ela psedTim e();
gument, co pozwala wykorzystać
prawo wielkich liczb (im więcej
r
n
‘ 1 Pomiar czasu pracy jednego z algorytmów sortujących
prób, tym podzielenie łącznego Z tego rozdziału dla określonych danych
czasu pracy przez liczbę powtó
rzeń daje dokładniejsze szacunki rzeczywistego średniego czasu wykonania) i zni
welować efekty obciążenia systemu. Zachęcamy do eksperymentów z programem
SortCompare na własnym komputerze. Pomaga to poznać stopień, w jakim wnioski
na temat sortowania przez wstawianie i wybieranie są prawdziwe.
268 RO ZD ZIA Ł 2 Sortowanie
Porównywanie dwóch algorytmów sortujących
public c la s s SortCompare
{
public s t a t ic double tim e (Strin g alg, Doublet] a)
{ /* Zobacz te kst. */ }
public s t a t ic double timeRandomInput(String alg, in t N, in t T)
{ // Użycie algorytmu alg do posortowania T losowych t a b l ic
// o długości N.
double total = 0.0;
Doublet] a = new Double[N];
f o r (in t t = 0; t < T; t++)
{ // Przeprowadzenie jednego eksperymentu (generowanie
// i sortowanie t a b l ic y ) ,
fo r (in t i = 0; i < N; i++)
a [i ] = [Link] ;
total += time(alg, a ) ;
}
return t o t a l ;
}
public s t a t i c void m ain(String[] args)
{
S t r in g a l g l = a r g s [ 0 ] ;
S t r in g alg2 = a r g s [ l ] ;
in t N = I n t e g e r . p a r s e ln t ( a r g s [ 2 ] ) ;
in t T = I n t e g e r . p a r s e ln t ( a r g s [ 3 ] ) ;
double t l = timeRandomInput(algl, N, T ) ; // Suma dla a lg l.
double t2 = timeRandomInput(alg2, N, T ) ; // Suma dla alg2.
S t d O u t .p r in t f("Dla %d losowych wartości Double\n technika %s j e s t " ,
N, a l g l ) ;
S t d O u t .p r in t f (" % . l f razy szybsza od % s\n ", t 2 / t l, alg 2);
}
}
Ten klient uruchamia dwie techniki sortowania (ich nazwy podano w pierwszych dwóch ar
gumentach wiersza poleceń) dla tablicy zawierającej N (trzeci argument) losowych wartości
typu Double z przedziału od 0.0 do 1.0, ponawia eksperyment T razy (czwarty argument
wiersza poleceń), a następnie wyświetla stosunek łącznych czasów działania.
% java SortCompare In s e r t i o n S e le c tio n 1000 100
Dla 1000 losowych wartości Double
technika I n s e r t i o n j e s t 1.7 razy szybsza od Se le ction
2.1 n Podstawowe metody sortowania 269
cech a d celowo jest nieco niejasna (wartość małej stałej jest nieokreślona, a ponadto
nie ma założenia o podobnych kosztach porównań i przestawień), dlatego okazuje
się prawdziwa w wielu sytuacjach. Kiedy to możliwe, kluczowe aspekty wydajności
każdego z analizowanych algorytmów staramy się ująć w stwierdzeniach tego ro
dzaju. Jak opisano to w r o z d z i a l e i ., każda omawiana Cecha wymaga naukowego
przetestowania w danej sytuacji, czasem z wykorzystaniem bardziej dopracowanych
hipotez opartych na powiązanym Twierdzeniu (matematycznej prawdzie).
W kontekście praktycznych zastosowań jest jeszcze jeden kluczowy krok —
przeprowadzenie eksperymentów w celu walidacji hipotez dla używanych danych.
Omawianie tego etapu odkładamy do p o d r o z d z i a ł u 2.5 i ćwiczeń. Jeśli w omawia
nym przykładzie klucze sortujące nie są unikatowe i (lub) losowo uporządkowane,
c e c h a d może nie być prawdziwa. Tablicę m ożna losowo uporządkować za pomocą
metody [Link](), jednak aplikacje z dużą liczbą równych kluczy wyma
gają dokładnych analiz.
Omówienie analiz algorytmów ma stanowić punkt wyjścia — nie mają to być osta
teczne wnioski. Jeśli zainteresują Cię inne kwestie dotyczące wydajności algorytmów,
możesz je zbadać za pom ocą narzędzia w rodzaju SortCompare. Ćwiczenia dają wiele
okazji do przeprowadzenia takich badań.
n i e z a g ł ę b i a m y się bardziej w porównywanie wydajności sortowania przez wsta
wianie i wybieranie, ponieważ o wiele bardziej interesują nas algorytmy działające
od nich setki, tysiące, a nawet miliony razy szybciej. Jest jednak kilka powodów, dla
których warto zrozumieć podstawowe algorytmy. Algorytmy te:
D Pomagają poznać podstawowe zasady.
■ Zapewniają punkt odniesienia w obszarze wydajności.
n Są stosowane w pewnych specjalnych sytuacjach.
■ Mogą być podstawą do rozwijania lepszych algorytmów.
Z tych powodów stosujemy to samo podejście i rozważamy podstawowe algorytmy
dla każdego problem u omawianego w książce — nie tylko do sortowania. Programy
w rodzaju SortCompare odgrywają kluczową rolę w technice stopniowego rozwija
nia algorytmów. Na każdym etapie m ożna użyć takiego program u do ocenienia,
czy nowy algorytm lub usprawniona wersja znanego zapewnia oczekiwane zyski
wydajności.
270 RO ZD ZIA Ł 2 ■ Sortowanie
Sortowanie Shella Aby pokazać znaczenie znajomości podstawowych metod
sortowania, omawiamy szybki algorytm oparty na sortowaniu przez wstawianie.
Sortowanie przez wstawianie jest wolne dla dużych nieuporządkowanych tablic,
ponieważ jedyne przestawienia dotyczą tu przyległych elementów, dlatego wartości
można przenosić w tablicy tylko po jednym miejscu. Jeśli element o najmniejszym
kluczu znajduje się na końcu tablicy, potrzeba N - 1 przestawień, aby umieścić go na
docelowej pozycji. Sortowanie Shella to proste rozwinięcie sortowania przez wstawia
nie. Przyspieszenie działania wynika tu z możliwości przestawiania oddalonych ele
mentów tablicy. Prowadzi to do powstawania częściowo posortowanych tablic, które
m ożna ostatecznie wydajnie posortować za pomocą sortowania przez wstawianie.
Pomysł polega na uporządkowaniu tablicy w taki sposób, aby co h-te elementy (roz
poczynając od dowolnego miejsca) były posortowanymi podciągami. Mówimy, że taka
h_ 4 tablicajestpo h-sortowaniu. Ujmijmy
l e e a m h l e p s o l t s x r to inaczej — tablica po /i-sortowa-
L---------------M--------------— p -------------- T niu to h niezależnie posortowanych
E----------------H----------------s -----------— s i wymieszanych ze sobą podciągów.
E L 0 x Przeprowadzając /i-sortowanie dla
A E L R dużych wartości h można przenosić
Ciąg po h-sortowaniu to h wymieszanych posortowanych podciągów elementy tablicy na duże odległości,
co ułatwia h-sortowanie dla mniej
szych wartości h. Zastosowanie takiej procedury dla dowolnego ciągu wartości h koń
czącego się wartością 1 daje posortowaną tablicę. Tak działa sortowanie Shella. W imple
mentacji w a l g o r y t m i e 2 .3 , pokazanym na następnej stronie, użyto ciągu malejących
wartości Vi(3k - 1). Rozpoczęto od największego przyrostu mniejszego od N /3, po czym
jest on zmniejszany o 1. Taki ciąg nazywany jest cięgiem odstępów, a l g o r y t m 2.3 sam
oblicza ciąg odstępów. Inna możliwość to zapisanie takiego ciągu w tablicy.
Jednym ze sposobów na zaimplementowanie sortowania Shella jest użycie — dla
każdego h — sortowania przez wstawianie niezależnie dla każdego z h podciągów.
Ponieważ podciągi są niezależne, można użyć jeszcze prostszego podejścia. Przy h-
sortowaniu tablicy wystarczy wstawić każdy element między poprzednie w podciągu
dla danego h, przestawiając go z elementami o wyższych kluczach (przenosząc te
ostatnie o jedną pozycję w prawo w podciągu). Do wykonania tego zadania używamy
kodu sortowania przez wstawianie, zmodyfikowanego tak, aby dekrementacja wyno
siła h zamiast 1 przy poruszaniu się po tablicy. Ta obserwacja pozwala zredukować
implementację sortowania Shella do procesu podobnego do sortowania przez wsta
wianie dla każdego odstępu.
Sortowanie Shella zapewnia wydajność przez równoważenie rozmiaru i częściowego
uporządkowania (w podciągach). Początkowo podciągi są krótkie. Na dalszych etapach
podciągi są częściowo posortowane. W obu sytuacjach uruchamiane jest sortowanie
przez wstawianie. Stopień częściowego posortowania podciągów jest zmienny i zależy
w dużym stopniu od ciągu odstępów. Określenie wydajności sortowania Shella nie jest
proste, a l g o r y t m 2.3 to jedyna z omawianych tu metod sortowania, dla której nie
scharakteryzowano dokładnie wydajności dla losowo uporządkowanych tablic.
2.1 Podstawowe metody sortowania 271
ALGORYTM 2.3. Sortowanie Shella
public c la s s Shell
{
public s t a t i c void sort(Comparable[] a)
{ // Sortowanie a[] w kolejności rosnącej,
in t N = [Link];
in t h = 1;
while (h < N/3) h = 3*h + 1; // 1, 4, 13, 40, 121, 364, 1093, ...
while (h >= 1)
{ // h-sortowanie ta b lic y ,
fo r (in t i = h; i < N; i++)
{ // Wstawianie a [i ] między a [i - h ] ,a [ i - 2 * h ] , a [ i- 3 *h ] itd.
fo r (in t j = i ; j >= h && l e s s ( a [ j ] , a[j-h ] ) ; j -= h)
exch(a, j, j - h ) ;
}
h = h/3;
}
}
// Metody l e s s Q , exch(), isS o rte d () i main() opisano na stro n ie 257.
Oto zwięzła implementacja sortowania Shella. Należało zmodyfikować wstawianie przez
sortowanie ( a l g o r y t m 2 . 2 ) pod kątem fi-sortowania tablicy i dodać pętlę zewnętrzną do
zmniejszania wartości h w ciągu odstępów, który zaczyna się od stałej części tablicy, a kończy
wartością 1 .
% java SortCompare Sh ell I n s e r t i o n 100000 100
Dla 100000 losowych wartości Double
technika Sh ell j e s t 600 razy szybsza od I n s e r t i o n
D a n e wejś ciowe S H E L L S O R T E X A M P L E
13 -s ortow anie P H E L L S O R T E X A M s L E
4 -sorto w an ie L E E A M H L E P S O L T s X R
1-so rto w an ie A E E E H L L L M O P R S s T X
Ślad działania sortowania Shella (zawartość tablicy po każdym przejściu)
272 RO ZD ZIA Ł 2 a Sortowanie
Jak ustalić, który ciąg odstępów należy zastosować? Zwykle trudno jest odpowiedzieć
na to pytanie. Wydajność algorytmu zależy nie tylko od wartości odstępów, ale też
od arytmetycznych zależności między nimi, na przykład ich wspólnymi dzielnikami
i innymi cechami. Przebadano wiele różnych ciągów odstępów, jednak nie udowod-
niono, że któryś z nich jest najlep
Dane wejściowe S H E L L S 0 R T E X A M p L E
szy. Ciąg odstępów zastosowany
13-sortowanie P H E L L s 0 R T E X A Ms L E
w a l g o r y t m i e 2.3 jest łatwy do
P H E L L s 0 R T E X A Ms L E
obliczenia i w użyciu oraz zapew
P H E L L s 0 R T E X A Ms L E
4-sortowanie L H E L P s 0 R T E X A M s L E nia wydajność niemal tak wysoką,
L H E L P s 0 R T E X A M s L E jak bardziej zaawansowane ciągi
L H E L P s 0 R T E X A M s L E odstępów, dla których udowod
L H E L P s 0 R T E X A M s L E niono wyższą wydajność dla naj
L H E L P s 0 R T E X A M s L E gorszego przypadku. Możliwe, że
L E E L P H 0 R T S X A M s L E ciągi odstępów o znacząco wyż
L E E L P H 0 R T S X A M s L E szej wydajności wciąż czekają na
L E E A P H 0 L T S X R M s L E odkrycie.
L E E A MH 0 L P s X R T s L E Sortowanie Shella jest przydat
L E E A MH0 L P s X R T s L E
ne nawet dla dużych tablic, zwłasz
L E E A MH L L P s 0 R T s X E
cza w porównaniu z sortowaniem
L E E A MH L E P s 0 L T s X R
1 -sortowanie E L E A M H L E P s 0 L T s X R przez wybieranie i wstawianie.
E E L A M H L E P s 0 L T s X R Działa też dobrze dla dowolnie
A E E L M H L E P s 0 L T s X R (niekoniecznie losowo) uporząd
A E E L M H L E P s 0 L T s X R kowanych tablic. Utworzenie tab
A E E H L M L E P s 0 L T s X R licy, dla której sortowanie Shella
A E E H L L M E P s 0 L T s X R działa powoli dla określonego cią
A E E E H L L M P s 0 L T s X R gu odstępów, jest zwykle trudne.
A E E E H L L MP s 0 L T s X R Za pom ocą programu
A E E E H L L M P s 0 L T s X R SortCompare można się prze
A E E E H L L M 0 P s L T s X R konać, że sortowanie Shella jest
A E E E H L. L L M 0 p S T s X R
znacznie szybsze od sortowania
A E E E H L L L M0 p S T s X R
przez wstawianie lub wybieranie,
A E E E H L L L M0 p S S T X R
A E E E H L L L M 0 p S s T X R a przewaga szybkości rośnie wraz
A E E E H L L L M 0 p R s s T X z rozmiarem tablicy. Przed dalszą
Wynik A E E E H L L L M0 p R s s T X lekturą zastosuj na swoim kom
puterze program SortCompare do
Szczegółowy ślad działania sortowania Shella (wstawianie)
porównania sortowania Shella
z sortowaniem przez wstawianie
i wybieranie dla tablic o rozmiarach będących potęgami dwójki (zobacz ć w i c z e n i e
2 .1 . 2 7 ). Przekonasz się, że sortowanie Shella umożliwia rozwiązanie problemów,
z którymi nie radzą sobie prostsze algorytmy. Ten przykład to pierwsza praktycz-
2.1 a Podstawowe metody sortowania 273
Dane wejściowe
W izu aln y śla d d z ia ła n ia s o rto w a n ia S hella
na ilustracja ważnej zasady pojawiającej się na kartach książki — osiągnięcie zysków
w szybkości umożliwiających rozwiązanie problemów, z którymi nie można poradzić
sobie w inny sposób, jest jedną z głównych przyczyn prowadzenia badań nad wydajnoś
cią i projektowaniem algorytmów.
Zbadanie cech z obszaru wydajności sortowania Shella wymaga matematycznych
analiz wykraczających poza zakres tej książki. Jeśli chcesz się o tym przekonać, zasta
nów się nad tym, jak udow odnić następujący fakt — tablica posortowana według
h-sortowania pozostaje taka po k-sortowaniu. Jeśli chodzi o wydajność a l g o r y t m u 2 .3 ,
najważniejsza jest tu wiedza o tym, że czas wykonania sortowania Shella nie musi być
kwadratowy. W iadomo na przykład, że dla najgorszego przypadku liczba porównań
w a l g o r y t m i e 2.3 jest proporcjonalna do N312. To, że prosta modyfikacja pozwa
la złamać barierę kwadratowego czasu wykonania, jest ciekawym spostrzeżeniem,
zwłaszcza że uzyskanie tego efektu jest głównym celem w wielu problemach z obsza
ru projektowania algorytmów.
274 RO ZD ZIA Ł 2 □ Sortowanie
Nie istnieją matematyczne dane dotyczące średniej liczby porównań w sortowaniu
Shella dla losowo uporządkowanych danych wejściowych. Opracowano ciągi odstę
pów, które pozwalają zmniejszyć asymptotyczny wzrost liczby porównań dla najgor
szego przypadku do N4I}, N 514, N 615i tak dalej, jednak wiele z tych badań m a znaczenie
akademickie, ponieważ dla stosowanych w praktyce wartości N poszczególne funkcje
prawie nie różnią się od siebie (i od stałego czynnika N).
W praktyce m ożna bezpiecznie wykorzystać dawne badania naukowe nad sor
towaniem Shella, stosując ciąg odstępów z a l g o r y t m u 2.3 (lub jeden z ciągów od
stępów przedstawionych w ćwiczeniach w końcowej części podrozdziału; ciągi te
pozwalają zwiększyć wydajność o 20 - 40%). Ponadto m ożna łatwo przeprowadzić
walidację przedstawionych poniżej hipotez.
Cecha E. Liczba porównań w sortowaniu Shella o odstępach 1, 4, 13, 40, 121,
364 i tak dalej jest ograniczona przez mały m nożnik N razy liczba użytych od
stępów.
Dowód. Zmodyfikowanie a l g o r y t m u 2 .3 tak, aby zliczał porównania i dzielił
je przez liczbę odstępów, to proste ćwiczenie (zobacz ć w i c z e n i e 2 .1 .1 2 ). Według
rozbudowanych eksperymentów średnia liczba porównań na odstęp może wy
nosić N u5, jednak dość trudno jest określić tempo wzrostu tej funkcji dla nied
użych N. Cecha ta wydaje się dość mało zależna od modelu danych wejściowych.
czasem stosują sortowanie Shella, ponieważ zapewnia
d o ś w ia d c z e n i p r o g r a m iś c i
akceptowalny czas wykonania nawet dla stosunkowo dużych tablic, wymaga małej
ilości kodu i nie zajmuje dodatkowej pamięci. W kilku następnych podrozdziałach
opisano metody, które są wydajniejsze, ale — za wyjątkiem bardzo dużych N — tylko
dwukrotnie (lub nawet mniej), a ponadto są bardziej skomplikowane. Jeśli potrzebu
jesz m etody sortowania, a sortowanie systemowe jest niedostępne (kod ma działać na
przykład na sprzęcie lub w systemie zagnieżdżonym), możesz swobodnie zastosować
sortowanie Shella, a później ustalić, czy warto zastąpić je bardziej zaawansowanym
rozwiązaniem.
2.1 s Podstawowe metody sortowania 275
P y ta n ia i o d p o w ie d z i
P. Sortowanie wydaje się sztucznym problemem. Czy nie istnieje wiele innych, dużo
ciekawszych zadań wykonywanych za pomocą komputerów?
O. Możliwe, jednak liczne z tych ciekawych operacji są możliwe dzięki szybkim algo
rytm om sortowania. Wiele przykładów znajdziesz w p o d r o z d z i a l e 2.5 i w dalszych
fragmentach książki. Warto teraz zapoznać się z sortowaniem, ponieważ problem
ten jest łatwy do zrozumienia i pozwala docenić pomysłowość twórców szybszych
algorytmów.
P. Dlaczego istnieje tak wiele algorytmów sortowania?
O. Jednym z powodów jest to, że wydajność wielu algorytmów zależy od danych
wejściowych, dlatego poszczególne algorytmy mogą być odpowiednie dla różnych
zastosowań i określonych rodzajów danych. Przykładowo, sortowanie przez wstawia
nie jest m etodą wybieraną dla częściowo posortowanych lub krótkich tablic. Ważne
są też inne ograniczenia, takie jak pamięć i sposób traktowania równych kluczy.
Do tego pytania wracamy w p o d r o z d z i a l e 2 .5 .
P. Po co stosować krótkie metody pomocnicze w rodzaju 1ess () i exch () ?
O. Są to podstawowe abstrakcyjne operacje potrzebne w każdym algorytmie sor
towania, a kod jest bardziej zrozumiały dzięki zastosowaniu tych operacji. Ponadto
metody te pozwalają przenosić kod bezpośrednio do innych środowisk. Duża część
kodu a l g o r y t m ó w 2 . 1 i 2 .2 to kod prawidłowy także w kilku innych językach pro
gramowania. Nawet w Javie można wykorzystać ten kod jako podstawę do sortowa
nia typów prostych (bez interfejsu Comparable). Wystarczy zaimplementować m eto
dę less () za pom ocą kodu v < w.
P. Kiedy urucham iam program SortCompare, za każdym razem otrzymuję inne wy
niki (różne od tych z książki). Dlaczego tak się dzieje?
O. Zacznijmy od tego, że masz inny kom puter od używanego przez nas; dotyczy
to też systemu operacyjnego, środowiska Javy itd. Wszystkie te różnice mogą pro
wadzić do drobnych różnic w kodzie maszynowym odpowiadającym algorytmom.
Różnice między kolejnymi uruchomieniami mogą wynikać z działania różnych apli
kacji i wielu innych czynników. Przeprowadzenie bardzo dużej liczby prób powinno
zniwelować problem. Warto zauważyć, że małe różnice w wydajności algorytmów są
współcześnie trudne do zauważenia. Jest to główna przyczyna tego, że koncentrujemy
się na dużych różnicach!
276 RO ZD ZIA Ł 2 a Sortowanie
j ĆWICZENIA
2.1.1. Przedstaw (jako ślad działania kodu w stylu zastosowanym dla alg o ryt
mu 2 .i), jak przebiega porządkowanie tablicy E A S Y Q U E S T I 0 Nprzy sorto
waniu przez wybieranie.
2.1.2. Jaka jest maksymalna liczba przestawień elementu w czasie sortowania przez
wybieranie? Jaka jest średnia liczba przestawień elementu?
2.1.3. Podaj przykładową N-elementową tablicę, która prowadzi do maksymalnej
liczby udanych testów a [j] < a [min] (co prowadzi do aktualizacji wartości mi n)
w czasie sortowania przez wybieranie ( a l g o r y t m 2 . 1 ).
2.1.4. Przedstaw (jako ślad działania kodu w stylu zastosowanym dla a lg o ryt
mu 2 .2 ), jak przebiega porządkowanie tablicy E A S Y Q U E S T I 0 Nprzy sorto
waniu przez wstawianie.
2.1.5. Dla każdego z dwóch warunków z wewnętrznej pętli fo r sortowania przez
wstawianie (a l g o r y t m 2 . 2 ) opisz tablicę N elementów, dla której dany warunek jest
zawsze fałszywy po zakończeniu działania pętli.
2.1.6. Która metoda, sortowanie przez wybieranie czy sortowanie przez wstawianie,
działa szybciej dla tablicy, w której wszystkie klucze są takie same?
2.1.7. Która metoda, sortowanie przez wybieranie czy sortowanie przez wstawianie,
działa szybciej dla tablicy, w której elementy mają kolejność odwrotną względem do
celowej?
2.1.8. Załóżmy, że sortowanie przez wstawianie zastosowano dla losowo uporząd
kowanej tablicy, w której elementy przyjmują jedną z trzech wartości. Czy czas wy
konania jest liniowy, kwadratowy, czy pośredni?
2.1.9. Przedstaw(jakośladdziałaniakoduwstyluzastosowanymdlaALGORYTM U 2 .3 ),
jak przebiega porządkow anie tablicy E A S Y S N E L L S 0 R T Q U E S T I 0 N
przy sortow aniu Shella.
2.1.10. Dlaczego nie stosuje się sortowania przez wybieranie przy /;-sortowaniu
w sortowaniu Shella?
2.1.11. Zaimplementuj wersję sortowania Shella, która przechowuje ciąg odstępów
w tablicy, zamiast go obliczać.
2.1.12. Zmodyfikuj sortowanie Shella tak, aby dla każdego odstępu wyświetla
ło liczbę porównań podzieloną przez rozmiar tablicy. Napisz klienta testowego do
sprawdzania hipotezy, wedle której liczba ta jest niewielką stałą. Klient ma sortować
tablice losowych wartości typu Doubl e. Tablice mają mieć rozmiary będące potęgami
10 (zacznij od długości 100 ).
2.1 a Podstawowe metody sortowania
PROBLEMY DO ROZWIĄZANIA
2.1.13. Sortowanie talii kart. Wyjaśnij, jaką metodą uporządkowałbyś talię kart
według kolorów (w kolejności piki, kiery, trefle, kara) i według wartości kart w ra
mach każdego koloru. Uwzględnij następujące warunki — karty są ułożone w rzę
dzie przednią częścią do dołu, a jedyne dozwolone operacje to sprawdzenie wartości
dwóch kart i ich przestawienie (obróconych przednią częścią do dołu).
2.1.14. Sortowanie struktury dequeue. Wyjaśnij, jak posortowałbyś talię kart, jeśli
jedyne dozwolone operacje to sprawdzanie wartości dwóch pierwszych kart, przed
stawianie dwóch pierwszych kart i przenoszenie pierwszej karty na koniec talii.
2.1.15. Kosztowne przestawienia. Pracownik firmy spedycyjnej ma za zadanie zmienić
uporządkowanie dużej liczby skrzyń według czasu ich wysyłki. Koszty porównań są tu
więc bardzo niskie (wystarczy sprawdzić nalepić) w porównaniu z kosztem przestawień
(trzeba przenieść skrzynie). Magazyn jest prawie pełny. Dostępne jest dodatkowe miej
sce na tylko jedną skrzynię. Jaką metodę sortowania powinien zastosować pracownik?
2.1.16. Sprawdzanie poprawności. Napisz metodę check(), która wywołuje metodę
so rt () dla danej tablicy i zwraca t rue, jeśli m etoda so rt () sortuje tablicę oraz zacho
wuje w tablicy te same elementy, co początkowo. W przeciwnym razie check () ma
zwracać fal se. M etoda s o rt() może przestawiać dane nie tylko za pom ocą metody
exch (). Możesz użyć m etody Arrays .s o r t () i przyjąć, że działa poprawnie.
2.1.17. Animacja. Dodaj do Idas In s e rtio n i Sel ection kod, aby rysowały zawartość
tablicy w formie pionowych słupków, tak jak na wizualnych śladach z tego podroz
działu. Kod m a wyświetlać słupki po każdym przebiegu, co prowadzi do powstania
animacji kończącej się obrazem posortowanej tablicy, na którym słupki rozmieszczo
ne są według wysokości. Wskazówka: użyj klienta podobnego do tego z tekstu, gene
rującego losowe wartości typu Doubl e, wstaw w odpowiednich miejscach wywołania
show() w kodzie sortującym i zaimplementuj metodę show(), która czyści zawartość
obrazu i rysuje słupki.
2.1.18. Wizualny ślad. Zmodyfikuj rozwiązanie poprzedniego ćwiczenia tak, aby
klasy In s e rtio n i Selection tworzyły wizualne ślady, takie jak te pokazane w tym
podrozdziale. Wskazówka: przemyślane zastosowanie metody setY scale() pozwala
łatwo rozwiązać problem. Dodatkowe zadanie: dodaj kod potrzebny do utworzenia
czerwonych i szarych elementów, takich jak na rysunkach z podrozdziału.
2.1.19. Najgorszy przypadek dla sortowania Shella. Utwórz tablicę o 100 elemen
tach, zawierającą wartości od 1 do 100, dla której sortowanie Shella z odstępami 1 4
13 40 wymaga możliwie dużej liczby porównań.
2.1.20. Najlepszy przypadek dla sortowania Shella. Jaki jest najlepszy przypadek dla
sortowania Shella? Wyjaśnij odpowiedź.
278 RO ZD ZIA Ł 2 Q Sortowanie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
2.1.21. Transakcje z możliwością porównywania. Używając jako modelu kodu kla
sy Date (strona 259), rozwiń implementację klasy Transaction ( ć w i c z e n i e 1 .2 .1 3 )
o obsługę interfejsu Comparabl e, tak aby kolejność transakcji była wyznaczana przez
ich wartość.
Rozwiązanie:
public c la s s Transaction implements Comparable<Transaction>
{
p rivate final double amount;
public in t compareTo(Transaction that)
{
i f ([Link] > [Link]) return +1;
i f ([Link] < [Link]) return -1;
return 0;
}
}
2.1.22. Klient testowy do sortowania transakcji. Napisz klasę SortTransactions za
wierającą metodę statyczną mai n (), która wczytuje ciąg transakcji ze standardowego
wejścia, sortuje je i wyświetla wynik w standardowym wyjściu (zobacz ć w i c z e n i e
1-3-17)-
Rozwiązanie:
p ublic c la s s SortTransactions
{
public s t a t ic T ra n sa ctio n [] readTransactions()
( // Zobacz ćwiczenie 1.3.17. }
public s t a t i c void m ain(String[] args)
{
Transaction[] tra n sa ction s = re ad T ran saction s();
Shell . s o r t ( t r a n s a c t io n s ) ;
f o r (Transaction t : tran saction s)
S td O u t .p rin t ln (t );
}
}
2.1 o Podstawowe metody sortowania 279
EKSPERYMENTY
2.1.23. Sortowanie talii. Poproś kilku znajomych, aby posortowali talię kart (zobacz
ć w i c z e n i e 2 . 1 . 1 3 ). Obserwuj ich starannie i zapisz stosowane przez nich metody.
2.1.24. Sortowanie przez wstawianie z wartownikiem. Opracuj implementację sorto
wania przez wstawianie, w której nie występuje test j>0 w pętli wewnętrznej. W tym
celu najpierw umieść najmniejszy element na odpowiedniej pozycji. Użyj metody
SortCompare do sprawdzenia skuteczności rozwiązania. Uwaga: technika ta często
pozwala uniknąć sprawdzania wyjścia indeksu poza przedział. Element umożliwiają
cy uniknięcie testu to wartownik.
2.1.25. Sortowanie przez wstawianie bez przestawień. Opracuj implementację sorto
wania przez wstawianie, w której większe elementy przenoszone są w prawo o jedną
pozycję za pom ocą jednego dostępu do tablicy na element (a nie przy użyciu metody
exch ()). Użyj program u SortCompare do oceny skuteczności rozwiązania.
2.1.26. Typy proste. Opracuj wersję sortowania przez wstawianie, która sortuje tab
lice wartości typu i nt. Porównaj wydajność tej wersji i implementacji podanej w tek
ście (która sortuje wartości typu Integer oraz niejawnie stosuje autoboxing i autoun-
boxing do przekształcania danych).
2.1.27. Sortowanie Shella ma złożoność poniżej kwadratowej. Użyj programu
SortCompare do porównania na swoim komputerze sortowania Shella z sortowaniem
przez wstawianie i sortowaniem przez wybieranie. Użyj tablic o rozmiarach będących
potęgami dwójki (zacznij od długości 128).
2.1.28. Równe klucze. Sformułuj i sprawdź hipotezę dotyczącą czasu wykonania
sortowania przez wstawianie i sortowania przez wybieranie dla tablic, które zawiera
ją tylko dwie wartości klucza. Załóż, że wystąpienie każdej z obu wartości jest równie
prawdopodobne.
2.1.29. Odstępy w sortowaniu Shella. Przeprowadź eksperymenty, aby porównać
ciąg odstępów z a l g o r y t m u 2.3 z ciągiem 1 , 5 , 1 9 , 4 1 , 1 0 9 , 2 0 9 , 5 0 5 , 9 2 9 ,2 1 6 1 , 3 9 0 5 ,
8929, 16001, 3 6 2 8 9 , 6 4 7 6 9 , 1 4 6305, 2 6 0 6 0 9 (utworzonym przez złączenie ciągów
9 x 4 k - 9 x 2 k + 1 i 4 k - 3 x 2 k + 1 ). Zobacz ć w i c z e n i e 2 . 1 . 1 1 .
2.1.30. Odstępy geometryczne. Przeprowadź eksperymenty, aby ustalić wartość t
prowadzącą do najkrótszego czasu wykonania sortowania Shella dla losowych tablic
dla ciągu odstępów 1, Ld, Lf2_|, \_t3_I, I_i4j i tak dalej dla N = 106. Podaj wartości t i ciągi
odstępów dla trzech najlepszych znalezionych wartości.
280 RO ZD ZIA Ł 2 □ Sortowanie
EKSPERYMENTY (ciąg dalszy)
W dalszych ćwiczeniach opisano różne klienty pomocne w ocenie metod sortowania.
Programy te mają być punktem wyjścia do zrozumienia cech związanych z wydajnością
na podstawie losowych danych. We wszystkich programach użyj metody t im e () (takjak
w programie SortC om pare), co pozwala uzyskać dokładniejsze wyniki przez określenie
większej liczby prób w drugim argumencie wiersza poleceń. Do ćwiczeń tych wracamy
w dalszych podrozdziałach przy ocenianiu bardziej zaawansowanych metod.
Test podwajania. Napisz klienta, który wykonuje test podwajania dla algo
2 . 1 .3 1 .
rytmów sortowania. Zacznij od N równego 1000 i wyświetl N, prognozowaną liczbę
sekund, rzeczywistą liczbę sekund i stosunek czasu dla podwojonych wartości N. Użyj
tego program u do walidacji stwierdzenia, że sortowanie przez wstawianie i sortowa
nie przez wybieranie działają w czasie kwadratowym dla losowych danych wejścio
wych. Sformułuj i przetestuj hipotezę dla sortowania Shella.
Wykresy czasów wykonania. Napisz klienta, który używa biblioteki StdDraw
2 .1 .3 2 .
do rysowania wykresów czasów wykonania algorytmu dla losowych danych wejścio
wych i różnych rozmiarów tablicy. Możesz dodać jeden lub dwa argumenty wiersza
poleceń. Postaraj się zaprojektować przydatne narzędzie.
2 . 1 .3 3 . Rozkład. Napisz klienta, który wchodzi w nieskończoną pętlę i urucham ia
metodę s o rt() dla tablic o rozmiarze podanym jako trzeci argument wiersza pole
ceń, mierzy czas każdego wykonania metody i używa biblioteki StdDraw do rysowania
wykresu średnich czasów wykonania. Powinien powstać rozkład czasów wykonania.
2 . 1 .3 4 . Przypadki skrajne. Napisz klienta, który urucham ia metodę s o r t () dla tru d
nych lub „patologicznych” przypadków, które mogą wystąpić w praktycznych zasto
sowaniach. Oto kilka przykładów: już uporządkowane tablice, tablice o odwróconej
kolejności, tablice, w których wszystkie klucze mają tę samą wartość, tablice składa
jące się z tylko dwóch różnych wartości i tablice o wielkości 0 lub 1 .
Rozkłady nierównomierne. Napisz klienta, który generuje dane testowe, lo
2 . 1 .3 5 .
sowo porządkując obiekty za pom ocą rozkładów innych niż równomierny. Oto kilka
takich rozkładów:
° Gaussa,
° Poissona,
■ geometryczny,
■ dyskretny (w ć w i c z e n i u 2 .1.28 opisano specjalny przypadek).
Opracuj i przetestuj hipotezę dotyczącą wpływu takich danych wejściowych na wy
dajność algorytmów opisanych w podrozdziale.
2.1 □ Podstawowe metody sortowania 281
2.1.36. Dane nierównomierne. Napisz klienta generującego dane testowe, które nie
są równomierne. Oto przykłady:
° jedna połowa danych to zera, a druga — jedynki;
■ połowa danych to zera, połowa z reszty to jedynki, połowa pozostałych to dwój
ki i tak dalej;
D jedna połowa danych to zera, a druga — losowe wartości typu i nt.
Sformułuj i przetestuj hipotezy dotyczące wpływu takich danych wejściowych na wy
dajność algorytmów z tego podrozdziału.
2.1.37. Częściowo posortowane. Napisz klienta, który generuje częściowo posorto
wane tablice, takie jak:
0 posortowana w 95% z losowymi wartościami w ostatnich 5%;
0 z wszystkimi elementami znajdującymi się nie dalej niż 10 miejsc od ostatecz
nej lokalizacji;
° posortowana oprócz 5% elementów losowo rozrzuconych po tablicy.
Sformułuj i przetestuj hipotezę dotyczącą wpływu takich danych wejściowych na wy
dajność algorytmów opisanych w tym podrozdziale.
2.1.38. Różne typy elementów. Napisz klienta, który generuje tablice elementów róż
nych typów o losowych wartościach kluczy. Przykładowe typy mogą obejmować:
° klucz typu S tri ng (o przynajmniej 10 znakach) i jedną wartość typu doubl e;
° klucz typu doubl e i 10 wartości typu S tri ng (o przynajmniej 10 znakach);
° klucz typu i nt i jedną wartość typu i nt [ 20].
Sformułuj i przetestuj hipotezę na tem at wpływu takich danych wejściowych na wy
dajność algorytmów z tego podrozdziału.
2.2. S O R T O W A N IE P R Z E Z S C A L A N IE
a lg o r ytm y omawiane w tym podrozdziale są oparte na prostej operacji — scala
niu, czyli łączeniu dwóch uporządkowanych tablic w jedną większą i uporządkowaną
tablicę. Operacja ta bezpośrednio prowadzi do powstania prostej rekurencyjnej m e
tody o nazwie sortowanie przez scalanie. Aby posortować tablicę, należy podzielić ją
na dwie połowy, rekurencyjnie posortować każdą z nich, a następnie scalić wyniki.
Jak się okaże, jedną z najatrakcyjniejszych cech sortowania przez scalanie jest to, że
gwarantuje posortowanie dowolnej tablicy N elementów w czasie proporcjonalnym
do N log N. Główną wadą tej techniki jest to, że wymaga dodatkowej pamięci w ilości
proporcjonalnej do N.
Dane wejściowe M E R G E S O R T E X A M P L E
Sortowanie lewej połowy E E G M O R R S T E X A M P L E
Sortowanie prawej połowy E E G M O R R S A E E L M P T X
Scalanie wyników A E E E E G L M M O P R R S T X
Przebieg sortowania przez scalanie
Abstrakcyjne scalanie w miejscu Prosty sposób na zaimplementowanie sca
lania polega na zaprojektowaniu metody, która scala dwie uporządkowane tablice
obiektów zgodnych z interfejsem Comparable w trzecią tablicę. Łatwo jest zaimple
mentować tę strategię. Należy utworzyć odpowiedniej wielkości tablicę wynikową,
a następnie wybierać kolejno najmniejszy pozostały element z dwóch tablic wejścio
wych jako następny dodawany do tablicy wynikowej.
Jednak przy sortowaniu przez scalanie dużej tablicy potrzebnych jest wiele opera
cji scalania, dlatego koszt każdorazowego tworzenia nowej tablicy wynikowej może
być zbyt duży. Bardziej pożądana jest metoda działająca w miejscu. Powinna ona
umożliwiać posortowanie w miejscu pierwszej połowy tablicy, posortowanie w miej
scu drugiej połowy, a następnie scalenie obu części przez przenoszenie elementów
w tablicy bez zajmowania dużej ilości dodatkowej pamięci. Warto zatrzymać się na
chwilę i zastanowić nad tym, jak uzyskać taki efekt. Na pierwszy rzut oka problem
wygląda na taki, który musi mieć proste rozwiązanie. Jednak znane rozwiązania są
dość skomplikowane, zwłaszcza w porównaniu z metodami, które wymagają dodat
kowej pamięci.
Mimo to abstrakcja scalania w miejscu jest przydatna. Dlatego używamy sygna
tury merge(a, lo , mid, hi ) dla metod, które umieszczają wynik scalania podtablic
a [ l o . . mi d] i a [mi d+1.. hi ] w jednej uporządkowanej tablicy a [1 o. .h i]. Na następnej
stronie tę metodę scalania zaimplementowano za pomocą kilku wierszy kodu, które
kopiują wszystkie dane do pomocniczej tablicy i scalają je z powrotem w pierwotnej
tablicy. Inne podejście opisano w ć w i c z e n i u 2 .2 .1 0 .
282
2.2 Sortowanie przez scalanie 283
Abstrakcyjne scalanie w miejscu
public s t a t i c void merge(Comparable[] a, in t lo , in t mid, in t hi)
{ / / Scalanie a [1 o..mid] z a[mid+ 1 . . h i ] .
in t i = lo , j = mid+1 ;
fo r (in t k = lo ; k <= h i; k++) / / Kopiowanie a [ lo ..h i ] do aux[lo. . h i ] .
aux[k] = a[k] ;
fo r (in t k = lo ; k <= h i; k++) / / Scalanie z powrotem do a [lo . . h i ] .
if (i > mi d) a[k] = aux □++] ;
e ls e i f (j > hi ) a [k] = aux[i++] ;
e ls e i f (le s s ( a u x [ j] , aux [i ] ) ) a [k] = aux[j++];
el se a [k] = aux [i++] ;
}
Ta metoda najpierw kopiuje dane do pomocniczej tablicy aux [], a następnie scala je z powro
tem w tablicy a []. Przy scalaniu (druga pętla for) występują cztery warunki — wyczerpano
lewą połowę (należy pobrać dane z prawej), wyczerpano prawą połowę (należy pobrać dane
z lewej), aktualny klucz po prawej ma wartość mniejszą niż aktualny klucz po lewej (należy
pobrać dane z prawej), aktualny klucz po prawej ma wartość większą lub równą względem
aktualnego klucza po lewej (należy pobrać dane z lewej).
a[] a u x []
k 0 1 2 3 4 5 6 7 8 9 i j 0 1 2 3 4 5 6 7 8 9
Dane wejściowe E E G M R A C E R T
Kopia E E G M R A C E R T E E G M R A C E R T
0 5
0 A 0 6 E E G M R A C E R T
1 A C 0 7 E E G M R C E R T
2 A C E 1 7 E E G M R E R T
3 A C E E 2 7 E G M R E R T
4 A C E E E 2 8 G M R E R T
5 A C E E E G 3 8 G M R R T
6 A C E E E G M 4 8 M R R T
7 A C E E E G M R 5 8 R R T
8 A C E E E G M R R 5 9 R T
9 A C E E E G M R R T 6 10 T
Scalony wynik A C E E E G M R R T
Ślad działania abstrakcyjnego scalania w miejscu
284 RO ZD ZIA Ł 2 a Sortowanie
Z stępujące sortow anie przez S o r to w a n ie s o r t (a, 0, 15)
lewej s o r t (a, 0, 7)
scalanie a lgorytm 2.4 to rekurencyj-
połowy s o r t (a, 0, 3)
na implementacja sortowania przez sca
s o r t ( a , 0 , 1)
lanie oparta na abstrakcyjnym scalaniu m e rg e (a , 0, 0 1)
w miejscu. Jest to jeden z najbardziej zna s o r t ( a , 2 , 3)
nych przykładów przydatności paradyg m e rg e (a , 2, 2 3)
m e r g e ( a , 0 , 1 3)
matu dziel i zwyciężaj do projektowania
s o r t ( a , 4 , 7)
wydajnych algorytmów. Rekurencyjny
s o r t ( a , 4 , 5)
kod jest podstawą indukcyjnego dowo m e r g e ( a , 4, 4 , 5)
du na to, że algorytm sortuje tablicę. Jeśli s o r t ( a , 5 , 7)
kod sortuje dwie podtablice, sortuje całą m e r g e ( a , 6 , 6 , 7)
tablicę, scalając podtablice. m e r g e ( a , 4 , 5 , 7)
S or to w a n ie m e r g e ( a , 0 , 3 , 7)
Aby zrozumieć sortowanie przez sca
prawej s o r t ( a , 8 , 15)
lanie, warto starannie rozważyć dynamikę połowy s o r t ( a , 8 , 11)
wywołań metody przedstawionych w śla s o r t ( a , 8 , 9)
dzie po prawej stronie. Aby posortować m e r g e ( a , 8 , 8 , 9)
tablicę a [0 .. 15], metoda so rt () wywołu s o r t ( a , 1 0 , 11 )
m e r g e ( a , 1 0 , 10 , U)
je samą siebie w celu posortowania tabli
m e r g e ( a , 8 , 9 , 11)
cy a [0.. 7], potem wywołuje samą siebie,
s o r t ( a , 1 2 , 15 )
żeby posortować a [0.. 3] i a [0 .. 1], po s o r t ( a , 1 2 , 13)
czym wykonuje pierwsze scalanie a [0] m e r g e ( a , 1 2 , 1 2 , 13)
i a [ 1 ] po wywołaniu samej siebie w celu s o r t ( a , 1 4 , 15)
posortowania a [0] i a [ 1 ] (z uwagi na m e rg e ( a , 14, 1 4 , 1 5 )
m e r g e ( a , 1 2 , 1 3 , 15 )
zwięzłość w śladzie pominięto wywołania
Scalanie m e r g e ( a , 8 , 1 1 , 15)
dla przypadku podstawowego — sortowa w yników m e r g e j a , 0 , 7 , 15)
nia pojedynczych elementów). Następnie
Ślad wywołań przy zstępującym sortowaniu
scalane są elementy a [2] z a [3], potem przez scalanie
a [0 .. 1] z a [2 .. 3] itd. Na podstawie śladu
widać, że kod zapewnia uporządkowane wywoływanie metody merge (). To spostrzeżenie
okaże się przydatne dalej w podrozdziale.
Opisany tu rekurencyjny kod stanowi też podstawę do analizowania czasu wyko
nania sortowania przez scalanie. Ponieważ pokazana m etoda jest prototypowa w pa
radygmacie projektowania algorytmów typu dziel i zwyciężaj, analizy omówiono
szczegółowo.
Twierdzenie F. Zstępujące sortowanie przez scalanie wymaga od Vz N lg N d o N
lg N porównań przy sortowaniu tablicy o długości N.
Dowód. Niech liczba porównań potrzebnych do posortowania tablicy o dłu
gości N wynosi C(N). C(0) = C(l) = 0, a dla N > 0 można napisać zależność
rekurencyjną, która bezpośrednio odpowiada rekurencyjnej metodzie so rt (),
co pozwala określić górne ograniczenie liczby porównań:
C(N) < C(|_N/2 _|) + C(LN/2 ~|) + N
2.2 Sortowanie przez scalanie 285
ALGORYTM 2.4. Zstępujące sortowanie przez scalanie
public c la s s Merge
{
private s t a t ic Comparable[] aux; // Tablica pomocnicza do scalania.
public s t a t i c void sort(Comparable[] a)
{
aux = new Comparable[[Link]]; // Jednokrotna alokacja pamięci.
s o rt(a , 0, [Link] - 1);
}
private s t a t ic void sort(Comparable[] a, in t lo, in t hi)
{ // Sortowanie a [ l o . . h i ] .
i f (hi <= lo) return;
in t mid = lo + (hi - lo)/2;
so rt(a , lo, mid); // Sortowanie lewej połowy.
so rt(a , mid+1, h i) ; // Sortowanie prawej połowy.
merge(a, lo, mid, h i) ; // Scalanie wyników (kod na stro n ie 283).
}
}
Aby posortować podtablicę a [ 1o . .h i], należy podzielić ją na dwie części — a [lo . .mid]
i a [mid+1 . .h i] — posortować je niezależnie od siebie (przez wywołania rekurencyjne) i sca
lić uzyskane uporządkowane podtablicę w celu otrzymania wyniku.
a[]
lo hi 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
V /
\ / M E R G E S O R T E X A M P L E
m erge (a , 0, 0, 1) E M R G E s O R T E X A M P L E
m e rge (a , 2, 2, 3) E M G R E s O R T E X A M P L E
m e rge (a , 0, 1, 3) E G M R E s 0 R T E X A M P L E
m e rge (a , 4, 4, 5) E G M R E s 0 R T E X A M P L E
mergeCa, 6, 6, 7) E G M R E s 0 R T E X A M P L E
mergeCa, 4, 5, 7) E G M R E 0 R S T E X A | P L E
m erge (a, 0, 3, 7) E E G M O R R S T E X A M P L E
m e rge (a , 8, 8, 9) E E G M O R R S E T X A M P L E
m e rge (a , 10, 10, 11) E E G M O R R s E T A X M P L E
m erge (a, 8, 9, 11) E E G M 0 R R s A E T X M P L E
mergeCa, 12, 12, 13) E E G M 0 R R s A E T X M P L E
mergeCa, 14, 14, 15) E E G M 0 R R s A E T X M P E L
m e rge (a , 12, 13, 15) E E G M 0 R R s A E T X E L M P
m erge fa, 8, 11, 15) E E G M 0 R R s A E E L M P T X
m erge (a, 0, 7, 15) A E E E E G L M M O P R R S T X
Ślad efektów scalania przy zstępującym sortowaniu przez scalanie
286 RO ZD ZIA Ł 2 ■ Sortowanie
Pierwszy wyraz po prawej stronie to liczba porównań przy sortowaniu lewej poło
wy tablicy. Drugi wyraz to liczba porównań przy sortowaniu prawej połowy. Trzeci
wyraz to liczba porównań przy scalaniu. Wynika z tego dolne ograniczenie:
C ( N ) < C ( L W 2 j ) + C([N/2]) + Ln /2]
ponieważ liczba porównań przy scalaniu wynosi co najmniej l_M2 _|.
Dokładne rozwiązanie dla rekurencji uzyskujemy, kiedy obie strony są równe,
a N jest potęgą dwójki (na przykład N = 2"). Po pierwsze, ponieważ LlV/2 j ~ [ n /2~\
= 2 ”'1, otrzymujemy:
C( 2") = 2C(2"-1) + 2"
Dzieląc obie strony przez 2”, uzyskujemy:
C(2")/2" = C(2,M)/2"-' + 1
Po zastosowaniu tego samego równania do pierwszego wyrazu po pierwszej stro
nie otrzymujemy:
C(2")/2n = C(2"-2)/2 "-2 + 1 + 1
Powtórzenie poprzedniego kroku kolejnych n - 1 razy daje:
C(2")/2" - C(2°)/2(l + n
Po pom nożeniu obu stron przez 2n dochodzimy do rozwiązania:
C(N) = C(2") = n 2" = M lg iV
Dokładne rozwiązania dla ogólnego N są bardziej skomplikowane, ale nietrud
no zastosować to samo wnioskowanie do nierówności opisujących ograniczenie
liczby porównań, aby udowodnić uzyskany wynik dla dowolnych wartości N.
Dowód jest prawidłowy niezależnie od danych wejściowych i ich kolejności.
Inny sposób na zrozumienie t w i e r d z e n i a f polega na przyjrzeniu się pokazanemu
dalej drzewu. Każdy jego węzeł reprezentuje podtablicę, dla której metoda s o rt()
wywołuje metodę merge(). Drzewo ma dokładnie n poziomów. Dla k równych od 0
do n - 1 k-ty poziom od góry obejmuje 2k podtablic, z których każda ma długość 2"'k,
dlatego wymaga najwyżej 2”'k porównań przy scalaniu. Dlatego koszt dla każdego z n
poziomów wynosi 2k x 2"'k - 2", co oznacza łączny koszt n 2" = N Ig N.
Ig w
( a [ Q . . l ] ) ( a [ 2 . .3 ] ) ( a [ 4 . . 5 ] ) ( a [ 6 . . 7 ] ) ( a [ 8 . .9] ) (a [ 1 0 . . 1 1 ] ) (a [ 1 2 . . 1 3 ] ) (a [ 1 4 .
D rzew o zależności z p o d tab licam i przy so rto w an iu przez scalanie d la N = 16
2.2 Q Sortowanie przez scalanie 287
Twierdzenie G. Zstępujące sortowanie przez scalanie wymaga najwyżej 6N lg N
dostępów do tablicy w celu posortowania tablicy o długości N.
Dowód. Każde scalanie wymaga najwyżej 6N dostępów do tablicy (2N na ko
piowanie, 2 N na przenoszenie z powrotem i najwyżej 2N na porównania). Wynik
oparty jest na tym samym wnioskowaniu, co dla t w i e r d z e n i a f.
i g są informacją, że można oczekiwać, iż czas wykonania sortowania
t w ie r d z e n ia f
przez scalanie będzie proporcjonalny do N log N. Pozwala to przejść na wyższy po
ziom względem podstawowych m etod z p o d r o z d z i a ł u 2 .1 , ponieważ dowiadujemy
się, że m ożna sortować wielkie tablice, przy czym czas rośnie logarytmicznie wzglę
dem liczby elementów. Za pomocą sortowania przez scalanie (ale już nie przy użyciu
sortowania przez wstawianie lub wybieranie) m ożna sortować miliony lub więcej ele
mentów. Główną wadą sortowania przez scalanie jest to, że wymaga dodatkowej p a
mięci w ilości proporcjonalnej do N (pamięć ta jest potrzebna na tablicę pomocniczą
przy scalaniu). Jeśli ilość pamięci jest mała, trzeba rozważyć inną metodę. Z drugiej
strony, można znacznie skrócić czas sortowania przez scalanie, wprowadzając dobrze
przemyślane zmiany w implementacji.
Stosowanie sortow ania p rzez w staw ianie dla m ałych podtablic Większość algo
rytmów rekurencyjnych można usprawnić, traktując małe przypadki w odmienny
sposób. Rekurencja gwarantuje, że metoda zostanie zastosowana do małych przy
padków, dlatego usprawnienia w ich obsłudze prowadzą do ulepszenia całego algo
rytmu. W sortowaniu wiadomo, że sortowanie przez wstawianie (lub wybieranie) jest
proste, dlatego dla małych podtablic będzie szybsze od sortowania przez scalanie. Jak
zwykle, można zrozumieć działanie sortowania przez scalanie na podstawie śladu
wizualnego. Ślad wizualny przedstawiony na następnej stronie obrazuje działanie im
plementacji sortowania przez scalanie z przełączeniem m etody dla małych podtablic.
Zmiana algorytmu na sortowanie przez wstawianie dla małych podtablic (na przy
kład o długości 15 lub krótszych) poprawia czas wykonania typowej implementacji
sortowania przez scalanie o 10 - 15% (zobacz ć w i c z e n i e 2 .2 . 2 3 ).
S p ra w d za n ie , c z y ta b lica j e s t j u ż u p o r z ą d k o w a n a Można skrócić czas wykonania
do liniowego dla uporządkowanych tablic, dodając test, który powoduje pominięcie
wywołania merge(), jeśli a [mi d] ma wartość mniejszą lub równą a [mi d+1]. Nadal
trzeba wykonać wszystkie rekurencyjne wywołania, jednak czas wykonania dla po
sortowanych podtablic jest liniowy (zobacz ć w i c z e n i e 2 .2 .8 ).
E lim in o w a n ie k o p io w a n ia d a n ych d o ta b lic y p o m o c n ic z e j Można wyelimino
wać czas (ale nie pamięć) potrzebny na kopiowanie danych do tablicy pomocniczej
używanej przy scalaniu. Służą do tego dwa wywołania m etody sortującej — jedno
przyjmuje dane wejściowe z tablicy i umieszcza posortowane dane wyjściowe w tab
licy pomocniczej; drugie pobiera dane wejściowe z tablicy pomocniczej i umieszcza
posortowane dane wyjściowe w pierwotnej tablicy. Dzięki temu podejściu i małej
288 R O ZD ZIA Ł 2 □ Sortowanie
Pierw sza p o d tab lica illllllllllllillllllllili Jilll hi li iiliii!!![Link]..ili.i
D ruga p o d tab lica i! .lilii Ib li „L iiIILLi L.
Pierw sze scalanie m il
iiiiiil! [Link] ..ll
.......... ........................................... iiiilli li ,il,[Link]..iii .Jllll, ill. [Link].
.....Niiiiiiiiilllllllll........... lllilllllllllll, ll „LuIII,Lii«,.,[Link],
Pierwsza połowa
jest posortowana ............................. umilili lllllllllllllll, ll „Lnll Ll . i i l i Ji .Jllll,[Link].
........ 111111111111111111111111111 IIIIIIIIIIIIIIL.......[Link] .Jllll,lii,[Link].
...........iiiiiiiiiiiiiiiiiiiiiiiii lllllllllllllll ........Illl.......... III: .[Link],
........liiiiiiiiiiiiiiiiiiiiiiiiii lllllllllllllll ..........Illllllllll .[Link],
...... liiiiiiiiiiiiiiiiiiiiiiiiii lllllllllllllll...,.............IIIIIIII. ..... mil Ii J i ,
............... .. lllllllllllllll............... Illllllllll. ...... ii .........Illll
........ ......... . lllllllllllllll ...... Illllllllllllllll
D ruga połow a
je s t p o so rto w a n a -.» n iiillll mu ...mu
W ynik ........................ ...
Wizualny ślad sortowania przez scalanie z przełączeniem metody dla małych podtablic
2.2 o Sortowanie przez scalanie 289
sztuczce w rekurencji można uporządkow ać w yw ołania w taki sposób, aby na każ
dym poziom ie zam ieniać role tablicy na dane wejściowe i tablicy pom ocniczej (zo
bacz ć w ic z e n ie 2 .2 . 1 1 ).
Warto powtórzyć tu kwestię poruszoną w r o z d z i a l e 1 . Łatwo o niej zapomnieć,
dlatego wymaga przypomnienia. W kontekście lokalnym traktujemy każdy algo
rytm z książki tak, jakby był kluczowy w pewnym zastosowaniu. W ujęciu global
nym staramy się dojść do ogólnych wniosków i zarekomendować jedno z podejść.
Omówienie usprawnień niekoniecznie oznacza, że zawsze warto je stosować; może
być tylko ostrzeżeniem, aby nie wyciągać jednoznacznych wniosków na tem at wydaj
ności na podstawie pierwszych implementacji. Przy rozwiązywaniu nowego proble
mu najlepiej jest zastosować najprostszą znaną implementację, a następnie uspraw
nić ją, jeśli algorytm okaże się wąskim gardłem. Wprowadzanie usprawnień, które
skracają czas wykonania tylko o stały czynnik, może w innej sytuacji nie być warte
zachodu. Trzeba sprawdzić skuteczność konkretnych usprawnień, przeprowadzając
eksperymenty, co opisano w ćwiczeniach.
W kontekście sortowania przez scalanie trzy wymienione dalej usprawnienia są
proste do implementacji i warto się nad nim i zastanowić przy używaniu tej techniki
(na przykład w sytuacjach opisanych w końcowej części rozdziału).
Wstępujące sortowanie przez scalanie Rekurencyjna implementacja sor
towania przez scalanie jest prototypem w paradygmacie projektowania algorytmów
typu dziel i zwyciężaj. W tym podejściu problem jest rozwiązywany przez podział na
mniejsze fragmenty, rozwiązywanie podproblemów i używanie wyników do rozwią
zania całego problemu. Choć opisujemy scalanie dwóch dużych podtablic, większość
operacji scalania dotyczy krótkich podtablic. Inny sposób na zaimplementowanie
sortowania przez scalanie to uporządkowanie operacji
sz = 1
w taki sposób, aby scalanie wszystkich krótkich podtablic
miało miejsce w jednym przejściu, w drugim scalane były
pary tych podtablic i tak dalej — aż do operacji scalającej
całą tablicę. M etoda ta wymaga jeszcze mniej kodu niż
standardowa implementacja rekurencyjna. Zaczynamy
od przebiegu ze scalaniem 1 na 1 (poszczególne elementy
traktowane są jak podtablice o długości 1). Następnie ma
miejsce przebieg ze scalaniem 2 na 2 (scalanie podtablic
o długości 2 w celu utworzenia 4-elementowych podtab
lic), potem 4 na 4 i tak dalej. W każdym przebiegu przy
ostatnim scalaniu druga podtablica może być mniejsza
od pierwszej (co nie stanowi problemu dla m etody mer-
ge ()), jednak w innych sytuacjach scalanie dotyczy pod
tablic o równej wielkości, a w każdym przebiegu długość
sortowanych podtablic jest podwajana. Wizualny ślad wstępującego
sortowania przez scalanie
290 R O ZD ZIA Ł 2 Sortowanie
Wstępujące sortowanie przez scalanie
public c la s s MergeBU
{
prívate s t a t i c Comparable[] aux; // Tablica pomocnicza do scalania.
// Kod metody merge() znajduje s ię na stro n ie 283.
public s t a t ic void sort(Comparable[] a)
{ // Wykonuje lg N przebiegów ze scalaniem par.
in t N = [Link];
aux = new Comparable[N ];
fo r (in t sz = 1; sz < N; sz = sz+sz) // sz: rozmiar podtablicy.
for (in t lo = 0; lo < N-sz; lo += sz+sz) // lo: indeks podtablicy.
merge(a, lo, lo+ sz-1 , Math.m in(lo+sz+sz-l, N - l ) ) ;
}
}
Wstępujące sortowanie przez scalanie obejmuje serię przebiegów po całej tablicy, w których
scalane są po dwie podtablice o wielkości sz. Początkowo sz jest równe 1, a każdy prze
bieg powoduje podwojenie tej wartości. Ostatnia podtablica ma rozmiar sz tylko wtedy, jeśli
wielkość tablicy jest wielokrotnością sz (w przeciwnym razie podtablica jest krótsza).
a [i]
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
SZ = 1
M E R G E s 0 R T E X A M P L E
mergefa, 0, 0, 1) E M R G E s 0 R T E X A M P L E
mergefa, 2, 2, 3) E M G R E s 0 R T E X A M P L E
merge(a, 4, 4, 5) E M G R E s 0 R T E X A M P L E
mergefa, 6, 6, 7) E M G R E s 0 R T E X A M P L E
mergefa, 8, 8, 9) E M G R E s 0 R E T X A M P L E
mergefa, 10, 10, 11) E M G R E s 0 R E T A X M P L E
mergefa, 12, 12, 13) E M G R E s 0 R E T A X M P L E
mergefa, 14, 14, 15) E M G R E s 0 R E T A X M P E L
sz = 2
mergefa, 0, 1, 3) E G M R E s 0 R E T A X M P E L
mergefa, 4, 5, 7) E G M R E 0 R s E T A X M P E L
mergefa, 8, 9, 11) E G M R E 0 R S A E T X M P E L
mergefa, 12, 13, 15) E G M R E 0 R s A E T X E L M P
sz =4
mergefa, 0, 3, 7) E E G M O R R s A E T X E L M P
mergefa, 8, 11, 15) E E G M 0 R R s A E E L M P T X
OO
II
w
N
mergefa, 0, 7, 15) A E E E E G L M M 0 P R R S T X
Ślad z wynikami scalania we wstępującym sortowaniu przez scalanie
2.2 0 Sortowanie przez scalanie 291
Twierdzenie H. Wstępujące sortowanie przez scalanie wymaga od Vi N lg N do
N lg N porównań i najwyżej 6N lg N dostępów do tablicy przy sortowaniu tablicy
o długości N.
Dowód. Liczba przebiegów przez tablicę wynosi dokładnie Lig NJ (jest to war
tość n, dla której 2" < N < 2n+1). W każdym przebiegu liczba dostępów do tablicy
wynosi dokładnie 6N, a liczba porównań to najwyżej N i nie mniej niż N/2.
k i e d y d ł u g o ś ć t a b l i c y j e s t p o t ę g ą d w ó j k i , wstępujące i zstępujące sortowanie
przez scalanie obejmuje dokładnie te same porównania i dostępy do tablicy, choć
w odwrotnej kolejności. Jeżeli tablica ma inną długość, ciągi porównań i dostępów do
tablicy w obu wersjach algorytmu będą różne (zobacz ć w i c z e n i e 2 .2 .5 ).
Wstępujące sortowanie przez scalanie stosuje się do sortowania danych na listach
powiązanych. Sortowana lista jest traktowana jak podlisty o rozmiarze 1 . Metoda two
rzy posortowane podtablice o rozmiarze 2 powiązanych elementów, następnie o roz
miarze 4 itd. M etoda modyfikuje odnośniki, co pozwala posortować listę w miejscu,
bez tworzenia nowych węzłów listy.
Oba sposoby implementowania algorytmu dziel i zwyciężaj, zstępujący i wstępu
jący, są intuicyjne. Oto wniosek, jaki można wyciągnąć z sortowania przez scalanie
— przy napotkaniu algorytmu opartego na jednym ze sposobów warto zastanowić
się nad drugim. Czy chcesz rozwiązać problem, dzieląc go na mniejsze (i rozwiązując
je rekurencyjnie), tak jak w metodzie Merge. s o rt (), czy chcesz łączyć mniejsze roz
wiązania w większe, tak jak w metodzie [Link] ()?
Złożoność sortowania Ważnym powodem, dla którego warto znać sortowa
nie przez scalanie, jest to, że technika ta służy do dowodzenia podstawowego wyni
ku z obszaru złożoności obliczeniowej, pomagającego zrozumieć naturalną trudność
sortowania. Ogólnie złożoność obliczeniowa odgrywa istotną rolę w projektowaniu
algorytmów, a wspomniany wynik jest bezpośrednio związany z projektowaniem al
gorytmów sortowania, dlatego omawiamy go szczegółowo.
Pierwszym krokiem przy badaniu złożoności jest ustalenie m odelu obliczeń.
Ogólnie badacze starają się ustalić najprostszy model adekwatny do problemu. W sor
towaniu badamy klasę algorytmów opartych na porównaniach, w których decyzje są
podejmowane na podstawie porównywania kluczy. Algorytm tego rodzaju może wy
konywać dowolne obliczenia między porównaniami, jednak nie może uzyskać żad
nych informacji o kluczu w inny sposób niż przez porównanie go z innym. Z uwagi
na wprowadzone w książce ograniczenie, związane z interfejsem API Comparabl e, do
tej klasy należą wszystkie algorytmy omawiane w rozdziale (zauważ, że pomijamy
koszt dostępów do tablicy), podobnie jak wiele innych algorytmów, które można so
bie wyobrazić. W r o z d z i a l e 5 . opisano algorytm, który działa nie tylko dla elemen
tów zgodnych z interfejsem Comparabl e.
RO ZD ZIA Ł 2 o Sortowanie
Twierdzenie I. Żaden algorytm sortowania oparty na porównaniach nie gwaran
tuje posortowania N elementów za pomocą mniej niż lg(N!) ~ N lg N porównań.
Dowód. Po pierwsze, zakładamy, że wszystkie klucze są różne, ponieważ każ
dy algorytm musi potrafić posortować dane wejściowe tego rodzaju. Używamy
drzewa binarnego do opisu ciągu porów nań. Każdy węzeł drzewa to albo liść
( i 0 i t y - . y j . co jest informacją, że sortowanie zakończono i wykryto, iż pierwot
ne dane wejściowe miały kolejność a [i 0] , a [i J ,..., a [i N1] , albo węzeł wewnętrzny
(Q ), który odpowiada operacji porównania a [i] z a [ j] , przy czym lewe pod-
drzewo to ciąg porównań dla sytuacji, w której a [i] jest mniejsze niż a [ j ] , a pra
we poddrzewo określa porównania dla sytuacji, kiedy a [i ] jest większe niż a [ j ] .
Każda ścieżka z korzenia do liścia odpowiada ciągowi porównań, które algorytm
stosuje, aby ustalić kolejność podaną w liściu. Oto przykładowe drzewo porów
nań dla N = 3:
Takie drzewo nigdy nie jest tworzone bezpośrednio — stanowi tylko narzędzie
matematyczne do opisu porównań używanych przez algorytm.
Pierwszym kluczowym spostrzeżeniem w dowodzie jest to, że drzewo musi
obejmować przynajmniej N\ liści, ponieważ dla N niepowtarzalnych kluczy ist
nieje N! różnych permutacji. Jeśli jest mniej niż N] liści, musi brakować pewnych
permutacji, a algorytm ich nie znajdzie.
Liczba węzłów wewnętrznych na ścieżce z korzenia do liścia to liczba porów
nań wykonywanych przez algorytm dla pewnych danych wejściowych. Interesuje
nas długość najdłuższej ścieżki w drzewie (wysokość drzewa), ponieważ wyznacza
ona liczbę porównań dla najgorszego przypadku. Podstawową cechą kombina-
toryczną drzew binarnych jest to, że drzewo o wysokości h ma nie więcej niż 2h
liści. Drzewo o wysokości h z maksymalną liczbą liści jest w pełni zbalansowane
(kompletne). Na następnej stronie przedstawiono rysunek dla h = 4.
2.2 ■ Sortowanie przez scalanie 293
W dwóch poprzednich akapitach pokazano, że każdy algorytm sortowania oparty
na porównaniach odpowiada drzewu porównań o wysokości h, przy czym:
NI < liczba liści < 2h
Wartość h to dokładnie liczba porównań dla najgorszego przypadku. Dlatego moż
na obliczyć logarytm o podstawie 2 dla obu stron równania i stwierdzić, że liczba
porównań w algorytmie musi wynosić co najmniej lg M . Przybliżenie Ig NI ~ Wig
N wynika bezpośrednio z przybliżenia Stirlinga dla silni (zobacz stronę 197).
Wynik ten to wskazówka określająca w czasie projektowania algorytmu sortowania,
jak dobrych efektów można oczekiwać. Nie znając tego wyniku, programista może
próbować opracować oparty na porównaniach algorytm sortowania, który dla naj
gorszego przypadku wymaga o połowę mniej porównań niż sortowanie przez scala
nie. Dolne ograniczenie w t w i e r d z e n i u i oznacza, że próby będą bezowocne — taki
algorytm nie istnieje. Jest to niezwykle mocne stwierdzenie, dotyczące dowolnego
algorytmu opartego na porównaniach.
t w i e r d z e n i e h oznacza, że liczba porównań w sortowaniu przez scalanie wy
nosi dla najgorszego przypadku ~ N Ig N. Wynik ten to górne ograniczenie związane
z trudnością problemu sortowania w tym sensie, że lepszy algorytm musi gwaran
tować mniejszą liczbę porównań. W t w i e r d z e n i u i napisano, że żaden algorytm
sortowania nie gwarantuje liczby porównań mniejszej niż ~ N lg N. Jest to dolne
ograniczenie dotyczące trudności problemu sortowania. Nawet najlepszy możliwy
algorytm wykonuje dla najgorszego przypadku tę liczbę porównań. Z tych dwóch
twierdzeń wynika poniższe.
RO ZD ZIA Ł 2 u Sortowanie
T w ierdzenie J. Sortowanie przez scalanie jest asymptotycznie optymalnym al
gorytmem sortowania opartym na porównaniach.
D ow ód. Twierdzenie to oznacza, że zarówno liczba porównań potrzebnych dla
najgorszego przypadku w sortowaniu przez scalanie, jak i minimalna liczba porów
nań, jaką można zagwarantować w dowolnym algorytmie sortowania opartym na
porównaniach, wynosi ~ N lg N. Fakty te opisano w t w i e r d z e n i a c h h i i.
Należy zauważyć, że — podobnie jak w modelu obliczeń — trzeba precyzyjnie zdefi
niować, czym jest algorytm optymalny. Można zawęzić definicję optymalności i za
żądać, aby optymalny algorytm sortowania wykonywał dokładnie lg M porównań.
Nie robimy tego, ponieważ dla dużych N nie da się dostrzec różnicy między takim
algorytmem a na przykład sortowaniem przez scalanie. Można też rozszerzyć defini
cję tak, aby ująć w niej dowolny algorytm sortowania, dla którego liczba porównań
dla najgorszego przypadku różni się od N lg N o stały czynnik. Nie postępujemy tak,
ponieważ dla dużych N można zauważyć różnicę między takim algorytmem a sorto
waniem przez scalanie.
Z Ł O Ż O N O Ś Ć O B L IC Z E N IO W A M O ŻE WYDAWAĆ S ię CZYM Ś A B STRA K C Y JN Y M , jednak
podstawowe badania nad naturalną trudnością problemów obliczeniowych nie wy
magają uzasadniania. Ponadto kiedy m ożna wykorzystać wiedzę o złożoności obli
czeniowej, pomaga ona w rozwijaniu dobrego oprogramowania. Po pierwsze, górne
ograniczenia pozwalają inżynierom oprogramowania zapewnić gwarancje wydaj
ności. Istnieje wiele udokumentowanych sytuacji, w których niska wydajność wy
nikała z zastosowania sortowania kwadratowego zamiast liniowo-logarytmicznego.
Po drugie, dolne ograniczenia pozwalają uniknąć pracy nad szukaniem nieosiągal
nych zysków w wydajności.
Jednak stwierdzenie optymalności sortowania przez scalanie to jeszcze nie koniec.
Nie należy mylnie sądzić, że nie warto zastanawiać się nad innymi m etodam i do
użytku w praktycznych zastosowaniach. Na przykład:
■ Sortowanie przez scalanie nie jest optymalne ze względu na wykorzystanie pa
mięci.
■ Najgorszy przypadek w praktyce może być mało prawdopodobny.
■ Ważne mogą być operacje różne od porównań (na przykład dostępy do tablicy).
■ Niektóre dane m ożna sortować bez przeprowadzania jakichkolwiek porównań.
Dlatego w książce omówiono też kilka innych m etod sortowania.
2.2 » Sortowanie przez scalanie 295
{ PYTANIA I ODPOWIEDZI
P. Czy sortowanie przez scalanie jest szybsze od sortowania Shella?
O. W praktyce czas wykonania obu tych algorytmów nie różni się więcej niż o mały
stały czynnik (jeśli w sortowaniu Shella zastosuje się dobrze sprawdzony ciąg odstę
pów, taki jak w a l g o r y t m i e 2 .3 ). Dlatego ich względna wydajność zależy od imple
mentacji.
% java SortCompare Merge Shell 100000
Dla 100000 losowych wartości Double
technika Merge j e s t 1.2 razy szybsza od Shell
Nikt nie zdołał teoretycznie udowodnić, że sortowanie Shella jest liniowo-logaryt
miczne dla danych losowych, dlatego możliwe, iż asymptotyczny wzrost czasu wy
konania dla najgorszego przypadku w sortowaniu Shella jest wyższy. Udowodniono
taką sytuację dla wydajności w najgorszym przypadku, jednak w praktyce nie ma to
znaczenia.
P. Dlaczego nie tworzymy tablicy aux [] jako lokalnej w metodzie merge () ?
O. Aby uniknąć narzutu związanego z tworzeniem tablicy przy każdym scalaniu,
nawet dla małej liczby elementów. Koszt ten mógłby stać się dominujący ze wzglę
du na czas wykonania sortowania przez scalanie (zobacz ć w i c z e n i e 2 .2 .26 ). Lepsze
rozwiązanie (które pominięto w tekście, aby uniknąć komplikacji w kodzie) polega
na utworzeniu tablicy aux[] lokalnie w metodzie s o rt() i przekazywaniu jej jako
argumentu do m etody merge () (zobacz ć w i c z e n i e 2 .2 .9 ).
P. Jaka jest wydajność sortowania przez scalanie, jeśli wartości w tablicy się powta
rzają?
O. Jeżeli wszystkie elementy mają tę samą wartość, czas wykonania jest liniowy (po
zastosowaniu dodatkowego testu, który pozwala pominąć scalanie, gdy tablica jest
posortowana). Jeśli jednak powtarza się więcej niż jedna wartość, trudno poprawić
wydajność. Załóżmy na przykład, że tablica wejściowa składa się z N elementów o da
nej wartości na pozycjach nieparzystych i N elementów o innej wartości na pozycjach
parzystych. Czas wykonania jest tu liniowo-logarytmiczny (tak jak dla elementów
o różnych wartościach), a nie liniowy.
296 RO ZD ZIA Ł 2 □ Sortowanie
ĆWICZENIA
2.2.1. Przedstaw ślad działania kodu (podobny do śladu z początkowej części pod
rozdziału), aby pokazać, jak klucze A E Q S U Y E I N O S T s ą scalane za pomocą
abstrakcyjnej metody merge() działającej w miejscu.
2.2.2. Przedstaw ślady działania kodu (podobne do śladu dla a l g o r y t m u 2 .4 ), aby
pokazać, jak klucze E A S Y Q U E S T I O Nsą sortowane za pom ocą zstępującego
sortowania przez scalanie.
2.2.3. Wykonaj ć w i c z e n i e 2 . 2.2 dla wstępującego sortowania przez scalanie.
2.2.4. Czy abstrakcyjne scalanie w miejscu zwraca poprawne dane wyjściowe wte
dy i tylko wtedy, jeśli dwie tablice wejściowe są posortowane? Udowodnij odpowiedź
lub przedstaw kontrprzykład.
2.2.5. Dla N = 39 podaj ciąg rozmiarów podtablic w operacjach scalania w algoryt
mach zstępującego i wstępującego sortowania przez scalanie.
2.2.6. Napisz program obliczający dokładną wartość liczby dostępów do tablicy
w zstępującym i wstępującym sortowaniu przez scalanie. Użyj programu do rysowa
nia wykresów dla wartości N od 1 do 512 i porównaj dokładne wartości z górnym
ograniczeniem — 6N lg N.
2 . 2 . 7 . Pokaż, że liczba porównań w sortowaniu przez scalanie jest monotonicznie
rosnąca (C(IV+1) > C(N) dla wszystkich N > 0).
2.2.8. Załóżmy, że a l g o r y t m 2.4 zmodyfikowano, aby pominąć wywołanie mer-
ge(), jeśli a [mi d] <= afmid+l]. Udowodnij, że w sortowaniu przez scalanie liczba
porównań dla posortowanej tablicy rośnie liniowo.
2.2.9. Stosowanie tablicy statycznej w rodzaju aux [] w bibliotekach jest niezalecane,
ponieważ liczne klienty mogą jednocześnie korzystać z klasy. Podaj implementację
klasy Merge bez statycznej tablicy. Nie twórz tablicy aux[] jako lokalnej w metodzie
merge() (zobacz p y t a n i a i o d p o w i e d z i do tego podrozdziału). Wskazówka: przeka
zuj tablicę pomocniczą jako argument do rekurencyjnej m etody s o rt().
2.2 o Sortowanie przez scalanie 297
p r o b l e m y d o r o z w ią z a n ia
2.2.10. Szybsze scalanie. Zaimplementuj wersję m etody merge(), kopiującą drugą
połowę tablicy a[] do aux[] w kolejności malejącej, a następnie scalającą dane z p o
wrotem w a []. Ta zmiana pozwala usunąć z pętli wewnętrznej kod do sprawdzania,
czy wyczerpano zawartość poszczególnych połówek. Uwaga: uzyskane sortowanie
nie jest stabilne (zobacz stronę 353).
2.2.11. Usprawnienia. Zaimplementuj trzy usprawnienia sortowania przez scalanie
opisane w tekście na stronie 287. Dodaj przełączenie m etody dla małych podtablic,
sprawdzanie, czy tablica jest już uporządkowana, i unikanie kopiowania przez prze
stawianie argumentów w kodzie rekurencyjnym.
2.2.12. Dodatkowa pamięć rosnąca wolniej niż liniowo. Opracuj implementację sca
lania, w której potrzebna dodatkowa pamięć wynosi tylko max(M, N/M). Wykorzystaj
następujący pomysł — podziel tablicę na N / M bloków o wielkości M (dla uproszcze
nia opisu zakładamy, że N to wielokrotność M). Następnie (i), traktując bloki jak
elementy z pierwszym kluczem jako kluczem sortowania, posortuj je za pom ocą sor
towania przez wybieranie i (ii) przejdź przez tablicę, scalając pierwszy blok z drugim,
drugi z trzecim itd.
2.2.13. Dolne ograniczenie dla typowego przypadku. Udowodnij, że oczekiwana licz
ba porównań w dowolnym algorytmie sortowania opartym na porównaniach musi
wynosić przynajmniej ~ N lg N (przy założeniu, że wszystkie możliwe kolejności
danych wejściowych są równie prawdopodobne). Wskazówka: oczekiwana liczba
porównań to przynajmniej długość zewnętrznej ścieżki w drzewie porównań (suma
długości ścieżek z korzenia do wszystkich liści); liczba ta jest m inim alna dla drzewa
zbalansowanego.
2.2.14. Scalanie posortowanych kolejek. Opracuj statyczną metodę, która jako argu
menty przyjmuje dwie kolejki posortowanych elementów i zwraca kolejkę utworzoną
przez scalenie dwóch pierwotnych w jedną posortowaną.
2.2.15. Wstępujące sortowanie przez scalanie kolejek. Opracuj implementację wstę
pującego sortowania przez scalanie na podstawie opisanego dalej podejścia. Dla N
elementów należy utworzyć N kolejek, z których każda ma zawierać jeden element.
Ponadto należy utworzyć kolejkę N kolejek, a następnie wielokrotnie stosować scala
nie z ć w i c z e n i a 2 .2.14 dla dwóch pierwszych kolejek i ponownie wstawiać scaloną
kolejkę na koniec. Proces należy powtarzać do momentu, w którym kolejka kolejek
obejmuje tylko jedną kolejkę.
298 R O ZD ZIA Ł 2 a Sortowanie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
2 .2.16. Naturalne sortowanie przez scalanie. Napisz wersję wstępującego sortowa
nia przez scalanie, w której wykorzystano uporządkowanie elementów w tablicach.
W tym celu przy szukaniu dwóch tablic do scalenia należy zawsze postępować tak:
znaleźć posortowaną podtablicę (zwiększając wskaźnik do czasu znalezienia ele
m entu mniejszego od poprzednika), następnie znaleźć drugą taką tablicę i je sca
lić. Przeanalizuj czas wykonania algorytmu w kategoriach rozmiaru tablicy i liczby
najdłuższych rosnących ciągów w tablicy.
2.2.17. Sortowanie list powiązanych. Zaimplementuj naturalne sortowanie przez
scalanie dla list powiązanych. Dla list powiązanych jest to technika stosowana z wy
boru, ponieważ nie wymaga dodatkowej pamięci i gwarantuje liniowo-logarytmicz-
ny czas wykonania.
2.2.18. Mieszanie elementów listy powiązanej. Opracuj i zaimplementuj algorytm
typu dziel i zwyciężaj, który losowo miesza elementy listy w czasie liniowo-logaryt-
micznym i wymaga logarytmicznie rosnącej ilości pamięci.
2.2.19. Inwersje. Opracuj i zaimplementuj liniowo-logarytmiczny algorytm do
określania liczby inwersji w danej tablicy (jest to też liczba przestawień potrzebnych
do posortowania danej tablicy — zobacz p o d r o z d z i a ł 2 . 1 ). Wartość ta jest powią
zana z odległością tau Kendalla; zobacz p o d r o z d z i a ł 2 .5 .
2.2.20. Sortowanie pośrednie. Opracuj i zaimplementuj wersję sortowania przez sca
lanie, która nie powoduje zmiany uporządkowania tablicy, ale zwraca tablicę perm
typu i nt [], w której perm[i ] to indeks i-tego najmniejszego elementu tablicy.
2.2.21. Trzykrotne powtórzenia. Dla trzech list obejmujących N nazw każda opracuj
algorytm liniowo-logarytmiczny do określania, czy istnieją nazwy powtarzające się
na każdej liście. Algorytm ma zwracać takie nazwy.
2.2.22. Trójścieżkowe sortowanie przez scalanie. Załóżmy, że zamiast dzielić tablicę
w każdym kroku na połowę, algorytm dzieli ją na trzy części, sortuje każdą z nich
i łączy je za pom ocą trój ścieżkowego scalania. Jakie jest tempo wzrostu czasu wyko
nania tego algorytmu?
2.2 a Sortowanie przez scalanie 299
^ e k s p e ry m e n ty
2 .2 .2 3 . Usprawnienia. Przeprowadź badania empiryczne, aby ocenić skuteczność
każdego z trzech opisanych usprawnień sortowania przez scalanie (zobacz ć w i c z e n i e
2 .2 . 1 1 ). Ponadto porównaj wydajność implementacji scalania podanej w tekście ze
scalaniem opisanym w ć w i c z e n i u 2 .2 . 10 . Empirycznie określ najlepszą wartość pa
rametru wyznaczającego, kiedy należy zastosować sortowanie przez wstawianie dla
małych podtablic.
2 .2 .2 4 . Usprawnienie ze sprawdzaniem uporządkowania. Przeprowadź empiryczne
badania dla dużych losowo uporządkowanych tablic, aby zbadać skuteczność m ody
fikacji opisanej w ć w i c z e n i u 2 .2.8 dla losowych danych. Sformułuj hipotezę doty
czącą średniej liczby udanych testów (kiedy to tablica jest posortowana) jako funkcję
od N (rozmiar całej tablicy do posortowania).
Wielościeżkowe sortowanie przez scalanie. Opracuj implementację sor
2 .2 .2 5 .
towania przez scalanie opartą na k-ścieżkowym (a nie dwuścieżkowym) scalaniu.
Przeanalizuj algorytm, sformułuj hipotezę na temat najlepszej wartości k i przepro
wadź eksperymenty, aby potwierdzić hipotezę.
Tworzenie tablicy. Użyj programu SortC om pare, aby ogólnie określić na swo
2 .2 .2 6 .
im komputerze wpływ, jaki na wydajność ma tworzenie tablicy aux[] w metodzie
m erge() zamiast w s o r t ().
2 .2 .2 7 . Długość podtablic. Przeprowadź sortowanie przez scalanie dla dużych loso
wych tablic i empirycznie określ (jako funkcję od Ai — sumy rozmiarów dwóch sca
lanych podtablic) średnią długość drugiej tablicy po wyczerpaniu pierwszej.
Sortowanie zstępujące a wstępujące. Użyj programu SortC om pare do porówna
2 .2 .2 8 .
nia zstępującego i wstępującego sortowania przez scalanie dla N = 10 \ 104, 105 i 10 6.
2 .2 .2 9 . Naturalne sortowanie przez scalanie. Określ empirycznie liczbę przebiegów
potrzebnych w naturalnym sortowaniu przez scalanie (zobacz ć w i c z e n i e 2 .2 .1 6 ) dla
losowych kluczy typu Long dla N = 103, 10ć i 10 9. Wskazówka: nie musisz implemen
tować sortowania (a nawet generować całych 64-bitowych kluczy) w celu ukończenia
ćwiczenia.
t e m a t e m t e g o p o d r o z d z i a ł u jest prawdopodobnie najczęściej obecnie stosowa
ny algorytm sortowania — sortowanie szybkie (ang. quicksort). Sortowanie szybkie
jest popularne, ponieważ nietrudno je zaimplementować, działa dobrze dla różnego
rodzaju danych wejściowych i w typowych zastosowaniach jest znacząco szybsze od
innych m etod sortowania. Korzystnymi cechami algorytmu sortowania szybldego
jest to, że działa w miejscu (wymaga tylko małego stosu pomocniczego) i w czasie
proporcjonalnym średnio do N log N przy sortowaniu tablicy o długości N. Żaden
z opisanych do tej pory algorytmów nie łączy tych dwóch cech. Ponadto sortowanie
szybkie ma krótszą pętlę wewnętrzną niż większość pozostałych algorytmów sorto
wania, co oznacza, że jest szybki zarówno w praktyce, jak i w teorii. Jego główną wadą
jest to, że jest wrażliwy — w tym sensie, że trzeba go starannie zaimplementować, aby
uniknąć niskiej wydajności. W literaturze udokumentowano wiele błędów prowa
dzących w praktyce do wydajności kwadratowej. Na szczęście, wyciągnięte wnioski
doprowadziły do różnych usprawnień algorytmu, które — jak się okaże — dodatkowo
zwiększają jego przydatność.
Podstawowy algorytm Sortowanie szybkie to technika typu dziel i zwyciężaj.
Działa przez podział tablicy na dwie podtablice i sortowanie podtablic niezależnie
od siebie. Sortowanie szybkie to uzupełnienie sortowania przez scalanie. Sortowanie
przez scalanie polega na podziale tablicy na dwie sortowane podtablice i łączeniu
uporządkowanych podtablic w całą posortowaną tablicę. W sortowaniu szybkim
tablica jest modyfikowana w taki sposób, że jeśli dwie podtablice są posortowane,
posortowana jest też cała tablica. W pierwszej technice wykonywane są dwa rekuren-
cyjne wywołania przed operacją na całej tablicy. W drugiej technice dwa rekurencyj-
ne wywołania mają miejsce po operacji na całej tablicy. W sortowaniu przez scalanie
tablica jest dzielona na połowę. W sortowaniu szybkim miejsce podziału zależy od
zawartości tablicy.
Dane w ejściow e Q U I C K S 0 R T E X A M P L E
M ieszanie K A T E L E P U I M Q C X 0 s
Element osiowy
Podział E C A I E K L P U T M Q R X 0 s
Nie większe Nie mniejsze ^
S o rto w an ie lew ej stro n y A C E E I K L P U T M Q R X 0 s
S o rto w an ie p raw ej strony A C E E I K L M 0 P Q R S T U X
Wynik A c E E I K L M 0 P Q R S T u X
Działanie sortowania szybkiego
300
M
2.3 Sortowanie szybkie 301
ALGORYTM 2.5. Sortowanie szybkie
p u b l i c c l a s s Quick
{
public s t a t i c void sort(Comparabl e[] a)
{
St d Ra n d om. s h u f f l e ( a ) ; / / Eliminowanie z a le ż n o śc i od d a n y c h w e j ś c i o w y c h .
sort(a, O, a . l e n g t h - 1);
}
private sta tic void sort(C om parable[] a, int lo, i n t hi)
{
if (hi <= l o ) return;
int j = partition(a, lo, hi); // Podział (zobacz s t r o n ę 303).
sort(a, lo, j-1 ); / / So rt o w a n ie lewej strony a[lo .. j - 1 ] .
s o r t( a , j+1, hi); / / S or to wa ni e prawej s t r o n y a [ j+ 1 .. hi].
}
}
Sortowanie szybkie to rekurencyjny program, który sortuje podtablicę a [1 o .. hi] za po
mocą m etody p arti tio n (). M etoda ta umieszcza a [i] w pewnym miejscu i porządkuje
pozostałe elementy w taki sposób, że rekurencyjne wywołania kończą sortowanie.
lo j hi 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Początkowe wartości U I C l< S O R T E X A M p L E
Q
Losowe mieszanie K R A T E L E P U I M Q C X O s
0 5 15 E C A I E K L P U T M Q R X O s
0 3 4 E C A E I K L P U T M Q R X O s
0 2 2 A C E E I K L P u T M Q R X 0 s
0 0 1 A C E E I K L P u T M Q R X 0 s
A 1 A C E E I K L P u T M Q R X 0 s
<r 4 4 A c E E I K L P u T M Q R X 0 s
6 6 15 A c E E I K L P u T M Q R X 0 s
Bez podziału 7 9 15 A c E E I K L M 0 P T Q R X u s
podtablic 7 7 8 A c E E I K L M 0 P T Q R X u s
o wielkości 1 ' 8 S A c E E I K L M 0 P T Q R X u s
10 13 15 A c E E I K L M 0 P S Q R T u X
10 12 12 A c E E I K L M 0 P R Q S T u X
10 11 11 A c E E I K L M 0 P Q R S T u X
10 10 A c E E I K L M 0 P Q R s T u X
14 14 15 A c E E I K L M 0 P Q R s T u X
*15 15 A c E E I K L M 0 P 0 R s T u X
Wynik A c E E I K L M 0 P Q R s T u X
30 2 RO ZD ZIA Ł 2 h Sortowanie
Istotą m etody jest proces podziału, który powoduje uporządkowanie tablicy w taki
sposób, aby spełnione były trzy poniższe warunki:
■ Element a [j] znajduje się na ostatecznym miejscu w tablicy (dla pewnego j).
■ Żaden element w przedziale od a [1 o] d o a [ j - l ] nie jest większy niż a [j].
■ Żaden element w przedziale od a [j+ 1 ] d o a [h i] nie jest mniejszy niż a [j].
Można posortować całą tablicę, dzieląc ją, a następnie rekurencyjnie stosując przed
stawioną metodę.
Ponieważ proces podziału zawsze umieszcza jeden element na ostatecznej pozy
cji, nietrudno jest utworzyć formalny dowód przez indukcję na to, że rekurencyjna
metoda poprawnie sortuje dane. Jeśli lewa i prawa podtablica są poprawnie posor
towane, wynikowa tablica, składająca się z lewej podtablicy (uporządkowanej i bez
elementów większych niż osiowy), elementu osiowego i prawej podtablicy (uporząd
kowanej i bez elementów mniejszych niż osiowy), jest posortowana, a l g o r y t m 2.5
to rekurencyjny program będący implementacją opisanego pomysłu. Jest to algorytm
z randomizację, ponieważ przed sortowaniem loso
Przed V
t t
wo miesza zawartość tablicy. Mieszanie stosuje się po
lo hi
to, aby móc przewidzieć cechy z obszaru wydajności
(i wiedzieć, że będą prawdziwe). Zagadnienie to opi
B U<v
W trakcie >V
sano dalej.
Aby uzupełnić implementację, trzeba zaimple
Po >V
mentować metodę dzielącą. Służy do tego następu
ł ł
lo hi jąca ogólna strategia — najpierw należy arbitralnie
Podział w sortowaniu szybkim wybrać a [1 o] jako element osiowy, który znajdzie się
na ostatecznej pozycji. Następnie należy sprawdzać
elementy od lewej strony tablicy do m om entu znalezienia elementu większego od
osiowego (lub m u równego), a następnie przeszukiwać elementy od prawej strony
tablicy do czasu wykrycia wartości mniejszej od osiowej (lub jej równej). Dwa ele
menty, na których się zatrzymano, znajdują się w niewłaściwych miejscach, dlatego
należy je przestawić. Kontynuacja tego procesu gwarantuje, że żaden element tablicy
na lewo od lewego indeksu (i) nie jest większy od elementu osiowego, a żaden ele
m ent na prawo od prawego indeksu (j) nie jest mniejszy od osiowego. Kiedy w arto
ści indeksów się przetną, wystarczy zakończyć proces podziału przez przestawienie
elementu osiowego a [1 o] z pierwszym od prawej elementem lewej podtablicy (a [ j ] )
i zwrócić indeks j .
Z implementowaniem sortowania szybkiego związanych jest kilka zaawansowa
nych kwestii. Uwzględniono je w kodzie i warto o nich wspomnieć, ponieważ każda
może prowadzić do powstania nieprawidłowego kodu i mieć duży wpływ na wydaj
ność. Dalej omówiono niektóre takie kwestie. W dalszej części podrozdziału opisano
trzy ważne usprawnienia algorytmiczne wyższego poziomu.
2.3 Sortowanie szybkie 303
Podział w sortowaniu szybkim
private s t a t ic in t p a r t i tion(Comparabl e[] a, in t lo, in t hi)
{ // Podział na a [ ł o . . i - l ] , a [ i ] , a [i +1.. hi ].
in t i = lo, j = hi +1; // Lewy i prawy indeks do przeglądania ta b lic y .
Comparable v = a [1 o] ; // Element osiowy,
while (true)
{ // Sprawdzanie po prawej, sprawdzanie po lewej, ustalanie,
// czy przeglądanie zakończono, oraz przestawianie,
while (1 e s s ( a [ + + i ] , v)) i f (i == hi) break;
while ( l e s s ( v , a [ — j ] ) ) i f (j == lo) break;
i f (i >= j) break;
exch(a, i , j ) ;
}
exch(a, lo, j ) ; // Umieszczanie v = a[j] na właściwym miejscu,
return j; // tak aby a [ l o . . j - l ] <= a [j] <= a [ j + l .. h i ] .
}
Kod dzieli tablicę według elementu v z pozycji a [1 o ]. Pętla główna kończy pracę, kiedy uży
wane do przeglądania tablicy indeksy i oraz j się przetną. W pętli indeks i jest zwiększany
dopóty, dopóki a [i ] ma wartość mniejszą niż v, natomiast indeks j jest zmniejszany dopóty,
dopóki a [j] ma wartość większą niż v. Wtedy ma miejsce przestawianie, co pozwala zacho
wać niezmiennik, zgodnie z którym żaden element na lewo od i nie jest większy niż v i żaden
element na prawo od j nie jest mniejszy niż v. Po przecięciu się indeksów można dokończyć
podział, przestawiając a [1 o] z a [j] (przez co wartość osiowa zostaje zapisana w a [ j ] ).
V a[]
i 1 2 3 4 5 6 7 8 9 10 1 1 12 13 14 15
j \ \°
Początkow e w artości 0 16 l< R A T E L E P U X M Q C X 0 s
P rzeg ląd an ie od lewej,
p rz e g lą d an ie od praw ej
1 12 l< _R - A - I __£___ L E P U jr_ c X 0 s
Przestaw ianie 1 12 K C TT T - r L E P U i ~TT~q— r X 0 s
Przeg ląd an ie od lewej,
3 9 K C A T E i M Q R X 0 s
p rz e g lą d an ie od praw ej
Przestaw ianie 3 9 K c A I '" b " L E p ~lT' T M Q R X 0 5
P rzeg ląd an ie od lewej,
5 6 K c A I E L E p u T M Q R X 0 s
p rz e g lą d an ie od praw ej
—
Przestaw ianie 5 6 K c A I E E L p u T M Q R X 0 s
Przeg ląd an ie o d lewej,
p rz e g lą d an ie od praw ej
6 5 K—C _ I _JE_- E__ L p u T M Q R X 0 s
Końcowe p rzestaw ian ie 6 5 E~" i f A I È j< L p u T M Q R X 0 s
Wynik 5 E c A I E K L p u T M Q R X 0 s
Ślad przebiegu podziału (zawartość tablicy przed każdym przestawianiem i po nim)
304 RO ZD ZIA Ł 2 a Sortow anie
P odział w miejscu Podział m ożna łatwo zaimplementować przez zastosowanie do
datkowej tablicy, jednak nie jest to o tyle łatwiejsze, aby warto było ponosić dodat
kowy koszt kopiowania podzielonej wersji z powrotem do oryginału. Początkujący
programista Javy może nawet tworzyć w metodzie rekurencyjnej nową tablicę dla
każdego podziału, co bardzo spowalnia sortowanie.
Pozostawanie w granicach Jeśli elementem osiowym jest najmniejszy lub najwięk
szy element, trzeba zadbać o to, aby wskaźniki nie wyszły poza lewy lub prawy koniec
tablicy. Implementacja m etody p a rti t i on () obejmuje test zabezpieczający przed taką
sytuacją. Test (j == 1o) jest zbędny, ponieważ element osiowy znajduje się na pozycji
a [1 o] i nie jest mniejszy niż on sam. Stosując podobną technikę po prawej stronie,
można łatwo wyeliminować oba testy (zobacz ć w i c z e n i e 2 .3 .1 7 ).
Zachowanie losowości Mieszanie powoduje losowe uporządkowanie tablicy. Po
nieważ wszystkie elementy tablicy są traktowane w ten sam sposób, a l g o r y t m 2.5
ma tę właściwość, że dwie podtablice także mają losowe uporządkowanie. Jest to bar
dzo ważne ze względu na możliwość prognozowania czasu wykonania algorytmu.
Inny sposób na zachowanie losowości polega na wyborze losowego elementu osio
wego w metodzie p a r titio n ().
Kończenie pracy pętli Doświadczeni programiści wiedzą, że powinni zadbać o to,
aby każda pętla kończyła działanie. Pętla z podziałem dla sortowania szybkiego nie
jest tu wyjątkiem. Właściwe sprawdzenie, czy wskaźniki się przecięły, jest nieco tru d
niejsze, niż może się wydawać. Częstym błędem jest pominięcie tego, że tablica może
zawierać inne elementy o wartości klucza takiej samej, jak w elemencie osiowym.
Elementy z kluczam i równym i kluczowi elementu osiowego Najlepiej kończyć
przeglądanie lewej strony na kluczach większych lub równych względem klucza ele
m entu osiowego, a przeglądanie prawej strony — na kluczach mniejszych lub równych
względem klucza elementu osiowego, tak jak w a l g o r y t m i e 2 . 5 . Choć to podejście
na pozór powoduje niepotrzebne przestawienia elementów o kluczach równych klu
czowi elementu osiowego, niezwykle ważne jest to, aby unikać kwadratowego czasu
wykonania w pewnych typowych zastosowaniach (zobacz ć w i c z e n i e 2 .3 .1 1 ). Dalej
opisano lepszą strategię stosowaną w sytuacji, kiedy tablica zawiera dużą liczbę ele
mentów o równych kluczach.
Kończenie rekurencji Doświadczeni programiści wiedzą też, że należy starannie za
dbać o to, aby każda m etoda rekurencyjna kończyła działanie. Także pod tym wzglę
dem sortowanie szybkie nie jest wyjątkiem. Częstym błędem w implementacji sorto
wania szybkiego jest nieuwzględnienie tego, że jeden element zawsze jest umieszcza
ny na docelowym miejscu, i doprowadzenie do wejścia w nieskończoną rekurencyjną
pętlę, jeśli element osiowy jest największym lub najmniejszym elementem tablicy.
2.3 ■ Sortowanie szybkie 305
C e c h y z w ią z a n e z w y d a jn o ś c ią Sortowanie szybkie poddano wielu bardzo
szczegółowym analizom matematycznym, dlatego m ożna precyzyjnie opisać jego
wydajność. Analizy potwierdzono poprzez liczne doświadczenia empiryczne i są
przydatnym narzędziem w dopracowywaniu algorytmu pod kątem optymalnej wy
dajności.
Wewnętrzna pętla sortowania szybkiego (w metodzie dzielącej) zwiększa indeks
i porównuje element tablicy ze stałą wartością. Prostota to jeden z czynników spra
wiających, że ten sposób sortowania jest szybki. Trudno wyobrazić sobie krótszą pętlę
wewnętrzną algorytmu sortowania. Sortowanie przez scalanie i Shella są zwykle wol
niejsze od sortowania szybkiego, ponieważ w pętli wewnętrznej przenoszą elementy.
Drugi czynnik sprawiający, że metoda jest szybka, to mała liczba porównań.
Ostatecznie wydajność sortowania zależy od tego, jak dobry jest podział tablicy, a to
z kolei zależy od wartości klucza elementu osiowego. Losowo uporządkowana tablica
dzielona jest na dwie mniejsze losowo uporządkowane podtablice, przy czym miejsce
podziału (dla niepowtarzalnych kluczy) może znajdować się w dowolnym punkcie
tablicy. Dalej pokazano analizy algorytmu pozwalające ustalić, jaka jest wydajność
tego podejścia w porównaniu z idealnym rozwiązaniem.
Najlepszym przypadkiem dla sortowania szybkiego jest sytuacja, w której każdy
podział rozbija tablicę dokładnie na dwie połowy. Wtedy liczba porównań w sor
towaniu szybkim odpowiada zależności rekurencyjnej z podejścia dziel i zwyciężaj
— CN = 2CNn + N. Wyraz 2CNn to koszt sortowania dwóch podtablic, a N to koszt
sprawdzenia każdego elementu za pomocą jednego lub drugiego indeksu służącego
do przeglądania tablicy. Tak jak w dowodzie t w i e r d z e n i a f (dla sortowania przez
scalanie) wiadomo, że rekurencja ma rozwiązanie o złożoności CN ~ N lg N. Choć
program nie zawsze działa tak dobrze, prawdą jest, że podział średnio wypada w po
łowie. Uwzględnianie dokładnego prawdopodobieństwa miejsca każdego podziału
komplikuje rekurencję i utrudnia rozwiązanie problemu, jednak ostateczny wynik jest
taki sam. Dowód sprawia, że można mieć pewność co do skuteczności sortowania
szybkiego. Jeśli nie jesteś zainteresowany matematyką, możesz pominąć dowód (i za
ufać nam). Jeżeli matematyka Cię ciekawi, dowód może wydać Ci się intrygujący.
Twierdzenie K. Sortowanie szybkie średnio wykonuje ~ 2N In N porównań
(i sześć razy mniej przestawień) przy sortowaniu tablicy o długości N i niepo
wtarzalnych kluczach.
Dowód. Niech CN to średnia liczba porównań potrzebnych do posortowania
N elementów o różnych wartościach. C() = C 1 = 0, a dla N > 1 można napisać za
leżność rekurencyjną, która jest bezpośrednim odwzorowaniem rekurencyjnego
programu:
306 R O ZD ZIA Ł 2 o Sortowanie
CN= N + 1 + (C 0 + Cj + ... + CN2 + Cn1) / N + (CN1 + CN2 + ... + Cg)/N
Pierwszy wyraz to koszt podziału (zawsze równy N + 1), drugi to średni koszt
sortowania lewej podtablicy (która może mieć dowolny rozmiar od 0 do N - 1),
a trzeci to średni koszt dla prawej podtablicy (taki sam, jak dla lewej podtablicy).
Po pom nożeniu przez N i wyciągnięciu wspólnego czynnika przed nawias uzy
skujemy równanie:
NC n = N (N + 1) + 2 (C0 + C, + ... + CN.2 + Cm )
Po odjęciu tej wartości od podobnego równania dla N - 1 otrzymujemy:
N C ^ iN -D C ^ ^ lN + lC ^
Po uporządkowaniu wyrazów i podzieleniu przez N {N + 1) mamy:
Cn/ ( N+ 1 ) = C J N + 2 / ( N + 1 )
co można skrócić do wyniku:
CN~ 2{N+ 1 )( 1/3 + V* + ... + 1 /(JV+ 1 ))
Wartość w nawiasach to 1 plus szacunkowa wartość obszaru pod krzywą 2lx
z przedziału od 3 do N, a przez całkowanie uzyskujemy CN ~ 2N ln N. Zauważmy,
że 2 M ln N ~ l,39N lg N, tak więc średnia liczba porównań jest tylko o około 39%
większa niż dla najlepszego przypadku.
Podobne (choć dużo bardziej skomplikowane) analizy są potrzebne do uzy
skania podanego wyniku dla liczby przestawień.
Jeśli klucze mogą być równe, co jest typowe w praktycznych zastosowaniach, pre
cyzyjne analizy są dużo bardziej skomplikowane, jednak nietrudno wykazać, że
średnia liczba porównań jest nie większa niż CN nawet przy powtarzających się
kluczach (na stronie 308 opisano sposób na usprawnienie sortowania szybkiego
w takiej sytuacji).
Mimo wielu zalet podstawowe sortowanie szybkie ma jedną potencjalną wadę
— może być niezwykle niewydajne, jeśli podziały są niezrównoważone. Pierwszy po
dział może być oparty na najmniejszym elemencie, drugi — na kolejnym najm niej
szym i tak dalej, dlatego program w każdym wywołaniu usuwa tylko jeden element,
co prowadzi do zbyt dużej liczby podziałów długich podtablic. Losowe mieszanie
tablicy przed sortowaniem szybkim służy właśnie uniknięciu takiej sytuacji. Operacja
ta sprawia, że niekorzystne podziały są tak mało prawdopodobne, że nie trzeba się
nimi przejmować.
2.3 a Sortowanie szybkie 307
Twierdzenie L. Sortowanie szybkie wykonuje dla najgorszego przypadku ~
AP/2 porównań, jednak losowe mieszanie zabezpiecza przed taką sytuacją.
Dowód. Zgodnie z przedstawionym wcześniej dowodem liczba porównań po
trzebnych, kiedy jedna z podtablic jest pusta, wynosi dla każdego podziału:
N + ( N - 1) + ( N - 2) + ... + 2 + 1 = ( N+ 1) N / 2
Oznacza to nie tylko tyle, że czas rośnie kwadratowo, ale też to, iż pamięć po
trzebna na rekurencyjne obliczenia rośnie liniowo, co dla dużych tablic jest nie-
akceptowalne. Jednak (pewnym nakładem pracy) można rozwinąć analizy prze
prowadzone dla średniej w celu ustalenia, że odchylenie standardowe dla liczby
porównań wynosi 0,65 N, dlatego czas wykonania wraz z rosnącym N dąży do
średniej i prawdopodobnie nie będzie od niej znacznie oddalony. Na przykład
nawet zgrubne szacunki oparte na nierówności Czebyszewa są dowodem na to,
że dla tablicy o milionie elementów prawdopodobieństwo tego, iż czas wykona
nia 10 -krotnie przekroczy średnią, jest mniejsze niż 0,00001 (a w rzeczywistości
prawdopodobieństwo to jest znacznie mniejsze). Prawdopodobieństwo, że czas
wykonania dla dużej tablicy będzie bliski kwadratowemu, jest tak niskie, że moż
na bezpiecznie pominąć tę możliwość (zobacz ć w i c z e n i e 2 .3 .1 0 ). Przykładowo,
prawdopodobieństwo, że dla dużej tablicy sortowanie szybkie będzie wymagać
na Twoim komputerze tylu porównań, co sortowanie przez wstawianie lub wy
bieranie, jest znacznie mniejsze niż prawdopodobieństwo, iż w trakcie sortowa
nia komputer zostanie trafiony przez błyskawicę!
p o d s u m u j m y — można mieć pewność, że czas wykonania a l g o r y t m u 2.5 będzie
różnił się o stałą od 1,39 N l g N przy sortowaniu N elementów. To samo dotyczy sor
towania przez scalanie, jednak sortowanie szybkie jest zwykle szybsze (mimo liczby
porównań większej o 39% procent), ponieważ obejmuje znacznie mniej przestawień
danych. Ta matematyczna gwarancja jest probabilistyczna, jednak z pewnością m oż
na na niej polegać.
U s p r a w n ie n ia a lg o r y tm u Sortowanie szybkie wymyślił w 1960 roku C.A.R.
Hoare. Od tego czasu wiele osób przebadało i usprawniło tę technikę. Kusząca jest
myśl o ulepszeniu sortowania szybkiego. Szybszy algorytm sortowania to „lepsza wer
sja dobrego” w dziedzinie nauk komputerowych, a sortowanie szybkie to zasłużona
metoda, która zachęca do wymyślania modyfikacji. Propozycje ulepszenia algoryt
mu zaczęły się pojawiać niemal od razu po opublikowaniu algorytmu przez Hoarea.
Nie wszystkie rozwiązania były udane, ponieważ algorytm jest tak zrównoważony, że
korzyści wynikające z usprawnień mogą zostać z naddatkiem zniwelowane przez nie
oczekiwane efekty uboczne. Jednak kilka pomysłów, które omawiamy dalej, okazało
się całkiem skutecznych.
308 R O ZD ZIA Ł 2 o Sortowanie
Jeśli kod sortujący ma być stosowany wielokrotnie lub służy do sortowania dużych
tablic (a zwłaszcza jeżeli ma pełnić funkcję sortowania bibliotecznego, stosowanego
do tablic o nieznanych cechach), warto zastanowić się nad usprawnieniami opisany
mi w kilku następnych akapitach. Jak wspomniano, trzeba przeprowadzić ekspery
menty, aby określić skuteczność usprawnień i ustalić param etry optymalne dla im
plementacji. Zwykle możliwe jest uzyskanie poprawy od 20 do 30%.
Przełączanie na sortowanie p rzez wstawianie Wydajność sortowania szybkiego,
podobnie jak większości algorytmów rekurencyjnych, można łatwo zwiększyć na
podstawie dwóch następujących obserwacji:
■ Dla małych podtablic sortowanie szybkie jest wolniejsze niż sortowanie przez
wstawianie.
■ M etoda s o rt() w sortowaniu szybkim jest rekurencyjna, dlatego może wywo
ływać samą siebie dla małych podtablic.
Dlatego dla małych podtablic warto zastąpić sortowanie szybkie sortowaniem przez
wstawianie. Prosta modyfikacja a l g o r y t m u 2.5 pozwala zastosować to usprawnie
nie. W metodzie s o rt() należy zastąpić instrukcję:
i f (hi <= lo) return;
instrukcją, która dla małych podtablic wywołuje sortowanie przez wstawianie:
i f (hi <= lo + M) { In s e r t i o n . s o r t ( a , lo, h i) ; return; }
Optymalna wartość przełączenia (M) zależy od systemu, jednak w większości sytuacji
sprawdza się dowolna wartość z przedziału od 5 do 15 (zobacz ć w i c z e n i e 2 .3 . 25 ).
Podział w miejscu m ediany trzech elem entów Drugi łatwy sposób na poprawę
wydajności sortowania szybkiego to użycie jako elementu osiowego mediany małej
próbki elementów pobranych z podtablicy. Rozwiązanie te zapewnia nieco lepszy p o
dział, jednak dzieje się to kosztem obliczania mediany. Okazuje się, że większa część
możliwej poprawy wynika z wyboru próbki o wielkości 3 oraz podziału w miejscu
środkowego elementu (zobacz ć w i c z e n i a 2 .3.18 i 2 . 3 . 1 9 ). Dodatkowo m ożna użyć
przykładowych elementów jako wartowników na końcach tablicy i usunąć oba testy
granic tablicy w metodzie p art i t i on ().
Sortowanie optymalne ze względu na entropię W praktyce często występują tablice
o dużej liczbie powtarzających się kluczy. Można na przykład sortować duży plik z da
nymi personelu według roku urodzenia lub w celu oddzielenia mężczyzn od kobiet.
W takiej sytuacji opisana implementacja sortowania szybkiego ma akceptowalną wy
dajność, jednak można ją znacznie poprawić. Przykładowo, podtablicy składającej się
tylko z równych sobie elementów (o jednej wartości klucza) nie trzeba dłużej przetwa
rzać, jednak implementacja nadal dzieli dane na mniejsze podtablice. Jeśli w wejściowej
tablicy istnieje duża liczba powtarzających się kluczy, rekurencyjna natura sortowania
szybkiego sprawia, że podtablice składające się wyłącznie z elementów o równych klu
czach będą często występować. Możliwe jest znaczące usprawnienie — z wydajności
liniowo-logarytmicznej (osiągniętej do tej pory) do wydajności liniowej.
2.3 □ Sortowanie szybkie 309
Lewa tab lica je st
częściow o p o so rto w a n a
O bie p o d ta b lic e są
częściow o p o so rto w a n e
Wynik
Sortowanie szybkie z podziałem w miejscu mediany trzech
elementów i przełączeniem metody dla krótkich podtablic
310 RO ZD ZIA Ł 2 ■ Sortowanie
Prosta technika polega na podziale tablicy na trzy części — po jednej na elemen
ty o kluczu mniejszym, równym i większym względem klucza elementu osiowego.
Utworzenie takiego podziału jest bardziej skomplikowane niż stosowanego wcześ
niej podziału na dwie części. Zaproponowano różne sposoby wykonania tego zada
nia. Zadanie to było klasycznym ćwiczeniem programistycznym spopularyzowanym
przez E.W. Dijkstrę jako problem holenderskiej flagi, ponieważ proces przypomina
sortowanie tablicy o trzech możliwych wartościach klucza, które mogą odpowiadać
trzem kolorom flagi.
Rozwiązanie Dijkstry dla tego problemu to niezwykle prosty kod do przeprowa
dzania podziału. Kod ten pokazano na następnej stronie. Rozwiązanie oparto na
jednym przejściu przez tablicę od lewej do prawej, przy czym przechowywany jest
wskaźnik 1 1 , dla którego a [1 o .. 1 1 - 1 ] są mniejsze niż v, wskaźnik gt, taki że a[g t+ l,
hi] są większe niż v, i wskaźnik i, dla którego a [ 1 1 . . i - 1 ] są równe v, a a [ i . .g t] nie
są jeszcze sprawdzone. Początkowo i jest równe lo. Należy przetworzyć a [i] , sto
sując porównania trójwartościowe dostępne poprzez interfejs Comparable (zamiast
używać 1 ess ()), co pozwala bezpośrednio obsłużyć trzy możliwe przypadki:
■ Element a [i ] jest mniejszy niż v — trzeba przestawić a [1 1 ] i a [i ] oraz zwięk
szyć 1 1 oraz i .
■ Element a [i] jest większy niż v — trzeba przestawić a [i] i a [gt] oraz zmniej
szyć gt.
■ Element a [i ] jest równy v — należy zwiększyć i .
Każda z tych operacji zarówno zachowuje niezmiennik, jak i zmniejsza wartość g t-i
(dlatego pętla zakończy działanie). Ponadto napotkanie prawie każdego elementu
prowadzi do przestawienia (wyjątkiem są elementy o kluczu równym kluczowi ele
mentu osiowego).
Choć omawiany kod wymyślono Przed
niedługo po wymyśleniu sortowania I t
lo hi
szybkiego, w latach 70. ubiegłego wie
ku, przestano z niego korzystać, ponie W trakcie <v = v r Hi Í T I » >v
ł ł ł
waż w standardowym przypadku, kiedy lt i gt
liczba powtarzających się kluczy nie jest Po <v =v >v
duża, wymagał znacznie więcej przesta I t t )
wień niż standardowa m etoda podziału lo lt gt hi
na dwie części. W latach 90. ubiegłe Podział na trzy części
go wieku J. Bentley i D. Mcllroy opra
cowali pomysłową implementację, która pozwala przezwyciężyć problem (zobacz
ć w i c z e n i e 2 .3 .22 ), i zaobserwowali, że podział na trzy części sprawia, iż w prakty
ce (nawet dla dużej liczby równych kluczy) sortowanie szybkie jest asymptotycznie
szybsze niż sortowanie przez scalanie i inne metody. Później J. Bentley i R. Sedgewick
udowodnili tę obserwację, co opisano dalej.
Udowodniono jednak, że sortowanie przez scalanie jest optymalne. Jak udało się
przekroczyć to dolne ograniczenie? Oto odpowiedź: t w i e r d z e n i e i z p o d r o z d z i a ł u
2.2 dotyczy wydajności dla najgorszego przypadku dla wszystkich możliwych da-
2.3 Sortowanie szybkie 311
Sortowanie szybkie z podziałem na trzy części
public c la s s Quick3way
{
private s t a t ic void sort(Comparable[] a, in t lo, in t hi)
{ // Publiczna metoda s o r t ( ) wywołująca tę metodę znajduje s ię na
// stro n ie 301.
i f (hi <= lo) return;
in t l t = lo, i = 1o + l , gt = hi;
Comparable v = a [1 o] ;
while (i <= gt)
{
in t cmp = a [ i ] .compareTo(v);
if (cmp < 0) exch(a, lt++, i++);
else i f (cmp > 0) exch(a, i, g t--);
else i++;
} // Teraz a [1 o . .11-1] < v = a [11 . . gt] < a [g t+ 1 . . h i].
s o rt (a , lo, l t - 1);
s o rt (a , gt + 1, h i ) ;
}
}
Ten kod dzieli tablicę, aby umieścić klucze równe elementowi osiowemu na docelowych po
zycjach, przez co nie trzeba uwzględniać tych kluczy w podtablicach w wywołaniach reku-
rencyjnych. Dla tablic o dużej liczbie powtarzających się kluczy jest to znacznie wydajniejsze
niż w standardowej implementacji sortowania szybkiego (zobacz opis w tekście).
V a[]
lt i gt \ o 1 2 3 4 5 6 7 8 9 10 11
0 0 11 R B W W R W B R R w B R
0 1 11 R B W w R W B R R w B R
1 2 11 B R W —w---- R__M___ B R R _JW — B— R
1 2 10 B R R — W-- R W B R R W B ■W
1 3 10 B R R W- -W___ B_ — w- B W
1 3 9 B R -,R, B — R " TflT~ B 1 T~ R T T " W W
2 4 9 B B R 'R R W B R R w w w
2 5 9 B B R R R W ■— - W w w
2 5 8 B B R R R w r:" w w w
2 5 7 B E R R R R .B R w w w w
2 6 7 B B R R B R w w w w
3 7 7 B B B —R R R'- R R w w w w
3 8 7 B B B R R R R R w w w w
3 8 7 B B B R R R R R w w w w
Ślad podziału na trzy części (zawartość tablicy po każdej Iteracji pętli)
312 RO ZD ZIA Ł 2 ■ Sortowanie
nil [Link]
iiiiBSiiiiliiii>inhiiiiiiiiiiiiogiDQIQBI9IBHI0101010000020000010002020002
... ¡DOI
■ .
i mu
p ń iA /rtP p lp m p n t n if l/ i n ę ir n A / p m il ^
Illllllllllllllllllllllllllllllllllllllllllllllllllllll
n nn n n □ o n a n nnn n n
905348235353485348532353235348483048010102234802
■g g a s s i n i a i l i l S _ _ _ _ _ _ _ _ _ _ _ _ _ _ ____ _________________________ . . . . . ___
.......................urn.....Illllllllllllllllllllllllllllllllllllllllllllllllllllll
i i i lllllllllllllllllllllllllllllllllllllllllllllllllllllll
iiimiiimiiiiHimlllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
Wizualny ślad sortowania szybkiego z podziałem na trzy części
nych wejściowych, natom iast teraz w ażna jest w ydajność dla najgorszego przypadku
z uw zględnieniem pew nych inform acji o w artościach kluczy. Sortow anie przez sca
lanie nie gw arantuje optym alnej w ydajności dla dowolnego układu pow tarzających
się kluczy w danych wejściowych. Technika ta jest liniow o-logarytm iczna dla losowo
uporządkow anej tablicy zawierającej stałą liczbę niepow tarzalnych w artości kluczy,
natom iast sortow anie szybkie z podziałem na 3 części jest dla takiej tablicy liniowe.
Patrząc na w izualny ślad przedstaw iony powyżej, m ożna zauważyć, że N razy liczba
w artości kluczy to konserw atyw ne ograniczenie czasu w ykonania.
W analizach precyzujących te kwestie uw zględniono rozkład w artości kluczy. Dla
N kluczy o k różnych w artościach dla każdego i od 1 do A: zdefiniow ano^ jako liczbę
w ystąpień i-tej w artości klucza, a p. jako f . I N , czyli praw dopodobieństw o, że i-ta
w artość klucza zostanie znaleziona po w ybraniu losowego elem entu tablicy. Entropia
Shannona dla kluczy (klasyczna m iara ilości inform acji) wynosi:
H = - (P, lg P , + Pi lg P 2 + - + Pk lg P *)
D la dowolnej tablicy sortow anych elem entów m ożna określić entropię, licząc w y
stąpienia poszczególnych w artości kluczy. Co ciekawe, na podstaw ie entropii m ożna
też określić zarów no dolne, jak i górne ograniczenie liczby porów nań potrzebnych
w sortow aniu szybkim z podziałem na trzy części.
Twierdzenie M. Żaden algorytm sortowania oparty na porównaniach nie gwaran
tuje posortow ania N elementów za pom ocą mniej niż N H - N porównań, gdzie H to
entropia Shannona zdefiniowana na podstawie liczby wystąpień wartości kluczy.
Zarys dowodu. W ynika to ze (stosunkow o prostego) uogólnienia dow odu na
dolne ograniczenie z t w ie r d z e n ia i z po d r o z d z ia ł u 2 .2 .
2.3 □ Sortowanie szybkie 313
Twierdzenie N. Sortowanie szybkie z podziałem na trzy części wymaga ~ (2ln 2)
N H porównań przy sortowaniu N elementów, gdzie H to entropia Shannona
zdefiniowana na podstawie liczby wystąpień wartości kluczy.
Zarys dowodu. Wynika to ze (stosunkowo trudnego) uogólnienia analiz dla
t w ie r d z e n ia k , dotyczących działania sortowania szybkiego dla typowego
przypadku. Tak jak dla różnych kluczy, tak i tu koszty są około 39% wyższe niż
w optymalnym rozwiązaniu (wydajność różni się jednak tylko o stały czynnik).
Zauważmy, że jeśli wszystkie klucze są inne (prawdopodobieństwo natrafienia na
dowolny to 1/N), H = lg N. Jest to zgodne z tw ie r d z e n ie m i z p o d r o z d z ia łu 2.2
i tw ie r d z e n ie m k. Najgorszy przypadek dla podziału na trzy części to sytuacja,
w której wszystkie klucze są inne. Jeśli klucze się powtarzają, rozwiązanie to może
być znacznie wydajniejsze od sortowania przez scalanie. Co ważniejsze, obie opisane
cechy sprawiają, że sortowanie szybkie z podziałem na trzy części jest optymalne ze
względu na entropię — w tym sensie, że średnia liczba porównań używanych przez
najlepszy możliwy algorytm sortowania oparty na porównaniach i średnia liczba p o
równań w sortowaniu szybkim z podziałem na trzy części różnią się stałym czynni
kiem dla dowolnego układu wartości kluczy.
Tak jak w standardowym sortowaniu szybkim, tak i tu czas wykonania dąży do śred
niej wraz ze wzrostem wielkości tablicy. Duże odchylenia od średniej są niezwykle rzad
kie, dlatego można przyjąć, że czas wykonania sortowania szybkiego z podziałem na trzy
części będzie proporcjonalny do N razy entropia rozkładu wartości kluczy. Ta cecha al
gorytmu jest ważna w praktyce, ponieważ oznacza skrócenie czasu sortowania z liniowo-
logarytmicznego do liniowego dla tablic o dużej liczbie powtarzających się kluczy. Kolejność
kluczy nie ma znaczenia, ponieważ algorytm miesza je, co zabezpiecza przed najgorszym
przypadkiem. Rozkład kluczy wyznacza entropię, a każdy algorytm oparty na porów
naniach wymaga nie mniej porównań, niż określa to entropia. Dostosowywanie się do
powtórzeń w danych wejściowych sprawia, że sortowanie szybkie z podziałem na trzy
części to algorytm używany z wyboru do sortowania w bibliotekach. Klienty sortujące
tablice o dużej liczbie powtarzających się kluczy nie należą do rzadkości.
st a r a n n ie d o pra c o w a n a w e r s ja sortowania szybkiego na większości komputerów
działa zwykle znacznie szybciej niż jakakolwiek inna metoda sortowania oparta na
porównaniach. Sortowanie szybkie jest powszechnie stosowane we współczesnej in
frastrukturze informatycznej, ponieważ omówione modele matematyczne sugerują, że
w praktycznych zastosowaniach metoda ta jest wydajniejsza od innych, a rozbudowane
eksperymenty i doświadczenie zebrane przez ostatnie dziesięciolecia to potwierdzają.
W r o z d z ia l e 5 . pokazano, że historia rozwijania algorytmów sortowania na tym
się nie kończy. Można opracować algorytmy, które w ogóle nie wymagają porównań!
Jednak pewna wersja sortowania szybkiego okazuje się najlepsza także w tym kon
tekście.
314 RO ZD ZIA Ł 2 * Sortow anie
PYTANIA I ODPOWIEDZI
P. Czy istnieje sposób na taki podział tablicy na dwie połowy, aby nie robić tego
w przypadkowym miejscu wyznaczanym przez element osiowy?
O. Eksperci głowią się nad tym pytaniem od lat. Problem sprowadza się do znalezie
nia mediany wśród wartości kluczy z tablicy i przeprowadzenia podziału na podstawie
tej wartości. Problem znajdowania mediany opisano na stronie 358. Operację można
przeprowadzić w czasie liniowym, jednak koszt wykonania jej za pomocą znanych al
gorytmów (opartych na podziale z sortowania szybkiego!) znacznie przekracza 39%
oszczędności, jakie można uzyskać przez podział tablicy na równe części.
P. Mam wrażenie, że losowe mieszanie tablicy zajmuje istotną część czasu potrzeb
nego na sortowanie. Czy naprawdę warto to robić?
O. Tak. Zabezpiecza to przed najgorszym przypadkiem i powoduje, że czas wyko
nania jest przewidywalny. Hoare zaproponował to podejście w ramach prezentacji
algorytmu w 1960 roku. Jest to prototypowy (i jeden z pierwszych) algorytm z ran-
domizacją.
P. Dlaczego poświęca się tyle uwagi elementom o równych kluczach?
O. Zagadnienie to w praktyce bezpośrednio wpływa na wydajność. Wiele osób
pomijało je przez dziesięciolecia. Efekt jest taki, że niektóre starsze implementacje
sortowania szybkiego działają w czasie kwadratowym dla tablic o dużej liczbie ele
mentów o równych kluczach. Tablice takie, oczywiście, występują w praktyce. Lepsze
implementacje, takie jak a l g o r y t m 2 .5 , działają dla takich tablic w czasie liniowo-
logarytmicznym. Jednak w wielu sytuacjach warto skrócić ten czas do liniowego, tak
jak w sortowaniu optymalnym ze względu na entropię.
2.3 o Sortowanie szybkie 315
ĆWICZENIA
2.3.1. Pokaż, za pomocą śladu podobnego do śladu użytego dla m etody p a r ti -
ti on (), jak m etoda ta dzieli tablicę E A S Y Q U E S T I O N .
2.3.2. Pokaż, za pomocą śladu podobnego do śladu użytego dla sortowania szybkiego
w tym podrozdziale, jak sortowanie szybkie sortuje tablicę E A S Y Q U E S T I O N .
W tym ćwiczeniu pom iń początkowe mieszanie.
2.3.3. Jaka jest maksymalna liczba przestawień największego elementu tablicy o dłu
gości N w czasie wykonywania m etody Qui ck. so rt () ?
2.3.4. Załóżmy, że pominięto początkowe losowe mieszanie. Podaj sześć 10-ele-
mentowych tablic, dla których metoda Quick.s o r t () musi wykonać taką liczbę p o
równań, jak dla najgorszego przypadku.
2.3.5. Podaj fragment kodu do sortowania tablicy, o której wiadomo, że klucze jej
elementów mają tylko dwie różne wartości.
2.3.6. Napisz program do obliczania dokładnej wartości C . Porównaj dokładny
wynik z przybliżeniem 2M n N dla N = 100, 1000 i 10 000.
2.3.7. Znajdź oczekiwaną liczbę podtablic o wielkości 0, 1 i 2 przy używaniu sor
towania szybkiego do sortowania tablicy o N elementach z różnymi kluczami. Jeśli
masz odpowiednią wiedzę matematyczną, przeprowadź obliczenia; w przeciwnym
razie przeprowadź eksperymenty, aby sformułować hipotezę.
2.3.8. Ile mniej więcej porównań wykonuje m etoda Quick, s o rt () przy sortowaniu
tablicy o N elementach, z których każdy ma tę samą wartość?
2.3.9. Wyjaśnij, co dzieje się po uruchom ieniu metody Quick.s o rt() dla tablicy
z tylko dwoma różnymi kluczami. Następnie wytłumacz, co dzieje się po uruchom ie
niu metody dla tablicy o trzech różnych kluczach.
2.3.10. Zgodnie z nierównością Czebyszewa prawdopodobieństwo tego, że losowa
zmienna będzie oddalona o więcej niż k odchyleń standardowych od średniej, jest
mniejsze niż l/k 2. Dla N = milion użyj nierówności Czebyszewa do ograniczenia
prawdopodobieństwa, że liczba porównań w sortowaniu szybkim będzie mniejsza
niż 100 miliardów (czyli 0,1 N2).
2.3.11. Załóżmy, że program pomija elementy o kluczach równych kluczowi ele
mentu osiowego, zamiast kończyć przeglądanie po ich napotkaniu. Wykaż, że czas
wykonania dla tej wersji sortowania szybkiego jest kwadratowy dla wszystkich tablic
o stałej liczbie różnych kluczy.
316 RO ZD ZIA Ł 2 □ Sortow anie
ĆWICZENIA (ciąg dalszy)
2.3.12. Pokaż, za pomocą śladu podobnego do śladu użytego dla kodu w tekście, jak
sortowanie optymalne ze względu na entropię podzieli początkowo tablicę B A B A B
ABACADABRA.
2.3.13. Jaka jest głębokość rekurencji w sortowaniu szybkim dla najlepszego, najgor
szego i typowego przypadku? Odpowiada ona rozmiarowi stosu potrzebnego przez
system do śledzenia rekurencyjnych wywołań. W ć w i c z e n i u 2 .3.20 znajdziesz spo
sób na zagwarantowanie, że głębokość rekurencji rośnie logarytmicznie dla najgor
szego przypadku.
2.3.14. Udowodnij, że przy stosowaniu sortowania szybkiego dla tablicy o N róż
nych elementach prawdopodobieństwo porównania i-tego oraz j -tego najwięk
szego elementu wynosi 2/(j - i). Następnie wykorzystaj wynik do udowodnienia
T W IE R D Z E N IA K.
2.3 o Sortowanie szybkie
[ p r o b l e m y d o r o z w ią z a n ia
2.3.15. Nakrętki i śruby (autor — G.J.E. Rawlins). Masz wymieszaną stertę N nakrę
tek i N śrub. Musisz szybko znaleźć pasujące do siebie pary nakrętek i śrub. Każda
nakrętka pasuje do dokładnie jednej śruby, a każda śruba pasuje do dokładnie jednej
nakrętki. Sprawdzając nakrętkę i śrubę, możesz stwierdzić, która część jest większa,
nie można jednak bezpośrednio porównać dwóch nakrętek lub śrub. Przedstaw wy
dajną metodę rozwiązania problemu.
2.3.16. Najlepszy przypadek. Napisz program, który generuje tablicę dla najlepszego
przypadku (wolną od powtórzeń) dla m etody s o rt() z a l g o r y t m u 2 .5 . Ma to być
tablica N elementów o różnych kluczach i cechująca się tym, że każdy podział daje
podtablice różniące się rozmiarem o najwyżej jeden element (ich wielkości mają być
takie same, jak dla tablicy o N równych kluczach). W tym ćwiczeniu pom iń począt
kowe mieszanie.
Dalsze ćwiczenia dotyczą odmian sortowania szybkiego. Każda wersja wymaga imple
mentacji, przy czym oczywiście warto użyć też programu SortCompare do eksperymen
tów w celu oceny skuteczności każdej proponowanej modyfikacji.
2.3.17. Wersja z wartownikami. Zmodyfikuj kod a l g o r y t m u 2 .5 , aby usunąć oba
testy granic w wewnętrznych pętlach whi 1e. Test lewego końca podtablicy jest zbęd
ny, ponieważ element osiowy jest wartownikiem (v nigdy nie jest mniejsza niż a [1 o ]).
Aby umożliwić usunięcie drugiego testu, bezpośrednio po mieszaniu umieść element
mający największy klucz w tablicy na pozycji a [le n g th -l]. Element ten nigdy nie
zmieni pozycji (chyba że zostanie przestawiony z elementem o identycznym kluczu)
i posłuży za wartownika we wszystldch podtablicach obejmujących koniec tablicy.
Uwaga: przy sortowaniu wewnętrznych podtablic lewy element podtablicy znajdują
cej się po prawej służy za wartownika na prawym krańcu danej podtablicy.
2.3.1 8 . Podział z medianą spośród trzech elementów. Dodaj do sortowania szybkiego
podział z m edianą spośród trzech elementów, jak opisano to w tekście (zobacz stronę
308). Przeprowadź testy podwajania, aby ustalić skuteczność zmiany.
2.3.19. Podział z medianą spośród pięciu elementów. Zaimplementuj sortowanie
szybkie oparte na podziale według mediany spośród losowej próbki pięciu elemen
tów podtablicy. Umieść elementy z próbld w odpowiednich końcach tablicy, tak aby
tylko mediana była uwzględniana w trakcie podziału. Przeprowadź testy podwajania
w celu określenia skuteczności zmiany. Porównaj opisaną technikę ze standardowym
algorytmem i z rozwiązaniem z podziałem według mediany spośród trzech elemen
tów (zobacz poprzednie ćwiczenie). Dodatkowe zadanie: opracuj oparty na medianie
spośród pięciu elementów algorytm wymagający mniej niż siedmiu porównań dla
dowolnych danych wejściowych.
318 R O ZD ZIA Ł 2 ■ Sortowanie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
2.3.20. Nierekurencyjne sortowanie szybkie. Zaimplementuj nierekurencyjną wersję
sortowania szybkiego, opartą na pętli głównej, w której podtablica jest zdejmowana
ze stosu w celu posortowania, a wynikowe podtablice są z powrotem dokładane do
stosu. Uwaga: najpierw umieść na stosie większą z podtablic, co gwarantuje, że na
stosie będzie znajdować się najwyżej lg N elementów.
2.3.21. Dolne ograniczenie przy sortowaniu tablic o równych kluczach. Dokończ
pierwszą część dowodu t w i e r d z e n i a m, stosując wnioskowanie z dowodu
t w i e r d z e n i a i i wykorzystując spostrzeżenie, że istnieje N\ / f {\...f0\ f k\ różnych spo
sobów na uporządkowanie kluczy o k różnych w artościach, gdzie i-ta wartość
występuje^! razy (= Np. w notacji z t w i e r d z e n i a m ), przy czym /)+...+/t = N.
2.3.22. Szybki podział na trzy części (autorzy — }. BentleyiD. Mcllroy). Zaimplementuj
sortowanie optymalne ze względu na entropię, oparte na przechowywaniu elementów
o równych kluczach po lewej i prawej stronie
Przed podtablicy. Przechowuj indeksy p i q, takie że
1 ł
lo hi a [ lo ..p - l] ia [ q + l..h i] są równe a [1 o], indeks
i, talu że a[p . . i - 1 ] są mniejsze od a [1 ], oraz
W trakcie _____ IK S ! = V indeks j, taki że a [j+ 1 .. q] są większe od a [1 o].
ł ł t t t ł
lo p i j q hi Do wewnętrznej pętli dzielącej dodaj kod, który
<v =v >v przed standardowymi porównaniami a [i ] i a [j]
ł t z v przestawia a [i ] z a [p] (i zwiększa p), jeśli a [i ]
lo hi
jest równy v, i przestawia a [j] z a [q] (i zmniej
P o d z ia ł n a trz y części m e to d ą B en tley a-M cllro y a sza q), jeżeli a [j] jest równy v. Po zakończeniu
działania pętli dzielącej należy uruchomić kod
ustawiający elementy o równych kluczach na odpowiednich pozycjach. Uwaga: kod ten
to uzupełnienie kodu przedstawionego w tekście w tym sensie, że wykonuje dodatkowe
przestawienia kluczy równych kluczowi elementu osiowego, podczas gdy kod z tekstu
dodatkowo przestawia klucze, które nie są równe kluczowi elementu osiowego.
2.3.23. Sortowanie systemowe Javy. Do implementacji z ć w i c z e n ia 2 .3.22 dodaj
kod z wykorzystaniem mediany Tukeya do obliczenia elementu osiowego. Należy
wybrać trzy grupy po trzy elementy, ustalić medianę w każdej z nich, a następnie za
stosować medianę trzech median jako element osiowy. Ponadto zastosuj przełączenie
do sortowania przez wstawianie dla małych podtablic.
2.3.24. Sortowanie próbkowe (autorzy — W. Frazer i A. McKellar). Zaimplementuj
sortowanie szybkie oparte na próbkach o wielkości 2k - 1. Najpierw należy posorto
wać próbkę, a następnie w rekurencyjnej metodzie dzielić tablicę według mediany
próbki i przenosić obie połowy reszty próbki do każdej podtablicy, tak aby można je
wykorzystać w podtablicach bez konieczności ponownego sortowania. Algorytm ten
nosi nazwę sortowania próbkowego.
2.3 ■ Sortowanie szybkie 319
[ eksperym enty
2.3.25. Przełączenie do sortowania przez wstawianie. Zaimplementuj sortowanie
szybkie z przełączeniem do sortowania przez wstawianie dla tablic o mniej niż M
elementach. Empirycznie ustal wartość M, dla której sortowanie szybkie działa naj
szybciej w Twoim środowisku obliczeniowym przy sortowaniu losowych tablic N
liczb typu doubl e dla N = 103, 104, 105 i 106. Program ma rysować wykres ze średni
mi czasami wykonania dla każdej wartości M od 0 do 30. Uwaga: musisz dodać do
a l g o r y t m u 2 .2 trzyargumentową metodę s o rt() do sortowania podtablic, tak aby
wywołanie I n s e rtio n .s o rt(a , lo , hi) sortowało podtablicę a [1 o. . h i ] .
2.3.26. Rozmiary podtablic. Napisz program generujący histogram z wielkościami
podtablic sortowanych przez wstawianie przy stosowaniu sortowania szybkiego dla
tablicy o rozmiarze N z przełączeniem metody dla podtablic o wielkości poniżej M.
Uruchom program dla M = 10, 20 i 50 oraz N = 105.
2.3.27. Ignorowanie małych podtablic. Przeprowadź eksperymenty, aby porównać
opisaną tu strategię radzenia sobie z małymi podtablicami z podejściem omówionym
w ć w i c z e n i u 2 .3 . 2 5 . Zignoruj małe podtablicę w sortowaniu szybkim, a następnie
uruchom sortowanie przez wstawianie. Uwaga: za pomocą eksperymentu tego ro
dzaju będziesz mógł oszacować wielkość bufora komputera, ponieważ wydajność
metody prawdopodobnie spadnie, kiedy tablica przestanie mieścić się w buforze.
2.3.28. Głębokość rekurencji. Przeprowadź empiryczne badania, aby ustalić średnią
głębokość rekurencji w sortowaniu szybkim z przełączeniem m etody dla tablic o roz
miarze M przy sortowaniu tablic o N różnych elementach. Przyjmij M = 10, 20 i 50
oraz N = 103, 10“, 105 i 106.
2.3.29. Randomizacja. Przeprowadź empiryczne badania, aby porównać wydajność
strategii wyboru losowego elementu osiowego z techniką opartą na początkowej ran-
domizacji tablicy (stosowaną w książce). Użyj przełączenia m etody dla tablic o wiel
kości M, a sortowane tablice mają mieć N różnych elementów. Przyjmij M = 10, 20
i 50 oraz N = 103, 104, 105 i 106.
2.3.30. Przypadki skrajne. Przetestuj sortowanie szybkie na dużych nielosowych
tablicach podobnych do tych opisanych w ć w i c z e n i a c h 2 . 1.35 i 2 . 1 .3 6 . Zastosuj
wersję z początkowym losowym mieszaniem i bez tej techniki. Jak mieszanie wpływa
na wydajność sortowania takich tablic?
2.3.31. Histogram czasów wykonania. Napisz program, który przyjmuje z wiersza
poleceń argumenty N i T, wykonuje T powtórzeń eksperymentu z uruchomieniem
sortowania szybkiego dla tablicy o N losowych wartościach typu Doubl e i rysuje hi
stogram z zarejestrowanymi czasami wykonania. Uruchom program dla N = 103,104,
105 i 106 oraz jak największego T, tak aby krzywe były gładkie. Główną trudnością
w tym ćwiczeniu jest odpowiednie skalowanie wyników eksperymentów.
2.4. K O L E JK I P R IO R Y T E T O W E
w w i e l u z a s t o s o w a n i a c h t r z e b a przetwarzać elementy o uporządkowanych klu
czach, jednak niekoniecznie wszystkie jednocześnie (ponadto dane nie muszą być
w pełni posortowane). Często należy utworzyć kolekcję takich elementów, przetwo
rzyć element o największym kluczu, następnie dodać kolejne elementy, przetwo
rzyć ten o obecnie największym kluczu itd. Prawdopodobnie masz kom puter (lub
telefon komórkowy), na którym kilka aplikacji może działać jednocześnie. Efekt ten
zwykle osiąga się przez określenie priorytetów zdarzeń powiązanych z aplikacjami
i wybieranie do przetwarzania zawsze tego zdarzenia, które ma najwyższy priorytet.
Przykładowo, w większości telefonów komórkowych przychodzące połączenia mają
wyższy priorytet niż gry.
Typ danych odpowiedni dla takich środowisk obsługuje dwie operacje: usuń mak
symalny i wstaw. Taki typ danych to kolejka priorytetowa. Używanie kolejek priory
tetowych przypomina korzystanie z kolejek (usuwanie najstarszego) i stosów (usu
wanie najnowszego), jednak opracowanie wydajnej implementacji sprawia tu więcej
trudności.
W tym podrozdziale po krótkim omówieniu podstawowych reprezentacji, w któ
rych jedna lub obie operacje działają w czasie liniowym, rozważamy klasyczną im
plementację kolejek priorytetowych, opartą na kopcu binarnym. W tej strukturze da
nych elementy są przechowywane w tablicy, a ich uporządkowanie podlega pewnym
regułom, umożliwiającym wydajne (w czasie logarytmicznym) zaimplementowanie
operacji usuń maksymalny i wstaw.
Wybrane ważne zastosowania kolejek priorytetowych to: systemy symulacji,
w których klucze odpowiadają czasom zachodzenia zdarzeń przetwarzanych w po
rządku chronologicznym; szeregowanie zadań, gdzie klucze odpowiadają prioryte
tom określającym, które zadania należy najpierw wykonać; obliczenia numeryczne,
gdzie klucze reprezentują błędy w obliczeniach i wyznaczają kolejność zajmowania
się usterkami. W r o z d z i a l e 6 . przedstawiono szczegółowe studium przypadku do
tyczące wykorzystania kolejek priorytetowych w symulacji kolizji cząsteczek.
Kolejkę priorytetową m ożna zastosować jako podstawę algorytmu sortowania,
wstawiając ciąg elementów, a następnie po kolei usuwając najmniejszy z pozostałych.
Ważny algorytm sortowania, sortowanie przez kopcowanie, także w naturalny spo
sób wynika z przedstawionej tu implementacji kolejek priorytetowych (opartej na
kopcu). W dalszej części książki pokazano, jak używać kolejek priorytetowych jako
cegiełek innych algorytmów. W r o z d z i a l e 4 . kolejki priorytetowe pełnią funkcję
abstrakcji odpowiednich do zaimplementowania kilku podstawowych algorytmów
przeszukiwania grafów. W r o z d z i a l e 5 . na podstawie opisanych tu m etod opraco
wano algorytm kompresji danych. To tylko kilka przykładów na to, jak ważną rolę
odgrywają kolejki priorytetowe przy projektowaniu algorytmów.
320
2.4 a Kolejki priorytetowe 321
I n t e r f e j s A P I Kolejka priorytetowa to prototypowy abstrakcyjny typ danych (zo
bacz p o d r o z d z i a ł 1 .2 ) — reprezentuje zbiór wartości i operacji na tych wartościach,
a także zapewnia wygodną abstrakcję, umożliwiającą oddzielenie klientów od różnych
implementacji omawianych w podrozdziale. Tutaj, tak jak w p o d r o z d z i a l e 1 .2 , pre
cyzyjnie zdefiniowano operacje i opracowano interfejs API z informacjami potrzeb
nymi klientom. Dla kolejek priorytetowych charakterystyczne są operacje usuń mak
symalny i wstaw, dlatego koncentrujemy się na nich. M etoda del Max () usuwa maksy
malny element, a in s e rt() wstawia dane. Zgodnie ze zwyczajem do porównywania
kluczy używana jest wyłącznie metoda pomocnicza 1 e s s () (tak jak w sortowaniu).
Dlatego jeśli elementy mogą mieć powtarzające się klucze, maksymalny oznacza do
wolny element o największej wartości klucza. Aby uzupełnić interfejs API, trzeba też
dodać konstruktory (podobne do tych używanych dla stosów i kolejek) oraz operację
sprawdź, czy pusta. Z uwagi na elastyczność zastosowano generyczną implementację
z typem sparametryzowanym Key i obsługującą interfejs Comparabl e. Rozwiązanie to
eliminuje podział na elementy i klucze oraz umożliwia bardziej przejrzysty i zwięzły
opis struktur danych oraz algorytmów. Przykładowo, używamy nazwy „największy
klucz” zamiast „największy element” lub „element o największym kluczu”.
Aby ułatwić pisanie kodu klienta, w interfejsie API umieszczono trzy konstruktory,
umożliwiające klientom budowanie kolejek priorytetowych o rozmiarze ustalonym
na początku (możliwe, że inicjowanych podaną tablicą kluczy). W celu zwiększe
nia przejrzystości kodu klienta stosujemy w odpowiednich miejscach odrębną klasę
Mi nPQ. Jest ona prawie identyczna z klasą MaxPQ, obejmuje jednak metodę del Mi n (),
która usuwa i zwraca element o najmniejszym kluczu. Każdą implementację klasy
MaxPQ można łatwo przekształcić na implementację klasy Mi nPQ i na odwrót — wy
starczy odwrócić porównanie w metodzie 1 ess ().
p ub lic c la s s MaxPQ<Key extends Comparable<Key>>
MaxPQ () Tworzenie kolejki priorytetowej
MaxPQ(int max) Tworzenie kolejki priorytetowej o początkowej pojemności max
MaxPQ ( Key [] a) Tworzenie kolejki priorytetowej na podstawie kluczy z tablicy a []
void in se rt(K e y v) Wstawianie klucza do kolejki priorytetowej
Key max() Zwracanie największego klucza
Key del Max () Zwracanie i usuwanie największego klucza
bool ean i sEmpty () Czy kolejka priorytetowa jest pusta ?
i nt s i ze () Liczba kluczy w kolejce priorytetowej
Interfejs API generycznej kolejki priorytetowej
322 R O ZD ZIA Ł 2 a Sortowanie
Klient kolejki priorytetowej K|. Tempo wzrostu
Aby docenić wartość abstrakcji Czas Pamięć
w postaci kolejki prioryteto
Klient sortowania Nlog N N
wej, warto rozważyć opisany tu
Klient kolejki priorytetowej
problem. Istnieje duży strumień NM M
, — implementacja podstawowa
wejściowy o N łańcuchach zna- r J r
ków i powiązanych wartościach Klient kolejki priorytetowej — N logM M
całkowitoliczbowych. Zadanie implementacja oparta na kopcu
polega na znalezieniu Mnajwięk- KosztVznalezienia M największych wartości
, , , . . i , w strumieniu N elementów
szych lub najmniejszych liczb
całkowitych (i powiązanych łańcuchów znaków) w strumieniu wejściowym. Można
przyjąć, że strumień opisuje transakcje finansowe (należy znaleźć duże wartości), poziom
pestycydów w produktach rolnych (należy znaleźć niskie wartości), żądania usług, wy
niki eksperymentu naukowego lub dowolne inne dane. W niektórych zastosowaniach
wielkość strumienia wejściowego jest tak duża, że można przyjąć, iż jest nieograniczona.
Jednym ze sposobów na rozwiązanie problemu jest posortowanie strumienia wejściowe
go i pobranie z niego Mnajwiększych kluczy. Jednak właśnie stwierdziliśmy, że strumień
jest za duży, aby było to możliwe. Inne podejście polega na porównywaniu każdego no
wego klucza z Mdotychczas największych, jednak jeśli Mnie jest małe, koszty tego roz
wiązania mogą okazać się nieakceptowalne. Za pomocą kolejek priorytetowych można
rozwiązać problem przy użyciu klienta TopM (przedstawiony na następnej stronie) klasy
MinPQ, pod warunkiem że uda się opracować wydajną implementację operacji in se rt()
i del Mi n (). Dokładnie to jest celem w niniejszym podrozdziale. Dla dużych wartości N, na
które można natrafić we współczesnej infrastrukturze informatycznej, od implementacji
tego rodzaju może zależeć, czy w ogóle możliwe będzie rozwiązanie problemu.
Podstawowe implementacje Podstawowe struktury danych opisane w ro z
zapewniają cztery bezpośrednie punkty wyjścia do implementowania kole
d z ia le i.
jek priorytetowych. Można użyć tablicy lub listy powiązanej w postaci uporządkowa
nej albo nieuporządkowanej. Implementacje tego rodzaju są przydatne dla krótkich
kolejek priorytetowych w sytuacjach, w których jedna z dwóch operacji jest zdecydo
wanie częstsza lub kiedy m ożna poczynić pewne założenia dotyczące kolejności klu
czy używanych w operacjach. Ponieważ implementacje są podstawowe, ograniczamy
się do krótkich opisów w tekście, a napisanie kodu pozostawiamy jako ćwiczenie
(zobacz ć w i c z e n i e 2 .4 .3 ).
Reprezentacja w postaci nieuporządkowanej tablicy Prawdopodobnie najprostsza
implementacja kolejki priorytetowej oparta jest na kodzie stosu ( p o d r o z d z i a ł 2 .1 ).
Kod operacji wstaw dla kolejki priorytetowej jest taki sam jak dla operacji dodaj dla sto
su. Aby zaimplementować operację usuń maksymalny, można dodać kod podobny do
pętli wewnętrznej sortowania przez wybieranie, aby przestawić maksymalny element
z elementem z końca i usunąć ten pierwszy (tak jak w metodzie pop () dla stosów).
Podobnie jak w stosach, można dodać kod do zmieniania wielkości tablicy, aby za
gwarantować, że struktura danych zawsze będzie pełna w przynajmniej jednej czwartej
i nigdy nie zostanie przepełniona.
2.4 Kolejki priorytetowe 323
Klient kolejki priorytetowej
p u b l i c c l a s s TopM
{
public s t a t i c void m ain(String[] args)
{ // W y ś w i e t l a n i e M n a j w i ę k s z y c h w a r t o ś c i ze s t r u m i e n i a we j ś c i o we g o ,
in t M = In te g e r. p a r s e l n t ( a r g s [0]);
M i n P Q < T r a n s a c t i o n > pq = new M i n P Q < T r a n s a c t i o n > ( M + l ) ;
while ([Link])
{ // Two r z e n i e el ementu na p od s t a w i e n a s t ę p n e g o w i e r s z a
// i dodawani e danych do k o l e j k i priorytetowej.
[Link](new T ra n s a c t i o n ( S t d In . r e a d L i n e ( ) ) ) ;
if ([Link] > M)
[Link](); // Usuwani e minimum, j e ś l i w k o l e j c e j e s t M+l
// elementów.
} // W k o l e j c e z n a j d u j e s i ę M n a j w i ę k s z y c h elementów.
S t a c k < T r a n s a c t i o n > s t a c k = new S t a c k < T r a n s a c t i o n > ( ) ;
while ([Link]) s t a c k . p u s h ( p q . d e ! M i n ()) ;
for (Transaction t : stack) [Link](t);
)
}
Klient klasy Mi nPQ przyjmuje liczbę całkowitą M(podaną w wierszu poleceń) i stru
mień wejściowy, w którym każdy wiersz zawiera transakcję, a następnie wyświet
la Mwierszy o największych wartościach. Wykorzystano przy tym opracowaną
przez nas klasę T r a n s a c t i o n (zobacz stronę 91, ć w ic z e n ie 1 .2.19 i ć w ic z e n ie
2 .1 .2 1 ) do zbudowania kolejki priorytetowej z wartościami jako kluczami. Kiedy
rozmiar kolejki priorytetowej przekracza M, pro
% more t in y B a tc h .tx t gram usuwa minim alną wartość po wstawieniu
Turing 6/17/1990 644.08 nowej. Po przetworzeniu wszystkich transakcji
vonNeumann 3/26/2002 4121.85
Di jk s t r a 8/22/2007 2678.40
M największych wartości pobieranych jest z ko
vonNeumann 1/11/1999 4409.74 lejki priorytetowej w kolejności rosnącej. Kod
Di jk s t r a 11/18/1995 837.42 umieszcza elementy na stosie, a następnie prze
Hoare 5/10/1993 3229.27
chodzi po nim, aby odwrócić kolejność wartości
vonNeumann 2/12/1994 4732.35
Hoare 8/18/1992 4381.21 i wyświetlić je w porządku malejącym.
Turi ng 1/11/2002 66.10
Thompson 2/27/2000 4747.08
Turing 2/11/1991 2156.86 % java TopM 5 < tin y B a tc h .tx t
Hoare 8/12/2003 1025.70 Thompson 2/27/2000 4747.08
vonNeumann 10/13/1993 2520.97 vonNeumann 2/12/1994 4732.35
Di jk s t r a 9/10/2000 708.95 vonNeumann 1/11/1999 4409.74
Turi ng 10/12/1993 3532.36 Hoare 8/18/1992 4381.21
Hoare 2/10/2005 4050.20 vonNeumann 3/26/2002 4121.85
324 R O ZD ZIA Ł 2 ■ Sortowanie
Reprezentacja w postaci uporządkow anej tablicy Inne podejście polega na napisa
niu dla operacji wstaw kodu, który przenosi elementy o jedną pozycję w prawo, przez
co klucze tablicy zachowują kolejność (tak jak w sortowaniu przez wstawianie). W ten
sposób największy element zawsze znajduje się na końcu, a kod operacji usuń maksy
malny w kolejce priorytetowej jest taki sam, jak kod operacji zdejmij dla stosu.
Reprezentacje w postaci listy pow iązanej Można też zacząć od opracowanego
przez nas kodu stosów opartego na liście powiązanej i zmodyfikować albo kod m e
tody pop (), tak aby wyszukiwał i zwracał maksimum, albo kod m etody push (), żeby
dodawał klucze w odwrotnej kolejności (wtedy m etoda pop () może odłączać i zwra
cać pierwszy — maksymalny — element listy).
Zastosowanie nieuporządkowanych ciągów to
Usuń
Struktura danych Wstaw prototypowe leniwe podejście do problemu.
maksymalny
Wykonanie zadania (znalezienie maksimum) jest
Tablica tu odraczane do momentu, kiedy trzeba je wyko
N
uporządkowana
nać. Wykorzystanie uporządkowanych ciągów to
Tablica prototypowa technika zachłanna; jak największa
N
nieuporządkowana część pracy (sortowanie listy przy wstawianiu
Kopiec log N log N elementów) wykonywana jest od razu, co zwięk
sza wydajność późniejszych operacji.
Niemożliwe 1 1 Istotna różnica między implementowaniem sto
Tempo wzrostu czasu wykonania dla najgorszego sów lub kolejek a implementowaniem kolejek prio
przypadku w kolejkach priorytetowych rytetowych związana jest z wydajnością. Dla stosów
i kolejek można opracować implementacje, w któ
rych wszystkie operacje działają w stałym czasie. W przypadku kolejek priorytetowych
we wszystkich podstawowych implementacjach albo operacja wstaw, albo operacja usuń
maksymalny dla najgorszego przypadku działa w czasie liniowym. Opisany dalej kopiec
umożliwia utworzenie implementacji, w której obie operacje działają szybko.
Zwracana Zawartość Zawartość
Operacja Argument Rozmiar
wartość (nieuporządkowana) (uporządkowana)
Wstaw
Wstaw
Wstaw
Usuń maks.
Wstaw
Wstaw
Wstaw
Usuń maks.
Wstaw
Wstaw
Wstaw
Usuń maks.
Ciąg operacji na kolejce priorytetowej
2.4 ■ Kolejki priorytetowe 325
D e f in ic je k o p c a Kopiec binarny to struktura danych umożliwiająca tworzenie
wydajnych podstawowych operacji na kolejkach priorytetowych. W kopcu binarnym
klucze są przechowywane w tablicy, w której każdy klucz jest zawsze większy lub rów
ny względem kluczy na dwóch innych określonych pozycjach. Z kolei każdy z tych
dwóch kluczy musi być większy lub równy względem dwóch dodatkowych kluczy itd.
To uporządkowanie łatwo zrozumieć po wyobrażeniu sobie, że klucze znajdują się
w drzewie binarnym, w którym każdy klucz powiązany jest z dwoma mniejszymi.
Definicja. Drzewo binarne jest uporządkowane w kopiec, jeśli klucz w każdym
węźle jest większy lub równy względem kluczy w dwóch dzieciach węzła (jeśli te
istnieją).
Tym samym klucz w każdym węźle drzewa binarnego uporządkowanego w kopiec j est
mniejszy lub równy względem klucza w węźle rodzica (jeśli ten istnieje). Przechodząc
w górę od dowolnego węzła, natrafiamy na niemalejący ciąg kluczy. Poruszając się
w dół, otrzymujemy nierosnący ciąg kluczy. Oto ważne spostrzeżenie.
Twierdzenie O. Największy klucz w drzewie binarnym uporządkowanym
w stertę znajduje się w korzeniu.
Dowód. Przez indukcję na wielkości drzewa.
Reprezentacja sterty binarnej Przy stosowaniu struktury powiązanej do repre
zentowania drzew binarnych uporządkowanych w stertę, z każdym kluczem trze
ba powiązać trzy odnośniki, aby m ożna poruszać się w górę i w dół drzewa (każdy
węzeł musi posiadać jeden wskaźnik do rodzica i po jednym do każdego dziecka).
Wyjątkowo wygodne jest użycie zupełnego drzewa binarnego, takiego jak naryso
wano po prawej. Aby utworzyć taką strukturę, należy wstawić korzeń, a następnie
poruszać się w dół i od lewej do prawej, rysując i łącząc dwa węzły z każdym węzłem
z wyższego poziomu do m om entu dodania N węzłów. Drzewa zupełne umożliwiają
zastosowanie zwięzłej reprezentacji w po
staci tablicy, która nie obejmuje bezpo
średnich odnośników. Zupełne drzewa bi
narne można zapisać sekwencyjnie w tab
licy przez umieszczenie węzłów według
poziomów — korzeń zajmuje pozycję 1 .,
jego dzieci — pozycje 2. i 3., ich dzieci
pozycje 4., 5., 6 . i 7. itd. Z u p e łn e d rz e w o b in a r n e u p o rz ą d k o w a n e w s te r tę
326 R O ZD ZIA Ł 2 Q Sortowanie
Definicja. Kopiec binarny to kolekcja kluczy zapisana jako zupełne drzewo bi
narne uporządkowane w kopiec, reprezentowana według poziomów w tablicy
(z wolnym pierwszym elementem).
Z uwagi na zwięzłość od tego miejsca p o
mijamy dookreślenie „binarny” i używamy
nazwy kopiec na kopiec binarny. W kopcu
rodzic węzła z pozycji k zajmuje pozycję
Lk/żJ i — odwrotnie — dzieci węzła z po
zycji k znajdują się na pozycjach 2k i 2k +
1. Zamiast bezpośrednio używać odnośni
ków (tak jak w drzewach binarnych om a
wianych w r o z d z i a l e 3 .), można poruszać
się w górę i w dół za pomocą prostych ope
racji arytmetycznych na indeksach tablicy.
Aby przejść w górę drzewa z punktu a [k],
należy ustawić k na k/2. W celu przejścia
w dół drzewa trzeba ustawić k na 2 *k lub
2 * k + l.
R e p re z e n ta c je k o p c a Zupełne drzewa binarne reprezento
wane jako tablice (kopce) to stosunkowo
sztywne struktury, są jednak wystarczająco elastyczne, aby m ożna przy ich użyciu
zaimplementować wydajne operacje na kolejkach priorytetowych. Posłużą do opra
cowania implementacji z operacjami wstaw i usuń maksymalny działającymi w cza
sie logarytmicznym. W algorytmach wykorzystano możliwość poruszania się w górę
i w dół drzewa bez używania wskaźników. Algorytmy te mają gwarantowaną wydaj
ność logarytmiczną z uwagi na opisaną dalej cechę zupełnych drzew binarnych.
Twierdzenie P. Wysokość zupełnego drzewa binarnego o rozmiarze N wynosi
LlgNj.
Dowód. Wynik łatwo jest udowodnić przez indukcję lub zauważając, że wyso
kość rośnie o 1 dla N będących potęgami dwójki.
2.4 a Kolejki priorytetowe 327
A lg o r y t m y o p a r te n a k o p c a c h Kopiec o wielkości N zapisujemy w prywatnej
tablicy pq[] o długości N + 1. Pozycja pq [0] pozostaje wolna, a kopiec znajduje się
na pozycjach od pq [ 1] do pq[N]. W kontekście algorytmów sortowania dostęp do
kluczy odbywa się tylko poprzez prywatne
funkcje pomocnicze le s s () i exch(), ponie p r i v a t e bo ole an l e s s ( i n t i , i n t j )
{ r e t u r n p q [ i ] . c o m p a r e T o ( p q [ j ] ) < 0; }
waż jednak wszystkie elementy znajdują się
w zmiennej egzemplarza pq [], na następnej p r iv a t e void e x c h (in t i , in t j)
stronie użyto zwięzłych implementacji, bez { Key t = p q [ i ] ; p q [ i ] = p q [ j ] ; p q [ j ] = t; }
przekazywania nazwy tablicy jako param e
tru. Rozważane operacje na kopcu najpierw Metody do porównywania i przestawiania dla
wprowadzają prostą zmianę, która może implementacji opartych na kopcu
naruszyć kopiec, a następnie przechodzą po
nim, modyfikując go w odpowiedni sposób, aby zagwarantować, że struktura kopca
jest zachowana. Proces ten to przywracanie struktury kopca.
Występują dwa przypadki. Przy zwiększaniu priorytetu pewnego węzła (lub doda
waniu nowego w dolnej części kopca) trzeba przejść w górę, aby przywrócić strukturę
kopca. Przy zmniejszaniu priorytetu (na przykład po zastąpieniu węzła w korzeniu
nowym węzłem o mniejszym kluczu) trzeba przejść w dół w celu przywrócenia struk
tury kopca. Najpierw zastanówmy się nad tym, jak zaimplementować te dwie pom oc
nicze operacje. Następnie pokazujemy, jak wykorzystać je do zaimplementowania
operacji wstaw i usuń maksymalny.
Przywracanie stru ktu ry kopca p rzy przechodzeniu do góry (w ypływ anie) Jeśli
struktura kopca zostaje naruszona, ponieważ klucz węzła stał się większy niż klucz
rodzica, m ożna zrobić krok w kierunku przywrócenia struktury, przestawiając węzeł
z rodzicem. Po przestawieniu węzeł jest większy niż każde z dzieci (jednym jest dawny
rodzic, a drugi jest mniejszy niż
dawny rodzic, ponieważ był jego
dzieckiem), jednak węzeł nadal
może być większy od rodzica.
Można to naprawić w ten sam
Narusza strukturę kopca
(klucz większy niż w rodzicu) sposób i powtarzać proces, prze
chodząc w górę sterty do czasu
napotkania węzła o większym
kluczu lub korzenia. Napisanie
kodu tego procesu jest proste.
Wystarczy pamiętać, że rodzic
węzła z pozycji k zajmuje pozy
cję k/ 2 . Pętla w metodzie swim()
zachowuje niezmiennik, zgod
Przywracanie struktury kopca przy nie z którym struktura kopca
przechodzeniu w górę (wypływanie)
może być naruszona tylko wtedy,
328 RO ZD ZIA Ł 2 n Sortowanie
kiedy węzeł z pozycji k jest większy od rodzi p riv a te void sw im (int k)
ca. Dlatego po dojściu do momentu, w którym {
węzeł nie jest większy od rodzica, wiadomo, w h ile (k > 1 && 1e s s (k / 2 , k))
{
że struktura kopca jest zachowana w nim ca exch(k/2, k ) ;
łym. Nazwa techniki pochodzi stąd, że można k = k/2;
wyobrazić sobie, )
Narusza strukturę kopca iż nowy węzeł
(o zbyt dużym Implementacja przywracania struktury
kluczu) wypływa kopca przy przechodzeniu w górę
w kopcu na wyż (wypływanie)
szy poziom.
P rzy w ra c a n ie s tr u k tu r y ko pca p r z y p r z e c h o d ze n iu
w d ó ł (za ta p ia n ie ) Jeśli struktura kopca jest n aru
szona, ponieważ klucz węzła stał się m niejszy niż klucz
jednego lub obu dzieci, m ożna zrobić krok w kierun
ku naprawienia struktury przez przestawienie węzła
z w iększym z dwójki dzieci. Może to spowodować na
ruszenie w węźle dziecka. Należy je naprawić w ten
sam sposób i kontynuować proces, przechodząc w dół
Z m ian a s tr u k tu r y k o p c a p rz y kopca do m om entu napotkania węzła, w którym każde
D rz e c h o d z e n iu w d ó ł (z a ta p ia n ie )
z dzieci jest mniejsze (lub równe), albo natrafienia na
koniec struktury. Także tu kod bezpośrednio wynika
z tego, że dzieci węzła na pozycji k znajdują się w kopcu na pozycjach 2 k i 2 k+l.
Nazwa m etody wynika z tego, że węzeł o za małym kluczu trzeba zatopić przez
umieszczenie na niższym poziomie kopca.
w yobraźm y że kopiec re
s o b ie ,
p riv a te void s i n k (in t k)
prezentuje bezwzględną hierarchię {
korporacyjną, w której każde dzie w hile (2*k <= N)
cko węzła odpowiada podwładnemu (
in t j = 2*k;
(a rodzic to bezpośredni przełożo i f (j < N && l e s s ( j , j+ 1 )) j++;
ny). Opisane operacje m ożna wtedy i f ( ! le s s ( k , j ) ) break;
zinterpretować w ciekawy sposób. exch(k, j ) ;
k = j;
Operacja swim() odpowiada pojawie
1
niu się w firmie obiecującego nowego 1
menedżera, który otrzymuje awanse
Implementacja zmiany struktury kopca
(i zamienia się stanowiskami z przeło
przy przechodzeniu w dół (zatapianie)
żonymi o niższych umiejętnościach)
do czasu natrafienia na szefa o wyż
szych kwalifikacjach. Operacja si nk () jest analogiczna do sytuacji, w której prezes
firmy rezygnuje i zostaje zastąpiony przez osobę z zewnątrz. Jeśli podwładny preze
sa ma mocniejszą pozycję niż nowy człowiek, zamieniają się stanowiskami. Należy
2.4 □ Kolejki priorytetowe 329
przejść w dół drzewa i degradować nową osobę oraz awansować innych ludzi do
momentu dojścia do poziomu kompetencji danej osoby, kiedy nie będzie miała wyżej
wykwalifikowanych podwładnych. Ten scenariusz rzadko ma miejsce w rzeczywi
stym świecie, jednak może pomóc w lepszym zrozumieniu podstawowych operacji
na stertach.
Operacje s in k () i swim() są podstawą wydajnej implementacji interfejsu API ko
lejek priorytetowych. Poniżej przedstawiono rysunki obrazujące interfejs, a na na
stępnej stronie znajduje się jego implementacja ( a l g o r y t m 2 .6 ).
Wstaw. Należy dodać nowy klucz na koniec tablicy, zwiększyć rozmiar kopca, a na
stępnie spowodować wypłynięcie klucza w celu przywrócenia struktury kopca.
Usuń m aksym alny. Należy usunąć największy klucz z korzenia, umieścić na jego
miejscu element z końca kopca, zmniejszyć wielkość kopca i spowodować zatopie
nie przestawionego elementu w celu przywrócenia struktury.
a l g o r y t m 2.6 to rozwiązanie podstawowego problemu postawionego na początku
podrozdziału, dotyczącego opracowania implementacji interfejsu API kolejki priory
tetowej, w którym operacje wstaw i usuń maksymalny zajmują czas rosnący logaryt
micznie względem wielkości kolejki.
m aksym aln y
Usuw any klucz
Przestawienie tego
klucza z korzeniem
D od an ie klucza do kopca Usuw anie
narusza jego strukturę węzła z kopca
Wypływanie ^
Zatapianie
Operacje na kopcu
330 RO ZD ZIA Ł 2 Sortowanie
ALGORYTM 2.6. Kolejka priorytetowa oparta na stercie
public c la s s MaxPQ<Key extends Comparable<Key»
{
private Key[] pq; // Zupełne drzewo binarne uporządkowane w kopiec
private in t N = 0; // na pozycjach pq[l..N] (pq[0] je s t wolna).
public MaxPQ(int maxN)
{ pq = ( Key[]) new Comparable[maxN+1]; }
public boolean isEmptyO
{ return N == 0; }
public in t s iz e ()
{ return N; }
public void insert(K ey v)
{
pq[++N] = v;
swim(N);
public Key delMaxQ
{
Key max = pq[1]; // Pobieranie maksymalnego klucza z korzenia.
exch(l, N --); // Przestawianie z ostatnim elementem.
pq[N+l] = n u li; // Unikanie zbędnych re fere n cji.
sin k (l); // Przywracanie stru k tu ry kopca,
return max;
}
// Implementacje metod pomocniczych znajdują s ię na stronach 157 - 159.
private boolean l e s s ( i n t i , in t j)
private void exch(int i , in t j)
private void swim(int k)
private void s i n k ( i n t k)
Kolejka priorytetowa jest przechowywana w uporządkowanym w kopiec zupełnym
drzewie binarnym w tablicy pq[], w której pozycja pq [0] jest wolna, a N kluczy kolejki
priorytetowej zajmuje miejsca od pq [1] do pq[N]. W implementacji metody in s e rt()
należy zwiększyć N, dodać nowy element na koniec, a następnie użyć m etody swim()
do przywrócenia struktury kopca. W metodzie del Max () trzeba pobrać zwracaną war
tość z elementu pq[l], przenieść element pq[N] na pozycję pq[l], zmniejszyć rozmiar
kopca i użyć m etody sink() do przywrócenia struktury. Ponadto obecnie wolną pozy
cję pq [N+l] należy ustawić na nul 1, aby umożliwić systemowi przywrócenie powiązanej
z nią pamięci. Kod do dynamicznego zmieniania wielkości tablicy, jak zwykle, pom inię
to (zobacz p o d r o z d z i a ł 1 .3 ). Inne konstruktory opisano w ć w i c z e n i u 2 .4 . 1 9 .
2.4 b Kolejki priorytetowe 331
Twierdzenie Q. W kolejce priorytetowej o N insert P
©
kluczach algorytmy oparte na kopcu wymaga
ją nie więcej niż 1 + lg N porównań w operacji
insert Q
wstaw i nie więcej niż 2 lg N porównań w operacji
usuń maksymalny.
insert E
Dowód. Obie operacje obejmują przechodze
nie ścieżką od korzenia do dna kopca, a zgodnie
z t w i e r d z e n i e m p liczba odnośników na ścieżce U su ń m a k sy m a ln y (Q )
wynosi nie więcej niż lg N. Operacja usuń maksy
malny wymaga dwóch porównań na każdy węzeł W staw X
ścieżki (oprócz sytuacji po dojściu do dna) — jedne
go na znalezienie dziecka o większym kluczu i dru
giego na ustalenie, czy dziecko należy „awansować”. W staw A
W typowych zastosowaniach, gdzie potrzebna jest
duża liczba wymieszanych operacji wstawiania i usu
wania maksymalnego elementu w dużych kolejkach W staw M
priorytetowych, t w i e r d z e n i e q pozwala uzyskać
ważny przełom w zakresie wydajności, co opisano
w tabeli pokazanej na stronie 324. Implementacje
U su ń m a k sy m a ln y (X )
podstawowe, oparte na uporządkowanej lub nieupo
rządkowanej tablicy, wymagają dla jednej z operacji
czasu rosnącego liniowo, natomiast implementacja
oparta na kopcu gwarantuje logarytmiczny czas wy W staw P
konania obu operacji. Od tego usprawnienia może
zależeć, czy problem w ogóle uda się rozwiązać.
Kopce a-arne N ietrudno zmodyfikować kod i zbu W staw L
dować kopce oparte na tablicowej reprezentacji zu
pełnych drzew trójkowych uporządkowanych w ko
piec. Element na pozycji k jest tu większy lub równy
W staw E
względem elementów z pozycji 3k-l, 3k i 3/c+l oraz
mniejszy lub równy względem elementów z pozycji
l_(k+I)/3_|. Jest to prawdą dla wszystkich indeksów
od 1 do N w tablicy o N elementach. Niewiele tru d U suń m a k sy m a ln y ( P )
niejsze jest stosowanie kopców o d dzieciach dla do
wolnego d. Występuje tu zależność między niższym
kosztem wynikającym ze zmniejszonej wysokości
drzewa (logrf N) a wyższym kosztem wyszukiwania O p e ra cje kolejki p rio ry te to w ej w y k o n y w an e na kopcu
największego spośród d dzieci węzła. Efektywność
zależy od szczegółów implementacji i oczekiwanej
względnej częstotliwości wykonywania operacji.
332 R O ZD ZIA Ł 2 h Sortow anie
Z m ia n a w ie lk o śc i ta b lic y Można dodać konstruktor bez parametrów, kod do po
dwajania wielkości tablicy w metodzie i n sert () i kod do zmniejszania tej wielkości
w metodzie del Max () (podobne rozwiązanie zastosowano do stosów w p o d r o z d z i a l e
1 .3 ). W klientach nie trzeba wtedy przejmować się arbitralnym ograniczeniem roz
miaru. Logarytmiczne ograniczenia czasu wynikające z t w i e r d z e n i a q są oblicza
ne z uwzględnieniem am ortyzacji, jeśli wielkość kolejki priorytetowej jest arbitralna
i można zmieniać wielkość tablic (zobacz ć w i c z e n i e 2 .4 .2 2 ).
N ie z m ie n n o ść k lu c zy Kolejka priorytetowa obejmuje obiekty tworzone przez klien-
ty, jednak zakładamy, że kod klienta nie modyfikuje kluczy (co mogłoby naruszyć
strukturę kopca). Można opracować mechanizm wymuszający przestrzeganie tego
założenia, jednak programiści zwykle tego nie robią, ponieważ komplikuje to kod
i często powoduje spadek wydajności.
In d ek so w a n a k o le jk a p r io r y te to w a W wielu zastosowaniach sensowne jest um oż
liwienie klientom wskazywania elementów znajdujących się już w kolejce prioryteto
wej. Łatwym sposobem na osiągnięcie tego celu jest powiązanie z każdym elementem
niepowtarzalnego całkowitoliczbowego indeksu. Ponadto w klientach często istnieje
zbiór elementów mający znaną wielkość N i używane są (równolegle) tablice do prze
chowywania informacji na temat tych elementów. Indeks umożliwia wtedy wskazy
wanie elementów w innych fragmentach kodu klienta. Na podstawie tego opisu można
zaproponować następujący interfejs API.
p u b lic c la s s IndexMinPQ<Item extends Comparable<Item>>
Tworzy kolejkę priorytetowij o pojemności maxN
IndexMinPQfint maxN)
i dozwolonych indeksach z przedziału od 0 do maxN-l
void in s e r t f in t k, Item item) Wstawia element item i więżę go z k
void ch an ge(int k, Item item) Zmienia element powiązany z k na item
boolean c o n ta in s f in t k) Czy kjest powiązane z jakim ś elementem?
void d e le t e fin t k) Usuwa k i powiązany element
Item m in() Zwraca element minimalny
in t m inlndex() Zwraca indeks elementu minimalnego
in t d elM in() Usuwa element minimalny i zwraca jego indeks
boolean isEm pty() Czy kolejka priorytetowa jest pusta?
in t s iz e ( ) Zwraca liczbę elementów w kolejce priorytetowej
Interfejs API generycznej kolejki priorytetowej z powiązanymi indeksami
2.4 n Kolejki priorytetowe 333
O tym typie danych można myśleć jak o implementacji tablicy, jednak z szybkim do
stępem do najmniejszego elementu. W rzeczywistości możliwości są jeszcze większe —
typ zapewnia szybki dostęp do minimalnej wartości określonego podzbioru elementów
(tych wstawionych). Oznacza to, że zmienna pq typu IndexMi nPQ reprezentuje podzbiór
tablicy p q[0. . N -l]. Wywołanie p [Link](k, item) dodaje do tego podzbioru kiusta-
wiapq[k] = item. Wywołanie [Link](k, item) ustawia pq [k] = item. Oba wywo
łania zachowują strukturę potrzebną do obsługi innych operacji — przede wszystkim
delMin() (usuwa i zwraca indeks minimalnego klucza) i change() (zmienia element
powiązany z indeksem, który już znajduje się w strukturze danych — tak jak wywo
łanie pq [i] = i tern). Operacje te są ważne w wielu zastosowaniach, a można ich uży
wać z uwagi na możliwość wskazywania kluczy (za pomocą indeksu). W ć w i c z e n i u
2 .4.33 opisano, jak rozwinąć a l g o r y t m 2 .6 , aby zaimplementować niezwykle wydajną
indeksowaną kolejkę priorytetową za pomocą bardzo małej
ilości kodu. Intuicyjnie widać, że kiedy element na kopcu się Operacja
Tempo wzrostu
zmienia, można przywrócić strukturę kopca przez zatapianie liczby porównań
(po zwiększeniu klucza) lub wypływanie (po zmniejszeniu in s e r t ( ) log N
klucza). Do wykonania tych operacji służy indeks, który po
change() log N
zwala znaleźć element w kopcu. Możliwość zlokalizowania
elementu w kopcu pozwala też dodać do interfejsu API ope c o n ta in s() 1
rację delete(). d e le te () log N
Twierdzenie Q (ciąg dalszy). W indeksowanej kolejce
mi n () 1
priorytetowej o długości N liczba potrzebnych porów m inlndex() 1
nań w operacjach wstaw, zmień priorytet, usuń i usuń
d elM in() log N
minimalny jest proporcjonalna najwyżej do log N.
Koszty dla najgorszego przypadku
Dowód. Wynika bezpośrednio z analizy kodu i tego, że dla /V-elementowej indeksowanej
wszystkie ścieżki w kopcu mają długość najwyżej ~lg N. kolejki priorytetowej
opartej na kopcu
To omówienie dotyczy kolejki obsługującej minimum. W witrynie dostępna jest też
wersja obsługująca maksimum — IndexMaxPQ.
K lient indeksow anej kolejki priorytetow ej Przedstawiony na stronie 334 klient
Mul t i way klasy IndexMi nPQ rozwiązuje problem scalania wielościeżkowego (ang. mul-
tiway merge) — scala kilka posortowanych strum ieni wejściowych w jeden posorto
wany strum ień wyjściowy. Problem ten pojawia się w wielu kontekstach. Strumienie
mogą zawierać dane wyjściowe z narzędzi naukowych (posortowane według czasu),
listę informacji z witryny, na przykład o piosenkach lub filmach (posortowane we
dług tytułu i nazwiska artysty), transakcje handlowe (posortowane według num eru
rachunku lub czasu) itd. Jeśli dostępna jest wystarczająca ilość pamięci, m ożna wczy
tać wszystkie elementy do tablicy i posortować je, jednak za pom ocą kolejki prioryte
towej można wczytać strumienie wejściowe i umieścić je na wyjściu w posortowanej
postaci niezależnie od ich długości.
334 RO ZD ZIA Ł 2 Sortow anie
Klient kolejki priorytetowej wykonujący scalanie wielościeżkowe
public c la s s Multiway
{
public s t a t ic void merge(In[] streams)
{
in t N = [Link];
IndexMinPQ<String> pq = new IndexMinPQ<String>(N);
f o r (in t i = 0; i < N; i++)
i f ( ! streams[i] .isEm ptyO)
p q . i n s e r t ( i , s t r e a m s [ i] .r e a d S t r i n g O ) ;
while (![Link] ptyO )
{
S td O u t .p rin t ln (p q .m in ());
in t i = [Link]();
i f ( ! streams[i] .isEm ptyO)
p q . i n s e r t ( i , streams[i] . r e a d S t r i n g O ) ;
}
}
public s t a t ic void m ain (Strin g[] args)
{
in t N = [Link];
I n [] streams = new In[N];
f o r (in t i = 0 ; i < N; i++)
streams[i] = new I n ( a r g s [ i ] );
merge(streams);
}
}
Klient I ndexMi n PQscala posortowane strumienie wejściowe (podane jako argumenty w wier
szu poleceń) w jeden posortowany strumień wyjściowy kierowany do standardowego wyjścia
(zobacz opis w tekście). Indeks w każdym strumieniu jest powiązany z kluczem (następnym
łańcuchem znaków w strumieniu). Po zainicjowaniu klient wchodzi w pętlę, która wyświetla
najmniejszy łańcuch znaków z kolejki i usuwa powiązany element, a następnie dodaje nowy
element na następny łańcuch znaków z danego strumienia. Z uwagi na zwięzłość dane wyj
ściowe pokazano poniżej w jednym wierszu. W rzeczywistych danych wyjściowych istnieje
jeden łańcuch znaków na wiersz.
% more m [Link] t
A B C F G I I Z
% more [Link]
B D H P Q Q
% more [Link] % java Multiway m [Link] t [Link] [Link]
A B E F J N A A B B B C D E F F G H I I J N P Q Q Z
2.4 ra Kolejki priorytetowe 335
S o r to w a n ie p r z e z k o p c o w a n ie Kolejkę priorytetową m ożna wykorzystać do
sortowania. Wszystkie elementy do posortowania należy umieścić w kolejce prio
rytetowej z łatwym dostępem do minimum, a następnie wielokrotnie użyć operacji
usuń minimalny, aby usunąć elementy w odpowiedniej kolejności. Zastosowanie do
tego kolejki priorytetowej w postaci tablicy nieuporządkowanej to odpowiednik sor
towania przez wybieranie. Wykorzystanie tablicy uporządkowanej odpowiada sor
towaniu przez wstawianie. Jaką metodę uzyskamy po zastosowaniu kopca? Zupełnie
odmienną! Dalej używamy kopca do opracowania klasycznego i eleganckiego algo
rytmu sortowania — sortowania przez kopcowanie.
Sortowanie przez kopcowanie ma dwa etapy — tworzenie kopca, kiedy to pier
wotna tablica jest porządkowana w kopiec, i sortowanie, kiedy to elementy są usuwa
ne z kopca w malejącej kolejności w celu uzyskania posortowanych danych. W celu
zachowania spójności z przeanalizowanym kodem używamy kolejki priorytetowej
z łatwym dostępem do maksimum i wielokrotnie usuwamy maksymalny element.
Ponieważ najważniejsze jest tu sortowanie, rezygnujemy z ukrywania reprezentacji
kolejki priorytetowej i bezpośrednio stosujemy metody swim() i si n k (). Umożliwia
to posortowanie tablicy bez używania dodatkowej pamięci, przez zachowanie kopca
w tablicy w posortowanej kolejności.
Tworzenie kopca Jak trudny jest proces budowania kopca na podstawie N elemen
tów? Z pewnością można wykonać to zadanie w czasie proporcjonalnym do N log
N. Należy przejść przez tablicę od lewej do prawej, używając m etody swim() do za
pewnienia, że elementy na lewo od sprawdzanego elementu tworzą zupełne drzewo
uporządkowane w kopiec, tak jak przy kolejnych operacjach wstawiania do kolejki
priorytetowej. Sprytna, dużo wydajniejsza metoda polega na przejściu od prawej do
lewej i użyciu metody sink() do tworzenia podkopców. Każda pozycja w tablicy to
korzeń małego podkopca. Metoda si nk () działa także dla takich struktur. Jeśli dwoje
dzieci węzła to kopce, wywołanie metody sink() dla węzła powoduje przekształce
nie w kopiec poddrzewa z korzeniem w rodzicu. Proces ten przez indukcję tworzy
kopiec. Przeglądanie rozpoczyna się w połowie tablicy, ponieważ można przeskoczyć
podkopce o wielkości 1. Proces kończy się na pozycji 1, po zakończeniu budowa
nia kopca przez jedno wywołanie metody si nk (). Tworzenie kopca to nieintuicyjny
pierwszy etap sortowania, ponieważ celem jest uzyskanie danych uporządkowanych
w kopiec, gdzie największy element umieszczony jest na początku tablicy (a inne
duże elementy — blisko początku), a nie na końcu, gdzie powinien się znaleźć.
Twierdzenie R. Tworzenie kopca przez zatapianie wymaga dla N elementów
mniej niż 2N porównań i mniej niż N przestawień.
Dowód. Wynika to z obserwacji, że większość przetwarzanych kopców jest mała.
Przykładowo, aby zbudować kopiec o 127 elementach, należy przetworzyć 32 kop
ce o wielkości 3, 16 kopców 7-elementowych, 8 kopców o 15 elementach, 4 kopce
o rozmiarze 31,2 kopce z 63 elementami i 1 kopiec o wielkości 127, co daje 32x1 +
16x2 + 8x3 + 4x4 + 2x5 + 1x6 = 120 przestawień (i dwa razy tyle porównań) dla
najgorszego przypadku. Kompletny dowód opisano w ć w i c z e n i u 2 .4 .20 .
336 R O ZD ZIA Ł 2 Sortowanie
ALGORYTM 2.7. Sortowanie przez kopcowanie
p u b lic s t a t i c void sort(C om parable[] a)
i
in t N = [Link];
f o r ( i n t k = N/2; k >= 1; k - )
s i n k ( a , k, N);
while (N > 1)
r
i
exch(a, 1, N— );
s i n k ( a , 1, N);
}
}
Kod sortuje elementy od a [ 1] do a [N], używając metody Si n k () (zmodyfikowanej tak, aby
przyjmowała jako argum enty a [] i N). Pętla f o r tworzy kopiec. Pętla whi 1e przestawia naj-
większy element a [1] z a[N], a następnie naprawia kopiec; proces ten trwa do czasu opróż-
nienia kopca. Zmniej szenie indeksów tablicy w implementacjach metody exch() i 1e s s (
pozwala utworzyć wersję, która sortuje elementy od a [0] do a [N- 1], tak jak inne metody
sortowania.
a [i]
N k 0 1 2 3 4 5 6 7 8 9 10 11
Początkowe wartości s 0 R T E X A M P L E
11 5 s 0 R T L X A M P E E
11 4 s 0 R T L X A M P E E
11 3 s 0 X T L R A M P E E
11 2 s T X P L R A M 0 E E
11 1 X T S P L R A M 0 E E
Uporządkowane w kopiec X T S P L R A M 0 E E
10 1 T P S 0 L R A M E E X
9 1 S P R 0 L E A M E T X
8 1 R P E 0 L E A M S T X
7 1 P 0 E M L E A R S T X
6 1 0 M E A L E P R s T X
5 1 M L E A E 0 P R s T X
4 1 L E E A M 0 P R s T X
3 1 E A E L M 0 P R s T X
2 1 E A E L M 0 P R s T X
1 1 A E E L M 0 P R s T X
Posortowane wyniki A E E L M 0 P R s T X
Ślad przebiegu sortowania przez kopcowanie
(zawartość tablicy po każdym „zatopieniu")
2.4 h Kolejki priorytetowe 337
Tworzenie kopca Sortowanie
e x c h (l, 6)
s i n k ( l , 5)
Punkt wyjścia (kopiec)
sink(5, e x c h (l, 11) e x c h (l, 5)
s i n k ( l , 10) s i n k (1, 4)
M O P
R S T X
sin k (4 , 11) e x c h (l, 10) e x c h (l, 4)
s i n k ( l , 9) s i n k ( l , 3)
L M O P
R S T X
sin k (3 , 11) e x c h (l, 9) e x c h (l, 3) ^
s i n k ( l , 8) s i n k ( l , 2)
L M O p
R S T X
e x c h (l, 8) e x c h ( l , 2) g)
s i n k ( l , 7) s i n k ( l , 1)
L M O P
R S T X
e xch (l, 7)
s in k (l, 11 ) s i n k ( l , 6)
2 E 3 E
4L 5 M 60 7P
1 9 10 11
R S T X
Efekt (posortowane)
Efekt (kopiec)
Sortowanie przez kopcowanie - tworzenie kopca (po lewej) i sortowanie (po prawej)
338 R O ZD ZIA Ł 2 a Sortowanie
Dane Sortowanie Większość pracy w sortowa
wejściowe III 1.1 niu przez kopcowanie ma miejsce w drugim
etapie, przy usuwaniu największego z pozo
stałych elementów z kopca i umieszczaniu
li I i . l i
go w tablicy na pozycji zwolnionej przez
I I I skrócenie kopca. Proces ten przypomina
ii.,il i I.I nieco sortowanie przez wybieranie (pobie
Struktura ranie elementów w malejącej, a nie rosnącej
kopca ^ u IIIIIIIIIb .iIiIiI.I
kolejności), jednak wymaga znacznie mniej
Czerwone
elementy sq porównań, ponieważ sterta zapewnia dużo
zatapiane wydajniejszy sposób wyszukiwania naj
III I I większego elementu w nieposortowanej
części tablicy.
II I i .1
Illlliilli..iii* 1
Twierdzenie S. Sortowanie przez kop
I I I ■ 1
cowanie wymaga mniej niż 2N lg N +
II 1 fili 2N porównań (i o połowę mniej przesta
I K I I H 1 1 . 1 . . 11I Szare elementy wień) przy sortowaniu N elementów.
/ nie zmieniają
lllliiii.i..ill ; '■'• y ' pozycji Dowód. Wyraz 2N dotyczy kosztu two
1811
rzenia kopca (zobacz t w i e r d z e n i e r ).
II i i 1
Wyraz 2N lg N wynika z ograniczenia
l i i . I 1191
kosztu każdej operacji „zatapiania”
l i . 1 nil w czasie sortowania do 2lg N (zobacz
T W IE R D Z E N IE Q ).
li n 1 Czarne elementy
3 y) i Slł uwzględniane
1 i D 1 zz przy przestawianiu
a lg o r ytm 2.7 to pełna implementacja op
ii ■ i 1118
arta na opisanych pomysłach. Jest to kla
li H il syczny algorytm sortowania przez kopco
1 . \ 1118 wanie, wymyślony przez J. W. J. Williamsa
i dopracowany przez R. W. Floyda w 1964
l . .1 9188
roku. Choć pętle w programie na pozór
■. ■ 99 88 wykonują inne zadania (pierwsza tworzy
...............I l l l l l l 1988 kopiec, a druga niszczy go w ramach sorto
wania), obie są oparte na metodzie sin k ().
c c !; k IN I
towane Przedstawiamy implementację wykracza
dane ...111111111111 llll jącą poza opisany interfejs API dla kolejek
priorytetowych, aby podkreślić prostotę
zualny ślad działania sortowania przez kopcowanie
omawianego algorytmu sortowania (m eto
da so rt () zajmuje osiem wierszy, a metoda
sink() — kolejnych osiem) i pokazać sor
towanie w miejscu.
2.4 □ Kolejki priorytetowe 339
Działanie algorytmu — jak zwykle — można lepiej zrozumieć, analizując ślad wi
zualny. Początkowo może się wydawać, że proces wykonuje operacje odwrotne od
sortowania, ponieważ przenosi duże elementy na początek tablicy w ramach tworze
nia kopca. Jednak później metoda bardziej przypomina lustrzane odbicie sortowania
przez wybieranie (choć wymaga znacznie mniej porównań).
Wiele osób badało sposoby na usprawnienie implementacji kolejek prioryteto
wych opartych na kopcach i sortowania przez kopcowanie (podobnie jak wszystkich
innych omawianych metod). Dalej pokrótce opisano jedną z modyfikacji.
Zatapianie do poziom u dna i późniejsze wypływanie Większość elementów ponow
nie wstawianych do kopca w czasie sortowania dociera do dna. Floyd w 1964 roku
zauważył, że można zaoszczędzić czas przez pominięcie sprawdzania, czy element
znalazł się na docelowej pozycji. W tym celu wystarczy awansować większe z dwójki
dzieci do m om entu dotarcia węzła do dna, a następnie przenieść węzeł w górę ster
ty na właściwe miejsce. Rozwiązanie to zmniejsza liczbę porównań asymptotycznie
o czynnik równy 2 , co pozwala zbliżyć się do liczby potrzebnej w sortowaniu przez
scalanie (dla losowo uporządkowanych tablic). M etoda wymaga dodatkowych ope
racji porządkujących i jest przydatna w praktyce tylko wtedy, kiedy koszt porównań
jest stosunkowo wysoki (na przykład przy sortowaniu elementów o długich kluczach,
takich jak łańcuchy znaków).
s o r t o w a n i e p r z e z k o p c o w a n i e m a d u ż e z n a c z e n i e w dziedzinie badań nad zło
żonością sortowania (zobacz stronę 291), ponieważ jako jedyna z opisanych metod
jest optymalna (w ramach stałego czynnika) w zakresie wykorzystania czasu i pam ię
ci. Dla najgorszego przypadku gwarantowana jest liczba ~2N lg N porównań i stała
ilość dodatkowej pamięci. Przy bardzo małej ilości pamięci (na przykład w syste
mach osadzonych lub w tanich urządzeniach przenośnych) technika ta jest popular
na, ponieważ m ożna ją zaimplementować za pom ocą kilkudziesięciu wierszy (nawet
w kodzie maszynowym) przy zachowaniu optymalnej wydajności. Jednak w typo
wych sytuacjach we współczesnych systemach rzadko się ją stosuje, ponieważ nie
współdziała z buforowaniem. Elementy tablicy rzadko są porównywane z bliskimi
elementami, dlatego liczba dostępów do innych buforów jest znacznie wyższa niż
w sortowaniu szybkim, sortowaniu przez scalanie, a nawet sortowaniu Shella, gdzie
większość porównań dotyczy bliskich elementów.
Jednak używanie kopców do implementowania kolejek priorytetowych jest coraz
powszechniejsze we współczesnych zastosowaniach, ponieważ umożliwia łatwe za
gwarantowanie logarytmicznego czasu wykonania w sytuacjach, kiedy duża liczba
operacji wstaw i usuń maksymalny jest wymieszana ze sobą. Kilka przykładów wyko
rzystania tej techniki opisano w dalszej części książki.
i
340 R O ZD ZIA Ł 2 □ Sortowanie
| PYTANIA I ODPOWIEDZI
P. Nadal nie jestem pewien, jaki jest cel stosowania kolejek priorytetowych. Dlaczego
nie wystarczy posortować danych, a następnie używać elementów umieszczonych
rosnąco w posortowanej tablicy?
O. Czasem przy przetwarzaniu danych, na przykład w programach TopM i Mul t i way,
łączna ilość danych jest zdecydowanie za duża, aby móc je posortować (a nawet za
pisać w pamięci). Jeśli szukasz 10 największych elementów wśród miliarda, czy na
prawdę chcesz sortować tablicę o miliardzie elementów? Za pom ocą kolejek priory
tetowych m ożna to zrobić przy użyciu 10-elementowej kolejki tego rodzaju. W in
nych sytuacjach może się zdarzyć, że w danym momencie nie wszystkie dane istnieją.
Trzeba na przykład pobrać dane z kolejki priorytetowej, przetworzyć je, a następnie
dodać do kolejki nowe elementy.
P. Dlaczego w klasie MaxPQ nie używamy interfejsu Comparabl e (stosowanego do sor
towania) zamiast generycznego elementu Item?
O. Użycie interfejsu wymagałoby, aby klient rzutował wartość zwracaną przez m eto
dę del Max () na rzeczywisty typ, na przykład S tri ng. Zwykle należy unikać rzutowa
nia w kodzie klienta.
P. Dlaczego w reprezentacji kopca nie używa się elementu a [0] ?
O. To podejście pozwala nieco uprościć obliczenia. N ietrudno jest zaimplementować
m etody dla kopca oparte na tablicy zaczynającej się od 0. Wtedy dziećmi elementu
a [0] są a [1] i a [2], dzieci elementu a [1] to a [3] i a [4], dzieci elementu a [2] to a [5]
i a [ 6] itd. Jednak większość programistów woli prostsze, stosowane także przez nas
obliczenia. Ponadto w niektórych zastosowaniach kopców przydatne jest ustawienie
a [ 0] jako wartownika (w rodzicu elementu a [ 1 ]).
P. Budowanie kopca w sortowaniu przez kopcowanie za pom ocą wstawiania ele
mentów jeden po drugim wydaje się prostsze niż skomplikowana metoda przecho
dzenia od dołu do góry, opisana na stronie 335. Po co stosować tę ostatnią?
O. W implementacji sortowania jest ona o 20% szybsza i wymaga o połowę mniej
skomplikowanego kodu (metoda swi m() nie jest potrzebna). Trudność zrozumienia
algorytmu nie zawsze ma dużo wspólnego z jego prostotą lub wydajnością.
P. Co się stanie po pominięciu fragmentu extends Comparabl e<Key> w implementa
cji klas podobnych do MaxPQ?
O. Jak zwykle najłatwiejszym sposobem na uzyskanie odpowiedzi jest spróbowanie.
Jeśli zrobisz to w klasie MaxPQ, otrzymasz błąd czasu kompilacji:
[Link][Link] cannot find symbol
symbol : method compareTo(Item)
Java informuje w ten sposób, że nie zna m etody compareTo() dla typu Item. Jest to
efekt pominięcia deklaracji Item extends Comparabl e<Item>.
2.4 a Kolejki priorytetowe 341
£ ĆWICZENIA
2.4.1 . Załóżmy, że kolejka priorytetow a jest początkowo pusta i otrzym ano ciąg
P R I 0 * R * * I * T * Y * * * Q U E * * * U * E (litera oznacza wstaw, a gwiazdka
— usuń maksymalny). Podaj ciąg liter zwróconych przez operacje usuń maksymalny.
2.4.2. Przeprowadź krytykę przedstawionego pomysłu: aby zaimplementować ope
rację znajdź maksymalny działającą w stałym czasie, m ożna użyć stosu lub kolejki
i śledzić maksymalną ze wstawionych do tej pory wartości, a następnie zwracać ją po
wywołaniu wspomnianej operacji.
2.4.3. Przedstaw implementacje kolejki priorytetowej z operacjami wstaw i usuń
maksymalny. Implementacje oprzyj na następujących strukturach danych: nieupo
rządkowanej tablicy, uporządkowanej tablicy, nieuporządkowanej liście powiązanej
i liście powiązanej. Dla każdej z czterech implementacji utwórz tabelę z ograniczenia
mi czasu wykonania wszystkich operacji dla najgorszego przypadku.
2.4.4. Czy tablica posortowana w porządku malejącym jest kopcem z łatwym do
stępem do maksimum?
2.4.5. Przedstaw kopiec uzyskany po wstawieniu kluczy E A S Y Q U E S T I O N
(w tej kolejności) do początkowo pustego kopca z łatwym dostępem do maksimum.
2.4.6. Na podstawie konwencji z ć w i c z e n i a 2 .4.1 podaj ciąg kopców wygenerowa
nych po wykonaniu operacji P R I O * R * * I * T * Y * * * Q U E * * * U * E
na początkowo pustym kopcu z łatwym dostępem do maksimum.
2.4.7. Największy element w kopcu musi występować na pozycji 1, a drugi musi
znajdować się na pozycji 2 lub 3. Podaj listę pozycji w 31-elementowym kopcu, na
których k-ty największy element (i) może oraz (ii) nie może się pojawić dla k = 2 ,3
i 4 (zakładamy, że wartości są różne).
2.4.8. Wykonaj poprzednie ćwiczenie dla k-tego najmniejszego elementu.
2.4.9. Narysuj wszystkie różne kopce, które m ożna utworzyć na podstawie pięciu
kluczy A B C D E. Następnie narysuj wszystkie różne kopce, które m ożna zbudować
przy użyciu pięciu kluczy A A A B B.
2.4.10. Załóżmy, że chcemy uniknąć m arnowania jednej pozycji w uporządkowanej
w kopiec tablicy pq [ ] . W tym celu największą wartość umieszczamy na pozycji pq [ 0],
dzieci na pozycjach pq [ 1 ] oraz pq [ 2 ] i tak dalej na kolejnych poziomach. Gdzie znaj
dują się rodzic i dzieci elementu pq [k] ?
2.4.11 . Przyjmijmy, że aplikacja wykonuje bardzo dużą liczbę operacji wstaw, nato
miast tylko nieliczne operacje usuń maksymalny. Która implementacja kolejki prio
rytetowej będzie Twoim zdaniem najwydajniejsza: kopiec, tablica nieuporządkowana
czy tablica uporządkowana?
342 RO ZD ZIA Ł 2 a Sortowanie
ĆWICZENIA (ciągdalszy)
2.4.12. Załóżmy, że w aplikacji potrzebnych jest wiele operacji znajdź maksymalny,
ale stosunkowo niewiele operacji wstaw i usuń maksymalny. Która implementacja
kolejki priorytetowej będzie Twoim zdaniem najskuteczniejsza — kopiec, tablica nie
uporządkowana czy tablica uporządkowana?
2.4.13. Opisz sposób na uniknięcie testu j < Nw metodzie sin k ().
2.4.14. Jaka jest minimalna liczba elementów, które trzeba przestawić w operacji
usuń maksymalny w kopcu o wielkości N i bez powtarzających się kluczy? Przedstaw
15-elementowy kopiec, dla którego można uzyskać to minimum. Wykonaj to samo
ćwiczenie dla dwóch i trzech kolejnych operacji usuń maksymalny.
2 .4.15. Zaprojektuj działający w czasie liniowym algorytm kontrolny do sprawdza
nia, czy tablica pq [] jest kopcem z łatwym dostępem do minimum.
2 .4 .1 6 . Dla N = 32 podaj tablice elementów, dla których w sortowaniu przez kopco-
wanie potrzeba maksymalnej i minimalnej liczby porównań.
2.4.17. Udowodnij, że zbudowanie /c-elementowej kolejki priorytetowej z łatwym
dostępem do m inim um i przeprowadzenie N - k operacji zastęp minimum (wstaw,
a następnie usuń m inim um ) powoduje pozostawienie w kolejce priorytetowej k naj
większych spośród N elementów.
2.4.18. Załóżmy, że klient klasy MaxPQ wywołuje metodę in s e rt() z elementem,
który jest większy od wszystkich innych elementów kolejki, a następnie natychmiast
wywołuje delMax(). Przyjmijmy, że klucze się nie powtarzają. Czy wynikowy kopiec
jest identyczny z kopcem sprzed operacji? Odpowiedz na to samo pytanie dla dwóch
operacji in s e rt() (pierwsza z kluczem większym od wszystkich innych z kolejki,
druga z kluczem większym od pierwszego), po których następują dwie operacje del -
Max().
2.4.19. Zaimplementuj dla klasy MaxPQ konstruktor, który przyjmuje jako argument
tablicę elementów. Wykorzystaj metodę tworzenia kopca od dołu do góry, opisaną
na stronie 335.
2.4.20. Udowodnij, że tworzenie kopca przez „zatapianie” wymaga mniej niż 2N
porównań i mniej niż N przestawień.
2.4 ■ Kolejki priorytetowe 343
£ PROBLEMY DO ROZWIĄZANIA
2.4.21. Podstawowe struktury danych. Wyjaśnij, jak użyć kolejki priorytetowej do
zaimplementowania opisanych w r o z d z i a l e i . typów danych dla stosu, kolejki i ko
lejki zwracającej losowy element.
2.4.22. Zmienianie rozmiaru tablicy. Dodaj do klasy MaxPQ możliwość zmiany wiel
kości tablicy. Udowodnij ograniczenia liczby dostępów (po amortyzacji) do tablicy
podobne do tych z t w i e r d z e n i a q .
2.4.23. Kopce a-arne. Uwzględnij koszt samych porównań i przyjmij, że znalezienie
t największych elementów wymaga t porównań. Na tej podstawie znajdź wartość f,
która minimalizuje współczynnik liczby porównań, N lg N, przy używaniu do sor
towania przez kopcowanie kopca o f-arnego. Najpierw zastosuj proste uogólnienie
działania m etody s i n k (). Następnie załóż, że m etoda Floyda pozwala zaoszczędzić
jedno porównanie w pętli wewnętrznej.
2.4.24. Kolejki priorytetowe z bezpośrednimi odnośnikami. Zaimplementuj kolejkę
priorytetową za pom ocą drzewa binarnego uporządkowanego w kopiec, użyj jed
nak potrójnie powiązanej struktury zamiast tablicy Potrzebne będą trzy odnośniki
na węzeł — dwa do przechodzenia w dół drzewa i jeden do poruszania się w górę.
Implementacja powinna gwarantować logarytmiczny czas wykonania operacji, na
wet jeśli nie wiadomo z góry, jaki jest maksymalny rozmiar kolejki priorytetowej.
2.4.25. Obliczeniowa teoria liczb. Napisz program [Link], który wyświetla
posortowane wszystkie liczby całkowite w postaci a 3 + b3 (gdzie a i b to liczby całko
wite od 0 do N), nie używając dużej ilości pamięci. Zamiast obliczać tablicę N 2 sum
i sortować je, należy zbudować kolejkę priorytetową z łatwym dostępem do minimum,
zawierającą początkowo elementy (O3, 0, 0), ( l 3, 1, 0), (23, 2,0),..., (N3, N, 0). Następnie,
dopóki kolejka priorytetowa jest niepusta, należy usuwać najmniejszy element (i3 + j3,
i,j), wyświetlać go, a potem — jeśli j < N — wstawiać element (i3 + (j+1)3, i, j+1). Użyj
programu do znalezienia wszystkich różnych wartości typu MInteger a, b, c i d od 0
do 10 6, takich że a3 + b3 = c3 + d3.
2.4.26. Kopiec bez przestawień. Ponieważ w operacjach sink() i swim() używana jest
prosta metoda exch (), elementy są wczytywane i zapisywane dwa razy częściej niż to ko
nieczne. Przedstaw wydajniejsze, podobne do sortowania przez wstawianie implemen
tacje, pozwalające uniknąć tego niewydajnego podejścia (zobacz ć w i c z e n i e 2 .1 .25 ).
2.4.27. Znajdź minimum. Dodaj do klasy MaxPQ metodę min(). Implementacja p o
winna działać w stałym czasie i korzystać ze stałej ilości pamięci.
2.4.28. Filtr pobieranych danych. Napisz klienta TopM, który wczytuje ze standardo
wego wejścia punkty (x , y, z), pobiera wartość M z wiersza poleceń i wyświetla M
punktów najbliższych (według odległości euklidesowej) początkowi układu. Oszacuj
czas działania klienta dla N = 108 i M = 104.
344 RO ZD ZIA Ł 2 a Sortowanie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
2.4.29. Kolejki priorytetowe z łatwym dostępem do minimum i maksimum.
Zaprojektuj typ danych, który umożliwia następujące operacje: wstaw, usuń mak
simum, usuń minimum (wszystkie działające w czasie logarytmicznym) oraz znajdź
maksimum i znajdź minimum (obie działające w czasie stałym). Wskazówka: użyj
dwóch kopców.
2.4.30. Dynamiczne znajdowanie mediany. Zaprojektuj typ danych umożliwiający
wykonanie operacji wstaw w czasie logarytmicznym, operacji znajdź medianę w cza
sie stałym i operacji usuń medianę w czasie logarytmicznym. Wskazówka: użyj kop
ców z łatwym dostępem do m inim um i maksimum.
2.4.31. Szybkie wstawianie. Opracuj opartą na porównaniach implementację inter
fejsu API klasy MinPQ. W implementacji operacja wstaw m a wymagać -lo g log N
porównań, a operacja usuń m inim alny 2 log N porównań. Wskazówka: w m e
todzie swim() zastosuj wyszukiwanie binarne na wskaźnikach rodzica, aby znaleźć
przodka.
2.4.32. Dolne ograniczenie. Udowodnij, że niemożliwe jest opracowanie opartej na
porównaniach implementacji interfejsu API klasy MinPQ, tak aby zarówno operacja
wstaw, jak i usuń minimalny miały gwarantowaną liczbę ~ N log log N porównań.
2.4.33. Implementacja indeksowanej kolejki priorytetowej. Zaimplementuj podsta
wowe operacje z interfejsu API indeksowanej kolejki priorytetowej, opisanego na
stronie 332. W tym celu zmodyfikuj a l g o r y t m 2.6 w następujący sposób: zmień
pq [] tak, aby przechowywała indeksy, dodaj tablicę keys [] na wartości kluczy i do
daj tablicę qp[], będącą odwrotnością tablicy pq []. Element qp[i] określa pozycję
wartości i w pq[] (jest to indeks j, taki że pq[j] to i). Następnie zmodyfikuj kod
a l g o r y t m u 2 .6 , aby zachowywał te struktury danych. Zastosuj konwencję, zgod
nie z którą qp [i ] = -1, jeśli i nie znajduje się w kolejce. Dodaj metodę contains()
sprawdzającą tę wartość. Musisz zmodyfikować m etody pomocnicze exch () i 1ess (),
jednak nie trzeba zmieniać m etod si nk() lub swim().
2.4 o Kolejki priorytetowe 345
Częściowe rozwiązanie:
public c la s s IndexMinPQ<Key extends Comparable<Key»
{
private in t N; // Liczba elementów w kolejce priorytetowej,
private i n t [] pq; // Kopiec binarny indeksowany od jedynki,
private in t [ ] qp; // Odwrotność: q p[pq[i]] = pq[qp[i]] = i.
private Key[] keys; // Elementy z priorytetami,
public IndexMinPQ(int maxN)
{
keys = (Key[]) new Comparable [maxN + 1];
pq = new int[maxN + 1];
qp = new int[maxN + 1];
fo r (in t i = 0; i <= maxN; i++) q p[i] = -1;
}
public boolean isEmptyO
{ return N == 0; }
public boolean co n t a in s (in t k)
( return qp[k] != -1; }
public void i n s e r t ( i n t k, Key key)
{
N++;
qp [k] = N;
pq[N] = k;
keys[k] = key;
swim(N);
}
public Item min()
{ return keys [pq [1 ]]; }
public in t delMin()
{
in t indexOfMin = pq[1];
exch(l, N --);
s in k (l);
keys[pq[N+l]] = n u l l ;
qp[pq[N+l]] = -1;
return indexOfMin;
}
}
346 R O ZD ZIA Ł 2 ■ Sortow anie
PROBLEMY DO ROZWIĄZANIA (ciągdalszy)
2.4.34. Implementacja indeksowanej kolejki priorytetowej (dodatkowe operacje).
Dodaj metody m in ln d e x(), change() i d e le te () do implementacji z ć w i c z e n i a
2-4-33-
Rozwiązanie:
public in t minlndex()
{ return p q [ l ] ; }
public void change(int k, Item item)
{
keys [k] = key;
swim(qp[k]);
s in k ( q p [ k ] );
}
public void d e le te (in t k)
{
exch(k, N --);
swim(qp[k]);
s in k ( q p [ k ] );
keys [pq [N+l] ] = n u l l ;
q p [p q [N + i]] = -i;
}
2.4.35. Pobieranie próbek z rozkładu dyskretnego. Napisz klasę Sample z konstruk
torem, który przyjmuje jako argument tablicę p [] wartości typu doubl e i obsługuje
dwie operacje — random() (zwraca indeks i z prawdopodobieństwem p [i]/T , gdzie
T to suma liczb z p []) i ch an g e (i, v) (zmienia wartość p [ i ] na v). Wskazówka: użyj
zupełnego drzewa binarnego, w którym każdy węzeł ma wagę p [i ]. W każdym węźle
zapisz skumulowaną wagę wszystkich węzłów z poddrzewa. Aby wygenerować loso
wy indeks, wybierz losową liczbę z przedziału 0 - T i użyj skumulowanych wag do
ustalenia, którą gałąź poddrzewa należy sprawdzić. Przy aktualizowaniu p [i] zmień
wszystkie wagi w węzłach na ścieżce z korzenia do i . Podobnie jak w przypadku kop
ców, tak i tu unikaj bezpośrednich wskaźników.
2.4 h Kolejki priorytetowe 347
[ e k spe r y m en t y
2.4.36- Spraw dzanie wydajności I. Napisz program kliencki do sprawdzania wydaj
ności, który używa operacji w staw do zapełnienia kolejki priorytetowej, a następnie
wywołuje operację usuń m aksym aln y w celu usunięcia połowy kluczy, używa operacji
wstaw do ponownego zapełnienia struktury, potem korzysta z operacji usuń m aksy
malny do usunięcia wszystkich kluczy. Proces ten powtarzany jest wielokrotnie na
różnych ciągach (i krótkich, i długich) kluczy. Zmierz czas każdego przebiegu i wy
świetl średnie czasy wykonania (lub narysuj wykres z nimi).
2.4.37. Spraw dzanie w ydajności II. Napisz program kliencki do sprawdzania wydaj
ności, który używa operacji w staw do zapełnienia kolejki priorytetowej, następnie
wykonuje tyle operacji usuń m aksym alny i w staw , ile zdoła w ciągu jednej sekundy.
Proces ten jest powtarzany wielokrotnie na różnych ciągach (i krótkich, i długich)
kluczy. Wyświetl średnią liczbę wykonanych operacji usuń m aksym aln y (lub narysuj
wykres z tą wartością).
2.4.38. Spraw dzanie popraw ności. Napisz program kliencki do sprawdzania po
prawności, używający m etod z interfejsu dla kolejki priorytetowej z a l g o r y t m u 2.6
dla trudnych lub „patologicznych” przypadków, które mogą pojawić się w praktycz
nych zastosowaniach. Proste przykłady to już uporządkowane klucze, klucze zapisa
ne w odwrotnej kolejności, same identyczne klucze i ciągi kluczy o dwóch różnych
wartościach.
2.4.39. K oszt tw orzen ia kopca. Określ empirycznie procent czasu, jaki w sortowaniu
przez kopcowanie zajmuje etap tworzenia kopca dla N = 103, 105 i 109.
2.4.40. M etoda Floyda. Zaimplementuj wersję sortowania przez kopcowanie opartą
na opisanym w tekście pomyśle Floyda (zatapianie do dna i późniejsze wypływanie).
Ustal liczbę porównań wykonywanych przez ten program i liczbę porównań w stan
dardowej implementacji dla losowo uporządkowanych różnych kluczy przy N - 10 3,
106 i 10 9.
2.4.41. Kopce a-arne. Zaimplementuj wersję sortowania przez kopcowanie opartą
na zupełnych drzewach 3-arnych i 4-arnych uporządkowanych w kopiec (tak jak
opisano to w tekście). Określ liczbę porównań w każdej wersji oraz liczbę porównań
w standardowej implementacji dla losowo uporządkowanych różnych kluczy przy
N = 10 3, 106 i 109.
2.4.42. Kopce w porzą d k u preorder. Zaimplementuj wersję sortowania przez kop
cowanie, której drzewo uporządkowane w kopiec reprezentowane jest w porządku
preorder, a nie według poziomów. Określ liczbę porównań w programie oraz liczbę
porównań w standardowej implementacji dla losowo uporządkowanych kluczy przy
N = 103,1 0 6 i 109.
algorytm y i kolejki priorytetowe mają wiele różnorodnych zastoso
s o r t o w a n ia
wań. Naszym celem w tym podrozdziale jest krótki przegląd niektórych zastosowań,
przedstawienie kluczowej roli opisanych wcześniej wydajnych m etod i omówienie
kroków potrzebnych do wykorzystania kodu do sortowania i obsługi kolejek prio
rytetowych.
Sortowanie jest tak przydatne głównie dlatego, że dużo łatwiej jest wyszukiwać
elementy w tablicach posortowanych niż w nieposortowanych. Już od ponad stu lat
ludzie wiedzą, że łatwo jest znaleźć num er telefonu w książce telefonicznej, w której
wpisy są posortowane według nazwisk. Obecnie cyfrowe odtwarzacze muzyki po
rządkują pliki według nazwisk wykonawców lub tytułów utworów; wyszukiwarki
wyświetlają wyniki według ich adekwatności w porządku malejącym; arkusze kal
kulacyjne wyświetlają kolumny posortowane według konkretnego pola; narzędzia
do przetwarzania macierzy sortują liczby rzeczywiste będące wartościami własnymi
macierzy w porządku malejącym itd. Kiedy tablica jest posortowana, łatwiejsze jest
wykonywanie także innych zadań — od wyszukiwania nazw w posortowanym in
deksie w końcowej części książki przez usuwanie powtórzeń na długich listach wy
syłkowych, osób uprawnionych do głosowania lub witryn po wykonywanie obliczeń
statystycznych, takich jak usuwanie skrajnych wartości, znajdowanie mediany lub
wyznaczanie percentyli.
Sortowanie bywa też kluczowym podproblemem w wielu obszarach, które na po
zór nie mają nic wspólnego z sortowaniem. Kompresja danych, grafika kom putero
wa, biologia obliczeniowa, zarządzanie łańcuchem dostaw, optymalizacja kombinato-
ryczna, wybory społeczne i głosowanie to tylko kilka z wielu przykładów. Algorytmy
rozważane w tym rozdziale odgrywają kluczową rolę w rozwijaniu wydajnych algo
rytmów przedstawionych w każdym z dalszych rozdziałów książki.
Najważniejsze jest sortowanie systemowe, dlatego rozpoczynamy od omówienia
wielu praktycznych zagadnień, pojawiających się przy projektowaniu sortowania do
użytku w różnorodnych klientach. Choć niektóre z omawianych zagadnień są specy
ficzne dla Javy, każda kwestia odzwierciedla trudności, które trzeba rozwiązać w do
wolnym systemie.
Głównym celem jest tu zademonstrowanie, że choć użyto stosunkowo prostych
mechanizmów, rozważane implementacje mają wiele zastosowań. Lista sprawdzo
nych zastosowań szybkich algorytmów sortowania jest długa, dlatego omawiamy
tylko mały wycinek: wybrane zastosowania naukowe, algorytmiczne i komercyjne.
O wiele więcej przykładów znajduje się w ćwiczeniach i w witrynie. Ponadto często
odwołujemy się do wcześniejszych fragmentów tego rozdziału w celu skutecznego
rozwiązania problemów omawianych w dalszych częściach tej książkil
2.5 B Zastosowania 349
S o rto w a n ie ró żn y ch ty p ó w d an ych Przedstawione implementacje sortują
tablice obiektów zgodnych z interfejsem Comparable. Ta konwencja Javy umożliwia
zastosowanie mechanizmu wywołań zwrotnych do sortowania tablic obiektów do
wolnego typu z implementacją tego interfejsu. Jak opisano to w p o d r o z d z i a l e 2.1,
zaimplementowanie interfejsu Comparable sprowadza się do zdefiniowania metody
compareTo() określającej porządek naturalny dla danego typu. Opracowany przez
nas kod m ożna natychmiast zastosować do sortowania tablic typu S tri ng, Integer,
Doubl e i innych, takich jak Fi 1e i URL, ponieważ wszystkie te typy implementują in
terfejs Comparable. Możliwość użycia tego samego kodu dla wszystkich typów jest
wygodna, jednak w standardowych sytuacjach potrzebna jest obsługa typów danych
zdefiniowanych na potrzeby danej aplikacji. Dlatego często implementuje się metodę
compareToO dla typów danych zdefiniowanych przez użytkownika, tak aby imple
mentowały interfejs Comparable, co umożliwia w kodzie klienta sortowanie tablic
określonego typu (i budowanie kolejek priorytetowych z wartościami tego typu).
Przykład — transakcje Przykładowym obszarem, w którym sortowanie ma wiele
zastosowań, jest komercyjne przetwarzanie danych. Wyobraźmy sobie firmę zajmu
jącą się handlem elektronicznym, która przechowuje rekord dla każdej transakcji
dotyczącej konta klienta. Rekord obejmuje wszystkie ważne informacje — nazwisko
klienta, datę, kwotę transakcji itd. Obecnie firmy przetwarzają miliony transakcji.
W ć w i c z e n i u 2.1 .2 1 pokazano, że porządek naturalny transakcji może być oparty na
ich wartości. Można zaimplementować takie rozwiązanie przez dodanie odpowied
niej metody compareToO do definicji klasy. Dzięki takiej definicji można przetwarzać
tablicę a[] obiektów typu Transaction po posortowaniu jej na przykład za pomocą
wywołania Q [Link](a). Metody sortowania nie znają typu danych Transaction,
ale interfejs Comparabl e Javy umożliwia zdefiniowanie porządku naturalnego, dlatego
można zastosować dowolną metodę do sortowania obiektów tego typu. Inna możli
wość to określenie, że obiekty typu Transact i on należy sortować według dat. Wymaga
to zaimplementowania metody compareToO porównującej pola Date. Ponieważ obiek
ty typu Date są zgodne z interfejsem Comparabl e, wystarczy wywołać metodę com-
pareTo() typu Date — nie trzeba implementować jej od podstaw. Sensowne jest też
porządkowanie danych według nazwisk odbiorców. Umożliwienie klientom zmiany
porządku to ciekawe zagadnienie, któremu wkrótce się przyjrzymy.
p u b lic in t com pareTo(Transaction that)
{ return this.w [Link] pareTo(that.w hen); }
Inna implementacja metody compareToO,
umożliwiająca sortowanie transakcji według dat
350 R O ZD ZIA Ł 2 0 Sortowanie
Sortowanie w skaźników Używane tu podejście jest nazywane w klasycznej litera
turze sortowaniem wskaźników, ponieważ kod przetwarza referencje do elementów
i nie przenosi samych danych. W językach w rodzaju C i C++ programista bezpośred
nio określa, czy chce manipulować danymi czy wskaźnikami. W Javie automatycznie
używa się wskaźników. Zawsze manipulujemy referencjami do obiektów (wskaź
nikami), a nie samymi obiektami (wyjątkiem są proste typy liczbowe). Sortowanie
wskaźników powoduje powstanie warstwy pośredniej. Tablica zawiera referencje
do sortowanych obiektów, a nie same obiekty. Pokrótce omawiamy pewne związane
z tym kwestie w kontekście sortowania. Jeśli tablica zawiera różne referencje, elemen
ty m ożna sortować według różnych części tych samych danych (na przykład według
wielu kluczy, co opisano dalej).
N iezm ienne klucze Sensowne jest założenie, że tablica może stać się nieuporząd
kowana, jeśli klient może zmieniać wartości kluczy po sortowaniu. Podobnie trudno
oczekiwać prawidłowego działania kolejki priorytetowej, jeśli klient może modyfi
kować wartości kluczy między operacjami. W Javie sensowne jest stosowanie klu
czy niezmiennych, co uniemożliwia ich modyfikowanie. Standardowe typy danych
stosowane jako klucze (na przykład S t r i ng, Integer, Doubl e i Fi 1e) są w większości
niezmienne.
N iekosztow ne przestaw ienia Inną zaletą stosowania referencji jest uniknięcie
kosztów przenoszenia całych elementów. Dla tablic o dużych elementach (i małych
kluczach) oszczędności są znaczne, ponieważ w porównaniach potrzebny jest do
stęp tylko do małej części elementu — dostęp do większości danych w trakcie sorto
wania jest zbędny. Podejście oparte na referencjach sprawia, że koszt przestawienia
jest równy kosztowi porównania dla ogólnych sytuacji obejmujących dowolnie duże
elementy (dzieje się to kosztem dodatkowej pamięci na referencje). Jeżeli klucze są
długie, przestawienia mogą nawet okazać się mniej kosztowne od porównań. Jednym
ze sposobów analizowania wydajności algorytmów sortowania tablic liczb jest przyj
rzenie się łącznej liczbie wykonywanych porównań i przestawień przy założeniu, że
koszty tych operacji są porównywalne. W Javie wnioski płynące z przyjęcia tych za
łożeń sprawdzają się dla szerokiej klasy zastosowań, ponieważ sortowane są obiekty
typów referencyjnych.
Różne porządki Istnieje wiele aplikacji, w których — w zależności od sytuacji
— przydatne są różne sposoby porządkowania sortowanych obiektów. Interfejs
Comparator Javy umożliwia określenie wielu porządków w jednej klasie. Interfejs ma
jedną metodę publiczną compare(), która porównuje dwa obiekty. Jeśli typ danych
implementuje interfejs Comparator, można przekazać obiekt zgodny z tym interfej
sem do m etody sort () (która z kolei przekazuje go do 1 ess ()), tak jak w przykładzie
na następnej stronie. Mechanizm interfejsu Comparator umożliwia sortowanie tab
lic obiektów każdego typu z wykorzystaniem dowolnego porządku zupełnego, który
zdefiniujemy. Użycie interfejsu Comparator zamiast interfejsu Comparabl e pozwala le
piej oddzielić definicję typu od definicji służącej do porównywania dwóch obiektów
2.5 Q Zastosowania 351
danego typu. Zwykle istnieje wiele sposobów porównywania obiektów, a mechanizm
interfejsu Comparator pozwala wybrać jeden z nich. Przykładowo, aby posortować
tablicę a [] łańcuchów znaków bez uwzględniania wielkości znaków, można wywołać
instrukcję I n s e r t i o n . s o rt(a , String.CASE_INSENSITIVE_ORDER), co powoduje uży
cie komparatora CASE_INSENSITIVE_ORDER zdefiniowanego w klasie S t r in g Javy. Jak
łatwo sobie wyobrazić, reguły porządkowania łańcuchów znaków są skomplikowane
i różne dla poszczególnych języków naturalnych, dlatego Java udostępnia wiele kom
paratorów dla typu S tri ng.
Elem enty o w ielu kluczach W typowych zastosowaniach elementy mają wiele
zmiennych egzemplarza, które można wykorzystać jako klucze sortowania. W przy
kładzie dotyczącym transakcji jeden klient może sortować listę transakcji według
nazwisk odbiorców (na przykład aby zebrać wszystkie transakcje poszczególnych
osób), inny — według kwoty (na przykład aby zidentyfikować transakcje o wyso
kiej wartości), a jeszcze inne klienty — według innych pól. Mechanizm interfejsu
Comparator pozwala zapewnić taką elastyczność. Można zdefiniować wiele kom pa
ratorów, tak jak w nowej implementacji typu Transaction, przedstawionej w dolnej
części następnej strony. Przy takiej definicji klient może posortować tablicę obiektów
typu Transacti on według czasu, wywołując instrukcję:
I n s e r t i o n . s o rt(a , new T [Link]())
lub według kwoty, za pomocą wywołania:
In s e r t i o n . s o rt(a , new Tran saction .HowMuchOrder())
W sortowaniu porównania odbywają się poprzez wywołania zwrotne do podanej
w kodzie klienta m etody compare() typu Transaction. Aby uniknąć kosztów two
rzenia nowego obiektu Comparator przy każdym sortowaniu, w definicji kom para
tora można użyć zmiennych egzemplarza publ i c final (to rozwiązanie zastosowano
w Javie do komparatora CASE_INSENSITI VE_0RDER).
p u b lic s t a t ic void so rt(O b je c t[] a, Comparator c)
{
in t N = a .le ngth;
f o r ( in t i = 1; i < N; i++)
f o r (in t j = i ; j > 0 && le s s ( c , a [ j ] , a [ j -1] ) ; j - - )
exch(a, j , j - 1 ) ;
}
p riv a te s t a t ic boolean less(C om parator c, Object v, Object w)
{ return [Link] (v, w) < 0; }
p riv a te s t a t ic void exch(O bject[] a, in t i , in t j)
{ Object t = a [ i ] ; a [ i] = a [j ] ; a [j] = t ; }
Sortowanie przez wstawianie z wykorzystaniem obiektu Comparator
352 RO ZD ZIA Ł 2 a Sortowanie
Kolejki priorytetow e z kom paratoram i Elastyczność, jaką dają komparatory, jest
przydatna także w kolejkach priorytetowych. Aby rozwinąć standardową implementa
cję ( a l g o r y t m 2 .6 ) o obsługę komparatorów, należy wykonać następujące kroki:
■ Zaimportować bibliotekę j ava. uti 1 . Comparator.
■ Dodać do klasy MaxPQ zm ienną egzemplarza comparator i konstruktor, który
przyjmuje komparator jako argument i inicjuje nim zmienną comparator.
■ Dodać do metody l e s s ( ) kod, który sprawdza, czy zmienna comparator ma
wartość nuli (i używa tej zmiennej, jeśli wartość jest różna od nul 1 ).
Po wprowadzeniu opisanych zmian m ożna na przykład budować różne kolejki prio
rytetowe za pom ocą kluczy obiektów Transacti on, używając do porządkowania cza
su, miejsca lub num eru konta. Po usunięciu z klasy Mi nPQ fragmentu Key extends
Comparabl e<Key> można nawet dodać obsługę kluczy, które nie mają określonego
porządku naturalnego.
import j a v a .u til.C om parator;
p u b lic c la s s Transaction
{
p riv a te final S t r in g who;
p riv a te final Date when;
p riv a te final double amount;
p u b lic s t a t ic c la s s WhoOrder implements Com parator<Transaction>
{
p u b lic in t com pare(Transaction v, T ran sa ction w)
{ re tu rn [Link]([Link]); }
1
p u b lic s t a t ic c la s s WhenOrder implements Com parator<Transaction>
{
p u b lic in t com pare(Transaction v, T ran sa ction w)
{ return [Link]([Link]); }
)
p u b lic s t a t ic c la s s HowMuchOrder implements Com parator<Transaction>
{
p u b lic in t com pare(Transaction v, T ran sa ction w)
{
i f ([Link] < [Link]) return -1;
i f ([Link] > [Link]) return +1;
return 0;
1
1
}
Sortowanie przez wstawianie z wykorzystaniem obiektów Comparator
2.5 o Zastosowania 353
Stabilność M etoda sortowania jest stabilna, jeśli zachowuje względny porządek
równych kluczy w tablicy. Często cecha ta jest ważna. Rozważmy na przykład apli
kację z obszaru handlu elektronicznego, w której trzeba przetwarzać dużą liczbę
transakcji mających lokalizację i znacznik czasu. Początkowo załóżmy, że transak
cje są zapisywane w tablicy w porządku ich nadchodzenia, dlatego mają kolejność
zgodną ze znacznikami czasu. Teraz przyjmijmy, że w celu dalszego przetwarzania
aplikacja musi podzielić transakcje według lokalizacji. Można to łatwo zrobić, sortu
jąc tablicę według lokalizacji. Jeśli sortowanie jest niestabilne, po sortowaniu trans
akcje dla każdego miasta mogą nie mieć kolejności zgodnej ze znacznikami czasu.
Programiści, którzy nie są świadomi tego zagadnienia, często są zaskoczeni, kiedy
pierwszy raz stykają się z taką sytuacją. Mają wrażenie, że niestabilny algorytm po
mylił dane. Niektóre z m etod sortowania omawianych w rozdziale są stabilne (sor
towanie przez wstawianie i przez scalanie), natomiast liczne inne (sortowanie przez
wybieranie, sortowanie Shella, sortowanie szybkie i sortowanie przez kopcowanie)
— nie. Istnieją sposoby na zapewnienie stabilnego działania każdej metody sortowa
nia (zobacz ć w i c z e n i e 2 .5 .1 8 ), jednak jeśli stabilność ma duże znaczenie, zwykle le
piej użyć stabilnego algorytmu. Łatwo traktować stabilność jako standardową cechę,
natomiast w rzeczywistości żadna ze stosowanych w praktyce m etod nie zapewnia
stabilności bez znaczących kosztów czasowych lub pamięciowych. Naukowcy wymy
ślili odpowiednie algorytmy, jednak programiści uznali je za zbyt skomplikowane,
aby były przydatne.
P o so rto w an e w ed łu g lokalizacji P o sortow ane w edług lokalizacji
Posortowane w edług czasu (wersja n iestabilna) (wersja stabilna)
Gdańsk [Link] G dańsk 09:25 G d a ń sk [Link]
Poznań [Link] G dańsk 09:03 G d a ń sk [Link]
Kraków [Link] G dańsk 09:21 G d a ń sk [Link]
Gdańsk [Link] G d a ń sk 09:19 G d a ń sk [Link]
Kraków [Link] G dańsk 09:19 G d a ń sk [Link]
Gdańsk [Link] G d a ń sk 09:00 G d a ń sk [Link]
Szc ze c i n [Link] G d a ń sk 09:35 G d a ń sk [Link]
Szc ze c i n [Link] G d a ń sk 09:00 G d a ń sk [Link]
Poznań [Link] Kraków 09:01 Nie są ju ż Kraków [Link] Nadal
Gdańsk [Link] Kraków 09:00 posortowane Kraków [Link] posortowane
Gdańsk [Link] Poznań 09:37 według czasu P oznań [Link] według czasu
Gdańsk [Link] Poznań 09:00 P oznań [Link]
Szc ze c i n 0 9 : 2 2 ::43 Poznań 09:14 P oznań [Link]
Szc ze c i n 0 9 : 2 2 :: 54 Szcze cin 09:10 Szc ze cin [Link]
Gdańsk 0 9 : 2 5 :: 52 Szcze cin 09:36 Szc ze cin [Link]
Gdańsk 0 9 : 3 5 :: 21 Szcze cin 09:22 S zcze cin [Link]
Szczeci n [Link] Szc ze cin 09:10 S zc ze cin [Link]
Poznań [Link] Szc ze cin 09:22 S zc ze cin [Link]
Stabilność przy sortowaniu według drugiego klucza
354 RO ZD ZIA Ł 2 a Sortowanie
Który algorytm sortowania mam zastosować? W rozdziale omówiliśmy
wiele algorytmów sortowania, dlatego pytanie to samo się nasuwa. To, który algo
rytm jest najlepszy, zależy w dużym stopniu od aplikacji i implementacji. Zbadaliśmy
jednak pewne m etody do ogólnego użytku, które w wielu zastosowaniach mogą być
prawie tak skuteczne, jak najlepsze możliwe.
Tabela w dolnej części tej strony to ogólny przegląd ważnych cech algorytmów
sortowania omówionych w rozdziale. We wszystkich przypadkach oprócz sortowania
Shella (gdzie tempo wzrostu jest szacunkowe), sortowania przez wstawianie (gdzie
tempo wzrostu zależy od kolejności kluczy na wejściu) i obu wersji sortowania szyb
kiego (gdzie tempo wzrostu jest probabilistyczne i może zależeć od rozkładu kluczy
na wejściu) pomnożenie tem pa wzrostu przez odpowiednie stałe to skuteczny sposób
na prognozowanie czasu wykonania. Stałe są częściowo zależne od algorytmu (na
przykład sortowanie przez kopcowanie wymaga dwa razy więcej porównań niż sor
towanie przez scalanie, a obie m etody wykonują znacznie więcej dostępów do tablicy
niż sortowanie szybkie), jednak przede wszystkim zależą od implementacji, kom pi
latora Javy i komputera; czynniki te wyznaczają liczbę wykonywanych instrukcji m a
szynowych i czas potrzebny na każdą z nich. Co najważniejsze, ponieważ są to stałe,
zwykle m ożna przewidzieć czas wykonania dla dużych N na podstawie eksperymen
tów dla mniejszych N i ekstrapolacji (stosując standardowy schemat podwajania).
Tempo wzrostu dla N elementów
Działa
Algorytm Stabilny? w m iejscu? Czas Dodatkowa Uwagi
wykonania pamięć
Sortowanie
Nie Tak N2
przez wybieranie
Zależy od
Sortowanie
Tak Tak Od N do N 2 kolejności
przez wstawianie
elementów
N log N?
Sortowanie Shella Nie Tak
N6/5?
Gwarancje
Sortowanie szybkie Nie Tak NlogN lg N
probabilistyczne
Probabilistyczne;
Sortowanie szybkie
OdNdoN zależy też od
z podziałem Nie Tak Ig N
na trzy części
log N rozkładu kluczy
na wejściu
Sortowanie
Tak Nie N\ogN N
przez scalanie
Sortowanie
Nie Tak N\ogN 1
przez kopcowanie
Cechy algorytm ów sortowania zw iązane z w ydajnością
2.5 n Zastosowania 355
Twierdzenie T. Sortowanie szybkie to najszybsza m etoda sortowania do użytku
ogólnego.
Dowód. Podstawą dla tej hipotezy są niezliczone implementacje sortowania
szybkiego w niezliczonych systemach komputerowych opracowane od czasu wy
myślenia algorytmu kilkadziesiąt lat temu. Ogólnie powodem, dla którego sor
towanie szybkie jest najszybsze, jest mała liczba instrukcji w pętli wewnętrznej
(ponadto algorytm dobrze współdziała z buforowaniem, ponieważ zwykle używa
danych sekwencyjnie), dlatego czas wykonania wynosi ~c N l g N, przy czym war
tość c jest mniejsza niż odpowiadających jej stałych w innych liniowo-logaryt-
micznych m etodach sortowania. Przy podziale na trzy części sortowanie szybkie
działa liniowo dla pewnych rozkładów kluczy, które mogą wystąpić w praktyce
(inne sortowania działają wtedy w czasie liniowo-logarytmicznym).
Dlatego w większości praktycznych sytuacji sortowanie szybkie jest m etodą stoso
waną z wyboru. Jednak z uwagi na wiele zastosowań sortowania oraz różnorodność
komputerów i systemów trudno jest uzasadnić stwierdzenia tego rodzaju. Pokazano
już na przykład jeden ważny wyjątek — jeśli ważna jest stabilność i dostępna jest
pamięć, najlepsze może być sortowanie przez scalanie. W r o z d z i a l e 5 . opisano inne
wyjątki. Za pom ocą narzędzi w rodzaju SortCompare oraz poświęcając odpowiednią
ilość czasu i pracy, można przeprowadzić dokładniejsze badania nad porównaniem
wydajności algorytmów i usprawnień na danym komputerze, co opisano w kilku
ćwiczeniach w końcowej części podrozdziału. Prawdopodobnie najlepszą interpre
tacją t w i e r d z e n i a t jest napisanie, że z pewnością warto rozważyć zastosowanie
sortowania szybkiego w każdej sytuacji, w której czas wykonania m a znaczenie.
Sortowanie typów prostych W pewnych zastosowaniach, gdzie wydajność ma klu
czowe znaczenie, najważniejsze może być sortowanie liczb, dlatego warto unikać
kosztów stosowania referencji i zamiast tego sortować typy proste. Rozważmy na
przykład różnicę między sortowaniem tablic z wartościami typu doubl e i typu Doubl e.
W pierwszym przypadku to same liczby są przestawiane i umieszczane w tablicy
w posortowanej kolejności. W drugiej sytuacji przestawiane są referencje do zawie
rających liczby obiektów typu Doubl e. Jeśli trzeba tylko posortować dużą tablicę liczb,
można uniknąć kosztów zapisywania tej samej liczby referencji i dodatkowych kosz
tów dostępu do wartości poprzez referencje (nie wspominając już o kosztach wywo
ływania m etod compareTo() i 1ess ()). Można opracować wydajne wersje sortowania
na potrzeby takich sytuacji, zastępując Comparabl e nazwą typu prostego i zmieniając
definicję le s s () lub zastępując wywołania metody 1 ess () kodem w rodzaju a [i ] <
a [j] (zobacz ć w i c z e n i e 2 .1 .26 ).
Sortowanie system owe Javy Jako przykład wykorzystania informacji z tabeli ze
strony 354 rozważmy podstawową metodę sortowania systemowego Javy — java,
u til .A rra y [Link] rt(). Z uwagi na przeciążenie typów argumentów nazwa ta repre
zentuje kolekcję metod:
RO ZD ZIA Ł 2 a Sortowanie
■ różne m etody dla poszczególnych typów prostych;
■ metody dla typów danych z implementacją interfejsu Comparabl e;
■ metodę używającą obiektu Comparator.
Programiści systemów Javy zdecydowali się stosować sortowanie szybkie (z podzia
łem na trzy części) w metodach dla typów prostych i sortowanie przez scalanie dla
m etod dla typów referencyjnych. Podstawowe praktyczne skutki tych wyborów to,
jak opisano, uzyskanie szybkości i niskiego wykorzystania pamięci (dla typów pro
stych) lub stabilności (dla typów referencyjnych).
a l g o r y t m y i p o m y s ł y , które rozważaliśmy, wykorzystano jako ważną część wielu
współczesnych systemów, w tym Javy. Przy rozwijaniu programów w Javie praw
dopodobnie stwierdzisz, że implementacje m etody A rrays.s o r t () dostępne w tym
języku (nieraz uzupełnione własnymi implementacjami m etod compareTo() i (lub)
compare()) zaspokoją Twoje potrzeby, ponieważ będziesz używał sortowania szyb
kiego z podziałem na trzy części lub sortowania przez scalanie, czyli sprawdzonych,
klasycznych algorytmów.
W książce w klientach wymagających sortowania ogólnie używamy własnej m eto
dy Quick. s o rt() (zazwyczaj) lub Merge, s o rt () (jeśli ważna jest stabilność i nie trze
ba oszczędzać pamięci). Możesz swobodnie korzystać z metody Ar rays, so rt (), o ile
nie istnieją istotne przesłaniu do użycia innej metody.
Redukcje Podejście, zgodnie z którym algorytmy sortowania mogą służyć do
rozwiązywania innych problemów, jest przykładem zastosowania podstawowej
techniki projektowania algorytmów — redukcji. Redukcję omawiamy szczegółowo
w r o z d z i a l e 6 . z uwagi na jej znaczenie w teorii algorytmów. Do tego czasu om ó
wimy kilka praktycznych przykładów. Redukcja oznacza, że algorytm opracowa
ny do rozwiązania jednego problemu wykorzystano do poradzenia sobie z innym.
Programiści często stosują redukcję (choć nie zawsze jest to bezpośrednio zazna
czone). Za każdym razem, kiedy używasz metody rozwiązującej problem B do roz
wiązania problemu A, przeprowadzasz redukcję A do B. Jednym z celów przy imple
mentowaniu algorytmów jest ułatwienie redukcji przez zapewnienie, że algorytmy
będą przydatne w tak różnorodnych zastosowaniach, jak to możliwe. Zaczynamy od
kilku podstawowych przykładów sortowania. Wiele problemów ma postać algoryt
micznych zagadek, przy czym kwadratowy algorytm działający przez atak siłowy jest
oczywisty. Często wcześniejsze posortowanie danych umożliwia rozwiązanie prob
lemu w dodatkowo liniowym czasie, co zmniejsza łączne koszty z kwadratowych do
liniowo-logarytmicznych.
Pow tórzenia Czy w tablicy obiektów Comparabl e powtarzają się klucze? Ile różnych
wartości kluczy istnieje? Które wartości powtarzają się najczęściej? W małych tabli
cach na pytania tego typu łatwo odpowiedzieć za pomocą algorytmu kwadratowego,
który porównuje każdy element tablicy z każdym innym. W dużych tablicach stoso
wanie algorytmów kwadratowych jest niemożliwe. Za pom ocą sortowania można
2.5 o Zastosowania
odpowiedzieć na pytania w czasie liniowo-logarytmicznym — najpierw trzeba p o
sortować tablicę, a następnie przejść po posortowanej tablicy i zapisać powtarzające
się klucze, które w uporządkowanej tablicy występują jeden za drugim. Fragment
kodu po prawej stronie określa liczbę różnych kluczy w tablicy. Prosta modyfika
cja kodu pozwala odpowiedzieć na postawione wcześniej pytania i wykonać zadania
w rodzaju wyświetlenia wszystkich różnych wartości, wszystkich powtarzających się
wartości i tak dalej — nawet dla dużych tablic.
Permutacje Permutada to tablica N
. , . . , ,. Q m c k .so rt(a );
liczb całkowitych, w której każda liczba -¡p^ count = y / / zakładamy, że [Link] > 0.
z przedziału od 0 do N - l występuje do- = l ; i < a .le n gth ; i++)
f o r ( in t i
kładnie raz. Odległość tau Kendalla mię- lf Ca[i].compareTo(a[i-l]) != 0)
, . . . . . count++;
dzy dwoma permutacjam i to liczba par,
które mają w nich odm ienną kolejność. Zliczanie różnych kluczy w tablicy a[]
Na przykład odległość tau Kendalla m ię
dzy permutacjami 0 3 1 6 2 5 4 a l 0 3 6 4 2 5 wynosi cztery, ponieważ pary 0-1,
3-1,2-4, 5-4 mają w nich inną kolejność, a pozostałe pary — taką samą. Miara ta jest
powszechnie stosowana. W socjologii służy do badania wyborów społecznych i te
orii głosowania, a w biologii molekularnej — do porównywania genów przy użyciu
profili ekspresji. Ponadto jest używana w wyszukiwarkach do porządkowania wyni
ków i w wielu innych zastosowaniach. Odległość tau Kendalla między permutacją
i permutacją tożsamościową (w której każdy element jest równy indeksowi) to liczba
inwersji w permutacji. Nietrudno zaprojektować kwadratowy algorytm obliczania tej
odległości oparty na sortowaniu przez wstawianie (przypomnij sobie t w i e r d z e n i e
c z p o d r o z d z i a ł u 2 . i ) . Wydajne obliczanie odległości tau Kendalla to ciekawe ćwi
czenie dla programisty (lub studenta!) znającego opisane wcześniej klasyczne algo
rytmy sortowania (zobacz ć w i c z e n i e 2 .5 .1 9 ).
Redukcje oparte na kolejkach priorytetow ych W p o d r o z d z i a l e 2.4 omówiono
dwa przykłady problemów, które można zredukować do ciągu operacji na kolejkach
priorytetowych. Program TopM (strona 323) wyszukuje w strum ieniu wejściowym M
elementów o najwyższym kluczu. Program Multiway (strona 334) scala M posorto
wanych strum ieni wejściowych, aby utworzyć posortowany strum ień wyjściowy. Oba
te problemy można łatwo rozwiązać za pom ocą kolejki priorytetowej o długości M.
M ediana i inne m iary statystyczne Ważnym zastosowaniem związanym z sorto
waniem, przy czym nie jest tu konieczne pełne sortowanie, jest określanie dla kolek
cji kluczy mediany, czyli wartości, od której połowa kluczy jest nie większa i połowa
kluczy jest nie mniejsza. Operacja ta jest często wykonywana w statystyce i w innych
obszarach przetwarzania danych. Znajdowanie mediany to specjalny przypadek wy
bierania — znajdowania k -tej najmniejszej wartości w kolekcji liczb. Wybieranie ma
wiele zastosowań w przetwarzaniu danych eksperymentalnych i innych. Medianę
i inne statystyki pozycyjne powszechnie stosuje się do podziału tablicy na mniejsze
grupy. Często tylko mała część dużej tablicy jest zapisywana na potrzeby dalszego
358 R O ZD ZIA Ł 2 n Sortowanie
przetwarzania. Wtedy program, który potrafi wybrać na przykład 10% największych
elementów tablicy, może być bardziej przydatny niż rozwiązanie sortujące całą tabli
cę. Program TopM z p o d r o z d z i a ł u 2.4 rozwiązuje problem nieograniczonego stru
mienia wejściowego za pom ocą kolejki
p u b lic s t a t ic Comparable
priorytetowej. Wydajną alternatywą dla
select(Com parable[] a, in t k)
programu TopM, jeśli elementy znajdują się {
w tablicy, jest samo sortowanie. Po wywo StdRandom .shuffle(a);
łaniu Qui ck. so rt (a) k najmniejszych war in t lo = 0, hi = a .le n g th - 1;
w h ile (hi > lo)
tości znajduje się w tablicy na pierwszych
{
k pozycjach (dla k mniejszego niż długość in t j = p a r t it io n ( a , lo , h i) ;
tablicy). Jednak podejście to wymaga sor if (j == k) return a [ k ] ;
e lse i f (j > k) hi =j - 1;
towania, dlatego czas wykonania jest li-
e ls e i f (j < k) lo =j +1;
niowo-logarytmiczny. Czy można uzyskać }
lepszy wynik? Znalezienie k najmniejszych return a [ k ] ;
wartości w tablicy jest łatwe dla bardzo
małych lub bardzo dużych k. Problem Wybieranie k najmniejszych elementów z a[]
okazuje się trudniejszy, jeśli k to określona
część rozmiaru tablicy, na przykład przy wyszukiwaniu mediany (k=N/2). Możesz
się zdziwić, ale można rozwiązać problem w czasie liniowym, tak jak w przedsta
wionej powyżej metodzie s e le c t() (ta implementacja
wymaga rzutowania w kodzie klienta; w witrynie znajduje
się bardziej dopracowany kod, gdzie wymóg ten nie obo
wiązuje). Metoda s e l e c t () przechowuje zmienne lo i hi,
które ograniczają podtablicę obejmującą indeks k wybie
ranego elementu, i używa podziału z sortowania szybkiego
do zmniejszenia rozmiaru podtablicy. Przypominamy, że
m etoda p a r titio n () zmienia uporządkowanie tablicy od
a [1 o] do a [hi] i zwraca liczbę całkowitą j, taką że w arto
ści od a[lo ] do a [j - 1 ] są mniejsze lub równe względem
a [ j ] , a wartości od a [j+ 1 ] do a [hi] są mniejsze lub równe
względem a [ j ] . Jeśli k jest równe j, proces jest zakończony.
W przeciwnym razie przy k < j trzeba kontynuować pracę
na lewej podtablicy (przez zmianę wartości hi na j - 1 ), a je
śli k > j, należy kontynuować proces dla prawej podtablicy
(przez zmianę wartości lo na j+1). W pętli zachowywany
jest niezmiennik, zgodnie z którym żaden element na lewo
od 1 o nie jest większy, a żaden element na prawo od hi nie
jest mniejszy niż elementy z przedziału a [1 o .. h i] . Po po
dziale niezmiennik zostaje zachowany i m ożna zmniejszać
przedział do momentu, w którym obejmuje tylko k. Wtedy
a [k] zawiera (/c+1 ) najmniejszy element, elementy z pozy
P o d z ia ł w celu z n ale z ie n ia m e d ia n y cji od a [0] do a [k - 1 ] są mniejsze (lub równe) względem
2.5 Q Zastosowania 359
a [k], a elementy od a [k+1 ] do końca tablicy są większe (lub równe) względem a [k].
Aby zrozumieć, dlaczego algorytm działa w czasie liniowo-logarytmicznym, załóżmy,
że dane za każdym razem dzielone są dokładnie na połowę. Wtedy liczba porównań
wynosi N + N /2 + N /4 + N/8 + ..., a proces kończy się po znalezieniu k-tego najm niej
szego elementu. Suma wyrazów wynosi mniej niż 2 N. Ponadto, tak jak w sortowa
niu szybkim, trzeba posłużyć się matematyką, aby znaleźć rzeczywiste ograniczenie,
które jest nieco wyższe. Także podobnie jak w sortowaniu szybkim, analizy dotyczą
podziału według losowego elementu, dlatego gwarancje są probabilistyczne.
Twierdzenie U. Średni czas działania algorytmu wybierania opartego na po
dziale jest liniowo-logarytmiczny.
Dowód. Analizy podobne do tych z dowodu t w i e r d z e n i a k dla sortowania
szybkiego (ale dużo bardziej złożone) prowadzą do wyniku, zgodnie z którym
średnia liczba porównań wynosi ~ 2N + 2k\n(N/k) + 2(N - k) \ n(N/ (N - k)).
Liczba ta rośnie liniowo dla dozwolonych wartości k. Zgodnie z tym wzorem
znalezienie mediany (k = N/2) wymaga średnio ~ (2 + 2ln 2)N porównań.
Zauważmy, że dla najgorszego przypadku algorytm jest kwadratowy, jednak ran-
domizacja chroni przed taką sytuacją (podobnie jak w sortowaniu szybkim).
Zaprojektowanie algorytmu wybierania, który gwarantuje liniową liczbę porównań
dla najgorszego przypadku, jest klasycznym problemem z obszaru złożoności oblicze
niowej. Na razie badania nie doprowadziły do utworzenia algorytmu przydatnego
w praktyce.
360 RO ZD ZIA Ł 2 ■ Sortowanie
Krótki przegląd zastosowań sortowania Bezpośrednie zastosowania sor
towania są znane, wszechobecne i zbyt liczne, aby można je wszystkie przytoczyć.
Sortujemy utwory muzyczne według tytułów lub nazwisk wykonawców; listy elek
troniczne lub połączenia telefoniczne według czasu albo źródła; zdjęcia według
dat. Uniwersytety sortują konta studentów według nazwisk lub identyfikatorów.
Operatorzy kart kredytowych sortują miliony, a nawet miliardy transakcji według
daty lub kwoty. Naukowcy nie tylko sortują dane eksperymentalne według czasu lub
innych identyfikatorów, ale też wykorzystują sortowanie do tworzenia szczegółowych
symulacji świata — od ruchu cząsteczek lub ciał niebieskich przez strukturę m ateria
łów po interakcje społeczne i związki. Trudno jest wskazać obszar przetwarzania,
w którym nie stosuje się sortowania! Aby rozwinąć to zagadnienie, w tym fragmencie
opisujemy przykłady zastosowań bardziej skomplikowanych niż omówione wcześniej
redukcje. Niektóre z tych przykładów badamy dokładniej w dalszej części książki.
Przetw arzanie kom ercyjne Świat jest pełen informacji. Instytucje rządowe i finanso
we oraz firmy komercyjne porządkują dużą część informacji, sortując je. Niezależnie
od tego, czy informacje to konta sortowane według nazwisk lub numerów, transak
cje sortowane według dat lub kwot, listy sortowane według kodów pocztowych lub
adresów, pliki sortowane według nazw lub dat albo inne dane — ich przetwarzanie
na pewnym etapie z pewnością wymaga algorytmu sortowania. Zwykle informacje
są uporządkowane w dużych bazach danych i posortowane według wielu kluczy, co
umożliwia wydajne wyszukiwanie. Skuteczna i powszechnie stosowana strategia po
lega na rejestrowaniu nowych informacji, dodawaniu ich do bazy, sortowaniu we
dług odpowiednich kluczy i scalaniu z istniejącą bazą danych posortowanych według
każdego klucza. Od wczesnego okresu używania narzędzi informatycznych opisane
metody stosuje się z powodzeniem do rozwijania rozbudowanej infrastruktury skła
dającej się z posortowanych danych i m etod do ich przetwarzania. Infrastruktura ta
stanowi podstawę wszystkich działań komercyjnych. Obecnie powszechnie przetwa
rza się tablice o milionach, a nawet miliardach elementów. Bez liniowo-logarytmicz-
nych algorytmów takich tablic nie dałoby się posortować, a przetwarzanie danych
byłoby niezwykle trudne lub niemożliwe.
W yszukiw anie inform acji Przechowywanie danych w posortowanej postaci um oż
liwia ich wydajne przeszukiwanie za pomocą klasycznego algorytmu wyszukiwania
binarnego (zobacz r o z d z i a ł i .) . Zobaczysz, że to samo podejście umożliwia łatwą
obsługę zapytań innego rodzaju. Ile elementów jest mniejszych od danego? Które
elementy znajdują się w danym przedziale? W r o z d z i a l e 3 . zajmujemy się pytania
mi tego rodzaju. Omawiamy też szczegółowo różne rozszerzenia sortowania i wy
szukiwania binarnego, umożliwiające łączenie zapytań z operacjami wstawiającymi
i usuwającymi obiekty ze zbioru. Zachowana jest przy tym gwarancja logarytmicznej
wydajności wszystkich operacji.
2.5 ■ Zastosowania 361
B adania operacyjne Dziedzina badań operacyjnych (BO; ang. operations research)
związana jest z rozwijaniem i stosowaniem modeli matematycznych do rozwiązywa
nia problemów oraz podejmowania decyzji. W książce pokazano kilka przykładów
zależności między BO a badaniami algorytmów. Zaczynamy w tym miejscu od za
stosowania sortowania w klasycznym problemie z dziedziny BO — w szeregowaniu.
Załóżmy, że trzeba wykonać N zadań, przy czym czas przetwarzania zadania; wynosi
tj. Należy wykonać wszystkie zadania, a jednocześnie zmaksymalizować zadowolenie
klientów przez minimalizację średniego czasu ukończenia zadania. Cel ten pozwala
osiągnąć reguła najpierw zadania o najkrótszym czasie przetwarzania, polegająca na
porządkowaniu zadań rosnąco według czasu przetwarzania. Można więc posortować
zadania według czasu przetwarzania lub umieścić je w kolejce priorytetowej z obsłu
gą minimum. Po uwzględnieniu innych ograniczeń i zastrzeżeń powstają rozmaite
inne problemy z obszaru szeregowania, często występujące w zastosowaniach prze
mysłowych i dobrze zbadane. Oto inny przykład — problem równoważenia obciąże
nia. Istnieje M identycznych procesorów i N zadań do wykonania, a celem jest zapla
nowanie wykonania wszystkich zadań w procesorach tak, aby m om ent ukończenia
ostatniego zadania był jak najwcześniejszy. Ten konkretny problem jest NP-zupełny
(zobacz r o z d z i a ł 6 . ) , dlatego nie oczekujemy, że znajdziemy praktyczny sposób na
obliczenie optymalnego planu. Jedną z metod, o której wiadomo, że generuje dobry
plan, jest reguła najpierw zadania o najdłuższym czasie przetwarzania. Polega ona na
pobieraniu zadań w malejącej kolejności według czasu przetwarzania i przypisywa
niu każdego zadania do pierwszego wolnego procesora. Aby zastosować algorytm,
trzeba najpierw posortować zadania w odwrotnej kolejności. Następnie utrzymywa
na jest kolejka priorytetowa M procesorów, gdzie priorytet to suma czasów przetwa
rzania jego zadań. Na każdym etapie należy usunąć procesor o najniższym prioryte
cie, przypisać do tego procesora następne zadanie i ponownie wstawić procesor do
kolejki priorytetowej.
Sym ulacje oparte na zdarzeniach Wiele zastosowań naukowych obejmuje symu
lacje, w których celem obliczeń jest modelowanie pewnego aspektu świata rzeczywi
stego, co ma pozwolić lepiej zrozumieć daną kwestię. Przed epoką informatyki na
ukowcy nie mieli dużego wyboru i musieli budować modele matematyczne. Obecnie
modele tego rodzaju są dobrze uzupełniane przez modele obliczeniowe. Wydajne
przeprowadzenie symulacji może być trudne, a od odpowiednich algorytmów za
leży, czy możliwe będzie ukończenie symulacji w sensownym czasie, czy trzeba bę
dzie zdecydować się na zaakceptowanie niedokładnych wyników lub oczekiwanie na
wykonanie obliczeń potrzebnych do uzyskania precyzyjnych danych. Szczegółowy
przykład dotyczący tej kwestii opisano w r o z d z i a l e 6 .
Obliczenia num eryczne Obliczenia naukowe często związane są z precyzją (jak
bardzo zbliżyliśmy się do dokładnej odpowiedzi?). Precyzja jest niezwykle ważna
przy wykonywaniu milionów obliczeń na szacunkowych wartościach, na przykład
na powszechnie stosowanych w komputerach zmiennoprzecinkowych reprezen
362 RO ZD ZIA Ł 2 n Sortowanie
tacjach liczb rzeczywistych. W niektórych algorytmach numerycznych używa się
kolejek priorytetowych i sortowania do kontrolowania precyzji obliczeń. Jednym ze
sposobów całkowania numerycznego (kwadratury), kiedy to celem jest oszacowa
nie obszaru pod krzywą, jest przechowywanie kolejki priorytetowej z szacunkowo
określoną precyzją zbioru podprzedziałów składających się na cały przedział. Proces
polega na usunięciu najmniej precyzyjnego podprzedziału, rozbiciu go na połowy
(co pozwala osiągnąć większą precyzję) i umieszczeniu połów z powrotem w kolejce
priorytetowej. Kroki te należy powtarzać do czasu uzyskania pożądanej precyzji.
W yszukiw anie kom binatoryczne Klasyczny paradygmat w dziedzinie sztucznej
inteligencji i przy rozwiązywaniu bardzo trudnych problemów polega na definiowa
niu zbioru konfiguracji z dobrze zdefiniowanymi przejściami z jednej konfiguracji
do następnej i priorytetami powiązanymi z każdym przejściem. Zdefiniowane są też
konfiguracje początkowa i docelowa (ta ostatnia odpowiada rozwiązaniu problemu).
Dobrze znany algorytm A* to proces rozwiązywania problemów, w którym konfi
guracja początkowa umieszczana jest w kolejce priorytetowej, a następnie opisane
dalej kroki wykonywane są do czasu dotarcia do celu. Oto te kroki: usunięcie kon
figuracji o najwyższym priorytecie i dodanie do kolejki wszystkich konfiguracji, do
których m ożna z niej dotrzeć w jednym ruchu. Proces ten, tak jak w symulacji opartej
na zdarzeniach, jest dostosowany do kolejek priorytetowych. Pozwala zredukować
rozwiązanie problemu do zdefiniowania efektywnej funkcji określania priorytetów.
Przykład opisano w ć w i c z e n i u 2 .5 .3 2 .
o p r ó c z t y c h b e z p o ś r e d n i c h z a s t o s o w a ń (a wymieniliśmy tylko małą ich część)
sortowanie i kolejki priorytetowe występują jako ważne abstrakcje w projektowaniu
algorytmów, dlatego często pojawiają się na kartach tej książki. Dalej przedstawiamy
wybrane przykłady zastosowań opisanych w książce. Wszystkie zastosowania wy
magają omówionych w rozdziale wydajnych implementacji algorytmów sortowania
i typu danych dla kolejki priorytetowej.
A lgorytm y P rim a i D ijkstry To klasyczne algorytmy opisane w r o z d z i a l e 4 .
Rozdział ten dotyczy algorytmów do przetwarzania grafów, czyli podstawowego
modelu obejmującego elementy i krawędzie łączące pary elementów. Podstawą tych
i kilku innych algorytmów jest przeszukiwanie grafów, co polega na przechodzeniu
między elementami wzdłuż krawędzi. Kolejki priorytetowe odgrywają podstawową
rolę w przeszukiwaniu grafów i umożliwiają stosowanie wydajnych algorytmów.
A lgorytm K ruskala To następny klasyczny algorytm dla grafów, w którym krawę
dzie mają wagi. Algorytm wymaga przetwarzania krawędzi w kolejności wyznacza
nej przez wagi. Czas wykonania jest tu zdominowany przez koszt sortowania.
2.5 ■ Zastosowania 363
Kompresja H u ffm a n a To klasyczny algorytm kompresji danych, polegający na
przetwarzaniu zbioru elementów z całkowitoliczbowymi wagami przez łączenie
dwóch mniejszych wartości w jedną większą, której waga to suma obu składników.
Zaimplementowanie tej operacji za pomocą kolejki priorytetowej jest bardzo proste.
Istnieje też kilka innych sposobów kompresji danych opartych na sortowaniu.
A lgorytm y przetw arzania łańcuchów zn a kó w Niezwykle ważne we współczes
nych zastosowaniach w obszarze kryptologii i badań nad genomem, często oparte są
na sortowaniu (zwykle stosuje się tu jedną z wyspecjalizowanych m etod sortowania
łańcuchów znaków, opisanych w r o z d z i a l e 5 .). W r o z d z i a l e 6 . omawiamy algo
rytmy do wyszuldwania w danym łańcuchu znaków najdłuższego powtarzającego się
podłańcucha. Algorytmy te najpierw sortują przyrostki łańcuchów znaków.
364 RO ZD ZIA Ł 2 ■ Sortowanie
| PYTANIA I ODPOWIEDZI
P. Czy w bibliotece Javy istnieje typ danych dla kolejki priorytetowej?
O. Tak, jest to typ j ava. uti 1. Pri ori tyQueue.
2.5 b Zastosowania 365
jj ĆWICZENIA
2.5.1 . Rozważmy następującą implementację metody compareTo () dla klasy S tr i ng.
W jaki sposób trzeci wiersz pozwala zwiększyć wydajność?
public in t compareTo(S trin g that)
{
i f ( t h is == that) return 0; // Chodzi o ten wiersz,
in t n = M a th .m in (th is .le n g th (), t h a t . l e n g t h Q ) ;
f o r (in t i = 0 ; i < n; i++)
{
if ( t h is . c h a r A t ( i) < th a t.c h a rA t (i) ) return -1;
else i f ( t h i s .c h a r A t ( i) > th a t.c h a rA t (i) ) return +1;
}
return t h is . le n g t h ( ) - t h a t .le n g t h Q ;
}
2.5.2. Napisz program, który wczytuje listę słów ze standardowego wejścia i wy
świetla wszystkie występujące na liście słowa składające się z dwóch innych. Na przy
kład jeśli lista obejmuje słowa po i południ e, słowem złożonym jest popołudni e.
2.5.3. Przeprowadź krytykę poniższej implementacji klasy, która ma reprezentować
stan rachunku. Dlaczego pokazana metoda compareTo () jest błędną implementacją
interfejsu Comparabl e?
public c la s s Balance implements Comparable<Balance>
(
private double amount;
public in t compareTo(Balance that)
{
i f ([Link] < [Link] - 0.005) return -1;
i f ([Link] > [Link] + 0.005) return +1;
return 0 ;
}
)
Opisz sposób na rozwiązanie problemu.
2.5.4. Zaimplementuj metodę S trin g [] dedup(String[] a), która zwraca obiekty
z tablicy a [] w posortowanej kolejności i bez powtórzeń.
2.5.5. Wyjaśnij, dlaczego sortowanie przez wybieranie jest niestabilne.
366 RO ZD ZIA Ł 2 ■ Sortow anie
ĆWICZENIA (ciąg dalszy)
2.5.6. Zaimplementuj rekurencyjną wersję m etody sel ect ( ).
2.5.7. Ile mniej więcej potrzeba porównań (średnio) do znalezienia najmniejszego
spośród N elementów za pomocą m etody s e le c t ()?
2.5.8. Napisz program Frequency, który wczytuje łańcuchy znaków ze standardowe
go wejścia i wyświetla liczbę wystąpień każdego łańcucha. Program ma porządkować
łańcuchy znaków malejąco według liczby wystąpień.
2.5.9. Opracuj typ danych umożliwiający napisanie klienta do sortowania plików
takich jak ten pokazany po prawej.
2.5.10. Utwórz typ danych Vers i on reprezentujący num er Dane wejściowe (wartość
wersji oprogramowania, na przykład 115.1.1, 115.10.1, transakcji dla indeksu DJI
z poszczególnych dni)
115.10.2. Zaimplementuj interfejs Comparable tak, aby
l-0 c t -2 8 3500000
wersja 115.1.1 była mniejsza niż 115.10.1 itd.
2 -0 c t-2 8 3850000
2.5.11. Jeden ze sposobów na opisanie wyników algoryt 3 -0 c t-2 8 4060000
4 -0 c t-2 8 4330000
m u sortowania polega na określeniu permutacji p[] dla 5 -0 ct-2 8 4360000
liczb od 0 do a .le n g th - 1 , takiej że p [i] określa końcową
lokalizację klucza znajdującego się początkowo w a [i]. 30-Dec-99 554680000
31-Dec-99 374049984
Podaj permutacje, które opisują wyniki sortowania przez 3-Jan-00 931800000
wstawianie, sortowania przez wybieranie, sortowania 4-Jan-00 1009000000
Shella, sortowania przez scalanie, sortowania szybkiego 5-Jan-00 1085500032
i sortowania przez kopcowanie dla tablicy zawierającej
siedem równych kluczy. Dane wyjściowe
19-Aug-40 130000
26-Aug-40 160000
2 4 - J u l-40 200000
10-Aug-42 210000
23-Jun-42 210000
23-J u l -02 2441019904
17-J u l -02 2566500096
15-J u l -02 2574799872
19-J u l -02 2654099968
2 4 - J u l-02 2775559936
2.5 a Zastosowania 367
PROBLEMY DO ROZWIĄZANIA
2.5.12. Szeregowanie. Napisz program [Link]. Program m a wczytywać ze standar
dowego wejścia nazwy zadań i czasy przetwarzania oraz wyświetlać plan, który m ini
malizuje średni czas ukończenia za pomocą reguły „najpierw zadania o najkrótszym
czasie przetwarzania”, opisanej na stronie 361.
2.5.13. Równoważenie obciążenia. Napisz program [Link]. Program ma przyjmo
wać jako argument liczbę całkowitą Mz wiersza poleceń, wczytywać nazwy zadań
i czasy przetwarzania ze standardowego wejścia oraz wyświetlać plan z przypisaniem
zadań do M procesorów. Plan ma w przybliżeniu minimalizować m om ent ukończenia
ostatniego zadania. Wykorzystaj regułę „najpierw zadania o najdłuższym czasie prze
twarzania”, opisaną na stronie 361.
2.5.14. Sortowanie według odwróconych nazw domeny. Napisz typ danych Domain
reprezentujący nazwy domeny. Typ ma obejmować odpowiednią metodę compa-
reTo(), w której porządkiem naturalnym jest kolejność odwróconych nazw dom e
ny. Przykładowo, odwróconą nazwą domeny [Link] jest [Link].
Technika ta jest przydatna do analizowania dzienników sieciowych. Wskazówka:
użyj metody [Link] l i t ( " \ \ . ") do rozbicia łańcucha znaków s na fragmenty ograni
czone kropkami. Napisz klienta, który wczytuje nazwy domeny ze standardowego
wejścia i wyświetla odwrócone nazwy w posortowanej kolejności.
2.5.15. Kampania oparta na spamie. Jako punktu wyjścia do nielegalnej kam pa
nii opartej na spamie użyj listy adresów e-mail z różnych dom en (domena to część
adresu e-mail po symbolu @). Aby lepiej sfałszować adresy zwrotne, wysyłaj e-ma-
ile z kont innych użytkowników z tej samej domeny. Przykładowo, możesz wysłać
fałszywy e-mail od użytkownika wayne@[Link] do rs@[Link]. W jaki
sposób przetworzysz listę e-maili, aby wydajnie wykonać zadanie?
2.5.16. Uczciwe wybory. Aby nie zmniejszać szans kandydatów, których nazwiska
zaczynają się na końcowe litery alfabetu, w Kalifornii nazwiska pojawiające się na
kartach do głosowania w wyborach gubernatora w 2003 roku posortowano w nastę
pującej kolejności:
R W Q O J M V A H B S G Z X N T C I E K U P D Y F L
Utwórz typ danych, w którym jest to porządek naturalny. Napisz klienta Cal i forni a
z jedną m etodą statyczną main(), która sortuje łańcuchy znaków według tego p o
rządku. Przyjmij, że każdy łańcuch znaków składa się wyłącznie z wielkich liter.
2.5.17. Sprawdzanie stabilności. Rozwiń metodę check() z ć w ic z e n ia aby
2 .1 .1 6 ,
wywoływała metodę s o rt () dla danej tablicy i zwracała true, jeśli s o rt () sortuje
tablicę w stabilny sposób. W przeciwnym razie należy zwrócić fal se. Nie zakładaj, że
metoda sort () przestawia dane wyłącznie za pom ocą m etody exch ().
368 RO ZD ZIA Ł 2 o Sortowanie
P R O B L E M Y D O R O Z W I Ą Z A N I A (ciąg dalszy)
2.5.18. Wymuszanie stabilności. Napisz metodę nakładkową, która zapewnia stabil
ność każdego sortowania. Utwórz w tym celu nowy typ klucza, umożliwiający dołą
czenie do kluczy ich indeksów. M etoda ma wywoływać metodę so rt () i przywracać
pierwotny porządek równych kluczy po sortowaniu.
2.5.19. Odległość tau Kendalla. Napisz program [Link], który w liniowo-lo-
garytmicznym czasie oblicza odległość tau Kendalla między dwoma permutacjami.
2.5.20. Czas bezczynności. Załóżmy, że komputer równoległy przetwarza N zadań.
Napisz program, który na podstawie listy czasów rozpoczęcia i zakończenia zadań
znajduje najdłuższy okres bezczynności maszyny oraz najdłuższy przedział, kiedy
maszyna nie jest bezczynna.
2.5.21. Sortowanie w wielu wymiarach. Napisz typ danych Vector do użytku w m e
todach sortujących wielowymiarowe wektory d liczb całkowitych. Metody mają po
rządkować wektory według pierwszego komponentu, te o równych kom ponentach
sortować według drugiego, następnie według trzeciego itd.
2.5.22. Handel na giełdzie. Inwestorzy składają na giełdzie elektronicznej polecenia
zakupu i sprzedaży określonych akcji, określając maksymalną cenę zakupu lub m ini
malną cenę sprzedaży oraz liczbę akcji. Opracuj program, który za pom ocą kolejki
priorytetowej łączy kupujących i sprzedających, oraz przetestuj go za pom ocą symu
lacji. Program ma przechowywać dwie kolejki priorytetowe — po jednej z kupujący
mi i sprzedającymi — oraz przeprowadzać transakcje, kiedy nowe polecenie można
dopasować do istniejącego (lub istniejących).
2.5.23. Używanie próbek przy wybieraniu. Zbadaj pomysł stosowania próbek do
usprawnienia wybierania. Wskazówka: zastosowanie mediany nie zawsze jest p o
mocne.
2.5.24. Stabilne kolejki priorytetowe. Opracuj stabilną implementację kolejki prio
rytetowej (zwracającą powtarzające się klucze w takiej kolejności, w jakiej je wsta
wiono).
2.5.25. Punkty w przestrzeni. Napisz trzy statyczne kom paratory dla typu danych
Poi nt2D ze strony 89. Jeden m a porównywać punkty według współrzędnej x, drugi —
według współrzędnej y, a trzeci — według odległości od początku układu. Ponadto
napisz dwa niestatyczne kom paratory dla tego typu. Jeden ma porównywać punkty
według odległości od podanego punktu, a drugi — według kąta biegunowego wzglę
dem podanego punktu.
2.5 o Zastosowania 369
2.5.26. Prosty wielokąt. Na podstawie N punktów w przestrzeni narysuj prosty wie
lokąt o N wierzchołkach. Wskazówka: znajdź punkt p o najmniejszej współrzędnej y
(jeśli dwa punkty mają tę samą jej wartość, uwzględnij współrzędną x). Połącz punk
ty w rosnącej kolejności według kąta biegunowego względem p.
2.5.27. Sortowanie tablic równoległych. Przy sortowaniu tablic równoległych przy
datna jest wersja m etody sortującej, która zwraca permutację — na przykład tablicę
index[] z posortowanymi indeksami. Dodaj do klasy In s e rtio n metodę in d ir e c t -
Sort (), która jako argument przyjmuje tablicę a [] z obiektami typu Comparabl e, jed
nak zamiast zmieniać kolejność elementów tablicy, zwraca tablicę i ndex [] z liczbami
całkowitymi, taką że przedział od a [i ndex[0]] do a [i ndex [N-l] ] obejmuje elementy
w kolejności rosnącej.
2.5.28. Sortowanie plików według nazw. Napisz program F ileS o rter, który jako
argument przyjmuje z wiersza poleceń nazwę katalogu i wyświetla wszystkie pliki
z tego katalogu posortowane według nazw. Wskazówka: użyj typu danych Fi 1e.
2.5.29. Sortowanie plików według rozmiaru i daty ostatniej modyfikacji. Napisz kom
paratory dla typu Fi 1e, aby umożliwić sortowanie w kolejności rosnącej i malejącej
według rozmiarów plików, w kolejności rosnącej i malejącej według nazw plików
oraz w kolejności rosnącej i malejącej według dat ostatniej modyfikacji. Użyj kom
paratorów w programie LS, który przyjmuje argument z wiersza poleceń i wyświetla
pliki z danego katalogu według określonej kolejności (na przykład opcja " -t" ozna
cza sortowanie według znaczników czasu). Dodaj obsługę wielu opcji, aby umożli
wić porządkowanie plików równych pod pewnym względem. Zapewnij stabilność
sortowania.
2.5.30. Twierdzenie Boernera. Jeśli posortujesz każdą kolumnę w macierzy, a na
stępnie posortujesz każdy wiersz, kolumny nadal będą posortowane — prawda czy
fałsz? Odpowiedź uzasadnij.
370 RO ZD ZIA Ł 2 h Sortowanie
|i EKSPERYMENTY
2.5.31. Powtórzenia. Napisz klienta, który przyjmuje jako argumenty z wiersza p o
leceń liczby całkowite M, N i T, a następnie używa opisanego kodu do wykonania T
powtórzeń eksperymentu. Oto jego opis: wygeneruj Włosowych wartości typu i nt od
0 do M - 1 i policz powtórzenia. Uruchom program dla T = 10, N = 103,1 0 4, 105 i 106
oraz M = NI 2, N i 2N. Zgodnie z teorią prawdopodobieństwa liczba powtórzeń po
winna wynosić mniej więcej (1 - e a), gdzie a = N/M. Wyświetl tabelę, która pozwoli
się upewnić, że eksperymenty potwierdzają prawdziwość wzoru.
2.5.32. Układanka 8-elementowa. Układanka 8 -elementowa to łamigłówka spopu
laryzowana przez S. Loyda w latach 70. XIX wieku. Zabawa odbywa się w siatce 3 na 3.
Używanych jest 8 klocków o num erach od 1 do 8 , a jedno pole pozostaje puste. Celem
jest uporządkowanie klocków we właściwej kolejności. Można przesunąć jeden z klo
cków w pionie lub poziomie (ale nie na ukos) na wolne pole. Napisz program, który
rozwiązuje tę łamigłówkę za pom ocą algorytmu A*. Zacznij od użycia jako priorytetu
sumy ruchów wykonanych w celu dojścia do danej pozycji i liczby klocków w nie
właściwych miejscach. Zauważ, że liczba ruchów, jakie trzeba wykonać dla danej po
zycji, jest równa co najmniej liczbie klocków na nieodpowiednim miejscu. Za liczbę
klocków na niewłaściwej pozycji spróbuj podstawić inne funkcje, na przykład sumę
odległości M anhattan każdego klocka od docelowego miejsca lub sumę kwadratów
takich odległości.
2.5.33. Losowe transakcje. Opracuj generator, który przyjmuje argument N i generu
je Włosowych obiektów typu Transaction (zobacz ć w i c z e n i a 2 . 1 . 2 1 1 2 . 1 . 2 2 ) . Posłuż
się możliwymi do uzasadnienia założeniami na temat transakcji. Następnie porównaj
wydajność sortowania Shella, sortowania przez scalanie, sortowania szybkiego i sor
towania przez kopcowanie przy sortowaniu N transakcji dla N = 103,1 0 4,1 0 5 i 106.
ROZDZIAŁ 3
3.1 Tablice symboli........................................................ 374
3.2 Drzewa wyszukiwań binarnych............................... 408
3.3 Zbalansowane drzewa wyszukiwań........................436
3.4 Tablice z haszowaniem............................................470
3.5 Zastosowania........................................................... 498
spółcześnie informatyka i internet zapewniają dostęp do dużej ilości infor
W macji. Możliwość wydajnego przeszukiwania jest podstawą do ich prze
twarzania. W tym rozdziale opisano ldasyczne algorytmy wyszukiwania,
których skuteczność przez dziesięciolecia udowodniono w wielu różnorodnych zasto
sowaniach. Bez algorytmów tego rodzaju powstanie infrastruktury informatycznej,
z której możemy współcześnie korzystać, nie byłoby możliwe.
Nazwa tablica symboli dotyczy abstrakcyjnego narzędzia służącego do zapisywania
informacji (wartości), które można później przeszukiwać i pobierać przez podanie
klucza. Natura kluczy i wartości zależy od aplikacji. Liczba kluczy i ilość informacji
mogą być niezwykle duże, dlatego zaimplementowanie wydajnej tablicy symboli jest
poważnym wyzwaniem informatycznym.
Tablice symboli czasem nazywa się słownikami przez analogię do tradycyjnego sy
stemu podawania definicji słów przez wymienienie tych ostatnich w porządku alfabe
tycznym. W słowniku języka polskiego kluczem jest słowo, a wartością — powiązany
ze słowem opis, obejmujący definicję, wymowę i etymologię. Tablice symboli czasem
nazywa się też indeksami. Jest to analogia do innego tradycyjnego systemu zapew
niania dostępu do nazw przez podawanie ich w kolejności alfabetycznej w końcowej
części książki (na przykład w podręczniku). W indeksie w książce kluczem jest szu
kana nazwa, a wartością — lista numerów stron, na których czytelnicy mogą znaleźć
w tekście dane słowo.
Po opisie podstawowych interfejsów API i dwóch podstawowych implementacji
przedstawiamy trzy klasyczne struktury danych, które umożliwiają utworzenie wy
dajnych implementacji tablic symboli. Te struktury to: binarne drzewa wyszukiwań,
drzewa czerwono-czarne i tablice z haszowaniem. Rozdział kończymy opisem kilku
rozszerzeń i zastosowań. Wiele rozwiązań nie byłoby możliwych bez wydajnych
algorytmów, które poznasz w tym rozdziale.
373
Główną funkcją tablic symboli jest łączenie wartości z kluczem. Klient może wstawiać
pary klucz-wartość do tablicy symboli i oczekiwać, że później będzie mógł znaleźć
wartość powiązaną z danym kluczem wśród wszystkich umieszczonych w tabeli par.
W rozdziale opisano kilka sposobów na ustrukturyzowanie takich danych, aby wy
dajne były nie tylko operacje wstaw i wyszukaj, ale też pewne inne przydatne funk
cje. W celu zaimplementowania tablicy symboli trzeba zdefiniować strukturę danych,
a następnie opracować algorytmy do wstawiania, wyszukiwania i wykonywania in
nych operacji związanych z tworzeniem struktury danych oraz manipulowaniem nią.
Wyszukiwanie jest tak ważne w tak wielu zastosowaniach informatycznych, że
tablice symboli są dostępne jako wysokopoziomowe abstrakcje w wielu środowiskach
programistycznych, w tym w Javie (implementacje tablicy symboli w Javie omówiono
w p o d r o z d z i a l e 3 . 5 ). W tabeli poniżej przedstawiono przykładowe klucze i war
tości, które mogą występować w typowych zastosowaniach. Dalej omówiono kilka
wzorcowych klientów, a w p o d r o z d z i a l e 3.5 pokazano, jak wydajnie stosować tab
lice symboli w klientach. Tablic symboli używamy też do rozwijania innych algoryt
mów w książce.
Definicja. Tablica symboli to struktura danych dla par klucz-wartość, obsługu
jąca dwie operacje: wstaw (umieść) nową parę do tablicy i znajdź (pobierz) war
tość powiązaną z danym kluczem.
Zastosowanie Cel wyszukiwania Klucz Wartość
Słownik Wyszukiwanie definicji Słowo Definicja
Wyszukiwanie
Indeks w książce Nazwa Lista numerów stron
odpowiednich stron
System wymiany Wyszukiwanie utworów Identyfikator
Tytuł piosenki
plików do pobrania komputera
Zarządzanie
Przetwarzanie transakcji Numer konta Szczegóły transakcji
kontem
Wyszukiwanie Wyszukiwanie
Słowo kluczowe Lista stron
w sieci W W W adekwatnych stron WWW
Wyszukiwanie typu
Kompilator Nazwa zmiennej Typ i wartość
i wartości
Typowe zastosowania tablicy symboli
3.1 a Tablice symboli 375
Interfejs API Tablica symboli to prototypowy abstrakcyjny typ danych (zobacz
Reprezentuje dobrze zdefiniowany zbiór wartości i operacji na nich,
r o z d z i a ł i.).
co umożliwia niezależne rozwijanie klientów i implementacji. Jak zwykle precyzyjnie
definiujemy operacje, określając interfejs API, który stanowi kontrakt między klien
tem a twórcą implementacji.
p u b lic c la s s ST<Key, Value>
ST() Tworzy tablicę symboli
Umieszcza parę klucz-wartość w tablicy
void put(Key key, Value v a l)
(jeśli wartość to nul 1 , klucz key należy usunąć z tablicy)
Zwraca wartość powiązaną z kluczem key
Val ue get(Key key)
(nul 1 .jeśli key nie istnieje)
void d elete(Key key) Usuwa z tablicy klucz key (i powiązaną wartość)
boolean con ta ins(K ey key) Czy istnieje wartość powiązana z kluczem key?
boolean i sEmpty() Czy tablica jest pusta?
i nt s iz e ( ) Zwraca liczbę par klucz-wartość obecnych w tablicy
Iterable<Key> k e y s() Zwraca wszystkie klucze z tablicy
Interfejs API generycznej podstawowej tablicy symboli
Przed przejściem do kodu klienta omawiamy kilka decyzji projektowych zastosowa
nych w implementacjach, aby kod był spójny, zwięzły i przydatny.
T ypy g e n e r y c zn e Podobnie jak przy sortowaniu, tak i tu używamy typów gene-
rycznych oraz omawiamy m etody bez określania typów przetwarzanych elementów.
W tablicach symboli podkreślamy różne funkcje kluczy i wartości w wyszukiwa
niu. W tym celu typy klucza i wartości są podawane bezpośrednio. Nie traktuje
my kluczy jako części elementów, jak miało to miejsce w kolejkach priorytetowych
w p o d r o z d z i a l e 2 .4 . Po omówieniu pewnych cech podstawowego interfejsu API
(zauważ, że na przykład nie określono tu porządku kluczy) przedstawiamy rozsze
rzenie, w którym klucze implementują interfejs Comparable, co umożliwia wprowa
dzenie wielu dodatkowych metod.
P o w ta r za ją c e się k lu c ze We wszystkich implementacjach stosujemy następujące
konwencje:
D Z każdym kluczem powiązana jest tylko jedna wartość (tabela nie obejmuje
powtarzających się kluczy).
° Kiedy klient umieszcza parę klucz-wartość w tablicy, która obejmuje już dany
klucz (i powiązaną wartość), nowa para zastępuje dawną.
Konwencje te są specyficzne dla abstrakcyjnej tablicy asocjacyjnej, pozwalającej trak
tować tablicę symboli jak zwykłą tablicę, której klucze to indeksy, a wartości to ele
menty tablicy. W tradycyjnej tablicy klucze to całkowitoliczbowe indeksy używane
376 RO ZD ZIA Ł 3 o W yszukiwanie
do uzyskania szybkiego dostępu do wartości tablicy. W tablicy asocjacyjnej (tablicy
symboli) klucze są dowolnego typu, jednak także je m ożna stosować do uzyskania
szybkiego dostępu do wartości. Niektóre języki programowania (nie Java) udostęp
niają specjalne mechanizmy i umożliwiają programistom używanie kodu w rodzaju
s t [key] zamiast st. get (key) i s t [key] = val zamiast s t . put (key, v a l ), gdzie key
i val to obiekty dowolnego typu.
K lu c ze o w a r to śc i n u li Klucze nie mogą mieć wartości nuli. Podobnie jak w wielu
innych mechanizmach Javy zastosowanie klucza o wartości nul 1 powoduje wyjątek
w czasie wykonywania programu (zobacz trzecie pytanie na stronie 399).
W a rto ści n u li Przyjęliśmy też, że klucz nie może być powiązany z wartością nul 1.
Konwencja ta jest bezpośrednio powiązana ze specyfikacją interfejsu API, wedle
której m etoda g et() ma zwracać wartość nuli dla kluczy, których nie ma w tabe
li. Powoduje to powiązanie wartości nul 1 z każdym kluczem nieobecnym w tabeli.
Podejście to ma dwa (zamierzone) skutki. Po pierwsze, m ożna ustalić, czy w tablicy
symboli zdefiniowano wartość powiązaną z danym kluczem, sprawdzając, czy m eto
da get () zwraca nul 1. Po drugie, m ożna zastosować wywołanie metody put () z nul 1
jako drugim argumentem (wartością), aby zaimplementować usuwanie, co opisano
w następnym akapicie.
U su w an ie Usuwanie w tablicy symboli zwykle odbywa się za pom ocą jednej z dwóch
strategii. Usuwanie leniwe polega na wiązaniu kluczy w tablicy z wartościami nul 1,
przy czym później wszystkie takie klucze są usuwane. Usuwanie zachłanne związa
ne jest z natychmiastowym usuwaniem kluczy z tablicy. Jak wcześniej opisano, kod
put (key, null) to łatwa (leniwa) implementacja metody d elete (key). Tam, gdzie
podano zachłanną implementację m etody del e t e (), zastępuje ona rozwiązanie do
myślne. W implementacjach tablicy symboli, w których nie użyto domyślnej metody
d e le te O , implementacje metody p u t() w kodzie z witryny zaczynają się od zabez
pieczającego kodu:
i f (val == n u ll) ( delete(key); return; }
Zapewnia on, że żaden klucz w tablicy nie jest powiązany z wartością nul 1. Z uwagi
na zwięzłość nie zamieszczamy tego kodu w książce (nie wywołujemy też metody
put () z wartością nul 1 w kodzie klienta).
M e to d y skrócone Aby kod klienta był przejrzysty, w interfejsie API uwzględniono m e
tody contains () i i sEmpty(). Ich domyślne implementacje przedstawiono w tym miej
scu. Z uwagi na zwięzłość dalej
. . Metoda Implementacja domyślna
me powtarzamy tego kodu — ------------------------------------------------------------------
zakładamy, że jest dostępny we void del ete (Key key) put(key, n u ll) ;
wszystkich implementacjach boolean con ta in s(k e y) return get(key) != n u li;
interfejsu API tablicy symboli
boolean isEm pty() return s i z e () *== 0;
i swobodnie korzystamy z tych
m eto d W kodzie klienta. Implementacje domyślne
3.1 a Tablice symboli 377
Iteracja Aby umożliwić klientom przetwarzanie wszystkich kluczy i wartości z tab
licy, możemy dodać fragment implements I t e r a b l e < K e y > do pierwszego wiersza
interfejsu API. Jest to informacja, że trzeba zaimplementować metodę i t e r a t o r ( ) ,
która zwraca iterator z odpowiednimi implementacjami m etod h a s N e x t ( ) i n e x t ( ) ,
opisanymi dla stosów i kolejek w p o d r o z d z i a l e 1 .3 . Dla tablicy symboli zastosowa
no prostsze podejście. Należy utworzyć metodę keys ( ) , która zwraca klientom obiekt
I t e r a b l e<Key> używany do iterowania po kluczach. Rozwiązanie to pozwala zacho
wać spójność z metodami definiowanymi dla uporządkowanych tablic symboli, które
umożliwiają klientom iterowanie po wybranym podzbiorze kluczy tablicy.
Równość kluczy Określanie, czy dany klucz znajduje się w tablicy symboli, oparte jest
na równości obiektów. Zagadnienie to opisano szczegółowo w p o d r o z d z i a l e 1.2 (zo
bacz stronę 114). Zgodnie z konwencjami Javy wszystkie obiekty dziedziczą metodę
equal s (), a jej implementacja dla standardowych typów, takich jak I n t e g e r , D ou b le
i S t r i ng, oraz bardziej skomplikowanych typów, na przykład Fi 1e i URL, to doskonały
punkt wyjścia do tworzenia własnych wersji. Przy stosowaniu tych typów danych
można użyć wbudowanych implementacji. Na przykład, jeśli x i y to wartości typu
S t r i n g , x . e q u a l s ( y ) m a wartość t r u e wtedy i tylko wtedy, jeśli x i y mają tę s a m ą
długość i są identyczne na każdej pozycji. Dla kluczy definiowanych przez klienty
trzeba przesłonić metodę equal s (), co opisano w p o d r o z d z i a l e 1 .2. Opracowanej
przez nas implementacji m etody equal s() dla typu Date (strona 115) m ożna użyć
jako szablonu do utworzenia m etody equal s() dla własnego typu. Jak opisano to
w kontekście kolejek priorytetowych na stronie 332, najlepszą praktyką jest tworze
nie typów Key jako niezmiennych, ponieważ w przeciwnym razie nie można zagwa
rantować spójności działania kodu.
378 R O ZD ZIA Ł 3 ■ W yszukiw anie
Uporządkowane tablice symboli W typowych zastosowaniach klucze to obiek
ty implementujące interfejs Comparable, dlatego można użyć kodu [Link](b)
do porównania kluczy a i b. W kilku implementacjach tablicy symboli kolejność
kluczy wyznaczaną przez interfejs Comparabl e wykorzystano do wydajnego zaimple
mentowania m etod put() i g e t(). Co ważniejsze, w takich implementacjach można
przyjąć, że tablice symboli przechowuję uporządkowane klucze, i opracować znacznie
bardziej rozbudowany interfejs API, z definicjami licznych naturalnych i przydat
nych operacji wymagających, aby klucze były uporządkowane. Załóżmy, że klucze
to godziny dnia. Może interesować Cię najwcześniejszy lub najpóźniejszy czas, zbiór
kluczy spomiędzy dwóch godzin itd. W większości sytuacji takie operacje nietrudno
jest zaimplementować za pomocą struktur danych i metod używanych w implemen
tacjach metod put ( ) i g e t( ) .W aplikacjach, w których klucze są zgodne z interfejsem
Comparabl e, w tym rozdziale implementujemy następujący interfejs API.
p u b lic c la s s ST<Key extends Comparable<Key>, Value>
ST () Tworzy uporządkowaną tablicę symboli
void put(Key key, Value v a l) Umieszcza parę klucz-wartość w tablicy
(usuwa klucz key z tablicy, jeśli wartość to nul 1)
Value get(Key key) Zwraca wartość powiązaną z kluczem key
(nuli, jeśli taki klucz nie istnieje)
void delete(Key key) Usuwa klucz key (i jego wartość) z tablicy
boolean con ta ins(K ey key) Czy istnieje wartość powiązana z kluczem key?
boolean isEm pty() Czy tablica jest pusta?
in t s iz e ( ) Zwraca liczbę par klucz-wartość
Key min() Zwraca najmniejszy klucz
Key max() Zwraca największy klucz
Key floor(Key key) Zwraca największy klucz mniejszy lub równy
względem key
Key c e ilin g (K e y key) Zwraca najmniejszy klucz większy lub równy
względem key
in t rank(Key key) Zwraca liczbę kluczy mniejszych niż key
Key se le c t ( in t k) Zwraca klucz z pozycji k
void d eleteM in O Usuwa najmniejszy klucz
void deleteMax() Usuwa największy klucz
in t size (K e y lo , Key h i) Zwraca liczbę kluczy z przedziału [1 o .. h i ]
Ite rab le<Ke y> keys(Key lo , Key h i) Zwraca klucze z przedziału [lo . .h i] (posortowane)
Ite rab le<Ke y> keys() Zwraca wszystkie klucze z tabeli (posortowane)
Interfejs API dla generycznej uporządkowanej tablicy symboli
3.1 ■ Tablice symboli 379
Informacją, że jeden z programów zawiera implementację tego interfejsu API, jest
obecność zmiennej typu generycznego Key e x t e n d s Comparabl e<Key> w deklaracji
klasy. Oznacza to, że kod wymaga, aby klucze były zgodne z interfejsem Comparabl e,
i obejmuje implementację bogatszego zbioru operacji. W spólnie operacje te obsługu
ją uporządkowaną tablicę symboli dla programów klienckich.
M in im u m i m a ksim u m Prawdopodobnie najbardziej naturalne zapytania na zbio
rze uporządkowanych kluczy dotyczą najmniejszego i największego klucza. Operacje
te pojawiły się już w kontekście kolejek priorytetowych w p o d r o z d z i a l e 2 .4 .
W uporządkowanej tablicy symboli ist
nieją też m etody do usuwania kluczy Klucze Wartości
maksymalnego i minimalnego (oraz mi n O — - 0 9 : 0 0 : 0 0 G d ańsk
[Link] Poznań
powiązanych wartości). Z uwagi na te
0 9 :0 0 :J 3 - Kraków
metody tablica symboli może działać get([Link]) [Link] G d ańsk
tak jak klasa In d e x M in P Q () omówiona [Link] Kraków
w p o d r o z d z i a l e 2 .4 . Główne różnice f1oor([Link]) — - 0 9 : 0 3 : 1 3 G d ańsk
0 9 :1 0 :11 s z c z e c i n
polegają na tym, że w kolejkach priory se"lect(7) — - 0 9 : 1 0 : 2 5 Szczeci n
tetowych mogą występować takie same [Link] Poznań
klucze (co jest niedozwolone w tablicach [Link] G d ańsk
[Link] G d ańsk
symboli), a tablice symboli obsługują
k e y s( 0 9 :1 5 : 00, [Link]) — - 0 9 : 2 1 : 0 5 G d ańsk
znacznie większy zbiór operacji. [Link] Szczeci n
[Link] Szczeci n
Podłoga i sufit Często przydatne jest [Link] G dańsk
obliczenie na podstawie otrzymanego c e i1 in g (0 9 :3 0 :0 0 ) [Link] G d ańsk
klucza podłogi (ang. floor), czyli naj [Link] szcze ci n
max() — - 0 9 : 3 7 : 4 4 Poznań
większego klucza mniejszego lub rów
s iz e ( 0 9 :1 5 :0 0 , 09:2 5 :00) wynosi 5
nego względem danego, oraz sufitu
ra n k (0 9 :1 0 :2 5 ) wynosi7
(ang. ceiling), czyli najmniejszego klucza
większego lub równego względem dane Przykłady operacji na uporządkowanej tablicy symboli
go. Nazwy te oparte są na funkcjach zde
finiowanych dla liczb rzeczywistych (podłoga dla liczby rzeczywistej x to największa
liczba całkowita mniejsza lub równa względem x, a sufit to najmniejsza liczba całko
wita większa lub równa względem x).
Pozycja i wybieranie Podstawowe operacje służące do określania miejsca nowego
klucza w porządku to ustalanie pozycji (znajdowanie liczby kluczy mniejszych od
danego) i wybieranie (znajdowanie klucza z danej pozycji). Aby sprawdzić, czy ro
zumiesz znaczenie tych operacji, upewnij się, że równość i == r a n k (sel ect ( i )) jest
spełniona dla wszystkich i z przedziału od 0 do s i ze () - 1 oraz że dla wszystkich klu
czy z tablicy spełniona jest równość key == s e l e c t ( r a n k ( k e y ) ) . Operacje te okazały
się już potrzebne w kontekście sortowania, w p o d r o z d z i a l e 2 .5 . W tablicach sym
boli trudność polega na wykonywaniu tych operacji szybko i w ciągach z operacjami
wstawiania, usuwania oraz wyszukiwania.
380 R O ZD ZIA Ł 3 n W yszukiw anie
Zapytania zakresowe Ile kluczy znajduje się w danym przedziale (między dwoma
podanymi kluczami)? Które klucze znajdują się w danym przedziale? Dwuargumento-
we metody si ze () i keys (), które odpowiadają na te pytania, są przydatne w wielu
zastosowaniach — zwłaszcza w dużych bazach danych. Możliwość obsługi takich za
pytań to jedna z głównych przyczyn popularności tablic symboli.
W yjątkowe przypadki Jeśli metoda ma zwracać klucz, a żaden klucz tablicy nie od
powiada opisowi, przyjmujemy, że należy zgłosić wyjątek (inne możliwe podejście,
także sensowne, to zwracanie wartości nul 1). Na przykład, m etody min(), max(), de-
1 eteMi n (), del eteMax (), floor () i cei 1 i ng () zgłaszają wyjątki, jeśli tablica jest pusta.
Podobnie działa wywołanie sel ect (k), jeśli k jest mniejsze niż 0 lub nie mniejsze niż
s iz e ().
M etody skrócone Jak pokazano już na przykładzie metod isEmpty() i c o n ta in s()
z podstawowego interfejsu API, w interfejsie znajdują się pewne nadmiarowe m eto
dy, co pozwala zwiększyć przejrzystość kodu klienta. Z uwagi na zwięzłość zakłada
my, że poniższe domyślne wersje znajdują się w każdej implementacji interfejsu API
uporządkowanej tablicy symboli (chyba że napisano inaczej).
Metoda Implementacja domyślna
void d eleteM in () d e le t e (m in ());
void deleteMax() d e le te (m a x ());
in t size (K e y lo , Key h i) i f ([Link] pareTo(lo) < 0)
return 0;
e lse i f (c o n t a in s ( h i) )
return ra n k (h i) - ra n k (lo ) + 1;
el se
return ra n k (h i) - ra n k (lo );
Iterable<Key> ke ys() return keys(m in (), m ax());
Dom yślne implementacje nadmiarowych metod dla uporządkowanej tablicy symboli
Równość kluczy (raz jeszcze) Do najlepszych praktyk w Javie należy zapewnianie
spójności m etody compareTo() z equal s() we wszystkich typach implementujących
interfejs Comparabl e. Oznacza to, że dla każdej pary wartości a i b w danym typie im
plementującym interfejs Comparable wyrażenia (a. compareTo(b) == 0) [Link] u als(b )
powinny mieć tę samą wartość. Aby uniknąć możliwej dwuznaczności, staramy się
nie używać metody equal s() w implementacjach uporządkowanych tablic symbo
li. Zamiast tego do porównywania kluczy używamy wyłącznie m etody compareTo ().
Wyrażenie logiczne [Link](b) == 0 oznacza: „Czy a i b są równe?” Zwykle
przejście tego testu oznacza udane zakończenie poszukiwań a w tablicy sym bo
li (przez znalezienie b). Jak pokazano w algorytmach sortowania, Java udostępnia
3.1 » Tablice symboli 381
standardowe implementacje m etody compareToQ dla wielu powszechnie stosowa
nych typów kluczy. Nietrudno też opracować implementację m etody compareToQ
dla własnego typu danych (zobacz p o d r o z d z i a ł 2 . 5 ).
M odel kosztów Niezależnie od tego, czy używamy m etody equalsQ (dla tablic
symboli, w których klucze nie implementują interfejsu Comparable) czy compare-
To() (dla uporządkowanych tablic symboli z kluczami implementującymi interfejs
Comparabl e), stosujemy określenie porównanie do operacji porównywania elemen
tów tablicy symboli z kluczem wyszukiwania. W większości implementacji tablicy
symboli operacja ta znajduje się w pętli
wewnętrznej. W nielicznych sytuacjach,
kiedy jest inaczej, liczone są też dostępy Model kosztów przy wyszukiwaniu. W cza
do tablicy. sie badania implementacji tablicy symboli
liczymy porównania (testy równości lub p o
równania kluczy). W (rzadkich) sytuacjach,
i m p l e m e n t a c j e t a b l i c s y m b o l i zw ykle
kiedy porównania nie znajdują się w pętli
różnią się u ż y w a n y m i stru k tu ra m i danych
wewnętrznej, liczymy dostępy do tablicy.
i im plem entacjam i m etod get ( ) i put ( ).
Nie zawsze przedstawiamy implementa
cje wszystkich pozostałych m etod opisanych w tekście, ponieważ opracowanie wielu
z nich to dobre ćwiczenie, pozwalające sprawdzić poziom zrozumienia używanych
struktur danych. Do rozróżniania implementacji służy opisowy przedrostek nazwy
ST, określający implementację zapisaną w klasie o danej nazwie. W klientach używa
my nazwy ST do wywoływania wzorcowej implementacji, chyba że chcemy wskazać
konkretną inną implementację. Stopniowo zaczniesz lepiej rozumieć przeznaczenie
metod z interfejsu API w kontekście licznych klientów i implementacji tablic sym
boli, które przedstawiamy i omawiamy w tym rozdziale oraz w dalszej części książki.
W pytaniach i odpowiedziach oraz w ćwiczeniach opisujemy też inne możliwości
w zakresie różnych wyborów projektowych omówionych w tym miejscu.
382 RO ZD ZIA Ł 3 * W yszukiwanie
Przykładowe klienty Choć szczegółowe rozważania na temat zastosowań odkła
damy do p o d r o z d z i a ł u 3 .5 , to przed przyjrzeniem się implementacjom warto roz
ważyć fragm enty kodu klienta. Opisujemy tu dwa klienty: klienta testowego, uży
wanego do śledzenia działania algorytm u dla małych danych wejściowych, i klienta
do pom iaru wydajności, służącego do uzasadnienia prac nad wydajnymi im ple
mentacjami.
K lient testowy Przy śledzeniu pracy algorytmów dla małych danych wejściowych
zakładamy, że dla wszystkich implementacji używany jest poniższy klient testowy.
Przyjmuje on ciąg łańcuchów znaków ze standardowego wejścia, tworzy tablicę sym
boli, w której wartość i powiązana jest z i -tym
p u b lic s t a t ic void m a in (S trin g []
args) łańcuchem znaków z wejścia, a następnie
{
ST <Strin g, In te g e r> s t ; wyświetla tablicę. W śladach działania za
st = new ST < Strin g, In te g e r> (); kładamy, że dane wejściowe to ciąg jedno-
znakowych łańcuchów. Najczęściej używa
fo r ( in t i 0; IS t d ln .is E m p t y O ; 1++)
my łańcucha znaków "S E A R C H E X A M
{
S t rin g key : S t d ln . r e a d S t r in g O ; P L E". Klient łączy klucz S z wartością 0,
st.p u t(k e y , i ) ; klucz R z wartością 3 i tak dalej, przy czym
} klucz E jest powiązany z wartością 12 (a nie
f o r (S tr in g s : s t . k e y s ( ) ) 1 lub 6), natomiast a ■
— z wartością 8 (a nie 2 ),
S t d O u t.p rin t ln (s + + st.g e t(s)); ponieważ z przyjętego tu działania tablicy
asocjacyjnej wynika, że każdy klucz jest po
wiązany z wartością podaną w najnowszym
Klient testow y podstawowej tablicy sym boli wywołaniu metody put (). W podstawo
wych implementacjach (bez uporządkowa
nia) kolejność kluczy w danych wyjściowych
Klucze E A R C H P L E klienta testowego jest nieokreślona (zależy
Wartości 1 2 3 4 5 10 11 12 od cech implementacji). Dla uporządkowa
Dane wyjściowe nej tablicy symboli klient testowy wyświetla
Dane wyjściowe dla
dla podstawowej
uporządkowanej posortowane klucze. Przedstawiony klient to
tablicy symboli
tablicy symboli
(jedna możliwość) przykładowy klient używający indeksu, po
11 8 zwalający zilustrować specjalny przypadek
10 4 podstawowego zastosowania tablicy symboli,
9 12 opisanego w p o d r o z d z i a l e 3 .5 .
7 5
5 11
4 9
3 10
8 3
12 0
0 7
Klucze, wartości i dane wyjściowe klienta testowego
3.1 n Tablice symboli 383
K lient do p o m ia ru w ydajności Program FrequencyCounter (pokazany na następnej
stronie) to klient tablicy symboli. Program określa liczbę wystąpień każdego łańcu
cha znaków (przy czym liczba znaków w łańcuchu nie może być mniejsza niż poda
na wartość progowa) w ciągu łańcuchów podanym w standardowym wejściu, a na
stępnie przechodzi po kluczach w celu znalezienia tego, który występuje najczęściej.
Klient ten to przykładowy klient używający słownika. Aplikację tego rodzaju opisano
szczegółowo w p o d r o z d z i a l e 3 . 5 . Klient odpowiada na proste pytanie: „Które sło
wo (mające nie mniej niż określoną liczbę znaków) najczęściej występuje w danym
tekście?”. W rozdziale mierzymy wydajność tego klienta dla trzech zbiorów danych
wejściowych: pierwszych pięciu wierszy książki Tale o f Two Cities C. Dickensa (plik
[Link]), tekstu całej tej książki (plik [Link]) i popularnej bazy danych z milio
nem losowych zdań z sieci WWW, tak zwanej bazy Leipzig Corpora Collection (plik
[Link]). Oto zawartość pliku [Link].
% more t in y T a le . tx t
i t was the best o f times i t was the worst o f times
i t was the age o f wisdom i t was the age o f f o o lis h n e s s
i t was the epoch of b e lie f i t was the epoch of in c r e d u lit y
i t was the season o f lig h t i t was the season of darkness
i t was the s p rin g o f hope i t was the w in ter o f d e sp a ir
Krótkie testowe dane wejściowe
Tekst ten zawiera w sumie 60 wystąpień 20 różnych słów. Cztery słowa występują po
10 razy (jest to najwyższa liczba). Na podstawie tych danych wejściowych program
FrequencyCounter wyświetla jedno ze słów i t, was, the lub of (wybrane mogą zostać róż
ne słowa; zależy to od cech implementacji tablicy symboli) i liczbę jego wystąpień — 10.
Łatwo dostrzec, że przy badaniu wydajności dla większych danych wejściowych
ważne będą dwie kwestie. Po pierwsze, każde słowo w danych wejściowych jest uży
wane jako klucz wyszukiwania jednokrotnie, dlatego istotna jest łączna liczba słów
w tekście. Po drugie, każde różne słowo z danych wejściowych jest umieszczane
w tablicy symboli (a łączna liczba różnych słów w danych wejściowych wyznacza
rozmiar tablicy po wstawieniu wszystkich kluczy), dlatego, oczywiście, znaczenie ma
łączna liczba słów w strumieniu wejściowym. Aby oszacować czas wykonania progra-
t in y T a le . t x t t a le .,tx t le ip z ig lM .t x t
Liczba Różne Liczba Różne Liczba Różne
słów słowa słów słowa słów słowa
Wszystkie słowa 60 20 135 635 10 679 21 191 455 534 580
Przynajmniej 8 liter 3 3 14 350 5737 4 239 597 299 593
Przynajmniej 10 liter 2 2 4583 2260 1 610 829 165 555
C ec h y w ię k sz y c h te s t o w y c h s tr u m ie n i w e jś c io w y c h
384 R O ZD ZIA Ł 3 W yszukiwanie
Klient tablicy symboli
public cla ss FrequencyCounter
{
public s t a t ic void m ain(String[] args)
{
in t minlen = In t e g e r . p a r s e ln t ( a r g s [0]); // Odcięcie według długości
// klucza.
ST<String, Integer> st = new ST<String, In teger>();
while (IS t d ln .isE m p t y O )
( // Tworzenie t a b l ic y symboli i z lic z a n ie wystąpień.
S t r in g word = S t d l n . re a d S trin g O ;
i f ([Link]() < minlen) continue; // Pomijanie krótkich kluczy.
i f (Ist.c o n tain s(w o rd )) [Link](word, 1 );
else [Link](word, [Link](word) + 1 );
}
// Wyszukiwanie klucza o największej li c z b i e wystąpień.
S trin g max =
[Link](max, 0 );
f o r (S t rin g word : s t . k e y s Q )
i f ([Link](word) > [Link](max))
max = word;
[Link](max + " " + st.g e t(m a x ));
Ten klient klasy ST zlicza wystąpienia łańcuchów znaków ze standardowego wejścia, a na
stępnie wyświetla łańcuch o największej liczbie wystąpień. Argument podawany w wierszu
poleceń określa dolne ograniczenie długości sprawdzanych kluczy.
% java FrequencyCounter 1 < t in y T a le . tx t
i t 10
% java FrequencyCounter 8 < t a le . t x t
b u sin e ss 122
% java FrequencyCounter 10 < le ip z ig lM . t x t
government 24763
3.1 o Tablice symboli 385
m u FrequencyCounter, należy ustalić obie te wartości (zacznij od ć w i c z e n i a 3 . 1 .6 ).
Zagadnienie to omawiamy szczegółowo po przedstawieniu kilku algorytmów, posta
raj się jednak pamiętać o potrzebach typowych aplikacji tego rodzaju. Przykładowo,
uruchomienie program u FrequencyCounter dla pliku leipziglM .txt i dla słów o dłu
gości równej przynajmniej 8 wymaga milionów wyszukiwań w tablicy zawierającej
setki tysięcy kluczy i wartości. Serwer w sieci W W W musi czasem obsługiwać miliar
dy transakcji na tablicach obejmujących miliony kluczy i wartości.
o t o p o d s t a w o w e p y t a n i e z w i ą z a n e z t y m k l i e n t e m i przykładami: „Czy m oż
na opracować implementację tablicy symboli, która potrafi obsłużyć bardzo dużą
liczbę operacji get() na dużej tablicy, zbudowanej za pom ocą dużej liczby wymie
szanych operacji get() i p u t ( ) ? ”. Jeśli liczba wyszukiwań jest nieduża, odpowied
nia będzie dowolna implementacja, jednak nie m ożna używać klientów w rodzaju
FrequencyCounter dla dużych problemów bez dobrej implementacji tablicy symboli.
Program FrequencyCounter ilustruje bardzo częstą sytuację. Ma opisane poniżej cechy,
wspólne wielu innym klientom tablic symboli:
D Operacje wyszukiwania i wstawiania są wymieszane.
n Liczba różnych kluczy jest duża.
° Prawdopodobne jest, że operacji wyszukiwania będzie znacząco więcej niż
wstawiania.
° Wzorce wyszukiwania i wstawiania, choć nieprzewidywalne, nie są losowe.
Celem jest opracowanie implementacji tablicy symboli, które umożliwiają stosowa
nie takich klientów do rozwiązywania typowych praktycznych problemów.
Rozważamy teraz dwie podstawowe implementacje i ich wydajność w kliencie
FrequencyCounter. Następnie, w kilku kolejnych podrozdziałach, przedstawiamy
klasyczne implementacje, pozwalające uzyskać doskonałą wydajność dla takich
klientów (nawet dla dużych strum ieni wejściowych i tablic).
386 RO ZD ZIA Ł 3 " W yszukiwanie
Sekwencyjne przeszukiwanie nieuporządkowanych list powiąza
nych Prostą strukturą danych na tablicę symboli jest lista powiązana z węzłami
zawierającymi klucze i wartości (tak jak w kodzie na następnej stronie). W imple
mentacji m etody get () należy przejść po liście, używając m etody equal s () do po
równywania klucza wyszukiwania z kluczem z każdego węzła listy. Po znalezieniu
pasującego klucza należy zwrócić odpowiednią wartość. Jeśli klucza nie znaleziono,
trzeba zwrócić nuli. W implementacji m etody p u t() także należy przejść po liście
i użyć m etody equal s () do porównywania klucza podanego przez klienta z klu
czem z każdego węzła listy. Po znalezieniu pasującego klucza trzeba zaktualizować
powiązaną z nim wartość za pom ocą wartości drugiego argumentu. Jeśli klucza nie
znaleziono, należy utworzyć nowy węzeł na podstawie podanych elementów (klucza
i wartości) oraz wstawić go na początek listy. Metoda ta to wyszukiwanie sekwencyjne.
Szukamy przez sprawdzanie kluczy tablicy jeden po drugim, a do sprawdzania dopa
sowania do klucza wyszukiwania służy metoda equal s ().
a l g o r y t m 3 .1 (Sequential SearchST) to implementacja interfejsu API podstawo
wej tablicy symboli. Wykorzystano tu standardowe mechanizmy przetwarzania list,
używane dla podstawowych struktur danych w r o z d z i a l e i . Opracowanie imple
mentacji m etod s iz e () , keys () i zachłannej wersji metody delete() pozostawiamy
jako ćwiczenia. Zachęcamy do ich wykonania. Pozwoli to utrwalić wiedzę na temat
listy powiązanej i interfejsu API podstawowej tablicy symboli.
Klucz Wartość fi rst
Czerwone
S 0 s 0 węzły sq nowe
E 1 E 1 S 0 Czarne węzły sq sprawdzane
A 2 A 2 E 1 S 0 przy wyszukiwaniu
R 3 R 3 A 2 E 1 S 0
C 4 C 4 R 3 A 2 E 1 S 0
Zakreślone pozycje
H 5 H 5 C 4 R 3 A 2 E 1 to zmieniane wartości
E 6 H 5 c 4 R 3 A 2 E
X 7 X 7 H 5 C 4 R 3 A 2 S 0
Szare węzły
A 8 X 7 H 5 C 4 R 3 A S | 0 - pozostajq nietknięte
M 9 M 9 X 7 H 5 C 4 R 3 E 6 S 0
P 10 P 10 M 9 X 7 H 5 C 4 A 8 E 6 S 0
L 11 L 11 P 10 M 9 X 7 H 5 R 3 A 8 E 6 S 0
E 12 L 11 P 10 M 9 X 7 H 5 R 3 A 8 E S 0
Ślad działania implementacji klasy ST (opartej na liście powiązanej) w standardowym kliencie używającym indeksu
3.1 Tablice symboli 387
ALGORYTM 3.1. Sekwencyjne wyszukiwanie (w nieuporządkowanych listach powiązanych)
public c la s s SequentialSearchST<Key, Value>
{
private Node first; // Pierwszy węzeł l i s t y powiązanej.
private c la s s Node
{ // Węzeł l i s t y powiązanej.
Key key;
V alue v a l ;
Node next;
public Node(Key key, Value val, Node next)
{
t h is .k e y = key;
[Link] = v a l;
th is .n e x t = next;
}
}
public Value get(Key key)
{ // Wyszukiwanie klucza i zwracanie powiązanej wartości,
fo r (Node x = first; x != n u ll; x = [Link])
i f ([Link]([Link]))
return x .v a l; // Trafienie,
return n u ll; // Chybienie.
}
public void put(Key key, Value val)
{ // Wyszukiwanie klucza. Aktualizowanie wartości po jego znalezieniu.
// J e ś li klucz je s t nowy, należy wydłużyć ta b licę ,
fo r (Node x = first; x != n u ll; x= [Link])
i f ([Link] ( x . key))
{ [Link] = val; return; } // Trafienie: aktualizowanie wartości,
first = new Node(key, val, first); // Chybienie: dodawanie nowego węzła.
}
}
W tej implementacji klasy ST użyto prywatnej klasy wewnętrznej Node do przechowywania
kluczy i wartości na nieuporządkowanej liście powiązanej. Kod metody g et() przeszukuje
sekwencyjnie listę, aby sprawdzić, czy klucz znajduje się w tablicy (jeśli tak, zwraca powiąza
ną wartość). Kod metody put () także przeszukuje sekwencyjnie listę, aby ustalić, czy klucz
znajduje się w tablicy. Jeżeli tak jest, metoda aktualizuje powiązaną wartość. W przeciwnym
razie tworzy nowy węzeł o podanych kluczu i wartości oraz wstawia go na początek listy.
Opracowanie implementacji metod s iz e (), keys() i zachłannej wersji metody d e le te ()
pozostawiamy jako ćwiczenia.
388 R O ZD ZIA Ł 3 h W yszukiw anie
Czy implementacja oparta na liście powiązanej umożliwia obsługę aplikacji ta
kich jak przykładowe klienty, które wymagają dużych tablic? Jak wspomniano,
analizowanie algorytmów dla tablic symboli jest bardziej skomplikowane niż ana
lizowanie algorytmów sortowania. Wynika to z trudności ze scharakteryzowaniem
ciągu operacji, które może wywołać dany klient. Jak napisano w kontekście progra
m u FrequencyCounter, najczęściej jest tak, że wzorce uruchamiania wyszukiwania
i wstawiania są nieprzewidywalne, natomiast nie są też losowe. Dlatego zwracamy
baczną uwagę na wydajność dla najgorszego przypadku. Z uwagi na zwięzłość cza
sem używamy określenia trafienie do opisu udanego wyszukiwania, a chybienie — do
opisu nieudanego wyszukiwania.
T w ierdzenie A. Nieudane wyszukiwanie elementu i wstawianie go w tablicy
symboli opartej na (nieuporządkowanej) liście powiązanej i zawierającej N par
klucz-wartość wymaga N porównań, a przy trafieniu w najgorszym przypadku
potrzebnych jest N porównań. Wstawienie N różnych kluczy do początkowo p u
stej tablicy symboli opartej na liście powiązanej wymaga - N 2/2 porównań.
D ow ód. Przy wyszukiwaniu klucza, który nie znajduje się na liście, z kluczem
wyszukiwania trzeba porównać każdy klucz z tablicy. Z uwagi na zasadę unie
możliwiającą powtarzanie się kluczy trzeba to zrobić przed wstawieniem każdego
elementu.
W niosek. Wstawienie N różnych kluczy do początkowo pustej tablicy symboli
opartej na liście powiązanej wymaga ~N 2/2 porównań.
To prawda, że czas wyszukiwania kluczy znajdujących się w tablicy nie musi rosnąć linio
wo. Przydatną miarą jest łączny koszt wyszukiwania wszystkich kluczy z tablicy podzie
lony przez N. Wartość ta to dokładnie oczekiwana liczba porównań potrzebnych przy
wyszukiwaniu w warunkach, kiedy wyszukiwanie dowolnego klucza z tabeli jest równie
prawdopodobne. Znalezienie takiego elementu nazywamy trafieniem przy wyszukiwa
niu losowym. Choć wzorce wyszukiwania w klientach zwykle nie są losowe, model ten
często dobrze je opisuje. Łatwo wykazać, że średnia liczba porównań do trafienia przy
wyszukiwaniu losowym wynosi ~N/2. Metoda g e t () w a l g o r y t m i e 3.1 wykonuje jed
no porównanie w celu znalezienia pierwszego klucza, dwa porównania do znalezienia
drugiego klucza i tak dalej. Średni koszt wynosi (1 + 2 + ... + N )/N - (N + 1)12 ~ N I2.
Analizy wyraźnie pokazują, że oparta na liście powiązanej implementacja z wyszu
kiwaniem sekwencyjnym jest zbyt wolna, aby używać jej do rozwiązywania dużych
problemów, takich jak przykładowe dane wejściowe, za pom ocą klientów w rodzaju
programu FrequencyCounter. Łączna liczba porównań jest proporcjonalna do ilo
czynu liczby wyszukiwań i liczby wstawień. Iloczyn ten wynosi 109 dla tekstu książki
Tale o f Two Cities i 1014 dla zbioru Leipzig Corpora.
3.1 o Tablice symboli 389
Walidacja wyników analiz wymaga, jak zwykle, przeprowadzenia eksperymentów.
W ramach przykładu zbadamy działanie programu FrequencyCounter dla podane
go w wierszu poleceń argumentu 8 i pliku [Link], który wymaga 14 350 operacji
put () (przypominamy, że każde słowo z danych wejściowych powoduje wywołanie
tej operacji i zaktualizowanie liczby wystąpień; pomijamy koszt łatwych do uniknię
cia wywołań m etody contains()). Tablica symboli rośnie do 5737 kluczy, tak więc
około operacji zwiększa rozmiar tablicy — pozostałe to wyszukiwania. Do wizu
alizowania działania kodu używamy klasy Vi sual Accumul a to r (zobacz stronę 107),
rysując przy jej użyciu dwa punkty powiązane z każdą operacją put ( ) . Dla i-tej ope
racji put () rysowana jest szara kropka, której współrzędna x jest równa i, a współ
rzędna y — liczbie porównań kluczy, oraz czerwona kropka, której współrzędna x to
i, a współrzędna y to skumulowana średnia liczba porównań klucza dla pierwszych
i operacji put (). Tak jak w każdych danych naukowych, tak i tu dane obejmują bar
dzo dużą ilość informacji (na rysunku znajduje się 14 350 szarych i 14 350 czerwo
nych punktów). W tym kontekście interesuje nas głównie to, że rysunek potwierdza
hipotezę o tym, iż w każdej operacji put () średnio sprawdzana jest około połowa
listy. Rzeczywista średnia wyniosła nieco poniżej połowy, jednak ten fakt (i dokładny
kształt krzywych) prawdopodobnie najlepiej wyjaśniają cechy aplikacji, a nie algoryt
mów (zobacz ć w i c z e n i e 3 . 1 .36 ).
Choć szczegółowe określanie wydajności konkretnych klientów bywa skompli
kowane, łatwo można sformułować pewne hipotezy i sprawdzić je w programie
FrequencyCount dla przykładowych lub losowo uporządkowanych danych wejściowych,
używając klienta w rodzaju programu Doubl i ngTest przedstawionego w r o z d z i a l e 1 .
Przeprowadzanie takich testów odkładamy do ćwiczeń i bardziej zaawansowanych
implementacji, które się pojawią. Jeśli jeszcze nie jesteś przekonany, że potrzebne
są szybsze implementacje, koniecznie wykonaj ćwiczenia (lub uruchom program
FrequencyCounter oparty na klasie Sequential SearchST dla pliku [Link]\).
Koszty wywołania j a v a F r e q u e n c y C o u n t e r 8 < t a l e . t x t z wykorzystaniem klasy S e q u e n t i a l S e a r c h S T
390 RO ZD ZIA Ł 3 0 W yszukiwanie
Wyszukiwanie binarne w uporządkowanej tablicy Rozważmy teraz kom
pletną implementację interfejsu API dla uporządkowanej tablicy symboli. Za struk
turę danych służy tu para równoległych tablic. Jedna przeznaczona jest na klucze,
a druga — na wartości. W a l g o r y t m i e 3.2 (BinarySearchST), przedstawionym na
następnej stronie, przechowywane są zgodne z interfejsem Comparable klucze w p o
sortowanej kolejności w tablicy, a indeksy tablicy wykorzystano, aby umożliwić im
plementację szybkiej m etody get () i innych operacji.
Istotą implementacji jest metoda rank(), która zwraca liczbę kluczy mniejszych
od danego. W metodzie get () metoda rank() precyzyjnie określa, gdzie m ożna zna
leźć klucz, jeśli znajduje się on w tablicy (lub informuje o tym, że klucza nie ma).
W metodzie put() metoda rank() dokładnie określa, w którym miejscu należy
zaktualizować wartość, jeśli klucz znajduje się w tablicy, lub gdzie należy wstawić
klucz, jeżeli nie m a go w tablicy. Wszystkie większe klucze należy przesunąć o jedną
pozycję (zaczynając od końca), aby zrobić miejsce, a następnie wystarczy wstawić
klucz i wartość na odpowiednią pozycję w ich tablicach. Także tu analiza programu
Bi narySearchST w połączeniu ze śladem działania klienta testowego to dobry sposób
na poznanie struktury danych.
Kod przechowuje równoległe tablice kluczy i wartości (inne rozwiązanie opisano
w ć w i c z e n i u 3 . 1 . 1 2 ). Podobnie jak implementacje generycznych stosów i kolejek
z r o z d z i a ł u 1 ., tak i to rozwiązanie jest nieco niewygodne, ponieważ wymaga utwo
rzenia tablicy Key typu Comparabl e i tablicy Val ue typu Object oraz zrzutowania ich
na tablice Key[] i ValueQ w konstruktorze. Jak zwykle można zastosować zmianę
wielkości tablic, aby w klientach nie trzeba było uwzględniać ich rozm iaru (warto
jednak pamiętać, że — jak się okaże — metoda ta jest za wolna do stosowania do
długich tablic).
keysj] va ls[]
Klucz Wartość 0 1 2 3 4 5 6 7 8 9 N 0 1 2 3 4 5 6 7 8 9
S 0 S 1 0
E 1 E S 2 1 0 Czarne elementy
Czerwone elementy
A 2 A E s 3 2 1 0 przesunięto wprawo
^ zostały wstawione X
R 3 A E R S 4 1 1 3 0
Szare 4 Zakreślone
C 4 A C E R s 5 1 3 0
elementy y elementy
H 5 A c E H R S iig ¿11
iKlic ymionih/
licimy 6 4 1 5 3 JL
zmieniły
E 6 A c E H R s pozycji 6 y 4 © ~T 0 wartość
X 7 A c E H R s X 7 2 4 6 5 3 0 7
A 8 A c E H R s X 7 4 6 5 3 0 y
®
M 9 A c E H M R S X 8 8 4 6 5 9 3 0 7
P 10 A c E H M P R S X 9 8 4 6 5 9 10 3 0 7
L 11 A c E H L M P R S X 10 8 4 6 5 11 9 10 3 0 7
E 12 A c E H L M P R S X 10 8 4 5 11 9 10 3 0 7
©
A c E H L M P R S X 8 4 12 5 11 9 10 3 0 7
Ślad działania standardowego klienta używającego indeksu;
implementacja tablicy symboli oparta jest tu na tablicy uporządkowanej
3.1 Tablice symboli 391
ALGORYTM 3.2. Wyszukiwanie binarne (w tablicy uporządkowanej)
public c la s s BinarySearchST<Key extends Comparable<Key>, Value>
{
private Key[] keys;
p rivate V alue[] va ls;
private in t N;
p ublic BinarySearchST(int capacity)
{ // Standardowy kod do zmiany wielkości ta blicy opisano w algorytmie 1.1.
keys = (Key[J) new Comparable [c a p a c ity ];
va ls = (Value[]) new O b ject[ca p acity];
}
public in t s iz e ()
{ return N; }
public Value get(Key key)
{
i f (isEm ptyO) return n u ll;
in t i = rank(key);
i f (i < N && k e y s [ i] .compareTo(key) == 0) return v a l s [ i ];
el se return nul 1 ;
}
public in t rank(Key key)
// Zobacz stronę 393.
public void put(Key key, Value val)
{ // Wyszukiwanie klucza. J e ś li i s t n ie j e , należy zaktualizować wartość.
// Jeżeli je s t nowy, trzeba powiększyć ta blicę ,
in t i = rank(key);
i f (i < N && keys [i ] .compareTo(key) == 0)
{ v a l s [ i ] = v a l ; return; }
fo r (i n t j = N; j > i ; j - - )
{ keys [j] = keys [ j - 1] ; v a l s [ j ] =val s [ j - 1] ; }
keys [i] = key; va ls [i] = val;
N++;
}
p ublic void delete(Key key)
// Tę metodę opisano w ćwiczeniu 3.1.16.
}
W tej implementacji tablicy symboli klucze i wartości znajdują się w równoległych tabli
cach. Implementacja metody put () przenosi większe klucze o jedną pozycję w prawo przed
wydłużeniem tablicy, tak jak oparta na tablicy implementacja stosu z p o d r o z d z i a ł u 1 .3 .
W tym miejscu pominięto kod do zmiany długości tablicy.
392 RO ZD ZIA Ł 3 ■ W yszukiwanie
W yszukiw anie binarne Klucze przechowywane są w tablicy uporządkowanej, aby
można było wykorzystać indeksy do znacznego zmniejszenia liczby porównań po
trzebnych przy każdym wyszukiwaniu. Umożliwia to klasyczny algorytm, wyszukiwa-
nie binarne, użyty jako przykład w r o z d z i a l e 1 .
p u b lic in t rank(Key key, in t lo , in t h i)
Kod przechowuje indeksy z posortowanej tab
1 licy kluczy, co pozwala ograniczyć podtablicę,
i f (hi < lo ) return lo ; w której może znajdować się klucz wyszukiwa
in t mid = lo + (hi - lo ) / Z;
in t cmp = [Link] pareTo(keys[m id]);
nia. Jeśli klucz wyszukiwania jest mniejszy niż
if (cmp < 0) klucz w połowie podtablicy, należy przeszukać
return rank(key, lo , m id- 1 ); jej lewą połowę. Jeżeli klucz wyszukiwania jest
e lse i f (cmp > 0)
większy niż środkowy, należy sprawdzić prawą
return rank(key, mid+ 1 , h i) ;
e lse return mid; połowę podtablicy. Trzecia możliwość jest taka,
1 że środkowy klucz jest równy szukanemu. W ko
dzie metody rank (), przedstawionym na następ
Rekurencyjne wyszukiwanie binarne
nej stronie, użyto wyszukiwania binarnego do
uzupełnienia opisanej implementacji tablicy
symboli. Warto starannie przeanalizować tę implementację. Analizy zaczynamy od
równoważnego rekurencyjnego kodu pokazanego po lewej. Wywołanie rank(key,
0, N-1) prowadzi do tego samego ciągu porównań, co wywołanie nierekurencyjnej
implementacji z a l g o r y t m u 3 .2 , jednak wersja rekurencyjna lepiej obrazuje struk
turę algorytmu, jak opisano to w p o d r o z d z i a l e 1 .1 . Rekurencyjna wersja m etody
rank() zachowuje następujące właściwości:
■ Jeśli klucz key znajduje się w tablicy, metoda zwraca jego indeks w tablicy (jest
on równy liczbie kluczy tablicy mniejszych od danego).
■ Jeżeli klucza key nie ma w tablicy, metoda także zwraca liczbę kluczy m niej
szych od niego.
Wartościowym zadaniem dla każdego programisty jest przekonanie się, że niere-
kurencyjna wersja metody rank() z a l g o r y t m u 3.2 działa w oczekiwany sposób.
Można albo udowodnić, że jest równoważna wersji rekurencyjnej, albo bezpośrednio
wykazać, że pętla zawsze kończy działanie z wartością 1 o równą dokładnie liczbie
kluczy w tablicy mniejszych niż key. Wskazówka: zauważ, że 1o ma początkowo war
tość 0 i nigdy nie maleje.
Inne operacje Ponieważ klucze są przechowywane w tablicy uporządkowanej, więk
szość operacji opartych na kolejności jest zwięzła i prosta, co widać w kodzie na
stronie 394. Przykładowo, wywołanie metody se le c t(k ) powoduje zwrócenie war
tości keys [k]. Opracowanie m etod d e le te () i floor() pozostawiamy jako ćwiczenia.
Zachęcamy do przyjrzenia się implementacji metody cei 1i ng () i dwuargumentowej
metody keys () oraz wykonania ćwiczeń w celu utrwalenia wiedzy o interfejsie API
dla uporządkowanej tablicy symboli i jego implementacji.
3.1 Tablice symboli 393
ALG O R YTM 3.2 (ciąg dalszy).
Wyszukiwanie binarne w tablicy uporządkowanej (wersja iteracyjna)
public in t rank(Key key)
{
in t lo = 0, hi = N-l;
while (lo <= hi)
{
in t mid = lo + (hi - lo) / 2 ;
in t cmp = [Link](keys[mid]);
if (cmp < 0 ) hi = mid - 1 ;
else i f (cmp > 0 ) lo = mid + 1 ;
else return mid;
}
return lo;
Tu do ustalenia liczby kluczy mniejszych niż key użyto klasycznej metody opisanej w tekście.
Należy porównać klucz key ze środkowym kluczem. Jeśli są równe, trzeba zwrócić indeks
środkowego klucza. Jeżeli key jest mniejszy, należy sprawdzić lewą połowę podtablicy, a jeśli
jest większy, przeszukać prawą połowę.
____________keys[]____________
Udane wyszukiwanie P 0 1 2 3 4 5 6 7 8 9
lo h i mid
Czarne litery
0 9 4 A c E H L M P
R S x to elementy
5 9 7 A C E H L M P R S X a [lo . .h i]
5 6 5 A C E H L M P R C zerw ona litera
6 6 6 A C E H L M P r S X to element a [mid]
Nieudane wyszukiwanie Q ^ Pętla kończy pracę przy
k ey s [mid] = p - return 6
lo h i mid
0 9 4 A C E H L M P R S X
5 9 7 A C E H L M P R S X
5 6 5 A C E H L M P R S X
7__6 6 A C E H L M P R S X
>Sx Pętla kończy pracę przy 1o > h i - return 7
Ślad działania wyszukiwania binarnego w metodzie rank() dla tablicy uporządkowanej
394 RO ZD ZIA Ł 3 W yszukiw anie
ALGORYTM 3.2 (ciąg dalszy).
Operacje na uporządkowanej tablicy symboli związane z wyszukiwaniem binarnym
publ ic Key min()
( return keys[ 0 ]; }
publ i c Key max()
( return keys [N- 1] ; }
public Key select (in t k)
{ return keys[ k ] ; }
public Key c e ilin g (K e y key)
{
in t i = rank(key);
return k e y s [ i];
}
public Key floor (Key key)
// Zobacz ćwiczenie 3.1.17.
public Key delete(Key key)
// Zobacz ćwiczenie 3.1.16.
public Iterable<Key> keys(Key lo, Key hi)
{
Queue<Key> q = new Queue<Key>();
for (in t i = ra n k (lo ); i < ra n k (h i); i++)
[Link](keys[i]);
i f (c o n ta in s (h i))
[Link](keys[rank(hi) ] ) ;
return q;
}
Te metody, wraz z metodami z ć w i c z e ń 3 . 1.16 i 3 .1 . 1 7 , uzupełniają implementację interfej
su API uporządkowanej tablicy symboli opartą na wyszukiwaniu binarnym w tablicy upo
rządkowanej. Metody mi n (), max () i sel ect () są banalne. Wystarczy w nich zwrócić odpo
wiedni klucz na podstawie znanej pozycji z tablicy. W pozostałych metodach kluczową rolę
odgrywa metoda rank(), która stanowi podstawę wyszukiwania binarnego. Implementacje
metod floor() i d e le te () są bardziej skomplikowane, ale i tak proste. Ich opracowanie po
zostawiamy jako ćwiczenie.
3.1 b Tablice symboli 395
Analizy wyszukiwania binarnego Rekurencyjną implementacja metody
rank() także bezpośrednio dowodzi, że wyszukiwanie binarne gwarantuje szybkie
wyszukiwanie, ponieważ odpowiada zależności rekurencyjnej określającej górne
ograniczenie liczby porównań.
Twierdzenie B. Wyszukiwanie binarne w tablicy uporządkowanej o N kluczach
wymaga nie więcej niż lg N + 1 porównań przy wyszukiwaniu (udanym lub nie
udanym).
Dowód. A nalizysąpodobnedoanalizsortowaniaprzezscalanie ( t w i e r d z e n i e f
w r o z d z i a l e 2 .), ale prostsze. Niech C(N) będzie liczbą porównań potrzebnych
do znalezienia klucza w tablicy symboli o wielkości N. Mamy C(0) = 0, C (l) = 1,
a dla N > 0 m ożna napisać zależność rekurencyjną, która bezpośrednio odpowia
da metodzie rekurencyjnej:
C(N) < C(|_N/2j) + 1
Niezależnie od tego, czy wyszukiwanie kontynuowane jest w lewą czy w pra
wą stronę, rozmiar podtablicy wynosi nie więcej niż l_M2j. Jedno porównanie
pozwala sprawdzić równość i wybrać stronę. Jeśli N ma wartość o jeden m niej
szą niż potęga dwójki (N = 2 "-l), zależność rekurencyjną łatwo jest obliczyć.
Po pierwsze, ponieważ \_N/lj = 2"-I-l, mamy:
C(2"-l) < C(2 'M- 1 ) + 1
Po zastosowaniu równania do pierwszego wyrazu po prawej stronie otrzymujemy:
C(2"-l) < C(2"'2-l) + 1 + 1
Powtórzenie poprzedniego kroku n - 2 razy daje:
C(2 "-l) < C(2°) + n
Ostatecznie otrzymujemy rozwiązanie:
C(N ) = C(2") < n + l < l g N + l
Dokładne rozwiązanie dla ogólnego N jest bardziej skomplikowane, jednak nie
trudno rozwinąć ten dowód, aby uzyskać podaną właściwość dla wszystkich war
tości N (zobacz ć w i c z e n i e 3 .1 .20). Za pom ocą wyszukiwania binarnego można
osiągnąć gwarancje logarytmicznego czasu wyszukiwania.
Przedstawiona implementacja m etody cei 1 i ng ()oparta jest na jednym wywołaniu
metody rank(), a domyślna, dwuargumentowa implementacja m etody siz e () dwu
krotnie wywołuje metodę rank(), dlatego dowód określa też, że wymienione opera
cje (oraz m etoda floor()) działają w czasie logarytmicznym. Operacje min(), max()
i sel ect () działają w czasie stałym.
396 RO ZD ZIA Ł 3 □ W yszukiwanie
Metoda Tempo wzrostu Mimo gwarantowanego logarytmicznego czasu wyszuki
czasu wykonania
wania klasa Bi narySearchST nie umożliwia używania klientów
put () N w rodzaju FrequencyCounter do rozwiązywania dużych prob
get () lemów, ponieważ metoda put () jest zbyt wolna. Wyszukiwanie
log N
binarne zmniejsza liczbę porównań, ale nie czas wykonania,
d eleteO N ponieważ jej zastosowanie nie zmienia tego, że liczba dostę
co n tains() log N pów do tablicy potrzebnych do zbudowania tablicy symboli
w tablicy uporządkowanej rośnie kwadratowo wraz z roz
si ze () 1
miarem tablicy, jeśli klucze są uporządkowane losowo (oraz
min() 1 w typowych sytuacjach w praktyce, kiedy to klucze, choć nie
są losowe, są dobrze opisane przez ten model).
max() 1
floorO log N Twierdzenie B (ciąg dalszy). Wstawienie nowego klu
c e ilin g O log N cza do uporządkowanej tablicy o rozmiarze N wymaga dla
najgorszego przypadku ~ 2N dostępów do tablicy, tak więc
rank() log N wstawienie N kluczy do początkowo pustej tablicy wymaga
se le c tO 1 dla najgorszego przypadku ~ N 2 dostępów do tablicy.
deleteM in() N Dowód. Taki sam, jak dla do w o d u a .
deleteMax() 1
Dla książki Tale of Two Cities, w której różnych kluczy jest 104,
Koszty w klasie Bi narySearchST koszt zbudowania tablicy to prawie 108dostępów do tablicy. Dla
pliku z projektu Leipzig, gdzie różnych kluczy jest 106, koszt
zbudowania tablicy wynosi ponad 1011 dostępów do tablicy. Choć na współczesnych komputerach
możliwe jest wykonanie takiej liczby operacji, koszty są niezwykle (i niepotrzebnie) wysokie.
Wróćmy do kosztów operacji put () w programie FrequencyCounter dla słów od długości 8
i więcej znaków. Widać tu zmniejszenie średniego kosztu z 2246 porównań (plus dostępy do
tablicy) na operację dla wersji Sequential SearchST do 484 dla wersji Bi narySearchST. Tak jak
wcześniej, w praktyce koszt jest nawet niższy, niż wskazują na to analizy, a poprawę ponownie
można przypisać cechom aplikacji (zobacz ć w i c z e n i e 3 . 1 .36 ). Poprawa robi duże wrażenie,
jednak — jak się okaże — można uzyskać znacznie lepsze wyniki.
5737-,
Koszty wywołania j a v a F re q u e n c y C o u n t e r 8 < t a l e . t x t z wykorzystaniem klasy B in a r y S e a r c h S T
3.1 □ Tablice symboli 397
Przegląd wstępny Wyszukiwanie binarne jest zwykle dużo lepsze od wyszu
kiwania sekwencyjnego i jest m etodą używaną z wyboru w wielu praktycznych
zastosowaniach. Tablice statyczne (w których niedozwolone jest wstawianie) war
to zainicjować i posortować, tak jak w wersji wyszukiwania binarnego opisanej
w r o z d z i a l e i. (zobacz stronę 111). Nawet jeśli większość par klucz-wartość jest
znana przed wykonaniem większości wyszukiwań (zdarza się to często), warto dodać
do klasyBi narySearchST konstruktor inicjujący i sortujący tablicę (zobacz ć w i c z e n i e
3 .1 . 1 2 ). W wielu zastosowaniach wyszukiwanie binarne jest jednak nieakceptowalne.
Zawodzi na przykład dla zbioru Leipzig Corpora, ponieważ operacje wyszukiwania
i wstawiania są wymieszane, a rozmiar tablicy jest za duży. Jak podkreślono wcześ-
niej, typowe współczesne ldienty wymagają tablic symboli, które umożliwiają utwo
rzenie szybkich implementacji zarówno wyszukiwania, jak i wstawiania. Oznacza to,
że możliwe musi być budowanie bardzo dużych tablic, w których można wstawiać
(a czasem i usuwać) pary klucz-wartość w nieprzewidywalnej kolejności, a między
tymi operacjami wyszukiwać dane.
Tabela poniżej to podsumowanie cech z obszaru wydajności, dotyczące podsta
wowych implementacji tablicy symboli opisanych w podrozdziale. W komórkach
podano pierwszy wyraz kosztów (liczbę dostępów do tablicy w wyszukiwaniu bi
narnym i liczbę porównań dla pozostałych metod), który wyznacza tempo wzrostu
czasu wykonania.
Koszt dla najgorszego Koszt dla typowego Wydajna obsługa
Algorytm (struktura przypadku (po IM przypadku (po N operacji na
danych) wstawieniach) losowych wstawieniach) uporządkowanych
Wyszukiwanie Wstawianie Trafienie Wstawianie danych?
Wyszukiwanie
sekwencje
(nieuporządkowana
lista powiązana)
Wyszukiwanie
binarne (tablica lg N 2N lg N N T ak
uporządkowana)
Podsumowanie kosztów działania podstawowych implementacji tablicy symboli
Podstawowe pytanie dotyczy tego, czy m ożna zaprojektować algorytmy i struk
tury danych umożliwiające logarytmiczne wykonywanie zarówno wyszukiwania,
jak i wstawiania. Odpowiedź jest jednoznaczna: Tak\ Przedstawienie tej odpowie
dzi to główny cel rozdziału. Obok umożliwienia szybkiego sortowania, co opisano
w r o z d z i a l e 2 ., opracowanie szybkiego wyszukiwania i wstawiania danych w tablicy
symboli to jeden z najważniejszych wkładów algorytmiki i jeden z najważniejszych
kroków w kierunku rozwinięcia bogatej infrastruktury informatycznej, z której m o
żemy obecnie korzystać.
398 RO ZD ZIA Ł 3 ■ W yszukiw anie
Jak m ożna osiągnąć wspomniany cel? Wydaje się, że aby umożliwić wydajne wsta
wianie, trzeba użyć struktury powiązanej. Jednak lista jednokrotnie powiązana unie
możliwia stosowanie wyszukiwania binarnego, ponieważ m etoda ta wymaga, aby
można było szybko pobrać środkowy element dowolnej podtablicy, używając indeksu
(a jedyny sposób na dotarcie do środka listy jednokrotnie powiązanej to podążanie
za odnośnikami). Połączenie wydajności wyszukiwania binarnego z elastycznością
struktur powiązanych wymaga zastosowania bardziej skomplikowanych struktur da
nych. Mogą to być zarówno binarne drzewa wyszukiwań (temat dwóch następnych
podrozdziałów), jak i tablice z haszowaniem (omówione w p o d r o z d z i a l e 3 .4 ).
W tym rozdziale omawiamy sześć implementacji tablicy symboli, dlatego krótki
przegląd wstępny jest uzasadniony. Tabela poniżej obejmuje listę struktur danych
wraz z ich głównymi zaletami i wadami w omawianym kontekście. Struktury wymie
niono w kolejności ich omawiania.
Cechy algorytmów i implementacji opisano bardziej szczegółowo w miejscach ich
omawiania, jednak krótka charakterystyka przedstawiona w tabeli pomoże przyjrzeć
się im w szerszym kontekście w trakcie ich poznawania. Ostateczny wniosek jest taki,
że istnieje lulka szybkich implementacji tablic symboli, które mogą dawać (i dają)
doskonałe efekty w niezliczonych zastosowaniach.
Struktura danych Implementacja Zalety Wady
Lista powiązana SeąuentialSearchST Najlepsza dla małych W olna dla dużych
(wyszukiwanie tablic symboli tablic symboli
sekwencyjne)
Tablica BinarySearchST Optymalne wyszukiwanie Wolne wstawianie
uporządkowana i wykorzystanie
(wyszukiwanie pamięci; obsługa
binarne) operacji zależnych od
uporządkowania
Binarne drzewo BST Łatwa w implementacji; Brak gwarancji;
wyszukiwań obsługa operacji potrzebna pamięć
zależnych od na odnośniki
uporządkowania
Zbalansowane RedBlackBST Optymalne wyszukiwanie Potrzebna pamięć
binarne drzewo i wstawianie; obsługa na odnośniki
wyszukiwań operacji zależnych od
uporządkowania
Tablica SeperateChainingHashST Szybkie wyszukiwanie Wymaga haszowania
z haszowaniem *-i nearProbi ngHashST ; wstawianie dla dla każdego typu;
popularnych typów brak obsługi operacji
danych zależnych od
uporządkowania;
wymaga pamięci na
odnośniki i pustą tablicę
Zalety i wady im plem entacji tablic symboli
3.1 o Tablice symboli
; PYTANIA I ODPOWIEDZI
P. Dlaczego nie użyć dla tablicy symboli typu Item implementującego interfejs
Comparabl e (w taki sam sposób, jak dla kolejek priorytetowych w p o d ro z d z ia le 2 .4 ),
zamiast stosować odrębne klucze i wartości?
O. Oba rozwiązania są sensowne. Te dwa podejścia ilustrują dwa różne sposoby
wiązania informacji z kluczami. Można zrobić to pośrednio, przez utworzenie typu
danych obejmującego klucz, i bezpośrednio, oddzielając klucze od wartości. W kon
tekście tablic symboli zdecydowaliśmy się oprzeć na abstrakcyjnej tablicy asocjacyj
nej. Zauważmy też, że klient określa w wyszukiwaniu sam klucz, a nie obiekt łączący
klucz i wartość.
P. Po co stosować metodę equal s ( ) ? Dlaczego nie używamy po prostu m etody com-
pareToO?
O. Nie wszystkie typy danych mają lducze, które można łatwo porównać, ale nawet
dla nich tablica symboli może być przydatna. Posłużmy się skrajnym przykładem
— jako kluczy m ożna użyć obrazów lub piosenek. Nie istnieje naturalny sposób na
stwierdzenie, który z tych elementów jest większy, jednak — pewnym nakładem pra
cy — z pewnością m ożna sprawdzić, czy są sobie równe.
P. Dlaczego niedozwolone jest przyjmowanie wartości nul 1 przez klucze?
O. Zakładamy, że typ Key dziedziczy po typie Object, ponieważ wywołujemy m eto
dy compareTo() lub equal s(). Jednak wywołanie w rodzaju [Link](b) spowo
dowałoby wyjątek pustego wskaźnika, gdyby a miało wartość nul 1. Wykluczając tę
możliwość, umożliwiamy pisanie prostszego kodu klienta.
P. Dlaczego nie używamy metody w rodzaju 1e s s ( ), którą zastosowano w sortowaniu?
O. Równość odgrywa specjalną rolę w tablicach symboli, dlatego potrzebna jest też
metoda do jej sprawdzania. Aby uniknąć tworzenia wielu m etod o w zasadzie tej sa
mej funkcji, wykorzystaliśmy wbudowane metody Javy — equal s () i compareTo().
P. Dlaczego w klasie Bi narySearchST nie zadeklarowano przed rzutowaniem tablicy
key [] jakoO bject[] (zamiast Comparabl e [] ), tak jak zrobiono to z tablicą val []?
O. Dobre pytanie. Użycie typu Object wywołałoby wyjątek ClassCastException,
ponieważ klucze muszą być zgodne z interfejsem Comparable (co gwarantuje, że
elementy tablicy key[] udostępniają metodę compareToQ). Dlatego trzeba zadekla
rować tablicę key [] jako Comparabl e []. Zagłębianie się w szczegóły projektu języka
programowania w celu wyjaśnienia przyczyn spowodowałoby odejście od tematu.
W książce używamy tego idiomu (nie stosujemy żadnych bardziej skomplikowa
nych rozwiązań) w kodzie, gdzie potrzebne są typy generyczne zgodne z interfejsem
Comparabl e i tablice.
400 R O ZD ZIA Ł 3 0 W yszukiw anie
PYTANIA I ODPOWIEDZI (ciąg dalszy)
P. Co zrobić, jeśli trzeba powiązać wiele wartości z jednym kluczem? Przykładowo,
czy przy używaniu kluczy typu Date nie będzie konieczne przetwarzanie równych
kluczy?
O. Może talc, a może nie. Dwa pociągi nie mogą przyjechać na stację o tej samej
godzinie tym samym torem (choć mogą pojawić się o tym samym czasie na róż
nych torach). Są dwa sposoby na poradzenie sobie z taką sytuacją — można użyć
innych informacji do zapewnienia jednoznaczności lub zastosować obiekt Queue
dla wartości o tym samym kluczu. Zastosowania tych technik opisano szczegółowo
W P O D R O Z D Z IA L E 3 .5 .
P. W stępne sortowanie tablicy, opisane na stronie 397, wydaje się być dobrym p o
mysłem. Dlaczego technikę tę omówiono tylko w ćwiczeniu ( ć w i c z e n i e 3 . 1 . 1 2 )?
O. Rzeczywiście, może to być m etoda używana z wyboru w niektórych zastosowa
niach. Jednak dodanie dla wygody wolnej m etody wstawiania do struktury danych
zaprojektowanej pod kątem szybkiego wyszukiwania to pułapka, ponieważ nieświa
domy twórca klienta może wymieszać operacje wyszukiwania i wstawiania w dużej
tablicy, doprowadzając do kwadratowego czasu wykonania. Taicie pułapki występu
ją zbyt często, dlatego hasło „kliencie, strzeż się” jest adekwatne przy korzystaniu
z oprogramowania opracowanego przez innych, zwłaszcza jeśli interfejsy są zbyt sze
rokie. Problem staje się groźny, kiedy duża liczba m etod jest dodawana dla wygody,
przy czym m etody te są pułapkami w obszarze wydajności, a autor klienta oczekuje
wydajnej implementacji wszystkich metod. Przykładem jest klasa ArrayList Javy
(zobacz ć w i c z e n i e 3 .5 .2 7 ).
3.1 ■ Tablice symboli 401
ĆWICZENIA
3.1.1. Napisz klienta, który tworzy tablicę symboli przez odwzorowanie ocen w p o
staci liter na liczby, tak jak w poniższej tabeli, a następnie wczytuje ze standardowego
wejścia listę ocen w formie liter oraz oblicza i wyświetla średnią z liczb odpowiada
jących ocenom.
A+ A A- B+ B B- C+ C C- D F
4,33 4,00 3,67 3,33 3,00 2,67 2,33 2,00 1,67 1,00 0,00
3.1.2. Opracuj implementację tablicy symboli ArrayST, w której do zaimplementowa
nia podstawowego interfejsu API tablicy symboli służy nieuporządkowana tablica.
3.1.3. Opracuj implementację tablicy symboli OrderedSequentialSearchST, w któ
rej do zaimplementowania interfejsu API uporządkowanej tablicy symboli użyto
uporządkowanej listy powiązanej.
3.1.4. Opracuj typy ADT Time (czas) i Event (zdarzenie), umożliwiające przetwa
rzanie danych w taki sposób, jak w przykładzie przedstawionym na stronie 379.
3.1.5. Zaimplementuj operacje siz e (), d elete () i keys() dla typu Sequential
SearchST.
3.1.6. Podaj liczbę zgłaszanych w programie FrequencyCounter wywołań m etod
put () i g et() jako funkcję od liczby wszystkich (W ) i różnych (D ) słów w danych
wejściowych.
3.1.7. Jaka jest średnia liczba różnych kluczy, które program FrequencyCounter
znajdzie wśród N losowych nieujemnych liczb całkowitych mniejszych niż 1000 dla
N = 10 , 10 2, 10 3, 10 4, 105 i 10 6?
3.1.8. Jakie jest najczęściej występujące w książce Tale o f Two Cities słowo o przy
najmniej 10 literach?
3.1.9. Dodaj do program u FrequencyCounter kod do rejestrowania ostatniego wy
wołania m etody put (). Wyświetl ostatnie wstawione słowo i liczbę wyrazów prze
tworzonych w strum ieniu wejściowym przed wstawieniem tego słowa. Uruchom
program dla pliku [Link] z wymaganą długością słów równą 1 , 8 i 10 .
3.1.10. Przedstaw ślad przebiegu procesu wstawiania kluczy E A S Y Q U E S T I O N
do początkowo pustej tablicy za pom ocą klasy Sequential SearchST. Ile porównań
jest potrzebnych?
3.1.11. Przedstaw ślad przebiegu procesu wstawiania kluczy E A S Y Q U E S T I O N
do początkowo pustej tablicy za pomocą klasy BinarySearchST. Ile porównań jest
potrzebnych?
402 RO ZD ZIA Ł 3 n W yszukiwanie
ĆWICZENIA (ciąg dalszy)
3.1.12. Zmodyfikuj klasę BinarySearchST przez zastosowanie jednej (zawierającej
klucze i wartości) tablicy obiektów typu Item zamiast dwóch równoległych tablic.
Dodaj konstruktor, który jako argument przyjmuje tablicę wartości typu Item i uży
wa sortowania przez scalanie do posortowania tablicy.
3.1.13. Której z opisanych w podrozdziale implementacji tablicy symboli użyłbyś
w aplikacji, która wykonuje 103 operacji put () i 106 operacji get () losowo wymiesza
nych ze sobą? Odpowiedź uzasadnij.
3.1.14. Której z opisanych w podrozdziale implementacji tablicy symboli użyłbyś
w aplikacji, która wykonuje 106 operacji put () i 103 operacji get () losowo wymiesza
nych ze sobą? Odpowiedź uzasadnij.
3.1.15. Załóżmy, że w kliencie klasy BinarySearchST operacje wyszukiwania są
1000 razy częstsze niż operacje wstawiania. Oszacuj, jaki procent całego czasu zaj
muje wstawianie, jeśli liczba wyszukiwań wynosi 10 3, 106 i 10 9.
3.1.16. Zaimplementuj metodę del e t e () dla klasy BinarySearchST.
3.1.17. Zaimplementuj metodę floor () dla klasy Bi narySearchST.
3.1.18. Udowodnij, że m etoda rank() wklasie BinarySearchST działa prawidłowo.
3.1.19. Zmodyfikuj program FrequencyCounter tak, aby wyświetlał wszystkie war
tości o największej liczbie wystąpień zamiast tylko jednej z nich. Wskazówka: użyj
typu Queue.
3.1.20. Dokończ dowód t w i e r d z e n i a b (wykaż, że jest prawdziwe dla wszystkich
wartości N). Wskazówka: zacznij od wykazania, że C(N) jest funkcją monotoniczną,
czyli że C(N) < C(N+l) dla wszystkich N > 0.
3.1 ■ Tablice symboli 403
; PROBLEMY DO ROZWIĄZANIA
3.1 .2 1 . Wykorzystanie pamięci. Porównaj wykorzystanie pamięci przez program y
Bi narySearchST i Sequent! al SearchST dla N par klucz-wartość. Przyjmij założenia
opisane w p o d r o z d z i a l e 1 .4 . Nie uwzględniaj pamięci na same klucze i wartości
— licz tylko referencje do nich. W program ie Bi narySearchST przyjmij, że dłu
gość tablicy jest modyfikowana tak, aby poziom zajęcia tablicy wynosił od 25% do
100%.
3.1.22. Wyszukiwanie samoporządkujące. Algorytm wyszukiwania samoporządku-
jącego zmienia uporządkowanie elementów tak, aby szybko znajdować te często uży
wane. Zmodyfikuj implementację wyszukiwania z ć w i c z e n i a 3 .1 .2 , aby przy każdym
trafieniu kod wykonywał następujące operacje: przenosił znalezioną parę klucz-war-
tość na początek listy, a wszystkie pary pomiędzy początkiem a zwolnioną pozycją
— o jedno miejsce w prawo. Heurystyka ta nosi nazwę przenoszenie na początek.
3.1.23. Analiza wyszukiwania binarnego. Udowodnij, że maksymalna liczba po
równań w wyszukiwaniu binarnym w tablicy o wielkości N wynosi dokładnie liczbę
bitów w binarnej reprezentacji liczby N, ponieważ operacja przenoszenia jednego
bitu w prawo przekształca reprezentację binarną liczby N na reprezentację binarną
wartości |_M2 _|.
3.1.24. Wyszukiwanie interpolacyjne. Załóżmy, że możliwe są operacje arytmetycz
ne na kluczach (klucze są na przykład wartościami typu Doubl e lub Integer). Napisz
wersję wyszukiwania binarnego, która odzwierciedla proces szukania na początku
słownika, jeśli słowo rozpoczyna się na jedną z początkowych liter alfabetu. Jeśli k
to szukana wartość klucza, kh to wartość pierwszego klucza w tablicy, a kh. to wartość
ostatniego klucza tablicy, należy najpierw sprawdzić nie w połowie, ale w [_(kv - k j /
(kh. - kh)J. Za pom ocą programu SeachCompare porównaj działanie tej implementacji
i klasy Bi narySearchST w kliencie FrequencyCounter.
3.1.25. Programowa pamięć podręczna. Ponieważ domyślna implementacja metody
contai ns () wywołuje metodę get (), wewnętrzna pętla programu FrequencyCounter:
i f (![Link] n tain s(w o rd )) [Link](w ord, 1 );
e ls e [Link](w ord, [Link](w ord) + 1 );
prowadzi do dwóch lub trzech wyszukiwań tego samego klucza. Aby umożliwić pisa
nie przejrzystego kodu klienta bez rezygnacji z wydajności, można użyć programowej
pamięci podręcznej, co polega na zapisaniu lokalizacji ostatniego używanego klucza
w zmiennej egzemplarza. Zmodyfikuj klasy Sequential SearchST i Bi narySearchST
tak, aby wykorzystać ten pomysł.
404 RO ZD ZIA Ł 3 n W yszukiw anie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
3.1.26. Liczba wystąpień w słowniku. Zmodyfikuj program FrequencyCounter tak,
aby przyjmował jako argument nazwę pliku słownika, określał liczbę wystąpień słów
ze standardowego wejścia, które występują w pliku, i wyświetlał dwie tabele słów
wraz z liczbą ich wystąpień. Jedna ma być posortowana według liczby wystąpień,
a druga — według kolejności znalezienia słów w słowniku.
3.1.27. Małe tablice. Załóżmy, że klient klasy BinarySearchST wykonuje S operacji
wyszukiwania i używa N różnych kluczy. Podaj tempo wzrostu S, tak aby koszt two
rzenia tablicy był taki sam jak koszt wszystkich wyszukiwań.
3.1.28. Uporządkowane wstawianie. Zmodyfikuj program BinarySearchST tak, aby
wstawienie klucza większego niż wszystkie obecne klucze z tablicy zajmowało stały
czas (żeby czas budowania tablicy przez wywołania m etody put () dla uporządkowa
nych kluczy rósł liniowo).
3.1.29. Klient testowy. Napisz klienta testowego [Link] do testowa
nia przedstawionych w tekście implementacji metod mi n (), max (), floor ( ) , cei 1 i ng ( ) ,
se lect(), rank(), deleteMin(), deleteMax() i keys(). Zacznij od standardowego
klienta używającego indeksu (strona 382). Dodaj kod umożliwiający programowi
przyjmowanie — kiedy to potrzebne — dodatkowego argumentu z wiersza poleceń.
3.1.30. Sprawdzanie. Dodaj do program u BinarySearchST asercje do sprawdzania
niezmienników algorytmu i integralności struktury danych po każdym wstawieniu
oraz usunięciu danych. Przykładowo, każdy indeks i powinien zawsze być równy
rank (s e le c t ( i )), a tablica zawsze powinna być uporządkowana.
3.1 a Tablice symboli 405
EKSPERYMENTY
3.1.31. Sprawdzanie wydajności. Napisz program do sprawdzania wydajności, któ
ry używa m etody put() do zapełnienia tablicy symboli, a następnie używa metody
get () w taki sposób, że każdy klucz tablicy jest znajdowany średnio 10 razy, a liczba
nieudanych wyszukiwań jest podobna. Program ma wykonywać te operacje wielo
krotnie dla losowych ciągów kluczy w postaci łańcuchów znaków o różnej długości
(od 2 do 50 znaków), mierzyć czas każdego przebiegu i wyświetlać lub rysować śred
nie czasy wykonania.
3.1.32. Sprawdzanie poprawności. Napisz program do sprawdzania poprawności
używający m etod z interfejsu API uporządkowanej tablicy symboli do trudnych lub
„patologicznych” danych, które mogą wystąpić w praktyce. Proste przykłady to ciągi
już uporządkowanych kluczy, ciągi kluczy ustawionych w odwrotnej kolejności, ciągi
kluczy o tej samej wartości i zbiory kluczy, w których występują tylko dwie różne
wartości.
3.1.33. Program dla wyszukiwania samoporządkującego. Napisz program dla samo-
porządkujących implementacji wyszukiwania (zobacz ć w i c z e n i e 3 . 1 .22 ). Program
ma używać metody g et() do zapełnienia tablicy symboli N kluczami, a następnie
wykonywać 10N udanych wyszukiwań według zdefiniowanego rozkładu prawdo
podobieństwa. Użyj program u do porównania czasu wykonania implementacji
z ć w i c z e n i a 3 . 1.22 z klasą Bi narySearchST dla N = 103, 104, 105 i 106. Zastosuj roz
kład prawdopodobieństwa, w którym ¿-ty najmniejszy klucz znajdowany jest z praw
dopodobieństwem l / 2 z.
3.1.34. Prawo Zipfa. Wykonaj poprzednie ćwiczenie dla rozkładu prawdopodo
bieństwa, w którym z-ty najmniejszy lducz znajdowany jest z prawdopodobieństwem
1 /(z'Hn), gdzie H wto liczba harmoniczna (zobacz stronę 197). Ten rozkład wyznacza
ny jest przez prawo Zipfa. Porównaj heurystykę „przenieś na początek” z optymalnym
uporządkowaniem rozkładów zastosowanym w poprzednim ćwiczeniu, gdzie klucze
przechowywane są w rosnącej kolejności (w malejącym porządku według oczekiwa
nej liczby wystąpień).
3.1.35. Sprawdzanie wydajności I. Przeprowadź testy podwajania, w których na
podstawie pierwszych N słów książki Tale o f Two Cities (dla różnych N) sprawdzana
jest hipoteza, że czas wykonania program u FrequencyCounter rośnie kwadratowo,
jeśli jako tablica symboli wykorzystywana jest ldasa Sequenti al SearchST.
3.1.36. Sprawdzanie wydajności II. Ustal empirycznie stosunek ilości czasu, jald kla
sa Bi narySearchST spędza w metodzie put (), do czasu wykonywania operacji get (),
ldedy program FrequencyCounter określa liczbę wystąpień liczb w milionie losowych
M-bitowych wartości typu i nt dla M = 10, 20 i 30. Wykonaj to ćwiczenie dla pliku
[Link] i porównaj wyniki.
406 RO ZD ZIA Ł 3 a W yszukiw anie
EKSPERYMENTY (ciąg dalszy)
3.1.38. Wykresy zamortyzowanychkosztów. Zmodyfikuj programy FrequencyCounter,
Sequenti al SearchST i Bi narySearchST tak, aby można było generować wykresy po
dobne do tych pokazanych w podrozdziale w celu pokazania kosztu każdej operacji
put () w czasie obliczeń.
3 .1.39. Czas rzeczywisty. Zmodyfikuj program FrequencyCounter przez wykorzy
stanie w nim bibliotek Stopwatch i StdDraw do rysowania wykresu, w którym na osi
x widoczna jest liczba wywołań m etod get () lub put(), a na osi y — łączny czas
wykonania (generowane punkty mają pokazywać skumulowany czas po każdym wy
wołaniu). Uruchom program dla pliku z książką Tale o f Two Cities, używając klasy
Sequential SearchST, a następnie BinarySearchST. Omów wyniki. Uwaga: obecność
punktów znacznie odbiegających od krzywej m ożna wytłumaczyć buforowaniem.
Omawianie tego zagadnienia wykracza poza zakres ćwiczenia.
3.1.40. Przełączenie na wyszukiwanie binarne. Znajdź wartości N, dla których wy
szukiwanie binarne w tablicy symboli o wielkości N jest 10, 100 i 1000 razy szybsze
niż wyszukiwanie sekwencyjne. Ustal prognozy wartości na podstawie analiz i zwe
ryfikuj je eksperymentalnie.
3.1.41. Przełączenie na wyszukiwanie interpolacyjne. Znajdź wartości N, dla których
wyszukiwanie interpolacyjne w tablicy symboli o długości N jest 1,2 i 10 razy szybsze
niż wyszukiwanie binarne. Zakładamy, że klucze to 32-bitowe liczby całkowite (zo
bacz ć w i c z e n i e 3 . 1 .24 ). Ustal prognozy wartości na podstawie analiz i zweryfikuj je
eksperymentalnie.
w t y m p o d r o z d z i a l e omawiamy implementację tablicy symboli łączącą elastycz
ność wstawiania do listy powiązanej z wydajnością wyszukiwania w tablicy uporząd
kowanej. Wykorzystanie dwóch odnośników na węzeł (zamiast jednego odnośnika,
jak miało to miejsce w listach powiązanych) prowadzi do utworzenia wydajnej imple
mentacji tablicy symboli opartej na binarnym drzewie wyszukiwań. Implementacja
ta to jeden z najbardziej podstawowych algorytmów w naukach komputerowych.
Zacznijmy od zdefiniowania podstawowej term i
Korze ń
nologii. Używamy tu struktur danych składających
się z węzłów obejmujących odnośniki. Odnośniki są
albo puste (nuli), albo stanowią referencje do innych
węzłów. W drzewie binarnym obowiązuje ogranicze
nie, zgodnie z którym do każdego węzła prowadzi
tylko jeden inny, nazywany rodzicem (wyjątkiem jest
korzeń, do którego nie prowadzi żaden węzeł). Każdy
S tru k tu ra d rz e w a b in a rn e g o węzeł m a dokładnie dwa odnośniki (lewy i prawy),
prowadzące do lewego dziecka i prawego dziecka.
Choć odnośniki prowadzą do węzłów, można traktować je tak, jakby prowadziły
do drzew binarnych, których korzeniami są wskazywane węzły. Dlatego drzewem
binarnym jest albo pusty węzeł, albo węzeł z lewym i prawym odnośnikiem, przy
czym każdy odnośnik prowadzi do (rozłącznego) poddrzewa, które samo jest drze
wem binarnym. W binarnym drzewie wyszukiwań każdy węzeł ma klucz i wartość.
Zachowana jest określona kolejność, co umożliwia wydajne wyszukiwanie.
Definicja. Binarne drzewo wyszukiwań (ang. binary search tree — BST) to spe
cyficzne drzewo binarne — każdy węzeł ma w nim klucz zgodny z interfejsem
Comparabl e (i powiązaną z nim wartość), a drzewo spełnia warunek, zgodnie z któ
rym klucz w każdym węźle jest większy niż klucze we wszystkich węzłach lewego
poddrzewa i mniejszy niż klucze we wszystkich węzłach prawego poddrzewa.
Na rysunkach drzew BST klucze umieszczamy
K lu c z
Lew y
w węzłach i używamy zwrotów w rodzaju „A
o d n o ś n ik E jest lewym dzieckiem E” w których węzły są
W artość
p o w ią z a n a
utożsamiane z kluczami. Linie łączące węzły
to odnośniki. Wartość powiązaną z kluczem
) ZR
przedstawiamy czarnym kolorem obok węzłów
Klucze m niejsze n iż E Klucze w iększe n iż E (w zależności od kontekstu czasem pomijamy
S tru k tu ra b in a rn e g o d rz e w a w y sz u k iw a ń wartości). Odnośniki każdego węzła łączą go
z węzłami poniżej. Wyjątkiem są odnośniki pu
ste, przedstawiane jako krótkie hnie pod węzłem. W przykładach, jak zwykle, korzystamy
z jednoliterowych kluczy generowanych przez testowego klienta używającego indeksu.
3.2 □ Drzewa wyszukiwań binarnych 409
P odstaw ow a im plem entacja a l g o r y t m 3.3 to definicja drzewa BST używana
w podrozdziale do implementowania interfejsu API tablicy symboli. Zaczynamy od
omówienia definicji tej klasycznej struktury danych oraz cech implementacji metod
get () (wyszukiwanie) i put () (wstawianie).
Reprezentacja Do definiowania węzłów drzew BST służy prywatna klasa zagnież
dżona (podobnie jak w listach powiązanych). Każdy węzeł obejmuje klucz, wartość,
lewy odnośnik, prawy odnośnik i liczbę węzłów (tam, gdzie
7 11 Liczba węzłów (N) 8
to istotne, dołączamy na rysunkach liczbę węzłów czerwoną
czcionką nad węzłem). Lewy odnośnik prowadzi do drzewa
BST z elementami o mniejszych kluczach, a prawy odnośnik —
do drzewa BST z elementami o większych kluczach. Zmienna
egzemplarza N określa liczbę węzłów w poddrzewie, którego
korzeniem jest dany węzeł. Pole to, jak się okaże, ułatwia za
implementowanie różnych operacji na uporządkowanej tablicy a c e h m r s x
symboli. Prywatna metoda si ze() w a l g o r y t m i e 3.3 przypi
suje wartość 0 do pustych odnośników, dzięki czemu m ożna tak
zarządzać polem, aby mieć pewność, że niezmiennik:
size(x ) = s iz e ( x .le f t) + s iz e (x .rig h t) + 1
jest spełniony dla każdego węzła x w drzewie.
Drzewo BST reprezentuje zbiór kluczy (i powiązanych war H M R
tości). Ten sam zbiór m ożna przedstawić za pomocą wielu róż
nych drzew BST. Po umieszczeniu kluczy w drzewie BST w taki ° wa ^en sam zb ió T k lu T zy * ^ ^
sposób, że wszystkie klucze w każdym lewym poddrzewie znaj
dują się na lewo od klucza z danego węzła, a wszystkie klucze w każdym węźle prawe
go poddrzewa znajdują się na prawo od danego węzła, klucze zawsze są posortowane.
Elastyczność wynikającą z możliwości reprezentowania posortowanych kluczy przez
wiele drzew BST wykorzystujemy do opracowania wydajnych algorytmów służących
do tworzenia i używania takich drzew.
W yszukiw anie Wyszukiwanie klucza w tablicy symboli ma dwa możliwe rezultaty.
Jeśli węzeł zawierający klucz znajduje się w tablicy, następuje trafienie, dlatego należy
zwrócić powiązaną wartość. W przeciwnym razie ma miejsce chybienie (zwracana
jest wartość nul 1). Rekurencyjny algorytm do wyszukiwania kluczy w drzewach BST
bezpośrednio wynika ze struktury rekurencji. Jeśli drzewo jest puste, następuje chy
bienie. Jeżeli klucz wyszukiwania jest równy kluczowi korzenia, m a miejsce trafienie.
W przeciwnym razie należy (rekurencyjnie) przeszukać odpowiednie poddrzewo,
przechodząc w lewo, jeśli klucz wyszukiwania jest mniejszy, i w prawo, jeżeli jest
większy. Rekurencyjna metoda g e t() przedstawiona na stronie 411 to bezpośred
nia implementacja tego algorytmu. Metoda jako pierwszy argument przyjmuje węzeł
(korzeń poddrzewa), a jako drugi — klucz. Początkowo używany jest korzeń całego
drzewa i klucz wyszukiwania. Kod zachowuje niezmiennik, zgodnie z którym żadna
część drzewa inna niż poddrzewo, którego korzeniem jest bieżący węzeł, nie może
410 RO ZD ZIA Ł 3 W yszukiw anie
ALGORYTM 3.3. Tablica symboli oparta na drzewie BST
public c la s s BST<Key extends Comparable<Key>, Value>
private Node root; // Korzeń drzewa BST.
private c la s s Node
private Key key; // Klucz.
private Value v a l ; // Powiązana wartość.
private Node l e f t , rig h t; // Odnośniki do poddrzew.
private in t N; // Liczba węzłów w poddrzewie, którego
// korzeniem j e s t dany węzeł.
public Node(Key key, Value val, in t N)
( th is .k e y = key; t h i s . val = val; th is.N = N; }
}
public in t s iz e ()
{ return s iz e ( r o o t ) ; }
private in t size(Node x)
{
i f (x == n u ll) return 0 ;
else return x.N;
}
public Value get(Key key)
// Zobacz stronę 411.
public void put(Key key, Value val)
// Zobacz stronę 411.
// Metody min(), max(), floor() i c e i l i n g ( ) przedstawiono na stro n ie 419.
// Metody se le c t() i rank() przedstawiono na stro n ie 421.
// Metody delete(), deleteMin() i deleteMax() przedstawiono na stronie 423.
// Metodę keys() przedstawiono na stro n ie 425.
W tej implementacji interfejsu API dla uporządkowanej tablicy symboli wykorzystano
drzewo BST zbudowane z obiektów typu Node, z których każdy obejmuje klucz, powiązaną
wartość, dwa odnośniki i liczbę węzłów (N). Każdy obiekt Node to poddrzewo zawierające N
węzłów. Jego lewy odnośnik prowadzi do obiektu Node, który jest korzeniem o mniejszych
kluczach, a prawy odnośnik prowadzi do obiektu Node będącego korzeniem poddrzewa
o większych lduczach. Zmienna egzemplarza root wskazuje obiekt Node, który jest korze
niem danego drzewa BST (obejmuje ono wszystkie klucze i powiązane wartości z tablicy
symboli). Implementacje pozostałych metod znajdują się dalej w podrozdziale.
3.2 Drzewa wyszukiwań binarnych 411
ALGORYTM 3.3 (ciąg dalszy). Wyszukiwanie i wstawianie w drzewach BST
public Value get(Key key)
{ return get(root, key); }
private Value get(Node x, Key key)
{ // Zwraca wartość powiązaną z kluczem z poddrzewa, którego korzeniem
// je s t x.
// J e ś li klucza nie ma w tym poddrzewie, metoda zwraca n u li.
i f (x == n u li) return n u li;
in t cmp = [Link]([Link]);
if (cmp < 0 ) return g e t ( x . l e f t , key);
else i f (cmp > 0 ) return g e t ( x . r ig h t , key);
else return x . v a l ;
}
public void put(Key key, Value val)
{ // Wyszukiwanie klucza. Aktualizowanie wartości, j e ś l i ją znaleziono.
// Przy nowej wartości należy powiększyć ta blicę ,
root = put(root, key, v a l);
}
private Node put(Node x, Key key, Value val)
{
// Zmiana wartości klucza na val, j e ś l i klucz znajduje s ię
// w poddrzewie z korzeniem x. W przeciwnym razie
// należy dodać do poddrzewa nowy węzeł i powiązać key z val.
i f (x == n u ll) return new Node(key, val, 1);
in t cmp = [Link]([Link]);
if (cmp < 0 ) x . l e f t = p u t ( x . le f t , key, v a l);
else i f (cmp > 0 ) x . r ig h t = p u t(x .rig h t, key, v a l);
else [Link] = v a l ;
x.N = s i z e ( x . l e f t ) + s i z e ( x . r i g h t ) + 1;
return x;
}
Te implementacje metod g e t() i put () dla interfejsu API tablicy symboli są charaktery
stycznymi rekurencyjnymi metodami dla drzew BST i służą jako wzorzec dla kilku innych
implementacji omawianych dalej w rozdziale. Każdą metodę można zrozumieć zarówno na
podstawie działającego kodu, jak i za pomocą dowodu przez indukcję na podstawie hipotezy
indukcyjnej przedstawionej na początku.
412 RO ZD ZIA Ł 3 ■ W yszukiw anie
Udane wyszukiwanie R N ieu d ane w yszu kiw a nie T
R jest mniejsze
niż S, dlatego należy T jest większe
szukać po lewej niż S, dlatego należy
Czarne węzły m o gą p a so w ać
do klucza w yszukiw ania szukać p o prawej
Szare węzły na pew no
nie pasują do klucza T jest mniejsze
Rjest większe niż E, wyszukiw ania niż X, dlatego należy
dlatego należy
szukać p o lewej
szukać p o prawej
O dnośnik jest pusty,
Znaleziono R (trafienie), dlatego T nie znajduje się
dlatego należy w drzewie (chybienie)
zwrócić wartość
Trafienia (po lewej) i chybienia (po prawej) w drzewie BST
obejmować węzła z kluczem równym kluczowi wyszukiwania. Podobnie jak wielkość
przedziału w wyszukiwaniu binarnym zmniejsza się mniej więcej o połowę w każdej
iteracji, tak i w wyszukiwaniu w drzewach BST rozmiar poddrzewa, którego korze
niem jest bieżący węzeł, zmniejsza się przy przechodzeniu w dół drzewa (w idealnych
warunkach o około połowę, natomiast co najmniej o jeden element). Proces kończy
się po znalezieniu węzła zawierającego klucz wyszukiwania (trafienie) lub kiedy bie
żące poddrzewo staje się puste (chybienie). Zaczynamy od góry, a m etoda w każdym
węźle rekurencyjnie wywołuje samą siebie dla jednego z dzieci węzła, tak więc wy
szukiwanie powoduje określenie ścieżki w drzewie. Przy trafieniu ścieżka kończy się
w węźle obejmującym klucz. Przy chybieniu końcem ścieżki jest pusty odnośnik.
W staw ianie Kod wyszukiwania w a l g o r y t m i e 3.3 jest prawie tak prosty, jak kod
wyszukiwania binarnego. Prostota to podstawowa cecha drzew BST. Ważniejszą pod
stawową cechą jest to, że zaimplementowanie wstawiania nie jest dużo trudniejsze niż
zaimplementowanie wyszukiwania. Wyszukiwanie klucza, którego nie ma w drzewie,
prowadzi do pustego odnośnika. Wystarczy wtedy zastąpić odnośnik nowym węzłem
zawierającym dany klucz (zobacz rysunek na następnej stronie). Rekurencyjna m eto
da put () z a l g o r y t m u 3.3 wykonuje zadanie za pom ocą kodu podobnego do kodu
wyszukiwania rekurencyjnego. Jeśli drzewo jest puste, należy zwrócić nowy węzeł
zawierający klucz i wartość. Jeżeli klucz wyszukiwania jest mniejszy niż klucz w ko
rzeniu, należy ustawić lewy odnośnik na wynik wstawiania klucza do lewego pod
drzewa. W przeciwnym razie trzeba ustawić prawy odnośnik na wynik wstawiania
klucza do prawego poddrzewa.
3.2 ■ Drzewa wyszukiwań binarnych 413
Rekurencja Warto poświęcić czas na zrozu
mienie działania rekurencyjnych implemen
tacji. Można sobie wyobrazić, że kod przed re-
kurencyjnymi wywołaniami przechodzi w dół
drzewa — porównuje dany klucz z kluczem
z każdego węzła i przechodzi w prawo lub
w lewo. Kod po rekurencyjnym wywołaniu
przechodzi w górę drzewa. W metodzie get () pustym odnośniku
oznacza to serię instrukcji return, natomiast
w put () przy przechodzeniu w górę ścieżki
należy ponownie ustawić odnośnik w każdym
rodzicu na dziecko ze ścieżki wyszukiwania
i zwiększyć liczbę węzłów. W prostych drze Tworzenie nowego węzła
wach BST jedyny nowy odnośnik znajduje się
na samym dole, natomiast ponowne ustawie
nie odnośników wyżej w ścieżce jest równie
łatwe, jak testowanie pozwalające uniknąć
ich ustawiania. Oprócz tego wystarczy zwięk
szyć liczbę węzłów w każdym węźle w ścież Ponowne ustawianie odnośników
ce. Używamy tu ogólniejszego kodu, który i zwiększanie liczby węzłów
przy przechodzeniu w górę
ustawia tę liczbę na jeden plus sumę liczb
w poddrzewach. Dalej w tym podrozdziale W sta w ia n ie d o d rz e w a BST
i w następnym podrozdziale omówiono bar
dziej zaawansowane algorytmy. Można je w naturalny sposób zapisać za pomocą tego
samego rekurencyjnego schematu, jednak algorytmy te modyfikują większą liczbę
odnośników na ścieżkach wyszukiwania i wymagają ogólniejszego kodu do aktuali
zacji liczby węzłów. Podstawowe drzewa BST często implementuje się za pomocą nie-
rekurencyjnego kodu (zobacz ć w i c z e n i e 3 .2 . 1 2 ). W przedstawianych tu implemen
tacjach stosujemy rekurencję, aby umożliwić przekonanie się, że kod działa w opisany
sposób, oraz aby przygotować podstawy pod bardziej zaawansowane algorytmy.
s t a r a n n a a n a l i z a śladu działania standardowego klienta używającego indeksu,
przedstawiona na następnej stronie, pomaga zrozumieć, jak rośnie drzewo BST.
Nowe węzły są dołączane do pustych odnośników w dolnej części drzewa. Struktura
drzewa nie zmienia się w żaden inny sposób. Przykładowo, pierwszy klucz wsta
wiany jest w korzeniu, drugi klucz — w jednym z dzieci korzenia itd. Ponieważ każ
dy węzeł ma dwa odnośniki, drzewo rośnie nie tylko w dół, ale też wszerz. Ponadto
sprawdzane są tylko klucze na ścieżce z korzenia do szukanego lub wstawianego
klucza, dlatego wraz z powiększaniem się drzewa procent badanych kluczy staje się
coraz mniejszy.
414 RO ZD ZIA Ł 3 b W yszukiwanie
Klucz Wartość Klucz Wartość
s o A 8
&
(A J8 IR )
Zmodyfikowana /
y CaaA (Hp
/A
E 1 wartość
M 9
A 2
R 3
P 10
C 4
H 5
Zmodyfikowana sp,
wartość
E 6
(aj l£ )
O ć) © x
AA AA
i E
X 7
(Aj iR )
(c ) ( h)
r\ aa /A aA
Ślad zm ian w drzewie BST dla standardow ego klienta używ ającego indeksu
3.2 o Drzewa wyszukiwań binarnych 415
A n a li z y Czas wykonania algorytmów działających Najlepszy przypadek
na drzewach BST zależy od kształtu drzew, który z ko
lei wynika z kolejności wstawiania kluczy. W najlepszym
przypadku drzewo o N węzłach może być idealnie zba-
lansowane. Między korzeniem a każdym odnośnikiem
pustym znajduje się ~lg N węzłów. W najgorszym przy Typowy przypadek
padku ścieżka wyszukiwania może obejmować N węzłów.
Równowaga w typowych drzewach okazuje się znacznie
bliższa najlepszemu niż najgorszemu przypadkowi.
W wielu zastosowaniach m ożna przyjąć następujący
prosty model: zakładamy, że klucze są (równomiernie) lo
sowe, czyli że wstawiono je w losowej kolejności. Analizy
dla tego modelu są oparte na spostrzeżeniu, że drzewa
BST są analogiczne do sortowania szybkiego. Węzeł w ko
rzeniu drzewa odpowiada pierwszemu elementowi osio
wemu z sortowania szybldego (żaden klucz po lewej stro
nie nie jest większy, a żaden klucz po prawej — mniejszy),
a poddrzewa są tworzone rekurencyjnie, co przypomina M ożliw e d rz e w a BST
rekurencyjne sortowanie podtablic w sortowaniu szyb
kim. To spostrzeżenie prowadzi do analiz cech drzew.
Twierdzenie C. Trafienia w drzewie BST zbudowanym na podstawie N loso
wych kluczy wymagają średnio ~2 ln N (około 1,39 lg N) porównań.
Dowód. Liczba porównań przy trafieniu kończącym się w danym węźle wynosi
1 plus głębokość węzła. Suma głębokości wszystkich węzłów to długość ścieżki
wewnętrznej drzewa. Dlatego szukana wartość to 1 plus średnia długość ścieżki
wewnętrznej drzewa BST, którą można ustalić za pom ocą tego samego wnio
skowania, co dla t w i e r d z e n i a k z p o d r o z d z i a ł u 2 .3 . Niech C;<to łączna dłu
gość ścieżki wewnętrznej drzewa BST zbudowanego przez wstawienie N losowo
uporządkowanych różnych kluczy. Średni koszt trafienia wynosi więc 1 + C JN .
Mamy C0=Cj=0, a dla N > 1 można podać zależność rekurencyjną, która bezpo
średnio odpowiada rekurencyjnej strukturze drzewa BST:
CN= N - 1 + (C 0+ Cm )/N + (C, + Cn_2) / N + ... ( C j + C0)/N
Wyraz N - 1 wynika z tego, że korzeń powoduje zwiększenie o 1 długości ścieżki
każdego z pozostałych N - 1 węzłów drzewa. Reszta wyrażenia dotyczy pod-
drzew, mających z równym prawdopodobieństwem dowolną z N wielkości.
Po uporządkowaniu wyrazów uzyskana zależność rekurencyjną jest prawie iden
tyczna z obliczoną w p o d r o z d z i a l e 2.3 dla sortowania szybkiego. Można wy
prowadzić z niej przybliżenie C ~2N ln N.
416 RO ZDZIAŁ 3 □ W yszukiwanie
Twierdzenie D. Wstawienia i chybienia w drzewie BST zbudowanym z N loso
wych kluczy wymagają średnio ~2 ln N (około 1,39 lg N) porównań.
Dowód. Wstawienia i chybienia wymagają średnio jednego więcej porównania
niż trafienia. Nietrudno udowodnić to przez indukcję (zobacz ć w i c z e n i e 3 .2 .1 6 ).
Zgodnie z t w i e r d z e n i e m c można oczekiwać, że koszt wyszukiwania w drzewach
BST z losowymi kluczami będzie około 39% wyższy niż przy wyszukiwaniu binar
nym. Według t w i e r d z e n i a D warto ponieść ten dodatkowy koszt, ponieważ koszt
wstawienia nowego klucza także jest logarytmiczny. Ta elastyczność była niemożliwa
przy wyszukiwaniu binarnym w uporządkowanej tablicy — wtedy liczba wymaga
nych dostępów do tablicy przy wstawianiu zwykle rośnie liniowo. Tak jak w sorto
waniu szybkim, tak i tu odchylenie standardowe liczby porównań jest niskie, dlatego
wzory stają się coraz dokładniejsze wraz z rosnącym N.
E ksp erym enty Jak dobrze model oparty na losowych kluczach pasuje do typo
wych klientów używających tablic symboli? Jak zawsze trzeba starannie zbadać tę
kwestię w konkretnych zastosowaniach praktycznych z uwagi na potencjalną dużą
zmienność wydajności. Na szczęście dla wielu klientów m odel ten dość dobrze opi
suje drzewa BST.
W przykładowym badaniu kosztów operacji put () w program ie FrequencyCounter
dla słów o długości 8 lub więcej średni koszt spada z 484 dostępów do tablicy lub
porównań na operację w klasie BinarySearchST do 13 dostępów w klasie BST. Jest
to szybkie potwierdzenie logarytmicznej wydajności obliczonej za pom ocą modelu
teoretycznego. Bardziej rozbudowane eksperymenty dla większych danych wejścio
wych przedstawiono w tabeli na następnej stronie. Na podstawie t w i e r d z e ń c i d
można prognozować, że liczba dostępów powinna wynosić mniej więcej dwukrot-
ność logarytmu naturalnego z rozmiaru tablicy. Wynika to z tego, że w prawie pełnej
tablicy większość operacji to wyszukiwania. Prognoza ta obciążona jest co najmniej
poniższymi nieścisłościami:
■ Wiele operacji przeprowadzanych jest na mniejszych tablicach.
■ Klucze nie są losowe.
D Rozmiar tablicy może być zbyt mały, aby przybliżenie 2 ln N było precyzyjne.
Mimo to, jak widać w tablicy, prognozy dla przypadków testowych i progra
m u FreąuencyCounter okazały się precyzyjne z dokładnością do kilku porównań.
Większość różnic m ożna wyjaśnić przez doprecyzowanie obliczeń matematycznych
w przybliżeniu (zobacz ć w i c z e n i e 3 .2 .3 5 ).
3.2 a Drzewa wyszukiwań binarnych 417
Skala powiększona 250 razy
w porównaniu z poprzednimi rysunkami
t a le . t x t le ip z ig lM . t x t
Słowa Różne Porównania sfowa Różne Porównania
łącznie słowa M odel Uzyskano ł4cznie słowa Model Uzyskano
Wszystkie słowa 135 635 10 679 18,6 17,5 21 191 455 534 580 23,4 22,1
Ponad 8 liter 14 350 5737 17,6 13,9 4 239 597 299 593 22,7 21,4
Ponad 10 liter 4582 2260 15,4 13,1 1 610 829 165 555 20,5 19,3
Średnia liczba porównań na operację put() w programie FreguencyCounter korzystającym z klasy BST
418 R O ZD ZIA Ł 3 □ W yszukiw anie
Metody oparte na uporządkowaniu i usuwanie Ważną przyczyną po
pularności drzew BST jest to, że umożliwiają zachowanie kolejności kluczy. Dlatego
można wykorzystać je jako podstawę do implementowania licznych metod z inter
fejsu API uporządkowanej tablicy symboli (zobacz stronę 378), umożliwiających
klientom dostęp do par klucz-wartość nie tylko przez podanie klucza, ale też według
względnej kolejności kluczy. Dalej omówiono implementacje różnych m etod inter
fejsu API uporządkowanych tablic symboli.
M inim um i m aksim um Jeśli lewy odnośnik korzenia jest pusty, najmniejszym klu
czem drzewa BST jest klucz korzenia. Jeżeli lewy odnośnik nie jest pusty, najmniejszym
kluczem drzewa BST jest najmniejszy klucz poddrzewa, którego korzeniem jest węzeł
wskazywany przez lewy odnośnik. Ten fragment to zarówno opis rekurencyjnej meto
dy mi n () ze strony 419, jak i indukcyjny dowód na to, że metoda znajduje najmniejszy
klucz drzewa BST. Przetwarzanie przebiega podobnie jak w prostej wersji iteracyjnej
(należy przechodzić w lewo do czasu znalezienia pustego odnośnika), jednak z uwagi na
spójność zastosowaliśmy rekurencję. Rekurencyjna metoda może zwracać obiekt typu
Key zamiast Node, jednak wspomniana metoda będzie później potrzebna do uzyskania
dostępu do obiektu Node zawierającego minimalny klucz. Znajdowanie klucza maksy
malnego przebiega podobnie, przy czym należy przechodzić w prawo, a nie w lewo.
Podłoga i sufit Jeśli dany klucz key ma war
Znajdowanie wartości f1oor(G)
tość mniejszą niż klucz korzenia drzewa BST,
podłoga dla wartości key (największy klucz
w drzewie BST mniejszy lub równy względem
key) musi znajdować się w lewym poddrzewie.
Jeżeli key ma wartość większą niż klucz korze
nia, podłoga dla wartości key może znajdować
się w prawym poddrzewie, ale tylko wtedy,
jeśli istnieje w nim klucz mniejszy lub rów
ny względem key. Gdy takiego klucza nie ma
(lub key jest równy kluczowi korzenia), pod
dlateao f lo o r f G ') może łogą dla key jest klucz korzenia. Także tu opis
jest podstawą zarówno rekurencyjnej metody
floor(), jak i indukcyjnego dowodu na to, że
metoda zwraca pożądany wynik. Po zamianie
lewej i prawej strony (oraz zależności mniejszy
oraz większy) uzyskamy funkcję cei 1 i ng ().
Wartość r l oo r (.gj w lewym
poddrzewie to n u li W ybieranie Wybieranie elementów drzewa
BST działa w sposób analogiczny do metody
opartej na podziale, stosowanej do wybiera
nia elementów tablicy (technikę tę omówio
no w p o d r o z d z i a l e 2 . 5 ). Pomaga w tym
przechowywana w węzłach BST zmienna N
O b lic z a n ie w a rto ści funkcji floor() z liczbą kluczy w poddrzewie, którego ko
rzeniem jest dany węzeł.
3.2 Drzewa wyszukiwań binarnych 419
ALGORYTM 3.3 (ciąg dalszy). Minimum, maksimum, podłoga i sufit dla drzew BST
public Key min()
return min(root).key;
private Node min(Node x)
i f ( x . l e f t == n u ll) return x;
return m i n ( x . l e f t ) ;
public Key floor (Key key)
Node x = floor(root, key);
i f (x == n u ll) return n u ll;
return [Link];
private Node floor (Node x, Key key)
(
i f (x == n u ll) return n u ll;
in t cmp = [Link]([Link]);
i f (cmp == 0 ) return x;
i f (cmp < 0 ) return floor ( x . l e f t , key);
Node t = floor([Link], key);
i f (t != n u ll) return t;
else return x;
}
Każda metoda klienta wywołuje odpowiednią metodę prywatną, która przyjmuje jako ar
gument dodatkowy odnośnik (do obiektu Node) i zwraca nul 1 lub obiekt Node zawierający
pożądany obiekt Key. Metoda działa w rekurencyjny sposób opisany w tekście. Metody max ()
i cei 1 i ng () są takie same jak mi n () oraz floor (), przy czym strony prawa i lewa (oraz ope
ratory <i >) są zamienione.
420 R O ZD ZIA Ł 3 n W yszukiw anie
Załóżmy, że szukamy klucza z pozycji k (ta s e t e c t ( 3 ) - w yszukiw anie klucza z pozycji 3.
kiego, od którego mniejszych jest dokładnie k
Liczba węzłów (n)
innych kluczy drzewa BST). Jeśli liczba kluczy
t w lewym poddrzewie jest większa niż k, nale
ży (rekurencyjnie) poszukać klucza z pozycji k AA
R)'
w lewym poddrzewie. Jeżeli t jest równe k, wy \
( C) H i
Lewe poddrzewo
starczy zwrócić klucz z korzenia. Dla t mniej ,M) obejmuje 8 kluczy,
AA dlatego klucza
szych niż k trzeba (rekurencyjnie) poszukać
z pozycji 3.
klucza z pozycji f c - f - l w prawym poddrzewie. należy szukać
Jak zwykle opis ten stanowi zarówno podstawę po lewej stronie
rekurencyjnej metody s e le c t(), pokazanej na
następnej stronie, jak i dowodu przez indukcję
na to, że metoda działa w oczekiwany sposób.
Pozycja Odwrotna metoda rank(), zwracająca
pozycję danego klucza, wygląda podobnie. Jeśli Lewe poddrzewo '
obejmuje 2 klucze,
klucz jest równy kluczowi korzenia, należy zwró dlatego klucza
cić liczbę kluczy z lewego poddrzewa — t. Jeżeli z pozycji 3-2-1 = 0
należy szukać
dany klucz jest mniejszy po prawej stronie
Przechodzenie w lewo niż w korzeniu, należy
do momentu dojścia
do pustego odnośnika zwrócić pozycję klucza
w lewym poddrzewie
\ Lewe poddrzewo
(rekurencyjnie ustaloną). obejmuje 2 klucze,
Dla klucza większego dlatego klucza
z pozycji 0 należy
niż klucz korzenia nale
szukać po lewej
Należy zwrócić ży zwrócić t plus 1 (aby stronie
prawy odnośnik uwzględnić klucz korze
danego węzła
nia) plus pozycja klucza
r\ w prawym poddrzewie
(rekurencyjnie obliczona).
(H)
/A H m) Usuwanie m inim um Lewe poddrzewo
i m aksim um Najtrud obejmuje 0 kluczy,
Dostępny dla mechanizmu
a szukamy klucza
przywracania pamięci niejszą do zaimplemen z pozycji 0., dlatego
towania operacją na należy zwrócić H
Aktualizowanie
odnośników i liczby węzłów drzewie BST jest metoda Wybieranie w drzewach BST
po wywołaniach
d elete (), usuwająca parę
klucz-wartość z tablicy symboli. W ramach rozgrzewki rozważ
my metodę deleteMin() (usuwa ona parę klucz-wartość z naj
mniejszym kluczem). Podobnie jak w przypadku metody put (),
tak i tu trzeba napisać rekurencyjną metodę, która przyjmuje
jako argument odnośnik do obiektu Node i zwraca odnośnik do
takiego obiektu, co pozwala odzwierciedlić zmiany w drzewie
Usuwanie minimum w drzewie BST przez przypisanie wyniku do odnośnika użytego jako argument.
3.2 Drzewa wyszukiwań binarnych 421
ALGORYTM 3.3 (ciąg dalszy). Wybieranie i pozycje w drzewach BST
public Key select (i nt k)
{
return se le c t(ro o t, k ) . key;
}
private Node select(Node x, in t k)
{ // Zwraca obiekt Node zawierający klucz z pozycji k.
i f (x == n u li) return n u li;
in t t = s i z e ( x . l e f t ) ;
if (t > k) return s e l e c t ( x . l e f t , k ) ;
else i f (t < k) return s e l e c t ( x . rig h t, k - t - 1 );
else return x;
}
public in t rank(Key key)
{ return rank(key, root); }
p rivate in t rank(Key key, Node x)
{ // Zwraca liczb ę kluczy mniejszych niż [Link] w poddrzewie o korzeniu x.
i f (x == n u li) return 0 ;
in t cmp = [Link]([Link]);
if (cmp < 0 ) return rank(key, x . l e f t ) ;
else i f (cmp > 0 ) return 1 + s i z e ( x . l e f t ) +rank(key, x . r i g h t ) ;
else return s i z e ( x . l e f t ) ;
}
W tym kodzie rekurencyjny schemat używany w całym rozdziale zastosowano w metodach
s e l e c t O i rank(). Wymagają one zastosowania przedstawionej na początku podrozdziału
metody prywatnej s i ze (), zwracającej liczbę węzłów w poddrzewach, których korzeniami
są poszczególne węzły.
422 RO ZD ZIA Ł 3 b W yszukiwanie
W metodzie del eteMi n () należy poruszać się w lewo do momentu znalezienia obiektu
Node, którego lewy odnośnik jest pusty. Wtedy trzeba zastąpić odnośnik do węzła jego
prawym odnośnikiem (wystarczy zwrócić prawy odnośnik w metodzie rekurencyjnej).
Usunięty węzeł, do którego nie prowadzą żadne odnośniki, jest dostępny dla mechani
zmu przywracania pamięci. Standardowe, rekurencyjne rozwiązanie po usunięciu wę
zła ustawia odpowiedni odnośnik w rodzicu i aktualizuje liczbę węzłów we wszystkich
węzłach na ścieżce do korzenia. Metoda del eteMax() działa symetrycznie.
Usuw anie E Usuwanie W podobny sposób można usunąć do
wolny węzeł, który ma jedno dziecko (lub w ogóle
Usuwany węzeł nie ma dzieci), co jednak trzeba zrobić, aby usunąć
\
węzeł mający dwoje dzieci? Istnieją dwa odnośni
ki, jednak w węźle rodzica jest miejsce tylko na je
¡A t
den z nich. Rozwiązanie tego problemu, zapropo
/ć ^ y - Szukanie klucza E
( m) nowane po raz pierwszy przez T. Hibbarda w 1962
roku, polega na usunięciu węzła x przez zastąpie
\ nie go następnikiem. Ponieważ x ma prawe dzie
(e) cko, następnikiem jest najmniejszy klucz z prawe
go poddrzewa. W czasie zastępowania zachowany
/"A . . Następnik zostaje porządek w drzewie, ponieważ między
Należy przejść w prawo, / 'j- \ Cmi n ( t . rig h t) j x . key a kluczem następnika nie ma żadnych in
a następnie w lewo do /
momentu napotkania
nych kluczy. Klucz x można zastąpić następnikiem
pustego lewego \ w czterech (!) prostych krokach. Oto one:
odnośnika — yu,
■ Zapisanie w t odnośnika do usuwanego węzła.
$
deleteMi n ( t . ri ght) ■ Ustawienie x tak, aby wskazywał następnik
v _ /
— m in ([Link] h t).
(C) (Mj ■ Ustawienie prawego odnośnika w x (ma on
wskazywać drzewo BST zawierające wszystkie
klucze większe niż [Link]) na del eteMi n ( t .
rig h t). Jest to odnośnik do drzewa BST za
wierającego wszystkie klucze większe niż
x . key po usunięciu.
* Ustawienie lewego odnośnika w x (wcześniej
Aktualizacja odnośników
i liczby węzłów po miał wartość nul 1 ) na t . 1 e f t (czyli wszystkie
rekurencyjnych wywołaniach klucze mniejsze niż usunięty klucz i jego na
U su w a n ie w d rz e w a c h BST stępnik).
Standardowe rekurencyjne rozwiązanie po re-
kurencyjnych wywołaniach ustawia odpowiedni odnośnik w rodzicu i zmniejsza
wartość pola z liczbą węzłów w węzłach na ścieżce do korzenia (aktualizowanie także
tu odbywa się przez ustawienie liczby w każdym węźle ścieżki na jeden plus suma
liczb węzłów z dzieci). Choć m etoda ta działa, ma wadę, która w praktyce może spo
wodować problemy z wydajnością. Decyzja o zastosowaniu następnika jest arbitralna
i niesymetryczna. Dlaczego nie użyć poprzednika? W praktyce warto losowo wybie
rać poprzednik lub następnik. Szczegółowo opisano to w ć w i c z e n i u 3 .2 .4 2 .
3.2 Drzewa wyszukiwań binarnych 423
ALGORYTM 3.3 (ciąg dalszy). Usuwanie z drzew BST
public void deleteMinO
{
root = d e leteMin( r o o t ) ;
}
private Node deleteMin(Node x)
{
i f ( x . l e f t == n u ll) return x . r ig h t ;
x . l e f t = d e l e t e M in ( x . l e f t ) ;
x.N = s i z e ( x . l e f t ) + s i z e ( x . r i g h t ) + 1;
return x;
}
public void delete(Key key)
{ root = delete(root, key); }
private Node delete(Node x, Key key)
{
i f (x == n u ll) return n u ll;
in t cmp = [Link]([Link]);
if (cmp < 0 ) x . l e f t = delete ( x . le f t , key);
else i f (cmp > 0 ) x . r ig h t = d e le t e ( x .rig h t , key);
el se
{
i f ( x . r ig h t == n u ll) return x . l e f t ;
i f ( x . l e f t == n u ll) return x . r ig h t ;
Node t = x;
x = m i n ( t . r i g h t ) ; // Zobacz stronę 419.
x . r i g h t = d e le t e M in ( t . r ig h t ) ;
x .le ft = [Link] ft;
}
x.N = s i z e ( x . l e f t ) + s i z e ( x . r i g h t ) + 1;
return x;
}
W tych metodach zaimplementowano zachłanne usuwanie Hibbarda dla drzew BST, co opi
sano w tekście na poprzedniej stronie. Kod metody delete() jest zwięzły, ale skompliko
wany. Prawdopodobnie najlepszy sposób na jego zrozumienie to przeczytać opis po lewej
stronie, spróbować samodzielnie napisać kod na podstawie tekstu, a następnie porównać swój
kod z kodem z książki. Przedstawiona tu metoda jest zwykle skuteczna, jednak jej wydajność
w dużych aplikacjach może być problematyczna (zobacz ć w i c z e n i e 3 .2 .42 ). Metoda del ete-
Max() wygląda tak samo, jak deleteMinO, jednak zamieniono w niej stronę prawą z lewą.
424 RO ZD ZIA Ł 3 ■ W yszukiwanie
Zapytania zakresowe Aby zaimplementować metodę keys (), zwracającą klucze z da
nego przedziału, należy zacząć od podstawowej rekurencyjnej metody poruszania się
po drzewach BST, nazywanej przechodzeniem w porządku inorder. Rozważmy wyświet
lanie po kolei wszystkich kluczy drzewa BST. W tym celu należy wyświetlić wszystkie
klucze z lewego poddrzewa (z definicji drzewa BST wynika, że są mniejsze niż klucz
korzenia), następnie klucz korzenia, a potem wszystkie klucze
p riv a te void print(N ode x) z prawego poddrzewa (według definicji drzewa BST są większe
( niż klucz korzenia). Tak działa kod pokazany po lewej. Jak zwy
i f (x == n u ll) return;
kle, opis służy za dowód przez indukcję, że kod wyświetla klu
p rin t ( x .le f t );
S t d O u t. p rin t ln (x . k e y ); cze po kolei. Aby zaimplementować dwuargumentową metodę
p rin t (x. r ig h t ) ; keys(), która zwraca klientowi wszystkie klucze z określonego
) przedziału, należy zmodyfikować ten kod. Trzeba dodać każ
dy klucz z przedziału do obiektu Queue i pominąć rekurencyjne
Wyświetlanie po kolei kluczy
drzewa BST wywołania dla poddrzew, które z pewnością nie zawierają klu
czy z danego przedziału. Tak jak w klasie BinarySearchST, tak
i tu zapisywanie kluczy w obiekcie Queue jest ukryte przed klientem. Chodzi o to, że
w klientach powinno być możliwe przetwarzanie wszystkich kluczy z danego przedzia
łu za pomocą konstrukcji foreach Javy, tak aby nie trzeba było znać struktury danych
użytej do implementacji interfejsu Iterable<Key>.
Analiza Jak wydajne są operacje oparte na kolejności na drzewach BST? Aby odpowie
dzieć na to pytanie, zastanówmy się nad wysokością drzewa (maksymalną głębokością
dowolnego węzła w drzewie). Wysokość drzewa określa koszt dla najgorszego przypad
ku dla wszystkich operacji na drzewie BST (wyjątkiem jest wyszukiwanie zakresowe,
które powoduje dodatkowe koszty proporcjonalne do liczby zwracanych kluczy).
Twierdzenie E. W drzewach BST wszystkie operacje w najgorszym przypadku
zajmują czas proporcjonalny do wysokości drzewa.
Dowód. Wszystkie metody schodzą w dół drzewa jedną ścieżką lub dwoma.
Długość ścieżki z definicji nie może być większa od wysokości drzewa.
Oczekujemy, że wysokość drzewa (koszt dla najgorszego przypadku) będzie więk
sza niż średnia długość ścieżki wewnętrznej zdefiniowana na stronie 415 (w średniej
uwzględniane są też krótkie ścieżki), jak duża jest jednak ta różnica? Pytanie to może
wydawać się podobne do pytań z t w i e r d z e ń c i d , jednak dużo trudniej jest udzielić
na nie odpowiedzi. Kwestia ta zdecydowanie wykracza poza zakres książki. J. Robson
w 1979 roku wykazał, że średnia wysokość drzewa BST zbudowanego z losowych
kluczy jest logarytmiczna, a L. Davroye później udowodnił, że dla dużych N wyso
kość zbliża się do 2,99 lg N. Dlatego jeśli wstawianie elementów w danej aplikacji jest
dobrze opisywane przez model oparty na kluczach losowych, jesteśmy na dobrej dro
dze do opracowania implementacji tablicy symboli, która pozwala wykonać wszyst-
3.2 Drzewa wyszukiwań binarnych 425
ALGORYTM 3.3 (ciąg dalszy). Wyszukiwanie zakresowe w drzewach BST
public Iterable<Key> keys()
{ return keys(min(), max()); }
public Iterable<Key> keys(Key lo, Key hi)
{
Queue<Key> queue = new Queue<Key>();
keys(root, queue, lo, h i);
return queue;
}
private void keys(Node x, Queue<Key> queue, Key lo, Key hi)
{
if (x == n u li) return;
in t cmplo = [Link]([Link]);
in t cmphi = [Link]([Link]);
if (cmplo < 0) k e y s ( x .le f t , queue, lo, h i) ;
if (cmplo <= 0 && cmphi >= 0) [Link]([Link]);
if (cmphi > 0) k e y s (x .rig h t, queue, lo, h i) ;
}
Aby dodać do kolejki wszystkie klucze z drzewa o korzeniu w danym węźle, które należą do
przedziału, należy rekurencyjnie dodać wszystkie klucze z lewego poddrzewa (jeśli któreś
z nich znajdują się w przedziale), następnie dodać węzeł korzenia (jeżeli należy do przedzia
łu), a potem rekurencyjnie dodać wszystkie klucze z prawego poddrzewa (jeśli którekolwiek
z nich znajdują się w przedziale).
W yszukiw anie w p rzed ziale [F . .T]
Czerwone klucze biorą udział w porów naniach,
ale nie należą do przedziału
Wyszukiwanie zakresowe w drzewach BST
426 RO ZD ZIA Ł 3 ■ W yszukiw anie
kie operacje w czasie logarytmicznym. Można oczekiwać, że w drzewie zbudowanym
z kluczy losowych żadna ścieżka nie będzie dłuższa niż 3 lg N, czego jednak można
się spodziewać, jeśli klucze nie są losowe? W następnym podrozdziale dowiesz się,
dlaczego w praktyce pytanie to nie ma znaczenia. Wynika to ze stosowania zbalan-
sowanych drzew BST, które gwarantują, że wysokość drzewa BST jest logarytmiczna
niezależnie od kolejności wstawiania kluczy.
p o d s u m u j m y — drzewa BST nie są trudne w implementacji i umożliwiają szybkie
wyszukiwanie oraz wstawianie w różnorodnych praktycznych zastosowaniach, jeśli
dobrym przybliżeniem procesu wstawiania kluczy jest model oparty na kluczach lo
sowych. W opisanych przykładach (i w wielu praktycznych sytuacjach) drzewa BST
pozwalają wykonać zadania, których nie można zrealizować w inny sposób. Ponadto
wielu programistów wybiera drzewa BST do implementowania tablic symboli, p o
nieważ umożliwiają szybkie określanie pozycji, wybieranie, usuwanie i wykonywanie
zapytań zakresowych. Jednak, jak podkreśliliśmy, w niektórych sytuacjach wydaj
ność drzew BST dla najgorszego przypadku jest nieakceptowalna. Wysoka wydajność
podstawowej implementacji drzew BST wymaga, aby klucze były odpowiednio loso
we. Wtedy drzewo zwykle nie obejmuje wielu długich ścieżek. W sortowaniu szyb
kim można przeprowadzić randomizację. Interfejs API tablicy symboli nie daje takiej
swobody, ponieważ to klient wykonuje operacje. Wystąpienie najgorszego przypadku
w praktyce jest możliwe. Dzieje się tak, kiedy w kliencie klucze wstawiane są po kolei
(lub w odwrotnej kolejności). Twórcy niektórych klientów z pewnością mogą próbo
wać to zrobić, jeśli zabraknie bezpośrednich ostrzeżeń. Ta możliwość to główna przy
czyna poszukiwania lepszych algorytmów i struktur danych, co omawiamy dalej.
Koszt dla najgorszego Koszt dla typowego Wydajne
przypadku (po N przypadku (po N operacje
Algorytm (struktura
wstawieniach) losowych wstawieniach) Zależne od
danych)
kolejności?
Wyszukiwanie Wstawianie Trafienie Wstawianie
Wyszukiwanie sekwencyjne
(nieuporządkowana N N NI 2 N Nie
lista powiązana)
Wyszukiwanie binarne
lg N N lg N NI 2 Tak
(tablica uporządkowana)
Binarne drzewa
N N 1,39 lg N 1,39 lg N Tak
wyszukiwań (BST)
Podsumowanie kosztów im plem entacji podstawowej tablicy sym boli (uzupełnione)
3.2 * Drzewa wyszukiwań binarnych 427
PYTANIA I O D PO W IED ZI
P. Zetknąłem się już z drzewami BST, ale bez stosowania rekurencji. Jakie są wady
i zalety użycia tej techniki?
O. Ogólnie implementacje rekurencyjne ułatwiają nieco weryfikację poprawności,
a implementacje nierekurencyjne są trochę wydajniejsze. W ć w i c z e n i u 3 .2.13 opi
sano implementację m etody get () w sytuacji, w której m ożna odczuć wyższą wydaj
ność. Jeśli drzewo jest niezbalansowane, głębokość stosu wywołań funkcji może sta
nowić problem w implementacji rekurencyjnej. Głównym powodem zastosowania
rekurencji jest łatwość przejścia do implementacji dla zbalansowanych drzew BST,
omówionych w następnym podrozdziale. Takie drzewa zdecydowanie łatwiej jest im
plementować i diagnozować za pomocą rekurencji.
P. Utrzymywanie pola z liczbą węzłów w obiektach Node wymaga dużo kodu. Czy
pole to jest niezbędne? Dlaczego na potrzeby m etody klienckiej s i ze () nie przecho
wujemy jednej zmiennej egzemplarza zawierającej liczbę węzłów w drzewie?
O. W metodach rank() i sel ect () potrzebny jest rozmiar poddrzew o korzeniach
w poszczególnych węzłach. Jeśli używasz tego typu operacji na uporządkowanych da
nych, możesz usprawnić kod, usuwając omawiane pole (zobacz ć w i c z e n i e 3 .2 . 1 2 ).
Zachowanie właściwej wartości pola z liczbą węzłów w każdym węźle jest trudne.
Warto przyjrzeć się tej kwestii w trakcie diagnozowania. Możesz też użyć rekurencji
do zaimplementowania m etody si ze() dla klientów, jednak wtedy zliczanie wszyst
kich węzłów zajmuje czas rosnący liniowo. Jest to niebezpieczne, ponieważ może pro
wadzić do niskiej wydajności programu klienckiego, jeśli jego autor nie zdaje sobie
sprawy, że tak prosta operacja jest tak kosztowna.
428 R O ZD ZIA Ł 3 0 W yszukiw anie
I ĆW ICZEN IA
3.2.1. Narysuj drzewo BST powstałe przez wstawienie kluczy E A S Y Q U E S T I 0 N
w tej kolejności (powiąż wartość i z i -tym kluczem, tak jak w tekście) do początkowo
pustego drzewa. Ilu porównań wymaga zbudowanie tego drzewa?
3.2.2. Wstawienie kluczy w kolejności A X C S E R Hdo początkowo pustego drze
wa BST prowadzi do najgorszego przypadku, kiedy to każdy węzeł ma jeden pusty
odnośnik. Wyjątkiem jest węzeł na dole, który ma dwa odnośniki. Podaj pięć innych
kolejności tych kluczy, prowadzących do najgorszego przypadku.
3.2.3. Podaj pięć kolejności kluczy A X C S E R H, które po wstawieniu do począt
kowo pustego drzewa BST prowadzą do najlepszego przypadku.
3.2.4. Załóżmy, że dane drzewo BST ma klucze w postaci liczb całkowitych od 1
do 10, a szukana jest wartość 5. Który z ciągów poniżej nie może być ciągiem spraw
dzanych kluczy?
a. 10, 9, 8 , 7, 6 , 5
b. 4, 10, 8 , 7, 5, 3
c. 1, 10, 2, 9, 3, 8 , 4, 7, 6 , 5
d. 2, 7, 3, 8 , 4, 5
e. 1, 2, 10, 4, 8 , 5
3.2.5. Załóżmy, że z góry oszacowano, jak często potrzebny jest dostęp do poszcze
gólnych kluczy wyszukiwania w drzewie BST, i można wstawić je w dowolnej kolej
ności. Czy klucze należy wstawić w rosnącej lub malejącej kolejności według prawdo
podobieństwa dostępu, czy w innym porządku? Wyjaśnij odpowiedź.
3.2.6. Dodaj do klasy BST metodę hei ght (), która oblicza wysokość drzewa. Opracuj
dwie implementacje — metodę rekurencyjną (ilość czasu i pamięci jest tu proporcjonalna
liniowo do wysokości drzewa) i metodę w rodzaju si ze(), która dodaje pole do każdego
węzła drzewa (ilość pamięci rośnie tu liniowo, a czas na obsługę zapytania jest stały).
3.2.7. Dodaj do klasy BST metodę avgCompares(), która określa średnią liczbę po
równań dla trafienia w danym drzewie BST (ta liczba to długość ścieżki wewnętrznej
drzewa podzielona przez jego rozmiar plus 1). Opracuj dwie implementacje — m e
todę rekurencyjną (ilość czasu i pamięci jest tu proporcjonalna liniowo do wysoko
ści drzewa) i metodę w rodzaju s i ze (), która dodaje pole do każdego węzła drzewa
(ilość pamięci rośnie tu liniowo, a czas na obsługę zapytania jest stały).
3.2.8. Napisz metodę statyczną o p t C o m p a r e s (), która przyjmuje argument w postaci
liczby całkowitej N i określa liczbę porównań dla dowolnego trafienia w optymalnym
(w pełni zbalansowanym) drzewie BST. Jeśli liczba odnośników jest potęgą dwójki,
3.2 Drzewa wyszukiwań binarnych 429
w drzewie wszystkie puste odnośniki znajdują się na tym samym poziomie; jeżeli ta
liczba jest inna, puste odnośniki występują na dwóch poziomach.
3.2.9. Narysuj wszystkie różne kształty drzew BST, które mogą powstać po wstawie
niu Nkluczy do początkowo pustego drzewa. Przyjmij N= 2, 3, 4, 5 i 6.
3.2.10. Napisz klienta testowego [Link] do testowania przedstawionych
w tekście implementacji m etod min(), max(), floor(), cei 1 in g (), s e l e c t (), rank(),
d e le te d , deleteM inQ, deleteMax() i keys(). Zacznij od standardowego klienta
używającego indeksu ze strony 382. W razie potrzeby dodaj kod do obsługi nowych
argumentów wiersza poleceń.
3.2.11. Ile jest kształtów drzew binarnych o N węzłach i wysokości JV? Na ile róż
nych sposobów można wstawić N różnych kluczy do początkowo pustego drzewa
BST, aby uzyskać drzewo o wysokości N? Zobacz ć w i c z e n i e 3 .2 .2 .
3.2.12. Opracuj implementację klasy BST pozbawioną m etod rank() i s e le c t() oraz
pola z liczbą węzłów w obiektach Node.
3.2.13. Przedstaw nierekurencyjne implementacje metod get () i put () dla klasy BST.
Częściowe rozwiązanie. Oto implementacja m etody g et():
public Value get(Key key)
{
Node x = root;
while (x != n u ll)
{
in t cmp = [Link]([Link]);
i f (cmp == 0) return x .v a l;
e lse i f (cmp < 0) x = x . l e f t ;
e lse i f (cmp > 0) x = x . r ig h t ;
}
return nul 1;
}
Implementacja m etody put () jest bardziej skomplikowana, ponieważ trzeba zacho
wać wskaźnik do węzła rodzica w celu dołączenia nowego węzła na dole drzewa.
Ponadto potrzebny jest drugi przebieg w celu sprawdzenia, czy klucz już znajduje się
w tablicy (wynika to z konieczności zaktualizowania pól z liczbą węzłów). Ponieważ
w implementacjach, w których wydajność jest kluczowa, operacji wyszukiwania jest
znacznie więcej niż wstawiania, zastosowanie pokazanego tu kodu m etody get () jest
uzasadnione. Wprowadzenie podobnych modyfikacji w metodzie put () może nie
przynieść odczuwalnych zmian.
afl
430 RO ZD ZIA Ł 3 s W yszukiwanie
ĆW ICZEN IA (ciąg dalszy)
3.2.14. Podaj nierekurencyjne implementacje m etod min(), max(), floor(), c e i-
lin g (), rank() i s e le c t().
3.2.15. Podaj ciąg węzłów sprawdzanych, kiedy metody z klasy BST są używane do
obliczenia każdej z poniższych wartości dla drzewa narysowanego po prawej.
a. floor("Q")
b. se lect(5 )
c. ceiling("Q ")
d. r a n k ("J ")
e. s i z e ( " D " , "T ")
f keys("D\ "T")
3.2.16. Zdefiniujmy długość ścieżki zewnętrznej drzewa jako sumę liczb węzłów
na ścieżkach z korzenia do wszystkich odnośników pustych. Udowodnij, że różnica
między długością ścieżki wewnętrznej i zewnętrznej dla dowolnego drzewa binarne
go o N węzłach wynosi 2N (zobacz t w i e r d z e n i e c ).
3.2.17. Narysuj serię drzew BST, które powstają w czasie usuwania kluczy z drzewa
zćw ic z e n ia 3 . 2.1 zgodnie z kolejnością ich wstawiania.
3.2.1 8 . Narysuj serię drzew BST, które powstają w czasie usuwania kluczy z drzewa
zćw ic z e n ia 3 .2.1 w porządku alfabetycznym.
3.2.19. Narysuj serię drzew BST, które powstają w czasie usuwania kluczy z drzewa
z ć w i c z e n i a 3 .2 .1 przez usuwanie za każdym razem klucza z korzenia.
3.2.20. Udowodnij, że czas wykonania dwuargumentowej metody keys () dla drze
wa BST o N węzłach jest najwyżej proporcjonalny do sumy wysokości drzewa i liczby
kluczy w zakresie.
3.2.21. Dodaj do klasy BST metodę randomKey(), która zwraca losowy klucz z tablicy
symboli w czasie proporcjonalnym do wysokości drzewa (dla najgorszego przypadku).
3.2.22. Udowodnij, że jeśli węzeł w drzewie BST ma dwoje dzieci, to następnik nie
ma lewego dziecka, a poprzednik nie ma prawego dziecka.
3.2.23. Czy metoda d e le te () jest przemienna? Czy usunięcie x, a następnie y daje
ten sam efekt, co usunięcie najpierw y, a następnie x?
3.2.24. Udowodnij, że żaden oparty na porównaniach algorytm nie buduje drzewa
BST za pomocą mniej niż lg(N!) ~ N Ig N porównań.
3.2 s Drzewa wyszukiwań binarnych 431
PROBLEMY DO ROZWIĄZANIA
3.2.25. W pełni zbalansowane drzewa. Napisz program, który do początkowo pu
stego drzewa BST wstawia zbiór kluczy w taki sposób, aby wygenerowane drzewo
umożliwiało wyszukiwanie w sposób analogiczny jak przy wyszukiwaniu binarnym
— ciąg porównań przy wyszukiwaniu dowolnego klucza w drzewie BST ma być taki
sam, jak ciąg porównań przy wyszukiwaniu binarnym tego samego klucza.
3.2.26. Dokładne prawdopodobieństwa. Ustal prawdopodobieństwo, że każde
z drzew z ć w i c z e n i a 3 .2.9 jest wynikiem wstawienia Nlosowych różnych elementów
do początkowo pustego drzewa.
3.2.27. Wykorzystanie pamięci. Porównaj wykorzystanie pamięci przez klasę BST
z wykorzystaniem pamięci przez klasy BinarySearchST i SequentialSearchST dla
N par klucz-wartość przy założeniach z p o d r o z d z i a ł u 1.4 (zobacz ć w i c z e n i e
3 . 1 .2 1 ). Pomiń pamięć na klucze i wartości — uwzględnij tylko pamięć na referen
cje. Narysuj wykres obrazujący dokładne wykorzystanie pamięci przez drzewo BST
o kluczach typu S t r in g i wartościach typu Integer (takie drzewa tworzy program
FrequencyCounter), a następnie oszacuj wykorzystanie pamięci (w bajtach) przez
drzewo BST zbudowane w programie FrequencyCounter dla książki Tale o f Two Cities
za pomocą klasy BST.
3.2.28. Programowa pamięć podręczna. Zmodyfikuj klasę BST, aby przechowywała
ostatnio używany obiekt typu Node w zmiennej egzemplarza, co pozwala na dostęp
do niego w stałym czasie, jeśli metoda put () lub get () użyje tego samego klucza
(zobacz ć w ic z e n ie 3 . 1 .25 ).
3.2.29. Sprawdzanie drzewa binarnego. Napisz rekurencyjną metodę i sBi naryTree (),
która przyjmuje jako argument obiekt typu Node. Metoda ma zwracać true, jeśli pole
z liczbą węzłów (N) poddrzewa jest spójne w strukturze danych, której korzeniem jest
dany węzeł. W przeciwnym razie metoda ma zwracać fal se. Uwaga: ten test gwarantu
je też, że w strukturze danych nie ma cykli, dlatego jest ona drzewem binarnym!
3.2.30. Sprawdzanie uporządkowania. Napisz rekurencyjną metodę isOrdered(),
która przyjmuje jako argumenty obiekt typu Node i dwa klucze, mi n oraz max, i zwraca
true, jeśli, po pierwsze, wszystkie klucze w drzewie mają wartości pomiędzy mi n oraz
max, po drugie, wartości mi n i max to najmniejszy oraz największy klucz drzewa, i po
trzecie, wszystkie klucze drzewa spełniają warunek uporządkowania dla drzew BST.
Jeśli choć jeden warunek nie jest spełniony, metoda ma zwracać fal se.
3.2.31. Sprawdzanie, czy występują identyczne klucze. Napisz metodę hasNoDupli-
cates ( ) . Metoda ma przyjmować jako argument obiekt typu Node i zwracać wartość
true, jeśli w drzewie binarnym, którego korzeniem jest węzeł podany jako argument,
nie istnieją równe sobie klucze. W przeciwnym razie metoda m a zwracać fal se.
Załóżmy, że drzewo przeszło test z poprzedniego ćwiczenia.
432 RO ZD ZIA Ł 3 n W yszukiwanie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
3.2.32. Sprawdzanie, czy struktura to drzewo. Napisz metodę i s BST (). Ma ona przyj
mować jako argument obiekt typu Node i zwracać true, jeśli węzeł podany jako ar
gument jest korzeniem drzewa BST. W przeciwnym razie metoda ma zwracać fal se.
Wskazówka: zadanie to jest trudniejsze, niż może się wydawać, ponieważ kolejność
wywoływania m etod z trzech poprzednich ćwiczeń jest istotna.
Rozwiązanie:
private boolean is B S T ()
{
i f ( ! is B in a ry T re e ( ro o t )) return fa lse ;
i f (!isO rdered(root, min(), max())) return fa ls e ;
i f (!hasNoDuplicates(root)) return fa lse ;
return true;
}
3.2.33. Sprawdzanie metod s e le c t() i rank(). Napisz metodę, która sprawdza
dla wszystkich i od 0 do s iz e ( ) - l, czy i jest równe ra n k (s e le c t( i) ). Ponadto m e
toda dla wszystkich kluczy drzewa BST ma sprawdzać, czy klucz key jest równy
select(ran k (k ey )).
3.2.34. Wątki. Cel to dodanie obsługi rozbudowanego interfejsu API ThreadedST,
tak aby można wykonać w stałym czasie dodatkowe operacje:
Key next (Key key) Zwraca klucz następujący p o k e y (nu}], jeśli key to m aksimum)
Key prev(Key key) Zwraca klucz poprzedzający key (n ull, jeśli key to m inim um )
Wymaga to dodania do obiektu Node pól pred i suce, obejmujących odnośniki do
węzłów poprzednika oraz następnika, i zmodyfikowania m etod put (), del eteMi n (),
deleteMax() id e le te () tak, aby zachowywały poprawność tych pól.
3.2.35. Dokładniejsza analiza. Doprecyzuj model matematyczny, aby lepiej wyjaśnić
wyniki eksperymentów z tabeli przedstawionej w tekście. Wykaż, że średnia liczba
porównań przy udanym wyszukiwaniu w drzewie zbudowanym z losowych kluczy
zbliża się do granicy 2 In N + 2y - 3 = 1,39 lg N - 1,85 wraz z rosnącym N (y to stała
Eulera równa 0,57721...). Wskazówka: nawiązując do analizy sortowania szybkiego
( p o d r o z d z i a ł 2 .3 ), wykorzystaj to, że całka z l/x dąży do In N + y.
3.2.36. Iterator. Czy m ożna napisać nierekurencyjną wersję metody keys(), która
wymaga pamięci w ilości proporcjonalnej do wysokości drzewa (niezależnie od licz
by kluczy w przedziale)?
3.2 □ Drzewa wyszukiwań binarnych 433
3.2.37. Przechodzenie według poziomów. Napisz metodę pri ntLevel (), która przyj
muje jako argument obiekt typu Node i wyświetla według poziomów (według odle
głości od korzenia, przy czym węzły z danego poziomu wyświetlane są od lewej do
prawej) klucze z poddrzewa o korzeniu w danym węźle. Wskazówka: użyj obiektu
typu Queue.
3.2.38. Rysowanie drzewa. Dodaj do klasy BST metodę draw(), która rysuje drzewa
BST podobne do tych przedstawionych w tekście. Wskazówka: użyj zmiennych eg
zemplarza do przechowywania współrzędnych węzłów i m etody rekurencyjnej do
ustawiania wartości tych zmiennych.
434 RO ZD ZIA Ł 3 Q W yszukiw anie
| EKSPERYMENTY
3.2.39. Typowy przypadek. Przeprowadź empiryczne badania, aby oszacować śred
nią i odchylenie standardowe liczby porównań dla udanego oraz nieudanego wyszu
kiwania w drzewie BST. Wykonaj 100 prób eksperymentu w postaci wstawiania N
losowych kluczy do początkowo pustego drzewa. Użyj N = 104, 105 i 106. Porównaj
wyniki ze wzorem na średnią przedstawionym w ć w i c z e n i u 3 .2 .3 5 .
3.2.40. Wysokość. Przeprowadź empiryczne badania, aby oszacować średnią wyso
kość drzewa BST przez uruchomienie 100 prób eksperymentu w postaci wstawiania
N losowych kluczy do początkowo pustego drzewa. Użyj N = 104, 105 i 106. Porównaj
wyniki z szacunkową wartością 2,99 lg N przedstawioną w tekście.
3.2.41. Reprezentacja tablicowa. Opracuj implementację drzewa BST, w której drze
wo reprezentowane jest za pom ocą trzech tablic (tworzonych na podstawie maksy
malnej wielkości podanej w konstruktorze). Jedna tablica ma obejmować klucze,
druga — indeksy odpowiadające lewym odnośnikom, a trzecia — indeksy odpowia
dające prawym odnośnikom. Porównaj wydajność tego program u i standardowej
implementacji.
3.2.42. Spadek wydajności przy usuwaniu metodą Hibbarda. Napisz program, który
pobiera z wiersza poleceń liczbę całkowitą N, buduje losowe drzewo BST o wielko
ści N, a następnie wchodzi w pętlę, w której usuwa losowy klucz (używając kodu
delete(sel ect ([Link] form(N)))) i wstawia losowy klucz. Pętla powtarzana
jest N 2 razy. Po pętli zmierz i wyświetl średnią długość ścieżki w drzewie (długość
ścieżki wewnętrznej podzieloną przez N plus 1). Uruchom program dla N = 102, 103
i 10 4, aby przetestować nieco sprzeczną z intuicją hipotezę, zgodnie z którą proces
ten zwiększa średnią długość ścieżki tak, że staje się proporcjonalna do pierwiastka
kwadratowego z N. Przeprowadź ten sam eksperyment dla implementacji metody
delete(), w której losowo wybierany jest węzeł poprzednika lub następnika.
3.2.43. Stosunek czasu wykonywania m etodput() iget(). Ustal empirycznie stosunek
czasu, przez jaki klasa BST wykonuje operacje put (), do czasu wykonywania operacji
get () przy korzystaniu z program u FrequencyCounter do określania liczby wystąpień
wartości w milionie losowo wygenerowanych liczb całkowitych.
3.2.44. Wykresy kosztów. Rozbuduj klasę BST tak, aby umożliwiała tworzenie wykre
sów takich jak pokazane w tym podrozdziale, przedstawiających koszt każdej opera
cji put () w trakcie obliczeń (zobacz ć w i c z e n i e 3 .1 .3 8 ).
3.2 ■ Drzewa wyszukiwań binarnych 435
3.2.45. Czas rzeczywisty. Rozbuduj program FrequencyCounter przez zastosowa
nie Idas Stopwatch i StdDraw do utworzenia wykresu, na którym oś x reprezentuje
liczbę wywołań m etody get() lub put (), a oś y — łączny czas wykonania (po każ
dym wywołaniu należy dodać punkt na podstawie skumulowanego czasu). Uruchom
program dla pliku z książką Tale o f Two Cities, używając klasy Sequential SearchST,
następnie klasy Bi narySearchST, a ostatecznie klasy BST. Omów wyniki. Uwaga: duże
zmiany na krzywej m ożna wyjaśnić buforowaniem; omawianie tej kwestii wykracza
poza zakres pytania (zobacz ć w i c z e n i e 3 .1 .39 ).
3.2.46. Przejście na drzewa wyszukiwań binarnych. Znajdź wartości N, dla których
zastosowanie drzewa wyszukiwań binarnych do zbudowania tablicy symboli o N loso
wych kluczach typu doubl e jest 10, 100 i 1000 razy szybsze niż przy wyszukiwaniu bi
narnym. Przedstaw prognozy na podstawie analiz i zweryfikuj je eksperymentalnie.
3.2.47. Średni czas wyszukiwania. Przeprowadź badania empiryczne, aby obliczyć
średnią i odchylenie standardowe średniej długości ścieżki do losowego węzła (jest to
długość ścieżki wewnętrznej podzielona przez wielkość drzewa plus jeden) w drze
wach BST zbudowanych przez wstawienie N losowych kluczy do początkowo puste
go drzewa. Przyjmij N od 100 do 10 000. Wykonaj 1000 prób dla każdej wielkości
drzewa. Przedstaw wyniki na wykresie Tuftea, takim jak w dolnej części strony, wraz
z krzywą dla funkcji 1,39 lg N - 1,85 (zobacz ć w i c z e n i a 3 .2.35 i 3 .2 .3 9 ).
Średnia długość ścieżki do losowego węzła w drzewach BST zbudowanych z losowych kluczy
3.3. Z B A L A N S O W A N E D R Z E W A W Y S Z U K IW A Ń
Algorytmy z poprzedniego podrozdziału działają dobrze w różnorodnych sytua
cjach, jednak mają niską wydajność dla najgorszego przypadku. W tym podrozdziale
przedstawiamy rodzaj binarnych drzew wyszukiwań, który gwarantuje logarytmiczny
poziom kosztów niezależnie od ciągu kluczy użytego do utworzenia drzewa. W ide
alnych warunkach binarne drzewo wyszukiwań powinno być w pełni zbalansowane.
Drzewo o N węzłach powinno mieć wysokość ~lg N, co pozwala zagwarantować, że
dowolne wyszukiwanie będzie wymagać ~lg N porównań, tak jak w wyszukiwaniu
binarnym (zobacz t w i e r d z e n i e b ) . Niestety, utrzymywanie w pełni zbalansowane-
go drzewa przy dynamicznym wstawianiu jest zbyt kosztowne. W tym podrozdziale
omawiamy strukturę danych, w której nieco rozluźniono wymóg pełnego zbalanso-
wania, aby zagwarantować logarytmiczną wydajność nie tylko operacji wstawiania
i wyszukiwania z interfejsu API dla tablicy symboli, ale też wszystkich operacji na
D r z e w a w y s z u k iw a ń 2 -3 Podstawowy krok, który pozwala osiągnąć elastycz
ność potrzebną do zagwarantowania zbalansowania drzewa wyszukiwań, związany
jest z umożliwieniem przechowywania w węzłach drzewa więcej niż jednego klucza.
Węzły w standardowym drzewie BST są podwójne (przechowują dwa odnośniki i je
den klucz), natomiast tu umożliwiamy tworzenie węzłów potrójnych (obejmujących
trzy odnośniki i dwa klucze). Zarówno wersja podwójna, jak i potrójna posiada jeden
odnośnik do każdego z przedziałów wyznaczanych przez klucze.
Definicja. Drzewo wyszukiwań 2-3 to drzewo, które jest albo puste, albo jest:
■ węzłem podwójnym — o jednym kluczu (i powiązanej wartości) oraz dwóch
odnośnikach; lewy prowadzi do drzewa wyszukiwań 2-3 z mniejszymi klucza
mi, a prawy — do drzewa wyszukiwań 2-3 z większymi kluczami;
■ węzłem potrójnym — o dwóch kluczach (i powiązanych wartościach) oraz
trzech odnośnikach; lewy prowadzi do drzewa wyszukiwań 2-3 z mniejszy
mi kluczami, środkowy do drzewa wyszukiwań 2-3 z kluczami o wartościach
pomiędzy wartościami kluczy z węzła, a prawy — do drzewa wyszukiwań 2-3
z większymi kluczami.
Jak zwykle odnośnik do pustego drzewa nazywamy odnośnikiem pustym.
Węzeł W pełni zbalansowane drzewo wyszukiwań 2-3 ma wszyst
potrójny Węzeł podwójny
kie puste odnośnild w takiej samej odległości od korzenia.
Aby zachować zwięzłość, nazwy drzewo 2-3 używamy do
określania w pełni zbalansowanego drzewa wyszukiwań 2-3
(w innych kontekstach nazwa ta oznacza bardziej ogólną
Pusty odnośnik strukturę). Dalej pokazujemy wydajne sposoby definiowania
S tru k tu ra d rz e w a w y szu k iw ań 2-3 i implementowania podstawowych operacji na węzłach po-
436
3.3 Q Zbalansowane drzewa wyszukiwań 437
Udane w yszukiw anie H Nieudane wyszukiwanie B
Hjest mniejsze niż M, dlatego Bjest mniejsze niż M, dlatego
należy szukać po lewej ' należy szukać po lewej
Hznajduje się Bjest mniejsze
między E /' J, niż E, dlatego
dlatego należy należy szukać
RJ szukać pośrodku po lewej
( E 3 i R) ( E 2,
Ca 0D (P) Cs x )
r\ r \
t
Znaleziono H, dlatego należy B ma wartość pomiędzy A / c, dlatego należy szukać pośrodku.
zwrócić wartość (trafienie) Odnośnik jest pusty, więc B nie znajduje się w drzewie (chybienie)
Trafienie (po lewej) i chybienie (po prawej) w drzewie 2-3
dwójnych i potrójnych oraz drzewach 2-3. Na razie załóżmy, że można wygodnie m ani
pulować takimi drzewami, i zobaczmy, jak zastosować je jako drzewa wyszukiwań.
W yszukiwanie Algorytm wyszukiwania kluczy w drzewach 2-3 to bezpośrednie
uogólnienie algorytmu wyszukiwania w drzewach BST. Aby ustalić, czy klucz znajduje
się w drzewie, należy najpierw porównać go z kluczami w korzeniu. Jeśli jest równy jed
nemu z nich, lducz znaleziono. W przeciwnym razie należy podążyć za odnośnikiem
z korzenia do poddrzewa odpowiadającego przedziałowi wartości klucza, w którym
może znajdować się klucz wyszukiwania. Jeśli ten odnośnik jest pusty, wyszukiwanie
jest nieudane. W przeciwnym razie należy rekurencyjnie przeszukać dane poddrzewo.
W stawianie do węzła podwójnego Aby wsta
wić nowy węzeł w drzewie 2-3, można wyko
nać nieudane wyszukiwanie, a następnie do
dać wartość na dole drzewa, tak jak robiono to
w drzewach BST. Jednak wtedy nowe drzewo
przestaje być w pełni zbalansowane. Głównym
powodem przydatności drzew 2-3 jest to, że
można wstawiać dane i zachować pełne zba-
lansowanie. Łatwo zrealizować to zadanie, jeśli
węzeł, w którym wyszukiwanie się kończy, jest
podwójny. Wystarczy zastąpić ten węzeł wę
Zastępowanie węzła podwójnego nowym złem potrójnym, zawierającym dawny klucz
węzłem potrójnym zawierającym K
i klucz wstawiany. Jeżeli wyszukiwanie kończy
Wstawianie do węzła podwójnego się w węźle potrójnym, potrzeba więcej pracy.
438 RO ZD ZIA Ł 3 n W yszukiw anie
W stawianie do drzewa składającego się z jednego węzła potrójnego W ramach
pierwszej rozgrzewki, przed rozważeniem ogólnego przypadku, załóżmy, że chcemy
wstawić element do małego drzewa 2-3, składającego się z jednego węzła potrójnego.
Takie drzewo obejmuje dwa klucze, a w jedynym węźle nie ma miejsca na nowy klucz.
Aby móc wstawić element, należy tymczasowo umieścić nowy klucz w węźle poczwór
nym, który jest naturalnym rozwinięciem węzła, mającym trzy klucze i cztery odnośni
ki. Utworzenie węzła poczwórnego jest wygodne, ponieważ można łatwo przekształcić
go na drzewo 2-3 składające się z trzech węzłów podwójnych — jednego dla klucza
środkowego (w korzeniu), jednego z najmniejszym z trzech kluczy (wskazuje na niego
lewy odnośnik korzenia) i jednego z największym Wstawianie S
z trzech kluczy (prowadzi do niego prawy odnoś C a ^Ę ) -i— Brak miejsca na S
nik korzenia). Jest to drzewo BST o trzech węzłach,
a jednocześnie w pełni zbalansowane drzewo wy s ——z- tn
( a e sj
Tworzenie węzła
y— i i C poczwórnego
szukiwań 2-3, w którym wszystkie puste odnośniki
Podział węzła
są tak samo oddalone od korzenia. Przed wstawia
poczwórnego
niem wysokość drzewa wynosi 0, a po wstawianiu na drzewo 2-3
— 1. Ten przypadek jest prosty, jednak warto się
nad nim zastanowić, ponieważ ilustruje powięk- Wstawianie do jednego węzła potrójnego
szanie wysokości drzew 2-3.
W stawianie do węzła potrójnego, którego rodzicem je st węzeł podw ójny W drugim
ćwiczeniu wstępnym załóżmy, że wyszukiwanie kończy się w węźle potrójnym, którego
rodzicem jest węzeł podwójny. Wtedy można zrobić miejsce na nowy klucz, zachowu
jąc przy tym pełne zbalansowanie drzewa. Wymaga to utworzenia tymczasowego węzła
poczwórnego, jak opisano wcześniej, jednak potem — zamiast tworzyć nowy węzeł na
środkowy klucz — należy przenieść środkowy klucz do rodzica węzła. Można trak
Wstawianie Z
tować to jak zastąpienie w rodzi
cu odnośnika do dawnego węzła
Wyszukiwanie z kończy się potrójnego odnośnikami po obu
J w tym węźle potrójnym stronach do nowych węzłów po
( A C J ( h) i L ) ( p ) ( s X dwójnych. Zgodnie z założeniem
m i M AA / \ W
w rodzicu dostępne jest miejsce.
Zastępowanie węzta potrójnego
tymczasowym węzłem
Rodzic był węzłem podwójnym
poczwórnym zawierającym Z (o jednym kluczu i dwóch odnoś
/ nikach), a staje się węzłem potrój
( a cl (h) (l) (p) ( s X z) nym (o dwóch kluczach i trzech
/ i \ r\ r\ a~\ > i \ <
odnośnikach). Transformacja nie
Zastępowanie węzła podwójnego wpływa na cechy (w pełni zbalan-
nowym węzłem potrójnym sowanego) drzewa 2-3. Drzewo
f zawierającym środkowy klucz
t, E J i ( R pozostaje uporządkowane, po
_r
( a c ) ( h ) ( l ) ( p) v nieważ środkowy klucz trafia do
Y t ~ś n a n a
rodzica, a także pozostaje w peł
Podział węzła poczwórnego na dwa węzły podwójne.
ni zbalansowane — jeśli przed
Środkowy klucz należy przenieść do rodzica
wstawianiem wszystkie odnośniki
Wstawianie do węzła potrójnego, którego rodzicem jest węzeł podwójny
3.3 o Zbalansow ane drzewa wyszukiwań 439
puste są w takiej samej odległości od korzenia, Wstawianie D
jest to prawdą także po wstawieniu elementu. Wyszukiwanie d
Upewnij się, że rozumiesz tę transformację. Jest kończy się w tym
węźle potrójnym f
ona istotą funkcjonowania drzew 2-3.
W staw ianie do w ęzła potrójnego, którego
Dodawanie nowego klucza D do
rodzicem je st w ęzeł potrójny Teraz załóżmy, węzta potrójnego, przez co powstaje
że wyszukiwanie kończy się w węźle, którego tymczasowy węzeł poczwórny
rodzicem jest węzeł potrójny. Także tu two
rzymy w opisany sposób tymczasowy węzeł
poczwórny, następnie dzielimy go i wstawia
my środkowy klucz do rodzica. Rodzic był Dodawanie klucza środkowego c do węzia potrójnego,
przez co powstaje tymczasowy węzeł poczwórny
węzłem potrójnym, dlatego należy zastąpić go
( m)
tymczasowym nowym węzłem poczwórnym,
zawierającym środkowy klucz z podziału
węzła poczwórnego. Następnie wykonujemy
dokładnie te same transformacje na nowym
Podział węzła poczwórnego na dwa węzły podwójne.
węźle. Dzielimy więc nowy węzeł poczwórny Środkowy klucz należy przenieść do rodzica
i wstawiamy jego środkowy klucz do jego ro
Dodawanie środkowego klucza
dzica. Rozwinięcie tego ogólnego przypadku E do węzła podwójnego;
jest oczywiste — należy poruszać się w górę powstaje nowy węzeł potrójny
drzewa, dzieląc węzły poczwórne i wstawiając
Wstawianie D
Wyszukiwanie d
kończy się w tym , Podział węzła poczwórnego na dwa węzły podwójne.
węźle potrójnym \ Środkowy klucz należy przenieść do rodzica
Wstawianie do węzła potrójnego,
którego rodzicem jest węzeł potrójny
Dodawanie nowego klucza D do węzła potrójnego,
przez co powstaje tymczasowy węzeł poczwórny
E 3 ich środkowe klucze do rodziców do
f L) m om entu natrafienia na węzeł podwój
ri ny (zastępujemy go węzłem potrójnym,
Dodawanie środkowego klucza c do węzła potrójnego, którego nie trzeba dalej dzielić) lub na
przez co powstaje tymczasowy węzeł poczwórny
węzeł potrójny będący korzeniem.
\
P odział korzenia Jeśli węzły potrójne
(
wa J (
wd ) (h) T l ) znajdują się na całej ścieżce od punk
\ / tu wstawiania do korzenia, ostatecznie
Podział węzła poczwórnego na dwa węzły podwójne.
Środkowy klucz należy przenieść do rodzica powstaje węzeł poczwórny w korzeniu.
Podział węzła poczwórnego Wtedy można postąpić tak samo, jak
na trzy węzły podwójne, przy wstawianiu do węzła składającego
co powoduje zwiększenie
wysokości drzewa o 1
się z jednego węzła potrójnego. Należy
podzielić tymczasowy węzeł poczwór
ny na trzy węzły podwójne, zwiększając
Podział korzenia
440 R O ZD ZIA Ł 3 n W yszukiw anie
w ten sposób wysokość
drzewa o 1. Warto zauwa
żyć, że ostatnia transfor
macja pozwala zachować
/ \ / \ / \ / \ / \ / \
/ Mniejsze\ / Między\ / M iędzy\ / M iędzy\ /M iędzy', j Większe \
pełne zbalansowanie drze
V niż a ) ( a i b ! ( b / c ) ( c /' d ) ( d i e ) \ niże
wa, ponieważ jest wykony T r - r - f /T-rrr-i / r- T —\ n ~ : ~ \ Jrrrm
wana w korzeniu.
Transformacje lokalne Po
dział tymczasowego węzła
poczwórnego na drzewo
/ \ / \ /• \ / \ / ^
2-3 obejmuje jedną z sześ 1 Mniejsze \ / Między'. / Między \ / Między\ / Między / W . iększe\
niż a ) ( a / b ) ( b i c ) ( c i d ) ( d /'e ) V niz e
ciu transformacji podsu I i-rrr-\ ¡[ -i jr r- r-\ rn -T -r-f n .. ■ -\
mowanych w dolnej części Podział węzła poczwórnego to lokalna transformacja
następnej strony. Węzeł zachowująca kolejność i pełne zbalansowanie
poczwórny może być ko
rzeniem. Może być lewym lub prawym dzieckiem węzła podwójnego. Może też być
lewym, środkowym lub prawym dzieckiem węzła potrójnego. Podstawą algorytmu
wstawiania do drzewa 2-3 jest to, że wszystkie transformacje są w pełni lokalne. Nie
trzeba sprawdzać ani modyfikować żadnej części drzewa oprócz określonych węzłów
i odnośników. Liczba odnośników zmienianych w każdej transformacji jest ograni
czona małą stałą. Transformacje są skuteczne, jeśli określony wzorzec wystąpi w do
wolnym miejscu drzewa — nie musi to być jego dół. Każda z transformacji powoduje
przeniesienie jednego z kluczy z węzła poczwórnego do rodzica tego węzła w drze
wie, a następnie odpowiednią zmianę struktury odnośników. Inne części drzewa nie
są przy tym naruszane.
W łaściwości globalne Omawiane transformacje lokalne zapewniają zachowanie
właściwości globalnych, czyli tego, że drzewo jest uporządkowane i w pełni zbalan-
sowane. Liczba odnośników na ścieżce od korzenia do dowolnego pustego odnoś
nika pozostaje taka sama. Powyżej pokazano kompletny diagram ilustrujący to dla
węzła poczwórnego, który jest środkowym dzieckiem węzła potrójnego. Jeśli przed
transformacją długość każdej ścieżki z korzenia do odnośnika pustego wynosi h, po
transformacji wartość ta się nie zmienia. Każda transformacja zachowuje tę właści
wość, nawet przy rozbiciu węzła poczwórnego na dwa węzły podwójne, przy zmianie
rodzica z węzła podwójnego na węzeł potrójny i przy zmianie węzła potrójnego na
tymczasowy węzeł poczwórny. Kiedy korzeń rozbijany jest na trzy węzły podwójne,
długość każdej ścieżki z korzenia do odnośnika pustego rośnie o 1. Jeśli nie jesteś do
końca przekonany o zachowaniu właściwości, wykonaj ć w i c z e n i e 3 .3 .7 , polegające
na rozwijaniu diagramów z górnej części poprzedniej strony dla pięciu pozostałych
przypadków. Zrozumienie tego, że każda transformacja lokalna zapewnia zachowa
nie kolejności i pełnego zbalansowania w całym drzewie, jest kluczem do zrozumie
nia omawianego algorytmu.
3.3 o Zbalansow ane drzewa wyszukiwań 441
Korzeń Rodzic to w ęzeł p otrójny
^ “A $
b d e
Rodzic to węzeł podwójny
L ew a - Xa c e X
- c t h >
P raw a
P r a w a z a b d
/
~ k
P o d z ia ł ty m c z a s o w e g o w ę z ła p o c z w ó r n e g o n a d rz e w o 2-3 (p o d s u m o w a n ie )
i n a c z e j n i ż s t a n d a r d o w e d r z e w a b s t , które rosną od góry w dół, drzewa 2-3 ros
ną od dołu w górę. Jeśli poświęcisz czas na staranne przeanalizowanie rysunku na na
stępnej stronie, gdzie pokazano ciąg drzew 2-3 generowanych przez standardowego
klienta testowego używającego indeksu i ciąg drzew 2-3 tworzonych przy wstawianiu
tych samych kluczy w porządku rosnącym, dobrze zrozumiesz sposób budowania
drzew 2-3. Przypomnijmy, że w drzewach BST wstawianie 10 kluczy w kolejności
rosnącej prowadziło do najgorszego przypadku — drzewa o wysokości 9. W drze
wach 2-3 ta wysokość to 2.
Wcześniejszy opis wystarcza do zdefiniowania implementacji tablicy symboli op
artej na drzewach 2-3. Analiza drzew 2-3 przebiega inaczej niż drzew BST, ponieważ
tu najważniejsza jest wydajność dla najgorszego przypadku, a nie dla typowego (kiedy
to wydajność badano na podstawie m odelu kluczy losowych). W implementacjach
tablic symboli zwykle nie można kontrolować kolejności, w jakiej klienty wstawiają
klucze do tablicy. Analiza najgorszego przypadku to jeden ze sposobów na zapewnie
nie gwarancji wydajności.
Twierdzenie F. Można zagwarantować, że operacje wyszukiwania i wstawiania
w drzewach 2-3 o N kluczach wymagają sprawdzenia najwyżej lg N węzłów.
Dowód. Wysokość drzewa 2-3 o N węzłach wynosi pomiędzy Llog 3 A/J = L(lg
N )/( lg 3)J (jeśli drzewo składa się z samych węzłów potrójnych) a Lig N_J (jeżeli
drzewo obejmuje same węzły podwójne). Zobacz ć w i c z e n i e 3 .3 .4 .
442 R O ZD ZIA Ł 3 a W yszukiwanie
Wstawianie S Wstawianie A
C A ) (S)
Gl j D
{ A C ) ( H ^M ) d l x )
>~t A Ca} ( e) CO / A 1
CO
d v . ^
i a ) ( e ) ( l) p Cs )
n n
a c)(h O ( p) Cs x ) (A ) (£ } (L ) ( p) ( s x '
nrK W \ / \ / /~ \ / A >-\ /O >—r~<
Standardowy klient używający indeksu Te same klucze wstawione w kolejności rosnącej
Ślady procesu tw orzenia drzew 2-3
3.3 o Zbalansow ane drzewa wyszukiwań 443
Drzewa 2-3 umożliwiają więc zagwarantowanie wysokiej wydajności dla najgorsze
go przypadku. Ilość czasu potrzebnego w każdym węźle na wykonanie poszczegól
nych operacji jest ograniczona stałą, a obie operacje sprawdzają węzły na tylko jed
nej ścieżce, tak więc m ożna zagwarantować, że łączny koszt każdego wyszukiwania
lub wstawiania będzie logarytmiczny. Przez porównanie drzewa 2-3 z dolnej części
strony 443 z drzewem BST utworzonym na podstawie tych samych kluczy (strona
417) można stwierdzić, że w pełni zbalansowane drzewo 2-3 m a niezwykle płaską
strukturę. Przykładowo, wysokość drzewa 2-3 zawierającego miliard kluczy wynosi
między 19 a 30. To zdumiewające, że m ożna zagwarantować, iż dowolne operacje
wyszukiwania i wstawiania dla miliarda kluczy będą wymagać sprawdzenia maksy
malnie 30 węzłów.
To jednak jeszcze nie koniec drogi do implementacji. Choć można napisać kod wy
konujący transformacje na różnych typach danych reprezentujących węzły podwójne
i potrójne, większość opisanych zadań jest niewygodna do zaimplementowania za
pomocą takiej bezpośredniej reprezentacji, ponieważ trzeba obsłużyć wiele różnych
przypadków. Konieczne jest przechowywanie dwóch różnych rodzajów węzłów, po
równywanie kluczy wyszukiwania z każdym z kluczy węzła, kopiowanie odnośni
ków i innych informacji z węzła jednego typu do innego, przekształcanie węzłów
z jednego typu na inny itd. Nie tylko wymaga to dużo kodu, ale też powoduje koszty
ogólne, które mogą sprawić, że algorytmy będą działały wolniej niż wyszukiwanie
i wstawianie w standardowych drzewach BST. Głównym celem zbalansowania jest
zabezpieczenie się przed najgorszym przypadkiem, jednak wolelibyśmy, aby koszty
tego zabezpieczenia były niskie. Na szczęście, jak się okaże, m ożna przeprowadzić
transformacje w jednolity sposób i przy niskich kosztach ogólnych.
\mmm / n A A A M M
Typowe drzewo 2-3 zbudowane na podstawie losowych kluczy
444 RO ZD ZIA Ł 3 o W yszukiwanie
C z e r w o n o -c z a r n e d r z e w a B S T Opisany algorytm wstawiania do drzew 2-3
nietrudno zrozumieć. Tu pokazujemy, że także jego implementowanie nie jest skom
plikowane. Omawiamy prostą reprezentację — czerwono-czarne drzewa BST — która
prowadzi do naturalnej implementacji. Ostatecznie potrzeba niewiele kodu, jednak
zrozumienie tego, jak i dlaczego kod wykonuje zadanie, wymaga zastanowienia się.
Z apisyw anie węzłów potrójnych Podstawowy węzeł potrójny
pomysł, na którym oparto czerwono-czarne drze
wa BST, polega na zapisaniu drzewa 2-3 na pod / Mniejszy\ / Między \ / Większy\
stawie standardowych drzew BST (składających ( niż a ; 1 a/b ) v n‘ż b /
li \ li ... 1 // . . . \
się z węzłów podwójnych) i dodaniu informacji
potrzebnych do zapisania węzłów potrójnych.
Są wtedy dwa rodzaje odnośników — czerwone,
, x v n iż b )
łączące dwa węzły podwójne reprezentujące wę Między \ \
aib )
zeł potrójny, i czarne, które scalają całe drzewo
2-3. Węzły potrójne przedstawiane są jako dwa Zapisywanie węzła potrójnego za pomocą
węzły podwójne połączone jednym odnośnikiem dwoch węzłów podwójnych połączonych
1 ' r j l c j j czerwonym odnośnikiem z lewej strony
czerwonym po lewej stronie (jeden z węzłów po
dwójnych jest lewym dzieckiem drugiego). Jedną z zalet takiej reprezentacji jest to,
że umożliwia użycie kodu m etody get () dla standardowych drzew BST bez modyfi
kowania go. Dla dowolnego drzewa 2-3 można natychmiast utworzyć odpowiadające
m u drzewo BST, przekształcając każdy węzeł w określony sposób. Drzewa BST repre
zentujące drzewa 2-3 nazywamy czerwono-czarnymi drzewami BST.
R ów now ażna definicja Inny sposób to zdefiniowanie czerwono-czarnego drzewa
BST jako drzewa BST z czerwonymi i czarnymi odnośnikami, spełniającego trzy p o
niższe warunki:
■ Odnośniki czerwone znajdują się po lewej stronie.
■ Żaden węzeł nie jest powiązany z dwoma odnośnikam i czerwonymi.
■ Drzewo jest w pełni zbalansowane ze względu na czarne odnośniki — każda
ścieżka z korzenia do pustego odnośnika obejmuje tę samą liczbę czarnych od
nośników.
Między czerwono-czarnymi drzewami BST zdefiniowanymi w ten sposób a drzewa
mi 2-3 występuje zależność 1 do 1.
Zależność 1 do 1 Jeśli czerwone odnośniki w czerwono-czarnym drzewie BST nary
sujemy poziomo, wszystkie puste odnośniki będą znajdować się w tej samej odległości
od korzenia. Jeżeli następnie złączymy węzły powiązane czerwonymi odnośnikami,
powstanie drzewo 2-3. Po narysowaniu węzłów potrójnych drzewa 2-3 jako dwóch wę-
Czerwono-czarne drzewo z poziomymi czerwonymi odnośnikami to drzewo 2-3
3.3 0 ¿balan sow an e drzewa wyszukiwań 445
złów podwójnych połączonych czerwonym
odnośnikiem po lewej stronie żaden węzeł nie
będzie miał dwóch czerwonych odnośników,
a drzewo będzie w pełni zbalansowane według
czarnych odnośników, ponieważ odpowiadają
one odnośnikom z drzewa 2-3, które z definicji
jest w pełni zbalansowane. Niezależnie od wy Poziome odnośniki czerwone
branej definicji czerwono-czarne drzewa BST
są zarówno drzewami BST, jak i drzewami 2-3.
Dlatego jeśli można zaimplementować algo
rytm wstawiania do drzewa 2-3 z zachowaniem
zależności 1 do 1 , można wykorzystać najlepsze
cechy obu struktur — prostą i wydajną metodę
wyszukiwania w standardowych drzewach BST
oraz wydajną metodę wstawiania z balansowa
niem dla drzew 2-3.
Zależność 1 do 1 między czerwono-czarnymi
Reprezentacja kolorów Dla wygody (ponie drzewami BST a drzewami 2-3
waż do każdego węzła prowadzi dokładnie je
den odnośnik — z jego rodzica)
kolory odnośników zapisujemy h . l e f t . c o l o r ma
h. r i g h t , c o l o r ma
w węzłach, przez dodanie do typu wartość RED (czerwony) \ S ' wartość BLACK (czarny)
danych Node zmiennej egzempla
rza c o lo r typu boolean. Zmienna
ma wartość true, jeśli odnośnik p r i v a t e s t a t i c f i n a l bo o le an r e d = tru e ;
od rodzica jest czerwony, oraz p r i v a t e s t a t i c f i n a l bo o le an b l a c k = f a l s e ;
wartość f a l se, jeżeli jest on czar p r i v a t e c l a s s Node
ny. Przyjęto, że odnośniki n u li są {
Key key; // K lu c z
czarne. Aby zwiększyć przejrzy
V a l ue v a l ; / / p o w i ą z a n e da ne
stość kodu, zdefiniowano stałe Node l e f t , r i g h t ; / / P o d d r z e w a
i nt N ; // L i c z b a w ę z ł ó w w p o d d r z e w i e
RED i BLACK używane do ustawia
bo o le an c o lo r ; // K o l o r o d n o ś n i k a z
nia oraz sprawdzania zmiennej. / / r o d z i c a do t e g o w ę z ł a
Metoda prywatna i sRed () służy
N o d e ( K e y k e y , v a l u e v a l , i n t N, b o o l e a n c o l o r )
do sprawdzania koloru odnośnika {
między węzłem a rodzicem. Przy th is.k e y = key;
t h i s .v a l = v a l;
określaniu koloru węzła ważny jest thi s .N = N;
prowadzący do niego odnośnik. t h i s . c o l o r = co lo r;
}
Rotacje W omawianej imple- }
mentacj i mogą wystąpić czerwone p riv a te bo o le an isR e d (N o d e x)
odnośniki po prawej stronie lub {
i f (x == n u l l ) r e t u r n f a l s e ;
dwa czerwone odnośniki z rzędu r e t u r n x . c o l o r = = RED;
w jednej operacji, jednak metody }
przed zakończeniem działania Reprezentacja węzła dla czerwono-czarnych drzew BST
446 R O ZD ZIA Ł 3 o W yszukiw anie
Może być prawy lub lewy zawsze rozwiązują te problemy przez odpowiednie ro
oraz czerwony lub czarny tacje. Rotacja zmienia położenie czerwonych odnośni
ków. Najpierw załóżmy, że istnieje czerwony odnośnik
po prawej stronie i trzeba go zrotować, aby znalazł się
, Mniejszy
po lewej (zobacz rysunek po lewej stronie). Ta operacja
z) // Między
\ \ // Większy
\ \ to rotacja w lewo. Przetwarzanie umieszczono w m e
1 /S J V niżs ) todzie, która przyjmuje jako argument odnośnik do
Node r o t a t e l _ e f t ( N o d e h) czerwono-czarnego drzewa BST i — przy założeniu,
{ że odnośnik prowadzi do obiektu h typu Node, którego
Node x = h . r i g h t ;
h. r i g h t = x . ) e f t ; prawy odnośnik jest czerwony — wprowadza niezbęd
x .1e f t = h ;
ne zmiany, po czym zwraca odnośnik do węzła będące
x .c o !o r = h .co lo r;
h . c o l o r = RED; go korzeniem czerwono-czarnego drzewa BST dla tego
x .N = h .N ;
h.N = 1 + s i z e ( h . l e f t )
samego zbioru kluczy, w którym lewy odnośnik jest
+ size (h . r i g h t ) ; czerwony. Jeśli sprawdzisz każdy wiersz kodu wzglę
r e t u r n x;
} x dem rysunków przed i po, zobaczysz, że operację łatwo
jest zrozumieć. Kod umieszcza w korzeniu większy za
miast mniejszego z dwóch kluczy. Implementacja rota
cji w prawo, która przekształca lewy czerwony odnoś
/ Większy\
nik w prawy, to ten sam kod z zamienionymi stronami
/ Mniejszy', / M ię d z y \ ■ ■
[ niż e ) f a is j (zobacz rysunek po lewej, w dolnej części strony).
Rotacja w lewo (prawego odnośnika węzła h) Ponowne ustawianie odnośnika w rodzicu po rotacji
Każda rotacja, niezależnie od strony, prowadzi do zwróce
,h
nia odnośnika. Zawsze używamy odnośnika zwróconego
przez metodę rotateR ight() lub rotatel_eft() do usta
wienia odpowiedniego odnośnika w rodzicu (lub w ko
rzeniu drzewa). Zwracany jest prawy lub lewy odnośnik,
jednak zawsze można użyć go do ustawienia odnośnika
w rodzicu. Odnośnik może być czerwony lub czarny.
Node r o t a t e R i g h t ( N o d e h) Metody rotateL eft() i rotateR ight() zachowują kolor
{ przez ustawienie zmiennej x . col or na h. col or. Może to
Node x = h . l e f t ;
h . l e f t = x. r i g h t ; spowodować powstanie w drzewie dwóch kolejnych czer
x . r i g h t = h;
x .c o lo r = h .co lo r;
wonych odnośników, jednak w algorytmach stosujemy
h . c o l o r = RED; rotację, aby rozwiązać ten problem. Przykładowo, kod:
x .N = h.N;
h.N = 1 + s i z e ( h . le f t ) h = ro ta te L e ft(h );
+ sizeCh. r ig h t ) ;
r e t u r n x;
rotuj e wlewo prawy czerwony odnośnik węzła h i ustawia
Xx i:
fi) h w taki sposób, aby prowadził do korzenia uzyskanego
poddrzewa (które zawiera wszystkie węzły poddrzewa,
do którego h prowadził przed rotacją, ale ma inny ko
/ Mniejszy \ /
V niż E ) /
rzeń). Łatwość pisania kodu tego rodzaju to główny po
wód stosowania rekurencyjnych implementacji metod
dla drzew BST. Dzięki temu można łatwo zastosować
Rotacja w prawo (lewego odnośnika węzła h)
rotację jako uzupełnienie normalnego wstawiania.
3.3 a Zbalansow ane drzewa wyszukiwań 447
r o t a c j ę m o ż n a w y k o r z y s t a ć , aby p o m ó c w za ch o w a Lewy Korzeń
n iu zależn o ści 1 d o 1 m ię d z y d rz e w a m i 2-3 a cze rw o n o -
cza rn y m i d rze w a m i BST p rz y w sta w ia n iu n o w y c h kluczy. Wyszukiwanie kończy się
w tym pustym odnośniku
Dzieje się tak, ponieważ rotacje zachowują dwie defini
Korzeń
cyjne cechy czerwono-czarnych drzew BST — kolejność tr
i pełne zbalansowanie. Oznacza to, że m ożna zastosować Czerwony odnośnik do
((aj \ nowego węzła zawierającego
rotacje czerwono-czarnych drzew BST bez obaw o zabu a powoduje przekształcenie
rzenie porządku lub pełnego zbalansowania. Dalej poka węzła podwójnego w potrójny
zujemy, jak wykorzystać rotacje do zachowania dwóch Prawy
^K orzeń
innych definicyjnych cech czerwono-czarnych drzew Wyszukiwanie kończy się
BST (brak kolejnych czerwonych odnośników na której w tym pustym odnośniku
kolwiek ze ścieżek i brak prawych czerwonych odnoś
Dołączony nowy węzeł
ników). Jako rozgrzewkę przedstawiamy kilka łatwych , z czerwonym odnośnikiem
[ b)
przypadków.
Korzeń
W staw ianie do jednego w ęzła podwójnego Czerwono-
. Po rotacji w lewo w celu
czarne drzewo BST o jednym węźle to jeden węzeł po lal W utworzenia dozwolonego
dwójny. Wystarczy wstawić drugi klucz, aby przekonać węzła potrójnego
się o potrzebie rotacji. Jeśli nowy klucz jest mniejszy od Wstawianie do pojedynczego węzła
klucza z drzewa, wystarczy utworzyć nowy (czerwony) podwójnego (dwa przypadki)
węzeł z nowym kluczem i gotowe — powstaje czerwo
Wstawianie C
no-czarne drzewo BST odpowiadające jednem u węzło
wi potrójnemu. Jednak jeżeli nowy klucz jest większy
od klucza w drzewie, dołączenie nowego (czerwonego)
węzła powoduje powstanie prawego czerwonego odnoś Tu należy dodać'
nika. Wtedy kod root = ro ta te L e ft(r o o t); uzupełnia nowy węzeł
wstawianie przez przestawienie czerwonego odnośnika Prawy odnośnik jest czerwony, dlatego
na lewo i zaktualizowanie odnośnika do korzenia drzewa. należy wykonać rotację w lewo
Efekt to w obu sytuacjach czerwono-czarne drzewo repre
zentujące jeden węzeł potrójny. Drzewo ma dwa klucze,
jeden lewy czerwony odnośnik i wysokość (według czar
nych odnośników) 1 .
W staw ianie do w ęzła podw ójnego w dolnej części
drzew a Klucze do czerwono-czarnego drzewa BST
wstawia się jak do zwykłego drzewa BST. Należy dodać
Wstawianie do węzła podwójnego
nowy węzeł na dole (z uwzględnieniem kolejności), jed na dole drzewa
nak zawsze musi on być powiązany z rodzicem za pomocą
czerwonego odnośnika. Jeśli rodzic to węzeł podwójny, m ożna postąpić jak w dwóch
opisanych wcześniej przypadkach. Jeżeli nowy węzeł jest dołączany za pomocą lewe
go odnośnika, rodzic staje się węzłem potrójnym. Przy dołączaniu węzła za pomocą
prawego odnośnika powstaje węzeł potrójny z czerwonym odnośnikiem w złą stronę.
Wtedy rotacja w lewo pozwala zakończyć operację.
448 R O ZD ZIA Ł 3 □ W yszukiwanie
W staw ianie do drzew a o trzech kluczach (do węzła potrójnego) Tę sytuację m oż
na sprowadzić do trzech przypadków — nowy klucz jest mniejszy niż oba klucze
z drzewa, zawiera się między nim i lub jest większy niż każdy z nich. W każdym przy
padku powstaje węzeł o dwóch czerwonych odnośnikach. Zadanie polega na rozwią
zaniu tego problemu.
■ Najprostszy z trzech przypadków ma miejsce wtedy, kiedy nowy klucz jest więk
szy niż dwa klucze w drzewie i dlatego dołączamy go do prawego odnośnika
węzła potrójnego. Powstaje wtedy drzewo zbalansowane z czerwonymi odnoś
nikami do węzłów zawierających mniejszy i większy klucz. Po zamianie kolo
rów tych dwóch odnośników z czerwonego na czarny powstaje drzewo zba
lansowane o wysokości 2, mające trzy węzły. Dokładnie to jest potrzebne do
zachowania zależności 1 do 1 względem drzewa 2-3. Dwa pozostałe przypadki
są ostatecznie sprowadzane do tego.
■ Jeśli nowy klucz jest mniejszy niż oba klucze drzewa i zostaje dołączony do le
wego odnośnika, powstają dwa kolejne czerwone odnośniki (każdy prowadzi
w lewo). Można sprowadzić to do poprzedniego przypadku (gdzie środkowy
klucz jest korzeniem połączonym z innymi kluczami dwoma czerwonymi od
nośnikami), wykonując rotację górnego odnośnika w prawo.
° Jeżeli nowy klucz znajduje się pomiędzy dwoma kluczami drzewa, powstają dwa
kolejne czerwone odnośniki. Górny jest skierowany w lewo, a dolny — wprawo.
Można to sprowadzić do poprzedniego przypadku (dwa kolejne lewe czerwone
odnośniki), obracając dolny odnośnik w lewo.
Podsumujmy — pożądany efekt uzyskujemy, wykonując zero, jedną lub dwie rotacje,
po czym następuje zmiana koloru dwóch dzieci korzenia. Tak jak przy poznawaniu
drzew 2-3, tak i tu upewnij się, że rozumiesz transformacje. Są one kluczem do działa
nia drzew czer-
Większy Mniejszy Pomiędzy .
Wyszukiwarie wono-czarnych.
kończy się Wyszukiwanie Zmiana koloru-
w tym pustym kończy się w tym
odnośniku Wyszukiwanie pustym odnośniku
Do zmiany ko
kończy się w tym
loru dwóch czer
pustym odnośniku
Dołączony wonych dzieci
Dołączony
nowy węzeł
nowy węzeł
węzła służy
z czerwonym Dołączony
odnośnikiem
z czerwonym p rz e d s ta w io n a
nowy węzeł odnośnikiem
z czerwonym po lewej m eto
odnośnikiem da flipColors().
Rotacja Rotacja Oprócz zamia
w prawo lewo
Kolor ny koloru dzieci
zmieniony Rotacja
na czarny
z czerwonego na
wprawo
czarny mody
Kolor
(b ) ^ zmieniony fikujemy kolor
Kolor
ę y f f y y na czarny rodzica z czarne-
zmieniony
na czarny go na czerwony.
Niezwykle waż-
Wstawianie do jednego węzła potrójnego (trzy przypadki)
3.3 c Zbatansowane drzewa wyszukiwań 449
Może być skierowany ną cechą tej operacji jest to, że — podob
wprawo lub w lewo nie jak rotacje — jest to transformacja lo
kalna, która zachowuje pełne zbalansowa-
nie drzewa według czarnych odnośników.
'\ / \
Między \ / Między \ / Większe '
Ponadto rozwiązanie to bezpośrednio
prowadzi do opisanej dalej pełnej imple
AZE ^ y Ei S J s ^ jv ż S _
mentacji.
v o i d f l i p C o l o r s ( N o d e h)
{ Zachow anie czarnego koloru korzenia
h . c o l o r = RED;
h . l e f t . c o l o r = BLACK;
W omówionym przypadku (wstawianie
h. r i g h t . c o l o r = BLACK; do jednego potrójnego węzła) kolor korze
} nia zmieniany jest na czerwony. Może się
Czerwony odnośnik
łączy środkowy to zdarzyć także w większych drzewach.
węzeł z rodzicem
Czerwony kolor korzenia wskazuje na to,
Wstawianie H
Między \ / Między \ / Większe >
A/E ) { E/S ) (v n iż 5
Zm iana kolorów przy p o d ziale w ęzła p o czw ó rn eg o
Dodawanie nowego
węzła w tym miejscu
Dwa lewe odnośniki
że korzeń jest częścią węzła potrójnego, jed pod rząd, dlatego należy
nak jest to nieprawda, dlatego po każdym zrotować jeden w lewo
wstawieniu elementu należy ustawić kolor
korzenia na czarny. Zauważmy, że wysokość
drzewa według czarnych odnośników rośnie
0 1 przy zmianie koloru korzenia z czarnego
na czerwony.
W staw ianie do w ęzła potrójnego na dole
drzewa Teraz załóżmy, że na dole drzewa
dodajemy nowy węzeł powiązany z węzłem
potrójnym. Powstają trzy omówione wcześ Prawy odnośnik jest czerwony,
niej przypadki. Nowy węzeł jest dołączony dlatego należy zrotować go w lewo
albo do prawego odnośnika węzła potrójne
go (wtedy wystarczy zmienić kolor), albo do
lewego odnośnika węzła potrójnego (wtedy
trzeba zrotować górny odnośnik w prawo
1 zmienić kolor), albo do środkowego od
nośnika węzła potrójnego (wtedy należy
zrotować dolny odnośnik w lewo, potem
górny w prawo, a następnie zmienić kolor).
Zmiana kolorów sprawia, że odnośnik do
450 RO ZD ZIA Ł 3 s W yszukiw anie
środkowego węzła staje się czerwony, co prowadzi do przeniesienia odnośnika do
rodzica; powstaje wtedy taka sama sytuacja w rodzicu, którą m ożna rozwiązać, prze
chodząc w górę drzewa.
Przenoszenie czerwonego odnośnika w górę drzew a Algorytm wstawiania do
drzewa 2-3 wymaga podziału węzła potrójnego i przeniesienia środkowego klucza
w górę w celu wstawienia go do rodzica. Proces ten należy powtarzać do m om en
tu napotkania węzła podwójnego lub korzenia. W każdym z opisanych przypadków
zadanie jest precyzyjnie wykonywane. Po niezbędnych rotacjach kolory są zmienia
ne, przez co środkowy węzeł staje się czerwony. Z perspektywy rodzica tego węzła
zmianę koloru odnośnika na czerwony można obsłużyć w dokładnie taki sam spo
sób, jak powstanie czerwonego odnośnika po dołączeniu nowego węzła — czerwony
odnośnik do środkowego węzła należy przenieść w górę. Trzy przypadki pokazane
na rysunku na następnej stronie ilustrują operacje, które trzeba wykonać w drzewie
czerwono-czarnym, aby zaimplementować kluczowe operacje związane z wstawia
niem do drzew 2-3 — wstawianie do węzła potrójnego, tworzenie tymczasowego wę
zła poczwórnego, jego podział i przenoszenie czerwonego odnośnika do środkowego
klucza w górę, do rodzica. Kontynuując ten sam proces, m ożna przenosić czerwony
odnośnik w górę drzewa do czasu napotkania węzła podwójnego lub korzenia.
PODSUMUJMY — M O ŻN A ZACHOWAĆ
zależność 1 do 1 między drzewami
2-3 a czerwono-czarnymi drzewami
BST w czasie wstawiania węzłów, od
powiednio stosując trzy proste opera
cje — rotację w lewo, rotację w prawo
i zmianę koloru. Węzeł można wsta
wić za pomocą wymienionych dalej
operacji, które należy wykonać jedna
po drugiej na każdym węźle przy po
ruszaniu się w górę drzewa od punktu
wstawiania:
a Jeśli prawe dziecko jest czerwo
ne, a lewe — czarne, należy wy
konać rotację w lewo.
° Jeżeli lewe dziecko i jego lewe dziecko są czerwone, należy wykonać rotację
w prawo.
n Jeśli każde z dzieci jest czerwone, należy zmienić kolor.
Z pewnością warto sprawdzić, czy ten ciąg operacji pokrywa każdy z opisanych przy
padków. Zauważmy, że pierwsza operacja obsługuje zarówno rotację potrzebną do
przechylenia węzła potrójnego w lewo, jeśli rodzic jest węzłem podwójnym, jak i do
przechylenia dolnego odnośnika w lewo, jeżeli nowy czerwony odnośnik jest środko
wym odnośnikiem węzła potrójnego.
3.3 Zbatansowane drzewa wyszukiwań 451
ALGORYTM 3.4. Wstawianie do czerwono-czarnego drzewa BST
public class RedBlackBST<Key extends Comparable<Key>, Value>
{
private Node root;
private class Node // Węzę? drzewa BST z bitem określającym kolor
// (zobacz stronę 445).
private boolean isRed(Node h) // Zobacz stronę 445.
private Node rotateLeft(Node h) // Zobacz stronę 446.
private Node rotateRight(Node h) // Zobacz stronę 446.
private void flipColors(Node h) // Zobacz stronę 448.
private int size() // Zobacz stronę 410.
public void put(Key key, Value val)
{ // Wyszukiwanie klucza. Aktualizowanie wartości, je śli znaleziono klucz.
// Jeżeli klucz jest nowy, należy powiększyć tablicę,
root = put(root, key, val);
[Link] = BLACK;
}
private Node put(Node h, Key key, Value val)
{
i f (h == null) // Standardowe wstawianie z czerwonym odnośnikiem do rodzica,
return new Node(key, val, 1, RED);
int cmp = [Link]([Link]);
if (cmp < 0) [Link] ft = put(h.1 e f t , key, val);
else i f (cmp > 0) [Link] = put([Link], key, val);
el se [Link] = v a l ;
i f (isRed([Link]) && !isRed([Link])) h = rotateLeft(h);
i f (isRed([Link]) && isR e d (h .le [Link] ft)) h = rotateRight(h);
i f (isRed([Link]) && isRed(h.r i g h t ) ) flipColors(h);
h.N = siz e (h .le ft) + size(h .righ t) + 1;
return h;
Kod rekurencyjnej metody put() dla czerwono-czarnych drzew BST jest prawie identyczny
z kodem metody put () dla podstawowych drzew BST. Wyjątkiem są trzy instrukcje i f po wy
wołaniach rekurencyjnych, które pozwalają zachować niemal pełne zbalansowanie w drzewie
przez zapewnienie zależności 1 do 1 względem drzew 2-3 przy poruszaniu się w górę ścieżki
wyszukiwania. Pierwsza instrukcja rotuje w lewo przechylony w prawo węzeł potrójny (lub
przechylony w prawo czerwony odnośnik na dole tymczasowego węzła poczwórnego). Druga
rotuje w prawo górny odnośnik w tymczasowym węźle poczwórnym o dwóch czerwonych
odnośnikach przechylonych w lewo. Trzecia zmienia kolory w celu przeniesienia czerwonego
odnośnika w górę drzewa (zobacz opis w tekście).
452 RO ZD ZIA Ł 3 □ W yszukiwanie
Wstawianie s W staw ianie A
Standardowy klient używający indeksu Te same klucze wstawiane w porządku rosnącym
Ś la d y t w o rz e n ia c z e rw o n o -c z a rn y c h d rz e w B ST
3.3 n Zbalansow ane drzewa wyszukiwań 453
Im plem entacja Ponieważ operacje związane z równoważeniem odbywają się
przy przechodzeniu w górę drzewa od punktu wstawiania, m ożna je łatwo zaim
plementować w standardowym rekurencyjnym rozwiązaniu. Wystarczy wykonać
te operacje po rekurencyjnych wywołaniach, co pokazano w a l g o r y t m i e 3 .4 . Trzy
operacje wymienione w poprzednim akapicie można wykonać w jednej instrukcji i f
sprawdzającej kolory dwóch węzłów drzewa. Choć ilość potrzebnego kodu jest nie
wielka, implementacja byłaby dość trudna do zrozumienia bez dwóch opracowanych
warstw abstrakcji (drzew 2-3 i czerwono-czarnych drzew BST). Kosztem sprawdza
nia koloru od trzech do pięciu węzłów (i czasem wykonania jednej lub dwóch rotacji
oraz zmiany kolorów, jeśli test kończy się powodzeniem) uzyskujemy drzewo BST,
które jest prawie w pełni zbalansowane.
Ślady działania standardowego klienta używającego indeksu i dla tych samych
lduczy wstawianych w kolejności rosnącej pokazano na stronie 452. Zastanowienie
się nad przykładami w kategoriach trzech operacji na drzewach czerwono-czarnych,
tak jak robiliśmy to wcześniej, to wartościowe ćwiczenie. Innym takim ćwiczeniem
jest sprawdzenie (na podstawie rysunku opartego na tych samych kluczach, przed
stawionego na stronie 442), czy algorytm zachowuje zależność względem drzew 2-3.
W obu sytuacjach możesz sprawdzić, czy rozumiesz algorytm, analizując transfor
macje (dwie zmiany koloru i dwie rotacje) potrzebne przy wstawianiu P do czerwo
no-czarnego drzewa BST (zobacz ć w i c z e n i e 3 .3 . 1 2 ).
Usuwanie Ponieważ metoda put() w a l g o r y t m ie 3.4 jest — jak dotąd — jed
ną z najbardziej skomplikowanych metod omawianych w książce, a implementacje
m etod deleteMin(), deleteMax() i delete() dla czerwono-czarnych drzew B S T są
nieco bardziej złożone, opracowanie ich pełnych implementacji pozostawiamy jako
ćwiczenia. Warto jednak przeanalizować podstawowe podejście. Aby je przedstawić,
wróćmy najpierw do drzew 2-3. Tak jak przy wstawianiu, tak i tu m ożna zdefiniować
ciąg lokalnych transformacji, które umożliwiają usunięcie węzła przy zachowaniu
pełnego zbalansowania. Proces jest nieco bardziej skomplikowany niż przy wstawia
niu, ponieważ transformacje mają miejsce zarówno przy poruszaniu się w dół ścieżki
wyszukiwania, kiedy to wprowadzane są tymczasowe węzły poczwórne (aby um oż
liwić usunięcie węzła), jak i przy przechodzeniu w górę ścieżki, w ramach podziału
pozostałych węzłów poczwórnych (odbywa się to tak jak przy wstawianiu).
Zstępujące drzewa 2-3-4 W ramach pierwszej rozgrzewki przed usuwaniem om a
wiamy prostszy algorytm, który wykonuje transformacje przy poruszaniu się w dół
i w górę ścieżki. Jest to algorytm wstawiania w drzewach 2-3-4, gdzie tymczasowe węzły
poczwórne poznane w drzewach 2-3 mogą pozostać w drzewie. Algorytm wstawiania
oparto na wykonywaniu transformacji przy przechodzeniu w dół ścieżki, aby zachować
niezmiennik, zgodnie z którym bieżący węzeł nie jest węzłem poczwórnym (dzięki cze
m u wiadomo, że będzie miejsce na wstawienie nowego klucza na dole). Przy poruszaniu
się w górę transformacje są wykonywane w celu zrównoważenia utworzonych węzłów
poczwórnych. Transformacje przy przechodzeniu w dół są dokładnie takie same, jak
454 RO ZD ZIA Ł 3 o W yszukiwanie
przy podziale węzłów poczwórnych w drzewach 2-3. Jeśli korzeń to węzeł poczwórny,
należy podzielić go na trzy węzły podwójne i zwiększyć tym samym wysokość drzewa
o 1. Przy przechodzeniu w dół drzewa po napotkaniu węzła poczwórnego z rodzicem
w postaci węzła podwójnego należy podzielić węzeł poczwórny na dwa węzły podwój
ne i przenieść środkowy klucz do rodzica, przekształcając go na węzeł potrójny. Jeśli
rodzicem węzła poczwórnego jest węzeł potrójny, należy podzielić węzeł poczwórny
na dwa węzły podwójne i przenieść środkowy klucz do rodzica, przekształcając go na
węzeł poczwórny. Z uwagi na niezmiennik nie trzeba się obawiać, że napotkamy węzeł
poczwórny, którego rodzicem też jest taki węzeł. Na dole, także z uwagi na niezmien
nik, znajduje się węzeł podwójny lub potrójny, dlatego do
W korzeniu stępne jest miejsce na nowy klucz. Aby zaimplementować
ten algorytm za pomocą czerwono-czarnych drzew BST,
wykonujemy następujące kroki:
Przy przechodzeniu w dół Przedstawiamy węzły poczwórne jako zbalansowane pod-
drzewo trzech węzłów podwójnych, w którym lewe i pra
A
a we dziecko jest powiązane z rodzicem czerwonym odnoś
nikiem.
A
A Dzielimy węzły poczwórne na drodze w dół drzewa przez
zmianę kolorów.
Równoważymy węzły poczwórne na drodze w górę drze
P A wa przez rotacje (tak jak przy wstawianiu).
Co ciekawe, zstępujące drzewa 2-3-4 m ożna zaim ple
m entować przez przeniesienie jednego wiersza kodu
w m etodzie put () z a l g o r y t m u 3 .4 . Należy przenieść
Na dole
A wywołanie colorFl i p () (i powiązany test) przed wywo
łanie rekurencyjne (między sprawdzanie w artości nuli
a porów nanie). W sytuacjach, kiedy wiele procesów
ma dostęp do tego samego drzewa, algorytm ten ma
a pewne zalety względem drzewa 2-3, ponieważ zawsze
a tu p działa w odległości odnośnika lub dwóch od bieżącego
węzła. A lgorytm y usuw ania opisane dalej są oparte na
Transformacje przy wstawianiu danych
w zstępujących drzewach 2-3-4 znanych schemacie i działają zarówno dla takich drzew,
jak i dla drzew 2-3.
Usuwanie m inim um W ramach drugiej rozgrzewki przed usuwaniem rozważmy
usuwanie m inimum z drzew 2-3. Podstawowy pomysł oparty jest na obserwacji, że
na dole drzewa można łatwo usunąć klucz z węzła potrójnego, ale już nie z węzła po
dwójnego. Usunięcie klucza z węzła podwójnego powoduje, że powstaje węzeł bez klu
czy. Naturalnym rozwiązaniem jest zastąpienie takiego węzła pustym odnośnikiem,
jednak operacja ta narusza warunek pełnego zbalansowania. Dlatego stosujemy na
stępujące podejście — aby zagwarantować, że dojdziemy do węzła podwójnego, przy
przechodzeniu w dół drzewa wykonujemy odpowiednie transformacje w celu zacho
wania niezmiennika, zgodnie z którym bieżący węzeł nie jest podwójny (może być wę
3.3 □ Zbalansow ane drzewa wyszukiwań 455
złem potrójnym lub tymczasowym poczwórnym). W korzeniu
W korzeniu możliwości są dwie — jeśli korzeń to
węzeł podwójny i każde z dzieci to węzeł podwójny,
W (c)
można przekształcić te trzy węzły w j eden poczwór
ny. W przeciwnym razie można „pożyczyć” klucz
z prawego brata, jeśli jest to konieczne, aby zagwa
rantować, że lewe dziecko korzenia nie jest węzłem p p m 1
podwójnym. Następnie, przy przechodzeniu w dół
drzewa, ma miejsce jedna z poniższych sytuacji: Przy przechodzeniu w dół
D Jeśli lewe dziecko bieżącego węzła nie jest
)
węzłem podwójnym, nie trzeba nic robić.
° Jeżeli lewe dziecko jest węzłem podwójnym, h i p m
a jego najbliższy brat nie jest takim węzłem,
należy przenieść klucz z brata do lewego
dziecka.
■ Jeśli lewe dziecko i jego najbliższy brat to wę
zły podwójne, należy połączyć je z najmniej Na dole
szym kluczem z rodzica, aby utworzyć węzeł
poczwórny, przekształcając rodzica z węzła (a b O
/] T\
potrójnego w podwójny lub z poczwórnego
w potrójny. Transformacje przy usuwaniu minimum
Kontynuując ten proces przy przechodzeniu za
pomocą lewych odnośników w dół drzewa, otrzymujemy węzeł potrójny lub po
czwórny z najmniejszym kluczem, dlatego m ożna usunąć klucz i przekształcić węzeł
potrójny na podwójny lub poczwórny na potrójny. Następnie, poruszając się w górę
drzewa, należy podzielić niewykorzystane tymczasowe węzły poczwórne.
Usuwanie Transformacje na ścieżce wyszukiwania opisane w kontekście usuwania
m inim um przydają się w trakcie wyszukiwania kluczy do zapewnienia, że bieżący
węzeł nie jest podwójny. Jeśli klucz wyszukiwania znajduje się na dole, można go
usunąć. Jeżeli znajduje się w innym miejscu, należy zastąpić go następnikiem, tak
jak w zwykłych drzewach BST. Następnie, z uwagi na to, że bieżący węzeł nie jest
podwójny, problem sprowadza się do usunięcia m inim um w poddrzewie, którego
korzeń nie jest węzłem podwójnym. Można zastosować procedurę opisaną wcześniej
dla takich poddrzew. Po usunięciu należy, jak zwykle, podzielić wszystkie pozostałe
węzły poczwórne na ścieżce wyszukiwania prowadzącej w górę drzewa.
w końcowej części podrozdziału dotyczą przykładów i imple
n ie k t ó r e ć w ic z e n ia
mentacji związanych z algorytmami usuwania. Osoby zainteresowane utworzeniem lub
zrozumieniem implementacji muszą opanować szczegóły omówione w ćwiczeniach.
Czytelnicy ogólnie zaciekawieni badaniem algorytmów powinni docenić znaczenie
tych metod. Opisana tu implementacja tablicy symboli jako pierwsza gwarantuje wy
dajne wykonanie operacji wyszukiwania, wstawiania i usuwania, co opisano dalej.
456 RO ZD ZIA Ł 3 a W yszukiwanie
Cechy czerwono-czarnych drzew BST Badanie cech czerwono-czarnych
drzew BST polega na sprawdzaniu odpowiedniości względem drzew 2-3, a następnie
stosowaniu analiz dotyczących drzew 2-3. Efekt końcowy jest taki, że wszystkie operacje
na tablicy symboli opartej na czerwono-czarnych drzewach BST mają gwarantowany
czas logarytmiczny względem rozmiaru drzewa (wyjątkiem jest wyszukiwanie zakreso
we, przy którym występują dodatkowe koszty czasowe proporcjonalne do liczby zwra
canych kluczy). Powtarzamy i podkreślamy tę kwestię z uwagi na jej znaczenie.
A n a lizy Najpierw ustalamy, że czerwono-czarne drzewa BST, choć nie są w pełni
zbalansowane, zawsze są tem u bliskie. Jest tak niezależnie od kolejności wstawiania
kluczy. Bezpośrednio wynika to z zależności 1 do 1 względem drzew 2-3 i cechy de
finicyjnej drzew 2-3 (pełnego zbalansowania).
Twierdzenie G. Wysokość czerwono-czarnego drzewa BST o N węzłach jest
nie większa niż 2 lg N.
Zarys dowodu. Najgorszym przypadkiem jest drzewo 2-3, w którym pierwsza
od lewej ścieżka składa się z węzłów potrójnych, a pozostałe węzły są podwójne.
Pierwsza od lewej ścieżka jest dwukrotnie dłuższa niż ścieżki o długości ~lg N,
które obejmują same węzły podwójne. Możliwe, choć niełatwe, jest utworzenie
ciągu kluczy powodującego utworzenie czerwono-czarnego drzewa BST, w którym
średnia długość ścieżki wynosi tyle, co w najgorszym przypadku, czyli 2 lg N. Jeśli
masz zdolności matematyczne, może zainteresować Cię zbadanie tego zagadnie
nia przez wykonanie ć w i c z e n i a 3 .3 .2 4 .
Górne ograniczenie jest konserwatywne. W eksperymentach obejmujących wstawia
nie losowych danych i wstawianie ciągów specyficznych dla typowych zastosowali po
twierdzono hipotezę, zgodnie z którą wyszukiwanie w czerwono-czarnych drzewach
BST o N węzłach wymaga średnio około 1,00 lg N - 0,5 porównań. Ponadto w praktyce
mało prawdopodobne jest wystąpienie wyraźnie wyższej średniej liczby porównań.
Typowe czerwono-czarne drzewo BST zbudowane z losowych kluczy (pominięto puste odnośniki)
3.3 a Zbalansow ane drzewa wyszukiwań 457
t a le . t x t le ip z ig lM . t x t
Słowa Różne Porównania Słowa Różne Porównania
łącznie słowa łącznie słowa
Model Uzyskano Model Uzyskano
Wszystkie 135 635 10 679 13,6 13,5 21 191 455 534 580 19,4 19,1
słowa
Przynajmniej 14 350 5737 12,6 12,1 4 239 597 299 593 18,7 18,4
8 liter
Przynajmniej 4582 2260 11,4 11,5 1 610 829 165 555 17,5 17,3
10 liter
Średnia liczba porównań na operację put () w programie FrequencyCounter
używającym klasy RedBlackBST
Cecha H. Średnia długość ścieżki z korzenia do węzła w czerwono-czarnym drzewie BST
o N węzłach wynosi -1,00 lg N.
Dowód. Typowe drzewa, takie jak pokazane na dole poprzedniej strony (a nawet te zbu
dowane przez wstawienie kluczy w rosnącej kolejności, przedstawione na dole tej strony),
są dość dobrze zbalansowane w porównaniu do typowych drzew BST (takich jak drzewa ze
strony 417). W tabeli na górze tej strony pokazano, że długości ścieżek (koszty wyszukiwa
nia) w programie FrequencyCounter są — zgodnie z oczekiwaniami — mniej więcej 40%
niższe niż dla podstawowych drzew BST. Od czasu wymyślenia czerwono-czarnych drzew
BST podobne wyniki zaobserwowano w niezliczonych programach i eksperymentach.
W przykładowej analizie kosztów operacji put () w programie FreąuencyCounter
dla słów o długości przynajmniej 8 liter widoczny jest dalszy spadek kosztów. Jest
to następne potwierdzenie wydajności logarytmicznej prognozowanej na podstawie
m odelu teoretycznego, choć — z uwagi na gwarancje opisane w t w i e r d z e n i u g
— potwierdzenie jest tu mniej zaskakujące niż dla drzew BST. Łączne oszczędności
wynoszą mniej niż 40% oszczędności kosztów wyszukiwania, ponieważ oprócz p o
równań uwzględniono też rotacje i zmianę kolorów.
Czerwono-czarne drzewo BST zbudowane z rosnących kluczy (pominięto puste odnośniki)
458 RO ZD ZIA Ł 3 a W yszukiw anie
Metoda g et() w czerwono-czarnych drzewach BST nie sprawdza koloru węzła, dla
tego mechanizm równoważenia nie powoduje dodatkowych kosztów. Wyszukiwanie
jest szybsze niż w podstawowych drzewach BST, ponieważ drzewo jest zbalansowane.
Każdy klucz jest wstawiany raz, ale może być używany w wielu, wielu operacjach wy
szukiwania, dlatego efekt końcowy jest taki, że czas wyszukiwania jest bliski optymal
nemu (ponieważ drzewa są prawie zbalansowane i w czasie wyszukiwania nie trzeba
wykonywać żadnych operacji w tym celu) i dzieje się to stosunkowo małym kosztem
(inaczej niż w wyszukiwaniu binarnym wstawianie odbywa się w czasie logarytmicz
nym). Pętla wewnętrzna przy wyszukiwaniu obejmuje operację porównywania, po
której następuje aktualizacja odnośnika. Pętla ta jest dość krótka, podobnie jak pętla
wewnętrzna wyszukiwania binarnego (porównanie i operacje arytmetyczne na indek
sach). Jest to pierwsza implementacja, która gwarantuje logarytmiczny czas wyszuki
wania i wstawiania oraz ma krótką pętlę wewnętrzną. Dlatego stosowanie tego rozwią
zania jest uzasadnione w wielu sytuacjach, w tym w implementacjach bibliotek.
Interfejs A P I dla uporządkow anej tablicy sym boli Jedną z najbardziej atrakcyj
nych cech czerwono-czarnych drzew BST jest to, że skomplikowany kod znajduje się
tylko w metodzie put () iw metodach związanych z usuwaniem. Można bez żadnych
zmian zastosować kod szukania m inim um i maksimum, wybierania, określania p o
zycji, podłogi oraz sufitu, a także zapytań zakresowych używany dla standardowych
drzew BST, ponieważ nie wymaga podawania koloru węzłów, a l g o r y t m 3.4 wraz
z tymi metodam i (i m etodam i usuwania) stanowi kompletną implementację inter
fejsu API dla uporządkowanej tablicy symboli. Ponadto we wszystkich metodach ko
rzystne jest prawie pełne zbalansowanie drzewa, ponieważ każda z tych m etod działa
najwyżej w czasie proporcjonalnym do wysokości drzewa. Dlatego t w i e r d z e n i e g
w połączeniu z t w i e r d z e n i e m e wystarczają do zagwarantowania logarytmicznego
czasu działania wszystkich wymienionych metod.
3.3 n Zbalansow ane drzewa wyszukiwań 459
Twierdzenie I. W czerwono-czarnych drzewach BST wymienione tu operacje
działają w czasie logarytmicznym dla najgorszego przypadku. Oto te operacje:
wyszukiwanie, wstawianie, znajdowanie m inim um i maksimum, określanie pod
łogi, sufitu i pozycji, wybieranie, usuwanie m inim um i maksimum, usuwanie
i zliczanie elementów w przedziale.
Dowód. Omówiliśmy już m etody g et() i put () oraz operacje usuwania. Dla
innych można bezpośrednio wykorzystać kod z p o d r o z d z i a ł u 3.2 (kod ig
noruje kolor węzłów). Gwarancje logarytmicznego czasu działania wynikają
z t w i e r d z e ń e i g oraz z tego, że każdy algorytm wykonuje stałą liczbę operacji
na każdym sprawdzanym węźle.
Po zastanowieniu można stwierdzić, że możliwość zapewnienia opisanych gwarancji
jest zaskakująca. W świecie pełnym informacji, w którym powstają tablice o trylio
nach lub kwadrylionach elementów, można zagwarantować ukończenie każdej ope
racji na takich tablicach za pomocą tylko kilkudziesięciu porównań.
Koszt dla najgorszego Koszt dla typow ego w ri ' hł
Algorytm (struktura przypadku (po N przypadku (po N ^
danych) [Link]) losowych wstawieniach) uporządkowanych
W yszukiwanie Wstawianie Trafienie Wstawianie danych?
Wyszukiwanie
sekwencyjne
N N N/2 N Nie
(nieuporządkowane
listy powiązane)
Wyszukiwanie binarne
(uporządkowane lg N N lg N N/2 Tak
tablice)
Drzewa wyszukiwań
N N 1,39 IgN 1,39 lgN Tak
binarnych
Drzewa 2-3 2 lg N 2 lg N 1,00 lgN 1,00 lgN Tak
(czerwono-czarne
drzewa BST)
P o d s u m o w a n ie k o s z t ó w im p le m e n t a c ji t a b lic y s y m b o li ( z a k tu a liz o w a n e )
460 RO ZD ZIA Ł 3 a W yszukiwanie
| PYTANIA I ODPOWIEDZI
P. Dlaczego nie pozwalamy na przechylanie węzłów potrójnych w dowolną stronę
i na występowanie w drzewach węzłów poczwórnych?
O. Są to ciekawe alternatywy, używane przez wielu programistów od dziesięcioleci.
Więcej o kilku możliwościach dowiesz się z ćwiczeń. Ograniczenie się do węzłów
przechylonych w lewo zmniejsza liczbę przypadków, co prowadzi do znacznie m niej
szej ilości kodu.
P. Dlaczego nie używamy tablicy wartości typu Key do reprezentowania węzłów p o
dwójnych, potrójnych i poczwórnych za pomocą jednego typu Node?
O. Dobre pytanie. Takie rozwiązanie zastosowano w drzewach zbalansowanych (ina
czej B-drzewach; zobacz r o z d z i a ł 6 .), w których dopuszczalna jest znacznie większa
liczba kluczy na węzeł. W małych węzłach w drzewach 2-3 koszty związane z prze
chowywaniem tablicy są zbyt duże.
P. Przy podziale węzła poczwórnego czasem kolor prawego węzła ustawiany jest na
RED w metodzie ro tate R ig h t(), a następnie od razu na BLACK w metodzie flipCo-
1ors (). Czy nie jest to zbędne?
O. Tak, ponadto czasem niepotrzebnie zmieniamy kolor środkowego węzła. W ogól
nym rozrachunku ponowne ustawienie kilku bitów ma bardzo małe znaczenie w p o
równaniu z poprawą czasu wykonania z liniowego na logarytmiczny dla wszystkich
operacji. Jednak w zastosowaniach, gdzie czas odgrywa krytyczną rolę, można um ieś
cić kod m etod ro tateR ig h t() i flipColors() bezpośrednio w miejscach wywołania
oraz wyeliminować dodatkowe sprawdzanie. Metody te używane są też do usuwa
nia. Uważamy, że kod jest nieco łatwiejszy w użytku, do zrozumienia i w pielęgnacji,
ponieważ mamy pewność, że zachowane jest pełne zbalansowanie według czarnych
odnośników.
3.3 a Zbalansow ane drzewa wyszukiwań 461
ĆWICZENIA
Narysuj drzewo 2-3 uzyskane przez wstawienie kluczy E A S Y Q U T I O N
3 .3 .1 .
(w tej kolejności) do początkowo pustego drzewa.
3 .3.2 .Narysuj drzewo 2-3 otrzymane przez wstawienie kluczy Y L P MX H C R AES
(w tej kolejności) do początkowo pustego drzewa.
Określ kolejność wstawiania kluczy S E A
3 .3 .3 . R C H X M, która prowadzi do po
wstania drzewa 2-3 o wysokości 1.
Udowodnij, że wysokość drzewa 2-3 o N kluczach wynosi pomiędzy ~ l_log3
3 .3 .4 .
N] ~ 0,63 lg N (dla drzewa złożonego z samych węzłów potrójnych) a ~ Lig N] (dla
drzewa zawierającego same węzły podwójne).
Na rysunku po prawej stronie pokazano wszystkie strukturalnie
3 .3 .5 . a
różne drzewa 2-3 o N kluczach dla N równego od 1 do 6 (kolejność
poddrzew nie jest tu istotna). Narysuj wszystkie strukturalnie różne
drzewa dla N - 7, 8, 9 i 10.
3 .3 .6 . Określ prawdopodobieństwo, że każde z drzew 2-3 z ć w i c z e n i a
3 .3.5 jest efektem wstawienia N losowych różnych kluczy do początko
wo pustego drzewa.
Narysuj diagramy, taicie jak w górnej części strony 440, dla pię
3 .3 .7 .
ciu innych przypadków przedstawionych na dole owej strony.
Przedstaw wszystkie możliwe sposoby na zapisanie węzła po
3 .3 .8 .
czwórnego za pom ocą trzech węzłów podwójnych powiązanych czer
wonymi odnośnikami (odnośniki nie muszą być skierowane w lewo).
3 .3 .9 . Które z poniższych drzew to czerwono-czarne drzewa BST?
Narysuj czerwono-czarne drzewo BST uzyskane przez wstawienie elemen
3 . 3 .1 0 .
tów o kluczach E A S Y Q U T I 0 N (w tej kolejności) do początkowo pustego
drzewa.
Narysuj czerwono-czarne drzewo BST uzyskane przez wstawienie elemen
3 . 3 .1 1 .
tów o kluczach Y L P H X H C R A E S (w tej kolejności) do początkowo pustego
drzewa.
462 ROZDZIAŁ 3 a Wyszukiwanie
ĆWICZENIA (ciąg dalszy)
3.3.12. Narysuj czerwono-czarne drzewo BST powstałe po każdej transformacji
(zmianie koloru lub rotacji) w czasie wstawiania P przez standardowego klienta uży
wającego indeksu.
3.3.13. Jeśli wstawiasz klucze w kolejności rosnącej do czerwono-czarnego drzewa
BST, wysokość drzewa jest monofonicznie rosnąca — prawda czy fałsz?
3.3.14. Narysuj czerwono-czarne drzewo BST uzyskane po wstawieniu kolejnych
liter od A do Kdo początkowo pustego drzewa. Następnie opisz, co się ogólnie dzieje,
kiedy drzewa są budowane przez wstawianie kluczy w porządku rosnącym (zobacz
też rysunek w tekście).
3.3.15. Wykonaj dwa poprzednie ćwi
czenia przy założeniu, że klucze są wsta
wiane w kolejności malejącej.
3 .3.16. Przedstaw efekt wstawienia n do
czerwono-czarnego drzewa BST z rysun
ku po prawej stronie (przedstawiono tyl
ko ścieżkę wyszukiwania; w odpowiedzi
uwzględnij wyłącznie widoczne węzły).
3.3.17. Wygeneruj dwa losowe 16-wę-
złowe czerwono-czarne drzewa BST.
Narysuj je (ręcznie lub za pom ocą progra
mu). Porównaj je z (niezbalansowanymi) drzewami BST zbudowanymi za pomocą
tych samych kluczy.
3.3.18. Narysuj wszystkie strukturalnie różne czerwono-czarne drzewa BST o N
kluczach dla N równego od 2 do 10 (zobacz ć w i c z e n i e 3 .3 .5 ).
3.3.19. Za pom ocą 1 przeznaczonego na kolor bitu na węzeł m ożna przedstawić
węzły podwójne, potrójne i poczwórne. Ile bitów na węzeł potrzeba do reprezento
wania węzłów o 5, 6 , 7 i 8 odnośnikach w drzewie binarnym?
3.3.20. Oblicz długość ścieżki wewnętrznej dla w pełni zbalansowanego drzewa
BST o N węzłach, gdzie N to potęga dwójki minus jeden.
3.3.21. Utwórz klienta testowego [Link] na podstawie rozwiązania ć w ic z e n ia
3 .2 . 1 0 .
3.3.22. Znajdź ciąg kluczy do wstawienia do drzewa BST i do czerwono-czarnego
drzewa BST, tak aby wysokość drzewa BST była mniejsza niż wysokość czerwono-
czarnego drzewa BST, lub udowodnij, że taki ciąg nie istnieje.
3.3 ■ Zbalansow ane drzewa wyszukiwań 463
j1 PROBLEMY DO ROZWIĄZANIA
3.3.23. Drzewa 2-3 bez wymogu zbalansowania. Opracuj implementację interfejsu
API podstawowej tablicy symboli. Jako strukturę danych wykorzystaj drzewa 2-3,
które nie muszą być zbalansowane. Dopuść przechylenie węzłów potrójnych w do
wolną stronę. Przy wstawianiu do węzła potrójnego na dole drzewa nowy węzeł do
łączaj za pom ocą czarnego odnośnika. Przeprowadź eksperymenty, aby opracować
hipotezę na temat szacunkowej średniej długości ścieżki w drzewie zbudowanym po
N losowych operacjach wstawiania.
3.3.24. Najgorszy przypadek dla czerwono-czarnych drzew BST. Pokaż, jak utworzyć
czerwono-czarne drzewo BST, aby zademonstrować, że w najgorszym przypadku
prawie wszystkie ścieżki z korzenia do pustego odnośnika w takim drzewie składają
cym się z N węzłów mają długość 2 lg N.
3.3.25. Zstępujące drzewa 2-3-4. Opracuj implementację interfejsu API tablicy sym
boli opartą na zbalansowanych drzewach 2-3-4. Użyj reprezentacji w postaci drzew
czerwono-czarnych i opisanej w tekście m etody wstawiania, polegającej na podziale
węzłów poczwórnych przez zmianę kolorów przy przechodzeniu w dół ścieżki wy
szukiwania i równoważeniu drzewa na drodze w górę.
3.3.26. Jedno przejście góra-dół. Opracuj zmodyfikowaną wersję rozwiązania
3 .2 .2 5 , która nie obejmuje rekurencji. Wszystkie operacje podziału i rów
ć w ic z e n ia
noważenia węzłów poczwórnych (oraz równoważenia węzłów potrójnych) wykonaj
przy przechodzeniu w dół drzewa, a na końcu wstaw dane na dole drzewa.
3.3.27. Zezwalanie na odnośniki skierowane wprawo. Opracuj zmodyfikowaną wer
sję rozwiązania ć w i c z e n i a 3 .3 .2 5 , w której dozwolone są czerwone odnośniki skie
rowane w prawo.
3.3.28. Wstępujące drzewa 2-3-4. Opracuj implementację interfejsu API podstawo
wej tablicy symboli, opartą na drzewach 2-3-4. Wykorzystaj reprezentację w postaci
drzew czerwono-czarnych i wstawianie m etodą dół-góra, opartą na tym samym re-
kurencyjnym podejściu, co a l g o r y t m 3 .4 . M etoda powinna dzielić tylko te ciągi
węzłów poczwórnych (jeśli taicie występują), które znajdują się na dole ścieżki wyszu
kiwania.
3.3.29. Optymalne wykorzystanie pamięci. Zmodyfikuj klasę RedBlackBST tak, aby
nie zajmowała dodatkowej pamięci na bit określający kolor. Wykorzystaj następują
cą sztuczkę — aby ustawić kolor węzła na czerwony, przestaw dwa jego odnośniki.
Następnie, aby sprawdzić, czy węzeł jest czerwony, określ, czy lewe dziecko jest więk
sze od prawego. Musisz zmodyfikować porównania, aby uwzględnić możliwe prze
stawienie odnośników. Technika wymaga zastąpienia porównań bitów porównania
mi kluczy (które prawdopodobnie są bardziej kosztowne), ale pozwala się przekonać,
że w razie potrzeby bit w węzłach m ożna wyeliminować.
464 RO ZD ZIA Ł 3 ■ W yszukiw anie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
3.3.30. Programowa pamięć podręczna. Zmodyfikuj klasę RedBlackBST, aby prze
chowywała ostatnio używany obiekt typu Node w zmiennej egzemplarza, co pozwala
uzyskać dostęp do obiektu w stałym czasie, jeśli następna operacja put () lub g et()
dotyczy tego samego klucza (zobacz ć w i c z e n i e 3 . 1 . 2 5 ).
3.3.31. Rysowanie drzew. Dodaj do klasy RedBlackBST metodę draw (), rysującą czerwo
no-czarne drzewa BST w rodzaju tych pokazanych w tekście (zobacz ć w ic z e n ie 3 .2 .38 ).
3.3.32. Drzewa AVL. Drzewo AVL to drzewo BST, w którym wysokość każdego węzła
i jego brata różni się najwyżej o 1 (najstarsze algorytmy dotyczące drzew zbalansowa-
nych są oparte na stosowaniu rotacji do zachowania zbalansowania wysokości drzew
AVL). Wykaż, że kolorowanie na czerwono odnośników prowadzących z węzłów o pa
rzystej wysokości do węzłów o nieparzystej wysokości w drzewie AVL daje (w pełni
zbalansowane) drzewo 2-3-4, w którym czerwone odnośniki nie zawsze są skierowane
w lewo. Dodatkowe zadanie: opracuj implementację interfejsu API tablicy symboli op
artą na opisanej strukturze danych. Jedną z możliwości jest przechowywanie wysokości
drzewa w każdym węźle i używanie rotacji po rekurencyjnych wywołaniach w celu
dostosowania wysokości. Inny sposób to użycie drzew czerwono-czarnych i metod
w rodzaju moveRedLef() imoveRedRight() z ć w i c z e ń 3 .3.39 i 3 .3 .40 .
3.3.33. Sprawdzanie. Dodaj do klasy RedBl ackBST metodę i s23() sprawdzającą, czy
żaden węzeł nie jest powiązany z dwoma czerwonymi odnośnikam i i czy nie istnieją
czerwone odnośniki skierowane w prawo, oraz metodę i sBalanced(), która spraw
dza, czy wszystkie ścieżki od korzenia do pustego odnośnika obejmują tę samą licz
bę czarnych odnośników. Połącz te m etody z kodem m etody i sBST() z ć w i c z e n i a
3 .2 .3 2 , aby utworzyć m etodę isRedBlackBST() służącą do sprawdzania, czy drzewo
jest czerwono-czarnym drzewem BST.
3.3.34. Wszystkie drzewa 2-3. Napisz kod generujący wszystkie strukturalnie różne
drzewa 2-3-4 o wysokości 2, 3 i 4. Jest ich, odpowiednio, 2, 3 i 127. Wskazówka: wy
korzystaj tablicę symboli.
3.3.35. Drzewa 2-3. Napisz program [Link]. Zastosuj w nim dwa rodzaje
węzłów do bezpośredniego zaimplementowania drzew wyszukiwań 2-3.
3.3.36. Drzewa 2-3-4-5-6-7-8. Opisz algorytmy wyszukiwania i wstawiania w drze
wach wyszukiwań 2-3-4-5-6-7-8.
3.3.37. Bez efektu pamięci. Wykaż, że czerwono-czarne drzewa BST nie są pozba
wione efektu pamięci. Przykładowo, jeśli wstawisz klucz mniejszy niż wszystkie klucze
drzewa, a następnie natychmiast usuniesz m inim um , może powstać inne drzewo.
3.3 Q Zbalansow ane drzewa wyszukiwań 465
3.3.38. Podstawowe twierdzenie o rotacjach. Wykaż, że każde drzewo BST można
przekształcić na dowolne inne drzewo BST o tych samych kluczach za pomocą ciągu
rotacji w lewo i prawo.
3.3.39. Usuwanie minimum. Zaimplementuj operację deleteM in() dla czerwono-
czarnych drzew BST analogiczną do opisanych w tekście transformacji (wykonywa
nych przy poruszaniu się w dół lewą stroną drzewa i zachowywaniu przy tym nie
zmiennika, zgodnie z którym bieżący węzeł nie jest węzłem podwójnym).
Rozwiązanie:
private Node moveRedLeft(Node h)
{ // Przy założeniu, że h je s t czerwony, a h . le f t i h .l e f t . l e f t
// są czarne, zmień kolor h . le f t lub jednego z jego dzieci
/ / n a czerwony.
f lip C o lo r s(h );
i f (is R e d (h .r i g h t . l e f t ) )
{
h .rig h t = ro t a te R ig h t(h .r i g h t ) ;
h = r o t a t e L e f t ( h );
}
return h;
}
public void deleteMin()
{
i f ( ! isR e d ( r o o t .1 eft) && !is R e d ( ro o t.r ig h t ) )
ro o t.c o lo r = RED;
root = d e leteM in (root);
i f (lis E m p ty O ) ro o t.c o lo r = BLACK;
}
private Node deleteMin(Node h)
{
i f ( h . le f t == n u ll)
return nul 1;
i f (! i sRed (h . 1eft) && ! i s Red (h . le f t . l e f t ) )
h = moveRedLeft(h);
h . le f t = d e le t e M in ( h . le f t ) ;
return balance(h);
}
Zakładamy, że istnieje metoda bal ance() składająca się z poniższego wiersza kodu:
i f (is R e d (h .r i g h t ) ) h = ro t a t e L e f t ( h ) ;
466 R O ZD ZIA Ł 3 n W yszukiw anie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
po którym następuje pięć ostatnich wierszy rekurencyjnej metody put () z a l g o r y t m u
3 .4 . Przyjmujemy też, że zastosowano implementację metody fli pCol ors () dopasowu
jącą kolory trzech węzłów (zamiast wersji przedstawionej w tekście w kontekście wsta
wiania). Przy usuwaniu należy ustawić rodzica na BLACK, a dwoje dzieci — na RED.
3.3.40. Usuwanie maksimum. Zaimplementuj operację deleteMax() dla czerwono-
czarnych drzew BST. Zauważ, że transformacje różnią się tu nieco od tych z poprzed
niego ćwiczenia, ponieważ czerwone odnośniki są skierowane w lewo.
Rozwiązanie:
private Node moveRedRight(Node h)
{ // Przy założeniu, że h je s t czerwony, a h .rig h t i h .r i g h t . l e f t
// są czarne,
// należy ustawić h .rig h t lub jedno z jego dzieci na czerwony.
flipColors(h)
i f ( ! is R e d (h . l e f t . l e f t ) )
h = ro t a t e R ig h t ( h );
return h;
}
p ublic void deleteMaxQ
{
i f ( l is R e d ( r o o t . le f t ) && li s R e d ( r o o t . r ig h t ) )
ro o t.c o lo r = RED;
root = deleteM ax(root);
i f (iis E m p ty O ) ro o t.c o lo r = BLACK;
}
p rivate Node deleteMax(Node h)
{
i f (is R e d ( h . le f t ))
h = ro t a t e R ig h t ( h );
i f (h . rig h t == n u ll)
return nul 1;
i f ( ! isR e d (h. rig h t) && !isR e d (h. r i g h t . l e f t ) )
h = moveRedRight(h);
h .r ig h t = d ele te M a x (h .righ t);
return balance(h);
}
3.3 □ Zbalansow ane drzewa wyszukiwań 467
3.3.41. Usuwanie. Zaimplementuj operację delete() dla czerwono-czarnych
drzew BST, łączącą m etody z dwóch poprzednich ćwiczeń z operacją del ete() dla
drzew BST.
Rozwiązanie:
public void delete(Key key)
{
i f ( l is R e d ( r o o t . le f t ) && !is R e d ( ro o t.r ig h t ) )
ro o t.c o lo r = RED;
root = delete(root, key);
i f (lis E m p ty O ) ro o t.c o lo r = BLACK;
}
private Node delete(Node h, Key key)
{
i f ([Link]([Link]) < 0)
{
i f ( ! isR e d (h . 1 eft) && !isR e d (h . l e f t . l e f t ) )
h = moveRedLeft(h);
h . le f t = d e le t e ( h .le f t, key);
}
el se
{
i f ( is R e d ( h . le f t ) )
h = ro t a te R ig h t( h );
i f ([Link]([Link]) == 0 && (h .rig h t == n u ll) )
return nul 1;
i f ( ! isR e d (h .rig h t) && ! i s R e d ( h . r ig h t . l e f t ) )
h = moveRedRight(h);
i f ([Link]([Link]) == 0)
{
[Link] = g e t ( h .rig h t , m i n ( h . r ig h t ) . k e y ) ;
[Link] = m in(h .righ t).ke y;
h .r ig h t = d e le t e M in ( h .r ig h t );
}
else h .r ig h t = d e le t e (h .rig h t, key);
}
return balan ce(h );
}
468 RO ZD ZIA Ł 3 a W yszukiw anie
Q EKSPERYMENTY
3.3.42. Zliczanie czerwonych węzłów. Napisz program, który określa procent czer
wonych węzłów w danym czerwono-czarnym drzewie BST. Przetestuj program przez
uruchomienie przynajmniej 100 powtórzeń eksperymentu polegającego na wstawie
niu Włosowych kluczy do początkowo pustego drzewa (przyjmij N = 104, 105 i 10s).
Sformułuj hipotezy.
3.3.43. Wykresy kosztów. Zmodyfikuj klasę RedBlackBST tak, aby można było two
rzyć wykresy podobne do przedstawionych w podrozdziale, pokazujących koszt każ
dej operacji put () w czasie obliczeń (zobacz ć w i c z e n i e 3 . 1 .38 ).
3.3.44. Średni czas wyszukiwania. Przeprowadź badania empiryczne, aby obliczyć
średnią i odchylenie standardowe średniej długości ścieżki do losowego węzła (czyli
długości ścieżki wewnętrznej podzielonej przez rozmiar drzewa) w czerwono-czar
nym drzewie BST zbudowanym przez wstawienie N losowych kluczy do początkowo
pustego drzewa (dla W od 1 do 10 000) .Wykonaj przynajmniej 1000 powtórzeń dla
każdej wielkości drzewa. Przedstaw wyniki jako wykres Tuftea, taki jak na dole tej
strony. Nałóż je na krzywą odpowiadającą funkcji Ig N - 0,5.
3.3.45. Zliczanie rotacji. Zmodyfikuj program z ć w i c z e n i a 3 .3 .4 3 , aby wyświet
lał liczbę rotacji i podziałów węzłów przeprowadzonych w celu zbudowania drzew.
Omów wyniki.
3.3.46. Wysokość. Zmodyfikuj program z ć w i c z e n i a 3 .3 .4 3 , aby wyświetlał wyso
kość czerwono-czarnych drzew BST. Omów wyniki.
Porów nania
Średnia długość ścieżki do losowego węzła w czerwono-czarnych drzewach BST zbudowanych z losowych kluczy
3 .4 .T A B L IC E Z H A S Z O W A N IE M
U
Jeśli kluczami są małe liczby całkowite, można użyć tablicy do zaimplementowania
nieuporządkowanej tablicy symboli. Klucze są wtedy indeksem tablicy, dlatego m oż
na zapisać wartość powiązaną z kluczem i na pozycji i tablicy, co zapewnia bezpo
średni dostęp do wartości. W tym podrozdziale omawiamy haszowanie — rozwinię
cie wspomnianej prostej m etody umożliwiające obsługę bardziej skomplikowanych
rodzajów kluczy. Pary klucz-wartość w tablicach wskazywane są na podstawie opera
cji arytmetycznych przekształcających klucze w indeksy tablicy.
Algorytmy wyszukiwania oparte na haszowaniu składają się z dwóch odrębnych czę
ści. Pierwsza oblicza funkcję haszującą, która przekształca klucz wyszukiwania na skrót
wyznaczający indeks tablicy. W idealnych warunkach różne
Klucz Skrót Wartość
pqi klucze odpowiadają różnym indeksom. Zwykle ideał ten jest
2 xyz
nieosiągalny, dlatego może się zdarzyć, że dwa klucze (lub
pqr
większa ich liczba) będą odpowiadać temu samemu indek
ijk
uvw sowi tablicy. Dlatego drugą częścią wyszukiwania opartego
na haszowaniu jest proces rozwiązywania kolizji, który po
zwala radzić sobie z taką sytuacją. Po opisaniu sposobu obli
Kolizja czania funkcji haszujących omawiamy dwa różne podejścia
i jk
do rozwiązywania kolizji — metodę łańcuchową (ang. sepa-
rate chaining) i próbkowanie liniowe (ang. linear probing).
Przy haszowaniu występuje klasyczny problem równo
ważenia czasu i pamięci. Gdyby nie było ograniczeń pa
mięciowych, przy każdym wyszukiwaniu wystarczyłby je
M-l den dostęp do pamięci — przez zastosowanie klucza jako
indeksu do (potencjalnie bardzo dużej) tablicy. Często jest
Haszowanie - istota problemu
to jednak niemożliwe, ponieważ jeśli liczba możliwych
wartości kluczy jest wielka, ilość potrzebnej pamięci jest
niedopuszczalnie duża. Z kolei gdyby nie istniały ograniczenia czasowe, wystarczy
łaby m inim alna ilość pamięci i wyszukiwanie sekwencyjne w nieuporządkowanej
tablicy. Haszowanie pozwala ograniczyć do rozsądnej ilości potrzebny czas i pamięć
oraz uzyskać równowagę między opisanymi skrajnymi sytuacjami. Okazuje się, że
w algorytmach haszowania m ożna zyskać czas kosztem pamięci (i na odwrót), dosto
sowując parametry. Nie wymaga to modyfikowania kodu. Aby ułatwić dobór w arto
ści parametrów, m ożna wykorzystać znane wyniki z teorii prawdopodobieństwa.
Teoria prawdopodobieństwa jest osiągnięciem z dziedziny analizy matematycznej,
którego omawianie wykracza poza zakres tej książki, jednak opisywane algorytmy
haszowania, w których wykorzystano wiedzę opartą na tej teorii, są dość proste i p o
wszechnie stosowane. Za pomocą haszowania m ożna zaimplementować w tablicach
symboli wyszukiwanie i wstawianie, które w typowych zastosowaniach wymagają
stałego (po amortyzacji) czasu na operację. Dlatego jest to m etoda w wielu sytuacjach
stosowana z wyboru do implementowania podstawowych tablic symboli.
470
3.4 □ Tablice z haszowaniem 471
F u n k c je h a s z u ją c e Pierwszy problem związany jest z obliczaniem funkcji ha-
szującej, która przekształca klucze na indeksy tablicy. Jeśli istnieje tablica mieszcząca
M par klucz-wartość, potrzebna jest funkcja haszująca, która potrafi przekształcić
dowolny klucz na indeks tej tablicy, czyli liczbę całkowitą z przedziału [0, M - 1],
Szukamy funkcji haszującej, która jest łatwa do obliczenia i zapewnia równomierny
rozkład kluczy. Dla każdego klucza wystąpienie dowolnej liczby całkowitej z prze
działu od 0 do M - 1 powinno być równie prawdopodobne (dla każ
Skrót Skrót
dego klucza z osobna). Ta idealna sytuacja jest nieco tajemnicza. Aby Klucz (/W=100) (M = 97)
zrozumieć haszowanie, warto zacząć od zastanowienia się nad tym, 212 12 18
jak zaimplementować taką funkcję. 618 18 36
Funkcja haszująca zależy od typu klucza. Ujmijmy to ściśle — dla 302 2 11
każdego używanego typu klucza potrzebna jest inna funkcja haszu 940 40 67
702 2 23
jąca. Jeśli klucz obejmuje liczbę, na przykład num er PESEL, m oż
704 4 25
na zacząć od tej wartości. Jeżeli klucz zawiera łańcuch znaków, taki 612 12 30
jak nazwisko osoby, trzeba przekształcić łańcuch znaków na liczbę. 606 6 24
Klucze składające się z wielu części, na przykład adresy pocztowe, 772 72 93
trzeba w jakiś sposób połączyć. Dla wielu często stosowanych ty 510 10 25
pów kluczy m ożna wykorzystać domyślne implementacje dostępne 423 23 35
650 50 68
w Javie. Pokrótce omawiamy możliwe implementacje dla różnych
317 17 26
typów kluczy. Pomoże Ci to zobaczyć, jak wygląda taka implementa 907 7 34
cja. Dla tworzonych przez siebie typów kluczy będziesz musiał sam 507 7 22
zapewnić implementacje. 304 4 13
714 14 35
Typowy p rzykła d Załóżmy, że w aplikacji kluczami są amerykańskie 857 57 81
num ery ubezpieczenia społecznego. Taki numer, na przykład 123-45- 801 1 25
6789, to dziewięciocyfrowa liczba podzielona na trzy pola. Pierwsze 900 0 27
określa obszar geograficzny, w którym przydzielono dany numer 413 13 25
701 1 22
(przykładowo, num ery ubezpieczenia społecznego, gdzie pierwsze
418 18 30
pole m a wartość 035, pochodzą z Rhode Island, a num ery o pierw 601 1 19
szym polu 214 przydzielono w Maryland). Dwa pozostałe pola iden
Haszowanie modularne
tyfikują daną osobę. Istnieje miliard (109) różnych numerów ubezpie
czenia społecznego, załóżmy jednak, że w aplikacji trzeba przetwarzać
tylko kilkaset kluczy, dlatego można użyć tablicy z haszowaniem o wielkości M = 1000.
Możliwym sposobem na zaimplementowanie funkcji haszującej jest użycie trzech cyfr
z klucza. Lepiej zastosować trzy cyfry z trzeciego pola niż z pierwszego (ponieważ użyt
kownicy mogą nie być równomiernie rozproszeni geograficznie), jednak jeszcze lepiej
użyć wszystkich dziewięciu cyfr jako wartości typu i nt, a następnie zastanowić się nad
opisanymi dalej funkcjami haszującymi dla liczb całkowitych.
D odatnie liczby całkow ite Najczęściej stosowaną m etodą haszowania liczb całkowi
tych jest haszowanie modularne. Jako rozmiar tablicy należy wybrać liczbę pierwszą
M i dla dowolnej dodatniej liczby całkowitej k obliczyć resztę z dzielenia k przez M.
Funkcję tę m ożna obliczyć w bardzo łatwy sposób (k % Mw Javie). Ponadto pozwala
skutecznie rozdzielić klucze równomiernie między 0 a M - 1. Jeśli M nie jest liczbą
472 RO ZD ZIA Ł 3 □ W yszukiw anie
pierwszą, może się okazać, że nie wszystkie bity klucza są uwzględniane, co prowadzi
do tego, że niemożliwy staje się równomierny podział wartości. Jeśli kluczami są na
przykład liczby o podstawie 10, a M to lOk, wtedy używanych będzie tylko k najmniej
znaczących cyfr. W ramach prostego przykładu sytuacji, w której wybór liczby różnej
niż pierwsza może prowadzić do problemów, przyjmijmy, że klucze to num ery kie
runkowe, a M = 100. Z przyczyn historycznych środkowa cyfra w większości kodów
w Stanach Zjednoczonych to 0 lub 1 , dlatego w podanym rozwiązaniu faworyzowane
są wartości poniżej 20, natomiast zastosowanie liczby pierwszej 97 pozwala lepiej
rozdzielić dane (jeszcze lepsza byłaby liczba pierwsza bardziej oddalona od 100 ).
Także adresy IP są liczbami binarnymi, które z przyczyn historycznych (podobnie
jak num ery kierunkowe) nie są losowe, dlatego rozmiar tablicy powinien być liczbą
pierwszą (a przede wszystkim nie być potęgą dwójld), jeśli chcemy zastosować haszo-
wanie m odularne do podziału adresów.
Liczby zm iennoprzecinkow e Jeśli kluczami są liczby rzeczywiste z przedziału od 0 do
1, można pomnożyć je przez M i zaokrąglić do najbliższej liczby całkowitej, aby uzyskać
indeks z przedziału od 0 do M - 1. Choć podejście to jest intuicyjne, ma wadę, ponieważ
większą wagę przypisuje się tu najbardziej znaczącym bitom kluczy. Najmniej znaczące
bity nie mają znaczenia. Jednym z rozwiązań jest użycie haszowania modularnego na
binarnej reprezentacji klucza (to podejście zastosowano w Javie).
Łańcuchy znaków Haszowanie m odularne działa też dla długich kluczy, talach jak
łańcuchy znaków. Można traktować je jak duże liczby całkowite. Przykładowy kod
pokazany po lewej oblicza funkcję haszowania m odularnego dla zmiennej s typu
String. Przypominamy, że m etoda charAt() zwraca wartość typu char Javy, czyli
16-bitową nieujemną liczbę całkowitą. Jeśli R
in t hash = 0;
jest większe niż wartość jakiegokolwiek znaku,
f o r (in t i = 0; i < s . 1 en gth(); i++)
hash = (R * hash + s .c h a r A t (i)) % M; obliczenia odbywają się tak, jakby wartość typu
S tring potraktowano jako N-cyfrową liczbę
Haszowanie klucza w postaci łańcucha znaków całkowitą o podstawie R. Metoda oblicza resztę
z dzielenia tej liczby przez M. Klasyczny algorytm
(metoda Homera) wykonuje to zadanie za pom ocą N operacji mnożenia, dzielenia
i dzielenia modulo. Jeśli wartość R jest odpowiednio mała, przez co nie następuje
przepełnienie, wynikiem jest — zgodnie z potrzebami — liczba całkowita pomiędzy
0 a M-l. Zastosowanie małej pierwszej liczby całkowitej, na przykład 31, gwarantuje,
że wszystkie bity każdego znaku są uwzględniane. W Javie w domyślnej im plementa
cji dla typu S tri ng wykorzystano podobną metodę.
Klucze złożone Jeśli typ lducza obejmuje kilka pól całkowitoliczbowych, zwykle
m ożna je połączyć w sposób opisany dla wartości typu String. Załóżmy, że klucz
wyszukiwania ma typ Date, obejmujący trzy pola całkowitoliczbowe: day (dwie cyfry
określające dzień), month (dwie cyfry określające miesiąc) i year (cztery cyfry okre
ślające rok). Należy obliczyć wartość:
in t hash = (((day * R + month) % M ) * R + year) % M;
3.4 □ Tablice z haszowaniem 473
Jeśli Rjest odpowiednio małe, tak aby nie nastąpiło przepełnienie, uzyskana wartość
to liczba całkowita pomiędzy 0 a M-l (zgodnie z potrzebami). Tu m ożna uniknąć
wewnętrznej operacji % Mprzez wybranie dla Rumiarkowanie dużej liczby pierwszej,
na przykład 31. Metodę tę, podobnie jak dla łańcuchów znaków, m ożna uogólnić, tak
aby obsługiwała dowolną liczbę pól.
Konwencje stosowane w Javie Java pomaga rozwiązać podstawowy problem (po
legający na tym, że każdy typ danych wymaga funkcji haszującej) przez to, że każdy
typ danych dziedziczy funkcję hashCode(), która zwraca 32-bitową liczbę całkowitą.
Implementacja metody hashCode() w typie danych musi być spójna względem meto
dy equals. Oznacza to, że jeśli wyrażenie [Link](b) ma wartość true, wywołanie
[Link]() musi zwracać tę samą wartość, co [Link](). Natomiast jeżeli warto
ści funkcji hashCode() są różne, wiadomo, że obiekty nie są sobie równe. Jeśli wartości
funkcji hashCode () są identyczne, obiekty mogą, ale nie muszą być równe. Trzeba użyć
metody equal s (), aby to ustalić. To podejście trzeba zastosować w każdym kliencie,
aby móc używać metody hashCode() dla tablic symboli. Warto zauważyć, że wynika
z tego, iż trzeba przesłonić obie metody, hashCode() i equal s(), jeśli haszowanie ma
działać dla typu zdefiniowanego przez użytkownika. Domyślna implementacja zwraca
adres maszynowy obiektu reprezentującego klucz. Rzadko jest to odpowiednia war
tość. Dla wielu często używanych typów (w tym S tri ng, Integer, Doubl e, Fi 1e i URL)
Java udostępnia implementacje metody hashCode () przesłaniające domyślną metodę.
Przekształcanie wartości fu n k c ji hashCode() na indeks tablicy Ponieważ celem
jest uzyskanie indeksu tablicy, a nie 32-bitowej liczby całkowitej, w implementacjach
łączymy wartość funkcji hashCode () z haszowaniem m odularnym, aby otrzymać
liczbę całkowitą pomiędzy 0 a M-l. Odbywa się to tak:
private in t hash(Key x)
{ return ([Link] & 0 x 7 f f f f f f f ) % M; }
Ten kod maskuje bit znaku (aby przekształcić 32-bitową liczbę w 31-bitową nieujemną
liczbę całkowitą), a następnie oblicza resztę z dzielenia przez M, tak jak w haszowa-
niu modularnym. Programiści przy stosowaniu podobnego kodu często używają liczb
pierwszych jako rozmiaru tablicy (M). Jest to próba uwzględnienia wszystkich bitów
skrótu. Uwaga: aby uniknąć niejednoznacz- ^ s E A R c H X M P L
ności, w przykładach dotyczących haszowania skrót(M=5) 2 0 0 4 4 4 2 4 3 3
pomijamy wszystkie obliczenia tego rodzaju, Skrót(M=i6) 6 10 4 14 5 4 15 l 14 6
a w zamian używamy wartości skrótów poda- Wartośd skrótów k|uczy stoSowane w przykładach
nych w tabeli po prawej stronie.
M etoda hashC ode() definiowana p rzez użytkow nika W kodzie klienta można
oczekiwać, że m etoda has hCode () rozdziela wszystkie klucze równomiernie między
możliwe 32-bitowe wartości. Oznacza to, że dla dowolnego obiektu x m ożna napisać
x. hashCode () i — w zasadzie — z równym prawdopodobieństwem oczekiwać jednej
z 232 możliwych 32-bitowych wartości. W Javie implementacje m etody hashCode ()
dla typów S t r i ng, Integer, Doubl e, Fi 1e i URL mają działać w ten sposób. Dla własne-
474 RO ZD ZIA Ł 3 o W yszukiwanie
p u b lic c la s s Transaction
go typu danych trzeba samodzielnie spróbować
f uzyskać ten efekt. Przykład dla typu Date przed
stawiony na stronie 472 to jedno z możliwych
p riv a te final S t r in g who;
rozwiązań — tworzenie liczb całkowitych ze
p riv a te final Date when;
p riv a te final double amount; zmiennych egzemplarza i stosowanie haszowa
nia modularnego. W Javie konwencja, zgodnie
p u b lic in t hashCode()
z którą wszystkie typy danych dziedziczą metodę
{
in t hash = 17; hashCode (), pozwala zastosować jeszcze prostsze
hash = 31 * hash + [Link]() ; podejście. Można użyć m etody hashCode() na
hash = 31 * hash + [Link]() ; zmiennych egzemplarza, aby przekształcić każdą
hash = 31 * hash
+ ((Double) amount).hashCode()
z nich w 32-bitową wartość typu i nt, a następnie
return hash; wykonać operacje arytmetyczne, co pokazano po
1 lewej dla typu Transaction. Warto zauważyć, że
zmienne egzemplarza typu prostego trzeba zrzu
tować na typ nakładkowy, aby móc użyć m etody
hashCode(). Także tu konkretna wartość używa
Implementowanie metody hashCodeQ w typie
zdefiniowanym przez użytkownika na w m nożeniu (w przykładzie jest to 31) nie ma
większego znaczenia.
Programowa pam ięć podręczna Jeśli obliczanie skrótów jest kosztowne, czasem warto
zapisać w pamięci podręcznej skrót każdego klucza. Polega to na przechowywaniu w obiek
tach typu klucza zmiennej egzemplarza hash obejmującej wartość funkcji hashCode () dla
każdego obiektu klucza (zobacz ć w i c z e n i e 3 .4 .25 ). Przy pierwszym wywołaniu metody
hashCode() trzeba obliczyć skrót (i wartość zmiennej hash), natomiast w późniejszych
wywołaniach wystarczy zwrócić obliczoną wartość. W Javie zastosowano tę technikę do
zmniejszenia kosztów obliczania funkcji hashCode () dla obiektów typu S tri ng.
po d su m u jm y — trzeba s p e ł n ić trzy g łó w n e w a r u n k i, aby zaimplementować
dobrą funkcję haszującą dla typu danych. Funkcja powinna:
0 być spójna (równe klucze muszą mieć ten sam skrót);
11 działać wydajnie;
■ równomiernie rozdzielać klucze.
Spełnienie wszystkich trzech warunków jest zadaniem dla ekspertów. Podobnie jak
w przypadku wielu innych wbudowanych mechanizmów, programiści Javy stosujący
haszowanie zakładają, że funkcja hashCode() działa poprawnie, jeśli nie ma dowo
dów na to, iż jest inaczej.
Mimo to należy zachować ostrożność przy stosowaniu haszowania w sytuacjach,
w których wysoka wydajność jest kluczowa. Zastosowanie nieodpowiedniej funkcji
haszującej to klasyczny przykład błędu z obszaru wydajności. Kod działa wtedy p o
prawnie, ale znacznie wolniej, niż oczekiwano. Prawdopodobnie najprostszym sposo
bem na zagwarantowanie równomiernego podziału jest upewnienie się, że wszystkie
bity klucza są równie istotne przy obliczaniu każdej wartości skrótu. Prawdopodobnie
najczęstszy błąd przy implementowaniu funkcji haszujących to pominięcie dużej
liczby bitów klucza. Jeśli wydajność m a znaczenie, to niezależnie od implementacji
3 .4 Tablice z haszowaniem 475
110 = 10679/97
2348485323484853532323484848485348532323535323532348532323
0 W a r t o ś ć k lu c z a
Liczba wystąpień wartości skrótów dla słów z książki ToleofTwo Cities (10 679 kluczy, M = 97)
warto przetestować każdą używaną funkcję haszującą. Co zajmuje więcej czasu: obli
czenie funkcji haszującej czy porównanie dwóch kluczy? Czy funkcja haszująca dzieli
typowy zbiór kluczy równomiernie między wartości od 0 do M - 1? Przeprowadzenie
prostych eksperymentów, które dają odpowiedzi na te pytania, może zabezpieczyć
twórców przyszłych klientów przed nieprzyjemnymi niespodziankami. Na powyż
szym histogramie pokazano, że opracowana przez nas implementacja metody hash ()
oparta na metodzie hashCode () typu danych S tring Javy prowadzi do sensownego
rozkładu słów z pliku z książką Tale ofTwo Cities.
Omówienie to oparte jest na podstawowym założeniu, przyjmowanym przy stoso
waniu haszowania. Przyjmujemy wyidealizowany model, którego nie spodziewamy
się zrealizować, ale który mimo to wyznacza sposób myślenia przy implementowaniu,
algorytmów haszowania. Oto to założenie.
Założenie J (założenie o równomiernym haszowania). Funkcje haszujące rów
nomiernie i niezależnie rozdzielają klucze między całkowitoliczbowe wartości
z przedziału od 0 do M - 1.
Omówienie. Z uwagi na arbitralne wybory z pewnością nie korzystamy z funk
cji, które dzielą klucze w równomierny i niezależny sposób w matematycznym
tych słów znaczeniu. Kwestia implementacji spójnych funkcji, które gwarantują
równomierny i niezależny podział kluczy, prowadzi do dogłębnych teoretycz
nych badań. Wynika z nich, że utworzenie takiej funkcji, która w dodatku jest
łatwa do obliczania, to cel bardzo trudny do osiągnięcia. W praktyce, podobnie
jak w przypadku liczb losowych generowanych przez metodę M [Link](),
większość programistów zadowala się funkcjami haszującymi, których nie m oż
na łatwo odróżnić od prawdziwie losowych. Jednak tylko nieliczni programiści
sprawdzają niezależność. Cecha ta występuje rzadko.
m im ot r u d n o ś c i z p o t w i e r d z e n i e m z a ł o ż e n i a j jest ono przydatnym sposobem
myślenia o haszowaniu. Wynika to z dwóch podstawowych powodów. Po pierwsze,
w czasie projektowania funkcji haszujących założenie wyznacza wartościowy cel i za
pobiega podejmowaniu arbitralnych decyzji, które mogłyby doprowadzić do nad
miernej liczby kolizji. Po drugie, choć potwierdzenie samego założenia może być nie
możliwe, można zastosować analizę matematyczną do opracowania hipotez na temat
wydajności algorytmów haszowania i sprawdzić je eksperymentalnie.
476 RO ZD ZIA Ł 3 □ W yszukiwanie
Haszowaeie metodą łańcuchową Funkcja buszująca przekształca klucze na in
deksy tablicy. Drugim składnikiem algorytmu haszowania jest mechanizm rozwiązywa
nia kolizji. Jest to strategia obsługi sytuacji, w których sieroty dwóch wstawianych kluczy
(lub większej ich liczby) określają ten sam indeks. Prostym i ogólnym sposobem roz
wiązywania kolizji jest zbudowanie dla każdego z M indeksów tablicy listy powiązanej
obejmującej pary klucz-wartość, w których skrót klucza odpowiada danemu indeksowi.
Ta metoda nazywana jest metodą łańcuchową, ponieważ elementy powodujące kolizję
są połączone w łańcuchy na odrębnych listach powiązanych. Pomysł polega na tym, aby
wybrać M na tyle duże, żeby hsty były wystarczająco krótkie i pozwalały na wydajne wy
szukiwanie za pomocą dwuetapowego procesu — obliczenia skrótu w celu znalezienia
listy, która może obejmować klucz, i sekwencyjnego wyszukania klucza na liście.
Jedną z możliwości jest rozwinięcie klasy SequentialSearchST ( a l g o r y t m 3 . 1 )
w celu zaimplementowania m etody łańcuchowej za pom ocą prostych list powiąza
nych (zobacz ć w i c z e n i e 3 .4 .2 ). Prostsze, choć nieco mniej wydajne rozwiązanie
polega na zastosowaniu ogólniejszego podejścia. Dla każdego z M indeksów tablicy
można zbudować tablicę symboli z kluczami, których skróty odpowiadają danemu
indeksowi. Pozwala to ponownie wykorzystać opracowany już kod. Implementacja
klasy SeperateChainingHashST ( a l g o r y t m 3 .5 ) oparta jest na tablicy obiektów
Sequential SearchST. Metody get () i put () zaimplementowano przez obliczanie
funkcji haszującej określającej, który obiekt Sequential SearchST może obejmować
klucz. Następnie, w celu ukończenia zadania, używana jest m etoda get () lub put ()
z klasy Sequenti al SearchST.
Ponieważ istnieje M list i Nkluczy, średnia długość list zawsze wynosi N/M . Nie ma
tu znaczenia rozkład kluczy między listy. Załóżmy na przykład, że wszystkie elementy
znajdują się na pierwszej liście. Średnia długość list wynosi (N+0+0+0+...+0)/M = N /
M. Niezależnie od rozkładu kluczy między listy suma długości list wynosi N, a śred
nia długość — N/M . M etoda łańcuchowa jest przydatna w praktyce, ponieważ dla
każdej listy bar
Klucz Skrót Wartość
s dzo prawdopo
2 0
TTTstT dobne jest, że bę
E 0 1
dzie obejmowała
A 0 7
N /M par klucz-
R 4 3 Ti rst.
nuli Niezależne obiekty typu
wartość. W typo
C 4 4 0 S e q u e n t!a lS e a r c h S T wych sytuacjach
H 4 5 1 Ti r s t . można zweryfi
2 X 7 S 0
E 0 6 kować ten wnio
3
X 2 7
4
sek Z Z A Ł O Ż E N IA J
Ti r s t ^
A 0 8 L 11 P 10 i spodziewać się
szybkiego działa
M 4 9
Ti r s t ^ nia wyszukiwania
X
P 3 10 M 9 H jr C 4 — R 3
oraz wstawiania.
L 3 11
E 0 12
Haszowanie metodą łańcuchową w standardowym kliencie używającym indeksu
3.4 Tablice z haszowaniem 477
ALGORYTM 3.5. Haszowanie metodą łańcuchową
public cla ss SeparateChainingHashST<Key, Value>
{
private in t N; // Liczba par klucz-wartość.
private in t M; // Rozmiar ta blicy z haszowaniem.
private SequentialSearchST<Key, Value>[] st; // Tablica obiektów ST.
public SeparateChaini ngHashST()
{ t h i s (997); }
public SeparateChainingHashST(int M)
{ // Tworzy M l i s t powiązanych,
thi s.M = M;
st = (SequentialSearchST<Key, V a lue>[]) new SequentialSearchST[M];
fo r (in t i = 0; i < M; i++)
st [i] = new SequentialSearchST();
}
p rivate in t hash(Key key)
{ return ([Link] & 0 x 7 f f f f f f f ) % M; }
public Value get(Key key)
{ return (Value) s t[ h a s h (k e y ) ]. g e t (k e y ); }
public void put(Key key, Value val)
{ st[h ash (key)].pu t(key, v a l); }
public Iterable<Key> keys()
// Zobacz ćwiczenie 3.4.19.
}
W tej implementacji podstawowej tablicy symboli przechowywana jest tablica list powiąza
nych, a do wyboru listy dla każdego klucza służy funkcja haszująca. Dla uproszczenia wyko
rzystano metody z klasy Sequenti al SearchST. Przy tworzeniu tablicy s t [] potrzebne jest rzu
towanie, ponieważ Java nie zezwala na tworzenie tablic dla typów generycznych. Konstruktor
domyślny tworzy 997 list, tak więc dla dużych tablic kod działa około 1000 razy szybciej niż
klasa Sequenti al SearchST. To szybkie rozwiązanie jest łatwym sposobem na osiągnięcie wy
sokiej wydajności, jeśli znana jest przybliżona liczba par klucz-wartość dodawanych za pomocą
metody put () przez klienta. Lepszym rozwiązaniem jest zmienianie wielkości tablicy, co nieza
leżnie od liczby par klucz-wartość pozwala zagwarantować, że listy będą krótkie (zobacz stronę
486 i ć w i c z e n i e 3 .4 . 18 ).
478 R O ZD ZIA Ł 3 □ W yszukiwanie
Twierdzenie K. Przy stosowaniu m etody łańcuchowej dla tablicy z haszowa-
niem o M listach i N kluczach prawdopodobieństwo (przy z a ł o ż e n i u j), że licz
ba kluczy na liście mieści się w małej wielokrotności ilorazu N /M , jest bardzo
bliskie 1 .
Zarys dowodu, z a ł o ż e n i e j sprawia, że można zastosować klasyczną teorię
prawdopodobieństwa. Przedstawiamy zarys dowodu dla czytelników zaznajo
mionych z podstawami analiz probabilistycznych. Prawdopodobieństwo, że dana
lista obejmuje dokładnie k kluczy, jest wyznaczane przez rozkład dwumianowy.
ot
Rozkład dwumianowy (N = 104, M = 103, a = 10)
Wynika to z opisanego wnioskowania — najpierw należy wybrać k z N kluczy. Te
k kluczy trafia na daną listę z prawdopodobieństwem 1/M, a pozostałych N - k
kluczy nie trafia na nią z prawdopodobieństwem 1 - (1/M). Za pomocą a = N /M
można zapisać wyrażenie w następujący sposób:
N -k
( ? ) ( * ) * ( » - #
Dla małego a dobrym przybli- (10;0,12572...)
-0,125
żeniem tego wyrażenia jest roz
kład Poissona:
a ke-n o
1 1
io
1
20
1
30
]ę 1 Rozkład Poissona (N = 104, M = 103, a = 10)
Wynika z tego, że prawdopodobieństwo, iż lista obejmuje więcej niż t a kluczy,
jest ograniczone wartością (a e/t)le~a. Dla spotykanych w praktyce parametrów
prawdopodobieństwo to jest niezwykle małe. Przykładowo, jeśli średnia dłu
gość list wynosi 10 , prawdopodobieństwo, że na jednej z nich znajdzie się ponad
20 kluczy, jest mniejsze niż (10 e/2) 2e 10 ~ 0,0084. Dla list o średniej długości 20
prawdopodobieństwo, że lista będzie obejmować 40 kluczy, wynosi mniej niż
(20 e/2) 2eJ0 = 0,0000016. Wynik ten nie gwarantuje, że każda lista będzie kró t
ka. W iadomo, że dla stałego a średnia długość najdłuższej listy rośnie w tempie
log N / log log N.
3.4 o Tablice z haszowaniem 479
Klasyczna analiza matematyczna jest atrakcyjna, jednak należy zauważyć, że wnio
skowanie całkowicie zależy od z a ł o ż e n i a j . Jeśli funkcja haszująca nie działa równo
m iernie i niezależnie, koszt wyszukiwania i wstawiania może być proporcjonalny do
N (nie jest więc niższy niż dla wyszukiwania sekwencyjnego), z a ł o ż e n i e j jest dużo
mocniejsze niż odpowiadające m u założenia dla innych omawianych algorytmów
probabilistycznych, a także dużo trudniejsze do zweryfikowania. Przy haszowaniu
zakładamy, że skrót każdego klucza, niezależnie jak złożonego, z równym prawdopo
dobieństwem będzie odpowiadał jednem u z M indeksów. Nie da się w ramach ekspe
rymentów sprawdzić każdego możliwego klucza, dlatego trzeba przeprowadzić bar
dziej zaawansowane badania, obejmujące losowe próbki ze zbioru możliwych kluczy
używanych w aplikacji, a następnie wykonać analizy statystyczne. Jeszcze lepsze jest
wykorzystanie samego algorytmu jako części testu w celu potwierdzenia zarówno
z a ł o ż e n i a j, jak i wynikających z niego wyników matematycznych.
Cecha L. Przy stosowaniu metody łańcuchowej dla tablicy z haszowaniem
0 M listach i N kluczach liczba porównań (testów równości) przy nieudanym
wyszukiwaniu i wstawianiu wynosi -N IM .
Dowód. Uzyskanie wysokiej wydajności algorytmów w praktyce nie wymaga,
aby funkcja haszująca zapewniała w pełni równomierny rozkład w technicznym
sensie opisanym w z a ł o ż e n i u j. Od lat 50. ubiegłego wieku niezliczeni progra
miści obserwowali przyspieszenie prognozowane na podstawie t w i e r d z e n i a k ,
1 to nawet dla funkcji haszujących, które z pewnością nie dają rozkładu rów
nomiernego. Przykładowo, na diagramie na stronie 480. pokazano, że rozkład
długości list w przykładowym programie FrequencyCounter (z wykorzystaniem
implementacji m etody hash() opartej na metodzie hashCode() dla typu String
Javy) precyzyjnie pasuje do modelu teoretycznego. Wyjątkiem jest (wielokrotnie
udokumentowana) niska wydajność wynikająca z zastosowania funkcji haszu
jących, w których nie uwzględniono wszystkich bitów kluczy. Jednak większość
dowodów uzyskana na podstawie doświadczeń praktyków pozwala bezpiecznie
stwierdzić, że haszowanie z wykorzystaniem metody łańcuchowej i tablicy o M
elementach przyspiesza wyszukiwanie i wstawianie w tablicy symboli M razy.
Wielkość tablicy W implementacji z metodą łańcuchową celem jest wybór rozmia
ru tablicy (M) w talu sposób, aby była na tyle mała, że nie prowadzi do marnowania
dużych fragmentów ciągłej pamięci na puste łańcuchy, a przy tym na tyle duża, że
nie trzeba tracić czasu na przeszukiwanie długich łańcuchów. Jedną z zalet metody
łańcuchowej jest to, że wybór długości tablicy nie ma krytycznego znaczenia. Jeśli
pojawi się więcej kluczy, niż oczekiwano, wyszukiwanie potrwa trochę dłużej, niż
gdyby utworzono większą tablicę. Jeżeli kluczy będzie mniej, wyszukiwanie będzie
bardzo krótkie, ale stanie się to kosztem zmarnowanej pamięci. Kiedy pamięci jest
dużo, można wybrać wystarczająco duże M, aby czas wyszukiwania był stały. Jeśli
480 RO ZD ZIA Ł 3 a W yszukiw anie
D ł u g o ś c i list (10 6 7 9 k lu cz y, M = 9 9 7)
Długości list w wywołaniu FrequencyC ounter 8 < t a l e . t x t z wykorzystaniem klasy separateC hainingH ashST
ilość pamięci jest ograniczona, nadal można zwiększyć wydajność M razy, ustawiając
tak duże M, na jakie m ożna sobie pozwolić. Na rysunku poniżej pokazano na przy
kładzie programu FrequencyCounter spadek średniego kosztu z tysięcy porównań na
operację dla klasy Sequenti al SearchST do małej stałej dla klasy SeperateChai ni ngST.
Jest to zgodne z oczekiwaniami. Inna możliwość to zmienianie długości tablicy w celu
zachowania krótkich list (zobacz ć w i c z e n i e 3 .4 .1 8 ).
Usuwanie Aby usunąć parę klucz-wartość, wystarczy określić skrót w celu znalezienia
obiektu Sequential SearchST zawierającego klucz, a następnie wywołać metodę dele
te () na tej tablicy (zobacz ć w i c z e n i e 3 .1 .5 ). Lepiej powtórnie wykorzystać kod w ten
sposób, niż ponownie implementować podstawowe operacje na liście powiązanej.
Operacje na kluczach uporządkow anych Głównym celem haszowania jest równo
m ierne rozłożenie kluczy, dlatego jakakolwiek kolejność zostaje w trakcie haszowa
nia utracona. Jeśli trzeba szybko znaleźć klucz minim alny lub maksymalny, znaleźć
klucze z danego przedziału lub zaimplementować inne operacje z interfejsu API dla
uporządkowanej tablicy symboli (strona 378), haszowanie nie jest odpowiednim roz
wiązaniem, ponieważ operacje te będą działać liniowo.
jest łatwą do napisania i prawdopodobnie naj
h a s z o w a n ie m e t o d ą ł a ń c u c h o w ą
szybszą (oraz najczęściej stosowaną) implementacją tablicy symboli w zastosowa
niach, w których kolejność kluczy jest nieistotna. Jeśli typ kluczy to jeden z wbudo
wanych typów Javy lub własny typ o dobrze przetestowanej implementacji metody
hashCode(), a l g o r y t m 3.5 zapewnia szybki i łatwy sposób wyszukiwania oraz wsta
wiania. Dalej omawiamy inną, też skuteczną, metodę rozwiązywania kolizji.
3.4 ■ Tablice z haszowaniem 481
H a s z o w a n ie z w y k o r z y s t a n ie m p r ó b k o w a n ia lin io w e g o Inne podejście
do haszowania polega na zapisaniu N par lducz-wartość w tablicy z haszowaniem
o rozmiarze M > N i wykorzystaniu pustych pozycji w tablicy do rozwiązywania koli
zji. Metody z tej grupy to techniki haszowania z adresowaniem otwartym.
Najprostszą techniką z adresowaniem otwartym jest próbkowanie liniowe. Jeśli
wystąpi kolizja (kiedy ustalony skrót odpowiada indeksowi tablicy zajętemu już
przez inny klucz), wystarczy zwiększyć indeks i sprawdzić następną pozycję tablicy.
W próbkowaniu liniowym występują trzy możliwe skutki:
■ Klucz jest równy kluczowi wyszukiwania — wyszukiwanie jest udane.
■ Pozycja jest pusta (na pozycji o danym indeksie znajduje się nuli) — wyszuki
wanie jest nieudane.
a Klucz nie jest równy kluczowi wyszukiwania — należy sprawdzić następną po-
zycję.
Należy obliczyć skrót klucza odpowiadający indeksowi tablicy, sprawdzić, czy klucz
wyszukiwania pasuje do klucza z danej pozycji, i kontynuować proces (przez zwięk
szanie indeksu i przechodzenie na początek tablicy po dojściu do końca) do m o
mentu znalezienia lducza wyszukiwania lub pustego elementu. Operację określania,
czy na danej pozycji znajduje się element o kluczu równym kluczowi wyszukiwania,
nazywa się czasem próbkowaniem. Stosujemy tę nazwę wym iennie z określeniem
porównywanie (którego używaliśmy wcześniej), choć niektóre operacje próbkowania
to testy wartości nuli.
Klucz Skrót Wartość 0 1 2 3 4 5 6 7 9 10 1 1 12 13 14 15
S 6 0
1 I 1 l s i l i l i 1 1— 1
1 1 0 ___1 1
1 S ! _ s E : j I
E 10 zerwone
elementy \
0 i 1 Szare i i
i { \ elbmepty ri\e
A 4 7 sq powę kA s 1 I Z -
_ 1 f 2 0 l- K ^ sąsprawdźane
i i
R 14 A s II 1 R I
z z l ___i 2 0 . 1L_ ___ 1 3 1
C 5 4 fiL/irnó z r C ZT 1E 1 1 Pk]
elementy sq 2 5 0 1 1 1 ___I___ L 3 J __
H 4 5 sorowclzane z r
i
c s ZT| I 1 E I 1 I~ r 1
1 1 5 0 5 i j 1 ’ ___ 1 13 1
E 10 fi
u n r A c s H. E 1 r 1
i i 2 5 0 5 © 1 13 ]
7 __ r i_ A c s H E : 1 1R |X
15
i 1 5 0 5 6 1 1 3 |7
4 8 A C s H1 E ! i i ~R ~ n r
H ~ r ~
i i ® 5 0 51 6 "‘ 1 13 1 7
1 9 M i A C s H1 E r | r i x Próbkowanie
9 1 1 S 5 0 5~i r 6 | 1 1 3| 7 przechodzi
14 10 [ P M1 | A C s H1 i E | i i R Lx do elementu 0
10 9 1___L _ 8 5 0 H JI 6 1 13 1 7
p mT ! ~A 1 c s H 1L f E~|
6 11 i 1 R1x
10 9 1 1 8 5 0 5 11 161 1 1 3 lT
10 12
p “ Ml [Z ł A j T I ~s~| ~H~| L E 1 |R |X keys []
10 9 i 1 8 5 0 5 ;ii 1 1 3 I 7 v a ls [ ]
Ślad działania standardowego klienta używającego indeksu, korzystającego
z implementacji tablicy symboli opartej na próbkowaniu liniowym
482 R O ZD ZIA Ł 3 W yszukiw anie
ALGORYTM 3.6. Haszowanie z próbkowaniem liniowym
public c la s s LinearProbingHashST<Key, Value>
{
private in t N; // Liczba par klucz-wartość wta b lic y ,
private in t M = 16; // Rozmiar ta b lic y zpróbkowaniem liniowym,
private Key[] keys; // Klucze,
private Value[] va ls; // Wartości.
public LinearProbingHashST()
{
keys = ( Key []) new Object[M];
va ls = (ValueJJ) new Object[M];
}
private in t hash(Key key)
{ return ([Link]() & 0 x 7 f f f f f f f ) % M; }
private void re s iz e ( ) // Zobacz stronę 486.
public void put(Key key, Value val)
{
i f (N >= M/2) re size(2*M ); // Podwajanie M (zobacz opis w te k ście ),
i nt i ;
fo r (i = hash(key); keys[i] != n u li; i = (i + 1) % M)
i f (k e y s[ i ] .equals(key)) { v a l s [ i ] = val; return; }
keys [i] = key;
val s [ i ] = v a l ;
N++;
}
public Value get(Key key)
{
f o r (in t i = hash(key); k e y s [ i] != n u ll; i = (i + 1) % M)
i f ( k e y s [ i] .equals(key))
return v a l s [ i ] ;
return n u l1;
)
}
Ta implementacja tablicy symboli pozwala przechowywać klucze i wartości w równoległych
tablicach (tak jak w klasie Bi narySearchsST), przy czym używane są puste pozycje (ozna
czone jako nul 1), kończące grupy kluczy. Jeśli nowy klucz trafia na puste miejsce, jest tam
zapisywany. W przeciwnym razie należy sekwencyjnie przejrzeć tablicę w celu znalezienia
pustej pozycji. A b y znaleźć klucz, trzeba sekwencyjnie przejrzeć tablicę, począwszy od in
deksu wyznaczanego przez skrót, a skończywszy na nul 1 (chybienie) lub danym kluczu (tra
fienie). Implementacji metody keys () dotyczy ć w i c z e n i e 3 .4 .1 9 .
3.4 □ Tablice z haszowaniem 483
Oto podstawowy pomysł, na którym oparte jest haszowanie z adresowaniem ot
wartym — zamiast wykorzystywać pamięć na referencje z listy powiązanej, używamy
jej na puste miejsca w tablicy z haszowaniem, określające koniec ciągu próbkowania.
Jak widać w klasie Li nearProbi ngHashST (a l g o r y t m 3 .6 ), zastosowanie tej techniki
do zaimplementowania interfejsu API tablicy symboli jest całkiem proste. Należy za
stosować równoległe tablice, po jednej na klucze i wartości, oraz użyć funkcji haszu-
jącej jako indeksu dającego dostęp do danych w omówiony wcześniej sposób.
Usuwanie Jak przebiega usuwanie pary klucz-wartość z tablicy opartej na próbko
waniu liniowym? Jeśli pomyślisz przez chwilę o tej sytuacji, zobaczysz, że ustawienie
na pozycji klucza wartości nul 1 jest niedopuszczalne, ponieważ spowoduje przed
wczesne zakończenie wyszukiwania klucza wstawionego do tablicy później. Załóżmy
na przykład, że usuwamy w ten sposób C w przedstawionym przykładzie, a następ
nie szukamy H. Wartość skrótu dla H wynosi 4, jed
nak klucz znajduje się na końcu grupy, na pozycji 7. p u b lic void delete(Key key)
Po ustawieniu pozycji 5. na nuli m etoda g e t() nie {
i f ( ! c o n ta in s(k e y )) re turn;
znajdzie H. Trzeba więc ponownie wstawić do tablicy i n t i = h a sh (k e y );
wszystkie klucze z grupy znajdujące się na prawo od w h ile ( ! k e y .e q u a ls (k e y s [i]))
i = (i + 1) % M;
usuniętego klucza. Proces ten jest bardziej skompli
k e y s [i] = n u l l ;
kowany, niż może się wydawać, dlatego zachęcamy val s [ i ] = n u l l ;
do zbadania działania kodu pokazanego po prawej i = (i + 1) % M ;
jako przykładu jego przebiegu (zobacz ć w i c z e n i e w hile ( k e y s [ i] != n u ll)
{
3-4-17)- Key keyToRedo = keys [ i ];
Value valToRedo = val s [ i ];
k e y s [i] = n u l l ;
t a k jak w m e t o d z i e ł a ń c u c h o w e j , tak i w adre-
val s [i ] = n u ll;
so w a n iu otw artym w yd ajność haszow an ia zależy o d N— ;
sto su n k u a = N/M , je d n a k tu jest o n interpretow any put(keyToRedo, valToRedo);
w in n y sposób, a określa m y jako współczynnik zapeł i = (i + 1) % M;
}
nienia (ang. load factor) dla tablicy z haszow aniem . N— ;
W metodzie łańcuchowej a to średnia liczba kluczy i f (N > 0 N == M/8) re size (M /2 );
na listę i zwykle wynosi więcej niż 1 . W próbkowaniu
lin io w y m a określa część zajętych elem entów tablicy ,, . , ....
1 -1 ii 1 1 Usuwanie przy próbkowaniu liniowym
— wartość ta nigdy nie jest większa niż 1. W klasie
Li nearProbi ngHashST nie można dopuścić, aby współczynnik zapełnienia był równy
1 (tablica jest wtedy całkowicie zapełniona), ponieważ przy nieudanym wyszukiwa
niu w pełnej tablicy program wejdzie w pętlę nieskończoną. Z uwagi na wydajność
należy zmieniać długość tablicy w taki sposób, aby współczynnik zapełnienia wyno
sił pomiędzy jedną ósmą a jedną drugą. Skuteczność tej strategii potwierdzają analizy
matematyczne, które omawiamy przed przedstawieniem szczegółów implementacji.
484 RO ZD ZIA Ł 3 o W yszukiw anie
Grupowanie Średni koszt próbkowania liniowego zależy od sposobu, w jaki ele
menty są złączane w ciągi zajętych elementów tablicy (grupy lub klastry) w trakcie
wstawiania. Przykładowo, kiedy w przykładzie wstawiany jest klucz C, powstaje trzy-
elementowa grupa (A S C), co oznacza, że potrzebne są cztery testy w celu wstawie
nia H, ponieważ skrót klucza H odpowiada pierwszemu miejscu w grupie. Wysoka
wydajność wymaga, oczywiście, krótkich grup.
Prawdopodobieństwo,
że ro w y klucz znajdzie się Wraz z zapełnianiem się tablicy wymóg ten może
P rze d
w tej grupie, wynosi 9/64 być trudny do spełnienia, ponieważ długie grupy
•• |»«■»«■»»»|»■»■««»»■»»»» ••
są w niej częste. Ponadto ponieważ wszystkie po
Wtedy klucz trafia zycje tablicy z równym prawdopodobieństwem
do tej grupy
odpowiadają wartości skrótu następnego wsta
Prowadzi to do utworzenia
wianego klucza (przy założeniu o równomiernym
Po y / dużo dłuższej grupy haszowaniu), z większym prawdopodobieństwem
• •• | | •• ••• ■ • •• •
zostaną wydłużone długie niż krótkie grupy, po
Grupowanie w próbkowaniu liniowym (M = 64) nieważ nowy klucz o skrócie pasującym do pozy
cji w grupie powoduje jej zwiększenie o 1 (a cza
sem nawet o więcej, jeśli tylko jedna pozycja oddziela daną grupę od następnej).
Dalej zajmujemy się ilościowym opisem wpływu efektu grupowania na wydajność
w próbkowaniu liniowym i wykorzystaniem tej wiedzy do określenia parametrów
w implementacjach.
Próbkowanie liniowe Losowy rozkład
k e y s [8 0 6 4 ..8 1 9 2 ]
Tablica wzorców (2048 kluczy; tablice zapisane jako wiersze o 128 pozycjach)
3.4 o Tablice z haszowaniem 485
A n a liz a p r ó b k o w a n ia lin io w e g o M im o s to s u n k o w o p ro s te j f o r m y w y n ik ó w ,
d o k ła d n e a n a liz o w a n ie p ró b k o w a n ia lin io w e g o je s t b a r d z o tr u d n y m z a d a n ie m .
W y p ro w a d z e n ie w 1962 ro k u p rz e z K n u th a o p is a n y c h d a lej w z o ró w b y ło p r z e ło m e m
w d z ie d z in ie a n a liz y a lg o ry tm ó w .
Twierdzenie M. P rz y p ró b k o w a n iu lin io w y m w ta b lic y z h a s z o w a n ie m o M li
s ta c h i N = a M k lu c z a c h ś r e d n ia lic z b a p o tr z e b n y c h te s tó w (p r z y z a ł o ż e n i u j)
w y n o si:
~ - - ( l ~ — !— ) o r a z ~ r i + 7 i Z")
2 1 -a 2 (1- a )
o d p o w ie d n io d la u d a n e g o w y sz u k iw a n ia i n ie u d a n e g o w y sz u k iw a n ia (lu b w sta w ia
n ia). K ie d y a w y n o s i m n ie j w ięcej 1/2, ś r e d n ia lic z b a te s tó w p rz y u d a n y m w y sz u
k iw a n iu w y n o s i o k o ło 3 /2 , a d la n ie u d a n e g o w y sz u k iw a n ia — o k o ło 5 /2 . S z a c u n k i
sta ją się m n ie j p re c y z y jn e , k ie d y a zb liż a się d o 1 , je d n a k w te d y n ie są p o trz e b n e ,
p o n ie w a ż p ró b k o w a n ie lin io w e s to su je m y ty lk o d la a m n ie js z y c h n iż 1/ 2 .
Omówienie. Ś re d n ią u s ta la m y p rz e z o b lic z e n ie k o s z tó w n ie u d a n e g o w y s z u
k iw a n ia ro z p o c z ę te g o n a k a ż d e j p o z y c ji ta b lic y i p o d z ie le n ie s u m y p rz e z M .
K ażd e n ie u d a n e w y s z u k iw a n ie w y m a g a p r z y n a jm n ie j je d n e g o te s tu , d la te g o
lic z y m y lic z b ę p r ó b p o p ie r w s z y m teście. R o z w a ż m y d w a n a s tę p u ją c e s k ra jn e
p r z y p a d k i w ta b lic y z p r ó b k o w a n ie m lin io w y m , k tó r a je s t w p o ło w ie p e łn a
( M = 2N ). W n a jle p s z y m p r z y p a d k u p o z y c je ta b lic y o in d e k s a c h p a rz y s ty c h są
p u s te , a o n ie p a r z y s ty c h — zaję te. W n a jg o r s z y m — je d n a p o ło w a ta b lic y je s t
p u s ta , a d r u g a — z a ję ta . Ś re d n ia d łu g o ś ć g r u p w o b u s y tu a c ja c h w y n o s i N /{ 2 N )
= 1 / 2 , je d n a k ś r e d n ia lic z b a te s tó w p r z y n ie u d a n y m w y s z u k iw a n iu je s t ró w n a 1
(k a ż d e w y s z u k iw a n ie w y m a g a p rz y n a jm n ie j je d n e j p ró b y ) p lu s ( 0 + 1 + 0 + 1 +
...)/(2 N ) = 1/2 d la n a jle p s z e g o p r z y p a d k u o ra z 1 p lu s ( N + ( N - 1) + ...)/(2 N ) ~
N / 4 d la n a jg o rs z e g o p r z y p a d k u . To w n io s k o w a n ie m o ż n a u o g ó ln ić , a b y p o k a
zać, że ś r e d n ia lic z b a p r ó b p r z y n ie u d a n y m w y s z u k iw a n iu je s t p r o p o r c jo n a ln a
d o k w a d ra tó w d łu g o ś c i g ru p . Jeśli g r u p a m a d łu g o ś ć t, w y ra ż e n ie ( t + ( i - 1 ) + ...
+ 2 + 1 ) / A i = f ( f + l ) /( 2 M ) u w z g lę d n ia w k ła d tej g ru p y w su m ę . S u m a d łu g o ś c i
g ru p w y n o s i N , d la te g o p o d o d a n iu k o s z tó w d la k a ż d e j p o z y c ji w ta b lic y o k a z u je
się, że łą c z n y ś r e d n i k o s z t d la n ie u d a n e g o w y s z u k iw a n ia to s u m a 1 + JV/(2 Ai)
i s u m y k w a d ra tó w d łu g o ś c i g ru p p o d z ie lo n a p rz e z 2M . T a k w ię c n a p o d s ta w ie
ta b lic y m o ż n a sz y b k o o b lic z y ć ś r e d n i k o s z t n ie u d a n e g o w y s z u k iw a n ia (z o b a c z
ć w ic z e n ie 3 . 4 . 2 1 ). O g ó ln ie g r u p y p o w s ta ją w s k o m p lik o w a n y m d y n a m ic z n y m
p ro c e s ie ( o p a r ty m n a a lg o r y tm ie p ró b k o w a n ia lin io w e g o ), k tó r y t r u d n o o p is a ć
a n a lity c z n ie . Z a g a d n ie n ie to w y k ra c z a p o z a z a k re s k sią ż k i.
486 RO ZD ZIA Ł 3 a W yszukiw anie
z g o d n i e z t w i e r d z e n i e m M (p r z y s ta n d a r d o w y m z a ł o ż e n i u j ) m o ż n a o c z e k iw a ć ,
że w y s z u k iw a n ie w p ra w ie p e łn e j ta b e li b ę d z ie w y m a g a ć b a r d z o d u ż e j lic z b y p ró b
(w ra z ze z b liż a n ie m się a d o 1 w a rto ś c i w z o ró w o p is u ją c y c h lic z b ę p ró b sta ją się b a r
d z o d u ż e ). J e d n a k lic z b a p ró b w y n o s i m ię d z y 1,5 a 2,5, je ś li m o ż n a z a g w a ra n to w a ć ,
że w s p ó łc z y n n ik z a p e łn ie n ia a w y n o s i p o n iż e j 1/2. R o z w a ż m y te r a z w y k o rz y s ta n ie
z m ia n y w ie lk o ś c i ta b lic y w ty m celu .
Zmienianie wielkości tablicy M ożna w y k o rz y s ta ć s ta n d a r d o w ą t e c h n i
k ę z m ia n y w ie lk o ś c i ta b lic y (z o b a c z r o z d z i a ł i . ) , a b y z a g w a ra n to w a ć , że w s p ó ł
c z y n n ik z a p e łn ie n ia n ig d y n ie p r z e k r o c z y 1/2. N a jp ie r w tr z e b a u tw o rz y ć w k la s ie
Li nearProbi ngHashST n o w y k o n s tr u k to r ,
p riv ate void r e s i z e ( i n t cap) k tó r y p rz y jm u je ja k o a r g u m e n t olcre-
Í ślo n y r o z m ia r ta b lic y (d o k o n s tr u k to r a
LinearProbinqHashST<Key, Value> t ; , . , , ,
t = new Li nearProbi ng HashSKKey, Va lu e>(ca p ); z a lg o r y t m u 3.6 n a le ż y d o d a ć w ie rsz ,
fo r (in t i = 0 ; i < M; i++) k tó r y u s ta w ia M na pew ną w a rto ś ć
if (keys[i] != n u ll ) przed utw orzeniem tablic). Potrzebna
t. put (keys [ i ] , vals [i]), . t te¿ m et0 d a r e s iz e ( ) , przedstaw io-
keys = t .k e y s ; 1 , . , ,
vals = t .v a l s ; n a P ° lew ej, k tó r a tw o rz y n o w y o b ie k t
M = t.M; Li nearProbi ngHashST o d a n y m ro z m ia -
) rz e , u m ie s z c z a w sz y stk ie k lu c z e i w a rto -
.......................... .. ... , . ści z tablicy w nowej tablicy, a następnie
Zmienianie wielkości tablicy z haszowaniem ' 1 ' Tr
przy próbkowaniu liniowym p o n o w n ie o b lic z a s k r ó ty w s z y s tk ic h k lu
c z y p o d k ą te m n o w e j ta b lic y . Te d o d a tk i
p o z w a la ją z a im p le m e n to w a ć p o d w a ja n ie r o z m ia r u tab lic y . W y w o ła n ie m e to d y r e
s i z e () w p ie rw s z e j in s tr u k c ji w m e to d z ie put () g w a ra n tu je , że ta b lic a je s t n a jw y ż ej
w p o ło w ie p e łn a . K o d tw o rz y d w u k r o tn ie w ię k sz ą ta b lic ę z h a s z o w a n ie m o ty c h s a
m y c h k lu c z a c h , c o p o w o d u je d w u k r o tn e z m n ie js z e n ie w a rto ś c i a . T a k ja k w in n y c h
z a s to s o w a n ia c h z m ie n ia n ia w ie lk o ś c i tab licy , ta k i tu tr z e b a d o d a ć w ie rsz :
i f (N > 0 && N <= M/8 ) resize(M/2);
ja k o o s ta tn ią in s tru k c ję w m e to d z ie del ete ( ) , a b y z a g w a ra n to w a ć , że ta b lic a je s t z a
p e łn io n a p rz y n a jm n ie j w je d n e j ó sm e j. D z ię k i te m u w ia d o m o , że ilo ść w y k o rz y s ta n e j
p a m ię c i z aw sze r ó ż n i się n ie w ię ce j n iż o s ta ły c z y n n ik o d lic z b y p a r k lu c z - w a r to ś ć
z a p is a n y c h w tab licy . P rz y z m ie n ia n iu w ie lk o ś c i ta b lic y w ia d o m o , że a < 1 / 2 .
M etoda łańcuchowa T a s a m a m e t o d a p o z w a la z a p e w n ić n ie w ie lk ą d łu g o ś ć list
( ś r e d n ią d łu g o ś ć p o m ię d z y 2 a 8 ) w m e to d z ie ła ń c u c h o w e j. N a le ż y z a s tą p ić ty p
Li nearProbi ngHashST p rz e z SeparateChai ni ngHashST w m e to d z ie res i ze () , w y w o ły
w a ć in s tr u k c ję r e s i z e (2*M) p r z y (N >= M/2) w m e to d z ie put () i w y w o ły w a ć in s tr u k
cje resize(M/2) p rz y (N > 0 && N <= M/8 ) w del e te ( ) . W m e to d z ie ła ń c u c h o w e j
z m ie n ia n ie w ie lk o ś c i ta b lic y je s t o p c jo n a ln e i n ie w a rte z a c h o d u , je śli m o ż n a rz e te ln ie
o sz a c o w a ć w ie lk o ś ć N w k lie n c ie . W y s ta rc z y w te d y w y b ra ć w ie lk o ś ć ta b lic y (M ) n a
p o d s ta w ie w ied zy , że cza s w y s z u k iw a n ia je s t p r o p o r c jo n a ln y d o 1 + N /M . W p ró b k o -
3.4 □ Tablice z haszowaniem 487
w a n iu lin io w y m z m ie n ia n ie w ie lk o ś c i ta b lic y je s t k o n ie c zn e . W k lie n c ie , k tó r y w sta w i
w ięcej p a r k lu c z - w a r to ś ć , n iż o c z e k iw a n o , p r o b le m e m m o ż e b y ć n ie ty lk o d łu g i czas
w y s z u k iw a n ia , a le n a w e t p ę tla n ie s k o ń c z o n a p o z a p e łn ie n iu się tab licy .
A n a l i z y z u w z g lę d n i e n ie m a m o r t y z a c j i Z p e rs p e k ty w y te o re ty c z n e j p r z y z m ie n ia
n iu w ie lk o ś c i ta b lic y tr z e b a z a d o w o lić się o g ra n ic z e n ie m z u w z g lę d n ie n ie m a m o r ty
zacji, p o n ie w a ż w ia d o m o , że w s ta w ia n ie p o w o d u ją c e p o d w o je n ie r o z m ia r u ta b lic y
w y m a g a d u ż e j lic z b y p ró b .
Twierdzenie N. Z ałó żm y , że ta b lic a z h a sz o w a n ie m je s t b u d o w a n a za p o m o c ą
z m ie n ia n ia w ielk o śc i tablicy. P o c z ą tk o w o ta b lic a je s t p u sta . P rz y z a ł o ż e n i u j każdy
ciąg t o p e ra c ji w y szu k iw a n ia , w sta w ia n ia i u su w a n ia n a ta b lic y sy m b o li m a o c z e k i
w a n y czas w y k o n a n ia p ro p o rc jo n a ln y d o t, a p o z io m w y k o rz y sta n ia p a m ię c i n ig d y
n ie ró ż n i się w ięcej n iż o sta ły c z y n n ik o d liczb y k lu c z y z a p isa n y c h w tablicy.
Dowód. Z a r ó w n o w m e to d z ie ła ń c u c h o w e j, j a k i p r z y p r ó b k o w a n iu lin io w y m
w y n ik a to z p ro s te g o p r z e f o r m u ło w a n ia u w z g lę d n ia ją c y c h a m o r ty z a c ję a n a liz
d łu g o ś c i ta b lic y (p o ra z p ie r w s z y p r z e d s ta w io n o je w r o z d z i a l e i . ) w p o łą c z e
n iu Z T W IE R D Z E N IA M I K i M.
K o sz ty w y w o ła n ia j a v a F r e q u e n c y C o u n t e r 8 < t a l e . t x t z w y k o rz y sta n ie m k la sy S e p a r a t e C h a in i n g H a s h S T (z p o d w a jan ie m )
K o s z t y w y w o ła n ia j a v a F r e q u e n c y C o u n t e r 8 < t a l e . t x t z w y k o rz y s t a n ie m k la sy L i n e a r C h a i n i n g H a s h S T (z p o d w a ja n ie m )
488 R O ZD ZIA Ł 3 Q W yszukiw anie
W y k re s y ś r e d n ic h s k u m u lo w a n y c h d la p rz y k ła d o w e g o p r o g r a m u F re q u e n c y C o u n te r
( p r z e d s ta w io n e n a d o le p o p rz e d n ie j s tro n y ) d o b r z e ilu s tr u ją d y n a m ik ę z m ie n ia n ia
w ie lk o ś c i ta b lic y w h a s z o w a n iu . P rz y k a ż d y m p o d w o je n iu w ie lk o ś c i ta b lic y ś r e d n ia
s k u m u lo w a n a ro ś n ie m n ie j w ię ce j o 1 , p o n ie w a ż d la k a ż d e g o k lu c z a ta b lic y tr z e b a
p o n o w n ie o b lic z y ć s k ró t. N a s tę p n ie ś r e d n ia sp a d a , p o n ie w a ż lic z b a k lu c z y o d p o w ia
d a ją c y c h k a ż d e j p o z y c ji ta b lic y z m n ie js z a się m n ie j w ię ce j o p o ło w ę , p r z y c z y m t e m
p o s p a d k u z m n ie js z a się w ra z z p o n o w n y m z a p e łn ia n ie m się tablicy.
Pamięć W s p o m n ie liś m y , że z ro z u m ie n ie w y k o rz y s ta n ia p a m ię c i to w a ż n y c z y n n ik
p rz y d o s tr a ja n iu a lg o r y tm ó w h a s z o w a n ia p o d k ą te m o p ty m a ln e j w y d a jn o ś c i. C h o ć
d o s tra ja n ie je s t z a d a n ie m d la e k sp e rtó w , w a rto ś c io w y m ć w ic z e n ie m je s t z g ru b n e
o k re ś le n ie ilo śc i p o tr z e b n e j p a m ię c i p rz e z o sz a c o w a n ie lic z b y u ż y w a n y c h r e f e r e n
cji. Jeśli p o m in ą ć p a m ię ć n a k lu c z e i w a rto ś c i, w p rz e d s ta w io n e j tu im p le m e n ta c ji
k la s y SeperateChai ni ngHashST p o tr z e b n a je s t p a m ię ć n a M re fe re n c ji d o o b ie k tó w
SequentialSearchST i M o b ie k tó w te g o ty p u . K a ż d y o b ie k t SequentialSearchST
o b e jm u je s ta n d a rd o w y c h 16 b a jtó w n a n a r z u t d la o b ie k tó w p lu s je d n ą 8 -b a jto w ą r e
fe re n c ję (first), a w s u m ie je s t N o b ie k tó w Node, z k tó r y c h k a ż d y o b e jm u je 2 4 b a j
ty n a n a r z u t d la o b ie k tó w i tr z y re fe re n c je (k ey, value i n e x t) . M o ż n a p o r ó w n a ć to
z d o d a tk o w ą re fe re n c ją n a w ę z e ł w d rz e w a c h w y s z u k iw a ń b in a r n y c h . P rz y p r ó b k o
w a n iu lin io w y m ze z m ie n ia n ie m w ie lk o ś c i ta b lic y (w c e lu u tr z y m a n ia w s p ó łc z y n
n ik a z a p e łn ie n ia m ię d z y je d n ą ó s m ą a je d n ą d r u g ą ) p o tr z e b n y c h je s t o d 4 N d o 1 6 N
re fe re n c ji. D la te g o s to s o w a n ie h a s z o w a n ia ze w z g lę d u n a p o z io m z u ż y c ia p a m ię c i
je s t z w y k le n ie u z a s a d n io n e . O b lic z e n ia w y g lą d a ją n ie c o in a c z e j d la ty p ó w p ro s ty c h
(z o b a c z ć w i c z e n i e 3 .4 . 2 4 ).
Metoda W ykorzystanie pamięci dla N elementów
(typy referencyjne)
Metoda łańcuchowa -4 8 N + 6 4 M
Próbkowanie liniowe M ię d zy - 3 2 N a - 1 2 8 N
Drzewa BST -5 6 N
W ykorzystanie pamięci w tablicach symboli
3.4 ■ Tablice z haszowaniem 489
o d z a r a n i a i n f o r m a t y k i n a u k o w c y b a d a li (i w c ią ż b a d a ją ) h a s z o w a n ie . O d k r y to
p rz y ty m w ie le s p o s o b ó w n a u s p r a w n ie n ie p o d s ta w o w y c h , o m ó w io n y c h w c z e śn ie j
a lg o ry tm ó w . D o s tę p n a je s t b o g a ta lite r a tu r a n a te n te m a t. W ię k s z o ś ć u s p r a w n ie ń p o
w o d u je o b n iż e n ie k rz y w e j n a w y k re sie p a m ię c i i c z a su . M o ż n a u z y sk a ć te n s a m czas
w y s z u k iw a n ia , w y k o rz y s tu ją c m n ie j p a m ię c i, lu b p rz y s p ie s z y ć w y s z u k iw a n ie p rz y
ty m s a m y m z u ż y c iu p a m ię c i. I n n e u s p r a w n ie n ia d o ty c z ą le p s z y c h g w a ra n c ji o c z e k i
w a n y c h k o s z tó w w y s z u k iw a n ia d la n a jg o rs z e g o p r z y p a d k u . Jeszcze in n e z w ią z a n e są
z le p s z y m i fu n k c ja m i h a s z u ją c y m i. N ie k tó r e m e to d y p o r u s z o n o w ć w ic z e n ia c h .
S z czeg ó ło w e w y n ik i p o r ó w n a n ia m e to d y ła ń c u c h o w e j i p ró b k o w a n ia lin io w e g o
za le ż ą o d m n ó s tw a s z c z e g ó łó w im p le m e n ta c y jn y c h o ra z w y m o g ó w p a m ię c io w y c h
i c z a so w y c h o b o w ią z u ją c y c h w k lie n c ie . Z w y k le n ie u z a s a d n io n e je s t w y b ie ra n ie m e
to d y ła ń c u c h o w e j z a m ia s t p r ó b k o w a n ia lin io w e g o n a p o d s ta w ie w y d a jn o ś c i (z o b a c z
ć w i c z e n i e 3 . 5 . 3 1 ). W p ra k ty c e p o d s ta w o w a ró ż n ic a w w y d a jn o ś c i m ię d z y ty m i
te c h n ik a m i w y n ik a z teg o , że w m e to d z ie ła ń c u c h o w e j d la k a ż d e j p a r y k lu c z - w a r to ś ć
u ż y w a n y je s t m a ły b lo k p a m ię c i, n a to m ia s t w p r ó b k o w a n iu lin io w y m z a jm o w a n e są
d w ie d u ż e ta b lic e d la całej tab licy . Jeśli ta b lic e są d u ż e , o b a r o z w ią z a n ia sta w ia ją in n e
w y m o g i sy s te m o w i z a rz ą d z a n ia p a m ię c ią . W e w s p ó łc z e s n y c h s y s te m a c h ro z w ią z y
w a n ie m te g o ro d z a ju d y le m a tó w p o w in n i z a jm o w a ć się e k s p e rc i w w y ją tk o w y c h sy
tu a c ja c h , w k tó r y c h w y d a jn o ś ć o d g ry w a k r y ty c z n ą ro lę.
P rz y o p ty m is ty c z n y c h z a ło ż e n ia c h m o ż n a o c z e k iw a ć , że h a s z o w a n ie z a p e w n i w y
k o n a n ie w s ta ły m c z a sie o p e ra c ji w y s z u k iw a n ia o ra z w s ta w ia n ia w ta b lic y s y m b o li
— i to n ie z a le ż n ie o d w ie lk o ś c i tab lic y . Jest to te o r e ty c z n ie o p ty m a ln a w y d a jn o ś ć d la
d o w o ln e j im p le m e n ta c ji ta b lic y s y m b o li. J e d n a k h a s z o w a n ie n ie je s t ro z w ią z a n ie m
u n iw e rs a ln y m . W y n ik a to z k ilk u p rz y c z y n . O to o n e:
° Potrzebna jest dobra funkcja haszująca dla każdego typu kluczy.
■ G w arancje w ydajności zależą o d jakości funkcji haszującej.
■ F u n k c je h a s z u ją c e m o g ą b y ć s k o m p lik o w a n e i k o s z to w n e d o o b lic z e n ia .
■ N iełatwo jest zapew nić obsługę operacji na uporządkow anych tablicach sym
boli.
Poruszyliśmy tylko podstawowe kwestie. Porów nanie haszowania z innym i om ów iony
m i m etodam i tw orzenia tablic symboli odkładam y do początku p o d r o z d z i a ł u 3 . 5 .
490 RO ZD ZIA Ł 3 o W yszukiw anie
I
P.
PYTANIA I ODPOWIEDZI
Jak m e to d a h a sh C o d e() z a im p le m e n to w a n a je s t k 5t primes [k]
w Javie d la ty p ó w I n t e g e r , Doubl e i Long? (2* - 5‘)
5 1 31
O . D la ty p u I n t e g e r z w ra c a 3 2 -b ito w ą w a rto ś ć . D la
6 3 61
ty p ó w D ouble i Long z w ra c a ró żn icę s y m e tr y c z n ą p ie r w
7 1 127
sz y c h 32 b itó w i d r u g ic h 32 b itó w s ta n d a rd o w e j m a s z y
8 5 251
n o w e j r e p r e z e n ta c ji liczby. Te ro z w ią z a n ia m o g ą n ie w y 9 3 509
d a w a ć się lo so w e , je d n a k s p e łn ia ją sw o ją fu n k c ję i r o z 10 3 1021
p ra s z a ją w a rto ś c i. 11 9 2039
12 3 4093
P. P rz y z m ie n ia n iu w ie lk o ś c i ta b lic y jej r o z m ia r z a
13 1 8191
w sze je s t p o tę g ą d w ó jk i. C z y n ie s ta n o w i to p ro b le m u ?
14 3 16381
U ż y w a n e są p rz e c ie ż ty lk o n a jm n ie j z n a c z ą c e b ity w y n i
15 19 32749
k u f u n k c ji h a sh C o d e ().
16 15 65521
O . Tak, zw łaszcza w im p le m e n ta c ja c h d o m y śln y ch . 17 1 131071
Jed n y m ze sp o s o b ó w n a ro z w ią z a n ie p ro b le m u je s t p o c z ą t 18 5 262139
kow e ro z ło ż e n ie w a rto śc i k lu c zy za p o m o c ą liczb y p ie rw 19 1 524287
20 3 1048573
szej w iększej n iż M, ta k ja k w p o n iż s z y m frag m en cie:
21 9 2097143
p r i v a t e i n t h ash (K ey x) 22 3 4194301
23 15 8388593
i n t t = > .hashCode() k 0 x 7 f f f f f f f ; 24 3 16777213
i f (IgM < 26) t = t ° prim e sp gM +5]; 25 39 33554393
r e t u r n t ‘i M; 26 5 67108859
} 27 39 134217689
K o d o p a r t y je s t n a z a ło ż e n iu , że p r z e c h o w u je m y z m i e n 28 57 268435399
29 3 536870909
n ą e g z e m p la rz a IgM, ró w n ą lg A i (n a le ż y z a in ic jo w a ć
30 35 1073741789
z m ie n n ą o d p o w ie d n ią w a rto ś c ią , a n a s tę p n ie z w ię k sz a ć
31 1 2147483647
ją p r z y p o d w a ja n iu i z m n ie js z a ć p r z y s k r a c a n iu o p o ł o
w ę ), i ta b lic ę p rim e s [] z n a jm n ie js z y m i lic z b a m i p ie r w Liczb y pierwsze określające
rozm iary tablicy
s z y m i w ię k s z y m i n iż k a ż d a p o tę g a d w ó jk i (z o b a c z ta b e
z haszow aniem
lę p o p ra w e j). S ta łą 5 w y b r a n o a rb itra ln ie . O c z e k u je m y ,
że p ie r w s z a o p e ra c ja % ro z d z ie la w a r to ś c i r ó w n o m ie r n ie m ię d z y w a r to ś c i m n ie js z e
n iż d a n a lic z b a p ie rw s z a , a d r u g a o d w z o ru je o k o ło p ię c iu z ty c h w a r to ś c i n a k a ż d ą
w a rto ś ć m n ie js z ą n iż M. Z a u w a ż m y , że d la d u ż e g o M p r z y d a tn o ś ć tej te c h n ik i je s t
d y sk u s y jn a .
P. Z a p o m n ia łe m , d la c z e g o n ie im p le m e n tu je m y m e to d y h a s h ( x ) p rz e z z w ró c e n ie
w a rto ś c i x .h a s h C o d e () % M?
O . P o tr z e b n y je s t w y n ik z p r z e d z ia łu o d 0 d o M -l, je d n a k w Javie fu n k c ja % m o ż e
z w ra c a ć w a rto ś ć u je m n ą .
3.4 n Tablice z haszowaniem 491
P. D la c z e g o w ię c n ie z a im p le m e n to w a ć m e to d y h a s h ( x ) p rz e z z w ró c e n ie w a rto ś c i
M a t h . a b s ( x . h a s h C o d e ( ) ) % M?
O . D o b r a p ró b a . N ie ste ty , m e to d a M a th .a b s ( ) z w ra c a w y n ik u je m n y d la n a jw ię k
szej m o ż liw e j lic z b y u je m n e j. W w ie lu ty p o w y c h o b lic z e n ia c h to p rz e p e łn ie n ie n ie
s ta n o w i rz e c z y w is te g o p ro b le m u , je d n a k p r z y h a s z o w a n iu m o ż e s p o w o d o w a ć , że
p r o g r a m p o k ilk u m ilia r d a c h w s ta w ie ń p r a w d o p o d o b n ie u le g n ie a w a rii. Jest to n ie
p rz y je m n a p e rs p e k ty w a . P rz y k ła d o w o , in s tr u k c ja s .h a s h C o d e Q w ja v i e d a je w a rto ś ć
- 2 31 d la w a rto ś c i " p o ly g e n e l u b ri c a n ts " ty p u S t r i ng. W y m y ś la n ie in n y c h ła ń c u c h ó w
z n ak ó w , k tó r y c h s k r ó t m a tę w a rto ś ć (lu b je s t r ó w n y 0 ), to c ie k a w a ła m ig łó w k a a lg o
ry tm ic z n a .
P. D la c z e g o w a l g o r y t m i e 3.5 n i e u ż y w a m y ld a s Bi n a ry S e a rc h S T lu b RedBl ackBST
z a m ia s t S e q u e n ti a l SearchS T ?
O . O g ó ln ie u s ta w ia m y p a r a m e tr y w ta k i s p o s ó b , a b y lic z b a k lu czy , k tó r y c h s k r ó t m a
d a n ą w a rto ś ć , b y ła m a ła . D la m a ły c h ta b lic z w y k le lep iej u ż y w a ć p o d s ta w o w y c h t a b
lic s y m b o li. W p e w n y c h s y tu a c ja c h za p o m o c ą h y b ry d o w y c h m e t o d m o ż n a u z y sk a ć
p e w n ą p o p ra w ę w y d a jn o ś ć , je d n a k te g o ro d z a ju d o s tra ja n ie n a jle p ie j p o z o s ta w ić e k s
p e r to m .
P. S zy b sze je s t w y s z u k iw a n ie za p o m o c ą h a s z o w a n ia c z y p r z y u ż y c iu c z e rw o n o -
c z a rn y c h d rz e w B ST?
O . Z a le ż y to o d ty p u k lu c z a . W y z n a c z a o n k o s z t o b lic z a n ia m e to d y hashC ode () w p o
r ó w n a n iu ze s to s o w a n ie m m e to d y co m p a re T o (). D la ty p o w y c h k lu c z y i d o m y ś ln y c h
im p le m e n ta c ji Javy k o s z ty te są z b liż o n e , d la te g o h a s z o w a n ie b ę d z ie z n a c z n ie s z y b
sze, p o n ie w a ż w y m a g a ty k o sta łe j lic z b y o p e ra c ji. N a le ż y je d n a k p a m ię ta ć , że o d p o
w ie d ź n ie je s t je d n o z n a c z n a , je ś li p o tr z e b n e są o p e ra c je n a u p o r z ą d k o w a n e j ta b licy ,
k tó r y c h n ie m o ż n a w y d a jn ie o b s łu g iw a ć z a p o m o c ą ta b lic z h a s z o w a n ie m . D a lsz e
o m ó w ie n ie z n a jd u je się w p o d r o z d z i a l e 3 .5 .
P. D la c z e g o p r z y p r ó b k o w a n iu lin io w y m n ie p o z w a la m y n a z a p e łn ie n ie ta b lic y n a
p rz y k ła d w tr z e c h c z w a rty c h ?
O . B ez k o n k r e tn e g o pow odu. M ożna w y b ra ć d o w o ln ą w a rto ś ć a, s to s u ją c
t w i e r d z e n i e m d o o s z a c o w a n ia k o s z tó w w y s z u k iw a n ia . D la a = 3 /4 ś r e d n i k o s z t
u d a n e g o w y s z u k iw a n ia w y n o s i 2,5, a n ie u d a n e g o w y s z u k iw a n ia — 8,5. Jeśli j e d
n a k p o z w o lim y n a w z ro s t a d o 7 /8 , ś r e d n i k o s z t n ie u d a n e g o w y s z u k iw a n ia w y n ie
sie 32,5, co m o ż e b y ć n ie a k c e p to w a ln e . W ra z z p r z y b liż a n ie m się a d o 1 s z a c u n k i
z t w i e r d z e n i a m s ta ją się n ie p ra w id ło w e , n ie n a le ż y je d n a k d o p u s z c z a ć , a b y ta b lic a
z a p e łn iła się w t a k d u ż y m s to p n iu .
492 RO ZD ZIA Ł 3 n W yszukiwanie
| ĆWICZENIA
3.4.1 . W s ta w k lu c z e E A S Y Q U T I 0 N (w tej k o le jn o ś c i) d o p o c z ą tk o w o p u s te j
ta b lic y o M = 5 lista c h . Z a sto s u j m e to d ę ła ń c u c h o w ą . U żyj f u n k c ji h a sz u ją c e j 11 k %
Md o p r z e k s z ta łc e n ia k - tej lite r y a lfa b e tu n a in d e k s tab licy .
3.4.2. O p ra c u j in n ą im p le m e n ta c ję k la s y S e p e ra te C h a i ni ngHashST, w k tó re j b e z p o
ś r e d n io s to s o w a n y je s t k o d lis t p o w ią z a n y c h z k la s y S e q u e n ti a l S earchS T .
3.4.3. Z m o d y fik u j im p le m e n ta c ję z p o p rz e d n ie g o ć w ic z e n ia p rz e z d o łą c z e n ie cał-
k o w ito lic z b o w e g o p o la d la k a ż d e j p a r y ld u c z -w a rto ś ć . P o le n a le ż y u s ta w ić n a lic z b ę
e le m e n tó w w ta b lic y w m o m e n c ie w s ta w ia n ia d a n e j p a ry . N a s tę p n ie z a im p le m e n tu j
m e to d ę u s u w a ją c ą w sz y stk ie k lu c z e (i p o w ią z a n e w a rto ś c i), d la k tó r y c h p o le m a w a r
to ś ć w ię k sz ą n iż d a n a lic z b a c a łk o w ita k. Uwaga: ta d o d a tk o w a fu n k c ja je s t p r z y d a tn a
p rz y im p le m e n to w a n iu ta b lic y s y m b o li d la k o m p ila to ra .
3.4.4. N a p isz p r o g r a m d o z n a jd o w a n ia w a rto ś c i a i M (p r z y c z y m Mm a b y ć ta k m a ta ,
ja k to m o ż liw e ), ta k ic h że fu n k c ja h a s z u ją c a (a * k) % M d o p rz e k s z ta łc a n ia k -tej
l i t e r y a lf a b e tu n a in d e k s ta b l ic y g e n e r u j e r ó ż n e w a r to ś c i ( b e z k o liz ji) d la k lu c z y
S E A R C H X M P L . E fe k t to ta k z w a n a id e a ln a f u n k c j a h a szu ją c a .
3.4.5. C z y p o n iż s z a im p le m e n ta c ja m e to d y h a sh C o d e () je s t d o p u s z c z a ln a ?
p u b l i c i n t h a sh C o d e ()
{ re tu rn 17; }
Jeśli ta k , o p is z e fe k t je j z a s to s o w a n ia . Jeżeli n ie , w y ja śn ij d la c z eg o .
3.4.6. Z a łó ż m y , że k lu c z e to f-b ito w e lic z b y c a łk o w ite . D la m o d u la r n e j f u n k c ji h a
szu jącej o p a rte j n a lic z b ie c a łk o w ite j M u d o w o d n ij, że k a ż d y b it k lu c z a m a tę c e c h ę , iż
is tn ie ją d w a k lu c z e ró ż n ią c e się ty lk o ty m b ite m i m a ją c e ró ż n e w a r to ś c i s k ró tu .
3.4.7. Z a s ta n ó w się n a d p e w n ą im p le m e n ta c ją h a s z o w a n ia m o d u la r n e g o d la k lu c z y
c a łk o w ito lic z b o w y c h , (a * k) % M, g d z ie a to d o w o ln a s ta ła lic z b a c a łk o w ita . C z y ta
z m ia n a p o w o d u je n a ty le d o b re w y m ie s z a n ie b itó w , że m o ż n a u ż y ć lic z b y M, k tó r a n ie
je s t p ie rw s z a ?
3.4.8. Ile p u s ty c h list m o ż n a o c z e k iw a ć p r z y w s ta w ie n iu N k lu c z y d o ta b lic y z h a -
sz o w a n ie m za p o m o c ą k la s y S e p a ra te C h a i ni ngHashST d la N = 10, 102, 1 0 \ 104, 10 5
i 106? W s k a zó w k a : z o b a c z ć w i c z e n i e 2 . 5 .3 1 .
3.4.9. Z a im p le m e n tu j z a c h ła n n ą m e to d ę d el e t e () d la kla sy S e p a ra te C h a i ni ngHashST.
3.4.10. W s ta w k lu c z e E A S Y Q U T I 0 N (w tej k o le jn o ś c i) d o p o c z ą tk o w o p u
stej ta b lic y o ro z m ia r z e M = 16, u ż y w a ją c p ró b k o w a n ia lin io w e g o . Z a s to s u j fu n k c ję
h a s z u j ą c ą l l k % M, a b y p rz e k s z ta łc ić k - tą lite rę a lf a b e tu n a in d e k s tab lic y . P o n o w n ie
w y k o n a j ć w ic z e n ie d la M = 10.
3.4 o Tablice z haszowaniem 493
[
3 .4 .1 1 . P rz e d s ta w z a w a rto ś ć ta b lic y z h a s z o w a n ie m u tw o rz o n e j p rz e z p ró b k o w a n ie
lin io w e p r z y w s ta w ia n iu k lu c z y E A S Y Q U T I 0 N (w tej k o le jn o ś c i) d o p o c z ą t
k o w o p u s te j ta b lic y o w y jśc io w y m ro z m ia r z e M = 4. T a b lic a je s t p o w ię k s z a n a p rz e z
p o d w a ja n ie , k ie d y sta je się w p o ło w ie p e łn a . U żyj fu n k c ji h a s z u ją c e j 11 k % M d o
p r z e k s z ta łc e n ia k -tej lite r y a lf a b e tu n a in d e k s tab licy .
3 .4 .1 2 . Z a łó ż m y , że k lu c z e o d A d o G (p o d a j ic h w a r to ś c i s k ró tó w ) są w s ta w ia n e
w p e w n e j k o le jn o ś c i d o p o c z ą tk o w o p u s te j ta b lic y o w ie lk o ś c i 7 z a p o m o c ą p r ó b k o
w a n ia lin io w e g o ( tu n ie z m ie n ia m y w ie lk o ś c i ta b lic y ). K tó ra z p o n iż s z y c h ta b lic n ie
m o ż e p o w s ta ć w te n s p o s ó b ?
a. E F G A C B D
b. C E B G F D A
c. B D F A C E G
d. C G B A D E F
e. F G B D A C E
f. G E C A D B F
P o d a j m in im a ln ą i m a k s y m a ln ą lic z b ę p ró b , k tó r e m o g ą b y ć p o tr z e b n e d o z b u d o w a
n ia ta b lic y o w ie lk o ś c i 7 za p o m o c ą ty c h k lu czy . P rz e d s ta w te ż k o le jn o ś ć w s ta w ia n ia
u z a s a d n ia ją c ą o d p o w ie d ź .
3 .4 .1 3 . K tó ry z p o n iż s z y c h s c e n a riu s z y p r o w a d z i d o o c z e k iw a n e g o lin io w eg o c z a su
w y k o n a n ia d la lo s o w e g o u d a n e g o w y s z u k iw a n ia za p o m o c ą p r ó b k o w a n ia lin io w e g o
w ta b lic y z h a s z o w a n ie m ?
a. S ieroty w s z y s tk ic h k lu c z y o d p o w ia d a ją te m u s a m e m u in d e k s o w i.
b. S k ró ty w s z y s tk ic h k lu c z y o d p o w ia d a ją r ó ż n y m in d e k s o m .
c. S k ró ty w s z y s tk ic h k lu c z y o d p o w ia d a ją in d e k s o w i o n u m e r z e p a rz y s ty m .
d. S k ró ty w s z y s tk ic h k lu c z y o d p o w ia d a ją ró ż n y m in d e k s o m o n u m e r a c h
p a rz y s ty c h .
3 .4 .1 4 . O d p o w ie d z n a p y ta n ie z p o p r z e d n ie g o ć w ic z e n ia d la n ie u d a n e g o w y s z u k i
w a n ia p r z y z a ło ż e n iu , że s k r ó ty k lu c z y w y s z u k iw a n ia z r ó w n y m p r a w d o p o d o b ie ń
s tw e m o d p o w ia d a ją k a ż d e j p o z y c ji tab licy .
3 .4 .1 5 . Ilu p o r ó w n a ń w y m a g a w n a jg o rs z y m p r z y p a d k u w s ta w ie n ie N k lu c z y d o
p o c z ą tk o w o p u s te j ta b lic y p rz y s to s o w a n iu p ró b k o w a n ia lin io w e g o z p o w ię k s z a n ie m
ta b lic y ?
3 .4 .1 6 . Z a łó ż m y , że s to s u je m y p r ó b k o w a n ie lin io w e , a ta b lic a o w ie lk o ś c i 106 je s t
w p o ło w ie z a p e łn io n a . Z a ję te są lo s o w o w y b ra n e p o z y c je . O sz a c u j p r a w d o p o d o b ie ń
stw o , że z a ję te są w sz y stk ie p o z y c je o in d e k s a c h p o d z ie ln y c h p r z e z 1 0 0 .
494 RO ZD ZIA Ł 3 a W yszukiwanie
ĆWICZENIA (ciąg dalszy)
3 .4 .1 7 . P rz e d s ta w e fe k t w y k o rz y s ta n ia m e to d y d el e t e () ze s tr o n y 4 8 3 d o u s u n ię c ia
C z ta b lic y u tw o rz o n e j p rz e z z a s to s o w a n ie k la s y Li n e a rP r o b i ngHashST w s t a n d a r d o
w y m k lie n c ie u ż y w a ją c y m in d e k s u ( p o k a z a n y m n a s tro n ie 4 8 1 ).
3 .4 .1 8 . D o d a j d o k la s y S e p a ra te C h a i ni ngHashST k o n s tr u k to r , k tó r y u m o ż liw ia
k lie n to m o k re ś le n ie ś re d n ie j lic z b y p r ó b d o p u s z c z a ln e j p r z y w y s z u k iw a n iu . Z a sto s u j
z m ie n ia n ie w ie lk o ś c i tab lic y , ta k a b y ś r e d n ia d łu g o ś ć lis t b y ła m n ie js z a o d o k re ś lo n e j
w a rto ś c i. U żyj te c h n ik i o p is a n e j n a s tr o n ie 4 9 0 w c e lu z a g w a ra n to w a n ia , że w s p ó ł
c z y n n ik w m e to d z ie hash () je s t lic z b ą p ie rw s z ą .
3 .4 .1 9 . Z a im p le m e n tu j m e to d ę keys () d la k la s S e p a ra te C h a i ni ngHashST
i Li n e a rP r o b i ngHashST.
3 .4 .2 0 . D o d a j d o k la s y Li n e a rP r o b i ngHashST m e to d ę , k tó r a o b lic z a ś r e d n i k o s z t
u d a n e g o w y s z u k iw a n ia w ta b lic y p r z y z a ło ż e n iu , że s z u k a n ie k a ż d e g o k lu c z a ta b lic y
je s t ró w n ie p r a w d o p o d o b n e .
3 .4 .2 1 . D o d a j d o k la s y Li n e a rP r o b i ngHashST m e to d ę , k tó r a o b lic z a ś r e d n i k o s z t
n ie u d a n e g o w y s z u k iw a n ia w ta b lic y p r z y z a ło ż e n iu , że s to s o w a n a je s t lo s o w a fu n k c ja
h a s z u ją c a . U w a g a : n ie m u s is z o b lic z a ć ż a d n e j f u n k c ji h a s z u ją c e j, a b y w y k o n a ć ć w i
c z en ie.
3 .4 .2 2 . Z a im p le m e n tu j m e to d ę h a sh C o d e () d la ró ż n y c h ty p ó w : PointŻ D , I n t e r v a l ,
I n t e r v a l 2D i D ate.
3 .4 .2 3 . R o z w a ż m y h a s z o w a n ie m o d u l a r n e d la k lu c z y w p o s ta c i ła ń c u c h ó w zn a k ó w .
P rz y jm ijm y R = 256 i M = 255. W y k a ż , że są to z łe w a rto ś c i, p o n ie w a ż k a ż d a p e r m u -
ta c ja lite r d a n e g o ła ń c u c h a z n a k ó w d a te n s a m sierót.
3 .4 .2 4 . P rz e a n a liz u j w y k o rz y s ta n ie p a m ię c i w m e to d z ie ła ń c u c h o w e j, p r ó b k o w a n iu
lin io w y m i d rz e w a c h B ST d la k lu c z y ty p u doubl e. P rz e d s ta w w y n ik i w ta b e li p o d o b
nej d o tej ze s tr o n y 488.
3.4 a Tablice z haszowaniem 495
PROBLEMY DO ROZWIĄZANIA
3.4.25. P a m ię ć p o d r ę c z n a p r z y h a sz o w a n iu . Z m o d y fik u j k la s ę T r a n s a c t i on ze s tro n y
474. ta k , ab y o b e jm o w a ła z m ie n n ą e g z e m p la rz a h ash , w k tó r e j m e t o d a hashC ode () p rz y
p ie rw s z y m w y w o ła n iu d la k a ż d e g o o b ie k tu z a p is u je w a rto ś ć s k ró tu . N ie tr z e b a w te
d y p o n o w n ie o b lic z a ć tej w a rto ś c i p r z y k o le jn y c h w y w o ła n ia c h . Uwaga: te c h n ik a ta
d z ia ła ty lk o d la ty p ó w n ie z m ie n n y c h .
3.4.26. L e n iw e u su w a n ie p rzy p ró b k o w a n iu lin io w y m . D odaj do k la sy
L in earP ro b in g H ash S T m e to d ę d e l e t e ( ) , k tó r a u s u w a p a r y k lu c z -w a rto ś ć p rz e z u s ta
w ie n ie w a rto ś c i n a nul 1 (b e z u s u w a n ia k lu c z a ). N a s tę p n ie n a le ż y u s u n ą ć p a rę z ta b lic y
w w y w o ła n iu r e s i z e ( ) . Uwaga: je śli p ó ź n ie js z a o p e ra c ja p u t( ) w ią ż e n o w ą w a rto ś ć
z d a n y m k lu c z e m , n a le ż y n a d p is a ć w a rto ś ć n u l i . U p e w n ij się, że w p ro g r a m ie p rz y p o
d e jm o w a n iu d e c y z ji o ro z s z e rz e n iu lu b z m n ie js z e n iu ta b lic y u w z g lę d n ia n a je s t lic z b a
zn a c z n ik ó w u su n ięc ia (an g . to m b sto n e ) te g o ro d z a ju , a ta k ż e lic z b a p u s ty c h p o zy cji.
3.4.27. D w u k r o tn e p ró b y . Z m o d y fik u j k la s ę S e p a ra te C h a in in g H a sh S T p rz e z u ż y c ie
d ru g ie j fu n k c ji h a s z u ją c e j i w y b ie ra n ie k ró ts z e j z d w ó c h list. P rz e d s ta w śla d d z ia ła n ia
p ro c e s u w s ta w ia n ia k lu c z y E A S Y Q U T I 0 N (w tej k o le jn o ś c i) d o p o c z ą tk o w o
p u ste j ta b lic y o w ie lk o ś c i M = 3. Z a s to s u j fu n k c ję 11 k % M (d la k -te j lite ry ) ja k o
p ie rw s z ą fu n k c ję h a s z u ją c ą i fu n k c ję 17 k % M (d la k - tej lite ry ) ja k o d r u g ą fu n k c ję
h a sz u ją c ą . P o d a j ś r e d n ią lic z b ę p ró b d la lo s o w e g o u d a n e g o i n ie u d a n e g o w y s z u k iw a
n ia w tej tab licy .
3.4.28. P o d w ó jn e h a szo w a n ie . Z m o d y fik u j k la s ę Li n e a rP r o b i ngHashST p rz e z u ż y c ie
d ru g ie j f u n k c ji h a s z u ją c e j d o d e fin io w a n ia c ią g u p ró b . Z a s tą p f r a g m e n t ( i + 1) % M
(o b a w y s tą p ie n ia ) k o d e m (i + k) % M, g d z ie k to ró ż n a o d z e ra i z a le ż n a o d k lu c z a
lic z b a c a łk o w ita w z g lę d n ie p ie r w s z a d la M. U waga: o s ta tn i w a r u n e k m o ż n a s p e łn ić
p rz e z z a ło ż e n ie , że Mto lic z b a p ie rw s z a . P rz e d s ta w p rz e b ie g p ro c e s u w s ta w ia n ia k lu
czy E A S Y Q U T I 0 N (w tej k o le jn o ś c i) d o p o c z ą tk o w o p u s te j ta b lic y o w ie lk o ś c i
M = 1 1 . U żyj f u n k c ji h a s z u ją c y c h o p is a n y c h w p o p r z e d n im ć w ic z e n iu . P o d a j ś r e d n ią
lic z b ę p ró b d la lo s o w e g o u d a n e g o i n ie u d a n e g o w y s z u k iw a n ia w u tw o rz o n e j tab licy .
3.4.29. U su w a n ie. Z a im p le m e n tu j z a c h ła n n ą m e to d ę d e l e t e ( ) d la te c h n ik o p is a
n y c h w d w ó c h p o p r z e d n ic h ć w ic z e n ia c h .
3.4.30. S ta ty s ty k a ch i k w a d ra t. D o d a j d o k la s y S e p a ra te C h a in in g S T m e to d ę d o o b
lic z a n ia s ta ty s ty k i y j d la ta b lic y z h a s z o w a n ie m . D la N k lu c z y i ta b lic y o w ie lk o ś c i A i
s ta ty s ty k a z d e fin io w a n a je s t ró w n a n ie m :
X2 = (M /N ) ( ( / - N / M Y + ( f1 - N / M Y + ... (fM_ , - N I M Y )
W r ó w n a n i u / , to lic z b a k lu czy , k tó r y c h s k r ó t m a w a rto ś ć i. T a s ta ty s ty k a to je d e n ze
s p o s o b ó w n a s p r a w d z e n ie z a ło ż e n ia d o ty c z ą c e g o te g o , że fu n k c ja h a s z u ją c a z w ra c a
lo s o w e w a rto ś c i. Jeśli ta k je s t, s ta ty s ty k a d la N > c M p o w in n a m ie ć w a rto ś ć p o m ię d z y
M - Vm a M + 4 m z p r a w d o p o d o b ie ń s tw e m 1 - l/c .
496 R O ZD ZIA Ł 3 □ W yszukiwanie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
3 .4 .3 1 . H a szo w a n ie d y n a m ic z n e (a n g . cu cko o h a sh in g ). O p ra c u j im p le m e n ta c ję
ta b lic y s y m b o li o b e jm u ją c ą d w ie ta b lic e z h a s z o w a n ie m i d w ie fu n k c je h a sz u ją c e .
K a ż d y k lu c z z n a jd u je się ty lk o w je d n e j z ta b lic . P rz y w s ta w ia n iu n o w e g o k lu c z a n a
le ż y u m ie ś c ić g o w je d n e j z ta b lic . Jeśli p o z y c ja w tej ta b lic y je s t z a ję ta , n a le ż y z a s tą p ić
d a w n y k lu c z n o w y m i p rz e n ie ś ć d a w n y k lu c z d o d ru g ie j ta b lic y (ta k ż e z n ie j n a le ż y
p rz e n ie ś ć k lu c z , k tó r y z n a jd u je się n a p o tr z e b n e j p o z y c ji). Jeśli w p ro c e s ie w y stą p i
cy k l, n a le ż y ro z p o c z ą ć o d n o w a . N a le ż y d b a ć o to , a b y ta b lic e b y ły z a p e łn io n e m n ie j
n iż w p o ło w ie . T a m e t o d a d la n a jg o rs z e g o p r z y p a d k u w y m a g a sta łe j lic z b y te s tó w
r ó w n o ś c i p r z y w y s z u k iw a n iu (c o o c z y w iste ) i sta łe g o c z a s u (p o a m o rty z a c ji) p rz y
w s ta w ia n iu .
3 .4 .3 2 . A ta k p r z e z h a sz o w a n ie . Z n a jd ź 2N ła ń c u c h ó w z n a k ó w , k a ż d y o d łu g o ś c i 2N,
d a ją c y c h tę s a m ą w a rto ś ć f u n k c ji hashC ode () p r z y z a ło ż e n iu , że je j im p le m e n ta c ja d la
ty p u S t r i ng w y g lą d a ta k :
public in t hashCode()
{
in t hash =0;
fo r (in t i = 0 ; i <le n gth (); i ++)
hash = (hash * 31) + c h a rA t(i);
return hash;
}
D u ż a p o d p o w ie d z : Aa i BB m a ją tę s a m ą w a rto ś ć .
3 .4 .3 3 . Z ła f u n k c j a h a szu ją c a . R o z w a ż p o n iż s z ą im p le m e n ta c ję f u n k c ji hashCode()
d la ty p u S t r i ng, u ż y w a n ą w e w c z e śn ie jsz y c h w e rs ja c h Javy:
public in t hashCode()
{
in t hash =0;
in t skip =[Link](l, 1ength{ ) / 8 ) ;
fo r (in t i = 0 ; i < 1ength(); i += skip)
hash = (hash * 37) + c h a rA t(i);
return hash;
}
W y ja śn ij, d la c z e g o T w o im z d a n ie m p i'o je k ta n c i z d e c y d o w a li się n a tę im p le m e n ta c ję
i d la c z e g o z re z y g n o w a li z n ie j n a rz e c z k o d u z p o p r z e d n ie g o ć w ic z e n ia .
3 .4 □ Tablice z haszowaniem 497
[ EKSPERYMENTY
3 .4 .3 4 . K o s z ty h a sz o w a n ia . O k re ś l e m p iry c z n ie s to s u n e k c z a s u p o tr z e b n e g o d o w y
k o n a n ia m e to d y hash () d o c z a su p o tr z e b n e g o d la m e to d y co m p areT o () d la c z ę sto
u ż y w a n y c h ty p ó w k lu czy , d la k tó r y c h m o ż n a u z y s k a ć s e n s o w n e w y n ik i.
3 .4 .3 5 . Testy chi k w a d ra t. U żyj ro z w ią z a n ia ć w ic z e n ia 3 .4 .3 0 d o s p r a w d z e n ia z a
ło ż e n ia , z g o d n ie z k tó r y m fu n k c je h a s z u ją c e d la c z ę sto u ż y w a n y c h ty p ó w k lu c z y g e
n e r u ją lo s o w e w a rto ś c i.
3 .4 .3 6 . Z a k re s d łu g o śc i list. N a p isz p ro g r a m , k tó r y za p o m o c ą m e to d y ła ń c u c h o w e j
w sta w ia W ło s o w y c h k lu c z y ty p u i n t d o ta b lic y o w ie lk o ś c i N / 1 0 0 , a n a s tę p n ie o k r e
śla d łu g o ś ć n a jk ró ts z e j i n a jd łu ż s z e j listy. P rz y jm ij N = 103, 104, 10 5 i 106.
3 .4 .3 7 . H y b ry d a . P rz e p r o w a d ź b a d a n ia e k s p e r y m e n ta ln e , a b y u s ta lić e fe k t z a s to
s o w a n ia k la s y RedBlackBST z a m ia s t SequentialSearchST d o ro z w ią z y w a n ia k o liz ji
w k la s ie SeparateChai ni ngHashST. Z a le tą r o z w ią z a n ia je s t g w a r a n to w a n a w y d a jn o ś ć
lo g a r y tm ic z n a (n a w e t d la z ły c h fu n k c ji h a s z u ją c y c h ). W a d ą je s t k o n ie c z n o ś ć u tr z y
m y w a n ia d w ó c h ró ż n y c h im p le m e n ta c ji ta b lic y s y m b o li. Ja k ie są p ra k ty c z n e e fe k ty
w p ro w a d z e n ia te g o ro z w ią z a n ia ?
3 .4 .3 8 . R o z k ła d p r z y m e to d z ie ła ń c u c h o w ej. N a p is z p ro g r a m , k tó r y za p o m o c ą m e
to d y ła ń c u c h o w e j w s ta w ia 1 0 5 lo s o w y c h n ie u je m n y c h lic z b c a łk o w ity c h m n ie js z y c h
n iż 1 0 6 d o ta b lic y o ro z m ia r z e 1 0 5 i ry s u je w y k re s łą c z n e j lic z b y p r ó b p o tr z e b n y c h
p rz y k a ż d e j z 10 3 k o le jn y c h o p e ra c ji w s ta w ia n ia . W y ja śn ij, w ja k i m s to p n iu w y n ik i
s ta n o w ią d o w ó d n a t w i e r d z e n i e ic .
3 .4 .3 9 . R o z k ła d p r z y p r ó b k o w a n iu lin io w y m . N a p isz p r o g r a m , k tó r y za p o m o c ą
p r ó b k o w a n ia lin io w e g o w s ta w ia N /2 lo s o w y c h k lu c z y ty p u i n t d o ta b lic y o r o z m ia
rz e N , a n a s tę p n ie n a p o d s ta w ie d łu g o ś c i g ru p o b lic z a ś r e d n i k o s z t n ie u d a n e g o w y
s z u k iw a n ia w w y n ik o w e j tab licy . P rz y jm ij N = 103, 104, 10 5 i 106. W y ja śn ij, w ja k im
s to p n iu w y n ik i s ta n o w ią d o w ó d n a t w ie r d z e n ie m .
3 .4 .4 0 . W ykresy. Z m o d y fik u j k la s y Li nearProbi ngHashST i SeparateChai ni ngHashST,
ab y u m o ż liw ić g e n e ro w a n ie w y k re s ó w p o d o b n y c h d o ty c h z te k s tu .
3 .4 .4 1 . D w u k r o tn e p ró b y . P rz e p r o w a d ź b a d a n ia e k s p e r y m e n ta ln e , a b y o c e n ić s k u
te c z n o ś ć d w u k r o tn y c h p r ó b (z o b a c z ć w ic z e n ie 3 .4 . 2 7 ).
3 .4 .4 2 . P o d w ó jn e h a sz o w a n ie . P rz e p r o w a d ź b a d a n ia e k s p e r y m e n ta ln e , a b y o c e n ić
s k u te c z n o ś ć p o d w ó jn e g o h a s z o w a n ia (z o b a c z ć w ic z e n ie 3 .4 .2 8 ).
3 .4 .4 3 . P ro b le m p a r k o w a n ia (a u to r — D . K n u th ). P rz e p r o w a d ź b a d a n ia e k s p e r y
m e n ta ln e , a b y p o tw ie r d z ić h ip o te z ę , z g o d n ie z k tó r ą lic z b a p o r ó w n a ń p o tr z e b n y c h
d o w s ta w ie n ia za p o m o c ą p ró b k o w a n ia lin io w e g o M lo s o w y c h ld u c z y d o ta b lic y
o w ie lk o ś c i M w y n o s i ~ cA P 12, g d z ie c = ~Jn! 2 .
3.5. Z A S T O S O W A N IA
od po czątków in f o r m a t y k i, k ie d y to ta b lic e s y m b o li u m o ż liw iły p r o g r a m is to m
p rz e jś c ie z lic z b o w y c h a d re s ó w i ję z y k a m a s z y n o w e g o n a n a z w y s y m b o lic z n e i ję z y
k i a se m b le ro w e , p o w s p ó łc z e s n e z a s to s o w a n ia z n o w e g o ty s ią c le c ia , w k tó r y c h n a
z w y s y m b o lic z n e m a ją o k re ś lo n e z n a c z e n ie w o g ó ln o ś w ia to w y c h s ie c ia c h k o m p u
te ro w y c h , sz y b k ie a lg o r y tm y w y s z u k iw a n ia o d g ry w a ły i n a d a l o d g ry w a ją k lu c z o w ą
ro lę. W s p ó łc z e s n e z a s to s o w a n ia ta b lic s y m b o li o b e jm u ją p o r z ą d k o w a n ie d a n y c h n a
u k o w y c h ( o d w y s z u k iw a n ia m a r k e r ó w lu b w z o rc ó w w g e n o m ie p o tw o rz e n ie m a p
w sz e c h św ia ta ), p o rz ą d k o w a n ie w ie d z y w sie c i W W W ( o d w y s z u k iw a n ia w s k le p a c h
in te rn e to w y c h p o u d o s tę p n ia n ie z a s o b ó w b ib lio te k w sie ci) i im p le m e n to w a n ie i n
f r a s tr u k tu r y in te rn e to w e j ( o d p rz e k a z y w a n ia p a k ie tó w m ię d z y m a s z y n a m i w siec i
W W W p o s y s te m y w y m ia n y p lik ó w i s tr u m ie n io w ą tr a n s m is ję w id e o ). Te i n ie z li
c z o n e in n e w a ż n e z a s to s o w a n ia s ta ły się m o ż liw e d z ię k i w y d a jn y m a lg o r y tm o m w y
s z u k iw a n ia . W ty m p o d r o z d z ia le o m a w ia m y k ilk a r e p r e z e n ta ty w n y c h p rz y k ła d ó w .
O to o ne:
■ K lie n t u ż y w a ją c y s ło w n ik a i k lie n t u ż y w a ją c y in d e k s u , w k tó r y c h m o ż liw y je s t
sz y b k i i e la s ty c z n y d o s tę p d o in f o rm a c ji z p lik ó w C S V (i w p o d o b n y c h f o r m a
ta c h ) , p o w s z e c h n ie s to s o w a n y c h d o p rz e c h o w y w a n ia d a n y c h w sieci W W W .
■ Klient używ ający indeksu do budowania indeksów odwrotnych dla zbiorów
plików.
■ Typ danych oparty na m acierzy rzadkiej; tablica sym boli służy tu do rozw iązy
w ania problem ów znacznie większych niż te, z którym i m ożna sobie poradzić
za pom ocą standardowej implementacji.
W r o z d z ia l e 6 . ro z w a ż a m y ta b lic ę s y m b o li o d p o w ie d n ią d la ta b e l z b a z d a n y c h
i s y s te m ó w p lik ó w , o b e jm u ją c y c h b a r d z o d u ż ą lic z b ę k lu c z y ( ta k d u ż ą , j a k m o ż n a
s o b ie w y o b ra z ić ).
T ab lice s y m b o li o d g ry w a ją te ż k lu c z o w ą ro lę w a lg o r y tm a c h o m a w ia n y c h w d a l
szej c z ę śc i k sią ż k i. T ab lic y s y m b o li u ż y w a m y n a p rz y k ła d d o re p r e z e n to w a n ia g ra fó w
( r o z d z i a ł 4 .) i p r z e tw a r z a n ia ła ń c u c h ó w z n a k ó w ( r o z d z i a ł 5 .).
Ja k p o k a z a n o w ty m ro z d z ia le , o p ra c o w a n ie im p le m e n ta c ji ta b lic y sy m b o li, k tó re
g w a ra n tu ją w y s o k ą w y d a jn o ś ć w s z y s tk ic h o p e ra c ji, je s t t r u d n y m z a d a n ie m . J e d n a k
o p is a n e im p le m e n ta c je są d o k ła d n ie p r z e b a d a n e , p o w s z e c h n ie s to s o w a n e i d o s tę p n e
w w ie lu ś r o d o w is k a c h p r o g r a m o w y c h (w ty m w b ib lio te k a c h Javy). O d tej p o r y p o
w in ie n e ś tr a k to w a ć a b s tra k c y jn ą ta b lic ę s y m b o li ja k k lu c z o w y e le m e n t T w o jej p r o
g ra m is ty c z n e j s k r z y n k i n a rz ę d z i.
498
3.5 ■ Zastosowania 499
Którą implementację tablicy symboli powinienem zastosować? Tabe
la w dolnej części tej strony obejmuje podsum owanie właściwości algorytmów, opisa
nych w twierdzeniach i cechach w tym rozdziale (wyjątkiem są w yniki dla — bardzo
rzadko spotykanego w praktyce — najgorszego przypadku w haszowaniu, pochodzące
z literatury naukowej). Z tabeli wyraźnie wynika, że w typowych zastosowaniach nale
ży wybierać m iędzy tablicą z haszowaniem a binarnym drzewem wyszukiwań.
Z a le ty h a s z o w a n ia w p o r ó w n a n iu z d rz e w a m i B ST to : p r o s ts z y k o d i o p ty m a ln y
(s ta ły ) czas w y s z u k iw a n ia , je ś li k lu c z e są s ta n d a rd o w e g o ty p u lu b są w y sta rc z a ją c o
p ro s te , a b y m o ż n a b y ło o p ra c o w a ć d la n ic h w y d a jn ą fu n k c ję h a s z u ją c ą , k tó r a w p r z y
b liż e n iu s p e łn ia z a ło ż e n ie o ró w n o m ie r n y m ro z k ła d z ie k lu czy . A o to z a le ty d rz e w
BST: są o p a r te n a p r o s ts z y m a b s tra k c y jn y m in te rfe js ie (n ie tr z e b a p ro je k to w a ć f u n k
cji h a sz u ją c e j); c z e rw o n o -c z a rn e d rz e w a B ST z a p e w n ia ją g w a ra n c je w y d a jn o ś c i d la
n a jg o rsz e g o p rz y p a d k u ; o b s łu g u ją w ię k sz y z e sta w o p e ra c ji ( n a p r z y k ła d o k re ś la n ie
p o z y c ji, w y b ie ra n ie , s o r to w a n ie i w y s z u k iw a n ie z a k re s o w e ). S to s u ją c p r o s tą re g u łę ,
p r o g r a m iś c i w y b ie ra ją h a s z o w a n ie , c h y b a że w a ż n y je s t p rz y n a jm n ie j je d e n z c z y n
n ik ó w w y m ie n io n y c h ja k o z a le ty d rz e w B ST — w te d y u ż y w a n e są c z e rw o n o -c z a rn e
d rz e w a BST. W r o z d z ia l e 5 . b a d a m y je d e n z w y ją tk ó w o d tej reg u ły . Jeśli k lu c z a m i
są d łu g ie ła ń c u c h y z n a k ó w , m o ż n a z b u d o w a ć s t r u k tu r y d a n y c h je s z c z e b a rd z ie j e la
sty c z n e o d c z e rw o n o -c z a rn y c h d rz e w B ST i je s z c z e sz y b sz e n iż h a s z o w a n ie .
Koszt Koszt
dla najgorszego przypadku dla typowego przypadku interfejs
Algorytm Pamięć
(po N wstawieniach) (po IMlosowych wstawieniach) klucza
(struktura danych) (w bajtach)
Wyszukiwanie Wstawianie Trafienie Wstawianie
Wyszukiwanie Al Al N I2 N equal s() 48 N
sekwencyjne (lista
nieuporządkowana)
Wyszukiwanie lg N N Ig h l N I2 compareTo() 16 N
binarne (tablica
uporządkowana)
Drzewo BST N N 1,39 lg Al 1,39 l g N compareTo() 64 Al
Drzewo wyszukiwań 2 lg Al 2 lg N 1,00 lg Al 1,00 lg Al compareTo() 64 N
2-3 (czerwono-czarne
drzewo BST)
Metoda łańcuchowa1 < lg2V < lg A l A1/(2M) N IM equal s() 48 N + 64
(tablica list) hashCode()
M
Próbkowanie liniowe1 c lg N c lg A l < 1 ,5 0 < 2 ,5 0 equal s() M ięd zy 32
(równoległe tablice) hashCode() Al a 128 Al
f Z funkcją haszującą działającą równomiernie i niezależnie
Podsumowanie asymptotycznych kosztów dla implementacji tablicy symboli
50 0 R O ZD ZIA Ł 3 » W yszukiw anie
Przedstaw ione przez nas im plem entacje tablicy sym boli są przydatne w wielu za
stosow aniach, przy czym opisane algorytm y m ożna łatwo zaadaptow ać p o d kątem
kilku innych możliwości. Rozw iązania te są pow szechnie stosow ane i w arto się im
przyjrzeć.
T ypy p ro ste Załóżmy, że tablica sym boli obejm uje klucze całkowitoliczbowe i p o
wiązane liczby zm iennoprzecinkow e. W standardow ym podejściu klucze i w artości
są zapisane za pom ocą typów nakładkow ych Integer i Double, dlatego potrzebne
są dwie dodatkow e referencje do pam ięci w celu uzyskania dostępu do każdej pary
klucz-w artość. Referencje te nie stanow ią problem u w aplikacji, która tysiące razy
w yszukuje tysiące kluczy, je d n a k m ogą prow adzić do nadm iernych kosztów, jeśli
trzeba m iliardy razy przeszukiw ać m iliony kluczy. Zastosow anie typu prostego za
m iast typu Key pozw ala zaoszczędzić jed ną referencję na każdą parę klucz-w artość.
Jeżeli pow iązana w artość też jest typu p ro
S tan d ard o w a im plem entacja
stego, m ożna pom inąć kolejną referencję.
Sytuację tę przedstaw iono po prawej dla Dane znajdują się
w obiektach Key i Val ue
m etody łańcuchow ej. Te sam e kwestie trz e / \
ba uw zględnić dla innych im plem entacji.
W zastosow aniach, gdzie wydajność o d
gryw a krytyczną rolę, w arto (i nietru d n o)
□
opracow ać wersje im plem entacji działające
w ten sposób (zobacz ć w i c z e n i e 3 .5 .4 ).
Im plem entacja o p a rta na ty p a c h prostych
P o w ta rza ją ce się k lu cze M ożliwość p o
Dane są przechowywane
w tarzania się kluczy w ym aga czasem spe w węzłach listy powiązanej
cjalnego zastanow ienia przy im plem ento
w aniu tablicy symboli. W w ielu zastosow a A
niach w arto pow iązać wiele w artości z tym
sam ym kluczem . Przykładowo, w systemie
przetw arzania transakcji liczne transakcje
m ogą mieć ten sam klucz klienta. Przyjęta Wykorzystanie pamięci w metodzie łańcuchowej
przez nas konw encja niedopuszczania do
pow tarzania się kluczy sprow adza się do pozostaw ienia zarządzania pow ielanym i
kluczam i klientowi. Przykładow ego klienta tego rodzaju opisujem y w dalszej części
podrozdziału. W wielu przedstaw ionych tu im plem entacjach m ożna zastanowić się
n a d pozostaw ieniem p ar klucz-w artość z pow tarzającym i się kluczam i w podstaw o
wej strukturze danych w spom agającej w yszukiwanie i zwracać dowolną w artość o d a
nym kluczu. M ożna też dodać m etody zwracające wszystkie w artości o danym klu
czu. Pokazane im plem entacje drzew BST i haszow ania nietru d n o zaadaptow ać w taki
sposób, aby przechow yw ać pow tarzające się klucze w strukturze danych. Uzyskanie
tego efektu dla czerw ono-czarnych drzew BST jest tylko trochę trudniejsze (zobacz
ć w i c z e n i a 3 . 5.9 i 3 . 5 . 1 0 ). Takie im plem entacje są często opisywane w literaturze
(w tym we wcześniejszych w ydaniach tej książki).
3.5 o Zastosowania 501
Biblioteki Javy Biblioteki j a v a . ú t il .TreeMap i j a v a . ú t il .HashMap Javy to im ple
m entacje tablicy sym boli oparte na czerw ono-czarnych drzew ach BST i haszow aniu
z w ykorzystaniem m etody łańcuchow ej. Biblioteka TreeMap nie obsługuje bezpo
średnio m eto d rank(), se le c t() i innych operacji na interfejsie API dla upo rząd
kowanych tablic symboli. Biblioteka HashMap jest w przybliżeniu odpow iednikiem
opracowanej przez nas im plem entacji klasy LinearProbingST. W ykorzystano w niej
zm ienianie wielkości tablicy w celu w ym uszenia w spółczynnika zapełnienia rów ne
go około 75%.
książce używamy dla tablicy sym boli im
aby t e k s t b y ł spójn y i k o n k re tn y , w
plem entacji opartej na czerw ono-czarnych drzew ach BST ( p o d r o z d z i a ł 3 .3 ) lub
opartej na haszow aniu z próbkow aniem liniow ym ( p o d r o z d z i a ł 3 .4 ). Z uwagi
na zwięzłość i w celu podkreślenia niezależności klienta od konkretnej im plem en
tacji, w kodzie klienta stosujem y litery ST jako skróconą nazwę klasy RedBlackBST
dla uporządkow anych tablic sym boli i określenie HashST jako skróconą nazwę klasy
Li nearProbi ngHashST (stosowaną, jeśli kolejność nie jest ważna i dostępne są funkcje
haszujące). Przyjm ujem y te konwencje, wiedząc, że w konkretnych zastosowaniach
niezbędne m ogą być inne wersje albo rozszerzenia jednego z algorytm ów lub jednej
ze stru k tu r danych. Której tablicy sym boli pow inieneś używać? N iezależnie od decy
zji przetestuj w ybraną wersję, aby się upew nić, że zapew nia oczekiw aną wydajność.
Interfejs API dla zbiorów N iektóre ldienty tablic sym boli nie wymagają p o
bierania wartości. Potrzebna jest jedynie m ożliw ość w staw iania kluczy do tablicy
i spraw dzania, czy klucz się w niej znajduje. Ponieważ pow tarzające się klucze są nie
dopuszczalne, operacje odpow iadają pokazanem u poniżej interfejsowi API. W ażny
jest tu tylko zbiór kluczy tablicy, a nie pow iązane z nim i wartości.
public c la s s SET<Key>
SET() Tworzy zbiór pusty
void add(Key key) Dodaje klucz key do zbioru
void delete(Key key) Usuwa klucz key ze zbioru
boolean co nta ins(K ey key) Czy klucz key znajduje się w zbiorze?
boolean isEmpty() Czy zbiór jest pusty?
int size() Zwraca liczbę kluczy w zbiorze
String to Strin g () Łańcuchowa reprezentacja zbioru
Interfejs API dla podstaw ow ego typu danych dla zbiorów
D ow olną im plem entację tablicy sym boli m ożna przekształcić na im plem entację typu
SET, pom ijając w artości lub używając prostej klasy nakładkowej (zobacz ć w i c z e n i a
od 3 . 5 .1 do 3 . 5 .3 ).
502 RO ZD ZIA Ł 3 a W yszukiw anie
R o z w in ię c ie ty p u SET ta k , a b y o b e jm o w a ł o p e ra c je o b lic z a n ia sumy, części wspól
nej, dopełnienia i in n e c z ę sto s to s o w a n e m a te m a ty c z n e o p e ra c je n a z b io ra c h , w y m a
g a b a rd z ie j z a a w a n s o w a n e g o in te rfe js u A P I (p rz y k ła d o w o , o p e ra c ja dopełnienia w y
m a g a m e c h a n iz m u d o o k re ś la n ia p rz e s trz e n i w s z y s tk ic h m o ż liw y c h k lu c z y ). S ta w ia
to p r z e d p r o g r a m is tą sz e re g c ie k a w y c h w y z w a ń a lg o ry tm ic z n y c h , co o m ó w io n o
w ć w i c z e n i u 3 .5 .1 7 .
T a k ja k d la ty p u ST, ta k i d la ty p u SET is tn ie ją w e rsje n ie u p o r z ą d k o w a n e i u p o
rz ą d k o w a n e . Jeśli k lu c z e są z g o d n e z in te rfe js e m Comparabl e, m o ż n a d o d a ć m e to d y
min(), max(), floor(), c e il in g ( ) , deleteMin(), deleteMax(), rank(), se le c t() o ra z
d w u a rg u m e n to w e w e rsje m e to d si ze () i get ( ) . P o w s ta n ie w te n s p o s ó b p e łn y in t e r
fejs A P I d la u p o rz ą d k o w a n y c h k lu czy . A b y d o s to s o w a ć się d o k o n w e n c ji z a s to s o w a
n ej d la n a z w y ST, u ż y w a m y o k re ś le n ia SET w k o d z ie k lie n ta d la z b io r ó w u p o r z ą d k o
w a n y c h i n a z w y HashSET, je ś li k o le jn o ś ć n ie m a z n a c z e n ia .
W r a m a c h p rz e d s ta w ia n ia z a s to s o w a ń k la s y SET o m a w ia m y k lie n ty filtrujące, k tó
re w c z y tu ją c ią g ła ń c u c h ó w z n a k ó w ze s ta n d a rd o w e g o w e jśc ia i p r z e k a z u ją w y b ra n e
ła ń c u c h y z n a k ó w d o s ta n d a rd o w e g o w y jśc ia. T a k ie k lie n ty p o r a z p ie r w s z y p o ja w iły
się w e w c z e sn y c h s y s te m a c h , w k tó r y c h p a m ię ć g łó w n a b y ła z d e c y d o w a n ie za m a ła n a
p o m ie s z c z e n ie w s z y s tk ic h d a n y c h . K lie n ty te n a d a l są p r z y d a tn e p r z y p is a n iu p r o g r a
m ó w p o b ie r a ją c y c h d a n e w e jśc io w e z sie c i W W W . Jak o p rz y k ła d o w e d a n e w e jśc io w e
w y k o rz y s ta m y p lik [Link] (z o b a c z s tr o n ę 3 8 3 ). W p rz y k ła d a c h — z u w a g i n a
c z y te ln o ść — z a c h o w u je m y n a w y jśc iu se k w e n c je n o w e g o w ie rs z a z w e jśc ia , c h o ć
k o d te g o n ie ro b i.
p ub lic c l a s s DeDup
Usuwanie p o w tó rzeń P ro to ty p o w y m {
p rz y k ła d e m k lie n ta filtru jąc e g o je s t p ub lic s t a t i c void m a in ( S tr in g [] args )
{
k lie n t k la sy SET lu b HashSET, k tó r y u su w a
HashSET<String> set;
p o w tó rz e n ia ze s tru m ie n ia w e jśc io w e set = new H as h S E T < S tr in g> ();
go. O p e ra c ję tę n a z y w a się usuwaniem while ( I S t d l n . i s E m p t y ())
powtórzeń (an g . dedup). P ro g r a m p rz e {
S t r i n g key = S t d l n . r e a d S t r i n g O ;
c h o w u je zb ió r n a p o tk a n y c h d o tej p o ry i f ()
k lu czy w p o s ta c i ła ń c u c h ó w znak ó w . Jeśli 1
[Link](key);
n a s tę p n y k lu c z znajduje się w zb io rze,
StdO [Link](key);
n a le ż y go p o m in ą ć . Jeżeli w z b io rz e nie
1
m a k lu cza , n a le ż y go d o d a ć i w yśw ietlić. 1
K lucze p o ja w ia ją się w s ta n d a rd o w y m 1
w y jściu w k o lejn o śc i, w jak iej w y stę p u ją
w s ta n d a rd o w y m w ejściu , p rz y c z y m d u Usuwanie powtórzeń
p lik a ty są p o m ija n e . P ro c e s te n w y m a g a
p a m ię c i w ilo ści p ro p o rc jo n a ln e j do % java DeDup < t i n y T a l e . t x t
i t was the best of times worst
liczb y ró ż n y c h k lu c zy w s tr u m ie n iu w ej
age wisdom f o o li s h n e s s
ścio w y m (zw y k le lic z b a ta je s t z n a c z n ie epoch b e l i e f i n c r e d u l i t y
m n ie js z a o d łączn ej liczb y klu czy ). season l i g h t darkness
sp rin g hope winte r d espair
3.5 ■ Zastosowania 503
B ia łe i c z a r n e li s t y I n n y k la s y c z n y filtr k o rz y s ta z k lu c z y z o d rę b n e g o p lik u d o d e
c y d o w a n ia , k tó r e k lu c z e ze s tr u m ie n ia w e jśc io w e g o n a le ż y p r z e p u ś c ić d o s tr u m i e n ia
w y jścio w eg o . T e n o g ó ln y p ro c e s m a w ie le n a tu r a ln y c h z a s to s o w a ń . N a jp ro s ts z y m
p rz y k ła d e m są b ia łe listy. K a ż d y k lu c z , k tó r y z n a jd u je się w p lik u z b ia łą listą , je s t
u z n a w a n y za „ d o b r y ”. K lie n t m o ż e p rz e k a z y w a ć d o s ta n d a rd o w e g o w y jśc ia k a ż d y
k lu c z , k tó r y n ie z n a jd u je się n a b ia łe j liśc ie , i p o m ija ć w sz y stk ie k lu c z e z n a jd u ją
ce się n a ta k ie j liśc ie ( ta k ja k w p rz y k ła d z ie o m ó w io n y m w p ie r w s z y m p r o g r a m ie
w r o z d z i a l e i.) . I n n y k lie n t m o ż e p rz e k a z y w a ć d o s ta n d a rd o w e g o w y jśc ia k a ż d y
k lu c z n ie z n a jd u ją c y się n a b ia łe j liśc ie i p o m ija ć w sz y stk ie k lu c z e , k tó r e z n a jd u ją się
n a ta k ie j liśc ie ( ta k d z ia ła k lie n t W h i t e F i l t e r
k la s y HashSET). W p r o g r a m ie p o c z to w y m p ub lic c l a s s W h i t e F i lt e r
m o ż n a w y k o rz y s ta ć filtro w a n ie p rz e z o k re ś le {
pub lic s t a t i c void m a in ( S tr in g [] args)
n ie a d re s ó w z n a jo m y c h i u z n a n ie w ia d o m o ś c i 1
o d in n y c h o s ó b z a s p a m . P rz e d s ta w io n y p r o HashSET<String> set;
set = new H a s h S E T < S tr in g> ();
g ra m tw o rz y o b ie k t HashSET n a p o d s ta w ie k lu
In in = new I n ( a r g s [0 ]) ;
czy z p o d a n e j listy, a n a s tę p n ie w c z y tu je k lu c z e while ( ! i n . isEm pty())
ze s ta n d a rd o w e g o w e jśc ia. Jeśli n a s tę p n y k lu c z [Link]([Link] );
z n a jd u je się w z b io rz e , n a le ż y g o w y św ie tlić . while ( I S t d l n . i s E m p t y O )
{
Jeżeli k lu c z n ie z n a jd u je się w z b io rz e , je s t ig
S t r i n g word = S t d l n . r e a d S t r i n g O ;
n o ro w a n y . C za rn a lista p e łn i o d w r o tn ą fu n k c ję i f (set.c ontain s(w ord))
i o b e jm u je „z łe ” k lu c z e . T ak że tu is tn ie ją d w ie StdOut. pri n t ln ( w o r d ) ;
n a tu r a ln e m e to d y filtro w a n ia . W p r z y k ła d o
w y m p r o g r a m ie p o c z to w y m m o ż n a o k re ś lić
a d re s y z n a n y c h s p a m e ró w i n a k a z a ć p r z e p u s z
c z a n ie w s z y s tk ic h w ia d o m o ś c i p o c h o d z ą c y c h Filtrowanie na podstawie białej listy
o d in n y c h n a d a w c ó w . M o ż n a z a im p le m e n to
w a ć k lie n ta BI ack F i 1 t e r k la s y HashSET, w k tó
% more l i s t . t x t
r y m z a n e g o w a n y je s t te s t filtru ją c y z p r o g r a was i t the of
m u W h i t e F i l t e r . W ty p o w y c h p ra k ty c z n y c h
% java W h i t e F i lt e r l i s t . t x t < t i n y T a le . t x t
z a s to s o w a n ia c h , n a p r z y k ła d u o p e r a to r ó w
it was the of i t was the of
k a r t k re d y to w y c h u ż y w a ją c y c h c z a rn y c h list it was the of i t was the of
d o o d filtro w y w a n ia n u m e r ó w s k ra d z io n y c h it was the of i t was the of
it was the of i t was the of
k a r t k r e d y to w y c h lu b w r u te r z e in te r n e to w y m
it was the of i t was the of
z b ia łą listą , d z ia ła ją c y m ja k z a p o ra , z w y k le
u ż y w a n e są b a r d z o d łu g ie lis ty i n ie o g r a n i % java B l a c k F i l t e r l i s t . t x t < t i n y T a l e . t x t
c z o n e s tr u m ie n ie w e jśc io w e o ra z o b o w ią z u ją best times worst times
age wisdom age f o o li s h n e s s
śc isłe w y m o g i c o d o c z a s u re a k c ji. O m ó w io n e
epoch b e l i e f epoch i n c r e d u l i t y
ro d z a je im p le m e n ta c ji ta b lic y s y m b o li u m o ż li season l i g h t season darkness
w ia ją ła tw ą o b s łu g ę ty c h w a ru n k ó w . s p rin g hope winte r despair
504 R O ZD ZIA Ł 3 o W yszukiw anie
Klienty używające słownika N a jb a rd z ie j p o d s ta w o w y ro d z a j k lie n ta ta b lic y
sy m b o li tw o rz y ta k ą ta b lic ę za p o m o c ą k o le jn y c h o p e ra c ji dodaj w c e lu u m o ż liw ie
n ia o b s łu g i ż ą d a ń pobierz. W w ie lu a p lik a c ja c h w y k o rz y s ta n o p o m y s ł z a s to s o w a n ia
ta b lic y s y m b o li ja k o dynamicznego s ło w n ik a , w k tó r y m m o ż n a ła tw o w y sz u k iw a ć
in f o rm a c je oraz je a k tu a liz o w a ć . P o n iż s z a lis ta z n a n y c h p rz y k ła d ó w d o w o d z i u ż y
te c z n o ś c i te g o p o d e jś c ia .
D Książka telefoniczna. Jeśli k lu c z e to n a z w is k a o só b , a w a r to ś c i to n u m e r y te le fo
n ó w , ta b lic a s y m b o li je s t o d p o w ie d n ik ie m k s ią ż k i te le fo n ic z n e j. B a rd z o is to tn ą
r ó ż n ic ą w p o r ó w n a n iu z p a p ie r o w ą k s ią ż k ą te le fo n ic z n ą je s t to , że m o ż n a d o d a
w a ć n o w e n a z w is k a lu b z m ie n ia ć is tn ie ją c e n u m e r y te le fo n ó w . P o n a d to m o ż n a
u ż y ć n u m e r u te le fo n u ja k o k lu c z a , a n a z w is k a — ja k o w a rto ś c i. Jeśli je sz c z e
n ig d y te g o n ie ro b iłe ś, s p ró b u j w p is a ć sw ój n u m e r te le fo n u (w ra z z n u m e r e m
k ie r u n k o w y m ) w w y sz u k iw a rc e .
■ Słownik. W ią z a n ie s łó w z ic h d e fin ic ja m i to z n a n a te c h n ik a , o d k tó re j p o c h o
d z i n a z w a „ s ło w n ik ”. O d s tu le c i lu d z ie tr z y m a ją w d o m a c h i b iu r a c h p a p ie ro w e
s ło w n ik i, a b y s p ra w d z a ć d e fin ic je i p is o w n ię (w a rto ś c i) słó w (k lu c z y ). Z u w a g i
n a d o b re im p le m e n ta c je ta b lic s y m b o li u ż y tk o w n ic y o c z e k u ją w b u d o w a n y c h
m o d u łó w s p r a w d z a n ia p is o w n i i n a ty c h m ia s to w e g o d o s tę p u d o d e fin ic ji słów .
■ Informacje o kontach. P o s ia d a c z e a k c ji re g u la rn ie s p ra w d z a ją ic h o b e c n ą c e n ę za
p o m o c ą sie c i W W W . K ilk a s e rw is ó w in te r n e to w y c h łą c z y s y m b o l a k c ji (k lu c z )
z jej o b e c n ą c e n ą (w a rto ś ć ), a z w y k le ta k ż e z w ie lo m a in n y m i in f o rm a c ja m i.
T e c h n ik a t a z n a jd u je w ie le k o m e r c y jn y c h z a s to s o w a ń . P rz y k ła d o w o , in s ty tu c je
fin a n s o w e w ią ż ą in f o rm a c je o k o n c ie z n a z w is k ie m lu b n u m e r e m k o n ta , a j e d
n o s tk i e d u k a c y jn e łą c z ą o c e n y z n a z w is k ie m s tu d e n ta lu b n u m e r e m id e n ty fi
k a c y jn y m .
° Badania genomu. S y m b o le o d g ry w a ją k lu c z o w ą ro lę w e w s p ó łc z e s n y c h b a
d a n ia c h n a d g e n o m e m . N a jp ro s ts z y m p r z y k ła d e m je s t w y k o rz y s ta n ie lite r A,
C, T i G d o r e p r e z e n to w a n ia n u k le o ty d ó w z n a le z io n y c h w D N A o rg a n iz m ó w .
D r u g im z n a jp r o s ts z y c h p rz y k ła d ó w je s t z a le ż n o ś ć m ię d z y k o d o n a m i (tró jk a m i
n u k le o ty d ó w ) a a m in o k w a s a m i (TTA to le u c y n a , TCT o d p o w ia d a s e r y n ie i ta k
d a le j), a ta k ż e m ię d z y s e k w e n c ja m i a m in o k w a s ó w a b ia łk a m i. B a d a c z e g e n o m u
s ta n d a r d o w o k o rz y s ta ją z ró ż n e g o ro d z a ju ta b lic s y m b o li d o p o r z ą d k o w a n ia
w iedzy .
° Dane z eksperymentów. W s p ó łc z e ś n i n a u k o w c y z d z ie d z in o d a stro fiz y k i p o
z o o lo g ię g e n e ru ją w ie lk ie ilo śc i d a n y c h z e k s p e ry m e n tó w . P o rz ą d k o w a n ie ty c h
d a n y c h i w y d a jn y d o s tę p d o n ic h są b a r d z o w a ż n e d o ic h z ro z u m ie n ia . T ab lice
s y m b o li to k lu c z o w y p u n k t w y jśc ia , a z a a w a n s o w a n e s t r u k tu r y d a n y c h i a lg o r y t
m y o p a r te n a ta b lic a c h s y m b o li są o b e c n ie w a ż n ą c z ę śc ią b a d a ń n a u k o w y c h .
* Kompilatory. J e d n y m z p ie rw s z y c h z a s to s o w a ń ta b lic s y m b o li b y ło p o r z ą d k o w a
n ie in f o r m a c ji n a p o tr z e b y p r o g r a m o w a n ia . P o c z ą tk o w o p r o g r a m y b y ły c ią g a m i
liczb , je d n a k p ro g r a m iś c i sz y b k o o d k ry li, że d u ż o w y g o d n ie js z e je s t s to s o w a n ie
n a z w s y m b o lic z n y c h d la o p e ra c ji i lo k a liz a c ji w p a m ię c i (n a z w z m ie n n y c h ).
P o w ią z a n ie n a z w z n u m e r a m i w y m a g a ta b lic y s y m b o li. W ra z z r o s n ą c ą d ł u
3.5 n Zastosowania 50 5
g o śc ią p r o g r a m ó w k o s z t o p e ra c ji n a ta b lic y s y m b o li s ta w a ł się w ą s k im g a rd łe m
w c zasie ro z w ija n ia p ro g r a m u , co d o p ro w a d z iło d o p o w s ta n ia s t r u k tu r d a n y c h
i a lg o r y tm ó w p o d o b n y c h d o ty c h o m ó w io n y c h w ro z d z ia le .
° Systemy plików. R e g u la rn ie k o rz y s ta m y z ta b lic s y m b o li d o p o r z ą d k o w a n ia d a
n y c h w s y s te m a c h k o m p u te ro w y c h . P r a w d o p o d o b n ie n a jb a rd z ie j z n a n y m p r z y
k ła d e m je s t system plików, k tó r y łą c z y n a z w ę p lik u (k lu c z ) z m ie js c e m p r z e c h o
w y w a n ia je g o z a w a rto ś c i (w a rto ś c ią ).
Obszar Klucz Wartość
W o d tw a r z a c z u m u z y c z n y m p o d o b n y
s y s te m w ią ż e ty tu ły u tw o ró w (k lu c z e ) Książka N azw isk o N um er
z lo k a liz a c ja m i n a g r a ń (w a rto ś c ia m i). telefoniczna tele fo n u
D Internetowy system DNS. S y stem n a z w
Słownik Słow o D efin icja
d o m e n y (an g . domain name system —
D N S ) s ta n o w i p o d s ta w ę p rz y p o r z ą d Konto N u m e r k o n ta S tan k o n ta
k o w a n iu in fo rm a c ji w in te rn e c ie i łąc z y
Badania genomu Ko d o n A m in o k w a s
a d re s y U R L (k lu cze; n a p rz y k ła d www.
p r i n c e to n .e d u lu b w w w .w ik ip e d ia .p l) Dane D a n e i czas W y n ik i
z ro z u m ia łe d la lu d z i z a d re s a m i IP (w a r Kompilator N azw a L okalizacja
to ś c ia m i; n a p rz y k ła d [Link] z m ie n n ej w p a m ię c i
lu b [Link]) z ro z u m ia ły m i d la
System wymiany T ytuł K o m p u te r
r u te ró w w sieci k o m p u te ro w e j. S y stem
plików u tw o ru
te n to „ k sią ż k a te le fo n ic z n a ” n o w e j g e
n e ra c ji. L u d z ie m o g ą u ż y w a ć ła tw y c h Internet W itry n a A d res IP
d o z a p a m ię ta n ia nazw , a k o m p u te ry
Typowe zastosowania słowników
— w w y d a jn y sp o s ó b p rz e tw a rz a ć liczby.
L iczb a w y sz u k iw a ń w ta b lic y sy m b o li
w y k o n y w a n a w ty m c e lu w ru te ra c h in te rn e to w y c h n a c a ły m św iecie je s t n ie z w y
k le d u ż a , d la te g o w y d a jn o ść je st, o czy w iście, is to tn a . D o in te r n e tu k a ż d e g o r o k u
p o d łą c z a n e są m ilio n y n o w y c h k o m p u te ró w i in n y c h u rz ą d z e ń , d la te g o ta b lic e
sy m b o li w r u te r a c h in te rn e to w y c h m u s z ą b y ć d y n a m ic z n e .
M im o r ó ż n o r o d n o ś c i lis ta ta to ty lk o re p r e z e n ta ty w n a p ró b k a , k tó r a m a d a ć p r z e d
s m a k w ie lo r a k ic h z a s to s o w a ń a b s tra k c y jn y c h ta b lic s y m b o li. K ie d y k o lw ie k o k re ś la sz
co ś z a p o m o c ą n azw y , k o rz y s ta s z z ta b lic y s y m b o li. S y ste m p lik ó w w k o m p u te r z e lu b
sieć W W W m o g ą ro b ić to z a C ie b ie , je d n a k g d z ie ś u ż y w a n a je s t ta k a ta b lic a .
W r a m a c h k o n k r e tn e g o p r z y k ła d u ro z w a ż m y k lie n ta ta b lic y sy m b o li, k tó re g o
m o ż n a u ż y ć d o w y s z u k iw a n ia in f o rm a c ji p rz e c h o w y w a n y c h w ta b e li w p lik u lu b n a
s tro n ie in te rn e to w e j w fo r m a c ie wartości rozdzielonych przecinkami (a n g . comma-
separated-value — ,csv). T e n p r o s ty f o r m a t p o z w a la z re a liz o w a ć z a d a n ie ( p r z y z n a
jem y , że m a ło a m b itn e ) p rz e c h o w y w a n ia d a n y c h ta b e la ry c z n y c h w fo r m ie c zy te ln e j
d la k a ż d e g o (i p r a w d o p o d o b n ie m o ż liw e j d o o d c z y ta n ia w p rz y s z ło ś c i) b e z k o n ie c z
n o ś c i s to s o w a n ia sp e c ja ln e j a p lik a c ji. D a n e m a ją p o s ta ć te k s to w ą , p o je d n y m w ie r
s z u n a lin ię , a w a rto ś c i są r o z d z ie lo n e p rz e c in k a m i. W w itr y n ie p o ś w ię c o n e j k sią ż c e
m o ż n a z n a le ź ć w ie le p lik ó w ,csv p o w ią z a n y c h z r ó ż n y m i o p is a n y m i z a s to s o w a n ia m i.
P rz y k ła d o w e p lik i to : [Link] (o d w z o ro w a n ia k o d o n ó w n a a m in o k w a s y ), [Link]
506 RO ZD ZIA Ł 3 ■ W yszukiw anie
% more [Link] (c e n a o tw a rc ia , w o lu m e n i c e n a z a m k n ię c ia d la
TTT,Phe,F,Phenyl alanine in d e k s u D JIA z k a ż d e g o d n ia n o to w a ń ) , [Link]
TTC,Phe,F,Phenyl alanine
TTA,Leu,L,Leucine
(w y b ó r w p is ó w z b a z y d a n y c h D N S ) i u p [Link]
TTG,Leu,L,Leucine (k o d k re s k o w e U P C p o w s z e c h n ie sto s o w a n e
TCT,Ser,S,Serine
d o id e n ty fik o w a n ia p r o d u k tó w ) . A rk u s z e k a l
TCC,Ser,S,Serine
k u la c y jn e i in n e p r o g r a m y d o p rz e tw a r z a n ia
GAA,Gly,G,Glutamic Acid
d a n y c h p o tr a fią w c z y ty w a ć o ra z z a p is y w a ć p li
GAG,Gly.G,Glutamic Acid
GGT,Gly,G,Glycine k i ,csv. W p rz y k ła d a c h p o k a z a n o , że m o ż n a te ż
GGC,Gly,G,Glyci ne n a p is a ć p r o g r a m Javy d o p r z e tw a r z a n ia ta k ic h
GGA,Gly,G,Glyci ne
GGG,Gly,G,Glycine d a n y c h w d o w o ln y sp o s ó b .
P r o g r a m LookupCSV ( n a n a s tę p n e j s tro n ie )
% more [Link]
tw o rz y z b ió r p a r k lu c z - w a r to ś ć n a p o d s ta w ie
20-0ct-87,1738.74,608099968,1841.01 p lik u C S V p o d a n e g o w w ie rs z u p o le c e ń , a n a
19-0ct-87,2164.16,604300032,1738.74
s tę p n ie w y św ie tla w a rto ś c i o d p o w ia d a ją c e k lu
16-0ct-87,2355.09,338500000,2246.73
15-0ct-87,2412.70,263200000,2355.09 c z o m w c z y ta n y m ze s ta n d a rd o w e g o w ejścia.
A r g u m e n ta m i p o d a w a n y m i w w ie rs z u p o le c e ń
30-0ct-29,230.98,10730000,258.47
29-0ct-29,252.38,16410000,230.07 są n a z w a p lik u i d w ie lic z b y c a łk o w ite (je d n a
28-0ct-29,295.18,9210000,260.64 o k re ś la p o le u ż y w a n e ja k o k lu c z , a d r u g a —
25-0ct-29,299.47,5920000,301.22
p o le p e łn ią c e fu n k c ję w a rto ś c i).
P rz y k ła d te n m a ilu s tro w a ć p rz y d a tn o ś ć
% more [Link]
i e la s ty c z n o ś ć a b s tra k c y jn e j ta b lic y s y m b o li.
www. e b a y . com, 6 6 .1 3 5 .1 9 2 . 8 7 Jak a w itr y n a m a a d re s IP128.112.136.35?www.
www.p r in c e t o n . e d u , 1 2 8 . 1 1 2 . 1 2 8 . 1 5
c s . pri nceton. edu. K tó ry a m in o k w a s o d p o w ia
w w w .cs. p r i n c e t o n . e d u , 1 2 8 . 1 1 2 .1 3 6 . 3 5
w w w .ha rva rd . e d u ,1 2 8 . 1 0 3 . 6 0 . 2 4 d a k o d o n o w i TCA? Seri ne. Jak i b y ł k u rs DJLA 29
www.y a le . e d u , 1 3 0 . 1 3 2 . 5 1 . 8
p a ź d z ie r n ik a 1929 ro k u ? 252.38. K tó ry p r o d u k t
[Link] n .c o m ,6 4 .2 3 6 .1 6 .2 0
w w w .[Link] ,2 1 6 .2 3 9 .4 1 . 9 9 m a k o d U P C 0002100001085? Kraft Parmesan.
www. n y t im e s . com ,1 9 9 .2 3 9 .1 3 6 .2 0 0 Z a p o m o c ą p r o g r a m u LookupCSV i w ła śc iw y c h
w w w .a p p le .c o m ,1 7 .1 1 2 .1 5 2 .3 2
[Link] a s h d o t . o r g , 6 6 . 3 5 . 2 5 0 . 1 5 1 p lik ó w ,csv m o ż n a ła tw o z n a le ź ć o d p o w ie d z i
www.e s p n .c o m ,1 9 9 .1 8 1 .1 3 5 .2 0 1 n a p y ta n ia te g o ro d z a ju .
w w w .w [Link] ,6 3 . 111. 6 6 .1 1
w w w .[Link] ,2 1 6 .1 0 9 .1 1 8 .6 5
W y d a jn o ś ć n ie j e st p r o b le m e m p r z y o b s łu d z e
z a p y ta ń in te ra k ty w n y c h (p o n ie w a ż k o m p u te r
p o tr a fi s p ra w d z ić m ilio n y w p is ó w w cz a sie p o
% more [Link]
tr z e b n y m n a w p is a n ie z a p y ta n ia ), d la te g o p rz y
0002058102040.,"1 1/4"” STANDARD STORM DOOR"
k o rz y s ta n iu z p r o g r a m u LookupCSV sz y b k ie i m
0002058102057.,"1 1/4"" STANDARD STORM DOOR"
0002058102125.,"DELUXE STORM DOOR UNIT" p le m e n ta c je k la s y ST są n ie o d c z u w a ln e . J e d n a k
0002082012728,"100/ per box","12 gauge shells" k ie d y p ro g r a m p rz e s z u k u je d a n e (a je s t ic h b a r
0002083110812,"Classical CO","'Bits and Pieces"’
002083142882,CD,"Garth Brooks - Ropin1 The Wind" d z o d u ż o ), w y d a jn o ś ć je s t w a ż n a . P rz y k ła d o w o ,
0002094000003,LB,"PATE PARISIEN" r u t e r in te r n e to w y m u s i c z a s e m p rz e s z u k iw a ć
0002098000009,LB,"PATE TRUFFLE COGNAC-M&H 8Z RW"
0002100001086,"16 02 “,"Kraft Parmesan"
m ilio n y a d re s ó w IP n a s e k u n d ę . W k sią ż c e
0002100002090,"15 pieces","Wrigley's Gum" p o k a z a n o ju ż p o tr z e b ę z a p e w n ie n ia w y so k ie j
0002100002434,"One pint","Trader Joe's milk"
w y d a jn o ś c i w p r o g r a m ie FrequencyCounter.
W ty m p o d r o z d z ia le p r z e d s ta w io n o k ilk a i n
Typowe pliki CSV n y c h p rz y k ła d ó w .
3.5 Zastosowania 507
Wyszukiwanie w słowniku
p u b li c c l a s s LookupCSV
p u b li c s t a t i c v o id m a i n ( S t r i n g [ ] a r g s )
{
In in = new l n ( a r g s [ 0 ] ) ;
i n t k e y F ie ld = I n t e g e r . p a r s e l n t ( a r g s [ l ] ) ;
i n t v a lF ie ld = I n t e g e r .p a r s e ln t ( a r g s [ 2 ] ) ;
S T < S tr in g , S tr in g > s t = new S T < S tr in g , S t r i n g > ( ) ;
w h ile ( i n .h a s N e x tL in e ( ) )
{
S tr in g li n e = in .r e a d L in e Q ;
S t r i n g [ ] to k e n s = 1 i n e . s p l i t ( " , " ) ;
S t r i n g key = t o k e n s [ k e y F i e l d ] ;
S tr in g val = to k e n s [ v a lF i e ld ] ;
s t.p u t(k e y , v a l) ;
}
w h ile ( I S t d l n .i s E m p t y O )
{
S t r i n g q u e ry = S t d l n . r e a d S t r i n g O ;
i f ( s t.c o n ta in s (q u e ry ))
S td O u t.p r in tln ( s t.g e t( q u e r y ) ) ;
}
Ten stero w an y d a n y m i k lie n t tab lic y sy m b o li w czytuje p a ry k lu c z -w a rto ść z plik u , a n a
stęp n ie w y św ietla w a rto śc i o d p o w ia d a ją c e k lu c z o m z n a le z io n y m w s ta n d a rd o w y m w yjściu.
K lucze i w a rto śc i są ła ń c u c h a m i znaków . O g ra n ic z n ik jest p o b ie ra n y ja k o a rg u m e n t z w ie r
sza p o leceń .
% java LookupCSV ip . c s v 1 0 % java LookupCSV [Link] 0 3
[Link] TCC
[Link] Seri ne
% java LookupCSV [Link] 0 3 % ja va LookupCSV [Link] 0 2
29-0 ct-29 0002100001086
230.07 K ra ft Parmesan
508 R O ZD ZIA Ł 3 n W yszukiw anie
W ć w ic z e n ia c h o p is a n o p o d o b n e , a le b a rd z ie j z a a w a n s o w a n e k lie n ty te s to w e d la p li
k ó w ,csv. P rz y k ła d o w o , m o ż n a u tw o rz y ć d y n a m ic z n y s ło w n ik , z e z w a la ją c n a m o d y f i
k a c ję (za p o m o c ą p o le c e ń ze s ta n d a rd o w e g o w e jśc ia ) w a rto ś c i p o w ią z a n e j z k lu c z e m .
M o ż n a te ż u m o ż liw ić w y s z u k iw a n ie z a k re s o w e lu b b u d o w a n ie w ie lu s ło w n ik ó w n a
p o d s ta w ie je d n e g o p lik u .
Klienty używające indeksu S ło w n ik i a m in o i.t x t
c e c h u ją się ty m , że z k a ż d y m k lu c z e m p o w ią Al ani n e ,A A T ,A A C ,G C T ,G C C ,G C A ,GCG
Arginine,CGT,C G C ,CGA,CGG,A G A ,AGG
z a n a je s t je d n a w a rto ś ć . M o ż n a w ię c b e z p o Aspartic Acid, g a t ,GAC
ś r e d n io w y k o rz y s ta ć ty p d a n y c h ST, o p a r ty Cystei n e ,T G T ,TGC
Glutamic Acid,GAA,GAG
n a a b s tra k c y jn e j ta b lic y a so c ja c y jn e j, łąc z ąc e j Glutami n e ,c a a ,CAG
z k a ż d y m k lu c z e m je d n ą w a rto ś ć . K a ż d y n u Gl yci n e ,g g t ,g g c ,g g a ,GGG Separator
Histidine,CAT,CAC
m e r k o n ta je d n o z n a c z n ie id e n ty fik u je k lie n Isoleucine,ATT,a t c ,ATA J
ta , k a ż d y k o d U P C je d n o z n a c z n ie o k re ś la Leuci n e ,T T A ,T T G ,CTT,CTC,C T A ,LTG
Lysi n e ,A A A ,AAG
p r o d u k t itd . O g ó ln ie , o c z y w iśc ie , z d a n y m
[Link]
k lu c z e m p o w ią z a n y c h m o ż e b y ć w ie le w a r Phenyl al anine,T T T ,TTC
to ś c i. P rz y k ła d o w o , w p lik u a m in o .c sv k a ż d y P roi i n e ,C C T ,c c c ,C C A ,CCG
Se ri n e ,T C T ,T C A ,T C G ,A G T ,AGC
k o d o n o k re ś la a m in o k w a s , a le k a ż d y a m i n o Stop,TAA,TAG,TGA
k w a s p o w ią z a n y je s t z lis tą k o d o n ó w , ta k ja k Th reoni n e ,A C T ,A C C ,A C A ,ACG
Tyrosine,TAT,TAC
w p rz y k ła d o w y m p lik u a m in o i.t x t w id o c z T ryptophan,TGG
n y m p o p ra w e j, w k tó r y m k a ż d y w ie rs z o b e j valine, g t t ,g t c ,g t a ,g t g
m u je a m in o k w a s i listę o d p o w ia d a ją c y c h m u t H / /
k o d o n ó w . In d e k s to ta b lic a sy m b o li, w k tó re j Klucz Wartości
z k a ż d y m k lu c z e m p o w ią z a n y c h je s t w iele Krótki plik indeksu (20 wierszy)
w a rto ś c i. O to k ilk a in n y c h p rz y k ła d ó w :
* T ra n sa kcje h a n d lo w e . J e d n y m ze s p o s o b ó w ś le d z e n ia tr a n s a k c ji z d a n e g o d n ia
w firm ie p rz e c h o w u ją c e j k o n ta k lie n tó w je s t u tr z y m y w a n ie in d e k s u ty c h t r a n s
ak cji. K lu c z e m je s t n u m e r k o n ta , a w a rto ś c ią — lis ta w y s tą p ie ń n u m e r u n a li
ście tra n s a k c ji.
“ W y s z u k iw a n ie w sieci W W W . K ie d y w p isu je sz sło w o k lu c z o w e i o tr z y m u je s z
listę o b e jm u ją c y c h je w itr y n , k o rz y s ta s z z in d e k s u u tw o rz o n e g o p rz e z w y s z u
k iw a rk ę . Z k a ż d y m k lu c z e m (z a p y ta n ie m ) p o w ią z a n a je s t je d n a w a rto ś ć (z b ió r
s tr o n ) , c h o ć w p ra k ty c e je s t to b a rd z ie j s k o m p lik o w a n e , p o n ie w a ż c z ę sto p o d a je
się w ie le klu czy .
■ F ilm y i w y k o n a w c y . P lik m o vies. t x t z w itr y n y (jeg o f r a g m e n t z n a jd u je się n a d o le
n a s tę p n e j s tro n y ) p o c h o d z i z b a z y IM D B (an g . In te r n e t M o v ie D a ta b a se ). K a ż d y
w ie rs z to ty t u ł film u (k lu c z ), p o k tó r y m n a s tę p u je lis ta w y k o n a w c ó w (w a rto ś c i)
ro z d z ie lo n y c h u k o ś n ik a m i.
3.5 a Zastosowania 509
In d e k s m o ż n a ła tw o z b u d o w a ć , u m ie s z c z a ją c w a r to ś c i w ią z a n e z k a ż d y m k lu c z e m
w p o je d y n c z e j s t r u k tu r z e d a n y c h ( n a p r z y k ła d Queue), a n a s tę p n ie łą c z ą c k lu c z
z w a r to ś c ią w p o s ta c i s t r u k t u r y d a n y c h . R o z w in ię c ie p r o g r a m u LookupCSV w te n
s p o s ó b je s t ła tw e , je d n a k p o z o s ta w ia m y to ja k o ć w ic z e n ie (z o b a c z ć w i c z e n i e
3 . 5 . 1 2 ) i w z a m ia n o m a w ia m y p r o g r a m Lookuplndex ze s t r o n y 5 1 1 , w k tó r y m t a b
lic ę s y m b o li w y k o r z y s ta n o d o z b u d o w a n ia in d e k s u n a p o d s ta w ie p lik ó w w r o d z a ju
a m in o I .t x t i m o v ie s .tx t ( s e p a r a
to r e m n ie m u s i b y c t u in a c z e j Dziedzina Klucz Wartość
n iż w p lik a c h ,csv — p rz e c in e k ;
Badania'nad genomem A m in o k w a s L ista k o d o n ó w
znak m ożna o k re ś lić w w ie r
sz u p o le c e ń ) . Po z b u d o w a n iu Handel N u m e r k o n ta L ista tra n sa k c ji
in d e k s u p ro g ra m Lookuplndex
Wyszukiwanie K lucz L ista
p rz y jm u je z a p y ta n ia o k lu c z
w sieci W W W w y sz u k iw a n ia s tro n W W W
i w y ś w ie tla w a r to ś c i p o w ią z a
n e z k a ż d y m k lu c z e m . C o c ie Baza IMDB F ilm L ista w y k o n aw có w
k a w sz e , p ro g ram Lookuplndex Typowe zastosowania indeksów
tw o rz y te ż in d e k s o d w r o tn y ,
w k tó r y m w a r to ś c i i k lu c z e p e łn i ą o d w r o tn e f u n k c je . W p r z y k ła d z ie d o ty c z ą c y m
a m in o k w a s ó w p r o g r a m d a je w ię c te s a m e m o ż liw o ś c i, c o p r o g r a m Lookup (p o z w a
la z n a le ź ć a m in o k w a s p o w ią z a n y z d a n y m k o d o n e m ) . N a p o d s ta w ie lis ty film ó w
i w y k o n a w c ó w m o ż liw e je s t d o d a tk o w o z n a le z ie n ie film ó w p o w ią z a n y c h z d a n y m
a k to r e m . P o ś r e d n io d a n e z a w ie ra ją te in f o r m a c je , j e d n a k t r u d n o je s t je u z y s k a ć
b e z z a s to s o w a n ia ta b lic y s y m b o li. S ta r a n n ie p r z e a n a liz u j te n p r z y k ła d , p o n ie w a ż
p o m a g a d o b r z e z r o z u m ie ć n a tu r ę ta b lic s y m b o li.
m o v ie s.t x t
Separator,,/"
T i n Men ( 1 9 8 7 ) / D e B o y , D a v id / B iu m e n fe id , A l a n / . . . /
T i r e z s u r l e p i a n i s t e (1960 )/H e ym a n n , C la u d e / . . . r
T i t a n i c ( 1 9 9 7 ) / M a z in , S t a n / . . . D i c a p r i o , L e o n a r d o / .. .
T i t u s ( 1 9 9 9 ) / w e is s k o p f , H erm ann/R hys, M a tt h e w / ...
To Be o r N ot t o Be ( 1 9 4 2 ) / v e r e b e s , E rn o ( I ) / . ..
To Be o r N ot t o Be ( 1 9 8 3 ) / . . . / B r o o k s , Mel ( I ) / . . .
To C a tc h a T h i e f ( 1 9 5 5 ) / P a r i s , M a n u e l/ . . .
To D ie F o r ( 1 9 9 5 ) / S m it h , K u r t w o o d / . . ./K idm an, N i c o l e / . . .
Klucz Wartości
Mały fragment dużego pliku z indeksem (ponad 250 000 wierszy)
RO ZD ZIA Ł 3 W yszukiw anie
In d e lc s o d w r o t n y In d e k s o d w r o tn y z w y k le u ż y w a n y je s t w sy tu a c ji, w k tó re j w a rto ś c i
słu ż ą d o lo k a liz o w a n ia k lu czy . D o s tę p n y c h je s t d u ż o d a n y c h i c h c e m y u s ta lić , g d z ie
z n a jd u ją się p o tr z e b n e k lu c z e . T a k d z ia ła k o le jn y p ro to ty p o w y k lie n t, w k tó r y m w y
m ie s z a n e są w y w o ła n ia g e t () i p u t ( ) . T a k ż e tu k a ż d y k lu c z w ią z a n y je s t ze s t r u k tu r ą
SET z lo k a liz a c ja m i, w k tó r y c h z n a jd u je się d a n y k lu c z . N a tu r a i s p o s ó b w y k o rz y s ta
n ia lo k a liz a c ji z a le ż y o d p r o g r a m u . W k sią ż c e lo k a liz a c ją m o ż e b y ć n u m e r s tro n y ;
w p r o g r a m ie — n u m e r w ie rs z a ; w b a d a n ia c h n a d g e n o m e m — p o z y c ja w se k w e n c ji
g e n e ty c z n e j itd .
n B a z a IM D B . W o m ó w io n y m p rz y k ła d z ie d a n e w e jśc io w e to in d e k s łą c z ą c y
k a ż d y film z lis tą w y k o n a w c ó w . In d e k s o d w r o tn y w ią ż e k a ż d e g o a k to r a z listą
film ó w .
° In d e k s k sią żk i. K a ż d y p o d r ę c z n ik m a in d e k s , w k tó r y m m o ż n a z n a le ź ć p o ję
cie i n u m e r stro n y , g d z ie o n o w y stę p u je . C h o ć u tw o rz e n ie d o b re g o in d e k s u
w y m a g a o d a u to r a w y e lim in o w a n ia p o to c z n y c h i n ie is to tn y c h słów , sy s te m
p rz y g o to w y w a n ia in d e k s u z p e w n o ś c ią k o rz y s ta z ta b lic y s y m b o li i w s p o m a g a
c a ły p ro c e s . C ie k a w y m
s p e c ja ln y m p rz y p ad Dziedzina Klucz Wartość
k ie m je s t sk o ro w id z.
Baza IMDB A k to r Z b ió r film ó w
Jego p rz y g o to w a n ie
p o le g a n a p o w ią z a n iu Książka P ojęcie Z b ió r stro n
k a ż d e g o sło w a z te k s Kompilator Id e n ty fik a to r Z b ió r m iejsc uży cia
tu ze z b io r e m pozy
Przeszukiwanie S zu k an e Z b ió r p lik ó w
cji, n a k tó r y c h sło w o
plików p o jęcie
to w y s tę p u je (z o b a c z
ć w i c z e n i e 3 . 5 . 2 0 ). Badania P o d se k w en c ja Z b ió r lo k alizacji
D K o m p ila to r. W d u ż y c h nad genomem
p ro g r a m a c h , w k tó Typowe indeksy odwrotne
r y c h u ż y w a n a je s t d u ż a
lic z b a sy m b o li, w a r to w ie d z ie ć , g d z ie w y k o rz y s ta n o k a ż d ą n a z w ę . H is to ry c z n ie
d r u k o w a n e ta b lic e s y m b o li b y ły je d n y m z n a jw a ż n ie js z y c h n a r z ę d z i s to s o w a
n y c h p rz e z p r o g r a m is tó w d o ś le d z e n ia m ie js c u ż y c ia s y m b o li w p ro g r a m a c h .
W e w s p ó łc z e s n y c h s y s te m a c h ta b lic e s y m b o li są p o d s ta w ą n a r z ę d z i p r o g r a m i
s ty c z n y c h u ż y w a n y c h p rz e z p r o g r a m is tó w d o z a rz ą d z a n ia n a z w a m i.
n P r z e s z u k iw a n ie p lik ó w . W s p ó łc z e s n e s y s te m y o p e ra c y jn e u m o ż liw ia ją w p is a n ie
p o ję c ia i z n a le z ie n ie n a z w z a w ie ra ją c y c h je p lik ó w . K lu c z e m je s t p o ję c ie , a w a r
to ś c ią — z b ió r o b e jm u ją c y c h je p lik ó w .
■ B a d a n ia n a d g e n o m e m . W ty p o w y m (c h o ć m o ż e n a d m ie r n ie u p ro s z c z o n y m )
s c e n a r iu s z u z b a d a ń n a d g e n o m e m n a u k o w ie c c h c e u s ta lić p o z y c je d a n e j s e k
w e n c ji g e n e ty c z n e j w is tn ie ją c y m g e n o m ie lu b z b io rz e g e n o m ó w . Is tn ie n ie lu b
b lisk o ść p e w n y c h se k w e n c ji m o ż e m ie ć z n a c z e n ie n a u k o w e . P u n k te m w y jśc ia
d o ta l a c h b a d a ń je s t in d e k s p r z y p o m in a ją c y s k o ro w id z , a le z m o d y f ik o w a
n y z u w a g i n a to , że g e n o m y n ie są p o d z ie lo n e n a sło w a (z o b a c z ć w i c z e n i e
3-5-15)-
3.5 Zastosowania 511
Przeszukiwanie indeksu (i indeksu odwrotnego)
public c la s s Lookuplndex
{
public s t a t i c void tnain(String[] args)
{
In in = new I n ( a r g s [ 0 ] ) ; // Baza danych dla indeksu.
S t r in g sp = a r g s [1]; // Separator.
ST<String, Q ueu e <Strin g» st = new ST<String, Q u e u e < S t r i n g » ( ) ;
ST<String, Queue<String>> ts = new ST<String, Q u e u e < S t r i n g » ( ) ;
while ([Link]())
{
S t r i n g [ ] a = in .re a d Lin e Q . s p l i t ( s p ) ;
S trin g key = a [ 0 ] ;
f o r (in t i = 1; i < [Link]; i++)
{
S trin g val = a [ i ];
i f (is t. c o n ta in s (k e y ) ) [Link](key, new Queue<String>());
i f ( it s . c o n t a in s ( v a l ) ) t s . p u t ( v a l, new Queue<String>());
[Link](key).enqueue(val);
t s . g e t ( v a l ) .enqueue(key);
}
}
while (¡S td ln .isE m p ty O )
(
S t r in g query = S t d ln . r e a d L i n e Q ; % java Lookuplndex [Link] t
i f ([Link] tains(qu e ry)) Se rin e
fo r (S t rin g s : [Link] t(qu e ry)) TCT
S td O u t .p rin t ln (" " + s ) ; TCA
TCG
i f ([Link] tains(qu e ry)) AGT
fo r (S trin g s : ts .ge t(qu e ry )) AGC
TCG
S td O u t .p rin t ln (" " + s ) ;
Seri ne
}
} % java Lookuplndex movies.t xt “/"
} Bacon, Kevin
M ystic R iv er (2003)
Frid ay the 13th (1980)
T en stero w an y d a n y m i k lie n t ta b lic y sy m b o li w czy F l a t l i n e r s (1990)
tu je z p lik u p a ry k lu c z -w a rto ść, a n a stę p n ie w y Few Good Men, A (1992)
św ietla w a rto ś c i o d p o w ia d a ją ce k lu c z o m p o d a n y m
w sta n d a rd o w y m w ejściu. K lu czam i są ła ń c u c h y Tin Men (1987)
Blumenfeld, Alan
znaków , a w a rto ś c ia m i — listy ła ń c u c h ó w znaków .
DeBoy, David
O g ra n ic z n ik je s t p o b ie ra n y ja k o a rg u m e n t z w iersza
p o leceń .
512 R O ZD ZIA Ł 3 o W yszukiw anie
P r o g r a m Fi 1 e In d e x (n a n a s tę p n e j s tro n ie ) p rz y jm u je z w ie rs z a p o le c e ń n a z w y p lik ó w
i w y k o rz y s tu je ta b lic ę s y m b o li d o z b u d o w a n ia in d e k s u o d w r o tn e g o łą c z ą c e g o k a ż d e
sło w o z d o w o ln e g o p lik u ze s t r u k tu r ą SET z n a z w a m i p lik ó w , w k tó r y c h d a n e s ło
w o się z n a jd u je . N a s tę p n ie p r o g r a m p rz y jm u je ze s ta n d a rd o w e g o w y jśc ia z a p y ta n ia
o sło w a k lu c z o w e i w y św ie tla p o w ią z a n e lis ty p lik ó w . P o d o b n ie d z ia ła ją p o p u la r n e
n a rz ę d z ia d o p rz e s z u k iw a n ia sie c i W W W lu b w y s z u k iw a n ia in f o rm a c ji n a k o m p u t e
rz e — n a le ż y w p is a ć sło w o k lu c z o w e , a b y u z y sk a ć listę m ie js c , w k tó r y c h w y stę p u je .
T w ó rc y ta k ic h n a r z ę d z i z w y k le w z b o g a c a ją p ro c e s , z w ra c a ją c b a c z n ą u w a g ę n a:
■ fo r m ę z a p y ta n ia ;
■ z b ió r in d e k s o w a n y c h p lik ó w lu b s tro n ;
n k o le jn o ś ć w y m ie n ia n ia p lik ó w w o d p o w ie d z i.
Z p e w n o ś c ią c z ę sto w p ro w a d z a s z w w y s z u k iw a rc e (k tó ra in d e k s u je d u ż ą c zę ść s tr o n
z sie c i W W W ) z a p y ta n ia o b e jm u ją c e w ie le słó w k lu c z o w y c h . W y s z u k iw a rk a z w ra c a
o d p o w ie d z i w e d łu g ic h a d e k w a tn o ś c i lu b z n a c z e n ia (d la C ie b ie a lb o re k la m o d a w -
c ó w ). W ć w ic z e n ia c h o p is a n y c h w k o ń c o w e j c z ę śc i p o d r o z d z i a łu p r z e d s ta w io n o n ie
k tó r e d o d a tk o w e te c h n ik i. D a le j o m a w ia m y ró ż n e k w e stie a lg o r y tm ic z n e z w ią z a n e
z p rz e s z u k iw a n ie m sie c i W W W , je d n a k ta b lic a s y m b o li je s t is to tą te g o p ro c e s u .
Z a c h ę c a m y , o c z y w iśc ie , d o p o b r a n i a p r o g r a m u Filelndex (a ta k ż e Lookuplndex)
z w itr y n y p o ś w ię c o n e j k sią ż c e i w y k o rz y s ta n ia g o d o z in d e k s o w a n ia k ilk u p lik ó w
te k s to w y c h n a T w o im k o m p u te r z e lu b in te re s u ją c y c h C ię w itr y n . P o z w o li to je s z c z e
b a rd z ie j d o c e n ić p r z y d a tn o ś ć ta b lic s y m b o li. Z o b a c z y s z te ż , że m o ż n a sz y b k o b u
d o w a ć d u ż e in d e k s y d la w ie lk ic h p lik ó w , p o n ie w a ż k a ż d a o p e ra c ja d o d a n ia i k a ż d e
ż ą d a n ie p o b r a n ia je s t w y k o n y w a n e b ły s k a w ic z n ie . Z a p e w n ie n ie n a ty c h m ia s to w e j r e
a k c ji d la d u ż y c h , d y n a m ic z n y c h ta b lic je s t je d n y m z k la s y c z n y c h o s ią g n ię ć w b a d a
n ia c h n a d a lg o ry tm a m i.
3.5 Zastosowania 513
Indeksowanie plików
import j a v a . i o . F i l e ;
public c la s s Filelndex
{
public s t a t i c void m ain(String[] args)
ST<String, S E T < F i l e » st = new ST<String, S E T < F i l e » ( ) ;
f o r (S t rin g filename : args)
{
F ile file = new Fi 1e(filename);
In in = new In ( fi le ) ;
while ( lin . is E m p t y O )
{
S t r in g word = i n . r e a d S t r i n g ( ) ;
i f ( 1 s t . contains(word)) [Link](word, new SET<Fi1e > ( ) );
SET<File> set = s t.g e t(w o rd );
se t.a dd (file );
while (IS t d ln .isE m p t y O )
{
S t r in g query = S t d l n . r e a d S t r i n g O ;
i f ([Link] tains(qu e ry ))
fo r ( F ile file : s t. g e t( q u e ry ))
S td O u t.p rin tln (" " + [Link]);
Ten k lie n t tab licy sy m b o li in d e k su je z b ió r plików . W tab lic y sy m b o li m o ż n a zn ale źć k ażd e
słow o z k ażd eg o p lik u . P ro g ra m p rz e c h o w u je o b ie k t SET z n a z w a m i p lik ó w zaw ierający ch
d a n e słow o. N azw y w o b ie k ta c h In m o g ą te ż d o ty czyć stro n in te rn e to w y c h , d lateg o k o d p o
zw ala p o n a d to b u d o w a ć in d e k sy o d w ro tn e d la ta k ic h stro n .
% more e x l . t x t % java File ln d e x e x * . t x t
i t was the best of times age
ex3.t xt
% more ex 2 .t xt ex4 .txt
i t was the worst of times best
[Link]
% more ex3 .t xt was
i t was the age of wisdom [Link]
ex 2 .txt
% more ex4 .t xt ex3.t xt
i t was the age of f o o li s h n e s s ex4.t xt
514 RO ZD ZIA Ł 3 □ W yszukiw anie
Wektory rzadkie W n a s tę p n y m p rz y k ła d z ie p o k a z a n o z n ac z e n ie tab lic sy m b o li
w o b lic z e n ia c h n a u k o w y c h i m a tem a ty c z n y c h . O p isu je m y tu p o d sta w o w e i z n a n e o b
liczenia, k tó re w ty p o w y c h p ra k ty c z n y c h za sto so w a n ia c h sta n o w ią w ąsk ie gard ło . D alej
p o k azu je m y , ja k ta b lic a sy m b o li p o z w a la w y e lim in o w ać w ąsk ie g a rd ło i u m o ż liw ić ro z
w iązan ie z n a c z n ie w ięk szy ch p ro b le m ó w . Te k o n k re tn e o b lic z e n ia b y ły p o d sta w ą o p r a
co w an eg o p rz e z S. B rin a i L. P a g e a a lg o ry tm u P ag eR an k , k tó r y n a p o c z ą tk u X X I w ie k u
d o p ro w a d z ił d o p o w sta n ia w y sz u k iw a rk i G o o g le (o b
a [] D x [] b []
licz e n ia te są d o b rz e z n a n ą m a te m a ty c z n ą a b stra k c ją
0 0.90 0 0 0 0,05' '0,036
p rz y d a tn ą ta k ż e w w ielu in n y c h k o n tek sta ch ).
0 0 0,36 0,36 0,18 0,04 0,297
0 0 0
P o d s ta w o w ą o p e ra c ją , k tó r ą o m a w ia m y , je s t
0,90 0 0,36 = 0,333
0,90 0 0 0 0 0,37 0,45 m n o ż e n ie m a c ie r z y p r z e z w e k to r. N a p o d s ta w ie
0,47 0 0,47 0 0 0,19 0,1927 m a c ie rz y i w e k to ra n a le ż y o b lic z y ć w y n ik o w y w e k
Mnożenie macierzy przez wektor to r, k tó re g o i- ty e le m e n t je s t ilo c zy n e m s k a la r n y m
d a n e g o w e k to ra o ra z i-te g o w ie rs z a m a c ie rz y . D la
u p ro s z c z e n ia ro z w a ż a m y sy tu a c ję , w k tó re j m a c ie rz je s t k w a d ra to w a (m a N w ie rs z y
i N k o lu m n ) , a w ie lk o ś ć w e k to ra to N . B a rd z o p ro s te je s t n a p is a n ie w Javie k o d u tej
o p e ra c ji, k tó r y d z ia ła w c z a sie p r o p o r c jo n a ln y m d o N 2 ( p o tr z e b a N o p e ra c ji m n o ż e
n ia d o o b lic z e n ia k a ż d e g o z N e le m e n tó w w y n ik o w e g o w e k to ra ) i w y m a g a p a m ię c i
w ilo śc i p r o p o r c jo n a ln e j d o N 2 (p a m ię ć p o tr z e b n a je s t n a z a p is a n ie m a c ie rz y ).
J e d n a k w p ra k ty c e N je s t c z ę sto b a r d z o d u ż e . P rz y k ła d o w o , w G o o g le u N to lic z b a
s tr o n w sie c i W W W . W c za sie p o w s ta w a n ia a lg o r y tm u P a g e R a n k b y ły d z ie s ią tk i lu b
s e tk i m ilia r d ó w s tro n . O d te g o c z a su ic h lic z b a z n a c z n ie w z ro s ła , d la te g o w a rto ś ć N 2
m o ż e w y n o s ić z n a c z n ie p o n a d 1020. W y m a g a to n ie o s ią g a ln e j ilo śc i c z a su i p a m ię c i,
d la te g o p o tr z e b n y je s t le p s z y a lg o ry tm .
N a szczęście , m a c ie rz c z ę sto je s t r z a d k a — o b e jm u je d u ż ą lic z b ę e le m e n tó w ró w
n y c h 0. W G o o g le ’u ś r e d n ia lic z b a n ie z e ro w y c h w a rto ś c i n a w ie rs z je s t m a łą sta łą .
P ra w ie w sz y stk ie s tr o n y W W W o b e jm u ją o d n o ś n ik i d o n ie lic z n y c h in n y c h s tr o n
(a n ie d o w s z y s tk ic h s tr o n z sie ci W W W ) . D la te g o m o ż n a p rz e d s ta w ić m a c ie rz ja k o
ta b lic ę r z a d k ic h w e k to ró w , u ż y w a ją c im p le
m e n ta c ji k la s y S p a rs e V e c to r, ta k ie j ja k k lie n t
k la s y HashST p rz e d s ta w io n y n a n a s tę p n e j s t r o dou ble[] [] a = new double[N] [N ];
n ie . Z a m ia s t u ż y w a ć k o d u a [ i ] [ j ] d o w s k a dou ble[] x = new doublejN];
doublej] b = new do uble [N];
z y w a n ia e le m e n tu z w ie rs z a i o ra z k o lu m n y
j , sto su j e m y in s tru k c j ę a [ i ] . p u t ( j , v a l) d o // Inicjo wanie a [ ] [ ] i x[].
u s ta w ia n ia w a rto ś c i w m a c ie rz y i p o le c e n ia
f o r ( in t i = 0; i < N; i++)
a [ i ] . g e t ( j ) d o p o b ie r a n ia w a rto ś c i. Ja k w i
{
d a ć w k o d z ie , m n o ż e n ie m a c ie rz y p rz e z w e k sum = 0. 0 ;
t o r za p o m o c ą tej k la s y je s t je s z c z e p ro s ts z e f o r ( i n t j = 0; j < N; j++)
sum += a [ i ] [ j ] * x [ j ] ;
n iż z a p o m o c ą r e p r e z e n ta c ji ta b lic o w e j ( p o
b [i ] = sum;
d e jś c ie to p o n a d to ja ś n ie j o p is u je o b lic z e n ia ).
C o w a ż n ie jsz e , ilo ść p o tr z e b n e g o c z a s u je s t
p r o p o r c jo n a ln a d o N p lu s lic z b a n ie z e ro w y c h Standardowa implementacja mnożenia
e le m e n tó w m a c ie rz y . macierzy przez wektor
3.5 Zastosowania 515
Wektor rzadki i iloczyn skalarny
public c la s s SparseVector
{
private HashST<Integer, Double> st;
public SparseVector()
( st = new HashST<Integer, Double>(); }
public in t s iz e ()
( return s t . s i z e Q ; }
public void p u t(in t i, double x)
( s t . put ( i , x); }
public double g e t ( in t i)
{
i f ( I s t . c o n t a i n s ( i ) ) return 0.0;
else return s t . g e t (i );
}
p ublic double dot(double[] that)
{
double sum = 0.0;
f o r (in t i : s t . k e y s Q )
sum += t h a t [ i ] * t h i s .g e t ( i );
return sum;
}
}
T en k lie n t ta b lic y sy m b o li to p ro s ta im p le m e n ta c ja w e k to ra rz a d k ie g o z w y d a jn y m o b li
c zan iem ilo czy n u sk alarn eg o . K o d m n o ż y k a ż d ą w a rto ść p rz e z jej o d p o w ie d n ik z d ru g ieg o
o p e ra n d u i d o d a je w y n ik d o łącznej sum y. L iczba p o trz e b n y c h m n o ż e ń jest ró w n a liczbie
n ieze ro w y ch e le m e n tó w w e k to ra rzadkiego.
516 RO ZDZIAŁ 3 □ W yszukiwanie
Tablica obiektów doubl e [] Tablica o b ie któw S p a r s e V e c t o r
0 1 2 3 4
0.0 .90 0.0 0.0 0.0
0 1 2 3 4
0.0 0.0 .3 6 .3 6 .1 8
0 1 2 3 4
0.0 0.0 0.0 .9 0 0.0
0 1 2 3 4
.9 0 0.0 0.0 0.0 0.0
0 1 2 3 4
.45 0.0 .45 0.0 0.0
a [4][2]
Reprezentacja macierzy rzadkiej
D la m a ły c h m a c ie rz y lu b m a c ie rz y , k tó r e n ie są rz a d k ie , k o s z ty p rz e c h o w y w a n ia
ta b lic y m o g ą b y ć is to tn e . W a rto je d n a k p o ś w ię c ić ch w ilę n a z ro z u m ie n ie sk u tk ó w
s to s o w a n ia ta b lic s y m b o li d la d u ż y c h m a c ie rz y r z a d k ic h . W y o b ra ź so b ie d u ż y p r o b
le m (ta k i ja k te n , p r z e d k tó r y m s ta n ę li B rin i P a g e ), w k tó r y m N w y n o s i 10 lu b 100
m ilia rd ó w , a ś r e d n ia lic z b a n ie z e ro w y c h e le m e n tó w n a w ie rs z je s t m n ie js z a n iż 1 0 .
W ta k ic h a p lik a c ja c h u ży c ie ta b lic sy m b o li p r z y s p ie s z a m n o ż e n ie m a c ie r z y p r z e z w e k
to r m ilia rd razy, a n a w e t b a rd zie j. P ro s ta n a tu r a a p lik a c ji n ie p o w in n a p rz e s ła n ia ć jej
z n a c z e n ia . P ro g r a m iś c i, k tó r z y n ie w y k o rz y s tu ją m o ż liw o ś c i z a o s z c z ę d z e n ia c z a su
i p a m ię c i w te n s p o s ó b , p o w a ż n ie o g ra n ic z a ją m o ż liw o ś ć ro z w ią z a n ia p ra k ty c z n y c h
p ro b le m ó w ; n a to m ia s t p r o g r a m iś c i, k tó r z y d e c y d u ją się n a m i lia r d k r o tn e p rz y s p ie
s z e n ie p r o g r a m u , je ś li je s t to w y k o n a ln e , p r a w d o p o d o b n ie z d o ła ją p o r a d z ić so b ie
z p r o b le m a m i n ie m o ż liw y m i d o ro z w ią z a n ia w in n y sp o s ó b .
B u d o w a n ie m a c ie rz y n a p o tr z e b y G o o g le a to z a d a n ie d la a p lik a c ji d o p r z e tw a
r z a n ia g ra fó w (i k lie n ta ta b lic y s y m b o li!), d z ia ła
jące j n a d u ż e j m a c ie rz y rz a d k ie j. Jeśli m a c ie rz je s t
SparseVector[] a;
d o s tę p n a , o b lic z a n ie w a rto ś c i P a g e R a n k p o le g a
a = new Spa rseV ector[N ];
n a m n o ż e n iu m a c ie rz y p rz e z w e k to r, z a s tę p o w a double[] x = new double [N];
n iu ź ró d ło w e g o w e k to ra w y n ik o w y m i p o w ta r z a doubl e [] b = new doubl e [N ];
n iu te g o p ro c e s u d o c z a s u u z y s k a n ia s p ó jn o ś c i
// Inicjo wanie a[] i x [ ] .
( g w a ra n tu ją to p o d s ta w o w e tw ie r d z e n ia z te o r ii
p ra w d o p o d o b ie ń s tw a ). D la te g o z a s to s o w a n ie k la f o r ( i n t i = 0; i < N; i++)
b[i] = a [i] ,dot(x);
sy w ro d z a ju S p a rs e V e c to r m o ż e p ro w a d z ić d o
o s z c z ę d n o ś c i w z a k re s ie c z a s u i p a m ię c i n a p o z io Mnożenie macierzy rzadkiej
m ie 1 0 , 1 0 0 , a n a w e t w ię ce j m ilia r d ó w razy. przez wektor
3.5 □ Zastosowania 517
P o d o b n e o s z c z ę d n o ś c i m o ż n a u z y sk a ć w w ie lu o b lic z e n ia c h n a u k o w y c h , d la te g o
rz a d k ie w e k to ry i m a c ie rz e są p o w s z e c h n ie s to s o w a n e i z w y k le w łą c z a n e w w y s p e
c ja liz o w a n e sy s te m y d o o b lic z e ń n a u k o w y c h . P rz y k o r z y s ta n iu z d u ż y c h w e k to ró w
i m a c ie rz y w a rto p r z e p r o w a d z ić p ro s te te s ty w y d a jn o ś c i, a b y n ie p o m in ą ć o k a z ji d o
u z y s k a n ia p rz e d s ta w io n y c h z y sk ó w w w y d a jn o ś c i. P o n a d to w ię k s z o ś ć ję z y k ó w p r o
g ra m o w a n ia u d o s tę p n ia w b u d o w a n e m e c h a n iz m y p r z e tw a r z a n ia ta b lic ty p ó w p r o
sty ch , d la te g o z a s to s o w a n ie ta b lic d la w e k to ró w , k tó r e n ie są rz a d k ie (ta k ja k w p r z y
k ła d z ie ), p o z w a la d o d a tk o w o p rz y s p ie sz y ć p ra c ę . W o m a w ia n y c h z a s to s o w a n ia c h
z p e w n o ś c ią w a r to d o b rz e z ro z u m ie ć k o s z ty i p o d ją ć o d p o w ie d n ie d e c y z je w z a k re s ie
im p le m e n ta c ji.
TA BLICE SYM BOLI SĄ G Ł Ó W N Y M W K Ł A D E M T E C H N IK A L G O R Y T M IC Z N Y C H W TOZWÓj
w s p ó łc z e s n e j in f r a s t r u k tu r y in f o rm a ty c z n e j z u w a g i n a m o ż liw o ś ć u z y s k a n ia b a r d z o
d u ż y c h o s z c z ę d n o ś c i w ró ż n o r o d n y c h p ra k ty c z n y c h z a s to s o w a n ia c h . T ab lic e s y m
b o li r o b ią ró ż n ic ę m ię d z y m o ż liw o ś c ią r o z w ią z a n ia d u ż e j g r u p y p r o b le m ó w a n ie
m o ż n o ś c ią p o r a d z e n ia s o b ie z n im i. W n ie w ie lu d z ie d z in a c h n a u k i lu b in ż y n ie rii
p r z e b a d a n o o d k ry c ia , k tó r e z m n ie js z a ją k o s z ty o 100 m ilia rd ó w razy. T ab lic e s y m b o li
są ta k im o d k r y c ie m , co p o k a z a n o w k ilk u p rz y k ła d a c h , a u s p r a w n ie n ia p rz y n io s ły
is to tn e p ra k ty c z n e efekty. O m ó w io n e s t r u k tu r y d a n y c h i a lg o r y tm y z p e w n o ś c ią n ie
są o s ta tn im sło w e m . W s z y s tk ie je o p ra c o w a n o w k ilk u o s ta tn ic h d z ie s ię c io le c ia c h ,
a ic h w ła ś c iw o ś c i n ie są w p e łn i z ro z u m ia łe . Z u w a g i n a ic h z n a c z e n ie im p le m e n
ta c je ta b lic s y m b o li w c ią ż są in te n s y w n ie b a d a n e p rz e z n a u k o w c ó w z c a łe g o św ia ta .
W w ie lu o b s z a r a c h s p o d z ie w a m y się n o w y c h ro z w ią z a ń w ra z ze w z ro s te m sk a li i z a
się g u z a s to s o w a ń ta b lic sy m b o li.
518 RO ZD ZIA Ł 3 o W yszukiw anie
| PY T A N IA I O D P O W IE D Z I
P. C z y o b ie k t SET m o ż e o b e jm o w a ć w a rto ś c i nul 1 ?
O . N ie. T a k ja k w ta b lic a c h sy m b o li, ta k i tu k lu c z a m i są o b ie k ty ró ż n e o d nul 1.
P. C z y s a m o b ie k t SET m o ż e m ie ć w a rto ś ć nul 1 ?
O . N ie. O b ie k t SET m o ż e b y ć p u s ty (n ie z a w ie ra ć o b ie k tó w ), je d n a k n ie m o ż e m ie ć
w a rto ś c i nul 1. Z m ie n n a ty p u SET ( ta k ja k k a ż d e g o ty p u d a n y c h w Javie) m o ż e m ie ć
w a rto ś ć nuli, o z n a c z a to je d n a k , że n ie w s k a z u je o b ie k tu SET. E fe k te m u ż y c ia i n
s tr u k c ji new d o u tw o rz e n ia o b ie k tu SET je s t z aw sz e o b ie k t r ó ż n y o d nul 1.
P. Jeśli w sz y stk ie d a n e z n a jd u ją się w p a m ię c i, n ie m a p o w o d u d o s to s o w a n ia filtra ,
p ra w d a ?
O . R zeczy w iśc ie . F iltro w a n ie p rz y n o s i n a jle p s z e efekty, k ie d y n ie w ia d o m o , ja k ie j
ilo śc i d a n y c h m o ż n a się s p o d z ie w a ć . W in n y c h s y tu a c ja c h te c h n ik a ta m o ż e b y ć
p rz y d a tn a , ale n ie je s t r o z w ią z a n ie m w s z y s tk ic h p ro b le m ó w .
P. M a m d a n e w a rk u s z u k a lk u la c y jn y m . C z y m o g ę u tw o rz y ć p r o g r a m p o d o b n y d o
LookupCSV d o ic h p rz e s z u k iw a n ia ?
O . P r o g r a m d o z a rz ą d z a n ia a rk u s z a m i k a lk u la c y jn y m i p r a w d o p o d o b n ie m a o p c ję
e k s p o r to w a n ia d a n y c h d o p lik u .csv, d la te g o m o ż e s z b e z p o ś r e d n io w y k o rz y s ta ć p r o
g r a m LookupCSV.
P. D o czeg o m o g ę p o tr z e b o w a ć p r o g r a m u F il e ln d e x ? C z y s y s te m o p e ra c y jn y n ie
ro z w ią z u je te g o s a m e g o p ro b le m u ?
O . Jeśli k o rz y s ta s z z s y s te m u o p e ra c y jn e g o , k tó r y z a s p o k a ja T w o je p o trz e b y , u ż y w aj
go n a d a l. P o d o b n ie j a k w ie le in n y c h p ro g ra m ó w , t a k i Fi 1 e ln d e x m a p o k a z y w a ć p o d
sta w o w e m e c h a n iz m y ró ż n y c h a p lik a c ji o ra z su g e ro w a ć m o ż liw o śc i.
P. D la c z e g o d o k la s y S p a rs e V e c to r n ie d o d a n o m e to d y d o t ( ) , k tó r a p rz y jm u je ja k o
a r g u m e n t o b ie k t ty p u S p a rs e V e c to r i z w ra c a o b ie k t te g o sa m e g o ty p u ?
O . To ta k ż e je s t d o b r y p o m y s ł, a je d n o c z e ś n ie c ie k a w e ć w ic z e n ie p ro g r a m is ty c z n e ,
w y m a g a ją c e n ie c o b a rd z ie j s k o m p lik o w a n e g o k o d u n iż z a p re z e n to w a n e ro z w ią z a n ie
(z o b a c z ć w i c z e n i e 3 . 5 . 1 6 ). N a p o tr z e b y o g ó ln e g o p r z e tw a r z a n ia m a c ie rz y w a r to d o
d a ć ta k ż e ty p S p a rs e M a tri x.
3.5 □ Zastosowania 519
|| ć w ic z e n ia
3 .5 .1 . Z a im p le m e n tu j ty p y SET i HashSET ja k o k la s y n a k ła d k o w e b ę d ą c e k lie n ta m i
k las ST i HashST. U d o s tę p n ij w y m y ś lo n e w a rto ś c i i z ig n o r u j je.
3 .5 .2 . O p ra c u j im p le m e n ta c ję S e q u e n ti a l SearchSE T d la ty p u SET. Z a c z n ij o d k o d u
k la sy S e q u e n ti a l S earch S T i u s u ń c a ły k o d z w ią z a n y z w a rto ś c ia m i.
3 .5 .3 . O p ra c u j im p le m e n ta c ję Bi n ary S earch S E T d la ty p u SET. Z a c z n ij o d k o d u k la s y
S e q u e n ti a l S earchS T i u s u ń c a ły k o d z w ią z a n y z w a rto ś c ia m i.
3 .5 .4 . O p ra c u j k la s y H ash S T in t i H ashS T double d o p rz e c h o w y w a n ia z b io ró w k lu c z y
ty p ó w p ro s ty c h i n t i doubl e. P rz e k s z ta łć ty p y g e n e ry c z n e n a ty p y p r o s te w k o d z ie
k la s y Li n e a rP r o b i ngHashST.
3 .5 .5 . O p ra c u j k la s y ST i n t i ST doubl e d o p rz e c h o w y w a n ia u p o rz ą d k o w a n y c h ta b lic
sy m b o li, k tó r y c h k lu c z e są ty p u p ro s te g o i n t i d o u b le . P rz e k s z ta łć ty p y g e n e ry c z n e
n a ty p y p r o s te w k o d z ie k la s y RedBl ackBST. P rz e te s tu j ro z w ią z a n ie , u ż y w a ją c ja k o
k lie n ta w e rsji k la s y S p a rs e V e c to r.
3 .5 .6 . O p ra c u j ld a s y H ashS E T int i H ashSE Tdouble d o p r z e c h o w y w a n ia z b io ró w
ld u c z y ty p u p ro s te g o i n t i doubl e. U s u ń k o d z w ią z a n y z w a r to ś c ia m i z ro z w ią z a n ia
ć w i c z e n i a 3 . 5 .4 .
3 .5 .7 . O p ra c u j M asy S E T in t i SE Tdouble d o p rz e c h o w y w a n ia z b io r ó w ld u c z y ty p u
p ro s te g o i n t i doubl e. U s u ń k o d z w ią z a n y z w a r to ś c ia m i z ro z w ią z a n ia ć w i c z e n i a
3-5-5-
3 .5 .8 . Z m o d y fik u j M asę Li n e a rP r o b i ngHashST ta k , a b y p rz e c h o w y w a ła p o w ta r z a ją
ce się ld u c z e w tab lic y . M e to d a g e t () m a z w ra c a ć d o w o ln ą w a rto ś ć p o w ią z a n ą z d a
n y m ld u c z e m , a m e t o d a d e l e t e () — u s u w a ć z ta b lic y w s zy s tk ie e le m e n ty o M u c z a c h
ró w n y c h d a n e m u .
3 .5 .9 . Z m o d y fik u j M asę BST ta k , a b y p rz e c h o w y w a ła p o w ta rz a ją c e się M u c z e w d r z e
w ie. M e to d a g e t () m a z w ra c a ć d o w o ln ą w a rto ś ć p o w ią z a n ą z d a n y m M u c z e m , a m e
to d a d el e t e () — u s u w a ć z d rz e w a w szy s tk ie w ę z ły o M u c z a c h ró w n y c h d a n e m u .
3 .5 .1 0 . Z m o d y fik u j M asę RedBl ackBST ta k , a b y p rz e c h o w y w a ła p o w ta rz a ją c e się
M u cze w d rz e w ie . M e to d a g e t () m a z w ra c a ć d o w o ln ą w a rto ś ć p o w ią z a n ą z d a n y m
M u czem , a m e to d a d e l e t e () — u s u w a ć z d rz e w a w szy s tk ie w ę z ły o M u c z a c h ró w n y c h
danem u.
520 RO ZD ZIA Ł 3 □ W yszukiw anie
ĆWICZENIA (ciąg dalszy)
3 . 5 . 1 1 . O p ra c u j k la s ę Mul t i SET. M a b y ć p o d o b n a d o k la s y SET, a le z e z w a la ć n a z a p is
ró w n y c h k lu c z y (je st to im p le m e n ta c ja w ie lo z b io r u ).
3 .5 .1 2 . Z m o d y fik u j p r o g r a m LookupCSV, a b y z k a ż d y m k lu c z e m w ią z a ł w sz y stk ie
w a rto ś c i w y s tę p u ją c e w p a r a c h k lu c z - w a r to ś ć z d a n y m k lu c z e m (a n ie ty lk o n a jn o w
szą w a rto ś ć , ta k ja k w a b s tra k c y jn e j ta b lic y a so c ja c y jn e j).
3 .5 .1 3 . U tw ó rz p ro g ra m RangeLookupCSV na p o d s ta w ie p ro g ram u LookupCSV.
P r o g r a m m a p rz y jm o w a ć ze s ta n d a rd o w e g o w e jśc ia d w ie w a r to ś c i k lu c z y i w y ś w ie t
la ć w sz y stk ie p a r y k lu c z - w a r to ś ć z p lik u ,csv, ta k ie że k lu c z e z n a jd u ją się w p o d a n y m
p rz e d z ia le .
3 .5 .1 4 . O p ra c u j i p rz e te s tu j m e to d ę s ta ty c z n ą i n v e r t ( ) , k tó r a ja k o a r g u m e n t p r z y j
m u je o b ie k t S T < S tri n g , B a g < S tri n g » i ja k o z w ra c a n ą w a rto ś ć g e n e ru je o d w r o tn o ś ć
d a n e j ta b lic y s y m b o li (ta b lic ę s y m b o li te g o s a m e g o ty p u ).
3 .5 .1 5 . N a p isz p ro g r a m , k tó r y p rz y jm u je ła ń c u c h z n a k ó w ze s ta n d a rd o w e g o w e jśc ia
i lic z b ę c a łk o w itą k ja k o a r g u m e n t w ie rs z a p o le c e ń , a n a s tę p n ie u m ie s z c z a w s t a n d a r
d o w y m w y jśc iu p o s o r to w a n ą listę k -g r a m ó w z n a le z io n y c h w ła ń c u c h u z n a k ó w , p rz y
cz y m p o k a ż d y m /c-g ra m ie n a le ż y p o d a ć je g o in d e k s w ła ń c u c h u .
3 .5 .1 6 . D o d a j d o k la s y S p a rs e V e c to r m e to d ę su m (), k tó r a p rz y jm u je ja k o a r g u m e n t
o b ie k t S p a rs e V e c to r i z w ra c a o b ie k t te g o ty p u , k tó r y je s t o b lic z o n ą w y ra z p o w y r a
zie s u m ą d a n e g o w e k to ra i w e k to ra p o d a n e g o ja k o a rg u m e n t. Uwaga: p o tr z e b n a je s t
m e to d a d el e t e ( ) (i sz c z e g ó ln a u w a g a n a p re c y z ję ) p r z y o b s łu d z e sy tu a c ji, w k tó re j
w a rto ś ć sta je się z e re m .
3.5 a Zastosowania 521
PROBLEMY DO ROZWIĄZANIA
3 .5 .1 7 . Z b io r y m a te m a ty c z n e . C e le m je s t o p ra c o w a n ie im p le m e n ta c ji in te rfe js u A P I
k la s y MathSET p rz e z n a c z o n e j d o p r z e tw a r z a n ia (z m ie n n y c h ) z b io r ó w m a te m a ty c z
n y ch .
public c l a s s MathSET<Key>
MathSET(Key[] universe) Tworzy zbiór
void add(Key key) Umieszcza klucz key w zbiorze
MathSET<Key> complemento Zwraca zbiór kluczy z przestrzeni, które nie
występują w zbiorze
void union(MathSET<Key> a) Umieszcza w zbiorze klucze z a, które jeszcze się
tu nie znajdują
void int er section(MathSET<Key> a) Usuwa ze zbioru wszystkie klucze, które nie
występują w a
void delete(Key key) Usuwa ze zbioru klucz key
boolean conta ins(K ey key) Czy klucz key znajduje się w zbiorze?
boolean isEmpty() Czy zbiór jest pusty?
int size () Zwraca liczbę kluczy w zbiorze
Interfejs API typu danych reprezentującego zbiór
Z a sto s u j ta b lic ę sy m b o li. D o d a tk o w e z a d a n ie : p rz e d s ta w z b io r y za p o m o c ą ta b lic
w a rto ś c i ty p u bool ean.
3 .5 . 1 8 . W ie lo zb io ry . P o p rz y jrz e n iu się ć w i c z e n i o m 3 . 5 .2 i 3 . 5.3 o ra z p o p r z e d n ie
m u ć w ic z e n iu o p ra c u j in te rfe js y A P I k la s Mul t i HashSET i Mul t i SET d la w ie lo z b io ró w
(z b io ró w , w k tó r y c h m o g ą w y s tę p o w a ć id e n ty c z n e k lu c z e ) o ra z im p le m e n ta c je k la s
S e p a ra te C h a in in g M u ltiS E T i B in a ry S e a rc h M u ltiS E T d la w ie lo z b io ró w i u p o r z ą d k o
w a n y c h w ie lo z b io ró w .
3 .5 .1 9 R ó w n e k lu c z e w ta b lica ch sy m b o li. N ie c h in te rfe js y A P I Mul t i ST (d la n ie u p o
rz ą d k o w a n y c h i u p o rz ą d k o w a n y c h ta b lic ) b ę d ą ta k ie sa m e , j a k in te rfe js y A P I ta b lic y
s y m b o li z d e fin io w a n e n a s tr o n a c h 3 7 5 i 3 7 8 , p r z y c z y m t u d o z w o lo n e m a ją b y ć r ó w
n e k lu c z e . M e to d a g e t () m a z w ra c a ć d o w o ln ą w a rto ś ć p o w ią z a n ą z d a n y m k lu c z e m ,
a in te rfe js m a o b e jm o w a ć n o w ą m e to d ę :
Iterable<Value> getA ll(Key key)
R O ZD ZIA Ł 3 a W yszukiw anie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
M e to d a ta m a z w ra c a ć w szy s tk ie w a r to ś c i p o w ią z a n e z d a n y m k lu c z e m . U ż y w a ją c
ja k o p u n k tu w y jśc ia k o d u k la s S e p a r a te C h a in i ngST i B in a ry S e a rc h S T , o p ra c u j i m
p le m e n ta c je Id as S e p a ra te C h a in in g M u ltiS T i B in a ry S e a rc h M u ltiS T d la o m a w ia n y c h
in terfe jsó w .
3 .5 .2 0 . S k o ro w id z . N a p is z k lie n ta C o n c o rd an ce k la s y ST, k tó r y u m ie s z c z a w s t a n d a r
d o w y m w y jśc iu sk o ro w id z ła ń c u c h ó w z n a k ó w ze s ta n d a rd o w e g o s t r u m ie n ia w e jśc ia
(z o b a c z s tr o n ę 51 0 ).
3 .5 .2 1 . S k o r o w id z o d w ro tn y . N a p is z p r o g r a m In v e r te d C o n c o rd a n c e , k tó r y p r z y j
m u je sk o ro w id z ze s ta n d a rd o w e g o w e jśc ia i w y św ie tla p ie r w o tn y ła ń c u c h z n a k ó w
w s ta n d a r d o w y m s t r u m i e n iu w y jścia . Uwaga: o b lic z e n ia z w ią z a n e są ze s ły n n ą h is to
rią d o ty c z ą c ą z w o jó w z n a d M o r z a M a rtw e g o . Z e s p ó ł, k tó r y o d k r y ł rę k o p isy , p o s t a
n o w ił u ta jn ić ic h tr e ś ć i u d o s tę p n ił ty lk o sk o ro w id z . P o p e w n y m c z a sie in n i b a d a c z e
o d k ry li, j a k o d w ró c ić sk o ro w id z , i o s ta te c z n ie u p u b lic z n io n o c a ły te k s t.
3 .5 .2 2 . W p e łn i z in d e k s o w a n e p lik i C S V . Z a im p le m e n tu j p r o g r a m Ful 1 LookupCSV
b ę d ą c y k lie n te m k la s y ST. P r o g r a m m a tw o rz y ć ta b lic ę o b ie k tó w ST (p o je d n y m n a
k a ż d e p o le ). N a p is z te ż k lie n ta te s to w e g o , k tó r y u m o ż liw i u ż y tk o w n ik o m o k re ś le n ie
w k a ż d y m z a p y ta n iu p ó l p e łn ią c y c h fu n k c ję k lu c z a i w a rto ś c i.
3 .5 .2 3 . M a c ie rze rza d k ie . O p ra c u j in te rfe js A P I i im p le m e n ta c ję rz a d k ic h m a c ie rz y
d w u w y m ia ro w y c h . Z a p e w n ij o b s łu g ę d o d a w a n ia i m n o ż e n ia m a c ie rz y . D o łą c z k o n -
s t r u k to r y d la w e k to ró w r e p r e z e n tu ją c y c h w ie rs z e i k o lu m n y .
3 .5 .2 4 . P r z e s z u k iw a n ie ro z łą c zn y c h p r z e d z ia łó w . Z a łó ż m y , że is tn ie je lis ta r o z łą c z
n y c h p r z e d z ia łó w e le m e n tó w . N a p is z fu n k c ję , k tó r a p rz y jm u je ja k o a r g u m e n t e le
m e n t i o k re ś la , w k tó r y m p r z e d z ia le się o n z n a jd u je (jeśli w o g ó le n a le ż y d o k tó re g o ś
z n ic h ) . P rz y k ła d o w o , je ś li e le m e n ty to lic z b y c a łk o w ite , a p rz e d z ia ły to 1643-2033,
5 5 3 2-7643, 8 9 9 9 -1 0 3 3 2 i 566 6 6 5 3 -5 6 6 9 3 2 1 , lic z b a 9122 z n a jd u je się w tr z e c im p r z e
d z ia le , a 8122 n ie n a le ż y d o ż a d n e g o z n ic h .
3 .5 .2 5 . P la n z a ję ć d la w y k ła d o w c ó w . W s e k r e ta ria c ie z n a n e g o p ó łn o c n o - w s c h o d
n ie g o u n iw e rs y te tu n ie d a w n o o p ra c o w a n o p la n , w e d le k tó r e g o w y k ła d o w c a m ia ł
w ty m s a m y m c z a sie p ro w a d z ić d w a ró ż n e w y k ła d y . O p is z m e to d ę w y k ry w a n ia t a
k ic h k o n flik tó w , a b y p o m ó c w u n ik n ię c iu p rz y s z ły c h p o m y łe k . D la u p ro s z c z e n ia z a
łó żm y , że w sz y stk ie z a ję c ia tr w a ją p o 50 m i n u t i z a c z y n a ją się o 9:00, 10:00, 11:00,
13:00, 14:00 lu b 15:00.
3 . 5 .2 6 P a m ię ć p o d r ę c z n a L R U . U tw ó rz s t r u k tu r ę d a n y c h u m o ż liw ia ją c ą d o s tę p
d o e le m e n tó w i ic h u s u w a n ie . O p e r a c ja d o s tę p u p o w o d u je w s ta w ie n ie e le m e n tu d o
s t r u k tu r y d a n y c h , je ś li e le m e n t je s z c z e się w n ie j n ie z n a jd u je . O p e r a c ja u s u w a n ia
k a s u je i z w ra c a n a jd łu ż e j n ie u ż y w a n y e le m e n t. W s k a zó w k a : p rz e c h o w u j e le m e n ty
3.5 a Zastosowania 523
w k o le jn o ś c i d o s tę p u d o n ic h n a liśc ie p o d w ó jn ie p o w ią z a n e j. U trz y m u j te ż w s k a ź n i
k i d o p ie rw s z e g o i o s ta tn ie g o w ę z ła . W y k o rz y s ta j ta b lic ę sy m b o li, w k tó re j k lu c z e to
e le m e n ty , a w a r to ś c i to m ie js c a n a liśc ie p o w ią z a n e j. P rz y d o s tę p ie d o e le m e n tu u s u ń
go z lis ty p o w ią z a n e j i w s ta w n a p o c z ą te k . P rz y u s u w a n iu e le m e n tu u s u ń g o z k o ń c a
i z ta b lic y s y m b o li.
3.5.27. L ista . O p ra c u j im p le m e n ta c ję p o n iż s z e g o in te rfe js u A P I.
p ub lic c l a s s L ist<Item > implements Itera ble<Item>
List() Tworzy listę
void addFront(Item item) Dodaje i tern na początek
void addBack(Item item) Dodaje i tern na koniec
Item d eleteFront() Usuwa z początku
Item deleteBackf) Usuwa z końca
void del ete(Item item) Usuwa i tern z listy
void add( i n t i , Item item) Dodaje i tem jako i -ty element listy
Item d e l e t e ( i n t i) Usuwa z listy i -ty element
boolean co n tains(Item item) Czy klucz key znajduje się na liście?
boolean isEmpty() Czy lista jest pusta?
int size() Zwraca liczbę elementów na liście
Interfejs API dla typu danych reprezentującego listę
W s k a zó w k a : u żyj d w ó c h ta b lic s y m b o li — je d n e j d o w y d a jn e g o w y s z u k iw a n ia i -te g o
e le m e n tu , d ru g ie j d o w y d a jn e g o s z u k a n ia o k re ś lo n y c h e le m e n tó w . In te rfe js j a v a ,
u t i l . Li s t Javy o b e jm u je m e to d y te g o ro d z a ju , je d n a k n ie u d o s tę p n ia ż a d n e j im p le
m e n ta c ji, k tó r a z a p e w n ia w y d a jn e d z ia ła n ie w s z y s tk ic h o p e ra c ji.
3.5.28. U n iQ u eu e. U tw ó rz ty p d a n y c h , k tó r y je s t k o le jk ą , p r z y c z y m e le m e n t m o ż n a
w sta w ić d o n ie j ty lk o raz . U żyj ta b lic y s y m b o li d o ś le d z e n ia w s z y s tk ic h w s ta w io n y c h
w p rz e s z ło ś c i e le m e n tó w , t a k a b y m o ż n a b y ło ig n o r o w a ć ż ą d a n ia p o n o w n e g o ic h d o
d a n ia .
524 R O ZD ZIA Ł 3 ■ W yszukiwanie
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
3.5.29. Tablica sy m b o li o d o stę p ie sw o b o d n y m . U tw ó rz ty p d a n y c h , k tó r y u m o ż liw ia
w s ta w ia n ie p a r k lu c z - w a r to ś ć , w y s z u k iw a n ie k lu c z a i z w ra c a n ie p o w ią z a n e j w a rto ś c i
o ra z u s u w a n ie i z w ra c a n ie lo s o w y c h k lu czy . W s k a zó w k a : p o łą c z ta b lic ę s y m b o li i k o
lejk ę z r a n d o m iz a c ją .
3 .5 □ Zastosowania 525
; eksperym en ty
3 .5 .3 0 . P o w tó rze n ia (p o n o w n ie ). W y k o n a j je s z c z e ra z ć w i c z e n i e 2 . 5 . 3 1 , u ż y w a ją c
filtra Dedup p r z e d s ta w io n e g o n a s tr o n ie 502. P o ró w n a j c z a sy w y k o n a n ia o b u r o z w ią
zań . N a s tę p n ie z a s to s u j filtr Dedup d o p r z e p r o w a d z e n ia e k s p e r y m e n tó w d la N = 107,
10 8 i 109. P o w tó rz e k s p e r y m e n ty d la lo s o w y c h w a r to ś c i ty p u 1 ong i o m ó w w y n ik i.
3 .5 .3 1 . S p ra w d za n ie p is o w n i. P o p o d a n iu p lik u d ic tio n a r y .tx t z w itr y n y ja k o a r
g u m e n tu w w ie rs z u p o le c e ń k lie n t B l a c k F i l t e r ze s tro n y 503 w y ś w ie tla w sz y stk ie
b łę d n ie n a p is a n e sło w a z p lik u te k s to w e g o p o d a n e g o w s ta n d a r d o w y m w e jśc iu . P rz y
u ż y c iu te g o k lie n ta p o ró w n a j w y d a jn o ś ć k la s RedBl ackBST, S e p a ra te C h a i n i ngHashST
i Li n e a rP r o b i ngHashST n a p o d s ta w ie p lik u W a r A n d P e a c e .tx t (d o s tę p n y w w itr y n ie )
i o m ó w w y n ik i.
3 .5 .3 2 . S ło w n ik . Z b a d a j w y d a jn o ś ć k lie n ta w ro d z a ju LookupCSV w sy tu a c ji, w k tó re j
w y d a jn o ś ć m a z n a c z e n ie . Z a p ro je k tu j s c e n a r iu s z g e n e ro w a n ia z a p y ta ń , z a m ia s t p o
b ie ra ć p o le c e n ia ze s ta n d a rd o w e g o w ejśc ia . P rz e p r o w a d ź te s ty w y d a jn o ś c i d la d ł u
g ic h d a n y c h w y jśc io w y c h i d u ż e j lic z b y z a p y ta ń .
3 .5 .3 3 . In d e k s o w a n ie . Z b a d a j k lie n ta w ro d z a ju Lookup In d e x w sy tu a c ji, w k tó re j w y
d a jn o ś ć m a z n a c z e n ie . Z a p ro je k tu j s c e n a r iu s z g e n e ro w a n ia z a p y ta ń , z a m ia s t p o b ie
ra ć p o le c e n ia ze s ta n d a rd o w e g o w e jścia . P rz e p r o w a d ź te s ty w y d a jn o ś c i d la d łu g ic h
d a n y c h w y jśc io w y c h i d u ż e j lic z b y z a p y ta ń .
3 .5 .3 4 . W e k to r y rz a d k ie . P rz e p r o w a d ź e k s p e r y m e n ty w c e lu p o r ó w n a n ia w y d a jn o
ści m n o ż e n ia m a c ie rz y p rz e z w e k to r za p o m o c ą k la s y S p a r s e V e c to r i s ta n d a rd o w e j
im p le m e n ta c ji o p a rte j n a ta b lic a c h .
3 .5 .3 5 . T y p y p ro ste . O c e ń p r z y d a tn o ś ć z a s to s o w a n ia ty p ó w p r o s ty c h z a m ia s t w a r to
ści ty p u I n t e g e r i D ouble w k la s a c h Li n e a r P r o b i ngHashST o r a z RedBl ackBST. Ile p a
m ię c i i c z a s u m o ż n a z a o sz c z ę d z ić d la d u ż e j lic z b y w y s z u k iw a ń w d u ż y c h ta b lic a c h ?
ROZDZIAŁ 4
4.1 Grafy n ie sk ie ro w a n e .................
4.2 Grafy sk ie ro w a n e ......................
4.3 Minimalne drzewa rozpinające.
4.4 Najkrótsze ś c ie ż k i.......................
o łą c z e n ia m ię d z y p a r a m i e le m e n tó w o d g ry w a ją k lu c z o w ą ro lę w b a rd z o r ó ż
DTL
n o r o d n y c h a p lik a c ja c h o b lic z e n io w y c h . R elacje w y z n a c z a n e p rz e z te p o łą c z e -
n ia b e z p o ś r e d n io z w ią z a n e są z n a tu r a ln y m i p y ta n ia m i: C z y istn ie je sp o s ó b n a
do jście z je d n e g o e le m e n tu d o in n e g o za p o m o c ą p o łą c z e ń ? Ile in n y c h e le m e n tó w je s t
p o łą c z o n y c h z d a n y m ? Jak i je s t n a jk ró ts z y c ią g p o łą c z e ń m ię d z y d a n y m e le m e n te m
a in n y m ?
D o m o d e lo w a n ia ta k ic h sy tu a c ji s łu ż ą a b s tra k c y jn e o b ie k ty m a te m a ty c z n e n a z y
w a n e g ra fa m i. W ty m ro z d z ia le sz c z e g ó ło w o o m a w ia m y p o d s ta w o w e c e c h y g rafó w ,
co d a je p o d s ta w y d o b a d a n ia ró ż n o r o d n y c h a lg o r y tm ó w p rz y d a tn y c h d o o d p o w ia d a
n ia n a p y ta n ia p o d o b n e d o p o s ta w io n y c h w c z e śn ie j. A lg o ry tm y te są p u n k te m w y j
ścia d o z m ie r z e n ia się z r ó ż n o r o d n y m i p ro b le m a m i. B ez d o b r y c h te c h n ik a lg o r y t
m ic z n y c h ro z w ią z a ń ty c h p ro b le m ó w n ie m o ż n a so b ie n a w e t w y o b ra z ić .
T e o ria g rafó w , je d n a z g łó w n y c h g a łę z i m a te m a ty k i, je s t in te n s y w n ie b a d a n a o d
s e te k lat. O d k r y to w ie le is to tn y c h i p rz y d a tn y c h c e c h a lg o ry tm ó w , o p ra c o w a n o lic z n e
w a ż n e a lg o ry tm y , a p o n a d to n a d a l b a d a n y c h je s t w ie le is to tn y c h p ro b le m ó w . W ty m
ro z d z ia le p rz e d s ta w ia m y r ó ż n o r o d n e p o d s ta w o w e a lg o r y tm y d la g rafó w , w a ż n e
w ro z m a ity c h z a s to s o w a n ia c h .
P o d o b n ie ja k w ie le in n y c h o m a w ia n y c h o b sz a ró w , ta k i a lg o r y tm ic z n e b a d a n ia
g ra fó w są s to s u n k o w o m ł o d ą d z ie d z in ą . C h o ć n ie k tó re p o d s ta w o w e a lg o r y tm y są
z n a n e o d stu le c i, w ię k s z o ś ć c ie k a w y c h a lg o r y tm ó w o d k r y to w c ią g u k ilk u o s ta tn ic h
d z ie s ię c io le c i d z ię k i p o ja w ie n iu się te c h n ik a lg o ry tm ic z n y c h , k tó r e b a d a m y . N a w e t
n a jp r o s ts z e a lg o r y tm y d la g ra fó w p ro w a d z ą d o p rz y d a tn y c h p r o g r a m ó w k o m p u t e
ro w y c h , a s k o m p lik o w a n e ro z w ią z a n ia , k tó r y m się p rz y jrz y m y , n a le ż ą d o je d n y c h
z n a jb a rd z ie j e le g a n c k ic h i c ie k a w y c h ze w s z y s tk ic h z n a n y c h a lg o ry tm ó w .
W c e lu p o k a z a n ia r ó ż n o r o d n o ś c i z a s to s o w a ń p r z e tw a r z a n ia g ra fó w p rz e g lą d a lg o
r y tm ó w z tej b o g a te j d z ie d z in y z a c z y n a m y o d k ilk u p rz y k ła d ó w .
527
528 R O ZD ZIA Ł 4 a Grafy
M apy O s o b a p la n u ją c a w y c ie c z k ę m o ż e p o tr z e b o w a ć o d p o w ie d z i n a p y ta n ia w r o
d z aju : „Jak a je s t n a jk r ó ts z a tr a s a z W ro c ła w ia d o G d a ń s k a ? ”. D o ś w ia d c z o n y p o d r ó ż
n ik , k tó r y z e tk n ą ł się z u tr u d n ie n i a m i n a n a jk ró ts z e j d ro d z e , m o ż e z a d a ć p y ta n ie :
„Ja k m o ż n a n a js z y b c ie j d o s ta ć się z W ro c ła w ia d o G d a ń s k a ? ”. A b y o d p o w ie d z ie ć n a
te p y ta n ia , tr z e b a p rz e tw o r z y ć in f o rm a c je n a te m a t p o łą c z e ń (d ró g ) m ię d z y e le m e n
ta m i (s k rz y ż o w a n ia m i).
Zaw artość stron W W W P rz y p r z e g lą d a n iu sieci W W W n a p o ty k a m y s tr o n y z a w ie
ra ją c e o d n o ś n ik i d o in n y c h s tro n . P r z e c h o d z im y m ię d z y s tr o n a m i, k lik a ją c te o d n o ś
n ik i. C a ła sieć W W W je s t g ra fe m , w k tó r y m e le m e n ta m i są s tro n y , a p o łą c z e n ia m i
— o d n o ś n ik i. A lg o ry tm y d o p r z e tw a r z a n ia g ra fó w są k lu c z o w y m i s k ła d n ik a m i w y
s z u k iw a re k p o m a g a ją c y c h lo k a liz o w a ć in f o rm a c je w sie c i W W W .
O bwody O b w ó d e le k try c z n y o b e jm u je p o łą c z o n e ze s o b ą u r z ą d z e n ia w ro d z a ju
tr a n z y s to ró w , o p o r n ik ó w i k o n d e n s a to ró w . S to s u je m y k o m p u t e r y d o k o n tr o lo w a n ia
m a s z y n w y tw a rz a ją c y c h o b w o d y i d o s p ra w d z a n ia , cz y o b w o d y s p e łn ia ją sw o je z a d a
n ia . P o tr z e b n e są o d p o w ie d z i n a p ro s te p y ta n ia w ro d z a ju : „C zy w y s tę p u je s p ię c ie ? ”,
a ta k ż e n a p y ta n ia s k o m p lik o w a n e , n a p rz y k ła d : „C zy m o ż n a u m ie ś c ić te n o b w ó d n a
c h ip ie b e z k rz y ż o w a n ia k a b li? ”. O d p o w ie d ź n a p ie r w s z e p y ta n ie z a le ż y ty lk o o d c e c h
p o łą c z e ń (k a b li), n a to m ia s t d o u d z ie le n ia o d p o w ie d z i n a d r u g ie p y ta n ie p o tr z e b n e są
sz c z e g ó ło w e in f o rm a c je o k a b la c h , p o łą c z o n y c h p rz e z n ie u r z ą d z e n ia c h i fiz y cz n y c h
o g ra n ic z e n ia c h c h ip a .
H arm onogram y P ro c e s p r o d u k c ji w y m a g a w y k o n a n ia w ie lu z a d a ń . O b o w ią z u ją
p r z y ty m o g ra n ic z e n ia , o k re ś la ją c e , że p e w n y c h z a d a ń n ie m o ż n a ro z p o c z ą ć p r z e d
z a k o ń c z e n ie m in n y c h . Jak m o ż n a u s z e re g o w a ć z a d a n ia ta k , a b y u w z g lę d n ić o g r a n i
c z e n ia , a p r z y ty m z a k o ń c z y ć c a ły p ro c e s w j a k n a jk r ó ts z y m czasie?
Handel S p rz e d a w c y i in s ty tu c je fin a n s o w e ś le d z ą z le c e n ia k u p n a i s p r z e d a ż y n a r y n
k u . P o łą c z e n ie re p r e z e n tu je tu tr a n s f e r g o tó w k i i to w a ró w m ię d z y in s ty tu c ją a k lie n
te m . W ie d z a o n a tu r z e s t r u k tu r y p o łą c z e n ia m o ż e w z b o g a c ić s p o s ó b r o z u m ie n ia
ry n k u .
D opasow yw anie S tu d e n c i u b ie g a ją się o m ie js c a w s e le k ty w n y c h o rg a n iz a c ja c h ,
ta k ic h ja k k lu b y to w a rz y s k ie , u n iw e rs y te ty c z y sz k o ły m u z y c z n e . E le m e n ty o d p o w ia
d a ją s t u d e n to m i o rg a n iz a c jo m , a p o łą c z e n ia re p r e z e n tu ją p o d a n ia . C h c e m y o d k r y ć
m e to d y d o p a s o w y w a n ia z a in te re s o w a n y c h s tu d e n tó w d o d o s tę p n y c h m iejsc .
Sieci kom puterowe S ieć k o m p u te r o w a s k ła d a się z p o w ią z a n y c h p u n k tó w , k tó re
w y sy łają, p rz e k a z u ją i o d b ie r a ją k o m u n ik a ty ró ż n e g o ty p u . C h c e m y p o z n a ć n a tu r ę
s t r u k tu r y w z a je m n y c h p o łą c z e ń , a b y m ó c k ła ś ć k a b le i k o n fig u ro w a ć p rz e łą c z n ik i
w y d a jn ie o b s łu g u ją c e r u c h .
ROZDZIAŁ 4 □ Grafy 529
O p ro g ra m o w a n ie K o m p ila to r tw o rz y grafy , a b y re p r e z e n to w a ć z w ią z k i m ię d z y
m o d u ła m i w d u ż y c h s y s te m a c h o p r o g r a m o w a n ia . E le m e n ta m i są r ó ż n e k la s y lu b
m o d u ły w c h o d z ą c e w s k ła d sy s te m u . P o łą c z e n ia d o ty c z ą a lb o m o ż liw o ś c i w y w o ła n ia
p rz e z m e to d ę z je d n e j k la s y in n e j m e to d y (a n a liz a sta ty c z n a ), a lb o s a m y c h w y w o ła ń
w tr a k c ie d z ia ła n ia s y s te m u (a n a liz a d y n a m ic z n a ). T rz e b a p rz e a n a liz o w a ć g ra f, ab y
u sta lić , ja k w n a jw y d a jn ie js z y s p o s ó b p rz y d z ie lić z a so b y p ro g r a m o w i.
S ie c i s p o łe c z n o ś c io w e P rz y k o r z y s ta n iu z sie c i s p o łe c z n o ś c io w e j tw o rz y s z b e z p o
ś r e d n ie p o łą c z e n ia ze z n a jo m y m i. E le m e n to m o d p o w ia d a ją o so b y , a p o łą c z e n ia p r o
w a d z ą d o z n a jo m y c h lu b fan ó w . O k re ś la n ie c e c h ta k ic h siec i je s t je d n y m z o b sz a ró w ,
g d zie w s p ó łc z e ś n ie w y k o rz y s tu je się p rz e tw a r z a n ie grafó w . D z ie d z in a ta je s t w a ż n a
n ie ty lk o d la f irm z a rz ą d z a ją c y c h s ie c ia m i s p o łe c z n o ś c io w y m i, a le te ż w p o lity c e ,
d y p lo m a c ji, ro z ry w c e , e d u k a c ji, m a r k e tin g u i w ie lu in n y c h o b s z a ra c h .
p r z y k ł a d y t e i l u s t r u j ą , z a k r e s z a s t o s o w a ń , w k tó r y c h g ra f y są o d p o w ie d n ią
a b s tra k c ją , a ta k ż e z a k re s p ro b le m ó w o b lic z e n io w y c h w y s tę p u ją c y c h w c z a sie k o r z y
s ta n ia z grafó w . P r z e b a d a n o ty s ią c e ta k ic h p ro b le m ó w . W ie le z n ic h m o ż n a ro z w ią z a ć
w k o n te k ś c ie je d n e g o z k ilk u p o d s ta -
w o w y c h m o d e li grafó w . N a jw a ż n ie jsz e Zastosowanie Element Połączenie
m o d e le p r z e b a d a m y w ty m ro z d z ia le .
Mapa S k rzy żo w an ie D ro g a
W p ra k ty c z n y c h z a s to s o w a n ia c h ilo ść
d a n y c h c z ę sto je s t b a r d z o d u ż a , d la te Zawartość sieci W W W S tro n a O d n o ś n ik
go o d w y d a jn y c h a lg o r y tm ó w zależy,
Obwód U rz ą d z e n ie K abel
czy p r o b le m d a się ro z w ią z a ć .
W ra m a c h p rz e g lą d u p r z e d s ta Harmonogram zadań Z a d a n ie O g ra n ic z e n ie
w ia m y c z te ry n a jw a ż n ie js z e ro d z a je Handel T ra n sak cja
K lien t
m o d e li g ra fó w : g r a fy n ie sk iero w a n e
Dopasowywanie S tu d e n t P o d a n ie
(z p r o s ty m i p o łą c z e n ia m i), g ra fy sk ie
ro w a n e (w k tó r y c h k ie r u n e k k a ż d e Sieci komputerowe Punkt P o łącze n ie
go p o łą c z e n ia m a z n a c z e n ie ), g ra fy
Oprogramowanie M e to d a W y w o łan ie
w a żo n e (g d z ie k a ż d e p o łą c z e n ie m a
o k re ś lo n ą w ag ę) i w a żo n e g r a fy sk ie Sieci społecznościowe O so b a Z n a jo m o ść
ro w a n e (g d z ie k a ż d e p o łą c z e n ie m a
Typowe zastosowania grafów
i k ie r u n e k , i w ag ę).
4.1. G R A F Y N IE S K IE R O W A N E
p u n k t e m w y j ś c i a je s t a n a liz a m o d e li g rafó w , w k tó r y c h k ra w ę d zie s ą n ic z y m w ię ce j
ja k p o łą c z e n ia m i m ię d z y w ie r z c h o łk a m i. N a z w ę g r a f n ie s k ie ro w a n y s to s u je m y ta m ,
g d z ie tr z e b a o d r ó ż n ić te n m o d e l o d in n y c h ( n a p rz y k ła d w ty tu le te g o p o d r o z d z ia łu ) ,
je d n a k — p o n ie w a ż je s t to n a jp r o s ts z y m o d e l — z a c z y n a m y o d p o n iż s z e j d e fin ic ji.
D efin icja . G r a f to z b ió r w ie rz c h o łk ó w i k o le k c ja k ra w ę d zi, z k tó r y c h k a ż d a łą c z y
p a r ę w ie rz c h o łk ó w .
N a z w y w ie rz c h o łk ó w n ie m a ją z n a c z e n ia , p o tr z e b n y je s t
je d n a k s p o s ó b n a ic h w s k a z y w a n ie . Z g o d n ie z k o n w e n
c ją d la w ie rz c h o łk ó w g ra f u o V w ie rz c h o łk a c h u ż y w a m y
n a z w o d 0 d o V - 1. G łó w n y m p o w o d e m z a s to s o w a n ia
te g o s y s te m u je s t ła tw o ś ć p is a n ia k o d u , k tó r y w w y d a jn y
s p o s ó b u z y sk u je d o s tę p d o in f o r m a c ji o d p o w ia d a ją c y c h
k a ż d e m u w ie rz c h o łk o w i (w y sta rc z y p o d a ć in d e k s y ta b l i
cy ). N ie tr u d n o z a sto so w a ć ta b lic ę sy m b o li d o u tw o rz e n ia
o d w z o ro w a n ia 1 d o 1 i p o w ią z a n ia V d o w o ln y c h n a z w
w ie rz c h o łk ó w z V lic z b a m i c a łk o w ity m i z p r z e d z ia łu
^ ^ 2) ° d 0 d o V - 1 (z o b a c z s tr o n ę 5 6 0 ), d la te g o w y g o d a , ja k ą
( | ) - ( o © d a je z a s to s o w a n ie in d e k s ó w ja k o n a z w w ie rz c h o łk ó w ,
n ie z m n ie js z a o g ó ln o ś c i ro z w ią z a n ia (i ty lk o n ie z n a c z n ie
■ysunki przedstawiające ten sam graf o b n iż a w y d a jn o ś ć ). Z a p is v-w o z n a c z a k ra w ę d ź łą c z ą c ą
v z w. Z a p is w-v to in n y s p o s ó b n a w s k a z a n ie tej sa m e j
N a r y s u n k u g ra f u k ó łk a o z n a c z a ją w ie rz c h o łld , a łą c z ą c e je lin ie — k ra w ę d z ie .
R y s u n e k p o z w a la in tu ic y jn ie z ro z u m ie ć s t r u k tu r ę g ra fu . J e d n a k in tu ic ja b y w a tu
m y lą c a , p o n ie w a ż g r a f je s t d e fin io w a n y n ie z a le ż n ie o d ry s u n k u . N a p rz y k ła d d w a
r y s u n k i p o lew ej re p r e z e n tu ją te n s a m g ra f, p o n ie w a ż s t r u k tu r a t a je s t n ic z y m w ięc e j
j a k (n ie u p o rz ą d k o w a n y m ) z b io r e m w ie rz c h o łk ó w i (n ie u p o rz ą d k o w a n ą ) k o le k c ją
k ra w ę d z i (p a r w ie rz c h o łk ó w ).
Pętla Krawędzie
A n o m a l i e D e fin ic ja d o p u s z c z a w y s tą p ie n ie d w ó c h p r o s ty c h a n o własna równolegle
m a lii. O to o n e: ł
■ pętla w łasna, czyli k ra w ę d ź łącząca w ie rz c h o łek z n im sam y m ;
■ k ra w ę d zie ró w n o leg łe, cz y li d w ie k ra w ę d z ie łą c z ą c e tę s a m ą Anomalie
p a rę w ie rz c h o łk ó w .
M a te m a ty c y c z a se m n a z y w a ją g ra fy o ró w n o le g ły c h k ra w ę d z ia c h m u ltig ra fa m i, a g ra fy
b e z k ra w ę d z i te g o ro d z a ju — g ra fa m i p ro s ty m i. W p rz e d s ta w ia n y c h p rz e z n a s im p le
m e n ta c ja c h o g ó ln ie p ę tle w ła sn e i k ra w ę d z ie ró w n o le g łe są d o p u s z c z a ln e (p o n ie w a ż
w y stę p u ją w p ra k ty c e ), je d n a k n ie u w z g lę d n ia m y ic h w p rz y k ła d a c h . D la te g o k a ż d ą
k ra w ę d ź m o ż n a w sk a z a ć za p o m o c ą n a z w d w ó c h łą c z o n y c h p rz e z n ią w ie rz c h o łk ó w .
530
4.1 n Grafy nieskierowane 531
S ło w n ic z e k Z g ra f a m i z w ią z a n y c h je s t w ie le n a zw . W ię k s z o ś ć p o ję ć m a p ro s te
d e fin icje. P rz e d s ta w ia m y je w je d n y m m ie js c u — tu ta j.
Jeśli is tn ie je k r a w ę d ź łą c z ą c a d w a w ie rz c h o łk i, m ó w im y , że są o n e są sia d u ją c e ,
a k ra w ę d ź je s t in c y d e n tn a d la o b u w ie rz c h o łk ó w . S to p ie ń w ie rz c h o łk a to lic z b a k r a
w ę d z i in c y d e n tn y c h . P o d g r a f to p o d z b ió r k ra w ę d z i g ra f u (i p o w ią z a n y c h w ie r z c h o ł
k ó w ), k tó r y s a m tw o r z y g raf. W ie le z a d a ń o b lic z e n io w y c h w y m a g a z id e n ty fik o w a n ia
p o d g ra f ó w ró ż n e g o ro d z a ju . S z c z e g ó ln ie c ie k a
w e są k ra w ę d z ie p o z w a la ją c e p rz e jś ć p rz e z ć ia ę Wierzchołek
w ie rz c h o łk ó w g ra fu .
D e f in i c ja . Ś c ie ż k a w g ra fie to c ią g w ie r z c h o ł
k ó w p o łą c z o n y c h k ra w ę d z ia m i. N a ścieżce
p ro stej ż a d e n w ie rz c h o łe k się n ie p o w ta rz a .
C y k l to śc ie ż k a , w k tó re j je d e n w ie rz c h o łe k
je s t z a ró w n o p o c z ą tk o w y m , ja k i k o ń c o w y m .
C ykl p r o s ty to ta k i, w k tó r y m k ra w ę d z ie a n i
w ie rz c h o łk i się n ie p o w ta rz a ją (w y ją tk ie m je s t
k o n ie c z n e p o w tó r z e n ie p ie rw s z e g o i o s ta tn ie
go w ie rz c h o łk a ). D łu g o ść śc ie ż k i lu b c y k lu to
lic z b a k ra w ę d z i.
N ajczęściej w y k o rz y s tu je m y p ro s te c y k le i p r o s te śc ie ż k i, p o m ija ją c p r z y ty m d o o lcre-
śle n ie p ro ste. Jeśli d o p u s z c z a ln e są p o w tó r z e n ia w ie rz c h o łk ó w , m ó w im y o ogólnych
ś c ie ż k a c h i c y k la c h . S tw ie rd z a m y , że w ie rz c h o łe k je s t p o łą c z o n y z in n y m , je ś li is tn ie
je śc ie ż k a o b e jm u ją c a o b a w ie rz c h o łk i. Ś c ie ż k ę o d u d o x z a p is u je m y ja k o u -v -w -x ,
a cy k l p ro w a d z ą c y z u d o v d o w d o x i z p o w r o te m d o u z a p is u je m y ja k o u -v -w -x -u .
N ie k tó re z o m a w ia n y c h a lg o r y tm ó w w y s z u k u ją ś c ie ż k i i c y k le. P o n a d to śc ie ż k i i c y
k le p ro w a d z ą d o ro z w a ż a ń n a d s t r u k tu r a ln y m i c e c h a m i c a ły c h grafó w .
D e f in i c ja . G r a f je s t sp ó jn y, je ś li is tn ie je śc ie ż k a z k a ż d e g o w ie rz c h o łk a d o k a ż
d e g o in n e g o w ie rz c h o łk a g ra fu . G r a f n ie sp ó jn y s k ła d a się ze sp ó jn y c h sk ła d o w y c h ,
k tó re są m a k s y m a ln y m i s p ó jn y m i p o d g ra f a m i.
In tu ic y jn ie stw ie rd z a m y , że g d y b y w ie rz c h o łk i b y ły fiz y c z n y m i o b ie k ta m i, ta k im i ja k
w ęzły lu b k o ra lik i, a k ra w ę d z ie — fiz y c z n y m i p o łą c z e n ia m i, n a p rz y k ła d s z n u r k a m i
lu b d r u c ik a m i, g r a f s p ó jn y p o z o s ta łb y w je d n e j c z ęśc i, g d y b y p o d n ie ś ć g o za p o m o c ą
d o w o ln e g o w ie rz c h o łk a , a g r a f n ie s p ó jn y s k ła d a łb y się z d w ó c h lu b w ię c ej e le m e n tó w
te g o ro d z a ju . P rz e tw a r z a n ie g ra fó w o g ó ln ie w y m a g a p rz e tw a r z a n ia o s o b n o s p ó jn y c h
sk ła d o w y c h .
532 R O ZD ZIA Ł 4 □ Grafy
G r a f a c y k lic z n y to ta k i, w k tó r y m n ie w y s tę p u ją 19 wierzchołków
Graf
cykle. K ilk a s p o ś r ó d o m a w ia n y c h a lg o r y tm ó w w y 18 krawędzi |
acykliczny
s z u k u je w g ra f a c h a c y k lic z n e p o d g r a f y o o k re ś lo
n y c h c e c h a c h . D o o p is u ta k ic h s t r u k tu r p o tr z e b n e są
d o d a tk o w e p o ję c ia .
D e f in i c ja . D r z e w o to a c y k lic z n y g r a f sp ó jn y .
R o z łą c z n y z b ió r d rz e w n a z y w a n y je s t la sem .
Spójny
D r z e w o ro zp in a ją c e d la g ra f u s p ó jn e g o to p o d -
g r a f s k ła d a ją c y się z w s z y s tk ic h w ie rz c h o łk ó w
Drzewo
g ra f u i b ę d ą c y je d n y m d rz e w e m . L a s ro z p in a ją c y
d la g ra f u to g r u p a d r z e w r o z p in a ją c y c h d la s p ó j
n y c h sk ła d o w y c h g ra fu .
T a d e fin ic ja d rz e w a je s t d o ś ć o g ó ln a . P o o d p o w ie d
n im d o p ra c o w a n iu o b e jm u je d rz e w a u ż y w a n e d o
m o d e lo w a n ia d z ia ła n ia p r o g r a m u ( h ie ra rc h ii w y w o
ła ń fu n k c ji) i s t r u k tu r d a n y c h (d r z e w a BST, d rz e w a
2 -3 itd .). M a te m a ty c z n e c e c h y d rz e w są d o b rz e p r z e
b a d a n e i in tu ic y jn e , d la te g o p rz y ta c z a m y je b e z d o
w o d ó w . P rz y k ła d o w o , g r a f G o V w ie rz c h o łk a c h je s t
d rz e w e m w te d y i ty lk o w ted y , je ś li s p e łn ia d o w o ln y
z p o n iż s z y c h p ię c iu w a ru n k ó w :
■ G m a V - 1 k ra w ę d z i i n ie m a cy k li. Las rozpinający
■ G m a V - 1 k ra w ę d z i i je s t sp ó jn y .
■ G je s t sp ó jn y , a le u s u n ię c ie d o w o ln e j k ra w ę d z i sp ra w ia , że sta je się n ie sp ó jn y .
■ G je s t acy k liczn y , je d n a k d o d a n ie d o w o ln e j k ra w ę d z i p o w o d u je p o w s ta n ie cy k lu .
■ K a ż d ą p a r ę w ie rz c h o łk ó w w G łą c z y d o k ła d n ie je d n a śc ie ż k a p ro s ta .
N ie k tó r e z o m a w ia n y c h a lg o r y tm ó w s łu ż ą d o w y s z u k iw a n ia d rz e w i la s ó w r o z p i n a
ją c y c h . O p is a n e p o w y ż e j c e c h y o d g ry w a ją w a ż n ą ro lę w a n a liz o w a n iu i im p le m e n to
w a n iu ty c h a lg o ry tm ó w .
G ęstość g ra f u o k re ś la , ile m o ż
Rzadki (£ = 200) Gęsty (£= 1000)
liw y c h k r a w ę d z i is tn ie je w grafie.
W g ra fie r z a d k im w y s tę p u je s t o s u n
k o w o n ie w ie le m o ż liw y c h k ra w ę d z i.
W g ra fie g ę s ty m b ra k u je s t o s u n k o
w o n ie w ie lu m o ż liw y c h k ra w ę d z i.
O g ó ln ie g r a f je s t u z n a w a n y z a rz a d k i,
je ś li lic z b a r ó ż n y c h k ra w ę d z i je s t n ie
w ię k sz a o p e w n ą n ie d u ż ą w ie lo k r o t
n o ś ć o d V. W p rz e c iw n y m ra z ie g r a f
Dwa grafy (IZ = 50)
je s t gęsty. T a p r o s ta re g u ła c z a se m
4.1 o Grafy nieskierowane
n ie p o z w a la p o d ją ć d e c y z ji ( n a p rz y k ła d k ie d y lic z b a k ra w ę d z i w y n o s i - c l / 3'2), j e d
n a k w p ra k ty c e p o d z ia ł n a g ra fy rz a d k ie i g ę ste je s t z w y k le b a r d z o w y ra ź n y . P ra w ie
w sz y stk ie o m a w ia n e z a s to s o w a n ia o p a r te są n a g ra fa c h rz a d k ic h .
G r a f d w u d z ie ln y to g raf, w k tó r y m w ie rz c h o łk i m o ż n a p o d z ie
lić n a d w a z b io ry , taicie że k a ż d a k ra w ę d ź łą c z y w ie rz c h o łe k z j e d
n e g o z b io r u z w ie rz c h o łk ie m z d ru g ie g o z b io ru . P rz y k ła d o w y g r a f
d w u d z ie ln y p o k a z a n o n a r y s u n k u p o p ra w e j. W ie r z c h o łk i z je d n e g o
z b io r u są k o lo r u c z e rw o n e g o , a z d ru g ie g o — c z a rn e g o . G ra fy d w u
d z ie ln e w p ra k ty c e w y s tę p u ją w w ie lu sy tu a c ja c h . J e d n ą z n ic h o m a
w ia m y s z c z e g ó ło w o w k o ń c o w e j c z ę śc i p o d r o z d z ia łu . Graf dwudzielny
p o t y m w s t ę p i e m o ż e m y p rz e jś ć d o a lg o r y tm ó w p r z e tw a r z a n ia g rafó w . Z a c z y n a m y
d o o m ó w ie n ia in te rfe js u A P I i im p le m e n ta c ji ty p u d a n y c h d la g rafó w . N a s tę p n ie o p i
s u je m y k la s y c z n e a lg o r y tm y d o p r z e s z u k iw a n ia g ra fó w i id e n ty fik o w a n ia s p ó jn y c h
s k ła d o w y c h . W k o ń c o w e j c z ę śc i p o d r o z d z ia łu o m a w ia m y p ra k ty c z n e z a s to s o w a n ia ,
w k tó ry c h n a z w a m i w ie rz c h o łk ó w n ie są lic zb y ca łk o w ite , a g ra fy o b e jm u ją d u ż ą lic z b ę
w ie rz c h o łk ó w i k ra w ę d z i.
534 RO ZD ZIA Ł 4 n Grafy
Typ danych dla grafów nieskierowanych P u n k te m w y jśc ia d o ro z w ija n ia
a lg o r y tm ó w p r z e tw a r z a n ia g ra fó w je s t in te rfe js A P I o b e jm u ją c y d e fin ic je p o d s ta w o
w y c h o p e ra c ji n a g rafie. T o p o d e jś c ie p o z w a la w y k o n y w a ć z a d a n ia z w ią z a n e z p r z e
tw a r z a n ie m g ra fó w — o d p o d s ta w o w y c h o p e ra c ji p o rz ą d k u ją c y c h p o z a a w a n s o w a n e
r o z w ią z a n ia tr u d n y c h p ro b le m ó w .
p ub lic c l a s s Graph
Gra ph( int V) Tworzy g raf bez krawędzi o V wierzchołkach
Graph(In in) Wczytuje graf ze strumienia wejściowego i n
i n t V() Zwraca liczbę wierzchołków
i n t E() Zwraca liczbę krawędzi
void addEdge(int v, i n t w) Dodaje do grafu krawędź v-w
I t e ra b le < In t e g e r> a d j ( i n t v) Zwraca wierzchołki sąsiadujące z v
String to Strin g O Reprezentacja w postaci łańcucha znaków
Interfejs API dla grafów nieskierowanych
P rz e d s ta w io n y in te rfe js A P I o b e jm u je d w a k o n s tru k to r y , m e to d y z w ra c a ją c e lic z b ę
w ie rz c h o łk ó w i k ra w ę d z i, m e to d ę d o d a ją c ą k ra w ę d ź , m e to d ę t o S t r i n g O o ra z m e
to d ę a d j ( ) , k tó r a u m o ż liw ia k lie n to w i ¿ te ro w a n ie p o w ie rz c h o łk a c h s ą s ia d u ją c y c h
z d a n y m (k o le jn o ś ć ite ra c ji n ie je s t o k re ś lo n a ). C o c ie k a w e, w sz y stk ie a lg o r y tm y
o m a w ia n e w ty m p o d r o z d z ia le m o ż n a u tw o rz y ć n a p o d s ta w ie p ro s te j a b s tra k c ji u ję
tej w m e to d z ie a d j () .
W d r u g im k o n s tr u k to r z e z a k ła d a m y , że d a n e w e jśc io w e o b e jm u ją 2 E + 2 w a r to
ści c a łk o w ito lic z b o w y c h — w a rto ś ć V, n a s tę p n ie w a rto ś ć E, a p o te m E p a r w a rto ś c i
m ię d z y 0 a V - 1 (k a ż d a p a r a o k re ś la k ra w ę d ź ). W p rz y k ła d a c h u ż y w a m y d w ó c h
p rz e d s ta w io n y c h p o n iż e j g rafó w , tin y G .tx t i m e d .iu m G .tx t.
W ta b e li n a n a s tę p n e j s tr o n ie p o k a z a n o k ilk a p rz y k ła d o w y c h fr a g m e n tó w k o d u
k lie n tó w k la s y G raph.
Format danych wejściowych konstruktora klasy Graph (dwa przykłady)
4.1 □ Grafy nieskierowane 535
Zadanie Implementacja
p ub lic s t a t i c in t degree(Graph G, i n t v)
f
Obliczanie stopnia in t degree = 0;
wierzchołka v f o r ( i n t w : G.a d j (v )) degree++;
return degree;
}
pub lic s t a t i c i n t maxDegree(Graph G)
{
in t max = 0;
Obliczanie f o r (i n t v = 0; v < G.V(); v++)
maksymalnego stopnia i f (degree(G, v) > max)
max = degree(G, v ) ;
return max;
Obliczanie p ublic s t a t i c i n t avgDegree(Graph G)
średniej ze stopni { return 2 * G.E() / G.V (); }
p ublic s t a t i c i n t numberOfSelfLoops(Graph G)
(
in t count = 0;
f o r (i n t v = 0; v < G.V(); v++)
Zliczanie
f o r (i n t w : [Link] (v ))
pętli własnej i f (v == w) count++;
return count/2; // Każdą krawędź policzono
// dwukrotnie.
}
p ub lic S t r i n g t o S t r i n g O
(
S t r i n g s = "W ierzchołki: " + V + ", krawędzie:" + E + " \ n " ;
Zwracanie f o r (i n t v = 0; v < V; v++)
łańcucha znaków {
reprezentującego s += v + ": ";
listy sąsiedztwa grafu f o r ( i n t w : t h i s .adj ( v ) )
(metoda egzemplarza s += w + " 11;
s += " \ n " ;
w klasie Graph)
}
return s;
}
Typowy kod do przetwarzania grafów
536 RO ZD ZIA Ł 4 □ Grafy
M o ż liw e r e p r e z e n ta c je P rz y p rz e tw a r z a n iu g ra fó w tr z e b a u sta lić , k tó r ą r e p r e z e n
ta c ję g ra f u ( s tr u k tu r ę d a n y c h ) z a sto so w a ć d o z a im p le m e n to w a n ia in te rfe js u A P I.
S tr u k tu r a m u s i s p e łn ia ć d w a p o d s ta w o w e w y m o g i.
■ D o s tę p n a m u s i b y ć p a m ię ć n a z a p is a n ie ro d z a jó w g rafó w , k tó r e p r a w d o p o d o b
n ie w y s tą p ią w a p lik ac ji.
D N a le ż y o p ra c o w a ć w y d a jn e ze w z g lę d u n a cza s im p le m e n ta c je m e to d e g z e m p la
rz a ld a s y G raph. S ą to p o d s ta w o w e m e to d y p o tr z e b n e d o tw o r z e n ia M ie n tó w d o
p r z e tw a r z a n ia grafów .
W y m o g i te n ie są w p e łn i p re c y z y jn e , j e d n a k p r z y d a ją się p r z y w y b o r z e je d n e j
z tr z e c h s t r u k t u r d a n y c h , k tó r e p r z y c h o d z ą n a m y ś l ja k o m o ż liw e r e p r e z e n ta c je
g rafó w . O to te s t r u k tu r y :
M a c ie r z s ą s ie d z tw a , w k tó re j
p rz e c h o w y w a n a je s t ta b lic a V
IIHIHłHII
n a V z w a r to ś c ia m i lo g ic z n y
Obiekty typu Bag
m i. E le m e n t w w ie rs z u v i k o
lu m n ie w m a w a rto ś ć t r u e , j e
śli w g ra fie is tn ie je k ra w ę d ź
in c y d e n tn a d la w ie rz c h o łk ó w
v i w. W p rz e c iw n y m ra z ie e le
m e n t m a w a rto ś ć fal se . T a r e
p r e z e n ta c ja n ie s p e łn ia p ie r w
szeg o w a r u n k u . G ra fy c z ę sto
m a ją m ilio n y w ie rz c h o łk ó w ,
a k o s z t p a m ię c i n a V 2 w a r to
śc i lo g ic z n y c h z n ie c h ę c a d o
>
Reprezentacje tej
s to s o w a n ia tej s tru k tu r y . samej krawędzi
Tablica k r a w ę d z i z e le m e n ta
m i ty p u Edge, k tó r e o b e jm u ją
d w ie z m ie n n e e g z e m p la rz a
ty p u i n t . T a b e z p o ś r e d n ia r e
p r e z e n ta c ja je s t p ro s ta , je d n a k
n ie s p e łn ia d ru g ie g o w a r u n
9 — 12
ku. Im p le m e n ta c ja m e to d y
ad j () w y m a g a tu s p r a w d z e n ia s . 11 9
w s z y s tk ic h k ra w ę d z i g ra fu .
Reprezentacja oparta na listach sąsiedztwa
Tablica list są s ie d ztw a , in d e k (dla grafu nieskierowanego)
so w a n a w ie rz c h o łk a m i i p rz e -
c h o w u ją c a lis ty w ie rz c h o łk ó w s ą s ia d u ją c y c h z d a n y m . T a s t r u k tu r a d a n y c h
w ty p o w y c h z a s to s o w a n ia c h s p e łn ia o b a w a r u n k i i to ją s to s u je m y w ro z d z ia le .
O p r ó c z c e ló w z o b s z a r u w y d a jn o ś c i są te ż in n e , w a ż n e w n ie k tó r y c h z a s to s o w a n ia c h
k w e stie , k tó r e m o ż n a w y k ry ć p o d o k ła d n y m p rz y jrz e n iu się s tr u k tu r o m . P rz y k ła d o w o ,
d o p u s z c z e n ie k ra w ę d z i ró w n o le g ły c h u n ie m o ż liw ia z a s to s o w a n ie m a c ie rz y s ą s ie d z
tw a , p o n ie w a ż z a p o m o c ą tej s t r u k tu r y n ie m o ż n a p r z e d s ta w ić ta k ic h k ra w ę d z i.
4.1 a Grafy nieskierowane 537
L is ty s ą s ie d z t w a S ta n d a r d o w ą re p r e z e n ta c ją g ra fó w rz a d k ic h je s t s tr u k tu r a d a n y c h
o n a z w ie listy są s ie d ztw a . W tej s tr u k tu r z e z w ie rz c h o łk ie m s k o ja rz o n e są w sz y stk ie
są s ia d u ją c e z n im w ie rz c h o łk i, z a p is a n e n a liśc ie p o w ią z a n e j. P rz e c h o w y w a n a je s t t a b
lica list, d la te g o n a p o d s ta w ie w ie rz c h o łk a m o ż n a n a ty c h m ia s t u z y s k a ć d o s tę p d o o d
p o w ie d n ie j listy. D o im p le m e n to w a n ia list u ż y w a m y ty p u A D T Bag z p o d r o z d z i a ł u
1.3 w w e rsji o p a rte j n a liśc ie p o w ią z a n e j. P o z w a la to d o d a w a ć n o w e k ra w ę d z ie w s ta
ły m c zasie i ite ro w a ć p o s ą s ia d u ją c y c h w ie rz c h o łk a c h w c z a sie s ta ły m n a k a ż d y ta k i
w ie rz c h o łe k . I m p le m e n ta c ja ty p u G raph, p r z e d s ta w io n a n a s tr o n ie 5 3 8 , o p a r ta je s t n a
ty m p o d e jś c iu . N a r y s u n k u n a p o p rz e d n ie j s tr o n ie p r z e d s ta w io n o s t r u k tu r ę d a n y c h
u tw o rz o n ą za p o m o c ą te g o k o d u n a p o d s ta w ie p lik u tin y G .tx t. A b y d o d a ć k ra w ę d ź
łą c z ą c ą v i w, n a le ż y d o d a ć w d o listy s ą s ie d z tw a d la v o ra z v d o lis ty s ą s ie d z tw a d la w.
T ak w ię c k a ż d a k ra w ę d ź w y s tę p u je w tej s tr u k tu r z e d w u k r o tn ie . O m a w ia n a im p le
m e n ta c ja ty p u G raph m a n a s tę p u ją c e c e c h y z o b s z a r u w y d a jn o śc i:
° P a m ię ć z a jm o w a n a je s t p r o p o r c jo n a ln ie d o V + E.
■ D o d a n ie k ra w ę d z i z a jm u je s ta ły czas.
D C z a s ite ro w a n ia p o w ie rz c h o łk a c h s ą s ia d u ją c y c h z v je s t p r o p o r c jo n a ln y d o
s to p n ia v (p o tr z e b n y je s t s ta ły czas n a p rz e tw a r z a n y s ą s ia d u ją c y w ie rz c h o łe k ).
C ech y te są o p ty m a ln e d la p rz e d sta w io n e g o z b io ru o p eracji, k tó r y je s t o d p o w ie d n i d la
o m aw ian y c h zasto so w a ń m e to d p rz e tw a rz a n ia grafów . K raw ęd zie ró w n o le g łe i p ę tie
w łasn e są tu d o z w o lo n e (k o d n ie sp ra w d z a ich w y stą p ie n ia ). Uwaga: w a ż n e je s t to, że k o
lejn o ść d o d a w a n ia k ra w ę d z i d o g ra fu je st w y z n a c z n ik ie m k o lejn o ści p o ja w ia n ia się w ie rz
c h o łk ó w w tab licy list sąsie d z tw a tw o rz o n y c h za p o m o c ą ty p u Graph. T en sa m g ra f m o ż n a
p rzed staw ić za p o m o c ą w ielu ró ż n y ch tab lic list sąsiedztw a. P rz y sto so w a n iu k o n s tru k to
ra w czytującego k ra w ę d z ie ze s tru m ie n ia w ejścio w eg o o z n a c z a to , że fo rm a t d a n y c h w e j
ściow ych i k o le jn o ść k raw ęd z i w p lik u je s t w y z n a c z n ik ie m k o le jn o śc i w ie rz c h o łk ó w n a
listach sąsied ztw a b u d o w a n y c h p rz y u ż y c iu
ldasy Graph. P o n iew aż a lg o ry tm y o p a rte są
n a m e to d z ie a d j () i p rz e tw a rz a ją w szy stk ie
sąsied n ie w ie rz c h o łk i b e z u w z g lę d n ia n ia ich
k o lejn o ści n a listach, k w estia ta n ie w p ły w a
n a p o p ra w n o ś ć k o d u , je d n a k w a rto o niej
p a m ię ta ć w czasie d ia g n o z o w a n ia lu b a n a li t i n y G .t x t
% j ava Graph t in y G . t x t
zo w an ia śla d u d z ia ła n ia p ro g ra m u . W celu
13 v e r t ic e s , 13 edges
u ła tw ie n ia ty c h z a d a ń zak ład am y , że k lasa 0: 6 2 1 5
5
Graph m a k lie n ta testo w eg o , k tó r y w czy tu je 3
1: 0 X
2: 0
1 4 Pierwszy sąsiedni
g ra f ze s tru m ie n ia w ejścio w eg o p o d a n e g o 12
3: 5
4: 5 6 3 wierzchołek z danych
ja k o a rg u m e n t w iersza p o le c e ń , a n a s tę p 4 wejściowych jest
5: 3 4 0
4 ostatnim na liście
n ie w y św ied a g ra f (uży w ając im p le m e n ta c ji 2
6: 0 4
7:
m e to d y t o S t r i n g ( ) ze stro n y 535), ab y p o 11 12
9 10
kazać k o le jn o ść w y stę p o w a n ia w ie rz c h o ł 9 : 11 10 12 Drugie wystąpienie
0 6
10: 9 każdej krawędzi
k ó w n a listach sąsied ztw a. W tej k o le jn o ści 7 8
9 11
1 1 : 9 12 wyróżniono kolorem
a lg o ry tm y p rz e tw a rz a ją w ie rz c h o łk i (zo b acz 5 3
12: 11 9 czerwonym
ć w ic z e n ie 4 . 1 .7 ). Dane wyjściowe dla danych
wejściowych w postaci listy krawędzi
538 R O ZD ZIA Ł 4 Grafy
Typ danych Graph
public c la s s Graph
{
private final in t V; 11 L i czba wierzchołków,
private in t E; // Li czba krawędzi,
private Bag<Integer>[] adj; // L i s t y sąsiedztwa.
public Graph(int V)
{
t h is .V = V; t h i s . E = 0;
adj = (B ag<Integer>[]) new Bag[V]; // Tworzenie t a b l i c y l i s t ,
fo r (in t v = 0; v < V; v++) // I ni cj o w a n i e w s z y st k i c h l i s t
adj [v] = new Ba g<In teg er> (); // (początkowo pust y ch) .
}
public Graph(In in)
{
th is(in .re a d ln t()); // Wczytywanie V i tworzeni e gr afu,
in t E = i n . r e a d l n t ( ) ; // Wczytywanie E.
fo r (in t i = 0; i < E; i++)
{ // Dodawanie krawędzi.
in t v = i n . r e a d l n t ( ) ; // Wczytywanie wi erz choł k a;
i n t w = i n . r e a d l n t ( ) ; // wczytywanie następnego wi er z ch o ł k a
addEdge(v, w); // i dodawanie ł ączącej j e krawędzi.
)
}
public in t V() { return V; }
public in t E() { return E; )
public void addEdge(int v, in t w)
{
a d j [ v ] .add(w); // Dodawanie w do l i s t y dl a v.
adj[w].add ( v ) ; // Dodawanie v do l i s t y dla w.
E++;
}
public Iterab le<In te ge r> adj (in t v)
{ return adj [ v ] ; }
}
W tej im p le m e n ta c ji ld asy Graph p rz e c h o w y w a n a je s t in d e k so w a n a w ie rz c h o łk a m i tab lica
list liczb całkow ity ch . K ażd a k ra w ę d ź w y stęp u je tu d w u k ro tn ie . Jeśli k ra w ęd ź łączy v z w,
w p o jaw ia się n a liście v, a v — n a liście w. D ru g i k o n s tru k to r w czy tu je g ra f ze s tru m ie n ia
w ejściow ego. G ra f m a tu fo rm at: V, E, lista p a r w a rto śc i ty p u i nt z p rz e d z ia łu o d 0 d o V - 1.
M e to d a t o S t r i ng () z n a jd u je się n a stro n ie 535.
4.1 ci Grafy nieskierowane 539
z p e w n o ś c i ą w a r t o z a s ta n o w ić się n a d in n y m i o p e ra c ja m i, k tó r e m o g ą b y ć p r z y
d a tn e w a p lik a c ja c h . Są to n a p r z y k ła d m e to d y d o :
° d o d a w a n ia w ie rz c h o łk a ,
° u s u w a n ia w ie rz c h o łk a .
J e d n y m ze s p o s o b ó w n a o b s łu g ę ta l a c h o p e ra c ji je s t ro z w in ię c ie in te rfe js u A P I p rz e z
z a s to s o w a n ie ta b lic y s y m b o li (ST) z a m ia s t ta b lic y in d e k s o w a n e j w ie rz c h o łk a m i (p o
tej z m ia n ie ja k o n a z w w ie rz c h o łk ó w n ie tr z e b a u ż y w a ć in d e k s ó w c a łk o w ito lic z b o -
w y c h ). M o ż n a te ż z a s ta n o w ić się n a d m e to d a m i d o :
° u s u w a n ia k ra w ę d z i,
0 s p ra w d z a n ia , c zy g r a f o b e jm u je k ra w ę d ź v-w.
A b y z a im p le m e n to w a ć te m e to d y (i u n ie m o ż liw ić is tn ie n ie k ra w ę d z i ró w n o le g ły c h ),
m o ż n a z a s to s o w a ć d la lis t s ą s ie d z tw a ty p SET z a m ia s t ty p u Bag. T ę m o ż liw o ś ć n a z y
w a m y re p r e z e n ta c ją w p o s ta c i z b io r u są s ie d ztw a . Jest k ilk a p o w o d ó w , d la k tó r y c h
w k sią ż c e n ie u ż y w a m y te g o ro z w ią z a n ia .
° O m a w ia n e t u k lie n ty n ie m u s z ą d o d a w a ć w ie rz c h o łk ó w , u s u w a ć w ie rz c h o łk ó w
i k ra w ę d z i a n i sp ra w d z a ć , c z y k ra w ę d ź is tn ie je .
D Jeśli k lie n ty w y m a g a ją ta k ic h o p e ra c ji, z w y k le w y w o łu ją je r z a d k o lu b d la k r ó t
k ic h lis t s ą s ie d z tw a , d la te g o ła tw y m r o z w ią z a n ie m je s t z a s to s o w a n ie im p le m e n
ta c ji p rz e z a ta k siło w y i ite ro w a n ie p o lis ta c h s ą s ie d z tw a .
a R e p re z e n ta c je o p a r te n a ty p a c h SET i ST n ie c o k o m p lik u ją k o d a lg o r y tm ó w o ra z
o d w ra c a ją o d n ic h u w ag ę .
° W p e w n y c h s y tu a c ja c h m o ż e n a s tą p ić s p a d e k w y d a jn o ś c i n a p o z io m ie lo g V.
N ie tr u d n o d o s to s o w a ć p rz e d s ta w io n e tu a lg o r y tm y d o in n y c h p ro je k tó w (n a p r z y
k ła d z a b ro n ić tw o rz e n ia k ra w ę d z i ró w n o le g ły c h lu b p ę tli w ła s n y c h ) b e z z n a c z n e g o
s p a d k u w y d a jn o ś c i. W ta b e li p o n iż e j z n a jd u je się p rz e g lą d c e c h z o b s z a r u w y d a jn o
ści d la ró ż n y c h ro z w ią z a ń . W ty p o w y c h z a s to s o w a n ia c h p r z e tw a r z a n e są d u ż e g ra fy
rz a d k ie , d la te g o s to s u je m y r e p r e z e n ta c je w p o s ta c i lis t są s ie d z tw a .
Dodawanie Sprawdzanie, Iterowanie po wierzchołkach
Struktura danych Pamięć
krawędzi v-w czy w sąsiaduje z v sąsiadujących z v
Lista kraw ędzi E 1 E E
M acierz sąsiedztwa V1 1 1 V
Listy sąsiedztw a E + V 1 degree(y) degree(y)
Z biory sąsiedztwa E + V log V lo g V lo g V + degree(y)
W ydajność (tempo wzrostu) dla typow ych implementacji typu Graph
540 RO ZD ZIA Ł 4 o Grafy
W z o r c e p r o j e k to w e z z a k r e s u p r z e t w a r z a n i a g r a f ó w P o n ie w a ż o m a w ia m y w iele
a lg o r y tm ó w p r z e tw a r z a n ia g rafó w , p ie r w s z y m c e le m p r o je k to w y m je s t o d d z ie le n ie
im p le m e n ta c ji o d r e p r e z e n ta c ji grafó w . W ty m c e lu d la k a ż d e g o z a d a n ia ro z w ija m y
s p e c y fic z n ą d la n ie g o k la sę . K lie n ty w c e lu w y k o n a n ia z a d a n ia m o g ą tw o rz y ć o b ie k ty
tej klasy. K o n s tr u k to r p rz e p r o w a d z a w s tę p n e p rz e tw a r z a n ie p r z y tw o r z e n iu s t r u k tu r
d a n y c h , a b y m ó c w y d a jn ie re a g o w a ć n a z a p y ta n ia o d k lie n ta . T y p o w y k lie n t tw o rz y
g raf, p rz e k a z u je g o d o k la s y z im p le m e n ta c ją a lg o r y tm u (ja k o a r g u m e n t k o n s t r u k
to r a ), a n a s tę p n ie w y w o łu je m e to d y k lie n c k ie w c e lu u s ta le n ia ró ż n y c h c e c h g ra fu .
W r a m a c h ro z g r z e w k i z a s ta n ó w m y się n a d p o n iż s z y m in te rfe js e m A P I.
p ub lic c l a s s Search
SearchfGraph G, in t s) Znajduje wierzchołki połączone
ze źródłowym wierzchołkiem s
boolean marked(int v) Czy v jest połączony z s?
i n t countf) Ile wierzchołków jest połączonych z s?
Interfejs API do przetwarzania grafów (rozgrzewka)
U ż y w a m y n a z w y w ie rz c h o łe k źr ó d ło w y , a b y o d ró ż n ić w ie rz c h o łe k p rz e k a z a n y ja k o
a r g u m e n t d o k o n s t r u k to r a o d in n y c h w ie rz c h o łk ó w g ra fu . W ty m in te rfe js ie A P I z a
d a n ie m k o n s t r u k to r a je s t z n a le z ie n ie w g ra fie w ie rz c h o łk ó w p o łą c z o n y c h ze ź r ó d ł o
w y m . N a s tę p n ie k o d k lie n ta w y w o łu je m e to d y e g z e m p la rz a marked() i c o u n t ( ) , a b y
p o z n a ć c e c h y g ra fu . N a z w a mar ked () (czy li „ o z n a c z o n y ”) n a w ią z u je d o p o d e jś c ia s to
s o w a n e g o w p o d s ta w o w y c h a lg o r y tm a c h o m a w ia n y c h w ro z d z ia le — m e t o d a p r z e
c h o d z i śc ie ż k ą z w ie rz c h o łk a ź ró d ło w e g o d o in n y c h w ie rz c h o łk ó w g ra f u i o z n a c z a
k a ż d y n a p o tk a n y . P rz y k ła d o w y k lie n t T e s t Search p r z e d s ta w io n y n a n a s tę p n e j s tro n ie
p o b ie r a z w ie rs z a p o le c e ń n a z w ę s t r u m ie n ia w e jśc io w e g o i n u m e r ź ró d ło w e g o w ie r z
c h o łk a , w c z y tu je g r a f ze s t r u m ie n ia w e jśc io w e g o (z a p o m o c ą d ru g ie g o k o n s t r u k to r a
k la s y Graph), tw o rz y o b ie k t Search d la d a n e g o g ra f u i w ie rz c h o łk a ź ró d ło w e g o o ra z
u ż y w a m e to d y m ar ked () d o w y ś w ie tle n ia w ie rz c h o łk ó w p o łą c z o n y c h ze ź ró d ło w y m .
P r o g r a m w y w o łu je te ż m e to d ę c o u n t( ) i w y św ie tla in f o rm a c ję , c z y g r a f je s t s p ó jn y
(g r a f je s t s p ó jn y w te d y i ty lk o w ted y , je ś li p r z y w y s z u k iw a n iu o z n a c z o n o w sz y stk ie
w ie rz c h o łk i).
4.1 B Grafy nieskierowane 541
p o k a z a l i ś m y j u ż je d e n ze s p o s o b ó w n a z a im p le m e n to w a n ie in te rfe js u A P I k la s y
S e a rc h . U m o ż liw ia ją to a lg o r y tm y z w ią z a n e z p r o b le m e m U n io n - F in d ( r o z d z i a ł i.).
K o n s tr u k to r m o ż e u tw o rz y ć o b ie k t ty p u UF, w y k o n a ć o p e ra c ję uni on () n a k a ż d e j k r a
w ę d z i g ra f u i o b s łu ż y ć o p e ra c ję m ar ked ( v) p rz e z w y w o ła n ie m e to d y c o n n e c te d ( s , v ) .
Z a im p le m e n to w a n ie m e to d y c o u n t () w y m a g a z a s to s o w a n ia w a ż o n e j w e rsji k la s y UF
i ro z w in ię c ia je j in te rfe js u A P I o m e to d ę c o u n t () z w ra c a ją c ą w a r to ś ć wt [find (v ) ] ( z o
b a c z ć w i c z e n i e 4 . 1 . 8 ). Im p le m e n ta c ja ta je s t p r o s ta i w y d a jn a , je d n a k ro z w ią z a n ie
o p is a n e d alej je s t je s z c z e ła tw ie js z e i sz y b sz e. O p a rliś m y je n a p r z e s z u k iw a n iu w g łę b .
Jest to je d n a z g łó w n y c h te c h n ik re k u r e n c y jn y c h , p o le g a ją c a n a p r z e c h o d z e n iu p o
k ra w ę d z ia c h g ra f u w c e lu z n a le z ie n ia w ie rz c h o łk ó w p o łą c z o n y c h z w ie rz c h o łk ie m
ź ró d ło w y m . P rz e s z u k iw a n ie w g łą b je s t p o d s ta w ą k ilk u a lg o r y tm ó w p rz e tw a r z a n ia
grafów , k tó r e o m a w ia m y w ro z d z ia le .
public c l a s s TestSearch
(
p ublic s t a t i c void m a in ( Str in g [] args )
{
Graph G = new Graph(new I n ( a r g s [ 0 ] ) ) ;
in t s = I n t e g e r . p a r s e l n t ( a r g s [ l ] );
Search search = new Search(G, s );
f o r (i n t v = 0; v < G.V (); v++)
i f ([Link](v))
S t d O u t. p rin t (v + " " ) ;
Std O ut.p rintlnO ;
i f (s earc [Link]() != G .V ( )) t in y G .t x t
Std O u t.p rint("N IE");
StdOu [Link] n t l n ( "spój ny" ) ; 13
} 0 5
) 4 3
0 1
Przykładowy klient do przetwarzania grafów (rozgrzewka) 9 12
64
5 4
% java TestSearch t in y G .t x t 0 0 2
11 12
0 1 2 3 4 5 6
9 10
NIEspójny
0 6
7 8
% java TestSearch t in y G .t x t 9 9 11
9 10 11 12 5 3
NIEspójny
542 RO ZD ZIA Ł 4 a Grafy
P rz e s z u k iw a n ie w g łą b C e c h y g ra f u c z ę sto o k re ś la się p rz e z s y s te m a ty c z n e
s p r a w d z a n ie k a ż d e g o w ie rz c h o łk a i w s z y s tk ic h je g o k ra w ę d z i. P e w n e p r o s te c e c h y
g ra fu , n a p rz y k ła d s to p ie ń w s z y s tk ic h w ie rz c h o łk ó w , m o ż n a ła tw o u s ta lić n a p o d s t a
w ie s a m y c h k ra w ę d z i (s p ra w d z a n y c h w d o w o ln e j k o le jn o ś c i). J e d n a k w ie le in n y c h
cech z w ią z a n y c h je s t ze śc ie ż k a m i,
Labirynt
d la te g o n a tu r a ln y s p o s ó b n a p o z n a n ie
ta k ic h w ła śc iw o ś c i to p r z e c h o d z e n ie
m ię d z y w ie rz c h o łk a m i w z d łu ż k r a w ę
d z i g ra fu . P ra w ie w sz y stk ie o m a w ia n e
Skrzyżowanie a lg o r y tm y p r z e tw a r z a n ia g ra fó w są
Alejka o p a r te n a ty m s a m y m p o d s ta w o w y m
Graf
m o d e lu a b s tra k c y jn y m , c h o ć s to s o w a
n e są ró ż n e stra te g ie . N a jp ro s ts z a je s t
o p is a n a t u k la s y c z n a m e to d a .
P r z e s z u k i w a n i e l a b i r y n t u O p ro c e s ie
Wierzchołek Kmwędź p r z e s z u k iw a n ia g ra f u w a rto p o m y ś le ć
w k a te g o r ia c h a n a lo g ic z n e g o p ro b
Odpowiadające sobie modele labiryntu
le m u o d łu g ie j h is to rii. P r o b le m e m
ty m je s t w y s z u k iw a n ie d ro g i w la b i
ry n c ie s k ła d a ją c y m się z a le je k p o łą c z o n y c h s k rz y ż o w a n ia m i.
N ie k tó re la b ir y n ty m o ż n a p rz e tw o r z y ć za p o m o c ą p ro s te j reg u ły ,
je d n a k w ię k sz o ś ć w y m a g a z a s to s o w a n ia b a rd z ie j z a a w a n s o w a
n ej stra te g ii. U ży c ie n a z w y la b ir y n t z a m ia s t g ra f, a le jk a z a m ia s t
k r a w ę d ź i s k r z y ż o w a n ie z a m ia s t w ie rz c h o łe k to z a b ie g c z y sto s e
m a n ty c z n y , k tó r y je d n a k p o m a g a in tu ic y jn ie z ro z u m ie ć p r o b
le m . J e d n ą ze sz tu c z e k p r z y e k s p lo ro w a n iu la b iry n tu , z n a n ą o d
c z a só w a n ty c z n y c h (p rz y n a jm n ie j o d c z a só w le g e n d y o T e z e u sz u
i M in o ta u rz e ), je s t a lg o r y tm T re m a u x . A b y s p ra w d z ić w sz y stk ie
a le jk i la b iry n tu , n a le ż y :
■ W y b ra ć d o w o ln ą n ie o z n a c z o n ą alejkę i ro z w in ą ć za so b ą nić.
■ O z n a c z y ć w sz y stk ie sk rz y ż o w a n ia i a le jk i w c z a sie p ie r w Eksplorowanie
szeg o p rz e jś c ia p r z e z n ie . metodą Tremaux
D W y c o fa ć się (w y k o rz y s tu ją c n ić ) p o n a p o tk a n iu o z n a c z o n e
go sk rz y ż o w a n ia .
■ W y c o fa ć się, je ś li sk rz y ż o w a n ie n a p o tk a n e w c za sie p o w r o tu n ie p ro w a d z i d o
n ie o z n a c z o n y c h a lejek .
N ić g w a ra n tu je , że z a w sze m o ż n a z n a le ź ć d ro g ę p o w r o tu , a o z n a c z e n ia p o z w a la ją
u n ik n ą ć d w u k r o tn e g o o d w ie d z a n ia a le je k lu b s k rz y ż o w a ń . U s ta le n ie , że z b a d a n o
c a ły la b iry n t, je s t b a rd z ie j s k o m p lik o w a n e . Z p r o b le m e m ty m lep iej z m ie rz y ć się
w k o n te k ś c ie p rz e s z u k iw a n ia g ra fu . E k s p lo ro w a n ie m e t o d ą T re m a u x je s t in tu ic y j
n y m p u n k te m w y jśc ia , je d n a k w y s tę p u ją t u p e w n e s u b te ln e ró ż n ic e w z g lę d e m e k s
p lo r o w a n ia g ra fu , d la te g o p rz e c h o d z im y te r a z d o p r z e s z u k iw a n ia grafó w .
4.1 □ Grafy nieskierowane 543
R o z g r z e w k a K la sy c z n a re k u r e n c y jn a m e to d a p rz e s z u k iw a n ia g ra fó w s p ó jn y c h ( o d
w ie d z a n ia w s z y s tk ic h w ie rz c h o łk ó w i k ra w ę d z i) o d z w ie rc ie d la e k s p lo ro w a n ie la b i
r y n tu m e to d ą T re m a u x , je s t je d n a k je sz c z e
ła tw ie jsz a d o o p is a n ia . W c e lu p rz e s z u k a n ia p u b lic c l a s s D epthF irstS earch
{
g ra fu n a le ż y w y w o ła ć re k u r e n c y jn ą m e to d ę , p riv ate booleanj] marked;
k tó r a p r z e c h o d z i p o w ie rz c h o łk a c h . P rz y o d p riv ate i n t count;
w ie d z a n iu w ie rz c h o łk ó w n a le ż y : p ub lic DepthFirstSearch(Graph G, in t s)
° O z n a c z y ć w ie rz c h o łe k ja k o o d w ie d z o n y . {
marked = new b o o le a n [G .V ()];
a O d w ie d z ić ( r e k u re n c y jn ie ) w sz y stk ie s ą dfs(G , s );
sie d n ie , ale n ie o z n a c z o n e w ie rz c h o łk i. }
Jest to m e t o d a p r z e s z u k iw a n ia w g łą b (an g .
p riv ate void dfs(Graph G, i n t v)
d e p th -first search — D F S ). W im p le m e n ta c ji f
in te rfe js u A P I k la s y S e a rc h u ż y w a m y m e to d y markedjv] = true;
count++;
p o k a z a n e j p o p ra w e j s tro n ie . M e to d a p r z e c h o for (in t w : [Link](v))
w u je ta b lic ę w a rto ś c i ty p u b o o le a n d o o z n a i f ( ! marked[w]) dfs(G , w );
}
c z a n ia w s z y s tk ic h w ie rz c h o łk ó w p o łą c z o n y c h
ze ź ró d ło w y m . M e to d a r e k u r e n c y jn a o z n a p ub lic boolean marked(int w)
{ return marked[w]; }
c za d a n y w ie rz c h o łe k i w y w o łu je s a m ą sie b ie
d la n ie o z n a c z o n y c h w ie rz c h o łk ó w z lis ty s ą p ub lic in t count()
{ return count; }
sie d z tw a . Jeśli g r a f je s t sp ó jn y , s p r a w d z a n e są
w sz y stk ie lis ty s ą s ie d z tw a . }
Przeszukiwanie w głąb
Twierdzenie A. M e to d a D F S o z n a c z a w sz y stk ie
w ie rz c h o łk i p o w ią z a n e ze ź ró d ło w y m i ro b i to
w c zasie p r o p o r c jo n a ln y m d o s u m y ic h s to p n i.
Dowód. N a jp ie rw d o w ie d ź m y , że a lg o ry tm o z n a
cza w sz y stk ie w ie rz c h o łk i p o w ią z a n e ze ź ró d ło w y m
s (i n ie o z n a c z a ż a d n y c h in n y c h ). K a żd y o z n a c z o n y
w ie rz c h o łe k je s t p o w ią z a n y z s, p o n ie w a ż a lg o ry tm
z n a jd u je w ie rz c h o łk i ty lk o p rz e z p rz e c h o d z e n ie
w z d łu ż k ra w ę d z i. Z ałó ż m y , że z s p o łą c z o n y je s t
p e w ie n n ie o z n a c z o n y w ie rz c h o łe k w. P o n ie w a ż s a m
s je s t o zn aczo n y , k a ż d a ście ż k a z s d o w m u s i o b e j
m o w a ć p rz y n a jm n ie j je d n ą k ra w ę d ź ze z b io r u o z n a
c z o n y c h w ie rz c h o łk ó w d o z b io r u w ie rz c h o łk ó w n ie
o z n a c z o n y c h (n ie c h b ę d z ie to k ra w ę d ź v -x ). Je d n a k
a lg o ry tm w y k ry łb y x p o o z n a c z e n iu v, d la te g o ta k a
k ra w ę d ź n ie m o ż e istn ie ć , p o w sta je w ię c s p rz e c z
n o ść. O g ra n ic z e n ie czaso w e w y n ik a z teg o , że o z n a
c z an ie g w a ra n tu je , iż k a ż d y w ie rz c h o łe k je s t o d w ie
d z a n y je d n o k r o tn ie (s p ra w d z e n ie o z n a c z e ń z a jm u je
czas p ro p o r c jo n a ln y d o s to p n ia w ie rz c h o łk a ).
544 R O ZD ZIA Ł 4 □ Grafy
A l e j k i j e d n o k i e r u n k o w e M e c h a n iz m w y w o ły w a n ia m e t o d i z w ra c a n ia ste ro w a n ia
w p r o g r a m ie o d p o w ia d a n ic i w la b iry n c ie . P o p r z e tw o r z e n iu w s z y s tk ic h k ra w ę d z i
p o w ią z a n y c h z w ie rz c h o łk ie m (s p ra w d z e n iu w s z y s tk ic h a le je k w y c h o d z ą c y c h ze
s k rz y ż o w a n ia ) n a le ż y z w ró c ić s te ro w a n ie (czy li z a w ró c ić ). A b y n a ry s o w a ć sy tu a c ję
o d p o w ia d a ją c ą e k s p lo ro w a n iu la b ir y n tu m e to d ą T re m a u x , tr z e b a w y o b ra z ić so b ie
la b ir y n t o b e jm u ją c y a le jk i je d n o k ie r u n k o w e (p o je d n e j w k a ż d y m k ie r u n k u ) . W te n
s a m s p o s ó b , w ja k i d w u k r o tn ie (je d e n ra z w k a ż d y m k ie r u n k u ) n a p o ty k a m y k a ż d ą
a le jk ę la b iry n tu , d w u k r o tn ie n a tr a fia m y te ż n a
t in y C G .t x t Standardowy rysunek k a ż d ą k ra w ę d ź (w y c h o d z ą c je d e n ra z z k a ż d e g o
z jej w ie rz c h o łk ó w ). P rz y e k s p lo ro w a n iu m e t o
d ą T re m a m t a lb o s p r a w d z a m y a lejk ę p ie rw s z y
0 5 ra z , a lb o w ra c a m y n ią z o z n a c z o n e g o w ie r z c h o ł
2 4
2 3
k a . W m e to d z ie D F S d la g ra f u n ie s k ie ro w a n e g o
^ ^ Rysunek z obiema krawędziami p o n a p o tk a n iu k ra w ę d z i v-w a lb o n a s tę p u je re -
k u re n c y jn e w y w o ła n ie (je śli w n ie je s t o z n a c z o
3 4
3 5 n y ), a lb o n a le ż y p o m in ą ć k r a w ę d ź (je ż e li w je s t
0 2 o z n a c z o n y ). P rz y d r u g i m n a p o tk a n iu k ra w ę d z i,
w c z a sie p r z e c h o d z e n ia w k ie r u n k u w-v, zaw sze
n a le ż y ją p o m in ą ć , p o n ie w a ż w ie rz c h o łe k d o c e
Listy sąsiedztwa
lo w y v z p e w n o ś c ią z o s ta ł ju ż o d w ie d z o n y (p rz y
p ie r w s z y m n a p o tk a n i u k ra w ę d z i).
Ś le d z e n ie d z i a ł a n i a m e t o d y D F S Ja k zw y k le
je d n y m z d o b r y c h s p o s o b ó w n a z ro z u m ie n ie a l
g o r y tm u je s t p rz e ś le d z e n ie je g o d z ia ła n ia n a m a
ły m p rz y k ła d z ie . Jest to sz c z e g ó ln ie o d c z u w a ln e
p r z y p rz e s z u k iw a n iu w g łąb . P ie rw s z ą rz e c z ą ,
o k tó re j n a le ż y p a m ię ta ć p r z y tw o r z e n iu śla d u ,
je s t to , że k o le jn o ś ć o k re ś la n ia s p ra w d z o n y c h
k ra w ę d z i i o d w ie d z o n y c h w ie rz c h o łk ó w z a le ż y
o d re p re ze n ta c ji, a n ie ty lk o o d g ra f u lu b a lg o
Spójny graf nieskierowany ry tm u . P o n ie w a ż m e to d a D F S s p ra w d z a je d y
n ie w ie rz c h o łld p o w ią z a n e ze ź ró d ło w y m , p rz y
tw o r z e n iu ś la d u ja k o p rz y k ła d u u ż y w a m y m a łe g o g ra f u s p ó jn e g o p rz e d s ta w io n e g o
p o lew ej s tro n ie . W p rz y k ła d z ie w ie rz c h o łe k 2 to p ie r w s z y w ie rz c h o łe k o d w ie d z a n y
p o 0, p o n ie w a ż w y s tę p u je ja k o p ie r w s z y n a liśc ie s ą s ie d z tw a w ie rz c h o łk a 0. D r u g ą
k w e stią , n a k tó r ą tr z e b a z w ró c ić u w a g ę , je s t to , że — j a k w s p o m n ie liś m y — m e to d a
D F S p r z e c h o d z i w z d łu ż k a ż d e j k ra w ę d z i d w u k r o tn ie i z aw sz e z n a jd u je o z n a c z o n y
w ie rz c h o łe k p o r a z d ru g i. J e d n y m z w n io s k ó w z te g o s p o s tr z e ż e n ia je s t to , że ś le
d z e n ie d z ia ła n ia m e to d y D F S z a jm u je d w u k r o tn ie w ię ce j c z a su , n iż m o ż n a sąd zić!
P rz y k ła d o w y g r a f m a ty lk o o s ie m k ra w ę d z i, tr z e b a je d n a k p rz e ś le d z ić d z ia ła n ie a lg o
r y t m u d la 16 e le m e n tó w z lis ty s ą s ie d z tw a .
4.1 □ Grafy nieskierowane 545
Szczegółowy ślad przeszukiw ania w głąb Na rysunku po prawej stronie pokazano
zawartość struktur danych bezpośrednio po oznaczeniu każdego wierzchołka w om a
wianym krótkim przykładzie (wierzchołkiem źródłowym jest 0). Wyszukiwanie roz
poczyna się, kiedy konstruktor wywołuje rekurencyjną metodę dfs () w celu odwie
dzenia i oznaczenia wierzchołka 0.
marked[] ad j [ ]
Oto dalszy przebieg tego procesu.
° Ponieważ wierzchołek 2 jest dfs(O)
pierwszy na liście sąsiedztwa
wierzchołka 0 i jest nieoznaczo
ny, m etoda dfs () rekurencyjnie
wywołuje samą siebie, aby od dfsC2)
S p r a w d z a n ie 0
wiedzić i oznaczyć 2 (system
umieszcza na stosie 0 i aktual
ną pozycję na liście sąsiedztwa
tego wierzchołka). d f s c i) n
^ 0 T 0 2 1 5
° Teraz 0 zajmuje pierwszą pozy [ S p r a w d z a n ie 0 1 T 1 02
I S p r a w d z a n ie 2 2 T 2 0 1 B 4
cję na liście sąsiedztwa 2 , ale jest 1 Gotow y 3 3 5 4 2
32
) 5 5 3 0
już oznaczony, dlatego metoda C
d fs() pomija 0. Ponieważ na
d f s (3 ) 1 5
liście sąsiedztwa wierzchołka 2 02
0 13
następny jest — nieoznaczony 5 4 2
3 2
— wierzchołek 1 , m etoda dfs () 3 0
rekurencyjnie wywołuje samą
d f s (5 )
siebie i odwiedza 1 . 2 1 5
j s p r a w d z a n ie 02
0 Odwiedziny 1 wyglądają ina 1 S p r a w d z a n ie 0 1. 3
5 4 2
5 G otow y
3 2
czej. Ponieważ oba wierzchołki 3 0
na liście (0 i 2 ) są już oznaczo
ne, wywołania rekurencyjne nie d f s (4 )
2 1 5
| S p r a w d z a n ie 3 02
są potrzebne, a metoda dfs () | S p r a w d z a n ie 2 0 13 4
4 G otow y 5 4 2
zwraca sterowanie z rekuren- S p r a w d z a n ie 2
3 2
3 0
cyjnego wywołania df s (1). Nas 3 G otow y
S p r a w d z a n ie 4
tępna sprawdzana krawędź to 2 G otow y
S p r a w d z a n ie 1
2-3 (ponieważ 3 to wierzchołek
S p r a w d z a n ie 5
po 1 na liście sąsiedztwa wierz 0 G otow y
chołka 2 ), tak więc metoda Ślad przeszukiwania w głąb w celu znalezienia wierzchołków powiązanych z 0
dfs () rekurencyjnie wywołuje
samą siebie w celu odwiedzenia i oznaczenia 3.
0 Na liście sąsiedztwa wierzchołka 3 pierwszy jest — nieoznaczony — wierzcho
łek 5, dlatego m etoda dfs () rekurencyjnie wywołuje samą siebie w celu odwie
dzenia i oznaczenia 5.
D Oba wierzchołki na liście sąsiedztwa 5 (3 i 0) są już oznaczone, dlatego dalsze
wywołania rekurencyjne są zbędne.
546 RO ZD ZIA Ł 4 0 Grafy
■ Następny na liście sąsiedztwa wierzchołka 3 jest — nieoznaczony — wierzcho
łek 4, dlatego m etoda dfs () rekurencyjnie wywołuje samą siebie w celu odwie
dzenia i oznaczenia 4. Jest to ostatni wierzchołek, który trzeba oznaczyć.
■ Po oznaczeniu wierzchołka 4 metoda d fs() musi sprawdzić wierzchołki z li
sty 4, potem pozostałe wierzchołki z listy 3, następnie z listy 2, a potem z listy 0.
Nie zgłasza jednak dalszych wywołań rekurencyjnych, ponieważ wszystkie
wierzchołki są oznaczone.
T E N PO D STA W O W Y R E K U REN CY JN Y SC H E M A T TO D O P IE R O P O C Z Ą T E K . Przeszukiwanie
w głąb jest skuteczne w wielu zadaniach związanych z przetwarzaniem grafów.
Przykładowo, w tym podrozdziale omawiamy wykorzystanie przeszukiwania w głąb
do rozwiązania problemu, który postawiliśmy w r o z d z i a l e i .
Określanie połączeń. Zapewnij dla grafów obsługę zapytań w postaci: Czy dwa
wierzchołki są powiązane? i Ile spójnych składowych istnieje w grafie?
Problem ten m ożna łatwo rozwiązać za pom ocą standardowego wzorca przetwa
rzania grafów. Porównamy to rozwiązanie z algorytmami Union-Find omówionymi
W P O D R O Z D Z IA L E 1 . 5 .
Pytanie: „Czy dwa wierzchołki są powiązane?” jest analogiczne do pytania:
„Czy istnieje ścieżka łącząca dwa wierzchołki?” Problem ten można nazwać wy
krywaniem ścieżki. Jednak struktury danych dla problemu Union-Find omówione
w p o d r o z d z i a l e 1.5 nie pozwalają rozwiązać problemu wyznaczania takich ścieżek.
Przeszukiwanie w głąb jest pierwszym z kilku opisanych tu podejść do rozwiązania
tego problemu, a ponadto dotyczy innej kwestii.
Ścieżki z jednego źródła. Dla grafu i źródłowego wierzchołka s zapewnij obsługę
zapytań w postaci: Czy istnieje ścieżka z s d o danego docelowego wierzchołka v? Jeśli
tak, znajdź taką ścieżkę.
Metoda DFS jest zwodniczo prosta, ponieważ jest oparta na znanej technice i ła
twa do zaimplementowania. W rzeczywistości jest to wyrafinowany i wartościowy
algorytm. Badacze nauczyli się korzystać z niego do rozwiązywania wielu trudnych
problemów. Wymieniliśmy już dwa pierwsze z kilku, które omówimy.
4.1 ■ G rafy nieskierowane 547
Wyznaczanie ścieżek Wyznaczanie ścieżek z jednego źródła to podstawowy
problem w dziedzinie przetwarzania grafów. Zgodnie ze standardowymi wzorcami
projektowymi używamy następującego interfejsu API.
p u b l i c c l a s s Paths
P aths(Graph G, in t s) Znajduje w G ścieżki ze źródła s
boolean hasPathTo(int v) Czy istnieje ścieżka z s do v ?
Iterable<Integer> pathTo(int v) Zwraca ścieżkę z s do v
(jeśli ścieżka nie istnieje, zwraca nul 1)
Interfejs API do implementacji problemu wyznaczania ścieżek
Konstruktor przyjmuje jako argument źród
łowy wierzchołek s i wyznacza ścieżki z s do p u b lic s t a t ic void m a in (S trin g [] args)
każdego wierzchołka powiązanego z s. Po {
Graph G = new Graph(new In ( a r g s [ 0 ] ) ) ;
utworzeniu obiektu Paths na podstawie źród
in t s = In t e g e r . p a r s e ln t ( a r g s [ l] );
łowego wierzchołka s klient może wykorzystać Paths search = new Paths(G, s );
metodę egzemplarza pathTo() do iterowania fo r ( in t v = 0; v < G.V(); v++)
po wierzchołkach na ścieżce z s do dowolnego {
S td O u t.p rin tfs + " do " + v + ": ")
wierzchołka powiązanego z s. Na razie akcep i f ([Link] T o(v))
tujemy dowolną ścieżkę. Dalej opracujemy im f o r ( i n t x : sea [Link](v))
plementacje znajdujące ścieżki o określonych i f (x == s) S t d O u t . p r i n t ( x ) ;
else S td O u t.p rint("-" + x ) ;
cechach. Klient testowy widoczny po prawej StdO u [Link] ();
stronie przyjmuje graf ze strumienia wejścio
wego i wierzchołek źródłowy z wiersza pole
ceń oraz wyświetla ścieżkę ze źródła do każde
Klient testowy dla implementacji klasy Paths
go powiązanego wierzchołka.
Implementacja a l g o r y t m 4.1 ze strony 548 to oparta na metodzie DFS implemen
tacja ldasy Paths, będąca rozwinięciem wstępnej wersji metody DepthFirstSearch ze
strony 543. W rozwinięciu dodano zmienną egzemplarza w postaci tablicy edgeTo[]
wartości typu i nt. Zmienna ta pełni funkcję szpulki z nicią z metody Tremami i pozwala
znaleźć ścieżkę z powrotem do s z każdego wierzchołka powiązanego z s. Zamiast śle
dzić ścieżkę z bieżącego wierzchołka do początku, program zapamiętuje ścieżkę z każ
dego wierzchołka do punktu wyjścia. W tym celu należy przez ustawienie edgeTo [w] na
v zapamiętać krawędź v-w, która prowadzi do wierzchołka wprzy pierwszym jego napot
kaniu. Oznacza to, że v-w to ostatnia krawędź na znanej
% java Paths tin y C G .txt 0 ścieżce z s do w. Wynikiem wyszukiwania jest drzewo
0 do 0: 0
0 do 1: 0-2-1
o korzeniu w źródłowym wierzchołku. Na prawo od
0 do 2: 0-2 kodu a l g o r y t m u 4.1 narysowano mały przykład. Aby
0 do 3: 0-2-3 odtworzyć ścieżkę z s do dowolnego wierzchołka v, me
0 do 4: 0 -2 -3 -4
toda pathTo() z a l g o r y t m u 4.1 wykorzystuje zmienną
0 do 5: 0 -2 -3 -5
x do przejścia w górę drzewa, ustawiając x na edgeTo [x]
548 R O ZD ZIA Ł 4 Grafy
ALGORYTM 4.1. Przeszukiwanie w głąb w celu znalezienia ścieżek w grafie
public c la s s DepthFirstPaths
{
private boolean[] marked; // Czy wywołano już dfs() dla danego wierzchołka?
private i n t [] edgeTo; // Ostatni wierzchołek na znanej ścieżce
// do wierzchołka,
private final in t s; // Wierzchołek źródłowy.
public De p th FirstP ath s(Graph G, in t s)
{
marked = new boolean[G .V ()];
edgeTo = new i nt [G. V () ];
th is.s = s;
dfs(G, s);
}
private void d f s (Graph G, in t v)
(
marked[v] = true;
fo r (in t w : [Link](v))
i f (¡marked[w])
{
edgeTo[w] = v;
dfs(G, w); 5 5
3 3 5
} 2 2 3 5
} 0 0 2 3 5
Ślad w y w o ła n ia m e to d y p a t h T o (5 )
public boolean hasPathTo(int v)
{ return marked[ v ] ; }
public Iterab le< In te ge r> pathTo(int v)
{
i f (IhasPathTo(v)) return n u ll;
Stack<Integer> path = new S ta c k < In te g e r> ();
fo r (in t x = v; x != s; x = edgeTo[x])
path .pu sh (x);
pa th .p u sh (s);
return path;
}
}
W tym kliencie klasy Graph wykorzystano przeszukiwanie w głąb do znalezienia ścieżek do
wszystkich wierzchołków grafu powiązanych z wierzchołkiem początkowym s. Kod meto
dy DepthFi rstSearch (strona 543) wyróżniono szarym kolorem. W celu zapisania ścieżek
do każdego wierzchołka w klasie przechowywana jest indeksowana wierzchołkami tablica
edgeTo [], w której edgeTo [w] = v oznacza, że v-w to krawędź użyta przy pierwszym przej
ściu do w. Tablica edgeTo [] to reprezentacja drzewa z odnośnikami do rodzica z korzeniem
w s i wszystkimi wierzchołkami powiązanymi z s.
4.1 e Grafy nieskierowane 549
(podobnie jak w algorytmach Union-Find e d g e T o []
w p o d r o z d z i a l e 1 .5 ), co powoduje umiesz- d fs ( O )
czenie na stosie każdego wierzchołka napotka
nego na drodze do s. Ponieważ stos jest zwra
cany jako obiekt typu Iterable, klient może
przejść po ścieżce z s do v.
dfs(2)
sprawdzanie 0
Szczegółowy ślad Na rysunku po prawej stro
nie przedstawiono zawartość tablicy edgeTo[]
bezpośrednio po oznaczeniu każdego wierz
chołka z przykładu (źródłem jest tu wierz
dfs(l)
chołek 0). Zawartość tablic marked[] i adj [] | Sprawdzani
jest taka sama jak w śladzie działania metody ! Sprawdzanie 2
1 Gotowy
DepthFi rstSearch ze strony 545. Takie same są
też: szczegółowy opis wywołań rekurencyjnych
i sprawdzone krawędzie, dlatego pominięto dfs(3)
te elementy śladu. W procesie przeszukiwa
nia w głąb do tablicy edgeTo[] dodawane są
krawędzie 0-2, 2-1, 2-3 i 3-4 (w tej kolejno
ści). Krawędzie te tworzą drzewo o korzeniu
dfs(5)
w wierzchołku źródłowym i zapewniają infor I Sprawdzanie 3
macje potrzebne w metodzie pathTo () do udo | Sprawdzanie 0
5 Gotowy
stępnienia klientowi ścieżki z wierzchołka 0 do
1,2,3, 4 lub 5 w opisany wcześniej sposób.
dfs(4)
I Sprawdzanie 3
k o n s t r u k t o r w klasie DepthFirstPaths różni I Sprawdzanie 2
się tylko kilkoma przypisaniami od konstruk 4 Gotowy
sprawdzanie 2
tora z klasy DepthFi rstSearch, dlatego także tu 3 Gotowy
prawdziwe jest t w i e r d z e n i e a ze strony 543. Sprawdzanie 4
2 Gotowy
Można do tego dodać następujące twierdzenie. Sprawdzanie 1
Sprawdzanie 5
0 Gotowy
Twierdzenie A (ciąg dalszy). Metoda
DFS pozwala udostępnić klientom ścież
kę z danego źródła do dowolnego ozna
czonego wierzchołka w czasie proporcjo
nalnym do długości ścieżki. Ślad przeszukiw ania w g łą b w celu znalezienia
w szystkich ścieżek w ychodzących z 0
Dowód. Przez indukcję na liczbie odwie
dzonych wierzchołków można stwierdzić, że
tablica edgeTo[] w klasie DepthFirstPaths
reprezentuje drzewo, którego korzeniem
jest wierzchołek źródłowy. Metoda path-
To() tworzy ścieżkę w czasie proporcjonal
nym do jej długości.
550 R O ZD ZIA Ł 4 a Grafy
Przeszukiwanie wszerz Ścieżki znalezione przy przeszukiwaniu w głąb zależą
nie tylko od grafu, ale też od reprezentacji danych i natury rekurencji. Często p o
trzebne jest rozwiązanie następującego problemu.
Najkrótsze ścieżki z jednego źródła. Dla grafu i źródłowego wierzchołka s należy
zapewnić obsługę odpowiedzi na pytania w postaci: Czy istnieje ścieżka z s do da
nego wierzchołka v? Jeśli tak, trzeba znaleźć najkrótszą taką ścieżkę (o minimalnej
liczbie krawędzi).
Klasyczna m etoda wykonywania tego zadania, przeszukiwanie wszerz (ang. breadth-
first search — BFS), jest też podstawą wielu algorytmów przetwarzania grafów, dlate
go omawiamy ją szczegółowo w tym podrozdziale. M etoda DFS nie jest zbyt pom oc
na przy rozwiązywaniu omawianego problemu, ponieważ kolejność przechodzenia
po grafie nie jest w niej związana z wyszukiwaniem najkrótszych ścieżek.
Natomiast metoda BFS jest do tego przeznaczona. Aby znaleźć najkrótszą
ścieżkę z s do v, należy zacząć w s i sprawdzić, czy v znajduje się wśród
wierzchołków, do których m ożna dotrzeć poprzez jedną krawędź, następ
nie poszukać v wśród wierzchołków dostępnych z s poprzez dwie krawę
dzie itd. Metoda DFS odpowiada eksplorowaniu labiryntu przez jednego
człowieka. Metoda BFS przypomina grupę poszukiwaczy, którzy wyru
szają we wszystkich kierunkach, przy czym każda osoba rozwija własną
nić. Kiedy trzeba zbadać więcej niż jedną alejkę, poszukiwacze rozdzielają
się, aby to zrobić. Kiedy dwie grupy się spotykają, łączą siły (używając nici
trzymanej przez grupę osób, które pierwsze dotarły do danego miejsca).
W programie po dojściu przy przeszukiwaniu grafu do punktu, w któ
rym trzeba przejść dalej więcej niż jedną krawędzią, należy wybrać jedną
z nich, a drugą zapisać w celu późniejszej eksploracji. W metodzie DFS
labiryntu wszerz stosujemy do tego stos (zarządzany przez system na potrzeby przeszu
kiwania rekurencyjnego). Zastosowanie charakterystycznej dla stosu
reguły LIFO odpowiada eksplorowaniu bliskich alejek w labiryncie. Spośród alejek
do sprawdzenia wybieramy tę ostatnio napotkaną. W metodzie BFS wierzchołki są
sprawdzane w kolejności wyznaczanej przez odległość od wierzchołka źródłowego.
Okazuje się, że można łatwo wymusić tę kolejność. Wystarczy wykorzystać kolejkę
(reguła FIFO) zamiast stosu (reguła LIFO). Spośród alejek do sprawdzenia trzeba
wybrać tę napotkaną najdawniej.
Im plem entacja a l g o r y t m 4.2 ze strony 552 to implementacja m etody BFS.
Rozwiązanie oparte jest na przechowywaniu kolejki wszystkich oznaczonych wierz
chołków, których listy sąsiedztwa jeszcze nie sprawdzono. Należy umieścić źródłowy
wierzchołek w kolejce, a potem — do m om entu opróżnienia kolejki — wykonywać
następujące kroki:
■ Pobierać z kolejki następny wierzchołek v i oznaczać go.
0 Umieszczać w kolejce wszystkie nieoznaczone wierzchołki sąsiadujące z v.
4.1 □ Grafy nieskierowane 551
Metoda b f s () w a l g o r y t m i e 4.2 nie jest re- e d g e T o []
O
kurencyjna. Zamiast niejawnego stosu tworzo
nego w trakcie rekurencji, bezpośrednio zasto
sowano kolejkę. Wynikiem przeszukiwania,
tak jak w metodzie DFS, jest tablica edgeTo[].
Efekt przeszukiwania wszerz w celu znalezienia
Tablica ta to oparte na odnośnikach do rodzica wszystkich ścieżek z wierzchołka 0
drzewo o korzeniu s, wyznaczające najkrótsze
ścieżki z s do każdego powiązanego
queue m arked [ ] e d g e T o [] a d j []
z nim wierzchołka. Ścieżki dla klien
tów można tworzyć za pomocą tej 2) 0 T 0
0 1
T 1
2
1
2
1 1
2
samej implementacji m etody path- 3 3
3 1
To(), którą wykorzystano dla m eto 4 4
5
4 i
5 1
4) 5
dy DFS W A LG O R Y T M IE 4 . 1 .
Na rysunku po prawej stronie
przedstawiono krok po kroku prze 2) 0 T 0 o i
T 1 0
szukiwanie przykładowego grafu
I 1
2 T 2 0
1 |
2 i
metodą BFS. Pokazano zawartość i
3
4
3
4
3
4 i
D 5 T 5 0 5 i
struktur danych na początku każ
dej iteracji pętli. Wierzchołek 0 jest
umieszczany w kolejce, a następnie 2) 0 T 0 0
1 1 T 1 0 1 0 2
w pętli program kończy wyszukiwa 2 T 2
0 2 1
I0 1
L 3 T 3 2 3 5 4
nie w następujący sposób: II 4 T 4 2 4 i
4) 5 T 5 0 5 i
n Usuwa 0 z kolejki i umieszcza
w kolejce sąsiednie wierzchoł
5 (C I ----------- ^ { 2
ki 2,1 i 5; oznacza każdy z nich 3
) 0 1T
1 T
0
1 0
0 2 1 5
1 0 2
4 2 T 2 0 2 0 1 3
i dla każdego ustawia wpis C l) Jy I
3 jT 3 2 3 5 4 2
w tablicy e dg eT o[] na 0. _____ A ' 4 T 4 2 4 3 2
Q ) 5 IT 5 0 5 3 0
n Usuwa 2 z kolejki, sprawdza
sąsiednie wierzchołki 0 i 1 (są
) 0 T 0 0 2 1 5
oznaczone) i umieszcza w ko ' 1 T 1 0 1 0 2
2 T 2 0 2 0 13
lejce sąsiednie wierzchołki 3 i 4; 3 T 3 2 3 5 4 2
4 T 4 2 4 3 2
oznacza te ostatnie i ustawia U ) 5 T 5 0 5 3 0
dla każdego z nich wpis w tab
licy e dgeT o[] na 2. [2 ) 0 T 0 0 2 1 5
° Usuwa 1 z kolejki i sprawdza i T 1 0 1 0 2
2 T 2 0 2 0 1 3
sąsiednie wierzchołki 0 i 2 3 T 3 2 3 5 4 2
4 T 4 2 4 3 2
(są oznaczone). 2
) 5 T 5 0 5 3 0
n Usuwa 5 z kolejki i sprawdza
Ślad przeszukiwania wszerz w celu znalezienia
sąsiednie wierzchołki 3 i 0 wszystkich ścieżek z wierzchołka O
(są oznaczone).
n Usuwa 3 z kolejki i sprawdza
sąsiednie wierzchołki 5, 4 i 2 (są oznaczone).
0 Usuwa 4 z kolejki i sprawdza sąsiednie wierzchołki 3 i 2 (są oznaczone).
552 R O ZD ZIA Ł 4 Grafy
ALGORYTM 4.2. Przeszukiwanie w szerz w celu znalezienia ścieżek w grafie
p u b lic c la s s B re a d th F irstP a th s
{
p riv a te boolean[] marked; // Czy znana j e s t n a jk ró tsz a śc ie ż k a do tego
// w ierzch o łka ?
p riv a te in t [ ] edgeTo; // O statni w ierzchołek na znanej śc ie ż c e do
// w ierzchołka,
p riv a te final in t s; // W ierzchołek źródłowy.
p u b lic B re a d th F irstP a th s(G ra p h G, in t s)
{
marked = new bo o lean[G.V ( ) ] ;
edgeTo = new i nt [G. V ()] ;
t h is . s = s;
bfs(G , s);
}
p riv a te void bfs(G raph G, in t s)
{
Queue<Integer> queue = new Q u e u e <In te ge r> ();
marked[s] = true; // Oznaczanie w ierzch ołka źródłowego
[Link](s); // i um ieszczanie go w kolejce,
w hile (¡q ueu [Link] m ptyf))
{
in t v = [Link](); // Usuwanie następnego wierzchołka z k o le jk i,
fo r ( in t w : G .a d j(v ))
i f (¡m arked[w]) // Dla każdego nieoznaczonego są sie d n ie go
// w ierzchołka:
{
edgeTo[w] = v; // zapisujem y o s ta t n ią krawędź na
// n a jk ró tsz e j śc ie ż c e ,
marked[w] = tru e ; // oznaczamy w ierzchołek, ponieważ ścieżka
// j e s t znana,
[Link](w); // i dodajemy w ierzchołek do k o le jk i.
}
}
}
p u b lic boolean h a sP a thT o (int v)
{ return m arked[v]; }
p u b lic Ite ra b le < In te g e r> p a th T o (in t v)
// Ten sam kod, co w metodzie DFS (stro n a 548).
}
W tym kliencie klasy Graph w ykorzystano przeszukiwanie wszerz do znalezienia w grafie
ścieżek o najmniejszej liczbie krawędzi, wychodzących ze źródłowego w ierzchołka s poda
nego w konstruktorze. M etoda b fs () oznacza wszystkie wierzchołki powiązane z s, dlatego
ldienty m ogą używać m etody hasPathTo() w celu ustalenia, czy dany wierzchołek v jest
pow iązany z s, oraz m etody pathTo () do pobierania ścieżki m iędzy s a v, cechującej się tym,
że żadna inna ścieżka m iędzy tym i wierzchołkam i nie obejmuje mniejszej liczby krawędzi.
4.1 □ Grafy nieskierowane 553
W tym przykładzie tablica edgeTo [] zostaje zapełniona po drugim kroku. Tu, tak jak
w metodzie DFS, po oznaczeniu wszystkich wierzchołków dalsze operacje to tylko
sprawdzanie krawędzi do już oznaczonych wierzchołków.
Twierdzenie B. Dla dowolnego wierzchołka v dostępnego z wierzchołka s m e
toda BFS oblicza najkrótszą ścieżkę między s a v (żadna inna ścieżka między
tymi wierzchołkami nie obejmuje mniejszej liczby krawędzi).
Dowód. Można łatwo udowodnić przez indukcję, że kolejka zawsze obejmuje
zero lub więcej wierzchołków oddalonych o k od wierzchołka źródłowego, a tak
że zero lub więcej wierzchołków o odległości k+1 od źródłowego dla pewnej licz
by całkowitej k (zaczynamy od k równego 0). Z tej cechy wynika, że wierzchołki
trafiają do kolejki i opuszczają ją w kolejności zgodnej z odległością od s. Kiedy
wierzchołek v trafi do kolejki, żadna krótsza ścieżka do v nie zostanie znaleziona
przed usunięciem wierzchołka z kolejki i żadna ścieżka znaleziona później nie
może być krótsza niż długość ścieżki w drzewie v.
Twierdzenie B (ciąg dalszy). M etoda BFS działa w czasie proporcjonalnym do
V+E (dla najgorszego przypadku).
Dowód. Zgodnie z t w i e r d z e n i e m a (strona 543) m etoda BFS oznacza wierz
chołki powiązane z s w czasie proporcjonalnym do sumy ich stopni. Jeśli graf jest
spójny, suma ta jest sumą stopni wszystkich wierzchołków (2 E).
Zauważmy, że m ożna użyć metody BFS do zaimplementowania interfejsu API kla
sy Search, zaimplementowanego wcześniej za pom ocą metody DFS. Potrzebna jest
tylko możliwość sprawdzenia wszystkich wierzchołków i krawędzi powiązanych
z wierzchołkiem źródłowym.
Jak wspomnieliśmy na początku, m etody DFS i BFS to pierwsze z kilku omawia
nych przykładów ogólnego podejścia do przeszukiwania grafów. Należy umieścić
źródłowy wierzchołek w strukturze danych, a następnie do czasu opróżnienia struk
tury wykonywać poniższe kroki:
° Pobierać następny wierzchołek v ze struktury danych i oznaczać go.
0 Umieszczać w strukturze danych wszystkie nieoznaczone wierzchołki sąsiadu
jące Z V.
Algorytmy różnią się jedynie regułą stosowaną do %jaya BreadthFirstPaths [Link] „
pobierania następnego wierzchołka ze struktury da- o do o: o
nych (w metodzie BFS jest to najdawniej dodany, a 0 do 1: 0-1
0 do 2: 0-2
w metodzie DFS — ostatnio dodany wierzchołek).
0 do 3: 0 - 2 - 3
Różnica ta prowadzi do zupełnie innego podejścia o do 4 : 0 - 2 - 4
do grafu, choć wszystkie wierzchołki i krawędzie o do 5: 0-5
powiązane z wierzchołkiem źródłowym są spraw
dzane niezależnie od użytej reguły.
554 RO ZD ZIA Ł 4 □ Grafy
N A RYSU N KACH PO OBU STRO NACH
(widać na nich działanie m etod DFS
i BFS dla przykładowego grafu z pli
ku [Link]) wyraźnie pokazano
różnice między ścieżkami znajdo
wanymi w obu podejściach. Metoda
DFS „zagłębia” się w graf i przecho
wuje stos punktów, w których ścieżki
się rozgałęziają. M etoda BFS działa
przez rozprzestrzenianie się po gra
fie; wykorzystano tu kolejkę do zapa
miętywania „frontu” odwiedzonych
wierzchołków. M etoda DFS eksploru
je graf, wyszukując nowe wierzchołki
znacznie oddalone od punktu wyjścia.
Bliższe wierzchołki są sprawdzane
tylko po napotkaniu ślepego zaułka.
Metoda BFS w pełni pokrywa obszar
blisko punktu wyjścia i przechodzi
dalej dopiero po zbadaniu wszystkich
pobliskich lokalizacji. Ścieżki w m e
todzie DFS są zwykle długie i kręte,
natomiast w metodzie BFS — krótkie
i proste. W zależności od aplikacji
pożądane może być jedno lub drugie
podejście (a czasem cechy ścieżek nie
mają znaczenia). W p o d r o z d z i a l e
4.4 omawiamy inne implementacje
interfejsu API klasy Paths, wyszuku
jące ścieżki o innych cechach.
100%
100%
S z u k a n ie ś c ie ż e k m e to d ą S z u k a n ie n a jk ró ts z y c h ście ż e k
DFS (250 w ie rz ch o łk ó w ) m e to d ą BFS (250 w ie rz ch o łk ó w )
4.1 □ Grafy nieskierowane 555
Spójne składowe Następnym bezpośrednim zastosowaniem przeszukiwania w głąb
jest znajdowanie spójnych składowych grafu. W p o d r o z d z i a l e 1.5 (strona 228) wspo
mnieliśmy, że „jest powiązany z” to relacja równoważności, dzieląca wierzchołki na klasy
równoważności (spójne składowe). Na potrzeby tego typowego zadania z obszaru prze
twarzania grafów definiujemy przedstawiony poniżej interfejs API.
p ub lic c la s s CC
CC(Graph G) Konstruktor ze wstępnym przetwarzaniem
boolean connected(int v, in t w) Czy v i w są powiązane?
in t count() Zwraca liczbę spójnych składowych
in t id ( in t v) Identyfikator składowej obejmującej v
(zprzedziału od 0 do c o u n t () -l)
Interfejs API do wyznaczania spójnych składowych
Metoda id () jest przeznaczona dla klientów do indeksowania tablicy za pomocą
składowych, tak jak w kliencie testowym poniżej, który wczytuje graf, a następnie
wyświetla liczbę spójnych składowych i wierzchołki z każdej składowej (po jednej
składowej na wiersz). Klient buduje w tym celu tablicę obiektów Bag i korzysta z iden
tyfikatora składowej każdego kom ponentu jako indeksu do tej tablicy w celu dodania
wierzchołka do odpowiedniego obiektu Bag. Jest to wzorcowy klient dla typowych
sytuacji, w których chcemy niezależnie przetwarzać spójne składowe.
Implementacja W implementacji klasy CC ( a l g o r y t m 4.3 na następnej stronie)
wykorzystano tablicę marked [] do znalezienia wierzchołka służącego jako punkt
wyjścia do przeszukiwania w głąb każdej
składowej. Pierwsze wywołanie rekuren- p u b lic s t a t ic void m a in (S trin g [] args)
{
cyjnej m etody d fs() dotyczy wierzchoł Graph G = new Graph(new In ( a r g s [ 0 ] ) ) ;
ka 0, co powoduje oznaczenie wszystkich CC cc = new CC(G );
wierzchołków powiązanych z 0. Następnie
in t M = c c .c o u n t Q ;
w pętli fo r konstruktor wyszukuje nieozna S t d O u t . p r in t ln ( " 1 iczba składowych: " + M);
czony wierzchołek i wywołuje rekurencyj-
ną metodę df s () w celu oznaczenia wszyst Bag<Integer>[] components;
components = (B a g < In te g e r> []) new Bag[M j;
kich powiązanych z nim wierzchołków.
fo r ( in t i = 0 ; i < M ; i++)
Kod przechowuje też indeksowaną wierz com ponents[i] = new B a g < In te g e r> ();
chołkami tablicę i d [ ] , która łączy tę samą fo r (in t v = 0 ; v < G .V (); v++)
c o m p o n e n ts[c c .id (v )].a d d (v );
wartość typu i nt z każdym wierzchołkiem
f o r (in t i = 0 ; i < M; i++)
z poszczególnych składowych. Tablica ta
1
upraszcza implementację metody connec- f o r (in t v: com ponents[i])
ted ( ) , która działa w tald sam sposób, jak Std O u t.p rin t(v + " " ) ;
Std O u t.p rin tl n ( ) ;
metoda connected() z p o d r o z d z i a ł u 1.5
(wystarczy sprawdzić, czy identyfikatory 1
są sobie równe). Tu identyfikator 0 jest
556 RO ZD ZIA Ł 4 Grafy
ALGORYTM 4.3. Przeszukiwanie w głąb w celu znalezienia spójnych składowych w grafie
public c la s s CC
{ % more tin y G .tx t
p rivate boolean[] marked;
13 v e rt ic e s , 13 edges
p rivate i n t [ ] id; 0: 6 2 1 5
p rivate in t count; 1: 0
2: 0
public CC(Graph G) 3: 5 4
{ 4: 5 6 3
marked = new b oolean[G.V ()]; 5: 3 4 0
id = new in t [ G .V( ) ] ; 6: 0 4
fo r (in t s = 0; s < G.V(); s++) 7: 8
i f ( ¡marked[s]) 8: 7
9: 11 10 12
{
10: 9
dfs(G, s );
11: 9 12
count++;
12: 11 9
}
} % java CC t in y G .tx t
lic z b a składowych: 3
p rivate void dfs(Graph G, in t v) 6 5 4 3 2 1 0
{ 8 7
marked[v] = true; 12 11 10 9
id[v] = count;
fo r (in t w : [Link] (v ) )
i f (!marked[w])
dfs(G, w);
}
public boolean connected(int v, in t w)
( return i d [v] == i d [w]; }
public in t id (in t v)
{ return i d [ v ] ; }
public in t countQ
( return count; }
Ten klient klasy Graph umożliwia swoim klientom niezależne przetwarzanie spójnych skła
dowych grafu. Kod metody DepthFirstSearch (strona 543) pokazano po lewej stronie
w kolorze szarym. Przetwarzanie oparte jest na indeksowanej wierzchołkami tablicy i d [],
takiej że id[v] ma wartość i, jeśli v znajduje się w i-tej przetwarzanej spójnej składowej.
Konstruktor znajduje nieoznaczony wierzchołek i wywołuje rekurencyjną metodę dfs(),
aby oznaczyć oraz zidentyfikować wszystkie wierzchołki powiązane ze znalezionym. Proces
ten trwa do czasu oznaczenia i zidentyfikowania wszystkich wierzchołków. Implementacje
metod egzemplarza connected(), i d () ic ou n t() są oczywiste.
4.1 e Grafy nieskierowane 557
t in y G . t x t
m a r k e d [] id []
B 9101112
dfsCO) T
d f s ( 6) T
S p r a w d z a n ie 0
d fs(4 ) T T T 0 0
d fs(5 ) T T T T 0 0 0
d fs(3 ) T T T T T 0 0 0 0
S p r a w d z a n ie 5
S p r a w d z a n ie 4
3 G otow y
S p r a w d z a n ie 4
S p r a w d z a n ie 0
5 G otow y
S p r a w d z a n ie 6
S p r a w d z a n ie 3
4 G otow y
6 G otow y
d f s ( 2) T T T T T T 0 0 0 0 0 0
[ S p r a w d z a n ie 0
2 G otow y
d fs (l) T T T T T T T 00 00 000
| S p r a w d z a n ie 0
I G otow y
S p r a w d z a n ie 5
0 G otow y
d fs(7 ) T T T T T T T T 0 0 0 0 0 0 0
d f s ( 8) T T T T T T T T T 0 0 0 0 0 0 0 1
| S p r a w d z a n ie 7
8 G otow y
7 G otow y
d fs(9 ) T T T T T T T T T T 0 0 0 0 0 0 1 2
cif s ( 1 1 ) T T T T T T T T T T 0 0 0 0 0 0 12 2
S p r a w d z a n ie 9
d f s ( 12 ) T T T T T T T T T T TT 0 0 0 0 0 0 12 2 2
S p r a w d z a n ie 11
S p r a w d z a n ie 9
12 G otow y
I I G otow y
d f s ( 10 ) T T T T T T T T T T T T T 0 0 0 0 0 0 1 2 2 2 2
| S p r a w d z a n ie 9
10 G otow y
S p r a w d z a n ie 12
9 G otow y
Ślad przeszukiwania w głąb w celu znalezienia spójnych składowych
558 R O ZD ZIA Ł 4 * Grafy
przypisywany do wszystkich wierzchołków z pierwszej przetwarzanej składowej,
1 jest przypisywany do wszystkich wierzchołków z drugiej przetwarzanej składowej
itd. Wszystkie identyfikatory zawierają się więc w przedziale od 0 do count () -1, jak
określono to w interfejsie API. Ta konwencja umożliwia stosowanie tablic indekso
wanych składowymi, tak jak w kliencie testowym ze strony 555.
Twierdzenie C. W metodzie DFS czas i pamięć potrzebne na wstępne przetwa
rzanie są proporcjonalne do V+E, jeśli możliwe ma być odpowiadanie w stałym
czasie na zapytania dotyczące połączeń w grafach.
Dowód. Wynika bezpośrednio z kodu. Każdy element na liście sąsiedztwa jest
sprawdzany dokładnie raz, a istnieje 2 E takich elementów (po dwa na każdą kra
wędź). Metody egzemplarza sprawdzają lub zwracają jedną albo dwie zmienne
egzemplarza.
A lgorytm y U nion-Find Jak wydajne jest rozwiązanie problemu określania po
łączeń oparte na metodzie DFS (klasa CC) w porównaniu z techniką Union-Find
z r o z d z i a ł u i.? Teoretycznie m etoda DFS jest szybsza, ponieważ zapewnia stały
czas wykonania, a technika Union-Find tego nie gwarantuje. W praktyce różnicę
m ożna pominąć, a technika Union-Find bywa szybsza, ponieważ nie trzeba w niej
budować pełnej reprezentacji grafu. Co ważniejsze, technika Union-Find działa na
bieżąco (w dowolnym momencie, nawet w czasie dodawania krawędzi, m ożna w cza
sie bliskim stałemu sprawdzić, czy dwa wierzchołki są połączone), natomiast rozwią
zanie oparte na metodzie DFS musi wstępnie przetworzyć graf. Dlatego czasem lepiej
użyć techniki Union-Find — na przykład kiedy jedynym zadaniem jest określenie,
czy połączenie istnieje, lub kiedy duża liczba zapytań jest wymieszana z instrukcjami
dodawania krawędzi. Metoda DFS może okazać się bardziej odpowiednia w typie
ADT dla grafów, ponieważ wydajnie wykorzystuje istniejącą infrastrukturę.
m etoda d fs słu ży d o podstawowych problemów. Jest to proste po
r o z w ią z y w a n ia
dejście, a relcurencja wyznacza sposób myślenia o przetwarzaniu i rozwijaniu zwięzłych
rozwiązań problemów z obszaru przetwarzania grafów. W tabeli na następnej stronie po
kazano dwa dodatkowe przykłady związane z rozwiązywaniem poniższych problemów.
W ykrywanie cykli. Odpowiadanie na pytanie: Czy dany graf jest acykliczny?
W ierzchołki w dwóch kolorach. Odpowiadanie na pytanie: Czy do wierzchołków
danego grafu można przypisać jeden z dwóch kolorów w taki sposób, że żadna kra
wędź nie łączy wierzchołków o tym samym kolorze? Równoznaczne jest pytanie:
Czy graf jest dwudzielny?
Tu, jak zwykle przy stosowaniu metody DFS, za prostym kodem kryje się bardziej
skomplikowane przetwarzanie. Dlatego warto przeanalizować przykłady, prześledzić
ich działanie dla małych przykładowych grafów oraz rozwinąć kod o sprawdzanie
cykli i kolorowanie (pozostawiamy to jako ćwiczenia).
4.1 b Grafy nieskierowane 559
Zadanie Implementacja
p u b lic c la s s Cycle
i
p riv a te boolean[] marked;
p riv a te boolean hasCycle;
p u b lic Cycle(Graph G)
(
marked = new b o o le a n [G .V ()];
f o r ( in t s = 0; s < G .V (); s++)
Czy graf G i f (!m arked[s])
dfs(G , s, s ) ;
jest acykliczny?
Zakładamy, że nie }
istnieję pętle własne p riv a te void dfs(G raph G, in t v, in t u)
ani krawędzie {
równoległe. marked[v] = true ;
f o r (in t w : G .a d j(v ))
i f ( ¡marked[w])
dfs(G , w, v ) ;
e lse i f (w != u) hasCycle = true;
p u b lic boolean hasCycle()
{ return hasCycle; }
}
p u b lic c la s s TwoColor
{
p riv a te boolean[] marked;
p riv a te boolean[] c o lo r;
p riv a te boolean isTw oColorable = true;
p u b lic TwoColor(Graph G)
(
marked = new boolean [G.V() ] ;
co lo r = new boolean[G .V( ) ] ;
fo r (in t s = 0; s < G.V(); s++)
i f (!m arked[s])
dfs(G , s ) ;
Czy graf }
jest dwudzielny
(czy można przypisać p riv a te void dfs(G raph G, in t v)
mu dwa kolory)? {
marked[v] = true ;
f o r ( in t w : G .a d j(v ))
i f ( ¡marked[w])
(
color[w ] = ¡c o lo r fv ];
dfs(G , w );
1
e lse i f (color[w ] == c o lo r[ v ] ) isTw oColorable = fa ls e ;
p u b lic boolean i s B i p a r t i t ę ()
{ return isTw oColorable; }
}
Więcej przykładów przetwarzania grafów metodą DFS
560 RO ZD ZIA Ł 4 o Grafy
Grafy symboli W typowych zastosowaniach przetwarzane są grafy zdefiniowane
w plikach lub na stronach WWW. Zwykle do definiowania i wskazywania wierzchoł
ków służą łańcuchy znaków, a nie liczby całkowite. Aby uwzględnić takie sytuacje,
zdefiniujmy format wejściowy o następujących cechach:
■ Nazwy wierzchołków to łańcuchy znaków.
■ Nazwy wierzchołków rozdziela określony ogranicznik (pozwala to na używanie
odstępów w nazwach).
■ Każdy wiersz reprezentuje zbiór krawędzi — pierwszy wierzchołek w wierszu
powiązany jest z wszystkimi pozostałymi wierzchołkami z tego wiersza.
■ Liczba wierzchołków, V, i liczba krawędzi, E, są wyznaczane pośrednio.
Poniżej pokazano krótki przykład — plik [Link], który reprezentuje model małego
systemu transportowego. Wierzchołki są tu kodami lotnisk w Stanach Zjednoczonych,
a łączące je krawędzie to połączenia lotnicze między wierzchołkami. Plik jest pro-
stą listą krawędzi. Na następnej stronie pokazano
ro u te [Link] t
większy przykład, oparty na pliku [Link] z ser
JFK MCO Vi Enie są bezpośrednio podane
ORD DEN
wisu IMDB, przedstawiony w p o d r o z d z i a l e 3 . 5 .
ORD HOU Przypomnijmy, że plik składa się z wierszy obej
DFW PHX
mujących tytuł filmu i listę wykonawców. W kon
JFK ATL
ORD DFW tekście przetwarzania grafów można traktować plik
ORD PHX jak graf z filmami i aktorami jako wierzchołkami,
ATL HOU
DEN PHX przy czym każdy wiersz to lista sąsiedztwa z krawę
PHX LAX dziami łączącymi film z wykonawcami. Zauważmy,
JFK ORD
DEN LAS że jest to graf dwudzielny. Nie istnieją krawędzie łą
czące aktorów z aktorami lub filmy z filmami.
Interfejs A P I Pokazany poniżej interfejs API okre
ATL MCO
HOU MCO
śla ldienta klasy Graph, umożliwiającego natychmia
LAS PHX stowe zastosowanie metod przetwarzania grafów
Przykładowy graf symboli (lista krawędzi) dla grafów wyznaczanych przez opisane pliki.
p u b lic c la s s SymbolGraph
Symbol Graph (S tr in g filename, S t rin g delim) Tworzy g raf określony w pliku
filename, używając ogranicznika
del im do rozdzielania nazw
wierzchołków
boolean c o n ta in s (S t r in g key) Czy key to wierzchołek?
in t in d e x (S t r in g key) Zwraca indeks powiązany z key
S t r in g name ( in t v) Zwraca klucz powiązany
z indeksem v
Graph G() Używany obiekt Graph
Interfejs API dla grafów z sym bolicznym i nazwam i wierzchołków
4.1 b Grafy nieskierowane 561
( P a t r ic k D ia l M
— ^ A lle n f o r M urder
M ~V _ L A
Enigma \ T 1 Tk . / se rr^ tta \
/ JA Kate
<
A Wi 1 son
Ete rnal su n sh in e
lbert J C Shane
/ \N
o f the S p o t le s s
Mind
7 n
I/ T— T
raovi e s .t x t ______ y f n je Sq bezpośrednio po d a n e
Tin Men (1987)/DeBoy, David/Blumenfeld, Alan/... /Geppi, Cindy/Hershey, Barbara...
Tirez sur 1e pianistę C1960)/Heymann, Claude/.../Berger, Nicole (I)...
Titanic (1997)/Mazin, Stan/...Dicaprio, Leonardo/..,/winslet, Kate/...
Titus (1999)/weisskopf, Hermann/Rhys, Matthew/. . ,/MCEwan, Geraldine Ogranicznik to,,/"
To Be or Not to Be (1942)/verebes, Erno (I)/.../Lombard, Carole (I)... /
To Be or Not to Be (1983)/.../Brooks, Mel (I)/.../Bancroft, Anne/... f
To Catch a Thief (1955)/Paris, Manuel/.../Grant, Cary/.../Kelly, Grace/...
To Die For (1995)/Smith, Kurtwood/.../Kid man, Nicole/.../ Tucci, Maria...
Film W ykonaw cy
Przykładowy graf symboli (listy sąsiedztwa)
562 RO ZD ZIA Ł 4 a Grafy
p u b lic s t a t ic void m a in (S trin g [] args) Podany interfejs API obejmuje kon
{ struktor do wczytywania i tworzenia
S t rin g filename = a rg s [0 ]; grafu oraz m etody klienckie name()
S t rin g delim = a r g s [1];
Symbol Graph sg = new Symbol Graph(filename del im );
i index() do przekształcania nazw
wierzchołków między łańcuchami
Graph G = s g .G(); znaków ze strum ienia wejściowego
a indeksami całkowitoliczbowymi
w hile (S td ln .h a sN e x tL in e O )
{ używanymi w m etodach przetwarza
S t rin g source = S t d In . r e a d L in e (); nia grafów.
f o r ( in t w : G .a d j(sg .in d e x (so u rc e )))
S t d O u t.p rin t ln (" " + [Link](w)); K lie n t te s to w y Klient testowy wi
doczny po lewej stronie tworzy graf
na podstawie pliku o nazwie poda
Klient testowy dla interfejsu API grafów symboli
nej jako pierwszy argument wier
sza poleceń (używa przy tym ogra
nicznika podanego jako drugi argument). Następnie
% java Symbol Graph ro u te s .tx t
klient przyjmuje zapytania ze standardowego wejścia.
JFK
ORD Użytkownik określa nazwę wierzchołka i otrzymuje li
ATL stę sąsiadujących z nim wierzchołków. Klient udostęp
MCO nia przydatny mechanizm indeksu odwrotnego, opisa
LAX
LAS
ny w p o d r o z d z i a l e 3 . 5 . W kontekście pliku [Link]
PHX można wpisać kod lotniska, aby znaleźć bezpośrednie
połączenia z nim. Informacje te nie są bezpośrednio
dostępne w pliku z danymi. W przypadku pliku movies.
% java Symbol Graph m [Link] 7 " txt m ożna podać nazwisko aktora, aby otrzymać listę
Tin Men (1987) filmów z bazy, w których dana osoba wystąpiła. Można
DeBoy, David
też wpisać tytuł filmu w celu uzyskania listy występu
Blumenfeld, Alan
jących w nim wykonawców. Wyświetlenie listy akto
G eppi, Cindy rów na podstawie tytułu jest niczym więcej jak powtó
Hershey, Barbara
rzeniem odpowiedniego wiersza z pliku wejściowego.
Bacon, Kevin Jednak zwracanie listy filmów, w których wystąpił po
M y stic R iv e r (2003) dany wykonawca, wymaga indeksu odwrotnego. Choć
Frid ay the 13th (1980) baza danych łączy filmy z wykonawcami, w modelu
F la t lin e r s (1990)
Few Good Men, A (1992)
grafu dwudzielnego aktorzy są też powiązani z filma
mi. Model ten automatycznie spełnia funkcję indek
su odwrotnego i — jak się okaże — stanowi podstawę
bardziej zaawansowanego przetwarzania.
4.1 u Grafy nieskierowane 563
skuteczne w każdej omawianej metodzie prze
o p is a n e p o d e jś c ie je s t , o c z y w iś c ie ,
twarzania grafów. Każdy klient może użyć metody i ndex (), aby przekształcić nazwę
wierzchołka na indeks używany przy przetwarzaniu grafu, i m etody name() w celu
przekształcenia indeksu na nazwę stosowaną w aplikacji.
Implementacja Pełną implementację klasy Symbol Graph przedstawiono na stronie 564.
Budowane są tam trzy struktury danych:
a tablica symboli s t z kluczami typu S tring (nazwami wierzchołków) i wartoś
ciami typu in t (indeksami);
° tablica keys[], która pełni funkcję indeksu odwrotnego i udostępnia nazwę
wierzchołka dla każdego indeksu całkowitoliczbowego;
o oparty na indeksach obiekt Graph g, służący do wskazywania wierzchołków.
Klasa Symbol Graph musi dwukrotnie przejść po danych w celu zbudowania wymie
nionych struktur. Wynika to głównie z tego, że do utworzenia obiektu Graph niezbęd
na jest liczba wierzchołków (V). W typowych praktycznych zastosowaniach utrzy
mywanie wartości V i E w pliku definiującym graf (wymagał tego konstruktor Graph
z początku podrozdziału) jest niewygodne. Przy korzystaniu z klasy Symbol Graph
można używać plików w rodzaju [Link] i [Link] oraz dodawać lub usuwać
elementy bez uwzględniania liczby różnych nazw.
Tablica sym boli Indeks o d w ro tn y Graf nieskierow any
ST <S t r i n g , l n t e g e r > st String[] ke y s Graph G
S tr u k t u r y d a n y c h w g ra fie s y m b o li
564 RO ZDZIAŁ 4 Grafy
Typ danych dla grafu symboli
public c la s s Symbol Graph
{
private ST<String, Integer> st; // Łańcuch znaków -> indeks,
private S t r i n g [ ] keys; // Indeks -> łańcuch znaków,
private Graph G; // Graf.
public Symbol Graph(String stream, S trin g sp)
{
st = new ST<String, In teg er> ();
In in = new In(stream); // Pierwszy przebieg polega na
while ([Link] extLine()) // tworzeniu indeksu
{
String[] a = in.readl_ine() . s p l i t ( s p ) ; // przez wczytywanie łańcuchów
fo r (in t i = 0; i < [Link]; i++) // znaków w celu powiązania
i f (!st.c o n ta in s(a [i])) // każdego specyficznego
// łańcucha
st.p u t(a [i], s t . s iz e O ) ; // z indeksem.
}
keys = new S t r i n g [ s t . s i z e ( ) ] ; // Indeks odwrotny do pobierania
f o r (S t rin g name : s t . k e y s Q ) // kluczy w postaci łańcuchów znaków
keys[[Link](name)] = name; // je s t ta b licą .
G = new G r a p h ( s t . s i z e ( ) ) ;
in = new In(stream); // Drugi przebieg,
while ([Link] extLine()) // Tworzenie grafu
{
S t r in g [] a = in .readLine() . s p l i t ( s p ) ; // przez łączenie
in t v = s t . g e t ( a [ 0 ] ); // pierwszego wierzchołka
fo r (in t i = 1; i < [Link]; i++) // z każdego wiersza
// z wszystkimi
[Link](v, s t .g e t ( a [ i ] ) ) ; // pozostałymi wierzchołkami.
}
public boolean co n t a in s (S t rin g s) ( return s t . c o n t a i n s ( s ) ; }
public in t in d e x(S trin g s) ( return s t . g e t ( s ) ; }
public S t r in g name(int v) { return keys[v]; }
public Graph G() ( return G; }
Ten klient klasy Graph umożliwia klientom definiowanie grafów za pomocą łańcuchów zna
ków określających nazwy wierzchołków zamiast przy użyciu indeksów całkowitoliczbowych.
Klient przechowuje zmienne egzemplarza — s t (tablicę symboli łączącą nazwy z indeksami),
keys (tablicę łączącą indeksy z nazwami) i G (graf, gdzie nazwy wierzchołków to liczby cał
kowite). W celu utworzenia tych struktur klient wykonuje dwa przebiegi po definicji grafu
(każdy wiersz zawiera łańcuch znaków i listę sąsiadujących łańcuchów, rozdzielonych ogra
nicznikiem sp).
4.1 n Grafy nieskierowane 565
Stopnie oddalenia Jednym z dwóch klasycznych zastosowań metod przetwarzania
grafów jest wyznaczanie stopnia oddalenia między dwoma osobami w sieci społecznej.
Aby skonkretyzować rozważania, omawiamy to zastosowanie w kategoriach zyskującej
popularność zabawy, nazywanej tu grq w Kevina Bacona (wykorzystujemy przy tym
opisany wcześniej graf z filmami i wykonawcami). Kevin Bacon to aktywny aktor, wystę
pujący w wielu filmach. Do każdego wykonawcy przypisujemy liczbę Bacona. Odbywa
się to tak: sam Bacon ma liczbę 0. Każdy aktor, który występował z Baconem w tym
samym filmie, ma liczbę Bacona 1. Wszyscy wykonawcy (oprócz samego Bacona) wy
stępujący z aktorem o liczbie 1 mają liczbę Bacona 2 itd. Przykładowo, Meryl Streep
ma liczbę Bacona 1, ponieważ występowała z Baconem w filmie The River Wild. Nicole
Kidman ma liczbę 2. Wprawdzie nie występowała w żadnym filmie z Baconem, ale grała
z Tomem Cruisem w filmie Days ofTJuinder, a Cruise występował z Baconem w filmie
A Few Good Men. Najprostszą wersją zabawy jest wyszukiwanie na podstawie nazwiska
aktora ciągu filmów i wykonawców prowadzącego do Kevina Bacona. Przykładowo,
miłośnik kina może wiedzieć, że Tom Hanks wystąpił w filmie Joe Versus the Volcano
z Lloydem Bridgesem, który grał w High Noon z Grace Kelly, która wystąpiła w Dial M
for Murder z Patrickiem Allenem,
występującym w The Eagle Has % java D egreesO fSeparation m [Link] "/ " "Bacon, Kevin"
Kidman, N icole
Landed z Donaldem Sutherlandem,
Bacon, Kevin
który wystąpił w Animal House Few Good Men, A (1992)
z Kevinem Baconem. Jednak ta C ru ise , Tom
Days o f Thunder (1990)
wiedza nie wystarcza do ustalenia
Kidman, N icole
liczby Bacona dla Toma Hanksa Grant, Cary
(wynosi ona 1, ponieważ Hanks Bacon, Kevin
wystąpił razem z Baconem w filmie M ystic R iv e r (2003)
W i l l i s , Susan
Apollo 13). Widać więc, że liczbę M a je stic , The (2001)
Bacona trzeba ustalić przez zlicze Landau, M artin
nie filmów na najkrótszej ścieżce, North by Northwest (1959)
Grant, Cary
dlatego trudno stwierdzić, kto
wygrał, nie używając kompute
ra. Oczywiście, w programie DegreesOfSeparation ze strony 567 (jest to klient klasy
Symbol Graph) widać, że klasa BreadthFirstPaths pozwala znaleźć najkrótszą ścieżkę
i wyznaczyć liczbę Bacona dla dowolnego aktora z pliku [Link]. Program przyjmuje
źródłowy wierzchołek z wiersza poleceń, a następnie przyjmuje zapytania ze standar
dowego wejścia i wyświetla najkrótszą ścieżkę ze źródła do wierzchołka z zapytania.
Ponieważ graf oparty na pliku movies. txt jest dwudzielny, wszystkie ścieżki przechodzą
na zmianę przez filmy i wykonawców, a wyświetlona ścieżka jest „dowodem” na jej po
prawność (jednak nie stanowi dowodu na to, że ścieżka jest najkrótsza; aby przekonać
znajomych o tym, że ścieżka jest najkrótsza, należy zaprezentować im t w i e r d z e n i e b).
Program DegreesOfSeparation znajduje najkrótsze ścieżki także w grafach, które nie
są dwudzielne. Wyznacza na przykład sposób na dotarcie z jednego lotniska z pliku
[Link] na inne za pomocą najmniejszej liczby połączeń.
566 RO ZD ZIA Ł 4 □ Grafy
m o żesz w ykorzystać program DegreesOfSeparation do uzyskania odpowiedzi na
ciekawe pytania dotyczące przemysłu filmowego. Możliwe jest na przykład ustalenie
oddalenia między filmami, a nie między wykonawcami. Co ważniejsze, kwestię od
dalenia przebadano także w wielu innych kontekstach. Matematycy grają w tę samą
grę na podstawie grafu opartego na współautorach prac naukowych i ich oddaleniu
od P. Erdósa — płodnego matematyka z XX wieku. Podobnie każdy w New Jersey
wydaje się mieć liczbę Brucea Springstina 2, ponieważ wszyscy w stanie znają kogoś,
kto twierdzi, że zna Brucea. Do gry w Erdosa potrzebna jest baza danych z wszyst
kimi pracami matematycznymi. Gra w Springstina jest nieco trudniejsza. W poważ
niejszym kontekście stopnie oddalenia odgrywają kluczową rolę w projektowaniu
komputerów i sieci komunikacyjnych, a także pomagają zrozumieć sieci naturalne
we wszystkich obszarach nauki.
% java DegreesO fSeparation m [Link] "/ " "Animal House (1978)"
T it a n ic (1997)
Animal House (1978)
A lle n , Karen ( I)
Raiders o f the Lost Ark (1981)
Ta ylor, Rocky ( I)
T it a n ic (1997)
To Catch a T h ie f (1955)
Animal House (1978)
Vernon, John ( I)
Topaz (1959)
H itchcock, A lfre d ( I)
To Catch a T h ie f (1955)
4.1 Grafy nieskierowane 567
Stopnie oddalenia
public c la s s DegreesOfSeparation
{
public s t a t ic void m ain(String[] args)
{
Symbol Graph sg = new Symbol Graph (args [0], a r g s [ l ] ) ;
Graph G = s g . G ( ) ;
S t r in g source = a r g s [2];
i f (is g .c o n ta in s (s o u rc e ))
{ S tdO [Link](source + " nie ma w b a z ie . " ) ; return; }
in t s = s g .in d e x (s o u rc e );
BreadthFirstPaths bfs = new BreadthFirstPaths(G, s ) ;
while (!Std In .isEm p ty ())
{
S t r in g sin k = S t d ln . r e a d L i n e Q ;
i f (s g .c o n t a in s ( sin k ))
(
in t t = s g . i n d e x ( s in k ) ;
i f ([Link](t))
fo r (in t v : [Link](t))
S td O u t .p rin t ln (" " + [Link](v));
else S t d O u t .p r in t ln ("N iepowiązane");
}
else S td O u t.p rin tln ("N ie i s t n i e j e w b a z ie ." );
}
}
}
Ten klient klas Symbol Graph i BreadthFi rstPaths znajduje najkrótsze ścieżki w grafach.
W przypadku pliku [Link] umożliwia grę w Kevina Bacona.
% java DegreesO fSeparation ro u t e s .tx t " " JFK
LAS
JFK
ORD
PHX
LAS
DFW
JFK
ORD
DFW
568 R O ZD ZIA Ł 4 o Grafy
P o d s u m o w a n i e W tym podrozdziale wprowadziliśmy kilka podstawowych za
gadnień, które rozwijamy w dalszej części rozdziału. Oto te zagadnienia:
D terminologia dotycząca grafów;
■ reprezentacja grafu umożliwiająca przetwarzanie dużych grafów rzadkich;
n wzorzec projektowy do przetwarzania grafów — algorytmy są implementowane
w klientach, które wstępnie przetwarzają graf w konstruktorze i budują struktu
ry danych umożliwiające wydajną obsługę zapytań na temat grafu;
■ przeszukiwanie w głąb i wszerz;
■ klasa umożliwiająca korzystanie z symbolicznych nazw wierzchołków.
Tabela poniżej to podsumowanie implementacji omówionych algorytmów dla gra
fów. Algorytmy te to dobre wprowadzenie do przetwarzania grafów, ponieważ wersje
tego kodu ponownie pojawią się przy analizowaniu bardziej skomplikowanych ro
dzajów grafów i zastosowań, a także — co z tego wynika — trudniejszych problemów
z obszaru przetwarzania. Te same pytania dotyczące połączeń i ścieżek między wierz
chołkami stają się dużo trudniejsze po dodaniu lderunków, a następnie wag do kra
wędzi grafu. Jednak te same podejścia są skuteczne przy odpowiadaniu także na takie
pytania i stanowią punkt wyjścia przy rozwiązywaniu trudniejszych problemów.
Problem Rozwiązanie Źródło
Połączenia z jednym źródłem DepthFirstSearch Strona 543
Ścieżki z jednego źródła DepthFi rstP ath s Strona 548
Najkrótsze ścieżki z jednego źródła BreadthFi rstP ath s Strona 552
Składowe CC Strona 556
Wykrywanie cykli Cycle Strona 559
Możliwość przypisania dwóch kolorów
TwoColor Strona 559
(grafy dwudzielne)
Problemy z obszaru przetwarzania grafów (nieskierowanych)
poruszone w podrozdziale
4.1 a Grafy nieskierowane 569
[j PYTANIA I ODPOWIEDZI
p. Dlaczego nie połączyliśmy wszystkich algorytmów w klasie Graph .java?
O. To prawda, można dodać metody obsługi zapytań (oraz wszystkie potrzebne pola
i metody prywatne) do podstawowej definicji typu ADT Graph. Choć takie podej
ście ma pewne zalety związane z abstrakcją danych, m a też poważne wady, ponieważ
dziedzina przetwarzania grafów jest znacznie rozleglejsza niż te związane z podsta
wowymi strukturam i danych omawianymi w p o d r o z d z i a l e 1 .3 . Oto najważniejsze
z tych wad:
° Istnieje tak dużo operacji do przetwarzania grafów, że nie da się ich precyzyjnie
zdefiniować w jednym interfejsie API.
0 Przy prostych zadaniach z dziedziny przetwarzania grafów trzeba korzystać
z tego samego interfejsu, co przy wykonywaniu skomplikowanych operacji.
0 Jedna metoda może korzystać z pól przeznaczonych do użytku przez inną m e
todę, co jest niezgodne z zasadami hermetyzacji, których chcemy przestrzegać.
Umieszczenie wszystkich m etod w jednej klasie nie jest niczym niezwykłym. Interfejsy
API obejmujące wiele m etod to szerokie interfejsy (zobacz stronę 109). W rozdzia
le poświęconym algorytmom przetwarzania grafów interfejs API tego rodzaju byłby
naprawdę szeroki.
P. Czy w klasie Symbol Graph rzeczywiście niezbędne są dwa przebiegi?
O. Nie. Można ponieść dodatkowy koszt na poziomie lg N i dodać bezpośrednią
obsługę m etody adj (), używając typu ST zamiast Bag. Implementację opartą na tym
pomyśle przedstawiliśmy w książce An Introduction to Programming in Java: An
Interdisciplinary Approach.
570 R O ZD ZIA Ł 4 Grafy
0 ĆWICZENIA
4.1.1. Jaka jest m inim alna liczba krawędzi w grafie o V wierzchołkach i bez równo
ległych krawędzi? Jaka jest minimalna liczba krawędzi w grafie o V wierzchołkach,
z których żaden nie jest izolowany?
ti ny G e x 2 .txt 4.1.2. Narysuj w stylu podobnym do rysunków z tekstu
12 (strona 536) listy sąsiedztwa zbudowane na podstawie pliku
16
8 4 [Link] (po lewej) przez konstruktor klasy Graph uży
2 3 wający strum ienia wejściowego.
111
06 4.1.3. Utwórz konstruktor kopiujący dla klasy Graph.
36
10 3 Konstruktor powinien przyjmować graf Gjako dane wejścio
7 11 we oraz tworzyć i inicjować nową kopię grafu. Zmiany wpro
78 wadzone przez klienta w G nie powinny wpływać na nowo
11 8
2 0 utworzony graf.
6 2
52 4.1.4. Dodaj do klasy Graph metodę hasEdge(), która przyj
5 10 muje dwa argumenty typu i nt (v i w) oraz zwraca true, jeśli
3 10
8 1 © graf obejmuje krawędź v-w, i fal se w przeciwnym razie.
4 1
4.1.5. Zmodyfikuj klasę Graph tale, aby graf nie mógł obej
mować krawędzi równoległych ani pętli własnych.
4.1.6. Rozważmy graf o czterech wierzchołkach oraz krawędziach 0-1, 1-2, 2-3 i 3-0.
Narysuj tablicę list sąsiedztwa, która nie mogła powstać przez wywołania addEdge()
dla tych krawędzi niezależnie od kolejności ich dodawania.
4.1.7. Opracuj dla klasy Graph klienta testowego, który wczytuje graf ze strumienia
wejściowego o nazwie podanej jako argument wiersza poleceń, a następnie wyświetla
ten graf, posługując się m etodą to S tri ng().
4 .1. 8 . Opracuj implementację interfejsu API ldasy Search ze strony 540. Wykorzystaj
typ UF, tak jak opisano to w tekście.
4.1.9. Przedstaw (w taki sposób, jak na rysunku ze strony 545) szczegółowy ślad
działania wywołania dfs(0) dla grafu zbudowanego przez konstruktor Graph dla
strumienia wejściowego na podstawie pliku [Link] (zobacz ć w i c z e n i e 4 . 1 .2 ).
Narysuj też drzewo reprezentowane przez tablicę edgeTo [].
4 .1.10. Udowodnij, że każdy graf spójny ma wierzchołek, którego usunięcie (wraz
z wszystkimi sąsiednimi krawędziami) nie prowadzi do powstania grafu niespójnego.
Napisz metodę DFS znajdującą taki wierzchołek. Wskazówka: rozważ wierzchołek,
którego wszystkie sąsiednie wierzchołki są oznaczone.
4.1.11. Narysuj drzewo reprezentowane przez tablicę edgeTo [] po wywołaniu
bfs(G, 0) w a l g o r y t m i e 4.2 dla grafu zbudowanego przez konstruktor Graph dla
strum ieni wejściowych na podstawie pliku [Link] (zobacz ć w i c z e n i e 4 . 1 .2 ).
4.1 ■ Grafy nieskierowane 571
4.1.12. W jaki sposób drzewo zbudo
wane m etodą BFS pozwala określić od
Te same listy, co dla danych
ległość między v a w, jeśli żaden z tych
wejściowych w postaci listy
wierzchołków nie jest korzeniem? krawędzi, przy czym kolejność
elementów na listach jest inna
4.1.13. D o d a j d o in te rfe jsu API klasy
B re ad th F irstP ath s m etodę d istT o (). /
ti n y G a d j .txt
Zaimplementuj ją tak, aby zwracała licz % java Graph [Link]
13 vertices, 13 edges
bę krawędzi w najkrótszej ścieżce między 13 ^
źródłem a danym wierzchołkiem. Metoda 0 12 5 6 Kolejność list
3 4 5 jest odwrócona
powinna działać w stałym czasie. 4 5 6 względem danych
7 8 wejściowych
4.1.14. Załóżmy, że przy przeszukiwa 9 10 11 12
niu wszerz zastosowaliśmy stos zamiast 11 12
kolejki. Czy także wtedy m etoda wyzna
czy najkrótsze ścieżki? 9: 12 11 10 Drugie wystąpienie
10: 9 każdej krawędzi
4.1.15. Zmodyfikuj w klasie Graph kon 11: 12 9 wyróżniono kolorem
12: 11 9 czerwonym
struktor dla strumieni wejściowych, aby
umożliwić pobieranie list sąsiedztwa ze
standardowego wejścia (podobnie jak w klasie Symbol Graph), takich jak pokazany po
prawej przykładowy plik [Link]. Na początku znajdują się liczby wierzchołków
i krawędzi, a dalej każdy wiersz obejmuje wierzchołek i listę sąsiednich wierzchołków.
4.1.16. Acentryczność wierzchołka v to długość najkrótszej ścieżki z danego wierz
chołka do wierzchołka najbardziej oddalonego od v. Średnica grafu to maksymal
na acentryczność wierzchołków grafu. Promień grafu to najmniejsza acentryczność
wierzchołków grafu. Środek to wierzchołek, którego acentryczność jest promieniem.
Zaimplementuj pokazany poniżej interfejs API.
p u b lic c la s s G raphProperties
G raphProperties(G raph G) Konstruktor (zwraca wyjątek, jeśli G nie jest spójny)
in t e c c e n t r ic it y ( in t v) Zwraca acentryczność wierzchołka v
in t diam eter() Zwraca średnicę grafu G
in t ra d iu s () Zwraca promień grafu G
in t ce nter() Zwraca środek grafu G
572 R O ZD ZIA Ł 4 0 Grafy
ĆWICZENIA (ciąg dalszy)
4.1.18. Obwód grafu to długość najkrótszego cyklu. Jeśli graf jest acykliczny, obwód
to nieskończoność. Dodaj do klasy GraphProperti es metodę gi rth () zwracającą ob
wód grafu. Wskazówka: uruchom metodę BFS dla każdego wierzchołka. Najkrótszy
cykl obejmujący s to najkrótsza ścieżka z s do pewnego wierzchołka v plus krawędź
łącząca v z powrotem z s.
4 .1.19. Przedstaw (w taki sposób, jak na rysunku ze strony 557) szczegółowy ślad
działania klasy CC przy wyszukiwaniu spójnych składowych w grafie zbudowanym
przez konstruktor klasy Graph dla strum ieni wejściowych na podstawie pliku tiny-
[Link] (zobacz ć w i c z e n i e 4 . 1 .2 ).
4 .1.20. Przedstaw (w taki sposób, jak na rysunkach w podrozdziale) szczegółowy
ślad działania klasy Cycle przy wyszukiwaniu cykli w grafie zbudowanym przez
konstruktor klasy Graph dla strum ieni wejściowych na podstawie pliku tinyGex2.
txt (zobacz ć w i c z e n i e 4 . 1 .2 ). Jakie jest tempo wzrostu czasu działania konstruktora
klasy Cyc! e dla najgorszego przypadku?
4 .1.21. Przedstaw (w taki sposób, jak na rysunkach w podrozdziale) szczegółowy
ślad działania klasy TwoColor przy określaniu możliwości przypisania dwóch kolo
rów do grafu zbudowanego przez konstruktor klasy Graph dla strum ieni wejściowych
na podstawie pliku [Link] (zobacz ć w i c z e n i e 4 .1 .2 ). Jakie jest tempo wzrostu
czasu działania konstruktora klasy TwoCol or dla najgorszego przypadku?
4.1 .2 2 . Uruchom program Symbol Graph dla pliku [Link], aby znaleźć liczbę
Bacona dla aktorów nominowanych w tym roku do nagrody Oscara.
4.1.23. Napisz program BaconHistogram, który wyświetla histogram liczb Bacona,
określający, ilu aktorów z pliku [Link] m a liczbę Bacona 0,1, 2,3... Dodaj katego
rię dla osób, dla których liczba ta jest nieskończona (dla wykonawców niepowiąza
nych z Kevinem Baconem).
4.1.24. Oblicz liczbę spójnych składowych w pliku [Link], wielkość największej
składowej i liczbę składowych o rozmiarze poniżej 10. Ustal acentryczność, średni
cę, promień, środek i obwód największej składowej grafu. Czy obejmuje ona Kevina
Bacona?
4.1.25. Zmodyfikuj program DegreesOfSeparati on, aby jako argument wiersza po
leceń przyjmował wartość y typu i nt i pomijał filmy starsze niż y lat.
4.1 a Grafy nieskierowane 573
4.1.26. Napisz klienta klasy Symbol Graph (podobnego do program u
który stosuje przeszukiwanie wgłęb zamiast przeszukiwania
D e g r e e s O f S e p a r a t i on),
wszerz do wyszukiwania ścieżek łączących dwóch aktorów. Program m a generować
dane wyjściowe podobne do pokazanych poniżej.
4.1.27. Określ ilość pamięci potrzebnej w klasie Graph do reprezentowania grafu
o iż wierzchołkach i E krawędziach. Zastosuj model kosztów pamięciowych opisany
w P O D R O Z D Z IA L E 1 .4 .
4.1.28. Dwa grafy są izomorficzne, jeśli m ożna przez zmianę nazw wierzchołków
jednego grafu sprawić, aby był identyczny z drugim. Narysuj wszystkie nieizomor-
ficzne grafy o dwóch, trzech, czterech i pięciu wierzchołkach.
4.1.29. Zmodyfikuj klasę Cycle tak, aby działała nawet dla grafów obejmujących
pętle własne oraz krawędzie równoległe.
% java DegreesOfSeparationDFS m [Link]
Źródło: Bacon, Kevin
Zapytanie: Kidman, N icole
Bacon, Kevin
M y stic R iv e r (2003)
O'Hara, Jenny
M atchstick Men (2003)
Grant, Beth
... [lic z b a filmów: 123] (!)
Law, Jude
Sky C a p t a in ... (2004)
J o lie , A ngelina
Pla ying by Heart (1998)
Anderson, G i lli a n ( I)
Cock and Bu ll Sto ry , A (2005)
Henderson, S h ir le y ( I)
24 Hour Party People (2002)
E cclesto n, C hristop he r
Gone in S ix t y Seconds (2000)
B a la h o u tis, Alexandra
Days o f Thunder (1990)
Kidman, N icole
R O ZD ZIA Ł 4 a Grafy
PROBLEMY DO ROZWIĄZANIA
4 .1.30. Cykle eulerowskie i hamiltonowskie. Rozważ grafy zdefiniowane przez cztery
poniższe zbiory krawędzi:
0-1 0-2 0-3 1-3 1-4 2-5 2-9 3-6 4-7 4-8 5-8 5-9 6-7 6-9 7-8
0-1 0-2 0-3 1-3 0-3 2-5 5-6 3-6 4-7 4-8 5-8 5-9 6-7 6-9 8-8
0-1 1-2 1-3 0-3 0-4 2-5 2-9 3-6 4-7 4-8 5-8 5-9 6-7 6-9 7-8
4-1 7-9 6-2 7-3 5-0 0-2 0-8 1-6 3-9 6-3 2-8 1-5 9-8 4-5 4-7
Które z tych grafów obejmują cykle Eulera (w talach cyklach każda krawędź jest od
wiedzana dokładnie raz)? Które grafy obejmują cykle Hamiltona (w takich cyklach
każdy wierzchołek jest odwiedzany dokładnie raz)?
4 .1.31. Wymienianie grafów. Ile istnieje różnych grafów nieskierowanych o V wierz
chołkach i E krawędziach (bez krawędzi równoległych)?
4 .1.32. Wykrywanie krawędzi równoległych. Wymyśl działający w czasie liniowym
algorytm do zliczania krawędzi równoległych w grafie.
4 .1.33. Cykle nieparzyste. Udowodnij, że graf jest dwudzielny (można go pokolo
rować dwoma kolorami) wtedy i tylko wtedy, jeśli nie obejmuje cykli o nieparzystej
długości.
4 .1.34. Graf symboli. Zaimplementuj jednoprzebiegową wersję klasy Symbol Graph
(nie musi być ona klientem klasy Graph). W operacjach na grafach w implementacji
można ponieść dodatkowe koszty na poziomie log U, potrzebne na wyszukiwanie
w tablicy symboli.
4 .1.35. Dwuspójność. Graf jest dwuspójny, jeśli każda para wierzchołków jest połą
czona dwoma rozłącznymi ścieżkami. Punkt artykulacji w grafie spójnym to wierz
chołek, którego usunięcie (wraz z sąsiednimi krawędziami) spowodowałoby, że graf
stałby się niespójny. Udowodnij, że każdy graf bez punktów artykulacji jest dwu
spójny. Wskazówka: dla pary wierzchołków s i t oraz łączącej je ścieżki wykorzystaj
fakt, że żaden z wierzchołków w ścieżce nie jest punktem artykulacji, do utworzenia
dwóch rozłącznych ścieżek łączących s i t.
4 .1.36. Spójność ze względu na krawędzie. Most w grafie to krawędź, której usunię
cie powoduje podział spójnego grafu na dwa rozłączne podgrafy. Graf bez mostów
jest spójny ze względu na krawędzie. Opracuj oparty na metodzie DFS typ danych do
określania, czy dany graf jest spójny ze względu na krawędzie.
4.1 o Grafy nieskierowane 575
4 . 1 . 3 7 . Grafy euklidesowe.
Zaprojektuj i zaimplementuj interfejs API klasy
EuclideanGraph. Klasa m a służyć do tworzenia grafów, których wierzchołkami są
punkty w przestrzeni współrzędnych. Dołącz metodę show() i wykorzystaj w niej
bibliotekę StdDraw do rysowania grafu.
4.1.38. Przetwarzanie obrazu. Zaimplementuj operację wypełniania na grafie wy
znaczanym przez połączenie sąsiednich punktów obrazu mających ten sam kolor.
576 RO ZD ZIA Ł 4 □ Grafy
| ' EKSPERYMENTY
4.1.39. Grafy losowe. Napisz program ErdosRenyiGraph, który przyjmuje z wiersza
poleceń wartości całkowitoliczbowe V i E, a następnie tworzy graf, generując E loso
wych par liczb całkowitych z przedziału od 0 do V -l. Uwaga-, generator ten tworzy
pętle własne i krawędzie równoległe.
4 .1.40. Losowe grafy proste. Napisz program RandomSimpleGraph, który przyjmuje
z wiersza poleceń wartości całkowitoliczbowe V i E, a następnie tworzy graf, gene
rując (z równym prawdopodobieństwem) jeden z możliwych grafów prostych o V
wierzchołkach i E krawędziach.
4.1.41. Losowe grafy rzadkie. Napisz program RandomSparseGraph do generowania
grafów rzadkich dla dobrze dobranego zbioru wartości V i E, tak aby można użyć
ich do przeprowadzenia sensownych testów empirycznych na grafach utworzonych
w modelu Erdósa-Renyiego.
4.1.42. Losowe grafy euklidesowe. Napisz używającego klasy Eucl i deanGraph klienta
RandomEucl ideanGraph (zobacz ć w i c z e n i e 4 . 1 .3 7 ), tworzącego grafy losowe przez
wygenerowanie w przestrzeni V losowych punktów i późniejsze połączenie każdego
punktu z wszystkimi punktam i w prom ieniu d od środka. Uwaga: graf prawie na
pewno będzie spójny, jeśli d jest większe od wartości progowej f \ g v T f v , i prawie na
pewno będzie niespójny, jeżeli d ma mniejszą wartość.
4 .1.43. Grafy losowe oparte na siatce. Napisz używającego klasy Eucl i deanGrap klien
ta RandomGri dGraph, który generuje grafy losowe, łącząc wierzchołki uporządkowane
w siatce f v na f y z ich sąsiadami (zobacz ć w i c z e n i e 1 . 5 . 1 5 ). Wzbogać program
tak, aby dodawał R dodatkowych losowych krawędzi. Dla dużych R zmniejsz siatkę
tak, aby łączna liczba krawędzi wynosiła mniej więcej V. Dodaj wersję, w której do
datkowa krawędź łączy wierzchołki s i t z prawdopodobieństwem odwrotnie propor
cjonalnym do odległości euklidesowej między tymi wierzchołkami.
4.1.44. Grafy w świecie rzeczywistym. Znajdź w sieci W W W duży graf ważony, na
przykład mapę z odległościami, połączenia telefoniczne o określonych kosztach lub
plan lotów z cenami. Napisz program RandomReal Graph, który tworzy graf, wybierając
losowo V wierzchołków i E krawędzi z podgrafu opartego na tych wierzchołkach.
4 .1.45. Losowe grafy przedziałowe. Rozważmy zbiór V przedziałów (par liczb rze
czywistych) na osi liczb rzeczywistych. Taka kolekcja wyznacza graf przedziałowy,
w którym każdemu przedziałowi odpowiada jeden wierzchołek. Jeśli przedziały choć
częściowo się pokrywają (mają wspólne punkty), między wierzchołkami istnieje kra
wędź. Napisz program generujący w przedziale jednostkowym V losowych przedzia
łów o długości d i tworzący odpowiedni graf przedziałowy. Wskazówka: użyj drzewa
BST.
4.1 a Grafy nieskierowcine 577
4.1.46. Losowe grafy dla systemu transportu. Jednym ze sposobów na zdefiniowa
nie systemu transportu jest użycie zbioru ciągów wierzchołków, w którym każdy
ciąg wyznacza ścieżkę łączącą wierzchołki. Przykładowo, ciąg 0-9-3-2 wyzna
cza krawędzie 0-9, 9-3 i 3-2. Napisz używającego klasy EuclideanGraph klienta
RandomT ransportati on, który tworzy graf na podstawie pliku wejściowego obejmującego
jeden ciąg na wiersz. Zastosuj nazwy symboliczne. Opracuj odpowiednie dane wejścio
we, tak aby program mógł zbudować graf odpowiadający systemowi paryskiego metra.
Testowanie wszystkich algorytmów i badanie każdego parametru w każdym modelu
grafów jest niewykonalne. Dla każdego z wymienionych dalej problemów napisz klien
ta, który rozwiązuje problem dla dowolnego grafu wejściowego. Następnie wybierz je
den z opisanych wcześniej generatorów do przeprowadzenia eksperymentów dla danego
modelu grafów. Wykorzystaj własny osąd przy ustalaniu eksperymentów (możesz oprzeć
się na wynikach wcześniejszych pomiarów). Napisz wyjaśnienie wyników i wnioski,
które można z nich wyciągnąć.
4.1.47. Długości ścieżek w metodzie DFS. Przeprowadź eksperymenty, aby empirycz
nie wyznaczyć prawdopodobieństwo, że program DepthFi rstP ath s znajdzie ścieżkę
między dwoma losowo wybranymi wierzchołkami, i obliczyć średnią długość znale
zionych ścieżek. Uwzględnij różne modele grafów.
4.1.48. Długości ścieżek w metodzie BFS. Przepi-owadź eksperymenty, aby empi
rycznie wyznaczyć prawdopodobieństwo, że program BreadthFi rstP ath s znajdzie
ścieżkę między dwoma losowo wybranymi wierzchołkami, i obliczyć średnią długość
znalezionych ścieżek. Uwzględnij różne modele grafów.
4.1.49. Spójne składowe. Przeprowadź eksperymenty, aby empirycznie ustalić roz
kład liczby składowych w losowych grafach różnego rodzaju. W tym celu wygeneruj
dużą liczbę grafów i narysuj histogram.
4.1.50. Możliwość przypisania dwóch kolorów. Większości grafów nie m ożna przy
pisać dwóch kolorów, a m etoda DFS pozwala szybko to stwierdzić. Przeprowadź te
sty empiryczne, aby zbadać liczbę krawędzi sprawdzanych przez program TwoCol or.
Uwzględnij różne modele grafów.
4.2. GRAFY SKIEROW ANE
W grafach skierowanych krawędzie są jednokierunkowe. Para wierzchołków wyzna
czająca każdą krawędź jest uporządkowana i określa jednostronne sąsiedztwo. Wiele
zastosowań (związanych na przykład z grafami reprezentującymi sieć WWW, ogra
niczenia przy szeregowaniu lub połączenia telefoniczne) m ożna w naturalny sposób
przedstawić za pom ocą grafów skierowanych. Jednostronne ograniczenie jest natu
ralne i łatwe do wymuszenia w implementacjach, dlatego wydaje się być niektopot-
liwe. Wymaga jednak dodatkowych struktur kombinatorycznych, co ma poważny
wpływ na algorytmy i spra-
Zastosowanie Wierzchołek Krawędź wia, że korzystanie z grafów
Łańcuch pokarmowy Gatunek Drapieżnik-ofiara skierowanych różni się od
Odnośnik stosowania grafów nieskie-
Materiały w Internecie Strona
rowanych. W tym podroz
Referencja
Program Moduł dziale omawiamy klasyczne
zewnętrzna
algorytmy do eksplorowania
Telefon komórkowy Telefon Połączenie i przetwarzania grafów skie
Środowisko naukowe Praca naukowa Cytowanie rowanych.
Finanse Papiery wartościowe Transakcja Słow nictw o Definicje doty
Internet Urządzenie Połączenie czące grafów skierowanych
Typowe zastosowania grafów skierowanych są prawie takie same, jak dla
grafów nieskierowanych (to
samo dotyczy niektórych al
gorytmów i programów). Warto jednak przytoczyć je jeszcze raz. Z drobnych róż
nic w sformułowaniach (związanych z kierunkiem krawędzi) wynikają zagadnienia
strukturalne będące istotą tego podrozdziału.
Definicja. Grafskierowany (inaczej digraf) to zbiór wierzchołków i krawędzi skie
rowanych. Każda krawędź skierowana łączy uporządkowaną parę wierzchołków.
Mówimy, że krawędź skierowana prowadzi z pierwszego do drugiego wierzchołka
w parze. Stopień wyjściowy wierzchołka w digrafie to liczba krawędzi wychodzących
z niego. Stopień wejściowy to liczba krawędzi wchodzących do wierzchołka. Przy
opisywaniu krawędzi w digrafach pomijamy człon skierowany, jeśli znaczenie wy
nika z kontekstu. Pierwszy wierzchołek w krawędzi skierowanej to głowa, a drugi
— ogon. Krawędzie skierowane rysujemy jako strzałld prowadzące z głowy do ogona.
Używamy zapisu v->w, aby określić krawędź digrafu prowadzącą z v do w. Tak jak
w grafach nieskierowanych, tak i tu kod obsługuje krawędzie równolegle i pętle włas
ne, jednak elementy te nie występują w przykładach i zwykle pomijamy je w tekście.
578
4.2 □ Grafy skierowane 579
Istnieją cztery różne sposoby powiązania dwóch wierzchołków w digrafie (pomijamy
tu anomalie) — brak krawędzi, krawędź v->w z v do w, krawędź w->v z wdo v lub dwie
krawędzie v->w i w->v (oznacza to połączenia w obu kierunkach).
Definicja. Ścieżka skierowana w digrafie to ciąg wierzchołków, w którym istnieje
(skierowana) krawędź prowadząca z każdego wierzchołka w ciągu do jego następ
nika. Cykl skierowany to ścieżka skierowana, na której przynajmniej jeden wierz
chołek pełni funkcję początku i końca. Cykl prosty to cykl bez powtarzających się
krawędzi lub wierzchołków (wyjątkiem jest wymagane powtórzenie pierwszego
i ostatniego wierzchołka). Długość ścieżki lub cyklu to liczba krawędzi.
Tak jak w grafach nieskierowanych, tak i tu zakła
Krawędź
damy, że ścieżki skierowane są proste — chyba że skierowana
rozluźnimy założenie przez wskazanie powtarza Cykl
skierowany ■ Wierzchołek
jących się wierzchołków (tak jak w definicji cy o długości 3
klu skierowanego) lub w celu uogólnienia ścież Ścieżka
Wierzchołek skierowana
ki skierowanej. Mówimy, że wierzchołek w jest 0 stopniu
osiągalny z wierzchołka v, jeśli istnieje ścieżka wejściowym 3
1stopniu
skierowana z v do w. Ponadto przyjmujemy, iż wyjściowym 2
każdy wierzchołek jest osiągalny z niego samego.
Oprócz tego przypadku fakt, że w digrafie w jest
osiągalny z v, nie stanowi informacji o tym, czy v J
jest osiągalny z w. To rozróżnienie jest oczywiste,
a przy tym — jak się okaże — bardzo ważne.
z r o z u m ie n ie a l g o r y t m ó w z tego podrozdziału wymaga zrozumienia rozróżnienia
między osiągalnością w digrafach i połączeniami w grafach nieskierowanych. Jest to
trudniejsze, niż może się wydawać. Przykładowo, choć prawie zawsze można natych
miast stwierdzić, czy dwa wierzchołki r małym grafie nieskierowanym są połączo
ne, odkrycie ścieżki skierowanej w digrafie
nie jest tak proste. Dowodem jest przykład
widoczny po lewej stronie. Przetwarzanie
digrafów przypomina poruszanie się po
mieście, w którym wszystkie ulice są jedno
kierunkowe, a kierunki nie tworzą spójnego
wzorca. Dotarcie z jednego punktu do d ru
giego może okazać się trudne. Sprzeczny
z tą intuicją jest fakt, że standardowa struk
tura danych używana do reprezentowania
digrafów jest prostsza niż odpowiadająca jej
Czy w ty m d ig ra fie m o ż n a d o trz e ć z v d o w?
reprezentacja grafów nieskierowanych!
R O ZD ZIA Ł 4 o Grafy
Typ danych Digraph Przedstawiony poniżej interfejs API i kod klasy Di graph
zaprezentowany na następnej stronie są prawie takie same, jak dla klasy Graph
(strona 538).
public c la s s Digraph
D ig ra p h (in t V) Tworzy digraf o V wierzchołkach i bez krawędzi
D ig ra p h (In in) Wczytuje digraf ze strumienia wejściowego i n
in t V() Zwraca liczbę wierzchołków
in t E() Zwraca liczbę krawędzi
void addEdge(int v, in t w) Dodaje do digrafu krawędź v->w
Wierzchołki powiązane z v krawędziami
Ite ra b le < In te g e r> a d j(i nt v)
wychodzącymi z v
Digraph re ve rse () Odwraca digraf
S t r in g t o S t r in g O Zwraca reprezentację w postaci łańcucha znaków
Interfejs API dla digrafów
Reprezentacja Używamy reprezentacji opartej na listach sąsiedztwa, przy czym
krawędź v->w jest reprezentowana na liście powiązanej odpowiadającej v jako węzeł
zawierający w. Reprezentacja ta bardzo przypomina rozwiązanie dla grafów nieskie-
rowanych, jest jednak jeszcze prostsza, ponieważ każda krawędź występuje tylko raz,
co pokazano na następnej stronie.
Form at danych wejściowych Kod konstruktora, który pobiera digraf ze strumienia
wejściowego, jest identyczny jak w tego rodzaju konstruktorze klasy Graph. Format
danych wejściowych jest taki sam, natomiast krawędzie są interpretowane jako skie
rowane. W formacie listy krawędzi para v wjest interpretowana jako krawędź v->w.
Odwracanie digrafu W interfejsie API klasy Di graph znalazła się dodatkowa meto
da, reverse (), zwracająca kopię digrafu po odwróceniu wszystkich krawędzi. Metoda
ta jest czasem potrzebna przy przetwarzaniu digrafów, ponieważ umożliwia klientom
znalezienie krawędzi prowadzących do każdego wierzchołka (metoda ad j () zwraca
tylko wierzchołki powiązane krawędziami wychodzącymi z każdego wierzchołka).
N a zw y sym boliczne W łatwy sposób można umożliwić klientom stosowanie
nazw symbolicznych przy korzystaniu z digrafów. Aby zaimplementować klasę
Symbol Di graph podobną do klasy Symbol Graph ze strony 564, należy zastąpić wszyst
kie wystąpienia słowa Graph słowem Di graph.
w a r t o p o ś w i ę c i ć c z a s na staranne przemyślenie różnic przez porównanie kodu
i rysunku przedstawionego po prawej stronie z odpowiednikami dla grafów nieskie-
rowanych (strony 536 i 538). W opartej na listach sąsiedztwa reprezentacji grafu nie-
skierowanego wiadomo, że jeśli v występuje na liście w, to w znajduje się na liście v.
W reprezentacji list sąsiedztwa dla digrafów nie m a takiej symetrii. Ta różnica ma
istotny wpływ na przetwarzanie digrafów.
4.2 Grafy skierowane 581
Typ danych dla grafów skierowanych (digrafów)
t in y D G . t x t
public c la s s Digraph
{
private final in t V;
private in t E;
private Bag<Integer>[] adj;
public Di graph (in t V)
{
t h i s . V = V;
t h i s . E = 0;
adj = (Bag<Integer>[]) new Bag[V];
fo r (in t v = 0; v < V; v++)
adj [v] = new Ba g<In teg er> ();
}
public in t V() { return V; }
public in t E() { return E; }
public void addEdge(int v, in t w)
(
adj [v] .add(w);
E++;
adj [] 0 T
}
public Iterable<Integer> a d j( in t v)
V 5 T
{ return adj [ v ] ; }
3 T
public Digraph reverse()
{ ^ 0
Digraph R = new Digraph(V);
f o r (in t v = 0; v < V; v++)
for (in t w : adj (v))
[Link](w, v ) ; ^ 0 -0
return R; V 7 9
} N. 11 10
A 12
Typ danych Digraph jest prawie identyczny z klasą
Graph (strona 538). Różnice polegają na tym, że tu A 4 12
metoda addEdge() wywołuje metodę add () tylko raz
S .
i dostępna jest metoda egzemplarza re v e rs e d , któ 9
ra zwraca kopię grafu z odwróconymi krawędziami.
Format danych wejściowych digrafu
Ponieważ część kodu można łatwo napisać na pod i reprezentacja w postaci list sąsiedztwa
stawie odpowiedniego kodu z ldasy Graph, pomijamy
metodę to S t r in g ( ) (zobacz tabelę na stronie 535)
i konstruktor oparty na strumieniu wejściowym (zo
bacz stronę 538).
582 RO ZD ZIA Ł 4 □ Grafy
Osiągalność w digrafach Pierwszym algorytmem przetwarzania grafów nie-
skierowanych był DepthFirstSearch (strona 543), rozwiązujący problem połączeń
z jednym źródłem. Algorytm ten umożliwia! klientom ustalenie, które wierzchołki
są powiązane z danym źródłem. Identyczny kod, w którym nazwę Graph zmieniono
na Di graph, rozwiązuje analogiczny problem dla digrafów:
Osiągalność z jednego źródła. Na podstawie digrafu i źródłowego wierzchołka s
zapewnij obsługę zapytań w postaci: Czy istnieje ścieżka skierowana z s do docelo
wego wierzchołka v?
Klasa Di rectedDFS, przedstawiona na następnej stronie, to nieco wzbogacona wersja
klasy DepthFi rstSearch, stanowiąca implementację poniższego interfejsu API.
public cla ss Di rectedDFS
Di rectedDFS (Digraph G, in t s) Znajduje w G wierzchołki
osiągalne z s
DirectedDFS(Digraph G, Znajduje w G wierzchołki
Iterab le<In te ge r> sources) osiągalne z sources
boolean marked(int v) Czy v jest osiągalny?
Interfejs API do określania oslągalności w digrafach
Przez dodanie drugiego konstruktora, przyjmującego listę wierzchołków, w interfej
sie API zapewniono klientom obsługę następującego uogólnienia problemu.
Osiągalność z wielu źródeł. Dla digrafu i zbioru źródłowych wierzchołków zapew
nij obsługę zapytań w postaci: Czy istnieje skierowana ścieżka z dowolnego wierz
chołka ze zbioru do danego wierzchołka docelowego v?
Problem ten powstaje przy rozwiązywaniu klasycznego zadania z obszaru przetwa
rzania łańcuchów znaków, omawianego w p o d r o z d z i a l e 5 .4 .
W klasie Di rectedDFS do rozwiązania opisanych problemów wykorzystano stan
dardowy paradygmat przetwarzania grafów i standardowe przeszukiwanie w głąb.
Kod dla każdego wierzchołka źródłowego wywołuje rekurencyjną metodę dfs(),
która oznacza każdy napotkany wierzchołek.
Twierdzenie D. M etoda DFS oznacza wszystkie wierzchołki digrafu osiągalne
z danego zbioru wierzchołków źródłowych w czasie proporcjonalnym do stopni
wyjściowych oznaczonych wierzchołków.
Dowód. Taki sam, jak dla t w ie r d z e n ia a ze strony 543.
4.2 Grafy skierowane 583
ALGORYTM 4.4. O siągalność w digrafach
public c la s s DirectedDFS
{
private boolean[] marked;
p ublic DirectedDFS(Digraph G, in t s)
{
marked = new boolean[G .V ()];
dfs(G, s );
}
p ublic DirectedDFS(Digraph G, Iterab le<In te ge r> sources)
{
marked = new boolean[G .V ()];
f o r (in t s : sources)
i f (!marked[s]) dfs(G, s ) ;
}
p rivate void dfs(Digraph G, in t v) % java D1rectedDFS tin yD G .tx t 1
marked[v] = true;
f o r (in t w : G.a d j(v ) ) % java DirectedDFS tin yD G .tx t 2
i f (¡marked[w]) dfs(G, w); 0 1 2 3 4 5
% java DirectedDFS tin yD G .tx t 1 2 5
0 1 2 3 4 5 6 9 10 11 12
public boolean marked(int v)
{ return markedfv]; }
public s t a t ic void m ain(String[] args)
{
Digraph G = new Digraph(new I n (a r g s [0 ]));
Bag<Integer> sources = new B a g<In teg er> ();
fo r (in t i = 1; i < [Link]; i++)
s o u r c e s .a d d (In t e g e r .p a r s e ln t ( a r g s [ i]));
DirectedDFS reachable = new DirectedDFS(G, sources);
f o r (in t v = 0; v < G.V(); v++)
i f ([Link](v)) StdO [Link](v + " ") ;
S t d O u t . p r in t ln ( ) ;
}
}
Ta implementacja przeszukiwania w głąb umożliwia klientom sprawdzenie, które wierzchołki
są osiągalne z danego wierzchołka lub zbioru wierzchołków.
584 RO ZD ZIA Ł 4 □ Grafy
marked[] ad j []
dfs(O) 0 T 0 51
1 1
2 2 0 3
3 3 52
4 4 32
5 5 4
dfs(5) 0 T 0 51
1 1
2 2 0 3
3 3 52
4 4 32
5 T 5 4
d f s (4 ) 0 T 0 5 1
1 1
2 2 0 3
3 3 5 2
4 T 4 3 2
5 T 5 4
d f s (3 )
S p r a w d z a n ie 5 0 T 0 51
1 1
2 2 0 3
3 T 3 52
4 T 4 32
5 T 5 4
d f s ( 2) 0 T 0 5 1
I S p r a w d z a n ie 1 1
1 S p r a w d z a n ie 2 T 2 0 3
2 G otow y 3 T 3 5 2
3 G o to w y 4 T 4 3 2
5 T 5 4
S p r a w d z a n ie 2
4 G otow y
5 G otow y
d fs(1) 0 T 0 5 1
1 G otow y 1 T 1
0 G otow y 2 T 2 0 3
3 T 3 5 2
4 T 4 3
5 T 5 4
Ślad przebiegu przeszukiwania w głąb w celu znalezienia
wierzchołków osiągalnych z wierzchołka 0 w digrafie
4.2 b Grafy skierowane 585
Ślad działania algorytmu dla przykładowego digrafu pokazano na stronie 584. Ślad
ten jest nieco prostszy niż odpowiadający mu ślad dla grafów nieskierowanych, p o
nieważ m etoda DFS jest algorytmem przetwarzania digrafów (z jedną reprezentacją
każdej krawędzi). Warto przyjrzeć się śladowi, aby utrwalić zrozumienie przeszuki
wania w głąb w digrafach.
Przywracanie pam ięci m etodą znacz Bezpośrednio
i zam iataj (ang. marle and sweep) dostępne obiekty
Określanie osiągalności z wielu źródeł
jest ważne w kontekście typowych sy
stemów zarządzania pamięcią, w tym
w wielu implementacjach Javy. Digraf,
w którym każdy wierzchołek repre
zentuje obiekt, a każda krawędź od
powiada referencji do obiektu, jest
dobrym modelem wykorzystania pa
mięci w działającym programie Javy.
W każdym momencie wykonywania
programu niektóre obiekty są dostępne
bezpośrednio, a każdy obiekt, do któ
rego nie można z nich dotrzeć, podlega
mechanizmowi przywracania pamięci.
W strategii przywracania pamięci me
todą znacz i zamiataj jeden bit na obiekt
rezerwowany jest na potrzeby mechanizmu przywracania pamięci. Mechanizm okre
sowo oznacza zbiór potencjalnie dostępnych obiektów, uruchamiając algorytm osią
galności dla digrafów (podobny do Di rectedDFS), i przechodzi przez wszystkie obiekty,
odzyskując pamięć nieoznaczonych, co pozwala wykorzystać ją na nowe obiekty.
Znajdow anie ścieżek w digrafach Algorytmy DepthFi rstP ath s ( a l g o r y t m 4.1 ze
strony 548) i BreadthFi rstP ath s ( a l g o r y t m 4.2 ze strony 552) również są przezna
czone głównie do przetwarzania digrafów. Także tu identyczne interfejsy API i kod
(z nazwą Graph zmienioną na Digraph) pozwalają skutecznie rozwiązać następujące
problemy.
Z n a jd o w a n ie ścieżek skierow an ych z je d n e g o źró d ła . Dla digrafu i wierzchołka
źródłowego s zapewnij obsługę zapytań w postaci: Czy istnieje ścieżka skierowana
z s do danego wierzchołka docelowego v? Jeśli tak, należy znaleźć taką ścieżkę.
Z n a jd o w a n ie n ajkrótszych ścieżek skierow an ych z je d n e g o źró d ła . Dla digrafu
i wierzchołka źródłowego s zapewnij obsługę zapytań w postaci: Czy istnieje ścież
ka skierowana z s do danego wierzchołka docelowego v? Jeśli tak, należy znaleźć
najkrótszą ścieżkę tego rodzaju (o minimalnej liczbie krawędzi).
W witrynie i w ćwiczeniach w końcowej części podrozdziału rozwiązania tych prob
lemów nazywamy DepthFi rstDi rectedPaths oraz BreadthFi rstDi rectedPaths.
586 R O ZD ZIA Ł 4 □ Grafy
Cykle i grafy D AG Cykle skierowane mają szcze
gólnie duże znaczenie w zastosowaniach zwią
zanych z przetwarzaniem digrafów. Wykrycie
bez komputera cykli skierowanych w typowym
digrafie może stanowić problem, jak widać na
rysunku po prawej stronie. Teoretycznie digraf
może mieć bardzo dużą liczbę cykli. W prakty
ce koncentrujemy się zwykle na małej ich liczbie
lub chcemy ustalić, że digraf ich nie obejmuje.
W ramach uzasadniania znaczenia cykli skie
rowanych przy przetwarzaniu grafów jako podsta
wowy przykład wykorzystamy wzorcowy problem, Czy ten digraf obejmuje cykl skierowany?
w którym bezpośrednio powstaje model digrafu.
Problem szeregowania zadań Opisywany tu model rozwiązywania problemów
ma wiele zastosowań. Związany jest z szeregowaniem zbioru zadań do wykonania
przy pewnych ograniczeniach. Należy określić, kiedy i jak zadania mają zostać zre
alizowane. Ograniczenia mogą dotyczyć czasu lub innych zasobów potrzebnych do
wykonania zadań. Najważniejszy rodzaj ograniczeń jest związany z pierwszeństwem.
Ograniczenia te określają, że dane zadania trzeba wykonać przed pewnymi inny
mi. Różne rodzaje dodatkowych ograniczeń prowadzą do wielu rozmaitych typów
problemów szeregowania, mających różny poziom trudności. Przebadano dosłow
nie tysiące różnych problemów, a dla wielu z nich naukowcy nadal szukają lepszych
algorytmów. Rozważmy na przykład studenta układającego plan kursów, przy czym
ukończenie pewnych kursów jest wymagane do wzięcia udziału w innych, tak jak
w poniższym przykładzie.
( A lg o ry tm y
A lg e b ra lin io w a —(A n a liz a m a te m a ty c z n a )
/ T e o re ty c z n e
\ n a u k i k o m p u te r o w e /
rz r r- s / W p ro w a d z e n ie d o \
v a z Y a n y c J \ n a u k k o m p u te ro w y c h /
( S z tu c z n a in te lig e n c ja ) - ( R o b o ty k a )
( P ro g ra m o w a n ie z a a w a n s o w a n e
l)
( U c z e n ie m a s z y n o w e J — « - ( Sieci n e u ro n o w e
( B io lo g ia o b lic z e n io w a ~ )
( Obliczenia naukowe
Problem szeregowania z ograniczeniami pierwszeństwa
4.2 Q Grafy skierowane 587
Przy dodatkowym założeniu, że student może wybierać po jednym kursie naraz,
problem m ożna opisać w następujący sposób.
Szeregowanie z ograniczeniami pierwszeństwa. Jak na podstawie zbioru zadań
do ukończenia i ograniczeń pierwszeństwa (określających, że przed rozpoczęciem
pewnych zadań trzeba ukończyć inne) uszeregować zadania tak, aby zostały wy
konane bez naruszania ograniczeń?
Dla każdego problemu tego rodzaju natychmiast przy
chodzi na myśl model digrafu. Wierzchołki odpowiada
ją zadaniom, a skierowane krawędzie — ograniczeniom
pierwszeństwa. Z uwagi na zwięzłość wracamy tu do stan
dardowego modelu, w którym wierzchołkom przypisane
są liczby całkowite (tak jak na rysunku po lewej stronie).
S ta n d a rd o w y m o d e l d ig ra fu W digrafach szeregowanie z ograniczeniami pierwszeństwa
sprowadza się do następującego podstawowego problemu.
Sortowanie topologiczne. Ustaw wierz Wszystkie krawędzie Wszystkie wymagania
prowadzą w dól wstępne są spełnione
chołki digrafu w takiej kolejności, aby
wszystkie krawędzie skierowane prowadzi I i
Analiza m atem atyczna
ły z wierzchołków z wcześniejszych pozy
cji do wierzchołków z dalszych miejsc (lub Algebra liniowa
ustal, że jest to niemożliwe).
W prow adzenie d o nauk k om puterow ych
Po prawej stronie pokazano porządek topolo
giczny dla przykładowego modelu. Wszystkie Program ow anie zaaw ansow ane
krawędzie prowadzą w dół, dlatego porządek
Algorytm y
stanowi rozwiązanie problemu szeregowania
z ograniczeniami pierwszeństwa, którego m o T eoretyczne nauki kom puterow e
delem jest dany digraf. Student może spełnić
Sztuczna inteligencja
wszystkie wymagania wstępne, uczestnicząc
w kursach w określonej kolejności. Jest to typo Robotyka
we zastosowanie. W tabeli poniżej przedstawio
Uczenie m aszynow e
no kilka innych reprezentatywnych zastosowań.
Sieci n euronow e
Zastosowanie Wierzchołek Krawędź
Szeregowanie Ograniczenia Bazy danych
Zadanie © ,
zadań pierwszeństwa
Obliczenia naukow e
Planowanie Wymagania
Kurs
kursów wstępne Biologia obliczeniow a
Dziedziczenie Klasa Javy extends
Sortowanie topologiczne
Arkusze
Komórka Wzór
kalkulacyjne
Dowiązania
Nazwa pliku Dowiązanie
symboliczne
Typowe zastosowania sortowania topologicznego
588 R O ZD ZIA Ł 4 0 Grafy
Cykle w digrafach Jeśli zadanie x trzeba ukończyć przed zadaniem y, zadanie y przed
zadaniem z, a zadanie z przed zadaniem x, ktoś musiał popełnić błąd, ponieważ nie
można uwzględnić wszystkich tych ograniczeń jednocześnie. Ogólnie jeśli w problemie
szeregowania z ograniczeniami pierwszeństwa występuje cykl skierowany, rozwiązanie
nie istnieje. Aby wykryć takie błędy, trzeba rozwiązać następujący problem.
W ykrywanie cykli skierowanych. Czy w danym digrafie występuje cykl skierowa
ny? Jeśli tak, znajdź wierzchołki w takim cyklu w kolejności od pewnego wierz
chołka z powrotem do niego.
Liczba cykli w grafie może rosnąć wykładniczo (zobacz ć w i c z e n i e 4 .2 . 1 1 ), dlatego
należy znaleźć tylko jeden z nich, a nie wszystkie. Przy szeregowaniu zadań i w wielu
innych zastosowaniach wymagane jest, aby digraf nie obejmował cykli skierowanych.
Dlatego digrafy bez takich cykli odgrywają specjalną rolę.
D e fin ic ja . Skierowany graf acykliczny (ang. directed acyclic graph — DAG) to
digraf bez cykli skierowanych.
Rozwiązanie problemu wykrywania cykli skierowanych wymaga udzielenia odpowie
dzi na następujące pytanie: Czy dany digrafjest grafem DAGI Opracowanie rozwią
zania opartego na przeszukiwaniu w głąb nie jest trudne. Można wykorzystać to, że
stos rekurencyjnych wywołań przechowywany przez system reprezentuje „obecnie”
przetwarzaną ścieżkę skierowaną (przypomina to nić prowadzącą do wejścia przy
eksplorowaniu labiryntu metodą Tremaux). Znalezienie krawędzi skierowanej v->w
do znajdującego się na stosie wierzchołka w oznacza, że znaleziono cykl, ponieważ
stos jest dowodem na istnienie ścieżki skierowanej z w do v, a krawędź v->w dopeł
nia cykl. Ponadto nieobecność krawędzi powrotnych oznacza, że graf jest acykliczny.
W klasie DirectedCycle, pokazanej na następnej stronie, wykorzystano ten pomysł
do zaimplementowania poniższego interfejsu API.
p u b lic c la s s D irectedCycle
D ire cte dC ycle(D igra ph G) ,,
Konstruktor wyszukujący cykle
boolean ha sC ycle() Czy G obejmuje cykl skierowany?
Ite ra b l e<Intege r> cyc! e () Zwraca wierzchołki z cyklu (jeśli cykl istnieje)
Interfejs API do wykrywania cykli skierowanych
marked[] edgeTof] o n sta c k f]
1 2 3 4 5 0 1 2 3 4 5 0 1 23 4 5
d fs(0 )
d fs(5 ) 0 0 0 0 0 0 1 0 00 0 0
d fs(4 ) 00 0 0 1 -------------5 0 1 0 00 0 1
d fs(3 ) 0 0 0 1 1 4 5 0 1 0 00 1 1
Sprawdzani e 0 0 111 4 5 0 1001 i(T )
Wykrywanie cykli skierowanych w digrafach
4.2 Grafy skierowane 589
W yszukiwanie cyklu skierow anego
p ublic c la s s D irectedCycle
{
private boolean[] marked;
private in t [] edgeTo;
p rivate Stack<Integer> cycle; // Wierzchołki w cyklu ( j e ś l i ten
// istn ie je ).
p rivate boolean[] onStack; // Wierzchołki na s t o s ie wywołań
// rekurencyjnych.
p ublic DirectedCycle(Digraph G)
{
onStack = new boolean[G .V()];
edgeTo = new i nt[G.V ()];
marked = new b oolean[G .V ()];
f o r (in t v = 0; v < G.V(); v++)
i ł (!marked[v]) dfs(G, v ) ;
}
private void d f s ( D i graph G, in t v) v w x c y c le
3 5 3 3
{ 3 5 4 4 3
onStack[v] = true; 3 5 4 5 43
marked[v] = true; 3 5 4 3 54 3
for (in t w : [Link](v))
i ł (t h is . h a s C y c le Q ) return; Ślad procesu wyznaczania cyklu
e lse i f (¡marked[w])
{ edgeTo [w] = v; dfs(G, w); }
e lse i f (onStackfw])
{
cycle = new S ta c k < In te g e r> ();
fo r (in t x = v; x != w; x = edgeTofx])
c y c le .p u s h (x );
[Link](w );
c y c le .p u s h (v );
}
onStack[v] = fa lse ;
}
public boolean hasCycleQ
{ return cycle != n u l l ; }
public Iterab le<In te ge r> cycle()
{ return cycle; }
J _______________________________________________________________________
W tej klasie do standardowej rekurencyjnej metody d f s( ) dodano tablicę wartości logicz
nych, toStack[], na wierzchołki, dla których nie zakończono wywołań rekurencyjnych.
Kiedy metoda wykrywa krawędź v->w do wierzchołka w, który znajduje się na stosie, oznacza
to znalezienie cyklu skierowanego. M ożna go odtworzyć na podstawie odnośników z tablicy
edgeTo [].
R O ZD ZIA Ł 4 n Grafy
W czasie wykonywania metody d f s( G , v) przeszliśmy ścieżką skierowaną ze źródła
do v. Na potrzeby śledzenia tej ścieżld w klasie Di rectedCycl e przechowywana jest
indeksowana wierzchołkami tablica onStack [ ] , w której wierzchołki są oznaczane na
podstawie stosu rekurencyjnych wywołań (przez ustawienie elementu onStack [v] na
true przy wywoływaniu metody dfs (G, v) i na fal se przy zwracaniu z niej sterowa
nia). W klasie Di rectedCycl e przechowywana jest też tablica edgeTo[], co pozwala
zwrócić cykl po jego wykryciu w taki sam sposób, jak w klasach DepthFi rstPaths
(strona 548) i BreadthFi rstP ath s (strona 552) zwracano ścieżki.
Kolejność p rzy przeszukiw aniu w głąb i sortowanie topologiczne Szeregowanie
z ograniczeniami pierwszeństwa sprowadza się do wyznaczenia porządku topolo
gicznego dla wierzchołków w grafie DAG. Umożliwia to poniższy interfejs API.
publ i c c la s s Topological_____________
Konstruktor używany do sortowania
Topological (Digraph G) topologicznego
boolean i S DAG () Czy &jest grafem DAG?
Iterabl e<Integer> order () Zwraca wierzchołki w porządku
topologicznym
Interfejs API na potrzeby sortowania topologicznego
Twierdzenie E. D igraf ma porządek topologiczny wtedy i tylko wtedy, jeśli jest
grafem DAG.
Dowód. Jeśli digraf obejmuje cykl skierowany, nie występuje w nim porządek
topologiczny, jednak algorytm, który wkrótce omówimy, wyznacza porządek
topologiczny dla dowolnego grafu DAG.
Co ciekawe, okazuje się, że przedstawiliśmy już algorytm sortowania topologicznego.
Wystarczy dodać jeden wiersz do standardowej rekurencyjnej techniki DFS! Aby to
udowodnić, zaczynamy od klasy DepthFi rstOrder ze strony 592. Klasę oparto na po
myśle, że przy przeszukiwaniu w głąb każdy wierzchołek odwiedzany jest dokładnie
raz. Jeśli zapiszemy w strukturze danych wierzchołki przekazywane jako argumenty
do rekurencyjnej m etody dfs ( ) , a następnie przejdziemy po tej strukturze, odwie
dzimy wszystkie wierzchołki grafu w kolejności wyznaczanej przez naturę struktury
danych i to, czy wierzchołki zapisywane są przed wywołaniami rekurencyjnymi czy
po nich. W typowych zastosowaniach istotne są trzy porządki wierzchołków.
■ Preorder. Wierzchołek umieszczany jest w kolejce przed wywołaniami rekuren
cyjnymi.
■ Postorder. Wierzchołek umieszczany jest w kolejce po wywołaniach rekuren
cyjnych.
■ Odwrócony postorder. Wierzchołek umieszczany jest na stosie po wywołaniach
rekurencyjnych.
4.2 □ Grafy skierowane 591
Na następnej stronie pokazano ślad działania klasy DepthFi rstOrder dla przykładowego
grafu DAG. Można w łatwy sposób zaimplementować metody pre () , post () i reverse-
Post() przydatne w zaawansowanych algorytmach przetwarzania grafów. Przykładowo,
metoda order () w klasie Topol ogi cal obejmuje wywołanie metody reversePost ().
Preorder odpowiada Postorder odpowiada
kolejności wywołań kolejności, w której
metody d f s O wierzchołki sq „gotowe"
( i
pre p ost re v e rse P o st
dfs COD 0
dfs(5) O 5 Kolejka Kolejka Stos
dfs(4) 0 5 4 /
4 Gotowy / 4 / 4 /
5 Gotowy 4 5 5 4
dfs Cl) 0 5 4 1
1 Gotowy 4 5 1 15 4
dfs(6) 0 5 4 1 6
dfs(9) 054169
dfs C U D 0 5 4 1 6 9 11
dfs(12) 0541691112
12 Gotowy 4 5 1 12 12 1 5 4
11 Gotowy 4 5 1 12 11 11 12 1 5 4
dfs(10) 0 5 4 1 6 9 1 1 1 2 10
10 Gotowy 4 5 1 12 11 10 10 11 12 1 5 4
Sprawdzani e 12
9 Gotowy 4 5 1 12 11 10 9 9 10 11 12 1 5 4
Sprawdzanie 4
6 Gotowy 4 5 1 12 U 10 9 6 6 9 10 11 12 1 5 4
0 Gotowy 4 5 1 1 2 1 1 10 9 6 0 0 6 9 10 1 1 1 2 1 5 4
Sprawdzanie 1
dfs(2) 0 5 4 1 6 9 1 1 1 2 10 2
Sprawdzanie 0
dfs(B) 0 5 4 1 6 9 1 1 12 10 2 3
Sprawdzanie 5
3 Gotowy 4 5 1 1 2 1 1 10 9 6 0 3 3 0 6 9 10 11 12 1 5 4
2 Gotowy
Sprawdzanie 3 4 5 1 12 11 10 9 6 0 3 2 2 3 0 6 9 10 11 1 2 1 5 4
Sprawdzanie 4
Sprawdzanie 5
Sprawdzanie 6
dfs(73 0 5 4 1 6 9 1 1 1 2 10 2 3 7
Sprawdzanie 6
2 Gotowy 4 5 1 1 2 1 1 10 9 6 0 3 2 7 7 2 3 0 6 9 10 1 1 1 2 1 5 4
dfs(8) 0 5 4 1 6 9 11 12 10 2 3 7
Sprawdzanie 7
8 Gotowy 4 5 1 1 2 1 1 10 9 6 0 3 2 7 8 8 7 2 3 0 6 9 10 l i t 2 1 5 4
Sprawdzanie 9
Sprawdzanie 10
Sprawdzanie 11
t
Odwrócony
Sprawdzanie 12 postorder
Wyznaczanie porządków (preorder, postorder i odwrócony postorder) w digrafie przy przeszukiwaniu w głąb
___
592 RO ZD ZIA Ł 4 Grafy
Porządkowanie wierzchołków digrafu przy przeszukiwaniu w głąb
public c la s s DepthFirstOrder
{
private boolean[] marked;
p rivate Queue<Integer> pre; // Wierzchołki w porządku preorder.
private Queue<Integer> post; // Wierzchołki w porządku postorder.
private Stack<Integer> reversePost; // Wierzchołki w odwróconym porządku
// postorder.
public DepthFirstOrder(Digraph G)
{
pre = new Queue<Integer>();
post = new Queue<Integer>();
reversePost = new S ta c k < In te g e r> ();
marked = new boolean[G.V() ];
for (in t v = 0; v < G.V(); v++)
i f (!marked[vj) dfs(G, v);
}
private void dfs(Digraph G, in t v)
{
[Link](v);
marked [v] = true;
fo r (in t w : [Link](v))
i f (! marked[w])
dfs(G, w);
[Link](v);
re ve rse P ost.p u sh (v);
}
public Iterab le< In te ge r> pre()
( return pre; }
public Iterab le<In te ge r> post()
{ return post; }
public Iterab le<In te ge r> re ve rseP ost()
{ return reversePost; }
}
Ta klasa umożliwia klientom przechodzenie po wierzchołkach w różnej kolejności wyzna
czonej przy przeszukiwaniu w głąb. Możliwość ta jest bardzo przydatna przy rozwijaniu za
awansowanych algorytmów przetwarzania grafów, ponieważ rekurencyjna natura przeszuki
wania pozwala udowodnić właściwości obliczeń (zobacz na przykład t w i e r d z e n i e f ).
4.2 Grafy skierowane 593
ALGORYTM 4.5. Sortow anie top ologiczn e
public c la s s Topological
i
private Iterable<Integer> order; // Porządek topologiczny.
public Topological(Digraph G)
{
DirectedCycle cyclefinder = new DirectedCycle(G);
i f (¡[Link])
{
DepthFirstOrder dfs = new DepthFirstOrder(G);
order = d f s . r e v e r s e P o s t ( ) ;
}
}
public Iterab le<In te ge r> order()
( return order; }
public boolean isDAG()
{ return order == n u li; }
public s t a t ic void m ain(String[] args)
{
S trin g filename = a r g s [0];
S t r in g separator = a r g s [ l ] ;
Symbol Digraph sg = new Symbol Digraph (filename, separator);
Topological top = new Topological ( s g .G( ) ) ;
f o r (in t v : [Link](j)
S td O u t.p rin tln (sg.n am e (v));
}
)
Ten klient klas DepthFi rstOrder i Di rectedCycl e zwraca porządek topologiczny dla grafu
DAG. Klient testowy rozwiązuje problem szeregowania z ograniczeniami pierwszeństwa dla
typu Symbol Di graph. Metoda egzemplarza order() zwraca nuli, jeśli dany digraf nie jest
grafem DAG; w przeciwnym razie zwraca iterator udostępniający wierzchołki w porządku
topologicznym. Kod klasy Symbol Di graph pominięto, ponieważ jest dokładnie taki sam, jak
kod klasy Symbol Graph (strona 564), przy czym we wszystkich miejscach słowo Graph należy
zastąpić słowem Di graph.
594 RO ZD ZIA Ł 4 o Grafy
Twierdzenie F. Odwrócony porządek postorder w grafie DAG odpowiada sor
towaniu topologicznemu.
Dowód. Rozważmy dowolną krawędź v->w. Po wywołaniu dfs(v) spełniony
musi być jeden z trzech warunków (zobacz rysunek na stronie 595):
■ M etoda dfs (w) została wywołana i zwróciła sterowanie (w jest oznaczony).
■ M etoda dfs (w) nie została jeszcze wywołana (w nie jest oznaczony), dlate
go wykrycie v->w powoduje — bezpośrednio lub pośrednio — wywołanie
dfs (w) (i zwrócenie sterowania) przed zwróceniem sterowania przez wy
wołanie dfs(v).
■ W momencie wywołania dfs(v) m etoda dfs (w) jest wywołana, ale nie
zwróciła sterowania; kluczem do dowodu jest to, że w grafach DAG ta sytu
acja jest niemożliwa, ponieważ z łańcucha wywołań rekurencyjnych wyni
ka istnienie ścieżki z w do v, a krawędź v->w domyka cykl skierowany.
W dwóch możliwych przypadkach dfs (w) zwraca sterowanie przed dfs (v), dla
tego w występuje przed v w porządku postorder i po v w odwróconym porządku
postorder. Dlatego, zgodnie z wymogami, każda krawędź v->w prowadzi z wcześ
niejszego wierzchołka do późniejszego.
% more jo b s . tx t
Algorytm y/Teoretyczne nauki komputerowe/Bazy danych/O bliczenia naukowe
Wprowadzenie do nauk komputerowych/zaawansowane Programowanie/Algorytmy
Zaawansowane programowanie/Obliczenia naukowe
O b licze n ia naukowe/Biologia obliczeniow a
Teoretyczne nauki komputerowe/Biol ogia obliczeniow a/Sztuczna in t e lig e n c ja
Algebra 1 i niowa/Teoretyczne nauki komputerowe
A n a liza matematyczna/Algebra lin iow a
Sztuczna in t e lig e n c ja / S ie c i neuronowe/Robotyka/Uczenie maszynowe
Uczenie maszynowe/Sieci neuronowe
% java Top ological j o b s . t x t "/ "
A n a liza matematyczna
Algebra lin iow a
Wprowadzenie do nauk komputerowych
Zaawansowane programowanie
Algorytmy
Teoretyczne nauki komputerowe
Sztuczna in t e lig e n c ja
Robotyka
Uczenie maszynowe
S ie c i neuronowe
Bazy danych
O b licze n ia naukowe
B io lo g ia obliczeniow a
Klasa Topological ( a l g o r y t m 4.5 ze strony 593) to implementacja, w której wy
korzystano przeszukiwanie w głąb do topologicznego posortowania grafu DAG. Na
następnej stronie pokazano ślad przebiegu tego procesu.
4.2 Q Grafy skierowane
Twierdzenie G. Za pomocą metody
DFS można topologicznie posortować
graf DAG w czasie proporcjonalnym
do V+E.
Dowód. Wynika bezpośrednio z ko dfs(O)
du. Wykonano jedno przeszukiwanie dfs(5)
w głąb, aby zagwarantować, że graf nie dfs(4)
Wywołanie dfs(5) dla 5
4 Gotowy
obejmuje cykli skierowanych, i drugie (nieoznaczonego sąsiada 0)
5 Gotowy zostaje zakończone przed
w celu odwrócenia porządku postorder. dfs(l) ukończeniem wywołania
Oba wywołania obejmują sprawdzenie 1 Gotowy dfs(0), dlatego krawędź '
dfs(6) 0->5 prowadzi w górę
wszystkich krawędzi i wszystkich wierz
dfs(9)
chołków, dlatego działają w czasie pro dfs(1 1 )
porcjonalnym do V+E. dfs(1 2 )
i 12 Gotowy
11 Gotowy
dfs(1 0 )
Mimo prostoty tego algorytmu przez 10 Gotowy
wiele lat nie znajdował się on w centrum Sprawdzanie 12
9 Gotowy
uwagi. Popularnością cieszył się za to
Sprawdzanie 4
bardziej intuicyjny algorytm, oparty na 6 Gotowy
przechowywaniu kolejki źródeł (zobacz 0 Gotowy
ć w ic z e n ie 4 . 2 . 30 ). Sprawdzanie 1
dfs(2 )
w p r a k t y c e sortowanie topologiczne Sprawdzanie 0
i wykrywanie cykli są ze sobą związane, dfs(3)
przy czym wykrywanie cykli pełni funkcję Sprawdzani e
Wywołanie dfs(6) dla 6
3 Gotowy (nieoznaczonego sąsiada 1)
narzędzia diagnostycznego. Przykładowo,
2 Gotowy zostaje zakończone przed
w aplikacji do szeregowania zadań cykl sprawdzanie 3 ukończeniem w yw ołania'
skierowany w uzyskanym digrafie repre Sprawdzanie 4 dfs (7), dlatego krawędź
Sprawdzanie 5 / 6->7 prowadzi w górę
zentuje błąd, który trzeba naprawić. Nie
Sprawdzanie 6
ma przy tym znaczenia forma planu za dfs(7) /
dań. Tak więc aplikacja do szeregowania | Sprawdzanie 6
Wszystkie krawędzie
zadań wykonuje trzy kroki: 7 Gotowy
prowadzą w górę. Należy
dfs(8)
° Określa zadania i ograniczenia obrócić kolejność, aby ł
Sprawdzanie 7 uzyskać porządek ,-jk
pierwszeństwa. 8 Gotowy topologiczny
° Sprawdza, czy istnieje rozwiązanie; Sprawdzanie 9
Sprawdzanie 10 t
w tym celu wykrywa i usuwa cykle Odwrócony porządek postorder
Sprawdzani e 11
odpowiada odwróconej kolejności,
w grafie dopóty, dopóki nie zlikwi Sprawdzani e 12 w jakiej wierzchołki stają się
duje ostatniego. „gotowe" (należy czytać od dołu)
° Rozwiązuje problem szeregowania,
O d w ró c o n y p o rz ą d e k p o s to r d e r w g ra fie DAG
stosując sortowanie topologiczne. o d p o w ia d a s o rto w a n iu to p o lo g ic z n e m u
Podobnie po wprowadzeniu zmian w planie można sprawdzić go pod kątem cykli
(za pom ocą klasy Di rectedCycl e), a następnie ustalić nowy plan (za pom ocą klasy
Topological).
596 R O ZD ZIA Ł 4 □ Grafy
S iln a s p ó j n o ś ć w d i g r a f a c h Staraliśmy się zachować rozróżnienie między osią-
galnością w digrafach a połączeniami w grafach nieskierowanych. W grafie nieskie-
rowanym dwa wierzchołki v i w są połączone, jeśli istnieje łącząca je
O ścieżka. Można wykorzystać tę ścieżkę do przejścia z v do wlub z w do v.
Natomiast w digrafie wierzchołek wjest osiągalny z wierzchołka v, je
żeli istnieje ścieżka skierowana z v do w, przy czym nie oznacza to, że
istnieje ścieżka skierowana z powrotem z w do v. Aby uzupełnić om ó
wienie digrafów, rozważmy naturalny odpowiednik połączeń z grafów
nieskierowanych.
Definicja. Dwa wierzchołki v i w są silnie połączone, jeśli każdy
jest osiągalny z drugiego (czyli jeśli istnieją ścieżki skierowane z v
do w i z w do v). Digraf jest silnie spójny, jeśli wszystkie wierzchołki
są silnie połączone.
Na rysunku po lewej stronie pokazano kilka przykładowych silnie
spójnych grafów. Jak widać, istotną rolę odgrywają tu cykle. Po przypo
mnieniu, że ogólny cykl skierowany to taki cykl skierowany, w którym
wierzchołki mogą się powtarzać, łatwo dostrzec, iż dwa wierzchołki są
silnie połączone wtedy i tylko wtedy, jeśli istnieje ogólny cykl skierowany
obejmujący je oba. [Dowód: utwórz ścieżki z v do w i z w do v).
Silnie spójne składow e Podobnie jak połączenia w grafach nieskiero
wanych, tak i silne połączenia w digrafach wyznaczają relację równo
ważności na zbiorze wierzchołków, ponieważ mają następujące cechy:
■ zwrotność — każdy wierzchołek v jest silnie połączony z samym sobą;
u symetryczność — jeśli v jest silnie połączony z w, to wjest silnie połączony z v;
■ przechodniość — jeśli v jest silnie połączony z w, a w jest silnie połączony z x,
to v jest silnie połączony z x.
Silne połączenie jest relacją równoważności, dlatego dzieli wierzchołki na klasy rów
noważności. Klasy te to maksymalne podzbiory wierzchołków silnie połączonych ze
sobą, przy czym każdy wierzchołek znajduje się w dokładnie jednym podzbiorze.
Te podzbiory nazywamy silnie spójnymi składowymi lub, krótko, silnymi składowymi.
Przykładowy digraf [Link] ma pięć silnie spójnych składowych, co pokazano na
rysunku po prawej stronie. Digraf o V wierzchoł
kach ma od 1 do V silnie spójnych składowych.
Silnie spójny digraf m a 1 silnie spójną składową,
a graf DAG m a V silnie spójnych składowych.
Zauważmy, że silnie spójne składowe są definio
wane w kategoriach wierzchołków, a nie krawę
dzi. Niektóre krawędzie łączą dwa wierzchołki
w tej samej silnie spójnej składowej. Inne łączą
4.2 Q Grafy skierowane 597
wierzchołki z różnych silnie spójnych składowych. Te ostatnie krawędzie nie wystę
pują w cyklach skierowanych. Podobnie jak wykrywanie spójnych składowych jest
często ważne przy przetwarzaniu grafów nieskierowanych, tak identyfikowanie silnie
spójnych składowych ma nieraz znaczenie przy przetwarzaniu digrafów.
Przykładow e zastosow ania Silna spójność to użyteczna abstrakcja, pomagająca
zrozumieć strukturę digrafu i wyznaczająca powiązane zbiory wierzchołków (silnie
spójne składowe). Przykładowo, silnie
spójne składowe mogą pomóc autorom Zastosowanie Wierzchołek Krawędź
podręcznika ustalić, które tematy warto
Sieć W W W Strona Odnośnik
połączyć, aprogram istom zdecydować, jak
uporządkować moduły programu. Na ry Podręcznik Temat Odwołanie
sunku poniżej pokazano przykład z dzie Oprogramowanie Moduł Wywołanie
dziny ekologii. Przedstawiony digraf to
model łańcucha pokarmowego łączącego Łańcuch Relacja
Organizm
organizmy. Wierzchołki reprezentują tu pokarmowy drapieżnilc-ofiara
gatunki, a krawędź z jednego wierzchołka Typowe zastosowania silnie spójnych składowych
do drugiego oznacza, że przedstawiciele
gatunku reprezentowanego przez wierzchołek wyjściowy są zjadane przez organizmy
z gatunku reprezentowanego przez wierzchołek docelowy. Badania naukowe oparte
na takich digrafach (ze starannie dobranymi zbiorami gatunków i dobrze udokum en
towanymi relacjami) odgrywają ważną rolę, ponieważ pomagają ekologom odpowie
dzieć na podstawowe pytania na tem at systemów ekologicznych. Silnie spójne skła
dowe w takich digrafach ułatwiają ekologom zrozumienie przepływu energii w łańcu
chu pokarmowym. Na rysunku
ze strony 603 pokazano digraf
reprezentujący zawartość sieci
WWW. Wierzchołki odpowia
dają tu stronom, a krawędzie —
odnośnikom między stronami.
Silnie spójne składowe w takim
digrafie pomagają inżynierom
sieci dzielić duże liczby stron
z sieci W W W w porcje o wiel
kości umożliwiającej przetwa
rzanie. Inne zagadnienia z po
dobnych obszarów i inne przy
kłady omówiono w ćwiczeniach
oraz w witrynie.
598 R O ZD ZIA Ł 4 Grafy
Potrzebny jest poniższy interfejs API. Jest to przeznaczony dla digrafów odpowiednik
klasy CC (strona 555).
public c la ss SCC
SC C (D igraph G) Konstruktor ze wstępnym przetwarzaniem
boolean stro n glyC o n n e cte d (in t v, in t w) Czy v i w są silnie połączone?
in t count() Liczba silnie spójnych składowych
Identyfikator składowej obejmującej v
in t i d ( i n t v) (wartość między 0 a count () -1)
Interfejs API do wyznaczania silnie spójnych składowych
Nietrudno opracować algorytm kwadratowy do wyznaczania silnie spójnych składo
wych (zobacz ć w i c z e n i e 4 .2 .2 3 ), jednak — jak zwykle — wymagania czasowe i pamię
ciowe rosnące w tempie kwadratowym uniemożliwiają przetwarzanie dużych digrafów,
występujących w praktycznych zastosowaniach, takich jak wcześniej opisane.
Algorytm Kosaraju W klasie CC ( a l g o r y t m 4.3 ze strony 556) pokazano, że wyzna
czanie spójnych składowych w grafach nieskierowanych to proste zastosowanie prze
szukiwania w głąb. W jaki sposób można wydajnie określać silnie spójne składowe
w digrafach? Co ciekawe, w klasie KosarajuSCC, przedstawionej na następnej stronie,
udało się wykonać to zadanie przez dodanie do klasy CC tylko kilku wierszy kodu.
n Do digrafu G należy zastosować klasę DepthFi rstO rder w celu ustalenia odwró
conego porządku postorder na odwrotności grafu — GR.
■ Należy uruchomić standardową metodę DFS na digrafie G, przy czym nieozna
czone wierzchołki trzeba pobierać w ustalonej wcześniej kolejności, a nie we
dług numerów.
■ Wszystkie wierzchołki osiągnięte w wywołaniach rekurencyjnej metody dfs ()
z konstruktora znajdują się w silnie spójnej składowej (!), dlatego należy ziden
tyfikować je w taki sposób, jak w klasie CC.
DFS dla G ( K o s a r a j u S C C ) DFS dla G" ( D e p t h F i r s t O r d e r )
Zakładamy, że v
jest osiągalny z s, v musi być \ G R musi
dfs(s)
dlatego G musi d fs C v ) „gotowy" d fs(D obejmować
obejmować przed s. Inaczej ścieżkę z s do v
dfs(v) ścieżkę z s do v ,, , wywołanie dfsfv)
v Gotowy-«— , '
7 dfs(v)
; / znalazłoby \ '
v Gotowy dfsCs) / sięp rzed \ _ v Gotowy
J dfs(s) w G \ ■
s Gotowy s Gotowy s Gotowy
i ! i
Niemożliwe, ponieważ G B
obejmuje ścieżkę z v do s
Dowód poprawności algorytmu Kosaraju
4.2 Grafy skierowane 599
ALGORYTM 4.6. Algorytm Kosaraju do wyznaczania silnych składowych
public c la s s KosarajuSCC
{
private boolean[] marked; // Oznaczone w ierzchołki,
private i n t [] id; // Identyfikatory składowych,
private in t count; // Liczba s iln y c h składowych.
public KosarajuSCC(Digraph G)
{
marked = new b oolean[G .V ()];
id = new i n t [ G . V ( ) ] ;
DepthFirstOrder order = new D e p th F irs tO rd e r(G .re v e rs e Q );
f o r (in t s : [Link]())
i f (!marked[s])
{ dfs(G, s ) ; count++; }
i % java KosarajuSCC tin yD G .tx t
lic z b a składowych: 5
p rivate void dfs(Digraph G, in t v) l
0 5 4 3 2
{
11 12 9 10
marked [v] = true;
6
id[v] = count; 8 7
fo r (in t w : [Link](v))
i f (! marked [w])
dfs(G, w);
public boolean stronglyConnected(int v, in t w)
{ return id[v] == id [w]; }
publ ic in t i d ( int v)
{ return i d [ v ] ; }
public in t count()
{ return count; }
}
Ta implementacja różni się od kodu klasy CC ( a l g o r y t m 4 .3 ) tylko wyróżnionym kodem
(i implementacją metody mai n (), w której użyto kodu ze strony 555 ze słowem Graph zmie
nionym na Di graph i nazwą CC zmodyfikowaną na Kosaraj uSCC). A by znaleźć silnie spójne
składowe, program wykonuje przeszukiwanie w głąb na odwróconym digrafie w celu wyzna
czenia porządku wierzchołków (odwróconego porządku postorder określonego przy prze
szukiwaniu) wykorzystywanego do przeszukiwania w głąb danego digrafu.
RO ZD ZIA Ł 4 □ Grafy
Algorytm Kosaraju to skrajny przykład metody, której kod łatwo napisać, ale trud
no zrozumieć. Kod jest wprawdzie tajemniczy, ale jeśli prześledzisz krok po kroku
dowód poniższego twierdzenia na podstawie rysunku ze strony 598, przekonasz się,
że algorytm jest poprawny.
T w ierdzenie H. W metodzie DFS uruchomionej dla digrafu G, w której ozna
czone wierzchołki są przetwarzane w odwróconym porządku postorder wyzna
czonym przez m etodę DFS uruchom ioną dla odwrotności tego digrafu, GR (algo
rytm Kosaraju), wierzchołki odwiedzone w każdym wywołaniu m etody rekuren-
cyjnej z konstruktora znajdują się w silnie spójnej składowej.
D ow ód. W pierwszym kroku dowodzimy przez zaprzeczenie, że każdy wierz
chołek v silnie połączony z s zostanie odwiedzony w wyniku wywołania w kon
struktorze instrukcji dfs (G, s). Załóżmy, że wierzchołek v silnie połączony z s
nie zostanie odwiedzony w wyniku takiego wywołania. Ponieważ istnieje ścieżka
z s do v, v musiał zostać wcześniej oznaczony. Jednak istnieje ścieżka z v do s,
dlatego s został oznaczony w wyniku wywołania dfs(G, v), tak więc konstruktor
nie mógł wywołać instrukcji dfs (G, s) — występuje sprzeczność.
Po drugie, dowodzimy, że każdy wierzchołek v odwiedzony w wyniku wywoła
nia w konstruktorze instrukcji dfs (G, s ) jest silnie połączony z s. Niech v będzie
wierzchołkiem odwiedzonym w wyniku wywołania dfs(G, s). Oznacza to, że
w G istnieje ścieżka z s do v, dlatego trzeba udowodnić tylko tyle, iż w G istnieje
ścieżka z v do s. To stwierdzenie jest równoważne temu, że w GR istnieje ścieżka
z s do v, wystarczy więc to udowodnić.
Istotą dowodu jest to, że z procesu tworzenia odwróconego porządku posto
rder wynika, iż w czasie stosowania m etody DFS do GR wywołanie dfs(G, v)
miało miejsce przed dfs (G, s). Dla wywołania dfs(G, v) trzeba więc rozważyć
tylko dwa przypadki. Wywołanie mogło mieć miejsce:
■ przed dfs(G, s) (a ponadto zostało zakończone przed wywołaniem dfs (G, s ) );
* podfs(G , s) (i zostało zakończone przed zakończeniem dfs (G, s )).
Pierwsza sytuacja jest niemożliwal, ponieważ w GRistnieje ścieżka z v do s. Z dru
giego przypadku wynika, że w G" istnieje ścieżka z s do v, co kończy dowód.
Na następnej stronie pokazano ślad działania algorytmu Kosaraju na pliku tinyDG.
txt. Na prawo od każdego śladu działania metody DFS widoczny jest rysunek digrafu.
Porządek wierzchołków odpowiada kolejności, w jakiej stają się „gotowe”. Tak więc od
czytanie w górę odwróconego digrafu po lewej stronie daje odwrócony porządek posto
rder, czyli kolejność, w jakiej nieoznaczone wierzchołki są sprawdzane w metodzie DFS
uruchomionej dla pierwotnego digrafu. Jak widać na rysunku, w drugim uruchomieniu
metody DFS ma miejsce wywołanie d f s ( l) (oznaczany jest wierzchołek 1), następnie
wywołanie dfs (0) (oznaczane są 5, 4, 3 i 2), potem sprawdzenie 2, 4, 5 i 3, później wy
wołanie dfs (11) (oznaczane są 11,12, 9 i 10), sprawdzenie 9,12 i 10, wywołanie dfs (6)
(oznaczany jest wierzchołek 6), a na końcu — wywołanie dfs (7) (oznaczane są 7 i 8).
4.2 Q Grafy skierowane 601
odwróconym digrafie (R e v erse P o st) DF5 na pierw otnym digrafie
,awdzanie nieoznaczonych wierzchołków w kolejności Sprawdzanie nieoznaczonych wierzchołków w kolejności
/ i 2 3 4 5 6 7 8 9 10 11 12 1 0 2 4 5 3 11 9 12 10 6 7 8
f dfs(l)
dfs(O )
dfs(6) V 1 Gotowy
I dfs(7:> /T J n m r*
dfs(5)
I dfsW
| sprawdzanie 7
/ ^
/ (7) dfs(4)
8 Gotow y / J dfs(3)
1 7 Gotowy / /CN Sprawdzanie 5
6 Gotowy dfs(2)
dfs(2) / V \ i Sprawdzanie 0
dfs(4) / ^ -n \ \ 1 Sprawdzanie 3
d f s ( ll) / no) \ \ 2 Gotowy
df s(9) \ \ \ 3 Gotowy
dfs(12) \ 1 JL \ \ \ Sprawdzanie 2
S p ra w d z a n ie 1 1 \ \ ( 1 2 ) ) \ \ 4 Gotowy Silnie
dfs(10) 5 Gotowy spójne
| sprawdzani Sprawdzanie 1 składowe
10 Gotowy V,0 Gotowy ,
12 Gotowy Sprawdzanie 2
sprawdzanie 8 Sprawdzanie 4
Sprawdzanie 6 Sprawdzanie 5
9 Gotowy Sp ra w d za n i e 3_________
11 Gotowy C dfs(1 1 ) ^ '
Sprawdzanie 6 Sprawdzanie 4
dfs(5) dfs(1 2 )
dfs(3) dfs(9)
I Sprawdzanie 4 I Sprawdzanie 11
I Sprawdzanie 2 | dfs(1 0 )
3 Gotowy i I Sprawdzanie 12
Sprawdzanie 0 1 10 Gotowy
5 Gotowy 9 Gotowy
4 Gotowy 12 Gotowy
Sprawdzanie 3 y n Gotowy „
2 Gotowy Sprawdzanie 9
0 Gotowy Sprawdzanie 12
dfs(l) Sprawdzanie 10
Sprawdzanie 0 C dfs(6)
1 Gotowy Sprawdzanie 9
Sprawdzanie 2 Sprawdzanie 4
Sprawdzanie 3 Sprawdzanie 0
Sprawdzanie 4 V 6 Gotowy J
Sprawdzanie 5 Odwrócony porządek f d f s (7) >
Sprawdzanie 6 postorder na potrzeby Sprawdzanie 6
Sprawdzanie 7 drugiego wywołania d f s ( ) dfs(8)
Sprawdzanie 8 (należy czytać od dołu) | sprawdzanie 7
Sprawdzanie 9 I Sprawdzanie 9
Sprawdzanie 10 8 Gotowy
Sprawdzanie 11 7 Gotowy _______
Sprawdzanie 12 Sprawdzanie 8
A lg o ry tm Kosaraju d o zn a jd o w a n ia silnie sp ó jn y ch s k ła d o w y c h w d igrafach
m
602 R O ZD ZIA Ł 4 a Grafy
Na następnej stronie pokazano większy przykład — bardzo mały podzbiór digrafu
będącego modelem sieci WWW.
a lg o ry tm k o s a ra ju r o z w i ą z u j e o p is a n y p o n iż e j o d p o w ie d n ik p r o b le m u o k r e
ś la n ia p o łą c z e ń w g ra fa c h n ie s k ie ro w a n y c h , p r z e d s ta w io n e g o po raz p ie rw s z y
w r o z d z i a l e i . i p o n o w n ie p rz y to c z o n e g o w p o d r o z d z i a l e 4.1 ( s t r o n a 546).
Silne połączenia. Na podstawie digrafu zapewnij obsługę zapytań w postaci: Czy
dwa podane wierzchołki są silnie połączone? i Ile silnie spójnych składowych obej
muje dany digrafł
To, czy można rozwiązać omawiany problem dla digrafów równie wydajnie, jak ana
logiczny problem określania połączeń w grafach nieskierowanych, przez pewien czas
było kwestią otwartą (problem rozwiązał R.E. Tarjan pod koniec lat 70. ubiegłego
wieku). Powstanie tak prostego rozwiązania, jak obecnie stosowane, było pewnym
zaskoczeniem.
Twierdzenie I. W stępne przetwarzanie w algorytmie Kosaraju wymaga czasu
i pamięci w ilości proporcjonalnej do V+£, a zapewnia obsługę w stałym czasie
zapytań dotyczących silnych połączeń w digrafie.
Dowód. Algorytm oblicza odwrotność digrafu i dwukrotnie przeprowadza
przeszukiwanie w głąb. Każdy z tych trzech kroków odbywa się w czasie pro
porcjonalnym do V+E. Pamięć na odwrotną kopię digrafu jest proporcjonalna
do V+E.
Osiągalnośćpo raz w tóry Za pom ocą klasy CC dla grafów nieskierowanych na pod
stawie tego, że wierzchołki v i wsą połączone, można wywnioskować, iż istnieje ścież
ka z v do w i (ta sama) ścieżka z w do v. Przy użyciu klasy KosarajuCC na podstawie
faktu, że v i w są silnie połączone, m ożna wywnioskować, iż istnieje ścieżka z v do
w i (inna) ścieżka z w do v. Co jednak z param i wierzchołków, które nie są silnie połą
czone? Możliwe, że istnieje ścieżka z v do wlub z w do v, a nie obie z nich.
Osiągalność dla dowolnej pary. Dla digrafu zapewnij obsługę pytań w postaci:
Czy istnieje ścieżka skierowana z danego wierzchołka v do innego wierzchołka w?
W grafach nieskierowanych analogiczny problem jest równoznaczny z problemem
określania połączeń. W przypadku digrafów ten problem różni się od problemu
określania silnych połączeń. W implementacji klasy CC zastosowano wstępne prze
twarzanie w czasie liniowym, aby zapewnić odpowiadanie w stałym czasie na tego
rodzaju zapytania dla grafów nieskierowanych. Czy można uzyskać podobną wydaj
ność dla digrafów? Nad tym na pozór niewinnym pytaniem eksperci zastanawiali się
przez dziesięciolecia. Aby lepiej zrozumieć trudność zadania, przyjrzyj się rysunkowi
na stronie 604, stanowiącemu ilustrację następującego podstawowego zagadnienia.
4.2 ei Grafy skierowane 603
604 RO ZD ZIA Ł 4 □ Grafy
Definicja. Domknięcie przechodnie digra-
fu G to inny digraf, o tym samym zbiorze
wierzchołków, przy czym krawędź z v do
wistnieje w domknięciu przechodnim wtedy
p 4 ( ^T \ \ c i tylko wtedy, jeśli w G wjest osiągalne z v.
Zgodnie z konwencją każdy wierzchołek
0 1 2 3 4 5 6 7 9 10 11 12
0
jest osiągalny z niego samego, dlatego do
1 mknięcie przechodnie ma V pętli własnych.
Pierwotna W przykładowym diagram ie istnieje tylko 13
2 12 jest
krawędź
3 (na czerwono) osiągalny krawędzi skierowanych, jednak domknięcie
z6
4 Pętla własna przechodnie obejmuje 102 ze 169 możliwych
5 (na szaro) krawędzi tego rodzaju. Ogólnie dom knięcie
6 przechodnie digrafu m a wiele więcej krawę
7 dzi niż sam digraf. Nieraz zdarza się, że do
8 mknięcie przechodnie grafu rzadkiego jest
9
gęste. Przykładowo, dom knięcie przechodnie
10
cyklu skierowanego o V wierzchołkach, obej
11
mujące V krawędzi skierowanych, jest digra-
12
fem pełnym, o V2 krawędziach skierowanych.
Domknięcie przechodnie
Ponieważ dom knięcia przechodnie są zwykle
gęste, standardowo przedstawiamy je za p o
m ocą macierzy wartości logicznych. Element w wierszu v i kolum nie w m a wartość
tru e wtedy i tylko wtedy, jeśli wjest osiągalne z v. Zam iast bezpośrednio wyznaczać
dom knięcie przechodnie, używamy przeszukiwania w głąb do zaimplementowania
następującego interfejsu API.
p u b lic c la s s T ra n sitiv e C lo s u re
T ra n sitiv e C lo su re (D ig ra p h G) Konstruktor ze wstępnym przetwarzaniem
boolean re a c h a b le (in t v, in t w) Czy wjest osiągalny z v?
Interfejs API do określania osiągalności dla dowolnych par
Kod w górnej części następnej strony to prosta implementacja oparta na klasie
DirectedDFS ( a l g o r y t m 4 .4 ). Rozwiązanie idealnie nadaje się dla małych lub gę
stych digrafów, jednak już nie dla dużych digrafów, które mogą wystąpić w praktyce.
Konstruktor wymaga pamięci w ilości proporcjonalnej do V2 i czasu proporcjonalnego
do V (V+E). Każdy z V obiektów Di rectedDFS zajmuje pamięć w ilości proporcjonal
nej do V (każdy obejmuje tablicę marked [] o rozmiarze V i sprawdza E krawędzi przy
4.2 b Grafy skierowane 605
oznaczeniu wierzchołków). Klasa TransitiveC1osure oblicza i zapisuje domknięcie
przechodnie digrafu G oraz zapewnia obsługę zapytań w stałym czasie. Wiersz v
w macierzy domknięcia przechodniego to tablica marked[] v-tego elementu z tabli
cy Di rectedDFS [] z klasy Transi t i veC1 osure. Czy m ożna zapewnić obsługę zapytań
w stałym czasie, wykonując wstępne przetwarza
nie w znacząco krótszym czasie i wykorzystując p u b lic c la s s T ra n sit iv e C lo s u re
istotnie mniej pamięci? Ogólne rozwiązanie, któ f
re obsługuje zapytania w stałym czasie, natomiast p riv a te D ire cte d D FS[] a l l ;
T ra n sitiv e C lo su re (D ig ra p h G)
wymaga pamięci rosnącej znacząco wolniej niż
{
kwadratowo, nie zostało dotąd wymyślone. Ma a ll = new Di rectedDFS[G.V () ];
to ważne skutki praktyczne. Do czasu opracowa fo r (in t v = 0; v < G .V (); v++)
a ll[ v ] = new DirectedDFS(G, v ) ;
nia rozwiązania nie m ożna na przykład liczyć na
}
poradzenie sobie z problemem osiągalności dla
dowolnych par w bardzo dużych digrafach, ta boolean re a c h a b le (in t v, in t w)
{ return a l 1 [v].m arked(w ); }
kich jak graf sieci WWW.
1
O siągalność dla dowolnych par
606 ROZDZIAŁ 4 a Grafy
Podsumowanie W tym podrozdziale przedstawiliśmy krawędzie skierowane i di-
grafy z naciskiem na relacje między przetwarzaniem digrafów a analogicznymi proble
mami dotyczącymi grafów nieskierowanych. Poruszyliśmy następujące zagadnienia:
■ słownictwo dotyczące digrafów;
■ spostrzeżenie, że reprezentacja i techniki są w zasadzie takie same, jak dla gra
fów nieskierowanych, natomiast niektóre problemy dotyczące digrafów są bar
dziej skomplikowane;
* cykle, grafy DAG, sortowanie topologiczne i szeregowanie z ograniczeniami
pierwszeństwa;
■ osiągalność, ścieżki i silne połączenia w digrafach.
W tabeli poniżej znajduje się podsumowanie implementacji omówionych algoryt
mów przetwarzania digrafów (wszystkie algorytmy oprócz jednego oparto na prze
szukiwaniu w głąb). Opisy wszystkich poruszonych problemów są proste, natomiast
rozwiązania wahają się od prostych adaptacji analogicznych algorytmów dla grafów
nieskierowanych po pomysłowe i zaskakujące metody. Przedstawione algorytmy
są punktem wyjścia do kilku bardziej zaawansowanych algorytmów, omówionych
w p o d r o z d z i a l e 4 .4 , poświęconym digrafom ważonym.
Problem Rozwiązanie Odwołanie
Osiągalność z jednego źródła i z wielu źródeł Di rectedDFS Strona 583
Ścieżki skierowane z jednego źródła DepthFi rs t D i rectedPaths Strona 585
Najkrótsze ścieżki skierowane z jednego źródła BreadthFi rstD i rectedPaths Strona 585
Wykrywanie cykli skierowanych D irectedCycle Strona 589
Porządki wierzchołków przy przeszukiwaniu w głęb DepthFi rstO rd e r Strona 592
Szeregowanie z ograniczeniami pierwszeństwa Topologi cal Strona 593
Sortowanie topologiczne Topological Strona 593
Silne połączenia KosorajuSCC Strona 599
Osiągalność dla dowolnych par T r a n s it i veClosure Strona 605
Problemy z obszaru przetwarzania digrafów om ów ione w podrozdziale
4.2 n Grafy skierowane 607
[ PYTANIA I ODPOWIEDZI
P. Czy pętla własna jest cyklem?
O. Tak, jednak pętla własna nie jest konieczna, aby wierzchołek był osiągalny z niego
samego.
608 ROZDZIAŁ 4 a Grafy
I ĆWICZENIA
4.2.1 . Jaka jest maksymalna liczba krawędzi w digrafie o V wierzchołkach i bez kra
wędzi równoległych? Jaka jest m inim alna liczba krawędzi w digrafie o V wierzchoł
kach, z których żaden nie jest izolowany?
4.2.2. Narysuj w sposób specyficzny dla rysunków z tekstu
12
16 (strona 536) listy sąsiedztwa budowane przez oparty na stru
8 4 m ieniu wejściowym konstruktor klasy Digraph na podstawie
2 3
1 11 pliku [Link], który przedstawiono po lewej stronie.
0 6
36 4.2.3. Utwórz dla klasy Di graph konstruktor kopiujący, któ
10 3 ry jako dane wejściowe przyjmuje digraf G oraz tworzy i ini
7 11
7 8 cjuje nową kopię digrafu. Wszelkie zmiany wprowadzane
11 8 przez klienta w G nie powinny wpływać na nowo utworzony
20
62 digraf.
52
5 10 4.2.4. Dodaj do klasy Digraph metodę hasEdge(). Metoda ma
3 10 przyjmować dwa argumenty typu i nt, v i w, oraz zwracać true,
81
4 1 jeśli w grafie istnieje krawędź v->w (w przeciwnym razie ma
zwracać fal se).
4.2.5. Zmodyfikuj klasę Digraph tak, aby krawędzie równoległe i pętle własne były
niedozwolone.
4.2.6. Opracuj klienta testowego dla klasy Di graph.
4.2.7. Stopień wejściowy wierzchołka w digrafie to liczba krawędzi skierowanych
prowadzących do tego wierzchołka, natomiast stopień wyjściowy to liczba krawę
dzi skierowanych wychodzących z wierzchołka. Żaden wierzchołek nie jest osią
galny z wierzchołka o stopniu wyjściowym 0 (taki wierzchołek nazywamy ujściem).
Wierzchołek o stopniu wejściowym 0 (nazywamy go źródłem) nie jest osiągalny
z żadnego innego wierzchołka. Digraf, w którym dozwolone są pętle własne i każdy
wierzchołek ma stopień wyjściowy jeden, to odwzorowanie (funkcja ze zbioru liczb
całkowitych od 0 do V-1 na nie same). Napisz program [Link], będący imple
mentacją poniższego interfejsu API.
p u b lic c la s s Degrees
Degrees (Di graph G) Konstruktor
i nt in d e g re e (in t v) Zwraca stopień wejściowy wierzchołka v
in t o u tdegre efint v) Zwraca stopień wyjściowy wierzchołka v
Ite ra b le < In te g e r> s o u r c e s () Zwraca źródła
Ite ra b le < In te g e r> s in k s ( ) Zwraca ujścia
boolean isMap() Czy Sjest odwzorowaniem?
4.2 n Grafy skierowane 609
4 .2.8. Narysuj wszystkie nieizomorficzne grafy DAG o dwóch, trzech, czterech
i pięciu wierzchołkach (zobacz ć w i c z e n i e 4 . 1 .28 ).
4.2.9. Napisz metodę, która sprawdza, czy dana permutacja wierzchołków grafu
DAG jest porządkiem topologicznym tego grafu.
4.2.10. Na podstawie grafu DAG ustal, czy istnieje porządek topologiczny, którego
nie można uzyskać przez zastosowanie algorytmu opartego na DFS niezależnie od
kolejności wybierania sąsiednich wierzchołków. Udowodnij odpowiedź.
4.2.11. Opisz rodzinę digrafów rzadkich, w których liczba cykli skierowanych roś
nie wykładniczo względem liczby wierzchołków.
4.2.12. Ile krawędzi istnieje w domknięciu przechodnim digrafu będącego prostą
ścieżką skierowaną o V wierzchołkach i V - 1 krawędziach?
4.2.13. Podaj domknięcie przechodnie digrafu o 10 wierzchołkach i następujących
krawędziach:
3->7 l->4 7->8 0->5 5->2 3->8 2->9 0->6 4->9 2->6 6->4
4.2.14. Udowodnij, że silnie spójne składowe grafu GRsą takie same, jak w grafie G.
4.2.15. Jak wyglądają silnie spójne składowe grafów DAG?
4.2.16. Co się stanie po uruchom ieniu algorytmu Kosaraju dla grafu DAG?
4.2.17. Odwrócony porządek postorder dla odwrotności grafu jest taki sam, jak po
rządek postorder dla grafu — prawda czy fałsz?
4.2.18. Oblicz zapotrzebowanie pamięciowe klasy Digraph o V wierzchołkach i E
krawędziach, posługując się modelem kosztów pamięciowych z p o d r o z d z i a ł u 1 .4 .
610 ROZDZIAŁ 4 n Grafy
! PROBLEMY DO ROZWIĄZANIA
4.2.19. Sortowanie topologiczne i BFS. Wyjaśnij, dlaczego opisany dalej algorytm
niekoniecznie wyznacza porządek topologiczny. Algorytm działa tak: uruchomienie
metody BFS i oznaczenie wierzchołków w porządku rosnącym według odległości od
źródła.
4.2.20. Skierowany cykl eulerowski. Cykl eulerowski to cykl skierowany, w któ
rym każda krawędź występuje dokładnie raz. Napisz korzystającego z klasy Graph
klienta Eul er, który znajduje cykl eulerowski lub informuje, że taki cykl nie istnieje.
Wskazówka : udowodnij, że digraf G obejmuje cykl eulerowski wtedy i tylko wtedy,
jeśli G jest spójny, a stopień wejściowy każdego wierzchołka jest równy jego stopnio
wi wyjściowemu.
4 .2.2 1 . Najbliższy wspólny przodek w grafach DAG. Na podstawie grafu DAG i dwóch
wierzchołków, v i w, znajdź najbliższego wspólnego przodka (ang. lowest common
ancestor — LCA) tych wierzchołków. LCA wierzchołków v i w to taki ich przodek,
który nie ma potomków będących przodkami v i w. Wyznaczanie przodka LCA jest
przydatne w językach programowania (przy wielodziedziczeniu), w analizie danych
genealogicznych (przy znajdowaniu poziomu chowu wsobnego w grafie reprezentu
jącym rodowód) i w innych obszarach. Wskazówka-, zdefiniuj wysokość wierzchołka
v w grafie DAG jako długość najdłuższej ścieżki z korzenia do v. W śród wierzchoł
ków będących przodkam i v i wprzodkiem LCA jest ten o największej wysokości.
4.2.22. Najkrótsza ścieżka przez przodka. Na podstawie grafu DAG i dwóch wierz
chołków, v i w, znajdź najkrótszą ścieżkę przez przodka między nimi. Ścieżka przez
przodka między v i wobejmuje wspólnego przodka x, a składa się z najkrótszej ścieżki
z v do x i najkrótszej ścieżki z wdo x. Najkrótsza ścieżka przez przodka to taka ścieżka
przez przodka, której łączna długość jest zminimalizowana. Rozgrzewka: znajdź graf
DAG, w którym najkrótsza ścieżka przez przodka prowadzi przez wspólnego przod
ka x, który nie jest przodkiem LCA. Wskazówka: uruchom dwukrotnie metodę BFS
— raz dla v i raz dla w.
4.2.23. Silnie spójna składowa. Opisz działający w czasie liniowym algorytm do wy
znaczania silnie spójnej sIdadowej, obejmującej dany wierzchołek v. Na podstawie
tego algorytmu opisz prosty algorytm kwadratowy do wyznaczania silnie spójnych
składowych digrafu.
4.2.24. Ścieżki hamiltonowskie w grafach DAG. Na podstawie grafu DAG zaprojektuj
działający w czasie liniowym algorytm do określania, czy istnieje ścieżka skierowana
przechodząca przez każdy wierzchołek dokładnie raz.
Odpowiedź: wykonaj sortowanie topologiczne i sprawdź, czy istnieje krawędź między
każdą kolejną parą wierzchołków w porządku topologicznym.
4.2 a Grafy skierowane 611
4.2.25. Unikatowy porządek topologiczny. Zaprojektuj algorytm do określania, czy
digraf ma unikatowy porządek topologiczny. Wskazówka: digraf ma unikatowy po
rządek topologiczny wtedy i tylko wtedy, jeśli istnieje krawędź skierowana między
każdą parą kolejnych wierzchołków w porządku topologicznym (czyli gdy digraf
obejmuje ścieżkę hamiltonowską). Jeżeli digraf ma wiele porządków topologicznych,
drugi taki porządek m ożna uzyskać, przestawiając parę kolejnych wierzchołków.
4.2.26. Problem spełnialności dla klauzul o dwóch literałach. Na podstawie równania
logicznego w koniunkcyjnej postaci normalnej o M klauzulach i N literałach, przy
czym każda klauzula obejmuje dokładnie dwa literały, ustal przypisanie spełniające
równanie (jeśli taicie istnieje). Wskazówka: utwórz digraf implikacji o 2N wierzchoł
kach (po jednym na literał i jego negację). Dla każdej klauzuli x + y uwzględnij kra
wędzie z y do x i z x do y. Aby klauzula x + y była spełniona, (i) jeśli y jest fałszywe,
x ma być prawdziwe oraz (ii) jeśli x jest fałszywe, y musi być prawdziwe. Twierdzenie:
równanie jest spełnialne wtedy i tylko wtedy, jeśli żadna zmienna x nie znajduje się
w tej samej silnie spójnej składowej, co jej negacja x. Ponadto spełniającym równanie
przypisaniem jest porządek topologiczny dla grafu DAG jądra (powstaje on przez
sprowadzenie każdej silnie spójnej składowej do pojedynczego wierzchołka).
4.2.27. Wyliczanie digrafów. Pokaż, że liczba różnych digrafów o V wierzchołkach
i bez krawędzi równoległych wynosi 21 . Ile istnieje digrafów obejmujących V wierz
chołków i E krawędzi? Następnie ustal górne ograniczenie procentu digrafów o 20
wierzchołkach, które m ożna będzie kiedykolwiek zbadać. Zakładamy, że każdy elek
tron we wszechświecie co nanosekundę sprawdza digraf, a wszechświat obejmuje
mniej niż 10 80 elektronów i przetrwa mniej niż 10 20 lat.
4.2.28. Wyliczanie grafów DAG. Podaj wzór na liczbę grafów DAG o V wierzchoł
kach i E krawędziach.
4.2.29. Wyrażenia arytmetyczne. Napisz klasę przetwarzającą grafy DAG, które re
prezentują wyrażenia arytmetyczne. Użyj tablicy indeksowanej wierzchołkami do
przechowywania wartości odpowiadających każdemu wierzchołkowi. Zakładamy, że
wartości odpowiadające liściom są znane. Opisz rodzinę wyrażeń arytmetycznych
cechujących się tym, że rozmiar drzewa wyrażenia rośnie wykładniczo względem
odpowiedniego grafu DAG (tak więc czas wykonania program u dla grafu DAG jest
proporcjonalny do logarytmu czasu wykonania dla drzewa).
612 ROZDZIAŁ 4 a Grafy
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
4.2.30. Sortowanie topologiczne oparte na kolejce. Opracuj implementację sortowa
nia topologicznego, w której przechowywana jest tablica indeksowana wierzchołka
mi, używana do śledzenia stopnia wejściowego każdego wierzchołka. Zainicjuj tablicę
i kolejkę źródeł w jednym przebiegu przez wszystkie krawędzie, tak jak w ć w i c z e n i u
4 .2 .7 . Następnie do m om entu opróżnienia kolejki źródłowej wykonuj poniższe ope
racje:
■ usuwanie źródła z kolejki i opisywanie go;
■ zmniejszanie w tablicy stopni wejściowych wartości odpowiadających wierz
chołkom docelowym każdej krawędzi z usuniętego wierzchołka;
■ jeśli wartość po zmniejszeniu dochodzi do 0, należy wstawić odpowiadający jej
wierzchołek do kolejki wierzchołków źródłowych.
4.2.31 Digrafy euklidesowe. Zmodyfikuj rozwiązanie ć w i c z e n i a 4 . 1 .37 , aby utwo
rzyć interfejs API Eucl ideanDigraph dla grafów, których wierzchołki są punktami
w przestrzeni. Ma to umożliwić korzystanie z reprezentacji graficznych.
4.2 □ Grafy skierowane 613
i EKSPERYMENTY
4.2.32. Losowe digrafy. Napisz program ErdosRenyiDigraph, który przyjmuje war
tości V i E z wiersza poleceń i tworzy digraf, generując E losowych par liczb całkowi
tych z przedziału od 0 do V-\. Uwaga: generator ten tworzy pętle własne i krawędzie
równoległe.
4.2.33. Losowe digrafy proste. Napisz program RandomDi graph, który przyjmuje war
tości V i E z wiersza poleceń i tworzy — z takim samym prawdopodobieństwem
— każdy z możliwych prostych digrafów o V wierzchołkach i E krawędziach.
4.2.34. Losowe digrafy rzadkie. Zmodyfikuj rozwiązanie ć w i c z e n i a 4 . 1 .4 1 , aby
utworzyć program RandomSparseDi graph. Program ma generować losowe digrafy
rzadkie na podstawie odpowiednio dobranych wartości V i E, tak aby m ożna wyko
rzystać uzyskane digrafy w testach empirycznych.
4.2.35. Losowe digrafy euklidesowe. Zmodyfikuj rozwiązanie ć w i c z e n i a 4 .1 .42 ,
aby utworzyć używającego klasy Eucl i deanDi graph klienta RandomEucl i deanDi graph,
który do każdej krawędzi przypisuje losowy kierunek.
4.2.36. Losowe digrafy oparte na siatce. Zmodyfikuj rozwiązanie ć w i c z e n i a 4 .1 .43 ,
aby utworzyć używającego klasy Eucl i deanDi graph klienta RandomGri dDi graph, który
do każdej krawędzi przypisuje losowy kierunek.
4.2.37. Digrafy w świecie rzeczywistym. Znajdź w internecie duży digraf. Może to
być graf transakcji w systemie elektronicznym lub digraf zdefiniowany na podstawie
odnośników ze stron WWW. Napisz program RandomReal Di graph, który tworzy graf
przez losowe wybranie V wierzchołków i E skierowanych krawędzi z podgrafu opar
tego na tych wierzchołkach.
4.2.38. Graf DAG w świecie rzeczywistym. Znajdź w internecie duży graf DAG. Graf
ten może być wyznaczany przez zależności klasa-definicja w dużym systemie opro
gramowania lub przez odnośniki do katalogów w dużym systemie plików. Napisz
program RandomReal DAG, który tworzy graf przez losowe wybranie V wierzchołków
i E skierowanych krawędzi z podgrafu opartego na tych wierzchołkach.
614 ROZDZIAŁ 4 o Grafy
E K S P E R Y M E N T Y (ciąg dalszy)
Testowanie wszystkich algorytmów i badanie każdego parametru w każdym modelu
grafów jest niewykonalne. Dla każdego z wymienionych dalej problemów napisz klien
ta, który rozwiązuje problem dla dowolnego grafu wejściowego. Następnie wybierz je
den z opisanych wcześniej generatorów do przeprowadzenia eksperymentów dla danego
modelu grafów. Wykorzystaj własną ocenę sytuacji przy doborze eksperymentów (mo
żesz oprzeć się na wynikach wcześniejszych pomiarów). Napisz wyjaśnienie wyników
i wnioski, które można z nich wyciągnąć.
4.2.39. Osiągalność. Przeprowadź eksperymenty, aby empirycznie ustalić średnią
liczbę wierzchołków osiągalnych z losowo wybranego wierzchołka. Uwzględnij róż
ne modele digrafów.
4.2.40. Długości ścieżek w metodzie DFS. Przeprowadź eksperymenty, aby empi
rycznie ustalić prawdopodobieństwo, że program DepthFi rstDi rectedPaths znaj
dzie ścieżkę między dwoma losowo wybranymi wierzchołkami, a także żeby obliczyć
średnią długość znalezionej ścieżki. Uwzględnij różne modele digrafów.
4.2.41. Długości ścieżek w metodzie BFS. Przeprowadź eksperymenty, aby empirycz
nie ustalić prawdopodobieństwo, że program BreadthFi rstDi rectedPaths znajdzie
ścieżkę między dwoma losowo wybranymi wierzchołkami, a także żeby obliczyć
średnią długość znalezionej ścieżki. Uwzględnij różne modele digrafów.
4.2.42. Silnie spójne składowe. Przeprowadź eksperymenty, aby empirycznie usta
lić rozkład liczby silnie spójnych składowych w losowych digrafach różnego typu.
W tym celu wygeneruj dużą liczbę digrafów i narysuj histogram.
4.3. M IN IM A LN E D R Z E W A R O Z P IN A JĄ C E
Graf ważony (inaczej graf z krawędziami ważonymi) oparty jest na modelu, w którym
z każdą krawędzią powiązane są wagi {koszty). Takie grafy są naturalnym modelem
w wielu obszarach. Na mapie lotów, gdzie krawędziom odpowiadają trasy, wagi mogą
reprezentować odległości lub ceny. W obwodzie elektrycznym, gdzie krawędziom
odpowiadają kable, wagi mogą reprezentować długość kabla, jego cenę lub czas prze
syłania sygnału. Naturalnym celem jest wtedy minimalizacja kosztów. W tym pod
rozdziale omawiamy modele nieskierowanych grafów ważonych i badamy algorytmy
dotyczące pewnego problemu.
ti [Link]
M inim alne drzewo rozpinające. Na podstawie
"■ *- 8
nieskierowanego grafu ważonego znajdź m ini
malne drzewo rozpinające (ang. minimum span-
4 5 0.35 Krawędź drzewa
4 7 0.37 ning tree — MST).
' M S T (czarna)
5 7 0.28
0 7 0.16
15 0.32 Definicja. Przypominamy, że drzewo rozpi
04 0.38
nające grafu to spójny podgrafbez cykli, obej
2 3 0.17
1 7 0.19 mujący wszystkie wierzchołki. Minimalne
0 2 0.26
drzewo rozpinające grafu ważonego to drze
1 7 0.36
1 3 0.29 wo rozpinające, którego waga (suma wag
2 7 0.34 krawędzi) jest nie większa niż waga innych
Krawędź spoza
6 2 0.40
3 b 0.52 drzewa M S T (szara) drzew rozpinających.
6 0 0.58
6 4 0.93
Graf ważony i jego drzewo MST W tym podrozdziale badamy dwa klasyczne al
gorytmy wyznaczania drzew MST — algorytm
Prima i algorytm Kruskala. Algorytmy
Zastosowanie Wierzchołek Krawędź
te są łatwe do zrozumienia i nietrud
Obwód Komponent Kabel ne do zaimplementowania. Należą
do najstarszych i najlepiej poznanych
Linie lotnicze Lotnisko Trasa lotu
algorytmów spośród opisanych w tej
Sieci energetyczne Elektrownia Linie przesyłowe książce. Zastosowanie w nich współ
czesnych struktur danych przynosi
Analiza obrazu Cechy Relacja bliskości
istotne korzyści. Ponieważ drzewa
charakterystyczne
MST mają wiele ważnych zastoso
Typowe zastosowania drzew M ST
wań, algorytmy do rozwiązywania
omawianego problemu są badane przynajmniej od lat 20 . ubiegłego wieku (począt
kowo w kontekście sieci energetycznych, później w ramach sieci telefonicznych).
Obecnie algorytmy wyznaczania drzew MST odgrywaj ą istotną rolę w proj ektowaniu
wielu rodzajów sieci (komunikacyjnych, elektrycznych, hydraulicznych, kom putero
wych, drogowych, kolejowych, powietrznych i wielu innych), a także przy badaniu
sieci biologicznych, chemicznych i fizycznych występujących w naturze.
616
4.3 o Minimalne drzewa rozpinające 617
Założenia Przy wyznaczaniu minimalnego drzewa rozpinającego mogą wystąpić
różne nietypowe sytuacje. Zwykle m ożna sobie z nim i łatwo poradzić. Aby uniknąć
późniejszych dygresji, przyjmujemy następujące konwencje.
a Graf jest spójny. Zgodnie z definicją drzewa rozpinającego graf musi być spójny,
aby istniało drzewo MST. Problem m ożna przedstawić też w inny sposób, na pod
stawie podstawowych cech drzew ( p o d r o z d z i a ł Jeśli graf nie jest spójny, nie istnieją drzewa MST
4 . 1 ). Należy znaleźć zbiór V - l krawędzi o m ini 4 5 0 .,61
malnej wadze i łączących graf. Jeśli graf nie jest 4 6 0 .,62
5 6 0 . 88
spójny, m ożna zaadaptować algorytmy, aby wy 1 5 0 .,11
znaczyć drzewo MST każdej spójnej składowej. 2 3 0 ..35
0 3 0 .,6
Zbiór tych drzew nazywamy minimalnym lasem 1 6 0 .,10
rozpinającym (zobacz ć w i c z e n i e 4 .3 .22 ). 0 2 0 .,22
Można niezależnie wyznaczyć
D Wagi krawędzi nie muszą odpowiadać odległoś drzewa MSTskładowych
ciom. Czasem w zrozumieniu algorytmów pom a
gają intuicyjne spostrzeżenia z obszaru geometrii, Wagi nie muszą być
dlatego przedstawiamy przykłady (takie jak graf proporcjonalne do odległości
na następnej stronie), w których wierzchołki to 4 6 0.62
5 6 0.88
punkty w przestrzeni, a wagi to odległości. Ważne 1 5 0.02
jest jednak, aby pamiętać, że wagi mogą repre 0 4 0.64
1 6 0.90
zentować czas, koszt lub zupełnie inną zmienną 0 2 0.22
— nie muszą być proporcjonalne do odległości. 1 2 0.50
1 3 0.97
° Wagi krawędzi mogą mieć wartość zero lub ujemną.
2 6 0.17
Jeśli wszystkie wagi krawędzi są dodatnie, drzewo
>ujemną
MST można zdefiniować jako podgraf o minimal
4 6 0.62
nej łącznej wadze, który łączy wszystkie wierzchoł C 6 0.88
ki. Taki podgraf musi tworzyć drzewo rozpinające. 1 5 0.02
0 4 -0 .9 9
Zgodnie z definicją drzewa rozpinające istnieją 1 6 0
także dla grafów, których wagi krawędzi są równe 0 2 0.22
1 2 0.50
zero lub mają wartość ujemną. 1 3 0.97
13 Wszystkie krawędzie mają różne wagi. Jeśli krawę 2 6 0.17
dzie mogą mieć identyczne wagi, minimalne drze owe,
wo rozpinające może nie być unikatowe (zobacz jeśli występują identyczne wagi
ć w i c z e n i e 4 .3 .2 ). Możliwość istnienia wielu drzew 1 2 1.00
1 3 0.50
MST komplikuje dowody poprawności niektórych 2 4 1.00
algorytmów, dlatego w omówieniu nie dopuszcza 3 4 0.50
my takiej sytuacji. Okazuje się, że założenie to nie
1 2 1.00
ogranicza przydatności rozwiązań, ponieważ opra 1 3 0.50
T 4 1.00
cowane przez nas algorytmy nie wymagają m ody
3 4 0.50
fikacji, aby działały dla równych wag.
Podsumujmy — w omówieniu zakładamy, że zadanie R óżne a n o m a lie w d rz e w a c h MST
polega na znalezieniu drzewa MST dla spójnego grafu
ważonego o dowolnych (ale różnych) wartościach.
618 ROZDZIAŁ 4 O Grafy
Przestrzegane zasady Zacznijmy od przypo- Dodanie krawędzi
m nienia dwóch cech definicyjnych drzew (cechy te
przedstawiono w p o d r o z d z i a l e 4 . 1 ).
■ Dodanie krawędzi łączącej dwa wierzchołki
w drzewie powoduje powstanie unikatowego
cyklu.
■ Usunięcie krawędzi z drzewa powoduje jego po
dział na dwa odrębne poddrzewa.
Cechy te są podstawą przy dowodzeniu głównej
właściwości drzew MST, która prowadzi do rozwi
nięcia omawianych w tym podrozdziale algorytmów
ich wyznaczania.
Usunięcie krawędzi dzieli
Właściwość przekroju Właściwość ta (nazywamy drzewo na dwie części
ją właściwością przekroju) związana jest z identyfi P o d s ta w o w e c ec h y d rz e w a
kowaniem krawędzi, które muszą znaleźć się w drze
wie MST dla danego grafu ważonego. Proces ten polega na podziale wierzchołków na
dwa zbiory i sprawdzaniu krawędzi łączących oba zbiory.
Definicja. Przekrój (ang. cut) grafu to podział jego wierzchołków na dwa niepuste
rozłączne zbiory. Krawędź przekroju (ang. Crossing edge) dla danego przekroju to kra
wędź, która łączy wierzchołek z jednego zbioru z wierzchołkiem z drugiego zbioru.
Przekrój określamy zazwyczaj przez podanie zbioru wierzchołków. Pośrednio przyj
mujemy przy tym założenie, że przekrój powoduje podział na zbiór wierzchołków
i jego dopełnienie, tak więc krawędź przekroju prowadzi z wierzchołka ze zbioru do
wierzchołka spoza niego. Na rysunkach przedstawiamy wierzchołki z jednej strony
przekroju szarym kolorem, a wierzchołki z drugiej strony przekroju — na biało.
Krawędzie przekroju między
szarymi a białymi wierzchołkami Twierdzenie J (właściwość przekroju). W dowol
mają kolor czerwony nym przekroju grafu ważonego krawędź przekroju
o minimalnej wadze znajduje się w drzewie MST grafu.
Dowód. Niech e będzie krawędzią przekroju o m i
nimalnej wadze, a T — drzewem MST. Można prze
prowadzić dowód przez zaprzeczenie. Załóżmy, że T
nie obejmuje e. Teraz rozważmy graf utworzony przez
dodanie e do T. Graf ten obejmuje cykl zawierający e.
Krawędźprzekroju o minimalnej wadze
musi znajdować się wdrzewie MST Cykl musi zawierać przynajmniej jedną inną krawędź
przekroju, na przykład f o wadze większej niż e (ponie
Właściwość przekroju
waż e ma minimalną wagę, a wagi wszystkich krawędzi
są różne). Przez u su n ięcie /i dodanie e otrzymujemy
drzewo rozpinające o niższej wadze, co jest niezgodne
z założeniem, że drzewo T jest minimalne.
4.3 n Minimalne drzewa rozpinające 619
Przy założeniu, że wagi krawędzi są
różne, dla każdego grafu spójnego moż
na utworzyć unikatowe drzewo MST
(zobacz ć w i c z e n i e 4.3 .3 ). Zgodnie
z właściwością przekroju najkrótsza kra
Przekrój z dwoma krawędziami wędź przekroju dla każdego przekroju
w drzewie MST
musi znajdować się w tym drzewie.
Rysunek po lewej stronie t w i e r
d z e n i a j to ilustracja właściwości przekroju. Zauważmy, że nie ma
wymogu, aby minimalna krawędź była jedyną krawędzią drzewa
MST łączącą oba zbiory. W typowych przekrojach istnieje kilka
krawędzi drzewa MST łączących wierzchołek z jednego zbioru
z wierzchołkiem z innego, co pokazano na rysunku powyżej.
Algorytm zachłanny Właściwość przekroju jest podstawą al
gorytmów omawianych w kontekście wyznaczania drzew MST.
Algorytmy te są wersją ogólnego paradygmatu — algorytmu
zachłannego. Tu należy zastosować właściwość przekroju, aby
uzyskać krawędź drzewa MST, i kontynuować ten proces do
momentu znalezienia wszystkich takich krawędzi. Algorytmy
różnią się sposobem przechowywania przekrojów i wykrywania
krawędzi przekroju o minimalnej wadze, są jednak wersjami po
niższego rozwiązania.
Twierdzenie K (algorytm zachłanny wyznaczania drzew
MST). Opisana metoda koloruje na czarno wszystkie krawę
dzie w drzewie MST dowolnego spójnego grafu ważonego
o V wierzchołkach. Początkowo wszystkie krawędzie są szare.
Algorytm znajduje przekrój bez czarnych krawędzi, koloruje
krawędź o minimalnej wadze na czarno i kontynuuje proces
do czasu pokolorowania na czarno V - 1 krawędzi.
Dowód. Dla uproszczenia zakładamy, że wagi krawędzi są
różne, choć twierdzenie jest prawdziwe także wtedy, kiedy
warunek ten nie jest spełniony (zobacz ć w i c z e n i e 4 .3 . 5 ).
Zgodnie z właściwością przekroju każda krawędź pokoloro
wana na czarno należy do drzewa MST. Jeśli czarnych kra
wędzi jest mniej niż V - 1, istnieje przekrój bez czarnych
krawędzi (przypominamy założenie, że graf jest spójny).
Kiedy czarnych jest V - 1 krawędzi, czarne krawędzie two Algorytm zachłanny
tworzenia drzew MST
rzą drzewo rozpinające.
Rysunek po prawej stronie to typowy ślad działania algorytmu zachłannego. Na każ
dym rysunku pokazano przekrój i krawędź o minimalnej wadze (gruba czerwona
linia) dodawaną przez algorytm do drzewa MST.
620 ROZDZIAŁ 4 n Grafy
Typ danych dla grafów ważonych Jak można przedstawić grafy ważone?
Prawdopodobnie najprostszym sposobem jest rozwinięcie podstawowej reprezenta
cji grafu z p o d r o z d z i a ł u 4 . 1 . W reprezentacji opartej na macierzy sąsiedztwa m a
cierz może obejmować wagi krawędzi zamiast wartości logicznych. W reprezentacji
opartej na listach sąsiedztwa można zdefiniować węzeł obejmujący zarówno wierz
chołek, jak i pole z wagą. Węzły te są umieszczane na listach sąsiedztwa (jak zwykle
koncentrujemy się na grafach rzadkich i opracowanie reprezentacji opartej na listach
sąsiedztwa pozostawiamy jako ćwiczenia). To klasyczne podejście jest atrakcyjne, tu
jednak stosujemy inną metodę. Jest ono tylko nieco bardziej skomplikowane, spra
wia, że programy są znacznie przydatniejsze w ogólnym kontekście, i wymaga ogól
niejszego interfejsu API, umożliwiającego przetwarzanie obiektów typu Edge.
p u b lic c la s s Edge implement Comparable<Edge>
Edge(in t v, in t w, double weight) Konstruktor inicjujący
double w e ig h t() Zwraca wagę danej krawędzi
in t e it h e r() Zwraca jeden z wierzchołków krawędzi
in t o t h e r (in t v) Zwraca drugi wierzchołek
in t compareTo(Edge that) Porównuje krawędź z e
S t r in g t o S t r in g O Zwraca reprezentację w postaci łańcucha znaków
In te rf e js API k ra w ę d z i w a ż o n e j
Metody e ith e r() i other(), zapewniające dostęp do wierzchołków krawędzi, mogą
wydawać się zagadkowe. Ich przydatność stanie się oczywista w czasie analizowania
kodu klienta. Implementacja interfejsu Edge znajduje się na stronie 622. Interfejs ten
jest podstawą interfejsu API klasy EdgeWeightedGraph, w której w naturalny sposób
wykorzystano obiekty Edge.
p u b lic c la s s EdgeWeightedGraph
EdgeWeightedGraph (in t V) Tworzy pusty graf o V wierzchołkach
EdgeWeightedGraph(In in ) Wczytuje g ra f ze strumienia wejściowego
in t V() Zwraca liczbę wierzchołków
in t E() Zwraca liczbę krawędzi
void addEdge(Edge e) Dodaje krawędź e do grafu
Iterable<Edge> a d j(in t v) Zwraca krawędzie powiązane z v
Iterable<Edge> edges() Zwraca wszystkie krawędzie grafu
S t rin g t o S t r in g O Zwraca reprezentację w postaci łańcucha znaków
Interfejs API dla grafów ważonych
4.3 □ Minimalne drzewa rozpinające 621
Ten interfejs API jest bardzo podobny do interfejsu API klasy Graph (strona 534).
Dwie ważne różnice polegają na tym, że nowa ldasa jest oparta na Hasie Edge i obej
muje dodatkową metodę edges() (przedstawiona po prawej stronie), która um oż
liwia Hientom iterowanie po wszystldch Hawędziach grafu (z pominięciem pętli
własnych). Pozostała część implementacji ldasy EdgeWeightedGraph, przedstawiona
na stronie 623, przypomina implementację gra
fów nieslderowanych bez wag z p o d r o z d z i a ł u p u b lic ite ra b le < E d g e > e d g e s()
4 .1 , przy czym zamiast list sąsiedztwa z liczbami ^
^ r ‘ ' Bag<Edge> b = new B a g< E d g e > ();
całkowitymi, które zastosowano w Hasie Graph, for (int v = 0; v < V; v++)
wykorzystano listy sąsiedztwa z obiektami Edge, f o r (Edge e : adj [v])
Na rysunku w dolnej części tej strony po- if (e-°ther(v) > v) [Link](e);
r e t u r n b*
kazano reprezentację grafu ważonego, którą j
Hasa EdgeWeightedGraph tworzy na podstawie
przyHadowegO pliku [Link]. Zawartość Pobieranie wszystkich krawędzi grafu w ażonego
każdego obiektu Bag pokazano jako listę powią
zaną, aby odzwierciedlić standardową implementację z p o d r o z d z i a ł u 1 .3 . W celu
uproszczenia rysunku każdy obiekt Edge pokazano jako parę wartości typu i nt i war
tość typu doubl e. Sama struktura danych to lista powiązana odnośników do obiektów
obejmujących wartości. Choć istnieją dwie referencje do każdego obiektu Edge (po
jednej na liście każdego wierzchołka), każdej krawędzi grafu odpowiada doHadnie
jeden obiekt Edge. Na rysunku krawędzie pojawiają się na każdej liście w kolejności
odwrotnej względem kolejności przetwarzania. Wynika to ze zbliżonego do stosu
charakteru standardowej implementacji listy powiązanej. Tak jak w Hasie Graph, tak
i tu przez zastosowanie ldasy Bag jednoznacznie określamy, że w kodzie ldienta nie są
przyjmowane żadne założenia co do kolejności obiektów na listach.
V 6 0 .58 0 2 . 26 |— *• 0 4 .38 — 0 7 .16 Obiekty
t i nyEW [Link]
typu Bag
816 N.
1 3 .29 — 1 2 .36 — 1 7 .19 |— .32
1 15
4 5 0.35
4 7 0 .3 7 V 2 |. 26 [— H 2 | 3 |. !7 |
6 2 |. 40 |— - 2 | 7 |. 34 1 2 .36 |— 0
5 7 0 .28
0 7 0 .1 6
1 5 0.32 3 6 |. 52 |— *j ! | 3 |. 29 2 | 3 |.17
0 4 0 .38
2 3 0 .17
6 4 .93 0 4 .38 (— 4 7 .37 4 5 .35
1 7 0.1 9
0 2 0.2 6 \ Referencje do tego
1 2 0.3 6 ''H 1 5 .32 |— | 5 7 .28 |— - 4 | 5 |.35 ----------- ^ sa m e g o obiektu
1 3 0.2 9 lyt
2 7 0.3 4
6 2 0.4 0 6 | 4 .93 — 6 0 .5 8 - 3 6 .52 6 | 2 |.40
3 6 0.52
6 0 0.5 8 s. 1 7 .19 — 0 7 .16 5 | 7 .28 — 5 7 .28 |
2 7 .34 —
6 4 0.9 3
Reprezentacja grafu ważonego
622 ROZDZIAŁ 4 Grafy
Typ danych dla krawędzi w ażonych
public c la ss Edge implements Comparable<Edge>
{
p rivate final in t v; // Jeden wierzchołek,
p rivate final in t w; // Drugi wierzchołek,
private final double weight; // Waga krawędzi.
public Edge(int v, in t w, double weight)
{
t h i s . v = v;
this.w = w;
th is.w eigh t = weight;
}
public double weight()
{ return weight; }
public in t e it h e r Q
{ return v; }
public in t o th e r(in t vertex)
{
if (vertex == v) return w;
else i f (vertex == w) return v;
else throw new RuntimeException("Błędna krawędź");
}
public in t compareTo(Edge that)
{
if (th is.w e igh t() < that.w eight()) return - 1 ;
else i f (th is.w e igh t() > that.w eight()) return + 1 ;
else return 0 ;
}
public S t r in g t o S t r in g Q
{ return [Link] at("%d-%d % .2 f", v, w, weight); }
}
W tym typie danych udostępniono metody e i t h e r () i o t h e r(). W kliencie można użyć
metody o th er (v), aby znaleźć drugi wierzchołek, kiedy znany jest v. Jeśli żaden wierzcho
łek nie jest znany, w klientach można zastosować idiomatyczny kod i nt v = e . e it h e r ( ) ,
w = e .o t h e r ( v ) ;, żeby uzyskać dostęp do obu wierzchołków obiektu e typu Edge.
4.3 Minimalne drzewa rozpinające 623
Typ danych dla grafów ważonych
public c la s s EdgeWeightedGraph
{
private final in t V; // Liczba wierzchołków,
private in t E; // Liczba krawędzi,
private Bag<Edge>[] adj; // L i s t y sąsiedztwa.
public EdgeWeightedGraph(int V)
{
t h i s . V = V;
th is.E = 0 ;
adj = (Bag<Edge>[]) new Bag[V];
fo r (in t v = 0; v < V; v++)
adj [v] = new Bag<Edge>();
public EdgeWeightedGraph(In in)
// Zobacz ćwiczenie 4.3.9.
p ublic in t V() { return V; }
public in t E() { return E; }
public void addEdge(Edge e)
{
in t v = e .e it h e r ( ), w = e .other(v);
a d j[ v ]. a d d (e );
adj [w] .add(e);
E++;
public Iterable<Edge> a d j(in t v)
{ return adj [ v ] ; )
public Iterable<Edge> edges()
// Zobacz stronę 621.
W tej implementacji przechowywana jest indeksowana wierzchołkami tablica list krawędzi.
Tak jak w klasie Graph (strona 538), tak i tu każda krawędź występuje dwukrotnie. Jeśli kra
wędź łączy v i w, pojawia się zarówno na liście v, jak i na liście w. Metoda edges () umiesz
cza wszystkie krawędzie w obiekcie Bag (strona 621). Utworzenie implementacji metody
to S trin g () pozostawiamy jako ćwiczenie.
624 ROZDZIAŁ 4 ¡a Grafy
Porównywanie kraw ędzi według wag Interfejs A P I określa, że w klasie Edge na
leży zaimplementować interfejs Comparable i umieścić kod m etody compareTo().
Naturalna kolejność krawędzi w grafie ważonym jest wyznaczana przez wagi. Dlatego
implementacja metody compareTo() jest prosta.
K rawędzie równoległe Podobnie jak w implementacjach grafów nieskierowanych,
tak i tu dopuszczalne są krawędzie równoległe. Inna możliwość to opracowanie bar
dziej skomplikowanej implementacji klasy EdgeWei ghtedGraph, gdzie takie krawędzie
są niedopuszczalne (na przykład przez zachowanie krawędzi o minimalnej wadze ze
zbioru krawędzi równoległych).
Pętle własne Pętle własne są dozwolone. Jednak w implementacji metody edges()
w klasie EdgeWei ghtedGraph nie uwzględniamy pętli własnych, choć mogą one wy
stępować w danych wyjściowych lub w strukturze danych. Nie ma to wpływu na al
gorytmy dla drzew MST, ponieważ drzewa tego rodzaju nie obejmują pętli własnych.
W zastosowaniach, w których takie pętle są istotne, potrzebne mogą być odpowied
nie modyfikacje w kodzie.
obiektów typu Edge prowadzi — jak się okaże — do
b e z p o ś r e d n ie z a s t o s o w a n ie
przejrzystego i zwięzłego kodu klienta. Odbywa się to niewielkim kosztem. Każdy
węzeł listy sąsiedztwa obejmuje referencję do obiektu typu Edge i nadmiarowe infor
macje (wszystkie węzły na liście sąsiedztwa v obejmują v). Trzeba też ponieść koszty
ogólne związane z obiektem. Choć istnieje tylko jedna kopia każdego obiektu typu
Edge, istnieją dwie referencje do każdego z nich. Inne (i często stosowane) podej
ście polega na przechowywaniu dla każdej krawędzi dwóch węzłów na liście (tak
jak w klasie Graph); w każdym węźle listy należy wtedy umieścić wierzchołek i wagę
krawędzi. Także to rozwiązanie wymaga poniesienia pewnych kosztów — dla każdej
krawędzi trzeba utworzyć dwa węzły, obejmujące dwie kopie wagi.
4.3 Q Minimalne drzewa rozpinające 625
In te r fe js API d o w y z n a c z a n ia d r z e w MST i k lie n t te s to w y Definiujemy
tu (jak zwykle przy przetwarzaniu grafów) interfejs API. Obejmuje on konstruktor,
który przyjmuje jako argument graf ważony i umożliwia wywoływanie m etod obsłu
gi zapytań klientów, zwracających drzewo MST i jego wagę. Jak m ożna przedstawić
samo drzewo MST? Drzewo MST dla grafu G to będący drzewem podgraf tego grafu.
Istnieje więc wiele możliwości. Oto najważniejsze z nich:
° lista krawędzi,
0 graf ważony,
n indeksowana wierzchołkami tablica z odnośnikami do rodziców.
Aby w klientach i implementacjach zapewnić jak największą elastyczność w zakresie
wyboru jednej z wymienionych możliwości, przyjęliśmy poniższy interfejs API.
p u b lic c la s s MST
MST (EdgeWeightedGraph G) Konstruktor
Iterable<Edge> edges() Zwraca wszystkie krawędzie drzewa M ST
double w e ig h t() Zwraca wagę drzewa M ST
Interfejs API dla implementacji drzew M ST
K lient testowy Jak zwykle tworzymy przykładowe grafy i rozwijamy klienta testowe
go do testowania implementacji. Przykładowego klienta pokazano poniżej. Program
wczytuje krawędzie ze strum ienia wejściowego, tworzy graf ważony, wyznacza drze
wo MST grafu oraz wyświetla krawędzie drzewa MST i jego wagę.
p u b lic s t a t ic void m a in (S trin g [] args)
In in = new In (a rgs [0 ]);
EdgeWeightedGraph G;
G = new EdgeW eightedGraph(in);
MST mst = new MST(G);
f o r (Edge e : m [Link] s())
S t d O u t . p r in t ln (e );
S td O u t.p rin tln (m st.w e ig h tO ) ;
Klient testowy do wyznaczania drzew M ST
626 ROZDZIAŁ 4 □ Grafy
D ane testowe W witrynie poświęconej książce dostępny jest plik [Link].
Zdefiniowano w nim mały przykładowy graf (przedstawiony na stronie 616), służący
do tworzenia szczegółowych śladów działania algorytmów dla drzew MST. W itryna
obejmuje też plik [Link]. Zawiera on graf ważony o 250 wierzchołkach,
narysowany w dolnej części następnej strony. Jest to przykładowy graf euklidesowy,
którego wierzchołki to punkty w przestrzeni, a krawędzie — linie łączące wierzchoł
ki. Wagi krawędzi są równe odległościom euklidesowym
między wierzchołkami. Takie grafy pomagają zrozumieć %more tinyEW G .txt
działanie algorytmów dla drzew MST, a ponadto stano- 816
wią model wielu typowych problemów praktycznych, 4 7 '37
o których wspomnieliśmy (na przykład map drogowych 5 7 .28
lub obwodów elektrycznych). W itryna obejmuje też 0 7 .16
1 5 .32
większy przykład — plik [Link] z definicją grafu
0 4 .38
euklidesowego o milionie wierzchołków. Naszym celem 2 3 .17
jest znajdowanie drzew MST dla takich grafów w sen- 1 7 .19
0 2 .26
sownym czasie.
1 2 .36
1 3 .29
2 7 .34
6 2 .40
3 6 .52
6 0 .58
6 4 .93
% j a v a MST tinyEW G .txt
0 -7 0 .1 6
1-7 0 .1 9
0 -2 0 .2 6
2 - 3 0 .1 7
5-7 0 .2 8
4 - 5 0 .3 5
6-2 0 .4 0
1.81
4.3 b Minimalne drzewa rozpinające 627
% more [Link]
250 1273
244 246 0.11 71 2
239 240 0.1 0616
238 245 0.0 6142
235 238 0 .07048
233 240 0 .07634
232 248 0.1 0223
231 248 0.1 0699
229 249 0 .10098
228 241 0.0 1473
226 231 0 .0 76 38
. . . [1263 i n n e kr awędzie]
% j a v a MST [Link]
0 225 0.0 2383
49 225 0 .0 3 3 14
44 49 0.02107
44 204 0.0 17 7 4
49 97 0.0 3121
202 204 0.04207
176 202 0 .04299
176 191 0.0 2089
68 176 0.0 4396
58 68 0.0 4795
. . . [239 in n y ch kraw ęd zi]
10.46351
Drzewo MST
Graf euklidesowy o 250 węzłach (i 1273 krawędziach) oraz odpowiadające mu drzewo WIST
628 ROZDZIAŁ 4 a Grafy
A l g o r y t m P r i m a Pierwsza z omawianych metod wyznaczania drzew MST, algo
rytm Prima, polega na dołączaniu na każdym etapie nowej krawędzi do pojedynczego
rosnącego drzewa. Należy zacząć od dowolnego wierzchołka i potraktować go jak jed-
nowierzchołkowe drzewo. Następnie trzeba dodać V - 1 krawędzi, zawsze wybierając
następną krawędź o minimalnej wadze (i kolorując ją na czarno) łączącą wierzcho
łek z drzewa z wierzchołkiem spoza niego (należy więc wybrać krawędź przekroju dla
przekroju wyznaczonego przez wierzchołki drzewa).
Krawędź
Krawędź przekroju
niewybieralna
(kolor czerwony)
Twierdzenie L. Algorytm Prima wyznacza drze
(kolor szary)
wo MST dla dowolnego spójnego grafu ważonego.
i
Dowód. Bezpośrednio wynika z t w i e r d z e n i a k .
Rosnące drzewo wyznacza przekroje bez czarnych
krawędzi. Algorytm pobiera krawędź przekroju
\ \
Krawędź przekroju o minimalnej wadze, dlatego po kolei koloru
o minimalnej wadze
musi występować je krawędzie na czarno na podstawie algorytmu
Krawędź w drzewie M ST
drzewa
zachłannego.
(kolor czarny I
•i pogrubienie)
Przedstawiony wcześniej jednozdaniowy opis algo
Algorytm Prima - wyznaczanie drzew MST
rytm u Prima pozostawia bez odpowiedzi kluczowe
pytanie — jak można w wydajny sposób znaleźć krawędź przekroju o minimalnej
wadze? Zaproponowano kilka metod. Niektóre z nich omawiamy po opracowaniu
kompletnego rozwiązania, opartego na wyjątkowo prostym podejściu.
S tru ktu ry danych W implementacji algorytmu Prima posługujemy się kilkoma
prostymi i znanymi strukturam i danych. Wierzchołki drzewa, krawędzie drzewa
i krawędzie przekroju reprezentujemy w następujący sposób.
■ Wierzchołki drzewa. Używamy indeksowanej wierzchołkami tablicy marked[]
z wartościami logicznymi, w której marked [v] ma wartości tru e, jeśli v znajduje
się w drzewie.
■ Krawędzie w drzewie. Stosujemy jedną z dwóch struktur danych — kolejkę mst
do zapisywania krawędzi drzewa MST lub indeksowaną wierzchołkami tablicę
edgeTo[] z obiektami typu Edge, w której edgeTo[v] to obiekt Edge łączący v
z drzewem.
° Krawędzie przekroju. Korzystamy z kolejki priorytetowej Mi n PQ<Edge>, w której
krawędzie są porównywane według wag (zobacz stronę 622).
Wymienione struktury danych umożliwiają udzielenie bezpośredniej odpowiedzi na
podstawowe pytanie: „Która krawędź przekroju ma m inim alną wagę?”.
Tworzenie zbioru kraw ędzi przekroju Przy dodawaniu krawędzi do drzewa za
wsze trzeba dodać do niego także wierzchołek. Aby utworzyć zbiór krawędzi prze
kroju, należy dodać do kolejki priorytetowej wszystkie krawędzie z danego wierz
chołka do wszystkich wierzchołków spoza drzewa (m ożna je ustalić za pom ocą
tablicy marked []). Trzeba jednak zrobić coś więcej. Każda krawędź, która łączy
dodany wierzchołek z wierzchołkiem z drzewa i już znajduje się w kolejce priory-
4.3 Q Minimalne drzewa rozpinające 629
tetowej, staje się niewybieralna (nie jest wtedy krawędzią przekroju, ponieważ łączy
dwa wierzchołki drzewa). W zachłannej im plementacji algorytm u Prim a można
usunąć takie krawędzie z kolejki priorytetowej. Najpierw omawiamy jednak leniwą
implementację, w której krawędzie pozostają w kolejce priorytetowej. Sprawdzanie
wybieralności odkładamy do m om entu usuwania krawędzi.
Po prawej stronie pokazano ślad dzia
0-7 0 16
łania algorytmu dla małego przykładowe 0 - 2 0 26
* Oznaczanie
go grafu [Link]. Na każdym rysun now ych 0-4 0 38
elementów 6 -0 0 58
ku znajduje się graf i kolejka priorytetowa
po odwiedzeniu wierzchołka (po dodaniu
go do drzewa i przetworzeniu krawędzi Krawędzie przekroju
na liście sąsiedztwa danego wierzchołka). (uporządkowane
według wagi)
Uporządkowaną zawartość kolejki priory
tetowej pokazano obok grafu, przy czym
nowe krawędzie są oznaczone gwiazdka 6-0 0.58
0-2 0.26
mi. Algorytm tworzy drzewo MST w na 5-7 0.28
stępujący sposób. 1-3 0.29
1-5 0.32
° Dodaje 0 do drzewa MST, a wszyst 2-7 0.34
kie krawędzie z listy sąsiedztwa 1-2 0.36
4-7 0.37
tego wierzchołka — do kolejki
0-4 0.38
priorytetowej. 0-6 0.58
D Dodaje 7 i krawędź 0-7 do drzewa
MST, a wszystkie krawędzie z listy
sąsiedztwa tego wierzchołka — do
kolejki priorytetowej. Krawędzie 5-7 0.28
niewybieralne l1 - 3 0 . 2 9
0 Dodaje 1 i krawędź 1-7 do drzewa (kolorszary) 'y 1-5 0.32
MST, a wszystkie krawędzie z listy 2-7 0 .34
1- 2 0 . 36
sąsiedztwa tego wierzchołka — do
4-7 0.37
kolejki priorytetowej. 0-4 0.38
6 - 2 0.40
° Dodaje 2 i krawędź 0-2 do drzewa
3-6 0.52
MST, a krawędzie 2-3 i 6-2 — do 6-0 0.58
kolejki priorytetowej. Krawędzie 2-7
i 1-2 stają się niewybieralne.
° Dodaje 3 i krawędź 2-3 do drzewa 1-2 0 . 3 6
MST, a krawędź 3-6 — do kolejki 4-7 0.37
0-4 0.38
priorytetowej. Krawędź 1-3 staje się 6 - 2 0.40
niewybieralna. 3-6 0.52
6-0 0.58
n Usuwa krawędzie niewybieralne 1-3, 6-4 0.93
1-5 i 2-7 z kolejki priorytetowej.
° Dodaje 5 i krawędź 5-7 do drzewa
MST, a krawędź 4-5 — do kolejki
priorytetowej. Krawędź 1-5 staje
się niewybieralna.
Ślad działania algorytmu Prima (wersja leniwa)
630 ROZDZIAŁ 4 a Grafy
■ Dodaje 4 i krawędź 4-5 do drzewa MST, a krawędź 6-4 — do kolejki prioryteto
wej. Krawędzie 4-7 i 0-4 stają się niewybieralne.
■ Usuwa niewybieralne krawędzie 1-2, 4-7 i 0-4 z kolejki priorytetowej.
° Dodaje 6 i krawędź 6-2 do drzewa MST. Pozostałe krawędzie powiązane z 6
stają się niewybieralne.
Po dodaniu V wierzchołków (i U - 1 krawędzi) drzewo MST jest gotowe. Pozostałe kra
wędzie z kolejki priorytetowej są niewybieralne i nie trzeba ponownie ich sprawdzać.
Implementacja Po tym wstępie zaimplementowanie algorytmu Prima jest proste, co
pokazano w implementacji LazyPrimMST na następnej stronie. Tale jale implementacje
przeszukiwania w głąb i wszerz z dwóch poprzednich podrozdziałów, tak i ten algorytm
wyznacza drzewo MST w konstruktorze, co umożliwia metodom klienckim ustalanie
cech drzew MST. W algorytmie wykorzystano metodę prywatną vi s i t (), która umiesz
cza wierzchołek w drzewie, oznaczając go jako odwiedzony, a następnie dodając wszyst
kie sąsiednie wierzchołki wybieralne do kolejki priorytetowej. Gwarantuje to, że kolejka
priorytetowa obejmuje krawędzie przekroju łączące wierzchołki drzewa z wierzchołkami
spoza niego (a czasem także z kilkoma krawędziami niewybieralnymi). Pętla wewnętrz
na to kod odpowiadający jednozdaniowemu opisowi algorytmu. Fragment ten pobiera
krawędź z kolejki priorytetowej i (jeśli nie jest niewybieralna) dodaje ją do drzewa. Kod
ponadto dodaje do drzewa nowy wierzchołek, do którego prowadzi krawędź, i aktuali
zuje zbiór krawędzi przekroju, wywołując metodę vi s i t () z nowym wierzchołkiem jako
argumentem. Metoda wei ght () musi przejść po krawędziach drzewa w celu dodania wag
krawędzi (podejście leniwe) lub zapisywać bieżącą sumę w zmiennej egzemplarza (podej
ście zachłanne). Jej napisanie pozostawiamy jako ć w i c z e n i e 4 .3 .3 1 .
Czas w ykonania Jak szybki jest algorytm Prima? Na podstawie wiedzy o cechach
kolejek priorytetowych nietrudno odpowiedzieć na to pytanie.
Twierdzenie M. Leniwa wersja algorytmu Prima wymaga pamięci w ilości pro
porcjonalnej do E i czasu w ilości proporcjonalnej do E log E (dla najgorszego
przypadku), aby wyznaczyć drzewo MST dla spójnego grafu ważonego o E kra
wędziach i V wierzchołkach.
Dowód. Wąskim gardłem w algorytmie jest liczba porównań wag krawędzi
w metodach i n s e r t () i del Mi n () dla kolejki priorytetowej. Liczba krawędzi w ko
lejce priorytetowej wynosi najwyżej E i wyznacza ograniczenie ilości potrzebnej
pamięci. W najgorszym przypadku koszt wstawiania wynosi ~lg E, a koszt usu
wania m in im u m 2lg E (zobacz t w i e r d z e n i e o w r o z d z i a l e 2 .). Ponieważ
wstawianych jest najwyżej E krawędzi i tyle samo jest usuwanych, wynikają z tego
ograniczenia ilości czasu.
Ograniczenie czasu wykonania jest dość konserwatywne, ponieważ w praktyce liczba
krawędzi w kolejce priorytetowej jest zwykle znacznie niższa niż E. Istnienie tak pro
stego, wydajnego i przydatnego algorytmu dla tak trudnego zadania jest zaskakujące.
Dalej pokrótce omawiamy pewne usprawnienia. Szczegółowa ocena poprawek w za
stosowaniach, gdzie wydajność jest niezwykle istotna, stanowi zadanie dla ekspertów.
4.3 Minimalne drzewa rozpinające 631
Leniwa wersja algorytm u Prima
public c la s s LazyPrimMST
{
p rivate boolean[] marked; // Wierzchołki drzewa MST.
p rivate Queue<Edge> mst; // Krawędzie drzewa MST.
p rivate MinPQ<Edge> pq; // Krawędzie przekroju (i niewybieralne).
public LazyPrimMST(EdgeWeightedGraph G)
{
pq = new Mi nPQ<Edge>();
marked = new b oolean[G .V ()];
mst = new Queue<Edge>();
v i s it ( G , 0); // Zakładamy, że G je s t spójny (zobacz ćwiczenie 4.3.22).
while (![Link]())
{
Edge e = pq.d elM inQ ; // Pobieranie najmniejszej
// wagi.
in t v = e .e it h e r ( ) , w = [Link] er(v); // Krawędź z kolejki pq.
i f (marked[v] &&marked[w]) continue; // Pomijanie, j e ś l i je s t
// niewybieralna.
[Link](e); // Dodawanie krawędzi do
// drzewa.
if (!marked[v]) v i s i t ( G , v ) ; // Dodawanie wierzchołka v
// lub w
if (!marked[w]) v i s i t ( G , w); // do drzewa.
}
}
private void visit(EdgeWeightedGraph G, in t v)
{ // Oznaczanie v i dodawanie do pq wszystkich krawędzi z v do
// nieoznaczonych wierzchołków,
marked [v] = true;
fo r (Edge e : [Link](v))
i f (!marked[[Link](v)]) p q . in s e r t ( e ) ;
}
public Iterable<Edge> edges()
( return mst; }
public double weight() // Zobacz ćwiczenie 4.3.31.
}
W tej implementacji algorytmu Prima wykorzystano kolejkę priorytetową do przechowy
wania krawędzi przekroju, indeksowaną wierzchołkami tablicę do oznaczania wierzchołków
drzewa i kolejkę do przechowywania krawędzi drzewa MST. Ta implementacja to podejście
leniwe, w którym krawędzie niewybieralne pozostawiane są w kolejce priorytetowej.
632 ROZDZIAŁ 4 Q Grafy
Zachłanna wersja algorytmu Prima Aby spróbować usprawnić program
Lazy Pri mMST, m ożna usuwać niewybieralne krawędzie z kolejki priorytetowej, tak aby
kolejka ta obejmowała wyłącznie krawędzie przekroju, łączące wierzchołki z drzewa
i spoza niego. Można jednak usunąć jeszcze więcej krawędzi. Kluczem do tego jest
spostrzeżenie, że ważna jest tylko m inim alna krawędź z wierzchołków spoza drzewa
do wierzchołków drzewa. Przy dodawaniu wierzchołka v do drzewa jedyną możli-
v wą zmianą związaną z dowolnym wierzchołkiem w spoza
drzewa jest to, że dodanie v spowoduje przybliżenie w do
drzewa. Ujmijmy to krótko — w kolejce priorytetowej nie
trzeba przechowywać wszystkich krawędzi z w do wierz
chołków drzewa. Wystarczy śledzić krawędź o minimalnej
wadze i sprawdzać, czy dodanie v do drzewa wymaga zak
tualizowania m inim um (z uwagi na krawędź v-w o niższej
do drzewa wadze), co można zrobić w czasie przetwarzania krawędzi
z listy sąsiedztwa v. Można opisać to inaczej — w kolej
ce priorytetowej przechowywana jest tylko jedna krawędź dla każdego wierzchoł
ka w spoza drzewa. Jest to najkrótsza krawędź łącząca dany wierzchołek z drzewem.
Wszystkie dłuższe krawędzie z w do drzewa w pewnym momencie staną się niewy
bieralne, dlatego nie trzeba przechowywać ich w kolejce priorytetowej.
Klasa Pri mMST ( a l g o r y t m 4.7 na stronie 634) to implementacja algorytmu
Prima oparta na opracowanym przez nas typie danych dla kolejki prioryteto
wej ( p o d r o z d z i a ł 2 .4 , strona 332). Struktury danych markedj] i mst[] z klasy
LazyPrimMST zastąpiono tu dwoma tablicami (edgeTo[] i d istT o []) indeksowanymi
wierzchołkami. Tablice te mają następujące cechy.
■ Jeśli v nie znajduje się w drzewie, ale ma przynajmniej jedną krawędź prowa
dzącą do drzewa, element edgeTo[v] to najkrótsza krawędź prowadząca z v do
drzewa, a di stTo[v] to waga tej krawędzi.
■ Wszystkie wierzchołki v tego rodzaju są przechowywane w kolejce prioryteto
wej indeksów jako indeks v powiązany z wagą krawędzi edgeTo [v].
Oto najważniejsze implikacje tych cech — klucz minimalny z kolejki priorytetowej to
waga krawędzi przekroju o minimalnej wadze, a powiązany wierzchołek v należy jako
następny dodać do drzewa. Tablica markedj] nie jest potrzebna, ponieważ warunek
!marked[w] to odpowiednik warunku, zgodnie z którym distTo[w] to nieskończo
ność (a edgeTo [w] m a wartość nuli). W celu zarządzania strukturam i danych kod
klasy Pri mMST pobiera krawędź v z kolejki priorytetowej, a następnie sprawdza każdą
krawędź v-w na liście sąsiedztwa v. Jeśli wjest oznaczony, krawędź jest niewybieralna.
Jeżeli krawędź nie znajduje się w kolejce priorytetowej lub jej waga jest mniejsza od
obecnie uznawanej za najlepszą wartości edgeTo [w], kod aktualizuje struktury da
nych i ustawia v-w jako najlepszy znany sposób na połączenie v z drzewem.
Rysunek na następnej stronie to ślad działania klasy Pri mMST dla małego przykłado
wego grafu [Link]. Zawartość tablic edgeTo [] i di stTo [] dotyczy sytuacji po do
daniu każdego wierzchołka do drzewa MST. Kolory obrazują wierzchołki drzewa MST
(czarne indeksy), wierzchołki spoza drzewa MST (szare indeksy), krawędzie drzewa
4.3 s Minimalne drzewa rozpinające 633
MST (kolor czarny) i pary indeks-wartość z kolejki priorytetowej (kolor czerwony).
N a rysunkach najkrótszą krawędź łączącą każdy wierzchołek spoza drzewa MST
z wierzchołkiem z drzewa przedstawiono w kolorze czerwonym. Algorytm dodaje
krawędzie do drzewa MST w tej samej
e d g e T o [] d i s t T o []
kolejności, co wersja leniwa. Różnica 0
polega na operacjach na kolejce priory \ /
r
2 0 -. 0 .2 6
tetowej. Ta wersja tworzy drzewo MST 3
4 0 -4 0 .3 8
w opisany poniżej sposób.
6 6 -0 0 .5 8
° Dodaje 0 do drzewa MST, 7 0 -7 0 .1 6
a wszystkie krawędzie z listy są 0
1 1 -7 0 .1 9
siedztwa — do kolejki prioryte 2 0 -2 0 .2 6
towej, ponieważ każda taka kra 4 0 -4 0 .3 8
5 5 -7 0 .2 8
wędź jest najlepszym (jedynym) 6 6 -0 0 .5 8
7 0 -7 0 .1 6
znanym połączeniem między
0
wierzchołkiem z drzewa i wierz 1 1 -7 0 .1 9
2 0 -2 0 . 2 6 ■<—
chołkiem spoza niego. 3 1 -3 0 .2 9
D Dodaje 7 i 0-7 do drzewa MST 4 0 -4 0 .3 8
5 5 -7 0 .2 8
oraz 1-7 i 5-7 do kolejki prio 6 6 -0 0 .5 8
7 0 -7 0 .1 6
rytetowej. Krawędzie 4-7 i 2-7 0
nie wpływają na kolejkę priory 1 1 -7 0 .1 9
2 0 -2 0 .2 6
tetową, ponieważ ich wagi nie 3 2 -3 0 .1 7
4 0 -4 0 .3 8
są mniejsze niż wagi znanych 5 5 -7 0 .2 8
6 6 -2 0 .4 0
połączeń między drzewem MST 7 0 -7 0 .1 6
a wierzchołkami 4 i 2. 0
1 1 -7 0 .1 9
a Dodaje 1 i 1-7 do drzewa MST 2 0 -2 0 .2 6
3 2 -3 0 .1 7
oraz 1-3 do kolejki priorytetowej. 4 0 -4 0 .3 8
° Dodaje 2 i 2-0 do drzewa MST, 5 5 -7 0 .2 8
6 6 -2 0 .4 0
zastępuje 0-6 krawędzią 2-6 jako Gruba 7 0 -7 0 .1 6
najkrótszą krawędzią z wierz czerw ona - 0
najmniejsza 1 1 -7 0 .1 9
chołka z drzewa do 6 i zastępuje krawędź w pą, 2 0 -2 0 .2 6
3 2 -3 0 .1 7
1-3 krawędzią 2-3 jako najkrót następna do 4 4 -5 0 .3 5
dod ania do 5 5 -7 0 .2 8
szą krawędzią z wierzchołka drzewa M ST 6 6 -2 0 .4 0
7 0 -7 0 .1 6
z drzewa do 3.
0
■ Dodaje 3 i 2-3 do drzewa MST. 1 1 -7 0 .1 9
2 0 -2 0 .2 6
D Dodaje 5 i 5-7 do drzewa MST 3 2 -3 0 .1 7
4 4 -5 0 .3 5
oraz zastępuje 0-4 krawędzią 5 5 -7 0 .2 8
4-5 jako najkrótszą krawędzią 6 6 -2 0 .4 0
7 0 -7 0 .1 6
z wierzchołka z drzewa do 4.
0
n Dodaje 4 i 4-5 do drzewa MST. 1 1 -7 0 .1 9
2 0 -2 0 .2 6
0 Dodaje 6 i 6-2 do drzewa MST. 3 2 -3 0 .1 7
Po dodaniu V - 1 krawędzi drzewo 4 4 -5 0 .3 5
5 5 -7 0 .2 8
MST jest kompletne, a kolejka priory 6 6 -2 0 .4 0
7 0 -7 0 .1 6
tetowa — pusta.
Ślad działania algorytmu Prima (wersja zachłanna)
634 ROZDZIAŁ 4 Grafy
ALGORYTM 4.7. Algorytm Prima do w yznaczania drzew MST (wersja zachłanna)
public c la s s PrimMST
{
private Edge[] edgeTo; // Najkrótsza krawędź z wierzchołka
// drzewa.
private doublej] distTo; // distTo[w] = edgeTo[w].weight()
private booleanj] marked; // true, j e ś l i v znajduje s ię w drzewie,
private IndexMinPQ<Double> pq; // Wybieralne krawędzie przekroju.
public PrimMST(EdgeWeightedGraph G)
{
edgeTo = newEdge CG. V ()];
distTo = newdouble[G.V () ];
marked = newb oolean[G .V ()];
fo r (in t v = 0; v < G.V(); v++)
di stTo[v] = Double.POSITIVE_INFINITY;
pq = new IndexMinPQ<Double>(G.V () );
di s tT o [0] = 0.0;
p q .in se rt(0 , 0.0); // Inicjowanie pq za pomocą 0 i wagi 0.
while (![Link]())
v i s i t ( G , p q .d e lM in ( )); // Dodawanie najbliższego wierzchołka do
// drzewa.
}
private void visit(EdgeWeightedGraph G, in t v)
{ // Dodawanie v do drzewa i aktualizowanie s tru k tu r danych,
markedjv] = true;
f o r (Edge e : [Link](v))
{
in t w = e . o t h e r ( v ) ;
i f (markedjw]) continue; // Krawędź v-w je s t niewybieralna.
i f ([Link]() < di stTo [w])
{ // Krawędź e je st nowym najlepszym połączeniem między drzewem a w.
edgeTo[w] = e;
distTo[w] = [Link]();
i f ([Link](w)) [Link](w, di stTo[w]);
else [Link](w, distTo[w ]);
}
}
public Iterable<Edge> edges() // Zobacz ćwiczenie 4.3.21.
public double weight() // Zobacz ćwiczenie 4.3.31.
}
W tej implementacji algorytmu Prima w kolejce priorytetowej z indeksami przechowywane
są wybieralne krawędzie przekroju.
4.3 a Minimalne drzewa rozpinające 635
d o w ó d w z a s a d z i e i d e n t y c z n y z dowodem t w i e r d z e n i a m
pozwala się przekonać, że zachłanna wersja algorytmu Prima wy- 20%
znacza drzewo MST dla spójnego grafu ważonego w czasie pro
porcjonalnym do E log V i z wykorzystaniem dodatkowej pamię
ci w ilości proporcjonalnej do V (zobacz stronę 635). W dużych
grafach rzadkich, typowych w praktyce, nie istnieje asymptotycz
na różnica w czasie (ponieważ dla grafów rzadkich lg E ~ lg V),
a ilość potrzebnej pamięci jest mniejsza o stały (ale duży) czynnik.
Dalsze analizy i eksperymenty najlepiej pozostawić ekspertom
pracującym nad aplikacjami, w których wydajność ma krytyczne 40%
znaczenie. Ważnych jest wtedy wiele czynników, w tym implemen
tacje klas MinPQ i IndexMinPQ, reprezentacja grafów, właściwości
zastosowanego modelu grafu itd. Usprawnienia trzeba, jak zwykle,
dokładnie przemyśleć, ponieważ większa złożoność kodu jest uza
sadniona tylko wtedy, kiedy poprawa wydajności o stały czynnik
jest istotna (w skomplikowanych współczesnych systemach zmia
ny mogą nawet przynieść efekty przeciwne do oczekiwanych).
60%
Twierdzenie N. W zachłannej wersji algorytmu Prima przy
wyznaczaniu drzewa MST dla spójnego grafu ważonego o E
krawędziach i V wierzchołkach ilość potrzebnej pamięci jest
proporcjonalna do V, a czas wykonania jest proporcjonalny
do E log V (dla najgorszego przypadku).
Dowód. Liczba krawędzi w kolejce priorytetowej wynosi naj
wyżej V, a ponadto istnieją trzy tablice indeksowane wierzchoł 80%
kami, z czego wynika ograniczenie ilości pamięci. Algorytm
wykonuje V operacji wstaw, U operacji usuń minimalny i (dla
najgorszego przypadku) E operacji zmień priorytet. Te warto
ści, w połączeniu z informacją, że opracowana przez nas opar
ta na kopcu implementacja indeksowanej kolejki prioryteto
wej wykonuje wszystkie te operacje w czasie proporcjonalnym
do log V (zobacz stronę 333), wyznaczają górne ograniczenie
Drzewo MST
czasu wykonania.
Na rysunku po prawej stronie pokazano działanie algorytmu
Prima na grafie euklidesowym z pliku [Link] (graf ten
ma 250 wierzchołków). Jest to fascynujący dynamiczny proces
(zobacz też ć w i c z e n i e 4 .3 .27 ). Zazwyczaj drzewo rośnie przez
dołączenie nowego wierzchołka do wierzchołka dodanego w p o
przednim kroku. Po dojściu do obszaru, w którym nie ma bli
skich wierzchołków spoza drzewa, proces rozrastania jest wzna- Algorytm Prima
\ r ’ (250 wierzchołków)
wiany w innej części drzewa.
636 ROZDZIAŁ 4 o Grafy
Algorytm Kruskala Drugi szczegółowo
omawiany algorytm tworzenia drzew MST prze
twarza krawędzie według ich wag (od najmniej
szej do największej) i dodaje do drzewa MST
(koloruje na czarno) każdą krawędź, która nie
Następna krawędź
drzewa MSTma tworzy cyklu z wcześniej dodanymi. Proces koń
© kolor czerwony czy się po dodaniu V - 1 krawędzi. Czarne kra
wędzie tworzą las drzew, który stopniowo prze
Krawędzie grafu
(?) posortowane kształcany jest w pojedyncze drzewo — drzewo
© według wagi MST. Metoda ta to algorytm Kruskala.
Krawędź
© drzewa MST
(kolor czarny) Twierdzenie O. Algorytm Kruskala wy
© \
znacza drzewo MST dla dowolnego spójne
© 0 -7 0.1 6 go grafu ważonego.
2-3 0.17
1-7 0.19 Dowód. Wynika bezpośrednio z t w i e r d z e
© 0-2
5-7
0.2 6
0.28
n iak. Jeśli następna rozważana krawędź
1-3 0.29 nie tworzy cyklu względem czarnych kra
1-5 0.32
© © 2-7 0.34
4 -5 0.35
wędzi, to łączy przekrój wyznaczony przez
zbiór wierzchołków powiązanych z jednym
1-2 0.36 z wierzchołków krawędzi przez krawędzie
© 4-7 0.37
0-4 0.38 drzewa i dopełnienie tego zbioru. Ponieważ
6 -2 0.40 krawędź nie tworzy cyklu, jest jedyną napot
3-6 0.52
© 6-0 0.58 kaną do tej pory krawędzią przekroju, a po
6-4 0.93 nieważ krawędzie analizowane są według
X wag, jest to krawędź przekroju o minimalnej
Niepotrzebna
krawędź wadze. Tak więc algorytm po kolei pobie
(kolor szary)
ra krawędź przekroju o minimalnej wadze
©
Szare wierzchołki określają
(czyli działa w sposób zachłanny).
przekrój wyznaczony przez
. wierzchołki powiązane
zjednymz wierzchołków Algorytm Prima tworzy drzewo MST krawędź
czerwonej krawędzi
po krawędzi, znajdując w każdym kroku nową
© krawędź dołączaną do jednego rosnącego drze
wa. Algorytm Kruskala także tworzy drzewo
MST krawędź po krawędzi, natomiast wyszu
kuje krawędź łączącą dwa drzewa w lesie ros
nących drzew. Zaczynamy od niepełnego lasu
V drzew o jednym wierzchołku i wykonujemy
Ślad działania algorytmu Kruskala
operację łączenia dwóch drzew (za pomocą
najkrótszej możliwej krawędzi) do momentu,
w którym pozostaje tylko jedno drzewo — drze
wo MST.
4.3 a Minimalne drzewa rozpinające 637
Na rysunku na stronie 636 pokazano krok po kroku działanie algorytm u Krus-
kala na pliku [Link]. Pięć krawędzi o najmniejszych wagach jest dodawanych
do drzewa MST. Następnie algorytm uznaje krawędzie 1-3, 1-5 i 2-7 za niewybie-
ralne przed dołączeniem do drzewa MST krawędzi 4-5. Potem za niewybieralne
zostają uznane krawędzie 1-2, 4-7 i 0-4, po czym algorytm dodaje do drzewa MST
krawędź 6- 2 .
Z uwagi na omówione w książce narzędzia algorytmiczne także algorytm Kruskala
nietrudno jest zaimplementować. Stosujemy kolejkę priorytetową ( p o d r o z d z i a ł 2 .4 )
do analizowania krawędzi według wartości wag, strukturę Union-Find ( p o d r o z d z i a ł
1 .5) do wykrycia krawędzi powodujących cykle, a także kolejki ( p o d r o z d z i a ł 1 .3 )
do zapisywania krawędzi drzewa m s t . a l g o r y t m 4.8 to implementacja oparta na
tych strukturach. Zauważmy, że zapisywanie krawędzi drzewa MST w obiekcie Queue
oznacza, że klient w trakcie iterowania po krawędziach otrzyma je w kolejności ros
nącej według wag. Metoda wei ght () wymaga przejścia po kolejce i dodania wag kra
wędzi (można też przechowywać bieżącą sumę wag w zmiennej egzemplarza). Jej
napisanie pozostawiamy jako ć w i c z e n i e 4 .3 .3 1 .
Analiza czasu wykonania algorytmu Kruskala jest prosta, ponieważ znany jest
czas wykonania podstawowych operacji.
Twierdzenie N (ciąg dalszy). W algorytmie Kruskala przy wyznaczaniu drze
wa MST dla spójnego grafu ważonego o E krawędziach i V wierzchołkach ilość
wykorzystywanej pamięci jest proporcjonalna do E, a czas — do £ log E (dla
najgorszego przypadku).
Dowód. W implementacji wykorzystano konstruktor dla kolejek prioryteto
wych, który inicjuje kolejkę priorytetową wszystkimi krawędziami, co odbywa
się kosztem najwyżej E porównań (zobacz p o d r o z d z i a ł 2 .4 ). Po utworzeniu
kolejki priorytetowej dowód wygląda tak samo, jak dla algorytmu Prima. Liczba
krawędzi w kolejce priorytetowej wynosi najwyżej E (jest to ograniczenie ilości
pamięci), a koszt na operację wynosi najwyżej 2 lg £ porównań (jest to ograni
czenie ilości czasu). Algorytm Kruskala wykonuje też do £ operacji find () i do
V operacji uni on (), jednak koszty te nie wpływają na stopień wzrostu ogólnego
czasu wykonania, równy £ log £ (zobacz p o d r o z d z i a ł 1 .5 ).
Tak jak w algorytmie Prima, tak i tu ograniczenia kosztów są konserwatywne, po
nieważ algorytm kończy pracę po znalezieniu V - 1 krawędzi drzewa MST. Stopień
wzrostu rzeczywistych kosztów wynosi £ + £ 0 log £, gdzie EQto liczba krawędzi, któ
rych waga jest mniejsza niż waga krawędzi drzewa MST o najwyższej wadze. Mimo
to algorytm Kruskala jest ogólnie wolniejszy od algorytmu Prima, ponieważ dla każ
dej krawędzi wykonuje dodatkowo operację connected () (obok operacji na kolejce
priorytetowej, wykonywanych przez oba algorytmy dla każdej przetwarzanej krawę
dzi; zobacz ć w i c z e n i e 4 .3 .39 ).
638 ROZDZIAŁ 4 o Grafy
20 % Na rysunku po lewej stronie pokazano dynamiczny charak
ter algorytmu dla większego przykładu — pliku mediumEWG.
txt. Dość dobrze widać tu, że krawędzie są dodawane do lasu
zgodnie z ich długością.
40%
60%
Algorytm Kruskala
(250 wierzchołków)
w
4.3 Minimalne drzewa rozpinające 639
ALGORYTM 4.8. Algorytm Kruskala, służący do wyznaczania drzew MST
public c la s s KruskalMST
{
private Queue<Edge> mst;
public KruskalMST(EdgeWeightedGraph G)
{
mst = new Queue<Edge>();
MinPQ<Edge> pq = new MinPQ<Edge>([Link]());
UF uf = new UF(G. V ( ) );
while ( ! [Link]() && m [Link]() < G.V ()-1)
{
Edge e = [Link](); // Pobieranie z pq krawędzi
// o minimalnej
in t v = e .e it h e r ( ) , w = [Link] er(v); // wadze i powiązanych
// wierzchołków.
i f ([Link](v, w)) continue; // Pomijanie niewybieralnych
// krawędzi.
u f . union(v, w); // Scalanie komponentów.
[Link](e); // Dodawanie krawędzi do
// kolejki mst.
}
}
public Iterable<Edge> edges()
{ return mst; }
p ublic double weight() // Zobacz ćwiczenie 4.3.31.
}
W tej implementacji algorytmu Kruskala użyto kolejki do przechowywania krawędzi drze
wa MST, kolejki priorytetowej do przechowywania niesprawdzonych krawędzi i struktury
danych Union-Find do wykrywania niewybieralnych krawędzi. Krawędzie drzewa MST są
zwracane do klienta w kolejności rosnącej według wag. Napisanie metody weight() pozo
stawiamy jako ćwiczenie.
% java KruskalMST tinyEW [Link]
0-7 0.16
2-3 0.17
1-7 0.19
0-2 0.26
5-7 0.28
4-5 0.35
6-2 0.40
1.81
640 ROZDZIAŁ 4 a Grafy
Perspektywa Wyznaczanie drzew MST jest jednym z najlepiej przebadanych
problemów spośród omówionych w tej książce. Podstawowe rozwiązania wymyślono
na długo przed opracowaniem współczesnych struktur danych i technik analizowa
nia wydajności algorytmów — w czasach, kiedy wyznaczanie drzewa MST dla grafu
obejmującego na przykład 1000 krawędzi było bardzo żmudne. Opisane tu algorytmy
tworzenia drzew MST różnią się od dawnych głównie sposobem stosowania i wykorzy
staniem współczesnych algorytmów oraz struktur danych do wykonywania podstawo
wych zadań. Pozwala to (w połączeniu ze współczesnymi możliwościami obliczenio
wymi) wyznaczać drzewa MST o milionach, a nawet miliardach krawędzi.
Uwagi historyczne Implementację wyznaczającą drzewa MST dla grafów gęstych
(zobacz ć w i c z e n i e 4 .3 .29 ) po raz pierwszy zaprezentował R. Prim w 1961 roku,
a krótko potem, niezależnie od Prima, E.W. Dijkstra. Zwykle rozwiązanie nazywa się
algorytmem Prima, choć
technika Dijkstry jest ogól Stopień w zrostu dla najgorszego
niejsza. Jednak podstawo Algorytm przypadku dla Vwierzchołków i Ekrawędzi
wy pomysł zaprezentował Pamięć Czas
w 1939 roku V. Jarnik, Algorytm Prima p
j J- E logii
dlatego niektórzy autorzy (wersja leniwa)
nazywają metodę algoryt Algorytm Prima
V E log V
mem Jarnika, przypisując (wersja zachłanna)
Primowi (lub Dijkstrze) Algorytm Kruskala E £ logii
rolę twórcy wydajnej im Algorytm
plementacji algorytmu dla Fredmana-Tarjana V E + V log V
grafów gęstych. Po w pro Algorytm V Bardzo, bardzo blisko E
wadzeniu typu ADT dla Chazellea
kolejek priorytetowych Niemożliwe? V E
(początek lat 70. ubiegłe
go wieku) jego zastosowa- W ydajność algorytm ów do wyznaczania drzew M ST
nie do znajdowania drzew
MST dla grafów rzadkich
było proste. To, że drzewa MST dla grafów prostych można wyznaczyć w czasie pro
porcjonalnym do E log E, stało się powszechnie wiadome — żadnemu naukowcowi
nie przypisuje się wymyślenia tego rozwiązania. W 1984 roku M.L. Fredman i R.E.
Tarjan opracowali pewną strukturę danych, kopiec Fibonacciego, która pozwala obni
żyć teoretyczne ograniczenie stopnia wzrostu czasu wykonania algorytmu Prima do
E + Vlog V. J. Kruskal przedstawił swój algorytm w 1956 roku, jednak przez wiele lat
implementacje odpowiednich typów ADT nie zostały dokładnie przeanalizowane.
Oto inne ciekawostki historyczne — w pracy Kruskala opisana jest wersja algorytmu
Prima, a w pracy O. Boruvki z 1926 (!) roku przedstawiono oba podejścia. Praca
Boruvki dotyczyła dystrybucji prądu. Opisano w niej jeszcze inną metodę, łatwą do
zaimplementowania za pomocą współczesnych struktur danych (zobacz ć w i c z e n i a
4 .3.43 i 4 .3 .44 ). Metodę tę ponownie wymyślił M. Sollin w 1961 roku. Później metoda
4.3 h Minimalne drzewa rozpinające 641
ta zyskała popularność jako podstawa dla algorytmów do wyznaczania drzew MST
o wysokiej asymptotycznie wydajności i równoległych algorytmów do wyznaczania
drzew MST.
A lgorytm działający w czasie liniow ym Nie uzyskano natomiast żadnych teore
tycznych dowodów na to, że nie istnieje algorytm wyznaczania drzew MST dzia
łający dla wszystkich grafów w czasie liniowym. Jednak próby opracowania takich
algorytmów dla grafów rzadkich kończą się niepowodzeniem. Od lat 70. ubiegłego
wieku stosowanie abstrakcji typu Union-Find w algorytmie Kruskala i abstrakcji ko
lejki priorytetowej w algorytmie Prima są głównym powodem, dla którego wielu na
ukowców stara się rozwinąć lepsze implementacje tych typów ADT. Liczni badacze
koncentrują się na znalezieniu wydajnych implementacji kolejek priorytetowych, co
ma stać się kluczem do wydajnych algorytmów wyznaczania drzew MST dla grafów
rzadkich. Wielu innych naukowców analizowało odmiany algorytmu Boruvki jako
podstawy dla takich algorytmów działających w czasie bliskim liniowemu. Badania
te mogą ostatecznie doprowadzić do wymyślenia algorytmu wyznaczania drzew
MST działającego w praktyce w czasie liniowym. Wykazano nawet istnienie algo
rytmu z randomizacją cechującego się taką wydajnością. Ponadto badacze zbliżyli
się do celu, jakim jest liniowy czas wykonania. W 1997 roku B. Chazelle przedstawił
algorytm, który w dowolnej praktycznej sytuacji jest nieodróżnialny od algorytmu
działającego w czasie liniowym (choć można dowieść, że nie jest to taki algorytm).
Rozwiązanie jest jednak tak skomplikowane, że w praktyce nikt go nie stosuje. Choć
algorytmy opracowane w wyniku podobnych badań są przeważnie dość skompli
kowane, może się okazać, że uproszczone wersje niektórych z nich będą przydatne
w praktyce. Do tego czasu m ożna korzystać z podstawowych, omówionych tu algo
rytmów do wyznaczania drzew MST w czasie liniowym w większości praktycznych
sytuacji (czasem trzeba ponieść dodatkowe koszty w wysokości log V dla niektórych
grafów rzadkich).
w p o d s u m o w a n i u — problem wyznaczania drzew MST m ożna uznać za rozwiąza
ny na potrzeby zastosowań praktycznych. Dla większości grafów koszt wyznaczenia
drzewa MST jest tylko nieznacznie wyższy niż koszt wyodrębnienia krawędzi grafu.
Wyjątkiem od tej reguły są bardzo duże i wysoce rzadkie grafy. Jednak możliwa po
prawa wydajności w porównaniu z najlepszymi znanymi algorytmami nawet wtedy
jest równa małemu stałemu czynnikowi (prawdopodobnie wynoszącemu nie wię
cej niż 10). Wnioski te wynikają z wielu modeli grafów, a praktycy od dziesięcioleci
korzystają z algorytmów Prima i Kruskala do wyznaczania drzew MST dla bardzo
dużych grafów.
642 ROZDZIAŁ 4 a Grafy
; PYTANIA I ODPOWIEDZI
P. Czy algorytmy Prima i Kruskala działają dla grafów skierowanych?
O. Nie, w żadnym razie. Takich grafów dotyczy trudniejszy problem z obszaru prze
twarzania grafów — wyznaczanie drzewa o minimalnym koszcie.
4.3 n Minimalne drzewa rozpinające 643
ĆWICZENIA
4.3.1. Udowodnij, że można przeskalować wagi przez dodanie do każdej z nich dodat
niej stałej lub pomnożenie ich przez taką stałą i że nie wpływa to na drzewo MST.
4.3.2. Narysuj wszystkie drzewa MST dla grafu przedstawionego po prawej
(wagi wszystkich krawędzi są identyczne).
4.3.3. Wykaż, że jeśli wszystkie krawędzie grafu mają różne wagi, drzewo
MST jest unikatowe.
4.3.4. Rozważ twierdzenie, że grafowi ważonemu odpowiada unikatowe drzewo tyl
ko wtedy, jeśli wagi krawędzi są różne. Przedstaw dowód lub kontrprzykład.
4.3.5. Wykaż, że algorytm zachłanny działa poprawnie nawet wtedy, kiedy wagi kra
wędzi nie są różne.
4.3.6. Przedstaw drzewo MST grafu ważonego uzyskanego po usunięciu wierzchoł
ka 7 z pliku [Link] (zobacz stronę 616).
4.3.7. Jak wyznaczysz maksymalne drzewo rozpinające grafu ważonego?
4.3.8. Udowodnij tak zwaną właściwość cyklu — dla dowolnego cyklu w grafie wa
żonym (o różnych wagach krawędzi) krawędź o maksymalnej wadze w cyklu nie
należy do drzewa MST grafu.
4.3.9. Zaimplementuj konstruktor klasy EdgeWei ghtedGraph, który wczytuje graf ze
strumienia wejściowego. W tym celu zmodyfikuj konstruktor klasy Graph (zobacz
stronę 538).
4.3.10. Opracuj implementację klasy EdgeWei ghtedGraph dla grafów gęstych opartą
na reprezentacji w postaci macierzy sąsiedztwa (dwuwymiarowej tablicy wag). Nie
zezwalaj na występowanie krawędzi równoległych.
4.3.11. Określ ilość pamięci potrzebną w klasie EdgeWei ghtedGraph do reprezen
towania grafu o U wierzchołkach i E krawędziach. Zastosuj model kosztów opisany
W PODROZDZIALE 1 .4.
4.3.12. Załóżmy że krawędzie w grafie mają różne wagi. Czy najkrótsza krawędź
musi należeć do drzewa MST? Czy najdłuższa krawędź może należeć do drzewa
MST? Czy krawędź o minimalnej wadze z każdego cyklu należy do drzewa MST?
Udowodnij odpowiedź na każde pytanie lub przedstaw kontrprzykład.
4.3.13. Przedstaw kontrprzykład pokazujący dlaczego opisana strategia nie zawsze
wyznacza drzewo MST. Oto ta strategia: zacznij od dowolnego wierzchołka trakto
wanego jak drzewo MST o jednym wierzchołku. Następnie dodaj do niego V-1 kra
wędzi, zawsze pobierając następną krawędź o minimalnej wadze przyległą do wierz
chołka dodanego w ostatnim kroku do drzewa MST.
644 ROZDZIAŁ 4 0 Grafy
ĆWICZENIA (ciąg dalszy)
4.3.14. Wyznaczono drzewo MST dla grafu ważonego G. Załóżmy, że z grafu G usu
wamy krawędź, której brak nie prowadzi do utraty spójności. Opisz, jak wyznaczyć
drzewo MST nowego grafu w czasie proporcjonalnym do E.
4.3.15. Mamy drzewo MST dla grafu ważonego G i nową krawędź e. Opisz, jak zna
leźć drzewo MST nowego grafu w czasie proporcjonalnym do U.
4.3.16. Mamy drzewo MST dla grafu ważonego G i nową krawędź e. Napisz program
określający zakres wag krawędzi e, przy których znajdzie się ona w drzewie MST.
4.3.17. Zaimplementuj metodę to S trin g O dla klasy EdgeWeightedGraph.
4.3.18. Przedstaw ślady przebiegu procesu wyznaczania drzewa MST dla grafu
z ć w i c z e n i a 4 .3 .6 . Użyj leniwej wersji algorytmu Prima, zachłannej wersji algoryt
m u Prima oraz algorytmu Kruskala.
4.3.19. Załóżmy, że korzystasz z implementacji kolejki priorytetowej opartej na li
ście posortowanej. Jakie jest tempo wzrostu czasu wykonania dla najgorszego przy
padku przy korzystaniu z algorytmu Prima i algorytmu Kruskala dla grafów o V
wierzchołkach i E krawędziach? Kiedy takie podejście jest odpowiednie (jeśli w ogóle
występują taicie sytuacje)? Odpowiedź uzasadnij.
4.3.20. W dowolnym momencie działania algorytmu Kruskala każdy wierzchołek
jest bliższy pewnemu wierzchołkowi w poddrzewie niż dowolnemu wierzchołkowi
spoza poddrzewa — prawda czy fałsz? Odpowiedź udowodnij.
4.3.21. Przedstaw implementację m etody edges() z klasy PrimMST (strona 634).
Rozwiązanie:
public Iterable<Edge> edges()
(
Bag<Edge> mst = new Bag<Edge>();
fo r (in t v = 1; v < [Link]; v++)
[Link](edgeTo[v]);
return mst;
}
4.3 a Minimalne drzewa rozpinające 645
| PROBLEMY DO ROZWIĄZANIA
4.3.22. Minimalny las rozpinający. Opracuj wersje algorytmów Prima i Kruskala
wyznaczające m inimalny las rozpinający dla grafu ważonego, który może być nie
spójny. Wykorzystaj interfejs API do określania spójnych składowych ( p o d r o z d z i a ł
4 .1 ) i wyznacz drzewo MST dla każdej składowej.
4.3.23. Algorytm Vyssotskyego. Opracuj implementację, która wyznacza drzewo MST
na podstawie wielokrotnego wykorzystania właściwości cyklu (zobacz ć w i c z e n i e
4 .3 .8). Należy dodawać krawędzie po jednej do potencjalnego drzewa i usuwać kra
wędź o maksymalnej wadze z cyklu, kiedy ten powstanie. Uwaga: technika ta cieszy
się mniejszą popularnością niż inne omawiane metody, ponieważ stosunkowo tru d
no jest utrzymywać strukturę danych, która umożliwia wydajne zaimplementowanie
operacji „usuń z cyklu krawędź o maksymalnej wadze”.
4.3.24. Algorytm odwróć-usuń. Opracuj kod, który wyznacza drzewo MST w nastę
pujący sposób: zacznij od grafu obejmującego wszystkie krawędzie. Następnie wie
lokrotnie przejdź po krawędziach w porządku malejącym według wag. Dla każdej
krawędzi sprawdź, czy jej usunięcie prowadzi do powstania niespójnego grafu. Jeśli
nie, krawędź należy usunąć. Udowodnij, że algorytm wyznacza drzewo MST. Jaki jest
stopień wzrostu liczby porównań wag krawędzi wykonywanych przez kod?
4.3.25. Generator najgorszego przypadku. Opracuj sensowny generator grafów wa
żonych o V wierzchołkach i E krawędziach, dla których czas wykonania leniwej wersji
algorytmu Prim a nie jest liniowy. Wykonaj to samo ćwiczenie dla wersji zachłannej.
4.3.26. Krawędzie krytyczne. Krawędź drzewa MST, której usunięcie z grafu po
woduje zwiększenie wagi drzewa MST, to tak zwana krawędź krytyczna. Pokaż, jak
znaleźć wszystkie krawędzie krytyczne grafu w czasie proporcjonalnym do E log E.
Uwaga: w tym pytaniu zakładamy, że wagi krawędzi nie muszą być różne (przy róż
nych wagach wszystkie krawędzie drzewa MST są krytyczne).
4.3.27. Animacje. Napisz program kliencki, który generuje animacje pracy algo
rytmów wyznaczania drzew MST. Uruchom program dla pliku [Link].
Program ma wygenerować rysunki podobne do tych ze stron 633 i 636.
4.3.28. Struktury danych wydajne ze względu na pamięć. Opracuj implementację
leniwej wersji algorytmu Prima, wymagającą mniej pamięci. W tym celu zastosuj
dla EdgeWeightedGraph i Mi nPQ struktury danych niższego poziomu niż Bag i Edge.
Oszacuj ilość zaoszczędzonej pamięci jako funkcję od V i E. Posłuż się modelem
kosztów opisanym w p o d r o z d z i a l e 1.4 (zobacz ć w i c z e n i e 4 .3 . 1 1 ).
646 ROZDZIAŁ 4 o Grafy
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
4 .3.29. Grafy gęste. Opracuj implementację algorytmu Prima, opartą na podejściu
zachłannym (ale bez kolejki priorytetowej) i wyznaczającą drzewo MST za pomocą
V2 porównań wag krawędzi.
4.3.30. Euklidesowe grafy ważone. Zmodyfikuj rozwiązanie ć w i c z e n i a 4 . 1 .37 , aby
utworzyć interfejs API Eucl i deanEdgeWei ghtedGraph dla grafów, których wierzchołki
są punktam i w przestrzeni. Pozwoli to korzystać z reprezentacji graficznych.
4.3.31. Wagi drzew MST. Opracuj implementacje m etody w eight() dla klas
LazyPrimMST, PrimMST i KruskalMST, wykorzystując leniwą strategię, z iterowaniem po
krawędziach drzewa MST w momencie wywołania m etody wei ght () przez klienta.
Następnie opracuj inne implementacje, oparte na strategii zachłannej, z przechowy
waniem bieżącej sumy przy wyznaczaniu drzewa MST.
4.3.32. Określony zbiór. Mamy spójny graf ważony G i określony zbiór krawędzi S
(bez cykli). Opisz sposób wyznaczania minimalnego drzewa rozpinającego dla grafu
G, przy czym drzewo m a obejmować wszystkie krawędzie z S.
4.3.33. Sprawdzanie. Napisz metodę kliencką check() (korzystającą z klas MST
i EdgeWeightedGraph), opartą na wynikających z t w i e r d z e n i a j warunkach opty-
malności ze względu na przekrój, które pozwalają stwierdzić, że proponowany zbiór
krawędzi jest drzewem MST. Oto te warunki — zbiór krawędzi jest drzewem MST,
jeśli jest drzewem rozpinającym, a każda krawędź jest krawędzią o minimalnej wadze
dla przekroju wyznaczonego przez usunięcie tej krawędzi z drzewa. Jakie jest tempo
wzrostu czasu wykonania dla tej metody?
4.3 □ Minimalne drzewa rozpinające 647
I EKSPERYMENTY
4.3.34. Losowe rzadkie grafy ważone. Napisz generator losowych rzadldch grafów
ważonych oparty na rozwiązaniu ć w i c z e n i a 4 .1 .4 1 . Aby przypisać wagi krawę
dziom, zdefiniuj typ ADT dla losowych digrafów ważonych i napisz dwie implemen
tacje — jedną generującą wagi o rozkładzie równomiernym i jedną generującą wagi
0 rozkładzie Gaussa. Opracuj program kliencki do generowania losowych rzadkich
grafów ważonych na podstawie obu rozkładów wag i odpowiednio dobranych war
tości V i E, tak aby można przeprowadzić empiryczne testy na grafach o różnych
rozkładach wag.
4.3.35. Losowe euklidesowe grafy ważone. Zmodyfikuj rozwiązanie ć w i c z e n i a
4 .1.42 przez zapisanie odległości między wierzchołkami jako wagi każdej krawędzi.
4.3.36. Losowe grafy ważone oparte na siatce. Zmodyfikuj rozw iązanie ć w i c z e n i a
4 .1.43 przez przypisanie losowej wagi (z przedziału od 0 do 1 ) do każdej krawędzi.
4.3.37. Grafy ważone w świecie rzeczywistym. Znajdź w internecie duży graf ważo
ny. Może to być mapa z odległościami, połączenia telefoniczne z kosztami lub plan
lotów z cenami. Napisz program RandomReal EdgeWeightedGrap, tworzący graf wa
żony przez wybranie V losowych wierzchołków i E krawędzi z wagami z podgrafu
opartego na tych wierzchołkach.
Testowanie wszystkich algorytmów i badanie każdego parametru w każdym modelu
grafów jest niewykonalne. Dla każdego z wymienionych dalej problemów napisz klien
ta, który rozwiązuje problem dla dowolnego grafu wejściowego. Następnie wybierz je
den z opisanych wcześniej generatorów do przeprowadzenia eksperymentów dla danego
modelu grafów. Wykorzystaj własną ocenę sytuacji przy doborze eksperymentów (mo
żesz oprzeć się na wynikach wcześniejszych pomiarów). Napisz wyjaśnienie wyników
1wnioski, które można z nich wyciągnąć.
4.3.38. Koszty leniwego podejścia. Przeprowadź badania empiryczne, aby porównać
wydajność leniwej i zachłannej wersji algorytmu Prima. Uwzględnij różne rodzaje
grafów.
4.3.39. Algorytmy Prima i Kruskala. Przeprowadź badania empiryczne, aby porów
nać wydajność leniwej i zachłannej wersji algorytmu Prima z wydajnością algorytmu
Kruskala.
4.3.40. Zmniejszone koszty ogólne. Przeprowadź badania empiryczne, aby usta
lić skutki zastosowania typów prostych zamiast wartości typu Edge w klasie
EdgeWeightedGraph (zobacz ć w i c z e n i e 4 .3 .28 ).
648 ROZDZIAŁ 4 a Grafy
EKSPERYMENTY (ciąg dalszy)
4 .3.41. Najdłuższa krawędź drzewa MST. Przeprowadź badania empiryczne, aby
przeanalizować długość najdłuższej krawędzi drzewa MST i liczbę krawędzi grafu,
które nie są od niej dłuższe.
4.3.42. Podział. Opracuj implementację opartą na połączeniu algorytmu Kruskala
z podziałem z sortowania szybkiego (zastosowanym zamiast kolejki priorytetowej),
co pozwoli ustalić przynależność każdej krawędzi do drzewa MST bezpośrednio po
sprawdzeniu wszystkich mniejszych krawędzi.
4.3.43. Algorytm Boruvki. Opracuj implementację algorytmu Boruvki. Należy
utworzyć drzewo MST przez dodawanie krawędzi do rosnącego lasu drzew — tak
jak w algorytmie Kruskala, ale etapami. Na każdym etapie należy znaleźć krawędź
o minimalnej wadze łączącą każde drzewo z innym, a następnie dodać wszystkie te
krawędzie do drzewa MST. Aby uniknąć cykli, zakładamy, że wagi wszystkich krawę
dzi są różne. Wskazówka: utrzymuj indeksowaną wierzchołkami tablicę do identyfi
kowania krawędzi łączących każdą składową z jej najbliższym sąsiadem oraz wyko
rzystaj strukturę Union-Find.
4.3.44. Usprawniony algorytm Boruvki. Opracuj implementację algorytmu Boruvki,
opartą na zastosowaniu podwójnie powiązanych list cyklicznych do reprezentowa
nia poddrzew MST, co na każdym etapie umożliwi scalanie i przemianowywanie
poddrzew w czasie proporcjonalnym do E (ponadto niepotrzebna staje się struktura
Union-Find).
4.3.45. Zewnętrzne drzewa MST. Opisz, jak wyznaczyć drzewo MST grafu, który
jest tak duży, że w danym momencie w pamięci mieści się tylko U krawędzi.
4.3.46. Algorytm Johnsona. Opracuj implementację kolejki priorytetowej opartą na
kopcu z węzłami o d dzieciach (zobacz ć w i c z e n i e 2 .4 .4 1 ). Określ najlepszą war
tość d dla różnych modeli grafów ważonych.
4.4. N A JK R Ó T S Z E Ś C IE Ż K I
problem z obszaru przetwarzania gra
p r a w d o p o d o b n ie n a jb a r d z ie j in t u ic y j n y
fów dotyczy zadania często wykonywanego na przykład przy korzystaniu z mapy
elektronicznej lub systemu nawigacyjnego w celu uzyskania trasy z jednego miejsca
do drugiego. W takiej sytuacji zastosowanie modelu grafu jest oczywiste. Wierzchołki
odpowiadają skrzyżowaniom, a krawędzie — drogom, przy czym wagi krawędzi re
prezentują koszty, na przykład odległość lub czas przejazdu. Możliwość występowa
nia dróg jednokierunkowych oznacza, że trzeba uwzględnić digrafy ważone. W tym
modelu problem jest łatwy do sformułowania:
Znajdź najmniej kosztowną drogę z jednego wierzchołka do drugiego.
Oprócz bezpośrednich zastosowań tego rodzaju model najkrótszych ścieżek m oż
na wykorzystać w wielu innych problemach. Niektóre z nich na pozór w ogóle nie
są związane z przetwarzaniem grafów. Jednym z przykładów jest problem arbitrażu
z obszaru finansów, omówiony w końcowym fragmencie podrozdziału.
Przyjęliśmy ogólny model
oparty na digrafach ważonych Zastosowanie Wierzctiołek Krawędź
(stanowi on połączenie m ode Mapa Skrzyżowanie Droga
li Z PODROZDZIAŁÓW 4-2 i 4.3 ).
Sieć Ruter Połączenie
W p o d r o z d z i a l e 4.2 ważne było
ustalenie, czy można przejść z jed Ograniczenia
Plan zadań Zadanie
nego wierzchołka do innego. Tu pierwszeństwa
uwzględniane są wagi, podobnie Arbitraż Waluta Kurs wymiany
jak w nieskierowanych grafach Typowe zastosowania najkrótszych ścieżek
ważonych w p o d r o z d z i a l e 4 .3 .
Każda ścieżka skierowana w digra-
fie ważonym jest powiązana z wagą ścieżki, czyli sumą wag krawędzi ścieżki. Ta klu
czowa miara umożliwia sformułowanie problemu
Digraf ważony
4->5 0.35
w następujący sposób: „Znajdź mającą najniższą
5->4 0.35 wagę ścieżkę skierowaną z jednego wierzchołka
4->7 0.37
5->7 0.28
do drugiego”. Problem ten jest tematem podroz
7->5 0.28 działu. Na rysunku po lewej stronie przedstawio
5->1 0.32
no przykład.
0->4 0.38
0->2 0.26
7->3 0.39 Najkrótsza ścieżka z 0 do 6
1->3 0.29
0->2 0.26
2->7 0.34 Definicja. Najkrótsza ścieżka z wierzchołka s
2->7 0.34
6->2 0.40
3->6 0.52
7->3 0.39 do wierzchołka t w digrafie ważonym to ścież
3->6 0.52
6->0 0.58 ka skierowana z s do t, cechująca się tym, że
6->4 0.93
żadna inna ścieżka nie ma niższej wagi.
Digraf ważony i najkrótsza ścieżka
650
4.4 a Najkrótsze ścieżki 651
Tak więc w tym podrozdziale omawiamy klasyczne algorytmy dotyczące następują
cego problemu.
Najkrótsze ścieżki z jednego źródła. Dla digrafu ważonego i źródłowego wierz
chołka s zapewnij obsługę zapytań w postaci: Czy istnieje skierowana ścieżka z s do
danego docelowego wierzchołka t? Jeśli tak, należy znaleźć najkrótszą taką ścieżkę
(o minimalnej łącznej wadze).
Celem w tym podrozdziale jest omówienie poniższej listy zagadnień. Oto one:
0 opracowane przez nas interfejsy API i implementacje digrafów ważonych oraz
interfejs API do wyznaczania najkrótszych ścieżek z jednego źródła;
0 klasyczny algorytm Dijkstry dla wag nieujemnych;
° szybszy algorytm dla acyklicznych digrafów ważonych (ważonych grafów
DAG), działający także dla wag ujemnych;
D klasyczny algorytm Bellmana-Forda do ogólnego użytku — kiedy mogą występo
wać cykle i wagi ujemne oraz potrzebne są algorytmy do wyszukiwania cykli o wa
dze ujemnej i najkrótszych ścieżek w digrafach ważonych bez tego rodzaju cykli.
W kontekście algorytmów omawiamy też ich zastosowania.
Cechy najkrótszych ścieżek Podstawowa definicja problemu wyznaczania
najkrótszych ścieżek jest zwięzła, jednak nie poruszono w niej kilku kwestii, którym
warto się przyjrzeć przed rozpoczęciem tworzenia algorytmów i struktur danych
w celu rozwiązania problemu.
a Ścieżki są skierowane. W najkrótszej ścieżce trzeba uwzględnić kierunek kra
wędzi.
° Wagi nie zawsze odpowiadają odległościom. Intuicyjne, geometryczne ujęcie
może pomóc w zrozumieniu algorytmów, dlatego w przykładach wierzchoł
ki są punktam i w przestrzeni, a wagi — odległościami euklidesowymi, tak jak
w digrafie na następnej stronie. Jednak wagi mogą też reprezentować czas, koszt
lub zupełnie inną zmienną, dlatego w ogóle nie muszą być proporcjonalne do
odległości. Podkreślamy to, łącząc metafory — najkrótszą ścieżką jest tu ścieżka
o minimalnej wadze lub najniższym koszcie.
0 Nie wszystkie wierzchołki muszą być osiągalne. Jeśli t nie jest osiągalny z s, w ogó
le nie istnieje ścieżka w tym kierunku, dlatego nie m a też najkrótszej ścieżki z s
do t. Dla uproszczenia krótki, stosowany tu przykład to graf silnie spójny (każ
dy wierzchołek jest osiągalny z każdego innego wierzchołka).
° Wagi ujemne prowadzą do komplikacji. Na razie zakładamy, że wagi wszystkich
krawędzi są dodatnie (lub zerowe). Zaskakujące skutki zastosowania wag ujem
nych są głównym tematem ostatniego fragmentu podrozdziału.
B Najkrótsze ścieżki są zwykle proste. W algorytmach pomijane są krawędzie o ze
rowej wadze, dlatego wyznaczone najkrótsze ścieżki nie mają cykli.
■ Najkrótsze ścieżki nie zawsze są unikatowe. Może istnieć kilka ścieżek o najniż
szej wadze z jednego wierzchołka do drugiego. Zadowalamy się znalezieniem
jednej z nich.
652 ROZDZIAŁ 4 h Grafy
Reprezentacja tablicowa M ogę wysforować krawędzie równoległe i pętle
z krawędziam i do rodzica
własne. Uwzględniana jest tylko krawędź o naj
\ niższej wadze spośród krawędzi równoległych,
n u li
5 -> l a żadna najkrótsza ścieżka nie obejmuje pęt
0->2
7 -> 3
li własnej (wyjątkiem może być pętla o wadze
0 -> 4 zero, którą i tak pomijamy). W tekście niejawnie
4->5
3 ->6 zakładamy, że nie występują krawędzie równole
2 ->7
6->0 głe; pozwala to zastosować zapis v->w do jedno
nul 1
6 -> 2 znacznego wskazywania krawędzi z v do w, przy
1 ->3
6 -> 4
czym kod obsługuje też krawędzie równoległe.
7 ->5
3->6 Drzewo najkrótszych ścieżek Koncentrujemy się na
2 ->7 6->0 problemie wyznaczania najkrótszych ścieżek z jednego
5 -> l
nul 1 źródła, gdzie podawany jest wierzchołek źródłowy s.
1->3
Efektem obliczeń jest drzewo najkrótszych ścieżek (ang.
5->4
7->5 shortest-paths tree — SPT), które określa najkrótszą
3->6
2-> 7 ścieżkę z s do każdego wierzchołka osiągalnego z s.
Źródło
Definicja. Dla digrafu ważonego i określonego
wierzchołka s drzewo najkrótszych ścieżek wierz
chołka źródłowego s to podgraf obejmujący s
6->0
5->l i wszystkie wierzchołki osiągalne z s oraz tworzący
6 -> 2 drzewo skierowane o korzeniu w s. W drzewie tym
7->3
nul 1 każda ścieżka jest najkrótszą ścieżką w digrafie.
4->5
3->6
6 -> l 4 -> 7
5 -> l
Zawsze istnieje drzewo tego rodzaju. Mogą istnieć
6->2 (Ś V dwie ścieżki o tej samej długości łączące s z wierz
l- > 3 r p
5->4 chołkiem. Wtedy m ożna usunąć ostatnią krawędź
nul 1 11
3->6 m )
jednej z takich ścieżek i kontynuować ten proces
5->7 6->0 do czasu pozostania jednej ścieżki łączącej źródło
5->l
G D '' 6 -> 2
z każdym wierzchołkiem (powstaje wtedy drzewo
7->3 z korzeniem). Przez utworzenie drzewa najkrótszych
6->4
1| ,
7->5 ścieżek można udostęp - Krawędzie
& n u li nić klientom najkrótszą prow adzą o d źródła
6 -> 0 2->7
5 -> l ścieżkę z s do dowolne
6->2 (Tjz, go wierzchołka grafu,
7->3
5->4 11 posługując się repre
7->5 fi
3->6 ( 4) zentacją z krawędziami
n u li
do rodzica (to samo
Drzewa najkrótszych ścieżek podejście zastosowano
do ścieżek w grafach
W PODROZDZIALE 41. 1 ).
'
„ CDT
Drzewo SPT o 250 wierzchołkach
.
4.4 a Najkrótsze ścieżki 653
Typy danych dla digrafów ważonych Opracowany przez nas typ danych dla
krawędzi skierowanych jest prostszy niż typ dla krawędzi nieskierowanych, ponie
waż krawędzie skierowane prowadzą w jednym kierunku. Zamiast metod e ith e r()
io th e r() z klasy Edge, tu występują m etody from() i to ().
p ub lic c la s s DirectedEdge
D ire cte d Ed ge (in t v, in t w, double weight)
doubl e weight () Zwraca wagę danej krawędzi
in t from() Zwraca wierzchołek,
z którego wychodzi krawędź
in t t o () Zwraca wierzchołek,
do którego prowadzi krawędź
S t r in g t o S t r in g O Zwraca reprezentację
w postaci łańcucha znaków
Interfejs API dla krawędzi skierowanych z wagam i
Podobnie jak przy zmianie z typu Graph ( p o d r o z d z i a ł 4 . 1 ) na EdgeWeightedGraph
( p o d r o z d z i a ł 4 .3 ), tak i tu dołączamy metodę edges () i stosujemy typ Di rectedEdge
zamiast liczb całkowitych.
p u b lic c la s s EdgeWeightedDigraph
EdgeW eightedDigraph(int V) Zwraca pusty digraf o V wierzchołkach
EdgeWeightedDigraph (In in ) Tworzy digraf na podstawie in
in t V() Zwraca liczbę wierzchołków
in t E() Zwraca liczbę krawędzi
void addEdge(DirectedEdge e) Dodaje e do digrafu
Ite rab le<D irecte d E d ge> a d j(in t v) Zwraca krawędzie wychodzące z v
Ite rab le<D irecte d E d ge> edges() Zwraca wszystkie krawędzie digrafu
Zwraca reprezentację w postaci
S t r in g t o S t r in g O
łańcucha znaków
Interfejs API dla digrafów ważonych
Implementacje dwóch przedstawionych interfejsów API znajdują się na dwóch na
stępnych stronach. Są to naturalne rozwinięcia implementacji z p o d r o z d z i a ł ó w
4.2 i 4 .3 . Zamiast list sąsiedztwa z liczbami całkowitymi, które stosowano w kla
sie Digraph, w klasie EdgeWeightedDigraph wykorzystano listy sąsiedztwa z obiek
tami DirectedEdge. Tak jak zmiana typu Graph ( p o d r o z d z i a ł 4 .1 ) na Digraph
( p o d r o z d z i a ł 4 .2 ), tak i przejście z typu EdgeWei ghtedGraph ( p o d r o z d z i a ł 4 . 3 ) na
EdgeWeightedDigraph (w tym podrozdziale) pozwala uprościć kod, ponieważ każda
krawędź występuje w strukturze danych tylko jednokrotnie.
6 54 ROZDZIAŁ 4 Grafy
Typ danych dla skierowanych krawędzi z w agam i
public c la ss DirectedEdge
{
private final in t v; // Krawędź źródTowa.
private final in t w; // Krawędź docelowa,
private final double weight; // Waga krawędzi.
public DirectedEdge(int v, in t w, double weight)
{
t h i s . v = v;
t h i s . w = w;
this.w eight = weight;
}
public double weightQ
{ return weight; )
public in t from()
{ return v; }
public in t to()
{ return w; }
public S trin g t o S t r in g O
{ return [Link] at("%d->%d % .2 f", v, w, weight); }
}
Powyższa implementacja klasy Di rectedEdge jest prostsza niż implementacja dla nieskiero-
wanych krawędzi ważonych (klasa Edge z p o d r o z d z i a ł u 4 .3 ; zobacz stronę 622), ponieważ
dwa wierzchołki są tu odróżniane od siebie. W klientach do dostępu do dwóch wierzchoł
ków obiektu e typu Di rectedEdge służy idiomatyczny kod v = e . t o () , w = [Link];.
4.4 Najkrótsze ścieżki 655
Typ danych dla w ażonych digrafów
public c la s s EdgeWeightedDigraph
{
private final in t V; // Liczba wierzchołków,
private in t E; // Liczba krawędzi,
private Bag<DirectedEdge>[] adj; // L i s t y sąsiedztwa.
p ublic EdgeWeightedDigraph(int V)
{
t h is .V = V;
th is.E = 0 ;
adj = (Bag<DirectedEdge>[]) new Bag[ V ] ;
fo r (in t v = 0; v < V; v++)
adj [v] = new Bag<DirectedEdge>();
}
p ublic EdgeWeightedDigraph(In in)
// Zobacz ćwiczenie 4.4.2.
p ublic in t V() { return V; )
p ublic in t E() { return E; )
public void addEdge(DirectedEdge e)
{
adj [[Link]] .add(e);
E++;
}
public Iterable<Edge> a d j( in t v)
{ return adj [ v ] ; }
public Iterable<DirectedEdge> edges()
{
Bag<DirectedEdge> bag = new Bag<DirectedEdge>();
f o r (in t v = 0; v < V; v++)
for (DirectedEdge e : adj [ v ] )
[Link](e);
return bag;
}
}
Implementacja klasy EdgeWeightedDigraph jest połączeniem Idas EdgeWeightedGraph
i Di graph. Przechowywana jest tu indeksowana wierzchołkami tablica wielozbiorów obiek
tów Di rectedEdge. Tak jak w klasie Di graph, tak i tu każda krawędź występuje tylko raz.
Jeśli krawędź łączy v z w, pojawia się na liście sąsiedztwa v. Pętle własne i krawędzie rów
noległe są dozwolone. Napisanie implementacji metody t o S t r i n g O pozostawiamy jako
ć w i c z e n i e 4.4.2 .
656 ROZDZIAŁ 4 n Grafy
t i nyEW D.t x t
8
15
4 5 0.35
5 4 0.35
4 7 0.37
5 7 0.28
7 5 0.28
5 1 0.32
0 4 0.38
0 2 0.26
7 3 0.39
1 3 0 .2 9
2 7 0 .3 4
6 2 0.40
3 6 0.52
6 0 0.58
6 4 0.93
Reprezentacja digrafów ważonych
Na powyższym rysunku pokazano strukturę danych, którą klasa EdgeWei g h te d D i graph
tworzy jako reprezentację digrafu wyznaczanego przez krawędzie przedstawione po
lewej stronie po dodaniu ich w przedstawionej kolejności. Jak zwykle stosujemy typ
Bag do reprezentowania list sąsiedztwa i przedstawiamy je jako listy powiązane (jest
to standardowa reprezentacja). Tak jak w digrafach bez wag ( p o d r o z d z i a ł 4 . 2 ), tak
i tu w strukturze danych występuje tylko jedna reprezentacja każdej krawędzi.
Interfejs A P I do w yznaczania najkrótszych ścieżek Do wyznaczania najkrót
szych ścieżek stosujemy ten sam paradygmat projektowy, co w interfejsach API klas
D epthFirstPaths i BreadthFi rstP ath s z p o d r o z d z i a ł u 4 . 1 . Opracowane przez nas
algorytmy to implementacje poniższego interfejsu API, udostępniającego klientom
najkrótsze ścieżki i ich długości.
p u b lic c la s s SP
SP (EdgeWei ghtedDi graph G, in t s) Konstruktor
double d is t T o ( in t v) Zwraca odległość z s do v
(°°, jeśli ścieżka nie istnieje)
boolean hasPathT o(in t v) Czy istnieje ścieżka z s do v?
Ite ra b l e<Di rectedEdge> p a th T ofint v) Zwraca ścieżkę z s do v
(nul 1, jeśli ścieżka nie istnieje)
Interfejs API dla implementacji klasy do wyznaczania najkrótszych ścieżek
Konstruktor tworzy drzewo najkrótszych ścieżek i wyznacza odległości takich ście
żek. Metody obsługi zapytań klienta korzystają z tych struktur przy udostępnianiu
klientom długości i ścieżek (z możliwością ¿terowania).
4.4 □ Najkrótsze ścieżki 657
K lie n t te s to w y Poniżej przedstawiono przykładowego klienta. Przyjmuje on stru
mień wejściowy i indeks wierzchołka źródłowego jako argumenty wiersza poleceń,
wczytuje digraf ważony ze strum ienia wejściowego, wyznacza drzewo SPT na pod
stawie digrafu i źródła oraz wyświetla
najkrótszą ścieżkę ze źródła do każdego p u b lic s t a t ic void m a in (S trin g [] args)
z pozostałych wierzchołków. Zakładamy, {
EdgeWeightedDigraph G;
że klient testowy dostępny jest we wszyst G = new EdgeWeightedDigraph(new In ( a r g s [ 0 ] ) ) ;
kich implementacjach klas do wyzna in t s = In t e g e r . p a r s e ln t ( a r g s [ l] );
czania najkrótszych ścieżek. W przykła SP sp = new SP(G, s ) ;
dach korzystamy z pliku [Link], fo r ( in t t = 0; t < G .V (); t++)
przedstawionego na następnej stronie, {
w którym określone są krawędzie i wagi S td O u t.p rin t(s + " do " + t ) ;
S t d O u t . p r in t f (" (% 4 .2 f): ", s p . d i s t T o ( t ) ) ;
małego digrafu. Używamy go w śladach
i f (sp .h a sP a th T o (t))
działania algorytmów wyznaczania naj f o r (DirectedEdge e : sp .p a th T o (t))
krótszych ścieżek. Zastosowano tu ten Std O u t.p rin t(e + " " ) ;
S t d O u t . p r in t ln ( ) ;
sam format pliku, co dla algorytmów wy
1
znaczania drzew MST. Najpierw znajduje 1
się liczba wierzchołków V, dalej liczba
wierzchołków E, a następnie E Wier- Klient testowy dla algorytm ów wyznaczania
szy, z których każdy obejmuje indeksy najkrótszych ścieżek
dwóch wierzchołków i wagę. W poświę
conej książce witrynie znajdują się pliki
z kilkoma większymi digrafami skierowanymi (między innymi plik mediumEWD.
txt z definicją grafu o 250 wierzchołkach, przedstawionego na stronie 652). Na ry
sunku grafu każda linia reprezentuje krawędzie w obu kierunkach, dlatego plik ma
dwa razy więcej wierszy niż odpowiadający mu plik [Link], analizowany
w kontekście drzew MST. Na rysunku drzewa SPT każda linia reprezentuje krawędź
skierowaną od źródła do docelowego wierzchołka.
% java SP tinyEW [Link] 0
0 do 0 (0 .0 0):
0 do 1 (1 .0 5): 0->4 0.38 4->5 0.35 5 - > l 0.32
0 do 2 (0 .2 6): 0->2 0.26
0 do 3 (0 .9 9): 0->2 0.26 2->7 0.34 7->3 0.39
0 do 4 (0 .3 8): 0->4 0.38
0 do 5 (0 .7 3): 0->4 0.38 4->5 0.35
0 do 6 (1 .5 1): 0->2 0.26 2->7 0.34 7->3 0.39
0 do 7 (0 .6 0): 0->2 0.26 2->7 0.34
658 ROZDZIAŁ 4 □ Grafy
Struktury danych do wyznaczania najkrótszych ścieżek Struktury danych po
trzebne do wyznaczania najkrótszych ścieżek są proste.
D Krawędzie w drzewie najkrótszych ścieżek. Tak jak w algorytmach DFS, BFS
i Prima, tak i tu stosujemy reprezentację opartą na krawędziach z rodzica w po
staci indeksowanej wierzchołkami tablicy edgeTo[] obiektów DirectedEdge.
Element edgeTo[v] to krawędź łącząca v z jego rodzicem w drzewie (ostatnia
krawędź na najkrótszej ścieżce z s do v).
n Odległość do źródła. Używamy indeksowanej wierzchołkami tablicy di stTo [],
w której element di stTo[v] to długość najkrótszej znanej ścieżki z s do v.
Przyjmujemy konwencję, że edgeTo[s] ma wartość n uli, a di stTo[s] — 0. Ponadto
odległości do wierzchołków nieosiągalnych ze źródła mają wartość Doubl e . POSITIVE_
INFINITY. Jak zwykle typy danych do budowania tych struktur tworzymy w kon
struktorze, a następnie dodajemy
e d g e T o [] d i s t T o []
obsługę m etod egzemplarza korzy n u li 0
stających ze struktur danych przy 5 - > l 0 . 3 2 1 .0 5
0->2 0.26 0 .2 6
obsłudze zapytań klientów o naj 7 -> 3 0 .3 7 0 .9 7
krótsze ścieżki i ich długości. 0 - > 4 0 . 3 8 0 .3 8
4 -> 5 0.35 0 .7 3
3 -> 6 0 .5 2 1 .4 9
Relaksacja krawędzi Kod do 2 -> 7 0.34 0 .6 0
wyznaczania najkrótszych ście
Struktury danych do wyznaczania najkrótszych ścieżek
żek oparty jest na prostej operacji
— relaksacji. Początkowo znamy
tylko krawędzie i wagi grafu. Element di stTo [] dla źródła jest inicjowany wartością
0, a wszystkie pozostałe wpisy w tablicy di stTo [] są inicjowane wartością Doubl e .
POSITI VE_I NFINITY. Algorytm w trakcie działania zbiera informacje o najkrótszych
ścieżkach łączących źródło z każdym wierzchołldem ze struktur danych edgeToJ]
i di stTo []. Aktualizując te informacje przy napotkaniu krawędzi, można wyciągać
nowe wnioski na tem at najkrótszych ścieżek. Stosujemy relaksację krawędzi zdefi
niowaną w następujący sposób — relaksacja krawędzi v->w oznacza sprawdzenie, czy
najlepsza znana droga z s do wprowadzi z s do v, a następnie krawędzią z v do w; jeśli
tak jest, należy zaktualizować struktury danych, aby uwzględnić te informacje. Kod
przedstawiony po prawej stronie to implementacja tej operacji. Najlepsza znana odle
głość do wprzez v to sum adi stTo[v]
i e.w eight(). Jeżeli wartość ta nie
p riv a te void re iax(D irecte d Ed ge e)
jest mniejsza niż di stTo [w], m ó {
wimy, że krawędź jest niewybieral- in t v = e .fro m (), w = e . t o ( );
i f (di stTo [w] > d istT o [v ] + e .w e igh t())
na i pomijamy ją. Jeśli wartość jest
{
mniejsza, aktualizujemy struktury distTo[w ] = d istT o [v ] + e .w e igh tf);
danych. Na rysunku w dolnej czę edgeTofw] = e;
ści strony pokazano dwa możliwe 1
skutki relaksacji krawędzi. Albo
krawędź jest niewybieralna (tak jak Relaksacja krawędzi
4.4 a Najkrótsze ścieżki 659
w przykładzie po lewej) i nie trzeba wprowadzać zmian, albo krawędź v->w prowadzi
do krótszej ścieżki do w (tak jak w przykładzie po prawej) i należy zaktualizować
struktury edgeTo[w] i distTo[w] (co może spowodować, że niektóre inne krawędzie
staną się niewybieralne, a inne — wybieralne). Nazwa relaksacja związana jest z gu
mową taśm ą rozciągniętą na ścieżce łączącej dwa wierzchołki. Relaksacja krawędzi
przypomina zwolnienie napięcia gumowej taśmy przez przeciągnięcie jej wzdłuż
krótszej ścieżki (jeśli jest to możliwe). Mówimy, że krawędź e umożliwia relaksację,
jeśli m etoda rei ax() zmienia wartości di stT o [e .to ()] i ed g eT o [[Link] ()].
/
1 w^lal■***>+niAiinrUinrilnl mu/nrl II_>.1111ioct IKMlbinr^Ina
-1
Czarn
edgeT o[]
Relaksacja krawędzi (dwa przypadki)
ROZDZIAŁ 4 □ Grafy
Relaksacja w ierzchołka Wszystkie omawiane implementacje wykonują relaksację
każdej krawędzi prowadzącej z danego wierzchołka, co pokazano poniżej w (prze
ciążonej) implementacji metody re la x (). Zauważmy, że dowolna krawędź z wierz
chołka, dla którego element distTo[v]
ma wartość skończoną, do wierzchołka
o nieskończonej wartości d istT o [] jest
wybieralna i zostanie dodana w wyni
ku relaksacji do edgeTo[]. Jako pierwsza
zostanie dodana do edgeTo[] pewna kra
wędź wychodząca ze źródła. Algorytmy
sensownie wybierają wierzchołki, tak
więc przy każdej relaksacji wierzchołka
znajdowana jest ścieżka krótsza od naj
lepszej znanej do tej pory do pewnego
wierzchołka i stopniowo realizowany jest
cel — znalezienie najkrótszych ścieżek do
każdego wierzchołka.
p riv a te void relax(EdgeW eightedDigraph G, in t v)
1
f o r (DirectedEdge e : G .a d j(v))
1
in t w = e . t o ( ) ;
i f (distTo[w ] > di stTo [v] + e .w e igh t())
1
distTo[w ] = d istT o [v ] + e .w e igh t();
edgeTofw] = e;
1
1
Relaksacja wierzchołka
4.4 a Najkrótsze ścieżki 661
M etody obsługi za p ytań od klientów Podobnie jak w implementacjach interfejsów
API do znajdowania ścieżek z p o d r o z d z i a ł u 4.1 (i z ć w i c z e n i a 4 .1 .1 3 ), tak i tu
struktury danych edgeTo [] i di stTo [] są bezpośrednio wykorzystywane w metodach
obsługi zapytań od klientów — pathTo(), hasPathTo() i d istT o (), co pokazano po
niżej. Kod ten jest używany we wszystkich implementacjach technik wyznaczania
najkrótszych ścieżek. Jak już wspomnieliśmy, m etoda di stTo [v] jest sensowna tyl
ko wtedy, kiedy wierzchołek v jest osiągalny z s. Ponadto przyjęliśmy konwencję,
zgodnie z którą metoda di stTo () powinna zwracać nieskończoność dla wierzchoł
ków nieosiągalnych z s. Aby móc zastosować tę konwencję, inicjujemy wszystkie ele
menty tablicy di stT o [] wartością Double. POSITIVE_INFINITY, a element di stTo[s]
— wartością 0. Implementacje technik wyznaczania najkrótszych ścieżek ustawia
ją di stTo [v] na skończoną wartość dla wszystkich wierzchołków v osiągalnych ze
źródła. Można więc pominąć tablicę markedf], którą zwykle stosujemy do oznacza
nia osiągalnych wierzchołków przy przeszukiwaniu grafów, i w implementacji m e
tody hasPathTo(v) sprawdzać, czy wartość di stTo [v] jest równa Doubl e. POSITIVE_
INFINITY. W metodzie pathTo() stosujemy
v e d g e T o []
konwencję, zgodnie z którą pathTo(v) zwraca 0 nuli
1 5 -> l
nul 1, jeśli v nie jest osiągalny ze źródła, i ścież 2 0 -> 2
kę pozbawioną krawędzi, jeżeli v jest źródłem. 3 7 -> 3
4 0 -> 4
Dla osiągalnych wierzchołków należy przejść 5 4 -> 5
6 3 -> 6
w górę drzewa i umieścić znalezione krawę
p a th T o (6 )
dzie na stosie (w taki sam sposób, jak w kla
e path
sach DepthFi rstPaths i BreadthFi rstPaths). 3 -> 6
Na rysunku po prawej stronie pokazano znaj 7 -> 3 3 -> 6
2 -> 7 7 - > 3 3 -> 6
dowanie ścieżki 0->2->7->3->6 w przykłado 0 -> 2 2 - > 7 7 -> 3 3 -> 6
nuli 0 -> 2 2 - > 7 7 -> 3 3 -> 6
wym grafie.
Ślad działania metody pathToO
p u b lic double d is t T o ( in t v)
{ return d is t T o [ v ] ; }
p u b lic boolean hasPathT o(int v)
{ return d istT o [v ] < D ouble.P O S IT IV E IN F IN IT Y ; }
p u b lic Ite rab le<D irecte dEdge> pathTo(int v)
{
i f (Ih a sP a th T o (v)) return n u ll;
Stack<DirectedEdge> path = new Stack< D ire cte d Ed g e > ();
f o r (DirectedEdge e = edgeTo[v]; e != n u li; e = edgeTo[[Link] () ] )
p a th .p u sh (e );
return path;
1
Metody obsługi zapytań od klientów na temat najkrótszych ścieżek
662 ROZDZIAŁ 4 □ Grafy
Teoretyczne podstaw y algorytm ów wyznaczania najkrótszych ście
żek Relaksacja krawędzi to łatwa do zaimplementowania podstawowa operacja,
która zapewnia praktyczne podstawy implementacji algorytmów wyznaczania naj
krótszych ścieżek. Operacja ta jest też teoretyczną podstawą do zrozumienia algoryt
mów i umożliwia udowodnienie ich poprawności.
Warunki optymalności Poniższe twierdzenie określa równoznaczność między wa
runkiem globalnym (mówiącym, że uzyskane odległości są odległościami najkrót
szych ścieżek) a warunkiem lokalnym, sprawdzanym przy relaksacji krawędzi.
Twierdzenie P (warunki optymalności najkrótszych ścieżek). Niech G
będzie digrafem ważonym, s — wierzchołkiem źródłowym w G, a di stTo []
— indeksowaną wierzchołkami tablicą długości ścieżek w G, w której dla każ
dego v osiągalnego z s wartość di stTo[v] to długość pewnej ścieżki z s do v (dla
wszystkich v nieosiągalnych z s wartość di stTo [v] jest równa nieskończoności).
Wartości to długości najkrótszych ścieżek wtedy i tylko wtedy, jeśli spełniają nie
równość di stTo [w] <= distTo[v] + e.w eight() dla każdej krawędzi e na ścieżce
z v do w (co oznacza, że żadna z krawędzi nie jest wybieralna).
Dowód. Załóżmy, że di stTo [w] to długość najkrótszej ścieżki z s do w. Jeśli
distTo[w] > distTo[v] + e.w eight() dla pewnej krawędzi e z v do w, to e
daje ścieżkę z s do w (przez v) o długości mniejszej niż di stTo [w] — występuje
sprzeczność. Dlatego warunki optymalności są konieczne.
Aby udowodnić, że warunki optymalności są wystarczające, załóżmy, że wjest
osiągalny z s, a s = v0->v1->vz. . .->vk = w to najkrótsza ścieżka z s do w o wadze
0PTsw. Dla i od 1 do k oznaczmy krawędzie z v .-l do v. jako er Zgodnie z w arun
kami optymalności otrzymujemy poniższy ciąg nierówności.
distTo[w] = distT o[vk] <= distT o[vkl] + [Link]()
di stTo [vk_1] <= distT o[vkJ + ek l.w eight()
distT o[v2] <= distT o[vk] + e2.w eight()
di stTo[Vj] <= di stTo [s] + ej.w eight()
Po złączeniu nierówności i wyeliminowaniu di stTo[s] = 0.0 uzyskujemy
di stTo [w] <= ej.w eightO + . . . + ek.w eight() = 0PTsi)
Teraz di stT o [w] to długość pewnej ścieżki z s do w. Nie może być ona krótsza niż
najkrótsza ścieżka. Wykazaliśmy więc, że równość
OPTsw <= distTo[w]
L J
<= OPTsw
musi być spełniona.
4.4 □ Najkrótsze ścieżki 663
Sprawdzanie Ważnym praktycznym zastosowaniem t w i e r d z e n i a p jest wykorzy
stywanie go do sprawdzania algorytmów. Niezależnie od sposobu obliczania przez
algorytm wartości z tablicy di stTo [] m ożna sprawdzić, czy zawiera on długości naj
krótszych ścieżek. Wystarczy wykonać jeden przebieg przez krawędzie grafu i ustalić,
czy spełnione są warunki optymalności. Algorytmy wyznaczania najkrótszych ście
żek bywają skomplikowane, dlatego możliwość wydajnego sprawdzenia ich popraw
ności jest niezwykle istotna. Implementacje dostępne w witrynie obejmują metodę
check() służącą właśnie do tego. Metoda ta sprawdza ponadto, czy tablica edgeTo []
zawiera ścieżki ze źródła i jest zgodna z tablicą di stTo [].
Ogólny algorytm Warunki optymalności bezpośrednio prowadzą do ogólnego al
gorytmu, obejmującego wszystkie omówione algorytmy wyznaczania najkrótszych
ścieżek. Tymczasowo uwzględniamy tylko wagi nieujemne.
Twierdzenie Q (ogólny algorytm wyznaczania najkrótszych ścieżek).
Zainicjuj di stTo [s] za pomocą 0, a wszystkie inne wartości tablicy di stTo []
— nieskończonością. Kontynuuj w następujący sposób:
wykonuj relaksację każdej krawędzi grafu G do momentu,
w którym nie ma wybieralnych krawędzi.
Dla wszystkich wierzchołków wosiągalnych z s wartość di stTo [w] po tym proce
sie to długość najkrótszej ścieżki z s do w (a wartość edgeTo [] to ostatnia krawędź
tej ścieżki).
Dowód. Relaksacja krawędzi v->w zawsze powoduje ustawienie di stTo [w]
na długość pewnej ścieżki z s (i ustawienie edgeTo [w] na ostatnią krawędź tej
ścieżki). Dla każdego wierzchołka w osiągalnego z s pewna krawędź w najkrót
szej ścieżce do w jest wybieralna dopóty, dopóki di stTo [w] to nieskończoność.
Dlatego algorytm kontynuuje działanie dopóty, dopóki wartość di stTo [] dla
każdego wierzchołka osiągalnego z s nie przyjmie długości pewnej ścieżki do
tego wierzchołka. Dla każdego wierzchołka v, dla którego najkrótsza ścieżka jest
dobrze określona, w czasie działania algorytmu wartość di stTo [v] jest długością
pewnej (prostej) ścieżki z s do v i jest ściśle monotonicznie malejąca. Dlatego
można ją zmniejszyć najwyżej skończoną liczbę razy (jeden raz dla każdej ścieżki
prostej z s do v). Kiedy żadna krawędź nie jest wybieralna, można zastosować
T W IE R D Z E N IE P.
Kluczowym powodem do rozważania warunków optymalności i algorytmu ogól
nego jest to, że algorytm ten nie określa kolejności relaksacji krawędzi. Dlatego aby
udowodnić, że dowolny algorytm wyznacza najkrótsze ścieżki, wystarczy wykazać,
iż przeprowadza relaksację krawędzi do momentu, w którym nie pozostanie żadna
wybieralna krawędź.
664 ROZDZIAŁ 4 o Grafy
Algorytm Dijkstry W p o d r o z d z i a l e 4.3 omówiono algorytm Prima, służą
cy do wyznaczania drzewa MST dla nieskierowanego grafu ważonego. Algorytm
tworzy drzewo MST przez dołączanie w każdym kroku nowej krawędzi do poje
dynczego rosnącego drzewa. Algorytm Dijkstry to analogiczne rozwiązanie służące
do wyznaczania drzew SPT. Należy zacząć od zainicjowania d is t[s ] za pomocą 0,
a pozostałych elementów tablicy di stTo [] — przy użyciu dodatniej nieskończoności.
Następnie trzeba przeprowadzić relaksację i dodać do drzewa wierzchołek spoza niego
0 najniższej wartości di stTo []; proces ten jest powtarzany do momentu, w którym
wszystkie wierzchołki znajdują się w drzewie lub żaden wierzchołek spoza drzewa nie
ma skończonej wartości di stTo [].
Twierdzenie R. Algorytm Dijkstry rozwiązuje problem wyznaczania najkrót
szych ścieżek z jednego źródła dla digrafów ważonych o nieujemnych wagach.
Dowód. Jeśli v jest osiągalny ze źródła, dla każdej krawędzi v->w relaksacja jest
wykonywana dokładnie raz, w momencie relaksacji v, po czym di stTo [w] <=
di stTo [v] + e . wei ght (). Nierówność ta jest spełniona do m om entu zakończenia
pracy algorytmu, ponieważ wartość di stTo [w] może się tylko zmniejszać (każda
relaksacja może prowadzić tylko do zmniejszenia wartości di stT o[]), a wartość
di stTo [v] nigdy się nie zmienia (wagi krawędzi są nieujemne, a w każdym kroku
wybierana jest najmniejsza wartość di stT o[], dlatego żadna relaksacja nie może
ustawić di stT o [] na wartość mniejszą niż di stTo [v]). Dlatego po dodaniu do
drzewa wszystkich wierzchołków osiągalnych z s warunki optymalności najkrót
szych ścieżek są spełnione i m ożna zastosować t w i e r d z e n i e p .
S tru ktu ry danych Aby zaimplementować algorytm Dijkstry, do struktur di stTo []
1 edgeTof] należy dodać indeksowaną kolejkę priorytetową, pq, służącą do śledzenia
wierzchołków, które mogą zostać jako następne poddane relaksacji. Przypomnijmy,
że typ IndexMinPQ umożliwia powiązanie indeksów z kluczami (priorytetami) oraz
usuwanie i zwracanie indeksu powiązanego z najmniejszym kluczem. W omawia
nym kontekście zawsze łączymy wierzchołek v
Krawędź drzewa
(kolor czarny) z wartością di stTo [v], co bezpośrednio i na
Krawędź
przekroju tychmiast prowadzi do implementacji algoryt
(kolor m u Dijkstry. Ponadto przez indukcję natych
czerwony)
miast można stwierdzić, że elementy tablicy
edgeTo[] odpowiadające dostępnym wierz
chołkom tworzą drzewo — drzewo SPT.
Krawędź przekroju Inna perspektyw a Inny sposób na zrozumie
n a najkrótszej ścieżce
z s obejmującej tylko
nie działania algorytmu oparty jest na dowo
jedną taką krawędź dzie. Pracę algorytmu pokazano na rysunku po
m usi należeć do
lewej stronie. Obowiązuje niezmiennik, zgod
drzewa SPT
nie z którym wartości w tablicy di stTo [] od-
Algorytm Dijkstry do wyznaczania najkrótszych ścieżek
4.4 a Najkrótsze ścieżki 665
p o w ia d a ją c e w ie rz c h o łk o m d rz e w a to d łu g o ś c i Czerwony-
edgeTo[] di stT o[]
n a jk ró tsz y c h ścieżek , a d la k a ż d e g o w ie rz c h o łk a 0 0.00
1
w z k o le jk i p rio ry te to w e j w a rto ś ć d is tT o [w ] to 2 0->2 0.26 0.26
3
w aga n a jk ró tsz e j śc ieżk i z s d o w, k tó r a o b e jm u je 4 0->4 0.38 0.38
ty lk o p o ś r e d n ie w ie rz c h o łk i z d rz e w a i k o ń c z y
się k ra w ę d z ią p rz e k r o ju edgeT o[w ], W a rto ść ?\ Indeks t
Priorytet
d is tT o [ ] w ie rz c h o łk a o n a jn iż s z y m p r io r y te Czarny -
cie to w a g a n a jk ró tsz e j ścieżk i, n ie m n ie js z a n iż w drzewie SPT 0 0.00
w aga n a jk ró tsz e j śc ieżk i w ie rz c h o łk ó w , d la k tó 2 0->2 0.26 0.26
3
ry c h ju ż w y k o n a n o re la k sac ję , i n ie w ię k sza n iż 4 0->4 0.38 0.38
5
w aga n a jk ró tsz e j śc ieżk i w ie rz c h o łk ó w p rz e d r e 6
laksacją. W ie rz c h o łe k te n ja k o n a s tę p n y p o d le g a 7 2->7 0.34 0.60
relaksacji. R e lak sac ja d o s tę p n y c h w ie rz c h o łk ó w 0 0.00
o d b y w a się w k o le jn o śc i z g o d n e j z w a g a m i ich 2 0->2 0.26 0.26
n a jk ró tsz y c h śc ie ż e k z s. 4 0->4 0.38 0.38
5 4->5 0.35 0.73
R y s u n e k p o p ra w e j s tr o n ie to ś la d d z ia ła n ia 6
7 2->7 0.34 0.60
a lg o r y tm u d la m a łe g o p rz y k ła d o w e g o g ra fu
0 0.00
tin y E W D .tx t. A lg o ry tm tw o rz y d rz e w o S P T
w n a s tę p u ją c y s p o s ó b . 2 0->2 0.26 0.26
3 7->3 0.37 0.97
° D o d a je 0 d o d rz e w a , a s ą s ie d n ie w ie rz 4 0->4 0.38 0.38
5 4->5 0.35 0.73
c h o łk i, 2 i 4, d o k o le jk i p rio ry te to w e j.
7 2->7 0.34 0.60
° U su w a 2 z kolejki p rio ry teto w ej, d o d a je 0->2
0 0.00
d o d rz e w a i 7 d o k o le jk i p rio ry te to w e j. 1 5->l 0.32 1.05
° U su w a 4 z k o le jk i p rio ry te to w e j, d o d a je —*- 2 0->2 0.26 0.26
3 7->3 0.37 0.97
0-> 4 d o d rz e w a i 5 d o k o lejk i p r io r y te to 4 0->4 0.38 0.38
5 4->5 0.35 0.73
w ej. K ra w ę d ź 4->7 staje się n ie w y b ie ra ln a . 6
7 2->7 0.34 0.60
■ U su w a 7 z k o le jk i p rio ry te to w e j, d o d a je
0 0.00
2->7 d o d rz e w a i 3 d o k o lejk i p r io r y te to 1 5->l 0.32 1.05
w ej. K ra w ę d ź 7->5 staje się n ie w y b ie ra ln a . 2 0->2 0.26 0.26
3 7->3 0.37 0.97
n U s u w a 5 z k o le jk i p rio ry te to w e j, d o d a je 4 0->4 0.38 0.38
5 4->5 0.35 0.73
4~>5 d o d rz e w a i 1 d o k o le jk i p r io r y te to 6 3->6 0.52 1.49
7 2->7 0.34 0.60
w ej. K ra w ę d ź 5->7 staje się n ie w y b ie ra ln a .
0 0.00
* U su w a 3 z kolejki p rio ry teto w ej, d o d a je 7 ->3 1 5->l 0.32 1.05
2 0->2 0.26 0.26
d o d rz e w a i 6 d o k o le jk i p rio ry te to w e j. 7->3 0.37 0.97
3
■ U su w a 1 z k o le jk i p rio ry te to w e j i d o d a je 4 0->4 0.38 0.38
5 4->5 0.35 0.73
5 - > l d o d rz e w a . K ra w ę d ź l- > 3 sta je się 6 3->6 0.52 1.49
7 2->7 0.34 0.60
n ie w y b ie r a ln a .
0 0.00
° U s u w a 6 z k o le jk i p rio ry te to w e j i d o d a je 1 5->l 0.32 1.05
2 0->2 0.26 0.26
3-> 6 d o d rz e w a . 3 7->3 0.37 0.97
4 0->4 0.38 0.38
W ie r z c h o łk i są d o d a w a n e d o d rz e w a S P T w k o 5 4->5 0.35 0.73
6 3->6 0.52 1.49
le jn o śc i ro s n ą c e j w e d łu g o d le g ło ś c i o d ź ró d ła , 7 2->7 0.34 0.60
w sk a z y w a n e j p rz e z c z e rw o n e s trz a łk i z p ra w e j
Ślad działania algorytmu Dijkstry
s tro n y r y s u n k u .
RO ZD ZIA Ł 4 □ Grafy
Im p le m e n ta c ja a lg o r y tm u D ijk s try w k la s ie Di j k s t r a S P ( a l g o r y t m 4 .9 ) to k o d o d
z w ie rc ie d la ją c y je d n o z d a n io w y o p is a lg o r y tm u . N a p is a n ie te g o k o d u je s t m o ż liw e
d z ię k i d o d a n iu d o m e to d y r e l a x ( ) je d n e j in s tr u k c ji o b s łu g u ją c e j d w a p rz y p a d k i
— a lb o w ie rz c h o łe k to () p o w ią z a n y z k ra w ę d z ią n ie z n a jd u je się je s z c z e w k o lejc e
p rio ry te to w e j (w te d y n a le ż y u ż y ć m e to d y i n s e r t ( ) i d o d a ć g o d o k o le jk i), a lb o je s t
ju ż w k o le jc e (w te d y tr z e b a z m n ie js z y ć je g o p r i o r y te t za p o m o c ą m e to d y c h a n g e () ).
Twierdzenie R (ciąg dalszy). P rz y w y z n a c z a n iu d rz e w a S P T o k o r z e n iu w d a
n y m ź ró d le d la d ig r a f u w a ż o n e g o o E k ra w ę d z ia c h i V w ie rz c h o łk a c h a lg o r y tm
D ijk s try z a jm u je d o d a tk o w ą p a m ię ć w ilo śc i p r o p o r c jo n a ln e j d o V i d z ia ła w c z a
sie p r o p o r c jo n a ln y m d o E lo g V (d la n a jg o rs z e g o p rz y p a d k u ) .
Dowód. T a k i sa m , ja k d la a lg o r y tm u P r im a (z o b a c z t w i e r d z e n i e n ).
ja ic w s p o m n i e l i ś m y , i n n y s p o s ó b m y ś l e n i a o a lg o r y tm ie D ijk s tr y p o le g a n a p o
r ó w n a n iu g o z a lg o r y tm e m P r im a d o w y z n a c z a n ia d r z e w M S T ( p o d r o z d z i a ł 4 . 3 ,
s tr o n a 6 3 4 ). O b a a lg o r y tm y tw o rz ą d rz e w o z k o r z e n ie m p rz e z d o d a w a n ie k r a w ę
d z i d o ro s n ą c e g o d rz e w a . A lg o ry tm P r im a d o d a je n a s tę p n y w ie rz c h o łe k s p o z a
d rz e w a n a jb liż s z y d r z e w u . A lg o ry tm D ijk s try d o d a je n a s tę p n y w ie rz c h o łe k s p o z a
d rz e w a n a jb liż s z y źr ó d łu . T a b lic a m a rk e d [] n ie je s t p o tr z e b n a , p o n ie w a ż w a r u n e k
!m arked[w ] je s t r ó w n o z n a c z n y w a r u n k o w i m ó w ią c e m u , że d i s tT o [w] to n ie s k o ń
c z o n o ść . U jm ijm y to in a c z e j — p o z a s to s o w a n iu g ra fó w i k ra w ę d z i n ie s k ie ro w a n y c h
o ra z p o m in ię c iu re fe re n c ji d o d is tT o [ v ] w m e to d z ie r e l a x ( ) k o d a l g o r y t m u 4 .9
staje się im p le m e n ta c ją a l g o r y t m u 4 .7 — z a c h ła n n ą w e rs ją a lg o r y tm u P r im a (!).
P o n a d to n ie t r u d n o je s t o p ra c o w a ć le n iw ą w e rsję a lg o r y tm u D ijk s try , p o d o b n ą d o
k la s y LazyPrimMST ( s tr o n a 6 3 1 ).
O d m ia n y O p r a c o w a n a p rz e z n a s im p le m e n ta c ja a lg o r y tm u D ijk s try p o o d p o w ie d
n ic h m o d y f ik a c ja c h n a d a je się d o ro z w ią z a n ia in n y c h o d m ia n p ro b le m u , ta l a c h ja k
p o n iż s z a .
N a jk ró tsze ścieżk i z je d n e g o źró d ła w g rafach n ieskierow an ych . D la n ieskiero w a -
nego g ra f u w a ż o n e g o i w ie rz c h o łk a ź ró d ło w e g o s z a p e w n ij o b s łu g ę z a p y ta ń w p o
staci: C z y istn ie je śc ie żk a z s d o w ie rz c h o łk a d ocelow ego v? Jeśli ta k , n a le ż y z n a le ź ć
n a jk r ó tsz ą ta k ą ś c ie ż k ę (k tó re j łą c z n a w a g a je s t m in im a ln a ) .
R o z w ią z a n ie te g o p r o b le m u je s t n a ty c h m ia s to w e , je ś li g r a f n ie s k ie ro w a n y p o t r a k t u
je m y j a k d ig ra f. N a p o d s ta w ie g ra fu n ie s k ie ro w a n e g o n a le ż y u tw o rz y ć d ig r a f w a ż o n y
o ty c h s a m y c h w ie rz c h o łk a c h i d w ó c h k ra w ę d z ia c h s k ie ro w a n y c h (p o je d n e j w k a ż
d y m k ie r u n k u ) , o d p o w ia d a ją c y c h k a ż d e j k ra w ę d z i g ra fu . Is tn ie je z a le ż n o ś ć je d e n d o
je d n e g o m ię d z y ś c ie ż k a m i d ig r a fu a ś c ie ż k a m i g ra fu , a k o s z ty śc ie ż e k są ta k ie sam e.
O b a p r o b le m y w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k są a n a lo g ic z n e .
4.4 Najkrótsze ścieżki 667
ALGORYTM 4.9. Algorytm Dijkstry do wyznaczania najkrótszych ścieżek
public cl a ss DijkstraSP
{
p r i v a t e Di rect edEdge[] edgeTo;
p r i v a t e d o u b l e [] d i s t T o ;
p r i v a t e IndexMinPQ<Double> pq;
p u b l i c D i jk s tr aS P ( Ed g e We i ghtedDigraph G, i n t s)
{
edgeTo = new D i r e c t e d E d g e [ G . V ( ) ] ;
d i s t T o = new d o u b l e [ G . V ( ) ] ;
pq = new I ndex Mi nPQ<Doubl e>( G. V( ) ) ;
f o r ( i n t v = 0; v < G. V( ) ; v++)
di s tTo[ v] = D o u b l e . PO S I T I VE _ I NF I N I T Y ;
d i s t T o [ s ] = 0.0;
p q . i n s e rt (s , 0.0);
whi l e ( ! p q . i sE mpt y ( ) )
r el ax( G, p q .del Mi n ())
}
p r i v a t e voi d rel ax( EdgeWeightedDi graph G, i n t v)
{
f o r ( Di r e c t ed E d g e e : [Link] (v ))
{
int w = e . t o ( ) ;
i f (di stTo[w] > d i s t T o [ v ] + e . we i gh t ( ) )
i
di stTo [w] = di stTo [v] + e . we i g h t ( ) ;
edgeTo[w] = e;
i f ( pq. co nt a i n s ( w) ) [Link](w, d i s t T o [ w ] ) ;
else p q . i n s e r t (w , d i s t T o [ w ] ) ;
}
}
p u b l i c double d i s t T o ( i n t v) // Standardowe metody o bs ł ugi
// zapytań kl i entów
p u b l i c boolean ha s P a th T o ( i nt v) // dla implementacji techni k
// tworzeni a drzew SPT
p u b l i c I t er ab l e< E dge> p a t hT o ( i n t v) // (zobacz st r o nę 661).
Ta im p le m e n ta c ja a lg o ry tm u D ijk s try tw o rz y d rz e w o SPT, d o d a ją c k raw ę d ź p o kraw ęd zi,
p rz y czy m zaw sze w y b ie ra n a je st k ra w ęd ź z w ie rz c h o łk a d rz e w a d o n ajb liższeg o w ie rz c h o ł
kow i S w ie rz c h o łk a w sp o za drzew a.
668 RO ZD ZIA Ł 4 a Grafy
N a jk ró tsze śc ieżk i z e źr ó d ła d o u jścia. D la d ig r a fu w a ż o n e g o , w ie rz c h o łk a ź r ó d ło
w e g o s i w ie rz c h o łk a d o c e lo w e g o t z n a jd ź n a jk r ó ts z ą śc ie ż k ę z s d o t .
D o ro z w ią z a n ia te g o p r o b le m u w y k o rz y s ta m y a lg o r y tm D ijk s try , ale p rz e s z u k iw a n ie
z a k o ń c z y m y b e z p o ś r e d n io p o u s u n ię c iu t z k o le jk i p rio ry te to w e j.
N a jk ró tsze ścieżk i d la w szystkich p a r. D la d ig r a fu w a ż o n e g o z a p e w n ij o b słu g ę
z a p y ta ń w p o s ta c i: C z y d la w ie rz c h o łk a źró d ło w e g o s i w ie r z c h o łk a docelow ego t
istn ieje śc ie żk a z s do t ? Jeśli ta k , z n a jd ź n a jk r ó tsz ą śc ie ż k ę te g o ro d z a ju (o m i n i
m a ln e j łą c z n e j w a d z e ).
Z a s k a k u ją c o z w ię z ła im p le m e n ta c ja , p r z e d s ta w io n a p o n iż e j p o le w e j s tro n ie , r o z
w ią z u je p r o b le m n a jk r ó ts z y c h śc ie ż e k d la w s z y s tk ic h p a r o ra z p o tr z e b u je n a to
c z a s u i p a m ię c i w ilo śc i p r o p o r c jo n a ln e j d o T U lo g U. K o d tw o rz y ta b lic ę o b ie k tó w
Di j k s tra S P — p o je d n y m d la k a ż d e g o w ie rz c h o łk a ja k o ź ró d ła . P rz y o d p o w ia d a n iu
n a z a p y ta n ia k lie n tó w ź r ó d ło w y k o rz y s ty w a n e je s t d o d o s tę p u d o o d p o w ie d n ie g o
o b ie k tu z n a jk r ó ts z y m i ś c ie ż k a m i z je d n e g o ź ró d ła , a n a s tę p n ie w ie rz c h o łe k d o c e lo
w y je s t p rz e k a z y w a n y ja k o a r g u m e n t z a p y ta n ia .
N a jk ró tsze śc ieżk i w grafach eu klideso w ych . N a le ż y ro z w ią z a ć p r o b le m y n a jk r ó t
sz y c h śc ie ż e k z je d n e g o ź ró d ła , ze ź r ó d ła d o u jś c ia i d la w s z y s tk ic h p a r w g ra fa c h ,
w k tó r y c h w ie rz c h o łk i są p u n k ta m i w p rz e s trz e n i, a w a g i k r a w ę d z i są p r o p o r c jo
n a ln e d o o d le g ło ś c i e u k lid e s o w y c h m ię d z y w ie rz c h o łk a m i.
P ro s ta m o d y fik a c ja p o z w a la z n a c z n ie p rz y s p ie sz y ć d z ia ła n ie a lg o r y tm u D ijk s try
w ta k ic h p rz y p a d k a c h (z o b a c z ć w i c z e n i e 4 .4 . 2 7 ).
n a r y s u n k a c h n a n a s t ę p n e j s t r o n i e p o k a z a n o tw o rz e n ie p rz e z a lg o r y tm D ijk s try
d rz e w a S P T d la k ilk u ró ż n y c h ź ró d e ł g ra f u e u k lid e s o w e g o z d e fin io w a n e g o w p lik u
te s to w y m m e d iu m E W D .tx t (z o b a c z s t r o
pub lic c la s s D ij k s t r a A llP a ir s S P n ę 6 5 7 ). P rz y p o m n ijm y , że lin ie w g rafie
{ re p r e z e n tu ją k ra w ę d z ie s k ie ro w a n e w o b u
p r i v a t e Di j k s t r a S P [] a l l ;
k ie r u n k a c h . T ak że t u r y s u n k i są ilu s tra c ją
Di j k s t r a A l 1 Pai r s S P ( E d g e W e ig h t e d D ig r a p h G) c ie k a w e g o d y n a m ic z n e g o p ro c e s u .
1 D a le j o m a w ia m y a lg o r y tm y w y z n a c z a
all = new D i j k s t r a S P f G . V ()]
n ia n a jk r ó ts z y c h śc ie ż e k w a c y k lic z n y c h
f o r ( i n t v = 0; v < G. V ( ) ; v++)
a l l [v] = new D i j k s t r a S P ( G , v ) ; g ra fa c h w a ż o n y c h . P r o b le m te n m o ż n a
1 ro z w ią z a ć w c z asie lin io w y m (sz y b c ie j n iż
z a p o m o c ą a lg o r y tm u D ijk s try ). N a s tę p n ie
I t e r a b l e < E d g e > p a t h ( i n t s, in t t)
{ return a l l [ s ] . p a t h T o ( t ) ; }
ro z w a ż a m y te n s a m p r o b le m w k o n te k ś c ie
d ig r a fó w w a ż o n y c h o w a g a c h u je m n y c h ,
do ub le d i s t ( i n t s, i n t t) d la k tó r y c h a lg o r y tm D ijk s tr y n ie d z ia ła .
{ return a l l [ s ] . d i s t T o ( t ) ; }
1
W yznaczanie najkrótszych ścieżek dla wszystkich par
4.4 □ Najkrótsze ścieżki 669
Algorytm Dijkstry (250 wierzchołków, różne źródła)
670 R O ZD ZIA Ł 4 o Grafy
Acykliczne digrafy ważone W w ie lu n a tu r a ln y c h z a s to s o w a n ia c h w ia d o m o ,
że d ig r a fy w a ż o n e n ie m a ją cy k li s k ie ro w a n y c h . Z u w a g i n a z w ię z ło ść u ż y w a m y r ó w
n o z n a c z n e j n a z w y w a ż o n y g r a f D A G d o o k re ś la n ia a c y k lic z n y c h g ra fó w w a ż o n y c h .
T u o m a w ia m y a lg o r y tm w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k w w a ż o n y c h g ra fa c h D A G .
A lg o ry tm te n je s t p r o s ts z y i sz y b s z y n iż a lg o r y tm D ijk s try . O to je g o cech y :
■ P ro b le m d la je d n e g o ź ró d ła ro z w ią z u je w c z asie lin io w y m .
■ O b s łu g u je u je m n e w a g i k ra w ę d z i.
■ R o z w ią z u je p o w ią z a n e p ro b le m y , ta k ie ja k w y s z u k iw a n ie n a jd łu ż s z y c h ścieżek .
A lg o ry tm y te są p r o s ty m ro z w in ię c ie m a lg o r y tm u to p o lo g ic z n e g o s o r to w a n ia g ra fó w
D A G , o m ó w io n e g o w p o d r o z d z i a l e 4 .2 .
R e la k sa c ja w ie rz c h o łk a w p o łą c z e n iu t in y E W D A G .t x t
z s o r to w a n ie m to p o lo g ic z n y m n a ty c h
m ia s t z a p e w n ia ro z w ią z a n ie p r o b le m u 13-*- E
5 4 0.35
w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k z j e d 4 7 0.37
n e g o ź r ó d ła d la w a ż o n y c h g ra fó w D A G . 5 7 0.28
5 1 0.32
N a le ż y z a in ic jo w a ć d i s tT o [s ] za p o m o c ą 4 0 0.38
0 , a w sz y stk ie p o z o s ta łe e le m e n ty ta b lic y 0 2 0.26
d i s t [] — n ie s k o ń c z o n o ś c ią . N a s tę p n ie 3 7 0.39
1 3 0.29
w y s ta rc z y w y k o n a ć re la k s a c ję w ie r z c h o ł 7 2 0.34
ków , p o b ie r a ją c je w p o r z ą d k u to p o lo g ic z 6 2 0.40
3 6 0.52
n y m . S k u te c z n o ś c i m e to d y d o w o d z i r o 6 0 0.58
z u m o w a n ie p rz e d s ta w io n e d la a lg o r y tm u 6 4 0.93
D ijk s tr y n a s tr o n ie 664. Acykliczny digraf ważony z drzewem SPT
Twierdzenie S. P rz e z re la k s a c ję w ie rz c h o łk ó w w p o r z ą d k u to p o lo g ic z n y m
m o ż n a ro z w ią z a ć p r o b le m w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k z je d n e g o ź ró d ła
w w a ż o n y m g ra fie D A G w c za sie p r o p o r c jo n a ln y m d o E + V.
Dowód. R e la k sa c ja k a ż d e j k ra w ę d z i v->w je s t w y k o n y w a n a d o k ła d n ie ra z , w c z a
sie re la k s a c ji v, p o c z y m d i stT o [w] <= d i stT o [v] + e . w e i g h t ( ) . N ie ró w n o ś ć
ta je s t s p e łn io n a d o m o m e n tu z a k o ń c z e n ia p r a c y a lg o r y tm u , p o n ie w a ż w a rto ś ć
di s tT o [ v ] n ig d y się n ie z m ie n ia (z u w a g i n a p o r z ą d e k to p o lo g ic z n y ż a d n a k r a
w ę d ź p ro w a d z ą c a d o v n ie je s t p r z e tw a r z a n a p o re la k s a c ji v), a w a rto ś ć d i s tT o [w]
m o ż e ty lk o m a le ć (re la k s a c ja m o ż e p ro w a d z ić ty lk o d o z m n ie js z e n ia w a rto ś c i
di s tT o [] ) . D la te g o p o d o d a n iu d o d rz e w a w s z y s tk ic h w ie rz c h o łk ó w d o s tę p n y c h
z s w a r u n k i o p ty m a ln o ś c i n a jk r ó ts z y c h śc ie ż e k są s p e łn io n e i m o ż n a z a s to s o
w a ć t w i e r d z e n i e Q. O g ra n ic z e n ie c z a su d z ia ła n ia je s t o c z y w iste — z g o d n ie
z t w i e r d z e n i e m G ze s tr o n y 5 9 5 s o r to w a n ie to p o lo g ic z n e d z ia ła w c z a sie p r o
p o r c jo n a ln y m d o E + V , a d r u g i p rz e b ie g , z w ią z a n y z re la k sa c ją , k o ń c z y p ro c e s
p rz e z j e d n o k r o tn ą re la k s a c ję k a ż d e j k ra w ę d z i, c o ta k ż e z a jm u je cz a s p r o p o r c jo
n a ln y d o E + V.
4.4 □ Najkrótsze ścieżki 671
N a r y s u n k u p o p ra w e j s tr o n ie p o k a z a n o śla d Sortowanie topologiczne
5 1 3 6 4 7 0 2 e d g e T o []
d z ia ła n ia a lg o r y tm u d la p rz y k ła d o w e g o a c y k lic z n
U
1 5 -> l
n e g o d ig r a f u w a ż o n e g o tin y E W D A G .tx t. W ty m ,'7 Y
3
p rz y k ła d z ie a lg o r y tm tw o rz y d rz e w o n a jk ró ts z y c h 4 5 -> 4
śc ieżek z w ie rz c h o łk a 5 w o p is a n y p o n iż e j sp o s ó b . 5
6
D S to s u je m e to d ę D F S w c e lu u s ta le n ia p o r z ą d 7 5 -> 7
k u to p o lo g ic z n e g o 5 1 3 6 4 7 0 2. Pogrubiona czarna krawędź - w drzewie
° D o d a je d o d rz e w a 5 i w sz y stk ie w y c h o d z ą c e
1 5 -> l
z n ie g o k ra w ę d z ie .
l- > 3
° D o d a je d o d rz e w a 1 i k ra w ę d ź l-> 3 . 5 -> 4
° D o d a je d o d rz e w a 3 i k ra w ę d ź 3 -> 6 , ale ju ż
5 -> 7
n ie 3 -> 7 , p o n ie w a ż je s t n ie w y b ie ra ln a .
n D o d a je d o d rz e w a 6 o ra z k ra w ę d z ie 6->2 i 6->0, Czerwona krawędź -
dodawana do drzewa
ale ju ż n ie 6-> 4, p o n ie w a ż je s t n ie w y b ie ra ln a .
° D o d a je d o d rz e w a 4 i k ra w ę d ź 4-> 0, a le ju ż n ie l- > 3
4-> 7 , p o n ie w a ż je s t n ie w y b ie r a ln a . K ra w ę d ź 5 -> 4
6 -> 0 sta je się n ie w y b ie ra ln a . 3 -> 6
5 -> 7
D D o d a je d o d rz e w a 7 i k ra w ę d ź 7-> 2. K ra w ę d ź
6 -> 2 sta je się n ie w y b ie ra ln a . 6 -> 0
5 -> l
Q D o d a je d o d rz e w a 0, ale ju ż n ie p rz y le g łą k r a 6 -> 2
l- > 3
w ę d ź 0 -> 2 , p o n ie w a ż je s t n ie w y b ie ra ln a . 5 -> 4
° D o d a je d o d rz e w a 2. 3 -> 6
5 -> 7
N ie p r z e d s ta w io n o d o d a w a n ia 2 d o d rz e w a . Z w ie rz
c h o łk a o s ta tn ie g o w p o r z ą d k u to p o lo g ic z n y m n ie
4 -> 0
w y c h o d z ą ż a d n e k ra w ę d z ie . 5 -> l
6 ->2
Im p le m e n ta c ja ( a l g o r y t m 4.10 ) to p ro s te z a sto l- > 3
5 -> 4
so w an ie o m ó w io n e g o ju ż k o d u . Z a k ład a m y , że k la sa
3 -> 6
Topological o b e jm u je p rz e c ią ż o n e m e to d y d o s o r 5 -> 7
V
to w a n ia to p o lo g ic z n e g o , k o rz y sta ją c e z in te rfe jsó w Szara krawędź
- niewybieralna 4 -> 0
A P I M as EdgeWei ghtedDi graph i Di rectedEdge z teg o 5 -> l
p o d ro z d z ia łu (z o b a c z ć w ic z e n ie 4 .4 . 12 ). Z au w ażm y , 7 -> 2
l-> 3
że w tej im p le m e n ta c ji ta b lic a lo g ic z n a marked [] n ie 5 -> 4
je st p o trz e b n a . P o n ie w a ż w ie rz c h o łk i w d ig rafie acy- 3 -> 6
5 -> 7
ld ic z n y m są p rz e tw a rz a n e w p o rz ą d k u to p o lo g ic z
n y m , n ig d y p o n o w n ie n ie n a p o ty k a m y w ie rz c h o łk a , 4 -> 0
5 -> l
d la k tó re g o p rz e p ro w a d z o n o ju ż relak sację. T ru d n o 7 -> 2
l-> 3
u tw o rzy ć ro z w ią z a n ie w y d a jn ie jsz e o d a l g o r y t m u 5 -> 4
4. 10 . P o s o rto w a n iu to p o lo g ic z n y m k o n s tru k to r 3 -> 6
p rz e g lą d a g r a f i w y k o n u je rela k sa c ję k ażd ej M-aw ęd zi 5 -> 7
d o ld a d n ie raz. Jest to m e to d a sto so w a n a z w y b o ru d o Ślad procesu wyznaczania najkrótszych
w y szu M w an ia n a jk ró tsz y c h śc ież e k w g ra fa c h w a ż o ścieżek w ważonym grafie DAG
n ych, o k tó ry c h w ia d o m o , że są acyM iczne.
672 R O ZD ZIA Ł 4 Grafy
ALGORYTM 4.10. Wyznaczanie najkrótszych ścieżek w ważonych grafach DAG
public c la s s AcyclicSP
{
private DirectedEdge[] edgeTo;
private doublet] di stT o ;
public AcyclicSP(EdgeWeightedDigraph G, in t s)
{
edgeTo = new Di rectedEdge[G.V() ];
distTo = new double[G.V()];
fo r (in t v = 0; v < G.V(); v++)
d istTo[v] = Double.POSITIVE_INFINITY;
d istT o[s] = 0.0;
Topological top = new Topological (G);
fo r (in t v : [Link] er())
relax(G, v);
}
p rivate void relax(EdgeWeightedDigraph G, in t v)
// Zobacz stronę 660.
public double d is t T o ( in t v) // Standardowe metody obsługi
// zapytań od klientów
public boolean hasPathTo(int v) // dla implementacji technik
// tworzenia drzew SPT
public Iterable<Edge> pathTo(int v) // (zobacz stronę 661).
}
W ty m alg o ry tm ie w y z n a c z an ia n a jk ró tsz y c h ścieżek w w ażo n y c h g ra fac h D A G w y k o rz y
sta n o so rto w a n ie to p o lo g ic z n e ( a l g o r y t m 4.5 d o sto so w a n y d o Idas EdgeWei ghtedDi graph
i Di rectedEdge), aby u m o żliw ić relak sację w ie rz c h o łk ó w w p o rz ą d k u to p o lo g ic z n y m — o p e
racja ta w ystarcza d o w y z n a c ze n ia n a jk ró tsz y c h ścieżek.
% j a v a A c y c l i c S P tinyE W D [Link] 5
5 do 0 ( 0 . 7 3 ) : 5- > 4 0 . 3 5 4 - > 0 0. 3 8
5 do 1( 0 . 3 2 ) : 5 - > l 0.32
5 do 2(0 .62): 5->7 0.28 7 - > 2 0. 3 4
5 do 3( 0 . 6 2 ) : 5 - > l 0 . 3 2 l - > 3 0. 29
5 do 4 ( 0 . 3 5 ) : 5 - > 4 0.35
5 do 5( 0 . 0 0 ) :
5 do 6( 1 . 1 3 ) : 5 - > l 0 . 3 2 l - > 3 0. 2 9 3 - > 6 0.52
5 do 7 ( 0 . 2 8 ) : 5- >7 0.28
4.4 b Najkrótsze ścieżki 673
t w i e r d z e n i e s m a d u ż e z n a c z e n ie , p o n ie w a ż s ta n o w i k o n k r e tn y p rz y k ła d , w k tó r y m
b r a k c y k li z n a c z n ie u p ra s z c z a p ro b le m . P rz y w y z n a c z a n iu n a jk r ó ts z y c h śc ie ż e k m e
to d a o p a r ta n a s o r to w a n iu to p o lo g ic z n y m je s t sz y b s z a o d a lg o r y tm u D ijk s tr y o c z y n
n ik p r o p o r c jo n a ln y d o k o s z tó w o p e ra c ji n a k o le jc e p rio ry te to w e j w ty m a lg o ry tm ie .
P o n a d to d o w ó d t w i e r d z e n i a s n ie z a le ż y o d teg o , c z y k ra w ę d z ie są n ie u je m n e , d la
teg o d la w a ż o n y c h g ra fó w D A G m o ż n a u s u n ą ć to o g ra n ic z e n ie . D a le j o m a w ia m y
s k u tk i m o ż liw o ś c i w y s tę p o w a n ia k ra w ę d z i o u je m n y c h w a g a c h . R o z w a ż a m y p rz y
ty m z a s to s o w a n ie m o d e lu n a jk r ó ts z y c h śc ie ż e k d o ro z w ią z a n ia d w ó c h in n y c h p r o b
lem ó w , z k tó r y c h je d e n p o c z ą tk o w o w y d a je się b y ć d o ś ć o d le g ły o d d z ie d z in y p r z e
tw a rz a n ia grafów .
N a j d ł u ż s z e ś c ie ż k i R o z w a ż m y p r o b le m z n a jd o w a n ia n a jd łu ż s z e j ś c ie ż k i w w a ż o
n y c h g ra f a c h D A G , w k tó r y c h w a g i k ra w ę d z i m o g ą b y ć d o d a tn ie i u je m n e .
W yzn a c za n ie n a jd łu ższyc h ścieżek z je d n e g o źr ó d ła w w a żo n y ch g rafach D A G .
D la w a ż o n e g o g ra fu D A G (z d o z w o lo n y m i w a g a m i u je m n y m i) i ź ró d ło w e g o
w ie rz c h o łk a s z a p e w n ij o b s łu g ę z a p y ta ń w p o s ta c i: C z y istn ieje śc ie żk a sk ie ro w a n a
z s do d a n eg o w ie rz c h o łk a docelow ego v? Jeśli ta k , z n a jd ź n a jd łu ż s z ą ta k ą śc ie ż k ę
(o m a k s y m a ln e j łą c z n e j w a d z e ).
O m ó w io n y w c z e śn ie j a lg o r y tm z a p e w n ia sz y b k ie ro z w ią z a n ie te g o p ro b le m u .
Twierdzenie T. P ro b le m w y z n a c z a n ia n a jd łu ż s z y c h śc ie ż e k w w a ż o n y c h g r a
fa c h D A G m o ż n a ro z w ią z a ć w c z a sie p r o p o r c jo n a ln y m d o E + V.
Dowód. P rz y w y z n a c z a n iu n a jd łu ż s z y c h ś c ie ż e k n a le ż y u tw o rz y ć k o p ię d a n e g o
w a ż o n e g o g ra f u D A G , w k tó re j w sz y stk ie k ra w ę d z ie m a ją w a g i o z m ie n io n y m
z n a k u . N a jk r ó ts z a śc ie ż k a w k o p ii je s t n a jd łu ż s z ą śc ie ż k ą o ry g in a łu . A b y p r z e
k s z ta łc ić ro z w ią z a n ie p r o b le m u w y z n a c z a n ia n a jk r ó ts z y c h ś c ie ż e k n a ro z w ią z a n ie
p r o b le m u z n a jd o w a n ia n a jd łu ż s z y c h śc ie ż ek , n a le ż y o d w ró c ić z n a k i w a g w w y n i
ku. C z a s w y k o n a n ia m o ż n a u s ta lić b e z p o ś r e d n io n a p o d s ta w ie t w i e r d z e n i a s.
W y k o rz y s ta n ie te j tr a n s f o r m a c ji d o o p ra c o w a n ia k la s y A c y c lic L P , k tó r a z n a jd u
je n a jd łu ż s z e śc ie ż k i w w a ż o n y m g ra fie D A G , je s t p ro s te . Jeszcze ła tw ie js z y s p o s ó b
n a z a im p le m e n to w a n ie tej k la s y to s k o p io w a n ie k o d u k la s y A c y c lic S P , z m ie n ie n ie
w a rto ś c i d o in ic jo w a n ia e le m e n tó w ta b lic y d i s tT o [ ] n a D o u b le . NEGAT IV E_ IN FINI TY
i z m o d y fik o w a n ie n ie r ó w n o ś c i w m e to d z ie r e l a x ( ) . W o b u s y tu a c ja c h u z y s k u je m y
w y d a jn e ro z w ią z a n ie p r o b le m u w y z n a c z a n ia n a jd łu ż s z y c h śc ie ż e k w w a ż o n y c h g r a
fach D A G . W a rto p o ró w n a ć w y d a jn o ś ć te g o ro z w ią z a n ia z n a jle p s z y m z n a n y m a lg o
r y tm e m w y s z u k iw a n ia n a jd łu ż s z y c h śc ie ż e k p r o s ty c h d la o g ó ln y c h d ig r a fó w w a ż o
n y c h (w k tó r y c h w a g i k ra w ę d z i m o g ą b y ć u je m n e ), k tó r y d la n a jg o rs z e g o p r z y p a d k u
d z ia ła w cz a sie w y k ła d n ic z y m (z o b a c z r o z d z i a ł 6 .)! W y g lą d a n a to , że m o ż liw o ś ć
w y s tę p o w a n ia c y k li p o w o d u je w y k ła d n ic z y w z ro s t tr u d n o ś c i p ro b le m u .
674 RO ZD ZIA Ł 4 o Grafy
N a r y s u n k u p o p ra w e j s tr o n ie p o k a z a n o śla d Sortow anie topologiczne
5 1 3 6 4 7 0 2 edgeTo[]
p ro c e s u w y z n a c z a n ia n a jd łu ż s z y c h śc ie ż e k 0
1 5->l
w p rz y k ła d o w y m w a ż o n y m g ra fie D A G tin y -
E W D A G .tx t. M o ż n a p o r ó w n a ć te n r y s u n e k 4 5->4
ze ś la d e m p ro c e s u z n a jd o w a n ia n a jk ró ts z y c h
ś c ie ż e k w ty m s a m y m g ra fie D A G (s tro n a 7 5->7
6 7 1 ). W ty m p rz y k ła d z ie a lg o r y tm tw o rz y
0
d rz e w o n a jd łu ż s z y c h śc ie ż e k (a n g . lon g est- 1 5->l
p a th s tree — L P T ) z w ie rz c h o łk a 5 w o p is a n y 3 l->3
4 5->4
p o n iż e j sp o s ó b .
° S tosuje m e to d ę D FS d o u sta le n ia p o rz ą d 7 5->7
k u to p o lo g ic z n e g o 5 1 3 6 4 7 0 2.
n D o d a je d o d rz e w a 5 i w sz y stk ie w y c h o 0
1 5->l
d z ą c e z n ie g o k ra w ę d z ie .
3 l->3
° D o d a je d o d rz e w a 1 i k ra w ę d ź l-> 3 . 4 5->4
5
D D o d a je d o d r z e w a 3 o r a z k r a w ę d z ie 6 3->6
7 3->7
3-> 6 i 3 -> 7 . K ra w ę d ź 5->7 sta je się n ie -
w y b ie ra ln a . 0 6 -> 0
1 5->l
* D o d a je d o d rz e w a 6 o ra z k ra w ęd z ie 6->2, 2 6 -> 2
6-> 4 i 6-> 0. 3 l->3
4 6->4
n D o d a je d o d rz e w a 4 o ra z k ra w ę d z ie 4->0 5
6 3->6
i 4-> 7. K ra w ę d z ie 6-> 0 i 3->7 s ta ją się 7 3->7
n ie w y b ie ra ln e .
0 4->0
■ D o d a je d o d rz e w a 7 i k ra w ę d ź 7-> 2. 1 5->l
2 6 -> 2
K ra w ę d ź 6-> 2 s ta je się n ie w y b ie ra ln a . 3 l->3
4 6->4
° D o d a je d o d r z e w a 0 , a le n ie k r a w ę d ź 5
6 3->6
0 - > 2 , p o n ie w a ż je s t n ie w y b ie ra ln a .
7 4->7
■ D o d a je 2 d o d rz e w a (n ie p o k a z a n o n a
Obecnie niewybieralne
r y s u n k u ).
A lg o ry tm w y z n a c z a n ia n a jd łu ż sz y c h ście ż e k
p rz e tw a rz a w ie rz c h o łk i w tej sam ej k o le jn o śc i,
co a lg o ry tm z n a jd o w a n ia n a jk ró tsz y c h śc ie
żek, je d n a k d aje z u p e łn ie o d m ie n n y w y n ik .
0 4->0
1 5->l
2 7->2
3 l->3
4 6->4
6 3->6
7 4->7
Ślad procesu w yznaczania najdłuższych
ścieżek w acyklicznej sieci
4.4 □ Najkrótsze ścieżki 675
S z e r e g o w a n ie r ó w n o le g ły c h z a d a ń W p o s z u k iw a n iu p rz y k ła d o w e g o z a s to s o w a n ia
w ra c a m y d o p r o b le m ó w szere g o w a n ia , p o ra z p ie r w s z y o m ó w io n y c h w p o d r o z d z i a l e
4 .2 ( s tr o n a 5 8 6 ). R o z w a ż m y n a s tę p u ją c y p r o b le m z te g o o b s z a r u (r ó ż n ic e w p o r ó w
n a n iu z p r o b le m e m ze s tr o n y 5 8 7 w y r ó ż n io n o k u rs y w ą ).
R ó w n o le g łe s z e r e g o w a n ie z o g r a n ic z e n ia m i p ie r w s z e ń s tw a . Ja k n a p o d s ta w ie
z b io r u z a d a ń o o k re ślo n y m cza sie tr w a n ia i o g ra n ic z e ń p ie r w s z e ń s tw a (o k re ś la ją
cy ch , że p r z e d ro z p o c z ę c ie m p e w n y c h z a d a ń tr z e b a u k o ń c z y ć in n e ) u sz e re g o w a ć
z a d a n ia n a id e n ty c zn y c h p ro ceso ra ch (tylu , ile je s t p o tr z e b n e ), t a k a b y z o s ta ły w y k o
n a n e b e z n a r u s z a n ia o g ra n ic z e ń w m o ż liw ie n a jk r ó ts z y m c za sie ?
W p o d r o z d z i a l e 4 .2 n ie ja w n ie p rzy jęto , że m o d e l o p a rty je s t n a je d n y m p ro c eso rz e .
N ależy u szereg o w ać z a d a n ia w p o rz ą d k u to p o lo g ic z n y m , a łą c z n y czas to s u m a czasó w
w y k o n y w a n ia z a d ań . T eraz zak ład am y , że lic z b a d o stę p n y c h p ro c e s o ró w w y sta rcz a d o
w y k o n a n ia d o w o ln ej liczb y za d a ń . Jed y n e o g ra n ic z e n ia w y
n ik ają z p ie rw szeń stw a. T akże tu k o n ie c z n a m o ż e b y ć o b słu - Zadanie Czas Tl ze^a
... 1 /1 1 1 . trwania zakończyć przed
ga tysięcy, a n a w e t m m o n o w za d a ń , d lateg o p o trz e b n y jest
w y d ajn y a lg o ry tm . C o ciekaw e, istn ieje a lg o ry tm d ziałający 0 41.0 1 7
w czasie lin io w ym . P o d ejście n a z y w a n e m e to d ą ścieżki k r y 1 51.0 2
tycznej sta n o w i d o w ó d n a to , że o p isa n y p ro b le m je st a n a lo 2 50.0
giczn y d o p ro b le m u w y z n a c z a n ia n a jd łu ż sz y c h ścieżek w w a 3 35.0
ż o n y ch g ra fa c h D A G . M e to d ę tę sto so w a n o z p o w o d z e n ie m 4 3 8.0
w n iezliczo n y ch za sto so w a n ia c h p rz em y sło w y ch . 5 4 5.0
K o n c e n tru je m y się n a n a jw c z e śn ie jsz y m m o ż liw y m c z a
6 2 1.0 3 8
sie, n a k tó r y m o ż n a z a p la n o w a ć k a ż d e z a d a n ie . Z a k ła d a m y ,
7 3 2.0 3 8
że d o w o ln y d o s tę p n y p ro c e s o r m o ż e w y k o n y w a ć z ad a n ie .
8 3 2.0 2
R o z w a ż m y n a p rz y k ła d p ro b le m p rz e d s ta w io n y p o p r a
9 2 9.0 4 6
wej stro n ie . W ro z w ią z a n iu p o n iż e j u sta lo n o , że 1 7 3 .0 to
m in im a ln y m o ż liw y czas u k o ń c z e n ia d la d o w o ln e g o u sz e - Problem szeregowania zadań
re g o w a n ia z a d a ń z te g o p ro b le m u . U sz e re g o w a n ie s p e łn ia
w szy stk ie o g ra n ic z e n ia , a ż a d n e in n e u sz e re g o w a n ie n ie p o z w a la w y k o n a ć p ra c y p rz e d
c z a se m 17 3 .0 . W y n ik a to z k o le jn o śc i z a d a ń 0 -> 9 -> 6 -> 8 -> 2 . C iąg te n to ścieżka k r y
tyczn a w ty m p ro b le m ie . K a ż d y c ią g z a d a ń , w k tó r y m k a ż d e z a d a n ie m u s i n a stę p o w a ć
p o z a d a n iu p o p rz e d z a ją c y m je w ciąg u , w y z n a c z a d o ln e o g ra n ic z e n ie d łu g o ś c i u s z e re
g o w an ia. Jeśli z d e fin iu je m y d łu g o ś ć ta k ie g o c ią g u ja k o m o ż liw ie n a jw c z e śn ie jsz y czas
u k o ń c z e n ia z a d a ń (łą c z n y czas tr w a n ia z a d a ń ), n a jd łu ż s z y ciąg to śc ie ż k a k ry ty c z n a ,
p o n ie w a ż ja k ie k o lw ie k o p ó ź n ie n ie w czasie ro z p o c z ę c ia k tó re g o ś z z a d a ń p o w o d u je
p rz e s u n ię c ie n a jle p sz e g o m o ż liw e g o c z a su z a k o ń c z e n ia c ałe g o p ro je k tu .
1--------------------------------- i----------------------- 1---------------- i 1 1
0 41 70 91 123 173
Rozwiązanie problemu szeregowania równoległych zadań
676 RO ZD ZIA Ł 4 □ Grafy
Ograniczenie
D efin icja . M e to d a śc ie żk i k r y ty c z n e j p r z y sz e re g o w a n iu ró w n o le g ły m d z ia
ła w n a s tę p u ją c y s p o s ó b — n a le ż y z a c z ą ć o d u tw o r z e n ia w a ż o n e g o g ra f u D A G
o ź ró d le s, u jś c iu t o ra z d w ó c h w ie rz c h o łk a c h d la k a ż d e g o z a d a n ia (w ie r z c h o łk u
p o c z ą tk o w y m i k o ń c o w y m ). D o k a ż d e g o z a d a n ia n a le ż y d o d a ć k ra w ę d ź z w ie r z
c h o łk a p o c z ą tk o w e g o d o w ie rz c h o łk a k o ń c o w e g o o w a d z e ró w n e j c z a so w i tr w a
n ia z a d a n ia . D la k a ż d e g o o g ra n ic z e n ia p ie r w s z e ń s tw a v->w n a le ż y d o d a ć k r a
w ę d ź o w a d z e z e ro z w ie rz c h o łk a k o ń c o w e g o o d p o w ia d a ją c e g o v d o w ie rz c h o łk a
p o c z ą tk o w e g o o d p o w ia d a ją c e g o w. P o n a d to n a le ż y d o d a ć k ra w ę d z ie o w a d z e
z e ro ze ź r ó d ła d o w ie rz c h o łk a p o c z ą tk o w e g o k a ż d e g o z a d a n ia i z w ie rz c h o łk a
k o ń c o w e g o k a ż d e g o z a d a n ia d o u jśc ia . N a s tę p n ie tr z e b a z a p la n o w a ć k a ż d e z a d a
n ie n a cz as ró w n y d łu g o ś c i n a jd łu ż s z e j ś c ie ż k i ze ź ró d ła .
N a r y s u n k u w g ó rn e j części stro n y p rz e d s ta w io n o tę zale ż n o ść d la p rz y k ła d o w e g o
p ro b le m u . R y su n ek w d o ln e j części s tro n y ilu stru je ro z w ią z a n ie p ro b le m u w y z n a c z an ia
n ajd łu ż sz y c h ścieżek. Jak w sp o m n ia n o , g ra f o b e jm u je tr z y k ra w ę d z ie d la k a ż d e g o z a d a n ia
(k raw ęd zie o w ad z e zero ze ź ró d ła d o p o c z ą tk u i z k o ń c a d o u jścia o ra z k ra w ę d ź z p o c z ą t
k u d o k o ń c a ) i je d n ą k ra w ę d ź d la k aż d e g o o g ra n ic z e n ia p ierw sz e ń stw a . K lasa CPM, p r z e d
sta w io n a n a n a stęp n e j stro n ie , to p ro s ta im p le m e n ta c ja m e to d y ścieżek k ry ty czn y ch .
K lasa p rz e k sz ta łc a k a ż d y p ro b le m sz ere g o w an ia z a d a ń n a p ro b le m w y z n a c z a n ia n a jd łu ż
szej ścieżki w w a ż o n y m grafie D A G , w y k o rz y stu je k lasę Acycl i cLP d o je g o ro zw iązan ia,
a n a stę p n ie w y św ietla czasy ro z p o c z ę c ia z a d a ń i w y z n a c z a czas za k o ń c ze n ia .
Rozwiązanie problemu wyznaczania najdłuższych ścieżek dla przykładu z szeregowaniem zadań
4.4 Najkrótsze ścieżki 677
Metoda ścieżki krytycznej dla szeregowania zadań równoległych
z ograniczeniami pierwszeństwa
public c la s s CPM
% more j o b s P C . t x t
10
public s t a t ic void m ain(String[] args)
41 . 0 1 7 9
{ 51.0 2
in t N = S t d l n . r e a d l n t ( ) ; S t d ln . r e a d L i n e Q ; 50 0
EdgeWeightedDigraph G; 36.0
G = new EdgeWeightedDigraph(2*N+2); 38 .0
45 .0
in t s = 2*N, t = 2*N+1; 21-0 38
f o r (in t i = 0; i < N; i++) 32-° 38
| 32.0 2
S t r in g [] a = Std ln .re ad L in e Q .spl i t ( " \ \ s + " ) ; 29.0 46
double duration = [Link](a[0]);
[Link](new Directed Edge(i, i+N, d u ration ));
[Link](new DirectedEdge(s, i, 0 .0 ));
[Link](new DirectedEdge(i+N, t, 0 . 0 )) ;
fo r (in t j = 1; j < [Link]; j++)
{
in t successor = I n t e g e r . p a r s e ln t ( a [ j ] );
[Link](new DirectedEdge(i+N, successor, 0 .0 ));
1
}
AcyclicLP Ip = new AcyclicLP(G, s );
StdOut.p r i n t l n ( "Czasy rozpoczęci a : ") ;
f o r (in t i = 0; i < N; i++)
S [Link]("%4d: % 5 .1 f\n ", i , l p . d i s t T o ( i ) ) ;
S t d O u t .p r in t f("Czas zakończenia: % 5 .1 f\n ", l p . d i s t f o ( t ) ) ;
% j a v a CPM < j o b s P C . t x t
C za sy r o z p o c z ę c ia :
Ta im p le m e n ta c ja m e to d y ścieżki k ry ty c zn e j, p rz e z n a c z o n a 0 : 0.0
1: 41 .0
d o szereg o w an ia zad ań , re d u k u je p ro b le m b e z p o śre d n io d o
2: 123.0
p ro b le m u w y zn a c z a n ia n a jd łu ż sz y c h ścieżek w w ażo n y c h
3: 91 .0
g rafach D A G . P ro g ra m tw o rz y d ig ra f w a ż o n y (m u si być to 4: 70.0
g ra f D A G ) n a p o d sta w ie specyfikacji p ro b le m u szereg o w a 5: 0. 0
n ia zad a ń , zg o d n ie z m e to d ą ścieżki k ry ty c z n e j, a n a stę p n ie 6: 70 .0
7: 4 1 . 0
używ a k lasy A cycl i cLP (zo b acz t w i e r d z e n i e t ) d o z n a le
8: 91 .0
zien ia d rz e w a n ajd łu ż szy c h ścieżek i w y św ietlen ia ich d łu 9: 41 .0
gości (czyli czasów ro zp o c zę c ia k ażd eg o zad a n ia ). Czas z a k o ń c z e n ia : 17 3.0
678 R O ZD ZIA Ł 4 □ Grafy
O ryginał
Twierdzenie U. M e to d a śc ie ż k i k ry ty c z n e j p o z w a la ro z w ią z a ć w c z asie
Zadanie Rozpoczęcie
lin io w y m p r o b le m s z e re g o w a n ia ró w n o le g łe g o z o g r a n ic z e n ia m i p ie r w
0 0.0 s z e ń s tw a .
1 41.0
2 123.0 Dowód. D la c z e g o m e to d a śc ie ż k i k ry ty c z n e j d z ia ła ? P o p ra w n o ś ć a l
3 91.0 g o r y tm u w y n ik a z d w ó c h fak tó w . P o p ie rw s z e , k a ż d a śc ie ż k a w g ra fie
4 70.0 D A G to c ią g p o c z ą tk ó w i z a k o ń c z e ń z a d a ń o d d z ie lo n y c h o g r a n ic z e n ia
5 0.0
m i p ie r w s z e ń s tw a o w a d z e z e ro . D łu g o ś ć k a ż d e j śc ie ż k i ze ź ró d ła s d o
6 70.0
d o w o ln e g o w ie rz c h o łk a v w g ra fie to d o ln e o g ra n ic z e n ie c z a s u r o z p o
7 41.0
8 91.0 c z ę c ia (i z a k o ń c z e n ia ) z a d a n ia re p r e z e n to w a n e g o p rz e z v, p o n ie w a ż n a
9 41.0 ty m s a m y m k o m p u te r z e n ie m o ż n a u z y sk a ć w y n ik u le p s z e g o n iż p rz e z
u s z e re g o w a n ie z a d a ń je d n o p o d r u g im . D łu g o ś ć n a jd łu ż s z e j ś c ie ż k i z s
2 nie później
d o u jś c ia t to d o ln e o g ra n ic z e n ie c z a s u z a k o ń c z e n ia w s z y s tk ic h z a d a ń .
niż 12,0 po 4
P o d ru g ie , w sz y stk ie c z a sy ro z p o c z ę c ia i z a k o ń c z e n ia o d p o w ia d a ją c e
Zadanie Rozpoczęcie
n a jd łu ż s z y m ś c ie ż k o m są realne. K a ż d e z a d a n ie ro z p o c z y n a się p o z a k o ń
0 0.0 c z e n iu w s z y s tk ic h z a d a ń , k tó r y c h je s t n a s tę p n ik ie m w e d łu g o g ra n ic z e ń
1 41.0 p ie rw s z e ń s tw a . Jest ta k , p o n ie w a ż cza s r o z p o c z ę c ia to d łu g o ś ć n a jd łu ż
2 123.0
sze j śc ie ż k i ze ź r ó d ła d o d a n e g o w ie rz c h o łk a . D łu g o ś ć n a jd łu ż s z e j ś c ie ż
3 91.0
k i z s d o t to g ó rn e o g ra n ic z e n ie c z a s u z a k o ń c z e n ia w s z y s tk ic h z a d a ń .
4 1 11 . 0
5 0.0
W y d a jn o ś ć lin io w a a lg o r y tm u w y n ik a b e z p o ś r e d n io z t w i e r d z e n i a t.
6 70.0
7 41.0 S z e r e g o w a n ie z a d a ń r ó w n o le g ły c h z u w z g lę d n i e n ie m w z g lę d n y c h te r m i n ó w
8 91.0 g r a n ic z n y c h K o n w e n c jo n a ln e te r m in y g ra n ic z n e są w y z n a c z a n e w z g lę d e m
9 41.0
c z a su ro z p o c z ę c ia p ie rw s z e g o z a d a n ia . Z ałó żm y , że w p ro b le m ie sz e re g o w a n ia
z a d a ń m o ż n a z a sto so w a ć d o d a tk o w y ro d z a j o g ra n ic z e ń i o k re ślić , że z a d a n ie
2 nie później
niż 70,0 po 7 m u s i się ro z p o c z ą ć p r z e d u p ły w e m o k re ś lo n e g o c z a
Zadanie Czas Względem
su w z g lę d e m in n e g o z a d a n ia . T ak ie o g ra n ic z e n ia są
Zadanie Rozpoczęcie
2 12.0 4
c zę sto p o tr z e b n e w p ro c e s a c h p ro d u k c y jn y c h k r y
0 0.0 2 70.0 7
ty c z n y c h ze w z g lę d u n a czas i w w ie lu in n y c h s y tu a
1 41.0 4 80.0 0
cjac h , je d n a k z n a c z ą c o u tr u d n ia ją ro z w ią z a n ie p r o b
2 123.0 Terminy graniczne
le m u sz ere g o w a n ia . P rz y k ła d o w o załó żm y , ja k p o k a uwzględniane przy
3 91.0
z a n o p o lew ej, że trz e b a d o d a ć o g ra n ic z e n ie , z g o d n ie szeregowaniu zadań
4 111.0
5 0.0 z k tó r y m z a d a n ie 2 m a się ro z p o c z ą ć n ie p ó ź n ie j n iż
6 70.0 12 je d n o s te k c z a su p o ro z p o c z ę c iu z a d a n ia 4. T en te r m in je s t w isto c ie o g r a n i
7 53.0 c z e n ie m c z a su ro z p o c z ę c ia z a d a n ia 4. N ie m o ż e się o n o ro z p o c z ą ć w cześn iej
8 91.0
n iż 12 je d n o s te k c z a su p rz e d u ru c h o m ie n ie m z a d a n ia 2. W p rz y k ła d z ie w p la
9 41.0
n ie je s t m ie jsc e n a d o tr z y m a n ie te r m in u . M o ż n a p rz e s u n ą ć czas ro z p o c z ę c ia
4 nie później z a d a n ia 4 n a 111, czyli 12 je d n o s te k c z a su p r z e d p la n o w a n y m c z a se m r o z p o
niż 80,0 po 0
c z ęc ia z a d a n ia 2. Z au w aż m y , że g d y b y z a d a n ie 4 b y ło d łu g ie , z m ia n a s p o w o
N iem o żliw e! d o w a ła b y o p ó ź n ie n ie c z a su z a k o ń c z e n ia całe g o p ro je k tu . T ak że p o d o d a n iu
Względne d o p la n u te r m in u , z g o d n ie z k tó r y m z a d a n ie 2 m u s i ro z p o c z ą ć się n ie p ó ź n ie j
terminy graniczne n iż 70 je d n o s te k c z a su p o u r u c h o m ie n iu z a d a n ia 7, w p la n ie je s t m ie jsc e n a
przy szeregowaniu
zadań z m ia n ę c z a su ro z p o c z ę c ia z a d a n ia 7 n a 53 b e z k o n ie c z n o ś c i p rz e k ła d a n ia z a
4.4 □ Najkrótsze ścieżki 679
d a ń 3 i 8 . Jeśli je d n a k d o d a m y te r m in , w e d le k tó re g o z a d a n ie 4 m u s i się ro z p o c z y n a ć
nie p ó ź n ie j n iż 80 je d n o s te k p o z a d a n iu 0, p la n s ta n ie się n ie w y k o n a ln y . O g ra n ic z e n ia
określające, że z a d a n ie 4 tr z e b a u ru c h o m ić n ie p ó ź n ie j n iż 80 je d n o s te k c z a su p o z a
d a n iu 0, a z a d a n ie 2 — n ie p ó ź n ie j n iż 12 je d n o s te k c z a su p o z a d a n iu 4, o z n a c z a ją , że
z a d a n ie 2 n ie m o ż e ro z p o c z ą ć się p ó ź n ie j n iż 93 je d n o s tk i c z a su p o z a d a n iu 0. J e d n a k
z a d a n ie 2 ro z p o c z y n a się n ie w cz e śn ie j n iż 123 je d n o s tk i c z a su p o z a d a n iu 0. W y n ik a
to z ła ń c u c h a 0 (41 je d n o s te k ) p r z e d 9 (2 9 je d n o s te k ) p r z e d 6 (21 je d n o s te k ) p rz e d 8
(32 je d n o s tk i) p rz e d 2. D o d a w a n ie n o w y c h te r m in ó w p ro w a d z i, o czy w iście, d o zw ie lo
k ro tn ie n ia m o ż liw o śc i i p o w o d u je p rz e k s z ta łc e n ie ła tw e g o p ro b le m u w tru d n y .
Twierdzenie V. S z e re g o w a n ie z a d a ń ró w n o le g ły c h ze w z g lę d n y m i te r m in a m i
g ra n ic z n y m i to p r o b le m w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k w d ig ra fa c h w a ż o
n y c h (z m o ż liw y m i c y k la m i i w a g a m i u je m n y m i).
Dowód. N a le ż y z a sto so w a ć te n s a m p ro c e s , co w t w ie r d z e n iu u , i dodać k ra
w ę d ź d la k a ż d e g o te r m in u . Jeśli z a d a n ie v m u s i się ro z p o c z y n a ć w c ią g u d j e d n o
s te k c z a s u o d u r u c h o m ie n ia z a d a n ia w, tr z e b a d o d a ć k ra w ę d ź z v d o w o u je m n e j
w a d z e d. N a s tę p n ie n a le ż y p rz e k s z ta łc ić z a d a n ie n a p r o b le m w y z n a c z a n ia n a j
k ró ts z y c h śc ie ż e k , o d w ra c a ją c z n a k w s z y s tk ic h w a g d ig ra fu . D o w ó d p o p r a w n o ś c i
o b o w ią z u je te ż w ty m p r z y p a d k u — p o d w a r u n k ie m ż e p la n je s t w y k o n a ln y . Jak
się o k a ż e , u s ta le n ie , czy p la n je s t w y k o n a ln y , to z a d a n ie w y d łu ż a ją c e o b lic z e n ia .
W ty m p rz y k ła d z ie p o k a z a n o , że w a g i u je m n e m o g ą o d g ry w a ć k lu c z o w ą ro lę w m o
d e la c h p ra k ty c z n y c h sy tu a c ji. Jeśli m o ż n a z n a le ź ć w y d a jn e ro z w ią z a n ie p ro b le m u
w y z n a c z a n ia n a jk ró ts z y c h śc ie ż e k o b e jm u ją c y c h u je m n e w a g i, m o ż n a te ż z n a le ź ć
w y d a jn e ro z w ią z a n ie p r o b le m u s z e re g o w a n ia ró w n o le g ły c h z a d a ń ze w z g lę d n y
m i te r m i n a m i g ra n ic z n y m i. Ż a d e n z o m ó w io n y c h w c z e śn ie j a lg o r y tm ó w n ie je s t
tu o d p o w ie d n i. A lg o ry tm D ijk s try w y m a g a , a b y w a g i b y ły d o d a tn ie (lu b z e ro w e ),
a a l g o r y t m 4 . i o w y m a g a , ż e b y d ig r a f b y ł a c y k lic zn y . D a le j w y ja śn ia m y , ja k p o ra d z ić
so b ie z u je m n y m i w a g a m i w d ig ra fa c h , k tó r e m o g ą o b e jm o w a ć cy k le.
-70
Digraf ważony reprezentujący szeregowanie równoległe z ograniczeniami
pierwszeństwa i względnymi terminami granicznymi
680 R O ZD ZIA Ł 4 Q Grafy
Najkrótsze ścieżki w ogólnych digrafach ważonych W p rz y k ła d z ie s z e
re g o w a n ia z a d a ń z te r m i n a m i g ra n ic z n y m i p o k a z a n o , że w a g i u je m n e n ie są ty lk o
m a te m a ty c z n ą c ie k a w o stk ą ; w p r o s t p rz e c iw n ie — z n a c z n ie ro z s z e rz a ją z a k re s z a
s to s o w a ń m e to d y w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k ja k o m e to d y ro z w ią z y w a n ia
p ro b le m ó w . D la te g o te r a z o m a w ia m y a lg o r y tm y d la d ig ra fó w w a ż o n y c h , k tó r e m o g ą
o b e jm o w a ć z a r ó w n o c y k le , ja k i u je m n e w ag i. N a jp ie r w je d n a k p rz e d s ta w ia m y p e w
ti [Link] n e p o d s ta w o w e w ła śc iw o ś c i ta k ic h d ig r a
fów , a b y z m ie n ić in tu ic y jn e p o d e jś c ie d o
n a jk r ó ts z y c h śc ie że k . N a r y s u n k u p o le
4->5 0.35
5->4 0.35 w ej s tr o n ie w id o c z n y je s t k r ó tk i p rz y k ła d ,
4->7 0.37
n a k tó r y m pokazano s k u tk i u w z g lę d
5->7 0.28
7->5 0.28 n ia n ia w a g u je m n y c h p r z y w y z n a c z a n iu
5->l 0.32 n a jk r ó ts z y c h ście ż ek . P ra w d o p o d o b n ie
0->4 0.38
0->2 0.26 n a jw a ż n ie js z e je s t to , że k ie d y w y s tę p u ją
7->3 0.39 Wagi ujemne oznaczamy w a g i u je m n e , n a jk r ó ts z e śc ie ż k i o n isk ie j
1-> 3 0.29 linią przerywaną
2->7 0.34
w a d z e m a ją z w y k le w ięcej k ra w ę d z i n iż
6->2 - 1.2 0 ś c ie ż k i o w y ż sz e j w a d z e . W p r z y p a d k u
3->6 0.52
w a g d o d a tn ic h w a ż n e b y ło w y s z u k iw a
6->0 -1.40
6->4 -1.25 n ie sk ró tó w . J e d n a k je ś li w y s tę p u ją w a g i
u je m n e , w y s z u k iw a n e są o b ja z d y o b e j
Drzewo najkrótszych ścieżek z 0 edgeTo[] d istT o[]
0 m u ją c e k ra w ę d z ie o w a g a c h u je m n y c h .
1 5->l 0.93
P o w o d u je to , że in tu ic y jn e n a s ta w ie n ie n a
2 0->2 0.26
3 7->3 0.99 w y s z u k iw a n ie „ k ró tk ic h ” śc ie ż e k u t r u d
4 6->4 0.26
5 4->5 0.61 n ia z ro z u m ie n ie a lg o ry tm ó w . D la te g o
6 3->6 1.51 tr z e b a p o r z u c ić te n to k m y ś le n ia i z a s ta
7 2->7 0.60
n o w ić się n a d p r o b le m e m n a p o d s ta w o
Digraf ważony z ujemnymi wagami
w y m , a b s tra k c y jn y m p o z io m ie .
P r ó b a n u m e r I P ie rw s z y p o m y s ł, k tó r y s a m się n a rz u c a , p o le g a n a z n a le z ie n iu k r a
w ę d z i o n a jm n ie js z e j (n a jb a rd z ie j u je m n e j) w a d z e i d o d a n iu w a rto ś c i b e z w z g lę d n e j
tej w a g i d o w s z y s tk ic h k ra w ę d z i w c e lu p r z e k s z ta łc e n ia d ig r a fu n a w e rsję b e z w a g
u je m n y c h . To n a iw n e p o d e jś c ie w o g ó le n ie z a d z ia ła , p o n ie w a ż n a jk r ó ts z e śc ie ż k i
w n o w y m g ra fie n ie b ę d ą o d p o w ia d a ć n a jk r ó ts z y m ś c ie ż k o m w je g o p ie r w o tn e j w e r
sji. Im w ię c e j k ra w ę d z i ś c ie ż k a o b e jm u je , ty m w ię k sz e s z k o d y p o w o d u je ta k ie p r z e
k s z ta łc e n ie (z o b a c z ć w i c z e n i e 4 .4 . 1 4 ).
P r ó b a n u m e r I I D r u g i n a rz u c a ją c y się p o m y s ł p o le g a n a p ró b ie z a a d a p to w a n ia a l
g o r y tm u D ijk s try . P o d s ta w o w y p r o b le m z ty m p o d e jś c ie m p o le g a n a ty m , że a lg o
r y t m w y m a g a s p r a w d z e n ia śc ie ż e k w k o le jn o ś c i ro s n ą c e j w e d łu g ic h o d le g ło ś c i o d
ź ró d ła . W d o w o d z ie p o p r a w n o ś c i a lg o r y tm u w t w i e r d z e n i u r z a ło ż o n o , że d o d a n ie
k ra w ę d z i d o ś c ie ż k i p o w o d u je jej w y d łu ż e n ie . J e d n a k k a ż d a k ra w ę d ź o w a d z e u je m
n ej p ro w a d z i d o sk ró c e n ia śc ie żk i, d la te g o z a ło ż e n ie je s t t u n ie u z a s a d n io n e (z o b a c z
ć w i c z e n i e 4 .4 . 1 4 ).
4.4 o Najkrótsze ścieżki 681
[Link] C y k le u j e m n e P rz y r o z w a ż a n iu d i-
g rafó w , w k tó r y c h m o g ą w y s tę p o w a ć
15
4 5 0.35
k ra w ę d z ie o w agach u je m n y c h , n a j
5 4 - 0.66 k ró ts z e ś c ie ż k i n ie m a ją z n a c z e n ia , je ś li
4 7 0.37
w d ig ra fie is tn ie je c y k l o u je m n e j w a d z e .
0.28
7 5 0.28 R o z w a ż m y n a p r z y k ła d d ig r a f w id o c z
51 0.32 n y p o lew ej s tro n ie , n ie m a l id e n ty c z n y
0 4 0.38
0.26 z p ie r w s z y m p rz y k ła d e m . W y ją tk ie m
0.39 je s t to , że k ra w ę d ź 5-> 4 m a w a g ę - 0 .6 6 .
0.29
0.34 W a g a c y k lu 4 -> 7 -> 5 -> 4 w y n o s i tu :
0.40
0.52 0 .3 7 + 0 .2 8 - 0 .6 6 = - 0 .0 1
0.58
0.93 M o ż n a w ie lo k r o tn ie p r z e c h o d z ić p rz e z
Najkrótsza ścieżka z 0 do 6 te n c y k l i g e n e ro w a ć d o w o ln ie k ró tk ie
0->4->7->5->4->7->5 . . .••>.1">3 >6 ścieżk i! Z a u w a ż m y , że n ie w sz y stk ie k r a
Digraf ważony z ujemnym cyklem w ę d z ie w c y k lu s k ie ro w a n y m m u s z ą m ie ć
u je m n e w ag i. W a ż n a je s t s u m a w ag.
Definicja. C ykl u je m n y w d ig ra fie w a ż o n y m to c y k l sk ie ro w a n y , k tó re g o łą c z n a
s u m a (s u m a w a g k ra w ę d z i) je s t u je m n a .
T eraz z a łó ż m y , że p e w ie n w ie rz c h o łe k n a śc ie ż c e
Szary wierzchołek
z s d o o s ią g a ln e g o w ie rz c h o łk a v z n a jd u je się w c y -nieosiągalny z s
k lu u je m n y m . W te d y z a ło ż e n ie is tn ie n ia n a jk ró ts z e j
śc ie ż k i z s d o v p o w o d u je s p rz e c z n o ś ć , p o n ie w a ż
m o ż n a w y k o rz y s ta ć c y k l d o u tw o r z e n ia ś c ie ż k i o w a
Biały wierzchołek
d ze m n ie js z e j n iż d o w o ln a w a rto ś ć . O z n a c z a to , że
' - osiągalny z s
jeśli is tn ie ją c y k le u je m n e , p r o b le m w y z n a c z a n ia n a j
k ró ts z y c h śc ie ż e k je s t źle p o sta w io n y .
Czarny obrys
- istnieje
Twierdzenie W. N a jk ró ts z a ś c ie ż k a z s d o v najkrótsza
ścieżka z s
w d ig ra fie w a ż o n y m is tn ie je w te d y i ty lk o w ted y ,
je śli o b e c n a je s t p r z y n a jm n ie j je d n a s k ie ro w a n a
śc ie ż k a z s d o v o r a z ż a d e n w ie rz c h o łe k n a tej
ście ż c e n ie n a le ż y d o c y k lu s k ie ro w a n e g o .
Dowód. Z o b a c z w c z e śn ie jsz e o m ó w ie n ie
i ć w i c z e n i e 4 .4 . 2 9 .
Z auw ażm y, że w y m ó g , ab y n a jk ró tsz e ścieżk i n ie o b e jm o
w ały w ie rz c h o łk ó w n a le ż ą c y c h d o cykli u je m n y c h , o z n a /
Czerwony obrys - nie istnieje najkrótsza ścieżka z 5
cza, iż n a jk ró tsz e ścieżk i są p ro ste , d lateg o m o ż n a w y z n a
czyć d la w ie rz c h o łk ó w d rz e w o n a jk ró tsz y c h ścieżek, ta k Możliwości związane z najkrótszymi ścieżkami
ja k w g rafach z k ra w ę d z ia m i o d o d a tn ic h w ag ach .
682 R O ZD ZIA Ł 4 □ Grafy
P ró b a n u m e r III N ie z a le ż n ie o d w y s tę p o w a n ia c y k li u je m n y c h is tn ie je n a jk ró ts z a
ś c ie ż k a p ro s ta łą c z ą c a ź r ó d ło z k a ż d y m o s ią g a ln y m z n ie g o w ie rz c h o łk ie m . D la c z e g o
n ie z d e fin io w a ć n a jk r ó ts z y c h śc ie ż e k w ta k i s p o s ó b , a b y w y z n a c z a ć śc ie ż k i p ro s te ?
N ie ste ty , n a jle p s z y z n a n y a lg o r y tm ro z w ią z u ją c y te n p r o b le m d z ia ła d la n a jg o rsz e g o
p r z y p a d k u w c z a sie w y k ła d n ic z y m (z o b a c z r o z d z i a ł 6 .). O g ó ln ie u z n a je m y ta k ie
p ro b le m y za „ z b y t t r u d n e d o ro z w ią z a n ia ” i b a d a m y p ro s ts z e w e rsje.
ta k więc d o b rz e p o s ta w io n a i m o ż liw a d o r o z w i ą z a n i a wersja p ro b le m u w y
znaczania najkrótszych ścieżek w digrafach w a żo n ych w ym aga, aby algorytm :
D P rz y p is y w a ł d o w ie rz c h o łk ó w n ie d o s tę p n y c h ze ź r ó d ła w a g ę n a jk ró ts z e j śc ie ż k i
ró w n ą + °°.
■ P rz y p is y w a ł d o w ie rz c h o łk ó w ś c ie ż k i n a le ż ą c y c h d o c y k lu u je m n e g o w a g ę n a j
k ró ts z e j ś c ie ż k i ró w n ą
■ W y lic z a ł w a g ę n a jk ró ts z e j śc ie ż k i (i w y z n a c z a ł d rz e w o ) d la w s z y s tk ic h p o z o s ta
ły c h w ie rz c h o łk ó w .
W ty m p o d r o z d z ia le n a k ła d a liś m y o g ra n ic z e n ia n a p r o b le m w y z n a c z a n ia n a jk r ó t
sz y c h śc ie ż e k , t a k a b y m o ż n a o p ra c o w a ć a lg o r y tm y b ę d ą c e r o z w ią z a n ie m p ro b le m u .
N a jp ie r w w y k lu c z y liś m y m o ż liw o ś ć w y s tę p o w a n ia w a g u je m n y c h , a n a s tę p n ie — c y
k li s k ie ro w a n y c h . T e ra z p rz y jm u je m y lu ź n ie js z e o g ra n ic z e n ia i k o n c e n tr u je m y się n a
p o n iż s z y c h p r o b le m a c h d la o g ó ln y c h d ig ra fó w .
W ykryw an ie cykli ujem nych. C z y w d a n y m d ig ra fie w a ż o n y m w y stę p u je cy k l u je m
ny? Jeśli ta k , n a le ż y g o z n a le ź ć .
W y zn a cza n ie n a jk ró tszych ścieżek z je d n e g o źró d ła , je ś li cykle u jem n e są n ieo sią
galn e. D la d ig r a fu w a ż o n e g o i ź r ó d ła s, z k tó r e g o n ie o s ią g a ln e są c y k le u je m n e ,
z a p e w n ij o b s łu g ę z a p y ta ń w p o s ta c i: C zy istn ieje śc ie żk a sk ie ro w a n a z s d o d a n eg o
w ie rz c h o łk a d ocelow ego v? Jeśli ta k , z n a jd ź n a jk r ó tsz ą śc ie ż k ę te g o ro d z a ju (o m i
n im a ln e j łą c z n e j w a d z e ).
p o d s u m o w a n ie — c h o ć w y z n a c z a n ie n a jk r ó ts z y c h śc ie ż e k w d ig r a fa c h z c y k la
m i s k ie ro w a n y m i to źle p o s ta w io n y p r o b le m i n ie m o ż n a s k u te c z n ie ro z w ią z a ć g o
p rz e z z n a le z ie n ie n a jk r ó ts z y c h ś c ie ż e k p ro s ty c h , w p ra k ty c e m o ż n a z id e n ty fik o w a ć
cy k le u je m n e . P rz y k ła d o w o , w p ro b le m ie sz e re g o w a n ia z a d a ń z te r m i n a m i g r a
n ic z n y m i m o ż n a o c z e k iw a ć , że c y k le u je m n e b ę d ą w y s tę p o w a ć s to s u n k o w o r z a d
ko. O g ra n ic z e n ia i te r m in y g ra n ic z n e w y n ik a ją z o g ra n ic z e ń św ia ta rz e c z y w is te g o ,
d la te g o k a ż d y c y k l u je m n y p r a w d o p o d o b n ie w y n ik a z b łę d u w u ję c iu p ro b le m u .
S e n s o w n y m s p o s o b e m p o s tę p o w a n ia je s t w y k ry c ie c y k li u je m n y c h , n a p ra w ie n ie
b łę d ó w i z n a le z ie n ie u s z e r e g o w a n ia d la p r o b le m u p o z b a w io n e g o c y k li u je m n y c h .
W in n y c h s y tu a c ja c h z n a le z ie n ie c y k lu u je m n e g o je s t c e le m o b lic z e ń . O p is a n e d alej
p o d e jś c ie , o p ra c o w a n e p rz e z R. B e llm a n a i L. F o rd a p o d k o n ie c la t 50. u b ie g łe g o w ie
k u , to p r o s ty i s k u te c z n y p u n k t w y jśc ia d o p o r a d z e n ia so b ie z o b o m a p ro b le m a m i.
R o z w ią z a n ie d z ia ła te ż d la d ig r a fó w o w a g a c h d o d a tn ic h .
4.4 * Najkrótsze ścieżki 683
Twierdzenie X (algorytm Bellmana-Forda). O p is a n a d a le j m e to d a r o z w ią
zu je p r o b le m w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k z d a n e g o ź ró d ła s w d o w o ln y m
d ig ra fie w a ż o n y m o V w ie rz c h o łk a c h , p rz y c z y m n ie m o g ą is tn ie ć c y k le u je m n e
d o s tę p n e z s. O to ta m e to d a : n a le ż y z a in ic jo w a ć d i s tT o [s ] w a rto ś c ią 0, a w s z y s t
k ie p o z o s ta łe e le m e n ty ta b lic y di stT o [] — n ie s k o ń c z o n o ś c ią ; n a s tę p n ie tr z e b a
w y k o n a ć re la k s a c ję w s z y s tk ic h k ra w ę d z i d ig r a fu w d o w o ln e j k o le jn o ś c i i w y k o
n a ć V ta k ic h p rz e b ie g ó w .
Dowód. D la d o w o ln e g o w ie rz c h o łk a t o sią g a ln e g o z s n a le ż y ro z w a ż y ć k o n k r e t
n ą n a jk ró ts z ą ścieżk ę z s d o t — v 0-> v 1- > . . . -> v k, g d z ie v 0 to s, a vk to t . P o n ie w a ż
nie w y stę p u ją cy k le u je m n e , ta k a śc ie ż k a istn ieje , a k n ie je s t w ię k sz e n iż V - 1.
P rz e z in d u k c ję n a i p o k a z u je m y , że p o i - ty m p rz e b ie g u a lg o r y tm w y z n a c z a n a j
k ró ts z ą ścieżk ę z s d o v . P rz y p a d e k p o d s ta w o w y (i = 0 ) je s t try w ia ln y . P rz y z a ło
ż e n iu , że tw ie rd z e n ie je s t p ra w d z iw e d la i , v0-> V j-> .. . ->v. to n a jk ró ts z a śc ie ż k a z s
d o vf, a d i stT o [ v .] to jej d łu g o ść . W i -ty m p rz e b ie g u p rz e p ro w a d z a m y re la k sa c ję
k aż d e g o w ie rz c h o łk a , w ty m v., ta k w ięc d i s tT o [ v .+1] m a w a rto ś ć n ie w ię k sz ą n iż
d i s t T o f y ] p lu s w a g a v .-> v i+r P o i - t y m p rz e b ie g u d is tT o [ v .+1] m u s i b y ć ró w n e
di s tT o [ v .] p lu s w a g a v .-> v .+r W a rto ść n ie m o ż e być w ięk sza, p o n ie w a ż w i -ty m
p rz e b ie g u w y k o n u je m y re la k sa c ję k a ż d e g o w ie rz c h o łk a , w ty m v., o ra z n ie m o ż e
być m n ie js z a , p o n ie w a ż s ta n o w i d łu g o ś ć n a jk ró tsz e j ście ż k i — v0- > V j-> .. . - >vj+r
T ak w ię c a lg o ry tm w y z n a c z a n a jk ró ts z ą śc ie ż k ę z s d o v 1+1 p o ( i +1) p rz e b ie g a c h .
Twierdzenie W (ciąg dalszy). A lg o ry tm B e llm a n a -F o rd a d z ia ła w czasie p r o p o r
c jo n a ln y m d o E V i w y m a g a d o d a tk o w e j p a m ię c i w ilo śc i p ro p o r c jo n a ln e j d o V.
Dowód. K a ż d y z V p rz e b ie g ó w p o w o d u je re la k s a c ję E k ra w ę d z i.
M e to d a t a je s t b a r d z o o g ó ln a , p o n ie w a ż n ie n a r z u c a k o le jn o ś c i re la k s a c ji k ra w ę d z i.
D alej o g r a n ic z a m y u w a g ę d o m n ie j o g ó ln e j m e to d y , w k tó re j re la k s a c ja je s t w y k o n y
w a n a d la w s z y s tk ic h k ra w ę d z i (w d o w o ln y m p o rz ą d k u ) w y c h o d z ą c y c h z d o w o ln e g o
w ie rz c h o łk a . P o n iż s z y k o d d o w o d z i p r o s to ty te g o p o d e jś c ia :
f o r ( i n t p a s s = 0 ; p a s s < G. V( ) ; p a ss+ + )
f o r (v = 0 ; v < G. V( ) ; v++)
f o r (D ire c te d E d g e e : G. a d j ( v ) )
re la x (e );
N ie ro z w a ż a m y s z c z e g ó ło w o tej w e rsji, p o n ie w a ż z a w s z e p o w o d u je re la k s a c ję V E
k ra w ę d z i, a p r o s ta m o d y fik a c ja sp ra w ia , że w ty p o w y c h z a s to s o w a n ia c h a lg o r y tm
je s t z n a c z n ie w y d ajn iejsz y .
684 RO ZD ZIA Ł 4 o Grafy
Źródło A lg o r y tm B e llm a n a -F o r d a o p a r ty n a k o le jc e M o ż
e d g e T o []
I n a ła tw o z g ó ry ok reślić, że w iele k ra w ę d z i w d a n y m
p rz e b ie g u n ie u m o ż liw ia w y k o n a n ia u d a n e j re la k sa
l- > 3 cji. Jed y n e k ra w ęd z ie m o g ą c e sp o w o d o w a ć z m ia n ę
n © - © \
w ta b lic y di stT o [] w y c h o d z ą z w ierzc h o łk a , k tó reg o
© '- = := ^ ® * w a rto ś ć w tej ta b lic y zm o d y fik o w a n o w p o p rz e d n im
p rzeb ieg u . D o śle d z e n ia ta k ic h w ie rz c h o łk ó w u ż y w a
Na czerwono oznaczono
wierzchołki znajdujące się m y k o lejk i F IF O . P o lew ej stro n ie p o k a z a n o , ja k alg o
w kolejce w danym kroku
e d g e T o [] ry tm d z ia ła d la sta n d a rd o w e g o p rz y k ła d u z d o d a tn i
m i w ag am i. P o lew ej stro n ie r y s u n k u w id o c z n a jest
zaw a rto ść k o lejk i w d a n y m p rz e b ie g u (n a c z e rw o n o )
i w n a s tę p n y m p rz e b ie g u (n a c z a rn o ). P o czątk o w o
3 -> 6
w kolejce z n a jd u je się ź ró d ło . D rz e w o S P T m o ż n a
w y zn aczy ć w o p isa n y p o n iż e j sp o só b .
edgeTo [] ■ R e la k sa c ja k ra w ę d z i l- > 3 i u m ie s z c z e n ie
6->0 3 w k o lejce.
6->2 ■ R e la k sa c ja k ra w ę d z i 3-> 6 i u m ie s z c z e n ie
l->3
6->4 6 w k o lejce.
■ R e la k sa c ja k ra w ę d z i 6 -> 4 , 6 -> 0 i 6-> 2 o ra z
3->6
u m ie s z c z e n ie 4, 0 i 2 w k o le jce .
■ R e la k sa c ja k ra w ę d z i 4->7 i 4 -> 5 o ra z u m ie s z
edgeT o []
6->0 c z e n ie 7 i 5 w k o le jc e . N a s tę p n ie re la k sa c ja
6 ->2 k ra w ę d z i 0 -> 4 i 0 -> 2 , k tó r e są n ie w y b ie ra ln e ,
1->3
i re la k s a c ja k ra w ę d z i 2-> 7 (o r a z z m ia n a k o lo
6->4
4->5 r u k ra w ę d z i 4 -> 7 ).
\ 3->6
Krawędź 2->7 ■ R elak sacja k ra w ę d z i 7->5 (o ra z z m ia n a k o lo ru
o zmienionym kolorze k ra w ę d z i 4-> 5), p rz y c z y m n ie n a leż y u m ie s z
edge T o []
6~>0
czać 5 w kolejce, p o n ie w a ż ju ż się ta m zn ajd u je.
D alej n a stę p u je relak sacja k ra w ę d z i 7->3, k tó ra
6->2
1->3 je s t n ie w y b ie ra ln a . P o te m m a m iejsce re la k sa
6->4
7->5
c ja k ra w ę d z i 5 -> l, 5->4 i 5->7 (są n ie w y b ie ra l
3->6 n e ), p o c z y m k o lejk a staje się p u sta.
2->7
I m p le m e n ta c j a Z a im p le m e n to w a n ie a lg o ry tm u
edgeTo []
0 6-> 0 B e llm a n a -F o rd a w te n sp o s ó b w y m a g a z a sk a k u ją c o
1 n ie w ie le k o d u , co p o k a z a n o w a l g o r y t m i e 4 . 1 1 .
2 6 -> 2
3 l- > 3 R o z w ią z a n ie o p a rte je s t n a d w ó c h d o d a tk o w y c h
4 6->4
7->5 s tru k tu r a c h d a n y ch :
3->6 ■ k o le jc e q z w ie r z c h o łk a m i p rz e z n a c z o n y m i
2 -> 7
d o re la k sa c ji;
Ślad działania algorytmu Bellmana-Forda
■ in d e k s o w a n e j w ie rz c h o łk a m i ta b lic y onQ[]
z w a rto ś c ia m i ty p u b o o le a n , o k re ś la ją c y m i,
k tó r e w ie rz c h o łk i z n a jd u ją się w k o le jc e ( p o
z w a la to u n ik n ą ć d u p lik a tó w ).
4.4 0 Najkrótsze ścieżki 685
Z a c z y n a m y o d u m ie s z c z e n ia w k o le jc e ź r ó d ła s. N a s tę p n ie w c h o d z im y w p ę tlę , k tó r a
p o b ie r a w ie rz c h o łe k z k o le jk i i p rz e p r o w a d z a re la k sa c ję . W c e lu d o d a w a n ia w ie rz
c h o łk ó w d o k o le jk i ro z b u d o w a liś m y im p le m e n ta c ję m e to d y r e l a x ( ) ze s tr o n y 6 5 8 ,
ab y u m ie s z c z a ła w k o le jc e w ie rz c h o łe k d o c e lo w y k a ż d e j k ra w ę d z i, d la k tó re j w y k o
n a n o u d a n ą re la k s a c ję (n o w ą w e rsję p o k a z a n o w k o d z ie p o p ra w e j s tro n ie ) . U ż y te
s t r u k tu r y d a n y c h g w a ra n tu ją , że:
■ W k o le jc e z n a jd u je się ty lk o je d n a p r i y a t e VQid r e i ax(Ed g e W e ig h t e d D ig r a p h G, in t v)
k o p ia k a ż d e g o w ie rz c h o łk a .
° K a ż d y w ie rz c h o łe k , k tó re g o w a r f o r (D i re c t e d E d g e e : G. a d j ( v )
{
to ś c i ed g eT o [] i d i stT o [] z m ie n iły
in t w = e .to ();
się w p e w n y m p rz e b ie g u , z o s ta n ie if (dis tT o Jw ] > d i s t T o [ v ] + e . w e i g h t O )
p r z e tw o r z o n y w n a s tę p n y m . 1
dis t T o Jw ] = di stT o [v] + e . w e i g h t O ;
W c elu u z u p e łn ie n ia im p le m e n ta c ji t r z e
edgeTo[w] = e;
b a z a g w a ra n to w a ć , że a lg o r y tm z a k o ń c z y i f (! onQ[w])
d z ia ła n ie p o V p rz e b ie g a c h . J e d n y m ze {
[Link](w);
s p o s o b ó w n a o s ią g n ię c ie te g o c e lu je s t
onQ[w] = t r u e ;
b e z p o ś r e d n ie ś le d z e n ie lic z b y p rz e b ie g ó w .
}
W o p raco w an ej p rzez nas im p le m e n 1
ta c ji k la s y B e llm a n F o rd S P (a lg o ry tm if ( c o s t + + % G. V () == 0)
findNegativeC ycle();
4 .1 1 ) w y k o rz y s ta liś m y in n e p o d e jś c ie ,
}
o m ó w i o n e s z c z e g ó ł o w o n a s t r o n i e 689. }
T e c h n ik a p o le g a n a w y k ry w a n iu cy k li
ujem nych W p o dzbio rze kraw ędzi d igra fu Relaksacja w algorytmie Bellmana-Forda
zapisanych w edgeT o [] i k o ń c z y działanie
po znalezieniu takiego cyklu.
Twierdzenie Y. O p a r ta n a k o le jc e im p le m e n ta c ja a lg o r y tm u B e llm a n a -F o r d a
ro z w ią z u je p r o b le m w y z n a c z a n ia n a jk r ó ts z y c h ś c ie ż e k z d a n e g o ź r ó d ła s (lu b
z n a jd u je c y k l u je m n y o s ią g a ln y z s) d la d o w o ln e g o d ig r a fu w a ż o n e g o o V w ie rz
c h o łk a c h w c z a sie p r o p o r c jo n a ln y m d o E V i p r z y u ż y c iu d o d a tk o w e j p a m ię c i
w ilo śc i p r o p o r c jo n a ln e j d o V (d la n a jg o rs z e g o p r z y p a d k u ) .
Dowód. Jeśli n ie is tn ie je c y k l u je m n y o s ią g a ln y z s, a lg o r y tm k o ń c z y d z ia ła n ie
p o re la k s a c ja c h o d p o w ia d a ją c y c h p rz e b ie g o w i ( V - 1 ) g e n e ry c z n e g o a lg o r y tm u
o p is a n e g o w t w i e r d z e n i u x ( p o n ie w a ż w sz y stk ie n a jk r ó ts z e śc ie ż k i m a ją m n ie j
n iż V - 1 k ra w ę d z i). Jeżeli z s o s ią g a ln y je s t c y k l u je m n y , k o le jk a n ig d y n ie z o s ta
n ie o p r ó ż n io n a . P o re la k s a c ja c h o d p o w ia d a ją c y c h V -te m u p rz e b ie g o w i o g ó ln e g o
a lg o r y tm u o p is a n e g o w t w i e r d z e n i u x ta b lic a edgeTo [] o b e jm u je śc ie ż k ę z c y
k le m (łą c z y p e w ie n w ie rz c h o łe k w z n im s a m y m ), a c y k l te n m u s i b y ć u je m n y ,
p o n ie w a ż śc ie ż k a z s d o d ru g ie g o w y s tą p ie n ia w m u s i b y ć k ró ts z a n iż śc ie ż k a
z s d o p ie rw s z e g o w y s tą p ie n ia w, a b y w z n a la z ł się w śc ie ż c e p o r a z d ru g i. D la
n a jg o rs z e g o p r z y p a d k u a lg o r y tm d z ia ła ta k , ja k a lg o r y tm o g ó ln y , i w k a ż d y m z V
p rz e b ie g ó w w y k o n u je re la k s a c ję w s z y s tk ic h E k ra w ę d z i.
686 RO ZD ZIA Ł 4 Grafy
ALGORYTM 4.11. Algorytm Bellmana-Forda (oparty na kolejce)
public c la ss Bel ImanFordSP
(
private doublet] distTo; // DTugość ście żk i do v.
private DirectedEdge[] edgeTo; // Ostatnia krawędź ście ż k i do v.
private boolean[] onQ; // Czy dany wierzchołek znajduje
// s ię w kolejce?
private Queue<Integer> queue; // Wierzchołki po re la k s a c j i.
private in t cost; // Liczba wywołań metody re la x ().
private Iterable<DirectedEdge> cycle; // Czy edgeTo[] obejmuje cykl
// ujemny?
public BellmanFordSP(EdgeWeightedDigraph G, in t s)
{
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G .V()];
onQ = new boolean[G .V ()];
queue = new Queue<Integer>();
for (in t v = 0; v < G.V(); v++)
di stTo[v] = [Link] VE_INF I NI TY;
di stTo [s] = 0.0;
[Link](s);
onQ [s] = true;
while (¡[Link] Empty() && ![Link] egativeC ycle())
{
in t v = [Link]();
onQ[v] = fa lse ;
re la x(v);
}
}
private void r e la x ( in t v)
// Zobacz stronę 685.
public double d is t T o ( in t v) // Standardowe metody obsługi
// zapytań klientów
public boolean hasPathTo(int v) // dla implementacji technik
// tworzenia drzew SPT
public Iterable<Edge> pathTo(int v) // (zobacz stronę 661).
private void findNegativeCycle()
public boolean hasNegativeCycle()
public Iterable<Edge> negativeCycle()
// Zobacz stronę 689.
W tej im p le m e n ta c ji a lg o ry tm u B e llm a n a -F o rd a u ż y to w ersji m e to d y rei ax () u m ie szc z a ją
cej w kolejce F IF O w ie rz c h o łk i d o celo w e (z p o m in ię c ie m d u p lik a tó w ) k raw ę d z i, d la k tó ry c h
w y k o n a n o u d a n ą relak sację, i o k re so w o spraw d zającej, czy w edgeTo [] n ie w y stę p u je cykl
u je m n y (zo b acz o pis w tekście).
4.4 a Najkrótsze ścieżki 687
O p a r ty n a k o le jc e a lg o r y tm B e llm a n a -F o r d a je s t s k u te c z n ą i w y Przebiegi
d a jn ą m e to d ą ro z w ią z y w a n ia p r o b le m u w y z n a c z a n ia n a jk r ó t
szy ch śc ie ż e k , c z ę sto s to s o w a n ą w p ra k ty c e (n a w e t w te d y , k ie d y
w a g i są d o d a tn ie ) . N a r y s u n k u p o p ra w e j s tr o n ie p o k a z a n o , że
ro z w ią z a n ie d la p rz y k ła d u o 2 5 0 w ie rz c h o łk a c h m o ż n a z n a le ź ć
w 14 p rz e b ie g a c h i w y m a g a to m n ie j p o r ó w n a ń d łu g o ś c i ś c ie ż e k
n iż w a lg o r y tm ie D ijk stry .
W a g i u j e m n e N a n a s tę p n e j s tr o n ie p o k a z a n o ś la d d z ia ła n ia
Krawędzie z kolejki
a lg o r y tm u B e llm a n a -F o r d a d la d ig r a fu o w a g a c h u je m n y c h .
oznaczono na czerwono
Z a c z y n a m y o d ź ró d ła q, a n a s tę p n ie w y z n a c z a m y d rz e w o S P T
w o p is a n y p o n iż e j sp o s ó b .
° R e la k s a c ja k ra w ę d z i 0-> 2 i 0 -> 4 o ra z u m ie s z c z e n ie 2 i 4
w k o le jc e .
° R e la k sa c ja k ra w ę d z i 2-> 7 i u m ie s z c z e n ie 7 w k o le jc e , a n a
s tę p n ie re la k s a c ja k ra w ę d z i 4 -> 5 i u m ie s z c z e n ie 5 w k o
lejce. P o te m n a s tę p u je re la k s a c ja n ie w y b ie ra ln e j k ra w ę d z i
4-> 7.
° R e la k sa c ja k ra w ę d z i 7-> 3 i 5 - > l o ra z u m ie s z c z e n ie 3 i 1
w k o le jc e . P o te m m a m ie js c e re la k s a c ja n ie w y b ie r a ln y c h
k r a w ę d z i 5-> 4 i 5->7.
D R e la k s a c ja k ra w ę d z i 3-> 6 i u m ie s z c z e n ie 6 w k o le jce .
P o te m n a s tę p u je re la k s a c ja n ie w y b ie ra ln e j k ra w ę d z i l-> 3 .
D R e la k sa c ja k ra w ę d z i 6 -> 4 i u m ie s z c z e n ie 4 w k o le jce .
T a k r a w ę d ź m a w a g ę u je m n ą i d a je k ró ts z ą śc ie ż k ę d o 4,
d la te g o k ra w ę d z ie p r z y w ie rz c h o łk u 4 tr z e b a p o n o w n ie
p o d d a ć re la k s a c ji (p o r a z p ie r w s z y z ro b io n o to w p r z e
b ie g u 2 ). O d le g ło ś c i d o 5 i 1 n ie są ju ż p o p ra w n e , je d n a k
z m ie n i się to w p ó ź n ie js z y c h p rz e b ie g a c h .
° R e la k s a c ja k ra w ę d z i 4 -> 5 i u m ie s z c z e n ie 5 w k o le jc e .
P o te m n a s tę p u je re la k s a c ja k ra w ę d z i 4 -> 7 , k tó r a n a d a l je s t
n ie w y b ie r a ln a .
a R e la k sa c ja k ra w ę d z i 5 - > l i u m ie s z c z e n ie 1 w k o le jc e.
P o te m n a s tę p u je re la k sa c ja n ie w y b ie ra ln y c h k ra w ę d z i 5->4
i 5-> 7.
° R e la k sa c ja k ra w ę d z i l- > 3 , k tó r a n a d a l je s t n ie w y b ie ra ln a .
P o w o d u je to o p ró ż n ie n ie k o le jk i.
D rz e w o n a jk r ó ts z y c h śc ie ż e k d la te g o p rz y k ła d u to je d n a d łu g a
śc ie ż k a z 0 d o 1. R e la k sa c ja k ra w ę d z i z 4, 5 i 1 o d b y w a się d w u
k ro tn ie . P o n o w n e z a p o z n a n ie się z d o w o d e m t w i e r d z e n i a x
w ty m k o n te k ś c ie to d o b r y s p o s ó b n a le p s z e z ro z u m ie n ie r o z
w ią z a n ia .
Algorytm Bellmana-Forda
(250 wierzchołków)
688 R O ZD ZIA Ł 4 o Grafy
[Link]
4->5 0.35 edgeTo[] d istT o []
0
5->4 0.35
1
4->7 0 .3 7 2 0- > 2 0.26
5->7 0.28 3
7->5 0.28 4 0~>4 0.38
5 - > l 0.32 5 4->5 0.73
0-> 4 0.38 6
0->2 0.26 Źródło 7 2->7 0.60
7->3 0.39
l- > 3 0 .2 9 edgeTo[] d istT o []
2->7 0.34 0
1 5-> l 1.05
6->2 - 1 . 2 0
2 0- > 2 0.2 6
3->6 0.52 3 7->3 0.99
6->0 -1 .4 0 4 0 -> 4 0.38
6->4 -1 .2 5 5 4- > 5 0.73
6
7 2- > 7 0.60
edgeTo[] d istT o []
0
1 5->l 1 .0 5
2 0 -> 2 0.26
3 7 ->;3 0.99
4 0- >4 0.38
5 4- >5 0.73
6 3->6 1.51
7 2~>7 0.60
edgeTo[] d istT o []
0
1 5->l 1 .,05
2 0-> 2 0 ., 26
3 7- 7 0. Ju ż nie sq
4 6->4 0 .,26 wybieralne!
5 4->5 0. ,73
6 3 -> 6 1 ., 51
7 2-> 7 0 ., 60
edgeTo[] d istT o []
0
1 5->l 1.05
2 0~>2 0.26
3 7->3 0.99
4 6- > 4 0.26
5 4->5 0.61
6 3-->6 1.51
7 2-> 7 0.60
edgeTo[] distT o[
0
1 5->l 0.93
2 0->2 0.26
3 7->3 0.99
4 6->4 0.26
5 4->5 0.61
6 3->6 1.51
7 2->7 0.60
Ślad działania algorytmu Bellmana-Forda (przy wagach ujemnych)
4.4 o Najkrótsze ścieżki 689
W ykryw anie cykli ujem nych Opracowana przez nas implementacja klasy
Bel ImanFordSP wykrywa cykle ujemne, aby uniknąć pętli nieskończonej. Można
zastosować służący do wykrywania cykli kod, aby zapewnić klientom możliwość
sprawdzania i wyodrębniania cykli ujemnych. W tym celu dodajemy do interfejsu
API klasy SP (strona 656) następujące metody.
boolean h a s N e g a t i v e C y c l e ( ) Czy występuje cykl ujemny?
Ite rab le <D ire cte d Ed ge > ne gative Cycle() Zwraca cykl ujemny
( n u l i , jeśli nie ma takich cykli)
Rozwinięcie interfejsu API do wyznaczania najkrótszych ścieżek
o obsługę cykli ujemnych
Zaimplementowanie tych m etod nie jest trudne, czego dowodem jest kod pokazany
poniżej. Po wykonaniu kodu konstruktora z klasy Bel ImanFordSP wiadomo (z do
wodu t w i e r d z e n i a y ), że digraf ma dostępny ze źródła cykl ujemny wtedy i tylko
wtedy, jeśli kolejka jest niepusta po V-tym przebiegu po wszystkich krawędziach.
Ponadto podgraf z krawędziami z tablicy edgeTo [] musi obejmować cykl ujemny.
Zgodnie z tym w celu zaimplementowania m etody negativeCycle() tworzymy di
graf ważony z krawędzi z tablicy edgeTo [] i szukamy cyklu w tym digrafie. Do wy
krywania cyklu służy wersja klasy Di rectedCycl e z p o d r o z d z i a ł u 4 .2 , dostosowana
do digrafów ważonych (zobacz ć w i c z e n i e 4 .4 .1 2 ). Koszty sprawdzania zmniejszamy
w następujący sposób:
° Przez dodanie zmiennej egzemplarza
p r i v a t e v o i d fin d N e g a t iv e C y c le ()
cycle i metody prywatnej findNegati -
{
veCycle(), która ustawia zmienną cycle i n t V = e d g e T o .l e n g t h ;
na iterator po krawędziach, jeśli znalezio Edg eW eight edD igrap h s p t ;
s p t = new E d g e W e ig h t e d D ig r a p h ( V ) ;
no cykl ujemny (lub na n u li, jeżeli go nie f o r ( i n t v = 0; v < V; v++)
wykryto). i f (edgeTof v] != n u l l )
0 Przez wywoływanie m etody findNega- s p t . a d d E d g e ( e d g e T o [ v ] );
tiveC ycle() co V wywołań m etody re-
E dge W e ig h t e d C ycle F in d e r c f ;
la x ( ). c f = new E d g e W e i g h t e d C y c l e F i n d e r ( s p t ) ;
Podejście to gwarantuje, że pętla w konstruk
cycle = c f . c y c l e d ;
torze zakończy działanie. Ponadto klienty
1
mogą wywołać metodę hasNegativeCycle(),
aby ustalić, czy ze źródła dostępny jest cykl p u b l i c bo ole an h a s N e g a t i v e C y c l e ( )
ujemny, a wywołanie m etody negat i veCycl e () { return cycle != n u l 1; }
pozwala pobrać taki cykl. Dodanie możliwości p u b lic Iterable<Edge> nega tive Cy cle ()
wykrywania dowolnych cykli ujemnych w di { return cycle; }
grafie także jest prostym rozwinięciem rozwią
zania (zobacz Ć W IC Z E N IE 4 .4 .43 ). Metody do wykrywania cykli ujemnych używane
w algorytmie Bellmana-Forda
690 R O ZD ZIA Ł 4 0 Grafy
P o n iżej p o k a z a n o śla d d z ia ła n ia a lg o ry tm u B e llm a n a -F o rd a d la d ig r a fu z c y k le m u je m
n y m . D w a p ie rw s z e p rz e b ie g i są ta k ie sa m e , ja k d la g ra fu z p lik u tin y E W D n . tx t. W tr z e
c im p rz e b ie g u , p o re la k s a c ji k ra w ę d z i 7-> 3 i 5 - > l o ra z u m ie s z c z e n iu w ie rz c h o łk ó w
3 i 1 w k o le jc e , n a s tę p u je re la k s a c ja k ra w ę d z i o w a d z e u je m n e j, 5 -> 4 . W tra k c ie
tej relaksa cji w y k r y w a n y je s t c y k l u je m n y 4 -> 5 -> 4 . P o w o d u je to d o d a n ie k ra w ę d z i
5-> 4 d o d r z e w a i o d c ię c ie c y k lu o d ź r ó d ła 0 w ta b lic y edgeT o [ ] . O d te g o m o m e n tu
a lg o r y tm k r ą ż y w c y k lu i z m n ie js z a o d le g ło ś c i d o w s z y s tk ic h n a p o tk a n y c h w ie r z
c h o łk ó w . K o ń c z y się to w m o m e n c ie w y k ry c ia c y k lu , p r z y c z y m k o le jk a n ie je s t w te
d y p u s ta . C y k l z n a jd u je się w ta b lic y edgeTo [] i m o ż e z o s ta ć w y k r y ty p rz e z m e to d ę
fin d N e g a tiv e C y c le ().
tinyEWDnc..txt queue
edgeTo[] d istT o []
4->5
5->4
0.35
0. 66
\
4->7 0.37
5->7 0.28
7->5 0.28 4 0- >4 0 . 38
5->l 0.32 5 4->5 0.73
0->4 0.38
Zródto 7 2->7 0.60
0 -> 2 0.26
7->3 0.39
T3
edgeTo []
■
1—
o
l/l
M
1->3 0.29
2->7 0.34 1 5->l 1.05
6 -> 2 0.40 2 0->2 0.26
3->6 0.52 3 7->3 0.99
6->0 0.58 4 5->4 0.07 ^ Długość ścieżki
6->4 0.93 5 4- >5 0.73 0->4->5->4
6
7 2 -> 7 0.60
e d ge T o [] d ist T o []
0
1 5 ->1 1.05
2 0 -> 2 0.26
3 7->3 0.99
0- >4 0.07
5 4->5 0.42
6 3->6 1.51
7 2->7 0.44
Ślad działania algorytmu Bellmana-Forda (dla grafu z cyklem ujemnym)
4.4 Q Najkrótsze ścieżki 691
A r b i t r a ż Z a s ta n ó w m y się n a d r y n k ie m tr a n s a k c ji fin a n s o w y c h , g d z ie o d b y w a się
h a n d e l p a p ie r a m i w a rto ś c io w y m i. Jak o p rz y k ła d w y k o rz y sta m y ta b e le z k u rs a m i w a
lut, p o d o b n e d o ta b e li z p lik u [Link]. P ie rw sz y w ie rs z p lik u o b e jm u je lic z b ę w a lu t, V.
K ażd y n a s tę p n y w ie rs z d o ty c z y je d n e j w alu ty . P o d a n a je s t je j n a z w a , a d a le j k u rs y
w z g lę d e m in n y c h w a lu t. Z u w a g i n a z w ię z ło ść t u p o k a z a n o ty lk o p ię ć z s e te k w a lu t,
k tó r y m i h a n d lu je się n a w s p ó łc z e s n y c h ry n k a c h : d o la r y a m e r y k a ń s k ie (USD), e u ro
(EUR), f u n ty b ry ty js k ie (GBP), fr a n k i sz w a jc a rsk ie (CHF) i d o la r y k a n a d y js k ie (CAD). t - t a
w a rto ś ć w w ie rs z u s re p r e z e n tu je k u rs w y m ia n y — lic z b ę je d n o s te k w a lu ty o n a z w ie
z w ie rs z a t , k tó r e m o ż n a k u p ić za je d n o s tk ę w a lu
ty o n a z w ie z w ie rs z a s. Z g o d n ie z p rz y k ła d o w ą t a % more r a t e s . t x t
j
b e lą z a 1 0 0 0 d o la r ó w a m e r y k a ń s k ic h m o ż n a k u p ić
USD 1 0 .741 0..657 1..061 1..005
741 e u ro . T a b e la je s t o d p o w ie d n ik ie m p e łn e g o d i- EUR 1..349 1 0..888 1,.433 1..366
g ra fu w a żo n e g o , w k tó r y m w ie rz c h o łk i o d p o w ia GBP 1,.521 1 .125 1 1..614 1,.538
d ają w a lu to m , a k ra w ę d z ie — k u r s o m w y m ia n y . CHF 0,.942 0 .698 0..619 1 0..953
CAD 0..995 0 .732 0..650 1..049 1
K ra w ę d ź s - > t o w a d z e x o d p o w ia d a w y m ia n ie s
n a t p o k u rs ie x. Ś c ie ż k i w d ig ra fie w y z n a c z a ją w y
m ia n y w ie lo e ta p o w e . P o łą c z e n ie w c z e śn ie j w s p o m n ia n e j w y m ia n y z k ra w ę d z ią t- > u
o w a d z e y d a je śc ie ż k ę s - > t- > u , k tó r a r e p r e z e n tu je s p o s ó b w y m ia n y je d n e j je d n o s tk i
w a lu ty s n a xy je d n o s te k w a lu ty u. P rz y k ła d o w o , z a e u ro m o ż n a k u p ić 1 0 1 2 ,2 0 6 =
741 x 1,366 d o la r ó w k a n a d y js k ic h . Z a u w a ż m y , że d a je to le p s z y k u rs n iż p r z y b e z p o
ś re d n ie j w y m ia n ie d o la r ó w a m e r y k a ń s k ic h n a k a n a d y js k ie . M o ż n a o c z e k iw a ć , że xy
w e w s z y s tk ic h s y tu a c ja c h b ę d z ie ró w n e w a d z e s-> u , je d n a k ta b e le k u r s ó w w y m ia n y
s ta n o w ią s k o m p lik o w a n y sy s te m fin a n so w y , w k tó r y m n ie m o ż n a z a g w a ra n to w a ć
ta k iej s p ó jn o ś c i. D la te g o in te re s u ją c e je s t z n a le z ie n ie ta k ie j ś c ie ż k i z s d o u, d la k t ó
rej ilo c z y n w a g je s t m a k s y m a ln y . Jeszcze c ie k a w sz e są sy tu a c je , k ie d y ilo c z y n w a g
k ra w ę d z i je s t m n ie js z y n iż w a g a k ra w ę d z i z o s ta tn ie g o w ie rz c h o łk a z p o w r o te m d o
p ie rw s z e g o . W p rz y k ła d z ie z a k ła d a m y , że w a g a u -> s
w y n o s i z, a xyz > 1. W te d y c y k l s - > t- > u - > s u m o ż - ° - 741 * 1-366 4 -995 = 1.00714497
liw ia w y m ia n ę je d n e j je d n o s tk i w a lu ty s n a w ięcej
n iż je d n ą je d n o s tk ę (x y z) w a lu ty s. O z n a c z a to , że
m o ż n a o s ią g n ą ć z y sk w w y s o k o ś c i 100 (xyz - 1)
p ro c e n t, w y m ie n ia ją c s n a t n a u i z p o w r o te m n a s.
P rz y k ła d o w o , je ś li w y m ie n im y 1 0 1 2 ,2 0 6 d o la r ó w
k a n a d y js k ic h z p o w r o te m n a d o la r y a m e ry k a ń s k ie ,
o tr z y m a m y 1 0 1 2 ,2 0 6 x 0 ,9 9 5 = 1 0 0 7 ,1 4 4 9 7 d o la r ó w
a m e r y k a ń s k ic h , c o d a je z y sk 7 ,1 4 4 9 7 d o la ra . M o ż e
się w y d a w a ć , że to n ie d u ż o , je d n a k f i n n a h a n d lu ją c a
w a lu tą m o ż e o b ra c a ć m ilio n e m d o la r ó w i w y k o n y
w ać tr a n s a k c je co m in u tę , c o d a je z y sk w w y s o k o ś c i
p o n a d 7 0 0 0 d o la r ó w n a m in u tę , czy li p o n a d 4 2 0 0 0 0
d o la r ó w n a g o d z in ę ! T a s y tu a c ja to p rz y k ła d o k a z ji okazja do arbitrażu
692 R O ZD ZIA Ł 4 Grafy
Arbitraż przy wymianie walut
public c la s s Arbitrage
{
public s t a t ic void m ain(String[] args)
{
in t V = S t d l n . r e a d l n t ( ) ;
S t r in g [ ] name = new S trin g [V ] ;
EdgeWeightedDigraph G = new EdgeWeightedDigraph(V);
fo r (in t v = 0; v < V; v++)
{
name[v] = S td In .r e a d S tr in g () ;
fo r (in t w = 0; w < V; w++)
{
double rate = [Link] ;
DirectedEdge e = new DirectedEdge(v, w, -M a t h .lo g (ra te ));
[Link](e);
}
}
BellmanFordSP spt = new BellmanFordSP(G, 0);
i f ([Link]())
{
double stake = 1000.0;
fo r (DirectedEdge e : s p t.n e ga tiv eC y cle Q )
{
S td 0 u t. p r in tf ( "% 1 0 .5 f %s ", stake, name[[Link]( ) ] ) ;
stake *= M ath .exp(-e .w eigh t());
S t d O u t .p r in t f("= %10.5f % s\n ", stake, nam e[[Link]()]);
}
}
else S td O u t.p rin tln ("B ra k możliwości a r b i t r a ż u . " ) ;
T en k lie n t klasy Bel 1manFordSP w y szu k u je m o żliw o ści d o a rb itra ż u n a p o d sta w ie tab eli k u r
sów w y m ian y w alut. W ty m celu tw o rz y p e łn y g ra f re p re z e n tu ją c y tę tab elę, a n a stę p n ie k o
rzy sta z a lg o ry tm u B e llm a n a -F o rd a d o z n a le z ien ia cy k lu u je m n e g o w grafie.
% java A rb itrage < ra te [Link] t
100 0.000 00 USD = 74 1.0 0 000 EUR
741 .0 0 000 EUR = 1012. 206 00 CAD
101 2.206 00 CAD = 100 7.144 97 USD
4.4 Q Najkrótsze ścieżki
d o a rb itra ż u , c o u m o ż liw ia ło b y h a n d la r z o m o s ią g n ię c ie n ie o g r a n ic z o n y c h zysków ,
g d y b y n ie is tn ia ły c z y n n ik i s p o z a m o d e lu , ta k ie ja k o p ła ty tr a n s a k c y jn e lu b o g r a
n ic z e n ie w a rto ś c i tr a n s a k c ji. N a w e t z u w z g lę d n ie n ie m ty c h c z y n n ik ó w a r b itr a ż je s t
w p ra k ty c e b a r d z o zysk o w n y . C o p r o b le m te n m a w s p ó ln e g o z n a jk r ó ts z y m i ś c ie ż k a
m i? O d p o w ie d ź n a to p y ta n ie je s t z a s k a k u ją c o p ro s ta .
Twierdzenie Z. P ro b le m a r b itr a ż u to o d p o w ie d n ik p r o b le m u w y k ry w a n ia c y k li
u je m n y c h w d ig ra fa c h w a ż o n y c h .
Dowód. N a le ż y z a s tą p ić k a ż d ą w a g ę jej lo g a r y tm e m z o d w r ó c o n y m z n a k ie m .
P o te j z m ia n ie o b lic z e n ie w a g śc ie ż e k p rz e z p o m n o ż e n ie w a g k ra w ę d z i w p ie r w o t
nej w e rsji o d p o w ia d a d o d a n iu ic h w p rz e k s z ta łc o n y m p ro b le m ie . K a ż d y ilo c z y n
w ,...w Ł o d p o w ia d a s u m ie - l n ( w ,) - ln ( w ,) - ... - l n ( i y j . P rz e k s z ta łc o n e w a g i
k ra w ę d z i m o g ą b y ć u je m n e lu b d o d a tn ie , śc ie ż k a z v d o w u m o ż liw ia w y m ia n ę
z w a lu ty v n a w a lu tę w, a k a ż d y c y k l u je m n y o z n a c z a m o ż liw o ś ć a rb itra ż u .
W o p is a n y m p rz y k ła d z ie m o ż liw e są w sz y stk ie tr a n s a k c je , d la te g o d ig r a f je s t g ra fe m
p e łn y m , ta k w ię c k a ż d y cy k l u je m n y je s t o s ią g a ln y z d o w o ln e g o w ie rz c h o łk a . O g ó ln ie
n a g ie łd a c h n ie k tó re k ra w ę d z ie m o g ą b y ć n ie o b e c n e , d la te g o p o tr z e b n y je s t je d n o -
a rg u m e n to w y k o n s t r u k to r o p is a n y w ć w i c z e n i u 4 .4 .4 3 . N ie je s t z n a n y w y d a jn y a l
g o ry tm d o w y s z u k iw a n ia n a jle p szej o k a z ji d o a r b itr a ż u (n a jb a rd z ie j u je m n e g o c y k lu
w d ig ra fie ), p r z y c z y m s a m g r a f n ie m u s i b y ć b a r d z o d u ży , a b y p o tr z e b n a b y ła b a r d z o
d u ż a m o c o b lic z e n io w a d o ro z w ią z a n ia te g o p r o b le
-lnC .7 41 ) -lnC l. 36 6) -l n(.995)
m u . J e d n a k n a js z y b sz y a lg o r y tm d o w y s z u k iw a n ia
ja k ie jk o lw ie k m o ż liw o ś c i a r b itr a ż u je s t b a r d z o w a ż \ \ )
.2998 - .3119 + .0050 = -.0071
ny. H a n d la r z p o s ia d a ją c y ta k i a lg o r y tm p r a w d o p o
d o b n ie z d o ła w y k o rz y s ta ć w ie le m o ż liw o śc i, z a n im
d r u g i p o d w z g lę d e m s z y b k o ś c i a lg o r y tm z n a jd z ie
ja k ą k o lw ie k o k azję .
P R Z E K S Z T A Ł C E N IE Z D O W O D U T W IE R D Z E N IA Z je s t
p rz y d a tn e ta k ż e n ie z a le ż n ie o d a rb itra ż u , p o n ie w a ż
re d u k u je p r o b le m w y m ia n y w a lu t d o p r o b le m u w y
z n a c z a n ia n a jk r ó ts z y c h ście ż ek . P o n ie w a ż fu n k c ja
lo g a r y tm ic z n a je s t m o n o to n ic z n a i z m ie n ia m y z n a k
jej w y n ik u , ilo c z y n je s t m a k s y m a ln y , k ie d y s u m a
je s t m in im a ln a . W ag i k ra w ę d z i m o g ą b y ć u je m n e
lu b d o d a tn ie , a n a jk r ó ts z a śc ie ż k a z v d o w o k re ś la Cykl ujemny reprezentujący
n a jle p sz y s p o s ó b w y m ia n y w a lu ty v n a w a lu tę w. okazję do arbitrażu
694 RO ZD ZIA Ł 4 b Grafy
Perspektywa W ta b e li p o n iż e j p r z e d s ta w io n o p o d s u m o w a n ie w a ż n y c h c e c h o p i
sa n y c h w p o d r o z d z ia le a lg o r y tm ó w w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k . P ie rw s z y p o
w ó d w y b o r u je d n e g o z a lg o r y tm ó w z w ią z a n y je s t z p o d s ta w o w y m i c e c h a m i u ż y w a
n e g o d ig ra fu . C z y o b e jm u je w a g i u je m n e ? C z y m a cy k le? C z y w y s tę p u ją w n im cy k le
u je m n e ? T a k ż e in n e w ła śc iw o ś c i d ig ra fó w w a ż o n y c h m o g ą b y ć b a r d z o z ró ż n ic o w a
n e, d la te g o je ś li m o ż n a z a s to s o w a ć k ilk a a lg o ry tm ó w , w y b ó r je d n e g o z n ic h w y m a g a
p r z e p r o w a d z e n ia e k s p e ry m e n tó w .
Liczba porównań długości
ścieżek (tempo wzrostu) Dodatkowa
Algorytm Ograniczenia Główna zaleta
Typowy Najgorszy pamięć
przypadek przypadek
Dijkstry K raw ęd zie ElogV E lo g V V G w a ra n c je d la
(wersja zachłanna) o w ag ach n ajg o rszeg o
d o d a tn ic h p rz y p a d k u
Sortowanie W ażo n e E+V E+V V O p ty m a ln y
topologiczne g rafy D A G d la grafó w
acy k liczn y ch
Bellmana-Forda B ra k cykli E+V VE V 0 w ielu
(oparty na kolejce) u je m n y c h za sto so w a n ia ch
Cechy związane z wydajnością algorytmów wyznaczania najkrótszych ścieżek
U w a g i h is to r y c z n e P ro b le m y w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k in te n s y w n ie b a d a
n o o d la t 50. u b ie g łe g o w ie k u . H is to r ia a lg o r y tm u D ijk s try d o w y z n a c z a n ia n a jk r ó t
sz y c h śc ie ż e k je s t p o d o b n a d o h is to r ii a lg o r y tm u P r im a d o o b lic z a n ia d rz e w M S T
(i p o w ią z a n a z n ią ). N a z w a a lg o r y tm D ijk s tr y je s t p o w s z e c h n ie s to s o w a n a z a ró w
n o d o a b s tra k c y jn e j m e to d y tw o rz e n ia d rz e w S T P p rz e z d o d a w a n ie w ie rz c h o łk ó w
w k o le jn o ś c i ic h o d le g ło ś c i o d ź ró d ła , ja k i d o jej im p le m e n ta c ji, b ę d ą c e j o p ty m a l
n y m a lg o r y tm e m d la re p r e z e n ta c ji w p o s ta c i m a c ie rz y s ą s ie d z tw a . E.W . D ijk s tra o b a
ro z w ią z a n ia p rz e d s ta w ił w p r a c y z 1959 r o k u (w y k a z a ł te ż , że za p o m o c ą te g o s a
m e g o p o d e jś c ia m o ż n a w y z n a c z y ć d rz e w o M S T ). P o p ra w a w y d a jn o ś c i d la g ra fó w
rz a d k ic h w y n ik a z p ó ź n ie js z y c h u s p r a w n ie ń w im p le m e n ta c ja c h k o le je k p r i o r y te
to w y c h (te c h n ik i te n ie są s p e c y fic z n e d la p r o b le m u w y z n a c z a n ia n a jk r ó ts z y c h ś c ie
żek ). Z w ię k sz e n ie w y d a jn o ś c i a lg o r y tm u D ijk s tr y to je d n o z n a jw a ż n ie js z y c h z a s to
s o w a ń ty c h te c h n ik . P rz y k ła d o w o , z a p o m o c ą s t r u k tu r y d a n y c h n a z y w a n e j k o p c e m
F ibonacciego o g ra n ic z e n ie d la n a jg o rs z e g o p r z y p a d k u m o ż n a z m n ie js z y ć d o E + V
lo g V . A lg o ry tm B e llm a n a -F o r d a o k a z a ł się p r z y d a tn y w p ra k ty c e i z n a la z ł w ie le z a
4.4 □ Najkrótsze ścieżki 695
s to so w a ń , s z c z e g ó ln ie w z a k re s ie o g ó ln y c h d ig ra fó w w a ż o n y c h . C h o ć d la ty p o w y c h
z a s to s o w a ń czas w y k o n a n ia a lg o r y tm u B e llm a n a -F o r d a je s t zazw y czaj lin io w y , d la
n a jg o rsz e g o p r z y p a d k u w y n o s i V E . O p ra c o w a n ie a lg o r y tm u lin io w e g o (d la n a jg o r
szego p r z y p a d k u ) d o w y z n a c z a n ia n a jk ró ts z y c h ś c ie ż e k w g ra fa c h rz a d k ic h p o z o s ta je
k w e stią o tw a rtą . P o d s ta w o w y a lg o r y tm B e llm a n a -F o r d a z o s ta ł o p ra c o w a n y w la ta c h
50. u b ie g łe g o w ie k u p rz e z L. F o rd a i R. B e llm a n a . M im o b a r d z o d u ż e j p o p r a w y w w y
d a jn o ś c i, ja k ą z a o b s e r w o w a n o d la w ie lu in n y c h p ro b le m ó w z d z ie d z in y g rafó w , n ie
is tn ie ją n a ra z ie a lg o r y tm y o le p sz e j w y d a jn o ś c i d la n a jg o rs z e g o p r z y p a d k u d la d ig r a
fów z k ra w ę d z ia m i o w a g a c h u je m n y c h (ale b e z c y k li u je m n y c h ).
696 RO ZD ZIA Ł 4 ■ Grafy
| PYTANIA I ODPOWIEDZI
P. Po co definiować odrębne typy danych dla grafów nieskierowanych, skierowa
nych, ważonych grafów nieskierowanych i ważonych grafów skierowanych?
O. Robimy to zarówno ze względu na przejrzystość w kodzie klienta, jak i prost
szą oraz wydajniejszą implementację dla grafów bez wag. W niektórych aplikacjach
lub systemach trzeba przetwarzać grafy każdego rodzaju. Podręcznikowym zada
niem dla inżynierów oprogramowania jest zdefiniowanie typu ADT, na podstawie
którego można zdefiniować typy ADT dla grafów nieskierowanych bez wag (Graph,
p o d r o z d z i a ł 4 . 1 ), digrafów bez wag (Di graph, p o d r o z d z i a ł 4 . 2 ), nieskierowanych
grafów ważonych (EdgeWeightedGraph, p o d r o z d z i a ł 4 .3 ) lub digrafów ważonych
(EdgeWeightedDi graph, p o d r o z d z i a ł 4 .4 ).
P. Jak znaleźć najkrótsze ścieżki w nieskierowanych grafach ważonych?
O. Dla grafów o krawędziach dodatnich odpowiedni jest algorytm Dijkstry.
Należy utworzyć obiekt EdgeWeightedDi graph odpowiadający danemu obiektowi
EdgeWei ghtedGraph (w tym celu trzeba dodać dwie krawędzie skierowane — po jednej
w każdym kierunku — odpowiadające każdej krawędzi nieskierowanej), a następnie
uruchomić algorytm Dijkstry. Jeśli wagi krawędzi mogą być ujemne, dostępne są wy
dajne algorytmy, które są jednak bardziej skomplikowane od algorytmu Bellmana-
Forda.
4.4 a Najkrótsze ścieżki 697
ĆWICZENIA
4.4.1. D o d a n ie stałe j d o w a g i k a ż d e j k ra w ę d z i n ie z m ie n ia ro z w ią z a n ia p r o b le m u
w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k z je d n e g o ź r ó d ła — p r a w d a cz y fałsz?
4.4.2. U d o stę p n ij im p le m e n ta c ję m e to d y t o S t r i n g ( ) dla k la s y EdgeWeightedDigraph.
4.4.3. O p ra c u j d la g ra fó w g ę sty c h im p le m e n ta c ję k la s y EdgeWei g h ted D i g ra p h o p a r
tą n a m a c ie rz y s ą s ie d z tw a (d w u w y m ia ro w e j ta b lic y w ag ; z o b a c z ć w i c z e n i e 4 . 3 .9 ).
P o m iń k ra w ę d z ie ró w n o le g łe .
4.4.4. N a ry s u j d rz e w o S P T d la ź ró d ła 0 w d ig ra fie w a ż o n y m u z y s k a n y m p r z e z u s u
n ię c ie w ie rz c h o łk a 7 z g ra f u z p lik u tin y E W D .tx t (z o b a c z s tr o n ę 6 5 6 ). P rz e d s ta w r e
p r e z e n ta c ję d rz e w a S P T o p a r t ą n a o d n o ś n ik a c h d o ro d z ic ó w . W y k o n a j ć w ic z e n ie d la
te g o sa m e g o g ra f u z o d w r ó c o n y m i k ra w ę d z ia m i.
4 .4 .5 . Z m ie ń k ie r u n e k k ra w ę d z i 0-> 2 w p lik u tin y E W D .tx t (z o b a c z s tr o n ę 65 6 ).
N a ry su j d w a r ó ż n e d rz e w a S P T o k o r z e n iu w w ie rz c h o łk u 2 u z y s k a n e d la z m o d y f i
k o w a n e g o d ig r a fu w a ż o n e g o .
4.4.6. P rz e d s ta w ś la d p ro c e s u w y z n a c z a n ia d rz e w a S P T d la d ig r a fu z ć w i c z e n i a
4 .4.5 za p o m o c ą z a c h ła n n e j w e rsji a lg o r y tm u D ijk stry .
4.4.7. O p ra c u j w e rsję k la s y Di j k s t r a S P o b s łu g u ją c ą m e to d ę k lie n c k ą , k tó r a z w ra c a
dru g ą n a jk r ó ts z ą śc ie ż k ę z s d o t w d ig ra fie w a ż o n y m ( o r a z z w ra c a nul 1 , je ś li is tn ie je
ty lk o je d n a n a jk r ó ts z a śc ie ż k a ).
4 .4 . 8 . Ś red n ica d ig r a fu to d łu g o ś ć m a k s y m a ln e j s p o ś r ó d n a jk r ó ts z y c h ś c ie ż e k łą
c z ą c y c h p a r y w ie rz c h o łk ó w . N a p is z ld ie n ta k la s y Di j k s tra S P , k tó r y o k re ś la ś re d n ic ę
d ig r a fu ty p u EdgeWei g h ted D i g ra p h o n ie u je m n y c h w a g a c h .
4.4.9. W ta b e li p o n iż e j, o p a rte j n a d a w n e j m a p ie d ro g o w e j, z n a jd u ją się d łu g o ś c i
n a jk ró ts z y c h tr a s łą c z ą c y c h m ia s ta . Z n a jd u je się t u b łą d . P o p ra w ta b e lę . D o d a j te ż
ta b e lę o k re ś la ją c ą , ja k z n a le ź ć n a jk r ó ts z e trasy .
P ro v id e n c e W esterly N ew L ondon N o rw ic h
P ro v id e n c e - 53 54 48
W esterly 53 - 18 101
N ew L o n d o n 54 18 - 12
N o rw ic h 48 101 12 -
698 RO ZD ZIA Ł 4 □ Grafy
ĆWICZENIA (ciągdalszy)
4 .4 .1 0 . P rz y jm ijm y , że k ra w ę d z ie d ig r a fu z ć w i c z e n i a 4 .4 .4 są n ie s k ie ro w a n e , a k a ż
d a k ra w ę d ź o d p o w ia d a k ra w ę d z io m o ró w n y c h w a g a c h w o b u k ie r u n k a c h z d ig r a fu
w a ż o n e g o ze w s p o m n ia n e g o ć w ic z e n ia . W y k o n a j ć w i c z e n i e 4 .4 .6 d la u z y sk a n e g o
w te n s p o s ó b d ig r a fu w a ż o n e g o .
4 .4 .1 1 . W y k o rz y s ta j m o d e l k o s z tó w p a m ię c io w y c h z p o d r o z d z i a ł u 1 .4 d o u s ta le
n ia ilo śc i p a m ię c i p o tr z e b n e j w k la s ie EdgeWei g h ted D i g ra p h d o p rz e d s ta w ie n ia g ra fu
0 V w ie rz c h o łk a c h i E k ra w ę d z ia c h .
4 .4 .1 2 . Z a a d a p tu j k la s y Di re c te d C y c l e i T o p o io g i c a l z p o d r o z d z i a ł u 4 .2 ta k , a b y
k o rz y s ta ły z in te rfe js ó w A P I EdgeWei gh ted D i g ra p h i Di re c te d E d g e , p r z e d s ta w io n y c h
w ty m p o d ro z d z ia le . Z a im p le m e n tu j w te n s p o s ó b k la s y EdgeWei g h te d C y c le F in d e r
1 EdgeWei ghtedTopologi c a l .
4 .4 .1 3 . P rz e d s ta w ( ta k ja k w ś la d a c h w te k ś c ie ) p ro c e s w y z n a c z a n ia p rz e z a lg o r y tm
D ijk s try d rz e w a S P T d la d ig r a fu u z y s k a n e g o p rz e z u s u n ię c ie k ra w ę d z i 5->7 z p lik u
tin y E W D .tx t (z o b a c z s tr o n ę 65 6 ).
4 . 4 . 1 4 . P rz e d s ta w śc ie ż k i, k tó r e z o s ta n ą o d k r y te p rz e z d w a o p is a n e n a s tr o n ie 680
p r ó b n e ro z w ią z a n ia w p rz y k ła d o w y m g ra fie z p lik u tin y E W N d .tx t p o k a z a n y m n a
o w ej s tro n ie .
4 . 4 . 1 5 . Ja k d z ia ła a lg o r y tm B e llm a n a -F o r d a p o w y w o ła n iu m e to d y p ath T o ( v ) , je śli
n a ścieżc e z s d o v w y s tę p u je c y k l u je m n y ?
4 .4 .1 6 Z a łó ż m y , że p rz e k s z ta łc iliś m y o b ie k t EdgeWei g h te d G ra p h na o b ie k t
EdgeWei g h ted D i g ra p h , tw o rz ą c w ty m o s ta tn im d w a o b ie k ty Di re c te d E d g e (p o j e d
n y m w k a ż d y m k ie r u n k u ) d la k a ż d e g o o b ie k tu Edge z p ie rw s z e g o o b ie k tu (ja k o p is a
n o to w k o n te k ś c ie a lg o r y tm u D ijk s tr y w p y t a n i a c h i o d p o w i e d z i a c h n a s tro n ie
6 9 6 ). N a s tę p n ie s to s u je m y a lg o r y tm B e llm a n a -F o r d a . W y ja śn ij, d la c z e g o to p o d e j
ście d o p ro w a d z i d o s p e k ta k u la r n e j p o ra ż k i.
4 . 4 . 1 7 . C o się sta n ie , je ś li d o p u ś c im y m o ż liw o ś ć u m ie s z c z e n ia te g o s a m e g o w ie r z
c h o łk a w k o le jc e w ię ce j n iż ra z w je d n y m p rz e b ie g u w a lg o r y tm ie B e llm a n a -F o rd a ?
O d p o w ie d ź: cza s w y k o n a n ia a lg o r y tm u m o ż e w z ro s n ą ć d o w y k ła d n ic z e g o . O p is z n a
p rz y k ła d , ja k a lg o r y tm z a d z ia ła d la p e łn e g o d ig r a fu w a ż o n e g o , w k tó r y m w sz y stk ie
k ra w ę d z ie m a ją w a g ę - 1 .
4 .4 .1 8 , N a p isz k lie n ta k la s y CPM, k tó r y w y św ie tla w sz y stk ie ś c ie ż k i k ry ty c z n e .
4.4 n Najkrótsze ścieżki 699
4 .4 .1 9 Z n a jd ź c y k l o n a jn iż sz e j w a d z e (n a jle p s z ą o k a z ję d o a r b itr a ż u ) w p r z y k ła
d zie p r z e d s ta w io n y m w te k śc ie .
4 .4 .2 0 Z n a jd ź ta b e lę k u r s ó w w y m ia n y w a lu t w in te r n e c ie lu b w g azecie. W y k o rz y s ta j
ją d o u tw o r z e n ia ta b e li a rb itra ż u . U w a g a : u n ik a j ta b e l o p ra c o w a n y c h (w y lic z o n y c h )
n a p o d s ta w ie k ilk u w a rto ś c i — n ie d a ją o n e w y s ta rc z a ją c o p re c y z y jn y c h in f o rm a c ji
o k u rs a c h , a b y b y ły ciek a w e. D o d a tk o w e z a d a n ie : p o d b ij g ie łd ę w y m ia n y w a lu t!
4 .4 .2 1 . P rz e d s ta w (ta k ja k w ś la d a c h w te k ś c ie ) p ro c e s w y z n a c z a n ia d rz e w a S P T
p rz e z a lg o r y tm B e llm a n a -F o r d a d la d ig r a fu w a ż o n e g o z ć w i c z e n i a 4 .4 . 5 .
700 R O ZD ZIA Ł 4 □ Grafy
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
4 . 4 . 2 2 . W a g i w ie rz c h o łk ó w . P o k a ż , że p ro c e s w y z n a c z a n ia n a jk r ó ts z y c h ście ż e k
w d ig ra fie w a ż o n y m o n ie u je m n y c h w a g a c h w w ie rz c h o łk a c h (w a g a ś c ie ż k i to s u m a
w a g w ie rz c h o łk ó w ) m o ż n a p rz e p r o w a d z ić , tw o rz ą c d ig r a f w a ż o n y , w k tó r y m ty lk o
k ra w ę d z ie m a ją w ag i.
4 .4 .2 3 . N a jk ró tsze śc ie żk i z e ź r ó d ła d o ujścia. O p ra c u j in te rfe js A P I i im p le m e n ta c ję ,
ab y u m o ż liw ić w y k o rz y s ta n ie a lg o r y tm u D ijk s tr y d o ro z w ią z a n ia p r o b le m u w y z n a
c z a n ia n a jk ró ts z e j ś c ie ż k i z e ź r ó d ła d o u jścia w d ig r a fa c h w a ż o n y c h .
4 . 4 . 2 4 . N a jk ró tsze śc ie żk i z w ie lu źró d e ł. O p ra c u j in te rfe js A P I i im p le m e n ta c ję , aby
u m o ż liw ić z a s to s o w a n ie a lg o r y tm u D ijk s try d o r o z w ią z a n ia p r o b le m u w y z n a c z a n ia
n a jk r ó ts z y c h śc ie ż e k z w ie lu ź r ó d e ł d la d ig ra fó w w a ż o n y c h o d o d a tn ic h w a g a c h k r a
w ę d z i. N a p o d s ta w ie z b io r u ź r ó d e ł n a le ż y z n a le ź ć la s n a jk r ó ts z y c h śc ie ż ek , u m o ż
liw ia ją c y z a im p le m e n to w a n ie m e to d y , k tó r a z w ra c a k lie n to w i n a jk r ó ts z ą ścieżk ę
z d o w o ln e g o ź ró d ła d o k a ż d e g o w ie rz c h o łk a . W s k a z ó w k a : d o d a j d o k a ż d e g o ź ró d ła
p o m o c n ic z y w ie rz c h o łe k z k ra w ę d z ią o w a d z e z e ro lu b z a in ic ju j k o le jk ę p rio ry te to w ą
w s z y s tk im i ź r ó d ła m i i u s ta w ic h w a rto ś c i w ta b lic y di s tT o [] n a 0.
4 . 4 . 2 5 . N a jk r ó ts z a śc ie żk a m ię d z y d w o m a p o d z b io r a m i. D la d ig r a fu z k ra w ę d z ia m i
o d o d a tn ic h w a g a c h i d w ó c h o k re ś lo n y c h p o d z b io r ó w w ie rz c h o łk ó w , S i T, z n a jd ź
n a jk r ó ts z ą śc ie ż k ę z d o w o ln e g o w ie rz c h o łk a z S d o d o w o ln e g o w ie rz c h o łk a z T.
A lg o ry tm p o w in ie n d la n a jg o rs z e g o p r z y p a d k u d z ia ła ć w c z a sie p ro p o r c jo n a ln y m
d o E lo g V.
4 .4 .2 6 . N a jk ró tsze śc ie żk i z je d n e g o ź r ó d ła w g ra fa c h g ęstych . O p ra c u j w e rsję a lg o
r y t m u D ijk s try , k tó r a w y z n a c z a d rz e w o S P T n a p o d s ta w ie d a n e g o w ie rz c h o łk a w g ę
sty c h d ig r a fa c h w a ż o n y c h w c z asie p r o p o r c jo n a ln y m d o V2. Z a sto s u j re p r e z e n ta c ję
w p o s ta c i m a c ie rz y s ą s ie d z tw a (z o b a c z ć w i c z e n i a 4 .4 .3 i 4 . 3 . 2 9 ).
4 . 4 . 2 7 . N a jk r ó ts z e śc ie żk i w g ra fa c h e u k lid e so w y c h . Z a a d a p tu j in te rfe js y A P I, ab y
p rz y s p ie sz y ć d z ia ła n ie a lg o r y tm u D ijk s try w sy tu a c ji, k ie d y w ia d o m o , że w ie rz c h o łk i
są p u n k ta m i w p rz e s trz e n i.
4 .4 .2 8 . N a jd łu ż s z e śc ie żk i w g ra fa ch D A G . O p ra c u j im p le m e n ta c ję k la s y A cycl i cLP
ta k , a b y ro z w ią z y w a ła p r o b le m w y z n a c z a n ia n a jd łu ż s z y c h ś c ie ż e k w w a ż o n y c h g r a
fa c h D A G , ja k o p is a n o to w t w i e r d z e n i u t.
4 .4 .2 9 . O g ó ln a o p ty m a ln o ś ć . D o k o ń c z d o w ó d t w i e r d z e n i a w p rz e z p o k a z a n ie , że
je ś li is tn ie je śc ie ż k a s k ie ro w a n a z s d o v, a ż a d e n w ie rz c h o łe k n a śc ie ż c e z s d o v n ie
z n a jd u je się w c y k lu u je m n y m , to is tn ie je n a jk r ó ts z a ś c ie ż k a z s d o v ( w s k a z ó w k a :
z o b a c z t w i e r d z e n i e p).
4.4 n Najkrótsze ścieżki 701
4 . 4 .3 0 N a jk r ó ts z e śc ie żk i d la w szy stk ic h p a r w g ra fa ch z c y k la m i u je m n y m i. O p ra c u j
in te rfe js A P I p o d o b n y d o te g o z a im p le m e n to w a n e g o n a s tr o n ie 6 6 8 , słu ż ą c e g o d o
w y z n a c z a n ia n a jk r ó ts z y c h śc ie ż e k d la w s z y s tk ic h p a r w g ra fa c h b e z c y k li u je m n y c h .
O p ra c u j im p le m e n ta c ję o p a r tą n a w e rsji a lg o r y tm u B e llm a n a -F o r d a . A lg o r y tm m a
o k re ś la ć w a g i pi [ v ] , ta k ie że d la d o w o ln e j k ra w ę d z i v->w w a g a k ra w ę d z i p lu s ró ż n ic a
m ię d z y pi [v] a pi [w] je s t n ie u je m n a . N a s tę p n ie w y k o rz y sta j te w a g i d o z m ia n y w a g
g ra f u ta k , a b y m o ż n a b y ło w y k o rz y s ta ć a lg o r y tm D ijk s try d o z n a le z ie n ia w sz y stk ic h
n a jk r ó ts z y c h śc ie ż e k w g ra fie ze z m o d y f ik o w a n y m i w a g a m i.
4 .4 .3 1 . N a jk r ó ts z e śc ie żk i d la w szy stk ic h p a r w g ra fa ch lin io w y c h . D la lin io w e g o g r a
fu w a ż o n e g o (n ie s k ie r o w a n e g o g ra f u s p ó jn e g o , w k tó r y m p ra w ie w sz y stk ie w ie rz
c h o łk i są s to p n ia 2 ; w y ją te k to d w a p u n k ty k o ń c o w e o s to p n iu 1 ) o p ra c u j a lg o ry tm ,
k tó r y w s tę p n ie p r z e tw a r z a g r a f w c z a sie lin io w y m i w s ta ły m c z a sie z w ra c a d łu g o ś ć
n a jk ró ts z e j śc ie ż k i m ię d z y d w o m a w ie rz c h o łk a m i.
4 .4 .3 2 . H e u r y s ty k a s p r a w d z a n ia ro d zica . Z m o d y fik u j a lg o r y tm B e llm a n a -F o rd a ,
ab y o d w ie d z a ł w ie rz c h o łe k v ty lk o w ted y , je ś li je g o ro d z ic w d rz e w ie SPT, edgeTo [ v ] ,
n ie z n a jd u je się o b e c n ie w k o lejc e . C h e rk a ss k y , G o ld b e rg i R a d z ik d o n o s z ą o s k u
te c z n o ś c i tej h e u r y s ty k i w p ra k ty c e . U d o w o d n ij, że ro z w ią z a n ie p o p r a w n ie w y z n a c z a
n a jk r ó ts z e śc ie ż k i, p r z y c z y m czas w y k o n a n ia d la n a jg o rs z e g o p r z y p a d k u je s t p r o p o r
c jo n a ln y d o E V .
4 .4 .3 3 . N a jk r ó ts z e śc ie żk i w siatce. N a p o d s ta w ie m a c ie rz y N n a N d o d a tn i c h lic zb
c a łk o w ity c h w y z n a c z n a jk r ó ts z ą ście ż k ę z e le m e n tu ( 0 , 0 ) d o e le m e n tu ( N - 1 , N - 1 ),
g d z ie d łu g o ś ć ś c ie ż k i to s u m a lic z b c a łk o w ity c h n a ścieżce. P o n o w n ie w y k o n a j ć w i
c z e n ie , ale ty m r a z e m p rz y jm ij, że m o ż n a p o r u s z a ć się ty lk o w p ra w o i w d ó ł.
4 .4 .3 4 . N a jk r ó ts z a śc ie żk a m o n o to n ic z n a . D la d ig r a fu w a ż o n e g o z n a jd ź n a jk r ó ts z ą
śc ie ż k ę m o n o fo n ic z n ą z s d o k a ż d e g o in n e g o w ie rz c h o łk a . Ś c ie ż k a je s t m o n o t o n ic z
n a , je ś li w a g a k a ż d e j k ra w ę d z i n a śc ie ż c e je s t śc iśle ro s n ą c a lu b m a le ją c a . Ś c ież k a
p o w in n a b y ć p r o s ta (b e z p o w ta rz a ją c y c h się w ie rz c h o łk ó w ). W s k a z ó w k a : p r z e p r o
w a d ź re la k s a c ję k ra w ę d z i w k o le jn o ś c i ro s n ą c e j i z n a jd ź n a jle p s z ą śc ie ż k ę , a n a s tę p n ie
w y k o n a j re la k s a c ję k ra w ę d z i w p o r z ą d k u m a le ją c y m i w y z n a c z n a jle p s z ą ścież k ę.
4 . 4 . 3 5 . N a jk r ó ts z a śc ie żk a b ito n ic z n a . D la d ig r a fu z n a jd ź n a jk r ó ts z ą śc ie ż k ę b ito-
n ic z n ą z s d o k a ż d e g o in n e g o w ie rz c h o łk a (jeśli ta k a is tn ie je ). Ś c ie ż k a je s t b ito n ic z n a ,
je ż e li is tn ie je w ie rz c h o łe k p o ś r e d n i v, ta k i że k ra w ę d z ie z s d o v są śc iśle ro s n ą c e ,
a k ra w ę d z ie n a śc ie ż c e z v d o t — śc iśle m a le ją c e . Ś c ie ż k a p o w in n a b y ć p r o s ta (b e z
p o w ta rz a ją c y c h się w ie rz c h o łk ó w ).
702 RO ZD ZIA Ł 4 □ Grafy
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
4 . 4 . 3 6 . S ą sie d zi. O p ra c u j k lie n ta k la s y SP, k tó r y w y s z u k u je w sz y stk ie w ie rz c h o łk i
0 o k re ś lo n e j o d le g ło ś c i d o d d a n e g o w ie rz c h o łk a w d ig ra fie w a ż o n y m . C z a s w y k o
n a n ia m e to d y p o w in ie n b y ć p r o p o r c jo n a ln y d o w ię k sz ej z d w ó c h w a rto ś c i: ro z m ia r u
p o d g r a f u w y z n a c z o n e g o p rz e z te w ie rz c h o łk i i w ie rz c h o łk i s ą s ie d n ie a lb o V (czas
p o tr z e b n y n a z a in ic jo w a n ie s t r u k tu r d a n y c h ).
4 . 4 . 3 7 . K r a w ę d zie k ry ty c z n e . O p ra c u j a lg o r y tm d o w y s z u k iw a n ia k ra w ę d z i, k tó r y c h
u s u n ię c ie p o w o d u je m a k s y m a ln e z w ię k sz e n ie d łu g o ś c i n a jk ró ts z y c h śc ie ż e k z p e w
n e g o d a n e g o w ie rz c h o łk a d o in n e g o o k re ś lo n e g o w ie rz c h o łk a w d ig ra fie w a ż o n y m .
4 .4 .3 8 . W ra żliw o ść. O p ra c u j k lie n ta k la s y SP, k tó r y w y k o n u je a n a liz y w ra ż liw o ś c i
n a p o d s ta w ie k ra w ę d z i d ig r a fu w a ż o n e g o z u w z g lę d n ie n ie m p a r y w ie rz c h o łk ó w s
1 t . N a le ż y w y z n a c z y ć m a c ie rz V n a V w a rto ś c i lo g ic z n y c h , ta k ą że d la k a ż d e g o v
i w e le m e n t w w ie rs z u v i k o lu m n ie w m a w a rto ś ć t r u e , je ś li v->w to k ra w ę d ź w d ig ra fie
w a ż o n y m , k tó re j w a g ę m o ż n a z w ię k sz y ć b e z w y d łu ż a n ia n a jk ró ts z e j śc ie ż k i z v d o w.
W p rz e c iw n y m ra z ie e le m e n t m a w a rto ś ć f a l se .
4 . 4 . 3 9 . L e n iw a im p le m e n ta c ja a lg o r y tm u D ijk s try . O p ra c u j im p le m e n ta c ję o p is a n e j
w te k ś c ie le n iw e j w e rsji a lg o r y tm u D ijk stry .
4 .4 .4 0 . D r z e w o S P T z w ą s k im g a rd łe m . W y k a ż , że d rz e w o M S T d la g ra f u n ie s k ie -
ro w a n e g o je s t o d p o w ie d n ik ie m d rz e w a S P T z w ą s k im g a rd łe m — d la k a ż d e j p a r y
w ie rz c h o łk ó w v i w o k re ś lo n a je s t łą c z ą c a je śc ie ż k a , w k tó re j n a jd łu ż s z a k ra w ę d ź je s t
ta k k ró tk a , ja k to m o ż liw e .
4 . 4 . 4 1 . W y s z u k iw a n ie d w u k ie r u n k o w e . O p ra c u j k la s ę d o ro z w ią z y w a n ia p ro b le m u
n a jk ró ts z y c h śc ie ż e k ze ź ró d ła d o u jś c ia o p a r t ą n a k o d z ie a l g o r y t m u 4 .9 , je d n a k tu
k o le jk ę p r io r y te to w ą n a le ż y z a in ic jo w a ć z a ró w n o ź ró d łe m , ja k i u jś c ie m . R o z w ią z a n ie
to p ro w a d z i d o r o z r a s ta n ia się d rz e w a S P T o d k a ż d e g o w ie rz c h o łk a . G łó w n y m z a d a
n ie m je s t p re c y z y jn e o k re ś le n ie , co z ro b ić p r z y z e tk n ię c iu się o b u d rz e w SPT.
4 .4 .4 2 . N a jg o rs zy p r z y p a d e k (w a lg o r y tm ie D ijk s try ). O p is z ro d z in ę g ra fó w o V
w ie rz c h o łk a c h i E k ra w ę d z ia c h , d la k tó re j cz as w y k o n a n ia a lg o r y tm u D ijk s try je s t
ta k i ja k d la n a jg o rs z e g o p rz y p a d k u .
4.4 n Najkrótsze ścieżki 703
4 .4 .4 3 . W y k r y w a n ie cy kli u je m n y c h . Z a łó ż m y , że d o a l g o r y t m u 4 .1 1 d o d a n o k o n
s tru k to r , k tó r y r ó ż n i się o d p ie r w o tn e g o ty lk o ty m , że n ie p rz y jm u je d ru g ie g o a r
g u m e n tu i in ic ju je w sz y stk ie e le m e n ty ta b lic y d i s tT o [ ] w a rto ś c ią 0. W y k a ż , że je śli
k lie n t k o rz y s ta z te g o k o n s tr u k to r a , m e to d a h a s N e g a tiv e C y c le ( ) z w ra c a t r u e w te
d y i ty lk o w ted y , je ż e li g r a f m a c y k l u je m n y ( m e to d a n e g a ti v e C y c le ( ) z w ra c a te n
cykl).
O d p o w ied ź: ro z w a ż d ig r a f u tw o rz o n y n a p o d s ta w ie p ie r w o tn e g o p rz e z d o d a n ie d o
w sz y stk ic h p o z o s ta ły c h w ie rz c h o łk ó w n o w e g o ź r ó d ła z k ra w ę d z ią o w a d z e 0. P o j e d
n y m p rz e b ie g u w sz y stk ie e le m e n ty ta b lic y d i s tT o [] m a ją w a rto ś ć 0, a w y s z u k iw a n ie
cy k lu u je m n e g o o s ią g a ln e g o z d a n e g o ź ró d ła p rz e b ie g a a n a lo g ic z n ie d o s z u k a n ia c y
k lu u je m n e g o w d o w o ln y m m ie js c u p ie r w o tn e g o g ra fu .
4 .4 .4 4 . N a jg o rs zy p r z y p a d e k (w a lg o r y tm ie B e llm a n a -F o rd a ). O p is z ro d z in ę grafów ,
d la k tó r y c h a l g o r y t m 4 .1 1 d z ia ła w c z a sie p r o p o r c jo n a ln y m d o V E.
4 .4 .4 5 . S z y b k a w ersja a lg o r y tm u B e llm a n a -F o rd a . O p ra c u j a lg o r y tm , k tó r y ła m ie
lin io w o -lo g a ry tm ic z n ą b a rie rę c z a su w y k o n a n ia w p ro b le m ie w y z n a c z a n ia n a jk r ó t
szy ch ś c ie ż e k z je d n e g o ź ró d ła w o g ó ln y c h d ig r a fa c h w a ż o n y c h d la s p e c ja ln e g o p r z y
p a d k u , w k tó r y m w a g i to lic z b y c a łk o w ite o w a rto ś c i b e z w z g lę d n e j n ie w ię k sz e j n iż
p e w n a stała.
4 .4 .4 6 . A n im a c ja . N a p is z k lie n ta , k tó r y g e n e ru je d y n a m ic z n e a n im a c je d z ia ła n ia
a lg o r y tm u D ijk stry .
704 R O ZD ZIA Ł 4 □ Grafy
| EKSPERYMENTY
4.4.47. Losowe rzadkie digrafy ważone. Z m o d y fik u j ro z w ią z a n ie ć w i c z e n ia 4 .3.34
p rz e z p r z y p is a n ie k a ż d e j k ra w ę d z i lo s o w e g o k ie r u n k u .
4.4.48. Losowe euklidesowe digrafy ważone. Z m o d y fik u j rozw iązanie ć w ic z e n ia
4 .3.35 p rz e z p r z y p is a n ie k a ż d e j k ra w ę d z i lo s o w e g o k ie r u n k u .
4.4.49. Losowe digrafy ważone oparte na siatce. Z m o d y fik u j ro z w ią z a n ie ć w ic z e n ia
4 .3.36 przez przypisanie każdej krawędzi losowego kierunku.
4.4.50. Wagi ujemne I. Z m o d y fik u j g e n e r a to r y lo s o w y c h d ig ra fó w w a ż o n y c h ta k ,
ab y p rz e z z m ia n ę sk a li g e n e ro w a ły w a g i z p r z e d z ia łu o d x d o y (g d z ie x i y to w a rto ś c i
m ię d z y - l a l ) .
4.4.51. Wagi ujemne II. Z m o d y fik u j g e n e r a to r y lo s o w y c h d ig r a fó w w a ż o n y c h ta k ,
ab y g e n e ro w a ły w a g i u je m n e p rz e z o d w ró c e n ie z n a k u w o k re ś lo n y m p r o c e n c ie k r a
w ę d z i ( p o z io m te n p o d a w a n y je s t p rz e z k lie n ta ).
4.4.52. Wagi ujemne III. O p ra c u j k lie n ty k o rz y s ta ją c e z d ig r a fu w a ż o n e g o d o tw o
r z e n ia d ig ra fó w w a ż o n y c h o d u ż y m p r o c e n c ie w a g u je m n y c h , ale o n a jw y ż e j k ilk u
c y k la c h u je m n y c h . U w z g lę d n ij ja k n a jw ię k s z y p rz e d z ia ł w a r to ś c i V i E.
4.4 o Najkrótsze ścieżki 705
T esto w a n ie w szy s tk ic h a lg o r y tm ó w i b a d a n ie k a żd e g o p a r a m e tr u w k a ż d y m m o d e lu
g ra fó w je s t n ie w y k o n a ln e . D la k a żd e g o z w y m ie n io n y c h d a le j p r o b le m ó w n a p is z k lie n
ta, k tó r y ro z w ią z u je p r o b le m d la d o w o ln eg o d ig ra fu w ejściow ego. N a s tę p n ie w y b ie r z
je d e n z o p isa n ych w c ze śn ie j g e n e ra to ró w d o p r z e p r o w a d z e n ia e k s p e r y m e n tó w d la d a
nego m o d e lu grafów . W y k o r z y s ta j w ła sn ą ocen ę sy tu a c ji p r z y d o b o rz e e k s p e r y m e n tó w
(m o ż e s z o p rze ć się n a w y n ik a c h w c ze śn ie jszy c h p o m ia r ó w ). N a p is z w y ja śn ie n ie w y n i
k ó w i w n io sk i, k tó re m o ż n a z n ich w yciągnąć.
4 .4 .5 3 . P ro g n o zy . O sz a c u j z d o k ła d n o ś c ią d o 10 ra z y r o z m ia r n a jw ię k s z e g o g ra fu
s p e łn ia ją c e g o z a le ż n o ś ć E = \Q V , d la k tó re g o a lg o r y tm D ijk s tr y p o tr a f i w y z n a c z y ć
w sz y stk ie n a jk r ó ts z e ś c ie ż k i w 10 s e k u n d za p o m o c ą T w o jeg o k o m p u te r a i s y s te m u
o p e ra c y jn e g o .
4 . 4 . 5 4 . K o s z ty len iw eg o p o d e jśc ia . P rz e p r o w a d ź a n a liz y e m p iry c z n e , a b y p o ró w n a ć
w y d a jn o ś ć le n iw e j w e rsji a lg o r y tm u D ijk s try z w e rs ją z a c h ła n n ą d la ró ż n y c h m o d e li
d ig r a fó w w a ż o n y c h .
4 . 4 . 5 5 . A lg o r y tm Jo h n so n a . O p ra c u j im p le m e n ta c ję k o le jk i p rio ry te to w e j o p a r t ą n a
k o p c u z w ę z ła m i o d d z ie c ia c h . Z n a jd ź n a jle p s z ą w a rto ś ć d d la r ó ż n y c h m o d e li d i
g ra fó w w a ż o n y c h .
4 . 4 . 5 6 . M o d e l p r o b le m u a rb itra ż u . O p ra c u j m o d e l d o g e n e ro w a n ia lo s o w y c h p r o b
le m ó w a rb itra ż u . C e le m je s t g e n e ro w a n ie ta b e l ja k n a jb a rd z ie j z b liż o n y c h d o ta b e l
u ż y ty c h w ć w i c z e n i u 4 .4 . 2 0 .
4 .4 .5 7 . M o d e l sze re g o w a n ia ró w n o leg łych z a d a ń z te r m in a m i g r a n ic z n y m i. O p ra c u j
m o d e l d o g e n e ro w a n ia lo s o w y c h p r o b le m ó w s z e re g o w a n ia ró w n o le g ły c h z a d a ń
z t e r m i n a m i g ra n ic z n y m i. C e le m je s t g e n e ro w a n ie n ie try w ia ln y c h p ro b le m ó w , k tó re
p r a w d o p o d o b n ie są w y k o n a ln e .
ROZDZIAŁ 5
mli Łańcuchy znaków
5.1 Sortowanie łańcuchów z n a k ó w ...............................714
5.2 Drzewa t r i e ...................................................................742
5.3 W yszukiwanie p o d ła ń c u ch ó w ................................. 770
5.4 Wyrażenia re g u la rn e .................................................. 800
5.5 Kompresja danych........................................................822
o m u n ik u je m y się p rz e z w y m ia n ę ła ń c u c h ó w zn ak ó w . D la te g o lic z n e w a ż n e
K i z n a n e ap lik a c je są o p a rte n a p rz e tw a r z a n iu ła ń c u c h ó w zn ak ó w . W ty m r o z
d z iale o m a w ia m y k la sy c z n e a lg o ry tm y d o ro z w ią z y w a n ia p ro b le m ó w o b lic z e
n io w y c h z w y m ie n io n y c h p o n iż e j o b szaró w .
P r z e t w a r z a n ie in f o r m a c ji P rz y w y s z u k iw a n iu s tr o n W W W o b e jm u ją c y c h d a n e s ło
w o k lu c z o w e k o rz y s ta m y z a p lik a c ji d o p rz e tw a r z a n ia ła ń c u c h ó w zn a k ó w . W e w s p ó ł
c z e sn y m św iecie p ra k ty c z n ie w szy stk ie in f o rm a c je są z a p is a n e w fo r m ie s e k w e n c ji ła ń
c u c h ó w zn ak ó w , a a p lik a c je d o ic h p r z e tw a r z a n ia o d g ry w a ją n ie z w y k le w a ż n ą ro lę.
B a d a n ia n a d g e n o m e m N a u k o w c y z a jm u ją c y się b io lo g ią o b lic z e n io w ą p r a c u ją n a d
k o d e m g e n e ty c z n y m , w k tó r y m k o d D N A je s t z r e d u k o w a n y d o b a r d z o d łu g ic h ła ń c u
c h ó w s k ła d a ją c y c h się z c z te re c h z n a k ó w — A, C, T i G. W o s ta tn ic h la ta c h o p ra c o w a n o
ro z b u d o w a n e b a z y d a n y c h z k o d a m i o p is u ją c y m i r ó ż n o r o d n e ży w e o rg a n iz m y , d la
te g o p rz e tw a r z a n ie ła ń c u c h ó w z n a k ó w je s t w a ż n y m a s p e k te m w s p ó łc z e s n y c h b a d a ń
w d z ie d z in ie b io lo g ii o b lic z e n io w e j.
S y s t e m y k o m u n i k a c j i W r a m a c h p rz e s y ła n ia w ia d o m o ś c i te k s to w e j lu b w ia d o m o
ści e -m a il a lb o p o b ie r a n ia k s ią ż k i e le k tro n ic z n e j ła ń c u c h z n a k ó w je s t p rz e k a z y w a n y
z je d n e g o m ie js c a w in n e . A lg o ry tm y p r z e tw a r z a n ia ła ń c u c h ó w z n a k ó w o p ra c o w a n o
p o c z ą tk o w o w ła ś n ie n a p o tr z e b y a p lik a c ji w y k o n u ją c y c h te z a d a n ia .
S y s t e m y p r o g r a m o w a n i a P ro g r a m y to ła ń c u c h y z n a k ó w . K o m p ila to ry , in te r p r e te r y
i in n e a p lik a c je p rz e k s z ta łc a ją c e p r o g r a m y n a in s tru k c je m a s z y n o w e to n ie z w y k le
w a ż n e a p lik a c je , w k tó r y c h s to su je się z a a w a n s o w a n e te c h n ik i p r z e tw a r z a n ia ł a ń
c u c h ó w z n ak ó w . W s z y s tk ie ję z y k i p is a n e są p r z e d s ta w ia n e za p o m o c ą ła ń c u c h ó w
zn a k ó w , a n a s tę p n y m p o w o d e m ro z w ija n ia a lg o r y tm ó w p r z e tw a r z a n ia ła ń c u c h ó w
z n a k ó w b y ła te o r ia ję z y k ó w fo r m a ln y c h (je st to d z ie d z in a n a u k i o p is u ją c a z b io r y ła ń
c u c h ó w z n a k ó w ).
T a lis ta k ilk u is to tn y c h p rz y k ła d o w y c h o b s z a r ó w je s t ilu s tra c ją r ó ż n o r o d n o ś c i i z n a
c z e n ia a lg o r y tm ó w p r z e tw a r z a n ia ła ń c u c h ó w zn a k ó w .
707
708 RO ZD ZIA Ł 5 a Łań cuch y znaków
O to p la n te g o ro z d z ia łu . N a jp ie r w o m a w ia m y p o d s ta w o w e c e c h y ła ń c u c h ó w zn ak ó w ,
a d a lej, w p o d r o z d z i a ł a c h 5.1 i 5 . 2 , w r a c a m y d o in te rfe js ó w A P I s łu ż ą c y c h d o s o r
to w a n ia i w y s z u k iw a n ia , p r z e d s ta w io n y c h w r o z d z i a ł a c h 2 . i 3 . A lg o ry tm y , w k tó
ry c h w y k o rz y s ta n o s p e c y fic z n e c e c h y k lu c z y w p o s ta c i ła ń c u c h ó w z n a k ó w , są s z y b
sze i b a rd z ie j e la s ty c z n e o d w c z e śn ie j o p is a n y c h a lg o ry tm ó w . W p o d r o z d z i a l e 5.3
o m a w ia m y a lg o r y tm y w y s z u k iw a n ia p o d ła ń c u c h ó w , w ty m s ły n n y a lg o r y tm p r z y p i
s y w a n y K n u th o w i, M o r ris o w i i P ra tto w i. W p o d r o z d z i a l e 5 .4 w p ro w a d z a m y w y
r a ż e n ia reg u la rn e. N a ic h p o d s ta w ie o m a w ia m y p ro b le m d o p a s o w y w a n ia do w zo rca ,
k tó r y s ta n o w i u o g ó ln ie n ie p r o b le m u w y s z u k iw a n ia p o d ła ń c u c h ó w , o ra z p ro g r a m
grep — k lu c z o w e n a rz ę d z ie d o w y sz u k iw a n ia . K la sy c z n e a lg o r y tm y z te g o o b s z a r u
o p a rte są n a p o w ią z a n y c h z a g a d n ie n ia c h — ję z y k a c h fo r m a ln y c h i a u to m a ta c h s k o ń
czo n ych . p o d r o z d z i a ł 5.5 p o ś w ię c a m y w a ż n e m u z a g a d n ie n iu — k o m p re sji d a n y c h .
P ró b u je m y tu m a k s y m a ln ie z m n ie js z y ć r o z m ia r ła ń c u c h ó w zn a k ó w .
Zasady gry Z u w a g i n a p rz e jrz y sto ść i w y d a jn o ść im p le m e n ta c je są z a p is a n e za p o
m o c ą k la s y S t r i n g Javy, je d n a k celo w o k o rz y s ta m y z ja k n a jm n ie jsz e j lic z b y o p e ra c ji
z tej klasy, a b y u ła tw ić a d a p ta c ję a lg o ry tm ó w d o in n y c h ła ń c u c h o w y c h ty p ó w d a n y c h
i in n y c h ję z y k ó w p ro g ra m o w a n ia . Ł a ń c u c h y z n a k ó w p rz e d s ta w iliś m y szcz e g ó ło w o
w p o d r o z d z i a l e i . 2 , n a to m ia s t tu p o k ró tc e p rz y p o m in a m y ic h n a jw a ż n ie jsz e cechy.
Z n a k i O b ie k t S t r i n g to c ią g z n ak ó w . Z n a k i są ty p u c h a r i p rz y jm u ją j e d n ą z 2 16
m o ż liw y c h w a rto ś c i. P rz e z d z ie s ię c io le c ia p r o g r a m iś c i s to s o w a li z n a k i k o d o w a n e za
p o m o c ą 7 -b ito w e g o k o d u A S C II (ta b e lę k o n w e rs ji p r z e d s ta w io n o n a s tr o n ie 8 2 7 ) lu b
8 -b ito w e g o r o z s z e rz o n e g o k o d u A S C II, je d n a k w w ie lu w s p ó łc z e s n y c h z a s to s o w a
n ia c h p o tr z e b n e są 1 6 -b ito w e z n a k i U n ic o d e .
N i e z m i e n n o ś ć O b ie k ty S t r i ng są n ie z m ie n n e , d la te g o m o ż n a je sto so w a ć w in s tr u k
c ja c h p rz y p is a n ia o ra z ja k o a r g u m e n ty i w a rto ś c i z w ra c a n e m e t o d b e z o b a w o z m ia n ę
w a rto ś c i.
I n d e k s o w a n i e N a jc z ę śc ie j w y k o n y w a n ą o p e ra c ją je s t w y o d rę b n ia n ie określonego
z n a k u z ła ń c u c h a . S łu ż y d o te g o m e t o d a c h a r A t( ) k la s y S t r i n g Javy. O c z e k u je m y ,
że m e to d a w y k o n a z a d a n ie w s ta ły m czasie, ta k ja k b y ła ń c u c h z n a k ó w b y ł z a p is a n y
w ta b lic y c h a r [ ] . Jak o p is a n o w r o z d z i a l e i., je s t to u z a s a d n io n e o c z e k iw a n ie .
D łu g o ś ć W Javie o p e ra c ja w y z n a c z a n ia d łu g o śc i ła ń c u c h a z n a k ó w je s t z a im p le m e n
to w a n a w m e to d z ie length() k la s y String. T a k ż e tu o c z e k u je m y , że m e t o d a 1ength()
z a k o ń c z y d z ia ła n ie w s ta ły m czasie. O c z e k iw a n ie to je s t u z a s a d n io n e , c h o ć w n ie k t ó
ry c h ś r o d o w is k a c h p ro g r a m is ty c z n y c h tr z e b a z a c h o w a ć s ta ra n n o ś ć .
P o d ła ń c u c h M e to d a s u b s t r i ng () Javy to im p le m e n ta c ja o p e ra c ji w y o d rę b n ij określony
p o d ła ń c u c h . O c z e k u je m y , że m e to d a b ę d z ie d z ia ła ć w s ta ły m czasie, ta k ja k w s ta n d a r
d o w ej im p le m e n ta c ji w Javie. Jeśli n ie z n a s z m e to d y s u b s t r i ng () i p r z y c z y n , d la któ ry c h
d zia ła w s ta ły m czasie, k o n ie c zn ie p r z e c z y ta j o m ó w ie n ie sta n d a rd o w e j im p le m e n ta c ji
ła ń cu ch ó w z n a k ó w w Javie w p o d r o z d z ia le 1.2 (z o b a c z s tro n y 92 i 216 ).
R O ZD ZIA Ł 5 Q Łań cu ch y znaków 709
Z łą c z a n ie W Javie o p e ra c ja u tw ó rz
s . 1 e n gth O
n o w y ła ń cu ch z n a k ó w p r z e z d o łą czen ie
1
je d n e g o ła ń cu ch a do drugiego je s t w b u 0 1 2 3 4 5 6 7 8 9 10 11 12
d o w a n a ( o p a r ta n a o p e ra to rz e +) i d z ia ła — ► A T T A C K A T D A W N
w czasie p r o p o r c jo n a ln y m d o d łu g o ś c i
f
[Link](3) \\
w y n ik u . U n ik a m y tw o rz e n ia ła ń c u c h a
s . s u b s t r in g ( 7 , 1 1 )
z n a k ó w p rz e z d o d a w a n ie z n a k ó w je d e n
p o d ru g im , p o n ie w a ż w Javie czas w y Podstawowe operacje klasy S t r in g działające w czasie stałym
k o n a n ia ro ś n ie w te d y kw a d ra to w o . D o
w y k o n y w a n ia d o łą c z a n ia w Javie słu ż y
ld a sa S t r i ngBui 1 d e r.
T a b lic e z n a k ó w T y p S t r i n g w Javie n ie je s t ty p e m p ro s ty m . S ta n d a r d o w a im p le
m e n ta c ja o b e jm u je o p is a n e w c z e śn ie j o p e ra c je , p rz y s p ie sz a ją c e p is a n ie k o d u k lie n ta .
J e d n a k w ie le o m a w ia n y c h a lg o r y tm ó w m o ż e d z ia ła ć n a re p r e z e n ta c ji n is k o p o z io m o -
w ej, n a p r z y k ła d n a ta b lic y w a rto ś c i ty p u c h a r. W w ie lu k lie n ta c h ta k a r e p r e z e n ta c ja
je s t p re f e ro w a n a , p o n ie w a ż w y m a g a m n ie j p a m ię c i i cz a su . D la k ilk u o m a w ia n y c h
a lg o r y tm ó w k o s z t p rz e k s z ta łc a n ia z je d n e j re p r e z e n ta c ji n a d r u g ą b y łb y w y ż sz y
n iż k o s z t w y k o n a n ia a lg o r y tm u . Ja k p o k a z a n o w ta b e li p o n iż e j, ró ż n ic e w k o d z ie
d o p r z e tw a r z a n ia o b u r e p r e z e n ta c ji są n ie w ie lk ie ( m e to d a s u b s t r i ng () je s t b a rd z ie j
sk o m p lik o w a n a , d la te g o ją p o m ija m y ), ta k w ię c z a s to s o w a n ie je d n e j lu b d ru g ie j r e
p r e z e n ta c ji n ie p rz e s z k a d z a w z ro z u m ie n iu a lg o ry tm u .
p o z n a n i e w y d a j n o ś c i o m a w i a n y c h o p e r a c j i je s t k lu c z e m d o z r o z u m ie n ia w y
d a jn o ś c i k ilk u a lg o r y tm ó w p r z e tw a r z a n ia ła ń c u c h ó w z n a k ó w . N ie w s z y s tk ie ję z y
k i p r o g r a m o w a n ia u d o s tę p n ia ją im p le m e n ta c je k la s y S t r i n g o p r z e d s ta w io n y c h tu
c e c h a c h z o b s z a r u w y d a jn o ś c i. P rz y k ła d o w o , w p o w s z e c h n ie s to s o w a n y m ję z y k u
C o p e r a c ja p o b ie r a n ia p o d ła ń c u c h a i o k r e ś la n ia d łu g o ś c i ła ń c u c h a z n a k ó w z a jm u
je c z a s p r o p o r c jo n a l n y d o lic z b y z n a k ó w w ła ń c u c h u . Z a a d a p to w a n ie o p is y w a n y c h
a lg o r y tm ó w d o ta k ic h ję z y k ó w z a w sz e je s t m o ż liw e (tr z e b a z a im p le m e n to w a ć ty p
A D T p o d o b n y d o ty p u S t r i ng Javy), p r z y c z y m z w ią z a n e je s t to z r ó ż n y m i t r u d n o ś
c ia m i i m o ż liw o ś c ia m i.
Operacja Tablica znaków Klasa String Javy
Deklarowanie char[] a S trin g s
Dostęp do indeksowanych znaków a [i ] s . c h a rA t ( i )
Długość [Link] n gth s . 1 engt h ()
Konwersja a = [Link] rray(); s = new S t r i n g ( a ) ;
Dwa sposoby reprezentowania łańcuchów znaków w Javie
710 RO ZD ZIA Ł 5 a Łań cuch y znaków
W te k ś c ie k o r z y s ta m y g łó w n ie z ty p u d a n y c h S t r i ng i s w o b o d n ie s to s u je m y i n
d e k s o w a n ie o ra z o k re ś la n ie d łu g o ś c i, a c z a se m w y o d r ę b n ia n ie p o d ła ń c u c h ó w i z łą
cz a n ie . W a d e k w a tn y c h s y tu a c ja c h u d o s tę p n ia m y w w itr y n ie o d p o w ie d n i k o d o p a r ty
n a ta b lic a c h w a rto ś c i ty p u c h a r. W z a s to s o w a n ia c h , g d z ie w y d a jn o ś ć o d g ry w a k r y
ty c z n ą ro lę , p o d s ta w o w ą k w e stią p r z y w y b o rz e je d n e g o z d w ó c h k lie n tó w je s t cz ę sto
k o s z t d o s tę p u d o z n a k u (w ty p o w y c h im p le m e n ta c ja c h Jav y in s tr u k c ja a [ i ] d z ia ła
z n a c z n ie sz y b c iej n iż s . c h a rA t ( i )).
A lfa b e ty W n ie k tó r y c h a p lik a c ja c h u ż y w a n e są ła ń c u c h y z n a k ó w o p a r te n a o g r a
n ic z o n y m a lfa b e c ie . W ta k ic h s y tu a c ja c h c z ę sto w a r to z a s to s o w a ć k la s ę Al p h a b e t. Jej
in te rfe js A P I p r z e d s ta w io n o p o n iż e j.
p u b l i c c l a s s A lp ha be t
A l p h a b e t ( S t r i n g s) Tworzy nowy alfabet ze znaków z s
char t o C h a r ( i n t index ) Przekształca indeks na odpowiedni znak alfabetu
i n t t o I n d e x ( c h a r c) Przekształca c na indeks z przedziału od 0 do R - l
bool ean c o n t a i n s ( c h a r c) Czy c występuje w alfabecie?
int R() Zwraca podstawę (liczbę znaków w alfabecie)
i nt IgRO Zwraca liczbę bitów potrzebnych do zapisania indeksu
i n t [] t o I n d i c e s ( S t r i n g s) Przekształca s na liczbę całkowitą o podstawie R
Przekształca liczbę całkowitą o podstawie R
Strin g toC h ars(in t[] indices)
na łańcuch znaków oparty na alfabecie
Interfejs API klasy Alphabet
T e n in te rfe js A P I je s t o p a r ty n a k o n s tr u k to r z e , k tó r y p rz y jm u je a r g u m e n t w p o s ta c i
P -z n a k o w e g o ła ń c u c h a z n a k ó w o k re ś la ją c e g o a lfa b e t, o ra z n a m e to d a c h to C h a r ( )
i t o I n d e x ( ) , p rz e k s z ta łc a ją c y c h (w s ta ły m cz a sie ) d a n e m ię d z y z n a k a m i a w a r to ś
c ia m i ty p u i n t z p r z e d z ia łu o d 0 d o R - l . In te rfe js o b e jm u je te ż m e to d ę c o n t a i n s ( ) ,
s łu ż ą c ą d o s p ra w d z a n ia , c z y d a n y z n a k z n a jd u je się w a lfa b e c ie , o ra z m e to d y R()
i 1 gR () d o w y s z u k iw a n ia lic z b y z n a k ó w w a lfa b e c ie i lic z b y b itó w p o tr z e b n y c h d o
ic h r e p r e z e n to w a n ia . D o s tę p n e są te ż m e to d y t o I n d i c e s Q i to C h a r s ( ) d o p r z e
k s z ta łc a n ia m ię d z y ła ń c u c h a m i z n a k ó w a lfa b e tu a ta b lic a m i w a rto ś c i ty p u i n t. D la
w y g o d y w ta b e li w g ó rn e j c z ę śc i n a s tę p n e j s tr o n y p rz e d s ta w ia m y te ż w b u d o w a n e
alfab ety , z k tó r y c h m o ż n a k o rz y s ta ć za p o m o c ą k o d u w ro d z a ju [Link].
Z a im p le m e n to w a n ie k la s y A lp h a b e t to p ro s te z a d a n ie (z o b a c z ć w i c z e n i e 5 . 1 . 1 2 ).
N a s tr o n ie 711 p r z e d s ta w io n o p rz y k ła d o w e g o k lie n ta tej klasy.
T a b lic e i n d e k s o w a n e z n a k a m i J e d n ą z n a jw a ż n ie js z y c h p rz y c z y n s to s o w a n ia k la s y
Al p h a b e t je s t to , że w y d a jn o ś ć w ie lu a lg o r y tm ó w m o ż n a z w ię k sz y ć p rz e z z a s to s o w a
n ie ta b lic in d e k s o w a n y c h z n a k a m i. W y m a g a to p o w ią z a n ia z k a ż d y m z n a k ie m in f o r
m a c ji, k tó r e m o ż n a p o b r a ć za p o m o c ą je d n e g o d o s tę p u d o ta b lic y . D la ty p u S t r i ng
R O ZD ZIA Ł 5 D Łań cu ch y znaków 711
Nazwa R() lgR() Znaki
BINARY 2 1 01
DNA 4 2 ACTG
OCTAL 8 3 01234567
DECIMAL 10 4 0123456789
HEXADECIMAL 16 4 0 123456 7 8 9 A B C D E F
PROTEIN 20 5 A C D E F G H IK L M N P Q R S T V W Y
LOWERCASE 26 5 a b c d e fg h ijk lm n o p q rstu v w x y z
UPPERCASE 26 5 A B C D E F G H IJK L M N O P Q R S T U V W X Y Z
A B C D E F G H IJK L M N O P Q R S T U V W X Y Z
BASE64 64 6
a b cd efg h ijld m n o p q rstu v w x y zO 1 2 3 456789+ /
A SC II 128 7 Znaki ASCII
EXTENDED_ASCII 256 8 Znaki z rozszerzonego zestawu ASCII
UNIC0DE16 65536 16 Znaki Unicode
Standardowe alfabety
p u b l i c c l a s s Count
{
p u b l i c s t a t i c v o i d main ( S t r i ng[ ] args)
{
A lp h a b e t a l p h a = new A 1 p h a b e t ( a r g s [0 ] ) ;
in t R = alpha.R ();
int[] count = new i nt [ R ] ;
S trin g s = Std ln .re a d A l1();
int N = s .le n g t h ();
f o r ( i n t i = 0; i < N; i+ + )
% more a b r a . t x t
if (alp h [Link] n tain s([Link] arA t(i)))
ABRACADABRA!
count [ a l p h a . t o l n d e x ( s . c h a r A t ( i ) ) ] + + ;
% j a v a Count ABCDR < a b r a . t x t
f o r ( i n t c = 0; c < R; c++)
A 5
S td O ut.p rintln(alpha .toCha r(c)
B 2
+ " " + count [ c ] ) ;
C 1
D 1
R 2
Typowy klient klasy Alphabet
712 R O ZD ZIA Ł 5 0 Łań cuch y znaków
Javy tr z e b a u ż y ć ta b lic y o r o z m ia r z e 65 536. P rz y k o r z y s ta n iu z k la s y Al p h a b e t p o
tr z e b n a je s t ta b lic a z je d n y m e le m e n te m n a k a ż d y z n a k a lfa b e tu . N ie k tó re z o m a w ia
n y c h a lg o r y tm ó w g e n e ru ją w ie lk ie lic z b y ta k ic h ta b lic . W te d y p a m ię ć p o tr z e b n a n a
ta b lic e o ro z m ia r z e 65 5 3 6 m o ż e b y ć z b y t d u ż a . R o z w a ż m y n a p rz y k ła d k la s ę C ount
p o k a z a n ą w d o ln e j c z ę śc i p o p rz e d n ie j stro n y . K o d p o b ie r a ła ń c u c h z n a k ó w z w ie rs z a
p o le c e ń i w y św ie tla ta b e lę z lic z b ą w y s tą p ie ń z n a k ó w p o d a n y c h w s ta n d a r d o w y m
w e jśc iu . T a b lic a c o u n t [ ] , p rz e c h o w u ją c a lic z b y w y s tą p ie ń w k la s ie C ount, to p r z y
k ła d o w a ta b lic a in d e k s o w a n a z n a k a m i. T ak ie o b lic z e n ia m o g ą w y d a w a ć się b e z s e n
so w n e , w p ra k ty c e s ta n o w ią je d n a k p o d s ta w ę r o d z in y s z y b k ic h m e t o d s o r to w a n ia ,
o m ó w io n y c h w p o d r o z d z i a l e 5 . 1 .
L ic z b y Ja k w id a ć w k ilk u s ta n d a rd o w y c h w e rs ja c h k la s y Al p h a b e t, lic z b y c z ę sto są
re p r e z e n to w a n e ja k o ła ń c u c h y z n a k ó w . M e to d a to I n d i c e s () p rz e k s z ta łc a d o w o ln y
o b ie k t S t r i ng o p a r ty n a d a n y m o b ie k c ie Al p h a b e t n a lic z b ę o p o d s ta w ie R r e p r e z e n
to w a n ą ja k o ta b lic a i n t [] z w a r to ś c ia m i z p r z e d z ia łu o d 0 d o R - 1. W n ie k tó r y c h sy
tu a c ja c h w y k o n a n ie tej k o n w e rs ji p o z w a la u tw o rz y ć z w ię z ły k o d , p o n ie w a ż d o w o ln ą
c y frę m o ż n a w y k o rz y s ta ć ja k o in d e k s ta b lic y in d e k s o w a n e j z n a k a m i. P rz y k ła d o w o ,
je ś li w ia d o m o , że d a n e w e jśc io w e o b e jm u ją ty lk o z n a k i z d a n e g o a lfa b e tu , m o ż n a
z a stą p ić p ę tlę w e w n ę tr z n ą w C ount k r ó ts z y m k o d e m :
i n t [] a = a lp h a . t o l n d i c e s ( s ) ;
for (in t i = 0; i < N; i++)
c o u n t[a[i]]++;
W ty m k o n te k ś c ie R to p o d s ta w a s y s te m u lic z b o w e g o . K ilk a o m a w ia n y c h a lg o r y tm ó w
c z ę sto n a z y w a n y c h je s t m e to d a m i p o z y c y jn y m i, p o n ie w a ż d z ia ła ją c y fra p o cy frze.
% more p i . t x t
3141592653
5897932384
6264338327
9502884197
... [100 000 c y f r l i c z b y p i]
% j a v a Count 012 345678 9 < p i . t x t
0 9999
1 10137
2 9908
3 10026
4 9971
5 10026
6 10028
7 10025
8 9978
9 9902
RO ZD ZIA Ł 5 b Łań cu ch y znaków 713
m i m o z a l e t s to s o w a n ia w a lg o r y tm a c h p rz e tw a r z a n ia ła ń c u c h ó w z n a k ó w ty p u d a
n y c h w ro d z a ju k la s y Al phabet (z w ła sz c z a d la m a ły c h a lfa b e tó w ), w k sią ż c e n ie r o z
w ija m y w ła s n y c h o p a r ty c h n a o g ó ln e j k la s ie Al phabet im p le m e n ta c ji d la ła ń c u c h ó w
z n ak ó w . W y n ik a to z n a s tę p u ją c y c h p rz y c z y n :
■ W w ię k sz o ś c i k lie n tó w u ż y w a n y je s t ty p S t r i ng.
■ K o n w e rs ja n a in d e k s y i z n ic h c z ę sto z n a jd u je się w p ę tli w e w n ę trz n e j o ra z
z n a c z n ie s p o w a ln ia d z ia ła n ie k o d u .
■ K o d je s t b a rd z ie j sk o m p lik o w a n y , a ty m s a m y m i tr u d n ie js z y d o z ro z u m ie n ia .
D la te g o u ż y w a m y ty p u S t r i ng, w k o d z ie k o r z y s ta m y ze sta łe j R = 256 i p o d a je m y
R ja k o p a r a m e t r w a n a liz a c h . W o d p o w ie d n ic h m ie js c a c h o m a w ia m y w y d a jn o ś ć
o g ó ln y c h a lfa b e tó w . P e łn e im p le m e n ta c je o p a r t e n a k la s ie Al phabet z n a jd u ją się
w w itr y n ie .
w w i e l u z a s t o s o w a n i a c h s o r t o w a n i a k lu c z e w y z n a c z a ją c e p o r z ą d e k są ł a ń c u
c h a m i z n a k ó w . W ty m p o d r o z d z ia le o m a w ia m y m e to d y , w k tó r y c h w y k o rz y s ta n o
s p e c y fic z n e c e c h y ła ń c u c h ó w z n a k ó w d o o p ra c o w a n ia te c h n i k s o r to w a n ia k lu c z y
w tej p o s ta c i. T e c h n ik i te są w y d a jn ie js z e o d m e to d s o r to w a n ia d o o g ó ln e g o u ż y tk u ,
o p is a n y c h w r o z d z i a l e 2 .
R o z w a ż a m y t u d w a z a s a d n ic z o o d m ie n n e p o d e jś c ia d o s o r to w a n ia ła ń c u c h ó w
z n ak ó w . O b a to u z n a n e sp o so b y , o d d z ie s ię c io le c i p r z y d a tn e p ro g r a m is to m .
P ie rw s z e p o d e jś c ie p o le g a n a s p r a w d z a n iu z n a k ó w w k lu c z a c h w k o le jn o ś c i o d
p ra w e j d o lew ej. T ego ro d z a ju m e to d y n a z y w a n e są s o r to w a n ie m ła ń c u c h ó w z n ak ó w ,
p o c z ą w s z y o d n a jm n ie j z n a c z ą c e j cyfry. U ż y c ie p o ję c ia cy fra z a m ia s t z n a k w y n ik a
ze s to s o w a n ia tej sa m e j p o d s ta w o w e j m e to d y d o lic z b r ó ż n e g o ro d z a ju . Jeśli ła ń c u c h
z n a k ó w p o tr a k tu je m y ja k lic z b ę o p o d s ta w ie 2 5 6 , s p r a w d z a n ie z n a k ó w o d p ra w e j
d o lew ej o d p o w ia d a s p r a w d z a n iu n a jp ie rw n a jm n ie j z n a c z ą c y c h cyfr. T o p o d e jś c ie
je s t m e to d ą s to s o w a n ą z w y b o r u w a p lik a c ja c h s o r tu ją c y c h ła ń c u c h y z n a k ó w , je ś li
w sz y stk ie k lu c z e m a ją tę s a m ą d łu g o ś ć .
D ru g ie p o d e jś c ie o p a r te je s t n a s p r a w d z a n iu z n a k ó w w k lu c z a c h w k o le jn o ś c i o d
lew ej d o p ra w e j. N a jp ie r w a n a liz o w a n e są tu n a jb a rd z ie j z n a c z ą c e z n a k i. T eg o r o
d z a ju m e to d y n a z y w a n e są s o r to w a n ie m ła ń c u c h ó w z n a k ó w , p o c z ą w s z y o d n a jb a r
d z ie j z n a c z ą c e j cyfry. W p o d r o z d z ia le o m a w ia m y d w ie m e to d y te g o ro d z a ju . Są o n e
a tra k c y jn e , p o n ie w a ż n ie w y m a g a ją s p r a w d z a n ia w s z y s tk ic h z n a k ó w w e jśc io w y c h .
T e c h n ik i te p rz y p o m in a ją s o r to w a n ie szy b k ie , p o n ie w a ż d z ie lą s o r to w a n ą ta b lic ę n a
n ie z a le ż n e fra g m e n ty , co p o z w a la re k u r e n c y jn ie z a k o ń c z y ć s o r to w a n ie p rz e z z a s to
s o w a n ie tej sa m e j m e to d y d o p o d ta b lic . R ó ż n ic a p o le g a n a ty m , że tu p r z y p o d z ia
le u w z g lę d n ia n y je s t ty lk o p ie r w s z y z n a k k lu c z a s o r to w a n ia , n a to m ia s t p o r ó w n a n ia
w s o r to w a n iu s z y b k im d o ty c z ą c a łe g o k lu c z a . P ie rw s z a z o p is y w a n y c h m e t o d d z ieli
d a n e w e d łu g w a rto ś c i k a ż d e g o z n a k u . D r u g a d z ie li d a n e n a tr z y c z ę śc i — z k lu c z a m i
s o r to w a n ia , w k tó r y c h p ie r w s z y z n a k je s t m n ie js z y o d p ie rw s z e g o z n a k u k lu c z a o s io
w ego, ró w n y m u lu b w ię k sz y o d n ie g o .
P rz y a n a liz o w a n iu s o r to w a n ia ła ń c u c h ó w z n a k ó w w a ż n a je s t lic z b a z n a k ó w w a l
fa b e c ie . C h o ć k o n c e n tr u je m y się n a ła ń c u c h a c h z n a k ó w z r o z s z e rz o n e g o z e s ta w u
A S C II (R = 2 5 6 ), ro z w a ż a m y ta k ż e ła ń c u c h y z n a k ó w z d u ż o m n ie js z y c h a lfa b e tó w
( n a p rz y k ła d se k w e n c je w g e n o m ie ) i z n a c z n ie w ię k sz y c h z b io ró w z n a k ó w (ta k ic h ja k
o b e jm u ją c y 6 5 5 3 6 z n a k ó w z e sta w U n ic o d e , k tó r y je s t m ię d z y n a r o d o w y m s t a n d a r
d e m k o d o w a n ia ję z y k ó w n a tu r a ln y c h ) .
714
5.1 h Sortowanie łańcuchów znaków 715
Sortowanie przez zliczanie W ram ach ro z Dane wejściowe Posortowane dane
g rz e w k i o m a w ia m y p r o s tą m e to d ę s o r to w a n ia , s k u Nazwisko Grupa Według grup
Anderson 2 H arri s 1
te c z n ą , k ie d y k lu c z a m i są m a łe lic z b y c a łk o w ite .
Brown 3 Marti n 1
M e to d a ta , s o r to w a n ie p r z e z z lic z a n ie , je s t p r z y d a t n a
D a vi s 3 Moore 1
s a m a w so b ie , a ta k ż e ja k o p o d s ta w a d w ó c h z tr z e c h Garci a 4 Anderson 2
t e c h n i k s o r to w a n ia ła ń c u c h ó w z n a k ó w , k tó r e o m a w ia Harri s 1 M artin e z 2
m y w p o d r o z d z ia le . Ta ck s o n 3 Mi 11 e r 2
R o zw ażm y n a stę p u ją c y p ro b le m z o b sz a ru p rz e tw a rz a Jo h n s o n 4 Robi nson 2
Jo n e s 3 Whi te 2
n ia d an y ch . M o ż e p rz e d n im sta n ą ć n au czy ciel w y staw ia
M arti n 1 Brown 3
ją c y o c e n y u c z n io m p o d z ie lo n y m n a g ru p y — 1, 2, 3 itd. M a r t i nez 2 D a vi s 3
P rz y p e w n y c h o k azja ch trz e b a u p o rz ą d k o w a ć k lasę w e d łu g M ille r 2 Jackson 3
g ru p . P o n iew aż n u m e r y g ru p to m a łe liczb y całkow ite, Moore 1 Jones 3
m o ż n a zasto so w ać so rto w a n ie p rz e z zliczanie. Z ak ład am y , Robi nson 2 T a y lo r 3
Smi th 4 Wi 11 i ams 3
że in fo rm a c je są p rz e c h o w y w a n e w ta b lic y a [] z e le m e n ta
T a ylo r 3 Garci a 4
m i o b e jm u ją c y m i n az w isk o i n u m e r grupy. N u m e ry g ru p
Thomas 4 Jo h n s o n 4
to liczb y całk o w ite o d Thompson 4 S m it h 4
f o r (i = 0; i < N; i + + ) 0 d o R -l, a in s tru k c ja Whi te 2 Thomas 4
count [a [i ] .key () + 1]++; w i 1 1 iams 3 Thompson 4
a [i ] . key () z w ra c a n u
W i1 son 4 W i 1 son 4
m e r g ru p y o k reślo n e g o
c o u n t []
u c z n ia . M e to d a sk ła d a Klucze to małe
Zawsze 0 12 3 4
się z c z te re c h kroków . liczby całkowite
^ 0 0 0 0 0 0
Anderson 2 0 0 0 1 0 0 O p is u je m y k o le jn o k a ż Typowe dane przy sortowaniu przez zliczanie
Brown 3 0 0 0 1 1 0 d y z n ich .
D a vi s 3 0 0 0 1 2 0
Garci a 4 0 0 0 1 2 1 Z l i c z a n i e w y s tą p i e ń P ie rw s z y k r o k p o le g a n a u s t a
H arri s 1 0 0 1 1 2 1 le n iu lic z b y w y s tą p ie ń k a ż d e j w a rto ś c i k lu c z a . S łu ż y
Jackson 3 0 0 1 1 3 1 d o te g o ta b lic a count [] w a r to ś c i ty p u int. D la k a ż
Jo h n s o n 4 0 0 1 1
3 2 d e g o e le m e n tu u ż y w a m y k lu c z a d o u z y s k a n ia d o
Jones 3 0 0 1 1
4 2
s tę p u d o w a rto ś c i z ta b lic y count [] i z w ię k sz e n ia
M arti n 1 0 0 2 1
4 2
M a r t i nez 2 0 0 2 2
4 2 jej. Jeśli w a rto ś ć k lu c z a to r , z w ię k s z a m y w a rto ś ć
M ille r 2 0 0 2 3 4 2 count [ r + 1 ]. D la c z e g o +1? S ta n ie się to z r o z u m ia
Moore 1 0 0 3 3 4 2 łe w n a s tę p n y m k ro k u . W p rz y k ła d z ie w id o c z n y m
Robi nson 2 0 0 3 4 4 2 p o lew ej s tr o n ie n a jp ie r w z w ię k s z a m y w a rto ś ć c o
Smi th 4 0 0 3 4 4 3
unt [3 ], p o n ie w a ż Anderson n a le ż y d o g r u p y 2, p o
T a y lo r 3 0 0 3 4 5 3
0 te m d w u k r o tn ie z w ię k sz a m y w a rto ś ć count [4 ] , p o
Thomas 4 0 3 4 5 4
Thompson 4 0 0 3 4 5 5 n ie w a ż Brown i D av is są w g r u p ie 3 itd . Z a u w a ż m y ,
whi te 2 0 0 3 5 5 5 że count [0] z a w sz e m a w a rto ś ć 0 , a count [1] w ty m
W illia m s 3 0 0 3 5 6 5 p rz y k ła d z ie to ta k ż e 0 (ż a d e n u c z e ń n ie n a le ż y d o
W i 1 son 4 0 0 3 5 .6 6
g r u p y 0 ).
Liczba trójek x
Zliczanie wystąpień
716 R O ZD ZIA Ł 5 a Łań cuch y znaków
P rzelcształcanie liczb w y stą p ie ń na indelcsy N a s tę p for ( i n t r = 0; r < R; r++ )
n ie u ż y w a m y ta b lic y c o u n t [] , a b y d la k a żd ej w a rto ś c i c o u n t [ r+ 1 ] += c o u n t [ r ] ;
k lu c z a u sta lić p o z y c ję in d e k s u , o d k tó re g o w p o s o r
to w a n y c h d a n y c h w y stę p u ją e le m e n ty o ty m k luczu . count[]
W p rz y k ła d z ie p o ja w ia ją się tr z y e le m e n ty o k lu c z u 1
i p ięć e le m e n tó w o k lu c z u 2 , d la te g o e le m e n ty o k lu
c z u 3 z a jm u ją w p o so rto w a n e j ta b lic y p o z y c je o d 8 .
O g ó ln ie w c elu o trz y m a n ia in d e k s u p o c z ą tk o w e g o e le
m e n tó w o k lu c z u o d a n e j w a rto ś c i n a le ż y z su m o w a ć
liczb ę w y stą p ie ń m n ie js z y c h w a rto śc i. D la k ażd ej w a r
14 20
to śc i k lu c z a r s u m a liczb w y stą p ie ń d la w a rto ś c i k lu
Liczba kluczy mniejszych niż 3
czy m n ie js z y c h n iż r +1 je s t ró w n a su m ie liczb w y stą (początkowy indeks trójek
p ie ń w a rto ś c i k lu c z y m n ie jsz y c h n iż r p lu s c o u n t [ r ] . w danych wyjściowych)
D lateg o m o ż n a ła tw o p rz e jść o d lew ej d o pra w ej w celu Przekształcanie liczby wystąpień
p rz e k s z ta łc e n ia ta b lic y c o u n t [] n a ta b lic ę in d e k s ó w d o na indeksy początkowe
w y k o rz y sta n ia p rz y s o rto w a n iu d a n y c h .
R o z d z ie la n ie d a n ych P o p rz e k s z ta łc e n iu ta b lic y c o u n t [] n a ta b lic ę in d e k s ó w w y
k o n u je m y s o r to w a n ie , p rz e n o s z ą c e le m e n ty d o ta b lic y p o m o c n ic z e j a u x [ ] . K a ż d y
e le m e n t n a le ż y p rz e n ie ś ć d o ta b lic y aux [] n a p o z y c ję o k re ś lo n ą p rz e z w a rto ś ć ta b lic y
co u n t [] o d p o w ia d a ją c ą k lu
for (int i = 0 ; i <N; i++) c z o w i e le m e n tu , a n a s t ę p
aux [coun t [a [ i ] .key ( ) ] + + ] a [i] ;
n ie z w ię k sz y ć tę w a rto ś ć ,
aby u tr z y m a ć n a s tę p u ją c y
count[]
i 1 2 3 4 n ie z m ie n n ik d o ty c z ą c y t a b
0 0 3 8 14 lic y co u n t [] — d la k a ż d e j
1 0 8 14 a[o ] Anderson 2 Harri s 1 a u x [0]
w a rto ś c i k lu c z a r w a rto ś ć
Brown 3 / M arti n 1 a u x [ l]
2 0 4 9 14 a [ i]
co u n t [ r ] to p o z y c ja w t a b
3 0 4 10 14 a [2] D a vis 3 Moore 1 a u x[2 ]
lic y a u x [ ] , n a k tó re j n a le ż y
4 0 4 10 15 a [3] G arcia 4 \ Y / / Anderson 2 a u x [3]
\\\
1 2 u m ie ś c ić n a s tę p n y e le m e n t
5 1 4 10 15 a [4 ] H a rris V / M a rtin e z aux [4]
6 1 4 11 15 a [5 ] Jackson 3 Mi H e r 2 a u x [5] z k lu c z e m o w a rto ś c i r (je
\\
7 1 4 11 16 a[6] Jo h n s o n 4 Robi nson 2 a u x [6] śli ta k i is tn ie je ). P ro c e s te n
\A
8 1 4 12 16 a [7] Jo n e s 3 X. Whi te 2 a u x [7]
p r o w a d z i d o p o s o r to w a n ia
9 ? 4 12 16 a [8 ] M artin 1% \Brow n 3 a u x[8 ]
d a n y c h w je d n y m p r z e b ie
10 2 5 12 16 a[9] M a r t i n e z 2 ' I D avis 3 a u x [9]
a [10] M i l l e r 2 'J a c k s o n 3
g u p o d a n y c h , co p o k a z a n o
11 2 6 12 16 a u x [10]
12 3 6 12 16 a [ i i ] Moore 1 1/ ./ 'J o n e s 3 a u x [11] po lew e j s tro n ie . U w aga:
13 3 7 12 16 a[i 2] R o b i n s o n 2 ! M\ ^•Taylo r 3 a u x [12] w je d n y m z z a s to s o w a ń to ,
14 3 7 12 17 a [13] s m i t h 4 ywi H i ams 3 a u x [13]
że o p is a n a im p le m e n ta c ja
15 ?j 7 13 17 a [14] T a y l o r 3 \G a rcia 4 a u x [14]
7 / je s t sta b iln a , m a k lu c z o w e
16 a [is] Thomas 4 Jo h n son 4 a u x[1 5 ]
3 7 13 18 Y z n a c z e n ie . E le m e n ty o r ó w
17 3 7 13 19 a[16] Thompson 4 sm ith 4 a u x [16]
w h it e
U
2 Z Thomas 4 a u x [!7 ] n y c h k lu c z a c h są z b ie ra n e
18 3 8 13 19 a [17]
19 3 8 14 19 w illia m s
a [ i8 ] 3 Thompson 4 a u x [18] w g ru p y , je d n a k z a c h o w u ją
3 8 14 20 a [19] wi 1 son 4 wi I son 4 a u x [19] tę s a m ą w z g lę d n ą k o le jn o ść .
3 8 14 20
Rozdzielanie danych (wyróżniono rekordy o kluczu 3)
5.1 Q Sortowanie łańcuchów znaków 717
Przed
ł t t
count[0] count[l] count[2]
Sortowanie przez zliczanie (etap rozdzielania)
K o p io w a n ie z p o w r o t e m P o n ie w a ż w y k o n a liś m y s o r to w a n ie p r z e z p rz e n ie s ie n ie
e le m e n tó w d o ta b lic y p o m o c n ic z e j, o s ta tn im k r o k ie m je s t sk o p io w a n ie p o s o r to w a
n y c h w y n ik ó w z p o w r o te m d o p ie r w o tn e j tab licy .
Twierdzenie A, S o rto w a n ie p rz e z z lic z a n ie w y m a g a 8 N + 3 R + 1 d o s tę p ó w d o
ta b lic y w c e lu s ta b iln e g o p o s o r to w a n ia N e le m e n tó w , k tó r y c h k lu c z a m i są lic z b y
c a łk o w ite o d 0 d o R - 1.
Dowód. W y n ik a b e z p o ś re d n io z k o d u . Z a in ic jo w a n ie ta b lic y w y m a g a N + R + 1
d o s tę p ó w d o tablicy. P ierw sz a p ę tla zw ięk sza lic z n ik p rz y k a ż d y m z N p o w tó rz e ń (co
d aje 2N d o stę p ó w d o tab licy ). D ru g a p ę tla w y k o n u je R o p e ra c ji d o d a w a n ia {2R d o
stę p ó w d o tablicy ). T rzecia p ę tla N ra z y zw ięk sza lic z n ik i N ra z y p rz e n o s i d a n e (3N
d o s tę p ó w d o tab licy ). C z w a rta p ę tla N ra zy p rz e n o s i d a n e (2N d o stę p ó w d o tablicy).
O b ie o p e ra c je p rz e n o s z e n ia z a ch o w u ją w z g lę d n ą k o le jn o ść ró w n y c h so b ie kluczy.
S o rto w a n ie p rz e z z lic z a n ie je s t n ie z w y k le w y d a jn e
in t N = [Link];
w s y tu a c ja c h , k ie d y k lu c z a m i są m a łe lic z b y c a ł
k o w ite . P ro g r a m iś c i c z ę sto n ie p a m ię ta ją o tej m e S trin g!] aux = new S t r i n g [ N ] ;
to d z ie . Z r o z u m ie n ie jej d z ia ła n ia je s t p ie r w s z y m in t[] coun t = new i n t [ R + l ] ;
k r o k ie m n a d r o d z e d o z r o z u m ie n ia s o r to w a n ia ł a ń
// W yz naczanie l i c z b y powtórzeń,
c u c h ó w z n ak ó w . Z g o d n ie z t w i e r d z e n i e m a s o r to f o r ( i n t i = 0; i < N; i+ + )
w a n ie p rz e z z lic z a n ie n a r u s z a d o ln e o g ra n ic z e n ie N c o u n t [ a [ i ] . k e y ( ) + 1] ++ ;
// P r z e k s z t a ł c a n i e l i c z b w yst ąp ie ń
lo g N u d o w o d n io n e d la s o r to w a n ia . Jak to m o ż liw e ?
// na i n d e k s y ,
t w i e r d z e n i e i w p o d r o z d z i a l e 2 .2 d o ty c z y d o ln e - f o r ( i n t r = 0; r < R; r++)
go o g ra n ic z e n ia lic z b y p o tr z e b n y c h p o r ó w n a ń (k ie d y c o u n t [ r + 1 ] += c o u n t [ r ] ;
// R o z d z i e l a n i e rekordów,
d o s tę p d o d a n y c h o d b y w a się ty lk o za p o m o c ą m e
f o r ( i n t i = 0; i < N; i+ + )
to d y co m p a re T o ()). S o rto w a n ie p rz e z z lic z a n ie nie a u x [ c o u n t [ a [ i ] . key ( ) ] + + ] = a[i];
w y m a g a p o r ó w n a ń ( d o s tę p d o d a n y c h o d b y w a się // K opiow an ie z powrotem,
w y łą c z n ie p o p r z e z m e to d ę key ( ) ) . Jeśli R n ie r ó ż n i f o r ( i n t i = 0; i < N; i+ + )
a [ i] = au x [i];
się w ię c e j n iż o s ta ły c z y n n ik o d N , o tr z y m u je m y s o r
to w a n ie d z ia ła ją c e w c z asie lin io w y m .
Sortowanie przez zliczanie — a[].key to
liczba całkowita z przedziału [O, R)
718 RO ZD ZIA Ł 5 ■ Łań cu ch y znaków
Sortowanie łańcuchów znaków metodą LSD P ie r w s z a o m a w ia n a m e to d a
to s o r to w a n ie ła ń c u c h ó w z n a k ó w , p o c z ą w s z y o d n a jm n ie j zn a c z ą c e j c y fr y (a n g . least-
s ig n ific a n t-d ig it fi r s t — L S D ). R o z w a ż m y n a s tę p u ją c ą sy tu a c ję . Z a łó ż m y , że in ż y n ie r
o d p o w ie d z ia ln y za a u to s tr a d ę p ro je k tu je u rz ą d z e n ie ,
Dane wejściowe Posortowane dane
k tó r e z a p is u je n u m e r y re je s tr a c y jn e w s z y s tk ic h s a
4PGC938 1 IC K 7 5 0
m o c h o d ó w p rz e je ż d ż a ją c y c h z a tło c z o n ą a u to s tr a d ą
2 IY E 2 3 0 1 IC K 7 5 0
w p e w n y m o k re s ie . C e le m je s t u s ta le n ie lic z b y ró ż 3 C IO 7 2 0 10HV845
n ych p o ja z d ó w p o ru s z a ją c y c h się a u to s tra d ą . Jak 1 IC K 7 5 0 10H V845
w ia d o m o z p o d r o z d z i a ł u 2 .1 , ła tw y m s p o s o b e m 10HV845 10H V845
n a ro z w ią z a n ie te g o p r o b le m u je s t p o s o r to w a n ie 43ZY524 2 IY E 2 3 0
1 IC K 7 5 0 2RLA 629
lic z b i w y k o n a n ie p rz e b ie g u w c e lu z lic z e n ia r ó ż
3 C IO 7 2 0 2RLA 629
n y c h w a rto ś c i, ta k ja k w k la s ie Dedup ( s tr o n a 502).
10H V845 3ATW 723
N u m e r y re je s tr a c y jn e o b e jm u ją c y fr y i lite ry , d la te g o 10HV845 3 C IO 7 2 0
n a tu r a ln e je s t z a p is y w a n ie ic h ja k o ła ń c u c h ó w z n a 2RLA 629 3 C IO 7 2 0
ków . W n a jp ro s ts z e j s y tu a c ji ( n a p rz y k ła d d o ty c z ą c e j 2RLA 629 4D ZY524
3ATW 723 4PGC938
k a lifo rn ijs k ic h n u m e r ó w re je s tr a c y jn y c h p r z e d s ta ł
T
w io n y c h p o p ra w e j s tro n ie ) w sz y stk ie ła ń c u c h y m a ją
Wszystkie klucze są
tę s a m ą lic z b ę z n a k ó w . T a k a s y tu a c ja c z ę sto m a m ie j te] samej długości
sce w a p lik a c ja c h s o r tu ją c y c h d a n e . P rz y k ła d o w o , Typowe dane do sortowania
n u m e r y te le fo n ó w , n u m e r y k o n t b a n k o w y c h i a d re s y łańcuchów znaków metodą LSD
IP to ła ń c u c h y o stałej lic z b ie zn ak ó w .
S o rto w a n ie ta k i c h ła ń c u c h ó w z n a k ó w m o ż n a w y k o n a ć z a p o m o c ą s o r to w a n ia
p r z e z z lic z a n ie , co p o k a z a n o w a l g o r y t m i e 5 . 1 (k la s a LSD) i p r z e d s ta w io n y m p o d
n im p rz y k ła d z ie n a n a s tę p n e j s tr o n ie . Jeśli k a ż d y ła ń c u c h z n a k ó w m a d łu g o ś ć W ,
n a le ż y p o s o r to w a ć je W ra z y z a p o m o c ą s o r to w a n ia p r z e z z lic z a n ie , u ż y w a ją c k a ż
d ej p o z y c ji ja k o k lu c z a i p r z e c h o d z ą c o d p ra w e j d o le w e j. P o c z ą tk o w o n ie ła tw o
się p r z e k o n a ć , że m e t o d a t a tw o r z y p o s o r to w a n ą ta b lic ę . R z e c z y w iśc ie , t e c h n i k a ta
w o g ó le n ie z a d z ia ła , o ile im p le m e n ta c ja s o r to w a n ia p r z e z z lic z a n ie n ie b ę d z ie s t a
b iln a . W a r to o ty m p a m i ę ta ć i w r a c a ć d o p r z y k ła d u w c z a sie a n a liz o w a n ia d o w o d u
p o p r a w n o ś c i.
Twierdzenie B. S o rto w a n ie ła ń c u c h ó w z n a k ó w m e to d ą L SD s ta b iln ie s o r tu je
ła ń c u c h y z n a k ó w o sta łe j d łu g o ś c i.
Dowód. K lu c z o w e je s t to , a b y im p le m e n ta c ja s o r to w a n ia p rz e z z lic z a n ie b y ła
sta b iln a , o c z y m w s p o m n ia n o w t w i e r d z e n i u a . P o p o s o r to w a n iu (s ta b iln y m )
k lu c z y w e d łu g i o s ta tn ic h z n a k ó w w ia d o m o , że d w a d o w o ln e k lu c z e w y s tę p u ją
w o d p o w ie d n ie j k o le jn o ś c i w ta b lic y (w e d łu g ty lk o ty c h z n a k ó w ) a lb o z u w a g i
n a to , iż p ie r w s z y z i k o ń c o w y c h z n a k ó w je s t w n ic h r ó ż n y (w te d y p o r z ą d e k
je s t w y z n a c z o n y p rz e z s o r to w a n ie w e d łu g te g o z n a k u ), a lb o d la te g o , że p ie r w s z y
z i k o ń c o w y c h z n a k ó w je s t ta k i s a m (w te d y k o le jn o ś ć je s t z a p e w n ia n a d z ię k i s ta
b iln o ś c i). P rz e z in d u k c ję je s t to p ra w d z iw e ta k ż e d la i -1 .
5.1 Sortowanie łańcuchów znaków 719
ALGORYTM 5.1. Sortowanie łańcuchów znaków metodą LSD
p ublic c la s s LSD
{
public s t a t i c void s o r t ( S t r i n g [ ] a, in t W)
{ // Sortowanie a[] według W pierwszych znaków,
in t N = [Link];
in t R = 256;
S t r i n g [] aux = new String[N ] ;
fo r (in t d = W-l; d >= 0; d--)
{ // Sortowanie przez z lic z a n ie według d-tego znaku.
in t [ ] count = new in t[R + l] ; // Określanie lic z b y wystąpień,
f o r (in t i = 0; i < N; i++)
cou n t[a[i] .charAt(d) + 1]++;
f o r (in t r = 0; r < R; r++) // P rzekształcanie li c z b wystąpień
// na indeksy.
count [r+ l] += count [r] ;
fo r (in t i = 0; i < N; i++) // Rozdzielanie.
aux[count[a[i] .charAt(d)]++] = a [i ] ;
f o r (in t i = 0 ; i < N; i++) // Kopiowanie z powrotem,
a [i ] = aux[i] ;
}
}
}
A by p o so rto w a ć tab licę a [ ] o b e jm u ją c ą ła ń c u c h y znaków , z k tó ry c h k a ż d y sk ła d a się z d o
k ła d n ie Wznaków , n ale ż y w y k o n a ć Wo p e ra c ji so rto w a n ia p rz e z zliczan ie — p o je d n y m dla
każd ej pozycji, p rz e c h o d z ą c o d p raw ej d o lewej.
inp ut (W= 7) d= 6 4=5 4=4 d=3 d= 2 4=1 4=0 output
4PGC938 2IYE230 3 C I0720 : . 1. 230 2 R LA629 1 IC K 7 5 0 3ATW723 1 IC K 7 5 0 1 IC K 7 5 0
2 IY E 2 3 0 3CIO720 3C IO720 41ZY524 2R L A 6 2 9 1 IC K 7 5 0 3 C IO 7 2 0 1 IC K 7 5 0 1 IC K 7 5 0
3 C IO 7 2 0 1ICK750 3A TW723 2R LA 629 4PGC938 4PGC938 3 C IO 7 2 0 10HV845 10HV845
1 IC K 7 5 0 1ICK750 41ZY524 2RLA 629 2 IY E 2 3 0 10HV845 1 IC K 7 5 0 10HV845 10HV845
10HV845 3CIO720 2RLA629 3 C I0 7 2 0 1 IC K 7 5 0 10HV845 1 IC K 7 5 0 10HV845 10HV845
4D ZY524 3ATW723 2RLA629 3 C I0 7 2 0 1 IC K 7 5 0 10HV845 2 IY E 2 3 0 2 IY E 2 3 0 2 IY E 2 3 0
1 IC K 7 5 0 43ZY524 2 IY E 2 3 0 3ATW723 3CX 0720 3 C I0 7 2 0 4 JZ Y 5 2 4 2 R LA 629 2R LA 629
3 C IO 7 2 0 10HV845 4PGC938 1 IC K 7 5 0 3C I 0 7 2 0 3 C IO 7 2 0 10HV845 2RLA 629 2R LA 629
10HV845 10HV845 1 0 HV845 1 IC K 7 5 0 10 H V 8 4 5 2R LA 629 10HV845 3ATW723 3ATW723
10HV845 10HV845 10HV845 10HV845 10H V845 2R LA 629 10HV845 3 C IO 7 2 0 3 C IO 7 2 0
2R LA 629 4PG C938 10HV845 10HV845 10HV845 3ATW723 4PGC938 3 C IO 7 2 0 3 C IO 7 2 0
2R LA 629 2RLA629 1 IC K 7 5 0 10HV845 3ATW723 2 IY E 2 3 0 2R LA 629 43ZY524 41ZY524
3ATW723 2RLA629 1 IC K 7 5 0 4 PGC938 43ZY524 4.1ZY524 2R LA 629 4PGC938 4PGC938
720 R O ZD ZIA Ł 5 o Łań cuch y znaków
a w 0 A AA In n y m s p o s o b e m u ję c ia d o w o d u je s t z a s ta n o w ie n ie się n a d d a ls z y
76 7 A A 2 m i k ro k a m i. Jeśli z n a k i, k tó r y c h je sz c z e n ie s p r a w d z o n o , są w o b u
0 A AA A 3
7 A AA A 4 k lu c z a c h id e n ty c z n e , r ó ż n ic a m ię d z y k lu c z a m i m o ż e d o ty c z y ć ty lk o
AK A 2 A 5 sp r a w d z o n y c h ju ż z n a k ó w , d la te g o k lu c z e u p o rz ą d k o w a n o p ra w id ło w o
7W A 2 A 6
0 D 72 A 7 i — ze w z g lę d u n a s ta b iln o ś ć — to się n ie z m ie n i. N a to m ia s t je ż e li n ie
*6 ❖ 2 A 8 sp r a w d z o n e z n a k i ró ż n ią się o d sieb ie, z n a k i ju ż s p r a w d z o n e n ie m a ją
AW 7 3 A 9
z n a c z e n ia , a w d a ls z y c h p rz e b ie g a c h p a r a z o s ta n ie p o p r a w n ie u p o r z ą d
A A A3 A 10
0 9 A 3 AW k o w a n a n a p o d s ta w ie w a ż n ie js z y c h ró ż n ic .
79 ❖ 3 A D S o rto w a n ie p o z y c y jn e m e to d ą LSD to te c h n ik a s to s o w a n a w d a w
08 ❖ 4 A K
A 9 A 4 7 A n y c h m a s z y n a c h d o s o r to w a n ia k a r t p e rf o ro w a n y c h , o p ra c o w a n y c h n a
AK 74 72 p o c z ą tk u X X w ie k u i w y p rz e d z a ją c y c h w y k o rz y s ta n ie k o m p u te r ó w d o
0 4 A 4 7 3
A 5 A 5 74 k o m e r c y jn e g o p r z e tw a r z a n ia d a n y c h o k ilk a d z ie się c io le c i. M a s z y n y
AD 0 5 7 5 te p o tr a fiły ro z d z ie la ć k a r ty p e rf o ro w a n e m ię d z y 10 k o s z y k ó w w e d łu g
V 3 A 5 76
A 2 7 5 77 w z o rc a d z iu r e k w w y b ra n y c h k o lu m n a c h . Jeśli w o k re ś lo n y m z b io rz e
A10 76 78 k o lu m n k a r t ta lii z a p is a n e b y ły n u m e r y , o p e r a to r m ó g ł p o s o r to w a ć
A 9 A 6 79
k a r ty p rz e z p r z e tw o r z e n ie ic h w m a s z y n ie n a p o d s ta w ie c y fr y p ie r w
7 7 A 6 710
A 4 06 7 W szej o d p ra w e j i p ó ź n ie js z e p rz e tw o r z e n ie w y jśc io w e j ta lii w e d łu g n a
7 4 77 7 D
stę p n e j o d p ra w e j c y fr y i ta k d a le j — d o m o m e n tu d o ta r c ia d o p ie r w
A 10 A 7 7 K
AA A 7 ♦ A szej cyfry. F iz y c z n e u k ła d a n ie k a r t to s ta b iln y p ro c e s , o d p o w ia d a ją c y
❖ 5 ♦ 7 ♦ 2 s o r to w a n iu p rz e z z lic z a n ie . T a w e rsja s o r to w a n ia p o z y c y jn e g o m e to d ą
A3 0 8 0 3
78 78 0 4 L S D n ie ty lk o o d g ry w a ła w a ż n ą ro lę w k o m e r c y jn y c h a p lik a c ja c h aż d o
A 2 A 8 O5 la t 70. u b ie g łe g o w ie k u , ale te ż b y ła s to s o w a n a p rz e z w ie lu o s tro ż n y c h
❖ K A 8 ❖ 6
A 4 0 9 ♦ 7 p r o g r a m is tó w (i s tu d e n tó w !), k tó r z y m u s ie li p rz e c h o w y w a ć p r o g r a m y
A 7 79 ❖ 8 n a k a r ta c h p e rf o ro w a n y c h (p o je d n y m w ie rs z u n a k a rtę ) i z a p isy w a li
7 D A 9 ♦ 9
c ią g i lic z b w lu lk u o s ta tn ic h k o lu m n a c h ta lii z p r o g r a m e m , a b y m ó c
♦ W A 9 ♦ 10
A 6 A10 ❖ W m e c h a n ic z n ie p rz y w ró c ić k o le jn o ś ć k a r t p o ic h p r z y p a d k o w y m p o
A 3 ♦ 10 ❖ D
m ie s z a n iu . M e to d a t a je s t te ż e le g a n c k im s p o s o b e m s o r to w a n ia k a r t
A 7 A 10 ♦ K
A 8 710 A A d o gry. N a le ż y ro z ło ż y ć je n a 13 s to s ó w (p o je d n y m n a k a ż d ą w a rto ś ć ),
A 10 AW A 2 w y b ie ra ć sto s y p o k o le i i ro z k ła d a ć k a r ty n a c z te ry n o w e s to s y ( p o j e d
❖ 3 7 W A 3
710 AW A 4 n y m n a k a ż d y k o lo r ). T e n s ta b iln y p ro c e s r o z d a w a n ia p o w o d u je , że
O1 ♦ W A 5 k a r ty w r a m a c h k a ż d e g o k o lo r u są u p o rz ą d k o w a n e , ta k w ię c w y b ra n ie
A D 0 D A 6
72 AD A 7 s to s ó w w k o le jn o ś c i w y z n a c z a n e j p rz e z k o lo r y p o w o d u je u tw o rz e n ie
0 2 7 D A 8 p o s o r to w a n e j ta lii.
A 5 A D A 9
7 K W w ie lu z a s to s o w a n ia c h z w ią z a n y c h z s o r to w a n ie m ła ń c u c h ó w z n a
AK A10
7 5 AK A W k ó w k lu c z e n ie m a ją ta k ie j sa m e j d łu g o ś c i (d o ty c z y to n a w e t n u m e r ó w
06 ♦ K AD
re je s tr a c y jn y c h w n ie k tó r y c h s ta n a c h ). M o ż n a d o s to s o w a ć s o r to w a n ie
A8 7 K AK
ła ń c u c h ó w z n a k ó w m e t o d ą LSD, a b y d z ia ła ło ta k ż e w t a l a c h w a r u n
Sortowanie talii kart przez
k a c h . T o z a d a n ie p o z o s ta w ia m y je d n a k ja k o ć w ic z e n ie , p o n ie w a ż d a lej
sortowanie łańcuchów
znaków metodą LSD o m a w ia m y d w ie in n e m e to d y , z a p ro je k to w a n e s p e c ja ln ie p o d k ą te m
k lu c z y o z m ie n n e j d łu g o ś c i.
5.1 □ Sortowanie łańcuchów znaków 721
Z p e rs p e k ty w y te o re ty c z n e j s o r to w a n ie ła ń c u c h ó w z n a k ó w m e t o d ą L SD m a z n a
c z e n ie , p o n ie w a ż je s t te c h n ik ą s o r to w a n ia d z ia ła ją c ą w ty p o w y c h w a r u n k a c h w c z a
sie lin io w y m . N ie z a le ż n ie o d w a rto ś c i N m e t o d a w y k o n u je W p rz e b ie g ó w p o d a n y c h .
U jm ijm y to k o n k r e tn ie .
Twierdzenie B (ciąg dalszy). S o rto w a n ie ła ń c u c h ó w z n a k ó w m e t o d ą LSD w y
m a g a ~ 7 W N + 3 W R d o s tę p ó w d o ta b lic y i d o d a tk o w e j p a m ię c i w ilo ś c i p r o p o r
c jo n a ln e j d o N + R w c e lu p o s o r to w a n ia N e le m e n tó w , k tó r y c h k lu c z e to W -z n a
k o w e ła ń c u c h y z n a k ó w o p a r te n a R -z n a k o w y m alfa b ec ie.
Dowód. M e to d a o b e jm u je W p rz e b ie g ó w s o r to w a n ia p r z e z z lic z a n ie , a ta b li
cę a u x [] tr z e b a z a in ic jo w a ć ty lk o ra z. Ł ą c z n e w a r to ś c i w y n ik a ją b e z p o ś r e d n io
Z k o d u i T W IE R D Z E N IA A.
W ty p o w y c h z a s to s o w a n ia c h R je s t z n a c z n ie m n ie js z e n iż N , d la te g o z t w i e r d z e n i a
b w y n ik a , że łą c z n y czas w y k o n a n ia je s t p r o p o r c jo n a ln y d o W N . W e jś c io w a ta b lic a N
ła ń c u c h ó w zn a k ó w , z k tó r y c h k a ż d y m a W z n a k ó w , s k ła d a się w s u m ie z W N z n a k ó w ,
ta k w ię c czas w y k o n a n ia s o r to w a n ia ła ń c u c h ó w z n a k ó w m e t o d ą L SD r o ś n ie lin io w o
w z g lę d e m w ie lk o ś c i d a n y c h w e jśc io w y c h .
722 RO ZD ZIA Ł 5 □ Łań cuch y znaków
Sortowanie łańcuchów znaków metodą MSD W im p le m e n
aw AK AA ta c ji m e to d y s o r to w a n ia ła ń c u c h ó w z n a k ó w d o o g ó ln e g o u ż y tk u , s to s o
<96 AW A2 w a n e j w te d y , k ie d y ła ń c u c h y z n a k ó w n ie m a ją tej sa m e j d łu g o ś c i, z n a k i
0 A A9 A3
p rz e tw a r z a m y w k o le jn o ś c i o d lew ej d o p ra w e j. W ia d o m o , że ła ń c u c h y
¥A A5 A4
AK A2 A5 z n a k ó w r o z p o c z y n a ją c e się o d a p o w in n y w y s tę p o w a ć p r z e d ła ń c u c h a m i
¥W AA A6 z n a k ó w ro z p o c z y n a ją c y m i się lite rą b itd . N a tu r a ln y m s p o s o b e m n a z a
♦D A3 A7
A6 A4 A8 im p le m e n to w a n ie te g o ro z w ią z a n ia je s t r e k u r e n c y jn a m e to d a , n a z y w a
AW A6 A9 n a s o r to w a n ie m ła ń c u c h ó w z n a k ó w , p o c z ą w s z y o d n a jb a r d z ie j zn a c z ą c e j
AA A7 A10
♦9 A8 AW c y fr y (an g . m o st-sig n ific a n t-d ig it-first — M S D ). S to s u je m y s o r to w a n ie
<99 A10 AD p rz e z z lic z a n ie d o p o s o r to w a n ia ła ń c u c h ó w z n a k ó w w e d łu g p ie rw s z e g o
♦8 AD AK
A9 ¥6 ¥A z n a k u , a n a s tę p n ie (r e k u re n c y jn ie ) s o r tu je m y p o d ta b lic e o d p o w ia d a ją
AK ¥A ¥2 c e k a ż d e m u z n a k o w i (z w y łą c z e n ie m p ie rw s z e g o z n a k u , o k tó r y m w ia
0 4 ¥W ¥3
d o m o , że je s t ta k i s a m w e w sz y stk ic h ła ń c u c h a c h z d a n e j p o d ta b lic y ).
A5 ¥9 ¥4
AD ¥ 3 ¥ 5 S o rto w a n ie ła ń c u c h ó w z n a k ó w m e to d ą M S D , p o d o b n ie ja k s o r to w a n ie
V3 ¥7 ¥6 sz y b k ie , d z ie li ta b lic ę n a p o d ta b lic e , k tó r e m o ż n a p o s o r to w a ć n ie z a le ż n ie
A2 ¥4 ¥7
A10 ¥8 ¥8 w c e lu w y k o n a n ia z a d a n ia . T u je d n a k ta b lic a je s t d z ie lo n a n a p o d ta b lic e
A9 ¥ D ¥9 d la k a ż d e j m o ż liw e j w a rto ś c i p ie rw s z e g o z n a k u z a m ia s t n a d w ie lu b tr z y
¥ 7 ¥10 ¥10
A4 ¥2 ¥W części, co m a m ie js c e w s o r to w a n iu s z y b k im .
¥4 ¥K ¥D
♦ 10 ¥5 ¥ K K o n w e n c ja w y k r y w a n ia Sortowanie według Rekurencyjne sortowanie
AA ♦A ♦ A końca ła ń c u c h a zn a kó w wartości pierwszego znaku pod tablic (z pominięciem
♦5 ♦D ♦2 w celu podziału na podtablice pierwszego znaku)
A3 ♦9 ♦3 W so rto w a n iu ła ń c u c h ó w
¥8 ♦8 ♦4 z n a k ó w m e to d ą M S D trz e b a
A2 ♦4 ♦ 5
♦K ♦ 10 ♦6 zw ró cić szczeg ó h ią uw ag ę
A4 ♦5 ♦7 n a d o jśc ie d o k o ń c a ła ń c u
A7 ♦K ♦8 c h a znaków . A b y so rto w a n ie
¥ D ♦W ♦9
♦W ♦3 ♦ 10 d z iała ło p o p ra w n ie , pod-
A6 ♦7 ♦W tab lic a ła ń cu c h ó w , k tó ry ch
A3 ♦2 ♦D
A7 ♦6 ♦K z n a lu ju ż sp ra w d z o n o , m u s i
A8 AW AA w y stę p o w ać na p o c z ą tk u .
A10 A6 A2
♦3 AA A3 N ie n a leż y re k u re n c y jn ie
AK A4 2" ~
¥10 so rto w a ć teg o fr a g m e n
O7 AD A5
AD A10 A6 tu . A b y u łatw ić w y k o n a n ie
¥2 A9 A7 ty c h d w ó c h części obliczeń ,
❖2 A4 A8
sto su je m y p ry w a tn ą , d w u -
A5 A2 A9
¥ K A7 A10 a rg u m e n to w ą m e to d ę to -
¥5 A3 AW C h a r(), k tó ra p rze k sz ta łca
♦6 A5 AD
A8 A8 AK in d e k s o w a n y z n a k ła ń c u c h a
Sortowanie talii kart przez
n a in d e k s ta b lic y i z w ra c a - 1,
sortowanie łańcuchów jeśli p o d a n a p o z y c ja z n a k u
znaków metodą MSD
z n a jd u je się za k o ń c e m ła ń
cu ch a. N a stę p n ie w y sta rcz y
Sortow anie łańcuchów znaków metodą M S D
d o d a ć 1 d o k ażd ej zw racan ej
5.1 ■ Sortowanie łańcuchów znaków 723
w arto ści, ab y u zy sk ać n ie u je m n ą w a rto ść ty p u i n t, k tó rą m o ż n a z asto so w ać ja k o in d e k s
ta b lic y c o u n t []. To ro z w ią za n ie p o w o d u je , że n a k ażd ej p o zy cji ła ń c u c h a je s t R+l m o ż
liw y ch w a rto ś c i znaków . 0 o z n a c z a koniec łańcu ch a , 1 re p re - Dane wejściowe PoSortowane dane
z e n tu je p ie rw sz y z n a k alfab etu , 2 — d ru g i z n a k itd. P o n iew a ż s he a re
w so rto w a n iu p rz e z zliczan ie i b e z teg o p o trz e b n a je s t je d - sel 1s by
n a d o d a tk o w a p o zy cja , sto su je m y k o d i n t c o u n t [] = new seash el 1s seash el 1s
i n t [ R + l ] ; d o tw o rz e n ia ta b lic y z lic z b a m i w y stą p ie ń (i u sta - by seash el 1s
. . . . , . TT , , , . th e seash o re
w ia m y w szystkie jej w a rto śc i n a 0). Uwaga: w n ie k tó ry c h ję- s e a sh o re s e lls
z y k ach (n a p rz y k ła d w C i C + + ) d o stę p n a je s t w b u d o w a n a th e s e lls
m e to d a o z n a c z a n ia k o ń c a ła ń c u c h a znaków , d la teg o w ty ch s h e lls Klucze sh e
języ k ach p rz e d s ta w io n y tu k o d w y m a g a d o sto so w a n ia . sh e < * o różnej sh e
s e lls / długości s h e l l s
a re / s u r e ly
p o t y c h p r z y g o t o w a n i a c h z a im p le m e n to w a n ie s o rto w a - su r e i y th e
n ia ła ń c u c h ó w z n a k ó w m e to d ą M S D ( a l g o r y t m 5 . 2 ) w y - s e a s h e l! s th e
m a g a b a rd z o n ie w ie lk iej ilo śc i n o w e g o k o d u . N a le ż y d o d a ć Typowe dane nadające się do sortowania
te s t d o p rz e łą c z a n ia p ro g r a m u n a s o rto w a n ie p rz e z w sta w ia - łańcuchów znaków metodą m sd
n ie d la k ró tk ic h p o d ta b lic (słu ż y d o te g o s p e c ja ln a , o p is a n a d alej w e rsja s o rto w a n ia
p rz e z w sta w ia n ie ). T rz e b a te ż d o d a ć p ę tlę d o s o r to w a n ia p rz e z z lic z a n ie w celu z g ła
s z a n ia re k u re n c y jn y c h w y w o ła ń . Jak p o d s u m o w a n o to w ta b e li w d o ln e j części stro n y ,
w a rto ś c i w ta b lic y c o u n t [] (p o w y k o rz y sta n iu ich d o zlic z a n ia w y stą p ie ń , p rz e k s z ta łc e
n ia liczb n a in d e k s y i ro z d z ie le n ia d a n y c h ) z a p e w n ia ją in fo rm a c je p o tr z e b n e d o re k u -
re n c y jn e g o p o s o r to w a n ia p o d ta b lic o d p o w ia d a ją c y c h w a rto ś c i k a ż d e g o z n a k u .
O k r e ś lo n y a lf a b e t K o sz t s o r to w a n ia ła ń c u c h ó w z n a k ó w m e to d ą M S D w d u ż y m
s to p n iu z a le ż y o d lic z b y z n a k ó w w a lfab e cie . M e to d ę s o r to w a n ia m o ż n a ła tw o z m o
d y fik o w a ć , a b y p rz y jm o w a ła ja k o a r g u m e n t o b ie k t Al p h a b e t, c o p o z w a la p o p ra w ić
w y d a jn o ś ć w k lie n ta c h k o rz y s ta ją c y c h z ła ń c u c h ó w z n a k ó w p o c h o d z ą c y c h ze s t o s u n
k o w o k r ó tk ic h alfa b e tó w . P o tr z e b n e są n a s tę p u ją c e z m ia n y :
■ z a p is a n ie a lf a b e tu w z m ie n n e j e g z e m p la rz a a l pha w k o n s tr u k to r z e ;
■ u s ta w ie n ie w k o n s tr u k to r z e R n a a l p h a . R ( ) ;
■ z a s tą p ie n ie w m e to d z ie c h a r A t( ) w y w o ła n ia s . c h a r A t ( d ) in s tr u k c ją a l p h a .
t o ! n d e x ( s . c h a r A t ( d ) ).
Po zakończeniu Wartość counttr] wynosi:
etapu
dla d-tego znaku r= 0 r= 1 r m ię d z y w a R - 1 r= R r= R+ 1
Zliczanie Liczba łańcuchów Liczba łańcuchów znaków,
0 ( nieużywane )
wystąpień znaków 0 długości d których d-ty znak ma wartość r -2
Przekształcanie Indeks początkowy
Indeks początkowy dla łańcuchów znaków,
liczb wystąpień podtablicy dla łańcuchów Nieużywane
których d-ty znak ma wartość r-1
na indeksy znaków 0 długości d
Indeks początkowy podtablicy łańcuchów znaków, ... .
1 ., , j \ , . ,, Nieużywane
których d-ty znak ma wartość r '
Rozdzielanie 1 + indeks końcowy
podtablicy łańcuchów Nieużywane
znaków 0 długości d
Interpretacja wartości w tablicy count[] w czasie sortowania łańcuchów znaków metodą MSD
724 R O ZD ZIA Ł 5 Łań cu ch y znaków
ALGORYTM 5.2. Sortowanie łańcuchów znaków metodą MSD
public c la s s MSD
{
private s t a t ic in t R = 256; // Podstawa.
private s t a t i c final in t M = 15; // Przełączenie dla małych podtablic.
private s t a t i c S t r in g [ ] aux; // Tablica pomocnicza dorozd zie lan ia .
private s t a t i c in t ch arA t(Strin g s, in t d)
{ i f (d < s . l e n g t h O ) return [Link] arAt(d); else return -1; }
public s t a t ic void s o r t ( S t r i n g [ ] a)
{
in t N = [Link];
aux = new S t r i n g [ N ] ;
s o rt (a , 0, N-l, 0);
private s t a t i c void s o r t ( S t r i n g [ ] a, in t lo, in t hi, in t d)
{ // Sortowanie od a [1o] do a [ h i], począwszy od d-tego znaku.
i f (hi <= lo + M)
( In s e r t i o n . s o r t ( a , lo, h i, d ) ; return; }
in t [] count = new i nt[R+2] ; // Z lic z a n ie wystąpień,
fo r (in t i = lo; i <= hi; i++)
c o u n t [c h a r A t ( a [i], d) + 2]++;
fo r (in t r = 0; r < R+l; r++) // Przekształcanie lic z b y wystąpień
// na indeksy.
count [r+ l] += count [r] ;
f o r (in t i = lo; i <= hi ; i++) // Rozdzielanie.
a u x [c o u n t[c h a rA t(a [i], d) + 1]++] = a [ i] ;
fo r (in t i = lo; i <= hi; i++) // Kopiowanie z powrotem,
a [ i ] = aux[i - lo] ;
// Rekurencyjne sortowanie dla znaków okażdej wartości,
fo r (in t r = 0; r < R; r++)
so rt(a , lo + count[r] , lo + count[r+l] - 1, d+1) ;
}
}
A by p o so rto w a ć tab licę a [] z ła ń c u c h a m i znaków , n a le ż y u p o rz ą d k o w a ć je w e d łu g p ie rw s z e
go z n ak u , sto su jąc so rto w a n ie p rz e z zliczanie, a n a stę p n ie re k u re n c y jn ie p o so rto w a ć p o d ta b
lice o d p o w iad ają ce każd ej w a rto śc i p ierw szeg o zn ak u .
5.1 □ Sortowanie łańcuchów znaków 725
W przykładach stosujemy łańcuchy znaków składające się z małych liter. Można też
łatwo rozwinąć sortowanie łańcuchów znaków m etodą LSD o obsługę małych liter,
jednak zwykle ma to znacznie mniejszy wpływ na wydajność niż w sortowaniu m e
todą MSD.
k o d a l g o r y t m u 5.2 jest zwodniczo prosty i ukrywa dość skomplikowane obliczenia.
Z pewnością warto przeanalizować wysokopoziomowy ślad działania, przedstawiony
w dolnej części strony, i ślad wywołań rekurencyjnych, pokazany na następnej stro
nie. Upewnisz się w ten sposób, że rozumiesz zawiłości algorytmu. W śladzie wartość
progowa (M) dla przełączania algorytmu dla małych podtablic jest równa 0, dlatego do
końca stosowany jest podstawowy algorytm. Łańcuchy znaków w przykładzie oparte są
na alfabecie Al phabet. LOWERCASE, a R = 26. Warto pamiętać, że w typowych zastosowa
niach używany może być alfabet Al phabet. EXTENDED. A S C II, gdzie R = 256, lub alfabet
Al phabet. UNICODE, gdzie R = 65536. Dla dużych alfabetów sortowanie łańcuchów zna
ków metodą MSD jest tak proste, że aż niebezpieczne. Niewłaściwie zastosowane, może
wymagać bardzo dużej ilości czasu i pamięci. Przed szczegółowym omówieniem cech
z obszaru wydajności przedstawiamy trzy ważne zagadnienia (wszystkie poruszono już
w r o z d z i a l e 2 .), które trzeba uwzględnić w każdej aplikacji.
M ałe podtablice Podstawowy pomysł, na którym oparte jest sortowanie łańcuchów
znaków metodą MSD, jest skuteczny. W typowych zastosowaniach łańcuchy znaków
będą uporządkowane po sprawdzeniu tylko kilku znaków klucza. Ujmijmy to inaczej
— metoda szybko dzieli sortowaną tablicę na krótkie podtablice. Ma to jednak żarów-
Sortowanie przez zliczanie dla pierwszego znaku Rekurencyjne sortowanie podtablic
Rozdzielanie
Liczby Przekształcanie i kopiowanie Indeksy p o zakończeniu
wystąpień liczb na indeksy z powrotem etapu rozdzielania
s o r t ( a , 0,
she s o r t ( a , 1,
sel 1s
[ by
s e a s h e l1s so rt (a ,
by so rt(a .
sea
th e so rt(a . seashells
so rt(a ; 1 , i) eashells
8 ft so rtC a . l. i)
shore 9 i 9 l so rtC a , sells
10 j 10 j s ir t f a
sel 1s
th e 11 k 11 k so rtC a .
s h e ! 1s 12 1 she
13 m 13 m
sh e 14 n
she
14 u
se l 1s 15 o 15 o ortCa. . s hel 1 s
16 p 16 p sort! 1 . L) sh o r e
a re 17 q .17q 2. 1 . 1 )
so rt(a .
sure! y 18 r 18 r so rt(a , 2, u , i); surely
19 s 19 s so rtC a , 12. 13, i )
s e a s h e l1s so rtC a . 14. 13, the
so r t C a * 1-1, 13, he
so rtC a , 14. 13,
so rtC a . 14. 13,
so rtC a , 14,
2-j y so r t C a , 14
26 z so rtC a . 14. 13, 1)
so r t C a , 14. 13. 1)
Ślad przebiegu sortowania metodą MSD-ogólny poziom wywołania so rtC a, 0, 14, 0)
726 ROZDZIAŁ 5 ■ Łańcuchy znaków
Dane wejściowe d
she are a he are are are are are are
sel 1 s by lo^ by- by by by by by by
seasheU s she 'x s e l l s se ash e l1s sea sea sea seas sea
by s - !1 se a s h e l1s sea s e a s h e l1s s e a s h e l! s s e a s h e l 1s s e a s h e l1s seasheTp.
the s e a sh e lI s sea se a s h e l1s se ash e l1s s e a s h e l1s s e a s h e l!s s e a s h e l1s seashells
sea sea se lls sel 1 s sel 1 s sel 1 s sel 1 s sel 1s se lls '
shore shore s e a s h e l! s se lls sel 1 s sel 1 s sel 1 s sel 1 s se lls
the sh ells she she she she she she she
s h e l 1s she shore shore shore shore shore s h e l1s s h e l ls
she se lls sh e lls sh ells s h e l 1s s h e l1s s h e l 1s shore shore
se lls s u r e ly she she she she she she she
are seashel! s, s u re ly s u re ly sure! y s u re ly s u re ly s u r e ly surely
s u re ly the hi ^ the the the the the the the
se ash e l1s the the the the the the the the
W rów nych kluczach Koniec łańcucha
trzeba sprawdzić w ystępuje przed wartością
każdy zn a k jakiegokolw iek zn a k u
are are / are are are are/ are are
by h y / by by by by/ by by
sea yea sea sea sea S'ccl sea sea
s e a s h e lls s e a s h e U s s e a s h e l1s s e a s h e lls s e a sh e lls. /seashells s e a s h e l1s seashells
s e a s h e lls se a s h e l1s se ash e l!s s e a s h e l1s se a s h e l1/ s e a s h e lls s e a s h e lls seashells
sel 1 s se ll s sel 1s se ll s se lls / sel 1s sel 1 s se lls
se lls se ll s sel I s sel 1 s sel I s / se lls se lls sel I s
she she she she she / she she she
sh e !1s shells s h e l 1s s h e l 1s she she she she
she she she she s h e l1s sh e lls s h e l1s s h ells
shore shore shore shore shore shore shore shore
s u rely s u re ly s u re ly s u re ly s u re ly s u re ly s u re ly surely
the the the the the the the the
the the the the the the the the
Ś lad re k u re n c y jn y c h w y w o ła ń w s o rto w a n iu m e to d ą MSD
(b e z p rz e łą c z a n ia d la k ró tk ic h p o d ta b lic ; p o d ta b lic e o d łu g o ś c i 0 i 1 p o m in ię to )
no pozytywne, jak i negatywne skutki. Z pewnością trzeba będzie przetwarzać bardzo
dużą liczbę krótkich podtablic, dlatego lepiej się upewnić, że można to zrobić wydajnie.
Krótkie podtablice mają kluczowe znaczenie ze względu na wydajność sortowania łań
cuchów znaków metodą MSD. Przedstawiliśmy tę sytuację dla innych rekurencyjnych
technik sortowania (sortowania szybkiego i przez scalanie), jednak w tej metodzie ma
ona znacznie większe znaczenie. Załóżmy, że trzeba posortować miliony różnych łań
cuchów znaków ASCII (R - 256) bez przełączania algorytmu dla krótkich podtablic.
Każdy łańcuch znaków ostatecznie znajduje się w osobnej podtablicy, dlatego trzeba
sortować miliony podtablic o długości 1. Jednak każde takie sortowanie wymaga zai
nicjowania 258 elementów tablicy count [] wartości 0 i przekształcenia ich na indeksy.
Ten koszt staje się dominujący. Dla kodowania Unicode (R = 65536) sortowanie może
być tysiące razy wolniejsze. Wielu nieświadomych autorów klientów odkryło, że czas
wykonania wzrósł z minut do godzin po zamianie kodowania z ASCII na Unicode, co
wynika z opisanych przyczyn. Dlatego przełączanie na sortowanie przez wstawianie dla
_______
5.1 ■ Sortowanie łańcuchów znaków 1T1
p u b lic s t a t i c void s o r t ( S t r i n g [ ] a, i n t l o , in t h i, i n t d)
{ // S o r t o w a n ie od a [ l o ] do a [ h i ] , poc ząwszy od d - t e g o znaku,
f o r ( in t i = lo ; i <= h i ; 1++)
f o r ( i n t j = i ; j > 1o && l e s s ( a [ j ] , a[j-l], d); j — )
exch(a, j , j - 1 ) ;
}
p r i v a t e s t a t i c bo olean l e s s ( S t r i n g v, S t r i n g w, i n t d)
( r e t u r n v . s u b s t r i n g ( d ) . c o m p a r e T o ( w . s u b s t r i n g ( d ) ) < 0; }
S o r to w a n ie p rz e z w s ta w ia n ie d la ła ń c u c h ó w z n a k ó w , w k tó ry c h p ie rw s z y c h d
z n a k ó w j e s t id e n ty c z n y c h
król kich podtablic jest w sortowaniu metodą MSD niezbędne. Aby uniknąć kosztów
ponownego sprawdzania znaków, o których wiadomo, że są równe, można użyć wersji
sortowania przez wstawianie przedstawionej w górnej części strony. Wersja ta przyjmu
je dodatkowy argument d i działa według założenia, że pierwszych d znaków wszystkich
sortowanych łańcuchów nie różni się od siebie. Wydajność tego kodu zależy od tego,
czy operacja substring () działa w stałym czasie. Tak jak w sortowaniu szybkim i sor
towaniu przez scalanie większość korzyści z usprawnienia wynika z przełączania algo
rytmów dla małych wartości, jednak tu oszczędności są znacznie większe. Na rysunku
po prawej stronie pokazano wyniki eksperymentów, w których przełączenie się na sor
towanie przez wstawianie dla podtablic o wiel
100 % ■
kości 10 lub mniejszej skraca czas wykonania
10 -krotnie w typowych zastosowaniach.
Rów ne klucze Drugą pułapką w sortowaniu N = 100 000
metodą MSD jest to, że technika ta może oka N losowych tablic rejestracyjnych
100 prób na punkt
zać się stosunkowo wolna dla podtablic o dużej
liczbie równych kluczy. Jeśli podłańcuch wy
stępuje na tyle często, że nie da się zastosować
przełączania dla krótkich podtablic, potrzeb
ne będzie rekurencyjne wywołanie dla każ j 50% -
dego znaku we wszystkich równych kluczach.
Ponadto sortowanie przez zliczanie jest niewy-
dajnym sposobem na ustalenie, że wszystkie
znaki są równe. Nie tylko wymaga to spraw
dzenia każdego znaku i przeniesienia każde “■ 25% - '
go łańcucha, ale też zainicjowania wszystkich
liczników, przekształcenia liczb na indeksy itd. V .
10% ■
Dlatego najgorszym przypadkiem dla sortowa
nia m etodą MSD jest występowanie samych
T
równych kluczy. Ten sam problem występuje, 10 50
kiedy duża liczba kluczy ma wspólny długi Poziom przełączania
przedrostek, CO często zdarza się W praktyce. Skutki przełączania algorytmów dla krótkich podtablic
728 ROZDZIAŁ 5 ia Łańcuchy znaków
D odatkow a pam ięć Do dzielenia danych w metodzie MSD używane są dwie tablice
pomocnicze — tymczasowa tablica do rozdzielania kluczy (aux [] ) i tablica przecho
wująca liczby wystąpień przekształcane na indeksy wyznaczające podział (count [] ).
Tablica aux [] m a rozmiar N i m ożna ją utworzyć poza rekurencyjną m etodą s o rt ( ).
Tę część dodatkowej pamięci można wyeliminować kosztem stabilności (zobacz
ć w i c z e n i e 5 .1 . 1 7 ), jednak w praktycznych zastosowaniach m etody MSD zwykle nie
ma to większego znaczenia. Natom iast ważna może okazać się pamięć na tablicę
count [] (ponieważ tablicy nie można utworzyć poza rekurencyjną metodą s o rt()),
co opisano w t w i e r d z e n i u d poniżej.
M odel losowych łańcuchów zn a kó w Przy badaniu wydajności m etody MSD ko
rzystamy z modelu losowych łańcuchów znaków, w którym każdy łańcuch składa się
z niezależnych i losowych znaków, przy
Losowe Nielosowe Najgorszy
czym nie obowiązuje ograniczenie ich dłu (szybciej niż z powtórzeniami przypadek
gości. Długie równe sobie klucze w zasadzie liniowo) (prawie liniowo) (liniowo)
można pominąć, ponieważ ich występowa 1 E I O 4 0 2 a r e 1DNB377
1H L 4 9 1' by 1DNB377
nie jest niezwykle mało prawdopodobne.
1RO Z572 sea 1DNB377
Działanie m etody MSD w tym m odelu jest se a sh e its 1DNB377
2H XE734
podobne do jej funkcjonowania w modelu 2 IY E 2 3 0 s e a sh e lls 1DNB377
losowych kluczy o stałej długości, a tak 2XO R846 s e l 1s 1DNB377
że dla typowych danych w praktyce. We 3CD B573 setts 1DNB377
3 C V P 7 2 0 she 1DNB377
wszystkich trzech sytuacjach m etoda MSD
3 IG 7 3 1 9 sh e 1DNB377
zwykle sprawdza tylko kilka znaków z po
3K N A 382 sh e tls 1DNB377
czątku każdego klucza. 3TAV879 shore 1DNB377
4CQ P781 su re ly 1DNB377
W ydajność Czas wykonania m etody MSD
4 Q G I2 8 4 the 1DNB377
zależy od danych. W technikach opartych the
4YH V229 1DNB377
na porównaniach główne znaczenie miała
Znaki sprawdzane przy sortowaniu
kolejność kluczy. W metodzie MSD upo łańcuchów znaków metodą MSD
rządkowanie kluczy jest nieistotne, ważne
są jednak ich wartości.
■ Dla losowych danych wejściowych metoda MSD sprawdza tylko tyle znaków,
aby odróżnić klucze. Czas wykonania rośnie wolniej niż liniowo względem licz
by znaków w danych (metoda sprawdza małą część wejściowych znaków).
■ Dla nielosowych danych wejściowych metoda MSD nadal może działać szybciej
niż liniowo, jednak czasem musi sprawdzić więcej znaków niż w danych loso
wych. Zależy to od samych danych. Ważne jest, że metoda sprawdza wszystkie
znaki w równych kluczach, dlatego jeśli występuje duża liczba równych kluczy,
czas wykonania jest prawie liniowy.
■ W najgorszym przypadku metoda MSD sprawdza wszystkie znaki we wszystkich
kluczach, dlatego czas wykonania rośnie liniowo względem liczby znaków w da
nych (tak jak w sortowaniu łańcuchów znaków metodą LSD). W danych wejścio
wych dla najgorszego przypadku wszystkie łańcuchy znaków są sobie równe.
T 5.1 o Sortowanie łańcuchów znaków 729
W niektórych zastosowaniach występują różne klucze, dla których odpowiedni jest
model losowych łańcuchów znaków. W innych sytuacjach występuje duża liczba
równych kluczy lub długich wspólnych przedrostków, dlatego czas sortowania jest
bliższy najgorszemu przypadkowi. Aplikacja do przetwarzania num erów tablic re
jestracyjnych może mieć wydajność odpowiadającą dowolnemu punktowi między
tymi skrajnościami. Jeśli inżynier pobierze dane godzinne z obciążonej autostrady
międzypaństwowej, liczba powtórzeń może być niewielka. Jeżeli jednak pobierze
dane tygodniowe z lokalnej drogi, duplikatów będzie wiele, a wydajność zbliży się do
najgorszego przypadku.
Twierdzenie C. Aby posortować N losowych łańcuchów znaków opartych na
fł-znakowym alfabecie, metoda MSD sprawdza średnio około N log(( N znaków.
Zarys dowodu. Oczekujemy, że podtablice będą mniej więcej tej samej wielko
ści, dlatego rekurencyjna zależność CN = RC n/r + N w przybliżeniu opisuje wy
dajność. Prowadzi to do podanego wyniku i stanowi uogólnienie dowodu dla
sortowania szybkiego z r o z d z i a ł u 2 . Także ten opis nie jest w pełni precyzyjny,
ponieważ N/R to nie zawsze liczba całkowita, a podtablice mają tę samą wielkość
tylko po uśrednieniu (ponadto w praktyce liczba znaków w kluczach jest skoń
czona). Okazuje się, że czynniki te mają znacznie mniejszy wpływ na metodę
MSD niż na standardowe sortowanie szybkie, dlatego najstarszy wyraz wzoru na
czas wykonania jest rozwiązaniem zależności rekurencyjnej. Szczegółowe anali
zy będące dowodem tego faktu są klasycznym przykładem analizy algorytmów.
Po raz pierwszy przedstawił je Knuth na początku lat 70. ubiegłego wieku.
Zauważ, że długość klucza nie ma tu znaczenia. Jest to materiał do przemyślenia,
ilustrujący jednocześnie, dlaczego dowód wykracza poza zakres książki. Model loso
wych łańcuchów znaków dopuszcza zbliżanie się długości kluczy do nieskończono
ści. Występuje niezerowe prawdopodobieństwo, że określona liczba znaków w obu
kluczach jest taka sama, jednak prawdopodobieństwo to jest na tyle małe, iż nie od
grywa roli przy szacowaniu wydajności.
Jak opisano, liczba sprawdzanych znaków nie jest jedynym czynnikiem w m eto
dzie MSD. Trzeba też uwzględnić czas i pamięć potrzebne na zliczanie wystąpień
i przekształcanie liczb na indeksy.
Twierdzenie D. Metoda MSD wymaga od 8N + 3R do ~7w N + 3 WR dostępów
do tablicy przy sortowaniu N łańcuchów znaków opartych na ił-znakowym alfa
becie (w to średnia długość łańcucha znaków).
Dowód. Wynika bezpośrednio z kodu, t w i e r d z e n ia a i t w i e r d z e n ia b.
W najlepszym przypadku metoda MSD wymaga jednego przebiegu. W najgor
szym — działa jak m etoda LSD.
730 ROZDZIAŁ 5 o Łańcuchy znaków
Dla małych N dominującym czynnikiem jest R. Choć dokładne analizy łącznych
kosztów są trudne, m ożna oszacować koszty, zastanawiając się nad krótkimi podtab-
licami dla różnych kluczy. Bez przełączania algorytmów dla krótkich podtablic każdy
klucz występuje we własnej podtablicy, dlatego podtablice wymagają NR dostępów
do tablicy. Przy przełączeniu algorytmów dla podtablic o wielkości M występuje oko
ło N /M podtablic o wielkości M, dlatego N R /M dostępów do tablicy można zamienić
na N M /4 porównania, co określa, że należy dobrać M proporcjonalnie do pierwiast
ka kwadratowego z R.
Twierdzenie D (ciąg dalszy). Dla najgorszego przypadku przy sortowaniu N
łańcuchów znaków opartych na jR-znakowym alfabecie ilość pamięci potrzebnej
w metodzie MSD jest proporcjonalna do R razy długość najdłuższego łańcucha
znaków (plus N).
Dowód. Tablicę count[] trzeba utworzyć w metodzie s o rt() , dlatego łączna
ilość potrzebnej pamięci jest proporcjonalna do R razy głębokość rekurencji
(plus N na tablicę pomocniczą). Głębokość rekurencji to długość najdłuższego
łańcucha znaków, który jest przedrostkiem przynajmniej dwóch sortowanych
łańcuchów znaków.
Jak opisano, równe klucze powodują, że głębokość rekurencji jest proporcjonalna
do długości kluczy. Bezpośrednie wnioski praktyczne wynikające z t w i e r d z e n ia d
są takie, że m etoda MSD może przekroczyć ograniczenia czasowe lub pamięciowe
przy sortowaniu długich łańcuchów znaków opartych na dużych alfabetach. Jest tak
zwłaszcza wtedy, jeśli występują długie równe sobie klucze. Przykładowo, przy sto
sowaniu alfabetu Al phabet . UNICODE i ponad Mrównych 1000-znakowych łańcuchów
metoda MSD. s o rt () potrzebuje pamięci na ponad 65 milionów liczników!
g ł ó w n ą t r u d n o ś c ią przy zapewnianiu maksymalnej wydajności metody MSD dla
kluczy, które są długimi łańcuchami znaków, jest konieczność radzenia sobie z bra
kiem losowości w danych. Zwykle klucze mogą obejmować długie serie równych da
nych, a czasem fragmenty kluczy przyjmują tylko kilka powtarzających się wartości.
Przykładowo, w aplikacji do przetwarzania danych na tem at studentów mogą wystę
pować klucze z datą ukończenia liceum (cztery bajty, ale tylko jedna z kilku różnych
wartości), nazwą województwa (10 bajtów, ale jedna z 16 wartości) i płcią (jeden
bajt o jednej z dwóch wartości), a także nazwiskiem danej osoby (ten człon przy
pom ina losowe łańcuchy znaków, jednak prawdopodobnie nie jest krótki, rozkład
liter jest nierównomierny, a w polu o stałej długości występują końcowe puste zna
ki). Ograniczenia tego rodzaju prowadzą do dużej liczby pustych podtablic w czasie
sortowania m etodą MSD. Dalej omawiamy elegancki sposób na dostosowanie się do
takich sytuacji.
5.1 a Sortowanie łańcuchów znaków 731
Szybkie sortowanie łańcuchów znaków z podziałem na trzy części
Można też dostosować sortowanie szybkie do metody MSD, wykorzystując podział na
trzy części według pierwszego znaku kluczy i przechodząc do następnego znaku tylko
w środkowej podtablicy (z kluczami o pierw
Wartość pierwszego Należy rekurencyjnie
szym znaku równym znakowi, według którego znaku należy wykorzystać posortować podtablice
dzielone są dane). Nietrudno jest zaimplemento na podział na podtablice (z pominięciem pierwszego
z „mniejszymi", „równymi" znaku podtablicy
wać tę metodę, czego dowodem jest a l g o r y t m o „równych" wartościach)
i „większymi" wartościami
5 .3 . Wystarczy dodać do metody rekurencyjnej
z a l g o r y t m u 2.5 argument do śledzenia bieżące
go znaku, dostosować kod podziału na trzy części
przez wykorzystanie tego znaku i odpowiednio
zmodyfikować wywołania rekurencyjne.
Choć obliczenia przebiegają w innej kolej
ności, szybkie sortowanie łańcuchów znaków
z podziałem na trzy części sprowadza się do
sortowania tablicy według pierwszych znaków
kluczy (za pom ocą sortowania szybkiego), po
czym m etoda stosowana jest rekurencyjnie dla
pozostałych kluczy. Przy sortowaniu łańcu
chów znaków m etoda działa lepiej niż zwykłe
sortowanie szybkie i sortowanie łańcuchów
znaków metodą MSD. Opisana technika jest
połączeniem obu wymienionych algorytmów.
Szybkie sortowanie łańcuchów znaków z po
działem na trzy części powoduje podział tablicy
Przebieg szybkiego sortowania
na tylko trzy fragmenty, dlatego jeśli liczba nie-
łańcuchów znaków z podziałem na trzy części
pustych części jest duża, dane przenoszone są
częściej niż w metodzie MSD, ponieważ trzeba przeprowadzić serię podziałów na trzy
części, aby uzyskać efekt podziału na wiele fragmentów. Natomiast metoda MSD może
tworzyć dużą liczbę pustych podtablic, podczas gdy szybkie sortowanie łańcuchów
znaków z podziałem
Dane wejściowe Posortowane dane
na trzy części zawsze
e d u . p r i n c e t o n . CS com. adobe
daje tylko trzy podtab
com . a p p le com. a pp l e
edu. p ri n ce to n . cs com. cnn
lice. Tak więc szybkie
co m . cnn com. g o o g l e sortowanie łańcuchów
Dopasowywanie
com 9 ° ° 9 1e długich, e d u . p r i n c e t o n . c s znaków dobrze nadaje
edu u v a . c s przedrostków [Link] nc eto [Link] się do obsługi równych
edu p r i n c e t o n . c s [Link] nc eto n.c s kluczy, kluczy o długich
edu p n n c e t o n . c s . www e d u . p r i n c e t o n . c s .www
wspólnych przedrost
e du u v a . c s Powtarzające e d u . p r i n c e t o n . ee
edu. u v a . cs się klucze e d u . u v a . cs kach, kluczy przyjmu
edu. u v a . cs e d u . u v a . cs jących tylko kilka war
com . adobe e d u . u v a . cs tości i krótkich tablic,
e d u . p r i n c e t o n . ee e d u . u v a . cs czyli wszystkich sytua-
Typowe dane do sortowania szybkiego
łańcuchów znaków z podziałem na trzy części
732 ROZDZIAŁ 5 Łańcuchy znaków
ALGORYTM 5.3. Sortowanie szybkie łańcuchów znaków z podziałem na trzy części
public c la s s Q uick3string
{
private s t a t ic in t ch arA t(Strin g s, in t d)
{ i f (d < s .le n g t h ()) return [Link] arAt(d); else return -1; }
public s t a t i c void s o r t ( S t r i n g [ ] a)
( s o rt(a , 0, [Link] - 1, 0); }
private s t a t ic void s o r t ( S t r i n g [ ] a, in t lo, in t hi, in t d)
{
i f (hi <= lo) return;
in t I t = lo, gt = h i ;
in t v = c h a r A t ( a [ lo ] , d ) ;
in t i = lo + 1;
while (i <= gt)
{
in t t = c h a r A t ( a [ i ] , d ) ;
if (t < v) exch(a, lt++, i++);
else i f (t > v) exch(a, i, g t - - ) ;
else i++;
}
// a [1 o . .11-1] < v = a [11 . . gt] < a [g t+ 1 . . h i ]
s o rt(a , lo, l t - 1 , d);
i f (v >= 0) s o rt(a , I t , gt, d +1);
s o rt(a , gt+1, hi, d);
}
}
Aby posortować tablicę a [] z łańcuchami znaków, należy podzielić ją na trzy części według
pierwszego znaku, a następnie rekurencyjnie posortować trzy uzyskane podtablice: z łańcu
chami, w których pierwszy znak jest mniejszy niż znak uwzględniany przy podziale, z łań
cuchami z pierwszym znakiem równym znakowi podziału (w tej części pierwszy znak jest
pomijany przy dalszym sortowaniu), a także z łańcuchami o pierwszym znaku większym niż
znak podziału.
5.1 h Sortowanie łańcuchów znaków 733
cji, w których metoda MSD działa wolno. Szczególnie ważne jest to, że podział pasuje
do różnego rodzaju struktur w różnych częściach klucza. Ponadto szybkie sortowanie
łańcuchów znaków z podziałem na trzy części (podobnie jak zwykłe sortowanie szyb
kie) nie wymaga dodatkowej pamięci (potrzebny jest tylko tworzony pośrednio stos do
obsługi rekurencji), co jest istotną zaletą w porównaniu z metodą MSD, która wymaga
pamięci zarówno na liczniki wystąpień, jak i na tablicę pomocniczą.
Na rysunku w dolnej części strony pokazano wszystkie wywołania rekurencyjne,
które klasa Qui ck 3 stri ng wykonuje w przykładzie. Każda podtablica jest sortowana
za pomocą dokładnie trzech rekurencyjnych wywołań. Wyjątkiem jest sytuacja, kie
dy pomijamy rekurencyjne wywołanie po dojściu do końców równych łańcuchów
znaków w środkowej podtablicy.
W praktyce, jak zwykle, warto rozważyć różne standardowe usprawnienia imple
mentacji przedstawionej w a l g o r y t m i e 5 .3 .
Krótkie podtablice W każdym algorytmie rekurencyjnym m ożna zwiększyć wy
dajność, traktując krótkie podtablice w odm ienny sposób. Tu stosujemy sortowanie
przez wstawianie ze strony 727, gdzie pomijane są znaki, o których wiadomo, że są
równe. Zyski wynikające z tej zmiany mogą być znaczne, choć nie w takim stopniu,
jak w metodzie MSD.
O graniczony alfabet Na potrzeby obsługi specjalnych alfabetów m ożna dodać do
każdej metody argument alpha typu Alphabet i zastąpić w metodzie charAt() wy
wołanie [Link] t(d) instrukcją a lp h a .toIndex([Link] t(d)). Tu takie rozwiązanie
nie przynosi korzyści, a dodanie wspomnianego kodu może znacznie spowolnić al
gorytm, ponieważ kod ten działa w pętli wewnętrznej.
Szare paski reprezentują Dwa dalsze przebiegi są
she by are puste podtablice potrzebne na dotarcie do końca
sel 1 s are
seashel 1 s e a s h e l 1s se ashe I I s
ly he se
the e a s h e l 1s se a s h e 11 s
sea ea se
shore ho re se I l s
the u re l y s h e lls
s h e lls h e lls he
she he u re l y Brak rekurencyjnych
wywołań (koniec
s e lls e lls hore
łańcucha znaków)
ire 11s he su rely
surely th e le the the
seashel 1 s the le the the
Ślad rekurencyjnych wywołań sortowania szybkiego łańcuchów znaków
z podziałem na trzy części (bez przełączania dla krótkich podtablic)
734 ROZDZIAŁ 5 ■ Łańcuchy znaków
R andom izacja Tak jak w każdym sortowaniu szybkim, tak i tu ogólnie warto wstęp
nie wymieszać tablicę lub zastosować losowy element osiowy, przestawiając pierwszą
wartość z losową. Ma to przede wszystkim chronić przed najgorszym przypadkiem
w sytuacji, kiedy tablica jest już posortowana (lub prawie uporządkowana).
Dla kluczy w postaci łańcuchów znaków sortowanie szybkie i inne m etody sorto
wania z r o z d z i a ł u 2 . odpowiadają technice MSD, ponieważ metoda compareTo()
w klasie S tri ng uzyskuje dostęp do znaków w kolejności od lewej do prawej. Oznacza
to, że m etoda compareTo () sprawdza tylko pierwsze znaki, jeśli są różne, dwa począt
kowe znaki, jeśli pierwsze są identyczne, a drugie — odm ienne itd. Przykładowo,
jeśli pierwsze znaki wszystkich łańcuchów znaków są różne, standardowe sortowanie
sprawdzi tylko je, co automatycznie gwarantuje te same zyski w wydajności, co w m e
todzie MSD. Kluczowym pomysłem, na którym oparte jest sortowanie szybkie z p o
działem na trzy części, jest podjęcie specyficznych działań, kiedy pierwsze znaki są
równe. O a l g o r y t m i e 5.3 można myśleć jak o sposobie na śledzenie w sortowaniu
szybkim pierwszych znaków, o których wiadomo, że są równe. W krótkich podtabli-
cach, w których większość porównań została już wykonana, łańcuchy znaków mają
przeważnie dużą liczbę równych łańcuchów znaków. Standardowy algorytm musi
przejść po wszystkich tych znakach w każdym porównaniu, natomiast w algorytmie
z podziałem na trzy części nie jest to konieczne.
W ydajność Rozpatrzmy sytuację, w której klucze w postaci łańcuchów znaków są
długie (i — dla uproszczenia — mają tę samą długość), przy czym większość pierw
szych znaków jest taka sama. Wtedy czas wykonania standardowego sortowania
szybkiego jest proporcjonalny do długości łańcuchów znaków razy 2N ln N, a dla
sortowania szybkiego z podziałem na trzy części jest to N razy długość łańcuchów
znaków (w celu wykrycia wszystkich równych początkowych znaków) plus 2N ln N
porównań (w celu posortowania pozostałych, krótkich kluczy). Tak więc sortowanie
szybkie z podziałem na trzy części wymaga nawet 2 ln N razy mniej porównań niż
zwykłe sortowanie szybkie. Nierzadko się zdarza, że w praktyce klucze mają podobne
cechy, co w przedstawionym tu sztucznym przykładzie.
5.1 o Sortowanie łańcuchów znaków 735
Twierdzenie E. Aby posortować tablicę Włosowych łańcuchów znaków, sorto
wanie szybkie z podziałem na trzy części wymaga średnio ~2N ln N porównań
znaków.
Dowód. Są dwa sposoby na zrozumienie tego wyniku. Oto pierwszy — rozważ
my metodę odpowiadającą podziałowi według pierwszego znaku z sortowania
szybkiego i późniejsze rekurencyjne stosowanie tej m etody do podtablic. Nie jest
zaskoczeniem, że łączna liczba operacji jest wtedy porównywalna z sortowaniem
szybkim, jednak tu porównania dotyczą pojedynczych znaków, a nie całych klu
czy. Po drugie, po zastosowaniu sortowania szybkiego zamiast sortowania przez
zliczanie m ożna oczekiwać, że czas wykonania N logRN z t w i e r d z e n i a d zosta
nie zwielokrotniony o czynnik 2 ln R, ponieważ sortowanie szybkie wykonuje 2R
ln R kroków w celu posortowania R znaków, a nie R kroków potrzebnych dla tych
samych znaków w metodzie MSD. Pełny dowód pomijamy.
Jak podkreślono na stronie 728, warto rozważyć losowe łańcuchy znaków, jednak
do prognozowania wydajności w praktycznych sytuacjach potrzebne są bardziej
szczegółowe analizy. Badacze dokładnie przebadali opisany algorytm i udowodnili,
że — przy bardzo ogólnych założeniach i uwzględnianiu liczby porównań znaków
— żadne rozwiązanie nie może być szybsze niż sortowanie szybkie z podziałem na
trzy części o więcej niż stały czynnik. Aby docenić wszechstronność metody, warto
zauważyć, że sortowanie szybkie łańcuchów znaków z podziałem na trzy części nie
jest bezpośrednio zależne od rozmiaru alfabetu.
P rzykład — dzien niki sieciowe Jako przykładową sytuację, w której widoczne są
zalety sortowania szybkiego łańcuchów znaków z podziałem na trzy części, rozważ
my typowe współczesne zadanie z obszaru przetwarzania danych. Adm inistrator sy
stemu może udostępnić dziennik sieciowy z wszystkimi transakcjami dotyczącymi
witryny. Informacje na tem at transakcji obejmują nazwę domeny pierwotnej maszy
ny. Przykładowo, plik [Link] z witryny poświęconej książce to dziennik trans
akcji z jednego tygodnia z tej witryny. Dlaczego sortowanie szybkie łańcuchów zna
ków z podziałem na trzy części jest tak skuteczne dla plików tego rodzaju? Ponieważ
posortowane wyniki obejmują wiele długich wspólnych przedrostków, których w tej
metodzie nie trzeba ponownie sprawdzać.
■Mi
736 ROZDZIAŁ 5 B Łańcuchy znaków
Z którego algorytmu sortowania łańcuchów znaków powinienem
korzystać? Naturalne jest, że interesuje nas wydajność opisanych m etod sorto
wania łańcuchów znaków w porównaniu z technikami ogólnego użytku przedstawio
nymi w r o z d z i a l e 2 . W poniższej tabeli podsumowano ważne cechy omówionych
tu algorytmów sortowania łańcuchów znaków (dla porównania dołączono wiersze
z r o z d z i a ł u 2 ., dotyczące sortowania szybkiego, sortowania przez scalanie i sorto
wania szybkiego z podziałem na trzy części).
Tempo wzrostu typowej liczby
wywołań charAtO przy sortowaniu
ń/łańcuchów znaków z R-znakowego
Działa alfabetu (średnia długość w,
Algorytm Stabilny? Najlepsze dla
w miejscu? maksymalna — W)
Dodatkowa
Czas wykonania
pamięć
Sortowanie przez Małe lub
wstawianie dla Tak Tak Między N a N 2 uporządkowane
łańcuchów znaków tablice
Sortowanie
ogólnego użytku
Sortowanie szybkie Nie Tak N \ o g 2N lo g N
przy ograniczeniach
pamięci
Sortowanie przez Stabilne sortowanie
Tak Nie N l o g 2N N
scalanie ogólnego użytku
Sortowanie szybkie
Duża liczba równych
z podziałem na trzy Nie Tak Między N a N log N log N
kluczy
części
Sortowanie Krótkie łańcuchy
łańcuchów znaków Tak Nie NW N znaków o stałej
metodę LSD długości
Sortowanie
Losowe łańcuchy
łańcuchów znaków Tak Nie Między N a Nw N + WR
znaków
metodę MSD
Sortowanie
Sortowanie szybkie
ogólnego użytku; dla
łańcuchów znaków
Nie Tak Między N a Nw W + log N łańcuchów znaków
z podziałem
z długimi wspólnymi
na trzy części
przedrostkam i
Cechy z obszaru wydajności algorytmów sortowania łańcuchów znaków
Tak jak w r o z d z i a l e 2 ., tak i tu pom nożenie tem pa wzrostu przez odpowiednie
stałe zależne od algorytm u i danych to skuteczny sposób na przewidzenie czasu
wykonania.
W omówionych już sytuacjach i w wielu innych przykładach w ćwiczeniach poka
zano, że różne specjalne przypadlci wymagają stosowania odmiennych m etod i od
powiednich parametrów. Dzięki nim eksperci (może i Ty sam już nim jesteś) w pew
nych warunkach mogą uzyskać bardzo istotne oszczędności.
5.1 □ Sortowanie łańcuchów znaków 737
J PY T A N IA I O D P O W IE D Z I
P. Czy sortowanie systemowe w Javie korzysta z jednej z metod sortowania obiektów
S tri ng?
O. Nie, jednak standardowa implementacja obejmuje szybkie porównywanie łańcu
chów znaków, przez co sortowanie standardowe działa porównywalnie z opisanymi
tu metodami.
P. Tak więc do sortowania kluczy typu S tri ng powinienem używać sortowania sy
stemowego?
O. W Javie zwykle należy tak postępować, jednak jeśli liczba łańcuchów znaków jest
bardzo duża lub potrzebujesz wyjątkowo szybkiego sortowania, możesz zastosować
w tablicach typ char zamiast S tri ng i wykorzystać sortowanie pozycyjne.
P. Z czego wynika czynnik log 2 N w tabeli na poprzedniej stronie?
O. Jest odzwierciedleniem tego, że większość porównań w algorytmach z tym czyn
nikiem dotyczy kluczy o wspólnych przedrostkach o długości log N. W niedawnych
badaniach na podstawie starannych analiz matematycznych ustalono ten fakt dla lo
sowych łańcuchów znaków (więcej informacji znajdziesz w witrynie).
ROZDZIAŁ 5 ■ Łańcuchy znaków
[ ] Ć W IC Z E N IA
5.1.1. Opracuj implementację sortowania, która zlicza różne wartości kluczy, a na
stępnie na podstawie tablicy symboli i sortowania przez zliczanie sortuje tablicę.
Metoda ta nie nadaje się do użytku, jeśli liczba różnych wartości kluczy jest duża.
5.1.2. Przedstaw ślad przebiegu sortowania łańcuchów znaków metodą LSD dla na
stępujących kluczy:
no i s th t i fo al go pe to co to th ai of th pa
5.1.3. Przedstaw ślad przebiegu sortowania łańcuchów znaków metodą MSD dla
następujących kluczy:
no i s th t i fo al go pe to co to th ai of th pa
5.1.4. Przedstaw ślad przebiegu sortowania szybkiego łańcuchów znaków z podzia
łem na trzy części dla następujących kluczy:
no i s th ti fo al go pe to co to th ai of th pa
5.1.5. Przedstaw ślad przebiegu sortowania łańcuchów znaków m etodą MSD dla
następujących kluczy:
now i s the time f o r a ll good people to come to the aid of
5.1. 6 . Przedstaw ślad przebiegu sortowania szybkiego łańcuchów znaków z podzia
łem na trzy części dla następujących kluczy:
now i s the time fo r a ll good people to come to the aid of
5.1.7. Opracuj implementację sortowania przez zliczanie, w której wykorzystywana
jest tablica obiektów Queue.
5.1. 8 . Podaj liczbę znaków sprawdzanych w sortowaniu łańcuchów znaków metodą
MSD i sortowaniu szybldm łańcuchów znaków z podziałem na trzy części. Sortowany
plik obejmuje Nkluczy: a, aa, aaa, aaaa, aaaaa itd.
5.1.9. Opracuj implementację sortowania łańcuchów znaków metodą LSD działają
cą dla łańcuchów o zmiennej długości.
5.1.10. Jaka jest w najgorszym przypadku łączna liczba znaków sprawdzanych
w sortowaniu szybkim łańcuchów znaków z podziałem na trzy części przy sortowa
niu N łańcuchów o stałej długości (równej WJ)?
5.1 e Sortowanie łańcuchów znaków 739
j PROBLEMY DO ROZWIĄZANIA
5.1.11. Sortowanie oparte na kolejkach. Zaimplementuj sortowanie łańcuchów zna
ków m etodą MSD za pom ocą kolejek. Należy utrzymywać jedną kolejkę dla każdego
koszyka. Przy pierwszym przebiegu przez sortowane elementy należy wstawić każdy
element do odpowiedniej kolejki według wartości początkowego znaku. Następnie
trzeba posortować podlisty i złączyć wszystkie kolejki, aby uzyskać posortowane
dane. Zauważmy, że m etoda ta nie wymaga utrzymywania tablic count [] w rekuren-
cyjnej metodzie.
5.1.12. Alfabet. Opracuj implementację interfejsu API klasy Al phabet przedstawio
nego na stronie 710 i wykorzystaj ją do opracowania metod LSD oraz MSD dla ogól
nych alfabetów.
5.1.13. Sortowanie hybrydowe. Zbadaj pomysł wykorzystania standardowego sorto
wania łańcuchów znaków metodą MSD dla długich tablic (w celu wykorzystania za
let podziału na wiele części) i sortowania szybkiego łańcuchów znaków z podziałem
na trzy części dla krótkich tablic (aby uniknąć negatywnych skutków powstawania
dużej liczby pustych koszyków).
5.1.14. Sortowanie tablic. Opracuj metodę wykorzystującą sortowanie szybkie łań
cuchów znaków z podziałem na trzy części dla lduczy będących tablicami wartości
typu i nt.
5.1.15. Sortowanie szybsze od liniowego. Opracuj implementację sortowania w arto
ści typu i nt, która wykonuje dwa przebiegi przez tablicę w celu zastosowania metody
LSD dla początkowych 16 bitów kluczy, a następnie przeprowadza sortowanie przez
wstawianie.
5.1.16. Sortowanie list powiązanych. Opracuj implementację sortowania, która jako
argument pobiera listę powiązaną węzłów z kluczami typu S tring i sortuje węzły
(ostatecznie zwraca odnośnik do węzła o najmniejszym kluczu). Wykorzystaj sorto
wanie szybkie łańcuchów znaków z podziałem na trzy części.
5.1.17. Sortowanie przez zliczanie w miejscu. Opracuj wersję sortowania przez zli
czanie wymagającą stałej ilości dodatkowej pamięci. Udowodnij, że wersja jest stabil
na, lub przedstaw kontrprzykład.
740 ROZDZIAŁ 5 □ Łańcuchy znaków
U EKSPERYMENTY
5.1.18. Losowe klucze dziesiętne. Napisz metodę statyczną randomDecimal Keys, któ
ra jako argumenty przyjmuje wartości N i Wtypu i nt, a zwraca tablicę N wartości typu
S t r i ng, z których każda jest W-cyfrową liczbą dziesiętną.
5.1.19. Losowe kalifornijskie tablice rejestracyjne. Napisz metodę statyczną ran-
domPlatesCA, przyjmującą jako argument wartość N typu in t i zwracającą tablicę N
wartości typu S tring, reprezentujących kalifornijskie tablice rejestracyjne, takie jak
w przykładach z podrozdziału.
5.1.20. Losowe słowa o stałej długości. Napisz metodę statyczną randomFixed-
LengthWords, która jako argumenty przyjmuje wartości N i W typu in t oraz zwraca
tablicę Nwartości typu S t r i ng, składające się z Wznaków alfabetu.
5.1.21. Losowe elementy. Napisz metodę statyczną randomltems, która jako argu
m ent przyjmuje wartość Ntypu i nt i zwraca tablicę Nwartości typu S tri ng. Wartości
te mają być łańcuchami o długości od 15 do 30 znaków, składającymi się z trzech
pól: 4-znakowego pola w postaci jednego ze zbioru 10 łańcuchów; 10-znakowego
pola w postaci jednego ze zbioru 50 łańcuchów; 1-znakowego pola o jednej z dwóch
wartości; i 15-bajtowego pola z losowymi, wyrównanymi do lewej łańcuchami, które
z równym prawdopodobieństwem składają się z od 4 do 15 liter.
5.1.22. Czasy wykonania. Przy użyciu różnych generatorów kluczy porównaj czasy
wykonania sortowania łańcuchów znaków metodą MSD i sortowania szybkiego łań
cuchów znaków z podziałem na trzy części. Przy kluczach o stałej długości uwzględ
nij też sortowanie łańcuchów znaków metodą LSD.
5.1.23. Dostępy do tablicy. Przy użyciu różnych generatorów kluczy porównaj liczbę
dostępów do tablicy przy sortowaniu łańcuchów znaków m etodą MSD i sortowaniu
szybkim łańcuchów znaków z podziałem na trzy części. Przy kluczach o stałej dłu
gości uwzględnij też sortowanie łańcuchów znaków m etodą LSD.
5.1.24. Najdalszy uwzględniany znak po prawej. Porównaj pozycję najdalszego
uwzględnianego znaku po prawej przy sortowaniu łańcuchów znaków m etodą MSD
i sortowaniu szybkim łańcuchów znaków z podziałem na trzy części.
Tak jak przy sortowaniu, tak i przy przeszukiwaniu m ożna wykorzystać cechy łań
cuchów znaków, aby opracować m etody (implementacje tablic symboli), które
— jeśli kluczami wyszukiwania są łańcuchy znaków — są wydajniejsze niż opisane
w r o z d z i a l e 3 . techniki ogólnego użytku.
Metody omawiane w tym podrozdziale pozwalają osiągnąć w typowych zastoso
waniach następującą wydajność (dotyczy to nawet bardzo dużych tablic):
■ Czas udanego wyszukiwania jest proporcjonalny do długości klucza wyszuki
wania.
■ Czas nieudanego wyszukiwania wymaga sprawdzenia tylko kilku znaków.
Po zastanowieniu wydajność ta okazuje się być zdumiewająca i stanowi jedno z klu
czowych osiągnięć technik algorytmicznych. Omawiane rozwiązanie jest głównym
czynnikiem, który umożliwił opracowanie dostępnej obecnie infrastruktury infor
matycznej i sprawił, że można błyskawicznie uzyskać dostęp do tak wielu informacji.
Ponadto można rozwinąć interfejs API dla tablic symboli przez dodanie opartych na
znakach operacji zdefiniowanych dla kluczy w postaci łańcuchów znaków (choć nie
dotyczy to wszystkich typów kluczy zgodnych z interfejsem Com parabl e). Operacje te
dają duże możliwości i są całkiem przydatne w praktyce. Przedstawiono je w poniż
szym interfejsie API:
p u b lic c la s s Strin gST <Valu e>
S t r i ngST() Tworzy tablicę symboli
void p u t (S tr in g key, Value v a l) Umieszcza parę klucz-wartość w tablicy
(usuwa klucz key, jeśli wartość to n u li)
Value g e t ( S t r in g key) Zwraca wartość powiązaną z key
(lub n u li, jeśli klucz key nie istnieje)
void d e le t e (S tr in g key) Usuwa klucz key (i powiązaną z nim wartość)
boolean c o n t a in s (S t r in g key) Czy z kluczem key powiązana jest wartość?
boolean isEm pty() Czy tablica jest pusta?
S t r i ng lo n ge stP re fixO f(Strin g s) Zwraca najdłuższy klucz będący przedrostkiem s
Ite ra b le < S t rin g > keysW ithPrefix(String s) Zwraca wszystkie klucze z przedrostkiem s
Ite ra b le < S t rin g > keysThatM atch(String s) Zwraca wszystkie klucze pasujące do s
(. pasuje do dowolnego znaku)
i nt s iz e ( ) Zwraca liczbę par klucz-wartość
Ite ra b le < S t rin g > keys() Zwraca wszystkie klucze z tablicy
Interfejs API dla tablicy symboli z kluczami w postaci łańcuchów znaków
5.2 □ Drzewa tríe 743
Ten interfejs API różni się od interfejsu API tablicy symboli przedstawionego
w r o z d z i a l e 3 . w następujących obszarach:
■ Generyczny typ Key zastąpiono typem konkretnym S tri ng.
■ Dodano trzy nowe metody: 1ongestPrefixOf (), keysWithPrefix() i keysThat-
Match().
Zachowujemy podstawowe konwencje z r o z d z i a ł u 3 . dotyczące implementacji tab
licy symboli (niedozwolone są powtórzenia oraz klucze lub wartości równe nul 1).
Jak pokazano w kontekście sortowania kluczy w postaci łańcuchów znaków, czę
sto ważna jest możliwość korzystania z łańcuchów opartych na określonym alfabecie.
Proste i wydajne implementacje, stosowane z wyboru dla małych alfabetów, okazu
ją się bezużyteczne dla dużych alfabetów, ponieważ wymagają zbyt dużo pamięci.
W wielu sytuacjach warto dodać konstruktor umożliwiający klientom określenie al
fabetu. Implementację takiego konstruktora omawiamy w dalszej części podrozdzia
łu, jednak na razie pomijamy go w interfejsie API, co pozwoli skoncentrować się na
kluczach w postaci łańcuchów znaków.
W przykładach w dalszych opisach trzech nowych m etod wykorzystano klucze
she s e l l s sea s h e l l s by the sea shore.
■ M etoda 1ongestPrefixOf () jako argument pobiera łańcuch znaków i zwraca
najdłuższy klucz z tablicy symboli będący przedrostkiem tego łańcucha. Dla
przedstawionych kluczy wywołanie 1ongestPrefixOf("shel 1 ") zwraca she,
awywołanie lon g e stP re fix O f("sh e llso rt") — shells.
■ M etoda keysWithPrefix() jako argument przyjmuje łańcuch znaków i zwraca
wszystkie klucze z tablicy symboli, których dany łańcuch jest przedrostkiem.
Dla przedstawionych kluczy wywołanie keysWithPrefix("she") zwraca she,
a wywołanie keysWi thPrefix("se") — sel 1s i sea.
■ M etoda keysThatMatch() jako argument przyjmuje łańcuch znaków i zwraca
wszystkie klucze z tablicy symboli, które pasują do tego łańcucha. Kropka (.)
w argumencie pasuje do dowolnego znaku. Dla przedstawionych kluczy wywo
łanie keysThatMatch(".he") zwraca she, a wywołanie keysThatM atch("s.. ")
— she i sea.
Implementacje i zastosowania tych operacji omawiamy szczegółowo po przedsta
wieniu podstawowych m etod związanych z tablicą symboli. Opisane operacje są re
prezentatywne, jeśli chodzi o możliwości przetwarzania kluczy w postaci łańcuchów
znaków. W ćwiczeniach omawiamy kilka innych możliwości.
Aby skupić się na głównych pomysłach, koncentrujemy się na put () , get ( ) i no
wych metodach. Zakładamy (tak jak w r o z d z i a l e 3 .) stosowanie domyślnych im
plementacji m etod con tains() i isEmpty(), a przygotowanie implementacji metod
s i ze ( ) i del ete ( ) pozostawiamy jako ćwiczenia. Ponieważ łańcuchy znaków są zgod
ne z interfejsem Comparable, możliwe (i warte zachodu) jest rozwinięcie opisanego
interfejsu API o operacje na danych uporządkowanych, zdefiniowane w r o z d z i a l e 3 .
w interfejsie API dla uporządkowanych tablic symboli. Implementacje (zwykle pro
ste) opisane są w ćwiczeniach i w kodzie w poświęconej książce witrynie.
744 ROZDZIAŁ 5 □ Łańcuchy znaków
Drzewa trie W tym podrozdziale omawiamy drzewo wyszukiwań nazywane
trie. Jest to struktura danych zbudowana na podstawie kluczy w postaci łańcuchów
znaków i umożliwiająca wykorzystanie przy przeszukiwaniu znaków z klucza wy
szukiwania. Nazwa „trie” jest grą słowną i została wprowadzona przez E. Fredkina.
Struktura danych służy do pobierania (ang. retrieval), jednak jej nazwa wymawia
na jest jak słowo „try” (czyli próba), aby uniknąć pomyłki ze słowem „tree” (czyli
drzewo). Zaczynamy od wysokopoziomowego opisu podstawowych cech drzew trie,
w tym algorytmów wyszukiwania i wstawiania. Dalej przechodzimy do szczegółów
reprezentacji i implementacji w Javie.
Podstawowe cechy Drzewa trie, podobnie jak drzewa wyszukiwań, to struktury da
nych składające się z węzłów obejmujących odnośniki, które albo są równe nul 1 , albo
są referencją do innego węzła. Do każdego węzła prowadzi dokładnie jeden inny wę
zeł, tak zwany rodzic (wyjątkiem jest jeden węzeł, korzeń, do którego nie prowadzą
żadne węzły). Każdy węzeł obej- Odnośnik do drzewa trie
Korzeń
muje R odnośników, przy czym R z wszystkimi kluczami
rozpoczynającymi się od s
to wielkość alfabetu. Drzewa trie
Odnośnik do drzewa
często mają dużą liczbę pustych
trie z wszystkimi kluczami
odnośników, dlatego na rysun rozpoczynającymi się od sht
kach takie odnośniki zwykle się Wartość odpowiadająca
pomija. Choć odnośniki prowadzą she w węźle
odpowiadającym
do węzła, można też przyjąć, że ostatniemu znakowi
tego klucza
prowadzą do drzewa trie, którego
korzeniem jest wskazywany wę
zeł. Każdy odnośnik odpowiada
znakowi, a ponieważ prowadzi
Każdy węzeł
do dokładnie jednego węzła, wę oznaczony jest
zły oznaczamy za pomocą znaku znakiem powiązanym
z odnośnikiem
odpowiadającego odnośnikowi, wejściowym
Struktura drzewa trie
który prowadzi do danego węzła
(wyjątkiem jest korzeń, do którego nie prowadzą żadne odnośniki). Każdy węzeł ma
też określoną wartość. Może to być nuli lub wartość powiązana z jednym z kluczy (łań
cuchów znaków) z tablicy symboli. Wartość powiązana z każdym kluczem zapisywana
jest w węźle odpowiadającym ostatniemu znakowi klucza. Bardzo ważne jest, aby za
pamiętać, iż węzły o wartościach nuli mają ułatwiać przeszukiwanie drzewa trie i nie
odpowiadają kluczom. Przykładowe drzewo trie przedstawiono po prawej stronie.
Przeszukiw anie drzew a trie Znajdowanie wartości powiązanej z danym kluczem
(łańcuchem znaków) w drzewie trie to prosty proces, oparty na znakach z klucza wy
szukiwania. Każdy węzeł w drzewie trie obejmuje odnośnik odpowiadający każdemu
możliwemu znakowi łańcucha. Zaczynamy w korzeniu, a następnie przechodzimy
do odnośnika powiązanego z pierwszym znakiem klucza. Z tego węzła podążamy za
odnośnikiem powiązanym z drugim znakiem klucza. Dalej podążamy za odnośni
kiem odpowiadającym trzeciemu znakowi klucza i tak dalej. Proces kończy się znale-
5.2 □ Drzewa trie 745
Udane wyszukiwania Nieudane wyszukiwania
g e tC sh e lls" ) yet("shel1" )
Zwracanie wartości
węzła powiązanego
\
Wartość w węźle odpowiadającym
ostatniemu znakowi klucza to n u li,
z ostatnim znakiem klucza
dlatego należy zwrócić n u li
getCshe") getC'shore")
Przeszukiwanie
może się zakończyć Brak odnośnika dla
w węźle pośrednim litery o, dlatego
należy zwrócić n u li
Przykładowe operacje przeszukiwania drzewa trie
zieniem ostatniego znaku klucza lub pustego odnośnika. Na tym etapie spełniony jest
jeden z trzech warunków (przykłady pokazano na rysunku powyżej).
■ Wartość węzła odpowiadającego ostatniemu znakowi klucza jest różna od nul 1
(tak jak przy wyszukiwaniu łańcuchów shel 1 s i she po lewej stronie rysunku
powyżej). Oznacza to udane wyszukiwanie. Wartość powiązana z kluczem to
wartość w węźle odpowiadającym ostatniemu znakowi klucza.
■ Wartość węzła odpowiadającego ostatniemu znakowi klucza to nuli (tak jak przy
wyszukiwaniu łańcucha shel 1 , co pokazano w prawej górnej części rysunku po
wyżej). Oznacza to nieudane wyszukiwanie — klucz nie znajduje się w tablicy.
■ Wyszukiwanie kończy się pustym odnośnikiem (tak jak przy wyszukiwaniu
łańcucha shore, co pokazano w prawej dolnej części rysunku powyżej). Także
to oznacza nieudane wyszukiwanie.
We wszystkich sytuacjach wyszukiwanie wymaga sprawdzenia tylko węzłów na ścieżce
z korzenia do innego węzła drzewa trie.
746 ROZDZIAŁ 5 o Łańcuchy znaków
W staw ianie do drzew a trie Podobnie jak w drzewach wyszukiwań binarnych, tak
i tu wstawianie rozpoczyna się od wyszukiwania. W drzewie trie oznacza to wyko
rzystanie znaków klucza do przejścia w dól drzewa do m om entu napotkania ostat
niego znaku klucza lub odnośnika nul 1. Na tym etapie spełniony jest jeden z dwóch
warunków.
■ Napotkano odnośnik nul 1 przed dojściem do ostatniego znaku klucza. Wtedy
żaden węzeł drzewa trie nie odpowiada ostatniemu znakowi klucza, dlatego
trzeba utworzyć węzły dla każdego z nienapotkanych znaków klucza i ustawić
wartość w ostatnim z nich na wartość wiązaną z kluczem.
■ Napotkano ostatni znak klucza przed dojściem do odnośnika nul 1. Wtedy na
leży ustawić wartość węzła na wartość wiązaną z kluczem (niezależnie od tego,
czy wartość ta to nul 1 ), tak jak zwykle postępujemy w przypadku tablic asocja
cyjnych.
We wszystkich sytuacjach należy w drzewie trie sprawdzić lub utworzyć węzeł dla
każdego znaku klucza. Na następnej stronie pokazano tworzenie drzewa trie przez
standardowego używającego indeksów klienta z r o z d z i a ł u 3 . Drzewo tworzone jest
dla danych wejściowych:
she s e l l s sea s h e l ls by the sea shore
Reprezentacja w ęzłów Jak wspomnieliśmy na początku, rysunki drzew trie nie od
powiadają strukturom danych tworzonym przez programy, ponieważ pomijamy od
nośniki nul 1. Uwzględnienie odnośników nul 1 pozwala zrozumieć poniższe ważne
cechy drzew trie:
■ Każdy węzeł obejmuje R odnośników — po jednym na każdy możliwy znak.
■ Znaki i klucze są przechowywane w strukturze danych pośrednio.
Przykładowo, na rysunku poniżej pokazano drzewo trie dla kluczy składających się
z małych liter. Każdy węzeł ma tu wartość i 26 odnośników. Pierwszy odnośnik pro
wadzi do poddrzewa trie z kluczami rozpoczynającymi się od a, drugi prowadzi do
poddrzewa trie z podłańcuchami rozpoczynającymi się od b itd.
5.2 □ Drzewa trie 747
Klucz Wartość Klucz Wartość
Korzeń
she by
Wartość znajduje
się w węźle
odpowiadającym
ostatniemu znakowi
sells
t he
Po jednym
węźle na każdy -
znak klucza
sea
Klucz to ciąg fa)2 (T) (e)o
znaków na
drodze z korzenia
do wartości
shells 3
Węzły odpowiadające
końcowym znakom klucza
nie istnieją, dlatego należy
je utworzyć i ustawić
wartość w ostatnim z nich
Ślad procesu tworzenia drzewa trie przez standardowego klienta używającego indeksów
748 ROZDZIAŁ 5 0 Łańcuchy znaków
Klucze w drzewach trie są pośrednio reprezentowane przez ścieżki z korzenia, które
kończą się w węzłach o wartości różnej niż nuli. Przykładowo, łańcuch znaków sea jest
powiązany w drzewie trie z wartością 2, ponieważ 19. odnośnik w korzeniu (prowadzą
cy do drzewa trie z wszystkimi kluczami rozpoczynającymi się od s) jest różny od nul 1,
piąty odnośnik w węźle docelowym (prowadzący do drzewa trie z wszystkimi kluczami
zaczynającymi się od se) jest różny od nul 1 , a pierwszy odnośnik w węźle docelowym
odnośnika (prowadzący do drzewa trie z wszystkimi kluczami rozpoczynającymi się od
sea) ma wartość 2. Ani łańcuch znaków sea, ani znaki s, e i a nie są zapisane w struktu
rze danych. Struktura danych nie obejmuje żadnych znaków ani łańcuchów, a jedynie
odnośniki i wartości. Ponieważ parametr R odgrywa tu kluczową rolę, drzewo trie dla
alfabetu !Aznakowego nazywamy R-kierunkowym drzewem trie (ang. R-way trie).
napisanie przedstawionej na następnej stronie implementacji tablicy
p o t y m w s t ę p ie
symboli TrieST jest proste. Wykorzystano metody rekurencyjne, podobne do tych dla
drzew wyszukiwań z r o z d z i a ł u 3 ., oparte na prywatnej klasie Node, która obejmuje
zmienną egzemplarza val na wartości potrzebne klientom i tablicę next [] na referencje
do obiektów Node. Wspomniane metody to zwięzłe, rekurencyjne implementacje, któ
rym warto starannie się przyjrzeć. Dalej omawiamy implementacje konstruktora, który
jako argument przyjmuje obiekt Alphabet, oraz metod size(), keys(), longestPrefi-
xOf (), keysWithPrefix(), keysThatMatch() idelete(). Są to łatwe do zrozumienia meto
dy rekurencyjne, z których każda jest nieco bardziej skomplikowana od poprzedniej.
Określanie wielkości Tak jak w kontekście opisanych w r o z d z i a l e 3 . drzew wy
szukiwań binarnych, tak i tu przy implementowaniu m etody si ze () możliwe są trzy
rozwiązania:
■ Zachłanna implementacja, w której w zmiennej egzemplarza N przechowywana
jest liczba kluczy.
■ Wysoce zachłanna implementacja, w której liczba kluczy w poddrzewie trie
przechowywana jest w zmiennej egzemplarza w węźle, a jej aktualizacja ma
miejsce po rekurencyjnych wywołaniach
w metodach put () id e le te ( ). pub lic in t s iz e ( )
{ return s iz e ( r o o t ) ; }
■ Leniwa implementacja rekurencyjna po
dobna do przedstawionej po prawej; kod p r i v a t e i n t s i z e ( N o d e x)
przechodzi tu po wszystkich węzłach (
drzewa trie i zlicza węzły o wartości róż i f (x == n u l i ) r e t u r n 0;
nej od nul 1. i n t cnt = 0;
Tak jak dla binarnych drzew wyszukiwań, tak i f ( x . v a l != n u l l ) cn t++;
i tu leniwa implementacja jest pouczająca, ale f o r ( c h a r c = 0; c < R; C + + )
c n t += s i z e ( n e x t [ c ] );
należy jej unikać, ponieważ może prowadzić
do problemów z wydajnością kodu klienta. return cnt;
Implementacje zachłanne omówiono w ćwiczę- }
niach.
Leniwa rekurencyjna metoda sizeO
dla drzew trie
5.2 Drzewa trie 749
ALGORYTM [Link] symboli oparta na drzewie trie
p ublic c la s s TrieST<Value>
{
p rivate s t a t i c in t R = 256; // Podstawa,
private Node root; // Korzeń drzewa t r i e .
private s t a t ic c la s s Node
{
p rivate Object v a l ;
p rivate Node[] next = new Node[R];
}
public Value g e t ( S t r in g key)
{
Node x = get(root, key, 0);
i f (x == n u ll) return n u ll;
return (Value) x . v a l ;
}
private Node get(Node x, S trin g key, in t d)
{ // Zwracanie wartości powiązanej z kluczem z poddrzewa t r i e
// o korzeniu x.
i f (x == n u li) return n u li;
i f (d == [Link]()) return x;
char c = [Link](d); // d-ty znak klucza określa poddrzewo t r i e .
return g e t ( x . n e x t [ c ] , key, d+1);
}
public void p u t(S trin g key, Value val)
( root = put(root, key, val, 0); }
private Node put(Node x, S t r in g key, Value val, in t d)
{ // Zmiana wartości powiązanej z kluczem, j e ś l i klucz występuje
/ / w poddrzewie t r i e o korzeniu x.
i f (x == n u li) x = new Node();
i f (d == [Link]()) { [Link] = val; return x; }
char c = [Link](d); / / D o zidentyfikowania poddrzewa t r i e służy
// d-ty znak klucza.
[Link][c] = p u t(x .n e x t[ c ], key, val, d+1);
return x;
}
}
W tym kodzie do zaimplementowania tablicy symboli wykorzystano k-kierunkowe drze
wo trie. Na kilku dalszych stronach pokazano dodatkowe metody z interfejsu API tablicy
symboli przedstawionego na stronie 742. Zmodyfikowanie kodu tak, aby obsługiwał klucze
ze specjalnych alfabetów, jest proste (zobacz stronę 752). Wartość w obiekcie Node musi być
typu Object, ponieważ Java nie obsługuje tablic generycznych. W metodzie get () wartości
są rzutowane z powrotem na typ Val ue.
750 ROZDZIAŁ 5 B Łańcuchy znaków
Pobieranie kluczy Ponieważ znaki i klucze są reprezentowane w drzewa trie p o
średnio, umożliwienie klientom iterowania po kluczach jest trudne. Tak jak w binar
nych drzewach wyszukiwań, tak i tu zapisujemy klucze w postaci łańcuchów znaków
w obiektach Queue, jednak dla drzew trie trzeba utworzyć bezpośrednie reprezen
tacje wszystkich kluczy, a nie tylko znaleźć je w strukturze danych. Do tworzenia
łańcuchów służy rekurencyjna metoda prywatna col 1ect (). Jest podobna do metody
s iz e (), ale ponadto przechowuje łańcuch z ciągiem znaków ze ścieżki prowadzą
cej z korzenia. Przy każdym dojściu do węzła przez wywołanie metody col le c t( ) ,
w którym pierwszym argumentem jest ten węzeł, drugim argumentem jest łańcuch
znaków powiązany z tym węzłem (ciąg znaków ze ścieżki z korzenia do węzła).
Przy przechodzeniu do węzła dodaje
p u b lic I t e r a b le < S t r in g > keys() my powiązany z nim łańcuch znaków
{ return key sW ith Pre fix (""); } do kolejki, jeśli jego wartość jest róż
na od null, a następnie rekurencyjnie
p u b l i c I t e r a b l e < S t r i n g > k e y s W i t h P r e f i x ( S t r i n g pre)
odwiedzamy wszystkie węzły z tablicy
{
Q u e u e < S t ri n g > q = new Q u e u e < S t r i n g > ( ) ; odnośników (po jednym dla każdego
c o l l e c t ( g e t ( r o o t , p re , 0 ) , p re , q ) ; możliwego znaku). Aby w wywołaniu
r e t u r n q;
utworzyć klucz, należy dołączyć znak
odpowiadający odnośnikowi do bie
p r i v a t e v o i d c o l l e c t ( N o d e x, S t r i n g pre, żącego klucza. Metoda coli ect () słu
Q u e u e < S t r i n g > q)
ży do zapisywania kluczy w metodach
i f (x == n u l l ) r e t u r n ; keys() i keysWithPrefix() interfejsu
i f ( x . v a l != n u l l ) q.e nq u e u e ( p r e ) ; API. W implementacji metody keys ()
f o r ( c h a r c = 0; c < R; c++)
należy wywołać metodę keysWithPre-
col 1e c t ( x . n e x t [ c ] , pre + c, q);
fix() z pustym łańcuchem znaków jako
argumentem. W implementacji m eto
Zapisywanie kluczy z drzewa trie dy keysWithPrefix() trzeba wywołać
metodę get () w celu znalezienia węzła
keyswithPrefix("") ; drzewa trie, który odpowiada danemu
Klucz q
przedrostkowi (metoda zwraca nuli,
b :® T (¿) jeśli tald węzeł nie istnieje), a następnie
by by
1X ' X \~ \ użyć m etody c o li ect () do ukończenia
s \ (y > © ok ,(
se zadania. Na rysunku po lewej stronie
sea by sea @ 6 m (e )o (o )
sel Y Y )T pokazano ślad działania metody col -
s e ll © © © ! le c t( ) (lub keysWithPrefix("")) dla
s e lls by sea s e l l s X . ! X 1 X •'
sh ( s ) i Q ) i (e )7 przykładowego drzewa trie. W idoczna
she by sea s e l l s she >X !
s h e ll jest wartość klucza przekazywanego
s h e lls by sea s e l l s she s h e lls jako drugi argument i zawartość kolejki
sho
shor w każdym wywołaniu metody c o l l e
shore by sea s e l l s she s h e l l s shore
t
ct (). Rysunek w górnej części następnej
th strony to ilustracja tego procesu dla wy
the by sea s e l l s she s h e l l s shore the
wołania keysWithPrefix("sh").
Ślad D ro c e su D o b ie ra n ia k lu c z v z d rz e w a trie
5.2 a Drzewa trie 751
k e ysw ith p re fix ("
Klucz q
sh
she she
shel
shel 1
s h e lls she s h e lls
sho
shor
Wyszukiwania shore she s h e l l s shore
poddrzewa trie
dla wszystkich kluczy Pobieranie kluczy
rozpoczynających zdanegopoddrzewa trie
się od "sh"
Dopasowywanie przedrostków w drzewie trie
D opasow yw anie sym boli wieloznacznych W celu zaimplementowania metody
keysThatMatch () stosujemy podobny proces, jednak dodajemy argument określający
wzorzec dla m etody col 1 ect () i test, który pozwala sprawdzić, czy należy rekuren-
cyjnie wywołać metodę dla wszystkich odnośników (jest tak, jeśli znak we wzorcu to
symbol wieloznaczny; w przeciwnym razie wywołanie dotyczy tylko odnośnika od
powiadającego znakowi ze wzorca). Rozwiązanie pokazano w kodzie poniżej. Warto
ponadto zauważyć, że nie trzeba sprawdzać kluczy dłuższych od wzorca.
N a jd łu ższy przedrostek Aby znaleźć najdłuższy klucz będący przedrostkiem da
nego łańcucha znaków, należy użyć m etody rekurencyjnej w rodzaju g e t(), która
śledzi długość najdłuższego klucza znajdującego się na ścieżce wyszukiwania (przez
przekazywanie go jako param etru do m etody rekurencyjnej i aktualizowanie go po
napotkaniu każdego węzła o wartości różnej od nul 1). Wyszukiwanie kończy się po
natrafieniu na koniec łańcucha znaków lub odnośnik nuli (w zależności od tego,
co stanie się wcześniej).
p u b l i c I t e r a b l e < S t r i n g > k e y sT h a t M a t c h ( S t r i n g pat)
{
Q u e u e < S t ri n g > q = new Q u e u e < S t r i n g > ( ) ;
co lle ct(ro ot, pa t, q);
r e t u r n q;
}
p u b l i c v o i d c o l l e c t ( N o d e x, S t r i n g pre, S t r i n g pa t, Q u e u e < S t r i n g > q)
{
int d = p re .le n g th ();
if (x == n u l i ) return;
if (d == p a t . l e n g t h ( ) && x . v a l != n u l l ) [Link](pre);
if (d == p a t . l e n g t h O ) return;
ch a r next = p a t . c h a r A t ( d ) ;
f o r ( c h a r c = 0; c < R; C++)
if (ne xt == 1. 1 || next == c)
col 1e c t ( x . n e x t [ c ] , pre + c, p a t, q ) ;
}
Dopasowywanie symboli wieloznacznych w drzewie trie
752 ROZDZIAŁ 5 a Łańcuchy znaków
p u b l i c S t r i n g 1 o n g e s t P r e f i x O f ( S t r i n g s)
(
i n t l e n g t h = s e a r c h ( r o o t , s , 0, 0 ) ;
return s . s u b s t r in g ( 0 , length);
)
Wyszukiwanie kończy się
p r i v a t e i n t s e arc h (N o d e x, S t r i n g s , i n t d, i n t le n g th ) na końcu łańcucha znaków.
( Wartość jest różna od n u li,
if (x == n u l l ) return length; dlatego należy zwrócić she
if ([Link] != n u l l ) l e n g t h = d;
if (d == s . l e n g t h ( ) ) return leng th;
ch a r c = s . c h a r A t ( d ) ;
r e t u r n s e a r c h ( x . n e x t [ c ] , s , d+1, l e n g t h ) ; "shell"
Dopasowywanie najdłuższego przedrostka
danego łańcucha znaków
Wyszukiwanie kończy się na
końcu łańcucha znaków.
Wartość to nul 1, dlatego
Usuwanie Pierwszy krok potrzebny do usunię należy zwrócić she (jest to
ostatni klucz na ścieżce)
cia pary klucz-wartość z drzewa trie to wykorzy
stanie zwykłego wyszukiwania do znalezienia
węzła odpowiadającego danem u kluczowi i usta "shell sort"
wienia w węźle wartości nuli. Jeśli dany węzeł
obejmuje odnośnik do dziecka różny od nuli,
nie trzeba robić nic więcej. Jeżeli wszystkie od
nośniki są równe nuli, trzeba usunąć węzeł ze
struktury danych. W sytuacji, gdy w rodzicu po
tej operacji wszystkie odnośniki są równe nuli,
trzeba usunąć także rodzica itd. W implemen Wyszukiwanie kończy się
tacji na następnej stronie pokazano, że zadanie w odnośniku n ul 1. Należy
to można wykonać za pom ocą zaskakująco nie zw ró c/ćsh e lls (jestto
ostatni klucz na ścieżce)
wielkiej ilości kodu, stosując standardowy reku-
"shelters"
rencyjny schemat. Po wywołaniu rekurencyjnym
dla węzła x należy zwrócić nuli, jeśli wartość po
dana przez klienta i wszystkie odnośniki w da
nym węźle to nul 1. W przeciwnym razie należy
zwrócić x. Wyszukiwanie kończy się
w odnośniku n u li. Należy
zwrócić sh e (jest to
ostatni klucz na ścieżce)
Możliwe efekty wywołania
metody 1 ongestPref ixOf()
5.2 □ Drzewa trie 753
A lfabet a l g o r y t m 5 .4 , jak zwykle, jest p u b l i c v o i d d e l e t e ( S t r i n g key)
napisany pod kątem kluczy typu S tring { r o o t = d e l e t e ( r o o t , key, 0 ) ; }
Javy, jednak zmodyfikowanie implemen
p r i v a t e Node d e l e te ( N o d e x, S t r i n g key, i n t d)
tacji tak, aby obsługiwała klucze z dowol
{
nego innego alfabetu, jest proste. Oto, co i f (x == n u l l ) r e t u r n n u l l ;
trzeba zrobić: i f (d == k e y . l e n g t h O )
x . v a l = n u l 1;
■ Zaimplementować konstruktor, któ
else
ry jako argument przyjmuje obiekt {
Alphabet, ustawia zmienną egzem c ha r c = k e y . c h a r A t ( d ) ;
plarza typu Alphabet na wartość ar x . n e x t [ c ] = d e l e t e ( x . n e x t [ c ] , key, d+ 1) ;
}
gumentu, a zm ienną egzemplarza R
— na liczbę znaków w danym argu i f ( x . v a l != n u l l ) r e t u r n x;
mencie.
f o r ( c h a r c = 0; c < R; C++)
■ Wykorzystać w metodach g et() i f ( x . n e x t [ c ] != n u l l ) r e t u r n x;
i p u t() metodę toIndex() obiektu r e t u r n n u l 1;
Alphabet, aby przekształcić znaki }
łańcucha na indeksy z przedziału od
0 do R - 1. Usuwanie klucza (i powiązanej wartości) z drzewa trie
■ Wykorzystać metodę toChar () obiek
tu Al phabet do przekształcenia indeksów z przedziału od 0 do R - 1 na wartości
typu char. W metodach get () i put () operacja ta nie jest potrzebna, jest jednak
ważna w implementacjach m etod keys(), keysWithPrefix() i keysThatMatch ().
Dzięki tym zmianom można zaoszczędzić dużą ilość pamięci (tworząc tylko R od
nośników na węzeł), jeśli wiadomo, że klucze pochodzą z małego alfabetu. Dzieje się
to kosztem czasu potrzebnego na przekształcenia między znakami a indeksami.
de le te ("sh e lls") ;
(a ) 2 m ©o (a)2 Q ) © o
Ustawianie ( -j) ©
wartości y y i
na nuli (5 ), ( f ) ©1
Wartość jest różna od nuli, dlatego Odnośnik jest różny od nuli, dlatego
nie należy usuwać węzła (trzeba nie należy usuwać węzła (trzeba
Wartość i odnośniki to nuli, zwrócić odnośnik do niego) zwrócić oćjnośnik do niego)
dlatego należy usunąć węzeł
(i zwrócić odnośnik nuli)
Usuwanie klucza (i powiązanej wartości) z drzewa trie
754 ROZDZIAŁ 5 b Łańcuchy znaków
to zwięzła i kompletna implementacja interfejsu API dla tablicy
o m ó w io n y k o d
symboli z łańcuchami znaków, mająca wiele praktycznych zastosowań. W ćwicze
niach przedstawiono kilka odm ian i rozszerzeń implementacji. Dalej omawiamy
podstawowe cechy drzew trie i pewne ograniczenia dotyczące ich użyteczności.
Cechy drzew trie Jak zwykle, interesuje nas ilość czasu i pamięci potrzebna do
stosowania drzew trie w typowych sytuacjach. Drzewa trie gruntownie przebada
no i przeanalizowano, a ich podstawowe cechy są stosunkowo łatwe do zrozumienia
oraz wykorzystania.
Twierdzenie F. Struktura (kształt) drzewa trie nie zależy od kolejności wstawia
nia i usuwania kluczy. Dla każdego zbioru kluczy istnieje unikatowe drzewo trie.
Dowód. Wynika bezpośrednio z indukcji na poddrzewach trie.
Ten podstawowy fakt jest cechą charakterystyczną drzew trie. We wszystkich innych
omówionych do tej pory strukturach drzewiastych używanych do wyszukiwania
kształt tworzonego drzewa zależy zarówno od zbioru kluczy, jak i od kolejności ich
wstawiania.
Ograniczenia czasowe dla najgorszego przypadku p rzy wyszukiwaniu i wsta
wianiu Jak długo trwa znajdowanie wartości powiązanej z kluczem? W kontekście
drzew BST, haszowania i innych m etod opisanych w r o z d z i a l e 4 . odpowiedź na to
pytanie wymagała analiz matematycznych. Jednak w przypadku drzew trie udziele
nie odpowiedzi jest bardzo proste.
Twierdzenie G. Liczba dostępów do tablicy przy przeszukiwaniu drzewa trie
lub wstawianiu do niego klucza wynosi najwyżej 1 plus długość klucza.
Dowód. Wynika bezpośrednio z kodu. Rekurencyjne implementacje metod get ()
i put () obejmują argument d, który początkowo jest równy 0, zwiększa się w każ
dym wywołaniu i służy do zatrzymania rekurencji po dojściu do długości klucza.
Z perspektywy teoretycznej wnioskiem z t w i e r d z e n i a g jest to, że drzewa trie są
optymalne przy udanym wyszukiwaniu. Nie m ożna oczekiwać, że czas wyszukiwania
będzie rósł wolniej niż proporcjonalnie do długości klucza wyszukiwania. Niezależnie
od używanego algorytmu lub struktury danych nie m ożna bez sprawdzenia wszyst
kich znaków stwierdzić, czy znaleziono szukany klucz. W praktyce gwarancja ta jest
ważna, ponieważ nie zależy od liczby kluczy. Przy korzystaniu z kluczy 7-znakowych,
takich jak w numerach rejestracyjnych, wiadomo, że trzeba sprawdzić najwyżej 8 wę
złów, aby znaleźć lub wstawić dane. Przy stosowaniu 20-cyfrowych numerów kont
trzeba zbadać najwyżej 2 1 węzłów.
Ograniczenia oczekiwanego czasu nieudanego w yszukiw ania Załóżmy, że szuka
my klucza w drzewie trie i stwierdzamy, iż odnośnik w węźle korzenia odpowiadający
pierwszemu znakowi klucza to nuli. Wtedy przez sprawdzenie tylko jednego węzła
m ożna stwierdzić, że klucz nie znajduje się w tablicy. Sytuacja ta jest typowa. Jedną
z najważniejszych cech drzew jest to, że nieudane wyszukiwanie zwykle wymaga
sprawdzenia tylko kilku węzłów. Jeśli zakładamy, że klucze oparte są na modelu lo
sowych łańcuchów znaków (każdy znak z równym prawdopodobieństwem ma jedną
z R różnych wartości), m ożna to udowodnić.
Twierdzenie H. Średnia liczba węzłów sprawdzanych przy nieudanym wyszu
kiwaniu w drzewie trie zbudowanym dla N losowych kluczy na podstawie alfa
betu o wielkości R wynosi -lo g RN.
Zarys dow odu (dla czytelników znających analizę probabilistyczną). Prawdo
podobieństwo, że każdy z N kluczy w losowym drzewie trie różni się od losowego
klucza wyszukiwania przynajmniej jednym z początkowych t znaków, wynosi
(1 - R ‘)N. Odjęcie tej wartości od 1 wyznacza prawdopodobieństwo, że jeden
z kluczy w drzewie trie pasuje do klucza wyszukiwania we wszystkich począt
kowych t znakach. Oznacza to, że 1 - (1 - R'')N to prawdopodobieństwo, iż przy
wyszukiwaniu potrzebnych będzie więcej niż t porównań znaków. Z analizy pro
babilistycznej wiadomo, że dla t = 0, 1 , 2 ... suma prawdopodobieństw, iż losowa
zmienna całkowitoliczbowa jest większa od t, to średnia wartość losowej zm ien
nej. Tak więc średni koszt wyszukiwania wynosi:
1 - (1 - R ' i) N + 1 - (1 - R - 2) N + . . . + 1 - (1 - R ‘) N + -
Wykorzystując podstawowe przybliżenie (1 - l/x )x ~ e'1, stwierdzamy, że koszt
wyszukiwania wynosi mniej więcej:
(1 - e~NIR' ) + (1 - e~NIRl) + ... + (1 - e-N,R' ) +...
Składniki sumy są niezwykle bliskie 1 dla około lnRN wyrazów, gdzie R jest zna
cząco mniejsze niż N. Dla wszystkich wyrazów z R‘ wyraźnie większym niż N
wartości są niezwykle bliskie 0. Dla nielicznych wyrazów, w których R‘ ~ N, war
tości należą do przedziału od 0 do 1. Tak więc łączna suma wynosi około log AT.
W praktyce najważniejszym wnioskiem z przedstawionego dowodu jest to, że przy
nieudanym wyszukiwaniu długość klucza nie ma znaczenia. Przykładowo, zgodnie
z dowodem nieudane wyszukiwanie w drzewie zbudowanym na podstawie miliona
losowych kluczy wymaga sprawdzenia tylko trzech lub czterech węzłów niezależnie
od tego, czy kluczami są 7-cyfrowe num ery tablic rejestracyjnych, czy 20-cyfrowe
num ery kont. Choć nierozsądne jest oczekiwanie, że w praktyce wystąpią naprawdę
losowe klucze, można postawić hipotezę, że model odzwierciedla działanie algoryt
mów przetwarzania drzew trie dla kluczy w typowych zastosowaniach. Rzeczywiście,
756 ROZDZIAŁ 5 a Łańcuchy znaków
działanie tego rodzaju jest często spotykane w praktyce i stanowi ważny powód po
wszechnego stosowania drzew trie.
Pamięć Ile pamięci potrzeba na drzewo trie? Udzielenie odpowiedzi na to pytanie
(i ustalenie, jaka ilość pamięci jest dostępna) jest kluczowe, jeśli chcemy z powodze
niem korzystać z drzew trie.
Twierdzenie I. Liczba odnośników w drzewie trie wynosi pomiędzy RN a RNw,
gdzie w to średnia długość klucza.
Dowód. Dla każdego klucza w drzewie trie istnieje węzeł obejmujący powiąza
ną z tym kluczem wartość i R odnośników, tak więc liczba odnośników wynosi
co najmniej RN. Jeśli pierwsze znaki wszystkich kluczy są różne, istnieje węzeł
o R odnośnikach dla każdego znaku klucza, tak więc liczba odnośników to R razy
łączna liczba znaków klucza (czyli RNw).
W tabeli na następnej stronie pokazano koszty w typowych zastosowaniach, które
omawiamy. Z tabeli wynikają następujące praktyczne reguły dotyczące drzew trie:
■ Jeśli klucze są krótkie, liczba odnośników jest bliska RN.
■ Jeżeli klucze są długie, liczba odnośników jest bliska RNw.
• Tak więc zmniejszenie R pozwala zaoszczędzić bardzo dużą ilość pamięci.
Bardziej skomplikowanym wnioskiem utt«shens" i)-
z tabeli jest to, że należy zrozumieć cechy p u t c s h e iif is h " , 2);
wstawianych kluczy przed zastosowaniem Standardowe Bez jednokierunkowych
drzewo trie gałęzi
drzew trie.
Jednokierunkow e gałęzie Podstawowy
powód, dla którego potrzebna jest tak
duża ilość pamięci dla drzew trie z długi
mi kluczami, jest to, że takie klucze czę
sto powodują powstawanie długich „ogo
nów”. Każdy węzeł ma wtedy jeden odnoś
nik do następnego węzła (a tym samym
R - 1 odnośników nuli). Problem m oż
na łatwo rozwiązać (zobacz ć w i c z e n i e
5 . 2 .1 1 ). Drzewo trie może też obejmować
wewnętrzne jednokierunkowe gałęzie.
Przykładowo, dwa długie klucze mogą
być sobie równe z wyjątkiem ostatniego
znaku. Jest to nieco trudniejszy problem
(zobacz ć w i c z e n i e 5 .2 . 1 2 ). Zmiany mogą
sprawić, że ilość pamięci na drzewo trie
stanie się mniej istotnym czynnikiem niż Usuwanie jednokierunkowych gałęzi z drzewa trie
5.2 a Drzewa trie 757
w prostych, omówionych implementacjach, jednak w praktyce rozwiązania nie za
wsze są skuteczne. Dalej przedstawiamy inny sposób zmniejszenia ilości pamięci zaj
mowanej przez drzewa trie.
W p o d s u m o w a n iu można stwierdzić, że nie należy próbować stosować a l g o r y t m u
5.4 dla dużej liczby długich kluczy opartych na dużych alfabetach, ponieważ wymaga
nia pamięciowe wynoszą wtedy R razy łączna liczba znaków kluczy. W innych sytua
cjach, kiedy dostępna jest potrzebna ilość pamięci, trudno uzyskać wydajność lepszą
niż zapewniana przez drzewa trie.
Liczba odnośników w drzewie
Średnia Wielkość
Zastosowanie Typowy klucz trie zbudowanym na podstawie
długość (w) alfabetu (/?)
miliona kluczy
Kalifornijskie numery
4PGC938 7 256 256 milionów
tablic rejestracyjnych
256 4 miliardy
Num ery kont 024000199929932 99111 20
10 256 milionów
Adresy URL [Link] 28 256 4 miliardy
Przetwarzanie tekstu s e a sh e lls 11 256 256 milionów
Proteiny w danych 256 256 milionów
ACTGACTG 8
opisujących genom 4 4 miliony
Pamięć potrzebna na typowe drzewa trie
758 ROZDZIAŁ 5 Łańcuchy znaków
Trójkowe drzewa wyszukiwań
(drzewa TST) Aby uniknąć nad
miernych kosztów pamięciowych zwią
zanych z R-kierunkowymi drzewami
trie, m ożna wykorzystać inną repre
zentację — trójkowe drzewa wyszuki
wań (ang. ternary search trie — TST).
W drzewie TST każdy węzeł obejmuje
znak, trzy odnośniki i wartość. Trzy od
nośniki odpowiadają kluczom, w któ
rych przetwarzany znak jest mniejszy,
równy lub większy względem znaku Odnośnik do drzewa TST Odnośnik do drzewa TST
z danego węzła. W R-kierunkowym z wszystkimi kluczami z wszystkimi kluczami
rozpoczynającymi się od rozpoczynającymi się od s
drzewie trie ( a l g o r y t m 5 .4 ) węzły
drzewa są reprezentowane przez R od
nośników, a znak odpowiadający każ
demu odnośnikowi różnem u od nuli
jest pośrednio reprezentowany przez
indeks. W analogicznym drzewie TST
znaki występują w węzłach bezpośred
nio. Znaki odpowiadające kluczom
można znaleźć tylko przy podążaniu za
środkowymi odnośnikami.
W yszukiw anie i w staw ianie Kod do
wyszukiwania i wstawiania w imple
mentacji interfejsu API tablicy symboli
get("sea")
Dopasowanie - należy wybrać opartej na drzewach TST „sam się pisze”.
Niedopasowanie - należy środkowy odnośnik i przejść Przy wyszukiwaniu należy porównać
wybrać lewy lub prawy do następnego znaku
odnośnik bez przechodzenia pierwszy znak klucza ze znakiem z korze
do następnego znaku nia. Jeśli znak z klucza jest mniejszy, nale
ży podążyć za lewym odnośnikiem. Jeżeli
znaki są równe, trzeba wybrać środkowy
odnośnik i przejść do następnego znaku
Iducza wyszukiwania. W obu sytuacjach
algorytm jest stosowany rekurencyjnie.
Wyszukiwanie kończy się niepowodzeniem,
(1 ) © 00 (e)
jeśli napotkano odnośnik nul 1 lub gdy wę
T T T T
Zwracanie wartości ( s ) i i ( T ) (¿ )? (1 ) zeł, w którym zakończono poszukiwania,
powiązanej z ostatnim / p /T /p 1['
znakiem klucza X 0. ma wartość nuli. Jeżeli węzeł, w którym
T T zakończono proces, ma wartość różną od
Przykładowe przeszukiwanie drzewa TST nuli, wyszukiwanie kończy się powodze-
5.2 Drzewa trie 759
ALGORYTM 5.5. Tablica symboli oparta na drzewach TST
public c la s s TST<Value>
f
prívate Node root; // Korzeń drzewa t r i e .
prívate c la s s Node
{
char c; // Znak.
Node l e f t , mid, rig h t; // Lewe, środkowe i prawe poddrzewo t r i e .
V alue val ; // Wartość powiązana z łańcuchem znaków.
}
public Value get(String key) // Taka sama, jak dla drzew t r i e (strona 749).
private Node get(Node x, S trin g key, in t d)
{
i f (x == n u li) return n u li;
char c = [Link](d);
if (c < x.c) return g e t ( x . l e f t , key, d ) ;
e lse i f (c > x.c) return g e t ( x . r ig h t , key, d ) ;
e lse i f (d < [Link]() - 1)
return get([Link], key, d+1);
e lse return x;
}
p ublic void p u t(S trin g key, Value val)
( root = put(root, key, val, 0); }
p rivate Node put (Node x, S trin g key, Value val, in t d)
(
char c = [Link](d);
i f (x == n u li) ( x = new Node(); x.c = c; }
if (c< x.c) x .le ft = p u t ( x . le f t , key,val, d) ;
else i f (c> x.c) x . r ig h t = put(x. rig h t, key,val, d) ;
else i f (d< [Link]() - 1)
[Link] = put([Link], key, val, d+1);
e l se x . val =val ;
return x; ___
}
}
W tej implementacji użyto wartości c typu char i trzech odnośników na węzeł do utworzenia
drzewa trie do wyszukiwania łańcuchów znaków, w którym poddrzewa trie obejmują klucze
0 pierwszym znaku mniejszym niż c (lewe poddrzewo), równym c (środkowe poddrzewo)
1większym niż c (prawe poddrzewo).
760 ROZDZIAŁ 5 □ Łańcuchy znaków
niem. Aby wstawić nowy klucz, należy przeszukać dane, a następnie dodać nowe wę
zły dla znaków z „ogona” klucza, tak jak w drzewach trie. Szczegółowe implementa
cje metod pokazano w a l g o r y t m i e 5 .5 .
To rozwiązanie jest odpowiednikiem zaimplementowania każdego węzła ^-kie
runkowego drzewa trie jako drzewa wyszukiwań binarnych, w którym za klucze słu
żą znaki odpowiadające odnośnikom różnym od nul 1. W a l g o r y t m i e 5.4 wykorzy
stano tablicę indeksowaną kluczami. Drzewo TST i odpowiadające m u drzewo trie
pokazano powyżej. Przez nawiązanie do opisanej w r o z d z i a l e 3 . analogii między
drzewami wyszukiwań binarnych a algorytmami sortowania m ożna stwierdzić, że
drzewa TST odpowiadają sortowaniu szybkiemu łańcuchów znaków z podziałem na
trzy części w taki sam sposób, jak drzewa BST odpowiadają sortowaniu szybkiemu,
a drzewa trie — metodzie MSD. Na rysunkach na stronach 726 i 733 pokazano struk
turę wywołań rekurencyjnych w metodzie MSD i sortowaniu szybkim łańcuchów
znaków z podziałem na trzy części. Rysunki te odpowiadają drzewom trie i TST dla
tego samego zbioru kluczy, przedstawionym na stronie 758. Pamięć na odnośniki
w drzewach trie odpowiada pamięci na liczniki w sortowaniu łańcuchów znaków.
Rozgałęzianie na trzy części zapewnia skuteczne rozwiązanie obu problemów.
Standardowa tablica odnośników (/?= 26) Drzewo TST
się od su
Reprezentacje węzłów drzew trie
5.2 n Drzewa trie 761
Cechy drzew TST Drzewo TST to zwięzła reprezentacja R-kierunkowego
drzewa trie, jednak te dwie struktury danych mają zaskakująco odm ienne cechy.
Prawdopodobnie najważniejszą różnicą jest to, że CECHA A nie jest spełniona dla
drzew TST. Reprezentacje drzew BST dla każdego węzła drzewa trie zależą tu od ko
lejności wstawiania kluczy, tak jak w każdym innym drzewie BST.
Pam ięć Najważniejszą cechą drzew TST jest to, że każdy węzeł obejmuje tylko trzy
odnośniki, dlatego drzewo TST wymaga znacznie mniej pamięci niż odpowiadające
mu drzewo trie.
Twierdzenie J. Liczba odnośników w drzewie TST zbudowanym na podstawie
N kluczy w postaci łańcuchów znaków o średniej długości w wynosi pomiędzy
3N a 3Nw.
Dowód. Natychmiast wynika z tego samego wnioskowania, co w t w i e r d z e n i u i.
Rzeczywisty poziom wykorzystania pamięci jest przeważnie niższy niż górne ogra
niczenie trzech odnośników na znak, ponieważ klucze o wspólnych przedrostkach
współużytkują węzły na wysokich poziomach drzewa.
K oszt w yszukiw ania Aby ustalić koszt wyszukiwania (i wstawiania) danych w drze
wach TST, należy pomnożyć koszt dla powiązanego drzewa trie przez koszt porusza
nia się w reprezentacji BST każdego węzła drzewa trie.
Twierdzenie K. Nieudane wyszukiwanie w drzewie TST zbudowanym z N lo
sowych kluczy w postaci łańcuchów znaków wymaga średnio ~ln N porównań
znaków. Udane wyszukiwanie lub wstawianie w drzewach TST wymaga jednego
porównania znaku na każdy znak z klucza wyszukiwania.
Dowód. Koszt udanego wyszukiwania i wstawiania wynika bezpośrednio
z kodu. Koszt nieudanego wyszukiwania m ożna wyznaczyć na podstawie ar
gumentów omówionych w zarysie dowodu t w i e r d z e n i a h . Zakładamy, że na
ścieżce wyszukiwania wszystkie węzły oprócz ich stałej liczby (kilku węzłów
w górnej części) funkcjonują jak losowe drzewa BST dla R wartości znaków.
Średnia długość ścieżki wynosi In R, dlatego należy pomnożyć koszty czasowe
log((N = ln NIln R przez ln R.
W najgorszym przypadku węzeł może obejmować wszystkie R odnośników i być nie-
zbalansowany (rozciągnięty jak lista powiązana), dlatego należy pomnożyć wartość
przez R. W bardziej typowych sytuacjach m ożna oczekiwać ln R lub mniejszej liczby
porównań znaków na pierwszym poziomie (ponieważ węzeł korzenia działa jak loso
we drzewo BST dla R różnych wartości znaków) i czasem na kilku innych poziomach
(jeśli występują klucze o wspólnym przedrostku i do R różnych wartości znaku po
762 ROZDZIAŁ 5 □ Łańcuchy znaków
przedrostku). Ponadto dla większości znaków potrzebnych jest tylko kilka porównań
(ponieważ w większości węzłów w drzewach trie liczba wartości różnych od nuli
jest niewielka). Nieudane wyszukiwanie przeważnie wymaga tylko kilku porównań
znaków i kończy się odnośnikiem nuli w górnej części drzewa trie, a udane wyszu
kiwanie obejmuje tylko około jednego porównania na znak klucza wyszukiwania,
ponieważ większość znaków znajduje się w węzłach z jednokierunkowym i gałęziami
w dolnej części drzewa trie.
A lfabet Główną korzyścią ze stosowania drzew TST jest to, że płynnie dostosowują
się do nieregularności w kluczach wyszukiwania (takie nieregularności często wy
stępują w praktyce). Zauważmy, że nie ma powodu, aby umożliwiać tworzenie łań
cuchów znaków na podstawie alfabetu określonego przez klienta, co było niezwykle
ważne w przypadku drzew trie. Występują tu dwa główne efekty. Po pierwsze, klucze
w praktyce są oparte na dużych alfabetach, a częstotliwość występowania znaków
ze zbiorów jest daleka od równomiernej. W drzewach TST m ożna korzystać z 256-
znakowego kodowania ASCII lub 65 536-znakowego kodowania Unicode. Nie trzeba
się przy tym martwić o nadm ierne koszty węzłów o 256 lub 65 536 gałęziach ani
określać, które zbiory znaków są potrzebne. Łańcuchy znaków Unicode w alfabetach
niełacińskich mogą obejmować tysiące znaków. Drzewa TST wyjątkowo dobrze n a
dają się dla standardowych kluczy typu S tri ng Javy składających się z takich znaków.
Po drugie, klucze w praktycznych zastosowaniach często mają ustrukturyzowany for
mat, różny w poszczególnych aplikacjach. Czasem w jednej części klucza stosowane
są tylko litery, a w innej — same cyfry. W numerach kalifornijskich tablic rejestra
cyjnych drugi, trzeci i czwarty znak to duże litery (R = 26), a pozostałe znaki to cyfry
dziesiętne (R = 10). W drzewie TST dla talach kluczy niektóre węzły drzewa trie będą
reprezentowane jako 10-węzłowe drzewa BST (w miejscach, w których we wszyst
kich kluczach występują cyfry), a inne — jako 26-węzłowe drzewa BST (w miejscach,
gdzie we wszystkich kluczach są litery). Ta struktura powstaje automatycznie, bez
konieczności przeprowadzania specjalnych analiz kluczy.
Dopasowywanie przedrostków, pobieranie kluczy i dopasowywanie do symboli
wieloznacznych Ponieważ drzewo TST jest reprezentacją drzewa trie, implementacje
metod longestPrefixOf(), keys(), keysWithPrefix() i keysThatMatch() można łatwo
zaadaptować z analogicznego kodu dla drzew trie z poprzedniego podrozdziału. Jest
to wartościowe ćwiczenie, które pozwala utrwalić wiedzę na temat drzew trie i TST
(zobacz ć w i c z e n i e 5 .2 .9 ). Występują tu te same wady i zalety, co przy wyszukiwaniu
(rosnąca liniowo ilość pamięci, ale dodatkowy czynnik ln R na porównanie znaków).
Usuwanie Opracowanie m etody d e le te () dla drzew TST wymaga więcej pracy.
Każdy znak w usuwanym kluczu należy do drzewa BST. W drzewie trie m ożna usu
nąć odpowiadający znakowi odnośnik przez ustawienie odpowiedniego wpisu w tab
licy odnośników na nul 1. W drzewie TST usunięcie węzła odpowiadającego znakowi
wymaga usuwania węzłów z drzewa BST.
5.2 □ Drzewa trie 763
H ybrydow e drzew a T ST Łatwym usprawnieniem wyszukiwania w drzewach TST
jest zastosowanie dużego węzła z wieloma bezpośrednimi odnośnikami. Najprostsze
rozwiązanie to przechowywanie tablicy R drzew TST — po jednym na każdą możli
wą wartość pierwszego znaku kluczy. Jeśli R nie jest duże, m ożna zrobić to dla dwóch
pierwszych liter kluczy (i zastosować tablicę o wielkości R2). Aby ta m etoda była sku
teczna, początkowe znaki w kluczach muszą być równomiernie rozłożone. Algorytm
wyszukiwania hybrydowego odpowiada tu sposobowi wyszukiwania przez ludzi
nazwisk w książce telefonicznej. Pierwszy krok to wybór spośród wielu wartości
(„No tak, zacznijmy od A”), po czym następują wybory spośród dwóch możliwości
(„Jest przed Andrzejewski, ale po Abakanowicz”) i sekwencyjne dopasowywanie zna
ków („Aleksiejczuk — nie, nie ma nazwiska Algorytmy, ponieważ żadne nie zaczyna
się od Alg”). Programy tego rodzaju należą do najszybszych w zakresie wyszukiwania
kluczy w postaci łańcuchów znaków.
Jednokierunkow e gałęzie Dla drzew TST, podobnie jak dla drzew trie, m oż
na usprawnić wykorzystanie pamięci, umieszczając klucze w liściach w miejscach,
w których klucze są jednoznaczne, i usuwając jednokierunkowe gałęzie między wę
złami wewnętrznymi.
Twierdzenie L. Wyszukiwanie lub wstawianie w drzewach TST zbudowanych
z N losowych kluczy w postaci łańcuchów znaków bez zewnętrznych jednokie
runkowych gałęzi i z R‘ gałęziami w korzeniu średnio wymaga około In N - t ln R
porównań znaków.
Dowód. Te ogólne szacunki wynikają z tego samego rozumowania, które prze
prowadziliśmy, aby udowodnić t w i e r d z e n i e k . Zakładamy, że na ścieżce wy
szukiwania wszystkie oprócz stałej liczby węzłów (kilku w górnej części) funk
cjonują jak losowe drzewa BST dla R wartości znaków, dlatego koszty czasowe
należy pomnożyć przez ln R.
m im o p o k u s y dostrajania algorytmu w celu zmaksymalizowania wydajności nie na
leży zapominać o tym, że jedną z najatrakcyjniejszych cech drzew TST jest to, iż
zwalniają z konieczności uwzględniania specyfiki aplikacji i często zapewniają wyso
ką wydajność bez żadnych modyfikacji.
764 ROZDZIAŁ 5 □ Łańcuchy znaków
K tórej im p le m e n t a c j i ta b lic y s y m b o li z ła ń c u c h a m i z n a k ó w p o w i
n ie n e m u ży w a ć ? Tak jak przy sortowaniu łańcuchów znaków, tak i tu interesuje
nas wydajność omówionych m etod przeszukiwania łańcuchów znaków w porów na
niu z m etodam i do użytku ogólnego, opisanymi w r o z d z i a l e 3 . W poniższej tabeli
podsumowano ważne cechy algorytmów omówionych w tym podrozdziale (dla po
równania dołączono wiersze dotyczące drzew BST, czerwono-czarnych drzew BST
i haszowania z r o z d z i a ł u 3 .). W konkretnych zastosowaniach wartości te należy
traktować jako ogólne, a nie precyzyjne, ponieważ przy analizowaniu implementacji
tablic symboli rolę odgrywa bardzo wiele czynników (na przykład cechy kluczy i wy
konywane operacje).
Typowe tempo wzrostu dla N łańcuchów
znaków o średniej długości w opartych
Algorytm (struktura na fl-znakowym alfabecie
Najlepszy dla
danych) Znaki sprawdzane
Wykorzystywana
przy nieudanym
pamięć
wyszukiwaniu
Losowo
Drzewa BST cl dg W 64N
uporządkowane klucze
Drzewa wyszukiwań
2-3 (czerwono-czarne c2 (lgN )2 64N Gwarancje wydajności
drzewa BST)
Typy wbudowane
Próbkowanie liniowe i przechowywanie
W Od 32N do 128N
(tablice równoległe) skrótów w pamięci
podręcznej
Przeszukiwanie drzew
Od (8R+56JN Krótkie klucze i małe
trie (R-kierunkowe lo g rN
drzewa trie)
do (8R+56)Nw alfabety
Przeszukiwanie drzew Od 64N do
1,39 Ig N Klucze nielosowe
trie (drzewa TST) 64Mv
Wydajność algorytmów przeszukiwania łańcuchów znaków
Jeśli dostępna jest odpowiednia ilość pamięci, najszybciej działają R-kierunkowe
drzewa trie. W zasadzie wykonują zadanie za pomocą stałej liczby porównań zna
ków. Dla dużych alfabetów, kiedy może brakować pamięci potrzebnej do zastoso
wania R-kierunkowych drzew trie, lepsze są drzewa TST, ponieważ wymagają loga
rytmicznej liczby porównań znaków (drzewa BST wymagają logarytmicznej liczby
porównań kluczy). Haszowanie pozwala uzyskać porównywalną wydajność, jednak
nie zapewnia obsługi operacji na uporządkowanej tablicy symboli ani operacji z roz
szerzonego interfejsu API, takich jak dopasowywanie przedrostków lub symboli wie
loznacznych.
5.2 □ Drzewa trie
P Y T A N IA I O D P O W IE D Z I
P. Czy w sortowaniu systemowym w Javie wykorzystano jedną z opisanych m etod
do wyszukiwania lduczy typu S tri ng?
O. Nie.
766 ROZDZIAŁ 5 ■ Łańcuchy znaków
] Ć W IC Z E N IA
5.2.1. Narysuj jR-kierunkowe drzewo trie uzyskane przez wstawienie poniższych
kluczy w podanej kolejności do początkowo pustego drzewa tego rodzaju (nie rysuj
odnośników nul 1).
no i s th t i fo al go pe to co to th ai of th pa
5.2.2. Narysuj drzewo TST utworzone w wyniku wstawienia poniższych kluczy
w podanej kolejności do początkowo pustego drzewa tego rodzaju.
no i s th t i fo al go pe to co to th ai of th pa
5.2.3. Narysuj ^-kierunkowe drzewo trie uzyskane przez wstawienie poniższych
kluczy w podanej kolejności do początkowo pustego drzewa tego rodzaju (nie rysuj
odnośników nul 1).
now i s the time fo r a ll good people to come to the aid of
5.2.4. Narysuj drzewo TST utworzone w wyniku wstawienia poniższych kluczy
w podanej kolejności do początkowo pustego drzewa tego rodzaju.
now i s the time fo r a ll good people to come to the aid of
5.2.5. Opracuj nierekurencyjne wersje klas Tri eST i TST.
5.2.6. Zaimplementuj poniższy interfejs API typu danych S tri ngSET.
pu b lic c l a s s StringSET
Strin gSE T () Tworzy zbiór łańcuchów znaków
v o i d a d d ( S t r i n g key) Umieszcza klucz key w zbiorze
v o i d d e l e t e ( S t r i n g key) Usuwa klucz key ze zbioru
boolean c o n t a i n s ( S t r i n g key) Czy klucz key znajduje się w zbiorze?
bo ole an i s E m p t y O Czy zbiór jest pusty?
in t size () Zwraca liczbę kluczy zapisanych w zbiorze
int t o S trin g O Zwraca reprezentację zbioru w postaci łańcucha znaków
Interfejs API typu danych dla zbiorów łańcuchów znaków
5.2 0 Drzewa trie
1 PROBLEMY DO ROZWIĄZANIA
5.2.7. Pusty łańcuch znaków w drzewie TST. Kod dla drzew TST nie obsługuje pra
widłowo pustych łańcuchów znaków. Wyjaśnij problem i zaproponuj poprawkę.
5.2.8. Operacje na danych uporządkowanych w drzewach trie. Zaimplementuj m eto
dy floor(), cei 1 (), rank() i sel e c t() (ze standardowego interfejsu API ST dla danych
uporządkowanych, opisanego w r o z d z i a l e 3 .) dla klasy Tri eST.
5.2.9. Dodatkowe operacje dla drzew TST. Zaimplementuj metodę keys() i dodat
kowe metody przedstawione w tym podrozdziale — longestPrefixOf(), keysWith-
Prefix() i keysThatMatch() — dla typu TST.
5.2.10. Określanie wielkości. Zaimplementuj wysoce zachłanną wersję metody
s i ze () (przechowującą w każdym węźle liczbę kluczy w danym poddrzewie) dla klas
TrieST i TST.
5.2.11. Zewnętrzne jednokierunkowe gałęzie. Dodaj do klas Tri eST i TST kod, który
wyeliminuje zewnętrzne jednokierunkowe gałęzie.
5.2.12. Wewnętrzne jednokierunkowe gałęzie. Dodaj do ldas Tri eST i TST kod, który
wyeliminuje wewnętrzne jednokierunkowe gałęzie.
5.2.13. Hybrydowa klasa TST z R2gałęziami w korzeniu. Dodaj do Masy TST kod do
tworzenia wielu gałęzi na dwóch pierwszych poziomach (co opisano w tekście).
5.2.14. Unikatowepodłańcuchy o długości L. Napisz korzystającego z Masy TST Mien-
ta, który wczytuje tekst ze standardowego wejścia i określa liczbę unikatowych pod-
łańcuchów o długości L. PrzyMadowo, jeśli dane wejściowe to cgcgggcgcg, występuje
pięć unikatowych podłańcuchów o długości 3 — cgc, cgg, gcg, ggc i ggg. Wskazówka:
wykorzystaj metodę substring ( i , i + L) dla łańcuchów znaków do wyodrębnienia
i -tego podłańcucha, a następnie wstaw go do tablicy symboli.
5 .2.15. Unikatowe podłańcuchy. Napisz korzystającego z Masy TST Mienta, który
wczytuje tekst ze standardowego wejścia i określa liczbę różnych podłańcuchów
0 dowolnej długości. Można to zrobić w bardzo wydajny sposób za pomocą drzewa
przyrostków (zobacz r o z d z i a ł 6.).
5 .2.16. Podobieństwo dokumentów. Napisz korzystającego z Masy TST Mienta z m e
todą statyczną, która przyjmuje jako argumenty wiersza poleceń wartość L typu i nt
1 nazwy dwóch plików, a następnie określa L-podobieństwo między dokumentami,
czyli odległość euldidesową między wektorami częstotliwości wyznaczonymi przez
liczbę wystąpień każdego trigram u podzieloną przez liczbę trigramów. Dodaj m e
todę statyczną main(), która przyjmuje wartość L typu in t jako argument wiersza
poleceń i listę nazw plików ze standardowego wejścia, a następnie wyświetla macierz
L-podobieństwa dla wszystkich par dokumentów.
768 ROZDZIAŁ 5 o Łańcuchy znaków
PROBLEMY DO ROZWIĄZANIA (ciąg dalszy)
5.2.17. Sprawdzanie pisowni. Napisz korzystającego zklasy TST klienta Spel IChecker,
który jako argument wiersza poleceń przyjmuje nazwę pliku zawierającego słownik
słów angielskich, a następnie wczytuje łańcuch znaków ze standardowego wejścia
i wyświetla każde słowo, które nie występuje w słowniku. Wykorzystaj zbiór łańcu
chów znaków.
5.2.18. Biała lista. Napisz korzystającego z klasy TST klienta, który rozwiązuje prob
lem przedstawiony w p o d r o z d z i a l e i . i i ponownie omówiony w p o d r o z d z i a l e
3.5 (zobacz stronę 503).
5.2.19. Losowe numery telefonów. Napisz korzystającego z klasy TrieST klienta
(przy R = 10), który jako argument wiersza poleceń przyjmuje wartość N typu i nt
i wyświetla Nlosowych numerów telefonów w postaci (xxx) xxx-xxxx. Wykorzystaj
tablicę symboli, aby uniknąć wyboru tego samego num eru więcej niż raz. W celu
pominięcia nieprawdziwych numerów kierunkowych zastosuj plik [Link]
z poświęconej książce witryny.
5.2.20. Metoda containsPrefix(). Dodaj do typu StringSET (zobacz ć w i c z e n i e
5 .2 .6) metodę containsPrefix(). Metoda ma przyjmować łańcuch znaków s jako
dane wejściowe i zwracać true, jeśli w zbiorze występuje łańcuch znaków, którego s
jest przedrostkiem.
5.2.21. Dopasowywanie łańcuchów znaków. Dla listy krótkich łańcuchów znaków
należy zapewnić obsługę zapytań, w których użytkownik podaje łańcuch znaków
s, aby otrzymać wszystkie łańcuchy z listy obejmujące s. Zaprojektuj interfejs API
na potrzeby tego zadania i opracuj implementację w postaci klienta korzystającego
z klasy TST. Wskazówka: wstaw do drzewa TST przyrostki każdego słowa (na przy
kład s t r i ng, t r i ng, ri ng, i ng, ng, g).
5.2.22. Małpy przy maszynie. Załóżmy, że małpy, pisząc na maszynie, tworzą losowe
słowa przez dodawanie do bieżącego słowa 26 możliwych liter z prawdopodobień
stwem p i kończenie słowa z prawdopodobieństwem 1 - 26p. Napisz program do
oszacowania rozkładu długości uzyskanych słów. Jeśli ciąg "abc" zostanie wygenero
wany więcej niż raz, i tak należy liczyć go jednokrotnie.
5.2 o Drzewa trie 769
! EKSPERYM ENTY
5.2.23. Powtórzenia (ponownie). Ponownie wykonaj ć w i c z e n i e 3 . 5 .30 . Tym razem
wykorzystaj typ StringSET (zobacz ć w i c z e n i e 5 . 2 .6) zamiast HashSET. Porównaj
czasy wykonania obu rozwiązań. Następnie zastosuj typ Dedup do przeprowadzenia
eksperymentów dla N = 107, 108 i 109. Powtórz eksperymenty dla losowych wartości
typu 1 ong i omów wyniki.
5.2.24. Sprawdzanie pisowni. Ponownie wykonaj ć w i c z e n i e 3 .5 .3 1 , w którym wy
korzystano plik [Link] z poświęconej książce witryny i klienta BlackFil t e r ze
strony 503 do wyświetlenia wszystkich błędnie napisanych słów z pliku tekstowego.
Za pom ocą tego klienta porównaj wydajność typów TrieST i TST dla pliku [Link]
i omów wyniki.
5.2.25. Słownik. Ponownie wykonaj ć w i c z e n i e 3 .5 .3 2 . Zbadaj wydajność klienta
w rodzaju LookupCSV (za pomocą klas TrieST i TST) w sytuacji, w której wydajność
ma znaczenie. Zaprojektuj scenariusz generowania zapytań, zamiast przyjmować po
lecenia ze standardowego wejścia, i przeprowadź testy wydajności dla dużych danych
wejściowych i dużej liczby zapytań.
5.2.26. Indeksowanie. Ponownie wykonaj ć w i c z e n i e 3 .5 .3 3 . Zbadaj klienta w ro
dzaju LookupIndex (za pomocą klas TrieST i TST) w sytuacji, w której wydajność ma
znaczenie. Zaprojektuj scenariusz generowania zapytań, zamiast przyjmować pole
cenia ze standardowego wejścia, i przeprowadź testy wydajności dla dużych danych
wejściowych i dużej liczby zapytań.
j e d n ą z p o d s t a w o w y c h o p e r a c j i na łańcuchach znaków jest wyszukiwanie pod-
łańcuchów. Na podstawie tekstu o długości N i wzorca o długości M należy znaleźć
wystąpienia wzorca w tekście. Większość algorytmów rozwiązujących ten problem
m ożna łatwo rozwinąć, tak aby znajdowały wszystkie wystąpienia wzorca w tekście,
zliczały je lub udostępniały kontekst (podłańcuchy tekstu otaczające każde wystąpie
nie wzorca).
Wyszukiwanie słowa w edytorze tekstu lub wyszukiwarce oparte jest na wyszuki
waniu podłańcucha. Pierwotnym celem prac nad rozwiązaniem omawianego proble
m u było zapewnienie obsługi wyszukiwania. Innym klasycznym zastosowaniem jest
wyszukiwanie ważnych wzorców w przechwyconych wiadomościach. Dla dowódcy
wojskowego ważne może być znalezienie wzorca ATAK 0 ŚWICIE w przechwyconym
tekście. Hakera może interesować wzorzec Hasło: w pamięci komputera. We współ
czesnym świecie użytkownicy często przeszukują duże ilości informacji dostępnych
w sieci WWW.
Aby docenić opisane tu algorytmy, warto przyjąć, że szukane wzorce są stosunko
wo krótkie (M równe 100 lub 1000), a tekst — stosunkowo długi ( N równe milion lub
miliard). Przy wyszukiwaniu podłańcuchów zwykle wzorzec jest wstępnie przetwa
rzany, co m a umożliwiać szybkie wyszukiwanie wzorca w tekście.
Wyszukiwanie podłańcuchów to ciekawy i klasyczny problem. Odkryto kilka
bardzo różnych (i zaskakujących) algorytmów, które nie tylko udostępniają szereg
przydatnych praktycznych metod, ale też stanowią ilustrację różnych podstawowych
technik projektowania algorytmów.
Wzorzec — ► N E E D L E
Tekst — - I N A H A Y S T A C K N E E D L E I N A
Dopasowanie
Wyszukiwanie podłańcuchów
770
5.3 n Wyszukiwanie podiańcuchów 771
Krótka historia Omawiane algorytmy mają ciekawą historię. Przedstawiamy ją
w tym miejscu, aby pom óc zrozumieć kontekst dla różnych metod.
Istnieje prosty, oparty na ataku siłowym algorytm do wyszukiwania łańcuchów
znaków. Jest on powszechnie stosowany. Choć czas wykonania dla najgorszego przy
padku jest proporcjonalny do M N, łańcuchy znaków występujące w wielu zastoso
waniach prowadzą do czasu wykonania proporcjonalnego do M + N (wyjątkiem są
„patologiczne” sytuacje). Ponadto rozwiązanie jest dobrze dostosowane do standar
dowych cech architektury większości systemów komputerowych, dlatego zoptymali
zowana wersja jest punktem odniesienia trudnym do poprawienia nawet za pom ocą
pomysłowych algorytmów.
W 1970 roku S. Cook opracował teoretyczny dowód dotyczący pewnego typu m a
szyny abstrakcyjnej. Wynikało z niego, że istnieje algorytm, który dla najgorszego
przypadku rozwiązuje problem wyszukiwania podiańcuchów w czasie proporcjonal
nym do M + N. D.E. Knuth i V.R. Pratt starannie przepracowali rozwiązanie, któ
re Cook zastosował do udowodnienia twierdzenia (nie było ono przeznaczone od
użytku praktycznego), i przekształcili je na stosunkowo prosty oraz praktyczny algo
rytm. Wydawało się, że jest to rzadki i atrakcyjny przykład teoretycznych osiągnięć,
które m ożna natychmiast (i nieoczekiwanie) wykorzystać w praktyce. Okazało się
jednak, że J.H. Morris odkrył niemal ten sam algorytm jako rozwiązanie irytującego
problemu, na który natrafił w czasie implementowania edytora tekstu (Morris chciał
uniknąć konieczności cofania się w tekście). To, że ten sam algorytm powstał na pod
stawie dwóch tak różnych podejść, jest wiarygodnym dowodem na to, iż stanowi
podstawowe rozwiązanie problemu.
Knuth, Morris i Pratt nie zdecydowali się na opublikowanie algorytmu aż do 1976
roku, a do tego czasu R.S. Boyer i J.S. Moore (oraz, niezależnie, R.W. Gosper) od
kryli algorytm, który w wielu zastosowaniach jest znacznie szybszy, ponieważ często
sprawdza tylko część znaków tekstu. Algorytm ten stosuje się w wielu edytorach teks
tu w celu znacznego skrócenia czasu reakcji przy wyszukiwaniu podiańcuchów.
Zarówno algorytm Knutha-M orrisa-Pratta (KMP), jak i algorytm Boyera-Moorea
wymagają skomplikowanego wstępnego przetwarzania wzorca. Proces ten trudno
jest zrozumieć, co ogranicza zakres stosowania obu algorytmów. Według anegdoty
nieznany programista systemów stwierdził, że algorytm Morrisa jest zbyt trudny do
zrozumienia, i zastąpił go implementacją opartą na ataku siłowym.
W 1980 roku M.O. Rabin i R.M. Karp zastosowali haszowanie do opracowania
algorytmu niemal tale prostego, jak rozwiązanie oparte na ataku siłowym, ale działające
go z bardzo wysokim prawdopodobieństwem w czasie proporcjonalnym do M + N.
Ponadto algorytm ten m ożna rozwinąć do dwuwymiarowych wzorców i tekstów, dla
tego jest przydatniejszy od innych rozwiązań do przetwarzania obrazu.
Opisana historia jest dowodem na to, że poszukiwania lepszego algorytmu nadal
są bardzo często uzasadnione. Podejrzewamy, że nawet dla tego klasycznego proble
m u mogą pojawić się nowe rozwiązania.
772 ROZDZIAŁ 5 o Łańcuchy znaków
Wyszukiwanie podłańcuchów m etodą ataku siłowego Oczywistą m e
todą wyszukiwania podłańcuchów jest sprawdzanie na każdej pozycji tekstu, czy
wzorzec pasuje do danego fragmentu. Przedstawiona poniżej m etoda search () dzia
ła w ten sposób, aby znaleźć pierwsze wystąpienie wzorcowego łańcucha znaków pat
w tekście tx t. Program przecho
p u b l i c s t a t i c i n t s e a r c h ( S t r i n g pa t, S t r i n g t x t )
wuje jeden wskaźnik dla tekstu ( i )
{
int M = p a [Link] n g th (); oraz j eden wskaźnik dla wzorca (j ).
i n t N = t x t . l e n g t h ( )ś Dla każdego i kod ustawia j na 0
f o r ( i n t i = 0 ; i < = N - M ; i+ + ) i zwiększa tę wartość do m om entu
{
in t j;
wykrycia dopasowania lub końca
f o r (j = 0; j < M; j + + ) wzorca (j == M). Dojście do końca
i f ( t x t . c h a r A t ( i + j ) != p a t . c h a r A t ( j ) ) tekstu (i == N-M+l) przed końcem
break ;
i f (j == M) r e t u r n i ; // Z n a le z io n o .
wzorca oznacza brak dopasowania
} — wzorzec nie występuje w tek
r e t u r n N; // Nieudane w ysz uk iw anie . ście. Zgodnie z konwencją zwraca
my wartość N, aby poinformować
Wyszukiwanie podłańcucha metodą ataku siłowego o braku dopasowania.
W typowych aplikacjach do
przetwarzania tekstu indeks j rzadko rośnie, dlatego czas wyszukiwania jest propor
cjonalny do N. W prawie wszystkich porównaniach pierwszy znak wzorca pozwala
wykryć niedopasowanie. Załóżmy na przykład, że szukasz wzorca wzorca w tekście
tego akapitu. Do końcowej litery pierwszego wystąpienia wzorca występuje 176 zna
ków, przy czym tylko 10 z nich to w (a ciąg wz nie występuje ani razu), tak więc łączna
liczba porównań wynosi 176+10, co oznacza średnio 1,056 porównania na znak teks
tu. Nie ma jednak gwarancji, że algorytm zawsze będzie tak wydajny. Przykładowo,
wzorzec może zaczynać się długim ciągiem liter A. Jeśli także tekst obejmuje długie
ciągi liter A, wyszukiwanie podłańcucha będzie wolne.
i i i+j 0 1 2 3 4 5 6 7 8 9 10
txt — - A B A c A D A B R A C
0 2 2 A B R A ■pat
1 0 1 A B R A Czerwona litera
2 1 3 A B R "" oznacza niedopasowanie
3 0 3 / A J B R A Szare litery
4 1 5 / A B r A / podano w celach
Czarne litery * poglądowych
5 0 5 pasują do tekstu A B R A
6X 4 10 A B R A
Jeśli i ma wartość M, \
należy zwrócić i Dopasowanie
Wyszukiwanie podłańcucha metodą ataku siłowego
5.3 a Wyszukiwanie podiańcuchów 773
Twierdzenie M. Jeśli wzorzec ma długość M, a tekst — N, to wyszukiwanie
łańcuchów znaków m etodą ataku siłowego wymaga w najgorszym przypadku
~N M porównań znaków,
Dowód. Najgorszy przypadek ma miejsce, kiedy zarówno wzorzec, jak i tekst
to na przykład ciąg samych liter A, po których następuje B. Wtedy dla każdej z N
- M + 1 pozycji, gdzie może wystąpić dopasowanie, wszystkie znaki wzorca są
sprawdzane względem tekstu, co oznacza łączny koszt M (N - M + 1). Zwykle M
jest bardzo małe w porównaniu z N, tak więc łączna wartość to ~NM.
Sztuczne łańcuchy znaków tego rodzaju w zasadzie nie występują w tekstach w języ
ku polskim, jednak mogą się pojawić w innych zastosowaniach (na przykład w teks
tach binarnych), dlatego należy
i j i + j 0 1 2 3 4 5 6 7 8 9
poszukać lepszego algorytmu.
txt— ► A A A A A A A A A B
Inna implementacja, przed
0 4 4 A A A A B -*— pat
stawiona w dolnej części strony,
1 4 5 A A A A B
jest pouczająca. Program, tak
2 4 6 A A A A B
3 4 7 A A A A B
jak wcześniej, przechowuje je-
4 4 g a a a a b den wskaźnik do tekstu (i) oraz
5 5 io a a a a b jeden wskaźnik do wzorca (j).
,. ,
Wyszukiwanie podłancucnow metodą
, Dopóki
r
wskaźniki rprowadzą-z do
ataku siłowego (najgorszy przypadek) pasujących znaków, Są Z W ię k -
szane. Kod wykonuje dokładnie
tę samą liczbę porównań, co poprzednia implementacja. Aby to zrozumieć, nale
ży zauważyć, że i w tym kodzie to odpowiednik wartości i +j z poprzedniego kodu
— wartość ta wskazuje koniec ciągu już dopasowanych znaków w tekście (wcześniej
i wskazywał początek ciągu). Jeśli i oraz j wskazują niedopasowane znaki, należy
cofnąć oba wskaźniki — j do początku wzorca, a i tak, aby odpowiadał przesunięciu
wzorca o jedną pozycję w prawo w celu dopasowania go względem tekstu.
p u b l i c s t a t i c i n t s e a r c h ( S t r i n g pa t, S t r i n g t x t )
1
ant j , M = p a t . l e n g t h ( ) ;
int i , N = t x [Link] n g t h ( );
f o r (i = 0 , j =0; i <N && j < M; i+ + )
{
if ([Link] t(i) == [Link](j)) j++;
e l s e { i - = j ; j = 0; }
1
if (j == M) r e t u r n i - M; // Z n a l e z io n o ,
e lse r e t u r n N; // N ie z n a l e z i o n o .
Inna implementacja wyszukiwania podłańcuchów
metodą ataku siłowego (z bezpośrednim cofaniem)
774 ROZDZIAŁ 5 □ Łańcuchy znaków
Wyszukiwanie podłańcuchów metodą Knutha-Morrisa-Pratta Oto
podstawowy pomysł, na którym oparty jest algorytm odkryty przez Knutha, Morrisa
i Pratta — po wykryciu niedopasowania niektóre znaki tekstu są już znane (ponieważ
pasowały do znaków wzorca do punktu niedopasowania). Można to wykorzystać,
aby uniknąć cofania wskaźnika tekstu przed wszystkie znane znaki.
W ramach konkretnego przykładu załóżmy, że korzystamy z dwuznakowego al
fabetu i szukamy wzorca B A A A A A A A A A. Przyjmijmy, że dopasowaliśmy pięć
znaków wzorca, a w szóstym wykryto niedopasowanie. W iadomo wtedy, że sześć
wcześniejszych znaków w tekście to B A A A A B (pięć pierwszych pasuje do wzorca,
a szósty — nie), a wskaźnik tekstu wskazuje końcową literę B. Kluczowym spostrze
żeniem jest to, że nie trzeba cofać wskaźnika tekstu i, ponieważ cztery wcześniejsze
znaki w tekście to A — nie pasują one do pierwszego znaku wzorca. Ponadto znak
obecnie wskazywany przez i to B; znak ten pasuje do pierwszego znaku wzorca, dlate
go można zwiększyć i oraz porównać następny znak tekstu z drugim znakiem wzor
ca. To wnioskowanie prowadzi do spostrzeżenia, że dla tego wzorca można zmienić
klauzulę else w drugiej implementacji m etody ataku siłowego, tak aby tylko usta
wiała j = 1 (bez zmniejszania i ). Ponieważ wartość i w pętli się nie zmienia, metoda
wykonuje najwyżej N porównań znaków. Praktyczny skutek tej konkretnej zmiany
jest ograniczony do przedstawionego wzorca, jednak warto zastanowić się nad wy
korzystanym pomysłem. Algorytm Knutha-M orrisa-Pratta jest jego uogólnieniem.
Co zaskakujące, zawsze można znaleźć wartość, na jaką należy ustawić wskaźnik j
przy niedopasowaniu, dlatego nigdy nie trzeba zmniejszać wskaźnika i.
Tekst
Po niedopasowaniu
szóstego znaku
Metoda ataku sitowego
powoduje cofnięcie
i sprawdzenie tego znaku
Jednak cofnięcie
nie jest konieczne
Cofanie wskaźnika tekstu przy wyszukiwaniu podłańcuchów
Przeskoczenie wszystkich dopasowanych znaków po wykryciu niedopasowania nie
zadziała, jeśli wzorzec m ożna dopasować, począwszy od dowolnej pozycji przed miej
scem niedopasowania. Przykładowo, przy wyszukiwaniu wzorca A A B A A Aw tekście
A A B A A B A A A A niedopasowanie po raz pierwszy zostaje wykryte na pozycji 5,
jednak wyszukiwanie należy wznowić od pozycji 3, ponieważ w przeciwnym razie al
gorytm nie wykryje dopasowania. Algorytm KMP oparty jest na tym, że można z góry
ustalić, jak wznawiać wyszukiwanie, ponieważ zależy to tylko od wzorca.
5.3 o Wyszukiwanie podiańcuchów 775
Cofanie wskaźnika wzorca Przy wyszu j [Link](j) dfa[][j] Tekst (sam wzorzec)
kiwaniu podiańcuchów metodą KMP ni A B C ABABAC
gdy nie należy cofać wskaźnika tekstu ( i ), 0 A 1 A
atablicadfa[] [] służy do zapisywania, jak B
daleko należy cofnąć wskaźnik wzorca (j ) 0 A B AB A C
C
po wykryciu niedopasowania. Dla każde 0 A B AB A C
go znaku c wartość dfa[c] [j] to pozycja
1 B 2 AB
we wzorcu, którą należy porównać z na
AA
stępną pozycją w tekście po porównaniu 1 A B AB A C
c z p [Link] arA t(j). W trakcie wyszuki AC
0 A B AB A C
wania d fa [tx t.c h a rA t(i)] [j] to pozy
cja we wzorcu, którą należy porównać 2 A 3 ABA
z tx [Link] arA t(i+ l) po porównaniu tx t. ABB
ABABAC
charAt(i) z [Link] t(j). Przy dopa
ABC
sowaniu wystarczy przejść do następnego ABAB A C
znaku, dlatego dfa[[Link] t(j)] [j] to
A B AB
zawsze j+1. Przy niedopasowaniu znany
A BAA
jest nie tylko znak t x t . charAt ( i ), ale też ABABAC
j-1 wcześniejszych znaków tekstu. Jest to A B AC
ABABAC
pierwszych j-1 znaków wzorca. Dla każ
dego znaku c można sobie wyobrazić, że ABABA
przesuwamy kopię wzorca nad j znakami / ABABB
Dopasowanie (przejście 0 A B A B A C
(pierwszymi j - 1 znakami wzorca i zna
do następnego znaku); ABABC
kiem c; decydujemy, co zrobić, kiedy te należy ustawić df a [pat. A BAB A C
znaki to tx t . charAt (i-j+ 1 . .i) ) od lewej c h a r A t ( j ) ] [ j] na j+ 1 Znany znak tekstu
5 C A B A B A C / * momencie
do prawej i zatrzymujemy się, jeśli wszyst A B A B A A dopasowania
kie pokrywające się znaki pasują (lub jeśli 1 ABAB A C
nie ma dalszych znaków). Uzyskujemy Niedopasowanie / A BA BAB
(cofanie wskaźnika ^— *- ABABAC
w ten sposób następne miejsce, w któ wzorca) |
rym można dopasować wzorzec. Indeks Cofnięcie o długość maksymalnego
znaku wzorca porównywanego z tx t. pokrywania się początku wzorca
ze znanymi znakami tekstu
charAt(i+1) (d fa[tx t.c h arA t(i)] [ j ] )
Cofanie wskaźnika dla wzorca A B A B A C przy
precyzyjnie określa liczbę pokrywających wyszukiwaniu podiańcuchów metodą KMP
się znaków.
W yszukiw anie m etodą K M P Po wyznaczeniu tablicy df a [] [] uzyskujemy metodę
wyszukiwania podiańcuchów przedstawioną w górnej części następnej strony. Kiedy
i oraz j prowadzą do niepasujących do siebie znaków (przy sprawdzaniu dopaso
wania wzorca począwszy od pozycji i - j +1 w tekście), to następna możliwa pozycja
dopasowania wzorca to i-d fa [tx t.c h a rA t(i)] [j ]. Jednak z uwagi na sposób two
rzenia tablicy pierwszych d fa [tx t. charAt (i)] [j] znaków od tej pozycji pasuje do
pierwszych d fa [tx t.c h a rA t(i)] [j] znaków wzorca, dlatego nie trzeba cofać wskaź
nika i. Można ustawić j na d fa [tx t. charAt (i)] [j] oraz zwiększyć i. Tak właśnie
postępujemy, kiedy i oraz j prowadzą do pasujących znaków.
776 ROZDZIAŁ 5 a Łańcuchy znaków
Sym ulacja determ inistycznego autom atu skończonego Przydatne jest przedsta
wianie omawianego procesu w kategoriach deterministycznego automatu skończonego
(ang. deterministic finite-state automaton — DFA). Jak wskazuje na to nazwa, tablica
dfa [] [] wyznacza taki automat. Graficzna reprezentacja automatu DFA przedstawio
na w dolnej części strony składa
pub lic in t se a rc h (S t rin g txt) się ze stanów (wartości w okrę
{ // Symulowanie d z i a t a n i a automatu DFA na ła ńcu chu t x t .
gach) i przejść (opisane linie). Dla
int i, j, N = t x t . le n g t h ( ) ;
f o r (i = 0, j = 0; i < N && j < M; i+ + ) każdego znaku wzorca istnieje
j j§ d f a [ t x t . ch a rA t ( i )] [ j ] ; jeden stan, a każdy taki stan po
i f (j — M) r e t u r n i - M; // Z n a l e z io n o ,
wiązany jest z jednym przejściem
else r e t u r n N; // Nie z n a l e z i o n o .
dla każdej litery alfabetu. Dla
omawianych tu automatów DFA
Wyszukiwanie podtańcuchów metodą KMP służących do dopasowywania
(symulowanie działania automatu DFA)
podłańcuchów jednym z przejść
jest przejście po dopasowaniu (z j do j+1 i oznaczenie za pom ocą [Link] t (j)),
a wszystkie pozostałe — to przejścia po niedopasowaniu (w lewo). Stany odpowiadają
porównaniom znaków, po jednym na każdą wartość indeksu wzorca. Przejścia odpo
wiadają zmianie wartości indeksu wzorca. Przy sprawdzaniu w tekście znaku i , kiedy
występuje stan j , maszyna działa tak — „Zastosuj przejście do dfa [ t x t . charAt (i ) ] [j ]
i przejdź do następnego znaku, zwiększając i ”. Przy przejściu po dopasowaniu n a
leży przesunąć indeks w prawo o jedną pozycję, ponieważ d fa [p a t.c h a rA t(j)] [j]
to zawsze j+1. Przy przejściu po niedopasowaniu należy przesunąć indeks w lewo.
Maszyna wczytuje znaki tekstu jeden po drugim od lewej do prawej i po wczytaniu
każdego znaku przechodzi w nowy stan. Uwzględniliśmy też stan zatrzymania, M,
który nie ma przejść. Uruchamiamy maszynę w stanie 0. Jeśli maszyna dojdzie do
stanu M, oznacza to znalezienie w tekście podłańcucha pasującego do wzorca (m ó
wimy, że maszyna DFA rozpoznaje wzorzec). Jeżeli maszyna dojdzie do końca tekstu
przed przejściem w stan M, wia
Reprezentacja wewnętrzna domo, że wzorzec nie występuje
j o jako podłańcuch tekstu. Każdy
[Link](j) A A wzorzec odpowiada maszynie
1 3
dfa[] [j] 0 0 (jest ona reprezentowana przez
0 0 tablicę d fa [] [] z przejściami).
Przejście po Metoda search() w algorytmie
niedopasowaniu Przejście po KMP to program Javy symulują
(cofnięcie) dopasowaniu
Reprezentacja graficzna (zwiększenie) cy działanie opisanej maszyny.
Aby zrozumieć wyszukiwa
nie podłańcuchów za pomocą
automatu DFA, rozważmy dwie
najprostsze wykonywane przez
Stan zatrzymania niego operacje. Na początku, po
uruchom ieniu w stanie 0 na po
Automat DFA odpowiadający łańcuchowi znaków A B A B A C czątku tekstu, automat pozosta-
5.3 b Wyszukiwanie podłańcuchów 777
3 4 5 6 7 8 9 10 11 12 13 14 15 16 — t
W czytyw anie tego zn aku A A B A C A A B A B A C A A— tX t
A ktualny stan 0 1 1 2 3 0 1 1 2 3 4 5 6 — j
Przejście d o tego stanu B A C t
A B A C Zn ale zio no wzorzec;
należy zw rócić i - M = 9
B A B A C
A B A B A C
A B A B A C
A B A B A C
A B A B A C
A B A B A £
D op asow an ie:
należy ustawić j n a
A B A B A C
d fa [tx t.c h a rA tC O ] [ j ] =
d f a [ p a t .c h a r A t ( j) ] [ j] = j+19
N iedopasow anie: należy ustawić
j n a d fa [ tx t.c h a r A tC O ] [j]
W y m a g a przesunięcia wzorca, ab y d o p a so w a ć A
p a t.c h a rA t(j) d o t x t .c h a r A t ( i+ l) a
Ślad w yszu kiw a n ia p o d ła ń c u c h ó w m etod ą KMP (sym ulacja autom atu DFA) dla w zorca A B A B A C
je w stanie 0 i przegląda znaki tekstu do czasu znalezienia znaku równego pierwsze
m u znakowi wzorca. Wtedy przechodzi do następnego stanu i zaczyna pracę. W koń
cowym etapie procesu, po znalezieniu pasującego znaku, dopasowuje znaki wzorca
do prawego końca tekstu i przechodzi w wyższy stan do czasu wejścia w stan M. Ślad
w górnej części tej strony to ilustracja typowego przebiegu pracy przykładowego au
tom atu DFA. Każde dopasowanie powoduje przejście automatu DFA w następny stan
(odpowiada to zwiększeniu indeksu wzorca, j). Każde niedopasowanie cofa automat
DFA do wcześniejszego stanu (jest to odpowiednik ustawienia indeksu wzorca, j,
na mniejszą wartość). Indeks tekstu, i, jest zwiększany od lewej do prawej pozycja
po pozycji, natomiast indeks wzorca, j, jest modyfikowany skokowo we wzorcu na
podstawie działania automatu DFA.
Tworzenie au to m atu DFA Teraz, kiedy już znasz mechanizm, możemy przejść do
kluczowego pytania związanego z algorytmem KMP — jak utworzyć tablicę df a [] []
odpowiadającą danem u wzorcowi? Co zaskakujące, odpowiedź na to pytanie leży
w samym automacie DFA (!). Należy zastosować pomysłową (i dość skomplikowaną)
technikę, opracowaną przez Knutha, M orrisa i Pratta. Po wykryciu niedopasowania
w miejscu p [Link] arA t(j) ważne jest ustalenie, w jakim stanie automat DFA byłby,
gdyby cofnąć indeks tekstu i ponownie sprawdzić znaki tekstu napotkane po prze
sunięciu o jedną pozycję w prawo. Nie należy rzeczywiście cofać indeksu, a tylko
ponownie uruchomić automat DFA, jakby po cofnięciu indeksu.
778 ROZDZIAŁ 5 □ Łańcuchy znaków
Kluczowym spostrzeżeniem jest to, że konieczne byłoby ponowne sprawdzenie
znaków z pozycji od p [Link] arA t(l) do p a t.c h a rA t(j-l). Pomijamy pierwszy znak
(aby przesunąć wzorzec w prawo o jedną pozycję) i ostatni znak (z uwagi na niedo
pasowanie). Wszystkie znaki są znane, dlatego
dla każdej pozycji, na której wystąpiło niedopa
sowanie, można z góry ustalić stan, w którym
należy ponownie uruchomić automat DFA. Na
rysunku po lewej stronie pokazano możliwe
przejścia w przykładzie. Upewnij się, że rozu
miesz to rozwiązanie.
Co automat DFA powinien zrobić z następ
nym znakiem? Dokładnie to samo, co zrobiłby
po cofnięciu. Wyjątkiem jest znalezienie do
pasowania na pozycji p [Link] arA t(j) — wtedy
Symulowanie działania automatu DFA w celu powinien przejść w stan j+1. Przykładowo, aby
wyznaczenia stanów dla wzorca A B A B A C stwierdzić, co automat DFA powinien zrobić
po napotkaniu niedopasowania przy j = 5 dla
wzorca A B A B A C, należy wykorzystać automat DFA do ustalenia, że pełne cofnię
cie powoduje przejście w stan 3 dla B A B A , dlatego m ożna skopiować dfa[] [3] do
dfa [] [5], a następnie ustawić wpis dla C na 6, ponieważ p a t. charAt (5) to C (dopaso
wanie). Ponieważ przy tworzeniu stanu j trzeba ustalić tylko sposób działania auto
matu DFA dla j-1 znaków, zawsze można uzyskać potrzebne informacje z częściowo
ukończonego automatu DFA.
Ostatni kluczowy szczegół procesu dotyczy spostrzeżenia, że określanie pozycji
ponownego urucham iania (X) dla kolum ny j tablicy dfa [] [] jest łatwe, ponieważ
X < j, można więc wykorzystać do wykonania zadania częściowo utworzony au
tom at DFA. Następna wartość X to dfa [pat. charAt (j ) ] [X]. Wróćmy do przykładu
z poprzedniego akapitu — można zaktualizować wartość Xwartością dfa [' C1] [3] = 0
(nie korzystamy jednak z tej wartości, ponieważ tworzenie automatu DFA zostało
zakończone).
Powyższy opis prowadzi do zaskakująco zwięzłego kodu (przedstawionego poni
żej) do tworzenia automatu DFA odpowiadającego danem u wzorcowi. Dla każdego
j należy:
D skopiować wartość dfa [] [X] do d f a [ p a t . c h a r A t ( 0 ) ] [ 0 ] = 1;
d fa [] [j] (przy niedopasowaniu); f o r ( i n t X = 0, j = 1; j < M; j + + )
1:1 ustawić d fa [p a t.c h a rA t(j)] [j] na { // O b l i c z a n i e cif a [] [ .j ].
f o r ( i n t c = 0; c < R; c++)
j +1 (przy dopasowaniu);
d f a [ c ] [ j] = d f a [ c ] [ X ] ;
■ zaktualizować X. d fa [pa t. c h a rA t ( j )] [j] = j + 1 ;
Rysunek na następnej stronie to ślad dzia
X = d f a [ p a t . c h a r A t (j ) ] [ X ] ;
łania kodu dla przykładowych danych.
Aby się upewnić, że rozumiesz to rozwią
Tworzenie automatu DFA na potrzeby
zanie, wykonaj ć w i c z e n i a 5 .3.2 i 5 .3 .3 .
wyszukiwania podłańcuchów metodą KMP
5.3 n Wyszukiwanie podłańcuchów 779
0_ r i.c i
[Link](j) A
W “ ©
A 1
dfa[] [j] B O
c O
ł
j 0 1 © k y f '* Kopiowanie d f a [ ] [X] do d f a [] [ j ]
p a t.c h a rA t(j) A B (o )~ a — » - © - B — * ~ © d fa [p a t.c h a rA t(j)][j] = j+ 1 ;
A 1 1 <^" X c x = d fa [p a t.c h a rA t(j)] [ x ] ;
d fa [] [j] B 0 2
C 0 0
X
1
j 0 1 2 O ©
p a t.c h a rA t(j) A B A
© k * -—O©k -. B— A—^ ©
A 1 1 3
d fa [] [j] B 0 2 0
C 0 0 0
X
1
j 0 1 2 3
p a t.c h a rA t(j) A B A B
A 1 1 3 1
d fa [][j] B 0 2 0 4
C 0 0 0 0
X
1
j 0 1 2 3 4
pat. c h a r A t ( j ) A B A B A
A 1 1 3 1 5
d fa [] [ j] B 0 2 0 4 0
C 0 0 0 0 0
X
1
j 0 1 2 3 4 5
p a t.c h a rA t(j) A B A B A C
A 1 1 3 1 5 1
©
d fa [] [j] B 0 2 0 4 0 4
C 0 0 0 0 0 6
Tworzenie automatu DFA pod kątem wyszukiwania podłańcuchów metodą KMP we wzorcu A B A B A C
780 ROZDZIAŁ 5 Łańcuchy znaków
ALGORYTM 5.6. Wyszukiwanie podłańcuchów metodą Knutha-Morrisa-Pratta
public c la s s KMP
{
private S t r in g pat;
private i n t [ ] [ ] dfa;
public KMP (S t rin g pat)
( // Tworzenie maszyny DFA na podstawie wzorca,
t h i s . pat = pat;
in t M = p a t . le n g t h ( ) ;
in t R = 256;
dfa = new i nt [R] [M];
d fa[[Link](0)] [0] = 1;
fo r (in t X = 0, j = 1; j < M; j++)
{ // Wyznaczanie d f a [] [ j ] .
fo r (in t c = 0; c < R; C++)
d fa [ c ] [ j ] = dfa[c] [ X ] ; // Kopiowanie wartości
// dla niedopasowania.
d fa[p [Link] a rA t(j)] [j] = j+1; // Ustawianie wartości
// dla dopasowania.
X = d fa[[Link] rA t(j)][X]; // Aktualizowanie stanu ponownego
// uruchamiania.
}
}
public in t se arch (S trin g txt)
( // Symulowanie d zia ła n ia automatu DFA na łańcuchu txt.
in t i , j , N = t x t . l e n g t h ( ) , M = p a t . le n g t h ( ) ;
for (i = 0 , j = 0 ; i < N && j < M; i++)
j = d f a [t x t . c h a r A t (i)] [ j ] ;
i f (j == M) return i - M; // Znaleziono (dojście do końca wzorca),
else return N; // Nie znaleziono (dojście do końca tekstu).
}
p ublic s t a t ic void m ain(String[] args)
// Zobacz stronę 781.
}
Konstruktor w tej implementacji algorytmu Knutha-Morrisa-Pratta (służącego do wyszuki
wania podłańcuchów) tworzy na podstawie wzorca automat DFA, aby umożliwić działanie
metody search(), która potrafi znaleźć wzorzec w danym tekście. Program wykonuje to
samo zadanie, co metoda ataku siłowego, jednak działa szybciej dla wzorców, w których
występują powtórzenia.
% j a v a KMP AACAA
AABRAACADABRAACAADABRA
tekst: AABRAACADABRAACAADABRA
w zorzec: AACAA
5.3 Q Wyszukiwanie podłańcuchów 781
a lg o r y t m 5.6 z poprzedniej strony to implementacja poniższego interfejsu API.
p u b l i c c l a s s KMP
KMP ( S t r i ng pa t) Tworzy autom at DFA do wyszukiwania wzorca pat
int se arch (Strin g txt) Znajduje indeks wzorca pat w tekście t x t
Interfejs API do wyszukiwania podłańcuchów
W dolnej części strony przedstawiono typowego klienta testowego. Konstruktor two
rzy automat DFA na podstawie wzorca, a m etoda search () wykorzystuje automat do
znalezienia wzorca w danym tekście.
Twierdzenie N. Wyszukiwanie podłańcuchów metodą Knutha-M orrisa-Pratta
wymaga dostępu do nie więcej niż M + N znaków przy szukaniu wzorca o dłu
gości M w tekście o długości N.
Dowód. Wynika bezpośrednio z kodu. Potrzebny jest jeden dostęp do każdego
znaku wzorca przy wyznaczaniu tablicy dfa [] [] i jeden dostęp do każdego znaku
tekstu (w najgorszym przypadku) w metodzie search ().
Ważny jest też inny parametr. Dla P-znakowego alfabetu łączny czas wykonania
(i pamięć) przy tworzeniu automatu DFA rośnie proporcjonalnie do MR. Można
usunąć czynnik R, tworząc automat DFA, w którym każdy stan obejmuje przejście
dla dopasowania i dla niedopasowania (a nie dla każdego możliwego znaku), choć
proces ten jest bardziej zawiły.
Gwarancje liniowego czasu dla najgorszego przypadku w algorytmie KMP są waż
nym osiągnięciem teoretycznym. W praktyce przyspieszenie w porównaniu z ata
kiem siłowym często jest nieistotne, ponieważ rzadko szukane są wzorce z wieloma
powtórzeniami w tekście obejmującym liczne powtórzenia. M etoda ta ma jednak
praktyczną zaletę, ponieważ nigdy nie p o
woduje cofania w danych wejściowych. Ta p u b l i c s t a t i c v o i d main ( S t r i n g[] a r g s )
cecha sprawia, że dla strum ieni wejściowych {
S t r i n g pat = a r g s [ 0 ] ;
o nieokreślonej długości (na przykład dla
Strin g txt = a r g s[l];
standardowego wejścia) wyszukiwanie pod KMP kmp = new KMP (p at );
łańcuchów m etodą KMP jest wygodniejsze Std O u t.p rin tln ("T e kst: 11 + t x t ) ;
niż stosowanie algorytmów wymagających i n t o f f s e t = k m p . s e a r c h ( t xt);
StdO [Link]("W zorzec: ") ;
cofania (te ostatnie wymagają skomplikowa f o r ( i n t i = 0; i < o f f s e t ; i+ + )
nego buforowania). Co ciekawe, jeśli cofanie StdO ut.p rint(" ");
jest proste, m ożna uzyskać efekty znacząco Std O u t.p rin tln (p at);
lepsze niż za pom ocą metody KMP. Dalej
opisujemy technikę, która ogólnie prowadzi Ł , ..
r 1 ‘ x Klient testowy do wyszukiwania
do znacznej poprawy wydajności, ponieważ podłańcuchów metodą kmp
może cofać wskaźnik tekstu.
782 ROZDZIAŁ 5 b Łańcuchy znaków
Wyszukiwanie podłańcuchów metodą Boyera-Moore’a Jeśli cofanie się
w tekście nie stanowi problemu, m ożna opracować znacznie szybszą metodę wyszu
kiwania podłańcuchów. O parta jest ona na przeglądaniu wzorca od prawej do lewej
przy próbie dopasowania go do tekstu. Przykładowo, przy wyszukiwaniu podlańcu-
cha BAABBAA i dopasowaniu siódmego oraz szóstego znaku, ale już nie piątego, można
natychmiast przesunąć wzorzec o siedem pozycji w prawo i sprawdzić 14. znak teks
tu, ponieważ po częściowym dopasowaniu znaleziono ciąg XAA, gdzie Xjest różne od
B, a taki ciąg nie występuje we wzorcu. Ogólnie wzorzec z końca może występować
gdziekolwiek, dlatego potrzebna jest tablica pozycji wznawiania działania, tak jak
w algorytmie Knutha-M orrisa-Pratta. Nie analizujemy szczegółowo tego podejścia,
ponieważ jest całkiem podobne do implementacji metody Knutha-M orrisa-Pratta.
W zamian omawiamy inną sugestię Boyera i Moorea, która zwykle pozwala osiągnąć
jeszcze wyższą wydajność niż przeglądanie wzorca od prawej do lewej.
Tak jak w implementacji wyszukiwania podłańcuchów metodą KMP, tak i tu usta
lamy, co zrobić dalej, na podstawie niedopasowanego znaku tekstu, a także według
wzorca. Etap wstępnego przetwarzania jest potrzebny do stwierdzenia, co należy zro
bić, kiedy dany znak spowodował niedopasowanie (trzeba to określić dla każdego
możliwego znaku tekstu). Najprostsze zastosowanie tego pomysłu prowadzi bezpo
średnio do wydajnego i przydatnego kodu metody wyszukiwania podłańcuchów.
H eurystyka obsługi niedopasow ania zn a ku Rozważmy rysunek w dolnej części
tej strony. Pokazano na nim wyszukiwanie wzorca NEEDLE w tekście FINDINAHAYST
ACKNEEDLEINA. Poruszając się od prawej do lewej w celu dopasowania wzorca, naj
pierw należy porównać prawe E wzorca z N (znak na pozycji 5) z tekstu. Ponieważ N
występuje we wzorcu, przesuwamy wzorzec o pięć pozycji w prawo, aby dopasować
N w tekście do (pierwszego od prawej) N we wzorcu. Wtedy następuje porównanie
pierwszej od prawej litery wzorca, E, z S (znak na pozycji 10) z tekstu. Także tu na
stępuje niedopasowanie, jednak S nie występuje we wzorcu, tak więc m ożna przesu
nąć wzorzec o sześć pozycji w prawo. Dopasowujemy pierwsze od prawej E wzorca
z E na pozycji 16 w tekście, następnie znajdujemy niedopasowanie i wykrywamy N
na pozycji 15, co prowadzi do przesunięcia wzorca w prawo o pięć pozycji (tak jak
na początku). Ostatecznie stwierdzamy, przechodząc od prawej do lewej od pozycji
20, że wzorzec występuje w tekście. Ta m etoda pozwala dojść do pozycji pasującego
fragmentu kosztem czterech porównań znaków (sześć kolejnych potrzebnych jest na
zweryfikowanie dopasowania)!
i j 0 1 2 B 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Tekst— - F I N D I N A H A Y S T A C K N E E D L E I N A
0 5 N E E D L E - — Wzorzec
5 5 N E E D L E
11 4 N E E D L E
15 0 N E E D L E
\
Zwracanie i = 15
Heurystyka obsługi niedopasowania znaku przy wyszukiwaniu
podłańcuchów metodą Boyera-Wloore'a (od prawej do lewej)
5.3 o Wyszukiwanie podłańcuchów 783
P u n kt wyjścia W implementacji heury- N E E D L E
styki obsługi niedopasowania znaku korzy c 0 1 2 3 4 5 r ig h t [
A - 1 - 1 - 1 - 1 -1 -1 - 1 -1
stamy z tablicy rig h t[], która dla każdego
B -1 -1 -1 -1 -1 -1 -1 -1
znaku alfabetu wyznacza indeks pierwsze
C -1 -1 -1 -1 -1 -1 -1 -1
go od prawej wystąpienia znaku we wzor
D -1 -1 -1 -X 3 3 3 3
cu (jeśli znaku nie ma we wzorcu, wartość y
E -1 -1 1 2 2 5 5
to -1). Wartość ta precyzyjnie określa, jak -1
daleko należy przejść, jeśli dany znak wy L -1 -1 -1 -1 -1 4 4 4
stępuje w tekście i powoduje niedopaso M -1 -1 -1 -1 -1 -1 -1 -1
wanie w trakcie wyszukiwania łańcucha. N -1 0 0 0 0 0 0 0
W ramach inicjowania tablicy rig h t[] na -1
leży ustawić wszystkie wartości na - 1 , a na Wyznaczanie tablicy przeskoków na potrzeby
stępnie dla j od 0 do M-l ustawić wartość algorytmu Boyera-Moore'a
rig h t[p a t.c h a rA t(j)] na j. Proces ten po
kazano po prawej stronie dla przykładowego wzorca NEEDLE.
W yszukiw anie podłańcuchów Po napisaniu kodu do obliczania zawartości tablicy
rig h t[] opracowanie implementacji z a l g o r y t m u 5.7 jest proste. Dostępny jest in
deks i przesuwany od lewej do prawej w tekście oraz indeks j przesuwany od prawej
do lewej we wzorcu. W pętli wewnętrznej sprawdzamy, czy wzorzec pasuje do tekstu
na pozycji i. Jeśli tx t .charAt (i+ j) jest równe [Link] t (j) dla wszystkich j od M-l
do 0, ma miejsce dopasowanie. W przeciwnym razie wykryto niedopasowanie i ma
miejsce jedna z trzech sytuacji.
■ Jeśli znak powodujący niedopaso
wanie nie występuje we wzorcu, 1 1
T
można przesunąć wzorzec o j +1 po
N E E D
zycji w prawo (zwiększając i o j+ 1 ). Można zwiększyć
t przesunięcie za pomocą
Mniejsza wartość powoduje nałoże i tablicy podobne] do
nie na niepasujący znak jednego ze te]z metody KMP
Zwiększenie i o j+ 1 J
znaków wzorca. Przesunięcie po L
woduje nałożenie znanych znaków N E D L E
z początku wzorca na znane znaki Ustawianie j na M -l ^
j
z końca wzorca, dlatego można do
Heurystyka obsługi niedopasowania znaków
datkowo zwiększyć i po obliczeniu (niedopasowany znak nie występuje we wzorcu)
tablicy podobnej do tej z metody
KMP (zobacz przykład po prawej stronie).
■ Jeśli niedopasowany znak c występuje we wzorcu, należy użyć tablicy rig h t[]
do wyrównania wzorca z tekstem, tak aby znak pasował do swojego pierwsze
go od prawej wystąpienia we wzorcu. W tym celu należy zwiększyć i o j minus
rig h tjc ]. Mniejsza wartość powoduje nałożenie znaku z tekstu na niepasujący
znak wzorca (na prawo od pierwszego od prawej wystąpienia danego znaku).
Także tu można zwiększyć przesunięcie za pomocą tablicy podobnej do tej z me
tody KMP, co pokazano w górnym przykładzie na rysunku na stronie 785.
784 ROZDZIAŁ 5 Łańcuchy znaków
ALGORYTM 5.7. Wyszukiwanie podłańcuchów metodą Boyera-Moore'a
(heurystyka obsługi niedopasowania znaków)
public c la s s BoyerMoore
(
private i n t [] rig h t;
private S trin g pat;
BoyerMoore(String pat)
( // Obliczanie ta b lic y przeskoków,
t h i s . pat = pat;
in t M = p a t . le n g t h ( ) ;
in t R = 256;
rig h t = new i nt [R ];
for (in t c = 0; c < R; C++)
rig h t[ c ] = -1; // -1 dla znaków spoza wzorca,
for (in t j = 0; j < M; j++) // Pierwsza od prawej pozycja
rig h t[ p a t.c h a rA t (j) ] = j; // znaku we wzorcu.
}
public in t se a rch (S trin g txt)
{ // Wyszukiwanie wzorca w tekście txt.
in t N = t x t . l e n g t h ( ) ;
in t M = p a [Link] n g th ();
in t skip;
f o r (in t i = 0; i <= N-M; i += skip)
{ // Czy wzorzec pasuje do tekstu na pozycji i ?
skip = 0;
fo r (in t j = M -l; j >= 0; j — )
i f ([Link](j) != t x t . c h a r A t ( i+ j ) )
{
skip = j - r i g h t [ t x t . c h a r A t ( i + j ) ] ;
i f (skip < 1) skip = 1;
break;
}
i f (skip == 0) return i ; // Znaleziono.
}
return N; // Nie znaleziono.
}
public s t a t ic void main (S t ri ng[] args) // Zobacz stronę 781.
}
Konstruktor w tym algorytmie wyszukiwania podłańcuchów tworzy tablicę zwracającą
pierwsze od prawej wystąpienie we wzorcu każdego możliwego znaku. Metoda search()
sprawdza wzorzec od prawej do lewej i przesuwa wzorzec, aby nałożyć na siebie powodujący
niedopasowanie znak tekstu i pierwsze od prawej wystąpienie tego znaku we wzorcu.
5.3 □ Wyszukiwanie podłańcuchów 785
D Jeśli obliczenia nie spowodo P odstaw ow y pom ysł
i
wały zwiększenia i, wystarczy 1 1
zwiększyć tę zmienną, aby za N
gwarantować, że wzorzec zawsze N E D
t Można zwiększyć
jest przesuwany przynajmniej Zwiększanie i
przesunięcie za pomocą
o j - r ~ i g h t [ ' N '], ]
o jedną pozycję w prawo. Taką aby wyrównać tekst i
tablicy podobnej do
tej z metody KMP
sytuację pokazano na dolnym względem N ze wzorca 1
................................ N
przykładzie na rysunku po pra
N
wej stronie.
Ustawienie j na M- l '
a l g o r y t m 5.7 to prosta implementa :
cja opisanego procesu. Zauważmy, że H eurystyka nie je s t pom ocna
przypisanie - 1 do elementów tablicy i+:
rig h t[] odpowiadających znakom, ł 1
.................................... E
które nie występują we wzorcu, po
N E E D
zwala ujednolicić dwa pierwsze przy Wyrównanie tekstu t
padła (zwiększanie i o j - r ig h t[ tx t. względem pierwszego E i
od prawej spowodowałoby
charAt (i + j) ]).
przesunięcie wzorca w lewo
W kompletnym algorytmie
...................................... E L E
Boyera-Moorea uwzględniane są N E E D L E
Można zwiększyć
wstępnie obliczone niedopasowania przesunięcie za pomocą
wzorca do niego samego (podobnie Należy więc i tablicy podobnej do
zwiększyć i o 1 1 tej z metody KMP
jak w algorytmie KMP). Wersja ta E L
zapewnia wydajność liniową dla naj
gorszego przypadku ( a l g o r y t m 5.7 Ustawienie j na M -l
w najgorszym przypadku może dzia
łać w czasie proporcjonalnym do NM; H e u ry sty k a o b s łu g i n ie d o p a s o w a n ia z n a k u
(n ie p a su ją c y z n a k w y s tę p u je w e w zorcu)
zobacz ć w i c z e n i e 5 .3 .1 9 ). Pomijamy
te obliczenia, ponieważ heurystyka
obsługi niedopasowania znaków za
pewnia dobrą wydajność w typowych
praktycznych zastosowaniach.
Cecha O. Dla typowych danych wejściowych wyszukiwanie podłańcuchów za po
mocą heurystyki obsługi niedopasowania znaków Boyera-Moorea wymaga ~N /M
porównań w celu znalezienia wzorca o długości M w tekście o długości N.
Analiza. Wynik ten można udowodnić dla różnych modeli losowych łańcuchów
znaków, jednak modele te są zwykle nierealistyczne, dlatego pomijamy szczegóło
wy dowód. W wielu praktycznych sytuacjach jest tak, że we wzorcu występuje tylko
kilka spośród wszystkich znaków alfabetu, dlatego prawie wszystkie porównania
powodują pominięcie M znaków, co prowadzi do przedstawionego wyniku.
786 ROZDZIAŁ 5 Q Łańcuchy znaków
Wyszukiwanie metodą „odcisków palców” (metoda Rabina-Karpa)
M etoda opracowana przez M.O. Rabina i R.A. Karpa to zupełnie odm ienne podej
ście do wyszukiwania podłańcuchów, oparte na haszowaniu. Należy obliczyć wartość
funkcji haszującej dla wzorca, a następnie poszukać dopasowania, sprawdzając war
tość funkcji haszującej dla każdego możliwego M-znakowego podlańcucha tekstu.
Po znalezieniu podlańcucha o tej samej wartości skrótu m ożna sprawdzić, czy wy
stępuje dopasowanie. Proces ten to odpowiednik przechowywania wzorca w tablicy
z haszowaniem i sprawdzania każdego podlańcucha tekstu, jednak nie trzeba rezer
wować pamięci na tablicę z haszowaniem, ponieważ używana jest tylko jedna wartość.
Prosta implementacja oparta na tym opisie jest znacznie wolniejsza od wyszukiwania
przez atak siłowy (ponieważ obliczenie wartości funkcji haszującej uwzględniającej
wszystkie znaki jest znacznie kosztowniejsze niż samo porównanie znaków), jednak
Rabin i Karp wykazali, że można łatwo obliczyć funkcję haszującą dla M-znakowych
podłańcuchów w stałym czasie (po wstępnym przetwarzaniu), co w praktyce prowa
dzi do wyszukiwania podłańcuchów w czasie liniowym.
Podstawowy plan Łańcuch znaków o długości Modpowiada M-cyfrowej liczbie o pod
stawie R. Aby zastosować tablicę z haszowaniem o wielkości Q dla kluczy tego rodzaju,
potrzebujemy funkcji haszującej do przekształcania M-cyfrowych liczb o podstawie R
na wartości typu i nt z przedziału od 0 do Q-l. Rozwiązaniem jest haszowanie m odu
larne (zobacz p o d r o z d z i a ł 3 .4 ) — należy obliczyć resztę z dzielenia liczby przez Q.
W praktyce stosujemy losową liczbę pierwszą Q, wybierając jak największą możliwą
wartość, która nie powoduje przepełnienia (można tak zrobić, ponieważ tablicy z ha
szowaniem nie trzeba tu zapisywać). Technikę najprościej zrozumieć na podstawie
małegoQiR = 10, tak jak w przykładzie poniżej. Aby znaleźć wzorzec 2 6 5 3 5 w tek
ście 3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3, należy określić rozmiar tablicy Q(tu jest to 997),
wyznaczyć wartość skrótu (26535 % 997 = 613), a następnie poszukać dopasowania
przez obliczenie wartości skrótu dla każdego pięcioznakowego podlańcucha tekstu.
p a t .c h a r A t (j ) W przykładzie otrzymuje
j 0 1 2 3 4 my wartości skrótu 508,
2 6 5 3 5 % 997 = 613 201, 715, 971, 442 i 929, po
czym natrafiamy na dopa
tx t . c h a r A t ( i) sowanie — 613.
i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3
Obliczanie wartości f u n k
cji haszującej Jeśli war
0 3 1 4 1 5 % 997 = 508
tości są pięciocyfrowe,
1 1 4 1 5 9 % 997 = 201
można wykonać wszystkie
2 4 1 5 9 2 % 997 = 715
potrzebne obliczenia za po
3 1 5 9 2 6 % 997 = 971
mocą typu i nt, co jednak
4 5 9 2 6 5 % 997 = 442 Dopasc
442 Dopasowanie zrobić, jeśli M jest równe
5 9 2 6 5 3 % 997 = 929 / 100 lub 1000? Proste zasto
6 -*— Zwracanie
acanie i = 6 2 6 5 3 5 % 997 = 613 sowanie m etody Hornera,
Podstawy wyszukiwania podłańcuchów metodą Rabina-Karpa opisanej w p o d r o z d z i a l e
5.3 □ Wyszukiwanie podłańcuchów 787
3.4 w kontekście łańcuchów znaków i innych typów kluczy o wielu wartościach, pro
wadzi do kodu pokazanego po prawej, który w czasie proporcjonalnym do Moblicza
wartość funkcji haszującej dla M-cyfrowej liczby o podstawie R, reprezentowanej jako
tablica wartości typu char. Mjest przekazywane jako argument, dlatego — jak się okaże
— metodę m ożna zastosować zarówno do wzor
ca, jak i do tekstu. Dla każdej cyfry w liczbie p riv a t e long hash ( s t r i n g key, i n t M)
. . . , , . , . ,, { // O b licza nie s krótu dla key[ 0 . . M-1].
należy pomnożyć dotychczasową wartość h _ Q.
przez R, dodać cyfrę i obliczyć resztę z dzielenia for (i n t j = 0; j < M; j++)
przez Q. W dolnej części strony pokazano, jak h = (R * h + k ey.c h a rA t( j)) % Q;
obliczyć wartość funkcji haszującej dla wzorca. return h,
Tę samą metodę m ożna wykorzystać do obli
czenia wartości funkcji haszujących dla teks- M e t o d a H o rn e ra z a s to s o w a n a
tu, jednak koszt wyszukiwania podłańcuchów d o h a s z o w a n ia m o d u la r n e g o
obejmowałby mnożenie, dodawanie i obliczanie reszty dla każdego znaku tekstu. Dla
najgorszego przypadku daje to N M operacji, co oznacza brak poprawy w porównaniu
do ataku siłowego.
K luczow y p o m ysł Metoda Rabina-Karpa oparta jest na wydajnym obliczaniu funk
cji haszującej dla pozycji i +1 w tekście na podstawie wartości dla pozycji i . Technika
wynika bezpośrednio z prostych matematycznych wzorów. Zapis t. to wartość t x t .
charA t(i). Liczba odpowiadająca M-znakowemu podłańcuchowi tekstu tx t zaczy
nająca się od pozycji i jest równa:
x.1 =1 tR M1 + t.z+1 RM'2 + ... + t.i+ Mu-1 R°
Można założyć, że znana jest wartość h{x.) = x. mod Q. Przesunięcie o jedną pozycję
w prawo w tekście odpowiada zastąpieniu x przez:
x.j+1, = (x
v 1
- tR
i
M')R
'
+ ti + M
Należy odjąć początkową cyfrę, pomnożyć wartość przez R, a następnie dodać koń
cową cyfrę. Najważniejsze jest to, że nie trzeba przechowywać wartości liczb, a tylko
wartości ich reszt z dzielenia przez Q. Podstawową cechą operacji modulo jest to, że
jeśli obliczymy resztę z dzielenia przez Qpo każdej operacji arytmetycznej, uzyskamy
ten sam wynik, co po wykonaniu wszystkich operacji arytmetycznych i późniejszym
obliczeniu reszty. Tę cechę wy
korzystaliśmy już wcześniej, charA t(j)
przy implementowaniu ha- . 0 1 2 3 4
szowania m odularnego me- 2 g 5 3 f
todą Hornera (zobacz stronę 0 2 % 997 = 2 R Q
/ /
472). Efekt jest taki, że można 1 2 g % 997 = ( 2 n o + 6) % 997 = 26
wydajnie przesuwać się w tek-
, ’ 1 r Y . 2 2 6 5 % 997 = ( 2 6 * 1 0 + 5) % 997 = 265
scie w prawo o jedną pozycję
3 2 6 5 3 % 997 = ( 2 6 5 * 1 0 + 3) % 997 = 659
w stałym czasie niezależnie
4 2 6 5 3 5 % 997 = ( 6 5 9 * 1 0 + 5) % 997 = 613
od tego, czy Mjest równe 5,
100 czy 1000. Obliczanie wartości skrótu dla wzorca metodą Hornera
788 ROZDZIAŁ 5 0 Łańcuchy znaków
3 4 5 6 7 i Im p lem en ta cja Om ówienie
B ie żą ca w arto ść 1 4 1 5 9 2 6 5 - * - > 7- ^ bezpośrednio prowadzi do
N o w a w arto ść 4 1 5 9 2 6 5 implementacji wyszukiwa
nia podłańcuchów przedsta-
4 1 5 9 2 B ie żą ca w artość
„ „ „ „ W lO n e i W A L G O R Y T M IE 5 .8 .
- 4 0 0 0 0 ’ 3
,I 5r 9n i2 ,
Odjęcie p o cz ą tk o w e j cyfry
r Konstruktor oblicza wartość
* 1 0 M n o ż e n ie p rze z p o d sta w ę skrótu P^tHash dla wzorca.
1 5 9 2 0 Oblicza też wartość R mod
+ 6D o d a w a n ie n o w e j k o ń c o w e j cyfry Q i zapisuje ją W zmiennej
1 5 9 2 6 N o w a w arto ść RM. M etoda hashSearch() za-
Obliczanie klucza przy wyszukiwaniu podłańcuchów metodą działanie od obliczenia C Zyna
wartości funkcji haSZlljącej dla
Rabina-Karpa (przechodzenie w tekście w prawo o jedną pozycję)
pierwszych M znaków tekstu
i porównania wyniku z wartością skrótu dla wzorca. Jeśli wartości nie pasują, metoda
przechodzi dalej w tekście i za pomocą opisanej wcześniej techniki oblicza dla każdego
i wartość skrótu dla Mznaków, począwszy od pozycji i , zapisuje tę wartość w zmiennej
txtHash i porównuje każdą nową wartość skrótu z wartością patHash. Przy obliczaniu
wartości txtHash dodawane jest Q, co pozwala zagwarantować, że wszystkie wartości
są dodatnie, dzięki czemu określanie reszty przebiega prawidłowo.
Sztuczka — popraw ność m etody M onte Carlo Można oczekiwać, że po ustaleniu
dla M-znakowego podłańcucha tekstu tx t wartości skrótu, która pasuje do w arto
ści skrótu wzorca, kod porówna znaki podłańcucha ze wzorcem, aby sprawdzić, czy
występuje rzeczywiste dopasowanie, a nie tylko zbieżność skrótów. Nie postępujemy
w ten sposób, ponieważ wymaga to cofania się w tekście. Zamiast tego stosujemy
dowolnie dużą „wielkość” tablicy z haszowaniem, Q, ponieważ nie tworzymy taldej
tablicy, a jedynie sprawdzamy zbieżność z jednym kluczem — wzorcem. Używamy
i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3
0 3 % 997 = 3
/
1 3 1 % 997 = ( 3 * 1 0 + 1) % 997 = 31
2 3 1 4 % 997 = (3 1 * 1 0 + 4 ) % 997 = 314
3 3 1 4 1 %1 997 = (3 1 4 *1 0 + 1) % 997 = 150
4 3 1 4 1 5 % 997 = (1 5 0 * 1 0 + 5) % 997 = 508 /RM^ / R
5 1 4 1 5 9 % 997 = ( ( 5 0 8 + 3 * ( 9 9 7 - 3 0 ) ) * 1 0 + 9) % 997 = 201
6 4 1 5 9 2 % 997 = ( ( 2 0 1 + 1 * (9 9 7 - 3 0 ) ) *1 0 + 2) % 997 = 715 D o p a so w a n ie
7 1 5 9 2 6 % 997 = ((7 1 5 + 4 * ( 9 9 7 - 3 0 ) ) * 1 0 + 6 ) % 997 = 971
8 5 9 2 6 5 % 997 = ( ( 9 7 1 + 1 * ( 9 9 7 - 3 0 ) ) * 1 0 + 5) % 997 = 442
9 9 2 6 5 3 % 997 = ((4 4 2 + 5 * ( 9 9 7 - 3 0 ) ) * 1 0 + 3) % 997 = 929
10 — Z w ra c a n ie i-M + 1 2 6 5 3 5 % 997 = ( ( 9 2 9 + 9 * ( 9 9 7 - 3 0 ) ) * 1 0 + 5) % 997 = 613
Przykładowe wyszukiwanie podłańcuchów metodą Rabina-Karpa
5.3 Wyszukiwanie podłańcuchów 789
ALGORYTM 5.8. Wyszukiwanie podłańcuchów na podstawie „odcisków palców"
(metoda Rabina-Karpa)
p u b l i c c l a s s R a b in K a rp
{
p r iv a t e S t r i n g pat; // Wzorzec ( p o t r z e b n y ty lk o w w ersji Las V e g a s ) .
p r i v a t e long patHash; // W a rto ść s k r ó t u d l a w zorca.
p r i v a t e i n t M; // DTugość wzorca.
p r i v a t e l o n g Q; // Duża l i c z b a p ie r w s z a .
p r i v a t e i n t R = 256; // Ro zm ia r a l f a b e t u .
p r i v a t e l o n g RM; // RA (M -1) % Q
p u b l i c R a b i n K a r p ( S t r i n g pat)
{
t h i s . pat = p a t ; // Z a p i s y w a n i e w zorca ( p o t r z e b n e t y l k o w w e r s j i
// Las V e g a s ) .
this.M = p a t . l e n g t h ( ) ;
Q = lo n g R a n d o m P r i m e O ; // Zobacz ć w i c z e n i e 5 . 3 . 3 3 .
RM = 1;
f o r ( i n t i = 1; i <= M - l ; i + + ) // O b l i c z a n i e RA(M-1) % 0 na p o t r z e b y
RM = (R * RM) % Q; // usu w an ia p oczątko w ej c y f r y .
patHa sh = h a s h ( p a t , M);
p u b l i c b o o le a n c h e c k ( i n t i ) // Monte C a r l o (z o b a c z o p i s w t e k ś c i e ) .
{ re tu rn true; ) // W w e r s j i Las Vegas n a l e ż y porównać
// pat z t x t ( i . . i - M + 1 ) .
p r i v a t e lo n g h a s h ( S t r i n g key, i n t M)
// Zobacz o p i s w t e k ś c i e ( s t r o n a 7 8 7 ) .
p riva te in t se a rc h (S trin g txt)
{ // Szu ka p a s u j ą c e g o s k r ó t u w t e k ś c i e ,
in t N = t x t .le n g t h ();
long txtHash = h a sh ( tx t, M ) ;
i f (p a tH a s h == t x t H a s h && c h e c k ( O ) ) r e t u r n 0; // D opasow anie na
// p o c z ą t k u .
f o r ( i n t i = M; i < N; i + + )
( // Usuw anie początkow ej c y f r y , dodawanie końcowej c y f r y
// i s p r a w d z a n ie d op aso w a n ia .
t x t H a s h = ( t x t H a s h + Q - R M * t x t . c h a r A t ( i - M ) % Q) % Q;
t x t H a s h = ( t x t H a s h * R + t x t . c h a r A t ( i ) ) % Q;
i f (p atH a sh == t x t H a s h )
i f ( c h e c k ( i - M + 1 )) r e t u r n i - M + 1 ; // D opasow anie.
}
r e t u r n N; // N ie z n a l e z i o n o d op aso w a n ia .
}
}
Ten algorytm wyszukiwania podłańcuchów jest oparty na haszowaniu. Oblicza w konstruk
torze wartość skrótu dla wzorca, a następnie szuka w tekście pasującego skrótu.
790 ROZDZIAŁ 5 0 Łańcuchy znaków
wartości typu 1 ong większej niż 10 20, przez co prawdopodobieństwo, że losowy klucz
ma skrót o tej samej wartości, co wzorzec, jest mniejsze niż 10'20. Jest to niezwykle
m ała wartość. Jeżeli uznasz, że to i tak za dużo, możesz ponownie uruchomić algoryt
my, aby uzyskać prawdopodobieństwo niepowodzenia poniżej 1(L40. Omawiany algo
rytm jest wczesnym i znanym przykładem zastosowania m etody Monte Cario, który
pozwala zagwarantować określony czas ukończenia, jednak może — choć z małym
prawdopodobieństwem — wygenerować błędną odpowiedź. Inna metoda sprawdza
nia dopasowania może być wolna (czasem, z bardzo małym prawdopodobieństwem,
może działać tak, jak atak siłowy), jednak gwarantuje poprawność. Algorytmy tego
typu noszą nazwę Las Vegas.
Cecha P. Wersja Monte Cario wyszukiwania podłańcuchów m etodą Rabina-
Karpa działa w czasie liniowym i z bardzo wysokim prawdopodobieństwem daje
prawidłowy wynik, natomiast wersja Las Vegas działa prawidłowo i z bardzo d u
żym prawdopodobieństwem kończy pracę w czasie liniowym
Analiza. Zastosowanie bardzo dużej wartości Q, co jest możliwe z uwagi na
to, że nie trzeba przechowywać tablicy z haszowaniem, sprawia, iż wystąpienie
kolizji skrótów jest niezwykle mało prawdopodobne. Rabin i Karp wykazali, że
przy prawidłowym wyborze Q kolizja skrótów dla losowych łańcuchów znaków
występuje z prawdopodobieństwem 1/Q, dlatego dla występujących w praktyce
wartości zmiennych metoda nie wykrywa dopasowania skrótów, jeśli nie istnieje
pasujący podłańcuch, i znajduje tylko jedno dopasowanie skrótów, jeżeli pasują
cy podłańcuch istnieje. Teoretycznie może wystąpić kolizja skrótów bez dopaso
wania podłańcuchów, jednak w praktyce m ożna polegać na tym, że znaleziono
dopasowanie.
Jeśli Twoja wiara w teorię prawdopodobieństwa (lub model losowych łańcuchów zna
ków i kod używany do generowania liczb losowych) nie jest wystarczająca, możesz
dodać do m etody check() kod sprawdzający, czy tekst pasuje do wzorca. Powoduje
to przekształcenie a l g o r y t m u 5.8 w wersję Las Vegas (zobacz ć w i c z e n i e 5 .3 . 1 2 ).
Jeśli ponadto dodasz test, aby sprawdzić, czy nowy kod jest kiedykolwiek potrzebny,
możliwe, że z czasem nabierzesz zaufania do teorii prawdopodobieństwa.
w y s z u k i w a n i e p o d ł a ń c u c h ó w m e t o d ą r a b i n a - k a r p a jest nazywane wyszuki-
waniem za pomocą „odcisków palców”, ponieważ mała ilość informacji służy do re
prezentowania potencjalnie bardzo dużego wzorca. M etoda wyszukuje „odciski pal
ców” (wartość skrótu) w tekście. Algorytm jest wydajny, ponieważ „odciski palców”
można wydajnie obliczać i porównywać.
5.3 a Wyszukiwanie podłańcuchów 791
Podsumowanie Tabela w dolnej części strony to podsumowanie omówionych
algorytmów wyszukiwania podłańcuchów. Każdy z nich ma atrakcyjne cechy, co czę
sto się zdarza, jeśli kilka algorytmów wykonuje to samo zadanie. Wyszukiwanie przez
atak siłowy jest łatwe do zaimplementowania i działa w typowych sytuacjach (tech
nikę tę zastosowano w metodzie i n d e x O f ( ) klasy S t r i n g Javy). Algorytm Knutha-
M orrisa-Pratta gwarantuje liniowy czas wykonania bez cofania się w danych wej
ściowych. Rozwiązanie Boyera-Moorea w typowych sytuacjach jest szybsze od linio
wego (o czynnik M), a m etoda Rabina-Karpa działa liniowo. Każda technika m a też
wady. Wyszukiwanie przez atak siłowy może wymagać czasu w ilości proporcjonal
nej do MN. Metody Knutha-M orrisa-Pratta i Boyera-Moorea wymagają dodatkowej
pamięci, a technika Rabina-Karpa ma stosunkowo długą pętlę wewnętrzną (kilka
operacji arytmetycznych w odróżnieniu od porównań znaków w innych metodach).
Podsumowanie tych cech obejmuje tabela poniżej.
Liczba operacji C o fa n ie
D o d a tk o w a
A lg o ry tm W ersja w danych P o p ra w n y ?
p a m ię ć
G w a r a n to w a n a Typow o w e jśc io w y c h ?
A tak siłowy - MN 1 ,1 N Tak Tak 1
Pełny automat DFA
2N 1 .1 N Nie Tak MR
Knutha- (algorytm 5.6)
Morrisa-
Pratta Przejścia tylko przy
3N 1 .1 N Nie Tak M
niedopasowaniu
Pełny algorytm 3N N/M Tak Tak R
Boyera- Heurystyka obsługi
Moorea niedopasowania MN N/M Tak Tak R
znaków (algorytm 5.7)
Monte Cario
7N 7N Nie Tak/ 1
Rabina-Karpaf (algorytm 5.8)
Las Vegas 7Nł 7N Tak Tak 1
fGwarancje probabilistyczne przy równomiernej i niezależnej funkcji haszującej
Podsumowanie kosztów implementacji metod wyszukiwania podłańcuchów
792 ROZDZIAŁ 5 a Łańcuchy znaków
j PYTANIA I O D PO W IED ZI
P. Problem wyszukiwania podłańcuchów wydaje się trochę sztuczny. Czy naprawdę
muszę rozumieć wszystkie te skomplikowane algorytmy?
O. No cóż, przyspieszenie o czynnik równy M, jakie pozwala uzyskać metoda
Boyera-Moorea, może w praktyce przynieść imponujące efekty. Ponadto możliwość
strumieniowego przesyłania danych wejściowych (bez cofania) prowadzi do wielu
praktycznych zastosowań m etod KMP i Rabina-Karpa. Omawiane zagadnienie nie
tylko dotyczy bezpośrednich praktycznych zastosowań, ale też stanowi wprowadze
nie do korzystania z automatów abstrakcyjnych i randomizacji przy projektowaniu
algorytmów.
P. Dlaczego nie uprościć pracy przez przekształcenie każdego znaku na postać bi
narną i potraktowanie całego tekstu jako binarnego?
O. Pomysł ten nie jest skuteczny z uwagi na błędne dopasowania przy granicach
znaków.
5.3 Q Wyszukiwanie podłańcuchów
ĆWICZENIA
Opracuj implementację wyszukiwania podłańcuchów przez atak siłowy,
5 . 3 .1 .
Brute, opartą na tym samym interfejsie API, co a l g o r y t m 5 .6 .
Przedstaw zawartość tablicy dfa [] [] dla algorytmu Knutha-M orrisa-Pratta
5 . 3 .2 .
dla wzorca AAAAAAAAA. Narysuj automat DFA podobny do rysunków przedstawio
nych w tekście.
Przedstaw zawartość tablicy dfa[] [] dla algorytmu Knutha-M orrisa-Pratta
5 . 3 .3 .
dla wzorca ABRACADABRA. Narysuj automat DFA podobny do rysunków przedstawio
nych w tekście.
5.3.4. Napisz wydajną metodę, która jako argumenty przyjmuje łańcuch znaków tx t
i liczbę całkowitą Moraz zwraca pozycję pierwszego wystąpienia Mkolejnych odstę
pów w łańcuchu lub — jeśli taki ciąg nie występuje — wartość t x t . 1ength. Oszacuj
liczbę porównań znaków wykonywanych przez metodę dla typowego tekstu i dla naj
gorszego przypadku.
Opracuj implementację wyszukiwania podłańcuchów przez atak siłowy,
5 . 3 .5 .
BruteForceRL, przetwarzającą wzorzec od prawej do lewej (ma to być uproszczona
wersja a l g o r y t m u 5 .7 ).
5 . 3 .6 . Podaj zawartość tablicy ri ght [] wyznaczoną przez konstruktor z a l g o r y t m u
5.7 dla wzorca ABRACADABRA.
5.3.7. Dodaj do implementacji wyszukiwania podłańcuchów przez atak siłowy m e
todę count() do zliczania wystąpień wzorca i metodę searchAl 1 () do wyświetlania
wszystkich wystąpień.
Dodaj do klasy KMP metodę count() do zliczania wystąpień wzorca i metodę
5 . 3 .8 .
searchAl 1 () do wyświetlania wszystkich wystąpień.
5.3.9. Dodaj do klasy BoyerMoore metodę count () do zliczania wystąpień wzorca
i metodę searchAl 1 () do wyświetlania wszystkich wystąpień.
5.3.10. Dodaj do klasy RabinKarp metodę count () do zliczania wystąpień wzorca
i metodę searchAl 1 () do wyświetlania wszystkich wystąpień.
5 .[Link]órz dane dla najgorszego przypadku dla implementacji metody Boyera-
M oorea ( a l g o r y t m 5 .7 ), aby pokazać, że nie działa ona w czasie liniowym.
5.3.12. Do metody check() w klasie RabinKarp ( a l g o r y t m 5 .8) dodaj kod prze
kształcający rozwiązanie na wersję Las Vegas (należy sprawdzić, czy wzorzec pasuje
do tekstu na pozycji podanej jako argument).
794 ROZDZIAŁ 5 ■ Łańcuchy znaków
ĆW ICZEN IA (ciąg dalszy)
5.3.13. Wykaż, że w implementacji metody Boyera-Moorea ( a l g o r y t m 5 .7 ) m oż
na ustawić wartość ri ght [c] na przedostatnie wystąpienie c, jeśli c jest ostatnim zna
kiem wzorca.
5 . 3 . 1 4 . Opracuj wersje implementacji m etod wyszukiwania podłańcuchów z tego
podrozdziału, wykorzystując do reprezentowania wzorca i tekstu tablice char[] za
miast zmiennych typu S tri ng.
5.3.15. Opracuj implementację wyszukiwania podłańcuchów przez atak siłowy,
sprawdzającą wzorzec od prawej do lewej.
5 . 3 . 1 6 . Przedstaw ślad działania algorytmu opartego na ataku siłowym (tak jak na
rysunkach w tekście) dla poniższych wzorców i tekstów.
a. Wzorzec: AAAAAAAB Tekst: AAAAAAAAAAAAAAAAAAAAAAAAB
b. Wzorzec: ABABABAB Tekst: ABABABABAABABABABAAAAAAAA
5.3.17. Narysuj automat DFA z metody KMP dla poniższych wzorców.
a. AAAAAAB
b. AACAAAB
c. ABABABAB
d. ABAABAAABAAAB
e. ABAABCABAABCB
5.3.18. Załóżmy, że wzorzec i tekst to losowe łańcuchy znaków oparte na alfabecie
o rozmiarze R (równym przynajmniej 2). Wykaż, że oczekiwana liczba porównań
znaków w ataku siłowym wynosi (N - M + 1) (1 - R'M) / (1 - R ') < 2(N - M + 1).
5 . 3 . 1 9 . Przedstaw przykład, w którym algorytm Boyera-Moorea (w wersji z samą
heurystyką obsługi niedopasowania znaków) ma niską wydajność.
5 . 3 . 2 0 . Jak zmodyfikowałbyś algorytm Rabina-Karpa, aby ustalić, czy w tekście wy
stępuje dowolny z podzbioru k wzorców (załóżmy, że wszystkie są równej długości)?
Rozwiązanie: należy obliczyć skróty k wzorców i zapisać je w zbiorze StringSET
(zobacz ć w i c z e n i e 5 .2 .6).
5 . 3 . 2 1 . Jak zmodyfikowałbyś algorytm Rabina-Karpa, aby znaleźć dany wzorzec
przy dodatkowym warunku, zgodnie z którym środkowy znak jest symbolem wielo
znacznym (pasuje do niego dowolny znak)?
5.3 ■ Wyszukiwanie podłańcuchów
5.3.22. Jak zmodyfikowałbyś algorytm Rabina-Karpa, aby znaleźć wzorzec o wy
miarach H n a Y w tekście o wymiarach N na N?
5.3.23. Napisz program, który wczytuje znaki jeden po drugim i za każdym razem
określa, czy badany łańcuch jest palindromem. Wskazówka: użyj techniki haszowa-
nia z metody Rabina-Karpa.
796 ROZDZIAŁ 5 ® Łańcuchy znaków
[ j PROBLEMY DO ROZWIĄZANIA
5.3.24. Znajdowanie wszystkich wystąpień. Do każdego z czterech podanych w tek
ście algorytmów wyszukiwania podłańcuchów dodaj metodę findAl 1 (), która zwraca
wartość typu Iterab l e<Integer>, umożliwiającą klientom iterowanie po wszystkich
pozycjach wzorca w tekście.
5.3.25. Przesyłanie strumieniowe. Dodaj do klasy KMP metodę search(), która jako
argument przyjmuje zmienną typu In i w podanym strum ieniu wejściowym wyszu
kuje wzorzec bez korzystania z dodatkowych zmiennych egzemplarza. Następnie
zrób to samo dla klasy Rab i n Karp.
5.3.26. Wykrywanie rotacji cyklicznej. Napisz program, który dla dwóch łańcuchów
znaków określa, czy jeden z nich jest rotacją cykliczną drugiego (na przykład przy-
kl ad i kl adprzy).
5.3.27. Wykrywanie wielokrotnych powtórzeń. Wielokrotne powtórzenie bazowego
łańcucha znaków b w łańcuchu znaków s to taki podłańcuch s, który obejmuje przy
najmniej dwie kolejne kopie b (niepokrywające się). Wymyśl i zaimplementuj dzia
łający w czasie liniowym algorytm, który dla dwóch łańcuchów znaków b i s zwraca
indeks początku najdłuższego wielokrotnego powtórzenia b w s. Przykładowo, pro
gram powinien zwrócić 3, jeśli b ma wartość abcab, a s to abcabcababcababcababcab.
5.3.28. Buforowanie w wyszukiwaniu przez atak siłowy. Do rozwiązania ć w ic z e n ia
5 .3.1 dodaj metodę search(), która jako argument przyjmuje strumień wejściowy
(typu In) i wyszukuje wzorzec w podanym strumieniu wejściowym. Uwaga-, trzeba
utrzymywać bufor, w którym można umieścić przynajmniej Mwcześniejszych znaków
ze strumienia wejściowego. Zadanie polega na napisaniu wydajnego kodu do inicjowa
nia, aktualizowania i czyszczenia bufora dla dowolnego strumienia wejściowego.
5.3.29. Buforowanie w algorytmie Boyera-Moorea. Do a l g o r y t m u 5.7 dodaj m eto
dę search(), która jako argument przyjmuje strum ień wejściowy (typu In) i wyszu
kuje wzorzec w danym strum ieniu wejściowym.
5.3.30. Wyszukiwanie dwuwymiarowe. Zaimplementuj wersję algorytmu Rabina-
Karpa do wyszukiwania wzorców w tekście dwuwymiarowym. Przyjmij, że zarówno
wzorzec, jak i tekst to znaki tworzące prostokąt.
5.3.31. Wzorce losowe. Ile porównań znaków jest potrzebnych, aby znaleźć losowy
wzorzec o długości 100 w danym tekście?
Odpowiedź: ani jednego. Metoda:
public boolean search(char[] tx t)
( return false; }
5.3 □ Wyszukiwanie podłańcuchów 797
skutecznie rozwiązuje ten problem, ponieważ prawdopodobieństwo, że losowy wzo
rzec o długości 100 wystąpi w jakimkolwiek tekście, jest tak niskie, iż m ożna uznać
je za zerowe.
5.3.32. Unikatowepodłańcuchy. Rozwiąż ć w ic z e n ie 5 .2.14 za pom ocą pomysłu, na
którym oparta jest metoda Rabina-Karpa.
5.3.33. Losowe liczby pierwsze. Zaimplementuj metodę longRandomPrime() dla kla
sy RabinKarp ( a l g o r y t m 5 .8). Wskazówka: losowa n - cyfrowa liczba jest pierwsza
z prawdopodobieństwem proporcjonalnym do 1In.
5.3.34. Kod bez pętli. Maszyna JVM (i język asemblerowy komputera) obsługuje
instrukcję goto, dlatego wyszukiwanie m ożna „podłączyć” do kodu maszynowego,
takiego jak program widoczny po prawej (kod ten
in t i = -1;
działa analogicznie do symulacji automatu DFA dla
sm: i++;
wzorca z klasy KMPdfa, jest jednak znacznie wydaj sO: i f ( t x t [ i ] ) != ' A ' goto sm;
niejszy). Aby uniknąć sprawdzania przy każdym si: i f ( t x t [ i ] ) != ' A ' goto sO;
s2: i f ( t x t [ i ]) != ' B ' goto sO;
zwiększeniu i , czy napotkano koniec tekstu, zakła
s3: i f ( t x t [ i ]) != ' A ' goto s2;
damy, że sam wzorzec jest zapisany jako wartownik s4: i f ( t x t [ i ]) != ' A ' goto sO;
w Mostatnich znakach tekstu. Etykiety goto w kodzie s5: i f ( t x t [ i ] ) != ' A ' goto s3;
odpowiadają tablicy dfa []. Napisz metodę statyczną, return i - 8 ;
która jako dane wejściowe przyjmuje wzorzec, a jako w yszukiw anie podłańcucha a a b a a a bez pętli
dane wyjściowe generuje program bez pętli (taki jak
pokazany) wyszukujący dany wzorzec.
5.3.35. Metoda Boyera-Moored dla łańcuchów binarnych. Heurystyka obsługi niedo
pasowania znaków nie jest zbyt pom ocna w kontekście binarnych łańcuchów znaków,
ponieważ niedopasowanie mogą powodować tylko dwa możliwe znaki (i przeważnie
oba występują we wzorcu). Opracuj klasę do wyszukiwania podłańcuchów w łańcu
chach binarnych. Klasa ma grupować bity w „znaki”, które m ożna wykorzystać w taki
sam sposób, jak w a l g o r y t m i e 5 .7 . Uwaga: przy pobieraniu b bitów jednocześnie
potrzebna jest tablica ri ght [] o 2b elementach. Wartość b powinna być na tyle mała,
aby tablica nie była zbyt długa, a przy tym na tyle duża, aby dla większości ¿»-bitowych
fragmentów tekstu prawdopodobieństwo ich wystąpienia we wzorcu było niskie. We
wzorcu występuje M - b + 1 różnych ¿»-bitowych fragmentów (po jednym rozpoczy
nającym się od pozycji każdego bitu od 1 do M - ¿> + 1), dlatego wartość M - b + 1
powinna być znacznie niższa niż 2b. Przykładowo, jeśli 2b to około lg (4M), tablica
ri gth [] będzie w ponad % zapełniona wartościami -1. Trzeba jednak uważać, aby b
nie było mniejsze niż M l2, ponieważ w przeciwnym razie może nastąpić pominięcie
wzorca, jeśli zostanie podzielony między dwa ¿»-bitowe fragmenty tekstu.
798 ROZDZIAŁ 5 a Łańcuchy znaków
[ j EKSPERYMENTY
5.3.36. Losowy tekst. Napisz program, który jako argumenty przyjmuje liczby cał
kowite Mi N, generuje losowy binarny łańcuch o długości N, a następnie zlicza inne
wystąpienia ostatnich Mbitów tekstu. Uwaga: dla różnych wartości Modpowiednie
mogą być inne metody.
5.3.37. Metoda KMP dla losowego tekstu. Napisz klienta, który jako dane wejściowe
przyjmuje liczby całkowite M, Ni T, a następnie T razy wykonuje następujący ekspery
m ent — generuje losowy wzorzec o długości Mi losowy tekst o długości Noraz zlicza
porównania znaków potrzebne klasie KMP na znalezienie wzorca w tekście. Dopracuj
klasę KMP tak, aby udostępniała liczbę porównań, i wyświetl średnią liczbę porównań
dla T prób.
5.3.38. Metoda Boyera-Moorea dla losowego tekstu. Wykonaj poprzednie ćwiczenie
dla klasy BoyerMoore.
5.3.39. Czas działania. Napisz program, który mierzy czas wyszukiwania przez
cztery przedstawione m etody poniższego podłańcucha:
it is a f a r f a r b e tt e r thing th a t 1 do t h a n i have e v e r done
w tekście książki Tale o f Two Cities ([Link]). Omów, w jakim stopniu wyniki potwier
dzają postawione w tekście hipotezy na temat wydajności.
w w i e l u a p l i k a c j a c h potrzebne jest wyszukiwanie podłańcuchów bez komplet
nych informacji na temat wzorca. Użytkownik edytora tekstu może chcieć określić
tylko część wzorca, podać wzorzec pasujący do kilku różnych słów lub stwierdzić,
że akceptowalny jest jeden z kilku wzorców. Biolog może szukać sekwencji genów
spełniającej pewne warunki. W tym podrozdziale opisujemy, jak w wydajny sposób
przeprowadzić tego rodzaju dopasowywanie do wzorca.
Algorytmy z poprzedniego podrozdziału wymagają podania kompletnego wzorca,
dlatego trzeba rozważyć inne rozwiązania. Podstawowe mechanizmy, które tu opisu
jemy, stanowią podstawę bardzo rozbudowanej techniki wyszukiwania łańcuchów
znaków. Pozwala ona dopasowywać skomplikowane M-znakowe wzorce do frag
mentów N-znakowych tekstów w czasie proporcjonalnym do M N dla najgorszego
przypadku i znacznie szybciej w typowych sytuacjach.
Najpierw potrzebny jest sposób na opisywanie wzorców — precyzyjny sposób
określania wspomnianych wcześniej problemów wyszukiwania niepełnych podłań
cuchów. Specyfikacja musi obejmować bardziej zaawansowane operacje podstawowe
niż stosowaną w poprzednim podrozdziale operację „sprawdź, czy i-ty znak tekstu
pasuje do j-tego znaku wzorca”. Dlatego stosujemy wyrażenia regularne, które opisują
wzorce w połączeniu z trzema naturalnymi, podstawowymi i rozbudowanymi ope
racjami.
Programiści korzystają z wyrażeń regularnych od dziesięcioleci. Z uwagi na bły
skawicznie rosnącą liczbę możliwości przeszukiwania sieci W W W zakres zastoso
wań wyrażeń regularnych jeszcze się zwiększył. Na początku podrozdziału omawia
my liczne specyficzne zastosowania. Nie tylko pokazuje to przydatność i możliwości
wyrażeń regularnych, ale też pozwala lepiej poznać ich podstawowe cechy.
Tak jak w przypadku algorytmu KMP przedstawionego w poprzednim podroz
dziale, tak i tu rozważamy trzy podstawowe operacje w kategoriach abstrakcyjnego
automatu do wyszukiwania wzorców w tekście. Następnie, tak jak wcześniej, pokazu
jemy tworzenie takiego automatu i symulowanie jego działania przez algorytm dopa
sowywania wzorców. Oczywiście, automaty do dopasowywania wzorców są zwykle
bardziej skomplikowane niż automat DFA z algorytmu KMP, jednak są mniej złożo
ne, niż m ożna by podejrzewać.
Jak widać, rozwiązanie problemu dopasowywania wzorców jest blisko związane
z podstawowymi procesami z obszaru nauk komputerowych. Przykładowo, m eto
da używana w programie do wyszukiwania łańcuchów znaków wyznaczanych przez
dany opis wzorca przypomina metodę wykorzystywaną w systemie Javy do prze
kształcania danego program u Javy na program w języku maszynowym komputera.
Ponadto omawiane jest zagadnienie niedeterminizmu, które odgrywa kluczową rolę
w poszukiwaniu wydajnych algorytmów (zobacz r o z d z i a ł 6.).
800
5.4 □ Wyrażenia regularne 801
Opisywanie wzorców za pomocą wyrażeń regularnych Koncentrujemy
się na opisach wzorców składających się ze znaków, które są operandam i dla trzech
podstawowych operacji. W tym kontekście słowo język oznacza zbiór łańcuchów zna
ków (potencjalnie nieskończony), a słowo wzorzec — specyfikację języka. Rozważane
reguły są analogiczne do znanych reguł tworzenia wyrażeń arytmetycznych.
Złączanie (konkatenacja) Pierwszą podstawową operację stosowaliśmy w po
przednim podrozdziale. Przez napisanie ciągu AB tworzymy język {AB}. Obejmuje on
jeden dwuznakowy łańcuch, utworzony przez złączenie A i B.
L u bDruga podstawowa operacja umożliwia określanie różnych możliwości we
wzorcu. Jeśli dwie możliwości są połączone operatorem lub, obie należą do języka.
Do oznaczania tej operacji używamy symbolu | . Przykładowo, zapis A | Bwyznacza
język{A, B},azapisA | E | I | 0 | U— język{A, E, I, 0, U}. Złączanie ma wyż
szy priorytet niż operacja lub, tak więc zapis AB | BCD wyznacza język {AB, BCD}.
D om knięcie Trzecia podstawowa operacja umożliwia powielanie części wzorca.
Domknięcie wzorca to język łańcuchów znaków utworzony przez złączenie wzor
ca z nim samym dowolną liczbę razy (w tym zero). Domknięcie zapisujemy przez
umieszczenie symbolu * po powtarzanym wzorcu. Domknięcie ma wyższy priorytet
niż złączanie, dlatego zapis AB* wyznacza język składający się z łańcuchów znaków,
w którym występuje litera A, a po niej 0 lub więcej liter B. Zapis A * Bto język obejm u
jący łańcuchy znaków o 0 lub więcej literach A, po których następuje B. Pusty łańcuch
znaków, zapisywany jako e, znajduje się w każdym tekście (także w A*).
N aw iasy Nawiasy stosujemy do zmieniania domyślnych reguł pierwszeństwa.
Przykładowo, zapis C(AC |B)D wyznacza język {CACD, CBD}, zapis (A | C) ( (B | C) D) wy
znacza język {ABD, CBD, ACD, CCD}, azapis (AB)*— wyznacza język łańcuchów zna
ków utworzonych przez złączenie dowolnej (w tym zerowej) liczby wystąpień ciągu
AB — {e, AB, ABAB,
W y ra ż e n ie r e g u la r n e P a s u je d o N ie p a s u je d o
(A | B) (C | D) AC AD BC BD Każdego innego łańcucha znaków
A(B|C)*D AD ABD ACD ABCCBD BCD ADD ABCBC
A* | (A*BA*BA*)* AAA BBAABB BABAAA ABA BBB BABBAAA
P r z y k ła d o w e w y ra ż e n ia r e g u la r n e
Te proste reguły umożliwiają zapisanie wyrażeń regularnych, które — choć skom
plikowane — jednoznacznie i kompletnie opisują języki (kilka przykładów znajduje
się w tabeli powyżej). Język często można opisać w inny, prosty sposób, jednak jego
znalezienie bywa trudne. Przykładowo, wyrażenie regularne z ostatniego wiersza tabeli
wyznacza podzbiór (A | B) * z parzystą liczbą wystąpień B.
ROZDZIAŁ 5 o Łańcuchy znaków
w y r a ż e n ia reg u la rn e to obiekty formalne, prostsze nawet
n ie z w y k l e pro ste
od wyrażeń arytmetycznych poznawanych w szkole podstawowej. Ich prostotę wy
korzystujemy do opracowania zwięzłych i wydajnych algorytmów do przetwarzania
takich wyrażeń. Punktem wyjścia jest przedstawiona poniżej formalna definicja.
Definicja. Wyrażenie regularne jest:
■ puste;
■ jednym znakiem;
* wyrażeniem regularnym zapisanym w nawiasach;
■ przynajmniej dwoma złączonymi wyrażeniami regularnymi;
* przynajmniej dwoma wyrażeniami regularnymi rozdzielonymi operatorem
lub{ I);
■ wyrażeniem regularnym, po którym następuje operator domknięcia (*).
Definicja ta opisuje składnię wyrażeń regularnych i określa, z czego składa się p o
prawne wyrażenie regularne. Semantyka określa znaczenie danego wyrażenia regu
larnego i jest istotą nieformalnych opisów przedstawianych w podrozdziale. W ra
mach kontynuacji formalnej definicji podsum ujmy te opisy.
Definicja (ciąg dalszy). Każde wyrażenie regularne reprezentuje zbiór łańcu
chów znaków zdefiniowany w następujący sposób:
■ Puste wyrażenie regularne reprezentuje pusty zbiór łańcuchów znaków,
o 0 elementów.
■ Pusty łańcuch znaków, e, określający jednoelementowy zbiór obejmujący
tylko pusty łańcuch znaków.
■ Znak reprezentuje jednoelementowy zbiór łańcuchów znaków — sam siebie.
■ Wyrażenie regularne w nawiasach reprezentuje ten sam zbiór łańcuchów
znaków, co wyrażenie bez nawiasów.
* Wyrażenie regularne składające się z dwóch złączonych wyrażeń repre
zentuje iloczyn wektorowy zbiorów łańcuchów znaków reprezentowanych
przez poszczególne kom ponenty (zbiór obejmuje wszystkie możliwe łańcu
chy znaków, które m ożna utworzyć przez pobranie jednego łańcucha z każ
dego wyrażenia i złączenie ich zgodnie z kolejnością wyrażeń).
■ Wyrażenie regularne składające się z dwóch wyrażeń połączonych operato
rem lub reprezentuje sumę zbiorów reprezentowanych przez poszczególne
komponenty.
■ Wyrażenie regularne składające się z domknięcia wyrażenia reprezentuje e
(pusty łańcuch znaków) lub sumę zbiorów reprezentowanych przez złącze
nie dowolnej liczby kopii wyrażenia.
Ogólnie język opisywany przez dane wyrażenie regularne może być bardzo duży (po
tencjalnie nieskończony). Istnieje wiele różnych sposobów na opisanie każdego języka.
Należy próbować określać zwięzłe wzorce, podobnie jak próbujemy pisać zwięzłe
programy i implementować wydajne algorytmy.
5.4 Q Wyrażenia regularne 803
S k r ó t y W typowych zastosowaniach występują różne dodatki do podstawowych
reguł, umożliwiające tworzenie zwięzłych opisów dla przydatnych w praktyce języ
ków. W teorii każdy dodatek to tylko skrótowy zapis ciągu operacji obejmujących
wiele operandów. W praktyce dodatki to przydatne rozszerzenia podstawowych ope
racji, umożliwiające tworzenie zwięzłych wzorców.
D eskryp to ry zbiorów zn a k ó w Często
wygodna jest możliwość zastosowania N a zw a Z a p is P rz y k ła d
jednego znaku lub krótkiego ciągu do
Symbol
bezpośredniego opisania zbiorów zna A.B
wieloznaczny
ków. Znak kropki (.) to symbol wielo
znaczny, reprezentujący dowolny poje Określony zbiór U m ieszczony w [] [AEIOU] *
dynczy znak. Ciąg znaków w nawiasach Umieszczony w [], [A-Z]
kwadratowych reprezentuje dowolny Przedział [0-9]
rozdzielony znakiem -
z tych znaków. Ciąg może też reprezen
tować przedział znaków. Jeśli ciąg w na Umieszczony w [],
Dopełnienie [AAEI0U ]ł
wiasach kwadratowych jest poprzedzo poprzedzony znakiem *
ny znakiem C reprezentuje dowolny D e s k ry p to ry z b io ró w z n a k ó w
znak oprócz znaków z ciągu. Te zapisy
to proste sieroty ciągu operacji lub.
S k ró ty d la d o m k n ię c ia O perator domknięcia określa dowolną liczbę kopii operan-
du. W praktyce warto określić liczbę kopii lub zakres tej liczby. Znak plus (+) oznacza
przynajmniej jedną kopię, znak zapytania (?) zero lub jedną kopię, a wartość lub
przedział w nawiasach klamrowych ((}) — określoną liczbę kopii. Także te zapisy to
skróty dla ciągu podstawowych operacji złączania, lub i domknięcia.
Sekw en cje u cieczki Niektóre znaki, talde jak \, ., |, *, ( i ), to metaznaki używane
do tworzenia wyrażeń regularnych. Sekwencje ucieczki rozpoczynają się od znaku
ukośnika, \, który oddziela metaznaki od znaków alfabetu. Sekwencja ucieczki może
obejmować znak \, po którym następuje jeden m etaznak (reprezentujący dany znak).
Przykładowo, sekwencja W reprezentuje \. Inne sekwencje ucieczki służą do repre
zentowania znaków specjalnych i odstępów. Przykładowo, sekwencja \ t reprezentuje
znak tabulacji, \n to znak nowego wiersza, a \s to dowolny biały znak.
Z n a c z e n ie Z a p is P rz y k ła d S k ró t d la W ję z y k u P o z a ję z y k ie m
Przynajmniej 1 (AB)+ (AB)(AB)* AB ABABAB e BBBAAA
0 lub 1 (AB)? el AB e AB Dowolny inny
łańcuch znaków
Konkretna Wartość w {} (AB){3} (AB)(AB)(AB) ABABAB Dowolny inny
wartość łańcuch znaków
Przedział Przedział w {} (AB){l-2} (AB)|(AB)(AB) ABABAB Dowolny inny
łańcuch znaków
S k ró ty d la d o m k n ię c ia (d o o k r e ś la n ia lic z b y k o p ii o p e r a n d u )
804 ROZDZIAŁ 5 o Łańcuchy znaków
Zastosowania wyrażeń regularnych Wyrażenia regularne okazały się za
skakująco wszechstronnym narzędziem do opisywania języków przydatnych w prak
tyce. Dlatego są powszechnie stosowane i gruntownie analizowane. Aby przedstawić
wyrażenia regularne, a jednocześnie pomóc docenić ich przydatność, omawiamy
liczne praktyczne zastosowania przed przyjrzeniem się algorytmowi dopasowywania
wyrażeń regularnych. Wyrażenia te odgrywają też ważną rolę w teoretycznych na
ukach komputerowych. Opisanie tej roli w zakresie, na jaki zasługuje, wykracza poza
zakres książki, jednak w niektórych miejscach pokrótce przedstawiamy podstawowe
osiągnięcia teoretyczne.
W yszukiw anie podłańcuchów Ogólnie celem jest opracowanie algorytmu, który
określa, czy dany łańcuch znaków należy do zbioru łańcuchów znaków opisywanych
przez wyrażenie regularne. Jeśli tekst należy do języka, mówimy, że pasuje do wzor
ca. Dopasowywanie do wzorca za pom ocą wyrażeń regularnych stanowi uogólnienie
problemu wyszukiwania podłańcuchów, opisanego w p o d r o z d z i a l e 5 .3 . Ujmijmy
to precyzyjnie — przy wyszukiwaniu podłańcucha pat w tekście tx t należy spraw
dzić, czy tx t należy do języka opisywanego przez wzorzec . * p a t. *.
Spraw dzanie popraw ności Dopasowywanie wyrażeń regularnych często ma miej
sce przy korzystaniu z sieci WWW. Po wpisaniu daty lub num eru konta w kom er
cyjnej witrynie program do przetwarzania danych wejściowych musi sprawdzić, czy
użytkownik wprowadził odpowiedź we właściwym formacie. Jednym z podejść jest
napisanie kodu sprawdzającego wszystkie przypadki. Jeśli wprowadzana jest kwota
w dolarach, kod może sprawdzać, czy pierwszy symbol to $, czy następuje po nim
zbiór cyfr itd. Lepsze rozwiązanie polega na zdefiniowaniu wyrażenia regularnego,
które opisuje zbiór wszystkich dozwolonych danych wejściowych. Następnie spraw
dzanie, czy dane wejściowe są poprawne, odpowiada problemowi dopasowywania do
wzorca — czy dane wejściowe należą do języka opisywanego przez określone wyraże
nie regularne? Po rozpowszechnieniu tego rodzaju sprawdzania poprawności w sieci
W W W pojawiły się biblioteki wyrażeń regularnych dla często stosowanych danych.
Wyrażenie regularne jest zwykle znacznie dokładniejszym i bardziej zwięzłym zapi
sem zbioru wszystkich poprawnych łańcuchów znaków niż program, który sprawdza
wszystkie przypadki.
K o n te k s t W y ra ż e n ie r e g u la r n e P a s u ją c e ciąg i
Wyszukiwanie podłańcuchów .*NEEDLE.* A HAYSTACK NEEDLE IN
Numer telefonu \ ([0 -9 ]{3 }\)\ [0-9 ]{3 }-[0 -9]{4 } (800) 867-5309
Identyfikator w favie [$ _A -Za-z ][$_A-Z a-z O-9 ]* Pattern Matcher
Marker w genomie gc g(c gg|agg)*ctg gcgaggaggcggcggctg
Adres e-mail [a-z] +@( [ a - z ] + \ .) + (edu[com) rs@ [Link] [Link]
T y p o w e w y ra ż e n ia r e g u la r n e w a p lik a c ja c h (w e rs je u p ro s z c z o n e )
5.4 n Wyrażenia regularne 805
N arzędzia program isty Dopasowywanie do wzorców za pom ocą wyrażeń regu
larnych zapoczątkowano wraz z poleceniem g rep z Uniksa. Polecenie to wyświetla
wszystkie wiersze pasujące do danego wyrażenia. Od pokoleń jest to nieocenione
narzędzie programistów, a wyrażenia regularne wbudowano w wiele współczesnych
systemów programowania — od awk i emacs po Perla, Pythona i JavaScript. Załóżmy
na przykład, że w katalogu znajdują się dziesiątki plików .java. Chcesz ustalić, w któ
rych z nich znajduje się kod korzystający z biblioteki Stdln. Polecenie:
% grep Stdln * .ja v a
pozwala natychmiast uzyskać odpowiedź. Wyświetla wszystkie wiersze z wszystkich
plików pasujące do wyrażenia .*StdIn.*.
B adania nad genom em Biolodzy stosują wyrażenia regularne do rozwiązywania
ważnych problemów naukowych. Przykładowo, genom człowieka obejmuje frag
ment, który można opisać za pom ocą wyrażenia regularnego gcg(cgg)*ctg. Liczba
powtórzeń wzorca cgg jest wysoce zmienna wśród ludzi, a z dużą liczbą powtórzeń
związane są pewne choroby genetyczne, które mogą powodować opóźnienie umysło
we i inne symptomy.
W yszukiw anie Wyszukiwarki obsługują wyrażenia regularne, choć nie zawsze
w pełnej wersji. Zwykle jeśli użytkownik chce określić różne możliwości (za pomocą
znaku | ) lub powtórzenia (przy użyciu znaku *), może to zrobić.
M ożliwości W ramach pierwszego wprowadzenia do teoretycznych nauk kom pute
rowych warto zastanowić się nad zbiorem języków możliwych do opisania za pom o
cą wyrażeń regularnych. Zaskakujące jest na przykład to, że przy użyciu wyrażeń re
gularnych m ożna zaimplementować operację modulo. Wyrażenie (0 | 1 (01 *0)* 1)*
opisuje wszystkie łańcuchy składające się z 0 i 1 , będące binarną reprezentacją wielo
krotności trójki (!). Do języka należą 11, 110, 1001 i 1100, ale już nie 10, 1011 i 10000.
Ograniczenia Nie wszystkie języki m ożna wyrazić za pom ocą wyrażeń regularnych.
Skłaniającym do myślenia przykładem jest to, że żadne wyrażenie regularne nie opi
suje zbioru wszystkich łańcuchów znaków przedstawiających dozwolone wyrażenia
tego rodzaju. Oto prostsze przykłady — nie można wykorzystać wyrażeń regular
nych do sprawdzenia, czy nawiasy są dobrze sparowane lub czy występuje tyle samo
liter A, co B.
TE P R Z Y K ŁA D Y TO TYLKO W IE R Z C H O Ł E K GÓRY LODOWEJ. Wystarczy Wspomnieć, Że
wyrażenia regularne są przydatną częścią infrastruktury informatycznej i odegrały
istotną rolę przy próbie zrozumienia natury przetwarzania. Tak jak m etoda KMP, tak
i opisany dalej algorytm jest produktem ubocznym dążenia do tego zrozumienia.
806 ROZDZIAŁ 5 a Łańcuchy znaków
Niedeterministyczne automaty skończone Przypomnijmy, że algorytm
Knutha-M orrisa-Pratta można traktować jak zbudowany na podstawie wzorca au
tom at skończony do przeszukiwania tekstu. W kontekście dopasowywania wyrażeń
regularnych uogólniamy ten pomysł.
Automat skończony dla metody KMP przechodzi ze stanu w stan, sprawdzając znak
tekstu, a następnie wchodząc w inny, zależny od znaku stan. Automat informuje o do
pasowaniu wtedy i tylko wtedy, kiedy wchodzi w stan akceptacji. Sam algorytm symu
luje działanie automatu. Cechą automatu ułatwiającą opracowanie symulacji jest jego
determinizm. Przejście w każdy stan jest w pełni zależne od następnego znaku tekstu.
Aby zapewnić obsługę wyrażeń regularnych, należy opracować automat abstrakcyj
ny o większych możliwościach. Z uwagi na operację lub automat na podstawie jednego
znaku nie potrafi określić, czy wzorzec może wystąpić w danym miejscu. Z uwagi na
domknięcie nie może nawet stwierdzić, ile znaków trzeba będzie sprawdzić w celu wy
krycia niedopasowania. Aby przezwyciężyć te problemy, należy wbudować w automat
niedeterminizm. Jeśli dopasowanie do wzorca można sprawdzić na więcej niż jeden
sposób, maszyna powinna móc „odgadnąć” ten właściwy! To rozwiązanie wydaje się
niemożliwe do zrealizowania, jednak okazuje się, że można łatwo napisać program do
tworzenia niedeterministycznych automatów skończonych (ang. nondeterministic finite-
state automaton — NFA) i wydajnego symulowania ich działania. Schemat algorytmu
dopasowywania wyrażeń regularnych jest niemal taki sam, jak w metodzie KMP:
° tworzenie automatu NFA odpowiadającego danemu wyrażeniu regularnemu;
° symulowanie działania automatu NFA dla danego tekstu.
Twierdzenie Kleenea, ważne osiągnięcie z dziedziny teoretycznych nauk kom pute
rowych, gwarantuje, że każdemu wyrażeniu regularnemu odpowiada automat NFA
(i na odwrót). Omawiamy dowód konstruktywny tego faktu, pokazując, jak prze
kształcić dowolne wyrażenie regularne w automat NFA. Następnie, w celu zakończe
nia zadania, symulujemy działanie automatu NFA.
Zanim rozważymy, jak budować automaty NFA do dopasowywania do wzorców,
omówmy przykład, w którym pokazujemy cechy takich automatów i podstawowe
reguły ich stosowania. Na rysunku poniżej pokazano automat NFA, który określa,
czy tekst należy do języka opisanego przez wyrażenie regularne ( (A*B | AC) D). Jak po
kazano w przykładzie, automaty NFA mają następujące cechy:
D Automat NFA odpowiadający wyrażeniu regularnemu o długości M przyjmuje
dokładnie jeden stan na znak wzorca, początkowo jest w stanie 0 i ma (wirtualny)
stan akceptacji M.
Stan początkowy
Automat NFA dla wzorca ((A *B | A C )D )
5.4 n Wyrażenia regularne 807
■ Dla stanów odpowiadających znakom alfabetu istnieje krawędź wychodząca,
która prowadzi do stanu odpowiadającego następnemu znakowi wzorca (czar
ne krawędzie na rysunku).
■ Dla stanów odpowiadających metaznakom (,), | i * istnieje przynajmniej jedna
krawędź wychodząca (czerwone krawędzie na rysunku), która może prowadzić
do innego stanu.
■ Dla niektórych stanów istnieje wiele krawędzi wychodzących, jednak żaden
stan nie ma więcej niż jednej wychodzącej czarnej krawędzi.
Zgodnie z konwencją wszystkie wzorce umieszczamy w nawiasach, dlatego pierwszy
stan odpowiada lewemu nawiasowi, a ostatni — prawemu nawiasowi (i ma przejście
do stanu akceptacji).
Automaty NFA, tak jak automaty DFA z poprzedniego podrozdziału, urucham ia
my w stanie 0 i odczytujemy pierwszy znak tekstu. Automat NFA przechodzi ze stanu
w stan, czasem odczytując po jednym znaku tekstu od lewej do prawej. Występują
jednak pewne podstawowe różnice w porównaniu z automatem DFA:
■ Znaki występują na rysunkach w węzłach, a nie przy krawędziach.
■ Automat NFA rozpoznaje tekst dopiero po bezpośrednim odczytaniu wszyst
kich znaków, natomiast automat DFA rozpoznaje wzorzec w tekście bez ko
nieczności odczytania wszystkich znaków tekstu.
Różnice te nie muszą występować. Wybraliśmy taką wersję każdego automatu, która
najlepiej pasuje do badanych algorytmów.
Dalej koncentrujemy się na sprawdzeniu, czy tekst pasuje do wzorca. Do tego
potrzebny jest automat, który dochodzi do stanu akceptacji i przetwarza cały tekst.
Reguły przechodzenia z jednego stanu w drugi także są inne niż w automatach DFA.
W automacie NFA przebiega to tak:
■ Jeśli bieżący stan odpowiada znakowi alfabetu oraz bieżący znak tekstu pasuje
do danego znaku, automat może przejść przez znak tekstu i wybrać (czarne)
przejście do następnego stanu; takie przejście nazywamy przejściem po dopa
sowaniu.
° Automat może wybrać dowolną czerwoną krawędź do innego stanu bez spraw
dzania znaku tekstu; jest to e-przejście (inaczej przejście puste), odpowiadające
„dopasowaniu” pustego łańcucha znaków e.
A A A A B D
o— 1— 2 /- 3 — 2— 3— 2— 3-^2— 3^-4— 5-<-8— 9 —10—11
Przejście p o dop asow aniu - e-przejście - Osiągnięto stan akceptacji
przechodzenie do następnego zm iana stanu bez i spraw dzono wszystkie znaki -
znaku wejściowego i zm iana stanu do p asow yw an ia autom at NFA rozpoznai tekst
W y s z u k iw a n ie w z o rc a z a p o m o c ą a u to m a t u N F A d la w y ra ż e n ia ( ( A * B | A C ) D )
808 ROZDZIAŁ 5 b Łańcuchy znaków
Przykładowo załóżmy, że automat NFA
Brak
możliwości dla wyrażenia ( ( A * B | A C )
wyjścia D ) został uruchomiony (w stanie 0)
Błędna próba, jeśli ze stanu 4
dane wejściowe to dla danych wejściowych A A A A B D.
A A A A B D Na rysunku w dolnej części poprzed
A niej strony przedstawiono ciąg przejść
Brak
możliwości między stanami kończący się stanem
wyjścia akceptacji. Pokazano w ten sposób, że
ze stanu 7
tekst należy do zbioru łańcuchów zna
A A A A C Brak ków opisywanych przez dane wyraże
m ożliwości nie regularne — tekst pasuje do wzorca.
wyjścia
ze stanu 4 W kontekście automatu NFA mówimy,
Ciągi b e z d a lsz y c h p rz e jść d la a u to m a tu NFA
że automat rozpoznaje wzorzec.
d la w y ra ż e n ia ( { A * B [ A C ) D ) W przykładzie po lewej stronie po
kazano, że m ożna znaleźć ciąg przejść
powodujący zatrzymanie automatu NFA. Dotyczy to nawet tekstu w rodzaju A A A
A B D, który maszyna powinna rozpoznać. Przykładowo, jeśli automat NFA przej
dzie do stanu 4 przed sprawdzeniem wszystkich A, nie będzie miał gdzie przejść
dalej, ponieważ jedynym sposobem wyjścia ze stanu 4 jest dopasowanie B. W tych
dwóch przykładach pokazano niedeterministyczną naturę omawianego automatu.
Po sprawdzeniu A i przejściu w stan 3 automat NFA ma dwie możliwości — przejście
do stanu 4 lub powrót do stanu 2. Od tego wyboru zależy, czy automat dojdzie do sta
nu akceptacji (tak jak w pierwszym przykładzie), czy się zatrzyma (tak jak w drugim
przykładzie). Automat dokonuje też wyboru w stanie 1 (czy ma wybrać e-przejście
do stanu 2 czy do stanu 6 ).
W tych przykładach pokazano kluczową różnicę między automatami NFA i DFA.
Ponieważ w automacie NFA z danego stanu może wychodzić wiele krawędzi, przej
ście jest tu niedeterministyczne. Automat może wykonywać jedno przejście w jed
nym momencie i inne przejście w innym momencie, bez sprawdzania żadnego zna
ku tekstu. Aby zrozumieć działanie takiego automatu, wyobraźmy sobie, że automat
NFA potrafi zgadnąć, które przejście (jeśli w ogóle) prowadzi do stanu akceptacji dla
danego tekstu. Ujmijmy to inaczej — automat NFA rozpoznaje tekst wtedy i tylko
wtedy, jeśli jeden z ciągów przejść sprawdza wszystkie znaki tekstu oraz dochodzi do
stanu akceptacji po rozpoczęciu pracy od początku tekstu i stanu 0. Automat NFA nie
rozpoznaje tekstu wtedy i tylko wtedy, jeśli nie istnieje ciąg przejść po dopasowaniu
i e-przejść, który sprawdza wszystkie znaki tekstu i prowadzi do stanu akceptacji.
Ślad działania automatu NFA (tak jak automatu DFA) dla tekstu to ciąg zmian
stanów kończący się stanem końcowym. Każdy taki ciąg to dowód, że automat roz
poznaje tekst (mogą istnieć też inne dowody). Jak jednak znaleźć taki ciąg dla danego
tekstu? Jak udowodnić, że dla innego tekstu taki ciąg nie istnieje? Udzielenie odpo
wiedzi na takie pytania jest prostsze, niż może się wydawać — należy systematycznie
sprawdzić wszystkie możliwości.
5.4 e Wyrażenia regularne 809
Symulowanie działania automatu NFA Myśl, że automat potrafi odgadnąć
przejścia potrzebne do dotarcia do stanu akceptacji, przypomina pomysł napisania
programu potrafiącego odgadnąć właściwe rozwiązanie problemu — wydaje się nie
dorzeczna. Po zastanowieniu okazuje się, że zadanie nie jest takie trudne. Należy
sprawdzić wszystkie możliwe ciągi przejść. Jeśli jeden z nich prowadzi do stanu ak
ceptacji, zostanie znaleziony.
Reprezentacja Zacznijmy od tego, że potrzebna jest reprezentacja automatu NFA.
Wybór jest prosty. Samo wyrażenie regularne wyznacza nazwy stanów (liczby cał
kowite z przedziału od 0 do M, gdzie Mto liczba znaków w wyrażeniu regularnym).
Wyrażenie regularne jest przechowywane w tablicy re [] wartości typu char definiu
jących przejścia po dopasowaniu (jeśli re [i ] występuje w alfabecie, istnieje przejście
po dopasowaniu z i do i+1). Naturalną reprezentacją e-przejść jest digraf. Istnieją
krawędzie skierowane (czerwone krawędzie na rysunkach) łączące wierzchołki od 0
do M(po jednym na każdy stan). Można więc przedstawić wszystkie e-przejścia jako
digraf G. Proces tworzenia digrafu powiązanego z danym wyrażeniem regularnym
omawiamy po przedstawieniu procesu symulacji. Digraf dla przykładowych danych
obejmuje dziewięć krawędzi:
0 1 1 —» 2 1 —» 6 2 —» 3 3 —> 2 3 4 5 8 8 ^ 9 10 - > 1 1
Sym ulow anie działania autom atu NFA i osiągalność Aby zasymulować działanie
automatu NFA, należy śledzić zbiór stanów, które można napotkać w czasie sprawdza
nia przez automat bieżącego znaku wejściowego. Kluczowy jest tu znany proces okre
ślania osiągalności z wielu źródeł, omówiony w a l g o r y t m i e 4.4 (strona 583). Aby za
inicjować zbiór, należy znaleźć zbiór stanów osiągalnych przez e-przejścia ze stanu 0 .
Dla każdego takiego stanu należy sprawdzić, czy możliwe jest przejście po dopasowaniu
dla pierwszego znaku wejściowego. W ten sposób uzyskujemy zbiór możliwych stanów
automatu NFA po dopasowaniu pierwszego znaku wejściowego. Do tego zbioru należy
dodać wszystkie stany, które mogą wystąpić po e-przejściach z jednego ze stanów zbio
ru. Dla zbioru możliwych stanów automatu NFA bezpośrednio po dopasowaniu pierw
szego znaku wejściowego rozwiązanie problemu osiągalności z wielu źródeł w digrafie
e-przejść wyznacza zbiór stanów, które mogą prowadzić do przejść po dopasowaniu
dla drugiego znaku wejściowego. Początkowy zbiór stanów w przykładowym automacie
NFA to 0 1 2 3 4 6. Jeśli pierwszy znak to A, automat NFA może wybrać przejście po
dopasowaniu do stanu 3 lub 7. Następnie może wybrać e-przejścia z 3 do 2 lub z 3 do
4, tak więc zbiór stanów, które mogą prowadzić do przejścia po dopasowaniu dla dru
giego znaku, to 2 3 4 7. Powtarzanie tego procesu do czasu wyczerpania wszystkich
znaków tekstu prowadzi do jednego z dwóch skutków.
■ Zbiór możliwych stanów obejmuje stan akceptacji.
■ Zbiór możliwych stanów nie obejmuje stanu akceptacji.
Pierwszy ze skutków oznacza, że istnieje ciąg przejść umożliwiający automatowi NFA
dotarcie do stanu akceptacji. Należy więc poinformować o powodzeniu. Drugi sku
tek oznacza, że automat NFA zawsze zatrzymuje się dla danych wejściowych, dla-
810 ROZDZIAŁ 5 a Łańcuchy znaków
0 12 3 4 6 : Z b ió r s t a n ó w o s ią g a ln y c h p rze z E-przejścia o d p o c z ą t k u
3 7
2 3 4 7 Z b ió r s ta n ó w o s ią g a ln y c h przez E-przejścia p o d o p a s o w a n iu A
2 3 4 Z b ió r s t a n ó w o s ią g a ln y c h p rzez E-przejścia p o d o p a s o w a n iu A A
5 8 9 : Z b ió r s t a n ó w o sią g a ln y c h przez E-przejścia p o
o
10 : Z b ió r s t a n ó w o s ią g a ln y c h p o d o p a s o w a n iu A A B D
10 1 1 : Z b ió r s t a n ó w o sią g a ln y c h przez E-przejścia p o d o p a s o w a n iu A A B D
A k cep ta cja
S y m u lo w a n ie p ra c y a u to m a tu NFA d la w y ra ż e n ia
( ( A * B | A C ) D ) i d a n y c h w e jśc io w y c h A A B D
5.4 a Wyrażenia regularne 811
tego trzeba poinformować o niepowodzeniu. Za pom ocą typu danych SET i klasy
Di rectedDFS, opisanej w kontekście rozwiązywania problemu osiągalności z wielu
źródeł w digrafie, m ożna napisać kod symulujący działanie automatu NFA (widocz
ny poniżej) przez przekształcenie przedstawionego opisu w języku polskim. Poziom
zrozumienia kodu można sprawdzić, analizując ślad na poprzedniej stronie, gdzie
pokazano pełną symulację dla omawianego przykładu.
Twierdzenie Q. Ustalenie, czy N-znakowy łańcuch jest rozpoznawany przez
automat NFA odpowiadający M-znakowemu wyrażeniu regularnemu, zajmuje
— dla najgorszego przypadku — czas proporcjonalny do NM.
Dowód. Dla każdego z N znaków tekstu należy przejść po zbiorze stanów (jego
wielkość jest nie większa niż M) i uruchomić algorytm DFS na digrafie e-przejść.
Zgodnie z omówionym dalej schematem liczba krawędzi w digrafie jest nie więk
sza niż 2M, dlatego dla najgorszego przypadku czas każdego wykonania algoryt
m u DFS jest proporcjonalny do M.
Warto przez m om ent zastanowić się nad tym zaskakującym wynikiem. Koszt dla naj
gorszego przypadku, iloczyn długości tekstu i wzorca, jest taki sam, jak koszt dla naj
gorszego przypadku przy wyszukiwaniu podłańcuchów za pom ocą podstawowego
algorytmu, od którego zaczęliśmy p o d r o z d z i a ł 5 .3 .
p ublic boolean re c o g n i z e s ( S t r i n g tx t)
{ // Czy automat NFA rozpoznaje łańcuch t x t ?
Bag<Integer> pc = new B a g < In te g e r > ( );
DirectedDFS dfs = new DirectedDFS(G, 0);
f o r (i n t v = 0; v < G.V(); v++)
i f ([Link](v)) [Link](v);
f o r ( i n t i = 0; i < t x t . l e n g t h ( ) ; i++ )
{ // Wyznaczanie stanów automatu NFA dla t x t [ i +1].
Bag<Integer> match = new B a g < In te g e r > ( );
f o r ( i n t v : pc)
if (v < M)
i f (r e [v] == t x t . c h a r A t ( i ) || re [v] == ' . ' )
m a tch.a d d (v +l);
pc = new B a g < In te g e r > ( );
d fs '= new DirectedDFS(G, match);
f o r ( i n t v = 0; v < G.V (); v++)
i f ([Link](v)) [Link](v);
1
f o r ( i n t v : pc) i f (v == M) return true;
return f a l s e ;
1
Symulacja działania automatu NFA przy dopasowywaniu wzorca
812 ROZDZIAŁ 5 □ Łańcuchy znaków
Tworzenie automatu NFA odpowiadającego wyrażeniu regular
nemu Z uwagi na podobieństwo między wyrażeniami regularnymi i wyrażeniami
arytmetycznymi możliwe, że nie jest zaskoczeniem, iż przekształcanie wyrażeń regu
larnych na automat NFA przypomina proces obliczania wyrażeń arytmetycznych za
pomocą opartego na dwóch stosach algorytmu Dijkstry, opisanego w p o d r o z d z i a l e
1 .3 . Przekształcanie wyrażeń regularnych przebiega nieco odmiennie, ponieważ:
■ Dla wyrażeń regularnych nie istnieje bezpośredni operator złączania.
■ Dla wyrażeń regularnych istnieje operator jednoargum entowy (dla domknięcia
— *)•
* Dla wyrażeń regularnych istnieje tylko jeden operator binarny (dla operacji lub
-I)-
Zamiast analizować różnice i podobieństwa, omawiamy implementację dostosowaną
do wyrażeń regularnych. Przykładowo, potrzebny jest tylko jeden stos, a nie dwa.
Zgodnie z omówieniem reprezentacji z początku poprzedniego punktu trzeba
zbudować tylko digraf G składający się z wszystkich e-przejść. Samo wyrażenie regu
larne i formalne definicje omówione na początku podrozdziału zapewniają potrzeb
ne informacje. W zorując się na algorytmie Dijkstry, korzystamy ze stosu do śledzenia
pozycji lewych nawiasów i operatorów lub.
Złączanie W automacie NFA operacja złączania jest najprostsza do zaimplemento
wania. Przejścia po dopasowaniu dla stanów odpowiadających znakom alfabetu to
bezpośrednia implementacja złączania.
N aw iasy Indeks lewego nawiasu z wyrażenia regularnego należy umieścić na sto
sie. Przy każdym napotkaniu prawego nawiasu odpowiadający m u lewy nawias jest
zdejmowany ze stosu za pomocą opisanej dalej techniki. Stos, tak jak w algorytmie
Dijkstry, umożliwia obsługę zagnieżdżonych nawiasów w naturalny sposób.
D om knięcie Operator domknięcia (*) musi występować albo (i) po pojedynczym
znaku, kiedy to należy dodać e-przejścia do znaku i z niego, albo (ii) po prawym
nawiasie, kiedy to trzeba dodać e-przejścia do odpowiedniego lewego nawiasu (ze
szczytu stosu) i z niego.
Wyrażenie z lub Wyrażenie regularne w postaci (A | B), gdzie A i B są wyrażenia
mi regularnymi, przetwarzane jest przez dodanie dwóch e-przejść. Jedno prowadzi ze
stanu odpowiadającego lewemu nawiasowi do stanu odpowiadającego pierwszemu
znakowi B, a drugie — ze stanu odpowiadającego operatorowi | do stanu dla prawego
nawiasu. Na stosie należy umieścić indeks wyrażenia regularnego odpowiadający ope
ratorowi | (a także, o czym wspomniano wcześniej, indeks dla lewego nawiasu), tak
aby potrzebne informacje znajdowały się na szczycie stosu w momencie, kiedy będą
potrzebne (po dojściu do prawego nawiasu). Opisane e-przejścia umożliwiają automa
towi NFA wybór jednej z dwóch możliwości. Nie należy dodawać e-przejścia ze stanu
odpowiadającego operatorowi | do stanu o następnym większym indeksie, jak robimy
dla wszystkich pozostałych stanów. Jedynym sposobem wyjścia automatu NFA z takie
go stanu jest wybranie przejścia do stanu odpowiadającego prawemu nawiasowi.
5.4 □ Wyrażenia regularne 813
te p r o s t e r e g u ł y w y s t a r c z ą d o zbudowania automatów NFA odpowiadających
dowolnie skomplikowanym wyrażeniom regularnym, a l g o r y t m 5.9 to im plemen
tacja, w której konstruktor tworzy digraf e-przejść odpowiadający danem u wyra
żeniu regularnemu. Na dalszej stronie znajduje się ślad procesu tworzenia digrafu
dla przykładowych danych. Inne przykłady m ożna znaleźć w dolnej części tej strony
i w ćwiczeniach. Zachęcamy do utrwalenia zrozumienia procesu na podstawie włas
nych przykładów. Z uwagi na zwięzłość i przejrzystość kilka szczegółów (obsługę
metaznaków, deskryptory zbiorów znaków, skróty dla domknięć i wielościeżkowe
operacje lub) omawiamy w ćwiczeniach (zobacz ć w i c z e n i a od 5 .4.16 do 5 .4 .2 1 ).
Tworzenie digrafu wymaga zaskakująco mało kodu, a służący do tego algorytm jest
jednym z najbardziej pomysłowych, jakie kiedykolwiek widzieliśmy.
D om knięcie p o je d y n c ze g o znaku
G .a d d E d g e ( i, i + 1 ) ;
G .a d d E d g e ( i+ l, i ) ;
W yrażenie d om knięcia
G .a d d E d g e O p , i + 1 ) ;
G .a d d E d g e ( i+ l, I p ) ;
W yrażenie lub
G .a d d E d g e ( l p , o r+ 1 );
G .a d d E d g e f o r , i);
R eg u ły tw o rz e n ia a u to m a tu NFA
0 -H T h -
A u to m a t NFA o d p o w ia d a ją c y w z o rc o w i ( . * A B ( ( C | D * E ) F ) * G )
814 ROZDZIAŁ 5 Łańcuchy znaków
ALGORYTM 5.9. Dopasowywanie do wzorca za pomocą wyrażeń regularnych (narzędzie grep)
public class NFA
{
p riv ate char[] re; / / P r z e j ś c i a po d o p a s o w a n i u ,
p r i v a t e D i g r a p h G; / / P r z e j ś c i a e.
p r i v a t e i n t M; / / Liczba stanów.
p u b l i c NFA(String regexp)
{ // T w o r z e n i e m a s z y n y NFA d l a d a n e g o w y r a ż e n i a r e g u l a r n e g o .
S t a c k < I n t e g e r > o p s = new S t a c k < I n t e g e r > ( ) ;
re = re g ex p .toCharArrayO ;
M = [Link];
G = new Di g r a p h ( M + l ) ;
for (int i = 0; i < M; i ++ )
(
int Ip = i ;
if ( r e [ i ] == ' ( ' II re [i] == ' | ' )
[Link](i);
el se i f ( r e [ i ] == ' ) ' )
(
i n t o r = ops . p o p ( ) ;
if (re[or] == 1 | 1)
{
lp = o p s . p o p O ;
[Link](lp, or+1);
[Link](or, i);
}
e ls e lp = or;
}
if (i < M- l && r e [ i + l ] == ' * ' ) // Przechodzenie d a le j.
(
[Link](lp, i+1);
[Link](i+l, l p);
}
if ( r e [i ] == ' ( ' || re [i] == || re[i] == ' ) ' )
[Link](i, i+1);
}
}
p ublic boolean re c o g n iz e s (S trin g t x t)
// Czy a u t o m a t NFA r o z p o z n a j e t e k s t t x t ? (Zobacz s t r o n ę 8 1 1 ) .
}
Konstruktor buduje tu automat NFA odpowiadający danemu wyrażeniu regularnemu, two
rząc digraf e-przejść.
W yrażenia regularne
816 ROZDZIAŁ 5 □ Łańcuchy znaków
Twierdzenie R. Tworzenie automatu NFA odpowiadającego M-znakowemu
wyrażeniu regularnemu wymaga czasu i pamięci w ilości proporcjonalnej do M
(dla najgorszego przypadku).
Dowód. Dla każdego z M znaków wyrażenia regularnego dodawane są najwy
żej trzy e-przejścia i czasem wykonywane są jedna lub dwie operacje na stosie.
Klasyczny klient GREP do dopasowywania do wzorców, przedstawiony w kodzie po
lewej stronie, przyjmuje wyrażenie regularne jako argument i wyświetla te wiersze
ze standardowego wejścia, które obejm u
p ub lic c l a s s GREP ją podłańcuch należący do języka opisy
i wanego przez dane wyrażenie regularne.
pub lic s t a t i c void m a in ( S tr in g [] args)
Klient ten pojawił się w pierwszych im
{
S t r i n g regexp = + arg s[0 ] + plementacjach Unilcsa i był nieodłącznym
NFA nfa = new NFA(regexp); narzędziem wielu pokoleń programistów.
while ( S td ln .h a s N e x t L in e O )
{
S t r i n g t x t = St d ln .h a s N e x t L i n e O ;
i f (n f a . r e c o g n i z e s ( t x t ) )
StdO u [Link] (txt);
}
}
1
K lasy czn y u o g ó ln io n y k lie n t a u t o m a t u NFA, s łu ż ą c y d o
d o p a s o w y w a n ia d o w z o rc a z a p o m o c ą w y ra ż e ń r e g u la rn y c h
% more t i n y L . t x t
AC
AD
AAA
ABD
ADD
BCD
ABCCBD
BABAAA
BABBAAA
% java GREP 11(A*B|AC)D" < t i n y L . t x t
ABD
ABCCBD
% java GREP Stdln < [Link]
while ( S td ln .h a s N e x t L in e O )
S t r i n g t x t = St d ln .h a s N e x t L i n e O ;
5.4 a Wyrażenia regularne 817
PYTANIA I O D PO W IED ZI
P. Jaka jest różnica między nuli a 6?
O. Pierwsza wartość oznacza pusty zbiór. Druga określa pusty łańcuch znaków. Może
istnieć zbiór, który obejmuje jeden element, e, a tym samym nie jest pusty.
818 ROZDZIAŁ 5 a Łańcuchy znaków
| Ć W IC Z E N IA
Podaj wyrażenie regularne, które opisuje wszystkie łańcuchy znaków obej
5 . 4 .1 .
mujące:
■ dokładnie cztery kolejne litery A;
■ nie więcej niż cztery kolejne litery A;
■ przynajmniej jedno wystąpienie czterech kolejnych liter A.
5 .4 .2 . Podaj krótki opis w języku polskim każdego z poniższych wyrażeń regular
nych.
a. .*
b. A. *A | A
c. . *ABBABBA.*
d. . *A.*A.*A.*A.*
Jaka jest maksymalna liczba różnych łańcuchów znaków, które można opisać
5 . 4 .3 .
za pomocą wyrażeń regularnych o M operatorach lub, ale bez operatorów dom knię
cia (dozwolone są nawiasy i złączenia)?
5 . 4 .4 . Narysuj automat NFA odpowiadający wzorcowi ( ( (A | B) * | CD* | EFG) *) *.
5 .4 .5 . Narysuj digraf e-przejść dla automatu NFA z ć w ic z e n ia 5 .4 .4 .
Podaj zbiory stanów osiągalnych dla automatu NFA z ć w i c z e n i a 5 .4.4
5 . 4 .6 .
po dopasowaniu każdego znaku i późniejsze e-przejścia dla danych wejściowych
ABBACEFGEFGCAAB.
5 . 4 .7 . Przekształć klienta GREP ze strony 816 na klienta GREPmatch, który umieszcza
wzorzec w cudzysłowach, ale nie dodaje sekwencji .* przed wzorcem i po nim, dla
tego wyświetla tylko wiersze będące łańcuchami znaków z języka opisywanego przez
dane wyrażenie regularne. Podaj skutki wywołania każdego z poniższych poleceń:
a. % j a v a GREPmatch " ( A | B ) ( C | D ) " < t i n y L . t x t
b. % j a v a GREPmatch " A ( B| C) * D" < t i n y L . t x t
c. % j a v a GREPmatch "( A*B| AC) D" < t i n y L . t x t
5 . 4 .8 . Napisz wyrażenie regularne dla każdego z poniższych zbiorów łańcuchów bi
narnych:
a. Zawierającego przynajmniej trzy kolejne cyfry 1.
b. Zawierającego podłańcuch 110.
c. Zawierającego podłańcuch 1101100.
d. Niezawierającego podłańcucha 110.
5.4 a Wyrażenia regularne 819
5.4.9. Napisz wyrażenie regularne opisujące łańcuchy binarne obejmujące przynaj
mniej dwie cyfry 0, które jednak nie mogą występować obok siebie.
5.4.10. Napisz wyrażenie regularne dla każdego z poniższych zbiorów łańcuchów
binarnych.
a. Obejmującego przynajmniej trzy znaki, przy czym trzecim znakiem musi
być 0.
b. Obejmującego liczbę cyfr 0 podzielną przez 3.
c. Rozpoczynającego i kończącego się tym samym znakiem.
d. O nieparzystej długości.
e. Rozpoczynającego się cyfrą 0 i o nieparzystej długości lub rozpoczynającego
się cyfrą 1 i o parzystej długości.
f. O długości przynajmniej 1 i najwyżej 3.
5 .4.11. Dla każdego z poniższych wyrażeń regularnych określ, ile istnieje pasują
cych do nich łańcuchów bitów o długości równej 1000 .
a. 0(0 | 1 ) * 1
b. 0* 1 0 1 *
c. (1 | 0 1 )*
5 .4.12 Napisz wyrażenia regularne Javy dla poniższych zbiorów łańcuchów.
a. Numerów telefonów w postaci (609) 555-1234.
b. Numerów dowodów osobistych, na przykład AAA123321.
c. Dat, takich jak 31 grudnia 1999.
d. Adresów IP w postaci a . b . c . d, gdzie każda litera może reprezentować jedną,
dwie lub trzy cyfry, na przykład [Link].
e. Numerów tablic rejestracyjnych, rozpoczynających się od czterech cyfr, po
których następują dwie duże litery.
820 ROZDZIAŁ 5 n Łańcuchy znaków
i PRO BLEM Y DO R O ZW IĄ ZA N IA
5.4.13. Trudne wyrażenia regularne. Utwórz wyrażenia regularne opisujące każdy
z poniższych zbiorów łańcuchów opartych na alfabecie binarnym.
a. Wszystkie łańcuchy oprócz 1 1 i 1 1 1 .
b. Łańcuchy z cyfrą 1 na każdej nieparzystej pozycji.
c. Łańcuchy obejmujące przynajmniej dwie cyfry 0 i przynajmniej jedną cyfrę 1.
d. Łańcuchy bez dwóch kolejnych cyfr 1 .
5.4.14. Podzielność wartości binarnych. Utwórz wyrażenia regularne opisujące
wszystkie łańcuchy binarne, które po zinterpretowaniu jako liczby binarne będą:
a. podzielne przez 2 ;
b. podzielne przez 3;
c. podzielne przez 123.
5.4.15. Jednopoziomowe wyrażenia regularne. Utwórz wyrażenie regularne Javy
opisujące zbiór łańcuchów znaków, które są poprawnymi wyrażeniami regularnymi
dla alfabetu binarnego, przy czym nie obejmują nawiasów zagnieżdżonych w innych
nawiasach. Przykładowo, wyrażenie (0. * 1) * or (1. *0) * należy do tego języka, ale
wyrażenie ( 1(0 or 1 ) 1 )* do niego nie należy.
5.4.16. Wielościeżkowe operacje lub. Dodaj wielościeżkowe operacje lub do
klasy NFA. Kod powinien tworzyć automat narysowany poniżej dla wzorca
( .*AB( (C | D| E) F)*G).
A u to m a t NFA o d p o w ia d a ją c y w z o rc o w i ( . 4 A B ( ( C | D | E ) F ) 1 G )
5.4 Q Wyrażenia regularne 821
5.4.17. Symbole wieloznaczne. Dodaj do klasy NFA obsługę symboli wieloznacznych.
5.4.18. Jeden lub więcej. Dodaj do klasy NFA obsługę operatora domknięcia +.
5.4.19. Określony zbiór. Dodaj do klasy NFA obsługę deskryptorów określonego
zbioru.
5.4.20. Przedział. Dodaj do klasy NFA obsługę deskryptorów przedziałów.
5.4.21. Dopełnienie. Dodaj do klasy NFA obsługę deskryptorów dopełnienia.
5.4.22 Dowód. Opracuj wersję klasy NFA, która wyświetla dowód na to, że dany łań
cuch znaków należy do języka rozpoznawanego przez automat NFA (dowodem jest
ciąg przejść między stanami prowadzący do stanu akceptacji).
5.5. KOMPRESJA DANYCH
W świecie dostępnych jest mnóstwo danych, a algorytmy zaprojektowane do ich
wydajnego reprezentowania odgrywają ważną rolę we współczesnej infrastruktu
rze informatycznej. Są dwa podstawowe powody kompresowania danych — w celu
zaoszczędzenia pamięci przy zapisywaniu informacji i w celu zaoszczędzenia czasu
przy ich przesyłaniu. Oba powody pozostają istotne od wielu generacji technologii
kompresji danych i są zrozumiałe dla każdego, kto potrzebuje nowego dysku lub
oczekuje na pobranie dużego pliku.
Z pewnością zetknąłeś się z kompresją w kontekście obrazów cyfrowych, dźwięku,
filmów i danych wielu innych rodzajów. Omawiane tu algorytmy pozwalają zaoszczę
dzić pamięć z uwagi na to, że w większości plików znajduje się wiele nadmiarowych
danych. Przykładowo, pliki tekstowe obejmują pewne sekwencje znaków występu
jące znacznie częściej od innych. W plikach z bitmapami z zakodowanym obrazem
znajdują się duże jednorodne obszary. Pliki z cyfrową reprezentacją obrazów, filmów,
dźwięków i innych sygnałów analogowych obejmują długie powtarzające się wzorce.
Omawiamy tu podstawowy algorytm oraz dwie zaawansowane i powszechnie
stosowane metody. Kompresja uzyskiwana przy ich użyciu zależy od cech danych
wejściowych. Dla tekstu typowe są oszczędności rzędu 20 - 50%, a w niektórych
sytuacjach może to być od 50 do 90%. Jak widać, skuteczność m etod kompresji da
nych jest zależna od danych wejściowych. Uwaga: w książce określenie „wydajność”
zwykle związane jest z czasem. W kontekście kompresji danych zwykle dotyczy ono
stopnia kompresji, jaki m ożna uzyskać, choć zwracamy uwagę także na czas potrzeb
ny do wykonania zadania.
Z jednej strony, techniki kompresji danych są obecnie mniej istotne, ponieważ
koszt pamięci komputei'owej znacznie spadł i typowy użytkownik ma do dyspozycji
znacznie większą jej ilość. Z drugiej strony, techniki te zyskały na znaczeniu, ponie
waż z uwagi na tak dużą ilość używanej pamięci możliwe są większe oszczędności.
Wraz z pojawieniem się internetu zaczęto powszechnie stosować kompresję danych,
ponieważ jest to tani sposób na skrócenie czasu transmisji dużych ilości danych.
Kompresja danych ma bogatą historię (tu przedstawiamy tylko krótkie wprowa
dzenie do tego tematu). Z pewnością warto zastanowić się nad rolą tego zagadnienia
w przyszłości. Każda osoba poznająca algorytmy odniesie korzyści z analizy kom
presji danych, ponieważ algorytmy z tego obszaru są klasyczne, eleganckie, ciekawe
i skuteczne.
5.5 a Kompresja danych
Reguły działania Wszystkie typy danych przetwarzane za pom ocą współczes
nych systemów komputerowych mają pewną wspólną cechę — ostatecznie są repre
zentowane w postaci binarnej. Wszelkie dane m ożna traktować jak ciągi bitów (lub
bajtów). W podrozdziale stosujemy nazwę strumień bitów do opisu ciągów bitów,
a nazwę strumień bajtów — do określania bitów rozpatrywanych jako ciągi bajtów
o stałej wielkości. Strumień bitów lub bajtów można zapisać jako plik na komputerze
lub przesłać jako wiadomość w internecie.
Podstaw owy m odel Na podstawie tego opisu podstawowy model kompresji danych
jest dość prosty. Obejmuje dwa podstawowe komponenty. Każdy z nich jest czarną
skrzynką, która wczytuje i zapisuje strumienie bitów.
■ Skrzynka kompresująca przekształca strum ień bitów B na skompresowaną wer
sję C(B).
■ Skrzynka rozpakowująca przekształca C(B) z powrotem na B.
Zapis |B| oznacza liczbę bitów w strumieniu. Celem jest zminimalizowanie wartości
|C(fJ)|/|.B|, czyli współczynnika kompresji.
K om presow anie R ozpakow yw anie
Strumień bitów B Wersjo skompresowano - C(B)
I 0110110X 01... > | 110 10 11111 .. | -» 0110110101...
_. _—..... .
P o d s ta w o w y m o d e l k o m p re s ji d a n y c h
Model ten dotyczy tak zwanej kompresji bezstratnej. Ważne jest tu, aby nie nastąpiła
utrata informacji (w tym sensie, że efekt kompresji i rozpakowania strum ienia bitów
musi co do bitu odpowiadać oryginałowi). Kompresja bezstratna jest wymagana dla
wielu typów plików, na przykład dla danych numerycznych lub kodu wykonywalne
go. Dla pewnych typów plików (takich jak obrazy, filmy lub piosenki) dopuszczalne
są m etody kompresji, w których następuje utrata pewnych informacji. Dekoder ge
neruje tu tylko przybliżoną wersję pierwotnego pliku. W metodach stratnych ocenia
się — obok współczynnika kompresji — także subiektywną jakość. W tej książce nie
omawiamy kompresji stratnej.
Odczyt i zapis danych binarnych Kompletny opis kodowania informacji na
komputerze jest zależny od systemu i wykracza poza zakres książki. Jednak za p o
mocą kilku podstawowych założeń i dwóch prostych interfejsów API można oddzie
lić implementacje od specyfiki systemu. W spom niane interfejsy API, BinaryStdln
i BinaryStdOut, są oparte na używanych wcześniej interfejsach API Stdln i StdOut,
jednak służą do odczytu oraz zapisu bitów, podczas gdy Stdln i StdOut są przezna
czone dla strumieni znaków Unicode. Wartość typu in t w StdOut to ciąg znaków
(reprezentacja dziesiętna). Wartość tego typu w BinaryStdOut to ciąg bitów (repre
zentacja binarna).
824 ROZDZIAŁ 5 b Łańcuchy znaków
Binarne wejście i wyjście W większości współczesnych systemów, w tym w Javie,
operacje wejścia-wyjścia oparte są na strum ieniach 8-bitowych bajtów, dlatego m oż
na wczytywać i zapisywać strumienie bajtów w taki sposób, aby dopasować formaty
wejścia-wyjścia do wewnętrznych reprezentacji typów prostych — m ożna zakodo
wać 8 -bitowy typ char za pomocą jednego bajta, 16-bitowy typ short za pomocą
dwóch bajtów, 32-bitowy typ i nt za pomocą czterech bajtów itd. Ponieważ przy kom
presji danych podstawową abstrakcją są strumienie bitów, m ożna pójść o krok dalej
i umożliwić klientom odczyt oraz zapis poszczególnych bitów wymieszanych z da
nymi typów prostych. Celem jest zminimalizowanie konieczności konwersji typów
w programach klienckich, a także uwzględnienie konwencji stosowanych w systemie
operacyjnym do reprezentowania danych. Do odczytu strum ieni bitów ze standardo
wego wejścia służy poniższy interfejs API.
p ub lic c l a s s B in ary Std ln
boolean readBoolean() Odczyt 1 bitu danych i zwrócenie wartości typu boolean
char readChar() Odczyt 8 bitów danych i zwrócenie wartości typu char
char re ad Char (int r) Odczyt r (między 1 a 16) bitów danych i zwrócenie wartości typu char
Podobne metody dla typów byte (8 bitów), short (16 bitów), i nt (32 bity), 1ong i doubl e (64 bity)
boolean isEmpty() Czy strumień bitów jest pusty?
void c l o s e d Zam yka strumień bitów
In te rf e js API z m e to d a m i s ta ty c z n y m i d o o d c z y tu d a n y c h
z e s tr u m ie n ia b itó w z e s ta n d a r d o w e g o w e jśc ia
Kluczową cechą tej abstrakcji jest to, że — inaczej niż w klasie Stdln — dane ze stan
dardowego wejścia nie zawsze są wyrównane względem granic bajtów. Jeśli strum ień
wejściowy obejmuje jeden bajt, klient wczyta go po jednym bicie za pomocą ośmiu
wywołań readBoolean(). M etoda cl ose() nie jest niezbędna, ale w celu eleganckiego
zakończenia pracy należy wywołać ją w kliencie, aby określić, że dalsze bity nie będą
wczytywane. Tak jak w przypadku klas Stdln i StdOut, tak i tu używamy poniższe
go uzupełniającego interfejsu API do zapisywania strum ieni bitów w standardowym
wyjściu.
p ub lic c l a s s BinaryStdOut
void write (bool ean c) Zapis określonego bitu
void w r ite (ch a r c) Zapis określonej 8-bitowej wartości typu char
void w r ite (ch a r c, i nt r) Zapis r (między 1 a 16) najmniej znaczących bitów wartości typu char
Podobne metody dla typów byte (8 bitów), short (16 bitów), in t (32 bity), long i double (64 bity)
void c l o s e d Zam yka strumień bitów
Interfejs API ze statycznymi metodami do zapisu strumienia bitów do standardowego wyjścia
5 .5 ■ Kompresja danych 825
Metoda clo se() strum ienia wyjścia jest niezbędna. Klient musi wywołać cl ose (),
aby zagwarantować, że wszystkie bity określone we wcześniejszych wywołaniach
wri te () trafiły do strum ienia bitów i że ostatni bajt jest uzupełniony zerami, co po
woduje wyrównanie bajta w danych wyjściowych (zapewnia to zgodność z systemem
plików). Z klasami Stdln i StdOut powiązane są interfejsy API In i Out. Tu dostęp
ne są podobne klasy Binaryln i BinaryOut, umożliwiające bezpośrednie korzystanie
z plików z danymi binarnymi.
P rzykład W ramach prostego przykładu załóżmy, że istnieje typ danych, w którym
data reprezentowana jest za pomocą trzech wartości typu i nt (miesiąca, dnia i roku).
Zapisanie tych wartości w formacie 12/31/1999 za pomocą klasy StdOut wymaga
10 znaków, czyli 80 bitów. Zapisanie tych wartości bezpośrednio za pom ocą klasy
BinaryStdOut wymaga 96 bitów (32 bitów na każdą z trzech wartości typu int). Po
zastosowaniu bardziej ekonomicznej reprezentacji, w której miesiąc i dzień zapisany
jest za pom ocą typu byte, a rok — przy użyciu typu short, potrzebne są 32 bity. Klasa
BinaryStdOut pozwala też zapisać pole 4-bitowe, pole 5-bitowe i pole 12-bitowe, co
daje w sumie 21 bitów (a dokładniej — 24 bity, ponieważ pliki muszą obejmować
całkowitą liczbę 8 -bitowych bajtów, dlatego m etoda close() dodaje na końcu trzy
bity 0). Ważna uwaga: uzyskiwanie talach oszczędności samo w sobie stanowi prostą
formę kompresji danych.
Z rzu ty binarne Jak sprawdzić zawartość strum ienia bitów lub bajtów w trakcie
diagnozowania? Na to pytanie próbowali odpowiedzieć sobie pierwsi programiści
w czasach, kiedy jedynym sposobem na znalezienie błędu było sprawdzenie każdego
bitu w pamięci. Pojęcie zrzut jest stosowane od początków informatyki do opisywania
strum ieni bitów w formie czytelnej dla człowieka. Jeśli spróbujesz otworzyć plik za
Strum ień znaków (S td O u t)
S td o u t.p r in t( m o n th + " /" + day + " /" + y e ar);
I o o iio o o lo o iio o io b o io iiiio o iio iić o o iio o o lo o io iiiio o iio o o io o iiio o L O O iiio o io o iiio o ij
l 2 / 3 1 / 1 / 9 9 9 80 bitów
Trzy w artości ty p u i n t (B in a ry S td O u t) 8-bitowa reprezentacja
cyfry 9 w kodzie ASCII
B in a ry S td O u t.w rite (m o n th );
B in a ry S td O u t.w rite (d a y ) ; 32-bitowacalkowitoliczbowa
reprezentacjo wartości 31
B in a ry S td O u t.w rite (y e a r);
| o o o o o o o o l o o o o o o o o io o o o o o Q o |o o o o i i o o |o Qoooo oo: ooo oooo ojo ooo ooo o,oo oii iii ioo oooo oolo o o o o o o o lo o o o o i i i j i i Q o i i i i
12 31 1999 96 bitów
Dwie w artości ty p u c h a r Pole 4-bitow e, pole 5-bitow e
i je d n a ty p u s h o r t (Bi n a ry S td O u t) i p o le 12-bitow e (Bi n a ry S td O u t)
B i n a r y S t d O u t . w r i t e ( C c h a r ) m onth); B in ary S td O u t.w rite (m o n th , 4);
B in a r y S td O u t.w rit e ( (c h a r) day); B i n a r y S t d O u t . w r i t e ( d a y , 5 );
B in aryStd O u t.w rite (C sh o rt) year); B in a r y s t d o u t . w r it e ( y e a r , 12);
|o o o o iio o d o o iiii/o o o o o n ijiio o iiit| | i i o o n i i |i o i i i i i o |o i i l l o n (
12 31 1999 32 bity 12 31 1999 21 bitów (plus 3 bity na wyrównanie
bajta w metodzie c l o s e O J
C ztery s p o s o b y n a u m ie s z c z e n ie d a ty w s ta n d a rd o w y m w yjściu
826 ROZDZIAŁ 5 a Łańcuchy znaków
p ub lic c l a s s BinaryDump pom ocą edytora lub wyświetlić go
{ w taki sam sposób, w jaki oglądasz
p ub lic s t a t i c void m a in ( S tr in g [] args ) pliki tekstowe (lub po prostu u ru
{ chomisz program używający klasy
i n t width = I n t e g e r . p a r s e l n t ( a r g s [0 ]);
i n t cnt; BinaryStdOut), prawdopodobnie
f o r (cnt = 0; ! B i n a r y S t d I n . i s E m p t y ( ) ; cnt++) zobaczysz bezsensowne dane (zale
! ży to od używanego systemu). Klasa
i f (width == 0) continue;
i f (cnt != 0 && cnt % width == 0) BinaryStdln pozwala uniknąć tego
StdO u [Link] (); typu zależności od systemu przez
i f (B ina ryStd ln .read B oole an O ) napisanie własnych programów do
Std O u t.p rint("l");
else S t d O u t. p rin t("0 ");
przekształcania strum ieni bitów
} w taki sposób, aby można wyświet
StdO u [Link] (); lać je za pom ocą standardowych
S t d O u t . p r i n t ln ( " L i c z b a bitow: " + cn t) ;
narzędzi. Przykładowo, widoczny
po lewej program BinaryDump to
klient lclasy Bi nary Stdln wyświetla
W y św ie tla n ie s tru m ie n ia b itó w w s ta n d a rd o w y m (z n a k o w y m ) w yjściu
jący bity ze standardowego wejścia,
zakodowane za pomocą znaków 0
i 1 . Program jest przydatny do diagnozowania przy pracy z krótMmi danymi wej
ściowymi. Podobny Mient, HexDump, grupuje dane w 8 -bitowe bajty i wyświetla każdy
z nich w postaci dwóch cyfr szesnastkowych, z których każda reprezentuje 4 bity.
Klient P i c t u r e D u m p wyświetla bity z obiektu P i c t u r e . Bity 0 reprezentują tu białe
piksele, a bity 1 to czarne piksele. Ta obrazkowa reprezentacja jest często przydatna
do identyfikowania wzorców w strumieniach bitów. Programy Bi nar yDump, HexDump
i Pi c t u r e D u m p można pobrać z poświęconej książce witryny. Przy pracy z plikami bi
narnymi zwyMe stosujemy potoki i przekierowywanie na poziomie wiersza poleceń.
Dane wyjściowe programu kodującego można potokowo skierować do programu
Bi nar yDump, HexDump lub Pi c t u r e D u m p albo przekierować je do pliku.
S tand ard o w y stru m ień znaków Strum ień bitów re p rezen to w an y za pom ocą cyfr szesnastkow ych
% more a b r a . t x t % ja v a HexDump 4 < a b r a . t x t
ABRACADABRA! 41 42 52 41
43 41 44 41
Strum ień bitów re p rezen to w an y za p o m o cą znaków 0 i 1 42 52 41 21
L ic z b a bitów: 96
% ja va BinaryDump 16 < a b r a . t x t
0100000101000010 S trum ień b itó w re p re z e n to w a n y ja k o pik sele z o b ie k tu Picture
0101001001000001
0100001101000001
% ja va PictureDump 16 6 < [Link]
l i Li
0100010001000001 Powiększone okno
0100001001010010 16 na 6 pikseli
0100000100100001
L ic zb a bitów: 96 L iczb a bitów: 96
Cztery sposoby interpretowania strumienia bitów
5.5 Q Kompresja danych 827
K odow anie A S C II Przy zastosowaniu 0 1 2 3 4 5 6 7 8 9 A B C D E F
program u HexDump do strum ienia bitów, NULSOHSTXETX¡- i ENQ ACK BEL GS HT LF VT FF CR so
który obejmuje znaki ASCII, przydatna r-i.L [> 1 DCZ DC3 DC-I U,1 SYNËTBCAMEMSUEr--c FS GS P.S us
jest tabela pokazana po prawej stronie. SP ! " # $ % & i ( ) + J - /
Pierwszą z dwóch cyfr szesnastkowych 0 1 2 3 4 5 6 7 8 9 < = > ?
J
należy potraktować jak indeks wier
@ A B c D E F G H I 3 K L M N 0
sza, a drugą — jak indeks kolumny,
P Q R s T U V W X Y Z [ \ ] A _
aby określić w ten sposób zakodowany - a b c d e f
g h i j k i m n 0
znak. Przykładowo, kod 31 oznacza cy
frę 1, kod 4A to litera J itd. Tabela doty P q r s t u V w X y z { i } ~
czy 7-bitowego kodowania ASCII, dla T abela konwersji m iędzy kodow aniem
szesnastkow ym a kodow aniem ASCII
tego pierwsza cyfra szesnastkowa musi
być równa 7 lub mniej. Liczby szesnast
kowe rozpoczynające się od 0 lub 1 (oraz liczby 20 i 7F) odpowiadają niewyświet-
lanym znakom sterującym. Wiele znaków sterujących to pozostałości po czasach,
kiedy urządzeniami fizycznymi, takim i jak maszyny do pisania, sterowano za pom o
cą znaków ASCII. W tabeli wyróżniono kilka takich znaków, które mogą wystąpić
w zrzutach. Przykładowo, SP to znak spacji, NUL to znak pusty, LF to znak wysuwu
wiersza, a CR to znak powrotu karetki.
p o d s u m u j m y — kompresja danych wymaga zmiany myślenia o standardowym wej
ściu i wyjściu przez uwzględnienie binarnego kodowania danych. Klasy Bi n a r y S t d l n
i Bi naryStdOut zapewniają potrzebne metody. Metody te umożliwiają w programach
klienckich wyraźne oddzielenie zapisu informacji przeznaczonych do przechowywa
nia w plikach i przesyłania (odczytywanych przez programy) od wyświetlania infor
macji (odczytywanych przez ludzi).
828 ROZDZIAŁ 5 Q Łańcuchy znaków
Ograniczenia Aby docenić algorytmy kompresji danych, trzeba zrozumieć pod
stawowe ograniczenia. Naukowcy opracowali wyczerpujące i ważne podstawy teore
tyczne dotyczące tych ograniczeń. Zagadnienia te omawiamy pokrótce w końcowej
części podrozdziału, jednak kilka pomysłów pomoże rozpocząć analizy.
Uniwersalne algorytm y kom presji danych Dostępne są algorytmiczne narzędzia,
których przydatność udowodniono dla bardzo wielu problemów, dlatego może się
wydawać, że celem powinien być uniwersalny algorytm kompresji danych, pozwalają
cy skrócić każdy strum ień bitów. Trzeba jednak przyjąć skromniejsze cele, ponieważ
opracowanie uniwersalnej m etody kompresji danych jest niemożliwe.
Twierdzenie S. Żaden algorytm nie potrań skompresować ł
dowolnego strum ienia bitów. u
Dowód. Omawiamy dwa dowody, które dotyczą tej samej m y I
1 1
śli. Pierwszy to dowód przez zaprzeczenie. Załóżmy, że istnie
ł
je algorytm kompresujący każdy strum ień bitów. Można więc u
wykorzystać ten algorytm do skompresowania jego danych
T
wyjściowych i uzyskania jeszcze krótszego strumienia. Proces 1
m ożna kontynuować do m om entu uzyskania strumienia bitów
o długości 0! Wniosek, że algorytm kompresuje każdy stru
mień bitów do 0 bitów, jest absurdalny, podobnie jak założenie,
iż algorytm potrafi skompresować dowolny strum ień bitów.
Drugi dowód oparty jest na wyliczaniu. Załóżmy, że istnieje
algorytm, który zapewnia kompresję bezstratną każdego 1000 -
bitowego strumienia. Oznacza to, że każdy taki strum ień musi
odpowiadać odm iennem u krótszemu strumieniowi. Istnieje
jednak tylko 1 + 2 + 4 + ... + 2 " e + 2999 = 2 101)0 - 1 strum ieni
bitów mających mniej niż 1000 bitów oraz 2 1000 strum ieni bi
tów o 1000 bitów, dlatego algorytm nie może skompresować
każdego strumienia. To wnioskowanie staje się bardziej prze
konujące, jeśli rozważymy mocniejsze stwierdzenia. Załóżmy,
że celem jest uzyskanie współczynnika kompresji na poziomie
ponad 50%. Trzeba zdawać sobie sprawę, że jest to możliwe tyl
ko dla około 1 z 2500 1000 -bitowych strum ieni bitów!
Rozważania te m ożna ująć inaczej — w każdym algorytmie kom- £
presji danych kompresja 1000 -bitowego losowego strum ienia o po- u n iw e rs a ln a
łowę będzie możliwa w najwyżej 1 przypadku na 2500. Po natrafię- k o m p re s ja d a n y ch ?
niu na nowy algorytm kompresji bezstratnej można mieć pewność,
że nie zapewnia istotnej kompresji dla losowych strum ieni bitów.
Wniosek, że nie m ożna liczyć na kompresję losowych łańcuchów
znaków, jest punktem wyjścia do zrozumienia kompresji danych.
5.5 o Kompresja danych 829
% j a v a R a n d o m B it s [ ja v a PictureDump 2000 500
L i c z b a bitów: 1000000
T ru d n y d o s k o m p re s o w a n ia plik - m ilio n p s e u d o lo s o w y c h b itó w
Regularnie przetwarzamy łańcuchy znaków obejmujące miliony lub miliardy bitów,
jednak zdecydowana większość możliwych łańcuchów tego rodzaju nigdy nie wystę
puje, dlatego nie należy zniechęcać się teoretycznymi wynikami. Regularnie przetwa
rzane strumienie bitów są zwykle wysoce ustrukturyzowane, co m ożna wykorzystać
w kontekście kompresji.
Nierozstrzygalność Rozważmy przedstawiony w górnej części strony łańcuch milio
na bitów. Łańcuch wygląda na losowy, dlatego prawdopodobnie nie uda się znaleźć
bezstratnego algorytmu do jego skompresowania. Istnieje jednak sposób na zapisanie
tego łańcucha za pomocą tylko kilku tysięcy bitów, ponieważ dane wygenerowano
przy użyciu przedstawionego poniżej programu (jest to generator liczb pseudoloso
wych, podobny do metody Math. r a ndom () Javy). Algorytm kompresji, który kompre
suje dane przez zapisanie programu w kodzie ASCII i rozpakowuje je przez wczytanie
oraz uruchomienie programu, zapewnia współczynnik kompresji na poziomie 0,3.
Trudno uzyskać lepszy wynik (a współczynnik można obniżyć w dowolnym stopniu,
generując więcej bitów). Kompresja takiego pliku wymaga odkrycia programu uży
tego do wygenerowania danych. Przykład ten nie jest tak sztuczny, jak może się na
pozór wydawać. Przy kompresowaniu filmów, dawnych książek wczytanych za pomocą
skanera lub niezliczonych innych typów plików z sieci W W W mamy pewną wiedzę
o programach zastosowanych do utworze
nia plików. Stwierdzenie, że duża część prze- publ i c cl ass RandomBits
twarzanych danych jest generowana przez {
programy, prowadzi do zaawansowanych za- pub lic s t a t i c void m a in ( S tr in g [] args)
gadnień z teorii obliczeń, a ponadto pozwala ^ int x = lllU -
zrozumieć wyzwania związane z kompresją for (int i = 0; i < 1000000; i++)
danych. Przykładowo, można udowodnić, że i
problem optymalnej kompresji danych (zna- * . .
lezienia najkrótszego programu generującego j
dany łańcuch) jest nierozstrzygalny. Nie tyl- B i n a r y S t d O u t .c lo s e O ;
ko nie istnieje algorytm kompresujący każdy ^
strumień bitów, ale też nie można opracować
Strategii tworzenia najlepszego algorytmu! „ S k o m p re s o w a n y " s tr u m ie ń m ilio n a b itó w
830 ROZDZIAŁ 5 n Łańcuchy znaków
Praktyczne skutki opisanych ograniczeń są takie, że przy tworzeniu m etod kompresji
bezstratnej trzeba wykorzystać znaną strukturę kompresowanych strum ieni bitów.
W czterech omawianych metodach wykorzystano kolejno poniższe cechy struktu
ralne:
■ małe alfabety;
■ długie ciągi identycznych bitów lub znaków;
■ często używane znaki;
■ długie wielokrotnie występujące ciągi bitów lub znaków.
Jeśli wiadomo, że dany strum ień bitów ma przynajmniej jedną z tych cech, m ożna go
skompresować za pomocą jednej z opisanych dalej metod. W przeciwnym razie i tak
często warto wypróbować te techniki, ponieważ struktura danych może nie być oczy
wista, a m etody te mają wiele zastosowań. Jak się okaże, każda m etoda ma param etry
i wersje, które mogą wymagać dostosowania w celu optymalnego skompresowania
konkretnego strum ienia bitów. Pierwszym i ostatnim krokiem jest dowiedzenie się
czegoś o strukturze danych oraz wykorzystanie tej wiedzy do ich skompresowania,
prawdopodobnie za pom ocą jednej z omawianych technik.
5.5 n Kompresja danych 831
Rozgrzewka — genom W ramach przygotowań do bardziej skomplikowanych
algorytmów kompresji danych omawiamy podstawowe, ale bardzo ważne zadanie
z tego obszaru. We wszystkich implementacjach stosujemy konwencje wprowadzone
w tym przykładzie.
D a n e o g e n o m i e W ram ach pierwszego przykładu rozważmy poniższy łańcuch
znaków.
ATAGATGCATAGCGCATAGCTAGATGTGCTAGCAT
W standardowym kodowaniu ASCII — 1 bajt (8 bitów) na znak — ten łańcuch zna
ków jest strum ieniem bitów o długości 8 x 35 = 280. Łańcuchy znaków tego rodzaju
są niezwykle ważne we współczesnej biologii, ponieważ biolodzy stosują litery A, C, T
i Gdo reprezentowania czterech nukleotydów z DNA żywych organizmów. Genom to
sekwencja nukleotydów. Naukowcy wiedzą,
że zrozumienie cech genomu jest kluczem p ub lic s t a t i c void compress()
do zrozumienia procesów związanych z ży f
Alphabet DNA = new A l phabet("ACTG");
wymi organizmami, takich jak życie, śmierć
String s = B in aryStd ln .re a d Strin gO ;
i choroby. Znane są genomy wielu żywych int N = s . le n g t h ( ) ;
organizmów, a naukowcy piszą programy do Bin a r y Std O u t .w r ite (N );
f o r (i n t i = 0; i < N; i++ )
badania struktury tych sekwencji.
{ // Za pis dwubitowego kodu znaku,
K o m p r e s j a z a p o m o c ą k o d u 2 - b i t o w e g o
i n t d = D N A . t o I n d e x ( s . c h a r A t ( i) ) ;
Bin aryStd O ut.w rite (d , DNA.1g R ( ) );
Prostą cechą genomów jest to, że obejmują
1
tylko cztery różne znaki, dlatego można za BinaryStdO [Link] ;
kodować je za pomocą dwóch bitów na znak,
tak jak w pokazanej po prawej metodzie
, M e to d a k o m p re s ji d la d a n y c h o g e n o m ie
compress(). Choc wiadomo, ze strum ień
wejściowy obejmuje znaki, do wczytywania danych używamy klasy Bi naryStdln, aby
podkreślić zastosowanie się do standardowego modelu kompresji danych (ze stru
mienia bitów na strum ień bitów). W skompresowanym pliku zapisana jest liczba za
kodowanych znaków, co gwarantuje prawidłowe odkodowanie danych, jeśli ostatni
bit nie znajduje się na końcu bajta. Ponieważ program przekształca każdy 8-bitowy
znak na 2-bitowy kod i zwiększa długość danych tylko o 32 bity, wraz z rosnącą liczbą
znaków poziom współczynnika kompresji zbliża się do 25%.
R o z p a k o w y w a n i e Metoda expand (), przedstawiona na górze
d l a k o d u 2 - b i t o w e g o
następnej strony, rozpakowuje strum ień bitów utworzony przez metodę compress ().
Tak jak przy kompresji, m etoda wczytuje strum ień bitów i zapisuje strum ień bitów,
zgodnie z podstawowym modelem kompresji danych. Strumień bitów generowany
jako dane wyjściowe to pierwotne dane wejściowe.
832 ROZDZIAŁ 5 n Łańcuchy znaków
t o s a m o p o d e j ś c i e sprawdza się też dla in
p ub lic s t a t i c void expand()
{ nych alfabetów o stałym rozmiarze, jednak
Alphabet DNA = new AlphabetC'ACTG"); opracowanie ogólnej wersji pozostawiamy
i n t w = D N A .lg R ();
int N = B in aryStd In .read Int();
jako łatwe ćwiczenie (zobacz ć w i c z e n i e
f o r ( i n t i = 0; i < N; i++) 5-5-25)-
{ // Odczyt dwóch bitów i zapis znaku, Przedstawione m etody nie są w pełni
char c = B in aryStd [Link] d C ha r(w );
zgodne ze standardowym modelem kom
B i naryStdO [Link] e (D N A .t o C h a r( c));
} presji danych, ponieważ skompresowany
Bin aryStdO [Link] ; strum ień bitów nie obejmuje wszystkich in
formacji potrzebnych do jego odkodowania.
M e to d a ro z p a k o w y w a n ia d a n y c h o g e n o m ie
To, że alfabet składa się z liter A, C, T i G, jest
określone w dwóch metodach. Konwencja
ta jest sensowna w obszarach w rodzaju badań nad genomem, gdzie ten sam kod
jest wykorzystywany wielokrotnie. W innych sytuacjach trzeba czasem podać alfabet
w zakodowanej wiadomości (zobacz ć w i c z e n i e 5 .5 .25 ). Norm ą w dziedzinie kom
presji danych jest uwzględnianie takich kosztów przy porównywaniu metod.
W początkowym okresie badań nad genomem ustalanie sekwencji genomu było
długim i żmudnym zadaniem, dlatego sekwencje były stosunkowo krótkie, a n a
ukowcy korzystali ze standardowego kodowania ASCII do zapisywania i przesyła
nia sekwencji. Potem proces prowadzenia eksperymentów znacznie przyspieszono.
Obecnie znane są liczne i długie genomy (genom człowieka obejmuje ponad 1010
bitów), a oszczędności na poziomie 75%, co zapewniają opisane metody, są bardzo
istotne. Czy m ożna jeszcze bardziej zwiększyć poziom kompresji? Jest to bardzo cie
kawe i naukowe pytanie — możliwość kompresji pozwala zakładać istnienie pewnej
struktury w danych, a podstawowym zadaniem współczesnych badań nad genomem
jest jej odkrycie. Standardowe m etody kompresji danych, takie jak opisane dalej, są
uważane za nieskuteczne zarówno dla zapisanych za pom ocą kodu 2 -bitowego da
nych o genomie, jak i dla danych losowych.
Metody c o m p r e s s () i e x p a n d ()
zapisano jako m etody statyczne p ub lic c l a s s Genome
w tej samej klasie, wraz z prostym {
p ub lic s t a t i c void compress()
sterownikiem pokazanym po prawej // Zobacz op is w te k ś c ie .
stronie. Aby przetestować poziom
pub lic s t a t i c void expand()
zrozumienia reguł gry i podstawo
// Zobacz op is w t ekście .
we narzędzia używane do kom pre
sji danych, należy zrozumieć różne pub lic s t a t i c void m a in ( S tr in g [] args)
polecenia z następnej strony (i skut {
i f ( a r g s [ 0 ] . e q u a l s ( " - " ) ) compressO;
ki ich wykonania), gdzie metody i f ( a r g s [ 0 ] , e q u a l s ( " + " ) ) expand();
G e n o m e . c o m p r e s s () i G e n o m e . e x }
p a n d ! ) są wywoływane dla przykła
dowych danych.
S p o s ó b tw o rz e n ia p a k ie tu z m e to d a m i k o m p re s ji d a n y c h
5.5 a Kompresja danych 833
Krótki przypadek testowy (264 bity)
% more [Link]
ATAGATGCATAGCGCATAGCTAGATGTGCTAGC
java BinaryDump 64 < [Link]
0100000101010100010000010100011101000001010101000100011101000011
0100000101010100010000010100011101000011010001110100001101000001
0101010001000001010001110100001101010100010000010100011101000001
0101010001000111010101000100011101000011010101000100000101000111
01000011
Liczba bitów: 264
% java Genome - < [Link]
? ? -<--------------- Nie m ożna wyświetlić strum ienia bitów w standardowym wyjściu
% ja va Genome - < [Link] | java BinaryDump 64
0000000000000000000000000010000100100011001011010010001101110100
1000110110001100101110110110001101000000
Liczba bitów: 104
% java Genome - < [Link] |java HexDump 8
00 00 00 21 23 2d 23 74
8d 8c bb 63 40
Liczba bitów: 104
% java Genome - < [Link] > genomeTiny.2bit
% ja va Genome + < genomeTiny.2bit
ATAGATGCATAGCGCATAGCTAGATGTGCTAGC ^
Cykl kompresji i rozpakowywana
% java Genome - < [Link] | java Genoine_+ d4 ep¡erw0tt,edímewejic¡owe
ATAGATGCATAGCGCATAGCTAGATGTGCTAGC -t--------------
K rótki p r z y p a d e k t e s t o w y (2 6 4 b ity )
% j a v a P ic tu re D u m p 512 100 < g e n o m e V i r u s . t x t
L i c z b a b it ó w : 50000
% j a v a Genome - < g e n o m e v i r u s . t x t | j a v a P ic tu re D u m p 512 25
L i c z b a b it ó w : 12536
K o m p re sja i ro z p a k o w y w a n ie sek w e n c ji g e n o m u za p o m o c ą k o d o w a n ia 2 -b ito w e g o
834 ROZDZIAŁ 5 o Łańcuchy znaków
Kodowanie długości serii Najprostszym rodzajem nadmiarowości w stru
mieniach bitów są długie serie powtarzających się bitów. Dalej omawiamy klasyczną
metodę, kodowanie długości serii, która pozwala wykorzystać tę nadmiarowość do
kompresowania danych. Rozważmy na przykład poniższy 40-bitowy łańcuch:
0000000000000001111111000000011111111111
Łańcuch składa się z 15 cyfr 0, 7 cyfr 1, 7 cyfr 0, a następnie 11 cyfr 1. Można za
kodować go za pom ocą liczb 15, 7, 7 i 11. Wszystkie strum ienie bitów składają się
z naprzemiennych serii zer i jedynek. Wystarczy zakodować długość tych serii. Jeśli
dla przykładowych danych zastosujemy 4 bity do zakodowania liczb i zaczniemy od
serii cyfr 0, otrzymamy 16-bitowy łańcuch znaków:
1111011101111011
15 = 1111,7 = 0111,7 = 0111, a następnie 11 = 1011. W spółczynnik kompresji wyno
si tu 16/40 = 40%. Aby przekształcić ten opis w skuteczną metodę kompresji danych,
trzeba uwzględnić następujące kwestie.
■ Ile bitów potrzeba do zapisania długości serii?
° Co zrobić po napotkaniu serii dłuższej niż maksymalna długość wyznaczana
przez wybraną liczbę bitów?
n Co zrobić, jeśli serie są krótsze niż liczba bitów potrzebna do zapisania ich dłu
gości?
Przede wszystkim interesują nas długie strumienie bitów o stosunkowo niewielu
krótkich seriach, dlatego dokonaliśmy opisanych poniżej wyborów.
° Długość serii wynosi od 0 do 255 i jest kodowana za pom ocą 8 bitów.
■ Wszystkie długości traktujemy jak krótsze niż 256, dołączając w razie potrzeby
serię o długości 0 .
■ Krótkie serie też kodujemy, nawet jeśli może to zwiększyć długość danych wyj
ściowych.
Rozwiązanie oparte na tych wyborach bardzo łatwo jest zaimplementować, a także
okazuje się bardzo skuteczne dla kilku rodzajów strum ieni bitów powszechnie napo
tykanych w praktyce. Technika ta nie jest skuteczna, kiedy liczba krótkich serii jest
duża. Bity m ożna zaoszczędzić tylko wtedy, kiedy seria jest dłuższa niż liczba bitów
potrzebna do jej zapisania w kodzie binarnym.
B itm apy Kodowanie długości serii jest skuteczne na przykład dla bitmap, powszech
nie stosowanych do reprezentowania obrazów i zeskanowanych dokumentów. Z uwa
gi na zwięzłość i prostotę omawiamy bitmapy o wartościach binarnych uporządko
wane w strumienie bitów przez pobranie pikseli w kolejności wyznaczanej przez
wiersze. Do wyświetlania zawartości bitmap służy program Pi ctureDump. Można
łatwo napisać program do przekształcania obrazu z jednego z wielu bezstratnych
formatów zdefiniowanych dla zrzutów ekranu lub zeskanowanych dokumentów na
bitmapę. Przykład demonstrujący skuteczność kodowania długości serii oparty jest
na zrzucie z tej książki, a konkretnie — na literze q (w różnych rozdzielczościach).
5.5 □ Kompresja danych 835
Koncentrujemy się na zrzucie binarnym 7 cyfr 1
% java BinaryDump 32 < [Link]
dla zrzutu ekranu o wymiarach 32 na 48 OOOOOOOOOOOOOOOOOOOOOOOOOOO-O-ffOOO
pikseli. Po prawej stronie pokazano zrzut 000 00 000 0000000000000 ooo.&cioooo 00
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 ll l l f f O O O O O O O O o 15 7 10
00000000000011111111111111100000 12 15 5
binarny wraz z długościami serii w każ 00000000001111000011111111100000 10
10 44 4 99 5
00000000111100000000011111100000 8 44 9 9 6 6 5
dym wierszu. Ponieważ każdy wiersz za 00000001110000000000001111100000 77 33 12
12 55 5
00000011110000000000001111100000 66 44 12
12 55 5
czyna i kończy się cyframi 0, wszystkie 00000111100000000000001111100000 55 44 13
13 55 5
00001111000000000000001111100000 44 44 14
14 55 5
wiersze obejmują nieparzystą liczbę serii. 00001111000000000000001111100000 44 44 14
14 55 5
00011110000000000000001111100000 33 44 15
15 55 5
Ponieważ koniec każdego wiersza jest 00011110000000000000001111100000 22 55 15
15 55 5
00111110000000000000001111100000 22 55 15
15 55 5
kontynuowany w następnym, długości 00111110000000000000001111100000 22 55 15
15 55 5
00111110000000000000001111100000 22 55 15
15 55 5
serii w strum ieniu bitów są sumą dłu 00111110000000000000001111100000 22 55 15
15 55 5
00111110000000000000001111100000 22 55 15
15 55 5
gości ostatniej serii z każdego wiersza 00111110000000000000001111100000 22 55 15
15 55 5
00111110000000000000001111100000 22 55 15
15 55 5
i pierwszej serii z następnego (oraz dłu 00111110000000000000001111100000 22 55 15
15 55 5
00111111000000000000001111100000 22 14 55
66 14 5
gości odpowiednich wierszy składających 00111111000000000000001111100000 22 66 14
14 55 5
00011111100000000000001111100000 33 66 13
13 55 5
się z samych cyfr 0). 00011111100000000000001111100000 33 66 13
13 55 5
00001111110000000000001111100000 44 66 12
12 55 5
00001111111000000000001111100000 44 77 11
11 55 5
Im plem entacja Przedstawiony wcześ 00000111111100000000001111100000 55 77 10
10 55 5
00000011111111000000011111100000 66 88 7 66 5
niej nieformalny opis bezpośrednio 00000001111111111111111111100000 7 20 5
00000000011111111111001111100000 9 11 2 5
prowadzi do implementacji m etod com 00000000000011111000001111100000 22 5 5
00000000000000000000001111100000 22 5 5
press () i expand() zaprezentowanych na 00000000000000000000001111100000 22 5 5
00000000000000000000001111100000 22 5 5
następnej stronie. Kod m etody expand () 00000000000000000000001111100000 22 5 5
00000000000000000000001111100000 22 5 5
jest, jak zwykle, prostszy — wczytuje dłu 00000000000000000000001111100000 22 5 5
00000000000000000000001111100000 22 5 5
gość serii, zapisuje odpowiednią liczbę 00000000000000000000001111100000 22 5 5
00000000000000000000001111100000 22 5 5
kopii bieżącego bitu, uzupełnia bieżący 00000000000000000000001111100000 22 5 5
00000000000000000000001111100000 22 5 5
bajt i kontynuuje proces do czasu wy 00000000000000000000011111110000 21
18
7 4
12 2
00000000000000000011111111111100
czerpania danych wejściowych. Metoda 00000000000000000U .il11111111110 17 14 1
oooooooooooo ooooootmoooooooooooo 32
compress () nie jest dużo bardziej skom 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 o o o T k l o o o o 0 0 0 0 0 32
plikowana. Dopóki w strum ieniu danych Liczba bitów: 1536 " ^ 7 7 cyfrO
wejściowych znajdują się bity, wykonuje T y p ow a b itm a p a o ra z d łu g o ś c i serii z k a ż d e g o w ie rsza
następujące kroki:
H Wczytuje bit.
D Jeśli dany bit różni się od ostatnio wczytanego, zapisuje liczbę wystąpień i zeruje
licznik.
■ Jeśli dany bit jest taki sam, jak ostatnio wczytany, a liczba wystąpień jest maksy
malna, zapisuje tę liczbę, zapisuje 0 i zeruje licznik.
■ Zwiększa wartość licznika.
Po opróżnieniu strum ienia wejściowego zapis wartości licznika (długości ostatniej
serii) kończy proces.
Zw iększanie rozdzielczości bitm ap Podstawową przyczyną powszechnego stoso
wania kodowania długości serii do bitmap jest to, że skuteczność m etody znacznie
rośnie wraz ze wzrostem rozdzielczości. Łatwo dostrzec, dlaczego jest to prawdą.
Załóżmy, że w przykładzie podwajamy rozdzielczość. Oczywiste stają się wtedy na
stępujące kwestie:
836 ROZDZIAŁ 5 o Łańcuchy znaków
Liczba bitów rośnie czterokrotnie.
Liczba serii rośnie około dwukrotnie.
Długości serii rosną około dwukrotnie.
Liczba bitów w skompresowanej wersji rośnie około dwukrotnie.
Dlatego współczynnik kompresji zmniejsza się o połowę!
Bez kodowania długości serii przy podwojeniu
p ub lic s t a t i c void expand() rozdzielczości ilość potrzebnej pamięci roś
{ nie czterokrotnie. Przy stosowaniu omawianej
boolean b = f a l s e ;
while ( I B i n a r y S t d ln . i s E m p t y O )
techniki podwojenie rozdzielczości powoduje
{ tylko podwojenie ilości pamięci. Oznacza to,
char cnt = B in a r y S t d l n . r e a d C h a r Q ; że ilość pamięci rośnie, a współczynnik kom
f o r ( i n t i = 0 ; i < cnt; i++)
presji maleje liniowo wraz z rozdzielczością.
Bin aryStd O ut.w rit e ( b ) ;
b = !b; Przykładowo, dla litery q o niskiej rozdzielczo
} ści współczynnik kompresji wynosi 74%. Jeśli
B i n a r y S t d O u t . c lo s e ( ) ;
zwiększymy rozdzielczość do 64 na 96, współ
czynnik wyniesie 37%. Zmiana ta jest wyraźnie
p ub lic s t a t i c void compress() widoczna w danych wyjściowych z programu
f Pi ctureDump, pokazanych na rysunku na na
char cnt = 0;
boolean b, old = f a l s e ;
stępnej stronie. Litera o wyższej rozdzielczości
while ( I B i n a r y S t d ln . i s E m p t y O ) zajmuje czterokrotnie więcej miejsca niż lite
{ ra o niższej rozdzielczości (dwukrotnie więcej
b = [Link];
w obu wymiarach), natomiast wielkość skom
i f (b != old)
{ presowanej wersji rośnie tylko dwukrotnie (dwa
Bi [Link] t e ( c n t ) ; razy w jednym wymiarze). Jeśli zwiększymy roz
cnt = 0;
dzielczość jeszcze bardziej, do wymiarów 128 na
old = !old;
192 (bliżej tego, co jest potrzebne przy druku),
el se współczynnik zmniejszy się do 18% (zobacz
{ ć w i c z e n i e 5 . 5 . 5 ).
i f (cnt == 255)
{
B i n a r y S td O u t .w r ite (c n t); KO D O W A N IE DŁU G O ŚCI SE R II JEST BARDZO
cnt = 0;
s k u t e c z n e w wielu sytuacjach, j ednak w bardzo
B in a r y S td O u t .w r ite (c n t);
licznych przypadkach strum ienie bitów przezna
czone do kompresji (na przykład typowy tekst
cnt++;
w języku polskim) mogą w ogóle nie obejmować
}
B i n a r y S td O u t .w r ite (c n t); długich serii. Dalej omawiamy dwie m etody
B i n a r y S t d O u t . c lo s e ( ) ; skuteczne dla różnorodnych plików. Techniki
te są powszechnie stosowane i prawdopodobnie
M e to d y ro z p a k o w y w a n ia i k o m p re s ji
korzystałeś z jednej lub z obu tych m etod przy
p rz y k o d o w a n iu d łu g o ś c i serii pobieraniu danych z sieci WWW.
5.5 Kompresja danych 837
Krótki przypadek testowy (40 bitów)
% java BinaryDump 40 < 4r un s.b in
0000000000000001111111000000011111111111
Liczba bitów: 40
% java RunLength - < 4 run s.b in | java HexDump
0f 07 07 Ob
.................................... w sp ó łc zy n n ik k o m p re sji 3 2 /4 0 = 80%
Liczba bitów: 32
% java RunLength - < 4 run s.b in | java RunLength + | java BinaryDump 40
0000000000000001111111000000011111111111
Liczba bitów* 40 — -— E fe kte m k o m p resji i ro zp a ko w a n ia
są p ie r w o tn e d a n e w ejściow e
Tekst w formacie ASCII (96 bitów)
% java RunLength - < ab ra .tx t | java HexDump 24
01 01 05 01 01 01 04 01 02 01 01 01 02 01 02 01 05 01 01 01 04 02 01 01
05 01 01 01 03 01 03 01 05 01 01 01 04 01 02 01 01 01 02 01 02 01 05 01
02 01 04 01 ,
^ 416 < W s p ó łc z y n n ik ko m p resji to 4 1 6 /9 6 = 433%
— n ie n a leży sto so w a ć k o d o w a n ia długości serii d la tek stu w fo r m a c ie A S C II!
Bitmapa (1536 bitów)
q
i java PictureDump 32 48 < [Link]
% java RunLength - < [Link] > q 3 2 x48 .b in .rle
% java HexDump 16 < q 32x48.b in.r í e
4f 07 16 0 f Of 04 04 09 Od 04 09 06 Oc 03 Oc 05
Ob 04 Oc 05 Oa 04 Od 05 09 04 Oe 05 09 04 Oe 05
Liczba bitów: 1536
08 04 Of 05 08 04 Of 05 07 05 Of 05 07 05 Of 05
% java PictureDump 32 36 < [Link]
07 05 Of 05 07 05 Of 05 07 05 Of 05 07 05 Of 05
07 05 Of 05 07 05 Of 05 07 06 Oe 05 07 06 Oe 05
08 06 Od 05 08 06 Od 05 09 06 Oc 05 09 07 Ob 05
Oa 07 Oa 05 Ob 08 07 06 Oc 14 Oe Ob 02 05 11 05
Liczba bitów: 1144
05 05 lb 05 Ib 05 lb 05 lb 05 Ib 05 lb 05 lb 05
lb 05 lb 05 lb 05 lb 05 la 07 16 Oc 13 Oe 41
Liczba bitów: 1144 -<---- _— W sp ó łc z y n n ik k o m p resji java PictureDump 64 96 < [Link]
to 1 1 4 4 /1 5 3 6 = 74%
Bitmapa o wyższej rozdzielczości (6144 bity)
% java BinaryDump 0 [Link]
6144 b i t s
% java RunLength - < [Link] java BinaryDump 0
Liczba bitów: 2296 W sp ó łc z y n n ik k o m p re sji to 2 2 9 6 /6 1 4 4 = 37%
Liczba bitów: 6144
% java PictureDump 64 36 < [Link]
W i*. El
Liczba bitów: 2296
K o m p re so w a n ie i ro z p a k o w y w a n ie stru m ie n i b itó w za p o m o c ą k o d o w a n ia d łu g o ści serii
838 ROZDZIAŁ 5 a Łańcuchy znaków
Kompresja Huffmana Tu omawiamy technikę kompresji danych, która po
zwala zaoszczędzić dużą ilość pamięci w plikach z tekstem w języku naturalnym
(i w plikach wielu innych rodzajów). Pomysł polega na rezygnacji ze standardowego
sposobu przechowywania plików tekstowych. Zamiast zapisywać każdy znak za po
mocą 7 lub 8 bitów, używamy mniejszej liczby bitów na znaki występujące często,
a większej — na znaki pojawiające się rzadko.
Aby przedstawić podstawowe pomysły, zaczynamy od krótkiego przykładu.
Załóżmy, że chcemy zapisać łańcuch ABRACADABRA!. Zakodowanie go za pom ocą
7-bitowego kodu ASCII daje poniższy łańcuch bitów:
100000110000101010010100000110000111000001 -
100010010000011000010101001010000010100001
Aby go odkodować, wystarczy wczytywać dane po 7 bitów i przekształcać je według
tabeli ASCII ze strony 827. Przy standardowym kodowaniu występująca tylko raz
litera D wymaga tyle samo bitów, co litera A, pojawiająca się pięć razy. Kompresja
Huffmana jest oparta na pomyśle, że można zaoszczędzić pamięć, kodując często
używane znaki za pom ocą mniejszej liczby bitów, niż potrzeba ich na rzadko stoso
wane znaki. Pozwala to zmniejszyć łączną liczbę używanych bitów.
K ody bezprefiksowe o zm iennej długości Kod wiąże każdy znak z łańcuchem bitów
i ma postać tablicy symboli, w której kluczami są znaki, a wartościami — łańcuchy
bitów. Początkowo możemy spróbować przypisać najkrótsze łańcuchy bitów do naj
częściej występujących liter. A można zakodować jako 0, Bjako 1, Rjako 00, Cjako 01,
Djako 10, a ! jako 11. Wtedy łańcuch ABRACADABRA! jest kodowany jako 0 1 00 0 01 0
10 0 1 00 0 11. Ta reprezentacja wymaga tylko 17 bitów w porównaniu z 77 bitami
dla 7-bitowego kodowania ASCII. Nie jest to jednak prawdziwy kod, ponieważ wy
maga odstępów rozdzielających znaki. Bez odstępów łańcuch bitów wygląda tak:
01000010100100011
i m ożna go odkodować jako CRRDDCRCB lub kilka innych łańcuchów znaków. Nadal
jednak 17 bitów plus 10 ograniczników to mniej niż pierwotna wersja, co wynika
głównie z tego, że nie są potrzebne bity do kodowania liter, które nie występują w tek
ście. Następny krok polega na wykorzystaniu tego, że ograniczniki nie są potrzebne,
jeśli kod żadnego znaku nie jest przedrostkiem innego. Kod o tej właściwości to kod
bezprefiksowy. Przedstawiony wcześniej kod nie jest bezprefiksowy, ponieważ 0, kod
litery A, jest przedrostkiem 00, kodu litery R. Przykładowo, jeśli zakodujemy A jako
0, B jako 1111, C jako 110, Djako 100, R jako 1110, a ! jako 101, poniższy 30-bitowy
łańcuch będzie m ożna odkodować w tyko jeden sposób:
011111110011001000111111100101
ABRACADABRA! Wszystkie kody bezprefiksowe można jednoznacznie odkodować (bez
konieczności stosowania ograniczników) w ten sposób, dlatego są powszechnie sto
sowane w praktyce. Zauważmy, że kody o stałej długości, takie jak 7-bitowe kodowa
nie ASCII, są bezprefiksowe.
5.5 o Kompresja danych 839
R eprezentacja k o d ó w bezprefiksowych z a p o Tablica słó w ko d o w y c h R eprezentacja w p ostaci d rze w a trie
Klucz Wartość
m ocą d rzew a trie Jednym z wygodnych sposo
! 101
bów na reprezentowanie kodów bezprefiksowych A 0
jest drzewo trie (zobacz p o d r o z d z i a ł 5.2 ). Każde B 1111
drzewo trie o Modnośnikach nuli jest bezprefikso- C 1 10
D 1 00
wym kodem dla Mznaków. Należy zastąpić odnoś R 1110
niki nul 1 odnośnikami do liści (węzłów o dwóch
odnośnikach nul 1 ), z których każdy obejmuje ko
S k o m p r e s o w a n y ła ń c u c h b itó w
dowany znak, i zdefiniować kod każdego znaku 0111111100 11001000 111111100101 — 3 0 b itów
za pomocą łańcucha bitów zdefiniowanego przez A B RA CA DA B RA !
ścieżkę z korzenia do znaku (w standardowy dla
drzew trie sposób — łącząc 0 z przejściem w lewo Tablica słó w ko d o w y c h Reprezentacja w p ostaci drzew a trie
Klucz Wartość
i 1 z przejściem w prawo). Przykładowo, na ry ! 101
sunku po prawej stronie pokazano dwa bezprefik- A 11
sowe kody dla znaków z łańcucha ABRACADABRA!. B 00
C 010
Na górze znajduje się opisany wcześniej kod
D 1 00
o zmiennej długości. Poniżej pokazano kod, który R 011
powoduje powstanie łańcucha:
S k o m p r e s o w a n y ła ń cu c h b itó w
11000111101011100110001111101 110 0 0 11110 10 1110 0 110 0 0 111110 1 2 9 b itów
A B R A C A D A B R A !
Obejmuje on 29 bitów, czyli jest o 1 bit krótszy.
Czy istnieje drzewo trie, które zapewnia lepszą Dwa kody bezprefiksowe
kompresję? Jak znaleźć drzewo trie prowadzące do najlepszego kodu bezprefikso-
wego? Okazuje się, że istnieje elegancka odpowiedź na te pytania. Ma ona postać al
gorytmu, który dla dowolnego łańcucha znaków oblicza drzewo trie prowadzące do
strum ienia bitów o minimalnej długości. Aby móc dokonać uczciwego porównania
z innymi kodami, trzeba też uwzględnić bity samego kodu, ponieważ łańcucha nie
można bez niego odkodować, a — jak się okaże — kod zależy od łańcucha. Ogólną
metodę wyszukiwania optymalnego kodu bezprefiksowego opracował (w trakcie stu
diów!) D. Huffman w 1952 roku. Technika ta jest nazywana kodowaniem Hoffmana.
Ogólne om ów ien ie Stosowanie bezprefiksowego kodu do kompresji danych obej
muje pięć głównych etapów. Strumień bitów przeznaczony do zakodowania należy
potraktować jak strum ień bajtów i zastosować kod bezprefiksowy do znaków w na
stępujący sposób:
■ utworzyć drzewo trie dla kodowania;
■ zapisać drzewo trie (zakodowane jako strum ień bitów) do zastosowania przy
rozpakowywaniu;
■ wykorzystać drzewo trie do zakodowania strum ienia bajtów jako strumienia
bitów.
Rozpakowywanie wymaga:
■ wczytania drzewa trie (zakodowanego na początku strum ienia bitów);
■ wykorzystania drzewa trie do odkodowania strum ienia bitów.
Aby pom óc lepiej zrozumieć i docenić ten proces, omawiamy te kroki zgodnie z ich
trudnością.
840 ROZDZIAŁ 5 a Łańcuchy znaków
p riv a t e s t a t i c c l a s s Node implements Comparable<Node>
Węzły drzewa trie Zaczy
{ // Węzeł drzewa t r i e Huffmana. namy od klasy Node przed
p riv a t e char ch; // Nieużywana dla węzłów wewnętrznych stawionej po lewej stronie.
p riv a t e in t freq; // Nieużywana przy rozpakowywaniu,
Przypomina ona zagnieżdżo
p riv a t e final Node l e f t , r i g h t ;
ne klasy używane wcześniej
Node(char ch, i n t freq, Node l e f t , Node ri g h t ) do tworzenia drzew binarnych
{ i drzew trie. Każdy obiekt
t h i s . c h = ch;
t h i s . f r e q = freq; Node obejmuje referencje 1e ft
t h is . le f t = left; i rig h t do innych obiektów
t h is . r ig h t = right; tego typu. W ten sposób wy
1
znaczana jest struktura drze
p ub lic boolean i s L e a f ( ) wa trie. Każdy obiekt Node
{ return l e f t == null &8 r i g h t == n u ll ; } obejmuje też zmienną egzem
plarza freq, używaną przy
p ub lic i n t compareTofNode that)
( return t h i s . f r e q - t h a t.f r e q ; tworzeniu drzewa, i zmienną
egzemplarz ch, wykorzysty
waną w węzłach do reprezen
R e p r e z e n ta c ja d r z e w a tr ie
towania kodowanych znaków.
R ozpakow yw anie za pom ocą kodów bez-
pub lic s t a t i c void expand()
{ prefiksow ych Rozpakowywanie strum ie
Node root = re ad T rie f); nia bitów zakodowanego za pomocą kodu
int N = B in a ryStd ln .re a d ln tf);
bezprefiksowego jest proste, jeśli dostęp
f o r ( i n t i = 0; i < N; i++)
( // Rozpakowywanie i- t e g o słowa kodowego. ne jest drzewo trie wyznaczające ten kod.
Node x = root; W idoczna po lewej stronie m etoda ex
while ( I x . i s L e a f O ) pand () to implementacja tego procesu. Po
i f (B in a ryStdln .readB oole an f))
x = [Link];
wczytaniu drzewa trie ze standardowego
else x = x . l e f t ; wejścia za pomocą opisanej dalej metody
B i n a r y S td O u t .w r ite (x . c h ); readT rie() można wykorzystać to drzewo
} do rozpakowania reszty strum ienia bitów.
B i n a r y S t d O u t . c lo s e f ) ;
} Przebiega to tak — należy zacząć od korze
nia i poruszać się w dół drzewa trie zgod
R o z p a k o w y w a n ie ( o d k o d o w y w a n le ) n a p o d s ta w ie
nie ze strum ieniem bitów (wczytując bit
k o d u b e z p r e f ik s o w e g o
wejściowy i przechodząc w lewo, jeśli ma
wartość 0, lub w prawo, jeżeli jego wartość
to 1). Po napotkaniu liścia należy zapisać znak z danego węzła i wrócić do korzenia.
Po przeanalizowaniu działania tej metody dla krótkiego bezprefiksowego kodu z na
stępnej strony zrozumiesz i docenisz ten proces. Przykładowo, aby odkodować łań
cuch bitów 0 1 1 1 1 1 0 0 1 0 1 1 . . . , należy zacząć od korzenia i przejść w lewo, ponieważ
pierwszy bit to 0, i zapisać A. Następnie trzeba wrócić do korzenia, trzykrotnie przejść
w prawo i zapisać B; potem wrócić do korzenia, przejść dwukrotnie w prawo, potem
w lewo i zapisać Ritd. Prostota procesu rozpakowywania jest powodem popularności
kodów bezprefiksowych, a szczególnie kompresji Huffmana.
5.5 s Kompresja danych 841
p riv a t e s t a t i c S t r i n g [ ] buildCode(Node root)
{ // Tworzenie t a b l i c y wyszukiwania na podstawie drzewa t r i e .
S t r i n g [] st = new S t r i n g [ R ] ;
b uildCode(st, root,
retu rn s t;
}
p riv a t e s t a t i c void b uild C o d e (S tr in g[] s t, Node x, S t r i n g s)
{ // Tworzenie t a b l i c y wyszukiwania na podstawie drzewa t r i e (r e k u r e n c y jn i e ).
i f ([Link] af())
{ s t [ x . c h ] = s; retu rn ; )
build Co de(st, x . l e f t , s + ' 0 ' ) ;
build Co de(st, x . r i g h t , s + ' 1 ' ) ;
}
T w o rz e n ie ta b lic y k o d o w a n ia n a p o d s ta w ie d rz e w a trie d la k o d u b e z p r e f ik s o w e g o
Kompresja za pom ocą kodów bezprefiksowych Tablica słó w ko d o w y c h Reprezentacja w p ostac i drze w a trie
Przy kompresji drzewo trie z definicją kodu wy Klucz Wartość
korzystujemy do utworzenia tablicy kodów, co po 1010
kazano w metodzie bui 1dCode() w górnej części 0
111
strony. Metoda ta jest zwięzła i elegancka, ale nie 1011
co skomplikowana, dlatego zasługuje na staranną 100
analizę. Dla dowolnego drzewa trie tworzy tabli 110
cę, w której z każdym znakiem drzewa trie (znaki
reprezentowane są jako obiekty String składają Kod Huffmana
ce się z cyfr Oi l ) powiązany jest łańcuch bitów.
Tablica kodów jest tablicą łączącą z każdym znakiem obiekt S tri ng. Liczba znaków
nie jest tu duża, dlatego z uwagi na wydajność stosujemy indeksowaną znakami tablicę
s t [] zamiast ogólnej tablicy symboli. W celu utworzenia tablicy metoda bui 1dCode ()
rekurencyjnie przechodzi po drzewie, przechowuje łańcuch binarny odpowiadający
ścieżce z korzenia do każdego węzła (lewe odnośniki oznaczają 0, a prawe — 1 ) i za
pisuje słowo kodowe odpowiadające każdemu znakowi po znalezieniu znaku w liściu.
Po zbudowaniu tablicy kodów przeprowadzenie kompresji jest proste — wystarczy
znaleźć kod dla każdego znaku z danych wejściowych. Aby zastosować przedstawio
ne po prawej kodowanie do skompresowania łańcucha ABRACADABRA!, należy zapisać
0 (słowo kodowe powiązane z A), następnie 111
(słowo kodowe powiązane z B), potem 110 (sło f o r ( i n t i = 0; i < in p u [Link] n gt h ; i++)
wo kodowe powiązane z R) itd. Zadanie to wy {
konuje fragment kodu przedstawiony po prawej. S t r i n g code = st [i nput [i ] ] ;
f o r ( i n t j = 0; j < c o d e . le n g t h ( ) ; j++)
Należy znaleźć obiekt String powiązany z każ
i f ([Link] arA t( j) == ' 1 ' )
dym znakiem z danych wejściowych, przekształ B i n a r y S t d O u t .w r i t e (t ru e );
cić go na wartości 0 i 1 w tablicy elementów typu e l s e B in a r y S t d O u t . w r i t e ( f a l s e ) ;
char oraz zapisać uzyskany łańcuch bitów w da 1
nych wyjściowych. Kompresowanie za pomocą tablicy kodów
842 ROZDZIAŁ 5 o Łańcuchy znaków
Tworzenie drzew a trie W czasie lektury opisu procesu przydatny będzie rysunek
z następnej strony, na którym pokazano proces tworzenia drzewa trie Huffmana dla
poniższych danych wejściowych:
i t was t h e b e s t o f t i m e s i t was t h e w o r s t o f t i m e s
Kodowane znaki znajdują się w liściach. Ponadto w każdym węźle przechowywana jest
zmienna egzemplarza f req, reprezentująca liczbę wystąpień wszystkich znaków w pod-
drzewie, którego korzeniem jest dany węzeł. Pierwszy krok polega na utworzeniu lasu
drzewa o jednym węźle (liści), po jednym drzewie na każdy znak ze strumienia wejścio
wego. Do każdego drzewa przypisana jest wartość f req równa liczbie wystąpień znaku
w danych wejściowych. W przykładzie dane wejściowe obejmują 8 liter t, 5 liter s, 11
odstępów itd. Ważna uwaga — aby określić liczbę wystąpień, trzeba wczytać cały stru
mień wejściowy; kodowanie Huffmana to algorytm dwuprzebiegowy, ponieważ wyma
ga wczytania strumienia wejściowego drugi raz w celu skompresowania go. Następnie
tworzymy od dołu do góry (według liczb wystąpień znaków) drzewo trie potrzebne do
kodowania. Przy tworzeniu drzewa trie traktujemy je jak binarne drzewo trie z liczba
mi wystąpień zapisanymi w węzłach. Po zakończeniu tego procesu postrzegamy je jak
drzewo trie używane do kodowania w opisany wcześniej sposób. Proces ten przebiega
w następujący sposób — należy znaleźć dwa węzły o najmniejszej liczbie wystąpień,
a następnie utworzyć nowy węzeł, którego dwa wspomniane węzły są dziećmi (i w któ
rym liczba wystąpień jest równa sumie tych liczb w dzieciach). Operacja ta zmniejsza
liczbę drzew trie w lesie o jedno. Następnie powtarzamy proces — trzeba znaleźć dwa
węzły o najmniejszej liczbie wystąpień i w opisany wcześniej sposób utworzyć nowy
węzeł. Proces ten można zaimplementować w prosty sposób za pomocą kolejki prio
rytetowej, tak jak w metodzie bui 1dTri e() poniżej. Dla przejrzystości drzewa trie na
rysunku przedstawiono w posortowanej kolejności. Na dalszych etapach procesu po
wstają coraz większe drzewa trie, a jednocześnie w każdym kroku liczba drzew trie
w lesie zmniejsza się o jeden (dwa drzewa są usuwane, a jedno — dodawane).
p riv a t e s t a t i c Node b u i l d T r i e ( i n t [ ] freq)
{
// Inicjowanie k ole j k i priorytetow ej za pomocą drzew jednowęzTowych.
MinPQ<Node> pq = new MinPQ<Node>();
f o r (char c = 0; c < R; C++)
i f ( f r e q [c] > 0)
[Link] (n ew Node(c, f r e q [ c ] , n u l l , n u l i ) ) ;
while ( p q . siz e ( ) > 1)
{ // Sc ala n ie dwóch najmniejszych drzew.
Node x = p q . d e lM i n ( ) ;
Node y = [Link] Mi n ();
Node parent = new N o d e ( ' \ 0 ', x . fr e q + y .f r e q , x, y ) ;
p [Link](parent);
1
return p q . d e lM i n ( ) ;
1
Tworzenie drzewa trie na potrzeby kodowania Huffmana
5.5 n Kompresja danych 843
Z dolnego poziom u
lewej kolum ny
1 \ 2 2 2 2 2 3 3 4 5 6 8 U
Diva drzewa trie
o najmniejszych
w agach■
N o w y rodzic dla
tych dw óch drzew
D o górn ego poziom u
prawej kolum ny
Tworzenie drzewa trie na potrzeby kodowania Huffmana
844 ROZDZIAŁ 5 ■ Łańcuchy znaków
R eprezentacja w postaci drzew a trie Tablica słów kodow ych
Klucz Wartość
LF 101010
SP 01
a 11011
b 101011
e 000
f 11000
h 11001
i 1011
m 11010
o 0011
r 10100
s 100
t 111
w 0010
z korzen ia to 1 1 0 1 0 , d la te go
11010 to k o d d la „m”
Kod H u ffm an a d la s tru m ie n ia z n a k ó w , , i t w as t h e b e s t o f tim e s i t w as t h e w o r s t o f tim e s LF”
Ostatecznie wszystkie węzły są łączone w jedno drzewo trie. Liście w tym drzewie
obejmują kodowane znaki i liczbę wystąpień znaków w danych wejściowych. Każdy
węzeł, który nie jest liściem, obejmuje sumę liczb wystąpień z dwójki dzieci. Węzły
o małej liczbie wystąpień są mocno zagłębione w drzewie trie, a węzły o dużej licz
bie wystąpień znajdują się blisko korzenia. Liczba wystąpień w korzeniu jest równa
liczbie znaków w danych wejściowych. Ponieważ utworzono binarne drzewo trie,
w którym znaki występują tylko w liściach, drzewo to wyznacza bezprefiksowy kod
dla użytych znaków. Po zastosowaniu tablicy słów kodowych utworzonej za pomocą
m etody bui 1dCode () w tym przykładzie (tablicę te pokazano w prawej części rysunku
na początku strony) otrzymujemy wyjściowy łańcuch bitów:
10111110100101101110001111110010000110101100 -
0 1001110100111100001111101111010000100011011 -
11101001011011100011111100100001001000111010 -
01001110100111100001111101111010000100101010
Łańcuch obejmuje 176 bitów, co daje oszczędność na poziomie 57% w porównaniu
z 408 bitami potrzebnymi do zakodowania 51 znaków w standardowym, 8-bitowym
kodowaniu ASCII (nie uwzględniamy tu kosztów kodu, czym zajmujemy się dalej).
Ponadto, ponieważ jest to kod Huffmana, żaden inny kod bezprefiksowy nie pozwala
zakodować danych wejściowych za pom ocą mniejszej liczby bitów.
O ptym alność Często występujące znaki występują bliżej korzenia drzewa niż rza
dziej pojawiające się symbole, dlatego są kodowane za pom ocą mniejszej liczby bi
tów. Kod jest więc dobry, ale czy jest to optymalny kod bezprefiksowy? Aby odpowie
dzieć na to pytanie, zaczynamy od zdefiniowania ważonej długości ścieżki zewnętrznej
drzewa. Długość ta jest równa sumie iloczynów wag (liczb wystąpień) i głębokości
(zobacz stronę 238) dla wszystkich liści.
5.5 n Kompresja danych 845
Twierdzenie T. Dla dowolnego kodu bezprefiksowego długość zakodowanego
łańcucha bitów jest równa ważonej długości ścieżki zewnętrznej drzewa trie.
Dowód. Głębokość każdego liścia to liczba bitów potrzebnych do zakodowania
znaku z liścia. Tak więc ważona długość ścieżki zewnętrznej to długość zakodo
wanego łańcucha bitów — odpowiada sumie iloczynów liczb wystąpień i liczb
bitów na wystąpienie dla wszystkich liter.
W przykładzie jest jeden liść o odległości 2 (SP o liczbie wystąpień 11), trzy liście o od
ległości 3 (e, s i t o łącznej liczbie wystąpień 19), trzy liście o odległości 4 (w, o oraz
i o łącznej liczbie wystąpień 10), pięć liści o odległości 5 (r, f, h, mi a o łącznej liczbie
wystąpień 9) i dwa liście o odległości 6 (LF i b o łącznej liczbie wystąpień 2). Suma
wynosi więc 2x11 + 3x19 + 4x10 + 5x9 + 6x2 = 176. Jest to, zgodnie z oczekiwania
mi, długość wyjściowego łańcucha bitów.
Twierdzenie U. Dla zbioru r symboli i liczb wystąpień algorytm Huffmana
tworzy optymalny kod bezprefiksowy.
Dowód. Oparty jest na indukcji od r. Załóżmy, że kod Huffmana jest optymal
ny dla dowolnego zbioru o mniej niż r symbolach. Niech T’;f będzie kodem ob
liczonym m etodą Huffmana dla zbioru symboli i powiązanych liczb wystąpień
(Sj, r ), ..., (sr, f r). Oznaczmy długość kodu (ważoną długość ścieżki zewnętrznej
drzewa trie) przez W{TH). Przyjmijmy, że (s.,f ') i (s., /)) to dwa pierwsze wybrane
symbole. Algorytm oblicza kod TH* dla zbioru n -l symboli, gdzie {s?f ) i isy f )
zastąpiono przez +_/p, gdzie s* to nowy symbol w liściu na pewnej głęboko
ści d. Zauważmy, że:
W (TH) = w(Tn*) - d(fi + f) + (d + 1)(/; +f) = W (T/) + (/; +f)
Teraz rozważmy optymalne drzewo trie T dla (s , r j , ..., (sr, / r). Wysokość drzewa
wynosi h. Zauważmy, że głębokość (s., _/p i (s., f.) musi wynosić h (w przeciw
nym razie można utworzyć drzewo trie o mniejszej długości ścieżki zewnętrz
nej, przestawiając te węzły z węzłami na głębokości h). Ponadto przyjmijmy, że
(s., _/j) i to bracia — wymaga to przestawienia (s., _/)) z bratem węzła (s;,_/j).
Teraz rozważmy drzewo T* uzyskane przez zastąpienie rodzica węzłów węzłem
Zauważmy, że — zgodnie z przedstawionym wcześniej wnioskowaniem
- W ( T ) = W(T*) + (fi +f]l
Według hipotezy indukcyjnej TH* jest optymalne — W {TH*) < W (T'*).
Dlatego:
W (Th) = W ( T /) + (fi +f]) < W ( D + (f. + f) = W (D
Ponieważ T jest optymalne, równość musi być spełniona, tak więc THjest opty
malne.
846 ROZDZIAŁ 5 b Łańcuchy znaków
Kiedy trzeba wybrać węzeł, może się zdarzyć, że kilka z nich ma tę samą wagę.
Metoda Huffmana nie określa, jak dokonać wyboru w takiej sytuacji. Nie określa też
lewej i prawej pozycji dzieci. Różne wybory prowadzą do różnych kodów Huffmana,
jednak wszystkie takie kody powodują zakodowanie kom unikatu za pom ocą kodu
bezprefiksowego o optymal
nej liczbie bitów.
Zapis i odczyt drzew a trie
Jak podkreśliliśmy, podane
wcześniej oszczędności nie są
w pełni dokładne, ponieważ
skompresowanego strum ie
nia bitów nie można odkodo-
Liście wać bez drzewa trie. Dlatego
i oprócz kosztów zapisu sa
0101 0 0 0 0 0 1 0 0 1 0 1 0 0 0 1 0 001000010101010000110101010010101000010
mego łańcucha bitów trze
t t
Węz/y wewnętrzne ba uwzględnić koszt zapisu
Przechodzenie w porządku preorder w celu w skompresowanych danych
zakodowania drzewa trie jako strumienia bitów
wyjściowych także drzewa
trie. Jeśli dane wejściowe są
długie, koszt ten jest stosunkowo niski, jednak pełny system kompresji danych wy
maga tu zapisu drzewa trie w strum ieniu bitów na etapie kompresowania i odczytu
drzewa w czasie rozpakowywania. Jak zakodować drzewo trie w strumieniu bitów,
a następnie je rozpakować? Co zaskakujące, oba zadania można wykonać za pomocą
prostych procedur rekurencyjnych, opartych na przechodzeniu w porządku preorder
przez drzewo trie. Przedstawiona poniżej procedura w riteT rie() przechodzi przez
drzewo trie w takim właśnie porządku. Po dotarciu do węzła wewnętrznego zapisuje
jeden bit 0. Po dojściu do liścia zapisuje bit 1, po którym następuje 8 -bitowy kod ASCII
znaku z danego liścia. Powyżej pokazano łańcuch bitów z zakodowanym drzewem trie
Huffmana dla przykładowego łańcucha ABRACADABRA!. Pierwszy bit to 0 (odpowiada
on korzeniowi). Ponieważ potem metoda natrafia na liść z literą A, następny bit to 1, po
czym następuje 8-bitowy kod ASCII dla litery A — 0100001. Dwa dalsze bity to 0, po
nieważ metoda napotyka
dwa węzły wewnętrzne p r i v a t e s t a t i c voi d w r i t e T r i e ( N o d e x)
itd. Powiązana metoda { / / Za pi s drzewa t r i e zakodowanego j a k o ł a ńc uc h bi t ów,
i f ([Link]))
readTri e () ze strony 847
(
odtwarza drzewo trie [Link](true);
na podstawie łańcucha [Link]([Link]);
return;
bitów. Metoda wczytuje
1
jeden bit, aby ustalić ro [Link] e ( f a l s e ) ;
dzaj następnego węzła. w riteTrie([Link]);
[Link]);
Jeśli jest to liść (bit to 1),
1
metoda wczytuje kolejny
Zapis drzewa trie jako łańcucha bitów
5.5 Q Kompresja danych 847
p r i v a t e s t a t i c Node r e a d T r i e ( )
{
i f ([Link]))
r e t u r n new N o d e ( B i n a r y S t d I n . r e a d C h a r ( ) , 0, n u l l , n u l l ) ;
r e t u r n new N o d e ( ' \ 0 ' , 0, r e a d T r i e ( ) , r e a d T r i e f ) ) ;
}
Odtwarzanie drzewa na podstawie reprezentacji łańcucha bitów
w porządku preorder
znak i tworzy liść. Jeżeli jest to węzeł wewnętrzny (bit to 0), metoda tworzy węzeł we
wnętrzny, a następnie rekurencyjnie tworzy jego lewe i prawe poddrzewo. Upewnij się,
że rozumiesz te metody — ich prostota może być myląca.
Im p le m e n ta c ja ko m p resji H u ffm a n a Wraz z opisanymi wcześniej metodam i
b u i l d C o d e ( ) , b u i l d T r i e ( ) , r e a d T r i e ( ) i w r i t e T r i e ( ) (oraz przedstawioną na po
czątku m etodą e x p a n d Q ) a l g o r y t m 5.10 jest kompletną implementacją kom pre
sji Huffmana. Rozwińmy omówienie, które przedstawiliśmy kilka stron wcześniej
— strum ień bitów można traktować jak strum ień 8-bitowych wartości typu c h a r
i kompresować go w następujący sposób:
■ Wczytać dane wejściowe.
■ Zapisać w tablicy liczbę wystąpień każdej wartości typu char z danych wejścio
wych.
■ Utworzyć na potrzeby kodowania drzewo trie Huffmana odpowiadające licz
bom wystąpień.
■ Utworzyć odpowiednią tablicę słów kodowych, aby powiązać łańcuch bitów
z każdą wartością typu char z danych wejściowych.
■ Zapisać drzewo trie zakodowane jako łańcuch bitów.
■ Zapisać liczbę znaków w danych wyjściowych zakodowaną jako łańcuch bitów.
n Wykorzystać tablicę słów kodowych do zapisu słowa kodowego dla każdego
znaku wejściowego.
W celu rozpakowania strum ienia bitów zakodowanego w ten sposób należy:
■ Wczytać drzewo trie (zakodowane na początku strum ienia bitów).
■ Wczytać liczbę znaków do odkodowania.
n Wykorzystać drzewo trie do odkodowania strum ienia bitów.
Kompresja Huffmana wymaga czterech rekurencyjnych m etod przetwarzania drzew
trie i siedmioetapowego procesu kompresji. Jest tym samym jednym z najbardziej
złożonych algorytmów omawianych w książce, ale też jednym z najczęściej stosowa
nych (z uwagi na jego skuteczność).
848 ROZDZIAŁ 5 Łańcuchy znaków
ALGORYTM 5.10. Kompresja Huffmana
p u b l i c c l a s s Huffman
{
p r i v a t e s t a t i c i n t R = 256; // A l f a b e t A S C I I .
// Kod wewnętrznej k l a s y Node z n a j d z i e s z na s t r o n i e 840.
// Metody pom ocnicze i metodę e x p a n d () p r z e d s t a w i o n o w t e k ś c i e .
p u b l i c s t a t i c v o i d c o m p r e s s ()
{
// O d czy t danych w e jś c io w y c h .
S t r in g s = B in a r y S t d ln .r e a d S t r in g ();
ch a r[] in pu t = s . t o C h a r A r r a y ( ) ;
// T w o r z e n ie t a b l i c y l i c z b w y s t ą p i e ń ,
i nt □ f r e q = new i n t [ R ] ;
fo r ( in t i = 0; i < in p u [Link] n g th ; i++ )
fre q [in p u t[i]]+ + ;
// T w o r z e n ie drzewa t r i e d l a kodowania Huffmana.
Node r o o t = b u i l d T r i e ( f r e q ) ;
// T w o r z e n ie t a b l i c y kodów ( r e k u r e n c y j n i e ) .
S t r i n g [] s t = new S t r i n g [ R ] ;
b u ild C o d e (st, ro ot, " " ) ;
// Z a p i s drzewa t r i e na p o t r z e b y odkodowywania ( r e k u r e n c y j n i e ) .
w rite T rie (ro o t);
// Z a p i s l i c z b y znaków.
B i n a r y S t d O u t . w r i t e ( i n p u t. l e n g t h ) ;
// W y k o r z y s t a n i e kodu Huffman do za k od ow a n ia danych w e jś c io w y c h ,
f o r ( i n t i = 0; i < i n p u t . l e n g t h ; i+ + )
{
S t r i n g code = s t [ i nput [ i ] ] ;
f o r ( i n t j = 0; j < c o d e . l e n g t h ( ) ; j + + )
i f ( c o d e . c h a r A t ( j ) == ' 1 ' )
B in a ry Std O u t.w rite (tru e );
e lse B in a r y S td O u t.w rite (fa lse );
}
B in a ry S td 0 u t.c lo se ();
Ta implementacja kodowania Huffmana tworzy drzewo trie na potrzeby kodowania.
Używane są przy tym różne metody pomocnicze zaprezentowane i wyjaśnione na kilku
wcześniejszych stronach tekstu.
5.5 B Kompresja danych 849
Przypadek testowy (96 bitów)
% more a b r a . t x t
abracadabra !
% j a v a Huffman - < a b r a . t x t | j a v a BinaryDump 60
010100000100101000100010000101010100001101010100101010000100
000000000000000000000000000110001111100101101000111110010100
L ic z b a b itó w : 1 2 0 ->------ W spółczynnik kompresji w ynosi 120/96 = 1 2 5 % z uw agi
n a 59 bitów na drzewo trie i 32 bity na liczbę znaków
Przykład z tekstu (408 znaków)
% more t i n y t i n y T a l e . t x t
i t was t h e b e s t o f t i m e s i t was t h e w o r s t o f t i m e s
% j a v a Huffman - < t i n y t i n y T a l e . t x t [ j a v a BinaryDump 64
0001011001010101110111101101111100100000001011100110010111001001
0000101010110001010110100100010110011010110100001011011011011000
0110111010000000000000000000000000000110011101111101001011011100
0111111001000011010110001001110100111100001111101111010000100011
0111110100101101110001111110010000100100011101001001110100111100
00111110111101000010010101000000
L ic z b a b itó w : 352 -*------ W spółczynnik kompresji wynosi 352/408 = 8 6 % i to m im o
137 bitów na drzewo trie oraz 32 bitów na liczbę znaków
% j a v a Huffman - < t i n y t i n y T a l e . t x t
| j a v a Huffman +
i t was t h e b e s t o f t i m e s i t was t h e w o r s t o f t i m e s
Pierwszy rozdział książki Tale of Two Cities
% i a v a PictureDump 512 90 < m e d T a l e . t x t
L ic z b a b itó w : 45056
% j a v a Huffman - < m e d T a i e . t x t | j a v a PictureDump 512 47
L ic z b a b it ó w : 23912 -* W spółczynnik kompresji w ynosi 23912/45056 = 5 3 %
Cały tekst książki Tale of Two Cities
% j a v a BinaryDump 0 < t a l e . t x t
L ic z b a b it ó w : 5812552
% j a v a Huffman - < t a l e . t x t > t a l e . t x t . h u f
% j a v a BinaryDump 0 < t a l e . t x t . h u f
L ic z b a b it ó w : 3043928 •*------ W spółczynnik kompresji w ynosi 3043928/5812552 = 52%
Kompresowanie i rozpakowywanie strumieni bajtów za pomocą kodowania Huffmana
850 ROZDZIAŁ 5 □ Łańcuchy znaków
j e d n ą z p r z y c z y n p o p u l a r n o ś c i kompresji Huffmana jest jej skuteczność dla róż
nych typów plików, a nie tylko dla tekstów w języku naturalnym. Starannie napisali
śmy kod metody, tak aby działała prawidłowo dla dowolnej 8-bitowej wartości w każ
dym 8-bitowym znaku. Oznacza to, że można ją zastosować do dowolnego strum ie
nia bajtów. Na rysunku w dolnej części strony pokazano kilka przykładów dotyczą
cych typów plików wspomnianych we wcześniejszej części podrozdziału. Widać tu,
że kompresja Huffmana jest konkurencyjna względem kodowania za pom ocą kodów
o stałej długości i kodowania długości serii, choć metody te zaprojektowano w taki
sposób, aby działały dobrze dla określonych typów plików. Warto zrozumieć powody
dobrego działania kodowania Huffmana w przykładowych obszarach. W przypadku
danych o genomie kompresja Huffmana „odkrywa” kod 2-bitowy, ponieważ cztery li
tery występują tu z mniej więcej równą częstotliwością, dlatego drzewo trie dla kodo
wania Huffmana jest zbalansowane, a każdemu znakowi przypisywany jest 2-bitowy
kod. Jeśli chodzi o kodowanie długości serii, 00000000 i 1 1 1 1 1 1 1 1 to prawdopodobnie
najczęściej występujące znaki, dlatego zostaną zakodowane za pom ocą dwóch lub
trzech bitów, co prowadzi do znacznej kompresji.
Wirus (50000 bitów)
% j a v a Genome - < g e n o m e v i r u s . t x t | j a v a PictureDump 512 25
L iczb a b itów : 12556
% j a v a Huffman - < g e n o m e v i r u s . t x t | j a v a PictureDump 512 25
«a*,™«: --- ----
L iczba b itó w : 12576 - ------ W kompresji Huffmana potrzeba tylko 40 bitów więcej
niż w wyspecjalizowanym kodzie 2-bitowym
Bitmapa (1536 bitów)
% j a v a RunLength - < [Link] | j a v a BinaryDump 0
L iczba bitów : 1144
% j a v a Huffman - < [Link] | j a v a BinaryDump 0
L iczb a b itó w : 8 1 6 - Kompresja Huffmana w ym aga o 2 9 % bitów mniej
niż wyspecjalizowana metoda
Bitmapa o większej rozdzielczości
% j a v a RunLength - < q 6 [Link] | j a v a BinaryDump 0
L iczba bitów : 2296
% j a v a Huffman - < q 6 [Link] [ j a v a BinaryDump 0
L iczb a b itów : 2032 Przy większej rozdzielczości różnica zmniejsza się do 11%
Compresowanie danych o genom ie i bitm ap za pom ocą kodowania Huffmana oraz wyspecjalizowanych metod
5.5 □ Kompresja danych 851
Pod koniec lat 70. i na początku lat 80. wymyślono zaskakującą alternatywę do
kompresji Huffmana. A. Lempel, J. Ziv i T. Welch opracowali jedną z najczęściej sto
sowanych m etod kompresji. Jest ona łatwa w implementacji i działa dobrze dla pli
ków różnego typu.
Podstawowy plan jest uzupełnieniem pomysłu z kodowania Huffmana. Zamiast
przechowywać tablicę słów kodowych o zmiennej długości dla wzorców o stałej dłu
gości z danych wejściowych, można przechowywać tablicę słów kodowych o stałej
długości dla wzorców o zmiennej długości. Zaskakującą dodatkową cechą tej metody
jest to, że — inaczej niż przy kodowaniu Huffmana — nie trzeba kodować tablicy.
Kompresja L Z W Aby pomóc zrozumieć pomysł, omawiamy przykład kompresji,
w którym dane wejściowe to 7-bitowe znaki ASCII, a dane wyjściowe to strum ień
8 -bitowych bajtów. W praktyce zwykle stosujemy większe wartości tych parametrów
— w opracowanych przez nas implementacjach używamy 8-bitowych danych wej
ściowych i 12-bitowych danych wyjściowych. Bajty wejściowe określamy jako znaki,
ciągi bajtów wejściowych — jako łańcuchy znaków, a bajty wyjściowe — jako sło
wa kodowe, choć w innych kontekstach pojęcia te mają nieco odm ienne znaczenie.
Algorytm kompresji LZW jest oparty na przechowywaniu tablicy symboli, która łą
czy klucze w postaci łańcuchów znaków z wartościami słów kodowych (o stałej dłu
gości). Tablicę symboli należy zainicjować za pom ocą 128 możliwych kluczy w po
staci pojedynczych znaków. Następnie trzeba powiązać klucze z 8-bitowymi słowami
kodowymi uzyskanymi przez dołączenie 0 do 7-bitowej wartości definiującej każdy
znak. Z uwagi na zwięzłość i przejrzystość stosujemy dla wartości słów kodowych za
pis szesnastkowy — 41 to słowo kodowe dla A w kodzie ASCII, 52 odpowiada literze
R itd. Słowo kodowe 80 jest zarezerwowane i oznacza koniec pliku. Pozostałe warto
ści słów kodowych (od 81 do FF) przypisujemy różnym napotkanym podłańcuchom
z danych wyjściowych. Zaczynamy od 81 i zwiększamy wartość dla każdego nowego
dodanego klucza. Przy kompresowaniu, dopóki występują niepobrane znaki w da
nych wyjściowych, wykonujemy następujące kroki.
D Znajdow anie w tablicy sym boli najdłuższego łańcucha znaków s, który jest
przedrostkiem niezakodow anego jeszcze fragm entu danych wejściowych.
n Zapisywanie 8-bitowej wartości (słowa kodowego) powiązanej z s.
■ Pobieranie jednego znaku po s z danych wejściowych.
n Wiązanie w tablicy symboli następnej wartości słowa kodowego z s + c (c do
łączonego do s), gdzie c to następny znak w danych wejściowych.
W ostatnim kroku należy przejść naprzód, aby sprawdzić następny znak z danych wej
ściowych w celu utworzenia kolejnego elementu słownika. Tak więc znak c to znak
następny (ang. lookahead). Na razie załóżmy, że po wyczerpaniu się wartości słów ko
dowych (po przypisaniu wartości FF do jednego z łańcuchów znaków) kończymy do
dawanie elementów do tablicy symboli. Dalej omawiamy inne rozwiązania.
852 ROZDZIAŁ 5 h Łańcuchy znaków
Przykładow a kompresja L Z W Na rysunku poniżej przedstawiono szczegółowo
przebieg kompresji LZW dla przykładowych danych wejściowych — ABRACADABRABRABRA.
Dla pierwszych siedmiu znaków najdłuższy pasujący przedrostek obejmuje tylko je
den znak, dlatego należy zwrócić słowo kodowe powiązane z tym znakiem i powiązać
słowa kodowe od 81 do 87 z dwuznakowymi łańcuchami. Dalej program znajduje
przedrostek pasujący do AB (dlatego zwraca 81 i dodaje ABR do tablicy), RA (program
zwraca 83 i dodaje RAC do tablicy), BR (zwrócenie 82 i dodanie BRA do tablicy) oraz ABR
(zwrócenie 88 i dodanie ABRA do tablicy), po czym pozostaje ostatnia litera A (należy
zwrócić jej słowo kodowe — 41).
Dane
A B R A C A D A B R A B R A B R A Koniec
wejściowe
pliku
A B R A c A D A B R A B R A B R
Dane A I
wyjściowe
41 42 52 41 43 41 44 81 83 82 88 41 80
Tablica słów kodowych
Klucz Wartość
AB 8 1 AB AB AB AB AB AB A [3 AB AB AB AB 81
f BR 82 BR BR 13 R BR BR 13 R BR BR BR BR 82
RA RA RA RA RA RA RA RA RA 83
Wejściowy
A C 84 AC AC AC AC AC AC AC AC 84
podłańcuch
CA 85 CA CA CA CA CA CA CA 85
Słowo kodowe / AD 86 AD AD AD AD AD AD 86
w metodzie LZW Znak DA 87 DA DA DA DA DA 87
następny ABR 88 ABR ABR ABR ABR 88
RAB 89 RAB RAB RAB 89
B RA 8A BRA B RA 8A
ABRA 8B ABRA 8B
Kom presja LZW dla łańcucha ABRACADABRABRABRA
Dane wejściowe to 17 znaków ASCII po 7 bitów każdy, co w sumie daje 119 bi
tów. Dane wyjściowe to 12 słów kodowych po 8 bitów każdy — łącznie 96 bitów.
Współczynnik kompresji wynosi 82% nawet w tym krótkim przykładzie.
Reprezentacja kompresji L Z W za pom ocą drzew a trie Kompresja LZW oparta
jest na dwóch operacjach na tablicy symboli:
B znajdowaniu pasującego najdłuższego przedrostka dla danych wejściowych za
pom ocą klucza z tablicy symboli;
■ dodawaniu elementu łączącego na
stępne słowo kodowe z kluczem
utworzonym przez dołączenie znaku
następnego do danego klucza.
S truktury danych dla drzew trie p rzedsta
w ione W PO D R O Z D Z IA LE 5-2 Są doStOSO-
w ane do tych operacji. D rzew o trie rep re
zentujące om aw iany przykład pokazano
po prawej stronie. Aby znaleźć najdłuższy
pasujący przedrostek, należy przejść po
drzew ie trie, począw szy od korzenia, i d o
pasow ać etykiety węzłów do wejściowych Drzewo trfe reprezentujące tablicę kodów LZW
5.5 □ Kompresja danych 853
znaków. W celu dodania nowego słowa kodowego nowy węzeł opisany kolejnym
słowem kodowym i znakiem następnym trzeba połączyć z węzłem, w którym zakoń
czono wyszukiwanie. W praktyce z uwagi na oszczędność pamięci stosujemy drzewa
TST, opisane w p o d r o z d z i a l e 5 .2 . Warto zwrócić uwagę na różnicę w porównaniu
z drzewami trie dla kodowania Huffmana, gdzie drzewa trie są przydatne, ponie
waż żaden przedrostek słowa kodowego sam nie jest słowem kodowym. W metodzie
LZW drzewa trie są użyteczne, ponieważ każdy przedrostek klucza dla wejściowego
podłańcucha sam też jest kluczem.
R ozpakow yw anie w m etodzie L Z W Dane wejściowe przy rozpakowywaniu w m e
todzie LZW są w omawianym przykładzie ciągiem 8-bitowych słów kodowych. Dane
wyjściowe to łańcuch 7-bitowych znaków ASCII. Aby zaimplementować rozpako
wywanie, należy utworzyć tablicę symboli, w której łańcuchy znaków są powiązane
z wartościami słowa kodowego (jest to odwrotność tablicy używanej przy kom preso
waniu). Trzeba zapełnić elementy tablicy od 00 do 7F jednoznakowymi łańcuchami,
po jednym dla każdego znaku ASCII, ustawić pierwszą nieprzypisaną wartość słowa
kodowego na 81 (wartość 80 oznacza koniec pliku), ustawić wartość bieżącego łań
cucha znaków, v al, na jednoznakowy łańcuch obejmujący pierwszy znak, a następ
nie wykonywać poniższe kroki do m om entu wczytania słowa kodowego 80 (koniec
pliku):
H Zapisać bieżący łańcuch znaków, v al.
* Wczytać słowo kodowe x z danych wejściowych.
■ Ustawić s na wartość powiązaną z x w tablicy symboli.
a Powiązać w tablicy symboli następną nieprzypisaną wartość słowa kodowego
z val + c, gdzie c to pierwszy znak z s.
■ Ustawić wartość bieżącego łańcucha znaków, v al, na s.
Proces ten jest bardziej skomplikowany niż kompresowanie. Wynika to ze znaku na
stępnego. Trzeba wczytać kolejne słowo kodowe, aby pobrać pierwszy znak z powią
zanego z nim łańcucha, co powoduje desynchronizację procesu o jeden krok. Dla
pierwszych siedmiu słów kodowych metoda tylko sprawdza i zapisuje odpowiedni
znak, a następnie idzie naprzód o jeden znak i dodaje dwuznakowy element do tablicy
Dane wejściowe 41 42 52 41 43 41 44 81 83 82 88 41 80
D a n e wyjściowe A B R A C A D A B R A B R A B R A
O d w ró c o n a tablica
s ł ó w k o d ow yc h
Klucz Wartość
81 AB AB AB AB AB AB AB AB AB AB AB 81 AB
82 B R BR BR BR BR BR BR BR BR BR 82 BR
83 R A RA RA RA RA RA RA RA RA 83 RA
84 A C AC AC AC AC AC AC AC 84 AC
85 C A CA CA CA CA CA CA 85 CA
86 A D AD AD AD AD AD 86 AD
87 D A DA DA DA DA 87 DA
.88 A B R AB R AB R AB R 88 ABR
Słow o kodow e ^
z m etody L Z W 89 R A B RAB RA B 89 RAB
/
Wejściowy 8A B R A B RA 8A BRA
podlańcuch 8B ABRA 8B ABRA
Rozpakowywanie w metodzie LZW dla kodów 41 42 52 41 43 41 44 81 83 82 88 41 80
854 RO Z D Z IAŁ 5 Łańcuchy znaków
ALGORYTM 5.11. Kompresja LZW
p u b lic c la s s LZW
{
p rivate s t a t ic final i n t R = 256; // L i c z b a znaków w e jś c io w y c h ,
p riva te s t a t ic final i n t L = 409 6 ; // L i c z b a stów kodowych = 2^12.
p rivate s t a t ic final i n t W = 12; // S z e r o k o ś ć stó wa kodowego.
p u b l i c s t a t i c v o i d c o m p r e s s ()
{
S t r in g input = B in a r y S t d ln . r e a d S t r in g Q ;
T S T < I n t e g e r > s t = new T S T < I n t e g e r > ( ) ;
f o r ( i n t i = 0; i < R; i + + )
s t . p u t ( " " + (char) i , i ) ;
i n t code = R + l ; // R t o sło w o kodowe o z n a c z a j ą c e k o n i e c p l i k u .
w h ile ( in p u t . le n g th () > 0)
(
Strin g s = st. l o n g e s t P r e f i x O f ( i n p u t ) ; // Z najdow anie n a j d ł u ż s z e g o
// p a s u ją c e g o p r z e d r o s t k a .
B i n a r y S t d O u t . w r i t e ( s t . g e t ( s ) , W); // W y ś w i e t la n i e kodu d la s.
in t t = s . le n g t h ( ) ;
i f (t < i n p u t . l e n g t h ( ) && code < L) // Dodawanie s do t a b l i c y
// symbol i .
s t . p u t ( i n p u t . s u b s t r i n g ( 0 , t + 1 ), c o d e + + ) ;
in p u t = i n p u t . s u b s t r i n g ( t ) ; // P rze ch o d ze n ie za s w danych
// w e jś c io w y c h .
i
B i n a r y S t d O u t . w r i t e ( R , W); // Z a p i s końca p l i k u .
B in a ry Std O u t.c lo se ();
}
p u b l i c s t a t i c v o i d e x p a n d ()
// Zobacz s t r o n ę 856.
)
W tej implementacji kompresji danych Lempela-Ziva-Welcha wykorzystano 8-bitowe bajty
wejściowe i 12-bitowe słowa kodowe. Rozwiązanie to jest odpowiednie dla dowolnie dużych
plików. Słowa kodowe dla krótkiego przykładu są podobne do tych opisanych w tekście — są to
jednoznakowe słowa kodowe poprzedzone 0; inne słowa kodowe rozpoczynają się od 100 .
% more abr aLZW. t xt
ABRACADABRABRABRA
% j a v a LZW - < abr aLZW. t xt | j a v a HexDump 20
04 10 42 05 20 41 04 30 41 04 41 01 10 31 02 10 80 41 10 00
Liczba bi t ów: 150
5.5 0 Kompresja danych 855
symboli, tak jak wcześniej. Następnie wczytuje 81 (dlatego zapisuje AB i dodaje ABR do
tablicy), 83 (dlatego zapisuje RAi dodaje RAB do tablicy), 82 (dlatego zapisuje BR i dodaje
BRAdo tablicy) i 88 (co powoduje zapisanie ABR i dodanie ABRA do tablicy). Pozostaje 41.
Ostatecznie metoda dochodzi do znaku końca pliku, 80, dlatego zapisuje A. Na końcu
procesu zapisane są, zgodnie z oczekiwaniami, pierwotne dane wejściowe. Program
buduje też tę samą tablicę kodów, co przy kompresowaniu, jednak role kluczy i warto
ści są tu odwrócone. Zauważmy, że dla tablicy można zastosować prostą reprezentację
w postaci tablicy łańcuchów znaków indeksowanej słowami kodowymi.
Skom plikow ana sytuacja W opisanym procesie występuje drobny błąd. Studenci
(i doświadczeni programiści!) często wykrywają go dopiero po opracowaniu im
plementacji na podstawie wcześniejszego opisu. Problem, pokazany w przykładzie
po prawej stronie, polega na tym, że
Kompresja
proces sprawdzania znaku następne Da n e wejściowe A B A B A B A
go może spowodować przejście o je Dopasow anie A B A B A B A
den znak za daleko. W przykładzie D a n e wyjściowe 4 1 42 81 83 80
Tablica s ł ó w ko d ow yc h
wejściowy łańcuch znaków: Klucz Wartość
A B 81 AB AB AB 81
ABABABA B A 82 BA BA 82
ABA 83 ABA 83
jest kompresowany do pięciu wyj
ściowych słów kodowych: Rozpakowywanie
D a n e wejściowe 41 42 81 83 80
41 42 81 83 80 D a n e wyjściowe A B A B ? _______ M u si być równe
(zobacz poniżej)
81 A B AB AB
Pokazano to w górnej części rysun 82 B A B 1 D o uzupełnienia elementu
? ^ p o t r z e b n y jest znak następny
ku. Aby rozpakować dane, należy AB.
wczytać słowo kodowe 41, zapisać Kolejny znak d anych wyjściowych - znak następny!
A, wczytać słowo kodowe 42 w celu Rozpakowywanie metodą LZW - skomplikowana sytuacja
pobrania znaku następnego, dodać
AB jako element 81 tablicy, zapisać B powiązane z 42, wczytać słowo kodowe 81, żeby
pobrać znak następny, dodać BAjako element 82 tablicy i zapisać AB powiązane z 81.
Do tej pory wszystko przebiega prawidłowo. Jednak po wczytaniu słowa kodowego
83 w celu pobrania znaku następnego występuje problem, ponieważ słowo to wczyta
no w celu uzupełnienia elementu 83 tablicy! Na szczęście, m ożna łatwo sprawdzić ten
warunek (zachodzi on, kiedy słowo kodowe jest taicie samo, jak uzupełniany element
tablicy) i rozwiązać problem (znak następny musi być pierwszym znakiem w danym
elemencie tablicy ponieważ będzie to kolejny znak do zapisania). Zgodnie z tą logiką
w przykładzie znakiem następnym musi być A (pierwszy znak w ABA). Dlatego zarów
no kolejny wyjściowy łańcuch znaków, jak i element 83 tablicy to ABA.
Im plem entacja Po tym opisie zaimplementowanie kodowania LZW jest proste. Kod
pokazano w a l g o r y t m i e 5 .i i na poprzedniej stronie (implementacja metody expand ()
znajduje się na następnej stronie). W implementacjach dane wejściowe to 8-bitowe baj
ty (dlatego można skompresować dowolny plik, a nie tylko łańcuchy znaków), a dane
wyjściowe to 1 2 -bitowe słowa kodowe (co pozwala uzyskać lepszą kompresję przez za-
856 ROZDZIAŁ 5 Łańcuchy znaków
ALGORYTM 5.11 (ciąg dalszy). R ozpakowywanie w m etodzie LZW
p u b lic s t a t i c vo id expandQ
{
S t r i n g [] s t = new S t r i n g [ L ] ;
in t i; // N a stę pn a d o s tę p n a w a r t o ś ć s ło w a kodowego.
f o r ( i = 0; i < R; i + + ) // I n i c j o w a n i e t a b l i c y na z n a k i .
s t [i ] = " " + ( c h a r ) i ;
s t [i ++] = " // (N ieu żyw a n y ) znak n a s t ę p n y d l a końca p l i k u .
i n t codeword = B i n a r y S t d l n . r e a d l n t ( W ) ;
S t r i n g va l = s t [ c o d e w o r d ] ;
w h ile (true )
{
B in a r y S t d O u t . w r it e ( v a l); // Z a p i s b ie ż ą c e g o p o d ła ń c u c h a .
codeword = B i n a r y S t d l n . r e a d l n t ( W ) ;
i f (codeword == R) b re a k ;
S t r i n g s = s t [codew ord]; // P o b i e r a n i e n a st ę p n e g o s ło w a
// kodowego.
i f (i == codeword) // J e ś l i znak n a s t ę p n y j e s t
// niepraw idłow y,
s = val + v a l . c h a r A t ( O ) ; // n a l e ż y u tw o rz y ć sło w o kodowe na
// p o d s t a w ie p o p r z e d n i e g o .
i f (i < L)
s t [ i + + ] = va l + s . c h a r A t ( O ) ; // Dodawanie nowego elementu do
// t a b l i c y kodów.
v a l = s; // A k t u a l i z o w a n i e b ie ż ą c e g o słow a
// kodowego.
}
B in a ry Std O u t.c lo se ();
Implementacja rozpakowywania w algorytmie Lempela-Ziva-Welcha jest nieco bardziej
skomplikowana niż implementacja kompresowania, ponieważ trzeba wyodrębnić znak na
stępny z kolejnego słowa kodowego i z uwagi na skomplikowaną sytuację, w której znak
następny jest nieprawidłowy (zobacz opis w tekście).
% j a v a LZW - < abr aLZW. t xt | j a v a LZW +
ABRACADABRABRABRA
% more ababLZW. txt
ABABABA
% j a v a LZW - < ababLZW.t xt | j a v a LZW +
ABABABA
5.5 □ Kompresja danych 857
stosowanie dużo większego słownika). Wartości te zapisano w ostatnich zmiennych
egzemplarza R, L i Ww kodzie. Dla tablicy kodów w metodzie compress () użyto drzewa
TST (zobacz p o d r o z d z i a ł 5 .2 ), wykorzystując możliwość napisania wydajnej imple
mentacji metody 1o n g e s t P r e f ix O f () za pomocą drzewa trie. Odwróconą tablicę ko
dów w metodzie expand () przedstawiono jako tablicę łańcuchów znaków. Przy takich
rozwiązaniach kod metod compress () i expand ( ) jest czymś więcej niż przekształconą
wiersz po wierszu wersją opisów z tekstu. Metody te są bardzo skuteczne w ich obecnej
postaci. Dla niektórych plików można poprawić działanie metod przez opróżnianie
tablicy słów kodowych i zaczynanie procesu od początku po wykorzystaniu wszystkich
wartości słów kodowych. Te usprawnienia, wraz z eksperymentami dotyczącymi ich
wydajności, omówiono w ćwiczeniach w końcowej części podrozdziału.
warto poświęcić chwilę na staranne zapoznanie się z przykładami dzia
jak z w y k l e
łania kompresji LZW, przedstawionymi wraz z program am i i w dolnej części tej stro
ny. Przez kilka dziesięcioleci od czasu wymyślenia m etody udowodniono, że jest ona
wszechstronną i skuteczną techniką kompresji danych.
Wirus (50000 bitów)
% j a v a Genome - < g e n o m e V ir u s . t x t | j a v a PictureDump 512 25
i tíSvuc- ? tí
L ic zb a b itó w : 12536
% j a v a LZW - < g e n o m e V ir u s . t x t | j a v a PictureDump 512 36
L ic z b a b itó w : 18232 -* Nie takdobra, ja k kod 2-bitowy, poniew aż występuje m alo
pow tórzeń danych
Bitmapa (6144 bity)
% j a v a RunLength - < q 6 4 x 9 6 .b in | j a v a BinaryDump 0
L iczb a bitó w : 2296
% j a v a LZW - < q 6 4 x 9 6 .b in | j a v a BinaryDump 0
L ic z b a b itó w : 2824 -< Nie tak dobra, ja k kodow anie długości serii, poniew aż plikjest zbyt m aiy
Cały tekst książki Tale of Two Cities (5812552 bity)
% j a v a BinaryDump 0 < t a l e . t x t
L ic zb a b itó w : 5812552
% j a v a Huffman - < t a l e . t x t | j a v a BinaryDump 0
L ic zba b itó w : 3043928
% j a v a LZW - < t a l e . t x t | j a v a BinaryDump 0
L ic z b a b it ó w : 2667952 W spółczynnik kompresji w ynosi 2667952/5812552 = 4 6 %
(najlepszy z dotychczasow ych wyników)
Kompresowanie i rozpakowywanie różnych plików za pomocą 12-bitowego kodowania LZW
858 ROZDZIAŁ 5 □ Łańcuchy znaków
PYTANIA I ODPOWIEDZI
P. Dlaczego zastosowano klasy Bi naryStdln i BinaryStdOut?
O. Trzeba wybrać między wydajnością a wygodą. Klasa Stdln jednocześnie obsłu
guje 8 bitów, a klasa BinaryStdln musi przetworzyć każdy bit. Większość aplikacji
korzysta ze strum ieni bajtów. Kompresowanie danych to wyjątkowe zadanie.
P. Po co stosować metodę cl ose () ?
O. Ten wymóg wynika z tego, że standardowe dane wyjściowe to strum ień bajtów,
dlatego m etoda Bi naryStdOut musi wiedzieć, kiedy ma zapisać ostatni bajt.
P. Czy można łączyć klasy Stdln i Bi naryStdln?
O. Nie jest to dobry pomysł. Z uwagi na zależności od systemu i implementacji nie
wiadomo, co się wtedy stanie. Opracowane przez nas implementacje zgłoszą wtedy
wyjątek. Jednak łączenie klas StdOut i Bi naryStdOut, co robimy w kodzie, nie prowa
dzi do problemów.
P. Dlaczego klasa Node ma modyfikator s ta ti c w klasie Huffman?
O. Opracowane przez nas algorytmy kompresji danych mają postać kolekcji m etod
statycznych, a nie implementacji typów danych.
P. Czy m ożna zagwarantować przynajmniej to, że algorytm kompresji nie zwiększy
długości strum ienia bitów?
O. Można po prostu skopiować dane wejściowe w danych wyjściowych, trzeba jed
nak poinformować o rezygnacji ze standardowego sposobu kompresji. Producenci
implementacji komercyjnych dają czasem takie gwarancje, gwarancje te są jednak
słabe, a samym rozwiązaniom daleko od uniwersalności. Typowe algorytmy kom pre
sji nie osiągają nawet drugiego kroku pierwszego dowodu t w i e r d z e n i a s . Niewiele
algorytmów potrafi dodatkowo skompresować łańcuch bitów utworzony przez ten
sam algorytm.
5.5 0 Kompresja danych 859
OĆWICZENIA
5.5.1. Rozważmy cztery kody o zmiennej dłu Sym bol Kod 1 Kod 2 Kod 3 Kod 4
gości przedstawione w tabeli po prawej stronie. A 0 0 1 1
Które z tych kodów są bezprefiksowe? Które
B 100 1 01 01
można jednoznacznie odkodować? Dla tych
ostatnich odkoduj łańcuch 1000000000000. C 10 00 001 001
D 11 11 0001 000
5.5.2. Podaj przykład kodu, który umożliwia
jednoznaczne odkodowywanie, a który nie jest
bezprefiksowy.
Odpowiedź: każdy kod bezsufiksowy umożliwia jednoznaczne odkodowywanie.
5.5.3. Podaj przykład kodu, który umożliwia jednoznaczne odkodowywanie,
a nie jest wolny ani bezprefiksowy, ani bezsufiksowy.
Odpowiedź: {0 0 1 1 , 0 1 1 , 1 1 , 1 1 1 0 } lub {0 1 , 1 0 , 0 1 1 , 1 1 0 }.
5.5.4. Czy kody { 01, 1001, 1011, 111, 1110 } i{ 01, 1001, 1011, 111, 1110
} umożliwiają jednoznaczne odkodowywanie? Jeśli nie, podaj łańcuch znaków, który
m ożna zakodować na dwa sposoby.
5.5.5. Użyj program u RunLength do pliku [Link] z poświęconej książce wi
tryny. Ile bitów ma skompresowany plik?
5.5.6. Ile bitów potrzeba do zakodowania N kopii symbolu a, a ile przy kodowaniu
N kopii ciągu abc (podaj wartość jako funkcję od iV)?
5.5.7. Przedstaw efekt kodowania łańcuchów znaków a, aa, aaa, aaaa,... (łańcuchów
znaków składających się z N kopii a) za pom ocą kodowania długości serii, metody
Huffmana i LZW. Jaki jest współczynnik kompresji wyrażony jako funkcja od NI
5.5.8. Przedstaw efekt kodowania łańcuchów znaków ab, abab, ababab, abababab,
... (łańcuchów znaków składających się z N powtórzeń ab) za pomocą kodowania
długości serii, m etody Huffmana i LZW. Jaki jest współczynnik kompresji wyrażony
jako funkcja od N?
5.5.9. Oszacuj współczynnik kompresji uzysldwany za pom ocą kodowania długości
serii, m etody Huffmana i LZW dla losowego łańcucha znaków ASCII o długości N
(na każdej pozycji wszystkie znaki występują tu z równym prawdopodobieństwem).
5.5.10. Przedstaw (tak jak na rysunkach w tekście) tworzenie drzewa w kodowaniu
Huffmana przy zastosowaniu klasy Huffman do łańcucha znaków "i t was the age of
fool i shness". Ile bitów zajmuje skompresowany strumień?
860 ROZDZIAŁ 5 □ Łańcuchy znaków
ĆWICZENIA (ciąg dalszy)
5 .5.11. Jak wygląda kod Huffmana dla łańcucha znaków, którego wszystkie znaki
pochodzą z dwuznakowego alfabetu? Podaj przykład, w którym potrzebna jest m ak
symalna liczba bitów w kodzie Huffmana dla N -znakowego łańcucha ze znakami
z dwuznakowego alfabetu.
5.5.12. Załóżmy, że prawdopodobieństwo wystąpienia każdego symbolu to ujemna
potęga liczby 2 . Opisz uzyskany kod Huffmana.
5.5.13. Załóżmy, że liczba wystąpień każdego symbolu jest równa. Opisz uzyskany
kod Huffmana.
5.5.14. Załóżmy, że liczba wystąpień każdego kodowanego znaku jest inna. Czy
drzewo w kodowaniu Huffmana jest wtedy unikatowe?
5.5.15. Kodowanie Huffmana można rozwinąć w prosty sposób, aby zakodować
znaki 2-bitowe (za pom ocą drzew 4-kierunkowych). Jaka jest najważniejsza zaleta
i wada tego rozwiązania?
5.5.16. Jak poniższe dane będą wyglądać po zakodowaniu m etodą LZW?
a. T0BE0RN0TT0BE
b. YABBADABBADABBADOO
c. AAAAAAAAAAAAAAAAAAAAA
5.5.17. Opisz skomplikowaną sytuację w kodowaniu LZW.
Odpowiedź: po napotkaniu ciągu cScSc, gdzie c to symbol, a S to łańcuch znaków, cS
znajduje się już w słowniku, ale cSc — jeszcze nie.
5.5.18. Niech F to /c-ta liczba Fibonacciego. Rozważmy N symboli, gdzie k-ty sym
bol występuje Fk razy. Zauważmy, że Fj + F, + ... + FN- FN+2 - 1. Opisz kod Huffmana.
Wskazówka: najdłuższe słowo kodowe m a długość N - 1.
5.5.19. Pokaż, że istnieje przynajmniej 2N1 różnych kodów Huffmana odpowiadają
cych danemu zbiorowi N symboli.
5.5.20. Podaj kod Huffmana, w którym liczba wystąpień cyfry 0 w danych wyjścio
wych jest znacznie, znacznie większa niż liczba wystąpień cyfry 1 .
Odpowiedź: jeśli znak A występuje milion razy, a znak B — tylko raz, słowo kodowe
dla Ato 0, a słowo kodowe dla B to 1.
5.5 o Kompresja danych
5.5.21. Udowodnij, że długość dwóch najdłuższych słów kodowych w kodzie
Huffmana jest taka sama.
5.5.22. Udowodnij następujący fakt na tem at kodów Huffmana — jeśli liczba wystą
pień symbolu i jest większa niż liczba wystąpień symbolu j, długość słowa kodowego
symbolu i jest mniejsza lub równa długości słowa kodowego symbolu j.
5.5.23. Jaki będzie efekt rozbicia łańcucha znaków zakodowanego m etodą Huffmana
na pięciobitowe znaki i zakodowania tego łańcucha za pom ocą tej samej techniki?
5.5.24. Pokaż (tak jak na rysunkach w tekście) zbudowane na potrzeby kodowania
drzewo trie oraz proces kompresowania i rozpakowywania przy stosowaniu metody
LZWdla poniższego łańcucha znaków:
i t was th e b e s t o f tim e s i t was th e w o r s t o f tim e s
862 ROZDZIAŁ 5 ■ Łańcuchy znaków
| PROBLEMY DO ROZWIĄZANIA
5.5.25. Kod o stałej długości. Zaimplementuj klasę RLE. Wykorzystaj w niej kod
o stałej długości do kompresowania strum ieni bajtów ASCII za pomocą stosunkowo
niewielu znaków. Kod należy przesyłać jako część zakodowanego strum ienia bitów.
Dodaj do m etody com press () kod do tworzenia łańcucha znaków al pha z wszystkimi
różnymi znakami występującymi w wiadomości. Wykorzystaj ten łańcuch do utwo
rzenia obiektu Al phabet do zastosowania w metodzie com press (). Łańcuch znaków
al pha (znaki w kodzie 8 -bitowym i długość) należy podać przed skompresowanym
strumieniem bitów. Do m etody expand () dodaj kod wczytujący alfabet przed rozpa
kowywaniem danych.
5.5.26. Ponowne tworzenie słownika w metodzie LZW . Zmodyfikuj klasę LZW tak,
aby po zapełnieniu słownika opróżniała go i zaczynała pracę od nowa. W niektórych
zastosowaniach jest to zalecane podejście, ponieważ zapewnia lepsze dostosowanie
do zmian ogólnego charakteru danych wejściowych.
5.5.27. Długie powtórzenia. Oszacuj współczynnik kompresji uzyskiwany w kodo
waniu długości serii, metodzie Huffmana i LZW dla łańcuchów znaków w długości
2N utworzonych przez złączenie dwóch kopii losowych łańcuchów znaków ASCII
o długości N (zobacz ć w i c z e n i e 5 .5 .9 ). Przyjmij wszelkie założenia, które uznasz za
zasadne.
ROZDZIAŁ 6
l i l i Kontekst
e w s p ó ł c z e s n y m ś w i e c i e urządzenia obliczeniowe są wszechobecne.
W W ciągu lulku ostatnich dziesięcioleci przeszliśmy z rzeczywistości, w któ
rej takie urządzenia były praktycznie nieznane, do świata, w lctórym miliar
dy osób regularnie z nich korzystają. Ponadto współczesne telefony komórkowe oferują
znacznie większe możliwości niż superkomputery dostępne jeszcze 30 lat temu garstce
wybrańców. Wiele algorytmów umożliwiających skuteczne działanie urządzeń to roz
wiązania opisane w tej książce. Dlaczego? Ponieważ przetrwają najsilniejsi. Skalowalne
(liniowe i liniowo-logarytmiczne) algorytmy odegrały kluczową rolę w postępie i sta
nowiły dowód na to, jak ważne jest rozwijanie wydajnych algorytmów. Badacze pracu
jący w latach 60. i 70. ubiegłego wieku zbudowali podstawową infrastrukturę, z której
możemy obecnie korzystać dzięki wspomnianym algorytmom. Naukowcy wiedzieli, iż
skalowalne algorytmy są kluczem do przyszłości. Osiągnięcia kilku ostatnich dziesię
cioleci potwierdziły ich wizję. Teraz, gdy infrastruktura jest gotowa, ludzie zaczynają
jej używać w różnych celach. Znane jest spostrzeżenie B. Chazellea — wiek XX był
wiekiem równań, natomiast wiek XXI to wiek algorytmów.
Omówienie podstawowych algorytmów przedstawionych w książce to tylko punkt
wyjścia. Bliski jest dzień, w którym algorytmom poświęcone będą całe studia (a może
już tak jest?). W obszarze zastosowań komercyjnych, obliczeń naukowych, inżynierii,
badań operacyjnych i w wielu innych dziedzinach — zbyt różnorodnych, aby można
o nich nawet wspomnieć — od wydajnych algorytmów zależy, czy uda się rozwiązać
problemy współczesnego świata, czy w ogóle nie będzie m ożna się z nim i zmierzyć.
W książce kładziemy nacisk na badanie ważnych i przydatnych algorytmów. W tym
rozdziale podkreślamy to podejście i omawiamy przykłady dotyczące roli przedsta
wionych algorytmów (i naszego podejścia do ich badania) w kilku zaawansowanych
kontekstach. Aby podkreślić zasięg wpływu algorytmów, zaczynamy od bardzo krót
kiego omówienia kilku ważnych obszarów zastosowań. W celu pokazania znaczenia
algorytmów dalej szczegółowo przedstawiamy specyficzne przykłady i wprowadzenie
do teorii algorytmów. W obu sytuacjach jest to tylko krótki przegląd w końcowej czę
ści długiej książki, który siłą rzeczy jest wyrywkowy. Na każdy wspomniany obszar
przypadają dziesiątki innych, równie szerokich. Na każdą opisaną kwestię przypada
865
866 KONTEKST
wiele innych, równie ważnych. Na każdy omówiony tu szczegółowy przykład przypa
dają setki, jeśli nie tysiące innych, równie znaczących.
Z a s t o s o w
a n i a k o m e r c y j n e Pojawienie się internetu spowodowało podkreślenie
kluczowej roli algorytmów w zastosowaniach komercyjnych. Wszystkie aplikacje,
z których regularnie korzystasz, działają lepiej dzięki omówionym klasycznym algo
rytmom. Oto obszary, z których pochodzą te aplikacje:
■ infrastruktura (systemy operacyjne, bazy danych, rozwiązania komunikacyjne),
D aplikacje (klienty e-mail, edytory tekstu, programy do obróbki zdjęć),
■ publikacje (książki, magazyny, materiały internetowe),
n sieci (sieci bezprzewodowe, sieci społecznościowe, internet),
■ przetwarzanie transakcji (finansowych, handlowych, wyszukiwanie w sieci
WWW).
Jako ważny przykład omawiamy w tym rozdziale drzewa zbalansowane. Jest to „za
służona” struktura danych, opracowana na potrzeby komputerów typu mainstream
w latach 60. ubiegłego wieku i nadal używana jako podstawa współczesnych syste
mów baz danych. Opisujemy też tablice przyrostkowe (inaczej sufiksowe) stosowane
do indeksowania tekstu.
O b l i c z e n i a n a u k o w e Od czasu, kiedy von Neumann opracował sortowanie przez
scalanie w 1950 roku, algorytmy odgrywają kluczową rolę w obliczeniach nauko
wych. Współcześni naukowcy generują mnóstwo danych eksperymentalnych oraz
stosują modele matematyczne i obliczeniowe do zrozumienia świata naturalnego.
Wykorzystują przy tym:
n obliczenia matematyczne (wielomiany, macierze, równania różniczkowe),
D przetwarzanie danych (wyników i obserwacji eksperymentalnych, zwłaszcza
w dziedzinie badań nad genomem),
■ modele obliczeniowe i symulacje.
Wszystkie te obszary wymagają złożonych i rozbudowanych obliczeń na olbrzymich
ilościach danych. Jako szczegółowy przykład zastosowania z dziedziny obliczeń na
ukowych przedstawiamy w tym rozdziale klasyczne symulacje sterowane zdarzenia
mi. Pomysł polega na tym, aby podtrzymywać model skomplikowanego rzeczywiste
go systemu i kontrolować zmiany zachodzące w modelu. Istnieje wiele zastosowań
tego podstawowego podejścia. Omawiamy też podstawowy problem przetwarzania
danych w badaniach nad genomem.
Niemal z definicji współczesna inżynieria oparta jest na technologii.
I n ż y n i e r i a
Współczesna technologia oparta jest na komputerach, dlatego algorytmy odgrywają
kluczową rolę w:
■ obliczeniach matematycznych i przetwarzaniu danych,
D projektowaniu wspomaganym komputerowo i produkcji,
B inżynierii opartej na algorytmach (sieci, systemy sterowania),
■ obrazowaniu i innych systemach medycznych.
KONTEKST 867
Inżynierowie i naukowcy korzystają z wielu tych samych narzędzi i podejść. Przyk
ładowo, naukowcy tworzą modele obliczeniowe i symulacje w celu zrozumienia
świata naturalnego. Inżynierowie opracowują modele obliczeniowe i symulacje na
potrzeby projektowania, budowania i kontrolowania rozwijanych obiektów.
B adania operacyjne Badacze i naukowcy z dziedziny badań operacyjnych rozwijają
oraz stosują modele matematyczne do rozwiązywania problemów takich jak:
n szeregowanie,
0 podejmowanie decyzji,
■ przypisywanie zasobów.
Problem wyszukiwania najkrótszej ścieżki, opisany w p o d r o z d z i a l e 4 .4 , jest kla
sycznym problemem z dziedziny badań operacyjnych. Wracamy do tego zagadnie
nia i rozważamy problem maksymalnego przepływu, omawiamy znaczenie redukcji
i wyjaśniamy jej znaczenie ze względu na ogólne modele rozwiązywania problemów,
a przede wszystkim bardzo ważny w badaniach operacyjnych model programowania
liniowego.
w wielu podobszarach nauk komputerowych
a lg o r y t m y o dg ryw ają w a żn ą ro lę
i mają zastosowania we wszystkich tych dziedzinach. Obszary te to między innymi:
° geometria obliczeniowa,
D kryptografia,
13 bazy danych,
■ języki i systemy programowania,
D sztuczna inteligencja.
W każdej dziedzinie bardzo ważne jest ujęcie problemów oraz znalezienie wydaj
nych algorytmów i struktur danych do ich rozwiązywania. Niektóre z omówionych
algorytmów m ożna zastosować bezpośrednio. Co ważniejsze, ogólne podejście do
projektowania, implementowania i analizowania algorytmów, na którym oparta jest
ta książka, okazało się skuteczne we wszystkich wymienionych obszarach. Efekt ten
wykracza poza nauki komputerowe i dotyczy także wielu innych dziedzin — od gier
przez muzykę, lingwistykę i finanse po nauki o mózgu.
Opracowano tak wiele ważnych i przydatnych algorytmów, że trzeba poznać oraz
zrozumieć zależności między nimi. Rozdział ten (i całą książkę!) kończymy wpro
wadzeniem do teorii algorytmów ze szczególnym naciskiem na nierozwiązywalność
i pytanie, czy N=NP, nadal stanowiące klucz do zrozumienia praktycznych problemów,
które chcemy rozwiązać.
868 KONTEKST
Symulacja sterowana zdarzeniami Pierwszy przykład to fundamentalne
zastosowanie algorytmów w nauce — symulowanie ruchu w systemie cząsteczek za
chowujących się zgodnie z prawami zderzeń sprężystych. Naukowcy stosują takie
systemy, aby m óc zrozumieć i prognozować funkcjonowanie systemów fizycznych.
Model ten dotyczy ruchu cząsteczek w gazie, dynamiki reakcji chemicznych, dyfu
zji atomowej, upakowania kul, stabilności pierścieni wokół planet, przejść fazowych
pewnych elementów, jednowymiarowych niezależnych systemów grawitacji, propa
gacji frontu i wielu innych dziedzin. Zastosowania są różnorodne — od dynamiki
molekularnej, gdzie obiektami są małe (mniejsze od atomu) cząsteczki, po astrofizy
kę, gdzie obiektami są duże ciała niebieskie.
Rozwiązanie problemu wymaga nieco fizyki na poziomie szkoły wyższej, trochę
inżynierii oprogramowania i porcji wiedzy o algorytmach. Większość kwestii fizycz
nych omawiamy w ćwiczeniach w końcowej części rozdziału, co pozwoli skoncentro
wać się na podstawowym zagadnieniu — wykorzystaniu do rozwiązania problemu
podstawowego narzędzia algorytmicznego (kolejek priorytetowych opartych na kop
cu), które umożliwia przeprowadzenie obliczeń niewykonalnych w inny sposób.
M odel oparty na tw ardych dyskach Zaczynamy od wyidealizowanego m odelu
ruchu atomów lub cząsteczek w kontenerze. Model ma następujące cechy:
■ Poruszające się cząsteczki wchodzą w interakcje poprzez zderzenia sprężyste ze
sobą i ze ścianami.
■ Każda cząsteczka to dysk o znanych param etrach — pozycji, prędkości, masie
i promieniu.
■ Nie działają żadne inne siły.
Ten prosty model odgrywa kluczową rolę w mechanice staty
stycznej. Jest to obszar, w którym obserwacje makroskopowe
(dotyczące na przykład tem peratury i ciśnienia) są wiązane
z dynamiką mikroskopową (związaną na przykład z ruchem
Przesunięcie czasu do f + dt
• A
poszczególnych atomów i cząsteczek). Maxwell i Boltzmann
wykorzystali ten model do wyprowadzenia rozkładu pręd
© # kości cząsteczek wchodzących w interakcje jako funkcji
Przesunięcie czasu do f + 2dt temperatury. Einstein na podstawie tego modelu wyjaśnił
ruchy Browna pyłków kwiatowych zanurzonych w wodzie.
• ¿1 Założenie, że nie działają żadne inne siły, oznacza, iż cząstecz
ki między zderzeniami poruszają się po liniach prostych ze
stałą prędkością. Jeśli uwzględnimy na przykład tarcie i ruch
Cofnięcie czasu do mom entu zderzenia obrotowy, uzyskamy bardziej precyzyjny m odel ruchu zna
nych obiektów fizycznych, takich jak kule bilardowe na stole.
Sym ulacje sterowane czasem Podstawowym celem jest
utrzymanie modelu. Oznacza to, że chcemy śledzić pozycje
Symulacja sterowana czasem
i prędkości wszystkich cząsteczek w czasie. Wymaga to prze
prowadzenia podstawowych obliczeń. Na podstawie pozycji
Symulacja sterowana zdarzeniami 869
i prędkości w danym czasie t należy zaktualizować je tak, aby odzwierciedlały sy
tuację w późniejszym czasie t+dt dla określonej ilości czasu dt. Jeśli cząsteczki są
na tyle oddalone od siebie i od ścian, że zderzenie nie nastąpi przed czasem t+dt,
obliczenia są proste. Ponieważ cząsteczki poruszają się po liniach prostych, należy
zastosować prędkość każdej cząsteczki do zaktualizowania jej pozycji. Problemem
jest uwzględnienie zderzeń. Jedno z podejść, symulacja sterowana czasem, jest oparte
na zastosowaniu stałej wartości dt. Przy każdej aktualizacji trzeba sprawdzić wszyst
kie pary cząsteczek, ustalić, czy dwie z nich nie zajmują tej Wartość dt jest zbyt mała -
samej pozycji, a następnie cofnąć się do m om entu pierwszego obliczenia są zbyt częste
zderzenia. Na tym etapie m ożna odpowiednio zaktualizować
prędkości obu cząsteczek, aby uwzględnić zderzenie (służą do
tego opisane dalej obliczenia). Przy symulowaniu ruchu dużej
liczby cząsteczek podejście to wymaga dużej mocy oblicze
niowej. Jeśli czas dt jest mierzony w sekundach (zwykle są to Wartość dt jest zbyt duża - może
nastąpić pominięcie zderzenia
ułamki sekund), symulowanie funkcjonowania systemu o N
cząsteczkach przez jedną sekundę zajmuje czas proporcjonal
ny do ISPIdt. Koszt ten zniechęca do stosowania algorytmu
(jest wyższy niż dla standardowych algorytmów kwadrato
wych). W istotnych zastosowaniach N jest bardzo duże, a dt
•i
— bardzo małe. Problem polega na tym, że jeśli dt jest zbyt
małe, koszt obliczeń jest
'
wysoki,
<
a zprzyi zbyt
i
dużym
i
dt może Podstawowy Problem z symulacjami
sterowanymi czasem
nastąpić pominięcie zderzenia.
Sym ulacja sterowana zdarzeniam i Stosujemy inne podejście, w którym istotne są
tylko m om enty występowania zderzeń. Przede wszystkim zawsze interesuje nas na
stępne zderzenie, ponieważ do tego m om entu odpowiednia jest prosta aktualizacja
pozycji wszystkich cząsteczek na podstawie ich prędkości. Dlatego przechowujemy
kolejkę priorytetową zdarzeń, w której zdarzenie to potencjalne zderzenie w pewnym
przyszłym momencie — albo między dwoma cząsteczkami, albo między cząsteczką
a ścianą. Priorytetem powiązanym z każdym zdarzeniem jest jego czas, dlatego po
operacji usuń minimalny na kolejce priorytetowej uzyskujemy następne potencjalne
zderzenie.
Prognozowanie zdarzeń Jak m ożna zidentyfikować potencjalne zderzenia? Pręd
kości cząsteczek zapewniają potrzebne informacje. Załóżmy na przykład, że w czasie
t cząsteczka o prom ieniu s zajmuje pozycję (r., r ) i porusza się z prędkością (y , v )
w jednostkowym pudełku. Rozważmy pionową ścianę. Wartość x= 1, a y wynosi mię
dzy 0 a 1. Interesująca jest tu pozioma składowa ruchu, dlatego m ożna skoncentro
wać się na składowej x dla pozycji r i składowej x dla prędkości v . Jeśli wartość vx jest
ujemna, cząsteczka nie znajduje się na torze kolizyjnym względem ściany, jednak przy
dodatniej wartości v może nastąpić zderzenie ze ścianą. Odległość w poziomie do
ściany (1 - s - r ) m ożna podzielić przez wartość poziomej składowej prędkości (y),
aby odkryć, że cząsteczka uderzy w ścianę po dt = (1 - s - r )/v jednostkach czasu.
870 KONTEKST
Efekt (w czasie t + dt)
Prędkość po zderzeniu = (~vt, vy)
Pozycja p o zderzeniu - (1 - s , r + v dt)
Prognoza (w czasie t)
dt = czas do zderzenia ze ścianą Ś c ia n a
= odległość/prędkość (r,,r.) ' przy
= (1 - s - r ) / v x=;
Prognoza i efekt zderzenia cząsteczki ze ścianą
Cząsteczka będzie wtedy zajmować pozycję (1 - s, r + v dt), o ile wcześniej nie zderzy
się z inną cząsteczką lub poziomą ścianą. Należy więc umieścić w kolejce prioryteto
wej element o priorytecie t + d t {i odpowiednich informacjach opisujących zdarzenie
zderzenia cząsteczki ze ścianą). Obliczenia przy prognozowaniu zderzenia z innymi
ścianami wyglądają podobnie (zobacz ć w i c z e n i e 6 . i ). Obliczenia zderzenia dwóch
cząsteczek też przebiegają podobnie, ale są bardziej skomplikowane. Zauważmy, że
obliczenia często prowadzą do prognoz, zgodnie z którymi zderzenie nie nastąpi (je
śli cząsteczka oddala się od ściany lub dwie cząsteczki oddalają się od siebie). Nie
trzeba wtedy umieszczać żadnych danych w kolejce priorytetowej. Na potrzeby ob
sługi sytuacji innego rodzaju, kiedy czas prognozowanego zderzenia jest zbyt daleki,
aby go uwzględniać, dodajemy param etr 1i mit. Określa on uwzględniany przedział
czasu, dlatego można pominąć wszelkie zdarzenia, których prognozowany czas jest
późniejszy niż 1 i mi t.
E fekt zderzenia Kiedy nastąpi zderzenie, trzeba określić jego efekt, stosując wzory
fizyczne określające zachowanie cząsteczki po zderzeniu sprężystym ze ścianą lub
inną cząsteczką. W omawianym przykładzie, w którym cząsteczka zderza się z pio
nową ścianą, po wystąpieniu zderzenia prędkość cząsteczki zmienia się z (y_, v ) na
(-vv, v ). Obliczenia efektu zderzenia dla innych ścian przebiegają analogicznie, a dla
zderzenia dwóch cząsteczek wyglądają podobnie, są jednak bardziej skomplikowane
(zobacz ć w i c z e n i e 6 .1 ).
Prognoza (w czasie t)
Cząsteczki zderzają się, chyba
że jedna przejdzie po za punkt
przecięcia przed dotarciem Efekt (w czasie t + dt)
do niego drugiej Po zderzeniu prędkości obu
cząsteczek zmieniają się
Prognoza i efekt zderzenia dwóch cząsteczek
Symulacja sterowana zdarzeniami 871
Unieważnione zdarzenia Z uwagi na wcześniejsze zderzenia Poruszanie się cząsteczki
w kierunku ściany
wiele prognozowanych zderzeń nie zachodzi. Aby zapewnić
obsługę tej sytuacji, dla każdej cząsteczki należy przechowywać
zmienną egzemplarza z liczbą zderzeń, w których cząsteczka
brała udział. Przy usuwaniu zdarzenia z kolejki priorytetowej
w celu jego przetworzenia należy sprawdzić, czy liczba odpo \
Cząsteczki poruszają się
po torze kolizyjnym
wiadająca cząsteczce zmieniła się od czasu wygenerowania zda
rzenia. Ten sposób obsługi unieważnionych zdarzeń to podej Prognozowalne zdarzenia
ście leniwe — kiedy cząsteczka bierze udział w zderzeniu, po
zostawiamy powiązane z nią unieważnione już zdarzenia
Cząsteczka oddalająca
się od ściany w kolejce priorytetowej i ignorujemy je, kiedy nadejdzie
ich czas. Inne, zachłanne podejście polega na usunięciu
z kolejki priorytetowej wszystkich nowych potencjalnych
zderzeń z udziałem danej cząsteczki. Ta m etoda wymaga
bardziej zaawansowanej kolejki priorytetowej (z imple
mentacją operacji usuń).
jest podstawą do kompletnej sterowanej
t o o m ó w ie n ie
Cząsteczki oddalające
się od siebie zdarzeniami symulacji ruchu cząsteczek wchodzących ze
sobą w interakcje zgodnie z fizycznymi prawami zderzeń
sprężystych. Architektura oprogramowania obejmuje tu
trzy klasy — typ danych P a r t i cl e, ukrywający obliczenia
Jedna cząsteczka dotyczące cząsteczek, typ danych E v e n t dla prognozowa
dociera do punktu
zderzenia przed drugą nych zdarzeń i wykonującego sy
Dwie cząsteczki na torze kolizyjnym
4
mulacje klienta C o l i i s i o n S y s t e m .
Istotą symulacji jest typ Mi nPQ, któ
I Zderzenie zanadto
ry obejmuje uporządkowane w cza
oddalone w czasie sie zdarzenia. Dalej omawiamy im
Można przewidzieć, plementacje klas P a r t i c l e , Event
że te zdarzenia nie zajdą i Col 1 i sio n S y s te m . Działanie trzeciej cząsteczki -
zderzenie nie występuje
Unieważnione zdarzenie
872 KONTEKST
Cząsteczki w ć w i c z e n i u 6.1 szkicowo opisano implementację typu danych dla czą
steczek, opartą na bezpośrednim zastosowaniu praw ruchu Newtona. Klient odpo
wiedzialny za symulację musi mieć możliwość poruszania cząsteczek, wyświetlania
ich i wykonywania różnych obliczeń związanych ze zderzeniami. Szczegółowo przed
stawiono to w poniższym interfejsie API.
public clas s P a r tic ie
Particle() Tworzy nową losową cząsteczkę w jednostce
kw adratowej
Particle( Tworzy cząsteczkę o danych cechach:
d o ubl e r x , do u b l e r y , pozycji,
do u bl e vx, do u b l e vy, prędkości,
do u bl e s, prom ieniu,
do u bl e mass) m asie
voi d dr aw() Wyświetla cząsteczkę
voi d move( doubl e d t ) Z m ienia pozycję, aby uw zględnić upływ czasu d t
i n t count() Zwraca liczbę zderzeń z udziałem danej cząsteczki
d o ubl e t i m e T o H i t ( P a r t i c l e b) Zwraca czas do zderzenia cząsteczki z b
doubl e t i me To Hi t Ho r i z o n t a l Wa l l () Zwraca czas do zderzenia cząsteczki z poziom ą ścianą
d o ubl e t i m e T o Hi t Ve r t i c a l Wa l l () Zwraca czas do zderzenia cząsteczki z pionową ścianą
voi d b o u n c e O f f ( P a r t i c l e b) Zm ienia prędkości cząsteczki, aby uwzględnić
zderzenie
voi d bounc e Of f Ho r i z ont a l Wa l l () Z m ienia prędkość, aby uw zględnić zderzenie
z poziom ą ścianą
voi d bounceOf f Ver t i ca l Wal 1 () Z m ienia prędkość, aby uw zględnić zderzenie
z pionow ą ścianą
Interfejs API dla obiektów w postaci poruszających się cząsteczek
Wszystkie trzy m etody timeToHit*() zwracają wartość Double.POSITIVE_INFINITY
w (dość częstej) sytuacji, kiedy kurs nie jest kolizyjny. Metody te umożliwiają prze
widywanie wszystkich przyszłych zderzeń z udziałem danej cząsteczki. W kolejce
priorytetowej umieszczane jest każde zdarzenie, które ma zajść przed czasem lim it.
Zawsze przy przetwarzaniu zdarzenia odpowiadającego zderzeniu dwóch cząste
czek wywoływana jest metoda bounce(), która zmienia prędkości obu cząsteczek,
aby odzwierciedlić zderzenie. Przy zdarzeniach odpowiadających zderzeniu między
cząsteczką a ścianą wywoływana jest m etoda bounceOff*().
Symulacja sterowana zdarzeniami 873
Z darzenia W prywatnej klasie umieszczamy opis obiektów umieszczanych w ko
lejce priorytetowej (zdarzeń). Zmienna egzemplarza time obejmuje czas, w którym
zgodnie z prognozami zdarzenie ma nastąpić. Zmienne egzemplarza a i b odpowia
dają cząsteczkom powiązanym ze zdarzeniem. Istnieją trzy różne rodzaje zdarzeń —
cząsteczka może uderzyć w pionową ścianę, w poziomą ścianę lub w inną cząsteczkę.
Aby uzyskać płynne dynamiczne wyświetlanie ruchu cząsteczek, dodano czwarty
rodzaj zdarzeń — ponowne wyświetlanie, które powoduje wyświetlenie wszystkich
cząsteczek na obecnie zajmowanych pozycjach. W implementacji klasy Event zasto
sowano pewną sztuczkę — wartości cząsteczek mogą być równe nuli, co pozwala
zakodować cztery różne typy zdarzeń w następujący sposób:
■ ani a, ani b nie ma wartości nuli — zderzenie dwóch cząsteczek;
■ a jest różne od n u li, b jest równe nuli — zderzenie a z pionową ścianą;
■ a jest równe nul 1 , b jest różne od nuli — zderzenie b z poziomą ścianą;
■ a i b równe nuli — zdarzenie ponownego wyświetlania (wyświetlanie wszyst
kich cząsteczek).
Choć nie jest to programowanie obiektowe na najwyższym poziomie, rozwiązanie
jest intuicyjne i umożliwia pisanie prostego kodu klienta. Poniżej pokazano imple
mentację.
p r i v a t e c l a s s Event impl ement s Comparabl e<Event >
f
p r i v a t e final d o ubl e t i me ;
p r i v a t e final P a r t i c l e a, b;
p r i v a t e final i n t count A, count B;
p u bl i c Event(double t , P a r t i c l e a , P a r t i c l e b)
1 / / Tworzenie nowego z d a r z e n i a , wy s t ę p u j ą c e g o w c z a s i e t i d o t y c z ą c e g o a o r a z b.
[Link] = t;
this.a = a;
this.b = b;
i f ( a != n u l i ) count A = a . c o u n t ( ) ; e l s e count A = - 1 ;
i f (b != n u l i ) countB = b . c o u n t ( ) ; e l s e countB = - 1;
1
p u b l i c i n t compar eTo( Event t h a t )
1
if ( t h i s . t i m e < t h a t . t i m e ) r etu rn -1;
e l s e i f ( t h i s . t i m e > t h a t . t i m e ) r e t u r n +1;
e l s e r e t u r n 0;
1
p u b l i c b o ol e a n i s V a l i d ( )
1
i f (a != n u l l && a . c o u n t ( ) != countA) r e t u r n f a l s e ;
i f (b != n u l l && b . c o u n t () != count B) r e t u r n f a l s e ;
return true;
}
}
Klasa Event służąca do symulowania ruchu cząsteczek
874 KONTEKST
Drugą sztuczką w im plem entacji klasy Event jest przechowywanie zm iennych
egzemplarza countA i countB. Znajduje się w nich liczba zderzeń z udziałem każdej
cząsteczki w chwili utworzenia zdarzenia. Jeśli liczby te nie zmieniły się do m om entu
usuwania zdarzenia z kolejki priorytetowej, można zasymulować wystąpienie zda
rzenia. Jeśli jednak jedna z wartości uległa zmianie między m om entem umieszczenia
zdarzenia w kolejce priorytetowej a czasem jego usuwania, wiadomo, że zdarzenie
zostało unieważnione i można je pominąć. M etoda i sVal i d () umożliwia sprawdze
nie tego warunku w kodzie klienta.
K od do sym ulow ania ruchu Po ukryciu szczegółów obliczeń w klasach P a rtic ie
i Event samo symulowanie wymaga zaskakująco niewiele kodu, co widać w imple
mentacji klasy Col l i s i onSystem (zobacz strony 875 i 876). Większość obliczeń jest
ukrytych w pokazanej na tej stronie metodzie predi ctCol 1i si ons (). Metoda ta oblicza
wszystkie poten-
private voi d p r e d i c t Co l 1 i s i o n s ( P a r t i d e a , dou bl e l i m i t ) c j alneprzyszłezde-
* ., , . rżenia z udziałem
i f (a == n u l i ) r e t u r n ;
f o r ( i n t i = 0; i < p a r t i c l e s . l e n g t h ; i++) cząsteczki a (z in-
{ / / Umieszczani e w pq z d e r z e n i a z udzi ał em c z ą s t e c z k i p a r t i cl es [ i ] . nymi cząsteczka-
doubl e d t = a . t i m e T o H i t ( p a r t i c l e s [ i ] ) ; mi k b śdanam i)
i f ( t + d t <= l i m i t )
p q . i n s e r t ( n e w E v e n t ( t + d t , a , p a r t i cl es [ i ] )); * umieszcza zda-
} rżenie odpowiada-
do u bl e dtX = a . t i m e T o H i t Ve r t i c a l Wa l l ( ) ; ; a c e p a ¿ d e m u 7,de-
i f ( t + dtX <= l i m i t ) . , , .
pq. i n s e r t (new E v e n t ( t + dtX, a , n u l i ) ) ; rzemu w kolejce
do u bl e dtY = a . t i me To Hi t Ho r i z o n t a l Wa l 1(); priorytetowej,
if ( t + dtY <= l i m i t ) Istotą symulacji
p q . i n s e r t ( n e w E v e n t ( t + dtY, n u l i , a ) ) ; . . , . .
j Jest przedstawiona
na stronie 876 me-
Prognozow aniezderzeń z innymi cząsteczkami toda Sim ulate().
Algorytm należy
zainicjować przez wywołanie metody predi ctColl i sions () dla każdej cząsteczki,
aby zapełnić kolejkę priorytetową możliwymi zderzeniami par cząsteczka - cząstecz
ka i cząsteczka - ściana. Następnie algorytm wchodzi w główną pętlę symulacji ste
rowanej zdarzeniami. Działa ona tak:
■ Usuwa najbliższe zdarzenie (o minimalnym priorytecie t).
■ Jeśli zdarzenie jest unieważnione, pomija je.
a Przesuwa wszystkie cząsteczki do czasu t według toru wyznaczanego przez linię
prostą.
■ Aktualizuje prędkości cząsteczek uczestniczących w zderzeniach.
° Wywołuje metodę p re d ic tC o llisio n s(), aby przewidzieć przyszłe zderzenia
obejmujące cząsteczki, które uczestniczyły w zderzeniach, i wstawia do kolejki
priorytetowej zdarzenie odpowiadające każdemu prognozowanemu zderzeniu.
Symulacja sterowana zdarzeniami 875
Oparta na zdarzeniach symulacja zderzeń cząsteczek (zarys)
p u b lic c la s s C o llisio n S y ste m
{
p r i v a t e c l a s s Event implements C o m pa ra b le <E ve n t>
{ / * Zobacz o p i s w t e k ś c i e . * / }
p r i v a t e M in P Q < Ev en t> pq; // K o l e j k a p r i o r y t e t o w a ,
p r i v a t e d o u b le t = 0 . 0 ; // Z e g a r używany w s y m u l a c j i ,
p rivate P a r tic le [] p a r tic le s; // T a b l i c a c z ą s t e c z e k .
p u b lic C o llis io n S y st e m (P a r t ic le [ ] p a r t ic le s )
{ t h is .p a r t ic le s = p a rtic le s; }
p r i v a t e v o i d p r e d i c t C o l l i s i o n s ( P a r t i c l e a, d o u b le l i m i t )
( / * Zobacz o p i s w t e k ś c i e . * / }
p u b l i c v o i d r e d r a w ( d o u b le l i m i t , d o u b le Hz)
{ // Ponowne w y ś w i e t l a n i e w s z y s t k i c h c z ą s t e c z e k .
Std D ra w .cle a rQ ;
f o r ( i n t i = 0 ; i < p a r t i cl e s . 1 e n g t h ; i + + ) p a r t i c l e s [ i ] . d r a w ( ) ;
StdDraw .show (20);
i f (t < lim it )
p q . i n s e r t ( n e w E v e n t ( t + 1.0 / Hz, n u l l , n u l i ) ) ;
)
p u b lic vo id s im u la te (d o u b le l i m i t , d o u b le Hz)
( / * Zobacz n a s t ę p n ą s t r o n ę . * / }
p u b lic s t a t i c vo id m a in ( S t r in g [ ] args)
{
StdDraw .show (O );
in t N = In te g e r.p a r se ln t (a r g s [0 ]);
P a r t i c l e [ ] p a r t i c l e s = new P a r t i c l e [ N ] ;
f o r ( i n t i = 0; i < N; i + + )
p a r t i c l e s [ i ] = new P a r t i cl e ( ) ;
C o l l i s i o n S y s t e m syste m = new C o l i i s i o n S y s t e m ( p a r t i c l e s ) ;
syste m .sim u la te (10 0 00 , 0 .5 );
}
Ta klasa to klient kolejki priorytetowej s y m u lu ją c y ru c h w systemie cząsteczek w czasie.
K lie n t testowy mai n () przyjm uje a rgu m e n t w iersza poleceń N, tw o rz y N lo s o w y c h cząste
czek, tw o rz y obiekt Col 1 i s i onSystem składający się z tych cząsteczek i w y w o łu je metodę
s im u l a t e ( ) , aby prze prow a dzić symulację. Z m i e n n e egze mplarza to kolejka prioryteto wa
u ż y w a n a do symulacji, czas i cząsteczki.
876 KONTEKST
Oparta na zdarzeniach symulacja zderzeń cząsteczek (głów na pętla)
p u b lic v o id sim u la te (d o u b le l i m i t , d o u b le Hz)
{
pq = new M i n P Q < E v e n t > ( ) ;
f o r ( i n t i = 0; i < p a r t i c l e s . l e n g t h ; i+ + )
p re d ic tC o llisio n s(p a rtic le s[i], lim it ) ;
p q . i n s e r t ( n e w E v e n t ( 0 , n u l l , n u l i ) ) ; // Dodaje z d a r z e n i e ponownego
// w y ś w i e t l a n i a .
w h ile ( !p q .isE m p ty ())
{ // P r z e t w a r z a n i e je d n e g o z d a r z e n i a w c e l u p o s u n i ę c i a s y m u l a c j i
// do p rzo du .
Event e ve n t = p q . d e l M i n Q ;
i f ( ¡ e v e n t . i s V a l i d ( ) ) continue;
f o r ( i n t i = 0; i < p a r t i c l e s . l e n g t h ; i++)
p artic le s[i].m o ve (e v e n [Link] e - t) ; // A k t u a l i z o w a n i e c z a s u
t = e [Link] e; // i p o z y c j i c z ą s t e c z e k .
P a rticle a = event.a, b = event.b;
if (a != n u l l && b != n u l l ) a . b o u n c e O f f ( b ) ;
else i f (a != n u l l && b == n u l l ) a . b o u n c e O f f H o r i z o n t a l W a l 1 ( ) ;
else i f (a == n u l l && b != n u l l ) b . b o u n c e O f f V e r t i c a l W a l 1 ( ) ;
else i f (a == n u l l && b == n u l l ) r e d r a w ( l i m i t , H z);
p re d ic tC o llisio n s(a , lim it);
p re d ic t C o l1is i o n s ( b , lim it);
Ta metoda reprezentuje główną część symulacji sterowanej zdarzeniami. Najpierw kolejka
priorytetowa jest inicjowana zdarzeniami reprezentującymi wszystkie przyszłe zderzenia
z udziałem każdej cząsteczki. Następnie główna pętla pobiera zdarzenie z kolejki, aktualizuje
czas i pozycje cząsteczek oraz dodaje nowe zdarzenia, aby odzwierciedlić zmiany.
% java Col 1iso n S y ste m 5 Zderzenie
Symulacja sterowana zdarzeniami 877
Ta symulacja może być podstawą do obliczeń różnych cech systemu, co omówiono
w ćwiczeniach. Przykładowo, jedną z podstawowych właściwości jest ciśnienie wy
wierane przez cząsteczki na ściany. Jednym ze sposobów na obliczenie ciśnienia jest
śledzenie liczby i wagi zderzeń ze ścianami (są to łatwe obliczenia oparte na masie
i prędkości cząsteczki), co pozwala łatwo wyznaczyć łączne ciśnienie. Podobne obli
czenia związane są z temperaturą.
W ydajność Jak opisano to na początku, przy symulacji sterowanej zdarzeniami in
teresuje nas uniknięcie wymagającej obliczeniowo pętli wewnętrznej cechującej sy
mulację sterowaną czasem.
Twierdzenie A. Symulacja sterowana zdarzeniami dla N cząsteczek wymaga
najwyżej N 2 operacji na kolejce priorytetowej przy inicjowaniu i najwyżej N ope
racji na kolejce priorytetowej na zderzenie (potrzebna jest też jedna dodatkowa
operacja na kolejce priorytetowej na każde unieważnione zderzenie).
Dowód. Wynika bezpośrednio z kodu.
Przy stosowaniu standardowej implementacji kolejki priorytetowej z p o d r o z d z i a ł u
2 .4 , gdzie gwarantowany jest logarytmiczny czas na operację, czas potrzebny na ob
sługę zderzeń jest liniowo-logarytmiczny. Dlatego możliwe jest przeprowadzanie sy
mulacji dla dużej liczby cząsteczek.
dotyczy niezliczonych innych dziedzin — od
s y m u l a c j a s t e r o w a n a z d a r z e n ia m i
astrofizyki po robotykę — związanych z modelowaniem fizycznym poruszających się
obiektów. W tych zastosowaniach potrzebne może być rozwinięcie m odelu o inne
rodzaje ciał, o działanie w trzech wymiarach, o wpływ innych sił itd. Z każdym roz
winięciem związane są specyficzne trudności obliczeniowe. Podejście sterowane zda
rzeniami prowadzi do bardziej niezawodnych, precyzyjnych i wydajnych symulacji
niż wiele innych możliwości, a wydajność kolejki priorytetowej opartej na kopcu
umożliwia przeprowadzenie obliczeń, które czasem są niewykonalne w inny sposób.
Symulacje są ważne jako narzędzie pomagające naukowcom zrozumieć cechy
świata naturalnego we wszystkich obszarach nauki i inżynierii. Obszary zastosowań
— od procesów produkcji przez systemy biologiczne i systemy finansowe po złożo
ne struktury inżynieryjne — są zbyt liczne, aby je wymieniać. W wielu zastosowa
niach dodatkowa wydajność, jaką zapewnia kolejka priorytetowa oparta na kopcu
lub wydajny algorytm sortowania, robi dużą różnicę w jakości i możliwym zakresie
symulacji.
878 KONTEKST
Drzewa zbalansowane w r o z d z i a l e 3 . pokazano algorytmy zapewniające do
stęp do elementów w bardzo dużych kolekcjach danych. Rozwiązania te mają istotne
znaczenie praktyczne. Wyszukiwanie jest podstawową operacją na dużych zbiorach
danych, a w wielu środowiskach obliczeniowych spora część zasobów zużywana jest
właśnie na nią. Wraz z pojawieniem się sieci W W W dostępne stały się olbrzymie ilo
ści informacji, które wykorzystuje się do wykonywania zadań. Ważne jest, aby takie
dane m ożna było wydajnie przeszukiwać. W tym podrozdziale opisujemy rozwinię
cie algorytmów dla drzew zbalansowanych ( p o d r o z d z i a ł 3 .3 ). Wersja ta umożliwia
wyszukiwanie zewnętrzne w tablicach symboli, które są przechowywane na dysku lub
w sieci WWW, dlatego mogą być dużo większe niż tablice omawiane do tej pory
(mieszczące się w adresowalnej pamięci). We współczesnych systemach oprogram o
wania zaciera się rozróżnienie na pliki lokalne i strony W W W (elementy te mogą być
przechowywane na zdalnym komputerze), dlatego ilość przeszukiwanych danych
jest praktycznie nieograniczona. Co zaskakujące, przedstawione tu m etody umożli
wiają wyszukiwanie i wstawianie danych w tablicach symboli obejmujących tryliony
lub więcej elementów, a wymagają przy tym tylko czterech lub pięciu referencji do
małych bloków danych.
M odel kosztów Mechanizmy przechowywania danych są bardzo zróżnicowane
i wciąż się zmieniają, dlatego korzystamy z prostego modelu, aby ująć podstawowe
cechy. Używamy nazwy strona do określania ciągłych bloków danych i pojęcia spraw
dzanie do określania pierwszego dostępu do strony. Zakładamy, że dostęp do stro
ny obejmuje wczytanie jej zawartości do pamięci
lokalnej, dlatego kolejne dostępy są stosunkowo
M odel kosztów dla drzew
mało kosztowne. Stroną może być plik na lokal
z b a la n so w a n y c h . P rzy
nym komputerze, strona W W W na zdalnym kom
analizowaniu algorytmów
puterze, część pliku na serwerze itd. Celem jest
wyszukiwania zewnętrzne
opracowanie implementacji wyszukiwania, w któ
go określamy liczbę dostę
rych znalezienie dowolnego klucza wymaga nie
pów do strony (w celu od
wielkiej liczby sprawdzeń. Unikamy konkretnych
czytu lub zapisu).
założeń na temat wielkości strony lub stosunku
czasu potrzebnego na sprawdzanie (przyjmujemy,
że wymaga to komunikacji ze zdalnym urządzeniem) do czasu potrzebnego później
do uzyskania dostępu do elementów z bloku (przyjmujemy, że odpowiada za to lo
kalny procesor). W typowych sytuacjach stosunek ten może wynosić 100, 1000 lub
10 000. Większa precyzja nie jest tu potrzebna, ponieważ algorytmy nie są specjalnie
wrażliwe na różnice w wartościach znajdujących się w uwzględnianym tu zakresie.
Drzewa zbalansowane (b-drzewa) Podejście polega na rozwinięciu drzew 2-3 opi
sanych w p o d r o z d z i a l e 3 .3 , przy czym nowa wersja różni się ważnym elementem
— zamiast przechowywać dane w drzewie, tworzymy drzewo na podstawie kopii klu
czy, a każda kopia klucza powiązana jest z odnośnikiem. Podejście to umożliwia łatwe
oddzielenie indeksu od samej tablicy (podobnie wygląda indeks w książce). Tak jak
w drzewach 2-3, tak i tu narzucane jest górne i dolne ograniczenie liczby par klucz-
Drzewa zbalansow ane 879
odnośnik, które mogą znajdować się w każdym węźle. Ustalamy parametr M (zgodnie
z konwencją jest to liczba parzysta) i tworzymy drzewa wielokierunkowe. W drzewach
każdy węzeł ma najwyżej M - 1 par klucz-odnośnik (zakładamy, że M jest na tyle małe,
iż węzeł o M dzieciach zmieści się na stronie) i przynajmniej M l2 takich par (co two
rzy rozgałęzienia zapewniające, że ścieżki wyszukiwania są krótkie). Wyjątkiem jest
korzeń — może mieć mniej niż M l2 par klucz-odnośnik, przy czym ich liczba musi
wynosić przynajmniej 2. Nazwę tej struktury (b-tree) wymyślili Bayer i McCreight,
którzy w 1970 roku jako pierwsi naukowcy wpadli na pomysł wykorzystania wielokie
runkowych drzew zbalansowanych do wyszukiwania zewnętrznego. Niektórzy stosują
określenie b-drzewa tylko do opisu struktury danych tworzonej przez algorytm zapro
ponowany przez Bayera i McCreighta. Tu używamy tego określenia jak ogólnej nazwy
struktur danych opartych na wielokierunkowych zbalansowanych drzewach wyszuki
wań i stałym rozmiarze strony. Wartość Ai podajemy za pomocą terminologii „drzewo
zbalansowane rzędu M ”. W drzewach zbalansowanych rzędu 4 każdy węzeł ma najwyżej
3 i co najmniej 2 pary klucz-odnośnik. W drzewach zbalansowanych rzędu 6 każdy węzeł
ma najwyżej 5 i co najmniej 3 pary odnośników (wyjątkiem jest korzeń, który może mieć
2 pary klucz-odnośnik) itd. Przyczyna wyjątkowego traktowania korzenia dla większych
M stanie się jasna przy szczegółowym omawianiu algorytmu tworzenia drzewa.
Konwencje Aby zilustrować podstawowe mechanizmy, rozważmy implementację
uporządkowanego typu SET (z kluczami i bez wartości). Pouczającym ćwiczeniem
jest rozwinięcie tego typu do uporządkowanego typu ST w celu powiązania kluczy
z wartościami (zobacz ć w i c z e n i e 6. 1 6 ). Celem jest dodanie m etod add () i con-
ta i ns () dla zbioru kluczy, który może być bardzo duży. Stosujemy klucze uporząd
kowane, ponieważ tworzymy uogólnione drzewa wyszukiwań, które oparte są na
kluczach uporządkowanych. Rozwinięcie przedstawionej implementacji o obsługę
innych operacji na danych uporządkowanych także jest pouczającym ćwiczeniem.
Przy stosowaniu wyszukiwania zewnętrznego indeks często przechowywany jest nie
zależnie od danych. Dla drzew zbalansowanych efekt ten można uzyskać za pomocą
dwóch różnych rodzajów węzłów. Są to:
■ węzły wewnętrzne, w których kopie kluczy są powiązane ze stronami;
■ węzły zewnętrzne, obejmujące referencje do samych danych.
Węzeł o 2 dzieciach
* 1K I 1/
1 1 1 Wewnętrzny węzeł
Klucz pełniący funkcję wartownika o 3 dzieciach
Każdy czerw ony klucz jest kopią /
" I D 1H | | j. m inim alnego klucza poddrzew a - KIQijJ L
Zewnętrzny węzeł Zewnętrzny węzeł ~I I i Zewnętrzny węzeł
o 3 dzieciach o 4 dzieciach
\ /
Wszystkie węzły oprócz korzenia
Klucze klienta (czarne) znajdują mają po 3,4 lub 5 dzieci
się w węzłach zewnętrznych
Struktura drzewa zbalansowanego dla zbioru (M = 6)
880 KONTEKST
Każdy klucz w węźle wewnętrznym powiązany jest z innym węzłem, będącym ko
rzeniem drzewa, które zawiera wszystkie klucze większe lub równe względem da
nego klucza i mniejsze niż następny największy klucz, jeśli taki istnieje. Wygodne
jest stosowanie specjalnego klucza, tak zwanego wartownika, o wartości mniejszej
niż wszystkie pozostałe klucze, i umieszczenie tego klucza w węźle korzenia powią
zanym z drzewem obejmującym wszystkie klucze. Tu tablica symboli nie zawiera
powtarzających się kluczy, ale używamy kopii kluczy (w węzłach wewnętrznych) na
potrzeby wyszukiwania. W przykładach używamy kluczy jednoliterowych i znaku
*, który reprezentuje wartownika, mającego wartość mniejszą niż wszystkie pozo
stałe klucze. Te konwencje pozwalają nieco uprościć kod, dlatego stanowią wygod
ną (i powszechnie stosowaną) alternatywę do mieszania danych z odnośnikam i
w węzłach wewnętrznych (które to rozwiązanie stosowaliśmy w innych drzewach
wyszukiwań).
W yszukiw anie i w staw ianie Wyszukiwanie w drzewach zbalansowanych oparte
jest na rekurencyjnym przeszukiwaniu jedynego poddrzewa, które może obejmować
klucz wyszukiwania. Każde wyszukiwanie kończy się w węźle zewnętrznym, który
zawiera klucz wtedy i tylko wtedy, jeśli dany klucz znajduje się w zbiorze. Ponadto
można zakończyć proces trafieniem po napotkaniu kopii klucza wyszukiwania
w węźle wewnętrznym, jednak tu zawsze kontynuujemy wyszukiwanie do m om entu
dojścia do węzła zewnętrznego, ponieważ łatwiej jest wtedy rozwinąć kod do imple
mentacji opartej na uporządkowanej tablicy symboli (ponadto dla dużego M sytuacja
natrafienia na klucz wyszukiwania w węźle wewnętrznym zdarza się rzadko). Aby
przedstawić precyzyjne informacje, rozważmy wyszukiwanie w drzewie zbalansowa-
nym rzędu 6 . Składa się ono z węzłów o 3 parach klucz-odnośnik, węzłów o 4 parach
klucz-odnośnik, węzłów o 5 parach klucz-odnośnik i czasem węzła o dwóch takich
parach w korzeniu. Przy wyszukiwaniu należy zacząć od korzenia i poruszać się m ię
dzy węzłami, znajdując dla klucza wyszukiwania odpowiedni przedział w danym
węźle i korzystając z odpowiedniego odnośnika do przejścia do następnego węzła.
Ostatecznie proces prowadzi do strony zawierającej klucze, która znajduje się w dol
nej części drzewa. Wyszukiwanie kończymy trafieniem, jeśli klucz wyszukiwania
znajduje się na danej stronie. Jeżeli klucza tam nie ma, wyszukiwanie jest nieudane.
Tu, tak jak w drzewach 2-3, można zastosować kod rekurencyjny do wstawienia n o
wego klucza w dolnej części drzewa. Jeśli nie ma miejsca na klucz, dolny węzeł zostaje
tymczasowo przepełniony (obejmuje sześć par klucz-odnośnik), po czym należy go
podzielić, przechodząc w górę drzewa po wywołaniu rekurencyjnym. Jeżeli węzeł
obejmuje sześć par klucz-odnośnik, trzeba podzielić go na węzeł o dwóch parach
połączony z dwoma węzłami o trzech parach. W innych miejscach drzewa należy
zastąpić dowolny węzeł o k parach połączony z węzłem o sześciu parach przez węzeł
0 (k+ 1 ) parach połączony z dwoma węzłami o trzech parach. Zastąpienie trójki przez
M l2 i szóstki przez Ai powoduje przekształcenie omówienia w opis wyszukiwania
1 wstawiania danych w drzewach zbalansowanych rzędu M oraz prowadzi do nastę
pującej definicji.
Drzewa zbalansowane 881
Definicja. Drzewo zbalansowane rzędu M (gdzie M to parzysta liczba dodatnia)
to drzewo, które albo jest zewnętrznym węzłem o k elementach (o k kluczach
i powiązanych informacjach), albo składa się z wewnętrznych węzłów o k parach
klucz-odnośnik (każdy o k kluczach i k odnośnikach do drzewa zbalansowanego
reprezentującego każdy z k przedziałów wyznaczanych przez klucze), oraz ma
następujące cechy strukturalne — każda ścieżka z korzenia do węzła zewnętrz
nego musi mieć tę samą długość (pełne zbalansowanie), a k musi mieć wartość
między 2 a M - 1 w korzeniu i między M l 2 a M - 1 w każdym innym węźle.
Wyszukiwanie E
Podążanie za tym
odnośnikiem, poniew aż E -
występuje m iędzy * a l<
S
Szukanie E w tym x
węźle zewnętrznym
Wyszukiwanie w drzewie zbalansowanym dla zbioru (M = 6)
Wstawianie A
*1 I 1/1
N o w y klucz (A) pow oduje N o w y klucz (C) pow oduje
przepełnienie i podział przepełnienie i podział
Wstawianie nowego klucza do drzewa zbalansowanego dla zbioru
882 KONTEKST
Reprezentacja Jak opisano, mamy dużą swobodę przy wyborze konkretnych repre
zentacji węzłów w drzewach zbalansowanych. Wybory te są ukryte za interfejsem
API Page, który łączy klucze z odnośnikami do obiektów Page i udostępnia opera
cje potrzebne do sprawdzenia przepełnienia stron, ich podziału i rozróżniania stron
wewnętrznych od zewnętrznych. Obiekt Page m ożna traktować jak przechowywaną
zewnętrznie (w pliku na komputerze lub w sieci W W W ) tablicę symboli. Pojęcia
open i close w interfejsie API dotyczą procesu wprowadzania zewnętrznej strony do
wewnętrznej pamięci i zapisywania zawartości strony w pierwotnej lokalizacji (jeśli
to konieczne). M etoda add () dla stron wewnętrznych to operacja na tablicy symboli
łącząca daną stronę z m inimalnym kluczem drzewa, którego korzeniem jest dana
strona. Metody add() i contains() dla stron zewnętrznych przypominają ich odpo
wiedniki z klasy SET. Siłą napędową każdej implementacji jest m etoda spl i t (), która
dzieli pełną stronę przez przeniesienie M l2 par klucz-wartość o pozycji większej niż
M l 2 do nowego obiektu Page i zwraca referencję do nowej strony. W ć w i c z e n i u
6.15 opisano implementację klasy Page opartą na klasie Bi narySearchST. W tej wersji
drzewa zbalansowane przechowywane są w pamięci, tak jak w innych implementa
cjach wyszukiwania. W niektórych systemach rozwiązanie to sprawdza się też przy
wyszukiwaniu zewnętrznym, ponieważ system pamięci wirtualnej może obsługiwać
referencje dyskowe. Bardziej typowe implementacje spotykane w praktyce obejmują
specyficzny dla sprzętu kod do zapisu i odczytu stron. W ć w i c z e n i u 6.19 zachęcamy
do zastanowienia się nad implementacją klasy Page za pom ocą stron WWW. W tek
ście pomijamy takie szczegóły, co pozwala podkreślić przydatność drzew zbalanso
wanych w różnorodnych kontekstach.
p u b l i c c l a s s Page<Key>
Page ( b ool e a n bottom) Tworzy i otwiera stronę
voi d c l o s e ( ) Z am yka stronę
voi d add(Key key) Umieszcza klucz w zew nętrznej stronie
voi d add( Page p) Otwiera p i um ieszcza w danej stronie w ew nętrznej
element, który łączy z p najm niejszy klucz z p
b o o l ean i s E x t e r n a l () C zy strona je st ze w n ę trzn a ?
bool ean c o n t a i n s ( K e y key) C zy klucz znajduje się na stronie?
Page next (Key key) Zwraca poddrzew o, które pow in n o obejm ować klucz
bool ean i s F u l l () C zy strona je st przepełniona?
Page s p l i t () Przenosi połow ę kluczy o najw yższych pozycjach
z danej strony do nowej strony
I t er a b l e < Ke y > keys Zwraca iteratorpo kluczach ze strony
Interfejs API strony z drzewa zbalansowanego
Drzewa zbalansow ane 883
Po tych przygotowaniach napisanie kodu klasy BTreeSET (strona 884) jest zaskakują
co proste. W metodzie eon ta i ns () należy wykorzystać kod rekurencyjny, który jako
argument przyjmuje obiekt Page i obsługuje trzy przypadła:
a Jeśli strona jest zewnętrzna i klucz znajduje się na stronie, należy zwrócić true.
a Jeśli strona jest zewnętrzna, ale klucz nie znajduje się na stronie, należy zwrócić
fal se.
0 W przeciwnym razie należy rekurencyjnie wywołać metodę dla poddrzewa,
które może obejmować dany klucz.
W metodzie put () należy zastosować to samo rekurencyjne podejście, przy czym
jeśli w trakcie wyszukiwania klucz nie zostanie znaleziony, trzeba wstawić go na dole
drzewa, a następnie podzielić wszystkie pełne węzły przy przechodzeniu w górę.
Wydajność Najważniejszą cechą drzew zbalansowanych jest to, że dla rozsądnych
wartości param etru M koszt wyszukiwania jest w praktycznych zastosowaniach stały.
Twierdzenie B. Wyszukiwanie lub wstawianie w drzewie zbalansowanym stop
nia M o N elementach wymaga od logAfN do logM/2jV próbek. W praktycznych
zastosowaniach wartość ta jest stała.
Dowód. Cecha ta wynika z obserwacji, że wszystkie węzły wewnętrzne drzewa
(czyli węzły inne niż korzeń i węzły zewnętrzne) mają od M l2 do M - 1 odnośni
ków, ponieważ powstały w wyniku podziału pełnego węzła o M kluczach i mogą
tylko rosnąć (przy podziale dzieckaj.W najlepszym przypadku węzły tworzą
drzewo pełne ze współczynnikiem rozgałęziania równym M - 1, co bezpośred
nio prowadzi do podanego ograniczenia. W najgorszym przypadku w korzeniu
znajdują się dwa elementy, z których każdy prowadzi do drzewa pełnego rzędu
M l2. Obliczenie logarytmu o podstawie M prowadzi do uzyskania bardzo m a
łych wartości. Przykładowo, dla M = 1000 wysokość drzewa wynosi poniżej 4 dla
N mniejszego niż 62,5 miliarda.
W typowych sytuacjach m ożna zmniejszyć koszt o jedno sprawdzanie, przechowując
korzeń w pamięci wewnętrznej. Przy przeszukiwaniu dysku lub sieci W W W można
samodzielnie wykonać ten krok przed uruchomieniem aplikacji obejmującej dużą
liczbę wyszukiwań. W pamięci wirtualnej z pamięcią podręczną korzeń jest węzłem,
który z największym prawdopodobieństwem znajdzie się w szybkiej pamięci, ponie
waż jest najczęściej używanym węzłem.
Pam ięć Ciekawe jest też zapotrzebowanie na pamięć na drzewa zbalansowane
w praktycznych zastosowaniach. Z uwagi na sposób tworzenia strony są przynajmniej
w połowie pełne, dlatego w najgorszym przypadku drzewa zbalansowane zajmują
około dwukrotnie więcej pamięci, niż jest to konieczne na klucze, plus dodatkową
pamięć na odnośniki. A. Yao w 1979 roku udowodnił (za pom ocą analiz m atem a
tycznych wykraczających poza zakres tej książki), że jeśli klucze są losowe, średnia
884 KONTEKST
ALGORYTM 6.12. Im plem entacja drzew zbalansow anych dla zbiorów
p u b l i c c l a s s B TreeSET<Key e x t e n d s C o m p a r a b l e < K e y »
{
p r i v a t e Page r o o t = new P a g e ( t r u e ) ;
p u b l i c B T re e S E T ( K e y s e n t i n e l )
{ a d d ( s e n t in e l); }
p u b l i c b o o le an c o n t a i n s ( K e y key)
{ return c o n ta in s( ro o t, key); }
p r i v a t e b o o le a n c o n t a i n s ( P a g e h, Key key)
{
i f ( h . i s E x t e r n a l ()) re tu rn h .c o n ta in s( k e y );
re tu rn c o n t a in s ( h . n e x t ( k e y ) , key);
}
p u b l i c v o i d a d d (K e y key)
{
put ( r o o t , k e y ) ;
i f ( r o o t . i s F u l l ())
{
Page l e f t h a l f = r o o t ;
Page r i g h t h a l f = r o o t . s p l i t ( ) ;
r o o t = new P a g e ( f a l s e ) ;
ro o t.a d d (le fth a lf);
ro o t.a d d (rig h th a lf);
}
}
p u b l i c v o i d a d d (P ag e h, Key key)
{
if ( h . is E x t e r n a l ()) { [Link](key); return; }
Page n e x t = h . n e x t ( k e y ) ;
add(next, key);
i f ( n e x t . is F u ll ())
h .a d d (n e x [Link] lit());
n e x t.c lo se ();
}
}
W tej implementacji wykorzystano w opisany w tekście sposób wielokierunkowe zbalanso-
wane drzewa wyszukiwań. Zastosowano typ danych Page, umożliwiający wyszukiwanie (za
pomocą kluczy połączonych z poddrzewami, w których może znajdować się szukany klucz)
oraz wstawianie (służy do tego test przepełnienia i metoda podziału strony).
111
Drzewa zbalansowane 885
Tworzenie dużego drzewa zbalansowanego
886 KONTEKST
liczba kluczy w węźle wynosi około M ln 2, dlatego mniej więcej 44% pamięci pozo
staje niewykorzystane. Podobnie jak dla wielu innych algorytmów wyszukiwania, tak
i tu losowy model dobrze prognozuje rozkład kluczy występujący w praktyce.
z t w i e r d z e n i a b s ą n i e z w y k l e i s t o t n e i warte przemyślenia.
w n io s k i p ł y n ą c e
Czy odgadłbyś, że można opracować taką implementację wyszukiwania, która po
zwala zagwarantować koszt czterech lub pięciu sprawdzeń przy wyszukiwaniu i wsta
wianiu danych w największych plikach, jakie w praktyce się przetwarza? Drzewa
zbalansowane są powszechnie stosowane, ponieważ pozwalają osiągnąć ten poziom.
W praktyce główną trudnością przy tworzeniu implementacji jest zapewnienie pa
mięci na węzły drzewa zbalansowanego, jednak nawet ten problem coraz łatwiej jest
rozwiązać, ponieważ w typowych urządzeniach dostępna jest coraz większa ilość pa
mięci.
Od razu przychodzą na myśl liczne odmiany abstrakcyjnych drzew zbalansowa-
nych. Jedna z kategorii pozwala zaoszczędzić czas przez umieszczenie w węzłach we
wnętrznych tak wielu referencji do stron, jak to możliwe, co zwiększa współczynnik
rozgałęzienia i spłaszcza drzewo. Inna kategoria umożliwia wydajniejsze przecho
wywanie przez łączenie węzłów z braćmi przed podziałem. Wersję i param etry al
gorytmu można dostosować do konkretnych urządzeń i zastosowań. Choć możliwa
poprawa jest nie większa niż o stały mały czynnik, może okazać się bardzo ważna
w sytuacjach, w których tablica jest bardzo duża lub trzeba przetwarzać bardzo liczne
transakcje — a właśnie w takich sytuacjach drzewa zbalansowane są tak skuteczne.
Tablice przyrostkowe 887
Tablice przyrostkowe Wydajne algorytmy do przetwarzania łańcuchów zna
ków odgrywają kluczową rolę w zastosowaniach komercyjnych i obliczeniach nauko
wych. Zastosowania informatyki w XXI wieku są w coraz większym stopniu oparte
na łańcuchach znaków — od niezliczonych łańcuchów znaków składających się na
strony W W W przeszukiwane przez miliardy użytkowników po rozbudowane bazy
danych z genomami badanymi przez naukowców chcących odkryć tajemnicę życia.
Jak zwykle, pewne klasyczne algorytmy są skuteczne, rozwijane są jednak zaskakują
ce nowe rozwiązania. Dalej omawiamy strukturę danych i interfejs API będące pod
stawą niektórych z tych algorytmów. Zaczynamy od omówienia typowego (i klasycz
nego) problemu z obszaru przetwarzania łańcuchów znaków.
N ajdłuższy pow tarzający się łańcuch znaków Jaki jest najdłuższy podłańcuch po
jawiający się przynajmniej dwukrotnie w danym łańcuchu znaków? Przykładowo,
najdłuższy powtarzający się podłańcuch w łańcuchu znaków "to be or not to be"
to "to be". Zastanów się chwilę nad tym, jak rozwiązałbyś ten problem. Czy potrafisz
znaleźć najdłuższy powtarzający się podłańcuch w łańcuchu o milionach znaków?
Problem ten jest łatwy do opisania i ma wiele ważnych zastosowań, w tym w obsza
rze kompresji danych, kryptografii i wspomaganej komputerowo analizie muzyki.
Standardową techniką stosowaną przy rozwijaniu dużych systemów oprogramowa
nia jest refaktoryzacja kodu. Programiści często tworzą nowe programy przez wy
cinanie i wklejanie kodu ze starszych aplikacji. W dużych programach rozwijanych
przez długi czas zastąpienie powtarzającego się kodu wywołaniami jednej jego ko
pii może znacznie ułatwić zrozumienie i konserwację rozwiązania. Usprawnienie
można wprowadzić przez znalezienie długich, powtarzających się podłańcuchów
w programie. Inne zastosowanie dotyczy biologii obliczeniowej. Czy w danym ge
nomie występują długie identyczne fragmenty? Także tu podstawowym problemem
obliczeniowym jest znalezienie w łańcuchu znaków najdłuższego powtarzającego się
podłańcucha. Naukowców zwykle interesują dużo bardziej szczegółowe pytania (ba
dacze chcą tak naprawdę zrozumieć naturę powtarzających się podłańcuchów), jed
nak — oczywiście — nie łatwiej jest na nie odpowiedzieć niż na podstawowe pytanie
o najdłuższy powtarzający się podłańcuch.
R ozw iązanie oparte na ataku siłow ym W ramach rozgrzewki rozważmy nastę
pujące proste zadanie — w dwóch łańcuchach znaków należy znaleźć najdłuższy
wspólny przedrostek (najdłuższy podłańcuch będący przedrostkiem obu łańcuchów).
Przykładowo, najdłuższy wspólny przed
rostek łańcuchów a c ctg tta ac i accgttaa private s t a ti c in t lcp(String s, String t)
to acc. Kod widoczny po prawej stronie (
. . j . ., . i , i n t N = Ma t h. m i n ( s . l e n g t h ( ) , t . l e n g t h ( ) ) ;
jest przydatnym punktem wyjścia do bar- fQr {int . = 0; . < i++)
dziej skomplikowanych zadań. To rozwią- i f ( s . c h a r A t ( i ) !=’ t . c h a r A t ( i )) r e t u r n i;
zanie działa w czasie proporcjonalnym do r e t u r n N;
długości pasującego fragmentu. Jak jednak ^
znaleźć najdłuższy powtarzający się pod- Najdłuższy wspólny przedrostek dwóch łańcuchów znaków
888 KONTEKST
łańcuch w danym łańcuchu znaków? M etoda 1cp() pozwala natychmiast wymyślić
rozwiązanie oparte na ataku siłowym. Należy porównać podłańcuch zaczynający się
na każdej pozycji i łańcucha z podłańcuchem rozpoczynającym się na każdej innej
pozycji początkowej j i zapisywać najdłuższy znaleziony pasujący fragment. Kod nie
jest przydatny dla długich łańcuchów znaków, ponieważ czas wykonania rośnie co
najmniej kwadratowo wraz z długością łańcucha znaków. Jak zwykle liczba różnych
par i oraz j wynosi N ( N - 1)/2, tak więc liczba wywołań metody 1cp() w tym podej
ściu to - N 2/2. Zastosowanie tego rozwiązania do sekwencji z genomu mającej milio
ny znaków wymaga trylionów wywołań m etody 1 cp (), co jest nieakceptowalne.
R o zw ią za n ie o p a rte n a so rto w a n iu p rzy ro stk ó w Opisane tu sprytne podejście,
w którym w nieoczekiwany sposób wykorzystano sortowanie, jest skutecznym spo
sobem wyszukiwania najdłuższego powtarzającego
Wejściowy łańcuch znaków
się podłańcucha nawet w bardzo długich łańcu 0 1 2 3 4 5 6 7 8 91011121314
chach. Należy użyć m etody su b strin g () Javy do a a c a a g t t t a c a a g c
utworzenia tablicy łańcuchów znaków składających Przyrostki
się z przyrostków s (czyli podłańcuchów zaczy 0 a a c a a g t t t a c a a g c
1 a c a a g t t t a c a a g c
nających się na każdej pozycji i ciągnących się do 2 c a a g t t t a c a a g c
końca), a następnie posortować tę tablicę. Kluczową 3 a a g t t t a c a a g c
4 a g t t t a c a a g c
cechą algorytmu jest to, że każdy podłańcuch jest 5 g t t t a c a a g c
6 t t t a c a a g c
przedrostkiem jednego z przyrostków tablicy. Po 7 t t a c a a g c
sortowaniu najdłuższe powtarzające się podłańcu- 8 t a c a a g c
9 a c a a g c
chy występują w tablicy obok siebie. Dlatego wystar 10 c a a g c
czy raz przejść po posortowanej tablicy i zapisywać 11 a a g c
12 a g c
najdłuższe pasujące przedrostki sąsiednich łańcu 13 9 c
14 c
chów znaków. To podejście jest znacznie wydajniej
sze niż atak siłowy, jednak przed zaimplementowa Posortowane przyrostki
0 a a c a a g t t t a c a a g c
niem i przeanalizowaniem rozwiązania omawiamy 11 a a g c
inne zastosowanie sortowania przyrostków. 3 a a g t t t a c a a g c
9 a c a a g c
1 a c a a g t t t a c a a g c
12 a g c
4 a g t t t a c a a g c
14 C
10 c a a g c
2 c a a g t t t a c a a g c
13 g c
5 g t t t a c a a g c
8 t a c a a g c
7 t t a c a a g c
6 t t t a c a a g c
Najdłuższy powtarzający się podłańcuch
1 9
a a c a a g t t t a c a a g c
Wyznaczanie najdłuższego
powtarzającego się podłańcucha
przez sortowanie przyrostków
Tablice przyrostkowe 889
Indeksow anie łańcucha znaków Przy próbie znalezienia konkretnego podłańcu-
cha w długim tekście — na przykład w edytorze tekstu lub na stronie wyświetlanej
w przeglądarce — wykonujesz wyszukiwanie podłańcucha. Problem ten omówiliśmy
w p o d r o z d z i a l e 5 .3 . Zakładamy tu, że tekst jest stosunkowo długi, i koncentrujemy
się na wstępnym przetworzeniu podłańcucha. Celem jest wydajne znajdowanie tego
podłańcucha w dowolnym tekście. Po wprowadzeniu kluczy wyszukiwania w prze
glądarce wykonujesz wyszukiwanie za pomocą, kluczy w postaci łańcuchów znaków,
co jest tematem p o d r o z d z i a ł u 5 .2 . Wyszukiwarka musi wstępnie sprawdzić indeks,
ponieważ przeglądanie wszystkich stron W W W pod kątem podanych kluczy jest zbyt
kosztowne. Jak opisano to w p o d r o z d z i a l e 3.5 (zobacz klasę Fi 1elndex na stronie
513), najlepiej użyć indeksu odwróconego, łączącego każdy możliwy szukany łań
cuch znaków z wszystkimi
zawierającymi gO stronam i P o k i w a n i e tablicy symboli za pomocą
kluczy w postaci łańcuchów znaków -
WWW. W takiej tablicy należy znaleźć strony zawierające klucz
symboli każdy element to Klucz Wartość
klucz w postaci łańcucha
znaków, a każda wartość
to zbiór wskaźników (każ
dy wskaźnik określa infor
macje potrzebne do znale
it was the best
zienia wystąpienia danego
klucza w sieci WWW; in
formacją może być adres
URL będący nazwą strony
i całkowitoliczbowa pozy
cja na tej stronie). W prak
tyce taka tablica symboli Wyszukiwanie podłapcucha -
byłaby zdecydowanie za należy znaleźć klucz na stronie
duża, dlatego w wyszuki
warkach stosowane są róż Wyidealizowane ujęcie typowego wyszukiwania w sieci WWW
ne zaawansowane algoryt
my do zmniejszania jej wielkości. Jednym z podejść jest porządkowanie stron W W W
według ich wagi (na przykład za pomocą wspomnianego na stronie 519 algorytmu
PageRank) i uwzględnianie tylko stron na wysokich pozycjach. Inny sposób na skró
cenie tablicy symboli w celu umożliwienia wyszukiwania za pom ocą kluczy w postaci
łańcuchów znaków polega na powiązaniu adresów URL ze słowami (podłańcuchami
ograniczonymi odstępami), które są kluczami w przygotowanym indeksie. Wtedy
przy wyszukiwaniu słowa wyszukiwarka może wykorzystać indeks do znalezienia
(ważnych) stron obejmujących klucze wyszukiwania (słowa), a następnie przeprowa
dzić na każdej stronie wyszukiwanie podłańcuchów. Jednak w tej technice program
nie wskaże w tekście słowa "koparka", jeśli szukane jest słowo "arka". W niektórych
sytuacjach warto zbudować indeks, aby ułatwić znalezienie dowolnego podłańcucha
w danym tekście. Rozwiązanie to może być przydatne w badaniach lingwistycznych
890 KONTEKST
ważnych dzieł literackich, przy ana Klucz Wartość
lizie sekwencji genomu będącego i t was the
b e st i
obiektem badań wielu naukowców i t was 1 111•
i t was thu
lub dla popularnej strony WWW. of wi sdom
i t was • i
W idealnych warunkach indeks
i t was ;u
łączy wszystkie możliwe podłań- epoch of
cuchy tekstu z każdą pozycją, na
której się znajdują, co pokazano po
prawej
r 1
stronie. Podstawowy‘ rprob- Wyidealizowane ujęcie indeksu łańcuchów znaków z tekstu
lem z tym rozwiązaniem polega na
tym, że liczba możliwych podłańcuchów jest zbyt duża, aby móc utworzyć w tablicy
symboli element dla każdego podłańcucha (tekst o N znakach obejmuje N(N + 1)/2
podłańcuchów). W tablicy dla przykładu widocznego po prawej stronie trzeba utwo
rzyć elementy dla podłańcuchów b, be, bes, best, best o, best of, e, es, est, e s t o, e st
of, s, st, s t o, s t of, t, t o, t of, o, of i wielu, wielu innych. M ożna wykorzystać sor
towanie przyrostków, aby rozwiązać problem w analogiczny sposób, jak w pierwszej
implementacji tablicy symboli, używając wyszukiwania binarnego ( p o d r o z d z i a ł
3 .1 ). Każdy z N przyrostków należy potraktować jak klucz, utworzyć posortowaną
tablicę kluczy (przyrostków) i zastosować wyszukiwanie binarne do przeszukiwania
tablicy przez porównywanie klucza wyszukiwania z każdym przyrostkiem.
Przyrostki Posortowana tablica przyrostkowa
it was the best of times it was the best of times it was the
t was the best of times it was the it was the
was the best of t imes it was the of times it was the
was the best of ti mes it was the the
as the best of tim es it was the the best of times it was the
s the best of time it was the times it was the
the best of times it was the was the
the best of times it was the was the best of times it was the
he best of times i t was the as the ■select(9)
e best of times it was the as the best of times it was the-*"'
best of times it was the best of times it was the
best of times it w as the . , . e
est of times it wa t he index(9) e best of times it was the
st of times it was the es it was the
t of times it was the est of times it was the
of times it was t he f times it was the
of times it was th he
f times it was the he best of times it was the
times it was the imes it was the
times it was the it was the
imes it was the 20 0 10 it was the best of times it was the
mes it was the mes it was the
es it was the of times it was the
s it was the s it was the
it was the 1cp(20)' s the
it was the s the best of times it was the
t was the st of times it was the
was the t of times it was the
was the t was the
as the t was the best of times it was the
s the rank("th")- the
the the best of times it was the
the times it was the
he was the
e was the best of times it was the
M // f
Przedziały obejmujące " t h"
znalezione przez metodę rank()
w czasie wyszukiwania binarnego
Wyszukiwanie binarne w tablicy przyrostkowej
Tablice przyrostkowe 891
Interfejs A P I i kod kliencki Na potrzeby kodu klienckiego rozwiązującego dwa
opisane problemy utworzyliśmy pokazany poniżej interfejs API. Interfejs obejmuje
konstruktor, metodę le n g th (), m etody s e le c t() i index() (zwracają łańcuch zna
ków i indeks przyrostka z danej pozycji z posortowanej listy przyrostków), metodę
lcp () (zwraca długość najdłuższego wspólnego przedrostka każdych dwóch przy
rostków sąsiadujących na posortowanej liście) oraz metodę rank() (zwraca liczbę
przyrostków mniejszych od danego klucza; w ten sposób używamy jej od m om entu
pierwszego przedstawienia wyszukiwania binarnego w r o z d z i a l e i .). Nazwa tablica
przyrostkowa oznacza tu abstrakcyjną posortowaną listę przyrostków — strukturą
danych nie musi być tu tablica łańcuchów znaków.
p u b l i c c l a s s SuffixArray
SuffixArray ( S t r i ng t e x t ) Tworzy tablicę przyrostkow ą dla łańcucha t e x t
i n t 1e ng t h () Zwraca długość łańcucha t e x t
String s e l e c t f i n t i) Zwraca i - ty elem ent tablicy przyrostkow ej
(dla i p o m ię d zy 0 a N-l)
i n t index( int i) Zwraca indeks elem entu s e l e c t ( i )
(dla i po m ięd zy 0 a N-l)
i n t l ep ( i n t i ) Zwraca długość najdłuższego wspólnego przedrostka
s e l e c t ( i ) i s e l e c t ( i - l ) (dla i p o m ię d zy 0 a N-l )
i n t r ank ( S t r i n g key) Zwraca liczbę przyrostków m niejszych niż klucz key
Interfejs API dla tablicy przyrostkowej
W przykładzie na poprzedniej stronie select (9) to "as the best of times...", in d e x (9)
to 4, a l e p (20) to 10, ponieważ " it was th e b e s t of times..." i " it was t h e " mają
wspólny przedrostek " it was t h e " o długości 10. Wywołanie r a n k ( " t h " ) zwraca war
tość 30. Ponadto zauważmy, że sel e c t( rank ( k e y ) ) to pierwszy możliwy przyrostek na
posortowanej liście przyrostków, którego key jest przedrostkiem. Wszystkie pozostałe
wystąpienia klucza key podane są bezpośrednio za pierwszym (zobacz rysunek na po
przedniej stronie). Za pomocą podanego interfejsu API można natychmiast napisać
kod kliencki pokazany na dwóch następnych stronach. Klasa LRS (strona 8 92 ) znajduje
najdłuższy powtarzający się podłańcuch w tekście podanym w standardowym wejściu.
W tym celu tworzy tablicę przyrostkową, a następnie przegląda posortowane przyrost
ki w celu znalezienia maksymalnej wartości zwróconej przez metodę 1cp (). Klasa KWIC
(strona 893) tworzy tablicę przyrostkową dla tekstu wskazanego za pomocą argumentu
wiersza poleceń, a następnie przyjmuje zapytania ze standardowego wejścia i wyświetla
wszystkie wystąpienia każdego zapytania w tekście (wraz z określoną liczbą znaków
przed każdym wystąpieniem i po nim, aby przedstawić kontekst). Nazwa KWICpochodzi
od angielskiego keyword-in-context (czyli słowo kluczowe w kontekście). Określenie to
istnieje przynajmniej od lat 60. ubiegłego wieku. Prostota i wydajność przedstawionego
kodu klienckiego w typowych zastosowaniach związanych z przetwarzaniem łańcu
chów znaków są zaskakujące. Stanowi to dowód na znaczenie starannego projektowa
nia interfejsu API (i wartość prostego, ale odkrywczego pomysłu).
892 KONTEKST
p u b l i c c l a s s LRS
{
p u b l i c s t a t i c voi d m a i n ( S t r i n g [ ] a r g s )
{
String t e x t = Stdln.readAl1();
int N = te x t. lengthO ;
SufflxArray sa = new S u f f i x A r r a y ( t e x t ) ;
String lrs =
f o r ( i n t i = 1; i < N; i++)
{
i n t length = s a . l c p ( i ) ;
i f (length > l r s . l e n g t h O )
lrs = s a .s e le c t( i).s u b s tr in g (0 , length);
}
[Link](lrs);
}
}
Klient do wyznaczania najdłuższego powtarzającego się podłańcucha
% more [Link]
i t was t h e b e s t o f t i me s i t was t h e w o r s t o f t i mes
i t was t h e age o f wisdom i t was t h e age o f f o o l i s h n e s s
i t was t h e epoch o f b e l i e f i t was t h e epoch o f i n c r e d u l i t y
i t was t h e s e ason o f l i g h t i t was t h e s e a s on o f d a r k n e s s
i t was t h e s p r i n g o f hope i t was t h e w i n t e r o f d e s p a i r
% j a v a LRS < t i n y T a l e . t x t
s t o f t i me s i t was t h e
Tablice przyrostkowe 893
p u b l i c c l a s s KWIC
1
p u b l i c s t a t i c voi d mai n ( S t r i n g [] a r g s )
{
In i n = new I n ( a r g s [ 0 ] ) ;
i n t context = I n t e g e r . p a r s e l n t ( a r g s [1]);
String text = in.readAl1 ( ) .replaceAl1( " \\s + " , "
int N = [Link]();
SuffixArray sa = new SuffixArray ( t e x t ) ;
wh i l e ( S t d l n . h a s N e x t L i n e O )
{
String q = S t d l n . r e a d L i n e O ;
f o r ( i n t i = s a . r a n k ( q ) ; i < N && s a . s e l e c t ( i ) . s t a r t s W i t h ( q ) ; i++)
{
i n t from = Mat h. max(0, s a . i n d e x ( i ) - c o n t e x t ) ;
i n t t o = Ma t h . mi n ( N- l , from + q . l e n g t h ( ) + 2 * c o n t e x t ) ;
[Link]([Link](from, t o ) ) ;
)
[Link]();
}
}
}
Klient do tworzenia indeksu stów kluczowych w kontekście
% j a v a KWIC t a l e . t x t 15
search
o s t g i 1e s s t o s e a r c h f o r c o n t r a b a n d
her unavailing search f o r your fat he
l e and gone i n search of her husband
t pr ovi nces in s e a r c h o f i mp o v e r i s h e
d i s p e r s i n g in search of o t h e r c a r r i
n t h a t bed and s e a r c h t h e s t r a w hold
b e t t e r thing
t i s a f a r f a r b e t t e r t h i n g t h a t i do t h a n
some s e n s e o f b e tte r things else forgotte
was c a p a b l e o f b etter things mr c a r t o n e n t
894 KONTEKST
Im plem entacja Kod na następnej stronie jest prostą implementacją interfejsu API
klasy Su f fix A r ra y . Jej zmienne egzemplarza to tablica łańcuchów znaków i (użyta
w celu skrócenia kodu) zmienna Nprzechowująca długość tej tablicy (długość łańcu
cha znaków i liczbę przyrostków). Konstruktor tworzy tablicę przyrostkową i sortuje
ją, dlatego wywołanie s e l e c t (i) jedynie zwraca wartość s u ff ix e s [i]. Także imple
mentacja metody i ndex () zajmuje jeden wiersz, jest on jednak skomplikowany i wy
nika ze spostrzeżenia, że długość łańcucha znaków z przyrostkiem jest jednoznacznym
wyznacznikiem jego początku. Przyrostek o długości Nzaczyna się na pozycji 0, przyrostek
o długości N-l — na pozycji 1, przyrostek o długości N-2 — na pozycji 2 itd. Dlatego
w wywołaniu i ndex ( i ) wystarczy zwrócić N - su ffixe s [i] , l e n g t h ( ) . Implementację
metody 1 cp() można opracować natychmiast, ponieważ statyczna metoda 1 cp() ze
strony 8 8 7 i metoda ran k () są w zasadzie takie same, jak w implementacji wyszukiwa
nia binarnego dla tablicy symboli (strona 393). Także tu prostota i elegancja implemen
tacji nie powinny ukrywać tego, że jest to skomplikowany algorytm, umożliwiający
rozwiązanie ważnych problemów (talach jak wyszukiwanie najdłuższego powtarzają
cego się podłańcucha), które bez niego wydają się być niewykonalne.
W ydajność Wydajność sortowania przyrostków wynika z tego, że w Javie wyodręb
nianie podłańcuchów wymaga stałej ilości pamięci. Każdy podłańcuch składa się ze
standardowego narzutu na obiekt, wskaźnika do pierwotnego łańcucha i długości.
Dlatego rozmiar indeksu rośnie liniowo względem rozmiaru łańcucha znaków. Jest
to nieco sprzeczne z intuicją, ponieważ łączna liczba znaków w przyrostkach wynosi
-AP/2 i jest funkcją kwadratową wielkości łańcucha znaków. Ponadto należy uwzględ
nić kwadratowe tempo wzrostu przy rozważaniu kosztów sortowania tablicy przyrost
ków. Bardzo ważne jest, aby pamiętać, że podejście to jest skuteczne dla długich łań
cuchów znaków z uwagi na ich reprezentację stosowaną w Javie. Przestawianie dwóch
łańcuchów znaków wymaga przestawienia samych referencji, a nie całych łańcuchów.
Dlatego koszt porównywania dwóch łańcuchów znaków może być proporcjonalny do
długości łańcuchów, jeśli ich wspólne przedrostki są bardzo długie, jednak większość
porównań w typowych sytuacjach kończy się po kilku znakach. Wtedy czas wykonania
sortowania przyrostków jest liniowo-logarytmiczny. Przykładowo, w wielu zastosowa
niach uzasadnione jest przyjęcie modelu losowych łańcuchów znaków.
Twierdzenie C. Za pomocą sortowania szybkiego z podziałem na trzy części
można utworzyć tablicę przyrostkową dla losowego łańcucha znaków o długości N,
wykorzystując średnio pamięć w ilości proporcjonalnej do N i ~2N In N porów
nań znaków.
Omówienie. Ograniczenie pamięciowe jest oczywiste, jednak ogranicze
nie czasowe wynika ze szczegółowych i skomplikowanych rezultatów badań P.
Jacqueta i W. Szpankowskiego. Wynika z nich, że koszt sortowania przyrostków
jest asymptotycznie taki sam, jak koszt sortowania N losowych łańcuchów zna
ków (zobacz t w i e r d z e n i e e na stronie 735).
Tablice przyrostkowe 895
ALGORYTM 6 .1 3 .Tablica przyrostkowa (im plem entacja podstaw ow a)
p u b lic c l a s s SuffixArray
{
p r i v a t e final S t r i n g [] s u f f ix e s ; // T a b l i c a p r z y r o s t k o w a .
p r i v a t e final i n t N; // D ł u g o ś ć ł a ń c u c h a znaków (i t a b l i c y ) .
p u b l i c S u f f i x A r r a y ( S t r i n g s)
{
N = s . lengthO ;
s u ff ix e s = new S t r i ng [N] ;
for ( i n t i = 0; i < N; i + + )
s u ff ix e s [ i ] = [Link] b strin g (i) ;
Q uick3w [Link](suffïxes) ;
}
p u b lic in t le n g t h O { r e t u r n N; }
p u b lic S t r in g s e le c t ( in t i) {r e t u r n s u ff ix e s [i ] ; }
p u b lic in t in d e x (in t i) {r e t u r n N - s u ff ix e s [ i ] . l e n g t h () ; }
p riva te s t a t ic i n t l c p ( S t r i n g s, S t r i n g t)
// Zobacz s t r o n ę 887.
p u b lic in t lc p ( in t i)
{ r e t u r n 1 c p ( s u f f i x e s [ i ] , s u ffix e s [ i -1] ) ; )
p u b lic in t ra n k (S tr in g key)
{ // W y s z u k iw a n ie b i n a r n e ,
i n t l o = 0, hi = N - 1;
w h ile ( l o <= h i )
{
in t mid = l o + (h i - lo ) / 2;
in t cmp = key. compareTo( s u ffix e s [mi d] ) ;
if (cmp < 0) hi = mid - 1;
e lse i f (cmp > 0 ) l o = mid + 1;
e l s e r e t u r n mid;
}
re turn lo ;
Wydajność tej implementacji interfejsu API klasy SuffixArray wynika z tego, że wartości
typu S tri ng są w Javie niezmienne, dlatego podłańcuchy to referencje o stałej wielkości,
a wyodrębnianie podłańcuchów zajmuje stały czas (zobacz opis w tekście).
896 KONTEKST
Usprawnione im plem entacje Dla najgorszego przypadku podstawowa implemen
tacja klasy SuffixArray ma niską wydajność. Przykładowo, jeśli wszystkie znaki są
sobie równe, w sortowaniu należy sprawdzić każdy znak każdego podłańcucha, dla
tego czas rośnie kwadratowo. Dla łańcuchów znaków używanych w przykładach, na
przykład sekwencji genomu lub tekstu
W ejściowy łańcuch znaków
w języku naturalnym, prawdopodobnie a a c a a g t t t a c a a g c
nie sprawi to problemów, jednak algo
Przyrostki najd łu ższeg o pow tó rzen ia (M = 5)
rytm może działać powoli dla tekstów a c a a g
C a a g Każdy występuje przynajmniej
z długimi seriami identycznych zna aa g dwukrotnie jako przedrostek
ków. Inny sposób spojrzenia na problem a g łańcucha znaków z przyrostkiem
związany jest z obserwacją, że koszt zna
lezienia najdłuższego powtarzającego się P o sortow ane przyrostki danych w ejściow ych
a a c a a gt t t a c a a g c
podłańcucha rośnie kwadratowo wzglę a a g c
dem długości tego podłańcucha, ponieważ 3 a a g 11 t a c a a g c
a c a a g c
trzeba sprawdzić wszystkie przedrostki 5 a c a a g t t t a c a a g c
w powtórzeniu (zobacz rysunek po pra a g c
2 a g 111 a c a a gc
wej stronie). Nie stanowi to problemu c
c a a g c
w tekstach w rodzaju książki A Tale of c a a g t t t a c a a g c
Two Cities, gdzie najdłuższe powtórzenie: gc
g t t t a c a a gc
" s dropped because i t would have t a c a a gc
been a bad t h i n g f o r me i n a t t a c a a gc
t 1 1 a c a a gc
w o r l d l y p o i n t o f vie w i " Koszt porównań
wynosi przynajmniej:
ma tylko 84 znaki. Problem jest jednak 1 + 2 + ... + M -M 7 2
poważny przy przetwarzaniu genomu, K oszt d z ia ła n ia k lasy LRS ro ś n ie k w a d ra to w o
gdzie długie powtarzające się podłańcu- w z g lę d e m d łu g o ś c i p o w tó rz e n ia
chy nie są niczym niezwykłym. Jak m oż
na poprawić kwadratową złożoność wyszukiwania powtórzeń? P. Weiner w badaniach
z 1973 roku wykazał, że można zagwarantować liniowy czas rozwiązania problemu
wyszukiwania najdłuższych powtarzających siępodłańcuchów. Algorytm Weinera op
arty jest na tworzeniu drzewa przyrostkowego (inaczej drzewa sufiksowego; jest to
drzewo trie zawierające przyrostki). Drzewa przyrostkowe, z uwagi na liczne wskaź
niki na znak, w wielu praktycznych problemach zajmują jednak zbyt wiele miejsca,
co doprowadziło do powstania tablic przyrostkowych. W latach 90. ubiegłego wieku
U. M anber i E. Myers zaprezentowali liniowo-logarytmiczny algorytm tworzenia tab
lic przyrostkowych i metodę, która wykonuje wstępne przetwarzanie w tym samym
czasie, jaki potrzebny jest na sortowanie przyrostków, a umożliwia wykonanie m e
tody lcp() w stałym czasie. Od tego czasu opracowano kilka działających w czasie
liniowym algorytmów sortowania przyrostków. Po pewnych zmianach implementa
cja Manbera-Myersa może też obsługiwać dwuargumentową wersję m etody 1cp {),
wyszukującą w czasie liniowym najdłuższy wspólny przedrostek dwóch podanych
przyrostków, które nie muszą sąsiadować ze sobą. Jest to znaczne usprawnienie w po
równaniu z prostą implementacją. Uzyskane efekty są zaskakujące, ponieważ osiąg
nięta wydajność przekracza oczeldwania.
Tablice przyrostkowe 897
Twierdzenie D. Za pom ocą tablic przyrostkowych m ożna sortować przyrostki
i wyszukiwać najdłuższe powtarzające się podłańcuchy w czasie liniowym.
Dowód. Omawianie znakomitych algorytmów do wykonywania tych zadań
wykracza poza zakres książki, w witrynie można jednak znaleźć kod, w którym
konstruktor klasy SuffixArray działa w czasie liniowym, a metoda 1cp () obsłu
guje zapytania w czasie stałym.
Implementacja klasy SuffixArray oparta na tych pomysłach umożliwia wydajne roz
wiązanie licznych problemów z obszaru przetwarzania łańcuchów znaków. Wystarczy
zastosować prosty kod kliencki, taki jak w przykładowych klasach LRS i KWIC.
p r z y r o s t k o w e s ą u k o r o n o w a n i e m dziesięcioleci badań, które rozpo
t a b l ic e
częto wraz z powstaniem drzew trie dla indeksów słów kluczowych w kontekście
w latach 60. ubiegłego wieku. Wielu naukowców przez kilka dziesięcioleci pracowało
nad wykorzystaniem opisanych algorytmów w praktyce — od przeniesienia słownika
Oxford English Dictionary do internetu przez pierwsze wyszukiwarki internetowe po
określanie sekwencji ludzkiego genomu. Historia ta pomaga lepiej zrozumieć zna
czenie projektowania i analizowania algorytmów.
898 KONTEKST
Algorytmy dla sieci przepływowych
Dalej omawiamy model grafów, który okazał się
przydatny nie tylko dlatego, że stanowi prosty
sposób rozwiązywania problemów, przydatny
w wielu praktycznych zastosowaniach, ale też
z uwagi na istnienie wydajnych algorytmów roz
wiązywania problemów w ramach tego modelu.
Opisane tu rozwiązanie pokazuje sprzeczność
między dążeniem do tworzenia implementacji
o ogólnych zastosowaniach a chęcią rozwijania
wydajnych rozwiązań konkretnych problemów.
Badania nad algorytmami z obszaru przepły
wów w sieci są fascynujące, ponieważ zbliżają
nas do zwięzłych i eleganckich implementacji
zgodnych z oboma wymienionymi celami. Jak
się okaże, istnieją proste implementacje, które
gwarantują czas wykonania proporcjonalny do
wielomianu opartego na rozmiarze sieci.
Klasyczne rozwiązania problemów z obszaru
sieci przepływowych są powiązane z innymi algo
Przekierowanie jednej
rytm am i dla grafów, omówionymi w r o z d z i a jednostki przepływu
z l->3->5 do l->4->5
l e 4 . Za pomocą opracowanych narzędzi algo
rytmicznych można napisać zaskakująco zwięzłe
programy do rozwiązywania tych problemów.
Jak pokazujemy w wielu innych sytuacjach, do
bre algorytmy i struktury danych mogą prowa
dzić do znacznego skrócenia czasów wykonania.
Wciąż trwają badania nad lepszymi implemen
tacjami i algorytmami, a także odkrywane są
nowe podejścia.
M odel fizyczn y Zaczynamy od wyidealizowa
nego modelu fizycznego, w którym można intui
cyjnie zrozumieć kilka podstawowych pomysłów.
Wyobraźmy sobie zbiór połączonych rur na ropę
o różnej przepustowości i przełączników sterują
cych przepływem na styku rur, jak pokazano to
po prawej stronie. Ponadto załóżmy, że sieć ma
jedno źródło (na przykład pole naftowe) i jedno
ujście (na przykład dużą rafinerię), do którego
ostatecznie prowadzą wszystkie rury. W każdym
wierzchołku przepływ ropy jest zrównoważony,
kiedy ilość wpływającej ropy jest równa ilości
Algorytmy dla sieci przepływowych 899
ropy wypływającej. Przepływ i przepustowość
rur mierzymy w tych samych jednostkach (na W każdym wierzchołku
(z wyjątkiem źródła i ujścia)
przykład w litrach na sekundę). Jeśli w każdym przepływ wejściowy
przełączniku łączna przepustowość rur wej jest rów ny wyjściowemu
ściowych jest równa łącznej przepustowości rur
wyjściowych, nie ma problemu. Wystarczy wy
korzystać pełną przepustowość wszystkich rur.
W przeciwnym razie nie wszystkie rury są pełne, Loka|na równowaga sieci przeptywowej
jednak ropa płynie w sieci kontrolowana przez
ustawienia przełączników na stykach. Przełączniki zapewniają równowagę lokalną
na stykach — ilość ropy wpływająca w każdym styku jest równa ilości ropy wypły
wającej. Rozważmy sieć widoczną na rysunku na poprzedniej stronie. Operatorzy
mogą włączyć przepływ przez otwarcie przełączników na ścieżce 0->l->3->5, która
ma przepustowość dwóch jednostek przepływu, i późniejsze otwarcie przełączników
na ścieżce 0->2->4->5 w celu wprowadzenia do sieci następnej jednostki przepływu.
Ponieważ 0->l, 2->4 i 3->5 są pełne, nie ma możliwości, aby bezpośrednio zwiększyć
przepływ z 0 do 5. Jednak po zmianie ustawienia przełącznika 1 tak, aby przekiero-
wać przepływ w celu zapełnienia odcinka l->4, dostępna staje się przepustowość na
odcinku 3->5, co umożliwia dodanie jednostki na ścieżce 0->2->4->5. Nawet w tej
prostej sieci znalezienie dla przełączników ustawień, które zwiększają przepływ, nie
jest prostym zadaniem. W skomplikowanych sieciach interesuje nas następujące py
tanie — jakie ustawienia przełączników maksymalizują ilość ropy płynącej ze źródła
do ujścia? Można utworzyć bezpośredni model tej sytuacji za pomocą digrafu ważo
nego o jednym źródle i jednym ujściu. Krawędzie w sieci odpowiadają rurom z ropą,
wierzchołki to styki z przełącznikami, które kontrolują ilość ropy płynącej każdą wy
chodzącą krawędzią, a wagi krawędzi odpowiadają przepustowości rur. Zakładamy,
że krawędzie są skierowane, a w poszczególnych rurach ropa może płynąć w tylko
jednym kierunku. Przepływ dla każdej rury jest określony i mniejszy lub równy jej
przepustowości. W każdym wierzchołku zachowana jest równowaga — przepływ wej-
tinyFN .t xt Standardowy Rysunek Rysunek Reprezentacja oparta
rysunek z przepustowościami z przepływami na przepływach
2.0 2.0
3.0 1.0
3.0 2.0
1.0 0.0
1.0 0.0
1.0 1.0
2.0 2.0
3.0 1.0
t
Poziom przepływ u
pow iązany z każdą
krawędzią
Struktura problemu sieci przepływowej
900 KONTEKST
ściowy jest równy wyjściowemu. Ta abstrakcyjna sieć przepływowa jest przydatnym
modelem rozwiązywania problemów, który w różnorodnych sytuacjach można za
stosować bezpośrednio, a w jeszcze liczniejszych — pośrednio. Czasem odwołujemy
się do opisu przepływu ropy przez rury, aby intuicyjnie przedstawić podstawowe za
gadnienia, jednak omówienie w równym stopniu dotyczy dóbr przesyłanych w kana
łach dystrybucji i licznych innych sytuacji. Podobnie jak przy stosowaniu odległości
w algorytmach wyznaczania najkrótszych ścieżek, tak i tu można zrezygnować z in
tuicyjnych fizycznych pomysłów, kiedy jest to wygodne, ponieważ wszystkie definicje,
właściwości i algorytmy są w pełni oparte na modelu abstrakcyjnym, który nie musi
być zgodny z prawami fizyki. Główną przyczyną zainteresowania modelem sieci prze
pływowej jest to, że umożliwia on rozwiązanie przez redukcję licznych innych proble
mów, co pokazano w następnym punkcie.
D efinicje Z uwagi na dużą liczbę zastosowań warto rozważyć precyzyjne definicje
pojęć i zagadnień, które wcześniej nieformalnie przedstawiliśmy.
Definicja. Sieć przepływowa to digraf ważony o dodatnich wagach krawędzi
(wagi te nazywamy przepustowością). Sieć przepływowa st obejmuje dwa specy
ficzne wierzchołki — źródło s i ujście t.
Czasem stwierdzamy, że krawędzie mają nieskończoną przepustowość. Może to
oznaczać, że przepływ nie jest porównywany z przepustowością takich krawędzi.
Można też użyć wartości stosowanej jako wartownik, która jest większa niż wartość
dowolnego przepływu. Łączny przepływ docierający do wierzchołka (suma przepły
wów w krawędziach wejściowych) to przepływ wejściowy, a łączny przepływ wycho
dzący z wierzchołka (suma przepływów w krawędziach wyjściowych) to przepływ
wyjściowy. Różnica między tymi wartościami (przepływ wejściowy minus wyjścio
wy) to przepływ netto. Aby uprościć omówienie, zakładamy, że żadne krawędzie nie
wychodzą z t ani nie wchodzą do s.
Definicja. Przepływ st w sieci przepływowej st to zbiór nieujemnych wartości
powiązanych z każdą krawędzią, nazywanych przepływami krawędzi. Mówimy, że
przepływ jest możliwy, jeśli spełniony jest warunek, zgodnie z którym przepływ
żadnej krawędzi nie jest większy niż przepustowość krawędzi, oraz warunek lokal
nej równowagi, wedle którego przepływ netto w każdym wierzchołku wynosi zero
(z wyjątkiem wierzchołków s i i).
Przepływ wejściowy ujścia określamy jako wartość przepływu st. W t w i e r d z e n i u c
okaże się, że wartość ta jest równa przepływowi wyjściowemu źródła. Na podstawie
tych definicji można w prosty sposób formalnie przedstawić podstawowy problem.
Algorytmy dla sieci przepływowych 901
M aksym alny przepływ st. Dla sieci przepływowej st znajdź przepływ st, taki że
żaden inny przepływ z s do t nie ma większej wartości.
Taki przepływ nazywamy maksymalnym, a problem wyszukiwania go w sieci — prob
lemem przepływu maksymalnego. W niektórych zastosowaniach wystarczy ustalić
samą wartość przepływu maksymalnego, jednak zwykle chcemy poznać przepływy
(wartości przepływów krawędzi) związane z tą wartością.
Interfejsy A P I Interfejsy API FlowEdge i FlowNetwork przedstawione na stronie 902
są prostym rozwinięciem interfejsów API z r o z d z i a ł u 3 . Na stronie 908 omawiamy
implementację klasy FlowEdge opartą na dodaniu do klasy WeightedEdge ze strony
622 zmiennej egzemplarza obejmującej wartość przepływu. Przepływy mają kieru
nek, jednak podstawą klasy FI owEdge nie jest klasa Wei ghtedDi rectedEdge, ponieważ
korzystamy z ogólniejszej abstrakcji — opisanej poniżej sieci rezydualnej. Przy imple
mentowaniu sieci rezydualnej każda krawędź ma pojawiać się na listach sąsiedztwa
obu wierzchołków. Sieć rezydualna umożliwia dodawanie i odejmowanie przepływów
oraz sprawdzanie, czy krawędź jest pełna (nie można dodać przepływów) lub pusta
(nie można odjąć przepływów). Abstrakcja jest zaimplementowana na podstawie opi
sanych dalej metod residualC apacity() i addResidualFlow(). Implementacja klasy
FlowNetwork jest w zasadzie identyczna z implementacją klasy EdgeWeightedGraph
ze strony 623, dlatego jej
nie przedstawiamy. Aby p r i v a t e bo ol ean 1ocal Eq(Fl owNet wor k G, i n t v)
uprościć format plików, { / / Sprawdzani e równowagi l o k a l n e j w wi e r z c h o ł k u v.
stosujemy konwencję, doubl e EPSILON = 1E-11;
doubl e netflow = 0 . 0 ;
zgodnie z którą źródło to f o r (FlowEdge e : G. a d j ( v ) )
0, a ujście — V -l. Przy i f (v == e . f r o m O ) netflow -= e. f l ow() ;
takich interfejsach API else netflow += e. f l owf) ;
cel działania algorytmów
r e t u r n Mat h. abs( net fl ow) < EPSILON;
wyznaczania przepływu }
maksymalnego jest prosty
p r i v a t e bo ol ean i s F e a s i b l e ( F l o w N e t w o r k G)
— należy utworzyć sieć,
{
a następnie przypisać do / / Sp r a wdzani e, czy pr zepł y w w każdej krawędzi j e s t ni euj emny
zmiennych egzemplarza / / i n i e wi ęks zy n i ż pr z e p u s t o wo ś ć ,
określających przepływy f o r ( i n t V = 0; V < G. V( ) ; v++)
f o r (FlowEdge e : G . a d j ( v ) )
wartości, które maksyma i f ([Link] owO < 0 | | [Link]() > e . c a p O )
lizują przepływ w sieci. return false;
Po prawej stronie poka
/ / Sprawdzani e równowagi l o k a l n e j w każdym wi e r z c h o ł k u ,
zano m etody klienckie
f o r ( i n t v = 0; v < G. V( ) ; v++)
do sprawdzania, czy prze i f (v !=s && v != t && !1 o c a l E q ( v ) )
pływ jest możliwy. Zwykle return false;
m ożna to sprawdzić
return true;
w ostatniej operacji algo
}
rytm u wyznaczania prze
pływu maksymalnego. Sprawdzanie, czy przepływ jest możliwy w danej sieci przepływowej
902 KONTEKST
public class FlowEdge
F l owEdge ( i nt v, i n t w, d o ubl e cap)
i n t fromf) Zwraca wierzchołek,
z którego wychodzi dana kraw ędź
i n t t o () Zwraca wierzchołek,
do którego prow adzi dana kraw ędź
i n t o t h e r ( i n t v) Zwraca drugi p u n k t końcowy
doubl e c a p a c i t y ( ) Zwraca pojem ność danej krawędzi
d o u b l e flow() Zwraca p rzepływ dla danej krawędzi
d o u b l e r e s i d u a l C a p a c i t y T o f i n t v) Zwraca rezydualną pojem ność
prow adzącą do v
d o u b l e add Fl owTo( i nt v, doubl e d e l t a ) Dodaje przepływ del t a prow adzący do v
String toString() Zwraca reprezentację
w postaci łańcucha znaków
Interfejs API dla krawędzi sieci przepływowej
p u b l i c c l a s s FlowNetwork
Fl owNet wo r k( i nt V) Zwraca pustą sieć przepływ ów o V wierzchołkach
FI owNetwork(In i n) Tworzy sieć na podstaw ie strum ienia wejściowego
int V() Zwraca liczbę w ierzchołków
int E() Zwraca liczbę krawędzi
voi d addEdge (FI owEdge e) Dodaje e do danej sieci przepływ ów
I t e r a b l e<FlowEdge> a d j f i n t v) Zwraca krawędzie wychodzące z v
I t e r a b l e<Fl owEdge> edges () Zwraca wszystkie krawędzie
z danej sieci przepływ ów
S t r i ng t o S t r i ng () Zwraca reprezentację w postaci łańcucha znaków
Interfejs API dla sieci przepływowej
Reprezentacja sieci przepływowej
Algorytmy dla sieci przepływowych 903
Algorytm Forda-Fulkersona W 1962 roku L. R. Ford i D. R.
Fulkerson opracowali skuteczne rozwiązanie problemu prze
pływu maksymalnego. Jest to uniwersalna metoda stopniowego
zwiększania przepływów na ścieżkach ze źródła do ujścia, będąca
podstawą rodziny algorytmów. W klasycznej literaturze metoda
nazywana jest algorytmem Forda-Fulkersona. Powszechnie stoso
wana jest też nazwa algorytm ścieżki powiększającej. Rozważmy
dowolną ścieżkę skierowaną ze źródła do ujścia w sieci przepły
wowej st. Niech x będzie minimalną spośród niewykorzysty
wanych przepustowości krawędzi na ścieżce. Można zwiększyć Dodawanie jednej
jednostki przepływu
wartość przepływu sieci o co najmniej x, podnosząc przepływ do ścieżki 0->2->3
we wszystkich krawędziach ścieżki o ten poziom. Pierwsza pró
ba wyznaczenia przepływu sieci polega na powtarzaniu opisanej
operacji. Należy znaleźć inną ścieżkę, zwiększyć przepływ dla tej
ścieżki i kontynuować proces do momentu, w którym na wszyst
kich ścieżkach ze źródła do ujścia znajduje się przynajmniej jed Zakłócenie
na pełna krawędź (nie można wtedy bardziej zwiększyć przepły rów now agi
wu w opisany sposób). Algorytm ten w niektórych sytuacjach
Odejmowanie jednej
wyznacza przepływ maksymalny, ale w innych (na przykład dla jednostki przepływu
wprowadzającego przykładu ze strony 898) się nie sprawdza. od l->3 (przejście
przez 3—>1)
Aby usprawnić algorytm tak, żeby zawsze znajdował przepływ
maksymalny, należy rozważyć ogólniejszy sposób zwiększania Zakłócenie
rów now agi
przepływu na ścieżce ze źródła do ujścia, oparty na grafie nie-
skierowanym odpowiadającym sieci. Krawędzie na takiej ścieżce
są skierowane albo do przodu, zgodnie z przepływem (na ścież
ce ze źródła do ujścia przechodzimy krawędzią z wierzchołka
źródłowego do docelowego), albo do tyłu, niezgodnie z przepły
wem (na ścieżce ze źródła do ujścia przechodzimy krawędzią
Dodawanie jednej
z wierzchołka docelowego do źródłowego). W dowolnej ścieżce jednostki przepływu
do ścieżki l->4->5
bez pełnych krawędzi do przodu i bez pustych krawędzi do tyłu
można zwiększyć przepływ sieci przez podniesienie przepływu
w krawędziach do przodu i zmniejszenie go w krawędziach do
tyłu. Poziom, o jaki można zwiększyć przepływ, jest ograni
czony minimalną niewykorzystaną przepustowością krawędzi
do przodu i przepływami krawędzi do tyłu. Opisana ścieżka to
ścieżka powiększająca. Przykładową ścieżkę tego typu pokazano
po prawej. W nowym przepływie przynajmniej jedna z krawędzi
Ścieżka powiększająca
do przodu w ścieżce staje się pełna lub przynajmniej jedna z kra (0 -> 2-> 3->l->4 ->5 )
wędzi do tyłu staje się pusta. Zarysowany tu proces jest podsta
wą klasycznego algorytmu Forda-Fulkersona do wyznaczania
przepływu maksymalnego (metody ścieżki powiększającej). Oto
podsumowanie tego procesu.
904 KONTEKST
Algorytm Forda-Fulkersona do wyznaczania przepływu maksymalnego.
Początkowo przepływ w każdym miejscu powinien być zerowy. Należy zwiększać
go wzdłuż ścieżki powiększającej ze źródła do ujścia (na której nie ma pełnych
krawędzi do przodu lub pustych krawędzi do tyłu) i kontynuować ten proces do
momentu, w którym w sieci nie będzie takich ścieżek.
Co zaskakujące, niezależnie od sposobu doboru ścieżek metoda ta zawsze znajduje
przepływ maksymalny (choć muszą występować pewne warunki techniczne związane
z numerycznymi właściwościami przepływu). Algorytm ten, podobnie jak zachłanny al
gorytm wyznaczania drzew MST ( p o d r o z d z i a ł 4 .3 ) i ogólna metoda wyznaczania naj
krótszych ścieżek ( p o d r o z d z i a ł 4 .4 ), jest przydatnym ogólnym algorytmem, ponieważ
pozwala określić poprawność całej rodziny bardziej specyficznych rozwiązań. Do wybo
ru ścieżek można wykorzystać dowolną metodę. Opracowano kilka algorytmów wyzna
czania sekwencji ścieżek powiększających. Wszystkie te metody prowadzą do uzyskania
przepływu maksymalnego. Algorytmy różnią się ze względu na liczbę wyznaczanych
ścieżek powiększających i koszty znalezienia każdej ścieżki, wszystkie jednak stanowią
implementację algorytmu Forda-Fulkersona i znajdują przepływ maksymalny.
Twierdzenie przepływ u m aksymalnego i przekroju minimalnego Aby pokazać,
że każdy przepływ wyznaczony przez dowolną implementację algorytmu Forda-
Fulkersona rzeczywiście jest przepływem maksymalnym, udowodnimy twierdzenie
przepływu maksymalnego i przekroju minimalnego. Zrozumienie tego twierdzenia jest
kluczowym krokiem na drodze do zrozumienia algorytmów dotyczących sieci przepły
wowych. Jak wskazuje na to nazwa, twierdzenie oparte jest na bezpośrednim związku
między przepływami i przekrojami w sieci, dlatego zaczynamy od zdefiniowania pojęć
związanych z przekrojami. W p o d r o z d z i a l e 4.3 stwierdziliśmy, że przekrój w grafie to
podział wierzchołków na dwa rozłączne zbiory, a krawędź przekroju to taka krawędź,
która łączy wierzchołek z jednego zbioru z wierzchołldem z drugiego. W kontekście
sieci przepływowych definicję te należy doprecyzować w następujący sposób.
Definicja. Przekrój st to przekrój, który powoduje umieszczenie wierzchołka s
w jednym zbiorze, a wierzchołka t — w innym.
Każda krawędź przekroju st jest albo krawędzią st, łączącą wierzchołek ze zbioru obej
mującego s z wierzchołldem ze zbioru obejmującego t, albo krawędzią ts, prowadzą
cą w odwrotnym kierunku. Czasem zbiór krawędzi st przekroju nazywamy zbiorem
przekroju. Przepustowość przekroju st w sieci przepływowej to suma przepustowości
krawędzi st danego przekroju. Przepływ przekroju (ang. flow across) dla przekroju st
to różnica między sumą przepływów w krawędziach st przekroju a sumą przepły
wów w jego krawędziach ts. Po usunięciu wszystkich krawędzi st (zbioru przekroju)
z przekroju st sieci nie istnieje żadna ścieżka z s do t, jednak po dodaniu dowolnej
takiej krawędzi ścieżka może ponownie zaistnieć. Przekroje są odpowiednią abstrak
Algorytmy dla sieci przepływowych 905
cją w wielu zastosowaniach. W m odelu przepływu ropy przekrój zapewnia sposób na
całkowite wstrzymanie przepływu ze źródła do ujścia. Jeśli przepustowość przekroju
potraktujem y jak koszt wykonania tego zadania, zatrzymanie przepływu w najbar
dziej ekonomiczny sposób wymaga rozwiązania następującego problemu.
M inim alny przekrój st. W sieci st znajdź taki przekrój st, aby przepustowość żad
nego innego przekroju nie była mniejsza. Z uwagi na zwięzłość nazywamy taki
przekrój minimalnym, a problem znajdowania go w sieci — problemem przekroju
minimalnego.
W tym ujęciu problemu przekroju minimalnego nie ma słowa o przepływach. Można
odnieść wrażenie, że podane definicje nie dotyczą algorytmu ścieżki powiększającej.
Pozornie wyznaczenie przekroju minimalnego (zbioru krawędzi) wydaje się łatwiej
sze niż obliczenie przekroju maksymalnego (co wymaga przypisania wag do wszyst
kich krawędzi). Jednak problemy przekroju maksymalnego i przekroju minimalnego
są ściśle powiązane. Sama m etoda wyznaczania ścieżki powiększającej stanowi tego
dowód. Oparty jest on na następującej podstawowej zależności między przepływa
mi i przekrojami, która jest bezpośrednim dowodem na to, że lokalna równowaga
w przepływie st oznacza także równowagę globalną (pierwszy wniosek) i określa gór
ne ograniczenie wartości dowolnego przepływu st (drugi wniosek).
T w ie r d z e n ie E. W
dowolnym przepływie st
przepływ przekroju dla każdego przekroju st jest
równy wartości danego przepływu.
P rze p ływ e m
D o w ó d . Niech C będzie zbiorem wierzchołków p rzekroju jest
ró żn ica m ię d z y
obejmującym s, a C( — zbiorem wierzchołków za
p rz e p ły w e m
wierającym t. Twierdzenie wynika bezpośrednio w e jścio w ym
z indukcji na rozmiarze C(. Właściwość z twier a p rz e p ły w e m
w yjścio w ym
dzenia jest z definicji prawdziwa, jeśli Cj obejmuje
tylko t, a po przeniesieniu wierzchołka z C do C(
lokalna równowaga dla tego wierzchołka powo
duje zachowanie właściwości. Przez przenoszenie W a rto ścią p rz e p ły w u
jest p rz e p ły w
wierzchołków w ten sposób m ożna utworzyć do
w e jścio w y d o t
wolny przekrój st.
W n io se k . Przepływ wyjściowy z s jest równy przepływowi wejściowemu do t
(wartości przepływu st).
D o w ó d . Należy przyjąć C równe {s}.
W n io se k . Wartość żadnego przepływu st nie może przekraczać przepustowości
żadnego przekroju st.
906 KONTEKST
T w ie r d z e n ie F ( tw ie r d z e n ie p r z e p ły w u m a k s y m a ln e g o i p rze k r o ju m in i
m a ln e g o ). Niech / będzie przepływem st. Trzy poniższe warunki są równo
znaczne.
i. Istnieje przekrój st, którego przepustowość jest równa wartości przepływ u/
ii. P rzepływ /jest przepływem maksymalnym.
iii. Nie istnieje ścieżka powiększająca powiązana z/
D o w ó d . Z warunku i — zgodnie z wnioskiem z t w i e r d z e n i a e — wynika wa
runek ii. Z warunku ii wynika warunek iii, ponieważ istnienie ścieżki powiększa
jącej oznacza istnienie przepływu o większej wartości, co jest sprzeczne z w arun
kiem m ak sy m aln o śd /
Pozostaje udowodnić, że z w arunku iii wynika warunek i. Niech C będzie
zbiorem wszystkich wierzchołków osiągalnych z s przez ścieżkę nieskierowaną,
która nie obejmuje pełnych krawędzi do przodu lub pustych krawędzi do tyłu.
Niech C obejmuje wszystkie pozostałe wierzchołki. Wtedy t musi znajdować się
w Cf, tak więc (C , C() jest przekrojem st, w którym zbiór przekroju składa się
w całości z pełnych krawędzi do przodu lub pustych krawędzi do tyłu. Przepływ
przekroju w takim przekroju jest równy przepustowości przekroju (ponieważ
krawędzie do przodu są pełne, a krawędzie do tyłu — puste), a także wartości
sieci przepływowej (zgodnie z t w i e r d z e n i e m e ).
W n io s e k (w ła ś c iw o ś ć lic z b c a łk o w ity c h ). Jeśli pojemnościom odpowiadają
liczby całkowite, istnieje całkowitoliczbowy przepływ maksymalny, a algorytm
Forda-Fulkersona go znajdzie.
D o w ó d . Każda ścieżka powiększająca powoduje zwiększenie przepływu o do
datnią liczbę całkowitą (m inimum niewykorzystanych przepustowości krawędzi
do przodu i przepływów krawędzi do tyłu — wszystkie te wartości zawsze są
dodatnim i liczbami całkowitymi).
Można wyznaczyć przepływ maksymalny za pom ocą niecałkowitoliczbowych prze
pływów — nawet jeśli wszystldm przepustowościom odpowiadają liczby całkowite
(nie ma jednak potrzeby rozważać takich przepływów). Spostrzeżenie z wniosku jest
ważne teoretycznie — stosowanie liczb rzeczywistych dla pojemności i przepływów,
co sami zrobiliśmy i co jest często spotykane w praktyce, może prowadzić do nieprzy
jemnych anomalii. W iadomo na przykład, że algorytm Forda-Fulkersona może gene
rować nieskończoną serię ścieżek powiększających, która nie prowadzi do uzyskania
wartości przepływu maksymalnego. Jednak omawiana tu wersja algorytmu zawsze
prowadzi do takiej wartości, nawet jeśli wartości przepustowości i przepływów to
liczby rzeczywiste. Niezależnie od metody wybranej do szukania ścieżek zwiększania
i niezależnie od znalezionych ścieżek zawsze otrzymujemy przepływ, dla którego nie
istnieje ścieżka powiększająca, co oznacza, że jest przepływem maksymalnym.
Algorytmy dla sieci przepływowych 907
Sieć rezydualna Ogólny algorytm Forda-Fulkersona nie narzuca żadnej konkretnej
m etody znajdowania ścieżek powiększających. Jak m ożna znaleźć ścieżkę bez pełnych
krawędzi do przodu i pustych krawędzi do tyłu? Zacznijmy od poniższej definicji.
D e fin ic ja . Dla sieci przepływowej st i przepływu st sieć rezydualna przepływu
obejmuje te same wierzchołki, co pierwotna sieć, i jedną lub dwie krawędzie sieci
rezydualnej (wyznaczane w opisany dalej sposób) na każdą krawędź oryginału.
Dla każdej krawędzi e z v do w z pierwotnej sieci n ie c h / będzie jej przepływem,
a c — przepustowością. Jeśli w a rto ś ć / jest dodatnia, należy dodać do sieci re
zydualnej krawędź w->v o przepustow ości/. Jeżeli w a rto ść /je st mniejsza niż c_,
do sieci rezydualnej trzeba dodać krawędź v->w o przepustowości c - / .
Jeśli krawędź e z v do wjest pusta ( / jest równe 0), w sieci rezydualnej istnieje jedna od
powiadająca jej krawędź v->w o pojemności c . Jeżeli krawędź jest pusta ( /je s t równe
c ), w sieci rezydualnej istnieje jedna odpowiadająca jej krawędź w->v o p ojem ności/.
Jeżeli krawędź nie jest ani pusta, ani pełna, sieć rezydualna obejmuje krawędzie v->w
i w->v o odpowiednich przepustowościach. Przykład pokazano w dolnej części stro
ny. Początkowo reprezentacja sieci rezydualnej może być niezrozumiała, ponieważ
krawędzie odpowiadające przepływowi prowadzą w odwrotnym kierunku względem
przepływu. Krawędzie do przodu reprezentują pozostałą przepustowość (wartość
przepływu, jaką można dodać przy przechodzeniu daną krawędzią), a krawędzie do
tyłu reprezentują przepływ (wartość przepływu, jaką m ożna odjąć przy przechodze
niu określoną krawędzią). Na stronie 908 pokazano metody klasy FI owEdge potrzebne
do zaimplementowania abstrakcyjnej sieci rezydualnej. Te implementacje sprawiają,
że algorytmy mogą pracować na sieci rezydualnej, choć w rzeczywistości sprawdzają
przepustowości i zmieniają przepływy (przez referencje do krawędzi) w krawędziach
klienta. Metody from() i other () umożliwiają przetwarzanie krawędzi prowadzących
w obu kierunkach — wywołanie e . other (v) zwraca punkt końcowy krawędzi e, który
nie jest v. Metody residualCapTo() i addResidual FlowToO składają się na implemen-
Rysunek z przepływ em Reprezentacja przepływ u Sieć rezydualna
908 KONTEKST
Typ danych dla krawędzi z przepływem (sieć rezydualna)
p u b li c c l a s s FlowEdge
{
private final i n t v; // Wierzchołek źródłowy krawędzi
private final i n t w; // Wierzchołek docelowy krawędzi
private final double c a p a c it y ; // Przepustowość,
private double flow; // Przepływ.
p u b li c Flow Edge(int v, i n t w, double ca p a c ity )
{
t h i s . v = v;
t h i s . w = w;
t h i s . c a p a c i t y = c a p a c it y ;
[Link] = 0.0;
}
p u b li c i n t from() { return v; }
p u b li c i n t t o ( ) { re tu rn w; }
p u b li c double c a p a c i t y () { re tu rn c a p a c it y ; }
p u b li c double flow() ( re tu rn flow; }
p u b li c i n t o t h e r ( i n t vertex)
// Taka sama, jak w k l a s i e Edge.
p u b li c double r e s i d u a l C a p a c i t y T o ( i n t vertex)
{
if (vertex == v) re tu rn flow;
e lse i f (vertex == w) re tu rn cap - flow;
e l s e throw new R u ntim eE xcep tio n("N ie sp ójn a krawędź");
}
p u b li c vo id addResidual F lo w T o (in t ve rte x, double de lta )
(
if (vertex == v) flow -= d e lta ;
else i f ( ve rte x == w) flow += d e lta ;
e l s e throw new R u ntim eE xcep tio n("N ie sp ójn a krawędź");
p u b li c S t r i n g t o S t r i n g O
( re tu rn S t r in g . f o r m a t ( "% d - > % d % . 2 f % . 2 f " , v, w, c a p a c it y , flow);
W tej implementacji klasy FI owEdge do implementacji klasy Di rectedEdge dla krawędzi
ważonych z p o d r o z d z i a ł u 4.4 (zobacz stronę 654) dodano zmienną egzemplarza flow
i dwie metody, co pozwala zaimplementować rezydualną sieć przepływową.
Algorytmy dla sieci przepływowych 909
tację sieci rezydualnej. Sieci rezydualne umożliwiają wykorzystanie przeszukiwania
grafu do znalezienia ścieżki powiększającej, ponieważ w sieci rezydualnej każda ścież
ka ze źródła do ujścia bezpośrednio odpowiada ścieżce powiększającej z pierwotnej
sieci. Zwiększenie przepływu w ścieżce oznacza wprowadzenie zmian w sieci rezy
dualnej, na przykład przynajmniej jedna krawędź staje się pełna lub pusta, dlatego
przynajmniej jedna krawędź w sieci rezydualnej zmienia kierunek lub znika (jednak
zastosowanie abstrakcyjnej sieci rezydualnej oznacza, że wystarczy sprawdzić, czy
przepustowość jest dodatnia, i nie trzeba wstawiać ani usuwać krawędzi).
M etoda najkrótszej ścieżki powiększającej Prawdopodobnie najprostszą implemen
tacją algorytmu Forda-Fulkersona jest wykorzystanie najkrótszej ścieżki powiększają
cej (według liczby krawędzi w ścieżce, a nie według przepływu lub przepustowości).
Metodę tę zaproponowali J. Edmonds i R. Karp w 1972 roku. Tu wyszukiwanie ścieżki
powiększającej sprowadza się do zastosowania w sieci rezydualnej wyszukiwania wszerz
w dokładnie tej wersji, jaką opisano w p o d r o z d z i a l e 4.1 (można się o tym przekonać,
porównując poniższą implementację metody hasAugmentingPath() z implementacją
wyszukiwania wszerz z a l g o r y t m u 4.2 ze strony 552). Graf rezydualny jest digra-
fem, a omawiany algorytm służy przede wszystkim do przetwarzania digrafów, o czym
wspomniano na stronie 697. Omawiana metoda stanowi podstawę pełnej implemen
tacji, przedstawionej w a l g o r y t m i e 6.14 na następnej stronie. Jest to zaskakująco
zwięzła implementacja, oparta na opracowanych narzędziach. Rozwiązanie nazywamy
algorytmem przepływu maksymalnego opartym na najkrótszej ścieżce powiększającej.
Ślad działania algorytmu dla przykładowych danych pokazano na stronie 911.
p r i v a t e boolean hasAug m enting Pa th(Flow Netw ork G, i n t s , i n t t )
1
marked = new b o o l e a n [ G . V ( ) ] ; // Czy znana j e s t ś c i e ż k a do danego w i e r z c h o ł k a ?
edgeTo = new FIo w E dge [G .V ( ) ] ; // O s t a t n i w i e r z c h o ł e k na ś c i e ż c e .
Q ue u e < In t e g e r> q = new Q u e u e < I n t e g e r > ( ) ;
m ar ke d[ s] = t r u e ; // O zna cz anie ź r ó d ł a
[Link](s); // i u m ie s z c z a n ie go w k o l e j c e ,
w h i l e ( ! q . i s E m p t y ( ))
{
int v = [Link];
f o r (FlowEdge e : G . a d j ( v ) )
1
in t w = e .o th e r(v );
if ( e . r e s i d u a l C a p a c i t y T o ( w ) > 0 && !marked[w])
{ // D la każdej krawędzi do nie o zn a czo ne go w i e r z c h o ł k a ( s i e c i rezydualnej):
edgeTo[w] = e; // z a p is y w a n ie o s t a t n i e j krawędzi na ś c i e ż c e ;
markedjw] = t r u e ; // o z n a c z a n ie w, ponieważ ś c i e ż k a j e s t znana,
q .e n q u e u e ( w ) ; // i dodawanie w do k o l e j k i .
1
1
1
r e t u r n m ar ked[ t ] ;
Znajdowanie ścieżki powiększającej w sieci rezydualnej przez wyszukiwanie wszerz
910 KONTEKST
ALGORYTM 6.14. Algorytm Forda-Fulkersona do wyznaczania
przepływu maksymalnego oparty na najkrótszej ścieżce powiększającej
p u b li c c l a s s F o rd F u lkerson
{
p r iv a t e boolean[] marked; // Czy rezydualn y g r a f obejmuje ś cie ż k ę s - > v ?
p r iv a t e FlowEdge[] edgeTo; // O sta tn ia krawędź n a jk ró tsz e j ś c i e ż k i s->v.
p r iv a t e double value; // Bieżąca wartość przepływu maksymalnego.
p u b li c FordFulkerson(FlowNetwork G, i n t s, i n t t)
{ // Znajdowanie przepływu maksymalnego z s do t w s i e c i przepływowej G.
w hile (hasAugmentingPath(G, s, t ) )
{ // Dopóki i s t n i e j e ś c ie ż k a powiększająca, na leży j ą zastosować.
// O b li c z a n i e przepustowości w wąskim gard le ,
double b o t t le = [Link] I V E _ IN F IN IT Y ;
f o r ( i n t v = t; v != s; v = e d g e T o [ v ] . o t h e r ( v ) )
b o t t le = M a th .m in (b o ttle , e d g e T o [v ]. r e s i d u a l C a p a c i t y T o ( v ) );
// Zw iększanie przepływu.
f o r ( i n t v = t; v != s; v = e d g e T o [ v ] , o t h e r ( v ) )
edgeTo[v] .addResidual FlowTo(v, b o t t l e ) ;
value += b o t t le ;
}
}
p u b li c double v a lu e ( ) { re t u rn value; }
p u b li c boolean i n C u t ( i n t v) { re t u rn marked[v]; }
p u b li c s t a t i c vo id m a i n ( S t r i n g [ ] a rg s)
(
FlowNetwork G = new FlowNetwork(new I n ( a r g s [ 0 ] ) ) ;
i n t s = 0, t = G.V() - 1;
FordFul kerson maxflow = new FordFul kerson(G, s, t ) ;
S t d O u t . p r i n t l n ( " P r z e p ł y w maksymalny z " + s + " do " + t ) ;
f o r ( i n t v = 0; v < G .V ( ); v++)
f o r (FlowEdge e : G . a d j ( v ) )
i f ( ( v == e . fro m ( ) ) && [Link] > 0)
Std O u t.p rin tln (" " + e ) ;
S t d O u t . p r i n t ln ( " W a r t o ś ć przepływu maksymalnego = " + [Link]( ) ) ;
}
}
Ta implementacja algorytmu Forda-Fulkersona znajduje najkrótszą ścieżkę powiększającą
w sieci rezydualnej, określa przepustowość w wąskim gardle w tej ścieżce i zwiększa prze
pływ ścieżki. Proces ten jest kontynuowany do momentu, w którym nie istnieje żadna ścieżka
ze źródła do ujścia.
Algorytmy dla sieci przepływowych 911
Początkow o pusta sieć Sieć rezydualna
Dodaw anie dwóch jednostek
przepływ u do ścieżki 0 -> l-> 3 -> 5
% java FordFulkerson tin y F N .tx t
Pr zepływ maksymalny z 0 do 5
Dodaw anie jednej jednostki 0- > 2 3 . 0 2 . 0
p rzepływ u do ścieżki 0 -> 2 -> 4 -> 5 0- > l 2.0 2.0
1- >4 1.0 1.0
1->3 3 . 0 1.0
2- >3 1. 0 1.0
2 - > 4 1. 0 1.0
3->5 2.0 2.0
4->5 3.0 2.0
W artość przepływ u maksymalnego = 4 . 0
Dodaw anie jednej jednostki
p rzepływ u do ścieżki 0 -> 2 -> 3 -> l-> 4 -> 5
Ślad działania algorytmu Forda-Fulkersona
opartego na ścieżce powiększającej
KONTEKST
N ajkrótsze ście żki p ow iększające w w iększych sieciach p rzepływ ow ych
W ydajność Większy przykład pokazano na rysunku powyżej. Jak widać na rysunku,
długości ścieżek powiększających tworzą niemalejący ciąg. Ten fakt to pierwszy lducz
do analizy wydajności algorytmu.
T w ie r d z e n ie G. Liczba ścieżek powiększających potrzebnych w opartej na naj
krótszej ścieżce powiększającej implementacji algorytmu Forda-Fulkersona do
wyznaczania przepływu maksymalnego dla sieci przepływowej o V wierzchoł
kach i E krawędziach wynosi co najmniej EV/2.
Z arys d o w o d u . Każda ścieżka powiększająca obejmuje krawędź krytyczną. Jest
to krawędź usuwana z sieci rezydualnej, ponieważ odpowiada albo krawędzi
do przodu, która została całkowicie zapełniona, albo opróżnionej krawędzi do
tyłu. Za każdym razem, kiedy krawędź jest krawędzią krytyczną, długość bieg
nącej przez nią ścieżki powiększającej musi wzrosnąć o dwie jednostki (zobacz
ć w i c z e n i e 6 . 39 ). Ponieważ ścieżka powiększająca ma długość najwyżej V, każda
krawędź może być jedną z najwyżej V!2 ścieżek powiększających, a łączna liczba
ścieżek powiększających wynosi najwyżej EV/2.
Algorytmy dla sieci przepływowych 913
W n io se k . O parta na najkrótszej ścieżce powiększającej implementacja algoryt
m u Forda-Fulkersona do wyznaczania przepływu maksymalnego działa w czasie
proporcjonalnym do VE2/2 (dla najgorszego przypadku).
D o w ó d . Wyszukiwanie wszerz wymaga sprawdzenia najwyżej E krawędzi.
Górne ograniczenie t w i e r d z e n i a g jest bardzo konserwatywne. Przykładowo, graf
pokazany na rysunku w górnej części strony 912 obejmuje 11 wierzchołków i 20 kra
wędzi, dlatego zgodnie z ograniczeniem algorytm przetwarza nie więcej niż 1 1 0 ście
żek powiększających (w rzeczywistości przetwarza ich 14).
Inne im plem entacje Oto inna implementacja algorytmu Forda-Fulkersona, za
sugerowana przez Edmondsa i Karpa. W tej wersji należy zacząć od ścieżki, która
powoduje zwiększenie przepływu o największą wartość. Nazywamy tę metodę al
gorytmem wyznaczania przepływu maksymalnego opartym na ścieżce powiększają
cej o maksymalnej przepustowości. To podejście (a także inne) m ożna zaimplemen
tować za pom ocą kolejki priorytetowej i przez drobną modyfikację opracowanej
przez nas implementacji algorytmu Dijkstry do wyznaczania najkrótszych ścieżek.
Rozwiązanie polega na wybieraniu krawędzi z kolejki priorytetowej w taki sposób,
aby uzyskać maksymalny przepływ, który można dodać do krawędzi do przodu lub
odjąć od krawędzi do tyłu. Można też poszukać najdłuższej ścieżki powiększającej
lub dokonywać losowego wyboru. Przeprowadzenie kompletnych analiz w celu usta
lenia najlepszej m etody jest skomplikowanym zadaniem, ponieważ czas wykonania
algorytmów zależy od:
■ liczby ścieżek powiększających potrzebnych do ustalenia przepływu maksy
malnego;
■ czasu potrzebnego na znalezienie każdej ścieżki powiększającej.
Wartości te mogą znacznie różnić się od siebie. Zależy to od przetwarzanej sieci
i strategii przeszukiwania grafu. Opracowano też kilka innych podejść do wyznacza
nia przepływu maksymalnego. W praktyce niektóre z nich mogą równać się z algo
rytm em Forda-Fulkersona. Opracowanie m odelu matematycznego dla algorytmów
wyznaczania przepływów maksymalnych, który pozwoliłby zweryfikować hipote
zy, jest poważnym wyzwaniem. Analizy takich algorytmów są ciekawą dziedziną,
w której prowadzonych jest wiele badań. W kontekście teoretycznym dla wielu al
gorytmów wyznaczania przepływu maksymalnego określono ograniczenia wydaj
ności dla najgorszego przypadku, jednak ograniczenia te są zwykle znacznie wyższe
niż rzeczywiste koszty obserwowane w aplikacjach, a także nieco wyższe niż b a
nalne dolne ograniczenie (liniowy czas wykonania). Rozbieżność między znanymi
a możliwymi rozwiązaniami jest tu większa niż dla innych problemów omówionych
do tego miejsca.
914 KONTEKST
p r a k t y c z n e z a s t o s o w a n i a algorytmów wyznaczania przepływu maksymalnego
pozostają zarówno sztuką, jak i nauką. Sztuka polega na wyborze strategii, która jest
najwydajniejsza w danej praktycznej sytuacji. Nauka związana jest ze zrozumieniem
podstawowej natury problemu. Czy istnieją nieodkryte jeszcze struktury danych
i algorytmy, które pozwalają rozwiązać problem przepływu maksymalnego w czasie
liniowym? A może uda się udowodnić, że rozwiązanie o tej wydajności nie istnieje?
Stopień wzrostu czasu wykonania dla najgorszego
Algorytm przypadku przy V wierzchołkach i E krawędziach
o całkowitoliczbowych pojemnościach (maksymalnie Q
Forda-Fulkersona oparty
VE2
na najkrótszej ścieżce powiększającej
Forda-Fulkersona oparty
E2log C
na maksymalnej ścieżce powiększającej
Algorytm preflow push EV log (E/V2)
M ożliwy? y+£?
W ydajność algo rytm ó w w yzn aczan ia przepływ u m aksym alnego
Redukcja 915
R e d u k c j a W książce koncentrowaliśmy się na przedstawieniu specyficznego
problem u i późniejszym opracowywaniu algorytmów oraz struktur danych w celu
jego rozwiązania. W kilku sytuacjach (wiele z nich wymieniono dalej) odkryliśmy,
że m ożna wygodnie rozwiązać problem przez przedstawienie go jako wersji innego,
rozwiązanego już problemu. Formalne ujęcie tego spostrzeżenia to cenny punkt wyj
ścia do badania zależności między różnymi opisanymi problemami i algorytmami.
D efin icja . Mówimy, że problem A można zredukować do innego problemu B,
jeśli m ożna wykorzystać algorytm rozwiązujący problem B do opracowania al
gorytm u rozwiązującego problem A.
Jest to znane zagadnienie z obszaru rozwijania oprogramowania — stosowanie m e
tody bibliotecznej do rozwiązania problemu oznacza zredukowanie go do problemu
rozwiązywanego przez tę metodę. W książce nieformalnie nazywamy problemy, któ
re m ożna zredukować do danego, zastosowaniami.
Redukcje w obszarze sortow an ia Na redukcję po raz pierwszy natknęliśmy się
w r o z d z i a l e 2 ., gdzie stwierdziliśmy, że wydajny algorytm sortowania przydaje się do
wydajnego rozwiązywania wielu innych problemów, które na pozór w ogóle nie są po
wiązane z sortowaniem. Rozważyliśmy między innymi wymienione poniżej problemy.
Z najdow anie m ediany. Należy znaleźć m edianę w zbiorze liczb.
R óżne w artości. Należy określić liczbę różnych wartości w zbiorze liczb.
Szeregowanie zadań w celu zm inim alizowania średniego czasu ukończenia pracy.
Zbiór zadań o określonym czasie działania należy uszeregować do wykonania na jed
nym procesorze w taki sposób, aby zminimalizować średni czas ukończenia pracy.
T w ie r d z e n ie H. Do sortowania m ożna zredukować następujące problemy:
■ znajdowanie mediany,
■ zliczanie różnych wartości,
■ szeregowanie w celu zminimalizowania średniego czasu ukończenia pracy.
D o w ó d . Zobacz stronę 357 i ć w i c z e n i e 2 . 5 . 1 2 .
KONTEKST
Przy redukcji trzeba zwrócić uwagę na koszty. Można na przykład znaleźć medianę
dla zbioru liczb w czasie liniowym, jednak po redukcji do sortowania koszt będzie
liniowo-logarytmiczny. Nawet wtedy dodatkowy koszt może być akceptowalny, po
nieważ korzystamy z istniejącej implementacji sortowania. Sortowanie jest wartoś
ciowe z trzech powodów:
■ Jest przydatne samo w sobie.
■ Istnieją wydajne algorytmy do wykonywania tego zadania.
■ Do sortowania m ożna zredukować wiele problemów.
Problem o takich cechach nazywamy modelem rozwiązywania problemów. Dobrze
zaprojektowane modele rozwiązywania problemów, podobnie jak dobrze zbudowa
ne biblioteki oprogramowania, znacznie rozszerzają zakres problemów, które m oż
na wydajnie rozwiązać. Jedną z pułapek przy koncentrowaniu się na modelach roz
wiązywania problemów jest tak zwany młotek Maslowa — jeśli masz tylko młotek,
wszystko wydaje się być gwoździem. Pomysł ten jest powszechnie przypisywany A.
Maslowowi i pochodzi z lat 60. ubiegłego wieku. Przez skoncentrowanie się na kilku
modelach rozwiązywania problemów możemy stosować je jak młotek Maslowa do
rozwiązywania każdego napotkanego zadania. Uniemożliwia to odkrycie lepszych
algorytmów do rozwiązania danego problemu, a nawet nowych modeli rozwiązy
wania problemów. Choć omawiane modele są ważne, skuteczne i użyteczne w wielu
sytuacjach, warto też rozważać inne możliwości.
Redukcje do problem u w yznaczania najkrótszych ścieżek W p o d r o z d z i a l e 4.4
ponownie zetknęliśmy się z redukcją — tym razem w kontekście algorytmów wyzna
czania najkrótszych ścieżek. Rozważyliśmy między innymi opisane poniżej problemy.
W yznaczanie najkrótszych ścieżek z jedn ego źródła w grafach nieskierowanych.
Dla ważonego grafu nieskierowanego o nieujemnych wagach i wierzchołku źród
łowym s zapewnij obsługę zapytań w postaci: Czy istnieje ścieżka z s do danego
wierzchołka docelowego v? Jeśli tak, należy znaleźć najkrótszą ścieżkę tego rodzaju
(o minimalnej wadze).
Szeregowanie równoległych zadań z ograniczeniami pierw szeństw a. Dla zbioru
zadań z danym czasem wykonania i ograniczeniami pierwszeństwa, określającymi,
że pewne zadania trzeba ukończyć przed rozpoczęciem innych, uszereguj zadania
na identycznych procesorach (tylu, ile jest potrzebnych), tak aby możliwie najwcześ
niej zakończyć wykonywanie ostatniego zadania przy zachowaniu ograniczeń.
A rbitraż. Znajdź możliwość arbitrażu w danej tabeli kursów wymiany walut.
Dwa ostatnie problemy wydają się nie być bezpośrednio związane z problemem wy
znaczania najkrótszych ścieżek, pokazaliśmy jednak, że m ożna je skutecznie rozwią
zać za pom ocą najkrótszych ścieżek. Przykłady te, choć ważne, są tylko ilustracją
zagadnienia. Wiele ważnych problemów (zbyt wiele, aby je tutaj omawiać) m ożna
zredukować do problemu wyznaczania najkrótszych ścieżek. Jest to skuteczny i ważny
model rozwiązywania problemów.
Redukcja 917
T w ie r d z e n ie I. Poniższe problemy można zredukować do problemu wyznacza
nia najkrótszych ścieżek w digrafach ważonych:
■ wyznaczanie najkrótszych ścieżek z jednego źródła w grafach nieskierowa-
nych o nieujemnych wagach;
■ szeregowanie zadań równoległych z ograniczeniami pierwszeństwa;
* arbitraż;
■ wiele innych problemów.
P r z y k ła d o w e d o w o d y . Zobacz strony 666, 667 i 692.
R edukcje d o p ro b lem u w yzn a cza n ia p rze p ły w u m aksym alnego Także algorytmy
do wyznaczania przepływu maksymalnego są ważne w szerszym kontekście. Można
pom inąć różne ograniczenia dotyczące sieci przepływowej i rozwiązać powiąza
ne problemy dotyczące przepływów, inne problemy z obszaru przetwarzania sieci
i grafów, a także problemy, które w ogóle nie dotyczą sieci. Oto kilka przykładowych
problemów.
Obsadzanie stanowisk. Uniwersyteckie biuro karier organizuje rozmowy rekruta
cyjne dla grupy studentów w różnych firmach. Rozmowy te prowadzą do złożenia
określonej liczby ofert pracy. Zakładamy, że rozmowa, po której następuje złożenie
oferty pracy, oznacza zainteresowanie studenta stanowiskiem i firmy studentem.
W interesie wszystkich stron leży maksymalizacja liczby obsadzonych stanowisk.
Czy m ożna dopasować każdego studenta do stanowiska? Jaka jest maksymalna
liczba stanowisk, które m ożna obsadzić?
D ystrybucja produktów. Firma wytwarza jeden produkt w kilku fabrykach, po
siada centra dystrybucji, gdzie produkt jest tymczasowo przechowywany, i sklepy
detaliczne, gdzie ma miejsce sprzedaż. Firma musi regularnie rozsyłać produkt
z fabryk przez centra dystrybucji do sklepów detalicznych, korzystając z kanałów
dystrybucji o różnej przepustowości. Czy m ożna przesłać produkt z magazynów
do sklepów w taki sposób, aby wszędzie zrównoważyć podaż z popytem?
Niezawodność sieci. W uproszczonym modelu sieć komputerowa składa się z ze
stawu magistrali, które łączą komputery poprzez przełączniki w taki sposób, że
istnieje ścieżka między dwoma dowolnymi komputerami. Jaka jest m inimalna
liczba magistrali, które trzeba przeciąć, aby rozłączyć pewną parę komputerów?
Problemy te wydają się być niepowiązane ze sobą i z sieciami przepływowymi, ale
wszystkie m ożna zredukować do problemu wyznaczania przepływu maksymalnego.
918 KONTEKST
T w ie r d z e n ie J. Poniższe problemy m ożna zredukować do problemu wyznacza
nia przepływu maksymalnego:
■ obsadzanie stanowisk;
* dystrybucja produktu;
■ niezawodność sieci;
* wiele innych problemów.
P rz y k ła d o w y d o w ó d . Przedstawiamy dowód dla pierwszego punktu (jest to tak
zwany problem maksymalnego skojarzenia w grafie dwudzielnym), a opracowanie po
zostałych dowodów pozostawiamy jako ćwiczenia. W problemie obsadzania stano
wisk należy przejść do problemu wyznaczania przepływu maksymalnego przez doda
nie krawędzi prowadzących od studentów do firm i dodanie wierzchołka źródłowego
z krawędziami skierowanymi do wszystkich studentów oraz wierzchołka ujściowego
z krawędziami skierowanymi z wszystkich firm. Każdej krawędzi należy przypisać
przepustowość 1. Dowolne całkowitoliczbowe rozwiązanie problemu wyznaczania
przepływu maksymalnego dla tej sieci jest rozwiązaniem powiązanego problemu
skojarzeń w grafie dwudzielnym (zobacz wniosek z t w i e r d z e n i a f ) . Skojarzenie
odpowiada dokładnie tym krawędziom między wierzchołkami z obu zbiorów, które
zostały zapełnione do poziomu przepustowości przez algorytm wyznaczania prze
pływu maksymalnego. Po pierwsze, dla sieci przepływowej zawsze istnieje prawid
łowe skojarzenie. Ponieważ każdy wierzchołek ma albo wchodzącą (z ujścia), albo
wychodzącą (do źródła) krawędź o przepustowości 1 , przepływ może w nim wynosić
najwyżej jedną jednostkę, z czego wynika, że każdy wierzchołek przy skojarzeniu zo
stanie uwzględniony najwyżej raz. Po drugie, żadne skojarzenie nie może obejmować
większej liczby krawędzi, ponieważ prowadziłoby do przepływu o wartości większej
niż wartość uzyskana przez algorytm wyznaczania przepływu maksymalnego.
Problem skojarzeń w grafie dw udzielnym Skojarzenie (rozwiązanie)
1 Alicja 7 Adobe Alicja — Amazon
Adobe Alicja Problem ujęty jako sieć przepływ ow a Przepływ m aksym alny
Robert — Yahoo
Amazon Robert Karolina — Facebook
Facebook Dawid
Dawid — Adobe
2 Robert 8 Amazon
Adobe Alicja Eliza — G oogle
Amazon Robert Marek — IBM
Yahoo Dawid
3 Karolina 9 Facebook
Facebook Alicja
Google Karolina
IBM 10 Google
4 Dawid Karolina
Adobe Eliza
Amazon 11 IBM
5 Eliza Karolina
Google Eliza
IBM Marek
Yahoo 12 Yahoo
6 Marek Robert
IBM Eliza
Yahoo Marek
Przykład redukcji problem u skojarzeń w grafie dwudzielnym do sieci przepływ ow ej
Redukcja 919
Na rysunku po prawej stronie pokazano, że algorytm wyznaczania
przepływu maksymalnego oparty na ścieżce powiększającej może
wykorzystać ścieżki s->l->7->t, s->2->8->t, s->3->9->t, s->5->10-
>t, s-> 6~ > ll-> t i s->4->7->l->8->2->12->t do uzyskania skojarze
nia 1-8, 2-12, 3-9, 4-7, 5-1 i 6-10. Tak więc w przykładzie można
dopasować wszystkich studentów do stanowisk. Każda ścieżka po
większająca powoduje zapełnienie jednej krawędzi ze źródła i jednej
krawędzi do ujścia. Zauważmy, że zapełniane krawędzie nigdy nie są
krawędziami do tyłu, dlatego istnieje najwyżej V ścieżek powiększają
cych, a łączny czas wykonania jest proporcjonalny do VE.
W Y Z N A C Z A N IE N A JK R Ó T SZY C H Ś C IE Ż E K I PR Z E PŁ Y W U M A K SY M A LN EG O
to ważne modele rozwiązywania problemów, ponieważ mają te same
cechy, które podano dla sortowania:
■ Są przydatne same w sobie.
■ Istnieją dla nich wydajne algorytmy, rozwiązujące dany problem.
B Można zredukować do nich wiele innych problemów.
To krótkie omówienie jest tylko wprowadzeniem do zagadnienia.
Jeśli uczestniczysz w kursie z badań operacyjnych, poznasz wiele in
nych problemów, które można zredukować do wymienionych i in
nych modeli rozwiązywania problemów.
Program ow alne liniowe Jedną z podstaw badań operacyjnych jest
programowanie liniowe. Technika ta związana jest z redukowaniem
danego problemu do opisanego poniżej ujęcia matematycznego.
Programowanie liniowe. Dla zbioru Ai nierówności liniowych i rów
ności liniowych obejmujących N zmiennych oraz liniowej funkcji celu
z N zmiennych znajdź wartości zmiennych, dla których wartość funk
cji celu jest maksymalna, lub określ, że rozwiązanie nie istnieje.
M aksymalizowanie
Programowanie liniowe jest niezwykle waż
w a rto ści/ + h
z uwzględnieniem nym modelem rozwiązywania problemów,
Ścieżka
ograniczeń: ponieważ: z krawędziami
0 <a< 2 a tyłu
Bardzo wiele ważnych problemów można
0 <b<3
0< c< 3
zredukować do programowania liniowego.
0< d< 1 ’ Istnieją wydajne algorytmy do rozwiązywa
0< e< 1 nia problemów z obszaru programowania
0 </< 1 liniowego.
0 - S — 2 Punkt „przydatny sam w sobie”, który wy
0< h< 3
mienialiśmy przy innych modelach rozwią Ś c ie ż k i p o w ię k sza ją c e p rzy
a = c+d
b = e+f zywania problemów, nie jest tu potrzebny, ko ja rze n iu w g ra fie d w u d zie ln ym
c+ e= g ponieważ tak wiele praktycznych problemów
d+f= h można zredukować do programowania liniowego.
Przykład z dziedziny
programowania liniowego
920 KONTEKST
T w ie r d z e n ie K. Do programowania liniowego m ożna sprowadzić następujące
problemy:
■ wyznaczanie przepływu maksymalnego;
■ wyznaczanie najkrótszych ścieżek;
■ wiele, wiele innych problemów.
P r z y k ła d o w y d o w ó d . Udowodnimy pierwszy punkt, a udowodnienie drugie
go pozostawiamy jako ć w i c z e n i e 6 .4 9 . Rozważmy system nierówności i równo
ści, który obejmuje jedną zmienną odpowiadającą każdej krawędzi, dwie nierów
ności odpowiadające każdej krawędzi i jedną równość odpowiadającą każdemu
wierzchołkowi (za wyjątkiem źródła i ujścia). Wartość zmiennej to przepływ
krawędzi, nierówności określają, że przepływ musi wynosić między 0 a przepu
stowością krawędzi, a zgodnie z równościami łączny przepływ w krawędziach
wchodzących do każdego wierzchołka musi być równy łącznemu przepływowi
w krawędziach wychodzących. Każdy problem wyznaczania przepływu maksy
malnego m ożna przekształcić w ten sposób na problem z dziedziny programowa
nia liniowego, a rozwiązanie łatwo jest przekształcić w drugą stronę. Na rysunku
poniżej szczegółowo przedstawiono przykład.
Problem w yznaczania Rozw iązanie problem u
przepływ u m aksym alnego w yznaczania przepływ u
m aksym alnego
Rozw iązanie
Przepływ maksymalny
Po przekształceniu na
program ow anie liniowe z dziedziny
z 0 do 5
2.0 program owania 0->2 3 .0 2 .0
3 .0 Maksymalizacja wartości
liniow ego 0- > l 2.0 2.0
3 .0 x 3S+x4Sz uwzględnieniem
1. 0 ograniczeń 1-> 4 1 .0 1 .0
1. 0 0 < x o, < 2 *01 = 2 1-> 3 3 .0 1 .0
1.0 0 < x OJ< 3 2-> 3 1 .0 1 .0
(N
X°
II
2.0 0 - x13 2 3 * ,3 = 1 2->4 1 .0 1 .0
3 .0
0 - x,4 - 1 *14 = 1 3- > 5 2 . 0 2 . 0
t 0 < x 23<1 *23=1 4->5 3 .0 2 .0
Przepustowości
0 < x M<1 *2, = 1 W artość przepływ u
NJ
0 < x 35< 2
II
0<x„s <3
rs
II
*01 = * ,3 + *,4
*02 =*23 + *24
*t3 = *23 + *35
*.4 = *24+*45
Przykład redukcji sieci przepływ owej do program owania liniowego
Redukcja 921
Stwierdzenie „wiele, wiele innych problemów” w t w i e r d z e n i u k ma trzy aspekty.
Po pierwsze, można bardzo łatwo rozwinąć model i dodać ograniczenia. Po drugie,
redukcja jest przechodnia, dlatego wszystkie problemy, które m ożna zredukować do
wyznaczania najkrótszych ścieżek i przepływu maksymalnego, można też zredu
kować do programowania liniowego. Po trzecie (w ogólniejszym ujęciu), problemy
optymalizacji dowolnego rodzaju można bezpośrednio sformułować jako problemy
z obszaru programowania liniowego. Pojęcie programowanie liniowe oznacza „ujęcie
problemu optymalizacji w formie problemu z obszaru programowania liniowego”.
Określenie to stosowano, jeszcze zanim zaczęto używać słowa programowanie w kon
tekście komputerów. Równie ważne jak to, że bardzo wiele problemów można zre
dukować do programowania liniowego, jest to, że od dziesięcioleci znane są wydajne
algorytmy z tego obszaru. Najbardziej znany z nich, opracowany przez G. Dantziga
w latach 40. ubiegłego wieku, to tak zwany algorytm sympleksowy. N ietrudno go zro
zumieć (w poświęconej książce witrynie znajduje się jego prosta implementacja).
Bardziej współcześnie algorytm elipsoidalny, zaprezentowany przez L.G. Khachiana
w 1979 roku, doprowadził do powstania w latach 80. ubiegłego wieku metody pu n k
tu wewnętrznego. Udowodniono, że jest ona skutecznym uzupełnieniem algorytmu
sympleksowego dla bardzo rozbudowanych problemów z dziedziny programowa
nia liniowego, rozwiązywanych we współczesnych aplikacjach. Obecnie narzędzia
do rozwiązywania problemów z obszaru programowania liniowego są niezawodne,
dokładnie przetestowane, wydajne i niezbędne do funkcjonowania współczesnych
korporacji. Ponadto znacznie zwiększyły się zastosowania takich narzędzi w kontek
ście naukowym, a nawet w programowaniu aplikacji. Jeśli można przedstawić dany
problem jako problem z dziedziny programowania liniowego, prawdopodobnie uda
się go rozwiązać.
w B A R D ZO K O N K R ET N Y M SENSIE P R O G R A M O W A N IE L IN IO W E JEST M A TKĄ modeli TOZ-
wiązywania problemów, ponieważ tak wiele problemów można zredukować do tego
obszaru. Prowadzi to do pytania, czy istnieje model rozwiązywania problemów jesz
cze bardziej rozbudowany niż programowanie liniowe. Jakiego rodzaju problemów
nie można zredukować do programowania liniowego? Oto przykładowy problem
tego rodzaju.
Rów noważenie obciążenia. Jak uszeregować na dwóch identycznych procesorach
zbiór zadań o określonym czasie wykonania tak, aby zminimalizować czas do
ukończenia wszystkich zadań?
Czy m ożna przedstawić bardziej ogólny model rozwiązywania problemów i wydajnie
rozwiązywać konkretne problemy za pomocą tego modelu? Ten tok myślenia prowa
dzi do nierozwiązywalności, co jest ostatnim tematem poruszanym w książce.
922 KONTEKST
Nierozwiązywalność Algorytmy omówione w książce służą do rozwiązywania
praktycznych problemów, dlatego zużywają sensowną ilość zasobów. Praktyczna uży
teczność większości tych algorytmów jest oczywista, a przy wielu problemach mamy
komfort wyboru spośród kilku wydajnych algorytmów. Niestety, dla wielu innych
występujących w praktyce problemów nie istnieją wydajne rozwiązania. Co gorsze,
dla dużej klasy problemów nie można nawet stwierdzić, czy istnieje wydajne rozwią
zanie. Ten stan rzeczy jest źródłem frustracji dla programistów i projektantów algo
rytmów, którzy nie potrafią znaleźć żadnego wydajnego algorytmu dla szerokiej gru
py praktycznych problemów, oraz dla teoretyków, niezdolnych udowodnić, że dane
problemy są trudne. W tej dziedzinie przeprowadzono wiele badań. Doprowadziły
one do opracowania mechanizmów, które pozwalają stwierdzić, że nowe problemy są
„trudne do rozwiązania” w konkretnym technicznym sensie. Choć duża część zagad
nień z tego obszaru wykracza poza zakres książki, podstawowe kwestie są nietrudne
do opanowania. Przedstawiamy je w tym miejscu, ponieważ każdy programista po
napotkaniu nowego problem u powinien wiedzieć, że dla niektórych problemów nie
znane są algorytmy o gwarantowanej wydajności.
Podstawowe prace Jednym z najpiękniej szych i najbardziej intrygujących odkryć
intelektualnych XX wieku, opracowanym przez A. Turinga w latach 30., jest maszyna
Turinga — prosty model obliczeń, który jest wystarczająco ogólny, aby przedstawić
przy jego użyciu dowolny program komputerowy lub działanie urządzenia oblicze
niowego. Maszyna Turinga to automat skończony, który wczytuje dane wejściowe,
przechodzi ze stanu w stan i zapisuje dane wyjściowe. Maszyny Turinga są podstawą
teoretycznych nauk komputerowych, co wynika z dwóch opisanych poniżej kwestii.
° Uniwersalność. Za pomocą maszyny Turinga m ożna zasymulować działanie
wszystkich możliwych do fizycznego zbudowania urządzeń obliczeniowych
(jest to tak zwana hipoteza Churcha-Turinga). Jest to stwierdzenie na temat
świata naturalnego i nie m ożna go udowodnić (nie m ożna też go sfalsyfiko-
wać). Na rzecz hipotezy przemawia to, że matematycy i badacze z obszaru nauk
komputerowych opracowali liczne modele obliczeń, przy czym dla wszystkich
udowodniono, że są odpowiednikiem maszyny Turinga.
■ Obliczalność. Istnieją problemy, których nie m ożna rozwiązać za pom ocą m a
szyny Turinga (ani, z uwagi na zasadę uniwersalności, przez żadne urządzenie
obliczeniowe). Jest to prawda matematyczna. Problem stopu (żaden program
nie może określić, czy dany program się zatrzyma) jest znanym przykładowym
problemem tego rodzaju.
W omawianym kontekście interesuje nas trzecie zagadnienie, związane z wydajnoś
cią urządzeń obliczeniowych.
n Rozszerzona hipoteza Churcha-Turinga. Tempo wzrostu czasu wykonania pro
gramu przy rozwiązywania problemu na dowolnym urządzeniu obliczeniowym
nie różni się więcej niż o wielomianowy czynnik od tempa wzrostu dla pewnego
program u rozwiązującego ten problem na maszynie Turinga (lub w dowolnym
urządzeniu obliczeniowym).
Nierozwiązywalność 923
Także to stwierdzenie dotyczy świata naturalnego i jest poparte tym, że pracę wszyst
kich znanych urządzeń obliczeniowych m ożna zasymulować za pom ocą maszyny
Turinga przy zwiększeniu kosztów o nie więcej niż czynnik wielomianowy. W ostat
nich latach obliczenia kwantowe sprawiły, że niektórzy naukowcy zaczęli wątpić
w prawdziwość rozszerzonej hipotezy Churcha-Turinga. Większość badaczy zgadza
się, że z praktycznego punktu widzenia twierdzenie to będzie jeszcze przez pewien
czas „bezpieczne”, jednak wielu naukowców ciężko pracuje nad sfalsyfikowaniem
twierdzenia.
Czas w ykonania rosnący w ykładniczo Celem teorii nierozwiązywalności jest od
dzielenie problemów, które m ożna rozwiązać w czasie rosnącym wielomianowo, od
problemów, które w najgorszym przypadku wymagają — prawdopodobnie — czasu
rosnącego wykładniczo. Algorytmy działające w czasie wykładniczym warto trakto
wać tak, jakby dla danych wejściowych o rozmiarze N działały w czasie proporcjonal
nym do 2N (co najmniej). Istota wnioskowania nie zmienia się po zastąpieniu liczby
2 dowolną wartością a > 1. Ogólnie przyjmujemy, że algorytm działający w czasie
wykładniczym nie gwarantuje rozwiązania problemu o rozmiarze na przykład 100
w sensownym czasie, ponieważ niezależnie od szybkości komputera nikt nie może
czekać na wykonanie przez algorytm 2 100 kroków. Postęp technologiczny nie m a zna
czenia w obliczu wykładniczego czasu działania. Superkomputer może być trylion
razy szybszy od liczydła, jednak żadne
z tych urządzeń nie pozwala rozwiązać p u b l i c c l a s s Lo nge st Pa th
problem u wymagającego wykonania {
p r i v a t e b o o le a n !] marked;
2 100 kroków. Czasem linia podziału na p r i v a t e i n t max;
„łatwe” i „trudne” problemy jest wyraź
na. Przykładowo, w p o d r o z d z i a l e 4.1 p u b l i c Lo nge st P a th ( G ra p h G, i n t s, i n t t )
przeanalizowaliśmy algorytm rozwią 1
marked = new bool e a n [ G . V ( ) ] ;
zujący opisany poniżej problem. dfs(G, s, t , 0 ) ;
1
Długość najkrótszej ścieżki. Jaka jest
długość najkrótszej ścieżki z danego p r i v a t e v o id d f s ( G r a p h G, i n t v, i n t t , in t i)
wierzchołka s do danego wierzchoł {
if (v == t && i > max) max = i ;
ka i w określonym grafie? if (v == t ) return;
marked [v] = t r u e ;
Nie zbadaliśmy jednak algorytmów dla
f o r ( i n t w : [Link] ( v ) )
poniższego problemu, który wydaje się if (!m arked[w ]) d f s ( G , w, t , i + 1 ) ;
być taki sam. marked [v] = f a l s e ;
}
Długość najdłuższej ścieżki. Jaka jest
długość najdłuższej prostej ścieżki p u b l i c i n t maxLength()
{ r e t u r n max; }
z danego wierzchołka s do danego
}
wierzchołka t w określonym grafie?
O kreślan ie d łu g o ści najdłuższe j ścieżki w grafie
924 KONTEKST
Kluczowe jest to, że wedle obecnej wiedzy problemy te znajdują się niemal na prze
ciwnych końcach skali trudności. Wyszukiwanie wszerz pozwala rozwiązać pierwszy
problem w czasie liniowym, jednak wszystkie znane algorytmy rozwiązujące drugi
problem działają dla najgorszego przypadku w czasie wykładniczym. Kod w dolnej
części poprzedniej strony to wersja przeszukiwania w głąb wykonująca to zadanie.
Rozwiązanie to przypomina zwykłe przeszukiwanie w głąb, jednak sprawdza wszyst
kie ścieżki proste z s do t w digrafie, aby znaleźć najdłuższą z nich.
Problemy przeszukiw ania Duża rozbieżność między problemami możliwymi do
rozwiązania za pom ocą „wydajnych” algorytmów w rodzaju tych przedstawionych
w książce a problemami, które wymagają znalezienia rozwiązania wśród potencjalnie
wielkiej liczby możliwości, powoduje, że za pom ocą prostego formalnego modelu
m ożna zbadać zależności między problemami różnego typu. Pierwszy krok polega
na określeniu rodzaju analizowanego problemu.
D e fin ic ja . Problem przeszukiwania to problem mający rozwiązania tego rodzaju,
że czas potrzebny do sprawdzenia poprawności każdego rozwiązania jest ogra
niczony wielomianowo względem rozm iaru danych wejściowych. Mówimy, że
algorytm rozwiązuje problem przeszukiwania, jeśli dla dowolnych danych wej
ściowych albo zwraca rozwiązanie, albo informuje, że rozwiązanie nie istnieje.
W górnej części następnej strony przedstawiono cztery konkretne problemy istotne
w kontekście nierozwiązywalności. Są to tak zwane problemy spełnialności. W celu
stwierdzenia, że dany problem jest problemem przeszukiwania, trzeba wykazać, że
rozwiązanie jest wystarczająco dobrze określone, tak aby można wydajnie sprawdzić
jego poprawność. Rozwiązanie problemu przeszukiwania przypomina szukanie igły
w stogu siana, przy czym jedyne założenie jest takie, że zdołasz rozpoznać igłę, kiedy
ją zobaczysz. Przykładowo, jeśli otrzymasz wartości zmiennych w każdym proble
mie spełnialności przedstawionym w górnej części strony 925, będziesz mógł łatwo
stwierdzić, czy każda równość lub nierówność jest spełniona, jednak szukanie takich
wartości to zupełnie inna sprawa. Problemy przeszukiwania często określa się m ia
nem NP. Pochodzenie tej nazwy wyjaśniamy na stronie 926.
D e fin ic ja . NP to zbiór wszystkich problemów przeszukiwania.
NP to nic więcej jak precyzyjne ujęcie wszystkich problemów, które naukowcy, inży
nierowie i programiści aplikacji chcą rozwiązać za pom ocą programów, które kończą
działanie w akceptowalnym czasie.
Nierozwiązywalność
Spełnialność dla równań liniowych. Dla zbioru M równań liniowych obejmują
cych N zmiennych znajdź wartości zmiennych, które spełniają wszystkie równa
nia, lub określ, że takie rozwiązanie nie istnieje.
Spełnialność dla równań nieliniowych (ujęcie program ow ania liniowego za
pom ocą przeszukiw an ia). Dla zbioru M nierówności liniowych obejmujących N
zmiennych znajdź wartości zmiennych, które spełniają wszystkie nierówności, lub
stwierdź, że takie rozwiązanie nie istnieje.
Spełnialność dla nierówności liniowych z w artościam i 0 i 1 (ujęcie program o
w ania liniowego z w artościam i 0 i 1 za pom ocą w yszukiw ania). Dla zbioru M
nierówności liniowych obejmujących N zmiennych całkowitoliczbowych znajdź
przypisanie wartości 0 i 1 do zmiennych, które spełnia wszystkie nierówności, lub
określ, że takie rozwiązanie nie istnieje.
Spełnialność fo rm u ł logicznych. Dla zbioru M równań obejmujących operacje
i oraz lub na N zmiennych logicznych znajdź wartości zmiennych, które spełniają
wszystkie równania, lub stwierdź, że takie rozwiązanie nie istnieje.
W ybrane problem y przeszukiw ania
Inne rodzaje problem ów Problemy przeszukiwania są jednym z wielu sposobów na
scharakteryzowanie zbioru problemów, który stanowi podstawę badań nad nieroz-
wiązywalnością. Inne możliwości to problemy decyzyjne (czy rozwiązanie istnieje?)
i optymalizacyjne (które rozwiązanie jest najlepsze?). Przykładowo, problem określa
nia długości najdłuższych ścieżek, opisany na stronie 923, jest problemem optyma
lizacyjnym, a nie przeszukiwania (na podstawie rozwiązania nie m ożna stwierdzić,
czy uzyskano długość najdłuższej ścieżki). Odpowiadającym tem u problemem prze
szukiwania jest znajdowanie prostej ścieżki łączącej wszystkie wierzchołki (jest to tak
zwany problem ścieżki Hamiltona), a problemem decyzyjnym — zadanie pytania, czy
istnieje ścieżka prosta łącząca wszystkie wierzchołki. Arbitraż, spełnialność formuł
logicznych i wyznaczanie ścieżek Hamiltona to problemy przeszukiwania. Zapytanie
o to, czy istnieje rozwiązanie jednego z tych problemów, prowadzi do problemu de
cyzyjnego. Wyznaczanie najkrótszej i najdłuższej ścieżki oraz przepływu maksymal
nego i programowanie liniowe to problemy optymalizacyjne. Choć problemy prze
szukiwania, decyzyjne i optymalizacyjne technicznie nie są odpowiednikami, zwykle
można je zredukować do siebie (zobacz ć w i c z e n i a 6.58 i 6.59 ), a opisane główne
wnioski dotyczą wszystkich trzech rodzajów problemów.
926 KONTEKST
Ł a tw e p ro b le m y p rzeszu k iw a n ia W definicji problemów NP nie m a nic na temat
trudności znalezienia rozwiązania. W spom niano tylko o sprawdzeniu, że dany wy
nik jest rozwiązaniem. Drugi z dwóch zbiorów problemów będących podstawą ba
dań nad nierozwiązywalnością, zbiór P, związany jest z trudnością znalezienia roz
wiązania. W tym m odelu wydajność algorytmu jest funkcją od liczby bitów użytych
do zakodowania danych wejściowych.
D efin icja . P to zbiór wszystkich problemów przeszukiwania, które można roz
wiązać w czasie wielomianowym.
W definicji niejawnie zawarty jest pomysł, że wielomianowe ograniczenie czasu wy
konania jest ograniczeniem dla najgorszego przypadku. Aby problem należał do zbio
ru P, musi istnieć algorytm gwarantujący rozwiązanie go w czasie wielomianowym.
Zauważmy, że wielomian w ogóle nie jest określony. Z wielomianowym ograniczeniem
są zgodne rozwiązania liniowe, liniowo-logarytmiczne, kwadratowe i sześcienne, dla
tego definicja obejmuje standardowe algorytmy omawiane do tej pory. Czas działa
nia algorytmu zależy od użytego komputera, jednak zgodnie z rozszerzoną hipotezą
Churcha-Turinga nie m a to znaczenia. Według hipotezy istnienie rozwiązania wie
lomianowego na jednym urządzeniu obliczeniowym implikuje istnienie takiego roz
wiązania na dowolnym innym urządzeniu. Sortowanie należy do zbioru P, ponieważ
— na przykład — sortowanie przez wstawianie działa w czasie proporcjonalnym do N 2
(istnienie liniowo-logarytmicznych algorytmów sortowania nie jest w tym kontekście
ważne). Do zbioru należy też algorytm wyznaczania najkrótszych ścieżek, określania
spełnialności równania liniowego i wiele innych. Istnienie wydajnego algorytmu roz
wiązującego problem jest dowodem na to, że problem należy do zbioru P. Ujmijmy to
inaczej — P jest niczym więcej jak precyzyjnym ujęciem wszystkich problemów, które
naukowcy, inżynierowie i programiści aplikacji rozwiązują za pomocą programów,
dla których można zagwarantować ukończenie pracy w rozsądnym czasie.
N ied eterm in izm Litera N w nazwie NP dotyczy niedeterminizmu. Chodzi tu o to,
że — teoretycznie — jednym ze sposobów na zwiększenie mocy komputerów jest
wbudowanie w nie niedeterminizmu. Należy sprawić, że kiedy algorytm będzie m u
siał dokonać wyboru, zdoła „odgadnąć” właściwe rozwiązanie. Na potrzeby om ó
wienia możemy przyjąć, że algorytm dla niedeterministycznej maszyny „zgaduje”
rozwiązanie, a następnie sprawdza, czy jest ono prawidłowe. W maszynie Turinga
wbudowanie niedeterm inizmu jest proste — wystarczy zdefiniować dwa różne stany
będące następnikiem danego stanu, a jako rozwiązanie uznać wszystkie dozwolone
ścieżki do pożądanego wyniku. Nawet jeśli niedeterminizm jest matematyczną fik
cją, jest przydatnym pomysłem. Przykładowo, w p o d r o z d z i a l e 5.4 wykorzystaliśmy
niedeterm inizm jako narzędzie do projektowania algorytmów. Algorytm do dopa
sowywania do wzorca na podstawie wyrażeń regularnych oparty jest na wydajnym
symulowaniu pracy maszyny niedeterministycznej.
Nierozwiązywalność 927
A lgorytm
Problem Dane wejściowe Opis Przykład Rozwiązanie
w ielom iano w y
Znaleźć ścieżkę prostą
Ścieżki
Graf G przechodzącą przez 0-2-1-3
H am iltona
każdy wierzchołek
R ozkładanie Liczba Znaleźć nietrywialny
97605257271 8784561
na czynniki całkowita x czynnik liczby x
Spełnialność M zmiennych Przypisanie do x - y< 1
nierówności x=1
o wartościach zmiennych wartości 2x - z < 2
liniowych y= 1
z wartościami
O il spełniających c +y > 2
z=0
O il N nierówności nierówności z>0
W szystkie
problem y Zobacz tabelę poniżej
ze zbioru P
Przykładow e problem y NP
Algorytm
Problem Dane wejściowe Opis Przykład Rozwiązanie
wielomianowy?
W yznaczanie Graf G
Znaleźć najkrótszą Przeszukiwanie
najkrótszych Wierzchołki 0-3
ścieżek st
ścieżkę z s do t wszerz
sit
Znaleźć permutację,
w której elementy Sortowanie
Sortowanie Tablica a 2,8 8,5 4,1 1,3 302 1
z a są w kolejności przez scalanie
rosnącej
Spełnialność Przypisanie do
M zmiennych Eliminacja x + y = 1,5 x = 0,5
równości zmiennych wartości
liniowej
N równań Gaussa 2x - y - 0 7=1
spełniających równości
Przypisanie do x - y< 1,5
Spełnialność x = 2,0
M zmiennych zmiennych wartości Algorytm 2x - z < 0
nierówności y= 1,5
liniowej
N nierówności spełniających elipsoidalny x + y > 3,5
z = 4,0
nierówności z > 4,0
Przykładowe problemy P
928 KONTEKST
Podstawowe p ytanie Niedeterminizm daje tak wielkie możliwości, że wydaje się
niemal absurdem poważne traktowanie go. Po co zastanawiać się nad magicznym
narzędziem, które sprawia, że trudne problemy wydają się banalne? Oto odpowiedź
— choć niedeterm inizm wydaje się dawać olbrzymie możliwości, nikt nie udowod
nił, że pomaga rozwiązać jakikolwiek konkretny problem! Ujmijmy to inaczej — nikt
nie znalazł choćby jednego problemu, dla którego m ożna udowodnić, że należy do
zbioru NP, ale nie do P (nie udowodniono nawet, że istnieje problem dla którego m oż
na to udowodnić). Dlatego poniższe pytanie pozostaje otwarte:
czy P = NP?
Pytanie to po raz pierwszy postawił K. Gödel w słynnym, napisanym w 1950 roku
liście do J. von Neumanna. Do tej pory matematycy i badacze z dziedziny nauk kom
puterowych nie potrafią sobie z nim poradzić. Inne sposoby ujęcia tego pytania rzu
cają światło na podstawową naturę problemu.
■ Czy istnieją jakiekolwiek trudne do rozwiązania problemy przeszukiwania?
B Czy możliwe byłoby wydajniejsze rozwiązanie niektórych problemów przeszuki
wania, gdyby udało się zbudować niedeterministyczne urządzenie obliczeniowe?
Brak odpowiedzi na te pytania jest niezwykle frustrujący, ponieważ liczne ważne prob
lemy praktyczne należą do zbioru NP, natomiast nie wiadomo, czy należą do zbioru P
(najlepsze znane dla nich algorytmy deterministyczne działają w czasie wykładniczym).
Gdyby udało się udowodnić, że problem nie należy do zbioru P, można by zrezygnować
z poszukiwań wydajnych rozwiązań danego problemu. Ponieważ dowód nie istnieje,
możliwe, że uda się odkryć wydajne algorytmy. Prawie nikt nie wierzy w to, że P = NP.
Włożono wiele wysiłku w udowodnienie przeciwnej tezy, jednak zrealizowanie tego
celu nadal jest otwartym problemem badawczym w dziedzinie nauk komputerowych.
R edukcje w ielom ianow e Na stronie 915 opisano, że aby wykazać, iż problem A m oż
na zredukować do innego problemu B, należy pokazać, że możliwe jest rozwiązanie
dowolnego egzemplarza problemu A w trzech krokach:
■ przekształcając go na egzemplarz problemu B,
■ rozwiązując egzemplarz problemu B,
■ przekształcając rozwiązanie problemu B na rozwiązanie problemu A.
Jeśli m ożna wydajnie wykonać przekształcenia (i rozwiązać B), to m ożna wydajnie
rozwiązać A. W obecnym kontekście słowo wydajny używane jest w najsłabszym m oż
liwym sensie — należy rozwiązać A, rozwiązując najwyżej wielomianową liczbę eg
zemplarzy B i stosując przekształcenia wymagające najwyżej wielomianowego czasu.
W tym przypadku mówimy, że A m ożna zredukować wielomianowo do B. Wcześniej
stosowaliśmy redukcję w celu przedstawienia modeli rozwiązywania problemów.
Modele te pozwalają znacznie zwiększyć zakres problemów, które można rozwiązać
za pom ocą wydajnych algorytmów. Tu korzystamy z redukcji w inny sposób — aby
udowodnić, że problem jest trudny do rozwiązania. Jeśli wiadomo, że problem A jest
trudny do rozwiązania i m ożna go wielomianowo zredukować do B, także B musi
być trudny do rozwiązania. W przeciwnym razie gwarancje wielomianowego czasu
rozwiązania B prowadziłyby do gwarancji wielomianowego czasu wykonania A.
Nierozwiązywalność 929
Problem spełnialności form uł logicznych
T w ie r d z e n ie L. Spełnialność formuł logicz (.x'l lu bx2lu bx3) i
nych można zredukować wielomianowo do (xtlubx'2lu b x}) i
spełnialności nierówności liniowych z liczbami (x jlu b x '2lubx'3) i
(xj lub x'2 lub x3)
całkowitymi 0 i 1 .
Zapis w postaci problem u spełnialności
D o w ó d . Dla problemu spełnialności formuł nierów ności liniow ych z liczbam i całkow itym i 0 i 1
logicznych należy zdefiniować zbiór nierówno c ( to 1 wtedy
i tylko wtedy,
ści, w którym jedna zmienna o wartości 0 lub 1 jeśli pierwsza
odpowiada każdej zmiennej logicznej i każdej klauzula jest
spelnialna
klauzuli, co pokazano w przykładzie po pra
wej stronie. W ten sposób m ożna przekształcić
rozwiązanie problemu spełnialności nierów c2>x,
c2 > 1 - x 2
ności liniowych z liczbami całkowitymi 0 i 1 c2> x 3
na rozwiązanie problemu spełnialności formuł c , < x , + (1 - X 2) + X 3
logicznych przez przypisanie każdej zmiennej
c2 > l - x ,
logicznej wartości prawda, jeśli odpowiadająca c3> 1 - x 2
jej zm ienna całkowitoliczbowa to 1 , i wartości c3> l - x 3
fałsz, jeśli wartość powiązanej zmiennej to 0 . Cj £ ( 1 - x j ) + X 2 + (1 - x3)
C4 > 1 - x ,
c4 > 1 - x 2
W n io se k . Jeśli problem spełnialności jest tru d
ny do rozwiązania, to samo dotyczy program o c <(1 - x , ) + (1 —X ) + X
wania liniowego z liczbami całkowitymi.
s < c, s to I wtedy
s<c2 . i tylko wtedy,
Jest to ważne stwierdzenie na tem at względnej tru d jeśli wszystkie
s<c2
ności rozwiązania obu problemów nawet w obliczu eto I
braku precyzyjnej definicji określenia trudne do roz s > c , + c , + c, + c , - 3
wiązania. W tym kontekście „trudne do rozwiąza
Przykład redukcji spełnialności formuł
nia” oznacza „poza zbiorem P”. Ogólnie stosujemy logicznych do spełnialności
słowo nierozwiązywalne do opisu problemów, które nierówności liniowych
z liczbami całkowitymi 0 i 1
nie znajdują się w zbiorze P. Począwszy od opubli
kowanej w 1972 roku przełomowej pracy R. Karpa,
naukowcy wykazali dla dosłownie dziesiątek tysięcy
problemów z różnych obszarów zależności redukcyjne tego rodzaju. Ponadto zależ
ności te oznaczają znacznie więcej niż powiązanie między poszczególnymi proble
mami, co opisujemy poniżej.
N P-zupełność O wielu, wielu problemach wiadomo, że należą do zbioru NP, ale
prawdopodobnie nie należą do zbioru P. Oznacza to, że m ożna łatwo sprawdzić, czy
dane rozwiązanie jest poprawne, ale — mimo znacznych wysiłków — nikt nie zdo
łał opracować wydajnego algorytmu do znajdowania rozwiązania. Co zaskakujące,
wszystkie z tych licznych problemów mają pewną dodatkową cechę, która stanowi
przekonujący dowód na rzecz tego, że P * NP.
co
KONTEKST
D e fin ic ja . Problem przeszukiwania A jest NP-zupełny, jeśli wszystkie problemy
ze. zbioru NP można zredukować wielomianowo do A.
Definicja ta umożliwia poprawienie definicji określenia „trudne do rozwiązania”
— oznacza ono „nierozwiązywalne, chyba że P = NP”. Jeśli którykolwiek NP-zupełny
problem można rozwiązać w czasie wielomianowym na maszynie deterministycznej,
dotyczy to wszystkich problemów w zbiorze NP (a więc P = NP). Dlatego można przy
jąć, że ogólne niepowodzenie naukowców przy szukaniu wydajnych algorytmów dla
problemów z tego zbioru jest równoznaczne z niepowodzeniem próby udowodnie
nia, że P = NP. Oznacza to, że nie powinniśmy spodziewać się znalezienia algorytmów
o gwarantowanym wielomianowym czasie działania. O większości praktycznych
problemów wyszukiwania wiadomo, że albo należą do zbioru P, albo są NP-zupełne.
Twierdzenie C ooka-Levina Redukcja pozwala wykorzystać NP-zupełność jedne
go problemu do wnioskowania na temat NP-zupełności innego. Jednak redukcji nie
m ożna zastosować w jednej sytuacji — do udowodnienia, że pierwszy problem jest
NP-zupełny. Taki dowód przeprowadzili niezależnie S. Cook i L. Levin na początku
lat 70. ubiegłego wieku.
T w ie r d z e n ie M ( tw ie r d z e n ie C o o k a -L e v in a ). Problem spełnialności formuł
logicznych jest NP-zupełny.
N ie z w y k le k rótki z a r y s d o w o d u . Celem jest pokazanie, że jeśli istnieje dzia
łający w czasie wielomianowym algorytm dla problemu spełnialności formuł lo
gicznych, to wszystkie problemy ze zbioru NP można rozwiązać w takim czasie.
Niedeterministyczna maszyna Turinga potrafi rozwiązać dowolny problem ze
zbioru NP, dlatego pierwszy krok w dowodzie wymaga opisania każdej cechy tej
maszyny w kategoriach formuł logicznych, takich jak pojawiające się w problemie
spełnialności formuł logicznych. W ten sposób można powiązać każdy problem ze
zbioru NP (problemy te można przedstawić jako programy na niedeterministyczną
maszynę Turinga) z pewną wersją spełnialności (programem przekształconym na
formułę logiczną). Rozwiązanie problemu spełnialności odpowiada symulacji dzia
łania danego programu w maszynie dla określonych danych wejściowych, uzysku
jemy więc rozwiązanie egzemplarza danego problemu. Dalsze szczegóły dowodu
znacznie wykraczają poza zakres książki. Na szczęście, potrzebny jest tylko jeden
dowód — dużo łatwiej jest zastosować redukcję, niż udowodnić NP-zupełność.
Twierdzenie Cooka-Levina w połączeniu z późniejszymi tysiącami redukcji wielomia
nowych z problemów NP-zupełnych pozostawia dwie możliwości — albo P = NP i nie
istnieją nierozwiązywalne problemy przeszukiwania (wszystkie problemy przeszuki
wania można rozwiązać w czasie wielomianowym), albo P =£ NP i istnieją nierozwiązy
walne problemy przeszukiwania (niektórych takich problemów nie można rozwiązać
Nierozwiązywalność 931
w czasie wielomianowym). Problemy NP-zupełne są częste
w ważnych naturalnych praktycznych zastosowaniach, co
stanowi istotny powód do szukania dobrych algorytmów
rozwiązujących te problemy. Brak dobrego algorytmu dla
któregokolwiek z tych problemów jest mocnym dowodem
na to, że P =£ NP. Większość naukowców uważa, że tak właś
nie jest. Jednak to, że dla żadnego z omawianych problemów
nie udowodniono, iż nie należy do P, można interpretować
jako podstawę podobnego pośredniego dowodu, przedsta
wionego na poprzedniej stronie. Niezależnie od tego, czy
P = NP, ważnym w praktyce faktem jest to, że najlepszy zna
ny algorytm dla dowolnego problemu NP-zupelnego działa
dla najgorszego przypadku w czasie wykładniczym.
Klasyfikowanie problemów Aby udowodnić, że problem przeszukiwania należy do P,
trzeba opracować wielomianowy algorytm do rozwiązania go, na przykład przez re
dukcję do problemu, o którym wiadomo, że należy do P. W celu udowodnienia, że
problem ze zbioru NP jest NP-zupełny, trzeba pokazać, że pewien problem NP-zupełny
m ożna zredukować wielomianowo do danego (czyli że wielomianowy algorytm dla
nowego problemu m ożna wykorzystać do rozwiązania problemu NP-zupełnego, a na
stępnie do rozwiązania wszystkich problemów ze zbioru NP). W ten sposób wykazano
NP-zupełność tysięcy problemów, podobnie jak zrobiliśmy to dla programowania li
niowego w t w i e r d z e n i u l . Lista na stronie 932, obejmująca kilka problemów opisa
nych przez Karpa, jest reprezentatywna, ale obejmuje tylko małą część znanych prob
lemów NP-zupełnych. Określenie problemu jako łatwego (w zbiorze P) lub trudnego
do rozwiązania (NP-zupełne) może być:
■ proste — znany algorytm eliminacji m etodą Gaussa pozwala udowodnić, że
problem spełnialności równań liniowych należy do P;
■ skomplikowane, ale nie trudne — o p r a c o w a n i e d o w o d u p o d o b n e g o d o t e g o
z t w ie r d z e n ia l w y m a g a n ie c o d o ś w ia d c z e n ia i p r a k ty k i, je d n a k s a m d o w ó d
je s t ła tw y d o z ro z u m ie n ia ;
■ niezwykle trudne — długo nie można było określić, do jakiej kategorii nale
ży programowanie liniowe, jednak algorytm elipsoidalny Khachiana pozwolił
udowodnić, że problem ten należy do P;
■ na razie niewykonalne — wciąż nie określono kategorii na przykład dla problemów
izomorfizmu grafów (znajdowania dla dwóch grafów takiego sposobu przemiano
wania wierzchołków jednego z nich, aby grafy były identyczne) i rozkładania na
czynniki (znajdowania dla danej liczby całkowitej nietrywialnego czynnika).
Jest to bogata i aktywnie rozwijana dziedzina badań, w której wciąż pojawiają się tysiące
prac naukowych rocznie. Jak wskazuje na to kilka ostatnich pozycji na liście na stronie
932, badania te związane są z wieloma obszarami. Przypomnijmy, że definicja zbioru
NP dotyczy problemów, które naukowcy, inżynierowie i programiści chcą rozwiązywać
w sensownym czasie. Wszystkie takie problemy wymagają sklasyfikowania!
932 KONTEKST
W ybrane znan e problem y N P-zupełne
Spełnialność fo rm u ł logicznych. Dla zbioru M równań obejmujących N zm ien
nych logicznych znajdź wartości zmiennych, przy których spełnione są wszystkie
równania, lub stwierdź, że takie wartości nie istnieją.
P rogram ow anie liniow e z w ykorzystaniem liczb całkowitych. Dla zbioru M nie
równości liniowych obejmujących N zmiennych całkowitoliczbowych znajdź war
tości zmiennych, przy których spełnione są wszystkie nierówności, lub stwierdź,
że takie wartości nie istnieją.
R ów now ażenie obciążenia. Jak na dwóch procesorach uszeregować zbiór zadań
o określonym czasie trwania tak, aby ukończyć je wszystkie w czasie T?
Pokrycie w ierzchołkowe. Na podstawie grafu i liczby całkowitej C znajdź zbiór
C wierzchołków, taki że każda krawędź w grafie jest incydentna do przynajmniej
jednego wierzchołka ze zbioru.
Ścieżka H am iltona. Znajdź w grafie ścieżkę prostą, która przechodzi przez każdy
wierzchołek dokładnie raz, lub stwierdź, że taka ścieżka nie istnieje.
Z w ijanie białek. Na podstawie poziomu energii M znajdź zwiniętą trójwymiaro
wą konformację białka, mającą energię potencjalną mniejszą niż M.
M odel Isinga. Na podstawie modelu Isinga dla trójwymiarowej kraty i progu ener
gii E określ, czy istnieje podgraf o energii swobodnej mniejszej niż E.
R yzyko dla portfela inwestycji o danej stopie zw rotu. Na podstawie portfela in
westycji o danym łącznym koszcie, danym zwrocie, określonym ryzyku przypisa
nym do każdej inwestycji i progu M znajdź taką alokację inwestycji, aby ryzyko
było niższe niż M.
Nierozwiązywalność 933
R adzenie sobie z N P-zupełnościę W praktyce trzeba znaleźć jakieś rozwiązanie dla
tego dużego zbioru problemów, dlatego ważne jest ustalenie sposobów na radzenie
sobie z nimi. Nie m ożna wyczerpująco omówić tej bogatej dziedziny badań w jed
nym akapicie, można jednak pokrótce opisać wypróbowane podejścia. Jedno z nich
polega na zmianie problemu i znalezieniu „przybliżonego” algorytmu, który znaj
duje niekoniecznie najlepsze, ale dobre rozwiązanie. Przykładowo, łatwo jest zna
leźć rozwiązanie problemu komiwojażera w przestrzeni euklidesowej, które nie różni
się więcej niż dwukrotnie od optymalnego. Niestety, ta technika nie zawsze chroni
przed NP-zupełnością przy szukaniu lepszych przybliżeń. Inne podejście polega na
opracowaniu algorytmu, który wydajnie rozwiązuje prawie wszystkie występujące
w praktyce wystąpienia problemu, choć istnieją dane wejściowe (najgorszy przypa
dek), dla których nie można znaleźć rozwiązania. Najbardziej znanym przykładem
tego podejścia są narzędzia do rozwiązywania problemów z obszaru programowania
liniowego z wykorzystaniem liczb całkowitych. Narzędzia te od dziesięcioleci służą
do rozwiązywania bardzo rozbudowanych problemów optymalizacyjnych w niezli
czonych zastosowaniach przemysłowych. Choć mechanizmy te mogą działać w cza
sie wykładniczym, w praktyce dane wejściowe są różne od danych dla najgorszego
przypadku. Trzecie podejście polega na korzystaniu z „wydajnych” algorytmów wy
kładniczych, tak zwanych algorytmów z nawrotami (ang. backtracking), co pozwala
uniknąć sprawdzania wszystkich możliwych rozwiązań. Istnieje jednak duża luka
między czasem wielomianowym a wykładniczym, której teoria nie opisuje. Co zrobić
z algorytmem, który działa w czasie proporcjonalnym do NlogNlub 2 ','v ?
N P -Z U P E Ł N O Ś Ć D O TY CZY W SZ Y ST K IC H OBSZARÓW Omówionych
ZA STO SO W A Ń
w książce — problemy NP-zupełne pojawiają się przy programowaniu podstawowym,
sortowaniu, wyszukiwaniu, przetwarzaniu grafów, przetwarzaniu łańcuchów zna
ków, obliczeniach naukowych, programowaniu systemów, badaniach operacyjnych
i w każdym innym obszarze, w którym potrzebne jest przetwarzanie. Najważniejszym
praktycznym zastosowaniem teorii NP-zupełności jest to, że zapewnia mechanizm
odkrywania, czy nowy problem z dowolnego z wielu obszarów jest „łatwy” czy „trud
ny”. Jeśli uda się znaleźć wydajny algorytm rozwiązujący nowy problem, trudność nie
istnieje. Jeżeli nie można znaleźć takiego algorytmu, dowód na to, że problem jest
NP-zupełny, jest informacją, iż opracowanie wydajnego algorytmu byłoby niezwy
kłym osiągnięciem. Jest to jednocześnie sugestia, że prawdopodobnie należy wypró
bować inne podejście. Duża liczba wydajnych algorytmów zbadanych w tej książce
to dowód na to, że od czasu Euklidesa wiele nauczyliśmy się o wydajnych metodach
obliczeniowych. Teoria NP-zupełności pokazuje natomiast, że nadal pozostaje wiele
do zrobienia.
934 KONTEKST
j ĆWICZENIA dotyczące symulowania zderzeń
6.1. Uzupełnij implementację m etody predi ctCol l i s i ons () i klasy P arti cl e w opi
sany w tekście sposób. Zderzenia sprężyste między param i twardych dysków oparte
są na trzech równaniach. Dotyczą one: a) zachowania pędu liniowego, b) zachowa
nia energii kinetycznej, c) tego, że przy zderzeniu norm alna siła działa prostopadle
względem punktu zderzenia (zakładamy brak tarcia lub ruchu obrotowego). Więcej
szczegółów znajduje się w witrynie poświęconej książce.
6.2. Opracuj wersje klas CollisionSystem, P a rtic le i Event obsługujące zderzenia
między wieloma cząsteczkami. Zderzenia te są ważne przy symulowaniu rozbicia
w grze w bilard (to ćwiczenie jest trudne!).
6.3. Opracuj wersje klas Col l i s i onSystem, P arti cl e i Event działające w trzech wy
miarach.
6.4. Zbadaj pomysł na poprawę wydajności m etody sim ulate() w klasie
Col l i s i onSystem przez podział obszaru na prostokątne komórki i dodanie nowego
typu zdarzeń, tak aby w każdej porcji czasu konieczne było prognozowanie zderzeń
z cząsteczkami z tylko jednej z dziewięciu przyległych komórek. Podejście to zmniej
sza liczbę obliczanych prognoz kosztem śledzenia ruchu cząsteczek między kom ór
kami.
6.5. Wprowadź entropię do klasy Col 1i sionSystem i wykorzystaj ją do potwierdze
nia klasycznych wyników.
6.6. Ruchy Browna. W 1827 roku botanik Robert Brown zaobserwował za pomocą
mikroskopu ruch zanurzonych w wodzie pyłków kwiatowych. Stwierdził, że ruchy
pyłków są losowe (są to tak zwane ruchy Browna). Badano to zjawisko, jednak prze
konujące wyjaśnienie uzyskano dopiero po przedstawieniu matematycznych wyni
ków przez Einsteina w 1905 roku. Oto wyjaśnienie Einsteina — ruch pyłków jest
powodowany przez miliony małych molekuł zderzających się z większymi cząstecz
kami. Przeprowadź symulację ilustrującą to zjawisko.
6.7. Temperatura. Dodaj do klasy P a rtic ie metodę tem perature(), która zwraca
iloczyn masy cząsteczki i kwadratu jej szybkości podzielonej przez dkB, gdzie d= 2 to
liczba wymiarów, a kB= 1,3806503x10'23 to stała Boltzmanna. Temperatura systemu to
średnia wartość takich iloczynów. Następnie dodaj metodę tem perature() do klasy
CollisionSystem i napisz kod, który okresowo wyświetla wartość temperatury, co
pozwala sprawdzić, czy jest ona stała.
935
6 . 8 . Rozkład Maxwella-Boltzmanna. Rozkład szybkości cząsteczek w modelu dla
twardych dysków odpowiada rozkładowi Maxwella-Boltzmanna (przy założeniu,
że system osiągnął równowagę termiczną, a cząsteczki są wystarczająco ciężkie,
aby m ożna pominąć efekty z obszaru mechaniki kwantowej), który jest rozkładem
Reyleigha w dwóch wymiarach. Kształt rozkładu zależy od temperatury. Napisz pro
gram, który tworzy histogram szybkości cząsteczek, i sprawdź działanie kodu dla
różnych temperatur.
6.9. Arbitralny kształt. Cząsteczki poruszają się bardzo szybko (szybciej niż odrzuto
wiec), jednak rozpraszają się powoli, ponieważ zderzają się z innymi cząsteczkami, co
zmienia ich kierunek. Rozwiń omawiany model o nowy kształt — dwa połączone rurką
pojemniki zawierające dwa różne rodzaje cząsteczek. Przeprowadź symulację i zmierz
procent cząsteczek każdego rodzaju w każdym pojemniku jako funkcję czasu.
6 .10. Przewijanie. Po przeprowadzeniu symulacji odwróć wszystkie szybkości, a na
stępnie uruchom system, tak aby działał wstecz. System powinien wrócić do pierwot
nego stanu! Ustal błąd zaokrąglania przez pom iar różnicy między końcowym a po
czątkowym stanem systemu.
6.11. Ciśnienie. Dodaj do klasy P a rtic ie metodę p ressu re(), która mierzy ciśnienie
na podstawie liczby i siły zderzeń ze ścianami. Ciśnienie systemu to suma tych sił.
Następnie dodaj do klasy Col 1isionSystem metodę p ressure() i napisz klienta, który
sprawdza prawdziwość równania pv = nRT.
6.12. Implementacja indeksowanej kolejki priorytetowej. Opracuj wersję klasy
Col 1 i si onSystem opartą na indeksowanej kolejce priorytetowej. Spraw, aby rozmiar
kolejki priorytetowej rósł najwyżej liniowo względem liczby cząsteczek (a nie kwa
dratowo lub w inny sposób).
6.13. Wydajność kolejki priorytetowej. Dopracuj kolejkę priorytetową i przetestuj
klasę Pressure w różnych temperaturach, aby wykryć wąskie gardło w obliczeniach.
Jeśli to uzasadnione, spróbuj przełączyć program na inną implementację kolejki prio
rytetowej, aby uzyskać wyższą wydajność przy wysokich temperaturach.
936 KONTEKST
H ĆWICZENIA dotyczące drzew zbalansowanych
6.14. Załóżmy, że w trzypoziomowym drzewie m ożna pozwolić sobie na przecho
wywanie a odnośników w pamięci wewnętrznej, od b do 2 b odnośników na stronach
reprezentujących węzły wewnętrzne i od c do 2 c elementów na stronach reprezentu
jących węzły zewnętrzne. Jaka jest maksymalna liczba elementów, które m ożna prze
chowywać w takim drzewie (podaj ją jako funkcję od a, b i c)?
6.15. Opracuj implementację klasy Page, w której każdy węzeł drzewa zbalansowa-
nego reprezentowany jest jako obiekt BinarySearchST.
6.16. Rozwiń klasę BTreeSET, aby opracować implementację BTreeST, w której klu
cze powiązane są z wartościami i obsługiwany jest kompletny interfejs API dla upo
rządkowanej tablicy symboli, obejmujący m etody min(), max(), floor(), cei 1 in g (),
deleteM in(), deleteMax(), s e le c t(), rank() i dwuargumentowe wersje metod
s iz e () i g e t ().
6.17. Za pomocą klasy StdDraw napisz program do wizualizowania rozrastania się
drzew zbalansowanych (efekt ma być podobny jak w tekście).
6.18. Oszacuj średnią liczbę sprawdzeń na wyszukiwanie w drzewach zbalanso
wanych przy S losowych wyszukiwaniach i typowym systemie pamięci podręcznej,
w którym w pamięci przechowywanych jest T ostatnio używanych stron (ich użycie
nie zwiększa liczby sprawdzeń). Przyjmij, że S jest znacznie większe niż T.
6.19. Wyszukiwanie w sieci W W W . Opracuj implementację klasy Page, w której
węzły drzewa zbalansowanego reprezentowane są jako pliki tekstowe na stronach
WWW. Kod ma służyć do indeksowania sieci WWW. Zastosuj plik z szukanymi wy
rażeniami. Strony W W W do zindeksowania należy pobierać ze standardowego wej
ścia. Aby zachować kontrolę, zastosuj param etr wiersza poleceń m i ustaw górny limit
na 10 "' węzłów wewnętrznych (skonsultuj się z administratorem systemu przed u ru
chomieniem programu dla dużego m). Wykorzystaj m-cyfrowe liczby do nazwania
węzłów wewnętrznych. Przykładowo, jeśli m to 4, nazwy węzłów to BTreeNodeOOOO,
BTreeNodeOOOl, BTreeNode0002 itd. Na stronach przechowuj pary łańcuchów znaków.
Dodaj do interfejsu API operację clo se() przydatną przy sortowaniu i zapisie. Aby
przetestować implementację, poszukaj informacji o sobie i znajomych w uniwersy
teckiej witrynie.
937
6.20. Drzewa B*. Zastanów się nad heurystyką podziału braci dla drzew zbalanso-
wanych (jest ona stosowana w drzewach B*). Kiedy trzeba podzielić węzeł, ponieważ
obejmuje M elementów, należy najpierw połączyć węzeł z bratem. Jeśli brat obejmuje
k elementów, a k < M - 1, należy zmienić układ elementów przez umieszczenie w bra
cie i pełnym węźle po około (M+k)l2 węzły. Jeżeli k jest większe, należy utworzyć
nowy węzeł i umieścić w każdym z trzech węzłów po około 2M/3 węzły. Ponadto
dopuszczalne jest zwiększenie korzenia do około 4M/3 elementów, a kiedy to ogra
niczenie zostanie osiągnięte, należy podzielić korzeń i utworzyć nowy o dwóch ele
mentach. Podaj ograniczenia liczby sprawdzeń potrzebnych przy wyszukiwaniu lub
wstawianiu w drzewie B* rzędu M o N elementach. Porównaj te ograniczenia z ogra
niczeniami dla drzew zbalansowanych (zobacz t w i e r d z e n i e b ). Opracuj implemen
tację wstawiania dla drzew B*.
6.21. Napisz program do określania średniej liczby stron zewnętrznych w drzewie
zbalansowanym rzędu M zbudowanym przez Włosowych operacji wstawiania do po
czątkowo pustego drzewa. Uruchom program dla rozsądnych wartości M i N.
6.22. Jeśli system obsługuje pamięć wirtualną, zaprojektuj i przeprowadź ekspery
m enty w celu porównania wydajności drzew zbalansowanych z wydajnością wyszu
kiwania binarnego dla losowego wyszukiwania w bardzo dużych tablicach symboli.
6.23. Przeprowadź eksperymenty na implementacji strony Page z ć w i c z e n i a 6 .15 ,
aby określić wartość M, która zapewnia najkrótszy czas wyszukiwania dla implementa
cji drzewa zbalansowanego wykonującej losowe operacje wyszukiwania w bardzo du
żej tablicy symboli. Uwzględnij tylko wartości M będące wielokrotnością liczby 100.
6.24. Przeprowadź eksperymenty, aby porównać czas wyszukiwania dla wewnętrz
nych drzew zbalansowanych (używając wartości M ustalonej w poprzednim ćwicze
niu), haszowania z próbkowaniem liniowym i drzew czerwono-czarnych przy loso
wych operacjach wyszukiwania w bardzo dużych tablicach symboli.
938 KONTEKST
| ĆWICZENIA dotyczące tablicy przyrostkowej
6 .2 5 . Podaj (w taki w sposób, jak na rysunku na stronie 890) tablice przyrostko
we, posortowane przyrostki oraz tablice i ndex [] i 1 cp [] dla poniższych łańcuchów
znaków:
a. abacadaba
b. m i s s i s s i p p i
c. abcdefghij
d. aaaaaaaaaa
6.26. Zidentyfikuj problem w poniższym fragmencie kodu wyznaczającym wszyst
kie przyrostki w sortowaniu przyrostków.
suffix =
f o r ( i n t i = s . l e n g t h ( ) - 1 ; i >= 0; i - - )
{
suffix = s . c h a r A t ( i ) + suffix;
s u ffix e s [ i] = suffix;
}
Odpowiedź: tempo wzrostu czasu i pamięci jest kwadratowe.
6.27. W niektórych sytuacjach potrzebne jest sortowanie rotacji cyklicznych teks
tu obejmujących wszystkie znaki. Dla i od 0 do N - 1 i-ta rotacja cykliczna tekstu
o długości N to ostatnich N - i znaków, po których następuje i pierwszych znaków.
Zidentyfikuj problem w poniższym fragmencie kodu wyznaczającym wszystkie ro
tacje cykliczne.
in t N = s . l e n g t h ( ) ;
fo r (in t i = 0 ; i < N; i+ +)
ro ta tio n [i] = s . s u b s t r i n g ( i , N) + s . s u b s t r i n g ( 0 , i ) ;
Odpowiedź: tempo wzrostu czasu i pamięci jest kwadratowe.
6.28. Zaprojektuj działający w czasie liniowym algorytm do wyznaczania wszyst
kich rotacji cyklicznych tekstu.
Odpowiedź:
S t r i n g t = s + s;
int N = s . le n g t h ( ) ;
f o r ( i n t i = 0; i < N; i++)
ro ta tio n [i] = r . s u b s t r in g ( i, i + N);
6.29. Przy założeniach opisanych w p o d r o z d z i a l e 1.4 podaj poziom wykorzysta
nia pamięci przez obiekt SuffixArray obejmujący łańcuch znaków o długości N.
939
6 .30. Najdłuższy wspólny podłańcuch. Napisz używającego klasy SuffixArray klienta
LCS, który przyjmuje dwie nazwy plików jako argumenty wiersza poleceń, wczytuje
dwa pliki tekstowe i w liniowym czasie znajduje najdłuższy podłańcuch występują
cy w obu plikach (w 1970 roku D. Knuth postawił hipotezę, że jest to niemożliwe).
Wskazówka: utwórz tablicę przyrostkową dla s#t, gdzie s i t to dwa sprawdzane łań
cuchy znaków, a # to znak, który nie występuje w żadnym z nich.
6.31. Transformata Burrowsa-Wheelera. Transformata Burrowsa-Wheelera jest
stosowana w algorytmach kompresji danych, w tym w bz i p2 i wysoce wydajnych
metodach sekwencjonowania w badaniach nad genomem. Napisz klienta klasy
SuffixArray, który oblicza tę transformatę w czasie liniowym w następujący sposób
— dla łańcucha znaków o długości N (zakończonego znakiem specjalnym końca pli
ku, $, który jest mniejszy niż pozostałe znaki) rozważ macierz N na N, w której każdy
wiersz obejmuje inną rotację cykliczną pierwotnego łańcucha znaków. Posortuj wier
sze leksykograńcznie. Transformata Burrowsa-Wheelera to pierwsza od prawej ko
lum na w posortowanej macierzy. Przykładowo, transform ata dla tekstu missi ssip -
pi $ to i pssm$pi ssi i . Transformata odwrotna Burrowsa-Wheelera powstaje w wyniku
odwrotnego procesu (na przykład dla i pssm$pi ssi i jest to mi ssi ssi ppi $). Napisz
też klienta, który na podstawie transformaty Burrowsa-Wheelera wyznacza w czasie
liniowym transformatę odwrotną.
6.32. Linearyzacja dla cyklicznych łańcuchów znaków. Napisz klienta klasy
SuffixArray, który na podstawie łańcucha znaków znajduje w czasie liniowym naj
mniejszą leksykograńcznie rotację cykliczną. Problem ten występuje w chemicznych
bazach danych z cząsteczkami cyklicznymi. W bazach tych każda cząsteczka jest
reprezentowana jako cykliczny łańcuch znaków, a reprezentacja kanoniczna (naj
mniejsza rotacja cykliczna) jest używana do wyszukiwania, kiedy kluczem może być
dowolna rotacja (zobacz ć w i c z e n i a 6.27 i 6. 28 ).
6.33. Najdłuższy podłańcuch powtarzający się k razy. Napisz klienta klasy SuffixArray,
który na podstawie łańcucha znaków i liczby całkowitej k znajduje najdłuższy pod
łańcuch powtarzający się k lub więcej razy.
6.34. Długie powtarzające się podłańcuchy. Napisz klienta klasy SuffixArray, który
na podstawie łańcucha znaków i liczby całkowitej Lwyszukuje wszystkie powtarzają
ce się podłańcuchy o długości L lub większej.
6.35. Liczby wystąpień k-gramów. Opracuj i zaimplementuj typ ADT do wstępnego
przetwarzania łańcuchów znaków, tak aby m ożna było wydajnie odpowiadać na py
tania w formie: Ile razy pojawia się dany k-gram7. Każde zapytanie powinno działać
w czasie proporcjonalnym do k log N (dla najgorszego przypadku), gdzie N to dłu
gość łańcucha znaków.
940 KONTEKST
J ĆWICZENIA dotyczące przepływu maksymalnego
6.36. Jeśli przepustowości to dodatnie liczby całkowite mniejsze niż M, jaka jest
maksymalna możliwa wartość przepływu dla dowolnej sieci s t o V wierzchołkach i E
krawędziach? Podaj dwie odpowiedzi dotyczące sytuacji z dozwolonymi i niedozwo
lonymi krawędziami równoległymi.
6.37. Podaj algorytm rozwiązujący problem przepływu maksymalnego dla przy
padku, w którym sieć tworzy drzewo po usunięciu ujścia.
6.38. Prawda czy fałsz? Jeśli prawda, podaj krótki dowód, jeżeli fałsz — przedstaw
kontrprzykład.
a. W przepływie maksymalnym nie występują cykle skierowane, w których każ
da krawędź ma przepływ dodatni.
b. Istnieje przepływ maksymalny, w którym nie występuje cykl skierowany obej
mujący same krawędzie o przepływie dodatnim.
c. Jeśli przepustowości wszystkich krawędzi są różne, przepływ maksymalny j est
unikatowy.
d. Jeśli przepustowości wszystkich krawędzi zostaną zwiększone o stałą addy-
tywną, przekrój minim alny pozostanie niezmieniony.
e. Jeśli przepustowości wszystkich krawędzi zostaną pom nożone przez dodatnią
liczbę całkowitą, przekrój m inimalny pozostanie niezmieniony.
6.39. Uzupełnij dowód t w i e r d z e n i a g — pokaż, że zawsze kiedy krawędź jest kry
tyczna, długość ścieżki powiększającej przez nią musi wzrosnąć o dwa.
6.40. Znajdź w internecie dużą sieć, którą możesz wykorzystać do testowania na
realistycznych danych algorytmów do wyznaczania przepływu. Możliwości to sieci
transportowe (drogowe, kolejowe lub powietrzne), komunikacyjne (telefoniczne lub
komputerowe) lub dystrybucji. Jeśli przepustowości są nieokreślone, opracuj sen
sowny model, aby je dodać. Napisz program, który na podstawie danych tworzy sieci
przepływowe. Jeśli to uzasadnione, opracuj dodatkowe m etody prywatne do „czysz
czenia” danych.
6.41. Napisz generator losowych sieci rzadkich z przepustowościami całkowitolicz-
bowymi z przedziału od 0 do 220. Zastosuj odrębną klasę na przepustowości i opracuj
dwie implementacje — jedna m a generować przepustowości o równomiernym roz
kładzie, a druga — o rozkładzie Gaussa. Zaimplementuj programy klienckie generu
jące sieci losowe dla obu rozkładów wag. Wykorzystaj dobrze dobrany zbiór wartości
V i E, tak aby można wykorzystać sieci do przeprowadzenia testów empirycznych na
grafach odpowiadających różnym rozkładom wag krawędzi.
941
6.42. Napisz program, który generuje V losowych punktów w przestrzeni, a następ
nie tworzy sieć przepływową o krawędziach (w obu kierunkach) łączących wszystkie
pary punktów oddalonych o nie więcej niż daną odległość d od siebie przez ustawie
nie przepustowości każdej krawędzi za pomocą jednego z losowych modeli opisa
nych w poprzednim ćwiczeniu.
6.43. Podstawowe redukcje. Opracuj klienty klasy FordFul kerson do znajdowania
przepływu maksymalnego w sieci przepływowej każdego z poniższych typów. Oto
te sieci:
B nieskierowana;
• bez ograniczeń liczby źródeł lub ujść oraz krawędzi wchodzących do źródła
albo wychodzących z ujścia;
■ z dolnym ograniczeniem przepustowości;
■ z ograniczeniami przepustowości w wierzchołkach.
6.44. Dystrybucja produktów. Załóżmy, że przepływ reprezentuje produkty przesy
łane ciężarówkami między miastami. Przepływ wzdłuż krawędzi u-v odpowiada licz
bie produktów przesyłanych z miasta u do v danego dnia. Napisz klienta, który co
dziennie wyświetla informacje dla kierowców dotyczące tego, ile produktów i gdzie
należy odebrać oraz ile produktów i gdzie należy wyładować. Przyjmij, że nie ma
ograniczenia liczby kierowców i że żadne produkty nie są wysyłane z danego punktu
dystrybucji przed dotarciem ich wszystkich do tego miejsca.
6.45. Obsadzanie stanowisk. Opracuj klienta klasy FordFul kerson, który rozwiązuje
problem obsadzania stanowisk. Wykorzystaj redukcję z t w i e r d z e n i a j . Użyj tablicy
symboli do przekształcenia nazw symbolicznych na liczby całkowite potrzebne w sie
ci przepływowej.
6.46 Utwórz rodzinę problemów kojarzenia w grafach dwudzielnych, w której
średnia długość ścieżek powiększających używanych przez dowolny oparty na takich
ścieżkach algorytm rozwiązujący powiązany problem wyznaczania przepływu m ak
symalnego jest proporcjonalna do E.
6.47. Połączenia st. Opracuj klienta klasy FordFul kerson, który dla nieskierowane-
go grafu G oraz wierzchołków s i t określa m inim alną liczbę krawędzi w G, których
usunięcie powoduje odłączenie t od s.
6.48. Ścieżki rozłączne. Opracuj klienta klasy FordFul kerson, który dla nieskierowa-
nego grafu G oraz wierzchołków s i t określa maksymalną liczbę rozłącznych ścieżek
z s do t.
942 KONTEKST
j ĆWICZENIA dotyczące redukcji i nierozwiązywalności
6 .49 . Znajdź nietrywialny czynnik liczby 37703491.
6 .50 . Udowodnij, że problem wyznaczania najkrótszych ścieżek m ożna zredukować
do programowania liniowego.
6 .51 . Czy może istnieć algorytm, który rozwiązuje problem NP-zupełny średnio
w czasie A/1“^ , jeśli P ^ NP? Wyjaśnij odpowiedź.
6 .52 . Przyjmij, że ktoś odkrył algorytm, który gwarantuje rozwiązanie problemu
spelnialności formuł logicznych w czasie proporcjonalnym do l ,l w. Czy wynika
z tego, że m ożna rozwiązać inne problemy NP-zupełne w czasie proporcjonalnym do
1 , 1 *?
6 .53 . Jakie znaczenie miałby program rozwiązujący problem programowania linio
wego dla liczb całkowitych w czasie proporcjonalnym do 1 , 1 *?
6 .54 . Podaj wielomianową redukcję problemu pokrycia wierzchołkowego do prob
lemu spełnialności nierówności liniowych z liczbami całkowitymi 0 i 1 .
6 .55 . Udowodnij, że problem znajdowania ścieżki Hamiltona w grafie skierowa
nym jest NP-zupełny, wykorzystując NP-zupełność problemu wyznaczania ścieżki
Hamiltona dla grafów nieskierowanych.
6 .56 . Przyjmij, że wiadomo, iż dwa problemy są NP-zupełne. Czy oznacza to, że ist
nieje wielomianowa redukcja między nimi?
6 .57 . Przyjmij, że problem X jest NP-zupełny, m ożna go zredukować wielomianowo
do Y, a Y m ożna zredukować wielomianowo do X. Czy Y musi być NP-zupełny?
Odpowiedź: nie, ponieważ Y może nie należeć do zbioru NP.
6 .58 . Przyjmij, że istnieje algorytm rozwiązujący problem spełnialności formuł
logicznych (wersję decyzyjną), w którym należy stwierdzić, czy istnieją wartości
zmiennych logicznych spełniające wyrażenie logiczne. Pokaż, jak znaleźć wartości
zmiennych.
6 .59 . Przyjmij, że istnieje algorytm rozwiązujący problem pokrycia wierzchołko
wego (wersję decyzyjną), w którym należy stwierdzić, czy istnieje pokrycie wierz
chołkowe o danym rozmiarze. Pokaż, jak rozwiązać problem wyznaczania pokrycia
wierzchołkowego o minim alnym rozmiarze w wersji optymalizacyjnej.
6.60. Wyjaśnij, dlaczego wersja optymalizacyjna problemu pokrycia wierzchołko
wego nie musi być problemem przeszukiwania.
Odpowiedź: nie znamy wydajnego sposobu na stwierdzenie, że dane rozwiązanie jest
najlepsze (choć można zastosować wyszukiwanie binarne dla wersji z przeszukiwa
niem, aby znaleźć najlepsze rozwiązanie).
6.61. Załóż, że X i Y to dwa problemy przeszukiwania, a X m ożna zredukować wie-
lomianowo do Y. Jakie wnioski m ożna wyciągnąć na tej podstawie?
a. Jeśli Y jest NP-zupełny, dotyczy to także X.
b. Jeśli X jest NP-zupełny, dotyczy to także Y.
c. Jeśli X należy do P, dotyczy to także Y.
d. Jeśli Y należy do P, dotyczy to także X.
6.62. Przyjmij, że P =£ NP. Jakie wnioski m ożna wyciągnąć na tej podstawie?
e. Jeśli X jest NP-zupełny, nie można rozwiązać X w czasie wielomianowym.
f Jeśli X należy do NP, nie można rozwiązać X w czasie wielomianowym.
g. Jeśli X należy do NP, ale nie jest NP-zupełny, m ożna rozwiązać X w czasie wie
lomianowym.
h. Jeśli X należy do P, X nie jest NP-zupełny.
ALGORYTMY
P o d sta w y Grafy
1.1. Stos oparty na powiększaniu tablicy 4.1. Wyszukiwanie w głąb
1.2. Stos oparty na liście powiązanej 4.2. Wyszukiwanie wszerz
1.3. Kolejka FIFO 4.3. Spójne składowe
1.4. Wielozbiór 4.4. Osiągalność
1.5. Algorytm Union-Find 4.5. Sortowanie topologiczne
4.6. Silne składowe (algorytm Kosaraju)
S o rto w a n ie
4.7. Minimalne drzewo rozpinające (algorytm Prima)
2.1. Sortowanie przez wybieranie
4.8. Minimalne drzewo rozpinające (algorytm Kruskala)
2.2. Sortowanie przez wstawianie
4.9. Wyznaczanie najkrótszych ścieżek (algorytm Dijkstry)
2.3. Sortowanie Shella
4.10. Wyznaczanie najkrótszych ścieżek w grafach DAG
2.4. Sortowanie przez scalanie z zatapianiem
4.11. Wyznaczanie najkrótszych ścieżek
Sortowanie przez scalanie z wypływaniem
(algorytm Bellmana-Forda)
2.5. Sortowanie szybkie
Sortowanie szybkie z podziałem na trzy części
Ł ańcuchy zn a kó w
2.6. Kolejka priorytetowa oparta na kopcu
5.1. Sortowanie łańcuchów znaków metodą LSD
2.7. Sortowanie przez kopcowanie
5.2. Sortowanie łańcuchów znaków metodą MSD
Tablice sy m b o li 5.3. Sortowanie szybkie łańcuchów znaków
z podziałem na trzy części
3.1. Wyszukiwanie sekwencyjne
5.4. Tablica symboli oparta na drzewie trie
3.2. Wyszukiwanie binarne
5.5. Tablica symboli oparta na drzewie TST
3.3. Drzewa wyszukiwań binarnych
5.6. Wyszukiwanie podłańcuchów
3.4. Czerwono-czarne drzewa BST (algorytm Knutha-Morrisa-Pratta)
3.5. Haszowanie metodą łańcuchową 5.7. Wyszukiwanie podłańcuchów
(algorytm Boyera-Moorea)
3.6. Haszowanie z próbkowaniem liniowym
5.8. Wyszukiwanie podłańcuchów
(algorytm Rabina-Karpa)
5.9. Dopasowywanie do wzorca
za pomocą wyrażeń regularnych
5.10. Kompresja i rozpakowywanie metodę Huffmana
5.11. Kompresja i rozpakowywanie metodą L Z W
944
KLIENTY
P o d sta w y Ł ańcuchy zn a k ó w
Białe listy Dopasowywanie do wzorca
za pomocą wyrażeń regularnych
Wartościowanie wyrażeń
Kompresja Huffmana
Określanie połączeń
Kompresja L Z W
S o rto w a n ie
K o n tek st
Porównywanie dwóch algorytmów
Symulacja zderzeń cząsteczek
M największych elementów
Zbiory oparte na drzewach zbalansowanych
Scalanie wielościeżkowe
Tablice przyrostkowe (podstawowe)
Tablice sy m b o li N ajdłuższy powtarzający się podłańcuch
Usuwanie powtórzeń Słowa kluczowe w kontekście
Określanie liczby wystąpień Wyznaczanie przepływu maksymalnego
(algorytm Forda-Fulkersona)
Wyszukiwanie w słowniku
Indeksowanie plików
Iloczyn skalarny dla wektorów rzadkich
G rafy
Typ danych dla grafów symbolicznych
Stopnie oddalenia
Metoda PERT
Arbitraż
945
l i l i Skorowidz
przez scalenie, 18, 201, 282, 284, 289,
300, 305, 310, 313, 353, 354, 355, 736,
ADT, Patrz: dane typ abstrakcyjny przez wstawianie, 18, 262,270, 287,
akumulator, 104 308, 353, 354, 736
wizualny, 106 przez wybieranie, 18, 260, 339, 353, 354
alejka, 542, 550 przez zliczanie, 715, 717, 718
jednokierunkow a, 544 Shella, 270, 305, 353, 354
alfabet, 709, 714, 723, 733, 753, 762, systemowego Javy, 355
algorytm szybkiego, 18,217,300-315,353-356,736
A*, 362 topologicznego, 590, 670, 694
analiza, 17 z podziałem na trzy części, 731, 736
Bellmana-Forda, 18, 683, 684, 687, 694, tablicy symboli, 62
694, 695, Tremaux, 542, 544, 588
Boyera-M oorea, 771, 782, 791, U nion-Find, 558
Dijkstry, 18, 140, 362, 664, 680, 694, wyszukiwania, 18, 19, 373, 409, 437, 459,
Euklidesa, 16 479, 880, 889,
Forda-Fulkersona, 903, 904, 907, 909, 914, binarnego, 20, 201, 390, 392, 395, 397,
haszowania, Patrz: haszowanie, tablica 398,408, 426, 459, 499
z haszowaniem
podłańcuchów, 708, 770, 772, 774, 782,
Jarnika, 640, Patrz też: algorytm Prima 786, 790, 800, 804, 889
KMP, Patrz: algorytm Knutha-Morrisa-Pratta sekwencyjnego, 386, 388, 397, 426,
K nutha-M orrisa-Pratta,, 771, 774, 775,
459, 499
781,782, 791,806, z random izacją, 210, 302
kolejki priorytetowej, Patrz: kolejka zachłanny, 619
priorytetowa amortyzacja kosztów, 210, 244,487
Kosaraju, 598, 602, anomalia, Patrz: graf anomalia
Kruskala, 18, 362, 616, 636, 641,
API, Patrz: interfejs API
Las Vegas, 790
argum ent, 83
Prima, 18, 362, 616, 628, 636, 640, 641,
asercja, 119
666, 694,
atak siłowy, 772, 773, 791, 887
wersja leniwa, 629 autoboxing, 134
wersja zachłanna, 629, 632, 635,
autom at
Rabina-Karpa, 786, 787, 790, 791,
DFA, Patrz: autom at skończony
sortowania, 18, 19, 255, 265, 267, 320, 335,
deterministyczny
348, 354, 360, 714, 888,
NFA, Patrz: autom at skończony
LSD, 718, 736, niedeterm inistyczny
łańcucha znaków, 714, 718, 722,731,736,
skończony, 708, 922
MSD, 722, 725, 728, 729, 736,
deterministyczny, 776, 777
przez kopcowanie, 18, 335,338,353,354,
niedetrministyczny, 806, 809, 811, 816
948 SKOROWIDZ
B typ, 76, 258, 349, 534, 620, 653
abstrakcyjny, 15, 76, 86, 87, 96, 108,
Bellman R., 682, 695, Patrz też: algorytm
110, 165, 321,537
Bellmana-Forda
definicja, Patrz: definicja typu danych
Bentley J., 310
Digraph, 580
BFS, Patrz: graf przeszukiwanie wszerz
generyczny, 132, 134, 146, 150, 365
biała lista, 60, 196, 503
nakładkowy, 114, 134
biblioteka
niezmienny, 117
Javy, 41, 501
prosty, 23, 134, 355, 500
m etod statycznych, 22, 34, 38
referencyjny, 134
zewnętrzna, 39
sparametryzowany, Patrz: typ
błąd, 119
generyczny
Boruvka O., 640
zmienny, 117
Boyer Robert S., 771, Patrz też: algorytm
U nion-Find, 15, 62, 228, 541
Boyera-M oorea
wejściowe, 209
Brin S., 514
Davroye L„ 424
definicja typu danych, 22, 34
C DFS, Patrz: graf przeszukiwanie w głąb
cecha A, 192 digraf, Patrz: graf skierowany
cecha D, 267, 269 Dijlcstra Edsger Wybe, 18,140, 310, 640, 694,
cecha E, 264 Patrz też: algorytm Dijkstry
cecha H , 457 domknięcie, 801, 802, 803, 812
cecha L, 479 przechodnie, 604
cecha O, 785 dopasowanie do wzorca, 708
Chazelle Bernard, 865 drzewo, 237, 238, 286, 292, 532
Churcha-Turinga hipoteza, Patrz: rozszerzona 2-3, 436, 456, 459, 532, 764
hipoteza Churcha-Turinga 2-3-4,453
chybienie, 274, 388, 409, 412, 416 binarne, 18, 168, 325, 398, 408, 409, 426,
Cook S., 771, 930, Patrz też: twierdzenie 459, 499, 532, 764
Cooka-Levina czerwono-czarne, 444,456,459, 501,764
cyld, Patrz: graf cykl zupełne, 325, 326
czarna lista, 503 BST, Patrz: drzewo binarne
czas wykonania, 192, 204, 207, 258, 260, 266, korzeń, 237, 408, 409, 439, 744
359, 362, 425, 458, 470, 489, 499, 630, 637, LPT, Patrz: drzewo najdłuższych ścieżek
755, 923 m inim alne rozpinające, 18, 616, 619, 625,
636, 641, 694
D MST, Patrz: drzewo m inim alne rozpinające
dane najdłuższych ścieżek, 674
abstrakcja, 15, 22, 62, 76 najkrótszych ścieżek, 652, 666, 694
kompresja, 19, 363, 708, 822, 828 rozpinające, 532, 616
Huffmana, 838 SPT, Patrz: drzewo najkrótszych ścieżek
kopiec binarny, Patrz: kopiec binarny trie, 742, 744, 754, 764, 839, 840, 842, 852
lista powiązana, 15, 132, 154, 155,162, 165, trójkowe, 758, 761, 762
168, 213, 324, 386, 388, 398, 426, 580 hybrydowe, 763
lista sąsiedztwa, Patrz: lista sąsiedztwa TST, Patrz: drzewo trójkowe
łańcuch znaków, 19, 22, 46, 92, 114, 117, unikatowe, 617
363, 472, 560, 707, 714, 718, 722, 731, wielkość, 238
736, 887 wysokość, 292, 424, 436, 456
długość, 708, 887 zbalansowane, 18, 19, 398, 436, 458, 878
podłańcuch, 770 dynamiczne określanie połączeń, 228
struktura, 15, 16 dziecko, 325, 327, 328, 408, 422
kompozycja, 168 dziedziczenie
powiązana, 132 implementacji, 113
tablica, Patrz: tablica interfejsu, 112
dziel i zwyciężaj, 300, 305
SKOROWIDZ 949
E rzadld, 532
skierowany, 529, 578, 585, 588, 596,653, 900
egzemplarz, 96
acyldiczny, Patrz: graf acyldiczny
element, 362 ważony
osierocony, 149
spójny, 531, 596, 617, 636
osiowy, 302, 308
symboli, 560
entropia, 308, 312, 313
ścieżka, 531, 547, 553, 585,650, 673, 680,
Euklidesa algorytm, Patrz: algorytm Euldidesa
916,919
długość, 531, 579, 923
F krytyczna, Patrz: ścieżka krytyczna
filtrowanie na podstawie białej listy, 20 ogólna, 531
Floyd R. W., 338, 339 powiększająca, 903, 909
Ford Lester Randolph, 682, 695, Patrz też: skierowana, 579
algorytm Bellmana-Forda, algorytm waga, 650
Forda-Fulkersona ważony, 529, 616, 620, 628, 636, 650
Fredm an M.L., 640 skierowany, 529, 653, 664, 670,
Fulkerson D.R., 903, Patrz też: algorytm 680, 900
Forda-Fulkersona wierzchołek, 530, 560, 578, 628
funkcja, 34, Patrz też: m etoda statyczna sąsiadujący, 531
hashCode(), 473 źródłowy, 540
haszująca, 470, 471,474 z krawędziami ważonymi, Patrz: graf
ważony
G grep, 708
głowa, 578
Google, 514,516
Gosper R.W., 771 haszowanie, 18, 398, 470, 478, 499, 771, 786
gra w Kevina Bacona, 565 m etodą łańcuchową, 476, 480
graf, 18, 168, 362, 516, 527, 530, 531, 898 m odularne, 471, 472
acykliczny, 532, 558, 588, 668, 670 równomierne, 475
ważony, 586, 590, 594, 595, 670, 671, z adresowaniem otwartym, 481, 483
673, 676 z próbkowaniem liniowym, 481, 484,
anomalia, 530 485, 501
cyld, 531,586, 588 hermetyzacja, 108
ogólny, 531 Hoare C.A.R., 217, 307
prosty, 531 Huffmana kompresja, Patrz: dane kompresja
skierowany, 579 H uffmana
ujemny, 681, 682, 689
DAG, Patrz: graf acykliczny ważony I
dwudzielny, 533, 558 identyfikator, 23
euklidesowy, 626, 635, 668
iloczyn skalarny, 514, 515
gęsty, 532, 640
implementacja, dziedziczenie, 113
krawędź, 362, 530, 560, 578, 588, 617,
indeks, 332, 470, 508, Patrz też: tablica symboli
624, 628
odwrotny, 510
incydentna, 531
instrukcja, 22, 26
równoległa, 530
deldaracja, 22, 26
waga, 617, 624, 636
foreach, 135, 136, 138, 150
multigraf, 530
pętla, 22, 26, 27
nieskierowany, 529, 534, 616, 666
przypisania, 22, 26, 28, 81
niespójny, 531
return, 22, 26
podgraf, 531
w arunkowa, 22, 26, 27
prosty, 530
wywołanie, 22, 26
przekrój, 518, 628
interfejs
przeszukiwanie
Comparable, 408
w głąb, 18, 542, 543, 545, 554, 558, 582
dziedziczenie, 112
wszerz, 18, 550, 551, 553, 554
•ft-.
950 SKOROWIDZ
interfejs krawędź, Patrz: graf krawędź
API, 15, 40, 77, 100, 109, 133, 135, 152, Kruskal V.J., 640, Patrz też: algorytm Kruskala
231, 320, 332, 375, 378, 410, 458, 501,
534, 555, 742, 710560, 580, 620, 625, 653, L
656, 781, 824, 872, 882, 891, 901
labirynt, 542
iteracja, 132
las, 237
iterator, 151
rozpinający, 532
¿terowanie, 135, 150
Levin L ., 930, Patrz też: twierdzenie Cooka-
Levina
J liczba, 712
Jarnik. V., 640 całkowita, 22, 23, 471
język rzeczywista, 22, 23, 472
formalny, 708 trójkątna, 197
LISP, 165 zmiennoprzecinkowa, 472
lista
K biała, Patrz: biała lista
Karp Richard M., 771, 786, 790, Patrz też: czarna, Patrz: czarna lista
algorytm Rabina-Karpa powiązana, Patrz: dane lista powiązana
ldasa sąsiedztwa, 537, 580, Patrz też: tablica list
Javy, Patrz: program Javy sąsiedztwa
równoważności, 228
klient, 100, 102, 110, 322, 382, 504, 508, 562, Ł
625, 657 łańcuch znaków Patrz: dane łańcuch znaków
klucz, 256, 308, 313, 320, 325, 326, 328, 350,
374, 373, 375, 377, 379, 380, 471, 470, 408, M
388, 715, 383, 480, 727, 744, 750
macierz
kolejność, 418, 480
rzadka, 514, 517
nuli, 376, 386
sąsiedztwa, 536
powtarzający się, 500
maszyna Turinga, 922
złożony, 462
M cCarthy John, 165
K nuth D onald E„ 190, 192, 217, 708, 771,
M cllroy D., 310
Patrz też: algorytm K nutha-M orrisa-Pratta
mediana, 357, 915
kod klienta, 78, 79, 81, 83, 85, 88, 104, 132, 135,
metaznak, 803
152, 891
m etoda
kolejka, 15, 132, 162, 320, 684
egzemplarza, 80, 98
FIFO, 133, 138,162, 166, 550
hashCodeO, 473
LIFO, Patrz: stos
H om era, 472
priorytetowa, 62, 320, 324, 331, 332, 339,
łańcuchowa, 470, 476, 478, 479, 480, 483,
348, 357, 362
486, 499
indeksowana, 332
naukowa, 184
z komparatorem, 352
niestatyczna, 81
kompilacja, Patrz: program Javy kompilacja
pozycyjna, 712
kompresja danych, Patrz: dane kompresja
statyczna, 22, 34, 81, 110
Huffmana, 363,847, Patrz też: dane kompresja
model
LZW, 851
kosztów, 194, 195, 232, 258, 381, 878
konstruktor, 96, 106, 321, 555, 580
losowych łańcuchów znaków, 728
kopiec
matematyczny, 190
a-arny, 331
programowania, 15, 18, 20, 38
binarny, 320, 324, 325, 326, 327
Moore G ordon Earle, 206
przywracanie struktury, 327, 328
Moore J. Strother, 771, Patrz też: algorytm
Fibonacciego, 640, 694
Boyera-M oorea
korzeń, Patrz: drzewo korzeń
M orris J.H., 708, 771, Patrz też: algorytm
Kosaraju Sambasiva Rao, Patrz: algorytm
K nutha-M orrisa-Pratta
Kosaraju
multigraf, Patrz: graf m ultigraf
SKOROWIDZ 951
N przeciążenie, 24, 355
przekrój grafu, Patrz: graf przekrój
N eum ann, 866, Patrz też: algorytm przepełnienie, 148, 472
sortow ania przez scalenie przepływ, 898, 899, 900, 901, 904, 905, 917, 919
N ewtona współczynnik, 197
maksymalny, 19
niedeterm inizm , 800, 806, 926 przepustowość, 904
NP-zupełność, 929, 930, 933 przybliżenie Stirlinga, 197
przydział
O listowy, 168
obiekt, 79,81,83, 84,213 sekwencyjny, 168
geometryczny, 88 przyrostek, 888
kolekcja, 132
typu String, 214 R
obserwacja, 185 Rabin Michael O., 771, 786, 790
odległość tau Kendalla, 357 redukcja, 356, 357, 915
odnośnik, 408, 446, 744 wielomianowa, 928
pusty, 436 referencja, 154, 621, 624
ogon, 578 zbędna, 149
optymalność, 662, 844 rekord, 154
osiągalność, 579, 582, 602, 809 rekurencja, 37, 154, 282, 284, 289, 300, 392,
546, 395, 413, 418, 543, 722, 730
P relaksacja, 660, 662, 663, 670
Page L„ 514 Robson J., 424
Page Rank, 514, 516, 889 rodzic, 237, 325, 327, 438, 446, 744
pamięć, 116, 206, 212, 217, 258, 287,474, rotacja, 446,457
488, 595, 630, 637, 728, 756, 883 rozszerzona hipoteza Churcha-Turinga, 922,926
perm utacja, 357 rysowanie, 54
pętla własna, 530, 624, 652 rzutowanie, 25
podgraf, Patrz: graf podgraf
podłoga, 379, 418 s
podtablica, 725, 733 Sedgewick R„ 310
potok, 52 sieć
powtórzenie, 356, 502, przepływowa, 898, 900
Pratt Vaughan R., 708, 771, Patrz też: algorytm rezydualna, 907
K nutha-M orrisa-Pratta skrzyżowanie, 542, 650
prawo M oorea, 206 słownik, Patrz: tablica symboli
Prim R ., 640, Patrz też: algorytm Prima Sollin M„ 640
problem sortowanie, Patrz: algorytm sortowania
arbitrażu, 693 stabilność, 353
określania połączeń, 15, 18
sterta binarna, 325, 331
szeregowania zadań, Patrz: szeregowanie
stopień
zadań oddalenia, 565
ścieżki Hamiltona, 925, 927 wejściowy, 578
Union-Find, 228, 232, 541 wyjściowy, 578
wyszukiwania najkrótszej ścieżki, 15, 18
stos, 15, 132, 133, 139, 159, 162, 166, 550,
problemy przeszukiwania, 924, 925, 926, 931
320, 322
program Javy, 22, 38
o stałej pojemności, 144
kompilacja, 22
sufit, 379, 418
urucham ianie, 22 symulacja sterowana zdarzeniami, 868, 869, 877
program owanie
szeregowanie zadań, 586, 588, 675, 678, 679
kontraktowe, 119
szybka m etoda
m odularne, 15, 38, 108
find, 234,243
obiektowe, 62, 108
union, 236, 238, 243,
próbkowanie liniowe, 470, 481, 486, 499, 501 z wagami, 239, 243
przechodzeniem w porządku inorder, 424
SKOROWIDZ
ścieżka, Patrz: graf ścieżka twierdzenie R, 333, 666, 816
ścieżka krytyczna, 676, 678 twierdzenie S, 338, 670, 673, 828
twierdzenie T, 355, 673, 845
T twierdzenie U, 359, 678, 845
twierdzenie V, 679
tablica, 15, 22, 84, 117, 144, 148, 151, 165, 168,
twierdzenie W, 681, 683
214, 260, 262, 287,326, 332,473,479
abstrakcyjna asocjacyjna, 375 twierdzenie X, 683
dwuwymiarowa, 31, 215 twierdzenie Y, 685
twierdzenie Z, 693
elementów, 256
indeksowana znakami, 710
krawędzi, 536 U
list sąsiedztwa, 536 ujście, 898, 899, 904
nieuporządkowana, 322 urucham ianie, Patrz: program Javy
o zmiennej długości, 15 urucham ianie
przyrostkowa, 887, 897
sufiksowa, 19 W
symboli, 373, 375, 426, 458, 498, 504,
wartość logiczna, 22, 23
514, 516
wejście-wyjście, 48, 94, 824
uporządkowana, 378, 390
wektor rzadki, 514, 515, 517
uporządkowana, 287, 322,324,398,418,458
węzeł, 154, 168, 237, 238,408,422, 744
z haszowaniem, 398, 470, 485
głębokość, 239, 241
znaków, 709, 764
poczwórny, 439, 454
Tarjan R.E., 640
podwójny, 436, 437,447
technika zachłanna, 324, 376
potrójny, 436, 438, 444, 448, 449
tem po wzrostu, 191, 198, 201
rekord, 154
term inal wirtualny, 22
usuwanie, 422
test jednostkowy, 38
wielozbiór, 15, 132,133, 136, 166, 168
trafienie, 388, 409, 412,415
wierzchołek, Patrz: graf wierzchołek
Turing A., 922, Patrz też: maszyna Turinga
W illiams J. W. J., 338
twierdzenie, 195
wskaźnik, 350, 775
Cooka-Levina, 930, Patrz też: twierdzenie M
współczynnik
Kleenea, 806
Newtona, 197
przepływu maksymalnego i przekroju
zapełnienia, 483
minimalnego, 904
wstawiane, 412, 437, 447, 449, 470, 880
twierdzenie A, 260, 388, 543, 549, 717, 877
wybieranie, 357
twierdzenie B, 194, 195, 196, 262, 395, 396,
wydajność, 15, 60, 102, 104, 209, 217, 239, 258,
436, 553, 718, 721, 883, 886
270, 289, 300, 312, 324, 331, 355, 474, 709,
twierdzenie C, 205, 218, 264, 415,424, 558,
728, 734, 877, 894, 912
729, 894
wyjątek, 119
twierdzenie D, 210,415,424, 582, 729, 730, 897
wyrażenie, 22, 23, 25
twierdzenie E, 211,424, 459, 590, 735, 905,
regularne, 708, 800, 801, 802, 804, Patrz
twierdzenie F, 235, 284, 287, 441, 594, 754, 906
też: grep
twierdzenie G, 196, 238, 239, 287, 456, 458,
wyszukiwanie, Patrz: algorytm wyszukiwania
459, 595,912,913
twierdzenie H, 241, 242, 291, 293, 294, 600,
755,915
Z
twierdzenie I, 292, 310, 313, 459, 602, 917 zachłanność, Patrz: technika zachłanna
twierdzenie f, 294, 518, 761, 918 założenie J, 475, 478, 479, 485, 487
twierdzenie K, 478, 487, 619, 761, 920, 921 złączanie, 801, 812
twierdzenie L, 628, 763, 929 zmienna, 23, 96, 99, 154
twierdzenie M, 312,485,486,487,630, 773,930 znak alfanumeryczny, 23
twierdzenie N, 313, 487, 635, 637, 666, 781
twierdzenie O, 325, 636 ź
twierdzenie P, 326, 662 źródło, 651, 652, 658, 668, 898, 904
twierdzenie Q, 331, 333, 663, 811