Cos'è la ricorsione e quando dovrei usarla?


121

Uno degli argomenti che sembra emergere regolarmente nelle mailing list e nelle discussioni online è il merito (o la sua mancanza) di conseguire una laurea in informatica. Un argomento che sembra emergere più volte per la parte negativa è che hanno codificato per un certo numero di anni e non hanno mai usato la ricorsione.

Quindi la domanda è:

  1. Cos'è la ricorsione?
  2. Quando dovrei usare la ricorsione?
  3. Perché le persone non usano la ricorsione?


3
Questo può aiutare a afferrare il concetto: navigare per il collegamento fornito sul secondo commento della questione in questa pagina e fare quello che i commenti dicono di fare: stackoverflow.com/questions/3021/...
dtmland

Risposte:


86

Ci sono un certo numero di buone spiegazioni della ricorsione in questo thread, questa risposta riguarda il motivo per cui non dovresti usarla nella maggior parte dei linguaggi. * Nella maggior parte delle principali implementazioni del linguaggio imperativo (cioè ogni importante implementazione di C, C ++, Basic, Python , Ruby, Java e C #) iterazione è di gran lunga preferibile alla ricorsione.

Per capire il motivo, segui i passaggi utilizzati dalle lingue precedenti per chiamare una funzione:

  1. lo spazio è scavato nella pila per gli argomenti e le variabili locali della funzione
  2. gli argomenti della funzione vengono copiati in questo nuovo spazio
  3. il controllo passa alla funzione
  4. il codice della funzione viene eseguito
  5. il risultato della funzione viene copiato in un valore restituito
  6. la pila viene riavvolta nella posizione precedente
  7. il controllo torna al punto in cui è stata chiamata la funzione

Fare tutti questi passaggi richiede tempo, di solito un po 'più di quanto ci vuole per iterare attraverso un ciclo. Tuttavia, il vero problema è nel passaggio # 1. Quando molti programmi vengono avviati, allocano un singolo blocco di memoria per il loro stack e quando esauriscono quella memoria (spesso, ma non sempre a causa della ricorsione), il programma si arresta in modo anomalo a causa di un overflow dello stack .

Quindi in queste lingue la ricorsione è più lenta e ti rende vulnerabile agli arresti anomali. Tuttavia, ci sono ancora alcuni argomenti per usarlo. In generale, il codice scritto in modo ricorsivo è più breve e un po 'più elegante, una volta che sai come leggerlo.

Esiste una tecnica che gli implementatori del linguaggio possono utilizzare chiamata ottimizzazione delle chiamate tail che può eliminare alcune classi di overflow dello stack. In poche parole: se l'espressione di ritorno di una funzione è semplicemente il risultato di una chiamata di funzione, non è necessario aggiungere un nuovo livello allo stack, è possibile riutilizzare quello corrente per la funzione chiamata. Purtroppo, poche implementazioni imperative del linguaggio hanno l'ottimizzazione delle chiamate di coda incorporata.

* Amo la ricorsione. Il mio linguaggio statico preferito non usa affatto i loop, la ricorsione è l'unico modo per fare qualcosa ripetutamente. Semplicemente non penso che la ricorsione sia generalmente una buona idea in lingue che non sono ottimizzate per essa.

** A proposito Mario, il nome tipico della tua funzione ArrangeString è "join" e sarei sorpreso se la tua lingua preferita non ne avesse già un'implementazione.


1
È bello vedere una spiegazione dell'overhead intrinseco della ricorsione. Ho accennato anche a questo nella mia risposta. Ma per me, il grande punto di forza della ricorsione è ciò che puoi fare con lo stack di chiamate. Puoi scrivere un algoritmo succinto con ricorsione che si ramifica ripetutamente, permettendoti di fare cose come la scansione delle gerarchie (relazioni genitore / figlio) con facilità. Vedi la mia risposta per un esempio.
Steve Wortham

7
Molto deluso di trovare la risposta migliore a una domanda intitolata "Che cos'è la ricorsione e quando dovrei usarla?" in realtà non rispondo a nessuno di questi, per non parlare dell'estremo pregiudizio che mette in guardia contro la ricorsione, nonostante il suo uso diffuso nella maggior parte delle lingue che hai menzionato (non c'è niente di specificamente sbagliato in quello che hai detto, ma sembra che tu stia esagerando il problema e sottoesagerando l'utilità).
Bernhard Barker

2
Probabilmente hai ragione @Dukeling. Per contesto, quando ho scritto questa risposta c'erano molte ottime spiegazioni della ricorsione già scritte e l'ho scritta con l'intenzione di essere un'aggiunta a quelle informazioni, non la risposta migliore. In pratica, quando ho bisogno di camminare su un albero o gestire qualsiasi altra struttura di dati annidata, di solito mi rivolgo alla ricorsione e devo ancora raggiungere uno stack overflow di mia creazione in natura.
Peter Burns

63

Semplice esempio inglese di ricorsione.

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.

1
su + per toccare il cuore :)
Suhail Mumtaz Awan

C'è una storia simile come questa per i bambini piccoli che non si addormentano nei racconti popolari cinesi, me ne sono appena ricordata e mi ricorda come funziona la ricorsione nel mondo reale.
Harvey Lin

49

Nel senso più elementare dell'informatica, la ricorsione è una funzione che chiama se stessa. Supponi di avere una struttura di elenchi collegati:

struct Node {
    Node* next;
};

E vuoi scoprire quanto è lungo un elenco collegato, puoi farlo con la ricorsione:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(Questo ovviamente potrebbe essere fatto anche con un ciclo for, ma è utile come illustrazione del concetto)


@Christopher: Questo è un simpatico, semplice esempio di ricorsione. Nello specifico questo è un esempio di ricorsione della coda. Tuttavia, come ha affermato Andreas, può essere facilmente riscritto (in modo più efficiente) con un ciclo for. Come spiego nella mia risposta, ci sono usi migliori per la ricorsione.
Steve Wortham

2
hai davvero bisogno di un'altra dichiarazione qui?
Adrien Be

1
No, è lì solo per chiarezza.
Andreas Brinck

@SteveWortham: questo non è ricorsivo alla coda come scritto; length(list->next)deve ancora tornare a in length(list)modo che quest'ultimo possa aggiungere 1 al risultato. Se fosse stato scritto per trasmettere la lunghezza fino ad ora, solo allora potremmo dimenticare che il chiamante esisteva. Mi piace int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao

46

Ogni volta che una funzione chiama se stessa, creando un ciclo, questa è ricorsione. Come per qualsiasi cosa, ci sono usi buoni e usi cattivi per la ricorsione.

L'esempio più semplice è la ricorsione in coda in cui l'ultima riga della funzione è una chiamata a se stessa:

int FloorByTen(int num)
{
    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);
}

Tuttavia, questo è un esempio debole, quasi inutile perché può essere facilmente sostituito da un'iterazione più efficiente. Dopo tutto, la ricorsione soffre dell'overhead della chiamata di funzione, che nell'esempio sopra potrebbe essere sostanziale rispetto all'operazione all'interno della funzione stessa.

Quindi l'intera ragione per fare la ricorsione piuttosto che l'iterazione dovrebbe essere quella di sfruttare lo stack di chiamate per fare alcune cose intelligenti. Ad esempio, se chiami una funzione più volte con parametri diversi all'interno dello stesso ciclo, questo è un modo per eseguire la ramificazione . Un classico esempio è il triangolo di Sierpinski .

inserisci qui la descrizione dell'immagine

Puoi disegnare uno di quelli molto semplicemente con la ricorsione, dove lo stack di chiamate si dirama in 3 direzioni:

private void BuildVertices(double x, double y, double len)
{
    if (len > 0.002)
    {
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    }
}

Se provi a fare la stessa cosa con l'iterazione, penso che troverai che ci vuole molto più codice per essere realizzato.

Altri casi d'uso comuni potrebbero includere l'attraversamento di gerarchie, ad es. Crawler di siti Web, confronti di directory, ecc.

Conclusione

In termini pratici, la ricorsione ha più senso ogni volta che è necessaria una ramificazione iterativa.


27

La ricorsione è un metodo per risolvere i problemi basato sulla mentalità divide et impera. L'idea di base è che si prende il problema originale e lo si divide in istanze più piccole (più facilmente risolvibili) di se stesso, si risolvono quelle istanze più piccole (di solito utilizzando di nuovo lo stesso algoritmo) e poi le si riassembla nella soluzione finale.

L'esempio canonico è una routine per generare il Fattoriale di n. Il Fattoriale di n viene calcolato moltiplicando tutti i numeri compresi tra 1 e n. Una soluzione iterativa in C # ha questo aspetto:

public int Fact(int n)
{
  int fact = 1;

  for( int i = 2; i <= n; i++)
  {
    fact = fact * i;
  }

  return fact;
}

Non c'è niente di sorprendente nella soluzione iterativa e dovrebbe avere senso per chiunque abbia familiarità con C #.

La soluzione ricorsiva si trova riconoscendo che l'ennesimo Fattoriale è n * Fatto (n-1). O per dirla in un altro modo, se sai cos'è un particolare numero fattoriale puoi calcolare quello successivo. Ecco la soluzione ricorsiva in C #:

public int FactRec(int n)
{
  if( n < 2 )
  {
    return 1;
  }

  return n * FactRec( n - 1 );
}

La prima parte di questa funzione è nota come Base Case (o talvolta Guard Clause) ed è ciò che impedisce all'algoritmo di funzionare per sempre. Restituisce semplicemente il valore 1 ogni volta che la funzione viene chiamata con un valore pari o inferiore a 1. La seconda parte è più interessante ed è nota come passo ricorsivo . Qui chiamiamo lo stesso metodo con un parametro leggermente modificato (lo decrementiamo di 1) e poi moltiplichiamo il risultato con la nostra copia di n.

Quando viene rilevato per la prima volta, questo può creare confusione, quindi è istruttivo esaminare come funziona quando viene eseguito. Immagina di chiamare FactRec (5). Entriamo nella routine, non veniamo presi dal caso base e così finiamo così:

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

Se rientriamo nel metodo con il parametro 4 non veniamo nuovamente fermati dalla clausola di guardia e quindi finiamo a:

// In FactRec(4)
return 4 * FactRec(3);

Se sostituiamo questo valore di ritorno nel valore di ritorno sopra, otteniamo

// In FactRec(5)
return 5 * (4 * FactRec(3));

Questo dovrebbe darti un indizio su come è arrivata la soluzione finale, quindi seguiremo rapidamente e mostreremo ogni passaggio durante la discesa:

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

Quella sostituzione finale avviene quando viene attivato il caso base. A questo punto abbiamo una semplice formula algrebrica da risolvere che equivale in primo luogo direttamente alla definizione di Fattoriali.

È istruttivo notare che ogni chiamata al metodo comporta l'attivazione di un caso base o una chiamata allo stesso metodo in cui i parametri sono più vicini a un caso base (spesso chiamata chiamata ricorsiva). Se questo non è il caso, il metodo verrà eseguito per sempre.


2
Buona spiegazione, ma penso sia importante notare che questa è semplicemente ricorsione in coda e non offre alcun vantaggio rispetto alla soluzione iterativa. È più o meno la stessa quantità di codice e verrà eseguito più lentamente a causa dell'overhead della chiamata di funzione.
Steve Wortham

1
@SteveWortham: questa non è la ricorsione della coda. Nel passaggio ricorsivo, il risultato di FactRec()deve essere moltiplicato per nprima di tornare.
rvighne

12

La ricorsione risolve un problema con una funzione che chiama se stessa. Un buon esempio di ciò è una funzione fattoriale. Il fattoriale è un problema di matematica in cui il fattoriale di 5, ad esempio, è 5 * 4 * 3 * 2 * 1. Questa funzione risolve questo problema in C # per interi positivi (non testato - potrebbe esserci un bug).

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

9

La ricorsione si riferisce a un metodo che risolve un problema risolvendo una versione più piccola del problema e quindi utilizzando quel risultato più qualche altro calcolo per formulare la risposta al problema originale. Spesso, nel processo di risoluzione della versione più piccola, il metodo risolverà una versione ancora più piccola del problema, e così via, fino a raggiungere un "caso base" che è banale da risolvere.

Ad esempio, per calcolare un fattoriale per il numero X, è possibile rappresentarlo come X times the factorial of X-1. Pertanto, il metodo "ricorre" per trovare il fattoriale di X-1, quindi moltiplica tutto ciò che ha ottenuto Xper dare una risposta finale. Ovviamente, per trovare il fattoriale di X-1, calcolerà prima il fattoriale di X-2e così via. Il caso base sarebbe quando Xè 0 o 1, nel qual caso sa di tornare da 1allora 0! = 1! = 1.


1
Penso che ciò a cui ti riferisci non sia la ricorsione ma il principio di progettazione dell'algoritmo <a href=" en.wikipedia.org/wiki/… e Conquer</a>. Guarda ad esempio il <a href = " en.wikipedia. org / wiki / Ackermann_function "> Ackermans function </a>.
Gabriel Ščerbák

2
No, non mi riferisco a D&C. D&C implica che esistono 2 o più sottoproblemi, la ricorsione di per sé no (ad esempio, l'esempio fattoriale fornito qui non è D&C - è completamente lineare). D&C è essenzialmente un sottoinsieme della ricorsione.
Amber

3
Citato dall'articolo esatto che hai collegato: "Un algoritmo divide et impera funziona suddividendo ricorsivamente un problema in due o più problemi secondari dello stesso tipo (o correlato)"
Amber

Non penso che sia un'ottima spiegazione, poiché la ricorsione in senso stretto non deve risolvere affatto il problema. Potresti semplicemente chiamare te stesso (e overflow).
UK-AL

Sto usando la tua spiegazione in un articolo che sto scrivendo per PHP Master anche se non posso attribuirla a te. Spero non ti dispiaccia.
frostymarvelous

9

Considera un vecchio problema ben noto :

In matematica, il massimo comune divisore (mcd) ... di due o più numeri interi diversi da zero, è il più grande intero positivo che divide i numeri senza resto.

La definizione di gcd è sorprendentemente semplice:

definizione mcd

dove mod è l' operatore modulo (ovvero il resto dopo la divisione intera).

In inglese, questa definizione dice il massimo comun divisore di qualsiasi numero e zero è quel numero, e il massimo comun divisore di due numeri m ed n è il massimo comun divisore di n ed il resto dopo la divisione m dal n .

Se desideri sapere perché funziona, consulta l'articolo di Wikipedia sull'algoritmo euclideo .

Calcoliamo mcd (10, 8) come esempio. Ogni passaggio è uguale a quello immediatamente precedente:

  1. mcd (10, 8)
  2. mcd (10, 10 mod 8)
  3. mcd (8, 2)
  4. mcd (8, 8 mod 2)
  5. mcd (2, 0)
  6. 2

Nel primo passaggio, 8 non è uguale a zero, quindi si applica la seconda parte della definizione. 10 mod 8 = 2 perché 8 va in 10 una volta con un resto di 2. Al punto 3, la seconda parte si applica di nuovo, ma questa volta 8 mod 2 = 0 perché 2 divide 8 senza resto. Al passaggio 5, il secondo argomento è 0, quindi la risposta è 2.

Hai notato che gcd appare sia a sinistra che a destra del segno di uguale? Un matematico direbbe che questa definizione è ricorsiva perché l'espressione che stai definendo ricorre all'interno della sua definizione.

Le definizioni ricorsive tendono ad essere eleganti. Ad esempio, una definizione ricorsiva per la somma di un elenco è

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

dove headè il primo elemento in una lista ed tailè il resto della lista. Nota chesum ricorre all'interno della sua definizione alla fine.

Forse preferiresti invece il valore massimo in un elenco:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

Potresti definire la moltiplicazione di interi non negativi in ​​modo ricorsivo per trasformarla in una serie di aggiunte:

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

Se quella parte sulla trasformazione della moltiplicazione in una serie di aggiunte non ha senso, prova ad espandere alcuni semplici esempi per vedere come funziona.

L'ordinamento di fusione ha una bella definizione ricorsiva:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

Le definizioni ricorsive sono ovunque se sai cosa cercare. Notate come tutte queste definizioni abbiano casi base molto semplici, ad esempio , mcd (m, 0) = m. I casi ricorsivi riducono il problema per arrivare alle risposte facili.

Con questa comprensione, ora puoi apprezzare gli altri algoritmi nell'articolo di Wikipedia sulla ricorsione !


8
  1. Una funzione che chiama se stessa
  2. Quando una funzione può essere (facilmente) scomposta in un'operazione semplice più la stessa funzione su una parte più piccola del problema. Dovrei dire, piuttosto, che questo lo rende un buon candidato per la ricorsione.
  3. Loro fanno!

L'esempio canonico è il fattoriale che assomiglia a:

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

In generale, la ricorsione non è necessariamente veloce (l'overhead delle chiamate di funzione tende ad essere elevato perché le funzioni ricorsive tendono ad essere piccole, vedi sopra) e possono soffrire di alcuni problemi (overflow dello stack chiunque?). Alcuni dicono che tendono ad essere difficili da ottenere "giusti" in casi non banali, ma non ci credo davvero. In alcune situazioni, la ricorsione ha più senso ed è il modo più elegante e chiaro per scrivere una particolare funzione. Va notato che alcuni linguaggi prediligono soluzioni ricorsive e le ottimizzano molto di più (viene in mente LISP).


6

Una funzione ricorsiva è quella che chiama se stessa. Il motivo più comune che ho trovato per usarlo è attraversare una struttura ad albero. Ad esempio, se ho un TreeView con caselle di controllo (pensa all'installazione di un nuovo programma, alla pagina "scegli le funzionalità da installare"), potrei volere un pulsante "controlla tutto" che sarebbe qualcosa del genere (pseudocodice):

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

Quindi puoi vedere che checkRecursively controlla prima il nodo a cui è passato, quindi chiama se stesso per ciascuno dei figli di quel nodo.

Devi stare un po 'attento con la ricorsione. Se entri in un ciclo ricorsivo infinito, otterrai un'eccezione Stack Overflow :)

Non riesco a pensare a un motivo per cui le persone non dovrebbero usarlo, quando appropriato. È utile in alcune circostanze e non in altre.

Penso che, poiché è una tecnica interessante, alcuni programmatori forse finiscono per usarla più spesso di quanto dovrebbero, senza una vera giustificazione. Questo ha dato alla ricorsione un brutto nome in alcuni ambienti.


5

La ricorsione è un'espressione che fa riferimento direttamente o indirettamente a se stessa.

Considera gli acronimi ricorsivi come un semplice esempio:

  • GNU sta per GNU's Not Unix
  • PHP sta per PHP: Hypertext Preprocessor
  • YAML sta per YAML Ain't Markup Language
  • WINE sta per Wine Is Not an Emulator
  • VISA sta per Visa International Service Association

Altri esempi su Wikipedia


4

La ricorsione funziona meglio con quelli che mi piace chiamare "problemi frattali", in cui hai a che fare con una cosa grande che è composta da versioni più piccole di quella cosa grande, ognuna delle quali è una versione ancora più piccola della cosa grande, e così via. Se devi attraversare o cercare qualcosa come un albero o strutture identiche annidate, hai un problema che potrebbe essere un buon candidato per la ricorsione.

Le persone evitano la ricorsione per una serie di motivi:

  1. La maggior parte delle persone (me compreso) ha tagliato i denti di programmazione sulla programmazione procedurale o orientata agli oggetti rispetto alla programmazione funzionale. Per queste persone, l'approccio iterativo (che in genere utilizza i loop) sembra più naturale.

  2. A quelli di noi che hanno tagliato i denti nella programmazione procedurale o orientata agli oggetti è stato spesso detto di evitare la ricorsione perché è soggetta a errori.

  3. Ci viene spesso detto che la ricorsione è lenta. Chiamare e tornare ripetutamente da una routine comporta un sacco di stack push e popping, che è più lento del loop. Penso che alcuni linguaggi lo gestiscano meglio di altri, e questi linguaggi molto probabilmente non sono quelli in cui il paradigma dominante è procedurale o orientato agli oggetti.

  4. Per almeno un paio di linguaggi di programmazione che ho usato, ricordo di aver sentito consigli di non usare la ricorsione se supera una certa profondità perché il suo stack non è così profondo.


4

Un'istruzione ricorsiva è quella in cui definisci il processo di cosa fare dopo come una combinazione degli input e di ciò che hai già fatto.

Ad esempio, prendi fattoriale:

factorial(6) = 6*5*4*3*2*1

Ma è facile vedere fattoriale (6) anche:

6 * factorial(5) = 6*(5*4*3*2*1).

Quindi in generale:

factorial(n) = n*factorial(n-1)

Ovviamente, la cosa complicata della ricorsione è che se vuoi definire le cose in base a ciò che hai già fatto, ci deve essere un punto di partenza.

In questo esempio, creiamo un caso speciale definendo fattoriale (1) = 1.

Ora lo vediamo dal basso verso l'alto:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

Poiché abbiamo definito fattoriale (1) = 1, raggiungiamo il "fondo".

In generale, le procedure ricorsive hanno due parti:

1) La parte ricorsiva, che definisce alcune procedure in termini di nuovi input combinati con quanto "già fatto" tramite la stessa procedura. (cioè factorial(n) = n*factorial(n-1))

2) Una parte di base, che assicura che il processo non si ripeta per sempre dandogli un punto di partenza (es factorial(1) = 1 )

All'inizio può creare un po 'di confusione, ma basta guardare un mucchio di esempi e tutto dovrebbe venire insieme. Se vuoi una comprensione molto più profonda del concetto, studia l'induzione matematica. Inoltre, tieni presente che alcune lingue ottimizzano per le chiamate ricorsive mentre altre no. È abbastanza facile creare funzioni ricorsive follemente lente se non stai attento, ma ci sono anche tecniche per renderle performanti nella maggior parte dei casi.

Spero che questo ti aiuti...


4

Mi piace questa definizione:
nella ricorsione, una routine risolve da sola una piccola parte di un problema, divide il problema in parti più piccole e quindi chiama se stessa per risolvere ciascuna delle parti più piccole.

Mi piace anche la discussione di Steve McConnell sulla ricorsione in Code Complete, dove critica gli esempi usati nei libri di informatica sulla ricorsione.

Non usare la ricorsione per fattoriali o numeri di Fibonacci

Un problema con i libri di testo di informatica è che presentano stupidi esempi di ricorsione. Gli esempi tipici sono il calcolo di un fattoriale o il calcolo di una sequenza di Fibonacci. La ricorsione è uno strumento potente ed è davvero stupido usarla in entrambi i casi. Se un programmatore che ha lavorato per me usasse la ricorsione per calcolare un fattoriale, assumerei qualcun altro.

Ho pensato che questo fosse un punto molto interessante da sollevare e potrebbe essere un motivo per cui la ricorsione viene spesso fraintesa.

EDIT: Questo non è stato uno scavo alla risposta di Dav - non avevo visto quella risposta quando ho pubblicato questo


6
La maggior parte del motivo per cui i fattoriali o le sequenze di fibonacci vengono utilizzati come esempi è perché sono elementi comuni che sono definiti in modo ricorsivo, e quindi si prestano naturalmente a esempi di ricorsione per calcolarli, anche se questo non è effettivamente il metodo migliore da un punto di vista CS.
Amber

Sono d'accordo - Ho appena scoperto mentre stavo leggendo il libro che era un punto interessante da sollevare nel mezzo di una sezione sulla ricorsione
Robben_Ford_Fan_boy

4

1.) Un metodo è ricorsivo se può chiamare se stesso; direttamente:

void f() {
   ... f() ... 
}

o indirettamente:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2.) Quando usare la ricorsione

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

3.) Le persone usano la ricorsione solo quando è molto complesso scrivere codice iterativo. Ad esempio, le tecniche di attraversamento dell'albero come preorder, postorder possono essere rese sia iterative che ricorsive. Ma di solito usiamo ricorsivo a causa della sua semplicità.


Che ne dici di ridurre la complessità quando dividi e conquista riguardo ai perf?
mfrachet

4

Ecco un semplice esempio: quanti elementi in un set. (ci sono modi migliori per contare le cose, ma questo è un bell'esempio ricorsivo semplice.)

Innanzitutto, abbiamo bisogno di due regole:

  1. se il set è vuoto, il conteggio degli elementi nel set è zero (duh!).
  2. se il set non è vuoto, il conteggio è uno più il numero di elementi nel set dopo che un elemento è stato rimosso.

Supponi di avere un set come questo: [xxx]. contiamo quanti articoli ci sono.

  1. l'insieme è [xxx] che non è vuoto, quindi applichiamo la regola 2. il numero di elementi è uno più il numero di elementi in [xx] (cioè abbiamo rimosso un elemento).
  2. l'insieme è [xx], quindi applichiamo di nuovo la regola 2: uno + numero di elementi in [x].
  3. l'insieme è [x], che corrisponde ancora alla regola 2: uno + numero di elementi in [].
  4. Ora l'insieme è [], che soddisfa la regola 1: il conteggio è zero!
  5. Ora che conosciamo la risposta nel passaggio 4 (0), possiamo risolvere il passaggio 3 (1 + 0)
  6. Allo stesso modo, ora che conosciamo la risposta al passaggio 3 (1), possiamo risolvere il passaggio 2 (1 + 1)
  7. E finalmente ora che conosciamo la risposta nel passaggio 2 (2), possiamo risolvere il passaggio 1 (1 + 2) e ottenere il conteggio degli elementi in [xxx], che è 3. Evviva!

Possiamo rappresentarlo come:

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

Quando applichi una soluzione ricorsiva, di solito hai almeno 2 regole:

  • la base, il semplice caso che afferma cosa succede quando hai "esaurito" tutti i tuoi dati. Di solito si tratta di una variazione di "se non hai dati da elaborare, la tua risposta è X"
  • la regola ricorsiva, che afferma cosa succede se hai ancora dati. Di solito si tratta di una sorta di regola che dice "fai qualcosa per rimpicciolire il tuo set di dati e riapplica le tue regole al set di dati più piccolo".

Se traduciamo quanto sopra in pseudocodice, otteniamo:

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

Ci sono molti più esempi utili (attraversare un albero, per esempio) che sono sicuro che altre persone copriranno.


3

Bene, questa è una definizione abbastanza decente che hai. E anche wikipedia ha una buona definizione. Quindi aggiungerò un'altra (probabilmente peggiore) definizione per te.

Quando le persone si riferiscono alla "ricorsione", di solito parlano di una funzione che hanno scritto che chiama se stessa ripetutamente finché non ha terminato il suo lavoro. La ricorsione può essere utile quando si attraversano le gerarchie nelle strutture dati.


3

Un esempio: una definizione ricorsiva di una scala è: Una scala consiste di: - un singolo gradino e una scala (ricorsione) - o solo un singolo gradino (terminazione)


2

Per ricorrere a un problema risolto: non fare nulla, il gioco è fatto.
Per ricorrere a un problema aperto: eseguire il passaggio successivo, quindi ricorrere al resto.


2

In parole povere: supponi di poter fare 3 cose:

  1. Prendi una mela
  2. Annotare i segni di riscontro
  3. Conta i segni di riscontro

Hai molte mele davanti a un tavolo e vuoi sapere quante mele ci sono.

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

Il processo di ripetizione della stessa cosa finché non hai finito è chiamato ricorsione.

Spero che questa sia la risposta in "inglese semplice" che stai cercando!


1
Aspetta, ho molti segni di conteggio davanti a me su un tavolo, e ora voglio sapere quanti segni di conteggio ci sono. Posso in qualche modo usare le mele per questo?
Christoffer Hammarström

Se prendi una mela da terra (quando le hai messe lì durante il processo) e la metti sul tavolo ogni volta che gratti un segno di conteggio della lista fino a quando non sono rimasti segni di conteggio, sono abbastanza sicuro che tu finire con una quantità di mele sul tavolo pari al numero di segni di conteggio che avevi. Ora conta solo quelle mele per un successo immediato! (nota: questo processo non è più ricorsione, ma un ciclo infinito)
Bastiaan Linders

2

Una funzione ricorsiva è una funzione che contiene una chiamata a se stessa. Una struttura ricorsiva è una struttura che contiene un'istanza di se stessa. Puoi combinare i due come una classe ricorsiva. La parte fondamentale di un elemento ricorsivo è che contiene un'istanza / chiamata di se stesso.

Considera due specchi uno di fronte all'altro. Abbiamo visto il preciso effetto infinito che producono. Ogni riflesso è un'istanza di uno specchio, che è contenuta in un'altra istanza di uno specchio, ecc. Lo specchio che contiene un riflesso di se stesso è la ricorsione.

Un albero di ricerca binario è un buon esempio di programmazione di ricorsione. La struttura è ricorsiva con ogni nodo contenente 2 istanze di un nodo. Anche le funzioni per lavorare su un albero di ricerca binario sono ricorsive.


2

Questa è una vecchia domanda, ma voglio aggiungere una risposta dal punto di vista logistico (cioè non dal punto di vista della correttezza dell'algoritmo o dal punto di vista delle prestazioni).

Uso Java per lavoro e Java non supporta la funzione annidata. In quanto tale, se voglio fare la ricorsione, potrei dover definire una funzione esterna (che esiste solo perché il mio codice urta contro la regola burocratica di Java), oppure potrei dover rifattorizzare il codice del tutto (cosa che odio davvero fare).

Pertanto, spesso evito la ricorsione e utilizzo invece l'operazione di stack, perché la ricorsione stessa è essenzialmente un'operazione di stack.


1

Vuoi usarlo ogni volta che hai una struttura ad albero. È molto utile nella lettura di XML.


1

La ricorsione come si applica alla programmazione è fondamentalmente chiamare una funzione dall'interno della propria definizione (dentro se stessa), con parametri diversi in modo da compiere un compito.


1

"Se ho un martello, fai sembrare tutto un chiodo."

La ricorsione è una strategia di risoluzione dei problemi per problemi enormi , in cui ad ogni passo "trasforma 2 piccole cose in una cosa più grande", ogni volta con lo stesso martello.

Esempio

Supponiamo che la tua scrivania sia ricoperta da un disordine disorganizzato di 1024 fogli. Come si fa a creare una pila di fogli ordinata e pulita dal disordine, usando la ricorsione?

  1. Dividi: distribuisci tutti i fogli, in modo da avere un solo foglio in ogni "pila".
  2. Conquistare:
    1. Vai in giro, mettendo ogni foglio sopra un altro foglio. Ora hai pile di 2.
    2. Vai in giro, mettendo ogni pila da 2 sopra un'altra pila da 2. Ora hai pile di 4.
    3. Vai in giro, mettendo ogni pila da 4 sopra un'altra pila da 4. Ora hai pile di 8.
    4. ... ancora e ancora ...
    5. Ora hai un'enorme pila di 1024 fogli!

Si noti che questo è abbastanza intuitivo, a parte contare tutto (che non è strettamente necessario). Potresti non arrivare fino a pile da 1 foglio, in realtà, ma potresti e funzionerebbe comunque. La parte importante è il martello: con le braccia, puoi sempre mettere una pila sopra l'altra per creare una pila più grande, e non importa (entro limiti ragionevoli) quanto sia grande una pila.


6
Stai descrivendo divide et impera. Sebbene questo sia un esempio di ricorsione, non è affatto l'unico.
Konrad Rudolph

Va bene. Non sto cercando di catturare [il mondo della ricorsione] [1] in una frase, qui. Voglio una spiegazione intuitiva. [1]: facebook.com/pages/Recursion-Fairy/269711978049
Andres Jaan Tack

1

La ricorsione è il processo in cui una chiamata al metodo è in grado di eseguire un determinato compito. Riduce la ridondanza del codice. La maggior parte delle funzioni o dei metodi ricorsivi deve avere una condizione per interrompere la chiamata ricussiva, cioè impedire che si chiami se una condizione è soddisfatta - questo impedisce la creazione di un ciclo infinito. Non tutte le funzioni sono adatte per essere utilizzate in modo ricorsivo.


1

ehi, scusa se la mia opinione è d'accordo con qualcuno, sto solo cercando di spiegare la ricorsione in un inglese semplice.

supponi di avere tre manager: Jack, John e Morgan. Jack gestisce 2 programmatori, John - 3 e Morgan - 5. darai a ogni manager 300 $ e vorrai sapere quanto costerebbe. La risposta è ovvia, ma cosa succede se 2 dei dipendenti di Morgan sono anche manager?

QUI arriva la ricorsione. si parte dalla cima della gerarchia. il costo estivo è di 0 $. inizi con Jack, quindi controlla se ha dei manager come dipendenti. se trovi qualcuno di loro, controlla se hanno dei manager come dipendenti e così via. Aggiungi 300 $ al costo estivo ogni volta che trovi un manager. quando hai finito con Jack, vai da John, dai suoi dipendenti e poi da Morgan.

Non saprai mai quanti cicli farai prima di ottenere una risposta, anche se sai quanti manager hai e quanto budget puoi spendere.

La ricorsione è un albero, con rami e foglie, chiamati rispettivamente genitori e figli. Quando si utilizza un algoritmo di ricorsione, si costruisce più o meno consapevolmente un albero a partire dai dati.


1

In parole povere, ricorsione significa ripetere qualcosa ancora e ancora.

Nella programmazione un esempio è chiamare la funzione al suo interno.

Guarda il seguente esempio di calcolo fattoriale di un numero:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}

1
In parole povere, ripetere qualcosa ancora e ancora è chiamato iterazione.
toon81

1

Qualsiasi algoritmo mostra una ricorsione strutturale su un tipo di dati se consiste fondamentalmente in un'istruzione switch con un caso per ogni caso del tipo di dati.

ad esempio, quando lavori su un tipo

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

un algoritmo ricorsivo strutturale avrebbe la forma

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

questo è davvero il modo più ovvio per scrivere qualsiasi algoritmo che funzioni su una struttura dati.

ora, quando guardi gli interi (beh, i numeri naturali) come definiti usando gli assiomi di Peano

 integer = 0 | succ(integer)

si vede che un algoritmo ricorsivo strutturale su interi assomiglia a questo

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

la ben nota funzione fattoriale è circa l'esempio più banale di questa forma.


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.