Algorithmique 1 · L2 · Section 3/6
Tris et structures de données
Progression
#Algorithmes de tri
Prérequis: tableaux, boucles, récursion, analyse de complexité (chapitre correspondant), tas binaire (module Structures de données).
Objectifs:
- Comprendre pourquoi aucun tri par comparaison ne descend sous Ω(n log n) dans le pire cas.
- Pour chaque tri dominant: l'invariant de correction, les coûts (pire, moyen, meilleur), les propriétés (stabilité, place).
- Savoir quand un tri non comparatif s'impose et à quelles conditions.
Le tri est le banc d'essai parfait: chaque algorithme porte un compromis différent entre simplicité, garanties et usage mémoire, et chacun se prouve par un invariant.
#La borne inférieure des tris par comparaison
Théorème: tout algorithme de tri qui ne peut qu'ordonner deux éléments en les comparant effectue Ω(n log n) comparaisons dans le pire cas.
Preuve: pour une entrée de taille n, le comportement de l'algorithme (suite des comparaisons et échanges) est déterminé par l'ordre initial, inconnu. Représentons son exécution par un arbre de décision binaire (comparaison = nœud interne, deux branches selon le résultat). Il doit exister au moins une feuille distincte par permutation possible de l'entrée, donc au moins n! feuilles. Un arbre binaire de hauteur h a au plus 2^h feuilles: , d'où (l'encadrement de log(n!) est établi dans le chapitre Complexité des algorithmes). Le pire cas suit au moins un chemin racine-feuille de cette hauteur.
Conséquence: tri fusion et tri par tas atteignent la borne (Θ(n log n) garantie). Sortir de la borne exige de ne pas décider uniquement par comparaisons: c'est le rôle des tris non comparatifs en fin de chapitre.
#Ce que le modèle de l'arbre de décision suppose exactement
Un tri par comparaison n'obtient d'information sur l'ordre des éléments que par des tests du type A[i] ≤ A[j], A[i] < A[j], A[i] = A[j], etc. Il n'inspecte pas les valeurs autrement: aucun accès à la représentation binaire, aucun calcul arithmétique sur les clés. Trois précisions rendent la preuve rigoureuse.
- Éléments distincts sans perte de généralité. On prouve la borne pour des entrées à éléments deux à deux distincts. Un minorant obtenu sur ce sous-ensemble d'entrées reste un minorant sur l'ensemble complet, qui contient ces entrées.
- Les tests d'égalité sont inutiles. Un tri correct n'a jamais besoin de conclure
A[i] = A[j]: il peut toujours traiter l'égalité commeA[i] ≤ A[j], ce qui donne un ordre valide. Les quatre tests≤,≥,<,>sont donc équivalents en information; on suppose que toutes les comparaisons ont la formeA[i] ≤ A[j]. - Chaque permutation doit être une feuille atteignable. Un arbre de décision est un arbre binaire complet (chaque nœud est une feuille ou a deux enfants). Chaque nœud interne est étiqueté par une comparaison
i : jportant sur les positions initiales des éléments; chaque feuille est étiquetée par la permutation⟨π(1), …, π(n)⟩établie. La longueur du plus long chemin de la racine à une feuille atteignable est le nombre de comparaisons dans le pire cas; la borne sur la hauteur de l'arbre est donc une borne sur le coût de l'algorithme.
Corollaire. Tri fusion et tri par tas sont asymptotiquement optimaux parmi les tris par comparaison: leurs bornes supérieures Θ(n log n) coïncident avec la borne inférieure Ω(n log n) du théorème. Aucun tri par comparaison ne peut être plus rapide qu'eux de plus d'un facteur constant.
Exercice. Quel est le plus petit nombre de comparaisons possible dans le meilleur cas d'un tri par comparaison? Réponse: n − 1. Une feuille située à profondeur d correspond à une exécution qui n'a effectué que d comparaisons; ces comparaisons définissent un ordre partiel sur les n éléments, et la feuille n'est légitime que si cet ordre partiel admet une unique extension totale. Or un ordre partiel sur n éléments n'a une unique extension totale que si son graphe de comparabilité est connexe, ce qui demande au moins n − 1 arêtes. Donc toute feuille est à profondeur ≥ n − 1, et l'algorithme qui compare A[1] à A[2], puis A[2] à A[3], etc., atteint cette profondeur sur l'entrée déjà triée.
Exercice. On sait que l'entrée est partiellement triée: l'élément initialement en position i avec i mod 4 = 0 est à sa place définitive ou à une position d'écart; rien n'est connu des autres positions. La borne Ω(n log n) tient-elle encore? Oui. Restreignons-nous au sous-ensemble des entrées où les éléments contraints sont déjà à leur place: les 3n/4 éléments restants peuvent alors occuper les positions libres dans n'importe quel ordre, ce qui donne (3n/4)! entrées distinctes, donc au moins (3n/4)! feuilles atteignables, donc une hauteur d'arbre au moins log((3n/4)!) = Θ(n log n). Une entrée partiellement triée ne fait pas tomber la borne du pire cas; elle abaisse seulement le coût de certains algorithmes adaptatifs sur ces entrées.
#Tris quadratiques
#Tri par insertion
Invariant: au début de l'itération i, le segment t[0..i−1] contient les i premiers éléments de l'entrée, triés. L'itération prend t[i], décale vers la droite les éléments plus grands, et l'insère à sa place.
Suivez la trace sur [5, 3, 1, 4] dans la visualisation: la zone triée grandit d'une case à chaque itération, le trait marquant la frontière.
Le pseudocode du cours, en indices 1 à n:
1INSERTION-SORT(A, n)21 pour i = 2 à n32 clé = A[i] // l'élément à insérer dans la main43 // insérer A[i] dans la zone triée A[1..i−1]54 j = i − 165 tant que j > 0 et A[j] > clé76 A[j+1] = A[j] // décaler vers la droite87 j = j − 198 A[j+1] = cléInvariant de boucle, énoncé exact: au début de chaque itération de la boucle pour (indice i), le sous-tableau A[1..i−1] contient les éléments initialement en A[1..i−1], mais en ordre trié.
- Initialisation. Avant la première itération, i = 2 et A[1..1] se réduit à A[1], qui est bien l'élément initialement en position 1 et qui est trié par trivialité.
- Maintenance. La boucle
tant quedéplace vers la droite tous les éléments strictement supérieurs à clé, puis écrit clé dans la première case libre rencontrée. Avant l'itération, A[1..i−1] est trié et contient les éléments initiaux de A[1..i−1]; après, A[1..i] est trié et contient les éléments initiaux de A[1..i]. Incrémenter i rétablit donc l'invariant. - Terminaison. La boucle s'arrête quand i dépasse n, c'est-à-dire quand i = n + 1. En substituant cette valeur dans l'invariant: A[1..n] contient les éléments initialement en A[1..n], en ordre trié. L'algorithme est correct.
C'est une récurrence déguisée: la maintenance est l'étape inductive, l'initialisation le cas de base, et la terminaison arrête l'induction pour conclure sur l'état final.
#Le coût exact, ligne par ligne
Chaque ligne coûte un temps constant c_k par exécution; les lignes 5 à 7 s'exécutent t_i fois pour l'itération i (le nombre de décalages plus un).
| Ligne | Instruction | Coût unitaire | Nombre d'exécutions |
|---|---|---|---|
| 1 | pour i = 2 à n | c₁ | n |
| 2 | clé = A[i] | c₂ | n − 1 |
| 4 | j = i − 1 | c₄ | n − 1 |
| 5 | tant que j > 0 et A[j] > clé | c₅ | Σᵢ tᵢ |
| 6 | A[j+1] = A[j] | c₆ | Σᵢ (tᵢ − 1) |
| 7 | j = j − 1 | c₇ | Σᵢ (tᵢ − 1) |
| 8 | A[j+1] = clé | c₈ | n − 1 |
Le coût total est donc
T(n) = c₁n + (c₂ + c₄ + c₈)(n − 1) + c₅ Σᵢ tᵢ + (c₆ + c₇) Σᵢ (tᵢ − 1)
où les sommes portent sur i = 2..n. Tout le comportement de l'algorithme est encapsulé dans les tᵢ: c'est le contenu de l'entrée, et rien d'autre, qui décide du coût.
Meilleur cas (entrée déjà triée). La condition A[j] > clé est fausse dès le premier test, donc tᵢ = 1 pour tout i. Les sommes valent Σ tᵢ = n − 1 et Σ (tᵢ − 1) = 0:
T(n) = c₁n + (c₂ + c₄ + c₅ + c₈)(n − 1) = an + b = Θ(n)
Une comparaison par élément, aucun décalage: le coût est linéaire, et l'algorithme détecte le tri en le lisant une fois.
Pire cas (entrée triée à l'envers). L'élément A[i] est plus petit que tous ceux qui le précèdent, donc le tant que va jusqu'au bout: tᵢ = i. Alors Σᵢ₌₂ⁿ tᵢ = n(n+1)/2 − 1 et Σᵢ₌₂ⁿ (tᵢ − 1) = n(n−1)/2:
T(n) = (c₅/2 + c₆/2 + c₇/2)n² + (c₁ + c₂ + c₄ + c₅/2 − c₆/2 − c₇/2 + c₈)n − (c₂ + c₄ + c₈) = an² + bn + c = Θ(n²)
Le n² vient de la somme arithmétique des décalages: 1 + 2 + … + (n−1) = n(n−1)/2. Aucune astuce d'implémentation ne fait disparaître ce terme dans le modèle de comparaison; il faudra une autre stratégie (diviser pour régner) pour y échapper.
Cas moyen. Si toutes les permutations de l'entrée sont équiprobables, l'élément A[i] doit être inséré en moyenne au milieu de la zone triée, soit i/2 décalages: Σᵢ i/2 = n(n−1)/4 = Θ(n²). Même classe que le pire cas, constante deux fois plus petite.
#Trace complète du tri par insertion
Sur A = ⟨5, 2, 4, 6, 1, 3⟩, en indiquant après chaque itération le contenu du tableau et la zone triée (entre crochets):
| i | clé | Comparaisons et décalages | Tableau |
|---|---|---|---|
| 2 | 2 | 5 > 2: décaler 5 | [2 5] 4 6 1 3 |
| 3 | 4 | 5 > 4: décaler; 2 ≤ 4: arrêt | [2 4 5] 6 1 3 |
| 4 | 6 | 5 ≤ 6: arrêt immédiat | [2 4 5 6] 1 3 |
| 5 | 1 | 6, 5, 4, 2 > 1: quatre décalages | [1 2 4 5 6] 3 |
| 6 | 3 | 6, 5, 4 > 3: trois décalages; 2 ≤ 3: arrêt | [1 2 3 4 5 6] |
Détail de l'itération i = 5, la plus instructive: le tableau est 2 4 5 6 1 3, clé = 1. La boucle tant que décale 6, puis 5, puis 4, puis 2 (quatre décalages, j descend jusqu'à 0), puis écrit 1 en position 1. Le nombre total de décalages sur cette entrée vaut 1 + 1 + 0 + 4 + 3 = 9, à comparer aux n(n−1)/2 = 15 du pire cas et aux n − 1 = 5 du meilleur cas.
Coûts: pire cas Θ(n²) (entrée triée à l'envers, chaque élément traverse toute la zone triée); meilleur cas Θ(n) (entrée déjà triée: une comparaison par élément, aucun décalage); moyen Θ(n²). Stable (les éléments égaux conservent leur ordre relatif, car on ne décale que les strictement plus grands: la condition A[j] > clé est stricte) et en place, O(1) d'espace auxiliaire.
Usage: petits tableaux (des constantes minuscules battent le n log n), tableaux presque triés (coût quasi linéaire, d'où son rôle de finition dans les tris hybrides: Timsort utilisé par Python, segments courts dans les implémentations réelles du tri rapide).
#Pourquoi l'insertion gagne sur les petits tableaux: le seuil exact
Considérons un tri fusion modifié qui trie d'abord n/k sous-listes de longueur k par tri par insertion, puis fusionne ces n/k listes par le mécanisme standard.
- Le tri par insertion des n/k sous-listes coûte Θ(k²) chacune, soit Θ(nk) au total.
- La fusion des n/k listes triées demande lg(n/k) niveaux de fusion, chacun en Θ(n): Θ(n lg(n/k)).
- Total: Θ(nk + n lg(n/k)).
Comparé à Θ(n lg n) pour le tri fusion pur, le terme supplémentaire nk est négligeable exactement quand k = Θ(lg n). En dessous de cette taille, la version hybride reste asymptotiquement identique au tri fusion et gagne les constantes; au-dessus, elle dégénère. En pratique on ne choisit pas k théoriquement mais expérimentalement, sur la machine et le langage réels: c'est le plus grand k pour lequel le tri par insertion reste plus rapide que le tri fusion sur une liste de k éléments. C'est exactement ce que font Timsort (seuil de 32 à 64 selon les implémentations) et les tris rapides de bibliothèque standard.
#Tri par sélection
Invariant: après l'itération i, t[0..i−1] contient les i plus petits éléments de l'entrée, triés et à leur place définitive. L'itération cherche le minimum de t[i..n−1] et l'échange avec t[i].
Inspection visuelle sur la même entrée:
Tri par sélection
Le tri par sélection trouve le plus petit élément et le place au début du tableau.
Coût: exactement n(n−1)/2 comparaisons dans tous les cas (on scanne toujours tout le reste du tableau): Θ(n²), aucun meilleur cas. Mais au plus n − 1 échanges: c'est le tri à retenir quand l'écriture est très coûteuse (grosses structures, mémoire flash). Non stable dans sa version classique (l'échange déplace un élément par-dessus d'autres).
#Tri à bulles
Compare chaque paire adjacente et échange si nécessaire, en propageant le plus grand vers la fin; on répète jusqu'à ce qu'une passe complète ne fasse aucun échange.
Coûts: Θ(n²) en moyenne et au pire, Θ(n) au mieux (détection d'arrêt sur entrée triée). Stable et en place, mais plus d'échanges que le tri par insertion pour un même profil. Intérêt strictement pédagogique: il matérialise la notion d'invariant (après la passe j, les j plus grands sont en place à la fin), mais aucune raison de l'employer.
#Tris en Θ(n log n)
#Tri fusion (merge sort)
Diviser pour régner, sur l'invariant suivant: la fusion de deux segments triés rend un segment trié.
- Couper le tableau en deux moitiés.
- Trier récursivement chaque moitié.
- Fusionner: comparer les têtes des deux segments, extraire la plus petite, répéter.
1def tri_fusion(t):2 if len(t) <= 1:3 return t4 mi = len(t) // 25 gauche = tri_fusion(t[:mi])6 droit = tri_fusion(t[mi:])7 return fusion(gauche, droit)8 9def fusion(g, d):10 resultat = []11 i = j = 012 while i < len(g) and j < len(d):13 if g[i] <= d[j]: # <= garantit la stabilité14 resultat.append(g[i]); i += 1Le pseudocode du cours, avec la fusion sur place et ses sentinelles:
1MERGE-SORT(A, p, r)21 si p < r32 q = ⌊(p + r) / 2⌋ // point de coupe43 MERGE-SORT(A, p, q) // trier A[p..q]54 MERGE-SORT(A, q+1, r) // trier A[q+1..r]65 MERGE(A, p, q, r) // fusionner les deux moitiés7 8MERGE(A, p, q, r)91 n₁ = q − p + 1 ; n₂ = r − q102 soient L[1..n₁+1] et R[1..n₂+1] deux tableaux neufs113 pour i = 1 à n₁ : L[i] = A[p + i − 1] // copier la moitié gauche124 pour j = 1 à n₂ : R[j] = A[q + j] // copier la moitié droite135 L[n₁ + 1] = ∞ ; R[n₂ + 1] = ∞ // sentinelles146 i = 1 ; j = 1Les deux sentinelles valant +∞ ne sont jamais recopiées dans A: dès qu'une des deux moitiés est épuisée, sa sentinelle perd tous les tests et c'est l'autre moitié qui se déverse. Elles suppriment les deux tests de bord (« i est-il encore dans L? ») du corps de boucle, au prix de deux cases supplémentaires.
#Invariant de la fusion
Invariant. Au début de chaque itération de la boucle pour k, le sous-tableau A[p..k−1] contient les k − p plus petits éléments de L[1..n₁+1] et R[1..n₂+1], en ordre trié. De plus, L[i] et R[j] sont les plus petits éléments de leurs tableaux respectifs qui n'ont pas encore été recopiés dans A.
- Initialisation. k = p, donc A[p..k−1] est vide et contient bien les 0 plus petits éléments. i = j = 1, donc L[1] et R[1] sont les plus petits éléments non recopiés de chaque tableau.
- Maintenance. Supposons L[i] ≤ R[j]. Alors L[i] est le plus petit élément non encore recopié de L ∪ R. Comme A[p..k−1] contient les k − p plus petits, écrire L[i] en A[k] fait que A[p..k] contient les k − p + 1 plus petits, en ordre trié (car L[i] ≥ A[k−1] par l'hypothèse). Incrémenter i rétablit la seconde partie de l'invariant. Le cas L[i] > R[j] est symétrique, avec R.
- Terminaison. À la sortie, k = r + 1: A[p..r] contient les r − p + 1 plus petits éléments de L ∪ R en ordre trié. Or L et R contiennent à eux deux exactement les éléments initialement dans A[p..r]. Donc A[p..r] est trié.
Coût de MERGE. Les lignes 1 à 6 prennent un temps constant; les deux copies prennent Θ(n₁ + n₂) = Θ(n) où n = r − p + 1. La boucle finale s'exécute exactement n fois, puisque chaque itération recopie un élément et que chaque élément est recopié une fois: au total Θ(n). Le coût de la fusion est donc linéaire en la taille du segment fusionné, quelle que soit la répartition des valeurs.
#Analyse du tri fusion
Récurrence. Les trois étapes de la méthode diviser pour régner donnent chacune leur terme.
- Diviser: calculer le milieu q coûte Θ(1).
- Régner: deux sous-problèmes de taille n/2, soit 2T(n/2).
- Combiner: une fusion sur n éléments, soit Θ(n).
T(n) = Θ(1) si n = 1, sinon T(n) = 2T(n/2) + Θ(n).
Théorème maître. Ici a = 2, b = 2, donc c = log₂ 2 = 1, et f(n) = Θ(n) = Θ(n^c): c'est le cas 2, d'où T(n) = Θ(n^c log n) = Θ(n log n).
Dérivation par arbre de récurrence. Pour n puissance de 2, notons cn le coût de découpe et de fusion au niveau supérieur:
- niveau 0: coût cn;
- niveau 1: 2 nœuds, chacun coûtant c(n/2), total 2 × cn/2 = cn;
- niveau j: 2^j nœuds, chacun coûtant c(n/2^j), total 2^j × cn/2^j = cn.
Chaque niveau coûte donc cn: doubler le nombre de nœuds divise exactement par deux le travail de chacun. Il y a lg n + 1 niveaux au total (du niveau 0 au niveau des feuilles), chaque niveau coûtant cn, soit un total de cn(lg n + 1) = cn lg n + cn = Θ(n log n). Le terme cn des feuilles correspond aux n sous-tableaux de taille 1, chacun résolu en temps constant.
Le point capital: aucun terme de cette somme ne dépend de l'ordre des valeurs. La structure des appels récursifs ne dépend que de n. C'est pourquoi le tri fusion est en Θ(n log n) dans tous les cas, meilleur, moyen et pire — contrairement au tri rapide.
Coûts: dans tous les cas (Master Theorem, cas 2): l'entrée n'a aucune influence sur la structure des appels. Espace: Θ(n) auxiliaire (segments temporaires). Stable (le <= de la fusion conserve l'ordre des égaux entre segments). Non en place au sens strict.
#Trace complète du tri fusion
Sur A = ⟨12, 3, 7, 9, 14, 6, 11, 2⟩, la descente coupe jusqu'aux singletons, puis la remontée fusionne:
| Étape | Segments fusionnés | Résultat |
|---|---|---|
| division | 12, 3, 7, 9, 14, 6, 11, 2 | 8 sous-tableaux de 1 élément |
| fusion 1 | ⟨12⟩ et ⟨3⟩ | ⟨3, 12⟩ |
| fusion 2 | ⟨7⟩ et ⟨9⟩ | ⟨7, 9⟩ |
| fusion 3 | ⟨14⟩ et ⟨6⟩ | ⟨6, 14⟩ |
| fusion 4 | ⟨11⟩ et ⟨2⟩ | ⟨2, 11⟩ |
| fusion 5 | ⟨3, 12⟩ et ⟨7, 9⟩ | ⟨3, 7, 9, 12⟩ |
| fusion 6 | ⟨6, 14⟩ et ⟨2, 11⟩ | ⟨2, 6, 11, 14⟩ |
| fusion 7 | ⟨3, 7, 9, 12⟩ et ⟨2, 6, 11, 14⟩ | ⟨2, 3, 6, 7, 9, 11, 12, 14⟩ |
Détail de la fusion 5, la première à comparer deux éléments de chaque côté: têtes 3 et 7 → 3 sort; têtes 12 et 7 → 7; 12 et 9 → 9; il ne reste que 12, déversé avec sa sentinelle. Détail de la fusion 7, la fusion finale: 3/2 → 2; 3/6 → 3; 7/6 → 6; 7/11 → 7; 9/11 → 9; 12/11 → 11; 12/14 → 12; reste 14. Vérifiez que l'ordre relatif des éléments égaux est préservé: c'est la seule chose que le choix de ≤ (et non <) dans la comparaison achète.
C'est le tri de la garantie: échéance stricte, données adverses, flux externes (tri externe sur disque: la fusion séquentielle s'adapte aux lectures séquentielles).
#Tri rapide (quick sort)
Invariant: après le partitionnement autour d'un pivot, le pivot est à sa place définitive, tout ce qui est plus petit à sa gauche, plus grand à sa droite.
- Choisir un pivot.
- Partitionner: répartir les éléments de part et d'autre.
- Trier récursivement les deux côtés.
Coûts: meilleur et moyen (partitions équilibrées, ); pire cas Θ(n²) quand le pivot est systématiquement extrémal (entrée déjà triée avec pivot au premier élément: ). Espace: O(log n) en moyenne (pile de récursion), O(n) au pire cas.
Parades au pire cas: pivot aléatoire (l'espérance du coût reste Θ(n log n) quelle que soit l'entrée: le hasard est côté de l'algorithme, l'adversaire ne peut plus viser le pivot), médiane de trois éléments, coupure vers le tri par insertion sous un petit seuil. En place, généralement le plus rapide en pratique sur tableaux en mémoire (excellentes constantes: accès séquentiels, peu de copies), mais non stable dans les versions en place.
#Tri par tas (heap sort)
- Construire un tas max en place dans le tableau: O(n) (voir le chapitre Arbres et tas).
- Répéter: échanger la racine (maximum) avec le dernier élément, réduire la taille logique du tas, faire descendre la nouvelle racine: O(log n) par extraction.
Le tableau se remplit du plus grand au plus petit, depuis la fin. Coût Θ(n log n) garanti, O(1) espace auxiliaire, toutes les opérations sur place. Non stable. C'est le tri de la garantie en place: il ne devance jamais le tri rapide en moyenne, mais il ne peut ni dégénérer comme lui ni consommer comme le tri fusion.
#Récapitulatif
| Algorithme | Moyen | Pire cas | Espace aux. | Stable | En place |
|---|---|---|---|---|---|
| Insertion | O(n²) | O(n²) | O(1) | oui | oui |
| Sélection | O(n²) | O(n²) | O(1) | non | oui |
| Bulles | O(n²) | O(n²) | O(1) | oui | oui |
| Fusion | O(n log n) | O(n log n) | O(n) | oui | non |
| Rapide | O(n log n) | O(n²) | O(log n) moy. | non | oui |
| Tas | O(n log n) | O(n log n) | O(1) | non | oui |
| Comptage | O(n + k) | O(n + k) | O(n + k) | oui | non |
Lecture: aucune ligne ne domine sur toutes les colonnes. Fusion est le choix de robustesse, rapide le choix de vitesse moyenne sous pivot aléatoire, tas le choix de la garantie en place, insertion le choix des petites tailles et des entrées presque triées.
#Tris non comparatifs
Ces tris échappent à la borne Ω(n log n) en exploitant la valeur des éléments, pas uniquement leur ordre. La contrepartie: chacun impose des conditions sur les données.
#Tri par comptage
Pour des entiers dans un intervalle [0, k]:
- Compter les occurrences de chaque valeur: O(n).
- Reconstruire le tableau trié en déroulant les comptes, ou par comptes cumulés pour le rang de chaque élément: O(k).
Le pseudocode du cours:
1COUNTING-SORT(A, n, k)21 soit B[1..n] le tableau de sortie et C[0..k] un tableau de compteurs32 pour i = 0 à k43 C[i] = 0 // aucun élément compté54 pour j = 1 à n65 C[A[j]] = C[A[j]] + 1 // C[i] = nombre d'éléments égaux à i76 // C[i] contient maintenant le nombre d'éléments égaux à i87 pour i = 1 à k98 C[i] = C[i] + C[i − 1] // C[i] = nombre d'éléments ≤ i109 // C[i] contient maintenant le nombre d'éléments inférieurs ou égaux à i1110 pour j = n à 1 // parcours ARRIÈRE: garantit la stabilité1211 B[C[A[j]]] = A[j]1312 C[A[j]] = C[A[j]] − 11413 renvoyer BTrois remarques sur le pseudocode, chacune porteuse d'un piège:
- Les compteurs sont indexés de 0 à k, donc k + 1 cases: oublier la case d'indice 0 est l'erreur classique quand on transpose le pseudocode dans un langage à indices commençant à 0.
- La ligne 8 transforme les effectifs en rangs cumulés. Avant,
C[i]dit combien d'éléments valent exactement i; après,C[i]dit combien valent au plus i, c'est-à-dire la position de sortie du dernier élément de valeur i. - La ligne 10 parcourt l'entrée à l'envers, et c'est ce qui rend le tri stable. Voir la démonstration plus bas.
#Coût
- Lignes 2 à 3: Θ(k).
- Lignes 4 à 5: Θ(n).
- Lignes 7 à 8: Θ(k).
- Lignes 10 à 12: Θ(n).
Total Θ(n + k), espace Θ(n + k) (les tableaux B et C). Quand k = O(n), le coût est Θ(n): c'est un tri linéaire. Le tri par comptage n'effectue aucune comparaison entre éléments d'entrée: il n'utilise que leur valeur comme indice. C'est précisément ce qui lui permet d'échapper au théorème de la borne Ω(n log n), qui ne vaut que pour les tris par comparaison.
En pratique, on emploie le tri par comptage quand k est du même ordre que n (trier les notes sur 100 d'un millier d'élèves: Θ(n + k) ≈ linéaire). Il devient absurde quand k explose: trier trois entiers de 64 bits par comptage demanderait 2⁶⁴ compteurs, soit plus de cases mémoire que d'atomes dans un grain de sable, pour trier trois nombres.
#Trace complète du tri par comptage
Sur A = ⟨2, 5, 3, 0, 2, 3, 0, 3⟩ (n = 8, k = 5):
| Étape | Contenu |
|---|---|
| après les comptages (lignes 4–5) | C = ⟨2, 0, 2, 3, 0, 1⟩ |
| après les rangs cumulés (lignes 7–8) | C = ⟨2, 2, 4, 7, 7, 8⟩ |
| j = 8: A[8] = 3, C[3] = 7 | B[7] = 3, C[3] → 6 |
| j = 7: A[7] = 0, C[0] = 2 | B[2] = 0, C[0] → 1 |
| j = 6: A[6] = 3, C[3] = 6 | B[6] = 3, C[3] → 5 |
| j = 5: A[5] = 2, C[2] = 4 | B[4] = 2, C[2] → 3 |
| j = 4: A[4] = 0, C[0] = 1 | B[1] = 0, C[0] → 0 |
| j = 3: A[3] = 3, C[3] = 5 | B[5] = 3, C[3] → 4 |
| j = 2: A[2] = 5, C[5] = 8 | B[8] = 5, C[5] → 7 |
| j = 1: A[1] = 2, C[2] = 3 | B[3] = 2, C[2] → 2 |
| résultat | B = ⟨0, 0, 2, 2, 3, 3, 3, 5⟩ |
Lecture des rangs cumulés: C[3] = 7 après la ligne 8 signifie « il y a 7 éléments inférieurs ou égaux à 3 ». Le dernier 3 de l'entrée doit donc aller en position 7. Les compteurs sont ensuite décrémentés, ce qui place chaque 3 suivant une case plus à gauche, en respectant l'ordre d'apparition initial.
#Invariant de la dernière boucle
Invariant. Au début de chaque itération de la boucle pour j, le dernier élément de A de valeur i qui n'a pas encore été recopié dans B doit être placé en B[C[i]].
- Initialisation. Avant la première itération (j = n), aucun élément n'a été recopié et C[i] vaut, pour chaque valeur i, le nombre d'éléments ≤ i, c'est-à-dire l'indice du dernier emplacement disponible pour la valeur i. L'élément A[n], de valeur v, doit donc aller en B[C[v]]: c'est exactement ce qu'écrit la ligne 11.
- Maintenance. Après avoir écrit A[j] en B[C[v]] et décrémenté C[v], les éléments de valeur v encore non recopiés sont exactement ceux d'indices < j, et C[v] pointe maintenant sur l'emplacement juste avant celui qu'on vient de remplir: l'invariant est conservé pour la valeur v. Pour les autres valeurs i ≠ v, ni les éléments concernés ni C[i] n'ont changé.
- Terminaison. À j = 0, tous les éléments ont été recopiés; l'invariant garantit qu'ils l'ont été dans l'ordre croissant des valeurs, et donc B est trié.
#Stabilité, démontrée
Le tri par comptage est stable: deux éléments de même valeur apparaissent dans B dans le même ordre relatif que dans A.
Preuve: considérons deux éléments x et y de valeur v, avec x apparaissant avant y dans A (positions j_x < j_y). Le parcours se fait de j = n vers 1, donc y est traité avant x. À ce moment, C[v] vaut le nombre d'éléments ≤ v, c'est-à-dire la position de sortie du dernier élément de valeur v. y est donc écrit le plus à droite de la plage réservée aux v, puis C[v] est décrémenté. Quand x est traité à son tour, C[v] a diminué exactement du nombre d'éléments de valeur v traités entre-temps, dont y: x est donc écrit immédiatement à gauche de y. L'ordre initial est préservé.
Conséquence: remplacer le parcours arrière par un parcours avant (pour j = 1 à n) laisse l'algorithme correct — les mêmes éléments sont écrits aux mêmes positions, dans un ordre différent — mais détruit la stabilité: le premier élément de valeur v rencontré irait le plus à droite. C'est un cas rare d'algorithme à la fois correct et non stable selon l'ordre de parcours d'une boucle.
La stabilité n'est pas un luxe ici: c'est elle qui rend le tri par base correct (section suivante), puisque chaque passe doit préserver le classement établi par la précédente.
#Variantes utiles
- Tri en place avec deux tableaux. Si le tableau ne porte aucune donnée satellite (on ne trie que des clés), on peut se passer de B et recopier le résultat dans A, en utilisant C comme compteur d'écriture. L'algorithme reste linéaire mais n'est plus stable: l'information d'ordre d'apparition est perdue.
- Compter sur une plage. Pour répondre à « combien d'entiers de l'entrée tombent dans l'intervalle [a, b]? », il suffit des rangs cumulés: la réponse est
C[b] − C[a − 1]. Après un prétraitement en Θ(n + k), chaque requête coûte Θ(1), ce qui est le bon compromis quand les requêtes sont nombreuses et l'entrée figée. - Clés à partie fractionnaire. Si chaque clé a au plus d chiffres décimaux après la virgule, on multiplie tout par 10^d et on compte sur les entiers obtenus: Θ(n + 10^d).
#Tri par base (radix sort)
Trier les éléments par chiffre successif, du chiffre le moins significatif vers le plus significatif, avec un tri stable (comptage) à chaque passe. La stabilité est l'argument clé: le classement établi par une passe n'est pas détruit par la suivante, car les chiffres plus significatifs prennent le relais uniquement quand ils diffèrent.
1RADIX-SORT(A, n, d)21 pour i = 1 à d32 utiliser un tri stable pour trier A sur le chiffre iLe pseudocode tient en deux lignes parce que toute la difficulté est dans le choix du tri stable. En pratique on emploie COUNTING-SORT, dont l'alphabet est la base choisie.
Lemme. Étant donné n nombres à d chiffres, chaque chiffre prenant au plus k valeurs possibles, RADIX-SORT trie correctement ces nombres en Θ(d(n + k)) si le tri stable utilisé coûte Θ(n + k).
Preuve de correction. Par récurrence sur la passe i. Après la passe i, les nombres sont triés selon leurs i chiffres de poids faible. Cas de base (i = 0): aucun chiffre traité, la propriété est vide. Étape: supposons les nombres triés sur les i − 1 chiffres de poids faible avant la passe i; le tri stable sur le chiffre i place les nombres par ordre du chiffre i, et, à chiffre i égal, conserve l'ordre établi par la passe précédente (c'est exactement la stabilité). Or deux nombres qui ont le même chiffre i se comparent par leurs chiffres de poids faible: l'ordre hérité est donc le bon. Ceux qui diffèrent sur le chiffre i sont ordonnés par lui, ce qui est correct puisque c'est le chiffre le plus significatif parmi les i traités. Après d passes, les nombres sont triés sur les d chiffres, c'est-à-dire complètement.
Preuve du coût. Chaque passe est un tri par comptage sur n éléments et un alphabet de k valeurs: Θ(n + k). Il y a d passes, d'où Θ(d(n + k)). Quand d est constant et k = O(n), le coût total est Θ(n): radix sort est linéaire.
Trace. Sur les sept nombres à trois chiffres décimaux 329, 457, 657, 839, 436, 720, 355:
| Passe | Clé de tri | Résultat |
|---|---|---|
| entrée | — | 329, 457, 657, 839, 436, 720, 355 |
| 1 | chiffre des unités | 720, 355, 436, 457, 657, 329, 839 |
| 2 | chiffre des dizaines | 720, 329, 436, 839, 355, 457, 657 |
| 3 | chiffre des centaines | 329, 355, 436, 457, 657, 720, 839 |
Détail de la passe 1 (unités): 720 (0), 355 (5), 436 (6), 457 (7), 657 (7), 329 (9), 839 (9). Les deux nombres d'unité 7 sortent dans leur ordre d'entrée (457 avant 657) et les deux d'unité 9 également (329 avant 839): c'est la stabilité à l'œuvre. Passe 2 (dizaines) sur cette liste: 720 et 329 ont 2, 436 et 839 ont 3, 355, 457 et 657 ont 5 — les groupes sortent dans l'ordre 2, 3, 5 et, à l'intérieur de chaque groupe, dans l'ordre hérité de la passe 1, ce qui met 329 après 720 (dizaine 2) puis 436 avant 839 (dizaine 3). Passe 3 (centaines): 329 et 355 (3), 436 et 457 (4), 657 (6), 720 (7), 839 (8). Le résultat final est trié.
Généralisation aux clés binaires. Pour des nombres de b bits et un entier r ≤ b, on découpe chaque clé en ⌈b/r⌉ chiffres de r bits, chacun valant entre 0 et 2^r − 1. Chaque passe coûte alors Θ(n + 2^r) et le total vaut Θ((b/r)(n + 2^r)). Si b = O(lg n), le meilleur choix est r = b, une seule passe en Θ(n). Si b > lg n, le r optimal vaut lg n, ce qui donne Θ(bn / lg n). Sur des entiers 32 bits on peut par exemple prendre r = 8 (quatre passes sur des chiffres d'un octet), choix courant parce qu'il rend le tableau de comptage minuscule et tient dans le cache.
Radix sort contre tri rapide. Si b = O(lg n) et r = lg n, radix sort est en Θ(n), apparemment meilleur que le Θ(n log n) attendu du tri rapide. Les constantes cachées renversent souvent la conclusion: chaque passe de radix sort balaie les clés en entier, alors qu'une comparaison de tri rapide ne touche qu'un mot. Sur une machine réelle, le tri rapide exploite mieux le cache et gagne fréquemment sur des clés de 32 ou 64 bits; radix sort reprend l'avantage sur des clés longues et de structure régulière (chaînes, IP, horodatages), et il n'est pas en place.
Pour d chiffres en base k: coût O(d(n + k)). Pour des entiers machine sur 64 bits en base 2¹⁶: d = 4 passes, O(n) en pratique. Conditions: clé décomposable en chiffres, alphabet fini.
#Tri par paniers (bucket sort)
Distribuer les éléments dans p paniers selon des bornes régulières, trier chaque panier (souvent par insertion), concaténer. Sous hypothèse d'une distribution à peu près uniforme dans l'intervalle des bornes, l'effectif moyen par panier est n/p: coût moyen O(n) avec p bien choisi. Le pire cas reste O(n²) (tous les éléments dans un panier). Sensible à la distribution: à réserver aux données qui la justifient.
#Exercice 1: le tri fusion est stable
Prouvez que le tri fusion tel qu'implémenté ci-dessus est stable.
Correction: la stabilité se joue uniquement dans la fusion. Quand les deux têtes sont égales, g[i] <= d[j] retient g[i], l'élément de gauche. Or, tout élément du segment gauche précède tout élément du segment droit dans l'ordre initial (la coupe est en deux moitiés contiguës, et les récursions trient sans réordonner entre segments). Donc pour deux éléments égaux, celui apparu en premier dans l'entrée est extrait le premier à chaque fusion, et par récursion sur les niveaux de fusion, l'ordre relatif initial est préservé jusqu'au résultat final. (Avec un < strict au lieu du <=, l'élément de droite sortirait le premier: la stabilité serait perdue, sans rien changer au coût.)
#Exercice 2: compter pour distinguer correct et rapide
Instrumentez le tri par sélection et le tri par fusion avec un compteur de comparaisons, et mesurez sur trois entrées de taille 2000: (a) triée, (b) triée à l'envers, (c) aléatoire.
Correction attendue (à confronter à vos mesures):
| Entrée (n = 2000) | Sélection | Fusion |
|---|---|---|
| Triée | 1 999 000 | ≈ 2000 × 11 ≈ 22 000 |
| Inversée | 1 999 000 | ≈ 22 000 |
| Aléatoire | 1 999 000 | ≈ 22 000 |
Le tri par sélection est un vrai tri (les trois sorties sont correctes, vérifiables avec sorted), mais insensible au contenu: exactement n(n−1)/2 comparaisons partout, 1 999 000 pour n = 2000. Le tri fusion: environ n log₂ n comparaisons, du même ordre sur les trois entrées, conformément au Θ(n log n) garanti. Ce que l'exercice enseigne: les deux tris sont corrects sur toute entrée, leurs courbes de flux de travail diffèrent radicalement; la correction se vérifie sur les sorties (les trois cases « trié: True »), la performance se vérifie sur les comptes, et aucune des deux preuves ne remplace l'autre.
#Exercice 3: dérouler le tri par insertion
Illustrez l'exécution de INSERTION-SORT sur A = ⟨31, 41, 59, 26, 41, 58⟩. Donnez l'état du tableau après chaque itération de la boucle pour et le nombre total de décalages.
Correction:
| i | clé | Travail de la boucle tant que | Tableau après l'itération |
|---|---|---|---|
| 2 | 41 | 31 ≤ 41: arrêt immédiat | 31 41 59 26 41 58 |
| 3 | 59 | 41 ≤ 59: arrêt immédiat | 31 41 59 26 41 58 |
| 4 | 26 | 59, 41, 31 > 26: trois décalages | 26 31 41 59 41 58 |
| 5 | 41 | 59 > 41: un décalage; 41 ≤ 41: arrêt | 26 31 41 41 59 58 |
| 6 | 58 | 59 > 58: un décalage | 26 31 41 41 58 59 |
Total: 5 décalages, à comparer aux 15 du pire cas (n = 6, n(n−1)/2) et aux 5 du meilleur cas. Point à ne pas manquer à l'itération i = 5: la comparaison est A[j] > clé (strictement), donc le 41 déjà en place ne se décale pas et le nouveau 41 s'insère à sa droite. C'est exactement cette stricte inégalité qui rend le tri stable.
#Exercice 4: dérouler le tri fusion
Illustrez l'exécution de MERGE-SORT sur A = ⟨3, 41, 52, 26, 38, 57, 9, 49⟩: donnez la séquence des divisions et des fusions.
Correction:
| Étape | Opération | Résultat |
|---|---|---|
| division | 8 singletons | ⟨3⟩ ⟨41⟩ ⟨52⟩ ⟨26⟩ ⟨38⟩ ⟨57⟩ ⟨9⟩ ⟨49⟩ |
| fusion | ⟨3⟩ + ⟨41⟩ | ⟨3, 41⟩ |
| fusion | ⟨52⟩ + ⟨26⟩ | ⟨26, 52⟩ |
| fusion | ⟨3, 41⟩ + ⟨26, 52⟩ | ⟨3, 26, 41, 52⟩ |
| fusion | ⟨38⟩ + ⟨57⟩ | ⟨38, 57⟩ |
| fusion | ⟨9⟩ + ⟨49⟩ | ⟨9, 49⟩ |
| fusion | ⟨38, 57⟩ + ⟨9, 49⟩ | ⟨9, 38, 49, 57⟩ |
| fusion | ⟨3, 26, 41, 52⟩ + ⟨9, 38, 49, 57⟩ | ⟨3, 9, 26, 38, 41, 49, 52, 57⟩ |
Détail de la fusion finale, élément par élément: têtes 3 et 9 → 3; 26 et 9 → 9; 26 et 38 → 26; 41 et 38 → 38; 41 et 49 → 41; 52 et 49 → 49; 52 et 57 → 52; il ne reste que 57, déversé. Huit éléments triés en 7 comparaisons. Ce nombre n'est pas un minimum absolu: fusionner deux suites triées de 4 éléments demande au moins 4 comparaisons (cas où toute une moitié précède l'autre) et au plus 7 (les deux suites s'entrelacent complètement, comme ici). Le nombre total de comparaisons de l'algorithme est quant à lui toujours compris entre environ (n/2)·lg n et n·lg n − n + 1, quel que soit l'ordre de l'entrée.
#Exercice 5: dérouler le tri par comptage
Illustrez COUNTING-SORT sur A = ⟨6, 0, 2, 0, 1, 3, 4, 6, 1, 3, 2⟩ avec k = 6. Donnez C après chaque boucle, puis B.
Correction (n = 11, k = 6):
| Étape | Contenu |
|---|---|
| effectifs (lignes 4–5) | C = ⟨2, 2, 2, 2, 1, 0, 2⟩ |
| rangs cumulés (lignes 7–8) | C = ⟨2, 4, 6, 8, 9, 9, 11⟩ |
| placements (parcours arrière) | B[6]=2, B[8]=3, B[4]=1, B[11]=6, B[9]=4, B[7]=3, B[3]=1, B[2]=0, B[5]=2, B[1]=0, B[10]=6 |
| résultat | B = ⟨0, 0, 1, 1, 2, 2, 3, 3, 4, 6, 6⟩ |
Vérification de la stabilité sur cet exemple: les deux 6 de A occupent les positions 1 et 8; ils sortent en positions 10 et 11, dans cet ordre. Les deux 3 occupent les positions 6 et 10; ils sortent en 7 et 8, dans cet ordre. Un parcours avant au lieu du parcours arrière aurait inversé chacun de ces couples.
#Exercice 6: le tri par insertion, version récursive
Écrivez une version récursive du tri par insertion: pour trier A[1..n], on trie récursivement A[1..n−1], puis on insère A[n] à sa place dans la zone triée. Donnez la récurrence du coût dans le pire cas et sa solution.
Correction:
1INSERTION-SORT-REC(A, n)21 si n > 132 INSERTION-SORT-REC(A, n − 1) // A[1..n−1] est maintenant trié43 clé = A[n]54 j = n − 165 tant que j > 0 et A[j] > clé76 A[j+1] = A[j]87 j = j − 198 A[j+1] = cléLa correction découle directement de l'invariant: après la ligne 2, A[1..n−1] est trié et contient les éléments initiaux de A[1..n−1]; les lignes 3 à 8 insèrent A[n] dans cette zone triée, comme dans la version itérative. Récurrence du pire cas: T(n) = T(n−1) + Θ(n), car l'insertion peut décaler jusqu'à n − 1 éléments. En déroulant, T(n) = Θ(n) + Θ(n−1) + … + Θ(1) = Θ(n²). La récursion ne change rien au coût: elle ne fait que déplacer la boucle pour dans la pile d'appels, et elle consomme Θ(n) d'espace de pile au lieu de O(1). Leçon: transformer une boucle en récursion n'améliore pas la complexité, et coûte de la mémoire.
#Exercice 7: recherche binaire dans le tri par insertion
Écrivez la recherche binaire, montrez qu'elle coûte Θ(lg n), puis répondez: remplacer la recherche linéaire de la boucle tant que du tri par insertion par une recherche binaire améliore-t-il le tri au pire cas jusqu'à Θ(n log n)?
Correction. Recherche binaire:
1RECHERCHE-BINAIRE(A, p, r, x)21 si p > r alors renvoyer NIL32 q = ⌊(p + r) / 2⌋43 si A[q] == x alors renvoyer q54 si A[q] < x alors renvoyer RECHERCHE-BINAIRE(A, q+1, r, x)65 sinon renvoyer RECHERCHE-BINAIRE(A, p, q−1, x)Chaque appel élimine la moitié du tableau: T(n) = T(n/2) + Θ(1), donc T(n) = Θ(lg n) par le théorème maître (cas 2). La recherche binaire est correcte parce que le tableau est trié, ce qui permet de savoir de quel côté de A[q] se trouve x.
Réponse à la question: non. La recherche binaire fait tomber le nombre de comparaisons à Θ(n lg n) — chaque insertion coûte lg i au lieu de i — mais elle ne touche pas au nombre de décalages, qui reste Θ(n²) dans le pire cas: après avoir trouvé la position en Θ(lg n), il faut quand même décaler jusqu'à i − 1 éléments pour libérer la case. Le coût total est donc Θ(n lg n) + Θ(n²) = Θ(n²), inchangé. Le tri par insertion paie les déplacements, pas les comparaisons; seule une structure qui rend le déplacement local bon marché (liste chaînée, arbre) attaquerait ce terme — et elle introduirait un coût de parcours équivalent. C'est le tri fusion, avec sa copie linéaire par niveau, qui franchit réellement la barre.
#Exercice 8: deux éléments de somme donnée en Θ(n log n)
Étant donné un ensemble S de n entiers et un entier x, décidez s'il existe deux éléments de S dont la somme vaut exactement x. Votre algorithme doit coûter Θ(n log n) dans le pire cas.
Correction:
1DEUX-SOMMES(S, x)21 T = MERGE-SORT(S) // Θ(n log n)32 pour chaque a de T // n itérations43 si RECHERCHE-BINAIRE(T, x − a) ≠ NIL54 renvoyer VRAI // Θ(lg n) par recherche65 renvoyer FAUXCoût: Θ(n log n) pour le tri, puis n recherches binaires en Θ(lg n) chacune, soit Θ(n lg n). Total Θ(n log n). Correction: si a + b = x avec a, b ∈ S, alors en parcourant a on finit par tester x − a = b, présent dans T (trié, donc la recherche binaire le trouve); inversement, tout succès de la ligne 3 exhibe un couple valide. Attention au cas a = b: le couple (a, a) n'est valide que si a apparaît deux fois dans S; avec la version « ensemble » (sans doublon), il faut exclure l'élément lui-même, ce que la recherche binaire ne fait pas seule — détail à traiter explicitement.
#Exercice 9: corriger le tri à bulles
Le tri à bulles classique est
1BUBBLESORT(A, n)21 pour i = 1 à n − 132 pour j = n à i + 143 si A[j] < A[j − 1]54 échanger A[j] et A[j − 1]Énoncez les deux invariants (boucle interne, boucle externe) et prouvez-les. Quelle est la complexité dans le pire cas? Que manque-t-il à cette version pour avoir un meilleur cas linéaire?
Correction. Ce qu'il faut prouver pour la correction: (i) l'inégalité A′[1] ≤ A′[2] ≤ … ≤ A′[n] sur la sortie, et (ii) que A′ est une permutation de A — c'est-à-dire que l'algorithme ne perd ni ne duplique aucun élément. L'échange mutuel de deux cases préserve le multiensemble, donc (ii) est immédiat; tout le travail est dans (i).
Invariant de la boucle interne (lignes 2–4): au début de chaque itération d'indice j, A[j] est le minimum de A[j..n].
- Initialisation: j = n, et A[n] est le minimum de A[n..n].
- Maintenance: par hypothèse A[j] = min(A[j..n]). Le corps compare A[j] et A[j−1] et les échange si A[j] < A[j−1]; après le corps, A[j−1] = min(A[j−1], A[j]) = min(A[j−1..n]). Décrémenter j place cette valeur en position j−1, ce qui rétablit l'invariant pour le nouvel indice.
- Terminaison: à j = i, on a A[i] = min(A[i..n]).
Invariant de la boucle externe (lignes 1–4): au début de chaque itération d'indice i, le sous-tableau A[1..i−1] est trié et contient les i − 1 plus petits éléments de A.
- Initialisation: i = 1, le sous-tableau A[1..0] est vide.
- Maintenance: par la terminaison de la boucle interne, A[i] = min(A[i..n]). Comme A[1..i−1] contient déjà les i − 1 plus petits éléments, tous ≤ A[i], le sous-tableau A[1..i] est trié et contient les i plus petits éléments.
- Terminaison: à i = n, A[1..n−1] est trié et contient les n − 1 plus petits éléments; le dernier est donc en place et A est trié.
Complexité. Les deux boucles sont imbriquées sans dépendre du contenu: exactement n(n−1)/2 comparaisons, soit Θ(n²) dans tous les cas. Ce qui manque: un drapeau d'échange. En sortant de la boucle externe dès qu'une passe complète n'a provoqué aucun échange, on obtient Θ(n) sur une entrée déjà triée. La version ci-dessus, telle quelle, reste quadratique même sur une entrée triée — c'est la variante avec détection d'arrêt qui a un meilleur cas linéaire. Même améliorée, elle effectue davantage d'échanges que le tri par insertion sur le même profil d'entrée: sa valeur est pédagogique, elle matérialise l'invariant, pas pratique.