#!/usr/bin/env python # coding: utf-8 # # La récursivité # # La notion de récursivité est une notion essentielle, et pas seulement en informatique. # # En effet, pour expliquer une situation, on utilise souvent cette même situation à un état précédent. # # ## Introduction : # #

A faire vous même n°1 :

# # - Regarder cette vidéo d'introduction... # In[1]: from IPython.display import HTML HTML("""
""") # **La récursivité est donc une méthode de résolution de problèmes qui consiste à décomposer le problème en sous-problèmes identiques de plus en plus petits jusqu’à obtenir un problème suffisamment petit pour qu’il puisse être résolu de manière triviale.** # #
Recursif-Gru.jpg
# ## Exemple : # # Étant donné une liste d’entiers `L=[2,12,1,8,5,10,20]`, calculer la somme des éléments de cette liste. # # Comme les listes sont itérables, nous pouvons simplement résoudre ce problème avec l’un ou l'autre de ces algorithmes que l’on dit **itératifs** : # # ```pseudo # Données : liste ← une liste d’entiers # fonction somme_1(liste) # S ← 0 # n ← longueur de la liste # Pour i allant de 0 à n faire # S ← S + liste[i] # renvoyer S # ``` # # OU # # ```pseudo # Données : liste ← une liste d’entiers # fonction somme_2(liste) # S ← 0 # Pour chaque element de liste faire # S ← S + element # renvoyer S # ``` # # OU # # ```pseudo # Données : liste ← une liste d’entiers # fonction somme_3(liste) # S ← 0 # i ← 0 # Tant que i < longueur de la liste # S ← S + liste[i] # i ← i + 1 # renvoyer S # ``` # #

A faire vous même n°2 :

# # - Pour vous échauffer, implémenter en Python ces trois algorithmes itératifs et tester les : # In[ ]: # In[ ]: assert somme([2,12,1,8,5,10,20]) == 58, "Test non concluant !" print("Test concluant !") # Supposons maintenant que nous n’ayons pas la possibilité de faire de "boucles". # # On peut alors aborder le problème sous un autre angle : # # > La somme des termes de cette liste est: 2 + ( la somme des termes de [12,1,8,5,10,20]) # > # > Soit : 2 + (12 + (la somme des termes de [1,8,5,10,20]) # > # > et ainsi de suite... jusqu’à : 2 + (12 + (1 + (8 + (5 + (10 + ( la somme des termes de [20])))))) # > # > il est clair que : la somme des termes de [20] est 20 # > # > Au final le calcul à faire est : 2 + (12 + (1 + (8 + (5 + (10 + (20)))))) = 58 # # Considérons alors une fonction `sommeliste(liste)` et qui renvoie le résultat de la somme des éléments de la liste. # # L’algorithme ci-dessous que l’on dit **récursif** réalise cette seconde approche: # # ```pseudo # Données : liste ← une liste d’entiers # fonction sommeliste(liste) # Si la longueur de la liste est égale à 1 alors # renvoyer liste[0] # Sinon # renvoyer liste[0] + sommeliste(liste[1:]) # ``` # #

A faire vous même n°3 :

# # - Implémenter en Python cette fonction et la tester également : # In[ ]: # In[ ]: assert sommeliste([2,12,1,8,5,10,20]) == 58, "Test non concluant" print("Test concluant !") #

A faire vous même n°4 :

# # - Comparer les vitesses d’exécutions des programmes version itérative et version récursive de l’exemple précédent. # # > Pour tester la vitesse d’exécution d’une fonction on utilise le module timeit, comme le montre le code ci-dessous pour une liste de 1000 entiers choisis aléatoirement entre 0 et 100 avec le module random. # # # In[ ]: from timeit import default_timer as timer from random import randint # Ajouter les codes des deux fonctions à comparer ici L=[randint(0,100) for i in range(1000)] debut = timer() print(sommeliste(L)) fin = timer() print(fin-debut) debut = timer() print(somme(L)) fin = timer() print(fin - debut) # - Que pouvez-vous en conclure (pour cet exemple)? # # ... # ## Complexité et fonctionnement d'un programme récursif : # #

A faire vous même n°5 :

# # - Observer le fonctionnement des appels récursifs de la fonction `sommeliste(liste)` dans votre programme avec l'outil [Python tutor](http://pythontutor.com/) permet de visualiser pas à pas ce que fait votre script. # # > Il est possible de l'utiliser dans un notebook jupyter (mais hors Capytale) pour celà il faut : # > 1. l'installer (si ce n'est pas déjà fait) : ``%pip install metakernel`` # > 1. puis exécuter les instructions de la cellule suivante : # In[ ]: from metakernel import register_ipython_magics register_ipython_magics() # > # >3. inscrire la commande ``%%tutor`` devant le script à tester dans la cellule : # In[ ]: get_ipython().run_cell_magic('tutor', '', '# Script à tester :\n\n# Ajouter le code de votre fonction sommeliste(liste) ici\n\n\nprint(sommeliste([2,12,1,8,5,10,20]))\n') # Un programme est une suite d’instructions, son exécution peut être représentée par un parcours de chemin ayant une origine et une extrémité. # # Lors de l’appel d’une fonction, cette suite est interrompue le temps de cette fonction, avant de reprendre à l’endroit où le programme s’est arrêté. # # Recursif-Pile_1.png # # Au moment où débute cette bifurcation, le processeur sauvegarde un certain nombre d’informations : adresse de retour, état des # variables, etc. # # Toutes ces données forment ce qu’on appelle le contexte du programme, et elles sont stockées dans ce qu’on appelle la pile d’exécution. # # À la fin de l’exécution de la fonction, le contexte est sorti pour permettre la poursuite de l’exécution du programme. # # Recursif-Pile_.png # # Lors de l’exécution d’une fonction récursive, chaque appel récursif conduit, au moment où il se produit, à un empilement du contexte dans la pile d’exécution. # # Lorsqu’au bout de n appels se produit la condition d’arrêt, les différents contextes sont progressivement dépilés pour poursuivre l’exécution de la fonction. # # Il est important de prendre conscience qu’**une fonction récursive s’accompagne d’une complexité qui va croître avec le nombre d’appels récursifs** (en général linéairement, mais ce n’est pas une règle, cela dépend du contenu du contexte). # ## Les trois règles... # # A l'instar des robots d’[Azimov](https://fr.wikipedia.org/wiki/Trois_lois_de_la_robotique), les algorithmes récursifs doivent obéir à trois règles : # # 1. Un algorithme récursif doit avoir un " état trivial ", cela permet d’avoir une condition d’arrêt. # # > Dans notre exemple, il s’agit de : _si la liste est de longueur 1 alors on renvoie le seul élément de la liste_. # # 2. Un algorithme récursif doit conduire vers cet " état d’arrêt ", cela permet de ne pas faire une infinité d’appels récursifs. # # > Dans notre exemple, à chaque appel récursif, la liste est diminuée d’un élément donc nécessairement elle finira par n’en n’avoir plus qu’un. # # 3. Un algorithme récursif s’appelle lui même... # ## Itératif vs Récursif # # Il n’existe pas de réponse définitive à la question de savoir si un algorithme récursif est préférable à un algorithme itératif ou le contraire. # # Il a été prouvé que ces deux paradigmes de programmation sont équivalents ; autrement dit, tout algorithme itératif possède une version récursive, et réciproquement. # # Le choix du langage peut aussi avoir son importance : un langage fonctionnel est conçu pour exploiter la récursivité et le programmeur est naturellement amené à choisir la version récursive de l’algorithme qu’il souhaite écrire. # # À l’inverse, Python, même s’il l’autorise, ne favorise pas l’écriture récursive (limitation basse (1000 par défaut) du nombre # d’appels récursifs). # # Enfin, le choix d’écrire une fonction récursive ou itérative peut dépendre du problème à résoudre : certains problèmes se résolvent particulièrement simplement sous forme récursive. #

Application 1 : version récursive du convertisseur...

# # # > ### Petit rappel : # > # >Une méthode de conversion consiste à diviser le nombre décimal à convertir par la base b et conserver le reste de la division. Le quotient obtenu est divisé par b et conserver le reste. Il faut répéter l’opération sur chaque quotient obtenu. # > # > Par exemple, pour la conversion : $91$ = $01011011_2$ # > # >ConversionBinaire2 # > # >Les restes successifs sont écrits, en commençant par le dernier, de la gauche vers la droite. Cette méthode est dite « Méthode des divisions successives ». # # Voici un programme itératif qui réalise ce changement de base : # In[11]: def decTob(n,b): assert (b > 1 and b < 17) , "b doit être compris entre 2 et 16" signes = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"] mot = "" while n != 0: mot = signes[n % b] + mot n = n // b return mot # Analysons ce programme : # # - La première ligne permet de s’assurer que les conditions sur b sont assurées ; # - La liste signes nous permet d’avoir accès aux symboles représentant les nombres jusqu’à la base 16 ; # - On utilisera la variable mot de type str pour le résultat ; # - Tant que n != 0 (tant que le quotient n’est pas nul), on ajoute par la gauche le reste au résultat et on remplace n par le nouveau quotient n // b. # #

A faire vous même n°6 :

# # - Tester ce programme avec différentes bases pour vérifier que vous obtenez les mêmes résultats qu'avec : # ```python # print(bin(77)[2:]) ## affiche 77 en base 2 # print(oct(77)[2:]) ## affiche 77 en base 8 # print(hex(77)[2:]) ## affiche 77 en base 16 # ``` # In[ ]: # Recherchons maintenant une version récursive de ce programme. # # L’idée est : # # ```python # decTobr(n, b) = decTobr(n // b, b) + reste # ``` # # Pour ce faire suivons les trois règles : # # 1. Un algorithme récursif doit avoir un "état trivial", cela permet d’avoir une condition d’arrêt. # # 2. Un algorithme récursif doit conduire vers cet "état d’arrêt", cela permet de ne pas faire une infinité d’appels récursifs. # # 3. Un algorithme récursif s’appelle lui même... # # #

A faire vous même n°7 :

# # - Répondre aux questions : # # Q1) Dans notre cas quel est "l’état trivial" ? # # # ... # # Q2) Expliquer ce qui va conduire à cet "état trivial" ? # # ... # # ... # # ... # # - Réaliser une version récursive du programme précédent : # In[ ]: #

Application 2 : Calculer le factoriel d'un nombre :

# # # Écrire les versions itérative et récursive de fonctions calculant $n!$ où ${n\in\mathbf{N}}$ ; # # sachant que : $n! = n × (n − 1) × (n − 2) × ... × 2 × 1 $ ; # # et que par définition $0! = 1$. # In[ ]: # **** # ## Références aux programmes : # # #

Langages et programmation

# # # # # # # # # # # # #
ContenusCapacités attenduesCommentaires
Récursivité.Écrire un programme récursif.
Analyser le fonctionnement d’un programme récursif.
Des exemples relevant de domaines variés sont à privilégier.
# # # Licence Creative Commons
Ce document, basé sur le travail de Stephan VAN ZUIJLEN, est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International. # # Pour toute question, suggestion ou commentaire : eric.madec@ecmorlaix.fr # In[ ]: