L2S3 Harmonisation UFR d’IEEA – Université Lille 1 Algorithmique 2015–2016 TP : Tri Rapide Dans ce TP vous pouvez utiliser les fonctions données dans le fichier permutation.py disponible sur le portail. 1 Principe Le tri rapide, ou quicksort, est fondé comme le tri par fusion sur le paradigme "diviser pour régner". C’est en effet également un algorithme récursif qui procède en divisant le tableau en deux sous-tableaux, les tri (récursivement) et recombine les deux parties. La différence entre ces deux algorithmes de tri réside dans la manière de diviser le tableau puis de recombiner. Contrairement au tri par fusion, l’étape la plus coûteuse du tri rapide, donc celle où s’effectue tout le travail, est la division en deux sous tableaux. La recombinaison et immédiate et ne nécessite aucun travail. De plus le tri rapide est un tri "en place" ou "sur place" : un espace mémoire constant en plus du tableau à trier est suffisant pour effectuer ce tri. De manière plus détaillée le principe du tri rapide est le suivant : 1. Diviser/Partitionner : Un élément du tableau est choisi arbitrairement, on appel cet élément le pivot. La division consiste à placer tous les éléments de valeur inférieure à celle du pivot au début du tableau et tous les supérieurs en fin de tableau. Les éléments de valeur égale au pivot se trouvent au milieu. Le tableau est donc divisé en trois parties : les éléments inférieurs, égaux puis supérieurs au pivot. 2. Trier : Il s’agit simplement de trier récursivement les deux parties contenant les éléments inférieurs et supérieurs au pivot. 3. Recombiner : Rien à faire, le tableau est déjà trié ! 2 Choix du pivot Comme vous le verrez dans la suite du TP choisir une bonne valeur de pivot est très important pour avoir la meilleur complexité possible. Cependant choisir le pivot optimal à chaque appel récursif nécessite presque autant d’opérations que pour trier le tableau, c’est pourquoi on choisit en général une valeur aléatoirement dans le tableau. Q1 . Ecrivez la fonction pivot retournant une valeur ( pas un indice !) choisie aléatoirement dans le tableau t entre les indices d et f , où t, d et f sont donnés en paramètre. 3 Partition Il y a plusieurs méthodes pour écrire un algorithme itératif de partition, l’une d’elles consiste à maintenir quatre zones délimitées par trois indices, a, b et c, comme illustré sur la Figure 1 : 1. les éléments inférieurs au pivot se trouvent aux indices strictement inférieurs à a, 1 2. les éléments égaux au pivot se trouvent entre les indices a et c − 1, 3. les éléments supérieurs au pivot sont aux indices strictement supérieurs à b, 4. les éléments restants entre les indices c et b n’ont pas encore été comparés au pivot et n’ont donc pas encore été placés dans l’une des trois précédentes parties. a < pivot c = pivot b ??? > pivot Figure 1 – Partition L’idée est de comparer l’élément d’indice c au pivot puis de l’insérer dans l’une des trois parties, inférieur, supérieur ou égale au pivot, suivant sa valeur. 3.1 La fonction partition Q2 . Donnez les conditions vérifiées par les indices a, b et c lorsque la partition est terminée, c’est-à-dire lorsqu’il ne reste plus d’éléments qui n’ont pas été comparés au pivot. Q3 . Quel est, en fonction (éventuellement) de a, b et c, le plus grand indice x d’un élément strictement inférieur au pivot à la fin de l’algorithme de partition ? Q4 . Quel est, en fonction (éventuellement) de a, b et c, le plus petit indice y d’un élément strictement supérieur au pivot à la fin de l’algorithme de partition ? Q5 . Donnez les valeurs initiales des indices a, b et c en fonction des indices d et f de début et fin de tableau, c’est-à-dire lorsqu’aucun élément n’a encore été comparé au pivot. Q6 . Ecrivez la fonction partition qui partitionne selon valeur p du pivot les éléments du tableau t compris entre les indices d et f en trois parties : éléments inférieurs, égaux et supérieurs au pivot. Cette fonction prend en paramètre t, d, f et p et retourne le couple (x,y) d’indices où x est le plus grand indice d’un élément strictement inférieur au pivot et y est le plus petit indice d’un élément supérieur au pivot à la fin de l’algorithme de paritionnement. Q7 . Quelle propriété intéressante pour des éléments au cours d’un algorithme de tri est vérifiée par tous les éléments de valeur égale au pivot lorsque la partition est terminée ? 3.2 Complexité Q8 . L’algorithme a-t-il des cas pire ou meilleur que les autres en terme de nombre de comparaisons effectuées entre le pivot et les autres éléments du tableau ? Si oui donnez ces cas, sinon expliquez pourquoi. Q9 . Combien de comparaisons entre le pivot et les autres éléments du tableau t entre les indices d et f sont effectuées ? Vous pouvez justifier votre réponse par un argument (convaincant) ou un calcul. 2 Q10 . Donnez l’ordre de grandeur asymptotique du nombre de comparaisons. 4 Tri récursif Toute la difficulté du tri se trouve dans la partition du tableau et comme pour le tri par fusion, une fois la partie délicate de l’algorithme encapsulée dans une fonction (partition ici) l’algorithme récursif de tri ne prend plus que quelque lignes à écrire. 4.1 La procédure tri_rapide Q11 . Ecrivez la procédure tri_rapide prenant en paramètre un tableau t et deux indices d et f de début et fin de tableau et qui effectue un tri rapide sur t. Vous pouvez naturellement utiliser les fonctions pivot et partition écrites précédemment. Q12 . Justifiez rapidement que l’algorithme effectue bien un tri du tableau. Vous pouvez pour cela utiliser la propriété vérifiée par les éléments égaux à la valeur du pivot à la fin de la fonction partition. 4.2 Complexité Q13 . Donnez l’équation de récurrence décrivant le nombre de comparaisons entre éléments du tableau effectuées lors de l’exécution de la procédure tri_rapide sur un tableau de taille n. Dans un premier temps on pourra considérer qu’il y a nI éléments inférieurs et nS éléments supérieurs au pivot. De plus, dans la suite on supposera qu’une valeur n’apparaît qu’une seule fois dans le tableau et donc qu’il ne peut pas y avoir plusieurs cases contenant une valeur égale au pivot. Q14 . Que devient l’équation de récurrence s’il y a toujours autant d’éléments inférieurs que d’éléments supérieurs au pivot ? Q15 . Résoudre l’équation dans ce cas là et donner un ordre de grandeur asymptotique du nombre de comparaisons que le tri effectue. Q16 . Que devient l’équation de récurrence si le pivot est toujours l’élément maximum ou minimum du tableau ? Q17 . Résoudre l’équation dans ce cas là et donner un ordre de grandeur asymptotique du nombre de comparaisons que le tri effectue. Q18 . Déduire de vos résultats précédents un pire et un meilleur cas pour l’algorithme du tri rapide. Q19 . Le tri rapide est il intéressant pour un tableau presque trié ? Justifiez votre réponse. Remarque : La complexité en moyenne du tri rapide, c’est-à-dire la moyenne du nombre d’opérations sur toutes les données possibles de taille n (= tous les tableau possibles de taille n), est en Θ(n log n). En pratique ce tri est généralement plus efficace que le tri par fusion dont la complexité est toujours en Θ(n log n). 3 4.3 Preuve de correction Prouver qu’un algorithme est correct consiste simplement à montrer que le calcul ou le traitement qu’il effectue est bien celui pour lequel il a été écrit : à partir de données vérifiant ses conditions d’utilisation il conduira toujours à un état final ou résultat qui vérifie une propriété attendue. Une preuve de correction est partielle si elle montre seulement que si l’algorithme s’arrête alors il est correct. Il faut dans ce cas si possible la compléter par une preuve de terminaison, c’est-à-dire montrer que l’algorithme va effectivement s’arrêter. La logique de Hoare permet à partir d’une propriété vraie en début d’algorithme, comme une condition d’utilisation, de déduire des propriétés vérifiées à chaque étape, c’est-à-dire entre chaque calculs élémentaires, puis en fin de l’algorithme. Ecrire une preuve compléte à la main est en général fastidieux. Cependant la mise en évidence d’invariant de boucle permet souvent de se convaincre qu’un algorithme itératif est correct. Un invariant de boucle est une propriété qui est vérifiée après la phase d’initialisation, reste vraie à l’issue de chaque itération et qui conjointement avec la condition d’arrêt de la boucle permet de montrer que le résultat obtenu à la fin de la boucle est bien celui qui est attendu. Q20 . Proposez un invariant (de boucle) pour (l’unique boucle de) l’algorithme de partitionnement. Q21 . Votre invariant est-il vrai au début de la première itération ? Q22 . Expliquer pourquoi si l’invariant est vérifié en début d’une itération il sera toujours vrai au début de l’itération suivante. Q23 . Donnez une preuve de correction informelle et partielle de l’algorithme. Q24 . En observant ce qu’il se passe à chaque itération, justifiez que l’algorithme de partionnement va bien se terminer. 4