This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
dydaktyka:cprog:2015:recursion [2015/11/23 12:37] pkleczek [Rekurencja] |
— (current) | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Rekurencja ====== | ||
- | Rekurencja, zwana także rekursją (//recursion//) oznacza odwoływanie się funkcji (bądź definicji) do samej siebie. | ||
- | |||
- | Oto przykład prostego programu, który wykorzystuje rekurencję. | ||
- | <code c> | ||
- | #include <stdio.h> | ||
- | #include <stdlib.h> | ||
- | |||
- | void gora_i_dol(int n) | ||
- | { | ||
- | printf("Poziom %d\n", n); | ||
- | |||
- | if (n < 3) { | ||
- | gora_i_dol(n + 1); | ||
- | } | ||
- | |||
- | printf("POZIOM %d\n", n); | ||
- | } | ||
- | |||
- | int main() | ||
- | { | ||
- | gora_i_dol(1); | ||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | Efekt działania powyższego programu (zwróć uwagę na kolejność komunikatów): | ||
- | <code> | ||
- | Poziom 1 | ||
- | Poziom 2 | ||
- | Poziom 3 | ||
- | POZIOM 3 | ||
- | POZIOM 2 | ||
- | POZIOM 1 | ||
- | </code> | ||
- | |||
- | Zasady rekurencji: | ||
- | |||
- | * Każdy poziom rekurencji posiada swoje własne (niezależne od innych poziomów) zmienne -- bo wywoływana jest nowa funkcja. | ||
- | * Każdemu wywołaniu funkcji odpowiada jeden powrót. Po wykonaniu instrukcji ''return'' na końcu ostatniego (najgłębszego) poziomu rekurencji, program przechodzi do poprzedniego poziomu rekurencji (w miejsce tuż po wywołaniu funkcji) -- a nie do funkcji ''main()''. | ||
- | * Po powrocie do miejsca wywołania funkcji wykonywane są dalsze instrukcje znajdujące się w funkcji. | ||
- | * Chociaż każdy poziom rekurencji posiada swój własny zestaw zmiennych, kod funkcji nie jest zwielokrotniany. Po prostu podczas kolejnego wywołania tej samej funkcji program ponownie przechodzi na jej początek, tyle że z nowym zestawem zmiennych. | ||
- | |||
- | :!: Jeśli do wyboru mamy napisać pętlę bądź użyć rekurencji, to ze względów wydajności lepiej użyć pętli. Rekurencja wymaga o wiele więcej pamięci na dodatkowe zestawy zmiennych! Natomiast w niektórych przypadkach użycie rekurencji jest dużo prostsze i bardziej intuicyjne... | ||
- | |||
- | ---- | ||
- | |||
- | **Przykład -- obliczanie potęgi liczby** | ||
- | |||
- | Znanym z matematyki przykładem definicji rekurencyjnej jest definicja potęgi liczby naturalnej o wykładniku naturalnym: | ||
- | $$ | ||
- | a^n := | ||
- | \begin{cases} | ||
- | 1 & \mbox{dla } n = 0; \\ | ||
- | a \cdot a^{n-1} & \mbox{dla } n > 0. \\ | ||
- | \end{cases} | ||
- | \qquad | ||
- | a, n \in \mathbb{N} | ||
- | $$ | ||
- | |||
- | W poniższym programie umieszczono dodatkowe "diagnostyczne" fragmenty kodu umożliwiające śledzenie zmian wartości zmiennych. \\ | ||
- | Zmienna ''poziom'' oznacza poziom rekurencji, czyli który raz z kolei funkcja wywołała samą siebie. \\ | ||
- | (W dalszej części laboratorium znajduje się wersja pozbawiona tych fragmentów.) | ||
- | <code c> | ||
- | #include <stdio.h> | ||
- | #include <stdlib.h> | ||
- | |||
- | int potega(int a, int n, int poziom); | ||
- | |||
- | int main () | ||
- | { | ||
- | int a = 3; | ||
- | int n = 2; | ||
- | |||
- | printf("%d^%d = %d\n", a, n, potega(a, n, 0)); | ||
- | |||
- | return 0; | ||
- | } | ||
- | |||
- | int potega(int a, int n, int poziom) | ||
- | { | ||
- | int p; | ||
- | int i; | ||
- | |||
- | for (i = 0; i < poziom; i = i + 1) { | ||
- | printf(" "); | ||
- | } | ||
- | printf("poziom %d: n = %d\n", poziom, n); | ||
- | |||
- | if (n == 0) { | ||
- | p = 1; | ||
- | } else { | ||
- | p = a * potega (a, n - 1, poziom + 1); | ||
- | } | ||
- | |||
- | for (i = 0; i < poziom; i = i + 1) { | ||
- | printf(" "); | ||
- | } | ||
- | printf("poziom %d: p = %d\n", poziom, p); | ||
- | |||
- | return p; | ||
- | } | ||
- | </code> | ||
- | |||
- | Schemat ilustrujący zmianę zmiennych podczas wykonania powyższego programu: | ||
- | {{:dydaktyka:cprog:2015:recursion_stack.png|}} | ||
- | |||
- | |||
- | Poniżej kod programu realizującego to samo zadanie, tyle że pozbawionego części "diagnostycznej": | ||
- | <code c> | ||
- | #include <stdio.h> | ||
- | #include <stdlib.h> | ||
- | |||
- | int potega(int a, int n) | ||
- | { | ||
- | if (n == 0) { | ||
- | return 1; | ||
- | } else { | ||
- | return a * potega (a, n - 1); | ||
- | } | ||
- | } | ||
- | |||
- | |||
- | int main () | ||
- | { | ||
- | int a = 3; | ||
- | int n = 4; | ||
- | |||
- | printf("%d^%d = %d\n", a, n, potega(a, n)); | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | To przykład //rekurencji końcowej//, kiedy to wywołanie rekurencyjne znajduje się na końcu funkcji, tuż przed instrukcją ''return''. | ||
- | |||
- | |||
- | ===== Jak projektować funkcję rekurencyjną? ===== | ||
- | |||
- | Rekurencja oznacza po prostu to, że funkcja wywołuje samą siebie. Musi jednak przypadek, w którym funkcja ta __nie__ wywoła samej siebie -- w przeciwnym razie program wpadnie w nieskończony cykl wywołań, wyczerpie dostępny limit pamięci na wywołania funkcji i... zostanie "ubity" przez system operacyjny. Przypadek, w którym funkcja nie wywołuje samej siebie, nazywamy **przypadkiem podstawowym** (w przykładzie z potęgowaniem był to przypadek dla $n = 0$). Projektowanie funkcji rekurencyjnej sprowadza się więc do wyboru rozsądnego przypadku podstawowego, a następnie upewnieniu się, że każdy ciąg wywołań funkcji w końcu osiągnie ten przypadek. | ||
- | |||
- | ===== Inne przykłady rekurencji... ===== | ||
- | |||
- | Ciekawymi przykładami definicji rekurencyjnych są [[https://pl.wikipedia.org/wiki/Fraktal|fraktale]], czyli figury samopodobne. \\ | ||
- | Przykład fraktala -- zbiór Mandelbrota:\\ | ||
- | {{https://upload.wikimedia.org/wikipedia/commons/2/21/Mandel_zoom_00_mandelbrot_set.jpg?400}} | ||
- | |||
- | [size=11]Niestety, takimi rzeczami nie będziemy się zajmować na tych zajęciach :-|[/size] | ||
- | |||
- | ---- | ||
- | |||
- | Rekurencję można wykorzystać do znajdowania [[https://pl.wikipedia.org/wiki/Najwi%C4%99kszy_wsp%C3%B3lny_dzielnik#Za_pomoc.C4.85_algorytmu_Euklidesa|największego wspólnego dzielnika]] dwóch liczb całkowitych. | ||
- | |||
- | Rekurencja stanowi serce klasy algorytmów [[https://pl.wikipedia.org/wiki/Dziel_i_zwyci%C4%99%C5%BCaj|Dziel i zwyciężaj]], których idea opiera się na rekurencyjnym podziale (trudnego) problemu na (łatwe do rozwiązania) podproblemy i odpowiedniego "scalania" ich rozwiązań w celu uzyskania rozwiązania początkowego problemu. | ||
- | ====== Zadania podsumowujące ====== | ||
- | |||
- | ===== Zadanie POW ===== | ||
- | |||
- | ([size=10]poziom trudności:[/size] {{stars>1/4}}) | ||
- | |||
- | Napisz program, który obliczy silnię zadanej liczby naturalnej $n$ metodą rekurencyjną. | ||
- | |||
- | Rekurencyjna definicja silni: | ||
- | $$ | ||
- | n!=\begin{cases} | ||
- | 1 & \mbox{ dla }n=0 \\ | ||
- | n\cdot(n-1)! & \mbox{ dla }n\geqslant1 | ||
- | \end{cases} | ||
- | $$ | ||
- | |||
- | ===== Zadanie PRI ===== | ||
- | |||
- | ([size=10]poziom trudności:[/size] {{stars>2/4}}) | ||
- | |||
- | Napisz program sprawdzający, czy zadana liczba naturalna jest liczbą pierwszą -- oczywiście metodą rekurencyjną, bez użycia jakichkolwiek pętli. | ||
- | |||
- | ===== Zadanie SUM ===== | ||
- | |||
- | ([size=10]poziom trudności:[/size] {{stars>2/4}}) | ||
- | |||
- | Napisz funkcję, która w rekurencyjny sposób obliczy sumę cyfr liczby naturalnej przekazanej jako argument. | ||
- | |||
- | //Wskazówka: Zadanie to można rozwiązać stosując podobne podejście i podobne operacje arytmetyczne, co w [[dydaktyka:cprog:2015:loops-solutions#zadanie_21|zadaniu na sumowanie cyfr]]. // \\ | ||
- | |||
- | ===== Zadanie BIN ===== | ||
- | |||
- | ([size=10]poziom trudności:[/size] {{stars>3/4}}) | ||
- | |||
- | Napisz program, który wypisze na ekranie reprezentację binarną zadanej liczby całkowitej dodatniej. Zadanie zrealizuj z użyciem rekurencji.\\ | ||
- | Przykład: dla liczby ''15'' program powinien wypisać ''1111''. | ||
- | |||
- | //Wskazówka 1: Wypisuj kolejne pozycje liczby binarnej -- znak po znaku (nie próbuj wypisać wszystkiego od razu). // \\ | ||
- | //Wskazówka 2: Od której strony da się zacząć wypisywanie? Zastanów się, w jakiej kolejności zrealizować operacje: (1) wyświetlanie cyfry dwójkowej na ekranie i (2) ponowne wywołanie funkcji.// \\ | ||
- | //Wskazówka 3: Zadanie to można rozwiązać stosując podobne podejście i podobne operacje arytmetyczne, co w [[dydaktyka:cprog:2015:loops-solutions#zadanie_21|zadaniu na sumowanie cyfr]]. // \\ | ||
- | //Wskazówka 4: Do weryfikacji poprawności otrzymanych wyników możesz skorzystać z windowsowego kalkulatora w widoku programisty.// |