Wstęp do programowania/Rekursja: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Pch (dyskusja | edycje)
Nie podano opisu zmian
mNie podano opisu zmian
 
(Nie pokazano 9 wersji utworzonych przez 3 użytkowników)
Linia 1: Linia 1:
Częstokroć stajemy  przed  problemem,  który  łatwo  byłoby  
Częstokroć stajemy  przed  problemem,  który  łatwo  byłoby  
rozwiązać, gdybyśmy mieli w  ręku  odpowiedź  dla  mniejszych  
rozwiązać, gdybyśmy mieli w  ręku  odpowiedź  dla  mniejszych  
danych.  Dla  przykładu,  obliczenie  silni  liczby  <math> n</math>  wymaga  
danych.  Dla  przykładu,  obliczenie  silni  liczby  <math>n</math>  wymaga  
przemnożenia silni <math> (n-1)</math> przez <math> n</math>. Wiemy zatem, że:  
przemnożenia silni <math>(n-1)</math> przez <math>n</math>. Wiemy zatem, że:  
<span id=""/>  
<span id=""/>  
<center><math> 0!=1; n!=n*(n-1)!</math>  dla <math>n>0</math></center>
<center><math>0!=1; n!=n*(n-1)!</math>  dla <math>n>0</math></center>


Możemy tę parę wzorów przyjąć za definicję funkcji  silnia.  
Możemy tę parę wzorów przyjąć za definicję funkcji  silnia.  
Takie definiowanie nazywamy ''rekurencyjnym'' lub  
Takie definiowanie nazywamy ''rekurencyjnym'' lub
''indukcyjnym''. My jednak  słowo  ''em  indukcja''
''indukcyjnym''. My jednak  słowo  ''indukcja''
wolimy zachować  do  określenia  sposobu  dowodzenia.  Będziemy  
wolimy zachować  do  określenia  sposobu  dowodzenia.  Będziemy  
więc  mówili  o  rekurencyjnych  definicjach  i  indukcyjnych  
więc  mówili  o  rekurencyjnych  definicjach  i  indukcyjnych  
dowodach.
dowodach.


Dla  przykładu,  jeżeli  ciąg  <math> T_0,T_1,\ldots </math>  zdefiniujemy rekurencyjnie  
Dla  przykładu,  jeżeli  ciąg  <math>T_0,T_1,\ldots</math>  zdefiniujemy rekurencyjnie  
następująco:
następująco:
<span id=""/>
<span id=""/>
<center><math> T_0=0; T_n = 2T_{n-1}+1, </math> dla <math>n>0</math></center>,
<center><math>T_0=0; T_n = 2T_{n-1}+1</math> dla <math>n>0</math></center>,


to  używając  metody  indukcji  matematycznej,  możemy  łatwo  
to  używając  metody  indukcji  matematycznej,  możemy  łatwo  
udowodnić, że <math> T_n = 2^n-1</math>.
udowodnić, że <math>T_n = 2^n-1</math>.


Napotykamy jednak często na problemy, które w  odróżnieniu  od  
Napotykamy jednak często na problemy, które w  odróżnieniu  od  
funkcji  ''silnia'',  czy  ciągu  <math> T_n</math>,  nie  mają  prostej,  
funkcji  ''silnia'',  czy  ciągu  <math>T_n</math>,  nie  mają  prostej,  
nierekurencyjnej  postaci,  albo  wręcz  nie  jest  ona  nam   
nierekurencyjnej  postaci,  albo  wręcz  nie  jest  ona  nam   
znana. Dla przykładu liczby Fibonacciego znane są co najmniej od 1202 roku,  
znana. Dla przykładu liczby Fibonacciego znane są co najmniej od 1202 roku,  
i  generowanie  kolejnych  liczb  <math> F_n</math>  bezpośrednio  ze  wzoru  
i  generowanie  kolejnych  liczb  <math>F_n</math>  bezpośrednio  ze  wzoru  
rekurencyjnego nie stanowi żadnego problemu, jednak  
rekurencyjnego nie stanowi żadnego problemu, jednak  
dopiero Euler, a niezależnie od niego w 100 lat później w roku 1843 francuski matematyk J.Binet udowodnił wzór bezpośrednio wyliczający n-tą liczbę Fibonacciego:
dopiero Euler, a niezależnie od niego w 100 lat później w roku 1843 francuski matematyk J.Binet udowodnił wzór bezpośrednio wyliczający n-tą liczbę Fibonacciego:


<span id=""/> <center><math> F_n=\frac{1}{\sqrt{5}}\left(\left(\frac{\sqrt{5}+1}{2}  
<span id=""/> <center><math>F_n=\frac{1}{\sqrt{5}}\left(\left(\frac{\sqrt{5}+1}{2}  
\right)^n -\left(  \frac{1-\sqrt{5}}{2}\right)^n  \right)
\right)^n -\left(  \frac{1-\sqrt{5}}{2}\right)^n  \right)
</math></center>
</math></center>
Linia 62: Linia 62:




Wywołanie tej funkcji od pewnego naturalnego argumentu  <math> n\ge  0</math>  
Wywołanie tej funkcji od pewnego naturalnego argumentu  <math>n\ge  0</math>  
spowoduje, że identyfikatorowi  ''silnia''  zostanie  nadana  
spowoduje, że identyfikatorowi  ''silnia''  zostanie  nadana  
wartość <math> n!</math>. Stanie się to w następujący  sposób.  Najpierw  
wartość <math>n!</math>. Stanie się to w następujący  sposób.  Najpierw  
sprawdzimy, czy <math> n</math>  równa  się  zero.  Jeżeli  tak,  to  wynik  
sprawdzimy, czy <math>n</math>  równa  się  zero.  Jeżeli  tak,  to  wynik  
wyniesie 1.  Jeżeli  nie,  to  wywołana  zostanie  funkcja  ''silnia'' od parametru <math> n-1</math>,  a  jej  wynik  przemnożony  zostanie  
wyniesie 1.  Jeżeli  nie,  to  wywołana  zostanie  funkcja  ''silnia'' od parametru <math>n-1</math>,  a  jej  wynik  przemnożony  zostanie  
przez  <math> n</math>  dając  ostateczną  wartość  funkcji.  Wykonanie  
przez  <math>n</math>  dając  ostateczną  wartość  funkcji.  Wykonanie  
instrukcji mnożenia przez <math> n</math> zostanie więc zawieszone  na  czas  
instrukcji mnożenia przez <math>n</math> zostanie więc zawieszone  na  czas  
obliczenia wartości <math>{\em silnia}(n-1)</math>. Zawartość  sumatora  i  
obliczenia wartości <math>\mathit{ silnia}(n-1)</math>. Zawartość  sumatora  i  
wszystkich  rejestrów  używanych  przez  komputer  do  obliczeń  
wszystkich  rejestrów  używanych  przez  komputer  do  obliczeń  
zostaną więc automatycznie zapamiętane przez system wykonujący  
zostaną więc automatycznie zapamiętane przez system wykonujący  
program tak,  że  <math>{\em silnia}(n-1)</math>  wykona  się  w  czystym  
program tak,  że  <math>\mathit{  silnia}(n-1)</math>  wykona  się  w  czystym  
środowisku.  Po  obliczeniu  silni  z  <math> n-1</math>,  zawartość  stanu  
środowisku.  Po  obliczeniu  silni  z  <math>n-1</math>,  zawartość  stanu  
komputera zostanie odtworzona i mnożenie przez <math> n</math> będzie mogło  
komputera zostanie odtworzona i mnożenie przez <math>n</math> będzie mogło  
zostać zakończone.
zostać zakończone.


Rzecz jasna <math>{\em silnia}(n-1)</math> zostanie obliczona w  analogiczny  
Rzecz jasna <math>\mathit{ silnia}(n-1)</math> zostanie obliczona w  analogiczny  
sposób. Rolę wartości <math>n</math> przejmie <math> n-1</math>, zatem wywoła się  w miarę potrzeby <math>{\em silnia}(n-2)</math>  itd.  Widać  więc,  że  do  
sposób. Rolę wartości <math>n</math> przejmie <math>n-1</math>, zatem wywoła się  w miarę potrzeby <math>\mathit{ silnia}(n-2)</math>  itd.  Widać  więc,  że  do  
wywołania funkcji silnia od jakiegoś  dużego  parametru  wymaga się  
wywołania funkcji silnia od jakiegoś  dużego  parametru  wymaga się  
zawieszenia wykonywania  mnożeń  na  wielu  poziomach  wywołań
zawieszenia wykonywania  mnożeń  na  wielu  poziomach  wywołań
rekurencyjnych (po jednym dla każdego <math> 1 \le k \le n</math>).  
rekurencyjnych (po jednym dla każdego <math>1 \le k \le n</math>).  


Zauważmy, że  podobnie  jak  w  przypadku  źle  skonstruowanych  
Zauważmy, że  podobnie  jak  w  przypadku  źle  skonstruowanych  
Linia 96: Linia 96:


Spróbujmy zastosować technikę rekurencyjną do  napisania  
Spróbujmy zastosować technikę rekurencyjną do  napisania  
funkcji obliczającej n-tą liczbę Fibonacciego <math> F_n</math>.
funkcji obliczającej n-tą liczbę Fibonacciego <math>F_n</math>.
{{kotwica|kod_zrodlowy|}}
{{kotwica|kod_zrodlowy|}}
   '''function''' Fibo(n:integer);  
   '''function''' Fibo(n:integer):Integer;  
   {funkcja liczy n-ta liczbe Fibonacciego dla n>=0}
   {funkcja liczy n-ta liczbe Fibonacciego dla n>=0}
     '''begin'''
     '''begin'''
Linia 109: Linia 109:
zadziałać. Została napisana zgodnie z podaną wyżej definicją  
zadziałać. Została napisana zgodnie z podaną wyżej definicją  
liczb Fibonacciego. Spróbujmy zatem  prześledzić,  jak  będzie  
liczb Fibonacciego. Spróbujmy zatem  prześledzić,  jak  będzie  
się wykonywać  dla <math> n=4</math>.  
się wykonywać  dla <math>n=4</math>.  
Aby obliczyć wartość <math> F_4</math>,  będziemy  musieli  wywołać  ''Fibo'' dla <math> n=2</math> i dla <math> n=3</math>, a  następnie  dodać  te  wartości.  
Aby obliczyć wartość <math>F_4</math>,  będziemy  musieli  wywołać  ''Fibo'' dla <math>n=2</math> i dla <math>n=3</math>, a  następnie  dodać  te  wartości.  
Zauważmy jednak, że obliczywszy <math>{\em Fibo}(2)</math>  weźmiemy  się  
Zauważmy jednak, że obliczywszy <math>\mathit{ Fibo}(2)</math>  weźmiemy  się  
za liczenie <math>{\em Fibo}(3)</math> od nowa. Do policzenia <math>{\em Fibo}(3)</math>  
za liczenie <math>\mathit{ Fibo}(3)</math> od nowa. Do policzenia <math>\mathit{ Fibo}(3)</math>  
będziemy jednak potrzebowali wartości <math>{\em Fibo}(1)</math> oraz <math>{\em
będziemy jednak potrzebowali wartości <math>\mathit{ Fibo}(1)</math> oraz <math>\mathit{  
Fibo}(2)</math>.  Ponieważ  komputer  nie  otrzymał  od  nas  żadnych  
Fibo}(2)</math>.  Ponieważ  komputer  nie  otrzymał  od  nas  żadnych  
wskazówek  dotyczących  wykorzystania  raz  już  obliczonych  
wskazówek  dotyczących  wykorzystania  raz  już  obliczonych  
wartości, więc zacznie od nowa obliczać <math>{\em Fibo}(2)</math>.  
wartości, więc zacznie od nowa obliczać <math>\mathit{ Fibo}(2)</math>.  


Ta drobna niegospodarność będzie nas dużo  kosztować.  Liczba  
Ta drobna niegospodarność będzie nas dużo  kosztować.  Liczba  
wywołań funkcji ''Fibo''  będzie  bowiem  proporcjonalna  do  
wywołań funkcji ''Fibo''  będzie  bowiem  proporcjonalna  do  
wartości wykładniczej ze względu na <math> n</math>. Oznacza to,  jak  już  
wartości wykładniczej ze względu na <math>n</math>. Oznacza to,  jak  już  
wiemy, że nawet dla niewielkich stosunkowo  danych,  rzędu  100,  
wiemy, że nawet dla niewielkich stosunkowo  danych,  rzędu  100,  
żaden komputer na świecie  nie  poradzi  sobie  z  zakończeniem  
żaden komputer na świecie  nie  poradzi  sobie  z  zakończeniem  
Linia 129: Linia 129:
dopuścimy do więcej niż jednokrotnego  wywołania  funkcji  dla  
dopuścimy do więcej niż jednokrotnego  wywołania  funkcji  dla  
tych samych  danych.  Wystarczy  zatem  stworzyć  bank  danych  o  
tych samych  danych.  Wystarczy  zatem  stworzyć  bank  danych  o  
obliczonych już wartościach  <math> F_n</math>.  W  tym  celu  zadeklarujemy  
obliczonych już wartościach  <math>F_n</math>.  W  tym  celu  zadeklarujemy  
sobie tablicę <math> F</math>, w której  będziemy  przechowywali  obliczone  
sobie tablicę <math>F</math>, w której  będziemy  przechowywali  obliczone  
już wartości <math> F_n</math>.  Aby  zaznaczyć,  że  dana  wartość  nie  
już wartości <math>F_n</math>.  Aby  zaznaczyć,  że  dana  wartość  nie  
została jeszcze obliczona, wypełnimy tablicę  minus  jedynkami.  
została jeszcze obliczona, wypełnimy tablicę  minus  jedynkami.  
Funkcję ''Fibo'' będziemy zatem liczyli rekurencyjnie  jedynie  
Funkcję ''Fibo'' będziemy zatem liczyli rekurencyjnie  jedynie  
Linia 138: Linia 138:


{{kotwica|kod_zrodlowy|}}
{{kotwica|kod_zrodlowy|}}
   '''var''' F:array[0..m] '''of''' integer;
   '''var''' F:array[0..m] '''of''' Integer;
   
   
   '''function''' Fibo1(n:integer);  
   '''function''' Fibo1(n:integer): Integer;  
   {
   {
   funkcja liczy n-ta liczbe  Fibonacciego  dla  n>=0,  korzystając  
   funkcja liczy n-ta liczbe  Fibonacciego  dla  n>=0,  korzystając  
Linia 191: Linia 191:
możliwie    szybki    sposób    krążki? Przenieść jeden krążek to żadna sztuka. Jak jednak poradzić  sobie  z  ich  
możliwie    szybki    sposób    krążki? Przenieść jeden krążek to żadna sztuka. Jak jednak poradzić  sobie  z  ich  
większą liczbą? Załóżmy indukcyjnie,  
większą liczbą? Załóżmy indukcyjnie,  
że umiemy tego dokonać dla <math> n-1</math> krążków. Rekurencyjnie  nasz  
że umiemy tego dokonać dla <math>n-1</math> krążków. Rekurencyjnie  nasz  
algorytm formułuje się bardzo  prosto.  Aby  przełożyć  wieżę  
algorytm formułuje się bardzo  prosto.  Aby  przełożyć  wieżę  
złożoną z n krążków z jednego pręta na  drugi  przy  pomocy  
złożoną z n krążków z jednego pręta na  drugi  przy  pomocy  
trzeciego, należy <math> n-1</math> krążków przełożyć na pręt  trzeci,  
trzeciego, należy <math>n-1</math> krążków przełożyć na pręt  trzeci,  
następnie przełożyć krążek <math> n</math> na pręt drugi, a  następnie  
następnie przełożyć krążek <math>n</math> na pręt drugi, a  następnie  
<math> (n-1)</math>-elementową wieżę przełożyć z pręta trzeciego na  drugi  
<math> (n-1) </math>-elementową wieżę przełożyć z pręta trzeciego na  drugi  
przy pomocy pierwszego.  
przy pomocy pierwszego.  


Zauważmy przy okazji, że liczba przenosin <math> n</math> krążków, <math> T_n</math>,  
Zauważmy przy okazji, że liczba przenosin <math> n</math> krążków, <math> T_n</math>,  
wymaga  dwukrotnego  przeniesienia  wieży złożonej z <math>n-1</math>  
wymaga  dwukrotnego  przeniesienia  wieży złożonej z <math> n-1 </math>  
krążków  oraz  jednokrotnego  przeniesienia  jednego  krążka.  
krążków  oraz  jednokrotnego  przeniesienia  jednego  krążka.  
Mamy więc wzór:
Mamy więc wzór:


<span id=""/> <center><math> T_1 = 1; \ T_n=2T_{n-1}+1</math></center>
<span id=""/> <center><math> T_1 = 1;\ T_n=2T_{n-1} + 1</math></center>


zatem
zatem
   
   
<center><math> T_n=2(2^{n-1}-1)+1=2^n-1</math>.</center>
<center><math> T_n = 2(2^{n-1}-1)+1 = 2^n-1</math>.</center>


Liczba przenosin jest  więc  wykładnicza  ze  względu  na  <math>n</math>.  
Liczba przenosin jest  więc  wykładnicza  ze  względu  na  <math>n</math>.  
Linia 220: Linia 220:
następująco.  Zakładamy,  że  zamiast  dźwigania  krążków  
następująco.  Zakładamy,  że  zamiast  dźwigania  krążków  
komputer będzie wypisywał kolejne  ruchy  w  postaci  przenieś  
komputer będzie wypisywał kolejne  ruchy  w  postaci  przenieś  
krążek z pręta <math> i</math> na pręt <math> j</math>.
krążek z pręta <math>i</math> na pręt <math>j</math>.


{{kotwica|kod_zrodlowy|}}
{{kotwica|kod_zrodlowy|}}
Linia 245: Linia 245:


Nasza procedura będzie teraz działała w czasie wykładniczym ze  
Nasza procedura będzie teraz działała w czasie wykładniczym ze  
względu na <math> n</math>. Dzieje  się  tak  w  zasadzie  zawsze,  gdy  bez  
względu na <math>n</math>. Dzieje  się  tak  w  zasadzie  zawsze,  gdy  bez  
żadnych finezji dokonujemy we  wnętrzu  procedury  więcej  niż  
żadnych finezji dokonujemy we  wnętrzu  procedury  więcej  niż  
jednokrotnego wywołania rekurencyjnego tej  procedury. Tym  razem  
jednokrotnego wywołania rekurencyjnego tej  procedury. Tym  razem  
jednak nie będzie to stanowiło wady rozwiązania - taka  jest  
jednak nie będzie to stanowiło wady rozwiązania - taka  jest  
natura problemu: odpowiedź w postaci ciągu ruchów do  wykonania  
natura problemu: odpowiedź w postaci ciągu ruchów do  wykonania  
ma długość wykładniczą ze względu na <math> n</math>,  więc  trudno  tu  
ma długość wykładniczą ze względu na <math>n</math>,  więc  trudno  tu  
cokolwiek poprawić. Rzecz jasna  procedurę  należy  wywoływać  
cokolwiek poprawić. Rzecz jasna  procedurę  należy  wywoływać  
dla danych znacznie mniejszych niż 64.
dla danych znacznie mniejszych niż 64.

Aktualna wersja na dzień 14:36, 24 lip 2024

Częstokroć stajemy przed problemem, który łatwo byłoby rozwiązać, gdybyśmy mieli w ręku odpowiedź dla mniejszych danych. Dla przykładu, obliczenie silni liczby n wymaga przemnożenia silni (n1) przez n. Wiemy zatem, że:

0!=1;n!=n*(n1)! dla n>0

Możemy tę parę wzorów przyjąć za definicję funkcji silnia. Takie definiowanie nazywamy rekurencyjnym lub indukcyjnym. My jednak słowo indukcja wolimy zachować do określenia sposobu dowodzenia. Będziemy więc mówili o rekurencyjnych definicjach i indukcyjnych dowodach.

Dla przykładu, jeżeli ciąg T0,T1, zdefiniujemy rekurencyjnie następująco:

T0=0;Tn=2Tn1+1 dla n>0

,

to używając metody indukcji matematycznej, możemy łatwo udowodnić, że Tn=2n1.

Napotykamy jednak często na problemy, które w odróżnieniu od funkcji silnia, czy ciągu Tn, nie mają prostej, nierekurencyjnej postaci, albo wręcz nie jest ona nam znana. Dla przykładu liczby Fibonacciego znane są co najmniej od 1202 roku, i generowanie kolejnych liczb Fn bezpośrednio ze wzoru rekurencyjnego nie stanowi żadnego problemu, jednak dopiero Euler, a niezależnie od niego w 100 lat później w roku 1843 francuski matematyk J.Binet udowodnił wzór bezpośrednio wyliczający n-tą liczbę Fibonacciego:

Fn=15((5+12)n(152)n)

Znajomość tego wzoru wcale nie rozwiązuje nam wszystkich problemów związanych z wyliczeniem n-tej liczby Fibonacciego. Przede wszystkim komputer mógłby mieć trudności ze stwierdzeniem, że wynik jest liczbą naturalną, choć w istocie powyższy wzór Eulera-Bineta generuje jedynie liczby naturalne.

Gdyby bowiem próbował wyciągać pierwiastek z pięciu, to nieuniknione stałoby się zaokrąglenie wyniku i w rezultacie moglibyśmy otrzymać wartość lekko różniącą się od liczby naturalnej, która byłaby spodziewanym wynikiem obliczeń. Poza tym lekką przesadą możnaby nazwać używanie tak skomplikowanych operacji jak potęgowanie liczb niewymiernych w celu uzyskania w miarę prostej liczby naturalnej.

Ponieważ stosujemy zasadę, że to komputer powinien dostosowywać się w miarę możliwości do potrzeb człowieka, więc mechanizmy pozwalające na korzystanie z rekurencji istnieją w większości współczesnych języków programowania. W Pascalu użycie rekurencji jest niezwykle naturalne:

 function silnia(n:Integer):Integer;
 begin
   if n=0 then silnia := 1
          else silnia := n*silnia(n-1)
 end;


Wywołanie tej funkcji od pewnego naturalnego argumentu n0 spowoduje, że identyfikatorowi silnia zostanie nadana wartość n!. Stanie się to w następujący sposób. Najpierw sprawdzimy, czy n równa się zero. Jeżeli tak, to wynik wyniesie 1. Jeżeli nie, to wywołana zostanie funkcja silnia od parametru n1, a jej wynik przemnożony zostanie przez n dając ostateczną wartość funkcji. Wykonanie instrukcji mnożenia przez n zostanie więc zawieszone na czas obliczenia wartości silnia(n1). Zawartość sumatora i wszystkich rejestrów używanych przez komputer do obliczeń zostaną więc automatycznie zapamiętane przez system wykonujący program tak, że silnia(n1) wykona się w czystym środowisku. Po obliczeniu silni z n1, zawartość stanu komputera zostanie odtworzona i mnożenie przez n będzie mogło zostać zakończone.

Rzecz jasna silnia(n1) zostanie obliczona w analogiczny sposób. Rolę wartości n przejmie n1, zatem wywoła się w miarę potrzeby silnia(n2) itd. Widać więc, że do wywołania funkcji silnia od jakiegoś dużego parametru wymaga się zawieszenia wykonywania mnożeń na wielu poziomach wywołań rekurencyjnych (po jednym dla każdego 1kn).

Zauważmy, że podobnie jak w przypadku źle skonstruowanych pętli, możemy nabawić się kłopotów wywołując funkcję silnia od argumentu ujemnego. System zacznie wtedy wywoływać kaskadę silni wołanych dla parametrów ujemnych coraz bardziej oddalonych od zera. Gdyby pamięć komputera była nieskończona, spowodowałoby to nieskończone zapętlenie się programu. Każde wywołanie funkcji wymaga jednak zapamiętania aktualnego stanu komputera. Zużywa to dostępną pamięć blokując potrzebny jej fragment do końca wywołania funkcji; w naszym przypadku ten koniec nigdy nie nastąpi. Program zostanie zatem zerwany przez system wykonujący z powodu braku pamięci. Nie jest to jednak jedyna groźba, którą napotykamy stosując rekurencję. Znacznie poważniejszym problemem może okazać się nieprawidłowa organizacja rekurencji powodująca brzemienne w skutkach zużycie czasu wykonywanego programu.

Spróbujmy zastosować technikę rekurencyjną do napisania funkcji obliczającej n-tą liczbę Fibonacciego Fn.

 function Fibo(n:integer):Integer; 
 {funkcja liczy n-ta liczbe Fibonacciego dla n>=0}
   begin
     if n <= 1 then Fibo := n
     else Fibo := Fibo(n-2) + Fibo(n-1)
   end; {Fibo}


Na pierwszy rzut oka widać, że funkcja powinna dobrze zadziałać. Została napisana zgodnie z podaną wyżej definicją liczb Fibonacciego. Spróbujmy zatem prześledzić, jak będzie się wykonywać dla n=4. Aby obliczyć wartość F4, będziemy musieli wywołać Fibo dla n=2 i dla n=3, a następnie dodać te wartości. Zauważmy jednak, że obliczywszy Fibo(2) weźmiemy się za liczenie Fibo(3) od nowa. Do policzenia Fibo(3) będziemy jednak potrzebowali wartości Fibo(1) oraz Fibo(2). Ponieważ komputer nie otrzymał od nas żadnych wskazówek dotyczących wykorzystania raz już obliczonych wartości, więc zacznie od nowa obliczać Fibo(2).

Ta drobna niegospodarność będzie nas dużo kosztować. Liczba wywołań funkcji Fibo będzie bowiem proporcjonalna do wartości wykładniczej ze względu na n. Oznacza to, jak już wiemy, że nawet dla niewielkich stosunkowo danych, rzędu 100, żaden komputer na świecie nie poradzi sobie z zakończeniem obliczeń, niezależnie od tego jak jest szybki i jak wiele czasu mu damy.

Cały problem będzie rozwiązany bezboleśnie, jeżeli tylko nie dopuścimy do więcej niż jednokrotnego wywołania funkcji dla tych samych danych. Wystarczy zatem stworzyć bank danych o obliczonych już wartościach Fn. W tym celu zadeklarujemy sobie tablicę F, w której będziemy przechowywali obliczone już wartości Fn. Aby zaznaczyć, że dana wartość nie została jeszcze obliczona, wypełnimy tablicę minus jedynkami. Funkcję Fibo będziemy zatem liczyli rekurencyjnie jedynie w miarę potrzeby, czyli wtedy, gdy dla danego argumentu liczymy ją po raz pierwszy. Zmodyfikujmy zatem naszą funkcję.

 var F:array[0..m] of Integer;

 function Fibo1(n:integer): Integer; 
 {
 funkcja liczy n-ta liczbe  Fibonacciego  dla  n>=0,  korzystając 
 przy tym z globalnej tablicy F, przy czym
 F[i] = F_i, jezeli F_i jest juz obliczone
 F[i] = -1,  jezeli nie jest jeszcze obliczone
 Założenie: n>=0
 }
   begin
   if  F[n]<0  then {Wartosc F[n] nie jest jeszcze obliczona, więc
                    zaczniemy  od  wypełnienia  tablicy  właściwą 
                    wartością}
     if n <= 1 then F[n]:=n 
     else F[n] := Fibo1(n-2) + Fibo1(n-1);
 {i teraz dopiero nadamy odpowiednią wartośc identyfikatorowi Fibo.
  W tablicy F[n] znajduje się zawsze własciwa wartość}
   Fibo1 := F[n]
   end; {Fibo1}


Wykonanie tego programu dla paru danych szybko przekona nas o skuteczności tego ulepszenia. Tym razem powinniśmy także uważać na stosunkowo nieduży zakres typu integer, i jeżeli chcemy obliczać większe liczby Fibonacciego, musimy użyć innego typu (np. longint).

Rzecz jasna istnieją znacznie prostsze metody pozwalające nam na obliczenie n-tej liczby Fibonacciego. Podobnie jak w przypadku silni, można to zrobić jedną prostą pętlą z czterema przypisaniami. Są jednak problemy, dla których znalezienie nierekurencyjnego rozwiązania ani nie jest proste, ani warte zachodu ze względu na elegancję i efektywność rekurencji w tych przypadkach.

Wielu znane jest zapewne zadanie o wieżach z Hanoi. Legenda głosi, że w pewnej świątyni buddyjskiej w Hanoi znajdują się trzy wbite w ziemię pręty. Na jednym z nich początkowo umieszczono 64 koła o malejących średnicach. Należy te koła przenieść z pierwszego pręta na drugi przy pomocy trzeciego, respektując następujące zasady:

  1. jednorazowo można przenieść jeden krążek
  2. krążek można nałożyć na dowolny z prętów pod

warunkiem, że kładzie się go na pusty pręt lub na krążek o większej średnicy. Ponoć mnisi przekładają od jakiegoś czasu te krążki, a gdy skończą, nastąpi koniec świata.

Jak można wyobrazić sobie najprostszy algorytm przekładający w możliwie szybki sposób krążki? Przenieść jeden krążek to żadna sztuka. Jak jednak poradzić sobie z ich większą liczbą? Załóżmy indukcyjnie, że umiemy tego dokonać dla n1 krążków. Rekurencyjnie nasz algorytm formułuje się bardzo prosto. Aby przełożyć wieżę złożoną z n krążków z jednego pręta na drugi przy pomocy trzeciego, należy n1 krążków przełożyć na pręt trzeci, następnie przełożyć krążek n na pręt drugi, a następnie (n1)-elementową wieżę przełożyć z pręta trzeciego na drugi przy pomocy pierwszego.

Zauważmy przy okazji, że liczba przenosin n krążków, Tn, wymaga dwukrotnego przeniesienia wieży złożonej z n1 krążków oraz jednokrotnego przeniesienia jednego krążka. Mamy więc wzór:

T1=1; Tn=2Tn1+1

zatem

Tn=2(2n11)+1=2n1.

Liczba przenosin jest więc wykładnicza ze względu na n. Możemy spać spokojnie. Nawet jeżeli mnisi będą bardzo wydajni, to prędzej nastąpi koniec Układu Słonecznego, aniżeli przełożenie tych 64 krążków (zakładając, że mnisi zaczęli w czasach historycznych).

Procedura, która dokona wygenerowania kolejnych ruchów przenoszących n-elementową wieżę może więc wyglądać następująco. Zakładamy, że zamiast dźwigania krążków komputer będzie wypisywał kolejne ruchy w postaci przenieś krążek z pręta i na pręt j.

 procedure Hanoi (n, skad, dokad:integer);
   { procedura przenosi n krążkow z wieży skąd na wieżę dokąd }
           procedure przenies (co, skad, dokad:integer);
           { Tu rzecz jasna można wpisać dowolnie inteligentną procedurę}
           begin
            writeln ('PRZENIES ', CO, ' Z ', skad, ' NA ', dokad)
           end;
   begin
    if n<=1 then
       przenies (1, skad, dokad)
    else
     begin
       {zauważmy, że jeżeli jeden pręt ma numer i, a drugi j, to trzeci
        z nich ma numer 6-i-j}
       hanoi   (n-1, skad, 6-skad-dokad);
       przenies (n,   skad,  dokad );
       hanoi   (n-1, 6-skad-dokad, dokad)
     end
   end;


Nasza procedura będzie teraz działała w czasie wykładniczym ze względu na n. Dzieje się tak w zasadzie zawsze, gdy bez żadnych finezji dokonujemy we wnętrzu procedury więcej niż jednokrotnego wywołania rekurencyjnego tej procedury. Tym razem jednak nie będzie to stanowiło wady rozwiązania - taka jest natura problemu: odpowiedź w postaci ciągu ruchów do wykonania ma długość wykładniczą ze względu na n, więc trudno tu cokolwiek poprawić. Rzecz jasna procedurę należy wywoływać dla danych znacznie mniejszych niż 64.