ajout de tous les cours et TP préparés cet été

This commit is contained in:
2026-01-17 23:10:49 +01:00
parent ed9415bc81
commit 301cf5a98f
125 changed files with 21614 additions and 542 deletions

View File

@@ -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 dUtilisation 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>.