L'idée de récursivité n'est pas très courante dans le monde réel. Donc, cela semble un peu déroutant pour les programmeurs débutants. Mais je suppose qu'ils s'habituent progressivement au concept. Alors, quelle peut être une belle explication pour eux de saisir l'idée facilement?
Pour expliquer récursivité , j'utilise une combinaison d'explications différentes, généralement pour essayer de:
Pour commencer, Wolfram | Alpha le définit en termes plus simples que Wikipedia :
Expression telle que chaque terme est généré en répétant une opération mathématique particulière.
Si votre élève (ou la personne que vous expliquez aussi, à partir de maintenant je dirai élève) a au moins une formation mathématique, il a évidemment déjà rencontré la récursivité en étudiant les séries et leur notion de récursivité et leur relation de récurrence .
Une très bonne façon de commencer est alors de démontrer avec une série et de dire que c'est tout simplement de quoi parle la récursivité:
Habituellement, vous obtenez au mieux un "huh huh, whatev '" parce qu'ils ne l'utilisent toujours pas, ou plus probablement juste un ronflement très profond.
Pour le reste, c'est en fait une version détaillée de ce que j'ai présenté dans le Addendum de ma réponse pour le question que vous avez pointée concernant les pointeurs (mauvais jeu de mots).
À ce stade, mes élèves savent généralement comment imprimer quelque chose à l'écran. En supposant que nous utilisons C, ils savent imprimer un seul caractère en utilisant write
ou printf
. Ils connaissent également les boucles de contrôle.
J'ai généralement recours à quelques problèmes de programmation répétitifs et simples jusqu'à ce qu'ils l'obtiennent:
Factorielle
Factorielle est un concept mathématique très simple à comprendre, et l'implémentation est très proche de sa représentation mathématique. Cependant, ils pourraient ne pas l'obtenir au début.
Alphabets
La version alphabétique est intéressante pour leur apprendre à réfléchir à l'ordre de leurs énoncés récursifs. Comme avec les pointeurs, ils vous lanceront simplement des lignes au hasard. Le but est de les amener à réaliser qu'une boucle peut être inversée soit en modifiant les conditions [~ # ~] ou [~ # ~] par inverser simplement l'ordre des instructions dans votre fonction. C'est là que l'impression de l'alphabet aide, car c'est quelque chose de visuel pour eux. Demandez-leur simplement d'écrire une fonction qui imprimera un caractère pour chaque appel, et s'appelle de manière récursive pour écrire le suivant (ou précédent).
Fans de FP, ignorez le fait que l'impression de trucs sur le flux de sortie est un effet secondaire pour l'instant ... Ne soyons pas trop ennuyeux sur le front FP. (Mais si vous utilisez un langage prenant en charge les listes, n'hésitez pas à concaténer une liste à chaque itération et à simplement imprimer le résultat final. Mais généralement, je les commence par C, qui n'est malheureusement pas le meilleur pour ce type de problèmes et de concepts) .
Exponentiation
Le problème d'exponentiation est légèrement plus difficile ( à ce stade d'apprentissage). Évidemment, le concept est exactement le même que pour un factoriel et il n'y a pas de complexité supplémentaire ... sauf que vous avez plusieurs paramètres. Et cela suffit généralement à embrouiller les gens et à les rejeter au début.
Sa forme simple:
peut être exprimé ainsi par récurrence:
Plus difficile
Une fois que ces problèmes simples ont été montrés ET réimplémentés dans les tutoriels, vous pouvez donner des exercices légèrement plus difficiles (mais très classiques):
Remarque: Encore une fois, certains d'entre eux ne sont vraiment pas plus difficiles ... Ils abordent simplement le problème exactement sous le même angle, ou légèrement différent. Mais la pratique rend parfait.
Une référence
Une lecture ne fait jamais de mal. Eh bien, au début, ils se sentiront encore plus perdus. C'est le genre de chose qui grandit sur vous et qui se trouve à l'arrière de votre tête jusqu'au jour où vous réalisez que vous l'avez enfin. Et puis vous repensez à ces trucs que vous lisez. Les pages récursion , récursivité en informatique et relation de récurrence sur Wikipédia feraient pour l'instant.
Niveau/Profondeur
En supposant que vos élèves n'ont pas beaucoup d'expérience en codage, fournissez des talons de code. Après les premières tentatives, donnez-leur une fonction d'impression qui peut afficher le niveau de récursivité. L'impression de la valeur numérique du niveau aide.
Le diagramme de la pile comme tiroirs
L'indentation d'un résultat imprimé (ou de la sortie du niveau) aide également, car elle donne une autre représentation visuelle de ce que fait votre programme, en ouvrant et en fermant des contextes de pile comme des tiroirs ou des dossiers dans un explorateur de système de fichiers.
Acronymes récursifs
Si votre élève est déjà un peu familiarisé avec la culture informatique, il se peut qu'il utilise déjà certains projets/logiciels avec des noms utilisant acronymes récursifs . C'est une tradition qui existe depuis un certain temps, en particulier dans les projets GNU. Certains exemples incluent:
Récursif:
Mutuellement récursif:
Demandez-leur d'essayer de trouver le leur.
De même, il existe de nombreuses occurrences d'humour récursif, comme la correction recherche récursive de Google . Pour plus d'informations sur la récursivité, lisez cette réponse .
Quelques problèmes avec lesquels les gens ont généralement du mal et pour lesquels vous avez besoin de connaître les réponses.
Pourquoi, oh Dieu pourquoi ???
Pourquoi ferais-tu ça? Une bonne raison, mais non évidente, est qu'il est souvent plus simple d'exprimer un problème de cette façon. Une raison pas si bonne mais évidente est que cela prend souvent moins de frappe (ne les faites pas se sentir tellement l33t si vous utilisez simplement la récursivité ...).
Certains problèmes sont certainement plus faciles à résoudre lors de l'utilisation d'une approche récursive. En règle générale, tout problème que vous pouvez résoudre en utilisant un paradigme Divide and Conquer s'adaptera à un algorithme de récursion à plusieurs branches.
Qu'est-ce que N à nouveau ??
Pourquoi mon n
ou (quel que soit le nom de votre variable) est-il différent à chaque fois? Les débutants ont généralement du mal à comprendre ce qu'est une variable et un paramètre, et comment les choses nommées n
dans votre programme peuvent avoir des valeurs différentes. Alors maintenant, si cette valeur est dans la boucle de contrôle ou la récursivité, c'est encore pire! Soyez gentil et n'utilisez pas les mêmes noms de variables partout, et précisez que les paramètres ne sont que des variables .
Conditions de fin
Comment déterminer ma condition finale? C'est facile, demandez-leur de dire les étapes à voix haute. Par exemple pour le départ factoriel à partir de 5, puis 4, puis ... jusqu'à 0.
Le diable est dans les détails
Ne parlez pas de choses précoces comme optimisation des appels de queue . Je sais, je sais, TCO est sympa, mais ils s'en moquent au début. Donnez-leur un peu de temps pour faire le tour du processus d'une manière qui leur convient. N'hésitez pas à briser leur monde plus tard, mais donnez-leur une pause.
De même, ne parlez pas directement de la première conférence sur le pile d'appel et sa consommation de mémoire et ... eh bien ... le débordement de pile . Je donne souvent des cours particuliers à des étudiants qui me montrent des conférences où ils ont 50 diapositives sur tout il faut savoir sur la récursivité quand ils peuvent à peine écrire une boucle correctement à ce stade. C'est un bon exemple de la façon dont une référence vous aidera plus tard mais pour le moment, juste confond vous profondément.
Mais s'il vous plaît, en temps voulu, précisez qu'il y a raisons de suivre la voie itérative ou récursive .
Récurrence mutuelle
Nous avons vu que les fonctions peuvent être récursives, et même qu'elles peuvent avoir plusieurs points d'appel (8-reines, Hanoi, Fibonacci ou même un algorithme d'exploration pour un dragueur de mines). Mais qu'en est-il appels mutuellement récursifs ? Commencez également par les mathématiques ici. f(x) = g(x) + h(x)
où g(x) = f(x) + l(x)
et h
et l
font juste des trucs.
Commencer avec des séries uniquement mathématiques facilite l'écriture et la mise en œuvre car le contrat est clairement défini par les expressions. Par exemple, les séquences féminines et masculines de Hofstadter :
Cependant, en termes de code, il convient de noter que la mise en œuvre d'une solution mutuellement récursive conduit souvent à la duplication de code et devrait plutôt être rationalisée en une seule forme récursive (Voir Peter Norvig 's Résoudre chaque puzzle Sudok .
L'appel d'une fonction à l'intérieur de cette même fonction.
Il est important de savoir comment l'utiliser, quand l'utiliser et comment éviter une mauvaise conception, ce qui vous oblige à l'essayer par vous-même et à comprendre ce qui se passe.
La chose la plus importante que vous devez savoir est de faire très attention à ne pas obtenir une boucle qui ne se termine jamais. La réponse de pramodc84 à votre question a cette faute: elle ne finit jamais ...
Une fonction récursive doit toujours rechercher une condition pour déterminer si elle doit s'appeler de nouveau ou non.
L'exemple le plus classique pour utiliser la récursivité est de travailler avec un arbre sans limite statique en profondeur. Il s'agit d'une tâche que vous devez utiliser la récursivité.
La programmation récursive est le processus de réduction progressive d'un problème afin de résoudre plus facilement les versions de lui-même.
Chaque fonction récursive a tendance à:
Lorsque l'étape 2 est antérieure à 3 et lorsque l'étape 4 est triviale (une concaténation, une somme ou rien), cela permet récursivité de la queue . L'étape 2 doit souvent venir après l'étape 3, car les résultats du ou des sous-domaines du problème peuvent être nécessaires pour terminer l'étape en cours.
Prenez la traversée d'un arbre binaire simple. La traversée peut être effectuée en précommande, en commande ou en post-commande, selon ce qui est requis.
B
A C
Pré-commande: B A C
traverse(tree):
visit the node
traverse(left)
traverse(right)
Dans l'ordre: A B C
traverse(tree):
traverse(left)
visit the node
traverse(right)
Post-commande: A C B
traverse(tree):
traverse(left)
traverse(right)
visit the node
De très nombreux problèmes récursifs sont des cas spécifiques d'une opération map ou fold - la compréhension de ces deux opérations peut conduire à une compréhension significative des bons cas d'utilisation pour la récursivité.
L'OP a déclaré que la récursivité n'existe pas dans le monde réel, mais je prie de différer.
Prenons le "fonctionnement" du monde réel de couper une pizza. Vous avez sorti la pizza du four et pour la servir, vous devez la couper en deux, puis couper les moitiés en deux, puis encore couper les moitiés résultantes en deux.
L'opération de couper la pizza que vous effectuez encore et encore jusqu'à ce que vous obteniez le résultat souhaité (le nombre de tranches). Et pour les arguments, disons qu'une pizza non coupée est une tranche elle-même.
Voici un exemple dans Ruby:
def cut_pizza (tranches_existantes, tranches_souhaitées) si tranches_existantes! = tranches_souhaitées # nous n'avons pas encore assez de tranches pour nourrir tout le monde, donc # nous sommes couper les tranches de pizza, doublant ainsi leur nombre new_slices = existing_slices * 2 # et voici l'appel récursif cut_pizza (new_slices, desire_slices) else # nous avons le nombre de tranches souhaité, nous renvoyons donc # ici au lieu de continuer à recurse retourner les tranches_existantes fin fin pizza = 1 # une pizza entière, 'une tranche' cut_pizza (pizza, 8) # => nous aurons 8
Donc, le monde réel coupe une pizza et la récursion fait la même chose encore et encore jusqu'à ce que vous ayez ce que vous voulez.
Les opérations que vous trouverez que le recadrage que vous pouvez implémenter avec des fonctions récursives sont:
Je recommande d'écrire un programme pour rechercher un fichier en fonction de son nom de fichier et d'essayer d'écrire une fonction qui s'appelle jusqu'à ce qu'elle soit trouvée, la signature ressemblerait à ceci:
find_file_by_name(file_name_we_are_looking_for, path_to_look_in)
Vous pouvez donc l'appeler comme ceci:
find_file_by_name('httpd.conf', '/etc') # damn it i can never find Apache's conf
C'est simplement de la mécanique de programmation à mon avis, une façon d'éliminer intelligemment la duplication. Vous pouvez réécrire cela en utilisant des variables, mais c'est une solution "plus agréable". Il n'y a rien de mystérieux ou de difficile à ce sujet. Vous écrirez quelques fonctions récursives, il cliquera et huzzah une autre astuce mécanique dans votre boîte à outils de programmation.
Crédit supplémentaire Le cut_pizza
l'exemple ci-dessus vous donnera une erreur de niveau de pile trop profonde si vous lui demandez un nombre de tranches qui n'est pas une puissance de 2 (c'est-à-dire 2 ou 4 ou 8 ou 16). Pouvez-vous le modifier pour que si quelqu'un demande 10 tranches, il ne fonctionnera pas éternellement?
D'accord, je vais essayer de garder cela simple et concis.
Les fonctions récursives sont des fonctions qui s'appellent elles-mêmes. La fonction récursive se compose de trois choses:
La meilleure façon d'écrire des méthodes récursives est de penser à la méthode que vous essayez d'écrire comme un exemple simple ne gérant qu'une boucle du processus que vous souhaitez parcourir, puis ajoutez l'appel à la méthode elle-même et ajoutez quand vous le souhaitez mettre fin. La meilleure façon d'apprendre est de pratiquer comme toutes choses.
Comme il s'agit du site Web des programmeurs, je m'abstiendrai d'écrire du code, mais voici un bon lien
si vous avez cette blague, vous avez ce que signifie la récursivité.
La récursivité est un outil qu'un programmeur peut utiliser pour invoquer un appel de fonction sur lui-même. La séquence de Fibonacci est l'exemple type de l'utilisation de la récursivité.
La plupart du code récursif sinon tous peuvent être exprimés en fonction itérative, mais c'est généralement désordonné. Les structures de données telles que les arbres, l'arbre de recherche binaire et même le tri rapide sont de bons exemples d'autres programmes récursifs.
La récursivité est utilisée pour rendre le code moins bâclé, gardez à l'esprit qu'il est généralement plus lent et nécessite plus de mémoire.
J'aime utiliser celui-ci:
Si vous êtes à l'entrée du magasin, passez simplement par là. Sinon, faites un pas, puis parcourez le reste du chemin jusqu'au magasin.
Il est essentiel d'inclure trois aspects:
En fait, nous utilisons beaucoup la récursivité dans la vie quotidienne; nous n'y pensons tout simplement pas de cette façon.
Le meilleur exemple que je voudrais vous montrer est le langage de programmation C de K & R. la page d'index aussi.
Josh K a déjà mentionné les poupées Matroshka . Supposons que vous vouliez apprendre quelque chose que seule la poupée la plus petite connaît. Le problème est que vous ne pouvez pas vraiment lui parler directement, car elle vit à l'origine à l'intérieur la poupée plus grande qui sur la première photo est placée sur sa gauche. Cette structure va comme ça (une poupée vit à l'intérieur de la poupée plus grande) jusqu'à se retrouver seulement avec la plus grande.
Donc, la seule chose que vous pouvez faire est de poser votre question à la poupée la plus grande. La poupée la plus grande (qui ne connaît pas la réponse) devra passer votre question à la poupée la plus courte (qui sur la première photo est à sa droite). Comme elle n'a pas non plus la réponse, elle doit demander à la prochaine poupée plus courte. Cela ira comme ça jusqu'à ce que le message atteigne la poupée la plus courte. La poupée la plus courte (qui est la seule à connaître la réponse secrète) passera la réponse à la prochaine poupée plus grande (trouvée sur sa gauche), qui la transmettra à la prochaine poupée plus grande ... et cela continuera jusqu'à la réponse atteint sa destination finale, qui est la poupée la plus haute et enfin ... vous :)
C'est vraiment ce que fait la récursivité. Une fonction/méthode s'appelle jusqu'à obtenir la réponse attendue. C'est pourquoi lorsque vous écrivez du code récursif, il est très important de décider quand la récursivité doit se terminer.
Ce n'est pas la meilleure explication mais j'espère que cela aide.
récursivité n. - Un modèle de conception d'algorithme où une opération est définie en termes d'elle-même.
L'exemple classique consiste à trouver la factorielle d'un nombre, n !. 0! = 1, et pour tout autre nombre naturel N, la factorielle de N est le produit de tous les nombres naturels inférieurs ou égaux à N. Donc, 6! = 6 * 5 * 4 * 3 * 2 * 1 = 720. Cette définition de base vous permettrait de créer une solution itérative simple:
int Fact(int degree)
{
int result = 1;
for(int i=degree; i>1; i--)
result *= i;
return result;
}
Cependant, examinez à nouveau l'opération. 6! = 6 * 5 * 4 * 3 * 2 * 1. Par la même définition, 5! = 5 * 4 * 3 * 2 * 1, ce qui signifie que nous pouvons dire 6! = 6 * (5!). À son tour, 5! = 5 * (4!) Et ainsi de suite. Ce faisant, nous réduisons le problème à une opération effectuée sur le résultat de toutes les opérations précédentes. Cela se réduit finalement à un point, appelé cas de base, où le résultat est connu par définition. Dans notre cas, 0! = 1 (on pourrait dans la plupart des cas dire aussi 1! = 1). En informatique, nous sommes souvent autorisés à définir des algorithmes d'une manière très similaire, en faisant appeler la méthode elle-même et en passant une entrée plus petite, réduisant ainsi le problème à travers de nombreuses récursions dans un cas de base:
int Fact(int degree)
{
if(degree==0) return 1; //the base case; 0! = 1 by definition
else return degree * Fact(degree -1); //the recursive case; N! = N*(N-1)!
}
Cela peut, dans de nombreuses langues, être encore simplifié en utilisant l'opérateur ternaire (parfois considéré comme une fonction Iif dans les langues qui ne fournissent pas l'opérateur en tant que tel):
int Fact(int degree)
{
//reads equivalently to the above, but is concise and often optimizable
return degree==0 ? 1: degree * Fact(degree -1);
}
Avantages:
Désavantages:
L'exemple que j'utilise est un problème que j'ai rencontré dans la vraie vie. Vous avez un conteneur (comme un grand sac à dos que vous comptez emporter en voyage) et vous souhaitez connaître le poids total. Vous avez dans le conteneur deux ou trois articles en vrac, et quelques autres conteneurs (par exemple, des sacs de rangement). Le poids du conteneur total est évidemment le poids du conteneur vide plus le poids de tout ce qu'il contient. Pour les articles en vrac, vous pouvez simplement les peser, et pour les sacs de rangement, vous pouvez simplement les peser ou vous pouvez dire "bien le poids de chaque sac de rangement est le poids du conteneur vide plus le poids de tout ce qu'il contient". Et puis vous continuez à entrer dans des conteneurs dans des conteneurs et ainsi de suite jusqu'à ce que vous arriviez à un point où il n'y a que des objets en vrac dans un conteneur. C'est la récursivité.
Vous pensez peut-être que cela ne se produit jamais dans la vie réelle, mais imaginez-vous en train de compter ou de additionner les salaires des personnes dans une entreprise ou une division particulière, qui a un mélange de personnes qui travaillent simplement pour l'entreprise, des personnes dans les divisions, puis dans les divisions il y a des départements et ainsi de suite. Ou des ventes dans un pays qui a des régions, dont certaines ont des sous-régions, etc. Ce genre de problèmes se produit tout le temps dans les affaires.
La récursivité peut être utilisée pour résoudre de nombreux problèmes de comptage. Par exemple, supposons que vous avez un groupe de n personnes à une fête (n> 1), et que tout le monde serre la main de tout le monde exactement une fois. Combien de poignées de main ont lieu? Vous savez peut-être que la solution est C (n, 2) = n (n-1)/2, mais vous pouvez résoudre récursivement comme suit:
Supposons qu'il n'y ait que deux personnes. Ensuite (par inspection) la réponse est évidemment 1.
Supposons que vous ayez trois personnes. Choisissez une personne et notez qu'elle serre la main de deux autres personnes. Après cela, vous devez compter uniquement les poignées de main entre les deux autres personnes. Nous l'avons déjà fait tout à l'heure, et c'est 1. Donc, la réponse est 2 + 1 = 3.
Supposons que vous ayez n personnes. Suivant la même logique que précédemment, c'est (n-1) + (nombre de poignées de main entre n-1 personnes). En expansion, nous obtenons (n-1) + (n-2) + ... + 1.
Exprimé comme une fonction récursive,
f (2) = 1
f (n) = n-1 + f (n-1), n> 2
Voici un exemple réel de récursivité.
Laissez-les imaginer qu'ils ont une collection de bandes dessinées et vous allez tout mélanger en un gros tas. Attention - s'ils ont vraiment une collection, ils peuvent vous tuer instantanément lorsque vous mentionnez simplement l'idée de le faire.
Maintenant, laissez-les trier ce gros tas de bandes dessinées non triées à l'aide de ce manuel:
Manual: How to sort a pile of comics
Check the pile if it is already sorted. If it is, then done.
As long as there are comics in the pile, put each one on another pile,
ordered from left to right in ascending order:
If your current pile contains different comics, pile them by comic.
If not and your current pile contains different years, pile them by year.
If not and your current pile contains different tenth digits, pile them
by this digit: Issue 1 to 9, 10 to 19, and so on.
If not then "pile" them by issue number.
Refer to the "Manual: How to sort a pile of comics" to separately sort each
of the new piles.
Collect the piles back to a big pile from left to right.
Done.
La bonne chose ici est: quand ils sont à des problèmes uniques, ils ont le "cadre de pile" complet avec les piles locales visibles devant eux au sol. Donnez-leur plusieurs impressions du manuel et mettez-en un de côté pour chaque niveau de pile avec une marque où vous vous trouvez actuellement à ce niveau (c.-à-d. L'état des variables locales), afin de pouvoir y continuer à chaque fin.
C'est à cela que sert essentiellement la récursivité: effectuer le même processus, juste à un niveau de détail plus fin, plus vous y entrez.
Dans la vie (par opposition à dans un programme informatique), la récursivité se produit rarement sous notre contrôle direct, car cela peut être déroutant de se produire. De plus, la perception concerne généralement les effets secondaires, plutôt que d'être purement fonctionnelle, donc si une récursivité se produit, vous ne la remarquerez peut-être pas.
Cependant, la récursivité se produit ici dans le monde. Beaucoup.
Un bon exemple est (une version simplifiée de) le cycle de l'eau:
C'est un cycle qui fait que son moi se reproduise. C'est récursif.
Un autre endroit où vous pouvez obtenir la récursivité est en anglais (et en langage humain en général). Vous ne le reconnaîtrez peut-être pas au début, mais la façon dont nous pouvons générer une phrase est récursive, car les règles nous permettent d'incorporer une instance d'un symbole à côté d'une autre instance du même symbole.
De l'instinct de langue de Steven Pinker:
si la fille mange de la crème glacée ou la fille mange des bonbons alors le garçon mange des hot-dogs
C'est une phrase entière qui contient d'autres phrases entières:
la fille mange de la glace
la fille mange des bonbons
le garçon mange des hot-dogs
L'acte de comprendre la phrase complète implique de comprendre des phrases plus petites, qui utilisent le même ensemble de ruse mentale pour être compris que la phrase complète.
Pour comprendre la récursivité du point de vue de la programmation, il est plus facile d'examiner un problème qui peut être résolu avec la récursivité et de comprendre pourquoi il devrait l'être et ce que cela signifie que vous devez faire.
Pour l'exemple, j'utiliserai la plus grande fonction de diviseur commune, ou gcd pour faire court.
Vous avez vos deux nombres a
et b
. Pour trouver leur gcd (en supposant que ni l'un ni l'autre n'est 0), vous devez vérifier si a
est divisible également en b
. Si c'est le cas, b
est le gcd, sinon vous devez vérifier le gcd de b
et le reste de a/b
.
Vous devriez déjà pouvoir voir qu'il s'agit d'une fonction récursive, car vous avez la fonction gcd appelant la fonction gcd. Juste pour le marteler, il est ici en c # (encore une fois, en supposant que 0 n'est jamais passé en paramètre):
int gcd(int a, int b)
{
if (a % b == 0) //this is a stopping condition
{
return b;
}
return (gcd(b, a % b)); //the call to gcd here makes this function recursive
}
Dans un programme, il est important d'avoir une condition d'arrêt, sinon votre fonction se reproduira pour toujours, ce qui finira par provoquer un débordement de pile!
La raison d'utiliser la récursivité ici, plutôt qu'une boucle while ou une autre construction itérative, est que lorsque vous lisez le code, il vous indique ce qu'il fait et ce qui se passera ensuite, il est donc plus facile de déterminer s'il fonctionne correctement. .