La complexité algorithmique
Quantifier la performance d'un algorithme. Notations O, Ω, Θ. Analyse des algorithmes itératifs, récursifs, et "diviser pour régner". Cas Fibonacci, tri fusion, théorème Master.
📄 Ressources du cours
🎥 Vidéos recommandées
-
🇫🇷
Algorithmique (14/14) - Complexité des algorithmes
Mohamed Chiny · 06:27 · 202k vues — Le calcul de la complexité d'un algorithme (complexité asymptotique) consiste à évaluer la quantité des ressources utilisées par l'algorithme en question (ou...
-
🇫🇷
Méthode de calcul de la complexité d'un algorithme | Rachid Guerraoui
Wandida, EPFL · 11:05 · 192k vues — Cette leçon donne une méthode pratique pour déterminer la complexité d'un algorithme. Elle consiste à voir comment l'algorithme évolue en augmentant la taill...
-
🇬🇧
Introduction to Big O Notation and Time Complexity (Data Structures & Algorithms #7)
CS Dojo · 36:22 · 960k vues — Big O notation and time complexity, explained. Check out Brilliant.org (https://brilliant.org/CSDojo/), a website for learning math and computer science conc...
1. Généralités sur la complexité
Une mesure de complexité estime le temps de calcul ou l'espace mémoire utilisé par un algorithme.
Le calcul de complexité se fait indépendamment de :
- la machine sur laquelle s'exécute l'algorithme
- le langage de programmation utilisé
n.
Notations
A: un algorithmeD(n): l'ensemble des données de taillenP(d): probabilité de la donnéedCoût_A(d): coût de l'algorithme pour la donnéed
2. Calcul — algorithmes non récursifs
Sans structures de contrôle
Dénombrer simplement les opérations successives :
def celsius_fahrenheit(C):
F = C * 1.8 + 32 # 2 op arithmetiques + 1 affectation
return F # T(n) = 3, complexite constante
Structures conditionnelles
La complexité dépend des données. Trois formes de complexité :
| Cas | Définition | Formule |
|---|---|---|
| Meilleur | situation la plus favorable | T(n) = mind∈D(n) {Coût_A(d)} |
| Pire | situation la plus défavorable | T(n) = maxd∈D(n) {Coût_A(d)} |
| Moyenne | selon une loi de proba p | T(n) = Σd∈D(n) {p(d) · Coût_A(d)} |
On calcule le plus souvent la complexité dans le pire des cas — c'est elle qui est pertinente pour garantir une borne. Toujours envisager le pire.
Exemple — structure conditionnelle
def f(x, y):
if x > y:
h = x * x # 1 op arith + 1 affectation
else:
h = y * y + 1 # 2 op arith + 1 affectation
return h
# Comparaison : 1
# Pire des cas (else) : 1 + 3 = 4
# Meilleur cas (if) : 1 + 2 = 3
# Moyen (proba 0.5) : 1 + 0.5×2 + 0.5×3 = 3.5
Structures itératives
Temps total = somme du coût du test + corps de la boucle + gestion du compteur, multiplié par le nombre d'itérations.
def somme_1(n):
S = 0
for i in range(n):
S = S + i # n iterations -> O(n)
return S
def somme_2(n):
S = n * (n + 1) // 2 # formule directe -> O(1)
return S
Deux fonctions équivalentes en résultat, mais somme_1 est linéaire et somme_2 est constante. Le choix d'algorithme prime sur l'optimisation locale.
3. Comportement asymptotique — O, Ω, Θ
Trois notations pour comparer le comportement à l'infini de deux fonctions définies sur ℕ à valeurs dans ℝ⁺.
O(g(n)) = {f : ℕ → ℝ⁺ | ∃ k > 0 et n₀ ≥ 0 tels que ∀ n ≥ n₀, 0 ≤ f(n) ≤ k · g(n)}
Si f(n) ∈ O(g(n)), on dit que g(n) est une borne supérieure asymptotique de f(n) — g domine f.
Ω(g(n)) = {f : ℕ → ℝ⁺ | ∃ k > 0 et n₀ ≥ 0 tels que ∀ n ≥ n₀, 0 ≤ k · g(n) ≤ f(n)}
Θ(g(n)) = {f : ℕ → ℝ⁺ | ∃ k₁ > 0, k₂ > 0, n₀ ≥ 0 tels que ∀ n ≥ n₀, 0 ≤ k₁·g(n) ≤ f(n) ≤ k₂·g(n)}
Encadre f à un facteur constant près.
f(n) ∈ Θ(g(n)) ⇔ f(n) ∈ O(g(n)) ET f(n) ∈ Ω(g(n)).Aussi :
f(n) ∈ O(g(n)) ⇒ g(n) ∈ Ω(f(n)) (la borne supérieure de l'un est la borne inférieure de l'autre).
Classes de complexité usuelles
| O(·) | Classe | Exemple typique | Pour n=100 et 10⁻⁶s/op |
|---|---|---|---|
1 | constante | accès tableau, hash | ~1 µs |
log(n) | logarithmique | recherche dichotomique | ~7 µs |
n | linéaire | parcours simple | ~100 µs |
n·log(n) | quasi-linéaire | tris efficaces (fusion, rapide) | ~660 µs |
n² | quadratique | tri à bulles, sélection | ~10 ms |
n³ | cubique | multiplication de matrices naïve | ~1 s |
2ⁿ | exponentielle | Hanoï, Fibonacci naïf | ~10²² années |
n! | factorielle | énumération de permutations | incalculable |
À partir de la complexité quadratique, on quitte le « scalable ». Au-delà du cubique, on est dans l'algorithmique de recherche, pas en pratique.
4. Algorithmes récursifs simples
Un algorithme récursif décompose le problème en sous-problèmes de même nature. Sa complexité est une suite définie par récurrence.
Cas 1 — Factorielle
def factorielleRecursive(n):
if n == 0:
return 1
else:
return n * factorielleRecursive(n - 1)
- Si
n = 0: 1 comparaison ⇒T(0) = 1 - Si
n > 0: 1 comparaison + 1 multiplication + 1 soustraction + l'appel récursif ⇒T(n) = T(n-1) + 3
T(n) = 3n + 1 ⇒ T(n) = Θ(n), linéaire.
Cas 2 — Tours de Hanoï
def hanoi(de, vers, via, n):
if n == 1:
print(de, "vers", vers)
else:
hanoi(de, via, vers, n - 1)
print(de, "vers", vers)
hanoi(via, vers, de, n - 1)
T(1) = 1T(n) = 2 · T(n-1) + 1
T(n) = Σk=0n-1 2k = 2ⁿ - 1 ⇒ T(n) = Θ(2ⁿ), exponentiel.
5. Suites récurrentes linéaires
un est récurrente linéaire d'ordre 2 s'il existe a et b tels que pour tout n :
un+2 = a · un+1 + b · un
Équation caractéristique
On résout x² − a·x − b = 0 :
| Cas | Solution | Forme générale de un |
|---|---|---|
2 racines distinctes x₁, x₂ | réelles | un = λ·x₁ⁿ + μ·x₂ⁿ |
1 racine double x | réelle | un = λ·xⁿ + μ·n·xⁿ |
| Pas de solution | — | Ne se présente pas en calcul de complexité |
Cas Fibonacci
fib(n) = if n == 0 : 0
else if n == 1 : 1
else : fib(n-1) + fib(n-2)
Équation récurrente (en ignorant les constantes) :
T(n) = T(n-1) + T(n-2)
Avec l'équation caractéristique x² − x − 1 = 0, on obtient le nombre d'or φ = (1+√5)/2 :
T(n) = (1/√5) · (φⁿ − (1−φ)ⁿ)
Donc T(n) = Θ(φⁿ) ≈ Θ(1.618ⁿ), exponentiel.
PGCD — un cas plus subtil
def pgcd(x, y):
if y == 0:
return x
else:
return pgcd(y, x mod y)
Pas d'équation récurrente directe — on s'appuie sur Fibonacci. On montre par récurrence que si pgcd(x, y) fait k divisions avec x > y > 0, alors x ≥ Fk+2 (le (k+2)-ième terme de Fibonacci).
En passant au log avec φ : k ≤ logφ(√5 + 1) − 3.
T(n) = O(log(x)) pour l'algorithme d'Euclide. Logarithmique.
6. Diviser pour régner
- Diviser le problème en sous-problèmes de même nature
- Régner : résoudre récursivement les sous-problèmes
- Combiner les solutions pour former la solution finale
Tri par fusion
- Diviser le tableau en deux moitiés
- Trier récursivement les deux moitiés
- Fusionner les deux tableaux triés
- Coût de la fusion :
Θ(n)
Équation récurrente : T(n) = 2·T(n/2) + Θ(n) avec T(1) = 1.
Théorème Master
a ≥ 1, b ≥ 1, et T une équation récurrente T(n) = a · T(n/b) + f(n).Dans le cas où
f(n) = Θ(nd) :
T(n) = ⎧ Θ(nd) si a < bd
⎨ Θ(nd · log(n)) si a = bd
⎩ Θ(nlog_b(a)) si a > bd
Interprétation :
n= taille du problèmea= nombre de sous-problèmesn/b= taille de chaque sous-problèmef(n)= coût de la subdivision et de la combinaison
Application : tri par fusion
Tri par fusion : T(n) = 2 · T(n/2) + Θ(n) → a=2, b=2, d=1. Donc bd = 2 = a ⇒ T(n) = Θ(n · log(n)), quasi-linéaire.
Application : multiplication de matrices (Strassen)
Décomposition en sous-matrices : T(n) = 8·T(n/2) + Θ(n²) → a=8, b=2, d=2. bd=4 < a=8 ⇒ T(n) = Θ(nlog₂(8)) = Θ(n³). Pas d'amélioration par rapport à l'algorithme naïf. (Strassen avec 7 récursions au lieu de 8 donne Θ(nlog₂(7)) ≈ Θ(n2.81) — sous-cubique).
7. Correction d'un algorithme — invariants
Démarche pour prouver un algorithme itératif
- Analyser les boucles, en commençant par la boucle la plus interne
- Concevoir un invariant de boucle
- Prouver l'invariant par récurrence :
- Montrer qu'il est vrai avant la première itération
- Montrer que s'il est vrai à l'itération
k, il l'est à l'itérationk+1
- Utiliser l'invariant pour prouver la terminaison
- Utiliser l'invariant pour prouver que le résultat est correct
Exemple — somme d'un tableau
def somme(t):
S = 0
n = len(t)
for i in range(n):
S += t[i]
return S
Invariant proposé : à la fin de l'itération i, S contient la somme des i premiers éléments de t :
Si = Σk=0i-1 t[k]
Preuve par récurrence :
- Base :
S₁ = S_initial + t[0] = t[0]. La propriété est vraie pouri = 1. - Hérédité : si
Si = Σ t[k]jusqu'à l'indexi-1, alorsSi+1 = Si + t[i] = Σ jusqu'à i. ✓
L'algorithme termine après n itérations. La complexité est Θ(n).
Validité d'un algorithme récursif
Trois choses à prouver :
- Cas d'arrêt : le résultat produit est correct
- Hérédité : sous hypothèse que les appels récursifs sont corrects, le résultat l'est aussi
- Convergence : toute séquence d'appels conduit à un cas d'arrêt
★ Réviser le chapitre
🃏 Flashcards
O = borne supérieure. Ω = borne inférieure. Θ = encadre exactement (les deux à la fois).O(log n) — on divise la taille par 2 à chaque itération.T(n) = T(n-1) + T(n-2) ⇒ T(n) = Θ(φⁿ) avec φ = nombre d'or ≈ 1.618.T(n) = 2·T(n-1) + 1 = 2ⁿ - 1 = Θ(2ⁿ).O(log x). Démonstration via la suite de Fibonacci.T(n) = a·T(n/b) + Θ(nd) : compare a à bd. Trois cas selon <, =, >.Θ(n · log n). Master : a=2, b=2, d=1 → a = bd ⇒ n · log n.✎ Quiz éclair — classer les complexités
for i in range(n): for j in range(n): tab[i][j] = i*j ?n!, 2ⁿ, n², log n par ordre croissant asymptotique :T(n) = T(n-1) + 5 est :T(n) = 3·T(n/2) + n. Avec a=3, b=2, d=1 : bd=2 < a=3. Théorème Master :f(n) = 3n² + 5n + 7. Quelle borne minimale satisfait f(n) ∈ O(g(n)) ?n = 2ᵏ éléments :Score : 0 / 7 ·
❓ Q/R
Comment résoudre T(n) = 2·T(n/2) + n² ?
a=2, b=2, d=2. On compare a à bd = 4 : a = 2 < 4.Cas 1 de Master ⇒
T(n) = Θ(n²). Le coût du combine domine la récursion.
Pourquoi un fold récursif est tail-recursive pour foldLeft mais pas foldRight ?
foldRight : on doit attendre le résultat de l'appel récursif (sur la queue) avant de combiner avec l'élément courant. L'appel récursif n'est pas la dernière opération.
Conséquence :
foldLeft sur 1 million d'éléments est OK, foldRight peut faire StackOverflowError.
Comment prouver la correction d'un algo récursif ?
- Cas d'arrêt : la valeur retournée dans le cas de base est correcte. Ex :
fact(0) = 1. - Hérédité : sous hypothèse de récurrence (les appels récursifs renvoient le bon résultat), montrer que la combinaison donne le bon résultat. Ex : si
fact(n-1) = (n-1)!, alorsn · fact(n-1) = n!. - Convergence : toute séquence d'appels mène à un cas d'arrêt. Ex :
ndécroît strictement et est borné inférieurement par 0.
À quoi sert le calcul de complexité dans la vraie vie ?
- Choisir entre algorithmes — un tri quasi-linéaire (n log n) bat un quadratique (n²) dès quelques milliers d'éléments.
- Détecter les goulots — un
O(n²)dans une boucle qui tourne sur 1 M d'éléments est suspect. - Dimensionner — pour traiter 10⁹ requêtes/jour, on ne peut pas se permettre du linéaire sur 10⁶ items à chaque requête.
- Communiquer — annoncer « algo O(n log n) » est plus précis que « rapide ».