Metoda rekurencji uniwersalnej stosowana jest do rekursji postaci
T(n)=aT(n/b)+f(n),
(10)gdzie
a≥1, b>1, f
jest pewną funkcją nieujemną określoną na podzbiorze liczb naturalnych.Rekursja (10) opisuje czas działania algorytmu, który dzieli problem rozmiaru n na a podproblemów o rozmiarze n/b.
Każdy z a podproblemów jest rozwiązywany rekurencyjnie w czasie T(n/b).
Koszt dzielenia problemu oraz łączenia rezultatów częściowych jest opisany funkcją f.
Ważne twierdzenie!!
Problem:
•tablica n liczb całkowitych tab[n]=tab[0], tab[1], …, tab[n-1];
•czy w tablicy tab występuje liczba x (podana jako parametr)?
Rozumowanie:
wziąć pierwszy niezbadany element tablicy n-elementowej;
jeśli aktualnie analizowany element tablicy jest równy x, to:
wypisz „Sukces” i zakończ;
w przeciwnym wypadku:
zbadaj pozostałą część tablicy n-1 elementowej.
Gdy przebadaliśmy całą tablicę i element nie został znaleziony, można np. wyświetlić komunikat o
niepowodzeniu.
Przykładowa realizacja:
int const n=10;
int tab[n]={1,2,3,2,-7,44,5,1,0,-3};
void szukaj(int tab[n], int left, int right, int x) //left, right - lewa i prawa granica obszaru poszukiwań
//tab - tablica
//x - wartość do odnalezienia {
if (left>right)
cout << "Element " << x << " nie został odnaleziony\n";
else
if (tab[left]==x)
cout <<"Znalazłem szukany element "<< x <<endl;
else
szukaj(tab, left+1,right,x);
}
Program ilustruje podstawowe cechy typowego programu rekurencyjnego:
•element znaleziony
•przekroczenie zakresu tablicy Zakończenie programu
jest jasno określone
•z tablicy o rozmiarze n schodzimy do tablicy o rozmiarze n-1
Duży problem zostaje rozbity na problemy elementarne, które umiemy
rozwiązać i na analogiczny problem, tylko o mniejszym
stopniu skomplikowaniu
złe określenie warunku zakończenia programu niewłaściwa (nieefektywna)
dekompozycja problemu
Przykład: Obliczanie silni
unsigned long int silnia (int x) {
if (x==0) return 1;
else
return x*silnia(x-1);
} Jak się liczy 3!
Proces przekazywania wyniku cząstkowego z poziomu niższego na wyższy
Zagłębianie się programu z
poziomu n na n-1 w celu dotarcia do przypadku
elementarnego 0!
Obliczanie wyników cząstkowych
Przykład: Ciąg Fibonacciego.
(Elementy tego ciągu stanowią liczby naturalne takie, że kolejny wyraz (z
wyjątkiem dwóch pierwszych) jest sumą dwóch poprzednich, tj. 1, 1, 2, 3, 5, 8, 13,…)
unsigned long int Fibonaci(int n) {
if(n<2)
return n else
return Fibonaci(n-1)+Fibonaci(n-2);
} Obliczanie czwartego
elementu ciągu
Każde zacieniowane
wyrażenie stanowi problem elementarny
Znaczna część obliczeń jest wykonywana więcej niż jeden raz!!
•Programy rekurencyjne są zazwyczaj dość pamięciożerne.
•Program obliczający 3! wywoła sam siebie tylko 3 razy, ale Fibonacci już nie jest taki łatwy do analizy.
Przykład
unsigned long int funkcja(int x) {
if(x>100)
return (x-10);
else
return funkcja(funkcja(x+11));
}
Ile wywołań?
Co się dzieje na większym przedziale liczbowym niż na rysunku? –ćw.
Stack overflow, czyli funkcja Ackermanna
#include <iostream.h>
int A(int n,int p) {
if (n==0) return 1;
if ((p==0)&&(n>=1))
if (n==1)return 2;
else return n+2;
if ((p>=1)&&(n>=1))
return A(A(n-1,p),p-1);
}
int main() {
cout << "A(3,4)="<<A(3,4) <<endl;
}
Jaki jest powód komunikatu:
„Stack overflow!” (przepełnienie stosu) podczas próby jego
wykonania?
Nastąpiła znaczna ilość
wywołań funkcji Ackermanna.
Stack overflow, czyli funkcja Ackermanna Analiza wywołań Pobieżna analiza funkcji A prowadzi do spostrzeżenia:
. 2 ...
2 )
1 , 1 (
) 0 ), 1 , 1 (
( )
1 , ( ,
1 A n A A n A n n
n
Analogicznie dla 2 otrzymamy:
. 2 ...
) 2 , 1 (
2 )
1 ), 2 , 1 (
( )
2 , ( ,
1 A n A A n A n
nn
Z samej definicji funkcji Ackermanna możemy wywnioskować, że
. 1 )
3 , 0 ( ,
2 )
2 ), 3 , 1 (
( )
3 , ( ,
1
( 1,3)
n A n A A n
A nA
Na bazie tych równań możliwe jest rekurencyjne udowodnienie, że
. 2
) 3 , ( ,
1
2 2 n
n A
n
Stack overflow, czyli funkcja Ackermanna Analiza wywołań Nieco gorsza jest sytuacja dla A(n,4), bo trudno jest podać wzór ogólny. Ale można zobaczyć przykłady liczbowe:
. 2
. 65536 2
. 4 2
. 2
) 4 , 4 (
) 4 , 3 (
) 4 , 2 (
) 4 , 1 (
65536 2
2
2
2 22
A
A
A
A
Błąd programisty! Sprowokowanie nieskończonej ilości wywołań rekurencyjnych!
Przykład:
int niesk(int n) {
if(n==1)
return 1;
else
if ((n%2)==0) //czy n jest parzyste?
return niesk(n-2)*n;
else
return niesk(n-1)*n;
}
Dla n>=2 wszystkie wywołania rekurencyjne kończą się parzystą liczbą n. Zatem dojdziemy do
n=2, potem n=0, n=-2,……
Nigdzie po drodze nie ma przypadku elementarnego!
Błąd programisty! Jak go uniknąć?
Sprawdzić matematycznie poprawność definicji rekurencyjnej, tzn.
•określić dziedziny wartości funkcji,
•udowodnić, że się ona zakończy,
•podać złożoność obliczeniową
To nie wystarczy! Nie wiadomo, jak rzeczywisty kompilator wykona tę funkcję.
int N(int n, int p) {
if(n==0)
return 1;
else
return N(n-1,N(n-p,p));
}
Można udowodnić matematycznie, że powyższa definicja jest poprawna w tym sensie, że dla dowolnych n>=0, p>=0 jej wynik jest określony i wynosi 1.
Zakłada się, że wartość argumentu wywołania funkcji jest obliczana tylko wtedy, gdy jest to konieczne. Jak wykona to typowy kompilator C++?
Wszystkie parametry funkcji rekurencyjnej są obliczane jako pierwsze, a potem wywołana jest funkcja – wywołanie przez wartość.
int N(int n, int p) {
if(n==0)
return 1;
else
return N(n-1,N(n-p,p));
}
Wszystkie parametry funkcji rekurencyjnej są obliczane jako pierwsze, a potem wywołana jest funkcja – wywołanie przez wartość.
Zapętlenie jest spowodowane próbą obliczenia parametru p, tymczasem to drugie wywołanie nie jest potrzebne do zakończenia funkcji!
Kompilator tego nie wie!
•Łatwe do zrozumienia
•Zajmują mało miejsca (liczba wierszy kodu) – ewentualnie łatwo znaleźć błędy
Jak w takim razie usunąć wady?
Inaczej zbudować rekurencję.
Rekurencja „naturalna”
–poprzednie przykłady
Rekurencja „z parametrem dodatkowym”
Na czym polega?
unsigned long int silnia (int x) {
if (x==0) return 1;
else
return x*silnia(x-1);
}
unsigned long int silnia2 (int x, int tmp=1) {
if (x==0)
return tmp;
else
return silnia2(x-1,x*tmp);
}
Parametry domyślne funkcji fun(int a, int k=1)
Funkcja może być wywołana na dwa sposoby:
•Poprzez określenie wartości drugiego
parametru, np. fun(2,5), wtedy k przyjmuje
wartość 5;
•Bez określania wartości drugiego parametru, np.
fun(12), wtedy k przyjmuje wartość domyślną równą tej podanej w nagłówku, czyli 1.
Parametr dodatkowy przekazuje elementy wyniku końcowego – program nie ma potrzeby
przekazywania wyniku obliczeń do góry, piętro po piętrze – ostatni aktywny poziom dostarczy wynik!
Twierdzenie o rekurencji uniwersalnej podaje metodę rozwiązania tego typu rekurencji.
Zapis tego algorytmu w pseudokodzie:
Wywołania rekurencyjne
Ćwiczenie:
Zapisać w C++
Twierdzenie:
Komentarz:
Twierdzenie:
Gdy n jest dowolną liczbą naturalną rozwiązanie rekurencji jest trudniejsze. Należy wykorzystać następujący wzór sumacyjny:
Twierdzenie:
Dowód:
Korzystając z podanego wcześniej wzoru sumacyjnego, możemy policzyć wartość sumy:
co daje ostatecznie:
Rodzaje sortowania (wg pamięci)
Wewnętrzne Zbiór do posortowania mieści się w pamięci
Zewnętrzne
Zbiór do posortowania mieści się w pamięci
zewnętrznej, np. na dyskach (wykorzystuje się
tylko stałą – małą – ilość pamięci wewnętrznej.
Rodzaje sortowania (wg operacji)
Adaptacyjne
Wykonuje się różne sekwencje operacji dla
różnych układów danych
Nieadaptacyjne
Sekwencja
wykonywanych operacji nie zależy od
kolejności danych.
Parametry wydajnościowe sortowania:
→
czas działania algorytmu→
ilość dodatkowej pamięci zużywanej przez algorytmWykorzystuje tyle miejsca, ile potrzeba na
zapisanie sortowanych danych + mały stos
lub tablica
Używa reprezentacji w postaci listy połączonej
albo innego sposobu pośredniego dostępu
do danych, czyli wymaga dodatkowej
pamięci na n wskaźników lub
indeksów
Wymaga dodatkowej pamięci na pełną kopię sortowanych
danych
Definicja.
Sortowanie jest stabilne, jeśli zachowuje względną kolejność elementów o jednakowych kluczach.
Przykład.
Lista studentów uporządkowana alfabetycznie według nazwisk. Jeśli chcemy ją posortować wg ocen, to studenci mający taką samą ocenę nadal będą w liście ułożeni alfabetycznie.