Pr. EL HADIQ Zouhair
Pour le tri rapide et le tri par fusion : spécification, correction (invariant de boucle et récurrence bien fondée), terminaison (variant et fonction de mesure) et complexité.
Un algorithme est correct (au sens total) s'il vérifie :
\[ \text{Correction totale} \;=\; \text{Correction partielle} \;+\; \text{Terminaison} \]
La correction partielle dit : si l'algorithme termine, la sortie respecte la spécification. La terminaison dit : pour toute entrée valide, il s'arrête en temps fini. Outils : invariant de boucle (itératif) ou récurrence bien fondée (récursif) pour la correction ; variant (itératif) ou fonction de mesure (récursif) pour la terminaison.
| Objectif | Boucle (itératif) | Récursion |
|---|---|---|
| Correction | Invariant : initialisation + conservation + sortie | Récurrence bien fondée sur la mesure : base + hérédité |
| Terminaison | Variant \(V \in \mathbb{N}\) strictement décroissant (Floyd) | Mesure \(\mu \in \mathbb{N}\) décroissante à chaque appel |
Le tri par fusion repose sur une fonction récursive tri_fusion et une fonction auxiliaire itérative fusion. On analyse d'abord la fusion (boucle → invariant + variant), puis le tri lui-même (récursion → mesure + récurrence).
fusion : spécificationdef fusion(A, B):
C = []
i, j = 0, 0
while i < len(A) and j < len(B):
if A[i] <= B[j]:
C.append(A[i]); i += 1
else:
C.append(B[j]); j += 1
return C + A[i:] + B[j:]
fusion(A,B) renvoie une liste triée contenant exactement les éléments de \(A\) et de \(B\) (avec multiplicités).
fusion : invariant de boucleInitialisation. Avant le premier tour, \(C = [\,]\) et \(i = j = 0\). \((I_1)\) et \((I_2)\) sont vraies (\(C\) vide, \(A[0{:}0] = B[0{:}0] = [\,]\)) ; \((I_3)\) est vraie car \(C\) n'a aucun élément. ✓
Conservation. Supposons \(I\) vrai et la condition de boucle vraie (\(i < \operatorname{len}(A)\) et \(j < \operatorname{len}(B)\)). On ajoute \(m = \min(A[i], B[j])\) à \(C\) et on avance le compteur correspondant. Par \((I_3)\), tout élément de \(C\) est \(\le A[i]\) et \(\le B[j]\), donc \(\le m\) : \(C\) reste triée et \(m\) est bien placé en fin \((I_1)\). On a consommé un élément de plus de \(A\) ou de \(B\), donc \((I_2)\) reste vrai. Comme \(A\) et \(B\) sont triées, le nouveau \(A[i]\) (ou \(B[j]\)) est \(\ge m \ge\) tous les éléments de \(C\) : \((I_3)\) est rétabli. ✓
Sortie. À la fin de la boucle, \(i = \operatorname{len}(A)\) ou \(j = \operatorname{len}(B)\) : l'une des deux listes est entièrement consommée. On renvoie C + A[i:] + B[j:]. Par \((I_2)\), \(C\) contient \(A[0{:}i]\) et \(B[0{:}j]\) ; on lui ajoute le reste (\(A[i{:}]\) ou \(B[j{:}]\), l'autre étant vide). Par \((I_3)\) et le tri de \(A, B\), ce reste est trié et tous ses éléments sont \(\ge\) ceux de \(C\). Le résultat est donc trié et contient exactement les éléments de \(A\) et \(B\) : la postcondition est satisfaite. ✓
fusion : variant de boucleÀ chaque tour, on incrémente \(i\) ou \(j\) de \(1\) (jamais les deux), donc \(V\) décroît strictement de \(1\). De plus, tant que la boucle s'exécute, \(i < \operatorname{len}(A)\) et \(j < \operatorname{len}(B)\), donc \(V \ge 1 > 0\). Par le théorème de terminaison de Floyd (une suite d'entiers naturels strictement décroissante est finie, car \((\mathbb{N}, <)\) est bien fondé), la boucle effectue un nombre fini de tours. ✓
Le nombre de tours est au plus \(\operatorname{len}(A) + \operatorname{len}(B)\) : la fusion de deux listes de tailles \(m\) et \(p\) est en \(O(m + p)\).
tri_fusion : fonction de mesuredef tri_fusion(L):
if len(L) <= 1:
return L
m = len(L) // 2
return fusion(tri_fusion(L[:m]), tri_fusion(L[m:]))
Pour une liste de longueur \(n \ge 2\), avec \(m = \lfloor n/2 \rfloor\), les appels récursifs portent sur \(L[{:}m]\) et \(L[m{:}]\) de longueurs \(\lfloor n/2 \rfloor\) et \(\lceil n/2 \rceil\). Comme \(n \ge 2\), on a \(\lfloor n/2 \rfloor < n\) et \(\lceil n/2 \rceil \le n - 1 < n\) : la mesure décroît strictement à chaque appel. Comme \(\mathbb{N}\) est bien fondé, on atteint le cas de base en un nombre fini d'étapes : tri_fusion termine. (La fonction fusion termine d'après A.3.) ✓
tri_fusion : récurrence bien fondéetri_fusion(L) renvoie une liste triée contenant exactement les éléments de \(L\) (une permutation triée de \(L\)).
On raisonne par récurrence forte sur la mesure \(\mu(L) = \operatorname{len}(L)\) (récurrence bien fondée sur \((\mathbb{N}, <)\)).
Cas de base (\(\operatorname{len}(L) \le 1\)). Une liste vide ou à un élément est déjà triée ; elle est renvoyée telle quelle. \(P(L)\) est vraie. ✓
Hérédité. Soit \(n \ge 2\) ; supposons \(P\) vraie pour toute liste de longueur \(< n\). Pour \(L\) de longueur \(n\) : par hypothèse de récurrence (longueurs \(< n\)), tri_fusion(L[:m]) et tri_fusion(L[m:]) renvoient des permutations triées de \(L[{:}m]\) et \(L[m{:}]\). Par la correction de fusion (A.2), fusion(gauche, droite) renvoie une liste triée contenant exactement les éléments de gauche et droite, c'est-à-dire de \(L[{:}m]\) et \(L[m{:}]\), soit exactement les éléments de \(L\). Donc \(P(L)\) est vraie. ✓
Par récurrence bien fondée, \(P(L)\) est vraie pour toute liste \(L\) : tri_fusion est partiellement correct. Avec la terminaison (A.4), il est totalement correct.
tri_fusion (calcul)Mise en équation. La fonction tri_fusion fait deux appels récursifs sur des listes de taille \(n/2\), puis une fusion qui parcourt les \(n\) éléments (coût linéaire, A.3). En notant \(c\) une constante :
\[ T(n) = 2\,T\!\left(\tfrac{n}{2}\right) + c\,n \quad (n \ge 2), \qquad T(1) = O(1) \]
Résolution par déroulement. On remplace successivement \(T(n/2)\), \(T(n/4)\), … :
\[ \begin{aligned} T(n) &= 2\,T\!\left(\tfrac{n}{2}\right) + c\,n \\ &= 2\Big[2\,T\!\left(\tfrac{n}{4}\right) + c\tfrac{n}{2}\Big] + c\,n = 4\,T\!\left(\tfrac{n}{4}\right) + 2c\,n \\ &= 8\,T\!\left(\tfrac{n}{8}\right) + 3c\,n \\ &= \;\cdots\; = 2^{k}\,T\!\left(\tfrac{n}{2^{k}}\right) + k\,c\,n \end{aligned} \]
Le déroulement s'arrête quand la sous-liste a taille \(1\), c'est-à-dire \(\dfrac{n}{2^{k}} = 1\), soit \(k = \log_2 n\). On obtient alors :
\[ T(n) = n\,T(1) + (\log_2 n)\,c\,n = c\,n\log_2 n + O(n) = \boxed{O(n\log n)} \]
On retrouve ce résultat par le théorème maître : pour \(T(n) = a\,T(n/b) + O(n)\) avec \(a = b = 2\), on a \(n^{\log_b a} = n^{1} = n\), cas où la complexité vaut \(O(n\log n)\).
Comparaisons et conclusion. Chaque niveau effectue au plus \(n - 1\) comparaisons et il y a \(\log_2 n\) niveaux : le nombre de comparaisons est de l'ordre de \(n\log_2 n\). Cette complexité ne dépend pas du contenu : \(O(n\log n)\) est le même au meilleur et au pire des cas, donc garanti. La complexité spatiale est \(O(n)\) (listes auxiliaires) : le tri par fusion n'est pas en place, mais il est stable.
def tri_rapide(L):
if len(L) <= 1:
return L
pivot = L[0]
petits = [x for x in L[1:] if x <= pivot]
grands = [x for x in L[1:] if x > pivot]
return tri_rapide(petits) + [pivot] + tri_rapide(grands)
tri_rapide(L) renvoie une permutation triée par ordre croissant de \(L\).
Ce lemme remplace ici l'invariant de boucle : les deux compréhensions parcourent \(L[1{:}]\) et chaque \(x\) y est rangé selon la comparaison à pivot, de façon exhaustive et exclusive. C'est l'analogue « ensembliste » de l'invariant d'une partition écrite avec une boucle.
tri_rapide : fonction de mesureLe pivot est retiré avant les appels récursifs : \(\operatorname{len}(\text{petits}) + \operatorname{len}(\text{grands}) = \operatorname{len}(L) - 1\). Donc \(\operatorname{len}(\text{petits}) \le n - 1 < n\) et \(\operatorname{len}(\text{grands}) \le n - 1 < n\) : la mesure décroît strictement à chaque appel récursif. Comme \(\mathbb{N}\) est bien fondé, le cas de base est atteint en un nombre fini d'étapes : tri_rapide termine pour toute entrée. ✓
tri_rapide : récurrence bien fondéetri_rapide(L) renvoie une permutation triée par ordre croissant de \(L\).
Récurrence forte sur \(\mu(L) = \operatorname{len}(L)\).
Cas de base (\(\operatorname{len}(L) \le 1\)). \(L\) est déjà triée et renvoyée telle quelle : \(P(L)\) vraie. ✓
Hérédité. Soit \(n \ge 2\), \(P\) supposée vraie pour toute liste de longueur \(< n\). Comme \(\operatorname{len}(\text{petits}) < n\) et \(\operatorname{len}(\text{grands}) < n\), l'hypothèse de récurrence donne : tri_rapide(petits) est une permutation triée de petits, et tri_rapide(grands) une permutation triée de grands. La valeur renvoyée tri_rapide(petits) + [pivot] + tri_rapide(grands) :
Donc \(P(L)\) est vraie. Par récurrence bien fondée, tri_rapide est partiellement correct ; avec B.2, il est totalement correct. ✓
tri_rapide (calcul)L'étape de partition parcourt \(L[1{:}]\) (deux compréhensions) : elle coûte \(c\,n\) opérations, soit \(O(n)\). Le coût total dépend de l'équilibre des deux sous-listes, donc du pivot.
Pire des cas. Le pivot est toujours le plus petit (ou le plus grand) élément : une sous-liste est vide, l'autre a \(n - 1\) éléments. La récurrence est \(T(n) = T(n - 1) + c\,n\). On la déroule :
\[ \begin{aligned} T(n) &= T(n-1) + c\,n = T(n-2) + c(n-1) + c\,n = \cdots \\ &= c\,\big(n + (n-1) + \cdots + 2\big) + T(1) \\ &= c\left(\frac{n(n+1)}{2} - 1\right) + O(1) = \boxed{O(n^2)} \end{aligned} \]
Meilleur cas et cas moyen. La partition équilibre les deux côtés (\(n/2\) chacun) : \(T(n) = 2\,T(n/2) + c\,n\), exactement la récurrence du tri par fusion (A.6), de solution \(O(n\log n)\). En moyenne sur tous les ordres d'entrée, le nombre moyen de comparaisons vaut environ \(2n\ln n \approx 1{,}39\,n\log_2 n\), donc également \(O(n\log n)\).
| Cas | Récurrence | Complexité |
|---|---|---|
| Moyen / meilleur (partition équilibrée) | \(T(n) = 2\,T(n/2) + O(n)\) | \(O(n\log n)\) |
| Pire (pivot extrémal, liste triée) | \(T(n) = T(n-1) + O(n)\) | \(O(n^2)\) |
| Tri | Correction | Terminaison | Complexité |
|---|---|---|---|
| Tri fusion | Invariant de fusion + récurrence bien fondée sur \(\operatorname{len}\) | Variant \((m-i)+(p-j)\) ; mesure \(\mu = \operatorname{len}(L)\) | \(O(n\log n)\) garanti |
| Tri rapide | Lemme de partition + récurrence bien fondée sur \(\operatorname{len}\) | Mesure \(\mu = \operatorname{len}(L)\) (pivot retiré) | \(O(n\log n)\) moyen, \(O(n^2)\) pire |
CPGE MP/PSI — Informatique · Analyse des tris : correction, terminaison et complexité