Cet algorithme s'appelle le « tri rapide ». Alors
rapide, rapide, c'est vite dit. Dans quelle mesure est-il rapide ?
Nous allons ici regarder la
complexité de l'algorithme.
En d'autres termes, nous allons chercher à évaluer le nombre d'opérations nécessaires au tri de notre tableau en fonction de la taille de ce dernier. Cette évaluation ne sera pas précise : nous ne dirons pas « il va falloir 1032 opérations pour ranger un tableau de 700 cases ». Nous allons plutôt chercher à voir comment le temps d'exécution varie lorsque la taille du tableau elle-même varie.
Cependant, cette valeur n'est pas toujours la même, elle dépend naturellement des données que nous devons trier. Nous allons donc étudier deux cas : le pire cas possible, et le meilleur cas possible.
Pire cas possible
Prenons un exemple simple, le cas dans lequel quickSort est le moins bon. Supposons que l'on veuille ranger :
10 9 8 7 6 5 4 3 2 1
avec notre algorithme. Ça vous paraît ridicule ? Pourtant, quickSort ne peut pas « voir » qu'il suffit d'inverser les valeurs, et il va trier comme d'habitude. On choisit donc la première valeur,
10, comme pivot, puis on effectue la partition :
1 9 8 7 6 5 4 3 2 10
Note : les valeurs en gras sont celles qui sont placées définitivement, et qui seront donc ignorées par l'algorithme dans la suite de son déroulement.
On a simplement permuté
1 et
10. Mais toutes les autres valeurs sont inférieures à
10, donc aucune n'est déplacée ! Maintenant, on recommence la même opération, en ignorant le
10 bien placé à la fin. On choisit
1 comme pivot. Or, toutes les valeurs lui étant supérieures sont après lui dans le tableau. On ne fait donc rien (mais on a quand même parcouru le tableau une fois pour s'en rendre compte).
Rebelotte en ignorant le
1 et le
10 :
1 2 8 7 6 5 4 3 9 10
On se retrouve en fait dans la même situation qu'initialement : le
9, plus grande valeur du tableau, a été permutée avec le 2, puis, comme toutes les autres valeurs sont inférieures à 9, nos deux recherches d'éléments mal placés se sont « croisées », et on n'a rien permuté de plus.
De même, après cette étape, un autre passage déterminera que le
2 est bien placé et qu'on peut l'ignorer.
Ce schéma se répètera ainsi jusqu'à ce que tout le tableau soit trié. On se rend compte que, à chaque étape, on place correctement
une valeur et que, pour ce faire, on parcourt tout l'ensemble tableau à trier. Ainsi, le premier passage fait 10 tests, le second 9, et ainsi de suite. Au final, on aura effectué 10 + 9 + 8 + ... + 3 + 2 + 1 =
55 tests et
10 permutations (une par passage).
Cette idée se généralise très facilement à des tableaux de longueur
N : quickSort fera N + (N - 1) + (N - 2) + ... + 3 + 2 + 1 tests, et N permutations. En calculant la somme N + (N - 1) + (N - 2) + ... + 3 + 2 + 1, on trouve qu'elle est égale à
N*(N+1)/2, soit
grossièrement N². Ainsi, si vous doublez la taille du tableau,
dans le pire des cas, le temps d'exécution quadruplera.
Meilleur cas possible
Nous avons vu le cas pessimiste, le pire comportement possible de quickSort. Maintenant, quel est son meilleur comportement ? Prenons un nouvel exemple. Cette fois-ci, les valeurs à trier sont :
6 1 2 5 4 3 9 8 7 11 10
Ici, on choisit comme pivot le numéro
6. On le permute avec la première valeur rencontrée (en partant de la droite) qui lui est inférieure, à savoir le 3. Il n'y a aucune autre permutation à faire, on divise donc le tableau en deux :
3 1 2 5 4 6 9 8 7 11 10
On trie maintenant chaque tableau indépendamment. Dans celui de droite, je choisis
3 comme pivot, et je le permute avec la première valeur qui lui est inférieure en partant de la droite : le 2. De même pour l'autre tableau, on choisit
9 comme pivot, et on le permute avec le
7. On divise chacun des tableaux en deux et on obtient...
2 1 3 5 4 6 7 8 9 11 10
... un tableau tout coloré ! Une dernière étape, triviale, consiste à ordonner les différents tableaux de 2 éléments. Ce qui nous donne...
1 2 3 4 5 6 7 8 9 10 11
... un tableau trié.
Regardons maintenant combien d'opérations nous avons eu à effectuer. Pour la première étape, nous avons simplement parcouru le tableau, nous avons donc effectué
11 opérations. À la seconde étape, nous avons parcouru deux sous-tableaux faisant chacun 5 cases, donc en tout
10 opérations. À la troisième étape, nous avons parcouru 4 tableaux de 2 cases chacun, soit
8 opérations. Soit un total de 11 + 10 + 8 =
29 tests effectués. Pour les déplacements, nous en avons fait
1, puis
2, puis
4, soit
7 permutations.
La question se pose maintenant de savoir comment généraliser ce résultat à un tableau de longueur
N ? Pour ce faire, nous devons d'abord remarquer qu'à chaque étape du processus, nous doublons le nombre de sous-tableaux considérés, mais que la taille de chacun de ces sous-tableaux est divisée par deux. Donc, au final, chaque étape demandera
N opérations.
Il nous faut maintenant compter le nombre d'étapes nécessaires pour trier un tableau. Pour cela, on se pose la question : « quand dois-je arrêter de trier ? Quand le travail est-il fini ? ». Le tri est bien évidemment terminé lorsque nous atteignons des sous-tableaux de taille 1. Moralité : il y a autant d'étapes que de divisions par 2 nécessaires pour passer de N à 1. La fonction qui nous dit « combien de fois diviser N par 2 avant d'arriver à 1 » existe : c'est une fonction qui s'appelle le
logarithme de base 2, que l'on notera ici
log.
Du coup, le tri rapide a fait
N * log(N) tests. Cela signifie que si je double la taille du tableau,
dans le meilleur des cas, on double le temps d'exécution, et on lui ajoute une certaine durée (toujours la même).
À titre indicatif
uniquement, voici le tracé de la courbe
N*log(N), de manière à ce que ceux qui ne voient pas bien à quoi elle ressemble puissent se faire une idée. J'insiste : elle est juste là pour vous donner une idée, n'essayez pas de vous en servir pour mesurer le temps d'exécution de l'algorithme. D'ailleurs, les chiffres ont été volontairement effacés.
Le nombre de permutations, lui, est moins évident à évaluer, nous ne le traiterons donc pas ici.
Dans le pire cas, on a bien vu quel tableau provoquait le comportement lent : quand le tableau est rangé à l'envers. Mais, dans le meilleur des cas, on ne voit pas bien quelles caractéristiques font que le tableau provoque un bon tri. Comment les reconnaître ?
Les tableaux provoquant les meilleurs cas sont ceux dont le pivot est proche de la
médiane du tableau, c'est-à-dire qu'il y a autant de valeurs supérieures au pivot que de valeurs inférieures.
Mon exemple était choisi de manière à ce que chaque sous-tableau soit également un tableau optimal, il en a donc résulté que le tri rapide fut particulièrement efficace dans ce cas.
Au final...
Les cas que nous avons vus ici sont des extrêmes. Dans la vie de tous les jours, les tableaux que vous allez trier ne seront pas comme ceux que l'on a vus, ils oscilleront quelque part entre les deux. De même, l'efficacité du tri variera entre les deux extrêmes que nous avons calculés.
Cependant, comme nous avons vu quels facteurs tendaient à nous rapprocher du meilleur cas ou du pire cas, on peut tenter de jouer sur eux afin de produire souvent de bons cas. On peut par exemple choisir un pivot plus intelligent que la première valeur de la série.
L'optimisation du tri rapide est un domaine qui prendrait un tutoriel entier à vous enseigner, nous verrons donc cela dans un prochain épisode.