TD14. Premières fonctions récursives
Le sujet de ce TD est disponible au format pdf ici.
L'objectif de ce TD est d'écrire nos premières fonctions récursives et de bien comprendre le flot de contrôle lors que ces fonctions récursives sont exécutées.
Préambule :
Dans ce TD nous allons faire de nouveaux dessins :)
À titre d'exemple, voici un programme Python exécutant une fonction récursive ainsi que le dessin associé à l'exécution de cette fonction.
Assurez-vous d'avoir bien compris ce qui se passe lors de l'exécution et demandez-vous ce qu'il se passe si on remplace 3
par -1
lors de l'appel initial à la fonction affiche_n_fois_hello
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Exercice 1 : conjecture de Syracuse
On se propose de ré-implémenter le calcul du nombre d’étapes pour que la suite de Syracuse atteigne 1. Pour rappel la suite de Syracuse est définie à partir d'un entier u_0 quelconque par :
- si u_i est pair alors u_{i+1} = u_i/2
- sinon u_{i+1} = 3 \times u_i + 1
Question 1
Implémenter la fonction calcule_nb_etapes_avant_1
en utilisant la récursivité.
Correction question 1
Cliquez ici pour révéler la correction.
Pour commencer, commençons par regarder une vidéo d'introduction aux fonctions récursives réalisée pendant la saison COVID 2020/2021.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Question 2
Exécuter pas-à-pas l'appel de fonction calcule_nb_etapes_avant_1(10)
.
Pour cela, dessiner schématiquement l'exécution sur le modèle du dessin de l'exécution de la fonction affiche_n_fois_hello
ci dessus.
Correction question 2
Cliquez ici pour révéler la correction.
Correction vidéo de l'exercice :
Exercice 2 : exponentiation rapide
On souhaite calculer x^y où x
est un float
ou un int
et y
un int
strictement plus grand que zéro.
Question 1
En remarquant que pour y
pair on a x^y = (x^{y/2})^2, proposer une fonction récursive calculant très rapidement x^y.
Correction question 1
Cliquez ici pour révéler la correction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Question 2
Exécuter pas à pas l'appel de fonction exponentiation(2, 10)
.
Pour cela, dessiner schématiquement l'exécution sur le modèle du dessin de l'exécution de la fonction affiche_n_fois_hello
ci dessus.
Correction question 2
Cliquez ici pour révéler la correction.
Correction vidéo de l'exercice :
Exercice 3 : suite de Fibonacci
On considère le programme suivant :
1 2 3 4 5 6 7 8 9 |
|
Question 1
Qu'affiche ce programme sur la sortie standard ?
Correction question 1
Cliquez ici pour révéler la correction.
Le plus simple pour savoir ce que va afficher le programme est de dessiner son exécution :
Une fois le dessin réalisé, on peut facilement dire ce que le programme va afficher en suivant l'exécution et en sachant que rang x
sera affiché quand on rentre dans un appel :
1 2 3 4 5 6 7 8 9 10 |
|
Question 2
Intuitivement, quelle est la complexité de la fonction fibonacci
?
Correction question 2
Cliquez ici pour révéler la correction.
En dessinant l'arbre d'appel plutôt que l'exécution au cours du temps, on imagine que c'est presque O(2^rang)
car tous les niveaux de l'arbre vont être "remplis" sauf le dernier.
Correction vidéo de l'exercice :
Exercice 4 : dichotomie récursive
Question 1
Implémenter une recherche dichotomique récursive dans un tableau trié.
La fonction implémentée renvoie True
si l'élément cherché est présent dans le tableau et False
sinon.
Correction question 1
Cliquez ici pour révéler la correction.
Il y a de très nombreuses façons de répondre à cette question. Le code ci-dessous en montre trois :
- une solution avec
tranches
(slices
en anglais) ; - une solution sans
tranches
; - une solution avec le module standard
bisect
.
L'objectif de l'exercice est de discuter des performances.
Créer des tranches coûte (O(taille de la tranche))
.
Pourquoi ? Parce qu'une nouvelle list
est créée, et qu'il faut donc recopier tous les éléments.
Ici au pire cas les tranches successives sont de taille n/2 , n/4 ... 0
, d’où un coût total linéaire en n
(en temps comme en espace).
On perd alors tout l’intérêt d’une dichotomie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
Le code ci-dessus donne les performances suivantes sur ma (Manu) machine :
1 2 3 4 5 6 |
|
On voit bien sur ces résultats que les tranches coûtent cher mais aussi que c'est mieux d'utiliser le module bisect
.
C'est attendu : avec l'implémentation de cpython
que nous utilisons c'est du C optimisé aux petits oignons, et pas du récursif !
Exercice 5 : Fibonacci qui va vite (pour aller plus loin)
Question 1
Implémenter une autre version de la fonction fibonacci
bien plus efficace que celle de l'exercice 3.
Correction question 1
Cliquez ici pour révéler la correction.
Deux solutions sont possibles pour avoir du O(rang)
:
- une solution itérative ;
- une solution récursive avec mémoisation.
Les voici :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
Le code ci-dessus donne les performances suivantes sur ma (Manu) machine :
1 2 3 4 5 6 |
|
Donc il semblerait que le facteur constant de la solution itérative soit deux fois plus petit que celui de la version récursive avec mémoisation. C'est certainement lié au coût des appels de fonction.
Bonus de Fred:
Un étudiant m'a proposé de calculer avec l'exponentiation rapide la rang-ième puissance de la matrice
0 1 1 1
pour un coût de O(log(rang)).