User Tools

Site Tools


dydaktyka:cprog:2015:recursion

This is an old revision of the document!


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ę.

#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;
}

Efekt działania powyższego programu (zwróć uwagę na kolejność komunikatów):

Poziom 1
Poziom 2
Poziom 3
POZIOM 3
POZIOM 2
POZIOM 1

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.)

#include <stdio.h>
#include <stdlib.h>
 
int potega(int a, int n, int poziom);
 
int main ()
{
    int a = 3;
    int b = 2;
    int n = 4;
 
    printf("%d^%d = %d\n", a, b, 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;
}

Schemat ilustrujący zmianę zmiennych podczas wykonania powyższego programu:

Poniżej kod programu realizującego to samo zadanie, tyle że pozbawionego części “diagnostycznej”:

#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;
}

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ą fraktale, czyli figury samopodobne.
Przykład fraktala – zbiór Mandelbrota:
upload.wikimedia.org_wikipedia_commons_2_21_mandel_zoom_00_mandelbrot_set.jpg

Niestety, takimi rzeczami nie będziemy się zajmować na tych zajęciach :-|


Rekurencję można wykorzystać do znajdowania największego wspólnego dzielnika dwóch liczb całkowitych.

Rekurencja stanowi serce klasy algorytmów 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

(poziom trudności: )

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

(poziom trudności: )

Napisz program sprawdzający, czy zadana liczba naturalna jest liczbą pierwszą – oczywiście metodą rekurencyjną, bez użycia jakichkolwiek pętli.

Zadanie SUM

(poziom trudności: )

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 zadaniu na sumowanie cyfr.

Zadanie BIN

(poziom trudności: )

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 zadaniu na sumowanie cyfr.
Wskazówka 4: Do weryfikacji poprawności otrzymanych wyników możesz skorzystać z windowsowego kalkulatora w widoku programisty.

dydaktyka/cprog/2015/recursion.1448274081.txt.gz · Last modified: 2020/03/25 11:46 (external edit)