Algorithmes de tri 1. Présentation • Les algorithmes de tri constituent une classe étudiée depuis longtemps en algorithmique. Ils sont intéressants car, d’un point de vue pratique, la nécessité de trier des données est présente dans la plupart des domaines de l’informatique. De plus, d’un point de vue pédagogique, ils permettent d’illustrer de façon très parlante la notion de complexité algorithmique abordée dans ce cours. • Nous définissons d’abord la notion d’algorithme de tri de façon générale, ainsi que les différents aspects utilisés lorsqu’on veut les caractériser et les comparer. Dans un deuxième temps, nous présentons en détails les algorithmes de tri les plus connus. 1. Présentation 1.1 Définition On définit un algorithme de tri de la façon suivante : • Algorithme de tri : algorithme dont le but est d’organiser une collection d’éléments muni d’une relation d’ordre. Notez bien qu’il s’agit d’une collection, et non pas d’un ensemble, ce qui signifie qu’il est possible que le groupe d’éléments que l’on veut trier contienne plusieurs fois la même valeur. 1. Présentation 1.1 Définition Il est important de noter que le principe utilisé pour le tri est complètement indépendant de la nature de la relation d’ordre utilisée. On peut utiliser le même algorithme pour trier des entiers ou des chaînes de caractères. Tout ce qui va changer, c’est que dans un cas on utiliser l’opérateur C <=, alors que dans l’autre il faudra créer une fonction capable de comparer des chaînes de caractères. 1. Présentation 1.1 Définition Dans le cas le plus simple du tri, on va utiliser la relation d’ordre pour organiser les objets contenus dans l’ensemble 𝑋 sur laquelle elle est définie. Mais il est aussi possible que les éléments que l’on veut trier soient de type complexe, en particulier de type structuré. La valeur de 𝑋 associée à l’élément est alors appelée une clé : • Clé de tri : champ de l’élément à trier qui est utilisée pour effectuer la comparaison. 1. Présentation 1.1 Définition Dans le cas le plus simple du tri, on va utiliser la relation d’ordre pour organiser les objets contenus dans l’ensemble 𝑋 sur laquelle elle est définie. Mais il est aussi possible que les éléments que l’on veut trier soient de type complexe, en particulier de type structuré. La valeur de 𝑋 associée à l’élément est alors appelée une clé : • Clé de tri : champ de l’élément à trier qui est utilisée pour effectuer la comparaison. 1. Présentation 1.1 Définition Autrement dit, la clé est la partie de l’élément sur laquelle la relation d’ordre s’applique. C’est cette clé qui permet de comparer deux éléments, et de décider lequel doit être placé avant l’autre dans la séquence triée. exemples : • éléments simples : o les éléments sont des entiers o la clé correspond à l’élément tout entier • éléments complexes : o les éléments sont des étudiants caractérisés par un nom, un prénom et un âge. o si on effectue un tri par rapport à leur âge, alors l’âge est la clé et les autres données ne sont pas considérées lors du tri. 1. Présentation 1.1 Définition On peut également définir des clés multiples, c’est-à-dire utiliser une première clé pour ordonner deux éléments, puis une autre clé en cas d’égalité, et éventuellement encore d’autres clés supplémentaires. exemple : dans l’exemple des étudiants, on pourrait ainsi décider de les trier en fonction de leur âge d’abord, puis de leur nom quand deux étudiants ont le même âge. 1. Présentation 1.2 Caractéristiques On classe les différents algorithmes de tris suivant plusieurs propriétés : complexité algorithmique, caractère en place, caractère stable. Pour la complexité, on procède comme on l’a fait précédemment : meilleur/moyen/pire des cas, spatial/temporel. Les deux autres aspects sont spécifiques au problème de tri. • Caractère en place : se dit d’un tri qui trie la collection en la modifiant directement, i.e. sans passer par une structure de données secondaire. En particulier, certains tris utilisent une copie de la collection, ce qui implique une plus grande consommation de temps (puisqu’il faut copier la collection) et de mémoire (puisqu’il faut stocker la copie). Le fait de travailler directement sur la structure originale est donc plus efficace de ce point de vue-là. 1. Présentation 1.2 Caractéristiques • Caractère stable : se dit d’un tri qui ne modifie pas la position relative de deux éléments de même clé. exemple : supposons qu’on a deux étudiants 𝐴 et 𝐵 de même âge, et que l’étudiant 𝐴 est placé avant l’étudiant 𝐵 dans la collection initiale : Pour des raisons pédagogiques, les différents algorithmes de tri abordés dans ce cours seront appliqués à des tableaux d’entiers de taille N, mais ils peuvent bien sûr être appliqués à : • N’importe quel type de données comparable autre que les entiers (réels, structures…) ; 1. Présentation 1.2 Caractéristiques • N’importe quelle structure séquentielle autre que les tableaux (listes…). De plus, on considèrera dans ce cours que l’objectif est de trier les tableaux dans l’ordre croissant : le tri dans l’ordre décroissant peut être facilement obtenu en inversant les comparaisons effectuées dans les algorithmes. 2. Tri à bulles 2.1 Principe Le principe du tri à bulle est de reproduire le déplacement d’une bulle d’air dans un liquide : elle remonte progressivement à la surface. On appelle cycle la séquence d’actions suivante : 1. On part du premier élément du tableau. 2. On compare les éléments consécutifs deux à deux : o Si le premier est supérieur au deuxième : ils sont échangés ; o Sinon : on ne fait rien. 3. On passe aux deux éléments consécutifs suivants, jusqu’à arriver à la fin du tableau. On répète ce cycle jusqu’à ce que plus aucun échange ne soit effectué. 2. Tri à bulles 2.1 Principe exemple : 2. Tri à bulles 2.1 Principe exemple : Dans la figure ci-dessus : • Les valeurs encadrées sont les deux éléments consécutifs comparés. • Les valeurs en gris sont celles que l’on vient d’échanger. 2. Tri à bulles 2.1 Principe Il s’agit d’une version basique du tri à bulle, qui peut être améliorée, car certaines des opérations qu’elle effectue sont inutiles. En effet, on peut remarquer qu’à chaque cycle, le tri à bulle fait remonter une valeur jusqu’à ce qu’elle atteigne sa place définitive. Par exemple : à la fin du 1er cycle, la plus grande valeur est dans la 1ère case en partant de la droite du tableau. À la fin du 2ème cycle, la 2ème plus grande valeur est dans la 2ème case en partant de la droite du tableau, etc. Au ième cycle, il est donc inutile d’effectuer les comparaisons après la case 𝑁−𝑖, puisque ces valeurs sont forcément supérieures à toutes celles qui sont placées avant la case 𝑁−𝑖. 2. Tri à bulles 2.1 Principe Si on reprend l’exemple précédent, en représentant en rouge les cases dont on sait que la valeur ne changera plus jamais, et en supprimant les opérations inutiles on obtiendra ceci : Le nombre de cycles ne change pas, mais leur Durée diminue. 2. Tri à bulles 2.2 Implémentation Soit tri_bulle(int tab[]) la fonction qui trie le tableau d’entiers tab de taille 𝑁 en utilisant le principe du tri à bulles. 2. Tri à bulles 2.2 Implémentation Complexité temporelle : • ligne 1 : affectation : 𝑂(1). • ligne 2 : do : o ligne 3 : affectation : 𝑂(1). o ligne 4 : for : ligne 4 : opérations élémentaires : 𝑂(1). ligne 5 : if : opérations élémentaires : 𝑂(1). répétitions : N fois dans le pire des cas. total : 𝑁×𝑂(1) = 𝑂(𝑁). o lignes 10 et 11 : opérations élémentaires 𝑂 1 . o répétitions : N fois dans le pire des cas. o total : 𝑁×𝑂(𝑁) = 𝑂(𝑁2). • total : 𝑇(𝑁) = 𝑂(1) + 𝑂(𝑁2) = 𝑂(𝑁2). 2. Tri à bulles 2.2 Implémentation Complexité spatiale : • 4 variables simples : 𝑂(1). • 1 tableau de taille N : 𝑂(𝑁). • total : 𝑆(𝑁) = 𝑂(𝑁). Propriétés : • En place : oui (on trie en modifiant directement le tableau). • Stable : oui (on ne permute deux éléments qu’après une comparaison stricte). 3. Tri par sélection 3.1 Principe Le principe du tri par sélection est de déterminer quel est le maximum du tableau, de le déplacer à l’endroit qu’il devrait occuper si le tableau était trié, puis de continuer avec les valeurs restantes. 1. On cherche la plus grande valeur du tableau. 2. On déplace cette valeur dans le tableau afin de la placer au bon endroit, c'est-à-dire à la dernière place (case 𝑁−1). 3. On cherche la 2ème plus grande valeur du tableau : on cherche donc la plus grande valeur du sous-tableau allant de la case 0 à la case 𝑁−2 incluse. 4. On déplace cette valeur dans le tableau afin de la placer au bon endroit, c'est-à-dire à l’avant-dernière place (case 𝑁−2), soit la 2ème case en partant de la fin du tableau. 3. Tri par sélection 1. 2. 3. 4. 3.1 Principe On cherche la plus grande valeur du tableau. On déplace cette valeur dans le tableau afin de la placer au bon endroit, c'est-à-dire à la dernière place (case 𝑁−1). On cherche la 2ème plus grande valeur du tableau : on cherche donc la plus grande valeur du sous-tableau allant de la case 0 à la case 𝑁−2 incluse. On déplace cette valeur dans le tableau afin de la placer au bon endroit, c'est-à-dire à l’avant-dernière place (case 𝑁−2), soit la 2ème case en partant de la fin du tableau. 5. On cherche la 3ème plus grande valeur du tableau : on cherche donc la plus grande valeur du sous-tableau allant de la case 0 à la case 𝑁−3 incluse. 6. On déplace cette valeur dans le tableau afin de la placer au bon endroit, c'est-à-dire à l’antépénultième place (case 𝑁−3), soit la 3ème case en partant de la fin du tableau 7. On répète ce traitement jusqu’à ce que le sous-tableau à traiter ne contienne plus qu’une seule case (la case 0), qui contiendra obligatoirement le minimum de tout le tableau. 3. Tri par sélection 3.1 Principe exemple : 3. Tri par sélection 3.1 Principe Dans la figure ci-dessus : • Les valeurs encadrées sont les éléments comparés. • La valeur en gris est le maximum du sous-tableau. • Les valeurs claires représentent la partie du tableau qui a été triée. 3. Tri par sélection 3.2 Implémentation Soit tri_selection(int tab[]) la fonction qui trie le tableau d’entiers tab de taille 𝑁 en utilisant le principe du tri par sélection. 3. Tri par sélection 3.2 Implémentation Complexité temporelle : • ligne 1 : for : o lignes 1 et 2 : opérations élémentaires : 𝑂(1). o ligne 3 : for : ligne 3 : opérations élémentaires : 𝑂(1). ligne 4 : if : opérations élémentaires : 𝑂(1). répétitions : N fois au pire. total : 𝑁×𝑂(1) = 𝑂(𝑁). o lignes 6, 7 et 8 : opérations élémentaires : 𝑂(1). o répétitions : N fois au pire. o total : 𝑁×𝑂(𝑁) = 𝑂(𝑁2). • total : 𝑇(𝑁) = 𝑂(𝑁2). 3. Tri par sélection 3.2 Implémentation Complexité spatiale : • 4 variables simples : 𝑂(1). • 1 tableau de taille N : 𝑂(𝑁). • total : 𝑆(𝑁) = 𝑂(𝑁). Propriétés : • En place : oui (on trie en modifiant directement le tableau). • Stable : non (quand le maximum est placé à la fin, on modifie forcément sa position relativement à d’autres éléments dont la clé possède la même valeur). 4. Tri par insertion 4.1 Principe Le tri par insertion effectue un découpage du tableau en deux parties : • Une partie pas encore triée ; • Une partie déjà triée. Initialement, la partie triée est constituée d’un seul élément : le premier élément du tableau. Le principe de l’algorithme consiste alors à répéter le traitement suivant : 1. Sélectionner le premier élément de la partie non-triée ; 2. L’insérer à la bonne place dans la partie triée ; 3. Jusqu’à ce qu’il n’y ait plus aucun élément dans la partie non-triée. 4. Tri par insertion 4.1 Principe Dans la figure ci-dessus, la valeur en gris est l’élément de la partie non-triée qu’on insère dans la partie triée. 4. Tri par insertion 4.2 Implémentation Soit void tri_insertion(int tab[]) la fonction qui trie le tableau d’entiers tab de taille 𝑁 en utilisant le principe du tri par insertion. 4. Tri par insertion 4.2 Implémentation Complexité temporelle : • ligne 1 : for : o lignes 2 et 3 : opérations élémentaires : 𝑂(1). o ligne 4 : while : lignes 5 et 6 : opérations élémentaires : 𝑂(1). répétitions : 𝑁−1 au pire. total : 𝑁×𝑂(1) = 𝑂(𝑁). o ligne 7 : affectation : 𝑂(1). o répétitions : 𝑁−1 au pire. o total : (𝑁−1) ×𝑂(𝑁) = 𝑂(𝑁2). • total : 𝑇(𝑁) = 𝑂(𝑁2). 5. Tri fusion 5.1 Principe Le tri fusion fonctionne sur le principe diviser-pour-régner : plusieurs petits problèmes sont plus faciles à résoudre qu’un seul gros problème. Le tri se déroule en trois étapes : 1. Division : le tableau est coupé en deux (à peu près) en son milieu ; 2. Tri : chaque moitié est triée séparément récursivement ; 3. Fusion : les deux moitiés triées sont fusionnées pour obtenir une version triée du tableau initial. 5. Tri fusion 5.1 Principe exemple : 5. Tri fusion 5.1 Principe exemple : 5. Tri fusion 5.2 Implémentation Chaque phase de l’algorithme est implémentée dans une fonction différente. Soit void division(int tab[], int tab1[], int taille1, int tab2[], int taille2) la fonction qui divise le tableau d’entiers tab en deux parties stockées dans les tableaux tab1 et tab2. Les tailles de tab1 et tab2 sont respectivement taille1 et taille2, et elles sont passées en paramètres (i.e. elles sont déjà calculées). 5. Tri fusion 5.2 Implémentation 5. Tri fusion 5.2 Implémentation Complexité temporelle : • ligne 1 : for : • ligne 2 : affectation : 𝑂(1). o répétitions : 𝑁/2 fois dans le pire des cas. o total : 𝑁/2 × 𝑂(1) = 𝑂(𝑁). • ligne 3 : for : o ligne 4 : affectation : 𝑂(1). o répétitions : 𝑁/2 fois dans le pire des cas. o total : 𝑁/2 × 𝑂(1) = 𝑂(𝑁). • total : 𝑇(𝑁) = 𝑂(𝑁) + 𝑂(𝑁) = 𝑂(𝑁). 5. Tri fusion 5.2 Implémentation Complexité spatiale : • 3 variables simples : 𝑂(1). • 1 tableau de taille N : 𝑂(𝑁). • 2 tableau de taille 𝑁/2 : 𝑂(𝑁). • total : 𝑆(𝑁) = 𝑂(𝑁). 5. Tri fusion 5.2 Implémentation Soit void fusion(int tab[], int tab1[], int taille1, int tab2[], int taille2) la fonction qui fusionne deux tableaux d’entiers triés tab1 et tab2 en un seul tableau trié tab. On connaît les tailles de tab1 et tab2, qui sont respectivement taille1 et taille2. 5. Tri fusion 5.2 Implémentation 5. Tri fusion 5.2 Implémentation Complexité temporelle : • ligne 1 : affectation : 𝑂(1). • ligne 2 : while : o ligne 3 : if : lignes 4 et 5 : affectation : 𝑂(1). lignes 6 et 7 : affectation : 𝑂(1). total : 𝑂(1). o ligne 8 : affectation : 𝑂(1). o répétitions : 2𝑁/2−1 = 𝑁−1 fois dans le pire des cas. o total : (𝑁−1) × 𝑂(1) = 𝑂(𝑁). 5. Tri fusion 5.2 Implémentation Complexité temporelle (suite) : • ligne 9 : if : o ligne 10 : for : lignes 11 et 12 : affectation : 𝑂(1). répétitions : 𝑁/2 fois dans le pire des cas. total : 𝑁/2 × 𝑂(1) = 𝑂(𝑁). o ligne 13 : for : lignes 14 et 15 : affectation : 𝑂(1). répétitions : 𝑁/2 fois dans le pire des cas. total : 𝑁/2 × 𝑂(1) = 𝑂(𝑁). o total : max(𝑂(𝑁), 𝑂(𝑁)) = 𝑂(𝑁). • total : 𝑇(𝑁) = 𝑂(1) + 𝑂(𝑁) + 𝑂(𝑁) = 𝑂(𝑁). 5. Tri fusion 5.2 Implémentation Complexité spatiale : • 6 variables simples : 𝑂(1). • 1 tableau de taille N : 𝑂(𝑁). • 2 tableau de taille 𝑁/2 : 𝑂(𝑁). • total : 𝑆(𝑁) = 𝑂(𝑁). 5. Tri fusion 5.2 Implémentation Enfin, Soit void tri_fusion(int tab[], int taille) la fonction qui trie récursivement un tableau d’entiers tab de taille taille, grâce aux fonctions division et fusion. 5. Tri fusion 5.2 Implémentation Complexité temporelle : • complexité du cas d’arrêt 𝑛=1 : o lignes 1, 2 et 3 : affectations : 𝑂(1). o total : 𝑇(1) = 𝑂(1). • complexité du cas général 𝑛>0 : o lignes 1, 2 et 3 : affectations : 𝑂(1). o ligne 4 : if : ligne 5 : division : 𝑂(𝑁). lignes 6 et 7 : tri_fusion : 𝑇 (𝑁/2). ligne 8 : fusion : 𝑂(𝑁). total : 2𝑂(𝑁) + 2𝑇(𝑁/2). o o total : 𝑇(𝑁) = 2𝑇(𝑁/2) + 2𝑂(𝑁) + 𝑂(1) = 2𝑇(𝑁/2) + 𝑂(𝑁). 5. Tri fusion 5.2 Implémentation Complexité temporelle (suite) : • résolution de la récurrence : o selon la formule : si 𝑇 𝑛 =𝑐𝑇(𝑛/𝑑) + Θ(𝑛k) pour 𝑐=𝑑k alors on a une complexité en 𝑂(𝑛klog𝑛). o o ici, on a : 𝑐=𝑑=2 et 𝑘=1. d’où 𝑇(𝑁) = 𝑂(𝑁log𝑁). 5. Tri fusion 5.2 Implémentation Complexité spatiale : • • • • • 3 variables simples : 𝑂(1). 1 tableau de taille N : 𝑂(𝑁). 2 tableaux de taille 𝑁/2 : 𝑂(𝑁). profondeur de l’arbre d’appels : log𝑁. total : 𝑆(𝑁) = 𝑂(𝑁log𝑁). Propriétés : • En place : non, car on utilise plusieurs tableaux lors des différentes phases. C’est essentiellement l’étape de fusion qui empêche de faire un tri en place. • Stabilité : oui (lors de la fusion, en cas de clés identiques, on recopie d’abord la valeur du tableau gauche puis celle du tableau droit). 6. Tri rapide 6.1 Principe Le tri rapide est également appelé : tri de Hoare, tri par segmentation, tri des bijoutiers… L’algorithme est le suivant : 1. On choisit un élément du tableau qui servira de pivot. 2. On effectue des échanges de valeurs dans le tableau, de manière à ce que : o Les valeurs inférieures au pivot soient placées à sa gauche. o Les valeurs supérieures au pivot soient placées à sa droite. 3. On applique récursivement ce traitement sur les deux parties du tableau (i.e. à gauche et à droite du pivot). 6. Tri rapide 6.1 Principe Remarque : on peut remarquer que le choix du pivot est essentiel. L’idéal est un pivot qui partage le tableau en deux parties de tailles égales. Mais déterminer un tel pivot coûte trop de temps, c’est pourquoi il est en général choisi de façon arbitraire. Ici par exemple, on prend la première case du tableau. Entre ces deux extrêmes, des méthodes d’approximation peu coûteuses existent pour faire un choix de compromis. 6. Tri rapide 6.1 Principe exemple : Dans figure ci-dessus, la valeur indiquée en gris correspond au pivot sélectionné pour diviser le tableau en deux. 6. Tri rapide 6.2 Implémentation Soit void tri_rapide(int tab[], int d, int f) la fonction récursive qui trie le tableau d’entiers tab de taille 𝑁 en utilisant le principe du tri rapide. Les paramètres d et f marquent respectivement le début et la fin du sous-tableau en cours de traitement. Ces deux paramètres valent donc respectivement 0 et N-1 lors du premier appel. 6. Tri rapide 6.2 Implémentation 6. Tri rapide 6.2 Implémentation Complexité temporelle : • complexité du cas d’arrêt 𝑛=1 : o ligne 1 : affectation : 𝑂(1). o total : 𝑇(1) = 𝑂(1). • complexité du cas général 𝑛>0 : o ligne 1 : affectation : 𝑂(1). o ligne 2 : if : ligne 3 : affectations : 𝑂(1). ligne 4 : for : ligne 5 : if : lignes 6, 7, 8 et 9 : affectation : 𝑂(1). répétitions : 𝑁−1 fois si le pivot est le min et se trouve au début ou est le max et se trouve à la fin. total : (𝑁−1) 𝑂(1) = 𝑂(𝑁). 6. Tri rapide 6.2 Implémentation Complexité temporelle (suite) : lignes 10, 11, et 12 : affectations : 𝑂(1). ligne 13 : if : ligne 14 : tri_rapide : 𝑇(𝑁−1). ligne 15 : if : ligne 16 : tri_rapide : 𝑇(1) = 𝑂(1). total : 𝑂(1) + 𝑂(𝑁) + 𝑂(1) + 𝑇(𝑁−1) + 𝑂(1). o total : 𝑇(𝑁) = 𝑂(𝑁) + 𝑇(𝑁−1). 6. Tri rapide 6.2 Implémentation Complexité temporelle (fin) : • résolution de la récurrence : o selon la formule : si 𝑇(𝑛) = 𝑇(𝑛−1) + Θ(𝑛𝑘) alors on a une complexité en O(𝑛𝑘+1). o ici, on a : 𝑘=1. d’où 𝑇(𝑁) = 𝑂(𝑁2). 6. Tri rapide • remarques : 6.2 Implémentation o complexité en moyenne : 𝑂(𝑁log𝑁). o le temps dont l’algorithme a besoin pour trier le tableau dépend fortement du pivot choisi : Dans le pire des cas, le pivot se retrouve complètement au début ou à la fin du tableau. On a donc un sous-tableau de taille 𝑁−1 et un autre de taille 0, et on obtient une complexité en 𝑂(𝑁2). Dans le meilleur des cas, le pivot sépare le tableau en deux sous-tableaux de taille égale 𝑁/2. On retrouve alors la même complexité que pour le tri fusion : 𝑂(𝑁log𝑁). 6. Tri rapide 6.2 Implémentation Complexité spatiale : • 6 variables simples : 𝑂(1). • 1 tableau de taille N : 𝑂(𝑁). • profondeur de l’arbre d’appels (pire des cas) : 𝑁. • total : 𝑆(𝑁) = 𝑂(𝑁2). • remarque : si on considère que le même tableau est passé à chaque fois, on a 𝑆(𝑁) = 𝑂(𝑁). Propriétés : • En place : oui (on travaille directement dans le tableau initial). • Stabilité : non (lors du déplacement des valeurs en fonction du pivot, des valeurs de même clé peuvent être échangées).