Bac à sable Chaînes et listes Fonctions Numpy Piles Récursivité Tris

Récursivité


Fonction récursive vs itérative

Définition

Une fonction f est dite récursive si son exécution peut provoquer un ou plusieurs appels de la fonction f elle-même.

La conception d’une fonction récursive est analogue à un raisonnement par récurrence.

Dans les exemples ci-dessous, on propose une version itérative et une ou plusieurs versions récursives.

Exemple

On considère la suite définie par \(u_0 = 1\) et \(u_n = 2 u_{n-1}\). (Rq : on vérifie sans peine que \(u_n = 2^n\)).

Dans le code d'une fonction récursive f, deux points sont essentiels :
- la présence d'un test d'arrêt, la fonction retourne (instruction return) la valeur correpondant à l'initialisation ;
- l'appel récursif (précédé de l'instruction return) qui correspond à la relation de récurrence

Remarquer dans le code ci-dessous que les itérations (calculs répétés) effectuées grâce à la boucle for dans la fonction itérative sont remplacées par les appels successifs dans la fonction récursive (il n'y a pas de boucle dans la fonction récursive dans cet exemple).


Le test d'arrêt doit impérativement précéder l'appel récursif sous peine d'appels imbriqués conduisant à une erreur :
RecursionError: maximum recursion depth exceeded
.

Remarque : le nombre d'appels récursifs est limité par défaut à 1000 par python.
Il est possible de modifier ce nombre :
import sys
sys.setrecursionlimit(5000).

Analyse de l'exécution

Cliquer sur le bouton "Next" dans l'animation ci-dessous et visualiser les appels successifs (n diminue) suivis de la phase de remontée : lorsque n vaut 0, la fonction renvoie la valeur 1 qui est transmise à l'appel précédent et ainsi de suite.

Les appels successifs sont empilés dans une pile au fur et à mesure de l'exécution :

Etat de la pile
1er appel2ème appel3ème appel4ème appel5ème appelDépilage u(0)Dépilage u(1)Dépilage u(2)Dépilage u(3)
u(0) → 1
u(1)=2*u(0)u(1)=2*u(0)u(1)=2*1 → 2
u(2)=2*u(1)u(2)=2*u(1)u(2)=2*u(1)u(2)=2*u(1)u(2)=2*2 → 4
u(3)=2*u(2)u(3)=2*u(2)u(3)=2*u(2)u(3)=2*u(2)u(3)=2*u(2)u(3)=2*u(2)u(3)=2*4 → 8
u(4)=2*u(3)u(4)=2*u(3)u(4)=2*u(3)u(4)=2*u(3)u(4)=2*u(3)u(4)=2*u(3)u(4)=2*u(3)u(4)=2*u(3)u(4)=2*8 → 16

Sauvegardes

Il est impératif de sauvegarder régulièrement le code en le collant dans un fichier texte (Notepad++) ou dans l'éditeur de pyzo.

Applications directes

Somme des n premiers entiers

  1. Ecrire une fonction itérative sommeI(n:int)->int (cette écriture signifie que le paramètre n est un entier et que la fonction renvoie un entier) renvoyant \(\displaystyle\sum_{i=0}^{n} i \).
  2. Tester avec n = 200 (le résultat peut facilement être vérifié de tête).
  3. On pose \(S_n = \displaystyle\sum_{i=0}^{n} i \). Ecrire la relation de récurrence entre \(S_n\) et \(S_{n-1}\).
  4. En déduire une fonction récursive sommeR(n:int)->int renvoyant \(\displaystyle\sum_{i=0}^{n} i \).
  5. Tester avec n = 200.
  6. Tester avec n = 2000 ; pour quelle raison obtient-on un message d'erreur ?

L'écriture def f(paramètre:type)->type s'appelle une annotation, elle permet de préciser le type des paramètres et le type de la grandeur renvoyée afin de faciliter la compréhension deu code (sans effet sur l'exécution : aucune vérification n'est effectuée).


Présence d'un élément dans une liste

Rappels - Slicing : L[i:] renvoie la sous-liste composée des éléments de L à partir de l'index i ; L[-1] renvoie le dernier élément de L.
En déduire ce que renvoie L[:-1] puis vérifier sur un exemple.

  1. Ecrire une fonction itérative presentI(L:list,e)->bool renvoyant un booléen (True ou False) suivant que l'élément e est présent ou non dans la liste L.
  2. Effectuer au moins deux tests (élement présent / élément absent).
  3. Quelle relation de récurrence peut-on imaginer ?
  4. En déduire une fonction récursive presentR(L:list,e)->bool effectuant le même travail. Plusieurs solutions sont envisageables suivant que l'élément enlevé de la liste est le premier ou le dernier.
  5. Tester la fonction.

Fonction mystere

Tester les commandes suivantes : False + 5 ; True + 5. Conclusion ?


Que fait la fonction suivante ? Expliquer son fonctionnement.


Remarque les parenthèses autour de L[0]==e sont indispensables pour forcer l'évaluation. On pourrait aussi écrire int((L[0]==e)).

pgcd par la méthode d’Euclide

Algorithme : pgcd(a, 0) = a et pgcd(a, b) = pgcd(b, a%b)
« Pour calculer le pgcd de a et de b, on remplace a par b et b par a%b tant que b n'est pas nul »
Rq : au cours de cet algorithme a et b sont modfiés.

Rappel : a%b est le reste dans la division Euclidienne de a par b.

  1. Ecrire une fonction itérative pgcdI(a,b:int)->int.
  2. Effectuer des tests.
  3. Déduire de la relation de récurrence ci-dessus une fonction récursive pgcdR(a,b:int)->int.
  4. Tester la fonction.


Récursivité - Compléments

Récursivité - Ordre de l'évaluation des appels

Rappel : 'a'*4 renvoie aaaa.

Ecrire une fonction récursive f(n) renvoyant, avec n = 5 :
*****
****
***
**
*

Ecrire une fonction récursive g(n) renvoyant, avec n = 5 :
*
**
***
****
*****


Appels récursifs multiples

On considère la suite définie par \(u_0 = 2\) et \(u_n = \displaystyle\frac{1}{2} \left( u_{n-1} + \displaystyle\frac{3}{u_{n-1}} \right)\).

Pour quelle raison le code suivant est-il terriblement maladroit ?


Le problème est resolu en utilisant une variable permettant de stocker le résultat de \(u_{n-1}\) :


Prolongement : exercice sur la suite de Fibonacci (plus loin) pour un exemple de suite où \(u_n\) est définie à patir de \(u_{n-1}\) et \(u_{n-2}\).

Appel récursif à l'intérieur d'une fonction

Que fait la fonction suivante ? Expliquer son fonctionnement.


Récursivité - Complexité temporelle

Factorielle

  1. Ecrire une fonction itérative factorielleI(n:int)->int renvoyant \(n !\).
  2. Tester avec n = 10 (vérifier le résultat avec l'une des fonctions de python, numpy et scipy : math.factorial, numpy.math.factorial et scipy.math.factorial).
  3. Ecrire la relation de récurrence entre \(n!\) et \((n-1) !\).
  4. En déduire une fonction récursive factorielleR(n:int)->int.
  5. Tester avec n = 10.

Etude expérimentale de la complexité temporelle

On souhaite comparer les performances de différentes fonctions programmées en langage python (factorielle, puissance...) à celles des bibliothèques scientifiques scipy et numpy.
On utilise la fonction perf_counter() du module time qui renvoie une durée écoulée depuis un instant origine : on accède à une durée en effectuant la différence entre deux valeurs consécutives renvoyées par perf_counter().

La fonction "temps" ci-dessous permet de calculer un temps moyen d'exécution d'une fonction. L'ordinateur étant un système multitâche, il n'est pas évident que toutes les ressources disponibles (processeur, mémoire...) soient les mêmes d'un calcul à l'autre, on effectue donc une moyenne sur un grand nombre d'exécutions.

La fonction "temps()" ci-dessous est utilisée pour évaluer des temps de calcul : regarder le code pour en comprendre les étapes essentielles (ignorer la ligne print(...)) et observer l'exemple d'utilisation.

Pour évaluer la moyenne de 1000 exécutions de la fonction \(f(x,y)\) avec les arguments x = -2 et y = 5, le code à saisir est :
temps(f,-2,5)


Opérateur splat : def f(*args): permet de définir une fonction avec un nombre variable de paramètres.

Comparer les temps d'exécution de factorielleI, factorielleR, math.factorial, numpy.math.factorial et scipy.math.factorial avec n = 200.


Exponentiation rapide (calcul de xn)

Rappel : \(\left\lfloor a \right\rfloor\) désigne la partie entière de a. En python, \(\left\lfloor \frac{a}{2} \right\rfloor\) est obtenu par a//2.
Pour tester la parité d'un entier n, il suffit de calculer le reste dans la division Euclidienne de n par 2 : n % 2 en python.

  1. Ecrire une fonction itérative puissanceI(x:float,n:int)->float renvoyant \(x^n\) sans utiliser l'opérateur ** ou une fonction python prédéfinie.
  2. Tester la fonction.
  3. Ecrire la relation de récurrence entre \(x^n\) et \(x^{n-1}\).
  4. En déduire une fonction récursive puissanceR(x:float,n:int)->float.
  5. Tester.
  6. L'algorithme d'exponentiation rapide est le suivant :
    - si n est pair \(x^n = x^{\left\lfloor \frac{n}{2} \right\rfloor} \times x^{\left\lfloor \frac{n}{2} \right\rfloor}\) ;
    - si n est impair \(x^n = x^{\left\lfloor \frac{n}{2} \right\rfloor} \times x^{\left\lfloor \frac{n}{2} \right\rfloor} \times x\).
    Illustrer l’intérêt de cette méthode, en déroulant l’algorithme « à la main » et en comptant le nombre de multiplications à effectuer pour calculer x37 avec cette méthode et comparer avec la fonction itérative puissanceI.
  7. Ecrire une fonction récursive puissance2(x:float,n:int)->float en utilisant l'algorithme d'exponentiation rapide.
  8. Tester la fonction.
  9. En utilisant l’écriture de n en base 2 : \((n)_{10} = \displaystyle\sum\limits_{i = 0}^p {a_i 2^i} \) (où ai est égal à 0 ou 1), montrer que la complexité de cette fonction est en \(O(log(n))\).

Etude expérimentale de la complexité temporelle

Comparer les temps d'exécution de puissanceI, puissanceR, puissance2, math.pow, numpy.math.pow et scipy.math.pow avec x = 2.5 et n = 500.


Suite de Fibonacci

Rappels - Matrices


La suite de Fibonacci est définie par : \(\left\{ \begin{array}{l}{F_0} = 1\\{F_1} = 1\\{F_n} = {F_{n - 1}} + {F_{n - 2}}\,\,\,\,\,n \ge 2\end{array} \right.\).

  1. Ecrire une fonction itérative fibonacciI(n:int)->int.
  2. Tester la fonction.
  3. Déduire de la relation de récurrence ci-dessus une fonction récursive fibonacciR(n:int)->int.
  4. Tester.
  5. Expliquer pour quelle raison cette fonction est terriblement inefficace (en temps de calcul).
  6. La relation de récurrence ci-dessus peut s’écrire sous forme matricielle : \(\left[ \begin{array}{l}{F_n}\\{F_{n - 1}}\end{array} \right] = \left[ {\begin{array}{*{20}{c}}1&1\\1&0\end{array}} \right]\left[ \begin{array}{l}{F_{n - 1}}\\{F_{n - 2}}\end{array} \right]\)
    En déduire une relation entre \(\left[ \begin{array}{l}{F_n}\\{F_{n - 1}}\end{array} \right] \) et \(\left[ \begin{array}{l}{F_1}\\{F_0}\end{array} \right]\).
  7. Écrire une fonction puissanceM(M,n) (de complexité optimum) qui renvoie \(M^n\) où M> est une matrice carrée et n> un entier.
  8. En déduire une fonction fibonacciM(n:int)->int plus efficace.
  9. Tester la fonction.

Etude expérimentale de la complexité temporelle

Comparer les temps d'exécution de fibonacciI et fibonacciM.