Complexité des algorithmes itératifs et récursifs : cours et exercices corrigés de prépa
Coût d'un algorithme, opérations élémentaires, complexité temporelle et spatiale dans le meilleur et le pire cas, notation grand O et ordres de grandeur usuels, coût des opérations de Python, complexité des programmes itératifs et récursifs, équations de récurrence usuelles, vérification par la mesure.
- 17méthodes
- 32exercices corrigés
- 3problèmes
- 20pièges
- 36figures
La complexité d'un algorithme mesure le nombre d'opérations élémentaires qu'il fait en fonction de la taille de son entrée, sans dépendre de la machine. On l'exprime avec la notation grand O, dans le meilleur et le pire cas, en temps et en mémoire. Le chapitre ouvre l'informatique de deuxième année en MP, PSI et TSI au Maroc : depuis 2019, l'exercice d'ouverture de l'épreuve d'informatique du CNC demande presque chaque année de « déterminer la complexité, avec justification ». Tu y trouves le décompte des boucles for et while, le coût caché des opérations de Python (in, tranches, concaténation), la complexité d'une fonction récursive par une équation de récurrence, la complexité spatiale et la vérification par la mesure, puis des exercices corrigés tirés des sujets du CNC.
Ce que tu sauras faire
Un programme juste peut être inutilisable : il répond en une seconde sur un petit exemple et ne finit pas sur les vraies données. La complexité mesure le coût d'un algorithme indépendamment de la machine, pour comparer deux algorithmes et prévoir ce qui se passera sur une grande entrée. À la fin de ce chapitre, tu dois savoir :
- dire la taille de l'entrée et les opérations qu'on compte, et distinguer complexité temporelle et complexité spatiale ;
- trouver une entrée de meilleur cas et une entrée de pire cas, et compter les opérations dans chacun ;
- utiliser la notation et reconnaître les ordres de grandeur usuels ;
- calculer la complexité d'un programme itératif : séquences, tests, boucles
foretwhile, appels de fonctions ; - connaître le coût des opérations de Python sur les listes, les chaînes et les dictionnaires, et repérer un coût caché ;
- écrire et résoudre l'équation de récurrence d'une fonction récursive, y compris quand elle coupe en deux ;
- calculer la mémoire utilisée par un programme, pile d'appels comprise ;
- vérifier une complexité par la mesure, et rédiger une justification sur une copie.
L'essentiel : Poser le problème Extrait gratuit
- Taille de l'entrée : longueur d'une liste ou d'une chaîne, valeur d'un entier, dimensions d'une matrice ; plusieurs tailles si besoin ( et ), toujours nommées.
- Opération élémentaire : coût indépendant de (affectation, comparaison, opération arithmétique sur des nombres bornés, accès
L[i]). - Complexité temporelle : nombre d'opérations élémentaires en fonction de . Complexité spatiale : mémoire supplémentaire, pile d'appels comprise.
- Meilleur cas, pire cas : à fixé, l'entrée qui coûte le moins, celle qui coûte le plus. Le cas moyen se nomme, il ne se calcule pas au programme.
Une méthode du chapitre Extrait gratuit
C'est la question type de l'exercice d'ouverture du CNC et de nombreux problèmes : quelques lignes de code, et « déterminer la complexité de la fonction, justifier votre réponse ».
La justification est exigée et c'est elle qui rapporte les points : une réponse sans justification ne vaut rien. Le pire cas est sous-entendu quand l'énoncé ne précise pas ; la notation attendue est .
- Lire la fonction et nommer la taille de l'entrée (méthode 1).
- Repérer le cas étudié (pire cas par défaut) et l'entrée qui l'atteint (méthode 3).
- Compter : tours de boucles (méthodes 5, 6), coûts cachés (méthode 7), ou équation de récurrence (méthode 8).
- Conclure par un simplifié (méthode 4).
- Rédiger en deux ou trois phrases : définition de , décompte, conclusion.
« Notons = len(L). Dans le pire cas (aucune sortie anticipée, la boucle va jusqu'au bout), la boucle fait tours, chacun avec une comparaison : la complexité est en . »
def est_croissante(L):
# L : liste de nombres ; renvoie True si L est
# triée dans l'ordre croissant, False sinon
for i in range(len(L) - 1):
if L[i] > L[i + 1]:
return False
return True
Réponse rédigée : « Notons = len(L). Chaque tour fait une comparaison, en temps constant. Dans le pire cas (liste croissante, par exemple [1, 3, 3, 8]), la boucle fait tours : la complexité est en . Dans le meilleur cas (L[0] > L[1]), un seul tour : . »
- Donner le résultat sans justification.
- Oublier de définir .
- Recopier la complexité d'une fonction ressemblante sans compter.
Exercices 2, 3, 4, 5, 6, 14, 15, 16 et 17, problèmes 1, 2 et 3.
Un exercice corrigé Extrait gratuit
Aux questions 1 et 3 du sujet, on a écrit les deux fonctions suivantes (la fonction somme(n) de ce sujet n'a rien à voir avec la fonction somme(L) du cours).
def somme(n):
# n : entier, n >= 1 ; renvoie 1 + 2 + ... + n
s = 0
for i in range(1, n + 1):
s = s + i
return s
def terme(k):
# k : entier, k >= 1 ; renvoie k / (1 + 2 + ... + k)
return k / somme(k)
print(somme(4), terme(4))
Le programme affiche :
10 0.4
Q.2. Déterminer la complexité de la fonction somme(n).
Q.4. Déterminer la complexité de la fonction terme(k).
- Méthode 1 : ici la taille de l'entrée est la valeur de l'entier, pour la première fonction, pour la seconde.
- Méthode 7 :
terme(k)ne contient pas de boucle, mais elle appelle une fonction qui en contient une.
Q.2.
Analyse.
La boucle for i in range(1, n + 1) fait tours ; chaque tour fait une addition et une affectation, en temps constant.
La fonction somme(n) fait tours de boucle de coût constant, plus l'initialisation et le return : sa complexité est en , linéaire en la valeur de .
Q.4. Analyse. Une fonction sans boucle n'est pas forcément en temps constant : on regarde ce qu'elle appelle.
terme(k) fait un appel somme(k), de coût , puis une division, en temps constant. Sa complexité est en .
Interprétation : ces deux fonctions ne sont pas les meilleures possibles. La formule donne la même valeur en un nombre constant d'opérations ; c'est le premier exemple du cours (section 1.1).
Au CNC, la première question de complexité porte souvent sur une fonction de l'exercice d'ouverture : on nomme le paramètre, on compte les tours, on n'oublie pas le coût des fonctions appelées. Voir les méthodes 1, 5 et 17.
Le cours détaillé, toute la fiche « L'essentiel », 17 méthodes, 20 pièges corrigés, la carte mentale, 32 exercices corrigés, 3 problèmes de concours, les fiches à mémoriser et l'assistant qui répond à tes questions. Ce chapitre est offert : il s’ouvre en entier avec un compte d’essai gratuit.
Ouvrir l'essai gratuitUne question ? WhatsAppPlan du cours
- 1. Pourquoi mesurer le coût d'un algorithme
- 1.1 Un même problème, plusieurs algorithmes
- 1.2 Mesurer le temps, et ses limites
- 2. Taille de l'entrée et opérations élémentaires
- 2.1 La taille de l'entrée
- 2.2 Opérations élémentaires et modèle de coût
- 2.3 Compter les opérations d'un programme itératif
- 3. Complexité temporelle et complexité spatiale
- 3.1 Complexité temporelle
- 3.2 Complexité spatiale
- 3.3 Le temps contre la mémoire
- 4. Meilleur cas, pire cas
- 4.1 Définitions
- 4.2 La recherche séquentielle
- 4.3 Le minimum d'une liste
- 4.4 Le cas moyen
- 5. La notation grand O
- 5.1 Définition
- 5.2 Règles de calcul
- 5.3 Plusieurs paramètres
- 6. Les ordres de grandeur usuels
- 6.1 Les classes de complexité
- 6.2 Ce que ces classes veulent dire en temps
- 7. Complexité d'un programme itératif
- 7.1 Séquence et alternative
- 7.2 Boucles
for - 7.3 Boucles
while - 7.4 Appels de fonctions
- 7.5 Deux exemples au meilleur et au pire cas
- 7.6 Un exemple type : évaluer un polynôme
- 8. Le coût des opérations de Python
- 8.1 Le tableau des coûts
- 8.2 Le coût caché
- 8.3 Remplacer une liste par un dictionnaire
- 8.4 Les chaînes de caractères
- 9. Complexité d'une fonction récursive
- 9.1 L'équation de récurrence
- 9.2 Le dépliage
- 9.3 Les récurrences qui divisent par 2
- 9.4 L'arbre des appels et le coût par niveau
- 9.5 Deux appels sur , ou sur et
- 9.6 Tableau des récurrences usuelles
- 9.7 Meilleur et pire cas d'une fonction récursive
- 9.8 Le coût caché des tranches et des copies
- 10. Complexité spatiale
- 10.1 Programmes itératifs
- 10.2 Programmes récursifs : la pile d'appels
- 10.3 Meilleur et pire cas en espace
- 10.4 La pile a une taille limitée
- 11. Vérifier par la mesure
- 11.1 Doubler la taille
- 11.2 L'échelle log-log
- 11.3 Compter au lieu de chronométrer
- 11.4 Ce que la mesure prouve, et ce qu'elle ne prouve pas
Les méthodes du chapitre
Chaque méthode est une fiche : quand l'utiliser, le rappel de cours, les étapes, la rédaction attendue sur la copie, un exemple traité, les erreurs à éviter.
- Identifier la taille de l'entrée et les paramètres de la complexité
- Choisir l'opération élémentaire et dresser le tableau des coûts
- Trouver et écrire une entrée de meilleur cas et une entrée de pire cas
- Simplifier en O
- Compter les tours de boucles for
- Compter les tours d'une boucle while
- Tenir compte du coût caché des opérations de Python et des fonctions appelées
- Écrire l'équation de récurrence d'une fonction récursive
- Résoudre une récurrence par dépliage ou comme suite arithmético-géométrique
- Résoudre une récurrence qui divise par 2
- Estimer un coût récursif par l'arbre des appels
- Repérer et supprimer le coût caché des tranches et des copies dans une fonction récursive
- Calculer la complexité spatiale, au meilleur et au pire cas
- Comparer deux versions et améliorer un programme
- Écrire un programme de complexité imposée et justifier qu'il la respecte
- Vérifier une complexité par la mesure et par un compteur
- Répondre à une question « Déterminer la complexité, avec justification » en temps limité
Les exercices corrigés
Chaque exercice a ses indications, sa correction rédigée et ce qu'il faut en retenir.
Application directe (13)
- Compter les opérations d'une somme pondérée
- Deux complexités d'ouverture
- Une factorielle cachée dans un calcul
- Factorielle et somme de factorielles
- Une somme entre deux bornes
- Grands éléments et médian
- Meilleur et pire cas de deux parcours
- Simplifier en O
- Le prix d'une ligne de Python
- Diviseurs et nombres premiers : compter les tours
- Nombre de chiffres et écriture binaire
- La dichotomie au mieux et au pire
- La factorielle récursive : récurrence, appels, pile
Entraînement (14)
- Une complexité qui dépend des valeurs
- Les nombres amis
- La méthode d'Euler et le coût des ajouts
- Compter les niveaux de gris
- Six boucles à compter
- Évaluer un polynôme : puissances, Horner et tranches
- Le coût caché des tranches dans une fonction récursive
- Puissance rapide : un appel ou deux
- Le tri par insertion au mieux et au pire
- Égalité de deux listes, itérative puis récursive
- Résoudre exactement : une suite à deux appels et Hanoï
- Le maximum par moitiés
- La mémoire de cinq fonctions
- Mesurer pour vérifier
Approfondissement (5)
- La tranche de somme maximale
- L'entier manquant
- Complexité et taille de l'entrée
- Fibonacci naïf : encadrer le coût
- L'algorithme d'Euclide est logarithmique
Les problèmes de concours
Des extraits de sujets réels, corrigés en entier, avec la durée conseillée et les méthodes mobilisées.
- Les deux points les plus rapprochés
- Rechercher un motif dans une séquence d'ADN
- Le carré magique
Questions fréquentes
Comment calculer la complexité d'un algorithme ?
On nomme d'abord la taille de l'entrée, (longueur d'une liste, valeur d'un entier, ou plusieurs paramètres). On choisit le cas étudié, souvent le pire. On compte les opérations élémentaires : une boucle de tours dont chaque tour coûte coûte , deux boucles imbriquées dépendantes font tours, une variable divisée par 2 donne tours. On conclut par un simplifié et une phrase de justification : sans justification, la réponse ne rapporte rien.
Que veut dire la notation grand O ?
signifie qu'il existe une constante et un rang tels que pour tout : croît au plus comme , à une constante près. On garde le terme dominant et on oublie les constantes : , . La base du logarithme ne compte pas, mais celle d'une exponentielle compte : n'est pas un .
Quelle différence entre meilleur cas et pire cas ?
On fixe la taille , puis on cherche l'entrée de cette taille qui coûte le moins (meilleur cas) et celle qui coûte le plus (pire cas). Pour la recherche séquentielle d'un élément dans une liste, le meilleur cas est l'élément en tête, , et le pire cas l'élément absent, . Le meilleur cas n'est jamais « petit ».
Comment calculer la complexité d'une fonction récursive ?
On écrit une équation de récurrence : le coût d'un appel hors appels récursifs, plus le coût des appels, avec la valeur au cas de base. donne , comme la factorielle ; donne , comme la dichotomie ou la puissance rapide ; donne , comme le tri par fusion ; donne , comme les tours de Hanoï. Pour une récurrence qui divise par 2, on suppose .
Quelle est la complexité de x in L, append et des tranches en Python ?
Sur une liste de longueur : L.append(x), L[i] et len(L) coûtent ; x in L, min(L), L.pop(0) et L = L + [x] coûtent ; la tranche L[i:j] est une copie et coûte . Sur un dictionnaire, k in d coûte en moyenne. Une boucle qui contient x in L est donc souvent quadratique, et une fonction récursive qui s'appelle sur L[1:len(L)] coûte au lieu de .
Quelle est la complexité de la dichotomie ?
Sur une liste triée de longueur , la recherche dichotomique fait au plus tours, soit 10 tours pour 1000 éléments et 20 pour un million : dans le pire cas, dans le meilleur cas (élément trouvé au premier milieu). Écrite avec des tranches au lieu d'indices, elle recopie la moitié de la liste à chaque appel et devient .
Comment y accéder
Ce chapitre fait partie des chapitres offerts : il s'ouvre en entier avec un compte d'essai gratuit. Créer un compte d'essai ou se connecter. Pour l'accès complet, les tarifs ou une question, écris-nous sur WhatsApp.