Algorytmy [4 ed.] 9788328337114

Algorytmy od zawsze porównywane były do przepisów kucharskich. Z celnością tego porównania trudno dyskutować, na pewno j

1,493 99 31MB

Polish Pages [949] Year 2012

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Algorytmy [4 ed.]
 9788328337114

Citation preview

W y d a n i e

R O B E R T

S E D G E W I C K

b . 7 ii

Helion

K E V I N

IV

W A Y N E

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­ 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.

K

i s t r u k t u r d a n y c h jest podstawą w każdym programie 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. p o z n a w a n ie a l g o r y t m ó w

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­ 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 wielozbioró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.

K

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, odpowiedzią jest p. W przeciwnym razie należy wykonania ć w i c z e ń 1 .1.24 i 1 .1 .2 5 , podzielić p przez q i wykorzystać resztę r. na przykład po przeczytaniu p o d ­ 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; i n t r = p % q; rytm jest skończony, deterministycz­ 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 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.

b a d a n ia n a d a lg o r y t m a m i

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

ł Standardowe wyjście (strona 49)

% 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

-499569

t

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 niskopoziomową 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

-jnt d o u b le boole an c h a r

Zbiór wartości i operacji na nich (wbudowany w język Java)

Identyfikator

a abc Ab$ a b abl23 lo hi

Ciąg liter, cyfr i symboli _ oraz $, przy czym pierwszym znakiem nie może być cyfra

Zmienna

[dowolny identyfikator]

Nazwy wartości typu danych

typ danych

+

Operator

Literał Wyrażenie

- *

/

int double boolean char

1 0 -42 2.0 1.0e-15 3.14 true f a l s e ' a ' ' + ' ' 9 ' 1\ n 1

int double boolean

lo + (hi - lo )/2 1.0e-15 * t lo = 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++) { // Przestawia a [ i ] i losowy element z a [ i .. N - l ] . i n t r = i + StdRandom.uniform(N-i); double temp = a [ i ] ; a[i] = a [ r ] ; a [r] = temp;

Losowo zmienia pozycje elementów w tablicy wartości typu doubl e (zobacz ćwiczenie 1.1.36)

} ) 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. Typ

Zbiór wartości

Typowe . literały

Typowe wyrażenie O p e r a t o r y -------------------------;---------------------—------ —-----Wyrażenie Wartość

String

Ciągi znaków

“AB" "W ita j" "2.5"

+ (złączanie)

"W ita j, " + "O lu " "12" + "34" "1 " + "+ " + "2 "

"W ita j, Olu" "1234" "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)

s ta tic Strin g t o S tr in g (in t i)

Przekształcanie s na wartość typu i nt 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 tości wejściowe z argumentów wiersza pole­ wiersza poleceń 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­ tego trzeba pokrótce omówić podstawowe Plikowe operacje mechanizmy udostępniane przez większość wejścia-wyjścia Standardowe współczesnych systemów operacyjnych rysowanie 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.parjava 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 właściwy typ. Typowe polecenia systemu operacyjnego

1.1

a

Standardow e wyjście Opracowana przez nas biblioteka StdOut zapewnia obsługę standardo­ wego wyjścia. Domyślnie system łączy standardo­ we wyjście z oknem terminala. M etoda p r i n t () umieszcza argument w standardowym wyjściu. Metoda p rin tln () dodaje nowy wiersz, a me­ toda p r in tf ( ) obsługuje sformatowane wyjście w opisany dalej sposób. Java udostępnia podobną 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ń).

Podstawowy model program ow ania

Znak zachęty

V

49

Wywołanie metody statycznej m a in () z programu Random Seq

% j a v a Random Seq 5 1 0 0 .0 2 0 0 .0 - \~

Wywołanie środowiska uruchomieniowego Javy

a r g s [0] a r g s [1] a r g s [2]

Struktura polecenia

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 ()

s t a t i c void

p r in tf (String f, ...)

Wyświetla nowy wiersz 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­ leży pobrać do katalogu robo­ czego plik StdOut.java z wi­ tryny i stosować kod w rodza­ ju StdO ut.pri n t1n ( "Wi t a j , św iecie!"J; do ich wywoły­ wania. Po prawej przedsta­ wiono prostego klienta.

public c la s s RandomSeq

{ public s t a t i c void main(String[] args) { // 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]); double lo = Double.parse Double(args[l]); double hi = Double.parseDouble(args[2]); for (in t i = 0; i < N; i++)

{ 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 123.43 lenia. Najprostszy rodzaj łańcucha formatującego zaczyna 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 104.02 dziesiętnych opartych na typach całkowitoliczbowych Javy), f (dla wartości zmiennoprzecinkowych) i s (dla wartości

RandomSeq 5 100.0 200.0

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 łańcuchy formatujące

Wartość łańcucha przekształcona na dane wyjściowe

Typ

Kod

Typowy literał

in t

d

512

double

f e

1595.1680010754388

% 14.2f "% .7 f" '% 14.4e

1595.17 1595.168001111 1.5952e+03

String

s

"W itaj, wiosno

"%1 4s" "%-14s" V 1 4 .5 S

Witaj, wiosno Witaj, wiosno Wi taj

512 512

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; in t cnt = 0; zawierać ciąg wartości oddzielonych 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 ( ); cnt++; itd.). Domyślnie system wiąże stan­ ) 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ą lu b ). 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> Średnia wynosi 2.90123 cechy niektórych urządzeń wejścia i upraszcza imple­ 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 static

in t re ad lnt()

static

double readDoubleO

t r u e, jeśli nie ma więcej wartości; w innej sytuacji fal se

Wczytuje wartość typu i nt 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 przekierować 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óźniejPrzekierowywanie 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 zastosowania różnych im­ Przekierowywanie standardowego wyjścia do pliku 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 przekierować 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 % j a v a RandomSeq 1000 1 0 0 .0 2 0 0 .0

% java Average < d a ta .tx t

| 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 write 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)

static

double[] re adDouble s(Str ing name)

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 i nt Wczytuje wartości typu doubl e 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 tego miejsca opracowane przez nas abstrakcje wejściawyjś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­ 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 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 po prawej stronie. Figury geometryczne można wypeł­ 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.

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 ) ;

S t d D r a w .s q u a r e ( x , y ,

r) ;

(x,y)

d o u b le [ ] x = {x 0, x l , x2, x3 }; d o u b ie [] y = {yO, y l , y2, y 3 } ; StdDra w.p oiyg onCx, y) ;

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) s t a t i c void setPenColor( Color c)

Ustawia szerokość pióra na r 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

Podstawowy m odel program owania

Implementacja rysowania (fragment kodu)

Dane

Wartości funkcji

o

i n t N = 100; StdDraw.set Xscale(0, N) ; StdDra w.set Ysc ale(0 , N*N); StdDraw.setPenRadius(.Ol) ; f o r ( i n t i = 1; i 0; n /= 2) s = (n % 2) + s;

67

RO ZD ZIA Ł 1

n

ĆWICZENIA

Podstawy

(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 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 static

double dot(dou ble[] x, double[] y) doublet] [] mult (d ou b le [] [] a, doublet] [] b)

Iloczyn wektorowy Iloczyn macierzy

static

doublet] m ult (d ouble [] []

a, doublet] x)

Transpozycja Iloczyn macierz-wektor

static

doublet] mult(double[] y, doublet] □ a)

Iloczyn wektor-macierz

s t a t i c doublet] [] transpose (d ouble [] t] a)

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 = fo r (in t i = 1; fo r (in t j = d ist[i+ j]

new double[2*SIDES+1]; i = 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

Rodzaj klasy

Przykłady

Cechy

Metody statyczne

Math Stdln StdOut

Brak zmiennych egzemplarza

Niezmienne abstrakcyjne typy danych

Date Tran sact ion S t r i n g Integer

Wszystkie zmienne egzemplarza mają modyfikator pri vate Wszystkie zmienne egzemplarza mają modyfikator finał Kopiowanie zabezpieczające typów referencyjnych Uwaga: cechy te są konieczne, ale niewystarczające

Zmienne abstrakcyjne typy danych

Abstrakcyjne typy danych z efektami ubocznymi dla wejścia-wyjścia

Counter Accumulator

Wszystkie zmienne egzemplarza mają modyfikator pri vate Nie wszystkie zmienne egzemplarza mają modyfikator finał

V isua l Accumulator In Out Draw

Wszystkie zmienne egzemplarza mają modyfikator pri vate Metody egzemplarza wykonują operacje wejścia-wyjścia

Klasy Javy (implementacje typów danych)

121

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

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 niezainicjowanej 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

123

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 nadldasy. 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

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 sSpace(), 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.isWhiteSpace(), 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.

1 25

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

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 ( ) ; 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 queue = new Queue(); 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

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 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 co lle c tio n = new Queue();

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. , ż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. w arto zau w ażyć

135

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­ add( ) we obliczane jest przez dodanie kwadratów różnic 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 ). Przetwarzanie każdej kulki m Zapisanie wszystkich wartości w kolekcji Bag jest (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

T y p o w y k lie n t ty p u B ag

p u b lic c la s s Sta ts

{ p u b lic s t a t ic void m a in (S trin g [] a rgs)

( Bag numbers = new Bag(); 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

°

Wielozbiory, kolejki i stosy

137

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 { życiu — od osób czekających w kolejce po bilet do teatru, przez samochody stoją­ m m m ce przed budką poboru opłat, po zadania N o w y element trafia na koniec oczekujące na wykonanie przez aplikację Dodaw anie do kolejki ł w komputerze. Podstawą wszystkich reguł m 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 stać obsłużona jako pierwsza. Tak właśnie CD działa kolejka FIFO. Kolejki są naturalnym Pierwszy element modelem wielu codziennych zjawisk i od­ opuszcza grywają kluczową rolę w wielu aplikacjach. Usuwanie kolejkę z kolejki I Kiedy klient przechodzi po elementach ko­ lejki za pom ocą techniki fo r each, elementy są przetwarzane w kolejności ich dodawa­ Następny element opuszcza nia do kolejki. Kolejki w aplikacjach stosu­ Usuwanie kolejkę je się głównie po to, aby zapisać elementy z kolejki ł w kolekcji, zachowując przy tym ich względ­ m CD 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 q = new Q ueu e< Intege r> (); odpowiednia, ponieważ powoduje w hile (lin . is E m p t y O ) q .e n q u e u e (in .re a d ln t ()); umieszczanie liczb w tablicy w ko­ 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++) a [i ] = q.dequeued ; rzystano autoboxing i autounboxing 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.

Tc

mmmm Ta

Tm m m m m

m m Tmmmm

Tm m m

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 na wierzch od góry. Obecnie nie używamy tylu doku­ mentów, co kiedyś, jednak ta sama zasada sta­ nowi podstawę kilku regularnie używanych Nowy (czarny) aplikacji. Przykładowo, wiele osób porząd­ jest dokładany p u sh ( . kuje pocztę elektroniczną za pomocą stosu. 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 szarego czytane, jeśli stos nigdy nie jest opróżniany. ' = 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 itep u b lic c la s s Reverse ratora dla stosu w aplikacji jest chęć 1 p u b lic s t a t ic void m a in (S trin g [] a rgs) zachowania elementów kolekcji z jed­ 1 noczesnym odwróceniem ich względ­ 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> (); w h ile (IS td ln .is E m p t y O ) Reverse, widoczny po prawej stronie, 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 [ ] Stack

args)

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 else i f

(s.equals("/")) (s.equals("sqrt"))

ops.push(s)

else i f

(s.equalsC)"))

{

/ / Jeśli //

symbol t o

ops.push(s)

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 else i f

(op.equals("+")) (op.equals("-"))

= vals.popO = vals.popO

+ v; - v;

else i f

(op.equals("*"))

= vals.popO

* v;

else i f else i f

(op.equals("/")) (op.equals("sqrt"))

= vals.popO

/ v;

= 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 * (

(

1+100

1+ ( 5 *20 )

4 * 5 ) ) )

)

)

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

Lewy naw ias - ignorow any

( l + ( ( 2 + 3 ) * ( 4 * 5 ) ) ) jI,

Stos

operandów ~''x_

Stos operatorów ' sx

y

. .,

fcd

p j --------| + 1

O perand - um ieszczany n a stosie operandów

l + ( ( 2 + 3 ) * ( 4 * 5 ) ) ) ^

Operator - um ieszczany na stosie operatorów

+ ( ( 2 + 3 ) * ( 4 * 5 ) ) ) ( ( 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) ))

11 5 4 II + * 1 Il *5 *4 1+ II1 5* 4* 5: II* u 5 20 ; L+. 11 100 1+ A

| 101

4*5))) * 5) ) ) 5) ) )

) ) )

|

) ) )

i

Ślad działania opartego na dwóch stosach algorytmu Dijkstry do obliczania wyrażeń arytmetycznych

143

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] i umieszcza każdy łańcuch na stosie, chyba że (dodaj) (zdejmij) 0 4 1 2 3 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 be or to tyle samo czasu niezależnie od wielkości sto­ not 4 be or to 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 be or to not be Takie narzędzie opisujemy dalej. Wkładając not 3 be or to 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 be to o r that be tu implementacja posłuży za model dla im­ is 2 or to is 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

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) void S t rin g boolean in t

Tworzenie pustego stosu o pojemności cap Dodawanie łańcucha znaków

push (S t r in g item) pop()

Usuwanie ostatnio dodanego łańcucha znaków

isEm p tyO

Czy stos jest pusty?

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

145

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 Nazwa Item to parametr typu, czyli symboliczny zastępnik, zamiast którego m oż­ na podać konkretny typ używany w kliencie. Fragment FixedCapacityStack 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

I n t e r f e js API p u b lic c la s s Fixed C apacityStack F ix e d C a p a city Sta ck (in t cap) void push(Item item) Item pop()

Dodawanie elementu Usuwanie ostatnio dodanego elementu

boolean isEm ptyO

Czy stos jest pusty?

in t s iz e ( )

Klient testowy

Tworzenie pustego stosu o pojemności cap

Liczba elementów na stosie

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 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

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 p u b lic in t s iz e ( )

{ return N == 0; } { 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

1 47

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 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

to be or not to -

to

be -

be not

that

-

is

that or be

0 1 2 3 4 5 4 5 4 3 4 3 2 1 2

1

1

2

3

or

null

4

5

6

7

to

null

null

null

null to

2 4

be not

8

null be

null null that 4 3

null

null null

null is

Ślad procesu zmieniania wielkości tablicy przy wykonywaniu serii operacji push() i pop()

149

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 iterowania 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 c o lle c tio n = new S tack(); 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, odpowiadający interfejsowi: public in te rfa c e Iterable { Iterator 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. 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

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 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

{ 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

{ 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-

151

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. 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 ). alg o rytm

Std ln

StdOut

(dodaj)

(usuń)

N 5

head

a[]

fLa a i1l1 0

1

2

3

4

be

or

not

to

0

5

to

5

-

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

6

7

Ś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

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 implements Iterable

{ 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 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 (

//

Obsługa i t e r a c j i

p rivate public public public

w p o r z ą d k u LIFO,

in t i = N; boolean hasNextQ ( return Item next() ( return void remove() (

i > 0; } a [ - - i]; } }

} } 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.

153

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 nieobsł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

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 item na pożądaną wartość (dla uproszczenia załóżmy, że Item to S tri ng): first, item

Node f i r s t = new Node(); f i r s t .i tem = " t o " ; fi rst to

= "t o ";

nuli

second.item = "be"; th ird .ite m

= " o r ";

oraz ustawić pola next na listę powiązaną: first.next

Node second = new NodeO ; s e c o n d .i tem = " b e " ; f i r s t . n e x t = second;

= 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 referencja do węzła z referencją do nuli, czyli do pustej listy powiązanej), podobnie jak se­ cond (jest to referencja do węzła z referencją do thi rd, czyli do listy powiązanej) i first (jest 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.

Node t h i r d = new Node(); th ird .ite m = "o r"; seco n d .n e xt = t h i r d ; fi rst

second thi rd

wiązanie listy

elementów. W opisanym przykładzie first 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: 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

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.

155

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­ to cze prostsza — wystarczy przypisać do first war­ be or tość first.n ex t. Zwykle przed przypisaniem należy null pobrać wartość elementu (przez zapisanie jej do fir s t zmiennej typu Item), ponieważ po zmianie wartości zmiennej first dostęp do wskazywanego przez nią wcześniej węzła może być niemożliwy. Standardowo 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 obiekt węzła staje się osierocony, a system zarządza­ 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 W sta w ia n ie i u su w a n ie n a innych 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 (do pierwszego elementu) i la s t (do ostatniego elementu). Oto te operacje; " wstawianie na początek, ■ usuwanie z początku, ■ wstawianie na koniec.

Node l a s t = new N o d e O ; la st.ite m = " n o t " ;

D ołączanie no w ego w ęzła na koniec listy

o l d l a s t . next = l a s t ;

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

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 generycznego Item. Fragment 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

159

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 implements Iterable 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 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 implements Iterable

{ 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­ 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. l is t y

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 iterowanie 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

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 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 implements Iterable

{ 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 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

{ 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

Tablica

Indeks zapewnia natychmiastowy dostęp do każdego elementu

Trzeba znać wielkość w czasie inicjowania

Lista powiązana

Ilość zajmowanej pamięci jest proporcjonalna do wielkości

Dostęp do elementu 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

SPOSÓB PRZEDSTAW IEN IA WIELOZBIORÓW , KOLEJEK I STOSÓW W tym podrozdziale 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 do rodzica

1.5

Uni onFi nd

Tablica liczb całkowitych

Binarne drzewo wyszukiwań

3.2, 3.3

BST

Dwa odnośniki na węzeł

Łańcuch znaków

5.1

S t rin g

Sterta binarna

2.4

PQ

Tablica z haszowaniem (metoda łańcuchowa)

3.4

SeperateChai ni ngHashST

Tablica z haszowaniem (badanie liniowe)

3.4

Li nearProbi ngHashST

Listy sąsiedztwa dla grafów

4.1, 4.2

Graph

Drzewo trie

5.2

TrieST

Trójkowe drzewo trie

5.3

TST

Tablica, przesunięcie i długość Tablica obiektów Tablica list powiązanych Dwie tablice obiektów Tablica obiektów typu Bag Węzeł z tablicą odnośników Trzy odnośniki na węzeł

Przykładowe struktury danych rozwijane w książce

169

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 stack = new Stack(); 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 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 kowariantnymi (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[] 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[] (w skrócie Stack[]), którą trzeba zrzutować na typ Stack[].

1.3

Q

Wielozbiory, kolejki i stosy

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,

171

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 jednowierszowy 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, wiadomo, że w pierwszym potrzebny jest porządek LIFO, a w drugim — porządek FIFO.

1.3

h

Wielozbiory, kolejki i stosy

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. b. c. d. e. f. g. h.

4 4 2 4 1 0 1 2

3 2 1 0 9 8 7 6 5 6 8 7 5 3 2 9 0 1 5 6 7 4 8 9 3 1 0 3 2 1 0 5 6 7 8 9 2 3 4 5 6 9 8 7 0 4 6 5 3 8 1 7 2 9 4 7 9 8 6 5 3 0 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 wysokopoziomowy opis działania kodu dla dodatniej liczby całkowitej N. Stack 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).

173

RO ZD ZIA Ł 1

Q

Podstawy

ĆWICZENIA (ciąg dalszy) 1.3.6. Jaki efekt dla kolejki q ma wykonanie poniższego kodu? Stack stack = new S tack(); 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

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.

175

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

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; }

177

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 rekurencyjnie 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

I

o

Wielozbiory, kolejki i stosy

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 implements Ite rable Deque() boolean isEm ptyO in t s iz e ( )

Tworzenie pustej deque Czy deque jest pusta? 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 implements Ite rab le RandomBag () boolean isEm ptyO in t s i z e () void add (Item item)

Tworzenie pustego wielozbioru z dostępem losowym Czy wielozbiór jest pusty? Liczba elementów w wielozbiorze 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! permutacji powinna być równie prawdopodobna). Wskazówka: umieść elementy w tablicy i określ dla nich losową kolejność w konstruktorze iteratora.

1 79

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 RandomQueue() boolean isEm pty()

Tworzenie pustej kolejki z dostępem losowym 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 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 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 General i zedQueue ()

Tworzenie pustej kolejki Czy kolejka jest pusta?

boolean isEm pty() void in s e rt(Ite m x)

Dodawanie elementu

Item d e le t e (in t k)

Usuwanie i zwracanie k-tego elementu „ , , 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 r = new Queue(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 t = new Stack(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

Tworzenie pustego bufora

B u ffe r() 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

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.

183

ludzie Z a­ czynają używać ich do rozwiązywania trudnych problemów lub przetwarzania du­ żych ilości danych. Niezmiennie prowadzi to do pytań w rodzaju:

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

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

185

Analizy algorytm ów

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 { p u b lic s t a t ic in t count (i nt [] a) 1 .4 .26 ). Jako danych wejściowych użyj { // Z lic z a n ie tró je k sumujących s ię do pliku lM ints.txt z witryny. Plik zawiera in t N = a .le n gth ; milion losowo wygenerowanych war­ in t cnt = 0; f o r ( in t i = 0; i < N; i++) tości typu in t. Druga, ósma i dziesiąta 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) cnt++; w pliku? Program ThreeSum potrafi to return cnt; obliczyć, ale czy robi to w rozsądnym } czasie? Jaka jest zależność między roz­ p u b lic s t a t ic void m a in (S trin g [] args) miarem problem u (N) a czasem działa­ 1 nia programu? W ramach pierwszego 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-

0.

Jak długo potrwa wykonywanie programu dla danego N?

186

R O Z D Z IA L I

% morę lM in t s . t x t 324110

-442472 626686 -157678 508681 123414 -77867

155091 129801

287381 604242 686904 -247109 77867

o

Podstawy

ny (zawierają one 1000, 2000, 4000 i 8000 liczb całkowitych z pliku lM ints.txt). Można szybko określić, że w pliku lK ints.txt znajduje się 70 trójek sumujących się do 0, a w pliku 2Kints.txt — 528 takich trój­ ek. Programowi znacznie więcej czasu zajmuje ustalenie, że w pliku 4Kints.txt jest 4039 trójek sumujących się do 0, a w czasie oczekiwania na zakończenie sprawdzania pliku 8Kints.txt zadasz sobie pytanie: Jak długo potrwa wykonywanie programu? Jak się okaże, uzyskanie odpo­ wiedzi na to pytanie dla omawia­ % j a v a Threesum l K i n t s . t x t nego kodu jest łatwe. Dość często m ożna poczynić całkiem precy­ tik tik tik zyjne prognozy w czasie działania programu.

982455 -210707 -922943 -738817 85168 855430

Stoper W iarygodny pom iar do­ kładnego czasu działania progra­ mu może być trudny. Na szczęś­ cie, zwykle wystarczą szacunki. Chcemy móc odróżnić programy kończące pracę w kilka sekund lub m inut od tych, których działanie zajmuje kilka dni, miesięcy, a nawet więcej czasu. Chcemy też wiedzieć, kiedy jeden program jest dwa razy szybszy od innego w wykonywaniu da­ nego zadania. Nadal trzeba dokonać dokładnych pomiarów w celu wygenerowania danych eks­ perymentalnych, które m ożna wykorzystać do sformułowania i walidacji hipotez dotyczących zależności między czasem działania a rozmia­ rem problemu. Do pomiarów posłuży typ danych Stopwatch przedstawiony na następnej stronie. Metoda elapsedTime() zwraca czas (w sekun­ dach), który upłynął od czasu utworzenia obiek­ tu. Implementacja oparta jest na metodzie syste­ mowej currentTimeMi 11 i s () Javy, zwracającej aktualny czas w milisekundach. M etoda ta w m o­ mencie wywołania konstruktora zapisuje czas, a w chwili wywołania metody el apsedTime() jest ponownie używana do obliczenia czasu, jaki upłynął.

70 % j a v a Threesum 2 l C i n t s . t x t tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik tik

528 % j a v a Threesum 4 K i n t s . t x t

0

k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik k tik tik tik tik tik tik tik

4039 Obserwowanie czasu działania programu

1.4

a

Analizy algorytmów

I n t e r f e j s API public cla ss Stopwatch Stopwatch () double elapsedTim e()

Klient testowy

Tworzenie stopem Zwracanie czasu, ja ki upłynął od czasu utworzenia obiektu

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

1 87

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 prognozyweryfikacja. 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

program do przeprowadzania eksperym entów % java DoublingTest 250 0.0 500 0.0

public c la s s D oublingTest

(

p ub lic s t a t i c double t im e T r ia l( in t N) { // Czas d z ia ła n ia metody ThreeSum.count() dla N // losowych 6-cyfrow ych lic z b całkow itych, in t MAX = 1000000; i nt [] a = new i nt [N ]; 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 ();

1000 0 . 1 2000 0 .8

4000 6.4 8000 51.1

} 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);

)

Diagram logarytm iczny

5 1 ,2 -

Prosta o nachyleniu 3 .

25,6 1 2 ,8

-

6 ,4 3 ,2 “

5 tOl i'6“ 0 ,8 -

0,4 0,2 0,1

1 2 R o zm ia r p ro b le m u - N (w tysiącach)

Analizy danych eksperymentalnych (czas wykonania metody ThreeSum,countQ)

4

8

Ig W (w tysiącach)

1 89

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

191

Analizy algorytmów

193

w wyrażeniu tym, co typowe dla wyra­ ż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 ta umożliwia stosowanie przybliżeń z tyl­ dę, w których pomija się wyrazy o niskich potęgach, komplikujące wzory i mające niewielki wpływ na potrzebną wartość: Definicja. Zapis ~/(N) to reprezen­ tacja dowolnej funkcji, dla której wy­ nik dzielenia przez/(N ) zbliża się do 1 wraz z rosnącym N. g(N) ~ f{N ) ozna­ cza, że g(N )/f{N ) zbliża się do 1 wraz z rosnącym N.

Tempo wzrostu Opis

Funkcja

Stałe

1

Logarytmiczne

log N

Liniowe

N

Liniowologarytmiczne

N lo g N

Kwadratowe

N2

Sześcienne

N3

Wykładnicze

2n

Często spotykane funkcje tempa wzrostu

N 3/6

Przybliżenie oparte na najstarszym wyrazie wielomianu

Funkcja

Przybliżenie z tyldą

Tempo wzrostu

N 76 - N 2/2 + N/3

■N76

N3

N72 + NI2

~ N 2/2

N2

lg N + 1

~ lg N

lg N

3

~3

1

Typowe przybliżenia z tyldą

Przykładowo, przybliżenie ~ bP/6 opisuje liczbę wy­ wołań instrukcji i f w program ie ThreeSum, ponieważ NV6 - AP/2 + N/3 podzielone przez N 76 zbliża się do 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 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 ­ pa wzrostu zwykle nie podaje się podstawy, ponieważ szczegół ten można uwzględnić w a. Dotyczy stosun­ kowo nielicznych funkcji, często spotykanych przy analizowaniu tempa wzrostu czasu wykonania progra­ m u i pokazanych w tabeli po lewej stronie (wyjątkiem jest funkcja wykładnicza, którą omówiono w rozdziale „ k o n t e k s t ” ) . Bardziej szczegółowy opis tych funkcji i krótkie wyjaśnienie, dlaczego używa się ich w anali­ zach algorytmów, znajduje się po omówieniu progra­ mu ThreeSum.

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 sumujących się do 0 trójek wśród N liczb) wynosi N 3.

ThreeSum

(określa liczbę

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

public class ThreeSum

{ public static int count(int[] a)

{ int N = a.length; i n t c n t = 0;

g

B

ii

C

c

for (int i = 0 ; li < N: i++|l for (int j = i+1; |j < N; j++|) for (int k = j+l;|k < if

n

~N2/2 |

; k++|^

-NV6 I

Ca [i ] + a [ j ] + a [k] == 0) 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 instrukcji

Czas w sekundach

E

0

D

i

C

Liczba wywołań x

Łączny czas

(zależy od danych wejściowych) N3/ 6

- A P/2

+ NI 3

i, (7^/6 - AP/2 + AT/3)

2

N2/2 + NI 2

t2 (AP/2 + NI2)

B

3

N

t3N

A

4

1

Łączna suma

(i,/6) AP + {t 12 - 116) N2 2 1 + (f,/3 - tJ2 + i3) N + K + fox

Przybliżenie z tyldą

Tempo wzrostu

~

(tJ / 6 ) iSP (dla małego X) AP

Przykładowa analiza czasu w ykonania program u

193

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 (liczba operacji dostępu M odel kosztów Skoncentrujmy się na właściwoś­ do tablicy w celu odczytu ciach algorytmów. Określmy w tym celu model lub zapisu). kosztów, który wyznacza podstawowe operacje wy­ 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

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

195

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

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 logarytm binarny

LlgNj

Największa liczba całkowita nie większa niż lg N; (liczba 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 szeregu harmonicznego

Hw= 1 + Vi + 1/3 + 'A + ... + 1/N ~ ln N

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 Funkcja wykładnicza

( k ) ~ Nk/k\, gdzie k to mała stała (1 - \lx)x ~ \ le

Przybliżenia przydatne przy analizowaniu algorytm ów

197

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

Nazwa

Tempo wzrostu

Stałe

Logarytmiczne

log N

Liniowe

N

o

Analizy algorytmów

Typow y kod

Opis

Przykład

a = b + c;

Instrukcja

Dodawanie dwóch liczb

[zobacz stronę 59]

Dzielenie na pół

Wyszukiwanie binarne

Pętla

Znajdowanie maksimum

„Dziel i zwyciężaj”

Sortowanie przez scalanie

double max = a [ 0 ] ;

LiniowoN log N logarytmiczne

for (int i = 1; i < N; i++) if ( a [ i] > max) max = a [ i ] ;

[.zobacz ALGORYTM 2.4]

N1

f o r ( in t i = 0 ; i < N ; i++ ) fo r ( in t j = i+ 1 ; j < N; j++) i f ( a [ i] + a [j] == 0) cnt++;

Podwójna pętla

Sprawdzanie wszystkich par

Sześcienne

N}

f o r (in t i = 0; i < N; i++ ) f o r (in t j = 1+1; j < N; j++) f o r (in t k = j+1; k < N; k++) i f ( a [ i] + a [j] + a[k] == 0) cnt++;

Potrójna pętla

Sprawdzanie wszystkich trójek

Wykładnicze

2N

Wyszukiwanie wyczerpujące

Sprawdzanie wszystkich podzbiorów

Kwadratowe

[zobacz r o zd z ia ł 6.]

Podsum owanie popularnych hipotez dotyczących tempa wzrostu

199

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-logarytmiczne. Z uwagi na zwięzłość skracamy to do stwierdzenia, że sortowanie przez scalanie jest liniowo-logarytmiczne. Diagramy po lewej stronie pokazują, jak ważne jest tem po wzrostu w prakty­ Rozmiar p ro b lem u (w tysiącach) ce. Oś x określa rozmiar problemu, a oś y — czas wykonania. Diagramy pokazują, W y k re s lo g a r y tm ic z n y ż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 linioi 1 r~ i------1---- 1--- 1---- 1---- 1---- 1 wo-logarytmicznych algorytmów dla pod­ 1 2 4 8 512 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 nie jest zwiększany. ■ Jeśli wyszukiwanie binarne zwraca j > i, a [i] + a [j] = 0, dlatego należy zwiększyć licznik. ° Jeżeli wyszukiwanie binarne zwraca j z przedziału od 0 do i , także otrzy­ mujemy a [i] + a [j] = 0, ale nie należy zwiększać licznika, aby nie zliczać par dwukrotnie. Wynik obliczeń jest dokładnie taki sam jak w algorytmie kwadratowym, ale roz­ wiązanie działa znacznie szybciej. Czas wykonania sortowania przez scalanie jest proporcjonalny do N log N, a N wyszuki­

public cUss TwoSumFast 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ę A rra y s.s o r t (a ); in t N = a .le n gth ; in t cnt = 0; f o r (in t i = 0 ; i < N; 1++) i f ( B in a r y S e a r c h .r a n k (- a [ i], a) > i) cnt++; return cnt;

0.

p u b lic s t a t ic void main ( S t r in g ! ] args)

1 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 ));

1 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 pytania brzmią: nie dla sum par p u b lic s t a t ic in t c o u n t{in t[] a) { // 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 ; cji liniowych lub kwadratowych in t cnt = 0; 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) że najlepszy możliwy algorytm cnt++; dla sum trójek jest kwadratowy. return cnt; Dolne ograniczenie tempa wzro­ stu czasu wykonania dla najgor­ p u b lic s t a t ic void m a in (S trin g [] args) 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 podRozwiązanie o złożoności N2 Ig N dla problemu sum trójek

1.4

rozdziale 2.2 w kontekście sortowania. Niebanalne dolne ograniczenia trudno jest ustalić, są jednak bardzo przy­ datne w poszukiwaniu wydajnych algorytmów. PRZY K ŁA D Y Z T E G O

P O D R O Z D Z IA Ł U SĄ PODSTAW Ą d o

a

Analizy algorytm ów

203

Algorytm

Tempo wzrostu czasu wykonania

TwoSum

N2

TwoSumFast

N log N

ThreeSum omawiania algorytmów w tej książce. Zastosowano opisa­ 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

1 p u b lic s t a t ic double t im e T r ia l( in t N) // Tak samo, jak w programie D oublingTest (stron a 189) p u b lic s t a t ic void m a in (S trin g [] args)

{ double prev = t im e T r ia l(125); fo r (in t N = 250; true ; N + s N)

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;

1 1

)

Wyniki eksperymentów % ja va D oublingR atio 0.0 2.7 250 500 0.0 4.8 1000 0.1 6.9 2000 0.8 7.7 6.4 8.0 4000 8000 51.1 8.0

Prognozy 16000 408.8 32000 3270.4 64000 26163.2

8.0 8.0 8.0

1.4

Q

Analizy algorytm ów

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. 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. należy

rozw ażyć

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-

205

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 10krotnie 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 .. , , ... , 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

T e m p o w z ro s tu c z a s u r

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 n a 10 ra z y s z y b s z y m k o m p u

O p is

F u n k c ja

C z y n n ik 2

C z y n n ik 10

Liniowe

N

2

10

D zień

K ilka g o d z in

Liniowologarytmiczne

N lo g N

2

10

D zień

K ilka g o d zin

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

2n

2 9N

N ig d y

N ig d y

Wykładnicze

2

n

c z a s d la 10/V

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

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 internetu 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.

207

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 haszują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

N + 4 + 8 + 1 6 + ... + 2 N = 5 N - 4

a

Analizy algorytm ów

211

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 N T 5 >/ Czerwone kropki określają potrzebnych przy inicjowaniu struktury O średnią skum ulowaną 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 Zamortyzowane koszty dodawania operacja zajmuje czas liniowy. Są to analizy 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. jest odkrycie tylu ważnych informacji o algo­ 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. z a d a n ie m

a n a l it y k a a lg o r y t m ó w

5

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 alokaTyp Bajty cji pamięci. Ma on zwolnić programistów z konieczności zajmowania się pamięcią. Oczywiście, w odpowiednich sytuacjach warto korzystać boolean 1 z tego mechanizmu. Jednak trzeba wiedzieć (przynajmniej w przybliże­ byte 1 niu), kiedy pamięciowe wymagania program u uniemożliwią rozwiąza­ nie danego problemu. char 2 Analizowanie wykorzystania pamięci jest dużo łatwiejsze od anali­ in t 4 zowania czasu wykonania — przede wszystkim dlatego, że nie trzeba float uwzględniać tak wielu instrukcji program u (ważne są tylko deklaracje), 4 a analizy pozwalają zredukować złożone obiekty do typów prostych, long 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

adresów służy 8 bajtów, co jest typowe dla powszechnie używanych obecnie architek­ tur 64-bitowych. Warto jednak pamiętać, że w wielu starszych maszynach używano ar­ chitektury 32-bitowej, która wymagała tylko 4 bajtów na adres maszynowy.



Analizy algorytmów

213

Obiekt nakładkowy dla liczb całkowitych 24 bajty p u b lic c l a s s In t e g e r

{

p r iv a t e i n t x;

}

Narzut dla obiektu

Dopełnienie

Obiekt dla daty

32 bajty

Wartość typu i n t

Obiekty Aby określić, ile pamięci zajmuje p u b l i c c l a s s D a te { Narzut obiekt, należy dodać ilość pamięci zajmo­ p r iv a t e i n t day; dla p r i v a t e i n t m onth; waną przez każdą zmienną egzemplarza obiektu p r iv a t e in t ye a r; do narzutu powiązanego z każdym obiek­ day Wartości month 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 32 bajty pamięci i informacje związane z synchroni­ Obiekt licznika p u b lic c l a s s C ounter zacją. Ponadto pamięć jest zwykle dopełnia­ { Narzut p r i v a t e S t r i n g name; na do wielokrotności 8 bajtów (jest to słowo dla p r iv a t e i n t co u n t; Referencja obiektu do obiektu maszynowe w maszynach 64-bitowych). Na }" ^ s trin g przykład obiekt typu Integer zajmuje 24 Wartość cou nt bajty (16 bajtów narzutu, 4 bajty na zmienną 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 { Narzut dą zmienną egzemplarza typu i nt i 4 baj­ 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

T a b lic a

215

d o u b le t ] c = new d o u b le [ N ] ;

a = new i n t [ N ] ;

Narzut dla obiektu

Wartość typu i n t (A bajty)

Analizy algorytmów

Tablica w a rto ści ty p u d o u b l e

w a rto ści ty p u i n t

in t []

o

16 bajtów

Wartość typu i n t (4 bajty)

N Dopełnienie

16 bajtów

Narzut dla obiektu

N Dopełnienie

. A/ wartości ; typu i n t ' (4A/ bajtów)

N wartości typu d o u b le y / (8N bajtów)

24 + 8 N 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 ) d o u b le f ] t] t ; t = new d o u b le fM ] [N] ;

16 bajtów

M referencji (8M bajtów)

Date[]

~40/V

doublet] []

~8NM

Łącznie: 24 + 8 M + M x (24 + 8N) = 24 + 32 M + 3 MN

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

N wartości typu d o u b le j S / (8N bajtów)

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)

40bajtów p u b lic c l a s s S t r in g

{

p r iv a t e p r iv a t e p r iv a t e p r iv a t e

c h a r [ ] v a lu e ; in t o ffs e t; i n t c o u n t; i n t h a sh ;

}”

Narzut dla obiektu

v a lu e o ffse t h a sh

- Referencja

■" Wartości ' typu i n t

Dopełnienie

P rz y k ła d o w y p o d ła ń c u c h S t r i n g genome = CGCCTGGCGTCTGTAC"; 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

Za pomocą metody substring () można utworzyć nowy obiekt typu S tring (40 bajtów), ko­ rzystając jednak z tej samej tablicy value[], dlatego podlańcuch istniejącego łańcucha zna­ ków zajmuje tylko 40 bajtów. Tablica znaków zawierająca pierwotny łańcuch znaków otrzy­ muje nową nazwę w obiekcie podłańcucha. Pola z pozycją i długością wyznaczają podłańcuch. Ujmijmy to inaczej — podłańcuch zajmuje stałą ilość dodatkowej pamięci, a utworzenie go zajmu­ je stały czas, nawet jeśli długości łańcucha i pod­ ł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 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. te

1.4



Analizy algorytmów

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.

217

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

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 liniowologarytmiczny, 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.

219

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 fo r (in t

= N; n > 0; n /= 2) i = 0 ; i < n; i++)

sum++; b) in t sum = 0 ; fo r (in t i fo r (in t

sum++;

= 1; i < N; i * = 2 ) j = 0;j < i ; j++)

1.4

c)

a

Analizy algorytmów

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-logarytmicznego. 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 howMany(), 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

221

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

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.

Struktura danych

Typ elementu

Lista powiązana

i nt In teger

Tablica o zmiennej wielkości

i nt In teger

Koszt umieszczenia N wartości typu int -------------------------------------------------------------------Referencje do danych Tworzone obiekty

N 3N

N 2N

-5 N -5 N

lg N ~N

2

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.

Struktura danych

Typ elementu

Pamięć potrzebna dla N wartości typu int (w bajtach)

Lista powiązana

in t In te g e r

-32 N -56 N

Tablica o zmiennej wielkości

in t In teger

Od -4 N do -16 N Od -32 N do -56 N

W ykorzystanie pamięci w stosach (różne implementacje)

225

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 autoboxingu i autounboxingu. Opracuj implementację typu FixedCapacityStackOfInts i użyj klienta podobnego do programu Doubl i ngRati o do porównania jej wydajności z generycznym typem FixedCapacityStack 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

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.

227

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 środowiskach programistycznych można zadeklarować dwie zmienne jako równoznaczne (są wtedy referen­ cjami do tego samego obiektu). Po serii takich dekla­ racji system musi mieć możliwość określenia, czy dane dwie nazwy są równoznaczne. Jest to jedno z wczes­ nych zastosowań (związane z językiem programowania FORTRAN), które doprowadziło do opracowania oma­ wianych dalej algorytmów. Zbiory m atem atyczne Na bardziej abstrakcyjnym po­ ziomie m ożna traktować liczby całkowite jak wartości zbiorów matematycznych. Przy przetwarzaniu pary p q należy sprawdzić, czy elementy należą do tego samego zbioru. Jeśli nie, należy połączyć zbiory zawierające p i q, umieszczając je w jednym zbiorze.

0.

1. 2.

5.

6. 7m 8# 9# •

3. 4.

0—

0

4 3 3 8

6 5

9 4

2 1

• ro •



:

u :n n

5 9

5 0

7 2

Nie należy wyświetlać par, które już sq połączone

a b y u j e d n o l i c i ć o p i s , w dalszej części podrozdziału 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

1

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

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

Inicjowanie N p u n któ w nazwami w postaci liczb całkowitych (od 0 do N-lJ

UF ( i nt N)

void in t

u n io n (in t p, in t q)

, boolean connected(int p, in t q) in t

count ()

Dodawanie połączenia między p a ą Identyfikator składowej dla p (od 0 do N -l)

find (i nt p)

Zwraca true, jeśli p i q znajdują się w tej samej 1 r j x i 1 składowej 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

231

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 jednowierszowej 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­ stępnej strony. Przechowywane są dwie zmienne egzemplarza: 10 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 Aby przetestować przydatność interfejsu API i przygoto­ 9 4 wać podstawy do pisania kodu, w metodzie main() umieści­ 2 1 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 i ciąg par liczb całkowitych (każda z przedziału od 0 do N-l), 6 1 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. Jeżeli punkty nie są połączone, program wywołuje metodę % more mediumllF.txt 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, użyte w krótkim przykładzie przedstawionym na stronie 229; [lic z b a połączeń: 900] 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 z dwoma milionami połączeń dla miliona punktów. Celem jest 786321 134521 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­

% morę tin y U F .tx t

p o d s u m u jm y



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[] private

id;

i n t count;

// // //

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 ) , 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 id [i]

= i ;

} p ublic i n t count() ( re tu rn count; } p u b l i c boolean c o n n e c t e d ( i n t p, { r e t u r n f i n d ( p ) == f i n d ( q ) ; }

int

q)

4

3

3 6 9 2 5

8 5 4 1 0

uf

< t in y U F . tx t

72 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] p q 0 1 2 3 4 5 6 7 8 9 wtedy, jeśli p i q należą do tego same­ 59 1 1 1 8 8 1 1 1 8 8 go komponentu). Aby zachować nie­ 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 59 1 1 1 8 8 1 1 1 8 8 jest, nie trzeba nic robić. W przeciw­ 8 8 8 8 8 8 8 8 8 8 nym razie jest tak, że wszystkie elemen­ Przegląd techniki z szybką metodą find ty tablicy i d [] odpowiadające punktom 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

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­ no do problemu dynamicznego określania połączeń. Jeśli 3 4 5 6 7 8 9 0 1 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) ~ ■> 5 6 7 8 9 8 0 1 2 3 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 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 rośnie kwadratowo. Analizy te można uogólnić i stwier­ T 4 0 1 8 8 5 5 7 8 9 dzić, że technika z szybką metodą find () jest kwadratowa 0 1 2 8 8 5 5 7 8 8 dla typowych zastosowań, w których ostatecznie liczba 1 0 1 2 8 S 5 5 7 8 8 0 1 1 8 8 5 5 7 8 8 składowych jest niewielka. Za pomocą testu podwajania 9 0 1 1 8 8 5 5 7 8 8 można łatwo sprawdzić tę hipotezę na własnym kompu­ 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 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 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 0 1 1 1 o\ 8 11' 1 1 S 8 largeUF.txt. Jeśli nadal nie jesteś przekonany i uważasz, że 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 dla par z pliku largeUF.txt. Nieunikniony id [ p ] n o id [q ] (wyróżnione) wniosek jest taki, że nie można rozwiązać i d [p] i i d [q] są takie same, dlatego zmiany nie są potrzebne takiego problemu za pomocą algorytmu z szybką metodą find(), trzeba więc po­ Ślad działania techniki z szybką metodą find szukać lepszych algorytmów.

p q 4 3 3 6 9 2 8 5 7 6 1 6

0 1 2 3 4 5 6 7 8 9

235

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) { // Wyszukiwanie nazwy sktadowej. punktu, który posiada odnoś­ 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); in t qRoot = find(q); ces prowadzi do tego samego i f (pRoot == qRoot) re turn; korzenia. Aby proces był po­ prawny, metoda union(p, q) id [pRoot] = qRoot; musi zachowywać opisany nie­ count— ; zmiennik. Można łatwo osiąg­ 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­ mentacja zmienia nazwę id [ ] to reprezentacja lasu drzew 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 m etodą union() na pliku f i n d (5 ) to f i n d (9 ) to 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. ® © @ © © © © ® ® ® 0 1 2 3 4 5 6 7 8 9 p q 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­ 8 9 0 1 1 8 3 5 5 7 8 8 nie dochodząc do korzenia 5 0 0 1 1 8 3 5 5 7 8 8 drzewa zawierającego dany 0 1 1 8 3 0 5 7 8 8 węzeł. To ostatnie stwier­ dzenie m ożna udowodnić 72 0 1 1 8 3 0 5 7 8 8 przez indukcję. Prawdą jest, 0 1 1 8 3 0 5 1 8 8 że po zainicjowaniu tablicy każdy węzeł posiada odnoś­ 0 1 1 8 3 0 5 1 8 S nik do samego siebie. Jeśli 6 1 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

lo

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[] wej. Jednak o ile jest szybszy? Analizowanie ® © (D © 1 2 3 4 ... p q kosztów techniki z szybką metodą u n io n () ( p © © (?) i 0 1 3 4 ... 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 (4) 0 3 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).

1O

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 , / przypadki podobne do opisanego się v drzewo ) ( Może umieścić nie zdarzą. Zamiast arbitralnie łączyć 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 < sr więcej kodu i nowej tablicy na przecho­ f Mniejsze Większe \ / Mniejsze wywanie liczby węzłów, co pokazano na V drzewo drzewo ) V drzewo stronie 240, jednak zapewnia znaczną S z y b k a m e to d a u n i o n O z w a g a m i poprawę wydajności. Jest to algorytm 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.

większe drzewo

Większe drzewo J

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 % java WeightedQuickUnionUF < mediumUF.txt uni on () jest zawsze równa (i jest potęgą dwój­ 528 503 ki). Przedstawione struktury drzewiaste wy­ 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ą.

\ 1

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;

// // private i n t [] sz; // // private in t count; //

Odnośniki do rodziców (w t a b l ic y indeksowanej punktami). Wielkości składowych określonych za pomocą korzeni (w t a b l ic y indeksowanej punktami), 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

p q

®©®©®®®0

@ © © ( 4 ) © © ® ® ® ®

o i

® © © © © © ®

®©©

2 3

® ® @ © © © © ® ® ® p q

6 5

® © ©

9 4

® © ©

2 1

® © © ©

241

Studium przypadku — problem Union-Find

D ane w ejściow e dla n ajg o rszeg o przypadku

Przykładow e dane wejściow e

4 3

h

©©®® © © ®

© ® ® (?)

4 5

JS i © ®

6 7

& ® © © 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 ®

JK

Ą

o o

A^ 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

Algorytm

0

Studium przypadku — problem Union-Find

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() z wagami i kompresję ścieżek

N

Niemożliwe

N

Bardzo, bardzo blisko 1 (z amortyzacją); zobacz ć w i c z e n i e 1 .5.13 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.

243

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­ 1300sunku po lewej stronie pokazano szczegóło­ wo wydajność algorytmów wspomagających tworzenie aplikacji klienta do dynamicz­ nego określania połączeń dla przykła­ Jedna szara kropka dla każdego połączenia du z 625 punktami (plik mediumUF. przetworzonego przez klienta 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ą ¿-tego przetwarzanego połączenia należy przynajmniej 625 dostępów 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 458 Czerwone sumę dotychczasowych dostępów do tablicy. kropki oznaczają Następnie wystarczy narysować szarą kropkę skumulowaną średnią 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­ łanie algorytmu. W technice z szybką me­ Operacje metody c o n n e c te d ( ) wymagają dokładnie dwóch dostępów do tablicy todą find() każda operacja union() wymaga \ ______ przynajmniej 625 dostępów (plus jeden na “n Liczba p o łączeń 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­ Operacje f i n d O łączeń wymaga wywołania metody union(), 1 0 0 —1 stają się kosztowne dlatego skumulowana średnia jest bliska 625. Później większość połączeń prowadzi do wy­ OJ 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 kowo wysoka. Dane wejściowe, dla których 0 - 1duż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

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.

245

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 nieid [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ą

z wagami ustawiono i d [find (p)] na q zamiast na i d [find ( q ) ] . Czy uzyskany algorytm będzie poprawny? union()

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

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ść.

249

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

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.

251

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, 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.

S

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

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 [] { C om parable t = a [ i ] ;

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, i n t i ,

a [i ] = a [ j ] ; a [ j ]

= t;

in t j)

}

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

W klasie tej przedstawiono konwencje używane dalej do implementowania technik sortowania tab­ lic. Dla każdego algorytmu sortowania pokazano metodę s o rt() z podobnej klasy, przy czym nazwę Example zmieniono na nazwę odpowiednią dla al­ gorytmu. Klient testowy sortuje łańcuchy znaków ze standardowego wejścia, jednak metody sortowa­ nia zadziałają dla dowolnego typu danych imple­ mentującego interfejs Comparabl e.

S 0 R T E X A M P L E % j a v a Example < t i n y . t x t A E E L M O P R S T X

% more w o r d s 3 . t x t bed bug dad y e s zoo . . . a l l bad y e t % j a v a Example < w o r d s . t x t all

bad bed bug dad . . . y e s y e t zoo

257

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ść).

Model kosztów dla sortowania. Przy analizowaniu algorytmów sortowania liczone są porównania i przestawienia. Dla algorytmów, które nie przestawiają elementów, liczone są dostępy do tablicy.

Czas w ykonania Testujemy też wydajność algorytmów. Zaczynamy od udowodnienia faktów na temat liczby podstawowych operacji (porównań i przestawień oraz czasem liczby dostępów tablicy w celu odczytu lub zapisu), które różne algorytmy sortowania wykonują dla rozmaitych naturalnych modeli danych wejściowych. Następnie używamy tych faktów do opracowania hipotez dotyczących względnej wydajności algorytmów. Prezentujemy też narzędzia do eksperymentalnego sprawdzania hipotez. 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]; f o r ( i n t i = 0; i < N; i+ + )

do posortowania N losowych wartości typu Double. Przy samodzielnym tworzeniu typów można umożliwić w kodzie klienta sortowanie

= S t d R a n d o m . u n if o r m O ; Quick.s o r t ( a ) ;

danych określonego typu, implementując inter-

Sortowanie tablicy losowych wartości

a [i]

2.1



Podstawowe metody sortowania

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­ p u b l i c c l a s s Date implements Comparable bę całkowitą — ujem ną (zwykle - 1 ) dla { vw. 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­ p u b l i c i n t d a y() { r e t u r n day; } śli v i w mają niezgodne typy lub jedna 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 ) 1 0 zwrotna (v=v dla każdego v), 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 vv, 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; i f ( t h is . m o n t h < th at .m o nth ) r e t u r n - 1 ; to w=v), 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 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 merge(), jeśli a [mi d] 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) ; if (j == k) return a [ k ] ; tablicy). Jednak podejście to wymaga sor­ e lse i f (j > k) hi =j - 1; towania, dlatego czas wykonania jest lie ls e i f (j < k) lo =j +1; niowo-logarytmiczny. Czy można uzyskać } return a [ k ] ; lepszy wynik? Znalezienie k najmniejszych 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­ cji od a [0] do a [k - 1 ] są mniejsze (lub równe) względem P o d z ia ł w celu z n ale z ie n ia m e d ia n y

2.5

Q

Zastosowania

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 randomizacja 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.

359

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-logarytmicznych 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

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­

361

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

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.

363

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

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

( 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.

365

366

RO ZD ZIA Ł 2



ĆWICZENIA

Sortow anie

(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 wersji oprogramowania, na przykład 115.1.1, 115.10.1, 115.10.2. Zaimplementuj interfejs Comparable tak, aby wersja 115.1.1 była mniejsza niż 115.10.1 itd. 2.5.11. Jeden ze sposobów na opisanie wyników algoryt­ m u sortowania polega na określeniu permutacji p[] dla 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]. Podaj permutacje, które opisują wyniki sortowania przez wstawianie, sortowania przez wybieranie, sortowania Shella, sortowania przez scalanie, sortowania szybkiego i sortowania przez kopcowanie dla tablicy zawierającej siedem równych kluczy.

Dane wejściowe (wartość transakcji dla indeksu DJI z poszczególnych dni) l-0 c t -2 8 2 -0 c t-2 8 3 -0 c t-2 8 4 -0 c t-2 8 5 -0 ct-2 8

3500000 3850000 4060000 4330000 4360000

30-Dec-99 31-Dec-99 3-Jan-00 4-Jan-00 5-Jan-00

554680000 374049984 931800000 1009000000 1085500032

Dane wyjściowe 19-Aug-40 26-Aug-40 2 4 - J u l-40 10-Aug-42 23-Jun-42

130000 160000 200000 210000 210000

23-J u l -02 17-J u l -02 15-J u l -02 19-J u l -02 2 4 - J u l-02

2441019904 2566500096 2574799872 2654099968 2775559936

2.5

a

Zastosowania

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ę compareTo(), 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-maile z kont innych użytkowników z tej samej domeny. Przykładowo, możesz wysłać fałszywy e-mail od użytkownika [email protected] do [email protected]. 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

aby 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 (). ć w ic z e n ia

2 .1 .1 6 ,

367

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-logarytmicznym 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

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.

369

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­ 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.

W

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

Indeks w książce

Wyszukiwanie odpowiednich stron

Nazwa

Lista numerów stron

System wymiany plików

Wyszukiwanie utworów do pobrania

Tytuł piosenki

Identyfikator komputera

Zarządzanie kontem

Przetwarzanie transakcji

Numer konta

Szczegóły transakcji

Wyszukiwanie w sieci W W W

Wyszukiwanie adekwatnych stron WWW

Słowo kluczowe

Lista stron

Kompilator

Wyszukiwanie typu i wartości

Nazwa zmiennej

Typ i wartość

Typowe zastosowania tablicy symboli

3.1

a

Tablice symboli

Interfejs API Tablica symboli to prototypowy abstrakcyjny typ danych (zobacz Reprezentuje dobrze zdefiniowany zbiór wartości i operacji na nich, 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. r o z d z i a ł i.).

p u b lic c la s s ST

Tworzy tablicę symboli

ST() void put(Key key, Value v a l)

Val ue get(Key key) void d elete(Key key)

Umieszcza parę klucz-wartość w tablicy (jeśli wartość to nul 1 , klucz key należy usunąć z tablicy) Zwraca wartość powiązaną z kluczem key (nul 1 .jeśli key nie istnieje) 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 ( ) Iterable k e y s()

Zwraca liczbę par klucz-wartość obecnych w tablicy 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 generycznych 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

375

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

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

377

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 ST () void put(Key key, Value v a l)

Value get(Key key)

void delete(Key key)

Tworzy uporządkowaną tablicę symboli Umieszcza parę klucz-wartość w tablicy (usuwa klucz key z tablicy, jeśli wartość to nul 1) Zwraca wartość powiązaną z kluczem key (nuli, jeśli taki klucz nie istnieje) 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 keys(Key lo , Key h i)

Zwraca klucze z przedziału [lo . .h i] (posortowane)

Ite rab le 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 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­ Klucze Wartości nieją też m etody do usuwania kluczy mi n O — 0 9 : 0 0 : 0 0 G d ańsk maksymalnego i minimalnego (oraz 09:00:03 Poznań powiązanych wartości). Z uwagi na te Kraków 0 9 :0 0 :J 3 metody tablica symboli może działać get(09:00:13) 09:00:59 G d ańsk Kraków tak jak klasa In d e x M in P Q () omówiona 09:01:10 f1oor(09:05:00) — 0 9 : 0 3 : 1 3 G d ańsk w p o d r o z d z i a l e 2 .4 . Główne różnice 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ń 09:19:32 G d ańsk klucze (co jest niedozwolone w tablicach 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 c e i1 in g (0 9 :3 0 :0 0 ) obliczenie na podstawie otrzymanego 09:35:21 G d ańsk 09:36:14 szcze ci n klucza podłogi (ang. floor), czyli naj­ 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 Przykłady operacji na uporządkowanej tablicy symboli większego lub równego względem dane­ 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? Dwuargumentowe 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(), de1 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)

Iterable ke ys()

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 ); 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 compareTo() (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, Model kosztów przy wyszukiwaniu. W cza­ kiedy jest inaczej, liczone są też dostępy sie badania implementacji tablicy symboli do tablicy. 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 { wyświetla tablicę. W śladach działania za­ ST s t ; st = new ST < Strin g, In te g e r> (); kładamy, że dane wejściowe to ciąg jednoznakowych łańcuchów. Najczęściej używa­ 0; IS t d ln .is E m p t y O ; 1++) fo r ( in t i my łańcucha znaków "S E A R C H E X A M { P L E". Klient łączy klucz S z wartością 0, S t rin g key : S t d ln . r e a d S t r in g O ; 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 1 lub 6), natomiast a ■ — z wartością 8 (a nie 2 ), f o r (S tr in g s : s t . k e y s ( ) ) 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 P L E klienta testowego jest nieokreślona (zależy E A R C H 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 przykładowy klient używający indeksu, po­ (jedna możliwość) zwalający zilustrować specjalny przypadek 8 11 podstawowego zastosowania tablicy symboli, 10 4 12 9 opisanego w p o d r o z d z i a l e 3 .5 . 5 7 11 5 9 4 10 3 3 8 12

0

0

7

Klucze, wartości i dane wyjściowe klienta testowego

3.1

n

Tablice symboli

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 i t was i t was i t was i t was i t was

t in y T a le . tx t the best o f times i t was the worst o f times the age o f wisdom i t was the age o f f o o lis h n e s s the epoch of b e lie f i t was the epoch of in c r e d u lit y the season o f lig h t i t was the season of darkness 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 słów

Różne słowa

Liczba słów

Różne słowa

Liczba słów

Różne 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

383

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 st = new ST(); 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 b u sin e ss 122

8 < t a le . t x t

% java FrequencyCounter 10 < le ip z ig lM . t x t government 24763

3.1

o

Tablice symboli

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).

385

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. fi rst

Klucz

Wartość

S

0

s

E

1

E 1

S

A

2

A 2

E 1

S

0

R

3

R 3

A 2

E

1

C

0

Czerwone węzły sq nowe

0

Czarne węzły sq sprawdzane przy wyszukiwaniu

S

0

4

C 4

R

3

A

2

E 1

S 0

H

5

H 5

C 4

R

3

A 2

E 1

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

A

8

X 7

H 5

C

4

R

3

A

S | 0

M

9

M 9

X

7

H

5

C 4

R 3

E 6

S

P

10

P 10

M 9

X

7

H 5

C 4

A

8

E 6

S

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

Zakreślone pozycje to zmieniane wartości

Szare węzły - pozostajq nietknięte

0 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

{ 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

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

389

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] Klucz

Wartość

0

1

S E A

0 1 2

S E A

S

R C H

3 4

A A

5

A

E

6 7

A

X A

M P

L E

8 9

10 11 12

E

2

3

4

5

va ls[] 6

A

M P M P

A

c

M

A A A A

E

8

9

Czerwone elementy ^ zostały wstawione

s

E R S C E R s c E H R c E H R c E H R c E H R c E H M c E H M c E H L c E H L

A

7

H

L

Szare elementy iig ¿11 ymionih/ iKlic licimy pozycji

S s s s

X X

R

S

P

R

P

X S X R S X R

S

X

R

S

X

N

0

1

2

1 2 3

0 1 2

0 1

0

4

1

1

5 6

4

3 1

y

4

1

3 5

0 3 JL

4 © 4 6 4 6

5 5

~T 0 0 3 3 0

7 y

9

3

0

7

9

10

3

0

5 11 5 11

9 10 9 10

3

5 11

9 10

3

6 7 7

y 2 ®

8 9

8 4

6

8

10 10

8 8

6 6

8

4 4 4

© 4 12

3

0

5 5

4

5

6

7

8

9

Czarne elementy przesunięto wprawo

X

Ślad działania standardowego klienta używającego indeksu; implementacja tablicy symboli oparta jest tu na tablicy uporządkowanej

Zakreślone elementy zmieniły wartość

3

7 0 0 0

7 7 7

3.1

Tablice symboli

391

ALGORYTM 3.2. Wyszukiwanie binarne (w tablicy uporządkowanej) public c la s s BinarySearchST

{ 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, wyszukiwanie binarne, użyty jako przykład w r o z d z i a l e 1 . Kod przechowuje indeksy z posortowanej tab­ p u b lic in t rank(Key key, in t lo , in t h i) licy kluczy, co pozwala ograniczyć podtablicę, 1 i f (hi < lo ) return lo ; w której może znajdować się klucz wyszukiwa­ in t mid = lo + (hi - lo ) / Z; nia. Jeśli klucz wyszukiwania jest mniejszy niż in t cmp = key.com pareTo(keys[m id]); klucz w połowie podtablicy, należy przeszukać if (cmp < 0) 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) ; połowę podtablicy. Trzecia możliwość jest taka, e lse return mid; że środkowy klucz jest równy szukanemu. W ko­ 1 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 nierekurencyjna 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 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[]____________ 3 4 5 6 7 8 9 1 2

Udane wyszukiwanie P 0

lo

h i mid

0

9

4

A

c E

H

L

5

M

P

9

7

A C E

H

L

M

P

R R

5 6

5

A C E

H

L

M

P

R

6

6

A

C E

H

L

M P

6

r ^

Nieudane wyszukiwanie Q

S S

x X

a [lo . .h i]

S X

C zerw ona litera to element a [mid]

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

P

R

S

X

6

5

A

C E

M

P

6

A C E

L L L

M

5

H H H

R R

S X S X

7__6

M P

Czarne litery to elementy

>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 keys(Key lo, Key hi)

{ Queue q = new Queue(); 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

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.

395

396

RO ZD ZIA Ł 3



W yszukiwanie

Metoda

Tempo wzrostu czasu wykonania

put ()

N

get ()

log N

d eleteO

N

co n tains()

log N

si ze ()

1

min()

1

max()

1

floorO

log N

c e ilin g O

log N

rank()

log N

se le c tO

1

deleteM in()

N

deleteMax()

1

Mimo gwarantowanego logarytmicznego czasu wyszuki­ wania klasa Bi narySearchST nie umożliwia używania klientów w rodzaju FrequencyCounter do rozwiązywania dużych prob­ lemów, ponieważ metoda put () jest zbyt wolna. Wyszukiwanie binarne zmniejsza liczbę porównań, ale nie czas wykonania, ponieważ jej zastosowanie nie zmienia tego, że liczba dostę­ pów do tablicy potrzebnych do zbudowania tablicy symboli w tablicy uporządkowanej rośnie kwadratowo wraz z roz­ miarem tablicy, jeśli klucze są uporządkowane losowo (oraz w typowych sytuacjach w praktyce, kiedy to klucze, choć nie są losowe, są dobrze opisane przez ten model). Twierdzenie B (ciąg dalszy). Wstawienie nowego klu­ cza do uporządkowanej tablicy o rozmiarze N wymaga dla najgorszego przypadku ~ 2N dostępów do tablicy, tak więc wstawienie N kluczy do początkowo pustej tablicy wymaga dla najgorszego przypadku ~ N 2 dostępów do tablicy. Dowód. Taki sam, jak dla

do w o d u a

.

Dla książki Tale of Two Cities, w której różnych kluczy jest 104, 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. Koszty w klasie Bi narySearchST

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

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.

Algorytm (struktura danych)

Koszt dla najgorszego przypadku (po IM wstawieniach)

Koszt dla typowego przypadku (po N losowych wstawieniach)

Wydajna obsługa operacji na uporządkowanych

Wyszukiwanie

Wstawianie

Trafienie

Wstawianie

danych?

lg N

2N

lg N

N

T ak

Wyszukiwanie

sekwencje (nieuporządkowana lista powiązana) Wyszukiwanie binarne (tablica 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ć.

397

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

Lista powiązana (wyszukiwanie sekwencyjne)

SeąuentialSearchST

Tablica uporządkowana (wyszukiwanie binarne)

BinarySearchST

Binarne drzewo wyszukiwań

BST

Zbalansowane binarne drzewo wyszukiwań

RedBlackBST

Tablica

SeperateChainingHashST *-i nearProbi ngHashST

z haszowaniem

Zalety

Najlepsza dla małych tablic symboli

Wady

W olna dla dużych tablic symboli

Optymalne wyszukiwanie Wolne wstawianie i wykorzystanie pamięci; obsługa operacji zależnych od uporządkowania Łatwa w implementacji; obsługa operacji zależnych od uporządkowania

Brak gwarancji; potrzebna pamięć na odnośniki

Optymalne wyszukiwanie Potrzebna pamięć na odnośniki i wstawianie; obsługa operacji zależnych od uporządkowania Szybkie wyszukiwanie ; wstawianie dla popularnych typów danych

Zalety i wady im plem entacji tablic symboli

Wymaga haszowania dla każdego typu; brak obsługi operacji zależnych od uporządkowania; wymaga pamięci na odnośniki i pustą tablicę

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 zamiast stosować odrębne klucze i wartości?

o d ro z d z ia le

2 .4 ),

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 compareToO? 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

Ć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?

401

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

; 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ądkują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-wartość 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ł.

403

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

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 samoporzą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.

405

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 węzeł m a dokładnie dwa odnośniki (lewy i prawy), S tru k tu ra d rz e w a b in a rn e g o 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 w węzłach i używamy zwrotów w rodzaju „A Lew y jest lewym dzieckiem E” w których węzły są o d n o ś n ik E W artość utożsamiane z kluczami. Linie łączące węzły p o w ią z a n a 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 wartości). Odnośniki każdego węzła łączą go S tru k tu ra b in a rn e g o d rz e w a w y sz u k iw a ń 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. K lu c z

3.2



409

Drzewa wyszukiwań binarnych

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) 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 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:

8

s

x

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 private Node root;

// Korzeń drzewa BST.

private c la s s Node private private private private

Key key; Value v a l ; Node l e f t , rig h t; in t N;

// // // // //

Klucz. Powiązana wartość. Odnośniki do poddrzew. 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 Metody Metody Metodę

min(), max(), floor() i c e i l i n g ( ) przedstawiono na stro n ie 419. se le c t() i rank() przedstawiono na stro n ie 421. delete(), deleteMin() i deleteMax() przedstawiono na stronie 423. 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

Czarne węzły m o gą p a so w ać do klucza w yszukiw ania

Rjest większe niż E, dlatego należy szukać p o prawej

N ieu d ane w yszu kiw a nie T

R jest mniejsze niż S, dlatego należy szukać po lewej

Szare węzły na pew no nie pasują do klucza wyszukiw ania

Znaleziono R (trafienie), dlatego należy zwrócić wartość

T jest większe niż S, dlatego należy szukać p o prawej

T jest mniejsze niż X, dlatego należy szukać p o lewej O dnośnik jest pusty, dlatego T nie znajduje się w drzewie (chybienie)

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

Rekurencja Warto poświęcić czas na zrozu­ mienie działania rekurencyjnych implemen­ tacji. Można sobie wyobrazić, że kod przed rekurencyjnymi 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 pustym odnośniku przechodzi w górę drzewa. W metodzie get () 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 i zwiększanie liczby węzłów ce. Używamy tu ogólniejszego kodu, który przy przechodzeniu w górę ustawia tę liczbę na jeden plus sumę liczb W sta w ia n ie d o d rz e w a BST w poddrzewach. Dalej w tym podrozdziale 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ą nierekurencyjnego 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.

413

414

RO ZD ZIA Ł 3

W yszukiwanie

b

Klucz

Wartość

Klucz

Wartość

s

o

A

8

&

(A J8

y CaaA Zmodyfikowana / E

wartość

1

A

2

R

3

C

4

H

5

Zmodyfikowana wartość E

6

M

9

P

10

sp,

(aj

l£ )

O ć)

©

AA

X

IR )

(Hp /A

AA

x

i E

7 (Aj

(c )

r\

iR ) ( h) aa

/A

aA

Ślad zm ian w drzewie BST dla standardow ego klienta używ ającego indeksu

3.2

o

A n a li z y Czas wykonania algorytmów działających 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 zbalansowane. Między korzeniem a każdym odnośnikiem pustym znajduje się ~lg N węzłów. W najgorszym przy­ 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 rekurencyjne sortowanie podtablic w sortowaniu szyb­ kim. To spostrzeżenie prowadzi do analiz cech drzew.

Drzewa wyszukiwań binarnych

Najlepszy przypadek

Typowy przypadek

M ożliw e d rz e w a BST

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; 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 poddrzew, 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.

415

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 dwukrotność 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

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

Porównania

Słowa

Różne

łącznie

słowa

M odel

Uzyskano

135 635 10 679

18,6

17,5

Ponad 8 liter

14 350

17,6

Ponad 10 liter

4582

15,4

Wszystkie słowa

5737 2260

sfowa

Różne

ł4cznie

słowa

Porównania Model

Uzyskano

21 191 455 534 580

23,4

22,1

13,9

4 239 597

299 593

22,7

21,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

417

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. Znajdowanie wartości f1oor(G)

dlateao f lo o r f G ') może

Wartość r l oo r (.gj w lewym poddrzewie to n u li

O b lic z a n ie w a rto ści funkcji floor()

Podłoga i sufit Jeśli dany klucz key ma war­ 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­ ł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 (). 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 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 ) są zamienione.

420

R O ZD ZIA Ł 3

n

W yszukiw anie

Załóżmy, że szukamy klucza z pozycji k (ta­ kiego, od którego mniejszych jest dokładnie k 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 w lewym poddrzewie. Jeżeli t jest równe k, wy­ starczy zwrócić klucz z korzenia. Dla t mniej­ szych niż k trzeba (rekurencyjnie) poszukać klucza z pozycji f c - f - l w prawym poddrzewie. Jak zwykle opis ten stanowi zarówno podstawę 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 klucz jest równy kluczowi korzenia, należy zwró­ cić liczbę kluczy z lewego poddrzewa — t. Jeżeli dany klucz jest mniejszy Przechodzenie w lewo niż w korzeniu, należy do momentu dojścia zwrócić pozycję klucza do pustego odnośnika w lewym poddrzewie (rekurencyjnie ustaloną). Dla klucza większego niż klucz korzenia nale­ ży zwrócić t plus 1 (aby Należy zwrócić prawy odnośnik uwzględnić klucz korze­ danego węzła nia) plus pozycja klucza w prawym poddrzewie r\ (rekurencyjnie obliczona).

s e t e c t ( 3 ) - w yszukiw anie klucza z pozycji 3.

Liczba węzłów (n)

( C)

H

R)' \

i

,M) AA

AA

Lewe poddrzewo obejmuje 8 kluczy, dlatego klucza z pozycji 3. należy szukać po lewej stronie

Lewe poddrzewo ' obejmuje 2 klucze, dlatego klucza z pozycji 3-2-1 = 0 należy szukać po prawej stronie

\ Lewe poddrzewo obejmuje 2 klucze, dlatego klucza z pozycji 0 należy szukać po lewej stronie

(H)

/A

H

m)

Dostępny dla mechanizmu przywracania pamięci Aktualizowanie odnośników i liczby węzłów po wywołaniach

Usuwanie minimum w drzewie BST

Usuwanie m inim um Lewe poddrzewo obejmuje 0 kluczy, i m aksim um Najtrud­ a szukamy klucza niejszą do zaimplemen­ z pozycji 0., dlatego należy zwrócić H towania operacją na Wybieranie w drzewach BST drzewie BST jest metoda 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 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. 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 nowane po raz pierwszy przez T. Hibbarda w 1962 ( m) 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 zostaje porządek w drzewie, ponieważ między Następnik /"A . . 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 / nych kluczy. Klucz x można zastąpić następnikiem momentu napotkania pustego lewego \ w czterech (!) prostych krokach. Oto one: odnośnika — yu, ■ Zapisanie w t odnośnika do usuwanego węzła. $ ■ Ustawienie x tak, aby wskazywał następnik deleteMi n ( t . ri ght) v _ / — m in (t.rig h t). ■ Ustawienie prawego odnośnika w x (ma on (C) (Mj 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 miał wartość nul 1 ) na t . 1 e f t (czyli wszystkie i liczby węzłów po rekurencyjnych wywołaniach klucze mniejsze niż usunięty klucz i jego na­ stępnik). U su w a n ie w d rz e w a c h BST Standardowe rekurencyjne rozwiązanie po rekurencyjnych 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 . Usuw anie E

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 eteMax() 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 z prawego poddrzewa (według definicji drzewa BST są większe p riv a te void print(N ode x) ( 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 ); cze po kolei. Aby zaimplementować dwuargumentową metodę S t d O u t. p rin t ln (x . k e y ); 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 wywołania dla poddrzew, które z pewnością nie zawierają klu­ drzewa BST 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. 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 keys() { return keys(min(), max()); } public Iterable keys(Key lo, Key hi)

{ Queue queue = new Queue(); keys(root, queue, lo, h i); return queue;

} private void keys(Node x, Queue queue, Key lo, Key hi)

{ if in t in t if if if

(x == n u li) return; cmplo = lo.compareTo(x.key); cmphi = hi.compareTo(x.key); (cmplo < 0) k e y s ( x .le f t , queue, lo, h i) ; (cmplo = 0) queue.enqueue(x.key); (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 zbalansowanych 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.

Algorytm (struktura danych)

Koszt dla najgorszego przypadku (po N wstawieniach)

Koszt dla typowego przypadku (po N losowych wstawieniach)

Wydajne operacje Zależne od kolejności?

Wyszukiwanie

Wstawianie

Trafienie

Wstawianie

Wyszukiwanie sekwencyjne (nieuporządkowana lista powiązana)

N

N

NI 2

N

Nie

Wyszukiwanie binarne (tablica uporządkowana)

lg N

N

lg N

NI 2

Tak

Binarne drzewa wyszukiwań (BST)

N

N

1,39 lg N

1,39 lg N

Tak

Podsumowanie kosztów im plem entacji podstawowej tablicy sym boli (uzupełnione)

3.2

*

Drzewa wyszukiwań binarnych

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.

427

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

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

429

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 ilin 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

"T")

keys("D\

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

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ę hasNoDuplicates ( ) . 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.

431

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

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.

433

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

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

435

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 zbalansowanego drzewa przy dynamicznym wstawianiu jest zbyt kosztowne. W tym podrozdziale omawiamy strukturę danych, w której nieco rozluźniono wymóg pełnego zbalansowania, 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ł potrójny

Węzeł podwójny

Pusty odnośnik S tru k tu ra d rz e w a w y szu k iw ań 2-3

436

W pełni zbalansowane drzewo wyszukiwań 2-3 ma wszyst­ 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ą strukturę). Dalej pokazujemy wydajne sposoby definiowania i implementowania podstawowych operacji na węzłach po-

3.3

Q Zbalansowane drzewa wyszukiwań

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ę między E /' J, dlatego należy RJ szukać pośrodku

( E 3

Ca

Bjest mniejsze niż E, dlatego należy szukać po lewej

i R)

( E 2,

0D (P) Cs x )

r\ r \

t Znaleziono H, dlatego należy zwrócić wartość (trafienie)

B ma wartość pomiędzy A / c, dlatego należy szukać pośrodku. 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.

Zastępowanie węzła podwójnego nowym węzłem potrójnym zawierającym K Wstawianie do węzła podwójnego

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 zbalansowanie. Ł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ę­ złem potrójnym, zawierającym dawny klucz i klucz wstawiany. Jeżeli wyszukiwanie kończy się w węźle potrójnym, potrzeba więcej pracy.

437

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, s ——z- tn Tworzenie węzła a jednocześnie w pełni zbalansowane drzewo wy­ ( a e sj 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­ tować to jak zastąpienie w rodzi­ Wstawianie Z cu odnośnika do dawnego węzła potrójnego odnośnikami po obu Wyszukiwanie z kończy się 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 Rodzic był węzłem podwójnym tymczasowym węzłem (o jednym kluczu i dwóch odnoś­ poczwórnym zawierającym Z 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 wpływa na cechy (w pełni zbalanZastępowanie węzła podwójnego 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 nieważ środkowy klucz trafia do ( a c ) ( h ) ( l ) ( p) v 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

puste są w takiej samej odległości od korzenia, jest to prawdą także po wstawieniu elementu. Upewnij się, że rozumiesz tę transformację. Jest ona istotą funkcjonowania drzew 2-3. W staw ianie do w ęzła potrójnego, którego rodzicem je st w ęzeł potrójny Teraz załóżmy, że wyszukiwanie kończy się w węźle, którego 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ł węzłem potrójnym, dlatego należy zastąpić go 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 węźle. Dzielimy więc nowy węzeł poczwórny i wstawiamy jego środkowy klucz do jego ro­ dzica. Rozwinięcie tego ogólnego przypadku jest oczywiste — należy poruszać się w górę drzewa, dzieląc węzły poczwórne i wstawiając

o

Zbalansow ane drzewa wyszukiwań

439

Wstawianie D

Wyszukiwanie d kończy się w tym węźle potrójnym f

Dodawanie nowego klucza D do węzta potrójnego, przez co powstaje tymczasowy węzeł poczwórny

Dodawanie klucza środkowego c do węzia potrójnego, przez co powstaje tymczasowy węzeł poczwórny

( m)

Podział węzła poczwórnego na dwa węzły podwójne. Środkowy klucz należy przenieść do rodzica Dodawanie środkowego klucza E do węzła podwójnego; powstaje nowy węzeł potrójny

Wstawianie D

Wyszukiwanie d kończy się w tym , węźle potrójnym \

Podział węzła poczwórnego na dwa węzły podwójne. Ś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

f L) ri Dodawanie środkowego klucza c do węzła potrójnego, przez co powstaje tymczasowy węzeł poczwórny

\ ( wa J ( wd ) (h) T l )

\

/

Podział węzła poczwórnego na dwa węzły podwójne. Środkowy klucz należy przenieść do rodzica Podział węzła poczwórnego na trzy węzły podwójne, co powoduje zwiększenie wysokości drzewa o 1

Podział korzenia

ich środkowe klucze do rodziców do m om entu natrafienia na węzeł podwój­ ny (zastępujemy go węzłem potrójnym, którego nie trzeba dalej dzielić) lub na węzeł potrójny będący korzeniem. P odział korzenia Jeśli węzły potrójne znajdują się na całej ścieżce od punk­ tu wstawiania do korzenia, ostatecznie powstaje węzeł poczwórny w korzeniu. Wtedy można postąpić tak samo, jak przy wstawianiu do węzła składającego 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

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ć pełne zbalansowanie drze­ wa, ponieważ jest wykony­ wana w korzeniu.

/ \ / \ / \ / \ / \ / \ / Mniejsze\ / Między\ / M iędzy\ / M iędzy\ /M iędzy', j Większe \ V niż a ) ( a i b ! ( b / c ) ( c /' d ) ( d i e ) \ niże T r - r - f /T-rrr-i / r- T —\

n ~ : ~ \

Jrrrm

Transformacje lokalne Po­ dział tymczasowego węzła poczwórnego na drzewo / \ / \ /• \ / \ / ^ . iększe\ 2-3 obejmuje jedną z sześ­ 1 Mniejsze \ / Między'. / Między \ / Między\ / Między / W niż a ) ( a / b ) ( b i c ) ( c i d ) ( d /'e ) V niz e I i-rrr-\ ¡[ -i jr r- r-\ rn -T -r-f n .. ■ -\ ciu transformacji podsu­ mowanych w dolnej części Podział węzła poczwórnego to lokalna transformacja zachowująca kolejność i pełne zbalansowanie następnej strony. Węzeł 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 zbalansowane. 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ń

Rodzic to w ęzeł p otrójny

Korzeń

^ “A

b d e

$

Rodzic to węzeł podwójny L ew a

Xa c e X

-

- c t h > P raw a

a b d

P r a w a z

/

~ 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 .

441

442

R O ZD ZIA Ł 3

Wstawianie

a

W yszukiwanie

Wstawianie A

S

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) n n

a c)(h O

nrK W \

( p) Cs x )

(A )

/ \ /

Standardowy klient używający indeksu

(£ }

(L )

^

p

( p) (

Cs )

s

x

'

/~ \ / A >-\ /O >—r~< Te same klucze wstawione w kolejności rosnącej

Ślady procesu tw orzenia drzew 2-3

3.3

o

Zbalansow ane drzewa wyszukiwań

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

443

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 pomysł, na którym oparto czerwono-czarne drze­ wa BST, polega na zapisaniu drzewa 2-3 na pod­ stawie standardowych drzew BST (składających 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, łączące dwa węzły podwójne reprezentujące wę­ zeł potrójny, i czarne, które scalają całe drzewo 2-3. Węzły potrójne przedstawiane są jako dwa węzły podwójne połączone jednym odnośnikiem 1

' r

j

l

c

j

j

węzeł potrójny

/ Mniejszy\ ( niż a ;

li

/ Między 1 a/b

\ / ) v

Większy\ n‘ż b /

\ li ... 1 // . . . \

,

x v

Między \ aib )

n iż b

)

\

Zapisywanie węzła potrójnego za pomocą dwoch węzłów podwójnych połączonych 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

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­ 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.

0

¿balan sow an e drzewa wyszukiwań

445

Poziome odnośniki czerwone

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 ; 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 ; od rodzica jest czerwony, oraz 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ą { // K lu c z Key key; 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 // L i c z b a w ę z ł ó w w p o d d r z e w i e i nt N ; 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 { th is.k e y = key; między węzłem a rodzicem. Przy t h i s .v a l = v a l; określaniu koloru węzła ważny jest thi s .N = N; t h i s . c o l o r = co lo r; prowadzący do niego odnośnik. } } Rotacje W omawianej implementacj i mogą wystąpić czerwone odnośniki po prawej stronie lub dwa czerwone odnośniki z rzędu w jednej operacji, jednak metody przed zakończeniem działania

p riv a te

bo o le an isR e d (N o d e x)

{ i f (x == n u l l ) r e t u r n f a l s e ; r e t u r n x . c o l o r = = RED;

} 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 oraz czerwony lub czarny

, Mniejszy \ \ z) // Między

1

\ \ // Większy

/S J V niżs )

Node r o t a t e l _ e f t ( N o d e h)

{ Node x = h . r i g h t ; h. r i g h t = x . ) e f t ; x .1e f t = h ; x .c o !o r = h .co lo r; h . c o l o r = RED; x .N = h .N ;

h.N = 1 + s i z e ( h . l e f t ) + size (h . r i g h t ) ; r e t u r n x; x

}

/ Większy\ / Mniejszy', / M ię d z y \ [ niż e ) f a is j





Rotacja w lewo (prawego odnośnika węzła h) ,h

Node r o t a t e R i g h t ( N o d e h)

{ Node x = h . l e f t ; h . l e f t = x. r i g h t ; x . r i g h t = h; x .c o lo r = h .co lo r; h . c o l o r = RED; x .N = h.N;

h.N =

1 + s i z e ( h . le f t )

+ sizeCh. r ig h t ) ; r e t u r n x;

Xx i: fi)

/ Mniejszy \ / niż E ) /

V

Rotacja w prawo (lewego odnośnika węzła h)

zawsze rozwiązują te problemy przez odpowiednie ro­ 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ę po lewej (zobacz rysunek po lewej stronie). Ta operacja to rotacja w lewo. Przetwarzanie umieszczono w m e­ todzie, która przyjmuje jako argument odnośnik do czerwono-czarnego drzewa BST i — przy założeniu, że odnośnik prowadzi do obiektu h typu Node, którego prawy odnośnik jest czerwony — wprowadza niezbęd­ ne zmiany, po czym zwraca odnośnik do węzła będące­ go korzeniem czerwono-czarnego drzewa BST dla tego samego zbioru kluczy, w którym lewy odnośnik jest czerwony. Jeśli sprawdzisz każdy wiersz kodu wzglę­ 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ś­ nik w prawy, to ten sam kod z zamienionymi stronami (zobacz rysunek po lewej, w dolnej części strony). Ponowne ustawianie odnośnika w rodzicu po rotacji Każda rotacja, niezależnie od strony, prowadzi do zwróce­ 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. Metody rotateL eft() i rotateR ight() zachowują kolor przez ustawienie zmiennej x . col or na h. col or. Może to spowodować powstanie w drzewie dwóch kolejnych czer­ wonych odnośników, jednak w algorytmach stosujemy rotację, aby rozwiązać ten problem. Przykładowo, kod: h = ro ta te L e ft(h ); rotuj e wlewo prawy czerwony odnośnik węzła h i ustawia 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­ 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ć rotację jako uzupełnienie normalnego wstawiania.

3.3

a

Zbalansow ane drzewa wyszukiwań

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

447

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. Dzieje się tak, ponieważ rotacje zachowują dwie defini­ cyjne cechy czerwono-czarnych drzew BST — kolejność i pełne zbalansowanie. Oznacza to, że m ożna zastosować rotacje czerwono-czarnych drzew BST bez obaw o zabu­ rzenie porządku lub pełnego zbalansowania. Dalej poka­ zujemy, jak wykorzystać rotacje do zachowania dwóch innych definicyjnych cech czerwono-czarnych drzew BST (brak kolejnych czerwonych odnośników na której­ kolwiek ze ścieżek i brak prawych czerwonych odnoś­ ników). Jako rozgrzewkę przedstawiamy kilka łatwych przypadków.

Wyszukiwanie kończy się w tym pustym odnośniku tr ((aj

Prawy

\

Korzeń

Czerwony odnośnik do nowego węzła zawierającego a powoduje przekształcenie węzła podwójnego w potrójny

^K orzeń Wyszukiwanie kończy się w tym pustym odnośniku Dołączony nowy węzeł , z czerwonym odnośnikiem [ b) Korzeń

W staw ianie do jednego w ęzła podwójnego Czerwonoczarne drzewo BST o jednym węźle to jeden węzeł po­ dwójny. Wystarczy wstawić drugi klucz, aby przekonać się o potrzebie rotacji. Jeśli nowy klucz jest mniejszy od klucza z drzewa, wystarczy utworzyć nowy (czerwony) węzeł z nowym kluczem i gotowe — powstaje czerwo­ 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ś­ nika. Wtedy kod root = ro ta te L e ft(r o o t); uzupełnia wstawianie przez przestawienie czerwonego odnośnika na lewo i zaktualizowanie odnośnika do korzenia drzewa. 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 .

lal

. Po rotacji w lewo w celu W utworzenia dozwolonego węzła potrójnego

Wstawianie do pojedynczego węzła podwójnego (dwa przypadki) Wstawianie C

Tu należy dodać' nowy węzeł Prawy odnośnik jest czerwony, dlatego należy wykonać rotację w lewo

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

Większy

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 czerMniejszy Pomiędzy . wono-czarnych. Wyszukiwarie kończy się Wyszukiwanie Zmiana koloruw tym pustym kończy się w tym Do zmiany ko­ Wyszukiwanie odnośniku pustym odnośniku kończy się w tym loru dwóch czer­ pustym odnośniku wonych dzieci Dołączony Dołączony nowy węzeł węzła służy nowy węzeł z czerwonym Dołączony p rz e d s ta w io n a z czerwonym odnośnikiem nowy węzeł odnośnikiem po lewej m eto­ z czerwonym odnośnikiem da flipColors(). Rotacja Rotacja Oprócz zamia­ w prawo lewo Kolor ny koloru dzieci zmieniony Rotacja z czerwonego na na czarny wprawo czarny mody­ Kolor fikujemy kolor (b ) ^ zmieniony Kolor ę y f f y y na czarny rodzica z czarnezmieniony na czarny go na czerwony. Niezwykle ważWstawianie do jednego węzła potrójnego (trzy przypadki)

3.3

Może być skierowany wprawo lub w lewo

'\ / \ Między \

/

AZE ^ y

Między

Ei S

\ /

J

Większe

'

s ^ jv ż S _

v o i d f l i p C o l o r s ( N o d e h)

{

h . c o l o r = RED; h . l e f t . c o l o r = BLACK; h. r i g h t . c o l o r = BLACK;

}

Czerwony odnośnik łączy środkowy węzeł z rodzicem

c

Zbatansowane drzewa wyszukiwań

449

ną cechą tej operacji jest to, że — podob­ nie jak rotacje — jest to transformacja lo­ kalna, która zachowuje pełne zbalansowanie drzewa według czarnych odnośników. Ponadto rozwiązanie to bezpośrednio prowadzi do opisanej dalej pełnej imple­ mentacji. Zachow anie czarnego koloru korzenia W omówionym przypadku (wstawianie do jednego potrójnego węzła) kolor korze­ nia zmieniany jest na czerwony. Może się to zdarzyć także w większych drzewach. Czerwony kolor korzenia wskazuje na to, Wstawianie H

Między

\ /

Między

A/E

) {

E/S

\ / ) (v

Większe 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

że korzeń jest częścią węzła potrójnego, jed­ nak jest to nieprawda, dlatego po każdym 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ś­ niej przypadki. Nowy węzeł jest dołączony 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

Dwa lewe odnośniki pod rząd, dlatego należy zrotować jeden w lewo

Prawy odnośnik jest czerwony, dlatego należy zrotować go w lewo

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

{

private Node root; private class Node // Węzę? drzewa BST z bitem określającym kolor // (zobacz stronę 445). private private private private

boolean isRed(Node h) Node rotateLeft(Node h) Node rotateRight(Node h) void flipColors(Node h)

private int size()

// // // //

Zobacz stronę Zobacz stronę Zobacz stronę Zobacz stronę

445. 446. 446. 448.

// 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

E

A

R

C

H

X

M

P

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ń

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

453

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 poddrzewo trzech węzłów podwójnych, w którym lewe i pra­ A we dziecko jest powiązane z rodzicem czerwonym odnoś­ nikiem. 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­ wa przez rotacje (tak jak przy wstawianiu). P A 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ść 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 Na dole ma dostęp do tego samego drzewa, algorytm ten ma pewne zalety względem drzewa 2-3, ponieważ zawsze działa w odległości odnośnika lub dwóch od bieżącego tu p węzła. A lgorytm y usuw ania opisane dalej są oparte na Transformacje przy wstawianiu danych znanych schemacie i działają zarówno dla takich drzew, w zstępujących drzewach 2-3-4 jak i dla drzew 2-3.

a

A

A

a a

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ół Przy przechodzeniu w dół drzewa, ma miejsce jedna z poniższych sytuacji: 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ł (a b O poczwórny, przekształcając rodzica z węzła /] T\ potrójnego w podwójny lub z poczwórnego Transformacje przy usuwaniu minimum w potrójny. 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­ 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. n ie k t ó r e ć w ic z e n ia

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

t a le . t x t

Słowa łącznie

Różne słowa

457

Zbalansow ane drzewa wyszukiwań

le ip z ig lM . t x t

Porównania Model

Słowa łącznie

Różne słowa

Uzyskano

Porównania Model

Uzyskano

Wszystkie słowa

135 635 10 679

13,6

13,5

21 191 455 534 580

19,4

19,1

Przynajmniej 8 liter

14 350

12,6

12,1

4 239 597 299 593

18,7

18,4

Przynajmniej 10 liter

4582

17,5

17,3

5737 2260

11,4

11,5

1 610 829

165 555

Ś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ń

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ń.

Algorytm (struktura danych)

Koszt dla najgorszego przypadku (po N wstaw.en.ach)

Koszt dla typow ego przypadku (po N losowych wstawieniach)

w ri '

hł ^ uporządkowanych

W yszukiwanie

Wstawianie

Trafienie

Wstawianie

danych?

Wyszukiwanie sekwencyjne (nieuporządkowane listy powiązane)

N

N

N/2

N

Nie

Wyszukiwanie binarne (uporządkowane tablice)

lg N

N

lg N

N/2

Tak

Drzewa wyszukiwań binarnych

N

N

1,39 IgN

1,39 lgN

Tak

2 lg N

2 lg N

1,00 lgN

1,00 lgN

Tak

Drzewa 2-3 (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 )

459

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 flipCo1ors (). 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.

a

3.3

Zbalansow ane drzewa wyszukiwań

ĆWICZENIA Narysuj drzewo 2-3 uzyskane przez wstawienie kluczy E A S Y Q U T I O N (w tej kolejności) do początkowo pustego drzewa.

3 .3 .1 .

Narysuj drzewo 2-3 otrzymane przez wstawienie kluczy Y L (w tej kolejności) do początkowo pustego drzewa.

3 .3.2 .

Określ kolejność wstawiania kluczy S E A wstania drzewa 2-3 o wysokości 1. 3 .3 .3 .

R

C

H

P

MX H C

R

AES

X M, która prowadzi do po­

Udowodnij, że wysokość drzewa 2-3 o N kluczach wynosi pomiędzy ~ l_log3 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). 3 .3 .4 .

Na rysunku po prawej stronie pokazano wszystkie strukturalnie 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 .5 .

3 .3 .6 .

a

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ę­ ciu innych przypadków przedstawionych na dole owej strony.

3 .3 .7 .

Przedstaw wszystkie możliwe sposoby na zapisanie węzła po­ 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 .8 .

3 .3 .9 .

Które z poniższych drzew to czerwono-czarne drzewa BST?

Narysuj czerwono-czarne drzewo BST uzyskane przez wstawienie elemen­ tów o kluczach E A S Y Q U T I 0 N (w tej kolejności) do początkowo pustego drzewa. 3 . 3 .1 0 .

Narysuj czerwono-czarne drzewo BST uzyskane przez wstawienie elemen­ tów o kluczach Y L P H X H C R A E S (w tej kolejności) do początkowo pustego drzewa. 3 . 3 .1 1 .

461

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 3 .2 . 1 0 .

ic z e n ia

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ść czerwonoczarnego drzewa BST, lub udowodnij, że taki ciąg nie istnieje.

3.3



Zbalansow ane drzewa wyszukiwań

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­ 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. ć w ic z e n ia

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 rekurencyjnym 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ć.

463

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 zbalansowanych 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ń

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 czerwonoczarnych 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 ) ;

465

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 czerwonoczarnych 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ń

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 ); }

467

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.

Porów nania

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.

Ś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ść klucze odpowiadają różnym indeksom. Zwykle ideał ten jest pqi 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 sowi tablicy. Dlatego drugą częścią wyszukiwania opartego uvw 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. separate 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

F u n k c je h a s z u ją c e Pierwszy problem związany jest z obliczaniem funkcji haszują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 dego klucza z osobna). Ta idealna sytuacja jest nieco tajemnicza. Aby Klucz (/W=100) zrozumieć haszowanie, warto zacząć od zastanowienia się nad tym, 212 12 jak zaimplementować taką funkcję. 618 18 302 2 Funkcja haszująca zależy od typu klucza. Ujmijmy to ściśle — dla 940 40 każdego używanego typu klucza potrzebna jest inna funkcja haszu­ 702 2 jąca. Jeśli klucz obejmuje liczbę, na przykład num er PESEL, m oż­ 704 4 na zacząć od tej wartości. Jeżeli klucz zawiera łańcuch znaków, taki 612 12 jak nazwisko osoby, trzeba przekształcić łańcuch znaków na liczbę. 606 6 Klucze składające się z wielu części, na przykład adresy pocztowe, 772 72 10 510 trzeba w jakiś sposób połączyć. Dla wielu często stosowanych ty­ 423 23 pów kluczy m ożna wykorzystać domyślne implementacje dostępne 650 50 w Javie. Pokrótce omawiamy możliwe implementacje dla różnych 317 17 typów kluczy. Pomoże Ci to zobaczyć, jak wygląda taka implementa­ 907 7 cja. Dla tworzonych przez siebie typów kluczy będziesz musiał sam 507 7 304 4 zapewnić implementacje. 714

14

471

Skrót (M = 97)

18 36

11 67 23 25 30 24 93 25 35

68 26 34

22 13 35 81 25 27 25

Typowy p rzykła d Załóżmy, że w aplikacji kluczami są amerykańskie 857 57 num ery ubezpieczenia społecznego. Taki numer, na przykład 123-451 801 900 0 6789, to dziewięciocyfrowa liczba podzielona na trzy pola. Pierwsze 413 13 określa obszar geograficzny, w którym przydzielono dany numer 22 701 1 (przykładowo, num ery ubezpieczenia społecznego, gdzie pierwsze 30 418 18 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ć haszowanie 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++) obliczenia odbywają się tak, jakby wartość typu hash = (R * hash + s .c h a r A t (i)) % M; S tring potraktowano jako N-cyfrową liczbę całkowitą o podstawie R. Metoda oblicza resztę Haszowanie klucza w postaci łańcucha znaków 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 haszowaniu 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ąć niejednoznaczności, w przykładach dotyczących haszowania pomijamy wszystkie obliczenia tego rodzaju, a w zamian używamy wartości skrótów podanych w tabeli po prawej stronie.

^ s E A R c H X M P L skrót(M=5) 2 0 0 4 4 4 2 4 3 3 Skrót(M=i6) 6 10 4 14 5 4 15 l 14 6 Wartośd skrótów k|uczy stoSowane w przykładach

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

f 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 in t hashCode()

{ in t hash = 17; hash = 31 * hash + who.hashCode() ; hash = 31 * hash + when.hashCode() ; hash = 31 * hash + ((Double) amount).hashCode() return hash;

1

Implementowanie metody hashCodeQ w typie zdefiniowanym przez użytkownika

go typu danych trzeba samodzielnie spróbować uzyskać ten efekt. Przykład dla typu Date przed­ stawiony na stronie 472 to jedno z możliwych rozwiązań — tworzenie liczb całkowitych ze zmiennych egzemplarza i stosowanie haszowa­ nia modularnego. W Javie konwencja, zgodnie z którą wszystkie typy danych dziedziczą metodę hashCode (), pozwala zastosować jeszcze prostsze podejście. Można użyć m etody hashCode() na zmiennych egzemplarza, aby przekształcić każdą z nich w 32-bitową wartość typu i nt, a następnie wykonać operacje arytmetyczne, co pokazano po 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­ 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

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. t 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. m im o

475

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ść dzo prawdopo­ s 2 0 dobne jest, że bę­ TTTstT E 0 1 dzie obejmowała 7 A 0 N /M par kluczTi rst. R 4 3 wartość. W typo­ nuli Niezależne obiekty typu 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 kować ten wnio­ E 0 6 3 sek Z Z A Ł O Ż E N IA J 7 X 2 4 Ti r s t ^ i spodziewać się L 11 P 10 A 0 8 szybkiego działa­ M 4 9 Ti r s t ^ nia wyszukiwania X P 3 10 H jr M 9 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

{ private in t N; // Liczba par klucz-wartość. private in t M; // Rozmiar ta blicy z haszowaniem. private SequentialSearchST[] 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[]) 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 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 haszowaniem 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żeniem tego wyrażenia jest roz­ kład Poissona:

a ke-n ]ę 1

(10;0,12572...)

1 o

1 io

1

20

-0,125

1 30

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

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

479

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­ 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.

h a s z o w a n ie m e t o d ą ł a ń c u c h o w ą

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ą pozycję. 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ść

S

6

0

E A

10 4

1

1

14

C

5 4

2

I

1

1

S

!

7

f i

1

i

z z l ___i 4

fiL/irnó

5

elementy sq sorowclzane 1

E

10 15

fi u

8

1

9

6

10

n

i __ r

7

4

14

3

1

zerwone elementy \ i { \ sq powę

_

R

H

0

10

11

12

i

1

r

p

6 l s

7

i

l

i

9

10 1 1 12 13 14 15

l

i

1

s

_

E : i

0

kA

s

2

0

A

s

2

0

z r 2

z r

i

1

1 I Z l- K ^

II

C ZT

. 1L_ 1E 1

5

0

1

c 5

0

5 i

s ZT|

I

1

j

1

1

E I 1 ’

c 5 c

s

H.

0

5

s

H

5

0

5

6

s

H1

E !

0

51

6

A

C 5 C

s

H1

S

5

0

5~i

r

6

A

C 5 c

s

H1

i

E

0 H JI s

H 1L f

5

0

5

2

A

1

A ®

8

!

9 1

1

8

[Z ł

A

1

8

~A

1

E :

E

11

j T I ~s~| ~H~| L 5 0 5 ;ii

| |

6

E~|

161 E

1 1

I

Szare i i elbmepty ri\e sąsprawdźane 1 R I ___ 1 3 1

1 Pk] ___I___ L 3 J __

___ 1

I~ r 1 13 1

1 1 1 i "‘ 1 r 1

1 r 1 13 ] 1R |X 1 3 |7 i ~R ~ n r 13 1 7 | r i x 1 3| 7

i 1

i R Lx 13 1 7

i 1

1 R1x

1

E ©

1—

___1 j

0

mT

p “ Ml 10 9 i

5 1

A i i_

H ~ r ~ i i M i 9 1 1 | [ P M1 10 9 1___L _ 10

4

Próbkowanie przechodzi do elementu 0

1 3 lT

1

|R |X

1

1 3 I 7

Ślad działania standardowego klienta używającego indeksu, korzystającego z implementacji tablicy symboli opartej na próbkowaniu liniowym

keys [] v a ls [ ]

482

R O ZD ZIA Ł 3

W yszukiw anie

ALGORYTM 3.6. Haszowanie z próbkowaniem liniowym public c la s s LinearProbingHashST

{ private private private private

in t N; in t M = 16; Key[] keys; Value[] va ls;

// Liczba par klucz-wartość wta b lic y , // Rozmiar ta b lic y zpróbkowaniem liniowym, // Klucze, // 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

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 haszują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­ p u b lic void delete(Key key) nak klucz znajduje się na końcu grupy, na pozycji 7. { 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 ); w h ile ( ! k e y .e q u a ls (k e y s [i])) wszystkie klucze z grupy znajdujące się na prawo od 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 ; w hile ( k e y s [ i] != n u ll) jako przykładu jego przebiegu (zobacz ć w i c z e n i e { 3-4-17)Key keyToRedo = keys [ i ]; t a k jak

Value valToRedo = val s [ i ]; k e y s [i] = n u l l ; val s [i ] = n u ll; N— ; put(keyToRedo, valToRedo); i = (i + 1) % M;

w m e t o d z i e ł a ń c u c h o w e j , tak i w adre-

so w a n iu otw artym w yd ajność haszow an ia zależy o d sto su n k u a = N/M , je d n a k tu jest o n interpretow any w in n y sposób, a określa m y jako współczynnik zapeł­

}

nienia (ang. load factor) dla tablicy z haszow aniem . W metodzie łańcuchowej a to średnia liczba kluczy 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

N— ; i f (N > 0 N == M/8) re size (M /2 );

,,

.

,

....

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.

483

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 trzyelementowa 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, Wraz z zapełnianiem się tablicy wymóg ten może że ro w y klucz znajdzie się w tej grupie, wynosi 9/64 być trudny do spełnienia, ponieważ długie grupy P rze d •• |»«■»«■»»»|»■»■««»»■»»»» •• 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­ wianego klucza (przy założeniu o równomiernym Prowadzi to do utworzenia haszowaniu), z większym prawdopodobieństwem Po y / dużo dłuższej grupy • •• | | •• ••• ■ • •• • 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

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

o

p ro s te j

Tablice z haszowaniem

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.

485

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 t ; t = new Li nearProbi ng HashSKKey, Va lu e>(ca p );

fo r (in t i = 0 ; if

i < M; i++)

, . z a lg o r y t m u

k tó r y u s ta w ia

(keys[i] != n u ll ) t. put (keys [ i ] , vals [i]),

,

, ,

3.6 n a le ż y d o d a ć w ie rsz , M

na

pew ną

w a rto ś ć

przed utw orzeniem tablic). Potrzebna . t te¿ m et0 d a r e s iz e ( ) , przedstaw io1 , . , , n a P ° lew ej, k tó r a tw o rz y n o w y o b ie k t Li nearProbi ngHashST o d a n y m ro z m ia -

keys = t .k e y s ;

vals = t .v a l s ; M = t.M; )

rz e , u m ie s z c z a w sz y stk ie k lu c z e i w a rto -

.......................... .. ...

,

.

Zmienianie wielkości tablicy z haszowaniem przy próbkowaniu liniowym

ści z tablicy w nowej tablicy, a następnie '

1

'

Tr

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/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 .hashCode() k 0 x 7 f f f f f f f ; i f (IgM < 26) t = t ° prim e sp gM +5]; r e t u r n t ‘i M; } 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 ­ 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 ć 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 ć 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 ­ 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 ­

31 61 127 251 509 1021 2039 4093 8191 16381 32749 65521 131071 262139 524287 1048573 2097143 4194301 8388593 16777213 33554393 67108859 134217689 268435399 536870909 1073741789 2147483647

Liczb y pierwsze określające rozm iary tablicy 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

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 .

491

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

[ 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 .

493

494

RO ZD ZIA Ł 3

a

ĆWICZENIA

W yszukiwanie

(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

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 .

495

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 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 128.112.136.35 www.cs.princeton.edu

% java LookupCSV amino.csv 0 3 TCC Seri ne

% java LookupCSV DJIA.csv 0 3 29-0 ct-29 230.07

% ja va LookupCSV UPC.csv 0 2 0002100001086 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

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ą ­ 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 ­ ś 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 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 z k a ż d y m k lu c z e m je d n ą w a rto ś ć . K a ż d y n u ­ 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 ­ 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 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 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 ­ 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 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 ­ 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 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 ­ 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­

a m in o i.t x t

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 Aspartic Acid, g a t ,GAC Cystei n e ,T G T ,TGC Glutamic Acid,GAA,GAG Glutami n e ,c a a ,CAG Gl yci n e ,g g t ,g g c ,g g a ,GGG Separator Histidine,CAT,CAC Isoleucine,ATT,a t c ,ATA J Leuci n e ,T T A ,T T G ,CTT,CTC,C T A ,LTG Lysi n e ,A A A ,AAG Methionine.ATG Phenyl al anine,T T T ,TTC 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 Stop,TAA,TAG,TGA Th reoni n e ,A C T ,A C C ,A C A ,ACG Tyrosine,TAT,TAC T ryptophan,TGG 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

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

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

H

/

/

Wartości

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 n iż w p lik a c h ,csv — p rz e c in e k ; znak sz u

m ożna

o k re ś lić

p o le c e ń ) .

in d e k s u

w

w ie r ­

Po

z b u d o w a n iu

p ro g ra m

Lookuplndex

p rz y jm u je

z a p y ta n ia

o

k lu c z

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 ­ k a w sz e ,

p ro g ram

tw o rz y

te ż

Dziedzina

Klucz

Wartość

Badania'nad genomem

A m in o k w a s

L ista k o d o n ó w

Handel

N u m e r k o n ta

L ista tra n sa k c ji

Wyszukiwanie w sieci W W W

K lucz

L ista

w y sz u k iw a n ia

s tro n W W W

Baza IMDB

F ilm

L ista w y k o n aw có w

Lookuplndex

in d e k s

Typowe zastosowania indeksów

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 Klucz

Wartość

Baza IMDB

A k to r

Z b ió r film ó w

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

s p e c ja ln y m k ie m

je s t

Jego

tu

p rz y p ad ­ sk o ro w id z.

Dziedzina

p rz y g o to w a n ie

ze z b io r e m

cji, n a k tó r y c h sło w o 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 ).

D K o m p ila to r. W d u ż y c h p ro g r a m a c h ,

w

Id e n ty fik a to r

Z b ió r m iejsc uży cia

pozy­ Przeszukiwanie plików Badania nad genomem

k tó ­

S zu k an e p o jęcie

Z b ió r p lik ó w

P o d se k w en c ja

Z b ió r lo k alizacji

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

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

s to s o w a n ia tej s tru k tu r y . Tablica k r a w ę d z i z e le m e n ta ­

Reprezentacje tej samej krawędzi

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 ­ ku.

Im p le m e n ta c ja

9 — 12

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 . Tablica list są s ie d ztw a , in d e k ­

Reprezentacja oparta na listach sąsiedztwa (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

zo w an ia śla d u d z ia ła n ia p ro g ra m u . W celu u ła tw ie n ia ty c h z a d a ń zak ład am y , że k lasa Graph m a k lie n ta testo w eg o , k tó r y w czy tu je g ra f ze s tru m ie n ia w ejścio w eg o p o d a n e g o ja k o a rg u m e n t w iersza p o le c e ń , a n a s tę p ­ n ie w y św ied a g ra f (uży w ając im p le m e n ta c ji m e to d y t o S t r i n g ( ) ze stro n y 535), ab y p o ­ kazać k o le jn o ść w y stę p o w a n ia w ie rz c h o ł­ k ó w n a listach sąsied ztw a. W tej k o le jn o ści 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 ć w ic z e n ie

4 . 1 .7 ).

5 3

1 12 4 4 2

11 12 9 10

0 6 7 8 9 11 5 3

% j ava Graph t in y G . t x t 13 v e r t ic e s , 13 edges 0: 6 2 1 5

1: 0 2: 0

4 3: 5 4: 5 6 3 5: 3 4 0 0 4 7: 6:

X Pierwszy sąsiedni wierzchołek z danych wejściowych jest ostatnim na liście

9 : 11 10 12 Drugie wystąpienie 10: 9 każdej krawędzi 1 1 : 9 12 wyróżniono kolorem 12: 11 9 czerwonym 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[] 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[]) 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 (); // (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 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

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 krawędzi v-w

Sprawdzanie, czy w sąsiaduje z v

Iterowanie po wierzchołkach sąsiadujących z v

E

1

E

E

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)

Struktura danych Lista kraw ędzi M acierz sąsiedztwa

Pamięć

W ydajność (tempo wzrostu) dla typow ych implementacji typu Graph

539

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)

boolean marked(int v) i n t countf)

Znajduje wierzchołki połączone ze źródłowym wierzchołkiem s Czy v jest połączony z s? 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

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 ( )) Std O u t.p rint("N IE"); StdOu t.pri n t l n ( "spój ny" ) ;

} )

t in y G .t x t

13 0 5 4 3

0 1 Przykładowy klient do przetwarzania grafów (rozgrzewka)

% java TestSearch t in y G .t x t 0 0 1 2 3 4 5 6 NIEspójny % java TestSearch t in y G .t x t 9 9 10 11 12 NIEspójny

9 12 64 5 4

0 2 11 12 9 10

0 6 7 8 9 11 5 3

541

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

Labirynt

z w ią z a n y c h je s t ze śc ie ż k a m i,

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 Alejka

Graf

a lg o r y tm y p r z e tw a r z a n ia

g ra fó w



o p a r te n a ty m s a m y m p o d s ta w o w y m 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ź Odpowiadające sobie modele labiryntu

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 ­

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 ­ szeg o p rz e jś c ia p r z e z n ie .

Eksplorowanie 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 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 u b lic c l a s s D epthF irstS earch

{

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 booleanj] marked; 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 ()]; dfs(G , s );

a O d w ie d z ić ( r e k u re n c y jn ie ) w sz y stk ie 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) f markedjv] = true; count++; for (in t w : G.adj(v)) i f ( ! marked[w]) dfs(G , w );

d e p th -first search — D F S ). W im p le m e n ta c ji 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 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 ­ 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 ­

}

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

p ub lic boolean marked(int w) { return marked[w]; }

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 ­ 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 Standardowy rysunek

t in y C G .t x t

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 2 4

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 ł­ 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

2 3

^ ^

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

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

3 5

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

Listy sąsiedztwa

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 ­ 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 dfsC2) wywołuje samą siebie, aby od­ 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 2 1 5 ^ 0 T [ S p r a w d z a n ie 0 1 02 1 T ° Teraz 0 zajmuje pierwszą pozy­ 2 T 2 0 1 B 4 I S p r a w d z a n ie 2 3 3 5 4 2 1 Gotow y cję na liście sąsiedztwa 2 , ale jest 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 3 0 — wierzchołek 1 , m etoda dfs () rekurencyjnie wywołuje samą d f s (5 ) 2 1 5 siebie i odwiedza 1 . j s p r a w d z a n ie 02 0 1. 3 1 S p r a w d z a n ie 0 Odwiedziny 1 wyglądają ina­ 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­ d f s (4 ) ne, wywołania rekurencyjne nie 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 5 4 2 4 G otow y zwraca sterowanie z rekuren3 2 S p r a w d z a n ie 2 3 0 3 G otow y cyjnego wywołania df s (1). Nas­ 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

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) boolean hasPathTo(int v) Iterable pathTo(int v)

Znajduje w G ścieżki ze źródła s Czy istnieje ścieżka z s do 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 każdego wierzchołka powiązanego z s. Po utworzeniu obiektu Paths na podstawie źród­ łowego wierzchołka s klient może wykorzystać metodę egzemplarza pathTo() do iterowania po wierzchołkach na ścieżce z s do dowolnego wierzchołka powiązanego z s. Na razie akcep­ tujemy dowolną ścieżkę. Dalej opracujemy im­ plementacje znajdujące ścieżki o określonych cechach. Klient testowy widoczny po prawej 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­ go powiązanego wierzchołka.

p u b lic s t a t ic void m a in (S trin g [] args)

{ Graph G = new Graph(new In ( a r g s [ 0 ] ) ) ; in t s = In t e g e r . p a r s e ln t ( a r g s [ l] ); Paths search = new Paths(G, s ); fo r ( in t v = 0; v < G.V(); v++)

{ S td O u t.p rin tfs + " do " + v + ": ") i f (search.hasPath T o(v)) f o r ( i n t x : sea rch.pathTo(v)) 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 ) ; StdO u t.println ();

Klient testowy dla implementacji klasy Paths

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 o korzeniu w źródłowym wierzchołku. Na prawo od 0 do 1: 0-2-1 kodu a l g o r y t m u 4.1 narysowano mały przykład. Aby 0 do 2: 0-2 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]

547

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 edgeTo th is.s dfs(G,

= new boolean[G .V ()]; = new i nt [G. V () ]; = s; 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);

} } public boolean hasPathTo(int v) { return marked[ v ] ; }

5 3 2 0

5 3 5 2 3 5 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 Iterab le< In te ge r> pathTo(int v)

{ i f (IhasPathTo(v)) return n u ll; Stack 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

(podobnie jak w algorytmach Union-Find w p o d r o z d z i a l e 1 .5 ), co powoduje umieszczenie 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. 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­ chołek 0). Zawartość tablic marked[] i adj [] jest taka sama jak w śladzie działania metody DepthFi rstSearch ze strony 545. Takie same są też: szczegółowy opis wywołań rekurencyjnych i sprawdzone krawędzie, dlatego pominięto 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 w wierzchołku źródłowym i zapewniają infor­ macje potrzebne w metodzie pathTo () do udo­ stępnienia klientowi ścieżki z wierzchołka 0 do 1,2,3, 4 lub 5 w opisany wcześniej sposób. k o n s t r u k t o r w klasie DepthFirstPaths różni się tylko kilkoma przypisaniami od konstruk­ tora z klasy DepthFi rstSearch, dlatego także tu prawdziwe jest t w i e r d z e n i e a ze strony 543. Można do tego dodać następujące twierdzenie.

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. 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 pathTo() tworzy ścieżkę w czasie proporcjonal­ nym do jej długości.

e

549

Grafy nieskierowane

e d g e T o []

d fs ( O )

dfs(2) sprawdzanie 0

dfs(l) | Sprawdzani ! Sprawdzanie 2 1 Gotowy

dfs(3)

dfs(5) I Sprawdzanie 3 | Sprawdzanie 0 5 Gotowy

dfs(4) I Sprawdzanie 3 I Sprawdzanie 2 4 Gotowy sprawdzanie 2 3 Gotowy Sprawdzanie 4 2 Gotowy Sprawdzanie 1 Sprawdzanie 5 0 Gotowy

Ślad przeszukiw ania w g łą b w celu znalezienia w szystkich ścieżek w ychodzących z 0

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. breadthfirst 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

e d g e T o [] Metoda b f s () w a l g o r y t m i e 4.2 nie jest reO 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 m arked [ ] e d g e T o [] a d j [] queue z nim wierzchołka. Ścieżki dla klien­ T 0 2) 0 0 1 tów można tworzyć za pomocą tej 1 1 1 T 1 2 2 2 samej implementacji m etody path3 3 3 1 4 To(), którą wykorzystano dla m eto­ 4 4 i 5 5 1 4) 5 dy DFS W A LG O R Y T M IE 4 . 1 . Na rysunku po prawej stronie 0 2) 0 T o i przedstawiono krok po kroku prze­ T 1 0 1 | I 1 2 T 2 0 szukiwanie przykładowego grafu 2 i 3 3 3 metodą BFS. Pokazano zawartość 4 i i 4 4 5 0 5 i D 5 T struktur danych na początku każ­ dej iteracji pętli. Wierzchołek 0 jest 0 0 2) 0 T umieszczany w kolejce, a następnie 1 0 1 0 2 1 1 T 2 T 2 I0 1 0 2 1 w pętli program kończy wyszukiwa­ 3 2 3 5 4 L 3 T 4 2 II 4 T nie w następujący sposób: 4 i 5 0 4) 5 T 5 i n Usuwa 0 z kolejki i umieszcza w kolejce sąsiednie wierzchoł­ (C I ----------- ^ { 2 5 0 2 1 5 ) 0 1T 0 ki 2,1 i 5; oznacza każdy z nich 3 1 T 1 0 1 0 2 4 2 T 2 0 2 0 1 3 C l) Jy I i dla każdego ustawia wpis 3 5 4 2 3 jT 3 2 4 3 2 _____ A ' 4 T 4 2 w tablicy e dg eT o[] na 0. Q ) 5 IT 5 0 5 3 0 n Usuwa 2 z kolejki, sprawdza sąsiednie wierzchołki 0 i 1 (są 0 0 2 1 5 ) 0 T ' 1 T 1 0 1 0 2 oznaczone) i umieszcza w ko­ 2 T 2 0 2 0 13 3 2 lejce sąsiednie wierzchołki 3 i 4; 3 T 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. 0 0 2 1 5 [2 ) 0 T T i 1 0 1 0 2 ° Usuwa 1 z kolejki i sprawdza 2 T 2 0 2 0 1 3 3 T 3 2 3 5 4 2 sąsiednie wierzchołki 0 i 2 4 T 4 2 4 3 2 2 0 (są oznaczone). 5 5 3 0 ) 5 T 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; // // p riv a te in t [ ] edgeTo; // // p riv a te final in t s; //

Czy znana j e s t n a jk ró tsz a śc ie ż k a do tego w ierzch o łka ? O statni w ierzchołek na znanej śc ie ż c e do w ierzchołka, 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 edgeTo t h is . s bfs(G ,

= new bo o lean[G.V ( ) ] ; = new i nt [G. V ()] ; = s; s);

} p riv a te void bfs(G raph G, in t s)

{ Queue queue = new Q u e u e (); 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(); fo r ( in t w : G .a d j(v )) i f (¡m arked[w])

// Usuwanie następnego wierzchołka z k o le jk i, // Dla każdego nieoznaczonego są sie d n ie go // w ierzchołka:

{ edgeTo[w] = v; marked[w] = tru e ; queue.enqueue(w);

// zapisujem y o s ta t n ią krawędź na // n a jk ró tsz e j śc ie ż c e , // oznaczamy w ierzchołek, ponieważ ścieżka // j e s t znana, // 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 dao do o: o 0 do 1: 0-1 nych (w metodzie BFS jest to najdawniej dodany, a 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%

S z u k a n ie ś c ie ż e k m e to d ą DFS (250 w ie rz ch o łk ó w )

100%

S z u k a n ie n a jk ró ts z y c h ście ż e k 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) boolean connected(int v, in t w)

Konstruktor ze wstępnym przetwarzaniem 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 p u b lic s t a t ic void m a in (S trin g [] args) składowej. Pierwsze wywołanie rekuren{ 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 rekurencyjBag[] components; ną metodę df s () w celu oznaczenia wszyst­ 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> (); fo r (in t v = 0 ; v < G .V (); v++) chołkami tablicę i d [ ] , która łączy tę samą 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 connecf o r (in t v: com ponents[i]) Std O u t.p rin t(v + " " ) ; ted ( ) , która działa w tald sam sposób, jak 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

{ p rivate boolean[] marked; p rivate i n t [ ] id; p rivate in t count;

% more tin y G .tx t 13 v e rt ic e s , 13 edges 0: 6 2 1 5 1: 0 2: 0

public CC(Graph G)

{ marked = new b oolean[G.V ()]; id = new in t [ G .V( ) ] ; fo r (in t s = 0; s < G.V(); s++) i f ( ¡marked[s])

{ dfs(G, s ); count++;

} } p rivate void dfs(Graph G, in t v)

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

3: 5 4 4: 5 6 3 5: 3 4 0 6: 0 4 7: 8 8: 7 9: 11 10 12 10: 9 11: 9 12 12: 11 9 % java CC t in y G .tx t lic z b a składowych: 3 6 5 4 3 2 1 0 8 7 12 11 10 9

} 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

t in y G . t x t

id []

m a r k e d []

B 9101112

dfsCO) d f s ( 6) S p r a w d z a n ie 0 d fs(4 ) d fs(5 ) d fs(3 ) 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) [ S p r a w d z a n ie 0 2 G otow y d fs (l) | 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 ) d f s ( 8) | S p r a w d z a n ie 7 8 G otow y 7 G otow y d fs(9 ) cif s ( 1 1 ) S p r a w d z a n ie 9 d f s ( 12 ) 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 ) | 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

T T T T T

T T T T T T T T T

T

T T T T T

0

0

0 0 0

0 0 0 0

0

0 0 0 0 0

T T T T T T T

00 00 000

T T T T T T T T T T T T T T T T T

0 0 0 0 0 0 0 0 0 0 0 0 0 0

1

T T T T T T T T T T T T T T T T T T T T

0 0 0 0 0 0 0 0 0 0 0 0

1 2 12

2

TT

0 0 0 0 0 0

12

2 2

T T T T T T T T T T T T T

0 0 0 0 0 0

1 2 2 2 2

T T T T T T T T T T

Ślad przeszukiwania w głąb w celu znalezienia spójnych składowych

557

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ę. podstawowych problemów. Jest to proste po­ 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. m etoda

d fs słu ży d o

r o z w ią z y w a n ia

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

Zadanie

b

Grafy nieskierowane

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++) i f (!m arked[s]) dfs(G , s, s ) ;

Czy graf G jest acykliczny? Zakładamy, że nie istnieję pętle własne ani krawędzie równoległe.

} p riv a te void dfs(G raph G, in t v, in t u)

{ 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ć mu dwa kolory)?

} p riv a te void dfs(G raph G, in t v)

{ 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

559

RO ZD ZIA Ł 4

560

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 prostą 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­ Vi Enie są bezpośrednio podane JFK MCO wisu IMDB, przedstawiony w p o d r o z d z i a l e 3 . 5 . ORD DEN Przypomnijmy, że plik składa się z wierszy obej­ ORD HOU DFW PHX mujących tytuł filmu i listę wykonawców. W kon­ JFK ATL tekście przetwarzania grafów można traktować plik ORD DFW ORD PHX jak graf z filmami i aktorami jako wierzchołkami, ATL HOU przy czym każdy wiersz to lista sąsiedztwa z krawę­ DEN PHX PHX LAX dziami łączącymi film z wykonawcami. Zauważmy, JFK ORD że jest to graf dwudzielny. Nie istnieją krawędzie łą­ DEN LAS czące aktorów z aktorami lub filmy z filmami. ATL MCO HOU MCO LAS PHX

Przykładowy graf symboli (lista krawędzi)

Interfejs A P I Pokazany poniżej interfejs API okre­ śla ldienta klasy Graph, umożliwiającego natychmia­ stowe zastosowanie metod przetwarzania grafów 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) in t in d e x (S t r in g key) S t r in g name ( in t v)

Graph G()

Czy key to wierzchołek? Zwraca indeks powiązany z key Zwraca klucz powiązany z indeksem v Używany obiekt Graph

Interfejs API dla grafów z sym bolicznym i nazwam i wierzchołków

4.1

( — ^

P a t r ic k A lle n

\ T

JA

/

1

Kate

< A

~V _ L A

Tk .

/ se rr^ tta \ Wi 1 son

lbert J C Shane / \N 7 n

Ete rnal su n sh in e o f the S p o t le s s Mind

I/

Grafy nieskierowane

D ia l M f o r M urder

M Enigma

b

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)

561

562

RO ZD ZIA Ł 4

a

Grafy

p u b lic s t a t ic void m a in (S trin g [] args)

{ S t rin g filename = a rg s [0 ]; S t rin g delim = a r g s [1]; Symbol Graph sg = new Symbol Graph(filename Graph G = s g .G(); w hile (S td ln .h a sN e x tL in e O )

{ S t rin g source = S t d In . r e a d L in e (); 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));

del im );

Podany interfejs API obejmuje kon­ struktor do wczytywania i tworzenia grafu oraz m etody klienckie name() i index() do przekształcania nazw wierzchołków między łańcuchami znaków ze strum ienia wejściowego a indeksami całkowitoliczbowymi używanymi w m etodach przetwarza­ nia grafó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­ nej jako pierwszy argument wier­ Klient testowy dla interfejsu API grafów symboli 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 Użytkownik określa nazwę wierzchołka i otrzymuje li­ ORD stę sąsiadujących z nim wierzchołków. Klient udostęp­ ATL MCO nia przydatny mechanizm indeksu odwrotnego, opisa­ LAX ny w p o d r o z d z i a l e 3 . 5 . W kontekście pliku routes.txt LAS można wpisać kod lotniska, aby znaleźć bezpośrednie PHX połączenia z nim. Informacje te nie są bezpośrednio dostępne w pliku z danymi. W przypadku pliku movies. txt m ożna podać nazwisko aktora, aby otrzymać listę % java Symbol Graph m ovies.txt 7 " 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. Jednak zwracanie listy filmów, w których wystąpił po­ Bacon, Kevin dany wykonawca, wymaga indeksu odwrotnego. Choć M y stic R iv e r (2003) Frid ay the 13th (1980) baza danych łączy filmy z wykonawcami, w modelu F la t lin e r s (1990) grafu dwudzielnego aktorzy są też powiązani z filma­ Few Good Men, A (1992) 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

skuteczne w każdej omawianej metodzie prze­ 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. o p is a n e p o d e jś c ie je s t , o c z y w iś c ie

,

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 st

ke y s

S tr u k t u r y d a n y c h w g ra fie s y m b o li

563

564

RO ZDZIAŁ 4

Grafy

Typ danych dla grafu symboli public c la s s Symbol Graph

{ private ST private S t r i n g [ ] keys; private Graph G;

st; // Łańcuch znaków -> indeks, // Indeks -> łańcuch znaków, // Graf.

public Symbol Graph(String stream, S trin g sp)

{ st = new ST (); 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); while (in.hasN extLine())

// Drugi przebieg, // 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 public public 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 ) ; } in t in d e x(S trin g s) ( return s t . g e t ( s ) ; } S t r in g name(int v) { return keys[v]; } 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

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, % java D egreesO fSeparation m ovies.txt "/ " "Bacon, występującym w The Eagle Has Kidman, N icole Landed z Donaldem Sutherlandem, Bacon, Kevin który wystąpił w Animal House Few Good Men, A (1992) C ru ise , Tom z Kevinem Baconem. Jednak ta 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 M ystic R iv e r (2003) wystąpił razem z Baconem w filmie 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 North by Northwest (1959) nie filmów na najkrótszej ścieżce, 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ń.

565

Kevin"

566

RO ZD ZIA Ł 4



Grafy

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. m o żesz w ykorzystać

% 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 (grafy dwudzielne)

TwoColor

Strona 559

Problemy z obszaru przetwarzania grafów (nieskierowanych) poruszone w podrozdziale

4.1

a

Grafy nieskierowane

[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.

569

570

Grafy

R O ZD ZIA Ł 4

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? 4.1.2. Narysuj w stylu podobnym do rysunków z tekstu (strona 536) listy sąsiedztwa zbudowane na podstawie pliku tinyGex2.txt (po lewej) przez konstruktor klasy Graph uży­ wający strum ienia wejściowego.

ti ny G e x 2 .txt

12

16 8 2 1 0

4 3

11 6 36 10 3 7 11 78 11 8 2 0 6 2 52 5 10 3 10 8 1

4.1.3. Utwórz konstruktor kopiujący dla klasy Graph. Konstruktor powinien przyjmować graf Gjako dane wejścio­ we oraz tworzyć i inicjować nową kopię grafu. Zmiany wpro­ wadzone przez klienta w G nie powinny wpływać na nowo utworzony graf.

©

4.1.4. Dodaj do klasy Graph metodę hasEdge(), która przyj­ muje dwa argumenty typu i nt (v i w) oraz zwraca true, jeśli 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

4.1.12. W jaki sposób drzewo zbudo­ wane m etodą BFS pozwala określić od­ ległość między v a w, jeśli żaden z tych wierzchołków nie jest korzeniem?



Grafy nieskierowane

571

Te same listy, co dla danych wejściowych w postaci listy 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 ().

Zaimplementuj ją tak, aby zwracała licz­ bę krawędzi w najkrótszej ścieżce między źródłem a danym wierzchołkiem. Metoda powinna działać w stałym czasie. 4.1.14. Załóżmy, że przy przeszukiwa­ niu wszerz zastosowaliśmy stos zamiast kolejki. Czy także wtedy m etoda wyzna­ czy najkrótsze ścieżki?

ti n y G a d j .txt

13 ^ 0 12 5 6 3 4 5 4 5 6 7 8 9 10 11 12

% java Graph tinyGadj.txt 13 vertices, 13 edges

Kolejność list jest odwrócona względem danych wejściowych

11 12

9: 12 11 10 10: 9 11: 12 9 12: 11 9

Drugie wystąpienie każdej krawędzi wyróżniono kolorem czerwonym

4.1.15. Zmodyfikuj w klasie Graph kon­ 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 tinyGex2.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

4.1.26. Napisz

klienta klasy Symbol Graph (podobnego do program u który stosuje przeszukiwanie wgłęb zamiast przeszukiwania wszerz do wyszukiwania ścieżek łączących dwóch aktorów. Program m a generować dane wyjściowe podobne do pokazanych poniżej. D e g r e e s O f S e p a r a t i on),

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 nieizomorficzne 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

573

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

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.

575

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

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.

577

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ć niektopotliwe. Wymaga jednak dodatkowych struktur kombinatorycznych, co ma poważny wpływ na algorytmy i spraZastosowanie Wierzchołek Krawędź wia, że korzystanie z grafów Drapieżnik-ofiara skierowanych różni się od Gatunek Łańcuch pokarmowy stosowania grafów nieskieMateriały w Internecie Odnośnik Strona rowanych. W tym podroz­ Referencja dziale omawiamy klasyczne Program Moduł zewnętrzna algorytmy do eksplorowania Telefon komórkowy Połączenie Telefon i przetwarzania grafów skie­ Środowisko naukowe Cytowanie Praca naukowa rowanych. Finanse

Papiery wartościowe

Transakcja

Internet

Urządzenie

Połączenie

Słow nictw o Definicje doty­ czące grafów skierowanych są prawie takie same, jak dla Typowe zastosowania grafów skierowanych 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­ damy, że ścieżki skierowane są proste — chyba że rozluźnimy założenie przez wskazanie powtarza­ jących się wierzchołków (tak jak w definicji cy­ klu skierowanego) lub w celu uogólnienia ścież­ ki skierowanej. Mówimy, że wierzchołek w jest osiągalny z wierzchołka v, jeśli istnieje ścieżka skierowana z v do w. Ponadto przyjmujemy, iż 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 jest osiągalny z w. To rozróżnienie jest oczywiste, a przy tym — jak się okaże — bardzo ważne.

Krawędź skierowana Cykl skierowany ■ o długości 3

Wierzchołek Ścieżka skierowana

Wierzchołek 0 stopniu wejściowym 3 1stopniu wyjściowym 2

J

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 reprezentacja grafów nieskierowanych! Czy w ty m d ig ra fie m o ż n a d o trz e ć z v d o w? z r o z u m ie n ie a l g o r y t m ó w

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 Zwraca liczbę krawędzi

in t E() void addEdge(int v, in t w) Ite ra b le < In te g e r> a d j(i nt v) Digraph re ve rse () S t r in g t o S t r in g O

Dodaje do digrafu krawędź v->w Wierzchołki powiązane z v krawędziami wychodzącymi z v Odwraca digraf 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 nieskierowanych, 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 nieskierowanych (strony 536 i 538). W opartej na listach sąsiedztwa reprezentacji grafu nieskierowanego 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

Typ danych dla grafów skierowanych (digrafów) public c la s s Digraph

{

t in y D G . t x t

private final in t V; private in t E; private Bag[] adj; public Di graph (in t V)

{ t h i s . V = V; t h i s . E = 0; adj = (Bag[]) new Bag[V]; fo r (in t v = 0; v < V; v++) adj [v] = new Ba g ();

} 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 []

} public Iterable a d j( in t v) { return adj [ v ] ; }

V

0

T

5

T

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 ) ; return R;

} Typ danych Digraph jest prawie identyczny z klasą Graph (strona 538). Różnice polegają na tym, że tu metoda addEdge() wywołuje metodę add () tylko raz i dostępna jest metoda egzemplarza re v e rs e d , któ­ ra zwraca kopię grafu z odwróconymi krawędziami. Ponieważ część kodu można łatwo napisać na pod­ 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).

^ 0 -0 V 7 9 N. 11

10

A 12 A 4

12

S .

9

Format danych wejściowych digrafu i reprezentacja w postaci list sąsiedztwa

581

582

RO ZD ZIA Ł 4



Grafy

Osiągalność w digrafach Pierwszym algorytmem przetwarzania grafów nieskierowanych 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) DirectedDFS(Digraph G, Iterab le sources) boolean marked(int v)

Znajduje w G wierzchołki osiągalne z s Znajduje w G wierzchołki osiągalne z sources 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 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) marked[v] = true; f o r (in t w : G.a d j(v ) ) i f (¡marked[w]) dfs(G, w);

public boolean marked(int v) { return markedfv]; }

% java D1rectedDFS tin yD G .tx t 1

% java DirectedDFS tin yD G .tx t 2 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 s t a t ic void m ain(String[] args)

{ Digraph G = new Digraph(new I n (a r g s [0 ])); Bag sources = new B a g (); 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[] 0 T

dfs(O)

1

0

0

51

3 4 5

T

2 3 4 5

0 3 52 32 4

0

T

5 1

2 3 4 5

T T

0 1 2 3 4 5

0

T

2

1

d f s (3 ) S p r a w d z a n ie 5

d f s ( 2) I S p r a w d z a n ie 1 S p r a w d z a n ie 2 G otow y 3 G o to w y S p r a w d z a n ie 2 4 G otow y 5 G otow y

d fs(1) 1 G otow y 0 G otow y

0 3 52 32 4

T

1

d f s (4 )

1

2 3 4 5

2 3 4 5

dfs(5)

ad j [] 0 51

1

0 3 5 2 3 2 4

0 1

51

T T T

2 3 4 5

0 3 52 32 4

T

0

5 1

2 3 4 5

T T T T

2 3 4 5

0 3 5 2 3 2 4

0 1 2 3 4 5

T T T T T T

0

5 1

2 3 4 5

0 3 5 2 3 4

1

2 3 4 5

0

1

Ślad przebiegu przeszukiwania w głąb w celu znalezienia wierzchołków osiągalnych z wierzchołka 0 w digrafie

1

1

4.2

b

Grafy skierowane

Ś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 i zam iataj (ang. marle and sweep)

Bezpośrednio 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.

585

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, w którym bezpośrednio powstaje model digrafu.

Czy ten digraf obejmuje cykl skierowany?

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 / 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 /

rz

( S z tu c z n a in te lig e n c ja ) ( P ro g ra m o w a n ie z a a w a n s o w a n e

- ( R o b o ty k a )

l)

( B io lo g ia o b lic z e n io w a ~ )

( 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

( 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ń?

S ta n d a rd o w y m o d e l d ig ra fu

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). W digrafach szeregowanie z ograniczeniami pierwszeństwa sprowadza się do następującego podstawowego problemu.

Sortowanie topologiczne. Ustaw wierz­ chołki digrafu w takiej kolejności, aby wszystkie krawędzie skierowane prowadzi­ ły z wierzchołków z wcześniejszych pozy­ cji do wierzchołków z dalszych miejsc (lub ustal, że jest to niemożliwe).

Wszystkie krawędzie prowadzą w dól

Wszystkie wymagania wstępne są spełnione

i

I

Analiza m atem atyczna Algebra liniowa W prow adzenie d o nauk k om puterow ych

Po prawej stronie pokazano porządek topolo­ giczny dla przykładowego modelu. Wszystkie krawędzie prowadzą w dół, dlatego porządek stanowi rozwiązanie problemu szeregowania z ograniczeniami pierwszeństwa, którego m o­ delem jest dany digraf. Student może spełnić wszystkie wymagania wstępne, uczestnicząc w kursach w określonej kolejności. Jest to typo­ we zastosowanie. W tabeli poniżej przedstawio­ no kilka innych reprezentatywnych zastosowań. Zastosowanie

Wierzchołek

Szeregowanie zadań

Zadanie

Program ow anie zaaw ansow ane Algorytm y T eoretyczne nauki kom puterow e Sztuczna inteligencja Robotyka Uczenie m aszynow e Sieci n euronow e

Krawędź

Planowanie kursów

Kurs

Dziedziczenie

Klasa Javy

Ograniczenia pierwszeństwa Wymagania wstępne extends

Arkusze kalkulacyjne

Komórka

Wzór

Dowiązania symboliczne

Nazwa pliku

Dowiązanie

© ,

Bazy danych Obliczenia naukow e Biologia obliczeniow a Sortowanie topologiczne

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) boolean ha sC ycle()

,,

Konstruktor wyszukujący cykle Czy G obejmuje cykl skierowany? Zwraca wierzchołki z cyklu (jeśli cykl istnieje)

Ite ra b l e cyc! e ()

Interfejs API do wykrywania cykli skierowanych

marked[] 1 2 3 4 5 d fs(0 ) d fs(5 ) d fs(4 ) d fs(3 ) Sprawdzani e

0 0 0 0 0 00 0 0 1 0 0 0 1 1 0 0 111

edgeTof] 0 1 2 3 4 5

0 -------------5 0 4 5 0 4 5 0

Wykrywanie cykli skierowanych w digrafach

0

1 1 1

o n sta c k f] 1 23 4 5 0 00 0 0

0 00 0 1 0 00 1 1

1001

i(T )

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 cycle; // // p rivate boolean[] onStack; // //

Wierzchołki w cyklu ( j e ś l i ten istn ie je ). 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)

{ onStack[v] = true; marked[v] = true; for (in t w : G.adj(v)) i ł (t h is . h a s C y c le Q ) return; e lse i f (¡marked[w]) { edgeTo [w] = v; dfs(G, w); } e lse i f (onStackfw])

v

w

3 3 3

3

x

5 3 5 4 5 4 5 4

c y c le

3 4 3 5 43 3 54 3

Ślad procesu wyznaczania cyklu

{ 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 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 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



591

Grafy skierowane

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 reversePost() przydatne w zaawansowanych algorytmach przetwarzania grafów. Przykładowo, metoda order () w klasie Topol ogi cal obejmuje wywołanie metody reversePost ().

Preorder odpowiada kolejności wywołań metody d f s O

Postorder odpowiada kolejności, w której wierzchołki sq „gotowe"

i

(

re v e rse P o st

p ost

pre

0

dfs COD dfs(5) dfs(4) 4 Gotowy 5 Gotowy

dfs Cl)

O 5 0 5 4

/

dfs(2)

/

4 4 5

/

4 5 4

/

0 5 4 1

1 Gotowy dfs(6) dfs(9) dfs C U D dfs(12) 12 Gotowy 11 Gotowy dfs(10) 10 Gotowy Sprawdzani e 12 9 Gotowy Sprawdzanie 4 6 Gotowy 0 Gotowy Sprawdzanie 1

Stos

Kolejka

Kolejka

4 5 1

15 4

4 5 1 12 4 5 1 12 11

12 1 5 4 11 12 1 5 4

4 5 1 12 11 10

10 11 12 1 5 4

4 5 1 12 11 10 9

9 10 11 12 1 5 4

4 5 1 12 U 10 9 6 4 5 1 1 2 1 1 10 9 6 0

6 9 10 11 12 1 5 4 0 6 9 10 1 1 1 2 1 5 4

4 5 1 1 2 1 1 10 9 6 0 3

3 0 6 9 10 11 12 1 5 4

4 5 1 12 11 10 9 6 0 3 2

2 3 0 6 9 10 11 1 2 1 5 4

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

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

0 5 4 1 6

054169 0 5 4 1 6 9 11 0541691112 0 5 4 1 6 9 1 1 1 2 10

0 5 4 1 6 9 1 1 1 2 10 2

Sprawdzanie 0 dfs(B) Sprawdzanie 5 3 Gotowy 2 Gotowy Sprawdzanie 3 Sprawdzanie 4 Sprawdzanie 5 Sprawdzanie 6 dfs(73 Sprawdzanie 6 2 Gotowy dfs(8)

0 5 4 1 6 9 1 1 12 10 2 3

0 5 4 1 6 9 1 1 1 2 10 2 3 7 0 5 4 1 6 9 11 12 10 2 3 7

Sprawdzanie 7 8 Gotowy

Sprawdzanie 9 Sprawdzanie 10 Sprawdzanie 11 Sprawdzanie 12

t

Odwrócony 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 pre; // Wierzchołki w porządku preorder. private Queue post; // Wierzchołki w porządku postorder. private Stack reversePost; // Wierzchołki w odwróconym porządku // postorder. public DepthFirstOrder(Digraph G)

{ pre = new Queue(); post = new Queue(); 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 post() { return post; } public Iterab le 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 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 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­ du. Wykonano jedno przeszukiwanie w głąb, aby zagwarantować, że graf nie obejmuje cykli skierowanych, i drugie w celu odwrócenia porządku postorder. Oba wywołania obejmują sprawdzenie wszystkich krawędzi i wszystkich wierz­ chołków, dlatego działają w czasie pro­ porcjonalnym do V+E.

Mimo prostoty tego algorytmu przez wiele lat nie znajdował się on w centrum uwagi. Popularnością cieszył się za to bardziej intuicyjny algorytm, oparty na przechowywaniu kolejki źródeł (zobacz ć w ic z e n ie

4 . 2 . 30 ).

w p r a k t y c e sortowanie topologiczne i wykrywanie cykli są ze sobą związane, przy czym wykrywanie cykli pełni funkcję narzędzia diagnostycznego. Przykładowo, w aplikacji do szeregowania zadań cykl skierowany w uzyskanym digrafie repre­ zentuje błąd, który trzeba naprawić. Nie ma przy tym znaczenia forma planu za­ dań. Tak więc aplikacja do szeregowania zadań wykonuje trzy kroki: ° Określa zadania i ograniczenia pierwszeństwa. ° Sprawdza, czy istnieje rozwiązanie; w tym celu wykrywa i usuwa cykle w grafie dopóty, dopóki nie zlikwi­ duje ostatniego. ° Rozwiązuje problem szeregowania, stosując sortowanie topologiczne.

dfs(O) dfs(5) dfs(4) Wywołanie dfs(5) dla 5 4 Gotowy (nieoznaczonego sąsiada 0) 5 Gotowy zostaje zakończone przed dfs(l) ukończeniem wywołania 1 Gotowy dfs(0), dlatego krawędź ' 0->5 prowadzi w górę dfs(6) dfs(9) dfs(1 1 ) dfs(1 2 ) i 12 Gotowy 11 Gotowy dfs(1 0 ) 10 Gotowy Sprawdzanie 12 9 Gotowy Sprawdzanie 4 6 Gotowy 0 Gotowy Sprawdzanie 1 dfs(2 ) Sprawdzanie 0 dfs(3) Sprawdzani e Wywołanie dfs(6) dla 6 3 Gotowy (nieoznaczonego sąsiada 1) 2 Gotowy zostaje zakończone przed sprawdzanie 3 ukończeniem w yw ołania' dfs (7), dlatego krawędź Sprawdzanie 4 / 6->7 prowadzi w górę Sprawdzanie 5 Sprawdzanie 6 dfs(7) / | Sprawdzanie 6 7 Gotowy dfs(8) Sprawdzanie 7 8 Gotowy Sprawdzanie 9 Sprawdzanie 10 Sprawdzani e 11 Sprawdzani e 12

Wszystkie krawędzie prowadzą w górę. Należy obrócić kolejność, aby uzyskać porządek topologiczny

ł ,-jk

t

Odwrócony porządek postorder odpowiada odwróconej kolejności, w jakiej wierzchołki stają się „gotowe" (należy czytać od dołu)

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 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 nieskierowanym 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 Krawędź Zastosowanie Wierzchołek spójne składowe mogą pomóc autorom podręcznika ustalić, które tematy warto Odnośnik Strona Sieć W W W połączyć, aprogram istom zdecydować, jak Odwołanie Podręcznik Temat uporządkować moduły programu. Na ry­ sunku poniżej pokazano przykład z dzie­ Wywołanie Oprogramowanie Moduł dziny ekologii. Przedstawiony digraf to Relacja Łańcuch model łańcucha pokarmowego łączącego Organizm pokarmowy drapieżnilc-ofiara organizmy. Wierzchołki reprezentują tu 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 Konstruktor ze wstępnym przetwarzaniem

SC C (D igraph G)

Czy v i w są silnie połączone?

boolean stro n glyC o n n e cte d (in t v, in t w) in t count()

Liczba silnie spójnych składowych

in t i d ( i n t v)

Identyfikator składowej obejmującej 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(s) dfs(v)

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, dlatego G musi obejmować ścieżkę z s do v

v musi być

„gotowy" przed s. Inaczej ,, , wywołanie v Gotowy-«— , ' 7

v Gotowy

\

d fs C v )

;

dfsfv)

dfs(v)

/ znalazłoby \

dfsCs) /

J

s Gotowy

d fs(D

sięp rzed dfs(s) w G

\ _

\ ■

s Gotowy

i

'

v Gotowy

!

Niemożliwe, ponieważ G B obejmuje ścieżkę z v do s Dowód poprawności algorytmu Kosaraju

s

i

Gotowy

G R musi obejmować ścieżkę z s do v

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; id[v] = count; fo r (in t w : G.adj(v)) i f (! marked [w]) dfs(G, w);

6

8 7

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 rekurencyjnej 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

odwróconym digrafie (R e v erse P o st)

,awdzanie nieoznaczonych wierzchołków w kolejności

dfs(O )

f dfs(l) V 1 Gotowy

8 Gotow y 1 7 Gotowy 6 Gotowy

/T J / ^ / (7)

7 / /

J /CN

dfs(2) dfs(4) d f s ( ll) df s(9) dfs(12)

/ V \ / ^ -n \ \ / no) \ \ \ \ \ \ 1 JL \ \ \ S p ra w d z a n ie 1 1 \ \ ( 1 2 ) ) \ \ dfs(10)

| sprawdzani 10 Gotowy 12 Gotowy sprawdzanie 8 Sprawdzanie 6 9 Gotowy 11 Gotowy Sprawdzanie 6 dfs(5) dfs(3) I Sprawdzanie 4 I Sprawdzanie 2 3 Gotowy Sprawdzanie 0 5 Gotowy 4 Gotowy Sprawdzanie 3 2 Gotowy 0 Gotowy dfs(l) Sprawdzanie 0 1 Gotowy Sprawdzanie 2 Sprawdzanie 3 Sprawdzanie 4 Odwrócony porządek Sprawdzanie 5 postorder na potrzeby Sprawdzanie 6 drugiego wywołania d f s ( ) Sprawdzanie 7 Sprawdzanie 8 (należy czytać od dołu) Sprawdzanie 9 Sprawdzanie 10 Sprawdzanie 11 Sprawdzanie 12

11 9 12 10 6 7 8

n m r*

dfs(5) dfs(4) dfs(3) Sprawdzanie 5 dfs(2) i Sprawdzanie 0 1 Sprawdzanie 3 2 Gotowy 3 Gotowy Sprawdzanie 2 4 Gotowy 5 Gotowy Sprawdzanie 1 V,0 Gotowy , Sprawdzanie 2 Sprawdzanie 4 Sprawdzanie 5 Sp ra w d za n i e 3_________ C dfs(1 1 ) ^ ' Sprawdzanie 4 dfs(1 2 ) dfs(9) I Sprawdzanie 11 | dfs(1 0 ) i I Sprawdzanie 12 1 10 Gotowy 9 Gotowy 12 Gotowy y n Gotowy „ Sprawdzanie 9 Sprawdzanie 12 Sprawdzanie 10 C dfs(6) Sprawdzanie 9 Sprawdzanie 4 Sprawdzanie 0 V 6 Gotowy J f d f s (7) > Sprawdzanie 6 dfs(8) | sprawdzanie 7 I Sprawdzanie 9 8 Gotowy 7 Gotowy _______ 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

601

Sprawdzanie nieoznaczonych wierzchołków w kolejności 1 0 2 4 5 3

I dfs(7:> I dfsW | sprawdzanie

Grafy skierowane

DF5 na pierw otnym digrafie

/ i 2 3 4 5 6 7 8 9 10 11 12

dfs(6)

Q

Silnie spójne składowe

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 ś la n ia

k o s a ra ju

p o łą c z e ń

w

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 ­

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

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

po

raz

p ie rw s z y

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 Ł

p

0

0 1 2

3 4 5

6 7

8 9

10 11 12

1

4

4



Grafy

(

^T \ \ c

Definicja. Domknięcie przechodnie digrafu G to inny digraf, o tym samym zbiorze wierzchołków, przy czym krawędź z v do wistnieje w domknięciu przechodnim wtedy i tylko wtedy, jeśli w G wjest osiągalne z v.

Zgodnie z konwencją każdy wierzchołek jest osiągalny z niego samego, dlatego do­ mknięcie przechodnie ma V pętli własnych. Pierwotna W przykładowym diagram ie istnieje tylko 13 12 jest krawędź krawędzi skierowanych, jednak domknięcie osiągalny (na czerwono) z6 przechodnie obejmuje 102 ze 169 możliwych Pętla własna (na szaro) krawędzi tego rodzaju. Ogólnie dom knięcie przechodnie digrafu m a wiele więcej krawę­ dzi niż sam digraf. Nieraz zdarza się, że do­ mknięcie przechodnie grafu rzadkiego jest gęste. Przykładowo, dom knięcie przechodnie cyklu skierowanego o V wierzchołkach, obej­ mujące V krawędzi skierowanych, jest digrafem 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. 2

3

4

5

6

7

9 10 11 12

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 p riv a te D ire cte d D FS[] a l l ; re obsługuje zapytania w stałym czasie, natomiast 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 () ]; fo r (in t v = 0; v < G .V (); v++) to ważne skutki praktyczne. Do czasu opracowa­ 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 boolean re a c h a b le (in t v, in t w) dowolnych par w bardzo dużych digrafach, ta­ { 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 digrafy 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

[ 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.

607

608

ROZDZIAŁ 4

I

a

Grafy

Ć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

(strona 536) listy sąsiedztwa budowane przez oparty na stru­ m ieniu wejściowym konstruktor klasy Digraph na podstawie pliku tinyDGex2.txt, który przedstawiono po lewej stronie.

16

8 2 1 0

4 3

11 6 36 10 3 7 11 7 8 11 8 20 62 52 5 10 3 10 81 4 1

4.2.3. Utwórz dla klasy Di graph konstruktor kopiujący, któ­

ry jako dane wejściowe przyjmuje digraf G oraz tworzy i ini­ cjuje nową kopię digrafu. Wszelkie zmiany wprowadzane przez klienta w G nie powinny wpływać na nowo utworzony digraf. 4.2.4. Dodaj do klasy Digraph metodę hasEdge(). Metoda ma

przyjmować dwa argumenty typu i nt, v i w, oraz zwracać true, 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

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 .

609

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

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).

611

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

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.

613

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. M inim alne drzewo rozpinające. Na podstawie ti nyEWG.txt nieskierowanego grafu ważonego znajdź m ini­ "■ *- 8 malne drzewo rozpinające (ang. minimum span4 5 0.35 Krawędź drzewa ning tree — MST). 4 7 0.37 ' M S T (czarna)

5 7 0

0.28

7

0.16

5 04

0.32 0.38

2

3

0.17

1

7

0.19

0

2

0.26

1

1 7 0.36 1 3 0.29 2 7 0.34 6 2

0.40

3 b 0.52 6 0 0.58 6 4 0.93

Definicja. Przypominamy, że drzewo rozpi­

nające grafu to spójny podgrafbez cykli, obej­ mujący wszystkie wierzchołki. Minimalne drzewo rozpinające grafu ważonego to drze­ Krawędź spoza drzewa M S T (szara)

wo rozpinające, którego waga (suma wag krawędzi) jest nie większa niż waga innych drzew rozpinających.

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­ ne do zaimplementowania. Należą Kabel Komponent Obwód do najstarszych i najlepiej poznanych Linie lotnicze Lotnisko Trasa lotu algorytmów spośród opisanych w tej Sieci energetyczne Linie przesyłowe książce. Zastosowanie w nich współ­ Elektrownia czesnych struktur danych przynosi Analiza obrazu Relacja bliskości Cechy 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. Graf ważony i jego drzewo MST

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 4 6 0 .,62 malnej wadze i łączących graf. Jeśli graf nie jest 5 6 0 . 88 spójny, m ożna zaadaptować algorytmy, aby wy­ 1 5 0 .,11 2 3 0 ..35 znaczyć drzewo MST każdej spójnej składowej. 0 3 0 .,6 Zbiór tych drzew nazywamy minimalnym lasem 1 6 0 .,10 0 2 0 .,22 rozpinającym (zobacz ć w i c z e n i e 4 .3 .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 4 6 0.62 na następnej stronie), w których wierzchołki to 5 6 0.88 punkty w przestrzeni, a wagi to odległości. Ważne 1 5 0.02 0 4 0.64 jest jednak, aby pamiętać, że wagi mogą repre­ 1 6 0.90 zentować czas, koszt lub zupełnie inną zmienną 0 2 0.22 1 2 0.50 — nie muszą być proporcjonalne do odległości. 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 C 6 0.88 nej łącznej wadze, który łączy wszystkie wierzchoł­ 1 5 0.02 ki. Taki podgraf musi tworzyć drzewo rozpinające. 0 4 -0 .9 9 Zgodnie z definicją drzewa rozpinające istnieją 1 6 0 0 2 0.22 także dla grafów, których wagi krawędzi są równe 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 1 2 1.00 ć w i c z e n i e 4 .3 .2 ). Możliwość istnienia wielu drzew 1 3 0.50 MST komplikuje dowody poprawności niektórych 2 4 1.00 3 4 0.50 algorytmów, dlatego w omówieniu nie dopuszcza­ 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. R óżne a n o m a lie w d rz e w a c h MST Podsumujmy — w omówieniu zakładamy, że zadanie 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 przypom 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. Właściwość przekroju Właściwość ta (nazywamy ją właściwością przekroju) związana jest z identyfi­

Dodanie krawędzi

Usunięcie krawędzi dzieli drzewo na dwie części 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 mają kolor czerwony

Krawędźprzekroju o minimalnej wadze musi znajdować się wdrzewie MST Właściwość przekroju

Twierdzenie J (właściwość przekroju). W dowol­ 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. Cykl musi zawierać przynajmniej jedną inną krawędź przekroju, na przykład f o wadze większej niż e (ponie­ 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­ rzą drzewo rozpinające.

Algorytm zachłanny tworzenia drzew MST

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(in t v, in t w, double weight) double w e ig h t()

Konstruktor inicjujący 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 a d j(in t v)

Zwraca krawędzie powiązane z v

Iterable 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 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 poif (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.

t i nyEW G.txt

816 4 4 5 0 1 0 2 1 0 1 1 2 6 3 6 6

V N.

5 7 7 7 5 4 3 7 2 2 3 7 2 6 0 4

0.35 0 .3 7 0 .28 0 .1 6 0.32 0 .38 0 .17 0.1 9 0.2 6 0.3 6 0.2 9 0.3 4 0.4 0 0.52 0.5 8 0.9 3

V

6

0 .58

0

2 . 26 |— *• 0

4 .38 —

0

1

3 .29 —

1

2 .36 —

1

7 .19 |—

1 15

6

2 |. 40 |— - 2 | 7 |. 34

1

2 .36 |—

0

2 |. 26 [— H 2 | 3 |. !7 |

3

6 |. 52 |— *j ! | 3 |. 29

2 | 3 |.17

6

4 .93

4

5 .35

0

4 .38 (—

4

7 .37

7 .16 .32

\ ''H 1

5 .32 |— | 5

7 .28 |— - 4 | 5 |.35

Obiekty typu Bag

----------- ^

Referencje do tego sa m e g o obiektu

lyt

s.

6 | 4 .93 —

6

0 .5 8 -

3

6 .52

6 | 2 |.40

2

1

7 .19 —

0

7 .16

5 | 7 .28 —

7 .34 —

Reprezentacja grafu ważonego

5

7 .28 |

622

ROZDZIAŁ 4

Grafy

Typ danych dla krawędzi w ażonych public c la ss Edge implements Comparable {

p rivate final p rivate final private final

in t v; in t w; double weight;

// Jeden wierzchołek, // Drugi wierzchołek, // 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 else else

(th is.w e igh t() < that.w eight()) i f (th is.w e igh t() > that.w eight())

return - 1 ; return + 1 ; 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[] adj; // L i s t y sąsiedztwa. public EdgeWeightedGraph(int V) {

t h i s . V = V; th is.E = 0 ; adj = (Bag[]) new Bag[V]; fo r (in t v = 0; v < V; v++) adj [v] = new Bag();

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 a d j(in t v) { return adj [ v ] ; ) public Iterable 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 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. b e z p o ś r e d n ie z a s t o s o w a n ie

4.3

Q Minimalne drzewa rozpinające

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) Iterable edges() double w e ig h t()

Konstruktor Zwraca wszystkie krawędzie drzewa M ST 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

625

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 stano816 wią model wielu typowych problemów praktycznych, 4 7 '37 o których wspomnieliśmy (na przykład map drogowych 5 7 .28 0 7 .16 lub obwodów elektrycznych). W itryna obejmuje też 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 sen1 7 .19 0 2 .26 sownym czasie. 1 1 2 6 3 6 6

2 3 7 2 6 0 4

.36 .29 .34 .40 .52 .58 .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

% 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

627

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 jednowierzchoł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ź niewybieralna (kolor szary)

Krawędź przekroju (kolor czerwony)

i

\ \

Krawędź drzewa (kolor czarny •i pogrubienie)

Krawędź przekroju o minimalnej wadze musi występować w drzewie M ST

Twierdzenie L. Algorytm Prima wyznacza drze­ wo MST dla dowolnego spójnego grafu ważonego. 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 o minimalnej wadze, dlatego po kolei koloru­ je krawędzie na czarno na podstawie algorytmu zachłannego.

I

Przedstawiony wcześniej jednozdaniowy opis algo­ 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.

Algorytm Prima - wyznaczanie drzew MST

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, 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 0-4 0 38 go grafu tinyEWG.txt. Na każdym rysun­ now ych 6 -0 0 58 elementów ku znajduje się graf i kolejka priorytetowa po odwiedzeniu wierzchołka (po dodaniu go do drzewa i przetworzeniu krawędzi Krawędzie przekroju (uporządkowane na liście sąsiedztwa danego wierzchołka). 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 1-3 0.29 stępujący sposób. 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 Krawędzie kolejki priorytetowej. 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 4-7 0.37 MST, a krawędź 3-6 — do kolejki 0-4 0.38 priorytetowej. Krawędź 1-3 staje się 6 - 2 0.40 3-6 0.52 niewybieralna. 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 mst; // Krawędzie drzewa MST. p rivate MinPQ pq; // Krawędzie przekroju (i niewybieralne). public LazyPrimMST(EdgeWeightedGraph G) {

pq = new Mi nPQ(); marked = new b oolean[G .V ()]; mst = new Queue(); 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 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żliv 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

633

Minimalne drzewa rozpinające

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 [] kolejności, co wersja leniwa. Różnica 0 \ polega na operacjach na kolejce priory­ 2 0 -. 3 tetowej. Ta wersja tworzy drzewo MST 4 0 -4 w opisany poniżej sposób. 6 6 -0 ° Dodaje 0 do drzewa MST, 7 0 -7 0 a wszystkie krawędzie z listy są­ 1 1 -7 siedztwa — do kolejki prioryte­ 2 0 -2 towej, ponieważ każda taka kra­ 4 0 -4 5 5 -7 wędź jest najlepszym (jedynym) 6 6 -0 7 0 -7 znanym połączeniem między 0 wierzchołkiem z drzewa i wierz­ 1 1 -7 2 0 -2 chołkiem spoza niego. 3 1 -3 4 0 -4 D Dodaje 7 i 0-7 do drzewa MST 5 5 -7 6 6 -0 oraz 1-7 i 5-7 do kolejki prio­ 7 0 -7 rytetowej. Krawędzie 4-7 i 2-7 0 1 1 -7 nie wpływają na kolejkę priory­ 2 0 -2 3 2 -3 tetową, ponieważ ich wagi nie 4 0 -4 są mniejsze niż wagi znanych 5 5 -7 6 6 -2 połączeń między drzewem MST 7 0 -7 a wierzchołkami 4 i 2. 0 1 1 -7 a Dodaje 1 i 1-7 do drzewa MST 2 0 -2 3 2 -3 oraz 1-3 do kolejki priorytetowej. 4 0 -4 5 5 -7 ° Dodaje 2 i 2-0 do drzewa MST, 6 6 -2 7 0 -7 zastępuje 0-6 krawędzią 2-6 jako Gruba czerw ona 0 najkrótszą krawędzią z wierz­ 1 1 -7 najmniejsza 2 0 -2 chołka z drzewa do 6 i zastępuje krawędź w pą, 3 2 -3 następna do 1-3 krawędzią 2-3 jako najkrót­ 4 4 -5 dod ania do 5 5 -7 szą krawędzią z wierzchołka 6 6 -2 drzewa M ST 7 0 -7 z drzewa do 3. 0 ■ Dodaje 3 i 2-3 do drzewa MST. 1 1 -7 2 0 -2 D Dodaje 5 i 5-7 do drzewa MST 3 2 -3 4 4 -5 oraz zastępuje 0-4 krawędzią 5 5 -7 6 6 -2 4-5 jako najkrótszą krawędzią 7 0 -7 z wierzchołka z drzewa do 4. 0 n Dodaje 4 i 4-5 do drzewa MST. 1 1 -7 2 0 -2 0 Dodaje 6 i 6-2 do drzewa MST. 3 2 -3 4 4 -5 Po dodaniu V - 1 krawędzi drzewo 5 5 -7 6 6 -2 MST jest kompletne, a kolejka priory­ 7 0 -7 tetowa — pusta.

d i s t T o []

/

r

0 .2 6 0 .3 8 0 .5 8 0 .1 6 0 .1 9 0 .2 6 0 .3 8 0 .2 8 0 .5 8 0 .1 6 0 .1 9 0 . 2 6 ■5 0.35 5->4 0.35 wagę ścieżkę skierowaną z jednego wierzchołka 4->7 0.37 do drugiego”. Problem ten jest tematem podroz­ 5->7 0.28 działu. Na rysunku po lewej stronie przedstawio­ 7->5 0.28 5->1 0.32 no przykład. 0->4 0.38 0->2 0.26 7->3 0.39 1->3 0.29 2->7 0.34 6->2 0.40 3->6 0.52 6->0 0.58

Najkrótsza ścieżka z 0 do 6 0->2 0.26 2->7 0.34 7->3 0.39 3->6 0.52

6->4 0.93

Digraf ważony i najkrótsza ścieżka

650

Definicja. Najkrótsza ścieżka z wierzchołka s do wierzchołka t w digrafie ważonym to ścież­ ka skierowana z s do t, cechująca się tym, że żadna inna ścieżka nie ma niższej wagi.

4.4

a

Najkrótsze ścieżki

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.

651

ROZDZIAŁ 4

652

h

Grafy

Reprezentacja tablicowa z krawędziam i do rodzica

\ n u li 5 -> l 0->2 7 -> 3 0 -> 4 4->5 3 ->6 2 ->7

6->0 nul 1 6 -> 2

1 ->3 6 -> 4 7 ->5 3->6 2 ->7

6->0 5 -> l nul 1 1->3 5->4 7->5 3->6 2-> 7

M ogę wysforować krawędzie równoległe i pętle

własne. Uwzględniana jest tylko krawędź o naj­ niższej wadze spośród krawędzi równoległych, a żadna najkrótsza ścieżka nie obejmuje pęt­ li własnej (wyjątkiem może być pętla o wadze zero, którą i tak pomijamy). W tekście niejawnie zakładamy, że nie występują krawędzie równole­ głe; pozwala to zastosować zapis v->w do jedno­ znacznego wskazywania krawędzi z v do w, przy czym kod obsługuje też krawędzie równoległe. Drzewo najkrótszych ścieżek Koncentrujemy się na problemie wyznaczania najkrótszych ścieżek z jednego źródła, gdzie podawany jest wierzchołek źródłowy s. Efektem obliczeń jest drzewo najkrótszych ścieżek (ang. shortest-paths tree — SPT), które określa najkrótszą ścieżkę z s do każdego wierzchołka osiągalnego z s.

Źródło

6->0

5->l 6 -> 2

6 -> l 5 -> l 6->2 l- > 3 5->4 nul 1 3->6 5->7

7->3 nul 1 4->5 3->6 4 -> 7 (Ś V r p

11 m

)

6->0

5->l G D ''

1|

,

& 6 -> 0 5 -> l 6->2 7->3 5->4 7->5 3->6 n u li

(Tjz, 11

fi ( 4)

Drzewa najkrótszych ścieżek

6 -> 2

7->3 6->4 7->5 n u li 2->7

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 i wszystkie wierzchołki osiągalne z s oraz tworzący drzewo skierowane o korzeniu w s. W drzewie tym każda ścieżka jest najkrótszą ścieżką w digrafie.

Zawsze istnieje drzewo tego rodzaju. Mogą istnieć dwie ścieżki o tej samej długości łączące s z wierz­ chołkiem. Wtedy m ożna usunąć ostatnią krawędź jednej z takich ścieżek i kontynuować ten proces do czasu pozostania jednej ścieżki łączącej źródło z każdym wierzchołkiem (powstaje wtedy drzewo z korzeniem). Przez utworzenie drzewa najkrótszych ścieżek można udostęp Krawędzie prow adzą o d źródła nić klientom najkrótszą ścieżkę z s do dowolne­ go wierzchołka grafu, posługując się repre­ zentacją z krawędziami do rodzica (to samo 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

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)

Zwraca wagę danej krawędzi

doubl e weight () 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 a d j(in t v)

Zwraca krawędzie wychodzące z v

Ite rab le edges()

Zwraca wszystkie krawędzie digrafu

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

653

6 54

ROZDZIAŁ 4

Grafy

Typ danych dla skierowanych krawędzi z w agam i public c la ss DirectedEdge { private final private final private final

in t v; in t w; double weight;

// Krawędź źródTowa. // Krawędź docelowa, // 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 nieskierowanych 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 private private

final in t V; in t E; Bag[] adj;

// Liczba wierzchołków, // Liczba krawędzi, // L i s t y sąsiedztwa.

p ublic EdgeWeightedDigraph(int V) {

t h is .V = V; th is.E = 0 ; adj = (Bag[]) new Bag[ V ] ; fo r (in t v = 0; v < V; v++) adj [v] = new Bag(); }

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 a d j( in t v) { return adj [ v ] ; } public Iterable edges() {

Bag bag = new Bag(); 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 5 4 4 7 5 7 7 5 5 1 0 4 0 2 7 3 1 3 2 7 6 2 3 6 6 0 6 4

0.35 0.35 0.37 0.28 0.28 0.32 0.38 0.26 0.39 0 .2 9 0 .3 4 0.40 0.52 0.58 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)

Zwraca odległość z s do v (°°, jeśli ścieżka nie istnieje)

double d is t T o ( in t v)

boolean hasPathT o(in t Ite ra b l e p a th T ofint v)

Konstruktor

v)

Czy istnieje ścieżka z s do 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 p u b lic s t a t ic void m a in (S trin g [] args) najkrótszą ścieżkę ze źródła do każdego { 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 ] ) ) ; in t s = In t e g e r . p a r s e ln t ( a r g s [ l] ); kich implementacjach klas do wyzna­ SP sp = new SP(G, s ) ; czania najkrótszych ścieżek. W przykła­ dach korzystamy z pliku tinyEWD.txt, fo r ( in t t = 0; t < G .V (); t++) przedstawionego na następnej stronie, { S td O u t.p rin t(s + " do " + t ) ; w którym określone są krawędzie i wagi 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)) Std O u t.p rin t(e + " " ) ; krótszych ścieżek. Zastosowano tu ten 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.

% 0 0 0 0 0 0 0 0

java do 0 do 1 do 2 do 3 do 4 do 5 do 6 do 7

SP tinyEW D.txt 0 (0 .0 0): (1 .0 5): 0->4 0.38 (0 .2 6): 0->2 0.26 (0 .9 9): 0->2 0.26 (0 .3 8): 0->4 0.38 (0 .7 3): 0->4 0.38 (1 .5 1): 0->2 0.26 (0 .6 0): 0->2 0.26

4->5 0.35

5 - > l 0.32

2->7 0.34

7->3 0.39

4->5 0.35 2->7 0.34 2->7 0.34

7->3 0.39

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 5 > l 0 . 3 2 1 .0 5 stających ze struktur danych przy 0 .2 6 0->2 0.26 obsłudze zapytań klientów o naj­ 0 .9 7 7 -> 3 0 .3 7 0 > 4 0 . 3 8 0 .3 8 krótsze ścieżki i ich długości. Relaksacja

krawędzi Kod

4 -> 5 0.35 3 -> 6 0 .5 2

0 .7 3 1 .4 9

do 0 .6 0 2 -> 7 0.34 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 ó­ { in t v = e .fro m (), w = e . t o ( ); wimy, że krawędź jest niewybierali 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; 1 ści strony pokazano dwa możliwe skutki relaksacji krawędzi. Albo krawędź jest niewybieralna (tak jak Relaksacja krawędzi

4.4

a

Najkrótsze ścieżki

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)

659

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

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 3 7 -> 3 kę pozbawioną krawędzi, jeżeli v jest źródłem. 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 7 -> 3 3 -> 6 Na rysunku po prawej stronie pokazano znaj­ 2 -> 7 7 - > 3 3 -> 6 dowanie ścieżki 0->2->7->3->6 w przykłado­ 2 - > 7 7 -> 3 3 -> 6 0 -> 2 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 pathTo(int v) { i f (Ih a sP a th T o (v)) return n u ll; Stack 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

661

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 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] 2 0.26

0.26

n a jk ró tsz y c h śc ie ż e k z s.

4 5

0->4 0.38 4->5 0.35

0.38 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

0.00

6 2->7 0.34

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

7

tin y E W D .tx t. A lg o ry tm tw o rz y d rz e w o S P T

0

w n a s tę p u ją c y s p o s ó b .

2 3 4 5

0->2 7->3 0->4 4->5

7

2->7 0.34

0.60

0.32 0.26 0.37 0.38 0.35

0.00 1.05 0.26 0.97 0.38 0.73

7

2->7 0.34

0.60

0 1 2 3 4 5 6 7

5->l 0->2 7->3 0->4 4->5 3->6 2->7

0.32 0.26 0.37 0.38 0.35 0.52 0.34

0.00 1.05 0.26 0.97 0.38 0.73 1.49 0.60

0 1 2 3 4 5 6 7

5->l 0->2 7->3 0->4 4->5 3->6 2->7

0.32 0.26 0.37 0.38 0.35 0.52 0.34

0.00 1.05 0.26 0.97 0.38 0.73 1.49 0.60

0 1 2 3 4 5 6 7

5->l 0->2 7->3 0->4 4->5 3->6 2->7

0.32 0.26 0.37 0.38 0.35 0.52 0.34

0.00 1.05 0.26 0.97 0.38 0.73 1.49 0.60

° D o d a je 0 d o d rz e w a , a s ą s ie d n ie w ie rz ­ c h o łk i, 2 i 4, d o k o le jk i p rio ry te to w e j. ° U su w a 2 z kolejki p rio ry teto w ej, d o d a je 0->2 d o d rz e w a i 7 d o k o le jk i p rio ry te to w e j. ° U su w a 4 z k o le jk i p rio ry te to w e j, d o d a je 0-> 4 d o d rz e w a i 5 d o k o lejk i p r io r y te to ­ w ej. K ra w ę d ź 4->7 staje się n ie w y b ie ra ln a . ■ U su w a 7 z k o le jk i p rio ry te to w e j, d o d a je 2->7 d o d rz e w a i 3 d o k o lejk i p r io r y te to ­ w ej. K ra w ę d ź 7->5 staje się n ie w y b ie ra ln a . 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~>5 d o d rz e w a i 1 d o k o le jk i p r io r y te to ­ w ej. K ra w ę d ź 5->7 staje się n ie w y b ie ra ln a . * U su w a 3 z kolejki p rio ry teto w ej, d o d a je 7 ->3 d o d rz e w a i 6 d o k o le jk i p rio ry te to w e j. ■ 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 5 - > l d o d rz e w a . K ra w ę d ź l- > 3 sta je się n ie w y b ie r a ln a . ° 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 3-> 6 d o d rz e w a . 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 ­ 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 , 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 s tro n y r y s u n k u .

—*-

0 1 2 3 4 5

0.60 0.00

5->l 0->2 7->3 0->4 4->5

0.26 0.37 0.38 0.35

0.26 0.97 0.38 0.73

6

Ślad działania algorytmu Dijkstry

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 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( 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)

// // p u b l i c boolean ha s P a th T o ( i nt v) // // p u b l i c I t er ab l e< E dge> p a t hT o ( i n t v) //

Standardowe metody o bs ł ugi zapytań kl i entów dla implementacji techni k tworzeni a drzew SPT (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 ­ n ę 6 5 7 ). P rz y p o m n ijm y , że lin ie w g rafie

pub lic c la s s D ij k s t r a A llP a ir s S P

{

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 . 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 ­

1 all

= new D i j k s t r a S P f G . V ()]

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 ) ;

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 g ra fa c h w a ż o n y c h . P r o b le m

te n m o ż n a

ro z w ią z a ć w c z asie lin io w y m (sz y b c ie j n iż

1

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) { return a l l [ s ] . d i s t T o ( 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 . }

1 W yznaczanie najkrótszych ścieżek dla wszystkich par

4.4

Algorytm Dijkstry (250 wierzchołków, różne źródła)



Najkrótsze ścieżki

669

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 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 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 ­ n e g o ź r ó d ła d la w a ż o n y c h g ra fó w D A G . N a le ż y z a in ic jo w a ć d i s tT o [s ] za p o m o c ą 0 , a w sz y stk ie p o z o s ta łe e le m e n ty ta b lic y

d i s t [] — n ie s k o ń c z o n o ś c ią . N a s tę p n ie 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 ł­ 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­ 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 ­ 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 D ijk s tr y n a s tr o n ie 664.

t in y E W D A G .t x t

13-*- E 5 4 0.35 4 7 0.37 5 7 0.28 5 1 0.32 4 0 0.38 0 2 0.26 3 7 0.39 1 3 0.29 7 2 0.34 6 2 0.40 3 6 0.52 6 0 0.58 6 4 0.93 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]

l

,'7 Y

3 4

5 -> 4

5 6

ś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 .

7

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 ­ 2.

n

U

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

k u to p o lo g ic z n e g o 5 1 3 6 4 7 0

671

Najkrótsze ścieżki

Sortowanie topologiczne

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 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



5 -> 7

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

z n ie g o k ra w ę d z ie .

5 -> l l- > 3 5 -> 4

° D o d a je d o d rz e w a 1 i k ra w ę d ź l-> 3 . ° 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, ale ju ż n ie 6-> 4, p o n ie w a ż je s t n ie w y b ie ra ln a .

Czerwona krawędź dodawana do drzewa

° 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 5 -> 4

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 ź 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 6 -> 2 l- > 3 5 -> 4

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 ­ w ę d ź 0 -> 2 , 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 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 5 -> l

w y c h o d z ą ż a d n e k ra w ę d z ie .

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

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­ 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 A P I M as EdgeWei ghtedDi graph i Di rectedEdge z teg o 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 , ż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 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 acyld 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 , 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 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

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 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 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 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 ­ n ych, o k tó ry c h w ia d o m o , że są acyM iczne.

V

Szara krawędź - niewybieralna

3 -> 6 5 -> 7

4 -> 0 5 -> l 7 -> 2 l-> 3 5 -> 4 3 -> 6 5 -> 7

4 -> 0 5 -> l 7 -> 2 l-> 3 5 -> 4 3 -> 6 5 -> 7

Ślad procesu wyznaczania najkrótszych ścieżek w ważonym grafie DAG

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) public boolean hasPathTo(int v) public Iterable pathTo(int v)

// // // // //

Standardowe metody obsługi zapytań od klientów dla implementacji technik tworzenia drzew SPT (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

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

5 do

7 ( 0 . 2 8 ) : 5- >7 0.28

4 - > 0 0. 3 8

l - > 3 0. 2 9

3 - > 6 0.52

4.4

b

Najkrótsze ścieżki

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 .

673

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 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

Sortow anie topologiczne 5 1 3 6 4 7 0 2

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

0

edgeTo[]

1

5->l

4

5->4

7

5->7

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 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 4

l->3 5->4

7

5->7

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 ­ k u to p o lo g ic z n e g o 5 1 3 6 4 7 0 2.

0

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 ­ d z ą c e z n ie g o k ra w ę d z ie . ° D o d a je d o d rz e w a 1 i k ra w ę d ź l-> 3 .

1

5->l

3 4

l->3 5->4

6 7

3->6 3->7

0 1 2 3 4

6 -> 0 5->l 6 -> 2 l->3 6->4

6 7

3->6 3->7

0 1

4->0 5->l

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 3-> 6 i 3 -> 7 . K ra w ę d ź 5->7 sta je się n ie w y b ie ra ln a . * D o d a je d o d rz e w a 6 o ra z k ra w ęd z ie 6->2, 6-> 4 i 6-> 0.

5

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 i 4-> 7. K ra w ę d z ie 6-> 0 i 3->7 s ta ją się n ie w y b ie ra ln e . ■ 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 s ta je się n ie w y b ie ra ln a . ° D o d a je d o d r z e w a 0 , a le n ie k r a w ę d ź

6 -> 2

3 4

l->3 6->4

5

0 - > 2 , p o n ie w a ż je s t n ie w y b ie ra ln a .

■ 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

2

6 7

3->6 4->7

0 1 2 3 4

4->0 5->l 7->2 l->3 6->4

6 7

3->6 4->7

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 .

Ślad procesu w yznaczania najdłuższych ścieżek w acyklicznej sieci

4.4



675

Najkrótsze ścieżki

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 ... 1 /1 1 1 . 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

Zadanie

Czas trwania

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

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

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 ­

Tl ze^a zakończyć przed 1

7 2

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

6

2 1.0

3

8

7

3 2.0

3

8

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 ­ 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 , ż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 . 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 ­ 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 -

8

3 2.0

9

2 9.0

2 4

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 .

L

1--------------------------------- i----------------------- 1---------------- i

0

41

70

91

6

1

123

Rozwiązanie problemu szeregowania równoległych zadań

1

173

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

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 ; EdgeWeightedDigraph G; G = new EdgeWeightedDigraph(2*N+2);

50 0 36.0 38 .0

45 .0

in t s = 2*N, t = 2*N+1; f o r (in t i = 0; i < N; i++)

21-0

|

32.0

32-°

S t r in g [] a = Std ln .re ad L in e Q .spl i t ( " \ \ s + " ) ; 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++)

29.0

38

38 2 46

{

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 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 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 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

0 : 0.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­ 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

5: 0. 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 ­ zien ia d rz e w a n ajd łu ż szy c h ścieżek i w y św ietlen ia ich d łu ­ gości (czyli czasów ro zp o c zę c ia k ażd eg o zad a n ia ).

1: 41 .0 2: 123.0 3: 91 .0 4: 70.0 6: 70 .0 7: 4 1 . 0 8: 91 .0 9: 41 .0 Czas z a k o ń c z e n ia :

17 3.0

677

678

R O ZD ZIA Ł 4



Grafy

O ryginał

Zadanie Rozpoczęcie

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 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

1 2

41.0 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 4

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

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

6 7

70.0 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

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

9

s z e ń s tw a .

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 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 ­

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 niż 12,0 po 4

Zadanie Rozpoczęcie

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 ń . 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 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

3 4

91.0 1 11 . 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 ż ­ 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 ń . 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.

5

0.0

6 7

70.0 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

2 nie później niż 70,0 po 7

Zadanie Rozpoczęcie

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 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 ­ 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ą 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 ­

Zadanie Czas Względem 2 2

12.0 70.0

4 7

4

80.0

0

0

0.0

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 2

41.0 123.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 ­

3

91.0

4

111.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

5 6

0.0 70.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ż

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

9

41.0

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 ­

Terminy graniczne uwzględniane przy szeregowaniu zadań

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­

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 ­ 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 niż 80,0 po 0

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 ­

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

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 ­

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 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 ­ zadań

4.4



Najkrótsze ścieżki

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

679

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 ­ 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 ­

ti nyEWDn.txt

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 4->5 5->4 4->7 5->7 7->5 5->l 0->4 0->2 7->3 1-> 3 2->7 6->2

0.35 0.35 0.37 0.28 0.28 0.32 0.38 0.26 0.39 0.29 0.34 -

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 ­ 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 , n a k tó r y m

pokazano

s k u tk i u w z g lę d ­

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 n a jk r ó ts z y c h

ście ż ek .

P ra w d o p o d o b n ie

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ą Wagi ujemne oznaczamy linią przerywaną

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 w a d z e m a ją z w y k le w ięcej k ra w ę d z i n iż ś 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

1.2 0

3->6 0.52 6->0 -1.40 6->4 -1.25

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 ­ 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

Drzewo najkrótszych ścieżek z 0

edgeTo[] d istT o[] 0 1 2 3 4 5 6 7

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­ 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 .

5->l 0->2 7->3 6->4 4->5 3->6 2->7

0.93 0.26 0.99 0.26 0.61 1.51 0.60

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 w y s z u k iw a n ie „ k ró tk ic h ” śc ie ż e k u t r u d ­ n ia

z ro z u m ie n ie

a lg o ry tm ó w .

D la te g o

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 ­ 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

tinyEWDnc.txt

C y k le

u j e m n e P rz y

o

Najkrótsze ścieżki

r o z w a ż a n iu

681

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 5 4 4 7 7 5 51 0 4

-

k ra w ę d z ie

0.35 0.66 0.37 0.28 0.28 0.32 0.38 0.26 0.39 0.29 0.34 0.40 0.52 0.58 0.93

o

w agach

u je m n y c h ,

n a j­

k ró ts z e ś c ie ż k i n ie m a ją z n a c z e n ia , je ś li 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 . 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 ­ n y p o lew ej s tro n ie , n ie m a l id e n ty c z n y z p ie r w s z y m

p rz y k ła d e m .

W y ją tk ie m

je s t to , że k ra w ę d ź 5-> 4 m a w a g ę - 0 .6 6 . W a g a c y k lu 4 -> 7 -> 5 -> 4 w y n o s i tu : 0 .3 7 + 0 .2 8 - 0 .6 6 = - 0 .0 1 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 te n c y k l i g e n e ro w a ć d o w o ln ie k ró tk ie

Najkrótsza ścieżka z 0 do 6 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 ­ w ę d z ie w c y k lu s k ie ro w a n y m m u s z ą m ie ć

Digraf ważony z ujemnym cyklem

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 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 ­

Szary wierzchołek -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 ' - osiągalny z s

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 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 najkrótsza ścieżka z s

Twierdzenie W. N a jk ró ts z a ś c ie ż k a z s d o v 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 ­ 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 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 .

/

Czerwony obrys - nie istnieje najkrótsza ścieżka z Możliwości związane z najkrótszymi ścieżkami

5

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

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 .

683

RO ZD ZIA Ł 4

684

o

Grafy

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 ż ­

Źródł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 ­

n ©

© '-

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 ę 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

*

Na czerwono oznaczono wierzchołki znajdujące się w kolejce w danym kroku

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 ­ 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 ­ 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 []

6->0 6->2 l->3 6->4 3->6

■ 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

3-> 6

i u m ie s z c z e n ie

3 w k o lejce. ■ R e la k sa c ja

k ra w ę d z i

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 u m ie s z c z e n ie 4, 0 i 2 w k o le jce .

edgeT o []

6->0 6 ->2 1->3 6->4 4->5 3->6 2->7

\

Krawędź o zmienionym kolorze

■ 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 ­ 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 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 , 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 ­ r u k ra w ę d z i 4 -> 7 ). ■ R elak sacja k ra w ę d z i 7->5 (o ra z z m ia n a k o lo ru 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 6->4 7->5 3->6 2->7

je s t n ie w y b ie ra ln a . P o te m m a m iejsce re la k sa ­ c ja k ra w ę d z i 5 -> l, 5->4 i 5->7 (są n ie w y b ie ra l­ n e ), p o c z y m k o lejk a staje się p u sta. I m p le m e n ta c j a Z a im p le m e n to w a n ie

edgeTo []

0

6-> 0

1

a lg o ry tm u

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 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 3->6

s tru k tu r a c h d a n y ch :

2 -> 7

Ślad działania algorytmu Bellmana-Forda

■ 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 d o re la k sa c ji; ■ 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 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

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 f o r (D i re c t e d E d g e e : G. a d j ( v )

w a r­

{

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

p r z e tw o r z o n y w n a s tę p n y m .

1

(dis tT o Jw ] > d i s t T o [ v ] + e . w e i g h t O ) dis t T o Jw ]

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 ­

= di stT o [v] + e . w e i g h t O ;

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

ta c ji

4 .1 1 )

k la s y

p rzez

nas

B e llm a n F o rd S P

w y k o rz y s ta liś m y

in n e

}

im p le m e n ­

1 if

(a lg o ry tm 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.

( c o s t + + % G. V () == 0) findNegativeC ycle();

} }

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.

v)

686

RO ZD ZIA Ł 4

Grafy

ALGORYTM 4.11. Algorytm Bellmana-Forda (oparty na kolejce) public c la ss Bel ImanFordSP (

private doublet] distTo; private DirectedEdge[] edgeTo; private boolean[] onQ;

// // // // // // // //

private Queue queue; private in t cost; private Iterable cycle;

DTugość ście żk i do v. Ostatnia krawędź ście ż k i do v. Czy dany wierzchołek znajduje s ię w kolejce? Wierzchołki po re la k s a c j i. Liczba wywołań metody re la x (). 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(); 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) public boolean hasPathTo(int v) public Iterable pathTo(int v)

// // // // //

Standardowe metody obsługi zapytań klientów dla implementacji technik tworzenia drzew SPT (zobacz stronę 661).

private void findNegativeCycle() public boolean hasNegativeCycle() public Iterable 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

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 ­

a

Najkrótsze ścieżki

687

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

Krawędzie z kolejki 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

tinyEWDn.txt 4->5 0.35 5->4 0.35 4->7 0 .3 7 5->7 0.28 7->5 0.28 5 - > l 0.32 0-> 4 0.38 0->2 0.26 7->3 0.39 l- > 3 0 .2 9 2->7 0.34 6->2 - 1 . 2 0 3->6 0.52 6->0 -1 .4 0 6->4 -1 .2 5

Grafy

edgeTo[] d istT o [] 0 1 2 3 4

0- > 2 0~>4

0.38

5

4->5

0.73

2->7

0.60

0.26

6

Źródło

7

edgeTo[] d istT o []

0 1

5-> l

1.05

2

0- > 2

0.2 6

3

7->3

0.99

4 5 6 7

0 -> 4 4- > 5

0.38 0.73

2- > 7

0.60

edgeTo[] d istT o [] 0 1 2 3 5

5->l 0 -> 2 7 ->;3 0- >4 4- >5

3->6

1.51

7

2~>7

0.60

4

6

1 .0 5 0.26 0.99 0.38 0.73

edgeTo[] d istT o [] 0

1

5->l

2 3

0-> 2 7- 7

4 5

6->4 4->5

6 7

3 -> 6 2-> 7

1 .,05 0 ., 26 0. 0 .,26 0. ,73 1 ., 51 0 ., 60

edgeTo[] d istT o [] 0

1

5->l

1.05

2 3 4

0~>2 7->3 6- > 4

0.26 0.99 0.26

5

4->5

0.61

6 7

3-->6 2-> 7

1.51 0.60

0 1 2 3 4 5

6 7

edgeTo[] distT o[ 5->l

0->2 7->3 6->4 4->5 3->6 2->7

0.93 0.26 0.99 0.26 0.61 1.51 0.60

Ślad działania algorytmu Bellmana-Forda (przy wagach ujemnych)

Ju ż nie sq wybieralne!

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 ( ) Ite rab le ne gative Cycle()

Czy występuje cykl ujemny? 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 ; Edg eW eight edD igrap h s p t ; na iterator po krawędziach, jeśli znalezio­ 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 ) s p t . a d d E d g e ( e d g e T o [ v ] ); 0 Przez wywoływanie m etody findNegativeC ycle() co V wywołań m etody reE 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­ torze zakończy działanie. Ponadto klienty mogą wywołać metodę hasNegativeCycle(), aby ustalić, czy ze źródła dostępny jest cykl ujemny, a wywołanie m etody negat i veCycl e () pozwala pobrać taki cykl. Dodanie możliwości wykrywania dowolnych cykli ujemnych w di­ grafie także jest prostym rozwinięciem rozwią­ zania (zobacz

Ć W IC Z E N IE

4 .4 .43 ).

cycle = c f . c y c l e d ;

1 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 ( ) { return cycle

!= n u l 1; }

p u b lic Iterable nega tive Cy cle () { return cycle;

}

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 4->5 5->4 4->7 5->7 7->5 5->l 0->4 0 -> 2

6 -> 2

3->6

6->0 6->4

edgeTo[] d istT o []

\

Zródto

4

0- >4

0 . 38

5

4->5

0.73

7

2->7

0.60

edgeTo []

o 1— ■ M l/l

2->7

queue

T3

7->3

1->3

0.35 0. 66 0.37 0.28 0.28 0.32 0.38 0.26 0.39 0.29 0.34 0.40 0.52 0.58 0.93

1

5->l

1.05

2

0->2

0.26

3 4

7->3 5->4

0.99 0.07 ^

Długość ścieżki

5 6

4- >5

0.73

0->4->5->4

7

2 -> 7

0.60

e d ge T o

0 1 2 3

5

6 7

[]

d ist T o []

5 ->1 0 -> 2 7->3 0- >4

1.05 0.26 0.99 0.07

4->5 3->6 2->7

0.42 1.51 0.44

Ślad działania algorytmu Bellmana-Forda (dla grafu z cyklem ujemnym)

4.4

Q

691

Najkrótsze ścieżki

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

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ć

j

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-

USD

1

0 .741

0..657

1..061

1..005

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 ­ 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 ż ­

-lnC .7 41 )

\

-lnC l. 36 6)

\

-l n(.995)

)

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

Cykl ujemny reprezentujący 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 .

Algorytm

Ograniczenia

Dijkstry (wersja zachłanna)

K raw ęd zie

Sortowanie topologiczne

Liczba porównań długości ścieżek (tempo wzrostu) Typowy przypadek

Najgorszy przypadek

ElogV

E lo g V

Dodatkowa pamięć

Główna zaleta

V

G w a ra n c je d la

o w ag ach

n ajg o rszeg o

d o d a tn ic h

p rz y p a d k u

W ażo n e

E+V

E+V

V

g rafy D A G

O p ty m a ln y d la grafó w acy k liczn y ch

Bellmana-Forda (oparty na kolejce)

B ra k cykli u je m n y c h

E+V

VE

V

0 w ielu

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

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 ).

695

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 BellmanaForda.

4.4

a

Najkrótsze ścieżki

Ć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

-

697

698

RO ZD ZIA Ł 4



ĆWICZENIA

Grafy

(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

4 .4 .1 9

n

Najkrótsze ścieżki

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 .

699

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

4 . 4 .3 0

n

Najkrótsze ścieżki

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 iton 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 ).

701

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

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 .

703

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

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 .

705

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

K

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 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

Z łą c z a n ie W

Q

Łań cu ch y znaków

Javie o p e ra c ja u tw ó rz

709

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 ­ 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

0 — ►

A

1 T

2

3

4

5

T

A

C

K

6 A

7

8 T

9 D

10 11 12 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

Deklarowanie

Tablica znaków char[]

a

Klasa String Javy 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

Tworzy nowy alfabet ze znaków z s

A l p h a b e t ( S t r i n g 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 Czy c występuje w alfabecie?

bool ean c o n t a i n s ( c h a r c) int

R()

Zwraca podstawę (liczbę znaków w alfabecie)

i nt

IgRO

Zwraca liczbę bitów potrzebnych do zapisania indeksu Przekształca s na liczbę całkowitą o podstawie R

i n t [] t o I n d i c e s ( S t r i n g s) Strin g toC h ars(in t[]

indices)

Przekształca liczbę całkowitą o podstawie R 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

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

BASE64

64

6

A SC II

128

7

Znaki ASCII

EXTENDED_ASCII

256

8

Znaki z rozszerzonego zestawu ASCII

UNIC0DE16

65536

16

Znaki Unicode

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 cd efg h ijld m n o p q rstu v w x y zO 1 2 3 456789+ /

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+ + ) if

(alp h a.co n tain s(s.ch arA t(i))) count [ a l p h a . t o l n d e x ( s . c h a r A t ( i ) ) ] + + ;

f o r ( i n t c = 0; c < R; c++) S td O ut.p rintln(alpha .toCha r(c) + " " + count [ c ] ) ;

% more a b r a . t x t ABRACADABRA! % j a v a Count ABCDR < a b r a . t x t A 5 B 2 C 1 D 1 R 2

Typowy klient klasy Alphabet

711

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

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 .

713

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

Sortowanie przez zliczanie

W

ram ach

h

ro z ­

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 ­ te c z n ą ,

k ie d y

k lu c z a m i



m a łe

lic z b y

c a łk o w ite .

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 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 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 ­ m y w p o d r o z d z ia le . 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 ­ 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 ­ 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. 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 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, m o ż n a zasto so w ać so rto w a n ie p rz e z zliczanie. Z ak ład am y , ż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 ­ 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 to liczb y całk o w ite o d f o r (i = 0; i < N; i + + )

0 d o R -l, a in s tru k c ja

count [a [i ] .key () + 1]++;

^ 0

Robi nson Smi th T a y lo r Thomas Thompson whi te W illia m s W i 1 son

2 3 3 4

1 3 4 3

1 2 2 1 2 4 3 4 4

2 3 4

m e r g ru p y o k reślo n e g o

c o u n t [] 12 3 4

Zawsze 0 Anderson Brown D a vi s Garci a H arri s Jackson Jo h n s o n Jones M arti n M a r t i nez M ille r Moore

a [i ] . key () z w ra c a n u ­

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 1 1 1 1 2 2 2

0 1 1 1 1 1 1 1 1 1 2

0 0 1 2 2 2 3

u c z n ia . M e to d a sk ła d a

0 0 0 0 1 1 1 2 2 2 2 2 2 2

3 4 4 4 3 4 3 3 4 3 4 4 3 4 4 3 3 4 5 3 3 4 5 4 3 3 3 3

Liczba trójek x Zliczanie wystąpień

4 5 5 5 5 5 5 6 5 5 .6 6

się z c z te re c h kroków . O p is u je m y k o le jn o k a ż ­

Sortowanie łańcuchów znaków

Dane wejściowe Nazwisko Grupa Anderson Brown D a vi s Garci a Harri s Ta ck s o n Jo h n s o n Jo n e s M arti n M a r t i nez M ille r Moore Robi nson Smi th T a ylo r Thomas Thompson Whi te w i 1 1 iams W i1 son

2 3 3 4

1 3 4 3

1 2 2 1 2 4 3 4 4

2 3 4

Posortowane dane Według grup H arri s Marti n Moore Anderson M artin e z Mi 11 e r Robi nson Whi te Brown D a vi s Jackson Jones T a y lo r Wi 11 i ams Garci a Jo h n s o n S m it h Thomas Thompson W i 1 son

1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4

Klucze to małe liczby całkowite Typowe dane przy sortowaniu przez zliczanie

d y z n ich . 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 ­ 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 d o te g o ta b lic a count [] w a r to ś c i ty p u int. D la k a ż ­ 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 ­ 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 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 ś ć

count [ r + 1 ]. D la c z e g o +1? S ta n ie się to z r o z u m ia ­ ł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 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 ­

unt [3 ], p o n ie w a ż Anderson n a le ż y d o g r u p y 2, p o ­ 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 ­ 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 , że count [0] z a w sz e m a w a rto ś ć 0 , a count [1] w ty m 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 g r u p y 0 ).

715

R O ZD ZIA Ł 5

716

a

Łań cuch y znaków

P rzelcształcanie liczb w y stą p ie ń na indelcsy N a s tę p ­

for

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

( i n t r = 0; r < R; r++ ) 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 ­ count[]

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 . 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­ 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 ­ 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ą ­ 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 ] . 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 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

14 20 Liczba kluczy mniejszych niż 3 (początkowy indeks trójek w danych wyjściowych) Przekształcanie liczby wystąpień 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 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 )

if

(d == p a t . l e n g t h ( ) && x . v a l

return;

if

(d == p a t . l e n g t h O )

!= n u l l )

q.enqueue(pre);

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

shore

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ę na końcu łańcucha znaków. Wartość jest różna od n u li, dlatego należy zwrócić she

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 )

( if

(x == n u l l )

if

(x.val

return length;

if

(d == s . l e n g t h ( ) )

!= n u l l )

l e n g t h = d; 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

Usuwanie Pierwszy krok potrzebny do usunię­ 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­ 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­ tacji na następnej stronie pokazano, że zadanie to można wykonać za pom ocą zaskakująco nie­ wielkiej ilości kodu, stosując standardowy rekurencyjny 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ę na końcu łańcucha znaków. Wartość to nul 1, dlatego należy zwrócić she (jest to ostatni klucz na ścieżce)

"shell sort"

Wyszukiwanie kończy się w odnośniku n ul 1. Należy zw ró c/ćsh e lls (jestto ostatni klucz na ścieżce) "shelters"

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 ; i f (d == k e y . l e n g t h O ) trzeba zrobić: x . v a l = n u l 1; ■ Zaimplementować konstruktor, któ­ else ry jako argument przyjmuje obiekt { c ha r c = k e y . c h a r A t ( d ) ; Alphabet, ustawia zmienną egzem­ x . n e x t [ c ] = d e l e t e ( x . n e x t [ c ] , key, d+ 1) ; plarza typu Alphabet na wartość ar­ } 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

Ustawianie wartości y na nuli (5

( -j) y ), ( f )

(a)2 Q ) © o © i

©1

Wartość i odnośniki to nuli, dlatego należy usunąć węzeł (i zwrócić odnośnik nuli)

Wartość jest różna od nuli, dlatego nie należy usuwać węzła (trzeba zwrócić odnośnik do niego)

Odnośnik jest różny od nuli, dlatego nie należy usuwać węzła (trzeba zwrócić oćjnośnik do niego)

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 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. o m ó w io n y k o d

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 z tabeli jest to, że należy zrozumieć cechy wstawianych kluczy przed zastosowaniem 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ż

utt«shens" i)p u t c s h e iif is h " , 2); Standardowe drzewo trie

Bez jednokierunkowych gałęzi

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.

Zastosowanie

Typowy klucz

Średnia długość (w)

Wielkość alfabetu (/?)

Liczba odnośników w drzewie trie zbudowanym na podstawie miliona kluczy

Kalifornijskie numery tablic rejestracyjnych

4PGC938

7

256

256 milionów

Num ery kont

024000199929932 99111

256 20

10

4 miliardy 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 opisujących genom

ACTGACTG

8

256

256 milionów 4 miliony

4

Pamięć potrzebna na typowe drzewa trie

758

Łańcuchy znaków

ROZDZIAŁ 5

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 z danego węzła. W R-kierunkowym 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.

Odnośnik do drzewa TST z wszystkimi kluczami rozpoczynającymi się od

Odnośnik do drzewa TST z wszystkimi kluczami rozpoczynającymi się od s

W yszukiw anie i w staw ianie Kod do wyszukiwania i wstawiania w imple­ mentacji interfejsu API tablicy symboli get("sea")

Niedopasowanie - należy wybrać lewy lub prawy odnośnik bez przechodzenia do następnego znaku

Dopasowanie - należy wybrać środkowy odnośnik i przejść do następnego znaku

(1 ) ©

00

T T

T T

Zwracanie wartości ( s ) i i ( T ) powiązanej z ostatnim / p /T znakiem klucza X

T

(e)

(¿ )? (1 ) /p

1[' 0.

T

Przykładowe przeszukiwanie drzewa TST

opartej na drzewach TST „sam się pisze”. Przy wyszukiwaniu należy porównać pierwszy znak klucza ze znakiem z korze­ 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, jeśli napotkano odnośnik nul 1 lub gdy wę­ zeł, w którym zakończono poszukiwania, ma wartość nuli. Jeżeli węzeł, w którym zakończono proces, ma wartość różną od 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

f prívate Node root; // Korzeń drzewa t r i e . prívate c la s s Node

{ char c; Node l e f t , mid, rig h t; V alue val ;

// Znak. // Lewe, środkowe i prawe poddrzewo t r i e . // 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, else i f (c> x.c) x . r ig h t = put(x. rig h t, key,val, else i f (d< key.length() 1) x.mid = put(x.mid, key, val, d+1); e l se x . val =val ; return x;

d) ; d) ;

___

} } 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

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ć niezbalansowany (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

761

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 256znakowego 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

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.

763

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).

Algorytm (struktura danych)

Drzewa BST Drzewa wyszukiwań 2-3 (czerwono-czarne drzewa BST)

Typowe tempo wzrostu dla N łańcuchów znaków o średniej długości w opartych na fl-znakowym alfabecie

Najlepszy dla

Znaki sprawdzane przy nieudanym wyszukiwaniu

Wykorzystywana pamięć

cl dg W

64N

Losowo uporządkowane klucze

c2 (lgN )2

64N

Gwarancje wydajności

Próbkowanie liniowe (tablice równoległe)

W

Od 32N do 128N

Typy wbudowane i przechowywanie skrótów w pamięci podręcznej

Przeszukiwanie drzew trie (R-kierunkowe drzewa trie)

lo g rN

Od (8R+56JN do (8R+56)Nw

Krótkie klucze i małe alfabety

Przeszukiwanie drzew trie (drzewa TST)

1,39 Ig N

Od 64N do 64Mv

Klucze nielosowe

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(), keysWithPrefix() 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 Mienta, 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

! 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ń.

769

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 Tekst — - I

E N

E A

D H

L

E

A

Y

S

T

A

C

K

N

E

E

D

L

Dopasowanie Wyszukiwanie podłańcuchów

770

E

I

N

A

5.3

n

Wyszukiwanie podiańcuchów

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.

771

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 ) { oraz j eden wskaźnik dla wzorca (j ). int M = p a t.le n g th (); 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 { wykrycia dopasowania lub końca in t j; wzorca (j == M). Dojście do końca f o r (j = 0; j < M; j + + ) 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 ; wzorca oznacza brak dopasowania i f (j == M) r e t u r n i ; // Z n a le z io n o . — 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ć o braku dopasowania. Wyszukiwanie podłańcucha metodą ataku siłowego 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

0

2

2

i+j

0

txt — - A

A

1

2

B

A

c

B

R

A

1

0

1

2 3

1 0

3 3

4

1

5

5

0

5

6X

4 10 Jeśli i ma wartość M, należy zwrócić i

A

/

3 4 A

5

6

7

8

9 10

D

A

B

R

A

Czerwona litera "" oznacza niedopasowanie

B

R

A

A

B

R

/

A J B

R

A

A

B

r

A

A

B

R

A

B

Czarne litery pasują do tekstu

C

■pat

Szare litery

/ podano w celach *

A

poglądowych

R A \ Dopasowanie

Wyszukiwanie podłańcucha metodą ataku siłowego

5.3

a

Wyszukiwanie podiańcuchów

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 jak wcześniej, przechowuje je3 4 7 A A A A B 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). ,. , , Dopóki wskaźniki rprowadzą-z do Wyszukiwanie podłancucnow metodą r 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 + 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

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. Druga 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}. L u b

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.

801

ROZDZIAŁ 5

o

Łańcuchy znaków

obiekty formalne, prostsze nawet 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.

w y r a ż e n ia

reg u la rn e

to

n ie z w y k l e

pro ste

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

803

Wyrażenia regularne

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 jednego znaku lub krótkiego ciągu do bezpośredniego opisania zbiorów zna­ ków. Znak kropki (.) to symbol wielo­ znaczny, reprezentujący dowolny poje­ dynczy znak. Ciąg znaków w nawiasach kwadratowych reprezentuje dowolny z tych znaków. Ciąg może też reprezen­ tować przedział znaków. Jeśli ciąg w na­ wiasach kwadratowych jest poprzedzo­ ny znakiem C reprezentuje dowolny znak oprócz znaków z ciągu. Te zapisy to proste sieroty ciągu operacji lub.

P rz y k ła d

Z a p is

N a zw a

Symbol wieloznaczny

A.B

Określony zbiór

U m ieszczony w []

[AEIOU] *

Przedział

Umieszczony w [], rozdzielony znakiem -

[A-Z] [0-9]

Dopełnienie

Umieszczony w [], poprzedzony znakiem *

[AAEI0U ]ł

D e s k ry p to ry z b io ró w z n a k ó w

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

Przynajmniej 1

Konkretna wartość Przedział

S k ró t d la

W ję z y k u

(AB)+

(AB)(AB)*

AB ABABAB

e BBBAAA

e AB

Dowolny inny łańcuch znaków

(AB)(AB)(AB)

ABABAB

Dowolny inny łańcuch znaków

(AB)|(AB)(AB)

ABABAB

Dowolny inny łańcuch znaków

(AB)?

0 lub 1 Wartość w {}

P o z a ję z y k ie m

P rz y k ła d

(AB){3}

Przedział w {} (AB){l-2}

el AB

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

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.

805

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 finitestate 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.

o Stan początkowy

Automat NFA dla wzorca

((A *B | A C )D )

5.4

Wyrażenia regularne

n

■ 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- 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-

809

810

ROZDZIAŁ 5

Łańcuchy znaków

a

3 4

0 12

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

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 7

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

2 3 4

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 :

10 1 1

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

: 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

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 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 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 ) 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);

|| re [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

811

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

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. te

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 , G .a d d E d g e f o r ,

o r+ 1 ); 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 if

Ip = i ; ( r e [ i ] == ' ( ' ops.push(i);

el se i f

II

re [i]

== ' | ' )

( 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 ]

== ' ( '

G.addEdge(i,

||

re [i]

==

||

re[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 ­ { plementacjach Unilcsa i był nieodłącznym S t r i n g regexp = + arg s[0 ] + 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

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.

817

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­ mujące: 5 . 4 .1 .

■ 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ć za pomocą wyrażeń regularnych o M operatorach lub, ale bez operatorów dom knię­ cia (dozwolone są nawiasy i złączenia)? 5 . 4 .3 .

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 po dopasowaniu każdego znaku i późniejsze e-przejścia dla danych wejściowych 5 . 4 .6 .

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

% j a v a GREPmatch " ( A | B ) ( C | 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. b. c. d.

Zawierającego przynajmniej trzy kolejne cyfry 1. Zawierającego podłańcuch 110. Zawierającego podłańcuch 1101100. Niezawierającego podłańcucha 110.

5.4

a

Wyrażenia regularne

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. b. c. d. e. f.

Obejmującego przynajmniej trzy znaki, przy czym trzecim znakiem musi być 0. Obejmującego liczbę cyfr 0 podzielną przez 3. Rozpoczynającego i kończącego się tym samym znakiem. O nieparzystej długości. Rozpoczynającego się cyfrą 0 i o nieparzystej długości lub rozpoczynającego się cyfrą 1 i o parzystej długości. 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. b. c.

5 .4.12 a. b. c. d. e.

0(0 | 1 ) * 1 0* 1 0 1 * (1 | 0 1 )*

Napisz wyrażenia regularne Javy dla poniższych zbiorów łańcuchów. Numerów telefonów w postaci (609) 555-1234. Numerów dowodów osobistych, na przykład AAA123321. Dat, takich jak 31 grudnia 1999. 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. Numerów tablic rejestracyjnych, rozpoczynających się od czterech cyfr, po których następują dwie duże litery.

819

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

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).

821

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. R ozpakow yw anie

K om presow anie

Wersjo skompresowano - C(B)

Strumień bitów 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

Odczyt 1 bitu danych i zwrócenie wartości typu boolean

boolean readBoolean()

Odczyt 8 bitów danych i zwrócenie wartości typu char

char readChar()

Odczyt r (między 1 a 16) bitów danych i zwrócenie wartości typu char

char re ad Char (int r)

Podobne metody dla typów byte (8 bitów), short (16 bitów), i nt (32 bity), 1ong i doubl e (64 bity) Czy strumień bitów jest pusty?

boolean isEmpty()

Zam yka strumień bitów

void c l o s e d

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



825

Kompresja danych

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

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 ) ; B in a ry S td O u t.w rite (y e a r);

1

/

Trzy w artości ty p u i n t (B in a ry S td O u t)

9

/

9

9

80 bitów

8-bitowa reprezentacja cyfry 9 w kodzie ASCII 32-bitowacalkowitoliczbowa reprezentacjo wartości 31

| 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 Dwie w artości ty p u c h a r i je d n a ty p u s h o r t (Bi n a ry S td O u t)

31

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 a r y S td O u t.w rit e ( (c h a r) day); B in aryStd O u t.w rite (C sh o rt) year); |o o o o iio o d o o iiii/o o o o o n ijiio o iiit|

12

31

1999

1999 Pole 4-bitow e, pole 5-bitow e i p o le 12-bitow e (Bi n a ry S td O u t)

96 bitów

B in ary S td O u t.w rite (m o n th , 4); B i n a r y S t d O u t . w r i t e ( d a y , 5 ); B in a r y s t d o u t . w r it e ( y e a r , 12); | i i o o n i i |i o i i i i i o |o i i l l o n (

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

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 ]); BinaryStdOut), prawdopodobnie i n t cnt; 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; BinaryStdln pozwala uniknąć tego i f (cnt != 0 && cnt % width == 0) 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"); przekształcania strum ieni bitów else S t d O u t. p rin t("0 "); 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. p ub lic c l a s s BinaryDump

{

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 ABRACADABRA! Strum ień bitów re p rezen to w an y za p o m o cą znaków 0 i 1

% ja va BinaryDump 16 < a b r a . t x t 0100000101000010 0101001001000001 0100001101000001 0100010001000001 0100001001010010 0100000100100001 L ic zb a bitów: 96

% ja v a HexDump 4 < a b r a . t x t 41 42 52 41 43 41 44 41 42 52 41 21 L ic z b a bitów: 96 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

% ja va PictureDump 16

l i Li

L iczb a bitów: 96

Cztery sposoby interpretowania strumienia bitów

6 < abra.txt Powiększone okno 16 na 6 pikseli

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­ P q r s t u V w X y z { i } ~ frę 1, kod 4A to litera J itd. Tabela doty­ T abela konwersji m iędzy kodow aniem czy 7-bitowego kodowania ASCII, dla­ 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świetlanym 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. Dowód. Omawiamy dwa dowody, które dotyczą tej samej m y­ ś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 wykorzystać ten algorytm do skompresowania jego danych wyjściowych i uzyskania jeszcze krótszego strumienia. Proces 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 kompresji danych kompresja 1000 -bitowego losowego strum ienia o połowę będzie możliwa w najwyżej 1 przypadku na 2500. Po natrafię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.

ł u I 1

1

ł u T 1

£ u n iw e rs a ln a k o m p re s ja d a n y ch ?

5.5

o

Kompresja danych

% 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 zapub lic s t a t i c void m a in ( S tr in g [] args) gadnień z teorii obliczeń, a ponadto pozwala zrozumieć wyzwania związane z kompresją danych. Przykładowo, można udowodnić, że problem optymalnej kompresji danych (znalezienia najkrótszego programu generującego dany łańcuch) jest nierozstrzygalny. Nie tylko nie istnieje algorytm kompresujący każdy strumień bitów, ale też nie można opracować Strategii tworzenia najlepszego algorytmu!

^

x = lllU for (int i = 0; i < 1000000; i++) i * . . j

int

B i n a r y S t d O u t .c lo s e O ;

^ „ S k o m p re s o w a n y " s tr u m ie ń m ilio n a b itó w

829

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

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() f do zrozumienia procesów związanych z ży­ 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 ( ) ; Bin a r y Std O u t .w r ite (N ); organizmów, a naukowcy piszą programy do f o r (i n t i = 0; i < N; i++ ) badania struktury tych sekwencji. {

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

// Za pis dwubitowego kodu znaku, 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%. Metoda expand (), przedstawiona na górze 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. R

o z p a k o w

y w

a n i e

d l a

k o d u

2 - b i t o w

e g o

831

832

ROZDZIAŁ 5

n Łańcuchy znaków

p ub lic s t a t i c void expand()

{ Alphabet DNA = new AlphabetC'ACTG"); i n t w = D N A .lg R (); int N = B in aryStd In .read Int(); f o r ( i n t i = 0; i < N; i++) { // Odczyt dwóch bitów i zapis znaku, char c = B in aryStd ln.rea d C ha r(w ); B i naryStdO ut.writ e (D N A .t o C h a r( c));

t o s a m o p o d e j ś c i e sprawdza się też dla in­ nych alfabetów o stałym rozmiarze, jednak opracowanie ogólnej wersji pozostawiamy jako łatwe ćwiczenie (zobacz ć w i c z e n i e

5-5-25)Przedstawione m etody nie są w pełni zgodne ze standardowym modelem kom ­ presji danych, ponieważ skompresowany } Bin aryStdO ut.closeO ; strum ień bitów nie obejmuje wszystkich in­ formacji potrzebnych do jego odkodowania. To, że alfabet składa się z liter A, C, T i G, jest 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 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 () p ub lic c l a s s Genome zapisano jako m etody statyczne { 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­ ki ich wykonania), gdzie metody 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.

i f ( a r g s [ 0 ] . e q u a l s ( " - " ) ) compressO; i f ( a r g s [ 0 ] , e q u a l s ( " + " ) ) expand();

}

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

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 ? ? - genomeTiny.2bit % ja va Genome + < genomeTiny.2bit ATAGATGCATAGCGCATAGCTAGATGTGCTAGC ^ % java Genome - < genomeTiny.txt | java Genoine_+ ATAGATGCATAGCGCATAGCTAGATGTGCTAGC -t--------------

Cykl kompresji i rozpakowywana

d4 ep¡erw0tt,edímewejic¡owe

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

L i c z b a b it ó w :

| j a v a P ic tu re D u m p 512 25

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

833

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

Koncentrujemy się na zrzucie binarnym dla zrzutu ekranu o wymiarach 32 na 48 pikseli. Po prawej stronie pokazano zrzut binarny wraz z długościami serii w każ­ dym wierszu. Ponieważ każdy wiersz za­ czyna i kończy się cyframi 0, wszystkie wiersze obejmują nieparzystą liczbę serii. Ponieważ koniec każdego wiersza jest kontynuowany w następnym, długości serii w strum ieniu bitów są sumą dłu­ gości ostatniej serii z każdego wiersza i pierwszej serii z następnego (oraz dłu­ gości odpowiednich wierszy składających się z samych cyfr 0).



Kompresja danych

835

7 cyfr 1 % java BinaryDump 32 < q32x48.bin OOOOOOOOOOOOOOOOOOOOOOOOOOO-O-ffOOO

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 00000000000011111111111111100000 00000000001111000011111111100000 00000000111100000000011111100000 00000001110000000000001111100000 00000011110000000000001111100000 00000111100000000000001111100000 00001111000000000000001111100000 00001111000000000000001111100000 00011110000000000000001111100000 00011110000000000000001111100000 00111110000000000000001111100000 00111110000000000000001111100000 00111110000000000000001111100000 00111110000000000000001111100000 00111110000000000000001111100000 00111110000000000000001111100000 00111110000000000000001111100000 00111110000000000000001111100000 00111111000000000000001111100000 00111111000000000000001111100000 00011111100000000000001111100000 00011111100000000000001111100000 00001111110000000000001111100000 00001111111000000000001111100000 00000111111100000000001111100000 00000011111111000000011111100000 00000001111111111111111111100000 00000000011111111111001111100000 00000000000011111000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000001111100000 00000000000000000000011111110000 00000000000000000011111111111100 00000000000000000U .il11111111110

15 12 10 10 8 77 66 55 44 44 33 22 22 22 22 22 22 22 22 22 22 22 33 33 44 44 55 66 7 9 22 22 22 22 22 22 22 22 22 22 22 22 21 18 17 32 32

7 10 15 5 44 4 99 44 9 9 6 6 5 12 55 33 12 12 55 44 12 13 55 44 13 14 55 44 14 14 55 44 14 15 55 44 15 15 55 55 15 15 55 55 15 15 55 55 15 15 55 55 15 15 55 55 15 15 55 55 15 15 55 55 15 15 55 55 15 15 55 55 15 14 55 66 14 14 55 66 14 13 55 66 13 13 55 66 13 12 55 66 12 11 55 77 11 10 55 77 10 88 7 66 20 5 11 2 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 7 4 12 2 14 1

Im plem entacja Przedstawiony wcześ­ niej nieformalny opis bezpośrednio prowadzi do implementacji m etod com­ press () i expand() zaprezentowanych na następnej stronie. Kod m etody expand () jest, jak zwykle, prostszy — wczytuje dłu­ gość serii, zapisuje odpowiednią liczbę kopii bieżącego bitu, uzupełnia bieżący bajt i kontynuuje proces do czasu wy­ czerpania danych wejściowych. Metoda oooooooooooo ooooootmoooooooooooo 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 " ^ 7 7 cyfrO plikowana. Dopóki w strum ieniu danych Liczba bitów: 1536 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:

5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5

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 ; techniki podwojenie rozdzielczości powoduje while ( I B i n a r y S t d ln . i s E m p t y O ) 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 ) ; Przykładowo, dla litery q o niskiej rozdzielczo­ b = !b; ś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; stępnej stronie. Litera o wyższej rozdzielczości boolean b, old = f a l s e ; zajmuje czterokrotnie więcej miejsca niż lite­ while ( I B i n a r y S t d ln . i s E m p t y O ) { 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); cnt = 0; B in a r y S td O u t .w r ite (c n t);

cnt++;

} B i n a r y S td O u t .w r ite (c n t); B i n a r y S t d O u t . c lo s e ( ) ;

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 p rz y k o d o w a n iu d łu g o ś c i serii

KO D O W A N IE

DŁU G O ŚCI

SE R II

JEST

BARDZO

s k u t e c z n e w wielu sytuacjach, j ednak w bardzo licznych przypadkach strum ienie bitów przezna­ czone do kompresji (na przykład typowy tekst w języku polskim) mogą w ogóle nie obejmować długich serii. Dalej omawiamy dwie m etody skuteczne dla różnorodnych plików. Techniki te są powszechnie stosowane i prawdopodobnie korzystałeś z jednej lub z obu tych m etod przy 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 05 01 01 01 03 01 03 01 05 01 01 01 04 01 02 01 01 02 01 04 01 , ^

416


q 3 2 x48 .b in .rle HexDump 16 < q 32x48.b in.r í e 16 0 f Of 04 04 09 Od 04 09 06 Oc 03 Oc 05 Oc 05 Oa 04 Od 05 09 04 Oe 05 09 04 Oe 05 Of 05 08 04 Of 05 07 05 Of 05 07 05 Of 05 Of 05 07 05 Of 05 07 05 Of 05 07 05 Of 05 Of 05 07 05 Of 05 07 06 Oe 05 07 06 Oe 05 Od 05 08 06 Od 05 09 06 Oc 05 09 07 Ob 05 Oa 05 Ob 08 07 06 Oc 14 Oe Ob 02 05 11 05 lb 05 Ib 05 lb 05 lb 05 Ib 05 lb 05 lb 05 lb 05 lb 05 lb 05 la 07 16 Oc 13 Oe 41 W sp ó łc z y n n ik k o m p resji bitów: 1144 - 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 SP

101010 01

a b

11011 101011

e

000

f h

11000 11001

i m

1011 11010

o r

0011

s

10100 100

t w

0010

111

z korzen ia to 1 1 0 1 0 , d la te go

11010 to k o d d la 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

„m”

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

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.

845

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 odkodować bez drzewa trie. Dlatego Liście oprócz kosztów zapisu sa­ i 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) { / / Za pi s drzewa t r i e zakodowanego j a k o ł a ńc uc h bi t ów, itd. Powiązana metoda i f (x.isLeaff)) readTri e () ze strony 847 ( odtwarza drzewo trie BinaryStdOut.write(true); BinaryStdOut.write(x.ch); na podstawie łańcucha return; bitów. Metoda wczytuje 1 jeden bit, aby ustalić ro­ BinaryStd0ut.writ e ( f a l s e ) ; w riteTrie(x.left); dzaj następnego węzła. 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

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ść).

847

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 ; fre q [in p u t[i]]+ + ;

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 w rite T rie (ro o t);

na p o t r z e b y odkodowywania ( r e k u r e n c y j n i e ) .

// 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

Przypadek testowy (96 bitów) % more a b r a . t x t abracadabra ! | 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

% j a v a Huffman - < a b r a . t x t

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 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

% j a v a Huffman - < t i n y t i n y T a l e . t x t

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

L ic z b a b it ó w : 23912 -*

| j a v a PictureDump 512 47

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

849

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

«a*,™«:

| j a v a PictureDump 512 25

--- ----

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

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.

851

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 wejściowe

B

R

A

C

A

D

A

A

B

R

A

c

A

D

A B

R A

B R

A B R

42

52

41

43

41

44

81

83

82

88

AB BR RA AC CA AD DA ABR RAB 89

AB BR RA AC CA AD DA ABR RAB B RA 8A

AB BR RA AC CA AD DA ABR RAB BRA ABRA 8B

Dane 41 wyjściowe

B

R

A

B

R

A

B

R

Koniec pliku

A

A

I

41

80

Tablica słów kodowych Klucz

AB 8 1 AB AB BR 82 BR

f

Wejściowy podłańcuch Słowo kodowe w metodzie LZW

AB BR RA A C 84

AB 13 R RA AC CA 85

/ Znak następny

AB BR RA AC CA AD 86

AB BR RA AC CA AD DA 87

A [3 13 R RA AC CA AD DA ABR 88

AB BR RA AC CA AD DA ABR RAB B RA ABRA

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ą doStOSOw 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

Wartość

81 82 83 84 85

86 87 88 89 8A 8B

5.5



853

Kompresja danych

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

D a n e wyjściowe

A

B

R

A

C

A

D

81 AB

AB 82 B R

AB BR 83 R A

AB BR RA 84 A C

AB BR RA AC 85 C A

AB BR RA AC CA 86 A D

AB BR RA AC CA AD 87 D A

Słow o kodow e ^ z m etody L Z W

81 A B

AB BR RA AC CA AD DA .88 A B R

/ Wejściowy podlańcuch

83 R A

AB BR RA AC CA AD DA AB R 89 R A B

82

88

B R

41

AB BR RA AC CA AD DA AB R RAB

AB BR RA AC CA AD DA AB R RA B B RA

8A B R A 8B

80

A

A B R

O d w ró c o n a tablica s ł ó w k o d ow yc h Klucz Wartość 81 AB

ABRA

Rozpakowywanie w metodzie LZW dla kodów 41 42 52 41 43 41 44 81 83 82 88 41 80

82

BR

83

RA

84

AC

85

CA

86

AD

87

DA

88

ABR

89

RAB

8A

BRA

8B

ABRA

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 p riva te s t a t ic p rivate s t a t ic

final i n t final i n t final i n t

R = 256; L = 409 6 ; W = 12;

// L i c z b a znaków w e jś c io w y c h , // L i c z b a stów kodowych = 2^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); B in a ry Std O u t.c lo se ();

// Z a p i s końca p l i k u .

} 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 A B A B A B go może spowodować przejście o je­ Dopasow anie A B A B A B A 83 42 81 den znak za daleko. W przykładzie D a n e wyjściowe 4 1 Tablica s ł ó w ko d ow yc h wejściowy łańcuch znaków: Klucz Wartość A B 81

ABABABA

AB B A 82

AB BA ABA

jest kompresowany do pięciu wyj­ ściowych słów kodowych: 41 42 81 83 80

Rozpakowywanie D a n e wejściowe 41 D a n e wyjściowe A 81 A B

42 B AB

81 A B

AB 83

83 ?

80

81

BA

82

ABA

83

80 _______ M u si być równe (zobacz poniżej)

AB

82 B A B 1 D o uzupełnienia elementu Pokazano to w górnej części rysun­ ? ^ p o t r z e b n y jest znak następny AB. ku. Aby rozpakować dane, należy 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); // 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]; // // // i f (i == codeword) // // s = val + v a l . c h a r A t ( O ) ; // i f (i < L) s t [ i + + ] = va l + s . c h a r A t ( O ) ; // // // v a l = s; //

Z a p i s b ie ż ą c e g o p o d ła ń c u c h a .

P o b i e r a n i e n a st ę p n e g o s ło w a kodowego. J e ś l i znak n a s t ę p n y j e s t niepraw idłow y, 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 . Dodawanie nowego elementu do t a b l i c y kodów. 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

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­ ł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. jak z w y k l e

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 L ic zba b itó w : 3043928

%

j a v a LZW - < t a l e . t x t L ic z b a b it ó w : 2667952

%

| j a v a BinaryDump 0

| j a v a BinaryDump 0 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

857

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

859

Kompresja danych

O

ĆWICZENIA

5.5.1. Rozważmy cztery kody o zmiennej dłu­ gości przedstawione w tabeli po prawej stronie. Które z tych kodów są bezprefiksowe? Które można jednoznacznie odkodować? Dla tych ostatnich odkoduj łańcuch 1000000000000. 5.5.2. Podaj przykład kodu, który umożliwia jednoznaczne odkodowywanie, a który nie jest bezprefiksowy.

Sym bol

Kod 1

Kod 2

Kod 3

Kod 4

A

0

0

1

1

B

100

1

01

01

C

10

00

001

001

D

11

11

0001

000

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 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

W

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. 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. Z a s t o s o w

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. 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. I n ż y n i e r i a

KONTEKST

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 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ć.

a lg o r y t m y o dg ryw ają w a żn ą ro lę

867

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 poszczególnych atomów i cząsteczek). Maxwell i Boltzmann A wykorzystali ten model do wyprowadzenia rozkładu pręd­ © # kości cząsteczek wchodzących w interakcje jako funkcji temperatury. Einstein na podstawie tego modelu wyjaśnił Przesunięcie czasu do f + 2dt ruchy Browna pyłków kwiatowych zanurzonych w wodzie. 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.



• ¿1

Symulacja sterowana czasem

Sym ulacje sterowane czasem Podstawowym celem jest utrzymanie modelu. Oznacza to, że chcemy śledzić pozycje 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 obliczenia są zbyt częste samej pozycji, a następnie cofnąć się do m om entu pierwszego 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­ Wartość dt jest zbyt duża - może niowej. Jeśli czas dt jest mierzony w sekundach (zwykle są to 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 — bardzo małe. Problem polega na tym, że jeśli dt jest zbyt małe, koszt obliczeń jest wysoki, a zprzyi zbyt dużym dt może Podstawowy Problem z symulacjami ' < i i sterowanymi czasem nastąpić pominięcie zderzenia.

•i

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ą = odległość/prędkość = (1 - s - r ) / v

Ś c ia n a

' przy

(r,,r.)

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 do niego drugiej

Efekt (w czasie t + dt) Po zderzeniu prędkości obu cząsteczek zmieniają się

Prognoza i efekt zderzenia dwóch cząsteczek

Symulacja sterowana zdarzeniami

871

Poruszanie się cząsteczki Unieważnione zdarzenia Z uwagi na wcześniejsze zderzenia 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 w kolejce priorytetowej i ignorujemy je, kiedy nadejdzie się od ściany 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ń).

\

Cząsteczki oddalające się od siebie

Jedna cząsteczka dociera do punktu zderzenia przed drugą

I

4

Zderzenie zanadto oddalone w czasie

Można przewidzieć, że te zdarzenia nie zajdą

jest podstawą do kompletnej sterowanej 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 dotyczące cząsteczek, typ danych E v e n t dla prognozowa­ nych zdarzeń i wykonującego sy­ Dwie cząsteczki na torze kolizyjnym mulacje klienta C o l i i s i o n S y s t e m . Istotą symulacji jest typ Mi nPQ, któ­ ry obejmuje uporządkowane w cza­ sie zdarzenia. Dalej omawiamy im ­ plementacje klas P a r t i c l e , Event t o o m ó w ie n ie

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( d o ubl e do u bl e do u bl e do u bl e

Tworzy cząsteczkę o danych cechach: pozycji, prędkości, prom ieniu, m asie

r x , do u b l e r y , vx, do u b l e vy, s, mass)

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

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

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 , 1

P a r t i c l e a , P a r t i c l e b)

/ / 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

873

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.

private

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 potenvoi 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++) {

/ / 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 ] doubl e d t = a . t i m e T o H i t ( p a r t i c l e s [ i ] ) ; i f ( t + d t x, c2 > 1 - x 2

c2> x 3 c , < x , + (1 - X 2) + X 3

c2 > l - x ,

c3> 1 - x 2 c3> l - x 3 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­ wania liniowego z liczbami całkowitymi.

c