Ogni ricorsione può essere convertita in iterazione?


181

Un thread reddit ha sollevato una domanda apparentemente interessante:

Le funzioni ricorsive della coda possono essere banalmente convertite in funzioni iterative. Altri, possono essere trasformati utilizzando uno stack esplicito. Ogni ricorsione può essere trasformata in iterazione?

L'esempio (contatore?) Nel post è la coppia:

(define (num-ways x y)
  (case ((= x 0) 1)
        ((= y 0) 1)
        (num-ways2 x y) ))

(define (num-ways2 x y)
  (+ (num-ways (- x 1) y)
     (num-ways x (- y 1))

3
Non vedo come questo sia un controesempio. La tecnica dello stack funzionerà. Non sarà carino e non lo scriverò, ma è fattibile. Sembra che akdas lo riconosca nel tuo link.
Matthew Flaschen,

Il tuo (num-way xy) è solo (x + y) choosex = (x + y)! / (X! Y!), Che non ha bisogno di ricorsione.
ShreevatsaR,


Direi che la ricorsione è solo una comodità.
e2-e4,

Risposte:


181

Puoi sempre trasformare una funzione ricorsiva in una funzione iterativa? Sì, assolutamente, e la tesi di Church-Turing lo dimostra se la memoria serve. In termini laici, afferma che ciò che è calcolabile dalle funzioni ricorsive è calcolabile da un modello iterativo (come la macchina di Turing) e viceversa. La tesi non ti dice esattamente come fare la conversione, ma dice che è sicuramente possibile.

In molti casi, convertire una funzione ricorsiva è semplice. Knuth offre diverse tecniche in "The Art of Computer Programming". E spesso, una cosa calcolata in modo ricorsivo può essere calcolata con un approccio completamente diverso in meno tempo e spazio. Il classico esempio di ciò sono i numeri di Fibonacci o le loro sequenze. Hai sicuramente incontrato questo problema nel tuo piano di studi.

Sul rovescio della medaglia, possiamo certamente immaginare un sistema di programmazione così avanzato da trattare una definizione ricorsiva di una formula come un invito a memorizzare i risultati precedenti, offrendo così il vantaggio di velocità senza il fastidio di dire esattamente al computer quali passi per seguire nel calcolo di una formula con una definizione ricorsiva. Dijkstra quasi certamente immaginava un tale sistema. Ha trascorso molto tempo cercando di separare l'implementazione dalla semantica di un linguaggio di programmazione. Inoltre, i suoi linguaggi di programmazione non deterministici e multiprocessore sono in una lega superiore al programmatore professionista praticante.

In ultima analisi, molte funzioni sono semplicemente più facili da capire, leggere e scrivere in forma ricorsiva. A meno che non ci sia un motivo convincente, probabilmente non dovresti (manualmente) convertire queste funzioni in un algoritmo esplicitamente iterativo. Il tuo computer gestirà quel lavoro correttamente.

Vedo un motivo convincente. Supponiamo di avere un prototipo di sistema in un linguaggio di altissimo livello come [ indossare biancheria intima di amianto ] Scheme, Lisp, Haskell, OCaml, Perl o Pascal. Supponiamo che le condizioni siano tali da richiedere un'implementazione in C o Java. (Forse è politica.) Quindi potresti sicuramente avere alcune funzioni scritte in modo ricorsivo ma che, tradotte letteralmente, farebbero esplodere il tuo sistema di runtime. Ad esempio, in Scheme è possibile la ricorsione infinita della coda, ma lo stesso linguaggio causa un problema per gli ambienti C esistenti. Un altro esempio è l'uso di funzioni nidificate lessicali e ambito statico, che Pascal supporta ma C no.

In queste circostanze, potresti provare a superare la resistenza politica alla lingua originale. Potresti ritrovarti a reimplementare male Lisp, come nella decima legge di Greenspun. Oppure potresti trovare un approccio completamente diverso alla soluzione. Ma in ogni caso, c'è sicuramente un modo.


10
Church-Turing non è ancora stato provato?
Liran Orevi,

15
@eyelidlessness: se puoi implementare A in B, significa che B ha almeno la stessa potenza di A. Se non puoi eseguire qualche dichiarazione di A in A-implementazione-di-B, allora non è un'implementazione. Se A può essere implementato in B e B può essere implementato in A, potenza (A)> = potenza (B) e potenza (B)> = potenza (A). L'unica soluzione è potenza (A) == potenza (B).
Tordek,

6
ri: 1o paragrafo: Stai parlando dell'equivalenza dei modelli di calcolo, non della tesi di Church-Turing. L'equivalenza è stata dimostrata da AFAIR da Church e / o Turing, ma non è la tesi. La tesi è un fatto sperimentale che tutto ciò che è intuitivamente calcolabile è calcolabile in senso matematico rigoroso (da macchine di Turing / funzioni ricorsive ecc.). Potrebbe essere smentito se usando le leggi della fisica potessimo costruire alcuni computer non classici che calcolano qualcosa che le macchine di Turing non possono fare (es. Fermare il problema). Mentre l'equivalenza è un teorema matematico e non sarà smentito.
sdcvvc,

7
In che modo questa risposta ha ottenuto voti positivi? Prima mescola la completezza di Turing con la tesi di Church-Turing, quindi crea un insieme di errate onde manuali, menzionando sistemi "avanzati" e rilasciando una pigra ricorsione infinita della coda (che puoi fare in C o in qualsiasi linguaggio completo di Turing perché ... uh. Qualcuno sa cosa significa Turing completo?). Quindi una speranzosa conclusione di mano, come questa era una domanda su Oprah e tutto ciò di cui hai bisogno è essere positivo ed edificante? Risposta orribile!
ex0du5,

8
E i bs sulla semantica ??? Veramente? Questa è una domanda sulle trasformazioni sintattiche, e in qualche modo è diventato un ottimo modo per nominare drop Dijkstra e sottintendere che tu sappia qualcosa sul pi-calculus? Consentitemi di chiarire questo aspetto: se si guardi alla semantica denotazionale di una lingua o qualche altro modello non avrà alcuna attinenza con la risposta a questa domanda. Che la lingua sia assembly o un linguaggio di modellazione del dominio generativo non significa nulla. Si tratta solo della completezza di Turing e della trasformazione di "variabili stack" in "una pila di variabili".
ex0du5,

43

È sempre possibile scrivere un modulo non ricorsivo per ogni funzione ricorsiva?

Sì. Una semplice prova formale è quella di dimostrare che entrambe le ricorsioni µ che un calcolo non ricorsivo come GOTO sono entrambi Turing completi. Poiché tutti i calcoli completi di Turing sono strettamente equivalenti nella loro potenza espressiva, tutte le funzioni ricorsive possono essere implementate dal calcolo non ricorsivo di Turing.

Sfortunatamente, non riesco a trovare una buona definizione formale di GOTO online, quindi eccone una:

Un programma GOTO è una sequenza di comandi P eseguiti su una macchina del registro tale che P è uno dei seguenti:

  • HALT, che interrompe l'esecuzione
  • r = r + 1 dove r qualsiasi registro
  • r = r – 1 dove r qualsiasi registro
  • GOTO x dove x un'etichetta
  • IF r ≠ 0 GOTO xdov'è rqualsiasi registro ex è un'etichetta
  • Un'etichetta, seguita da uno qualsiasi dei comandi precedenti.

Tuttavia, le conversioni tra funzioni ricorsive e non ricorsive non sono sempre banali (tranne che per una razionale implementazione manuale dello stack di chiamate).

Per ulteriori informazioni consultare questa risposta .


Bella risposta! Tuttavia, nella pratica, ho grandi difficoltà a trasformare gli algos ricorsivi in ​​quelli iterativi. Ad esempio, finora non sono stato in grado di trasformare il typer monomorfo presentato qui community.topcoder.com/… in un algoritmo iterativo
Nils

31

La ricorsione è implementata come stack o costrutti simili negli interpreti o nei compilatori reali. Quindi puoi sicuramente convertire una funzione ricorsiva in una controparte iterativa perché è così che viene sempre eseguita (se automaticamente) . Duplicherai semplicemente il lavoro del compilatore in modo ad hoc e probabilmente in modo molto brutto e inefficiente.


13

Fondamentalmente sì, in sostanza quello che devi fare è sostituire le chiamate di metodo (che implicitamente spingono lo stato sullo stack) in push espliciti dello stack per ricordare dove era arrivata la "chiamata precedente" e quindi eseguire il "metodo chiamato" anziché.

Immagino che la combinazione di un ciclo, uno stack e una macchina a stati possa essere utilizzata per tutti gli scenari simulando sostanzialmente le chiamate al metodo. Se questo sarà o meno "migliore" (o più veloce o più efficiente in un certo senso) non è davvero possibile dire in generale.


9
  • Il flusso di esecuzione della funzione ricorsiva può essere rappresentato come un albero.

  • La stessa logica può essere fatta da un ciclo, che usa una struttura di dati per attraversare quell'albero.

  • Il primo attraversamento in profondità può essere fatto usando uno stack, il primo in movimento può essere fatto usando una coda.

Quindi la risposta è sì. Perché: https://stackoverflow.com/a/531721/2128327 .

È possibile eseguire una ricorsione in un singolo ciclo? Si perchè

una macchina Turing fa tutto ciò che fa eseguendo un singolo ciclo:

  1. recuperare un'istruzione,
  2. valutalo,
  3. vai 1.

7

Sì, usando esplicitamente uno stack (ma la ricorsione è molto più piacevole da leggere, IMHO).


17
Non direi che è sempre più piacevole da leggere. Sia l'iterazione che la ricorsione hanno il loro posto.
Matthew Flaschen,

6

Sì, è sempre possibile scrivere una versione non ricorsiva. La banale soluzione consiste nell'utilizzare una struttura di dati dello stack e simulare l'esecuzione ricorsiva.


Che o vanifica lo scopo se la struttura dei dati dello stack è allocata nello stack o impiega più tempo se è allocata nell'heap, no? Sembra banale ma inefficiente per me.
Conradkleinespel,

1
@conradk In alcuni casi, è la cosa pratica da fare se è necessario eseguire un'operazione ricorsiva ad albero su un problema sufficientemente grande da esaurire lo stack di chiamate; la memoria heap è in genere molto più abbondante.
jamesdlin,

4

In linea di principio è sempre possibile rimuovere la ricorsione e sostituirla con iterazione in un linguaggio che ha uno stato infinito sia per le strutture di dati che per lo stack di chiamate. Questa è una conseguenza fondamentale della tesi di Church-Turing.

Dato un vero linguaggio di programmazione, la risposta non è così ovvia. Il problema è che è del tutto possibile avere una lingua in cui la quantità di memoria che può essere allocata nel programma è limitata ma in cui la quantità di stack di chiamate che può essere utilizzata è illimitata (32-bit C dove l'indirizzo delle variabili dello stack non è accessibile). In questo caso, la ricorsione è più potente semplicemente perché ha più memoria che può usare; non c'è abbastanza memoria allocabile esplicitamente per emulare lo stack di chiamate. Per una discussione dettagliata su questo, vedere questa discussione .


2

Tutte le funzioni calcolabili possono essere calcolate dalle macchine di Turing e quindi i sistemi ricorsivi e le macchine di Turing (sistemi iterativi) sono equivalenti.


1

A volte sostituire la ricorsione è molto più semplice di così. La ricorsione era la cosa alla moda insegnata in CS negli anni '90, e quindi molti sviluppatori medi di quel tempo pensarono che se avessi risolto qualcosa con la ricorsione, era una soluzione migliore. Quindi userebbero la ricorsione invece di fare un ciclo indietro per invertire l'ordine, o cose stupide come quella. Quindi a volte rimuovere la ricorsione è un semplice tipo di esercizio "duh, era ovvio".

Questo è meno un problema ora, poiché la moda si è spostata verso altre tecnologie.



0

Appart dallo stack esplicito, un altro modello per convertire la ricorsione in iterazione è con l'uso di un trampolino.

Qui, le funzioni restituiscono il risultato finale o una chiusura della chiamata di funzione che altrimenti avrebbe eseguito. Quindi, la funzione di avvio (trampolino) continua a invocare le chiusure restituite fino al raggiungimento del risultato finale.

Questo approccio funziona per le funzioni reciprocamente ricorsive, ma temo che funzioni solo per le chiamate in coda.

http://en.wikipedia.org/wiki/Trampoline_(computers)


0

Direi di sì - una chiamata di funzione non è altro che un'operazione goto e stack (in parole povere). Tutto quello che devi fare è imitare lo stack che viene creato mentre invochi funzioni e fare qualcosa di simile a un goto (puoi imitare goto con lingue che non hanno esplicitamente anche questa parola chiave).


1
Penso che l'OP stia cercando una prova o qualcos'altro di sostanziale
Tim



-1

tazzego, ricorsione significa che una funzione si chiamerà se ti piace o no. Quando le persone parlano del fatto che le cose possano essere fatte o meno senza ricorsione, significano questo e non si può dire "no, non è vero, perché non sono d'accordo con la definizione di ricorsione" come affermazione valida.

Con questo in mente, quasi tutto il resto che dici è una sciocchezza. L'unica altra cosa che dici che non ha senso è l'idea che non puoi immaginare di programmare senza un callstack. Questo è qualcosa che era stato fatto per decenni fino a quando l'uso di un callstack non è diventato popolare. Le vecchie versioni di FORTRAN mancavano di un callstack e funzionavano perfettamente.

A proposito, esistono linguaggi completi di Turing che implementano la ricorsione (es. SML) solo come mezzo per eseguire il looping. Esistono anche linguaggi completi di Turing che implementano l'iterazione solo come mezzo di looping (ad esempio FORTRAN IV). La tesi di Church-Turing dimostra che tutto il possibile in una lingua solo ricorsiva può essere fatto in una lingua non ricorsiva e viceversa dal fatto che entrambi hanno la proprietà della completezza di turing.


-3

Ecco un algoritmo iterativo:

def howmany(x,y)
  a = {}
  for n in (0..x+y)
    for m in (0..n)
      a[[m,n-m]] = if m==0 or n-m==0 then 1 else a[[m-1,n-m]] + a[[m,n-m-1]] end
    end
  end
  return a[[x,y]]
end
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.