Au-delà des tris simples vus en première année, deux méthodes efficaces fondées sur « diviser pour régner » : le tri rapide et le tri par fusion.
🎯 Objectifs du chapitre
Rappeler les tris simples de première année et leur complexité.
Comprendre les notions de tri en place et de stabilité.
Définir et implémenter le tri rapide ; discuter le choix du pivot.
Définir et implémenter le tri par fusion.
Comparer les complexités des différentes méthodes.
Sommaire
Rappels : les tris simples de première année
Deux propriétés clés : tri en place et stabilité
Le tri rapide (quicksort)
Le tri par fusion (merge sort)
Comparaison des algorithmes de tri
1. Rappels : les tris simples de première année
En première année, trois tris ont été étudiés. Tous ont une complexité quadratique \(O(n^2)\) dans le pire des cas, ce qui les rend lents sur de grandes listes.
Tri
Principe
Complexité (pire cas)
Sélection
Chercher le minimum et le placer en tête, puis recommencer sur le reste.
\(O(n^2)\)
Insertion
Insérer chaque élément à sa place dans la partie déjà triée.
\(O(n^2)\)
À bulles
Échanger les éléments adjacents mal ordonnés, en plusieurs passes.
\(O(n^2)\)
L'objectif de ce chapitre est d'atteindre une complexité bien meilleure, \(O(n \log n)\). Reprenons d'abord en détail les deux tris simples les plus utilisés.
1.1 Le tri par sélection
Principe. À l'étape \(i\), on cherche le minimum de la partie non triée \(t[i..n-1]\) et on l'échange avec \(t[i]\). La partie gauche grandit d'une case à chaque étape jusqu'à ce que tout le tableau soit trié.
Exemple de déroulement sur le tableau \(t = [29,\ 10,\ 14,\ 37,\ 13]\). À chaque passe, le minimum de la zone non triée (en gras) est amené en tête de cette zone :
Passe
Zone non triée
Minimum trouvé
Tableau après échange
1
[29, 10, 14, 37, 13]
10 (indice 1)
10 | 29, 14, 37, 13
2
29, 14, 37, 13
13 (indice 4)
10, 13 | 14, 37, 29
3
14, 37, 29
14 (déjà en place)
10, 13, 14 | 37, 29
4
37, 29
29 (indice 4)
10, 13, 14, 29 | 37
Fin
—
—
10, 13, 14, 29, 37
Implémentation (en place). Une seule application directe du principe :
deftri_selection(t):
n = len(t)
for i inrange(n - 1):
imin = i
for j inrange(i + 1, n):
if t[j] < t[imin]:
imin = j
t[i], t[imin] = t[imin], t[i] # échange le minimum en position ireturn t
Exemple d'application. Comme le tri par sélection effectue au plus \(n-1\) échanges (un seul par passe), il est adapté aux situations où écrire une donnée coûte cher : tri de gros enregistrements en mémoire, ou supports dont le nombre d'écritures est limité (mémoires flash). Il minimise le nombre de déplacements, même s'il fait toujours autant de comparaisons.
Exemple de calcul de complexité. À la passe \(i\) (pour \(i\) de \(0\) à \(n-2\)), la recherche du minimum de \(t[i..n-1]\) demande \(n-1-i\) comparaisons. Le nombre total de comparaisons est donc
\[ C(n) = \sum_{i=0}^{n-2} (n-1-i) = (n-1) + (n-2) + \dots + 1 = \frac{n(n-1)}{2}. \]
Ce nombre ne dépend pas du contenu du tableau : la complexité est \(\Theta(n^2)\) dans tous les cas (meilleur, moyen, pire). Les échanges, eux, sont au plus \(n-1\), soit \(O(n)\).
1.2 Le tri par insertion
Principe. On parcourt le tableau de gauche à droite. À l'étape \(i\), l'élément \(t[i]\) est inséré à sa place dans la partie déjà triée \(t[0..i-1]\), en le décalant vers la gauche tant qu'il est plus petit que son voisin.
deftri_insertion(t):
for i inrange(1, len(t)):
cle = t[i]
j = i - 1while j >= 0and t[j] > cle:
t[j + 1] = t[j]
j -= 1
t[j + 1] = cle
return t
✍️ Exercice (à faire avant de regarder la solution).
Déroulez le tri par insertion sur le tableau \(t = [5,\ 2,\ 4,\ 6,\ 1,\ 3]\) en donnant l'état du tableau après chaque insertion.
Calculez la complexité du tri par insertion dans le meilleur et dans le pire des cas.
👁️ Afficher / masquer la solution
1. Déroulement sur \([5, 2, 4, 6, 1, 3]\). La barre | sépare la partie triée (à gauche) du reste.
Élément inséré
Tableau après insertion
départ
5 | 2, 4, 6, 1, 3
2
2, 5 | 4, 6, 1, 3
4
2, 4, 5 | 6, 1, 3
6
2, 4, 5, 6 | 1, 3
1
1, 2, 4, 5, 6 | 3
3
1, 2, 3, 4, 5, 6
2. Complexité. Notons \(c_i\) le nombre de décalages effectués pour insérer \(t[i]\).
• Meilleur cas — tableau déjà trié : la condition t[j] > cle est fausse immédiatement, donc \(1\) comparaison par étape et \(0\) décalage. Total \(= n-1 = \Theta(n)\).
• Pire cas — tableau trié à l'envers : insérer \(t[i]\) demande \(i\) décalages. Le total est
\[ \sum_{i=1}^{n-1} i = \frac{n(n-1)}{2} = \Theta(n^2). \]
Le tri par insertion est donc \(O(n^2)\) au pire, mais très efficace sur des données presque triées (proche de \(O(n)\)) — c'est sa grande qualité par rapport au tri par sélection.
2. Deux propriétés clés : tri en place et stabilité
Pour caractériser un algorithme de tri, deux propriétés sont importantes :
Tri en place. Un tri est en place s'il n'utilise qu'une quantité de mémoire supplémentaire constante \(O(1)\), en réorganisant les éléments à l'intérieur du tableau d'origine (sans créer de copie de taille n).
Stabilité. Un tri est stable s'il conserve l'ordre relatif initial des éléments ayant une clé égale. Utile lorsqu'on trie selon plusieurs critères successifs.
Définition mathématique. Soit un tableau \(t = (t_0, t_1, \dots, t_{n-1})\) muni d'une fonction de clé \(\text{cl\'e}\). Un tri est stable si, pour tout couple d'indices \(i < j\) tel que \(\text{cl\'e}(t_i) = \text{cl\'e}(t_j)\), l'élément issu de l'indice \(i\) apparaît avant celui issu de l'indice \(j\) dans le tableau trié.
\( i < j \ \text{ et } \ \text{cl\'e}(t_i) = \text{cl\'e}(t_j) \quad \Longrightarrow \quad \sigma(i) < \sigma(j) \)
où \(\sigma\) est la permutation appliquée par le tri (position finale de chaque élément).
Pour voir la différence, ajoutons une étiquette à chaque valeur et trions selon la valeur uniquement. Partons de \([\,(3,a),\ (3,c),\ (2,b)\,]\) (l'étiquette \(a\) est avant \(c\) au départ) :
Tri
Résultat
Ordre de (3,a) et (3,c)
Tri par insertion (stable)
(2,b), (3,a), (3,c)
conservé : a avant c ✔
Tri par sélection (non stable)
(2,b), (3,c), (3,a)
inversé : c avant a ✘
Pourquoi le tri par sélection casse l'ordre ici : à la première passe, le minimum \(2\) (étiquette \(b\), en position 2) est échangé avec la position 0, occupée par \((3,a)\). Cet échange envoie \((3,a)\) après \((3,c)\), détruisant leur ordre initial. C'est le mécanisme d'échange « à distance » qui rend le tri par sélection non stable.
Application concrète : on trie une liste de personnes déjà triées par prénom, cette fois par âge. Un tri stable garde, parmi les personnes de même âge, l'ordre alphabétique acquis précédemment ; un tri non stable peut le détruire.
3. Le tri rapide (quicksort)
Principe (diviser pour régner). On choisit un élément appelé pivot. On réorganise la liste (étape de partition) de sorte que les éléments plus petits que le pivot soient à sa gauche et les plus grands à sa droite. Le pivot est alors à sa place définitive. On applique ensuite récursivement le même procédé aux deux sous-listes gauche et droite.
Illustration graphique (arbre d'appels) sur \([3,\ 8,\ 1,\ 5,\ 2]\), pivot = premier élément. Chaque nœud montre la liste reçue ; le pivot est en rouge, et les flèches mènent aux sous-listes « \(\leq\) pivot » (gauche) et « \(>\) pivot » (droite). On remonte ensuite en concaténant gauche + pivot + droite.
Une implémentation simple et lisible (non en place, qui construit deux sous-listes) :
deftri_rapide(L):
iflen(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)
L'étape de partition d'une liste de taille \(n\) parcourt les \(n-1\) autres éléments : son coût est \(\Theta(n)\). Notons \(T(n)\) le coût total du tri. Détaillons les deux cas extrêmes.
Meilleur des cas — pivot toujours « médian ». La partition coupe la liste en deux moitiés de taille \(\approx n/2\) :
\[ T(n) = 2\,T\!\left(\tfrac{n}{2}\right) + c\,n. \]
En déroulant, au niveau \(k\) il y a \(2^k\) sous-listes de taille \(n/2^k\), donc un travail de \(2^k \cdot c\,\tfrac{n}{2^k} = c\,n\) par niveau. L'arbre a \(\log_2 n\) niveaux, d'où
\[ T(n) = c\,n \times \log_2 n = \Theta(n \log n). \]
Pire des cas — pivot toujours extrémal (minimum ou maximum, par ex. sur une liste déjà triée avec ce choix de pivot). Une seule sous-liste est non vide et perd un seul élément à chaque étape :
\[ T(n) = T(n-1) + c\,n. \]
En déroulant :
\[ T(n) = c\,n + c\,(n-1) + \dots + c\cdot 1 = c\,\frac{n(n+1)}{2} = \Theta(n^2). \]
En moyenne (pivot quelconque), on démontre que le coût reste \(\Theta(n \log n)\) : le pire cas est rare dès que le pivot est bien choisi.
Choix du pivot. Prendre systématiquement le premier élément rend le pire cas fréquent (listes déjà triées). On préfère un pivot aléatoire, ou la médiane de trois (premier, milieu, dernier), pour rendre le pire cas très improbable et garantir \(O(n \log n)\) en pratique.
Le tri rapide « classique » (avec partition à l'intérieur du tableau) est en place (\(O(1)\) d'espace auxiliaire, hormis la pile de récursion) mais non stable.
4. Le tri par fusion (merge sort)
Principe (diviser pour régner). On coupe la liste en deux moitiés, on trie récursivement chacune, puis on fusionne les deux moitiés triées en une seule liste triée. La fusion parcourt les deux listes en parallèle et choisit à chaque étape le plus petit élément de tête.
Illustration graphique (arbre de division puis de fusion) sur \([3,\ 8,\ 1,\ 5,\ 2]\). On descend en coupant en deux jusqu'aux singletons, puis on remonte en fusionnant :
L'étape de fusion de deux listes déjà triées :
deffusion(A, B):
C = []
i, j = 0, 0while i < len(A) and j < len(B):
if A[i] <= B[j]:
C.append(A[i]); i += 1else:
C.append(B[j]); j += 1return C + A[i:] + B[j:]
Le tri par fusion lui-même :
deftri_fusion(L):
iflen(L) <= 1:
return L
m = len(L) // 2
gauche = tri_fusion(L[:m])
droite = tri_fusion(L[m:])
return fusion(gauche, droite)
✍️ Exercice (à faire avant de regarder la solution).
Déroulez la fusion des deux listes triées \(A = [1,\ 4,\ 7]\) et \(B = [2,\ 3,\ 9]\), étape par étape.
Établissez la relation de récurrence vérifiée par \(T(n)\), le coût du tri par fusion, et déduisez-en la complexité dans le pire des cas.
👁️ Afficher / masquer la solution
1. Fusion de \(A=[1,4,7]\) et \(B=[2,3,9]\). À chaque étape on compare les têtes \(A[i]\) et \(B[j]\) et on prend la plus petite.
Comparaison
Élément ajouté
C (résultat)
1 ≤ 2
1 (de A)
[1]
4 > 2
2 (de B)
[1, 2]
4 > 3
3 (de B)
[1, 2, 3]
4 ≤ 9
4 (de A)
[1, 2, 3, 4]
7 ≤ 9
7 (de A)
[1, 2, 3, 4, 7]
A vide
9 (reste de B)
[1, 2, 3, 4, 7, 9]
La fusion de deux listes de tailles \(m\) et \(p\) coûte \(\Theta(m+p)\) : chaque élément est ajouté une seule fois.
2. Récurrence et pire cas. Le tri coupe la liste en deux moitiés de taille \(n/2\), les trie, puis les fusionne en \(\Theta(n)\). D'où, dans tous les cas :
\[ T(n) = 2\,T\!\left(\tfrac{n}{2}\right) + c\,n. \]
L'arbre de division a \(\log_2 n\) niveaux ; à chaque niveau, l'ensemble des fusions traite \(n\) éléments, soit un coût \(c\,n\) par niveau. Donc
\[ T(n) = c\,n \times \log_2 n = \Theta(n \log n). \]
Contrairement au tri rapide, la coupe en deux moitiés égales est garantie quel que soit le contenu : il n'y a pas de pire cas dégradé. La complexité est \(\Theta(n\log n)\) au meilleur, en moyenne et au pire.
En contrepartie, le tri par fusion n'est pas en place : il nécessite \(O(n)\) de mémoire supplémentaire pour les listes fusionnées. Il est en revanche stable (à condition d'utiliser <= dans la fusion).
5. Comparaison des algorithmes de tri
Tri
Moyenne
Pire cas
En place
Stable
Sélection
\(O(n^2)\)
\(O(n^2)\)
Oui
Non
Insertion
\(O(n^2)\)
\(O(n^2)\)
Oui
Oui
Tri rapide
\(O(n \log n)\)
\(O(n^2)\)
Oui
Non
Tri fusion
\(O(n \log n)\)
\(O(n \log n)\)
Non
Oui
✅ Points essentiels.
Les tris simples sont en \(O(n^2)\) ; tri rapide et tri fusion atteignent \(O(n \log n)\).
Tri rapide : \(O(n \log n)\) en moyenne, \(O(n^2)\) au pire ; en place mais non stable ; le choix du pivot est crucial.
Tri fusion : \(O(n \log n)\) garanti, stable, mais nécessite \(O(n)\) d'espace supplémentaire.
Les deux reposent sur le paradigme « diviser pour régner ».
CPGE MP/PSI — Informatique · Chapitre : Les algorithmes de tri · Conforme au programme officiel 2023