On parle de récursivité d'un algorithme lorsqu'il utilise une fonction qui vérifie la particularité suivante :
Une fonction est dite récursive lorsqu'à un moment de son exécution, elle fait appel à elle-même.
Cet appel peut se faire une fois, deux fois, ou plusieurs fois de suite : la fonction se retrouve "imbriquée dans elle-même" !
Étrange particularité...pour mieux illustrer ce concept "d'appel à soi-même", voila quelques exemples de récursivité en dehors du domaine de l'informatique :
Dans le logo de cette célèbre marque de fromage fondu, l'image de la vache se retrouve à l'identique dans ses boucles d'oreilles, et dans l'image sur ces dernières, on retrouve la même image sur les boucles d'oreille, etc...
Le triangle de Sierpiński est un exemple de figure fractale; elle est constituée en répétant le même motif quelque soit l'échelle à laquelle on la regarde.
Un peu plus subtil...
https://imgs.xkcd.com/comics/self_description.pngDans tous ces exemples, on se sert d'un objet pour fabriquer l'objet lui-même ! C'est ça le principe de la récursivité...mais en informatique, au lieu d'une "mise en abyme" infinie comme dans ces exemples, il faut impérativement que ce "bouclage sur soi-même" s'arrête à un moment ou un autre !
La récursivité s'oppose à la programmation itérative, où les instructions s’exécutent les unes après les autres jusqu'à arriver au résultat recherché; nous verrons quand il est possible de transformer un programme itératif en programme récursif ( l'inverse étant généralement beaucoup plus difficile ), et dans quel cas lequel de ces paradigmes est le "meilleur" à utiliser.
Considérons le code suivant :
Ah oui, super...moi je peux faire ça avec une simple boucle...
Et effectivement, une boucle peut facilement être transformée en un algorithme récursif ( et vice et versa ), mais ce qui nous intéresse ici, c'est : comment ça marche ???
Examinons pour cela le déroulement du script :
f(n)
en lui passant l'argument 5else
est exécuté : on affiche donc "Salut !"
f(n)
à partir d'elle-même !f(n)
n'est plus rappelée.Et bien voila, le résultat de la fonction f(n)
a été obtenu en faisant des appels successifs à elle-même, et c'est ça la base de la récursivité.
Cet exemple n'est pas très intéressant par rapport à une simple boucle, car chaque récursion ( = appels successifs de la fonction à elle-même ) ne fait qu'appeler la fonction, avec un
paramètre différent à chaque fois certes, mais sans rien lui retourner...
L'étude de l'exemple suivant, déjà plus "évolué, va nous permettre également de préciser la manière de concevoir un algorithme récursif et tout le vocabulaire qui tourne autour de la récursivité.
On veut maintenant écrire une fonction qui calcule la somme des entiers de 1 jusqu'à N inclus, paramètre passé à la fonction.
Par exemple, pour N = 10, la fonction calculera la somme : 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55
On utilise une boucle dont l'indice varie de 1 jusqu'à N, et on ajoute à chaque tour de boucle la valeur de cet indice à une variable somme :
Il faut voir les choses sous un autre angle, et réfléchir cette fois en terme de relation qui existe entre les différentes valeurs additionnées.
On peut ainsi écrire :
De même :
Etc, etc...Autrement dit, on a pour un entier N quelconque : somme(N) = N + somme(N - 1). La somme totale est égale à l'entier le plus grand + la somme des entiers inférieurs, elle-même égale à l'entier inférieur + la somme des entiers inférieurs, elle-même égale à etc, etc...
Et voila la relation ( on parle en mathématiques de relation de récurrence ) que nous allons utiliser pour obtenir la somme des N entiers :
def somme_recurs(N):
return N + somme_recurs( N - 1 )
→ la fonction va s'appeler elle-même "en chaîne", en se passant à chaque appel un argument différent; le résultat retourné par la fonction sera obtenu à partir de celui retourné par le niveau "inférieur" de récursion, qui lui-même sera obtenu à partir du résultat du niveau encore en dessous, et ainsi de suite...
Chaque appel de la fonction n'est donc pas immédiatement suivi du renvoi de son résultat : il est "en attente" du calcul du résultat suivant...Voila pourquoi on parle d'appels récursifs de la fonction = appel réalisé alors que l’interprétation d’un appel précédent de la même fonction n’est pas terminé.
Mais du coup, comment le programme peut-il se "souvenir" des résultats des différents niveaux de récursions ? Chaque appel successif de la fonction récursive "n'écrase"-t-il pas le résultat du précédent ?
En réalité, non : un mécanisme est prévu pour cela, la pile d'exécution. A chaque appel d'une fonction, l'ordinateur stocke ( il "empile" ) quelque part en mémoire différentes valeurs, notamment l'adresse mémoire correspondant au point du programme où la fonction a été appelée, de façon à savoir "où" retourner une fois le travail de cette dernière terminé; il y stocke également le résultat retourné par la fonction, tant que le retour de la fonction n'est pas terminé. Les différents résultats d'une récursion, "empilés" successivement ne sont donc pas oubliés, et sont "dépilés" au fur et à mesure de la "remontée" de chaque niveau.
Vous étudierez le fonctionnement général d'une pile, c'est au programme...
On voit donc qu'il manque encore à notre algorithme une chose indispensable : il faut que les appels successifs de la fonction à elle-même s'arrêtent à un moment, de façon à "déclencher" la "remontée" progressive des résultats d'un niveau de récursion au niveau supérieur, sinon les appels se prolongeront à l'infini sans que l'on n'obtienne aucun résultat !.
Il faut donc prévoir à tout algorithme récursif une condition d'arrêt, que l'on appelle également cas de base. Ce cas correspond à la situation où il n'est pas nécessaire d'effectuer de calcul pour obtenir le résultat qui sera renvoyé au niveau supérieur.
Dans notre exemple, c'est lorsque N = 0 qu'il faut arrêter l'algorithme : en effet, il n'y alors plus rien à calculer.
A ce moment, la fonction doit donc renvoyer 0 ( on n'ajoute plus rien à la somme ) : il n'y a plus de nouvel appel à la fonction, la récursion s'arrête alors, et les différents résultats sont remontés successivement vers les niveaux supérieurs.
La fonction complète :
def somme_recurs(N):
if N == 0:
return 0
return N + somme_recurs( N - 1 )
print(somme_recurs(10))
Cela semble peut-être un peu "magique", mais en fait tout est bien entendu parfaitement logique; voila l'arbre des appels et des retours successifs dans le cas où N = 4 ( résultat = 10 ) :
Pensez à installer le module recviz
au préalable.
En résumé, une fonction récursive doit comporter :
Pour illustrer le dernier critère, voila un exemple de récursion qui peut poser problème ( c'est notre tout premier exemple ) :
def f(n):
# condition d'arrêt
if n == 0:
print("Bye-bye !")
else :
print("Salut !")
return f(n-1)
Tout est là, cas de base, appel récursif. L'appel de cette fonction avec un paramètre n positif ne pose effectivement aucun problème :
>>> f(5)
Salut !
Salut !
Salut !
Salut !
Salut !
Bye-bye !
>>>
Par contre, avec n négatif, la condition d'arrêt ne sera jamais vérifiée : la chaîne des appels ne terminera jamais !
>>> f(-5)
Salut !
Salut !
.....
.....
Salut !
Salut !
Salut !
Salut !
Traceback (most recent call last):
File "test.py", line 8, in
f(-5)
File "test.py", line 6, in f
f(n-1)
File "test.py", line 6, in f
f(n-1)
File "test.py", line 6, in f
f(n-1)
[Previous line repeated 1009 more times]
File "test.py", line 5, in f
print("Salut !")
RecursionError: maximum recursion depth exceeded while pickling an object
>>>
( L'erreur alors rencontrée correspond à un "débordement de pile" et est expliquée ci-dessous.)
Sachant que d'après les travaux Alan Turing, il n'existe aucun moyen algorithmique pour prouver qu'un programme se terminera ( voir le chapitre sur la calculabilité ), c'est au concepteur de l'algorithme de vérifier que celui-ci est correctement écrit...
Que dire ce cette façon d'envisager les algorithmes ? Il se trouve que de nombreux algorithmes itératifs peuvent se transformer en leur version récursive : c'est le cas par exemple des boucles for
:
Version itérative | Version récursive |
---|---|
|
|
( Que fait cette fonction ? Ce sera le but d'un exercice que vous traiterez ci-dessous...mais vous avez peut-être déjà trouvé ?)
Que faut-il mieux utiliser alors ?
Inconvénients | Avantages |
---|---|
|
|
La réponse à la question posée ( itératif ou récursif ? ) est donc la suivante :
On considère la fonction récursive de l'exemple du paragraphe 3.
Une fonction récursive peut bien entendu avoir plusieurs paramètres, dont les valeurs initiales peuvent être précisées par défaut... mais chacun ne sera pas forcément modifié à chaque appel récursif.
Les exercices suivants sont des exemples de telles fonctions.
On rappelle que la puissance x d'un nombre y est définie par : yx = y * y * y * ..... * y ( x fois )
Écrire une fonction chaine_numerique()
qui construit récursivement une chaîne constituée des nombres de i jusqu'à n, valeurs passées en paramètres
de la fonction.
Par exemple, chaine_numerique(3, 12) doit renvoyer "3456789101112"
.
Écrire une fonction est_dans()
qui :
True
) ou pas ( renvoie False
).Ne pas oublier qu'une chaîne de caractère n'est pas mutable : on ne peut donc pas lui "enlever de caractère"...
Une indication : il va falloir parcourir la chaîne depuis son premier caractère jusqu'à son dernier, en testant à chaque fois si le caractère est celui recherché; la récursion remplace donc dans
ce cas une simple boucle.
On va donc se servir d'un paramètre supplémentaire ( en plus de la chaîne à "explorer" ) dans la fonction ( ici, le paramètre i ), qui servira de "pointeur d'index" pour parcourir la chaîne; ce "pointeur" sera incrémenté
à chaque appel récursif, jusqu'à ce que l'on atteigne la fin de la chaîne ( cas de base 😏 ! ).
On donne à ce paramètre une valeur par défaut ( 0 ), ce qui permet d'omettre de le passer en argument lors du premier appel de la fonction depuis le programme principal, améliorant
ainsi l'interface de la fonction du point de vue de l'utilisateur.
On peut donner à une fonction un nombre quelconque de paramètre(s) ayant une valeur par défaut. Mais il faut bien les indiquer après les paramètres sans valeur dans la liste des paramètres de la fonction.