Lõpprekursioon ja keerukus
Kui rekursiivne avaldis sisaldab
mitut (varemarvutatud) avaldist, peab lõpprekursiivse programmi saamiseks 
"kaasa vedama" mitut seniarvutatud jadaliiget ja lõpprekursiivse programmi saamine
võib tunduda liiga komplitseerituna, et sellega 
üldse jännata. 
  Vaatleme näitena Fibonacci arve. Itaalia linna Pisa matemaatik Leonardo Fibonacci uuris oma aastal 1200 
 ilmunud raamatus "Liber Abaci" ("Raamat Arvelauast") järgmist probleemi:
Vaatleme näitena Fibonacci arve. Itaalia linna Pisa matemaatik Leonardo Fibonacci uuris oma aastal 1200 
 ilmunud raamatus "Liber Abaci" ("Raamat Arvelauast") järgmist probleemi:
	Kui iga vähemalt 1 kuu vana paar küülikuid "toodab" järgmise kuu lõpuks uue
	paari küülikuid ja meil on algul vaid 
	üks paar vastsündinud küülikuid, siis kui palju paare on meil poole aasta, aasta
	või kahe aasta pärast
	(eeldades, et kõik küülikud on surematud)? 
	Kui N-dal kuul on K(N) paari
	küülikuid, siis järgmise kuu lõpul on alles need K(N) paari.
	Kuna eelmise kuu lõpul olemas olnud 
	 K(N-1) paari on kõik juba rohkem kui
	 1 kuu vanad ja paljunevad, siis
	 K(N+1) = K(N) + K(N-1) 
     Eelmisel kuul sündinud K(N)-K(N-1) paari
      järgmisel kuul veel ei paljune; tulemusena saame nn Fibonacci rea:
	 1,1,2,3,5,8,11,...  
	 kus iga järgmine liige on kahe eelmise liikme summa. Selle rea liikemete arvutamine Prologi predikaadina on täiesti analoogiline 
	 faktoriaali arvutamisega, kuid ühe funktsiooni 
	 K(N) väärtuse asemel peetakse meeles kogu rida K(0), K(1), K(2),...:
	 fibonacci_jada(0,[1]) :-
!.
fibonacci_jada(1,[1,1]) :-
!.
fibonacci_jada(N,[P,P1,P2|Muud]) :-
N1 is N - 1,
fibonacci_jada(N1,[P1,P2|Muud]),
P is P1 + P2.
 
Kui see predikaat ei ole lõpprekursiivne. Laseme Prologil endal arvutada, mitu alamülesannet fibonacci_jada ta päringu 
fibonacci_jada(N,F) täimisel käivitab:
alamylesandeid(0,1) :-
!.
alamylesandeid(1,1) :-
!.
alamylesandeid(N,Ylesandeid) :-
N1 is N - 1,
N2 is N - 2,
alamylesandeid(N1,Ylesandeid1),
alamylesandeid(N2,Ylesandeid2),
Ylesandeid is Ylesandeid1 + Ylesandeid2 + 1.
 Et hinnata alamülesannete arvu kasvu, teeme mõne päringu:
Et hinnata alamülesannete arvu kasvu, teeme mõne päringu:
?- alamylesandeid(10,N).
  
N = 177 
  
 ?- alamylesandeid(20,N).
  N = 21891 
     ?- alamülesandeid(30,N).
    
  N = 2692537 ;
 ?- alamylesandeid(50,N).
  tuli 15 minuti pärast vastus:
  Local stack full 
 Et paremini aru saada, kuidas alamülesannete arv kasvab, on kõrval  lastud
 Prologil välja joonestada selle funktsiooni graafik 
 (vastavat programmi on selgitatud  SWI-Prologi graafikalaiendust
 XPCE käsitlevas
 osas); on selge, et see funktsioon kasvab  eksponentsiaalselt, s.t. selliselt
 programmeeritud Fibonacci arvude arvutamise programm kõlbab vaid väga väikeste
  argumendi N väärtuste korral.
 Fibonacci arvude lõpprekursiivse programmi saame, kui peame meeles (näiteks nimistuna) kaks viimast Fibonacci arvu:
 
 fibonacci_arv(0,1):-!.
 fibonacci_arv(N,F):-
 fibonacci_jada1(1,N,[1,1],[F|_]).
 
fibonacci_jada1(N,N,L,L):-!.
 fibonacci_jada1(N1,N,[F1,F],L):-
   N2 is N1+1,
   F2 is F1+F,
   fibonacci_jada1(N2,N,[F2,F1],L).
  Nüüd kasvab alamülesannete arv lineaarselt: N ühe võrra suurendades lisandub vaid kaks arvutusoperatsiooni:
Nüüd kasvab alamülesannete arv lineaarselt: N ühe võrra suurendades lisandub vaid kaks arvutusoperatsiooni:
alamylesandeid1(0,1):-!.
alamylesandeid1(N,Y):-
N1 is N-1,
alamylesandeid1(N1,Y1),
Y is Y1 + 2.
 
  ?- alamylesandeid1(20,Y)
Y = 41 
  ?- alamylesandeid1(100,Y)
Y = 201 
Vaatleme näitena nimistu tagurpidi pööramist. Naiivne ettekujutus selle ülesande sooritamiseks vajalikust predikaadist 
on tavaliselt selline: pöörame nimistu Keha tagurpidi ja siis liidame selle lõppu nimistu Pea, s.t. üheelemendilise nimistu, mis 
koosneb nimistu Pea-st:
nreverse([X|L0],L) :-
nreverse(L0,L1),
concatenate(L1,[X],L).
nreverse([],[]).
concatenate([X|L1],L2,[X|L3]) :-
  concatenate(L1,L2,L3).
  concatenate([],L,L).
 
Predikaat nreverse pole lõpprekursiivne. Tegelikult on nimistu tagurpidi pööramine väga lihtne lõpprekursiivne ülesanne: kui 
järjest tõsta esialgse nimistu esimene element teise nimistu (mis alguses on tühi) algusesse, siis viimasena tõstetakse esimese nimistu viimane element, 
s.t. tulemuseks saamegi esialgse nimistu tagurpidi kujul:
pööra([E|L1],L2,L):-
pööra(L1,[E|L2],L).
pööra([], L,L).
integers(N,NL):-
integers(1,N,NL).
integers(Low, High, [Low | Rest]) :- 
Low =< High, !,
M is Low+1,
integers(M, High, Rest).
integers(_,_,[]).
test1(N) :-
integers(N,L),
nreverse(L,X).
test2(N):-
  integers(N,L),
  reverse1(L,[],TL).
?- time(test1(200)).
  % 20,906 inferences, 0.01 CPU in 0.01 seconds (100% CPU, 2087594 Lips)
  ?- time(test2(200)).
  % 806 inferences, 0.00 CPU in 0.00 seconds (?% CPU, Infinite Lips)
 
 - lõpprekursiivse meetodi kasutamisel tegi Prolog peaaegu kaks pool korda vähem
 tööd
 !
 
Ülesandeid:
 
 1. 
  Fibonacci jada tüüpi rekursiivselt määratud jadasid kasutatakse sageli nn pseudojuhuslike arvude 
  saamiseks (arvuti on deterministlik ja siin täiesti juhuslikke arve pole võimalik saada); sageli kasutatakse näiteks jadasid
  
  X(N) = X(N-17) + X(N-5) (mod 2**31)
  (selle jada periood on 2**47). Tee programm selliste pseudojuhuslike arvude arvutamiseks (loomulikult peab arvud X(0),X(1),...,X(16) ette andma,
  kuid need võib valida vabalt, kui vad vaid ei ole kõik samad).   
 
Küsimused, probleemid: 
 ©2004
    Jaak Henno
  ©2004
    Jaak Henno