MP1 Lycée Janson de Sailly Algorithmes de tri On note A[0 ...n − 1] un tableau contenant n éléments qui peuvent être des entiers, des réels ou, plus généralement, toute collection de valeurs sur lesquelles on peut définir une relation d’ordre total. En pratique nous allons étudier des tableaux d’entiers positifs. Algorithmes de tri Table des matières 1 Tri par insertion 1 2 Tri 2.1 2.2 2.3 fusion Principe . . . . . . . . . . . . . . . . . . . . . . . . . . Algorithme de fusion . . . . . . . . . . . . . . . . . . . Algorithme du tri par fusion . . . . . . . . . . . . . . . 2 2 2 4 3 Tri rapide (quick sort) 3.1 Algorithme du pivot . . . . . . . . . . . . . . . . . . . 3.2 Algorithme de tri rapide . . . . . . . . . . . . . . . . . 5 5 6 4 Application : recherche de la médiane dans une liste 7 On peut accéder à chaque élément du tableau au moyen de son indice : entier i ∈ [[0...n - 1]]. A[i] désigne le (i + 1)ème élément du tableau. A[0] est le premier élément du tableau et A[n − 1] le dernier. La convention adoptée pour les indices est celle que Python utilise pour les listes ou les tableaux numpy. 1 Tri par insertion Cet algorithme s’inspire du tri que fait un joueur de cartes lors de la distribution du jeu. Les cartes qu’il a en main sont déjà triées, la plus petite étant à gauche et la plus grande à droite. Lorsqu’on lui distribue une nouvelle carte, il va l’insérer à sa place en la comparant aux plus grandes valeurs de son jeu. But : étudier trois algorithmes qui permettent de trier les différents éléments d’un tableau. Supposons que la prochaine "carte" à insérer à la bonne place soit A[i]. On suppose que le sous-tableau A[0 ...i − 1] est déjà trié. • L’idée est de créer un trou à l’emplacement où se trouve A[i] en la sauvegardant temporairement dans une variable temp. • On compare A[i − 1] et A[i]. Si A[i − 1] 6 A[i] c’est terminé et on remet temp à sa place. Si A[i − 1] > A[i] on décale le contenu A[i − 1] dans la case libre A[i], ce qui déplace le trou vers la gauche du tableau. On recommence en comparant A[i − 2] avec A[i]. Le processus s’arrête lorsque on a trouvé k tel que A[i − k] 6 A[i] ou si on atteint A[0]. • On recommence tout le processus avec la nouvelle carte à insérer, c’est à dire A[i + 1]. 1 MP1 Lycée Janson de Sailly Précondition : n > 1 Algorithmes de tri # Au moins 2 éléments dans le tableau 2 fonction TRI_INSERTION(A, n) : 1. 2.1 pour i allant de 1 à n − 1 faire : 2. temp = A[i] 3. j=i 4. tant que { j > 0 et A[j − 1] > temp } faire : A[j] = A[j − 1] 6. j =j−1 7. A[j] = temp Intérêt : Le tri se fait sur place c’est à dire à l’intérieur même du tableau. Pas besoin de réserver un espace mémoire supplémentaire. 2.2 1. Écrire cet algorithme en langage Python et le tester avec la liste proposée sur le site mp1. Dans notre cas, les deux jeux de cartes sont en fait un sous-tableau A[p...q, q + 1, ...r] de A, où p 6 q < r. Le premier jeux de carte est représenté par A[p...q] et le second jeu par A[q + 1, ..., r]. Chacun est déjà trié séparément, la plus petite valeur étant celle d’indice le plus bas et la plus grande valeur celle d’indice le plus élevé. On aura donc : 2. En vous aidant du tableau ci-dessous, calculer la complexité temporelle de cette fonction dans le meilleur des cas, puis dans le pire des cas (à définir). 1 2 3 4 5 6 7 pour i allant de 1 à n-1 : temp = A[i] j=i tant que ( j > 0 et A[j - 1] > temp ) : A[j] =A[j - 1] j=j-1 A[j] = temp Coût Algorithme de fusion L’algorithme le plus important de ce tri est celui qui permet la fusion de deux jeux de cartes triés séparément pour n’en former qu’un dans lequel toutes les cartes sont triées. Réalisation fonction TRI_INSERTION(A, n) Principe Le tri fusion est un exemple du paradigme diviser pour résoudre. Cela consiste à prendre un problème et à le diviser en deux sous - problèmes plus simples à résoudre. On utilise ensuite les deux solutions pour résoudre le problème initial Prenons l’exemple d’un jeu de 32 cartes à trier. L’idée est de partager ce jeu en deux jeux de 16 cartes (diviser) et de trier séparément ces deux jeux (résoudre). On ré-assemble ensuite chacun des deux jeux, grâce à un algorithme de fusion. # j est l’indice du trou dans le tableau 5. Tri fusion A[p] 6 A[p + 1] 6 ... 6 A[q] Nbre de fois et A[q + 1] 6 ... 6 A[r] L’algorithme de fusion est écrit dans une fonction FUSION qui prend en argument le tableau A et les trois entiers p, q et r : c1 c2 c3 • On commence par "vider" A[p...r] dans deux sous-tableau : G pour tous les éléments allant de A[p] à A[q] et D pour tous les éléments allant de A[q+1] à A[r]. • On remplit ensuite à nouveau A[p...r] en comparant chaque élément de G et de D afin de le placer à la bonne place dans A. c4 c5 c6 c7 2 MP1 Lycée Janson de Sailly Algorithmes de tri fonction FUSION(A, p, q, r) : Variables locales : tableaux d’entiers G[0...q − p + 1] , D[0...r − q] 1 pour i allant de 0 à q − p faire : 2 G[i] = A[p + i] 3 pour j allant de 0 à r − q − 1 faire : 4 D[j] = A[q + j + 1] 5 G[q − p + 1] = ∞ 6 D[r − q] = ∞ 7 i=0 8 j=0 9 pour k allant de p à r faire : 10 si G[i] 6 D[j] faire : 11 A[k] = G[i] 12 i=i+1 13 sinon : 14 A[k] = D[j] 15 j =j+1 Réalisation : 1. Écrire la fonction FUSION en Python 2. À l’aide du tableau ci-dessous, montrer que sa complexité temporelle T(n), avec n = r − p + 1 (nombre d’éléments du sous tableau A[p...r] est O(n). fonction FUSION(A, p, q, r) : Aux lignes 5 et 6, on a recours à une astuce qui consiste à placer une valeur appelée ∞ à la fin des tableaux G et D : en pratique, c’est une valeur strictement supérieure à toutes les valeurs du tableau A qui fait office de "butée" ou encore de sentinelle. Cela évite d’avoir à introduire un test au sein de la boucle "pour" destiné à savoir quand on atteint la fin d’un de ces deux tableaux. 1 2 pour i allant de 0 à q − p : G[i] = A[p + i] c1 c2 3 4 pour j allant de 0 à r − q : D[j] = A[q + j + 1] c3 c4 5 6 7 8 G[q − p + 1] = infini D[r − q] = infini i=0 j=0 c5 c6 c7 c8 9 pour k allant de p à r : c9 10 11 12 13 14 15 Les lignes 7, 8 et 9 introduisent 3 entiers i, j et k qui sont les indices des tableaux G, D et A. Pour chaque valeur de k entre p et q, on compare G[i] et D[j] et on place la plus petite des deux valeurs dans A[k]. On augmente ensuite i ou j suivant la valeur qui a été transférée. Le processus est réitéré pour toutes les valeurs de k variant de p à r, grâce à la boucle pour, ligne 9. 3 Coût si G[i] 6 D[j] : A[k] = G[i] i=i+1 sinon : A[k] = D[j] j =j+1 c10 c11 c12 0 c14 c15 Nbre de fois MP1 Lycée Janson de Sailly 2.3 Algorithmes de tri Le tri du tableau entier se fait en appelant la fonction TRI_FUSION(A, 0, n − 1). Algorithme du tri par fusion Soit à trier le sous-tableau A[p...r] (p 6 r) extrait de A[0...n − 1]. Réalisation : La fonction TRI_FUSION, prend en paramètres le tableau A et les deux entiers p et r. Elle utilise le paradigme "diviser pour résoudre", c’est à dire qu’elle va séparer A[p...q] en deux tableaux de tailles à peu près identique qu’elle va trier séparément, avec une approche purement récursive. 1. Écrire cette fonction en Python et la tester sur la liste fournie sur le site mp1. 2. Dessiner l’arbre des appels récursifs de la fonction TRI_FUSION pour A = [7, 3, 1, 8, 4]. 3. En vous aidant du tableau ci-dessous et en supposant que n = r − p + 1 est toujours divisible par 2 (hypothèse simplificatrice mais efficace), montrer que sa complexité temporelle T (n) vérifie l’équation (approchée) : fonction TRI_FUSION(A, p, r) : 1. si p < r faire : p+r 2. q=E 2 3. TRI_FUSION(A, p, q) 4. TRI_FUSION(A, q + 1, r) 5. FUSION(A, p, q, r) T (n) = 2 T + O(n) T (2p ) , résoudre 2p cette équation de récurrence et montrer que T (n) = O(n lg(n) ) où lg est le logarithme en base 2. 4. En posant n = 2p et en étudiant la suite up = La ligne 1 teste si p < r. Dans le cas contraire, on a soit une absurdité (r < p), soit il n’y a rien à trier (p == r) puisque le soustableau ne renferme alors qu’un seul élément. 1 2 3 4 5 p+q (partie entière) ce 2 qui permet de diviser A[p...r] en deux sous tableaux de tailles égales ou environ égales : A[p..q] et A[q + 1...r] : diviser. À la ligne 2, on définit l’entier q = E n 2 La fonction s’appelle ensuite de façon récursive lignes 3 et 4 pour trier les deux sous-tableaux obtenus : résoudre. À la ligne 5, on ré-assemble enfin les deux sous-tableaux triés grâce à la fonction FUSION étudiée à la section précédente. 4 fonction TRI_FUSION(A, p, r) : Coût si p < r : q = E( (p + r)/2 ) TRI_FUSION(A, p, q) TRI_FUSION(A, q + 1, r) FUSION(A, p, q, r) c1 c2 Nbre de fois MP1 Lycée Janson de Sailly 3 Algorithmes de tri La fonction PIVOT ci-dessous permet de réordonner les éléments A[p], A[p + 1], ..., A[q] en plaçant toutes les valeurs inférieures au pivot à gauche de celui-ci et toutes les valeurs supérieures au pivot à droite. Elle retourne l’indice ind_p du pivot, après que A[p...q] ait été ré-ordonné. Tri rapide (quick sort) Le tri rapide est très souvent utilisé, bien que ce ne soit pas le meilleur en terme de complexité temporelle. Comme le tri par fusion, il fonctionne lui aussi sur le principe diviser pour résoudre et sur l’approche purement récursive. En voici le principe : fonction PIVOT(A, p, q) : 1 si p < q : 2 pivot = A[q] 3 temp = A[p] 4 min = p 5 max = q 6 tant que min < max : 7 si temp > pivot : 8 A[max] = temp 9 max = max - 1 10 temp = A[max] 11 sinon : 12 A[min] = temp 13 min = min + 1 14 temp = A[min] 15 A[min] = pivot 16 retourner min 17 sinon : 18 retourner −1 • On commence par choisir une valeur particulière du tableau qu’on appelle le pivot. On ré-ordonne ensuite le tableau en plaçant toutes les valeurs < pivot à gauche de celui-ci et toutes les valeurs > pivot à droite de celui-ci. L’algorithme PIVOT se charge de faire cela. • À la fin de l’algorithme précédent, le pivot est à la bonne place dans le tableau. Soit ind_p son indice. • Le tableau A est ensuite divisé en deux sous-tableaux A[0...ind_p −1] et A[ind_p + 1...n − 1] à qui on va appliquer séparément l’algorithme PIVOT : on recommence le premier point avec chacun des deux sous-tableaux, selon une approche récursive. 3.1 Algorithme du pivot Prenons un tableau A[0...n − 1] contenant n > 1 éléments entiers positifs que l’on souhaite trier. Pour des raisons qui vont apparaître plus tard, on va commencer par s’intéresser au sous-tableau A[p...q] délimité par les indices p et q, vérifiant : 06p<q 6n−1 À la ligne 1, on place une condition p < q pour lancer l’algorithme. En effet, si p > q, c’est absurde et si p == q, A ne contient qu’un seul élément et il n’est pas nécessaire de le réordonner. Dans ce cas l’indice retourné vaut −1 : cette valeur ne pouvant pas être celle d’un indice, cela permet de placer une sentinelle dans le programme. On choisit comme pivot de ce sous tableau A[p...q] son dernier élément A[q]. Ligne 2, le pivot choisi est toujours A[q], valeur de plus haut indice et on crée un trou dans A en sauvegardant cette valeur dans la 5 MP1 Lycée Janson de Sailly Algorithmes de tri fonction PIVOT(A, p, q) variable pivot. À la ligne 3, on crée un deuxième trou dans A en sauvegardant temporairement A[p] dans une autre variable, nommée temp. 1 2 3 4 5 Les indices des deux trous sont appelés min et max. Ces indices vont varier au cours de la progression de l’algorithme. Au début, min vaut p et max est égal à q (initialisation). À partir de la ligne 6, on s’engage dans une boucle qui ne va s’arrêter que lorsque min sera égal à max, avec deux options : si ... sinon ... 6 7 8 9 10 11 12 13 14 15 16 • Si temp > pivot, on place la valeur contenue dans temp dans A[max] et on diminue max d’une unité : le trou indicé par max se déplace donc vers la gauche. On crée alors un nouveau trou en max-1 en sauvegardant la valeur dans temp. • Au contraire, si temp 6 pivot, on place la valeur contenue dans temp dans A[min] puis on augmente min d’une unité : le trou se déplace donc vers la droite. On crée alors un nouveau trou en min + 1 en sauvegardant la valeur dans temp. 17 18 À la ligne 15, tous les éléments du tableau sont correctement positionnés et il reste un seul trou d’indice min == max. Il ne reste plus qu’à remettre l’élément pivot à cette position. 3.2 La fonction retourne alors l’indice du pivot (ou −1). Coût si p < q : pivot = A[q] temp = A[p] min = p max = q tant que min < max : si temp > pivot : A[max] = temp max = max - 1 temp = A[max] sinon : A[min] = temp min = min + 1 temp = A[min] A[min] = pivot retourner min sinon : retourner −1 Nbre de fois c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 0 c12 c13 c14 c15 c16 0 c18 Algorithme de tri rapide Voici maintenant l’algorithme du tri rapide. Il est écrit sous la forme d’une fonction TRI_RAPIDE(A, p, q) qui prend comme paramètres un tableau A[0...n − 1] et les deux entiers p et q qui délimitent le sous-tableau A[p...q] qui va être trié. Réalisation : 1. Écrire la fonction PIVOT en Python. 2. En vous aidant du tableau ci-contre, montrer que sa complexité temporelle T (n) où n = q − p + 1 est le nombre d’éléments du sous-tableau A[p...q] vérifie T (n) = O(n). • À la ligne 1, la fonction PIVOT est appelée. Elle ré-ordonne A[p...q] et place le pivot à la bonne position : tous les éléments de A[p...q] inférieurs au pivot sont à sa gauche et tous ceux qui 6 MP1 Lycée Janson de Sailly Algorithmes de tri L̂ = [x̂0 , ..., x̂n−1 ] formée des mêmes éléments écrits dans l’ordre croissant. sont supérieurs au pivot sont à sa droite. • On récupère l’indice du pivot dans la variable ind_p. • Lorsque n = 2p + 1 est impair, la valeur médiane est la valeur x̂p pour laquelle il existe p éléments strictement plus petits et p éléments strictement plus grands. • Lorsque n = 2p est pair, on appelle valeurs médianes les deux éléments x̂p−1 et x̂p . • Il ne reste plus qu’à trier les deux sous-tableaux A[p...ind_p −1] et A[ind_p + 1...q], ce qui est fait aux lignes 3 et 4 en appelant la fonction TRI_RAPIDE de façon récursive. fonction TRI_RAPIDE(A, p, q) : 1 2 3 4 Réalisation : considérons une liste A non triée composée de n éléments tous distincts deux à deux. Écrire une fonction MEDIANE(A) qui retourne la valeur ou les valeurs médianes de A et dont la complexité soit en O(n lg n). ind_p = PIVOT(A, p, q) si ind_p != −1 : TRI_RAPIDE(A, p, ind_p −1) TRI_RAPIDE(A, ind_p + 1, q) Si vous voulez trier le tableau entier A[0.. n − 1] tout entier, il suffit d’appeler la fonction TRI_RAPIDE(A, 0, n − 1). Réalisation : 1. Écrire cette fonction en Python et vérifier son fonctionnement sur la liste exemple du site mp1. 2. Dessiner l’arbre des appels récursifs lorsque A = [7, 3, 1, 8, 4]. On peut montrer que la complexité temporelle T (n) de cette fonction est O(n2 ) dans le pire des cas et O(n lg(n) ) dans le meilleur des cas. 4 Application : recherche de la médiane dans une liste Soit L = [x0 , ..., xn−1 ] une liste de n valeurs distinctes appartenant à un ensemble totalement ordonnée. On lui associe la liste triée 7