La ricorsione è mai più veloce del looping?


286

So che la ricorsione a volte è molto più pulita del looping e non sto chiedendo nulla su quando dovrei usare la ricorsione sull'iterazione, so che ci sono già molte domande al riguardo.

Quello che sto chiedendo è: la ricorsione è sempre più veloce di un ciclo? A me sembra che saresti sempre in grado di perfezionare un loop e farlo funzionare più rapidamente di una funzione ricorsiva perché il loop è assente impostando costantemente nuovi frame di stack.

Sto specificatamente cercando se la ricorsione è più veloce nelle applicazioni in cui la ricorsione è il modo giusto di gestire i dati, come in alcune funzioni di ordinamento, negli alberi binari, ecc.


3
A volte la procedura iterativa o le formule in forma chiusa per alcune ricorrenze impiegano secoli per presentarsi. Penso che solo in quei momenti la ricorsione sia più veloce :) lol
Pratik Deoghare

24
Parlando da solo, preferisco di gran lunga l'iterazione. ;-)
Iteratore

possibile duplicato di ricorsione o iterazione?
nawfal,


@PratikDeoghare No, la domanda non riguarda la scelta di un algoritmo completamente diverso. È sempre possibile convertire una funzione ricorsiva in un metodo funzionalmente identico che utilizza un ciclo. Ad esempio, questa risposta ha lo stesso algoritmo sia in formato ricorsivo che ciclica . In generale, inserirai una tupla di argomenti nella funzione ricorsiva in uno stack, spingendo nello stack per chiamare, scartando dallo stack per tornare dalla funzione.
TamaMcGlinn,

Risposte:


358

Questo dipende dalla lingua utilizzata. Hai scritto "linguaggio indipendente", quindi fornirò alcuni esempi.

In Java, C e Python, la ricorsione è piuttosto costosa rispetto all'iterazione (in generale) perché richiede l'allocazione di un nuovo stack frame. In alcuni compilatori C, è possibile utilizzare un flag di compilatore per eliminare questo sovraccarico, che trasforma alcuni tipi di ricorsione (in realtà, alcuni tipi di chiamate di coda) in salti anziché in chiamate di funzione.

Nelle implementazioni del linguaggio di programmazione funzionale, a volte, l'iterazione può essere molto costosa e la ricorsione può essere molto economica. In molti, la ricorsione si trasforma in un semplice salto, ma la modifica della variabile loop (che è mutabile) a volte richiede alcune operazioni relativamente pesanti, specialmente su implementazioni che supportano più thread di esecuzione. La mutazione è costosa in alcuni di questi ambienti a causa dell'interazione tra il mutatore e il garbage collector, se entrambi potrebbero essere in esecuzione contemporaneamente.

So che in alcune implementazioni di Scheme, la ricorsione sarà generalmente più veloce del looping.

In breve, la risposta dipende dal codice e dall'implementazione. Usa lo stile che preferisci. Se stai usando un linguaggio funzionale, la ricorsione potrebbe essere più veloce. Se stai usando un linguaggio imperativo, l'iterazione è probabilmente più veloce. In alcuni ambienti, entrambi i metodi comporteranno la generazione dello stesso assieme (inseriscilo nel tubo e fumalo).

Addendum: in alcuni ambienti, la migliore alternativa non è né la ricorsione né l'iterazione, ma funzioni di ordine superiore. Questi includono "mappa", "filtro" e "riduci" (che è anche chiamato "piega"). Non solo questi sono lo stile preferito, non solo sono spesso più puliti, ma in alcuni ambienti queste funzioni sono le prime (o le uniche) a ottenere un impulso dalla parallelizzazione automatica, quindi possono essere significativamente più veloci rispetto all'iterazione o alla ricorsione. Data Parallel Haskell è un esempio di tale ambiente.

Le comprensioni dell'elenco sono un'altra alternativa, ma di solito sono solo zucchero sintattico per funzioni di iterazione, ricorsione o ordine superiore.


48
I +1, e vorrei commentare che "ricorsione" e "cicli" sono proprio ciò che gli umani chiamano il loro codice. Ciò che conta per la performance non è come si nominano le cose, ma piuttosto come vengono compilate / interpretate. La ricorsione, per definizione, è un concetto matematico e ha poco a che fare con i frame dello stack e le cose di assemblaggio.
P:

1
Inoltre, la ricorsione è, in generale, l'approccio più naturale nei linguaggi funzionali e l'iterazione è normalmente più intuitiva nei linguaggi imperativi. È improbabile che la differenza di prestazioni sia evidente, quindi usa tutto ciò che ti sembra più naturale per quel particolare linguaggio. Ad esempio, probabilmente non vorrai usare l'iterazione in Haskell quando la ricorsione è molto più semplice.
Sasha Chedygov,

4
Generalmente la ricorsione è compilata in loop, con loop che sono un costrutto di livello inferiore. Perché? Poiché la ricorsione è in genere ben fondata su una struttura di dati, inducendo un'algebra F iniziale e consentendo di provare alcune proprietà sulla terminazione insieme ad argomenti induttivi sulla struttura del calcolo (ricorsivo). Il processo attraverso il quale viene compilata la ricorsione in loop è l'ottimizzazione della coda.
Kristopher Micinski,

Ciò che conta di più sono le operazioni non eseguite. Più "IO", più devi elaborare. I dati di non IOing (ovvero l'indicizzazione) sono sempre il più grande aumento delle prestazioni di qualsiasi sistema perché non è necessario elaborarli in primo luogo.
Jeff Fischer,

53

la ricorsione è mai più veloce di un ciclo?

No, Iterazione sarà sempre più veloce di Ricorsione. (in un'architettura Von Neumann)

Spiegazione:

Se si creano da zero le operazioni minime di un computer generico, "Iteration" viene prima come blocco predefinito e richiede meno risorse rispetto alla "ricorsione", ergo è più veloce.

Costruire una macchina pseudo-informatica da zero:

Mettiti in discussione : di cosa hai bisogno per calcolare un valore, cioè per seguire un algoritmo e raggiungere un risultato?

Stabiliremo una gerarchia di concetti, partendo da zero e definendo in primo luogo i concetti di base e fondamentali, quindi costruiremo concetti di secondo livello con quelli e così via.

  1. Primo concetto: celle di memoria, memoria, stato . Per fare qualcosa hai bisogno di luoghi in cui archiviare i valori dei risultati finali e intermedi. Supponiamo di avere un array infinito di celle "intere", chiamate Memory , M [0..Infinite].

  2. Istruzioni: fai qualcosa - trasforma una cella, cambia il suo valore. alter state . Ogni istruzione interessante esegue una trasformazione. Le istruzioni di base sono:

    a) Impostare e spostare le celle di memoria

    • memorizzare un valore in memoria, ad esempio: memorizzare 5 m [4]
    • copiare un valore in un'altra posizione: ad esempio: store m [4] m [8]

    b) Logica e aritmetica

    • e, o, xor, no
    • aggiungi, sub, mul, div. es. aggiungi m [7] m [8]
  3. Un agente esecutivo : un nucleo in una CPU moderna. Un "agente" è qualcosa che può eseguire istruzioni. Un agente può anche essere una persona che segue l'algoritmo su carta.

  4. Ordine dei passaggi: una sequenza di istruzioni : vale a dire: fare prima questo, farlo dopo, ecc. Una sequenza imperativa di istruzioni. Anche le espressioni di una riga sono "una sequenza imperativa di istruzioni". Se hai un'espressione con un "ordine di valutazione" specifico, allora hai dei passaggi . Significa che anche una singola espressione composta ha "passi" impliciti e ha anche una variabile locale implicita (chiamiamola "risultato"). per esempio:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    L'espressione sopra implica 3 passaggi con una variabile "risultato" implicita.

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    Quindi anche le espressioni infix, dato che hai un ordine di valutazione specifico, sono una sequenza imperativa di istruzioni . L'espressione implica una sequenza di operazioni da eseguire in un ordine specifico e, poiché vi sono passaggi , esiste anche una variabile intermedia "risultato" implicita.

  5. Puntatore istruzioni : se si dispone di una sequenza di passaggi, è presente anche un "puntatore istruzioni" implicito. Il puntatore dell'istruzione segna l'istruzione successiva e avanza dopo che l'istruzione è stata letta ma prima che l'istruzione venga eseguita.

    In questa macchina pseudo-informatica, il puntatore a istruzioni fa parte della memoria . (Nota: normalmente il puntatore di istruzioni sarà un "registro speciale" in un core della CPU, ma qui semplificheremo i concetti e assumeremo che tutti i dati (registri inclusi) facciano parte di "Memoria")

  6. Salta : una volta che hai un numero ordinato di passaggi e un puntatore a istruzioni , puoi applicare l' istruzione " store " per modificare il valore del puntatore a istruzioni stesso. Chiameremo questo uso specifico delle istruzioni del negozio con un nuovo nome: Salta . Usiamo un nuovo nome perché è più facile pensarlo come un nuovo concetto. Modificando il puntatore alle istruzioni stiamo dicendo all'agente di "andare al passaggio x".

  7. Iterazione infinita : saltando indietro, ora puoi far "ripetere" all'agente un certo numero di passaggi. A questo punto abbiamo Iterazione infinita.

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. Condizionale : esecuzione condizionale delle istruzioni. Con la clausola "condizionale", è possibile eseguire in modo condizionale una delle numerose istruzioni basate sullo stato corrente (che può essere impostato con un'istruzione precedente).

  9. Iterazione corretta : ora con la clausola condizionale , possiamo sfuggire al ciclo infinito dell'istruzione jump back . Ora abbiamo un ciclo condizionale e quindi una corretta Iterazione

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. Denominazione : assegnazione di nomi a una specifica posizione di memoria contenente dati o trattenendo un passaggio . Questa è solo una "comodità" da avere. Non aggiungiamo nuove istruzioni avendo la capacità di definire "nomi" per le posizioni di memoria. La "denominazione" non è un'istruzione per l'agente, è solo una comodità per noi. La denominazione rende il codice (a questo punto) più facile da leggere e più facile da modificare.

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. Sottoprogramma di un livello : supponiamo che ci sia una serie di passaggi che devi eseguire frequentemente. È possibile memorizzare i passaggi in una posizione denominata in memoria e quindi saltare a quella posizione quando è necessario eseguirli (chiamata). Alla fine della sequenza dovrai tornare al punto di chiamata per continuare l'esecuzione. Con questo meccanismo, stai creando nuove istruzioni (subroutine) componendo le istruzioni di base.

    Implementazione: (non sono richiesti nuovi concetti)

    • Conservare il puntatore di istruzioni corrente in una posizione di memoria predefinita
    • salta alla subroutine
    • al termine della subroutine, si recupera il puntatore istruzioni dalla posizione di memoria predefinita, tornando effettivamente alle seguenti istruzioni della chiamata originale

    Problema con l' implementazione a un livello : non è possibile chiamare un'altra subroutine da una subroutine. In tal caso, sovrascriverai l'indirizzo di ritorno (variabile globale), quindi non puoi nidificare le chiamate.

    Per implementare meglio le subroutine: è necessario uno STACK

  12. Stack : definisci uno spazio di memoria che funzioni come "stack", puoi "spingere" i valori nello stack e anche "pop" l'ultimo valore "spinto". Per implementare uno stack avrai bisogno di uno Stack Pointer (simile all'Istruttore Pointer) che punta alla vera "testa" dello stack. Quando si "spinge" un valore, il puntatore dello stack diminuisce e si memorizza il valore. Quando si "pop", si ottiene il valore dal puntatore dello stack effettivo e quindi il puntatore dello stack viene incrementato.

  13. Subroutine Ora che abbiamo uno stack possiamo implementare subroutine appropriate che consentano chiamate nidificate . L'implementazione è simile, ma invece di memorizzare il puntatore di istruzioni in una posizione di memoria predefinita, "spingiamo" il valore dell'IP nello stack . Alla fine della subroutine, abbiamo semplicemente "pop" il valore dallo stack, saltando effettivamente indietro all'istruzione dopo la chiamata originale . Questa implementazione, con uno "stack", consente di chiamare una subroutine da un'altra subroutine. Con questa implementazione possiamo creare diversi livelli di astrazione quando definiamo nuove istruzioni come subroutine, usando le istruzioni di base o altre subroutine come elementi costitutivi.

  14. Ricorsione : cosa succede quando una subroutine si chiama? Questo si chiama "ricorsione".

    Problema: sovrascrivendo i risultati intermedi locali è possibile memorizzare in memoria un sottoprogramma. Poiché chiamate / riutilizzate gli stessi passaggi, se il risultato intermedio viene archiviato in posizioni di memoria predefinite (variabili globali), verranno sovrascritte sulle chiamate nidificate.

    Soluzione: per consentire la ricorsione, le subroutine dovrebbero archiviare i risultati intermedi locali nello stack , pertanto, su ogni chiamata ricorsiva (diretta o indiretta) i risultati intermedi vengono memorizzati in posizioni di memoria diverse.

...

dopo aver raggiunto la ricorsione ci fermiamo qui.

Conclusione:

In una Von Neumann Architecture, chiaramente "Iterazione" è un concetto più semplice / base di "Ricorsione" . Abbiamo una forma di "Iterazione" al livello 7, mentre "Ricorsione" è al livello 14 della gerarchia dei concetti.

L'iterazione sarà sempre più veloce nel codice macchina perché implica meno istruzioni e quindi meno cicli della CPU.

Qual è il migliore"?

  • Dovresti usare "iterazione" quando elabori strutture di dati semplici e sequenziali, e ovunque farà un "semplice ciclo".

  • Dovresti usare la "ricorsione" quando devi elaborare una struttura di dati ricorsiva (mi piace chiamarli "Strutture di dati frattali") o quando la soluzione ricorsiva è chiaramente più "elegante".

Consiglio : utilizzare lo strumento migliore per il lavoro, ma comprendere il funzionamento interno di ogni strumento per scegliere saggiamente.

Infine, nota che hai molte opportunità di usare la ricorsione. Hai strutture dati ricorsive ovunque, ne stai osservando una adesso: parti del DOM che supportano ciò che stai leggendo sono un RDS, un'espressione JSON è un RDS, il file system gerarchico nel tuo computer è un RDS, ovvero: hai una directory radice, contenente file e directory, ogni directory contenente file e directory, ognuna di quelle directory contenenti file e directory ...


2
Stai assumendo che la tua progressione sia 1) necessaria e 2) che si fermi lì dove l'hai fatto. Ma 1) non è necessario (ad esempio, la ricorsione può essere trasformata in un salto, come spiegato dalla risposta accettata, quindi non è necessario lo stack), e 2) non deve fermarsi qui (ad esempio, alla fine tu raggiungerai l'elaborazione simultanea, che potrebbe richiedere dei blocchi se hai uno stato mutabile mentre introduci nel secondo passaggio, quindi tutto rallenta; mentre una soluzione immutabile come una funzionale / ricorsiva eviterebbe il blocco, quindi potrebbe essere più veloce / più parallela) .
hmijail piange le dimissioni il

2
"la ricorsione può essere trasformata in un salto" è falsa. La ricorsione veramente utile non può essere trasformata in un salto. La chiamata di coda "ricorsione" è un caso speciale, in cui si codifica "come ricorsione" qualcosa che può essere semplificato in un ciclo dal compilatore. Inoltre stai unendo "immutabile" con "ricorsione", quelli sono concetti ortogonali.
Lucio M. Tato,

"La ricorsione veramente utile non può essere trasformata in un salto" -> quindi l'ottimizzazione della coda è in qualche modo inutile? Inoltre, l'immutabilità e la ricorsione potrebbero essere ortogonali, ma tu fai un collegamento in loop con contatori mutabili - guarda il tuo passaggio 9. Mi sembra che tu pensi che il loop e la ricorsione siano concetti radicalmente diversi; non lo sono. stackoverflow.com/questions/2651112/...
Cordoglio hmijail resignees

@hmijail Penso che una parola migliore di "utile" sia "vera". La ricorsione della coda non è una vera ricorsione perché utilizza solo la funzione che chiama la sintassi per mascherare la ramificazione incondizionata, cioè l'iterazione. La vera ricorsione ci fornisce uno stack di backtracking. Tuttavia, la ricorsione della coda è ancora espressiva, il che la rende utile. Le proprietà della ricorsione che rendono facile o più semplice analizzare il codice per correttezza sono conferite al codice iterativo quando viene espresso usando le chiamate di coda. Anche se a volte questo è leggermente compensato da un'ulteriore complicazione nella versione di coda come parametri extra.
Kaz

34

La ricorsione può benissimo essere più veloce laddove l'alternativa è quella di gestire esplicitamente uno stack, come negli algoritmi di ordinamento o albero binario menzionati.

Ho avuto un caso in cui riscrivere un algoritmo ricorsivo in Java lo ha reso più lento.

Quindi l'approccio giusto è prima di scriverlo nel modo più naturale, ottimizzare solo se la profilazione mostra che è fondamentale, e quindi misurare il presunto miglioramento.


2
+1 per " prima scrivilo nel modo più naturale " e soprattutto " ottimizza solo se la profilazione mostra che è fondamentale "
TripeHound

2
+1 per riconoscere che lo stack hardware può essere più veloce di uno stack in-heap software, implementato manualmente. Mostrando efficacemente che tutte le risposte "no" sono errate.
sh1


12

Considera cosa si deve assolutamente fare per ciascuno, iterazione e ricorsione.

  • iterazione: un salto all'inizio del loop
  • ricorsione: un salto all'inizio della funzione chiamata

Vedi che qui non c'è molto spazio per le differenze.

(Suppongo che la ricorsione sia un richiamo e che il compilatore sia consapevole di tale ottimizzazione).


9

La maggior parte delle risposte qui dimenticano l'ovvio colpevole del perché la ricorsione è spesso più lenta delle soluzioni iterative. È collegato all'accumulo e alla demolizione dei frame dello stack, ma non è esattamente quello. In genere è una grande differenza nella memorizzazione della variabile automatica per ogni ricorsione. In un algoritmo iterativo con un ciclo, le variabili sono spesso contenute nei registri e anche se si riversano, risiedono nella cache di livello 1. In un algoritmo ricorsivo, tutti gli stati intermedi della variabile sono memorizzati nello stack, il che significa che genereranno molti più sversamenti di memoria. Ciò significa che anche se eseguirà la stessa quantità di operazioni, avrà molti accessi di memoria nel hot loop e ciò che lo rende peggio, queste operazioni di memoria hanno un pessimo tasso di riutilizzo che rende le cache meno efficaci.

Gli algoritmi ricorsivi TL; DR hanno generalmente un comportamento cache peggiore rispetto a quelli iterativi.


6

La maggior parte delle risposte qui sono sbagliate . La risposta giusta è che dipende . Ad esempio, qui ci sono due funzioni C che attraversano un albero. Innanzitutto quello ricorsivo:

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

Ed ecco la stessa funzione implementata usando l'iterazione:

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

Non è importante comprendere i dettagli del codice. Sono solo pnodi e questo P_FOR_EACH_CHILDfa camminare. Nella versione iterativa abbiamo bisogno di uno stack esplicito stsu cui i nodi vengono spinti e quindi spuntati e manipolati.

La funzione ricorsiva è molto più veloce di quella iterativa. Il motivo è perché in quest'ultimo, per ogni elemento, è necessaria una CALLfunzione st_pushe poi un'altra a st_pop.

Nel primo, hai solo il ricorsivo CALLper ciascun nodo.

Inoltre, l'accesso alle variabili sul callstack è incredibilmente veloce. Significa che stai leggendo dalla memoria che probabilmente si troverà sempre nella cache più interna. Uno stack esplicito, d'altra parte, deve essere supportato da malloc: memoria ed dall'heap a cui è molto più lento accedere.

Con un'attenta ottimizzazione, come inline st_pushe st_pop, posso raggiungere approssimativamente la parità con l'approccio ricorsivo. Ma almeno sul mio computer, il costo di accesso alla memoria dell'heap è maggiore del costo della chiamata ricorsiva.

Ma questa discussione è per lo più discutibile perché la camminata ricorsiva sugli alberi non è corretta . Se si dispone di un albero abbastanza grande, si esaurirà lo spazio del callstack, motivo per cui è necessario utilizzare un algoritmo iterativo.


Posso confermare di essermi imbattuto in una situazione simile e che ci sono situazioni in cui la ricorsione può essere più rapida di una pila manuale sull'heap. Soprattutto quando l'ottimizzazione è attivata nel compilatore per evitare il sovraccarico di chiamare una funzione.
while1fork

1
Ha effettuato un attraversamento pre-ordine di un albero binario a 7 nodi 10 ^ 8 volte. Ricorsione 25ns. Stack esplicito (controllato o meno - non fa molta differenza) ~ 15 ns. La ricorsione deve fare di più (registrare il salvataggio e il restauro + (di solito) allineamenti del telaio più rigidi) oltre a spingere e saltare. (E peggiora con PLT nelle librerie collegate dinamicamente.) Non è necessario allocare in pila lo stack esplicito. Puoi fare un ostacolo il cui primo frame è nello stack di chiamate normale in modo da non sacrificare la località della cache per il caso più comune in cui non superi il primo blocco.
PSkocik,

3

In generale, no, la ricorsione non sarà più veloce di un ciclo in qualsiasi utilizzo realistico che abbia implementazioni praticabili in entrambe le forme. Voglio dire, certo, potresti codificare i loop che impiegano un'eternità, ma ci sarebbero modi migliori per implementare lo stesso loop che potrebbe superare qualsiasi implementazione dello stesso problema tramite la ricorsione.

Hai colpito l'unghia sulla testa riguardo al motivo; la creazione e la distruzione di stack frame è più costosa di un semplice salto.

Tuttavia, si noti che ho detto "ha implementazioni praticabili in entrambe le forme". Per cose come molti algoritmi di ordinamento, tende a non esserci un modo molto attuabile di implementarli che non imposta efficacemente la propria versione di uno stack, a causa della generazione di "compiti" figlio che sono intrinsecamente parte del processo. Pertanto, la ricorsione può essere altrettanto veloce del tentativo di implementare l'algoritmo tramite il looping.

Modifica: questa risposta presuppone linguaggi non funzionali, in cui la maggior parte dei tipi di dati di base sono modificabili. Non si applica ai linguaggi funzionali.


Questo è anche il motivo per cui molti casi di ricorsione sono spesso ottimizzati dai compilatori in lingue in cui la ricorsione è frequentemente utilizzata. In F #, ad esempio, oltre al pieno supporto alle funzioni ricorsive della coda con il codice operativo .tail, spesso si vede una funzione ricorsiva compilata come un ciclo.
em70,

Sì. La ricorsione della coda a volte può essere il migliore di entrambi i mondi: il modo funzionalmente "appropriato" per implementare un'attività ricorsiva e le prestazioni dell'uso di un ciclo.
Ambra

1
Questo non è, in generale, corretto. In alcuni ambienti, la mutazione (che interagisce con GC) è più costosa della ricorsione della coda, che si trasforma in un loop più semplice nell'output che non utilizza un frame stack aggiuntivo.
Dietrich Epp,

2

In qualsiasi sistema realistico, no, la creazione di uno stack frame sarà sempre più costosa di un INC e un JMP. Ecco perché i compilatori davvero buoni trasformano automaticamente la ricorsione della coda in una chiamata allo stesso frame, cioè senza il sovraccarico, in modo da ottenere la versione di origine più leggibile e la versione compilata più efficiente. Un compilatore davvero valido dovrebbe anche essere in grado di trasformare la ricorsione normale in ricorsione della coda laddove ciò sia possibile.


1

La programmazione funzionale riguarda più " cosa " anziché " come ".

Gli implementatori di linguaggio troveranno un modo per ottimizzare il funzionamento del codice, se non cerchiamo di renderlo più ottimizzato di quanto debba essere. La ricorsione può anche essere ottimizzata nelle lingue che supportano l'ottimizzazione delle chiamate di coda.

Ciò che conta di più dal punto di vista del programmatore è la leggibilità e la manutenibilità piuttosto che l'ottimizzazione in primo luogo. Ancora una volta, "l'ottimizzazione prematura è la radice di tutti i mali".


0

Questa è un'ipotesi. Generalmente la ricorsione probabilmente non batte il looping spesso o mai su problemi di dimensioni decenti se entrambi utilizzano algoritmi davvero buoni (senza contare le difficoltà di implementazione), potrebbe essere diverso se utilizzato con un linguaggio con ricorsione call call (e un algoritmo ricorsivo di coda e con i loop anche come parte della lingua), che probabilmente avrebbero molto simili e forse preferirebbe anche la ricorsione qualche volta.


0

Secondo la teoria sono le stesse cose. La ricorsione e il loop con la stessa complessità O () funzioneranno con la stessa velocità teorica, ma ovviamente la velocità reale dipende dal linguaggio, dal compilatore e dal processore. L'esempio con la potenza del numero può essere codificato in modo iterativo con O (ln (n)):

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }

1
Big O è "proporzionale a". Quindi entrambi lo sono O(n), ma uno potrebbe richiedere xtempi più lunghi dell'altro, per tutti n.
ctrl-alt-delor,
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.