Vous savez compter, vous savez simplifier, vous connaissez
les familles. Il reste à acquérir le réflexe : regarder un code et annoncer sa
complexité sans crayon.
Deux règles, et c'est tout
Règle d'addition. Deux blocs qui se suivent : on garde le
plus coûteux.
trier(tab) # O(n log n)
for x in tab: # O(n)
print(x)
# total : O(n log n) : le tri écrase le parcours
Règle de multiplication. Un bloc à l'intérieur d'une boucle :
on multiplie.
for i in range(n): # n fois
dichotomie(tab, i) # O(log n) chacune
# total : O(n log n)
Reconnaître les boucles
Le nombre de tours ne se lit pas toujours sur la première ligne. Trois
schémas à connaître.
# incrément de 1 : n tours -> O(n)
i = 0
while i < n:
i += 1
# incrément de 100 : n/100 tours -> O(n), car 1/100 est une constante
i = 0
while i < n:
i += 100
# multiplication par 2 : log₂ n tours -> O(log n)
i = 1
while i < n:
i *= 2
La question à se poser : la variable de boucle
s'ajoute-t-elle ou se multiplie-t-elle ?
Ajouter donne du linéaire, multiplier donne du logarithmique.
Les boucles imbriquées dépendantes
Quand la boucle intérieure dépend de l'extérieure, il faut sommer.
for i in range(n):
for j in range(i): # i tours, pas n
...
Total : 0 + 1 + 2 + … + (n−1) = n(n−1)/2, soit O(n²). La
moitié du travail de la version carrée, mais la même famille.
Voici un cas plus retors, qu'on croit souvent quadratique :
i = 1
while i < n:
for j in range(n): # n tours
...
i *= 2 # log n tours de la boucle externe
La boucle externe multiplie : log n tours. L'interne en fait n. Total :
O(n log n).
Le piège des bibliothèques
Un appel de fonction ne coûte pas une opération : il coûte ce que la fonction
fait. Voici le tableau qui sauve le plus de programmes.
Opération Python
Complexité
Pourquoi
len(liste)
O(1)
la taille est stockée
liste[i]
O(1)
accès direct
liste.append(x)
O(1) amorti
voir chapitre 8
liste.insert(0, x)
O(n)
tout décaler
liste.pop(0)
O(n)
tout décaler
x in liste
O(n)
parcours
x in ensemble
O(1)
hachage
x in dico
O(1)
hachage
liste.sort()
O(n log n)
Timsort
min(), max(), sum()
O(n)
parcours
chaine + chaine
O(n)
recopie complète
Le classique qui tue. Ce code paraît linéaire :
uniques = []
for x in donnees: # n tours
if x not in uniques: # O(n) a chaque tour !
uniques.append(x)
Il est en fait O(n²), car le in sur une liste
reparcourt tout. Avec un ensemble à la place, on retombe en O(n) :
vus = set()
uniques = []
for x in donnees:
if x not in vus: # O(1)
vus.add(x)
uniques.append(x)
Sur 100 000 valeurs, la première version met plusieurs minutes, la seconde une
fraction de seconde. Une ligne de différence.
La concaténation de chaînes
Même histoire, autre déguisement.
# O(n²) : chaque + recopie toute la chaîne construite jusque-là
texte = ""
for mot in mots:
texte += mot + " "
# O(n) : on accumule, on assemble une seule fois
texte = " ".join(mots)
En Python, les chaînes sont immuables : texte += mot ne modifie
pas texte, il en fabrique une copie neuve. Au tour numéro k, cette
copie fait k caractères. Total : 1 + 2 + … + n, encore notre somme en n²/2.
Les fonctions qu'on écrit soi-même
Pour analyser une fonction qui en appelle d'autres, on remplace chaque appel
par sa complexité, puis on applique les deux règles.
Addition de quatre blocs : O(n) + O(n log n) + O(n) + O(1). On garde le plus
gros : O(n log n).
La marche à suivre
Qu'est-ce que n ? Le nombre d'éléments ? La longueur d'une
chaîne ? La valeur d'un entier ? Toute analyse commence là.
Repérer les boucles, et pour chacune compter les tours.
Vérifier chaque appel : un in, un +
sur des chaînes, un insert(0, …) cachent des parcours.
Multiplier l'imbriqué, garder le maximum de
ce qui se suit.
À retenir
Blocs successifs : on garde le pire. Blocs imbriqués : on multiplie.
Variable qui s'ajoute → linéaire. Variable qui se multiplie →
logarithmique.
Les coûts cachés des bibliothèques transforment un linéaire en quadratique
sans qu'une ligne ne l'annonce.
in sur une liste : O(n). Sur un ensemble : O(1). Retenez cette
seule ligne et vous éviterez la moitié des programmes lents.
Exercices d'application
Exercice 1
Quelle est la complexité de ce code ? for i in range(n): j = 1 while j < n: j *= 3
Voir le corrigeMasquer le corrige
La boucle externe fait n tours. L'interne multiplie j par 3 : elle s'arrête après log₃ n tours.
T(n) = n × log₃ n → O(n log n)
La base 3 disparaît : passer de log₃ à log₂ revient à multiplier par une constante.
Le piège était de voir deux boucles imbriquées et de répondre n². Regardez toujours comment la variable progresse.
Exercice 2
Ce code est censé compter les valeurs communes à deux listes de n éléments. Quelle est sa complexité, et comment faire mieux ? c = 0 for x in a: if x in b: c += 1
Voir le corrigeMasquer le corrige
La boucle fait n tours, et chaque x in b parcourt les n éléments de b.
T(n) = n × n → O(n²)
Il suffit de convertir b en ensemble, une seule fois :
vus = set(b) # O(n), une fois
c = 0
for x in a: # n tours
if x in vus: # O(1) maintenant
c += 1
# total : O(n)
Sur deux listes de 50 000 éléments, on passe de 2,5 milliards de comparaisons à 100 000. Le code ne change que d'une ligne.
Exercice 3
Pourquoi ces deux fonctions, qui renvoient le même résultat, n'ont-elles pas la même complexité ? # A def inverser(s): r = "" for c in s: r = c + r return r
# B def inverser(s): return s[::-1]
Voir le corrigeMasquer le corrige
Version A : O(n²). À chaque tour, c + r fabrique une chaîne neuve en recopiant tout r. Au tour k, cela coûte k caractères. Total : 1 + 2 + … + n = n(n+1)/2.
Version B : O(n). Le découpage inversé alloue la chaîne finale une fois et la remplit à l'envers. Une seule passe.
La leçon : en Python, toute accumulation dans une chaîne par + à l'intérieur d'une boucle est quadratique. Accumulez dans une liste, puis "".join(...).
Cherchez avant d'ouvrir le corrige : c'est en butant sur les difficultes que l'on progresse.
Questions / Réponses
Aucune question. Soyez le premier à poser une question.