Professional Documents
Culture Documents
Procedure ricorsive
Luso di procedure ricorsive permette spesso di descrivere un algoritmo in maniera semplice e concisa,
mettendo in rilievo la tecnica adottata per la soluzione del problema e facilitando quindi la fase di pro-
gettazione. Inoltre lanalisi risulta in molti casi semplificata poiche la valutazione del tempo di calcolo
si riduce alla soluzione di equazioni di ricorrenza.
Questo capitolo e` dedicato allanalisi delle procedure di questo tipo; vogliamo innanzitutto mostrare
come in generale queste possano essere trasformate in programmi iterativi, che non prevedono cio`e
lesecuzione di chiamate ricorsive, direttamente implementabili su macchine RASP (o RAM). Un pregio
particolare di questa traduzione e` quello di mettere in evidenza la quantit`a di spazio di memoria richiesto
dalla ricorsione.
Tra gli algoritmi che si possono facilmente descrivere mediante procedure ricorsive risultano di
particolare interesse la ricerca binaria e gli algoritmi di esplorazione di alberi e grafi.
63
64 CAPITOLO 5. PROCEDURE RICORSIVE
B
@ B
@ B
@ B
@ B
@ B
@ B
@ B
@ B
@B
@B
B@
B @
B @
B @
B @
B @
B @
r B @
B @
r1 r2 B r3 @ rn
naturale ad essere risolti con procedure ricorsive, ottenendo algoritmi risolutivi in generale semplici e
chiari.
Una difficolt`a che sorge e` legata al fatto che il modello di macchina RAM (o RASP) non e` in grado
di eseguire direttamente algoritmi ricorsivi. Ne consegue che e` di estremo interesse trovare tecniche di
traduzione di procedure ricorsive in codice RAM e sviluppare metodi che permettano di valutare le mis-
ure di complessit`a dellesecuzione del codice tradotto semplicemente analizzando le procedure ricorsive
stesse. Qui delineiamo una tecnica per implementare la ricorsione su macchine RASP semplice e diret-
ta, mostrando nel contempo che il problema di stimare il tempo di calcolo del programma ottenuto pu`o
essere ridotto alla soluzione di equazioni di ricorrenza. Questo metodo di traduzione delle procedure ri-
corsive in programmi puramente iterativi (nei quali cio`e la ricorsione non e` permessa) e` del tutto generale
e a grandi linee e` lo stesso procedimento applicato dai compilatori che di fatto traducono in linguaggio
macchina programmi scritti in un linguaggio ad alto livello.
Cominciamo osservando che la chiamata di una procedura B da parte di A (qui A pu`o essere il pro-
gramma principale, oppure una procedura, oppure la procedura B stessa) consiste essenzialmente di due
operazioni:
1. lesecuzione di B pu`o terminare; a questo punto viene passato ad A il valore della funzione calcolata
da B (se B e` una procedura di calcolo di una funzione) e lesecuzione di A riprende dal punto di
ritorno;
Come conseguenza si ha che lultima procedura chiamata e` la prima a terminare lesecuzione: questo
giustifica luso di una pila per memorizzare i dati di tutte le chiamate di procedura che non hanno ancora
terminato lesecuzione.
Gli elementi della pila sono chiamati record di attivazione e sono identificati da blocchi di registri
consecutivi; ogni chiamata di procedura usa un record di attivazione per memorizzare i dati non globali
utili.
Record di attivazione:
Chiamata di A
..
.
Record di attivazione:
Programma Principale
Supponiamo che, come nello schema precedente, la procedura A sia correntemente in esecuzione e
chiami la procedura B. In questo caso lesecuzione di B prevede le seguenti fasi:
1. Viene posto al top nella pila un nuovo record di attivazione per la chiamata di B di opportuna
dimensione; tale record contiene:
(a) se B e` una procedura che calcola una funzione, lesecuzione ha termine con un comando
return E; il valore di E viene calcolato e passato allopportuna variabile nel record di
attivazione della chiamata di A;
(b) il punto di ritorno e` ottenuto dal record di attivazione di B;
(c) il record di attivazione di B viene tolto dal top della pila; lesecuzione di A pu`o continuare.
E` quindi possibile associare ad un algoritmo ricorsivo un algoritmo eseguibile su RASP (o RAM) che
calcola la stessa funzione. Tale algoritmo rappresenter`a la semantica operazionale RASP dellalgoritmo
ricorsivo.
Allo scopo di disegnare schematicamente lalgoritmo, diamo per prima cosa la descrizione del record
di attivazione di una procedura A del tipo:
66 CAPITOLO 5. PROCEDURE RICORSIVE
Procedura A
..
.
z := B(a)
(1) Istruzione
..
.
Se nel corso della esecuzione A chiama B, il record di attivazione di B viene mostrato nella figura
5.2.
Variabili locali di B
Record di attivazione
della chiamata di B c@ r c I (1)
Parametro@
formale Punto di ritorno
@
Record di attivazione @
R
@
della chiamata di A
Variabili locali di A
Il cuore della simulazione iterativa di una procedura ricorsiva e` allora delineato nel seguente schema:
Figura 5.3: Contenuto della pila per la procedura FIB su ingresso 3 nei primi 4 passi di esecuzione.
Affrontiamo ora il problema di analisi degli algoritmi ricorsivi: dato un algoritmo ricorsivo, stimare
il tempo di calcolo della sua esecuzione su macchina RASP (o RAM). Tale stima pu`o essere fatta agevol-
mente senza entrare nei dettagli della esecuzione iterativa, ma semplicemente attraverso lanalisi della
procedura ricorsiva stessa.
Si procede come segue, supponendo che lalgoritmo sia descritto da M procedure P1 , P2 , . . . , PM ,
comprendenti il programma principale:
2. Si esprime Tk (n) in funzione dei tempi delle procedure chiamate da Pk , valutati negli opportuni
valori dei parametri; a tal riguardo osserviamo che il tempo di esecuzione della istruzione z :=
Pj (a) e` la somma del tempo di esecuzione della procedura Pj su ingresso a, del tempo di chiamata
di Pj (necessario a predisporre il record di attivazione) e di quello di ritorno.
68 CAPITOLO 5. PROCEDURE RICORSIVE
Si ottiene in tal modo un sistema di M equazioni di ricorrenza che, risolto, permette di stimare Tk (n)
per tutti i k (1 k M ), ed in particolare per il programma principale. Allo studio delle equazioni di
ricorrenza sar`a dedicato il prossimo capitolo.
A scopo esemplificativo, effettuiamo una stima del tempo di calcolo della procedura FIB(n) col
criterio di costo uniforme. Per semplicit`a, supponiamo che la macchina sia in grado di effettuare una
chiamata in una unit`a di tempo (pi`u unaltra unit`a per ricevere il risultato). Sia TF IB (n) il tempo di
calcolo (su RAM !) della procedura ricorsiva FIB su input n; tale valore pu`o essere ottenuto attraverso
lanalisi dei tempi richiesti dalle singole istruzioni:
else a := n 1 (T empo : 3)
x := FIB(a) (T empo : 2 + TF IB (n 1) )
(A) b := n 2 (T empo : 3)
y := FIB(b) (T empo : 2 + TF IB (n 2) )
(B) return(x + y) (T empo : 4)
Esercizi
1) Valutare lordine di grandezza dello spazio di memoria richiesto dalla procedura FIB su input n assumen-
do il criterio di costo uniforme.
2) Svolgere lesercizio precedente assumendo il criterio di costo logaritmico.
Procedura F (x)
if C(x) then D
else begin
E
y := g(x)
F (y)
end
Qui C(x) e` una condizione che dipende dal valore di x, mentre E e D sono opportuni blocchi di
istruzioni. La funzione g(x) invece determina un nuovo valore del parametro di input per la F di
dimensione ridotta rispetto a quello di x.
Allora, se a e` un qualunque valore per x, la chiamata F (a) e` equivalente alla seguente procedura:
begin
x := a
while C(x) do
begin
E
x := g(x)
end
D
end
Il problema pu`o essere risolto applicando il noto procedimento di ricerca binaria. Questo consiste nel
confrontare a con lelemento del vettore B di indice k = b n+1 2 c. Se i due elementi sono uguali si
restituisce lintero k; altrimenti, si prosegue la ricerca nel sottovettore di sinistra, (B[1], . . . , B[k 1]),
o in quello di destra, (B[k + 1], . . . , B[n]), a seconda se a < B[k] oppure a > B[k].
Il procedimento e` definito in maniera naturale mediante la procedura ricorsiva Ricercabin(i, j) che
descriviamo nel seguito. Questa svolge la ricerca nel sottovettore delle componenti di B comprese tra
gli indici i e j, 1 i j n, supponendo i parametri a e B come variabili globali.
70 CAPITOLO 5. PROCEDURE RICORSIVE
Procedura Ricercabin(i, j)
if j < i then return 0
else begin
k := b i+j
2 c
if a = B[k] then return k
else if a < B[k] then return Ricercabin(i, k 1)
else return Ricercabin(k + 1, j)
end
Lalgoritmo che risolve il problema e` quindi limitato alla chiamata Ricercabin(1, n). La sua analisi e`
molto semplice poiche , nel caso peggiore, a ogni chiamata ricorsiva la dimensione del vettore su cui si
svolge la ricerca viene dimezzata. Assumendo quindi il criterio di costo uniforme lalgoritmo termina in
O(log n) passi.
Cerchiamo ora di descrivere una versione iterativa dello stesso procedimento. Osserviamo che il
programma appena descritto esegue una ricorsione terminale e questa e` lunica chiamata ricorsiva della
procedura. Applicando allora il procedimento definito sopra otteniamo il seguente programma iterativo
nel quale non viene utilizzata alcuna pila.
begin
i := 1
j := n
out := 0
while i j out = 0 do
begin
k := b i+j
2 c
if a = B[k] then out := k
else if a < B[k] then j := k 1
else i := k + 1
end
return out
end
Osserva che lo spazio di memoria richiesto da questultima procedura, escludendo quello necessario per
mantenere il vettore B di ingresso, e` O(1) secondo il criterio uniforme.