11.8 Funkcje rekurencyjne

Funkcje w C/C++ mogą wywoływać same siebie — takie funkcje nazywamy funkcjami rekurencyjnymi. Przy definiowaniu takich funkcji trzeba oczywiście zadbać, aby ciąg wywołań skończył się. Zatem przed wywołaniem samej siebie funkcja zwykle sprawdza pewien warunek i jeśli nie jest on spełniony, nie dokonuje już samo-wywołania.

Jako przykład rozpatrzmy funkcję gcd z poniższego programu. Oblicza ona i zwraca największy wspólny dzielnik (ang. greatest common divisor) swoich dwóch argumentów, które są liczbami naturalnymi (całkowitymi dodatnimi). Użyty algorytm to znana wszystkim modyfikacja algorytmu Euklidesa opisanego w Elementach jako Twierdzenie VII.2 (i będącego wypadkiem szczególnym wcześniejszego i bardziej ogólnego algorytmu, przypisywanemu Teajtetosowi):


P71: gcd.cpp     Algorytm Euklidesa - realizacja rekurencyjna

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int gcd(int a, int b) {
      5.      return b == 0 ? a : gcd(b, a % b);
      6.  }
      7.  
      8.  int main() {
      9.      int x, y;
     10.  
     11.      x = 732591; y = 1272585;
     12.      cout << "gcd(" << x << "," << y << ")\t= "
     13.           << gcd(x,y) << endl;
     14.  
     15.      x = 732591; y = 1270;
     16.      cout << "gcd(" << x << "," << y << ")\t= "
     17.           << gcd(x,y) << endl;
     18.  }

Sama funkcja realizująca algorytm składa się w zasadzie z jednej instrukcji return. Dopóki zmienna lokalna b nie stanie się równa zeru, funkcja wywołuje się rekurencyjnie. Z analizy teoretycznej wynika, że dla prawidłowych danych (a więc liczb naturalnych, czyli, w szczególności, dodatnich) po skończonej liczbie kroków wartość b musi stać się równa zeru. Tak więc warunek zakończenia (tzw. warunek stopu) jest zapewniony i funkcja działa prawidłowo:

    gcd(732591,1272585)     = 129
    gcd(732591,1270)        = 1
„Samowywołanie” się funkcji jest w zasadzie normalnym wywołaniem: wszystkie lokalne zmienne są tworzone oddzielnie w każdym wywołaniu, mechanizm przekazywania argumentów jest taki sam jak dla funkcji nierekursywnych. Na przykład funkcja gcd kreuje zmienne lokalne ab a następnie, obliczając wyrażenie za dwukropkiem, wywołuje tę samą funkcję gcd i czeka na zwrócenie wyniku. To następne „wcielenie” funkcji też tworzy swoje zmienne lokalne ab, woła gcd i czeka na wynik, aby go zwrócić, itd. Kiedy w końcu b stanie się zero, funkcja zwróci a, które zostanie zwrócone przez poprzednie „wcielenie”, które zostanie zwrócone przez poprzednie „wcielenie”, itd.

Funkcje rekurencyjne trzeba stosować z umiarem i umiejętnie. Nieprzemyślane zastosowanie rekurencji może bowiem prowadzić do kombinatorycznej eksplozji liczby wywołań i rozmiaru stosu potrzebnego do realizacji rekurencji. Tak na przykład bywa, gdy w treści funkcji wywołanie samej siebie występuje dwukrotnie, jak w klasycznym przykładzie nieprawidłowego obliczania wartości wyrazów ciągu Fibonacciego. Definicja tego ciągu jest następująca:
Fn = n dla n < 2 oraz Fn = Fn-1 + Fn-2 dla n≥2

Naturalnym sposobem zaprogramowania takiej funkcji jest postać rekurencyjna jak w następującym programie (w linii , po dwukropku, mamy tu dwukrotne wywołanie przez funkcję samej siebie):


P72: fib.cpp     Ciąg Fibonacciego: zły przykład rekurencji

      1.  #include <iostream>
      2.  #include <iomanip>
      3.  using namespace std;
      4.  
      5.  int licznik;
      6.  
      7.  int fib(int n) {
      8.      licznik++;
      9.      return (n < 2) ? n : fib(n-1) + fib(n-2);    
     10.  }
     11.  
     12.  int main() {
     13.      cout << "\n  i       Fib(i)  # wywolan\n"
     14.              "---------------------------" << endl;
     15.      for (int i = 10; i <= 43; i += 3) {
     16.          licznik = 0;
     17.          int w = fib(i);
     18.          cout << setw(3)  << i << setw(12) << w   
     19.               << setw(12) << licznik << endl;
     20.      }
     21.  }

W programie wprowadziliśmy zmienną globalną licznik zliczającą liczbę wywołań funkcji fib podczas znajdowania elementów ciągu Fibonacciego — zmienna ta jest zerowana przed przystąpieniem do wyliczania każdego kolejnego elementu ciągu. W ten sposób możemy wypisać nie tylko wartości kolejnych liczb Fibonacciego, ale również liczbę wywołań funkcji, jak były potrzebne do ich wyliczenia. Obliczenia zabrały kilka sekund; większość tego czasu zużyta została na obliczenie ostatniej wartości, czyli F43. Poniższy wydruk z tego programu pokazuje, że do obliczenia F43 potrzebne było prawie półtora miliarda wywołań!

      i       Fib(i)  # wywolan
    ---------------------------
     10          55         177
     13         233         753
     16         987        3193
     19        4181       13529
     22       17711       57313
     25       75025      242785
     28      317811     1028457
     31     1346269     4356617
     34     5702887    18454929
     37    24157817    78176337
     40   102334155   331160281
     43   433494437  1402817465
Oczywiście wykonanie wersji „normalnej”, a więc tzw. iteracyjnej, tej funkcji zabiera ułamek sekundy i w ogóle nie zużywa miejsca na stosie. [Użyte w tym programie tzw. manipulatory (setw w linii ) służą jedynie do przejrzystego sformatowania tabelki wyników — będą one omówione w rozdziale o operacjach we/wy ].

T.R. Werner, 25 lutego 2017; 22:31