Algorithmes de tri
Problème général du tri On dispose d’un tableau unidimensionnel ou d’une liste d’éléments d’un ensemble muni d’une relation d’ordre totale (le plus souvent des nombres avec la relation ≤ mais pas seulement: penser au tri alphabétique) On cherche à les ordonner (disons par ordre croissant pour fixer les idées) On peut accepter de créer des listes intermédiaires pour réaliser le tri ou bien imposer de ne travailler que sur la liste initiale (par des permutations) Le paramètre de complexité sera la longueur n de la liste. Les opérations à effectuer seront des comparaisons et des affectations. On cherche bien sûr à optimiser la complexité en fonction de n quand n est très grand, mais on peut aussi être amené à rechercher à optimiser la complexité pour certains types de données particuliers (ex: liste déjà « bien prétriées »)
Tri par sélection: principe On commence par mettre le plus petit élément à gauche suivant le même principe que dans l’algorithme de recherche du minimum (une variable pivot initialisée à 𝑙[0] est comparée aux valeurs successives de la liste, on échange la valeur rencontrée avec le pivot lorsqu’elle est plus petite) Ensuite on recommence en ignorant le premier élément, etc… A l’étape numéro 𝑖, les 𝑖 premiers éléments de la liste sont correctement ordonnés (il s’agit de l’invariant de boucle qui garantit la validité de l’algorithme)
Tri par sélection: pseudo-code Donnée: une liste l de longueur 𝒏≥𝟐 (numérotée de 0 à 𝑛−1) Pour 𝒊=𝟎 à 𝒏−𝟐: 𝑝𝑖𝑣𝑜𝑡:=𝑙[𝑖] Pour 𝑗=𝑖+1 à 𝑛−1: Si 𝑙[𝑗]<𝑝𝑖𝑣𝑜𝑡: 𝑝𝑖𝑣𝑜𝑡=𝑙[𝑗] échanger 𝑙[𝑗] et 𝑙 𝑖 Renvoyer l
Tri par sélection: complexité A l’étape 𝑖 on fait au plus 𝑛−𝑖 comparaison et 𝑛−𝑖 échanges (chacun comportant 2 affectations de valeurs de la liste) Il y a donc au maximum 𝑛−1 +…+1= 𝑛(𝑛−1) 2 comparaisons et le double d’affectations de valeurs dans la liste. Complexité en 𝑂( 𝑛 2 )
Tri par insertion : principe Il s’agit comme dans le tri sélection de procéder en n-1 étape de telle sorte qu’au terme de l’étape i les i premières valeurs soient bien ordonnées. La différence réside dans la manière dont on ordonne: à l’étape i, l’élément 𝑥=𝑙[𝑖] est comparé à ses prédecesseurs dans la liste en partant du plus grand ; tant que 𝑥<𝑙[𝑗] on continue à parcourir les prédecesseurs de 𝑙[𝑖] par ordre décroissant. On s’arrête dès qu’on rencontre un j pour lequel 𝑙[𝑗−1]≤𝑥. Dans ce cas on « insère » 𝑥 entre 𝑙[𝑗−1] et 𝑙[𝑗]
Tri par insertion: pseudo-code Donnée: une liste l de longueur 𝒏≥𝟐 (numérotée de 0 à 𝑛−1) Pour 𝒊=𝟎 à 𝒏−𝟐: 𝑥:=𝑙[𝑖+𝟏] 𝑗=𝑖 Tant que (𝑗≥𝟏 et 𝒍[𝒋]>𝒙 ) 𝑗 =𝑗−1 déplacer la valeur 𝑥 entre 𝑙[𝑗−1] et 𝑙[𝑗] ∗ Renvoyer l ∗ : tous les termes de la liste à partir de l[j] sont alors décalés vers la droite
Tri par insertion: complexité A l’étape i on fait au plus 𝑖 comparaison et au plus 𝑖 affectation (on intercale un élément dans la liste et on décale ses successeurs de 1) Il y a donc au maximum 1+…+(𝑛−1)= 𝑛(𝑛−1) 2 comparaisons et 𝑛 affectations de valeurs dans la liste. Complexité en 𝑂( 𝑛 2 )
Tri par fusion : principe Il s’agit d’un algorithme récursif basé sur le principe « diviser pour régner » Il repose sur la remarque suivante: si 𝑙1 et 𝑙2 sont deux listes de taille 𝑛 2 bien triées, alors on peut fusionner 𝑙1 et 𝑙2 en une liste l de taille 𝑛 bien triée en un temps linéaire (on commence par prendre le minimum de 𝑙1[0] et 𝑙2[0] et on affecte sa valeur à 𝑙[0] puis on l’enlève de sa liste d’origine ; on recommence alors avec les listes restantes etc…)
Tri par fusion : principe On en déduit l’algorithme de tri par fusion: On partitionne la liste de taille 𝑛 à trier en deux sous-listes à peu près de longueur 𝑛/2 On trie chacune des deux sous-listes en appelant la fonction de tri (récursivité) On fusionne les deux sous-listes ainsi triées
Tri par fusion : pseudo-code Donnée: une liste l de longueur 𝒏≥𝟐 (numérotée de 0 à 𝑛−1) On suppose disposer d’une fonction fusion qui fabrique une liste bien ordonnée à partir de deux sous-listes Si 𝑛=1: Renvoyer 𝑙 Sinon: créer 𝑙1 sous- liste comprenant les 𝑝=[𝑛/2] premiers termes de 𝑙1 et 𝑙2 sous-liste de taille 𝑛−𝑝 comprenant les derniers termes de 𝑙 𝑙𝑏𝑖𝑠1,𝑙𝑏𝑖𝑠2=𝑡𝑟𝑖(𝑙1),𝑡𝑟𝑖(𝑙2) Renvoyer 𝑓𝑢𝑠𝑖𝑜𝑛(𝑙1𝑏𝑖𝑠,𝑙2𝑏𝑖𝑠)
Tri par fusion:complexité (pire des cas) Le nombre de comparaisons maximum effectuées pour fusionner deux listes triées (tailles 𝑝 et 𝑞) est min(𝑝,𝑞) et le nombre max d’affectations dans la nouvelle liste est 𝑝+𝑞. Par suite le nombre d’opérations élémentaires est en 𝐴× 𝑚𝑎𝑥 𝑝,𝑞 avec (𝐴=constante) Quand on travaille à une instance de la récursivité avec une liste de taille 𝑘 on effectue un appel à fusion qui s’appliquera aux deux « demi-listes » dont la taille est au plus 𝑘 2 +1 et même 𝑘 2 lorsque k est pair En tenant compte de l’appel à fusion cela donne une majoration de la complexité 𝐶 𝑛 du tri d’une liste de taille 𝑛: 𝑪 𝒏 ≤ 𝟐∗𝑪 𝒏/𝟐 +𝟏 +𝐀∗( 𝒏 𝟐 +𝟏) (on peut enlever les +1 lorsque n est pair) Complexité en 𝑂(𝑛 log 𝑛 )
TRI RAPIDE (QUIcksort) : principe Il s’agit d’un algorithme récursif On choisit un élément dans la liste (le pivot): on peut choisir par exemple le dernier terme mais aussi un élément aléatoire (dans le cas d’un élément aléatoire on commence par effectuer une permutation pour placer le pivot en fin de liste) On effectue des comparaisons et permutations jusqu’à ce que tous les éléments inférieurs au pivot soient en début de tableau et les autres à la fin (on termine en insérant le pivot à sa place) Le pivot partitionne ainsi le tableau en deux éléments de longueur plus petite que l’on trie par appel récursif à la fonction.
Tri rapide : le partitionnement Revenons sur le cœur de l’algorithme çàd le partitionnement des valeurs de la liste en fonction de leur place vis-à-vis du pivot: Après avoir placé le pivot en fin de liste, on parcourt les éléments de la liste de gauche à droite et on les compare au pivot: Si l[i] ≤pivot: on ne fait rien Sinon: on parcourt les successeurs de l[i] (tant que c’est posssible) jusqu’à rencontrer un terme ≤pivot; lorsque c’est le cas on permute ce terme avec l[i] On passe au terme d’indice i+1 (attention: notons que ce terme n’a pas nécessairement la même valeur qu’au début de l’étape précédente)
Tri rapide: terminaison et validité La terminaison repose sur le fait qu’au terme de l’étape de partitionnement les deux sous-listes délimitées par le pivot sont de taille strictement inférieures à celles de la liste dont on est parti. La validité vient du fait que le pivot est « à sa place » au terme de chaque partitionnement (c’est l’invariant de boucle) La validité globale de l’algorithme s’en déduit bien puisqu’au bout de k étapes il y a au moins k éléments bien placés
Tri rapide: complexité Si on fixe le choix du pivot (par exemple dernier terme de la liste à chaque étape): la complexité dans le pire des cas est en 𝑂( 𝑛 2 ) (comparer avec l’algorithme par fusion) La complexité en moyenne est en 𝑂(𝑛𝑙𝑜𝑔𝑛) Si on choisit le pivot aléatoirement à chaque étape (loi uniforme): Complexité moyenne (en moyennant sur le choix du pivot) pour tout jeu de données en 𝑶(𝒏 𝐥𝐨𝐠 𝒏 ) Complexité dans le pire des cas (avec pire choix de pivot à chaque étape) en 𝑶 (𝒏 𝟐 )