Vous êtes sur la page 1sur 26

A LGORITHMIQUE

L G

O R I T

H M I Q U E

Notes du cours FS/1/6584


Année préparatoire au master 60 en informatique ULB–UMH
Gilles G EERAERTS (Université Libre de Bruxelles)

A NNÉE ACADÉMIQUE 2008–2009 (2 E ÉDITION )


2

Le personnage en couverture est le mathématicien persan Mohamed I BN M USSA A L -


K HAWARIZMI (783 ?–850 ?) dont le nom a donné le mot français algorithme, car il en
a décrit plusieurs. Le titre d’un de ses ouvrages a également donné à la langue fran-
çaise le mot algèbre. A L -K HAWARIZMI est considéré par beaucoup comme le père de
l’algèbre moderne (il a d’ailleurs introduit l’habitude d’appeller x l’inconnue dans une
équation). Pour autant, ses travaux ne se limitent pas à ce domaine : on lui doit d’im-
portantes contributions dans les domaines de l’arithmétique, de l’astronomie, ou de la
géographie.

Ce document a été mis en page sous LATEX 2ε .


Table des matières

1 Introduction 11
1.1 Le contenu et les objectifs du cours . . . . . . . . . . . . . . . . . . . 11
1.1.1 Algorithmes, Algorithmique . . . . . . . . . . . . . . . . . . 11
1.1.2 Les objectifs et l’orientation du cours . . . . . . . . . . . . . 14
1.2 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

2 Préliminaires 15
2.1 Rappels de mathématiques . . . . . . . . . . . . . . . . . . . . . . . 15
2.1.1 Sommes et produits . . . . . . . . . . . . . . . . . . . . . . . 15
2.1.2 Plancher et plafond . . . . . . . . . . . . . . . . . . . . . . . 16
2.2 Le pseudo-langage . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.2.1 Types et variables . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.2 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.3 Gestion de la mémoire . . . . . . . . . . . . . . . . . . . . . 18

3 Itération, induction, récursivité 21


3.1 Itération et induction . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.1.1 Raisonnement par induction . . . . . . . . . . . . . . . . . . 24
3.2 Preuves de programmes . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2.1 Notion d’invariant de boucle . . . . . . . . . . . . . . . . . . 28
3.2.2 Un exemple de preuve par invariant . . . . . . . . . . . . . . 29
3.3 Récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.3.1 Récursivité et induction . . . . . . . . . . . . . . . . . . . . 32
3.3.2 Fonctions récursives . . . . . . . . . . . . . . . . . . . . . . 33

4 La complexité algorithmique 35
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2 Notion d’efficacité . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2.1 Mesure du temps de calcul . . . . . . . . . . . . . . . . . . . 36
4.2.2 Compter le nombre d’étapes . . . . . . . . . . . . . . . . . . 37
4.3 La notion de grand O . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.3.1 Intuition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.3.2 Définition formelle . . . . . . . . . . . . . . . . . . . . . . . 40
4.3.3 Classes de complexité algorithmique . . . . . . . . . . . . . . 42
4.3.4 Règles de combinaison et de simplification . . . . . . . . . . 42
4.4 Le cas des programmes récursifs . . . . . . . . . . . . . . . . . . . . 44
4.4.1 Utilisation de l’induction . . . . . . . . . . . . . . . . . . . . 45

3
4 TABLE DES MATIÈRES

5 Les listes 49
5.1 Motivation : critique des tableaux . . . . . . . . . . . . . . . . . . . . 49
5.2 Les listes simplement liées . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.1 Comparaison aux tableaux . . . . . . . . . . . . . . . . . . . 52
5.2.2 Algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.2.3 Listes triées . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.3 Les listes circulaires avec élément pré-tête . . . . . . . . . . . . . . . 60
5.3.1 Différences par rapport aux listes « simples » . . . . . . . . . 60
5.3.2 Algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.4 Les listes doublement liées . . . . . . . . . . . . . . . . . . . . . . . 68
5.5 Implémentation des listes dans les vecteurs . . . . . . . . . . . . . . 70
5.5.1 Application . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.6 Vue récursive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.6.1 Algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . 76

6 Les piles et les files 79


6.1 Les piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
6.1.1 Implémentation dans une liste . . . . . . . . . . . . . . . . . 80
6.1.2 Implémentation dans un vecteur . . . . . . . . . . . . . . . . 81
6.2 Les files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.2.1 Implémentation dans une liste . . . . . . . . . . . . . . . . . 85
6.2.2 Implémentation dans un vecteur . . . . . . . . . . . . . . . . 87
6.3 Applications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.3.1 Vérification des parenthèses . . . . . . . . . . . . . . . . . . 92
6.3.2 Évaluation d’une expression en notation postfixée . . . . . . . 93

7 Les arbres 97
7.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
7.1.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.1.2 Vocabulaire . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
7.1.3 Cas particuliers . . . . . . . . . . . . . . . . . . . . . . . . . 102
7.1.4 Implémentation . . . . . . . . . . . . . . . . . . . . . . . . . 105
7.1.5 Application : les arbres d’expressions . . . . . . . . . . . . . 106
7.2 Parcours des arbres binaires . . . . . . . . . . . . . . . . . . . . . . . 109
7.2.1 Parcours préfixé ou parcours en profondeur . . . . . . . . . . 110
7.2.2 Parcours infixe . . . . . . . . . . . . . . . . . . . . . . . . . 115
7.2.3 Parcours postfixé . . . . . . . . . . . . . . . . . . . . . . . . 115
7.2.4 Le parcours par niveaux ou parcours en largeur . . . . . . . . 116
7.2.5 Applications des parcours . . . . . . . . . . . . . . . . . . . 118

8 Algorithmes de recherche 121


8.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
8.2 Données stockées dans des vecteurs . . . . . . . . . . . . . . . . . . 122
8.2.1 Recherche linéaire simple . . . . . . . . . . . . . . . . . . . 122
8.2.2 Recherche linéaire dans un vecteur trié . . . . . . . . . . . . 123
8.2.3 Recherche dichotomique dans un vecteur trié . . . . . . . . . 125
8.2.4 Tables de hachage . . . . . . . . . . . . . . . . . . . . . . . 130
8.3 Données stockées dans des listes . . . . . . . . . . . . . . . . . . . . 131
8.4 Données stockées dans des arbres . . . . . . . . . . . . . . . . . . . 132
8.4.1 Parcours simple . . . . . . . . . . . . . . . . . . . . . . . . . 132
TABLE DES MATIÈRES 5

8.4.2 Arbres binaires de recherche . . . . . . . . . . . . . . . . . . 133

9 Conclusion : aux limites de l’algorithmique 147


6 TABLE DES MATIÈRES
Table des figures

1.1 Une illustration du tri par sélection . . . . . . . . . . . . . . . . . . . 13


1.2 Une illustration de la méthodologie du cours . . . . . . . . . . . . . . 14

3.1 L’ordinogramme de la boucle tant que. . . . . . . . . . . . . . . . . 24

4.1 Comparaison de polynômes de degrés 3 et 4 (petite échelle). . . . . . 40


4.2 Comparaison de polynômes de degrés 3 et 4 (grande échelle). . . . . . 41
4.3 Comparaison de différentes fonctions caractérisant les principales classes
de complexité algorithmique . . . . . . . . . . . . . . . . . . . . . . 42
4.4 Une illustration de l’exécution de Facto(k). . . . . . . . . . . . . . . 47

5.1 Un exemple de liste . . . . . . . . . . . . . . . . . . . . . . . . . . . 51


5.2 Un exemple de suppression dans une liste simplement liée . . . . . . 52
5.3 Un exemple d’insertion dans une liste simplement liée. . . . . . . . . 52
5.4 Un exemple de déplacement d’un élément dans une liste simplement liée 53
5.5 Un exemple de liste circulaire avec élément pré-tête. . . . . . . . . . 62
5.6 Un exemple d’insertion en tête dans une liste circulaire avec élément
pré-tête. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.7 Une liste circulaire avec élément pré-tête vide. . . . . . . . . . . . . . 63
5.8 Illustration de la recherche d’une information dans une liste circulaire
avec élément pré-tête . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.9 Un exemple de liste doublement liée. . . . . . . . . . . . . . . . . . . 68
5.10 Un exemple d’insertion dans une liste doublement liée . . . . . . . . 69
5.11 Un exemple de liste implémentée dans un vecteur, et son équivalent
utilisant des pointeurs. . . . . . . . . . . . . . . . . . . . . . . . . . 71

6.1 Exemple de pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80


6.2 La pile de la Fig. 6.1 implémentée dans une liste. . . . . . . . . . . . 81
6.3 Exemple de manipulation de file dans un vecteur . . . . . . . . . . . 89
6.4 Vecteur implémentant une file vu comme un vecteur circulaire . . . . 90

7.1 Exemple d’arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98


7.2 Exemple d’arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
7.3 La vue récursive de l’arbre de la Fig. 7.2. . . . . . . . . . . . . . . . 100
7.4 Quelques illustrations du vocabulaire sur les arbres. . . . . . . . . . . 101
7.5 Un exemple d’arbre binaire équilibré. . . . . . . . . . . . . . . . . . 102
7.6 Un exemple d’arbre d’expression qui représente (3 + 5) ∗ 2. . . . . . . 109
7.7 Les quatre parcours d’arbre classiques. . . . . . . . . . . . . . . . . . 111
7.8 Illustration du parcours en largeur . . . . . . . . . . . . . . . . . . . 117

7
8 TABLE DES FIGURES

8.1 Illustration de la recherche dichotomique . . . . . . . . . . . . . . . . 126


8.2 Exemple d’arbre binaire de recherche (équilibré) . . . . . . . . . . . 134
8.3 L’ordre sur les nœuds induit par un arbre binaire de recherche . . . . . 135
8.4 Exemples d’insertion dans un ABR . . . . . . . . . . . . . . . . . . . 137
8.5 Illustration des trois cas de suppression dans un ABR . . . . . . . . . 140
8.6 Le détachement du maximum dans un ABR . . . . . . . . . . . . . . 142
8.7 Exemple d’arbre dont la hauteur est en O du nombre de nœuds . . . . 146
78 TABLE DES FIGURES
Chapitre 6

Les piles et les files

Dans ce chapitre, nous allons étudier deux nouvelles structures de données très
couramment utilisées : les files (queues en anglais) et les piles (stacks en anglais).
Les noms de ces structures évoquent clairement leurs fonctionnements respec-
tifs. Toutes deux permettent de stocker des informations ordonnées de façon linéaire
(comme dans une liste). Comme dans une file d’attente dans un commerce, il ne sera
possible d’ajouter de l’information qu’à la fin d’une file, et de n’en prélever qu’au dé-
but. Une pile, par contre, se comporte comme une pile d’assiettes : on n’aura accès
qu’à l’information se trouvant au sommet de la pile, que ce soit lors de l’ajout, de la
suppression ou de la lecture.
Les files et les piles ont énormément d’applications en informatique. Elles sont
présentes dans de nombreux composants d’un système d’exploitation. Elles sont égale-
ment nécessaires pour définir de façon simple et élégante certains algorithmes, comme
nous les verrons à la fin de ce chapitre, et dans le chapitre suivant.
Étant donné que les informations stockées dans une file ou dans une pile le sont de
façon linéaire, on aura recours soit aux listes, soit aux tableaux pour implémenter ces
structures, comme nous le verrons dans ce chapitre.

6.1 Les piles


Une pile est un type de données formé d’une séquence d’éléments a1 , a2 , . . . , an .
Cette séquence peut être vide, ce qu’on représentera par ε. Les opérations autorisées sur
la pile ne peuvent affecter que le dernier élément de la séquence. On appelle ce dernier
élément le sommet de la pile. En effet, on représente une pile comme un empilement
d’éléments, l’élément a1 étant dans le fond de la pile, avec l’élément a2 au-dessus de
lui, etc, jusqu’à avoir an au sommet. Cette représentation intuitive est montrée à la
Fig. 6.1.
Les opérations autorisées sur la pile sont les suivantes :
PileVide Cette opération crée une nouvelle pile, vide (c’est-à-dire ε).
Push Cette opération reçoit en paramètre une pile (vide ou non) et un élément. Elle
ajoute l’élément au sommet de la pile. Plus précisément, si la pile contient la
séquence a1 , a2 , . . . , an , et que l’élément à ajouter est x, la pile résultante sera
a1 , a2 , . . . , an , x. Si la pile reçue est vide, la pile résultante sera simplement x.

79
80 CHAPITRE 6. LES PILES ET LES FILES

F IG . 6.1 – Un exemple de pile contenant (de bas en haut) les valeurs 2, 3, 4 et 5.

Pop Cette opération est symétrique de Push. Elle reçoit une pile, et supprime l’élé-
ment au somment de la pile, s’il existe. Plus formellement, si la pile reçue est
a1 , . . . , an , avec n ≥ 2, la pile résultante est a1 , . . . , an−1 . Si la pile reçue est a1 , la
pile résultante est ε. On ne peut pas faire de Pop sur une pile vide.
Top Cette opération permet de consulter l’information stockée dans la pile. Suivant
la politique de la pile, qui ne donne accès qu’à son sommet, on ne peut consul-
ter que l’information stockée au sommet de la pile. Top reçoit donc une pile
a1 , . . . an , et renvoie an . Top n’est pas définie quand la pile est vide.
EstVide Cette opération reçoit une pile et dit si la pile est vide ou non. Elle renvoie
donc vrai si et seulement si la pile reçue est ε.
Exemple 17 Considérons une pile initialement vide ε. Si nous effectuons le Push de
trois valeurs 5, 7 et 9 (successivement), nous obtenons la pile 5, 7, 9 (le fond est bien
en début de séquence). L’opération EstVide retourne alors faux. En faisant un Pop,
on obtient la pile 5, 7. L’opération Top renvoie alors 7. Avec deux nouveaux Pop, on
obtient la pile vide ε. Il n’est plus possible de faire de Pop ou de Top.
On voit donc que la politique d’accès à la pile ne permet de consulter que l’élément
le plus récemment inséré dans la pile, car c’est celui qui est présent au sommet. Au-
trement dit, le premier élément qu’on pourra supprimer d’une pile est celui qui a été
inséré en dernier. Cette propriété, qui caractérise bien les opérations propres à la pile,
est exprimée en anglais par last in, first out, abrégé en LIFO.

6.1.1 Implémentation dans une liste


Comme nous l’avons dit dans l’introduction de ce chapitre, la structure d’une pile
est la même que celle d’une liste. La différence tient dans les manipulations qui sont
autorisées (des opérations comme la suppression d’un élément arbitraire, par exemple,
ne seront pas autorisées).
On peut donc tout naturellement implémenter une pile dans une liste. Pour faciliter
les opérations, il est plus commode de stocker le sommet de la pile en tête de liste, étant
6.1. LES PILES 81

5 4 3 2 NIL

F IG . 6.2 – La pile de la Fig. 6.1 implémentée dans une liste.

donné que c’est la seule partie de la pile que nous devrons manipuler directement. Les
éléments sont donc stockés dans la liste de haut en bas. La Fig. 6.2 illustre cela.
Ainsi, il est facile d’implémenter les opérations sur la pile à l’aide des opérations
sur la liste :
PileVide Créer une pile vide revient à créer une liste vide, grâce à ListeVide.
Push Le Push revient à effectuer une insertion en tête de la liste, ce qui peut se faire à
l’aide d’InsèreTête.
Pop Le Pop revient à faire une suppression en tête, à l’aide de SuppressionPremier.
Top Le Top revient à consulter l’information stockée dans le premier élément, auquel
on accède à l’aide de la fonction PremierElem.
EstVide La pile est vide si et seulement si la liste est vide. On fait donc appel à la
fonction EstVide définie sur les listes.
Remarquons que toutes ces fonctions sont correctes indépendemment du type de liste
(simplement ou doublement liée, avec ou sans élément pré-tête) choisi.
Comme une pile est une liste, on définira le type Pile comme une structure conte-
nant simplement un pointeur vers la tête de la liste. Ainsi, on cache les détails d’implé-
mentation, et les signatures des fonctions seront les mêmes si on choisit d’implémenter
la pile autrement (voir section suivante) :
struct Pile
Elem * som ;

À titre d’exemple, l’Algorithme 23 donne le code de ces cinq primitives dans le


cas d’une pile d’entiers. Remarquons que toutes les opérations qui modifient la pile
renvoient une nouvelle pile qui est le résultat de la modification1 . Remarquons enfin
que toutes opérations s’effectuent en temps constant.

6.1.2 Implémentation dans un vecteur


Naturellement, il est également possible d’implémenter une pile dans un vecteur.
L’idée consiste à stocker les éléments de bas en haut, c’est-à-dire en mettant l’élément
au fond de la pile dans la première case du vecteur, etc. Cette disposition est naturelle
car la pile ne peut croître que par son sommet. Si celui-ci était stocké au début du
1 On aurait pu éviter cela en passant des pointeurs vers les objets de type pile. Nous considérons néan-

moins qu’il s’agit là d’un détail d’implémentation, qui dépend du langage choisi (certains langages comme
COBOL n’admettent pas les pointeurs, d’autres comme C++ offrent la possibilité d’utiliser des paramètres
par référence, dans d’autres encore, comme Java, on ne manipule que des références, etc.)
82 CHAPITRE 6. LES PILES ET LES FILES

Pile PileVide()
début
Pile P ;
P.som := ListeVide() ;
retournerP ;
fin

Pile Push(Pile P, Entier i)


début
P.som :=InsèreTête(P.som, i) ;
retourner P ;
fin

Pile Pop(Pile P, Entier i)


début
P.som :=SuppressionPremier(P.som) ;
retourner P ;
fin

Entier Top(Pile P)
début
Elem * p :=PremierElem(P.som) ;
retourner p.info ;
fin

Booléen EstVide(Pile P)
début
retourner EstVide(P.som) ;
fin
Algorithme 23 : L’implémentation d’une pile d’entiers dans une liste.
6.2. LES FILES 83

vecteur, on serait obligé de recourir à des tassements et des déplacements du vecteur


à chaque Push ou Pop, pour garder le sommet dans la première case et ne pas perdre
d’information.
Afin de pouvoir réaliser les opérations sur le sommet, il est nécessaire de retenir
où celui-ci se situe dans le vecteur. Cela peut se faire à l’aide d’une variable entière,
qui indique l’indice de la case contenant le sommet. Dans le cas où la pile est vide, on
prendra une valeur conventionnelle (0 par exemple).
On implémentera donc la pile à l’aide d’un vecteur V et d’une variable entière som,
telles que la pile représentée est V [1],V [2], . . . ,V [som] dans la cas où som > 0, et ε, si
som = 0. Dans le cas présent, une pile sera donc plutôt un enregistrement du type :
struct Pile
Entier V [n] ;
Entier som ;

Les opérations sont alors faciles à implémenter :


PileVide Cette opération consiste simplement à initialiser la variable som à 0 pour
indiquer que le vecteur ne contient pas d’éléments de la pile.
Push La nouvelle information peut être stockée en V [som + 1], et som doit être incré-
mentée. Cette opération suppose que le vecteur V est de taille suffisante pour
accueillir le nouvel élément.
Pop Le Pop consiste simplement à décrémenter la variable som. Remarquons que cette
décrémentation est bien cohérente avec notre choix de mettre P.som à 0 quand la
pile est vide.
Top Il suffit de renvoyer l’information stockée au sommet, c’est-à-dire dans V [som].
EstVide La pile est vide si et seulement si la variable som vaut 0.
Ces opérations sont détaillées à l’Algorithme 24, et sont toutes en O(1).

6.2 Les files


Une file est également une séquence a1 , a2 , . . . , an d’éléments, mais les opérations
applicables diffèrent par rapport à la pile. Dans une file, l’écriture d’une part, et la
lecture et l’écriture, d’autre part, ne se font pas « au même endroit » : on peut ajouter
une information en début de file, mais on n’a le droit d’écrire qu’à la fin de la file.
Plus formellement, si la file contient la séquence a1 , a2 , . . . , an , on appellera a1 le
début de la file, et an la fin de la file. Les opérations autorisées sur une file sont les
suivantes :
FileVide Cette opération crée une file vide (dénotée par ε elle aussi).
Enqueue Cette opération reçoit une file et un élément x en paramètre, et ajoute x
dans la file. Si la file est constituée de la séquence a1 , a2 , . . . , an , on obtient la
file contenant la séquence a1 , a2 , . . . , an , x. Si la file est vide, on obtient la file
contenant la séquence x.
Dequeue Cette opération reçoit une file et supprime l’élément en début de file. Si la
file reçue est a1 , . . . , an , avec n ≥ 2, la file résultante est a2 , . . . , an . Si la file reçue
est a1 , la file résultante est la file vide ε. Naturellement, la suppression n’est pas
autorisée sur une file vide.
84 CHAPITRE 6. LES PILES ET LES FILES

Pile PileVide()
début
Pile P ;
P.som := 0 ;
retourner P ;
fin

Pile Push(Pile P, Entier i)


début
P.som := P.som + 1 ;
P.V[P.som] := i ;
retourner P ;
fin

Pile Pop(Pile P)
début
P.som := P.som − 1;
retourner P ;
fin

Entier Top(Pile P)
début
retourner P.V[P.som] ;
fin

Booléen EstVide(Pile P)
début
si P.som = 0 alors
retourner vrai ;
sinon
retourner faux ;
fin
Algorithme 24 : L’implémentation d’une pile d’entiers dans un vecteur.
6.2. LES FILES 85

First Cette opération reçoit une file et renvoie l’élément en début de file. Cette opéra-
tion n’est pas autorisée sur une file vide. Par contre, si la file reçue est a1 , . . . , an ,
le résultat est a1 .
EstVide Cette opération reçoit une file et renvoie vrai si et seulement si la file est vide.

Exemple 18 Considérons la file vide ε. Effectuons trois Enqueue successifs des va-
leurs 5, 7 et 9. On obtient alors la file 5, 7, 9 (c’est-à-dire la même séquence que dans
le cas de la pile). Un Dequeue donne la file 7, 9. Appliquées à cette file, l’opération
First renvoie 7, et l’opération EstVide renvoie faux. En faisant deux Dequeue supplé-
mentaires, on ré-obtient la file vide, sur laquelle ni Dequeue ni First ne peuvent être
appliqués.

6.2.1 Implémentation dans une liste


Il est aisé d’implémenter une file dans une liste. On choisira de mettre le début
de la file en tête de liste, et la fin de la file en fin de liste. Le type File sera alors :
struct File
Elem * deb ;

Cette convention étant fixée, il est facile de traduire les opérations sur une file en
terme d’opérations sur une liste :
FileVide revient à créer une liste vide.
Enqueue revient à ajouter un élément en fin de liste.
Dequeue revient à effectuer une suppression en tête.
First revient à renvoyer l’information stockée dans l’élément de tête.
EstVide revient à tester si la liste est vide.
L’Algorithme 25 détaille ces opérations.
Remarquons que toutes ces opérations s’effectuent en temps constant, sauf l’opéra-
tion Enqueue, qui demande de parcourir toute la liste pour insérer en fin, ce qui est en
O(n). Cette complexité peut être ramenée à O(1) si on garde en permanence un poin-
teur vers le dernier élément de la liste (en plus du pointeur vers la tête). On considère
alors un type File défini comme suit :
struct File
Elem * deb ;
Elem * fin ;

On peut alors adapter les algorithmes manipulant les files pour tenir compte de cette
nouvelle structure. Ceux-ci sont donnés à l’Algorithme 26. Remarquons que, comme
nous utilisons une structure quelque peu différente des listes chaînées simples, nous
sommes obligés d’écrire des algorithmes ad hoc, ce qui est quelque peu moins élégant
que la solution présentée à l’Algorithme 25.
Voici le détail de ces fonctions :
FileVide Il faut prendre garde à mettre la valeur NIL tant dans le pointeur de début
que dans le pointeur de fin.
Enqueue L’accès à la fin de la liste est maintenant simplifié grâce au pointeur de fin.
Il faut d’abord créer l’élément à ajouter, avec l’information i, et le champ next
à NIL (puisque cet élément deviendra le dernier de la liste). Ensuite, on ajoute
86 CHAPITRE 6. LES PILES ET LES FILES

File FileVide()
début
File F ;
F.deb := ListeVide() ;
retourner F ;
fin

File Enqueue(File F, Entier i)


début
F.deb :=InsèreFin(F.deb, i) ;
retourner F ;
fin

File Dequeue(File F)
début
F.deb :=SuppressionPremier(F.deb) ;
retourner F ;
fin

Entier First(File F)
début
Elem * p :=PremierElem(F.deb) ;
retourner p.info ;
fin

Booléen EstVide(File F)
début
retourner EstVide(F.deb) ;
fin
Algorithme 25 : L’implémentation d’un file dans une liste, avec un seul pointeur.
6.2. LES FILES 87

le nouvel élément à la suite de celui pointé par F.fin, à condition que ce dernier
pointeur ne soit pas nul. Dans le cas contraire, la file est en fait vide, et il suffit
de faire pointer à la fois F.deb et F.fin sur le nouvel élément.
Dequeue La suppression en tête se fait de la même manière que dans une liste simple,
en utilisant F.deb comme pointeur tête. Il faut prendre garde à stocker NIL dans
F.fin si cette suppression donne lieu à une file vide (c’est-à-dire si la file ne
contenait qu’un seul élément avant la suppression).
First Cette opération revient à renvoyer l’information stockée dans l’élément de tête.
EstVide Cette opération revient à tester si la liste est vide, ce qui peut se vérifier en
testant un des deux pointeurs.

6.2.2 Implémentation dans un vecteur


Comme dans le cas des piles, il est possible d’implémenter une file dans un vecteur.
La question de l’ordre de stockage se pose à nouveau. Néanmoins, contrairement au cas
de la file, il n’y a pas d’ordre meilleur qu’un autre. En effet, tant le début que la fin de
la file peuvent évoluer, alors que dans le cas de la pile, seul le sommet pouvait être
modifié.
On devra donc repérer dans le vecteur V (qui contient les éléments de la file), deux
positions deb et f in. On adoptera le type suivant :
struct File
Entier V [n] ;
Entier deb ;
Entier f in ;

On stockera les éléments de la file entre les cases deb et f in − 1 (comprises), du


début à la fin de la file. Initialement, nous aurons deb = f in = 1, ce qui produit bien
une file vide (il n’y a pas de cases entre les indices 1 et 0). On stockera les nouvelles
valeurs dans la case f in, que l’on incrémentera alors. Symétriquement, un Dequeue
s’obtiendra simplement en incrémentant deb.
Malheureusement, cette simple politique de gestion du vecteur ne saurait être sa-
tisfaisante. Observons l’exemple de la Fig. 6.3. On y a représenté différentes étapes de
manipulation d’un vecteur contenant une file, et la file correspondante. Initialement, la
file est vide. On effectue ensuite 4 Enqueue, ce qui fait croître la valeur de f in. Puis,
on effectue deux Dequeue. Comme on le voit sur la figure, les cases 1 et 2 deviennent
alors inutilisables, car les éléments qui seront ensuite ajoutés apparaîtront à droite de
la case d’indice f in. Lorsque cet indice atteindra la dernière case du vecteur, il ne sera
plus possible de faire de nouveaux Enqueue. Ainsi, chaque Dequeue diminue la taille
potentielle de la file.
On peut palier ce problème en considérant que le vecteur est circulaire, tel qu’illus-
tré à la Fig. 6.4. Cela signifie qu’on considère qu’il existe une case « à droite » de la
case d’indice n, qui est la case d’indice 1. Ainsi, quand on incrémente f in ou deb, et
que la valeur d’un des deux dépasse n, il convient de la remettre à 1. Ceci peut aisément
s’obtenir à l’aide de l’opérateur modulo.
Dans le cadre de ce cours, nous avons adopté la convention de numéroter les cases
du vecteur de 1 à n. Ainsi, l’opération d’incrémentation « circulaire » d’un indice i
parcourant le tableau est i := (i mod n) + 1. Dans le cas où les cases sont numérotées
88 CHAPITRE 6. LES PILES ET LES FILES

File FileVide()
début
File F ;
F.deb := ListeVide() ;
F.fin := F.deb ;
retourner F ;
fin

File Enqueue(File F, Entier i)


début
Elem * p :=new Elem ;
p.info := i ;
p.next := NIL ;
si EstVide(F) alors
F.deb := p ;
F.fin := p ;
sinon
F.fin.next := p ;
F.fin := p ;
retourner F ;
fin

File Dequeue(File F)
début
Elem * p := F.deb ;
F.deb := F.deb.next ;
delete p ;
si F.deb = NIL alors
F.fin := NIL ;
retourner F ;
fin

Entier First(File F)
début
retourner F.deb.info ;
fin

Booléen EstVide(File F)
début
si F.deb = NIL alors
retourner vrai ;
sinon
retourner faux ;
fin
Algorithme 26 : L’implémentation d’un file dans une liste, avec deux pointeurs.
6.2. LES FILES 89

deb
fin

? ? ? ? ? ε

deb fin

5 7 9 11 ? 5, 7, 9, 11

deb fin

5 7 9 11 ? 9, 11

fin deb

5 7 9 11 13 9, 11, 13

F IG . 6.3 – Un exemple de manipulation de file dans un vecteur. Les cases grisées sont
celles qui font partie de la file.

de 0 à n − 1 (comme dans le langage C ou C++, par exemple), l’opération devient


i := (i + 1)mod n.
Avec cette politique de gestion du vecteur, si la file est stockée dans un vecteur
de taille n, il est à tout moment possible d’y maintenir au plus n − 1 éléments, et ce,
quelque soient les opérations qui ont déjà été effectuées. En effet, la politique de gestion
circulaire permet de « ré-utiliser » les cases laissées libres par les Dequeuesuccessifs,
même si celles-ci sont au début du vecteur.
Par contre, il n’est pas possible de stocker n valeurs dans le vecteur. Imaginons que
le vecteur représente une file qui contient n − 1 valeurs. Dans ce cas, toutes les cases
sont utilisées, sauf une, qui est forcément à l’indice f in. Nous avons deux possibilités :
1. Soit deb > 1. Dans ce cas, f in = deb − 1. Effectuer un Enqueueva avoir pour
effet d’incrémenter f in, et nous arons alors f in = deb.
2. Soit deb = 1. Dans ce cas, la case inutilisée est forcément en fin de vecteur, et
nous avons donc f in = n. En effectuant un Enqueue, on obtient f in = 1 (en
raison du modulo), et donc, à nouveau f in = deb.
Dans les deux cas, on obtient deb égal à f in, ce qui correspond à une file vide, selon
nos conventions. On ne serait donc plus en mesure de différencier une file contenant n
éléments et une file vide. On ne peut donc pas se permettre de stocker un ne élément.
Les fonctions qu’on obtient sont données à l’Algorithme 27. Voici leurs descrip-
tions :
FileVide Il suffit de stocker 1 dans F.deb et F.fin.
90 CHAPITRE 6. LES PILES ET LES FILES

fin deb

5 7 9 11 13 9, 11, 13

fin

13
5

11 7

deb

F IG . 6.4 – Le vecteur implémentant la file vu comme un vecteur circulaire. Les cases


grisées sont celles qui font partie de la file.

Enqueue La nouvelle valeur doit être stockée dans la case d’indice F.fin (pour rappel,
cet indice indique la première case libre, et non pas la dernière case occupée).
On doit ensuite incrémenter F.fin, en tenant compte du caractère circulaire du
vecteur.
On a également tenu compte, dans cette fonction, d’une possibilité de dépasse-
ment de capacité. Si le vecteur contient déjà n − 1 éléments de la file, l’insertion
n’est pas effectuée, et un message d’erreur est affiché. On détecte cette situa-
tion grâce aux deux cas discutés ci-dessus : soit f in = deb − 1, soit deb = 1 et
f in = n.
Dequeue Cette opération consiste simplement à incrémenter F.deb, de façon circu-
laire. Les informations supprimées restent dans le vecteur, mais sont ignorées.
First La case contenant le début de la file est celle d’indice F.deb. On renvoie simple-
ment son contenu.
EstVide La file est vide si et seulement si les indices de début et de fin sont égaux.
Attention, ils ne sont pas forcément égaux à 1 (ils peuvent avoir avancé au fur et
à mesure des Enqueue et Dequeue).

6.3 Applications
Terminons ce chapitre en présentant deux applications des piles (nous verrons
d’autres applications des piles et des files dans le chapitre suivant).
Outre ces applications que nous donnons à titre d’exemple, rappelons tout de même
que les files et les piles ont de nombreuses applications dans les systèmes d’exploita-
tion et les compilateurs. La partie de la mémoire qui retient le contexte d’un processus
6.3. APPLICATIONS 91

File FileVide()
début
File F ;
F.deb := 1 ;
F.fin := 1 ;
retourner F ;
fin

File Enqueue(File F, Entier i)


début
si f in = deb − 1 ou ( f in = n et deb = 1) alors
Afficher « Erreur : dépassement de capacité » ;
sinon
F.V[F.fin] := i ;
F.fin := (F.fin mod n) + 1 ;
retourner F ;
fin

File Dequeue(File F)
début
F.deb := (F.deb mod n) + 1 ;
retourner F ;
fin

Entier First(File F)
début
retourner F.V[F.deb] ;
fin

Booléen EstVide(File F)
début
si F.deb = F.fin alors
retourner vrai ;
sinon
retourner faux ;
fin
Algorithme 27 : L’implémentation d’un file dans une vecteur (circulaire).
92 CHAPITRE 6. LES PILES ET LES FILES

lorsque celui-ci est swappé out est gérée comme une pile (il s’agit du stack système).
Un compilateur gère les contextes des fonctions à l’aide d’une pile également : la fonc-
tion courante a son contexte stocké au sommet de la pile, et les fonctions appelantes
ont leurs contextes respectifs stockés dans la pile, selon l’ordre des appels. Ainsi, un
appel de fonction correspond à un Push, et un retour à un Pop. Enfin, les files trouvent
une application naturelle chaque fois qu’une file d’attente est nécessaire, comme, par
exemple, dans un spooler d’impression, ou dans un serveur web.

6.3.1 Vérification des parenthèses


La première application consiste à écrire un algorithme qui vérifie si une expression
est bien parenthésée. Nous considérerons des expressions arithmétiques, formées de
nombres entiers et des quatre opérateurs. Ces expressions pourront être parenthésées à
l’aide de deux types de parenthèses différentes : () et [].
Une expression est dite est bien parenthésée si :
1. Pour toute parenthèse fermante d’un certain type, il existe une parenthèse ou-
vrante du même type qui la précède, et qui n’a pas encore été fermée.
2. Toute parenthèse ouvrante d’un certain type est fermée par une parenthèse fer-
mante du même type.

Exemple 19 Les expressions suivantes sont bien parenthésées :


– ([3 + 5] ∗ (2 ∗ 9))
– 1+4
– ([(2)])
Les expressions suivantes ne sont pas bien parenthésées :
– (3 + 5] : les types de parenthèses ne correspondent pas.
– ((3) : une des deux parenthèses n’a pas été fermée.
– (4)) : on ferme une parenthèse qui n’a pas été ouverte.

L’algorithme que nous proposons reçoit une expression S, telle que décrite ci-
dessus, et accède à chacun des n éléments qui la composent séquentiellement, et que
nous dénotons par S1 , S2 , . . . , Sn . Par exemple, dans (13 + 2), nous avons S1 = (, S2 =
13, S3 = +, etc.
Cet algorithme considère chaque élément Si l’un après l’autre (i = 1, . . . n). À tout
moment, il doit savoir quelles sont les parenthèses qui sont encore ouvertes, et il doit
également savoir dans quelle ordre ces parenthèses ont été ouvertes. Ainsi, quand il
rencontre une parenthèse fermante, il doit simplement comparer cette parenthèse fer-
mante à la dernière parenthèse ouvrante qui n’a pas encore été fermée, et vérifier que
les types correspondent.
On voit donc qu’il est nécessaire de pouvoir stocker toutes les parenthèses ou-
vrantes, dans l’ordre où elles ont été rencontrées, tout en ne devant maintenir un accès
direct qu’à la dernière parenthèse ouverte qui n’a pas encore été fermée. Ceci peut
aisément être obtenu à l’aide d’une pile :
– À chaque parenthèse ouvrante rencontrée, on effectue un Push.
– À chaque parenthèse fermante rencontrée, on vérifie que la pile n’est pas vide à
l’aide de EstVide. Si la pile est vide, on a trouvé une parenthèse fermante qui
n’a pas été ouverte. Sinon, on vérifie à l’aide de Top que la parenthèse fermante
correspond bien à la dernière parenthèse ouvrante, et on effectue une Pop.
– On ignore les autres caractères.
6.3. APPLICATIONS 93

À la fin du parcours de l’expression, la pile doit être vide, car, dans le cas contraire,
cela signifie qu’une parenthèse ouvrante n’a pas été fermée. L’Algorithme 28 présente
cette solution.

Booléen VérifierParenthèses(Expression S, Entier n)


début
Entier i = 1 ;
Pile P := PileVide() ;
tant que i ≤ n faire
si Si = ( ou Si = [ alors
P :=Push(P, Si ) ;
sinon si Si =) alors
si EstVide(P) ou Top(P) 6= ( alors
retourner faux ;
sinon
P :=Pop(P) ;
sinon si Si =] alors
si EstVide(P) ou Top(P) 6= [ alors
retourner faux ;
sinon
P :=Pop(P) ;
i := i + 1 ;
si EstVide(P) alors
retourner vrai ;
sinon
retourner faux ;
fin
Algorithme 28 : Un algorithme pour vérifier qu’une expression est bien parenthésée.

6.3.2 Évaluation d’une expression en notation postfixée


La seconde application consiste à donner un algorithme qui évalue une expression
arithmétique en notation postfixée. Ces expressions sont quelque peu différentes des
expressions « classiques », en ce sens que les opérateurs arithmétiques sont placés
après les deux opérandes, et non pas entre les deux opérandes.

Exemple 20 Par exemple, l’expression 3 + 2 s’écrit 3 2 + en notation postfixée.


L’expression (3 + 2) ∗ 5 est le produit de l’expression 3 + 2 et de l’expression 5. La
première se note 3 2 + en notation postfixée, et la seconde reste 5. Le produit des deux
se note donc 3 2 + 5 ∗.
Comme on le voit, l’avantage de cette notation est qu’elle permet de se passer des
parenthèses, la priorité étant implicite.

Pour évaluer2 une expression S en notation postfixée, on procède comme suit. On


inspecte chaque élément Si de l’expression l’un après l’autre. Si nous rencontrons un
2 Ce problème n’est pas purement théorique : de nombreux compilateurs convertissent les expressions

arithmétiques en expressions postfixées, et les évaluent, ou génèrent le code pour les évaluer selon un algo-
rithme similaire à celui que nous allons présenter.
94 CHAPITRE 6. LES PILES ET LES FILES

entier, nous devons le retenir jusqu’à ce que nous trouvions l’opérateur qui va lui être
appliqué (et qui le suit forcément). Chaque fois que nous rencontrons un opérateur,
nous savons qu’il s’applique aux deux dernières valeurs calculées. Nous devons donc
retenir toutes les valeurs qui ont été lues, et qui n’ont pas encore servi dans une opé-
ration. Ces valeurs doivent être retenues dans l’ordre où elles ont été lues. Après avoir
effectué le calcul, nous devons également retenir le résultat, puisqu’il pourrait lui aussi
être l’opérande d’un autre opérateur (par exemple, dans 3 2 + 5 ∗, il faut retenir le
résultat de 3 2 +, puisqu’il devra être multiplié par 5).
À nouveau, nous voyons que la mémoire dont nous avons besoin suit une politique
de pile : à tout moment, nous devons retenir toutes les dernières valeurs lues ou cal-
culées, et ce, dans l’ordre où elles l’ont été. Mais pour appliquer un opérateur, nous
n’avons besoin que des deux dernières valeurs.
Notre algorithme parcourra donc l’expression depuis Si jusqu’à Sn , et fonctionnera
ainsi :
– Chaque valeur entière rencontrée est stockée sur la pile grâce à Push.
– À chaque opérateur, on Pop les deux dernières valeurs de la pile, on effectue
l’opération et on Push le résultat.
À la fin, la valeur de l’expression se trouve au sommet de la pile. L’Algorithme 29
présente cette procédure.
6.3. APPLICATIONS 95

Entier ÉvaluationPostFix(Expression S, Entier n)


début
Entier i := 1, x1 , x2 ;
Pile P := PileVide() ;
tant que i ≤ n faire
si Si = + alors
x1 := Top(P) ;
P :=Pop(P) ;
x2 := Top(P) ;
P :=Pop(P) ;
Push(P, x2 + x1 ) ;
sinon si Si = − alors
x1 := Top(P) ;
P :=Pop(P) ;
x2 := Top(P) ;
P :=Pop(P) ;
Push(P, x2 − x1 ) ;
sinon si Si = ∗ alors
x1 := Top(P) ;
P :=Pop(P) ;
x2 := Top(P) ;
P :=Pop(P) ;
Push(P, x2 ∗ x1 ) ;
sinon si Si = / alors
x1 := Top(P) ;
P :=Pop(P) ;
x2 := Top(P) ;
P :=Pop(P) ;
Push(P, x2 /x1 ) ;
sinon
P := Push(P, Si ) ;
i := i + 1 ;
retourner Top(P) ;
fin
Algorithme 29 : Un algorithme pour évaluer une expression postfixée.