ajout de tous les cours et TP préparés cet été
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
## Recursivité
|
||||
# Récursivité
|
||||
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
### Un peu de culture
|
||||
|
||||
La récursivité, en informatique, est une pratique qui permet à une fonction de s'appeler elle-même.
|
||||
On peut comparer cela à la [mise en abyme](https://fr.wikipedia.org/wiki/Mise_en_abyme), procédé littéraire qui décrit une oeuvre incrustée dans elle même
|
||||
On peut comparer cela à la [mise en abyme](https://fr.wikipedia.org/wiki/Mise_en_abyme), procédé littéraire qui décrit une œuvre incrustée dans elle-même.
|
||||
|
||||
<img src="assets/vache_qui_rit.gif" alt="vache qui rit" style="zoom: 67%;" />
|
||||
|
||||
**La fonction récursive s'appelle elle même dans sa définition.**
|
||||
**La fonction récursive s'appelle elle-même dans sa définition.**
|
||||
|
||||
-------------------------------
|
||||
|
||||
@@ -39,9 +39,9 @@ def fct_recursive() :
|
||||
fct_recursive()
|
||||
```
|
||||
|
||||
Cette fonction s'appelle elle même, mais elle présente un problème.
|
||||
Cette fonction s'appelle elle-même, mais elle présente un problème.
|
||||
|
||||
Si nous l'appelons, le résultat sera celui-ci :
|
||||
Si nous l'appelons, le résultat sera celui-ci :
|
||||
|
||||
```python
|
||||
>>> fct_recursive()
|
||||
@@ -55,33 +55,38 @@ Cette fonction s'appelle elle même, mais elle présente un problème.
|
||||
...
|
||||
```
|
||||
|
||||
Tout comme les boucles, il faut créer une instruction permettant d'arrêter l'exécution du code, en récursif nous l'appelons **le cas d'arrêt**. Il permettra d'éviter les appels **infinis**
|
||||
Tout comme les boucles, il faut créer une instruction permettant d'arrêter l'exécution du code, en récursif nous l'appelons **le cas de base** (ou cas d'arrêt). Il permettra d'éviter les appels **infinis**.
|
||||
|
||||
|
||||
|
||||
### Le cas d'arrêt
|
||||
### Le cas de base
|
||||
|
||||
Afin de stopper l'exécution du code il est donc essentiel de créer une instruction n'appelant pas notre fonction.
|
||||
|
||||
Par exemple :
|
||||
Par exemple :
|
||||
|
||||
Voici la fonction somme(n) se définissant comme :
|
||||
|
||||
$`somme(n) = \left\{ \begin{array}{ll} 0 {~si~} n = 0\\ n + somme(n -1){~sinon.} \end{array}\right.`$
|
||||
$$
|
||||
somme(n) = \begin{cases}
|
||||
0 & \text{si } n = 0 \text{ (cas de base)}\\
|
||||
n + somme(n-1) & \text{sinon (cas récursif)}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
```python
|
||||
def somme(n) :
|
||||
if n == 0 :
|
||||
return 0
|
||||
return 0 # Cas de base
|
||||
else :
|
||||
return n + somme(n-1)
|
||||
return n + somme(n-1) # Cas récursif
|
||||
```
|
||||
|
||||
Cette fonction s'appelle donc elle même jusqu'à avoir n égal à 0.
|
||||
Cette fonction s'appelle donc elle-même jusqu'à avoir n égal à 0.
|
||||
|
||||
Mais quel sera le résultat de *somme(3)* ?
|
||||
Mais quel sera le résultat de *somme(3)* ?
|
||||
|
||||
Pour obtenir cette réponse nous allons dérouler *à la main* le code :
|
||||
Pour obtenir cette réponse nous allons dérouler *à la main* le code :
|
||||
|
||||
```
|
||||
somme(3) = 3 + somme(2)
|
||||
@@ -92,11 +97,11 @@ somme(0) = 0
|
||||
|
||||
Voici les différents appels de fonctions pour somme(3). Mais là, le résultat n'est pas encore obtenu.
|
||||
|
||||
Pour cela il faut pour cela reprendre les appels de fonctions pour revenir jusqu'à somme(3)
|
||||
Pour cela il faut reprendre les appels de fonctions pour revenir jusqu'à somme(3) :
|
||||
|
||||
```
|
||||
somme(0) = 0
|
||||
somme(1) = 1 + 0 = 1
|
||||
somme(1) = 1 + 0 = 1
|
||||
somme(2) = 2 + 1 = 3
|
||||
somme(3) = 3 + 3 = 6
|
||||
```
|
||||
@@ -105,15 +110,48 @@ Ici nous observons donc 4 appels de fonctions pour résoudre somme(3).
|
||||
|
||||
-----------
|
||||
|
||||
### Appel de fonction en python
|
||||
### La pile d'appels
|
||||
|
||||
En python il existe une limite au nombre d'appel de fonction que l'on peut faire pour une fonction récursive.
|
||||
Lorsqu'une fonction récursive s'exécute, Python utilise une structure de données appelée **pile d'appels** (ou *call stack*) pour gérer les différents appels.
|
||||
|
||||
#### Principe de fonctionnement
|
||||
|
||||
À chaque appel de fonction :
|
||||
1. Python **empile** un nouveau *contexte d'exécution* contenant les paramètres et variables locales
|
||||
2. Lorsque le cas de base est atteint, les appels sont **dépilés** un par un
|
||||
3. Chaque résultat remonte vers l'appel précédent
|
||||
|
||||
#### Visualisation avec somme(3)
|
||||
|
||||
```
|
||||
EMPILEMENT DÉPILEMENT
|
||||
(descente) (remontée)
|
||||
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ somme(0) │ ← cas de base │ somme(0) │ → retourne 0
|
||||
├─────────────┤ ├─────────────┤
|
||||
│ somme(1) │ attend somme(0) │ somme(1) │ → retourne 1+0 = 1
|
||||
├─────────────┤ ├─────────────┤
|
||||
│ somme(2) │ attend somme(1) │ somme(2) │ → retourne 2+1 = 3
|
||||
├─────────────┤ ├─────────────┤
|
||||
│ somme(3) │ attend somme(2) │ somme(3) │ → retourne 3+3 = 6
|
||||
└─────────────┘ └─────────────┘
|
||||
BASE BASE
|
||||
```
|
||||
|
||||
> La pile fonctionne selon le principe **LIFO** (Last In, First Out) : le dernier appel empilé est le premier à être résolu.
|
||||
|
||||
-----------
|
||||
|
||||
### Limite de récursion en Python
|
||||
|
||||
En Python il existe une limite au nombre d'appels de fonction que l'on peut faire pour une fonction récursive.
|
||||
|
||||
En effet, avec notre exemple nous voyons que pour obtenir somme(3) nous faisons 4 appels de fonction.
|
||||
|
||||
Ces appels de fonction **s'empilent** en espace mémoire, python peut stocker autour de 1000 appels.
|
||||
Ces appels de fonction **s'empilent** en espace mémoire. Python peut stocker environ **1000 appels** par défaut.
|
||||
|
||||
Si cette limite est dépassée alors l'erreur **RecursionError** apparaîtra.
|
||||
Si cette limite est dépassée alors l'erreur **RecursionError** apparaîtra :
|
||||
|
||||
```python
|
||||
>>> somme(1001)
|
||||
@@ -121,37 +159,195 @@ Si cette limite est dépassée alors l'erreur **RecursionError** apparaîtra.
|
||||
RecursionError: maximum recursion depth exceeded in comparison
|
||||
```
|
||||
|
||||
### Autre type de récursivité
|
||||
> Cette limite existe pour protéger la mémoire de l'ordinateur. On peut la modifier avec `sys.setrecursionlimit()`, mais ce n'est généralement pas recommandé.
|
||||
|
||||
Il existe divers types de récursivité :
|
||||
-----------
|
||||
|
||||
- La récursivité multiple
|
||||
- La récursivité double
|
||||
- La récursivité imbriquée
|
||||
- etc ...
|
||||
### Terminaison d'une fonction récursive
|
||||
|
||||
Ces différents types de récursivité sont quelques peu différents de ce que l'on vient de voir. Mais nous pourrions les rencontrer dans la suite du programme.
|
||||
Une question essentielle se pose : **comment être sûr qu'une fonction récursive s'arrête ?**
|
||||
|
||||
### Récursivité double
|
||||
#### Le variant de boucle
|
||||
|
||||
En effet à la différence de la récursivité dite **simple** il y aura ici plusieurs appels de fonctions dans une même instruction.
|
||||
Pour prouver qu'une fonction récursive termine, on utilise un **variant** : une quantité entière qui :
|
||||
- est **positive ou nulle** à chaque appel
|
||||
- **décroît strictement** à chaque appel récursif
|
||||
|
||||
> En mathématiques, la suite de Fibonacci est une suite de nombres entiers dont chaque terme successif représente la somme des deux termes précédents, et qui commence par 0 puis 1. Ainsi, les dix premiers termes qui la composent sont 0, 1, 1, 2, 3, 5, 8, 13, 21 et 34.
|
||||
Quand le variant atteint 0 (ou une valeur minimale), on est dans le cas de base.
|
||||
|
||||
#### Exemple avec somme(n)
|
||||
|
||||
Pour la fonction `somme(n)` :
|
||||
- **Variant** : la valeur de `n`
|
||||
- À chaque appel récursif, on appelle `somme(n-1)`, donc `n` décroît de 1
|
||||
- Quand `n = 0`, on atteint le cas de base
|
||||
|
||||
| Appel | Valeur du variant (n) |
|
||||
|-------|----------------------|
|
||||
| somme(3) | 3 |
|
||||
| somme(2) | 2 |
|
||||
| somme(1) | 1 |
|
||||
| somme(0) | 0 → cas de base |
|
||||
|
||||
Le variant décroît strictement et atteint 0 : **la fonction termine**.
|
||||
|
||||
#### Attention aux erreurs
|
||||
|
||||
```python
|
||||
def mauvaise_somme(n):
|
||||
if n == 0:
|
||||
return 0
|
||||
else:
|
||||
return n + mauvaise_somme(n + 1) # n augmente !
|
||||
```
|
||||
|
||||
Ici, le paramètre **augmente** au lieu de diminuer : la fonction ne terminera jamais (jusqu'à la `RecursionError`).
|
||||
|
||||
-----------
|
||||
|
||||
### Écrire une fonction récursive : méthodologie
|
||||
|
||||
Pour écrire correctement une fonction récursive, il faut identifier **trois éléments** :
|
||||
|
||||
| Élément | Question à se poser | Exemple avec `somme(n)` |
|
||||
|---------|---------------------|------------------------|
|
||||
| **Cas de base** | Quel est le cas le plus simple, qui ne nécessite pas d'appel récursif ? | `n == 0` → retourne `0` |
|
||||
| **Cas récursif** | Comment décomposer le problème en un sous-problème plus petit ? | `somme(n) = n + somme(n-1)` |
|
||||
| **Variant** | Quelle quantité décroît à chaque appel ? | La valeur de `n` |
|
||||
|
||||
#### Exemple : la factorielle
|
||||
|
||||
On souhaite écrire `factorielle(n)` qui calcule `n! = n × (n-1) × ... × 2 × 1`.
|
||||
|
||||
1. **Cas de base** : `factorielle(0) = 1` (par convention, 0! = 1)
|
||||
2. **Cas récursif** : `factorielle(n) = n × factorielle(n-1)`
|
||||
3. **Variant** : `n` décroît de 1 à chaque appel
|
||||
|
||||
```python
|
||||
def factorielle(n):
|
||||
if n == 0:
|
||||
return 1 # Cas de base
|
||||
else:
|
||||
return n * factorielle(n - 1) # Cas récursif
|
||||
```
|
||||
|
||||
Vérification :
|
||||
```
|
||||
factorielle(4) = 4 × factorielle(3)
|
||||
= 4 × 3 × factorielle(2)
|
||||
= 4 × 3 × 2 × factorielle(1)
|
||||
= 4 × 3 × 2 × 1 × factorielle(0)
|
||||
= 4 × 3 × 2 × 1 × 1
|
||||
= 24
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||
### Récursif vs Itératif
|
||||
|
||||
Toute fonction récursive peut être réécrite de manière **itérative** (avec des boucles), et inversement.
|
||||
|
||||
#### Comparaison sur l'exemple de la somme
|
||||
|
||||
**Version récursive :**
|
||||
```python
|
||||
def somme_recursive(n):
|
||||
if n == 0:
|
||||
return 0
|
||||
else:
|
||||
return n + somme_recursive(n - 1)
|
||||
```
|
||||
|
||||
**Version itérative :**
|
||||
```python
|
||||
def somme_iterative(n):
|
||||
resultat = 0
|
||||
for i in range(1, n + 1):
|
||||
resultat += i
|
||||
return resultat
|
||||
```
|
||||
|
||||
#### Avantages et inconvénients
|
||||
|
||||
| Critère | Récursif | Itératif |
|
||||
|---------|----------|----------|
|
||||
| **Lisibilité** | Souvent plus élégant et proche de la définition mathématique | Peut être plus verbeux |
|
||||
| **Mémoire** | Utilise la pile d'appels (risque de débordement) | Utilise peu de mémoire |
|
||||
| **Performance** | Peut être plus lent (overhead des appels) | Généralement plus rapide |
|
||||
| **Cas d'usage** | Structures récursives (arbres, graphes), diviser pour régner | Calculs simples, itérations |
|
||||
|
||||
> En pratique, on choisit la récursivité quand elle rend le code **plus clair** ou quand le problème est **naturellement récursif** (parcours d'arbres, fractales, etc.).
|
||||
|
||||
-----------
|
||||
|
||||
### Autres types de récursivité
|
||||
|
||||
Il existe divers types de récursivité :
|
||||
|
||||
- La **récursivité simple** : un seul appel récursif (comme `somme`)
|
||||
- La **récursivité multiple** : plusieurs appels récursifs dans la fonction
|
||||
- La **récursivité mutuelle** : deux fonctions qui s'appellent mutuellement
|
||||
- La **récursivité terminale** : l'appel récursif est la dernière opération
|
||||
|
||||
Ces différents types de récursivité sont quelque peu différents de ce que l'on vient de voir. Mais nous pourrions les rencontrer dans la suite du programme.
|
||||
|
||||
### Récursivité multiple : la suite de Fibonacci
|
||||
|
||||
À la différence de la récursivité dite **simple**, il y aura ici **plusieurs appels de fonctions** dans une même instruction.
|
||||
|
||||
> En mathématiques, la suite de Fibonacci est une suite de nombres entiers dont chaque terme successif représente la somme des deux termes précédents, et qui commence par 0 puis 1. Ainsi, les dix premiers termes qui la composent sont 0, 1, 1, 2, 3, 5, 8, 13, 21 et 34.
|
||||
|
||||
$$
|
||||
fib(n) = \begin{cases}
|
||||
0 & \text{si } n = 0\\
|
||||
1 & \text{si } n = 1\\
|
||||
fib(n-1) + fib(n-2) & \text{sinon}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
```python
|
||||
def fibonacci(n):
|
||||
if n == 0 :
|
||||
if n == 0:
|
||||
return 0
|
||||
elif n == 1 :
|
||||
elif n == 1:
|
||||
return 1
|
||||
else :
|
||||
return fibonnaci(n-1) + fibonnaci(n-2)
|
||||
else:
|
||||
return fibonacci(n-1) + fibonacci(n-2)
|
||||
```
|
||||
|
||||
#### Arbre des appels pour fibonacci(4)
|
||||
|
||||
```
|
||||
fibonacci(4)
|
||||
/ \
|
||||
fibonacci(3) fibonacci(2)
|
||||
/ \ / \
|
||||
fibonacci(2) fibonacci(1) fibonacci(1) fibonacci(0)
|
||||
/ \ | | |
|
||||
fibonacci(1) fibonacci(0) 1 1 0
|
||||
| |
|
||||
1 0
|
||||
```
|
||||
|
||||
On remarque que `fibonacci(2)` est calculé **deux fois**, et `fibonacci(1)` **trois fois**. Cette redondance rend l'algorithme très inefficace pour de grandes valeurs de `n`.
|
||||
|
||||
> Ce problème sera résolu grâce à la **programmation dynamique**, que nous verrons dans un prochain chapitre.
|
||||
|
||||
-----------------
|
||||
|
||||
### À retenir
|
||||
|
||||
- Une fonction **récursive** s'appelle elle-même
|
||||
- Elle doit avoir un **cas de base** pour s'arrêter
|
||||
- On prouve la **terminaison** avec un **variant** (quantité qui décroît)
|
||||
- Les appels s'empilent dans la **pile d'appels**
|
||||
- Python limite le nombre d'appels récursifs (~1000)
|
||||
- Récursif ≠ toujours meilleur : choisir selon le problème
|
||||
|
||||
-----------------
|
||||
|
||||
Auteurs : Florian Mathieu, Timothée Decoster, Enzo Frémeaux
|
||||
|
||||
Licence CC BY NC
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Licence Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a> <br />Ce cours est mis à disposition selon les termes de la <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International</a>.
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Licence Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a> <br />Ce cours est mis à disposition selon les termes de la <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International</a>.
|
||||
|
||||
Reference in New Issue
Block a user