Capire la ricorsione [chiuso]


225

Sto avendo grossi problemi a capire la ricorsione a scuola. Ogni volta che il professore ne parla, mi sembra di averlo ma non appena lo provo da solo mi fa esplodere completamente il cervello.

Stavo cercando di risolvere le torri di Hanoi tutta la notte e mi ha lasciato senza fiato. Il mio libro di testo ha solo circa 30 pagine in ricorsione, quindi non è troppo utile. Qualcuno sa di libri o risorse che possono aiutare a chiarire questo argomento?


200
Per capire la ricorsione, devi prima capire la ricorsione.
Paul Tomblin,

40
Ricorsione: Vedi ricorsione
Loren Pechtel,

36
@Paul: ho capito lo scherzo, ma ho sempre pensato che fosse tecnicamente sbagliato. Dov'è la condizione di base che causa la fine dell'algoritmo? Questo è un requisito fondamentale per la ricorsione. =)
Sergio Acosta,

70
Ci proverò: "Per capire la ricorsione devi capire la ricorsione, finché non la capisci." =)
Sergio Acosta,

91
Date un'occhiata a questa domanda potrebbe aiutare stackoverflow.com/questions/717725/understanding-recursion
Omar Kooheji

Risposte:


598

Come svuoti un vaso contenente cinque fiori?

Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente quattro fiori.

Come svuoti un vaso contenente quattro fiori?

Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente tre fiori.

Come svuoti un vaso contenente tre fiori?

Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente due fiori.

Come svuoti un vaso contenente due fiori?

Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente un fiore.

Come svuoti un vaso contenente un fiore?

Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso senza fiori.

Come svuoti un vaso senza fiori?

Risposta: se il vaso non è vuoto, tiri fuori un fiore ma il vaso è vuoto, quindi hai finito.

È ripetitivo. Generalizziamolo:

Come svuoti un vaso contenente N fiori?

Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente fiori N-1 .

Hmm, possiamo vederlo nel codice?

void emptyVase( int flowersInVase ) {
  if( flowersInVase > 0 ) {
   // take one flower and
    emptyVase( flowersInVase - 1 ) ;

  } else {
   // the vase is empty, nothing to do
  }
}

Hmm, non avremmo potuto farlo in un ciclo for?

Perché, sì, la ricorsione può essere sostituita con l'iterazione, ma spesso la ricorsione è più elegante.

Parliamo di alberi. Nell'informatica, un albero è una struttura composta da nodi , in cui ogni nodo ha un numero di figli che sono anche nodi o null. Un albero binario è un albero fatto di nodi che hanno esattamente due figli, in genere chiamati "sinistra" e "destra"; ancora una volta i bambini possono essere nodi o null. Una radice è un nodo che non è figlio di nessun altro nodo.

Immagina che un nodo, oltre ai suoi figli, abbia un valore, un numero e immagina di voler sommare tutti i valori in qualche albero.

Per sommare il valore in ogni nodo, aggiungeremmo il valore del nodo stesso al valore del suo figlio sinistro, se presente, e il valore del suo figlio destro, se presente. Ora ricorda che i bambini, se non sono nulli, sono anche nodi.

Quindi per sommare il figlio sinistro, aggiungeremmo il valore del nodo figlio stesso al valore del figlio sinistro, se presente, e il valore del figlio destro, se presente.

Quindi per sommare il valore del figlio sinistro del figlio sinistro, aggiungeremo il valore del nodo figlio stesso al valore del figlio sinistro, se presente, e il valore del figlio destro, se presente.

Forse hai previsto dove sto andando con questo e vorresti vedere un po 'di codice? OK:

struct node {
  node* left;
  node* right;
  int value;
} ;

int sumNode( node* root ) {
  // if there is no tree, its sum is zero
  if( root == null ) {
    return 0 ;

  } else { // there is a tree
    return root->value + sumNode( root->left ) + sumNode( root->right ) ;
  }
}

Si noti che invece di testare esplicitamente i bambini per vedere se sono null o nodi, facciamo semplicemente in modo che la funzione ricorsiva restituisca zero per un nodo null.

Quindi diciamo che abbiamo un albero che assomiglia a questo (i numeri sono valori, le barre indicano i bambini e @ indica che il puntatore punta a null):

     5
    / \
   4   3
  /\   /\
 2  1 @  @
/\  /\
@@  @@

Se chiamiamo sumNode sulla radice (il nodo con valore 5), restituiremo:

return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

Espandiamolo sul posto. Ovunque vediamo sumNode, lo sostituiremo con l'espansione dell'istruzione return:

sumNode( node-with-value-5);
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

return 5 + 4 + sumNode( node-with-value-2 ) + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + 0 + 0
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + sumNode(null ) + sumNode( null ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + 0 + 0 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 
 + 3  ;

return 5 + 4 
 + 2 
 + 1 
 + 3  ;

return 5 + 4 
 + 3
 + 3  ;

return 5 + 7
 + 3  ;

return 5 + 10 ;

return 15 ;

Ora vedi come abbiamo conquistato una struttura di profondità e "ramificazione" arbitrarie, considerandola l'applicazione ripetuta di un modello composito? ogni volta attraverso la nostra funzione sumNode, ci siamo occupati di un solo nodo, usando un singolo ramo if / then e due semplici dichiarazioni di ritorno che quasi si sono scritte da sole, direttamente dalle nostre specifiche?

How to sum a node:
 If a node is null 
   its sum is zero
 otherwise 
   its sum is its value 
   plus the sum of its left child node
   plus the sum of its right child node

Questo è il potere della ricorsione.


L'esempio di vaso sopra è un esempio di ricorsione della coda . Tutto ciò che la ricorsione della coda significa è che nella funzione ricorsiva, se ricorrevamo (cioè, se avessimo chiamato di nuovo la funzione), quella era l'ultima cosa che facevamo.

L'esempio dell'albero non è ricorsivo alla coda, perché anche se l'ultima cosa che abbiamo fatto è stata quella di reclutare il bambino giusto, prima di farlo abbiamo ricorsato il bambino sinistro.

In effetti, l'ordine in cui abbiamo chiamato i figli e aggiunto il valore del nodo corrente non ha alcuna importanza, perché l'aggiunta è commutativa.

Ora diamo un'occhiata a un'operazione in cui l'ordine conta. Useremo un albero binario di nodi, ma questa volta il valore mantenuto sarà un carattere, non un numero.

Il nostro albero avrà una proprietà speciale, che per ogni nodo, il suo carattere viene dopo (in ordine alfabetico) il personaggio tenuto dal figlio sinistro e prima (in ordine alfabetico) il personaggio tenuto dal figlio destro.

Quello che vogliamo fare è stampare l'albero in ordine alfabetico. È facile da fare, data la proprietà speciale dell'albero. Stampiamo solo il figlio sinistro, quindi il carattere del nodo, quindi il figlio destro.

Non vogliamo solo stampare volenti o nolenti, quindi passeremo alla nostra funzione qualcosa su cui stampare. Questo sarà un oggetto con una funzione di stampa (carattere); non dobbiamo preoccuparci di come funziona, solo che quando viene chiamata la stampa, stamperà qualcosa, da qualche parte.

Vediamo che nel codice:

struct node {
  node* left;
  node* right;
  char value;
} ;

// don't worry about this code
class Printer {
  private ostream& out;
  Printer( ostream& o ) :out(o) {}
  void print( char c ) { out << c; }
}

// worry about this code
int printNode( node* root, Printer& printer ) {
  // if there is no tree, do nothing
  if( root == null ) {
    return ;

  } else { // there is a tree
    printNode( root->left, printer );
    printer.print( value );
    printNode( root->right, printer );
}

Printer printer( std::cout ) ;
node* root = makeTree() ; // this function returns a tree, somehow
printNode( root, printer );

Oltre all'ordine delle operazioni che contano, questo esempio mostra che possiamo trasferire le cose in una funzione ricorsiva. L'unica cosa che dobbiamo fare è assicurarci che ad ogni chiamata ricorsiva, continuiamo a passarla. Abbiamo passato un puntatore a nodo e una stampante alla funzione e ad ogni chiamata ricorsiva li abbiamo passati "in basso".

Ora se il nostro albero appare così:

         k
        / \
       h   n
      /\   /\
     a  j @  @
    /\ /\
    @@ i@
       /\
       @@

Cosa stamperemo?

From k, we go left to
  h, where we go left to
    a, where we go left to 
      null, where we do nothing and so
    we return to a, where we print 'a' and then go right to
      null, where we do nothing and so
    we return to a and are done, so
  we return to h, where we print 'h' and then go right to
    j, where we go left to
      i, where we go left to 
        null, where we do nothing and so
      we return to i, where we print 'i' and then go right to
        null, where we do nothing and so
      we return to i and are done, so
    we return to j, where we print 'j' and then go right to
      null, where we do nothing and so
    we return to j and are done, so
  we return to h and are done, so
we return to k, where we print 'k' and then go right to
  n where we go left to 
    null, where we do nothing and so
  we return to n, where we print 'n' and then go right to
    null, where we do nothing and so
  we return to n and are done, so 
we return to k and are done, so we return to the caller

Quindi, se guardiamo solo le linee in cui siamo stati stampati:

    we return to a, where we print 'a' and then go right to
  we return to h, where we print 'h' and then go right to
      we return to i, where we print 'i' and then go right to
    we return to j, where we print 'j' and then go right to
we return to k, where we print 'k' and then go right to
  we return to n, where we print 'n' and then go right to

Vediamo che abbiamo stampato "ahijkn", che è davvero in ordine alfabetico.

Riusciamo a stampare un intero albero, in ordine alfabetico, solo sapendo come stampare un singolo nodo in ordine alfabetico. Il che era giusto (perché il nostro albero aveva la speciale proprietà di ordinare i valori alla sinistra dei valori alfabeticamente successivi) sapendo di stampare il figlio sinistro prima di stampare il valore del nodo e di stampare il figlio giusto dopo aver stampato il valore del nodo.

E questo è il potere della ricorsione: essere in grado di fare cose intere sapendo solo come fare una parte del tutto (e sapere quando smettere di ricorrere).

Ricordando che nella maggior parte delle lingue, operatore || ("o") cortocircuiti quando il suo primo operando è vero, la funzione ricorsiva generale è:

void recurse() { doWeStop() || recurse(); } 

Luc M commenta:

SO dovrebbe creare un badge per questo tipo di risposta. Congratulazioni!

Grazie Luc! Ma, in realtà, poiché ho modificato questa risposta più di quattro volte (per aggiungere l'ultimo esempio, ma principalmente per correggere errori di battitura e lucidarlo - digitando su una minuscola tastiera per netbook è difficile), non riesco a ottenere più punti per questo . Il che mi scoraggia in qualche modo dallo sforzo maggiore nelle risposte future.

Vedi il mio commento qui su questo: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699


35

Il tuo cervello è esploso perché è entrato in una ricorsione infinita. Questo è un errore comune per i principianti.

Che ci crediate o no, capite già la ricorsione, vieni semplicemente trascinato giù da una metafora comune, ma difettosa per una funzione: una piccola scatola con roba che entra e esce.

Pensa invece a un'attività o una procedura, ad esempio "scopri di più sulla ricorsione in rete". È ricorsivo e non ci sono problemi. Per completare questa attività potresti:

a) Leggi la pagina dei risultati di Google per "ricorsione"
b) Dopo averlo letto, segui il primo link su di esso e ...
a.1) Leggi quella nuova pagina sulla ricorsione 
b.1) Dopo averlo letto, segui il primo link su di esso e ...
a.2) Leggi quella nuova pagina sulla ricorsione 
b.2) Dopo averlo letto, segui il primo link su di esso e ...

Come puoi vedere, hai fatto cose ricorsive per molto tempo senza problemi.

Per quanto tempo continueresti a fare questo compito? Per sempre fino a quando il tuo cervello esplode? Certo che no, ti fermerai ad un dato punto, ogni volta che ritieni di aver completato l'attività.

Non è necessario specificarlo quando ti viene chiesto di "saperne di più sulla ricorsione in rete", perché sei un essere umano e puoi dedurlo da solo.

Il computer non può dedurre jack, quindi è necessario includere un finale esplicito: "scopri di più sulla ricorsione in rete, FINO A quando non lo capisci o hai letto un massimo di 10 pagine ".

Hai anche dedotto che dovresti iniziare dalla pagina dei risultati di Google per "ricorsione", e di nuovo è qualcosa che un computer non può fare. La descrizione completa del nostro compito ricorsivo deve includere anche un punto di partenza esplicito:

"scopri di più sulla ricorsione in rete, FINO A quando non lo capisci o hai letto un massimo di 10 pagine e a partire da www.google.com/search?q=recursion "

Per capire tutto, ti suggerisco di provare uno di questi libri:

  • Lisp comune: una delicata introduzione al calcolo simbolico. Questa è la più simpatica spiegazione non matematica della ricorsione.
  • Il piccolo intrigatore.

6
La metafora di "function = small box of I / O" funziona con la ricorsione fintanto che immagini anche che ci sia una fabbrica là fuori che produce cloni infiniti e la tua piccola box può ingoiare altre piccole scatole.
effimero

2
Interessante ... Quindi, in futuro i robot google qualcosa e impareranno da soli usando i primi 10 collegamenti. :) :)
kumar,

2
@kumar non lo sta già facendo Google con Internet ..?
TJ

1
grandi libri, grazie per la raccomandazione
Max Koretskyi

+1 per "Il tuo cervello è esploso perché è entrato in una ricorsione infinita. Questo è un errore comune per i principianti."
Stack Underflow,

26

Per capire la ricorsione, tutto ciò che devi fare è guardare sull'etichetta della tua bottiglia di shampoo:

function repeat()
{
   rinse();
   lather();
   repeat();
}

Il problema è che non esiste alcuna condizione di terminazione e la ricorsione si ripeterà indefinitamente o fino a quando non si esauriscono shampoo o acqua calda (condizioni di terminazione esterne, simili al soffiaggio della pila).


6
Grazie dar7yl - questo mi ha SEMPRE infastidito sulle bottiglie di shampoo. (Immagino di essere sempre stato destinato alla programmazione). Anche se scommetto che il ragazzo che ha deciso di aggiungere "Ripeti" alla fine delle istruzioni ha reso la società milioni.
kenj0418

5
Spero che rinse()dopo di telather()
CoderDennis,

@JakeWilson se si utilizza l'ottimizzazione di coda chiamata - certo. allo stato attuale, però - è una ricorsione completamente valida.

1
@ dar7yl quindi è per questo che la mia bottiglia di shampoo è sempre vuota ...
Brandon Ling

11

Se vuoi un libro che faccia un buon lavoro di spiegazione della ricorsione in termini semplici, dai un'occhiata a Gödel, Escher, Bach: An Eternal Golden Braid di Douglas Hofstadter, in particolare il Capitolo 5. Oltre alla ricorsione fa un buon lavoro di spiegazione una serie di concetti complessi in informatica e matematica in modo comprensibile, con una spiegazione che si basa su un'altra. Se non hai mai avuto molta esposizione a questo tipo di concetti prima, può essere un libro piuttosto strabiliante.


E poi vagare per il resto dei libri di Hofstadter. Il mio preferito al momento è quello sulla traduzione della poesia: Le Ton Beau do Marot . Non proprio un argomento CS, ma solleva questioni interessanti su ciò che la traduzione è davvero e significa.
RBerteig,

9

Questo è più un reclamo che una domanda. Hai una domanda più specifica sulla ricorsione? Come la moltiplicazione, non è una cosa di cui le persone scrivono molto.

A proposito di moltiplicazione, pensa a questo.

Domanda:

Che cos'è un * b?

Risposta:

Se b è 1, è a. Altrimenti, è a + a * (b-1).

Che cos'è un * (b-1)? Vedi la domanda sopra per un modo per risolverlo.


@Andrew Grimm: bella domanda. Questa definizione è per numeri naturali, non numeri interi.
S. Lott,

9

Penso che questo metodo molto semplice dovrebbe aiutarti a capire la ricorsione. Il metodo chiamerà se stesso fino a quando una determinata condizione è vera e quindi restituirà:

function writeNumbers( aNumber ){
 write(aNumber);
 if( aNumber > 0 ){
  writeNumbers( aNumber - 1 );
 }
 else{
  return;
 }
}

Questa funzione stamperà tutti i numeri dal primo numero che lo alimenterai fino allo 0. Pertanto:

writeNumbers( 10 );
//This wil write: 10 9 8 7 6 5 4 3 2 1 0
//and then stop because aNumber is no longer larger then 0

Quello che succede bassamente è che writeNumbers (10) scriverà 10 e poi chiamerà writeNumbers (9) che scriverà 9 e poi chiamerà writeNumber (8) ecc. Fino a writeNumbers (1) scrive 1 e poi chiama writeNumbers (0) che scriverà 0 butt non chiamerà writeNumbers (-1);

Questo codice è essenzialmente lo stesso di:

for(i=10; i>0; i--){
 write(i);
}

Quindi perché usare la ricorsione potresti chiedere, se un for-loop fa essenzialmente lo stesso. Bene, usi principalmente la ricorsione quando dovresti annidare per i loop ma non saprai quanto in profondità sono nidificati. Ad esempio, quando si stampano articoli da array nidificati:

var nestedArray = Array('Im a string', 
                        Array('Im a string nested in an array', 'me too!'),
                        'Im a string again',
                        Array('More nesting!',
                              Array('nested even more!')
                              ),
                        'Im the last string');
function printArrayItems( stringOrArray ){
 if(typeof stringOrArray === 'Array'){
   for(i=0; i<stringOrArray.length; i++){ 
     printArrayItems( stringOrArray[i] );
   }
 }
 else{
   write( stringOrArray );
 }
}

printArrayItems( stringOrArray );
//this will write:
//'Im a string' 'Im a string nested in an array' 'me too' 'Im a string again'
//'More nesting' 'Nested even more' 'Im the last string'

Questa funzione potrebbe richiedere un array che potrebbe essere nidificato in 100 livelli, mentre scrivere un ciclo for richiederebbe quindi di annidarlo 100 volte:

for(i=0; i<nestedArray.length; i++){
 if(typeof nestedArray[i] == 'Array'){
  for(a=0; i<nestedArray[i].length; a++){
   if(typeof nestedArray[i][a] == 'Array'){
    for(b=0; b<nestedArray[i][a].length; b++){
     //This would be enough for the nestedAaray we have now, but you would have
     //to nest the for loops even more if you would nest the array another level
     write( nestedArray[i][a][b] );
    }//end for b
   }//endif typeod nestedArray[i][a] == 'Array'
   else{ write( nestedArray[i][a] ); }
  }//end for a
 }//endif typeod nestedArray[i] == 'Array'
 else{ write( nestedArray[i] ); }
}//end for i

Come puoi vedere, il metodo ricorsivo è molto meglio.


1
LOL - Mi ci è voluto un secondo per capire che stavi usando JavaScript! Ho visto "funzione" e ho pensato che PHP abbia capito che le variabili non iniziavano con $. Poi ho pensato a C # per l'uso della parola var - ma i metodi non sono chiamati funzioni!
ozzy432836

8

In realtà usi la ricorsione per ridurre la complessità del tuo problema. Si applica la ricorsione fino a quando non si raggiunge un semplice caso di base che può essere risolto facilmente. Con questo puoi risolvere l'ultimo passaggio ricorsivo. E con questo tutti gli altri passaggi ricorsivi fino al problema originale.


1
Sono d'accordo con questa risposta. Il trucco è identificare e risolvere il caso base (più semplice). E quindi esprimere il problema in termini di quel caso più semplice (che hai già risolto).
Sergio Acosta,

6

Proverò a spiegarlo con un esempio.

Sai cosa n! si intende? In caso contrario: http://en.wikipedia.org/wiki/Factorial

3! = 1 * 2 * 3 = 6

ecco alcuni pseudocodici

function factorial(n) {
  if (n==0) return 1
  else return (n * factorial(n-1))
}

Quindi proviamo:

factorial(3)

è n 0?

no!

quindi scaviamo più a fondo con la nostra ricorsione:

3 * factorial(3-1)

3-1 = 2

è 2 == 0?

no!

quindi andiamo più in profondità! 3 * 2 * fattoriale (2-1) 2-1 = 1

è 1 == 0?

no!

quindi andiamo più in profondità! 3 * 2 * 1 * fattoriale (1-1) 1-1 = 0

è 0 == 0?

sì!

abbiamo un caso banale

quindi abbiamo 3 * 2 * 1 * 1 = 6

spero che ti abbia aiutato


Questo non è un modo utile di pensare alla ricorsione. Un errore comune che i principianti commettono è quello di provare a immaginare cosa succede all'interno della chiamata recusiva, invece di fidarsi / dimostrare che restituirà la risposta corretta - e questa risposta sembra incoraggiarlo.
ShreevatsaR,

quale sarebbe un modo migliore di comprendere la ricorsione? non dico che devi guardare ogni funzione ricorsiva in questo modo. Ma mi ha aiutato a capire come funziona.
Zoran Zaric,

1
[Non ho votato -1, BTW.] Potresti pensare in questo modo: confidando che fattoriale (n-1) dia correttamente (n-1)! = (N-1) * ... * 2 * 1, quindi n fattoriale (n-1) dà n * (n-1) ... * 2 * 1, che è n !. O qualunque cosa. [Se stai cercando di imparare a scrivere da solo le funzioni ricorsive, non solo vedere cosa fa una funzione.]
ShreevatsaR

Ho usato i fattoriali per spiegare la ricorsione, e penso che uno dei motivi più comuni per cui fallisce come esempio sia perché lo spiegante non ama la matematica, e ne viene coinvolto. (Se qualcuno che non ama la matematica debba o meno codificare è un'altra domanda). Per questa ragione, generalmente cerco di usare un esempio non matematico ove possibile.
Tony Meyer,

5

ricorsione

Metodo A, chiama Metodo A chiama Metodo A. Alla fine uno di questi metodi A non chiamerà e non uscirà, ma è ricorsione perché qualcosa chiama se stesso.

Esempio di ricorsione in cui desidero stampare tutti i nomi delle cartelle sul disco rigido: (in c #)

public void PrintFolderNames(DirectoryInfo directory)
{
    Console.WriteLine(directory.Name);

    DirectoryInfo[] children = directory.GetDirectories();

    foreach(var child in children)
    {
        PrintFolderNames(child); // See we call ourself here...
    }
}

dov'è il caso base in questo esempio?
Kunal Mukherjee,

4

Quale libro stai usando?

Il manuale standard sugli algoritmi che è effettivamente buono è Cormen & Rivest. La mia esperienza è che insegna abbastanza bene la ricorsione.

La ricorsione è una delle parti più difficili della programmazione da comprendere e, sebbene richieda l'istinto, può essere appresa. Ma ha bisogno di una buona descrizione, buoni esempi e buone illustrazioni.

Inoltre, 30 pagine in generale sono molte, 30 pagine in un singolo linguaggio di programmazione sono confuse. Non cercare di imparare la ricorsione in C o Java, prima di aver compreso la ricorsione in generale da un libro generale.


4

Una funzione ricorsiva è semplicemente una funzione che si chiama tutte le volte che è necessario. È utile se devi elaborare qualcosa più volte, ma non sei sicuro di quante volte sarà effettivamente richiesto. In un certo senso, potresti pensare a una funzione ricorsiva come a un tipo di loop. Come in un ciclo, tuttavia, dovrai specificare le condizioni affinché il processo venga interrotto, altrimenti diventerà infinito.


4

http://javabat.com è un posto divertente ed eccitante per praticare la ricorsione. I loro esempi iniziano abbastanza leggeri e funzionano in modo approfondito (se vuoi portarlo così lontano). Nota: il loro approccio è imparare praticando. Ecco una funzione ricorsiva che ho scritto semplicemente per sostituire un ciclo for.

Il ciclo for:

public printBar(length)
{
  String holder = "";
  for (int index = 0; i < length; i++)
  {
    holder += "*"
  }
  return holder;
}

Ecco la ricorsione per fare la stessa cosa. (nota che sovraccarichiamo il primo metodo per assicurarci che sia usato proprio come sopra). Abbiamo anche un altro metodo per mantenere il nostro indice (simile al modo sopra indicato dall'istruzione for). La funzione ricorsiva deve mantenere il proprio indice.

public String printBar(int Length) // Method, to call the recursive function
{
  printBar(length, 0);
}

public String printBar(int length, int index) //Overloaded recursive method
{
  // To get a better idea of how this works without a for loop
  // you can also replace this if/else with the for loop and
  // operationally, it should do the same thing.
  if (index >= length)
    return "";
  else
    return "*" + printBar(length, index + 1); // Make recursive call
}

Per farla breve, la ricorsione è un buon modo per scrivere meno codice. In quest'ultimo caso, notiamo che abbiamo un'istruzione if. Se la nostra condizione è stata raggiunta, usciremo dalla ricorsione e torneremo al metodo precedente, che ritorna al metodo precedente, ecc. Se ho inviato un printBar (8), ottengo ********. Spero che con un esempio di una semplice funzione che faccia la stessa cosa di un ciclo for che forse questo possa aiutare. Puoi esercitarti di più su Java Bat però.


javabat.com è un sito estremamente utile che ti aiuterà a pensare in modo ricorsivo. Consiglio vivamente di andarci e di provare a risolvere i problemi ricorsivi da soli.
Paradius,

3

Il modo veramente matematico di guardare alla costruzione di una funzione ricorsiva sarebbe il seguente:

1: Immagina di avere una funzione corretta per f (n-1), costruisci f in modo che f (n) sia corretto. 2: crea f, in modo tale che f (1) sia corretto.

Ecco come puoi dimostrare che la funzione è corretta, matematicamente, e si chiama Induzione . È equivalente ad avere diversi casi base o funzioni più complicate su più variabili). È anche equivalente a immaginare che f (x) sia corretto per tutte le x

Ora per un esempio "semplice". Crea una funzione in grado di determinare se è possibile avere una combinazione di monete di 5 centesimi e 7 centesimi per fare x centesimi. Ad esempio, è possibile avere 17 centesimi di 2x5 + 1x7, ma impossibile avere 16 centesimi.

Ora immagina di avere una funzione che ti dice se è possibile creare x centesimi, purché x <n. Chiamare questa funzione can_create_coins_small. Dovrebbe essere abbastanza semplice immaginare come realizzare la funzione per n. Ora costruisci la tua funzione:

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins_small(n-7))
        return true;
    else if (n >= 5 && can_create_coins_small(n-5))
        return true;
    else
        return false;
}

Il trucco qui è rendersi conto che il fatto che can_create_coins funziona per n, significa che è possibile sostituire can_create_coins con can_create_coins_small, dando:

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

Un'ultima cosa da fare è avere un caso base per fermare la ricorsione infinita. Si noti che se si sta tentando di creare 0 centesimi, ciò è possibile senza monete. Aggiungendo questa condizione si ottiene:

bool can_create_coins(int n)
{
    if (n == 0)
        return true;
    else if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

Si può dimostrare che questa funzione tornerà sempre, usando un metodo chiamato discesa infinita , ma qui non è necessario. Puoi immaginare che f (n) chiama solo valori più bassi di n e alla fine raggiungerà sempre 0.

Per utilizzare queste informazioni per risolvere il problema della Torre di Hanoi, penso che il trucco sia supporre che tu abbia una funzione per spostare n-1 compresse da a a b (per qualsiasi a / b), provando a spostare n tabelle da a a b .


3

Semplice esempio ricorsivo in Common Lisp :

MYMAP applica una funzione a ciascun elemento in un elenco.

1) una lista vuota non ha alcun elemento, quindi restituiamo la lista vuota - () e NIL sono entrambi la lista vuota.

2) applica la funzione al primo elenco, chiama MYMAP per il resto dell'elenco (la chiamata ricorsiva) e combina entrambi i risultati in un nuovo elenco.

(DEFUN MYMAP (FUNCTION LIST)
  (IF (NULL LIST)
      ()
      (CONS (FUNCALL FUNCTION (FIRST LIST))
            (MYMAP FUNCTION (REST LIST)))))

Guardiamo l'esecuzione tracciata. Inserendo una funzione, gli argomenti vengono stampati. All'uscita da una funzione, il risultato viene stampato. Per ogni chiamata ricorsiva, l'output sarà rientrato a livello.

Questo esempio chiama la funzione SIN su ciascun numero in un elenco (1 2 3 4).

Command: (mymap 'sin '(1 2 3 4))

1 Enter MYMAP SIN (1 2 3 4)
| 2 Enter MYMAP SIN (2 3 4)
|   3 Enter MYMAP SIN (3 4)
|   | 4 Enter MYMAP SIN (4)
|   |   5 Enter MYMAP SIN NIL
|   |   5 Exit MYMAP NIL
|   | 4 Exit MYMAP (-0.75680256)
|   3 Exit MYMAP (0.14112002 -0.75680256)
| 2 Exit MYMAP (0.9092975 0.14112002 -0.75680256)
1 Exit MYMAP (0.841471 0.9092975 0.14112002 -0.75680256)

Questo è il nostro risultato :

(0.841471 0.9092975 0.14112002 -0.75680256)

CHE COSA CON TUTTE LE TAPPE? Seriamente, però, sono passati di moda in LISP circa 20 anni fa.
Sebastian Krog,

Bene, l'ho scritto su un modello Lisp Machine, che ora ha 17 anni. In realtà ho scritto la funzione senza la formattazione nel listener, ho fatto alcune modifiche e poi ho usato PPRINT per formattarla. Ciò ha trasformato il codice in CAPS.
Rainer Joswig,

3

Per spiegare la ricorsione a un bambino di sei anni, prima spiegalo a un bambino di cinque anni, quindi attendi un anno.

In realtà, questo è un utile contro-esempio, perché la tua chiamata ricorsiva dovrebbe essere più semplice, non più difficile. Sarebbe ancora più difficile spiegare la ricorsione a un bambino di cinque anni e sebbene tu possa fermare la ricorsione a 0, non hai una soluzione semplice per spiegare la ricorsione a un bambino di zero anni.

Per risolvere un problema usando la ricorsione, prima suddividilo in uno o più problemi più semplici che puoi risolvere allo stesso modo, e poi quando il problema è abbastanza semplice da risolvere senza ulteriore ricorsione, puoi tornare a livelli più alti.

In effetti, quella era una definizione ricorsiva di come risolvere un problema con la ricorsione.


3

I bambini usano implicitamente la ricorsione, ad esempio:

Road trip a Disney World

Ci siamo ancora? (No)

Ci siamo ancora? (Presto)

Ci siamo ancora? (Quasi ...)

Ci siamo ancora? (SHHHH)

Siamo arrivati?(!!!!!)

A quel punto il bambino si addormenta ...

Questa funzione di conto alla rovescia è un semplice esempio:

function countdown()
      {
      return (arguments[0] > 0 ?
        (
        console.log(arguments[0]),countdown(arguments[0] - 1)) : 
        "done"
        );
      }
countdown(10);

Anche la legge di Hofstadter applicata ai progetti software è rilevante.

L'essenza del linguaggio umano è, secondo Chomsky, la capacità dei cervelli finiti di produrre ciò che considera grammatiche infinite. Con questo significa non solo che non esiste un limite superiore a ciò che possiamo dire, ma che non esiste un limite superiore al numero di frasi della nostra lingua, non esiste un limite superiore alla dimensione di una particolare frase. Chomsky ha affermato che lo strumento fondamentale che sta alla base di tutta questa creatività del linguaggio umano è la ricorsione: la capacità di ripetersi di una frase all'interno di un'altra frase dello stesso tipo. Se dico "casa del fratello di John", ho un sostantivo "casa", che si presenta in una frase di nome, "casa del fratello", e quella frase di nome si trova in un'altra frase di nome "casa del fratello di Giovanni". Questo ha molto senso, ed è

Riferimenti


2

Quando lavoro con soluzioni ricorsive, cerco sempre di:

  • Stabilire prima il caso base, ovvero quando n = 1 in una soluzione fattoriale
  • Prova a trovare una regola generale per ogni altro caso

Inoltre ci sono diversi tipi di soluzioni ricorsive, c'è l'approccio di divisione e conquista che è utile per i frattali e molti altri.

Aiuterebbe anche se potessi lavorare su problemi più semplici prima solo per capire. Alcuni esempi stanno risolvendo per il fattoriale e generando l'ennesimo numero di fibonacci.

Per i riferimenti, consiglio vivamente Algorithms di Robert Sedgewick.

Spero che aiuti. In bocca al lupo.


Mi chiedo se per prima cosa non sia meglio elaborare una regola generale, la chiamata ricorsiva, che è "più semplice" di quella con cui hai iniziato. Quindi il caso base dovrebbe diventare ovvio in base al caso più semplice. Questo è il modo in cui tendo a pensare a risolvere un problema in modo ricorsivo.
dlaliberte,

2

Ahia. Ho cercato di capire le Torri di Hanoi l'anno scorso. La cosa difficile di TOH è che non è un semplice esempio di ricorsione: ci sono ricorsioni annidate che cambiano anche il ruolo delle torri in ogni chiamata. L'unico modo in cui potevo avere un senso aveva senso visualizzare letteralmente il movimento degli anelli nella mia mente e verbalizzare quale sarebbe stata la chiamata ricorsiva. Vorrei iniziare con un singolo squillo, poi due, poi tre. In realtà ho ordinato il gioco su Internet. Mi ci vollero forse due o tre giorni per spezzarmi il cervello per ottenerlo.


1

Una funzione ricorsiva è come una molla che comprimi un po 'ad ogni chiamata. Ad ogni passaggio, metti un po 'di informazioni (contesto attuale) in una pila. Quando viene raggiunto il passaggio finale, la molla viene rilasciata, raccogliendo tutti i valori (contesti) in una volta!

Non sono sicuro che questa metafora sia efficace ... :-)

Ad ogni modo, al di là degli esempi classici (fattoriale che è l'esempio peggiore poiché è inefficiente e facilmente appiattibile, Fibonacci, Hanoi ...) che sono un po 'artificiali (li uso raramente, se non mai, in casi di programmazione reali), è interessante vedere dove è veramente usato.

Un caso molto comune è camminare su un albero (o un grafico, ma gli alberi sono più comuni, in generale).
Ad esempio, una gerarchia di cartelle: per elencare i file, è necessario scorrere su di essi. Se trovi una sottodirectory, la funzione che elenca i file si chiama automaticamente con la nuova cartella come argomento. Quando ritorna dall'elenco di questa nuova cartella (e delle sue sottocartelle!), Riprende il suo contesto al file (o cartella) successivo.
Un altro caso concreto è quando si disegna una gerarchia di componenti della GUI: è comune avere contenitori, come i riquadri, per contenere anche componenti che possono essere riquadri o componenti composti, ecc. La routine di verniciatura chiama ricorsivamente la funzione di verniciatura di ciascun componente, che chiama la funzione di disegno di tutti i componenti che contiene, ecc.

Non sono sicuro di essere molto chiaro, ma mi piace mostrare l'uso del materiale didattico nel mondo reale, in quanto era qualcosa su cui mi imbattevo in passato.


1

Pensa a un'ape operaia. Cerca di fare il miele. Fa il suo lavoro e si aspetta che altre api operaie facciano il resto del miele. E quando il nido d'ape è pieno, si ferma.

Pensalo come una magia. Hai una funzione che ha lo stesso nome con quella che stai cercando di implementare e quando gli dai il sottoproblema, lo risolve per te e l'unica cosa che devi fare è integrare la soluzione della tua parte con la soluzione che ti ha dato.

Ad esempio, vogliamo calcolare la lunghezza di un elenco. Chiamiamo la nostra funzione magical_length e il nostro aiutante magico con magical_length Sappiamo che se diamo alla sublist che non ha il primo elemento, ci darà la lunghezza della sublist per magia. Quindi l'unica cosa che dobbiamo pensare è come integrare queste informazioni con il nostro lavoro. La lunghezza del primo elemento è 1 e magic_counter ci fornisce la lunghezza dell'elenco secondario n-1, quindi la lunghezza totale è (n-1) + 1 -> n

int magical_length( list )
  sublist = rest_of_the_list( list )
  sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
  return 1 + sublist_length

Tuttavia questa risposta è incompleta perché non abbiamo considerato cosa succede se diamo un elenco vuoto. Abbiamo pensato che l'elenco che abbiamo sempre abbia almeno un elemento. Pertanto, dobbiamo pensare a quale dovrebbe essere la risposta se ci viene fornito un elenco vuoto e la risposta è ovviamente 0. Quindi aggiungi queste informazioni alla nostra funzione e questa si chiama condizione base / limite.

int magical_length( list )
  if ( list is empty) then
    return 0
  else
    sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
    return 1 + sublist_length
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.