La récursivité ============== ... une autre manière de penser un algorithme <br><br> *Ce document a été créé en collaboration avec ChatGPT (version 4o du 05 nov. 2024) pour transposer le cours correspondant (accessible [ici](recursivite)) en un diaporama.* *Pour voir la conversation avec ChatGPT : [cliquer ici](https://chatgpt.com/share/67338c36-cc2c-8008-8dc3-1f8f951ad611)* --- # Introduction ---- ## Une image <img class="r-stretch centre image-responsive" src="data/album_pink_floyd.jpg" alt="pochette album"> Pochette de l'album _Ummagumma_ de Pink Floyd ---- ## Quelques définitions - **Récursivité** : Démarche qui fait référence à l'objet même de la démarche. - **Récursivité en programmation** : Fait pour un objet de s'appeler lui-même. - Un objet est **récursif** s'il se définit par lui-même. - Une construction est **récursive** si elle se définit par elle-même. ---- ## Exemples - Décrire un processus dépendant de données en utilisant le même processus sur des données *plus simples* (c'est ce que nous allons faire !). - Montrer une image contenant des images similaires (ex. _Ummagumma_). - Faire pointer un article vers lui-même ou un article qui pointe vers lui, ou vers un article qui, par une succession de pointeurs, pointe vers l'article dont on est parti. --- # Retour sur les listes ---- ## Constructions récursives dans les listes - Le constructeur `construit(e, L)` crée une liste avec `e` comme tête et `L` comme queue. - Ex. pour créer la liste de nombres `5, 3, 8, 1` : ```python construit(5, construit(3, construit(8, construit(1, listevide())))) ``` - Représentation en `tuple` : ```python (5, (3, (8, (1, None)))) ``` ---- ## Fonction `dernier(L)` - Opération `dernier(L)` renvoie le dernier élément d'une liste `L`. - Version itérative (déjà écrite) : ```python def dernier(L): while reste(L) != listevide(): L = reste(L) return premier(L) ``` ---- ### Version récursive de `dernier` - Commençons par réfléchir ... - Comment obtenir le dernier élément d'une liste `L` ? - <span class="fragment">Si le reste de `L` est vide :</span> <span class="fragment">`dernier(L)` = </span> <span class="fragment">`premier(L)`</span> - <span class="fragment">Sinon,</span> <span class="r-stack"><span class="fragment">`dernier(L) = dernier(...)`</span><span class="fragment">`dernier(L) = dernier(reste(L))`</span></span> ---- ### Version récursive de `dernier` - Exemple : trouver `dernier(5, 3, 8, 1)` ```plaintext dernier(5, 3, 8, 1) = dernier(3, 8, 1) = dernier(8, 1) = dernier(1) # le reste est vide = 1 ``` ---- ### Analyse ```python {.line-numbers} [] def dernier(L): if reste(L) == listevide(): return premier(L) else: return dernier(reste(L)) ``` - Cette fonction est bien **récursive** puisqu'elle s'appelle elle-même (ligne 5) - On trouve ici le schéma classique d'un algorithme récursif : - On définit le **cas de base** (ici lorsque le reste est vide) qui est un cas pour lequel on peut donner le résultat facilement et qui fournit donc une **condition d'arrêt** des appels récursifs (indispensable !) ; - Sinon, on fait un **appel récursif** à la fonction mais sur une donnée plus petite (ici une liste plus petite). ---- ### Analyse ```python {.line-numbers} [] def dernier(L): if reste(L) == listevide(): return premier(L) else: return dernier(reste(L)) ``` - Les appels récursifs se font sur des données dont la **taille diminue** : on est sûr d'aboutir au *cas de base* qui stoppe les appels récursifs et assure la **terminaison** de l'algorithme --- # Déroulement d'une fonction récursive ---- ## Déroulement d'une fonction récursive - Chaque appel récursif **met en pause** l'exécution en attente du résultat de l'appel suivant. - Concrètement, il y a **2 phases** : - **Dépliage** (ou **descente**) : Les appels sont tour à tour mis en pause jusqu'au dernier appel qui fourni un resultat. - **Évaluation** : Le dernier résultat est renvoyé en remontant à l'appel précédent, qui l'utilise pour calculer son propre résultat et le renvoyer à l'appel précédent, etc. ---- ### Exemple de `dernier` ```python def dernier(L): if reste(L) == listevide(): return premier(L) else: return dernier(reste(L)) ``` - Calcul de `dernier((5, (3, (8, None)))` : <img class="centre image-responsive" src="data/deroulement_dernier.png"> ---- ### Exemple de `dernier` ```python {.line-numbers} [] def dernier(L): if reste(L) == listevide(): return premier(L) else: return dernier(reste(L)) ``` - Autre représentation : <img class="centre image-responsive r-stretch" src="data/deroulement_execution.png"> ---- ### ✍️ À faire Exercice 1 --- # Autre exemple : calcul de puissances ---- ## Fonction récursive pour `2^n` - Calcul de $2^n$ en récursif : - Cas de base ($n=0$) : $2^0 = 1$ - Sinon : $2^n = 2 \times 2^{n-1}$ (on se ramène à une puissance plus petite) - Résumé : <img src="data/definition_deux_puissance.png"> ​ ---- ## Fonction récursive pour `2^n` <img src="data/definition_deux_puissance.png"> - Implémentation en Python : ```python def deux_puissance(n): if n == 0: # cas de base return 1 else: # sinon appel récursif avec un argument plus petit return 2 * deux_puissance(n-1) ``` ```python >>> deux_puissance(3) 8 ``` ---- ## Déroulement de `deux_puissance` **Arbre d'appel** <img src="data/deroulement_deux_puissance.png"> ---- **Autre représentation** ``` deux_puissance(3) -> 2 * deux_puissance(2) -> DEPLIAGE -> -> 2 * 2 * deux_puissance(1) -> -> -> 2 * 2 * 2 * deux_puissance(0) ....................................................... <- <- <- 2 * 2 * 2 * 1 <- <- 2 * 2 * 2 <- EVALUATION <- 2 * 4 8 ``` ---- **Autre représentation (bis)** ``` deux_puissance(3) 2 * deux_puissance(2) = ? appel à deux_puissance(2) 2 * deux_puissance(1) = ? DEPLIAGE appel à deux_puissance(1) 2 * deux_puissance(0) = ? appel à deux_puissance(0) ...................................................... renvoie 1 2 * 1 renvoie 2 2 * 2 EVALUATION renvoie 4 2 * 4 renvoie 8 ``` ---- **Empilement des appels** <img class="centre image-responsive r-stretch" src="data/pile_execution.png" alt="pile d'exécution"> ---- **Utilisation d'outils** - Python Tutor : [lien vers pythontutor](https://www.pythontutor.com/visualize.html#code=def%20deux_puissance%28n%29%3A%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20return%202%20*%20deux_puissance%28n-1%29%0A%0Adeux_puissance%283%29&cumulative=true&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false). - Encore mieux : le logiciel **Thonny** ![](https://upload.wikimedia.org/wikipedia/commons/e/e2/Thonny_logo.png) ---- ### ✍️ À faire Exercices 2, 3, 4, 5 --- # Récursif vs Itératif ---- ## Récursif vs Itératif - Tout programme récursif peut être transformé en un programme itératif. - Réciproquement, tout programme itératif peut être transformé en récursif. - **Question** : Quelle méthode privilégier pour écrire un programme ? ---- ## Exemple : Puissances de deux **Version itérative** ```python def deux_puissance_iter(n): reponse = 1 for i in range(n): reponse = reponse * 2 return reponse ``` **Version récursive** ```python def deux_puissance_rec(n): if n == 0: return 1 else: return 2 * deux_puissance_rec(n-1) ``` ---- # Méthodes de Raisonnement - En itératif : - On pense à **la suite des instructions pour progresser des données vers le résultat**. - En récursif : - On **commence par exprimer le résultat à obtenir**. - On définit les **cas de base** et le (ou les) **cas récursif**. - Les deux approches sont à la base de : - *Programmation impérative* (itératif) - *Programmation fonctionnelle* (récursif) ---- ## Avantages de la Récursivité - **Lisibilité** : souvent plus élégante (question de goût) et concise. - **Efficace pour** : - Algorithmes sur des structures de données (listes, arbres, graphes). - Algorithmes « diviser pour régner ». ---- ## Inconvénients de la Récursivité : Efficacité - **En temps** : - Un programme récursif doit être traduit en itératif par le compilateur (un processeur est fondamentalement itératif) - L'exécution est souvent un peu plus lente, mais d'ordre de grandeur similaire. ---- ## Inconvénients de la Récursivité : Efficacité (suite) - **En espace** : - Les appels récursifs doivent stocker le contexte (paramètres, adresse de retour). - Ces contextes sont empilés (*pile* d'exécution, on le voit bien avec Thonny) - La mémoire requise pour la pile peut causer des erreurs de débordement. ---- ## Limites en Python - Python permet d'écrire des fonctions récursives mais n'est pas optimisé pour faire du récursif comparé à d'autres langages spécialisés dans le récursif - Le nombre d'appels récursifs est limité (on peut augmenter la valeur néanmoins) - Dépassement : une erreur `RecursionError` est levée. - Exemple : ```python >>> deux_puissance_rec(1000) RecursionError: maximum recursion depth exceeded in comparison ``` - Version itérative réussit : ```python >>> deux_puissance_iter(1000) # Donne le résultat sans erreur ... # un très grand nombre ``` --- # Bilan - Un programme est dit **récursif** lorsqu'il s'appelle lui-même. - La récursivité est un **principe algorithmique** : - La solution d’un problème est exprimée par un problème similaire mais sur un objet plus petit. - Pour une fonction récursive, il faut : - **Définir le(s) cas de base**. - **Déterminer les cas récursifs**. ---- # Bilan (suite) - Avantages de la récursivité (vs. itératif): - peut être vue comme plus élégante, concise et compréhensible que l'itératif. - Inconvénients de la récursivité (vs. itératif) : - efficacité en temps : un peu plus lent mais même ordre de grandeur - efficacité en espace (mémoire) : - le contexte des appels récursifs en pause doit être gardé en mémoire (dans une *pile*) - peut occasionner des débordements de la pile (*stack overflow*) --- **Références :** - Documents ressources de l'équipe éducative du DIU EIL, Université de Nantes, Christophe JERMANN et Christophe DECLERCQ, licence CC BY-NC-SA. --- Germain BECKER, Lycée Mounier, ANGERS Ressource éducative libre distribuée sous [Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International](http://creativecommons.org/licenses/by-nc-sa/4.0/) ![Licence Creative Commons](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png)