Chapitre 1
01 — Complexité Algorithmique
> **Objectif** : Maîtriser la notation asymptotique (Big-O, Omega, Theta), calculer la complexité temps et espace, et résoudre les récurrences. ---
01 — Complexité Algorithmique : Cours complet
Niveau : Université / Ingénierie — Durée de lecture : ~50 min
Table des matières
- Introduction : pourquoi analyser ?
- Définitions : Big-O, Omega, Theta
- Règles de calcul
- Complexités classiques
- Complexité spatiale
- Analyse amortie
- Résolution de récurrences
- Analyse empirique vs théorique
- Diagrammes de comparaison
- Résumé
1. Introduction : pourquoi analyser ?
La complexité d'un algorithme mesure sa croissance en fonction de la taille n de l'entrée. Deux raisons fondamentales :
- Prédictibilité : estimer le temps d'exécution sans exécuter.
- Comparaison : choisir entre deux algorithmes indépendamment de la machine.
Point crucial : on ne mesure pas des secondes (dépend de la machine) mais des opérations élémentaires en fonction de
n. La machine n'introduit qu'une constante multiplicative.
1.1 Exemple fondateur
def somme_liste(liste):
total = 0
for x in liste:
total += x
return total
- Opérations : 1 initialisation +
nitérations (1 addition + 1 incrément) + 1 retour. - Total :
1 + 3n + 1 = 3n + 2opérations. - Pour
n = 10^6: 3 × 10^6 opérations. Pourn = 10^9: 3 × 10^9. - Le facteur
3est une constante. Ce qui compte : le temps est proportionnel à n → O(n).
2. Définitions : Big-O, Omega, Theta
2.1 Notation Big-O — borne supérieure
Définition :
f(n) = O(g(n))s'il existe des constantesc > 0etn₀ ≥ 1telles que pour toutn ≥ n₀:0 ≤ f(n) ≤ c · g(n).
Intuition : à partir d'un seuil n₀, f est toujours sous c·g. C'est une borne supérieure asymptotique.
Diagramme en cours de génération...
⚠️ Big-O est une borne supérieure, pas une égalité. Un algorithme O(n²) peut très bien être aussi O(n) — il faut chercher la borne la plus serrée (tight bound) : pour 3n+2, le plus serré est O(n).
2.2 Notation Big-Omega — borne inférieure
Définition :
f(n) = Ω(g(n))s'il existec > 0etn₀ ≥ 1tels que pour toutn ≥ n₀:f(n) ≥ c · g(n) ≥ 0.
Intuition : à partir de n₀, f est toujours au-dessus de c·g. Exemple : 3n + 2 = Ω(n) (et même = Ω(1), mais le plus serré est Ω(n)).
2.3 Notation Big-Theta — borne serrée
Définition :
f(n) = Θ(g(n))si à la foisf(n) = O(g(n))etf(n) = Ω(g(n)).
Intuition : f et g croissent à la même vitesse : c₁·g(n) ≤ f(n) ≤ c₂·g(n) pour n ≥ n₀.
3n + 2 = Θ(n) ✓ (entre 3n et 5n pour n ≥ 1)
3n + 2 = Θ(n²) ✗ (pas de c₁ tel que c₁·n² ≤ 3n+2)
2.4 Tableau récapitulatif
| Notation | Rôle | Traduction | Définition formelle |
|---|---|---|---|
f = O(g) | Borne supérieure | « au plus » | f ≤ c·g |
f = Ω(g) | Borne inférieure | « au moins » | f ≥ c·g |
f = Θ(g) | Borne serrée | « exactement » | c₁·g ≤ f ≤ c₂·g |
f = o(g) | Négligeable | « strictement moins » | f/g → 0 |
f = ω(g) | Dominante | « strictement plus » | f/g → ∞ |
2.5 Limites et rapports
Pour comparer f et g, on calcule souvent : lim f(n)/g(n).
| Limite | Conclusion |
|---|---|
| 0 | f = o(g), donc f = O(g) mais pas Θ(g) |
| c > 0 fini | f = Θ(g) |
| +∞ | f = ω(g), donc f = Ω(g) mais pas Θ(g) |
Exemple : f = 7n² + 3n, g = n². f/g → 7 → f = Θ(n²).
3. Règles de calcul
3.1 Règle de la somme
Si deux travaux s'exécutent séquentiellement (l'un après l'autre) :
T(n) = T₁(n) + T₂(n) → T(n) = O(max(g₁, g₂))
Intuition : le plus coûteux domine.
for x in liste: # O(n)
...
for i in liste: # O(n²) → domine
for j in liste:
...
# Total : O(n) + O(n²) = O(n²)
3.2 Règle du produit
Si deux travaux sont imbriqués (boucle dans boucle) :
T(n) = T₁(n) × T₂(n) → T(n) = O(g₁ × g₂)
for i in range(n): # n fois
for j in range(m): # m fois
pass # O(1)
# Total : O(n × m). Si m = n : O(n²)
3.3 Règle de composition
f(g(n)) : si g(n) ≤ O(h(n)), alors f(g(n)) ≤ O(f(h(n))).
Exemple : f(x) = 2^x, g(n) = 3n → 2^(3n) = (2³)^n = 8^n = O(8^n).
3.4 Règle de domination (hiérarchie)
O(1) < O(log n) < O(√n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2^n) < O(n!)
Une fonction est dominée par une fonction de rang supérieur : O(n) + O(n²) = O(n²).
3.5 Cas particuliers
- Logarithmes : toutes les bases sont équivalentes :
log₂(n) = Θ(log₁₀(n))carlog₂ n = log₁₀ n / log₁₀ 2(constante). - Exposants :
2^net3^nne sont pas équivalents :3^n = ω(2^n). - Polynômes vs exponentielles : pour tout k fixé,
n^k = o(2^n). - Log vs polynôme :
(log n)^k = o(n^ε)pour tout k, ε > 0.
3.6 Règles de simplification
O(1) constant — pas de boucle
O(log n) — division par 2 à chaque étape (recherche binaire)
O(n) — une boucle simple
O(n log n) — boucle + division (tri par fusion)
O(n²) — boucle imbriquée (tri à bulles)
O(2^n) — exploration de tous les sous-ensembles
O(n!) — exploration de toutes les permutations
4. Complexités classiques
4.1 O(1) — Constant
def acces(liste, i):
return liste[i] # accès direct, indépendant de n
Applications : accès indexé, opérations arithmétiques, opérations sur hash table.
4.2 O(log n) — Logarithmique
def recherche_binaire(liste, cible):
gauche, droite = 0, len(liste) - 1
while gauche <= droite:
milieu = (gauche + droite) // 2
if liste[milieu] == cible:
return milieu
if liste[milieu] < cible:
gauche = milieu + 1
else:
droite = milieu - 1
return -1
À chaque étape, on divise l'espace par 2 : après k étapes, il reste n/2^k éléments. Termine quand n/2^k = 1 → k = log₂ n.
Nombre de comparaisons : au plus ⌊log₂(n)⌋ + 1. Pour n = 10^9 → seulement 30 comparaisons.
4.3 O(n) — Linéaire
Une boucle parcourant les n éléments. Exemples : recherche linéaire, somme, maximum.
4.4 O(n log n) — Quasi-linéaire
Divide-and-conquer où chaque niveau coûte O(n) sur log₂ n niveaux. Exemples : merge sort, heap sort, quick sort (moyenne).
4.5 O(n²) — Quadratique
Boucles imbriquées. Exemples : tri à bulles, tri par insertion, tri par sélection, produit de matrices naïf.
4.6 O(2^n) — Exponentielle
Exploration de tous les sous-ensembles. Exemple : Fibonacci naïf, sous-ensemble-sum, SAT brut.
4.7 O(n!) — Factorielle
Exploration de toutes les permutations. Exemple : voyageur de commerce brut (n = 20 → 2,4 × 10^18 opérations → impossible).
4.8 Tableau des complexités (n = 10^6 opérations/s)
| Complexité | n = 10 | n = 100 | n = 10³ | n = 10⁶ |
|---|---|---|---|---|
| O(1) | 1 µs | 1 µs | 1 µs | 1 µs |
| O(log n) | 3 µs | 7 µs | 10 µs | 20 µs |
| O(n) | 10 µs | 100 µs | 1 ms | 1 s |
| O(n log n) | 33 µs | 700 µs | 10 ms | 20 s |
| O(n²) | 100 µs | 10 ms | 1 s | 11,6 jours |
| O(2^n) | 1 ms | 4 × 10^16 ans | — | — |
| O(n!) | 3,6 s | — | — | — |
5. Complexité spatiale
La complexité spatiale mesure la mémoire supplémentaire (hors entrée) utilisée.
5.1 Exemples
def somme(liste): # espace O(1) : une seule variable
total = 0
for x in liste:
total += x
return total
def double(liste): # espace O(n) : nouvelle liste
return [x * 2 for x in liste]
def fibonacci_memo(n): # espace O(n) : dictionnaire
memo = {}
...
5.2 Récursion : espace de pile
Chaque appel récursif empile un cadre (frame). La profondeur de récursion détermine l'espace :
def factorielle(n):
return 1 if n <= 1 else n * factorielle(n - 1) # O(n) espace (n frames)
5.3 Trade-off classique
| Algorithme | Temps | Espace |
|---|---|---|
| Merge sort | O(n log n) | O(n) |
| Heap sort | O(n log n) | O(1) |
| Quick sort | O(n log n) moy | O(log n) |
| Trie | O(m) recherche | O(nodes) |
6. Analyse amortie
L'analyse amortie étudie le coût moyen d'une opération sur une séquence d'opérations, même si certaines opérations sont très coûteuses ponctuellement.
6.1 Tableau dynamique (dynamic array)
En Python, une liste s'agrandit dynamiquement. Stratégie : doubler la capacité quand pleine.
- Coût d'insertion « normale » : O(1).
- Coût quand la table est pleine (doubling) : O(n) — on copie les n éléments.
Séquence de n insertions : copies totales = 1 + 2 + 4 + … + n/2 + n = 2n - 1 = O(n).
Coût amorti : O(n)/n = O(1) par insertion. Les insertions rares à O(n) sont « diluées ».
Diagramme en cours de génération...
6.2 Méthodes d'analyse
| Méthode | Principe |
|---|---|
| Agrégation | Coût total sur n opérations / n |
| Comptable | Chaque opération « paie » d'avance ses futures copies |
| Potentiel | Fonction Φ(état) ; coût amorti = coût réel + ΔΦ |
6.3 Compteur binaire (incremental counter)
Incrémenter un compteur binaire de n bits : l'opération simple coûte O(1), mais l'opération sur un compteur de k bits où k bits basculent... En séquence de n incréments :
- Le bit 0 bascule n fois.
- Le bit 1 bascule n/2 fois.
- Le bit i bascule n/2^i fois.
Coût total = Σ n/2^i = 2n → O(n) pour n incréments, soit O(1) amorti.
6.4 Exemple en code
class TableauDynamique:
def __init__(self):
self.capacite = 1
self.taille = 0
self.data = [None] * self.capacite
def ajouter(self, x):
if self.taille == self.capacite:
self.capacite *= 2
nouveau = [None] * self.capacite
for i in range(self.taille): # O(n) ponctuel
nouveau[i] = self.data[i]
self.data = nouveau
self.data[self.taille] = x
self.taille += 1
7. Résolution de récurrences
7.1 Écrire une récurrence
Le temps T(n) d'un algorithme récursif s'écrit :
T(n) = a·T(n/b) + f(n)
a: nombre de sous-problèmes.n/b: taille de chaque sous-problème.f(n): coût de division + combinaison.
Exemple merge sort : T(n) = 2·T(n/2) + O(n) (2 moitiés + fusion linéaire).
7.2 Méthode de substitution
- Deviner la forme (souvent O(n^k) ou O(n^k log n)).
- Supposer qu'elle est vraie pour les petites tailles.
- Remplacer et vérifier par induction.
Exemple : T(n) = 2·T(n/2) + n. Devinons T(n) = O(n log n).
T(n) = 2·T(n/2) + n
≤ 2·(c·(n/2)·log(n/2)) + n
= c·n·log(n/2) + n
= c·n·log n - c·n + n
≤ c·n·log n (pour c ≥ 1)
Vérifié → T(n) = Θ(n log n).
7.3 Méthode de l'arbre de récurrence
On déplie la récurrence en arbre et on somme les coûts par niveau.
Diagramme en cours de génération...
Pour T(n) = 2T(n/2) + n :
- Niveau 0 : coût n
- Niveau 1 : 2 × (n/2) = n
- Niveau k : 2^k × (n/2^k) = n
- Nombre de niveaux : log₂ n (on divise jusqu'à 1)
- Total : n × log₂ n = Θ(n log n)
7.4 Master Theorem
Pour T(n) = a·T(n/b) + f(n) avec a ≥ 1, b > 1, on compare f(n) à n^(log_b a).
| Cas | Condition | Solution |
|---|---|---|
| 1 | f(n) = O(n^(log_b a − ε)) | T(n) = Θ(n^(log_b a)) |
| 2 | f(n) = Θ(n^(log_b a)) | T(n) = Θ(n^(log_b a) · log n) |
| 3 | f(n) = Ω(n^(log_b a + ε)) ET a·f(n/b) ≤ c·f(n) | T(n) = Θ(f(n)) |
Exemples :
| Récurrence | a | b | log_b a | f(n) | Cas | Résultat |
|---|---|---|---|---|---|---|
| T(n)=2T(n/2)+n | 2 | 2 | 1 | n | 2 | Θ(n log n) |
| T(n)=T(n/2)+1 | 1 | 2 | 0 | 1 | 2 | Θ(log n) |
| T(n)=2T(n/2)+n² | 2 | 2 | 1 | n² | 3 | Θ(n²) |
| T(n)=T(n/2)+n | 1 | 2 | 0 | n | 3 | Θ(n) |
| T(n)=7T(n/2)+n² | 7 | 2 | log₂7≈2,81 | n² | 1 | Θ(n^(log₂7)) ≈ Θ(n^2,81) |
7.5 Limites du Master theorem
Ne s'applique pas si f(n) ne suit pas une puissance de n comparée à n^(log_b a) (ex. : f(n) = n·log n avec log_b a = 1 — le « gap » polylog). Utiliser alors l'arbre ou la substitution.
8. Analyse empirique vs théorique
8.1 Comparaison
| Aspect | Analyse théorique | Analyse empirique |
|---|---|---|
| Précision | Indépendante de la machine | Dépend de la machine |
| Échelle | Toutes tailles | Tailles testées |
| Objectif | Comparer asymptotiquement | Vérifier/estimer |
| Coût | Papier/crayon | Benchmarks |
8.2 Protocole de benchmark
- Varier n : puissances de 2 ou ×10 (100, 1000, 10⁴, …).
- Répliquer : exécuter au moins 5 fois, prendre la médiane.
- Échauffer (warm-up) pour la JIT et les caches.
- Définir l'entrée : pire cas, meilleur cas, cas moyen.
- Vérifier la cohérence : le rapport T(2n)/T(n) doit approcher le rapport théorique.
8.3 Exemple Python
import time, random
def chronometrer(fonction, args, repetitions=5):
temps = []
for _ in range(repetitions):
debut = time.perf_counter()
fonction(*args)
temps.append(time.perf_counter() - debut)
return sorted(temps)[len(temps) // 2] # médiane
# Recherche linéaire : T(2n)/T(n) ≈ 2 (linéaire)
# Recherche binaire : T(2n)/T(n) ≈ 1 (logarithme)
8.4 Limites du théorique
- Les constantes cachées peuvent rendre un O(n²) plus rapide qu'un O(n log n) pour les petites n.
- La localité de cache (cache locality) : le merge sort est O(n log n) mais utilise plus de mémoire que le quick sort.
- Les entrées réelles ne sont pas le pire cas mathématique.
9. Diagrammes de comparaison
9.1 Croissance des fonctions
Diagramme en cours de génération...
9.2 Temps estimés (n = 10⁶, 10⁹ op/s)
| Complexité | Temps |
|---|---|
| O(log n) | ~20 µs |
| O(n) | ~1 ms |
| O(n log n) | ~20 ms |
| O(n²) | ~16 min |
| O(n³) | ~11 jours |
| O(2^n) | 10^301 000 ans |
10. Résumé
- Big-O = borne supérieure ; Ω = borne inférieure ; Θ = borne serrée.
- Règles : somme = max, produit = multiplication, composition, hiérarchie de domination.
- Hiérarchie : 1 < log n < √n < n < n log n < n² < n³ < 2^n < n!.
- Espace : mesurer la mémoire additionnelle ; récursion → O(profondeur).
- Amorti : tableau dynamique et compteur binaire : O(1) amorti.
- Récurrences : substitution, arbre, Master theorem (3 cas).
- Empirique vs théorique : complémentaires ; le théorique domine pour comparer.
Exercices d'auto-évaluation
- Montrez que
5n³ + 2n² + 7 = Θ(n³). - Calculez la complexité :
for i: for j < i: O(1). Réponse : Σ i = n(n-1)/2 = O(n²). - Résolvez T(n) = 4T(n/2) + n par Master theorem. (log_b a = 2, cas 1 → Θ(n²).)
- Un algorithme double son temps quand n est multiplié par 2. Quelle complexité ?
- Expliquez pourquoi le coût amorti d'un push de tableau dynamique est O(1).
- Donnez la complexité spatiale de la recherche binaire récursive.