Algorithmie PC 2 : Tris corrigé 1 1.1 Tri par bulles Principe du tri par Bulles (variante du tri par sélection) On considère un tableau tab de n données sur lesquelles il existe une relation d’ordre. On souhaite trier ce tableau dans l’ordre croissant. Pour cela, on réalise autant de passes sur les données que nécessaire en échangeant les éléments adjacents s’ils sont dans un mauvais ordre relatif : parcourir tab[0], ..., tab[n-1] en intervertissant les éléments tab[j-1] et tab[j] non ordonnés pour tout 1 ≤ j ≤ n − 1. 1.2 Écrire un algorithme mettant en œuvre la méthode décrite. Algorithme 1 Tri par bulles Données tab n Début changement=Vrai Tant que changement = Vrai faire changement = Faux de i =1 a n-2 faire Si tab[i-1] > tab[i] alors tempo = tab[i-1] tab[i-1] = tab[i] tab[i] =tempo rendre tab Fin 1 1.3 Tester l’algorithme sur le tableau tab=[10, 7, 8, 4, 5] L’exécution de l’algorithme est résumé en table 1. tab[0] 10 7 7 7 7 7 7 7 7 7 7 4 4 4 4 4 4 4 4 4 tab[1] 7 10 8 8 8 8 8 4 4 4 4 7 5 5 5 5 5 5 5 5 tab[2] 8 8 10 4 4 4 4 8 5 5 5 5 7 7 7 7 7 7 7 7 tab[3] 4 4 4 10 5 5 5 5 8 8 8 8 8 8 8 8 8 8 8 8 tab[4] 5 5 5 5 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 boucle tant que 1 i 1 2 3 4 2 1 2 3 4 3 1 2 3 4 4 1 2 3 4 Table 1: Exécution du tri bulle avec tab=[10, 7, 8, 4, 5] 1.4 Expliquer pourquoi le tri par bulles est bien une méthode de tri ? Combien de passes sont-elles nécessaires pour trier le tableau ? C’est bien une méthode de tri (ouf) car à chaque passe un nouvel élément sera trié et ne changera plus de place. Plus précisément : • lors de la première passe, l’élément de plus grande valeur est échangé avec chacun des éléments situés à sa droite (i.e. les éléments d’indices supérieurs) jusqu’à ce qu’il se trouve à sa place définitive, (à l’extrémité droite du tableau en tab[n-1]), • à la seconde passe, c’est l’élément de seconde plus grande valeur qui est mis à sa place définitive, • et ainsi de suite . . . Il y a donc au maximum n passes à effectuer. 2 1.5 Quel est la complextié de cet algorithme ? Il y a au maximum n boucles “tant que” et dans chaque boucle “tant que” un parcours de tous les éléments du tableau et des opérations en O(1). La complexité est donc en O(n2 ). 1.6 Y a-t-il des améliorations possibles à notre algorithme ? Comme on sait que à la fin de la passe i les i dernières positions du tableau sont triés, il n’est pas nécessaire de les consulter. Cela ne changera pas la complexité théorique (en O(n2 )) mais réduira tout de même le nombre d’opérations effectuées. 1.7 Pour quelles types de données cet algorithme est-il efficace ? Pour des données presque (ou déjà) triées. 2 Tri rapide Le tri rapide (quicksort pour les anglicistes) est certainement le tri le plus utilisé, et ceci pour plusieurs raisons, en particulier : • facile à implémenter, • efficace pour de nombreux types de données, • peu gourmand en ressources. Cependant, il comporte aussi des défauts, en particulier il est récursif et donc une petite erreur de programmation peut se révéler désastreuse et difficile à corriger, il effectue un nombre quadratique d’opérations dans le pire des cas (nlog(n) opérations en moyenne comme vous allez le calculer). 2.1 Algorithme L’algorithme 2 est une implémentation du tri rapide. Lancer l’algorithme sur le tableau précédent ([10, 7, 8, 4, 5]). Pourquoi est-ce une méthode de tri ? Les variables gauche et droite sont les bornes dans lesquelles le tableau sera trié. Le principe de l’algorithme est, entre les bornes, de choisir un élément (pivot) que l’on va placer à l’endroit qu’il occupera lorsque les données seront triées. Pour cela, on s’arrange pour qu’à gauche de pivot tous les éléments soient plus petit que lui et qu’à droite tous les éléments soient plus grand que lui. Une fois ceci fait, on relance l’algorithme pour les parties à gauche et à droite de pivot. 3 Algorithme 2 Tri rapide. Initialisé par gauche=0 et droite=n-1 Données tab gauche droite Début Si droite > gauche alors stop = Faux i = gauche j = droite-1 pivot = tab[droite] Tant que stop = Faux faire (1) tant que tab[i] < pivot faire i = i+1 (2) tant que tab[j] > pivot faire j = j-1 Si i≥j alors stop = vrai Sinon (3) tempo = tab[i] tab[i] = tab[j] tab[j] = tempo (4) i=i+1 j=j-1 (5) tempo = tab[i] tab[i] = tab[droite] tab[droite] = tempo (6) triRapide(tab, gauche,i-1) (7) triRapide(tab, i+1,droite) rendre tab Fin 4 Plus précisément, à la fin de (1) et (2) i pointe sur la première occurrence du tableau après gauche plus grande que pivot et j sur la plus grande occurrence du tableau avant droite plus petite que pivot. Si i<j, toute les valeurs n’ont pas été regardées et on peu échanger tab[i] et tab[j] (étape (3)) pour agrandir notre recherche (après l’échange, tous les éléments plus petit que i sont < que pivot et tous les éléments plus grand que j sont > pivot. Avant l’échange tab[i]=>pivot et tab[j]<=pivot). On peut donc réitérer les étapes (1) et (2). Une fois que i>= j, tous les éléments entre gauche et droite ont été vus, et, tous les éléments de gauche à i-1 sont plus petit ou égaux à pivot et tous les éléments entre i et droite sont plus grand ou égaux à pivot. L’échange entre tab[i] et tab[droite] (étape 5) nous assure donc que la place de la valeur pivot restera inchangée à la suite du tri (c’est à dire que l’on peut trouver un tri du tableau où la valeur de tab[i] sera pivot). Il ne nous reste plus qu’à trier les bouts de tableaux restant, c’est à dire les éléments entre gauche et i-1 (étape 6) et ceux entre i+1 et droite (étape 7). 2.2 Cette méthode de tri est-elle stable ? Non, car la position finale du pivot ne peut être connue et donc il n’y a aucune raison que la méthode soit stable. 2.3 Quelle est la complexité (au pire) de cet algorithme ? À chaque étape, un élément est trié. Tri Rapide est donc lancé au maximum n fois. La complexité de la boucle “tant que stop=Faux” incrémente deux compteur i et j, et s’arrête lorsque i ≥ j. Le nombre d’opérations est donc de l’ordre de O(n). La complexité finale de l’algorithme est donc en O(n2 ). 2.4 Complexité moyenne La formule de récurrence de la complexité moyenne peut être construite comme suit : C(N) = “opérations de la boucle tant que stop=Faux” + “moyenne du nombre d’opérations effectué par les deux appels récursifs” La première partie est linéaire, on peut donc la borner par p.N où p est une constante quelque soit N. Pour la deuxième partie, la taille des tableaux qui vont être passés aux appels récursifs n’est pas connue, et dépend de la position finale du pivot. Si c’est la position k du tableau qui est la position finale du pivot, le nombre d’opérations effectués par ces appels est C(k-1) + C(N-k). Chaque case du tableau ayant une probabilité 1/N d’êtreP la position finale du tableau, donc le nombre moyen d’opérations est : (1/N ) ∗ 1≤k≤N (C(k − 1) + C(N − k)). 5 L’équation que nous avons à résoudre est donc : X C(N ) = pN + 1/N (C(k − 1) + C(N − k)) 1≤k≤N Avec comme conditions initiales C(1) = C(0) = 0. C(N ) P = pN + 1/N 1≤k≤N (C(k − 1) + C(N − k)) P P = pN + 1/N ∗ 1≤k≤N C(k − 1) + 1/N ∗ 1≤k≤N C(N − k) P = pN + 2/N ∗ 1≤k≤N C(k − 1) P On a donc N C(N ) − pN 2 = 2 1≤k≤N C(k − 1). Ainsi (N − 1)C(N − 1) − P p(N − 1)2 = 2 1≤k≤N −1 C(k − 1), et donc : N C(N ) − pN 2 = (N − 1)C(N − 1) − p(N − 1)2 + 2C(N − 1), et finalement : N C(N ) = p(2N − 1) + (N + 1)C(N − 1) en divisant l’équation ci-avant par N (N + 1) on obtient : C(N )/(N + 1) = 2p/(N + 1) − p/(N (N + 1)) + C(N − 1)/N = 2p/(N + 1) P − p/(N (N + 1)) + 2p/(N ) − p/(N (N − 1)) + C(N − 2)/(N − 1) = ... = 3≤k≤N 2/(k + 1) − P p/(k(k + 1)). 3≤k≤N P P On a ainsi C(N )/(N + 1) ≤ 2 1≤k≤N 1/k. Comme 1≤k≤N 1/k est équivalent à ln(N ), on en conclut bien que C(N )/(N + 1) est en O(ln(N )) et donc que C(N ) est en O(N ∗ ln(N )). 6