(dziel i rządź, programowanie dynamiczne i algorytmy zachłanne).

Transkrypt

(dziel i rządź, programowanie dynamiczne i algorytmy zachłanne).
[12] Metody projektowania algorytmów (dziel i rządź, programowanie
dynamiczne i algorytmy zachłanne).
Tworzenie projektów informatycznych opiera się w dużej mierze na
formułowaniu i implementacji algorytmów, które mają za zadanie właściwe
przetworzenie danych i rozwiązanie postawionych przed nami problemów.
Algorytmy można sklasyfikować na kilka różnych sposobów,ale wśród nich
najważniejszy jest podział ze względu na techniki ich konstruowania. Są pewne
techniki tworzenia algorytmów, których zastosowanie prowadzi do efektywniejszego
rozwiązywania problemów niż za pomocą algorytmów konstruowanych w sposób
„spontaniczny”.
Przedstawimy kilka z nich:
•
•
•
•
•
Dziel i rządź
prowadzi nierzadko do bardzo efektywnych rozwiązań
polega na rekurencyjnym dzieleniu problemu na dwa mniejsze podproblemy
dzielenie ma miejsce tak długo aż podproblemy stają się proste do
bezpośredniego rozwiązania
zwykle podproblemy są „mniejszymi kopiami” podproblemu z którego
powstały
oficjalnie pierwszy raz zastosowano tę metodą w 1960 roku
Algorytm sprawdza czy w podanej, posortowanej tablicy znajduje się element o danej
wartości.
• tablica jest dzielona na coraz mniejsze elementy (na pół)
• jako potencjalny element do wyszukania typuje się element środkowy
• w zależności od wartości elementu środkowego, kontynuuje się
przeszukiwanie odpowiedniej części tablicy (zawężenie przedziału)
• podział kończy się gdy znajdziemy szukany element lub gdy przedział osiągnie
długość 0
Dla tablicy 1.000.000 elementów nie musimy sprawdzać każdego z nich - wystarczy
tylko 20 kroków.
Programowanie dynamiczne
• stosowane głównie do rozwiązywanie problemów optymalizacyjnych
• alternatywa dla pewnych zagadnień rozwiązywanych metodami zachłannymi
W odniesieniu do programowania opartego o „dziel i zwyciężaj”:
• Jeżeli podproblemy, na które został podzielony problem główny, nie są
niezależne to w różnych podproblemach wykonywane są wiele razy te same
obliczenia, warto jest wtedy zastosować ulepszenie tej metody jakim jest
zastosowanie programowania dynamicznego
Programowanie dynamiczne zasady
• wyniki poszczególnych obliczeń są zapamiętywane w pomocniczej tablicy
• tablica ta jest wykorzystywana w kolejnych krokach
• eliminuje to konieczność wielokrotnego powtarzania tych samych obliczeń
Dla każdego podproblemu obliczenia są zatem wykonane tylko raz, a ich wynik jest
zapamiętywany
Programowanie dynamiczne zastosowanie
• automatach do kawy przy wydawaniu reszty w taki sposób, by monet było jak
najmniej.
• algorytm Floyda-Warshalla (najkrótsze ścieżki między wszystkimi
wierzchołkami w grafie)
•
•
•
•
Algorytmy zachłanne
algorytm w każdym kroku dokonuje wyboru będącego na daną chwilę tym
najlepszym (najbliższym końcowemu rozwiązaniu)
podejmuje decyzje optymalne tylko lokalnie
kontynuuje działania wynikające z poprzednich decyzji
podejście często okazuje się nieoptymalne
Algorytmy zachłanne przykład
• Algorytm Kruskala (MST)
• Algorytm Dijkstry (najkrótsza ścieżka w grafie o nieujemnych wagach)
[13] Elementarne i nieelementarne metody sortowania.
Przyjęło się mówić że elementarne metody sortowania to te których czas
działania jest równy O(n2).
Zaliczają się do nich następujące algorytmy sortowania :
1.1. Sortowanie przez selekcję(selection sort)
Jest nieadaptacyjne, wewnętrzne i stabilne oraz nie wymaga dodatkowej pamięci.
Jego czas działania jest określony z góry O(n2). Sortowanie to jest najlepsze, spośród
innych elementarnych, do sortowania elementów o małych kluczach i dużych polach,
ponieważ wykonuje najmniej wstawień.
W pierwszym przebiegu algorytm znajduje najmniejszy element w tablicy i zamienia
go z pierwszym. W drugim przebiegu algorytm znajduje najmniejszy element w
podtablicy [2..r] i zamienia go z drugim. I tak aż do zamiany r-tego elementu z r-1
elementem.
1.2. Przez wstawianie Sortowanie przez wstawienie (insertion sort)
to algorytm, którego czas działania wynosi O(n2). Jest on skuteczny dla małej ilości
danych. Jest to jeden z prostszych i jeden z bardziej znanych algorytmów sortowania.
Jest on stabilny i nie wymaga dodatkowej pamięci (działa w miejscu). Najważniejszą
operacją w opisywanym algorytmie sortowania jest wstawianie wybranego elementu
na listę uporządkowaną. Zasady są następujące:
1) Na początku sortowania lista uporządkowana zawiera tylko jeden, ostatni element
zbioru. Jednoelementowa lista jest zawsze uporządkowana.
2) Ze zbioru zawsze wybieramy element leżący tuż przed listą uporządkowaną.
Element ten zapamiętujemy w zewnętrznej zmiennej. Miejsce, które zajmował,
możemy potraktować jak puste.
3) Wybrany element porównujemy z kolejnymi elementami listy uporządkowanej.
4) Jeśli natrafimy na koniec listy, element wybrany wstawiamy na puste miejsce lista rozrasta się o nowy element.
5) Jeśli element listy jest większy od wybranego, to element wybrany wstawiamy na
puste miejsce - lista rozrasta się o nowy element. 6) Jeśli element listy nie jest
większy od wybranego, to element listy przesuwamy na puste miejsce. Dzięki tej
operacji puste miejsce wędruje na liście przed kolejny element. Kontynuujemy
porównywanie, aż wystąpi sytuacja z punktu 4 lub 5.
1.3. Sortowanie bąbelkowe
Jest to algorytm nieadaptacyjny, wewnętrzny i stabilny oraz nie wymagający
dodatkowej pamięci. Jego czas działania jest określony z góry przez wynosi O(n2)–
algorytm wykonuje w najgorszym i średnim przypadku około porównao i
zamian. Zasada działania opiera się na cyklicznym porównywaniu par sąsiadujących
elementów i zamianie ich kolejności w przypadku niespełnienia kryterium
porządkowego zbioru. Operację tę wykonujemy dotąd, aż cały zbiór zostanie
posortowany.
Lista kroków
1: Dla j = 1,2,...,n - 1: wykonuj Krok 2
2: Dla i = 1,2,...,n - 1: jeśli d[i] > d[i + 1], to d[i] ↔ d[i + 1]
3: Zakończ gdzie: n- liczba elementów w sortowanym zbiorze d[ ]- zbiór nelementowy, który będzie sortowany. Elementy zbioru mają indeksy od 1 do n.
Do „nieelementarnych” algorytmów sortowania zaliczamy :
1.4. Sortowanie szybkie(QuickSort)
Algorytm sortowania szybkiego opiera się na strategii "dziel i zwyciężaj" (ang.
divide and conquer), którą możemy krótko scharakteryzować w trzech punktach:
 DZIEL - problem główny zostaje podzielony na podproblemy
 ZWYCIĘŻAJ - znajdujemy rozwiązanie podproblemów
 POŁĄCZ - rozwiązania podproblemów zostają połączone w rozwiązanie problemu
głównego
Idea sortowania szybkiego jest następująca:
 DZIEL: najpierw sortowany zbiór dzielimy na dwie części w taki sposób, aby
wszystkie elementy leżące w pierwszej części (zwanej lewą partycją) były mniejsze
lub równe od wszystkich elementów drugiej części zbioru (zwanej prawą partycją).
 ZWYCIĘŻAJ : każdą z partycji sortujemy rekurencyjnie tym samym algorytmem.
 POŁĄCZ : połączenie tych dwóch partycji w jeden zbiór daje w wyniku zbiór
posortowany.
W przypadku typowym algorytm ten jest najszybszym algorytmem sortującym
z klasy złożoności obliczeniowej O(n log n) - stąd pochodzi jego popularność w
zastosowaniach. Musimy jednak pamiętać, iż w pewnych sytuacjach (zależnych od
sposobu wyboru piwotu oraz niekorzystnego ułożenia danych wejściowych) klasa
złożoności obliczeniowej tego algorytmu może się degradować do O(n2), co więcej,
poziom wywołao rekurencyjnych może spowodować przepełnienie stosu i
zablokowanie komputera. Z tych powodów algorytmu sortowania szybkiego nie
można stosować bezmyślnie w każdej sytuacji tylko dlatego, iż jest uważany za jeden
z najszybszych algorytmów sortujących - zawsze należy przeprowadzić analizę
możliwych danych wejściowych właśnie pod kątem przypadku niekorzystnego.
1.5. Sortowanie przez łączenie(scalanie)
Jest nieadaptacyjne, zewnętrzne i stabilne oraz wymaga dodatkowej pamięci
proporcjonalnej do n. Jego czas działania jest określony z góry przez O(n log(n)) .
Sortowanie przez scalanie jest nieadaptacyjne, ale jest za to relatywnie szybkie
niezależnie od układu danych. W związku z tym algorytm ten stosuje się, gdy
jednocześnie ważna jest szybkość algorytmu i nie akceptowana jest wydajność
najgorszego przypadku innych sortowao, a do tego możemy jeszcze pozwolić sobie
na poświęcenie trochę pamięci na operację sortownia. Ideą działania algorytmu jest
dzielenie zbioru danych na mniejsze zbiory, aż do uzyskania n zbiorów
jednoelementowych, które same z siebie są posortowane , następnie zbiory te są
łączone w coraz większe zbiory posortowane, aż do uzyskania jednego,
posortowanego zbioru nelementowego. Etap dzielenia nie jest skomplikowany,
dzielenie następuje bez sprawdzania jakichkolwiek warunków. Dzięki temu, w
przeciwieostwie do algorytmu „sortowania szybkiego”, następuje pełne rozwinięcie
wszystkich gałęzi drzewa. Z kolei łączenie zbiorów posortowanych wymaga
odpowiedniego wybierania poszczególnych elementów z łączonych zbiorów z
uwzględnieniem faktu, że wielkość zbioru nie musi być równa (parzysta i nieparzysta
ilość elementów), oraz tego, iż wybieranie elementów z poszczególnych zbiorów nie
musi następować naprzemiennie, przez co jeden zbiór może osiągać swój koniec
wcześniej niż drugi. Robi sie to w następujący sposób. Kopiujemy zawartość zbioru
głównego do struktury pomocniczej. Następnie, operując wyłącznie na kopii,
ustawiamy wskaźniki na początki kolejnych zbiorów i porównujemy wskazywane
wartości. Mniejszą wartość wpisujemy do zbioru głównego i przesuwamy
odpowiedni wskaźnik o 1 i czynności powtarzamy, aż do momentu, gdy jeden ze
wskaźników osiągnie koniec zbioru. Wówczas mamy do rozpatrzenia dwa
przypadki, gdy zbiór 1 osiągnął koniec i gdy zbiór 2 osiągnął koniec. W przypadku
pierwszym nie będzie problemu, elementy w zbiorze głównym są już posortowane i
ułożone na właściwych miejscach. W przypadku drugim trzeba skopiować pozostałe
elementy zbioru pierwszego po kolei na koniec. Po zakooczeniu wszystkich operacji
otrzymujemy posortowany zbiór główny.
1.6. Sortowanie pozycyjne
Algorytm sortowania porządkujący stabilnie ciągi wartości (liczb, słów) względem
konkretnych cyfr, znaków itp, kolejno od najmniej znaczących do najbardziej
znaczących pozycji. Złożoność obliczeniowa jest równa O(d(n + k)), gdzie k to liczba
różnych cyfr, a d liczba cyfr w kluczach. Wymaga O(n + k) dodatkowej pamięci.
Pozycją (ang. radix) nazywamy miejsce cyfry w zapisie liczby.. Algorytm sortujący
musi być stabilny, tzn. nie może zmieniać kolejności elementów równych, w
przeciwnym razie efekty poprzednich sortowao zostaną utracone.
Sortowanie pozycyjne możemy także zastosować do sortowania rekordów baz
danych. Na przykład chcemy posortować książkę telefoniczną według nazwisk, a w
razie gdyby się one powtarzały to według imion, a w przypadku identyczności imion
i nazwisk według numeru telefonu. Aby otrzymać taki wynik powinniśmy tą książkę
telefoniczną posortować najpierw według numeru telefonu, potem według imion, a
na koocu według nazwisk. Złożoność obliczeniowa takiego sortowania pozycyjnego
na pewno nie będzie O(n). Wynika to z tego, że do posortowania np. nazwisk trudno
jest użyć sortowania przez zliczanie.
[14] Elementarne metody wyszukiwania. Haszowanie.
1.1. Wyszukiwanie liniowe/sekwencyjne
Wyszukiwanie liniowe (ang. linear search), zwane również sekwencyjnym
(ang. sequential search) polega na przeglądaniu kolejnych elementów zbioru Z. Jeśli
przeglądany element posiada odpowiednie własności (np. jest liczbą o poszukiwanej
wartości), to zwracamy jego pozycję w zbiorze i kooczymy. W przeciwnym razie
kontynuujemy poszukiwania aż do przejrzenia wszystkich pozostałych elementów
zbioru Z. W przypadku pesymistycznym, gdy poszukiwanego elementu nie ma w
zbiorze lub też znajduje się on na samym koocu zbioru, algorytm musi wykonać
przynajmniej n obiegów pętli sprawdzającej poszczególne elementy. Wynika z tego,
iż pesymistyczna klasa złożoności obliczeniowej jest równa O(n), czyli jest liniowa stąd pochodzi nazwa metody wyszukującej. Często chcemy znaleźć wszystkie
wystąpienia w zbiorze poszukiwanej wartości elementu. W takim przypadku
algorytm na wejściu powinien otrzymywać dodatkowo pozycję (indeks) elementu, od
którego ma rozpocząd wyszukiwanie. Pozycję tę przy kolejnym przeszukiwaniu
podajemy zawsze o 1 większą od ostatnio znalezionej. Dzięki temu nowe
poszukiwanie rozpocznie się tuż za poprzednio znalezionym elementem. Schemat
algorytmu:
n - liczba elementów w tablicy Z* +, n ∈N Z[ ]- tablica zawierająca elementy do
przeszukania. Indeksy elementów rozpoczynają się od 0, a kooczą na n-1 p - indeks
pierwszego elementu Z* +, od którego rozpoczniemy poszukiwania. p ∈C k poszukiwana wartość, czyli tzw. klucz, wg którego wyszukujemy elementy w Z* +
01: Dla i = p,p+1,...,n-1: wykonuj krok 2 ; przeglądamy kolejne elementy w zbiorze
02: Jeśli Z[i] = k, to zakoocz zwracając i ; jeśli napotkamy poszukiwany element,
zwracamy jego pozycję
03: Zakończ zwracając -1 ; jeśli elementu nie ma w tablicy, zwracamy -1
1.2. Wyszukiwanie binarne
Wyszukiwanie binarne jest algorytmem opierającym się na metodzie dziel i
zwyciężaj, który w czasie logarytmicznym stwierdza, czy szukany element znajduje
się w uporządkowanej tablicy i jeśli się znajduje, podaje jego indeks. Np. jeśli tablica
zawiera milion elementów, wyszukiwanie binarne musi sprawdzić maksymalnie 20
elementów () w celu znalezienia żądanej wartości. Dla porównania wyszukiwanie
liniowe wymaga w najgorszym przypadku przeglądnięcia wszystkich elementów
tablicy. Zasada działania : Jeśli zbiór jest pusty, to kooczymy algorytm z wynikiem
negatywnym. W przeciwnym razie wyznaczamy element leżący w środku zbioru.
Porównujemy poszukiwany element z elementem środkowym. Jeśli są sobie równe,
to zadanie wyszukania elementu jest wypełnione i kooczymy algorytm. W
przeciwnym razie element środkowy dzieli zbiór na dwie partycje - lewą z
elementami mniejszymi od środkowego oraz prawą z elementami większymi. Jeśli
porównywany element jest mniejszy od środkowego elementu zbioru, to za nowy
zbiór poszukiwao przyjmujemy lewą partycję. W przeciwnym razie za nowy zbiór
przyjmujemy prawą partycję. W obu przypadkach rozpoczynamy poszukiwania od
początku, ale w nowo wyznaczonym zbiorze.
1.3. Wyszukiwanie max lub min
Zadanie znajdowania elementu maksymalnego lub minimalnego jest typowym
zadaniem wyszukiwania, które rozwiązujemy przy pomocy algorytmu wyszukiwania
liniowego. Za tymczasowy maksymalny (minimalny) element przyjmujemy pierwszy
element zbioru. Następnie element tymczasowy porównujemy z kolejnymi
elementami. Jeśli któryś z porównywanych elementów jest większy (mniejszy) od
elementu tymczasowego, to za nowy tymczasowy element maksymalny (minimalny)
przyjmujemy porównywany element zbioru. Gdy cały zbiór zostanie przeglądnięty, w
elemencie tymczasowym otrzymamy element maksymalny (minimalny) w zbiorze.
Poniżej podajemy algorytm wyszukiwania max. Wyszukiwanie min wykonuje się
identycznie, zmianie ulega tylko warunek porównujący element tymczasowy z
elementem zbioru
Schemat algorytmu:
n - liczba elementów w tablicy Z* +, n ∈N Z[ ] - tablica zawierająca elementy do
zliczania. Indeksy elementów rozpoczynają się od 0, a kooczą na n - 1.. maxZ tymczasowy element maksymalny
1: maxZ ← Z[0] ; za tymczasowy element maksymalny bierzemy pierwszy element
2: Dla i = 1,2,...,n-1 wykonuj K03 ; przeglądamy następne elementy zbioru
3: Jeśli Z[i] > maxZ, to maxZ ← Z[i] ; jeśli natrafimy na większy od maxZ, to
zapamiętujemy go w maxZ 4: Zakoocz zwracając maxZ
2.4. Naiwne wyszukiwanie wzorca w tekście
Algorytm N - naiwny - ustawia okno o długości wzorca p na pierwszej pozycji
w łańcuchu s. Następnie sprawdza, czy zawartość tego okna jest równa wzorcowi p.
Jeśli tak, pozycja okna jest zwracana jako wynik, po czym okno przesuwa się o jedną
pozycję w prawo i cała procedura powtarza się. Algorytm kończymy, gdy okno
wyjdzie poza koniec łańcucha. Klasa pesymistycznej złożoności obliczeniowej
algorytmu N jest równa O(n × m), gdzie n oznacza liczbę znaków tekstu, a m liczbę
znaków wzorca. Jednakże w typowych warunkach algorytm pracuje w czasie O(n),
ponieważ zwykle wystarczy porównanie kilku początkowych znaków okna z
wzorcem, aby stwierdzić, iż są one niezgodne.
2.5. Alogorytm Karpa-Rabina
Danemu wzorcu możemy przyporządkować odpowiadającą mu wartość
dziesiętną - klucz. Dla danego tekstu obliczamy wartości dziesiętne kolejnych
podsłów długości wzorca zaczynając od początku tekstu – uzyskujemy różne klucze.
Teraz wystarczy porównać wartość dziesiętną odpowiadającą wzorcu z wartościami
dziesiętnymi odpowiadającymi kolejnym podsłowom czyli sprawdzamy czy klucze
są identyczne. Jeżeli są one równe możemy podejrzewać, że wzorzec występuje w
tekście.
2.6 Haszowanie
Haszowanie jest to pewna technika rozwiązywania ogólnego problemu
słownika. Przez problem słownika rozumiemy tutaj takie zorganizowanie struktur
danych i algorytmów, aby można było w miarę efektywnie przechowywać i
wyszukiwać elementy należące do pewnego dużego zbioru danych (uniwersum).
Przykładem takiego uniwersum mogą być liczby lub napisy (wyrazy) zbudowane z
liter jakiegoś alfabetu.

Podobne dokumenty