136 61 31MB
Polish Pages [949] Year 2012
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
są
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
są
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 przeszuku