Ricorsione o mentre loop


123

Stavo leggendo alcune pratiche di intervista di sviluppo, in particolare sulle domande tecniche e sui test posti durante le interviste e sono inciampato parecchie volte sui detti del genere "Ok hai risolto il problema con un ciclo while, ora puoi farlo con ricorsione ", o" tutti possono risolvere questo con un ciclo di 100 linee mentre, ma possono farlo in una funzione ricorsiva a 5 linee? " eccetera.

La mia domanda è: la ricorsione è generalmente migliore di if / while / for build?

Onestamente ho sempre pensato che la ricorsione non fosse da preferire, perché è limitata alla memoria dello stack che è molto più piccola dell'heap, anche fare un gran numero di chiamate funzione / metodo non è ottimale dal punto di vista delle prestazioni, ma potrei essere in errore...



73
In tema di ricorsione, questo sembra abbastanza interessante.
dan_waterworth,

4
@dan_waterworth anche, questo aiuterebbe: google.fr/… ma mi sembra sempre di sbagliarlo: P
Shivan Dragon

@ShivanDragon Ho pensato così ^ _ ^ Molto appropriato che l'ho pubblicato ieri :-)
Neal

2
Negli ambienti embedded in cui ho lavorato in ricorsione, nella migliore delle ipotesi è malvisto e nel peggiore dei casi ti fa frustare pubblicamente. Lo spazio di stack limitato in effetti lo rende illegale.
Fred Thomsen il

Risposte:


192

La ricorsione non è intrinsecamente migliore o peggiore dei loop: ognuno ha vantaggi e svantaggi e quelli dipendono anche dal linguaggio di programmazione (e dall'implementazione).

Tecnicamente, i loop iterativi si adattano meglio ai sistemi informatici tipici a livello hardware: a livello di codice macchina, un loop è solo un test e un salto condizionale, mentre la ricorsione (implementata ingenuamente) comporta la spinta di un frame dello stack, il salto, il ritorno e il ritorno indietro dallo stack. OTOH, molti casi di ricorsione (specialmente quelli che sono banalmente equivalenti ai loop iterativi) possono essere scritti in modo da evitare il push / pop dello stack; ciò è possibile quando la chiamata di funzione ricorsiva è l'ultima cosa che accade nel corpo della funzione prima di tornare ed è comunemente nota come ottimizzazione della chiamata di coda (o ottimizzazione della ricorsione di coda ). Una funzione ricorsiva ottimizzata per il richiamo della coda equivale in gran parte a un ciclo iterativo a livello di codice macchina.

Un'altra considerazione è che i cicli iterativi richiedono aggiornamenti di stato distruttivi, il che li rende incompatibili con la semantica del linguaggio puro (senza effetti collaterali). Questo è il motivo per cui i linguaggi puri come Haskell non hanno affatto costrutti loop e molti altri linguaggi di programmazione funzionale o li mancano completamente o li evitano il più possibile.

Il motivo per cui queste domande appaiono così tanto nelle interviste, tuttavia, è perché per rispondere ad esse è necessaria una conoscenza approfondita di molti concetti di programmazione vitale - variabili, chiamate di funzione, ambito e, naturalmente, loop e ricorsione - e hai portare sul tavolo la flessibilità mentale che ti consente di affrontare un problema da due angolazioni radicalmente diverse e di muoverti tra manifestazioni diverse dello stesso concetto.

L'esperienza e la ricerca suggeriscono che esiste una linea tra le persone che hanno la capacità di comprendere variabili, puntatori e ricorsione e quelle che non lo fanno. Quasi tutto il resto della programmazione, inclusi framework, API, linguaggi di programmazione e relativi casi limite, può essere acquisito attraverso lo studio e l'esperienza, ma se non si è in grado di sviluppare un'intuizione per questi tre concetti fondamentali, non si è idonei a essere programmatori. Tradurre un semplice ciclo iterativo in una versione ricorsiva è il modo più rapido possibile di filtrare i non programmatori - anche un programmatore piuttosto inesperto può farlo in 15 minuti, ed è un problema molto indipendente dal linguaggio, quindi il candidato può scegliere una lingua a loro scelta invece di inciampare su idiosincrasie.

Se si riceve una domanda come questa in un'intervista, è un buon segno: significa che il potenziale datore di lavoro è alla ricerca di persone in grado di programmare, non di persone che hanno memorizzato il manuale di uno strumento di programmazione.


3
Mi piace di più la tua risposta, perché tocca gli aspetti più importanti che potrebbe avere una risposta a questa domanda, spiega le parti tecniche principali e offre anche una buona visione ingrandita su come questo problema si adatta alla sfera della programmazione.
Drago Shivan,

1
Ho anche notato uno schema in una programmazione di sincronizzazione in cui i loop sono evitati a favore di chiamate ricorsive su un iteratore che contiene un metodo .next (). Presumo che il codice di lunga durata non diventi troppo avido di CPU.
Evan Plaice,

1
La versione iterativa prevede anche la spinta e il pop-up dei valori. Devi solo scrivere manualmente il codice per farlo. La versione ricorsiva sta spingendo lo stato sullo stack in un algoritmo iterativo che di solito è necessario simulare manualmente spingendo lo stato in una certa struttura. Solo gli algoritmi più banali non hanno bisogno di questo stato e in questi casi il compilatore può di solito individuare la ricorsione della coda e piantare una soluzione iterativa.
Martin York,

1
@tdammers Potresti dirmi dove posso leggere lo studio che hai citato "L'esperienza e la ricerca suggeriscono che esiste una linea tra le persone ..." Questo mi sembra molto interessante.
Yoo Matsuo,

2
Una cosa che hai dimenticato di menzionare, il codice iterativo tende a funzionare meglio quando hai a che fare con un singolo thread di esecuzione, ma gli algoritmi ricorsivi tendono a prestarsi ad essere eseguiti in più thread.
GordonM,

37

Dipende.

  • Alcuni problemi sono molto suscettibili alle soluzioni ricorsive, ad esempio quicksort
  • Alcune lingue non supportano realmente la ricorsione, ad esempio i primi FORTRAN
  • Alcune lingue assumono la ricorsione come mezzo principale per il looping, ad esempio Haskell

Vale anche la pena notare che il supporto per la ricorsione della coda rende equivalenti i cicli ricorsivi e iterativi della coda, cioè la ricorsione non deve sempre sprecare lo stack.

Inoltre, un algoritmo ricorsivo può sempre essere implementato in modo iterativo utilizzando uno stack esplicito .

Infine, noterei che una soluzione a cinque righe è probabilmente sempre migliore di una a 100 righe (supponendo che siano effettivamente equivalenti).


5
Bella risposta (+1). "una soluzione a 5 righe è probabilmente sempre migliore di una a 100 righe": penso che la concisione non sia l'unico vantaggio della ricorsione. L'uso di una chiamata ricorsiva ti costringe a rendere esplicite le dipendenze funzionali tra valori in diverse iterazioni.
Giorgio

4
Le soluzioni più brevi tendono ad essere migliori, ma esiste qualcosa di eccessivamente conciso.
dan_waterworth,

5
@dan_waterworth rispetto a "100 line" è piuttosto difficile essere eccessivamente concisi
moscerino

4
@Giorgio, puoi ridurre i programmi rimuovendo il codice non necessario o rendendo implicite le cose esplicite. Finché ti attieni alla prima, migliorerai la qualità.
dan_waterworth,

1
@jk, penso che sia un'altra forma di rendere implicite le informazioni esplicite. Le informazioni su ciò a cui viene utilizzata una variabile vengono rimosse dal nome in cui sono esplicite e vengono spinte nell'uso implicito.
dan_waterworth,

17

Non c'è universalmente concordato sulla definizione di "migliore" quando si tratta di programmazione, ma lo prenderò per dire "più facile da mantenere / leggere".

La ricorsione ha più potere espressivo dei costrutti di loop iterativi: lo dico perché un ciclo while equivale a una funzione ricorsiva della coda e le funzioni ricorsive non devono necessariamente essere ricorsive della coda. Costrutti potenti di solito sono una brutta cosa perché ti permettono di fare cose difficili da leggere. Tuttavia, la ricorsione ti dà la possibilità di scrivere loop senza usare la mutabilità e per me la mutabilità è molto più potente della ricorsione.

Quindi, dal basso potere espressivo all'alto potere espressivo, i costrutti in loop si accumulano in questo modo:

  • Funzioni ricorsive di coda che utilizzano dati immutabili,
  • Funzioni ricorsive che utilizzano dati immutabili,
  • Mentre i loop che utilizzano dati mutabili,
  • Funzioni ricorsive di coda che utilizzano dati mutabili,
  • Funzioni ricorsive che utilizzano dati mutabili,

Idealmente, useresti i costrutti meno espressivi che puoi. Naturalmente, se la tua lingua non supporta l'ottimizzazione delle chiamate di coda, ciò potrebbe influenzare anche la scelta del costrutto di loop.


1
"un ciclo while equivale a una funzione ricorsiva della coda e le funzioni ricorsive non devono necessariamente essere ricorsive della coda": +1. È possibile simulare la ricorsione mediante un ciclo while + una pila.
Giorgio

1
Non sono sicuro di essere d'accordo al 100%, ma è sicuramente una prospettiva interessante, quindi +1 per quello.
Konrad Rudolph,

+1 per un'ottima risposta e anche per aver menzionato il fatto che alcune lingue (o compilatori) non ottimizzano le chiamate in coda.
Drago Shivan,

@Giorgio, "Puoi simulare la ricorsione mediante un ciclo while + uno stack", ecco perché ho detto potere espressivo. Computazionalmente, sono ugualmente potenti.
dan_waterworth,

@dan_waterworth: Esatto, e come hai detto nella tua risposta, la sola ricorsione è più espressiva di un ciclo while perché è necessario aggiungere uno stack a un ciclo while per simulare la ricorsione.
Giorgio,

7

La ricorsione è spesso meno ovvia. Meno ovvio è più difficile da mantenere.

Se scrivi for(i=0;i<ITER_LIMIT;i++){somefunction(i);}nel flusso principale, chiarisci perfettamente che stai scrivendo un ciclo. Se scrivi somefunction(ITER_LIMIT);non chiarisci davvero cosa accadrà. Solo vedere contenuti: quella somefunction(int x)chiamata somefunction(x-1)ti dice che è in effetti un ciclo che utilizza iterazioni. Inoltre, non puoi facilmente mettere una condizione di escape con break;da qualche parte a metà delle iterazioni, devi aggiungere un condizionale che sarà passato completamente indietro o lanciare un'eccezione. (e le eccezioni aggiungono ancora complessità ...)

In sostanza, se è una scelta ovvia tra iterazione e ricorsione, fai la cosa intuitiva. Se l'iterazione fa facilmente il lavoro, salvare 2 righe raramente vale i mal di testa che può creare a lungo termine.

Ovviamente se ti sta risparmiando 98 linee, è una questione completamente diversa.

Ci sono situazioni in cui la ricorsione si adatta semplicemente perfettamente e non sono inusuali. Attraversamento di strutture ad albero, reti collegate in modo multiplo, strutture che possono contenere il proprio tipo, matrici frastagliate multidimensionali, praticamente tutto ciò che non è né un vettore semplice né un array di dimensioni fisse. Se attraversi un percorso noto e rettilineo, procedi. Se ti immergi nell'ignoto, ricontattati.

In sostanza, se somefunction(x-1)deve essere chiamato da se stesso più di una volta per livello, dimentica le iterazioni.

... Scrivere in modo iterativo funzioni per compiti che vengono eseguiti meglio dalla ricorsione è possibile ma non piacevole. Ovunque tu usi int, hai bisogno di qualcosa del genere stack<int>. L'ho fatto una volta, più come esercizio che per scopi pratici. Posso assicurarti che una volta affrontato un compito del genere non avrai dubbi come quelli che hai espresso.


10
Ciò che è ovvio e ciò che è meno ovvio dipende parzialmente da ciò a cui sei abituato. L'iterazione è stata utilizzata più spesso nei linguaggi di programmazione perché è più vicina al modo di lavorare della CPU (ovvero utilizza meno memoria ed esegue più velocemente). Ma se sei abituato a pensare in modo induttivo, la ricorsione può essere altrettanto intuitiva.
Giorgio

5
"Se vedono una parola chiave loop, sanno che è un ciclo. Ma non esiste una parola chiave recurse, possono riconoscerla solo vedendo f (x-1) all'interno di f (x).": Quando chiami una funzione ricorsiva lo fai non voglio sapere che è ricorsivo. Allo stesso modo, quando si chiama una funzione contenente un ciclo, non si desidera sapere che contiene un ciclo.
Giorgio,

3
@SF: Sì, ma puoi vederlo solo se guardi il corpo della funzione. Nel caso di un loop, vedi il loop, in caso di ricorsione, vedi che la funzione si chiama da sola.
Giorgio,

5
@SF: Mi sembra un po 'un ragionamento circolare: "Se nella mia intuizione è un ciclo, allora è un ciclo". mappuò essere definita come una funzione ricorsiva (vedi ad esempio haskell.org/tutorial/functions.html ), anche se è intuitivamente chiaro che attraversa un elenco e applica una funzione a ciascun membro dell'elenco.
Giorgio,

5
@SF, mapnon è una parola chiave, è una funzione regolare, ma è un po 'irrilevante. Quando i programmatori funzionali usano la ricorsione, di solito non è perché vogliono eseguire una sequenza di azioni, ma perché il problema che si sta risolvendo può essere espresso come una funzione e un elenco di argomenti. Il problema può quindi essere ridotto a un'altra funzione e a un altro elenco di argomenti. Alla fine, hai un problema che può essere risolto banalmente.
dan_waterworth,

6

Come al solito, ciò è senza risposta in generale perché ci sono fattori aggiuntivi, che in pratica sono ampiamente disuguali tra i casi e ineguali tra loro all'interno di un caso d'uso. Ecco alcune delle pressioni.

  • Il codice breve ed elegante è in generale superiore al codice lungo e complesso.
  • Tuttavia, l'ultimo punto è in qualche modo invalidato se la base di sviluppatori non ha familiarità con la ricorsione e non è disposta / incapace di apprendere. Potrebbe persino diventare un leggero negativo piuttosto che positivo.
  • La ricorsione può essere dannosa per l'efficienza se in pratica avrai bisogno di chiamate profondamente annidate e non puoi usare la ricorsione della coda (o il tuo ambiente non può ottimizzare la ricorsione della coda).
  • La ricorsione è anche negativa in molti casi se non è possibile memorizzare correttamente nella cache i risultati intermedi. Ad esempio, l'esempio comune dell'utilizzo della ricorsione dell'albero per calcolare i numeri di Fibonacci ha un rendimento orribile se non si memorizza nella cache. Se fai cache, è semplice, veloce, elegante e assolutamente meraviglioso.
  • La ricorsione non è applicabile in alcuni casi, tanto quanto l'iterazione in altri, e assolutamente necessaria in altri ancora. Scorrere in lunghe catene di regole aziendali di solito non è affatto aiutato dalla ricorsione. L'iterazione attraverso flussi di dati può essere utilmente eseguita con la ricorsione. L'iterazione su strutture di dati dinamici multidimensionali (ad esempio labirinti, alberi di oggetti ...) è praticamente impossibile senza ricorsione, esplicita o implicita. Si noti che in questi casi, la ricorsione esplicita è molto meglio che implicita: nulla è più doloroso della lettura del codice in cui qualcuno ha implementato il proprio stack buggy ad hoc, incompleto all'interno del linguaggio solo per evitare la spaventosa parola R.

Cosa intendi per cache nella relazione alla ricorsione?
Giorgio,

@Giorgio presumibilmente memoization
jk.

Feh. Se il tuo ambiente non ottimizza le chiamate di coda, dovresti trovare un ambiente migliore e se i tuoi sviluppatori non hanno familiarità con la ricorsione, dovresti trovare sviluppatori migliori. Alcuni standard, gente!
CA McCann,

1

La ricorsione riguarda la ripetizione della chiamata alla funzione, il ciclo riguarda la ripetizione del salto da inserire nella memoria.

Dovrebbe essere menzionato anche sull'overflow dello stack - http://it.wikipedia.org/wiki/Stack_overflow


1
Ricorsione significa una funzione la cui definizione implica la chiamata stessa.
Hardmath

1
Mentre la tua semplice definizione non è esattamente precisa al 100%, sei l'unico che ha menzionato lo overflow dello stack.
Qix,

1

Dipende davvero dalla convenienza o dal requisito:

Se si utilizza il linguaggio di programmazione Python , supporta la ricorsione, ma per impostazione predefinita esiste un limite per la profondità di ricorsione (1000). Se supera il limite, avremo un errore o un'eccezione. Tale limite può essere modificato, ma se lo facciamo potremmo riscontrare situazioni anomale nella lingua.

In questo momento (numero di chiamate superiore alla profondità di ricorsione), dobbiamo preferire i costrutti loop. Voglio dire, se la dimensione dello stack non è sufficiente, dobbiamo preferire i costrutti di loop.


3
Ecco un blog di Guido van Rossum sul perché non vuole l'ottimizzazione della ricorsione della coda in Python (supportando l'idea che lingue diverse adottano approcci tattici distinti).
Hardmath,

-1

Usa il modello di progettazione della strategia.

  • La ricorsione è pulita
  • I loop sono (probabilmente) efficienti

A seconda del carico (e / o di altre condizioni), scegline uno.


5
Aspetta cosa? Come si inserisce il modello di strategia qui? E la seconda frase sembra una frase vuota.
Konrad Rudolph,

@KonradRudolph Vorrei andare per la ricorsione. Per set di dati molto grandi, passerei ai loop. Ecco cosa intendevo. Chiedo scusa se non fosse chiaro.
Pravin Sonawane,

3
Ah. Bene, non sono ancora sicuro che questo possa essere chiamato "modello di progettazione strategica", che ha un significato molto fisso e lo usi metaforico. Ma ora almeno vedo dove stai andando.
Konrad Rudolph,

@KonradRudolph ha imparato un'importante lezione. Spiega a fondo cosa vuoi dire .. Grazie .. che mi ha aiutato .. :)
Pravin Sonawane,

2
@Pravin Sonawane: se è possibile utilizzare l'ottimizzazione della ricorsione della coda, è possibile utilizzare la ricorsione anche su enormi set di dati.
Giorgio
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.