Capire come funzionano le funzioni ricorsive


115

Come spiega il titolo, ho una domanda di programmazione molto fondamentale che non sono ancora riuscito a risolvere. Filtrare tutte le (estremamente intelligenti) "Per capire la ricorsione, devi prima capire la ricorsione." risposte da vari thread in linea ancora non riesco a capirlo.

Comprendendo che di fronte al non sapere ciò che non sappiamo, possiamo tendere a fare le domande sbagliate o fare le domande giuste in modo errato Condividerò ciò che "penso" la mia domanda è nella speranza che qualcuno con una prospettiva simile possa condividerne alcuni un po 'di conoscenza che mi aiuterà ad accendere la lampadina ricorsiva!

Ecco la funzione (la sintassi è scritta in Swift):

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

Useremo 2 e 5 come argomenti:

println(sumInts(a: 2, b: 5))

Ovviamente la risposta è 14. Ma non mi è chiaro come si ottiene questo valore.

Questi sono i miei 2 problemi:

  1. La funzione viene chiamata in modo ricorsivo finché non viene soddisfatta una condizione. Questa condizione è a> b. Quando questa condizione è soddisfatta, restituire 0. A prima vista, mi aspetto che il valore restituito sia 0, il che è ovviamente errato.

  2. Stampando il valore di 'a' su ogni iterazione si ottiene un valore che mi sarei aspettato: 2, 3, 4, 5 (a quel punto 5 + 1> b che soddisfa la prima condizione: a> b) ma continuo a non farlo ' Per vedere come si ottiene il valore di 14.

Il mio primo pensiero è che qualcosa di simile al seguente stia accadendo magicamente:

var answer = a;
answer += a+1 until a > b;
return answer;   

Quindi escludendo la magia, non sto ottenendo qualcosa. Mi piacerebbe capire cosa sta succedendo più che solo implicitamente.

Se qualcuno potesse gentilmente spiegare cosa succede tecnicamente durante questo tipo di funzione e perché il risultato non è 0 e come, alla fine, a + sumInts(a: a + 1, b: b) = 14sarei per sempre in debito con te.


7
La ricorsione è uno di quei concetti di programmazione che sono molto più facili da capire in termini matematici che in codice; c'è una buona definizione qui
blgt

5
LearnYouARecursion, set di problemi completi da un professore di livello mondiale!
recursion.ninja

15
Ho solo ho esortarvi a digitare "ricorsione" nella casella di ricerca di Google. Una di quelle uova di Pasqua. Non ti rovinerò la sorpresa.
Floris


Risposte:


107

Penso che la confusione derivi dal pensare che "la stessa funzione" venga chiamata molte volte. Se lo consideri come "molte copie della stessa funzione chiamate", potrebbe essere più chiaro:

Solo una copia della funzione restituisce sempre 0 e non è la prima (è l'ultima). Quindi il risultato della chiamata del primo non è 0.

Per la seconda parte di confusione, penso che sarà più facile precisare la ricorsione in inglese. Leggi questa riga:

return a + sumInts(a + 1, b: b)

come "restituisce il valore di 'a' più (il valore di ritorno di un'altra copia della funzione, che è il valore di 'a' più della copia (il valore di ritorno di un'altra copia della funzione, che è il valore della seconda copia di ' a 'plus (... ", con ogni copia della funzione che genera una nuova copia di se stessa con a aumentato di 1, finché la condizione a> b non viene soddisfatta.

Quando raggiungi la condizione a> b che è vera, hai una lunga pila (potenzialmente arbitraria) di copie della funzione tutte nel mezzo dell'esecuzione, tutte in attesa del risultato della copia successiva per scoprire cosa stanno dovrebbe aggiungere a "a".

(modifica: anche, qualcosa di cui essere consapevoli è che lo stack di copie della funzione che ho citato è una cosa reale che occupa la memoria reale e andrà in crash il tuo programma se diventa troppo grande. Il compilatore può ottimizzarlo in alcuni casi, ma l'esaurimento dello spazio dello stack è una limitazione significativa e sfortunata delle funzioni ricorsive in molte lingue)


7
Catfish_Man: Penso che tu l'abbia inchiodato! Pensarlo come diverse "copie" della stessa funzione ha perfettamente senso. Ci sto ancora girando la testa, ma penso che tu mi abbia mandato sulla strada giusta! Grazie per aver dedicato del tempo alla tua giornata impegnativa per aiutare un collega programmatore! Contrassegnerò la tua risposta come risposta corretta. Vi auguro una buona giornata!
Jason Elwood,

13
Questa è una buona analogia, anche se fai attenzione a non prenderla troppo alla lettera poiché ogni "copia" è in realtà lo stesso identico codice. Ciò che è diverso per ogni copia sono tutti i dati su cui sta lavorando.
Tim B

2
Non sono molto contento di pensarlo come una copia. Trovo che una spiegazione più intuitiva sia differenziare la funzione stessa (il codice, cosa fa) e una chiamata di funzione (istanziazione di quella funzione) a cui è associato uno stack frame / contesto di esecuzione. La funzione non possiede le sue variabili locali, vengono istanziate quando la funzione viene chiamata (invocata). Ma immagino che questo servirà come introduzione alla ricorsione
Thomas il

5
La terminologia corretta è che ci sono diverse invocazioni della funzione. Ogni invocazione ha le proprie istanze di variabili ae b.
Theodore Norvell,

6
Sì, c'è una notevole quantità di precisione che potrebbe essere aggiunta a questa risposta. Ho volutamente tralasciato la distinzione tra "istanze di una funzione" e "registrazioni di attivazione di invocazioni di una singola funzione", perché si trattava di un carico concettuale extra che non aiuta veramente a comprendere il problema. Aiuta a comprendere altri problemi, quindi è ancora informazioni utili, solo altrove. Questi commenti sembrano un bel posto per questo :)
Catfish_Man

130

1.La funzione viene chiamata ricorsivamente fino a quando non viene soddisfatta una condizione. Questa condizione è a > b. Quando questa condizione è soddisfatta, restituire 0. A prima vista, mi aspetto che il valore restituito sia 0, il che è ovviamente errato.

Ecco cosa sumInts(2,5)penserebbe il computer se fosse in grado di:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

Come vedi, alcune chiamate alla funzione sumIntsrestituiscono effettivamente 0, ma questo non è il valore finale perché il computer deve ancora aggiungere 5 a quello 0, quindi 4 al risultato, quindi 3, quindi 2, come descritto dalle ultime quattro frasi di i pensieri del nostro computer. Si noti che nella ricorsione, il computer non deve solo calcolare la chiamata ricorsiva, ma deve anche ricordare cosa fare con il valore restituito dalla chiamata ricorsiva. C'è un'area speciale della memoria del computer chiamata stack dove viene salvato questo tipo di informazioni, questo spazio è limitato e le funzioni troppo ricorsive possono esaurire lo stack: questo è lo stack overflow dà il nome al nostro sito web più amato.

La tua affermazione sembra presupporre implicitamente che il computer dimentichi a che cosa si trovava quando effettua una chiamata ricorsiva, ma non è così, ecco perché la tua conclusione non corrisponde alla tua osservazione.

2.Stampare il valore di 'a' su ogni iterazione restituisce un valore che mi aspetterei: 2, 3, 4, 5 (a quel punto 5 + 1> b che soddisfa la prima condizione: a> b) ma io non vedo come si ottiene il valore di 14.

Questo perché il valore restituito non è un asé stesso, ma la somma del valore ae del valore restituito dalla chiamata ricorsiva.


3
Grazie per aver dedicato del tempo a scrivere questa ottima risposta Michael! +1!
Jason Elwood,

9
@JasonElwood Forse è utile se modifichi in sumIntsmodo che annoti effettivamente i "pensieri del computer". Una volta che hai scritto una mano di tali funzioni, probabilmente avrai "capito"!
Michael Le Barbier Grünewald,

4
Questa è una buona risposta, anche se noto che non è necessario che l'attivazione della funzione avvenga su una struttura dati chiamata "stack". La ricorsione potrebbe essere implementata dallo stile di passaggio di continuazione, nel qual caso non c'è affatto stack. Lo stack è solo uno - particolarmente efficiente, e quindi di uso comune - reificazione della nozione di continuazione.
Eric Lippert

1
@EricLippert Sebbene le tecniche utilizzate per implementare la ricorsività siano un argomento interessante di per sé , non sono sicuro che sarebbe utile per l'OP - che vuole capire "come funziona" - essere esposto alla varietà di meccanismi utilizzati. Mentre la continuazione passando stile o linguaggi basati espansione (ad esempio TeX e m4) non sono intrinsecamente più difficile di paradigmi di programmazione più comuni, non voglio reato chiunque, etichettando queste “esotici” e un po ' bugia come “succede sempre la pila” dovrebbe aiutare l'OP a comprendere il concetto. (E una specie di stack è sempre coinvolto.)
Michael Le Barbier Grünewald

1
Deve esserci un modo per il software di ricordare cosa stava facendo, chiamare la funzione in modo ricorsivo e poi tornare allo stato originale quando ritorna. Questo meccanismo agisce come uno stack, quindi è conveniente chiamarlo stack, anche se viene utilizzata un'altra struttura dati.
Barmar

48

Per comprendere la ricorsione devi pensare al problema in modo diverso. Invece di una grande sequenza logica di passaggi che ha senso nel suo insieme, invece prendi un problema grande e suddividilo in problemi più piccoli e risolvi quelli, una volta che hai una risposta per i problemi secondari, combini i risultati dei problemi secondari per creare il soluzione al problema più grande. Pensa a te e ai tuoi amici che dovete contare il numero di biglie in un enorme secchio. Prendi ognuno un secchio più piccolo e vai a contare quelli individualmente e quando hai finito aggiungi i totali insieme .. Bene, ora se ognuno di voi trova un amico e dividi ulteriormente i secchi, allora devi solo aspettare che questi altri amici lo facciano. calcolare i loro totali, riportarli a ciascuno di voi, sommarli. E così via.

È necessario ricordare che ogni volta che la funzione chiama se stessa in modo ricorsivo, crea un nuovo contesto con un sottoinsieme del problema, una volta risolta quella parte viene restituita in modo che l'iterazione precedente possa essere completata.

Lascia che ti mostri i passaggi:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

una volta che sumInts (a: 6, b: 5) è stato eseguito, i risultati possono essere calcolati in modo da risalire la catena con i risultati che si ottengono:

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

Un altro modo per rappresentare la struttura della ricorsione:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 

2
Molto bene, Rob. L'hai messo in un modo molto chiaro e facile da capire. Grazie per aver dedicato del tempo!
Jason Elwood

3
Questa è la rappresentazione più chiara di ciò che sta accadendo, senza entrare nella teoria e nei dettagli tecnici, mostra chiaramente ogni fase dell'esecuzione.
Bryan

2
Mi fa piacere. :) non è sempre facile spiegare queste cose. Grazie per il complimento.
Rob il

1
+1. Ecco come lo descriverei, in particolare con il tuo ultimo esempio della struttura. È utile srotolare visivamente ciò che sta accadendo.
KChaloux

40

La ricorsione è un argomento difficile da capire e non penso di poterle rendere pienamente giustizia qui. Invece, cercherò di concentrarmi sul particolare pezzo di codice che hai qui e cercherò di descrivere sia l'intuizione del perché la soluzione funziona sia la meccanica di come il codice calcola il suo risultato.

Il codice che hai fornito qui risolve il seguente problema: vuoi conoscere la somma di tutti gli interi da a a b, inclusi. Per il tuo esempio, vuoi la somma dei numeri da 2 a 5, inclusi, che è

2 + 3 + 4 + 5

Quando si cerca di risolvere un problema in modo ricorsivo, uno dei primi passi dovrebbe essere capire come scomporre il problema in un problema più piccolo con la stessa struttura. Supponiamo quindi di voler sommare i numeri da 2 a 5 inclusi. Un modo per semplificare ciò è notare che la somma di cui sopra può essere riscritta come

2 + (3 + 4 + 5)

Qui, (3 + 4 + 5) sembra essere la somma di tutti gli interi compresi tra 3 e 5, inclusi. In altre parole, se vuoi conoscere la somma di tutti gli interi compresi tra 2 e 5, inizia calcolando la somma di tutti gli interi compresi tra 3 e 5, quindi aggiungi 2.

Quindi come si calcola la somma di tutti gli interi compresi tra 3 e 5, inclusi? Ebbene, quella somma lo è

3 + 4 + 5

che può essere pensato invece come

3 + (4 + 5)

Qui, (4 + 5) è la somma di tutti gli interi compresi tra 4 e 5, inclusi. Quindi, se volessi calcolare la somma di tutti i numeri tra 3 e 5, inclusi, dovresti calcolare la somma di tutti i numeri interi tra 4 e 5, quindi aggiungere 3.

C'è uno schema qui! Se vuoi calcolare la somma degli interi tra a e b, inclusi, puoi fare quanto segue. Innanzitutto, calcola la somma degli interi compresi tra a + 1 e b, inclusi. Successivamente, aggiungi a a quel totale. Noterai che "calcolare la somma degli interi tra a + 1 eb, inclusi" sembra essere più o meno lo stesso tipo di problema che stiamo già cercando di risolvere, ma con parametri leggermente diversi. Piuttosto che calcolare da a a b, inclusi, stiamo calcolando da a + 1 a b, inclusi. Questo è il passaggio ricorsivo: per risolvere il problema più grande ("somma da a a b, inclusiva"), riduciamo il problema a una versione più piccola di se stesso ("somma da a + 1 a b, inclusiva").

Se dai un'occhiata al codice che hai sopra, noterai che c'è questo passaggio:

return a + sumInts(a + 1, b: b)

Questo codice è semplicemente una traduzione della logica di cui sopra: se vuoi sommare da a a b, inclusi, inizia sommando a + 1 in b, inclusi (questa è la chiamata ricorsiva a sumInts), quindi aggiungi a.

Ovviamente, da solo questo approccio non funzionerà. Ad esempio, come calcolare la somma di tutti i numeri interi compresi tra 5 e 5 inclusi? Bene, usando la nostra logica corrente, dovresti calcolare la somma di tutti gli interi compresi tra 6 e 5, inclusi, quindi aggiungere 5. Quindi come si calcola la somma di tutti gli interi tra 6 e 5, inclusi? Bene, usando la nostra logica attuale, dovresti calcolare la somma di tutti i numeri interi tra 7 e 5, inclusi, quindi aggiungere 6. Noterai un problema qui - questo continua ad andare avanti!

Nel problem solving ricorsivo, ci deve essere un modo per smettere di semplificare il problema e invece andare a risolverlo direttamente. In genere, si trova un caso semplice in cui la risposta può essere determinata immediatamente, quindi si struttura la soluzione per risolvere direttamente casi semplici quando si presentano. Questo è in genere chiamato caso base o base ricorsiva .

Allora qual è il caso di base in questo particolare problema? Quando si sommano numeri interi da a a b, inclusi, se a sembra essere più grande di b, la risposta è 0 - non ci sono numeri nell'intervallo! Pertanto, struttureremo la nostra soluzione come segue:

  1. Se a> b, la risposta è 0.
  2. Altrimenti (a ≤ b), ottieni la risposta come segue:
    1. Calcola la somma degli interi compresi tra a + 1 e b.
    2. Aggiungi una per ottenere la risposta.

Ora confronta questo pseudocodice con il tuo codice effettivo:

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b: b)
    }
}

Si noti che c'è quasi esattamente una mappa uno-a-uno tra la soluzione delineata in pseudocodice e questo codice effettivo. Il primo passo è il caso base: nel caso in cui chiedi la somma di un intervallo di numeri vuoto, ottieni 0. Altrimenti, calcola la somma tra a + 1 eb, quindi aggiungi a.

Finora ho fornito solo un'idea di alto livello dietro il codice. Ma avevi altre due ottime domande. Primo, perché non restituisce sempre 0, dato che la funzione dice di restituire 0 se a> b? Secondo, da dove viene effettivamente il 14? Diamo un'occhiata a questi a turno.

Proviamo un caso molto, molto semplice. Cosa succede se chiami sumInts(6, 5)? In questo caso, tracciando il codice, vedrai che la funzione restituisce solo 0. Questa è la cosa giusta da fare, per - non ci sono numeri nell'intervallo. Ora prova qualcosa di più difficile. Cosa succede quando chiami sumInts(5, 5)? Bene, ecco cosa succede:

  1. Tu chiami sumInts(5, 5). elseCadiamo nel ramo, che restituisce il valore di `a + sumInts (6, 5).
  2. Per sumInts(5, 5)determinare cosa sumInts(6, 5)sia, dobbiamo mettere in pausa ciò che stiamo facendo e fare una chiamata a sumInts(6, 5).
  3. sumInts(6, 5)viene chiamato. Entra in iffiliale e ritorna 0. Tuttavia, questa istanza di è sumIntsstata chiamata da sumInts(5, 5), quindi il valore restituito viene comunicato a sumInts(5, 5), non al chiamante di primo livello.
  4. sumInts(5, 5)ora può calcolare 5 + sumInts(6, 5)per tornare indietro 5. Quindi lo restituisce al chiamante di primo livello.

Nota come è stato formato il valore 5 qui. Abbiamo iniziato con una chiamata attiva a sumInts. Ciò ha attivato un'altra chiamata ricorsiva e il valore restituito da quella chiamata ha comunicato le informazioni a sumInts(5, 5). La chiamata a sumInts(5, 5)quindi a sua volta ha eseguito dei calcoli e ha restituito un valore al chiamante.

Se lo provi sumInts(4, 5), ecco cosa succederà:

  • sumInts(4, 5)cerca di tornare 4 + sumInts(5, 5). Per farlo, chiama sumInts(5, 5).
    • sumInts(5, 5)cerca di tornare 5 + sumInts(6, 5). Per farlo, chiama sumInts(6, 5).
    • sumInts(6, 5)restituisce 0 a sumInts(5, 5).</li> <li>sumInts (5, 5) now has a value forsumInts (6, 5) , namely 0. It then returns5 + 0 = 5`.
  • sumInts(4, 5)ora ha un valore per sumInts(5, 5), cioè 5. Quindi ritorna 4 + 5 = 9.

In altre parole, il valore restituito è formato sommando i valori uno alla volta, ogni volta prendendo un valore restituito da una particolare chiamata ricorsiva a sumIntse aggiungendo il valore corrente di a. Quando la ricorsione tocca il fondo, la chiamata più profonda restituisce 0. Tuttavia, quel valore non esce immediatamente dalla catena di chiamate ricorsive; invece, restituisce semplicemente il valore alla chiamata ricorsiva uno strato sopra di esso. In questo modo, ogni chiamata ricorsiva aggiunge semplicemente un numero in più e lo restituisce più in alto nella catena, culminando con la somma complessiva. Come esercizio, prova a tracciarlosumInts(2, 5) , che è ciò con cui volevi iniziare.

Spero che questo ti aiuti!


3
Grazie per aver dedicato del tempo alla tua giornata impegnativa per condividere una risposta così esauriente! Ci sono un sacco di ottime informazioni qui che mi stanno aiutando a capire le funzioni ricorsive e sicuramente aiuteranno gli altri che si imbatteranno in questo post in futuro. grazie ancora e buona giornata!
Jason Elwood,

22

Finora hai alcune buone risposte, ma ne aggiungerò un'altra che prende una strada diversa.

Prima di tutto, ho scritto molti articoli su semplici algoritmi ricorsivi che potresti trovare interessanti; vedere

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

Questi sono nell'ordine più recente, quindi inizia dal basso.

In secondo luogo, finora tutte le risposte hanno descritto la semantica ricorsiva considerando l' attivazione della funzione . Ogni chiamata effettua una nuova attivazione e la chiamata ricorsiva viene eseguita nel contesto di questa attivazione. Questo è un buon modo per pensarci, ma c'è un altro modo equivalente: ricerca e sostituzione del testo intelligente .

Lasciami riscrivere la tua funzione in una forma leggermente più compatta; non pensare che sia in una lingua particolare.

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

Spero che abbia un senso. Se non hai familiarità con l'operatore condizionale, è della forma condition ? consequence : alternativee il suo significato diventerà chiaro.

Ora vogliamo valutare. s(2,5) Lo facciamo sostituendo testualmente la chiamata con il corpo della funzione, quindi sostituiamo acon 2e bcon 5:

s(2, 5) 
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

Ora valuta il condizionale. Sostituiamo testualmente 2 > 5con false.

---> false ? 0 : 2 + s(2 + 1, 5)

Ora sostituisci testualmente tutti i falsi condizionali con l'alternativa e tutti i veri condizionali con la conseguenza. Abbiamo solo false condizioni, quindi sostituiamo testualmente quell'espressione con l'alternativa:

---> 2 + s(2 + 1, 5)

Ora, per evitare di dover digitare tutti quei +segni, sostituisci testualmente l'aritmetica costante con il suo valore. (Questo è un po 'un trucco, ma non voglio dover tenere traccia di tutte le parentesi!)

---> 2 + s(3, 5)

Ora cerca e sostituisci, questa volta con il corpo per la chiamata, 3per ae 5per b. Metteremo la sostituzione per la chiamata tra parentesi:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

E ora continuiamo a fare gli stessi passaggi di sostituzione testuale:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))  
---> 2 + (3 + s(3 + 1, 5))                
---> 2 + (3 + s(4, 5))                     
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

Tutto ciò che abbiamo fatto qui è stata solo una semplice sostituzione testuale . In realtà non avrei dovuto sostituire "3" con "2 + 1" e così via fino a quando non ho dovuto farlo, ma pedagogicamente sarebbe diventato difficile da leggere.

L'attivazione della funzione non è altro che la sostituzione della chiamata di funzione con il corpo della chiamata e la sostituzione dei parametri formali con gli argomenti corrispondenti. Devi stare attento a introdurre le parentesi in modo intelligente, ma a parte questo, è solo la sostituzione del testo.

Ovviamente, la maggior parte delle lingue non implementa effettivamente l' attivazione come sostituzione del testo, ma logicamente è quello che è.

Allora cos'è una ricorsione illimitata? Una ricorsione dove la sostituzione testuale non si ferma! Notate come alla fine siamo arrivati ​​a un passaggio in cui non c'era più niente sda sostituire, e quindi abbiamo potuto semplicemente applicare le regole per l'aritmetica.


Buon esempio, ma ti spezza il cuore quando procedi a fare calcoli più complessi. Per esempio. Trovare l'antenato comune in un albero binario.
CodeYogi

11

Il modo in cui di solito capisco come funziona una funzione ricorsiva è guardando il caso base e lavorando all'indietro. Ecco quella tecnica applicata a questa funzione.

Prima il caso di base:

sumInts(6, 5) = 0

Quindi la chiamata appena sopra quella nello stack di chiamate :

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

Quindi la chiamata appena sopra quella nello stack di chiamate:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

E così via:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

E così via:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

Si noti che siamo arrivati ​​alla nostra chiamata originale alla funzione sumInts(2, 5) == 14

L'ordine in cui vengono eseguite queste chiamate:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

L'ordine in cui vengono restituite queste chiamate:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

Si noti che siamo giunti a una conclusione su come opera la funzione tracciando le chiamate nell'ordine in cui vengono restituite .


5

Ci provo.

Eseguendo l'equazione a + sumInts (a + 1, b), mostrerò come la risposta finale sia 14.

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

Facci sapere se hai ulteriori domande.

Ecco un altro esempio di funzioni ricorsive nel seguente esempio.

Un uomo si è appena laureato.

t è la quantità di tempo in anni.

Il numero totale effettivo di anni lavorati prima del pensionamento può essere calcolato come segue:

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

E questo dovrebbe essere appena sufficiente per deprimere chiunque, lol. ;-P


5

Ricorsione. In Computer Science la ricorsione è trattata in modo approfondito sotto il tema degli automi finiti.

Nella sua forma più semplice è un riferimento a sé. Ad esempio, dire che "la mia macchina è un'auto" è un'affermazione ricorsiva. Il problema è che l'affermazione è una ricorsione infinita in quanto non finirà mai. La definizione nella dichiarazione di "macchina" è che si tratta di una "macchina", quindi può essere sostituita. Tuttavia, non c'è fine perché in caso di sostituzione diventa ancora "la mia macchina è un'auto".

Questo potrebbe essere diverso se l'affermazione fosse "la mia macchina è una Bentley. La mia macchina è blu". In tal caso la sostituzione nella seconda situazione per l'auto potrebbe essere "bentley" risultando in "la mia bentley è blu". Questi tipi di sostituzioni vengono spiegati matematicamente in Informatica attraverso grammatiche libere dal contesto .

La sostituzione effettiva è una regola di produzione. Dato che l'affermazione è rappresentata da S e che car è una variabile che può essere un "bentley", questa affermazione può essere ricostruita ricorsivamente.

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

Questo può essere costruito in più modi, poiché ciascuno |significa che c'è una scelta. Spuò essere sostituito da una qualsiasi di queste scelte e S inizia sempre vuoto. I εmezzi per terminare la produzione. Proprio come Spossono essere sostituite, possono essere sostituite anche altre variabili (ce n'è solo una ed èC che rappresenterebbe "bentley").

Quindi iniziare con l' Sessere vuoto e sostituirlo con la prima scelta "my"S Sdiventa

"my"S

Spuò ancora essere sostituito in quanto rappresenta una variabile. Potremmo scegliere di nuovo "my" o ε per terminarlo, ma continuiamo a fare la nostra dichiarazione originale. Scegliamo lo spazio che significa Sè sostituito con" "S

"my "S

Quindi scegliamo C

"my "CS

E C ha solo una scelta per la sostituzione

"my bentley"S

E di nuovo lo spazio per S

"my bentley "S

E così via "my bentley is"S, "my bentley is "S, "my bentley is blue"S,"my bentley is blue" (in sostituzione di S per ε termina la produzione) e abbiamo costruito il nostro modo ricorsivo affermazione "la mia Bentley è blu".

Pensa alla ricorsione come a queste produzioni e sostituzioni. Ogni fase del processo sostituisce il suo predecessore per produrre il risultato finale. Nell'esempio esatto della somma ricorsiva da 2 a 5, si finisce con la produzione

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

Questo diventa

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14

Non sono sicuro che gli automi a stati finiti o le grammatiche prive di contesto siano i migliori esempi che possono aiutare a costruire una prima intuizione sulla ricorsione. Sono ottimi esempi, ma forse un po 'poco familiari ai programmatori che non hanno precedenti esperienze CS.
chi

4

Penso che il modo migliore per comprendere le funzioni ricorsive sia rendersi conto che sono fatte per elaborare strutture di dati ricorsive. Ma nella tua funzione originale sumInts(a: Int, b: Int)che calcola ricorsivamente la somma dei numeri da aa b, sembra non essere una struttura dati ricorsiva ... Proviamo una versione leggermente modificata in sumInts(a: Int, n: Int)cui nsono quanti numeri aggiungerai.

Ora, sumInts è ricorsivo su n, un numero naturale. Non è ancora un dato ricorsivo, giusto? Bene, un numero naturale potrebbe essere considerato una struttura di dati ricorsiva usando gli assiomi di Peano:

enum Natural = {
    case Zero
    case Successor(Natural)
}

Quindi, 0 = Zero, 1 = Succesor (Zero), 2 = Succesor (Succesor (Zero)) e così via.

Una volta che hai una struttura dati ricorsiva, hai il modello per la funzione. Per ogni caso non ricorsivo, puoi calcolare direttamente il valore. Per i casi ricorsivi si assume che la funzione ricorsiva stia già funzionando e la si usa per calcolare il caso, ma decostruendo l'argomento. Nel caso di Natural, significa che invece di Succesor(n)useremo n, o equivalentemente, invece di nuseremo n - 1.

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

Ora la funzione ricorsiva è più semplice da programmare. In primo luogo, il caso base, n=0. Cosa dovremmo restituire se non vogliamo aggiungere numeri? La risposta è ovviamente 0.

E il caso ricorsivo? Se vogliamo aggiungere nnumeri che iniziano con ae abbiamo già una sumIntsfunzione funzionante che funziona per n-1? Bene, dobbiamo aggiungere ae quindi invocare sumIntscon a + 1, quindi terminiamo con:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

La cosa bella è che ora non dovresti aver bisogno di pensare nel basso livello di ricorsione. Devi solo verificare che:

  • Per i casi base dei dati ricorsivi, calcola la risposta senza utilizzare la ricorsione.
  • Per i casi ricorsivi dei dati ricorsivi, calcola la risposta utilizzando la ricorsione sui dati destrutturati.

4

Potresti essere interessato all'implementazione delle funzioni di Nisan e Schocken . Il pdf collegato fa parte di un corso online gratuito. Descrive la seconda parte dell'implementazione di una macchina virtuale in cui lo studente dovrebbe scrivere un compilatore da linguaggio macchina virtuale a linguaggio macchina. L'implementazione della funzione che propongono è in grado di ricorsione perché è basata su stack.

Per introdurti all'implementazione della funzione: considera il seguente codice della macchina virtuale:

inserisci qui la descrizione dell'immagine

Se Swift è stato compilato con questo linguaggio della macchina virtuale, il seguente blocco di codice Swift:

mult(a: 2, b: 3) - 4

compilerebbe fino a

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

Il linguaggio della macchina virtuale è progettato attorno a uno stack globale . push constant ninserisce un numero intero in questo stack globale.

Dopo aver eseguito le righe 1 e 2, lo stack avrà il seguente aspetto:

256:  2  // Argument 0
257:  3  // Argument 1

256e 257sono indirizzi di memoria.

call mult inserisce il numero di riga di ritorno (3) nello stack e alloca lo spazio per le variabili locali della funzione.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

... e va all'etichetta function mult. Il codice all'interno multviene eseguito. Come risultato dell'esecuzione di quel codice, calcoliamo il prodotto di 2 e 3, che è memorizzato nella 0a variabile locale della funzione.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

Poco prima di returnentrare da mult, noterai la riga:

push local 0  // push result

Spingeremo il prodotto in pila.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

Quando torniamo, accade quanto segue:

  • Posiziona l'ultimo valore nello stack all'indirizzo di memoria dell'argomento 0 (256 in questo caso). Questo sembra essere il posto più conveniente per metterlo.
  • Elimina tutto ciò che è nello stack fino all'indirizzo dello 0 ° argomento.
  • Vai al numero della linea di ritorno (3 in questo caso) e poi avanza.

Dopo essere tornati siamo pronti per eseguire la riga 4, e il nostro stack si presenta così:

256:  6  // product that we just returned

Ora mettiamo 4 in pila.

256:  6
257:  4

subè una funzione primitiva del linguaggio della macchina virtuale. Prende due argomenti e restituisce il risultato nel solito indirizzo: quello dello 0 ° argomento.

Ora abbiamo

256:  2  // 6 - 4 = 2

Ora che sai come funziona una chiamata di funzione, è relativamente semplice capire come funziona la ricorsione. Nessuna magia , solo una pila.

Ho implementato la tua sumIntsfunzione in questo linguaggio della macchina virtuale:

function sumInts 0     // `0` means it has no local variables.
  label IF
    push argument 0
    push argument 1
    lte              
    if-goto ELSE_CASE
    push constant 0
    return
  label ELSE_CASE
    push constant 2
    push argument 0
    push constant 1
    add
    push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

Ora lo chiamerò:

push constant 2
push constant 5
call sumInts           // Line 21

Il codice viene eseguito e si arriva fino al punto di arresto in cui lteritorna false. Ecco come appare lo stack a questo punto:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

Ora "rilassiamo" la nostra ricorsione. return0 e vai alla riga 15 e avanza.

271:  5
272:  0

Riga 16: add

271:  5

Linea 17: return5 e vai alla linea 15 e avanza.

267:  4
268:  5

Riga 16: add

267:  9

Linea 17: return9 e vai alla linea 15 e avanza.

263:  3
264:  9

Riga 16: add

263:  12

Linea return17:12 e vai alla linea 15 e avanza.

259:  2
260:  12

Riga 16: add

259:  14

Linea return17:14 e vai alla linea 21 e avanza.

256:  14

Ecco qua. Ricorsione: glorificato goto.


4

Un ottimo suggerimento che ho riscontrato nell'apprendimento e nella comprensione della ricorsione è quello di dedicare un po 'di tempo all'apprendimento di una lingua che non ha alcuna forma di costruzione di loop diversa dalla ricorsione. In questo modo avrai un'ottima idea di come USARE la ricorsione attraverso la pratica.

Ho seguito http://www.htdp.org/ che, oltre ad essere un tutorial su Scheme, è anche un'ottima introduzione su come progettare programmi in termini di architettura e design.

Ma fondamentalmente, devi investire un po 'di tempo. Senza una comprensione "ferma" della ricorsione, alcuni algoritmi, come il backtracking, ti sembreranno sempre "difficili" o addirittura "magici". Quindi, persevera. :-D

Spero che questo aiuti e buona fortuna!


3

Ci sono già molte buone risposte. Sto ancora provando.
Quando viene chiamata, una funzione ottiene uno spazio di memoria assegnato, che è impilato sullo spazio di memoria della funzione chiamante. In questo spazio di memoria, la funzione conserva i parametri ad essa passati, le variabili ed i loro valori. Questo spazio di memoria svanisce insieme alla chiamata di ritorno finale della funzione. Man mano che l'idea di stack va, lo spazio di memoria della funzione chiamante diventa ora attivo.

Per le chiamate ricorsive, la stessa funzione ottiene più spazio di memoria impilati uno sull'altro. È tutto. La semplice idea di come funziona lo stack nella memoria di un computer dovrebbe farti capire come avviene la ricorsione durante l'implementazione.


3

Un po 'fuori tema, lo so, ma ... prova a cercare la ricorsione su Google ... Vedrai per esempio cosa significa :-)


Le versioni precedenti di Google restituivano il seguente testo (citato a memoria):

ricorsione

Vedi ricorsione

Il 10 settembre 2014, la battuta sulla ricorsione è stata aggiornata:

ricorsione

Forse intendi: ricorsione


Per un'altra risposta, vedi questa risposta .


3

Pensa alla ricorsione come a più cloni che fanno la stessa cosa ...

Chiedi di clonare [1]: "somma numeri compresi tra 2 e 5"

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

e voilá !!


2

Molte delle risposte precedenti sono molto buone. Una tecnica utile per risolvere la ricorsione, tuttavia, è spiegare prima cosa vogliamo fare e codificare come farebbe un essere umano. Nel caso precedente, vogliamo riassumere una sequenza di numeri interi consecutivi (usando i numeri dall'alto):

2, 3, 4, 5  //adding these numbers would sum to 14

Ora, nota che queste righe sono confuse (non sbagliate, ma confuse).

if (a > b) {
    return 0 
}

Perché il test a>b? E perchéreturn 0

Cambiamo il codice per riflettere più da vicino ciò che fa un essere umano

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

Possiamo farlo ancora più umano come? Sì! Di solito riassumiamo da sinistra a destra (2 + 3 + ...). Ma la ricorsione sopra si somma da destra a sinistra (... + 4 + 5). Cambia il codice per rispecchiarlo ( -può essere un po 'intimidatorio, ma non molto)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

Alcuni potrebbero trovare questa funzione più confusa poiché stiamo iniziando dall'estremità "lontana", ma la pratica può farla sembrare naturale (ed è un'altra buona tecnica di "pensiero": provare "entrambi" i lati quando si risolve una ricorsione). E ancora, la funzione riflette ciò che un umano (la maggior parte?) Fa: prende la somma di tutti gli interi a sinistra e aggiunge il "successivo" intero a destra.


2

Stavo attraversando un periodo difficile per capire la ricorsione, quindi ho trovato questo blog e ho già visto questa domanda, quindi ho pensato di dover condividere. Devi leggere questo blog, l'ho trovato estremamente utile, spiega con stack e spiega anche come funziona due ricorsioni con stack passo dopo passo. Ti consiglio di capire prima come funziona lo stack, cosa che spiega molto bene qui: Journey to the Stack

then now you will understand how recursion works now take a look of this post: Comprendi la ricorsione passo dopo passo

inserisci qui la descrizione dell'immagine

È un programma:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


2

La ricorsione ha iniziato ad avere senso per me quando ho smesso di leggere ciò che gli altri dicono al riguardo o di vederlo come qualcosa che posso evitare e ho semplicemente scritto codice. Ho riscontrato un problema con una soluzione e ho provato a duplicare la soluzione senza cercare. Ho guardato la soluzione solo quando sono rimasto impotente. Poi sono tornato a provare a duplicarlo. L'ho fatto di nuovo su più problemi fino a quando non ho sviluppato la mia comprensione e il mio senso di come identificare un problema ricorsivo e risolverlo. Quando sono arrivato a questo livello, ho iniziato a inventare problemi ea risolverli. Questo mi ha aiutato di più. A volte, le cose possono essere apprese solo provandole da soli e lottando; finché non "capisci".


0

Lascia che te lo dica con un esempio della serie di Fibonacci, Fibonacci è

t (n) = t (n - 1) + n;

se n = 0 allora 1

quindi cerchiamo vedere come funziona la ricorsione, ho appena sostituire nin t(n)con n-1e così via. sembra:

t (n-1) = t (n - 2) + n + 1;

t (n-1) = t (n - 3) + n + 1 + n;

t (n-1) = t (n - 4) + n + 1 + n + 2 + n;

.

.

.

t (n) = t (nk) + ... + (nk-3) + (nk-2) + (nk-1) + n;

sappiamo se è t(0)=(n-k)uguale a 1allora n-k=0quindi n=ksostituiamo kcon n:

t (n) = t (nn) + ... + (n-n + 3) + (n-n + 2) + (n-n + 1) + n;

se omettiamo n-nallora:

t (n) = t (0) + ... + 3 + 2 + 1 + (n-1) + n;

così 3+2+1+(n-1)+nè il numero naturale. calcola comeΣ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

il risultato per fib è: O(1 + n²) = O(n²)

Questo è il modo migliore per comprendere la relazione ricorsiva

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.