Aller au menu - Aller au contenu

[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)
Avatar
Auteur : GuilOooo
Visualisations : 7 750

Plus d'informations Plus d'informations
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 !
Sommaire du tutoriel :
Icône du chapitre

Le principe

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à ? o_O

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
42 5 38 37 21


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 :p .

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 :pirate: !


Passons à la pratique !

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à, , et sur google).

Code : OCaml
1
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 :



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 : C
1
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 : C
1
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...

Rapide... À quel point ?

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 : GuilOooo
Il 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 :p ). 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).



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  - Paternité - Partage des conditions à l'identique. Ça signifie que vous pouvez le copier et le modifier librement, à condition de citer l'auteur original et de garder cette licence.
Retour en haut Retour en haut


Créé : le 13/06/2007 à 21:01:51
Modifié : le 22/08/2008 à 16:09:07
Avancement : 100%
Licence : Copie non autorisée

Changer de design | En savoir plus | Plan du site | Politique d'accessibilité | Règles | RSS tutoriels | RSS news
Édité par Simple IT SARL : Nous contacter | Notre blog | Revue de presse | Publicité

Y'a plus rien à lire, faut remonter maintenant !

Hébergement web - Correction de tutoriels - Créer un site
Vous souhaitez apparaître ici ? Contactez-nous.

Nombre de connectés 600 Zéros connectés | Requêtes SQL 8 requêtes | Temps de génération de la page : Total (SQL) 0.0346s (0.024s)