Vue d'ensemble
Deux programmes peuvent donner le même résultat et pourtant l'un s'exécuter en
une fraction de seconde, l'autre mettre des heures sur une grande entrée. Pour comparer des
algorithmes sans dépendre de la machine, on ne mesure pas un temps en secondes : on
compte le nombre d'opérations élémentaires en fonction de la taille n
de l'entrée. La notation grand-O résume ce décompte en un ordre de grandeur
(, , …). Cette fiche te montre comment lire la complexité d'un code
à l'œil, en décryptant chaque exemple ligne par ligne.
Prérequis
- Écrire et lire une boucle
foret une bouclewhile - Connaître la recherche par dichotomie dans un tableau trié
- Savoir ce qu'est le logarithme en base 2 : est l'exposant tel que
« Comment je sais si c'est du ou du rien qu'en regardant ? » C'est un réflexe qui se travaille, et il rapporte des points à chaque écrit. Nos mentors alumni X · Centrale · Mines t'entraînent à lire la complexité d'un code d'un coup d'œil, sur tes propres sujets, jusqu'à ce que ça devienne automatique.
Trouver un mentor →1. Compter les opérations, pas les secondes
Le temps réel d'un programme dépend de la machine, du langage, de ce que fait l'ordinateur à côté :
ce n'est pas une bonne mesure pour comparer des algorithmes. On raisonne donc sur le
nombre d'opérations élémentaires effectuées (une addition, une comparaison, un accès
t[i], une affectation…) en fonction de la taille de l'entrée, notée
n (le nombre d'éléments d'une liste, la valeur d'un entier, etc.).
La complexité temporelle d'un algorithme est une fonction qui donne le nombre d'opérations élémentaires effectuées sur une entrée de taille . On s'intéresse à son ordre de grandeur pour grand, à une constante multiplicative près. On écrit , et on lit « est un grand-O de », lorsqu'il existe une constante C > 0 et un rang tels que :
+7 ne changent pas la catégorie. Le grand-O gomme les constantes et les termes
négligeables pour ne garder que le terme dominant : est en
.
Prenons un premier exemple : la somme des éléments d'une liste.
def somme(t):
s = 0
for x in t:
s = s + x
return ss = 0Une affectation : 1 opération, faite une seule fois quelle que soit la taille de t.for x in t:La boucle parcourt les éléments. Le corps de la boucle est donc exécuté fois — c'est lui qui va donner l'ordre de grandeur.s = s + xUne addition + une affectation à chaque tour, donc environ opérations en tout. C'est le cœur du coût.return s1 opération, une seule fois.somme est en : on dit qu'elle a une
complexité linéaire. Doubler la taille de la liste double (à peu près) le travail.
2. Les familles d'ordres de grandeur
Presque toute la complexité au programme se range dans une poignée de familles. Le tableau ci-dessous est la chose à avoir en tête : il donne, pour chaque famille, le nombre d'opérations quand l'entrée grandit. C'est notre « table de trace » — elle trace non pas l'exécution d'un code, mais l'explosion du nombre d'opérations selon .
| Famille | Nom | |||
|---|---|---|---|---|
| constant | 1 | 1 | 1 | |
| logarithmique | ≈ 3 | ≈ 7 | ≈ 20 | |
| linéaire | 10 | 100 | 106 | |
| quasi-linéaire | ≈ 33 | ≈ 664 | ≈ 2 × 107 | |
| quadratique | 100 | 10 000 | 1012 | |
| exponentiel | 1024 | ≈ 1030 | inimaginable |
3. Meilleur cas, pire cas, cas moyen
Pour une même taille , le nombre d'opérations peut dépendre du contenu de l'entrée. On distingue alors trois complexités. L'exemple canonique est la recherche séquentielle : parcourir une liste jusqu'à trouver une valeur.
def recherche_seq(t, x):
for i in range(len(t)):
if t[i] == x:
return i # trouvé : on renvoie la position
return -1 # jamais trouvé : x est absentfor i in range(len(t)):On balaie les indices de à . Le nombre de tours réellement effectués dépend de où se trouve x.if t[i] == x:
return iUne comparaison par tour. Dès qu'on tombe sur x, on renvoie son indice et on s'arrête net : on ne fait pas les tours suivants.return -1Si la boucle va jusqu'au bout sans rien trouver, x est absent : on a fait comparaisons pour rien.- Meilleur cas —
xest en première position (t[0]) : 1 comparaison, donc . - Pire cas —
xest en dernière position, ou absent : comparaisons, donc . - Cas moyen — si
xest présent à une position au hasard : en moyenne comparaisons, soit encore .
4. Méthode : lire la complexité d'un code
- Une boucle qui parcourt éléments (et corps en ) → .
- Deux boucles imbriquées, chacune sur → le corps s'exécute fois → .
- Une grandeur divisée par 2 à chaque tour (comme la dichotomie) → .
- Des blocs en séquence (l'un après l'autre) → on additionne les coûts et on garde le plus grand : .
- On oublie les constantes et les termes non dominants.
Deux boucles imbriquées → quadratique
Comptons les paires d'éléments d'une liste dont la somme vaut une valeur cible s.
def compte_paires(t, s):
n = len(t)
c = 0
for i in range(n):
for j in range(i + 1, n):
if t[i] + t[j] == s:
c = c + 1
return cfor i in range(n):Boucle externe : tours, un pour chaque premier élément de la paire.for j in range(i + 1, n):Boucle interne : pour chaque i, on essaie tous les j > i. On teste ainsi chaque paire une seule fois. Le nombre total de tours internes est .if t[i] + t[j] == s:
c = c + 1Une addition + une comparaison par paire. C'est le corps en , répété fois.Division par 2 → logarithmique
La dichotomie (vue en prérequis) illustre le cas : à chaque tour, la zone de recherche est divisée par 2.
def dichotomie(t, x):
g, d = 0, len(t) - 1
while g <= d:
m = (g + d) // 2
if t[m] == x:
return m
elif t[m] < x:
g = m + 1 # on jette la moitié gauche
else:
d = m - 1 # on jette la moitié droite
return -1while g <= d:On répète tant que la zone est non vide. La question de complexité, c'est : combien de fois cette boucle tourne-t-elle ?m = (g + d) // 2On vise le milieu de la zone courante — coût .g = m + 1Cas « trop petit » : on garde la moitié droite. La taille de la zone est (au moins) divisée par 2.d = m - 1Cas « trop grand » : on garde la moitié gauche. Là encore, la zone est divisée par 2.Partant de éléments, la taille de la zone suit jusqu'à 1. Le nombre de divisions par 2 pour passer de à 1 est : la boucle fait de l'ordre de tours, d'où la complexité . C'est ce que précise le théorème suivant.
Sur un tableau trié de éléments, la recherche par dichotomie effectue au plus itérations de la boucle. Sa complexité dans le pire cas est donc .
Démonstration
Notons la taille de la zone de recherche , c'est-à-dire . Initialement . À chaque itération où l'on ne renvoie pas, on remplace par ou par avec ; dans les deux cas la nouvelle zone contient au plus éléments : la taille est (au moins) divisée par 2.
Après itérations, la zone contient donc au plus éléments. La boucle continue tant que la zone est non vide, soit . Or dès que \dfrac{n}{2^k} < 1, c'est-à-dire 2^k > n, la zone est vide et la boucle s'arrête. Cette condition est atteinte pour , car alors 2^k > n. Le nombre d'itérations est donc au plus .
Chaque itération coûte , d'où un coût total en .
Bloqué sur « pourquoi diviser par 2 donne un log » ? Ce genre de déclic, une fois posé clairement, ne te lâche plus. Nos mentors alumni X · Centrale · Mines reprennent la preuve avec toi et te font l'appliquer à d'autres algorithmes « diviser pour régner » de ton programme.
Trouver un mentor →5. Exercices d'application
Fais-les sur papier avant d'ouvrir le corrigé, puis passe au quiz en bas de fiche.
Donne la complexité (dans le pire cas) de cette fonction, en justifiant.
def maxi(t):
m = t[0]
for x in t:
if x > m:
m = x
return mVoir la correction détaillée
for parcourt les éléments de t.Quelle est la complexité de f en fonction de n = len(t) ? Justifie avec la règle des blocs en séquence.
def f(t):
n = len(t)
s = 0
for x in t: # bloc A
s = s + x
c = 0
for i in range(n): # bloc B
for j in range(n):
c = c + 1
return s + cVoir la correction détaillée
Combien de fois la boucle tourne-t-elle pour n = 1000 ? Quelle est la complexité en fonction de n ?
def compte(n):
r = 0
while n > 1:
r = r + 1
n = n // 2
return rVoir la correction détaillée
n est divisé par 2 (division entière) : .n n'atteigne 1 (la fonction renvoie donc 9). C'est cohérent avec .Récap final — Ce qu'il faut absolument retenir
À la veille d'une khôlle ou d'un DS, parcours cette checklist : tu dois pouvoir répondre « oui, sans hésiter » à chaque question.
- Sais-tu expliquer pourquoi on compte des opérations et non des secondes ?
- Sais-tu donner la définition semi-formelle de (constante et rang ) ?
- Sais-tu classer les familles , , , , , et dire laquelle explose le plus vite ?
- Sais-tu distinguer meilleur cas, pire cas et cas moyen sur la recherche séquentielle ?
- Sais-tu qu'une complexité annoncée sans précision est celle du pire cas ?
- Sais-tu reconnaître à l'œil qu'une boucle simple est , deux boucles imbriquées , une division par 2 ?
- Sais-tu appliquer la règle « blocs en séquence → on garde le max » ?
- Sais-tu démontrer que la dichotomie fait au plus itérations ?
À savoir refaire
- Coût logarithmique de la dichotomie — la zone est divisée par 2 à chaque tour, donc au plus itérations.