TD4-Algorithmes de Tri - Intégrale
TD4-Algorithmes de Tri - Intégrale
TD4-Algorithmes de Tri - Intégrale
Selection-sort(A,n)
Begin
for i = 1 to n – 1 do
min = i
for j = i + 1 to n do
if A[j] < A[min] then min = j
if min ≠ i then swap(A[i], A[min])
End
Dans tous les cas, pour trier n éléments, le tri par sélection effectue n(n-1)/2 comparaisons. Sa
complexité est donc (n²).
Selection-sort-derecursive (A,n)
Begin
i=1
while i ≤ n-1 do
min = i
for j = i + 1 to n do
if A[j] < A[min] then min = j
if min ≠ i then swap(A[i], A[min])
i =i+1
End
#include <iostream>
#include <vector>
/**
* Generic function returning the minimum of table @param A starting
* from the index @param k to the end of the table
*/
template <typename T>
unsigned long min(vector<T> A,unsigned long k){
T minimum = A[k];
unsigned long min_index = k;
for (unsigned long i = k+1 ; i<A.size() ; i++){
if(A[i] < minimum){
minimum = A[i];
min_index = i;
}
}
return min_index;
}
/**
* Generic function swapping two elements indexed by
* @param a and @param b of the table @param A
*/
template <typename T>
void swap(vector<T> &A,unsigned long a,unsigned long b){
T temp;
temp = A[a];
A[a] = A[b];
A[b] = temp;
}
/**
* Selection sort.
*/
template <typename T>
void selection_sort(vector<T> &A){
for(unsigned long i = 0 ; i<A.size() -1 ; i++){
swap(A,i,min(A,i));
}
}
Questions
Dans la première partie du cours, on a déjà étudié la version itérative de cet algorithme. Sa
complexité au meilleur cas étant en (n), dans la moyenne des cas et au pire cas en (n²).
Insertion-sort-recursive (A, n)
Begin
if n > 1 then
Insertion-sort-recursive (A, n - 1);
if A[n] < A[n - 1] then
aux:= A[n];
i := n;
repeat
A[i] := A[i - 1];
i := i - 1;
until (i = 1) or (aux > A[i - 1]);
A[i] := aux;
End
Questions
Tri à bulles
Le tri à bulles ou tri par propagation est un algorithme de tri qui consiste à faire remonter
progressivement les plus grands éléments d'un tableau, comme les bulles d'air remontent à la
surface d'un liquide. L'algorithme parcourt le tableau, et compare les couples d'éléments successifs.
Lorsque deux éléments successifs ne sont pas dans l'ordre croissant, ils sont échangés. Après chaque
parcours complet du tableau, l'algorithme recommence l'opération. Lorsqu'aucun échange n'a lieu
pendant un parcours, cela signifie que le tableau est trié. On arrête alors l'algorithme.
Bubble-sort-recursive (A,n)
Begin
swapped = false
for i = 1 to n - 1 do
if A[i] > A[i+1] then swap( A[i], A[i+1] )
swapped = true
if(swapped) then Bubble-sort-recursive (A, n);
End
Questions
1. Calculez la complexité de cet algorithme au meilleur cas, au pire cas et en moyenne des cas en
nombre de comparaisons.
2. Que peut-on dire sur sa complexité en nombre d’échanges ?
3. (Bonus) Ecrivez en C++ le code relatif aux versions itérative et récursive de cet algorithme.
Tri arborescent
La méthode du tri arborescent veut mémoriser toute information obtenue à l’issu des comparaisons
pour l’exploiter dans l’établissement de l’ordre final. Pour ce faire elle construit une arborescence qui
traduit la relation qui existe entre tous les éléments. Convenons donc de mettre à gauche d'une clé
toutes celles qui lui sont inférieures, et à droite toutes celles qui lui sont supérieures, et ainsi de suite
récursivement. Ainsi le tableau ordonné est obtenu en listant les éléments depuis la racine en suivant
le pseudo-code récursif suivant (appel initial List-Elements (root)):
List-Elements(node)
Begin
if exists(node->left-sub-tree) then List-Elements(node->left-sub-tree)
List(node)
if exists(node->right-sub-tree) then List-Elements(node->right-sub-tree)
End
Questions
1. Ecrire l’algorithme « Tri arborescent » qui s’appelle lui-même selon la valeur de la clé.
2. Quelle est la complexité de cet algorithme ?
La récursion termine quand la sous-séquence à trier est de longueur 1... car une telle séquence est
toujours triée.
La principale action de l’algorithme de tri par fusion est justement la fusion des deux listes triées. Le
principe de cette fusion est simple : à chaque étape, on compare les éléments minimaux des deux
sous-listes triées, le plus petit des deux étant l’élément minimal de l’ensemble on le met de côté et
on recommence. On conçoit ainsi un algorithme Fusionner (Merge) qui prend en entrée un tableau A
et trois entiers, p, q et r, tels que p ≤ q < r et tels que les tableaux A[p..q] et A[q+1..r] soient triés.
Merge(A, p, q, r)
Begin
i=p Indice servant à parcourir le tableau A[p..q]
j = q+1 Indice servant à parcourir le tableau A[q+1..r]
Let C a table of size (r - p+1) Tableau temporaire dans lequel on construit le résultat
k=1 Indice servant à parcourir le tableau temporaire
while i ≤ q and j ≤ r do Boucle de fusion
if A[i] < A[ j] then C[k] = A[i]
i = i+1
else C[k] = A[ j]
j = j+1
k = k+1
while i ≤ q do C[k] = A[i] on incorpore dans C les éléments de A[p::q]
i = i+1 qui n’y seraient pas encore ; s’il y en a,
k = k+1 les éléments de A[q+1::r] sont déjà tous dans C
while j ≤ r do C[k] = A[ j] on incorpore dans C les éléments de A[q+1::r]
j = j+1 qui n’y seraient pas encore ; s’il y en a,
k = k+1 les éléments de A[p::q] sont déjà tous dans C
for k = 1 to r – p +1 do on recopie le résultat dans le tableau originel
A[p+k-1] = C[k]
End
38 27 43 3 9 82 10 25 57 17
Diviser : Le tableau A[p..r] est partitionné (et réarrangé) en deux sous-tableaux non vides,
A[p..q] et A[q+1..r], tels que chaque élément de A[p..q] soit inférieur ou égal à chaque
élément de A*q+1..r+. L’indice q est calculé pendant la procédure de partitionnement.
Régner : Les deux sous-tableaux A[p..q] et A[q+1..r] sont triés par des appels récursifs.
Combiner : Comme les sous-tableaux sont triés sur place, aucun travail n’est nécessaire pour
les recombiner, le tableau A[p..r] est déjà trié !
Quick-sort(A, p, r)
Begin
if p < r then q = PARTITIONNING (A, p, r)
Quick-sort(A, p, q)
Quick-sort(A, q+1, r)
End
L’appel Quick-sort(A, 1, length(A)) trie le tableau A. Le point principal de l’algorithme est bien
évidemment le partitionnement qui réarrange le tableau A sur place :
PARTITIONNING (A, p, r)
Begin
x = A[p]
i = p-1
j = r+1
while true do
repeat j = j-1 until A[ j] <= x
repeat i = i+1 until A[i] >= x
if i < j then swap(A[i],A[ j])
else return j
End
Exemple de partitionnement :
Complexité
Pire cas
Le pire cas intervient quand le partitionnement produit une région à n-1 éléments et une à un
élément. Comme le partitionnement coûte (n) et que T(1) = (1), la récurrence pour le temps
d’exécution est : T(n) = T(n-1) + (n) D’où par sommation :
Meilleur cas
On subodore que le meilleur cas apparaît quand la procédure de partitionnement produit deux
régions de taille n/2 .
La récurrence est alors : T(n) = 2 T(n/2)+ (n) ce qui, d’après le cas 2 du théorème 1 nous donne :
T(n) = (nlogn)
Complexité en moyenne
Pour avoir une complexité moyenne, on tire au hasard l’indice de départ de partitionnement. Et on
démontre que la complexité moyenne et aussi égale à : T(n) = (nlogn)
Questions
1. Exécuter à la main l’algorithme du tri rapide en prenant comme tableau celui illustrant la
première invocation de la fonction PARTITIONNING.
Dans l'algorithme, on cherche à obtenir un tas (Heap en anglais), c'est-à-dire un arbre binaire
vérifiant les propriétés suivantes (les deux premières propriétés découlent de la manière dont on
considère les éléments du tableau) :
La différence maximale de profondeur entre deux feuilles est de 1 (i.e. toutes les feuilles se
trouvent sur la dernière ou sur l'avant-dernière ligne)
Les feuilles de profondeur maximale sont « tassées » sur la gauche.
Chaque nœud est de valeur supérieure (resp. inférieure) à celles de ses deux fils, pour un tri
ascendant (resp. descendant).
Un tas ou un arbre binaire presque complet peut être stocké dans un tableau, en posant que les deux
descendants de l'élément d'indice n sont les éléments d'indices 2n et 2n + 1 (pour un tableau indicé à
partir de 1). En d'autres termes, les nœuds de l'arbre sont placés dans le tableau ligne par ligne,
chaque ligne étant décrite de gauche à droite.
Une fois le tas de départ obtenu, l'opération de base de ce tri est le tamisage, ou percolation, d'un
élément, supposé le seul « mal placé » dans un arbre qui est presque un tas. Plus précisément,
considérons un arbre A = A[1] dont les deux sous-arbres (A[2] et A[3]) sont des tas, tandis que la
racine est éventuellement plus petite que ses fils. L'opération de tamisage consiste à échanger la
racine avec le plus grand de ses fils, et ainsi de suite récursivement jusqu'à ce qu'elle soit à sa place.
Pour construire un tas à partir d'un arbre quelconque, on tamise les racines de chaque sous-tas, de
bas en haut (par taille croissante) et de droite à gauche.
Pour trier un tableau à partir de ces opérations, on commence par le transformer en tas. On échange
la racine avec le dernier élément du tableau, et on restreint le tas en ne touchant plus au dernier
élément, c'est-à-dire à l'ancienne racine. On tamise la racine dans le nouveau tas, et on répète
l'opération sur le tas restreint jusqu'à l'avoir vidé et remplacé par un tableau trié.
Questions