In parole povere, cos'è la ricorsione?


74

L'idea della ricorsione non è molto comune nel mondo reale. Quindi, sembra un po 'confuso per i programmatori alle prime armi. Anche se, immagino, si abituano gradualmente al concetto. Quindi, quale può essere una bella spiegazione per loro di afferrare facilmente l'idea?


Ulteriori informazioni vengono condivise su questo argomento in Risorse per migliorare la comprensione della ricorsione?
Kenneth,


1
La ricorsione è quando una funzione può chiamare se stessa. "Se capisci completamente gli spazi dei nomi e l'ambito e come i parametri vengono passati a una funzione, allora conosci già la ricorsione. Posso mostrare esempi, ma dovresti essere in grado di capire come funzionano da soli." Gli studenti generalmente lottano con la ricorsione non tanto perché sono confusi, ma perché non hanno una solida conoscenza di ambito / spazio dei nomi variabile. Prima di immergerti nella ricorsione, assicurati che gli studenti possano tracciare correttamente un programma in cui hai intenzionalmente dato variabili a diversi ambiti con lo stesso nome per confonderle.
dspyz,


1
Per capire la ricorsione, devi prima capire la ricorsione
Goerman,

Risposte:


110

Per spiegare la ricorsione , uso una combinazione di spiegazioni diverse, di solito per entrambi tentare di:

  • spiegare il concetto,
  • spiegare perché è importante,
  • spiega come ottenerlo.

Per cominciare, Wolfram | Alpha lo definisce in termini più semplici di Wikipedia :

Un'espressione tale che ogni termine è generato ripetendo una particolare operazione matematica.


Matematica

Se il tuo studente (o la persona che spieghi anche tu, d'ora in poi dirò studente) ha almeno un background matematico, ovviamente hanno già incontrato la ricorsione studiando le serie e la loro nozione di ricorsività e la loro relazione di ricorrenza .

Un ottimo modo per iniziare è quindi dimostrare con una serie e dire che è semplicemente ciò che riguarda la ricorsione:

  • una funzione matematica ...
  • ... che si chiama per calcolare un valore corrispondente a un n-esimo elemento ...
  • ... e che definisce alcuni confini.

Di solito, o ottieni un "huh huh, whatev '" nella migliore delle ipotesi perché ancora non lo usano, o più probabilmente solo un russare molto profondo.


Esempi di codifica

Per il resto, in realtà è una versione dettagliata di ciò che ho presentato nell'addendum della mia risposta per la domanda che hai sollevato riguardo ai puntatori (bad pun).

In questa fase, i miei studenti di solito sanno come stampare qualcosa sullo schermo. Supponendo che stiamo usando C, sanno come stampare un singolo carattere usando writeo printf. Conoscono anche i circuiti di controllo.

Di solito ricorro ad alcuni problemi di programmazione ripetitivi e semplici fino a quando non lo ottengono:

  • l' operazione fattoriale ,
  • una stampante alfabetica,
  • una stampante alfabetica invertita,
  • l' operazione di esponenziale .

Fattoriale

Il fattoriale è un concetto matematico molto semplice da capire e l'implementazione è molto vicina alla sua rappresentazione matematica. Tuttavia, all'inizio potrebbero non capirlo.

Definizione ricorsiva dell'operazione fattoriale

alfabeti

La versione alfabetica è interessante per insegnare loro a pensare all'ordinamento delle loro dichiarazioni ricorsive. Come con i puntatori, ti lanceranno semplicemente delle linee in modo casuale. Il punto è portarli a rendersi conto che un ciclo può essere invertito modificando le condizioni OPPURE invertendo semplicemente l'ordine delle istruzioni nella tua funzione. Ecco dove la stampa dell'alfabeto aiuta, poiché è qualcosa di visivo per loro. Basta far scrivere a loro una funzione che stamperà un carattere per ogni chiamata e si chiamerà in modo ricorsivo per scrivere quello successivo (o precedente).

Fan FP, salta il fatto che stampare elementi sul flusso di output è un effetto collaterale per ora ... Non diventiamo troppo fastidiosi sul fronte FP. (Ma se usi una lingua con il supporto dell'elenco, sentiti libero di concatenare un elenco ad ogni iterazione e semplicemente stampare il risultato finale. Ma di solito li avvio con C, che purtroppo non è il migliore per questo tipo di problemi e concetti) .

elevamento a potenza

Il problema esponenziale è leggermente più difficile ( in questa fase dell'apprendimento). Ovviamente il concetto è esattamente lo stesso di un fattoriale e non c'è complessità aggiunta ... tranne per il fatto che hai più parametri. E questo di solito è abbastanza per confondere le persone e buttarle via all'inizio.

La sua forma semplice:

Forma semplice dell'operazione di esponenziazione

può essere espresso in questo modo dalla ricorrenza:

Relazione di ricorrenza per l'operazione di esponenziazione

Più forte

Una volta che questi semplici problemi sono stati mostrati E reimplementati nei tutorial, puoi dare esercizi leggermente più difficili (ma molto classici):

Nota: di nuovo, alcuni di questi non sono affatto più difficili ... Si limitano ad affrontare il problema esattamente dalla stessa angolazione, o leggermente diversa. Ma la pratica rende perfetti.


Helpers

Un riferimento

Alcune letture non fanno mai male. Bene all'inizio, e si sentiranno ancora più persi. È il genere di cose che cresce su di te e che rimane nella parte posteriore della testa fino a quando un giorno ti accorgi di averlo finalmente ottenuto. E poi ripensi a queste cose che leggi. La ricorsione , la ricorsione in Informatica e le pagine di relazione di ricorrenza su Wikipedia farebbero per ora.

Livello / profondità

Supponendo che i tuoi studenti non abbiano molta esperienza di programmazione, fornisci stub di codice. Dopo i primi tentativi, assegnare loro una funzione di stampa in grado di visualizzare il livello di ricorsione. La stampa del valore numerico del livello aiuta.

Il diagramma Stack-as-Drawers

Anche il rientro di un risultato stampato (o dell'output del livello) aiuta, poiché fornisce un'altra rappresentazione visiva di ciò che il programma sta facendo, aprendo e chiudendo contesti di stack come i cassetti o le cartelle in un esploratore del file system.

Acronimi ricorsivi

Se il tuo studente è già un po 'esperto nella cultura del computer, potrebbe già utilizzare alcuni progetti / software con nomi che utilizzano acronimi ricorsivi . È una tradizione che va in giro da qualche tempo, specialmente nei progetti GNU. Alcuni esempi includono:

Ricorsivo:

  • GNU - "GNU's Not Unix"
  • Nagios - "Nagios non vuole insistere sulla santità"
  • PHP - "PHP Hypertext Preprocessor" (e originariamente "Personal Home Page")
  • Vino - "Il vino non è un emulatore"
  • Zile - "Zile Is Lossy Emacs"

Reciprocamente ricorsivo:

  • HURD - "HIRD of Unix-Replacing Daemons" (dove HIRD è "HURD of Interfaces che rappresenta Depth")

Fagli provare a inventare il loro.

Allo stesso modo, ci sono molte occorrenze di umorismo ricorsivo, come la correzione ricorsiva della ricerca di Google . Per ulteriori informazioni sulla ricorsione, leggi questa risposta .


Insidie ​​e ulteriore apprendimento

Alcuni problemi con cui le persone di solito lottano e per i quali è necessario conoscere le risposte.

Perché, oh Dio, perché ???

Perché dovresti farlo? Un motivo valido ma non ovvio è che spesso è più semplice esprimere un problema in questo modo. Una ragione non così buona ma ovvia è che spesso ci vuole meno battitura a macchina (non farli sentire veramente l33t solo per usare la ricorsione però ...).

Alcuni problemi sono sicuramente più facili da risolvere quando si utilizza un approccio ricorsivo. In genere, qualsiasi problema che è possibile risolvere utilizzando un paradigma Divide and Conquer si adatta a un algoritmo di ricorsione multi-ramificato.

Cosa c'è di nuovo N ??

Perché il mio no (qualunque sia il nome della tua variabile) ogni volta è diverso? I principianti di solito hanno un problema a capire cosa sono una variabile e un parametro e come le cose nominate nnel tuo programma possono avere valori diversi. Quindi ora se questo valore è nel circuito di controllo o ricorsione, è ancora peggio! Sii gentile e non usare gli stessi nomi di variabili ovunque, e chiarisci che i parametri sono solo variabili .

Condizioni finali

Come posso determinare la mia condizione finale? È facile, basta far loro dire i passi ad alta voce. Ad esempio per il fattoriale iniziare da 5, quindi 4, quindi ... fino a 0.

Il diavolo è nei dettagli

Non parlare con cose precoci come l' ottimizzazione delle chiamate di coda . Lo so, lo so, il TCO è carino, ma all'inizio non gliene importa. Concedi loro un po 'di tempo per avvolgere la testa attorno al processo in un modo che funzioni per loro. Sentiti libero di frantumare il loro mondo più tardi, ma concedi loro una pausa.

Allo stesso modo, non parlare direttamente dalla prima lezione dello stack di chiamate e del suo consumo di memoria e ... beh ... lo stack trabocca . Spesso insegno agli studenti privatamente che mi mostrano lezioni in cui hanno 50 diapositive su tutto ciò che c'è da sapere sulla ricorsione quando riescono a malapena a scrivere correttamente un ciclo in questa fase. Questo è un buon esempio di come un riferimento ti aiuterà più tardi, ma in questo momento ti confonde profondamente.

Ma per favore, a tempo debito, chiarisci che ci sono ragioni per percorrere la strada iterativa o ricorsiva .

Ricorsione reciproca

Abbiamo visto che le funzioni possono essere ricorsive e persino che possono avere più punti di chiamata (8 regine, Hanoi, Fibonacci o persino un algoritmo di esplorazione per un dragamine). Ma che dire delle chiamate reciprocamente ricorsive ? Inizia anche qui con la matematica. f(x) = g(x) + h(x)dove g(x) = f(x) + l(x)e he lsolo fare cose.

Iniziare con solo serie matematiche semplifica la scrittura e l'implementazione poiché il contratto è chiaramente definito dalle espressioni. Ad esempio, le sequenze maschili e femminili di Hofstadter :

Sequenze maschili e femminili di Hofstadter

Tuttavia, in termini di codice, è da notare che l'implementazione di una soluzione reciprocamente ricorsivo spesso porta alla duplicazione del codice e dovrebbe piuttosto essere semplificato in una singola forma ricorsiva (Vedere Peter Norvig s' Solving Ogni Sudoku Puzzle .


5
Sto leggendo la tua risposta dopo averla vista dopo quasi 5 o 6 volte. Penso che sia stato bello ma troppo lungo per attirare altri utenti. Ho imparato molte cose sull'insegnamento della ricorsione qui. Come insegnante, vorresti per favore valutare la mia idea per insegnare recursion- programmers.stackexchange.com/questions/25052/…
Gulshan l'

9
@Gulshan, penso che questa risposta sia esaustiva come qualsiasi sarà ed è facilmente "scremata" da un lettore occasionale. Quindi, ottiene un static unsigned int vote = 1;da me. Perdona l'umorismo statico, se vuoi :) Questa è la risposta migliore finora.
Tim Post

1
@Gulsan: solo chi vuole imparare è disposto a prendersi il tempo necessario per farlo è propriamente :) Non mi dispiace davvero. A volte, una risposta breve è elegante e trasmette molte informazioni utili e necessarie per iniziare o spiegare un concetto generale. Volevo solo una risposta più lunga per quella, e considerando che il PO menziona una domanda per la quale mi è stata assegnata la risposta "corretta" e ne ho fatta una simile, ho ritenuto opportuno fornire lo stesso tipo di risposta. Sono contento che tu abbia imparato qualcosa.
haylem,

@Gulshan: ora riguardo alla tua risposta: il primo punto mi ha confuso molto, devo dire. Mi piace che descrivi il concetto di funzioni ricorsive come qualcosa che cambia stato in modo incrementale nel tempo, ma penso che il tuo modo di presentare sia un po 'strano. Dopo aver letto i tuoi 3 punti, non mi aspetto che molti studenti possano improvvisamente essere in grado di risolvere Hanoi. Ma potrebbe essere solo un problema di formulazione.
Haylem,

Mi sono imbattuto in un buon modo per dimostrare che la stessa variabile può avere valori diversi in diverse profondità di ricorsione: considera di far scrivere agli studenti i passi che stanno compiendo e i valori delle variabili nel seguire un codice ricorsivo. Quando raggiungono una ricorsione, farli ricominciare da capo su un nuovo pezzo. Quando raggiungono una condizione di uscita, riportali al pezzo precedente. Questo è essenzialmente emulare uno stack di chiamate, ma è un buon modo per mostrare queste differenze.
Andy Hunt,

58

La chiamata di una funzione all'interno di quella stessa funzione.


2
Questa è la migliore spiegazione da cui partire davvero. Semplice e al punto; una volta stabilito questo riepilogo, vai a tutti i dettagli del viaggio di andata e ritorno.
deridere il

27

La ricorsione è una funzione che si chiama da sola.

Come usarlo, quando usarlo e come evitare un cattivo design sono importanti da sapere, il che richiede di provarlo da soli e capire cosa succede.

La cosa più importante che devi sapere è stare molto attenti a non fare un ciclo che non finisce mai. La risposta di pramodc84 alla tua domanda ha questo errore: non finisce mai ...
Una funzione ricorsiva deve sempre verificare la presenza di una condizione per determinare se deve richiamare nuovamente se stessa.

L'esempio più classico per usare la ricorsione è lavorare con un albero senza limiti statici in profondità. Questa è un'attività che è necessario utilizzare la ricorsione.


Puoi comunque implementare il tuo ultimo paragrafo in modo iterativo, anche se ricorsivamente sarebbe ovviamente meglio. "Come usarlo, quando usarlo e come evitare un cattivo design sono importanti da sapere, il che richiede di provarlo da soli e capire cosa succede." Pensavo che il punto della domanda fosse quello di ottenere una spiegazione per quel genere di cose.
Hayylem,

@haylem: hai ragione su quella risposta al "Come usarlo, quando usarlo e come evitare un cattivo design .." sarebbe più sul posto di ciò che l'OP chiede (non solo "provalo tu stesso "come ho detto), ma ciò richiederebbe una lunga lezione del tipo di insegnamento più che una risposta rapida a una domanda qui. Tuttavia, hai fatto un ottimo lavoro con la tua risposta . +1 per quello ... Coloro che hanno davvero bisogno di una migliore comprensione del concetto trarranno beneficio dalla lettura della tua risposta.
timore re

che dire di un paio di funzioni che si chiamano a vicenda. A chiama B che chiama di nuovo A fino a quando non viene raggiunta una condizione. questo sarà ancora considerato ricorsione?
santiagozky,

Sì, la funzione achiama ancora se stessa, solo indirettamente (chiamando b).
kindall

1
@santiagozky: come diceva kindall , è ancora una ricorsione. Tuttavia, consiglierei di non farlo. Quando si utilizza la ricorsione, nel codice dovrebbe essere molto chiaro che la ricorsione è in atto. Se chiamare se stesso indirettamente tramite un'altra funzione, rende molto più difficile vedere cosa sta succedendo. Se non sai che una funzione si chiama in modo efficace, puoi entrare nella situazione in cui tu (o qualcun altro che non ha creato questa funzione) rompi alcune condizioni per la ricorsione (mentre cambia la funzionalità nel codice) e finisci in un vicolo cieco con un ciclo infinito.
timore

21

La programmazione ricorsiva è il processo di riduzione progressiva di un problema a versioni di se stesso più facili da risolvere.

Ogni funzione ricorsiva tende a:

  1. prendere un elenco da elaborare, o qualche altra struttura o dominio problematico
  2. gestire l'attuale punto / passo
  3. chiamarsi sul resto / i / sottodominio / i
  4. combinare o utilizzare i risultati del lavoro del sottodominio

Quando il passaggio 2 è precedente al 3 e quando il passaggio 4 è banale (una concatenazione, somma o nulla), ciò consente la ricorsione della coda . Il passaggio 2 spesso deve venire dopo il passaggio 3, poiché i risultati del sottodominio (i) del problema potrebbero essere necessari per completare il passaggio corrente.

Prendi l'attraversamento di un albero binario dritto in avanti. La traversata può essere effettuata in pre-ordine, in ordine o in ordine postale, a seconda di ciò che è richiesto.

   B
A     C

Pre-ordine: BAC

traverse(tree):
    visit the node
    traverse(left)
    traverse(right)

In ordine: ABC

traverse(tree):
    traverse(left)
    visit the node
    traverse(right)

Ordine postale: ACB

traverse(tree):
    traverse(left)
    traverse(right)
    visit the node

Molti problemi ricorsivi sono casi specifici di un'operazione su una mappa , o una piega - la comprensione di queste due operazioni può portare a una comprensione significativa di buoni casi d'uso per la ricorsione.


Il componente chiave della ricorsione pratica è l'idea di utilizzare la soluzione a un problema leggermente più piccolo per risolverne uno più grande. Altrimenti, hai solo una ricorsione infinita.
Barry Brown,

@Barry Brown: Esatto. Da qui la mia affermazione "... ridurre un problema per semplificare la risoluzione delle versioni di se stesso"
Orbling

Non lo direi necessariamente ... Succede spesso, specialmente nei problemi di divisione e conquista o per le situazioni in cui si definisce davvero una relazione di ricorrenza che si riduce a un semplice caso. Ma direi che si tratta più di dimostrare che per ogni iterazione N del tuo problema, c'è un caso calcolabile N + 1.
haylem

1
@Sean McMillan: la ricorsione è uno strumento potente se utilizzato in domini adatti. Troppo spesso lo vedo usato come un modo intelligente per gestire un problema relativamente banale, che offusca enormemente la vera natura del compito da svolgere.
Orbling

20

L'OP ha affermato che la ricorsione non esiste nel mondo reale, ma mi permetto di dissentire.

Prendiamo "l'operazione" nel mondo reale per tagliare una pizza. Hai tolto la pizza dal forno e per servirla devi tagliarla a metà, quindi tagliare quelle metà a metà, quindi tagliare nuovamente quelle metà risultanti a metà.

L'operazione di tagliare la pizza che esegui ancora e ancora fino a quando non ottieni il risultato desiderato (il numero di fette). E per amor di argomenti diciamo che una pizza non tagliata è una fetta in sé.

Ecco un esempio in Ruby:

def cut_pizza (esistenti_slices, desiderata_slices)
  ifisting_slices! = desiderata_slices
    # non abbiamo ancora abbastanza fette per sfamare tutti, quindi
    # stiamo tagliando le fette di pizza, raddoppiando così il loro numero
    new_slices =isting_slices * 2 
    # e questa qui è la chiamata ricorsiva
    cut_pizza (new_slices, wanted_slices)
  altro
    # abbiamo il numero di sezioni desiderato, quindi torniamo
    # qui invece di continuare a ricorrere
    restituisce esistenti_slice
  fine
fine

pizza = 1 # una pizza intera, 'una fetta'
cut_pizza (pizza, 8) # => ne avremo 8

Quindi l'operazione nel mondo reale sta tagliando una pizza, e la ricorsione sta facendo la stessa cosa ancora e ancora fino a quando non hai quello che vuoi.

Le operazioni che scoprirai che è possibile implementare con funzioni ricorsive sono:

  • Calcolo dell'interesse composto per un numero di mesi.
  • Ricerca di un file su un file system (perché i file system sono alberi a causa delle directory).
  • Qualunque cosa implichi il lavoro con gli alberi in generale, immagino.

Consiglio di scrivere un programma per cercare un file in base al nome del file e provare a scrivere una funzione che chiama se stessa fino a quando non viene trovata, la firma sarebbe simile a questa:

find_file_by_name(file_name_we_are_looking_for, path_to_look_in)

Quindi potresti chiamarlo così:

find_file_by_name('httpd.conf', '/etc') # damn it i can never find apache's conf

Secondo me è semplicemente la programmazione della meccanica, un modo per rimuovere abilmente la duplicazione. Puoi riscriverlo usando le variabili, ma questa è una soluzione "più bella". Non c'è nulla di misterioso o difficile al riguardo. Scriverai un paio di funzioni ricorsive, farà clic e huzzah un altro trucco meccanico nella tua casella degli strumenti di programmazione.

Credito extra L' cut_pizzaesempio sopra ti darà un errore di livello troppo profondo se lo chiedi per un numero di sezioni che non è una potenza di 2 (cioè 2 o 4 o 8 o 16). Puoi modificarlo in modo che se qualcuno chiede 10 sezioni non funzionerà per sempre?


16

Ok, cercherò di mantenerlo semplice e conciso.

Le funzioni ricorsive sono funzioni che si definiscono. La funzione ricorsiva consiste di tre cose:

  1. Logica
  2. Una chiamata a se stesso
  3. Quando terminare.

Il modo migliore per scrivere metodi ricorsivi è pensare al metodo che si sta tentando di scrivere come un semplice esempio gestendo solo un ciclo del processo su cui si desidera ripetere l'iterazione, quindi aggiungere la chiamata al metodo stesso e aggiungere quando si desidera terminare. Il modo migliore per imparare è esercitarsi come tutte le cose.

Dal momento che questo è il sito web dei programmatori, mi asterrò dalla scrittura del codice, ma qui è un buon collegamento

se hai preso quella battuta hai capito cosa significa ricorsione.



4
In senso stretto, la ricorsione non necessita di una condizione di risoluzione; Garantire che la ricorsione termina limita i tipi di problemi che la funzione ricorsiva può risolvere e ci sono alcuni tipi di presentazioni semantiche che non richiedono affatto la risoluzione.
Donal Fellows,

6

La ricorsione è uno strumento che un programmatore può usare per invocare una chiamata di funzione su se stessa. La sequenza di Fibonacci è l'esempio da manuale di come viene usata la ricorsione.

La maggior parte del codice ricorsivo, se non tutto, può essere espresso come funzione iterativa, ma di solito è disordinato. Buoni esempi di altri programmi ricorsivi sono Strutture dati come alberi, albero di ricerca binario e persino quicksort.

La ricorsione viene utilizzata per rendere il codice meno sciatto, tenere presente che di solito è più lento e richiede più memoria.


Se è più lento o richiede molta più memoria dipende dall'uso a portata di mano.
Orbling

5
Il calcolo della sequenza di Fibonacci è una cosa terribile da fare ricorsivamente. La traversata degli alberi è un uso molto più naturale della ricorsione. In genere, quando la ricorsione viene utilizzata bene, non è più lenta e non richiede più memoria, poiché è necessario mantenere uno stack proprio anziché lo stack di chiamate.
David Thornley,

1
@Dave: non lo contesterei, ma penso che Fibonacci sia un buon esempio per cominciare.
Bryan Harrington,

5

Mi piace usare questo:

Come vai al negozio?

Se sei all'ingresso del negozio, basta attraversarlo. Altrimenti, fai un passo, poi cammina fino al negozio.

È fondamentale includere tre aspetti:

  • Un banale caso di base
  • Risolvere una piccola parte del problema
  • Risolvere il resto del problema in modo ricorsivo

In realtà usiamo molto la ricorsione nella vita quotidiana; semplicemente non la pensiamo così.


Questo non è ricorsione. Lo sarebbe se lo dividi in due: cammina per metà del negozio, cammina per l'altra metà. Ricorso.

2
Questa è la ricorsione. Non si tratta di dividere e conquistare, ma è solo un tipo di ricorsione. Gli algoritmi grafici (come la ricerca di percorsi) sono pieni di concetti ricorsivi.
deadalnix,

2
Questa è una ricorsione, ma la considero un cattivo esempio, poiché è troppo facile tradurre mentalmente "fai un passo, poi cammina fino al negozio" in un algoritmo iterativo. Sento che questo equivale a convertire un forloop ben scritto in una funzione ricorsiva inutile.
Brian,

3

Il miglior esempio a cui ti farei riferimento è il linguaggio di programmazione C di K & R. In quel libro (e sto citando dalla memoria), la voce nella pagina dell'indice per la ricorsione (da sola) elenca la pagina effettiva in cui parlano di ricorsione e anche la pagina dell'indice.


2

Josh K ha già menzionato le bambole Matroshka . Supponi di voler imparare qualcosa che solo la bambola più corta conosce. Il problema è che non puoi davvero parlarle direttamente, perché inizialmente vive all'interno della bambola più alta che nella prima foto è posizionata alla sua sinistra. Questa struttura va così (una bambola vive all'interno della bambola più alta) fino a finire solo con la più alta.

Quindi l'unica cosa che puoi fare è porre la tua domanda alla bambola più alta. La bambola più alta (che non conosce la risposta) dovrà passare la tua domanda alla bambola più corta (che nella prima foto è alla sua destra). Dal momento che anche lei non ha la risposta, deve chiedere alla prossima bambola più corta. Questo andrà così fino a quando il messaggio non raggiungerà la bambola più corta. La bambola più corta (che è l'unica a conoscere la risposta segreta) passerà la risposta alla prossima bambola più alta (trovata alla sua sinistra), che la passerà alla successiva bambola più alta ... e questo continuerà fino alla risposta raggiunge la sua destinazione finale, che è la bambola più alta e finalmente ... tu :)

Questo è ciò che fa davvero la ricorsione. Una funzione / metodo si chiama fino a ottenere la risposta prevista. Ecco perché quando scrivi il codice ricorsivo è molto importante decidere quando terminare la ricorsione.

Non è la migliore spiegazione, ma si spera che aiuti.


2

Ricorsione n. - Un modello di progettazione dell'algoritmo in cui un'operazione è definita in termini di se stessa.

L'esempio classico è trovare il fattoriale di un numero, n !. 0! = 1, e per ogni altro numero naturale N, il fattoriale di N è il prodotto di tutti i numeri naturali minori o uguali a N. Quindi, 6! = 6 * 5 * 4 * 3 * 2 * 1 = 720. Questa definizione di base ti consentirebbe di creare una soluzione iterativa semplice:

int Fact(int degree)
{
    int result = 1;
    for(int i=degree; i>1; i--)
       result *= i;

    return result;
}

Tuttavia, esaminare nuovamente l'operazione. 6! = 6 * 5 * 4 * 3 * 2 * 1. Con la stessa definizione, 5! = 5 * 4 * 3 * 2 * 1, nel senso che possiamo dire 6! = 6 * (5!). A sua volta, 5! = 5 * (4!) E così via. In questo modo, riduciamo il problema a un'operazione eseguita sul risultato di tutte le operazioni precedenti. Questo alla fine si riduce a un punto, chiamato caso base, in cui il risultato è noto per definizione. Nel nostro caso, 0! = 1 (nella maggior parte dei casi potremmo anche dire che 1! = 1). Nell'informatica, ci è spesso permesso di definire algoritmi in un modo molto simile, facendo in modo che il metodo si chiami e passi un input più piccolo, riducendo così il problema attraverso molte ricorsioni a un caso base:

int Fact(int degree)
{
    if(degree==0) return 1; //the base case; 0! = 1 by definition
    else return degree * Fact(degree -1); //the recursive case; N! = N*(N-1)!
}

Ciò può, in molte lingue, essere ulteriormente semplificato utilizzando l'operatore ternario (a volte visto come una funzione Iif in lingue che non forniscono l'operatore in quanto tale):

int Fact(int degree)
{
    //reads equivalently to the above, but is concise and often optimizable
    return degree==0 ? 1: degree * Fact(degree -1);
}

vantaggi:

  • Espressione naturale - per molti tipi di algoritmi, questo è un modo molto naturale per esprimere la funzione.
  • LOC ridotto: spesso è molto più conciso definire una funzione in modo ricorsivo.
  • Velocità: in alcuni casi, a seconda della lingua e dell'architettura del computer, la ricorsione di un algoritmo è più rapida della soluzione iterativa equivalente, in genere perché effettuare una chiamata di funzione è un'operazione più veloce a livello hardware rispetto alle operazioni e all'accesso alla memoria necessari per eseguire il ciclo iterativo.
  • Divisibilità - Molti algoritmi ricorsivi sono della mentalità "dividi e conquista"; il risultato dell'operazione è una funzione del risultato della stessa operazione eseguita su ciascuna delle due metà dell'ingresso. Ciò consente di dividere il lavoro in due per ogni livello e, se disponibile, è possibile assegnare l'altra metà a un'altra "unità di esecuzione" da elaborare. Questo è generalmente più difficile o impossibile con un algoritmo iterativo.

svantaggi:

  • Richiede comprensione - Devi semplicemente "afferrare" il concetto di ricorsione per capire cosa sta succedendo e quindi scrivere e mantenere efficaci algoritmi ricorsivi. Altrimenti sembra solo magia nera.
  • Dipende dal contesto - Se la ricorsione è una buona idea o meno dipende da quanto elegantemente l'algoritmo possa essere definito in termini di se stesso. Sebbene sia possibile creare, ad esempio, un SelectionSort ricorsivo, l'algoritmo iterativo è in genere il più comprensibile.
  • Accesso alla RAM commerciale per stack di chiamate: in genere, le chiamate di funzione sono più economiche dell'accesso alla cache, il che può rendere la ricorsione più veloce dell'iterazione. Ma, di solito c'è un limite alla profondità dello stack di chiamate che può causare errori di ricorsione in cui funzionerà un algoritmo iterativo.
  • Ricorsione infinita - Devi sapere quando fermarti. È anche possibile l'iterazione infinita, ma i costrutti di loop coinvolti sono generalmente più facili da capire e quindi da debug.

1

L'esempio che uso è un problema che ho affrontato nella vita reale. Hai un container (come uno zaino grande che intendi portare in viaggio) e vuoi conoscere il peso totale. Hai nel contenitore due o tre oggetti sciolti e alcuni altri contenitori (diciamo, sacchi di roba). Il peso del contenitore totale è ovviamente il peso del contenitore vuoto più il peso di tutto ciò che contiene. Per gli articoli sfusi, puoi semplicemente pesarli, e per i sacchi di roba potresti semplicemente pesarli o potresti dire "bene il peso di ogni sacco è il peso del contenitore vuoto più il peso di tutto ciò che contiene". E poi continui ad andare in contenitori in contenitori e così via fino ad arrivare a un punto in cui ci sono solo oggetti sciolti in un contenitore. Questa è la ricorsione.

Potresti pensare che non accada mai nella vita reale, ma immagina di provare a contare o sommare gli stipendi di persone in una particolare azienda o divisione, che ha una miscela di persone che lavorano solo per l'azienda, persone in divisioni, quindi in le divisioni ci sono dipartimenti e così via. O le vendite in un paese che ha regioni, alcune delle quali hanno sottoregioni, ecc. Ecc. Questo tipo di problemi si verificano continuamente negli affari.


0

La ricorsione può essere utilizzata per risolvere molti problemi di conteggio. Ad esempio, supponi di avere un gruppo di n persone a una festa (n> 1) e che tutti stringano la mano di tutti gli altri esattamente una volta. Quante strette di mano avvengono? Potresti sapere che la soluzione è C (n, 2) = n (n-1) / 2, ma puoi risolvere ricorsivamente come segue:

Supponiamo che ci siano solo due persone. Quindi (per ispezione) la risposta è ovviamente 1.

Supponiamo di avere tre persone. Individua una persona e nota che si stringe la mano con altre due persone. Dopo di che devi contare solo le strette di mano tra le altre due persone. L'abbiamo già fatto proprio ora ed è 1. Quindi la risposta è 2 + 1 = 3.

Supponiamo di avere n persone. Seguendo la stessa logica di prima, è (n-1) + (numero di strette di mano tra n-1 persone). Espandendo, otteniamo (n-1) + (n-2) + ... + 1.

Espresso come funzione ricorsiva,

f (2) = 1
f (n) = n-1 + f (n-1), n> 2


0

Nella vita (al contrario di un programma per computer) la ricorsione si verifica raramente sotto il nostro controllo diretto, perché può essere fonte di confusione. Inoltre, la percezione tende a riguardare gli effetti collaterali, piuttosto che essere funzionalmente pura, quindi se si verifica una ricorsione non si può notare.

Tuttavia, la ricorsione avviene qui nel mondo. Un sacco.

Un buon esempio è (una versione semplificata di) il ciclo dell'acqua:

  • Il sole riscalda il lago
  • L'acqua sale verso il cielo e forma le nuvole
  • Le nuvole si spostano su una montagna
  • Sulla montagna l'aria diventa troppo fredda per trattenere la loro umidità
  • Cade la pioggia
  • Si forma un fiume
  • L'acqua nel fiume scorre nel lago

Questo è un ciclo che fa succedere di nuovo se stesso. È ricorsivo.

Un altro posto in cui puoi ricorrere è l'inglese (e la lingua umana in generale). All'inizio potresti non riconoscerlo, ma il modo in cui possiamo generare una frase è ricorsivo, perché le regole ci consentono di incorporare un'istanza di un simbolo in un'altra istanza dello stesso simbolo.

Da The Language Instinct di Steven Pinker:

se o la ragazza mangia un gelato o la ragazza mangia caramelle, allora il ragazzo mangia hot dog

Questa è un'intera frase che contiene altre frasi intere:

la ragazza mangia il gelato

la ragazza mangia caramelle

il ragazzo mangia hot dog

L'atto di comprendere l'intera frase implica la comprensione di frasi più piccole, che usano la stessa serie di trucchi mentali per essere comprese come la frase completa.

Per comprendere la ricorsione dal punto di vista della programmazione è più semplice esaminare un problema che può essere risolto con la ricorsione e capire perché dovrebbe essere e cosa significa che devi fare.

Per l'esempio userò la più grande funzione divisore comune, o gcd in breve.

Hai i tuoi due numeri ae b. Per trovare il loro gcd (supponendo che nessuno dei due sia 0) è necessario verificare se aè uniformemente divisibile in b. Se è allora bè il gcd, altrimenti è necessario controllare il gcd di be il resto di a/b.

Dovresti già essere in grado di vedere che questa è una funzione ricorsiva, poiché hai la funzione gcd che chiama la funzione gcd. Solo per martellarlo a casa, eccolo in c # (di nuovo, supponendo che 0 non venga mai passato come parametro):

int gcd(int a, int b)
{   
    if (a % b == 0) //this is a stopping condition
    {
        return b;
    }

    return (gcd(b, a % b)); //the call to gcd here makes this function recursive
}

In un programma, è importante avere una condizione di arresto, altrimenti la funzione si ripeterà per sempre, il che alla fine causerà un overflow dello stack!

La ragione per usare la ricorsione qui, piuttosto che un ciclo while o qualche altro costrutto iterativo, è che mentre leggi il codice ti dice cosa sta facendo e cosa accadrà dopo, quindi è più facile capire se funziona correttamente .


1
Ho trovato l'esempio del ciclo dell'acqua iterativo. Il secondo esempio di lingua sembra dividere e conquistare più della ricorsione.
Gulshan,

@Gulshan: Direi che il ciclo dell'acqua è ricorsivo perché si ripete, come una funzione ricorsiva. Non è come dipingere una stanza, dove esegui la stessa serie di passaggi su più oggetti (pareti, soffitto, ecc.), Come in un ciclo for. L'esempio del linguaggio usa divide e conquistare, ma anche la "funzione" che si chiama si auto-lavora per le frasi nidificate, quindi è ricorsiva in quel modo.
Matt Ellen,

Nel ciclo dell'acqua, il ciclo viene avviato dal sole e nessun altro elemento del ciclo sta facendo riavviare il sole. Quindi, dov'è la chiamata ricorsiva?
Gulshan,

Non c'è una chiamata ricorsiva! Non è una funzione. : D È ricorsivo perché provoca il ripetersi di se stesso. L'acqua del lago ritorna al lago e il ciclo ricomincia. Se qualche altro sistema stesse immettendo acqua nel lago, sarebbe iterativo.
Matt Ellen,

1
Il ciclo dell'acqua è un ciclo while. Certo, un ciclo while può essere espresso usando la ricorsione, ma farlo farà saltare la pila. Per favore, no.
Brian,

0

Ecco un esempio del mondo reale per la ricorsione.

Fagli immaginare che abbiano una collezione a fumetti e mescolerai tutto in un grande mucchio. Attenzione: se hanno davvero una collezione, potrebbero ucciderti all'istante quando menzioni semplicemente l'idea di farlo.

Ora lascia che ordinino questa grande pila di fumetti non ordinati con l'aiuto di questo manuale:

Manual: How to sort a pile of comics

Check the pile if it is already sorted. If it is, then done.

As long as there are comics in the pile, put each one on another pile, 
ordered from left to right in ascending order:

    If your current pile contains different comics, pile them by comic.
    If not and your current pile contains different years, pile them by year.
    If not and your current pile contains different tenth digits, pile them 
    by this digit: Issue 1 to 9, 10 to 19, and so on.
    If not then "pile" them by issue number.

Refer to the "Manual: How to sort a pile of comics" to separately sort each
of the new piles.

Collect the piles back to a big pile from left to right.

Done.

La cosa bella qui è: quando si tratta di singoli problemi, hanno l'intero "stack frame" con le pile locali visibili davanti a loro sul terreno. Fornisci loro più stampe del manuale e mettine da parte ogni livello di pila con un segno in cui ti trovi attualmente a questo livello (cioè lo stato delle variabili locali), in modo da poter continuare lì su ogni Fine.

Questo è fondamentalmente la ricorsione: eseguire lo stesso processo, solo a un livello di dettaglio più preciso, più ci si addentra.


-1
  • Termina se viene raggiunta la condizione di uscita
  • fare qualcosa per alterare lo stato delle cose
  • fare tutto da capo iniziando con lo stato attuale delle cose

La ricorsione è un modo molto conciso per esprimere qualcosa che deve essere ripetuto fino a quando non viene raggiunto qualcosa.



-2

Una bella spiegazione della ricorsione è letteralmente "un'azione che si ripete da dentro di sé".

Considera un pittore che dipinge un muro, è ricorsivo perché l'azione è "dipingi una striscia dal soffitto al pavimento piuttosto che spostarti un po 'a destra e (dipingi una striscia dal soffitto al pavimento che passa un po' a destra e (dipingi un striscia dal soffitto al pavimento piuttosto che spostati leggermente verso destra e (ecc.))) ".

La sua funzione paint () si chiama ripetutamente per creare la sua più grande funzione paint_wall ().

Spero che questo povero pittore abbia una sorta di condizione di arresto :)


6
Per me, l'esempio sembra più una procedura iterativa, non ricorsiva.
Gulshan,

@Gulshan: ricorsione e iterazione fanno cose simili, e spesso le cose funzioneranno bene con entrambi. I linguaggi funzionali in genere usano la ricorsione invece dell'iterazione. Esistono esempi migliori di ricorsione in cui sarebbe imbarazzante scrivere la stessa cosa in modo iterativo.
David Thornley,
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.