Algorytmy i złożono ć cz II


23
5. Podstawowe algorytmy i ich cechy.
5.1. Wyszukiwanie liniowe i binarne
5.1.1. Wyszukiwanie liniowe
Wyszukiwanie jest jedną z najczęściej wykonywanych
operacji na strukturach danych i dotyczy wszystkich,
omawianych w trakcie wykładu, struktur danych.
Wyszukując możemy mieć różne cele. Możemy szukać:
elementów posiadających określone cechy (w szczególności -
elementów najmniejszych, lub największych). Możemy też
zadowolić się tylko stwierdzeniem, czy element o określonych
cechach występuje w strukturze, czy też nie.
Przedstawiony na rys. 11 przykładowy algorytm zwraca
indeks tego elementu tablicy, którego wartość po raz pierwszy
równa się zadanej wartości x. Wyszukiwanie odbywa się w
jednowymiarowej tablicy danych typu całkowitego,
zadeklarowanej według (1) [str. 7]. W przypadku nie
stwierdzenia wystąpień elementów o wartościach równych
zadanej wartości x, algorytm zwraca sygnał o nieistnieniu
takich elementów.
Jest to przykład tzw. wyszukiwania liniowego. Jego cechą
charakterystyczną jest konieczność przeglądania, w sytuacji
najgorszego przypadku, całej struktury danych. W dalszej
części wykładu przedstawione zostanie wyszukiwanie
binarne, które jest znacznie efektywniejsze. Wymaga jednak
uporządkowania danych w strukturze.
24
Rysunek 11 powinien być okazją do zastanowienia się, czy
przedstawienie algorytmu w formie schematu blokowego jest
dobrą formą zapisu algorytmów.
s t a r t
Pobierz: t[ N ], x
i
0

t[i] < > x and i < N-1
N
t[i] < > x
T
T N
i
i + 1

Zwróć: i
Wyprowadz
"Nie ma takiego elementu"
s t o p
pobierz: N, x, wartości tablicy t[N];
jest = 0; //zakładamy,że wartość x nie występuje
i=0;
while((jest = = 0) and (i if( t[i]= =x ) jest =1; else i=i+1;
if(jest = = 1) wyprowadz: i;
else wyprowadz sygnał  Nie ma takiego elementu
Rys. 11. Algorytm wyszukiwania elementu o zadanej wartości
zapisany w postaci schematu blokowego, oraz w
postaci programu, zapisanego w języku C++
Sygnał, o którym mowa powyżej, powinien mieć postać
pewnej szczególnej wartości, której wystąpienie w tablicy nie
jest możliwe.
25
Następny przykładowy algorytm wyszukuje najmniejszy
element tablicy jednowymiarowej. Tego typu operacje są
wykonywane równie często, jak, przedstawione wyżej,
wyszukiwanie elementu o zadanej wartości
Zanim jednak przejdziemy do algorytmu rozwiązującego ten
problem zauważmy, że aby mógł być on w ogóle
rozwiązywalny, na elementach tablicy musi być określona
pewna relacja liniowego porządku, która polega na
możliwości wykonywania operatorów relacyjnych (< d" > e" =
`") w zbiorze wartości, znajdujących się w tablicy
wprowadz wartości do tablicy t[N];
t_min=t[0];
i_min=0;
for (int i=1; i<=N-1; i++)
if( t[i] {
t_min=t[i];
i_min=i;
}; wyprowadz: t_min, i_min
Rys. 12. Algorytm wyszukiwania najmniejszego elementu w
tablicy jednowymiarowej, zawierającej nie
powtarzające się wartości, w formie fragmentu
programu w języku C/C++
5.1.2. Wyszukiwanie połówkowe (binarne)
Zaprezentowany powyżej algorytm wyszukiwania elementu
tablicy miał cechy przeszukiwania liniowego. Jego schemat
jest prosty i naturalny:
26
1. Pobierz pierwszy element rozpatrywanej struktury,
2. Sprawdz, czy analizowany element jest elementem
poszukiwanym
Jeśli TAK  zakończ działanie algorytmu,
Jeśli NIE  pobierz kolejny element i roz-
pocznij realizację tego punktu
od początku.
Powyższy schemat sugeruje rekurencyjną wersję algorytmu.
W odniesieniu jednak do struktur liniowych o
nieskomplikowanej budowie, takich jak: tablice, pliki, czy
nawet listy liniowe, stosowanie rekurencji nie jest potrzebne.
Wystarczy zwykły proces iteracyjny.
Zauważmy, że dla struktur liniowych uporządkowanych,
takich jak: tablice, pliki i listy, średni czas wyszukania
elementu można skrócić o połowę. Bezcelowym jest bowiem
dalsze przeszukiwanie struktury po stwierdzeniu, że jej
elementy mają wartości wyższe (dla struktury
uporządkowanej rosnąco), niż wartość elementu
wyszukiwanego.
Poznamy teraz algorytm przeszukiwania połówkowego
zwany czasem przeszukiwaniem binarnym, który podobnie
jak omówione wyżej algorytmy wykorzystuje uporządkowanie
elementów struktury liniowej, a jednocześnie pomysł, na
którym opiera się idea, rozpatrywanych w dalszej części
wykładu, drzew binarnych poszukiwań.
Poniżej przedstawiono ogólny schemat algorytmu
przeszukiwania połówkowego dla uporządkowanej rosnąco,
jednowymiarowej, N-elementowej tablicy liczb całkowitych
t[N]. Szukamy elementu o kluczu równym zadanej wartości x.
27
1. Jeśli wartość x jest mniejsza od klucza pierwszego
elementu rozpatrywanej tablicy, lub większa od
klucza ostatniego elementu rozpatrywanej
tablicy element o kluczu x nie istnieje, koniec.
2. Jeśli analizowana tablica zawiera co najmniej 2
elementy podziel tablicę na połowę,
wykorzystując operator dzielenia całkowitego.
Niech middle oznacza indeks pierwszego elementu
prawej części tablicy,
Jeśli analizowana tablica zawiera 1 element niech
middle będzie indeksem tego elementu,
3. Jeśli x = = t[middle] znaleziono, koniec.
Jeśli analizowana tablica zawiera 1 element
element o kluczu x nie istnieje, koniec.
4. Jeśli x < t[middle], to element o kluczu x znajduje
się w lewej części tablicy (jeśli istnieje) przejdz
do punktu 2 przyjmując, że analizowaną tablicą jest
lewa jej część,
5. Jeśli x > t[middle], to element o kluczu x znajduje
się w prawej części tablicy (jeśli istnieje) przejdz
do punktu 2 przyjmując, że analizowaną tablicą jest
prawa jej część.
Intuicja podpowiada nam, że korzyści, jakie osiągamy
stosując algorytm wyszukiwania połówkowego, zamiast
wyszukiwania liniowego, powinny być znaczne.
Rzeczywistość przerasta jednak wszelkie oczekiwania, jego
złożoność obliczeniowa jest bowiem O (log2 N), wobec
liniowej złożoności obliczeniowej algorytmu wyszukiwania
liniowego O (N).
28
Złożoność obliczeniowa algorytmu, inaczej zwana kosztem
algorytmu jest funkcją podającą jak w sytuacji najgorszego
przypadku rośnie czas realizacji algorytmu w miarę
zwiększania rozmiarów zadania. Rozmiarem zadania,
polegającego na wyszukiwaniu elementu w jednowymiarowej
tablicy, będzie N, tj. ilość elementów tej tablicy.
Załóżmy przykładowo, że uporządkowana tablica zawiera aż
10 000 elementów. Średnia ilość porównań kluczy kolejnych
elementów tablicy z wartością poszukiwaną x wynosi przy
wyszukiwaniu liniowym 10000 / 2 = 5 000 porównań, podczas
gdy przy wyszukiwaniu połówkowym - nie więcej niż
log210 000, to jest około 13 porównań. Wynik jest
oszałamiający.
Natomiast dla tablic o małych rozmiarach nie warto używać
aż tak złożonego algorytmu, gdyż zysk czasowy będzie
niewielki. Na przykład, dla tablicy zawierającej 10
elementów, algorytm wyszukiwania liniowego będzie musiał
wykonać średnio 5 porównań, podczas gdy algorytm
wyszukiwania połówkowego potrzebuje nie więcej niż 3-4
powtórzenia, znacznie bardziej złożonej, pętli iteracyjnej.
5.2. Algorytmy sortowania tablic
Sortowanie tablic jest procesem, którego wynikiem
końcowym jest ustawienie elementów tablicy w kolejności
zgodnej z wybraną relacją liniowego porządku, lub w
porządku odwrotnym.
Opracowano wiele algorytmów sortowania tablic. Sortowanie
jest wdzięcznym zagadnieniem dydaktycznym, pokazującym
jak ten sam, niezbyt skomplikowany problem, rozwiązać
29
można na wiele różnych sposobów, opartych na bardzo
różnych pomysłach.
Algorytmy sortowania oceniać będziemy biorąc pod uwagę
niżej wymienione własności (pierwsze trzy z nich mogą
charakteryzować dowolne algorytmy, dwie ostatnie dotyczą
wyłącznie algorytmów sortowania):
5.2.1. Cechy algorytmów sortowania:
" prostota algorytmu,
Ta cecha jest dość istotna. Algorytmy o prostej
strukturze, oparte na prostym pomyśle, można łatwo
modyfikować i dostosowywać do aktualnych potrzeb
" zajętość pamięci,
Ta cecha jest bardzo istotna. Na ogół bowiem sięgamy do
metod tak zwanego sortowania w miejscu, zwanych
inaczej metodami  in situ (łac.). Ich zapotrzebowanie na
dodatkową pamięć ogranicza się na ogół do wielkości
zajmowanej przez wartość pojedynczego elementu
tablicy.
" koszt algorytmu
Musimy założyć, że rozmiarem zadania, polegającego na
sortowaniu jednowymiarowej tablicy, będzie N, tj. ilość
elementów tej tablicy. Większość algorytmów sortowania
charakteryzuje się kosztem O(N2), wymaga bowiem
dwóch pętli iteracyjnych, przy czym jedna z nich jest
zanurzona w drugiej.
" wrażliwość na uporządkowanie sortowanej tablicy,
Z tego punktu widzenia algorytmy sortowania
dzielić będziemy na:
- całkowicie niewrażliwe na uporządkowanie,
- częściowo wrażliwe na uporządkowanie,
30
- całkowicie wrażliwe na uporządkowanie.
W pierwszej grupie znajdą się algorytmy, dla których
uporządkowanie tablicy (pierwotne, bądz uporządkowanie
powstałe w trakcie sortowania) nie wpływa w sposób
zasadniczy na czas realizacji algorytmu.
Za algorytmy częściowo wrażliwe na uporządkowanie
uznamy te algorytmy sortowania, które w sposób znamienny
ograniczają ilość wykonywanych operacji w trakcie procesu
sortowania (np. poprzez zawieszenie wykonywania
pewnych pętli wewnętrznych).
Algorytmy sortowania całkowicie wrażliwe na
uporządkowanie potrafią w trakcie realizacji algorytmu,
niejako przy okazji, stwierdzić uporządkowanie tablicy
(pierwotne, bądz powstałe w dowolnym momencie procesu
sortowania), przerywając natychmiast proces sortowania.
Niżej przedstawiono ilustracje do czterech, wybranych metod
sortowania tablic. Dokładne omówienie przebiegu procesu
sortowania w tych przykładach zostanie podane na
wykładzie.
5.2.2. Sortowanie przez proste wstawianie
a)
indeksy 0 1 2 3 4
t
7 1 6 4 3
b)
indeksy 0 1 2 3 4
t
1 7 6 4 3
i
31
3
c) x
indeksy 0 1 2 3 4
t
1 6 7 4 3
i j
Rys. 13 Algorytm sortowania przez proste wstawianie
a) stan wyjściowy, b) stan po zakończeniu pierwszej
iteracji, c) ilustracja procesu przepisywania
elementów
5.2.3. Sortowanie przez prostą zamianę (sortowanie
bąbelkowe)
7 1 1 1 1
7
1 i 7 6 6 6
6 6 i 7 4 4
4 4 4 i 7 3
________
3 3 3 3 7 i
Rys. 14. Algorytm sortowania bąbelkowego
32
5.2.4. Algorytm sortowania przez podział (QuickSort)
5 2 7 1 3 4 6 8
2 1 3 4 7 6 8
3 4 6 8
1
Rys. 15 Idea algorytmu sortowania szybkiego QuickSort
5.2.5. Sortowanie z użyciem dodatkowej tablicy
Załóżmy, że elementami tablicy są rekordy (na rys. 16 w
owalu), z których każdy zawiera szereg pól. Jedno z nich
zostało wybrane jako klucz sortowania. Zaprezentowana niżej
metoda sortowania sprowadza się do wytworzenia dodatkowej
tablicy (zwanej tablicą indeksową), której pierwszy wiersz
przechowuje, ustawione w porządku rosnącym, klucze z
oryginalnej tablicy a drugi - odpowiadające kluczom indeksy.
indeksy 0 1 2 3 4
klucz 7 1 6 4 3
... ... ... ... ...
... ... ... ... ...
... ... ... ... ...
a)
klucz 1 3 4 6 7
indeksy 1 4 3 2 0
b)
33
Rys. 16 Tablica indeksowa b) powstała z posortowania
wejściowej tablicy rekordów a).
Owalem zaznaczono pojedynczy rekord.
Metoda ta wymaga użycia dodatkowej tablicy, w zamian 
pozwala zachować sortowaną tablicę w stanie pierwotnym, jak
również generować wiele tablic indeksowych wg różnych
kluczy. Ocenę złożoności czasowej pozostawmy czytelnikowi.
6. Algorytmy rekurencyjne
n * silnia(n-1) dla n > 0
silnia(n) =
1 dla n = 0
fib(n-1) + fib(n-2) dla n > 1
fib(n) =
1 dla n = 0,1
Rys. 17. Funkcje rekurencyjne  postać analityczna
int silnia( int n )
{ // n jest dowolną liczbą naturalną
if( n > 0 ) return n*silnia( n-1);
else return 1;
}
Rys.18 Funkcja rekurencyjna zapisana w języku C\C++
34
silnia(3) = 3 * silnia(2)
2 * silnia(1)
1 * silnia(0)
1
Rys. 19. Przebieg obliczeń wartości funkcji rekurencyjnej
silnia(3)
fib(n-1) + fib(n-2) dla n > 1
fib(n) =
1 dla n = 0,1
n 0 1 2 3 4 5 6 7 8 ...
fib(n) 1 1 2 3 5 8 13 21 35 ...
Rys. 20. Ciąg liczb Fibonacciego
35
5
3
4
3 2 2
1
2 1 1 0 1 0
0
1
Rys. 21. Drzewo wywołań rekurencyjnych dla
wywołania fib(5)
6.1.Rekurencja ogonowa
Z rekurencją ogonową mamy do czynienia, kiedy wywołanie
rekurencyjne jest ostatnim poleceniem algorytmu
rekurencyjnego. Wtedy rekurencja symuluje pętlę iteracyjną.
Jeśli wywołanie rekurencyjne nie jest ostatnim wywołaniem,
na każdym poziomie wywołania, algorytm wracając na dany
poziom, wykonuje dalsze czynności kończące algorytm.
36
Pojęcia, których znajomość jest niezbędna:
" głębokość rekurencji,
" liczba wywołań rekurencyjnych,
" maksymalna zajętość pamięci.
s = 1;
for( int i=1; i<=n; i++) s:=s*i;
Rys. 22. Iteracyjna postać algorytmu obliczania
wartości silnia(n) dla n e"
e" 0
e"
e"
0
1
1
2
n i
3 4
zm. a) s
6 b)
tymczasowa
stos stos
dla zmiennych dla zmiennych
Rys. 23. Derekursywacja
Na rys 23 porównano stosy dla zmiennych dla algorytmu
realizującego wywołanie funkcji silnia(3):
a) rekurencyjnego, w chwili po ostatnim wywołaniu
rekurencyjnym silnia(0),
b) iteracyjnego, po zakończeniu procesu iteracji
37
6.2. Rekurencja zagnieżdżona
Przykładem funkcji z rekurencją zagnieżdżoną jest podana w
1928 przez W. Ackermanna funkcja
m+1 jeśli n = 0
A(n,m) = A(n-1,1) jeśli n>0, m=0
A(n-1, A(n,m-1)) w pozostałych przyp.
Zagnieżdżenie rekurencji, dotyczące parametru m, powoduje
nieprawdopodobnie gwałtowne zapotrzebowanie na czas
obliczeń ze wzrostem m. Obliczono, że
16
A(1,4)=22 -3=265536-3
co jest liczbą nieporównanie większą od liczby wszystkich
atomów we wszechświecie (obecnie szacuje się, że liczba ta
jest rzędu 1080). Definicję funkcji Ackermana bardzo łatwo
jest zapisać w postaci funkcji rekurencyjnej, natomiast
zapisanie jej w formie innej, niż rekurencyjna, jest bardzo
kłopotliwe.
Koniec części 2


Wyszukiwarka