it-swarm.it

Ricorsione o iterazione?

C'è un impatto sulle prestazioni se utilizziamo un loop invece della ricorsione o viceversa negli algoritmi in cui entrambi possono servire allo stesso scopo? Ad esempio: controlla se la stringa data è un palindromo. Ho visto molti programmatori usare la ricorsione come mezzo per mostrare quando un semplice algoritmo di iterazione può adattarsi al conto. Il compilatore gioca un ruolo vitale nel decidere cosa usare?

210
Omnipotent

È possibile che la ricorsione sia più costosa, a seconda che la funzione ricorsiva sia coda ricorsiva (l'ultima linea è chiamata ricorsiva). La ricorsione della coda dovrebbe essere riconosciuta dal compilatore e ottimizzata alla sua controparte iterativa (mantenendo l'implementazione concisa e chiara che hai nel tuo codice).

Scriverei l'algoritmo nel modo più sensato ed è il più chiaro per il povero succhiatore (che sia te stesso o qualcun altro) che deve mantenere il codice in pochi mesi o anni. Se si verificano problemi di prestazioni, quindi profilare il codice e quindi e solo successivamente cercare l'ottimizzazione passando a un'implementazione iterativa. Potresti voler esaminare memoization e programmazione dinamica .

176
Paul Osborne

I loop possono ottenere un miglioramento delle prestazioni per il tuo programma. La ricorsione può ottenere un miglioramento delle prestazioni per il programmatore. Scegli quale è più importante nella tua situazione!

309
Leigh Caldwell

Confrontare la ricorsione con l'iterazione è come confrontare un cacciavite a croce con un cacciavite a testa piatta. Per lo più tu potresti rimuovi qualsiasi vite con testa a croce con una testa piatta, ma sarebbe più semplice se tu usassi il cacciavite progettato per quella vite, giusto?

Alcuni algoritmi si prestano semplicemente alla ricorsione a causa del modo in cui sono progettati (sequenze di Fibonacci, attraversamento di un albero come una struttura, ecc.). La ricorsione rende l'algoritmo più succinto e più comprensibile (quindi condivisibile e riutilizzabile).

Inoltre, alcuni algoritmi ricorsivi usano "Lazy Evaluation" che li rende più efficienti dei loro fratelli iterativi. Ciò significa che eseguono i calcoli costosi solo nel momento in cui sono necessari anziché ogni volta che il ciclo viene eseguito.

Dovrebbe essere abbastanza per iniziare. Prenderò alcuni articoli ed esempi anche per te.

Link 1: Haskel vs PHP (Ricorsione contro Iterazione)

Ecco un esempio in cui il programmatore ha dovuto elaborare un set di dati di grandi dimensioni utilizzando PHP. Dimostra quanto sarebbe stato facile gestire Haskel usando la ricorsione, ma poiché PHP non aveva un modo semplice per ottenere lo stesso metodo, è stato costretto a usare l'iterazione per ottenere il risultato.

http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html

Link 2: Mastering Recursion

Gran parte della cattiva reputazione della ricorsione deriva dagli alti costi e dall'inefficienza delle lingue imperative. L'autore di questo articolo parla di come ottimizzare gli algoritmi ricorsivi per renderli più veloci ed efficienti. Viene inoltre illustrato come convertire un loop tradizionale in una funzione ricorsiva e i vantaggi dell'utilizzo della ricorsione di coda. Le sue parole conclusive hanno davvero riassunto alcuni dei miei punti chiave che penso:

"la programmazione ricorsiva offre al programmatore un modo migliore di organizzare il codice in modo che sia sostenibile e logicamente coerente."

https://developer.ibm.com/articles/l-recurs/

Link 3: La ricorsione è sempre più veloce del looping? (Risposta)

Ecco un link a una risposta per una domanda StackOverflow simile alla tua. L'autore sottolinea che molti dei benchmark associati alla ricorsione o al loop sono molto specifici della lingua. Le lingue imperative sono in genere più veloci utilizzando un ciclo e più lente con la ricorsione e viceversa per le lingue funzionali. Immagino che il punto principale da trarre da questo collegamento sia che è molto difficile rispondere alla domanda in un senso della lingua agnostico/situazione cieco.

La ricorsione è mai più veloce del looping?

76
Swift

La ricorsione è più costosa in memoria, poiché ogni chiamata ricorsiva richiede generalmente che un indirizzo di memoria venga trasferito nello stack, in modo che in seguito il programma possa tornare a quel punto.

Tuttavia, ci sono molti casi in cui la ricorsione è molto più naturale e leggibile rispetto ai loop, come quando si lavora con gli alberi. In questi casi, consiglierei di attenersi alla ricorsione.

16
Doron Yaacoby

In genere, ci si aspetterebbe che la penalità prestazionale risieda nella direzione opposta. Le chiamate ricorsive possono portare alla costruzione di frame stack aggiuntivi; la penalità per questo varia. Inoltre, in alcune lingue come Python (più correttamente, in alcune implementazioni di alcune lingue ...), puoi eseguire limiti di stack piuttosto facilmente per attività che potresti specificare ricorsivamente, come trovare valore massimo in una struttura di dati ad albero. In questi casi, vuoi davvero restare con i loop.

Scrivere buone funzioni ricorsive può ridurre in qualche modo la penalità delle prestazioni, supponendo che tu abbia un compilatore che ottimizza le ricorsioni della coda, ecc. su.)

A parte i casi "Edge" (elaborazione ad alte prestazioni, profondità di ricorsione molto ampia, ecc.), È preferibile adottare l'approccio che più chiaramente esprime il tuo intento, è ben progettato ed è mantenibile. Ottimizza solo dopo aver identificato un'esigenza.

11
zweiterlinde

La ricorsione è migliore dell'iterazione per problemi che possono essere suddivisi in multipli, pezzi più piccoli.

Ad esempio, per creare un algoritmo ricorsivo di Fibonnaci, scomporre fib (n) in fib (n-1) e fib (n-2) e calcolare entrambe le parti. L'iterazione consente solo di ripetere ripetutamente una singola funzione.

Tuttavia, Fibonacci è in realtà un esempio non corretto e penso che l'iterazione sia effettivamente più efficiente. Si noti che fib (n) = fib (n-1) + fib (n-2) e fib (n-1) = fib (n-2) + fib (n-3). fib (n-1) viene calcolato due volte!

Un esempio migliore è un algoritmo ricorsivo per un albero. Il problema dell'analisi del nodo padre può essere suddiviso in multipli problemi minori nell'analisi di ciascun nodo figlio. A differenza dell'esempio di Fibonacci, i problemi più piccoli sono indipendenti l'uno dall'altro.

Quindi sì: la ricorsione è migliore dell'iterazione per problemi che possono essere suddivisi in problemi multipli, più piccoli, indipendenti e simili.

8
Ben

Le prestazioni si deteriorano quando si utilizza la ricorsione perché la chiamata di un metodo, in qualsiasi lingua, implica molta preparazione: il codice chiamante pubblica un indirizzo di ritorno, parametri di chiamata, alcune altre informazioni di contesto come i registri del processore potrebbero essere salvate da qualche parte, e al momento del ritorno il chiamato metodo invia un valore restituito che viene quindi recuperato dal chiamante e tutte le informazioni di contesto salvate in precedenza verranno ripristinate. la differenza di prestazioni tra un approccio iterativo e un approccio ricorsivo risiede nel tempo impiegato da queste operazioni.

Da un punto di vista dell'implementazione, inizi davvero a notare la differenza quando il tempo necessario per gestire il contesto chiamante è paragonabile al tempo impiegato dal tuo metodo per essere eseguito. Se l'esecuzione del metodo ricorsivo richiede più tempo rispetto alla parte di gestione del contesto chiamante, procedere in modo ricorsivo poiché il codice è generalmente più leggibile e di facile comprensione e non si noterà la perdita di prestazioni. Altrimenti diventa iterativo per motivi di efficienza.

7
entzik

Credo che la ricorsione della coda sia Java non è attualmente ottimizzato. I dettagli sono sparsi in tutta questa discussione su LtU e sui collegamenti associati. potrebbe essere una caratteristica della prossima versione 7, ma a quanto pare presenta alcune difficoltà quando combinato con Stack Inspection poiché alcuni frame mancherebbero. Stack Inspection è stato usato per implementare la loro sicurezza a grana fine modello da Java 2.

http://lambda-the-ultimate.org/node/13

6
Mike Edwards

In molti casi la ricorsione è più veloce a causa della memorizzazione nella cache, che migliora le prestazioni. Ad esempio, ecco una versione iterativa dell'ordinamento di tipo merge che utilizza la tradizionale routine di merge. Funzionerà più lentamente dell'implementazione ricorsiva a causa della memorizzazione nella cache di prestazioni migliorate.

Implementazione iterativa

public static void sort(Comparable[] a)
{
    int N = a.length;
    aux = new Comparable[N];
    for (int sz = 1; sz < N; sz = sz+sz)
        for (int lo = 0; lo < N-sz; lo += sz+sz)
            merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}

Implementazione ricorsiva

private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);
    sort(a, aux, mid+1, hi);
    merge(a, aux, lo, mid, hi);
}

PS: è quanto ha detto il professor Kevin Wayne (Università di Princeton) nel corso sugli algoritmi presentato su Coursera.

5
Nikunj Banka

Ci sono molti casi in cui fornisce una soluzione molto più elegante rispetto al metodo iterativo, l'esempio comune è l'attraversamento di un albero binario, quindi non è necessariamente più difficile da mantenere. In generale, le versioni iterative sono in genere un po 'più veloci (e durante l'ottimizzazione può sostituire una versione ricorsiva), ma le versioni ricorsive sono più semplici da comprendere e implementare correttamente.

5
Felix

La ricorsione è molto utile in alcune situazioni. Ad esempio, considera il codice per trovare il fattoriale

int factorial ( int input )
{
  int x, fact = 1;
  for ( x = input; x > 1; x--)
     fact *= x;
  return fact;
}

Ora consideralo usando la funzione ricorsiva

int factorial ( int input )
{
  if (input == 0)
  {
     return 1;
  }
  return input * factorial(input - 1);
}

Osservando questi due, possiamo vedere che la ricorsione è facile da capire. Ma se non viene usato con cura, può anche essere soggetto a molti errori. Supponiamo che se ci manca if (input == 0), il codice verrà eseguito per un po 'di tempo e termina con un overflow dello stack.

5
Harikrishnan

Dipende dalla lingua. In Java dovresti usare i loop. I linguaggi funzionali ottimizzano la ricorsione.

4
mc

Usando la ricorsione, stai sostenendo il costo di una chiamata di funzione con ogni "iterazione", mentre con un ciclo, l'unica cosa che di solito paghi è un incremento/decremento. Quindi, se il codice per il ciclo non è molto più complicato del codice per la soluzione ricorsiva, il ciclo di solito sarà superiore alla ricorsione.

4
MovEaxEsp

La ricorsione e l'iterazione dipendono dalla logica aziendale che si desidera implementare, sebbene nella maggior parte dei casi possa essere utilizzata in modo intercambiabile. La maggior parte degli sviluppatori ricorre alla ricorsione perché è più facile da capire.

4
Warrior

La ricorsione è più semplice (e quindi - più fondamentale) di ogni possibile definizione di iterazione. È possibile definire un sistema completo di Turing con solo un coppia di combinatori (sì, anche una ricorsione stessa è una nozione derivata in un tale sistema). Lambda il calcolo è un sistema fondamentale altrettanto potente, con funzioni ricorsive. Ma se vuoi definire correttamente un'iterazione, avresti bisogno di molte più primitive per iniziare.

Per quanto riguarda il codice, no, il codice ricorsivo è in effetti molto più facile da capire e da mantenere rispetto a quello puramente iterativo, poiché la maggior parte delle strutture di dati sono ricorsive. Ovviamente, per farlo bene uno avrebbe bisogno di una lingua con un supporto per funzioni e chiusure di alto ordine, almeno - per ottenere tutti i combinatori e iteratori standard in modo ordinato. In C++, ovviamente, le complicate soluzioni ricorsive possono sembrare un po 'brutte, a meno che tu non sia un utente hardcore di FC++ e simili.

3
SK-logic

Se stai solo iterando su un elenco, allora sicuramente, iterare via.

Un paio di altre risposte hanno menzionato l'attraversamento degli alberi (in profondità). È davvero un ottimo esempio, perché è una cosa molto comune fare una struttura di dati molto comune. La ricorsione è estremamente intuitiva per questo problema.

Dai un'occhiata ai metodi "trova" qui: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html

3
Joe Cheng

Penserei che nella ricorsione (non di coda) si verificherebbe un impatto sulle prestazioni per l'allocazione di un nuovo stack ecc. Ogni volta che viene chiamata la funzione (dipende ovviamente dalla lingua).

2
metadave

In C++ se la funzione ricorsiva è templata, il compilatore ha maggiori possibilità di ottimizzarla, poiché tutte le deduzioni di tipo e le istanze di funzione si verificheranno in fase di compilazione. I compilatori moderni possono anche incorporare la funzione, se possibile. Quindi se si usano flag di ottimizzazione come -O3 o -O2 nel g++, quindi le ricorsioni potrebbero avere la possibilità di essere più veloci delle iterazioni. Nei codici iterativi, il compilatore ha meno possibilità di ottimizzarlo, poiché è già nello stato più o meno ottimale (se scritto abbastanza bene).

Nel mio caso, stavo cercando di implementare l'espiazione della matrice quadrando usando gli oggetti matrice Armadillo, sia in modo ricorsivo che iterativo. L'algoritmo può essere trovato qui ... https://en.wikipedia.org/wiki/Exponentiation_by_squaring . Le mie funzioni erano basate su modelli e ho calcolato 1,000,00012x12 matrici elevate al potere 10. Ho ottenuto il seguente risultato:

iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec

iterative + No-optimisation flag  -> 2.83.. sec
recursive + No-optimisation flag  -> 4.15.. sec

Questi risultati sono stati ottenuti usando gcc-4.8 con flag c ++ 11 (-std=c++11) e Armadillo 6.1 con Intel mkl. Anche il compilatore Intel mostra risultati simili.

2
Titas Chanda

Ricorsione? Da dove comincio, wiki ti dirà "è il processo di ripetere gli articoli in modo auto-simile"

Ai tempi in cui stavo facendo C, la ricorsione in C++ era un mittente, cose come "Ricorsione in coda". Troverai anche molti algoritmi di ordinamento che utilizzano la ricorsione. Esempio di ordinamento rapido: http://alienryderflex.com/quicksort/

La ricorsione è come qualsiasi altro algoritmo utile per un problema specifico. Forse potresti non trovare subito un uso o spesso ma ci saranno problemi, sarai contento che sia disponibile.

2
Nickz

dipende dalla "profondità di ricorsione". dipende da quanto l'overhead della chiamata di funzione influenzerà il tempo totale di esecuzione.

Ad esempio, il calcolo del fattoriale classico in modo ricorsivo è molto inefficiente a causa di: - rischio di overflow dei dati - rischio di overflow dello stack - l'overhead della chiamata di funzione occupa l'80% dei tempi di esecuzione

mentre si sviluppa un algoritmo min-max per l'analisi della posizione nel gioco degli scacchi che analizzerà le successive mosse N può essere implementato in ricorsione sulla "profondità dell'analisi" (come sto facendo ^ _ ^)

2
ugasoft

Mike ha ragione. La ricorsione della coda è not ottimizzata dal Java o dalla JVM. Otterrai sempre un overflow dello stack con qualcosa del genere:

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}
1
noah

La ricorsione ha uno svantaggio che l'algoritmo che scrivi usando la ricorsione ha una complessità spaziale O(n). Mentre l'approccio iterativo ha una complessità spaziale di O (1). Questo è il vantaggio di usare l'iterazione su ricorsione Quindi perché usiamo la ricorsione?

Vedi sotto.

A volte è più semplice scrivere un algoritmo usando la ricorsione mentre è leggermente più difficile scrivere lo stesso algoritmo usando l'iterazione. In questo caso, se si sceglie di seguire l'approccio di iterazione, è necessario gestire lo stack da soli.

1

Devi tenere presente che utilizzando una ricorsione troppo profonda ti imbatterai in Stack Overflow, a seconda delle dimensioni dello stack consentite. Per evitare ciò, assicurati di fornire un caso base che termina la ricorsione.

1
Grigori A.

Per quanto ne so, Perl non ottimizza le chiamate ricorsive della coda, ma puoi fingere.

sub f{
  my($l,$r) = @_;

  if( $l >= $r ){
    return $l;
  } else {

    # return f( $l+1, $r );

    @_ = ( $l+1, $r );
    goto &f;

  }
}

Quando viene chiamato per la prima volta, alloca spazio nello stack. Quindi cambierà i suoi argomenti e riavvierà la subroutine, senza aggiungere altro allo stack. Farà quindi finta di non chiamarsi mai, trasformandolo in un processo iterativo.

Nota che non c'è "my @_;" o "local @_; ", se lo facessi non funzionerebbe più.

0
Brad Gilbert

Se le iterazioni sono atomiche e gli ordini di grandezza sono più costosi rispetto alla spinta di un nuovo stack frame e creando un nuovo thread e hai più core e tuo l'ambiente di runtime può utilizzarli tutti, quindi un approccio ricorsivo potrebbe produrre un enorme aumento delle prestazioni se combinato con il multithreading. Se il numero medio di iterazioni non è prevedibile, potrebbe essere una buona idea utilizzare un pool di thread che controllerà l'allocazione dei thread e impedirà al processo di creare troppi thread e controllare il sistema.

Ad esempio, in alcune lingue, esistono implementazioni ricorsive di ordinamento unione multithread.

Ma ancora una volta, il multithreading può essere utilizzato con il looping piuttosto che con la ricorsione, quindi quanto bene funzionerà questa combinazione dipende da più fattori tra cui il sistema operativo e il suo meccanismo di allocazione dei thread.

0
ccpizza

Usando solo Chrome 45.0.2454.85 m, la ricorsione sembra essere molto più veloce.

Ecco il codice:

(function recursionVsForLoop(global) {
    "use strict";

    // Perf test
    function perfTest() {}

    perfTest.prototype.do = function(ns, fn) {
        console.time(ns);
        fn();
        console.timeEnd(ns);
    };

    // Recursion method
    (function recur() {
        var count = 0;
        global.recurFn = function recurFn(fn, cycles) {
            fn();
            count = count + 1;
            if (count !== cycles) recurFn(fn, cycles);
        };
    })();

    // Looped method
    function loopFn(fn, cycles) {
        for (var i = 0; i < cycles; i++) {
            fn();
        }
    }

    // Tests
    var curTest = new perfTest(),
        testsToRun = 100;

    curTest.do('recursion', function() {
        recurFn(function() {
            console.log('a recur run.');
        }, testsToRun);
    });

    curTest.do('loop', function() {
        loopFn(function() {
            console.log('a loop run.');
        }, testsToRun);
    });

})(window);

[~ ~ #] [risultati ~ ~ #]

// 100 esegue utilizzando lo standard per il ciclo

100x per loop. Tempo per il completamento: 7.683ms

// 100 corse con approccio ricorsivo funzionale con ricorsione della coda

Esecuzione di ricorsione 100x. Tempo per il completamento: 4.841ms

Nello screenshot seguente, la ricorsione vince ancora con un margine maggiore quando viene eseguita a 300 cicli per test

Recursion wins again!

0
Alpha G33k