Algorithmes de tri (1/2) Problématique : étant donné une structure linéaire (tableau, liste, etc) contenant des valeurs d'un type ordonné, il faut trier les éléments en ordre croissant (ou décroissant). Il existe des dizaines d'algorithmes répondant à ce problème, utilisant quelques principes de base auxquels on ajoute des variantes On peut distinguer : les tris internes qui trient les éléments dans la structure qui les contient (tableau, liste, ...) ou au pire utilise une structure annexe de même type les tris externes utilisés lorsque la quantité d'éléments à trier est telle qu'ils ne tiennent pas en mémoire vive et qu'il faut stocker des éléments dans des fichiers ou des tables de bases de données Licence Informatique - Semestre 2 - Algorithmique et Programmation 1 Algorithmes de tri (2/2) Propriétés des algorithmes de tri : complexité en temps et en espace stabilité : l'algorithme ne modifie pas l'ordre initial des éléments égaux par comparaison tri sur place ou pas : l'algorithme n'utilise pas de mémoire supplémentaire (hormis éventuellement quelques indices de boucle ou booléens), les éléments sont triés directement dans la structure de données La plupart des algorithmes de tri utilisent la comparaison d'éléments 2 à 2 on montre que le nombre minimum de comparaisons à effectuer est nlog(n) si n est la taille de la structure. Les algorithmes en O(n2) sont donc peu efficaces, ceux au pire en O(nlog(n)) sont optimaux il existe des tris linéaires, mais ils n'utilisent pas la comparaison et sont limités à des cas très particuliers (tris de nombres dont on connait des propriétés statistiques) Licence Informatique - Semestre 2 - Algorithmique et Programmation 2 Tri à bulle (1/7) Principe du tri à bulle (bubble sort) : l'élément le plus grand remonte au bout du tableau, comme une bulle de champagne, puis on recommence pour que le deuxième plus grand remonte, etc. L'écriture de l'algorithme s'inspire de l'algorithme séquentiel qui vérifie si un tableau est trié. Algorithme : i désigne la dernière case à traiter fonction sans retour triBulle(entier[] tab){ entier i,j,temp; début pour (i allant de tab.longueur-2 à 1 pas -1) faire pour (j allant de 0 à i pas 1) faire si (tab[j] > tab[j+1]) alors temp <- tab[j]; tab[j] <- tab[j+1]; tab[j+1] <- temp; finsi finpour finpour fin Licence Informatique - Semestre 2 - Algorithmique et Programmation 3 Tri à bulle (2/7) Exemple : trier en ordre croissant le tableau suivant 701 | 17 | 2 | 268 | 415 | 45 | 45 | 102 premier tour de la première boucle (sur i) j=0 17 | 701 | 2 | 268 | 415 | 45 | 45 | 102 j=1 17 | 2 | 701 | 268 | 415 | 45 | 45 | 102 j=2 17 | 2 | 268 | 701 | 415 | 45 | 45 | 102 j=3 17 | 2 | 268 | 415 | 701 | 45 | 45 | 102 j=4 17 | 2 | 268 | 415 | 45 | 701 | 45 | 102 j=5 17 | 2 | 268 | 415 | 45 | 45 | 701 | 102 j=6 17 | 2 | 268 | 415 | 45 | 45 | 102 | 701 Licence Informatique - Semestre 2 - Algorithmique et Programmation 4 Tri à bulle (3/7) deuxième tour de la première boucle (sur i) 2 | 17 | 268 | 45 | 45 | 102 | 415 | 701 troisième tour de la première boucle (sur i) 2 | 17 | 45 | 45 | 102 | 268 | 415 | 701 quatrième tour de la première boucle (sur i) 2 | 17 | 45 | 45 | 102 | 268 | 415 | 701 Problème : l'algorithme va faire 3 tours supplémentaires de la boucle sur i, inutilement Licence Informatique - Semestre 2 - Algorithmique et Programmation 5 Tri à bulle (4/7) Complexité du tri à bulle en fonction de la taille n du tableau : la remontée du plus grand élément utilise (n-1) comparaisons, la remontée du deuxième plus grand utilise (n-2), etc. La complexité sera donc toujours proportionnelle à (n-1)+(n-2)+...+1 = n*(n-1)/2 soit O(n2). Le tri à bulle est en place et l'algorithme donné ici est stable (il serait instable si on utilisait ≥ au lieu de >) Une amélioration possible du tri bulle consiste à utiliser une variable booléenne drapeau qui permet de stopper le tri si plus aucune permutation n'a lieu Licence Informatique - Semestre 2 - Algorithmique et Programmation 6 Tri à bulle (5/7) fonction sans retour triBulle(entier[] tab){ entier i,j,temp; booleen b; début b <- faux; i <- tab.longueur-2; tantque ((non b) et (i > 0)) faire b <- vrai; pour (j allant de 0 à i pas 1) faire si (tab[j] > tab[j+1]) alors temp <- tab[j]; tab[j] <- tab[j+1]; tab[j+1] <- temp; b <- faux; finsi finpour i <- i-1; fintantque fin Remarques : la complexité au pire et en moyenne reste en O(n2) mais est linéaire au mieux cette amélioration n'est efficace que si le tableau est déjà « un peu » trié et qu'il devient trié « assez vite » Licence Informatique - Semestre 2 - Algorithmique et Programmation 7 Tri à bulle (6/7) Une autre amélioration possible du tri bulle est le tri Boustrophedon (du nom des écritures qui changent de sens à chaque ligne) : on parcourt le tableau de gauche à droite, en faisant remonter l'élément le plus grand on redescend le tableau de droite à gauche, en faisant descendre le plus petit on recommence en ignorant les 2 éléments déjà triés la complexité au pire et en moyenne est toujours en O(n2) mais est réduite du fait qu'on peut exploiter les bonnes positions des petits éléments comme des plus grands Licence Informatique - Semestre 2 - Algorithmique et Programmation 8 Tri à bulle (7/7) Tri par la méthode du tri à bulle de tableaux d'entiers générés aléatoirement Licence Informatique - Semestre 2 - Algorithmique et Programmation 9 Tri sélection (1/3) Principe du tri sélection (ou tri par extraction) : on parcourt le tableau pour trouver le plus grand élément, et une fois arrivé au bout du tableau on y place cet élément par permutation. Puis on recommence sur le reste du tableau. Algorithme : i désigne la dernière case à traiter fonction sans retour triSelection(entier[] tab){ entier i,j,temp,pg; debut i <- tab.longueur-1; tanque (i > 0) faire pg <- 0; pour (j allant de 0 à i pas 1) faire si (tab[j] > tab[pg]) alors pg <- j; finsi finpour temp <- tab[pg]; tab[pg] <- tab[i]; tab[i] <- temp; i <- i-1; fintantque fin Licence Informatique - Semestre 2 - Algorithmique et Programmation 10 Tri sélection (2/3) Complexité du tri sélection : à chaque tour de la boucle tantque, on fait i tours de boucle pour. La complexité sera donc dans tous les cas proportionnelle à (n-1)+(n-2)+...+1 =n*(n-1)/2 et donc en O(n2). Remarque : le tri sélection utilise une variable de plus que le tri bulle pour stocker l'indice du plus grand élément, mais il utilise moins d'instructions (une seule permutation par parcours du tableau) Le tri sélection est en place et l'algorithme donné ici est stable (il serait instable si on utilisait ≥ au lieu de >) Le tri sélection peut être amélioré en positionnant à chaque parcours du tableau le plus grand et le plus petit élément, selon le même principe que celui utilisé pour le tri Boustrophédon. dans ce cas, on fera 2 fois moins de tours de boucle tantque (on trie 2 éléments à chaque tour) mais à chaque tour, on effectuera plus d'opérations, la complexité reste donc quadratique mais s'améliore Licence Informatique - Semestre 2 - Algorithmique et Programmation 11 Tri sélection (3/3) Tri par la méthode du tri sélection de tableaux d'entiers générés aléatoirement Licence Informatique - Semestre 2 - Algorithmique et Programmation 12 Tri insertion (1/2) Principe du tri insertion : on prend deux éléments qu'on trie dans le bon ordre, puis un 3e qu'on insère à sa place parmi les 2 autres, puis un 4e qu'on insère à sa place parmi les 3 autres, etc. C'est la méthode la plus utilisée pour trier des dossiers, des cartes à jouer, ... Algorithme : i représente le nombre d'éléments triés fonction sans retour triInsertion(entier[] tab){ entier i, j, val; debut pour (i allant de 1 à tab.longueur-1 pas 1) faire val <- tab[i]; j <- i; tantque ((j > 0) et (tab[j-1] > val)) faire tab[j] <- tab[j-1]; j <- j-1; fintantque tab[j] <- val; finpour fin Licence Informatique - Semestre 2 - Algorithmique et Programmation 13 Tri insertion (2/2) Complexité du tri insertion : au pire, si le tableau est trié en ordre inverse, à chaque tour de la boucle pour, on fait i tours de boucle tantque. La complexité au pire sera donc proportionnelle à 1+2+...+(n-1) =n*(n-1)/2 et donc en O(n2). au mieux, si le tableau est déjà trié, on fera une seule comparaison par élément, sans permutation, sauf pour le premier élément. La complexité au mieux est donc proportionnelle à (n-1) et linéaire. en moyenne, chaque élément sera inséré au milieu de ceux déjà triés, le nombre de comparaisons sera donc de 0+1+3/2+...+n/2 soit n*(n+1)/4 et la complexité moyenne est donc en O(n2). Le tri par insertion est en place et l'algorithme donné ici est stable (il serait instable si on utilisait ≥ au lieu de >) Remarque : le tri insertion est donc environ 2 fois plus rapide que le tri sélection Licence Informatique - Semestre 2 - Algorithmique et Programmation 14 Tris quadratiques Comparaison des tris quadratiques sur des tableaux d'entiers générés aléatoirement Licence Informatique - Semestre 2 - Algorithmique et Programmation 15 Tri rapide (1/5) Principe du tri rapide (quicksort, tri de Hoare) : on prend le premier élément (le pivot), on le met à sa place en plaçant à sa gauche les éléments plus petits que lui et à sa droite les éléments plus grands que lui. Puis on répète l'opération d'une part sur le sous tableau de gauche, d'autre part sur le sous-tableau de droite. Algorithme : on partitionne le tableau autour de la valeur contenue dans la première case, puis on appelle l'algorithme sur chacune des sous-parties. Il nous faut donc : une fonction qui partitionne une certaine section [a,b] du tableau en plaçant au début les éléments plus petits que tab[a], puis tab[a], puis les éléments plus grands ou égaux à tab[a] et retourne l'indice de tab[a] dans le tableau partitionné (au départ, a vaudra 0 et b vaudra tab.longueur-1) une fonction de tri proprement dit qui appelle la fonction précédente puis lance des appels récursifs sur les sous-parties Licence Informatique - Semestre 2 - Algorithmique et Programmation 16 Tri rapide (2/5) fonction avec retour entier partition(entier[] tab, entier a, entier b){ entier pivot,i,temp; début pivot <- a; pour (i allant de a+1 à b pas 1) faire si (tab[i] < tab[pivot]) alors temp <- tab[i]; tab[i] <- tab[pivot+1]; tab[pivot+1] <- tab[pivot]; tab[pivot] <- temp; pivot <- pivot+1; finsi finpour return pivot; fin // cette fonction trie les éléments de tab d'indices compris entre a et b inclus fonction sans retour triRapide(entier[] tab, entier a, entier b){ entier pivot; début si (b > a) alors pivot <- partition(tab,a,b); triRapide(tab,a,pivot-1); triRapide(tab,pivot+1,b); finsi fin Licence Informatique - Semestre 2 - Algorithmique et Programmation 17 Tri rapide (3/5) Exemple d'exécution de la fonction de partitionnement : 70 | 17 | 2 | 268 | 415 | 45 | 45 | 102 i=1 et pivot=0 17 | 70 | 2 | 268 | 415 | 45 | 45 | 102 i=2 et pivot=1 17 | 2 | 70 | 268 | 415 | 45 | 45 | 102 i=3 et pivot=2 17 | 2 | 70 | 268 | 415 | 45 | 45 | 102 i=4 et pivot=2 17 | 2 | 70 | 268 | 415 | 45 | 45 | 102 i=5 et pivot=2 17 | 2 | 45 | 70 | 268 | 415 | 45 | 102 i=6 et pivot=3 17 | 2 | 45 | 45 | 70 | 268 | 415 | 102 i=7 et pivot=4 17 | 2 | 45 | 45 | 70 | 268 | 415 | 102 L'indice du pivot retourné est 4 Licence Informatique - Semestre 2 - Algorithmique et Programmation 18 Tri rapide (4/5) Complexité du tri rapide : la fonction de partitionnement est linéaire de la forme a*n où a est une constante. au mieux, dans le cas où le partitionnement coupe toujours le tableau en deux parties égales (à 1 près), la complexité c(n) de la fonction de tri est telle que c(n) = a*n + 2*c(n/2) + b où b est une constante. Dans ce cas, on a c(n) = O(n*log(n)). au pire, dans le cas où le partitionnement conduit systématiquement à ce qu'une des parties soit vide, la complexité c(n) de la fonction de tri est telle que c(n) = a*n + c(n-1) + b où b est une constante. Dans ce cas, on a c(n) = O(n2). en moyenne, on peut montrer que c(n) = O(n*log(n)). Le tri rapide n'est pas optimum dans tous les cas, mais en moyenne, il est bien plus rapide que les tris quadratiques Le tri rapide est en place, mais l'algorithme donné ici n'est pas stable Licence Informatique - Semestre 2 - Algorithmique et Programmation 19 Tri rapide (5/5) Améliorations possibles : quand le tableau est déjà « un peu trié », il vaut mieux choisir comme pivot l'élément du milieu du tableau, pour équilibrer les parties fonction avec retour entier partition(entier[] tab, entier a, entier b){ entier pivot,i,temp; début temp = tab[a]; tab[a] = tab[(a+b)/2]; tab[(a+b)/2] = temp; pivot <- a; ... fin quand le tableau n'est pas « un peu trié », on peut découvrir l'élément médian (tel que la moitié des éléments du tableau sont plus petits, et la moitié plus grands, à 1 près) avec un algorithme linéaire. Cette amélioration assure donc une complexité au pire en O(n*log(n)) au tri rapide. quand les parties à trier sont petites, il peut être plus rapide d'utiliser un tri par sélection pour les trier car ce tri est plus rapide sur de petits tableaux ... Licence Informatique - Semestre 2 - Algorithmique et Programmation 20 Tri fusion (1/3) Principe du tri fusion : on découpe le tableau en deux, on trie chacune des 2 parties, puis on fusionne les parties triées en respectant l'ordre. Algorithme : il nous faut donc une fonction qui fusionne deux sections contigues du tableau. Le plus simple est d'utiliser des tableaux supplémentaires (mais il est possible de faire du tri fusion en place). une fonction de tri proprement dit qui lance des appels récursifs sur les sousparties puis appelle la fonction précédente Licence Informatique - Semestre 2 - Algorithmique et Programmation 21 Tri fusion (2/3) // cette fonction fusionne les parties de tab d'indices dans [a,b] et [b+1,c], // en supposant que les éléments de ces parties soient triés en ordre croissant. fonction sans retour fusion(entier[] tab,entier a, entier b, entier c){ entier i,j,k; entier[b-a+1] t1; int[c-b] t2; début pour (i allant de 0 à t1.longueur-1 pas 1) faire t1[i] <- tab[a+i]; finpour pour (j allant de 0 à t2.longueur-1 pas 1) faire t2[j] <- tab[b+1+j]; finpour i <- 0; j <- 0; k <- a; tantque (k <= c) faire si (i >= t1.longueur) alors tab[k] <- t2[j]; j <- j+1; sinon si (j >= t2.longueur) alors tab[k] <- t1[i]; i <- i+1; sinon si (t1[i] <= t2[j]) alors tab[k] <- t1[i]; i <- i+1; sinon tab[k] <- t2[j]; j <- j+1; finsi finsi finsi k++; fintantque fin Licence Informatique - Semestre 2 - Algorithmique et Programmation 22 Tri fusion (3/3) fonction sans retour triFusion(entier[] tab, entier a, entier b){ début si (b>a) alors triFusion(tab,a,(a+b)/2); triFusion(tab,((a+b)/2)+1,b); fusion(tab,a,(a+b)/2,b); finsi fin Complexité du tri-fusion : la complexité de la fusion est linéaire de la forme a*n où a est une constante la complexité du tri est c(n) = c(n/2) + c(n/2) + a*n + b où b est une constante donc c(n) = O(n*log(n)) dans tous les cas Le tri fusion est donc optimum en complexité, mais généralement, il est moins rapide que le tri rapide, car il duplique le tableau. On peut optimiser le tri en ne dupliquant qu'une des deux parties. Le tri fusion est stable mais la version proposée ici n'est pas en place (il est possible d'en écrire des versions en place) Licence Informatique - Semestre 2 - Algorithmique et Programmation 23 Tris quasilinéaires Comparaison des tris quasilinéaires sur des tableaux d'entiers générés aléatoirement Licence Informatique - Semestre 2 - Algorithmique et Programmation 24