background image

1

Algorytmy i struktury danych

wykład 2

Algorytmy rekurencyjne

background image

2/41

Definicja i cechy algorytmu 

rekurencyjnego

algorytm, który odwołuje się do samego siebie 
(funkcja/procedura wywołuje siebie w czasie 
wykonywania)

duży problem zostaje rozłożony na tzw. problem 
elementarny (który umiemy rozwiązać) i problem o 
mniejszym stopniu komplikacji niż problem wyjściowy

podejście rekurencyjne jest zgodne z podejściem 
człowieka do rozwiązania wielu problemów

zakończenie algorytmu jest precyzyjnie określone

background image

3/41

Definicja algorytmu 

rekurencyjnego

start
procedura rekurencyjna

warunek

jeżeli tak → instrukcje
jeżeli nie → procedura rekurencyjna

koniec procedury
wywołanie procedury rekurencyjnej
koniec

background image

4/41

Przykład klasyczny czyli silnia

zadanie → obliczenie wartości n!

definicja matematyczna silni →

0! = 1

n! = n

(n-1)!

n jest liczbą naturalną, n ≥ 1

background image

5/41

Funkcja obliczająca wyrażenie n! 

– algorytmy iteracyjne

int n;

cout<<„podaj n: ”;

cin>>n;

int silnia=1;

for(int i=1;i<=n;i++)

{

silnia=silnia*i;

}

cout<<"\n" <<n <<"!=" 

<<silnia<<endl;

int silnia=1;

int i=1;

while(i<=n)

{

silnia=silnia*i;

i++;

}

cout<<"\n" <<n <<"!=" 

<<silnia<<endl;

background image

6/41

Analiza funkcji silnia –

algorytm rekurencyjny

problem zostaje rozłożony na dwa: elementarny (jeżeli 
n=0) oraz (jeżeli n>0) problem podobny jak wyjściowy, 
ale o mniejszym stopniu trudności → (n-1)! zamiast n!

koniec algorytmu następuje, gdy funkcja zagłębi się w 
sobie tyle razy, że w końcu obliczy wartość elementarną
silnia

wyniki cząstkowe nie mogą zostać obliczone w chwili 
wywołania kolejnego egzemplarza

w każdym kroku funkcja czeka na wynik następnego kroku 
i obliczenie wartości jest wstrzymywane

background image

7/41

Analiza funkcji silnia dla n=4

n=0 ? → nie

oblicz

4*3!

n=0 ? → nie

3*2!

n=0 ? → nie

2*1!

n=0 ? → nie

1*0!

n=0 ? → tak

1

background image

8/41

Funkcja obliczająca wyrażenie n! –

algorytm rekurencyjny

int fun_silnia(int n)

{

int silnia;

if(n==0) silnia=1;

else silnia=n*fun_silnia(n-1);

return silnia;

}

main()

{

int n;

cout<<"podaj n: "; cin>>n;

cout<<"\n" <<n <<"! = " <<fun_silnia(n);

return 0;

}

wewnętrzne 

wywołanie funkcji 

silnia

koniec funkcji silnia

background image

9/41

Obliczenie wyrażenia 4!

background image

10/41

Obciążenie pamięci

4*3!

4*3!

4*3!

4*3!

4*3!

4*3!

4*3!

3*2!

3*2!

3*2!

3*2!

3*2!

2*1!

2*1!

2*1!

1*0!

Algorytmy rekurencyjne określa się często mianem „pamięciożernych”, 

ponieważ poszczególne obliczenia cząstkowe są wstrzymywane aż do 

obliczenia tzw. zadania elementarnego. Dopiero wówczas realizowane 

są poszczególne, wstrzymane wcześniej, zadania cząstkowe i 

zwalniana jest zarezerwowana pamięć.

background image

11/41

Ciąg Fibonacciego - definicja

fib(0) = 0
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2) dla n ≥ 2

Pierwsze wyrazy: 0, 1, 1, 2, 3, 5, 8, 13

background image

12/41

Schemat  obliczania wyrazów 

ciągu Fibonacciego

fib(4)

fib(3)

fib(2)

fib(2)

fib(1)

fib(1)

fib(0)

fib(1)

fib(0)

fib(1)

fib(2)

→ obliczenia powtarzane

→ operacje elementarne

background image

13/41

Ciąg Fibonacciego - program

int fibonacci(int n)

{

int fib;

if(n==0) fib=0;

else if (n==1) fib=1;

else fib=fibonacci(n-1)+fibonacci(n-2);

return fib;

}

main()

{

int n;

cout<<"podaj n: "; cin>>n;

cout<<"\nfib("<<n<<") = " <<fibonacci(n);

return 0;

}

background image

14/41

Schemat  obliczania wyrazów 

ciągu Fibonacciego

pewne części obliczeń powtarzają się wielokrotnie

program „nie zauważa”, że pewne operacje 
obliczenia już wykonywał i nie korzysta z ich 
wyników

optymalne wykorzystanie algorytmu →
programowanie dynamiczne, utworzenie 
specjalnych tablic, przechowujących wyniki 
pośrednie, które można w każdej chwili 
wykorzystać

background image

15/41

Cechy algorytmu 

wyszukiwania

określone zakończenie algorytmu →
indeks left>right, lub element zostanie 
wyszukany

duży problem rozłożony na sumę
problemu elementarnego oraz problemu 
podobnego ale o niższym stopniu 
trudności

background image

16/41

Algorytm wyszukiwania

int tab1[8];

void szukaj(int t[]; int left, int right, int );

{

if (left>right) cout<<„nic nie wyszukano”;

else

if (tab[left]==wart)

cout<<„wartosc=”<<wart<<„ na pozycji: ”<<left;

else szukaj(tab, left+1, right, wartosc);

}

main()

{

for(int i=0;i<8;i++)

{ tab1[i]=i*10+10; cout<<tab1[i]; }

wartosc=50; szukaj(tab1,0,7,wartosc);

}

background image

17/41

Błędy popełniane przy używaniu 

algorytmów rekurencyjnych 

niewłaściwie sformułowany warunek 
końca

niepoprawne rozłożenie problemu

background image

18/41

Przyczyny zawieszania się

programów

nielegalne użycie zasobów

pętla nieskończona

brak pamięci

nieprawidłowe lub nieprecyzyjne 
określenie warunków zatrzymania 
algorytmu

błąd w algorytmie powodujący zbyt 
wolną pracę algorytmu

background image

19/41

Niekorzystne cechy algorytmów  

rekurencyjnych

brak możliwości obliczenia liczby 
zagłębień w siebie algorytmu → brak 
możliwości przewidywania czasu pracy 
algorytmu

możliwość skonstruowania algorytmu o 
nieskończonej liczbie wywołań

background image

20/41

Przykład algorytmu o 

nieskończonej liczbie operacji

int sprawdz(int liczba)

{

if(liczba==1) return 1;

else

{

if(liczba % 2)==0 

return liczba*sprawdz(liczba-2);

else return liczba*sprawdz(liczba-1);

}

}

main()

{

sprawdz(6);...

}

operacja „liczba % 2” sprawia, że program 
nigdy nie osiągnie wartości 1, która kończy 
jego działanie!

background image

21/41

Program dobry, wykonania 

brak

int policz(int n1, int n2)

{

if (n1==0) return 1;

else

return policz(n1-1,policz(n1-n2,n2))

}

main()

{

policz(1,0);

...

}

parametry funkcji rekurencyjnej obliczane s

ą

jako pierwsze, potem wywo

ływana jest 

funkcja, program próbuje obliczy

ć n2 

(niepotrzebnie) i si

ę zapętla!

(

źródło – P. Wróblewski)

background image

22/41

Kiedy należy unikać rekurencji?

jeżeli można algorytm rekurencyjny 

zastąpić iteracyjnym

jeżeli algorytm może dawać
niepoprawne wyniki dla pewnych 
zmiennych

background image

23/41

Przykłady zastosowania 

algorytmów rekurencyjnych

sortowanie (quicksort)

wyznaczanie pozycji wartości w tablicy

grafika

problem trwałego małżeństwa 
(optymalizacja)

wieże Hanoi

background image

24/41

Problem wież Hanoi - opis

„wieża” składa się z n krążków, o malejących 
średnicach, ułożonych jeden na drugim na słupku, przy 
czym krążek z największą średnicą jest na dole

należy przełożyć krążki z jednego słupka (źródłowego, 
S1) na drugi słupek (docelowy, S2), wykorzystując 
tylko jeden słupek pomocniczy (Sp) oraz nie 
dopuszczając do sytuacji, aby na krążek o mniejszej 
średnicy został nałożony krążek o większej średnicy

background image

25/41

Problem wież Hanoi - schemat

S1

S2

Sp

S1

S2

Sp

background image

26/41

Problem wież Hanoi – przykład 

dla n=3

1

2

3

4

5

7

6

8

S1

S2

Sp

background image

27/41

Problem wież Hanoi – analiza

jeżeli dany jest jeden krążek, to zadanie polega na 
przeniesieniu go ze słupka S1 na słupek docelowy 
S2 (zadanie elementarne)

jeżeli danych jest n≥2 krążków to zadanie 
rozkładamy na zadania cząstkowe przeniesienia n-
1 krążków

przenosimy zgodnie z zasadami n-1 krążków w S1 na Sp

przenosimy jeden krążek z S1 na S2

przenosimy zgodnie z zasadami n-1 krążków z Sp na S2

liczba operacji przenosin krążków: 2

n

-1

background image

28/41

Problem wież Hanoi – program

void hanoi(int n, int a, int b, int c)

{

if(n==1) cout<<"\nprzesu

ń krążek1 z "<<a<<" na "<<b;

else

{

hanoi(n-1,a,0,b);

cout<<"\nprzesu

ń krążek"<<n<<" z "<<a<<" na "<<b;

hanoi(n-1,3-a-b,b,a);

}

}

int _tmain(int argc, _TCHAR* argv[])

{

cout<<"1-p. pocz

ątkowy, 2-p. docelowy, 0-p. dodatkowy";

cout<<"ile kr

ążków przenie

 

ść? "; int ile_kr; cin>>ile_kr;

hanoi(ile_kr,1,2,0);

return 0;

}

background image

29/41

Algorytmy z powrotami

zadania rozwiązywane są metodą „prób i 
błędów”

zadania dzielone są na podzadania

rozwiązanie problemu realizowane jest 
na drodze stopniowego rozbudowywania 
i przeglądania drzewa podzadań

background image

30/41

Przykłady zastosowań algorytmów 

rekurencyjnych z powrotami

ruchy skoczka na szachownicy

problem 8 nieszachujących się hetmanów

szukanie drogi wyjścia z labiryntu

background image

31/41

Problem 8 hetmanów

zadanie: ustawienie na 
szachownicy 8 hetmanów 
w ten sposób, aby żaden 
z nich nie szachował
innego

H

H

H

H

H

H

H

H

background image

32/41

Problem 8 hetmanów –

założenia do programu

hetman szachuje figury będące w tej samej kolumnie, 

wierszu oraz na dwóch przekątnych (reguły gry)

każda kolumna może zawierać tylko jednego hetmana

problem sprowadza się do wyznaczenia pozycji 

zakresu 0..7 dla i-tego hetmana (w i-tej kolumnie)

w miejsce naturalnej reprezentacji szachownicy w 

postaci macierzy kwadratowej wprowadza się 4 tablice 

określające:

pozycję hetmana w i-tej kolumnie: int k[8]

brak hetmana w j-tym wierszu: bool w[8]

brak hetmana na k-tej przekątnej o kier. ld->pg: bool p1[15]

brak hetmana na k-tej przekątnej o kier. lg->pd: bool p2[15]

background image

33/41

Problem 8 hetmanów –

reprezentacja operacji

ustawienie hetmana:

k[i]=j; w[j]=false; 
p1[i+j]=false; 

p2[i-j+7]=false; 

pozycja dopuszczalna:

w[j]=true;

p1[i+j]=true;

p2[i-j+7]=true;

usuwanie hetmana:
w[j]=true; 
p1[i+j]=true;

p2[i-j+7]=true;

H

p1[0] p1[1]

p1[8]

p1[14]

p2[0]

p2[1]

p2[5]

p2[14]

i

j

background image

34/41

Problem 8 hetmanów –

rozwiązania

istnieją 92 rozwiązania 

12 spośród nich jest różnych, reszta 
wynika z symetrii szachownicy

background image

35/41

Problem 8 hetmanów -

algorytm

void ustaw(int i)

{

for(int j=0;j<8;j++)

if(w[j]==true && p1[i+j]==true && p2[i-j+7]==true)

{

k[i]=j; 

w[j]=false; p1[i+j]=false; p2[i-j+7]=false; 

if(i<7) ustaw(i+1);

else wypisz();

w[j]=true; p1[i+j]=true; p2[i-j+7]=true;

}

}

main() 

{

ustaw(0); ...

}

background image

36/41

Problem skoczka szachowego 

- opis

należy odwiedzić wszystkie pola 
kwadratowej planszy o zadanym 
wymiarze

poruszamy się ruchem konika 
szachowego

założenie: każde pole odwiedzamy tylko 
raz 

background image

37/41

Problem skoczka szachowego 

- algorytm

zadanie odwiedzenia wszystkich pól 
można podzielić na dwa problemy:

wykonania kolejnego ruchu

lub wykazanie, że kolejny ruch nie jest 
możliwy do wykonania

background image

38/41

Problem skoczka szachowego 

– przykład rozwiązania

5

16

23

10

3

22

11

4

15

24

17

6

19

2

9

12

21

8

25

14

7

18

13

20

1

background image

39/41

Problem skoczka szachowego 

– reprezentacja programowa

plansza jest reprezentowana przez tablicę

kwadratową:

int plansza[n][n];

pole jeszcze nie odwiedzone:

plansza[x][y] = 0;

pole już odwiedzone w i-tym ruchu:

plansza[x][y] = i;

liczba ruchów:

≤ i ≤ n

2

background image

40/41

Problem skoczka szachowego 

- procedura

procedure próba_następnego_ruchu;

pozycja_początkowa
repeat następny_możliwy_ruch_z_listy_ruchów

if dozwolony then zapisz_ruch
if są_jeszcze_wolne_pola then

próba_następnego_ruchu

if nieudany then

skasuj ostatni_ruch

until ruch_udany lub nie_ma_następnego_możliwego

background image

41

Dziękuję za uwagę