La procedura ricorsiva causerà un overflow dello stack? Dipende dal tipo di procedura ricorsiva che hai progettato, a seconda del problema, la maggior parte delle ingenue ricusioni possono essere convertite in chiamate di coda (in linguaggio TCO ottimizzato chiamata coda), che consente a una ricorsione di essere eseguita nello spazio di memoria costante senza usare la mutazione o altre cose di stato.
Nello schema, un processo iterativo:
(let ((i 0)
(max 10))
(let loop()
(cond ((< i max)
(printf "~A~N" i)
(set! i (+ i 1))
(loop))
(else i))))
Questa procedura utilizza memoria costante, che è uguale allo spazio necessario per memorizzare la chiamata al ciclo sulla pila. questa procedura non è una funzione, usa la mutazione per iterare (è anche una ricorsione;) ma ..).
Nello schema, due ricorsioni:
(define (fact-1 n)
(cond ((eq? n 1) n)
(else (* n (fact-1 (- n 1))))))
(define (fact-2 n carry)
(cond ((eq? n 1) carry)
(else (fact-2 (- n 1) (* carry n)))))
Fact-1 è una ricorsione normale, ed è molto funzionale, non v'è alcun cambiamento di stato, invece, l'uso della memoria cresce come nuove chiusure lessicali sono creato con ciascuna chiamata fact-1
, esaurendo infine la pila. Cresce come
=>(fact-1 10)
..(* 10 (fact-1 9))
..(* 10 (* 9 (fact-1 8)))
..(* 10 (* 9 (* 8 (fact-1 7))))
.. .....
..(* 10 (... (* 2 1) ...))
.. .....
..(* 10 362880)
=>3628800
considerando Fact-2 è ricorsiva, ma in forma di coda, così invece di costruire stack, e comprimendo le chiamate allo scenario di base, il valore viene passato avanti e otteniamo questo:
=>(fact-2 10 1)
..(fact-2 9 10)
..(fact-2 8 90)
..(fact-2 7 720)
.. .......(fact-2 1 362880)
=>3628800
Che equivale a rendere fact-1 in un processo di interazione, ma senza alcuna mutazione, in quanto i valori vengono inoltrati anziché essere assegnati. Si noti che ogni chiamata produce ancora una nuova chiusura lessicale, ma poiché la funzione non ritorna al chiamante originale, ma alla posizione dello stack dei chiamanti originali, il compilatore può scartare le chiusure precedenti invece di averle annidate l'una nell'altra, re-binding le variabili a ciascun livello di ricorsione.
Quindi, dove dovrei usare la ricorsione o l'iterazione Ciò dipende interamente dal processo da progettare e dal linguaggio utilizzato. Se la tua lingua non supporta TCO, dovrai utilizzare solo ricursioni superficiali e scrivere in loop procedure (ricorsive o iterative) in modo statico. Se si dispone di TCO, potrebbe essere preferibile utilizzare la ricorsione, le chiamate di coda o le informazioni sullo stato o una combinazione di esse. Non tutte le procedure ricorsive possono essere scritte in coda, e non tutti i processi iterativi possono essere scritti come ricorsione. Se sei preoccupato per l'utilizzo della memoria e desideri ricchezioni profonde, devi utilizzare le Tail-calls.
NOTA: Alcuni di voi avranno notato, ma la prima procedura è in realtà una chiamata coda troppo, ma l'esempio illustra ancora il punto di un'iterazione normale fare stateful! cose e in esecuzione nella memoria massima costante indipendentemente da tutti gli input validi.
Ovviamente, lo stack overflow [** può ** accadere in lisp] (http://stackoverflow.com/questions/15269193/stack-overflow-from-recursive-function-call-in-lisp), pure come Java. – azurefrog
@azurefrog modificato, grazie – morbidCode
La parte località si riferisce alla località di memoria, che coinvolge il modo in cui i valori vicini vengono memorizzati nella memoria. La località può spesso influire sulle prestazioni in modo drastico a causa del modo in cui funzionano le cache della CPU. Senza guardare la clip in questione, solo leggendo la citazione, la mia ipotesi su ciò che Harvey sta dicendo è che si ha un controllo molto piccolo sulla localizzazione della memoria in senso generale, specialmente nei linguaggi di programmazione di livello superiore, e quindi si scrivono algoritmi iterativi invece di quelli ricorsivi non influenzeranno questo. –