Algorytmy I Struktury Danych (W Nieznany (2)

background image

Wykład I

1. Pojęcie algorytmu

Definicja nieformalna

Algorytmem jest pewna ściśle określona procedura obliczeniowa, która dla właściwych

danych wejściowych „produkuje” żądane dane wyjściowe zwane wynikiem działania
algorytmu.

Inne definicje:
Algorytm to

ciąg kroków obliczeniowych prowadzących do przekształcania danych wejściowych w
dane wyjściowe;

skończony ciąg reguł, które aplikuje się na skończonej liczbie danych, pozwalający
rozwiązywać zbliżone do siebie klasy problemów;

zespół reguł charakterystyczny dla pewnych obliczeń lub czynności.

Źródłosłów:

Perski pisarz – matematyk Abu Jafar Mohammed ibn Musa al-Khowarizmi

( IXw n.e.)

podał regułę wyjaśniającą krok po kroku zasady operacji arytmetycznych na liczbach
dziesiętnych;

po łacinie jego nazwisko brzmi: Algorismus.

Słowo algorytm często jest również kojarzone z greckim matematykiem Euklidesem (365-

300 p.n.e.), który przedstawił m.in. sformalizowane algorytmy:

dzielenia kąta na połowy;

badania identyczności dwóch kątów;

obliczania Największego Wspólnego Podzielnika tzw. NWP dwóch liczb
naturalnych.

Algorytmy stworzone przez Euklidesa wykorzystywały w swoich działaniach elementarne

operacje, które mogły być wykonane w sposób jednoznaczny, tzn.

dla algorytmów geometrycznych były to operacje typu prosta – okrąg, wykonywane
przy pomocy cyrkla i linijki;

dla algorytmów obliczeniowych (numerycznych) były to operacje arytmetyczne: +, -, *, /
oraz porównywania.

Nas interesować będą algorytmy w kontekście informatycznym, tzn. przekształcające

efektywnie dane wejściowe na dane wyjściowe zwane również wynikiem.

Dane

wejściowe

Algorytm

Dane

wyjściowe

background image

Cechy algorytmu informatycznego:

!

posiada dane wejściowe, które pochodzą z dokładnie określonego zbioru, np. ze
zbioru liczb naturalnych N, ze zbioru liczb rzeczywistych R;

!

produkuje pewien wynik (niekoniecznie numeryczny);

!

jest precyzyjnie zdefiniowany, tzn. każdy jego krok jest jednoznacznie określony i
obejmuje wyłącznie operacje elementarne;

!

jest skończony, tzn. w skończonym czasie wyprodukuje wynik, a czas jego działania
powinien być możliwie precyzyjnie określony;

!

jest jednoznaczny ( inaczej powtarzalny), tzn. jego wielokrotne wykonywanie dla
identycznych danych wejściowych daje zawsze taki sam wynik;

!

jest kompletny, tzn. uwzględnia wszystkie możliwe przy-padki, jakie mogą wystąpić
podczas jego wykonywania;

!

jest uniwersalny, tzn. umożliwia rozwiązanie całej klasy zadań, a nie tylko
pojedynczego, ustalonego zadania.

Formy przedstawiania algorytmu informatycznego

!

opis słowny, zazwyczaj z formułami matematycznymi

!

schemat graficzny

schematy blokowe czyli ciągi znormalizowanych symboli graficznych
połączonych skierowanymi liniami

strukturogramy czyli graficzne schematy zwarte

!

specjalne języki matematyczne zwane pseudokodami

!

języki programowania wyższego poziomu np. PASCAL, Fortran, C, BASIC, ADA,
Simula, itp.

background image

Schematy blokowy – lista bloków

Tzw. schematy Bohma – Jacopiniego.

Bloki iteracyjne

Powtarzaj <ciąg czynności>
dopóki <warunek>

Dopóki <warunek>
wykonuj <ciąg czynności
>

początek

koniec

x=x

y

dla i=1 do n

wprowadzić

a,b,c

pisz
„Brak danych”

WE

W4

warunek

Blok decyzyjny

1

2

Bloki

łącznikowe

Blok

iteracyj

Blok

operacy

Blok

końca

Blok
począ

tk

Tak

Nie

Ciąg czynności

warunek

Tak

Nie

Ciąg czynności

warunek

Tak

Nie

background image

Strukturogramy zwarte

(Diagramy Nassi - Shnaeidermana)

A

Blok wejścia (klawiatura, plik)

A

Blok wyjścia (monitor, drukarka, plik)

Operacja

Blok operacyjny

Warunek

tak

nie

Blok 1

Blok 2

Blok decyzyjny

wyrażenie

W1

W2

W3

Inaczej

Blok 1

Blok 2

Blok 3

Blok

Blok wyboru z szeregu możliwości

i=1,m

Blok - treść pętli

Pętla - cykliczne wykonanie bloku dla 1,...,m

warunek

blok

pętla typu
dopóki <warunek> wykonuj <blok>

blok

warunek

pętla typu
powtarzaj <blok> aż do <warunek>

nazwa algorytmu

blok procedury
(odrębnego, nazwanego fragmentu algorytmu)

background image

Typy algorytmów

Z punktu widzenia organizacji wykonywanych czynności (obliczenia, rysowanie,

porównywanie) rozróżniamy trzy podstawowe typy algorytmów:

!

liniowy (sekwencyjny): wszystkie czynności są wykonywane w ściśle określonej
kolejności, jedna za drugą;

!

z rozgałęzieniami (selekcja): kolejność i zakres wykonywanych czynności zależy od
warunków, których spełnienie (lub niespełnienie) zależy od danych wejściowych oraz
pewnych wyników pośrednich;

!

iteracyjny (pętla): pewien ciąg identycznych czynności (tzw. cykl lub pętla) jest
powtarzalny wielokrotnie dla różnych danych.

Przykłady różnych form przedstawienia algorytmu.

Opis słowny
Problem: Oblicz wartość funkcji:

x

x

f(x)

=

Dane : dowolna liczba rzeczywista x

R

Wynik: wartość funkcji f(x) określona jest wzorem:

Algorytm:

krok 0:

Podaj wartość danej x

krok 1: Jeśli x>0, to f(x)=1.

Zakończ algorytm

krok2:

Jeśli x=0, to f(x)=0

{

}

0

x

Dla

<=

Zakończ algorytm

krok 3:

Mamy f(x)=-1.

{

}

0

x

Dla

<

Zakończ algorytm

Schemat blokowy

-1 dla x<0

f(x)=

0 dla x=0
1 dla x>0

początek

x=0

TAK

NIE

NIE

TAK

x<0

podaj x

wynik =

koniec

koniec

koniec

wynik = 1

wynik = 0

wynik=-

1

Koniec

background image
background image

Zapis w pseudokodzie :

Funkcja f(x)
początek

wczytanie x
jeżeli x>0 to pisz f(x)=1
w przeciwnym razie

jeżeli x=0 to pisz f(x)=0
w przeciwnym razie pisz f(x)= -1

koniec

Program w języku Turbo Pascal

PROGRAM funkcja;
VAR x: Real;
BEGIN

READ (x);
IF x>0 THEN WRITE(1)

ELSE IF x=0 THEN WRITE(0)

ELSE WRITE(-1);

END.

Kilka faktów z historii algorytmiki

W rozwoju algorytmiki można wskazać kilka istotnych dat oraz osób, które są ważnymi

punktami zwrotnymi:

1801 -

Francuz Joseph Marie Jacquard
- konstrukcja krosna tkackiego „programowanego” przez kartę perforowaną

1833 -

Anglik Charles Babbage
- konstrukcja maszyny do wyliczania pewnych formuł matematycznych

1890 -

Amerykanin Herman Hollerith
- maszyna do opracowywania danych statystycznych (spisy ludności)
1911 – powstała IBM

1930 -

badania teoretyczne
Allan Turing, Carl Gődel, Markov

1940 -

pierwsze maszyny liczące: MARK1, ENIAC
EDVAC
: pierwszyy prawdziwy komputer
- koncepcja Johannesa von Neumanna
program jako dane w pamięci komputera

lata 60-te - pierwsze duże systemy informatyczne (COBOL, ALGOL)

- początki metodologii: intuicja, wyczucie prowadziły do wielu błędów, małej
wiarygodności; potrzebne były duże nakłady na wdrożenie i dalszy rozwój

background image

- brak metod na sprawdzanie poprawności oprogramowania (jedynie testowanie do
pełnego wyeliminowania błędów).

Impulsem w kierunku przejścia od sprawdzania poprawności programów komputerowych

metodą prób i błędów do udowadniania, że posiada on pożądane własności (czyli jest poprawny)
był artykuł Johna McCarthyegoA basis for a mathematical theory of computation”. Innymi
teoretykami byli: Dijkstra, Hoare, Floyd, Writh.

2. Podstawowe zasady analizy algorytmów

Układając algorytmy (programy) staramy się poznać ich własności. Chcemy wiedzieć

między innymi:

!

czy ułożony algorytm jest rzeczywiście rozwiązaniem postawionego problemu;

!

czy przeznaczony czas pracy komputera jest wystarczający

!

czy ilość miejsca w pamięci jest wystarczająca

!

jakie będą skutki po dokonaniu pewnych zmian w algorytmie, czy zmiany te nie
zmienią wyników

!

czy będzie działał dla każdej możliwej konfiguracji danych wejściowych.

Te i inne problemy spowodowały powstanie działu informatyki o nazwie Analiza

algorytmów, którego podstawowym zadaniem jest szukanie najlepszych algorytmów. Szukanie
to sprowadza się do znalezienia odpowiedzi na następujące trzy pytania:

Pytanie 1

Czy dany problem MOŻE BYĆ rozwiązany na komputerze w dostępnym czasie i dostępnej
pamięci?

Pytanie 2

Który ze znanych algorytmów jest najlepszy (ogólnie i dla konkretnego zestawu danych
wejściowych); czy jest on algorytmem optymalnym?

Pytanie 3

Jak udowodnić, że wybrany algorytm rozwiąże dany problem?

Analizując więc algorytmy należy zwrócić uwagę na jego :

!

poprawność semantyczną, czyli czy wykonuje on postawione zadania;

!

prostotę;

!

czas działania;

!

ilość zajmowanej pamięci;

!

optymalność.

Przedstawione teraz będą metody analizy algorytmów, które są pomocne przy znajdowaniu

odpowiedzi na postawione wyżej pytania 1, 2 i 3.

A. Poprawność semantyczna,czyli algorytm rozwiąże postawione zadanie

Powiedzieliśmy wcześniej, że działanie algorytmu (oznaczmy go literą K ), zależy od

wartości początkowych otrzymanych z zewnątrz, czyli od tzw. danych wejściowych (α).

Np. - dla algorytmu obliczającego NWP(m,n) będą to dwie liczby naturalne m oraz n .

background image

- dla algorytmu znajdującego największy element w ciągu liczbowym (a

1

, ..., a

n

),

będą to elementy tego ciągu oraz liczba n oznaczająca długość ciągu.

Natomiast wartości, które algorytm przekazuje na zewnątrz nazywane są danymi

wyjściowymi (β) lub wynikiem algorytmu.

Np. - algorytm NWP(m,n) powinien podać jako wynik wartość największego wspólnego

podzielnika liczb m i n.

Mamy więc następujący układ:

Definicja

Mówimy, że algorytm K jest semantycznie poprawny względem warunków początkowego α
i końcowego β, jeżeli:

dla każdych danych wejściowych spełniających warunek α obliczenia algorytmu K
dochodzą do punktu końcowego
;

końcowe wartościowanie zmiennych spełnia warunek β.

Przyjmujemy, ze: algorytm semantycznie poprawny

algorytm

poprawny.

Odwracając zagadnienie, postawmy pytanie:

Kiedy algorytm jest niepoprawny ?.

Odpowiedzi mogą być również trzy:

Po pierwsze:

albo dochodzi do końca, ale wyniki nie spełniają warunku końcowego β
(źle liczy)

Po drugie:

albo zatrzymuje się w punkcie niekońcowym algorytmu
(zawiesza się)

Po trzecie:

albo obliczenia algorytmu są nieskończone
(zapętla się).

Co więc trzeba wykazać, żeby stwierdzić, że analizowany algorytm jest poprawny?

Wystarczy pokazać, że dla każdych danych wejściowych spełniających warunek α ma on

trzy następujące własności:

1) jeżeli obliczenie algorytmu K dochodzi do punktu końcowego, to otrzymane wyniki

spełniają warunek końcowy β;

2) obliczenie algorytmu K nie jest przerwane;

warunek początkowy

α

(dane wejściowe)

Algorytm K

warunek końcowy

β

(dane wyjściowe

)

background image

3) obliczenie algorytmu K nie jest nieskończone.

Czy zawsze te wszystkie trzy warunki są jednocześnie spełnione?

Jeżeli dla algorytmu K spełniony jest tylko warunek:

Pierwszy " to algorytm K jest

częściowo poprawny względem warunku początkowego α i warunku końcowego β;

Drugi " to algorytm K ma

własność określoności obliczeń względem warunku początkowego α;

Trzeci " to algorytm K ma

własność stopu względem warunku początkowego α..

B. Metoda niezmienników

Metoda niezmienników:

!

służy do dowodzenia poprawności algorytmów

!

polega na opisywaniu własności wartościowań zmiennych otrzymywanych, gdy
obliczenie algorytmu przechodzi przez jego wyróżnione punkty ;własności te
nazywane są właśnie niezmiennikami algorytmu.

Przykład

Rozwazmy algorytm dzielenia całkowitego liczb naturalnych. Jego sformułowanie jest

następujace:

Dla dwóch liczb naturalnych x, y znaleźć dwie liczby naturalne q oraz r takie, że:

r

y

+

=

q

x

gdzie

q - iloraz całkowity dzielenia x przez y
r - reszta z tego dzielenia.

Np. dla x=11, y=4 mamy 11 = 2 * 4 + 3

czyli q=2 oraz r=3

Zapis algorytmu w pseudokodzie:

początek

{

}

y

0

x

0

:

α

1: q:=0 ; r:=x ;
2: dopóki y<=r wykonuj

początek

q:=q+1 ; r:=r-y

koniec;

3:

{

}

y

<

+

=

r

0

r

y

q

x

:

β

koniec;

Mamy więc:

Warunek początkowy α : 0<=x

0<=y

Warunek końcowy β : x=q*y+r

0

r<y

Algorytm składa się z dwóch części:

1. incjalizacji wartości zmiennych: q oraz r (etykieta 1)

background image

2. iteracji dopóki , która oblicza iloraz q oraz resztę r z dzielenia całkowitego x przez y.

( etykieta 2)

Przeprowadzimy dowód na częściową poprawność tego algorytmu względem warunków α i

β. Polegać on będzie a pokazaniu, że:

dla danych wejściowych spełniających warunek początkowy α, jeżeli obliczenia dochodzą
do końca , to wartości zmiennych x, y, q oraz r spełniają warunek końcowy β.

Postawmy w tym celu następujące pytanie:

Jaki warunek spełniają zmienne x, y, q oraz r w punkcie pośrednim algorytmu, tzn. w
etykiecie 2, a dokładniej,
jaki warunek spełniają te zmienne w chwili sprawdzania warunku „y<=r” sterującego
iteracją dopóki?

Określmy w tym celu następujący nowy warunek
γ:

y

r

r

y

+

=

0

0

q

x

jako niezmiennik naszej instrukcji warunkowej dopóki.

Pokażemy, że za każdym razem,

gdy obliczenie algorytmu rozpoczyna się ze stanem spełniającym warunek

początkowy

α

orazdochodzi z wartościami x, y, q oraz r do sprawdzania

warunku iteracji „y<=r”,

wówczas

dla tych wartości zmiennych jest spełniony warunek γ.

Zauważmy, że istnieją tylko dwa przypadki dojścia obliczeń do etykiety 2 :

1) bezpośrednio od etykiety 1 i wtedy mamy

q=0, r=x,

a ponieważ z definicji warunku α mamy, że 0

x

0

y,

to warunek

γ: x=0*y+x=x

0

r

0

y

jest oczywiście spełniony

2) przy wyjściu od samej etykiety 2, gdy warunek zachodzi i powtarza się wykonanie

instrukcji iteracyjnej.

Załóżmy więc, że

rozpoczynamy działanie od etykiety 2

spełnione są warunki

γ:

y

r

r

y

+

=

0

0

q

x

oraz

warunek iteracji: „y<=r”

Wykonuje się wtedy instrukcja złożona, która

zmiennej q nadaje nową wartość q’=q+1

zmiennej r nadaje nową wartość r’=r-y

A wtedy mamy dla warunku γ:

γ(x, y, q’, r’)

x=q’*y+r’

0

r’

0

y

x=(q+1)*y+r-y i 0

r-y

0

y

Ponieważ:

1) (q+1)*y+r-y=q*y-r=x

więc 1-szy czynnik warunku γ jest spełniony;

background image

2) 0

r-y jest też prawdziwy, bo wiemy, że y

r z założenia warunku iteracyjnego, czyli

0

r-y

3) czynnik 0

y jest też spełniony, ponieważ założono, że warunek „0

y” zachodzi w

α

,a

wartość zmiennej y nie ulega zmianie.

Stosując indukcję względem liczby wykonywanych sprawdzeń warunku iteracji „y

r” (tj.

liczby przejść obliczeń przez etykietę 2), wnioskujemy, że przy każdym sprawdzeniu warunku
iteracji zachodzi warunek γ.

Mogą teraz zajść dwa przypadki:

1) Warunek iteracyjny „y

r” pozostaje ciągle spełniony i wtedy obliczenie NIGDY nie

dochodzi do etykiety 3;

2) W pewnej chwili przy sprawdzaniu warunku iteracji mamy y>r i obliczenie

przechodzi do etykiety 3.
Ponieważ wtedy spełniony jest również warunek γ, co przy fakcie, że y>r daje nam
prawdziwość warunku końcowego

β:

r

y

+

=

q

x

∧∧∧∧

0

≤≤≤≤

r<y

Udowodniliśmy więc następujący fakt:

jeżeli obliczenie algorytmu startuje z danymi wejściowymi spełniającymi warunek

αααα

i

dochodzi do punktu końcowego, to otrzymane wyniki spełniają warunek końcowy

ββββ

.

Oznacza to, że algorytm jest częściowo poprawny względem warunków

α

i

β

.

Nie jest on jednak poprawny, bo np. dla danych: x=0, y=0 jego obliczenia są nieskończone
(brak własności stopu!)

Uogólniając, metoda niezmienników polega na:

1) wyróżnieniu w programie pewnych szczególnych miejsc
2) przypisaniu do nich warunków na wartościowanie zmiennych algorytmu
3) udowodnieniu, że

jeżeli początkowy stan obliczenia spełnia warunek początkowy

α

, to ilekroć obliczenie

algorytmu dochodzi do wyróżnionego miejsca, zawsze przyporządkowany temu miejscu
warunek jest spełniony przez aktualne wartościowanie zmiennych.
W szczególności, gdy dochodzi do punktu końcowego
, to wartościowanie zmiennych
spełnia warunek końcowy

β

Należy zwrócić uwagę na to, że bardzo istotny jest wybór wyróżnionych miejsc (czyli

niezmienników) w analizowanej instrukcji.
Na przykład:

!

dla instrukcji typu dopóki jest to miejsce sprawdzania warunku sterującego iteracją

!

dla instrukcji typu powtarzaj są to dwa miejsca:

- początek instrukcji
- -miejsce sprawdzania warunku sterującego iteracją

C. Dowodzenie własności stopu

Stosuje się dwie metody dowodzenia:

1) Metoda liczników iteracji

background image

wprowadzenie dodatkowej zmiennej całkowitej służącej do obliczania liczby wykonań
instrukcji iteracyjnej (licznik wykonań iteracji)
# szuka się wyrażenia, którego wartość w trakcie realizacji algorytmu ograniczyłaby z

góry wartość tej zmiennej (licznika)

#

W przypadku rozpatrywanego algorytmu dzielenia:

poczatek

{

}

0

y

i

0

x

:

1

>

α

1: q:=0; r:=x
2: dopóki r

y wykonuj

początek

q:=q+1; r:=r-y

koniec

3:

{

}

<

+

=

r

0

i

r

y

q

x

:

β

zmienna q pełni rolę licznika iteracji dopóki; mamy zatem, że górna ilość wykonań iteracji
wynoai q

≤

x/y

2) Metoda malejących wielkości

rozpatruje się takie wielkości (wyrażenia arytmetyczne), które zwiększają (lub
zmniejszają) swoją wartość w sposób nieregularny i dla których istnieje wartość
ograniczająca je z góry (lub z dołu).

Przykład

Numeryczny algorytmy przybliżonego rozwiązywania równania nieliniowego metodą

bisekcji wykonuje iteracje tak długo, aż przedział zawierający pierwiastek tego równania
osiągnie zamierzoną szerokość

ε

>0 .

3

. ZŁOŻONOŚĆ OBLICZENIOWA

Każde wykonanie algorytmu na komputerze wymaga pewnej pracy oraz pewnej ilości

miejsca w jego pamięci. Dlatego też przed kodowaniem algorytmu do postaci akceptowanej
przez komputer, jego testowaniem a następnie użytkowaniem, powinniśmy odpowiedzieć sobie
na pytanie:

Czy nasz sprzęt umożliwia stosowanie rozważanego algorytmu dla przewidywanych
danych?

Okazuje się bowiem, że dla wielu znanych algorytmów czas ich działania zwiększa się zbyt

szybko w miarę, jak wzrasta wielkość danych wejściowych. Dokonując oceny różnych
algorytmów rozwiązujących dane zagadnienia chcemy wiedzieć, jaka jest ich złożoność
obliczeniowa
, czyli inaczej, jaka ilość zasobów komputerowych potrzebna jest do ich
wykonania.

Za podstawowe zasoby komputerowe uważa się:

czas działania algorytmu

background image

ilość zajmowanej pamięci.

Chcemy jednak, żeby te dwie wartości (czas i pamięć) były na tyle reprezentatywne, by np.

użytkownik małego komputera osobistego oraz użytkownik potężnego superkomputera,
używający tego samego algorytmu - mogli się porozumieć do do jego sprawności obliczeniowej.
Czy bowiem stwierdzenie:

Mój program (algorytm) jest szybki, bo rozwiązał zadanie w 55 sekund !

może posłużyć za obiektywną ocenę jego sprawności ?

Dodatkowo powinniśmy bowiem wiedzieć:

na jakim komputerze program był wykonywany

jaki miał procesor (częstotliwość zegara)

co działo się w pamięci komputera podczas jego wykonywania

jakiego kompilatora użyto do napisania programu, itp.

Stąd wynika oczywisty wniosek:

Ocena sprawności algorytmu przy pomocy kryteriów sprzętowych nie jest oceną

obiektywną!

Należy więc znaleźć miarę uniwersalną, która ma decydujący wpływ na czas

wykonywania określonego algorytmu. Parametrem tym jest rozmiar danych wejściowych
algorytmu.

Pojęcie to ma różne znaczenie dla różnych zagadnień, np.:

w sortowaniu ciągu n-elementowego jest to ilość elementów tego ciągu, czyli n.

dla zadań grafowych jest to liczba krawędzi rozpatrywanego grafu

przy wyznaczaniu wartości wielomianu - stopień tego wielomianu

dla operacji na macierzach - wymiary macierzy

dla wyliczenia wartości funkcji n! -wartość danej n.

Przed przystąpieniem do wyznaczania złożoności obliczeniowej algorytmów należy jeszcze

określić jednostki, w jakich będzie ona liczona.

Dla złożoności pamięciowej jednostką taką będzie słowo pamięci i jest to sprawa oczywista.

Dla złożoności czasowej problem jest bardziej złożony, ponieważ jednostka ta powinna

być własnością samego tylko algorytmu jako metody rozwiązania problemu, a nie powinna
zależeć od komputera, języka programowania, kodowania , itp.

W tym celu wyróżnia się w klasie algorytmów charakterystyczne dla niego operacje -

nazywane są one operacjami dominującymi.

Przykłady operacji dominujących:
# w algorytmach sortowania

porównanie dwóch elementów w ciągu wejściowym

przestawienie dwóch elementów w ciągu

# w algorytmach numerycznych, np. obliczania wartości wielomianu

operacje arytmetyczne: +, -, /, *

# w algorytmach przeglądanie drzewa binarnego

przejście dowiązania pomiędzy dwoma węzłami w drzewie.

background image

Za jednostkę złożoności czasowej przyjmuje się wykonanie jednej operacji dominującej.

Zanim przejdziemy do szczegółowego omówienia własności funkcji złożoności czasowej,

postawmy następujące pytanie:

Czy postęp w szybkości obliczeń komputerów zmniejsza znaczenie efektywności

algorytmów?

W celu ilustracji odpowiedzi na to pytanie rozważmy pięć algorytmów A

1

, A

2

,..., A

5

,

rozwiązujących ten sam problem i mających następujące złożoności czasowe:

Algorytm

Złożoność czasowa f(n)

f(n=10)

A

1

n

10

A

2

n*logn

33

A

3

n

2

100

A

4

n

3

1000

A

5

2

n

1024.

gdzie n - rozmiar danych wejściowych.

Zakładając, że jednostką czasu jest jedna milisekunda, pokażemy, jakiego maksymalnego

rzędu zadania mogą rozwiązać te algorytmy odpowiednio w: 1 sekundzie, 1 minucie, 1 godzinie.

Maksymalny rozmiar zadania

Algorytm

Złożoność czasowa

1sek

1min

1godzina

A

1

n

1000

6x10

4

3,6

10

6

A

2

nlogn

140

4893

2,0

10

5

A

3

n

2

31

244

1897

A

4

n

3

10

39

153

A

5

2

n

9

15

21

Przypuśćmy teraz, że nowa generacja komputerów jest 10-krotnie szybsza. Pokażemy, jaki

jest wzrost rozmiaru zadań, które mogą być rozwiązane dzięki wzrostowi szybkości
komputerów:

Algorytm

Złożoność

czasowa

Maksymalny rozmiar

zadania przed

przyśpieszeniem

Maksymalny rozmiar

zadania po

przyśpieszeniu

A

1

n

S

1

10S

1

A

2

n*logn

S

2

około 10S

2

dla dużych

n

A

3

n

2

S

3

3.16 S

3

A

4

n

3

S

4

2.15 S

4

A

5

2

n

S

5

S

5

+ 3.3

Zauważamy, że np.:

dla A

5

10-krotny wzrost szybkości pozwolił zwiększyć zadania tylko o 3;

dla A

3

rozmiar ten wzrósł więcej niż 3-krotnie

background image

dla A

1

rozmiar wzrósł 10-krotnie, czyli tyle, ile wzrosła prędkość komputera

Traktując 1 minutę jako podstawę porównania można łatwo stwierdzić, że:

zastępując A

4

przez A

3

możemy rozwiązać zadanie 6 razy większe

zastępując A

4

przez A

2

możemy rozwiązać zadanie 125 razy większe

# jest to lepsze niż 2-krotne zwiększenie zadania uzyskane przez 10-krotny

wzrost szybkości komputera.

WNIOSEK: Bardziej opłacalne jest konstruowanie szybszych algorytmów.

Rozpatrywane wyżej funkcje złożoności czasowej są tzw. złożonościami asymptotycznymi,

tzn. nie uwzględniają stałej proporcjonalności. Może się, więc zdarzyć, że analizując faktyczne
funkcje złożoności czasowej
, (dokładnie wyznaczone), algorytm asymptotycznie gorszy jest
szybszy dla małych zadań niż algorytm asymptotycznie lepszy.

Dla ilustracji przypuśćmy, że rozważane algorytmy A

1

, ..., A

5

mają następujące faktyczne

funkcje złożoności czasowej:

Algorytm

Funkcje złożoności czasowej

Kiedy najszybsze

A

1

1000 n

n>1024

A

2

100 n*logn

59

≤≤≤≤

n

≤≤≤≤

1024

A

3

10 n

2

10

≤≤≤≤

n

≤≤≤≤

58

A

4

n

3

A

5

2

n

2

≤≤≤≤

n

≤≤≤≤

9

Wyznaczając złożoność czasową algorytmu (jest to funkcja rozmiaru danych n) rozróżniamy

jej dwie postacie:

1) złożoność pesymistyczna

ilość operacji dominujących potrzebnych przy trafieniu na „najgorsze” dane wejściowe
rozmiaru n

2) złożoność oczekiwana

ilość operacji dominujących potrzebnych przy „typowych” danych wejściowych
rozmiaru n.

Formalne definicje tych pojęć są następujące:

Przez pesymistyczną złożoność czasową algorytmu rozumie się funkcję:

{

}

n

D

d

:

t(d)

sup

W(n)

=

gdzie: sup

- kres górny zbioru

D

n

- zbiór wszystkich możliwych zestawów danych wejściowych rozmiaru n

t(d)

- liczba operacji dominujących dla zestawu danych wejściowych d

Przez oczekiwaną złożoność czasową algorytmu rozumie się funkcję:

background image

=

0

k

nk

p

k

A(n)

gdzie: p

nk

- rozkład prawdopodobieństwa zmiennej losowej X

n

, tzn. prawdopodobieństwo, że

dla danych rozmiaru n algorytm wykona k operacji dominujących (k

0),

X

n

jest zmienną losową, której wartością jest t(d), dla d

D

n

czyli jest to wartość oczekiwaną ave(X

n

) zmiennej losowej X

n

.

W celu stwierdzenia, na ile funkcje W(n) i A(n) są reprezentatywne dla wszystkich danych

wejściowych rozmiaru n, rozważa się dwie wrażliwości algorytmu:

miarę wrażliwości pesymistycznej:

( ) ( )

{

}

n

2

1

2

1

D

d

,

d

:

d

t

d

t

sup

(n)

=

miarę wrażliwości oczekiwane:

)

dev(X

(n)

n

=

δ

gdzie dev(X

n

) jest standardowym odchyleniem zmiennej losowej X

n

, tzn.

=

=

0

k

nk

2

n

n

n

n

p

))

ave(X

-

(k

)

var(X

i

)

var(X

)

dev(X

,

var(X

n

) oznacza wariancję zmiennej losowej X

n

.

Jak należy interpretować wartości wrażliwości

∆∆∆∆

(n) i

δδδδ

(n):

Im większe wartości

∆∆∆∆

(n) i

δδδδ

(n), tym algorytm jest bardziej wrażliwy na dane wejściowe

i tym bardziej jego zachowanie w wypadku rzeczywistych danych może odbiegać od
zachowania opisanego funkcjami W(n) i A(n).

Przykład:

Algorytm przeszukiwanae sekwencyjnego ciągu.

Problem: Dla podanego n-elementowego ciągu liczb naturalnych oraz liczby x sprawdzić, czy

liczba x jest elementem tego ciągu.

Dane wejściowe:

n, (n

≥≥≥≥

0) liczba naturalna

L[1..n+1] tablica z elementami ciągu (a

1

, ..., a

n

) na miejscach od 1do n ,

x poszukiwany element.

Dane wyjściowe (wynik):
zmienna

logiczna

p taka, że

p= prawda gdy element x należy do ciągu
p= fałsz,

gdy element x nie należy do ciągu

Zapis algorytmu w pseudokodzie:

początek

j := 1;
L[n+1] := x;

background image

dopóki L[j]

#

x wykonuj j:= j+1;

p := j

n

koniec;

Realizacja algorytmu (zestaw danych 1)

Dla: n=4; ciągu (1,4,2,8), x=4 mamy:

L[1]=1, L[2]=4, L[3]=2, L[4]=8

j=1 , L[5]=4

1 # 4, TAK j=1+1=2 ( L[1]#4 )
4 # 4, NIE ( L[2)#4 )
p=2

4 (prawda)

Realizacja algorytmu (zestaw danych 2)

Ddla: n=4; ciągu (1,4,2,8), x=5 mamy

L[1]=1, L[2]=4, L[3]=2, L[4]=8

j=1 L[5]=5

1#5, TAK j=1+1=2 (L[1]#5 )
4#5, TAK j=2+1=3 (L[2]#5 )
2#5, TAK j=3+1=4 (L[3]#5 )
8#5, TAK j=4+1=5 (L[4]#5 )
5#5, NIE (L[5]#5 )
p=5

4 (falsz)

Analiza złożoności algorytmu:

Rozmiar danych wejściowych

: n

Operacje dominujące

: porównania L[j])#a

Pesymistyczna złożoność czasowa :

W(n)=n+1

Pesymistyczna wrażliwość czasowa:

∆∆∆∆

(n)=n

bo najwięcej porównań n+1
bo najmniej porównań 1
stąd

(n)=(n+1)-1=n

Jaka jest

więc oczekiwana złożoność czasowa ?

Niech prawdopodobieństwo znalezienia elementu x na każdym z n możliwych miejsc

jest takie samo, oraz element x należy do ciągu (a

1

,.... a

n),

tzn.

1,2,...n

k

dla

n

1

p

nk

=

=

Wówczas

2

1

n

2

)

1

n

(

n

n

1

n

1

k

n

1

n

1

k

n

1

n

1

k

nk

k

k

p

k

A(n)

+

+

=

=

=

=

=

=

=

=

Do obliczenia oczekiwanej wrażliwości czasowej należy znaleźć wcześniejsze wariancje

zmiennej losowej X

n

background image

( )

(

)

(

)

( )

(

)

(

)

2

12

1

12

n

12

1

n

4

1)

(n

6

1)

1)(2n

n(n

2

2

1

n

2

1)

n(n

2

1)

2(n

6

1)

1)(2n

n(n

n

1

n

1

k

n

1

2

2

1

n

n

1

k

nk

2

n

n

n

1

-

3

-

3n

-

2

4n

-

n

k

p

X

ave

k

)

var(X

2

2

=

+

=

=

=

+

=

=

=

+

+

+

+

+

+

+

+

+

=

+

=

i teraz

( )

0,25n

n

X

var

)

dev(X

δ(n)

3,464

n

12

n

2

12

1

n

n

=

=

=

=

Wniosek:

Funkcja wrażliwości i funkcja złożoności są liniowe, a więc mocno wrażliwe na układ

danych wejściowych.

Jest oczywiste, że faktyczna złożoność czasowa algorytmu w chwili jego użycia różni się od

wyliczonej teoretycznie, pewnym współczynnikiem proporcjonalności lub pewnymi
elementami stałymi. Ponieważ analiza złożoności interesuje się głównie zależnością
efektywności algorytmu (czasu działania) od rozmiaru problemu w sytuacji, gdy ten ostatni
wzrasta nieograniczenie, nie jest istotna dokładna formuła tej zależności, lecz jej zachowanie
asymptotyczne. Pomijamy w niej stałe elementy i inne, pozostawiając jedynie najbardziej
znaczące składniki f
ormuły, czyli te, które decydują o zachowaniu się formuły, gdy rozmiar n
dąży do nieskończoności.

Reasumując, istotną częścią informacji, która jest zawarta w funkcji złożoności czasowej

W(n) jest jej rząd wielkości, czyli jej zachowania asymptotyczne, gdy n dąży do
nieskończoności. Poniżej omówione będą pewne pojęcia związane z tym zagadnieniem.

Niech f, g, h : N→R

+

{0}, gdzie N - zbiór liczb naturalnych

R

+

- zbiór liczb rzeczywistych dodatnich

będą trzema dowolnymi funkcjami.

NOTACJA O

Mówimy, że funkcja f jest co najwyżej rzędu funkcji g, co zapisujemy jako

f(n)= O (g (n))

jeżeli istnieje stała rzeczywista c>0 i stała naturalna n

0

takie, że

f(n)

≤≤≤≤

c

⋅⋅⋅⋅

g(n), dla każdego n

n

0

c

⋅⋅⋅⋅

g(n)

f(n)

n

o

n

background image

f(n) = O(g(n))

background image

Np.

Dla (n) = n

2

+2n mamy, że n

2

+2n=O(n

2

), bo istnieje stała c=3>0 i stała

naturalna n

o

=1 takie, że

n

2

+2n

≤≤≤≤

3

⋅⋅⋅⋅

n

2

= 3g(n) dla każdego n

1

i g(n)= n

2

Czyli f(n) = n

2

+2n jest co najwyżej rzędu O(n

2

)

NOTACJA

Mówimy, że funkcja f jest co najmniej rzędu funkcji g, co zapisujemy jako

f(n) =

(g(n))

jeżeli istnieje stała c>0 i stała naturalna n

o

, takie, że

c

⋅⋅⋅⋅

g(n)

≤≤≤≤

f(n) dla każdego n

≥≥≥≥

n

o .

Inaczej mówiąc: g(n) = O(f(n))

f(n) =

(g(n))

NOTACJA

Θ

Mówimy, że funkcja f jest dokładnie rzędu funkcji g, co zapisujemy jako

f(n) =

Θ

Θ

Θ

Θ

(g(n))

jeżeli istnieja stałe c

1

>0, c

2

>0 oraz stała naturalna n

o,

takie, że

c

1

⋅⋅⋅⋅

g(n)

≤≤≤≤

f(n)

≤≤≤≤

c

2

⋅⋅⋅⋅

g(n)dla każdego n

n

o

Czyli inaczej f(n) = O(g(n)) i f(n) =

(g(n)).

f(n) =

Θ

Θ

Θ

Θ

(g(n))

c

⋅⋅⋅⋅

g(n)

f(n)

n

o

n

c

2

⋅⋅⋅⋅

g(n)

f(n)

n

o

n

c

1

⋅⋅⋅⋅

g(n

)

background image

Np. Dla funkcji

3n

2

n

2

1

f(n)

=

mamy, że

( )

2

n

3n

2

n

2

1

f(n)

Θ

=

=

ponieważ istnieją stałe c

1

>0,

c

2

>0 i n

o

, dla których

2

n

2

c

3n

-

2

n

2

1

2

n

1

c

dla każdego n

n

o

Można bowiem sprawdzić, że dla

7

o

n

,

2

1

2

c

,

14

1

1

c

=

=

=

zachodzi nierówność

7.

n

dla

2

n

2

1

3n

-

2

n

2

1

2

n

14

1

Do porównania rzędów wielkości dwóch danych funkcji f(n) i g(n) można wykorzystać

obliczenia następującej granicy:

g(n)

f(n)

lim

E

n

=

Jeżeli

E=+∞, to g(n) = O(f(n)) ale NIE f(n)=O(g(n))
E=c>0, to f(n) =

Θ

Θ

Θ

Θ

(g(n))

E=0, to f(n) = O(g(n))

ale NIE g(n)=O(f(n)).

Np. Dla f(n)=nlogn, g(n)=n

2

0

2

n

nlogn

lim

g(n)

f(n)

lim

n

n

=

=

czyli n·logn=O(n

2

) ale nie n

2

=O(n logn)

Większość ze znanych algorytmów ma złożoność czasową należącą do podanego niżej

wykazu. To, że funkcji tych jest raczej mało, wynika z własności „pochłaniania” przez „duże
O
” stałych współczynników i wyrazów niższego rzędu.

Np. Funkcje:

n

3

,

5 n

3

+2 n

2

-100n+5, 6 n

3

+4·n·log(n)+2

są rzędu O(n

3

)

Wykaz podstawowych złożoności czasowych

logn

złożoność logarytmiczna

np. algorytmy, w których zadanie rozmiaru n sprowadzane jest do
zadania rozmiaru n/2, plus pewna stała liczba działań

# np. przeszukiwanie binarne

n

złożoność liniowa

np. algorytmy, w których dla każdych n-elmentowych danych
wejściowych wykonywana jest stała liczba działań

background image

# np. algorytm Hornera obliczania wartości wielomianu

n

⋅⋅⋅⋅

logn

złożoność nlogn (liniowo-logarytmiczna)

np. algorytmy, w których zadanie rozmiaru n zostaje sprowadzone do
dwóch podzadań rozmiaru n/2, plus pewna liczba działań liniowa
względem rozmiaru n, potrzebnych do wykonania najpierw rozbicia a
następnie scalenia rozwiązań podzadań rozmiaru n/2 w rozwiązanie
rozmiaru n

# np. sortowanie przez scalanie

n

2

złożoność kwadratowa

np. algorytmy, w których jest wykonywana pewna stała liczba działań
dla każdej pary danych wejściowych (mają podwójną iterację

# np. wyzerowanie elementów tablicy 2-wymiarowej

n

3

, n

4

dalsze złożoność wielomianowe

logn

n

złożoność podwykładnicza

2

n

złożoność wykładnicza

np. algorytmy, w których jest wykonywana stała liczba działań, dla
każdego podzbioru danych wejściowych

n!

złożoność wykładnicza n!

np. algorytmy, w których jest wykonywana stała liczba działań, dla
każdej permutacji danych wejściowych
.

W celu uświadomienia sobie jak różne rodzaje złożoności czasowej wpływają na sens

stosowania ich do rozwiązywani problemów o dużym rozmiarze przeanalizujmy następujące
zestawienia.

Przyjmijmy, że komputer, na którym wykonujemy te algorytmy może wykonać 1 milion

operacji na 1 sek.

n=10

n=20

n=30

n=40

n=50

n

3

0,001s

0,008s

0,027s

0,064s

0,125s

2

n

0,001s

1,05s

17,9m

1,27d

35,7r

3

n

0,059s

58,1m

6,53r

3,86·10

5

r

2,28·10

10

r

n!

3,63s

7,71·10

4

r

8,41·10

18

r

2,59·10

34

r

9,64·10

50

r

dla n!=24! czas jest większy niż obecny wiek wszechświata.

background image

3.Rekurencja (rekursja)

Wiele ważnych algorytmów ma strukturę rekurencyjną, którą można opisać

następująco:

W celu rozwiązania danego problemu algorytm wywołuje sam siebie w sposób

bezpośredni lub pośredni.

Zastosowanie rekursji często prowadzi do bardziej zrozumiałych i zwięzłych opisów

algorytmów, niż było to możliwe bez rekursji. Do obliczenia złożoności czasowej algorytmów
rekurencyjnych stosujemy równania rekurencyjne. Przykład takiego równania wraz z
rozwiązaniem będzie podany w dalszej części.

Często zdarza się, że w celu rozwiązania pewnego problemu dzielimy go na podproblemy.

Następnie znajdujemy rozwiązania podproblemów – są one prostsze niż rozwiązania problemu
złozonego- i za ich pomocą znajdujemy rozwiązanie problemu wyjściowego.

Metoda ta, zwłaszcza stosowana rekurencyjnie, prowadzi często do efektywnych

algorytmów rozwiązujących takie problemy, których podproblemy mają identyczną postać, ale
są za to mniejszego rozmiaru.

Taka metoda konstrukcji algorytmów nazywana jest metodą „dziel i zwyciężaj” (ang. divide

and conquer).

Składa się ona z trzech etapów:

1) DZIEL : dzielimy problem na podproblemy
2) ZWYCIĘŻAJ : rozwiązujemy podproblemy rekurencyjnie
3) POŁĄCZ : łączymy rozwiązania podproblemów, aby otrzymać rozwiązanie

wyjściowego problemu.

Technikę „dziel i zwyciężaj” zilustrujemy dwoma przykładami:

Przykład 1

Problem: Dana jest tablica n liczb całkowitych a[1], ..., a[n].

Czy w tablicy występuje element x ?

Rozwiązanie:

Dziel:

Podzielimy tablice na dwie części:
CZĘŚĆ I:

pierwszy element tablicy, tzn. a[1]

CZĘŚĆ II:

pozostałe (n-1) elementów, tzn. a[2], ..., a[n]

Zwyciężaj: Jeżeli pierwszy element jest równy x, to wynikiem jest napis „TAK

WYSTĘPUJE”

i kończymy obliczenia,

w przeciwnym razie

sprawdzamy, czy x występuje w pozostałych n-1 elementach (czyli w

części II)

background image

Podane zostały warunki tylko pozytywnego zakończenia problemu. W przypadku, gdy

przebadaliśmy całą tablicę i element x nie został znaleziony, to działanie algorytmu powinno się
zakończyć wraz z podaniem napisu „NIE ZNALEZIONO”.

Realizacja rozwiązania problemu w postać programu może być następująca.

SZUKAJ ([a

1

, ..., a

n

], lewy, prawy, x)

{lewy- oznacza lewą, prawy- oznacza prawą granicę przeszukiwanego obszaru

tablicy;

na poczatku lewy=1, prawy=n}

początek

jeżeli lewy > prawy to „BRAK W CIĄGU”
w przeciwnym razie

jeżeli a[lewy]=x to „JEST W CIĄGU”
w przeciwnym razie SZUKAJ([a

1

, ..., a

n

], lewy+1, prawy, x)

koniec

Warunkiem zakończenia algorytmu jest:

albo znalezienie szukanego elementu :( a[lewy]=x )
albo wyjście poza obszar poszukiwany :(lewy>prawy).

Przykładowe realizacjealgorytmów dla dwóch zestawów danych:
1) dla ciągu: 0, 7, 8, 5 i szukanego elementu: x=8 dostajemy

SZUKAJ ([0,7,8,5],1,4,8) {ciąg,lewy=1, prawy=4, x=8}

1>4

Nie ( czy lewy>prawy ? )
0=8

Nie ( czy a[1]=x ? )
SZUKAJ ([0,7,8,5],2,4,8)

2>4

Nie ( czy lewy>prawy ?)
7=8

Nie ( czy a[2]=x ? )
SZUKAJ ([0,7,8,5],3,4,8)

3>4

Nie ( czy lewy>prawy ? )
8=8

Tak ( czy a[3]=x ? )

↓↓↓↓

” JEST W CIĄGU”

2) dla ciągu : 0, 7, 8, i szukanego elementu: x=5 dostajemy:

SZUKAJ ([0,7,8],1,3,5) {ciąg,lewy=1, prawy=3, x=5}

1>3

Nie ( czy lewy>prawy ? )
0=5

Nie ( czy a[1]=x ? )
SZUKAJ ([0,7,8],2,3,5)

2>3

Nie ( czy lewy>prawy ?)

background image

7=5

Nie ( czy a[2]=x ? )
SZUKAJ ([0,7,8],3,3,5)

3>3

Nie ( czy lewy>prawy ? )
8=5

Nie ( czy a[3]=x ? )

SZUKAJ ([0,7,8],4,3,5)

4>3

Tak

( czy lewy>prawy ? )

↓↓↓↓

” BRAK W CIĄGU”

Algorytm ten posiada dwie ważne cechy algorytmów rekurencyjnych:

1) zakończenie rekurencji jest jasno określone:

- znaleziono element ( a[lewy]=x )

lub

- przekroczono zakres tablicy ( lewy>prawy )

2) duży problem został rozbity na dwa mniejsze

1-szy - elementarny
2-gi - na analogiczny problem, tylko o mniejszym rozmiarze

Przykład 2

Problem:

Znaleźć największy i najmniejszy element w zbiorze n-elementowym S. Załóżmy
dodatkowo, że n jest potęgą liczby 2.

Rozwiązanie:

DZIEL:

Podziel zbiór S na dwa podzbiory S

1

i S

2

, z których każdy zawiera n/2

elementów.

ZWYCIĘŻAJ:

Znajdź minimalne i maksymalne elementy w zbiorach S

1

i S

2

a następnie

wybierz element mniejszy z dwóch znalezionych minimów i element
większy
z dwóch znalezionych maksimów.

Procedura realizująca ten algorytm może mieć następującą formę:

MAXMIN(S);
{ S- dany zbiór elementów }
początek

1. jeżeli |S|=2 wtedy { |S| oznacza moc zbioru S }

początek

2.

{ niech S={a, b} }

3.

powrót (MAX(a,b), MIN(a,b))

koniec

w przeciwnym razie
początek

4.

podziel S na dwa rozłączne równoliczne podzbiory S

1

i S

2

,

5.

(max1, min1):= MAXMIN(S

1

);

6.

(max2, min2):= MAXMIN(S

2

);

7.

powrót (MAX(max1, max2), MIN(min1, min2)

background image

koniec

koniec

Zauważmy, że wywoływanie rekurencji zostaje zastopowane, gdy zbiór, który mamy

podzielić, zawiera dokładnie 2 elementy!

Obliczymy teraz funkcję złożoności czasowej. Jako operację dominującą przyjmijmy

porównanie dwóch elementów zbioru S. Porównania te wykonują się w instrukcji 3 i 7.

Oznaczmy przez T(n) liczbę porównań między elementami zbioru S, które wykonuje

procedura MAXMIN w celu znalezienia maksymalnego i minimalnego elementu w zbiorze n-
elementowym.

Mamy

T(2)=1 dla n=2. (instrukcja 3)
natomiast dla n>2, T(n) jest równe całkowitej liczbie porównań wykonanych w
dwu wywołaniach MAXMIN (linie 5 i 6) dla zbiorów rozmiaru n/2, plus dwa
porównania
z linii 7.;l

Dostajemy, więc

>

+

=

=

2

n

dla

2

2T(n/2)

2

n

dla

1

T(n)

Można pokazać, ze rozwiązaniem tego równania rekurencyjnego jest funkcja:

.

2

n

)

n

(

T

2

3

=

a więc T(n)=O(n).

Klasycznym przykładem algorytmu rekurencyjnego jest obliczanie funkcji SILNIA (

ozn. n! )

Definicja funkcji n! jest następująca:

0! = 1
n! = 1

2

.....

(n-1)

n

co można łatwo zapisać w postaci rekurencyjnej:

<

=

0

n

dla

!

1)

(n

n

0

n

dla

1

n!

Przykładowy algorytm można przedstawić następująco:

Funkcja SILNIA(n);

początek

jeżeli n

0 wtedy pisz (1)

w przeciwnym wypadku

pisz (n*SILNIA(n-1))

koniec

Schemat realizacji funkcji SILNIA(3) jest następujący.

background image

SILNIA(3)

3

0 NIE 3!=3

2!

2

0 NIE 2!=2

1!

1

0 NIE 1!=1

0!

0

0 TAK

Dla n=0 nie następuje wywołanie rekurencyjne,
Dla n>0 kolejne wywołania odbywają się dla malejących systematycznie wartości argumentu
aż do wywołania z argumentem 0, który zatrzymuje całą rekursję.

OGÓLNIE:

zależność, której spełnienie przez algorytm wywołania gwarantuje uniknięcie dalszej
rekursji, nazywamy przypadkiem bazowym (ang. base case), lub warunkiem stopu rekursji
(ang. stopping case).

Jedną z konsekwencji każdego wywołania rekursywnego jest zajęcie pewnego obszaru
pamięci, który jest wykorzystywany przez stos, na którym zapamiętywane są wyniki kolejnych,
jeszcze nie zakończonych wywołań procedury. Stos podzielony jest na fragmenty stosu, będące
blokami kolejnych miejsc pamięci- każdy fragment zawiera dane o jednym trwającym
wywołaniu procedury. W przypadku bardzo dużej liczby wywołań rekurencyjnych szybko może
nastąpić przepełnienie stosu, a w konsekwencji przerwanie pracy procedury.

Przykładem takiej „kłopotliwej” procedury rekurencyjnej jest procedura generowania n-

tego wyrazu ciągu Fibonacciego

Ciąg Fibonacciego

Definicja ciągu Fibonacciego jest następująca:

f(0) = 1
f(1) = 1
f(n) = f(n-1) + f(n-2)

dla n

2.

Przykładowe początkowe wyrazy:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

Rekurencyjna funkcja obliczającą n-ty wyraz tego ciągu ma postać:

Funkcja Fibo(n);

początek

jeżeli n

1 wtedy Fibo:=1

w przeciwnym razie Fibo:=Fibo(n-1) + Fibo(n-2)

koniec;

3!=3

⋅⋅⋅⋅

2=6

↑↑↑↑

2!=2

⋅⋅⋅⋅

1=2

↑↑↑↑

1!=1

⋅⋅⋅⋅

1=1

↑↑↑↑

0!=1

background image

Prześledźmy schemat wywołań funkcji Fibo dla n=4

F(4)

F(3)

F(2)

F(2)

F(1)

F(1)

F(0)

F(1)

F(0)

F(4) = F(3) + F(2) = F(2) + F(1) + F(1) + F(0) = F(1) + F(0) + F(1) + F(1) + F(0) = 2F(0) +

3F(1) =
= (1+1) + (1+1+1) = 2 + 3 = 5

Widać tu wyraźnie, że pewna część obliczeń wywoływana jest wielokrotnie - 3-razy F(1) i 2-
razy F(0).
Algorytm ten ma złożoność czasową:

T(n)=(1,6)

n

. Przyczyną tak dużej złożoności

czasowej jest wielokrotne obliczanie tych samych wartości. Np.

dla n=30 wyraz F(30) ma 832040 wywołań funkcji F(0) i F(1),

natomiast odpowiadający mu algorytm iteracyjny ma tylko 30 dodawań !!!

Problem stopu rekursji

W wielu algorytmach rekurencyjnych, z pozoru poprawnie skonstruowanych, może być

ukryty poważy błąd polegający na nieskończonej ilości wywołań rekurencyjnych, czyli błędnie
sformułowanego warunku stopu.

Przeanalizujmy nastepujący przykład:

Funkcja Dokoncaswiata(n);

początek

jeżeli n=1 wtedy pisz(1)
w przeciwnym przypadku

jeżeli n mod 2=0

{ n jest parzysta }

wtedy pisz( Dokoncaswiata(n-2)*n )
w przeciwnym razie pisz(

Dokoncaswiata(n-1)*n )

koniec;

Z pozoru wszystko jest poprawne, bowiem:

1) jest przypadek bazowy (stop)
2) problem rzędu n jest upraszczany do problemu o rozmiarze niższym: n-1 lub n-2.

Dokładna analiza pokazuje jednak, że dla n

≥≥≥≥

2 wszystkie wywołania funkcji kończą się

parzystą wartością n, a to oznacza, że po dojściu do n=2 przejdziemy kolejno do n=0, potem n=-
2
, itd. Nigdy, więc nie znajdzie się przypadek bazowy ( n=1 ), który zatrzyma rekursję.

background image

Wniosek:

Należy zawsze sprawdzać, czy dla wartości parametrów wejściowych
należących do dziedziny wartości, które mogą być użyte, rekurencja kiedyś się
skończy
.

NWP - rekurencyjny algorytm Euklidesa

Na zakończenie rozważań o rekurencji omówimy jeden z klasycznych algorytmów

rekurencyjnych, którym jest algorytm Euklidesa obliczający największy wspólny podzielnik
dwu liczb naturalnych.

Definicja

Największym wspólnym podzielnikiem ( ozn. NWP ) dwu liczb całkowitych nazywamy

największą liczbę będącą podzielnikiem ich obu.

Poniższa zależność rekurencyjnia podana przez Euklidesa w IV wieku p.n.e., jest podstawą

obliczenia NWP:

=

=

0

n

mod

m

gdy

n)

mod

m

NWP(n,

0

n

mod

m

gdy

n

n)

NWP(m,

gdzie m, n >0.

Przykład:

Obliczyć NWP dla dwóch licz całkowitych: 243 i 114.

Mamy

NWP(243,114)= NWP(114,15)= NWP(15,9)= NWP(9,6)= NWP(6,3)= NWP(3,0)= 3

Postać pseudokodu funkcji EUKLID realizującej NWP :

Funkcja EUKLID(m,n);
{zakładamy, że m

0, n

0}

początek

jeżeli n=0 wtedy EUKLID:=m
w przeciwnym razie

EUKLID:= EUKLID (n, m mod n)

koniec;

Np. Dla m=30 i n=31 kolejne wywołania funkcji EUKLID sa następujaceL

EUKLID(30,21) " EUKLID(21,9) " EUKLID(9,3) " EUKLID(3,0) = 3

Mamy tu trzy rekurencyjne wywołania funkcji EUKLID. Ciąg wywołań nie może być

nieskończony, gdyż za każdym wywołaniem zmniejsza się wartość drugiego argumentu.

Zatem funkcja EUKLID zawsze się kończy i oblicza poprawny wynik.

Liczba wywołań rekurencyjnych funkcji EUKLID dla m>n>0 wynosi: O(log b).

4. Podstawowe struktury danych

background image

Dane, z którymi pracują algorytmy, są zbiorami, na których dozwolone są pewne operacje.

Rodzaj tych operacji określa wewnętrzna organizacja danego zbioru. Ze względu na to, że
operacje te mogą powiększać, zmniejszać lub zmieniać ich zawartość, zbiory takie nazywamy
zbiorami dynamicznymi.

Operacje wykonywane na zbiorach dynamicznych można podzielić na dwie grupy:

Zapytania – czyli operacje, które pozwalają uzyskać wyłącznie pewne informacje na

temat zbioru

Operacje modyfikujące - czyli operacje, które mogą zmieniać zbiór

Lista typowych operacji na zbiorach dynamicznych:

Oznaczenia: S- zbiór, x - element zbioru S, k- wartość klucza dla elementu zbioru S

SZUKAJ(S,k)

Zapytanie, które dla zbioru S oraz wartości klucza k, daje w
wyniku albo element x ze zbioru S, o kluczu równym k, albo NIC,
gdy taki element nie należy do S

DOPISZ(S,x)

Operacja modyfikująca, która dodaje element x do zbioru S

USUŃ(S,x)

Operacja modyfikująca, która usuwa element x ze zbioru S

MINIMUM(S)

Zapytanie, które podaje element ze zbioru S o najmniejszej
wartości klucza

MAKSIUM(S)

Zapytanie, które podaje element ze zbioru S o największej
wartości klucza

NASTĘPNIK(S,x)

Zapytanie, które podaje następnik elementu x w zbiorze S lub
NIC, gdy x jest największym w S

POPRZEDNIK(S,x)

Zapytanie, które podaje element poprzedzający x w zbiorze S
lub NIC, gdy x jest najmniejszym w S

Uwaga: Dla operacji typu ZAPYTANIE zakładamy, że zbiór S jest liniowo uporządkowany.

Operacja SZUKAJ może być wykonywana również na zbiorze nieuporządkowanym.

A)

Listy

Lista jest to skończony ciąg elementów

q=[x

1

, x

2

, ... x

n

],

czyli elementy ułożone w porządku liniowym. Porządek ten określają wskaźniki związane z
każdym elementem listy.

Skrajne elementy listy x

1

i x

n

nazywane są końcami listy (x

n

-lewym, x

n

-prawym), natomiast

wielkość |q|=n - długością listy (lub rozmiarem listy).

Szczególnym przypadkiem jest lista pusta: q=[] o długości 0.

Niech dane będą dwie listy:

background image

q=[x

1

, x

2

,... x

n

] , r=[y

1

, y

2

,... y

m

] oraz 0

i

j

n

Podstawowymi operacjami na listach są:

dostęp do elementu listy:

q[i]=x

i

podlista

q[i..j]=[x

i,

x

i+1

,... x

j

]

złożenie (konkatenacja) list

qr=[x

1,

...x

n

, y

1

,..., y

m

]

Obsługa listy ogranicza się do operacji wykonywanych na jej końcach:

a) front(q)=q[1]

- pobieranie lewego końca listy

b) push(q,x)=[x]&q

- wstawienie elementu x na lewy koniec listy

c) pop(q)=q[2...|q|]

- usunięcie bieżącego lewego krańca listy

d) rear(q)=q(|q|)

- pobieranie prawego końca listy

e) inject(q,x)= q&[x]

- wstawienie elementu x n prawy koniec listy

f) eject(q)= q[1,...,|q|-1] -usunięcie bieżącego prawego końca listy

Do budowy listy używane są dwa typy komórek:

Typ INFO;

-

jest komórką informacyjną zawierającą dwa wskaźniki: do początku i do końca
listy.

Typ ELEMENT

-

jest

komórką roboczą i zawiera dwa pola:

wartość elementu

wskaźnik do następnego elementu listy

Lewy koniec
(głowa)

???

Wartość

Prawy koniec
(ogon)

???

Gdzie
następny

???

Typ

INFO

Typ ELEMENT

W zależności od wykazu dozwolonych operacji rozróżniamy różne rodzaje list:

1) lista jednokierunkowa

25

475

3

NIC

głowa

ogon

INFO

ELEMENT

x

1

x

2

....

x

n

operacje: front, push, pop

2) lista jednokierunkowa cykliczna

background image

S

125

-42

operacje: front, push, pop, rear, inject

3) lista dwukierunkowa

18

8

125

45

NIC

NIC

3) lista dwukierunkowa cykliczna

5

8

40

50

x

1

x

2

....

x

n

5) STOS (lista jednokierunkowa)

Dozwolone operacje

dopisanie elementu od strony wierzchołka stosu PUSH

usunięcie elementu z wierzchołka stosu POP

Zasada LIFO

Last-In-First-Out

Stos pusty

push(5)

push(15)

pop(15)

15

5

5

5

6) Kolejka (lista jednokierunkowa)

Dozwolone operacje
-

pobieranie elementu z dołu ( z czoła kolejki )

-

dopisanie elementu od strony wierzchołka PUSH

-

usunięcie elementu od strony czoła INJECT

Zasada FIFO

First -In-First-Out

x

1

x

2

....

x

n

background image

kolejka pusta

push(5)

push(15)

15

5

5

15

Inject (5)

B) DRZEWA

Drzewo (ang. tree) jest strukturą podobną do listy jednokierunkowej, z tą różnicą, że każdy

element może mieć dowiązania do więcej niż jednego swojego następnika.
Na przykład:

A

B

C

D

E

F

G

H

I

J

Definicja drzewa (rekurencyjna)

Drzewem nazywamy

strukturę pustą (drzewo puste)

lub

węzeł, zwany korzeniem drzewa (ang. root), połączony z zerem lub większą liczbą drzew
zwanych poddrzewami (ang. subtrees)

Podstawowe terminy dotyczące drzew:

Korzeń

- wyróżniony wierzchołek

Gałąź

- dowolne połączenie pomiędzy węzłami

Liść

- węzeł nie mający poddrzew

Węzeł wewnętrzny - węzeł nie będący liściem.

Dla dwóch węzłów połączonych gałęzią zachodzą powiązania:

Węzeł rodzicielski:

ten, z którego gałęzi wychodzi

Węzeł potomny:

ten, do którego gałęzi wchodzi

Korzeń jest jedynym węzłem, który NIE MA węzła rodzicielskiego.

Wszystkie węzły leżące na drodze od korzenia do danego węzła nazywane są przodkami

tego ostatniego.

A

B

Rodzic dla B (poprzednik B, ojciec B)

Potomek A (następnik A, syn A)

background image

Węzły, dla których dany węzeł pełni rolę przodka nazywane są jego potomkami.
Bezpośredni potomkowie węzła nazywani są jego synami, on zaś nazywany jest ich ojcem.

Pomiędzy węzłami - synami tego samego węzła ojca - istnieje relacja rodzeństwa, a ich określa
się braćmi .

Stopniem węzła nazywamy liczbę jego następników, natomiast stopniem drzewa -

największą liczbę spośród stopni jego węzłów.

Głębokością węzła nazywamy liczbę większą o 1 od liczby jego przodków. Głębokość

drzewa (lub wysokość drzewa) to maksymalna spośród głębokości jego węzłów.

korzeń

Głębokość 1

A

B

C

D

Głębokość 2

gałęzie

E

F

Głębokość 3

H

I

J

liście

Głębokość 4

Synowie E

: H,I,J

Ojciec F

: D (zawsze TYLKO JEDEN)

Bracia

: H,I,J

Stopień E

: 3

Stopień drzewa

: 3

Głębokość F

: 3

Głębokość drzewa : : 4

Elementy składowe przykładowego drzewa

Drzewo stopnia 2 nazywane jest często drzewem binarnym. Struktura organizacji

dowolnego wierzchołka takiego drzewa jest następująca:

Adres ojca

Adres lewego syna

Adres prawego syna

Wartość węzła

Sposoby przeglądania (odwiedzania) wierzchołków drzewa

Metoda PREORDER

1. odwiedź korzeń drzewa
2. odwiedź lewe poddrzewo (jeżeli istnieje) metodą PREORDER
3. odwiedź prawe poddrzewo (jeżeli istnieje) metodą PREORDER

czyli Korzeń-Lewe-Prawe ( K-L-P)

Metoda INORDER

1. odwiedź lewe poddrzewo (jeżeli istnieje) metodą INORDER

background image

2. odwiedź korzeń drzewa
3. odwiedź prawe poddrzewo (jeżeli istnieje) metodą INORDER

czyli Lewe-Korzeń-Prawe ( L-K-P )

Metoda POSTORDER

1. odwiedź lewe poddrzewo (jeżeli istnieje) metodą POSTORDER
2. odwiedź prawe poddrzewo (jeżeli istnieje) metodą POSTORDER
3. odwiedź korzeń drzewa

czyli Lewe-Prawe-Korzeń ( L-P-K )

Metoda LEVELORDER

1. odwiedź korzeń drzewa
2. odwiedź następniki korzenia od lewej do prawej
3. odwiedź wnuki korzenia od lewej do prawej
4. itd...

Przykład

A

B

C

D

E

F

G

PREORDER =

A-B-D-E-C-F-G

( K-L-P )

1

2

5

3

4

6

7

INORDER

=

D-B-E-A-F-C-G

( L-K-P )

4

2

6

1

3

5

7

POSTORDER

= D-E-B-F-G-C-A

( L-P-K )

7

3

6

1

2

4

5

LEVELORDER

= A-B-C-D-E-F-G

background image

1

2

3

4

5

6

7


Wyszukiwarka

Podobne podstrony:
Algorytmy i struktury danych id Nieznany (2)
Algorytmy i struktury danych wy Nieznany (2)
Algorytmy i struktury danych Wykład 1 Reprezentacja informacji w komputerze
Algorytmy i struktury danych Wykład 3 i 4 Tablice, rekordy i zbiory
Algorytmy i struktury danych, AiSD C Lista04
Algorytmy i struktury danych 08 Algorytmy geometryczne
Instrukcja IEF Algorytmy i struktury danych lab2
Algorytmy, struktury danych i techniki programowania wydanie 3
Algorytmy i struktury danych 1
Ściaga sortowania, algorytmy i struktury danych
ukl 74xx, Informatyka PWr, Algorytmy i Struktury Danych, Architektura Systemów Komputerowych, Archit
cw 0 1, pwr, informatyka i zarządzanie, Informatyka, algorytmy i struktury danych
AIDS w7listy, studia, Semestr 2, Algorytmy i struktury danych AISD, AIDS
k balinska projektowanie algorytmow i struktur danych
W10seek, studia, Semestr 2, Algorytmy i struktury danych AISD, AIDS
ALS - 001-000 - Zadania - ZAJECIA, Informatyka - uczelnia, WWSI i WAT, wwsi, SEM II, Algorytmy i Str
kolokwium1sciaga, Studia Informatyka 2011, Semestr 2, Algorytmy i struktury danych

więcej podobnych podstron