[Plan du site]
Vous êtes ici ---
> Le Site du Zéro
> Les tutoriels
> Non-Officiels
> Programmation
> Algorithmique
> Le tri rapide : QSort
> Lecture du tutoriel
Le tri rapide : QSort
Vous vous apprêtez à lire un tutoriel rédigé par un membre de ce site. Malgré tout le soin que ce membre a pu apporter au tutoriel, nous ne pouvons pas garantir que les informations contenues sur cette page sont exactes à 100%. Merci de garder cela en tête lorsque vous lirez cette page ;o)
Salut à tous !
Aujourd'hui nous allons apprendre une nouvelle sorte de tri : le tri rapide, aussi appelé QSort (QuickSort = tri rapide en anglais). Le principe est assez original et donne de bons résultats sur les listes très désordonnées. Par contre, sur une liste presque déjà triée, il faudra trouver autre chose...
Dans tous les cas, je vous propose de découvrir ce nouvel algorithme sans plus tarder !
Bon, alors cet algorithme a un principe un peu original si vous êtes habitués au tri à bulles ou au tri par sélection. En fait, l'idée, c'est de séparer votre tableau en deux. Pour cela, on choisit une valeur de notre tableau de base, qu'on appelle
pivot. Le pivot est souvent la valeur de la première case du tableau. On construit alors deux « sous-tableaux » : l'un contient toutes les valeurs du premier tableaux qui sont
inférieures ou égales au pivot, l'autre contient les valeurs
supérieures au pivot.
Oui mais mon tableau n'est pas trié là ?
Non, il ne l'est pas. Mais il est déja un peu plus « rangé ». Afin de terminer le tri, il vous suffira de
retrier chacun des deux sous-tableaux avec qsort, puis de
concaténer les deux tableaux (cela signifie les mettre bout à bout, comme strcat le fait avec les chaînes de caractères en C).
Comme vous pouvez le voir, l'algorithme du tri rapide s'utilise lui même. On dit que c'est un algorithme
récursif (Bluestorm a écrit un
tutoriel sur la récursivité pour ceux que ça intéresse). Le dernier problème est donc de savoir quand-est ce qu'on arrête d'appeller récursivement QSort... La réponse est très simple : quand il n'y a plus rien à trier (c'est à dire, quand les sous-tableaux restants sont soit des
singletons, soit vides) !
Un petit exemple ?
Prenons cette liste :
Code : Console
Ici, je vais choisir comme pivot 37. En effet, il y a deux valeurs au dessus (38 et 42) et deux valeurs au dessous (5 et 21). C'est donc une valeur médiane, ce qui convient très bien pour un pivot (on verra que le principal problème de l'implémentation de quicksort est le choix du pivot). On prend donc la case 37 comme pivot et on fait passer de l'autre côté du pivot les valeurs qui ne sont pas à leur place :
Code : Console | 21 5 37 42 38
-------------
<37 || >37 |
Ici, on voit la liste initiale séparée en deux : au milieu, le pivot, 37. A gauche, les valeurs inférieures à 37 (<37) ont été insérées. De même à droite pour les valeurs supérieures à 37 (>37).
Bon, maintenant, on est sûrs que le 37 est à sa position définitive : il ne bougera plus. En effet, toutes les valeurs qui lui sont inférieures sont à sa gauche, et les autres sont à droite.
Vous pouvez donc recommencer à appliquer le tri rapide pour les deux sous-listes restantes : (21, 5) et (42, 38). Cependant, dites vous bien que dans un certain nombre de cas, quand on arrive à des sous-listes de cette taille, elles sont déjà « naturellement » triées (ceci dépend de l'implementation). Pour tout vous dire, j'en ai bavé pour trouver un exemple de liste qui ne soit pas trié immédiatement

.
Code : Console | 21 5 * 37 * 42 38
----------------------------
<21 || >21 ****** <42 || >42 |
Ce schéma montre les deux quickSorts appliqués chacun à une des sous-listes. On voit ici ces deux quickSorts effectués simultanément, juste après le choix du pivot, et avant le déplacement des valeurs de part et d'autres de ceux-ci.
Comme vous le voyez, on trie les deux listes complètement indépendamment. Bon, ici je me force à choisir un pivot et tout, mais il existe des variantes de QSort qui, à un certain stade, changent d'algorithme pour trier les sous-listes qui restent.
Une fois qu'on a fini le tri des deux sous-listes restantes, on obtient donc ceci :
Code : Console | 5 21 * 37 * 38 42
----------------------------
<21 || >21 ****** <42 || >42 |
Les deux quickSorts précédents, après l'étape de déplacement des valeurs. On remarque que les groupes de nombres entre les pivots sont soit des singletons, soit vides. QuickSort est donc terminé.
Nous sommes sûrs que les nombres 21, 37 et 38 sont à leur place définitive. En l'occurrence, il se trouve que la liste est triée. En effet, les sous-listes qui restent ne font qu'un nombre de long. On arrête donc de trier récursivement...
Voilà, vous avez saisi l'astuce ?
Oui, mais ton algorithme va impliquer de créer plein de sous-tableaux, et de sous-sous tableaux, de les parcourir... T'es sûr que ça va être si rapide que ça ? Et comment on va bien pouvoir l'implémenter ?
Il existe une implémentation du tri rapide qui est
en place (c'est à dire qu'elle n'utilise aucune mémoire supplémentaire, en dehors de celle du tableau initial). Elle élimine donc pas mal de problèmes d'utilisation de mémoire et de puissance de calcul.
Cependant, nous verrons d'abord comment implémenter une version « simple » de Qsort. Tout ça se passe dans la sous-partie suivante... En avant

!
Bon allez, implémentons tout ça.
QSort simple en OCaml
En premier lieu, nous allons implémenter un QSort qui obéit à la lettre à la définition de la sous-partie 1. Comme on l'a vu, cette implémentation risque d'être assez lourde à gérer à cause de tous les sous-tableaux. Pourtant, il est possible d'exprimer quicksort de façon très simple dans certains langages. Observons par exemple cette implémentation en OCaml par
Bluestorm (si vous ne conaissez rien au OCaml, voyez
par là,
là, et sur google).
Code : OCaml1
2
3
4
5 | let rec qsort liste = match liste with
[] | _::[] -> liste
| pivot::reste ->
let debut, fin = List.partition (fun a -> a < pivot) reste in
qsort debut @ [pivot] @ qsort fin
|
Ce code commence par déclarer la fonction
qsort, récursive, qui prend un paramètre (
liste), et qui regarde
quelle est la structure de la liste (c'est le
match liste with). Il y a alors plusieurs cas possibles :
- Si c'est une liste vide ou à un seul élément, alors on retourne la liste elle même
- Sinon, on appelle pivot le 1er élément de la liste, et reste le reste ;
puis on partitionne le reste en deux listes, début et fin, en utilisant l'expression a < pivot pour décider dans quelle liste placer chaque élément a ;
et enfin, on met bout à bout (c'est l'opérateur @) debut, le pivot, et fin, en appelant qsort sur debut et fin avant.
On voit donc que l'algorithme du tri rapide s'exprime de manière très simple et concise en OCaml. Ce code est sans doute « traduisible » en C, mais la partie « partition de la liste » risque d'être bien lourde à écrire. Il doit bien exister une autre solution pour les langages impératifs...
Une autre implémentation, en C
L'idée est extrêmement simple. Plutôt que de créer un nouveau tableau à chaque fois pour appeller récursivement qsort dessus, nous allons travailler sur un seul et unique tableau : celui qu'on nous fournit au départ.
Comment faire ?
Voici l'astuce : nous allons faire une fonction quickSort() qui prend trois paramètres, comme ceci :
Code : C1 | void quickSort(int tab[], int debut, int fin);
|
Le premier est bien évidemment le tableau à trier. Quant au second et au troisième, ce sont des
indices (c'est à dire des numéros de cases). La fonction quickSort se chargera donc de trier le tableau entre la case numéro
debut et la case numéro
fin ! Pour appeler récursivement
quickSort par la suite, il suffira de la rappeller avec le même tableau et les
debut/
fin qui vont bien. Plus besoin de créer des sous-tableaux !
Le seul obstacle qu'il nous reste : comment se débrouiller pour faire passer toutes les valeurs inférieures au pivot à sa gauche, et toutes les valeurs supérieures à sa droite ? Déjà, mettons nous d'accord tout de suite : dans un but de simplification,
nous prendrons comme pivot la première case du tableau.
Ensuite, nous allons faire deux
for. L'un d'entre eux parcourra le tableau de gauche à droite à la recherche d'un élément supérieur au pivot dans la partie gauche du tableau (qui n'est donc pas à sa place) ; l'autre parcourra le tableau de droite à gauche à la recherche d'un élément inférieur au pivot dans la partie droite du tableau.
Dès que les deux
for ont chacun trouvé un élément, on permute ces deux éléments. Puis on recommence, jusqu'à ce que les for se « croisent » (l'indice du premier for est supérieur à celui du second). À ce moment, le tableau est prêt : on peut mettre le pivot à sa place définitive, puis appeller récursivement quickSort (en se servant des indices des for pour trouver les nouveaux débuts/fin).
Compliqué ? Je vous l'accorde. Voici l'implémentation. Je vous recommande de la faire tourner avec différents tableaux, en y rajoutant des printf un peu partout pour comprendre ce qui se passe.
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | void permuter(int tab[], int i, int j)
{
int temp = tab[i];
tab[i] = tab[j];
tab[j] = temp;
}
void quickSort(int tab[], int debut, int fin)
{
int gauche = debut;
int droite = fin;
const int pivot = tab[debut];
/* PARTIE I
* Si la liste est trop petite pour etre triee, on ne fait rien
*/
if(debut >= fin)
return ;
/* PARTIE II
* On arrage le tableau pour avoir toutes les valeurs inferieures au pivot
* vers la gauche, toutes les valeurs superieures vers la droite, puis on
* place le pivot au bon endroit.
*/
while(1)
{
/* On cherche deux valeurs mal placees, et on les permute */
for( ; tab[gauche] < pivot; gauche++) {}
for( ; tab[droite] >= pivot; droite--) {}
if(gauche < droite)
{
permuter(tab, gauche, droite);
}
else
{
break;
}
}
/* PARTIE III
* On recommence sur la partie gauche, puis sur la partie droite du tableau.
*/
quickSort(tab, debut, gauche-1);
quickSort(tab, gauche+1, fin);
}
|
Que vous pouvez appeller comme ceci :
Code : C1 | quickSort(tab, 0, taille-1); /* Trie le tableau de la case 0 a la case taille-1, autrement dit tout le tableau) */
|
Bonne lecture et bonne prise de tête

!
Attention à ne jamais créer une fonction appellée qsort en C, car il existe une fonction standard portant ce nom ! Vous risquez de créer une collision, même si vous n'incluez pas stdlib.h (où ladite fonction est définie).
Dans la dernière partie, nous verrons les performances et optimisations possibles de quickSort...
Cet algorithme s'appelle le « tri rapide ». Alors
rapide, rapide, c'est vite dit. Dans quelle mesure est-il rapide ?
Une faible complexité
Nous allons ici regarder la
complexité de l'algorithme. Pour ceux qui ne connaissent pas, je m'auto-cite :
Citation : GuilOoooIl s'agit de savoir combien d'opérations on doit faire pour trier un tableau de N chiffres, en utilisant notre méthode. La complexité peut nous servir à évaluer la vitesse d'un algorithme sur telle machine, ou encore à comparer les algorithmes entre eux.
Le problème, c'est qu'évaluer très précisément le nombre d'opérations est souvent long et/ou difficile (ça va ensemble

). On va donc se contenter de calculer un ordre de grandeur : la complexité. Pour faire cet ordre de grandeur, on va négliger les instructions, qui, sur les machines récentes, sont extrêmement rapides à exécuter (moins d'une milli-seconde).
On va surtout s'intéresser aux boucles : ce sont elles qui prennent un temps non-négligeable. Et, pour faire simple, on va définir qu'un tour de boucle vaut 1. C'est la référence. Comme les boucles contiennent des instructions classiques qui sont super-rapides, il n'y a pas beaucoup de différences de l'une à l'autre (au niveau du temps), sauf s'il y a une boucle dans une boucle. Mais dans ce cas c'est une autre histoire

.
Provient de la
sous partie III du tuto sur le tri à paniers.
La complexité de cet algorithme est plus difficile à évaluer que celle du tri à paniers, cependant. Il faut faire un peu de maths, et fournir des explications accessibles n'est pas toujours facile. Alors, de peur de vous dire une bêtise, je ne vais pas rentrer ici dans le détail.
On sait que, dans la plupart des cas, nous allons avoir environ
1.38*N*log(N) opérations à réaliser pour N cases à trier. Ce qui nous donne, si on simplifie en enlevant les constantes, une complexité de
O(N*log(N)). (On note O( nombre ) la complexité d'un algorithme). Cette complexité est très compétitive.
Cependant, dans certains cas, la complexité peut tomber à
O(N2), qui est déjà bien plus mauvaise. Simplement, ce cas de figure ne survient pas souvent : il faudrait donner une liste presque déjà triée à QSort pour qu'il galère ainsi et nous donne ce mauvais score. Ainsi, si vous avez souvent des listes quasiment déjà triées à manipuler, vous pouvez vous rabattre sur le
tri par insertion ou
par paniers. Dans les autres cas, notre cher tri rapide fera l'affaire.
Comment optimiser le tri rapide ?
On a donc vu que ce tri était rapide en théorie. En pratique, ça dépendra avant tout de l'implémentation que vous allez utiliser. Voici quelques optimisations possibles, en vrac, sans explications sur le pourquoi du comment (certaines sont triviales, d'autres moins).
- Mélanger le tableau aléatoirement avant de le trier. Ça paraît débile, mais QSort adore les tableaux bien mélangés et a horreur de ceux déjà presque triés. En utilisant un mélange avant QSort, on augmente les chances d'avoir une liste très désordonnée.
- Lors des appels récursifs, trier d'abord le sous-tableau le plus long et ensuite le sous-tableau le moins long. C'est juste un test de plus, et ça peut accélerer le tri (ne me demandez pas pourquoi...)
- Utiliser un autre algorithme de tri sur les sous-tableaux quand ils deviennent trop petits. Certains algorithmes sont plus efficaces que QuickSort sur de petits tableaux.
- Choisir un bon pivot, idéalement la médiane du tableau. Ce n'est cependant pas possible si on trie autre chose que des nombres, et ça peut être complexe à gérer.
- Vous pouvez essayer de mettre chacun des deux appels récursifs à quickSort dans un thread séparé, afin de faire les deux tris simultanément. Ceci n'est profitable qu'avec un double core ou deux processeurs.
Chacune de ces optimisations a des avantages et des inconvéniants (elles accélèrent le tri au prix d'opérations préalables qui peuvent être lentes) et sont donc à utiliser seulement si le besoin s'en fait ressentir.
Voilà, vous pouvez maintenant trier vos tableaux avec QSort. C'est une méthode de tri très utilisée, vous la rencontrerez sûrement à un moment ou à un autre.
Elle est même souvent utilisée pour implémenter la fonction standard du tri en C (
doc. qsort) et du C++ (
doc. std::sort).
Pour plus d'informations, et de nombreuses implémentations, je vous renvoie encore une fois à
la page wikipédia qui traite du sujet.
Si vous avez un problème, une remarque, ou une suggestion, n'hésitez pas à me contacter par MP.
Bon codage
Ce tutoriel est mis à disposition sous contrat
Creative Commons 
. Ça signifie que vous pouvez le copier et le modifier librement, à condition de citer l'auteur original et de garder cette licence.