Ricorsione senza fattoriale, numeri di Fibonacci ecc


48

Quasi ogni articolo che posso trovare sulla ricorsione include esempi di numeri fattoriali o di Fibonacci, che sono:

  1. Matematica
  2. Inutile nella vita reale

Ci sono alcuni esempi interessanti di codice non matematico per insegnare la ricorsione?

Sto pensando ad algoritmi di divisione e conquista, ma di solito coinvolgono strutture dati complesse.


26
Mentre la tua domanda è completamente valida, esiterei a chiamare i numeri di Fibonacci inutili nella vita reale . Lo stesso vale per fattoriale .
Zach L,

2
The Little Schemer è un intero libro sulla ricorsione che non usa mai Fact o Fib. junix-linux-config.googlecode.com/files/…
Eric Wilson,

5
@Zach: Anche così, la ricorsione è un modo orribile per implementare i numeri di Fibonacci, a causa del tempo di esecuzione esponenziale.
dan04,

2
@ dan04: la ricorsione è un modo orribile di implementare quasi tutto a causa della possibilità di overflow dello stack nella maggior parte delle lingue.
R ..

5
@ dan04 a meno che la tua lingua non sia abbastanza intelligente da implementare l'ottimizzazione delle chiamate di coda come la maggior parte dei linguaggi funzionali, nel qual caso funziona perfettamente
Zachary K

Risposte:


107

Le strutture di directory / file sono il miglior esempio di utilizzo per la ricorsione, perché tutti le comprendono prima di iniziare, ma tutto ciò che coinvolge strutture simili ad alberi lo farà.

void GetAllFilePaths(Directory dir, List<string> paths)
{
    foreach(File file in dir.Files)
    {
        paths.Add(file.Path);
    }

    foreach(Directory subdir in dir.Directories)
    {
        GetAllFilePaths(subdir, paths)
    }
}

List<string> GetAllFilePaths(Directory dir)
{
    List<string> paths = new List<string>();
    GetAllFilePaths(dir, paths);
    return paths;
}

1
Grazie, penso che andrò con il filesystem. È qualcosa di concreto e può essere usato per molti esempi del mondo reale.
sinapsi,

9
Nota: il comando unix spesso esclude l'opzione -r (cp o rm per esempio). -r sta per ricorsivo.
deadalnix,

7
devi stare un po 'attento qui, poiché nel mondo reale i file system sono in realtà un grafico diretto non necessariamente un albero, se supportato dal file system, hard link ecc. possono creare join e cicli
jk.

1
@jk: le directory non possono essere hard link, quindi modulo alcune foglie che potrebbero apparire in più di una posizione, e supponendo che tu escluda i symlink, i filesystem del mondo reale sono alberi.
R ..

1
ci sono altre peculiarità in alcuni file system per le directory, ad esempio punti di analisi NTFS. il mio punto è che il codice che non è specificamente a conoscenza di questo può avere risultati inaspettati sui file system del mondo reale
jk.

51

Cerca cose che coinvolgono strutture ad albero. Un albero è relativamente facile da comprendere e la bellezza di una soluzione ricorsiva diventa evidente molto prima rispetto alle strutture di dati lineari come gli elenchi.

Cose a cui pensare:

  • filesystem: quelli sono fondamentalmente alberi; un bel compito di programmazione sarebbe quello di recuperare tutte le immagini .jpg in una determinata directory e in tutte le sue sottodirectory
  • antenato - dato un albero genealogico, trova il numero di generazioni che devi camminare per trovare un antenato comune; oppure verifica se due persone nella struttura appartengono alla stessa generazione; o controlla se due persone nell'albero possono legalmente sposarsi (dipende dalla giurisdizione :)
  • Documenti simili a HTML: converti tra la rappresentazione seriale (testo) di un documento e un albero DOM; eseguire operazioni su sottoinsiemi di un DOM (forse anche implementare un sottoinsieme di xpath?); ...

Questi sono tutti correlati agli scenari del mondo reale e possono tutti essere utilizzati in applicazioni di significato nel mondo reale.


Ovviamente, va notato che ogni volta che si ha la libertà di progettare la propria struttura ad albero, è quasi sempre meglio mantenere puntatori / riferimenti al genitore / al prossimo fratello / ecc. nei nodi in modo da poter iterare sull'albero senza ricorsione.
R ..

1
Che cosa hanno a che fare i puntatori con esso? Anche quando hai indicazioni per il primo figlio, il prossimo fratello e il genitore, devi comunque camminare attraverso l'albero in qualche modo, e in alcuni casi, la ricorsione è il modo più fattibile.
tdammers,

41

https://stackoverflow.com/questions/105838/real-world-examples-of-recursion

  • modellando un'infezione contagiosa
  • generare geometria
  • gestione delle directory
  • ordinamento

https://stackoverflow.com/questions/2085834/how-did-you-practically-use-recursion

  • raytracing
  • scacchi
  • analisi del codice sorgente (grammatica della lingua)

https://stackoverflow.com/questions/4945128/what-is-a-good-example-of-recursion-other-than-generating-a-fibonacci-sequence

  • Ricerca BST
  • Torri di Hanoi
  • ricerca palindromo

https://stackoverflow.com/questions/126756/examples-of-recursive-functions

  • Dà una bella storia in lingua inglese che illustra la ricorsione di una favola della buonanotte.

10
Sebbene ciò possa teoricamente rispondere alla domanda, sarebbe preferibile includere qui le parti essenziali di tali domande e risposte e fornire i collegamenti come riferimento. Se le domande vengono mai rimosse da SO, la tua risposta sarà completamente inutile.
Adam Lear

@Anna Bene, gli utenti non possono cancellare le loro domande, quindi quanto è probabile che accada?
Vemv,

4
@vemv Elimina voti, moderatori, regole su cosa cambia argomento ... può succedere. In entrambi i casi, sarebbe preferibile avere una risposta più completa qui che inviare un visitatore a quattro diverse pagine immediatamente.
Adam Lear

@Anna: seguendo questa linea di pensiero, ogni domanda chiusa come "duplicato esatto" dovrebbe avere la risposta dall'originale incollato in. Questa domanda È un duplicato esatto (delle domande su SO), perché dovrebbe ricevere un trattamento diverso da quello esatto duplicati di domande sui programmatori?
SF.

1
@SF Se potessimo chiuderlo come duplicato, lo faremmo, ma i duplicati tra siti non sono supportati. I programmatori sono un sito separato, quindi idealmente le risposte qui dovrebbero usare SO come qualsiasi altro riferimento, non delegarlo completamente. Non è diverso dal dire semplicemente "la tua risposta è in questo libro" - tecnicamente vero, ma non può essere usato immediatamente senza consultare il riferimento.
Adam Lear

23

Ecco alcuni altri problemi pratici che mi vengono in mente:

  • Unisci ordinamento
  • Ricerca binaria
  • Traversal, Insertion and Removal on Trees (ampiamente utilizzato nelle applicazioni di database)
  • Generatore di permutazioni
  • Risolutore di Sudoku (con backtracking)
  • Controllo ortografico (di nuovo con backtracking)
  • Analisi della sintassi (.eg, un programma che converte il prefisso in notazione postfissa)

11

QuickSort sarebbe il primo che mi viene in mente. Anche la ricerca binaria è un problema ricorsivo. Oltre a ciò ci sono intere classi di problemi che le soluzioni cadono quasi gratis quando inizi a lavorare con la ricorsione.


3
La ricerca binaria è spesso formulata come un problema ricorsivo, ma è banale (e spesso preferibile) implementarla in modo imperativo.
soffice

A seconda della lingua utilizzata, il codice può o meno essere esplicitamente ricorsivo o imperativo o ricorsivo. Ma è ancora un algoritmo ricorsivo in quanto si sta rompendo il problema in blocchi sempre più piccoli per arrivare alla soluzione.
Zaccaria K,

2
@Zachary: gli algoritmi che possono essere implementati con la ricorsione della coda (come la ricerca binaria) sono in una classe spaziale fondamentalmente diversa rispetto a quelli che richiedono una ricorsione reale (o le proprie strutture di stato con requisiti di spazio altrettanto costosi). Non credo sia utile insegnare loro come se fossero gli stessi.
R ..

8

Ordinamento, definito in modo ricorsivo in Python.

def sort( a ):
    if len(a) == 1: return a
    part1= sort( a[:len(a)//2] )
    part2= sort( a[len(a)//2:] )
    return merge( part1, part2 )

Unisci, definito in modo ricorsivo.

def merge( a, b ):
    if len(b) == 0: return a
    if len(a) == 0: return b
    if a[0] < b[0]:
        return [ a[0] ] + merge(a[1:], b)
    else:
        return [ b[0] ] + merge(a, b[1:]) 

Ricerca lineare, definita in modo ricorsivo.

def find( element, sequence ):
    if len(sequence) == 0: return False
    if element == sequence[0]: return True
    return find( element, sequence[1:] )

Ricerca binaria, definita in modo ricorsivo.

def binsearch( element, sequence ):
    if len(sequence) == 0: return False
    mid = len(sequence)//2
    if element < mid: 
        return binsearch( element, sequence[:mid] )
    else:
        return binsearch( element, sequence[mid:] )

6

In un certo senso, la ricorsione è tutta una questione di dividere e conquistare soluzioni, che sta trasformando lo spazio del problema in uno più piccolo per aiutare a trovare la soluzione per un problema semplice, e poi di solito tornando a ricostruire il problema originale per comporre la risposta giusta.

Alcuni esempi che non riguardano la matematica per insegnare la ricorsione (almeno quei problemi che ricordo dai miei anni universitari):

Questi sono esempi dell'utilizzo di Backtracking per risolvere il problema.

Altri problemi sono i classici del dominio dell'intelligenza artificiale: utilizzo della ricerca approfondita, ricerca di percorsi, pianificazione.

Tutti questi problemi riguardano una sorta di struttura di dati "complessa", ma se non vuoi insegnarlo con la matematica (numeri), le tue scelte potrebbero essere più limitate. Potresti voler iniziare a insegnare con una struttura di dati di base, come un Elenco collegato. Ad esempio, rappresentando i numeri naturali usando un elenco:

0 = elenco vuoto 1 = elenco con un nodo. 2 = elenco con 2 nodi. ...

quindi definire la somma di due numeri in termini di questa struttura di dati in questo modo: Vuoto + N = N Nodo (X) + N = Nodo (X + N)


5

Towers of Hanoi è una buona idea per aiutare a imparare la ricorsione.

Ci sono molte soluzioni sul web in molte lingue diverse.


3
Questo è in realtà a mio avviso un altro cattivo esempio. Prima di tutto, non è realistico; non è un problema che le persone abbiano effettivamente. In secondo luogo, ci sono soluzioni semplici non ricorsive. (Uno è: numera i dischi. Non spostare mai un disco su un disco della stessa parità e non annullare mai l'ultima mossa che hai fatto. Se segui queste due regole, risolverai il puzzle con la soluzione ottimale. Non è necessaria alcuna ricorsione. )
Eric Lippert,

5

Un rivelatore di Palindrome:

Inizia con una stringa: "ABCDEEDCBA" Se i caratteri di inizio e fine sono uguali, quindi ricorre e seleziona "BCDEEDCB", ecc ...


6
È anche banale da risolvere senza ricorsione e, IMHO, meglio risolto senza di essa.
Blrfl,

3
Concordato, ma il PO ha chiesto espressamente esempi di insegnamento con un uso minimo delle strutture di dati.
NWS,

5
Non è un buon esempio di insegnamento se i tuoi studenti possono immediatamente pensare a una soluzione non ricorsiva. Perché qualcuno dovrebbe prestare attenzione quando il tuo esempio è "Ecco qualcosa di banale da fare con un ciclo. Ora ti mostrerò un modo più difficile senza una ragione apparente."
Ripristina Monica il


4

Nei linguaggi di programmazione funzionale, quando non sono disponibili funzioni di ordine superiore, viene utilizzata la ricorsione al posto degli anelli imperativi per evitare lo stato mutabile.

F # è un linguaggio funzionale impuro che consente entrambi gli stili, quindi li confronterò entrambi qui. La seguente somma di tutti i numeri in un elenco.

Ciclo imperativo con variabile mutabile

let xlist = [1;2;3;4;5;6;7;8;9;10]
let mutable sum = 0
for x in xlist do
    sum <- sum + x

Ciclo ricorsivo senza stato mutabile

let xlist = [1;2;3;4;5;6;7;8;9;10]
let rec loop sum xlist = 
    match xlist with
    | [] -> sum
    | x::xlist -> loop (sum + x) xlist
let sum = loop 0 xlist

Si noti che questo tipo di aggregazione viene acquisito nella funzione di ordine superiore List.folde potrebbe essere scritto come List.fold (+) 0 xlisto addirittura ancora più semplicemente con la funzione di convenienza List.sumcome solo List.sum xlist.


meriteresti più punti rispetto a solo +1 da me. F # senza ricorsione sarebbe molto noioso, questo è ancora più vero per Haskell che non ha costrutti ciclici SOLO ricorsione!
schoetbi,

3

Ho usato pesantemente la ricorsione nel gioco giocando AI. Scrivendo in C ++, ho fatto uso di una serie di circa 7 funzioni che si chiamano tra loro in ordine (con la prima funzione che ha un'opzione per bypassare tutte quelle e chiamare invece una catena di altre 2 funzioni). La funzione finale in entrambe le catene ha chiamato nuovamente la prima funzione fino a quando la profondità rimanente che volevo cercare è passata a 0, nel qual caso la funzione finale chiamerebbe la mia funzione di valutazione e restituire il punteggio della posizione.

Le molteplici funzioni mi hanno permesso di diramare facilmente in base alle decisioni dei giocatori o agli eventi casuali nel gioco. Farei uso del pass-by-reference ogni volta che potevo, perché stavo passando attorno a strutture di dati molto grandi, ma a causa della struttura del gioco, era molto difficile avere una "mossa annullata" nella mia ricerca, quindi Utilizzerei il pass-by-value in alcune funzioni per mantenere invariati i miei dati originali. Per questo motivo, passare a un approccio basato su loop anziché a un approccio ricorsivo si è rivelato troppo difficile.

Puoi vedere una struttura di base di questo tipo di programma, vedi https://secure.wikimedia.org/wikipedia/en/wiki/Minimax#Pseudocode


3

Un esempio di vita reale molto valido nel mondo degli affari è qualcosa chiamato "distinta materiali". Questi sono i dati che rappresentano tutti i componenti che compongono un prodotto finito. Ad esempio, usiamo una bicicletta. Una bicicletta ha manubri, ruote, un telaio, ecc. E ciascuno di questi componenti può avere sottocomponenti. ad esempio, la ruota può avere raggi, uno stelo di valvola, ecc. In genere questi sono rappresentati in una struttura ad albero.

Ora per eseguire query su qualsiasi informazione aggregata sulla DBA o per modificare elementi in una DBA spesso si ricorre alla ricorsione.

    class BomPart
    {
        public string PartNumber { get; set; }
        public string Desription { get; set; }
        public int Quantity { get; set; }
        public bool Plastic { get; set; }
        public List<BomPart> Components = new List<BomPart>();
    }

E una chiamata ricorsiva di esempio ...

    static int ComponentCount(BomPart part)
    {
        int subCount = 0;
        foreach(BomPart p in part.Components)
            subCount += ComponentCount(p);
        return part.Quantity * Math.Max(1,subCount);

    }

Ovviamente la classe BomPart avrebbe molti più campi. Potrebbe essere necessario capire quanti componenti di plastica hai, quanta fatica ci vuole per costruire una parte completa, ecc. Tutto ciò torna all'utilità della ricorsione su una struttura ad albero.


Una distinta materiali può essere un grath diretto piuttosto che un albero, ad esempio la stessa specifica di vite può essere utilizzata da più di un componente.
Ian,

-1

Le relazioni familiari sono dei buoni esempi perché tutti li capiscono intuitivamente:

ancestor(joe, me) = (joe == me) 
                    OR ancestor(joe, me.father) 
                    OR ancestor(joe, me.mother);

in quale lingua è scritto questo codice?
törzsmókus,

@ törzsmókus Nessuna lingua specifica. La sintassi è abbastanza vicina a C, Obj-C, C ++, Java e molti altri linguaggi, ma se si desidera un codice reale potrebbe essere necessario sostituire un operatore appropriato come ||per OR.
Caleb,

-1

Una recursione interiore piuttosto inutile ma che mostra un buon funzionamento è ricorsiva strlen():

size_t strlen( const char* str )
{
    if( *str == 0 ) {
       return 0;
    }
    return 1 + strlen( str + 1 );
}

No matematica - una funzione molto semplice. Ovviamente non lo implementate in modo ricorsivo nella vita reale, ma è una buona demo di ricorsione.


-2

Un altro problema di ricorsione nel mondo reale a cui gli studenti possono riferirsi è quello di costruire il proprio crawler web che tira le informazioni da un sito Web e segue tutti i collegamenti all'interno di quel sito (e tutti i collegamenti da tali collegamenti, ecc.).


2
Questo è generalmente meglio servito da una coda di processo rispetto alla ricorsione in senso tradizionale.
soffice

-2

Ho risolto un problema con un modello di cavaliere (su una scacchiera) usando un programma ricorsivo. Avresti dovuto spostare il cavaliere in modo che toccasse ogni quadrato tranne alcuni quadrati segnati.

Semplicemente:

mark your "Current" square
gather a list of free squares that are valid moves
are there no valid moves?
    are all squares marked?
        you win!
for each free square
    recurse!
clear the mark on your current square.
return.    

Molti tipi di scenari "think-ahead" possono essere elaborati testando le possibilità future in un albero come questo.

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.