📈

Complexité algorithmique

Notation grand O, analyse et comparaison des algorithmes

O(n log n) Analyse Comparaison

🤔 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.
💡 Pourquoi pas mesurer le temps directement ? Parce que le temps d'exécution dépend du processeur, du langage, des optimisations du compilateur… La complexité est une mesure universelle indépendante de la machine.

📐 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

  1. On garde seulement le terme dominant : O(n² + n) → O(n²)
  2. On ignore les constantes multiplicatives : O(3n) → O(n)
  3. On considère le pire cas (sauf mention contraire)
  4. Additions de boucles indépendantes : O(n) + O(n) = O(n), pas O(2n)
  5. 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

⭐ Excellent

Indé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 bon

On 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

👍 Bon

On 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

👍 Acceptable

Lé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 possible

Deux 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

💀 Impraticable

Explose 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 = 10n = 100n = 1 000n = 10 000n = 1 000 000
O(1)11111
O(log n)37101420
O(n)101001 00010 0001 000 000
O(n log n)336649 966132 8772×10⁷
O(n²)10010 0001 000 00010⁸10¹²
O(2ⁿ)1 02410³⁰10³⁰¹∞ (pratique)∞

🔬 Comment calculer la complexité ?

Voici la méthode systématique pour analyser n'importe quel algorithme :

🧮 Méthode en 5 étapes

  1. Identifier n : quelle est la taille de l'entrée ? (longueur liste, valeur entier, nb nœuds graphe…)
  2. Repérer les boucles : chaque boucle qui parcourt n éléments ajoute un facteur n
  3. Analyser les boucles imbriquées : les multiplier entre elles
  4. Analyser la récursion : écrire la relation de récurrence T(n) = … + T(…)
  5. 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ⁿ) 💀
📐 Théorème maître (simplifié) :
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ᶜ)
Tri fusion : a=2, b=2, c=1 → log₂(2)=1=c → O(n log n) ✅

⚡ Règles rapides à mémoriser

Motif de codeComplexité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 //= 2O(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 lstO(n)Recherche séquentielle
x in set / x in dictO(1)Table de hachage
lst.append(x)O(1) amortiAjout 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

⛔ Confondre O(log n) et O(n log n)
log(1000000) ≈ 20 opérations. 1000000 × 20 = 20 millions opérations. Ce n'est pas du tout la même chose !
⛔ Croire que O(n) + O(n) = O(2n)
Non. O(n) + O(n) = O(n). Les constantes multiplicatives disparaissent. On ne garde que la classe de croissance.
⚠️ Boucle interne dépendant de i → toujours O(n²)
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).
⚠️ Complexité de in selon la structure
x in liste → O(n). x in set ou x in dict → O(1). Préférer set/dict pour les recherches fréquentes !
💡 Meilleur cas ≠ Pire cas
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.