Complexité algorithmique
Notation grand O, analyse et comparaison des algorithmes
🤔 Qu'est-ce que la complexité ?
Deux algorithmes peuvent résoudre le même problème, mais l'un peut être mille fois plus rapide que l'autre sur de grandes données. La complexité algorithmique est l'outil mathématique qui permet de comparer cette efficacité sans avoir à mesurer le temps réel (qui dépend du matériel, du langage, etc.).
On distingue deux types de complexité :
- Complexité temporelle : nombre d'opérations élémentaires en fonction de la taille de l'entrée n.
- Complexité spatiale : quantité de mémoire utilisée. Moins souvent demandée au bac, mais à connaître.
📐 La notation grand O
La notation O(f(n)) (lire "grand O de f de n") décrit le comportement asymptotique de l'algorithme : comment le nombre d'opérations grandit quand n devient très grand.
On dit que T(n) = O(f(n)) si, à partir d'un certain n₀,
T(n) ≤ c × f(n) pour une constante c > 0.
Autrement dit : f(n) est un majorant du coût, aux constantes près.
Exemples concrets :
T(n) = 3n + 7 → O(n) (le 3 et le 7 "disparaissent")
T(n) = 5n² + 2n + 1 → O(n²) (le terme dominant gagne)
T(n) = n·log(n) + n → O(n log n) (n·log n domine n pour grand n)
🔑 Règles de simplification grand O
- On garde seulement le terme dominant : O(n² + n) → O(n²)
- On ignore les constantes multiplicatives : O(3n) → O(n)
- On considère le pire cas (sauf mention contraire)
- Additions de boucles indépendantes : O(n) + O(n) = O(n), pas O(2n)
- Boucles imbriquées → multiplication : O(n) × O(n) = O(n²)
📊 Graphique comparatif
Cliquez sur les courbes pour les afficher ou masquer. Passez la souris sur le graphique pour lire les valeurs.
Axe X : taille de l'entrée n (0 → 30) · Axe Y : nombre d'opérations (échelle log)
📚 Catalogue des complexités
O(1) — Constante
⭐ ExcellentIndépendant de n. Le temps ne change pas quelle que soit la taille des données.
Exemples : accès à lst[i], push/pop pile, insertion dictionnaire.
x = lst[42] # toujours 1 opération
O(log n) — Logarithmique
✅ Très bonOn divise le problème par une constante à chaque étape. Croît très lentement.
Exemples : recherche dichotomique, opérations sur ABR équilibré.
# n=1 000 000 → ~20 étapes seulement
O(n) — Linéaire
👍 BonOn traite chaque élément une fois. Proportionnel à la taille des données.
Exemples : parcours liste, recherche séquentielle, calcul de somme.
for x in lst: ... # n opérations
O(n log n) — Quasi-linéaire
👍 AcceptableLégèrement plus que linéaire. C'est l'optimal théorique pour les tris par comparaison.
Exemples : tri fusion, tri rapide (moy.), tri par tas.
# n=1 000 000 → ~20 000 000 opérations
O(n²) — Quadratique
⚠️ À éviter si possibleDeux boucles imbriquées typiquement. Acceptable pour n ≤ 10 000 environ.
Exemples : tri bulles, tri insertion (pire cas), tri sélection.
for i ...: for j ...: ... # n² ops
O(2ⁿ) — Exponentielle
💀 ImpraticableExplose rapidement. Inutilisable au-delà de n ≈ 30–40 en pratique.
Exemples : énumération de tous les sous-ensembles, fibonacci naïf récursif.
# n=50 → 10^15 opérations… des années !
Tableau de comparaison numérique
| Complexité | n = 10 | n = 100 | n = 1 000 | n = 10 000 | n = 1 000 000 |
|---|---|---|---|---|---|
| O(1) | 1 | 1 | 1 | 1 | 1 |
| O(log n) | 3 | 7 | 10 | 14 | 20 |
| O(n) | 10 | 100 | 1 000 | 10 000 | 1 000 000 |
| O(n log n) | 33 | 664 | 9 966 | 132 877 | 2×10⁷ |
| O(n²) | 100 | 10 000 | 1 000 000 | 10⁸ | 10¹² |
| O(2ⁿ) | 1 024 | 10³⁰ | 10³⁰¹ | ∞ (pratique) | ∞ |
🔬 Comment calculer la complexité ?
Voici la méthode systématique pour analyser n'importe quel algorithme :
🧮 Méthode en 5 étapes
- Identifier n : quelle est la taille de l'entrée ? (longueur liste, valeur entier, nb nœuds graphe…)
- Repérer les boucles : chaque boucle qui parcourt n éléments ajoute un facteur n
- Analyser les boucles imbriquées : les multiplier entre elles
- Analyser la récursion : écrire la relation de récurrence T(n) = … + T(…)
- Garder le terme dominant et simplifier les constantes
Cas 1 — Boucles simples et imbriquées
# Une boucle → O(n)
for i in range(n): # n fois
traitement() # O(1) → total O(n)
# Deux boucles indépendantes → O(n) + O(n) = O(n)
for i in range(n): ... # O(n)
for j in range(n): ... # O(n)
# total : O(2n) = O(n)
# Deux boucles imbriquées → O(n²)
for i in range(n): # n fois
for j in range(n): # n fois
traitement() # O(1) → total O(n²)
# Boucle intérieure dépendant de i → toujours O(n²) !
for i in range(n): # n fois
for j in range(i): # 0+1+2+...+(n-1) = n(n-1)/2
traitement() # → O(n²) quand même
Cas 2 — Division par 2 à chaque étape
# Si on divise par 2 à chaque itération → O(log n)
n_copie = n
while n_copie > 1:
n_copie = n_copie // 2 # combien de fois ? log₂(n) fois
traitement()
# Exemple : pour n=1024, la boucle tourne 10 fois
# 1024 → 512 → 256 → 128 → 64 → 32 → 16 → 8 → 4 → 2 → 1
Cas 3 — Récursion : relations de récurrence
# Récursion linéaire : T(n) = T(n-1) + O(1)
def factorielle(n):
if n <= 1: return 1 # T(1) = O(1)
return n * factorielle(n-1) # T(n) = T(n-1) + O(1) → O(n)
# Diviser pour régner : T(n) = 2·T(n/2) + O(n)
def tri_fusion(lst):
if len(lst) <= 1: return lst # T(1) = O(1)
m = len(lst) // 2
g = tri_fusion(lst[:m]) # T(n/2)
d = tri_fusion(lst[m:]) # T(n/2)
return fusion(g, d) # O(n) pour fusionner
# T(n) = 2·T(n/2) + O(n) → O(n log n) (Théorème maître)
# Double récursion naïve : T(n) = 2·T(n-1) + O(1)
def fib_naif(n):
if n <= 1: return n
return fib_naif(n-1) + fib_naif(n-2) # → O(2ⁿ) 💀
Pour T(n) = a·T(n/b) + O(nᶜ) :
- Si log_b(a) > c → T(n) = O(n^log_b(a))
- Si log_b(a) = c → T(n) = O(nᶜ log n)
- Si log_b(a) < c → T(n) = O(nᶜ)
⚡ Règles rapides à mémoriser
| Motif de code | Complexité | Explication |
|---|---|---|
lst[i], dico[k] | O(1) | Accès direct par index/clé |
for x in lst: ... | O(n) | Une itération par élément |
while n > 1: n //= 2 | O(log n) | Division par constante |
for i: for j: ... | O(n²) | Deux boucles imbriquées |
sorted(lst) | O(n log n) | Timsort Python |
x in lst | O(n) | Recherche séquentielle |
x in set / x in dict | O(1) | Table de hachage |
lst.append(x) | O(1) amorti | Ajout en fin de liste |
lst.insert(0, x) | O(n) | Décale tous les éléments |
Récursion f(n-1) | O(n) | n appels récursifs |
Récursion f(n//2) | O(log n) | Profondeur log n |
Double récursion f(n-1)+f(n-2) | O(2ⁿ) | Arbre d'appels exponentiel |
🎯 Exercices interactifs
Quelle est la complexité de chaque fonction ?
🚨 Pièges fréquents
log(1000000) ≈ 20 opérations. 1000000 × 20 = 20 millions opérations. Ce n'est pas du tout la même chose !
Non. O(n) + O(n) = O(n). Les constantes multiplicatives disparaissent. On ne garde que la classe de croissance.
for i in range(n): for j in range(i): fait 0+1+…+(n-1) = n(n-1)/2 opérations = O(n²), pas O(n×n/2).
in selon la structurex in liste → O(n). x in set ou x in dict → O(1). Préférer set/dict pour les recherches fréquentes !
La recherche dichotomique peut trouver en 1 opération (meilleur cas O(1)), mais en O(log n) au pire. On raisonne toujours sur le pire cas sauf indication contraire.