Download as pdf or txt
Download as pdf or txt
You are on page 1of 949

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ą algs4.cs.princeton.edu. 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 BinarySearch.class, zawierającego niskopozio-
mową wersję programu w kodzie bajtowym Javy w pliku BinarySearch.class. 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 (Math.absft - 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++)
Std0ut.println(2*M ath.PI*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
a.len 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 < a.le ngth; i++)
i f (a [i] > max) max = a [i ];

Obliczanie średniej i n t N = a.length;


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 = a.length;


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 = a.lengt 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 = a.lengt 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 Double.NaN;
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 s.so 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 Double.NaN;
(metodą Newtona)
double e r r » le -15;
double t = c;
while (Math.abs(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 ath.abs(), Math.min() 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 Math.random(), 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ć
java.util.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 Math.logQ 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 Math.random() 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 + StdRandom.random() * (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 ) (StdRandom.random() * 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 + StdRandom.uniform(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 = StdRandom.random();
double sum = 0.0;
Losowa wartość typu i nt for (int i = 0; i < a.le 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 = a.length;
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 + StdRandom.uniform(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 ath.sqrt(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 r.par-
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 System.out. 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 StdOut.java 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 ut.pri n t1n ( "Wi t a j , double lo = Double.parse Double(args[l]);
św iecie!"J; do ich wywoły­ double hi = Double.parseDouble(args[2]);
wania. Po prawej przedsta­ for (in t i = 0; i < N; i++)
{
wiono prostego klienta. double x = StdRandom.uniform(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 ut.printf("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 data.txt. Każde wywołanie
metody StdOut.p r in t() lub S tdO ut.println() 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 data.txt 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
data.txt zamiast oczekiwania na wpisanie przez użytkownika danych w oknie ter­
minala. Kiedy program wywołuje metodę StdIn.readD 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 StdDraw.java 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. StdDraw.point() 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 StdDraw.RED). 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;
StdDraw.set Xscale(0, N) ;
StdDra w.set Ysc ale(0 , N*N);
StdDraw.setPenRadius(.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 ] = StdRandom.randomO;
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 ;
StdDraw.filledRectangle(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 ] = StdRandom.randomO;
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 ;
StdDraw.filledRectangle(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 = a.length - 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 s.so 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 largeW.txt
(z m ilio n e m liczb całkowitych) i largeT.txt (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 Double.NEGATIVEJNFINITY.

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 zera.Reszta 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 Stdln.isEm 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 . Tang.NoClassDefFoundError: 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 (Math.abs(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. System.out .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 t.prin 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 largeT.txt 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
heads.increm 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
head.increm 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 (StdRandom.bernoulli(0.5)) % java FI i p s 10
heads.increment(); 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 l.in 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;
c2.increment();
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 s.increment();
e lse t a i I s . i n c r e m e n t ();

i f (hea ds.tal 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 s.so 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 java.lang 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
java.aw t.Font 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
box.draw ();
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 = Math.randomO;
zwala to oszacować obszar prostokąta). Aby
double y = Math.randomO; 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 ox.co ntains(p)) c. i ncrem entO;
else p.draw();
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­
a.leng th() 7
stępną stronę). Nawet ta krótka lista obejmuje roz­
a.charAt(4) m
budowane operacje, wymagające zaawansowanych
a.concat(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 Stdln.StdOut 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 t.close 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 heads.increm 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))
heads.in c r e m e n t o ;
e ls e t a i l s.in 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 s.ta 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++)
a.addDataValue(StdRandom.random());
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++)
a.addDataValue(StdRandom.random());
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 w.set Xscale(0, t r i a l s ) ;
StdDraw.setYscale(0, max);
StdDraw.set Pen Radius(. 005);

p ub lic void addDataValue(double val)


(
N++;
t otal += v a l ;
StdDraw.setPenColor(StdDraw.DARK_GRAY);
StdDraw.point(N, v a l) ;
StdDraw.setPenColor(StdDraw.RED);
StdDraw.point(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 rgeW.txt < 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 eys.length; i++)
a [ i ] = k e y s [ i ] ; // Kopia zabezpieczająca.
A rrays.sort(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 ava.1ang.Comparable 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
java.lang.Iterable 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 x.equals(y) i y.eq 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 != that.day) return f a l s e ;
mentowania metody eq ua 1 s () i f (this.month != that.month) return f a l s e ;
dla dowolnego typu. Po zaimple­ i f ( t h i s . y e a r != that.year) 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 cter.isWhi-
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";
s.toUpperCase();
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 = s.toUpperCase() i s = s.su 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 = s.su 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 = input.spl i t ( " \ \ s + " ) ;
i n t [] in t s = new int[w ords.length;
f o r in t i = 0 ; i < word.length; 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 = date.spl i t ( " / " ) ;
month = In te g er.p arse ln t(fields[0 ]) ;
day = Integer.parselnt(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>();
queue.enqueue(new Date(12, 31, 1999));

Date next = queue.dequeue();

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> ();
stack.push(17) ; // Autoboxing (in t -> I n t e g e r ) .
in t i = stack.pop (); / / 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 rs.siz 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 ] = q.dequeued ;
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 (s.eq u a ls("("))
else i f (s.equals("+")) ops.push(s)
else i f (s.equals("-")) ops.push(s)
else i f (s.equals("*")) ops.push(s)
else i f (s.equals("/")) ops.push(s)
else i f (s.equals("sqrt")) ops.push(s)
else i f (s.equalsC)"))
{ / / 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 (op.equals("+")) = vals.popO + v;
else i f (op.equals("-")) = vals.popO - v;
else i f (op.equals("*")) = vals.popO * v;
else i f (op.equals("/")) = vals.popO / v;
else i f (op.equals("sqrt")) = M ath.sqrt(v);
vals.push(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(Double.parseDouble(s));
}
StdOut.println(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 == a.length) 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 == a.length/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


java.lang (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 == a.length/4) re s iz e(a.le 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
second.item = "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;
first.next = second;
second.next = 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 st.ite 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ść
x.item, a potem zmodyfikować x, żeby prowadziła do następnego obiektu Node listy
powiązanej (do zmiennej należy przypisać wartość x.next). 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 = x.next)
{
// Przetwarzanie x.item.
}
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 (!s.isE 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;
first.next = oldfirst;
N++;
}

public Item pop()


( // Zdejmowanie elementu z wierzchu stosu.
Item item = first, i tern;
first = first.next;
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 tobe.txt
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 ( " - " ) )
q.enqueue(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 = first.next;
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 ;
first.item = item;
first.next = 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 = current.item;
current = current.next;
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();

stack.push( 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 Stack.class i Stack$Node.class. 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)
{
stack.push(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 (Iq.isEm ptyO )
stack.push(q.dequeue!)) ;
while (¡stack.isE m ptyO )
q.enqueue(stack.pop( ) );

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:

x.next = 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 = x.next;
x.next = 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?

x.next = t ;
t.n e x t = x.next;

Odpowiedź: w czasie aktualizowania t.n e x t pole x.next 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 (first.next == n u ll) return first;
Node second = first.next;
Node rest = reverse(second);
second.next = first;
first.next = 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 ints.txt 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 ints.txt 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 lKints.txt, 1
2Kints.txt, 4Kints.txt i 8Kints.txt 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 ints.txt). Można szybko określić, że w pliku lK ints.txt znajduje się
-442472 70 trójek sumujących się do 0, a w pliku 2Kints.txt — 528 takich trój­
626686
-157678
ek. Programowi znacznie więcej czasu zajmuje ustalenie, że w pliku
508681 4Kints.txt jest 4039 trójek sumujących się do 0, a w czasie oczekiwania
123414 na zakończenie sprawdzania pliku 8Kints.txt 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] = StdRandom.uniform(-1000000, 1000000);
Stopwatch tim er = new Stopwatch();
in t cnt = ThreeSum .count(a);
double time = tim er.ela 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
ThreeSum.count() 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 ThreeSum.count() 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 ] = StdRandom.uniform(-MAX, MAX);
Stopwatch tim er = new Stopw atch();
in t cnt = ThreeSum .count(a);
return tim er.ela 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 ThreeSum.count() 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 ThreeSum.count () 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 = a.length;
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 = In.readlnts(args[0]);
StdOut.println(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 s.so 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 ints.txt), 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 s.so 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 ints.txt, 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 ints.txt 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 mediumllF.txt 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 tinyUF.txt 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 mediumUF.txt obejmuje 900 połączeń między 625 punk­
% more la rg e U F .txt
tami, co pokazano na stronie 230; plik largeUF.txt 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
largeUF.txt 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 (IStdln.isE m ptyO )
{
int p = Stdln.readlntO ;
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.
uf.union(p, q ) ; / / Ł ąc ze n ie składowych
StdO ut.println(p + " " + q ) ; / / i wyświetlanie połączenia.
}
StdO ut.println("liczba składowych: " + uf.count());

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 tinyUF.txt.

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 largeUF.txt. 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 largeUF.txt. 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 ] ] tinyUF.txt. Ś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 tinyUF.txt 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 tinyUF.txt 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 < mediumUF.txt
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 F.txt
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 = a.length;
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 lillllllll.lilllllll ll ■■■11lol |l | | | | |
lub słów, używamy pionowych słupków
■I l.nlllllllillll
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 .■llllll.llll
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 StdRandom.shuffle(). 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 (alg.equals("She!1")) She!1 . s o r t ( a ) ;
i f ( a lg . e q u a l s ( "N e r g e ") ) Merge.sort(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 uick.sort(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 ] = StdRandom.uniformO ;
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 StdRandom.shuffle(), 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 = a.length;
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 (this.amount > that.amount) return +1;
i f (this.amount < that.amount) 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[a.length]; // Jednokrotna alokacja pamięci.
s o rt(a , 0, a.length - 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!!!il.iili..ili.i

D ruga p o d tab lica i! .lilii Ib li „L iiIILLi L.

Pierw sze scalanie m il

iiiiiil! il.illl ..ll

.......... ........................................... iiiilli li ,il,nlilJ.iili..iii .Jllll, ill. Iil.lllill.

.....Niiiiiiiiilllllllll........... lllilllllllllll, ll „LuIII,Lii«,.,11.Jilll.lii.Iil.llhll,

Pierwsza połowa
jest posortowana ............................. umilili lllllllllllllll, ll „Lnll Ll . i i l i Ji .Jllll,lii.Iil.lllill.

........ 111111111111111111111111111 IIIIIIIIIIIIIIL.......llllllJ.iiL.iii .Jllll,lii,lil.lllill.

...........iiiiiiiiiiiiiiiiiiiiiiiii lllllllllllllll ........Illl.......... III: .Jllll.lii.Iil.lllill,

........liiiiiiiiiiiiiiiiiiiiiiiiii lllllllllllllll ..........Illllllllll .JliiI.iLlii.lliiIi,

...... 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 = a.length;
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 MergeBU.sort ()?

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 Ij.ll.ll.llljlillillJiliiiilIllilhjlililliiiJlIU.Ilh.lliliiiIlllljllLnljiijjliii
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 (Stdln.hasNextLineQ)
{ // 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.
pq.insert(new T ra n s a c t i o n ( S t d In . r e a d L i n e ( ) ) ) ;
if (pq.sizeQ > M)
pq.delMin(); // 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 (Ipq.isEmptyO) s t a c k . p u s h ( p q . d e ! M i n ()) ;
for (Transaction t : stack) StdOut.println(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 q.insert(k, item) dodaje do tego podzbioru kiusta-
wiapq[k] = item. Wywołanie pq.change(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 = streams.length;
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 (!pq.isEm ptyO )


{
S td O u t .p rin t ln (p q .m in ());
in t i = pq.delMin();
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 = args.length;
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 l.tx t
A B C F G I I Z
% more m2.txt
B D H P Q Q
% more m3.txt % java Multiway m l.tx t m2.txt m3.txt
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 = a.length;
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:
MaxPQ.java:21: 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 CubeSum.java, 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 uick.sort(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 hen.com 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 ransaction.WhenOrder())

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 c.compare (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 v.who.compareTo(w.when); }
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 v.when.compareTo(w.when); }
)
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 (v.amount < w.amount) return -1;
i f (v.amount > w.amount) 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 09:00:00 G dańsk 09:25 G d a ń sk 09:00:00


Poznań 09:00:03 G dańsk 09:03 G d a ń sk 09:00:59
Kraków 09:00:13 G dańsk 09:21 G d a ń sk 09:03:13
Gdańsk 09:00:59 G d a ń sk 09:19 G d a ń sk 09:19:32
Kraków 09:01:10 G dańsk 09:19 G d a ń sk 09:19:46
Gdańsk 09:03:13 G d a ń sk 09:00 G d a ń sk 09:21:05
Szc ze c i n 09:10:11 G d a ń sk 09:35 G d a ń sk 09:25:52
Szc ze c i n 09:10:25 G d a ń sk 09:00 G d a ń sk 09:35:21
Poznań 09:14:25 Kraków 09:01 Nie są ju ż Kraków 09:00:13 Nadal
Gdańsk 09:19:32 Kraków 09:00 posortowane Kraków 09:01:10 posortowane
Gdańsk 09:19:46 Poznań 09:37 według czasu P oznań 09:00:03 według czasu
Gdańsk 09:21:05 Poznań 09:00 P oznań 09:14:25
Szc ze c i n 0 9 : 2 2 ::43 Poznań 09:14 P oznań 09:37:44
Szc ze c i n 0 9 : 2 2 :: 54 Szcze cin 09:10 Szc ze cin 09:10:11
Gdańsk 0 9 : 2 5 :: 52 Szcze cin 09:36 Szc ze cin 09:10:25
Gdańsk 0 9 : 3 5 :: 21 Szcze cin 09:22 S zcze cin 09:22:43
Szczeci n 09:36:14 Szc ze cin 09:10 S zc ze cin 09:22:54
Poznań 09:37:44 Szc ze cin 09:22 S zc ze cin 09:36:14

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 s.so 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 a.length > 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 (this.amount < that.amount - 0.005) return -1;
i f (this.amount > that.amount + 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 SPT.java. 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 LPT.java. 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 cs.princeton.edu jest edu.princeton.es.
Technika ta jest przydatna do analizowania dzienników sieciowych. Wskazówka:
użyj metody s.sp 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@princeton.edu do rs@princeton.edu. 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 KendallTau.java, 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 a.compareTo(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
09:00:03 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(09:00:13) 09:00:59 G d ańsk
tak jak klasa In d e x M in P Q () omówiona 09:01:10 Kraków
w p o d r o z d z i a l e 2 .4 . Główne różnice f1oor(09:05:00) — - 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 09:14:25 Poznań
klucze (co jest niedozwolone w tablicach 09:19:32 G d ańsk
09:19:46 G d ańsk
symboli), a tablice symboli obsługują
k e y s( 0 9 :1 5 : 00, 09:25:00) — - 0 9 : 2 1 : 0 5 G d ańsk
znacznie większy zbiór operacji. 09:22:43 Szczeci n
09:22:54 Szczeci n
Podłoga i sufit Często przydatne jest 09:25:52 G dańsk
obliczenie na podstawie otrzymanego c e i1 in g (0 9 :3 0 :0 0 ) 09:35:21 G d ańsk
klucza podłogi (ang. floor), czyli naj­ 09:36:14 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 (hi.com 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) ia.eq 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 a.compareTo(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
tinyTale.txt), tekstu całej tej książki (plik tale.txt) i popularnej bazy danych z milio­
nem losowych zdań z sieci WWW, tak zwanej bazy Leipzig Corpora Collection (plik
leipziglM.txt). Oto zawartość pliku tinyTale.txt.

% 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 (word.length() < minlen) continue; // Pomijanie krótkich kluczy.
i f (Ist.c o n tain s(w o rd )) st.put(word, 1 );
else st.put(word, st.get(word) + 1 );
}
// Wyszukiwanie klucza o największej li c z b i e wystąpień.
S trin g max =
st.put(max, 0 );
f o r (S t rin g word : s t . k e y s Q )
i f (st.get(word) > st.get(max))
max = word;
StdOut.println(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;
this.val = 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 = x.next)
i f (key.equals(x.key))
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= x.next)
i f (key.equals ( x . key))
{ x.val = 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 tale.txt, 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 leipziglM.txt\).

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 = key.com 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 = key.compareTo(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++)
q.enqueue(keys[i]);
i f (c o n ta in s (h i))
q.enqueue(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 a.compareTo(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 tale.txt 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 (!st.co n tain s(w o rd )) st.put(w ord, 1 );
e ls e st.put(w ord, st.get(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 TestBinarySearch.java 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
tale.txt 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 = key.compareTo(x.key);
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 = key.compareTo(x.key);
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 x.val = 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 x.key;

private Node floor (Node x, Key key)


(
i f (x == n u ll) return n u ll;
in t cmp = key.compareTo(x.key);
i f (cmp == 0 ) return x;
i f (cmp < 0 ) return floor ( x . l e f t , key);
Node t = floor(x.right, 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ż x.key w poddrzewie o korzeniu x.
i f (x == n u li) return 0 ;
in t cmp = key.compareTo(x.key);
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 (t.rig h t).
(C) (Mj ■ Ustawienie prawego odnośnika w x (ma on
wskazywać drzewo BST zawierające wszystkie
klucze większe niż x.key) 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 = key.compareTo(x.key);
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 = t.le 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 = lo.compareTo(x.key);
in t cmphi = hi.compareTo(x.key);
if (cmplo < 0) k e y s ( x .le f t , queue, lo, h i) ;
if (cmplo <= 0 && cmphi >= 0) queue.enqueue(x.key);
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 TestBST.java 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 = key.compareTo(x.key);
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 (StdRandom.uni 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);
root.color = 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 = key.compareTo(h.key);


if (cmp < 0) h.le ft = put(h.1 e f t , key, val);
else i f (cmp > 0) h.right = put(h.right, key, val);
el se h.val = v a l ;

i f (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);


i f (isRed(h.left) && isR e d (h .le ft.le ft)) h = rotateRight(h);
i f (isRed(h.left) && 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) wstaw.en.ach) 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 TestRB.java 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 TwoThreeST.java. 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 (key.compareTo(h.key) < 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 (key.compareTo(h.key) == 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 (key.compareTo(h.key) == 0)
{
h.val = g e t ( h .rig h t , m i n ( h . r ig h t ) . k e y ) ;
h.key = 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 a.equals(b) ma wartość true, wywołanie
a.hashCode() musi zwracać tę samą wartość, co b.hashCode(). 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 (x.hashCodeQ & 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 + who.hashCode() ; podejście. Można użyć m etody hashCode() na
hash = 31 * hash + when.hashCode() ; 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 ath.random(),
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 (key.hashCodeQ & 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 (key.hashCode() & 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 =Math.max(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 tinyTale.txt (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 (!set.contains(key))
k lu czy w p o s ta c i ła ń c u c h ó w znak ó w . Jeśli 1
set.add(key);
n a s tę p n y k lu c z znajduje się w zb io rze,
StdO ut.println(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 set.add(in.readStringO );
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 208.216.181.15 z m ie n n ej w p a m ię c i
lu b 207.142.131.206) 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 : amino.csy (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 ), DJlA.csv
506 RO ZD ZIA Ł 3 ■ W yszukiw anie

% more amino.csv (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 ń ) , ip.csv
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 c.csv
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 DJIA.csv
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 ip.csv
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
www.cn n .c o m ,6 4 .2 3 6 .1 6 .2 0
w w w .google.com ,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
www.si 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 eather.com ,6 3 . 111. 6 6 .1 1
w w w .yahoo.com ,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 UPC.csv
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 amino.csv 0 3


128.112.136.35 TCC
www.cs.princeton.edu Seri ne

% java LookupCSV DJIA.csv 0 3 % ja va LookupCSV UPC.csv 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
Methionine.ATG
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 (in.hasNextLine())
{
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 < a.length; i++)
{
S trin g val = a [ i ];
i f (is t. c o n ta in s (k e y ) ) st.put(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>());
st.get(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 aminoI.tx t
i f (st.con tains(qu e ry)) Se rin e
fo r (S t rin g s : st.ge t(qu e ry)) TCT
S td O u t .p rin t ln (" " + s ) ; TCA
TCG
i f (ts.con 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)) st.put(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 (st.con 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 (" " + file.getNameO);

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
exl.txt
% more ex3 .t xt was
i t was the age of wisdom exl.txt
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 : G.adj (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 (search.marked(v))
S t d O u t. p rin t (v + " " ) ;
Std O ut.p rintlnO ;

i f (s earc h.count() != G .V ( )) t in y G .t x t
Std O u t.p rint("N IE");
StdOu t.pri 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 : G.adj(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 (search.hasPath T o(v))
tujemy dowolną ścieżkę. Dalej opracujemy im­ f o r ( i n t x : sea rch.pathTo(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 t.println ();
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 : G.adj(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
queue.enqueue(s); // i um ieszczanie go w kolejce,
w hile (¡q ueu e.isE m ptyf))
{
in t v = queue.dequeue(); // 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,
queue.enqueue(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 tinyCG.txt „
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 mediumG.txt) 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 : G.adj (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 routes.txt, 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 s.tx t
większy przykład, oparty na pliku movies.txt 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 (" " + sg.name(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 routes.txt
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 ovies.txt 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 routes.txt i movies.txt 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 (in.hasN 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 < a.length; 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[st.get(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 (in.hasN 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 < a.length; i++) // z każdego wiersza
// z wszystkimi
G.addEdge(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 ovies.txt "/ " "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 movies.txt. 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
routes.txt 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 ovies.txt "/ " "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 ut.println(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 (bfs.hasPathTo(t))
fo r (in t v : bfs.pathTo(t))
S td O u t .p rin t ln (" " + sg.name(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 movies.txt 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 tinyGex2.txt (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 tinyGex2.txt (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 tinyGex2.txt (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 tinyGadj.txt
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 tinyGadj.txt. 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-
Gex2.txt (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 tinyGex2.txt (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 movies.txt, 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 movies.txt 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 movies.txt, 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 ovies.txt


Ź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))
R.addEdge(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 < args.length; 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 (reachable.marked(v)) StdO ut.print(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 : G.adj(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 );
cycle.push(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)


{
pre.enqueue(v);

marked [v] = true;


fo r (in t w : G.adj(v))
i f (! marked[w])
dfs(G, w);

post.enqueue(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 (¡cyclefinder.hasCycleO)
{
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 : top.order(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 tinyDG.txt 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 : order.reversePost())
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 : G.adj(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 tinyDGex2.txt, 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 Degrees.java, 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 nyEWG.txt
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) b.add(e);
r e t u r n b*
kazano reprezentację grafu ważonego, którą j
Hasa EdgeWeightedGraph tworzy na podstawie
przyHadowegO pliku tinyEWG.txt. 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 G.txt
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 String.form 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 st.edge 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 tinyEWG.txt.


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 mediumEWG.txt. 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 largeEWG.txt 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 mediumEWG.txt
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 mediumEWG.txt
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 tinyEWG.txt. 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 (!pq.isEmpty())
{
Edge e = pq.d elM inQ ; // Pobieranie najmniejszej
// wagi.
in t v = e .e it h e r ( ) , w = e.oth er(v); // Krawędź z kolejki pq.
i f (marked[v] &&marked[w]) continue; // Pomijanie, j e ś l i je s t
// niewybieralna.
mst.enqueue(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 : G.adj(v))
i f (!marked[e.other(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 tinyEWG.txt. 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 (!pq.isEmpty())
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 : G.adj(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 (e.weight() < di stTo [w])
{ // Krawędź e je st nowym najlepszym połączeniem między drzewem a w.
edgeTo[w] = e;
distTo[w] = e.weight();
i f (pq.contains(w)) pq.change(w, di stTo[w]);
else pq.insert(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 mediumEWG.txt (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 tinyEWG.txt. 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>(G.edges());
UF uf = new UF(G. V ( ) );

while ( ! pq.isEmpty() && m st.size() < G.V ()-1)


{
Edge e = pq.delMin(); // Pobieranie z pq krawędzi
// o minimalnej
in t v = e .e it h e r ( ) , w = e.oth er(v); // wadze i powiązanych
// wierzchołków.
i f (uf.connected(v, w)) continue; // Pomijanie niewybieralnych
// krawędzi.
u f . union(v, w); // Scalanie komponentów.
mst.enqueue(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 G.txt


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 tinyEWG.txt (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 < edgeTo.length; v++)
mst.add(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 mediumEWG.txt.
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 String.form 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 = e.fromO;.
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 [e.fromO] .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 ] )
bag.add(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 tinyEWD.txt, 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 mediumEWG.txt, 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 D.txt 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 [e.to ()].

/
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[e.from () ] )
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] + ek.weight()
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 : G.adj (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) ) pq.change(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 : top.ord 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 AG.txt 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 = Double.parseDouble(a[0]);
G.addEdge(new Directed Edge(i, i+N, d u ration ));
G.addEdge(new DirectedEdge(s, i, 0 .0 ));
G.addEdge(new DirectedEdge(i+N, t, 0 . 0 )) ;
fo r (in t j = 1; j < a.length; j++)
{
in t successor = I n t e g e r . p a r s e ln t ( a [ j ] );
G.addEdge(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 tdOut.printf("%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 nyEWDn.txt 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

tinyEWDnc.txt 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 {
q.enqueue(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] = Double.POSITI VE_INF I NI TY;
di stTo [s] = 0.0;
queue.enqueue(s);
onQ [s] = true;
while (¡queue.is Empty() && !this.hasN egativeC ycle())
{
in t v = queue.dequeue();
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

tinyEWDn.txt
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 rates.txt. 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 = Stdln.readDoubleQ ;
DirectedEdge e = new DirectedEdge(v, w, -M a t h .lo g (ra te ));
G.addEdge(e);
}
}

BellmanFordSP spt = new BellmanFordSP(G, 0);


i f (spt.hasNegativeCycle())
{
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[e.from( ) ] ) ;
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[e.to()]);
}
}
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 s.tx 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
s.charAt(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ść a.le n gth s . 1 engt h ()

Konwersja a = s.toCharA 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 Alphabet.UNICODE.
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 a.co n tain s(s.ch 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 = a.length;
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 = a.length;
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 s.ch 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 = a.length;
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 edu.pri nc eto n.cs się do obsługi równych
edu p r i n c e t o n . c s edu.pri 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 s.ch 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, a.length - 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 s.charA t(d) instrukcją a lp h a .toIndex(s.charA 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 week.log.txt 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 5.4.Tablica 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 == key.length()) return x;
char c = key.charAt(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 == key.length()) { x.val = val; return x; }
char c = key.charAt(d); / / D o zidentyfikowania poddrzewa t r i e służy
// d-ty znak klucza.
x.next[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 ) q.enqueue(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 (x.val != 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 www.cs.princeton.edu 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 = key.charAt(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 < key.length() - 1)
return get(x.mid, 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 = key.charAt(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< key.length() - 1)
x.mid = put(x.mid, 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 AreaCodes.txt
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 dictionary.txt 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 war.txt
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 t.le 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 t.le n g t h ( );
f o r (i = 0 , j =0; i <N && j < M; i+ + )
{
if (txt.charA t(i) == pat.charAt(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 pat.charAt(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 at.ch 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 t.ch arA t(i+ l) po porównaniu tx t. ABB
ABABAC
charAt(i) z pat.charA t(j). Przy dopa­
ABC
sowaniu wystarczy przejść do następnego ABAB A C
znaku, dlatego dfa[pat.charA 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ą pat.charA 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
pat.charAt(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 at.ch 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 at.ch 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 at.ch 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
pat.charAt(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[pat.charAt(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 at.ch a rA t(j)] [j] = j+1; // Ustawianie wartości
// dla dopasowania.
X = d fa[pat.cha 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 ut.print("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 pat.charA 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 t.le 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 (pat.charAt(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 .3.11.Utwó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 (tale.txt). 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@ cs.pri nceton.edu

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 (dfs.marked(v)) pc.add(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 (dfs.marked(v)) pc.add(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 = re.length;
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] == ' | ' )
ops.push(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 ;
G.addEdge(lp, or+1);
G.addEdge(or, i);
}
e ls e lp = or;
}
if (i < M- l && r e [ i + l ] == ' * ' ) // Przechodzenie d a le j.
(
G.addEdge(lp, i+1);
G.addEdge(i+l, l p);
}
if ( r e [i ] == ' ( ' || re [i] == || re[i] == ' ) ' )
G.addEdge(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 t.println (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 < GREP.java


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 196.26.155.241.
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 t.println (); 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 t.println (); 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 < abra.txt

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 ut.closeQ ;
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 ln.rea d C ha r(w );
zgodne ze standardowym modelem kom ­
B i naryStdO ut.writ e (D N A .t o C h a r( c));
} presji danych, ponieważ skompresowany
Bin aryStdO ut.closeO ; 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 genomeTiny.txt
ATAGATGCATAGCGCATAGCTAGATGTGCTAGC

java BinaryDump 64 < genomeTiny.txt


0100000101010100010000010100011101000001010101000100011101000011
0100000101010100010000010100011101000011010001110100001101000001
0101010001000001010001110100001101010100010000010100011101000001
0101010001000111010101000100011101000011010101000100000101000111
01000011
Liczba bitów: 264

% java Genome - < genomeTiny.txt


? ? -<--------------- Nie m ożna wyświetlić strum ienia bitów w standardowym wyjściu

% ja va Genome - < genomeTiny.txt | java BinaryDump 64


0000000000000000000000000010000100100011001011010010001101110100
1000110110001100101110110110001101000000
Liczba bitów: 104

% java Genome - < genomeTiny.txt |java HexDump 8


00 00 00 21 23 2d 23 74
8d 8c bb 63 40
Liczba bitów: 104

% java Genome - < genomeTiny.txt > genomeTiny.2bit


% ja va Genome + < genomeTiny.2bit
ATAGATGCATAGCGCATAGCTAGATGTGCTAGC ^

Cykl kompresji i rozpakowywana


% java Genome - < genomeTiny.txt | 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 < q32x48.bin
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 = BinaryStdln.readBooleanO;
w obu wymiarach), natomiast wielkość skom­
i f (b != old)
{ presowanej wersji rośnie tylko dwukrotnie (dwa
Bi naryStdOut.wri 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 < q32x48.bin

% java RunLength - < q32x48.bin > 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 < q32x48.rle.bin
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 < q64x96.bin

to 1 1 4 4 /1 5 3 6 = 74%

Bitmapa o wyższej rozdzielczości (6144 bity)

% java BinaryDump 0 q64x96.bin


6144 b i t s
% java RunLength - < q64x96.bin 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 < q64x96.rle.bin


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 = x.right;
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 (x.isLe 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 t.le 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 (code.ch 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)
pq.insert (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 = pq.del Mi n ();
Node parent = new N o d e ( ' \ 0 ', x . fr e q + y .f r e q , x, y ) ;
p q.insert(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 (x.isLeaff))
readTri e () ze strony 847
(
odtwarza drzewo trie BinaryStdOut.write(true);
na podstawie łańcucha BinaryStdOut.write(x.ch);
return;
bitów. Metoda wczytuje
1
jeden bit, aby ustalić ro­ BinaryStd0ut.writ e ( f a l s e ) ;
dzaj następnego węzła. w riteTrie(x.left);
writeTriefx.right);
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 (BinaryStdln.readBooleanf))
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 t.le 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 - < q32x48.bin | j a v a BinaryDump 0
L iczba bitów : 1144

% j a v a Huffman - < q32x48.bin | 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 4x96.bin | j a v a BinaryDump 0
L iczba bitów : 2296

% j a v a Huffman - < q 6 4x96.bin [ 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 ql28xl92.bin 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.
this.time = 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 t.tim e - t) ; // A k t u a l i z o w a n i e c z a s u
t = e vent.tim 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 ()) { h.add(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 t.sp 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);
}
StdOut.println(lrs);
}
}

Klient do wyznaczania najdłuższego powtarzającego się podłańcucha

% more tinyTale.txt
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 = text.length();
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 ) ;
StdOut.println(text.substring(from, t o ) ) ;
)
StdOut.println();
}
}
}

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 ] = s.su b strin g (i) ;
Q uick3w ay.sort(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 (e.fl owO < 0 | | e.flow() > 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 ;
this.flow = 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
q.enqueue(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 = q.dequeued;
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 = Double.POSIT 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 ( ) ) && e.flowQ > 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 = " + maxflow.value( ) ) ;
}
}

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

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 : G.adj ( 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

You might also like