C'è qualcosa che può essere fatto con la ricorsione che non può essere fatto con i loop?


126

Ci sono momenti in cui usare la ricorsione è meglio che usare un ciclo e volte in cui usare un ciclo è meglio che usare la ricorsione. Scegliendo il "giusto" si possono risparmiare risorse e / o provocare un minor numero di righe di codice.

Esistono casi in cui un'attività può essere eseguita solo utilizzando la ricorsione, anziché un ciclo?


13
Ne dubito seriamente. La ricorsione è un ciclo glorificato.
Corse della leggerezza in orbita,

6
Vedendo le direzioni divergenti in cui vanno le risposte (e avendo appena fallito nel fornirne una migliore) potresti fare chiunque cerchi di rispondere a un favore se fornisci un po 'più di background e che tipo di risposta stai cercando. Vuoi una prova teorica per macchine ipotetiche (con spazio di archiviazione e tempo di esecuzione illimitati)? O esempi pratici? (Dove "sarebbe ridicolmente complicato" potrebbe qualificarsi come "impossibile da fare".) O qualcosa di diverso?
5gon12eder

8
@LightnessRacesinOrbit Per il mio oratore non madrelingua inglese, "La ricorsione è un ciclo glorificato" suona intendi "Potresti anche usare un costrutto loop invece di una chiamata ricorsiva ovunque, e il concetto non merita davvero il suo nome" . Forse allora interpreto in modo sbagliato il linguaggio "glorificato qualcosa".
hyde,

13
Che dire della funzione Ackermann? en.wikipedia.org/wiki/Ackermann_function , non particolarmente utile ma impossibile da eseguire tramite loop. (Potresti anche voler controllare questo video su youtube.com/watch?v=i7sm9dzFtEI di Computerphile)
WizardOfMenlo

8
@WizardOfMenlo il codice befunge è un'implementazione della soluzione ERRE (che è anche una soluzione interattiva ... con uno stack). Un approccio iterativo con uno stack può emulare una chiamata ricorsiva. Su qualsiasi programmazione opportunamente potente, un costrutto in loop può essere usato per emularne un altro. La macchina di registro con le istruzioni INC (r), JZDEC (r, z)può implementare una macchina di Turing. Non ha "ricorsioni": questo è un salto se zero altrimenti diminuisce. Se la funzione Ackermann è calcolabile (lo è), quella macchina del registro può farlo.

Risposte:


164

Sì e no. In definitiva, non c'è nulla di ricorsivo che può calcolare il looping, ma il looping richiede molto più impianto idraulico. Pertanto, l'unica cosa che la ricorsione può fare che i loop non possono fare è rendere alcuni compiti super facili.

Fai una passeggiata su un albero. Camminare su un albero con ricorsione è stupido-facile. È la cosa più naturale del mondo. Camminare su un albero con anelli è molto meno semplice. Devi mantenere uno stack o qualche altra struttura di dati per tenere traccia di ciò che hai fatto.

Spesso, la soluzione ricorsiva a un problema è più bella. Questo è un termine tecnico e conta.


120
Fondamentalmente, fare loop invece di ricorsione significa gestire manualmente lo stack.
Silviu Burcea,

15
... gli stack . La seguente situazione potrebbe preferire fortemente avere più di uno stack. Considera una funzione ricorsiva Ache trova qualcosa in un albero. Ogni Avolta che incontra quella cosa, lancia un'altra funzione ricorsiva Bche trova una cosa correlata nella sottostruttura nella posizione in cui è stata lanciata A. Una volta Bterminata la ricorsione a cui ritorna A, e quest'ultima continua la propria ricorsione. Uno può dichiarare uno stack per Ae uno per B, o mettere lo Bstack all'interno del Aciclo. Se uno insiste nell'usare un singolo stack, le cose diventano davvero complicate.
rwong

35
Therefore, the one thing recursion can do that loops can't is make some tasks super easy. E l'unica cosa che i loop possono fare che la ricorsione non può fare è rendere alcuni compiti super facili. Hai visto le cose brutte e non intuitive che devi fare per trasformare la maggior parte dei problemi naturalmente iterativi dalla ricorsione ingenua alla ricorsione della coda in modo che non facciano impazzire?
Mason Wheeler,

10
@MasonWheeler Il 99% delle volte queste "cose" possono essere meglio incapsulate all'interno di un operatore di ricorsione come mapo fold(in effetti se si sceglie di considerarle primitive, penso che si possa usare fold/ unfoldcome terza alternativa ai cicli o alla ricorsione). A meno che tu non stia scrivendo il codice della libreria, non ci sono molti casi in cui dovresti preoccuparti dell'implementazione dell'iterazione, piuttosto che dell'attività che dovrebbe svolgere - in pratica, ciò significa che i cicli espliciti e la ricorsione esplicita sono entrambi ugualmente poveri astrazioni che dovrebbero essere evitate ai massimi livelli.
Leushenko,

7
È possibile confrontare due stringhe confrontando ricorsivamente sottostringhe, ma solo confrontando ogni carattere, uno per uno, fino a quando non si ottiene una mancata corrispondenza, è possibile ottenere prestazioni migliori ed essere più chiari per il lettore.
Steven Burnap,

78

No.

Scendendo alle stesse basi dei minimi necessari per calcolare, hai solo bisogno di essere in grado di loop (questo da solo non è sufficiente, ma è una componente necessaria). Non importa come .

Qualsiasi linguaggio di programmazione in grado di implementare una Turing Machine, è chiamato Turing completo . E ci sono molte lingue che sono complete.

La mia lingua preferita nel modo in cui "funziona davvero?" La completezza di Turing è quella di FRACTRAN , che è completa di Turing . Ha una struttura ad anello e al suo interno è possibile implementare una macchina Turing. Pertanto, tutto ciò che è calcolabile, può essere implementato in un linguaggio che non ha ricorsione. Pertanto, non c'è nulla che la ricorsione possa darti in termini di calcolabilità che il semplice looping non può.

Questo si riduce davvero ad alcuni punti:

  • Tutto ciò che è calcolabile è calcolabile su una macchina di Turing
  • Qualsiasi lingua in grado di implementare una macchina di Turing (chiamata Turing completa), può calcolare tutto ciò che può fare qualsiasi altra lingua
  • Dato che ci sono macchine di Turing in lingue prive di ricorsione (e ce ne sono altre che ricorrono solo quando si entra in alcuni degli altri esolang), è necessariamente vero che non c'è nulla che tu possa fare con la ricorsione che non puoi fare con un loop (e niente che tu possa fare con un loop che non puoi fare con la ricorsione).

Questo non vuol dire che ci sono alcune classi di problemi che possono essere più facilmente pensate con la ricorsione piuttosto che con il ciclo, o con il ciclo piuttosto che con la ricorsione. Tuttavia, anche questi strumenti sono ugualmente potenti.

E mentre l'ho portato all'estremo 'esolang' (soprattutto perché puoi trovare cose che Turing sono complete e implementate in modi piuttosto strani), questo non significa che gli esolang siano in alcun modo opzionali. C'è un intero elenco di cose che sono accidentalmente complete di Turing tra cui Magic the Gathering, Sendmail, i modelli MediaWiki e il sistema di tipo Scala. Molti di questi sono tutt'altro che ottimali quando si tratta di fare qualcosa di pratico, è solo che puoi calcolare tutto ciò che è calcolabile usando questi strumenti.


Questa equivalenza può diventare particolarmente interessante quando si entra in un particolare tipo di ricorsione noto come coda di chiamata .

Se hai, diciamo, un metodo fattoriale scritto come:

int fact(int n) {
    return fact(n, 1);
}

int fact(int n, int accum) {
    if(n == 0) { return 1; }
    if(n == 1) { return accum; }
    return fact(n-1, n * accum);
}

Questo tipo di ricorsione verrà riscritto come un ciclo - nessuno stack utilizzato. Tali approcci sono infatti spesso più eleganti e più facili da comprendere rispetto al ciclo equivalente in fase di scrittura, ma ancora una volta, per ogni chiamata ricorsiva può esserci un ciclo equivalente scritto e per ogni ciclo può esserci una chiamata ricorsiva scritta.

Ci sono anche momenti in cui la conversione del semplice loop in una chiamata di coda può essere contorta e più difficile da capire.


Se vuoi approfondire la teoria, vedi la tesi di Church Turing . Potresti anche trovare utile la tesi di Church - Turing su CS.SE.


29
La completezza di Turing viene gettata in giro troppo come importa. Molte cose sono Turing Complete ( come Magic the Gathering ), ma ciò non significa che sia uguale a qualcos'altro che è Turing Complete. Almeno non ad un livello che conta. Non voglio camminare su un albero con Magic the Gathering.
Roger Roger, il

7
Una volta che puoi ridurre un problema a "questo ha la stessa potenza di una macchina Turing" è sufficiente per arrivarci. Le macchine di Turing sono un ostacolo piuttosto basso, ma è tutto ciò che serve. Non c'è nulla che un ciclo possa fare che la ricorsione non possa fare, né viceversa.

4
L'affermazione fatta in questa risposta è ovviamente corretta, ma oso dire che l'argomento non è davvero convincente. Le macchine di Turing non hanno un concetto diretto di ricorsione, quindi dire "puoi simulare una macchina di Turing senza ricorsione" non prova davvero nulla. Quello che dovresti mostrare per dimostrare l'affermazione è che le macchine di Turing possono simulare la ricorsione. Se non lo mostri, devi presumere fedelmente che l'ipotesi di Church-Turing valga anche per la ricorsione (cosa che fa) ma l'OP ha messo in dubbio questo.
5gon12eder

10
La domanda del PO è "can", non "best", o "più efficacemente" o qualche altro qualificatore. "Turing Complete" significa che tutto ciò che può essere fatto con la ricorsione può anche essere fatto con un ciclo. Se questo è il modo migliore per farlo in una particolare implementazione del linguaggio è una domanda completamente diversa.
Steven Burnap,

7
"Can" NON è praticamente la stessa cosa di "best". Quando confondi "non meglio" con "impossibile", rimani paralizzato perché non importa in che modo fai qualcosa, c'è quasi sempre un modo migliore.
Steven Burnap,

31

Esistono casi in cui un'attività può essere eseguita solo utilizzando la ricorsione, anziché un ciclo?

Puoi sempre trasformare l'algoritmo ricorsivo in un ciclo, che utilizza una struttura di dati Last-In-First-Out (stack AKA) per memorizzare lo stato temporaneo, perché la chiamata ricorsiva è esattamente quella, archiviando lo stato corrente in uno stack, procedendo con l'algoritmo, poi in seguito ripristinando lo stato. Quindi la risposta breve è: No, non ci sono casi del genere .

Tuttavia, un argomento può essere fatto per "sì". Facciamo un esempio semplice e concreto: unisci ordinamento. È necessario dividere i dati in due parti, unire l'ordinamento delle parti e quindi combinarle. Anche se non si esegue una vera chiamata di funzione del linguaggio di programmazione per unire l'ordinamento al fine di unire l'ordinamento sulle parti, è necessario implementare una funzionalità identica a quella di eseguire effettivamente una chiamata di funzione (spingere lo stato sul proprio stack, passare a inizio del ciclo con diversi parametri di avvio, quindi in seguito pop lo stato dallo stack).

Si tratta di ricorsione, se si implementa la chiamata ricorsione da soli, come passaggi separati di "stato push" e "salta all'inizio" e "stato pop"? E la risposta è: no, non si chiama ancora ricorsione, si chiama iterazione con stack esplicito (se si desidera utilizzare una terminologia consolidata).


Nota, questo dipende anche dalla definizione di "compito". Se il compito è ordinare, puoi farlo con molti algoritmi, molti dei quali non richiedono alcun tipo di ricorsione. Se l'attività consiste nell'implementare un algoritmo specifico, come unisci ordinamento, si applica l'ambiguità di cui sopra.

Quindi consideriamo la domanda, ci sono compiti generali, per i quali esistono solo algoritmi simili alla ricorsione. Dal commento di @WizardOfMenlo alla domanda, la funzione Ackermann ne è un semplice esempio. Quindi il concetto di ricorsione è autonomo, anche se può essere implementato con un diverso costrutto di programma per computer (iterazione con stack esplicito).


2
Quando si ha a che fare con un assembly per un processore stackless, queste due tecniche diventano improvvisamente una cosa sola.
Joshua,

@Joshua In effetti! È una questione di livello di astrazione. Se vai di livello o due in basso, sono solo porte logiche.
hyde,

2
Non è del tutto corretto. Per emulare la ricorsione con iterazione, è necessario uno stack in cui sia possibile l'accesso casuale. Un singolo stack senza accesso casuale più una quantità finita di memoria accessibile direttamente sarebbe un PDA, che non è completo di Turing.
Gilles,

@Gilles Vecchio post, ma perché è necessario uno stack ad accesso casuale? Inoltre, tutti i computer reali non sono nemmeno meno dei PDA, poiché hanno solo una quantità finita di memoria direttamente accessibile e nessuno stack (tranne che usando quella memoria)? Questo non sembra un'astrazione molto pratica, se dice "non possiamo fare la ricorsione nella realtà".
hyde,

20

Dipende da quanto rigorosamente definisci "ricorsione".

Se lo richiediamo rigorosamente per coinvolgere lo stack di chiamate (o qualsiasi altro meccanismo utilizzato per mantenere lo stato del programma), allora possiamo sempre sostituirlo con qualcosa che non lo fa. In effetti, i linguaggi che portano naturalmente a un uso intensivo della ricorsione tendono ad avere compilatori che fanno un uso pesante dell'ottimizzazione delle chiamate di coda, quindi ciò che scrivi è ricorsivo ma ciò che esegui è iterativo.

Ma consideriamo un caso in cui effettuiamo una chiamata ricorsiva e utilizziamo il risultato di una chiamata ricorsiva per quella chiamata ricorsiva.

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  if (m == 0)
    return  n+1;
  if (n == 0)
    return Ackermann(m - 1, 1);
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

Rendere la prima chiamata ricorsiva iterativa è facile:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
  if (m == 0)
    return  n+1;
  if (n == 0)
  {
    m--;
    n = 1;
    goto restart;
  }
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

Possiamo quindi ripulire rimuovere il gotoper allontanare i velociraptor e l'ombra di Dijkstra:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  while(m != 0)
  {
    if (n == 0)
    {
      m--;
      n = 1;
    }
    else
      return Ackermann(m - 1, Ackermann(m, n - 1));
  }
  return  n+1;
}

Ma per rimuovere le altre chiamate ricorsive dovremo archiviare i valori di alcune chiamate in uno stack:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  Stack<BigInteger> stack = new Stack<BigInteger>();
  stack.Push(m);
  while(stack.Count != 0)
  {
    m = stack.Pop();
    if(m == 0)
      n = n + 1;
    else if(n == 0)
    {
      stack.Push(m - 1);
      n = 1;
    }
    else
    {
      stack.Push(m - 1);
      stack.Push(m);
      --n;
    }
  }
  return n;
}

Ora, quando consideriamo il codice sorgente, abbiamo sicuramente trasformato il nostro metodo ricorsivo in uno iterativo.

Considerando che cosa è stato compilato, abbiamo trasformato il codice che utilizza lo stack di chiamate per implementare la ricorsione in codice che non lo fa (e nel fare ciò il codice trasformato che genererà un'eccezione di overflow dello stack per valori anche abbastanza piccoli in codice che semplicemente impiegare un tempo tremendamente lungo per tornare [vedi Come posso evitare che la mia funzione Ackerman trabocchi lo stack? per alcune ulteriori ottimizzazioni che lo fanno effettivamente tornare per molti più input possibili]).

Considerando come la ricorsione è implementata in generale, abbiamo trasformato il codice che utilizza lo stack di chiamate in codice che utilizza uno stack diverso per contenere le operazioni in sospeso. Potremmo quindi sostenere che è ancora ricorsivo, se considerato a quel livello basso.

E a quel livello, non ci sono davvero altri modi per aggirarlo. Quindi se consideri quel metodo come ricorsivo, allora ci sono davvero cose di cui non possiamo farne a meno. Generalmente sebbene non etichettiamo tale codice ricorsivo. Il termine ricorsione è utile perché copre un certo insieme di approcci e ci dà un modo di parlarne, e non ne stiamo più usando uno.

Ovviamente, tutto questo presuppone che tu abbia una scelta. Esistono sia lingue che vietano le chiamate ricorsive, sia lingue prive delle strutture cicliche necessarie per iterare.


È possibile sostituire lo stack di chiamate con qualcosa di equivalente se lo stack di chiamate è limitato o si ha accesso a una memoria illimitata all'esterno dello stack di chiamate. Esiste una classe significativa di problemi che sono risolvibili con automi push-down che hanno uno stack di chiamate illimitato, ma possono avere solo un numero finito di stati altrimenti.
supercat

Questa è la risposta migliore, forse l'unica risposta corretta. Anche il secondo esempio è ancora ricorsivo e, a questo livello, la risposta alla domanda originale è no . Con una definizione più ampia di ricorsione, la ricorsione per la funzione di Ackermann è impossibile da evitare.
Gerrit,

@gerrit e con uno più stretto, lo evita. In definitiva, si riduce ai limiti di ciò che facciamo o non applichiamo questa utile etichetta che utilizziamo per un determinato codice.
Jon Hanna,

1
Si è unito al sito per votare questo. La funzione di Ackermann / è / ricorsiva in natura. L'implementazione di una struttura ricorsiva con un ciclo e uno stack non la rende una soluzione iterativa, hai appena spostato la ricorsione nello spazio utente.
Aaron McMillin,

9

La risposta classica è "no", ma mi consente di approfondire il motivo per cui penso che "sì" sia una risposta migliore.


Prima di continuare, togliiamo qualcosa di mezzo: dal punto di vista della computabilità e della complessità:

  • La risposta è "no" se ti è permesso avere uno stack ausiliario durante il loop.
  • La risposta è "sì" se non ti è consentito alcun dato aggiuntivo durante il loop.

Bene, ora mettiamo un piede in terra pratica, mantenendo l'altro piede in terra teoria.


Lo stack di chiamate è una struttura di controllo , mentre uno stack manuale è una struttura di dati . Controllo e dati non sono concetti uguali, ma sono equivalenti nel senso che possono essere ridotti l'uno all'altro (o "emulati" l'uno dall'altro) dal punto di vista della computabilità o della complessità.

Quando potrebbe essere importante questa distinzione? Quando lavori con strumenti del mondo reale. Ecco un esempio:

Supponiamo che tu stia implementando N-way mergesort. Si potrebbe avere un forciclo che passa attraverso ciascuno dei Nsegmenti, chiama mergesortsu di loro separatamente, poi fonde i risultati.

Come potresti parallelizzare questo con OpenMP?

Nel regno ricorsivo, è estremamente semplice: basta mettere #pragma omp parallel forintorno al tuo loop che va da 1 a N, e il gioco è fatto. Nel regno iterativo, non puoi farlo. Devi generare manualmente i thread e passarli manualmente i dati appropriati in modo che sappiano cosa fare.

D'altra parte, ci sono altri strumenti (come i vettorizzatori automatici, ad esempio #pragma vector) che funzionano con i loop ma sono completamente inutili con la ricorsione.

Il punto è che, solo perché puoi dimostrare che i due paradigmi sono matematicamente equivalenti, ciò non significa che siano uguali nella pratica. Un problema che potrebbe essere banale da automatizzare in un paradigma (diciamo, parallelizzazione ad anello) potrebbe essere molto più difficile da risolvere nell'altro paradigma.

vale a dire: gli strumenti per un paradigma non si traducono automaticamente in altri paradigmi.

Di conseguenza, se hai bisogno di uno strumento per risolvere un problema, è probabile che lo strumento funzionerà solo con un particolare tipo di approccio, e di conseguenza non riuscirai a risolvere il problema con un approccio diverso, anche se puoi dimostrare matematicamente che il problema può essere risolto in entrambi i modi.


Inoltre, considera che l'insieme di problemi che possono essere risolti con un automa push-down è più grande dell'insieme che può essere risolto con un automa finito (sia deterministico che non) ma più piccolo dell'insieme che può essere risolto con un Macchina di Turing.
supercat

8

Mettendo da parte il ragionamento teorico, diamo un'occhiata a come appaiono la ricorsione e i loop dal punto di vista di una macchina (hardware o virtuale). La ricorsione è una combinazione di flusso di controllo che consente di avviare l'esecuzione di alcuni codici e di tornare al completamento (in una vista semplicistica quando i segnali e le eccezioni vengono ignorati) e dei dati passati a quell'altro codice (argomenti) e che vengono restituiti da (risultato). Di solito non è coinvolta alcuna gestione esplicita della memoria, tuttavia esiste un'allocazione implicita della memoria dello stack per salvare indirizzi di ritorno, argomenti, risultati e dati locali intermedi.

Un loop è una combinazione di flusso di controllo e dati locali. Confrontando questo con la ricorsione, possiamo vedere che la quantità di dati in questo caso è fissa. L'unico modo per superare questa limitazione è utilizzare la memoria dinamica (nota anche come heap ) che può essere allocata (e liberata) ogni volta che è necessario.

Riassumere:

  • Caso di ricorsione = Flusso di controllo + Stack (+ Heap)
  • Caso loop = flusso di controllo + heap

Supponendo che la parte del flusso di controllo sia ragionevolmente potente, l'unica differenza è nei tipi di memoria disponibili. Quindi, ci rimangono 4 casi (il potere espressivo è elencato tra parentesi):

  1. Nessuno stack, nessun heap: la ricorsione e le strutture dinamiche sono impossibili. (ricorsione = loop)
  2. Stack, no heap: la ricorsione è OK, le strutture dinamiche sono impossibili. (ricorsione> loop)
  3. Nessuno stack, heap: la ricorsione è impossibile, le strutture dinamiche sono OK. (ricorsione = loop)
  4. Stack, heap: ricorsione e strutture dinamiche sono OK. (ricorsione = loop)

Se le regole del gioco sono un po 'più rigide e l'implementazione ricorsiva non è consentita per usare i loop, otteniamo invece questo:

  1. Nessuno stack, nessun heap: la ricorsione e le strutture dinamiche sono impossibili. (ricorsione <loop)
  2. Stack, no heap: la ricorsione è OK, le strutture dinamiche sono impossibili. (ricorsione> loop)
  3. Nessuno stack, heap: la ricorsione è impossibile, le strutture dinamiche sono OK. (ricorsione <loop)
  4. Stack, heap: ricorsione e strutture dinamiche sono OK. (ricorsione = loop)

La differenza fondamentale con lo scenario precedente è che la mancanza di memoria dello stack non consente alla ricorsione senza loop di eseguire più passaggi durante l'esecuzione rispetto alle righe di codice.


2

Sì. Esistono diverse attività comuni facili da eseguire utilizzando la ricorsione, ma impossibili con i soli loop:

  • Causando overflow dello stack.
  • Programmatori principianti totalmente confusi.
  • Creazione di funzioni dall'aspetto rapido che in realtà sono O (n ^ n).

3
Per favore, questi sono davvero facili con i loop, li vedo sempre. Diamine, con un po 'di sforzo non hai nemmeno bisogno dei loop. Anche se la ricorsione è più facile.
AviD,

1
in realtà, A (0, n) = n + 1; A (m, 0) = A (m-1,1) se m> 0; A (m, n) = A (m-1, A (m, n-1)) se m> 0, n> 0 cresce anche un po 'più velocemente di O (n ^ n) (per m = n) :)
John Donn,

1
@JohnDonn Più che un po ', è super esponenziale. per n = 3 n ^ n ^ n per n = 4 n ^ n ^ n ^ n ^ n e così via. n alla n potenza n volte.
Aaron McMillin,

1

C'è una differenza tra le funzioni ricorsive e le funzioni ricorsive primitive. Le funzioni ricorsive primitive sono quelle calcolate mediante loop, in cui viene calcolato il conteggio massimo di iterazioni di ciascun loop prima che inizi l'esecuzione del loop. (E "ricorsivo" qui non ha nulla a che fare con l'uso della ricorsione).

Le funzioni ricorsive primitive sono strettamente meno potenti delle funzioni ricorsive. Otterresti lo stesso risultato se prendessi funzioni che utilizzano la ricorsione, in cui la profondità massima della ricorsione deve essere calcolata in anticipo.


3
Non sono sicuro di come questo si applica alla domanda sopra? Potete per favore rendere più chiara questa connessione?
Yakk,

1
Sostituendo il "loop" impreciso con l'importante distinzione tra "loop con conteggio di iterazioni limitato" e "loop con conteggio di iterazioni illimitato", che pensavo che tutti avrebbero saputo da CS 101.
gnasher729

certo, ma non si applica ancora alla domanda. La domanda riguarda il looping e la ricorsione, non la ricorsione e la ricorsione primitive. Immagina se qualcuno ti chiedesse delle differenze C / C ++ e tu avessi risposto della differenza tra K&R C e Ansi C. Certo che rende le cose più precise, ma non risponde alla domanda.
Yakk,

1

Se stai programmando in c ++ e usi c ++ 11, allora c'è una cosa che deve essere fatta usando le ricorsioni: le funzioni constexpr. Ma lo standard limita questo a 512, come spiegato in questa risposta . L'uso di loop in questo caso non è possibile, poiché in quel caso la funzione non può essere constexpr, ma questo è cambiato in c ++ 14.


0
  • Se la chiamata ricorsiva è la prima o l'ultima affermazione (escluso il controllo delle condizioni) di una funzione ricorsiva, è abbastanza facile da tradurre in una struttura ciclica.
  • Ma se la funzione fa alcune altre cose prima e dopo la chiamata ricorsiva , sarebbe ingombrante convertirla in loop.
  • Se la funzione ha più chiamate ricorsive, la conversione in codice che utilizza solo loop sarà praticamente impossibile. Sarà necessario un po 'di stack per tenere il passo con i dati. Nella ricorsione lo stack di chiamate stesso funzionerà come stack di dati.

La camminata sugli alberi ha più chiamate ricorsive (una per ogni bambino), ma è banalmente trasformata in un ciclo usando uno stack esplicito. I parser invece sono spesso fastidiosi da trasformare.
CodesInChaos,

@CodesInChaos Edited.
Gulshan,

-6

Sono d'accordo con le altre domande. Non c'è niente che tu possa fare con la ricorsione che non puoi fare con un loop.

MA , secondo me, la ricorsione può essere molto pericolosa. Innanzitutto, per alcuni è più difficile capire cosa sta realmente accadendo nel codice. In secondo luogo, almeno per C ++ (Java non sono sicuro) ogni passaggio di ricorsione ha un impatto sulla memoria perché ogni chiamata di metodo provoca l'accumulo di memoria e l'inizializzazione dell'intestazione dei metodi. In questo modo puoi far esplodere il tuo stack. Prova semplicemente la ricorsione dei numeri di Fibonacci con un alto valore di input.


2
Un'implementazione ricorsiva ingenua dei numeri di Fibonacci con ricorsione verrà eseguita "fuori dal tempo" prima che si esaurisca lo spazio dello stack. Immagino che ci siano altri problemi che sono meglio per questo esempio. Inoltre, per molti problemi una versione in loop ha lo stesso impatto di memoria di una ricorsiva, solo sullo heap anziché sullo stack (se il linguaggio di programmazione li distingue).
Paŭlo Ebermann,

6
Il loop può anche essere "molto pericoloso" se si dimentica di incrementare la variabile loop ...
h22

2
Quindi, in effetti, produrre deliberatamente un overflow dello stack è un'attività che diventa molto complicata senza ricorrere alla ricorsione.
5gon12eder,

@ 5gon12eder che ci porta a Quali metodi ci sono per evitare un overflow dello stack in un algoritmo ricorsivo? - scrivere per coinvolgere TCO o Memoisation può essere utile. Anche gli approcci iterativi vs. ricorsivi sono interessanti in quanto trattano due diversi approcci ricorsivi per Fibonacci.

1
La maggior parte delle volte se si verifica un overflow dello stack durante la ricorsione, si avrebbe avuto un blocco sulla versione iterativa. Almeno il primo lancia con una traccia dello stack.
Jon Hanna,
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.