Files
TermNSI/Recursivité/README.md

354 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Récursivité
### Le programme
![bo_recusivite.png](assets/bo_recursivite.png)
> La récursivité est un style de programmation permettant de simplifier l'écriture de nombreux problèmes.
### 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 œ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.**
-------------------------------
### Mauvais exemple
Rien de tel qu'un mauvais usage pour mieux comprendre un concept.
Voici une fonction récursive :
```python
def fct_recursive() :
print("Je fais un appel de la fonction")
fct_recursive()
```
Cette fonction s'appelle elle-même, mais elle présente un problème.
Si nous l'appelons, le résultat sera celui-ci :
```python
>>> fct_recursive()
"Je fais un appel de la fonction"
"Je fais un appel de la fonction"
"Je fais un appel de la fonction"
"Je fais un appel de la fonction"
"Je fais un appel de la fonction"
"Je fais un appel de la fonction"
"Je fais un appel de la fonction"
...
```
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 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 :
Voici la fonction somme(n) se définissant comme :
$$
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 # Cas de base
else :
return n + somme(n-1) # Cas récursif
```
Cette fonction s'appelle donc elle-même jusqu'à avoir n égal à 0.
Mais quel sera le résultat de *somme(3)* ?
Pour obtenir cette réponse nous allons dérouler *à la main* le code :
```
somme(3) = 3 + somme(2)
somme(2) = 2 + somme(1)
somme(1) = 1 + somme(0)
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 reprendre les appels de fonctions pour revenir jusqu'à somme(3) :
```
somme(0) = 0
somme(1) = 1 + 0 = 1
somme(2) = 2 + 1 = 3
somme(3) = 3 + 3 = 6
```
Ici nous observons donc 4 appels de fonctions pour résoudre somme(3).
-----------
### La pile d'appels
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 environ **1000 appels** par défaut.
Si cette limite est dépassée alors l'erreur **RecursionError** apparaîtra :
```python
>>> somme(1001)
...
RecursionError: maximum recursion depth exceeded in comparison
```
> 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é.
-----------
### Terminaison d'une fonction récursive
Une question essentielle se pose : **comment être sûr qu'une fonction récursive s'arrête ?**
#### Le variant de boucle
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
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:
return 0
elif n == 1:
return 1
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>.