Vous êtes sur la page 1sur 11

Leçon III : Algorithmes de tri et les structures de données

I- Algorithmes de tri
Généralité

On désigne par "tri" l'opération consistant à ordonner un ensemble d'éléments en fonction de


clés sur lesquelles est définie une relation d'ordre.
Les algorithmes de tri ont une grande importance pratique. Ils sont fondamentaux dans certains
domaines, comme l'informatique de gestion où l'on tri de manière quasi-systématique des
données avant de les utiliser.
L'étude du tri est également intéressante en elle-même car il s'agit sans doute du domaine de
l'algorithmique qui a été le plus étudié et qui a conduit à des résultats remarquables sur la
construction d'algorithmes et l'étude de leur complexité.
Dans notre cours nous allons voir les Tri par fusion, les Tri par tas et les Tri rapide tests
1- Tri par fusion
Il s’agit à nouveau d’un tri suivant le paradigme diviser pour régner. Le principe du tri fusion
(ou tri par interclassement) en est le suivant :
• On divise en deux moitiés la liste à trier (en prenant par exemple, un élément sur deux
pour chacune des listes).
• On trie chacune d’entre elles.
• On fusionne les deux moitiés obtenues pour reconstituer la liste triée.
Le pseudo code est le suivant :
1. ROCEDURE tri_bulle ( TABLEAU a[1:n])
2. passage ← 0
3. REPETER
4. permut ← FAUX
5. POUR i VARIANT DE 1 A n - 1 - passage FAIRE
6. SI a[i] > a[i+1] ALORS
7. echanger a[i] ET a[i+1]
8. permut ← VRAI
9. FIN SI
10. FIN POUR
11. passage ← passage + 1
12. TANT QUE permut = VRAI
2- Tri par tas
La structure de tas (binaire) est un tableau qui peut être vu comme un arbre binaire presque
complet.
Chaque nœud de l'arbre correspond à un élément du tableau qui contient la valeur du nœud.

La valeur de chaque nœud est à l'intérieur du cercle à ce nœud.


Le nombre au-dessus de chaque nœud est l'indice correspondant dans le tableau A.
Le tableau A représenté un tas à deux attributs :
longueur(A) : nombre d'éléments.
taille(A) : nombre d'éléments du tas rangé dans le tableau
Si i est l'indice d'un nœud, les indices de son père, fils gauche et fils droit se calculent comme
suit :

• Père
Fonction pere({E} i : entier) : entier
début
renvoyer (i mod 2)
fin

• Fils gauche
Fonction gauche({E} i : entier) : entier
début
renvoyer (2*i)
fin

• Fils droit
Fonction droit({E} i : entier) : entier
début
renvoyer (2*i+1)
fin
Les tas satisfont également la propriété de tas : pour chaque nœud i autre que la
racine,

• A [pere(i)] >= A[i]


On peut définir deux sortes de tas binaires : les tas min et les tas max.
• Tas-min : chaque élément est supérieur à son parent, le plus petit élément d'un tas min
est à la racine.

• Tas-max : chaque élément est inférieur à son parent, ainsi, le plus grand élément d'un
tas max est stocké dans la racine.
NB : Pour l'algorithme du tri par tas, on utilise des tas max. Les tas min servent
généralement dans les files de priorités.

2-1. Construire un tas et Algorithme de tri par tas


• Conservation de la structure de tas
ENTASSER est un sous-programme permettant la manipulation des tas.
Il prend en entrée un tableau A et un indice i dans le tableau.
Quand ENTASSER est appelée, on suppose que les arbres binaires enracinés en dans les
fonctions gauche(i) et droit(i) ( vu précédemment) sont des tas mais A[i] peut être plus petit que
ses fils, violant ainsi la propriété de tas.
Le rôle de ENTASSER est de faire « descendre » la valeur A[i] dans le tas de manière que le
sous arbre enraciné en i deviennent un tas.
Procédure ENTASSER ({E}A, i : entier)
var
lnd ,r , max, permut : entier
Début
lnd ← gauche(i) //************ la fonction gauche ramène l'indice du fils gauche
r ← droit(i) //************ la fonction droit ramène l'indice du fils droit
si (lnd <= taille(A) et A[lnd]>A[i]) alors //*** vérifie si la valeur de l'indice du fils gauche n'est
pas en dehors du tas et aussi si la valeur du tableau se trouvant à l'indice du fils gauche est
supérieure à la valeur du parent
max ← lnd //**** Si les deux conditions du si sont vérifiées alors max reçoit la valeur de
l'indice gauche
sinon
max ← i //**** Si les deux conditions du si ne sont pas vérifiées alors max reçoit la valeur de
l'indice du pere
finsi
si (r <= taille(A) et A[r] > A[max]) alors //*** vérifie si la valeur de l'indice du fils droit n'est
pas en dehors du tas et aussi si la valeur du tableau se trouvant à l'indice du droit est supérieure
à la valeur se trouvant à l'indice max
max ← r //**** Si les deux conditions du si sont vérifiées alors max reçoit la valeur de l'indice
droit
finsi
si max <> i alors //*******si l'indice max est différent de l'indice i alors on fait une permutation
et la procédure est appelée de manière récursive
permut ← A[i]
A[i] ← A[max]
A[max] ← permut
ENTASSER (A, max)
fin si
fin

• Construction d'un tas


La procédure ENTASSER peut s'utiliser à l'envers pour convertir un tableau A [1..n] avec n =
longueur (A)
En effet, les éléments A [(n mod 2)..n] sont des feuilles de l'arbre, chacun est au départ un tas à
un élément.
La procédure CONSTRUIRE_TAS traverse les nœuds restants et exécute ENTASSER sur
chacun d'eux.
L'ordre dans lequel les nœuds sont traités garanti que les sous arbres enracinés aux fils d'un
nœud i sont des tas avant que ENTASSER soit exécutée à ce nœud.
Procédure CONSTRUIRE_TAS ({E}A : entier)
Début
Pour i ← (longueur (A) mod 2) à 1 faire
ENTASSER (A, i)
fin pour
fin

• Algorithme Tri par tas


L'algorithme du tri par tas commence par utiliser CONSTRUIRE_TAS pour construire un tas
sur le tableau A [1..n] ou n = longueur (A).
Comme l'élément maximal du tableau est stocké à la racine A[1], on peut le placer dans sa
position finale correcte en l'échangeant avec A[n].
Si l'on « ôte » à présent le nœud n du tas (en décrémentant taille[A]), on observe que A[1 . . (n−
1)] peut facilement être transformé en tas max.
Les fils de la racine restent des tas, cependant la nouvelle racine peut violer la propriété des tas.
Si c'est le cas, un seul appel, ENTASSER (A, 1), restaure la propriété de tas, qui laisse derrière
un A [1... (n-1)].
L'algorithme du tri par tas répète alors ce processus pour le tas de taille n-1 jusqu'au tas de taille
2.
Procédure TRIER_TAS(A)
var
var
i,permut : entier
Début
CONSTRUIRE_TAS ({E}A : entier)
Pour i ← longueur(A) à 2 faire
permut ← A[1]
A[1] ← A[i]
A[i] ← permut
taille(A) ← taille(A) – 1
ENTASSER (A, 1)
fin pour
fin

3- Tri rapide tests


• Généralité du tri rapide
Les algorithmes récursifs utilisent l'approche « diviser pour régner ».
Le paradigme « diviser pour régner » donne lieu à 3 étapes à chaque niveau de récursivité :
1. Diviser le problème initial en un certain nombre de sous problèmes.
2. Régner sur les sous problèmes en les résolvant récursivement. Par ailleurs si la taille d'un
sous problème est assez réduite, on peut le résoudre directement.
3. Combiner les solutions aux sous problèmes en une solution complète pour le problème initial.
Le tri rapide suit très fidèlement la règle « diviser pour régner ».

• Description du tri rapide


Les trois (3) étapes du processus « diviser pour régner » employées dans le tri rapide pour trier
un sous tableau typique Tab[p..r] avec p < r, sont les suivants :
Diviser : le tableau Tab[p..r] est partitionné (réarrangé) en deux sous tableaux non vides
Tab[p..q] et Tab[q+1..r] tels que :
chaque élément de Tab[p..q] soit inférieur ou égal à chaque élément de Tab[q+1..r]. L'indice q
est calculé pendant la procédure de partitionnement.
Régner : les deux sous tableaux Tab[p..q] et Tab[q+1..r] sont triés par des appels récursifs à la
procédure principale du tri rapide.
Combiner : comme les sous tableaux sont triés sur place, aucun travail n'est nécessaire pour les
recombiner. Le tableau Tab[p..r] tout entier est maintenant trie.
La procédure du tri rapide est le suivant :
Procédure TRI_RAPIDE (Tab, p, r)
var
q : entier
Début
Si p < r alors
q ← Partition(Tab, p, r)
TRI_RAPIDE (Tab, p, q)
TRI_RAPIDE (Tab, q+1, r)
Finsi
Fin

• Partition du tableau
Principe :
Pour faire la partition d'un Tableau Tab en deux sous-tableaux L1 et L2 :
on choisit une valeur quelconque dans du tableau Tab (la dernière par exemple) que l'on
dénomme pivot, puis on construit
le sous-tableau L1 comme comprenant tous les éléments de L dont la valeur est inférieure ou
égale au pivot,
et l'on construit la sous-tableau L2 comme constituée de tous les éléments dont la valeur est
supérieure au pivot.
Tab = [ 4, 23, 3, 42, 2, 14, 45, 18, 38, 16 ]
Prenons comme pivot la dernière valeur pivot = 16
Nous obtenons par exemple :
L1 = [4, 14, 3, 2] // tous les valeurs du tableau inférieures ou égales au pivot
L2 = [23, 45, 18, 38, 42] // tous les valeurs du tableau supérieures au pivot
A cette étape voici l'arrangement de Tab :
Tab = L1 + pivot + L2 => [4, 14, 3, 2, 16, 23, 45, 18, 38, 42]
En appliquant la même démarche au deux sous-listes : L1 (pivot=2) et L2 (pivot=42)
Cas de L1 prenons comme pivot la dernière valeur pivot = 2
Nous aurons deux sous-tableaux à savoir L11( pour les valeurs inférieures ou égales au pivot)
et L12( pour les valeurs supérieures au pivot)
L11=[ ] liste vide // tous les valeurs du tableau inférieures ou égales au pivot
L12=[3, 4, 14] // tous les valeurs du tableau supérieures au pivot
L1=L11 + pivot + L12 => [2,3, 4,14]
Cas de L2 prenons comme pivot la dernière valeur pivot = 42
Nous aurons deux sous-tableaux à savoir L21( pour les valeurs inférieures ou égales au pivot)
et L22( pour les valeurs supérieures au pivot)
L21=[23, 38, 18] // tous les valeurs du tableau inférieures ou égales au pivot
L22=[45] // tous les valeurs du tableau supérieures au pivot
L2=L21 + pivot + L22 => [23, 38, 18, 42, 45]
A cette étape voici le nouvel arrangement de Tab :
Tab = [(2,3, 4, 14), 16, (23, 38, 18, 42, 45)]
On répétera cette action jusqu'au rangement complet de Tab
etc...
Ainsi de proche en proche en subdivisant le problème en deux sous-problèmes, à chaque étape
nous obtenons un pivot bien placé.
Le point principal de l'algorithme est la fonction Partition qui réarrange le sous tableau
Tab[p..r]. En plus, elle émet l'indice de partitionnement q.
fonction Partition( A, p ,r : entier ) : entier
var
i , j , piv , temp : entier
début
piv ← Tab[r];
i ← p-1;
j ← r;
repeter
repeter
i ← i+1
jusquà Tab[i] >= piv // recherche l'indice de la valeur supérieur e ou égale au pivot
repeter
j ← j-1
jusquà Tab[j] <= piv // recherche l'indice de la valeur inférieure ou égale au pivot
//****** Permuter les valeurs des indices de i et j *******
temp ← Tab[i];
Tab[i] ← Tab[j];
Tab[j] ← temp
jusqu à j <= i;
//******************* Refaire une autre permutation après la sortie de la boucle avec les
indice i,j et r
Tab[j] ← Tab[i];
Tab[i] ← Tab[r];
Tab[r] ← temp;
renvoyer i
FinPartition

II- Les structures de données


• Les listes chaînées
Une Liste chaînée est une structure linéaire qui n'a pas de dimension fixée à sa création. Ses
éléments de même type sont éparpillés dans la mémoire et reliés entre eux par des pointeurs.
Sa dimension peut être modifiée selon la place disponible en mémoire. La Liste est accessible
uniquement par sa tête de Liste c'est-à-dire son premier élément.
Pour les listes chaînées la séquence est mise en œuvre par le pointeur porté par chaque
élément qui indique l'emplacement de l'élément suivant. Le dernier élément de la liste ne
pointe sur rien (Nil).
On accède à un élément de la liste en parcourant les éléments grâce à leurs pointeurs.
Un élément d'une Liste est l'ensemble (ou structure) formé :
d'une donnée ou information,
d'un pointeur indiquant la position de l'élément suivant dans la Liste.
A chaque élément est associée une adresse mémoire.
Les Listes chaînées font appel à la notion de variable dynamique (pointeur).
Exemple :

Explication : La variable pointeur Pt pointe sur l'espace mémoire Pt^ d'adresse Adr1. Cette
cellule mémoire contient la valeur "UVCI" dans le champ Info et la valeur spéciale Nil dans
le champ Suivant.
Ce champ Suivant servira à indiquer quel est l'élément suivant lorsque la cellule fera partie
d'une Liste. La valeur Nil indique qu'il n'y a pas d'élément suivant. Pt^ est l'objet dont
l'adresse est rangée dans Pt.
Remarque : Les listes chaînées entraînent l'utilisation de procédures d'allocation et de
libération dynamiques de la mémoire. Ces procédures sont les suivantes :
Allouer(Pt) : réserve un espace mémoire Pt^ et donne pour valeur à Pt l'adresse de cet espace
mémoire. On alloue un espace mémoire pour un élément sur lequel pointe Pt.
Désallouer(Pt) : libère l'espace mémoire qui était occupé par l'élément à supprimer Pt^ sur
lequel pointe Pt.
Pour définir les variables utilisées ci-dessus, il faut :
//définir le type des éléments de liste :
Type Cellule= Structure
Info : Chaîne
Suivant : Liste
fin Structure
//définir le type du pointeur :
Type Liste = ^Cellule
//déclarer une variable pointeur à partir du type pointeur transcrit dans Liste :
Var
Pt : Liste
//allouer une cellule mémoire qui réserve un espace en mémoire et donne à Pt la valeur de
l'adresse de l'espace mémoire Pt^:
Allouer(Pt)
//affecter des valeur à l'espace mémoire Pt^:
Pt^.Info ← "UVCI"
Pt^.Suivant ← Nil
//Quand Pt = Nil alors Pt ne pointe sur rien.

• Représentation et type d'une liste chaînée


Soit la liste chaînée suivante (@ indique que le nombre qui le suit représente une adresse) :

Pour accéder au troisième élément de la Liste, il faut toujours débuter la lecture de la Liste par
son premier élément dans le pointeur duquel est indiqué la position du deuxième élément.
Dans le pointeur du deuxième élément de la Liste on trouve la position du troisième élément
etc.
Il existe différents types de listes chaînées :
Liste chaînée simple constituée d'éléments reliés entre eux par des pointeurs.
Liste chaînée ordonnée où l'élément suivant est plus grand que le précédent. L'insertion et la
suppression d'élément se font de façon à ce que la liste reste triée.
Liste doublement chaînée où chaque élément dispose non plus d'un mais de deux pointeurs
pointant respectivement sur l'élément précédent et l'élément suivant. Ceci permet de lire la
liste dans les deux sens, du premier vers le dernier élément ou inversement.
Liste circulaire où le dernier élément pointe sur le premier élément de la liste. S'il s'agit d'une
liste doublement chaînée alors de premier élément pointe également sur le dernier.
Ces différents types peuvent être mixés selon les besoins.
NB :
On utilise une liste chaînée plutôt qu'un tableau lorsque l'on doit traiter des objets
représentés par des suites sur lesquelles on doit effectuer de nombreuses suppressions et
de nombreux ajouts. Les manipulations sont alors plus rapides qu'avec des tableaux.

• Les piles
Une pile est une liste chaînée d'informations dans laquelle :
Un élément ne peut être ajouté qu'au sommet de la pile,
Un élément ne peut être retiré que du sommet de la pile.
Il s'agit donc d'une structure de type LIFO (Last In First Out) Traduction : « Le dernier
élément qui a été ajouté est le premier à sortir ».
On ne travaille que sur le sommet de la pile. Les éléments de la pile sont reliés entre eux à
la manière d'une liste chaînée. Ils possèdent un pointeur vers l'élément suivant.
Le dernier élément (tout en bas de la pile) doit pointer vers nil.

Quelque champ d'applications des piles :


Dans un navigateur web, une pile sert à mémoriser les pages Web visitées. L'adresse de
chaque nouvelle page visitée est empilée et l'utilisateur dépile l'adresse de la page
précédente en cliquant le bouton « Afficher la page précédente ».
L'évaluation des expressions mathématiques en notation post fixée (ou polonaise inverse)
utilise une pile.
La fonction « Annuler la frappe » (en anglais « Undo ») d'un traitement de texte mémorise
les modifications apportées au texte dans une pile.
Un algorithme de recherche en profondeur utilise une pile pour mémoriser les nœuds
visités.
Les algorithmes récursifs admis par certains langages (LISP, Algol, Pascal, C, etc.)
utilisent implicitement une pile d'appel. Dans un langage non récursif (FORTRAN par
exemple), on peut donc toujours simuler la récursion en créant les primitives de gestion
d'une pile.
Les opérations par défaut autorisées avec une pile sont :

Empiler : ajoute un élément sur la pile.


Depiler : enlève un élément de la pile et le renvoie.
La déclaration est identique à celle d'une liste chaînée,
Par exemple pour une pile d'entier :
Type categorie = Structure
age : entier
Suivant : Pile
FinStructure
Type Pile = ^categorie

• Les files
Une file est une liste chaînée d'informations qui est basée sur une structure de données basée
sur le principe « Premier entré, premier sorti », en anglais FIFO (First In, First Out), ce qui veut
dire que les premiers éléments ajoutés à la file seront les premiers à être récupérés.
Le fonctionnement ressemble à une file d'attente : les premières personnes à arriver sont les
premières personnes à sortir de la file.
Une file est comparable à une queue de clients à la caisse d'un magasin.
Les files servent à traiter les données dans l'ordre où on les a reçues et permettent de :
- gérer des processus en attente d'une ressource système (par exemple la liste des travaux à
éditer sur une imprimante)
- construire des systèmes de réservation
- certains moteurs multitâches, dans un système d'exploitation, qui doivent accorder du temps-
machine à chaque tâche, sans en privilégier aucune.
- un algorithme de parcours en largeur utilise une file pour mémoriser les nœuds visités.
- on utilise aussi des files pour créer toutes sortes de mémoires tampons (en anglais buffers).
etc.
Pour ne pas avoir à parcourir toute la liste au moment d'ajouter un élément en queue, on
maintient un pointeur de queue. Attention une file peut très bien être simplement chaînée
même s'il y a un pointeur de queue.

Les opérations par défaut autorisées avec une file sont :


- Enfiler toujours à la queue et jusqu'à la limite de la mémoire,
- Défiler toujours à la tête si la file n'est pas vide,
Le pointeur de tête pointe sur le premier élément de la file, et le pointeur de queue sur le dernier.
Il faut commencer par définir un type de variable pour chaque élément de la file. La déclaration
est identique à celle d'une liste chaînée.
- Par exemple pour une file de chaînes de caractères :
Type categorie = Structure
age : entier
Suivant : File
FinStructure
Type File = ^categorie

Vous aimerez peut-être aussi