TP 3

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 4

Programmation Structurée

TP 3 : tri et complexité
notions : Allocation 1D, Complexité, Tris

Le tri est l’opération consistant à ordonner un ensemble d’éléments en fonctions de clés sur lesquelles
est définie une relation d’ordre. Les algorithmes de tri sont fondamentaux dans de nombreux domaines,
en particulier la gestion où les données sont presque toujours triées selon un critère avant d’être traitées.

La notion de complexité permet de donner un sens numérique à l’idée intuitive de coût d’un algo-
rithme, en temps de calcul et en place mémoire. Il est cependant important de différencier la complexité
théorique, qui donne des ordres de grandeurs de ces coûts sans être liée à une machine, et la complexité
pratique, qui dépend d’une machine et de la manière dont l’algorithme est programmé. La littérature
évoque généralement la complexité théorique i.e. le nombre de comparaisons effectuées sur les clés et
le nombre de permutations effectuées sur les données dans le cas d’un tri. Nous illustrerons aussi la
complexité pratique par le temps d’exécution des fonctions.

Le tri interne est un tri opérant sur des données présentes en mémoire, tandis que le tri externe travaille
sur des données appartenant à des fichiers. Les tris internes seuls seront exposés, et trois catégories se
distinguent en fonction de leur complexité théorique (n est le nombre d’éléments à trier) :

– Les tris triviaux sont en O(n2 ) : tri bulle, tri par sélection, tri par insertion, ....
– Les tris en O(nx ) avec 1 < x < 2 : tri shell
– les tris en O(nlog(n)) : Tri par tas (Heap Sort), tri rapide (Quick Sort), tri MergeSort.

1 Principe du tri part tas


Ce tri est assez simple à programmer et est de complexité en O(nlog(n)),
le situant parmi les tris performants. Cette méthode de tri utilise une
structure de données sous-jacente qui est le tas (heap en anglais). Un tas 85
est un arbre binaire (un nœud de l’arbre a 2 fils : 85 a 73 et 36 pour 71 36
fils) parfait (tous les niveaux de l’arbre sont complètement remplis, sauf
peut-être le dernier, où tous les nœuds sont le plus à gauche possible) 55 49 26 27
qui est de plus partiellement ordonné : en tout nœud de l’arbre, la valeur 20 14 32 17
du nœud père est supérieure à la valeur de chacun de ses nœuds fils. Par
contre, les deux fils ne sont pas ordonnés entre eux. L’arbre ci contre
donne un exemple de tas.

Trois propriétés sont utilisées :


1. un tas est représenté par un tableau. Les deux fils sont obtenus grâce à la relation suivante : si on
examine le nœud i, les deux fils de ce nœud i sont donnés par les éléments d’indice 2 ∗ (i + 1) et
2 ∗ (i + 1) − 1.
2. Réciproquement, un nœud d’indice i dans le tableau a pour père le nœud d’indice (i − 1)/2
3. Dans un tas non vide, la valeur maximale est toujours située à la racine.

1
85 85 85 90
71 ... 71 ... 90 ... 85 ...
55 49 55 90 55 71 55 71
20 14 32 90 20 14 32 49 20 14 32 49 20 14 32 49

Figure 1 – ajout de 90 au tas

1.1 Ajout d’un élément à un tas de n éléments

Pour ajouter un élément à un tas de n éléments (donc le dernier élément du tableau est d’indice
n − 1), on place ce nouvel élément à l’indice n. Ensuite, on doit vérifier la propriété du tas, c’est à dire
que le père, d’indice (n − 1)/2 dans le tableau doit être supérieur à l’élément ajouté. Si c’est le cas, c’est
un tas et on s’arrête. Sinon, on échange les éléments d’indices n et (n − 1)/2. Et on itère entre le père de
(n − 1)/2 qui est d’indice ((n − 1)/2 − 1)/2 et l’élément échangé. On s’arrête bien sûr à la racine.

L’exemple de la figure ?? illustre les étapes de l’ajout du nœud de valeur 90 dans le tas initial (tableau)
suivant : 85 71 36 55 49 26 27 20 14 32. Le tableau devient successivement :
Étape 1 : 85 71 36 55 49 26 27 20 14 32 90 : échange de 90 et de son père (49)
Étape 2 : 85 71 36 55 90 26 27 20 14 32 49 : échange de 90 et de son père (71)
Étape 3 : 85 90 36 55 71 26 27 20 14 32 49 : échange de 90 et de son père (85)
Étape 4 : 90 85 36 55 71 26 27 20 14 32 49 : tas final

1.2 Suppression de la racine d’un tas de n éléments

L’exemple suivant illustre les étapes de la suppression du nœud de valeur 85 dans le tas initial
(tableau) suivant : 85 71 36 55 49 26 27 20 14 32 17 Quand on supprime la première valeur (85), on
l’échange avec la dernière valeur du tas (17). On doit maintenant se débrouiller pour que les n-1 premiers
éléments du tableau soit un tas. Il faut, à partir de la nouvelle racine (17) rétablir les propriétés du tas :
la valeur qui vient d’être changée doit respecter la relation fondamentale : le père est supérieur à ses
deux fils. Après avoir mis 17 à la racine, il faut donc, comme cette relation n’est pas vérifiée, échanger
17 d’indice 0 et le plus grand de ses fils d’indices 1 et 2 (ici le nœud 71 d’indice 1). On itère ensuite
cette opération entre le nœud venant d’être modifié (le nœud d’indice 1 qui vaut maintenant 17 et ses
fils d’indice 3 et 4). Il faut alors échanger 17 et 55. On procède ainsi jusqu’au dernier niveau (un nœud
d’indice supérieur ou égal à n/2 : le nœud 20 d’indice 7 ici). Notez que la valeur 85 est maintenant à
l’indice n-1 dans le tableau, qui forme un tas entre les indices 0 et n-2. Elle n’apparaı̂t pas dans l’arbre
représentant le tas.

Étape 1 : 85 71 36 55 49 26 27 20 14 32 17 : Suppression de 85 par échange avec 17


Étape 2 : 17 71 36 55 49 26 27 20 14 32 85 : Échange de 17 et du plus grand de ses fils (71)
Étape 3 : 71 17 36 55 49 26 27 20 14 32 85 : Échange de 17 et du plus grand de ses fils (55)
Étape 4 : 71 55 36 17 49 26 27 20 14 32 85 : Échange de 17 et du plus grand de ses fils (20)
Étape 5 : 71 55 36 20 49 26 27 17 14 32 85 : tas final

17 71 71 71
71 36. . . 17 36. . . 55 36. . . 55 36. . .
55 49 55 49 17 49 20 49
20 14 32 20 14 32 20 14 32 17 14 32

Figure 2 – Suppression de 85 du tas (remplacement par 17)

2
2 Tri

Pour effectuer un tri, il faut 2 étapes : la construction du tas, puis le tri.

2.1 Construction du tas à partir d’un tableau de n éléments

On construit un tas à partir du tableau en commençant par un tas ne comportant qu’un seul nombre :
le premier nombre du tableau. Le tas initial est donc le tableau limité à son premier élément. On ajoute
alors le deuxième nombre du tableau avec la fonction d’ajout dans un tas et le tableau forme alors un
tas entre les éléments d’indice 1 et 2. Puis on ajoute le troisième pour former un tas entre les indices 1
et 3, etc.

2.2 Tri par ordre croissant du tas de n éléments

Pour effectuer le tri, on supprime le premier élément qui est le plus grand du tas (donc du tableau) en
l’échangeant avec le ne élément du tas (le dernier) en utilisant la fonction suppression dans un tas (§??).
On a alors le plus grand élément en position n-1 et la partie d’indice 0..n-2 du tableau forme encore un
tas. On recommence en supprimant le premier par échange avec celui d’indice n-2, . . . Le résultat est un
tableau dont les éléments sont ordonnés par ordre croissant.

Attention : ne pas confondre le nombre d’éléments du tableau et le nombre d’éléments


du tableau qui forment un tas. Ce sont deux informations différentes.

3 Travail à réaliser

Exercice 1 Ecrire les fonctions suivantes pour réaliser le tri par tas :
– void augmentetas(double* tas, int n) : ajoute le nombre dont l’indice est  n  au tas  tas  exis-
tant. L’élément à ajouter est déjà dans le tableau  tas . Avant exécution de cette fonction, ce
tableau forme déjà un tas entre les indices 0 et n-1. Après exécution de cette fonction, ce tableau
forme un tas entre les indices 0 et n.
– void constructiontas(double* tas, int n) qui établit un tas à partir du tableau tas de n éléments.
Avant l’exécution de cette fonction, le tableau  tas  contient n éléments non organisés en tas.
Après exécution, les éléments du tableau  tas  ont été réorganisés pour former un tas entre les
indices 0 et n-1.
– void descendretas(double* tas, int n) : Cette fonction fait descendre le premier élément (la racine
du tas) à sa place. Avant l’appel, les éléments entre les indices 1 et n respectent les conditions d’un
tas, mais pas l’élément d’indice 0. Après l’exécution de cette fonction, les éléments d’indice 0 à n
forment un tas.
– void suppressiontas(double* tas, int n) : supprime le premier nombre de  tas  en l’échangeant
avec le dernier (indice n-1) et en réorganisant le tas ensuite. On suppose qu’avant l’appel de cette
fonction, le paramètre  tas  est un tas de n éléments (tableau). Après exécution, le paramètre
 tas  est un tas de n-1 éléments, l’élément supprimé (celui qui était le premier) est à l’indice n-1

dans le tableau  tas .


– void tri(double* tab, int n), qui trie le tableau  tas  par la méthode décrite ci dessus. La fonction
construit un tas, puis ordonne les éléments selon la méthode décrite. Après exécution, le paramètre
 tas  est tableau trié par ordre croissant de n éléments.

3
Exercice 2 Comparer avec d’autres tris
Écrire les fonctions de tri vues en cours :
– void tri_insertion(double* tab, int n) : qui trie le tableau tab de manière croissante en utilisant une
approche par insertion.
– void tri_rapide(double* tab, int n) : qui trie le tableau tab de manière croissante en utilisant une
approche par tri rapide (quick sort).

Exercice 3 Mesurer le temps


Pour évaluer la complexité expérimentale, on peut utiliser une mesure de temps de calcul. Vous pourrez
utiliser les fonctions suivantes pour mesurer précisément le temps utilisé par vos fonctions de tri.
#include <time.h>
main() {
clock_t debut, fin ;
debut=clock();
tri(tab,n);
fin=clock();
printf("duree=%f\n",((double)fin-debut)/CLOCKS_PER_SEC);
}

Vous aimerez peut-être aussi