L'iterazione può sostituire la ricorsione?


42

Ho visto tutto lo stack Overflow, ad esempio qui , qui , qui , qui , qui e alcuni altri che non mi interessa menzionare, che "qualsiasi programma che utilizza la ricorsione può essere convertito in un programma usando solo l'iterazione".

C'era anche un thread molto votato con una risposta molto votata che diceva sì, è possibile.

Ora non sto dicendo che si sbagliano. È solo che quella risposta contrasta la mia scarsa conoscenza e comprensione sull'informatica.

Credo che ogni funzione iterativa possa essere espressa come ricorsione, e Wikipedia ha una dichiarazione in tal senso. Tuttavia, dubito che il contrario sia vero. Per uno, dubito che le funzioni ricorsive non primitive possano essere espresse in modo iterativo.

Dubito anche che le iperoperazioni possano essere espresse in modo iterativo.

Nella sua risposta (che non capisco a proposito) alla mia domanda @YuvalFIlmus ha affermato che non è possibile convertire alcuna sequenza di operazioni matematiche in una sequenza di aggiunte.

Se la risposta di YF è davvero corretta (immagino che lo sia, ma il suo ragionamento era al di sopra della mia testa), ciò non significa che non tutte le ricorsioni possono essere convertite in iterazioni? Perché se fosse possibile convertire ogni ricorsione in iterazione, sarei in grado di esprimere tutte le operazioni come una sequenza di aggiunte.

La mia domanda è questa:

Ogni ricorsione può essere convertita in iterazione e perché?

Per favore, dai una risposta a un brillante liceale o capirà un corso di laurea del primo anno. Grazie.

PS Non so che cosa sia ricorsivo primitivo (so della funzione Ackermann, e che non è ricorsivo primitivo, ma è ancora calcolabile. Tutte le mie conoscenze su di esso provengono dalla pagina Wikipedia sulla funzione Ackermann.)

PPS: se la risposta è sì, potresti ad esempio scrivere una versione iterativa di una funzione non primitiva-ricorsiva. Ad esempio Ackermann nella risposta. Mi aiuterà a capire.


21
Tutto ciò che esegui su una CPU è iterativo. Potresti scriverlo in modo ricorsivo in un linguaggio di ordine superiore, ma viene comunque compilato in un gruppo di istruzioni iterative assembler. Quindi, letteralmente, il compilatore fa esattamente quello che stai chiedendo, converte tutta la tua ricorsiva fantasia in un mucchio di istruzioni iterative per una CPU.
Davor

6
A basso livello, la maggior parte di noi sa che la ricorsione è uguale a iterazione più stack. La ricorsione del modello di grammatiche senza contesto, mentre gli automi pushdown sono "programmi" iterativi con memoria stack.
Hendrik Jan

2
Basta sottolineare che è generalmente una buona idea attendere almeno 24 ore per vedere se compaiono altre risposte. La domanda accettata mi sembra abbastanza lunga e contorta per me, francamente, e sembra complicare deliberatamente le cose più del necessario. La risposta di base alla tua domanda è "lo stack utilizzato per la ricorsione può essere esplicitamente implementato in modo iterativo" e non deve essere molto più complicato di così. Considerazioni sul fatto che la memoria sia illimitata o non entrano in gioco in quanto tale funzionalità è necessaria anche per gli stack di ricorsione.
AnoE,

è possibile implementare l'emulatore di macchine di Turing con solo iterazione
Sarge Borsch,

In una lezione di lingue comparate che ho preso molto tempo fa, dovevamo scrivere la funzione di Ackermann in assemblea, in FORTRAN (non nel moderno Fortran) e in un linguaggio ricorsivo a scelta. Quindi sì, è possibile in pratica. E ovviamente è possibile in teoria. Vedi, ad esempio, la domanda dimostra che le macchine di Turing e il calcolo lambda sono equivalenti .
David Hammen,

Risposte:


52

È possibile sostituire la ricorsione con iterazione più memoria illimitata .

Se hai solo iterazioni (diciamo, mentre loop) e una quantità finita di memoria, allora tutto ciò che hai è un automa finito. Con una quantità finita di memoria, il calcolo ha un numero finito di passaggi possibili, quindi è possibile simularli tutti con un automa finito.

La memoria illimitata modifica l'affare. Questa memoria illimitata può assumere molte forme che risultano avere un potere espressivo equivalente. Ad esempio, una macchina Turing lo rende semplice: c'è un singolo nastro e il computer può solo spostarsi avanti o indietro sul nastro di un passo alla volta, ma è abbastanza per fare tutto ciò che è possibile fare con le funzioni ricorsive.

Una macchina Turing può essere vista come un modello idealizzato di un computer (macchina a stati finiti) con un po 'di spazio in più che cresce su richiesta. Si noti che è fondamentale che non solo non ci sia un limite finito sul nastro, ma anche dato l'input, non è possibile prevedere in modo affidabile la quantità di nastro necessaria. Se potessi prevedere (cioè calcolare) la quantità di nastro necessaria dall'input, potresti decidere se il calcolo si fermerebbe calcolando la dimensione massima del nastro e quindi trattando l'intero sistema, incluso il nastro ormai finito, come una macchina a stati finiti .

Un altro modo per simulare una macchina Turing con i computer è il seguente. Simula la macchina Turing con un programma per computer che memorizza l'inizio del nastro in memoria. Se il calcolo raggiunge la fine della parte del nastro che si adatta alla memoria, sostituire il computer con un computer più grande ed eseguire nuovamente il calcolo.

Supponiamo ora di voler simulare un calcolo ricorsivo con un computer. Le tecniche per eseguire funzioni ricorsive sono ben note: ogni chiamata di funzione ha un pezzo di memoria, chiamato frame di stack . Fondamentalmente, le funzioni ricorsive possono propagare le informazioni attraverso più chiamate passando variabili. In termini di implementazione su un computer, ciò significa che una chiamata di funzione potrebbe accedere al frame dello stack di una chiamata (grand-) * parent.

Un computer è un processore - una macchina a stati finiti (con un numero enorme di stati, ma qui stiamo facendo la teoria dei calcoli, quindi tutto ciò che conta è che è finito) - accoppiato con una memoria finita. Il microprocessore esegue un loop gigante mentre: "mentre l'alimentazione è accesa, leggere un'istruzione dalla memoria ed eseguirla". (I processori reali sono molto più complessi di così, ma non influiscono su ciò che possono calcolare, solo quanto velocemente e comodamente lo fanno.) Un computer può eseguire funzioni ricorsive con questo ciclo while per fornire iterazione, oltre al meccanismo per accedere alla memoria, inclusa la possibilità di aumentare le dimensioni della memoria a piacimento.

Se si limita la ricorsione alla ricorsione primitiva, è possibile limitare l'iterazione all'iterazione limitata . Cioè, invece di utilizzare i cicli while con un tempo di esecuzione imprevedibile, è possibile utilizzare per i cicli in cui il numero di iterazioni è noto all'inizio del ciclo¹. Il numero di iterazioni potrebbe non essere noto all'inizio del programma: può essere stato calcolato da cicli precedenti.

Non ho nemmeno intenzione di delineare una prova qui, ma esiste una relazione intuitiva tra il passaggio dalla ricorsione primitiva alla ricorsione completa e il passaggio da cicli a cicli continui: in entrambi i casi, implica non sapere in anticipo quando Stop. Con la ricorsione completa, questo viene fatto con l'operatore di minimizzazione, dove si continua fino a quando non si trova un parametro che soddisfa la condizione. Con i cicli while, questo viene fatto continuando fino a quando la condizione del loop è soddisfatta.

¹ I loop in linguaggi di tipo C possono eseguire iterazioni illimitate proprio come , è solo una questione di convenzione limitarli a iterazioni limitate. Quando le persone parlano di "per loop" nella teoria del calcolo, ciò significa solo loop che contano da 1 a n (o equivalenti). forwhilen


Accettando la spiegazione dettagliata, mantenuta al livello richiesto.
Tobi Alafin,

12
Penso che valga la pena notare che nei computer reali, la ricorsione consuma anche memoria (stack di chiamate in crescita). Quindi, nelle applicazioni pratiche, non vi è alcun requisito di memoria illimitata (perché tutto è delimitato da essa equamente). E la ricorsione spesso ha bisogno di più memoria dell'iterazione.
Agent_L

@Agent_L Sì, la ricorsione richiede memoria illimitata, per memorizzare tutti i frame dello stack. Con un approccio ricorsivo, è necessaria una memoria illimitata, ma non è intuitivamente separabile dal sequenziamento di operazioni come con le iterazioni.
Gilles 'SO- smetti di essere malvagio' il

1
Forse di interesse sarebbe la tesi di Church-Turing. Le macchine di Turing sono macchine altamente iterative senza alcun concetto (inerente) di ricorsione. Il calcolo lambda è un linguaggio ideato per esprimere i calcoli in modo ricorsivo. La tesi di Church-Turing ha mostrato che tutte le espressioni lambda potevano essere valutate su una macchina di Turing, collegando la ricorsione e l'iterazione.
Cort Ammon,

1
@BlackVegetable Se un metodo ricorsivo non ha variabili interne e l'unica memoria che utilizza è lo stack di chiamate (che può essere ottimizzato dal TCO), allora la sua versione iterativa probabilmente non avrà variabili interne e utilizzerà solo una quantità costante di memoria ( variabili) condivise tra tutte le iterazioni, come counter.
Agent_L

33

Ogni ricorsione può essere convertita in iterazione, come testimonia la tua CPU, che esegue programmi arbitrari usando un'iterazione infinita fetch-execute. Questa è una forma del teorema di Böhm-Jacopini . Inoltre, molti modelli di calcolo completi di Turing non presentano alcuna ricorsione, ad esempio macchine di Turing e macchine contatore .

Le funzioni ricorsive primitive corrispondono ai programmi che utilizzano l' iterazione limitata , ovvero è necessario specificare il numero di iterazioni in cui un ciclo viene eseguito in anticipo. L'iterazione limitata non può simulare la ricorsione in generale, poiché la funzione di Ackermann non è ricorsiva primitiva. Ma l'iterazione illimitata può simulare qualsiasi funzione parzialmente calcolabile.


25

un'(n,m)

S

spingere(S,X)XXpop(S)vuoto(S)

Ackermann(n0,m0):

  • S=
  • spingere(S,n0)
  • spingere(S,m0)
  • while (true):
    • mpop(S)
    • Se(vuoto(S)):
      • ritorno m
    • npop(S)
    • Se(n=0):
      • spingere(S,m+1)
    • altrimenti se (m=0):
      • spingere(S,n-1)
      • spingere(S,1)
    • altro:
      • spingere(S,n-1)
      • spingere(S,n)
      • spingere(S,m-1)

L'ho implementato in Ceylon, puoi eseguirlo nel WebIDE , se lo desideri. (Emette lo stack all'inizio di ogni iterazione del loop.)

Naturalmente, questo ha appena spostato lo stack di chiamate implicite dalla ricorsione in uno stack esplicito con i parametri e i valori di ritorno.


16
Penso che questo sia un punto importante. Hai dimostrato abbastanza esplicitamente che la ricorsione non è niente di speciale e che può essere rimossa semplicemente tenendo traccia dello stack di chiamate, piuttosto che lasciare che il compilatore lo faccia. La ricorsione è solo una funzione del compilatore che semplifica la scrittura di questo tipo di programma.
David Richerby,

4
Grazie per lo sforzo di scrivere il programma richiesto.
Tobi Alafin,

16

Ci sono già alcune grandi risposte (con le quali non posso nemmeno sperare di competere), ma vorrei presentare questa semplice spiegazione.

La ricorsione è solo la manipolazione dello stack di runtime. La ricorsione aggiunge un nuovo frame di stack (per la nuova invocazione della funzione ricorsiva) e il ritorno rimuove un frame di stack (per l'innovazione appena completata della funzione ricorsiva). La ricorsione causerà l'aggiunta / la rimozione di un numero di frame di stack, fino a quando alla fine non si chiudono tutti (si spera!) E il risultato viene restituito al chiamante.

Ora, cosa succederebbe se creassi il tuo stack, sostituissi le chiamate di funzioni ricorsive con lo push nello stack, sostituissi il ritorno da funzioni ricorsive con il pop-up dello stack e avessi un ciclo while che funzionava fino a quando lo stack era vuoto?


2

Per quanto ne so, e nella mia esperienza, puoi implementare qualsiasi ricorsione come iterazione. Come accennato in precedenza, la ricorsione utilizza lo stack, che è concettualmente illimitato, ma praticamente limitato (hai mai ricevuto un messaggio di overflow dello stack?). Nei miei primi giorni di programmazione (nel terzo trimestre dell'ultimo secolo dell'ultimo millennio) stavo usando linguaggi non ricorsivi implementando algoritmi ricorsivi e non ho avuto problemi. Non sono sicuro di come si dimostrerebbe, però.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.