Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1213 minDébutant

Pour aller plus loin : récursivité et complexité

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire une fonction récursive avec son cas de base ;
  • suivre la pile d'appels d'une fonction récursive ;
  • choisir entre version récursive et version itérative ;
  • estimer la complexité d'un algorithme avec la notation O.

4 exercices corrigés1 schéma

Une fonction récursive est une fonction qui s'appelle elle-même sur un problème plus petit, jusqu'à un cas simple résolu directement ; la complexité d'un algorithme mesure comment son temps d'exécution (ou sa mémoire) augmente avec la taille des données. Ces deux notions sont hors du programme de l'épreuve E3 du BTS SIO, mais elles servent en SLAM et dans toute poursuite d'études : ce chapitre en donne l'essentiel, avec des exemples en Python.

Une fonction qui s'appelle elle-même

La factorielle de n vaut n! = n × (n − 1) × … × 1. On remarque que n! = n × (n − 1)! : pour calculer 5!, il suffit de savoir calculer 4!, puis de multiplier par 5. Et 1! vaut 1, sans calcul. C'est une définition récursive : elle se sert d'elle-même, sur un nombre plus petit.

text
Fonction Factorielle(n : entier) : entier
// Précondition : n ≥ 1
    Si n = 1 Alors
        Retourner 1                       // cas de base
    Sinon
        Retourner n × Factorielle(n - 1)  // appel récursif
    FinSi
FinFonction
python
def factorielle(n):
    if n <= 1:
        return 1                        # cas de base
    return n * factorielle(n - 1)       # appel récursif


print(factorielle(5), factorielle(20))
text
120 2432902008176640000

Toute fonction récursive a deux parties, et il en faut absolument deux :

  1. Un cas de base, résolu sans appel récursif. Le programme dit « nécessité d'un test » et « nécessité de cas particuliers résolus sans appel à la récursivité ».
  2. Un appel récursif sur un problème plus petit, qui se rapproche du cas de base. C'est ce qui garantit la finitude.

La pile d'appels

Quand factorielle(4) appelle factorielle(3), le premier appel n'est pas fini : il attend le résultat pour faire sa multiplication. Python garde chaque appel en attente, avec ses variables locales, sur une pile d'appels. Les appels s'empilent jusqu'au cas de base, puis se dépilent en renvoyant leurs résultats.

Descente : les appels s'empilentRemontée : les résultats reviennentfactorielle(4) attend 4 × factorielle(3)factorielle(4) renvoie 4 × 6 = 24factorielle(3) attend 3 × factorielle(2)factorielle(3) renvoie 3 × 2 = 6factorielle(2) attend 2 × factorielle(1)factorielle(2) renvoie 2 × 1 = 2factorielle(1) : cas de basefactorielle(1) renvoie 1
La pile d'appels de factorielle(4) : les appels s'empilent jusqu'au cas de base, puis les résultats remontent

Cette pile a une taille limitée. Par défaut, Python refuse d'empiler plus de 1 000 appels environ : factorielle(5000) lève RecursionError: maximum recursion depth exceeded. Une fonction récursive sans cas de base (ou dont l'appel ne se rapproche pas du cas de base) provoque la même erreur : c'est l'équivalent récursif de la boucle infinie.

Récursif ou itératif ?

Tout algorithme récursif peut s'écrire avec une boucle, et inversement. La factorielle itérative est le produit du chapitre 4. Le programme de maths cite d'ailleurs la « conversion en algorithme non récursif ».

RécursifItératif
LisibilitéTrès naturel quand le problème est lui-même récursif (arbres, dossiers, expressions)Plus naturel pour un simple parcours
MémoireUn étage de pile par appel en coursQuelques variables
LimiteProfondeur maximale (environ 1 000 en Python)Aucune
RisqueOubli du cas de baseBoucle infinie

La récursivité brille quand les données sont imbriquées. Une arborescence de dossiers contient des fichiers et des sous-dossiers, qui contiennent eux-mêmes des fichiers et des sous-dossiers… Calculer sa taille totale avec des boucles est pénible ; en récursif, c'est quatre lignes :

python
def taille(element):
    """Taille d'un fichier (un entier, en Ko) ou d'un dossier (un dictionnaire)."""
    if isinstance(element, int):      # cas de base : un fichier
        return element
    total = 0
    for contenu in element.values():  # un dossier : la somme de ses contenus
        total = total + taille(contenu)
    return total


partage = {
    "compta": {"bilan.xlsx": 820, "factures": {"f1.pdf": 120, "f2.pdf": 95}},
    "rh": {"contrats": {"modele.docx": 45}},
    "lisezmoi.txt": 2,
}
print(taille(partage), "Ko")
text
1082 Ko

isinstance(element, int) teste si l'élément est un entier. La fonction ne sait pas à l'avance combien de niveaux de dossiers il y a : chaque appel traite un niveau et délègue les sous-dossiers à d'autres appels.

Attention en revanche aux récursions qui recalculent la même chose. La suite de Fibonacci écrite naïvement, fib(n) = fib(n - 1) + fib(n - 2), fait 21 891 appels pour fib(20) et 2 692 537 pour fib(30), car fib(28) est calculé deux fois, fib(27) trois fois, et ainsi de suite. La version itérative du chapitre 4 fait 30 tours de boucle.

La complexité d'un algorithme

Deux programmes peuvent donner le même résultat avec des temps très différents. Mesurer le temps en secondes dépend de la machine ; on compte plutôt le nombre d'opérations significatives (comparaisons, affectations, tours de boucle) en fonction de la taille n des données. C'est ce que tu as fait pour les tris.

On s'intéresse surtout au pire cas, qui donne une garantie, et à la façon dont le coût grandit quand n grandit, pas au nombre exact d'opérations.

La notation O

La notation O (« grand O ») décrit l'ordre de grandeur du nombre d'opérations, en ignorant les constantes et les termes secondaires : n(n − 1)/2 comparaisons s'écrit O(n²), 3n + 2 s'écrit O(n).

NotationNomExemple du coursn = 1 000n = 1 000 000
O(1)ConstanteAccéder à t[i], tester la parité11
O(log n)LogarithmiqueRecherche dichotomique~10~20
O(n)LinéaireSomme, maximum, recherche séquentielle1 0001 000 000
O(n log n)Quasi linéairesorted de Python, tri fusion~10 000~20 000 000
O(n²)QuadratiqueTris par sélection, à bulles, par insertion1 000 0001 000 000 000 000
O(2ⁿ)ExponentielleFibonacci récursif naïfinutilisableinutilisable

À raison de cent millions d'opérations par seconde (un ordre de grandeur pour un programme simple), O(n²) sur un million d'éléments représente plus de deux heures et demie de calcul ; O(n log n), une fraction de seconde. Le choix de l'algorithme compte bien plus que la vitesse de la machine.

Pour estimer la complexité d'un algorithme, trois règles suffisent :

  • des instructions sans boucle coûtent O(1) ;
  • une boucle de n tours qui contient un corps en O(1) coûte O(n) ; deux boucles imbriquées de n tours coûtent O(n²) (on multiplie) ; deux boucles successives coûtent O(n) + O(n) = O(n) (on additionne, et on garde le plus grand terme) ;
  • une boucle qui divise la taille par 2 à chaque tour coûte O(log n).

On parle aussi de complexité en mémoire (ou spatiale) : la place occupée en plus des données. Trier sur place coûte O(1) en mémoire ; construire une nouvelle liste de n éléments, O(n) ; une récursion de profondeur n, O(n) pour la pile d'appels.

Exercices

Exercice 1 sur 4 Somme des chiffres en récursif Réussi À revoir

Écris une fonction récursive somme_chiffres(n) qui renvoie la somme des chiffres d'un entier n ≥ 0 (2026 donne 2 + 0 + 2 + 6 = 10). Indique le cas de base et l'appel récursif.

Voir la réponseMasquer la réponse de l'exercice 1

La somme des chiffres de n est le dernier chiffre (n % 10) plus la somme des chiffres des autres (n // 10). Un nombre d'un seul chiffre est sa propre somme.

python
def somme_chiffres(n):
    if n < 10:                               # cas de base
        return n
    return n % 10 + somme_chiffres(n // 10)  # appel sur un nombre plus petit


print(somme_chiffres(2026))

Résultat : 10. Chaque appel enlève un chiffre : le nombre d'appels est le nombre de chiffres de n.

Tu avais trouvé ?

Exercice 2 sur 4 Trace de la pile Réussi À revoir

Écris la suite des appels et des valeurs renvoyées pour puissance(2, 3).

python
def puissance(x, n):
    if n == 0:
        return 1
    return x * puissance(x, n - 1)
Voir la réponseMasquer la réponse de l'exercice 2

Descente : puissance(2, 3) appelle puissance(2, 2), qui appelle puissance(2, 1), qui appelle puissance(2, 0).

Remontée : puissance(2, 0) renvoie 1 (cas de base) ; puissance(2, 1) renvoie 2 × 1 = 2 ; puissance(2, 2) renvoie 2 × 2 = 4 ; puissance(2, 3) renvoie 2 × 4 = 8.

Quatre appels sont empilés au plus fort de l'exécution.

Tu avais trouvé ?

Exercice 3 sur 4 Quelle complexité ? Réussi À revoir

Donne la complexité en fonction de n de chaque extrait.

python
# A
total = 0
for i in range(n):
    total = total + i
for j in range(n):
    total = total - j

# B
nb = 0
for i in range(n):
    for j in range(i):
        nb = nb + 1

# C
k = n
while k > 1:
    k = k // 2

# D
x = t[0] + t[n - 1]
Voir la réponseMasquer la réponse de l'exercice 3
  • A : deux boucles successives de n tours : 2n opérations, soit O(n).
  • B : boucles imbriquées dont l'interne fait i tours : 0 + 1 + … + (n − 1) = n(n − 1)/2 tours, soit O(n²).
  • C : k est divisé par 2 à chaque tour : environ log₂(n) tours, O(log n).
  • D : deux accès et une addition, quelle que soit la taille : O(1).

Tu avais trouvé ?

Exercice 4 sur 4 Recherche dichotomique récursive Réussi À revoir

Écris une version récursive de la recherche dichotomique : dicho_rec(t, x, deb, fin) cherche x dans t[deb..fin]. Quel est le cas de base ?

Voir la réponseMasquer la réponse de l'exercice 4

Deux cas de base : la zone est vide (deb > fin, on renvoie −1), ou l'élément du milieu est x (on renvoie son indice). Sinon, on relance la recherche sur la bonne moitié.

python
def dicho_rec(t, x, deb, fin):
    if deb > fin:
        return -1
    milieu = (deb + fin) // 2
    if t[milieu] == x:
        return milieu
    if t[milieu] < x:
        return dicho_rec(t, x, milieu + 1, fin)
    return dicho_rec(t, x, deb, milieu - 1)


t = [3, 8, 15, 23, 42, 56, 91]
print(dicho_rec(t, 42, 0, len(t) - 1), dicho_rec(t, 10, 0, len(t) - 1))

Résultat : 4 -1. La profondeur de la récursion est d'environ log₂(n) : 20 appels pour un million d'éléments, bien en dessous de la limite de Python.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : L'épreuve d'algorithmique de l'E3 (CCF)Revoir le sommaire du cours