SlideShare una empresa de Scribd logo
1 de 21
Descargar para leer sin conexión
1
Les algorithmes de tri
En passant à la machine à café, vous avez sans doute déjà croisé des développeurs. Vous avez sans doute constaté qu’ils ont l'air passionnés par leur métier, ce qui
rend leurs discussions animées. Et vous les avez sans doute entendu prononcer des mots comme « Bubble », « Quicksort », « logarithme » ou encore « complexité »,
qui semblent provenir d'une autre langue. Ce sont pourtant des notions primordiales en programmation. Dans cet article, nous allons tenter de démystifier ce
charabia.
« Bubble », « Quicksort », « Insertion » ou encore « Fusion » (pour ne citer
qu’eux) sont les noms donnés à des algorithmes de tri célèbres. Wikipedia
indiquei
qu’un algorithme est « une suite finie et non ambigüe d’opérations ou
d'instructions permettant de résoudre un problème ». Un algorithme de tri est
donc la « recette de cuisine » permettant à un logiciel de ranger les éléments
d’une liste.
On veut trier une liste lorsqu’on pense que ses éléments sont dans le désordre,
ou plus précisément dans un ordre qui ne nous convient pas. L’objectif du tri, en
tant qu’algorithme, est de mettre les éléments dans le bon ordre. Trier une liste
suppose donc qu’on est capable d’établir une relation d’ordre entre ses
éléments (ce qui n’est pas toujours simple et/ou possible). Par exemple, on peut
dire si Nicolas est (objectivement) plus petit que David. Quand je parle de « plus
petit », je fais instinctivement référence à la taille des personnes, mais on aurait
tout aussi bien pu comparer leurs poids, leurs quantités de cheveux, leurs notes
au baccalauréat, etc. Mais pour simplifier, disons juste qu’on se base sur le
critère de la taille. Dans cet article, nous n’allons traiter que des entiers pour
simplifier la discussion. La relation d’ordre sera donc établie.
« il faut choisir l’algorithme adapté au contexte… »
Si ce sujet revient souvent chez les développeurs, c’est parce que le besoin de
trier des données est omniprésent dans les programmes informatiques. Par
habitude ou par inattention, on ne réalise pas qu’on manipule sans arrêt des
données triées. Par exemple, disons qu’on s’intéresse à votre compte bancaire.
Sur votre relevé, les opérations sont triées par date. Quand vous souscrivez à
l’option Web, vous avez également la possibilité de trier selon les entêtes des
colonnes (montant, date, numéro d’opération, bénéficiaire, type, etc.), mais
vous ne vous êtes probablement jamais dit qu’il y avait un algorithme efficace
caché derrière cette fonctionnalité.
En Javaii
, on dispose de deux structures de données intéressantes à trier : les
listes et les tableaux. Les algorithmes à base de listes sont plus faciles à
programmer, mais sont généralement moins performants. Les tableaux
demandent parfois de se retourner le cerveau pour coller à l’esprit de
l’algorithme, mais le jeu en vaut la chandelle. C’est donc à ces derniers que nous
allons nous intéresser. Dans cet article, nous allons discuter d’une poignée
d’algorithmes. Pour que l’ensemble soit cohérent, je vous propose de créer
l’interface « Tri » qui définit la méthode « trier() ». Cette méthode ne renvoie
rien. Elle prend un tableau en paramètre et le tri « sur place ».
public interface Tri {
void trier(final int[] tab);
...
Il faut préciser qu’il existe des bons et des mauvais algorithmes de tri. Quand on
tri une petite liste et qu’on le fait rarement, même le plus mauvais des
algorithmes fera l’affaire. Par contre, quand on manipule des listes
conséquentes et/ou qu’on les trie souvent, il faut choisir avec attention
l’algorithme qui sera le plus adapté au contexte.
On teste en trois temps
Avant d’entrer dans le vif du sujet, nous allons écrire des tests simples, à l’aide
de la bibliothèque JUnit, qui nous permettront de vérifier de manière
automatique que les programmes fonctionnent correctement. Cela s’inscrit
2
dans une démarche qualité. Pour cela, nous allons employer la méthode 3T
(Tests en Trois Tempsiii
) qui s’inspire du TDD (Test Driven Development). Durant
l’écriture de cet article, cette démarche a permis d’identifier des erreurs qui
seraient très certainement passées inaperçues sans.
Tous les algorithmes doivent passer par la même moulinette de tri. On peut
donc en écrire une version « abstract », comme suit, dont devront hériter toutes
les classes de test :
public abstract class AbstractTriTest {
protected Tri tri;
@Test
public void testTriTabVide() {
final int[] tab = {};
doTestTri(tab);
}
@Test
public void testTriTabUnSeulElement() {
final int[] tab = { 1 };
doTestTri(tab);
}
@Test
public void testTriTabDeuxElements() {
final int[] tab = { 2, 1 };
doTestTri(tab);
}
...
@Test
public void testTriTabMelange() {
final int[] tab = { 8, 6, 3, 9, 2, 1, 4, 5, 7 };
doTestTri(tab);
}
@Test
public void testTriTabQuasiTrie() {
final int[] tab = { 1, 2, 3, 4, 5, 6, 9, 8, 7 };
doTestTri(tab);
}
...
protected void doTestTri(final int[] tab) {
tri.trier(tab);
int temp = Integer.MIN_VALUE;
for (final int elt : tab) {
Assert.assertTrue(temp <= elt);
temp = elt;
}
}
Ici, l’idée est de multiplier les cas de tests. Vous trouverez de nombreux autres
tests dans le code source proposé avec cet article sur le site Web de
Programmez. Un des tests (non présenté ici) a consisté à générer, selon diverses
distributions (aléatoire, croissante, décroissante, quasi croissante, gaussienne,
sinusoïdale, etc.), des fichiers (des petits et des gros, voire très gros) contenant
des listes à trier.
En entreprise, on entend souvent dire (à tort ou à raison) qu’il faut utiliser les
fonctionnalités du langage pour trier des tableaux. Cela va donc nous fournir un
premier algorithme de référence. On commence bien entendu par la classe de
test qui sera relativement simple :
public class JavaTriTest extends AbstractTriTest {
@Before
public void doBefore() {
tri = new JavaTri();
}
Dans la suite de cet article, les classes de test des autres algorithmes seront
toujours calquées sur ce modèle. Elles ne seront donc pas présentées.
« il faut utiliser les fonctionnalités du langage… »
La classe qui met ça en pratique sera également très simple puisqu’il suffit de
faire appel aux méthodes déjà existantes :
public class JavaTri implements Tri {
@Override
public void trier(final int[] tab) {
Arrays.sort(tab);
}
3
Les premières manières qui nous viennent à l'esprit
Tri par sélection
Sans le savoir, où sans s’en rendre compte, on est confronté très jeune à des
algorithmes de tri. Je dirais que cela se produit au plus tard en petite section de
maternelle, vers l’âge de trois ans, à l’occasion du passage du photographe
scolaire. Pour bien composer sa photo, le photographe doit placer les enfants
sur le banc en fonction de leurs tailles.
Pour cela, le photographe passe en revue les enfants à la recherche du plus
petit. Une fois trouvé, il le place sur le banc. Il réitère cette opération sur les
enfants restant en plaçant à chaque fois l’enfant sélectionné à la prochaine
place disponible sur le banc. L’algorithme se termine lorsqu’il ne reste plus
d’enfant. Si vous avez bien suivi, vous aurez compris que le dernier enfant à être
sélectionné, et donc à être placé sur le banc, est mécaniquement le plus grand.
Cet algorithme aurait pu s’appeler le « tri du photographe », mais on le trouve
dans la littérature sous le nom de « tri par sélection ».
Fig. 1 : Photo de classe. Crédit : Puyo / Podcast Science Fig. 2 : Tri par sélection
4
Pour programmer ce tri sur un tableau, il suffit donc de le parcourir à la
recherche du plus petit élément (ou du plus grand, ce qui revient au même) et
de le positionner au prochain emplacement disponible :
public class SelectionTri implements Tri {
@Override
public void trier(final int[] tab) {
for (int i = 0; i < tab.length; i++) {
// Chercher la position (pos) du plus petit dans la zone
restante
int pos = i;
for (int j = i; j < tab.length; j++) {
if (tab[j] < tab[pos]) {
pos = j;
}
}
final int petit = tab[pos];
// Delaler de i a pos
for (int j = pos; i < j; j--) {
tab[j] = tab[j - 1];
}
// mettre le plus petit trouve a la fin
tab[i] = petit;
}
}
Dans la suite, on notera « n » le nombre d’éléments dans la liste à classer. Dans
l’exemple, « n » correspond donc au nombre d’enfants dans la classe. À titre
d’illustration, disons qu’il y en a dix-huit (cf. dessins). Pour sélectionner le plus
petit, le photographe doit donc passer en revue les 18 élèves, ce qui nécessite
de faire 17 comparaisons. Pour le deuxième, il reste 17 élèves à comparer,
nécessitant 16 comparaisons. Pour le troisième il faudra faire 15 comparaisons,
et ainsi de suite… Au final, le photographe va donc faire « 17+16+15+…+1 »
comparaisons, ce qui est un peu long à calculer. Heureusement, on fait un peu
de math au lycée. Ça peut se factoriser à l’aide de la formule « n*(n-1) /2 », ce
qui donne « 153 » comparaisons pour une classe de dix-huit bambins.
À la fin de la journée, le photographe doit donc avoir un sacré mal de tête. Et
encore, il s’agit d’une classe relativement peu chargée. En école d’ingénieur,
nous étions 150 dans ma promotion. Le photographe devrait donc faire plus de
onze mille (11175) comparaisons pour préparer sa photo à l’aide du tri par
sélection : autant dire qu’il y passerait la journée. Sans vendre la mèche, on
devine déjà que le « tri par sélection » n’est pas réputé pour être très efficace.
Tri par insertion
Lorsqu’on est enfant, il y a un autre algorithme de tri qu’on apprend (ou
découvre) assez vite. C’est celui que les joueurs de cartes utilisent pour
organiser leurs mains. Le joueur pioche la carte qui est en haut du tas et la place
(l’insère) à la bonne position dans la main. Cet algorithme se nomme le « tri par
insertion ».
En première approche, le « tri par insertion » est donc l’inverse du « tri par
sélection ». Il est toutefois un peu plus efficace. Et, sans surprise, le code du
programme est très proche de celui qu’on a déjà vu :
public class InsertionTri implements Tri {
@Override
public void trier(final int[] tab) {
int pos;
// On peut commencer a 1 directement...
for (int i = 1; i < tab.length; i++) {
final int temp = tab[i];
// recherche position
for (pos = 0; pos < i; pos++) {
if (temp < tab[pos]) {
break;
}
}
// Decaler
for (int j = i; pos < j; j--) {
tab[j] = tab[j - 1];
}
// Insertion
tab[pos] = temp;
}
}
5
Fig. 3 : Tri par insertion
Si on accepte de chercher la bonne position en partant de la queue, on peut en
profiter pour réaliser le décalage du même coup.
for (int i = 1; i < tab.length; i++) {
final int temp = tab[i];
// Recherche et decalage d'un seul coup
for (pos = i; 0 < pos && temp < tab[pos - 1]; pos--) {
tab[pos] = tab[pos - 1];
}
// Insertion
tab[pos] = temp;
}
Décider si on lance la recherche à partie de la tête ou de la queue n’est pas
anodin. Selon que la liste initiale est plutôt croissante ou décroissante, l’une ou
l’autre des recherches sera plus efficace. Et puisqu'on en est à se demander
dans quel sens parcourir la liste, on pourrait aussi démarrer depuis la dernière
position utilisée. Il suffit ensuite de monter ou de descendre le curseur selon le
résultat des comparaisons et de s'arrêter lorsque ça s'inverse. Cela permet
d'optimiser en moyenne le traitement des sous-séquences. Dans le pire des cas,
cela sera équivalent à la recherche par une extrémité.
À titre d’illustration, prenons de jeu de la Beloteiv
dans lequel chaque joueur
reçoit huit cartes. Ici « n » vaudra donc « 8 ». Pour la première carte, il n’y a rien
à faire puisque la main est vide. Pour la deuxième carte, il y a une comparaison à
faire avec l’unique carte déjà en main pour savoir si elle doit aller à gauche ou à
droite. Pour la troisième carte, il y aura « au maximum » deux comparaisons à
effectuer. Il est important de comprendre qu’on arrête de faire des
comparaisons dès que la bonne position est identifiée. On fait ainsi tant qu’il
reste des cartes à piocher.
Au final, et dans le « pire des cas », on fera donc « 1+2+…+7 » comparaisons,
soit « 28 » au total. C’est la même formule (à l’envers) de calcul que pour le « tri
par sélection » et ce n’est donc pas mieux. En revanche, dans le « meilleur des
cas » où le joueur pioche les cartes dans l’ordre idéal (ie. triées dans l’ordre
croissant ou décroissant selon qu’on insère par la tête ou par la queue), il n’y
aura que 7 comparaisons. Et bien entendu, plus le nombre « n » est grand et
plus la différence est sensible.
6
Complexité
Comme vous l’avez surement déjà deviné, le nombre total d’opérations à
effectuer pour « dérouler » un algorithme est un critère majeur pour dire si
l’algorithme est efficace. Cela permet surtout de comparer l’efficacité de deux
algorithmes et de choisir le plus adapté.
Il est relativement « simple » de dénombrer « précisément » le nombre
d’opérations nécessaires pour « dérouler » un algorithme de tri lorsque le
nombre d’éléments « n » dans la liste est petit. Par exemple, on a vu qu’il faudra
réaliser « 153 » comparaisons pour trier « 18 » enfants à l’aide du « tri par
sélection ». Le même algorithme nécessite de réaliser « 11.175 » comparaisons
pour trier une classe de « 150 » élèves, et « 499.500 » pour trier les mille
employés d’une PME. Et si vous vouliez trier les « 81.338 » spectateurs d’un
match de foot au Stade de France, il faudrait « 3.307.894.453 » comparaisons.
Ces exemples mettent deux choses en évidence. Premièrement, la quantité
d’opérations nécessaire au déroulement du « tri par sélection » croît
rapidement lorsque le nombre « n » d’éléments dans la liste augmente.
Deuxièmement, il n’est pas nécessaire d’avoir une grande précision sur des
chiffres aussi importants. Quand on réalise « 3.307.894.453 » comparaisons, on
n’est plus à une comparaison près, ni à dix, ou cent, ou même cent millions.
D’ailleurs on aurait pu se contenter de dire qu’il fallait « environ trois milliards »
d’opérations. Ce qui compte, avec des valeurs si conséquentes, c’est d’avoir un
« ordre de grandeur », même vague. C’est ce qu’on va appeler la « complexité »
d’un algorithme.
En fait, il n’est pas possible de discuter des tris sans parler de « complexité ». Ce
mot à lui seul est synonyme de cauchemars pour bien des étudiants en école
d’info. Et pour ma part, je dois avouer que j’ai mis longtemps à en comprendre
les tenants et les aboutissants. Je me propose donc de vous l’expliquer
simplement, en prenant quelques raccourcis. Ne vous inquiétez pas si tout n’est
pas clair, ça va se clarifier au fur et à mesure.
« discuter des tris impose d’aborder la complexité… »
La complexité d’un tri de « n » éléments se note avec un « omicron » : dites
« grand O ». Par exemple, la complexité du « tri par sélection » sera en « O(n*(n-
1) /2 ) », où « n*(n-1) /2 » est la formule qu’on avait utilisée un peu plus tôt.
Comme on s’intéresse à des valeurs importantes et qu’on ne veut qu’un « ordre
de grandeur », on considèrera que cette « complexité » peut se « simplifier » en
« O(n*(n-1)) », qui peut elle-même se simplifier en « O(n²) ». On dira que
l’algorithme du « tri par sélection » est de « complexité quadratique » en
« O(n²) ».
Pour faire simple, quand vous multipliez par « 10 » le nombre « n » d’éléments,
vous multipliez par « 100 » le nombre d’opérations à réaliser lorsque vous
utilisez un algorithme de tri en « O(n²) ». Dit comme ça, ce n’est pas très
impressionnant alors essayons d’associer cela avec des durées pour voir à quel
point c’est mauvais. Disons qu’une opération prenne une nanoseconde (ns) en
moyenne (ce qui est très optimiste).
Nombre d’éléments
« n »
Nombre d’opérations
pour un tri en « O(n²) »
Durée pour un tri en
« O(n²) »
10 100 100 ns
100 10 000 10 us
1 000 1 000 000 1 ms
10 000 100 000 000 100 ms
100 000 10 000 000 000 10 s
1 000 000 1 000 000 000 000 16 min 40 s
10 000 000 100 000 000 000 000 27 heures
100 000 000 10 000 000 000 000 000 115 jours
1 000 000 000 1 000 000 000 000 000
000
31 ans
Alors que le tri de « 10 » éléments prendra seulement « 100 » nanosecondes à
l’aide d’un algorithme en « O(n²) », il faudra « 10 » microsecondes pour une
centaine d’éléments et carrément une milliseconde pour mille. Et ces temps
augmentent encore quand le nombre d’éléments augmente. Ca augmente
même très vite.
7
Pour un milliard d’éléments, ce qui reste raisonnable pour un programme
informatique, notamment dans le domaine de la finance, on aura besoin d’un
tiers de siècle. Autant dire que les données seront périmées depuis longtemps
quand l’algorithme aura fini son travail. L’ordinateur sera peut-être même déjà
passé à la poubelle. Et je ne vous parle même pas de la facture d’électricité
qu’on aurait tendance à oublier (coût en énergie, en ressource naturelle, en
entretien et en Euro).
Pour bien réaliser ce que ça représente, prenons un exemple plus concret. Pour
trier la population mondialev
, soit un peu plus de sept milliards de personnes (au
début de l’été 2014), il faudrait plus d’un millier et demi d’années (1655 ans
environ). Pour que le tri soit fini au moment où vous lisez cet article, il aurait
donc fallu le commencer en l’an 350 de notre ère, soit plus d’un siècle avant le
début du moyen âge (476 à 1453).
On parle de complexité en se plaçant dans la situation la plus défavorable pour
l’algorithme. Ça permet en quelque sorte de majorer. On peut aussi se placer
dans le cas le plus favorable ou dans le cas moyen. Par exemple et sans plus
d’explications, le « tri par insertion » sera de « complexité linéaire » en « O(n) »
dans le cas le plus favorable et en « O(n²) » dans les cas moyens et défavorables.
Bon, je ne vais pas vous embêter plus que ça avec cette histoire de complexité.
J’ai passé sous silence certains aspects (gestion de la mémoire, déplacements,
permutations, création d’objets, flux, etc.) qui peuvent avoir un impact en terme
de performance, en fonction de la plateforme et du langage utilisé, mais je
pense sincèrement que l’essentiel y est… pour l’instant…
En plus de la question des performances, on a pris l’habitude de classer (cf.
tableau récapitulatif en fin d’article) les algorithmes de tri selon des
caractéristiques importantes, comme la stabilité ou le fait de pouvoir trier sur
place, c’est-à-dire qu’on réarrange les éléments directement dans la structure
initiale. Un algorithme est dit « stable » lorsqu’il ne modifie pas l’ordre des
éléments de même valeur.
Enfin, une des principales contraintes concerne l’espace mémoire. Plus
spécifiquement, on se demande si on est capable de trier l’ensemble des
données en mémoire centrale. On dira que le tri est « interne » si c’est possible,
et « externe » sinon.
Comment faire mieux ?
On devine qu’il va falloir trouver comment faire mieux, notamment en utilisant
des algorithmes beaucoup plus rapides. Rassurez-vous, je ne vais pas vous
présenter tous les algorithmes de tri qui existent. Je vais me limiter aux plus
connus en insistant sur leurs points forts et leurs faiblesses.
Tri à bulle (Bubble Sort)
Le tri à bulle est certainement celui qu’on apprend à programmer en premier en
école d’informatique, avant même le « tri par sélection » et le « tri par
insertion ». Pour effectuer un « tri à bulle », il faut parcourir la liste en
permutant les éléments contigus qui ne sont pas dans le bon ordre. Par
exemple, si je trouve « 9 » dans la case « 4 » et seulement « 2 » dans la case
« 5 », alors j’échange leurs positions. Quand c’est fini, l’élément le plus grand se
retrouve donc en queue de liste. Cet élément est arrivé à sa bonne position,
mais le reste de la liste est encore potentiellement en désordre. On
recommence autant de fois qu’il y a d’éléments dans la liste, ce qui fait donc à
chaque fois remonter le plus grand élément restant.
8
Fig. 4 : Tri à bulle
Le nom du « tri à bulle » vient du fait que les plus gros éléments remontent
comme des bulles dans une flute de champagne. D’ailleurs, on n’utilise jamais le
« tri à bulle », car les bulles remontent très lentement.
public class BulleTri implements Tri {
@Override
public void trier(final int[] tab) {
for (int i = 0; i < tab.length - 1; i++) {
for (int j = 1; j < tab.length; j++) {
if (tab[j] < tab[j - 1]) {
// Permutation
permuter(j, j - 1, tab);
}
}
}
}
public static void permuter(final int indexA, final int indexB,
final int[] tab) {
final int temp = tab[indexA];
tab[indexA] = tab[indexB];
tab[indexB] = temp;
}
Pour une liste dans laquelle le nombre d’éléments « n » est de « 9 », on fera
« 8 » (ie. « n-1 ») comparaisons pour mettre le plus grand élément à sa bonne
position. Et on va répéter cela « 8 » fois également, ce qui nécessitera donc
« 64 » comparaisons. La complexité associée au « tri à bulle » sera donc en
« O((n-1)²) » qu’on simplifiera en « O(n²) ».
On peut améliorer l’algorithme de base. En effet, à chaque passage, un élément
supplémentaire se retrouve à sa place définitive. Il n’est donc plus nécessaire de
l’inclure dans les comparaisons suivantes.
for (int i = 0; i < tab.length - 1; i++) {
for (int j = 1; j < tab.length - i; j++) {
if (tab[j] < tab[j - 1]) {
permuter(j, j - 1, tab);
On n’aura alors besoin que de « (n-1)*n/2 » comparaisons, soit « 36 »
comparaisons pour une liste de « 9 » éléments. La complexité restera toutefois
9
en « O(n²) », le multiplicateur « ½ » ne changeant rien à l’histoire pour les
grandes valeurs, comme on l’a déjà vu.
« accusé d’être lent, le tri à bulle cache bien son jeu… »
En outre, on peut stopper le déroulement de l’algorithme dès lors qu’on détecte
qu’aucune permutation n’a été réalisée durant une boucle. Si la liste s’y prête,
cela peut devenir une sérieuse optimisation.
public class RapideDemiBulleTri implements Tri {
@Override
public void trier(int[] tab) {
for (int i = 0; i < tab.length; i++) {
boolean permutation = false;
for (int j = 1; j < tab.length - i; j++) {
if (tab[j] < tab[j - 1]) {
// Permutation
permuter(j, j - 1, tab);
permutation = true;
}
}
if (!permutation) {
break;
}
}
}
Le « tri à bulle » est souvent accusé d’être lent, mais comme vous pouvez le voir,
il cache bien son jeu. À titre personnel, je dois avouer que j’ai beaucoup
d’affection pour cet algorithme, même en sachant que ce n’est pas le meilleur.
Tri rapide (Quick Sort)
S’il y a un algorithme de tri dont vous avez forcément entendu parler à la
machine à café, c’est bien le « Quick sort ». C’est l’un des algorithmes les plus
utilisés et certainement celui qui présente le plus de variantes. Le « Quick Sort »,
aussi appelé « tri rapide », fait partie de la famille d’algorithme dont le
fonctionnement repose sur le principe « diviser pour régner ».
Pour réaliser un « tri rapide », on doit choisir un élément dans la liste, qu’on
appelle « pivot ». On divise ensuite la liste en deux sous-listes. La première, à
gauche, contient les éléments inférieurs au pivot. La seconde, à droite, contient
les éléments supérieurs au pivot.
« vous avez forcément entendu parler du Quick Sort… »
On reproduit alors récursivement ce choix du pivot et la division sur les listes de
gauche et de droite précédemment construites jusqu’à n’avoir que des sous-
listes de zéro ou un élément. Pour finir, il suffit de rassembler les éléments de
toutes les sous-listes dans l’ordre gauche-droite.
Fig. 5 : Tri rapide (pivot en tête)
Le principe du « tri rapide » est donc très simple : tout se passe pendant la
construction de l’arbre. En revanche, il est réellement (très) difficile de
programmer cet algorithme dans la version utilisant des tableaux. Plus
précisément, c’est la séparation en fonction du pivot qui pose problème à de
nombreux développeurs. On trouve d’ailleurs de nombreux codes faux (qui ne
passent pas mes tests) sur Internet. Je vous propose donc de commencer en
douceur avec une version employant des listes.
10
public class RapideViaListeTri implements Tri {
private List<Integer> trier(final List<Integer> liste) {
if (liste == null || liste.isEmpty()) {
return new ArrayList<>();
}
// Choix du pivot a gauche
final int pivot = liste.get(0);
// pour ne pas traiter le pivot dans la boucle
liste.remove(0);
final List<Integer> listeGauche = new ArrayList<>();
final List<Integer> listeDroite = new ArrayList<>();
// Separation en deux sous listes
for (final int elt : liste) {
if (elt < pivot) {
listeGauche.add(elt);
} else {
listeDroite.add(elt);
}
}
final List<Integer> result = new ArrayList<>();
result.addAll(trier(listeGauche));
result.add(pivot);
result.addAll(trier(listeDroite));
return result;
}
À la lecture de ce code, vous avez sans doute repéré plusieurs problèmes
techniques. La méthode fonctionne correctement (elle passe tous mes tests),
mais elle est lente. Il y a fondamentalement trois « erreurs ». D’abord de
nombreuses listes sont créées et manipulées durant le traitement, ce qui coute
cher. Ensuite, l’auto-unboxing réalisé lors de l’itération sur la (sous) liste à trier
ajoute encore un peu à l’addition. Et l’auto-boxing lors de l’ajout aux sous-listes
finit de « plomber » la note. Cela peut se résoudre en travaillant directement sur
des objets, et donc sur des références, en les comparant à l’aide de
« compareTo() » en lieu et place de l’opérateur de comparaison.
Enfin, l’algorithme ne prend pas en compte la présence (éventuelle) de plusieurs
valeurs égales au pivot. Quitte à avoir deux sous-listes, on peut bien en gérer
une troisième pour ce cas spécifique. Le nombre total d’éléments restera
inchangé et le coût du test supplémentaire sera largement compensé par le gain
en nombre de récursions.
public class RapideViaListe2Tri implements Tri {
private List<Integer> trier(final List<Integer> liste) {
if (liste == null || liste.isEmpty()) {
return new ArrayList<>();
}
// Pivot a gauche
final Integer pivot = liste.get(0);
final List<Integer> listeGauche = new ArrayList<>();
final List<Integer> listeCentre = new ArrayList<>();
final List<Integer> listeDroite = new ArrayList<>();
// Separation en deux sous listes
for (final Integer elt : liste) {
final int comp = elt.compareTo(pivot);
if (comp < 0) {
listeGauche.add(elt);
} else if (comp == 0) {
listeCentre.add(elt);
} else {
listeDroite.add(elt);
}
}
liste.clear();
liste.addAll(trier(listeGauche));
liste.addAll(listeCentre);
liste.addAll(trier(listeDroite));
return liste;
}
Voici maintenant une première version utilisant des tableaux. L’intérêt de cette
structure est de pouvoir effectuer le tri « sur place », en manipulant des
primitifs.
public class RapideTri implements Tri {
@Override
public void trier(final int[] tab) {
11
trier(tab, 0, tab.length - 1);
}
private void trier(final int[] tab, final int gauche, final int
droite) {
if (droite <= gauche) {
return;
}
// Pivot à gauche
int positionPivot = gauche;
permuter(positionPivot, droite, tab);
for (int i = gauche; i < droite; i++) {
if (tab[i] <= tab[droite]) {
permuter(i, positionPivot++, tab);
}
}
permuter(droite, positionPivot, tab);
// tri recursif des sous-listes
// Bien entendu on ne tri pas le pivot
trier(tab, gauche, positionPivot - 1);
trier(tab, positionPivot + 1, droite);
}
Comme vous le constatez, cette première version utilisant des tableaux est bien
plus complexe. On arrive toutefois encore à distinguer la forme de base de
l’algorithme. Un des gros défauts, difficiles à corriger, de cette version est de
devoir gérer le décalage d’un grand nombre de cases. Pour gagner encore un
peu en vitesse, on va devoir passer par des fonctions de « System », qui font des
manipulations de la mémoire de façon native.
public class RapideTri2 implements Tri {
@Override
public void trier(final int[] tab) {
if (tab.length < 2) {
return;
}
int[] tab2 = new int[tab.length];
trier(tab, tab2, 0, tab.length);
System.arraycopy(tab2, 0, tab, 0, tab.length);
}
private void trier(final int[] tab, final int[] tab2, final int
gauche, final int droite) {
// Choix du pivot a gauche
final int pivot = tab[gauche];
int posGauche = gauche;
int posDroite = droite;
// Separation en deux sous listes
for (int index = gauche + 1; index < droite; index++) {
int elt = tab[index];
if (elt < pivot) {
tab2[posGauche++] = elt;
} else if (elt > pivot) {
tab2[--posDroite] = elt;
}
}
Arrays.fill(tab2, posGauche, posDroite, pivot);
if (posGauche > gauche) {
trier(tab2, tab, gauche, posGauche);
System.arraycopy(tab, gauche, tab2, gauche, posGauche -
gauche);
}
if (posDroite < droite) {
trier(tab2, tab, posDroite, droite);
System.arraycopy(tab, posDroite, tab2, posDroite, droite -
posDroite);
}
}
Pour en finir avec le « tri rapide », je vous propose une version simplifiée de ce
qu’on peut lire dans les sources du JDK 6. Dans cette version, il faut regarder
avec attention pour bien distinguer la structure de l’algorithme de base.
public class RapideCopyJdkTri implements Tri {
@Override
public void trier(final int[] tab) {
if (tab.length == 0) {
return;
}
trier(tab, 0, tab.length);
}
private static void trier(final int tab[], final int gauche,
final int taille) {
final int pivot = tab[gauche];
12
int g = gauche;
int g2 = g;
int d = gauche + taille - 1;
int d2 = d;
while (true) {
while (g2 <= d && tab[g2] <= pivot) {
if (tab[g2] == pivot) {
permuter(g++, g2, tab);
}
g2++;
}
while (d >= g2 && pivot <= tab[d]) {
if (tab[d] == pivot) {
permuter(d, d2--, tab);
}
d--;
}
if (g2 > d) {
break;
}
permuter(g2++, d--, tab);
}
// Swap partition elements back to middle
int s, n = gauche + taille;
s = Math.min(g - gauche, g2 - g);
decaler(tab, gauche, g2 - s, s);
s = Math.min(d2 - d, n - d2 - 1);
decaler(tab, g2, n - s, s);
// Recursively sort non-partition-elements
if (1 < (s = g2 - g)) {
trier(tab, gauche, s);
}
if (1 < (s = d2 - d)) {
trier(tab, n - s, s);
}
}
public static void decaler(final int tab[], int a, int b, int n)
{
for (int i = 0; i < n; i++, a++, b++) {
permuter(a, b, tab);
}
}
Les gains de chaque version pourraient sembler minimes, en comparaison de
l’investissement, mais ils sont réellement importants. Pour bien se rendre
compte des différences de performance, voici les temps de traitement moyens
observés sur mon ordinateur portable, équipé d’un JDK 7.
Algo 1 000 d’éléments 1 000 000 d’éléments
RapideViaListeTri 15 ms 8 s
RapideViaListe2Tri 13 ms 829 ms
RapideTri 1 ms 909 ms
Rapide2Tri - 52 ms
RapideCopyJdkTri - 61 ms
JavaTri (JDK 7) - 48 ms
On constate que la seconde version à base de liste (RapideViaListe2Tri) est dix
fois plus rapide que la première (RapideViaListeTri) pour un million d’éléments à
trier, ce qui est loin d’être négligeable. La première version à base de tableau
(RapideTri) est presque aussi rapide que la seconde version à base de liste
(RapideViaListe2Tri). La seconde version à base de tableaux (Rapide2Tri) est,
quant à elle, dix-sept fois plus rapide que la première. Seule la méthode de Java
7 (JavaTriTest), qui est hybride, parvient à faire mieux.
Ici, on voit aussi que mon ordinateur est loin de pouvoir effectuer les opérations
élémentaires en une nanoseconde puisqu’on se serait alors attendu à une durée
de l’ordre de « 20 ms » pour un million d’éléments. Cela est dû en partie à la
puissance de la machine et en partie au fait qu’on a du mal à coller au plus près
de l’algorithme théorique optimal.
« le choix du bon pivot dépend de la distribution attendue… »
Une des grosses difficultés du « Quick Sort » réside dans le choix du bon pivot, le
risque étant de déséquilibrer les sous-listes qu’on va construire. Quand on n’a
pas de meilleure idée et faute de mieux, on peut prendre le premier élément
(fig. 5).
13
Fig. 6 : Tri rapide (pivot en tête) sur liste quasi triée
Lorsqu’on soupçonne que les données sont déjà à peu près triées et que le choix
du pivot en première position provoquera un déséquilibre (fig. 6), on peut aussi
préférer choisir le pivot au milieu (fig. 7).
Et quand on n’a aucune information sur les données, on peut également choisir
le pivot de manière aléatoire (fig. 7). Un mathématicien vous dirait que ça
permet de maximiser « l’espérance » que ça se passe bien. Personnellement, je
n’aime pas trop cette solution. Elle donne souvent de bons résultats, mais le
déroulement de l’algorithme est alors aléatoire. Or, en bon informaticien, je
n’aime pas trop quand c’est non reproductible. En outre, ça laisse la porte
ouverte pour toutes les combinaisons où ça peut mal tourner. Or, en
informatique, comme l’énonce la loi de Murphyvi
, tout ce qui peut mal tourner
va mal tourner.
Fig. 7 : Tri rapide (pivot au centre)
14
Fig. 8 : Tri rapide (pivot aléatoire)
Comme son nom l’indique, le « Quick Sort » a la réputation d’être rapide, ce qui
est mérité, mais parfois un peu usurpé. Pour le comprendre, il va encore falloir
parler de complexité.
Fig. 9 : Distribution aléatoire
Dans le cas favorable où les éléments sont uniformément distribués, c’est-à-dire
bien mélangées (fig. 9), l’algorithme produit un arbre équilibré avec « 2 » sous-
listes de taille « n/2 » à chaque appel récursif, soit « n » opérations à chaque
étape (une étape est un nœud de l’arbre ou une feuille).
À l’étape « 1 », on aura donc « 2 » sous-listes. À l’étape « 2 », on aura « 2*2 =
22
» sous listes. À l’étape « 3 », on aura « 2*2*2=23
» sous listes, et ainsi de
suite. À l’étape « p », on aura « 2*2*2*…*2=2p
» sous listes. L’algorithme
récursif s’arrête lorsqu’on arrive à un seul élément. S’il faut « p » étapes pour
cela, on pourra donc dire que « n=2p
». La fonction mathématique qui permet de
calculer la valeur de « p » quand on connait « n » est le « logarithme de n en
base 2 » qu’on note « log2(n) » ou « lg(n) » en raccourci. Au final, la complexité
du « tri rapide » sera donc en « O(n lg n) ».
Fig. 10 : Distribution croissante
Dans le cas défavorable où les éléments sont déjà triés (fig. 10), l’algorithme
produit un arbre complètement déséquilibré, ressemblant à une longue tige
tordue et ne permettant de trouver la position que d’un seul élément à chaque
étape. On aura donc une complexité équivalente à celle du « tri par sélection »
en « O(n²) ».
15
Si on raisonne de nouveau en terme de durée, on se rend compte qu’il y a une
véritable différence entre un algorithme de tri en « O(n lg n) » et un autre en
« O(n²) ».
Nombre d’éléments
« n »
Durée pour un tri en « O(n
lg n) »
Durée pour un tri en
« O(n²) »
10 33 ns 100 ns
100 664 ns 10 us
1 000 10 us 1 ms
10 000 132 us 100 ms
100 000 1,6 ms 10 s
1 000 000 20 ms 16 min 40 s
10 000 000 233 ms 27 heures
100 000 000 2,5 s 115 jours
1 000 000 000 29 s 31 ans
Population mondiale 4 min 1 655 ans
Souvenez-vous. Un peu plus tôt, je vous ai dit qu’il fallait « 100 ns » pour trier
une liste de « 10 » éléments à l’aide d’un algorithme en « O(n²) » et d’un bon
ordinateur. Si on se place dans un cas en « O(n lg n) », il ne faudra plus que « 33
ns ». Bon, la différence ne saute pas aux yeux. Pour un million d’éléments, on
passe de « 16 minutes » à « 20 ms », ce qui reste encore dans les limites du
temps réel. Et pour trier la population mondiale, on passe de « 1655 ans » à
seulement « 4 minutes ».
Notez qu’il existe une optimisation du « Quick sort » assez contre-intuitive. Elle
préconise de mélanger la liste avant de la trier. L’idée est de se rapprocher de la
complexité en « O(n lg n) » du cas favorable où la liste est complètement
mélangée, quitte à dépenser un « O(n) » à préparer la liste.
Tri fusion (Merge Sort)
Dans la grande famille des tris reposant sur le principe « diviser pour régner »,
on trouve également le « tri Fusion ». À mon sens, le « tri Fusion » est au « tri
rapide » ce que le « tri par insertion » est au « tri par sélection ». Dans cette
famille de tri, on travaille toujours en trois phases. D’abord on divise. Ensuite on
règne. Et pour finir, on réconcilie (fusionne). Alors que, pour le « Quick Sort »,
tout se fait à la construction de l’arbre, pour le « tri fusion », la partie
intéressante se situe lors de la phase de réconciliation.
Pour réaliser un « tri fusion », on commence par diviser la liste à trier en deux
sous-listes de même taille. On réitère récursivement cette opération jusqu’à
n’avoir que des listes d’un seul élément. Cela produit donc un arbre à peu près
équilibré. On remonte ensuite dans l’arbre en fusionnant les sous-listes à
chaque étape. Pour cela, on prend le plus petit élément qui se présente en tête
des deux sous-listes à fusionner et on recommence tant qu’il reste des
éléments. Quand on revient à la « racine » de l’arbre, la liste est triée.
Fig. 11 : Tri fusion
16
Le coût des divisions récursives est quasi gratuit. Il est systématique et ne
demande de réaliser aucune comparaison entre les éléments. Si vous avez
compris ce qu’on avait dit pour la complexité du « quick Sort », vous avez
certainement déjà deviné que la complexité du « tri fusion » sera en « O(n lg
n) » dans tous les cas puisqu’on force la production d’un arbre équilibré.
« le tri fusion force la production d’un arbre équilibré… »
Comme pour le « Quick sort », il est plus simple de programmer l’algorithme du
« tri Fusion » en commençant par une liste, pour bien le prendre en main.
public class FusionViaListeTri implements Tri {
private List<Integer> trier(final List<Integer> liste) {
if (liste.size() <= 1) {
return liste;
}
// Separation en deux sous listes
final int posCentre = liste.size() / 2;
List<Integer> listeGauche = liste.subList(0, posCentre);
List<Integer> listeDroite = liste.subList(posCentre,
liste.size());
// Tri des deux sous liste
listeGauche = trier(listeGauche);
listeDroite = trier(listeDroite);
// Fusion
final List<Integer> result = new ArrayList<>(liste.size());
final Iterator<Integer> iterGauche = listeGauche.iterator();
final Iterator<Integer> iterDroite = listeDroite.iterator();
Integer g = next(iterGauche);
Integer d = next(iterDroite);
while (g != null || d != null) {
if (d == null || g != null && g.compareTo(d) < 0) {
result.add(g);
g = next(iterGauche);
} else {
result.add(d);
d = next(iterDroite);
}
}
return result;
}
private static Integer next(final Iterator<Integer> iter) {
return (iter.hasNext()) ? iter.next() : null;
}
Pour réaliser le même traitement sur un tableau, il faudra décaler les éléments
lors de la phase de fusion des zones triées de gauche et de droite.
public class FusionTri implements Tri {
@Override
public void trier(int[] tab) {
trier(tab, 0, tab.length - 1);
}
private void trier(int[] tab, int debut, int fin) {
if (fin <= debut) {
return;
}
// appels recursifs
final int centre = (debut + fin) / 2;
trier(tab, debut, centre);
trier(tab, centre + 1, fin);
// Fusion
fusionner(tab, debut, centre, fin);
}
private void fusionner(int[] tab, int debut, int centre, int
fin) {
int g = centre;
int d = centre + 1;
while (debut <= g && d <= fin) {
if (tab[debut] < tab[d]) {
debut++;
} else {
// Decallage
int temp = tab[d];
for (int i = d - 1; debut <= i; i--) {
tab[i + 1] = tab[i];
}
tab[debut] = temp;
17
debut++;
g++;
d++;
}
}
}
Tri par interclassement monotone
À l’époque où on utilisait encore des bandes magnétiques, le « tri par
interclassement monotone » avait ses adeptes. Comme son nom l’indique, ce
tri se base sur la « monotonie » dans l’ordonnancement des éléments d’une
liste.
Pour dérouler cet algorithme, on répète deux phases jusqu’à ce que ce soit trié,
en utilisant des bandes magnétiques ou des zones mémoire temporaires « A »
et « B ».
Durant la phase « 1 », on recopie les éléments de la bande magnétique initiale
« I » vers la bande « A » tant que les éléments sont croissants. On copie vers la
bande « B » dès qu’ils sont décroissants. On continue sur la bande « B » jusqu’à
ce qu’il y ait une décroissance en reprenant alors sur la bande « A ». Pour
simplifier, on recopie les éléments sur une bande et on change de bande à
chaque décroissance.
Durant la phase « 2 », on fusionne les bandes « A » et « B » sur la bande « I » en
copiant toujours le plus petit élément en tête des bandes.
Je vous vois venir ; on n’en est plus à l’époque des bandes magnétiques. Cela
dit, le temps de transfert d’un tableau de la mémoire centrale aux caches CPU,
comparé à celui d’une liste chaînée, dont les éléments sont dispersés dans la
mémoire, se pose exactement dans les mêmes termes. Et puis, on peut utiliser
ce type de tri pour trier des gros fichiers dans un environnement contraint en
mémoire puisqu’il n’y a besoin d’ouvrir que deux flux de sortie et un d’entré,
ainsi qu’une paire de variables.
Ce tri fonctionne relativement bien sur des listes mélangées. Son
fonctionnement est presque magique quand on s’amuse à le dérouler sur
papier. Mais c’est avec les listes en partie triées qu’il dévoile tout son potentiel.
La connaissance, même faible, qu’on peut avoir à l’avance sur les données est
donc très importante.
Fig. 12 : Tri par interclassement monotone
Pour la complexité, très sommairement, il y a donc « n » lectures et « n »
écritures par phases. La longueur des sous-suites monotones est TRES
grossièrement « 2 » puis « 4 » puis « 8 », bref, on va avoir « lg(n) » étapes
(même raisonnement que pour un arbre). C’est assez approximatif, mais disons
que dans le pire des cas, c’est en « O(n lg n) » et que, dans le cas de listes
partiellement triées, c’est quasi linéaire en « O(n) ».
18
Fig. 13 : Tri par interclassement monotone (liste quasi triée)
Je vous invite à lire mon blogvii
pour en savoir un peu plus sur le « tri par
interclassement monotone » et découvrir quelques unes de ses variantes.
Positionnement direct (tri sans comparaison)
Parfois, on connait à l’avance la composition de la liste. Imaginons par exemple
que la liste soit composée des membres de la Suite de Fibonacci : 1, 1, 2, 3, 5, 8,
13, 21… Avec cette information en poche, on ne va pas perdre notre temps à
trier la liste puisque chaque élément porte intrinsèquement sa position. Par
exemple, on sait que l’élément « 13 » doit aller à la position « 6 ».
La puissance des machines là-dedans
D’après la loi de Mooreviii
, et en simplifiant, la puissance des processeurs double
tous les 18 mois. Les traitements doivent donc aller deux fois plus vite. C’est
vrai, mais c’est sans compter que quantité de données augmente également.
Imaginons qu’elle ne fait que doubler sur la même période. Instinctivement, on
se dit que ça double la quantité de calcul et que sera compensé par
l’augmentation de puissance, mais c’est trompeur.
Pour le comprendre, je vais devoir vous infliger encore un petit peu de math.
Prenons un algorithme en « O(n²) » comme le « tri par sélection ». On double la
quantité « n » de données et on divise par deux le temps de calcul pour que ce
soit cohérent avec l’augmentation de puissance. On aura donc « (2n)²/2 = 4n²/2
= 2n² », ce qui est évidement plus grand que « n² ». Doubler la puissance ne
suffit donc pas à compenser l’augmentation du volume de données et on ne
peut donc pas se reposer sur l’amélioration des matériels. Il faut donc choisir
avec attention son algorithme et l’adapter à son contexte.
D’ailleurs, les constructeurs de processeurs ne font plus la course à la vitesse ;
on n’essaie plus d’avoir des Méga Hertz, pour plein de bonnes raisons
technologiques. À la place, on préfère multiplier le nombre de cœurs. L’avenir
des tris complexes passe donc sans aucun doute par le parallélisme offert dans
les processeurs multicœurs. Des algorithmes comme le « Quick Sort » seront les
premiers à en profiter.
Conclusion
Alors, c’est quoi, le meilleur tri ? En fin d’année, Benny Scetbun répondait à une
interviewix
à propos de l’école « 42 ». Dans cette interview, il explique que le
« Quick Sort » était autrefois considéré (c’est malheureusement encore
enseigné à l’école) comme le meilleur algorithme de tri. Il précise toutefois que
cela dépend du contexte.
En effet, les listes qu’on doit manipuler sont statistiquement déjà ordonnées,
tout simplement parce qu’on les a déjà triées la veille, l’avant-veille, etc. Les
seuls éléments mal ordonnés sont ceux en queue (fig. 14), qu’on a ajoutés
depuis le dernier tri. Du coup, le « Quick sort » est un bon algorithme en théorie,
mais un mauvais dans la pratique. L’appliquer sur une liste déjà en partie triée le
place dans son pire cas de complexité en « O(n²) ».
19
Fig. 14 : Distribution croissante sauf en queue
Notez que la bonne stratégie à appliquer dans cet exemple aurait été de ne trier
que les données ajoutées récemment puis d’employer un algorithme similaire
au « tri par insertion », que je vous ai présenté au début de cet article, pour les
positionner correctement.
Combinaisons de tri
Les études des complexités servent d’une part à martyriser les étudiants en
école d’info, et d’autre part à savoir quand et comment utiliser les algorithmes.
Une des conséquences est qu’il peut être intéressant de combiner plusieurs
algorithmes.
Par exemple, la méthode de tri incluse dans le langage en Java (mon langage de
prédilection) garantit un tri stable avec des performances en « O(n lg n) », ce qui
est relativement honnête. Java utilise une combinaison d’algorithmes en
fonction de la taille des listes. En dessous de « 7 » éléments, ça utilise un « tri
par insertion ». Entre « 7 » et « 40 » éléments, ça emploie un « Quick Sort avec
une médiane de trois ». Pour les listes plus grosses, ça utilise un « Quick Sort
avec une médiane de neuf ». Les « médianes » désignent des variantes
classiques sur le choix du pivot. Notez que les langages évoluent. Ainsi, depuis la
version 7, Java utilise un « Quick Sort avec double pivot » pour les listes de plus
de « 7 » éléments.
Fig. 15 : Évolution du nombre d'opérations
Après avoir autant discuté de complexité durant cet article, on pourrait
s’étonner de ce choix dans le JDK. En effet, le « tri rapide » est en « O(n lg n) »
alors que le « tri par insertion » est en « O(n²) ». Or, quand « n » vaut « 6 », le
premier vaut « 15 » et le second « 36 », ce qui est bien supérieur. Mais
raisonner de cette façon est en réalité une erreur. D’abord, il ne faut pas oublier
que, même s’il est vrai que la complexité du « Quick Sort » est en « O(n lg n) »
dans le cas favorable, elle tombe en « O(n²) » dans le pire des cas (lorsque la
liste ou la sous-liste est triée).
Ensuite, la complexité doit être considérée uniquement pour des grandes
valeurs de « n ». Lorsqu’on manipule des listes petites, il faut dénombrer le
nombre réel d’opérations. Pour le « tri par insertion », la formule est « n*(n-
1)/2 » dans le pire des cas, comme on l’avait calculé un peu plus tôt. Et il se
trouve justement que ça donne « 15 » lorsque « n » vaut « 6 »… Sur le graphe
(fig. 15), on observe que les deux courbes se croisent entre les positions
d’abscisses « 6 » et « 7 ». Enfin, il faudra noter qu’on retrouve statistiquement
de nombreuses sous-séquences déjà ordonnées dans les grandes listes.
Tri ou presque
La course au meilleur algorithme est un sujet vraiment important pour certaines
entreprises. Un bon tri peut même s’apparenter à un avantage concurrentiel
20
sérieux. Dans certaines situations, on n’a pourtant pas réellement besoin d’un
résultat parfait.
Fig. 16 : Distribution quasi croissante
Reprenons l’exemple du photographe avec lequel on a commencé cet article. Il
doit trier les enfants selon leurs tailles pour bien composer la photo de classe.
Mais en réalité, il ne va pas s’amuser à mesurer chaque élève. Il fait ça au jugé.
Et s’il s’est un peu trompé dans l’ordre final (fig. 15), on pourrait parier que ça
ne se verra pas.
Tri du dormeur
Pour finir, et parce que je devine que vos yeux se ferment, je voudrais vous
présenter un algorithme de circonstance puisqu’il s’agit du « tri du dormeur ».
Ce tri a un nom qui me fait rigoler, d’autant qu’il le porte bien, car son principe
de fonctionnement consiste précisément à dormir…
Pour chaque élément de la liste, on lance un processus indépendant (un thread),
qui se met en sommeil (pause) pour une durée égale à la valeur de l’élément.
Par exemple, pour la valeur « 13 », le processus dort durant « 13 »
millisecondes. Lorsque le processus se réveille, on ajoute directement son
élément (« 13 » dans l’exemple) en queue d’une seconde liste. L’air de rien, ça
marche super bien (mille éléments traités en une seconde) pour des listes dont
les éléments n’ont pas des valeurs trop élevées. Le « tri du dormeur » nécessite
toutefois une quantité astronomique de mémoire et une expérience forte en
synchronisation multithread.
Finalement
Bref. On arrive à la fin de cet article, pour de bon. On aurait aussi pu discuter
d’autres algorithmes de tri célèbres comme le « tri par base » (« Radix sort »), le
« tri par tas » (« Heap sort »), le « tri par dénombrement », le « tri par paquets »
(« Bucket sort ») ou encore le « tri shell » dont le principe repose sur des
séquences (Pratt, Papernov-Stasevich, Sedgewick) et qui est monstrueusement
efficace sur des tableaux presque triés. En guise de conclusion, je vous invite à
les découvrir sur le Web, car ils valent le coup d’œil.
Algorithme Complexité Caractéristique
Sélection O(n²) Interne, stable, sur place
Insertion +O(n) / -O(n²) Interne, stable, sur place
Bulle O(n²) Interne, non stable, sur place
Rapide +O(n lg n) / -
O(n²)
Interne, non stable, sur place
Fusion O(n lg n) Interne/externe, stable, pas sur
place
Interclassement
monotone
O(n lg n)
Dormeur O(a n)
Base O(2c (n+k)) Interne, stable, pas sur place
Tas O(n lg n) Interne, non stable, sur place
Dénombrement O(2 (n+k)) Interne, stable, pas sur place
Paquets O(a n) Interne, stable, pas sur place
Shell Sedgewick O(n4/3
) Interne, stable, sur place
En fait, il n’existe pas d’algorithme de tri qu’on puisse considérer comme le
meilleur. On pourrait généraliser cela en informatique par le fait qu’il n’y a pas
de solution magique qui marche dans tous les cas. Par contre, on connait des
réponses qui fonctionnent relativement bien dans des situations spécifiques.
21
Quand on est développeur, il est indispensable de savoir comment le langage
(Java, Lisp…), la base de données (Oracle, MySql, Mongo…) ou encore le
progiciel (Kyriba, Excel…) trie les données. Il faut surtout savoir quand utiliser les
mécanismes fournis et quand les fuir.
Thierry Leriche-Dessirier
Architecte JEE freelance / Team leader / Professeur à l’ESIEA
http://www.icauda.com
Merci à Étienne Neveu, Fabien Marsaud, Nicolas Tupegabet (Podcast Sciencex
)
et Olivier Durin pour avoir participé à cet article.
i
Algorithme sur Wikipedia : http://fr.wikipedia.org/wiki/Algorithme
ii
On aurait pu écrire le même article dans un autre langage avec finalement assez peu de
retouches.
iii
3T : http://icauda.com/articles.php#3t
iv
La belote se joue à 4 joueurs avec un paquet de 32 cartes, soit 8 pour chaque joueur.
v
Population mondiale estimée au 1
er
juillet 2014 : 7 226 376 025 personnes.
vi
http://fr.wikipedia.org/wiki/Loi_de_Murphy
vii
http://blog.developpez.com/todaystip/p11899/dev/tri-par-insertion-monotonie
viii
http://fr.wikipedia.org/wiki/Loi_de_Moore
ix
Interview de Benny Scetbun : http://nicotupe.fr/Blog/2013/09/42-interview-de-benny-
scetbun/
x
Podcast Science : http://www.podcastscience.fm
PUBLICITE
Test DISC Essentiel gratuit
« Comportement et de Communication »
http://www.profil4.com/disc-essentiel.php

Más contenido relacionado

La actualidad más candente

récursivité algorithmique et complexité algorithmique et Les algorithmes de tri
récursivité algorithmique et complexité algorithmique et Les algorithmes de trirécursivité algorithmique et complexité algorithmique et Les algorithmes de tri
récursivité algorithmique et complexité algorithmique et Les algorithmes de triYassine Anddam
 
Cours algorithmique et complexite complet
Cours algorithmique et complexite completCours algorithmique et complexite complet
Cours algorithmique et complexite completChahrawoods Dmz
 
Travaux Dirigés : Algorithmique et Structure de Données
Travaux Dirigés : Algorithmique et Structure de DonnéesTravaux Dirigés : Algorithmique et Structure de Données
Travaux Dirigés : Algorithmique et Structure de DonnéesAnass41
 
Devoirs Algorithme + correction pour 4 si
Devoirs Algorithme + correction pour 4 siDevoirs Algorithme + correction pour 4 si
Devoirs Algorithme + correction pour 4 siNarûtö Bàl'Sèm
 
algo et complexité .pptx
algo et complexité  .pptxalgo et complexité  .pptx
algo et complexité .pptxtarekjedidi
 
Chapitre1: Langage Python
Chapitre1: Langage PythonChapitre1: Langage Python
Chapitre1: Langage PythonAziz Darouichi
 
Chap4 Récursivité en python
Chap4 Récursivité en pythonChap4 Récursivité en python
Chap4 Récursivité en pythonMariem ZAOUALI
 
Cours Algorithme: Matrice
Cours Algorithme: MatriceCours Algorithme: Matrice
Cours Algorithme: MatriceInforMatica34
 
Chapitre 5 arbres binaires
Chapitre 5 arbres binairesChapitre 5 arbres binaires
Chapitre 5 arbres binairesSana Aroussi
 
Les algorithmes recurrents
Les algorithmes recurrentsLes algorithmes recurrents
Les algorithmes recurrentsmohamed_SAYARI
 
resume algo 2023.pdf
resume algo 2023.pdfresume algo 2023.pdf
resume algo 2023.pdfsalah fenni
 
Fonctions chaine
Fonctions chaineFonctions chaine
Fonctions chaineAfef Ilahi
 
Java cours n° 2 - classe-objet-constructeur
Java   cours n° 2 - classe-objet-constructeurJava   cours n° 2 - classe-objet-constructeur
Java cours n° 2 - classe-objet-constructeurAbdelwahab Naji
 
Algorithmique et Structures de Données II
Algorithmique et Structures de Données IIAlgorithmique et Structures de Données II
Algorithmique et Structures de Données IIRiadh Bouslimi
 

La actualidad más candente (20)

récursivité algorithmique et complexité algorithmique et Les algorithmes de tri
récursivité algorithmique et complexité algorithmique et Les algorithmes de trirécursivité algorithmique et complexité algorithmique et Les algorithmes de tri
récursivité algorithmique et complexité algorithmique et Les algorithmes de tri
 
Cours algorithmique et complexite complet
Cours algorithmique et complexite completCours algorithmique et complexite complet
Cours algorithmique et complexite complet
 
Travaux Dirigés : Algorithmique et Structure de Données
Travaux Dirigés : Algorithmique et Structure de DonnéesTravaux Dirigés : Algorithmique et Structure de Données
Travaux Dirigés : Algorithmique et Structure de Données
 
Devoirs Algorithme + correction pour 4 si
Devoirs Algorithme + correction pour 4 siDevoirs Algorithme + correction pour 4 si
Devoirs Algorithme + correction pour 4 si
 
COURS_PYTHON_22.ppt
COURS_PYTHON_22.pptCOURS_PYTHON_22.ppt
COURS_PYTHON_22.ppt
 
algo et complexité .pptx
algo et complexité  .pptxalgo et complexité  .pptx
algo et complexité .pptx
 
Formation python 3
Formation python 3Formation python 3
Formation python 3
 
02 correction-td smi-s3-algo2
02 correction-td smi-s3-algo202 correction-td smi-s3-algo2
02 correction-td smi-s3-algo2
 
Chapitre1: Langage Python
Chapitre1: Langage PythonChapitre1: Langage Python
Chapitre1: Langage Python
 
Chap4 Récursivité en python
Chap4 Récursivité en pythonChap4 Récursivité en python
Chap4 Récursivité en python
 
Cours Algorithme: Matrice
Cours Algorithme: MatriceCours Algorithme: Matrice
Cours Algorithme: Matrice
 
Chapitre 5 arbres binaires
Chapitre 5 arbres binairesChapitre 5 arbres binaires
Chapitre 5 arbres binaires
 
Algo tri
Algo triAlgo tri
Algo tri
 
Les algorithmes recurrents
Les algorithmes recurrentsLes algorithmes recurrents
Les algorithmes recurrents
 
resume algo 2023.pdf
resume algo 2023.pdfresume algo 2023.pdf
resume algo 2023.pdf
 
Fonctions chaine
Fonctions chaineFonctions chaine
Fonctions chaine
 
Java cours n° 2 - classe-objet-constructeur
Java   cours n° 2 - classe-objet-constructeurJava   cours n° 2 - classe-objet-constructeur
Java cours n° 2 - classe-objet-constructeur
 
Serie
SerieSerie
Serie
 
Les enregistrements
Les enregistrements Les enregistrements
Les enregistrements
 
Algorithmique et Structures de Données II
Algorithmique et Structures de Données IIAlgorithmique et Structures de Données II
Algorithmique et Structures de Données II
 

Destacado

INF220 - Algo DUT SRC1 - Cours 1
INF220 - Algo DUT SRC1 - Cours 1INF220 - Algo DUT SRC1 - Cours 1
INF220 - Algo DUT SRC1 - Cours 1PGambette
 
Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...
Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...
Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...Pascale Chavaz Bengoa
 
20 nombres de power point 2013 compu 2.
20 nombres de power point 2013 compu 2.20 nombres de power point 2013 compu 2.
20 nombres de power point 2013 compu 2.robertoMonroy
 
Information metier hotellerie restauration personnel d'étage www.hotellerie r...
Information metier hotellerie restauration personnel d'étage www.hotellerie r...Information metier hotellerie restauration personnel d'étage www.hotellerie r...
Information metier hotellerie restauration personnel d'étage www.hotellerie r...Emploi Hotellerie Restauration
 
Phrase déclarative et phrase interrogative
Phrase déclarative et phrase interrogativePhrase déclarative et phrase interrogative
Phrase déclarative et phrase interrogativeritaestephan
 
Conseils pour l'achat de sa literie lematelas.fr
Conseils pour l'achat de sa literie lematelas.frConseils pour l'achat de sa literie lematelas.fr
Conseils pour l'achat de sa literie lematelas.frLematelas.fr
 
Histoire Pondichéry 2014
Histoire Pondichéry 2014Histoire Pondichéry 2014
Histoire Pondichéry 2014salleherodote
 
ô Catherine
ô Catherineô Catherine
ô Catherinefirstsite
 
Recours excès de pouvoir collège peyrouny v3.1 remarques avocat peiriguers +...
Recours excès de pouvoir  collège peyrouny v3.1 remarques avocat peiriguers +...Recours excès de pouvoir  collège peyrouny v3.1 remarques avocat peiriguers +...
Recours excès de pouvoir collège peyrouny v3.1 remarques avocat peiriguers +...blogVAP
 
Cérémonie de remise du Prix 2015 de la presse territoriale
Cérémonie de remise du Prix 2015 de la presse territorialeCérémonie de remise du Prix 2015 de la presse territoriale
Cérémonie de remise du Prix 2015 de la presse territorialeNastassja Korichi
 
French project abc final draft
French project abc final draftFrench project abc final draft
French project abc final draftjennifermyers4
 
Jean luc boeuf - Séance 6 - Droit administratif et institutions locales - Eu...
Jean luc boeuf  - Séance 6 - Droit administratif et institutions locales - Eu...Jean luc boeuf  - Séance 6 - Droit administratif et institutions locales - Eu...
Jean luc boeuf - Séance 6 - Droit administratif et institutions locales - Eu...Jean Luc Boeuf
 
Présentation carrière leo propre
Présentation carrière leo proprePrésentation carrière leo propre
Présentation carrière leo propreleo-francois17
 
Destination 100 % Evénements ! Programme des animations d'hiver à Sainte-Maxime
Destination 100 % Evénements ! Programme des animations d'hiver à Sainte-MaximeDestination 100 % Evénements ! Programme des animations d'hiver à Sainte-Maxime
Destination 100 % Evénements ! Programme des animations d'hiver à Sainte-MaximeSainte-Maxime Tourisme
 
Anatole france le jardin d'epicure
Anatole france le jardin d'epicureAnatole france le jardin d'epicure
Anatole france le jardin d'epicureNivaldo Freitas
 

Destacado (20)

Coursasdch3
Coursasdch3Coursasdch3
Coursasdch3
 
INF220 - Algo DUT SRC1 - Cours 1
INF220 - Algo DUT SRC1 - Cours 1INF220 - Algo DUT SRC1 - Cours 1
INF220 - Algo DUT SRC1 - Cours 1
 
Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...
Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...
Neutralité axiologique et autorégulation : regard sur l'intervention en éthiq...
 
20 nombres de power point 2013 compu 2.
20 nombres de power point 2013 compu 2.20 nombres de power point 2013 compu 2.
20 nombres de power point 2013 compu 2.
 
Diaporama isp v8 fr
Diaporama isp v8 frDiaporama isp v8 fr
Diaporama isp v8 fr
 
Information metier hotellerie restauration personnel d'étage www.hotellerie r...
Information metier hotellerie restauration personnel d'étage www.hotellerie r...Information metier hotellerie restauration personnel d'étage www.hotellerie r...
Information metier hotellerie restauration personnel d'étage www.hotellerie r...
 
Phrase déclarative et phrase interrogative
Phrase déclarative et phrase interrogativePhrase déclarative et phrase interrogative
Phrase déclarative et phrase interrogative
 
Conseils pour l'achat de sa literie lematelas.fr
Conseils pour l'achat de sa literie lematelas.frConseils pour l'achat de sa literie lematelas.fr
Conseils pour l'achat de sa literie lematelas.fr
 
Histoire Pondichéry 2014
Histoire Pondichéry 2014Histoire Pondichéry 2014
Histoire Pondichéry 2014
 
ô Catherine
ô Catherineô Catherine
ô Catherine
 
Quelques
QuelquesQuelques
Quelques
 
Recours excès de pouvoir collège peyrouny v3.1 remarques avocat peiriguers +...
Recours excès de pouvoir  collège peyrouny v3.1 remarques avocat peiriguers +...Recours excès de pouvoir  collège peyrouny v3.1 remarques avocat peiriguers +...
Recours excès de pouvoir collège peyrouny v3.1 remarques avocat peiriguers +...
 
Cérémonie de remise du Prix 2015 de la presse territoriale
Cérémonie de remise du Prix 2015 de la presse territorialeCérémonie de remise du Prix 2015 de la presse territoriale
Cérémonie de remise du Prix 2015 de la presse territoriale
 
French project abc final draft
French project abc final draftFrench project abc final draft
French project abc final draft
 
Jean luc boeuf - Séance 6 - Droit administratif et institutions locales - Eu...
Jean luc boeuf  - Séance 6 - Droit administratif et institutions locales - Eu...Jean luc boeuf  - Séance 6 - Droit administratif et institutions locales - Eu...
Jean luc boeuf - Séance 6 - Droit administratif et institutions locales - Eu...
 
Exp2 e leglae_open_data
Exp2 e leglae_open_dataExp2 e leglae_open_data
Exp2 e leglae_open_data
 
Présentation carrière leo propre
Présentation carrière leo proprePrésentation carrière leo propre
Présentation carrière leo propre
 
Destination 100 % Evénements ! Programme des animations d'hiver à Sainte-Maxime
Destination 100 % Evénements ! Programme des animations d'hiver à Sainte-MaximeDestination 100 % Evénements ! Programme des animations d'hiver à Sainte-Maxime
Destination 100 % Evénements ! Programme des animations d'hiver à Sainte-Maxime
 
Anatole france le jardin d'epicure
Anatole france le jardin d'epicureAnatole france le jardin d'epicure
Anatole france le jardin d'epicure
 
Analyse japon leonard ss14 final
Analyse japon leonard ss14 finalAnalyse japon leonard ss14 final
Analyse japon leonard ss14 final
 

Similar a Les algorithmes de tri

Apprentissage Automatique et moteurs de recherche
Apprentissage Automatique et moteurs de rechercheApprentissage Automatique et moteurs de recherche
Apprentissage Automatique et moteurs de recherchePhilippe YONNET
 
Visite guidée au pays de la donnée - Traitement automatique des données
Visite guidée au pays de la donnée - Traitement automatique des donnéesVisite guidée au pays de la donnée - Traitement automatique des données
Visite guidée au pays de la donnée - Traitement automatique des donnéesGautier Poupeau
 
Architecture Logiciel_GRASP11111111.pptx
Architecture Logiciel_GRASP11111111.pptxArchitecture Logiciel_GRASP11111111.pptx
Architecture Logiciel_GRASP11111111.pptxafamanalafa2001
 
TP Fouille de données (Data Mining) et Apprentissage Machine
TP Fouille de données (Data Mining) et Apprentissage MachineTP Fouille de données (Data Mining) et Apprentissage Machine
TP Fouille de données (Data Mining) et Apprentissage MachineBoubaker KHMILI
 
ALT.NET Modéliser Parallèle avec C# 4.0
ALT.NET Modéliser Parallèle avec C# 4.0ALT.NET Modéliser Parallèle avec C# 4.0
ALT.NET Modéliser Parallèle avec C# 4.0Bruno Boucard
 
Les 10 plus populaires algorithmes du machine learning
Les 10 plus populaires algorithmes du machine learningLes 10 plus populaires algorithmes du machine learning
Les 10 plus populaires algorithmes du machine learningHakim Nasaoui
 
Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"
Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"
Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"Fing
 
cours d'algorithmique et programmation 3sc final .pdf
cours d'algorithmique et programmation 3sc final .pdfcours d'algorithmique et programmation 3sc final .pdf
cours d'algorithmique et programmation 3sc final .pdfLamissGhoul1
 
569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signal569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signalMohammed moudine
 
الملف التربوي الرقمي
الملف التربوي الرقميالملف التربوي الرقمي
الملف التربوي الرقميMouez Babba
 
Quel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_problemeQuel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_problemePaul Blondel
 
Quel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_problemeQuel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_problemePaul Blondel
 
569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signal569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signalOussema Ben Khlifa
 
Parlez-vous le langage IA ? 30 notions pour comprendre l'IA
Parlez-vous le langage IA ? 30 notions pour comprendre l'IAParlez-vous le langage IA ? 30 notions pour comprendre l'IA
Parlez-vous le langage IA ? 30 notions pour comprendre l'IABigBrain Evolution
 

Similar a Les algorithmes de tri (20)

Apprentissage Automatique et moteurs de recherche
Apprentissage Automatique et moteurs de rechercheApprentissage Automatique et moteurs de recherche
Apprentissage Automatique et moteurs de recherche
 
Le Machine Learning... tous aux fourneaux !
Le Machine Learning... tous aux fourneaux !Le Machine Learning... tous aux fourneaux !
Le Machine Learning... tous aux fourneaux !
 
Visite guidée au pays de la donnée - Traitement automatique des données
Visite guidée au pays de la donnée - Traitement automatique des donnéesVisite guidée au pays de la donnée - Traitement automatique des données
Visite guidée au pays de la donnée - Traitement automatique des données
 
Architecture Logiciel_GRASP11111111.pptx
Architecture Logiciel_GRASP11111111.pptxArchitecture Logiciel_GRASP11111111.pptx
Architecture Logiciel_GRASP11111111.pptx
 
Cours de Génie Logiciel / ESIEA 2016-17
Cours de Génie Logiciel / ESIEA 2016-17Cours de Génie Logiciel / ESIEA 2016-17
Cours de Génie Logiciel / ESIEA 2016-17
 
TP Fouille de données (Data Mining) et Apprentissage Machine
TP Fouille de données (Data Mining) et Apprentissage MachineTP Fouille de données (Data Mining) et Apprentissage Machine
TP Fouille de données (Data Mining) et Apprentissage Machine
 
ALT.NET Modéliser Parallèle avec C# 4.0
ALT.NET Modéliser Parallèle avec C# 4.0ALT.NET Modéliser Parallèle avec C# 4.0
ALT.NET Modéliser Parallèle avec C# 4.0
 
Debuteraveclesmls
DebuteraveclesmlsDebuteraveclesmls
Debuteraveclesmls
 
Les 10 plus populaires algorithmes du machine learning
Les 10 plus populaires algorithmes du machine learningLes 10 plus populaires algorithmes du machine learning
Les 10 plus populaires algorithmes du machine learning
 
Julien dollon
Julien dollonJulien dollon
Julien dollon
 
Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"
Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"
Nos Systèmes by Fing : "Kit d'auto-évaluation des algorithmes"
 
cours d'algorithmique et programmation 3sc final .pdf
cours d'algorithmique et programmation 3sc final .pdfcours d'algorithmique et programmation 3sc final .pdf
cours d'algorithmique et programmation 3sc final .pdf
 
569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signal569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signal
 
الملف التربوي الرقمي
الملف التربوي الرقميالملف التربوي الرقمي
الملف التربوي الرقمي
 
Quel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_problemeQuel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_probleme
 
Quel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_problemeQuel algo ml_pour_mon_probleme
Quel algo ml_pour_mon_probleme
 
8 gl1
8 gl18 gl1
8 gl1
 
Cour algo
Cour algoCour algo
Cour algo
 
569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signal569036 bases-de-traitement-du-signal
569036 bases-de-traitement-du-signal
 
Parlez-vous le langage IA ? 30 notions pour comprendre l'IA
Parlez-vous le langage IA ? 30 notions pour comprendre l'IAParlez-vous le langage IA ? 30 notions pour comprendre l'IA
Parlez-vous le langage IA ? 30 notions pour comprendre l'IA
 

Más de Thierry Leriche-Dessirier

Rapport de test DISC de Groupe (Laurent Duval)
Rapport de test DISC de Groupe (Laurent Duval)Rapport de test DISC de Groupe (Laurent Duval)
Rapport de test DISC de Groupe (Laurent Duval)Thierry Leriche-Dessirier
 
Management en couleur avec DISC à Agile Tour Paris 2015
Management en couleur avec DISC à Agile Tour Paris 2015Management en couleur avec DISC à Agile Tour Paris 2015
Management en couleur avec DISC à Agile Tour Paris 2015Thierry Leriche-Dessirier
 

Más de Thierry Leriche-Dessirier (20)

Disc l'essentiel
Disc l'essentielDisc l'essentiel
Disc l'essentiel
 
Rapport DISC Pro de Lucie Durand
Rapport DISC Pro de Lucie DurandRapport DISC Pro de Lucie Durand
Rapport DISC Pro de Lucie Durand
 
Rapport de test DISC de Groupe (Laurent Duval)
Rapport de test DISC de Groupe (Laurent Duval)Rapport de test DISC de Groupe (Laurent Duval)
Rapport de test DISC de Groupe (Laurent Duval)
 
Memento DISC Influent (English)
Memento DISC Influent (English)Memento DISC Influent (English)
Memento DISC Influent (English)
 
Le management en couleurs avec le DISC
Le management en couleurs avec le DISCLe management en couleurs avec le DISC
Le management en couleurs avec le DISC
 
Memento DISC Stable
Memento DISC StableMemento DISC Stable
Memento DISC Stable
 
Memento DISC Influent
Memento DISC InfluentMemento DISC Influent
Memento DISC Influent
 
Memento DISC Dominant
Memento DISC DominantMemento DISC Dominant
Memento DISC Dominant
 
Memento Disc Consciencieux
Memento Disc ConsciencieuxMemento Disc Consciencieux
Memento Disc Consciencieux
 
Management en couleur avec DISC à Agile Tour Paris 2015
Management en couleur avec DISC à Agile Tour Paris 2015Management en couleur avec DISC à Agile Tour Paris 2015
Management en couleur avec DISC à Agile Tour Paris 2015
 
Management en couleur avec DISC
Management en couleur avec DISCManagement en couleur avec DISC
Management en couleur avec DISC
 
Puzzle 2 (4x4)
Puzzle 2 (4x4)Puzzle 2 (4x4)
Puzzle 2 (4x4)
 
Guava et Lombok au Normandy JUG
Guava et Lombok au Normandy JUGGuava et Lombok au Normandy JUG
Guava et Lombok au Normandy JUG
 
Guava et Lombok au Lyon JUG
Guava et Lombok au Lyon JUGGuava et Lombok au Lyon JUG
Guava et Lombok au Lyon JUG
 
Guava et Lombok au Lorraine JUG
Guava et Lombok au Lorraine JUGGuava et Lombok au Lorraine JUG
Guava et Lombok au Lorraine JUG
 
Guava et Lombok au Brezth JUG
Guava et Lombok au Brezth JUGGuava et Lombok au Brezth JUG
Guava et Lombok au Brezth JUG
 
Guava au Paris JUG
Guava au Paris JUGGuava au Paris JUG
Guava au Paris JUG
 
Memento scrum-equipe
Memento scrum-equipeMemento scrum-equipe
Memento scrum-equipe
 
Memento java
Memento javaMemento java
Memento java
 
Guava et Lombok au Bordeaux JUG
Guava et Lombok au Bordeaux JUGGuava et Lombok au Bordeaux JUG
Guava et Lombok au Bordeaux JUG
 

Les algorithmes de tri

  • 1. 1 Les algorithmes de tri En passant à la machine à café, vous avez sans doute déjà croisé des développeurs. Vous avez sans doute constaté qu’ils ont l'air passionnés par leur métier, ce qui rend leurs discussions animées. Et vous les avez sans doute entendu prononcer des mots comme « Bubble », « Quicksort », « logarithme » ou encore « complexité », qui semblent provenir d'une autre langue. Ce sont pourtant des notions primordiales en programmation. Dans cet article, nous allons tenter de démystifier ce charabia. « Bubble », « Quicksort », « Insertion » ou encore « Fusion » (pour ne citer qu’eux) sont les noms donnés à des algorithmes de tri célèbres. Wikipedia indiquei qu’un algorithme est « une suite finie et non ambigüe d’opérations ou d'instructions permettant de résoudre un problème ». Un algorithme de tri est donc la « recette de cuisine » permettant à un logiciel de ranger les éléments d’une liste. On veut trier une liste lorsqu’on pense que ses éléments sont dans le désordre, ou plus précisément dans un ordre qui ne nous convient pas. L’objectif du tri, en tant qu’algorithme, est de mettre les éléments dans le bon ordre. Trier une liste suppose donc qu’on est capable d’établir une relation d’ordre entre ses éléments (ce qui n’est pas toujours simple et/ou possible). Par exemple, on peut dire si Nicolas est (objectivement) plus petit que David. Quand je parle de « plus petit », je fais instinctivement référence à la taille des personnes, mais on aurait tout aussi bien pu comparer leurs poids, leurs quantités de cheveux, leurs notes au baccalauréat, etc. Mais pour simplifier, disons juste qu’on se base sur le critère de la taille. Dans cet article, nous n’allons traiter que des entiers pour simplifier la discussion. La relation d’ordre sera donc établie. « il faut choisir l’algorithme adapté au contexte… » Si ce sujet revient souvent chez les développeurs, c’est parce que le besoin de trier des données est omniprésent dans les programmes informatiques. Par habitude ou par inattention, on ne réalise pas qu’on manipule sans arrêt des données triées. Par exemple, disons qu’on s’intéresse à votre compte bancaire. Sur votre relevé, les opérations sont triées par date. Quand vous souscrivez à l’option Web, vous avez également la possibilité de trier selon les entêtes des colonnes (montant, date, numéro d’opération, bénéficiaire, type, etc.), mais vous ne vous êtes probablement jamais dit qu’il y avait un algorithme efficace caché derrière cette fonctionnalité. En Javaii , on dispose de deux structures de données intéressantes à trier : les listes et les tableaux. Les algorithmes à base de listes sont plus faciles à programmer, mais sont généralement moins performants. Les tableaux demandent parfois de se retourner le cerveau pour coller à l’esprit de l’algorithme, mais le jeu en vaut la chandelle. C’est donc à ces derniers que nous allons nous intéresser. Dans cet article, nous allons discuter d’une poignée d’algorithmes. Pour que l’ensemble soit cohérent, je vous propose de créer l’interface « Tri » qui définit la méthode « trier() ». Cette méthode ne renvoie rien. Elle prend un tableau en paramètre et le tri « sur place ». public interface Tri { void trier(final int[] tab); ... Il faut préciser qu’il existe des bons et des mauvais algorithmes de tri. Quand on tri une petite liste et qu’on le fait rarement, même le plus mauvais des algorithmes fera l’affaire. Par contre, quand on manipule des listes conséquentes et/ou qu’on les trie souvent, il faut choisir avec attention l’algorithme qui sera le plus adapté au contexte. On teste en trois temps Avant d’entrer dans le vif du sujet, nous allons écrire des tests simples, à l’aide de la bibliothèque JUnit, qui nous permettront de vérifier de manière automatique que les programmes fonctionnent correctement. Cela s’inscrit
  • 2. 2 dans une démarche qualité. Pour cela, nous allons employer la méthode 3T (Tests en Trois Tempsiii ) qui s’inspire du TDD (Test Driven Development). Durant l’écriture de cet article, cette démarche a permis d’identifier des erreurs qui seraient très certainement passées inaperçues sans. Tous les algorithmes doivent passer par la même moulinette de tri. On peut donc en écrire une version « abstract », comme suit, dont devront hériter toutes les classes de test : public abstract class AbstractTriTest { protected Tri tri; @Test public void testTriTabVide() { final int[] tab = {}; doTestTri(tab); } @Test public void testTriTabUnSeulElement() { final int[] tab = { 1 }; doTestTri(tab); } @Test public void testTriTabDeuxElements() { final int[] tab = { 2, 1 }; doTestTri(tab); } ... @Test public void testTriTabMelange() { final int[] tab = { 8, 6, 3, 9, 2, 1, 4, 5, 7 }; doTestTri(tab); } @Test public void testTriTabQuasiTrie() { final int[] tab = { 1, 2, 3, 4, 5, 6, 9, 8, 7 }; doTestTri(tab); } ... protected void doTestTri(final int[] tab) { tri.trier(tab); int temp = Integer.MIN_VALUE; for (final int elt : tab) { Assert.assertTrue(temp <= elt); temp = elt; } } Ici, l’idée est de multiplier les cas de tests. Vous trouverez de nombreux autres tests dans le code source proposé avec cet article sur le site Web de Programmez. Un des tests (non présenté ici) a consisté à générer, selon diverses distributions (aléatoire, croissante, décroissante, quasi croissante, gaussienne, sinusoïdale, etc.), des fichiers (des petits et des gros, voire très gros) contenant des listes à trier. En entreprise, on entend souvent dire (à tort ou à raison) qu’il faut utiliser les fonctionnalités du langage pour trier des tableaux. Cela va donc nous fournir un premier algorithme de référence. On commence bien entendu par la classe de test qui sera relativement simple : public class JavaTriTest extends AbstractTriTest { @Before public void doBefore() { tri = new JavaTri(); } Dans la suite de cet article, les classes de test des autres algorithmes seront toujours calquées sur ce modèle. Elles ne seront donc pas présentées. « il faut utiliser les fonctionnalités du langage… » La classe qui met ça en pratique sera également très simple puisqu’il suffit de faire appel aux méthodes déjà existantes : public class JavaTri implements Tri { @Override public void trier(final int[] tab) { Arrays.sort(tab); }
  • 3. 3 Les premières manières qui nous viennent à l'esprit Tri par sélection Sans le savoir, où sans s’en rendre compte, on est confronté très jeune à des algorithmes de tri. Je dirais que cela se produit au plus tard en petite section de maternelle, vers l’âge de trois ans, à l’occasion du passage du photographe scolaire. Pour bien composer sa photo, le photographe doit placer les enfants sur le banc en fonction de leurs tailles. Pour cela, le photographe passe en revue les enfants à la recherche du plus petit. Une fois trouvé, il le place sur le banc. Il réitère cette opération sur les enfants restant en plaçant à chaque fois l’enfant sélectionné à la prochaine place disponible sur le banc. L’algorithme se termine lorsqu’il ne reste plus d’enfant. Si vous avez bien suivi, vous aurez compris que le dernier enfant à être sélectionné, et donc à être placé sur le banc, est mécaniquement le plus grand. Cet algorithme aurait pu s’appeler le « tri du photographe », mais on le trouve dans la littérature sous le nom de « tri par sélection ». Fig. 1 : Photo de classe. Crédit : Puyo / Podcast Science Fig. 2 : Tri par sélection
  • 4. 4 Pour programmer ce tri sur un tableau, il suffit donc de le parcourir à la recherche du plus petit élément (ou du plus grand, ce qui revient au même) et de le positionner au prochain emplacement disponible : public class SelectionTri implements Tri { @Override public void trier(final int[] tab) { for (int i = 0; i < tab.length; i++) { // Chercher la position (pos) du plus petit dans la zone restante int pos = i; for (int j = i; j < tab.length; j++) { if (tab[j] < tab[pos]) { pos = j; } } final int petit = tab[pos]; // Delaler de i a pos for (int j = pos; i < j; j--) { tab[j] = tab[j - 1]; } // mettre le plus petit trouve a la fin tab[i] = petit; } } Dans la suite, on notera « n » le nombre d’éléments dans la liste à classer. Dans l’exemple, « n » correspond donc au nombre d’enfants dans la classe. À titre d’illustration, disons qu’il y en a dix-huit (cf. dessins). Pour sélectionner le plus petit, le photographe doit donc passer en revue les 18 élèves, ce qui nécessite de faire 17 comparaisons. Pour le deuxième, il reste 17 élèves à comparer, nécessitant 16 comparaisons. Pour le troisième il faudra faire 15 comparaisons, et ainsi de suite… Au final, le photographe va donc faire « 17+16+15+…+1 » comparaisons, ce qui est un peu long à calculer. Heureusement, on fait un peu de math au lycée. Ça peut se factoriser à l’aide de la formule « n*(n-1) /2 », ce qui donne « 153 » comparaisons pour une classe de dix-huit bambins. À la fin de la journée, le photographe doit donc avoir un sacré mal de tête. Et encore, il s’agit d’une classe relativement peu chargée. En école d’ingénieur, nous étions 150 dans ma promotion. Le photographe devrait donc faire plus de onze mille (11175) comparaisons pour préparer sa photo à l’aide du tri par sélection : autant dire qu’il y passerait la journée. Sans vendre la mèche, on devine déjà que le « tri par sélection » n’est pas réputé pour être très efficace. Tri par insertion Lorsqu’on est enfant, il y a un autre algorithme de tri qu’on apprend (ou découvre) assez vite. C’est celui que les joueurs de cartes utilisent pour organiser leurs mains. Le joueur pioche la carte qui est en haut du tas et la place (l’insère) à la bonne position dans la main. Cet algorithme se nomme le « tri par insertion ». En première approche, le « tri par insertion » est donc l’inverse du « tri par sélection ». Il est toutefois un peu plus efficace. Et, sans surprise, le code du programme est très proche de celui qu’on a déjà vu : public class InsertionTri implements Tri { @Override public void trier(final int[] tab) { int pos; // On peut commencer a 1 directement... for (int i = 1; i < tab.length; i++) { final int temp = tab[i]; // recherche position for (pos = 0; pos < i; pos++) { if (temp < tab[pos]) { break; } } // Decaler for (int j = i; pos < j; j--) { tab[j] = tab[j - 1]; } // Insertion tab[pos] = temp; } }
  • 5. 5 Fig. 3 : Tri par insertion Si on accepte de chercher la bonne position en partant de la queue, on peut en profiter pour réaliser le décalage du même coup. for (int i = 1; i < tab.length; i++) { final int temp = tab[i]; // Recherche et decalage d'un seul coup for (pos = i; 0 < pos && temp < tab[pos - 1]; pos--) { tab[pos] = tab[pos - 1]; } // Insertion tab[pos] = temp; } Décider si on lance la recherche à partie de la tête ou de la queue n’est pas anodin. Selon que la liste initiale est plutôt croissante ou décroissante, l’une ou l’autre des recherches sera plus efficace. Et puisqu'on en est à se demander dans quel sens parcourir la liste, on pourrait aussi démarrer depuis la dernière position utilisée. Il suffit ensuite de monter ou de descendre le curseur selon le résultat des comparaisons et de s'arrêter lorsque ça s'inverse. Cela permet d'optimiser en moyenne le traitement des sous-séquences. Dans le pire des cas, cela sera équivalent à la recherche par une extrémité. À titre d’illustration, prenons de jeu de la Beloteiv dans lequel chaque joueur reçoit huit cartes. Ici « n » vaudra donc « 8 ». Pour la première carte, il n’y a rien à faire puisque la main est vide. Pour la deuxième carte, il y a une comparaison à faire avec l’unique carte déjà en main pour savoir si elle doit aller à gauche ou à droite. Pour la troisième carte, il y aura « au maximum » deux comparaisons à effectuer. Il est important de comprendre qu’on arrête de faire des comparaisons dès que la bonne position est identifiée. On fait ainsi tant qu’il reste des cartes à piocher. Au final, et dans le « pire des cas », on fera donc « 1+2+…+7 » comparaisons, soit « 28 » au total. C’est la même formule (à l’envers) de calcul que pour le « tri par sélection » et ce n’est donc pas mieux. En revanche, dans le « meilleur des cas » où le joueur pioche les cartes dans l’ordre idéal (ie. triées dans l’ordre croissant ou décroissant selon qu’on insère par la tête ou par la queue), il n’y aura que 7 comparaisons. Et bien entendu, plus le nombre « n » est grand et plus la différence est sensible.
  • 6. 6 Complexité Comme vous l’avez surement déjà deviné, le nombre total d’opérations à effectuer pour « dérouler » un algorithme est un critère majeur pour dire si l’algorithme est efficace. Cela permet surtout de comparer l’efficacité de deux algorithmes et de choisir le plus adapté. Il est relativement « simple » de dénombrer « précisément » le nombre d’opérations nécessaires pour « dérouler » un algorithme de tri lorsque le nombre d’éléments « n » dans la liste est petit. Par exemple, on a vu qu’il faudra réaliser « 153 » comparaisons pour trier « 18 » enfants à l’aide du « tri par sélection ». Le même algorithme nécessite de réaliser « 11.175 » comparaisons pour trier une classe de « 150 » élèves, et « 499.500 » pour trier les mille employés d’une PME. Et si vous vouliez trier les « 81.338 » spectateurs d’un match de foot au Stade de France, il faudrait « 3.307.894.453 » comparaisons. Ces exemples mettent deux choses en évidence. Premièrement, la quantité d’opérations nécessaire au déroulement du « tri par sélection » croît rapidement lorsque le nombre « n » d’éléments dans la liste augmente. Deuxièmement, il n’est pas nécessaire d’avoir une grande précision sur des chiffres aussi importants. Quand on réalise « 3.307.894.453 » comparaisons, on n’est plus à une comparaison près, ni à dix, ou cent, ou même cent millions. D’ailleurs on aurait pu se contenter de dire qu’il fallait « environ trois milliards » d’opérations. Ce qui compte, avec des valeurs si conséquentes, c’est d’avoir un « ordre de grandeur », même vague. C’est ce qu’on va appeler la « complexité » d’un algorithme. En fait, il n’est pas possible de discuter des tris sans parler de « complexité ». Ce mot à lui seul est synonyme de cauchemars pour bien des étudiants en école d’info. Et pour ma part, je dois avouer que j’ai mis longtemps à en comprendre les tenants et les aboutissants. Je me propose donc de vous l’expliquer simplement, en prenant quelques raccourcis. Ne vous inquiétez pas si tout n’est pas clair, ça va se clarifier au fur et à mesure. « discuter des tris impose d’aborder la complexité… » La complexité d’un tri de « n » éléments se note avec un « omicron » : dites « grand O ». Par exemple, la complexité du « tri par sélection » sera en « O(n*(n- 1) /2 ) », où « n*(n-1) /2 » est la formule qu’on avait utilisée un peu plus tôt. Comme on s’intéresse à des valeurs importantes et qu’on ne veut qu’un « ordre de grandeur », on considèrera que cette « complexité » peut se « simplifier » en « O(n*(n-1)) », qui peut elle-même se simplifier en « O(n²) ». On dira que l’algorithme du « tri par sélection » est de « complexité quadratique » en « O(n²) ». Pour faire simple, quand vous multipliez par « 10 » le nombre « n » d’éléments, vous multipliez par « 100 » le nombre d’opérations à réaliser lorsque vous utilisez un algorithme de tri en « O(n²) ». Dit comme ça, ce n’est pas très impressionnant alors essayons d’associer cela avec des durées pour voir à quel point c’est mauvais. Disons qu’une opération prenne une nanoseconde (ns) en moyenne (ce qui est très optimiste). Nombre d’éléments « n » Nombre d’opérations pour un tri en « O(n²) » Durée pour un tri en « O(n²) » 10 100 100 ns 100 10 000 10 us 1 000 1 000 000 1 ms 10 000 100 000 000 100 ms 100 000 10 000 000 000 10 s 1 000 000 1 000 000 000 000 16 min 40 s 10 000 000 100 000 000 000 000 27 heures 100 000 000 10 000 000 000 000 000 115 jours 1 000 000 000 1 000 000 000 000 000 000 31 ans Alors que le tri de « 10 » éléments prendra seulement « 100 » nanosecondes à l’aide d’un algorithme en « O(n²) », il faudra « 10 » microsecondes pour une centaine d’éléments et carrément une milliseconde pour mille. Et ces temps augmentent encore quand le nombre d’éléments augmente. Ca augmente même très vite.
  • 7. 7 Pour un milliard d’éléments, ce qui reste raisonnable pour un programme informatique, notamment dans le domaine de la finance, on aura besoin d’un tiers de siècle. Autant dire que les données seront périmées depuis longtemps quand l’algorithme aura fini son travail. L’ordinateur sera peut-être même déjà passé à la poubelle. Et je ne vous parle même pas de la facture d’électricité qu’on aurait tendance à oublier (coût en énergie, en ressource naturelle, en entretien et en Euro). Pour bien réaliser ce que ça représente, prenons un exemple plus concret. Pour trier la population mondialev , soit un peu plus de sept milliards de personnes (au début de l’été 2014), il faudrait plus d’un millier et demi d’années (1655 ans environ). Pour que le tri soit fini au moment où vous lisez cet article, il aurait donc fallu le commencer en l’an 350 de notre ère, soit plus d’un siècle avant le début du moyen âge (476 à 1453). On parle de complexité en se plaçant dans la situation la plus défavorable pour l’algorithme. Ça permet en quelque sorte de majorer. On peut aussi se placer dans le cas le plus favorable ou dans le cas moyen. Par exemple et sans plus d’explications, le « tri par insertion » sera de « complexité linéaire » en « O(n) » dans le cas le plus favorable et en « O(n²) » dans les cas moyens et défavorables. Bon, je ne vais pas vous embêter plus que ça avec cette histoire de complexité. J’ai passé sous silence certains aspects (gestion de la mémoire, déplacements, permutations, création d’objets, flux, etc.) qui peuvent avoir un impact en terme de performance, en fonction de la plateforme et du langage utilisé, mais je pense sincèrement que l’essentiel y est… pour l’instant… En plus de la question des performances, on a pris l’habitude de classer (cf. tableau récapitulatif en fin d’article) les algorithmes de tri selon des caractéristiques importantes, comme la stabilité ou le fait de pouvoir trier sur place, c’est-à-dire qu’on réarrange les éléments directement dans la structure initiale. Un algorithme est dit « stable » lorsqu’il ne modifie pas l’ordre des éléments de même valeur. Enfin, une des principales contraintes concerne l’espace mémoire. Plus spécifiquement, on se demande si on est capable de trier l’ensemble des données en mémoire centrale. On dira que le tri est « interne » si c’est possible, et « externe » sinon. Comment faire mieux ? On devine qu’il va falloir trouver comment faire mieux, notamment en utilisant des algorithmes beaucoup plus rapides. Rassurez-vous, je ne vais pas vous présenter tous les algorithmes de tri qui existent. Je vais me limiter aux plus connus en insistant sur leurs points forts et leurs faiblesses. Tri à bulle (Bubble Sort) Le tri à bulle est certainement celui qu’on apprend à programmer en premier en école d’informatique, avant même le « tri par sélection » et le « tri par insertion ». Pour effectuer un « tri à bulle », il faut parcourir la liste en permutant les éléments contigus qui ne sont pas dans le bon ordre. Par exemple, si je trouve « 9 » dans la case « 4 » et seulement « 2 » dans la case « 5 », alors j’échange leurs positions. Quand c’est fini, l’élément le plus grand se retrouve donc en queue de liste. Cet élément est arrivé à sa bonne position, mais le reste de la liste est encore potentiellement en désordre. On recommence autant de fois qu’il y a d’éléments dans la liste, ce qui fait donc à chaque fois remonter le plus grand élément restant.
  • 8. 8 Fig. 4 : Tri à bulle Le nom du « tri à bulle » vient du fait que les plus gros éléments remontent comme des bulles dans une flute de champagne. D’ailleurs, on n’utilise jamais le « tri à bulle », car les bulles remontent très lentement. public class BulleTri implements Tri { @Override public void trier(final int[] tab) { for (int i = 0; i < tab.length - 1; i++) { for (int j = 1; j < tab.length; j++) { if (tab[j] < tab[j - 1]) { // Permutation permuter(j, j - 1, tab); } } } } public static void permuter(final int indexA, final int indexB, final int[] tab) { final int temp = tab[indexA]; tab[indexA] = tab[indexB]; tab[indexB] = temp; } Pour une liste dans laquelle le nombre d’éléments « n » est de « 9 », on fera « 8 » (ie. « n-1 ») comparaisons pour mettre le plus grand élément à sa bonne position. Et on va répéter cela « 8 » fois également, ce qui nécessitera donc « 64 » comparaisons. La complexité associée au « tri à bulle » sera donc en « O((n-1)²) » qu’on simplifiera en « O(n²) ». On peut améliorer l’algorithme de base. En effet, à chaque passage, un élément supplémentaire se retrouve à sa place définitive. Il n’est donc plus nécessaire de l’inclure dans les comparaisons suivantes. for (int i = 0; i < tab.length - 1; i++) { for (int j = 1; j < tab.length - i; j++) { if (tab[j] < tab[j - 1]) { permuter(j, j - 1, tab); On n’aura alors besoin que de « (n-1)*n/2 » comparaisons, soit « 36 » comparaisons pour une liste de « 9 » éléments. La complexité restera toutefois
  • 9. 9 en « O(n²) », le multiplicateur « ½ » ne changeant rien à l’histoire pour les grandes valeurs, comme on l’a déjà vu. « accusé d’être lent, le tri à bulle cache bien son jeu… » En outre, on peut stopper le déroulement de l’algorithme dès lors qu’on détecte qu’aucune permutation n’a été réalisée durant une boucle. Si la liste s’y prête, cela peut devenir une sérieuse optimisation. public class RapideDemiBulleTri implements Tri { @Override public void trier(int[] tab) { for (int i = 0; i < tab.length; i++) { boolean permutation = false; for (int j = 1; j < tab.length - i; j++) { if (tab[j] < tab[j - 1]) { // Permutation permuter(j, j - 1, tab); permutation = true; } } if (!permutation) { break; } } } Le « tri à bulle » est souvent accusé d’être lent, mais comme vous pouvez le voir, il cache bien son jeu. À titre personnel, je dois avouer que j’ai beaucoup d’affection pour cet algorithme, même en sachant que ce n’est pas le meilleur. Tri rapide (Quick Sort) S’il y a un algorithme de tri dont vous avez forcément entendu parler à la machine à café, c’est bien le « Quick sort ». C’est l’un des algorithmes les plus utilisés et certainement celui qui présente le plus de variantes. Le « Quick Sort », aussi appelé « tri rapide », fait partie de la famille d’algorithme dont le fonctionnement repose sur le principe « diviser pour régner ». Pour réaliser un « tri rapide », on doit choisir un élément dans la liste, qu’on appelle « pivot ». On divise ensuite la liste en deux sous-listes. La première, à gauche, contient les éléments inférieurs au pivot. La seconde, à droite, contient les éléments supérieurs au pivot. « vous avez forcément entendu parler du Quick Sort… » On reproduit alors récursivement ce choix du pivot et la division sur les listes de gauche et de droite précédemment construites jusqu’à n’avoir que des sous- listes de zéro ou un élément. Pour finir, il suffit de rassembler les éléments de toutes les sous-listes dans l’ordre gauche-droite. Fig. 5 : Tri rapide (pivot en tête) Le principe du « tri rapide » est donc très simple : tout se passe pendant la construction de l’arbre. En revanche, il est réellement (très) difficile de programmer cet algorithme dans la version utilisant des tableaux. Plus précisément, c’est la séparation en fonction du pivot qui pose problème à de nombreux développeurs. On trouve d’ailleurs de nombreux codes faux (qui ne passent pas mes tests) sur Internet. Je vous propose donc de commencer en douceur avec une version employant des listes.
  • 10. 10 public class RapideViaListeTri implements Tri { private List<Integer> trier(final List<Integer> liste) { if (liste == null || liste.isEmpty()) { return new ArrayList<>(); } // Choix du pivot a gauche final int pivot = liste.get(0); // pour ne pas traiter le pivot dans la boucle liste.remove(0); final List<Integer> listeGauche = new ArrayList<>(); final List<Integer> listeDroite = new ArrayList<>(); // Separation en deux sous listes for (final int elt : liste) { if (elt < pivot) { listeGauche.add(elt); } else { listeDroite.add(elt); } } final List<Integer> result = new ArrayList<>(); result.addAll(trier(listeGauche)); result.add(pivot); result.addAll(trier(listeDroite)); return result; } À la lecture de ce code, vous avez sans doute repéré plusieurs problèmes techniques. La méthode fonctionne correctement (elle passe tous mes tests), mais elle est lente. Il y a fondamentalement trois « erreurs ». D’abord de nombreuses listes sont créées et manipulées durant le traitement, ce qui coute cher. Ensuite, l’auto-unboxing réalisé lors de l’itération sur la (sous) liste à trier ajoute encore un peu à l’addition. Et l’auto-boxing lors de l’ajout aux sous-listes finit de « plomber » la note. Cela peut se résoudre en travaillant directement sur des objets, et donc sur des références, en les comparant à l’aide de « compareTo() » en lieu et place de l’opérateur de comparaison. Enfin, l’algorithme ne prend pas en compte la présence (éventuelle) de plusieurs valeurs égales au pivot. Quitte à avoir deux sous-listes, on peut bien en gérer une troisième pour ce cas spécifique. Le nombre total d’éléments restera inchangé et le coût du test supplémentaire sera largement compensé par le gain en nombre de récursions. public class RapideViaListe2Tri implements Tri { private List<Integer> trier(final List<Integer> liste) { if (liste == null || liste.isEmpty()) { return new ArrayList<>(); } // Pivot a gauche final Integer pivot = liste.get(0); final List<Integer> listeGauche = new ArrayList<>(); final List<Integer> listeCentre = new ArrayList<>(); final List<Integer> listeDroite = new ArrayList<>(); // Separation en deux sous listes for (final Integer elt : liste) { final int comp = elt.compareTo(pivot); if (comp < 0) { listeGauche.add(elt); } else if (comp == 0) { listeCentre.add(elt); } else { listeDroite.add(elt); } } liste.clear(); liste.addAll(trier(listeGauche)); liste.addAll(listeCentre); liste.addAll(trier(listeDroite)); return liste; } Voici maintenant une première version utilisant des tableaux. L’intérêt de cette structure est de pouvoir effectuer le tri « sur place », en manipulant des primitifs. public class RapideTri implements Tri { @Override public void trier(final int[] tab) {
  • 11. 11 trier(tab, 0, tab.length - 1); } private void trier(final int[] tab, final int gauche, final int droite) { if (droite <= gauche) { return; } // Pivot à gauche int positionPivot = gauche; permuter(positionPivot, droite, tab); for (int i = gauche; i < droite; i++) { if (tab[i] <= tab[droite]) { permuter(i, positionPivot++, tab); } } permuter(droite, positionPivot, tab); // tri recursif des sous-listes // Bien entendu on ne tri pas le pivot trier(tab, gauche, positionPivot - 1); trier(tab, positionPivot + 1, droite); } Comme vous le constatez, cette première version utilisant des tableaux est bien plus complexe. On arrive toutefois encore à distinguer la forme de base de l’algorithme. Un des gros défauts, difficiles à corriger, de cette version est de devoir gérer le décalage d’un grand nombre de cases. Pour gagner encore un peu en vitesse, on va devoir passer par des fonctions de « System », qui font des manipulations de la mémoire de façon native. public class RapideTri2 implements Tri { @Override public void trier(final int[] tab) { if (tab.length < 2) { return; } int[] tab2 = new int[tab.length]; trier(tab, tab2, 0, tab.length); System.arraycopy(tab2, 0, tab, 0, tab.length); } private void trier(final int[] tab, final int[] tab2, final int gauche, final int droite) { // Choix du pivot a gauche final int pivot = tab[gauche]; int posGauche = gauche; int posDroite = droite; // Separation en deux sous listes for (int index = gauche + 1; index < droite; index++) { int elt = tab[index]; if (elt < pivot) { tab2[posGauche++] = elt; } else if (elt > pivot) { tab2[--posDroite] = elt; } } Arrays.fill(tab2, posGauche, posDroite, pivot); if (posGauche > gauche) { trier(tab2, tab, gauche, posGauche); System.arraycopy(tab, gauche, tab2, gauche, posGauche - gauche); } if (posDroite < droite) { trier(tab2, tab, posDroite, droite); System.arraycopy(tab, posDroite, tab2, posDroite, droite - posDroite); } } Pour en finir avec le « tri rapide », je vous propose une version simplifiée de ce qu’on peut lire dans les sources du JDK 6. Dans cette version, il faut regarder avec attention pour bien distinguer la structure de l’algorithme de base. public class RapideCopyJdkTri implements Tri { @Override public void trier(final int[] tab) { if (tab.length == 0) { return; } trier(tab, 0, tab.length); } private static void trier(final int tab[], final int gauche, final int taille) { final int pivot = tab[gauche];
  • 12. 12 int g = gauche; int g2 = g; int d = gauche + taille - 1; int d2 = d; while (true) { while (g2 <= d && tab[g2] <= pivot) { if (tab[g2] == pivot) { permuter(g++, g2, tab); } g2++; } while (d >= g2 && pivot <= tab[d]) { if (tab[d] == pivot) { permuter(d, d2--, tab); } d--; } if (g2 > d) { break; } permuter(g2++, d--, tab); } // Swap partition elements back to middle int s, n = gauche + taille; s = Math.min(g - gauche, g2 - g); decaler(tab, gauche, g2 - s, s); s = Math.min(d2 - d, n - d2 - 1); decaler(tab, g2, n - s, s); // Recursively sort non-partition-elements if (1 < (s = g2 - g)) { trier(tab, gauche, s); } if (1 < (s = d2 - d)) { trier(tab, n - s, s); } } public static void decaler(final int tab[], int a, int b, int n) { for (int i = 0; i < n; i++, a++, b++) { permuter(a, b, tab); } } Les gains de chaque version pourraient sembler minimes, en comparaison de l’investissement, mais ils sont réellement importants. Pour bien se rendre compte des différences de performance, voici les temps de traitement moyens observés sur mon ordinateur portable, équipé d’un JDK 7. Algo 1 000 d’éléments 1 000 000 d’éléments RapideViaListeTri 15 ms 8 s RapideViaListe2Tri 13 ms 829 ms RapideTri 1 ms 909 ms Rapide2Tri - 52 ms RapideCopyJdkTri - 61 ms JavaTri (JDK 7) - 48 ms On constate que la seconde version à base de liste (RapideViaListe2Tri) est dix fois plus rapide que la première (RapideViaListeTri) pour un million d’éléments à trier, ce qui est loin d’être négligeable. La première version à base de tableau (RapideTri) est presque aussi rapide que la seconde version à base de liste (RapideViaListe2Tri). La seconde version à base de tableaux (Rapide2Tri) est, quant à elle, dix-sept fois plus rapide que la première. Seule la méthode de Java 7 (JavaTriTest), qui est hybride, parvient à faire mieux. Ici, on voit aussi que mon ordinateur est loin de pouvoir effectuer les opérations élémentaires en une nanoseconde puisqu’on se serait alors attendu à une durée de l’ordre de « 20 ms » pour un million d’éléments. Cela est dû en partie à la puissance de la machine et en partie au fait qu’on a du mal à coller au plus près de l’algorithme théorique optimal. « le choix du bon pivot dépend de la distribution attendue… » Une des grosses difficultés du « Quick Sort » réside dans le choix du bon pivot, le risque étant de déséquilibrer les sous-listes qu’on va construire. Quand on n’a pas de meilleure idée et faute de mieux, on peut prendre le premier élément (fig. 5).
  • 13. 13 Fig. 6 : Tri rapide (pivot en tête) sur liste quasi triée Lorsqu’on soupçonne que les données sont déjà à peu près triées et que le choix du pivot en première position provoquera un déséquilibre (fig. 6), on peut aussi préférer choisir le pivot au milieu (fig. 7). Et quand on n’a aucune information sur les données, on peut également choisir le pivot de manière aléatoire (fig. 7). Un mathématicien vous dirait que ça permet de maximiser « l’espérance » que ça se passe bien. Personnellement, je n’aime pas trop cette solution. Elle donne souvent de bons résultats, mais le déroulement de l’algorithme est alors aléatoire. Or, en bon informaticien, je n’aime pas trop quand c’est non reproductible. En outre, ça laisse la porte ouverte pour toutes les combinaisons où ça peut mal tourner. Or, en informatique, comme l’énonce la loi de Murphyvi , tout ce qui peut mal tourner va mal tourner. Fig. 7 : Tri rapide (pivot au centre)
  • 14. 14 Fig. 8 : Tri rapide (pivot aléatoire) Comme son nom l’indique, le « Quick Sort » a la réputation d’être rapide, ce qui est mérité, mais parfois un peu usurpé. Pour le comprendre, il va encore falloir parler de complexité. Fig. 9 : Distribution aléatoire Dans le cas favorable où les éléments sont uniformément distribués, c’est-à-dire bien mélangées (fig. 9), l’algorithme produit un arbre équilibré avec « 2 » sous- listes de taille « n/2 » à chaque appel récursif, soit « n » opérations à chaque étape (une étape est un nœud de l’arbre ou une feuille). À l’étape « 1 », on aura donc « 2 » sous-listes. À l’étape « 2 », on aura « 2*2 = 22 » sous listes. À l’étape « 3 », on aura « 2*2*2=23 » sous listes, et ainsi de suite. À l’étape « p », on aura « 2*2*2*…*2=2p » sous listes. L’algorithme récursif s’arrête lorsqu’on arrive à un seul élément. S’il faut « p » étapes pour cela, on pourra donc dire que « n=2p ». La fonction mathématique qui permet de calculer la valeur de « p » quand on connait « n » est le « logarithme de n en base 2 » qu’on note « log2(n) » ou « lg(n) » en raccourci. Au final, la complexité du « tri rapide » sera donc en « O(n lg n) ». Fig. 10 : Distribution croissante Dans le cas défavorable où les éléments sont déjà triés (fig. 10), l’algorithme produit un arbre complètement déséquilibré, ressemblant à une longue tige tordue et ne permettant de trouver la position que d’un seul élément à chaque étape. On aura donc une complexité équivalente à celle du « tri par sélection » en « O(n²) ».
  • 15. 15 Si on raisonne de nouveau en terme de durée, on se rend compte qu’il y a une véritable différence entre un algorithme de tri en « O(n lg n) » et un autre en « O(n²) ». Nombre d’éléments « n » Durée pour un tri en « O(n lg n) » Durée pour un tri en « O(n²) » 10 33 ns 100 ns 100 664 ns 10 us 1 000 10 us 1 ms 10 000 132 us 100 ms 100 000 1,6 ms 10 s 1 000 000 20 ms 16 min 40 s 10 000 000 233 ms 27 heures 100 000 000 2,5 s 115 jours 1 000 000 000 29 s 31 ans Population mondiale 4 min 1 655 ans Souvenez-vous. Un peu plus tôt, je vous ai dit qu’il fallait « 100 ns » pour trier une liste de « 10 » éléments à l’aide d’un algorithme en « O(n²) » et d’un bon ordinateur. Si on se place dans un cas en « O(n lg n) », il ne faudra plus que « 33 ns ». Bon, la différence ne saute pas aux yeux. Pour un million d’éléments, on passe de « 16 minutes » à « 20 ms », ce qui reste encore dans les limites du temps réel. Et pour trier la population mondiale, on passe de « 1655 ans » à seulement « 4 minutes ». Notez qu’il existe une optimisation du « Quick sort » assez contre-intuitive. Elle préconise de mélanger la liste avant de la trier. L’idée est de se rapprocher de la complexité en « O(n lg n) » du cas favorable où la liste est complètement mélangée, quitte à dépenser un « O(n) » à préparer la liste. Tri fusion (Merge Sort) Dans la grande famille des tris reposant sur le principe « diviser pour régner », on trouve également le « tri Fusion ». À mon sens, le « tri Fusion » est au « tri rapide » ce que le « tri par insertion » est au « tri par sélection ». Dans cette famille de tri, on travaille toujours en trois phases. D’abord on divise. Ensuite on règne. Et pour finir, on réconcilie (fusionne). Alors que, pour le « Quick Sort », tout se fait à la construction de l’arbre, pour le « tri fusion », la partie intéressante se situe lors de la phase de réconciliation. Pour réaliser un « tri fusion », on commence par diviser la liste à trier en deux sous-listes de même taille. On réitère récursivement cette opération jusqu’à n’avoir que des listes d’un seul élément. Cela produit donc un arbre à peu près équilibré. On remonte ensuite dans l’arbre en fusionnant les sous-listes à chaque étape. Pour cela, on prend le plus petit élément qui se présente en tête des deux sous-listes à fusionner et on recommence tant qu’il reste des éléments. Quand on revient à la « racine » de l’arbre, la liste est triée. Fig. 11 : Tri fusion
  • 16. 16 Le coût des divisions récursives est quasi gratuit. Il est systématique et ne demande de réaliser aucune comparaison entre les éléments. Si vous avez compris ce qu’on avait dit pour la complexité du « quick Sort », vous avez certainement déjà deviné que la complexité du « tri fusion » sera en « O(n lg n) » dans tous les cas puisqu’on force la production d’un arbre équilibré. « le tri fusion force la production d’un arbre équilibré… » Comme pour le « Quick sort », il est plus simple de programmer l’algorithme du « tri Fusion » en commençant par une liste, pour bien le prendre en main. public class FusionViaListeTri implements Tri { private List<Integer> trier(final List<Integer> liste) { if (liste.size() <= 1) { return liste; } // Separation en deux sous listes final int posCentre = liste.size() / 2; List<Integer> listeGauche = liste.subList(0, posCentre); List<Integer> listeDroite = liste.subList(posCentre, liste.size()); // Tri des deux sous liste listeGauche = trier(listeGauche); listeDroite = trier(listeDroite); // Fusion final List<Integer> result = new ArrayList<>(liste.size()); final Iterator<Integer> iterGauche = listeGauche.iterator(); final Iterator<Integer> iterDroite = listeDroite.iterator(); Integer g = next(iterGauche); Integer d = next(iterDroite); while (g != null || d != null) { if (d == null || g != null && g.compareTo(d) < 0) { result.add(g); g = next(iterGauche); } else { result.add(d); d = next(iterDroite); } } return result; } private static Integer next(final Iterator<Integer> iter) { return (iter.hasNext()) ? iter.next() : null; } Pour réaliser le même traitement sur un tableau, il faudra décaler les éléments lors de la phase de fusion des zones triées de gauche et de droite. public class FusionTri implements Tri { @Override public void trier(int[] tab) { trier(tab, 0, tab.length - 1); } private void trier(int[] tab, int debut, int fin) { if (fin <= debut) { return; } // appels recursifs final int centre = (debut + fin) / 2; trier(tab, debut, centre); trier(tab, centre + 1, fin); // Fusion fusionner(tab, debut, centre, fin); } private void fusionner(int[] tab, int debut, int centre, int fin) { int g = centre; int d = centre + 1; while (debut <= g && d <= fin) { if (tab[debut] < tab[d]) { debut++; } else { // Decallage int temp = tab[d]; for (int i = d - 1; debut <= i; i--) { tab[i + 1] = tab[i]; } tab[debut] = temp;
  • 17. 17 debut++; g++; d++; } } } Tri par interclassement monotone À l’époque où on utilisait encore des bandes magnétiques, le « tri par interclassement monotone » avait ses adeptes. Comme son nom l’indique, ce tri se base sur la « monotonie » dans l’ordonnancement des éléments d’une liste. Pour dérouler cet algorithme, on répète deux phases jusqu’à ce que ce soit trié, en utilisant des bandes magnétiques ou des zones mémoire temporaires « A » et « B ». Durant la phase « 1 », on recopie les éléments de la bande magnétique initiale « I » vers la bande « A » tant que les éléments sont croissants. On copie vers la bande « B » dès qu’ils sont décroissants. On continue sur la bande « B » jusqu’à ce qu’il y ait une décroissance en reprenant alors sur la bande « A ». Pour simplifier, on recopie les éléments sur une bande et on change de bande à chaque décroissance. Durant la phase « 2 », on fusionne les bandes « A » et « B » sur la bande « I » en copiant toujours le plus petit élément en tête des bandes. Je vous vois venir ; on n’en est plus à l’époque des bandes magnétiques. Cela dit, le temps de transfert d’un tableau de la mémoire centrale aux caches CPU, comparé à celui d’une liste chaînée, dont les éléments sont dispersés dans la mémoire, se pose exactement dans les mêmes termes. Et puis, on peut utiliser ce type de tri pour trier des gros fichiers dans un environnement contraint en mémoire puisqu’il n’y a besoin d’ouvrir que deux flux de sortie et un d’entré, ainsi qu’une paire de variables. Ce tri fonctionne relativement bien sur des listes mélangées. Son fonctionnement est presque magique quand on s’amuse à le dérouler sur papier. Mais c’est avec les listes en partie triées qu’il dévoile tout son potentiel. La connaissance, même faible, qu’on peut avoir à l’avance sur les données est donc très importante. Fig. 12 : Tri par interclassement monotone Pour la complexité, très sommairement, il y a donc « n » lectures et « n » écritures par phases. La longueur des sous-suites monotones est TRES grossièrement « 2 » puis « 4 » puis « 8 », bref, on va avoir « lg(n) » étapes (même raisonnement que pour un arbre). C’est assez approximatif, mais disons que dans le pire des cas, c’est en « O(n lg n) » et que, dans le cas de listes partiellement triées, c’est quasi linéaire en « O(n) ».
  • 18. 18 Fig. 13 : Tri par interclassement monotone (liste quasi triée) Je vous invite à lire mon blogvii pour en savoir un peu plus sur le « tri par interclassement monotone » et découvrir quelques unes de ses variantes. Positionnement direct (tri sans comparaison) Parfois, on connait à l’avance la composition de la liste. Imaginons par exemple que la liste soit composée des membres de la Suite de Fibonacci : 1, 1, 2, 3, 5, 8, 13, 21… Avec cette information en poche, on ne va pas perdre notre temps à trier la liste puisque chaque élément porte intrinsèquement sa position. Par exemple, on sait que l’élément « 13 » doit aller à la position « 6 ». La puissance des machines là-dedans D’après la loi de Mooreviii , et en simplifiant, la puissance des processeurs double tous les 18 mois. Les traitements doivent donc aller deux fois plus vite. C’est vrai, mais c’est sans compter que quantité de données augmente également. Imaginons qu’elle ne fait que doubler sur la même période. Instinctivement, on se dit que ça double la quantité de calcul et que sera compensé par l’augmentation de puissance, mais c’est trompeur. Pour le comprendre, je vais devoir vous infliger encore un petit peu de math. Prenons un algorithme en « O(n²) » comme le « tri par sélection ». On double la quantité « n » de données et on divise par deux le temps de calcul pour que ce soit cohérent avec l’augmentation de puissance. On aura donc « (2n)²/2 = 4n²/2 = 2n² », ce qui est évidement plus grand que « n² ». Doubler la puissance ne suffit donc pas à compenser l’augmentation du volume de données et on ne peut donc pas se reposer sur l’amélioration des matériels. Il faut donc choisir avec attention son algorithme et l’adapter à son contexte. D’ailleurs, les constructeurs de processeurs ne font plus la course à la vitesse ; on n’essaie plus d’avoir des Méga Hertz, pour plein de bonnes raisons technologiques. À la place, on préfère multiplier le nombre de cœurs. L’avenir des tris complexes passe donc sans aucun doute par le parallélisme offert dans les processeurs multicœurs. Des algorithmes comme le « Quick Sort » seront les premiers à en profiter. Conclusion Alors, c’est quoi, le meilleur tri ? En fin d’année, Benny Scetbun répondait à une interviewix à propos de l’école « 42 ». Dans cette interview, il explique que le « Quick Sort » était autrefois considéré (c’est malheureusement encore enseigné à l’école) comme le meilleur algorithme de tri. Il précise toutefois que cela dépend du contexte. En effet, les listes qu’on doit manipuler sont statistiquement déjà ordonnées, tout simplement parce qu’on les a déjà triées la veille, l’avant-veille, etc. Les seuls éléments mal ordonnés sont ceux en queue (fig. 14), qu’on a ajoutés depuis le dernier tri. Du coup, le « Quick sort » est un bon algorithme en théorie, mais un mauvais dans la pratique. L’appliquer sur une liste déjà en partie triée le place dans son pire cas de complexité en « O(n²) ».
  • 19. 19 Fig. 14 : Distribution croissante sauf en queue Notez que la bonne stratégie à appliquer dans cet exemple aurait été de ne trier que les données ajoutées récemment puis d’employer un algorithme similaire au « tri par insertion », que je vous ai présenté au début de cet article, pour les positionner correctement. Combinaisons de tri Les études des complexités servent d’une part à martyriser les étudiants en école d’info, et d’autre part à savoir quand et comment utiliser les algorithmes. Une des conséquences est qu’il peut être intéressant de combiner plusieurs algorithmes. Par exemple, la méthode de tri incluse dans le langage en Java (mon langage de prédilection) garantit un tri stable avec des performances en « O(n lg n) », ce qui est relativement honnête. Java utilise une combinaison d’algorithmes en fonction de la taille des listes. En dessous de « 7 » éléments, ça utilise un « tri par insertion ». Entre « 7 » et « 40 » éléments, ça emploie un « Quick Sort avec une médiane de trois ». Pour les listes plus grosses, ça utilise un « Quick Sort avec une médiane de neuf ». Les « médianes » désignent des variantes classiques sur le choix du pivot. Notez que les langages évoluent. Ainsi, depuis la version 7, Java utilise un « Quick Sort avec double pivot » pour les listes de plus de « 7 » éléments. Fig. 15 : Évolution du nombre d'opérations Après avoir autant discuté de complexité durant cet article, on pourrait s’étonner de ce choix dans le JDK. En effet, le « tri rapide » est en « O(n lg n) » alors que le « tri par insertion » est en « O(n²) ». Or, quand « n » vaut « 6 », le premier vaut « 15 » et le second « 36 », ce qui est bien supérieur. Mais raisonner de cette façon est en réalité une erreur. D’abord, il ne faut pas oublier que, même s’il est vrai que la complexité du « Quick Sort » est en « O(n lg n) » dans le cas favorable, elle tombe en « O(n²) » dans le pire des cas (lorsque la liste ou la sous-liste est triée). Ensuite, la complexité doit être considérée uniquement pour des grandes valeurs de « n ». Lorsqu’on manipule des listes petites, il faut dénombrer le nombre réel d’opérations. Pour le « tri par insertion », la formule est « n*(n- 1)/2 » dans le pire des cas, comme on l’avait calculé un peu plus tôt. Et il se trouve justement que ça donne « 15 » lorsque « n » vaut « 6 »… Sur le graphe (fig. 15), on observe que les deux courbes se croisent entre les positions d’abscisses « 6 » et « 7 ». Enfin, il faudra noter qu’on retrouve statistiquement de nombreuses sous-séquences déjà ordonnées dans les grandes listes. Tri ou presque La course au meilleur algorithme est un sujet vraiment important pour certaines entreprises. Un bon tri peut même s’apparenter à un avantage concurrentiel
  • 20. 20 sérieux. Dans certaines situations, on n’a pourtant pas réellement besoin d’un résultat parfait. Fig. 16 : Distribution quasi croissante Reprenons l’exemple du photographe avec lequel on a commencé cet article. Il doit trier les enfants selon leurs tailles pour bien composer la photo de classe. Mais en réalité, il ne va pas s’amuser à mesurer chaque élève. Il fait ça au jugé. Et s’il s’est un peu trompé dans l’ordre final (fig. 15), on pourrait parier que ça ne se verra pas. Tri du dormeur Pour finir, et parce que je devine que vos yeux se ferment, je voudrais vous présenter un algorithme de circonstance puisqu’il s’agit du « tri du dormeur ». Ce tri a un nom qui me fait rigoler, d’autant qu’il le porte bien, car son principe de fonctionnement consiste précisément à dormir… Pour chaque élément de la liste, on lance un processus indépendant (un thread), qui se met en sommeil (pause) pour une durée égale à la valeur de l’élément. Par exemple, pour la valeur « 13 », le processus dort durant « 13 » millisecondes. Lorsque le processus se réveille, on ajoute directement son élément (« 13 » dans l’exemple) en queue d’une seconde liste. L’air de rien, ça marche super bien (mille éléments traités en une seconde) pour des listes dont les éléments n’ont pas des valeurs trop élevées. Le « tri du dormeur » nécessite toutefois une quantité astronomique de mémoire et une expérience forte en synchronisation multithread. Finalement Bref. On arrive à la fin de cet article, pour de bon. On aurait aussi pu discuter d’autres algorithmes de tri célèbres comme le « tri par base » (« Radix sort »), le « tri par tas » (« Heap sort »), le « tri par dénombrement », le « tri par paquets » (« Bucket sort ») ou encore le « tri shell » dont le principe repose sur des séquences (Pratt, Papernov-Stasevich, Sedgewick) et qui est monstrueusement efficace sur des tableaux presque triés. En guise de conclusion, je vous invite à les découvrir sur le Web, car ils valent le coup d’œil. Algorithme Complexité Caractéristique Sélection O(n²) Interne, stable, sur place Insertion +O(n) / -O(n²) Interne, stable, sur place Bulle O(n²) Interne, non stable, sur place Rapide +O(n lg n) / - O(n²) Interne, non stable, sur place Fusion O(n lg n) Interne/externe, stable, pas sur place Interclassement monotone O(n lg n) Dormeur O(a n) Base O(2c (n+k)) Interne, stable, pas sur place Tas O(n lg n) Interne, non stable, sur place Dénombrement O(2 (n+k)) Interne, stable, pas sur place Paquets O(a n) Interne, stable, pas sur place Shell Sedgewick O(n4/3 ) Interne, stable, sur place En fait, il n’existe pas d’algorithme de tri qu’on puisse considérer comme le meilleur. On pourrait généraliser cela en informatique par le fait qu’il n’y a pas de solution magique qui marche dans tous les cas. Par contre, on connait des réponses qui fonctionnent relativement bien dans des situations spécifiques.
  • 21. 21 Quand on est développeur, il est indispensable de savoir comment le langage (Java, Lisp…), la base de données (Oracle, MySql, Mongo…) ou encore le progiciel (Kyriba, Excel…) trie les données. Il faut surtout savoir quand utiliser les mécanismes fournis et quand les fuir. Thierry Leriche-Dessirier Architecte JEE freelance / Team leader / Professeur à l’ESIEA http://www.icauda.com Merci à Étienne Neveu, Fabien Marsaud, Nicolas Tupegabet (Podcast Sciencex ) et Olivier Durin pour avoir participé à cet article. i Algorithme sur Wikipedia : http://fr.wikipedia.org/wiki/Algorithme ii On aurait pu écrire le même article dans un autre langage avec finalement assez peu de retouches. iii 3T : http://icauda.com/articles.php#3t iv La belote se joue à 4 joueurs avec un paquet de 32 cartes, soit 8 pour chaque joueur. v Population mondiale estimée au 1 er juillet 2014 : 7 226 376 025 personnes. vi http://fr.wikipedia.org/wiki/Loi_de_Murphy vii http://blog.developpez.com/todaystip/p11899/dev/tri-par-insertion-monotonie viii http://fr.wikipedia.org/wiki/Loi_de_Moore ix Interview de Benny Scetbun : http://nicotupe.fr/Blog/2013/09/42-interview-de-benny- scetbun/ x Podcast Science : http://www.podcastscience.fm PUBLICITE Test DISC Essentiel gratuit « Comportement et de Communication » http://www.profil4.com/disc-essentiel.php