L'algorithme du tri rapide est très certainement l'un des algorithmes les plus célèbres et les plus étudiés. Il a été conçu en 1961 par l'informaticien britannique Charles Hoare qui est également à l'origine, entre autres, de la logique qui porte son nom et qui est utilisée en preuve de programmes (cours de troisième année). Paradoxalement la complexité dans le pire des cas de ce tri n'est pas linéarithmique mais quadratique. Cependant, les cas défavorables sont peu fréquents et sa complexité moyenne est linéarithmique avec un coefficient caché meilleur que ceux des autres algorithmes de tri dans la même classe de complexité.
L'algorithme présenté ici est la version originelle dans laquelle le pivot est fixé à la première valeur. Il existe désormais de nombreuses déclinaisons de cet algorithme avec les corrections nécessaires pour que la complexité reste linéarithmique dans le pire des cas.
À l'instar du tri fusion, le tri rapide est un tri qui utilise le principe diviser pour régner pour trier les \(n\) entiers d'une liste indexée de \(1\) à \(n\). À chaque étape, la sous-liste \(L[p\!:\!r]\) est partitionnée en deux sous-listes gauche \(L[p\!:\!q]\) et droite \(L[q+1\!:\!r],\) mais cette fois la césure en \(q\) ne se fait plus nécessairement au milieu \((p+r)/2\) de la liste. Il en résulte un déséquilibre possible entre les tailles des deux sous-listes et si ce choix peut paraître étrange a priori, il se fait au bénéfice d'une propriété obtenue après le partitionnement et qui rend inutile l'opération de fusionnement au retour des appels récursifs : les éléments de la sous-liste \(L[p\!:\!q]\) sont tous inférieurs à tous les éléments de la sous-liste droite \(L[q+1\!:\!r],\) i.e.
\begin{equation} \label{eq:proptr} \forall i\in\ab{p}{q}\ \ \forall j\in\ab{q+1}{r}\quad L[i] \leqslant L[j], \end{equation}que l'on résume par
\begin{equation*} L[p\!:\!q]\leqslant L[q+1\!:\!r]. \end{equation*}Comme le tri se fait sur place, s'il opère récursivement sur ces deux sous-listes, il est clair que le simple retour des appels récursifs suffit à achever le travail. La propriété \((\ref{eq:proptr})\) est obtenue en échangeant des valeurs dans la liste pendant la phase de partitionnement, cette phase ne se limite donc pas à déterminer la position \(q\) de la césure. L'algorithme du tri rapide se résume donc essentiellement aux manipulations réalisées pour obtenir le partionnement de la liste \(L\) qui fixe la valeur \(q\).
Le principe du partitionnement est relativement simple, il consiste à ranger les éléments de la sous-liste \(L[p\!:\!r]\) inférieurs ou égaux à une valeur \(x\) appelée pivot dans la partie gauche et les éléments supérieurs ou égaux — et on s'attend à un inférieur strict ici, mais il s'agit bien à nouveau d'une inégalité large — dans la partie droite. Le choix de la valeur pivot a fait l'objet de nombreuses études et de déclinaisons de l'algorithme mais nous étudierons ici la version originale dans laquelle la valeur pivot est fixée à \(L[p],\) soit à la valeur contenue dans la première cellule de la sous-liste à partitionner. La valeur de césure \(q\) n'est obtenue qu'après ce travail.
ALGORITHME Tri-Rapide(@L,p,r)
DONNEES
L: liste de valeurs
p, r: entiers
VARIABLES
q: entier
DEBUT
SI (p < r)
q ← Partitionner(L,p,r)
Tri-Rapide(L,p,q)
Tri-Rapide(L,q + 1,r)
FSI
FIN
La structure de l'algorithme principal récursif est donc très similaire de celle du tri fusion avec l'absence d'appel à un algorithme de fusionnement rendu inutile grâce à la propriété \((\ref{eq:proptr})\).
La partition \(\ab{p}{r}=\ab{p}{q}\sqcup\ab{q+1}{r}\) est construite en fonction du pivot \(x:=L[p]\) en faisant croître les deux régions gauche \(\ab{p}{\color{#FF8}i}\) et droite \(\ab{\color{#F44}j}{r}\) initialement vides (au départ \({\color{#FF8}i}\leftarrow p,\) \({\color{#F44}j}\leftarrow r\)) en incrémentant et en décrémentant respectivement les indices \(i\) et \(j\) jusqu'au point de rencontre qui est précisément le point de césure \(q\) de l'intervalle. On peut visualiser le processus en imaginant que \(i\) et \(j\) sont deux sentinelles qui partent des extrémités opposées de la sous-liste \(L[p\!:\!r]\) et parcourent ses éléments en s'échangeant les valeurs rencontrées à chaque fois qu'elles ne sont pas du bon côté par rapport à la valeur pivot \(x\).
À chaque étape, exceptée la dernière, les deux régions \(\ab{p}{\color{#FF8}i}\) et \(\ab{\color{#F44}j}{r}\) sont telles que les éléments de \(L[p\!:\!i]\) sont tous inférieurs ou égaux au pivot \(x\) et ceux de \(L[j\!:\!r]\) sont tous supérieurs ou égaux au pivot \(x\). Pour mieux mettre en évidence le fonctionnement de l'algorithme on s'appuie sur deux algorithmes auxiliaires pour déplacer les sentinelles. L'initialisation de \(j\) à \(r+1\) au lieu de \(r\) à la ligne #10 tient au fait qu'elle est suivie à la ligne #11 par un appel à l'algorithme Reculer qui la décrémente une première fois, la sentinelle droite part bien de la fin de la sous-liste considérée en \(r.\)
ALGORITHME Reculer(L,x,@j) ALGORITHME Avancer(L,x,@i)
DONNEES DONNEES
L: liste de valeurs L: liste de valeurs
x: valeur x: valeur
j: entier i: entier
DEBUT DEBUT
j ← j - 1 i ← i + 1
TQ (L[j] > x) FAIRE TQ (L[i] < x) FAIRE
j ← j - 1 i ← i + 1
FTQ FTQ
FIN FIN
ALGORITHME Partitionner(@L,p,r):entier
DONNEES
L: liste de valeurs
p,r: entiers
VARIABLES
i,j,x: entiers
DEBUT
x ← L[p]
i ← p
j ← r + 1
Reculer(L,x,j)
TQ (i < j) FAIRE
Echanger(L,i,j)
Reculer(L,x,j)
Avancer(L,x,i)
FTQ
RENVOYER(j)
FIN