Vous êtes sur la page 1sur 219

Programmation et Algorithmique

INF 421, Ecole Polytechnique

Philippe Baptiste et Luc Maranget

Avant-propos
Ce polycopi est utilis pour le cours INF 421 intitul Les bases de la programmation et de
e
e
e
lalgorithmique. Ce cours fait suite au cours INF 311 Introduction a linformatique et prc`de le
`
e e
cours INF 431 intitul Fondements de linformatique. Lobjectif du cours est triple : (1) programe
mer en java, (2) ma
triser les bases de lalgorithmique sur des structures dynamiques (listes,
arbres) et (3) introduire quelques notions fondamentales dinformatique comme les expressions
rguli`res, les automates et lanalyse syntaxique.
e
e
Nous utilisions jusqu` lan pass le polycopi rdig par Jean Berstel et Jean-Eric Pin. Pour
a
e
e e e
prendre en compte les rcents changements dans lorganisation des cours dinformatique de
e

lEcole, nous avons dcid de rdiger une nouvelle version de ce polycopi. Nous esprons quelle
e e
e
e
e
est aussi claire, aussi prcise et aussi simple que la prcdente. Nous avons dailleurs conserv
e
e e
e
de nombreux passages de J. Berstel et J.-E. Pin (en particulier pour les chapitres relatifs aux
arbres).

Nous remercions nos coll`gues de lEcole Polytechnique, et plus particuli`rement ceux qui
e
e
ont dune mani`re ou dune autre contribu au succ`s du cours INF 421 : Philippe Chassignet,
e
e
e
Mathieu Cluzeau, Thomas Heide Clausen, Robert Cori, Xavier Dahan, Olivier Devillers, Thomas
Houtmann, Philippe Jacquet, Fabien Laguillaumie, Fabrice Le Fessant, Laurent Mauborgne,

David Monniaux, Sylvain Pradalier, Alejandro Ribes, Dominique Rossin, Eric Schost, Nicolas
Sendrier, Jean-Jacques Lvy, Franois Morain, Laurent Viennot et Axelle Ziegler. Merci aussi `
e
c
a
Christoph Drr du LIX, qui a magniquement illustr la page de couverture1 de ce polycopi.
u
e
e
Les auteurs peuvent tre contacts par courrier lectronique aux adresses suivantes :
e
e
e
Philippe.Baptiste@polytechnique.fr
Luc.Maranget@inria.fr
On peut aussi consulter la version html de ce polycopi ainsi que les pages des travaux dirigs
e
e
sur le site http ://www.enseignement.polytechnique.fr/informatique/inf421

Le 421 est un jeu de bar qui se joue au comptoir avec des ds.
e

Table des mati`res


e
I

Listes
1 Structure dynamique, structure squentielle
e
2 Listes cha ees, rvisions . . . . . . . . . . .
n
e
3 Tri des listes . . . . . . . . . . . . . . . . . .
4 Programmation objet . . . . . . . . . . . . .
5 Complment : listes boucles . . . . . . . . .
e
e

.
.
.
.
.

7
7
10
23
32
36

.
.
.
.

45
46
51
57
63

III Associations Tables de hachage


1 Statistique des mots . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Table de hachage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3 Choix des fonctions de hachage . . . . . . . . . . . . . . . . . . . . . . . . . . . .

65
65
68
78

II Piles et les
`
1 A quoi ca sert ? . . . . . . . . . . . . . .

2 Implmentation des piles . . . . . . . . .


e
3 Implmentation des les . . . . . . . . .
e
4 Type abstrait, choix de limplmentation
e

IV Arbres
1 Dnitions . . . . . . . . . . . . . . .
e
2 Union-Find, ou gestion des partitions
3 Arbres binaires . . . . . . . . . . . .
4 Arbres de syntaxe abstraite . . . . .
5 Files de priorit . . . . . . . . . . . .
e
6 Codage de Human . . . . . . . . . .

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.

.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

81
81
83
87
92
96
102

V Arbres binaires
113
1 Implantation des arbres binaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
2 Arbres binaires de recherche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
3 Arbres quilibrs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
e
e
VI Expressions rguli`res
e
e
1 Langages rguliers . . . . . . . . . . . . . . . .
e
2 Notations supplmentaires . . . . . . . . . . .
e
3 Programmation avec les expressions rguli`res
e
e
4 Implmentation des expressions rguli`res . .
e
e
e
5 Une autre approche du ltrage . . . . . . . . .
5

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

135
135
138
140
144
150

`
TABLE DES MATIERES

6
VII
Les
1
2
3
4
5
6
A Le
1
2
3
4

automates
Pourquoi tudier les automates . . . . . . . . .
e
Rappel : alphabets, mots, langages et probl`mes
e
Automates nis dterministes . . . . . . . . . .
e
Automates nis non-dterministes . . . . . . . .
e
Automates nis et expressions rguli`res . . . .
e
e
Un peu de Java . . . . . . . . . . . . . . . . . .

co t dun algorithme
u
Une dnition tr`s informelle des algorithmes
e
e
Des algorithmes ecaces ? . . . . . . . . . .
Quelques exemples . . . . . . . . . . . . . . .
Cot estim vs. cot rel . . . . . . . . . . . .
u
e
u e

B Morceaux de Java
1 Un langage plutt classe . . . .
o
2 Obscur objet . . . . . . . . . . .
3 Constructions de base . . . . . .
4 Exceptions . . . . . . . . . . . .
5 Entres-sorties . . . . . . . . . .
e
6 Quelques classes de biblioth`que
e
7 Pi`ges et astuces . . . . . . . . .
e

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

155
155
155
155
158
163
163

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

167
167
167
168
170

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

173
173
177
180
194
198
207
213

Rfrences
ee

215

Index

215

Chapitre I

Listes
1

Structure dynamique, structure squentielle


e

Les structures de donnes en algorithmique sont souvent complexes et de taille variable. Elles
e
sont souvent aussi dynamiques, au sens o` elles voluent en forme et en taille en cours dexcution
u
e
e
dun programme. Les listes sont lexemple le plus simple dune telle structure dynamique.
Laspect dynamique renvoie aussi ` la faon dont la mmoire ncessaire est alloue, la
a
c
e
e
e
mmoire peut tre alloue dynamiquement lors de lexcution du programme, ou statiquement
e
e
e
e
avant lexcution du programme. Lallocation statique suppose que le compilateur arrive `
e
a
conna
tre la taille de mmoire ` allouer (cest-a-dire ` demander au syst`me dexploitation),
e
a
a
e
cette taille est ensuite range dans le chier excutable produit et cest le syst`me dexploitation
e
e
e
qui alloue la mmoire demande au moment de lancer le programme. En Pascal par exemple, la
e
e
taille des tableaux globaux est donne par des constantes, de sorte les tableaux globaux peuvent
e
tre et sont allous statiquement.
e
e
program ;
var
t : array [1..100] of integer ;
begin
.
.
.
end.
Lallocation dynamique de la mmoire existe aussi en Pascal (il existe une fonction new), mais
e
elle est plus dlicate quen Java, car en Pascal, comme dans beaucoup de langages, le proe
grammeur doit rendre explicitement les cellules de mmoire dont il na plus besoin au syst`me
e
e
dexploitation. En revanche, le syst`me dexcution de Java est assez malin pour raliser cette
e
e
e
opration tout seul, ce qui permet de privilgier lallocation dynamique de la mmoire, qui est
e
e
e
bien plus souple que lallocation statique.
En Java toutes les structures de donnes sont alloues dynamiquement gnralement
e
e
e e
par new, mais les cha
nes sont galement alloues dynamiquement. Il nen reste pas moins
e
e
que les tableaux manquent un peu de souplesse, puisque leur taille est xe au moment de la
e
cration. Ainsi, un tableau nest pas tr`s commode pour stocker une suite de points entrs `
e
e
e a
la souris et termine par un double clic. Il faut dabord allouer un tableau de points ` une
e
a
taille par dfaut N suppose susante et ensuite grer le cas o` lutilisateur entre plus de
e
e
e
u
N points, par exemple en allouant un nouveau tableau plus grand dans lequel on recopie les
points dj` stocks. Il est bien plus commode dorganiser ces points comme une suite de petits
ea
e
blocs de mmoire, un bloc contenant un point et un lien vers le bloc suivant. On alloue alors
e
la mmoire proportionnellement au nombre de points stocks sans se poser de questions. Nous
e
e
avons redcouvert la liste (simplement cha ee). Une liste L E de E est :
e
n
7

CHAPITRE I. LISTES
(1) la liste vide,
(2) ou une paire (une cellule) qui contient un lment E et la liste des lments suivants.
ee
ee

Il sagit dune dnition par induction, dans le cas 2 le mot (( liste )) appara ` nouveau. On
e
t a
peut, sans vraiment tout expliquer, dire que lensemble L E est dcrit par lquation suivante.
e
e
L E = { } (E L E )
O` est la liste vide.
u
On notera que la dnition thorique des listes ne fait plus usage du mot lien. Mais d`s que
e
e
e
lon implmente les listes, le (( lien )) revient ncessairement. En Java, une liste est une rfrence
e
e
ee
(voir B.3.1.1), qui est :
(1) soit la rfrence null qui ne pointe nulle part et reprsente la liste vide,
ee
e
(2) ou bien une rfrence qui pointe vers une cellule contenant un lment et une liste.
ee
ee
Autrement dit voici une dnition possible des cellules de liste de points en Java.
e
class PointList {
Point val ;
PointList next ;

// Llment
e e
// La suite

PointList (Point val, PointList next) {


this.val = val ; this.next = next ;
}
}
La cellule de liste est donc un objet de la classe PointList et une valeur de type PointList est
bien une rfrence, puisque les valeurs des objets sont des rfrences. On peut comprendre un
ee
ee
aspect de lorganisation des listes ` laide de dessins qui dcrivent une abstraction de ltat de
a
e
e
la mmoire, cest-`-dire une simplication de cet tat, sans les dtails techniques. Par exemple,
e
a
e
e
on suppose donns deux points p1 et p2 , et on produit les listes :
e
PointList ps = new PointList (p1 , null) ;
PointList qs = new PointList (p2 , ps) ;
Alors, ltat nal de la mmoire est schmatis ainsi :
e
e
e
e
qs

ps
p2

p1

Etant entendu que, dans ce type de schma, les `ches reprsentent les rfrences, la barre
e
e
e
ee
diagonale null , les bo
tes nommes les variables, et les bo
e
tes anonymes les cellules mmoire
e
alloues par new (voir B.3.1).
e
Il semble que ce quest une liste dans la vie de tous les jours est clair : une liste est une
squence dlments. Par exemple la liste des admis ` un concours quelconque, ou la liste de
e
ee
a
courses que lon emporte au supermarch pour ne rien oublier. Lintuition est juste, elle fait
e
en particulier appara (surtout dans le premier exemple) que lordre des lments dune liste
tre
ee
importe. Mais mons nous un peu, car la liste de linformatique est tr`s contrainte. En eet,
e
e
les oprations lmentaires possibles sur une liste sont peu nombreuses :
e
ee
(1) Tester si une liste est la liste vide ou pas ( == null),
(2) et si est (une rfrence vers) une cellule (e, ), on peut
ee
(a) extraire le premier lment e (.val),
ee


1. STRUCTURE DYNAMIQUE, STRUCTURE SEQUENTIELLE

(b) ou bien extraire la liste qui suit (.next).


Pour construite les listes, il ny a que deux oprations,
e
(1) la liste vide existe ( null ),
(2) on peut ajouter un nouvel lment e en tte dune liste (new PointList (e, )).
ee
e
Et cest tout, toutes les autres oprations doivent tre programmes ` partir des oprations
e
e
e a
e
lmentaires.
ee
Une opration frquemment programme est le parcours de tous les lments dune liste.
e
e
e
ee
Cette opration se fait idalement selon un idiome, cest-`-dire selon une tournure frquemment
e
e
a
e
employe qui signale lintention du programmeur, ici une boucle for .
e
// ps est une PointList
for (PointList qs = ps ; qs != null ; qs = qs.next ) {
// Traiter llment qs.val
e e
}
Les programmeurs Java emploient normalement une telle boucle pour signaler que leur code
ralise un simple parcours de liste. D`s lors, la lecture de leur code en est facilite. Notons que
e
e
e
rien, ` part le souci de la coutume, ne nous empche demployer un autre type de boucle, par
a
e
exemple
PointList qs = ps ;
while (qs != null) {
// Traiter llment qs.val
e e
qs = qs.next ;
}
La liste simplement cha ee est une structure squentielle. Une structure squentielle ren
e
e
groupe des lments en squence et lacc`s ` un lment donn ne peut se faire quen parcourant
ee
e
e a
ee
e
la structure, jusqu` trouver llment cherch. Cette technique dacc`s squentiel soppose `
a
ee
e
e e
a
lacc`s direct. Un exemple simple de structure de donnes qui ore lacc`s direct est le tableau.
e
e
e
La distinction entre squentiel et direct se retrouve dans des dispositifs matriels : une bande
e
e
magntique nore quun acc`s squentiel puisquil faut drouler toute la bande pour en lire la
e
e e
e
n ; en revanche la mmoire de lordinateur ore lacc`s direct. En eet, on peut voir la mmoire
e
e
e
comme un grand tableau (doctets) dont les indices sont appels adresses. Lacc`s au contenu
e
e
dune case se fait simplement en donnant son adresse. En fait le tableau est une abstraction de
la mmoire de lordinateur.
e
Exercice 1 Donner deux autres exemples de dispositifs matriels orant acc`s squentiel et
e
e e
acc`s direct.
e
Solution. Lutilisateur qui entre un texte au clavier nore quun acc`s squentiel ` un proe e
a
gramme qui lit ce quil frappe. Un disque dur peut tre considr comme orant lacc`s direct,
e
ee
e
le dispositif dadressage des donnes est juste un peu plus compliqu que celui de la mmoire.
e
e
e
Les donnes sont rparties en cylindres qui correspondent ` des anneaux sur le disque, lacc`s `
e
e
a
e a
un cylindre donn se fait par un dplacement radial de la tte de lecture. Vu du programmeur
e
e
e
il existe donc des chiers ` acc`s uniquement squentiel (lentre standard si elle correspond au
a
e
e
e
clavier) et des chiers ` acc`s direct (lentre standard si elle correspond ` un chier, par une
a
e
e
a
redirection <chier ).
En rsum la liste est une structure dynamique, parce que la mmoire est alloue petit `
e
e
e
e
a
petit directement en fonction des besoins. La liste peut tre dnie inductivement, ce qui rend la
e
e
programmation rcursive assez naturelle (inductif et rcursif sont pour nous informaticiens des
e
e
synonymes). Enn, par nature, la liste est une structure de donnes squentielle, et le parcours
e
e
de la structure est privilgi par rapport ` lacc`s ` un lment arbitraire.
e e
a
e a
ee

10

CHAPITRE I. LISTES

Listes cha ees, rvisions


n
e

Les listes cha ees ont dj` t vues dans le cours prcdent. Nous commenons donc par
n
ea ee
e e
c
quelques rvisions. Attention, nous protons de ce que les listes ont (( dj` t vues en cours ))
e
eaee
pour systmatiser les techniques de programmation sur les listes. Il y a sans doute un vrai prot
e
a
` lire cette section. Si vous en doutez, essayez tout de suite les exercices de la section 2.4.

2.1

Oprations lmentaires sur les listes


e
ee

Comme rvision nous montrons comment raliser les ensembles (dentiers) ` partir des listes
e
e
a
(dentiers). Voici la dnition des cellules de liste dentiers.
e
class List {
int val ;
List next ;

// Llment
e e
// La suite

List (int val, List next) {


this.val = val ; this.next = next ;
}
}
Un ensemble est reprsent par une liste dont tous les lments sont deux ` deux distincts.
e
e
ee
a
Nous ne spcions aucune autre contrainte, en particulier aucun ordre des lments dune liste
e
ee
nest impos. Pour la programmation, un premier point tr`s important ` prendre en compte est
e
e
a
que null est un ensemble valable. Il est alors naturel dcrire des mthodes statiques, car null
e
e
qui ne poss`de aucune mthode est une valeur lgitime pour une variable de type List .
e
e
e
Commenons par calculer le cardinal den ensemble. Compte tenu de ce que les lments des
c
ee
listes sont deux ` deux distincts, il sut de calculer la longueur dune liste. Employons donc la
a
boucle idiomatique
static int card( List xs) {
int r = 0 ;
for ( ; xs != null ; xs = xs.next)
r++ ; // pour r = r+1 ;
return r ;
}
Ce premier code fait appara quil nest pas toujours utile de rserver une variable locale pour
tre
e

la boucle idiomatique. Ecrire xs = xs.next ne fait que changer la valeur de la variable locale
xs (qui appartient en propre ` lappel de mthode) et na pas dimpact sur la liste elle-mme.
a
e
e
Ainsi crire le code suivant ne pose pas de probl`me particulier.
e
e
List xs = new List (1, new List (2, new List (3, new List (4,null)))) l
int size = card(xs) ;
`
A la n du code, la variable xs existe toujours et rfrence toujours la premi`re cellule de la
ee
e
liste {1, 2, 3, 4}. En fait, cette variable xs na rien ` voir avec le param`tre homonyme de la
a
e
mthode card et heureusement.
e
Poursuivons en achant les ensembles, ou, plus exactement, en fabriquant une reprsentation
e
achable des ensembles sous forme de cha
ne. Voici un premier essai dcriture dune mthode
e
e
statique toString dans la classe List avec la boucle idiomatique.
static String toString( List xs) {
String r = "{" ;
for ( ; xs != null ; xs = xs.next )
r = r + xs.val + ", " ;


2. LISTES CHA EES, REVISIONS
IN

11

r = r + "}" ;
return r ;
}
Le code est critiquable :
Lachage est laid, il y a une virgule en trop ` la n. Par exemple, pour la liste des entiers
a
1 et 3, on obtient "{1, 3, }".
Le cot de production de la cha est quadratique en la longueur de la liste. En eet la
u
ne
concatnation de deux cha
e
nes (par +) cote en proportion de la somme des longueurs des
u
cha
nes concatnes, car il faut allouer une nouvelle cha pour y recopier les caract`res des
e e
ne
e
cha
nes concatnes. Pour une liste xs de longueur n on proc`de donc ` n concatnations
e e
e
a
e
de cha
nes, de tailles supposes rguli`rement croissantes ce qui m`ne au nal ` un cot
e
e
e
e
a
u
quadratique.
n(n + 1)
k
k + 2 k + n k =
2
Pour remdier au premier probl`me on peut distinguer le cas du premier lment de la liste.
e
e
ee
Pour remdier au second probl`me il faut utiliser un objet StringBuilder de la biblioth`que
e
e
e
Java (voir B.6.1.4). Les objets StringBuilder sont des cha
nes dynamiques, dont on peut
changer la taille. En particulier, ils poss`dent une mthode append(String str) qui permet
e
e
de leur ajouter une cha str ` la n, pour un cot que lon peut considrer proportionnel `
ne
a
u
e
a
la longueur de str. Bref on a :
static String toString( List xs) {
a
StringBuilder r = new StringBuilder () ; // Un StringBuilder ` nous
r.append("{") ;
i f (xs != null) {
e
e
r.append(xs.val) ; // ajouter lcriture dcimale du premier entier
xs = xs.next ;
// et celles des suivants prfixes par ", "
e
e
for ( ; xs != null ; xs = xs.next )
r.append(", " + xs.val) ;
}
r.append("}") ;
// Renvoyer la cha^ne contenue dans le StringBuilder

return r.toString() ;
}
Apr`s cet chauement, crivons ensuite la mthode mem qui teste lappartenance dun entier
e
e
e
e
a
` un ensemble.
static boolean mem(int x, List xs) {
for ( ; xs != null ; xs = xs.next) {
i f (x == xs.val) return true ;
}
return false ;
}
On emploie la boucle idiomatique, an de parcourir la liste et de retourner de la mthode mem
e
d`s quun entier gal ` x est trouv. Comme on le sait certainement dj` une criture rcursive
e
e
a
e
ea
e
e
est galement possible.
e
static boolean mem(int x, List xs) {
i f (xs == null) {
return false ;

12

CHAPITRE I. LISTES
} else {
return (x == xs.val) || mem(x, xs.next) ;
}

}
La version rcursive provient directement de la dnition inductive des listes, elle tient en deux
e
e
quations :
e
M (x, ) = faux
M (x, (x , X )) = (x = x ) M (x, X )
Ces deux quations sont le support dune preuve par induction structurelle vidente de la core
e
rection de mem.
Alors, que choisir ? De faon gnrale le code itratif (le premier, avec une boucle) est plus
c
e e
e
ecace que le code rcursif (le second), ne serait-ce que parce que les appels de mthode sont
e
e
assez coteux. Mais le code rcursif rsulte dune analyse systmatique de la structure de donnes
u
e
e
e
e
inductive, et il a bien plus de chances dtre juste du premier coup. Ici, dans un cas aussi simple,
e
le code itratif est prfrable (en Java qui favorise ce style).
e
ee

2.2

Programmation s re, style dit fonctionnel


u

Nous envisageons maintenant des mthodes qui fabriquent de nouveaux ensembles. Come
menons par la mthode add qui ajoute un lment ` un ensemble. Le point remarquable est
c
e
ee
a
quun ensemble contient au plus une fois un lment donn. Une fois disponible la mthode mem,
ee
e
e
crire add est tr`s facile.
e
e
static List add(int x, List xs) {
i f (mem(x, xs)) {
return xs ;
} else {
return new List (x, xs) ;
}
}
Attaquons ensuite la mthode remove qui enl`ve un lment x dun ensemble X. Nous
e
e
ee
choisissons de suivre le principe des structures de donnes dites fonctionnelles ou non-mutables
e
ou encore persistantes. Selon ce principe, on ne modie jamais le contenu des cellules de liste.
Cela revient ` considrer les listes comme dnies inductivement par L E = {} (E L E ),
a
e
e
et il est important dadmettre d`s maintenant que cette approche rend la programmation bien
e
plus sre.
u
Pour crire remove, raisonnons inductivement : dans un ensemble vide, il ny a rien ` enlever ;
e
a
tandis que dans un ensemble (une liste) X = (x , X ) on distingue deux cas :
Llment x ` enlever est gal ` x et alors X moins x est X , car X est une liste dlments
ee
a
e
a
ee
deux ` deux distincts et donc X ne contient pas x.
a
Sinon, il faut enlever x de X et ajouter x ` la liste obtenue, dont on sait quelle ne peut
a
.
pas contenir x
Soit en quations :
e
R(x, ) =
R(x, (x, X )) = X
R(x, (x , X )) = (x , R(x, X )) avec x = x
Et en Java :
static List remove(int x, List xs) {
i f (xs == null) {
return null ;


2. LISTES CHA EES, REVISIONS
IN

13

} else i f (x == xs.val) {
return xs.next ;
} else {
return new List (xs.val, remove(x, xs.next)) ;
}
}
En ce qui concerne ltat mmoire, remove(x,xs) renvoie une nouvelle liste (il y a des new),
e
e
dont le dbut est une copie du dbut de xs, jusqu` trouver llment x. Plus prcisment, si
e
e
a
ee
e e
nous crivons
e
List xs = new List (1, new List (2, new List (3, new List (4, null)))) ;
List ys = remove(3, xs) ;
Alors on a les structures suivantes en mmoire
e
xs

ys

(Les cellules alloues par remove sont grises.) On constate que la partie de la liste xs qui
e
e
prc`de llment supprim est recopie, tandis que la partie qui suit llment supprim est
e e
ee
e
e
ee
e
partage entre les deux listes. Dans le style persistant on en vient toujours ` copier les parties
e
a
des structures qui sont changes.
e
Au passage, si on crit plutt
e
o
List xs = new List (1, new List (2, new List (3, new List (4, null)))) ;
xs = remove(3, xs) ;
Alors on obtient
1
xs

Cest-`-dire exactement la mme chose, sauf que la variable xs pointe maintenant vers la nouvelle
a
e
liste et quil ny a plus de rfrence vers la premi`re cellule de lancienne liste. Il en rsulte
ee
e
e
que la mmoire occupe par les trois premi`res cellules de lancienne liste ne peut plus tre
e
e
e
e
accde par le programme. Ces cellules sont devenues les miettes et le gestionnaire de mmoire
e e
e
de lenvironnement dexcution de Java peut les rcuprer. Le ramassage des miettes (garbage
e
e
e
collection) est automatique en Java, et cest un des points forts du langage.

Ecrivons maintenant remove avec une boucle. Voici un premier essai : parcourir la liste xs
en la recopiant en vitant de copier tout lment gal ` x.
e
ee
e
a
static List remove(int x, List xs) {
List r = null ;
for ( ; xs != null ; xs = xs.next ) {
i f (x != xs.val)
r = new List (xs.val, r) ;
}
return r ;
}
Et ici appara une dirence, alors que la version rcursive de remove prserve lordre des
t
e
e
e
lments de la liste, la version itrative inverse cet ordre. Pour sen convaincre, on peut remarquer
ee
e
ee
que lorsque quune nouvelle cellule new List(xs.val, r) est construite, llment xs.val est

14

CHAPITRE I. LISTES
Fig. 1 Les quatre itrations de remove
e
xs

xs
1

4
r

xs
1

xs
3

4
r

ajout au dbut de la liste r, tandis quil est extrait de la n de la liste des lments dj`
e
e
ee
ea
parcourus. On peut aussi schmatiser ltat mmoire sur lexemple dj` utilis. Avant la boucle
e
e
e
ea
e
on a
x 3 xs
1

r
La gure 1 schmatise ltat de la mmoire ` la n de chacune des quatre itrations, la cellule
e
e
e
a
e
nouvelle tant grise. Et enn voici le bilan de ltat mmoire, pour le programme complet.
e
e
e
e
List xs = new List (1, new List (2, new List (3, new List (4, null)))) ;
List ys = remove(3, xs) ;
1

xs

4
ys

Linversion na pas dimportance ici, puisque les ensembles sont des listes dlments deux `
ee
a
deux distincts sans autre contrainte, le code itratif convient. Il nen y irait pas de mme si les
e
e
listes taient ordonnes.
e
e
Il est possible de procder itrativement et de conserver lordre des lments. Mais cest
e
e
ee
un peu plus dicile. On emploie une technique de programmation que faute de mieux nous
appellerons initialisation dire. Jusquici nous avons toujours appel le constructeur des listes
e e
e
a
` deux arguments, ce qui nous garantit une bonne initialisation des champs val et next des
cellules de liste. Nous nous donnons un nouveau constructeur List ( int val) qui initialise


2. LISTES CHA EES, REVISIONS
IN

15

seulement le champ val.1 Le champ next sera aect une seule fois plus tard, ce qui revient
e
moralement ` une initialisation.
a
List (int val) { this.val = val ; }
Pour illustrer linitialisation dire, nous commenons par un exemple plus facile.
ee
c

Exercice 2 Copier une liste en respectant lordre de ses lments, sans crire de mthode
ee
e
e
rcursive.
e
Solution. Lide revient ` parcourir la liste xs en copiant ses cellules et en dlgant linitialisae
a
ee
tion du champ next ` litration suivante. Il y a un petit dcalage ` grer au dbut et ` la n
a
e
e
a e
e
a
du parcours.
static List copy( List xs) {
i f (xs == null) return null ;
// Ici la copie poss`de au moins une cellule, que voici
e
List r = new List (xs.val) ;
// last pointe sur la derni`re cellule de la liste r
e
List last = r ;
// Parcourir la suite de xs
for ( xs = xs.next ; xs != null ; xs = xs.next ) {
// (( initialiser )) last.next ` une nouvelle cellule
a
last.next = new List (xs.val) ;
e
e
/ qui devient donc la derni`re cellule du rsultat /
last = last.next ;
}
// (( initialiser )) le next de la toute derni`re cellule ` une liste vide
e
a
last.next = null ; // Inutile, mais cest plus propre
return r ;
}
On observe que le code construit une liste sur laquelle il maintient deux rfrences, r pointe sur la
ee
premi`re cellule, tandis que last pointe sur la derni`re cellule sauf tr`s bri`vement au niveau
e
e
e
e
du commentaire /**. . . **/, o` last pointe sur lavant-derni`re cellule. Plus prcisment, la
u
e
e e
situation en (( rgime permanent )) avant la ligne qui prc`de le commentaire /**. . . **/ est
e
e e
la suivante :
xs
1
1
r

2 ?
last

Et apr`s ajout dune nouvelle cellule par last.next = new List(xs.val).


e
1

En fait, un champ de type objet non-initialis explicitement contient la valeur par dfaut null, voir B.3.3.
e
e

16

CHAPITRE I. LISTES

xs
1

3 ?

last

(La cellule qui vient dtre alloue est grise). Et enn apr`s excution de laectation last ==
e
e
e
e
e
last.next.
xs
1

1
r

2
2

3 ?

last

On conoit quavec un tel dispositif il est facile dajouter de nouvelles cellules de liste ` la n du
c
a
rsultat et possible de rendre le rsultat (avec r qui pointe sur la premi`re cellule). On observe
e
e
e
le cas particulier trait au dbut, il rsulte de la ncessit didentier le cas o` il ne faut pas
e
e
e
e
e
u
construire la premi`re cellule du rsultat.
e
e
Enn, la mthode itrative construit exactement la mme liste que la mthode rcursive,
e
e
e
e
e
mme si les cellules de listes sont alloues en ordre inverse. En eet la mthode rcursive ale
e
e
e
loue la cellule apr`s que la valeur du champ next est connue, tandis que la mthode itrative
e
e
e
lalloue avant.
La gure 2 donne la nouvelle version itrative de remove, crite selon le principe de linie
e
tialisation dire. Par rapport ` la copie de liste, le principal changement est la possibilit
ee
a
e
darrt de la copie ` lintrieur de la boucle. En eet, lorsque llment ` supprimer est identi
e
a
e
ee
a
e
(x == xs.val), nous connaissons la valeur ` ranger dans last.next, puisque par lhypoth`se
a
e
(( xs est un ensemble )), xs moins llment x est exactement xs.next, on peut donc arrter
ee
e
la copie. On observe galement les deux cas particuliers traits au dbut, ils rsultent, comme
e
e
e
e
dans la copie de liste, de la ncessit didentier les cas o` il ne faut pas construire la premi`re
e
e
u
e
cellule du rsultat. Au nal le code itratif est quand mme plus complexe que le code rcursif.
e
e
e
e
Par consquent, si lecacit importe peu, par exemple si les listes sont de petite taille, la
e
e
programmation rcursive est prfrable.
e
ee
Nous pouvons maintenant tendre les oprations lmentaires impliquant un lment et un
e
e
ee
ee
ensemble et programmer les oprations ensemblistes du test dinclusion, de lunion et de la
e
dirence. Pour programmer le test dinclusion xs ys, il sut de tester lappartenance de
e
tous les lments de xs ` ys.
ee
a
static boolean included( List xs, List ys) {
for ( ; xs != null ; xs = xs.next )
i f (!mem(xs.val, ys)) return false ;
return true ;
}


2. LISTES CHA EES, REVISIONS
IN

17

Fig. 2 Enlever un lment selon la technique de linitialisation dire


ee
ee
static List remove(int x, List xs) {
/* Deux cas particuliers */
i f (xs == null) return null ;
/* Ici xs != null, donc xs.val existe */
i f (x == xs.val) return xs.next ;
/* Ici la premi`re cellule du rsultat existe et contient xs.val */
e
e
List r = new List (xs.val) ;
List last = r ;
/* Cas gnral : copier xs (dans r), jusqu` trouver x */
e e
a
for ( xs = xs.next ; xs != null ; xs = xs.next ) {
i f (x == xs.val) {
// (( initialiser )) last.next ` xs moins xs.val (c-a-d. xs.next)
a
last.next = xs.next ;
return r ;
}
// (( initialiser )) last.next ` une nouvelle cellule
a
last.next = new List (xs.val) ;
last = last.next ;
}
// (( initialiser )) last.next a une liste vide
`
last.next = null ; // Inutile mais cest plus propre.
return r ;
}

Exercice 3 Programmer lunion et la dirence ensembliste.


e
Solution. Pour lunion, il sut dajouter tous les lment dun ensemble ` lautre.
ee
a
static List union( List xs, List ys) {
List r = ys ;
for ( ; xs != null ; xs = xs.next )
r = add(xs.val, r) ;
return r ;
}
On aurait mme pu se passer de la variable r pour crire ys = add(xs.val, ys) et puis
e
e
return ys. Mais la clart en aurait souert.
e
Pour la dirence des ensembles, lg`rement plus dlicate en raison de la non-commutativit,
e
e e
e
e
on enl`ve de xs tous les lments de ys.
e
ee
// Renvoie xs \ ys
static List diff( List xs, List ys) {
List r = xs ;
for ( ; ys != null ; ys = ys.next )
r = remove(ys.val, r) ;
return r ;
}

18

CHAPITRE I. LISTES

2.3

Programmation dangereuse, structures mutables

Les mthodes remove de la section prcdente copient tout ou partie des cellules de la liste
e
e e
passe en argument. On peut, par exemple dans un souci (souvent mal venu, disons le tout de
e
suite) dconomie de la mmoire, chercher ` viter ces copies. Cest parfaitement possible, `
e
e
a e
a
condition de sautoriser les aectations des champs des cellules de liste. On parle alors de structure de donnes mutable ou imprative. Ainsi, dans le remove rcursif on remplace lallocation
e
e
e
dune nouvelle cellule par la modication de lancienne cellule, et on obtient la mthode nremove
e
destructive ou en place suivante
static List nremove(int x, List xs) {
i f (xs == null) {
return null ;
} else i f (x == xs.val) {
return xs.next ;
} else {
xs.next = nremove(x, xs.next) ;
return xs ;
}
}
On peut adapter la seconde version itrative de remove (gure 2) selon le mme esprit, les
e
e
rfrences r et last pointant maintenant, non plus sur une copie de la liste passe en argument,
ee
e
mais sur cette liste elle-mme.
e
static List nremove(int x, List xs) {
/* Deux cas particuliers */
i f (xs == null) return null ;
i f (x == xs.val) return xs.next ;
// Ici la premi`re cellule du rsultat existe et contient xs.val,
e
e
List r = xs ;
List last = r ;
/* Cas gnral : chercher x dans xs pour enlever sa cellule */
e e
for ( xs = xs.next ; xs != null ; xs = xs.next ) {
/ Ici, on a last.next == xs /
i f (x == xs.val) {
// Enlever la cellule pointe par xs
e
last.next = xs.next ;
return r ;
}
last.next = xs ; // Affectation inutile
last = last.next ;
/ Exceptionnellement, on a xs == last /
}
last.next = null ; // Affectation inutile
return r ;
}
On note que la version itrative comporte des aectations de last.next inutiles. En eet, `
e
a
lintrieur de la boucle les rfrences xs et last.next sont toujours gales, car last pointe
e
ee
e
toujours sur la cellule qui prc`de xs dans la liste passe en argument, sauf tr`s bri`vement au
e e
e
e
e
niveau du second commentaire /**. . . **/. En rgime permanent, la situation au dbut dune
e
e
itration de boucle (avant le premier commentaire /**. . . **/) est la suivante
e


2. LISTES CHA EES, REVISIONS
IN

x 3

last

19

xs
1

Et en n ditration, apr`s le second commentaire /**. . . **/, on a


e
e
x 3

last

xs
1

Au dbut de litration suivante on a


e
e
x 3

last
1

xs
2

La recherche de xs.val == x est termine, et le champ next de la cellule last qui prc`de la
e
e e
cellule xs est aect.
e
x 3

last
1

xs
2

Puis la liste r est renvoye.


e
a
Au nal, leet de nremove( int x, List xs) sapparente ` un court-circuit de la cellule
qui contient x. Ainsi excuter
e
List xs = new List (1, new List (2, new List (3, new List (4, null)))) ;
List ys = nremove(3, xs) ;
produit ltat mmoire
e
e
xs
1

ys
Un probl`me majeur survient donc : lensemble xs est modi ! On comprend donc pourquoi
e
e
nremove est dite destructive. On peut se dire que cela nest pas si grave et que lon saura en
tenir compte. Mais cest assez vain, car si on supprime le premier lment de lensemble xs, le
ee
comportement est dirent
e
List xs = new List (1, new List (2, new List (3, new List (4, null)))) ;
List ys = nremove(1, xs) ;
Alors on obtient
xs
1
ys

20

CHAPITRE I. LISTES

Et on observe qualors lensemble xs na pas chang.


e
En conclusion, la programmation des structures mutables est tellement dlicate quil est
e
souvent plus raisonnable de sabstenir de lemployer. Comme toujours en programmation, la
maxime ci-dessus soure des exceptions, dans les quelques cas o` lon poss`de une ma
u
e
trise
compl`te des structures de donnes, et en particulier, quand on est absolument sr que les
e
e
u
structures modies ne sont connues daucune autre partie du programme.
e
Un bon exemple de ce type dexception est la technique dinitialisation dire employe dans
ee
e
la seconde mthode remove itrative de la gure 2. En eet, le champ next est aect une seule
e
e
e
fois, et seulement sur des cellules fra
ches, alloues par la mthode remove elle-mme, et qui ne
e
e
e
sont accessible qu` partir des variables r et last elles-mmes locales ` remove. Par consquent,
a
e
a
e
les modications apportes ` certaines cellules sont invisibles de lextrieur de remove.
e a
e

2.4

Contrle de la rvision
o
e

Nous avons divis les techniques de programmation sur les listes selon deux fois deux
e
catgories. Les listes peuvent tre fonctionnelles (persistantes) ou mutables (impratives), et
e
e
e
la programmation peut tre rcursive ou itrative.
e
e
e
An dtre bien sr davoir tout compris, programmons une opration classique sur les listes
e
u
e
des quatre faons possibles. Concatner deux listes signie, comme pour les cha
c
e
nes, ajouter les
lments dune liste ` la n de lautre. Nommons append la mthode qui concat`ne deux listes.
ee
a
e
e
La programmation rcursive non-mutable rsulte dune analyse simple de la structure inductive
e
e
de liste.
static List append( List xs, List ys) {
i f (xs == null) {
return ys ; // A(, Y ) = Y
} else {
return new List (xs.val, append(xs.next, ys)) ; // A((x, X), Y ) = (x, A(X, Y ))
}
}
Nommons nappend la version mutable de la concatnation. Lcriture rcursive de nappend `
e
e
e
a
partir de la version fonctionnelle est mcanique. Il sut de remplacer les allocations de cellules
e
par des modications.
static List nappend( List xs, List ys) {
i f (xs == null) {
return ys ;
} else {
xs.next = nappend(xs.next, ys)) ;
return xs ;
}
}
Pour la programmation itrative non-mutable, on a recours a linitialisation dire.
e
`
ee
static List append( List xs, List ys) {
i f (xs == null) return ys ;
List r = new List (x.val) ;
List last = r ;
for ( xs = xs.next ; xs != null ; xs = xs.next ) {
last.next = new List (xs.val) ;
last = last.next ;
}
last.next = ys ;
return r ;


2. LISTES CHA EES, REVISIONS
IN

21

}
Pour la programmation mutable et itrative, on supprime les allocations de la version none
mutable et on remplace les initialisations dires par des aectations des champs next de la
ee
liste passe comme premier argument. On nexplicite que la derni`re aectation de last.next
e
e
qui est la seule qui change quelque chose.
static List nappend( List xs, List ys) {
i f (xs == null) return ys ;
List r = xs ;
List last = r ;
for ( xs = xs.next ; xs != null ; xs = xs.next ) {
last = last.next ;
}
last.next = ys ;
return r ;
}
Voici un autre codage plus concis de la mme mthode, qui met en valeur la recherche de la
e
e
derni`re cellule de la liste xs.
e
static List nappend( List xs, List ys) {
i f (xs == null) return ys ;
List last = xs ;
for ( ; last.next != null ; last = last.next )
; // idiome : boucle qui ne fait rien
last.next = ys ;
return xs ;
}
Enn on constate que le cot de la concatnation est de lordre de n oprations lmentaires,
u
e
e
ee
o` n est la longueur de la premi`re liste.
u
e
Exercice 4 Que fait le programme suivant ?
List xs = new List (1, new List (2, new List (3, null))) ;
List ys = nappend(xs, xs) ;
Solution. Le programme aecte la rfrence xs au champ next de la derni`re cellule de la
ee
e
liste xs. Il en rsulte une liste (( boucle )).
e
e
xs
1

ys

Et pour tre vraiment sr davoir compris, nous conseillons encore un exercice, dailleurs un
e
u
peu pig.
e e
Exercice 5 Programmer les deux versions fonctionnelles, itrative et rcursive, de reverse la
e
e
mthode qui inverse une liste.
e
Solution. Une fois nest pas coutume, la version itrative est de loin la plus naturelle.
e

22

CHAPITRE I. LISTES

static List reverse( List xs) {


List r = null ;
for ( ; xs != null ; xs = xs.next )
r = new List (xs.val, r) ;
return r ;
}
En eet, en parcourant (lisant) une squence quelconque dentiers (par ex. en lisant des entiers
e
entrs au clavier) pour construire une liste, la liste est naturellement construite dans lordre
e
inverse de la lecture. De fait, les lments sont ajouts au dbut de la liste construite alors quil
ee
e
e
sont extraits de la n de la squence lue voir aussi le premier remove itratif de la section 2.2.
e
e
Cest souvent embtant, mais ici cest utile.
e

Ecrire la version rcursive est un peu gratuit en Java. Mais la dmarche ne manque pas
e
e
dintrt. Soit R la fonction qui inverse une liste et A la concatnation des listes, le premier
ee
e
lment de la liste que lon souhaite inverser doit aller a la n de la liste inverse. Soit lquation :
ee
`
e
e
R((x, X)) = A(R(X), (x, ))
On est donc tent dcrire :
e e
// Mauvais code, montre ce quil faut viter.
e
static List reverse( List xs) {
i f (xs == null) {
return null ;
} else {
return append(reverse(xs.next), new List (xs.val, null)) ;
}
}
Ce nest pas une tr`s bonne ide, car pour inverser une liste de longueur n, il en cotera au
e
e
u
moins de lordre de n2 oprations lmentaires, correspondant aux n append eectus sur des
e
ee
e
listes de taille n 1, . . . 0.
Un truc des plus classiques vient alors ` notre secours. Le truc revient ` considrer une
a
a
e
mthode auxiliaire rcursive qui prend deux arguments. Nous pourrions donner le truc directee
e
ment, mais prfrons faire semblant de le dduire de dmarches plus gnrales, lune thorique
ee
e
e
e e
e
et lautre pratique.
Commenons par la thorie. Gnralisons lquation prcdente, pour toute liste Y on a
c
e
e e
e
e e
A(R((x, X)), Y ) = A(A(R(X), (x, )), Y )
Or, quelles que soient les listes X, Y et Z, on a A(A(X, Y ), Z) = A(X, A(Y, Z)) (la concatnation
e
est associative). Il vient :
A(R((x, X)), Y ) = A(R(X), A((x, ), Y ))
Autrement dit :
A(R((x, X)), Y ) = A(R(X), (x, Y ))
Posons R (X, Y ) = A(R(X), Y ), autrement dit R renvoie la concatnation de X inverse et de Y .
e
e
Avec cette nouvelle notation, lquation prcdente scrit :
e
e e
e
R ((x, X)), Y ) = R (X, (x, Y ))

Equation qui permet de calculer R rcursivement, puisque de toute vidence R (, Y ) = Y .


e
e

3. TRI DES LISTES

23

private static reverseAux( List xs, List ys) {


i f (xs == null) {
return ys ;
} else {
return reverseAux(xs.next, new List (xs.val, ys)) ;
}
}
Par ailleurs on a R (X, ) = A(R(X), ) = R(X) et donc :
static List reverse( List xs) {
return reverseAux(xs, null) ;
}

Pour lapproche pratique du truc, il faut savoir que lon peut toujours et de faon assez
c
simple remplacer une boucle par une rcursion. Commenons par ngliger les dtails du corps
e
c
e
e
de la boucle de reverse itratif. Le corps de la boucle lit les variables xs et r puis modie r.
e
On rcrit donc la boucle ainsi
ee
for ( ; xs != null ; xs = xs.next )
r = body(xs, r) ;
O` la mthode body est vidente.
u
e
e
Considrons maintenant une itration de la boucle comme une mthode loop. Cette mthode
e
e
e
e
a besoin des param`tres xs et r. Si xs est null , alors il ny a pas ditration (pas dappel de
e
e
body) et il faut passer le rsultat r courant ` la suite du code. Sinon, il faut appeler litration
e
a
e
suivante en lui passant r modi par lappel ` body. Dans ce cas, le rsultat ` passer ` la suite du
e
a
e
a
a
code est celui de litration suivante. On op`re ensuite un petit saut smantique en comprenant
e
e
e
(( passer ` la suite du code )) comme (( retourner en tant que rsultat de la mthode loop )). Soit :
a
e
e
private static List loop( List xs, List r) {
i f (xs == null) {
return r ;
} else {
return loop(xs.next, body(xs, r)) ;
}
}
La mthode loop est dite rcursive terminale car la derni`re opration eectue avant de ree
e
e
e
e
tourner est un appel rcursif. Ou plus exactement lunique appel rcursif est en position dite
e
e
terminale.
Enn, dans le corps de reverse itratif, on remplace la boucle par un appel ` loop.
e
a
static List reverse( List xs) {
List r = null ;
r = loop(xs, r) ;
return r ;
}
On retrouve, apr`s nettoyage, le mme code quen suivant la dmarche thorique. De tous ces
e
e
e
e
discours, il faut surtout retenir lquivalence entre boucle et rcursion terminale.
e
e

Tri des listes

Nous disposons dsormais de techniques de programmation lmentaires sur les listes. Il est
e
ee
temps daborder des programmes un peu plus audacieux, et notamment de considrer des ale
gorithmes. Le tri est une opration frquemment eectue. En particulier, les tris interviennent
e
e
e

24

CHAPITRE I. LISTES

souvent dans dautres algorithmes, dans le cas frquent o` il plus simple ou plus ecace deece
u
tuer des traitements sur des donnes prises dans lordre plutt que dans le dsordre.
e
o
e
Exercice 6 Programmer la mthode static
e
dune liste.

List uniq( List xs) qui limine les doublons


e

Solution. Dans une liste trie les lments gaux se suivent. Pour enlever les doublons dune
e
ee
e
liste trie, il sut donc de parcourir la liste en la copiant et dviter de copier les lments gaux
e
e
ee
e
a
` leur successeur dans la liste.
private static List uniqSorted( List xs) {
e
e e
i f (xs == null || xs.next == null) { // xs a zro ou un lment
return xs ;
} // desormais xs != null && xs.next != null
else i f (xs.val == xs.next.val) ;
return uniqSorted(xs.next) ;
} else {
return new List (xs.val, uniqSorted(xs.next)) ;
}
}
La smantique squentielle du ou logique || permet la concision (voir B.7.3). En supposant
e
e
donne une mthode sort de tri des listes, on crit la mthode uniq qui accepte une liste
e
e
e
e
quelconque.
static List uniq( List xs) { return uniqSorted(sort(xs)) ; }
On peut discuter pour savoir si uniq ci-dessus est plus simple que la version sans tri ci-dessous
ou pas.
static List uniq( List xs) {
List r = null ;
for ( ; xs != null ; xs = xs.next )
i f (!mem(x.val, xs))
r = new List (xs.val, r) ;
return r ;
}
En revanche, comme on peut trier une liste au prix de de lordre de n log n oprations lmentaires
e
ee
(n longueur de xs), et que le second uniq eectue au pire de lordre de n2 oprations lmentaires,
e
ee
il est certain que le premier uniq est asymptotiquement plus ecace.
Les algorithmes de tri sont nombreux et ils ont fait lobjet danalyses tr`s fouilles, en raison
e
e
de leur intrt et de leur relative simplicit. Sur ce point [6] est une rfrence fondamentale.
ee
e
ee

3.1

Tri par insertion

Ce tri est un des tris de listes les plus naturels. Lalgorithme est simple : pour trier la liste xs,
on la parcourt en insrant ses lments ` leur place dans une liste rsultat qui est donc trie.
e
ee
a
e
e
Cest la technique gnralement employe pour trier une main dans les jeux de cartes.
e e
e
static List insertionSort( List xs) {
List r = null ;
for ( ; xs != null ; xs = xs.next )
r = insert(xs.val, r) ;
return r ;
}

3. TRI DES LISTES

25

Il reste ` crire linsertion dans une liste trie. Lanalyse inductive conduit ` la question suivante :
ae
e
a
soit x un entier et Y une liste trie, quand la liste (x, Y ) est elle trie ? De toute vidence cela
e
e
e
est vrai, si
Y est la liste vide ,
ou bien, Y = (y, Y ) et x y.
En eet, si x y, alors tous les lments de Y trie sont plus grands (au sens large) que x, par
ee
e
transitivit de . Notons P (X) le prdicat dni par
e
e
e
P (x, ) = vrai

P (x, (y, Y )) = x y

Soit I fonction dinsertion, selon lanalyse prcdente on pose


e e
I(x, Y ) = (x, Y )

si P (x, Y )

Si P (x, Y ) est invalide, alors Y scrit ncessairement Y = (y, Y ) et on est tent de poser
e
e
e
I(x, (y, Y ) = (y, I(x, Y ))
Et on aura raison. On montre dabord (par induction structurelle) le lemme vident que I(x, Y )
e
regroupe les lments de Y plus x. Ensuite on montre par induction structurelle que I(x, Y ) est
ee
trie.
e
Base Si P (x, Y ) est vrai (ce qui comprend le cas Y = ), alors I(x, Y ) est trie.
e

Induction Sinon, on a Y = (y, Y ). Par hypoth`se dinduction I(x, Y ) est trie. Par ailleurs,
e
e
) sont ceux de Y plus x, et donc y est plus petit (au sens large)
les lments de I(x, Y
ee
que tous les lments de I(x, Y ) (par hypoth`se (y, Y ) trie et P (x, Y ) invalide). Soit
ee
e
e
nalement (y, I(x, Y )) est trie.
e
Il reste ` programmer dabord le prdicat P ,
a
e
private static boolean here(int x, List ys) {
return ys == null || x <= ys.val ;
}

puis la mthode dinsertion insert.


e
private static List insert(int x, List ys) {
i f (here(x, ys)) {
return new List (x, ys) ;
} else { // NB: !here(ys) => ys != null
return new List (ys.val, insert(x, ys.next)) ;
}
}
Penchons nous maintenant sur le cot de notre implmentation du tri par insertion. Par cot
u
e
u
nous entendons dabord temps dexcution. Les constructions lmentaires de Java employes2
e
ee
e
sexcutent en temps constant cest ` dire que leur cot est born par une constante. Il en rsulte
e
a
u
e
e
que mme si le cot dun appel ` la mthode here est variable, selon que le param`tre ys vaut
e
u
a
e
e
null ou pas, ce cot reste infrieur ` une constante correspondant ` la somme des cots dun
u
e
a
a
u
appel de mthode ` deux param`tres, dun test contre null , dun acc`s au champs val etc.
e
a
e
e
Il nen va pas de mme de la mthode insert qui est rcursive. De fait, le cot dun appel
e
e
e
u
a
` insert sexprime comme une suite I(n) dune grandeur qui exprime la taille de lentre, ici
e
la longueur de la liste ys.
I(0) k0
2

I(n + 1) I(n) + k1

Attention, ce nest pas toujours le cas, une allocation de tableau par exemple prend un temps proportionnel
` la taille du tableau. Par ailleurs nous ngligeons le cot du garbage collector.
a
e
u

26

CHAPITRE I. LISTES

Notons que pour borner I(n + 1) nous avons suppos quun appel rcursif tait eectu. Il est
e
e
e
e
donc immdiat que I(n) est majore par k1 n + k0 . En premi`re approximation, la valeur des
e
e
e
constantes k1 et k0 importe peu et on en dduit linformation pertinente que I(n) est en O(n).
e
En regroupant le cot des diverses instructions de cot constant de la mthode insertionSort
u
u
e
selon quelles sont excutes une ou n fois, et en tenant compte des n appels ` insert, le
e e
a
cot S(n) dun appel de cette mthode est born ainsi :
u
e
e
n1

S(n)

k=0

I(k) + k2 n + k3 k1

n(n 1)
+ (k0 + k2 ) n + k3
2

Et donc le cot de insertionSort est en O(n2 ). Il sagit dun cot dans le cas le pire en
u
u
raison des majorations eectues (ici sur I(n) principalement). Notons que la notation f (n)
e
est en O(g(n)) (il existe une constante k telle que, pour n assez grand, on a f (n) k g(n))
est particuli`rement justie pour les cots dans le cas le pire. Il faut noter que lon peut
e
e
u
gnralement limiter les calculs aux termes dominants en n. Ici on dira, insertionSort appelle
e e
n fois insert qui est en O(k) pour k n, et que insertionSort est donc en O(n2 ).
On peut aussi compter prcisment une opration en particulier. Pour un tri on a tendance
e e
e
a
` compter les comparaisons entre lments. La dmarche se justie de deux faons : lopration
ee
e
c
e
compte est la plus coteuse (ici que lon songe au tri des lignes dun chier par exemple),
e
u
ou bien on peut aecter un compte born doprations lmentaires ` chacune des oprations
e
e
ee
a
e
comptes. Nous y reviendrons, mais comptons dabord les comparaisons eectues par un appel
e
e
a
` insert.
1 I(k + 1) k

I(0) = 0

On encadre ensuite facilement le nombre de comparaisons eectues par insertionSort pour


e
une liste de taille n + 1.
n S(n + 1)

n(n + 1)
2

Par consquent S(n) eectue au plus de lordre de n2 comparaisons et au moins de lordre


e
de n comparaisons. (( De lordre de )) a ici le sens prcis que nous avons trouv des ordres de
e
e
grandeur asymptotiques pour n tendant vers +. Plus exactement encore, S(n) est borne
e
2 ) et borne infrieurement par une une fonction en
suprieurement par une fonction en (n
e
e
e
(n). Il est rappel (cf. le cours prcdent) que f est dans (g) quand il existe deux constantes
e
e e
k1 et k2 telles que, pour n assez grand, on a
k1 g(n) f (n) k2 g(n)
Par ailleurs, nous disposons dune estimation raliste du cot global. En eet, dune part, `
e
u
a
une comparaison eectue dans un appel donn ` insert nous associons toutes les oprations du
e
ea
e
corps de cette mthode ; et dautre part, le cot des oprations en temps constant eectues lors
e
u
e
e
dune itration de la boucle de insertionSort peut tre aect aux comparaisons eectues
e
e
e
e
par lappel ` insert de cette itration.
a
e
Exercice 7 Donner des expriences particuli`res qui conduisent dune part ` un cot en n2 et
e
e
a
u
dautre ` un cot en n. Estimer le cot du tri de listes dentiers tirs au hasard.
a
u
u
e
Solution. Trier des listes de taille croissantes et dj` tries en ordre croissant donne un cot
ea e
u
2 , car les insertions se font toujours ` la n de la liste r. Mais si les listes sont tries en
en n
a
e
ordre dcroissant, alors les insertion se font toujours au dbut de la liste r et le cot est en n.
e
e
u

3. TRI DES LISTES

27

Enn dans une liste quelconque, les insertions se feront plutt vers le milieu de la liste r et le
o
cot sera plutt en n2 .
u
o
Plus rigoureusement, on peut estimer le nombre moyen de comparaisons, au moins dans le cas
plus facile du tri des listes de n entiers deux ` deux distincts. Considrons linsertion du k-i`me
a
e
e
lment dans la liste r trie qui est de taille k 1. Clairement, parmi toutes les permutations
ee
e
de k lments distincts, les sites dinsertion du dernier lment parmi les k 1 premiers tris se
ee
ee
e
rpartissent uniformment entre les k sites possibles. Dans ces conditions, si les permutations
e
e
sont quiprobables, le nombre moyen de comparaisons pour une insertion est donc
e
1
1
(1 + 2 + + k 1 + k 1) =
k
k

k(k + 1)
1
2

Par ailleurs, les permutations de n lments distincts se repartissent uniformment selon les
ee
e
ensembles de leurs k premiers lments. On a donc le cot en moyenne
ee
u
1

S(n) = 0 +
2

1
2(2 + 1)
1 + +
2
k

1
k(k + 1)
1 + +
2
n

n(n + 1)
1
2

Autrement dit
1
3
(n + 1)(n + 2)

ln n + O(1) = n2 + n ln n + O(1)
S(n) =
4
4
4
Soit nalement le cot moyen est en (n2 ). On note au passage que le cot moyen est ` peu
u
u
a
pr`s en rapport moiti du cot dans le cas le pire, ce qui conrme peut-tre le rsultat de notre
e
e
u
e
e
premier raisonnement hasardeux.
On peut pareillement estimer le cot en mmoire. Ici on peut se contenter de compter les
u
e
cellules de liste alloues, et voir quun tri alloue au plus de lordre de n2 cellules et au moins de
e
lordre de n cellules.

3.2

Tri fusion

Le tri par insertion rsulte essentiellement de lopration dinsertion dans une liste trie. Un
e
e
e
autre tri plus ecace rsulte de lopration de fusion de deux listes tris. Par dnition, la fusion
e
e
e
e
de deux listes tries est une liste trie qui regroupe les lments des deux listes fusionnes. Cette
e
e
ee
e
opration peut tre programme ecacement, mais commenons par la programmer correctee
e
e
c
ment. Sans que la programmation soit bien dicile, cest notre premier exemple de fonction qui
op`re par induction sur deux listes. Soit M (X, Y ) la fusion (M pour merge). Il y a deux cas `
e
a
considrer
e
Base De toute vidence, M (, Y ) = Y et M (X, ) = X
e

Induction On a X = (x, X ) et Y = (y, Y ). On distingue deux cas ` peu pr`s symtriques.


a
e
e
Si x y, alors x minore non seulement tous les lments de X (X est trie) mais aussi
ee
e
tous les lments de Y (transitivit de et Y est trie). Procdons ` lappel rcursif
ee
e
e
e
a
e
M (X , Y ), qui par hypoth`se dinduction renvoie une liste trie regroupant les lments
e
e
ee
de X et de Y . Donc, x minore tous les lments de M (X , Y ) cest ` dire que la liste
ee
a
, Y )), qui regroupe bien tous les lments de X et Y , est trie. On pose donc
(x, M (X
ee
e
M (X, Y ) = (x, M (X , Y )),

quand X = (x, X ), Y = (y, Y ) et x y

Sinon, alors y < x et donc y x. On raisonne comme ci-dessus, soit on pose


M (X, Y ) = (y, M (X, Y )),

quand X = (x, X ), Y = (y, Y ) et x > y

28

CHAPITRE I. LISTES

Et en Java, il vient
static List merge( List xs, List ys) {
i f (xs == null) {
return ys ;
} else i f (ys == null) {
return xs ;
e
} // NB: dsormais xs != null && ys != null
else i f (xs.val <= ys.val) {
return new List (xs.val, merge(xs.next, ys)) ;
} else { // NB: ys.val < xs.val
return new List (ys.val, merge(xs, ys.next)) ;
}
}
On observe que la mthode merge termine toujours, car les appels rcursifs seectuent sur une
e
e
structure plus petite que la structure passe en argument, ici une paire de listes.
e
Estimons le cot de la fusion de deux listes de tailles respectives n1 et n2 , en comptant les
u
comparaisons entre lments. Au mieux, tous les lments dune des listes sont infrieurs ` ceux
ee
ee
e
a
de lautre liste. Au pire, il y a une comparaison par lment de la liste produite.
ee
min(n1 , n2 ) M (n1 , n2 ) n1 + n2
Par ailleurs M (n1 , n2 ) est une estimation raisonnable du cot global, puisque compter les comu
paraisons revient quasiment ` compter les appels et que, hors un appel rcursif, un appel donn
a
e
e
eectue un nombre born doprations toutes en cot constant.
e
e
u
Trier une liste xs ` laide de la fusion est un bon exemple du principe diviser pour rsoudre.
a
e
On spare dabord xs en deux listes, que lon trie (inductivement) puis on fusionne les deux
e
listes. Pour sparer xs en deux listes, on peut regrouper les lments de rangs pair et impair
e
ee
dans respectivement deux listes, comme proc`de le dbut du code de la mthode mergeSort.
e
e
e
static List mergeSort( List xs) {
List ys = null, zs = null ;
e
boolean even = true ; // zro est pair
for ( List p = xs ; p != null ; p = p.next ) {
i f (even) {
ys = new List (p.val, ys) ;
} else {
zs = new List (p.val, zs) ;
}
even = !even ; // k+1 a la parit oppose ` celle de k
e
e a
}
.
.
.
Il reste ` procder aux appels rcursifs et ` la fusion, en prenant bien garde dappeler rcursivement
a
e
e
a
e
mergeSort exclusivement sur des listes de longueur strictement plus petite que la longueur de xs.
.
.
.
i f (zs == null) { // xs a zro ou un lment
e
e e
return xs ; // et alors xs est trie
e
} else { // Les longueurs de ys et sz sont < ` la longueur de xs
a
return merge(mergeSort(ys), mergeSort(zs)) ;
}
}

3. TRI DES LISTES

29

Comptons les comparaisons qui sont toutes eectues par la fusion :


e
S(0) = 0

S(n) = M (n/2 , n/2) + S(n/2) + S(n/2), pour n > 1

S(1) = 0

Soit lencadrement
n/2 + S(n/2) + S(n/2) S(n) n + S(n/2) + S(n/2)
Si n = 2p+1 on a la simplication tr`s nette
e
2p + 2 S(2p ) S(2p+1 ) 2p+1 + 2 S(2p )
Ou encore
1/2 + S(2p )/2p S(2p+1 )/2p+1 1 + S(2p )/2p
Les rcurrences sont de la forme T (0) = 0, T (p + 1) = k + T (p) de solution k p. On a donc
e
lencadrement
1/2 p 2p S(2p ) p 2p
Soit un cot de lordre de n log n qui se gnralise ` n quelconque (voir lexercice). Le point
u
e e
a
important est que le tri fusion eectue toujours de lordre de n log n comparaisons dlments.
ee
Par ailleurs, le compte des comparaisons estime le cot global de faon raliste, puisque que
u
c
e
lon peut aecter le cot de entre une et deux itrations de la boucle de sparation ` chaque
u
e
e
a
comparaison de la fusion.
Exercice 8 (Franchement optionnel) Gnraliser lordre de grandeur du compte des come e
paraisons ` n quelconque.
a
Solution. Dnissons une premi`re suite U (n) pour minorer S(n)
e
e
U (0) = 0

U (1) = 0

U (2q +2) = q +1+2U (q +1)

U (2q +3) = q +1+U (q +1)+U (q +2)

Notons que nous avons simplement dtaill


e
e
U (n) = n/2 + U (n/2) + U (n/2)
Si on y tient on peut montrer U (n) S(n) par une induction facile. Par ailleurs on sait dj`
ea
que U (2p ) vaut 1/2 p 2p . Toute lastuce est de montrer que pour p assez grand et n tel que
2p n < 2p+1 , on a U (2p ) U (n) < U (2p+1 ) Posons D(n) = U (n + 1) U (n). Il vient alors
D(0) = 0

D(1) = 1

D(2q + 2) = D(q + 1)

D(2q + 3) = 1 + D(q + 1)

La suite D(n) est donc de toute vidence ` valeurs dans N (cest-`-dire D(n) 0) et on a
e
a
a
certainement D(n) > 0 pour n impair. Ce qui sut pour prouver lencadrement pour tout p. En
eet U (2p ) U (n) rsulte de D(n) 0, tandis U (n) < U (2p+1 ) rsulte de ce que 2p+1 1 est
e
e
impair.
Soit maintenant n > 0, il existe un unique p, avec 2p n < 2p+1 et on a donc
1/2 p 2p U (n)
Par ailleurs, la fonction x log2 (x) est croissante. Or, pour n > 1 on a n/2 < 2p , il vient donc
1/2 log2 (n/2) n/2 U (n)

30

CHAPITRE I. LISTES
De mme, on borne suprieurement S(n) par la suite
e
e
V (0) = 0

V (1) = 0

V (n) = n + V (n/2) + V (n/2))

Suite dont on montre quelle est croissante, puis majore par log2 (2n) 2n. Soit enn
e
1/2 log2 (n/2) n/2 S(n) log2 (2n) 2n
On peut donc nalement conclure que S(n) est en (nlog n), ce qui est lessentiel. Si on souhaite
estimer les constantes caches dans le et pas seulement lordre de grandeur asymptotique, notre
e
encadrement est sans doute un peu large.

3.3

Trier de grandes listes

Pour conrmer le ralisme des analyses de complexit des tris, nous trions des listes de N
e
e
entiers et mesurons le temps dexcution des deux tris par insertion I et fusion M. Les lments
e
ee
des listes sont les N premiers entiers dans le dsordre.
e
30

20
3

T (s) 15
10
5

3
+

I
M

25

3 3
3 3
+ + + + + + +
3 3 3 3 3 3 3 + + + + + + + + + + + + +
0
0
1000 2000 3000 4000 5000 6000 7000 8000 9000 10000
N
Cette exprience semble conrmer que le tri par insertion est quadratique. Elle montre surtout
e
que le tri fusion est bien plus ecace que le tri par insertion.
Encourags, nous essayons de mesurer le temps dexcution du tri fusion pour des valeurs
e
e
de N plus grandes. Nous obtenons :
N=10000 T=0.093999
N=20000 T=0.411111
N=30000 T=0.609787
Exception in thread "main" java.lang.StackOverflowError
at List.merge(List.java:80)
at List.merge(List.java:78)
.
.
.
O` de lordre de 1000 lignes at List.merge. . . sont eaces.
u
e
On voit que notre programme choue en signalant que lexception StackOverowError
e
(dbordement de la pile) a t lance. Nous expliquerons plus tard prcisment ce qui se passe.
e
ee
e
e e
Pour le moment, contentons nous dadmettre que le nombre dappels de mthode en attente est
e
limit. Or ici, merge appelle merge qui appelle merge qui etc. et merge doit attendre le retour
e
de merge qui doit attendre le retour de merge qui etc. Et nous touchons l` une limite de la
a
programmation rcursive. En eet, alors que nous avons programm une mthode qui semble
e
e
e

3. TRI DES LISTES

31

thoriquement capable de trier des centaines de milliers dentiers, nous ne pouvons pas lexcuter
e
e
au del` de 30000 entiers.
a
Pour viter les centaines de milliers dappels imbriqus de merge nous pouvons reprogrammer
e
e
la fusion itrativement. On emploie la technique de linitialisation dire (voir lexercice 2),
e
ee
puisquil est ici crucial de construire la liste rsultat selon lordre des listes consommes.
e
e
static List merge( List xs, List ys) {
i f (xs == null) return ys ;
i f (ys == null) return xs ;
/* Ici le resultat a une premi`re cellule */
e
/* reste ` trouver ce qui va dedans */
a
List r = null ;
i f (xs.val <= ys.val) {
r = new List (xs.val) ; xs = xs.next ;
} else {
r = new List (ys.val) ; ys = ys.next ;
}
List last = r ; // Derni`re cellule de la liste rsultat
e
e
/* Rgime permanent */
e
while (xs != null && ys != null) {
i f (xs.val <= ys.val) {
last.next = new List (xs.val) ; xs = xs.next ;
} else {
last.next = new List (ys.val) ; ys = ys.next ;
}
last = last.next ; // Derni`re cellule ` nouveau
e
a
}
/* Ce nest pas fini, une des deux listes peut ne pas ^tre vide */
e
i f (xs == null) {
last.next = ys ;
} else {
last.next = xs ;
}
return r ;
}
La nouvelle mthode merge na rien de dicile ` comprendre, une fois assimile linitialisation
e
a
e
dire. Il faut aussi ne pas oublier dinitialiser le champ next de la toute derni`re cellule alloue.
ee
e
e
Mais attendons ! Le nouveau tri peut il fonctionner en pratique, puisque mergeSort elle mme
e
est rcursive ? Oui, car mergeSort sappelle sur des listes de longueur divise par deux, et donc le
e
e
nombre dappels imbriqus de mergeSort vaut au plus ` peu pr`s log2 (N ), certainement infrieur
e
a
e
e
a
` 32 dans nos expriences o` N est un int . Ceci illustre quil ny a pas lieu de (( drcursiver ))
e
u
ee
systmatiquement.
e
Plutt que de simplement mesurer le temps dexcution du nouveau mergeSort, nous allons
o
e
le comparer ` un autre tri : celui de la biblioth`que de Java. La mthode statique sort (classe
a
e
e
Arrays, package java.util) trie un tableau de int pass en argument. Puisque nous tenons
e
a
` trier une liste, il sut de changer cette liste en tableau, de trier le tableau, et de changer le
tableau en liste.
static List arraySort( List xs) {
// Copier les elments de xs dans un tableau t
e
int sz = card(xs) ; // Longueur de la liste
int [] t = new int[sz] ;
int k = 0 ;
for ( List p = xs ; xs != null ; xs = xs.next, k++) {

32

CHAPITRE I. LISTES
t[k] = xs.val ;
}
// Trier t
java.util.Arrays.sort(t) ;
// Fabriquer une liste avec les lments de t, attention ` lordre !
e e
a
List r = null ;
for (k = sz-1 ; k >= 0 ; k--) {
r = new List (t[k], r) ;
}
return r ;

}
Employer une mthode de tri de la biblioth`que est probablement une bonne ide, car elle est
e
e
e
crite avec beaucoup de soin et est donc tr`s ecace [1]. Observons quen supposant, et cest
e
e
plus que raisonnable, un cot du tri en O(N log N ), qui domine asymtotiquement nos transferts
u
de liste en tableau et rciproquement (en O(N )), on obtient un tri en O(N log N ). Dans les
e
mesures, M est notre nouveau tri fusion, tandis que A est le tri arraySort.
5
4.5
4
3.5
3
T (s) 2.5
2
1.5
1
0.5
0

3 3

3
3 3

3
M 3 3
A +
3 3 3

3
3
+
3 + + + + + + + + + + + + + + + + + +
+
0
50000
100000
150000
200000
250000
300000
N

On voit que notre mthode mergeSort conduit ` un temps dexcution qui semble ` peu pr`s
e
a
e
a
e
linaire, mais quil reste des progr`s ` accomplir. De fait, en programmant un tri fusion destructif
e
e a
(voir [7, Chapitre 12] par ex.) qui nalloue aucune cellule, on obtient des temps bien meilleurs.
`
A vrai dire, les temps sont de lordre du double de ceux du tri de la bibliot`que.
e

Programmation objet

Dans tout ce chapitre nous avons expos la structure des listes. Cest-`-dire que les proe
a
grammes qui se servent de la classe des listes le font directement, en pleine conscience quils
manipulent des structures de listes. Cette technique est parfaitement correcte et fconde, mais
e
elle ne correspond pas ` lesprit dun langage objet. Supposons par exemple que nous voulions
a
implmenter les ensembles dentiers. Il est alors plus conforme ` lesprit de la programmation
e
a
objet de dnir un objet (( ensemble dentiers )) Set. Cette mise en forme (( objet )) nabolit pas
e
la distinction fondamentale des deux sortes de structures de donnes, mutable et non mutable,
e
bien au contraire.

4.1

Ensembles non mutables

Spcions donc une classe Set des ensembles.


e
class Set {
Set() { . . . }

4. PROGRAMMATION OBJET

33

Set add(int x) { . . . }
Set remove(int x) { . . . }
}
Comparons la signature de mthode add des Set ` celle de la mthode add de la classe List .
e
a
e
static List add( List xs, int x)
La nouvelle mthode add nest plus statique, pour produire un ensemble augment, on utilise
e
e
maintenant la notation objet : par exemple s.add(1) renvoie le nouvel ensemble s { 1} alors
que prcdemment on crivait List .add(s, 1) (voir section 2.2). On note que pour pouvoir
e e
e
appeler la mthode add sur tous les objets Set, il nest plus possible dencoder lensemble vide
e
par null . Lensemble vide est maintenant un objet construit par new Set ().
Bien entendu, il faut quune structure concr`te regroupe les lments des ensembles. Le plus
e
ee
simple est dencapsuler une liste dans chaque objet Set.
class Set {
// Partie prive
e
private List xs ;
private Set ( List xs) { this.xs = xs ; }
// Partie visible
Set () { xs = null ; }
Set add(int x) { return new Set ( List .add(xs, x)) ; }
Set remove(int x) { return new Set ( List .remove(xs, x)) ; }
}
La variable dinstance xs et le constructeur qui prend une liste en argument sont privs, de sorte
e
que les clients de Set ne peuvent pas manipuler la liste cache. Les (( clients )) sont les parties
e
du programme qui appellent les mthodes de la classe Set.
e
Comme le montre lexemple suivant, il importe que les listes soient traites comme des listes
e
non-mutables. En eet, soit le bout de programme
Set
Set
Set
Set

s0
s1
s2
s3

=
=
=
=

new Set () ;
s0.add(1) ;
s1.add(2) ;
s2.add(3) ;

Nous obtenons ltat mmoire simpli


e
e
e
s3

s2

s1

s0

xs

xs

xs

xs

La mthode remove (page 12) ne modie pas les champs des cellules de listes, de sorte que que
e
le programme
Set s4 = s3.remove(2) ;
nentra aucune modication des ensembles s2, s1 et s0.
ne

34

CHAPITRE I. LISTES

s3

s2

s1

s0

xs

xs

xs

xs

3
s4

xs

Il semble bien que ce style dencapsulage dune structure non-mutable est peu utilis en
e
pratique. En tout cas, la biblioth`que de Java ne propose pas de tels ensembles (( fonctionnels )).
e
Les deux avantages de la technique, ` savoir une meilleure structuration, et une reprsentation
a
e
uniforme des ensembles, y compris lensemble vide, comme des objets, ne justient sans doute
pas la cellule de mmoire supplmentaire alloue systmatiquement pour chaque ensemble. Lene
e
e
e
capsulage se rencontre plus frquemment quand il sagit de protger une structure mutable.
e
e

4.2

Ensembles mutables

On peut aussi vouloir modier un ensemble et non pas en construire un nouveau. Il sagit
dun point de vue impratif, tant donn un ensemble s, appliquer lopration add( int x) ` s,
e
e
e
e
a
ajoute llment x ` s. Cette ide sexprime simplement en Java, en faisant de s un objet (dune
ee
a
e
nouvelle classe Set) et de add une mthode de cet objet. On ajoute alors un lment x ` s par
e
ee
a
un appel de mthode s.add(x), et la signature de add la plus naturelle est
e
void add(int x)
La nouvelle signature de add rv`le lintention imprative : s connu est modi et il ny a donc
e e
e
e
pas lieu de le renvoyer apr`s modication.
e
e
Pour programmer la nouvelle classe Set des ensembles impratifs, on a recours encore une
fois ` la technique dencapsulage. Les objets de la classe Set poss`dent une variable dinstance
a
e
qui est une List . Mais cette fois, les mthodes des objets Set modient la variable dinstance.
e
class Set {
// Partie prive
e
private List xs ;
// Partie accessible
Set () { this.xs = null ; }
void add(int x) { xs = List .add (x, xs) ; }
void remove(int x) { xs = List .nremove(x, xs) ; }
public String toString() { return List .toString(xs) ; }
}
a
Le champ xs est priv, an de garantir que la classe Set est la seule ` manipuler cette liste. Cette
e
prcaution autorise, mais nimpose en aucun cas, lemploi des listes mutables au prix dun risque
e
modr, comme dans la mthode remove qui appelle List .nremove. Le caract`re impratif des
ee
e
e
e
ensembles Set est clairement arm et on ne sera (peut-tre) pas surpris par lachage du
e
e
programme suivant :
Set s1 = new Set() ;
Set s2 = s1 ;
s1.add(1) ; s1.add(2) ;
System.out.println(s2) ;
Le programme ache {1, 2} car s1 et s2 sont deux noms du mme ensemble impratif.
e
e

4. PROGRAMMATION OBJET

35

s2
2
s1

xs

Plus gnralement, en utilisant une structure de donne imprative, on souhaite faire proter
e e
e
e
lensemble du programme de toutes les modications ralises sur cette structure. Cet eet ici
e e
recherch est facilement atteint en mutant le champ priv xs des objets Set, cest la raison
e
e
mme de lencapsulage.
e
Exercice 9 Enrichir les objets de la classe Set avec une mthode diff de signature :
e
e e
void diff(Set s) ; // enlever les lments de s
Aller dabord au plus simple, puis utiliser les structures de donnes mutables. Ne pas oublier de
e
considrer le cas s.diff(s)
e
Solution. On peut crire dabord
e
void diff(Set s) { this.xs = List .diff(this.xs, s.xs) ; }
O` List .diff est non destructive (voir exercice 3), et o` on a crit this .xs au lieu de xs pour
u
u
e
bien insister.
Utilsons maintenant les listes mutables. Compte tenu de la smantique, il semble lgitime de
e
e
modier les cellules accessibles ` partir de this .xs, mais pas celles accessibles ` partir de s.xs.
a
a
On tente donc :
void diff(Set s) {
for ( List ys = s.xs ; ys != null ; ys = ys.next)
this.xs = List .nremove(ys.val, this.xs) ;
}
Lemploi dune nouvelle variable ys simpose, car il ne faut certainement pas modier s.xs. Plus
prcisment on ne doit pas crire for ( ; s.xs != null ; s.xs = s.xs.next ). Notons au
e e
e
passage que la dclaration private List xs ninterdit pas la modication de s.xs : le code de
e
diff se trouve dans la classe Set et il a plein acc`s aux champs privs de tous les objets de la
e
e
classe Set.
On vrie que lauto-appel s.diff(s) est correct, mme si cest de justesse. Supposons par
e
e
exemple que xs pointe vers la liste (1, (2, (3, ))). Au dbut de la premi`re itration, on a
e
e
e
this .xs
1

ys
Dans ces conditions, enlever ys.val (1) de la liste this .xs revient simplement ` renvoyer
a
this .xs.next. Et ` la n de premi`re itration, on a
a
e
e
this .xs
1
ys

36

CHAPITRE I. LISTES

Les itrations suivantes sont similaires : on enl`ve toujours le premi`re lment de la liste pointe
e
e
e ee
e
par this .xs, de sorte quen dbut puis n de seconde itration, on a
e
e
this .xs

this .xs
2

ys

ys

Puis au dbut et ` la n de la derni`re itration, on a


e
a
e
e
this .xs

this .xs

ys

ys

Et donc, this .xs vaut bien null en sortie de mthode diff, ouf. Tout cela est quand mme
e
e
assez acrobatique.

Complment : listes boucles


e
e

Des lments peuvent tre organiss en squence, sans que ncessairement la squence soit
ee
e
e
e
e
e
nie. Si la squence est priodique, elle devient reprsentable par une liste qui au lieu de nir,
e
e
e
repointe sur une de ses propres cellules. Par exemple, on peut tre tent de reprsenter les
e
e
e
dcimales de 1/2 = 0.5, 1/11 = 0.090909 et 1/6 = 0.16666 par
e
5

Nous distinguons ici, dabord une liste nie, ensuite une liste circulaire le retour est sur la
premi`re cellule, et enn une liste boucle la plus gnrale.
e
e
e e

5.1

Reprsentation des polygones


e

Plus couramment, il est pratique de reprsenter un polygone ferm par la liste circulaire de
e
e
ses sommets. La squence des points est celle qui correspond ` lorganisation des sommets et
e
a
des artes.
e
A4

A3

B1

B0

B1

B0

B2

B3

B2

B3

A0
A2
A1

Les polygones tant ferms, les listes circulaires sont naturelles. Lordre des sommets dans la
e
e
liste est signicatif, ainsi les polygones (B0 B1 B2 B3 ) et (B0 B1 B3 B2 ) sont distincts.
Pour reprsenter les polygones en machine, nous dnissons une classe des cellules de liste
e
e
de points.

5. COMPLEMENT : LISTES BOUCLEES

37

import java.awt.* ;
class Poly {
private Point val ; private Poly next ;
private Poly (Point val, Poly next) { this.val = val ; this.next = next ; }
.
.
.
}
Tout est priv, compte tenu des manipulations dangereuses auxquelles nous allons nous livrer.
e
La classe Point des points est celle de la biblioth`que (package java.awt), elle est sans surprise :
e
p.x et p.y sont les coordonnes, p.distance(q) renvoie la distance entre les points p et q.
e
Nous allons concevoir et implmenter un algorithme de calcul de lenveloppe convexe dun
e
nuage de points, algorithme qui sappuie naturellement sur la reprsentation en machine des
e
polygones (convexes) par les listes circulaires. Mais commenons dabord par nous familiariser
c
tant avec la liste circulaire, quavec les quelques notions de gomtrie plane ncessaires. Pour
e e
e
nous simplier un peu la vie nous allons poser quelques hypoth`ses.
e
Les polygones ont toujours au moins un point. Par consquent, null nest pas un polygone.
e
Les points sont en position gnrale. Ce qui veut dire que les points sont deux ` deux
e e
a
distincts, et quil ny a jamais trois points alignes.
e
Le plan est celui dune fentre graphique Java, cest ` dire que lorigine des coordonnes
e
a
e
est en haut et ` gauche. Ce qui entra que le rep`re est en sens inverse du rep`re usuel
a
ne
e
e
du plan (laxe des y pointe vers le bas).

5.2

Oprations sur les polygones


e

Lintrt de la liste circulaire appara pour, par exemple, calculer le prim`tre dun polygone.
ee
t
e
e
Pour ce faire, il faut sommer les distances (A0 A1 ), (A1 A2 ), . . . (An1 A0 ). Si nous reprsentions
e
le polygone par la liste ordinaire de ses points, le cas de la derni`re arte (An1 A0 ) serait
e
e
particulier. Avec une liste circulaire, il nen est rien, puisque le champ next de la cellule du
point An1 pointe vers la cellule du point A0 . La sommation se fait donc simplement en un
parcours des sommets du polygone. An de ne pas sommer indniment, il convient darrter
e
e
le parcours juste apr`s cette (( derni`re )) arte, cest ` dire quand on retrouve la (( premi`re ))
e
e
e
a
e
cellule une seconde fois.
static double perimeter(Poly p) {
double r = 0.0 ;
Poly q = p ;
do {
r += q.val.distance(q.next.val) ;
q = q.next ;
e
e e
} while (q != p) ; // Attention : diffrence des rfrences (voir B.3.1.1)
return r ;
}
Lusage dune boucle do {. . .} while (. . .) (section B.3.4) est naturel. En eet, on veut nir
ditrer quand on voit la premi`re cellule une seconde fois, et non pas une premi`re fois. On
e
e
e
observe que le prim`tre calcul est 0.0 pour un polygone ` un sommet et deux fois la distance
e
e
e
a
(A0 A1 ) pour deux sommets.
Nous construisons un polygone ` partir de la squence de ses sommets, extraite dun chier
a
e
ou bien donne ` la souris. La lecture des sommets se fait selon le mod`le du Reader (voir
e a
e
section B.6.2.1). Soit donc une classe PointReader des lecteurs de points pour lire les sources
de points (chier ou interface graphique). Ses objets poss`dent une mthode read qui consomme
e
e

38

CHAPITRE I. LISTES

et renvoie le point suivant de la source, ou null quand la source est tarie (n de chier, double
clic). Nous crivons une mthode de construction du polygone form par une source de points.
e
e
e
static Poly readPoly(PointReader in) {
Point pt = in.read() ;
i f (pt == null) throw new Error ("Polygone vide interdit") ;
Poly r = new Poly (pt, readPoints(in)) ;
nappend(r,r) ; // Pour refermer la liste, voir exercice 4
return r ;
}
private static Poly readPoints(PointReader in) {
Point pt = in.read() ;
i f (pt == null) {
return null ;
} else {
return new Poly(pt, readPoints(in)) ;
}
}
La programmation est rcursive pour facilement prserver lordre des points de la source. La
e
e
liste est dabord lue puis referme. Cest de loin la technique la plus simple. Conformment
e
e
` la place, il lance
a
` nos conventions, le programme refuse de construire un polygone vide. A
lexception Error avec pour argument un message explicatif. Leet du lancer dexception peut
se rsumer comme un chec du programme, avec achage du message (voir B.4.2 pour les
e
e
dtails).
e
Nous nous posons maintenant la question didentier les polygones convexes. Soit maintenant
P un polygone. La paire des points p.val et p.next.val est une arte (A0 A1 ), cest-`-dire un
e
a
segment de droite orient. Un point quelconque Q, peut se trouver ` droite ou ` gauche du
e
a
a
segment (A0 A1 ), selon le signe du dterminant det(A0 A1 , A0 P ). La mthode suivante permet
e
e
donc didentier la position dun point relativement ` la premi`re arte du polygone P .
a
e
e
private static int getSide(Point a0, Point a1, Point q) {
return
(q.y - a0.y) * (a1.x - a0.x) - (q.x - a0.x) * (a1.y - a0.y) ;
}
static int getSide(Point q, Poly p) { return getSide (p.val, p.next.val, q) ; }
Par lhypoth`se de position gnrale, getSide ne renvoie jamais zro. Une vrication rapide
e
e e
e
e
nous dit que, dans le syst`me de coordonnes des fentres graphiques, si getSide(q, p) est
e
e
e
ngatif, alors le point q est a gauche de la premi`re arte du polygone p.
e
`
e
e
Un polygone est convexe (et bien orient) quand tous ses sommets sont ` gauche de toutes
e
a
ses artes. La vrication de convexit revient donc ` parcourir les artes du polygone et, pour
e
e
e
a
e
chaque arte ` vrier que tous les sommets autres que ceux de larte sont bien ` gauche.
e a e
e
a
static boolean isConvex(Poly p) {
Poly a = p ; // a est lar^te courante
e
do {
Poly s = a.next.next ; // s est le sommet courant
while (s != a) {
i f (getSide(s.val, a) > 0) return false ;
s = s.next ; // sommet suivant
}
e
a = a.next ; // ar^te suivante

5. COMPLEMENT : LISTES BOUCLEES

39

} while (a != p) ;
return true ;
}
On note les deux boucles imbriques, une boucle sur les sommets dans une boucle sur les artes.
e
e
La boucle sur les sommets est une boucle while par souci esthtique de ne pas appeler getSide
e
avec pour arguments des points qui ne seraient pas en position gnrale.
e e
Exercice 10 Que se passe-t-il si p est un polygone ` un, ` deux sommets ?
a
a
Solution. Dans les deux cas, aucune itration de la boucle intrieure sur les sommets nest
e
e
excute. Par consquent les polygones a un et deux sommets sont jugs convexes et bien orients.
e e
e
`
e
e
Pour un polygone ` n sommets, la boucle sur les artes est excute n fois, et ` chaque fois, la
a
e
e e
a
boucle sur les sommets est excute n 2 fois. La vrication de convexit cote donc de lordre
e e
e
e u
de n2 oprations lmentaires.
e
ee
Notons que la mthode getSide nous permet aussi de dterminer si un point est ` lintrieur
e
e
a
e
dun polygone convexe.
static boolean isInside(Point q, Poly p) {
Poly a = p ;
do {
i f (getSide(q, a) > 0) return false
a = a.next ;
} while (a != p) ;
return true ;
}
Notons que la convexit permet une dnition et une dtermination simplie de lintrieur.
e
e
e
e
e
La mthode isInside a un cot de lordre de n opration lmentaires, o` n est la taille du
e
u
e
ee
u
polygone.
Exercice 11 Que se passe-t-il si p est un polygone ` un, ` deux sommets ?
a
a
Solution. Le cas limite du polygone ` deux sommets est bien trait : pour trois points en
a
e
position gnrale, lun des deux appels ` getSide renvoie ncessairement un entier positif.
e e
a
e
Par consquent isInside renvoie ncessairement false, ce qui est conforme ` lintuition que
e
e
a
lintrieur dun polygone ` deux sommets est vide.
e
a
Il nen va pas de mme du polygone ` un seul sommet, dont lintrieur est a priori vide.
e
a
e
Or, dans ce cas, getSide est appele une seule fois, les deux sommets de larte tant le mme
e
e e
e
point. La mthode getSide renvoie donc zro, et isInside renvoie toujours true.
e
e

5.3

Calcul incrmental de lenveloppe convexe


e

Lenveloppe convexe dun nuage de points est le plus petit polygone convexe dont lintrieur
e
contient tous les points du nuage. Pour calculer lenveloppe convexe, nous allons procder
e
incrmentalement, cest-a-dire, en supposant que nous possdons dj` lenveloppe convexe des
e
e
ea
points dj` vus, construire lenveloppe convexe des points dj` vus plus un nouveau point.
ea
ea
Soit donc la squence de points P0 , P1 , . . . Pk , dont nous connaissons lenveloppe convexe E =
e
(E0 E1 . . . Ec ), Et soit Q = Pk+1 le nouveau point.
Si Q est ` lintrieur de E, lenveloppe convexe ne change pas.
a
e

40

CHAPITRE I. LISTES
Sinon, il faut identier deux sommets Ei et Ej , tels que tous les points de E sont ` gauche
a
de (Ei Q) et (QEj ). La nouvelle enveloppe convexe est alors obtenue en remplaant la
c
squence (Ei . . . Ej ) par (Ei QEj ).
e
E1
E0 = Ej

E2
E4

E3 = Ei
Sans raliser une dmonstration compl`te, il semble clair que Q se trouve ` droite des
e
e
e
a
artes supprimes et ` gauche des des artes conserves. Mieux, lorsque lon parcourt les
e
e
a
e
e
sommets du polygone, le sommet Ei marque une premi`re transition : Q est ` gauche de
e
a
(Ei1 Ei ) et ` droite de (Ei Ei+1 ), tandis que le sommet Ej marque la transition inverse.
a
On peut aussi imaginer que Q est une lampe, il faut alors supprimer les artes claires
e
e
e
de lenveloppe.
Il faut donc identier les sommets de E o` seectuent les transitions (( Q ` gauche puis ` droite ))
u
a
a
et inversement. Le plus simple para de se donner dabord une mthode getRight qui renvoie
t
e
la (( premi`re )) arte qui laisse Q ` droite, ` partir dun sommet initial suppos quelconque.
e
e
a
a
e
static Poly getRight(Poly e, Point q) {
Poly p = e ;
do {
i f (getSide(q, p) > 0) return p ;
p = p.next ;
} while (p != e) ;
return null ;
}
Notons que dans le cas o` le point Q est ` lintrieur de E, cest-`-dire ` gauche de toutes ses
u
a
e
a
a
artes, getRight renvoie null . On crit aussi une mthode getLeft pour dtecter la premi`re
e
e
e
e
e
arte de E qui laisse Q ` gauche. Le code est identique, au test getSide(q, p) > 0, ` changer
e
a
a
en getSide(q, p) < 0, pr`s.
e
La mthode extend dextension de lenveloppe convexe, trouve dabord une arte qui laisse
e
e
Q ` droite, si une telle arte existe. Puis, le cas chant, elle continue le parcours, en encha
a
e
e e
nant
un getLeft puis un getRight an didentier les points Ej puis Ei qui dlimitent la partie
e
(( claire )) de lenveloppe.
e
e
private static Poly extend(Poly e,
Poly oneRight = getRight(e, q) ;
i f (oneRight == null) return e ;
// Lar^te issue de oneRight est
e
Poly ej = getLeft(oneRight.next,
Poly ei = getRight(ej.next, q) ;
Poly r = new Poly(q, ej) ;
ei.next = r ;
return r ;
}

Point q) {
// Le point q est intrieur
e
e
claire
e
q) ; // lumi`re -> ombre
e
// ombre -> lumi`re
e

La structure de liste circulaire facilite lcriture des mthodes getRight et getLeft. Elle pere
e
met aussi, une fois trouvs Ei et Ej , une modication facile et en temps constant de lenveloppe
e
convexe. Les deux lignes qui eectuent cette modication sont directement inspires du dese
sin dextension de polygone convexe ci-dessus. Il est clair que chacun des appels ` getRight
a
et getLeft eectue au plus un tour du polygone E. La mthode extend eectue donc au pire
e

5. COMPLEMENT : LISTES BOUCLEES

41

trois tours du polygone E (en pratique au plus deux tours, en raison de lalternance des appels
a
` getRight et getLeft). Elle est donc en O(c), o` c est le nombre de sommets de lenveloppe
u
convexe.
Il nous reste pour calculer lenveloppe convexe dun nuage de points lus dans un PointReader
a
` initialiser le processus. On peut se convaincre assez facilement que extend fonctionne correctement d`s que E poss`de deux points, car dans ce cas il existe une arte claire et une arte
e
e
e e
e
e
sombre. On crit donc.
e
static Poly convexHull(PointReader in) {
Point q1 = in.read() ;
Point q2 = in.read() ;
i f (q1 == null || q2 == null) throw new (Error "Pas assez de points") ;
Poly e = new Poly (q1, new Poly (q2, null)) ;
nappend(e,e) ; // Refermer la liste
for ( ; ; ) { // Idiome : boucle infinie
Point q = in.read()
i f (q == null) return e ;
e = extend(e, q) ;
}
}
Pour un nuage de n points, on excute de lordre de n fois extend, dont le cot est en O(c),
e
u
sur des enveloppes qui poss`dent c n sommets. Le programme est donc en O(n2 ). Le temps
e
dexcution dpend parfois crucialement de lordre de prsentation des points. Dans le cas o` par
e
e
e
u
exemple lenveloppe convexe est forme par les trois premiers points lus, tous les points suivants
e
tant intrieurs ` ce triangle, alors on a un cot linaire. Malheureusement, si tous les points de
e
e
a
u
e
lentre se retrouvent au nal dans lenveloppe convexe, le cot est bien quadratique. La version
e
u
web du cours pointe vers un programme de dmonstration de cet algorithme.
e

5.4

Calcul ecace de lenveloppe convexe

Lenveloppe convexe dun nuage de n points du plan peut se calculer en O(n log n) oprations,
e
a
` laide de lalgorithme classique de la marche de Graham (Graham scan) Nous nexpliquerons
pas la marche de Graham, voir par exemple [2, Chapitre 35] ou [7, Chapitre 25]. En eet,
un ranement de la technique incrmentale inspire un algorithme tout aussi ecace, dumoins
e
asymptotiquement. De plus ce nouvel algorithme va nous permettre daborder une nouvelle sorte
de liste.
Rappelons lide de lalgorithme incrmental, sous une forme un peu image. Lenveloppe
e
e
e
convexe E est soumise ` la lumi`re mise par le nouveau point Q. En raison de sa convexit mme,
a
e e
e e
E poss`de une face claire et une face sombre. La face claire va du sommet Ei au sommet Ej ,
e
e
e
e
e
et lextension de lenveloppe revient ` remplacer la face claire par Ei QEj . Supposons conna
a
e
e
tre
un sommet P de lenveloppe convexe qui est (( clair )) par le nouveau point Q (cest-a-dire que
e
e
larte issue de P laisse Q ` droite). Le sens de rotation selon les next nous permet de trouver
e
a
la n de la face claire directement, cest-`-dire sans passer par la face sombre. Pour pouvoir
e
e
a
trouver le dbut de la face claire tout aussi directement, il faut pouvoir tourner dans lautre
e
e
e
sens ! Nous allons donc ajouter un nouveau champ prev ` nos cellules de polygones, pour les
a
cha
ner selon lautre sens de rotation.
class Poly {
.
.
.
private Poly prev ;

42

CHAPITRE I. LISTES
private Poly (Point val, Poly next, Poly prev) {
this(val, next) ; // Appeler lautre constructeur
this.prev = prev ;
}
.
.
.

}
Nos listes de sommets deviennent des listes doublement cha ees (et circulaires qui plus est), une
n
structure de donnes assez sophistique, que nous pouvons illustrer simplement par le schma
e
e
e
suivant :
Ej

P
Q
Ei
En suivant les champs prev (nots par des `ches courbes), nous trouvons facilement la premi`re
e
e
e
arte inverse sombre ` partir du point P , cest-`-dire le premier sommet Ei dont est issue une
e
a
a
arte inverse qui laisse le point Q ` droite.
e
a
static int getSidePrev(Poly a, Point q) {
return getSide(a.val, a.prev.val, q) ;
}
static Poly getRightPrev(Poly e, Point q) {
Poly p = e ;
for ( ; ; ) {
i f (getSidePrev(q, p) > 0) return p ;
p = p.prev ;
}
}
Et nous pouvons crire la nouvelle mthode dextension de lenveloppe convexe.
e
e
// Attention e.val doit ^tre (( eclair )) par q (ie getSide(e,q) > 0)
e
e
private static Poly extend2(Poly e, Point q) {
Poly ej = getLeft(e.next, q) ;
Poly ei = getRightPrev(e, q) ;
Poly r = new Poly(q, ej, ei) ;
ei.next = r ; ej.prev = r ;
return r ;
}
La mthode extend2 identie la face claire pour un cot proportionnel ` la taille de cette
e
e
e
u
a
face. Puis, elle construit la nouvelle enveloppe en remplaant la face claire par (Ei QEj ). Les
c
e
e
manipulations des champs next et prev sont directement inspires par le schma ci-dessus.
e
e
La mthode nest correcte que si Q claire larte issue de P . Ce sera le cas, quand
e
e
e
(1) P est le point dabscisse maximale parmi les points dj` vus.
ea
(2) Q est dabscisse strictement suprieure ` celle de de P .
e
a

5. COMPLEMENT : LISTES BOUCLEES

43

En fait lhypoth`se de position gnrale ninterdit pas que deux points poss`dent la mme abscisse
e
e e
e
e
(mais cest interdit pour trois points). En pareil cas, pour que Q claire larte issue de P , il
e
e
sut que lordonne de Q soit infrieure ` celle de P , en raison de la convention que laxe des
e
e
a
y pointe vers le bas. Autrement dit, P est strictement infrieur ` Q selon lordre suivant.
e
a
// p1 < p2 <=> compare(p1,p2) < 0
static int compare(Point p1, Point p2) {
i f (p1.x < p2.x) return -1 ;
else i f (p1.x > p2.x) return 1 ;
else i f (p1.y < p2.y) return 1 ;
else i f (p1.y > p2.y) return -1 ;
else return 0 ;
}
On note que le point ajout Q fait toujours partie de la nouvelle enveloppe convexe. En fait, il
e
sera le nouveau point distingu lors de la prochaine extension, comme exprim par la mthode
e
e
e
compl`te de calcul de lenveloppe convexe.
e
static Poly convexHullSorted(PointReader in) {
Point pt1 = in.read() ;
Point pt2 = in.read() ;
i f (pt1 == null || pt2 == null || compare(pt1,pt2) >= 0)
throw new Error ("Deux premiers point incorrects") ;
Poly p1 = new Poly(pt1, null, null) ;
Poly p2 = new Poly(pt2, p1, null) ;
p1.next = p2 ;
p1.prev = p2 ;
p2.prev = p1 ;
Poly e = p2 ;
for ( ; ; ) {
Point q = in.read() ;
i f (q == null) return e ;
i f (compare(e.val, q) >= 0) throw new Error ("Points non tris") ;
e
e = extend2(e, q) ;
}
}
On peut donc assurer la correction de lalgorithme complet en triant les points de lentre
e
selon les abscisses croissantes, puis les ordonnes dcroissantes en cas dgalit. Evidemment
e
e
e
e
pour pouvoir trier les points de lentre il faut la lire en entier, lalgorithme nest donc plus
e
incrmental.
e
Estimons maintenant le cot du nouvel algorithme de calcul de lenveloppe convexe de
u
n points. On peut trier les n points en O(n log n). Nous prtendons ensuite que le calcul propree
ment dit de lenveloppe est en O(n). En eet, le cot dun appel ` extend2 est en O(j i) o`
u
a
u
j i 1 est le nombre de sommets supprims de lenveloppe ` cette tape. Or un sommet est
e
a
e
supprim au plus une fois, ce qui conduit ` une somme des cots des appels ` extend2 en O(n).
e
a
u
a
Le tri domine et lalgorithme complet est donc en O(n log n). Cet exemple illustre que le choix
dune structure de donnes approprie (ici la liste doublement cha ee) est parfois crucial pour
e
e
n
atteindre la complexit thorique dun algorithme. Ici, avec une liste cha ee seulement selon
e e
n
les champs next, il nest pas possible de trouver le sommet Ei ` partir dun sommet de la face
a
claire sans faire le tour par la face sombre.
e
e

44

CHAPITRE I. LISTES

Chapitre II

Piles et les
Les piles et les les sont des exemples de structures de donnes que faute de mieux, nous
e
appellerons des sacs. Un sac ore trois oprations lmentaires :
e
ee
(1) tester si le sac est vide,
(2) ajouter un lment dans le sac,
ee
(3) retirer un lment du sac (tenter de retirer un lment dun sac vide dclenche une erreur).
ee
ee
e
Le sac est une structure imprative : un sac se modie au cours du temps. En Java, il est
e
logique de reprsenter un sac (dlments E) par un objet (dune classe Bag E ) qui poss`de
e
ee
e
trois mthodes.
e
class Bag E {
Bag E { . . . } // Construire un sac vide
boolean isEmpty() { . . . }
void add(E e) { . . . }
E remove() { . . . }
}
Ainsi on ajoute par exemple un lment e dans le sac b par b.add(e), ce qui modie ltat
ee
e
interne du sac.
Piles et les se distinguent par la relation entre lments ajouts et lments retirs. Dans le
ee
e
ee
e
cas des piles, cest le dernier lment ajout qui est retir. Dans le cas dune le cest le premier
ee
e
e
lment ajout qui est retir. On dit que la pile est une structure LIFO (last-in rst-out), et que
ee
e
e
la le est une structure FIFO (rst-in rst-out).
Si on reprsente pile et le par des tas de cases, on voit que la pile poss`de un sommet, o`
e
e
u
sont ajouts et do` sont retirs les lments, tandis que la le poss`de une entre et une sortie.
e
u
e
ee
e
e
Tout ceci est parfaitement conforme ` lintuition dune pile dassiettes, ou de la queue devant
a
Fig. 1 Une pile (ajouter et retirer du mme ct), une le (ajouter et retirer de cts opposs).
e
oe
oe
e
5
4
3
2
1
0

Sommet de pile

5
4
3
2
1
0

Sortie de le

Entre de le
e
0

un guichet, et sugg`re fortement des techniques dimplmentations pour les piles et les les.
e
e
45

46

CHAPITRE II. PILES ET FILES

Exercice 1 Soit la mthode build suivante qui ajoute tous les lments dune liste dans un sac
e
ee
de int puis construit une liste en vidant le sac.
static List build( List p) {
Bag bag = new Bag () ;
for ( ; p != null ; p = p.next)
bag.add(p.val) ;
List r = null ;
while (!bag.isEmpty())
r = new List (bag.remove(), r) ;
return r ;
}
Quelle est la liste renvoye dans le cas o` le sac est une pile, puis une le ?
e
u
Solution. Si le sac est une pile, alors build renvoie une copie de la liste p. Si le sac est une le,
alors build renvoie une copie en ordre inverse de la liste p.
Il y a un peu de vocabulaire spcique aux piles et aux les. Traditionnellement, ajouter
e
un lment dans une pile se dit empiler (push), et lenlever dpiler (pop), tandis quajouter un
ee
e
lment dans une le se dit enler, et lenlever dler.
ee
e

`
A quoi a sert ?
c

La le est peut-tre la structure la plus immdiate, elle modlise directement les les dattente
e
e
e
gres selon la politique premier-arriv, premier-servi. La pile est plus informatique par nature.
ee
e
Dans la vie, la politique dernier-arriv, premier-servi nest pas tr`s populaire. Elle correspond
e
e
pourtant ` une situation courante, la survenue de tches de plus en plus urgentes. Les piles
a
a
modlisent aussi tout syst`me o` lentre et la sortie se font par le mme passage oblig :
e
e
u
e
e
e
wagons sur une voie de garage, cabine de tlphrique, etc.
ee e

1.1

Premier arriv, premier servi


e

Utiliser une le informatique est donc naturel lorsque lon modlise une le dattente au sens
e
courant du terme. Considrons un exemple simple :
e
Un unique guichet ouvert 8h00 conscutives (soit 8 60 60 secondes).
e
Les clients arrivent et font la queue. La probabilit darrive dun nouveau client dans
e
e
lintervalle [t . . . t + 1[ est p.
Le temps que prend le service dun client suit une loi uniforme sur [30 . . . 300[.
Les clients, sil attendent trop longtemps, partent sans tre servis. Leur patience est une
e
loi uniforme sur [120 . . . 1800[
Notre objectif est dcrire une simulation informatique an destimer le rapport clients repartis
e
sans tre servis sur nombre total de clients, en fonction de la probabilit p.
e
e
Nous implmentons la simulation par une classe Simul. Pour raliser les tirages alatoires
e
e
e
ncessaires, nous employons une source pseudo-alatoire, les objets de la classe Random du
e
e
package java.util (voir B.1.4 pour la notion de package et B.6.3.1 pour une description
de Random).
// Source pseudo-alatoire
e
static private Random rand = new Random () ;
// Loi uniforme sur [min. . .max[
static private int uniform(int min, int max) {
return min + rand.nextInt(max-min) ;

`
1. A QUOI CA SERT ?

47

}
// Survenue dun vnement de probabilit prob [0. . .1]
e e
e
static private boolean occurs(double prob) {
return rand.nextDouble() < prob ;
}
Un client en attente est modlis par un objet de la classe Client . Un objet Client est cr
e e
ee
lors de larrive du client au temps t, ce qui donne loccasion de calculer lheure ` laquelle le
e
a
client exaspr repartira sil il na pas t servi.
ee
ee
class Client {
// Temps dattente
final static int waitMin = 120,

waitMax = 1800 ;

int arrival, departure ;


Client (int t) {
arrival = t ; departure = t+Simul.uniform(waitMin, waitMax) ;
}
}
Noter que les constantes qui gouvernent le temps dattente sont des variables nal de la
classe Client .
Limplmentation dune le sera dcrite plus tard. Pour linstant une le (de clients) est un
e
e
e
e
e
objet de la classe Fifo qui poss`de les mthodes isEmpty, add et remove dcrites pour les Bag
de lintroduction.
La simulation est discr`te. Durant la simulation, une variable tFree indique la date o` le
e
u
`
guichet sera libre. A chaque seconde t, il faut :
Faire un tirage alatoire pour savoir si un nouveau client arrive.
e
Si oui, ajouter le nouveau client (avec sa limite de temps) dans la le dattente.
Quand un client est disponible et que le guichet est libre (si t est suprieur ` tFree).
e
a
Extraire le client de la le.
Vrier quil est toujours l` (sinon, passer au (( client )) suivant).
e
a
Tirer alatoirement un temps de traitement.
e
Et ajuster tFree en consquence.
e
Le code qui ralise cette simulation na en donn par la gure 2. La mthode simul prend
e
ve
e
e
la probabilit darrive dun client en argument et renvoie le rapport clients non-servis sur
e
e
nombre total de clients. Le code suit les grandes lignes de la description prcdente. Il faut
e e
surtout noter comment on rsout le probl`me dune boucle while qui doit dune part sefe
e
fectuer tant que la le nest pas vide et dautre part cesser d`s quun client eectivement
e
prsent est extrait de la le (sortie par break). On note aussi la conversion de type explie
cite ((double)nbFedUp)/nbClients, ncessaire pour forcer la division ottante. Sans cette
e
conversion on aurait la division euclidienne. La gure 3 donne les rsultats de la simulation pour
e
diverses valeurs de la probabilit prob. Il sagit de moyennes sur dix essais.
e

1.2

Utilisation dune pile

Nous allons prendre en compte la nature tr`s informatique de la pile et proposer un exemple
e
plutt informatique. Les calculatrices dune cl`bre marque tats-unienne ont popularis une
o
ee
e
e
notation des calculs dite parfois (( polonaise inverse ))1 ou plus gnralement postxe. Les calculs
e e
1

En hommage au philosophe et mathmaticien Jan Lukasiewicz qui a introduit cette notation dans les
e
annes 20.
e

48

CHAPITRE II. PILES ET FILES

Fig. 2 Code de la simulation.


static double simul(double probNewClient) {
Fifo f = new Fifo () ;
int nbClients = 0, nbFedUp = 0 ;
int tFree = 0 ; // Le guichet est libre au temps tFree
for (int t = 0 ; t < tMax ; t++) {
// Tirer larrive dun client dans [t..t+1[
e
i f (occurs(probNewClient)) {
nbClients++ ;
f.add(new Client(t)) ;
}
i f (tFree <= t) {
// Le guichet est libre, servir un client (si il y en a encore un)
while (!f.isEmpty()) {
Client c = f.remove() ;
a
i f (c.departure >= t) { // Le client est encore l`
tFree = t + uniform(serviceMin, serviceMax) ;
break ; // Sortir de la boucle while
} else { // Le client tait parti
e
nbFedUp++ ;
}
}
}
}
return ((double)nbFedUp)/nbClients ;
}

Non-servis/venus

Fig. 3 Rsultat de la simulation de la le dattente


e
0.8
++
+
0.7
++
++ + +
++ + + +
+ + ++ +
++ +
+ ++
+++
++
0.6
++ +
+++
+ ++
+++
+ ++
0.5
++ +
+
++
++
+
++
+
0.4
+
+
+
+
+
++
+
0.3
++
+
+
+
++
0.2
+
++
+
+
++
0.1
+
+
+
+
++
0 ++ + + ++
+ + ++ +
++ + +
0
0.005
0.01
0.015
0.02
0.025
Clients/sec

`
1. A QUOI CA SERT ?

49

se font ` laide dune pile, les nombres sont simplement empils, et lexcution dune opration
a
e
e
e
op revient ` dabord dpiler un premier oprateur e1 , puis un second e2 , et enn ` empiler le
a
e
e
a
rsultat e2 op e1 . Par exemple, la gure 4 donne les tapes successives du calcul 6 3 2 - 1 + /
e
e
9 6 - * exprim en notation postxe. Le fabricant tats-unien de calculatrices arme quavec
e
e
Fig. 4 Calcul de lexpression postxe 6 3 2 - 1 + / 9 6 - *
3

3
6

2
3
6

1
6

1
1
6

2
6

9
3

6
6
9
3

3
3

un peu dhabitude la notation postxe est plus commode que la notation usuelle (dite inxe).
Cest peut-tre vrai, mais on peut en tout cas tre sr que linterprtation de la notation postxe
e
e
u
e
par une machine est bien plus facile ` raliser que celle de la notation inxe. En voici pour preuve
a e
le programme Calc qui ralise le calcul postxe donn sur la ligne de commande. Dans ce code
e
e
les objets de la classe Stack sont des piles dentiers.
class Calc {
public static void main (String [] arg) {
Stack stack = new Stack () ;
for (int k = 0 ; k < arg.length ; k++) {
String x = arg[k] ;
i f (x.equals("+")) {
int i1 = stack.pop(), i2 = stack.pop() ;
stack.push(i2+i1) ;
} else i f (x.equals("-")) {
int i1 = stack.pop(), i2 = stack.pop() ;
stack.push(i2-i1) ;
...
/* Idem pour "*" et "/" */
...
} else {
stack.push(Integer.parseInt(x)) ;
}
System.err.println(x + " -> " + stack) ;
}
System.out.println(stack.pop()) ;
}
}
Le source contient des messages de diagnostic (sur System.err on ninsistera jamais assez), un
essai2 nous donne (le sommet de pile est ` droite) :
a
% java Calc 6 3 2 - 1 + / 9 6 - *
6 -> [6]
3 -> [6, 3]
.
.
.
2

Il faut citer le symbole * par exemple par *, an dviter son interprtation par le shell, voir VI.3.1.
e
e

50

CHAPITRE II. PILES ET FILES

6 -> [3, 9, 6]
- -> [3, 3]
* -> [9]
9
La facilit dinterprtation de lexpression postxe provient de ce que sa dnition est tr`s
e
e
e
e
oprationnelle, elle dit exactement quoi faire. Dans le cas de la notation inxe, il faut rgler le
e
e
probl`me des priorits des oprateurs. Par exemple si on veut eectuer le calcul inxe 1 + 2 *
e
e
e
3 il faut procder ` la multiplication dabord (seconde opration), puis ` laddition (premi`re
e
a
e
a
e
opration) ; tandis que pour calculer 1 * 2 + 3, on eectue dabord la premi`re opration (mule
e
e
tiplication) puis la seconde (addition). Par contraste, la notation postxe oblige lutilisateur de
la calculatrice ` donner lordre dsir des oprations (comme par exemple 1 2 3 * + et 1 2 * 3
a
e e
e
+) et donc simplie dautant le travail de la calculatrice. Dans le mme ordre dide, la notation
e
e
postxe rend les parenth`ses inutiles (ou empche de les utiliser selon le point de vue).
e
e

Exercice 2 Ecrire un programme Inx qui lit une expression sous forme postxe et ache
lexpression inxe (compl`tement parenthse) qui correspond au calcul eectu par le proe
ee
e
e
nes.
gramme Calc. On supposera donne une classe Stack des piles de cha
Solution. Il sut tout simplement de construire la reprsentation inxe au lieu de calculer.
e
class I n f i x {
public static void main (String [] arg) {
Stack stack = new Stack () ;
for (int k = 0 ; k < arg.length ; k++) {
String x = arg[k] ;
i f (x.equals("+") || x.equals("-") || x.equals("*") || x.equals("/")) {
String i1 = stack.pop(), i2 = stack.pop() ;
stack.push("(" + i2 + x + i1 + ")") ;
} else {
stack.push(x) ;
}
System.err.println(x + " -> " + stack) ;
}
System.out.println(stack.pop()) ;
}
}
Lemploi systmatiques des parenth`ses permet de produire des expressions inxes justes. Sans
e
e
les parenth`ses 1 2 3 - - et 1 2 - 3 - conduiraient au mme rsultat 1 - 2 - 3 qui serait
e
e
e
incorrect pour 1 2 3 - -. Sur lexpression dj` donne, on obtient :
ea
e
% java Infix 6 3 2 - 1 + / 9 6 - *
6 -> [6]
3 -> [6, 3]
.
.
.
6 -> [(6/((3-2)+1)), 9, 6]
- -> [(6/((3-2)+1)), (9-6)]
* -> [((6/((3-2)+1))*(9-6))]
((6/((3-2)+1))*(9-6))
Nous verrons plus tard que se passer des parenth`ses inutiles ne se fait simplement qu` laide
e
a
dune nouvelle notion.


2. IMPLEMENTATION DES PILES

51

Implmentation des piles


e

Nous donnons dabord deux techniques dimplmentation possibles, avec un tableau et avec
e
une liste simplement cha ee. Ensuite, nous montrons comment utiliser la structure de pile toute
n
faite de la biblioth`que.
e

2.1

Une pile dans un tableau

Cest la technique conceptuellement la plus simple, directement inspire par le dessin de


e
gauche de la gure 1.
Plus prcisment, une pile (dentiers) contient un tableau dentiers, dont nous xons la taille
e e
arbitrairement ` une valeur constante. Seul le dbut du tableau (un segment initial) est utilis,
a
e
e
et un indice variable sp indique le pointeur de la pile. Le pointeur de pile indique la prochaine
position libre dans la pile, mais aussi le nombre dlments prsents dans la pile.
ee
e
0

sp

Dans ce schma, seules les 5 premi`res valeurs du tableau sont signicatives.


e
e
class Stack {
final static int SIZE = 10; // assez grand ?
private int sp ;
private int [] t ;
Stack () { // Construire une pile vide
t = new int[SIZE] ; sp = 0;
}
.
.
.
}
e
Notons que tous les champs des objets Stack sont privs (private, voir B.1.4). La taille des
0 1 2 3 4 5 6 7 8 9 10
piles est donne par une constante (nal), statique puisque quil ny a aucune raison de crer
e
e
de nombreux exemplaires de cette constante.
`
A la cration, une pile contient donc un tableau de SIZE = 10 entiers. La valeur initiale
e
contenue dans les cases du tableau est zro (voir B.3.6.2), mais cela na pas dimportance ici,
e
car aucune case nest valide (sp est nul).
0

sp

Il nous reste ` coder les trois mthodes des objets de la classe Stack pour respectivement, tester
a
e
si la pile est vide, empiler et dpiler.
e
boolean isEmpty() { return sp == 0 ; }
void push(int x) {
i f (sp >= SIZE) throw new Error ("Push : pile pleine") ;
t[sp++] = x ; // Ou bien : t[sp] = x ; sp = sp+1 ;
}

10

52

CHAPITRE II. PILES ET FILES

int pop() {
i f (isEmpty()) throw new Error ("Pop : pile vide") ;
return t[--sp] ; // Ou bien : sp = sp-1 ; return t[sp] ;
}
La gure 5 donne un exemple dvolution du tableau t et du pointeur de pile sp. Le code cie
Fig. 5 Empiler 9, puis dpiler.
e
0

sp

sp

sp

Empiler 9

Dpiler
e

dessus signale les erreurs en lanant lexception Error (voir B.4.2), et emploie les oprateurs
c
e
de post-incrment et pr-dcrment (voir B.3.4). Notons surtout que toutes oprations sont en
e
e e e
e
temps constant, indpendant du nombre dlments contenus dans la pile, ce qui semble attendu
e
ee
dans le cas doprations aussi2simples 4 5 6 et 7 8 9 10
e
que push
pop.
0 1
3
Les deux erreurs possibles "Push : pile pleine" et "Pop : pile vide" sont de natures
bien direntes. En eet, la premi`re dcoule dune contrainte technique (la pile est insusame
e
e
ment dimensionne), tandis que la seconde dcoule dune erreur de programmation de lutilisae
e
teur de la pile. Nous allons voir ce que nous pouvons faire au sujet de ces deux erreurs.
0 1 2 3 4 5 6 7 8 9 10
Commenons par lerreur "pile pleine". Une premi`re technique simple est de passer le
c
e
bb ` lutilisateur, en lui permettant de dimensionner la pile lui-mme.
e ea
e
class Stack {
...
0
6
8
Stack (int sz) { t 1 new int[sz] 5; sp =7 0 ; } 9
= 2 3 4

10

void push(int x) {
i f (sp >= t.length) throw new Error ("Push : pile pleine") ;
t[sp++] = x ;
}
...
}
Ainsi, en cas de pile pleine, on peut toujours compter sur lutilisateur pour recommencer avec
une pile plus grande. Cela para abusif, mais cest ` peu pr`s ce qui se passe au sujet de la pile
t
a
e
des appels de mthode.
e
Mais en fait, les tableaux de Java sont susamment exibles pour autoriser le redimensionnement automatique de la pile. Il sut dallouer un nouveau tableau plus grand, de recopier les
lments de lancien tableau dans le nouveau, puis de remplacer lancien tableau par le nouveau
ee
(et dans cet ordre, cest plus simple).


2. IMPLEMENTATION DES PILES

53

private void resize() {


int [] newT = new int [2 * this.t.length] ; // Allouer le nouveau tableau
for (int k = 0 ; k < sp ; k++)
// Recopier
newT[k] = this.t[k] ;
// Remplacer
this.t = newT ;
}
void push(int x) {
i f (sp >= t.length) resize() ;
t[sp++] = x ;
}
Le redimensionnement automatique ore une exibilit maximale. En contrepartie, il a un cot :
e
u
un appel ` push ne sexcute plus en temps garanti constant, puisquil peut arriver quun appel
a
e
a
` push entra un redimensionnement dont le cot est manifestement proportionnel ` la taille
ne
u
a
de la pile.
Mais vu globalement sur une excution compl`te, le cot de N appels ` push reste de lordre
e
e
u
a
de N . On dira alors que le cot amorti de push est constant. En eet, considrons N oprations
u
e
e
push. Au pire, il ny a pas de pop et la pile contient nalement N lments pour une taille 2P
ee
du tableau interne, o` 2P est la plus petite puissance de 2 suprieure ` N . En outre, supposons
u
e
a
que le tableau est redimensionn P fois (taille initiale 20 ). Lordre de grandeur du cot cumul
e
u
e
k=P
de tous les push est donc de lordre de N (cot constant de N push) plus k=1 2k = 2P +1 1
u
(cot des P redimensionnements). Soit un cot nal de lordre de N , puisque 2P < 2 N .
u
u
Pour ce qui est de la mmoire, on alloue au total de lordre de 2P +1 cellules de mmoire,
e
e
dont la moiti ont pu tre rcupres par le garbage collector. Notons nalement que le cot
e
e
e
ee
u
amorti constant est assur quand les tailles des tableaux successifs suivent une progression
e
gomtrique, dont la raison nest pas forcment 2. En revanche, le cot amorti devient linaire
e e
e
u
e
pour des tableaux les tailles suivent une progression arithmtique. Pour cette raison, crire
e
e
int [] newT = new int [t.length + K] est rarement une bonne ide.
e
Abordons maintenant lerreur "pile vide". Ici, il ny a aucun moyen de supprimer lerreur,
car rien nempchera jamais un programmeur maladroit ou fatigu de dpiler une pile vide. Mais
e
e
e
on peut signaler lerreur plus proprement, ce qui donne la possibilit au programmeur fautif de
e
la rparer. Pour ce faire, il convient de lancer, non plus lexception Error, mais une exception
e
spcique. Les exceptions sont compl`tement dcrites dans lannexe Java en B.4. Ici, nous nous
e
e
e
contentons dune prsentation minimale. La nouvelle exception StackEmpty se dnit ainsi.
e
e
class StackEmpty extends Exception { }
Cest en fait une dclaration de classe ordinaire, car une exception est un objet dune classe un
e
peu spciale (voir B.4.2). La nouvelle exception se lance ainsi.
e
int pop () throws StackEmpty {
i f (isEmpty()) throw new StackEmpty () ;
return t[--sp] ;
}
On note surtout que la mthode pop dclare (par throws, noter le (( s ))) quelle peut lane
e
e
cer lexception StackEmpty. Cette dclaration est ici obligatoire, parce que StackEmpty est
une Exception et non plus une Error. La prsence de la dclaration va obliger lutilisateur des
e
e
piles ` soccuper de lerreur possible.
a
Pour illustrer ce point revenons sur lexemple de la calculatrice Calc (voir 1.2). La calculatrice
emploie une pile stack, elle dpile et empile directement par stack.pop() et stack.push(...).
e
Nous supposons que la classe Stack est telle que ci-dessus (avec une mthode pop qui dclare
e
e
lancer StackEmpty) et que nous ne pouvons pas modier son code. Pour organiser un peu

54

CHAPITRE II. PILES ET FILES

la suite, nous ajoutons deux nouvelles mthodes statiques push et pop ` la classe Calc, et
e
a
transformons les appels de mthode dynamiques en appels statiques.
e
class Calc {
static int pop(Stack stack) { return stack.pop() ; }
static void push(Stack stack, int x) { stack.push(x) ; }
public static void main (String [] arg) {
...
i f (x.equals("+")) {
int i1 = pop(stack), i2 = pop(stack) ;
push(stack, i2+i1) ;
...
}
}
Le compilateur refuse le programme ainsi modi. Il exige le traitement de StackEmpty qui
e
peut tre lance par stack.pop(). Traiter lexception peut se faire en attrapant lexception, par
e
e
linstruction try.
static int pop(Stack stack) {
try {
return stack.pop() ;
} catch (StackEmpty e) {
return 0 ;
}
}
Leet est ici que si linstruction return stack.pop() choue parce que lexception StackEmpty
e
est lance, cest alors linstruction return 0 qui sexcute. Cela revient ` remplacer la valeur
e
e
a
(( exceptionnelle )) StackEmpty par la valeur plus normale 0 (voir B.4.1 pour les dtails). Par
e
consquent, une pile vide se comporte comme si elle contenait une innit de zros.
e
e
e
On peut aussi dcider de ne pas traiter lexception, mais alors il faut signaler que les mthodes
e
e
Calc.pop puis main peuvent lancer lexception StackEmpty, mme si cest indirectement.
e
class Calc {
static int pop(Stack stack) throws StackEmpty { return stack.pop() ; }
public static void main (String [] arg) throws StackEmpty {
...
}
}
Si lexception survient, elle remontera de mthode en mthode et fera nalement chouer le
e
e
e
programme. En fait, comme les dclarations throws, ici obligatoires, sont quand mme pnibles,
e
e
e
on a plutt tendance ` faire chouer le programme explicitement d`s que lexception atteint le
o
a
e
e
code que lon contrle.
o
static int pop(Stack stack) {
try {
return stack.pop() ;
} catch (StackEmpty e) {
System.err.println("Adieu : pop sur pile vide") ;
e
e
System.exit(2) ; // Arr^ter le programme immdiatement (voir B.6.1.5)
return 0 ; // Le compilateur exige ce return jamais excut
e
e
}
}


2. IMPLEMENTATION DES PILES

55

Bien sr, si on veut que dpiler une pile vide provoque un arrt du programme, il aurait t
u
e
e
ee
plus court de lancer new Error ("Pop : pile vide") dans la mthode pop des objets Stack.
e
Mais nous nous sommes interdit de modier la classe Stack.

2.2

Une pile dans une liste

Cest particuli`rement simple. En eet, les lments sont empils et dpils du mme cte
e
ee
e
e e
e
o
de la pile, qui peut tre le dbut dune liste. On suppose donc donne une classe des listes
e
e
e
(dentiers). Et on crit :
e
class Stack {
private List p ;
Stack () { p = null ; }
boolean isEmpty() { return p == null ; }
void push(int x) { p = new List (x, p) ; }
int pop() throws StackEmpty {
i f (p == null) throw new StackEmpty () ;
int r = p.val ;
p = p.next ;
return p ;
}
}
Voici un exemple dvolution de la liste p.
e
p
2

Empiler 9
p

Dpiler
e
p
9

2.3

Les piles de la biblioth`que


e

Il existe dj` une classe des piles dans la biblioth`que, la classe Stack du package java.util,
ea
e
implmente selon la technique des tableaux redimensionns. Mais une classe des piles de quoi ?
e
e
e
En eet, dans les deux sections prcdentes, nous navons cod que des piles de int en
e e
e
nous disant que pour coder une pile, par exemple de String, il nous susait de remplacer int
par String partout dans le code. Il est clair quune telle (( solution )) ne convient pas ` une classe
a
de biblioth`que qui est compile par le fabricant. Apr`s bien des hsitations, Java a rsolu ce
e
e
e
e
e
probl`me en proposant des classes gnriques, ou paramtres. La classe Stack est en fait une
e
e e
e e
classe Stack<E>, o` E est nimporte quelle classe, et les objets de la classe Stack<E> sont des
u

56

CHAPITRE II. PILES ET FILES

piles dobjets de la classe E. Formellement, Stack nest donc pas exactement une classe, mais
plutt une fonction des classes dans les classes. Informellement, on peut considrer que nous
o
e
disposons dune innit de classes Stack<String>, Stack< List > etc. Par exemple on code facie
lement la pile de cha
nes de lexercice 2 comme un objet de la classe java.util.Stack<String>.
import java.util.* ;
class I n f i x {
public static void main (String [] arg) {
Stack<String> stack = new Stack<String> () ;
...
String i1 = stack.pop(), i2 = stack.pop() ;
stack.push("(" + i2 + x + i1 + ")") ;
...
}
}
(Pour import, voir B.3.5.) Cela fonctionne parce que la classe Stack<E> poss`de bien des
e
mthodes pop et push, o` par exemple pop() renvoie un objet E, ici un String.
e
u
Comme le param`tre E dans Stack<E> est une classe, il nest pas possible de fabriquer des
e
piles de int . Plus gnralement, il est impossible de fabriquer des piles de scalaires (voir B.3.1
e e
pour la dnition des scalaires). Mais la biblioth`que fournit une classe associe par type scae
e
e
laire, par exemple Integer pour int . Un objet Integer nest gu`re plus quun objet dont une
e
variable dinstance (prive) contient un int (voir B.6.1.1). Il existe deux mthodes pour convere
e
tir un scalaire int en un objet Integer et rciproquement, valueOf (logiquement statique)
e
et intValue (logiquement dynamique). De sorte qucrire la calculatrice Calc (voir 1.2) avec
e
une pile de la biblioth`que semble assez lourd au premier abord.
e
import java.util.* ;
class Calc {
public static void main (String [] arg) {
Stack<Integer> stack = new Stack<Integer> () ;
...
int i1 = stack.pop().intValue(), i2 = stack.pop().intValue() ;
stack.push(Integer.valueOf(i2+i1)) ;
...
}
}
Mais en fait, le compilateur Java sait insrer les appels aux mthodes de conversion automatie
e
quement, cest-`-dire que lon peut crire plus directement.
a
e
import java.util.* ;
class Calc {
public static void main (String [] arg) {
Stack<Integer> stack = new Stack<Integer> () ;
...
int i1 = stack.pop(), i2 = stack.pop() ;
stack.push(i2+i1) ;
...
}
}
Tout semble donc se passer presque comme si il y avait une pile de int ; mais attention, il sagit
bien en fait dune pile de Integer. Tout se passe plutt comme si le compilateur traduisait
o
le programme avec les int vers le programme avec les Integer, et cest bien ce dernier qui
sexcute, avec les consquences prvisibles sur la performance en gnral et la consommation
e
e
e
e e
mmoire en particulier.
e


3. IMPLEMENTATION DES FILES

57

Implmentation des les


e

Comme pour les piles, nous prsentons trois techniques, tableaux, listes et biblioth`que.
e
e

3.1

Une le dans un tableau

Lide, conceptuellement simple mais un peu dlicate ` programmer, est de grer deux ine
e
a
e
dices : in qui marque la position o` ajouter le prochain lment et out qui marque la position
u
ee
do` proviendra le prochain lment enlev. Lindice out marque le dbut de la le et in sa
u
ee
e
e
n. Autrement dit, le contenu de la le va la case dindice out ` la case qui prc`de la case
a
e e
dindice in.
0 1 2 3 4 5 6 7 8 9
6

out (dbut)
e

in (n)

Dans le schma ci-dessus, les cases valides sont grises, de sorte que la le ci-dessus contient
e
e
les entiers 2, 7, 0, 9 (dans lordre, 2 est le premier entr et 9 le dernier entr). Au cours de la
e
e
vie de la le, les deux indices sont croissants. Plus prcisment, on incrmente in apr`s ajout
e e
e
e
et on incrmente out apr`s suppression. Lorsquun indice atteint la n du tableau, il fait tout
e
e
simplement le tour du tableau et repart ` zro. Il en rsulte que lon peut avoir out < in. Par
a e
e
exemple, voici une autre le contenant cette fois 2, 0, 4, 6, 2, 7.
0 1 2 3 4 5 6 7 8 9
6
0

2
1

7
2

0
33

9
4

8
5

in (n)

2
6

2
77

0
8

4
9

10

out (dbut)
e

Le tableau dune le est un tableau circulaire, que lon parcourt en incrmentant un indice
e
modulo n, o` n est la taille du tableau. Par exemple pour parcourir le contenu de la le, on
u
parcourt les indice de out ` (in 1) mod n. Soit un parcours de 1 ` 4 dans le premier exemple
a
a
et de 7 ` 2 dans le second.
a
Une derni`re dicult est de distinguer entre le vide et le pleine. Comme le montre le
e
e
schma suivant, les deux indices out et in ny susent pas.
e
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
1
6 2 7 0 9 8 2 2 0 4
6 2 7 0 9 8 2 2 0 4
0
4
4
0 in
1
out

4
2

10out

4
in

Ici, out (dbut) et in (n) valent tous deux 4. Pour conna le contenu de la le il faut donc
e
tre
parcourir le tableau de lindice 4 ` lindice 3. Une premi`re interprtation du parcours donne
a
e
e
une le vide, et une seconde une le pleine. On rsout facilement la dicult par une variable nb
e
e
supplmentaire, qui contient le nombre dlments contenus dans la le. Une autre solution aurait
e
ee
t de dcrter quune le qui contient n 1 lments est pleine. On teste en eet cette derni`re
ee
e e
ee
e
condition sans ambigu e par in + 1 congru ` out modulo n. Nous choisissons la solution de la
t
a
variable nb, car conna simplement le nombre dlments de la le est pratique, notamment
tre
ee
2 3 4 5 6 7
0 1 2 3 4 5 6 7 8 9 10
pour grer le1redimensionnement.8 9 10
e 0

58

CHAPITRE II. PILES ET FILES

Fig. 6 Implmentation dune le dans un tableau


e
class FifoEmpty extends Exception { }
class Fifo {
final static int SIZE=10 ;
private int in, out, nb ;
private int [] t ;
Fifo () { t = new int[SIZE] ; in = out = nb = 0 ; }
/* Increment modulo la taille du tableau t, utilis partout */
e
private int incr(int i) { return (i+1) % t.length ; }
boolean isEmpty() { return nb == 0 ; }
int remove() throws FifoEmpty {
i f (isEmpty()) throw new FifoEmpty () ;
int r = t[out] ;
out = incr(out) ; nb-- ; // Effectivement enlever
return r ;
}
void add(int x) {
i f (nb+1 >= t.length) resize() ;
t[in] = x ;
in = incr(in) ; nb++ ; // Effectivement ajouter
}
private void resize() {
int [] newT = new int[2*t.length] ; // Allouer
int i = out ; // indice du parcours de t
for (int k = 0 ; k < nb ; k++) {
// Copier
newT[k] = t[i] ;
i = incr(i) ;
}
t = newT ; out = 0 ; in = nb ;
// Remplacer
}
/* Mthode toString, donne un exemple de parcours de la file */
e
public String toString() {
StringBuilder b = new StringBuilder () ;
b.append("[") ;
i f (nb > 0) {
int i = out ;
b.append(t[i]) ; i = incr(i) ;
for ( ; i != in ; i = incr(i))
b.append(", " + t[i]) ;
}
b.append("]") ;
return b.toString() ;
}
}


3. IMPLEMENTATION DES FILES

59

La gure 6 donne la classe Fifo des les implmentes par un tableau, avec redimensionnee
e
ment automatique. Un point cl de ce code est la gestion des indices in et out. Pour ajouter un
e
lment dans la le, in est incrment (modulo n). Par exemple, voici lajout (mthode add) de
ee
e
e
e
lentier 11 dans une le.
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
6 2 7 0 9 8 2 2 0 4
6 2 7 11 9 8 2 2 0 4
3
in

4
in

7
out

7
out

Pour supprimer un lment, on incrmente out. Par exemple, voici la suppression du premier
ee
e
lment dune le (la mthode remove renvoie ici 2).
ee
e
0 1 2 3 4 5 6 7 8 9
6 2 7 11 9 8 2 2 0 4
4
in

0 1 2 3 4 5 6 7 8 9
6 2 7 11 9 8 2 2 0 4

7
out

4
in

8
out

Le redimensionnement (mthode resize) a pour eet de tasser les lments au dbut du nouveau
e
ee
e
tableau. Voici lexemple de lajout de 13 dans une le pleine de taille 4.
0 1 2 3
6 2 7 11
2
in

0 1 2 3 4 5 6 7
7 11 6 2 13 0 0 0

2
out

0
out

5
in

Avant ajout la le contient 7, 11, 6, 2, apr`s ajout elle contient 7, 11, 6, 2, 13.
e
Le code du redimensionnement (mthode resize) est un peu simpli par la prsence de
e
e
e
la variable nb. Pour copier les lments de t dans newT, le contrle est assur par une simple
ee
o
e
boucle sur le compte des lments transfrs. Un contrle sur lindice i dans le tableau t serait
ee
ee
o
plus dlicat. La mthode toString donne un exemple de ce style de parcours du contenu de
e
e
la le, entre les indices out (inclus) et in (exclu), qui doit particulariser le cas dune le vide
et eectuer la comparaison ` in ` partir du second lment parcouru (` cause du cas de la le
a
a
ee
a
pleine).

3.2

Une le dans une liste

Limplmentation est conceptuellement simple. Les lments de la le sont dans une liste, on
e
ee
enl`ve au dbut de la liste et on ajoute ` la n de la liste. Pour garantir des oprations en temps
e
e
a
e
constant, on utilise une rfrence sur la derni`re cellule de liste. Nous avons dj` largement
ee
e
ea
exploit cette ide, par exemple pour copier une liste itrativement (voir lexercice I.2). Nous
e
e
e
nous donnons donc une rfrence out sur la premi`re cellule de la liste, et une rfrence in sur
ee
e
ee
la derni`re cellule.
e
out

in
2

La le ci-dessus contient donc les entiers 2, 7, 0, 9 dans cette ordre. Une le vide est logiquement
identie par une variable out valant null . Par souci de cohrence in vaudra alors aussi null .
e
e

60

CHAPITRE II. PILES ET FILES

class Fifo {
private List out, in ;
Fifo () { out = in = null ; }
boolean isEmpty() { return out == null ; }
int remove() throws FifoEmpty {
i f (isEmpty()) throw new FifoEmpty () ;
int r = out.val ;
out = out.next ;
i f (out == null) in = null ; // La file est vide
return r ;
}
void add(int x) {
List last = in ;
in = new List (x, null) ;
e
i f (last == null) { // La file tait vide
out = in ;
} else {
last.next = in ;
}
}
}
On constate que le code est bien plus simple que dans le cas des tableaux (gure 6). La
mthode remove est essentiellement la mme que la mthode pop des piles (voir 2.2), car les deux
e
e
e
mthodes ralisent la mme opration denlever le premier lment dune liste. La mthode add
e
e
e
e
ee
e
est ` peine plus technique, voici par exemple la suite des tats mmoire quand on ajoute 3 ` la
a
e
e
a
le dj` dessine. La premi`re instruction List last = in garde une copie de la rfrence in.
ea
e
e
ee
out

in
2

9
last

Linstruction suivante in = new List (x, null ) alloue la nouvelle cellule de liste.
out

in
2

last
Et enn, linstruction last.next = in, ajoute eectivement la nouvelle cellule pointe par in
e
a
` la n de la liste-le.
out

in
2

9
last


3. IMPLEMENTATION DES FILES

3.3

61

Les les de la biblioth`que


e

Le package java.util de la biblioth`que ore plusieurs classes qui peuvent servir comme
e
une le, mme si nous ne respectons pas trop lesprit dans lequel ces classes sont organises. (Les
e
e
les de la biblioth`que Queue ne sont pas franchement compatibles avec notre mod`le simple
e
e
des les).
Nous nous focalisons sur la classe LinkedList. Comme le nom lindique ` peu pr`s, cette
a
e
classe est implmente par une liste cha ee. Elle ore en fait bien plus que la simple fonctione
e
n
nalit de le, mais elle poss`de les mthodes add et remove des les. La classe LinkedList est
e
e
e
gnrique, comme la classe Stack de la section 2.3, ce qui la rend relativement simple demploi.
e e
Exercice 3 Les objets LinkedList implmentent en fait les (( queues ` deux bouts )) (double ene
a
ded queue), qui orent deux couples de mthodes addFirst/removeFirst et addLast/removeLast
e
pour ajouter/enlever respectivement au dbut et ` la n de queue. An dassurer des oprations
e
a
e
en temps constant, la classe de biblioth`que repose sur les listes doublement cha ees. Voici une
e
n
classe DeList des cellules de liste doublement cha ee.
n
class DeList {
int val ; DeList next, prev ;
DeList (int val, DeList next, DeList prev) {
this.val = val ; this.next = next ; this.prev = prev ;
}
}
Le cha
nage double permet de parcourir la liste de la premi`re cellule ` la derni`re en suivant
e
a
e
les champs next, et de la derni`re ` la premi`re en suivant le champ prev.
e a
e
2

fst

11

lst

Autrement dit, si p est une rfrence vers une cellule de liste doublement cha ee qui nest pas la
ee
n
premi`re, on a lgalit p.prev.next == p ; et si p pointe vers la premi`re cellule, on a p.prev
e
e
e
e
== null. De mme, si p pointe vers une cellule qui nest pas la derni`re, on a p.next.prev ==
e
e
p ; et si p pointe vers la derni`re cellule, on a p.next == null.
e
Solution. Dcomposons une opration, par exemple addFirst, en ajoutant 3 au dbut de la
e
e
e
queue dj` dessine. Dans un premier temps on peut allouer la nouvelle cellule, avec un champ
ea
e
next correct (voir push en 2.2). Le champ prev peut aussi tre initialis immdiatement ` null .
e
e
e
a
fst

11

lst

Reste ensuite ` ajuster le champ prev de la nouvelle seconde cellule de la liste, cellule accessible
a
par fst.next ((fst.next).prev = fst).
fst

11

lst

62

CHAPITRE II. PILES ET FILES

Fig. 7 Classe des queues ` deux bouts


a
class DeQueue {
private DeList fst, lst ; // First and last cell
DeQueue () { fst = lst = null ; }
boolean isEmpty() { return fst == null ; }
void addFirst(int x) {
fst = new DeList(x, fst, null) ;
i f (lst == null) {
lst = fst ;
} else {
fst.next.prev = fst ;
}
}
void addLast(int x) {
lst = new DeList(x, null, lst) ;
i f (fst == null) {
fst = lst ;
} else {
lst.prev.next = lst ;
}
}
int removeFirst() {
i f (fst == null) throw new Error ("removeFirst: empty queue") ;
int r = fst.val ;
fst = fst.next ;
i f (fst == null) {
lst = null ;
} else {
fst.prev = null ;
}
return r ;
}
int removeLast() {
i f (lst == null) throw new Error ("removeLast: empty queue") ;
int r = lst.val ;
lst = lst.prev ;
i f (lst == null) {
fst = null ;
} else {
lst.next = null ;
}
return r ;
}
}


4. TYPE ABSTRAIT, CHOIX DE LIMPLEMENTATION

63

Pour supprimer par exemple le dernier lment de la queue, il sut de changer le contenu de
ee
lst (lst = lst.prev).

fst

11

lst

Reste ensuite ` ajuster le champ next de la nouvelle derni`re cellule (lst.next = null ).
a
e

fst

11

lst

Le code, pas si dicile est donn par la gure 7. Une fois compris le mcanisme dajout et de
e
e
retrait des cellules, le point dlicat est de bien grer le cas de la queue vide qui correspond `
e
e
a
fst et lst tous deux gaux ` null .
e
a

Type abstrait, choix de limplmentation


e

Nous avons prsent trois faons dimplmenter piles et les. Il est naturel de se demander
e
e
c
e
quelle implmentation choisir. Mais nous tenons dabord a faire remarquer que, du strict point
e
`
de vue de la fonctionnalit, la question ne se pose pas. Tout ce qui compte est davoir une pile
e
(ou une le). En eet, les trois implmentations des piles orent exactement le mme service,
e
e
de sorte que le choix dune implmentation particuli`re na aucun impact sur le rsultat dun
e
e
e
programme qui utilise une pile. La pile qui est dnie exclusivement par les services quelle ore
e
est un exemple de type de donnes abstrait.
e
Java ore des traits qui permettent de fabriquer des types abstraits qui prot`gent leur
e
implmentation des interventions intempestives. Dans nos deux classes Stack, le tableau (ainsi
e
que le pointeur de pile sp) et la liste sont des champs privs. Un programme qui utilise nos piles
e
doit donc le faire exclusivement par lintermdiaire des mthodes push et pop. Champs privs
e
e
e
et mthodes accessibles correspondent directement ` la notion de type abstrait dni par les
e
a
e
services oerts.
Il faut toutefois remarquer que, du point de vue du langage de programmation, lexistence
des deux (trois en fait) mthodes reste pour le moment une convention : nous avons convene
e
tionnellement appel Stack la classe des piles et conventionnellement admis quelle poss`de ces
e
trois mthodes :
e
boolean isEmpty() { . . . }
void push(int x) { . . . }
int pop() throws StackEmpty { . . . }
Il en rsulte par exemple que nous ne pouvons pas encore mlanger les piles en tableau et les piles
e
e
en liste dans le mme programme, ` moins de changer le nom des classes ce qui contredirait lide
e
a
e
dun type abstrait. Nous verrons au chapitre suivant comment procder, ` laide des interfaces.
e
a
Rpondons maintenant srieusement ` la question du choix de limplmentation. Dans disons
e
e
a
e
99 % des cas pour les les et 90 % des cas pour les piles, il faut choisir la classe de biblioth`que,
e
parce que cest la solution qui demande dcrire le moins de code. La dirence de pourcentage
e
e
sexplique en comparant le temps de lecture de la documentation au temps dcriture dune
e
classe des les ou des piles, et parce que la classe des piles est quand mme simple ` crire.
e
ae

64

CHAPITRE II. PILES ET FILES

La biblioth`que peut ne pas convenir pour des raisons decacit : le code est trop lent ou
e
e
trop gourmand en mmoire pour notre programme particulier et nous pouvons faire mieux que
e
lui. Par exemple, le code de biblioth`que entra la fabrication dobjets Integer en pagaille, et
e
ne
une pile de scalaires se montre au nal plus rapide. La classe de biblioth`que peut aussi ne pas
e
convenir parce quelle nore pas la fonctionnalit indite dont nous avons absolument besoin (par
e e
exemple une inversion compl`te de la pile), ou plus frquemment parce que programmer cette
e
e
fonctionnalit de la faon autorise par la structuration de la biblioth`que serait trop compliqu,
e
c
e
e
e
trop coteux, ou tout simplement impossible avec notre connaissance limite de Java.
u
e
Il reste alors ` choisir entre tableaux et listes. Il ny a pas de rponse toute faite ` cette
a
e
a
derni`re question. En eet, dune part, la dicult de lcriture dun programme dpend aussi
e
e
e
e
du got et de lexprience de chacun ; et dautre part, lecacit respective de lune ou de lautre
u
e
e
technique dpend de nombreux facteurs, (la pile cro
e
t-elle beaucoup, par exemple) et aussi de
lecacit de la gestion de la mmoire par le syst`me dexcution de Java. Toutefois, dans le cas
e
e
e
e
o` le redimensionnement est inutile, le choix dune pile-tableau simpose probablement, puisque
u
simplicit et ecacit concordent. Dans le cas gnral, on peut tout de mme prvoir quutiliser
e
e
e e
e
e
les listes demande dcrire un peu moins de code que dutiliser les tableaux (redimensionns). On
e
e
peut aussi penser que les tableaux seront un plus ecaces que les listes, ou en tout cas utiliseront
moins de mmoire au nal. En eet, fabriquer une pile-tableau de N lments demande dallouer
e
ee
log2 N objets (tableaux) contre N objets (cellules de listes) pour une pile-liste. Or, allouer un
objet est une opration ch`re.
e
e
Supposons quun objet occupe deux cases de mmoire en plus de lespace ncessaire pour ses
e
e
donnes (cest une hypoth`se assez probable pour Java). Une cellule de liste occupe donc 4 cases
e
e
et un tableau de taille n occupe 3+n cases (dont une case pour la longueur). Pour un programme
eectuant au total P push, la pile-liste aura allou au total 4 P cases de mmoire, tandis que
e
e
la pile-tableau aura allou entre une (si la taille initiale du tableau est 1, et que le programme
e
alterne push et pop) et environ 4 P + 3 log2 P cases de mmoire (dans le cas o` aucun pop ne
e
u
spare les push et o` le tableau est redimensionn par le dernier push). La pile-tableau nalloue
e
u
e
donc jamais signicativement plus de mmoire que la pile-liste, et gnralement plutt moins.
e
e e
o
Un autre cot intressant est lempreinte mmoire, la quantit de mmoire mobilise ` un
u
e
e
e
e
e a
instant donn. Une pile-le de N lments mobilise 4 N cases de mmoire. Une pile-tableau
e
ee
e
mobilise entre 3+N et un nombre arbitrairement grand de cases de mmoire (le nombre arbitraire
e
correspond ` la taille du tableau lorsque, dans le pass, la pile a atteint sa taille maximale). Un
a
e
nouvel lment intervient donc dans le choix de limplmentation : la profondeur maximale de
ee
e
pile au cours de la vie du programme. Si cette profondeur reste raisonnable le choix de la piletableau simpose, autrement le choix est moins net, mais la exibilit de lallocation petit-`-petit
e
a
des cellules de la pile-liste est un avantage. On peut aussi envisager de rduire la taille du tableau
e
interne de la pile tableau, par exemple en rduisant le tableau de la moiti de sa taille quand
e
e
il nest plus quau quart plein. Mais il faut alors y regarder ` deux fois, notre classe des piles
a
commence ` devenir complique est tout ce code ajout entra un prix en temps dexcution.
a
e
e
ne
e

Chapitre III

Associations Tables de hachage


Ce chapitre aborde un probl`me tr`s frquemment rencontr en informatique : la recherche
e
e e
e
dinformation dans un ensemble gr de faon dynamique. Nous nous plaons dans le cadre o`
ee
c
c
u
une information compl`te se retrouve normalement ` laide dune cl qui lidentie. La notion se
e
a
e
rencontre dans la vie de tous les jours, un exemple typique est lannuaire tlphonique : conna
ee
tre
le nom et les prnoms dun individu sut normalement pour retrouver son numro de tlphone.
e
e
ee
En cas dhomonymie absolue (tout de mme rare), on arrive toujours ` se dbrouiller. Dans les
e
a
e
socits modernes qui refusent le ou (et dans les ordinateurs) la cl doit identier un individu
ee
e
unique, do`, par exemple, lide du numro de scurit sociale,
u
e
e
e
e
Nous voulons un ensemble dynamique dinformations, cest-`-dire aussi pouvoir ajouter ou
a
supprimer un lment dinformation. On en vient naturellement ` dnir un type abstrait de
ee
a e
donnes, appel table dassociation qui ore les oprations suivantes.
e
e
e
Trouver linformation associ ` une cl donne.
ea
e
e
Ajouter une nouvelle association entre une cl et une information.
e
Retirer une cl de la table (avec linformation associe).
e
e
La seconde opration mrite dtre dtaille. Lors de lajout dune paire cl-information, nous
e
e
e
e
e
e
prcisons :
e
Sil existe dj` une information associe ` la cl dans la table, alors la nouvelle information
ea
e a
e
remplace lancienne.
Sinon, une nouvelle association est ajoute ` la table.
e a
Il en rsulte quil ny a jamais dans la table deux informations distinctes associes ` la mme
e
e a
e
cl.
e
Il nest pas trop dicile denvisager la possibilit inverse, en ajoutant une nouvelle association
e
a
` la table dans tous les cas, que la cl sy trouve dj` ou pas. Il faut alors rviser un peu les deux
e
ea
e
autres oprations, an didentier linformation concerne parmi les ventuellement multiples qui
e
e
e
sont associes ` une mme cl. En gnral, on choisit linformation la plus rcemment entre, ce
e a
e
e
e e
e
e
qui revient ` un comportement de pile.
a
Enn, il peut se faire que la cl fasse partie de linformation, comme cest gnralement le
e
e e
cas dans une base de donnes. Dans ce cas appelons enregistrement un lment dinformation.
e
ee
Un enregistrement est compos de plusieurs champs (nom, prnom, numro de scurit sociale,
e
e
e
e
e
sexe, date de naissance etc.) dont un certain nombre peuvent servir de cl. Il ny a l` aucune
e
a
dicult du moins en thorie. Il se peut aussi que linformation se rduise ` la cl, dans ce cas
e
e
e
a
e
la table dassociation se rduit ` lensemble (car il ny a pas de doublons).
e
a

Statistique des mots

Nous illustrons lintrt de la table dassociation par un exemple ludique : un programme Freq
ee
qui compte le nombre doccurrences des mots dun texte, dans le but de produire dintressantes
e
65

66

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE

statistiques. Soit un texte, par exemple notre hymne national, nous aurons
% java Freq marseillaise.txt
nous: 10
vous: 8
franais: 7
c
leur: 7
dans: 6
libert: 6
e
...

1.1

Rsolution par une table dassociation


e

Le probl`me ` rsoudre se dcompose naturellement en trois :


e
a e
e
(1) Lire un chier texte mot ` mot.
a
(2) Compter les occurrences des mots.
(3) Produire un bel achage, par exemple prsenter les mots dans lordre dcroissant du
e
e
nombre de leurs occurrences.
Supposons les premier et troisi`me points rsolus. Le troisi`me par un tri et le premier par une
e
e
e
classe WordReader des ux de mots. Les objets de cette classe poss`dent une mthode read qui
e
e
renvoie le mot suivant du texte (un String) ou null ` la n du texte.
a
Une table dassociation permet de rsoudre facilement le deuxi`me point : il sut dassocier
e
e
`
mots et entiers. Un mot qui nest pas dans la table est conventionnellement associ ` zro. A
ea e
chaque mot lu m on retrouve lentier i associ ` m, puis on change lassociation en m associ
ea
e
a
` i + 1. Nous pourrions spcier en franais une version rane de table dassociation (pas de
e
c
e
suppression, information valant zro par dfaut). Nous prfrons le faire directement en Java en
e
e
ee
dnissant une interface Assoc.
e
interface Assoc {
/* Crer/remplacer lassociation key val */
e
void put(String key, int val) ;
/* Trouver lentier associ ` key, ou zro. */
e a
e
int get(String key) ;
}
La dnition dinterface ressemble ` une dnition de classe, mais sans le code des mthodes.
e
a
e
e
Cette absence nempche nullement demployer une interface comme un type, nous pouvons
e
donc parfaitement crire une mthode count, qui compte les occurrences des mots du texte
e
e
dans un Assoc t pass en argument, sans que la classe exacte de t soit connue1 .
e
static void count(WordReader in, Assoc t) {
for (String word = in.read() ; word != null ; word = in.read()) {
i f (word.length() >= 4) {
// Retenir les mots suffisamment longs
word = word.toLowerCase() ; // Minusculer le mot
t.put(word, t.get(word)+1) ;
}
}
}
(Voir B.6.1.3 pour les deux mthodes des String employes). Une fois remplie, la table est
e
e
simplement ache sur la sortie standard.
e
1

e e
Vous noterez la dirence avec les classes Fifo et Stack du chapitre prcdent
e

1. STATISTIQUE DES MOTS

67

Assoc t = . . . ;
WordReader in = . . . ;
count(in, t) ;
System.out.println(t.toString()) ;
Les points de la cration du WordReader et du tri de la table dans sa mthode toString sont
e
e
e
e
e
e
supposs rsolus, nous estimerons donc le programme Freq crit d`s que nous aurons implment
e e
la table dassociation ncessaire.
e

1.2

Implmentation simple de la table dassociation


e

La technique dimplmentation la plus simple dune table dassociations est denvisager une
e
liste des paires cl-information. Dans lexemple de la classe Freq, les cls sont des cha
e
e
nes et les
informations des entiers, nous dnissons donc une classe simple des cellules de listes.
e
class AList {
String key ; int val ;
AList next ;
AList (String key, int val, List next) {
this.key = key ; this.val = val ; this.next = next ;
}
}
Nous aurions pu dnir dabord une classe des paires, puis une classe des listes de paires, mais
e
nous prfrons la solution plus conome en mmoire qui consiste ` ranger les deux composantes
ee
e
e
a
de la paire directement dans la cellule de liste.
e
e
Nous dotons la classe AList dune unique mthode (statique, null tant une liste valide)
destine ` retrouver une paire cl-information ` partir de sa cl.
e a
e
a
e
static AList getCell(String key, AList p) {
for ( ; p != null ; p = p.next)
i f (key.equals(p.key)) return p ;
return null ;
}
La mthode getCell renvoie la premi`re cellule de la liste p dont le champ vaut la cl passe en
e
e
e
e
argument. Si cette cellule nexiste pas, null est renvoy. Notons que les cha
e
nes sont compares
e
par la mthode equals et non pas par loprateur ==, cest plus prudent (voir B.3.1.1). Cette
e
e
e
mthode getCell sut pour crire la classe L des tables dassociations base sur une liste
e
e
dassociation interne.
class L implements Assoc {
private AList p ;
L() { p = null ; }
public int get(String key) {
AList r = AList.getCell(key, p) ;
i f (r == null) { // Absent de la table
return 0 ;
// Prsent dans la table
e
} else {
return r.val ;
}
}

68

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE


public void put(String key, int val) {
AList r = AList.getCell(key, p) ;
e
i f (r == null) { // Absent de la table, crer une nouvelle association
p = new AList (key, val, p) ;
e
} else { // Prsent dans la table, modifier lancienne association
r.val = val ;
}
}

}
Les objets L encapsulent une liste dassociation. Les mthodes get et put emploient toutes les
e
deux la mthode AList .getCell pour retrouver linformation associe ` la cl key passe en
e
e a
e
e
argument. La technique de lencapsulage est dsormais famili`re, nous lavons dj` exploite
e
e
ea
e
pour les ensembles, les piles et les les dans les chapitres prcdents.
e e
e
e
Mais il faut surtout noter une nouveaut : la classe L dclare implmenter linterface Assoc
e
(mot-cl implements). Cette dclaration entra deux consquences importantes.
e
e
ne
e
Le compilateur Java vrie que les objets de la classe L poss`dent bien les deux mthodes
e
e
e
spcies par linterface Assoc, avec les signatures conformes. Il y a un dtail trange : les
e e
e
e
mthodes spcies par une interface sont obligatoirement public.
e
e e
Un objet L peut prendre le type Assoc, ce qui arrive par exemple dans lappel suivant :
// in est un WordReader
count(in, new L()) ; // Compter les mots de in.
Notez que count ne conna pas la classe L, seulement linterface Assoc.
t
Notre programme Freq fonctionne, mais il nest pas tr`s ecace. En eet si la liste dassociation
e
est de taille N , une recherche par getCell peut prendre de lordre de N oprations, en particulier
e
dans le cas frquent o` la cl nest pas dans la liste. Il en rsulte que le programme Freq est en
e
u
e
e
O(n2 ) o` n est le nombre de mots de lentre. Pour atteindre une ecacit bien meilleure, nous
u
e
e
allons introduire la nouvelle notion de table de hachage.

Table de hachage

La table de hachage est une implmentation ecace de la table dassociation. Appelons


e
univers des cls lensemble U de toutes les cls possibles. Nous allons dabord observer quil existe
e
e
un cas particulier simple quand lunivers des cls est un petit intervalle entier, puis ramener le
e
cas gnral ` ce cas simple.
e e
a

2.1

Adressage direct

Cette technique tr`s ecace ne peut malheureusement sappliquer que dans des cas tr`s
e
e
particuliers. Il faut que lunivers des cls soit de la forme {0, . . . , n 1}, o` n est un entier pas
e
u
trop grand, et dautre part que deux lments distincts aient des cls distinctes (ce que nous
ee
e
avons dailleurs dj` suppos). Il sut alors dutiliser un tableau de taille n pour reprsenter la
ea
e
e
table dassociation.
Ce cas sapplique par exemple ` la base de donne des concurrents dune preuve sportive
a
e
e
qui exclut les ex-quos. Le rang ` larrive dun concurrent peut servir de cl dans la base de
a
e
e
donnes. Mais, ne nous leurrons pas un cas aussi simple est exceptionnel en pratique. Le fait
e
pertinent est de remarquer que le probl`me de la recherche dinformation se simplie beaucoup
e
quand les cls sont des entiers pris dans un petit intervalle.
e

2. TABLE DE HACHAGE

2.2

69

Table de hachage

Lide fondamentale de la table de hachage est de se ramener au cas de ladressage direct,


e
cest-`-dire de cls qui sont des indices dans un tableau. Soit m, entier pas trop grand, ` vrai
a
e
a
dire entier de lordre du nombre dlments dinformations que lon compte grer. On se donne
ee
e
une fonction
h : U {0, . . . , m 1}
appele fonction de hachage. Lide est de ranger llment de cl k non pas dans une case de
e
e
ee
e
tableau t[k], comme dans ladressage direct (cela na dailleurs aucun sens si k nest pas un
entier) , mais dans t[h(k)]. Nous reviendrons en 3 sur le choix, relativement dlicat, de la
e
fonction de hachage. Mais nous devons aronter d`s ` prsent une dicult. En eet, il devient
e a e
e
draisonnable dexclure le cas de cls (distinctes) k et k telles que h(k) = h(k ). La gure 1
e
e
illustre la survenue dune telle collision entre les cls k1 et k3 distinctes qui sont telles que
e
h(k1 ) = h(k3 ). Prcisons un peu le probl`me, supposons que la collision survient lors de lajout
e
e
Fig. 1 Une collision dans une table de hachage.

Univers U des cls


e
k2
k1
k3
k0

10
9
8
7
6
5
4
3
2
1
0

0
h(k2 )

h(k1 ) = h(k3 )

h(k0 )
m1
0

de llment dinformation v3 de cl k3 , alors quil existe dj` dans la table une cl k1 avec
ee
e
ea
e
h(k1 ) = h(k3 ). La question est alors : o` ranger linformation associe ` la cl k3 ?
u
e a
e

2.3

Rsolution des collisions par cha


e
nage

La solution la plus simple pour rsoudre les collisions consiste ` mettre tous les lments dine
a
ee
formation dont les cls ont mme valeur de hachage dans une liste. On parle alors de rsolution
e
e
e
des collisions par cha
nage. Dans le cas de lexemple de collision de la gure 1, on obtient la
situation de la gure 2. On remarque que les lments de la table t sont tout btement des listes
ee
e
dassociation, la liste t[i] regroupant tous les lments dinformation (k, v) de la table qui sont
ee
tels que h(k) vaut lindice i.
Nous proposons une nouvelle implmentation H des tables dassociations Assoc, en encape
sulant cette fois une table de hachage.
class H implements Assoc {
final static int SIZE=1024 ; // Assez grand ?
private AList [] t ; // Tableau interne de listes dassociations.
H() { t = new AList [SIZE] } ;
private int hash(String key) { return Math.abs(key.hashCode()) % t.length ;}

70

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE


Fig. 2 Rsolution des collisions par cha
e
nage.

Univers U des cls


e
k2
k1
k3
k0

10
9
8
7
6
5
4
3
2
1
0

k2 v2

k3 v3

k1 v1

k0 v0

public int get(String key) {


int h = hash(key) ;
AList r = AList.getCell(key, t[h]) ;
i f (r == null) {
return 0 ;
} else {
return r.val ;
}
}
public void put(String key, int val) {
int h = hash(key) ;
AList r = AList.getCell(key, t[h]) ;
i f (r == null) {
t[h] = new AList(key, val, t[h]) ;
} else {
r.val = val ;
}
}
}
Le code de la fonction de hachage hash est en fait assez simple, parce quil utilise la mthode
e
de hachage des cha
nes fournie par Java (toute la complexit est cache dans cette mthode)
e
e
e
dont il rduit le rsultat modulo la taille du tableau interne t, an de produire un indice valide.
e
e
La valeur absolue Math.abs est malheureusement ncessaire, car pour n ngatif, loprateur
e
e
e
(( reste de la division euclidienne )) % renvoie un rsultat ngatif (mis`re).
e
e
e
Il est surtout important de remarquer :
Le code est en fait presque le mme que celui de la classe L (page 67), en remplaant p
e
c
par t[h].
La classe H dclare implmenter linterface Assoc et le fait eectivement ce que le come
e
pilateur vrie. Un objet de la nouvelle classe H est donc un argument valide pour la
e
mthode count de la classe Freq.
e
Estimons le cot de put et de get pour une table qui contient N lments dinformation. On
u
ee
suppose que le cot du calcul de la fonction hachage est en O(1), et que hachage est uniforme,
u
cest-`-dire que la valeur de hachage dune cl vaut h [0 . . . m[ avec une probabilit 1/m.
a
e
e

2. TABLE DE HACHAGE

71

Ces deux hypoth`ses sont ralistes. Pour la premi`re, en supposant que le cot de calcul de la
e
e
e
u
fonction de hachage est proportionnel ` la longueur des mots, nous constatons que la longueur
a
des mots dun texte ordinaire de N mots est faible et indpendante de N .2 La seconde hypoth`se
e
e
traduit simplement que nous disposons dune (( bonne )) fonction de hachage, faisons conance
a
` la mthode hashCode des String.
e
Sous ces deux hypoth`ses, la recherche dun lment se fait en moyenne en temps (1 + ),
e
ee
o` = n/m est le facteur de charge (load factor ) de la table (n est le nombre de cls ` ranger et
u
e a
m est la taille du tableau). Plus prcisment une recherche infructueuse dans la table parcourt
e e
en moyenne cellules de listes, et une recherche fructueuse 1 + /2 1/(2m) cellules, cots
u
auquels on ajoute le cot du calcul de la fonction de hachage. Ce rsultat est dmontr dans [2,
u
e
e
e
section 12.2], contentons nous de remarquer que est tout simplement la longueur moyenne des
listes dassociations t[h].
Peut-tre faut il remarquer que le cot dune recherche dans le cas le pire est O(n), quand
e
u
toutes les cls entrent en collision. Mais employer les tables de hachage suppose de faire conance
e
au hasard (hachage uniforme) et donc de considrer plus le cas moyen que le cas le pire. Une
e
faon plus concr`te de voir les choses est de considrer que, par exemple lors du comptage des
c
e
e
mots dun texte, on ins`re et recherche de nombreux mots uniformment hachs, et que donc le
e
e
e
cot moyen donne une tr`s bonne indication du cot rencontr en pratique.
u
e
u
e
Dans un premier temps, pour notre implmentation simple de H qui dimensionne le tableau t
e
initialement, nous pouvons interprter le rsultat de complexit en moyenne dune recherche en
e
e
e
(1 + ), en constatant que si la taille du tableau interne est de lordre de n, alors nous avons
atteint un cot (en moyenne) de put et get en temps constant. Il peut sembler que nous nous
u
sommes livrs ` une suite dapproximations et d`-peu-pr`s, et cest un peu vrai. Il nen reste pas
e a
a
e
moins, et cest le principal, que les tables de hachage sont ecaces en pratique, essentiellement
sous rserve que pour une excution donne, les valeurs de hachage des cls se rpartissent
e
e
e
e
e
uniformment parmi les indices du tableau interne correctement dimensionn, mais aussi que le
e
e
cot du calcul de la fonction de hachage ne soit pas trop lev. Dans cet esprit pragmatique, on
u
e e
peut voir la table de hachage comme un moyen simple de diviser le cot des listes dassociation
u
dun facteur n, au prix de lallocation dun tableau de taille de lordre de n.
2.3.1

Complment : redimensionnement dynamique


e

Dans un deuxi`me temps, il est plus convenable, et ce sera aussi plus pratique, de redie
mensionner dynamiquement la table de hachage an de maintenir le facteur de charge dans des
limites raisonnables. Pour atteindre un cot amorti en temps constant pour put, il sut de deux
u
conditions (comme pour push dans le cas des piles, voir II.2.1)
La taille des tableaux internes doit suivre une progression gomtrique au cours du temps.
e e
Le cot du redimensionnement doit tre proportionnel au nombre dinformations stockes
u
e
e
dans la table au moment de ce redimensionnement.
Dnissons dabord une constante alpha qui est notre borne suprieure du facteur de charge,
e
e
et une variable dinstance nbKeys qui compte le nombre dassociations eectivement prsentes
e
dans la table.
final static double alpha = 4.0 ;
private int nbKeys = 0 ;
final static int SIZE = 16 ;
2
Un autre argument est de dire quil existe de lordre de N = K mots de taille infrieure ` , o` K est le
e
a
u
nombre de caract`res possibles. Dans ce cas le cot du calcul de la fonction de hachage est en O(log N ), rput
e
u
e
e
indpendant de n pour n N .
e

72

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE

Nous avons aussi chang la valeur de la taille par dfaut de la table, an de ne pas mobiliser
e
e
une quantit consquente de mmoire a priori. Cest aussi une bonne ide de procder ainsi an
e
e
e
e
e
que le redimensionnement ait eectivement lieu et que le code correspondant soit test.
e
La mthode de redimensionnement resize, ` ajouter ` la classe H double la taille du tableau
e
a
a
interne t.
private void resize() {
// Ancienne taille
int old_sz = t.length ;
int new_sz = 2*old_sz ;
// Nouvelle taille
AList [] oldT = t ;
// garder une rfrence sur lancien tableau
e e
t = new AList [new_sz] ;
// Allouer le nouveau tableau
/* Insrer toutes les paires cl-information de oldT
e
e
dans le nouveau tableau t */
for (int i = 0 ; i < old_sz ; i++) {
for (AList p = oldT[i] ; p != null ; p = p.next) {
int h = hash(p.key) ;
t[h] = new AList (p.key, p.val, t[h]) ;
}
}
}
Il faut noter que la fonction de hachage hash qui transforme les cls en indices du tableau t
e
dpend de la taille de t (de fait son code emploie this .t.length). Pour cette raison, le nouveau
e
tableau est directement rang dans le champ t de this et une rfrence sur lancien tableau est
e
ee
conserve dans la variable locale oldT, le temps de parcourir les paires cl-information contenues
e
e
dans les listes de lancien tableau pour les ajouter dans le nouveau tableau t. Le redimensionnement nest pas gratuit, il est mme assez coteux, mais il reste bien proportionnel au nombre
e
u
dinformations stockes sous rserve dun calcul en temps constant de la fonction de hachage.
e
e
Cest la mthode put qui tient ` jour le compte nbKey et appelle le mthode resize quand
e
a
e
le facteur de charge nbKeys/t.length dpasse alpha.
e
public void put(String key, int val) {
int h = hash(key) ;
AList r = AList.getCell(key, t[h]) ;
i f (r == null) {
t[h] = new AList(key, val, t[h]) ;
nbKeys++ ;
i f (t.length * alpha < nbKeys) {
resize() ;
}
} else {
r.val = val ;
}
}
Notez que le redimensionnement est, le cas chant, eectu apr`s ajout dune nouvelle associae e
e
e
tion. En eet, la valeur de hachage h nest valide que relativement ` la longueur de tableau t.
a

2.4

Adressage ouvert

Dans le hachage ` adressage ouvert, les lments dinformations sont stocks directement
a
ee
e
dans le tableau. Plus prcisment, la table de hachage est un tableau de paires cl-information.
e e
e

Le facteur de charge est donc ncessairement infrieur ` un. Etant donne une cl k on
e
e
a
e
e
recherche linformation associe ` k dabord dans la case dindice h(k), puis, si cette case est
e a
occupe par une information de cl k dirente de k, on continue la recherche en suivant une
e
e
e

2. TABLE DE HACHAGE

73

squence dindices prdnie, jusqu` trouver une case contenant une information dont la cl
e
e e
a
e
vaut k ou une une case libre. Dans le premier cas il existe un lment de cl k dans la table,
ee
e
dans le second il nen existe pas. La squence la plus simple consiste ` examiner successivement
e
a
les indices h(k), h(k) + 1, h(k) + 2 etc. modulo m taille de la table. Cest le sondage linaire
e
(linear probing).
Pour ajouter une information (k, v), on proc`de exactement de la mme mani`re, jusqu`
e
e
e
a
trouver une case libre ou une case contenant la paire (k, v ). Dans les deux cas, on dispose dune
case o` ranger (k, v), au besoin en crasant la valeur v anciennement associe ` k. Selon cette
u
e
e a
technique, une fois entre dans la table, une cl reste ` la mme place dans le tableau et est
e
e
a
e
accde selon la mme squence, ` condition de ne pas supprimer dinformations, ce que nous
e e
e
e
a
supposons.
Pour coder une nouvelle implmentation O de la table dassociation Assoc, qui utilise le
e
hachage avec adressage ouvert. Nous dnissons dabord une classe des paires cl-information.
e
e
class Pair {
String key ; int val ;
Pair(String key, int val) { this.key = key ; this.val = val ; }
}
Les objets O poss`dent en propre un tableau dobjets Pair . Le code de la classe O est donn
e
e
par la gure 3. Dans le constructeur, les cases du tableau new Pair [SIZE] sont implicitement
initialises ` null (voir B.3.6.2), qui est justement la valeur qui permet ` getSlot didentier
e a
a
les cases (( vides )). La mthode getSlot, charge de trouver la case o` ranger une association
e
e
u
en fonction de la cl, est appele par les deux mthodes put et get. La relative complexit de
e
e
e
e
getSlot justie cette organisation. La mthode getSlot peut chouer, quand la table est pleine
e
e
notez lemploi de la boucle do, voir B.3.4, ce qui rend la question du dimensionnement du
tableau interne plus critique que dans le cas du cha
nage.
Exercice 1 Modier le code de la classe O an de redimensionner automatiquement le tableau
interne, d`s que le facteur de charge dpasse une valeur critique alpha.
e
e
final static double alpha = 0.5 ;
Solution. Comme dans le cas du cha
nage, nous allons crire une mthode prive resize charge
e
e
e
e
dagrandir la table quand elle devient trop charge. La mthode put est modie pour grer le
e
e
e
e
compte nbKeys des informations eectivement prsentes dans la table, et appeler resize si
e
besoin est.
private int nbKeys = 0 ;
public void put(String key, int val) {
int h = getSlot(key) ;
Pair p = t[h] ;
i f (p == null) {
nbKeys++ ;
t[h] = new Pair(key, val) ;
i f (t.length * alpha < nbKeys) resize() ;
} else {
p.val = val ;
}
}

74

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE

Fig. 3 Implmentation dune table de hachage ` adressage ouvert


e
a
class O implements Assoc {
private final static int SIZE = 1024 ; // Assez grand ?
private Pair [] t ;
// Tableau interne de paires
O() { t = new Pair[SIZE] ; }
private int hash(String key) { return Math.abs(key.hashCode()) % t.length ; }
/* Mthode de recherche de la case associe ` key */
e
e a
private int getSlot(String key) {
int h0 = hash(key) ;
int h = h0 ;
do {
/* Si t[h] est (( vide )) ou contient la cl key, on a trouv */
e
e
i f (t[h] == null || key.equals(t[h].key)) return h ;
/* Sinon, passer ` la case suivante */
a
h++ ;
i f (h >= t.length) h = 0 ;
} while (h != h0) ;
throw new Error ("Table pleine") ; // On a fait le tour complet
}
public int
Pair p =
i f (p ==
return
} else {
return
}
}

get(String key) {
t[getSlot(key)] ;
null) {
0 ;
p.val ;

public void put(String key, int val) {


int h = getSlot(key) ;
Pair p = t[h] ;
i f (p == null) {
t[h] = new Pair(key, val) ;
} else {
p.val = val ;
}
}
}

2. TABLE DE HACHAGE

75

La mthode resize fait appel ` getSlot pour transfrer les informations de lancienne ` la
e
a
e
a
nouvelle table. Cest le meilleur moyen de garantir des ajouts compatibles avec les mthodes
e
put et get.
private void resize() {
int old_sz = t.length ;
int new_sz = 2*old_sz ;
Pair [] oldT = t ;
t = new Pair[new_sz] ;
for (int k = 0 ; k < old_sz ; k++) {
Pair p = oldT[k] ;
i f (p != null) t[getSlot(p.key)] = p ;
}
}
}
Il faut, comme dans le cas du cha
nage, prendre la prcaution de ranger le nouveau tableau dans
e
la variable dinstance t avant de commencer ` calculer les valeurs de hachage dans le nouveau
a
tableau. On note aussi que oldT[k] peut valoir null et quil faut en tenir compte.
On dmontre [6, section 6.4] quune recherche infructueuse entra en moyenne lexamen
e
ne
denviron 1/2 (1 + 1/(1 )2 ) cases et une recherche fructueuse denviron 1/2 (1 + 1/(1 )),
o` est le facteur de charge et sous rserve de hachage uniforme. Ces rsultats ne sont stricto
u
e
e
sensu plus valables pour proche de un, mais les formules donnent toujours un majorant. Bref,
pour un facteur de charge de 50 % on examine en moyenne pas plus de trois cases.
Le sondage linaire provoque des phnom`nes de regroupement (en plus des collisions).
e
e
e
Considrons par exemple la table ci-dessous, o` les cases encore libres sont en blanc :
e
u
1
0

10 11 12 13 14 15 16 17 18

En supposant que h(k) soit distribu uniformment, la probabilit pour que la case i soit choisie
e
e
e
a
` la prochaine insertion est la 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
suivante
0 1 2 3 4
P (0) = 1/19

P (2) = 2/19

P (3) = 1/19

P (8) = 5/19

P (9) = 1/19

P (12) = 3/19

P (13) = 1/19

P (14) = 1/19

P (16) = 2/19

P (18) = 2/19

Comme on le voit, la case 8 a la plus grande probabilit dtre occupe, ce qui accentuera le le
e e
e
regroupement des cases 47. Ce phnom`ne se rv`le rapidement quand des cls successives sont
e
e
e e
e
haches sur des entiers successifs, un cas qui se prsente en pratique avec le hachage modulo
e
e
(voir 3), quand les cls sont par exemple les valeurs successives dun compteur, ou, dans des
e
applications plus techniques, des adresses dobjets qui se suivent dans la mmoire.
e
Plusieurs solutions ont t proposes pour viter ce probl`me. La meilleure solution consiste
ee
e
e
e
a
` utiliser un double hachage. On se donne deux fonctions de hachage h : U {0, . . . , m 1} et
h : U {1, . . . , r 1} (r < m). Ensuite le sondage est eectu selon la squence h(k) + h (k),
e
e
h(k) + 2h (k), h(k) + 3h (k), etc. Les regroupements ne sont plus ` craindre essentiellement
a
parce que lincrment de la squence est lui aussi devenu une fonction uniforme de la cl. En
e
e
e
particulier en cas de collision selon h, il ny a aucune raison que les sondages se fassent selon
le mme incrment. Pour que le sondage puisse parcourir toute la table on prend h (k) > 0 et
e
e
h (k) premier avec m taille de la table. Pour ce faire on peut prendre m gal ` une puissance
e
a
(k) toujours impair, ou m premier et h (k) strictement infrieur ` m (par exemple
de deux et h
e
a
pour des cl enti`res h(k) = k mod m et h (k) = 1 + (k mod (m 2))
e
e
Dans [6, section 6.4] D. Knuth montre que, sous des hypoth`ses de distribution uniforme et
e
dindpendance des deux fonctions de hachage, le nombre moyen de sondages pour un hachage
e

76

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE

double est environ ln(1 )/ en cas de succ`s et ` 1/(1 ) en cas dchec.3 Le tableau
e
a
e
ci-dessous donne quelques valeurs numriques :
e
Facteur de charge
Succ`s
e
Echec

50 %
1.39
2.00

80 %
2.01
5.00

90 %
2.56
10.00

99 %
4.65
100.00

Comme on le voit = 80 % est un excellent compromis. Insistons sur le fait que les valeurs de
ce tableau sont indpendantes de n. Par consquent, avec un facteur charge de 80 %, il sut
e
e
en moyenne de deux essais pour retrouver un lment, mme avec dix milliards de cls ! Notons
ee
e
e
que pour le sondage linaire cet ordre de grandeur est atteint pour des tables ` moiti pleines.
e
a
e
Tandis que pour le cha
nage on peut aller jusqu` un facteur de charge denviron 4.
a
En xant ces valeurs de facteur de charge pour les trois techniques, nous galisons plus
e
ou moins les temps de recherche. Examinons alors la mmoire occupe pour n associations.
e
e
Nous constatons que le cha
nage consomme un tableau de taille n/4 plus n cellules de listes `
a
trois champs, soit 3 + n/4 + 5 n cases de mmoire en Java, en tenant compte de deux cases
e
supplmentaires par objet (voir II.4). Tandis que le sondage linaire consomme 3 + 2 n + 4 n
e
e
(tableau et paires), et le double hachage 3 + 5/4 n + 4 n (tableau et paires encore). Le gain
des deux derni`res techniques est donc minime, mais on peut coder avec moins de mmoire
e
e
(par exemple en grant deux tableaux, un pour les cls, un pour les valeurs). Lespace mmoire
e
e
e
employ devient alors respectivement 6 + 4 n et 6 + 5/2 n, soit nalement une conomie de
e
e
mmoire consquente pour le double hachage.
e
e

2.5

Tables de hachage de la libraire

Il aurait t tonnant que la libraire de Java nore pas de table de hachage, tant cette struce ee
ture est utile. La classe HashMap est une classe gnrique HashMap<K,V> paramtre par les
e e
e e
classes des cls K et des informations V (voir II.2.3). On crit donc une derni`re implmentation,
e
e
e
e
tr`s courte, de notre interface Assoc.
e
import java.util.* ;
class Lib implements Assoc {
private HashMap
<String,Integer> t ;
H() { t = new HashMap
<String,Integer> () ; }
public int get(String key) {
Integer val = t.get(key) ;
i f (val == null) {
return 0 ;
} else {
return val ;
}
}
public void put(String key, int val) { t.put(key,val) ; }
}
La table de hachage t est construite avec la taille initiale et le facteur de charge par dfaut
e
(respectivement 16 et 0.75). Les mthodes t.put et t.get sont celles des HashMap, qui se
e
`
comportent comme les ntres. A ceci pr`s que cls et informations sont obligatoirement des
o
e
e
objets et que lappel t.get(key) renvoie null quand la cl key nest pas dans la table t,
e
3

Ces valeurs proviennent dun mod`le simpli, et ont t vries exprimentalement.


e
e
ee e e
e

2. TABLE DE HACHAGE

77

fait que nous exploitons directement dans notre mthode get. On note la conversion automae
tique dun Integer en int (return val dans le corps de get) et dun int en Integer (appel
t.put(key,val) dans put).

2.6

Performance

Nous nous livrons ` une exprience consistant ` appliquer le programme Freq ` une srie de a
e
a
a
e
chiers contenant du source Java. Nous mesurons le temps cumul dexcution de la mthode count
e
e
e
(celle qui lit les mots un par un et accumule les comptes dans la table dassociation Assoc,
page 66), pour les quatre implmentations des tables dassociations.
e
La table L base sur les listes dassociations.
e
La table H base sur le hachage avec cha
e
nage (facteur de charge 4.0, taille initiale 16)
La table O base sur le hachage ouvert (sondage linaire, facteur de charge 0.5, taille
e
e
initiale 16)
La table Lib base sur les HashMap de la librairie (probablement cha
e
nage, facteur de
charge 0.75, taille initiale 16)
Toutes les tables de hachage sont redimensionnes automatiquement. Les rsultats (gure 4)
e
e

Fig. 4 temps dexcution de la mthode count


e
e
14
12
t (sec)

10
8
6
4
2
0

3
+ +
3
+ 2 2
2
3
3
+ 2
+
2
3
+
3
22
3
+
3
+ +
2 2
3
+
2
3
++
3
2 2
3
22
+ +
3
L 3
3
+
+
3
2 2
H +

+
3
2
+
3
2
O 2
3 +2
3
2 +
Lib
3 +
2

+
3
2
0
100000
200000
300000
400000
500000
600000
Nombre total de mots

font clairement appara que les listes dassociations sont hors-jeu et que les tables de hachage
tre
ont le comportement linaire attendu et mme que toutes les implmentations des tables de
e
e
e
hachages se valent, au moins dans le cadre de cet exprience.
e
En derni`re analyse, lecacit des tables de hachage repose sur luniformit de la fonction
e
e
e
de hachage des cha
nes de Java, cest donc surtout cette derni`re qui se montre performante ici.
e
En voici pour preuve (gure 5) lhistogramme des eectifs des longueurs des listes de collisions
a
` la n de lexprience H (tableau de taille m = 4096, nombre dassociations n = 16092). Cette
e
gure montre par exemple quil y a un peu plus de 800 listes de collisions de longueur trois.
En conclusion lintrt en pratique des tables de hachage est tr`s important, compte tenu des
ee
e
performances atteintes pour une dicult de ralisation particuli`rement faible (surtout si on
e
e
e
utilise la biblioth`que).
e

78

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE


Fig. 5 Rpartition des collisions
e
900
800
700
600
500
400
300
200
100
0

3
3.1

10

11

12

Choix des fonctions de hachage


En thorie
e

Rappelons quune fonction de hachage est une fonction h : U {0, . . . m 1}. Une bonne
fonction de hachage doit se rapprocher le plus possible dune rpartition uniforme. Formellement,
e
cela signie que si on se donne une probabilit P sur U et que lon choisit alatoirement chaque
e
e
cl k dans U , on ait pour tout j avec 0 j m 1,
e
P (k) =
{k|h(k)=j}

1
m

En pratique, on conna rarement la probabilit P et on se limite ` des heuristiques. En partit


e
a
culier, on veut souvent que des cls voisines donnent des valeurs de hachages tr`s distinctes.
e
e
Le cas que nous traitons ci-dessous est celui o` lunivers U est un sous-ensemble des entiers
u
naturels, car on peut toujours se ramener ` ce cas. Par exemple, si les cls sont des cha
a
e
nes de
caract`res, on peut interprter chaque caract`re comme un chire dune base B et la cha
e
e
e
ne
comme un entier crit dans cette base. Cest ` dire que la cha a0 a1 . . . an1 est lentier a0
e
a
ne
B n1 + a1 B n2 + + an1 . Si les caract`res sont quelconques on a B = 216 en Java et B = 28
e
en C ; si les caract`res des cls sont limits ` lalphabet minuscule, on peut prendre B = 26 ; etc.
e
e
e a
Nous pouvons donc revenir aux fonctions de hachage sur les entiers. Une technique courante
consiste ` prendre pour h(k) le reste de la division de k par m :
a
h(k) = k

mod m

Mais dans ce cas, certaines valeurs de m sont ` viter. Par exemple, si on prend m = 2r , h(k) ne
ae
dpendra que des r derniers bits de k. Ce qui veut dire, par exemple, que le dbut des cha
e
e
nes
longues (vues comme des entiers en base B = 2p ) ne compte plus, et conduit ` de nombreuses
a
collisions si les cls sont par exemple des adverbes (se terminant toutes par ment). Dans le mme
e
e
contexte, si on prend m = B 1, linterversion de deux caract`res passera inaperue. En eet,
e
c
comme (a B + b) (b B + a) = (a b)(B 1), on a
a B + b b B + a (mod m)
En pratique, une bonne valeur pour m est un nombre premier tel que B k a nest pas divisible
par m, pour des entiers k et a petits.

3. CHOIX DES FONCTIONS DE HACHAGE

79

Une autre technique consiste ` prendre


a
h(k) = m(Ck Ck)
o` C est une constante relle telle que 0 < C < 1. Cette mthode a lavantage de pouvoir
u
e
e
sappliquer ` toutes les valeurs de m. Il est mme conseill dans ce cas de prendre m = 2r
a
e
e
pour faciliter les calculs. Pour le choix de la constante C, Knuth recommande le nombre dor,

C = ( 5 1)/2 0, 618.

3.2

En pratique

Continuons de considrer les cha


e
nes comme des entiers en base 216 . Ces entiers sont tr`s
e
vite grands, ils ne tiennent plus dans un int d`s que la cha est de taille suprieure ` 2.
e
ne
e
a
On ne peut donc pas (pour un cot raisonnable, il existe des entiers en prcision arbitraire)
u
e
dabord transformer la cha en entier, puis calculer h. Dans le cas du hachage modulo un
ne
nombre premier, il reste possible de calculer modulo m. Mais en fait ce nest pas tr`s pratique :
e
la fonction de hachage (mthode hashCode) des cha
e
nes de Java est indpendante de la tailles
e
des tableaux internes des tables, puisquelle est dnie dans la classe des cha
e
nes et ne prend
`
pas une taille de tableau en argument. A toute cha elle associe un int qui est ensuite rduit
ne
e
en indice. Selon la documentation, lappel hashCode() de la cha a0 a1 . . . an1 renvoie a0
ne
31n1 + a1 31n2 + + an2 31 + an1 . Autrement dit le code de hashCode pourrait tre
e
celui-ci :
public int hashCode() {
int h = 0 ;
for (int k = 0 ; k < this.length ; k++)
h = 31 * h + this.charAt(k) ;
return h ;
}
Dans nos tables de hachage nous avons ensuite simplement rduit cet entier modulo la taille du
e
tableau interne.
Le calcul h = 31 * h + this .charAt(k) eectue un mlange entre une valeur courante
e
de h (qui est la valeur de hachage du prxe de la cha
e
ne) et la valeur de hachage dun caract`re
e
de la cha qui est le caract`re lui-mme, cest-`-dire son code en Unicode (voir B.3.2.3). Le
ne
e
e
a
multiplicateur 31 est un nombre premier, et ce nest pas un hasard. Lexprience nous a montr
e
e
que ce mlange simple fonctionne en pratique, sur un ensemble de cls qui sont les mots que lon
e
e
trouve dans des sources Java (voir en particulier la gure 5). Pour des cls plus gnrales cette
e
e e
faon de mlanger est critique [8], mais nous allons nous en tenir ` elle.
c
e
e
a
Car il faut parfois construire nos propres fonction de hachage, ou plus exactement rednir
e
nos propres mthodes hashCode. Notre table de hachage H appelle dabord la mthode hashCode
e
e
dune cl (pour trouver un indice dans le tableau interne, page 69), puis la mthode equals de
e
e
la mme cl (pour par exemple trouver sa place dans une liste de collision, page 67). La table
e
e
de hachage de la librairie proc`de similairement. Or, mme si tous les objets poss`dent bien
e
e
e
des mthodes hashCode et equals (comme ils poss`dent une mthode toString), les mthodes
e
e
e
e
par dfaut ne conviennent pas. En eet equals par dfaut exprime lgalit physique des cls
e
e
e
e
e
(voir B.3.1.1), tandis que hashCode renvoie essentiellement ladresse en mmoire de lobjet ! Il
e
faut rednir ces deux mthodes, comme nous avons dj` parfois redni la mthode toString
e
e
ea
e
e
(voir B.2.3). La classe String ne proc`de pas autrement, son hashCode et son equals se basent
e
non pas sur ladresse en mmoire de la cha
e
ne, mais sur le contenu des cha
nes.
Supposons donc que nous voulions nous servir de paires dentiers comme cls, cest ` dire
e
a
dobjets dune classe Pair .

80

CHAPITRE III. ASSOCIATIONS TABLES DE HACHAGE

class Pair {
int x, y ;
Pair (int x, int y) { this.x = x ; this.y = y ; }
}
Pour rednir hashCode nous utilisons tout simplement le mlangeur multiplicatif.
e
e
public int hashCode() { return x * 31 + y ; }
La mthode rednie est dclare public comme la mthode dorigine. Il importe en fait de
e
e
e
e
e
respecter toute la signature de la mthode dorigine. Or, la signature de la mthode equals des
e
e
Object (celle que nous voulons rednir) est :
e
public boolean equals(Object o)
Nous crivons donc (dans la classe Pair ) :
e
public boolean equals(Object o) {
Pair p = (Pair)o ; // Conversion de type, choue si o nest pas un Pair
e
return this.x == p.x && this.y == p.y ;
}

Pour la conversion de type, voir B.3.2.2. Evidemment, pour que les tables de hachage fonctionnent correctement il faut que lgalit selon equals (lappel p1 .equals(p2 ) renvoie true)
e
e
entra lgalit des code de hachage (p1 .hashCode() == p2 .hashCode()), ce que la documenne e
e
tation de la biblioth`que appelle le contrat gnral de hashCode.
e
e e

Chapitre IV

Arbres
Ce chapitre est consacr aux arbres, lun des concepts algorithmiques les plus importants de
e
linformatique. Les arbres servent ` reprsenter un ensemble de donnes structures hirarchia
e
e
e
e
quement. Plusieurs notions distinctes se cachent en fait sous cette terminologie : arbres libres,
arbres enracins, arbres binaires, etc. Ces dnitions sont prcises dans la section 1.
e
e
e e
Nous prsentons plusieurs applications des arbres : les arbres de dcision, les les de priorit,
e
e
e
le tri par tas et lalgorithme baptis (( union-nd )), qui sapplique dans une grande varit de
e
ee
situations. Les arbres binaires de recherche seront traits dans le chapitre suivant.
e

Dnitions
e

Pour prsenter les arbres de mani`re homog`ne, quelques termes emprunts aux graphes
e
e
e
e
sav`rent utiles. Nous prsenterons donc les graphes, puis successivement, les arbres libres, les
e
e
arbres enracins et les arbres ordonns.
e
e

1.1

Graphes

Un graphe G = (S, A) est un couple form dun ensemble de nuds S et dun ensemble A
e
darcs. Lensemble A est une partie de S S. Les nuds sont souvent reprsents par des points
e
e
dans le plan, et un arc a = (s, t) par une ligne oriente joignant s ` t. On dit que larc a part
e
a
de s et va ` t. Un chemin de s ` t est une suite (s = s0 , . . . , sn = t) de nuds tels que, pour
a
a
1 i n, (si1 , si ) soit un arc. Le nud s0 est lorigine du chemin et le nud sn son extrmit.
e e
Lentier n est la longueur du chemin. Cest un entier positif ou nul. Un circuit est un chemin de
longueur non nulle dont lorigine co
ncide avec lextrmit.
e e

`
Fig. 1 A gauche un graphe, ` droite un graphe non orient.
a
e
` oe
A ct de ces graphes, appels aussi graphes orients ((( digraph )) en anglais, pour (( directed
e
e
graph ))), il existe la variante des graphes non orients. Au lieu de couples de nuds, on consid`re
e
e
81

82

CHAPITRE IV. ARBRES

des paires {s, t} de nuds. Un graphe non orient est donn par un ensemble de ces paires,
e
e
appeles artes. Les concepts de chemin et circuit se transposent sans peine ` ce contexte.
e
e
a
Un chemin est simple si tous ses nuds sont distincts. Un graphe est connexe si deux quelconques de ses nuds sont relis par un chemin.
e

1.2

Arbres libres

Dans la suite de chapitre, nous prsentons des familles darbres de plus en plus contraints.
e
La famille la plus gnrale est forme des arbres libres. Un arbre libre est un graphe non orient
e e
e
e
non vide, connexe et sans circuit. La proposition suivante est laisse en exercice.
e

Fig. 2 Un arbre (( libre )).


Proposition 1 Soit G = (S, A) un graphe non orient non vide. Les conditions suivantes sont
e
quivalentes :
e
(1) G est un arbre libre,
(2) Deux nuds quelconques de S sont connects par un chemin simple unique,
e
(3) G est connexe, mais ne lest plus si lon retire une arte quelconque,
e
(4) G est sans circuit, mais ne lest plus si lon ajoute une arte quelconque,
e
(5) G est connexe, et Card(A) = Card(S) 1,
(6) G est sans circuit, et Card(A) = Card(S) 1.

1.3

Arbres enracins
e

Un arbre enracin ou arbre ((( rooted tree )) en anglais) est un arbre libre muni dun nud
e
distingu, appel sa racine. Soit T un arbre de racine r. Pour tout nud x, il existe un chemin
e
e
simple unique de r ` x. Tout nud y sur ce chemin est un anctre de x, et x est un descendant
a
e
de y. Le sous-arbre de racine x est larbre contenant tous les descendants de x. Lavant-dernier
nud y sur lunique chemin reliant r a x est le parent (ou le p`re ou la m`re) de x, et x est un
`
e
e
enfant (ou un ls ou une lle) de y. Larit dun nud est le nombre de ses enfants. Un nud
e
sans enfant est une feuille, un nud darit strictement positive est appel nud interne. La
e
e
hauteur dun arbre T est la longueur maximale dun chemin reliant sa racine ` une feuille. Un
a
arbre rduit ` un seul nud est de hauteur 0.
e
a
1
2
5

3
6

4
9

Fig. 3 Un arbre (( enracin )).


e

2. UNION-FIND, OU GESTION DES PARTITIONS

83

Les arbres admettent aussi une dnition rcursive. Un arbre sur un ensemble ni de nuds
e
e
est un couple form dun nud particulier, appel sa racine, et dune partition des nuds restants
e
e
en un ensemble darbres. Par exemple, larbre de la gure 3 correspond ` la dnition
a
e
T = (1, {(2, {(5), (6)}), (3, {(7), (8), (9)}), (4)})
Cette dnition rcursive est utile dans les preuves et dans la programmation. On montre ainsi
e
e
facilement que si tout nud interne dun arbre est darit au moins 2, alors larbre a strictement
e
plus de feuilles que de nuds internes.
Une fort est un ensemble darbres.
e

1.4

Arbres ordonns
e

Un arbre ordonn est un arbre dans lequel lensemble des enfants de chaque nud est totae
lement ordonn.
e

1
1.1
1.2.1

2
1.2

2.1

2.2

3
2.3

1.2.2

Fig. 4 Larbre ordonn de la table des mati`res dun livre.


e
e
Par exemple, un livre, structur en chapitres, sections, etc., se prsente comme un arbre
e
e
ordonn (voir gure 4). Les enfants dun nud dun arbre ordonn sont souvent reprsents,
e
e
e
e
dans un programme, par une liste attache au nud. Un autre solution est dassocier, ` chaque
e
a
nud, un tableau de ls. Cest une solution moins souple si le nombre de ls est destin `
e a
changer. Enn, on verra plus loin une autre reprsentation au moyen darbres binaires.
e

Union-Find, ou gestion des partitions

Comme premier exemple de lemploi des arbres et des forts, nous considrons un probl`me
e
e
e
cl`bre, et ` ce jour pas encore enti`rement rsolu, appel le probl`me Union-Find. Rappelons
ee
a
e
e
e
e
quune partition dun ensemble E est un ensemble de parties non vides de E, deux ` deux
a

disjointes et dont la runion est E. Etant donn une partition de lensemble {0, . . . , n 1}, on
e
e
veut rsoudre les deux probl`mes que voici :
e
e
trouver la classe dun lment (nd )
ee
faire lunion de deux classes (union).
Nous donnons dabord une solution du probl`me Union-Find, puis nous donnerons quelques
e
exemples dapplication.

2.1

Une solution du probl`me


e

En gnral, on part dune partition o` chaque classe est rduite ` un singleton, puis on traite
e e
u
e
a
une suite de requtes de lun des deux types ci-dessus.
e
Avant de traiter ce probl`me, il faut imaginer la faon de reprsenter une partition. Une
e
c
e
premi`re solution consiste ` reprsenter la partition par un tableau classe. Chaque classe est
e
a
e

84

CHAPITRE IV. ARBRES

identie par un entier par exemple, et classe[x] contient le numro de la classe de llment x
e
e
ee
(cf. gure 5).
x

classe[x]

Fig. 5 Tableau associ ` la partition {{2, 5, 8}, {0, 6}, {1}, {3, 4, 7, 9}}.
ea
Trouver la classe dun lment se fait en temps constant, mais fusionner deux classes prend
ee
un temps O(n), puisquil faut parcourir tout le tableau pour reprer les lments dont il faut
e
ee
changer la classe. Une deuxi`me solution, que nous dtaillons maintenant, consiste ` choisir un
e
e
a
reprsentant dans chaque classe. Fusionner deux classes revient alors ` changer de reprsentant
e
a
e
pour les lments de la classe fusionne. Il appara avantageux de reprsenter la partition par
ee
e
t
e
une fort. Chaque classe de la partition constitue un arbre de cette fort. La racine de larbre
e
e
est le reprsentant de sa classe. La gure 6 montre la fort associe ` une partition.
e
e
e a

Fig. 6 Fort associe ` la partition {{2, 5, 8}, {0, 6}, {1}, {3, 4, 7, 9}}.
e
e a
Une fort est reprsente par un tableau dentiers pere (cf. Figure 7). Chaque nud est
e
e
e
reprsent par un entier, et lentier pere[x] est le p`re du nud x. Une racine r na pas de
e
e
e
parent. On convient que, dans ce cas, pere[r] = r.
x
pere[x]

Fig. 7 Tableau associ ` la fort de la gure 6.


ea
e
On suppose donc dni un tableau
e
int[] pere = new int[n];
Ce tableau est initialis ` lidentit par
ea
e
static void initialisation()
{
for (int i = 0; i < pere.length ; i++)
pere[i] = i;
}
Chercher le reprsentant de la classe contenant un lment donn revient ` trouver la racine de
e
ee
e
a
larbre contenant un nud donn. Ceci se fait par la mthode suivante :
e
e
static int trouver(int x)

2. UNION-FIND, OU GESTION DES PARTITIONS

85

{
while (x != pere[x])
x = pere[x];
return x;
}
Lunion de deux arbres se ralise en ajoutant la racine de lun des deux arbres comme nouveau
e
ls ` la racine de lautre :
a
static void union(int x, int y)
{
int r = trouver(x);
int s = trouver(y);
if (r != s)
pere[r] = s;
}

Fig. 8 La fort de la gure 6 apr`s lunion pondre de 5 et 0.


e
e
ee
Il nest pas dicile de voir que chacune de ces deux mthodes est de complexit O(h), o` h
e
e
u
est la hauteur larbre (la plus grande des hauteurs des deux arbres). En fait, on peut amliorer
e
lecacit de lalgorithme par la r`gle suivante (voir gure 8) :
e
e
R`gle. Lors de lunion de deux arbres, la racine de larbre de moindre taille devient ls de la
e
racine de larbre de plus grande taille.
Pour mettre en uvre cette stratgie, on utilise un tableau supplmentaire qui mmorise la
e
e
e
taille des arbres, qui doit tre initialis ` 1 :
e
ea
int[] taille = new int[n];
La nouvelle mthode dunion scrit alors :
e
e
static void unionPondre(int x, int y)
e e
{
int r = trouver(x);
int s = trouver(y);
if (r == s)
return;
if (taille[r] > taille[s])
{
pere[s] = r;
taille[r] += taille[s];
}
else
{

86

CHAPITRE IV. ARBRES


pere[r] = s;
taille[s] += taille[r];
}
}

Lintrt de cette mthode vient de lobservation suivante :


ee
e
Lemme 2 La hauteur dun arbre ` n nuds cr par union pondre est au plus 1 + log2 n.
a
ee
ee
Preuve. Par rcurrence sur n. Pour n = 1, il ny a rien ` prouver. Si un arbre est obtenu par
e
a
union pondre dun arbre ` m nuds et dun arbre ` n m nuds, avec 1
ee
a
a
m
n/2, sa
hauteur est majore par
e
max(1 + log2 (n m), 2 + log2 m) .
Comme log2 m

log2 (n/2) = log2 n 1, cette valeur est majore par 1 + log2 n.


e
1

10

11

10

11

Fig. 9 (( Trouver )) 10 avec compression du chemin.


Une deuxi`me stratgie, applique cette fois-ci lors de la mthode trouver permet une nouvelle
e
e
e
e
amlioration considrable de la complexit. Elle est base sur la r`gle de compression de chemins
e
e
e
e
e
suivante :
R`gle. Apr`s tre remont du nud x ` sa racine r, on refait le parcours en faisant de chaque
e
e e
e
a
nud rencontr un ls de r.
e
La gure 9 montre la transformation dun arbre par une compression de chemin. Chacun des
nuds 10 et 4 devient ls de 1. Limplantation de cette r`gle se fait simplement.
e
static int trouverAvecCompression(int x)
{
int r = trouver(x);
while (x != r)
{
int y = pere[x];
pere[x] = r;
x = y;
}
return r;
}
Lensemble des deux stratgies permet dobtenir une complexit presque linaire.
e
e
e

3. ARBRES BINAIRES

87

Thor`me 3 (Tarjan) Avec lunion pondre et la compression des chemins, une suite de n 1
e e
ee
(( unions )) et de m (( trouver )) (m n) se ralise en temps O(n + m(n, m)), o` est linverse
e
u
dune sorte de fonction dAckermann.
En fait, on a (n, m) 2 pour m n et n < 265536 et par consquent, lalgorithme prcdent
e
e e
se comporte, dun point de vue pratique, comme un algorithme linaire en n + m. Pourtant,
e
Tarjan a montr quil nest pas linaire et on ne conna pas ` ce jour dalgorithme linaire.
e
e
t
a
e

2.2

Applications de lalgorithme Union-Find

Un premier exemple est la construction dun arbre couvrant un graphe donn. Au dpart,
e
e
chaque nud constitue ` lui seul un arbre. On prend ensuite les artes, et on fusionne les arbres
a
e
contenant les extrmits de larte si ces extrmits appartiennent ` des arbres dirents.
e
e
e
e
e
a
e
Un second exemple concerne les probl`mes de connexion dans un rseau. Voici un exemple de
e
e
tel probl`me. Huit ordinateurs sont connects ` travers un rseau. Lordinateur 1 est connect
e
e a
e
e
au 3, le 2 au 3, le 5 au 4, le 6 au 3, le 7 au 5, le 1 au 6 et le 7 au 8. Est-ce que les ordinateurs
4 et 6 peuvent communiquer ` travers le rseau ? Certes, il nest pas tr`s dicile de rsoudre
a
e
e
e
ce probl`me ` la main, mais imaginez la mme question pour un rseau dont la taille serait
e
a
e
e
de lordre de plusieurs millions. Comment rsoudre ce probl`me ecacement ? Cest la mme
e
e
e
solution que prcdemment ! On consid`re le rseau comme un graphe dont les nuds sont les
e e
e
e
ordinateurs. Au dpart, chaque nud constitue ` lui seul un arbre. On prend ensuite les artes
e
a
e
(i.e. les connexions entre deux ordinateurs), et on fusionne les arbres contenant les extrmits
e
e
de larte si ces extrmits appartiennent ` des arbres dirents.
e
e
e
a
e

Arbres binaires

La notion darbre binaire est assez dirente des dnitions prcdentes. Un arbre binaire
e
e
e e
sur un ensemble ni de nuds est soit vide, soit lunion disjointe dun nud appel sa racine,
e
dun arbre binaire appel sous-arbre gauche, et dun arbre binaire appel sous-arbre droit. Il est
e
e
utile de reprsenter un arbre binaire non vide sous la forme dun triplet A = (Ag , r, Ad ).
e
1

1
2

Fig. 10 Deux arbres binaires dirents.


e
Par exemple, larbre binaire sur la gauche de la gure 10 est (, 1, ((, 3, ), 2, )), alors que
larbre sur la droite de la gure 10 est ((, 2, (, 3, )), 1, ). Cet exemple montre quun arbre
binaire nest pas simplement un arbre ordonn dont tous les nuds sont darit au plus 2.
e
e
La distance dun nud x ` la racine ou la profondeur de x est gale ` la longueur du chemin
a
e
a
de la racine ` x. La hauteur dun arbre binaire est gale ` la plus grande des distances des feuilles
a
e
a
a
` la racine.
Proposition 4 Soit A un arbre binaire ` n nuds, de hauteur h. Alors h + 1
a
Preuve. Il y a au plus 2i nuds ` distance i, donc n
a

2h+1 1.

log2 (n + 1).

88

CHAPITRE IV. ARBRES


Un arbre binaire est complet si tout nud a 0 ou 2 ls.

Proposition 5 Dans un arbre binaire complet, le nombre de feuilles est gal au nombre de
e
nuds internes, plus 1.
Preuve. Notons, pour simplier, f (A) le nombre de feuilles et n(A) le nombre de nuds internes
de larbre binaire complet A. Il sagit de montrer que f (A) = n(A) + 1.
Le rsultat est vrai pour larbre binaire de hauteur 0. Considrons un arbre binaire complet
e
e
A = (Ag , r, Ad ). Les feuilles de A sont celles de Ag et de Ad et donc f (A) = f (Ag ) + f (Ad ). Les
nuds internes de A sont ceux de Ag , ceux de Ad et la racine, et donc n(A) = n(Ag ) + n(Ad ) + 1.
Comme Ag et Ad sont des arbres complets de hauteur infrieure ` celle de A, la rcurrence
e
a
e
sapplique et on a f (Ag ) = n(Ag ) + 1 et f (Ad ) = n(Ad ) + 1. On obtient nalement f (A) =
f (Ag ) + f (Ad ) = (n(Ag ) + 1) + (n(Ad ) + 1) = n(A) + 1.
On montre aussi que, dans un arbre binaire complet, il y a un nombre pair de nuds `
a
chaque niveau, sauf au niveau de la racine.

3.1

Compter les arbres binaires

La gure 11 montre les arbres binaires ayant 1, 2, 3 et 4 nuds.

Fig. 11 Les premiers arbres binaires.


Notons bn le nombre darbres ` n nuds. On a donc b0 = b1 = 1, b2 = 2, b3 = 5, b4 = 14.
a
Comme tout arbre A non vide scrit de mani`re unique sous forme dun triplet (Ag , r, Ad ), on
e
e
a pour n 1 la formule
n1

bn =

bi bni1
i=0

La srie gnratrice B(x) =


e
e e

n
n 0 bn x

vrie donc lquation


e
e

xB 2 (x) B(x) + 1 = 0 .
Comme les bn sont positifs, la rsolution de cette quation donne
e
e
bn =

1
2n
n+1 n

(2n)!
n!(n + 1)!

Les nombres bn sont connus comme les nombres de Catalan. Lexpression donne aussi bn
1/2 4n n3/2 + O(4n n5/2 ).

3.2

Arbres binaires et mots

Nous prsentons quelques concepts sur les mots qui servent ` plusieurs reprises. Dabord, ils
e
a
mettent en vidence des liens entre les parcours darbres binaires et certains ordres. Ensuite, ils
e
seront employs dans des algorithmes de compression.
e

3. ARBRES BINAIRES
3.2.1

89

Mots

Un alphabet est un ensemble de lettres, comme {0, 1} ou {a, b, c, d, r}. Un mot est une suite de
lettres, comme 0110 ou abracadabra. La longueur dun mot u, note |u|, est le nombre de lettres
e
de u : ainsi, |0110| = 4 et |abracadabra| = 11. Le mot vide, de longueur 0, est not . Etant
e
donns deux mots, le mot obtenu par concatnation est le mot form des deux mots juxtaposs.
e
e
e
e
Le produit de concatnation est not comme un produit. Si u = abra et v = cadabra, alors
e
e
uv = abracadabra. Un mot p est prxe (propre) dun mot u sil existe un mot v (non vide)
e
tel que u = pv. Ainsi, , abr, abrac sont des prxes propres de abraca. Un ensemble de mots
e
P est prxiel si tout prxe dun mot de P est lui-mme dans P . Par exemple, les ensembles
e
e
e
{, 1, 10, 11} et {, 0, 00, 01, 000, 001} sont prxiels.
e
3.2.2

Ordres sur les mots

Un ordre total sur lalphabet stend en un ordre total sur lensemble des mots de multiples
e
mani`res. Nous considrons deux ordres, lordre lexicographique et lordre des mots croiss ((( rae
e
e
dix order )) ou (( shortlex )) en anglais).
Lordre lexicographique, ou ordre du dictionnaire, est dni par u <lex v si seulement si u
e
, v = pbv , o` p est un mot, a et b sont des
est prxe de v ou u et v peuvent scrire u = pau
e
e
u
lettres, et a < b. Lordre des mots croiss est dni par u <mc v si et seulement si |u| < |v| ou
e
e
|u| = |v| et u <lex v. Par exemple, on a
bar <mc car <mc barda <mc radar <mc abracadabra
3.2.3

Codage des arbres binaires

Chaque arte (p, f ) dun arbre A binaire est tiquete par 0 si f est ls gauche de p, et par
e
e
e
1 si f est ls droit. Ltiquette du chemin qui m`ne de la racine a un nud est le mot form des
e
e
`
e
tiquettes de ses artes. Le code de larbre A est lensemble des tiquettes des chemins issus de la
e
e
e
racine. Cet ensemble est clairement prxiel, et rciproquement, tout ensemble ni prxiel de
e
e
e
mots forms de 0 et 1 est le code dun arbre binaire. La correspondance est, de plus, bijective.
e

0
0

1
1

1
1

Fig. 12 Le code de larbre est {, 0, 1, 00, 01, 10, 11, 010, 101, 110}.
Le code c(A) dun arbre binaire A se dnit dailleurs simplement par rcurrence : on a
e
e
c() = {} et si A = (Ag , r, Ad ), alors c(A) = 0c(Ag ) {} 1c(Ad ).

3.3

Parcours darbre

Un parcours darbre est une numration des nuds de larbre. Chaque parcours dnit un
e
e
e
ordre sur les nuds, dtermin par leur ordre dapparition dans cette numration.
e
e
e
e
On distingue les parcours de gauche ` droite, et les parcours de droite ` gauche. Dans un
a
a
parcours de gauche ` droite, le ls gauche dun nud prc`de le ls droit (et vice-versa pour un
a
e e
parcours de droite ` gauche). Ensuite, on distingue les parcours en profondeur et en largeur.
a

90

CHAPITRE IV. ARBRES

Le parcours en largeur num`re les nuds niveau par niveau. Ainsi, le parcours en largeur
e
e
de larbre 13 donne la squence a, b, c, d, e, f, g, h, i, k. On remarque que les codes des nuds
e
correspondants, , 0, 1, 00, 01, 10, 11, 010, 101, 110, sont en ordre croissant pour lordre des
mots croiss. Cest en fait une r`gle gnrale.
e
e
e e
R`gle. Lordre du parcours en largeur correspond ` lordre des mots croiss sur le code de
e
a
e
larbre.
On dnit trois parcours en profondeur privilgis qui sont
e
e e
le parcours prxe : tout nud est suivi des nuds de son sous-arbre gauche puis des
e
nuds de son sous-arbre droit, en abrg NGD (Nud, Gauche, Droite).
e e
le parcours inxe : tout nud est prcd des nuds de son sous-arbre gauche et suivi des
e e e
nuds de son sous-arbre droit, en abrg GND (Gauche, Nud, Droite).
e e
le parcours suxe, ou postxe : tout nud est prcd des nuds de son sous-arbre gauche
e e e
puis des nuds de son sous-arbre droit, en abrg GDN (Gauche, Droite, Nud).
e e
Les ordres correspondant sont appels ordres prxe, inxe et suxe. Considrons larbre de la
e
e
e
gure 13.
a
0

Fig. 13 Un arbre binaire. Les nuds sont nomms par des lettres.
e
Le parcours prxe donne les nuds dans lordre a, b, d, e, h, c, f, i, g, k, le parcours inxe
e
donne la suite d, b, h, e, a, f, i, c, k, g et le parcours suxe donne d, h, e, b, i, f, k, g, c, a. De mani`re
e
formelle, les parcours prxe (inxe, suxe) sont dnis comme suit. Si A est larbre vide, alors
e
e
pref(A) = inf(A) = su(A) = ; si A = (Ag , r, Ad ), et e(r) est le nom de r, alors
pref(A) = e(r)pref(Ag )pref(Ad )
inf(A) = inf(Ag )e(r)inf(Ad )
su(A) = su(Ag )su(Ad )e(r)

R`gle. Le parcours prxe dun arbre correspond ` lordre lexicographique sur le code de larbre.
e
e
a
Le parcours suxe correspond ` loppos de lordre lexicographique si lon convient que 1 < 0.
a
e
Quon se rassure, il y a aussi une interprtation pour le parcours inxe, mais elle est un peu
e
`
plus astucieuse ! A chaque nud x de larbre, on associe un nombre form du code du chemin
e
menant ` x, suivi de 1. Ce code complt est interprt comme la partie fractionnaire dun
a
ee
ee
nombre entre 0 et 1, crit en binaire. Pour larbre de la gure 13, les nombres obtenus sont
e

3. ARBRES BINAIRES

91

donns dans la table suivante


e
a
.1 = 1/2
b
.01 = 1/4
c
.11 = 3/4
d .001 = 1/8
e .011 = 3/8
f
.101 = 5/8
g .111 = 7/8
h .0101 = 5/16
i .1011 = 11/16
k .1101 = 13/16
Lordre induit sur les mots est appel lordre fractionnaire.
e
R`gle. Lordre inxe correspond ` lordre fractionnaire sur le code de larbre.
e
a
La programmation de ces parcours sera donne au chapitre V.
e

3.4

Une borne infrieure pour les tris par comparaisons


e

Voici une application surprenante des arbres ` lanalyse de complexit. Il existe de nombreux
a
e
algorithmes de tri, certains dont la complexit dans le pire des cas est en O(n2 ) comme les tris
e
par slection, par insertion ou ` bulles, dautres en O(n3/2 ) comme le tri Shell, et dautres en
e
a
O(n log n) comme le tri fusion ou le tri par tas, que nous verrons page 100. On peut se demander
sil est possible de trouver un algorithme de tri de complexit infrieure dans le pire des cas.
e e
Avant de rsoudre cette question, il faut bien prciser le mod`le de calcul que lon consid`re.
e
e
e
e
Un tri par comparaison est un algorithme qui trie en nutilisant que des comparaisons. On peut
supposer que les lments ` trier sont deux-`-deux distincts. Le mod`le utilis pour reprsenter
ee
a
a
e
e
e
un calcul est un arbre de dcision. Chaque comparaison entre lments dune squence ` trier
e
ee
e
a
est reprsente par un nud interne de larbre. Chaque nud pose une question. Le ls gauche
e
e
correspond ` une rponse ngative, le ls droit ` une rponse positive (gure 14).
a
e
e
a
e

a1 > a2
a2 > a3
(1, 2, 3)

a1 > a3

a1 > a3

(1, 3, 2)

(2, 1, 3)

(3, 1, 2)

a2 > a3

(2, 3, 1)

(3, 2, 1)

Fig. 14 Exemple darbre de dcision pour le tri.


e
Les feuilles reprsentent les permutations des lments ` eectuer pour obtenir la squence
e
ee
a
e
trie. Le nombre de comparaisons ` eectuer pour dterminer cette permutation est gale ` la
e
a
e
e
a
longueur du chemin de la racine ` la feuille.
a
Nous prouvons ici :
Thor`me 6 Tout algorithme de tri par comparaison eectue (n log n) comparaisons dans le
e e
pire des cas pour trier une suite de n lments.
ee

92

CHAPITRE IV. ARBRES

Preuve. Tout arbre de dcision pour trier n lments a n! feuilles, reprsentant toutes les pere
ee
e
mutations possibles. La hauteur de larbre est donc minore par log(n!). Or log(n!) = O(n log n)
e
par la formule de Stirling.
Deux prcisions pour clore cette parenth`se sur les tris. Tout dabord, le rsultat prcdent
e
e
e
e e
nest plus garanti si lon change de mod`le. Supposons par exemple que lon veuille classer les
e
notes (des entiers entre 0 et 20) provenant dun paquet de 400 copies. La faon la plus simple
c
et la plus ecace consiste ` utiliser un tableau T de taille 21, dont chaque entre T[i] sert
a
e
a
` compter les notes gales ` i. Il sut alors de lire les notes une par une et dincrmenter
e
a
e
le compteur correspondant. Une fois ce travail accompli, le tri est termin : il y a T[0] notes
e
gales ` 0, suivi de T[1] notes gales ` 1, etc. Cet algorithme est manifestement linaire et
e
a
e
a
e
ne fait aucune comparaison ! Pourtant, il ne contredit pas notre rsultat. Nous avons en eet
e
utilis implicitement une information supplmentaire : toutes les valeurs ` trier appartiennent `
e
e
a
a
lintervalle [0, 20]. Cet exemple montre quil faut bien rchir aux conditions particuli`res avant
e e
e
de choisir un algorithme.
Seconde remarque, on constate exprimentalement que lalgorithme de tri rapide (QuickSort),
e
dont la complexit dans le pire des cas est en O(n2 ), est le plus ecace en pratique. Comment
e
est-ce possible ? Tout simplement parce que notre rsultat ne concerne que la complexit dans
e
e
le pire des cas. Or QuickSort est un algorithme en O(n log n) en moyenne.

4
4.1

Arbres de syntaxe abstraite


Les expressions sont des arbres

Considrons une dnition des expressions arithmtiques avec un il neuf. Une expression
e
e
e
arithmtique e est :
e
un entier,
ou bien une opration e1 op e2 , o` e1 et e2 sont des expressions arithmtiques et op est un
e
u
e
oprateur (+, -, * et /).
e
Lil neuf ne voit pas cette dnition comme celle de lcriture usuelle (notation inxe) des
e
e
expressions, et dailleurs il manque les parenth`ses. Il voit une dnition inductive, lensemble
e
e
des expressions est solution de cette quation rcursive :
e
e
E = Z (E, +, E) (E, -, E) (E, *, E) (E, /, E)
Cette dnition inductive est une dnition darbre, les expressions sont des feuilles qui contiennent
e
e
un entier ou des nuds internes ` deux ls. Voir une expression comme un arbre vite toutes les
a
e
ambigu es de la notation inxe. Par exemple, les deux arbres de la gure 15 disent clairement
t
quels sont les arguments des oprations + et * dans les deux cas. Alors quen notation inxe,
e
Fig. 15 Deux arbres de syntaxe abstraite
+

*
*

1
2

+
3

3
2

pour bien se faire comprendre, il faut crire 1+(2*3) et (1+2)*3.


e

4. ARBRES DE SYNTAXE ABSTRAITE

93

D`s quun programme doit faire des choses un tant soit peu compliques avec les expressions
e
e
arithmtiques, il faut reprsenter ces expressions par des arbres de syntaxe abstraite. Le terme
e
e
(( abstraite )) se justie par opposition a la syntaxe (( concr`te )) qui est lcriture des expressions,
`
e
e
cest-`-dire ici la notation inxe. La production des arbres de syntaxes abstraite ` partir de la
a
a
syntaxe concr`te est lanalyse grammaticale (parsing), une question cruciale qui est tudie dans
e
e
e
le cours suivant INF 431.

4.2

Implmentation des arbres de syntaxe abstraite


e

Ecrivons une classe Exp des cellules darbre des expressions. Nous devons principalement
distinguer cinq sortes de nuds. Les entiers, qui sont des feuilles, et les quatre oprations, qui
e
ont deux ls. La technique dimplmentation la plus simple est de raliser tous ces nuds par
e
e
e
des objets dune seule classe Exp qui ont tous les champs ncessaires, plus un champ tag qui
indique la nature du nud.1 Le champ tag contient un entier cens tre lune de cinq constantes
ee
conventionnelles.
class Exp {
final static int INT=0, ADD=1, SUB=2, MUL=3, DIV=4 ;
int tag ;
// Utilis si tag == INT
e
int asInt ;
// Utiliss si tag {ADD, SUB, MUL, DIV}
e
Exp e1, e2 ;
Exp(int i) { tag = INT ; asInt = i ; }
Exp(Exp e1, int op, Exp e2) {
tag = op ; this.e1 = e1 ; this.e2 = e2 ;
}
}
Ainsi pour construire larbre de gauche de la gure 15, on crit :
e
new Exp
(new Exp(1),
ADD,
new Exp (new Exp(2), MUL, new Exp(3)))
Cest non seulement assez lourd, mais aussi source derreurs. On atteint ici la limite de ce
quautorise la surcharge des constructeurs. Il est plus commode de dnir cinq mthodes statiques
e
e
pour construire les divers nuds.
static Exp mkInt(int i) { return new Exp (i) ; }
static Exp add(Exp e1, Exp e2) { return new Exp (e1, ADD, e2) ; }
.
.
.
static Exp div(Exp e1, Exp e2) { return new Exp (e1, DIV, e2) ; }
Et lexpression dj` vue, se construit par :
ea
add(mkInt(1), mul(mkInt(2), mkInt(3)))
Ce qui est plus concis, sinon plus clair.
Un exemple dopration (( complique )) sur les expressions arithmtiques est le calcul de
e
e
e
leur valeur. Lopration nest complique que si nous essayons de leectuer directement sur les
e
e
e
notations inxes, car sur un arbre Exp cest tr`s facile.
1

Une technique plus lgante ` base dhritage des objets est possible.
ee
a
e

94

CHAPITRE IV. ARBRES


static int calc(Exp e) {
switch (e.tag) {
case INT: return e.asInt ;
case ADD: return calc(e.e1) + calc(e.e2) ;
case SUB: return calc(e.e1) - calc(e.e2) ;
case MUL: return calc(e.e1) * calc(e.e2) ;
case DIV: return calc(e.e1) / calc(e.e2) ;
}
throw new Error ("calc : arbre Exp incorrect") ;
}

Linstruction throw nale est ncessaire, car le compilateur na pas de moyen de savoir que le
e
champ tag contient obligatoirement lune des cinq constantes conventionnelles. En son absence,
le programme est rejet par le compilateur. Pour satisfaire le compilateur, on aurait aussi pu
e
renvoyer une valeur (( bidon )) par return 0, mais cest nettement moins conseill. Une erreur
e
est une erreur, en cas darbre incorrect, mieux vaut tout arrter que de faire semblant de rien.
e
Dans cet exemple typique, il faut surtout remarquer le lien tr`s fort entre la dnition
e
e
inductive de larbre et la structure rcursive de la mthode. La programmation sur les arbres de
e
e
syntaxe abstraite est naturellement rcursive.
e

4.3

Traduction de la notation postxe vers la notation inxe

Nous avons dj` trait cette question de faon incompl`te, en ne produisant que des notations
ea
e
c
e
inxes compl`tement parenthses (exercice II.2). Nous pouvons maintenant faire mieux.
e
ee
Lide est dabord dinterprter la notation postxe comme un arbre, puis dacher cet arbre,
e
e
en tenant compte des r`gles usuelles qui permettent de ne pas mettre toutes les parenth`ses.
e
e
Pour la premi`re opration il ne faut se poser aucune question, nous reprenons le calcul des
e
e
expressions donnes en notation postxe (voir II.1.2), en construisant un arbre au lieu de calculer
e
une valeur. Nous avons donc besoin dune pile darbres, ce qui est facile avec la classe des piles
de la biblioth`que (voir II.2.3).
e
static Exp postfixToExp(String [] arg) {
Stack<Exp> stack = new Stack<Exp> () ;
for (int k = 0 ; k < arg.length ; k++) {
Exp e1, e2 ;
String cmd = arg[k] ;
i f (cmd.equals("+")) {
e2 = stack.pop() ; e1 = stack.pop() ;
stack.push(add(e1,e2)) ;
} else i f (cmd.equals("-")) {
e2 = stack.pop() ; e1 = stack.pop() ;
stack.push(sub(e1,e2)) ;
} else i f (cmd.equals("*")) {
e2 = stack.pop() ; e1 = stack.pop() ;
stack.push(mul(e1,e2)) ;
} else i f (cmd.equals("/")) {
e2 = stack.pop() ; e1 = stack.pop() ;
stack.push(div(e1,e2)) ;
} else {
stack.push(mkInt(Integer.parseInt(arg[k]))) ;
}
}
return stack.pop() ;
}

4. ARBRES DE SYNTAXE ABSTRAITE

95

Examinons la question dacher un arbre Exp sous forme inxe sans abuser des parenth`ses.
e
Tout dabord, les parenth`ses autour dun entier ne sont jamais utiles. Ensuite, on distingue deux
e
classes doprateurs, les additifs (+ et -) et les multiplicatifs (* et /), les oprateurs dune classe
e
e
donne ont le mme comportement vis ` vis du parenthsage. Il y a cinq positions possibles : au
e
e
a
e
sommet de larbre, et ` gauche ou ` droite dun oprateur additif ou multiplicatif. On examine
a
a
e
ensuite lventuel parenthsage dun oprateur.
e
e
e
Lapplication des oprateurs additifs doit tre parenthse quand elle appara comme see
e
ee
t
cond argument dun oprateur additif (1-2+3 sinterpr`te comme (1-2)+3, il faut donc pae
e
renthser 1-(2+3)), ou comme argument dun oprateur multiplicatif (considrer (1+2)*3
e
e
e
et 1*(2+3)).
Lapplication des oprateurs multiplicatifs doit tre parenthse ` droite des oprateurs
e
e
ee a
e
multiplicatifs (mme raisonnement que pour les additifs).
e
Ceci nous conduit ` regrouper les positions possible en trois classes
a
(1) Sommet de larbre et ` gauche des additifs : ne rien parenthser.
a
e
`
(2) A droite des additifs et ` gauche des multiplicatifs : ne parenthser que les additifs.
a
e
` droite des multiplicatifs : parenthser tous les oprateurs.
(3) A
e
e
On identie les trois classes par 1, 2 et 3. On voit alors que les additifs sont ` parenthser
a
e
pour les classes strictement suprieures ` 1, et les multiplicatifs pour les classes strictement
e
a
suprieures ` 2. Ce qui conduit directement ` la mthode suivante qui prend en dernier argument
e
a
a
e
un entier lvl qui rend compte de la position de larbre e ` acher dans la sortie out.
a
static void expToInfix(PrintWriter out, Exp e, int lvl) {
switch (e.tag) {
case INT:
out.print(e.asInt) ; return ;
case ADD: case SUB:
i f (lvl > 1) out.print(() ;
expToInfix(out, e.e1, 1) ;
out.print(e.tag == ADD ? + : -) ;
expToInfix(out, e.e2, 2) ;
i f (lvl > 1) out.print()) ;
return ;
case MUL: case DIV:
i f (lvl > 2) out.print(() ;
expToInfix(out, e.e1, 2) ;
out.print(e.tag == MUL ? * : /) ;
expToInfix(out, e.e2, 3) ;
i f (lvl > 2) out.print()) ;
return ;
}
throw new Error ("expToInfix : arbre Exp incorrect") ;
}
La mthode expToInfix mlange rcursion et achage. Cela ne pose pas de dicult partie
e
e
e
culi`re : pour acher une opration il faut dabord acher le premier argument (rcursion) puis
e
e
e
loprateur et enn le second argument (rcursion encore).
e
e
La sortie est un PrintWriter qui poss`de une mthode print exactement comme System.out
e
e
mais est bien plus ecace (voir B.5.5.1). Le code utilise une particularit de linstruction switch :
e
on peut grouper les cas (ici des additifs et des multiplicatifs). Pour acher loprateur, on a ree
cours ` lexpression conditionnelle (voir B.7.2). Par exemple e.tag == ADD ? + : - vaut
a
+ si e.tag est gal ` ADD et - autrement et ici (( autrement )) signie ncessairement que
e
a
e
e.tag est gal ` SUB puisque nous sommes dans un cas regroup du switch ne concernant que
e
a
e
ADD et SUB.

96

CHAPITRE IV. ARBRES

Voici nalement la mthode main de la classe Exp qui appelle lachage inxe sur larbre
e
construit en lisant la notation postxe
public static void main (String [] arg) {
PrintWriter out = new PrintWriter (System.out) ;
Exp e = postfixToExp(arg) ;
expToInfix(out, e, 1) ;
out.println() ; out.flush() ;
}
Les PrintWriter sont bueriss, il faut vider le tampon par out.flush() avant de nir,
e
voir B.5.4. Reprenons lexemple de la gure II.4.
% java Exp 6 3 2 - 1 + / 9 6 - *
6/(3-2+1)*(9-6)
Ce qui est meilleur que lachage ((6/((3-2)+1))*(9-6)) de lexercice II.2.

Files de priorit
e

Nous avons dj` rencontr les les dattente. Les les de priorit sont des les dattente o` les
ea
e
e
u
lments ont un niveau de priorit. Le passage devant le guichet, ou le traitement de llment,
ee
e
ee
se fait en fonction de son niveau de priorit. Limplantation dune le de priorit est un exemple
e
e
dutilisation darbre, et cest pourquoi elle trouve naturellement sa place ici.
De mani`re plus formelle, une le de priorit est un type abstrait de donnes oprant sur un
e
e
e
e
ensemble ordonn, et muni des oprations suivantes :
e
e
trouver le plus grand lment
ee
insrer un lment
e
ee
retirer le plus grand lment
ee
Bien sr, on peut remplacer (( le plus grand lment )) par (( le plus petit lment )) en prenant
u
ee
ee
lordre oppos. Plusieurs implantations dune le de priorit sont envisageables : par tableau ou
e
e
par liste, ordonns ou non. Nous allons utiliser des tas. La table 1 prsente la complexit des
e
e
e
oprations des les de priorits selon la structure de donnes choisie.
e
e
e
Implantation
Tableau non ordonn
e
Liste non ordonne
e
Tableau ordonn
e
Liste ordonne
e
Tas

Trouver max
O(n)
O(n)
O(1)
O(1)
O(1)

Insrer
e
O(1)
O(1)
O(n)
O(n)
O(log n)

Retirer max
O(n)
O(1)a
O(1)
O(1)
O(log n)

Dans cette table, le cot de la suppression dans une liste non ordonne est calcul en
u
e
e
supposant llment dj` trouv.
ee
ea
e

Tab. 1 Complexit des implantations de les de priorit.


e
e

5.1

Tas

Un arbre binaire est tass si son code est un segment initial pour lordre des mots croiss.
e
e
En dautres termes, dans un tel arbre, tous les niveaux sont enti`rement remplis ` lexception
e
a
peut-tre du dernier niveau, et ce dernier niveau est rempli (( ` gauche )). La gure 16 montre un
e
a
arbre tass.
e


5. FILES DE PRIORITE

97

Proposition 7 La hauteur dun arbre tass ` n nuds est log2 n.


ea
23
15

12
4

5
8

2
Fig. 16 Un arbre tass.
e

Un tas (en anglais (( heap ))) est un arbre binaire tass tel que le contenu de chaque nud
e
soit suprieur ou gal ` celui de ses ls. Ceci entra par transitivit, que le contenu de chaque
e
e
a
ne,
e
nud est suprieur ou gal ` celui de ses descendants.
e
e
a
Larbre de la gure 16 est un tas.

5.2

Implantation dun tas

Un tas simplante facilement ` laide dun simple tableau. Les nuds dun arbre tass sont
a
e
numrots en largeur, de gauche ` droite. Ces numros sont des indices dans un tableau (cf
e e
a
e
gure 17).
0
23
1
15

2
7

3
12
7
4

4
5
8
8

5
6

6
1

9
2

Fig. 17 Un arbre tass, avec la numrotation de ses nuds.


e
e
Llment dindice i du tableau est le contenu du nud de numro i. Dans notre exemple, le
ee
e
tableau est :
i

ai

23

15

12

Le fait que larbre soit tass conduit ` un calcul tr`s simple des relations de liation dans larbre
e
a
e
(n est le nombre de ses nuds) :
racine
parent du nud i
ls gauche du nud i
ls droit du nud i
nud i est une feuille
nud i a un ls droit

:
:
:
:
:
:

nud 0
nud (i 1)/2
nud 2i + 1
nud 2i + 2
2i + 1 n
2i + 2 < n

98

CHAPITRE IV. ARBRES

Linsertion dun nouvel lment v se fait en deux temps : dabord, llment est ajout comme
ee
ee
e
contenu dun nouveau nud ` la n du dernier niveau de larbre, pour que larbre reste tass.
a
e
Ensuite, le contenu de ce nud, soit v, est compar au contenu de son p`re. Tant que le contenu
e
e
`
du p`re est plus petit que v, le contenu du p`re est descendu vers le ls. A la n, on remplace
e
e
par v le dernier contenu abaiss (voir gure 18).
e
0
23
1
15

2
7

3
12
7
4

4
5
8
8

9
2

5
6

6
1

10
21

Fig. 18 Un tas, avec remonte de la valeur 21 apr`s insertion.


e
e
La suppression se fait de mani`re similaire. Dabord, le contenu du nud le plus ` droite du
e
a
dernier niveau est transfr vers la racine, et ce nud est supprim. Ceci garantit que larbre
ee
e
reste tass. Ensuite, le contenu v de la racine est compar ` la plus grande des valeurs de ses ls
e
ea
(sil en a). Si cette valeur est suprieure ` v, elle est remonte et remplace le contenu du p`re.
e
a
e
e
On continue ensuite avec le ls. Par exemple, la suppression de 16 dans larbre de gauche de la
gure 19 conduit dabord ` larbre de droite de cette gure, et enn au tas de la gure 20
a
0
16

0
10

1
15

2
11

3
8
7
2

4
14
8
4

9
7

5
9
10
10

1
15
6
3

2
11

3
8
7
2

4
14
8
4

5
9

6
3

9
7

Fig. 19 Un tas, et la circulation des valeurs pendant la suppression de 16.


La complexit de chacune de ces oprations est majore par la hauteur de larbre qui est,
e
e
e
elle, logarithmique en la taille.
Un tas est naturellement prsent comme une classe, fournissant les trois mthodes maxie
e
e
mum(), inserer(), supprimer(). On range les donnes dans un tableau interne. Un constructeur
e
permet de faire linitialisation ncessaire. Voici le squelette :
e
class Tas
{
int[] a;
int nTas = 0;


5. FILES DE PRIORITE

99
0
15
1
14

2
11

3
8
7
2

4
10
8
4

5
9

6
3

9
7

Fig. 20 Le tas de la gure 19 apr`s suppression de 16.


e
Tas(int n)
{
nTas = 0;
a = new int[n];
}
int maximum()
{
return a[0];
}
void ajouter(int v) {...}
void supprimer() {...}
Avant de donner limplantation nale, voici une premi`re version ` laide de mthodes qui
e
a
e
re`tent les oprations de base.
e
e
void ajouter(int v)
{
int i = nTas;
++nTas;
while (!estRacine(i) && cle(parent(i)) < v)
{
cle(i) = cle(parent(i));
i = parent(i);
}
cle(i) = v;
}
De mme pour la suppression :
e
void supprimer()
{
--nTas;
cle(0) = cle(nTas);
int v = cle(0);
int i = 0;
while (!estFeuille(i))
{
int j = filsG(i);
if (existeFilsD(i) && cle(filsD(i)) > cle(filsG(i)))

100

CHAPITRE IV. ARBRES


j = filsD(i);
if (v >= cle(j)) break;
cle(i) = cle(j);
i = j;
}
cle(i) = v;
}

Il ne reste plus qu` remplacer ce pseudo-code par les instructions oprant directement sur le
a
e
tableau.
void ajouter(int v)
{
int i = nTas;
++nTas;
while (i > 0 && a[(i-1)/2] <= v)
{
a[i] = a[(i-1)/2];
i = (i-1)/2;
}
a[i] = v;
}
On notera que, puisque la hauteur dun tas ` n nuds est log2 n, le nombre de comparaisons
a
utilise par la mthode ajouter est en O(log n).
e
e
void supprimer()
{
int v = a[0] = a[--nTas];
int i = 0;
while (2*i + 1 < nTas)
{
int j = 2*i + 1;
if (j +1 < nTas && a[j+1] > a[j])
++j;
if (v >= a[j])
break;
a[i] = a[j];
i = j;
}
a[i] = v;
}
}
L` encore, la complexit de la mthode supprimer est en O(log n).
a
e
e
On peut se servir dun tas pour trier : on ins`re les lments ` trier dans le tas, puis on les
e
ee
a
extrait un par un. Ceci donne une mthode de tri appele tri par tas ((( heapsort )) en anglais).
e
e
static int[] triParTas(int[] a)
{
int n = a.length;
Tas t = new Tas(n);
for (int i = 0; i < n; i++)
t.ajouter(a[i]);
for (int i = n - 1; i >= 0; --i)


5. FILES DE PRIORITE

101

{
int v = t.maximum();
t.supprimer();
a[i] = v;
}
return a;
}
La complexit de ce tri est, dans le pire des cas, en O(n log n). En eet, on appelle n fois chacune
e
des mthodes ajouter et supprimer.
e

5.3

Arbres de slection
e

Une variante des arbres tasss sert ` la fusion de listes ordonnes. On est en prsence de k
e
a
e
e
suites de nombres ordonnes de faon dcroissante (ou croissante), et on veut les fusionner en
e
c
e
une seule suite croissante. Cette situation se prsente par exemple lorsquon veut fusionner des
e
donnes provenant de capteurs dirents.
e
e
`
Un algorithme (( na )) op`re de mani`re la suivante. A chaque tape, on consid`re le premier
f
e
e
e
e
lment de chacune des k listes (cest le plus grand dans chaque liste), puis on cherche le maxiee
mum de ces nombres. Ce nombre est insr dans la liste rsultat, et supprim de la liste dont il
ee
e
e
provient. La recherche du maximum de k lments cote k 1 comparaisons. Si la somme des
ee
u
longueurs des k listes de donnes est n, lalgorithme na est donc de complexit O(nk).
e
f
e
Il semble plausible que lon puisse gagner du temps en (( mmorisant )) les comparaisons que
e
lon a faites lors du calcul dun maximum. En eet, lorsque lon calcule le maximum suivant,
seule une donne sur les k donnes compares a chang. Lordre des k 1 autres lments reste le
e
e
e
e
ee
mme, et on peut conomiser des comparaisons si lon conna cet ordre au moins partiellement.
e
e
t
La solution que nous proposons est base sur un tas qui mmorise partiellement lordre. On
e
e
verra que lon peut eectuer la fusion en temps O(k + n log k). Le premier terme correspond au
(( prtraitement )), cest-`-dire ` la mise en place de la structure particuli`re.
e
a
a
e
Larbre de slection est un arbre tass ` k feuilles. Chaque feuille est en fait une liste, lune
e
ea
des k listes ` fusionner (voir gure 21). La hauteur de larbre est log k. Chaque nud de larbre
a

98
49

98

49

38
32
25
12
9

18

49
45
23
21
11
6

18
12
9
1

98

9
8
4
3
2

98
85
78
65
52
36

89

49
35
31
25
21
13

58
53
42
26
10

89
67
55
44
32
13

Fig. 21 Un arbre de slection pour 8 listes.


e
contient, comme cl, le maximum des cls de ses deux ls (ou le plus grand lment dune liste).
e
e
ee

102

CHAPITRE IV. ARBRES

En particulier, le maximum des lments en tte des k listes (en gris sur la gure 21) se trouve
ee
e
e
log k fois dans larbre, sur les nuds dun chemin menant ` la racine. Lextraction dun plus
a
grand lment se fait donc en temps constant. Cette extraction est suivie dun mise-`-jour :
ee
a
En descendant le chemin dont les cls portent le maximum, on aboutit ` la liste dont la valeur
e
a
de tte est ce maximum. Llment est remplac, dans la liste, par son suivant. Ensuite, on
e
ee
e
remonte vers la racine en recalculant, pour chaque nud rencontr, le valeur de la cl, avec la
e
e
`
nouvelle valeur du ls mis-`-jour. A la racine, on trouve le nouveau maximum (voir gure 22).
a
La mise-`-jour se fait donc en O(log k) oprations. La mise en place initiale de larbre est en
a
e

89
49

89

49

38
32
25
12
9

18

49
45
23
21
11
6

18
12
9
1

85

9
8
4
3
2

85
78
65
52
36

89

49
35
31
25
21
13

58
53
42
26
10

89
67
55
44
32
13

Fig. 22 Apr`s extraction du plus grand lment et recalcul.


e
ee
temps k.

6
6.1

Codage de Human
Compression des donnes
e

La compression des donnes est un probl`me algorithmique aussi important que le tri. On
e
e
distingue la compression sans perte, o` les donnes dcompresses sont identiques aux donnes
u
e
e
e
e
de dpart, et la compression avec perte. La compression sans perte est importante par exemple
e
dans la compression de textes, de donnes scientiques ou du code compil de programmes. En
e
e
revanche, pour la compression dimages et de sons, une perte peut tre accepte si elle nest pas
e
e
perceptible, ou si elle est automatiquement corrige par le rcepteur.
e
e
Parmi les algorithmes de compression sans perte, on distingue plusieurs catgories : les algoe
rithmes statistiques codent les caract`res frquents par des codes courts, et les caract`res rares
e
e
e
par des codes longs ; les algorithmes bass sur les dictionnaires enregistrent toutes les cha
e
nes de
caract`res trouves dans une table, et si une cha appara une deuxi`me fois, elle est remplace
e
e
ne
t
e
e
par son indice dans la table. Lalgorithme de Human est un algorithme statistique. Lalgorithme
de Ziv-Lempel est bas sur un dictionnaire. Enn, il existe des algorithmes (( numriques )). Le
e
e
codage arithmtique remplace un texte par un nombre rel entre 0 et 1 dont les chires en
e
e
criture binaire peuvent tre calculs au fur et ` mesure du codage. Le codage arithmtique
e
e
e
a
e
repose aussi sur la frquence des lettres. Ces algorithmes seront tudis dans le cours 431 ou en
e
e
e
majeure.

6. CODAGE DE HUFFMAN

6.2

103

Algorithme de Human

Lalgorithme de Human est un algorithme statistique. Les caract`res du texte clair sont
e
cods par des cha
e
nes de bits. Le choix de ces codes se fait dune part en fonction des frquences
e
dapparition de chaque lettre, de sorte que les lettres frquentes aient des codes courts, mais
e
aussi de faon ` rendre le dcodage facile. Pour cela, on choisit un code prxe, au sens dcrit
c a
e
e
e
ci-dessous. La version du codage de Human que nous dtaillons dans cette section est le codage
e
dit (( statique )) : les frquences nvoluent pas au cours de la compression, et le code reste xe.
e
e
Cest ce qui se passe dans les modems, ou dans les fax.
Une version plus sophistique est le codage dit (( adaptatif )), prsent bri`vement dans la
e
e
e
e
section 6.3. Dans ce cas, les frquences sont mises ` jour apr`s chaque compression de caract`re,
e
a
e
e
pour chaque fois optimiser le codage.
6.2.1

Codes prxes et arbres


e

Un ensemble P de mots non vides est un code prxe si aucun des mots de P nest prxe
e
e
propre dun autre mot de P .
Par exemple, lensemble {0, 100, 101, 111, 1100, 1101} est un code prxe. Un code prxe P
e
e
est complet si tout mot est prxe dun produit de mots de P . Le code de lexemple ci-dessus
e
est complet. Pour illustrer ce fait, prenons le mot 1011010101011110110011 : il est prxe du
e
produit
101 101 0 101 0 111 10 1100 111
Lintrt de ces dnitions vient des propositions suivantes.
ee
e
Proposition 8 Un ensemble ni de mots sur {0, 1} est un code prxe si et seulement sil est
e
le code des feuilles dun arbre binaire.
Rappelons quun arbre binaire est dit complet si tous ses nuds internes ont arit 2.
e
Proposition 9 Un ensemble ni de mots sur {0, 1} est un code prxe complet si et seulement
e
sil est le code des feuilles dun arbre binaire complet.
La gure 23 reprend larbre de la gure 12. Le code des feuilles est prxe, mais il nest pas
e
complet.

0
0

1
1

1
1

Fig. 23 Le code des feuilles est {00, 010, 101, 110}.


En revanche, le code des feuilles de larbre de la gure 24 est complet.

104

CHAPITRE IV. ARBRES

0
0
a

1
1

0
d

0
b

1
r

1
c

Fig. 24 Le code des feuilles est {00, 010, 011, 10, 11}.
Le codage dun texte par un code prxe (complet) se fait de la mani`re suivante : ` chaque
e
e
a
lettre est associe une feuille de larbre. Le code de la lettre est le code de la feuille, cest-`-dire
e
a
ltiquette du chemin de la racine ` la feuille. Dans le cas de larbre de la gure 24, on associe
e
a
les lettres aux feuilles de la gauche vers la droite, ce qui donne le tableau de codage suivant :
a
b
c
d
r

00
010
011
10
11

Le codage consiste simplement ` remplacer chaque lettre par son code. Ainsi, abracadabra devient
a
000101100011001000010110. Le fait que le code soit prxe permet un dcodage instantan : il
e
e
e
sut dentrer la cha caract`re par caract`re dans larbre, et de se laisser guider par les
ne
e
e
tiquettes. Lorsque lon est dans une feuille, on y trouve le caract`re correspondant, et on
e
e
recommence ` la racine. Quand le code est complet, on est sr de pouvoir toujours dcoder un
a
u
e
message, ventuellement ` un reste pr`s qui est un prxe dun mot du code.
e
a
e
e
Le probl`me qui se pose est de minimiser la taille du texte cod. Avec le code donn dans le
e
e
e
tableau ci-dessus, le texte abracadabra, une fois cod, est compos de 25 bits. Si lon choisit un
e
e
autre codage, comme par exemple
a 0
b 10
c 1101
d 1100
r 111
on observe que le mme mot se code par 01011101101011000101110 et donc avec 23 bits. Bien
e
sr, la taille du rsultat ne dpend que de la frquence dapparition de chaque lettre dans le
u
e
e
e
texte source. Ici, il y a 5 lettres a, 2 lettres b et r, et 1 fois la lettre c et d.
6.2.2

Construction de larbre

Lalgorithme de Human construit un arbre binaire complet qui donne un code optimal, en
ce sens que la taille du code produit est minimale parmi la taille de tous les codes produits `
a
laide de codes prxes complets.
e
Initialisation On construit une fort darbres binaires forms chacun dune seul feuille. Chaque
e
e
feuille correspond ` une lettre du texte, et a pour valeur la frquence dapparition de la
a
e
lettre dans le texte.
Itration On fusionne deux des arbres dont la frquence est minimale. Le nouvel arbre a pour
e
e
frquence la somme des frquences de ses deux sous-arbres.
e
e

6. CODAGE DE HUFFMAN

105

Arrt On termine quand il ne reste plus quun seul arbre qui est larbre rsultat.
e
e
La frquence dun arbre est, bien sr, la somme des frquences de ses feuilles. Voici une
e
u
e
illustration de cet algorithme sur le mot abracadabra. La premi`re tape conduit ` la cration
e e
a
e
des 5 arbres (feuilles) de la gure 25. Les feuilles des lettres c et d sont fusionnes (gure 26),
e
puis cet arbre avec la feuille b (gure 27), etc. Le rsultat est reprsent dans la gure 29.
e
e
e
5

Fig. 25 Les 5 arbres rduits chacun ` une feuille.


e
a

5
a

2
b

2
r

2
1
d

1
c

Fig. 26 Les feuilles des lettres c et d sont fusionnes.


e

5
a

2
b

4
2

1
d

2
r

1
c

Fig. 27 Larbre est fusionn avec la feuille de r.


e

5
a

6
2
b

4
2

1
d

2
r
1
c

Fig. 28 La feuille de b est fusionne avec larbre.


e
Notons quil y a beaucoup de choix dans lalgorithme : dune part, on peut choisir lequel des
deux arbres devient sous-arbre gauche ou droit. Ensuite, les deux sous-arbres de frquence minie
male ne sont peut-tre pas uniques, et l` encore, il y a des choix pour la fusion. On peut prouver
e
a
que cet algorithme donne un code optimal. Il en existe de nombreuses variantes. Lune delle
consiste ` grouper les lettres par deux (digrammes) ou mme par bloc plus grands, notamment
a
e
sil sagit de donnes binaires. Les techniques de compression avec dictionnaire (compression de
e
Ziv-Lempel) ou le codage arithmtique donnent en gnral de meilleurs taux de compression.
e
e e

106

CHAPITRE IV. ARBRES


11
5
a

6
2
b
1
d

4
2
1
c

2
r

Fig. 29 Larbre apr`s la derni`re fusion.


e
e
6.2.3

Choix de la reprsentation des donnes


e
e

Si le nombre de lettres gurant dans le texte est n, larbre de Human est de taille 2n 1.
On le reprsente ici par un tableau pere, o`, pour x 0 et y > 0 pere[x] = y si x est ls droit de
e
u
y et pere[x] = -y si x est ls gauche de y. Chaque caract`re c a une frquence dapparition dans
e
e
le texte, note freq[c]. Seuls les caract`res qui gurent dans le texte, i.e. de frquence non nulle,
e
e
e
vont tre reprsents dans larbre.
e
e
e
La cration de larbre est contrle par un tas (mais un (( tas-min )), cest-`-dire un tas avec
e
oe
a
extraction du minimum). La cl de la slection est la frquence des lettres et la frquence cumule
e
e
e
e
e
dans les arbres. Une autre mthode rencontre est lemploi de deux les.
e
e
Cette reprsentation des donnes est bien adapte au probl`me considr, mais est assez
e
e
e
e
ee
loigne des reprsentations darbres que nous verrons dans le chapitre suivant. Raliser une
e
e
e
e
implantation ` laide de cette deuxi`me reprsentation est un bon exercice.
a
e
e
6.2.4

Implantation

La classe Huffman a deux donnes statiques : le tableau pere pour reprsenter larbre, et un
e
e
tableau freq qui contient les frquences des lettres dans le texte, et aussi les frquences cumules
e
e
e
dans les arbres. On part du principe que les 26 lettres de lalphabet peuvent gurer dans le texte
(dans la pratique, on prendra plutt un alphabet de 128 ou de 256 caract`res).
o
e
class Huffman
{
final static int N = 26, M = 2*N - 1; // nombre de caract`res
e
static int[] pere = new int[M];
static int[] freq = new int[M];
public static void main(String[] args)
{
String s = args[0];
calculFrequences(s);
creerArbre();
String[] table = faireTable();
afficherCode(s,table);
System.out.println();
}
}
La mthode main dcrit lopration : on calcule les frquences des lettres, et le nombre de
e
e
e
e
lettres de frquence non nulle. On cre ensuite un tas de taille approprie, avec le tableau des
e
e
e

6. CODAGE DE HUFFMAN

107

frquences comme cls. Lopration principale est creerArbre() qui cre larbre de Human. La
e
e
e
e
table de codage est ensuite calcule et applique ` la cha ` compresser.
e
e a
ne a
Voyons les diverses tapes. Le calcul des frquences et du nombre de lettres de frquence non
e
e
e
nulle ne pose pas de probl`me. On notera toutefois lutilisation de la mthode charAt de la classe
e
e
String, qui donne lunicode du caract`re en position i. Comme on a suppos que lalphabet tait
e
e
e
az et que les unicodes de ces caract`res sont conscutifs, lexpression s.charAt(i) - a donne
e
e
bien le rang du i-`me caract`re dans lalphabet. Il faudrait bien sr modier cette mthode si
e
e
u
e
on prenait un alphabet ` 256 caract`res.
a
e
static void calculFrequences(String s)
{
for (int i = 0; i < s.length(); i++)
freq[s.charAt(i) - a]++;
}
static int nombreLettres()
{
int n = 0;
for (int i = 0; i < N; i++)
if (freq[i] > 0)
n++;
return n;
}
La mthode de cration darbre utilise tr`s exactement lalgorithme expos plus haut : dabord,
e
e
e
e
les lettres sont insres dans le tas ; ensuite, les deux lments de frquence minimale sont
ee
ee
e
extraits, et un nouvel arbre, dont la frquence est la somme des frquences des deux arbres
e
e
extraits, est insr dans le tas. Comme il y a n feuilles dans larbre, il y a n 1 crations de
ee
e
nuds internes.
static void creerArbre()
{
int n = nombreLettres();
Tas tas = new Tas(2*n-1, freq);
for (int i = 0; i < N; ++i)
if (freq[i] >0)
tas.ajouter(i);
for (int i = N; i < N+n-1; ++i)
{
int x = tas.minimum();
tas.supprimer();
int y = tas.minimum();
tas.supprimer();
freq[i] = freq[x] + freq[y];
pere[x] = -i;
pere[y] = i;
tas.ajouter(i);
}
}
Le calcul de la table de codage se fait par un parcours darbre :
static String code(int i)
{

108

CHAPITRE IV. ARBRES


if (pere[i] == 0) return "";
return code(Math.abs(pere[i])) + ((pere[i] < 0) ? "0" : "1");
}
static String[] faireTable()
{
String[] table = new String[N];
for (int i = 0; i < N; i++)
if (freq[i] >0)
table[i] = code(i);
return table;
}

Il reste ` examiner la ralisation du (( tas-min )). Il y a deux dirences avec les tas dj` vus :
a
e
e
ea
dune part, le maximum est remplac par le minimum (ce qui est ngligeable), et dautre part,
e
e
ce ne sont pas les valeurs des lments eux-mmes (les lettres du texte) qui sont utilises comme
ee
e
e
comparaison, mais une valeur qui leur est associe (la frquence de la lettre). Les mthodes des
e
e
e
tas dj` vues se rcrivent tr`s facilement dans ce cadre.
ea
e
e
class Tas
{
int[] a; // contient les caract`res
e
int nTas = 0;
int[] freq; // contient les frquences des caract`res
e
e
Tas(int taille, int[] freq)
{
this.freq = freq;
nTas = 0;
a = new int[taille];
}
int minimum()
{
return a[0];
}
void ajouter(int v)
{
int i = nTas;
++nTas;
while (i > 0 && freq[a[(i-1)/2]] >= freq[v])
{
a[i] = a[(i-1)/2];
i = (i-1)/2;
}
a[i] = v;
}
void supprimer()
{
int v = a[0] = a[--nTas];
int i = 0;
while (2*i + 1 < nTas)
{

6. CODAGE DE HUFFMAN

109

int j = 2*i + 1;
if (j +1 < nTas && freq[a[j+1]] < freq[a[j]])
++j;
if (freq[v] <= freq[a[j]])
break;
a[i] = a[j];
i = j;
}
a[i] = v;
}
}
Une fois la table du code calcule, encore faut-il la transmettre avec le texte comprim, pour
e
e
que le destinaire puisse dcompresser le message. Transmettre la table telle quelle est redondant
e
puisque lon transmet tous les chemins de la racine vers les feuilles. Il est plus conomique de faire
e
un parcours prxe de larbre, avec la valeur littrale des lettres reprsentes aux feuilles. Par
e
e
e
e
exemple, pour larbre de la gure 29, on obtient la reprsentation 01[a]01[b]001[d]1[c]1[r].
e
Une mthode plus simple est de transmettre la suite de frquence, quitte au destinataire de
e
e
reconstruire le code. Cette deuxi`me mthode admet un ranement (qui montre jusquo` peut
e
e
u
aller la recherche de lconomie de place) qui consiste ` non pas transmettre les valeurs exactes
e
a
des frquences, mais une squence de frquences ctives (` valeurs numriques plus petites, donc
e
e
e
a
e
plus courtes ` transmettre) qui donne le mme code de Human. Par exemple, les deux arbres
a
e
123
80

12
43

a
41

39

a
3

e
25

14

13

12

Fig. 30 Deux suites de frquences qui produisent le mme arbre.


e
e
de la gure 30 ont des frquences direntes, mais le mme code. Il est bien plus conomique de
e
e
e
e
transmettre la suite de nombres 5, 1, 2, 1, 3 que la suite initiale 43, 13, 12, 14, 41.

6.3

Algorithme de Human adaptatif

Les inconvnients de la mthode de compression de Human sont connus :


e
e
Il faut lire le texte enti`rement avant de lancer la compression.
e
Il faut aussi transmettre le code trouv.
e
Une version adaptative de lalgorithme de Human corrige ces dfauts. Le principe est le suivant :
e
Au dpart, toutes les lettres ont mme frquence (nulle) et le code est uniforme.
e
e
e
Pour chaque lettre x, le code de x est envoy, puis la frquence de la lettre est augmente
e
e
e
et larbre de Human est recalcul.
e
Evidemment, le code dune lettre change en cours de transmission : quand la frquence (relative)
e
dune lettre augmente, la longueur de son code diminue. Le code dune lettre sadapte ` sa
a
frquence. Le destinataire du message compress mime le codage de lexpditeur : il maintient
e
e
e

110

CHAPITRE IV. ARBRES

32 11

9 11

21 10

a
11 8

7 10

4 5

3 5
c

2 3

5 5

1 2

6 6

Fig. 31 Un arbre de Human avec un parcours compatible.


un arbre de Human quil met ` jour comme lexpditeur, et il sait donc ` tout moment quel
a
e
a
est le code dune lettre.
Les deux inconvnients du codage de Human (statique) sont corrigs par cette version. Il
e
e
nest pas ncessaire de calculer globalement les frquences des lettres du texte puisquelles sont
e
e
mises ` jour ` chaque pas. Il nest pas non plus ncessaire de transmettre la table de codage
a
a
e
puisquelle est recalcule par le destinataire.
e
Il existe par ailleurs une version (( simplie )) du codage de Human o` les frquences utilises
e
u
e
e
pour la construction de larbre ne sont pas les frquences des lettres du texte ` comprimer, mais
e
a
les frquences moyennes rencontres dans les textes de la langue considre. Bien entendu, le
e
e
ee
taux de compression sen ressent si la distribution des lettres dans le texte ` comprimer scarte
a
e
de cette moyenne. Cest dans ce cas que lalgorithme adaptatif sav`re particuli`rement utile.
e
e
Notons enn que le principe de lalgorithme adaptatif sapplique ` tous les algorithmes
a
de compression statistiques. Il existe, par exemple, une version adaptative de lalgorithme de
compression arithmtique.
e
Lalgorithme adaptatif op`re, comme lalgorithme statique, sur un arbre. Aux feuilles de
e
larbre se trouvent les lettres, avec leurs frquences. Aux nuds de larbre sont stockes les
e
e
frquences cumules, cest-`-dire la somme des frquences des feuilles du sous-arbre dont le
e
e
a
e
nud est la racine (voir gure 31). Larbre de Human est de plus muni dune liste de parcours
(un ordre total) qui a les deux proprits suivantes :
ee
le parcours est compatible avec les frquences (les frquences sont croissantes dans lordre
e
e
du parcours),
deux nuds fr`res sont toujours conscutifs.
e
e
On dmontre que, dans un arbre de Human, on peut toujours trouver un ordre de parcours
e
possdant ces proprits. Dans la gure 31, les nuds sont numrots dans un tel ordre. Larbre
e
ee
e e
de Human volue avec chaque lettre code, de la mani`re suivante. Soit x le caract`re lu dans
e
e
e
e
le texte clair. Il correspond ` une feuille de larbre. Le code correspondant est envoy, puis les
a
e
deux oprations suivantes sont ralises.
e
e e
Partant de la feuille du caract`re, sa frquence est incrmente, et on remonte vers la
e
e
e
e
racine en incrmentant les frquences dans les nuds rencontrs.
e
e
e
Avant lincrmentation, chaque nud est permut avec le dernier nud (celui de plus
e
e
grand numro) de mme frquence dans lordre de parcours.
e
e
e
La deuxi`me opration garantit que lordre reste compatible avec les frquences. Supposons que
e
e
e

6. CODAGE DE HUFFMAN

111

32 11

9 11

32 11

9 11

21 10

21 10

a
7 10

4 5

7 10

11 8

3 5

6 6
e

5 5
f

11 8

4 5

3 5

6 6

6 5

2 3

1 3

2 3

1 3

Fig. 32 Apr`s incrmentation de d (` gauche), et apr`s permutation des nuds (` droite).


e
e
a
e
a

32 11

9 11

33 11

8 12

21 10

21 10

a
7 10

11 8

6 6

7 10

6 5

11 9

e
4 5

3 5

6 6

6 5

a
2 3
b

e
2 3

5 4

5 3

1 3

1 3

Fig. 33 Avant permutation des nuds 8 et 9 (` gauche) , et arbre nal (` droite).


a
a

la lettre suivante du texte clair ` coder, dans larbre de la gure 31, soit une lettre d. Le code
a
mis est alors 1001. La frquence de la feuille d est incrmente. Le p`re de la feuille d a pour
e
e
e
e
e
frquence 5. Son numro dordre est 4, et le plus grand nud de frquence 5 a numro dordre
e
e
e
e
5. Les deux nuds sont changs (gure 32). De mme, avant dincrmenter le nud de numro
e
e
e
e
e
8, il est chang avec le nud de numro 9, de mme frquence (gure 33). Ensuite, la racine
e
e
e
e
e
est incrmente.
e
e
Pour terminer, voici une table de quelques statistiques concernant lalgorithme de Human.
Ces statistiques ont t obtenues ` une poque o` il y avait encore peu de donnes disponibles
ee
a
e
u
e
sous format lectronique, et les contes de Grimm en faisaient partie... Comme on le voit, le taux
e
de compression obtenu par les deux mthodes est sensiblement gal, car les donnes sont assez
e
e
e

112

CHAPITRE IV. ARBRES

homog`nes.
e
Taille (bits)
Human
Taille code
Total
Adaptatif

Contes de Grimm
700 000
439 613
522
440135
440164

Texte technique
700 000
518 361
954
519315
519561

Si on utilise un codage par digrammes (groupe de deux lettres), les statistiques sont les suivantes :

Taille (bits)
Human
Taille code
Total
Adaptatif

Contes de Grimm
700 000
383 264
10 880
394 144
393 969

Texte technique
700 000
442 564
31 488
474 052
472 534

On voit que le taux de compression est lg`rement meilleur.


e e

Chapitre V

Arbres binaires
Dans ce chapitre, nous traitons dabord les arbres binaires de recherche, puis les arbres
quilibrs.
e
e

Implantation des arbres binaires

Un arbre binaire qui nest pas vide est form dun nud, sa racine, et de deux sous-arbres
e
binaires, lun appel le ls gauche, lautre le ls droit. Nous nous intressons aux arbres contenant
e
e
des informations. Chaque nud porte une information, appele son contenu. Un arbre non vide
e
est donc enti`rement dcrit par le triplet (ls gauche, contenu de la racine, ls droit). Cette
e
e
dnition rcursive se traduit en une spcication de programmation. Il sut de prciser la
e
e
e
e
nature du contenu dun nud. Pour simplier, nous supposons que le contenu est un entier. On
obtient alors la dnition.
e
class Arbre
{
int contenu;
Arbre filsG, filsD;
Arbre(Arbre g, int c, Arbre d)
{
filsG = g;
contenu = c;
filsD = d;
}
}
Larbre vide est, comme dhabitude, reprsent par null. Un arbre rduit ` une feuille, de contenu
e
e
e
a
x, est cr par
ee
new Arbre(null, x, null)
Larbre de la gure 1 est cr par
ee
new Arbre(
new Arbre(
new Arbre(null, 3, null),
5,
new Arbre(
new Arbre(
new Arbre(null, 6, null),
8,
113

114

CHAPITRE V. ARBRES BINAIRES


null)
12,
new Arbre(null, 13, null))),
20,
new Arbre(
new Arbre(null, 21, null),
25,
new Arbre(null, 28, null)))
20
5
3

25
12

21

28

13

Fig. 1 Un arbre binaire portant des informations aux nuds.


Avant de poursuivre, reprenons le schma dj` utilis pour les listes. Quatre fonctions cae
ea
e
ractrisent les arbres : composer, cle, lsGauche, lsDroit. Elles simplantent facilement :
e
static Arbre composer(Arbre g, int c, Arbre d)
{
return new Arbre(g, c, d);
}
static int cle(Arbre a)
{
return a.contenu;
}
static Arbre filsGauche(Arbre a)
{
return a.filsG;
}
static Arbre filsDroit(Arbre a)
{
return a.filsD;
}
Les quatre fonctions sont lies par les quations suivantes :
e
e
cle(composer(g, c, d)) = c
lsGauche(composer(g, c, d)) = g
lsDroit(composer(g, c, d)) = d
composer(lsGauche(a), cle(a), lsDroit(a)) = a;

(a = null)

Comme pour les listes, ces quatre fonctions sont ` la base doprations non destructives.
a
e

1. IMPLANTATION DES ARBRES BINAIRES

115

La dnition rcursive des arbres binaires conduit naturellement ` une programmation


e
e
a
rcursive, comme pour les listes. Voici quelques exemples : la taille dun arbre, cest-`-dire le
e
a
nombre t(a) de ses nuds, sobtient par la formule
t(a) =

0
si a est vide
1 + t(ag ) + t(ad ) sinon.

o` sont nots ag et ad les sous-arbres gauche et droit de a. Do` la mthode


u
e
u
e
static int
{
if (a ==
return
return 1
}

taille(Arbre a)
null)
0;
+ taille(a.filsG) + taille(a.filsD);

Des formules semblables donnent le nombre de feuilles ou la hauteur dun arbre. Nous illustrons ce style de programmation par les parcours darbre dnis page 89. Les trois parcours en
e
profondeur scrivent :
e
static void parcoursPrfixe(Arbre a)
e
{
if (a == null)
return;
System.out.print(a.contenu + " ");
parcoursPrfixe(a.filsG);
e
parcoursPrfixe(a.filsD);
e
}
static void parcoursInfixe(Arbre a)
{
if (a == null)
return;
parcoursInfixe(a.filsG);
System.out.print(a.contenu + " ");
parcoursInfixe(a.filsD);
}
static void parcoursSuffixe(Arbre a)
{
if (a == null)
return;
parcoursSuffixe(a.filsG);
parcoursSuffixe(a.filsD);
System.out.print(a.contenu + " ");
}
Le parcours en largeur dun arbre binaire scrit simplement avec une le. Le parcours prxe
e
e
scrit lui aussi simplement de mani`re itrative, avec une pile. Nous reprenons les classes Pile
e
e
e
et File du chapitre I, sauf que ce sont, cette fois-ci, des piles ou des les darbres. On crit alors
e
static void parcoursPrfixeI(Arbre a)
e
{
if (a == null)
return;

116

CHAPITRE V. ARBRES BINAIRES


Pile p = new Pile();
p.ajouter(a);
while (!p.estVide())
{
a = p.valeur();
p.supprimer();
System.out.print(a.contenu + " ");
if (a.filsD != null)
p.ajouter(a.filsD);
if (a.filsG != null)
p.ajouter(a.filsG);
}
}
static void parcoursLargeurI(Arbre a)
{
if (a == null)
return;
File f = new File();
f.ajouter(a);
while (!f.estVide())
{
a = f.valeur();
f.supprimer();
System.out.print(a.contenu + " ");
if (a.filsG != null)
f.ajouter(a.filsG);
if (a.filsD != null)
f.ajouter(a.filsD);
}
}

1.1

Implantation des arbres ordonns par arbres binaires


e

Rappelons quun arbre est ordonn si la suite des ls de chaque nud est ordonne. Il est donc
e
e
naturel de reprsenter les ls dans une liste cha ee. Un nud contient aussi la rfrence ` son
e
n
ee
a
ls a e, cest-`-dire ` la tte de la liste de ses ls. Ainsi, chaque nud contient deux rfrences,
n
a
a
e
ee
celle ` son ls a e, et celle ` son fr`re cadet. En dautres termes, la structure des arbres binaires
a
n
a
e
convient parfaitement, sous rserve de rebaptiser lsAine le champ lsG et frereCadet le champ
e
lsD.
class ArbreOrdonne
{
int contenu;
Arbre filsAine, frereCadet;
Arbre(Arbre g, int c, Arbre d)
{
filsAine = g;
contenu = c;
frereCadet = d;
}
}

2. ARBRES BINAIRES DE RECHERCHE

117

Cette reprsentation est aussi appele (( ls gauche fr`re droit )) (voir gure 2).
e
e
e

1
2

3
6

7
10

4
8

10

Fig. 2 Reprsentation dun arbre ordonn par un arbre binaire.


e
e
Noter que la racine de larbre binaire na pas de ls droit. En fait, cette reprsentation stend
e
e
a
` la reprsentation, par un seul arbre binaire, dune fort ordonne darbres ordonns.
e
e
e
e

Arbres binaires de recherche

Les arbres binaires servent ` grer des informations. Chaque nud contient une donne prise
a e
e
dans un certain ensemble. Nous supposons dans cette section que cet ensemble est totalement
ordonn. Ceci est le cas par exemple pour les entiers et pour les mots.
e
Un arbre binaire a est un arbre binaire de recherche si, pour tout nud s de a, les contenus
des nuds du sous-arbre gauche de s sont strictement infrieurs au contenu de s, et que les
e
contenus des nuds du sous-arbre droit de s sont strictement suprieurs au contenu de s (cf.
e
gure 3).
15
12

20

14
10

13

16

21
17

Fig. 3 Un arbre binaire de recherche.


Une petite mise en garde : contrairement ` ce que lanalogie avec les tas pourrait laisser
a
croire, il ne sut pas de supposer que, pour tout nud s de larbre, le contenu du ls gauche
de s soit strictement infrieur au contenu de s, et que le contenu du ls droit de s soit suprieur
e
e
au contenu de s. Ainsi, dans larbre de la gure 3, si on change la valeur 13 en 11, on na plus
un arbre de recherche...
Une consquence directe de la dnition est la r`gle suivante :
e
e
e
R`gle. Dans un arbre binaire de recherche, le parcours inxe fournit les contenus des nuds en
e
ordre croissant.

118

CHAPITRE V. ARBRES BINAIRES

Une seconde r`gle permet de dterminer dans certains cas le nud qui prc`de un nud
e
e
e e
donn dans le parcours inxe.
e
R`gle. Si un nud poss`de un ls gauche, son prdcesseur dans le parcours inxe est le nud
e
e
e e
le plus ` droite dans son sous-arbre gauche. Ce nud nest pas ncessairement une feuille, mais
a
e
il na pas de ls droit.
Ainsi, dans larbre de la gure 3, la racine poss`de un ls gauche, et son prdcesseur est le
e
e e
nud de contenu 14, qui na pas de ls droit...
Les arbres binaires de recherche fournissent, comme on le verra, une implantation souvent
ecace dun type abstrait de donnes, appel dictionnaire, qui op`re sur un ensemble totalement
e
e
e
ordonn ` laide des oprations suivantes :
ea
e
rechercher un lment
ee
insrer un lment
e
ee
supprimer un lment
ee
Le dictionnaire est simplement une table dassociations (chapitre III) dont les informations sont
inexistantes. Plusieurs implantations dun dictionnaire sont envisageables : par tableau, par liste
ordonns ou non, par tas, et par arbre binaire de recherche. La table 1 rassemble la complexit
e
e
des oprations de dictionnaire selon la structure de donnes choisie.
e
e
Implantation
Tableau non ordonn
e
Liste non ordonne
e
Tableau ordonn
e
Liste ordonne
e
Tas
Arbre de recherche

Rechercher
O(n)
O(n)
O(log n)
O(n)
O(n)
O(h)

Insrer
e
O(1)a
O(1)a
O(n)
O(n)
O(log n)
O(h)

Supprimer
O(n)
O(1)b
O(n)
O(1)b
O(n)
O(h)

Ces cots supposent que lon sait que llment insr nest pas dans le dictionnaire.
u
ee
e e
Ces cots supposent llment supprim dj` trouv, ainsi que des oprations destrucu
ee
e ea
e
e
tives.
b

Tab. 1 Complexit des implantations de dictionnaires


e
Lentier h dsigne la hauteur de larbre. On voit que lorsque larbre est bien quilibr, ceste
e
e
a
`-dire lorsque la hauteur est proche du logarithme de la taille, les oprations sont ralisables de
e
e
mani`re particuli`rement ecace.
e
e

2.1

Recherche dune cl
e

Nous commenons limplantation des oprations de dictionnaire sur les arbres binaires de
c
e
recherche par lopration la plus simple, la recherche. Plutt que dcrire une mthode boolenne
e
o
e
e
e
qui teste la prsence dun lment dans larbre, nous crivons une mthode qui retourne larbre
e
ee
e
e
dont la racine porte llment cherch sil gure dans larbre, et null sinon. Comme toujours,
ee
e
il y a le choix entre une mthode rcursive, calque sur la dnition rcursive des arbres, et
e
e
e
e
e
une mthode itrative, cheminant dans larbre. Nous prsentons les deux, en commenant par
e
e
e
c
la mthode rcursive. Pour chercher si un lment x gure dans un arbre A, on commence par
e
e
ee
comparer x au contenu c de la racine de A. Sil y a galit, on a trouv la rponse ; sinon il y a
e
e
e
e
deux cas selon que x < c et x > c. Si x < c, alors x gure peut-tre dans le sous-arbre gauche Ag
e
de A, mais certainement pas dans le sous-arbre droit Ad . On limine ainsi de la recherche tous
e
les nuds du sous-arbre droit. Cette mthode nest pas sans rappeler la recherche dichotomique.
e
La mthode scrit rcursivement comme suit :
e
e
e

2. ARBRES BINAIRES DE RECHERCHE

119

static Arbre chercher(int x, Arbre a)


{
if (a == null || x == a.contenu)
return a;
if (x < a.contenu)
return chercher(x, a.filsG);
return chercher(x, a.filsD);
}
Cette mthode retourne null si larbre a ne contient pas x. Ceci inclut le cas o` larbre est vide.
e
u
Voici la mthode itrative.
e
e
static chercherI(int x, Arbre a)
{
while(a != null && x != a.contenu)
if (x < a.contenu)
a = a.filsG;
else
a = a.filsD;
return a;
}
On voit que la condition de continuation dans la mthode itrative chercherI est la ngation de
e
e
e
la condition darrt de la mthode rcursive, ce qui est logique.
e
e
e

2.2

Insertion dune cl
e

Ladjonction dun nouvel lment ` un arbre modie larbre. Nous sommes confronts au
ee
a
e
mme choix que pour les listes : soit on construit une nouvelle version de larbre (version non
e
destructive), soit on modie larbre existant (version destructive). Nous prsentons une mthode
e
e
rcursive dans les deux versions. Dans les deux cas, si lentier gure dj` dans larbre, on ne
e
ea
lajoute pas une deuxi`me fois. Voici la version destructive.
e
static Arbre inserer(int x, Arbre a)
{
if (a == null)
return new Arbre(null, x, null);
if (x < a.contenu)
a.filsG = inserer(x, a.filsG);
else if (x > a.contenu)
a.filsD = inserer(x, a.filsD);
return a;
}
Voici la version non destructive.
static Arbre inserer(int x, Arbre a)
{
if (a == null)
return new Arbre(null, x, null);
if (x < a.contenu)
{
Arbre b = inserer(x, a.filsG);
return new Arbre(b, a.contenu, a.filsD);
}

120

CHAPITRE V. ARBRES BINAIRES


else if (x > a.contenu)
{
Arbre b = inserer(x, a.filsD);
return new Arbre(a.filsG, a.contenu, b);
}
return a;
}

2.3

Suppression dune cl
e

La suppression dune cl dans un arbre est une opration plus complexe. Elle saccompagne
e
e
de la suppression dun nud. Comme on le verra, ce nest pas toujours le nud qui porte la
cl ` supprimer qui sera enlev. Soit s le nud qui porte la cl x ` supprimer. Trois cas sont `
ea
e
e a
a
considrer selon le nombre de ls du nud x :
e
si le nud s est une feuille, alors on llimine ;
e
si le nud s poss`de un seul ls, on limine s et on (( remonte )) ce ls.
e
e
si le nud s poss`de deux ls, on cherche le prdcesseur t de s. Celui-ci na pas de ls
e
e e
droit. On remplace le contenu de s par le contenu de t, et on limine t.
e
La suppression de la feuille qui porte la cl 13 est illustre dans la gure 4
e
e

15

15

12

20

14
10

16

13

12
21

20

17

14

16

10

21
17

Fig. 4 Suppression de la cl 13 par limination dune feuille.


e
e
La gure 5 illustre la (( remonte )) : le nud s qui porte la cl 16 na quun seul enfant. Cet
e
e
enfant devient lenfant du p`re de s, le nud de cl 20.
e
e
15

15

12

20

14
10

16

13

21
18

17

12

20

14
10

13

18
17

21
19

19

Fig. 5 Suppression de la cl 16 par remonte du ls.


e
e
Le cas dun nud ` deux ls est illustr dans la gure 6. La cl ` supprimer se trouve ` la racine
a
e
ea
a
de larbre. On ne supprime pas le nud, mais seulement sa cl, en remplaant la cl par une
e
c
e

2. ARBRES BINAIRES DE RECHERCHE

121

autre cl. Pour conserver lordre sur les cls, il ny a que deux choix : la cl du prdcesseur dans
e
e
e
e e
lordre inxe, ou la cl du successeur. Nous choisissons la premi`re solution. Ainsi, la cl 14 est
e
e
e
mise ` la racine de larbre. Nous sommes alors ramens au probl`me de la suppression du nud
a
e
e
du prdcesseur et de sa cl. Comme le prdcesseur est le nud le plus ` droite du sous-arbre
e e
e
e e
a
gauche, il na pas de ls droit, donc il a zro ou un ls, et sa suppression est couverte par les
e
deux premiers cas.
15

14

12

20

14
10

13

16

12
21

17

20
13

10

16

21
17

Fig. 6 Suppression de la cl 15 par substitution de la cl 14 et suppression de ce nud.


e
e
Trois mthodes coop`rent pour la suppression. La premi`re, suppression, recherche le nud
e
e
e
portant la cl ` supprimer ; la deuxi`me, suppressionRacine, eectue la suppression selon les cas
ea
e
numrs ci-dessus. La troisi`me, dernierDescendant est une mthode auxiliaire ; elle calcule le
e
ee
e
e
prcdesseur dun nud qui a un ls gauche.
e e
static Arbre supprimer(int x, Arbre a)
{
if (a == null)
return a;
if (x == a.contenu)
return supprimerRacine(a);
if (x < a.contenu)
a.filsG = supprimer(x, a.filsG);
else
a.filsD = supprimer(x, a.filsD);
return a;
}
La mthode suivante supprime la cl de la racine de larbre.
e
e
static Arbre supprimerRacine(Arbre a)
{
if (a.filsG == null)
return a.filsD;
if (a.filsD == null)
return a.filsG;
Arbre f = dernierDescendant(a.filsG);
a.contenu = f.contenu;
a.filsG = supprimer(f.contenu, a.filsG);
}
La derni`re mthode est toute simple :
e
e
static Arbre dernierDescendant(Arbre a)
{
if (a.filsD == null)

122

CHAPITRE V. ARBRES BINAIRES


return a;
return dernierDescendant(a.filsD);
}

La rcursivit croise entre les mthodes supprimer et supprimerRacine est droutante au premier
e
e
e
e
e
abord. En fait, lappel ` supprimer ` la derni`re ligne de supprimerRacine conduit au nud
a
a
e
prdcesseur de la racine de larbre, appel f. Comme ce nud na pas deux ls, il nappelle pas
e e
e
une deuxi`me fois la mthode supprimerRacine...
e
e
Il est intressant de voir une ralisation itrative de la suppression. Elle dmonte enti`rement
e
e
e
e
e
la (( mcanique )) de lalgorithme. En fait, chacune des trois mthodes peut sparment tre crite
e
e
e e
e e
de faon rcursive.
c
e
static Arbre supprimer(int x, Arbre a)
{
Arbre b = a;
while (a != null && x != a.contenu)
if (x < a.contenu)
a = a.filsG;
else
a = a.filsD;
if (a != null)
a = supprimerRacine(a);
return b;
}
Voici la deuxi`me.
e
static Arbre supprimerRacine(Arbre a)
{
if (a.filsG == null)
return a.filsD;
if (a.filsD == null)
return a.filsG;
Arbre b = a.filsG;
if (b.filsD == null)
{ // cas (i)
a.contenu = b.contenu;
a.filsG = b.filsG;
}
else
{ // cas (ii)
Arbre p = avantDernierDescendant(b);
Arbre f = p.filsD;
a.contenu = f.contenu;
p.filsD = f.filsG;
}
return a;
}
Et voici le calcul de lavant-dernier descendant :
static Arbre avantDernierDescendant(Arbre a)
{
while (a.filsD.filsD != null)
a = a.filsD;

2. ARBRES BINAIRES DE RECHERCHE

123

return a;
}
Dcrivons plus prcisment le fonctionnement de la mthode supprimerRacine. La premi`re partie
e
e e
e
e
permet de se ramener au cas o` la racine de larbre a a deux ls. On note b le ls gauche de a,
u
et pour dterminer le prdcesseur de la racine de a, on cherche le nud le plus ` droite dans
e
e e
a
larbre b. Deux cas peuvent se produire :
(i) la racine de b na pas de ls droit, ou
(ii) la racine de b a un ls droit.
Les deux cas sont illustrs sur les gures 7 et 8.
e
a
21

12

b
12

24

22

25

10

24
10

22

23

25
23

Fig. 7 Suppression de la racine, version itrative, cas (i).


e
a
21

20

b
12
8

24
14

10

13

b
12

22
p
16

25
23

f
20

24
14

10

13

22
p
16

25
23

18

18

Fig. 8 Suppression de la racine, version itrative, cas (ii).


e
Dans le premier cas, la cl de la racine de b est transfre ` la racine de a, et b est remplace
e
ee a
e
par son sous-arbre gauche (qui peut dailleurs tre vide). Dans le deuxi`me cas, on cherche
e
e
lavant-dernier descendant, not p, de b sur la branche droite de b, au moyen de la mthode
e
e
avantDernierDescendant. Cela peut tre b lui-mme, ou un de ses descendants (notons que dans
e
e
le cas (i), lavant-dernier descendant nexiste pas, ce qui explique le traitement spar opr dans
e e ee
ce cas). Le sous-arbre droit f de p nest pas vide par dnition. La cl de f est transfre ` la
e
e
ee a
racine de a, et f est remplac par son sous-arbre gauche ce qui fait dispara la racine de f .
e
tre

2.4

Hauteur moyenne

Il est facile de constater, sur nos implantations, que la recherche, linsertion et la suppression
dans un arbre binaire de recherche se font en complexit O(h), o` h est la hauteur de larbre.
e
u

124

CHAPITRE V. ARBRES BINAIRES

Le cas le pire, pour un arbre ` n nuds, est O(n). En ce qui concerne la hauteur moyenne,
a
deux cas sont ` considrer. La premi`re des propositions sapplique aux arbres, la deuxi`me aux
a
e
e
e
permutations.
Proposition 10 Lorsque tous les arbres binaires ` n nuds sont quiprobables, la hauteur
a
e

moyenne dun arbre binaire ` n nuds est en O( n).


a
Proposition 11 Lorsque toutes les permutations de {1, . . . , n} sont quiprobables, la hauteur
e
moyenne dun arbre binaire de recherche obtenu par insertion des entiers dune permutation
dans un arbre initialement vide est O(n log n).
La dirence provient du fait que plusieurs permutations peuvent donner le mme arbre. Par
e
e
exemple les permutations 2, 1, 3 et 2, 3, 1 produisent toutes les deux larbre de la gure 9.

2
1

Fig. 9 Larbre produit par la suite dinsertions 2, 1, 3, ou par la suite 2, 3, 1.

Arbres quilibrs
e
e

Comme nous lavons dj` constat, les cots de la recherche, de linsertion et de la suppression
ea
e
u
dans un arbre binaire de recherche sont de complexit O(h), o` h est la hauteur de larbre. Le cas
e
u
le pire, pour un arbre ` n nuds, est O(n). Ce cas est atteint par des arbres tr`s dsquilibrs,
a
e ee
e
ou (( liformes )). Pour viter que les arbres puissent prendre ces formes, on utilise des oprations
e
e
`
plus ou moins simples, mais peu coteuses en temps, de transformation darbres. A laide de ces
u
transformations on tend ` rendre larbre le plus rgulier possible dans un sens qui est mesur
a
e
e
par un param`tre dpendant en gnral de la hauteur. Une famille darbres satisfaisant une
e
e
e e
condition de rgularit est appele une famille darbres quilibrs. Plusieurs esp`ces de tels
e
e
e
e
e
e
arbres ont t dvelopps, notamment les arbres AVL, les arbres 2-3, les arbres rouge et noir,
ee e
e
ainsi quune myriade de variantes. Dans les langages comme Java ou C++, des modules de
gestion densembles sont prprogramms. Lorsquun ordre total existe sur les lments de ces
e
e
ee
ensembles, ils sont en gnral grs, en interne, par des arbres rouge et noir.
e e
ee

3.1

Arbres AVL

La famille des arbres AVL est nomme ainsi dapr`s leurs inventeurs, Adelson-Velskii et
e
e
Landis, qui les ont prsents en 1962. Au risque de para vieillots, nous dcrivons ces arbres
e
e
tre
e
plus en dtail parce que leur programmation peut tre mene jusquau bout, et parce que les
e
e
e
principes utiliss dans leur gestion se retrouvent dans dautres familles plus complexes.
e
Un arbre binaire est un arbre AVL si, pour tout nud de larbre, les hauteurs des sous-arbres
gauche et droit di`rent dau plus 1.
e
Rappelons quune feuille est un arbre de hauteur 0, et que larbre vide a la hauteur 1.
Larbre vide, et larbre rduit ` une feuille, sont des arbres AVL.
e
a

3. ARBRES EQUILIBRES

125
4
3

2
0

1
1

0
0

0
Fig. 10 Un arbre AVL, avec les hauteurs aux nuds.
Larbre de la gure 10 porte, dans chaque nud, la hauteur de son sous-arbre.
Un autre exemple est fourni par les arbres de Fibonacci, qui sont des arbres binaires An tels
que les sous-arbres gauche et droit de An sont respectivement An1 et An2 . Les premiers arbres
de Fibonacci sont donns dans la gure 11.
e

Fig. 11 Les premiers arbres de Fibonacci.


Lintrt des arbres AVL rsulte du fait que leur hauteur est toujours logarithmique en
ee
e
fonction de la taille de larbre. En dautres termes, la recherche, linsertion et la suppression
(sous rserve dun ventuel rquilibrage) se font en temps logarithmique. Plus prcisment, on
e
e
ee
e e
a la proprit que voici.
ee
Proposition 12 Soit A un arbre AVL ayant n nuds et de hauteur h. Alors
log2 (1 + n)
avec = 1/ log2 ((1 +

5)/2)

1+h

log2 (2 + n)

1.44.

Preuve. On a toujours n 2h+1 1, donc log2 (1 + n)


de nuds dun arbre AVL de hauteur h. Alors

1 + h. Soit N (h) le nombre minimum

N (h) = 1 + N (h 1) + N (h 2)
car un arbre minimal aura un sous-arbre de hauteur h 1 et lautre sous-arbre de hauteur
seulement h2. La suite F (h) = 1+N (h) vrie F (0) = 2, F (1) = 3, F (h+2) = F (h+1)+F (h)
e
pour h 0, donc
1
F (h) = (h+3 (h+3) )
5

1
e
o` = (1 + 5)/2. Il en rsulte que 1 + n F (h) > 5 (h+3 1), soit en passant au logarithme
u

en base , h + 3 < log ( 5(2 + n)) < log2 (2 + n)/ log2 + 2.


Par exemple, pour un arbre AVL qui a 100000 nuds, la hauteur est comprise entre 17 et 25.
Cest le nombre doprations quil faut donc pour rechercher, insrer ou supprimer une donne
e
e
e
dans un tel arbre.

126
3.1.1

CHAPITRE V. ARBRES BINAIRES


Rotations

Nous introduisons maintenant une opration sur les arbres appele rotation. Les rotations
e
e
sont illustres sur la gure 12. Soit A = (B, q, W ) un arbre binaire tel que B = (U, p, V ). La
e
rotation gauche est lopration
e
((U, p, V ), q, W ) (U, p, (V, q, W ))
et la rotation droite est lopration inverse. Les rotations gauche (droite) ne sont donc dnies
e
e
que pour les arbres binaires non vides dont le sous-arbre gauche (resp. droit) nest pas vide.

p
rotation droite

p
W
U

rotation gauche

q
U
V

Fig. 12 Rotation.
Remarquons en passant que pour larbre dune expression arithmtique, si les symboles
e
dopration p et q sont les mmes, les rotations expriment que lopration est associative.
e
e
e
Les rotations ont la proprit de pouvoir tre implantes en temps constant (voir ci-dessous),
ee
e
e
et de prserver lordre inxe. En dautres termes, si A est un arbre binaire de recherche, tout
e
arbre obtenu ` partir de A par une suite de rotations gauche ou droite dun sous-arbre de A
a
reste un arbre binaire de recherche. En revanche, comme le montre la gure 13, la proprit AVL
ee
nest pas conserve par rotation.
e
q

rotation droite
U
W

U
V

Fig. 13 Les rotations ne prservent pas la proprit AVL.


e
ee
Pour remdier ` cela, on consid`re une double rotation qui est en fait compose de deux
e
a
e
e
rotations. La gure 14 dcrit une double rotation droite, et montre comment elle est compose
e
e
dune rotation gauche du sous-arbre gauche suivie dun rotation droite. Plus prcisment, soit
e e
A = ((U, p, (V, q, W )), r, X) un arbre dont le sous-arbre gauche poss`de un sous-arbre droit. La
e
double rotation droite est lopration
e
A = ((U, p, (V, q, W )), r, X) A = ((U, p, V ), q, (W, r, X))
Vrions quelle est bien gale ` la composition de deux rotations. Dabord, une rotation gauche
e
e
a
de B = (U, p, (V, q, W )) donne B = ((U, p, V ), q, W ), et larbre A = (B, r, X) devient A =
(B , r, X) ; la rotation droite de A donne en eet A . On voit quune double rotation droite

3. ARBRES EQUILIBRES

127

diminue la hauteur relative des sous-arbres V et W , et augmente celle de X. La double rotation


gauche est dnie de mani`re symtrique.
e
e
e
r

double rotation
q

droite

U
V

W
r
q
p

rotation gauche

rotation droite

W
U

Fig. 14 Rotations doubles.


3.1.2

Implantation des rotations

Voici une implantation non destructive dune rotation gauche.


static Arbre rotationG(Arbre a)
// non destructive
{
Arbre b = a.filsD;
Arbre c = new Arbre(a.filsG, a.contenu, b.filsG);
return new Arbre(c, b.contenu, b.filsD);
}
La fonction suppose que le sous-arbre gauche, not b, nest pas vide. La rotation gauche dese
tructive est aussi simple ` crire.
ae
static Arbre rotationG(Arbre a)
{
Arbre b = a.filsD;
a.filsD = b.filsG;
b.filsG = a;
return b;
}

// destructive

Les double rotations scrivent par composition.


e
3.1.3

Insertion et suppression dans un arbre AVL

Linsertion et la suppression dans un arbre AVL peuvent transformer larbre en un arbre qui
ne satisfait plus la contrainte sur les hauteurs. Dans la gure 15, un nud portant ltiquette
e
50 est insr dans larbre de gauche. Apr`s insertion, on obtient larbre du milieu qui nest plus
ee
e
AVL. Une double rotation autour de la racine sut ` rquilibrer larbre.
a ee

128

CHAPITRE V. ARBRES BINAIRES


71
44
37

71
83 insertion

61

de 50

44
37

61
83

61

44

double
rotation

37

71
50

83

50
Fig. 15 Insertion suivie dune double rotation.
Cette proprit est gnrale. Apr`s une insertion (respectivement une suppression), il sut
ee
e e
e
de rquilibrer larbre par des rotations ou double rotations le long du chemin qui conduit ` la
ee
a
feuille o` linsertion (respectivement la suppression) a eu lieu. Lalgorithme est le suivant :
u
Algorithme. Soit A un arbre, G et D ses sous-arbres gauche et droit. On suppose que |h(G)
h(D)| = 2. Si h(G) h(D) = 2, on fait une rotation droite, mais prcde dune rotation gauche
e e e
de G si h(g) < h(d) (on note g et d les sous-arbres gauche et droit de G). Si h(G) h(D) = 2
on op`re de faon symtrique.
e
c
e
On peut montrer en exercice quil sut dune seule rotation ou double rotation pour rquiee
librer un arbre AVL apr`s une insertion. Cette proprit nest plus vraie pour une suppression.
e
ee
3.1.4

Implantation : la classe Avl

Pour limplantation, nous munissons chaque nud dun champ supplmentaire qui contient
e
la hauteur de larbre dont il est racine. Pour une feuille par exemple, ce champ a la valeur 0.
Pour larbre vide, qui est reprsent par null et qui nest donc pas un objet, la hauteur vaut 1.
e
e
La mthode H sert ` simplier lacc`s ` la hauteur dun arbre.
e
a
e a
class
{
int
int
Avl

Avl
contenu;
hauteur;
filsG, filsD;

Avl(Avl g, int c, Avl d)


{
filsG = g;
contenu = c;
filsD = d;
hauteur = 1 + Math.max(H(g),H(d));
}
static int H(Avl a)
{
return (a == null) ? -1 : a.hauteur;
}
static void calculerHauteur(Avl a)
{
a.hauteur = 1 + Math.max(H(a.filsG), H(a.filsD));
}
...
}

3. ARBRES EQUILIBRES

129

La mthode calculerHauteur recalcule la hauteur dun arbre ` partir des hauteurs de ses souse
a
arbres. Lusage de H permet de traiter de mani`re unie le cas o` lun de ses sous-arbres
e
e
u
serait larbre vide. Les rotations sont reprises de la section prcdente. On utilise la version non
e e
destructive qui rvalue la hauteur. Ces mthodes et les suivantes font toutes partie de la classe
ee
e
Avl.
static Avl rotationG(Avl a)
{
Avl b = a.filsD;
Avl c = new Avl(a.filsG, a.contenu, b.filsG);
return new Avl(c, b.contenu, b.filsD);
}
La mthode principale implante lalgorithme de rquilibrage expos plus haut.
e
ee
e
static Avl equilibrer(Avl a)
{
a.hauteur = 1 + Math.max(H(a.filsG), H(a.filsD));
if(H(a.filsG) - H(a.filsD) == 2)
{
if (H(a.filsG.filsG) < H(a.filsG.filsD))
a.filsG = rotationG(a.filsG);
return rotationD(a);
} //else version symtrique
e
if (H(a.filsG) - H(a.filsD) == -2)
{
if (H(a.filsD.filsD) < H(a.filsD.filsG))
a.filsD = rotationD(a.filsD);
return rotationG(a);
}
return a;
}
Il reste ` crire les mthodes dinsertion et de suppression, en prenant soin de rquilibrer
a e
e
ee
larbre ` chaque tape. On reprend simplement les mthodes dj` crites pour un arbre binaire
a
e
e
eae
de recherche gnral. Pour linsertion, on obtient
e e
static Avl inserer(int x, Avl a)
{
if (a == null)
return new Avl(null, x, null);
if (x < a.contenu)
a.filsG = inserer(x, a.filsG);
else if (x > a.contenu)
a.filsD = inserer(x, a.filsD);
return equilibrer(a); //seul changement
}
La suppression scrit comme suit
e
static Avl
{
if (a ==
return
if (x ==

supprimer(int x, Avl a)
null)
a;
a.contenu)

130

CHAPITRE V. ARBRES BINAIRES


return supprimerRacine(a);
if (x < a.contenu)
a.filsG = supprimer(x, a.filsG);
else
a.filsD = supprimer(x, a.filsD);
return equilibrer(a); // seul changement
}
static Avl supprimerRacine(Avl a)
{
if (a.filsG == null && a.filsD == null)
return null;
if (a.filsG == null)
return equilibrer(a.filsD);
if (a.filsD == null)
return equilibrer(a.filsG);
Avl b = dernierDescendant(a.filsG);
a.contenu = b.contenu;
a.filsG = supprimer(a.contenu, a.filsG);
return equilibrer(a); // seul changement
}
static Avl dernierDescendant(Avl a) // inchange
e
{
if (a.filsD == null)
return a;
return dernierDescendant(a.filsD);
}

3.2

B-arbres et arbres a-b

Dans cette section, nous dcrivons de mani`re succinte les arbres a-b. Il sagit dune des
e
e
variantes darbres quilibrs qui ont la proprit que toutes leurs feuilles sont au mme niveau,
e
e
ee
e
les nuds internes pouvant avoir un nombre variable de ls (ici entre a et b). Dans cette catgorie
e
darbres, on trouve aussi les B-arbres et en particulier les arbres 2-3-4. Les arbres rouge et noir
(ou bicolores) sont semblables.
Lintrt des arbres quilibrs est quils permettent des modications en temps logarithmique.
ee
e
e
Lorsque lon manipule de tr`s grands volumes de donnes, il survient un autre probl`me, ` savoir
e
e
e
a
lacc`s proprement dit aux donnes. En eet, les donnes ne tiennent pas en mmoire vive, et les
e
e
e
e
donnes sont donc accessibles seulement sur la mmoire de masse, un disque en gnral. Or, un
e
e
e e
seul acc`s disque peut prendre, en moyenne, environ autant de temps que 200 000 instructions.
e
Les B-arbres ou les arbres a-b servent, dans ce contexte, ` minimiser les acc`s au disque.
a
e
Un disque est divis en pages (par exemple de taille 512, 2048, 4092 ou 8192 octets). La
e
page est lunit de transfert entre mmoire centrale et disque. Il est donc rentable de grouper
e
e
les donnes par blocs, et de les manipuler de concert.
e
Les donnes sont en gnral repres par des cls, qui sont ranges dans un arbre. Si chaque
e
e e
ee
e
e
acc`s ` un nud requiert un acc`s disque, on a intrt ` avoir des nuds dont le nombre de ls
e a
e
ee a
est voisin de la taille dune page. De plus, la hauteur dun tel arbre qui mesure le nombre
dacc`s disques ncessaire est alors tr`s faible. En eet, si chaque nud a de lordre de 1000
e
e
e
ls, il sut dun arbre de hauteur 3 pour stocker un milliard de cls.
e
Nous considrons ici des arbres de recherche qui ne sont plus binaires, mais darit plus
e
e
grande. Chaque nud interne dun tel arbre contient, en plus des rfrences vers ses sous-arbres,
ee

3. ARBRES EQUILIBRES

131

des balises, cest-`-dire des valeurs de cl qui permettent de dterminer le sous-arbre o` se


a
e
e
u
trouve linformation cherche. Plus prcisment, si un nud interne poss`de d + 1 sous-arbres
e
e e
e
A0 , . . . , Ad , alors il est muni de d balises k1 , . . . , kd telles que
c0

k1 < c1

kd < cd

pour toute squence de cls (c0 , . . . , cd ), o` chaque ci est une cl du sous-arbre Ai (cf. gure 16).
e
e
u
e
k1

k2

kd

cls
e

A0

cls
e

]kd1 , kd ]

> kd

A1

k1

cls
e

]k1 , k2 ]

cls
e

Ad1

Ad

Fig. 16 Un nud interne muni de balises et ses sous-arbres.


Il en rsulte que pour chercher une cl c, on dtermine le sous-arbre Ai appropri en
e
e
e
e
dterminant lequel des intervalles ] , k1 ], ]k1 , k2 ],. . .,]kd1 , kd ], ]kd , [ contient la cl.
e
e
3.2.1

Arbres a-b

Soient a 2 et b 2a 1 deux entiers. Un arbre a-b est un arbre de recherche tel que
les feuilles ont toutes la mme profondeur,
e
la racine a au moins 2 ls (sauf si larbre est rduit ` sa racine) et au plus b ls,
e
a
les autres nuds internes ont au moins a et au plus b ls.
Les arbres 2-3 sont les arbres obtenus quand a et b prennent leurs valeurs minimales : tout nud
interne a alors 2 ou 3 ls.
Les B-arbres sont comme les arbres a-b avec b = 2a 1 mais avec une interprtation
e
dirente : les informations sont aussi stockes aux nuds internes, alors que, dans les arbres
e
e
que nous considrons, les cls aux nuds internes ne servent qu` la navigation.
e
e
a

8 20 40
6
4

11 12 14
7

10

12

14

21 26 30
18

21

25

30

42
36

41

50

Fig. 17 Un arbre 2-4. Un nud a entre 2 et 4 ls.


Larbre de la gure 17 reprsente un arbre 2-4. Les nuds internes contiennent les balises,
e
et les feuilles contiennent les cls. Lintrt des arbres a-b est justi par la proposition suivante.
e
ee
e
Proposition 13 Si un arbre a-b de hauteur h contient n feuilles, alors
log n/ log b

h < 1 + log(n/2)/ log a .

132

CHAPITRE V. ARBRES BINAIRES

Preuve. Tout nud a au plus b ls. Il y a donc au plus bh feuilles. Tout nud autre que la
racine a au moins a ls, et la racine en a au moins 2. Au total, il y a au moins 2ah1 feuilles,
donc 2ah1 n bh .
Il rsulte de la proposition prcdente que la hauteur dun arbre a-b ayant n feuilles est en
e
e e
O(log n). La complexit des algorithmes dcrit ci-dessous (insertion et suppression) sera donc
e
e
elle aussi en O(log n).
3.2.2

Insertion dans un arbre a-b

La recherche dans un arbre a-b repose sur le mme principe que celle utilise pour les arbres
e
e
binaires de recherche : on parcourt les balises du nud courant pour dterminer le sous-arbre
e
dans lequel il faut poursuivre la recherche. Pour linsertion, on commence par dterminer, par
e
une recherche, lemplacement de la feuille o` linsertion doit avoir lieu. On ins`re alors une
u
e
nouvelle feuille, et une balise approprie : la balise est la plus petite valeur de la cl ` insrer et
e
ea
e
de la cl ` sa droite.
ea
Reste le probl`me du rquilibrage qui se pose lorsque le nombre de ls dun nud dpasse
e
ee
e
le nombre autoris. Si un nud a b + 1 ls, alors il est clat en deux nuds qui se partagent
e
e
e
les ls de mani`re quitable : le premier nud reoit les (b + 1)/2 ls de gauche, le deuxi`me
e e
c
e
les ls de droite. Noter que b + 1 2a, et donc chaque nouveau nud aura au moins a ls. Les
balises sont galement partages, et la balise centrale restante est transmise au nud p`re, pour
e
e
e
quil puisse ` son tour procder ` linsertion des deux ls ` la place du nud clat. Linsertion
a
e
a
a
e
e
12
8

20 40

6
4

11
7

10

14 15
12

14

15

21 26 30
18

21

25

30

42
36

41

50

Fig. 18 Un arbre 2-4. Un nud a entre 2 et 4 ls.


de la cl 15 dans larbre 17 produit larbre de la gure 18. Cette insertion se fait par un double
e
clatement. Dabord, le nud aux balises 11, 12, 14 de larbre 17 est clat en deux. Mais alors,
e
e
e
la racine de larbre 17 a un nud de trop. La racine elle-mme est clate, ce qui fait augmenter
e
e
e
la hauteur de larbre.
Il est clair que linsertion dune nouvelle cl peut au pire faire clater les nuds sur le
e
e
chemin de son lieu dinsertion ` la racine et la racine elle-mme. Le cot est donc born
a
e
u
e
logarithmiquement en fonction du nombre de feuilles dans larbre.
3.2.3

Suppression dans un arbre a-b

Comme dhabitude, la suppression est plus complexe. Il sagit de fusionner un nud avec un
nud fr`re lorsquil na plus assez de ls, cest-`-dire si son nombre de ls descend au dessous
e
a
de a. Mais si son fr`re (gauche ou droit) a beaucoup de ls, la fusion des deux nuds risque de
e
conduire ` un nud qui a trop de ls et quil faut clater. On groupe ces deux oprations sous
a
e
e
la forme dun partage (voir gure 19).

3. ARBRES EQUILIBRES

133

8 20
6
4

8 21

11
7

10

21 26 30
12

21

25

30

6
36

20
7

10

26 30
21

25

30

36

Fig. 19 La suppression de 12 est absorbe par un partage.


e
Plus prcisment, lalgorithme est le suivant :
e e
Supprimer la feuille, puis la balise gurant sur le chemin de la feuille ` la racine.
a
Si les nuds ainsi modis ont toujours a ls, larbre est encore a-b. Si un nud poss`de
e
e
a 1 ls examiner les fr`res adjacents.
e
Si lun des fr`res poss`de au moins a + 1 ls, faire un partage avec ce fr`re.
e
e
e
Sinon, les fr`res adjacents ont a ls, et la fusion avec lun des deux produit un nud
e
ayant 2a 1 b ls.
8 20
6
6

11
7

10

21
13

21

6
25

20 21
7

10

21

25

Fig. 20 La suppression de 13 entra la fusion des deux nuds de droite.


ne
L` encore, le cot est major par un logarithme du nombre de feuilles. Il faut noter quil sagit
a
u
e
du comportement dans le cas le plus dfavorable. On peut en eet dmontrer
e
e
Proposition 14 On consid`re une suite quelconque de n insertions ou suppressions dans un
e
arbre 2-4 initialement vide. Alors le nombre total dclatements, de partage et de fusions est au
e
plus 3n/2.
Ceci signie donc quen moyenne, 1, 5 oprations susent pour rquilibrer larbre, et ce,
e
ee
quel que soit sa taille ! Des rsultats analogues valent pour des valeurs de a et b plus grandes.
e

134

CHAPITRE V. ARBRES BINAIRES

Chapitre VI

Expressions rguli`res
e
e
1

Langages rguliers
e

1.1

Les mots

On se donne un ensemble de caract`res, parfois dnomm alphabet. Lensemble des mots


e
e
e
sur , not , est lensemble des suites nies de caract`res. Un langage est un sous-ensemble
e
e
de , cest-`-dire un ensemble particulier de mots.
a
Par exemple si est lensemble des lettres usuelles, on peut dnir le langage L1 des mots
e
franais. Ou encore si est lensemble des chires de 0 ` 9, on peut dnir le langage L2 des
c
a
e
reprsentations en base 10 des entiers naturels. Dans le premier cas, on peut tout simplement tene
ter de dnir L1 comme lensemble de tous les mots prsents dans le dictionnaire de lAcadmie
e
e
e
franaise, dans le second cas, on peut recourir ` une dnition du style (( un entier (dcimal) est
c
a
e
e
le chire zro, ou une suite non vide de chires qui ne commence pas par zro )). Les expressions
e
e
rguli`res (ou rationnelles) sont une notation tr`s commode pour dnir des langages de mots
e
e
e
e
du style de L1 et L2 ci-dessus.
Dnissons tout de suite quelques notations sur les mots et les langages. Parmi les mots de
e
on distingue le mot vide not . Le mot vide est lunique mot de longueur zro. La concatnation
e
e
e
de deux mots m1 et m2 est le mot obtenu en mettant m1 ` la n de m2 . On note loprateur
a
e
de concatnation, mais on omet souvent cet oprateur et on note donc souvent la concatnation
e
e
e
m1 m2 par une simple juxtaposition m1 m2 . La concatnation est une opration associative qui
e
e
poss`de un lment neutre : le mot vide . Dans lcriture m = m1 m2 , m1 est le prxe du
e
ee
e
e
mot m, tandis que m2 est le suxe du mot m.
La concatnation stend naturellement aux ensembles de mots, on note L1 L2 le langage
e
e
obtenu en concatnant tous les mots de L1 avec les mots de L2 .
e
L1 L2 = {m1 m2 | m1 L1 m2 L2 }
Enn on note L le langage obtenu en concatnant les mots de L.
e
L0 = { }

Ln+1 = Ln L

Li

L =
iN

Cest-`-dire quun mot m de L est la concatnation de n mots (n 0) m1 , . . . mn , o` les mi


a
e
u
sont tous des mots de L.

1.2

Syntaxe des expressions rguli`res


e
e

Les expressions rguli`res ou motifs, notes p, sont dnies ainsi :


e
e
e
e
Le mot vide est une expression rguli`re.
e
e
135


`
CHAPITRE VI. EXPRESSIONS REGULIERES

136

Un caract`re c (un lment de lalphabet ) est une expression rguli`re.


e
ee
e
e
Si p1 et p2 sont des expressions rguli`res, alors lalternative p1 | p2 est une expression
e
e
rguli`re.
e
e
Si p1 et p2 sont des expressions rguli`res, alors la concatnation p1 p2 est une expression
e
e
e
rguli`re.
e
e
Si p est une expression rguli`re, alors la rptition p* est une expression rguli`re.
e
e
e e
e
e
On reconna ici la dnition dun arbre, et mme dun arbre de syntaxe abstraite. Il y a deux
t
e
e
sortes de feuilles, mot vide et caract`res ; deux sortes de nuds binaires, concatnation et altere
e
native ; et une sorte de nud unaire, la rptition. Comme dhabitude pour lever les ambigu es
e e
t
dans lcriture linaire dun arbre, on a recours ` des priorits (` savoir, la rptition est plus
e
e
a
e a
e e
prioritaire que la concatnation, elle-mme plus prioritaire que lalternative) et ` des parenth`ses.
e
e
a
e
Ainsi, pour = {a, b, c}, le motif ab*|ba* est ` comprendre comme ((a)(b*))|((b)(a*)),
a
ou plus informatiquement comme larbre de la gure 1. Pour se souvenir des priorits adoptes
e
e
Fig. 1 Larbre de syntaxe abstraite de lexpression ab*|ba*
|

on peut noter lanalogie avec les expressions arithmtiques : les oprations de concatnation
e
e
e
et dalternative ont les mme priorits respectives que la multiplication et laddition, enn la
e
e
rptition se comporte comme la mise ` la puissance.
e e
a
Dans ce polycopi nous exprimons les expressions rguli`res sous la forme (( programme ))
e
e
e
et non pas dexpression mathmatiques Concr`tement, nous avons crit ab*|ba* et non pas
e
e
e
| ba . Ce parti pris entra
ab
ne que le motif vide ne peut plus tre donn par , si ncessaire
e
e
e
nous le reprsenterons donc par exemple comme ().
e
Avec un peu dhabitude, on comprend nos critures comme des arbres de syntaxe abstraites.
e
Cela l`ve toutes les ambigu es dentre de jeu. On doit sans doute remarquer quen ralit, nos
e
t
e
e e
priorits ne l`vent pas toutes les ambigu es. En eet, on peut comprendre abc comme a(bc) ou
e
e
t
comme (ab)c, cest-`-dire comme larbre de gauche ou comme larbre de droite de la gure 2.
a
Au fond cette ambigu e na ici aucune importance, car lopration de concatnation des mots
t
e
e
est associative. Il en rsulte que la concatnation des motifs est galement associative. Comme
e
e
e
Fig. 2 Deux arbres possibles pour abc

a
b

c
b

nous allons le voir immdiatement, il en va de mme pour lalternative.


e
e


1. LANGAGES REGULIERS

1.3

137

Smantique des expressions rguli`res


e
e
e

Une expression arithmtique a une valeur (plus gnralement on dit aussi une smantique) :
e
e e
e
cest tout simplement lentier rsultat du calcul. De mme, une expression rguli`re a une valeur
e
e
e
e
qui est ici un ensemble de mots. Il est logique que la dnition de la smantique reprenne
e
e
la structure de la dnition des expressions rguli`res. Cest tout simplement une dnition
e
e
e
e
inductive, qui ` chaque expression rguli`re p associe un sous-ensemble [[p]] de .
a
e
e
[[]]
[[c]]
[[p1 | p2 ]]
[[p1 p2 ]]
[[p*]]

=
=
=
=
=

{}
{c}
[[p1 ]] [[p2 ]]
[[p1 ]] [[p2 ]]
[[p]]

Cette dnition a le parfum usuel des dnitions de smantique, on a presque rien crit en fait !
e
e
e
e
Tout a dj` t dit ou presque dans la section 1.1 sur les mots. Lorsque m appartient au langage
eae e
dni par p, on dit aussi que le motif p ltre le mot m. Un langage qui peut tre dni comme
e
e
e
la valeur dune expression rguli`re est dit langage rgulier.
e
e
e
Exemple 1 Nous savons donc que le langage des mots du franais selon lAcadmie est rgulier,
c
e
e
puisque quil est dni par une grosse alternative de tous les mots du dictionnaire publi par
e
e
cette institution. Les reprsentations dcimales des entiers sont galement un langage rgulier
e
e
e
e
dni par :
e
0|(1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)*
Exercice 1 Montrer quun langage qui contient un nombre ni de mots est rgulier.
e
Solution. Si L = {m1 , m2 , . . . , mn } est un langage ni contenant n mots, alors L est exactement
dcrit par lalternative de tous les mi .
e

1.4

Filtrage

La relation de ltrage m [[p ]], galement note p m, peut tre dnie directement par les
e
e
e
e
r`gles de la gure 3. La dnition de p m est faite selon le formalisme des r`gles dinfrence,
e
e
e
e
Fig. 3 Dnition de la relation p ltre m
e

Empty

Seq

Char

p1

p1 p2

StarEmpty

p*

OrRight

OrLeft

m1

p2

m2

p1

m1 m2

p2

p1 | p2

m1

p*

m2

p*

p1 | p2

m
m

m1 m2

StarSeq

qui est tr`s courant. Il permet la dnition inductive dun prdicat. Les r`gles se dcomposent
e
e
e
e
e
en axiomes (par ex. Empty) qui sont les cas de base de la dnition, et en r`gles dinfrence
e
e
e
proprement dites qui sont les cas inductifs de la dnition. Les r`gles sont ` comprendre comme
e
e
a


`
CHAPITRE VI. EXPRESSIONS REGULIERES

138

des implications : quand toutes les prmisses (au dessus du trait) sont vraies, alors la conclusion
e
(au dessous du trait) est vraie. En encha
nant les r`gles on obtient une preuve eective du
e
prdicat, appele arbre de drivation. Voici par exemple la preuve de ab*|ba* baa.
e
e
e
a
a
b

a
a*

b
ba*
ab*|ba*

a*

a* aa
baa

baa

Nous disposons donc de deux moyens dexprimer un langage rgulier, m [[p]] ou p m. Selon
e
les circonstances, il est plus facile demployer lun ou lautre de des moyens. Par exemple, pour
montrer quun motif ltre un mot, les r`gles dinfrences sont souvent commodes. En revanche,
e
e
pour montrer des galits de langages rguliers, il est souvent plus commode de se servir des
e
e
e
oprations sur les langages.
e
Exercice 2 Prouver que pour tout motif p, p* et p** dnissent le mme langage.
e
e
Solution. Pour tout langage on a (L ) = L par dnition de lopration de rptition sur les
e
e
e e
langages. Prouver lquivalence p* m p** m est plus pnible.
e
e

Notations supplmentaires
e

En pratique on rencontre diverses notations qui peuvent toutes tre exprimes ` laide des
e
e a
constructions de base des expressions rguli`res. Autrement dit, lemploi de ces notations nauge
e
mente pas la classe des langages rguliers, mais permet simplement dcrire des expressions
e
e
rguli`res plus compactes.
e
e
Le motif optionnel p ? dni comme p|.
e
La rptition au moins une fois p+ dnie comme pp*.
e e
e
Le joker, not ., qui est lalternative de tous les caract`res de . On a donc [[.]] = .
e
e
Exemple 2 Ainsi une notation plus conforme ` ce que les langages informatiques acceptent en
a
fait dentiers dcimaux est :
e
(0|1|2|3|4|5|6|7|8|9)+
Cest-`-dire une suite non-vide de chires dcimaux, sans chercher ` viter dventuels zros
a
e
a e
e
e
initiaux. Une notation possible pour les entiers relatifs est
- ?(0|1|2|3|4|5|6|7|8|9)+
Cest-`-dire un entier naturel prcd ventuellement dun signe moins.
a
e e ee
Il existe galement toute une varit de notations pour des ensembles de caract`res. Ces motifs
e
ee
e
sont des abrviations de lalternative notes entre crochets [. . .]. Donc, au sein de ces crochets,
e
e
on trouve une suite des classes de caract`res suivantes :
e
Un caract`re c se reprsente lui-mme.
e
e
e
Un intervalle c1 - c2 reprsente les caract`res dont les codes sont compris au sens large
e
e
entre les codes de c1 et c2 .


2. NOTATIONS SUPPLEMENTAIRES

139

La notation [^. . .] se comprend comme le complmentaire des classes de caract`res donnes. Il


e
e
e
est clair que les notations de classes de caract`res peuvent sexprimer comme des alternatives
e
de motifs caract`res. Dans le cas du complmentaire, cela suppose un alphabet ni, ce qui est
e
e
bien videmment le cas en pratique.
e
Exemple 3 Une notation compacte pour les entiers dcimaux est [0-9]+. Cela fonctionne parce
e
que dans tous les codages raisonnables les codes des chires sont conscutifs. De mme, puisquen
e
e
Java par exemple (en ASCII en fait) les codes des lettres sont conscutifs, lexpression rguli`re
e
e
e
[a-zA-Z] reprsente toutes les lettres minuscules et majuscules (non-accentues), Tandis que
e
e
[^a-zA-Z] reprsente tous les caract`res qui ne sont pas des lettres.
e
e
Exercice 3 Donner une expression rguli`re compacte qui dcrit la notation adopte pour les
e
e
e
e
` savoir, notation dcimale (sans zro initiaux), hexadcimale (introduite par
entiers par Java. A
e
e
e
0x, les chires de 10 ` 15 tant reprsentes par les lettres minuscules ou majuscules de a ` f),
a
e
e
a
et notation octale (introduite par un zro).
e
Solution. Il y a peu ` dire ` part :
a
a
(0|[1-9][0-9]*)|0x[0-9a-fA-F]+|0[0-7]+
On note que, selon lexpression donne, 0 est dcimal, 00 octal, et 09 interdit.
e
e
Enn certains caract`res spciaux sont exprimables ` laide de notations assez usuelles, du
e
e
a
style \n est le caract`re n de ligne et \t est la tabulation etc. Le caract`re barre oblique
e
e
inverse (backslash) introduit les notations de caract`res spciaux, mais permet aussi dexprimer
e
e
les caract`res actifs des expressions rguli`res comme eux-mmes, cest ` dire de les citer. Par
e
e
e
e
a
exemple le caract`re toile se note \* et, bien videmment, le caract`re barre oblique inverse se
e e
e
e
note \\. Il nest pas lusage de citer les caract`res quand cest inutile, ainsi [^*] reprsente tous
e
e
les caract`res sauf ltoile.
e
e
Exemple 4 Nous sommes dsormais en possession dun langage de description des ensembles de
e
mots assez puissant. Par exemple voici le motif qui dcrit une des deux sortes de commentaires
e
Java.
//[^\n]*\n
Cest-` dire quun commentaire commence par deux / et se poursuit jusqu` la n de la ligne
a
a
courante. Notons que le motif //.*\n ne convient pas, car il ltre par exemple :
//Commentaire avant
i++ ;
Exercice 4 Donner une expression rguli`re qui dcrit lautre sorte de commentaires de Java.
e
e
e
`
A savoir, un commentaire commence par /* et stend jusqu` */.
e
a
Solution. On vise un motif de la forme /\*p\*/.Autrement dit, les commentaires commencent
par le sous-mot /* et se terminent par le sous-mot */, et, comme on doit identier le premier
sous-mot */ pour fermer le commentaire, le motif p doit correspondre au langage P de tous les
mots dont */ nest pas un sous-mot. Notons tout de suite quun mot de P peut se terminer par
une suite dtoiles, on a donc p = q\**. Soit Q le langage du motif q, en dnissant Q comme
e
e
lensemble des mots dont aucun sous-mot nest */ et qui ne se terminent pas par une suite
non-vide dtoiles. Nous donnons ensuite ` q la forme q = r*, cest un moyen simple de prendre
e
a


`
CHAPITRE VI. EXPRESSIONS REGULIERES

140

en compte que Q contient des mots de longueur arbitraire. Il faut maintenant decomposer un
mot (non-vide) m comme un prxe appartenant ` R (langage de r) et un suxe m , tels que
e
a
m est dans Q, si et seulement si m est dans Q.
Si m commence par un caract`re c qui nest pas ltoile, alors m se dcompose facilement
e
e
e
, et lquivalence est vidente.
comme m = cm
e
e
Si m commence par une toile. Commenons par supposer m Q, alors cette toile peut
e
c
e
tre suivie dautres, et obligatoirement dun caract`re qui nest ni * ni /, ensuite on trouve
e
e
le suxe m . Autrement dit, on pose un prxe dcrit par \*+[^*/]. Il est alors clair que
e
e
m Q. Rciproquement si, m Q, alors le dernier caract`re du prxe nest pas une
e
e
e
toile, et on ne peut pas introduire le sous-mot */ en y ajoutant m , mme si m commence
e
e
par /.
Soit r = [^*]|\*+[^*/]. Au nal, la solution est :
/\*([^*]|\*+[^*/])*\*+/
La syntaxe concr`te est celle introduite dans les sections prcdentes. Larbre de syntaxe abstraite
e
e e
est donn ` la gure 4.
ea
Fig. 4 Syntaxe abstraite de /\*([^*]|\*+[^*/])*\*+/

\*

\*

[^*]
+

[^*/]

\*

Programmation avec les expressions rguli`res


e
e

Dans cette section nous nous livrons ` une description rapide de quelques outils qui emploient
a
les expressions rguli`res de faon cruciale. Le sujet est extrmement vaste, car le formalisme
e
e
c
e
des expressions rguli`res est susamment expressif pour permettre de nombreuses recherches
e
e
dans les chiers textes, voire de nombreuses transformations de ces chiers. Le livre de Jerey
E. F. Friedl [3] traite des expressions rguli`re du point de vue des outils.
e
e


`
3. PROGRAMMATION AVEC LES EXPRESSIONS REGULIERES

3.1

141

Dans linterpr`te de commandes Unix


e

Le shell cest ` dire linterpr`te de commandes Unix reconna quelques expressions rguli`res
a
e
t
e
e
quil applique aux noms des chiers du rpertoire courant. La syntaxe concr`te des motifs est
e
e
franchement particuli`re. Notons simplement que ? reprsente un caract`re quelconque (not
e
e
e
e
prcdemment .), tandis que * reprsente un mot quelconque (not prcdemment .*), et que
e e
e
e e e
lalternative se note ` peu pr`s {p1 ,p2 } (pour p1 | p2 ). . .
a
e
Ainsi, on pourra eacer tous les chiers objets Java par la commande :
% /bin/rm *.class
Dans le mme ordre dide, on peut compter toutes les lignes des chiers source du rpertoire
e
e
e
courant :
% wc -l *.java
La commande wc1 (pour word count) compte les lignes, mots et caract`res dans les chiers
e
donns en argument. Loption (( -l )) restreint lachage au seul compte des lignes. Enn, on
e
peut faire la liste de tous les chiers du rpertoire courant dont le nom comprend de un ` trois
e
a
caract`res :
e
% ls {?,??,???}

3.2

Recherche de lignes dans un chier, la commande egrep

La commande egrep motif chier ache toutes les lignes de chier dont motif ltre un
sous-mot (et non pas la ligne enti`re). La syntaxe des motifs est relativement conforme ` ce que
e
a
nous avons dj` dcrit. Supposons, comme cest souvent le cas, que le chier /usr/share/dict/
ea e
french de votre machine Unix est un dictionnaire franais, donn sous forme dun chier texte,
c
e
a
` raison dun mot par ligne. On peut alors trouver les mots qui contiennent au moins six fois la
lettre (( i )) de cette mani`re.
e
% egrep i.*i.*i.*i.*i.*i /usr/share/dict/french
indivisibilit
e
On notera que largument motif est donn entre simples quotes (( )), ceci an dviter que le
e
e
shell ninterpr`te les toiles comme faisant partie dun motif ` appliquer aux noms de chier. Ce
e
e
a
nest en fait pas toujours ncessaire, mais cest toujours prudent.
e

3.3

En Java

Le support pour les expressions rguli`res est assur par les classes Pattern et Matcher
e
e
e
du package java.util.regexp, Les objets de la classe Pattern implmentent les motifs. et
e
sont construits par la mthode statique Pattern.compile qui prend une cha reprsentant
e
ne
e
un motif en argument et renvoie un Pattern. On pourrait penser que la mthode compile
e
interpr`te la cha donne en argument et produit un arbre de syntaxe abstraite. En fait,
e
ne
e
par souci decacit, elle proc`de ` bien plus de travail, jusqu` produire un automate (voir le
e
e a
a
chapitre suivant). Pour le moment, il sut de considrer quun Pattern est la forme Java dun
e
motif.
Pour confronter un Pattern p ` un mot m il faut fabriquer cette fois un Matcher en invoa
quant la mthode matcher(String text) de p, largument text tant le mot m. En simpliant,
e
e
le Matcher obtenu est donc la combinaison dun motif p et dun mot m, il ore (entre autres !)
1

On obtient le dtail du fonctionnement dune commande Unix cmd par man cmd.
e

142

`
CHAPITRE VI. EXPRESSIONS REGULIERES

les mthodes matches() et find() qui renvoient des boolens. La premi`re mthode matches
e
e
e
e
teste si le motif p ltre le mot m, tandis que la seconde find teste si le motif p ltre un sous-mot
du mot m. Ainsi, par exemple, un moyen assez compliqu de savoir si un mot mot contient le
e
sous-mot sous au moins deux fois est dcrire :
e
static boolean estSousMotDeuxFois(String sous, String mot) {
return Pattern.compile(sous + ".*" + sous).matcher.().find(mot) ;
}
Nous en savons maintenant assez pour pouvoir crire la commande egrep en Java, la
e
classe Grep dont le code est donn ` la gure 5. Dans ce code, la mthode main se come a
e
porte principalement ainsi : elle ouvre le chier dont le nom est donn comme second argument
e
de la ligne de commande, par new FileReader(arg[1]) ; puis enveloppe le chier comme le
BueredReader (voir B.6.2.2) in, ceci an de pouvoir le lire ligne ` ligne ; enn le code appelle
a
la mthode grep. Cette derni`re, apr`s construction du Pattern pat, lapplique ` toutes les
e
e
e
a
lignes du chier. En cas de succ`s de lappel ` find, la ligne est ache sur la sortie standard.
e
a
e
Le comportement global est donc bien celui de la commande egrep.
Larchitecture du package java.util.regex peut para bien complique, et cest tout `
tre
e
a
vrai. Mais. . .
Lexistence de la classe Pattern se justie dabord par le souci dabstraction : les auteurs
ne souhaitent pas exposer comment sont implments les motifs, an de pouvoir changer
e
e
cette implmentation dans les versions futures de Java. Il y a galement un souci decae
e
cit, la transformation des cha
e
nes vers les motifs est coteuse et on souhaite la rentabiliser.
u
Par exemple dans notre Grep (gure 5), nous appelons Pattern.compile une seule fois
et pratiquons de nombreux ltrages.
Lexistence de la classe Matcher sexplique autrement : les Matcher poss`dent un tat
e
e
interne que les mthodes de ltrage modient. Ceci permet dabord dobtenir des informae
tions supplmentaires. Par exemple, si find russit, alors on obtient le sous-mot ltr en
e
e
e
appelant la mthode group(). Par ailleurs, lappel suivant ` find recherchera un sous-mot
e
a
ltr, non plus ` partir du dbut, mais au del` du dernier sous-mot identi par find.
e
a
e
a
e
Tout cela permet par exemple dextraire tous les entiers prsents dans une cha
e
ne.
static void allInts(String text) {
Matcher m = Pattern.compile("[0-9]+").matcher(text) ;
while (m.find()) {
System.out.println(m.group()) ;
}
}
On notera quil nest pas immdiat que le code ci-dessus ache bien tous les entiers de
e
la cha text. En eet si text est par exemple "12" les deux achages 12, ou encore 1
ne
puis 2 sont corrects : il nachent que des sous-mots ltrs par le motif "[0-9]+".
e
Plus gnralement, spcier compl`tement ce que fait le couple de mthode find/group
e e
e
e
e
est un peu dlicat. La solution ` mon avis la plus satisfaisante est spcier que le sous-mot
e
a
e
ltr est dabord le plus ` gauche et ensuite le le plus long. La spcication de Java nest
e
a
e
pas aussi gnrale : au lieu de dire globalement ce qui doit tre ltr, elle dcrit leet
e e
e
e
e
de chaque oprateur des expressions rguli`re individuellement. Ici elle dit que loprateur
e
e
e
e
(( + )) est avide (greedy), cest ` dire quil ltre le plus de caract`res possibles. Dans le cas
a
e
de lexpression rguli`re simple "[0-9]+", cela revient en eet ` ltrer les sous-mots les
e
e
a
plus longs.
Nous ne dcrirons pas plus en dtail les expressions rguli`res de Java. La documentation du
e
e
e
e
langage ore de nombreuses prcisions. En particulier, la documentation de la classe Pattern
e
comprend la description compl`te de la syntaxe des motifs qui est plus tendue que ce que nous
e
e


`
3. PROGRAMMATION AVEC LES EXPRESSIONS REGULIERES

Fig. 5 La commande egrep en Java, source Grep.java


import java.io.* ;
// Pour BufferedReader
import java.util.regex.* ; // Pour Pattern et Matcher
class Grep {
// Affiche les lignes de in dont un sous-mot est filtr par le motif p
e
static void grep(String p, BufferedReader in) throws IOException {
Pattern pat = Pattern.compile(p) ;
String line = in.readLine() ;
while (line != null) {
Matcher m = pat.matcher(line) ;
i f (m.find()) {
System.out.println(line) ;
}
line = in.readLine() ;
}
}
public static void main(String [] arg) {
i f (arg.length != 2) {
System.err.println("Usage: java Grep motif fichier") ;
System.exit(2) ;
}
try {
BufferedReader in = new BufferedReader (new FileReader (arg[1])) ;
grep(arg[0], in) ;
in.close() ;
} catch (IOException e) {
System.err.println("Malaise: " + e.getMessage()) ;
System.exit(2) ;
}
}
}

143


`
CHAPITRE VI. EXPRESSIONS REGULIERES

144

avons vu ici. Attention tout de mmes les descriptions de motifs sont donnes dans labsolu, alors
e
e
que les motifs sont souvent en pratique dans des cha
nes. If faut donc en plus tenir compte des
r`gles de citation dans les cha
e
nes. Ainsi le motif \p{L} ltre nimporte quelle lettre (y compris
les lettres accentues) mais on le donne sous la forme de la cha "\\p{L}", car il faut citer un
e
ne
backslash avec un backslash !

Implmentation des expressions rguli`res


e
e
e

Le but de cette section est de dcrire une technique dimplmentation possible des exprese
e
sions rguli`res en Java. Il sagit dune premi`re approche, beaucoup moins sophistique que
e
e
e
e
celle adopte notamment par la biblioth`que Java. Toutefois, on pourra, mme avec des teche
e
e
niques simples, dj` aborder les probl`mes de programmation poss et comprendre (( comment
ea
e
e
c
a marche )). De fait nous allons imiter larchitecture du package java.util.regexp et crire
e
nous aussi un package que nous appelons regex tout court.
Nous en protons donc pour crire un package. Tous les chiers source du package regex
e
commencent par la ligne package regex ; qui identie leur classe comme appartenant ` ce
a
package. En outre, il est pratique de regrouper ces chiers dans un sous-rpertoire nomm
e
e
justement regex.

4.1

Arbres de syntaxe abstraite

`
Nous reprenons les techniques de la section IV.4 sur les arbres de syntaxe abstraite. A savoir
nous dnissons une classe Re des nuds de larbre de syntaxe abstraite.
e
package regex ;
class Re {
private final static int

EMPTY=0, CHAR=1, WILD=2, OR=3, SEQ=4, STAR=5 ;

private int tag ;


private char asChar ;
private Re p1, p2 ;
private Re() {}
.
.
.
}
Nous dnissons cinq sortes de nuds, la sorte dun nud tant identie par son champ tag.
e
e
e
Des constantes nommes identient les cinq sortes de nuds. La correspondance entre constante
e
e
et sorte de nud est directe, on note la prsence de nuds (( WILD )) qui reprsentent les jokers.
e
Ensuite nous dnissons tous les champs ncessaires, un champ asChar utile quand le motif est
e
e
un caract`re (tag CHAR), et deux champs p1 et p2 utiles pour les nuds internes qui ont au plus
e
deux ls. Enn, le constructeur par dfaut est redni et dclar priv.
e
e
e
e
e
On construira les divers nuds en appelant des mthodes statiques bien nommes. Par
e
e
exemple, pour crer un motif caract`re, on appelle :
e
e
static Re charPat(char c) { // On ne peut pas nommer cette mthode (( char ))
e
Re r = new Re() ;
r.asChar = c ;
return r ;
}
Pour crer un motif rptition, on appelle :
e
e e

`
4. IMPLEMENTATION DES EXPRESSIONS REGULIERES

145

static Re star(Re p) {
Re r = new Re() ;
r.p1 = p ;
return p ;
}
Les autres autres mthodes de construction sont videntes.
e
e
Les mthodes statiques de construction ne se limitent videmment pas ` celles qui correse
e
a
pondent aux sortes de nuds existantes. On peut par exemple crire facilement une mthode
e
e
plus qui construit un motif p+ comme pp*.
static Re plus(Re p) {
return seq(p, star(p)) ;
}
Du point de vue de larchitecture, on peut remarquer que tous les champs et le constructeur sont privs. Rendre le constructeur priv oblige les utilisateurs de la classe Re appeler les
e
e
mthodes statiques de construction, de sorte quil est garanti que tous les champs utiles dans un
e
nud sont correctement initialiss. Rendre les champs privs interdira leur acc`s de lextrieur
e
e
e
e
e
e
de la classe Re. Au nal, la politique de visibilit des noms est tr`s stricte. Elle renforce la
scurit de la programmation, puisque si nous ne modions pas les champs dans la classe Re,
e
e
nous pourrons tre srs que personne ne le fait. En outre, la classe Re nest pas publique, son
e
u
e
acc`s est donc limit aux autres classes du package regex. La classe Re est donc compl`tement
e
e
invisible pour les utilisateurs du package.

4.2

Fabrication des expressions rguli`res


e
e

Nous prsentons maintenant notre classe Pattern , un modeste remplacement de la classe hoe
monyme de la biblioth`que Java. Pour le moment nous vitons les automates et nous contentons
e
e
de cacher un arbre Re dans un objet de la classe Pattern .
package regex ;
/* Une classe Pattern simple : encapsulage dun arbre de syntaxe abstraite */
public class Pattern {
private Re pat ;
private Pattern(Re pat) { this.pat = pat ; }
// Cha^ne -> Pattern

public static Pattern compile(String patString) {


Re re = Re.parse(patString) ;
return new Pattern(re) ;
}
// Fabriquer le M atcher
public Matcher matcher(String text) { return new Matcher(pat, text) ; }
}
Comme dans la classe de la biblioth`que, cest la mthode statique compile qui appelle le
e
e
constructeur, ici priv. La partie la plus technique de la tche de la mthode compile est
e
a
e
le passage de la syntaxe concr`te contenue dans la cha patString ` la syntaxe abstraite
e
ne
a
e
ee e a
e
reprsent par un arbre Re, opration dlgue ` la mthode Re.parse. Nous ne savons pas
e
e
crire cette mthode danalyse syntaxique (parsing). (cours INF 431). Mais ne soyons pas dcus,
e
e
e


`
CHAPITRE VI. EXPRESSIONS REGULIERES

146

nous pouvons dj` par exemple construire le motif qui reconna au moins k caract`res c, en
ea
t
e
appelant la mthode atLeast suivante, ` ajouter dans la classe Pattern .
e
a
public static Pattern atLeast(int k, char c) {
return new Pattern(buildAtLeast(k, c)) ;
}
private static Re buildAtLeast(int k, char c) {
i f (k <= 0) {
return Re.empty() ;
} else i f (k == 1) {
return Re.charPat(c) ;
} else {
return
Re.seq
(Re.charPat(c),
Re.seq(Re.star(Re.wild()), buildAtLeast(k-1, c)))
}
}
Enn, la mthode matcher de de la classe Pattern se contente dappeler le constructeur de
e
e
notre modeste classe Matcher, que nous allons dcrire.

4.3

Filtrage

Le source de la classe Matcher (gure 6) indique que les objets contiennent deux champs
pat et text, pour le motif et le texte a ltrer. Comme on pouvait sy attendre, le constructeur
`
Matcher(Re pat, String text) initialise ces deux champs. Mais les objets comportent trois
champs supplmentaires, mStart, mEnd et regStart.
e
La valeur du champ regStart indique lindice dans text du dbut de la recherche suivante,
e
cest-`-dire o` la mthode find doit commencer ` chercher une sous-cha ltre par pat.
a
u
e
a
ne
e
Ce champ permet donc aux appels successifs de find de communiquer entre eux.
Les champs mStart et mEnd identient la position de la derni`re sous-cha de text dont
e
ne
un appel ` find a dtermin que le motif pat la ltrait. La convention adopte est celle
a
e
e
e
de la mthode substring des objets String (voir la section B.6.1.3). Les deux champs
e
servent ` la communication entre un appel ` find et un appel subsquent ` group (voir
a
a
e
a
la n de la section 3.3).
La mthode find est la plus intressante, elle cherche ` identier une sous-cha ltre par
e
e
a
ne
e
pat, ` partir de la position regStart et de la gauche vers la droite. La technique adopte est
a
e
franchement na
ve, on essaie tout simplement de ltrer successivement toutes les sous-cha
nes
commenant ` une positon donne (start) des plus longues ` la cha vide. On renvoie true
c
a
e
a
ne
(apr`s mise ` jour de ltat du Matcher), d`s quune sous-cha ltre est trouve. Pour savoir si
e
a
e
e
ne
e
e
une sous-cha text[start. . .end[ est ltre, on fait appel ` la mthode statique Re.matches.
ne
e
a
e
Notons que cest notre parti-pris de rendre privs tous les champs de larbre de syntaxe des
e
expressions rguli`re qui oblige ` crire toute mthode qui a besoin dexaminer cette structure
e
e
ae
e
comme une mthode de la classe Re.
e

Exercice 5 Ecrire la mthode matches de la classe Matcher. On suivra la spcication de la


e
e
` savoir, lappel matches() teste le ltrage de toute lentre
classe Matcher de la biblioth`que. A
e
e
par le motif et on peut utiliser group() pour retrouver la cha ltre.
ne
e
Solution. Cest simple : un appel ` Re.matches et on aecte les champs mStart et mEnd selon
a
le rsultat.
e

`
4. IMPLEMENTATION DES EXPRESSIONS REGULIERES

Fig. 6 Notre classe Matcher


package regex ;
public class Matcher {
private Re pat ;
private String text ;
// Les recherches commencent a cette position dans text
`
private int regStart ;
// La derni`re sous-cha^ne filtre est text[mStart...mEnd[
e

e
private int mStart, mEnd ;
Matcher(Re
this.pat
regStart
mStart =
}

pat, String text) {


= pat ; this.text = text ;
// Commencer ` filtrer ` partir du dbut
a
a
e
= 0 ;
mEnd = -1 ; // Aucun motif encore reconnu

// Renvoie la derni`re sous-cha^ne filtre, si il y a lieu


e

e
public String group() {

e
i f (mStart == -1) throw new Error("Pas de sous-cha^ne filtre") ;
return text.substring(mStart, mEnd) ;
}
// Mthode de recherche des sous-cha^nes filtres a peu pr`s
e

e
e
// conforme ` celle des Matcher de java.util.regex
a
public boolean find() {
for (int start = regStart ; start <= text.length() ; start++)
for (int end = text.length() ; end >= start ; end--) {
i f (Re.matches(text, pat, start, end)) {
mStart = start ; mEnd = end ;
e
regStart = mEnd ; // Le prochain find commencera apr`s celui-ci
return true ;
}
}

mStart = mEnd = -1 ; // Pas de sous-cha^ne reconnue


regStart = 0 ;
// Recommencer au dbut, bizarre
e
return false ;
}
}

147

148

`
CHAPITRE VI. EXPRESSIONS REGULIERES

public boolean matches() {


i f (Re.matches(text, pat, 0, text.length())) {
mStart = 0 ;
mEnd = text.length() ;
return true ;
} else {
mStart = mEnd = -1 ;
return false ;
}
}

Pour crire la mthode matches de la classe Re, nous allons distinguer les divers motifs
e
e
possibles et suivre la dnition de p m de la gure 3.
e
// Test de pat
text[i. . .j[
static boolean matches(String text, Re pat, int i, int j) {
switch (pat.tag) {
.
.
.
}
throw new Error ("Arbre Re incorrect") ;
}
Notons bien que text[i. . .j[ est la cha dont nous cherchons ` savoir si elle est ltre par
ne
a
e
pat. La longueur de cette cha est j-i. Nous crivons maintenant le source du traitement des
ne
e
cinq sortes de motifs possibles, cest ` dire la liste des cas du switch ci-dessus. Le cas des motifs
a
vide, des caract`res et du joker est rapidement rgl.
e
e e
case EMPTY:
return i == j ;
case CHAR:
return i+1 == j && text.charAt(i) == pat.asChar ;
case WILD:
return i+1 == j ;
En eet, le motif vide ltre la cha vide et elle seule (j i = 0), le motif caract`re ne ltre
ne
e
que la cha compose de lui mme une fois, et le joker ltre toutes les cha
ne
e
e
nes de longueur un.
Le cas de lalternative est galement assez simple, il sut dessayer les deux termes de
e
lalternative (regles OrLeft et OrRight).
case OR:
return matches(text, pat.p1, i, j) || matches(text, pat.p2, i, j) ;
La squence (rule Seq) demande plus de travail. En eet il faut essayer toutes les dcompositions
e
e
en prxe et suxe de la cha teste, faute de quoi nous ne pourrions pas renvoyer false avec
e
ne
e
certitude.
case SEQ:
for (int k = i ; k <= j ; k++) {
i f (matches(text, pat.p1, i, k) && matches(text, pat.p2, k, j))
return true ;
}
return false ;z
Et enn, le cas de la rptition q* est un peu plus subtil, il est dabord clair (r`gle StarEmpty)
e e
e
quun motif q* ltre toujours la cha vide. Si la cha text[i. . .j[ est non-vide alors on
ne
ne
cherche ` la dcomposer en prxe et suxe et ` appliquer la r`gle StarSeq.
a
e
e
a
e

`
4. IMPLEMENTATION DES EXPRESSIONS REGULIERES

149

case STAR:
i f (i == j) {
return true ;
} else {
for (int k = i+1 ; k <= j ; k++) {
i f (matches(text, pat.p1, i, k) && matches(text, pat, k, j))
return true ;
}
return false ;
}
On note un point un peu subtil, dans le cas dune cha non-vide, on vite le cas k = j
ne
e
qui correspond ` une division de la cha teste en prxe vide et suxe complet. Si tel ntait
a
ne
e
e
e
pas le cas, la mthode matches pourrait ne pas terminer. En eet, le second appel rcursif
e
e
matches(text, pat, k, j) aurait alors les mmes arguments que lors de lappel. Un autre
e
point de vue est de considrer que lapplication de la r`gle StarSeq ` ce cas est inutile, dans le
e
e
a
sens quon ne risque pas de ne pas pouvoir prouver q* m parce que lon abstient de lemployer.
q

q*

q*

Linutilit de cette r`gle est particuli`rement agrante, puisquune des prmisses et la conclusion
e
e
e
e
sont identiques.

4.4

Emploi de notre package regex

Nos classes Pattern et Matcher sont susamment proches de celles de la biblioth`que pour
e
que lon puisse, dans le source Grep.java (gure 5), changer la ligne import java.util.regex.*
en import regex.*, ce qui nous donne le nouveau source ReGrep.java. D`s lors, ` condition
e
a
que le source des classes du package regex se trouve dans un sous-rpertoire regex, nous poue
vons compiler par javac ReGrep.java et nous obtenons un nouveau programme ReGrep qui
utilise notre implmentation des expressions rguli`res ` la place de celle de la biblioth`que.
e
e
e
a
e
Nous nous livrons ensuite ` des expriences en comparant les temps dexcution (par les
a
e
e
commandes time java Grep . . . et time java ReGrep . . . ).
(1) Dans le dictionnaire franais, nous recherchons les mots qui contiennent au moins n fois
c
la mme voyelle non accentue. Par exemple, pour n = 3 nous excutons la commande :
e
e
e
% java Grep (a.*a.*a|e.*e.*e|i.*i.*i|o.*o.*o|u.*u.*u) /usr/share/dict/french
Grep
ReGrep

1
2.7
3.1

2
2.5
9.7

3
1.9
16.4

4
1.7
17.9

5
1.6
18.6

6
1.5
18.0

On voit que notre technique, sans tre ridicule, est nettement moins ecace.
e
(2) Toujours dans le dictionnaire franais, nous recherchons les mots qui contiennent n fois
c
la lettre e, apr`s eacement des accents. Par exemple, pour n = 3 nous excutons la
e
e
commande :
% java Grep (e||`|^).*(e||`|^).*(e||`|^) /usr/share/dict/french
e e e
e e e
e e e
Grep
ReGrep

1
2.9
3.1

2
2.2
9.0

3
1.9
12.8

4
1.7
15.2

5
1.6
15.8

6
1.5
16.0

7
1.5
15.9


`
CHAPITRE VI. EXPRESSIONS REGULIERES

150

Cette exprience donne des rsultats similaires ` la prcdente. Plus prcisment dune part
e
e
a
e e
e e
la biblioth`que est plus rapide en valeur absolue ; et dautre part, nos temps dexcution
e
e
sont croissants, tandis que ceux de la biblioth`que sont dcroissants. Mais et cest impore
e
tant, il semble bien que les temps se stabilisent dans les deux cas.
(3) Nous recherchons une sous-cha ltre par X(.+)+X dans la cha XX= =, o` = =
ne
e
ne
u
est le caract`re = rpt n fois. Cette reconnaissance doit chouer, mais nous savons [3,
e
e ee
e
Chapitre 5] quelle risque de mettre en dicult limplmentation de la biblioth`que.
e
e
e
Grep
ReGrep

16
0.3
0.2

18
0.5
0.2

20
1.3
0.2

22
4.8
0.2

24
18.6
0.2

Et eectivement lemploi de la biblioth`que conduit ` un temps dexcution manifestement


e
a
e
exponentiel. Il est fascinant de constater que notre implmentation ne conduit pas ` cette
e
a
explosion du temps de calcul.

5
5.1

Une autre approche du ltrage


Langage driv
e e

Soit L un langage sur les mots et c un caract`re. Nous notons c1 L le langage driv, dni
e
e e e
comme les suxes m des mots de L qui sont de la forme c m.
c1 L = {m | c m L}
Dans le cas dun langage L rgulier engendr par le motif p, nous allons montrer que le langage
e
e
c1 L est rgulier en calculant le motif c1 p qui lengendre.
e
On observera dabord que, en toute rigueur, le langage c1 L peut tre vide, par exemple
e
} avec c = c. Or, selon notre dnition des langages rguliers, le langage vide nest
si L = { c
e
e
pas rgulier. Qu` cela ne tienne, nous inventons immdiatement un nouveau motif , qui ne
e
a
e
ltre aucun mot. En considrant les valeurs [[p]], on tend facilement les trois oprateurs des
e
e
e
expressions rguli`res.
e
e
p =

p =

|p = p

p| = p

* =

Ces r`gles nous permettent dliminer les occurrences internes de , de sorte quun motif est
e
e
dsormais ou un motif qui ne contient pas .
e
Proposition 15 Si L = [[p]], alors le
dni ainsi :
e
c1 =
c1 =
c1 c =
c1 c =
1 (p | p ) =
c
1
2
c1 (p1 p2 ) =
c1 (p1 p2 ) =
c1 p* =

langage c1 L est rgulier, engendr par le motif c1 p


e
e

pour c = c
1 p | c1 p
c
1
2
(c1 p1 ) p2
si p1
(c1 p1 ) p2 | c1 p2 si p1
(c1 p) p*

Preuve. On montre, par induction sur la structure de p, que pour tout mot m, on a lquivalence :
e
c1 p

m p

cm

e
ee
Seuls les trois derniers cas de la dnition de c1 p prsentent un intrt.
e

5. UNE AUTRE APPROCHE DU FILTRAGE

151

Posons p = p1 p2 et supposons en outre p1 . En revenant aux dnitions, par induction,


e
puis par dnition, on a les quivalences successives :
e
e

m = m1 m2
m = m1 m2
p1 c m1
c1 p1 m1 (c1 p1 ) p2

p1 p2 c m

p2 m2
p2 m2
Dans le cas o` p1 , il faut tenir compte dune autre dcomposition possible du mot c m
u
e
en prxe vide et suxe complet. Dans ce cas on a par induction :
e
p1
p2

cm

p1
c1 p2

Ce qui conduit ` lquivalence :


a e

p1 p2 c m

Et on peut conclure.
Posons p = q*. Alors on a :
q*

m = m1 m2
c1 p1 m1

p2 m2
p1
c1 p2

m = m1 m2
q c m1

q* m2

c m

En toute rigueur, lquivalence rsulte dun nombre arbitraire applications de la r`gle Stare
e
e
Seq pour un prxe vide et dune application de la mme r`gle pour la dcomposition crite
e
e
e
e
e
ci-dessus. On peut ensuite conclure par induction.
Par ailleurs, on dcide de la validit de p
e
e
Lemme 16 On note N (p) le prdicat p
e
N ()
N ()
N (c)
N (p1 | p2 )
N (p1 p2 )
N (p*)

par une simple induction structurelle.

. Le prdicat N (p) se calcule inductivement ainsi :


e
=
=
=
=
=
=

faux
vrai
faux
N (p1 ) N (P2 )
N (p1 ) N (P2 )
vrai

Preuve. Induction facile sur la structure des motifs.

5.2

Filtrage par la drivation des motifs


e

Pour savoir si un motif p ltre un mot m, on peut tout simplement itrer sur les caract`res du
e
e
mot, en calculant les drivations successives de p par les caract`res consomms. Un fois atteint
e
e
e
la n du mot, il ne reste qu` vrier si le motif driv ltre le mot vide.
a e
e e
Plus prcisment, les deux rsultats de la section prcdente (proposition 15 et lemme 16)
e e
e
e e
nous permettent dcrire deux nouvelles mthodes statiques dans la classe Re.
e
e

152

`
CHAPITRE VI. EXPRESSIONS REGULIERES

// Renvoie le motif c1 p ou null si c1 p est


static Re derivate(char c, Re p) ;
// Calculer N (p)
static boolean nullable(Re p) ;
Exercice 6 (Non corrig, vu en TP) Programmer ces deux mthodes. On fera attention `
e
e
a
la prsence du motif qui sera reprsent par null et ne se trouvera jamais ` lintrieur des
e
e
e
a
e
arbres Re. Cest-`-dire que le rsultat de Re.derivate est null ou un motif standard.
a
e
On peut alors crire par exemple la mthode matches (simplie, sans tenir compte de
e
e
e
mStart et mEnd) de la classe Matcher ainsi :
public boolean matches() {
Re d = pat ;
for (int k = 0 ; k < text.length() ; k++) {
d = Re.derivate(text.charAt(k), d) ;
i f (d == null) return false ;
}
return Re.nullable(d) ;
}
Exercice 7 Rcrire la mthode find de la classe Matcher (gure 6) en utilisant cette fois la
e
e
drivation de motifs. On seorcera de limiter le nombre des drivations de motif eectues, tout
e
e
e
en identiant la sous-cha ltre la plus ` gauche et la plus longue possible.
ne
e
a
Solution. Une solution possible est dintroduire une mthode findLongestPrefix qui cherche
e
le plus long prxe ltr ` partir de la position start. En cas de succ`s, la mthode renvoie la
e
ea
e
e
premi`re position non-ltre ; en cas dchec, la mthode renvoie -1.
e
e
e
e
private int findLongestPrefix(int start) {
Re d = pat ;
int found = -1 ;
i f (Re.nullable(d)) found = start ; // Trouv le prfixe vide
e
e
for (int k = start ; k < text.length() ; k++) {
d = Re.derivate(text.charAt(k), d) ;
i f (d == null) return found ;
e
e
i f (Re.nullable(d)) found=k+1 ; // Trouv le prfixe de longueur k+1-start
}
return found ;
}
La mthode findLongestPrefix calcule simplement les motifs drivs par tous les caract`res
e
e e
e
de text ` partir de la position start, en se souvenant (dans found) du ltrage le plus long. La
a
recherche sarrte quand la cha est lue en totalit ou quand le motif driv est .
e
ne
e
e e

Ecrire find est ensuite immdiat : chercher un prxe ltr dans tous les suxes possibles
e
e
e
de text.
public boolean find() {
for (int start = regStart ; start <= text.length() ; start++) {
int end = findLongestPrefix(start) ;
i f (end >= 0) {
mStart = start ; mEnd = end ;
regStart = mEnd ;

5. UNE AUTRE APPROCHE DU FILTRAGE

153

return true ;
}
}
mStart = mEnd = -1 ;
regStart = 0 ;
return false ;
}
On observe que, pour une position start donne, la nouvelle mthode find trouve bien la plus
e
e
longue sous-cha ltre. Une implantation qui donnerait la plus petite sous-cha ltre est
ne
e
ne
e
plus simple.
private int findShortestPrefix(int start) {
Re d = pat ;
e
e
i f (Re.nullable(d)) return start ; // Trouv le prfixe vide
for (int k = start ; k < text.length() ; k++) {
d = Re.derivate(text.charAt(k), d) ;
i f (d == null) return -1 ;
i f (Re.nullable(d)) return k+1 ;
}
return -1 ; // Aucun prfixe ne convient
e
}

La mesure des temps dexcution de la nouvelle classe Matcher fait appara de meilleurs
e
tre
performances dans les deux premi`res expriences de la section 4.4 et une consommation tr`s
e
e
e
importante de la mmoire dans la derni`re exprience.
e
e
e
Exercice 8 Calculer la succession des motifs drivs pour le motif X(.+)+X et le mot XX= =
e e
et justier le rsultat de la derni`re exprience.
e
e
e
Solution. Notons p0 , p1 etc. la suite des motifs drivs Il faut dabord driver deux fois par X.
e e
e
p0 = X(.+)+X

p1 = (.+)+X

p2 = .*(..*)*X

Pour calculer p2 on a exprim p+ comme pp*. Si on suit la dnition de la drivation on a en toute


e
e
e
rigueur des rsultat dirents par exemple, p1 = ()(.+)+X. Mais on enl`ve les motifs () des
e
e
e
squences, les motifs sont dj` bien assez compliqus comme cela. Soit le motif q = .*(..*)*,
e
ea
e
on a p2 = qX. Par ailleurs la drivation =1 q vaut q | q. Le motif p ltre le mot vide, mais la
e
drivation de X par = vaut . On a donc :
e
p3 = (q | q) X
Et en posant q1 = q et qn+1 = qn | qn il est clair que lon a :
pn+2 = qn+1 X
Motif dont la taille est exponentielle en n.

154

`
CHAPITRE VI. EXPRESSIONS REGULIERES

Chapitre VII

Les automates
1

Pourquoi tudier les automates


e

Ce chapitre est une tr`s succincte introduction ` la thorie des automates que vous aurez
e
a
e
loccasion de voir de faon dtaille si vous choisissez un cursus dinformatique. Ce chapitre est,
c
e
e
par nature, un peu plus thorique et un peu moins algorithmique que les prcdents.
e
e e
Les automates sont des objets mathmatiques, tr`s utiliss en informatique, qui permettent
e
e
e
de modliser un grand nombre de syst`mes (informatiques). Ltude des automates a commenc
e
e
e
e
vers la n des annes cinquante. Elle se base sur de nombreuses techniques (topologie, thorie des
e
e
graphes, logique, alg`bre, etc.). De faon tr`s informelle, un automate est un ensemble dtats
e
c
e
e

du syst`me, relis entre eux par des transitions qui sont marques par des symboles. Etant
e
e
e
donn un mot fourni en entre, lautomate lit les symboles du mot un par un et va dtat en
e
e
e
tat selon les transitions. Le mot lu est soit accept par lautomate soit rejet.
e
e
e
Avant de donner une dnition plus formelle des concepts dcrits ci-dessus, citons quelques
e
e
exemples classiques dutilisation dautomates :
Vrication dun circuit lectronique
e
e
Recherche doccurrence dans un texte (moteur de recherches sur le web, etc.)
Vrication de protocoles de communication
e
Compression de donnes
e
Compilation
Biologie (gnomique)
e
En dehors de ces utilisations pratiques des automates, notons quils sont aussi utiliss pour
e
modliser les ordinateurs et pour comprendre ce quun ordinateur peut faire (dcidabilit) et ce
e
e
e
quil sait faire ecacement (complexit). Cest donc une notion fondamentale de linformatique.
e

Rappel : alphabets, mots, langages et probl`mes


e

Nous reprenons ici les notations du chapitre VI. Lalphabet est un ensemble de caract`res
e
(ou symboles). un mot est une suite nie de caract`res. Lensemble des mots sur est not .
e
e
Un langage est un sous-ensemble de , cest-`-dire un ensemble particulier de mots. Parmi les
a
mots de on distingue le mot vide not . Le mot vide est lunique mot de longueur zro.
e
e

Automates nis dterministes


e
Un automate ni dterministe est un quintupl (Q, , , q0 , F ) constitu des lments suivants
e
e
e
ee
un alphabet ni ()
un ensemble ni dtats (Q)
e
155

156

CHAPITRE VII. LES AUTOMATES


une fonction de transition ( : Q Q)
un tat de dpart (q0 Q)
e
e
un ensemble dtats naux (ou acceptant) F Q
e

3.1

Fonctionnement dun automate ni dterministe


e

Lautomate prend en entre un mot et laccepte ou la rejette. On dit aussi quil le reconna
e
t
ou ne le reconna pas. Le langage associ a un automate est constitu de lensemble des mots
t
e`
e
quil reconna Voici comment lautomate proc`de pour dcider si un mot appartient ` son
t.
e
e
a
langage.
Le processus commence ` ltat de dpart q0
a e
e
Les symboles du mot sont lus les uns apr`s les les autres.
e
` la lecture de chaque symbole, on emploie la fonction de transition pour se dplacer
A
e
vers le prochain tat (en utilisant ltat actuel et le caract`re qui vient dtre lu).
e
e
e
e
le mot est reconnu si et seulement si le dernier tat (i.e., ltat correspondant ` la lecture
e
e
a
du dernier caract`re du mot) est un tat de F .
e
e
De faon plus formelle, pour dnir exactement le langage reconnu par un automate, nous
c
e

introduisons la fonction de transition tendue aux mots, . Elle se dnit rcursivement comme
e
e
e
suit.

A partir dun tat q en lisant le mot vide on reste dans ltat q, i.e., q Q, (q, ) = q
e
e
a avec c {}), et un tat

Etant donn un mot c se terminant par a (i.e., c = c


e
e

q de Q, (q, c) = (q, c a) = ((q, c ), a)


Nous pouvons maintenant dnir le langage L(A) accept par un automate ni dterministe
e
e
e
A = (Q, , , q0 , F ).

L(A) = {c|(q0 , c) F }

3.2

Des reprsentation compactes des automates


e

On peut associer ` un automate une table de transition qui dcrit de mani`re extensive la
a
e
e
fonction de transition :
Une colonne correspond ` un caract`re de lalphabet.
a
e
Une ligne correspond ` un tat de lautomate (ltat initial est prcd dune `che ;
a
e
e
e e e
e
ltat nal dune toile )
e
e
La valeur (q, a) pour q Q, a correspond ` ltat indiqu ` lintersection de la ligne q et
a e
ea
de la colonne a. Notons qu` partir de cette table il est ais de retrouver lensemble des tats
a
e
e
ainsi que lalphabet et donc didentier exactement lautomate.
Exemple 1 Considrons la table de transition ci-dessous.
e
1
2

a
1
1

b
2
2

Il correspond ` lautomate (Q, , , q0 , F ) avec


a
Q = {1, 2}
= {a, b}
(1, a) = 1, (1, b) = 2, (2, a) = 1, (2, b) = 2
q0 = 1
F = {2}
Il est facile de voir que le langage de cet automate est constitu exactement des mots composs
e
e
de a et de b qui se terminent par un b.


3. AUTOMATES FINIS DETERMINISTES

157

Pour reprsenter de faon tr`s intuitive un automate ni dterministe (Q, , , q0 , F ), on peut


e
c
e
e
utiliser un graphe de transition constitu des lments suivants :
e
ee
Un ensemble de sommets (chaque sommet reprsente un lment de Q).
e
ee
Un ensemble darcs entre les sommets valus par un symbole de (un arc entre les tats
e
e
q et q valu par le symbole s signie que (q, s) = q ).
e
Ltat initial q0 est marqu par une `che entrante.
e
e
e
Les tats naux F sont entours dune double ligne.
e
e
Lautomate de lexemple 1 est ainsi reprsent sur la gure 1.
e
e
b
a

a
Fig. 1 Un automate ni dterministe
e
Pour simplier encore cette reprsentation, un arc entre deux sommets q, q peut tre valu
e
e
e
par plusieurs symboles s1 , ..., sn spars par des virgules. Cette derni`re convention signie sime e
e
plement que i n, (q, si ) = q et elle permet dviter une multiplication darcs sur le graphe.
e
La gure 2 illustre une telle simplication.
b
a

a, b

b
Fig. 2 Deux reprsentations quivalentes du mme automate ni
e
e
e
Exercice 1 Quel est le langage reconnu par lautomate de la gure 2 ?
Solution. Tous les mots qui contiennent un b.

Exercice 2 Ecrire la table de transition de lautomate suivant. Quel est le langage reconnu ?
0
1

0, 1

B
Solution. La table de transition de lautomate est
A
B
C

0
B
B
C

1
A
C
C

Cet automate reconna les mots qui contiennent 01.


t
Exercice 3 Soit lautomate ni dterministe ({q0 , q1 , q2 , q3 }, {0, 1}, , q0 , {q0 }) donn par la table
e
e

158

CHAPITRE VII. LES AUTOMATES

q0
q1
q2
q3

0
q2
q3
q0
q1

1
q1
q0
q3
q2

Dessiner lautomate et montrer quil accepte 110101.


1

Solution.

q0

q1
1

1
q2

q3
1

Exercice 4 Construire un automate ni dterministe qui reconna le langage


e
t
L = {x {0, 1} |n1 (x) 0 mod 4}
o` n1 (x) est le nombre doccurrence du symbole 1 dans le mot x.
u
Solution.
0

0
0

0
1

1
D

1
Exercice 5 Construire les automates nis dterministes qui reconnaissent les langages suivants
e
L1 = {m (a + b) |chaque a de m est immdiatement prcd et immdiatement suivi dun b}
e
e e e
e
|m contienne ` la fois ab et ba}
L2 = {m (a + b)
a
L3 = {m (a + b) |m contienne exactement une occurrence de aaa}

Automates nis non-dterministes


e

Un automate ni non-dterministe est un automate tel que dans un tat donn, il peut y
e
e
e
avoir plusieurs transitions avec le mme symbole. le fonctionnement dun tel automate nest
e
donc pas totalement (( dtermin )), car on ne sait pas quel tat lautomate va choisir.
e
e
e
Les automates non-dterministes permettent de modliser facilement des probl`mes come
e
e
plexes. Ils peuvent tre convertis en des automates nis dterministe. Ces derniers peuvent tre
e
e
e
exponentiellement plus grand que les automates non dterministe dont ils sont issus.
e
Un automate ni non-dterministe est un quintupl : (Q, , , q0 , F )
e
e
un alphabet ni ()
un ensemble ni dtats (Q)
e


4. AUTOMATES FINIS NON-DETERMINISTES

159

une fonction de transition qui associe ` tout tat q Q et tout symbole s un sous
a
e
ensemble de Q not (q, s).
e
un tat de dpart (q0 )
e
e
un ensemble dtats naux (ou acceptant) F
e
Cest la fonction de transition qui di`re ici de celle utilise par les automates nis dterministes.
e
e
e
Remarquons que tout automate ni dterministe est aussi un automate ni non-dterministe.
e
e
Les reprsentations compactes des automates nis dterministes stendent naturellement
e
e
e
aux automates nis non-dterministes. Une cellule de la table de transition contient un souse
ensemble dtats (ventuellement vide).
e
e

4.1

Fonctionnement dun automate ni non-dterministe


e

Comme pour un automate ni dterministe, lautomate prend en entre un mot et laccepte


e
e
ou le rejette. Le langage associ est constitu de lensemble des mots quil reconna
e
e
t.
Exemple 2 Voici automate qui reconna les mots dnis sur lalphabet {a, b, c} qui comt
e
mencent par a et qui nissent par c.
a
c
q0

q1

La table associe ` cet automate est alors :


e a
q0
q1
q2

q2

a, b, c

a
{q1 }
{q1 }

{q1 }

{q1 , q2 }

Comme pour les automates dterministes, nous nous introduisons la fonction de transition
e
Elle se dnit rcursivement comme suit.
tendue aux mots, .
e
e
e
A partir dun tat q en lisant le mot vide (le mot vide ne contient aucun symbole et est
e

toujours not ), on reste dans ltat q, i.e., q Q, (q, ) = {q}


e
e

Etant donn un mot c se terminant par a (i.e., c = c a avec c {}), et un tat


e
e
q de Q,

(q, c) = (q, c a) =
(p, a)

p(q,c )

Nous pouvons maintenant dnir le langage L(A) accept par un automate ni dterministe
e
e
e
A = (Q, , , q0 , F ).

L(A) = {c|(q0 , c) F = }
Exercice 6 Construire lautomate ni non-dterministe associ ` la table ci-dessous.
e
ea
0
1
2
3

Solution.

a
{0, 1, 3}

{3}

b
{2}
{3}

{1}

160

CHAPITRE VII. LES AUTOMATES

1
a

b
b

a
0

3
a

2
Exercice 7 Construire un automate ni non-dterministe qui reconna les mots qui contiennent
e
t
church ou chomsky.

Solution.

c
0

s
5

c
h
8

u
9

r
10

c
11

12

13

Exercice 8 Construire un automate nis non-dterministe qui reconna les mots de lalphabet
e
t
{a, b} qui terminent par bab.
Solution.
a
b
0

a
1

Exercice 9 Construire un automate ni non-dterministe et un automate ni dterministe qui


e
e
reconna les mots sur lalphabet {a, b, c} dcrits par lexpression rguli`re (a + b + c) b(a + b + c).
t
e
e
e
Exercice 10 Construire un automate ni non-dterministe qui reconna les nombres dont le
e
t
dernier chire nappara quune fois.
t
Exercice 11 Modlisation dun jeu (dapr`s la page de Jean-Eric Pin). Le joueur a les yeux
e
e
bands. Face ` lui, un plateau sur lequel sont disposs en carr quatre jetons, blancs dun ct
e
a
e
e
oe
et noirs de lautre. Le but du jeu est davoir les quatre jetons du ct blanc. Pour cela, le joueur
oe
peut retourner autant de jetons quil le souhaite, mais sans les dplacer. A chaque tour, le ma
e
tre
de jeu annonce si la conguration obtenue est gagnante ou pas, puis eectue une rotation du


4. AUTOMATES FINIS NON-DETERMINISTES

161

plateau de zro, un, deux ou trois quarts de tours. La conguration de dpart est inconnue du
e
e
joueur, mais le ma de jeu annonce avant le dbut du jeu quelle nest pas gagnante. Chaque
tre
e
annonce prend une seconde, et il faut 3 secondes au joueur pour retourner les jetons. Pouvez-vous
aider le joueur ` gagner en moins dune minute ?
a

4.2

Dterminisation dun automate ni non-dterministe


e
e

Un automate ni dterministe est aussi non-dterministe. Donc tout langage reconnu par un
e
e
automate ni dterministe est reconnu par un automate ni non-dterministe. Plus surprenant,
e
e
la rciproque est aussi vraie (Thor`me de Rabin-Scott).
e
e e
Considrons un automate ni non-dterministe An = (Qn , , n , q0 , Fn ) et construisons un
e
e
automate ni dterministe Ad = (Qd , , d , {q0 }, Fd ) qui reconna exactement le mme langage.
e
t
e
Les alphabets de An et de Ad sont identiques.
Les tats de dpart sont respectivement q0 et le singleton {q0 }.
e
e
Qd est constitu de tous les sous-ensembles de Qn .
e
Fd est lensemble des sous-ensembles de Qn qui contiennent au moins un lment de Fn .
ee

Etant donn un sous ensemble S de Qn et un symbole a , on dnit la fonction de


e
e
transition d (S, a) de la mani`re suivante
e
d (S, a) =

n (q, a).
qS

Nous illustrons le thor`me de Rabin-Scott sur quelques exemples.


e e
Exemple 3 reprenons lexemple de lexercice 8. Il sagissait de construire un automate ni
non-dterministe reconnaissant les mots de lalphabet {a, b} qui terminent par bab. Lautomate
e
suivant rpond ` la question.
e
a
a
b

b
Essayons maintenant de le dterminiser en construisant un nouvel tat ` partir de chaque sous
e
e
a
ensemble dtat possible.
e
a
b
{0}

{0, 1}
a

{1}

{1, 3}
b

{3}
{2}

{0, 3}
{0, 2}

{0, 1, 3}

{2, 3}
{1, 2}

{1, 2, 3}

{0, 1, 2}

{0, 2, 3}

Remarquons que les tats {1}, {2}, {3}, {0, 2}, {0, 3}, {1, 2}, {2, 3}, {0, 1, 2}, {1, 2, 3}, {0, 2, 3}
e
sont inatteignables et peuvent tre retirs de lautomate.
e
e

162

CHAPITRE VII. LES AUTOMATES

En pratique, lors de la conversion, on ne cre pas immdiatement tous les tats de lautomate
e
e
e
ni dterministe. Les tats utiles sont cres quand on en a besoin en suivant la mthode de
e
e
e
e
construction ci-dessous :
Qd est initialis ` et soit E un ensemble dtats initialis ` E = {{q0 }}
ea
e
ea
Tant que E est non vide,
choisir un lment S de E (S est donc un sous ensemble de Qn ),
ee
ajouter S ` Qd ,
a
pour tout symbole a ,
+ calculer ltat S = qS n (q, a)
e
+ si S nest pas dj` dans Qd , lajouter ` E
ea
a
+ ajouter un arc sur lautomate entre S et S et la valuer par a
Exercice 12 Dterminiser lautomate de lexercice 7 (long).
e

4.3

Les transitions

Rappelons qu reprsente le mot vide. Une transition (note sur larc dun automate)
e
e
permet de passer dun tat ` lautre dun automate sans lire de symbole. Cette facilit permet
e
a
e
de programmer facilement des automates complexes.
Une table de transition peut tre associe ` un automate contenant des transition. La table
e
e a
est identique ` celle utilise pour un automate ni non-dterministe ` ceci pr`s quon la compl`te
a
e
e
a
e
e
dune colonne associe au caract`re vide .
e
e
Exemple 4 Pour illustrer les transitions, construisons un automate ni non dterministe qui
e
reconna les nombres dcimaux. Rappelons quun nombre dcimal est un nombre rel qui est
t
e
e
e
le quotient dun entier relatif par une puissance de dix. Plus prcisment, on souhaite pouvoir
e e
crire le nombre dcimal en commenant par un + ou un -, suivi dune suite de chires, dune
e
e
c
virgule et dune suite de chires. Bien entendu, le + ou le - sont optionnels, la premi`re
e
cha de chires ne peut pas tre vide et ne commence pas par 0 (sauf si le nombre dcimal
ne
e
e
est 0). La seconde cha ne se termine pas par 0. Si seconde cha est vide, on omet la ,.
ne
ne
0, , 9
A

, +,

1, , 9

1, 9

,
C

0
F
0, , 9
La transition de ltat A ` ltat B est rgie par , +, . Ainsi, on peut passer de A ` B soit en
e
a e
e
a
lisant +, soit en lisant soit enn en ne lisant rien.
La table de transition associe ` cet automate est alors :
e a
A
B
C
D
E
F

{B}

+
{B}

{B}

{D}

{F }
{C}
{D}

{C}

{D, E}

{C}

{D, E}

{C}

{D, E}

Exercice 13 On cherche ` construire un automate qui reconna les mots qui se terminent par
a
t
bab ou qui commencent par aba.


`
5. AUTOMATES FINIS ET EXPRESSIONS REGULIERES

163

On sait construire un automate qui reconna les mots qui se terminent par bab (exercice 8) :
t
a
b
0

a
1

b
il est facile de construire un automate qui reconna les mots qui commencent par aba.
t
a, b
b

a
4

a
6

Il sut alors dassembler ces automates avec une simple transition.


a, b
a
b
0

a
1

Lintroduction des transition ne change pas la nature des langages reconnus par les automates. Comme pour les automates non-dterministes que lon peut toujours dterminiser, il est
e
e
toujours possible dliminer les transition et dobtenir un automate ni dterministe quivalent.
e
e
e
Nous naborderons pas ici cette limination.
e

Automates nis et expressions rguli`res


e
e

Les automates nis et les expressions rguli`res ont la mme expressivit. En eet, le thor`me
e
e
e
e
e e
dquivalence des expressions rguli`res et des automates nis (Kleene) tablit que Le langage
e
e
e
e
accept par un automate ni correspond ` la valeur dune expression rguli`re et rciproquement.
e
a
e
e
e

Un peu de Java

Il est ais de reprsenter les automates nis sous la forme dun programme java. Notre objectif
e
e
est de
modliser un automate ni non-dterministe (sans transition)
e
e
et dcrire un programme qui dtermine si une cha de caract`re est reconnue par laue
e
ne
e
tomate.
Sans perte de gnralit, nous allons supposer que lalphabet est lensemble des caract`res ASCII
e e
e
e
et que les tats sont numrots ` partir de 0.
e
e e a

6.1

Mod`le
e

Le mod`le de donnes est alors tr`s simple :


e
e
e

164

CHAPITRE VII. LES AUTOMATES


Ltat initial de lautomate est indiqu par un entier q0.
e
e
La table de transition delta est un tableau bidimensionnel de listes dentiers (la liste
dentier tant ici le moyen le plus simple de reprsenter un ensemble dtats). Ainsi
e
e
e
delta[q][c] est la liste des tats atteignables ` partir de q en lisant le caract`re c.
e
a
e
Lensemble des tats finaux est une liste dentiers.
e
Enn, le mot que lon cherche ` reconna est une cha de caract`res String mot.
a
tre
ne
e
Soit donc en Java les deux classes List et Automate.

class Liste {
int val;
Liste suivant;
Liste(int v, Liste x) {
val = v;
suivant = x;
}
}
class Automate {
// tat initial
e
int q0;
Liste[][] delta; // fonction de transition
Liste finaux;
// tats finaux
e
Automate(int q, Liste f, Liste[][]d) {
q0 = q;
delta = d;
finaux = f;
}
}

6.2

Algorithme de recherche

Nous aurons besoin de quelques fonctions classiques sur les listes


static int longueur(Liste x) {
// La longueur dune liste
i f (x == null)
return 0;
else
return 1 + longueur(x.suivant);
}
// Le k `me lment
e
e e
static int kieme(Liste x, int k) {
i f (k == 1)
return x.val;
else
return kieme(x.suivant, k-1);
}
static boolean estDans(Liste x, int v) { // Le test dappartenance
i f (x == null)
return false;
else
return x.val == v || estDans(x.suivant, v);
}
La fonction accepter(String mot, Automate a) qui permet de vrier quun mot mot est ace
cept apr lautomate a appelle la fonction static boolean accepter(String mot, Automate
e
a, int i, int q).
static boolean accepter(String mot, Automate a) {

6. UN PEU DE JAVA

165

return accepter(mot, a, 0, a.q0);


}
static boolean accepter(String mot, Automate a, int i, int q) {
i f (i == mot.length())
return Liste.estDans(a.finaux, q);
else {
boolean resultat = false;
int c = mot.charAt(i);
// le code ASCII du caract`re courant
e
for (Liste nv_q = a.delta[q][c]; nv_q != null; nv_q = nv_q.suivant)
resultat = resultat || accepter(mot, a, i+1, nv_q.val);
return resultat;
}
}
La fonction static boolean accepter(String mot, int i, Automate a, int q) prend,
en plus de lautomate et du mot que lon tudie, deux autres param`tres :
e
e
La position du caract`re courant i dans le mot.
e
Ltat courant q.
e
Elle renvoie true si et seulement si le sous-mot de mot dont on a retir les i premiers caract`res
e
e
tait reconnu par un automate semblable ` a dont ltat initial serait q.
e
a
e
Remarquons que si i est le dernier caract`re du mot (ce qui correspond au test if (i == mot.length()))
e
alors il sut de tester lappartenance de q ` a.finaux. Si ce nest pas le cas, on va essayer dema
prunter toutes les transitions possibles. on explore ainsi tous les tats nv_q.val atteignable `
e
a
partir de q en lisant c. Une fois dans ltat nv_q.val, on appelle rcursivement accepter.
e
e

6.3

Mise en uvre sur un automate

Considrons lautomate suivant qui accepte toutes les cha


e
nes qui se terminent par 01.
0
0

1
1

0, 1
La table associe ` cet automate est alors :
e a
0
1
2

0
{0, 1}

1
{0}
{2}

Pour le construire, il nous sut de construire la table de transition.


public static void main(String [] arg) {
Liste[][] delta = new Liste[3][128];
delta[0][(int)0] = new Liste(0, new Liste(1, null));
delta[0][(int)1] = new Liste(0, null);
delta[1][(int)0] = null;
delta[1][(int)1] = new Liste(2, null);
delta[2][(int)0] = null;
delta[2][(int)1] = null;
Automate a = new Automate(0,
new Liste(2, null),
delta);

166

CHAPITRE VII. LES AUTOMATES


System.out.println("accepter = " + accepter(arg[0], a));
}

Remarquons que le code ASCII du caract`re 0 est obtenu par (int)0.


e
Exercice 14 Comment procder pour coder un automate avec des transitions ?
e

Annexe A

Le co t dun algorithme
u
Ce chapitre rappelle les dirents moyens dvaluer le cot dun algorithme.
e
e
u

Une dnition tr`s informelle des algorithmes


e
e

La formalisation de la notion dalgorithme est assez tardive. Les travaux fondamentaux de


Post et de Turing datent de la n des annes 30. Nous ne disposons pas, pour ce cours, des
e
outils qui permettent dexposer tr`s rigoureusement la notion dalgorithme et de complexit ale
e
gorithmique. Nous nous contenterons de la dnition suivante : Un algorithme est un assemblage
e
ordonn dinstruction lmentaires qui a partir dun tat initial bien spci permettent daboue
ee
`
e
e e
tir a un tat nal, lui aussi bien spci. Notons que pour quun algorithme puisse sexcuter, les
`
e
e e
e
donnes (dentres, de sortie et intermdiaires) sont structures. Ceci permet de les conserver,
e
e
e
e
de les utiliser et de les modier.
Les tapes lmentaires sont ventuellement rptes (notion de boucle) et sont soumises `
e
ee
e
e ee
a
des test logiques (instruction de contrle). Les algorithmes peuvent, la plupart du temps, tre
o
e
cods ` laide de programmes informatique
e a

Des algorithmes ecaces ?

Quest-ce quun bon algorithme ? Cest bien entendu un algorithme qui rpond correctement
e
au probl`me pos. On souhaite gnralement quil soit aussi rapide et nutilise pas trop de
e
e
e e
mmoire.
e
Quel que soit la mesure choisie, il est clair que la qualit de lalgorithme nest pas absolue
e
mais dpend des donnes dentre. Les mesures decacit sont donc des fonctions des donnes
e
e
e
e
e
dentre. Pour valuer le temps dexcution dun algorithme (par exemple un tri) sur une donne
e
e
e
e
d (par exemple un ensemble dentiers ` trier), on calcule le nombre doprations lmentaires
a
e
ee
(oprations arithmtiques, aectation, instruction de contrle, etc.) eectues pour traiter la
e
e
o
e
donne d. On admet communment que toute opration lmentaire prend un temps constant.
e
e
e
ee
Attention, cette hypoth`se nest valable que si tous les nombres sont cods sur un mme nombre
e
e
e
de bits.
Plutt que de calculer le nombre doprations associ ` chaque donne, on cherche souvent `
o
e
ea
e
a
valuer le nombre doprations ncessaires pour traiter les donnes qui ont une certaine taille. Il
e
e
e
e
existe souvent un param`tre naturel qui est un estimateur raisonnable de la taille dune donne
e
e
(par exemple, le nombre n dlments ` trier pour un algorithme de tri, la taille n, m dune
ee
a
matrice pour le calcul du dterminant, la longueur n dune liste pour une inversion, etc.). Pour
e
simplier nous supposerons dans la suite que la taille dune donne est value par un unique
e
e
e
entier n.
167


ANNEXE A. LE COUT DUN ALGORITHME

168

Les informaticiens sintressent ` lordre de grandeur des temps dexcution (ou de taille
e
a
e
mmoire) quand n devient tr`s grand. Le cot exact en secondes est en eet tr`s dicile `
e
e
u
e
a
mesurer et dpend de tr`s nombreux param`tres (cf. Section 4). On utilise donc les notations
e
e
e
O, et pour exprimer des relations de domination : Soient f et g deux fonctions dnies des
e
entiers naturels vers les entiers naturels.
f (n) = O(g(n)) si et seulement si il existe deux constantes positives n0 et B telles que
n n0 , f (n) Bg(n)
Ce qui signie que f ne cro pas plus vite que g. Un algorithme en O(1), cest un algorithme
t
dont le temps dexcution ne dpend pas de la taille des donnes. Cest donc un ensemble
e
e
e
constant doprations lmentaires (exemple : laddition de deux entiers). On dit dun
e
ee
algorithme quil est linaire si il utilise O(n) oprations lmentaires. Il est polynomial si
e
e
ee
il existe une constante a telle que le nombre total doprations lmentaires est O(na ).
e
ee
f (n) = (g(n)) si et seulement si il existe deux constantes positives n0 et B telles que
n n0 , f (n) Bg(n)
f (n) = (g(n)) si et seulement si il existe trois constantes positives n0 , B et C telles que
n n0 , Bg(n) f (n) Cg(n)
Dans la pratique, le temps dexcution dun algorithme dpend non seulement de n mais
e
e
aussi de la structure des donnes. Par exemple, le tri dune liste dentiers est, pour certains
e
algorithmes, plus rapide si la liste est partiellement trie plutt que dans un ordre parfaitement
e
o
alatoire. Pour analyser nement un algorithme, on a donc besoin de plusieurs mesures :
e
La complexit dans le pire cas. On mesure alors le nombre doprations avec les donnes
e
e
e
qui m`nent ` un nombre maximal doprations. Soit donc
e
a
e
complexit dans le pire cas =
e

max

d donne de taille n
e

C(d)

o` C(d) est le nombre doprations lmentaires pour excuter lalgorithme sur la donne
u
e
ee
e
e
dentre d.
e
La complexit en moyenne. Cette notion na de sens que si lon dispose dune hypoth`se
e
e
sur la distribution des donnes. Soit donc
e
complexit en moyenne =
e

(d)C(d)
d donne de taille n
e

o` (d) est la probabilit davoir en entre une instance d parmi toutes les donnes de
u
e
e
e
taille n.
Ces notions stendent ` la consommation mmoire dun algorithme. On parle alors de complexit
e
a
e
e
spatiale (maximale ou moyenne).
En pratique, le pire cas est rarement atteint et lanalyse en moyenne semble plus sduisante.
e
Attention tout de mme ` deux cueils pour les calculs en moyenne :
e
a
e
Pour que ce calcul garde un certain sens, il faut conna la distribution des donnes, ce
tre
e
qui est dlicat ` estimer. On fait parfois lhypoth`se que les donnes sont quiprobables
e
a
e
e
e
(ce qui est bien souvent totalement arbitraire).
Comme nous allons le voir, les calculs de complexit en moyenne sont fort dlicats ` mettre
e
e
a
en uvre.

Quelques exemples

Nous donnons ici des exemples simples et varis danalyse dalgorithmes. De nombreux autres
e
exemples sont dans le polycopi.
e

3. QUELQUES EXEMPLES

3.1

169

Factorielle

La premi`re version de notre programme permettant de calculer n! est itrative.


e
e
static int factorielle(int n) {
int f = 1;
for (int i= 2; i <= n; i++)
f = f * i;
return f;
}
Nous avons n 1 itrations au sein desquelles le nombre doprations lmentaires est constant.
e
e
ee
La complexit est O(n)
e
La seconde version est rcursive. Soit C(n) le nombre doprations ncessaires pour calculer
e
e
e
factorielle(n). Nous avons alors C(n) + C(n 1) o` est une constante qui majore le
u
nombre doprations prcdent lappel rcursif. De plus C(1) se calcule en temps constant et
e
e e
e
donc C(n) = O(n).
static int factorielle(int n) {
i f (n <= 0)
return 1;
else
return n * factorielle(n-1);
}

3.2

Recherche Dichotomique

Considrons un tableau T de n entiers tris. On cherche ` tester si un entier v donn se


e
e
a
e
trouve dans le tableau. Pour ce faire, on utilise une recherche dichotomique.
static boolean trouve(int[]
i f (min >= max) // vide
return false;
int mid = (min + max) /
i f (T[mid] == v)
return true;
else i f (T[mid] > v)
return trouve(T, v,
else
return trouve(T, v,
}

T, int v, int min, int max){


2;

min, mid);
mid + 1, max);

La fonction trouve cherche lentier v dans T entre les indices min et max -1. Pour eectuer une
recherche sur tout le tableau, il sut dappeler trouve(T, v, 0, T.length).
Le nombre total doprations est proportionnel au nombre de comparaisons C(n) eectues
e
e
par lalgorithme rcursif. Et donc, nous avons immdiatement : C(n) = 1 + C( n ). Soit donc,
e
e
2
C(n) = O(log n).

3.3

Tours de Hanoi

Le tr`s classique probl`me des tours de Hanoi consiste ` dplacer des disques de diam`tres
e
e
a e
e
dirents dune tour de dpart ` une tour darrive en passant par une tour intermdiaire. Les
e
e
a
e
e
r`gles suivantes doivent tre respectes : on ne peut dplacer quune disque ` la fois, et on ne
e
e
e
e
a
peut placer un disque que sur un disque plus grand ou sur un emplacement vide.
Identions les tours par un entier. Pour rsoudre ce probl`me, il sut de remarquer que si
e
e
lon sait dplacer une tour de taille n de la tour ini vers dest, alors pour dplacer une tour
e
e


ANNEXE A. LE COUT DUN ALGORITHME

170

de taille n + 1 de ini vers dest, il sut de dplacer une tour de taille n de ini vers temp, un
e
disque de ini versdest et nalement la tour de hauteur n detemp versdest.
public static void hanoi(int n, int ini, int temp, int dest){
i f (n == 1){ // on sait le faire
System.out.println("deplace" + ini + " " + dest);
return;
// sinon recursion
}
hanoi(n - 1, ini, dest, temp);
System.out.println("deplace" + ini + " " + dest);
hanoi(n-1, temp, ini, dest);
}
Notons C(n) le nombre dinstructions lmentaires pour calculer hanoi(n, ini, temp, dest).
ee
Nous avons alors C(n + 1) 2C(n) + , o` est une constante. Tour de Hanoi est exponentielle.
u

3.4

Recherche dun nombre dans un tableau, complexit en moyenne


e

On suppose que les lments dun tableau T de taille n sont des nombres entiers distribus
ee
e
de faon quiprobable entre 1 et k (une constante). Considrons maintenant lalgorithme de
c
e
e
recherche ci-dessous qui cherche une valeur v dans T.
static boolean trouve(int[] T, int v) {
for (int i = 0; i < T.length; i++)
i f (T[i] == v)
return true;
return false;
}
La complexit dans le pire cas est clairement O(n). Quelle est la complexit en moyenne ?
e
e
n tableaux. Parmi ceux-ci, (k 1)n ne contiennent pas v et
Remarquons que nous avons k
dans ce cas, lalgorithme proc`de ` exactement n itrations. Dans le cas contraire, lentier est
e a
e
dans le tableau et sa premi`re occurrence est alors i avec une probabilit de
e
e
(k 1)i1
ki
et il faut alors procder ` i itrations. Au total, nous avons une complexit moyenne de
e
a
e
e
C=
Or

(k 1)n
n+
kn
n

x,

ixi1 =
i=1

n
i=1

1 + xn (nx n 1)
(1 x)2

(il sut pour tablir ce rsultat de driver lidentit


e
e
e
e
C=n

(k 1)i1
i
ki

n
i
i=1 x

(k 1)n
(k 1)n
n
+k 1
(1 + )
n
n
k
k
k

1xn+1
1x )

et donc

=k 1 1

1
k

Co t estim vs. co t rel


u
e
u e

Les mesures prsentes dans ce chapitre ne sont que des estimations asymptotique des algoe
e
rithmes. En pratique, il faut parfois atteindre de tr`s grandes valeurs de n pour quun algorithme
e
en O(n log n) se comporte mieux quun algorithme quadratique.

4. COUT ESTIME VS. COUT REEL

171

Les analyses de complexit peuvent servir ` comparer des algorithmes mais le mod`le de cot
e
a
e
u
est relativement simple (par exemple, les oprations dacc`s aux disques, ou le trac rseau gnr
e
e
e
e ee
ne sont pas pris en compte alors que ces param`tres peuvent avoir une inuence considrable sur
e
e
un programme). Il est toujours ncessaire de procder ` des analyses exprimentales avant de
e
e
a
e
choisir le meilleur algorithme, i.e., lalgorithme spciquement adapt au probl`me que lon
e
e
e
cherche ` rsoudre.
a e

172

ANNEXE A. LE COUT DUN ALGORITHME

Annexe B

Morceaux de Java
1

Un langage plutt classe


o

Java est un langage objet avec des classes. Les classes ont une double fonction : structurer
les programmes et dire comment on construit les objets. Pour ce qui est de la seconde fonction,
il nest pas si facile de dnir ce quest exactement un objet dans le cas gnral. Disons quun
e
e e
objet poss`de un tat et des mthodes qui sont des esp`ces de fonctions propres ` chaque objet.
e
e
e
e
a
Par exemple tous les objets poss`dent une mthode toString sans argument et qui renvoie
e
e
une cha reprsentant lobjet et normalement utilisable pour lachage. La section 2 dcrit la
ne
e
e
construction des objets ` partir des classes.
a
Mais dcrivons dabord la structuration des programmes ralise par les classes. Les classes
e
e e
regroupent des membres qui sont le plus souvent des variables (plus prcisment des champs)
e e
et des mthodes, mais peuvent aussi tre dautres classes.
e
e

1.1

Programme de classe

Un programme se construit ` partir de une ou plusieurs classes, dont une au moins contient
a
une mthode main qui est le point dentre du programme. Les variables des classes sont les vae
e
riables globales du programme et leurs mthodes sont les fonctions du programme. Une variable
e
ou une mthode qui existent d`s que la classe existe sont dites statiques.
e
e
Commenons par un programme en une seule classe. Par exemple, la classe simple suivante
c
est un programme qui ache Coucou ! sur la console :
class Simple {
static String msg = "Coucou !" ;

// dclaration de variable
e

public static void main (String [] arg) // dclaration de mthode


e
e
{
System.out.println(msg) ;
}
}
Cette classe ne sert pas ` fabriquer des objets. Elle se sut ` elle mme. Par consquent tout
a
a
e
e
ce quelle dnit (variable msg et mthode main) est statique. Par re-consquent, toutes les
e
e
e
dclarations sont prcdes du mot-cl static, car si on ne met rien les membres ne sont
e
e e e
e
pas statiques. Si le source est contenu dans un chier Simple.java, il se compile par javac
Simple.java et se lance par java Simple. Cest une bonne ide de mettre les classes dans des
e
chiers homonymes, ca permet de sy retrouver.

En termes de programmation objet, la mthode main invoque la mthode println de lobjet


e
e
System.out, avec largument msg. System.out dsigne la variable out de la classe System,
e
173

174

ANNEXE B. MORCEAUX DE JAVA

qui fait partie de la biblioth`que standard de Java. Notons que msg est en fait Simple.msg,
e
mais dans la classe Simple, on peut se passer de rappeler que msg est une variable de la classe
Simple, alors autant en proter.
Reste ` se demander quel est lobjet rang dans System.out. Et bien, disons que cest un
a
e
objet dune autre classe (la classe PrintStream) qui a t mis l` par le syst`me Java et ne nous
ee
a
e
en proccupons plus pour le moment.
e

1.2

Complment : la mthode main


e
e

La dclaration de cette mthode doit obligatoirement tre de la forme :


e
e
e
public static void main (String [] arg)
En plus dtre statique, la mthode main doit imprativement tre publique (mot-cl public) et
e
e
e
e
e
prendre un tableau de cha en argument. Le sens du mot-cl public est expliqu plus loin.
ne
e
e
Le reste des obligations porte sur le type de largument de main (son nom est libre). Le tableau
de cha est initialis par le syst`me pour contenir les arguments de la ligne de commande. De
ne
e
e
sorte que lon peut facilement crire une commande echo en Java.1
e
class Echo {
public static void main (String [] arg) {
for (int i = 0 ; i < arg.length ; i++) {
System.out.println(arg[i]);
}
}
}
Ce qui nous donne apr`s compilation :
e
% java Echo coucou foo bar
coucou
foo
bar

1.3

Collaboration de classe

La classe-programme Simple utilise dj` une autre classe, la classe System crite par les
ea
e
auteurs du syst`me Java. Pour structurer vos programmes, vous pouvez (devez) vous aussi crire
e
e
plusieurs classes. Par exemple, rcrivons le programme simple ` laide de deux classes. Le message
e
a
est fourni par une classe Message
class Message {
static String msg = "Coucou !" ;
}
Tandis que le programme est modi ainsi :
e
class Simple {
public static void main (String [] arg) { // dclaration de mthode
e
e
System.out.println(Message.msg) ;
}
}
Si on met la classe Message dans un chier Message.java, elle sera compile automatiquement
e
lorsque lon compile le chier Simple.java (par javac Simple.java). Encore une bonne raison
pour mettre les classes dans des chiers homonymes.
1

echo est une commande Unix qui ache ses arguments


1. UN LANGAGE PLUTOT CLASSE

1.4

175

Mance de classe
e

Lorsque lon fabrique un programme avec plusieurs classes, lune dentre elles contient la
mthode main. Les autres fournissent des services, en gnral sous forme de mthodes accessibles
e
e e
e
a
` partir des autres classes.
Supposons que la classe Hello doit fournir un message de bienvenue, en anglais ou en franais.
c
On pourra crire.
e
class Hello {
private static String hello ;
static void setEnglish() { hello = "Hello!" ; }
static void setFrench() { hello = "Coucou !" ; }
static String getHello() { return hello ; }
static { setEnglish() ; }
}
Classe utilise par une nouvelle classe Simple.
e
class Simple {
public static void main (String [] arg) {
System.out.println(Hello.getHello()) ;
}
}
La variable hello est prive (mot-cl private) ce qui interdit son acc`s ` partir de code qui
e
e
e a
nest pas dans la classe Hello . Une mthode getHello est donc fournie, pour pouvoir lire le
e
message. Deux autres mthodes laissent la possibilit aux utilisateurs de la classe de slectionner
e
e
e
le message anglais ou le message franais. Enn, le bout de code static { setEnglish() ; }
c
est excut lors de la cration de la classe en machine, ce qui assure le choix initial de la langue
e e
e
du message. Finalement, la conception de la classe Hello garantit une proprit : ` tout instant,
ee a
Hello .hello contient ncessairement un message de bienvenue en franais ou en anglais.
e
c
La pratique de restreindre autant que possible la visibilit des variables et mthodes amliore
e
e
e
a
` la fois la sret et la structuration des programmes.
u e
Chaque classe propose un service, qui sera maintenu mme si la ralisation de ce service
e
e
change. Il est alors moins risqu de modier la ralisation dun service. En outre, puisque
e
e
seul le code de la classe peut modier les donnes prives, on peut garantir que ces donnes
e
e
e
seront dans un certain tat, puisquil sut de contrler le code dune seule classe pour
e
o
sen convaincre.
La structure des programmes est plus claire car, pour comprendre comment les diverses
classes interagissent, il sut de comprendre les mthodes (et variables) exportes (i.e.,
e
e
non-prives) des classes. En fait il sut normalement de comprendre les dclarations des
e
e
mthodes exportes assorties dun commentaire judicieux.
e
e
On parle dabstraction, la sparation en classe segmente le programme en units plus petites,
e
e
dont on na pas besoin de tout savoir.
`
A lintrieur de la classe elle-mme, la dmarche dabstraction revient ` crire plusieurs
e
e
e
a e
mthodes, chaque mthode ralisant une tche spcique. Les classes elle-mmes peuvent tre
e
e
e
a
e
e
e
regroupes en packages, qui constituent une nouvelle barri`re dabstraction. La biblioth`que de
e
e
e
Java, qui est norme, est structure en packages.
e
e
Le dcoupage en packages, puis en classes, puis en mthodes, qui interagissent selon des
e
e
conventions claires qui disent le quoi et cachent les dtails du comment, est un fondement de la
e
bonne programmation, cest-`-dire de la production de programmes comprhensibles et donc de
a
e
programmes qui sont (plus) facilement mis au point, puis modis.
e

176

ANNEXE B. MORCEAUX DE JAVA

Il y a en Java quatre niveaux de visibilit, du plus restrictif au plus permissif.


e
private : visible de la classe.
Rien : visible du package.
protected visible du package et des classes qui hritent de la classe (voir ci-dessous).
e
public : visible de partout.
Nous connaissons dj` quelques emplois de public.
ea
Toutes les classes dont les sources sont dans le rpertoire courant sont membres dun
e
mme package implicite. La classe qui initialise le syst`me dexcution de Java, puis lance
e
e
e
le programme de lutilisateur, nest pas membre de ce package. Il est donc logique que
main soit dclare public. Noter que dclarer public les autres mthodes de vos classes
e
e
e
e
na aucun sens, ` moins dtre train dcrire un package.
a
e
e
La dclaration compl`te de la mthode toString des objets est
e
e
e
public String toString() ;
Et cest logique, puisquil est normal de pouvoir acher un objet de nimporte o` dans le
u
programme.
Quand vous lisez la documentation dune classe de la biblioth`que (par exemple String)
e
vous avez peut tre dj` remarqu que tout est public (ou tr`s rarement protected).
e
ea
e
e

1.5

Reproduction de classe par hritage


e

La plus grande part de la puissance de la programmation objet rside dans le mcanisme de


e
e
lhritage. Comme premi`re approche, nous examinons rapidement lhritage et seulement du
e
e
e
point de vue de la classe. Soit une classe Foo qui hrite dune classe Bar.
e
class Foo extends Bar {
...
}
e
e
La classe Foo dmarre dans la vie avec toutes les variables et toutes les mthode de la classe
Bar. Mais la classe Foo ne va pas se contenter de dmarrer dans la vie, elle peut eectivement
e
tendre la classe dont elle hrite en se donnant de nouvelles mthodes et de nouvelles variables.
e
e
e
Elle peut aussi rednir les mthodes et variables hrites. Par exemple, on peut construire une
e
e
e e
classe HelloGoodBye qui ore un message dadieu en plus du message de bienvenue de Hello ,
et garantit que les deux messages sont dans la mme langue.
e
class HelloGoodBye extends Hello {
private static String goodbye ;
static void setEnglish() { Hello.setEnglish() ; goodbye = "Goodbye!" ; }
static void setFrench() { Hello.setFrench() ; goodbye = "Adieu !" ; }
static String getGoodBye() { return goodbye ; }
static { setEnglish() ; }
}
On note que deux mthodes sont rednies (setEnglish et setFrench) et quune mthode est
e
e
e
ajoute (getGoodBye). La mthode setEnglish ci-dessus doit, pour assurer la cohrence des
e
e
e
u
deux messages, appeler la mthode setEnglish de la classe Hello , do` lemploi dune notation
e
compl`te Hello .setEnglish.
e
Comme dmontr par la nouvelle et derni`re classe Simple, la classe HelloGoodBye a bien
e
e
e
reu la mthode getHello en hritage.
c
e
e
class Simple {
public static void main (String [] arg) {

2. OBSCUR OBJET

177

System.out.println(HelloGoodBye.getHello()) ;
System.out.println(HelloGoodBye.getGoodBye()) ;
}
}
En fait, lhritage des classes na que peu dintrt en pratique, lhritage des objets est bien
e
ee
e
plus utile.

Obscur objet

Dans cette section, nous examinons la dialectique question de la classe et de lobjet.

2.1

Utilisation des objets

Sans mme construire des objets nous mmes, nous en utilisons forcment, car lenvironnee
e
e
ment dexcution de Java est principalement en style objet. Autrement dit on sait dj` que les
e
ea
objets ont des mthodes.
e
(1) Les tableaux sont presque des objets.
(2) Les cha
nes String sont des objets.
(3) La biblioth`que construit des objets dont nous appelons les mthodes, voir out.println().
e
e
Les objets poss`dent aussi des champs galement appels variables dinstance, auxquels on acc`de
e
e
e
e
par la notation en (( . )) comme pour les variables de classes. Par exemple, si t est un tableau
t.length est la taille du tableau.
La biblioth`que de Java emploie normment les objets. Par exemple, le point est un objet
e
e
e
de la classe Point du package java.awt. On cre un nouveau point par un appel de constructeur
e
dont la syntaxe est :
Point p = new Point () ; // Point origine
On peut aussi crer un point en donnant explicitement ses coordonnes (enti`res) au construce
e
e
teur :
Point p = new Point (100,100) ;
On dit que le constructeur de la classe Point est surcharg (overloaded ), cest-`-dire quil y
e
a
a en fait deux constructeurs qui se distinguent par le type de leurs arguments. Les mthodes,
e
statiques ou non, peuvent galement tre surcharges.
e
e
e
On peut ensuite accder aux champs dun point ` laide de la notation usuelle. Les points
e
a
ont deux champs x et y qui sont leurs coordonnes horizontales et verticales :
e
i f (p.x == p.y)
System.out.println("Le point est sur la diagonale");
Les points poss`dent aussi des mthodes, par exemple la mthode distance, qui calcule la
e
e
e
distance euclidienne entre deux points, renvoye comme un
e
ottant double prcision double.
e
Un moyen assez compliqu dacher une approximation de 2 est donc
e
System.out.println(new Point ().distance(new Point (1, 1))) ;
Les objets sont relis aux classes de deux faons :
e
c
Lobjet est cr par un constructeur dni dans une classe. Ce constructeur dnit la classe
ee
e
e
dorigine de lobjet, et lobjet garde cette classe-l` toute sa vie.
a
Les classes sont aussi plus ou moins les types des objets, quand nous crivons
e
Point p = . . .

178

ANNEXE B. MORCEAUX DE JAVA

Nous dclarons une variable de type Point. Dans certaines conditions (voir les sections
e
2.3 et III.1.2), la classe-type peut changer au cours de la vie lobjet. Le plus souvent, cela
signie quun objet dont la classe reste immuable, peut, dans certaines conditions, tre
e
rang dans une variable dont le type est une autre classe.
e
Un premier exemple (assez extrme) de cette distinction est donn par null . La valeur null
e
e
na ni classe dorigine (elle nest pas cr par new), ni champs, ni mthodes, ni rien, mais alors
ee
e
rien du tout. En revanche, null appartient ` toutes les classes-types.
a

2.2

Fabrication des objets

Notre ide est de montrer comment crer des objets tr`s similaires aux points de la section
e
e
e
prcdente. On cre tr`s facilement une classe des paires (dentiers) de la faon suivante :
e e
e
e
c
class Pair {
int x ; int y ;
Pair () { this(0,0) ; }
Pair (int x, int y) { this.x = x ; this.y = y ; }
double distance(Pair p) {
int dx = p.x - this.x ;
int dy = p.y - this.y ;
return Math.sqrt (dx*dx + dy*dy) ; // Math.sqrt est la racine carre
e
}
}
Nous avons partout explicit les acc`s aux variables dinstance et par this .x et this .y. Nous
e
e
nous sommes aussi laisss aller ` employer la notation this (0,0) dans le constructeur sans
e
a
arguments, cela permet de partager le code entre constructeurs.
On remarque que les champs x et y ne sont pas introduits par le mot-cl static. Chaque
e
objet poss`de ses propres champs, le programme
e
Pair p1 = new Pair (0, 1) ;
Pair p2 = new Pair (2, 3) ;
System.out.println(p1.x + ", " + p2.y) ;
ache (( 0, 3 )), ce qui est somme toute peu surprenant. De mme, la mthode distance
e
e
est propre ` chaque objet, ce qui est logique. En eet, si p est une autre paire, les distances
a
p1.distance(p) et p2.distance(p) nont pas de raison particuli`res dtre gales. Rien nempche
e
e e
e
de mettre des membres static dans une classe ` crer des objets. Le concepteur de la classe Pair
a e
peut par exemple se dire quil est bien dommage de devoir crer deux objets si on veut sime
plement calculer une distance. Il pourrait donc inclure la mthode statique suivante dans sa
e
classe Pair .
static double distance(int x1, int y1, int x2, int y2) {
int dx = x2-x1, dy = y2-y1 ;
return Math.sqrt(dx*dx+dy*dy) ;
}
Il serait alors logique dcrire la mthode distance dynamique plutt ainsi :
e
e
o
double distance(Pair p) { return distance(this.x, this.y, p.x, p.y) ; }
O` bien sr, distance ( this .x,. . . se comprend comme Pair .distance ( this .x,. . .
u
u

2. OBSCUR OBJET

2.3

179

Hritage des objets, sous-typage


e

Lhritage dans toute sa puissance sera abord au cours suivant. Mais nous pratiquons dj`
e
e
ea
lhritage sans le savoir. En eet, les objets des classes que nous crivons ne dmarrent pas tout
e
e
e
nus dans la vie. Toute classe hrite implicitement (pas besoin de extends, voir la section 1.5)
e
de la classe Object. Les objets de la classe Object (et donc tous les objets) poss`dent quelques
e
mthodes, dont la fameuse mthode toString. Par consquent, le code suivant est accept par
e
e
e
e
le compilateur, et sexcute sans erreur. Tout se passe exactement comme si nous avions crit
e
e
une mthode toString, alors que cette mthode est en fait hrite.
e
e
e e
Pair p = new Pair (1, 0) ;
String repr = p.toString() ;
System.out.print(repr) ;
Ce que renvoie la mthode toString des Object nest pas bien beau.
e
Pair@10b62c9
e
On reconna le nom de la classe Pair suivi dun nombre en hexadcimal qui est plus ou moins
t
ladresse dans la mmoire de lobjet dont on a appel la mthode toString.
e
e
e
Mais nous pouvons rednir (override) la mthode toString dans la classe Pair .
e
e
// public car il faut respecter la dclaration initiale
e
public String toString() {
return "(" + x + ", " + y + ")" ;
}
Et lachage de p.toString() produit cette fois ci le rsultat bien plus satisfaisant (1, 0).
e
Mme si cest un peu bizarre, il nest au fond pas tr`s surprenant que lorsque nous appelons
e
e
la mthode toString de lintrieur de la classe Pair comme nous le faisons ci-dessus, ce soit la
e
e
nouvelle mthode toString qui est appele. Mais crivons maintenant plus directement :
e
e
e
System.out.print(p) ;
Et nous obtenons une fois encore lachage (1, 0). Or, nous aurons beau parcourir la liste des
neuf dnitions de mthodes print surcharges de la classe PrintStream, de print(boolean b)
e
e
e
a
` print(Object obj), nous navons bien videmment aucune chance dy trouver une mthode
e
e
print( Pair p).
e
Mais un objet de la classe Pair peut aussi tre vu comme un objet de la classe Object. Cest
le sous-typage : le type des objets Pair est un sous-type du type des objets Object (penser
sous-ensemble : Pair Object). En Java, lhritage entra le sous-typage, on dit alors plus
e
ne
synthtiquement que Pair est une sous-classe de Object.
e
On note que le compilateur proc`de dabord ` un sous-typage (il se dit quun objet Pair est
e
a
un objet Object), pour pouvoir rsoudre la surcharge (il slectionne la bonne mthode print
e
e
e
parmi les neuf possibles). La conversion de type vers un sur-type est assez discr`te, mais on peut
e
lexpliciter.
System.out.print((Object)p) ; // Change le type explicitement
Cest donc nalement la mthode print(Object obj) qui est appele. Mais ` lintrieur de
e
e
a
e
cette mthode, on ne sait pas que obj est en fait un objet Pair . En simpliant un peu, le code
e
de cette mthode est quivalent ` ceci
e
e
a
public void print (Object obj) {
i f (obj == null) {
this.print("null") ;
} else {
this.print(obj.toString()) ;

180

ANNEXE B. MORCEAUX DE JAVA

}
}
Cest-`-dire que, au cas de null pr`s, la mthode print(Object obj) appelle print(String s)
a
e
e
avec comme argument obj.toString(). Et l`, il y a une petite surprise : cest la mthode
a
e
toString() de la classe dorigine (la (( vraie )) classe de obj) qui est appele, et non pas la
e
mthode toString() des Object. Cest ce que lon appelle parfois la liaison tardive.
e

Constructions de base

Nous voquons des concepts qui regardent lensemble des langages de programmation, et pas
e
seulement les langages objet.

3.1

Valeurs, variables

Par valeur on entend en gnral le rsultat de lvaluation dune expression du langage de


e e
e
e
programmation. Si on prend un point de vue mathmatique, une valeur peut tre ` peu pr`s
e
e
a
e
nimporte quoi, un entier (de Z), un ensemble dentiers etc. Si on prend un point de vue technique,
une valeur est ce que lordinateur manipule facilement, ce qui entra des contraintes : par
ne
exemple, un entier na pas plus de 32 chires binaires (pas plus de 32 bits). Dans les descriptions
qui suivent nous entendons valeur plutt dans ce sens technique. Par variable on entend en
o
gnral (selon le point de vue technique) une case qui poss`de un nom o` peut tre range une
e e
e
u
e
e
valeur. Une variable est donc une portion nomme de la mmoire de la machine.
e
e
3.1.1

Scalaires et objets

Il y a en Java deux grandes catgories de valeurs les scalaires et les objets. La distinction
e
est en fait technique, elle tient ` la faon dont ces valeurs sont traites par la machine, ou plus
a
c
e
exactement sont ranges dans la mmoire. Les valeurs scalaires se susent ` elle-mmes. Les
e
e
a
e
valeurs des objets sont des rfrences. Une rfrence (( pointe )) vers quelque chose (une zone de
ee
ee

la mmoire) rfrence est un nom civilis pour pointeur ou adresse en mmoire. Ecrivons par
e
ee
e
e
exemple
int x = 1 ;
int [] t = {1, 2, 3} ;
Les variables x et t sont deux cases, qui contiennent chacune une valeur, la premi`re valeur
e
tant scalaire et la seconde une rfrence. Un schma rsume la situation.
e
ee
e
e
x 1

t
1 2 3

Le tableau {1, 2, 3} correspond ` une zone de mmoire qui contient des trois entiers, mais la
a
e
valeur qui est range dans la variable t est une rfrence pointant vers cette zone. Le schma
e
ee
e
est une simplication de ltat de la mmoire, les zones mmoires apparaissent comme des cases
e
e
e
(les variables portent un nom) et les rfrences apparaissent comme des `ches qui pointent vers
ee
e
les cases.
Si x et y sont deux variables, la construction y = x se traduit par une copie de la valeur
contenue dans la variable x dans la variable y, que cette valeur soit une rfrence ou non. Ainsi,
ee
le code
int y = x ;
int [] u = t ;

3. CONSTRUCTIONS DE BASE

181

produit ltat mmoire simpli suivant.


e
e
e
x 1 y 1

u
1 2 3

Le schma permet par exemple de comprendre pourquoi (ou plutt comment) le programme
e
o
suivant ache 4.
int [] t = {1, 2, 3} ;
int [] u = t ;
u[1] = 4 ;
System.out.println(t[1]) ;
Il existe une rfrence qui ne pointe nulle part null, nous pouvons lemployer partout o` une
ee
u
rfrence est attendue.
ee
int [] t = null ;
Dans les schmas nous reprsentons null ainsi :
e
e
t
Puisque null ne pointe nulle part il nest pas possible de le (( drfrencer )) cest ` dire daller
e ee
a
voir o` il pointe. Un essai par exemple de t[0] dclenche une erreur ` lexcution.
u
e
a
e

Egalit des valeurs


e
Loprateur dgalit == de Java sapplique aux valeurs ainsi que loprateur dirence !=.
e
e
e
e
e
Si lgalit de deux scalaires ne pose aucun probl`me, il faut comprendre que == entre deux
e
e
e
objets traduit lgalit des rfrences et que deux rfrences sont gales, si et seulement si elles
e
e
ee
ee
e
pointent vers la mme zone de mmoire. Autrement dit, le programme
e
e
int [] t = {1, 2, 3} ;
int [] u = t ;
int [] v = {1, 2, 3} ;
System.out.println("t==u : " + (t == u) + ", t==v : " + (t == v)) ;
ache t==u : true, t==v : false. Les rfrences t et u sont gales parce quelles pointent
ee
e
vers le mme objet. Les rfrences t et v qui pointent vers des objets distincts sont distinctes.
e
ee
Cela peut se comprendre si on revient aux tats mmoire simplis.
e
e
e
t

u
1 2 3

v
1 2 3

On dit parfois que == est lgalit physique. Lgalit physique donne parfois des rsultats sure
e
e
e
e
prenants. Soit le programme Test simple suivant
class Test {
public static void main (String [] arg) {
String t = "coucou" ;
String u = "coucou" ;
String v = "cou" ;
String w = v + v ;
System.out.println("t==u : " + (t == u) + ", t==w : " + (t == w)) ;
}
}

182

ANNEXE B. MORCEAUX DE JAVA

Une fois compil et lanc, ce programme ache t==u : true, t==w : false. Ce qui rv`le
e
e
e e
que les cha
nes (objets) rfrencs par t et u sont exactement les mmes, tandis que w est une
ee
e
e
autre cha
ne.
t

u
"coucou"

w
"coucou"

La plupart du temps, un programme a besoin de savoir si deux cha


nes ont exactement les
mmes caract`res et non pas si elles occupent la mme zone de mmoire. Il en rsulte principae
e
e
e
e
lement quil ne faut pas tester lgalit des cha
e
e
nes (et ` vrai dire des objets en gnral) par ==.
a
e e
Dans le cas des cha
nes, il existe une mthode equals spcialise (voir 6.1.3) qui compare les
e
e
e
cha
nes caract`re par caract`re. La mthode equals est lgalit structurelle des cha
e
e
e
e
e
nes.

3.2

Types

Gnralement un type est un ensemble de valeurs. Ce qui ne nous avance pas beaucoup !
e e
Disons plutt quun type regroupe des valeurs qui vont naturellement ensemble, parce quelles
o
ont des reprsentations en machine identiques (byte occupe 8 bits en machine, int en occupe 32),
e
ou surtout parce quelles vont naturellement ensemble (un objet Point est un point du plan, un
objet Object est un objet).
3.2.1

Typage statique

Java est un langage typ statiquement, cest-`-dire que si lon crit un programme incorrect
e
a
e
du point de vue des types, alors le compilateur refuse de le compiler. Il sagit non pas dune
contrainte irraisonne impose par des informaticiens fous, mais dune aide ` la mise au point
e
e
a
des programmes : la majorit des erreurs stupides ne passe pas la compilation. Par exemple, le
e
programme suivant contient deux erreurs de type :
class MalType {
static int incr (int i) { return i+1 ; }
static void mauvais() {
System.out.println(incr(true)) ; // Mauvais type
System.out.println(incr()) ;
// Oubli dargument
}
}
e
La compilation de la classe MalType choue :
% javac MalType.java
MalType.java:9: Incompatible type for method. Cant convert boolean to int.
System.out.println(incr(true)) ; // Mauvais type
^
MalType.java:10: No method matching incr() found in class MalType.
System.out.println(incr()) ;
// Oubli dargument
^
2 errors
Le syst`me de types de Java assez puissant et les classes permettent certaines audaces. La
e
plus courante se voit tr`s bien dans lutilisation de System.out.println (acher une ligne sur
e
la console), on peut passer nimporte quoi ou presque en argument, spar par des (( + )) :
e e

3. CONSTRUCTIONS DE BASE

183

System.out.println ("boolen :" + (10 < 11) + "entier : " + (314*413)) ;


e
Cela peut se comprendre si on sait que + est loprateur de concatnation sur les cha
e
e
nes, que
System.out.println prend une cha en argument et que le compilateur ins`re des converne
e
sions l` o` il sent que cest utile.
a u
Il y a huit types scalaires, ` savoir dabord quatre types (( entier )), byte, short, int et
a
long. Ces entiers sont en fait des entiers modulo 2p (avec p respectivement gal ` 8, 16, 32
e
a
et 64) les reprsentants des classes dquivalence modulo 2p sont centrs autour de zro, ceste
e
e
e
p1 (inclus) et 2p1 (exclu). On dit aussi que les quatre types entiers
a
`-dire compris entre 2
correspondent aux entiers reprsentables sur 8, 16, 32 et 64 chires binaires, selon la technique
e
dite du complment ` la base (loppos dun entier n est 2p n).
e
a
e
Les autres types scalaires sont les boolens boolean (deux valeurs true et false ), les
e
caract`res char et deux sortes de nombres ottants, simple prcision oat et double prcision
e
e
e
double. Lconomie de mmoire ralise en utilisant les oat (sur 32 bits) ` la place des
e
e
e e
a
double (64 bits) na dintrt que dans des cas spcialiss.
ee
e
e
Les tableaux sont un cas ` part (voir 3.6). Les classes sont des types pour les objets. Mais
a
attention, les types sont en fait bien plus une proprit du source des programmes, que des
ee
valeurs lors de lexcution. Nous essayons dviter de parler du (( type )) dun objet. En revanche,
e
e
il ny a aucune dicult ` parler du type dune variable qui peut contenir un objet, ou du type
ea
dun argument objet.
3.2.2

Conversion de type

La syntaxe de la conversion de type est simple


(type)expression
La smantique est un peu moins simple. Une conversion de type sadresse dabord au compilateur.
e
Elle lui dit de changer son opinion sur expression. Par exemple, comme nous lavons dj` vu
ea
en page 179
Pair p = . . . ;
System.out.println((Object)p) ;
dit explicitement au compilateur que lexpression p (normalement de type Pair ) est vue comme
un Object. Comme Pair est une sous-classe de Object (ce que sait le compilateur), ce changement dopinion est toujours possible et (Object)p ne correspond ` aucun calcul au cours de
a
lexcution. En fait, dans ce cas dune conversion vers un sur-type, on peut mme omettre la
e
e
conversion explicite, le compilateur saura changer son opinion tout seul si besoin est.
Il nen va pas de mme dans lautre sens
e
Object o = . . . ;
Pair p = (Pair)o ;
System.out.println(p.x) ;
Le compilateur accepte ce source, la conversion est ncessaire pour le faire changer dopinion sur
e
la valeur dabord range dans o, puis dans p : cet objet est nalement une paire, et poss`de donc
e
e
une variable dinstance x. Mais ici, rien ne le garantit, et lexpression ( Pair )o correspond ` une
a
vrication lors de lexcution. Si cette vrication choue, alors le programme choue aussi (en
e
e
e
e
e
lanant lexception ClassCastException). Il est malheureusement parfois ncessaire dutiliser
c
e
de telles conversions (vers un sous-type) voir III.3.2, mme quand on programme proprement en
e
ne mlangeant pas des objets de classes distinctes.
e
On peut aussi convertir le type des scalaires, cette fois les conversions entra
nent des transformations des valeurs converties, car les reprsentations internes des scalaires ne sont pas toutes
e
identiques. Par exemple, si on change un int (32 bits) en long (64 bits), la machine ralise un
e

184

ANNEXE B. MORCEAUX DE JAVA

certain travail. Ce calcul nest pas ici une vrication, mais un changement de reprsentation.
e
e
La plupart des conversions de types entre scalaires restent implicites et sont eectues ` loccae a
sion des oprations. Si par exemple on crit 1.5 + 2, alors le compilateur arrive ` comprendre
e
e
a
1.5 + (double)2, an deectuer une addition entre double. Il y a un cas o` on ins`re soitu
e
mme ce type de conversions.
e
// a et b sont des int, on veut calculer le pourcentage a/b
double p = 100 * (((double)a)/b) ;
(Notez labondance de parenth`ses pour bien spcier les arguments de la conversion et des
e
e
oprations.) Si on ne change pas explicitement un des arguments de la division en double,
e
alors (( / )) est la division euclidienne, alors que lon veut ici la division des ottants. On aurait
dailleurs pu crire :
e
double p = (100.0 * a) / b ;
En eet, le compilateur change alors a en double, pour avoir le mme type que lautre argument
e
de la multiplication. Ensuite, la division est entre un ottant et un int , et ce dernier est converti
an deectuer la division des ottants.
Le compilateur neectue jamais tout seul une conversion qui risque de faire perdre de lin`
formation. A la place, quand une telle conversion est ncessaire pour typer le programme, il
e
choue. Par exemple, prenons la partie enti`re dun double.
e
e
// d est une variable de type double, on veut prendre sa partie enti`re
e
int e = d ;
Le compilateur, assez bavard, nous dit :
T.java:4:
found
:
required:
int e

possible loss of precision


double
int
= d ;
^

Dans ce cas, on doit prendre ses responsabilits et crire


e
e
int e = (int)d ;
Il faut noter que nous avons eectivement pris le risque de faire nimporte quoi. Si d est trop
gros pour que sa partie enti`re tienne dans 32 bits (suprieure ` 231 1), alors on a un rsultat
e
e
a
e
trange. Le programme
e
double d = 1e100 ; // 10100
System.out.println(d + ", " + (int)d) ;
conduit ` acher 1.0E100, 2147483647.
a
3.2.3

Complment : caract`res
e
e

Les caract`res de Java sont dnis par deux normes internationales synchronises ISO/e
e
e
CEI 10646 et Unicode. Le nom gnrique le plus appropri semblant tre UTF-16. En simplie e
e
e
ant, une valeur du type char occupe 16 chires binaires et chaque valeur correspond ` un
a
caract`re. Cest simpli parce quen fait Unicode dnit plus de 216 caract`res et quil faut pare
e
e
e
fois plusieurs char pour faire un caract`re Unicode. Dans la suite nous ne tenons pas compte de
e
cette complexit supplmentaire introduite notamment pour reprsenter tous les idogrammes
e
e
e
e
chinois.
Un char a une double personnalit, est dune part un caract`re (comme a, etc.) et
e
e
e
dautre part un entier sur 16 bits (disons le (( code )) du caract`re), qui contrairement ` short est
e
a

3. CONSTRUCTIONS DE BASE

185

toujours positif. La premi`re personnalit dun caract`re se rv`le quand on lache, la seconde
e
e
e
e e
quand on le compare ` un autre caract`re. Les 128 premiers caract`res (cest-`-dire ceux dont les
a
e
e
a
codes sont compris entre 0 et 127) correspondent exactement ` un autre standard international
a
bien plus ancien, lASCII.
LASCII regroupe notamment les chires de 0 ` 9 et les lettres (non-accentues) mia
e
nuscules et majuscules, mais aussi un certain nombre de caract`res de contrle, dont les plus
e
o
frquents expriment le (( retour a la ligne )). Une petite digression va nous montrer que la stane
`
dardisation du jeu de caract`res nest malheureusement pas susante pour tout normaliser. En
e
Unix un retour ` la ligne sexprime par le caract`re line feed not \n, en Windows cest la
a
e
e
squence dun carriage return not \r et dun line feed, et en Mac OS, cest un carriage return
e
e
tout seul !
Le plus souvent ces dtails restent cachs, par exemple System.out.println() eectue
e
e
toujours un retour ` la ligne sur lachage de la console, cest le code de biblioth`que qui se
a
e
charge de fournir les bons caract`res ` la console selon le syst`me sur lequel le programme est en
e
a
e
train de sexcuter. Toutefois des probl`mes peuvent surgir en cas de transfert de chiers dun
e
e
syst`me ` lautre. . .
e
a

3.3

Dclarations
e

De faon gnrale, une dclaration tablit une correspondance entre un nom et une construcc
e e
e
e
tion nommable (variable, mthode, mais aussi constante camoue en variable). Les dclarations
e
e
e
de variable sont de la forme suivante :
modiers type name ;
Les modiers sont des mots-cls ( static , spcication de visibilit private etc., et nal pour
e
e
e
les constantes), le type est un type (genre int , int [] ou String) et name est un nom de
variable. Une bonne pratique est dinitialiser les variables d`s leur dclaration, ca vite bien des
e
e
e
oublis. Pour ce faire :
modiers type name = expression ;
O` expression est une expression du bon type. Par exemple, voici trois dclarations de variables,
u
e
de types respectifs (( entier )), cha et tableau de cha :
ne
ne
int i = 0;
String message = "coucou" ;
String [] jours =
{"dimanche", "lundi", "mardi", "mercredi",
"jeudi", "vendredi", "samedi"} ;
Les dclarations de (( variable )) sont en fait de trois sortes :
e
(1) Dans une classe,
(a) Une dclaration avec le modicateur static dnit une variable (un champ) de la
e
e
classe.
(b) Une dclaration sans le modicateur static dnit une variable dinstance des objets
e
e
de la classe.
(2) Dans une mthode, une dclaration de variable dnit une variable locale. Dans ce cas seul
e
e
e
le modicateur nal est autoris.
e
Ces trois sortes de (( variables )) sont toutes des cases nommes mais leurs comportements
e
sont dirents, voire tr`s dirents : les variables locales sont autonomes, et leurs noms obissent
e
e
e
e
a
` la porte lexicale (voir structure de bloc dans 3.4), les deux autres (( variables )) existent
e

186

ANNEXE B. MORCEAUX DE JAVA

comme sous-parties respectivement dune classe et dun objet ; elles sont normalement dsignes
e
e
comme C.x et o.x, o` C et o sont respectivement une classe et un objet.
u
Sur une note plus mineure, les variables de classe et dobjet non-initialises explicitement
e
contiennent en fait une valeur par dfaut qui est fonction de leur type zro pour les types
e
e
dentiers et de ottants (et pour char), false pour les boolean, et null pour les objets. En
revanche, comme le compilateur Java refuse lacc`s ` une variable locale quand celle-ci na pas
e a
t initialise explicitement, ou aecte avant lecture, il ny a pas lieu de parler dinitialisation
ee
e
e
par dfaut des variables locales.
e
Les dclarations de mthode ne peuvent se situer quau niveau des classes et suivent la forme
e
e
gnrale suivante :
e e
modiers type name (args)
Les modiers peuvent tre, static (mthode de classe), une spcication de visibilit, nal
e
e
e
e
(rednition interdite), synchronized (acc`s en exclusion mutuelle, voir le cours suivant) et
e
e
native (mthode crite dans un autre langage que Java). La partie type est le type du rsultat
e
e
e
de la mthode (void si il ny en a pas), name est le nom de la mthode et args sont les
e
e
dclarations des arguments de la mthode qui sont de btes dclarations de variables (sans le
e
e
e
e
(( ; )) nal) spares par des virgules (( , )). Lensemble de ces informations constitue la signature
e e
de la mthode.
e
Suit ensuite le corps de la mthode, entre accolades (( { )) et (( } )). Il est notable que les
e
dclarations darguments sont des dclarations de variables ordinaires. En fait on peut voir les
e
e
`
arguments dune mthode comme des variables locales presque normales. A ceci pr`s quil ny
e
e
a pas lieu dinitialiser les arguments ce qui dailleurs naurait aucun sens puisque les arguments
sont initialiss ` chaque appel de mthode.
e a
e

3.4

Principales expressions et instructions

Nous dcrivons maintenant ce qui se trouve dans le corps des mthodes, cest-`-dire le code
e
e
a
qui fait le vrai travail. Le corps dune mthode est une squence dinstructions). Les instruce
e
tions sont excutes. Une instruction (par ex. une aectation) peut inclure une expression. Les
e e
expressions sont values en un rsultat.
e
e
e
La distinction entre expressions (dont lvaluation produit un rsultat) et instructions (dont
e
e
lexcution ne produit rien) est assez hypocrite, car toute expression suivie de (( ; )) devient une
e
instruction (le rsultat est jet).
e
e
Les expressions les plus courantes sont :
Constantes Soit 1 (entier), true (boolen), "coucou !" (cha
e
ne), etc. Une constante amusante
est null , qui est un objet sans champs ni mthodes.
e
Usage de variable Soit (( x )), o` x est le nom dune variable (locale) dclare par ailleurs.
u
e
e
Mot-cl this Dans une mthode dynamique , this dsigne lobjet dont on a appel la mthode.
e
e
e
e
e
Dans un constructeur, this dsigne lobjet en cours de construction. Il en rsulte que this
e
e
nest jamais null , car si on en est arriv ` excuter un corps de mthode, cest bien que
ea e
e
lobjet dont a appel la mthode existait.
e
e
Acc`s ` un champ Si x est un nom de champ et classe un nom de classe C, alors (( C.x ))
e a
dsigne le contenu du champ m de C. De mme (( objet.x )) dsigne le champ de nom x
e
e
e
de lobjet objet. Notez que, contrairement ` x, objet nest pas forcment un nom, cest
a
e
une expression dont la valeur est un objet. Heureusement ou malheureusement, il existe
des notations abrges qui all`gent lcriture (voir 3.5), mais font parfois passer lacc`s `
e e
e
e
e a
un champ pour un usage de variable.
Appel de mthode Cest un peu comme pour les champs :
e

3. CONSTRUCTIONS DE BASE

187

statique Soit, C.m(...),


dynamique ou bien, objet.m(...).
O` m est un nom de mthode et (...) est une squence dexpressions spares par des
u
e
e
e e
virgules. Notez bien que les mmes notations abrges que pour les champs sappliquent
e
e e
au nom de la mthode. Incidemment, si une mthode a un type de retour void, son appel
e
e
nest pas vraiment une expression, cest une instruction.
Appel de constructeur Gnralement de la forme new C (...). La construction des tableaux
e e
est loccasion de nombreuses variantes, voir 3.6
Usage des oprateurs Par exemple i+1 (addition) ou i == 1 || i == 2 (oprateur galit et
e
e
e
e
oprateur ou logique). Quelques oprateurs inattendus sont donns en 7.2. Notons quun
e
e
e
(( oprateur )) en informatique est simplement une fonction dont lapplication se fait par une
e
syntaxe spciale. Lapplication des oprateurs est souvent inxe, cest-`-dire que loprateur
e
e
a
e
appara entre ses arguments. Mais elle peut tre prxe, cest-`-dire que loprateur est
t
e
e
a
e
avant son argument, comme dans le cas de la ngation des boolens ! ; ou encore postxe,
e
e
comme pour loprateur de post-incrment i++. En Java, comme souvent, les oprateurs
e
e
e
eux-mmes sont exclusivement composs de caract`res non alphabtiques (+, -, =, etc.).
e
e
e
e
Acc`s dans les tableaux Par exemple t[i], o` t est un tableau dni par ailleurs. Il nest pas
e
u
e
surprenant que lon puisse mettre une expression ` la place de i. Il est un peu plus surprea
nant que cela soit galement le cas pour t, comme par exemple dans t[i][j], ` comprendre
e
a
comme (t[i])[j] (t est un tableau de tableaux).
Aectation Par exemple i = 1, lexpression ` droite de = est calcule sa valeur est range dans
a
e
e
la variable donne ` gauche de =. En fait, on peut trouver autre chose quune variable `
e a
a
gauche de =, on peut trouver tout ce qui dsigne une case de mmoire, par exemple, un
e
e
lment de tableau t[i] ou une dsignation de champ objet.x.
ee
e
Laectation est une expression dont le rsultat est la valeur aecte. Ce qui permet des
e
e
trucs du genre i = j = 0, pour initialiser i et j ` zro. Cela se comprend si on lit cette
a e
expression comme i = (j = 0).
Expression parenthse Si e est une expression, alors (e) est aussi une expression. Cela permet
ee
essentiellement de contourner les priorits relatives des oprateurs, mais aussi de rendre
e
e
un source plus clair. Par exemple, on peut crire (i == 1) || (i == 2), cest peut-tre
e
e
plus lisible que i == 1 || i == 2.
Java a beaucoup emprunt au langage C, il reprend quelques expressions assez peu ordinaires.
e
Incrment, dcrment Soit i variable de type entier (en fait, soit e dsignation de case mmoire
e
e e
e
e
qui contient un entier). Alors lexpression i++ range i+1 dans i et renvoie lancienne valeur
de i. Lexpression i-- fait la mme chose avec i-1. Enn lexpression ++i (resp. --i) est
e
similaire, mais elle renvoie la valeur incrmente (resp. dcrmente) de i en rsultat.
e
e
e e
e
e
Aectations particuli`res La construction i op= expression est sensiblement quivalente ` i = i
e
e
a
op expression. Par exemple :
i *= 2
range deux fois le contenu de i dans i et renvoie donc ce contenu doubl. Les nauds
e
noteront que ++i est aussi i += 1.
Ces expressions avances sont gniales, mais il est de bon got de les employer avec parcimonie.
e
e
u
Que lon essaie de deviner ce que fait t[++i] *= --t[i++] et on comprendra.
Les instructions les plus courantes sont les suivantes :

Expression comme une instruction : (( e ; )) Evidemment, cette construction nest utile que si e
fait des eets de bord (cest-`-dire fait autre chose que rendre son rsultat). Cest bien sr
a
e
u
le cas dune aectation.

188

ANNEXE B. MORCEAUX DE JAVA

Squence On peut grouper plusieurs instructions en une squence dinstruction, les instructions
e
e
sexcutent dans lordre.
e
i = i + 1;
i = i + 1;
Dclarations de variables locales Une dclaration de variable (suivie de ;) est une instruction
e
e
qui rserve de la place pour la variable dclare et linitialise ventuellement.
e
e
e
e
int i = 0;
Il ne faut pas confondre aectation et dclaration, dans le premier cas, on modie le
e
contenu dune variable qui existe dj`, dans le second on cre une nouvelle variable.
ea
e
Bloc On peut mettre une squence dinstructions dans un bloc {. . . }. La porte des dclaration
e
e
e
internes au bloc steint ` la sortie du bloc. Par exemple, le programme
e
a
int i = 0 ;
{
int i = 1; // Dclaration dun nouvel i
e
System.out.println("i=" + i);
}
System.out.println("i=" + i);
ache une premi`re ligne i=1 puis une seconde i=0. Il faut bien comprendre que dun
e
achage ` lautre lusage de variable (( i )) ne fait pas rfrence ` la mme variable.
a
ee
a
e
Lorsque lon veut savoir ` quelle dclaration correspond un usage, la r`gle est de remonter
a
e
e
le source du regard vers le haut, jusqu` trouver la bonne dclaration. Cest ce que lon
a
e
appelle parfois, la porte lexicale.
e
Attention, seule la porte des variables est limite par les blocs, en revanche leet des
e
e
instruction passe all`grement les fronti`res de blocs. Par exemple, le programme
e
e
int i = 0 ;
{
i = 1; // Affectation de i
System.out.println("i=" + i);
}
System.out.println("i=" + i);
ache deux lignes i=1.
Retour de mthode On peut dans le corps dune mthode retourner ` tout moment par linse
e
a
truction (( return expression; )), o` expression est une expression dont le type est celui
u
des valeurs retournes par la mthode.
e
e
Par exemple, la mthode twice qui double son argument entier scrit
e
e
int twice (int i) {
return i+i ;
}
Si la mthode ne renvoie rien, alors return na pas dargument.
e
void rien () {
return ;
}
`
A noter que, si la derni`re instruction dune mthode est return ; (sans argument), alors
e
e
on peut lomettre. De sorte que la mthode rien scrit aussi :
e
e
void rien () { }

3. CONSTRUCTIONS DE BASE

189

Conditionnelle Cest la tr`s classique instruction if :


e
i f (i % 2 == 0) { // oprateur modulo
e
System.out.println("Cest pair") ;
} else {
System.out.println("Cest impair") ;
}
Instruction switch Cest une gnralisation de la conditionnelle aux types dentiers. Elle permet
e e
de (( brancher )) selon la valeur dun entier (de byte ` long, mais aussi char). Par exemple :
a
switch (i) {
case -1:
System.out.println("moins un") ;
break ;
case 0:
System.out.println("zro") ;
e
break ;
case 1:
System.out.println("un") ;
break ;
default:
System.out.println("beaucoup") ;
}
}
Selon la valeur de lentier i on slectionnera lune des trois clauses case, ou la clause par
e
dfaut default . Il faut surtout noter le break, qui renvoie le contrle ` la n du switch.
e
o a
En labsence de break lexcution se poursuit en squence, donc la clause suivante est
e
e
excute. Ainsi si on omet les break et que i vaut -1 on a lachage
e e
moins un
zro
e
un
beaucoup
Cette abominable particularit permet de grouper un peu les cas. Par exemple,
e
switch (i) {
case -1: case 0: case 1:
System.out.println("peu") ; break ;
default:
System.out.println("beaucoup") ;
}
Noter que si la clause se termine par return, alors break nest pas utile.
static String estimer(int i) {
switch (i) {
case -1: return "moins un" ;
e
case 0: return "zro" ;
case 1: return "un" ;
default: return "beaucoup" ;
}
}
Boucle while Cest la boucle la plus classique, celle que poss`dent tous les langages de proe
grammation, ou presque. Voici les entiers de zro ` neuf.
e a

190

ANNEXE B. MORCEAUX DE JAVA


int i = 0 ;
while (i < 10) {
System.out.println(i) ;
i = i+1 ;
}
Soit while (expression) instruction, on excute expression, qui est une expression boolenne,
e
e
si le rsultat est false cest ni, sinon on excute instruction et on recommence.
e
e

Boucle do Cest la mme chose mais on teste la condition ` la n du tour de boucle, conclusion :
e
a
on passe au moins une fois dans la boucle. Voici une autre faon dacher les entiers de 0
c
a
` 9.
int i = 0 ;
do {
System.out.println(i) ;
i = i+1 ;
} while (i < 10)
Boucle for Cest celle du langage C.
for (int i=0 ; i < 10 ; i = i+1)
System.out.println(i);
La syntaxe gnrale est
e e
for (einit ; econd ;
instruction

enext )

Lexpression einit est value une fois initialement, exceptionnellement (( lexpression )) einit
e
e
peut tre une dclaration de variable, auquel cas la porte de la variable est limite `
e
e
e
e a
la boucle. Lexpression econd est de type boolean, cest la condition teste avant chaque
e
itration (y compris la premi`re), litration a lieu si econd vaut true. Enn, enext est
e
e
e
value ` la n de chaque itration. Autrement dit une boucle for est presque la mme
e
e a
e
e
chose que la boucle while suivante.
{
einit ;
while (econd ) {
instruction
enext ;
}
}
Enn, notez que econd peut tre omise, en ce cas la condition est considre vraie. Cette
e
ee
particularit est principalement utilise dans lidiome de la boucle innie.
e
e
for ( ; ; ) { // Boucle infinie, on sort par break ou return
...
}
Gestion du contrle Certaines instructions permettent de (( sauter )) quelque-part de lintrieur
o
e
des boucles. Ce sont des formes polies de goto. Il sagit de break, qui fait sortir de la
boucle, et de continue qui commence litration suivante en sautant par dessus ce qui
e
reste de litration en cours.
e
Ainsi pour rechercher si lentier foo est prsent dans le tableau t, on peut crire
e
e
boolean trouve = false ;
for (int i = 0 ; i < t.length ; i++) {

3. CONSTRUCTIONS DE BASE

191

i f (t[i] == foo) {
trouve = true ;
break ;
}
}
// trouve == true <=> foo est dans t
Ou encore, si on veut cette fois compter les occurrences de foo dans t, on peut crire
e
int count = 0 ;
for (int i = 0 ; i < t.length ; i++) {
i f (t[i] != foo) continue ;
count++ ;
}
Noter que dans les deux cas on fait des choses un peu compliques. Dans le premier cas, on
e
pourrait faire une mthode et utiliser return, ou une boucle while sur trouve. Dans le
e
second cas, on pourrait crire le test dgalit. Dans certaines situations, ces instructions
e
e
e
sont pratiques (break plus frquemment que continue).
e

3.5

Notations compl`tes et abrges


e
e e

Pour les classes (et les constructeurs)


Quand une classe C est dnie dans un package pkg, son nom est pkg.C. Il en va dailleurs
e
de mme pour les constructeurs de la classe C. Ainsi, le nom complet de la classe Point dnie
e
e
dans le package java.awt est java.awt.Point. On est donc cens crire par exemple
ee
java.awt.Point p = new java.awt.Point(100, 50) ;
Cest tr`s lourd, pour lviter on peut introduire la dclaration suivante au dbut du chier
e
e
e
e
source
import java.awt.Point ;
Et si le nom de classe Point survient plus tard dans le chier, le compilateur sait que lon a
voulu dire java.awt.Point.
Une autre dclaration possible en dbut de chier est
e
e
import java.awt.* ;
Cela revient aux dclarations java.awt.C eectues pour toutes les classes C du package java.awt.
e
e
Pour les champs et les mthodes
e
Une variable (une mthode) dune classe C est normalement dsigne en la prxant par
e
e
e
e
C. ; mais dans le code de la classe elle-mme le nom de la variable sut. De mme, dans le
e
e
code des mthodes des objets (et des constructeurs) une variable dinstance (une mthode) est
e
e
normalement dsigne en la prxant par this . ; mais le nom de la variable (mthode) sut.
e
e
e
e
Les notations compl`tes permettent parfois dinsister sur la nature dune variable, et de sure
monter le masquage des variables dinstance et de classe par des variables locales. Ces masquages
ne sont pas toujours malvenus. Par exemple, nous avons tendance ` donner aux constructeurs des
a
arguments homonymes aux variables dinstance initialises. Ce qui reste possible en explicitant
e
les variables dinstance en tant que telles.
class List {
int val ; List next ;

192

ANNEXE B. MORCEAUX DE JAVA

List (int val, List next) { this.val = val ; this.next = next ; }


}
Dans le constructeur ci-dessus, this .val est la variable dinstance, val tout court est largument.

3.6
3.6.1

Les tableaux
Les types

Les tableaux sont presque des objets, presque car mais ils nont pas vraiment de classe, tout
se passe plus ou moins comme si, pour chaque type type, un type des tableaux dont les lments
ee
sont de type type tombait du ciel. Ce type des tableaux dont les lments sont de type type, se
ee
note (comme dans le langage C) (( type[] )). En particulier on a :
int [] ti ;
String [] ts ;

// tableau dentiers
// tableau de cha^nes

Les tableaux (( ` plusieurs dimensions )) ou matrices, nexistent pas en tant que tels, ce sont en
a
fait des tableaux de tableaux.
String [] [] tts ; // tableau de tableaux de cha^nes

3.6.2

Valeurs tableaux

Contrairement ` bien des langages, il est possible en Java, de fabriquer des tableaux direca
tement. On distingue :
La constante null avec laquelle on ne peut rien faire est aussi un tableau dont on ne peut
rien faire.
Lappel de constructeur : new type [taille], o` type est le type des lments du tableau
u
ee
et taille la taille du tableau. Par exemple, pour dclarer et initialiser un tableau t de trois
e
entiers, et un tableau ts de trois cha
nes, on crit :
e
static int [] ti = new int [3] ;
static String [] ts = new String [3] ;
Les lments du tableau sont initialiss (( par dfaut )), ` une valeur qui dpend de leur type
ee
e
e
a
e
zro pour les entiers et ottants, false pour les boolens, et null pour les objets. Cest
e
e
exactement le mme principe que pour les variables de classes et dinstance initialises par
e
e
dfaut (voir 3.3). Ainsi les deux dclarations ci-dessus produisent ltat mmoire simpli
e
e
e
e
e
0 0 0
ts
t
La notation par constructeur sapplique aussi aux tableaux de tableaux.
static String [] [] t1 = new String [3][3]

; // 3 x 3 cha^nes.

Laspect tableau de tableaux appara aussi. Il est possible domettre les derni`res tailles.
t
e
static String [] [] t2 = new String [3][]

; // 3 tableaux de cha^nes

Ce qui, compte tenu de la r`gle dinitialisation des objets ` null nous donne.
e
a
t2
t1

3. CONSTRUCTIONS DE BASE

193

Un tableau explicite, sous la forme (( {e1 , e2 ,. . . en } )), o` les ei sont des expressions du
u
type des lments du tableau. Par exemple, pour dclarer et initialiser le tableau ti des
ee
e
seize premiers entiers, on crira :
e
int [] ti = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16} ;
Dans le cas des tableaux de tableaux, on crira :
e
int [] [] tj = {{1,2}, {3}};
Ce qui nous donne ceci :
tj
3
1 2
Autrement dit tj[0] est le tableau {1,2} et tj[1] est le tableau {3}. Notons au passage
lexistence du tableau vide (tableau ` zro case) {} ` ne pas confondre avec null .
a e
a
La notation explicite ci-dessus est une esp`ce dabrviation rserve ` linitialisation des
e
e
e
e a
variables. Dans un autre contexte, il faut indiquer le type du tableau en invoquant le
constructeur. Ainsi new int [] {1,2} et new int [] [] {{1,2}, {3}} sont des valeurs tableaux.
3.6.3

Oprations sur les tableaux


e

Il y a deux oprations primitives :


e
Accder ` un lment : t[ei ], o` t est le tableau et ei une expression de type entier.
e
a
ee
u
Attention, les indices commencent a zro.
` e
Trouver la longueur dun tableau : cest une syntaxe objet, tout se passe comme si la
longueur tait le champ length du tableau, soit t.length.
e
La classe de biblioth`que Arrays (package java.util) fournit quelques mthodes toutes stae
e
tiques qui op`rent sur les tableaux, et notamment des tris.
e

3.7

Passage par valeur

La r`gle gnrale de Java est que les arguments sont passs par valeur. Cela signie que
e
e e
e
lappel de mthode se fait par copie des valeurs passes en argument et que chaque appel de
e
e
mthode dispose de sa propre version des param`tres. On doit noter que Java est ici parfaitement
e
e
cohrent : si une mthode f poss`de un argument int x, alors lappel f(2) revient ` crer, pour
e
e
e
a e
la dure de lexcution de cet appel une nouvelle variable (de nom x) qui est initialise ` 2.
e
e
e a
Examinons un exemple.
static void dedans(int i) {
i = i+2 ;
System.out.println("i=" + i)
}
static void dehors(int i) {
System.out.println("i=" + i) ;
dedans(i) ;
System.out.println("i=" + i) ;
}
Lappel de dehors(0) se traduira par lachage de trois lignes i=0, i=2, i=0. Cest tr`s seme
blable ` lexemple sur la structure de bloc. Ce qui compte cest la porte des variables. Le rsultat
a
e
e
est sans doute moins inattendu si on consid`re cet autre programme rigoureusement quivalent :
e
e

194

ANNEXE B. MORCEAUX DE JAVA

static void dedans(int j) { //


j = j+2 ;
System.out.println("j=" + j)
}

change ici i -> j

static void dehors(int i) {


System.out.println("i=" + i) ;
dedans(i) ;
System.out.println("i=" + i) ;
}
Mais ce que lon doit bien comprendre cest que le nom de largument de dedans, i ou j na
pas dimportance, cest une variable muette et heureusement.
La r`gle du passage des arguments par valeur sapplique aussi aux objets, mais alors cest
e
une rfrence vers lobjet qui est copie. Ceci concerne en particulier les tableaux.
ee
e
// Affichage dun tableau (dentiers) pass en argument
e
static void affiche(int[] t) {
for (int i = 0 ; i < t.length ; i++)
System.out.print ("t[" + i + "] = " + t[i] + " ") ;
System.out.println() ; // sauter une ligne
}
// trois faons dajouter deux
c
static void dedans (int[] t) {
t[0] = t[0] + 2;
t[1] += 2;
t[2]++ ; t[2]++ ;
}
static void dehors () {
e
int[] a = {1,2,3} ; // dclaration et initialisation dun tableau
affiche (a) ;
dedans (a) ;
affiche (a) ;
}
Lachage est le suivant :
1 2 3
3 4 5
Ce qui illustre bien quil ny a, tout au cours de lexcution du programme quun seul tableau,
e
a, t tant juste des noms dirents de ce mme tableau. On peut aussi renommer largument t
e
e
e
en a ca ne change rien.

Exceptions

Une fois un programme accept par le compilateur, il nest pas garanti quaucune erreur se
e
produira. Bien au contraire, lexprience nous apprend que des erreurs se produiront.
e

4.1

Exceptions lances par le syst`me dexcution


e
e
e

Certaines erreurs empchent lexcution de se poursuivre d`s quelles sont commises. Par
e
e
e
exemple, si nous cherchons ` accder ` la quatri`me case dun tableau qui ne comprend que
a
e
a
e

4. EXCEPTIONS

195

trois cases, le syst`me dexcution ne peut plus rien faire de raisonnable et il fait chouer le
e
e
e
programme.
class Test {
public static void main(String args[])
int [] a = {2, 3, 5};
System.out.println(a[3]);
}
}

Nous obtenons,
% java Test
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at Test.main(Test.java:4)
Le message ach nous signale que notre programme a chou ` cause dune exception dont
e
e
e a
le nom est ArrayIndexOutOfBoundsException (qui semble ` peu pr`s clair). Lachage
a
e
ore quelques bonus, lindice fautif, et surtout la ligne du programme qui a lanc lexception.
e
Dautres exemples classiques derreur sanctionne par une exception sont le drfrencement
e
eee
de null (sanctionn par NullPointerException) ou la division par zro (sanctionne par
e
e
e
ArithmeticException).
Lancer une exception ne signie pas du tout que le programme sarrte immdiatement. On
e
e
peut expliquer le mcanisme ainsi : lexception remplace un rsultat attendu (par exemple le
e
e
rsultat dune division) et ce rsultat (( exceptionnel )) est propag ` travers toutes les mthodes
e
e
ea
e
en attente dun rsultat, jusqu` la mthode main. Le syst`me dexcution de Java ache alors
e
a
e
e
e
un message pour signaler quune exception a t lance. Pour prciser cet eet de propagation
ee
e
e
considrons lexemple suivant dun programme Square.
e
class Square {
static int read(String s) {
return Integer.parseInt(s); // Lire lentier en base 10, voir 6.1.1
}
public static void main(String[] args)
int i = read(args[0]);
System.out.println(i*i);
}

}
Le programme Square est cens acher le carr de lentier pass sur la ligne de commande.
e
e
e
% java Square 11
121
Mais si nous donnons un argument qui nest pas la reprsentation dun entier en base dix, nous
e
obtenons ceci :
% java Square bonga
Exception in thread "main" java.lang.NumberFormatException: For input string: "bonga"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Integer.parseInt(Integer.java:447)
at java.lang.Integer.parseInt(Integer.java:497)
at Square.read(Square.java:3)
at Square.main(Square.java:7)

196

ANNEXE B. MORCEAUX DE JAVA

O` on voit bien la suite des appels en attente que lexception a du remonter avant datteindre
u
main.
Le mcanisme des exceptions est intgr dans le langage, ce qui veut dire que nous pouvons
e
e e
manipuler les exceptions. Ici, la survenue de lexception trahit une erreur de programmation :
en raction ` une entre incorrecte le programme devrait normalement avertir lutilisateur et
e
a
e
donner une explication. Pour ce faire on attrape lexception ` laide dune instruction spcique.
a
e
public static void main(String[] args) {
try {
int i = read(args[0]);
System.out.println(i*i);
} catch (NumberFormatException e) {
System.err.println("Usage : java Square nombre") ;
}
}
Et on obtient alors.
% java Square bonga
Usage : java Square nombre
Sans trop rentrer dans les dtails, linstruction try { instrtry } catch ( E e ) { instrfail }
e
sexcute comme linstruction instrtry , mais si une exception E est lance lors de cette excution,
e
e
e
alors linstruction instrfail est excute. En ce cas, lexception attrape dispara Dans linstruce e
e
t.
tion instrfail on peut accder ` lexception attrape par le truchement de la variable e. Cela permet
e
a
e
par exemple dacher lexception coupable par e.toString() comme ici, ou plus frquemment
e
le message contenu dans lexception par e.getMessage().
public static void main(String[] args) {
try {
int i = read(args[0]);
System.out.println(i*i);
} catch (NumberFormatException e) {
System.err.println("Usage : java Square nombre") ;

System.err.println("Echec sur : " + e) ;


}
}
Et on obtient alors
% java Square bonga
Usage : java Square nombre
chec sur : java.lang.NumberFormatException: For input string: "bonga"
E
Pour tre tout ` fait complet sur lexemple de Square, il y a encore un probl`me. Nous
e
a
e
crivons read(arg[0]) sans vrication que le tableau arg poss`de bien un lment. Nous
e
e
e
ee
devons aussi envisager dattraper une exception ArrayIndexOutOfBoundsException. Une
solution possible est dcrire :
e
private static void usage() {
System.err.println("Usage : java Square nombre") ;
System.exit(2) ;
}
public static void main(String[] args)
try {
int i = read(args[0]);

4. EXCEPTIONS

197

System.out.println(i*i);
} catch (NumberFormatException _) {
usage() ;
} catch (ArrayIndexOutOfBoundsException _) {
usage() ;
}
}
On note quil peut en fait y avoir plusieurs clauses catch. Les deux clauses catch appellent ici
la mthode usage, qui ache un bref message rsumant lusage correct du programme et arrte
e
e
e
lexcution.
e

4.2

Lancer une exception

Lorsque lon crit un composant quelconque, cest-`-dire du code rutilisable par autrui, on se
e
a
e
trouve parfois dans la situation de devoir signaler une erreur. Il faut alors procder exactement
e
comme le syst`me dexcution de Java et lancer une exception. Ainsi lutilisateur de notre
e
e
composant a lopportunit de pouvoir rparer son erreur. Au pire, un message comprhensible
e
e
e
sera ach.
e
Supposons par exemple une classe des piles dentiers. Une tentative de de dpiler sur une
e
pile vide se solde par une erreur, on lance alors une exception par linstruction throw.
int pop() {
i f (isEmpty()) throw new Error ("Pop: empty stack") ;
...
}
Il appara clairement quune exception est un objet dune classe particuli`re, ici la classe Error.
t
e
En raison de sa simplicit nous utilisons systmatiquement Error dans nos exemples.
e
e
Si vous prenez la peine de lire la documentation vous verrez que dans lesprit des auteurs de
la biblioth`que, les exceptions Error ne sont pas censes tre attrapes.
e
e e
e
An Error [. . . ] indicates serious problems that a reasonable application should not
try to catch. Most such errors are abnormal conditions.
Pour signaler des erreurs rparables, la documentation encourage plutt la classe Exception.
e
o
The class Exception and its subclasses [. . . ] indicates conditions that a reasonable
application might want to catch.
En fait, il vaut mieux ne pas lancer Exception, mais plutt une exception par nous dnie
o
e
(comme une sous-classe de Exception) ; ceci an dviter les interfrences avec les exceptions
e
e
lances par les mthodes de la biblioth`que.
e
e
e
Nous procdons donc en deux temps, dabord dclaration de la classe de notre exception.
e
e
class Stack {
.
.
.
static class Empty extends Exception { }
.
.
.
}
La classe Empty est dnie comme un membre statique de la classe Stack (oui, cest pose
sible), de sorte que lon la dsigne normalement comme Stack.Empty. Ce dtail mis ` part,
e
e
a
il sagit dune dclaration de classe normale, mais qui ne poss`de aucun membre en propre.
e
e
Le code indique simplement que Empty est une sous-classe (voir 2.3 et 1.5) de Exception.
Un constructeur par dfaut Empty () est implicitement fourni, qui se contente dappeler le
e
constructeur Exception ().

198

ANNEXE B. MORCEAUX DE JAVA


Ensuite, notre exception est lance comme dhabitude.
e

int pop() {
i f (isEmpty()) throw new Empty () ;
...
}
Mais alors, la compilation de la classe Stack choue.
e
% javac Stack.java
Stack.java:13: unreported exception Stack.Empty;
must be caught or declared to be thrown
if (isEmpty()) throw new Empty () ;
^
1 error
En eet, Java impose de dclarer quune mthode lance une exception Exception (mais pas
e
e
e
une exception Error). On dclare que la mthode pop lance lexception Empty par le mot cl
e
e
throws (noter le (( s ))).
int pop() throws Empty {
i f (isEmpty()) throw new Empty () ;
...
}
On peut considrer que les exceptions lances par une mthode font partie de sa signature
e
e
e
(voir 3.3), cest-`-dire font partie des informations ` conna pour pouvoir appeler la mthode.
a
a
tre
e
Les dclarations obligatoires des exceptions lances sont assez contraignantes, en eet il faut
e
e
tenir compte non seulement des exceptions lances directement, mais aussi de celles lances
e
e
indirectement par les mthodes appeles. Ainsi si on conoit une mthode remove( int n) pour
e
e
c
e
e
e
enlever n lments dune pile, on doit tenir compte de lexception Empty ventuellement lance
ee
par pop.
void remove (int n) throws Empty {
for ( ; n > 0 ; n--) {
pop() ;
}
}
Noter quil est galement possible pour remove de ne pas signaler que la pile comportait moins
e
de n lments. Cela revient ` attraper lexception Empty, et cest dautant plus facile que remove
ee
a
ne renvoie pas de rsultat.
e
void remove (int n) {
try {
for ( ; n > 0 ; n--) {
pop() ;
}
} catch (Empty e) { }
}

Entres-sorties
e

Par entres-sorties on entend au sens large la faon dont un programme lit et crit des
e
c
e
donnes, et au sens particulier linteraction entre le programme et le syst`me de chiers de la
e
e
machine.


5. ENTREES-SORTIES

199

Un chier est un ensemble de donnes qui survit ` la n de lexcution des programmes, et


e
a
e
mme, la plupart du temps, ` larrt de lordinateur. Les chiers se trouvent le plus souvent sur
e
a
e
le disque de la machine, mais ils peuvent aussi se trouver sur une disquette, ou sur le disque
dune autre machine et tre accds ` travers le rseau. Un syst`me dexploitation tel quUnix,
e
e e a
e
e
ore une abstraction des chiers, gnralement sous forme de ux (stream). Dans sa forme la
e e
plus simple le ux ore un acc`s squentiel : on peut lire le prochain lment dun ux (ce
e e
ee
qui le consomme) ou crire un lment dans un ux (gnralement ` la n). Dans la majorit
e
ee
e e
a
e
des cas (et surtout en Unix), les chiers contiennent du texte, il est alors logique, vu de Java,
de les considrer comme des ux de char. Par chier texte, nous entendons dabord chier
e
quun humain peut crire, lire et comprendre, par exemple un source Java. Les chiers qui ne
e
sinterpr`tent pas ainsi sont dits binaires, par exemple une image ou un chier de bytecode Java.
e
Il est logique, vu de Java, de considrer les chiers binaires comme des ux de byte. Par la suite
e
nous ne considrons que les chiers texte.
e
Quand vous lancez un programme dans une fentre de commandes (shell ), il peut lire ce que
e
vous tapez au clavier (ux dentre standard) et crire dans la fentre (ux de sortie standard).
e
e
e
Le shell permet de rediriger les ux standard vers des chiers (par <chier et >chier), ce qui
permet de lire et dcrire des chiers simplement. Enn il existe un troisi`me ux standard, la
e
e
sortie derreur standard normalement connecte ` la fentre comme la sortie standard. Mme
e a
e
e
si cela ne semble pas utile, il faut crire les messages de diagnostic et derreur dedans, et nous
e
allons voir pourquoi.

5.1

Le minimum ` savoir : une entre et une sortie standard


a
e

Nous connaissons dj` la sortie standard, cest System.out et System.err est la sortie
ea
derreur. Nous savons dj` crire dans ces ux de classe PrintStream, par leurs mthodes print
eae
e
et println. Pour lire lentre standard (System.in, comme vous pouviez vous en douter), cest
e
un rien plus complexe, mais sans plus.
Il faut tout dabord fabriquer un ux de caract`res, objet de la classe, Reader (voir aussi 6.2.1)
e
a
` partir de System.in :
Reader in = new InputStreamReader(System.in) ;
On note que lobjet InputStreamReader est rang dans une variable de type Reader. Et en
e
eet, InputStreamReader est une sous-classe de Reader (voir 2.3).
Ensuite, on peut lire le prochain caract`re de lentre par lappel in.read(). Cette mthode
e
e
e
renvoie un int , avec le comportement un peu tordu suivant :
Si un caract`re est lu, il est renvoy comme un entier.
e
e
Si on est ` la n de lentre, -1 est renvoy. La n de lentre est par vous signale par
a
e
e
e
e
(( Control-d )) au clavier et est la n de chier si il y a eu redirection.
En cas derreur lexception IOException est lance. Ces erreurs sont du genre de celles
e
dtectes au niveau du syst`me dexploitation, comme la panne dun disque (rare !), ou
e
e
e
un dlai abusif de transmission par le rseau (plus frquent). Bref, lexception est lance
e
e
e
e
quand, pour une raison ou une autre, la lecture est juge impossible.
e
Voici Cat un ltre trivial qui copie lentre standard dans la sortie standard :
e
import java.io.*; // La classe Reader est dans le package java.io
class Cat {
public static
try {
Reader in
for ( ; ;
int c =

void main(String [] arg) {


= new InputStreamReader(System.in) ;
) {// Boucle infinie, on sort par break
in.read ();

200

ANNEXE B. MORCEAUX DE JAVA


i f (c == -1) break;
System.out.print ((char)c);
}
} catch (IOException e) { // En cas de malaise
System.err.print("Malaise : " + e.getMessage()) ;
System.exit(2) ; // Terminer le programme sur erreur
}

}
}
Remarquons :
Il y a un try. . . catch (voir 4.1), car la mthode read dclare lancer lexception IOException,
e
e
`
qui doit tre attrape dans main si elle nest pas dclare dans la signature de main. A la n
e
e
e
e
du traitement de lexception, on utilise la mthode System.exit qui arrte le programme
e
e
dun coup. Le message derreur est crit dans la sortie derreur.
e
Pour acher c comme un caract`re, il faut le changer en caract`re, par (char)c. Sinon,
e
e
cest un code de caract`re qui sache.
e
On peut tester le programme Cat par exemple ainsi :
% java Cat < Cat.java
import java.io.*; // La classe Reader est dans le package java.io
.
.
.

5.2

Un peu plus que le minimum : lire des chiers

La commande cat de Unix est plus puissante que notre Cat. Si on donne des arguments sur
la ligne de commande, ils sont interprts comme des noms de chiers. Le contenu des chiers
ee
est alors ach sur la sortie standard, un chier apr`s lautre. La commande cat permet donc
e
e
de concatner des chiers. Par exemple
e
% cat a a > b
range deux copies mises bout-`-bout du contenu du chier a dans le chier b. Comme souvent, si
a
on ne donne pas dargument ` la commande cat, alors cest lentre standard qui est lue. Ainsi
a
e
cat < a > b copie toujours le chier a dans le chier b.
Pour lire un chier ` partir de son nom, on construit une nouvelle sorte de Reader : un
a
FileReader. Cela revient ` ouvrir un chier en lecture, cest-`-dire ` demander au syst`me
a
a
a
e
dexploitation de mobiliser les ressources ncessaires pour cette lecture. Si name est un nom de
e
chier, on proc`de ainsi.
e
Reader in = new FileReader (name) ;
Si le chier de nom name nexiste pas, une exception est lance (FileNotFoundException
e
sous-classe de IOException).
Il est de bon ton de fermer les chiers que lon a ouvert, an de rendre les ressources mobilises
e
lors de louverture. En eet un programme donn ne peut pas possder plus quun nombre x
e
e
e
de chiers ouvert en lecture. On ferme un Reader en appelant sa mthode close(). Il faut
e
noter quune tentative de lecture dans une entre ferme choue et se solde par le lancement
e
e e
dune exception spcique. Voici la nouvelle classe Cat2 .
e
import java.io.* ;
class Cat2 {
static void cat(Reader in) throws IOException {
for ( ; ; ) {


5. ENTREES-SORTIES

201

int c = in.read ();


i f (c == -1) break;
System.out.print ((char)c);
}
}
public static void main(String [] arg) {
i f (arg.length == 0) {
try {
cat(new InputStreamReader(System.in)) ;
} catch (IOException e) {
System.err.println("Malaise : " + e.getMessage()) ;
System.exit(2) ;
}
} else {
for (int k = 0 ; k < arg.length ; k++) {
try {
Reader in = new FileReader(arg[k]) ;
cat(in) ;
in.close() ;
} catch (IOException e) {
System.err.println("Malaise : " + e.getMessage()) ;
}
}
}
}
}
Remarquons :
La lecture dun chier est ralise par une mthode cat, car cette lecture sop`re dans
e e
e
e
deux contextes dirents. On note aussi lintrt du sous-typage : largument Reader in
e
ee
est ici un objet InputStreamReader ou FileReader, mais comme il nest pas utile de
le savoir dans la mthode cat, on en prote pour partager du code.
e
Le second try. . . catch est dans la boucle for ( int k = 0 ;. . . , et sa clause catch ne
contient pas dappel ` System.exit. Il en rsulte que le programme nchoue pas en cas
a
e
e
derreur de lecture dun chier donn par son nom ou mme de chier inexistant, car
e
e
FileNotFoundException est une sous-classe de IOException. Au lieu dchouer, on
e
passe au chier suivant.
Les messages derreur sont crits dans la sortie derreur et non pas dans la sortie standard.
e
Ainsi si le chier a existe mais que le chier x nexiste pas, la commande suivante a un
comportement raisonnable.
% java Cat2 a x a > b
Malaise : x (No such file or directory)
Un message derreur est ach dans la fentre, et b contient deux copies de a.
e
e

5.3

Encore un peu plus que le minimum : crire dans un chier


e

La sortie standard est souvent insusante, on peut vouloir crire plus dun chier ou ne
e
pas obliger lutilisateur ` faire une redirection. On se dit donc quil doit bien y avoir un moyen
a
dobtenir un ux de caract`res en sortie connect ` un chier dont on donne le nom. Le ux
e
e a
de sortie le plus simple est le Writer, qui poss`de une mthode write( int c) pour crire un
e
e
e
caract`re. Comme pour les Reader on utilise en pratique des sous-classes de Writer. Il existe
e
entre autres un FileWriter que nous utilisons pour crire une version simple de la commande
e

202

ANNEXE B. MORCEAUX DE JAVA

Unix cp , o` cp name1 name2 copie le contenu du chier name1 dans le chier name2 .
u
import java.io.* ;
class Cp {
public static void main(String [] arg) {
i f (arg.length != 2) {
System.err.println("Usage : java Cp name1 name2") ;
System.exit(2) ;
}
String name1 = arg[0], name2 = arg[1] ;
try {
Reader in = new FileReader(name1) ;
Writer out = new FileWriter(name2) ;
for ( ; ; ) {
int c = in.read() ;
i f (c == -1) break ;
out.write(c) ;
}
out.close() ; in.close() ;
} catch (IOException e) {
System.err.println("Malaise : " + e.getMessage()) ;
System.exit(2) ;
}
}
}
Il y a peu ` dire, mais remarquons que nous prenons soin de fermer les ux que nous ouvrons.
a
Cela semble une bonne habitude ` prendre (voir aussi 5.6 pour tout savoir ou presque).
a

5.4

Toujours plus loin : entres-sorties buerises


e
e

La documentation recommande lemploi de ux bueriss (buered dsol pas de terme


e
e e
franais adquat) pour atteindre lecacit maximale (( top eciency )). Lide des ux bueriss,
c
e
e
e
e
qui est tr`s gnrale, est la suivante : lcriture ou la lecture eective de caract`res a un cot xe
e e e
e
e
u
important, qui est pay quelque soit le nombre de caract`res eectivement transfrs entre chier
e
e
ee
et programme. Plus prcisment, crire ou lire n caract`res cote de lordre de K0 +K1 n, o` K0
e e
e
e
u
u
est bien plus grand que K1 . Cela peut sexpliquer par divers phnom`nes. Par exemple, un appel
e
e
au syst`me dexploitation est relativement lent, et une opration dentre-sortie signie un seul
e
e
e
appel syst`me, quelque soit le nombre de caract`res impliqus. Ou encore, le cot des transferts
e
e
e
u
entre disque et mmoire est largement indpendant du nombre de caract`res transfrs, jusqu`
e
e
e
ee
a
une certaine taille, de par la nature mme du dispositif physique (( disque )) qui lit et crit des
e
e
donnes par blocs de taille xe.
e
e
Pour xer les ides, prenons lexemple de lcriture. Lide est alors de ne pas crire eectie
e
e
e
vement chaque caract`re en raction ` un appel out.write(c) mais ` la place de le ranger dans
e
e
a
a
une zone mmoire appele tampon (buer ). Le tampon a une taille xe, et les caract`res du tame
e
e
pon sont eectivement transmis au syst`me dexploitation quand le tampon est plein. De cette
e
faon le cot xe K0 est pay moins souvent et lecacit totale est amliore. Les transferts
c
u
e
e
e e
vers les chiers ` travers un tampon mmoire prsentent linconvnient que le caract`re c peut
a
e
e
e
e
2 d`s que lon appelle out.write(c). Leet est particuli`rement
ne pas se trouver dans le chier e
e
gnant ` la n du programme. Si le tampon nest pas vid (ushed ), son contenu est perdu. En
e
a
e
eet, le tampon est de la mmoire appartenant au programme et qui donc dispara avec lui. Il
e
t
en rsulte gnralement que la n du ux ne se retrouve pas dans le chier. Il faut donc vider
e
e e
2

Ou plus exactement c nest pas dans les tampons du syst`me dexploitation, en route vers le chier.
e


5. ENTREES-SORTIES

203

le tampon avant de terminer le programme, ce que fait la mthode close() de fermeture des
e
ux, avant de fermer eectivement le ux. On peut aussi vider le tampon plus directement en
appelant mthode flush() des ux bueriss. Lexistence dun retard entre ce qui est crit par
e
e
e
le programme dans le ux et ce qui est eectivement crit dans le chier donne donc une raison
e
supplmentaire de fermer les chiers.
e
Pour ce qui est dun chier ouvert en lecture, la technique du tampon sapplique galement,
e
avec les mmes bnces en terme decacit. Dans ce cas, les lectures (read) se font dans le
e
e e
e
tampon, qui est rempli ` partir du chier quand une demande de lecture trouve un tampon
a
vide. Il y a alors bien entendu une avance ` la lecture mais ce dcalage ne pose pas les mmes
a
e
e
probl`mes que le retard ` lcriture.
e
a e
Un ux bueris en criture (resp. lecture) est un objet de la classe BueredWriter (resp.
e
e
BueredReader), qui se construit simplement ` partir dun Writer (resp. Reader), et qui
a
reste un Writer (resp. Reader). Voici une autre version Cp2 de la commande cp crite en
e
Java, qui emploie les entres-sorties buerises.
e
e
import java.io.* ;
class Cp2 {
public static void main(String [] arg) {
String name1 = arg[0], name2 = arg[1] ;
try {
Reader in = new BufferedReader (new FileReader(name1)) ;
Writer out = new BufferedWriter (new FileWriter (name2)) ;
for ( ; ; ) {
int c = in.read() ;
i f (c == -1) break ;
out.write(c) ;
}
out.close() ; in.close() ;
} catch (IOException e) {
System.err.println("Malaise : " + e.getMessage()) ;
System.exit(2) ;
}
}
}
Une mesure rapide des temps dexcution montre que le programme Cp2 est ` peu pr`s trois
e
a
e
fois plus rapide que Cp.
Les ux bueriss BueredWriter et BueredReader orent aussi une vue des chier
e
texte comme tant composs de lignes. Il existe une mthode Newline pour crire une n
e
e
e
e
le ligne, et une mthode readLine pour lire une ligne. On utilise souvent BueredReader
e
pour cette fonctionnalit de lecture ligne ` ligne. Il faut aussi noter que les lignes sont dnies
e
a
e
indpendamment de leur ralisation par les divers syst`mes dexploitation (voir 3.2.3).
e
e
e

5.5
5.5.1

Entres-sorties formates
e
e
Sorties formates
e

Nous savons dsormais crire dans un chier de nom name de faon ecace en construisant
e
e
c
un FileWriter, puis un BueredWriter.
Writer out = new BufferedWriter (new FileWriter name) ;
Toutefois, crire caract`re par caract`re (ou mme par cha avec write(String str)), nest
e
e
e
e
ne
pas toujours tr`s pratique. La mthode print de System.out est bien plus commode. Malheue
e
reusement, les PrintStream de la biblioth`que sont tr`s inecaces. Par exemple, la copie du
e
e

204

ANNEXE B. MORCEAUX DE JAVA

chier a dans le chier b par java Cat < a > b est environ vingt-cinq fois plus lente que java
Cp2 a b !
Mais la mthode print est bien pratique, voici par exemple une mthode simple qui ache
e
e
les nombres premiers jusqu` n directement dans System.out.
a
static void sieve(int n) {
boolean [] t = new boolean [n+1] ;
int p = 2 ;
for ( ; ; ) {
System.out.println(p) ; // Afficher p premier
/* Identifier les multiples de p */
for (int k = p+p ; k <= n ; k += p) t[k] = true ;
/* Chercher le prochain p premier */
do {
p++ ; i f (p > n) return ;
} while (t[p]) ;
}
}
Lapplication du crible dEratosth`ne est na mais le programme est inecace dabord ` cause
e
ve,
a
de ses achages. On sen rend compte en modiant sieve pour utiliser un PrintWriter. Les
objets de la classe PrintWriter sont des Writer (ux de caract`res) qui orent les sorties
e
formates, cest-`-dire quils poss`dent une mthode print (et prinln) surcharge qui all`ge un
e
a
e
e
e
e
peu la programmation. Par ailleurs, les PrintWriter sont bueriss par dfaut.
e
e
static void sieve(int n, PrintWriter out) throws IOException {
boolean [] t = new boolean [n+1] ;
int p = 2 ;
for ( ; ; ) {
out.println(p) ;
for (int k = p+p ; k <= n ; k += p) t[k] = true ;
do {
p++ ; i f (p > n) return ;
} while (t[p]) ;
}
}
public static void main(String [] arg) {
.
.
.
try {
PrintWriter out = new PrintWriter ("primes.txt") ;
sieve(n, out) ;
out.close() ;
} catch (IOException e) {
System.err.println("Malaise: " + e.getMessage()) ;
System.exit(2) ;
}
}
La nouvelle mthode sieve crit dans le PrintWriter out dont linitialisation est rendue pnible
e
e
e
par les exceptions possibles. Noter aussi que la sortie out est vide (et mme ferme) par la
e
e
e
mthode main qui la cre. Il y a plusieurs constructeurs des PrintWriter, qui prennent divers
e
ee
arguments. Par exemple, si lon avait voulu crire dans la sortie standard, et non pas dans le
e
chier primes.txt, on aurait tr`s bien pu (( emballer )) la sortie standard dans un PrintWriter,
e
par new PrintWriter(System.out). Pour ce qui est du temps dexcution des essais rapides
e


5. ENTREES-SORTIES

205

montrent que le nouveau sieve (avec PrintWriter) peut tre jusqu` dix fois plus rapide que
e
a
lancien (avec PrintStream). Nous insistons donc : System.out est vraiment tr`s inecace,
e
il faut renoncer ` son emploi d`s quil y a beaucoup de sorties.
a
e
5.5.2

Entres formates
e
e

Les Reader permettent de lire caract`re par caract`re, les BueredReader permettent
e
e
de lire ligne par ligne. Cest dj` pas mal, mais cest parfois insusant. Par exemple, on peut
ea
imaginer vouloir relire le chier primes.txt de la section prcdente. Ce que lon veut alors cest
e e
lire une squence dentiers int . Pour ce faire nous pourrions reconstituer nous mme les int `
e
e
a
partir de la squence de leurs chires, mais ca ne serait pas tr`s moderne.
e

e
Heureusement, il existe des objets qui savent faire ce style de lecture pour nous : ceux de la
classe Scanner du package java.util. Les objets Scanner sont des ux de lex`mes (tokens).
e
Les lex`mes sont simplement ` un langage informatique ce que les mots sont ` un langage dit
e
a
a
naturel. Par dfaut les lex`mes reconnus par un Scanner sont les mots de Java un entier, un
e
e
identicateur, une cha
ne, etc.
Un Scanner poss`de une mthode next() qui renvoie le lex`me suivant du ux dentre
e
e
e
e
(comme un String) et une mthode hasNext() pour savoir si il existe un lex`me suivant, ou si
e
e
le ux est termin. Il poss`de aussi des mthodes spcialises pour lire des lex`mes particuliers
e
e
e
e
e
e
ou savoir si le lex`me suivant existe et est un lex`me particulier (par exemple nextInt() et
e
e
hasNextInt() pour un int ). On peut donc utiliser primes.txt pour dcomposer un int en
e
facteurs premiers ainsi (mme si ce nest pas une tr`s bonne ide algorithmiquement parlant).
e
e
e
import java.util.* ;
import java.io.* ;
class Factor {
private static PrintWriter out = new PrintWriter (System.out) ;
private static void factor (int n, String filename) {
try {
Scanner scan = new Scanner (new FileReader (filename)) ;
out.print(n + ":") ;
while (n > 1) {
i f (!scan.hasNextInt())
throw new IOException ("Format of file " + filename) ;
int p = scan.nextInt() ;
while (n % p == 0) {
out.print(" " + p) ;
n /= p ; // Pour n = n / p ;
}
}
out.println() ; out.flush() ; scan.close() ;
} catch (IOException e) {
System.err.println("Malaise : " + e.getMessage()) ;
System.exit(2) ;
}
}
public static void main(String arg []) {
for (int k = 0 ; k < arg.length ; k++)
factor(Integer.parseInt(arg[k]), "primes.txt") ;
}

206

ANNEXE B. MORCEAUX DE JAVA

}
On note que le Scanner est construit ` partir dun Reader. Il y a bien entendu dautres
a
constructeurs. On note aussi que lentre scan est ferme explicitement par scan.close(), d`s
e
e
e
quelle nest plus utile ; tandis que la sortie nest que vide par out.flush(). En eet, il ne
e
serait pas adquat de fermer la sortie out qui sert plusieurs fois, mais il faut sassurer que les
e
sorties du programme sont bien envoyes au syst`me dexploitation.
e
e

5.6

Complment : tout ou presque sur les chiers texte


e

Si Java et Unix sont bien daccord sur ce quest en gros un chier texte (un chier quun
humain peut lire au moins en principe), ils ne sont plus daccord sur ce que sont les lments
ee
dun tel chier. Pour Java, un chier de texte est un ux de char (16 bits), tandis que pour
Unix cest un ux doctets (8 bits, un byte pour Java). Le passage de lun ` lautre demande
a
dappliquer un encodage.
Un exemple simple dencodage est par exemple ISO-8859-1, lencodage que java emploie
par dfaut sur les machines de lcole. Cest un encodage qui fonctionne pour presque toutes
e
e
les langues europennes (mais pas pour le symbole e). Cest un encodage simple sur 8 bits,
e
qui permet dexprimer 256 caract`res seulement parmi les 216 dUnicode. Pour voir les cae
ract`res dnis en ISO-8859-1, vous pouvez essayer man latin1 sur une machine de lcole.
e
e
e
Lencodage ISO-8859-1 est techniquement le plus simple possible : les codes des caract`res
e
sont les mmes ` la fois en ISO-8859-1 et en Unicode. Il nest donc tout simplement pas
e
a
possible dexprimer les char dont les codes sont suprieurs ` 256 en ISO-8859-1 (dont juse
a
tement e, dont la valeur Unicode hexadcimale est 0x20AC, note U+20AC). Il existe dautres
e
e
encodages 8 bits, dont ISO-8859-15 qui entre autres tablit justement la correspondance entre
e
le caract`re Unicode U+20AC et le byte 0xA4, au dpend du caract`re Unicode U+00A4 ( )
e
e
e
qui nest plus exprimable. Il existe bien entendu des encodages qui permettent dexprimer
tout Unicode, mais alors un caract`re Unicode peut sexprimer comme plusieurs octets. Un
e
encodage multi-octet rpandu est UTF-8, o` les caract`res Unicode sont reprsents par un
e
u
e
e
e
nombre variable doctets selon un syst`me un peu compliqu que nous ne dcrirons pas (voir
e
e
e
http ://fr.wikipedia.org/wiki/UTF-8 qui est raisonnablement clair).
Revenons aux ux de Java. Pour xer les ides nous considrons dabord les ux en lecture.
e
e
Un ux doctets est un InputStream. La classe InputStream fonctionne sur le mme principe
e
que la classe Reader : cest une sur-classe des divers ux de byte qui peuvent tre construits.
e
Par exemple on ouvre un ux doctets sur un chier name en crant un objet de la classe
e
FileInputStream.
InputStream inBytes = new FileInputStream (name) ;
Pour lire un ux doctets comme un ux de char, on fabrique un InputStreamReader.
Reader inChars = new InputStreamReader (inBytes) ;
Lencodage est ici implicite, cest lencodage par dfaut. On peut aussi expliciter lencodage en
e
donnant son nom comme second argument au constructeur.
Reader inChars = new InputStreamReader (inBytes, "UTF-8") ;
Et voil` nous disposons maintenant dun Reader sur un chier dont la suite doctets dnit
a
e
une suite de caract`res Unicode encods en UTF-8. Ici, comme UTF-8 est un encodage multie
e
octets, la lecture dun char dans inChars implique de lire un ou plusieurs byte dans le ux
inBytes sous-jacent. Les ux en criture suivent un schma similaire, il y a des ux de byte
e
e
(OutputStream) et des ux de char (Writer), avec une classe pour faire le pont entre les
deux (OutputStreamWriter). Par exemple, voici comment fabriquer un Writer connect `
ea
la sortie standard et qui crit de lUTF-8 sur la console :
e

`
6. QUELQUES CLASSES DE BIBLIOTHEQUE

207

// System.out (un PrintStream) est aussi un OutputStream


Writer outChar = new OutputStreamWriter (System.out, "UTF-8") ;
Notons quune fois obtenu un Reader ou un Writer nous pouvons fabriquer ce dont nous avons
besoin, par exemple un Scanner ou un PrintWriter etc., ` laide des constructeurs (( naturels ))
a
de ces classes, qui prennent un Reader ou un Writer en argument. Les diverses classes de ux
poss`dent parfois des constructeurs qui semblent permettre de court-circuiter le passage par un
e
InputStreamReader ou un OutputStreamWriter ; mais ce nest quune apparence, il y aura
toujours dcodage et encodage. Par exemple, new PrintWriter (String name) ouvre direce
tement le chier name, mais ` quelques optimisations internes toujours possibles pr`s, employer
a
e
ce constructeur synthtique revient ` :
e
a
new PrintWriter (new OutputStreamWriter (new FileOutputStream (name))) ;
Finalement, en thorie nous savons lire et crire tout Unicode. En pratique, il faut encore pouvoir
e
e
entrer ces caract`res au clavier et les visualiser dans une fentre, mais cest une autre histoire
e
e
qui ne regarde plus Java.
Toutes ces histoires dencodage et de dcodage de char en byte font qucrire un char dans
e
e
un Writer ou lire un char dans un Reader ne sont jamais des oprations simples.3 Comme on
e
a tendance a tout simplier on a parfois des surprises : par exemple, les FileWriter (voir 5.3)
poss`dent en fait dj` un tampon. On se rend vite compte de lexistence de ce tampon si on
e
ea
oublie de fermer un FileWriter. Si on en croit la documentation :
Each invocation of a write() method causes the encoding converter to be invoked on
the given character(s). The resulting bytes are accumulated in a buer before being
written to the underlying output stream.
Il sagit donc dun tampon de byte dans lequel le FileWriter stocke les octets rsultants de
e
lencodage quil eectue. On peut alors se demander do` provient le gain decacit constat en
u
e
e
emballant un FileWriter dans un BueredWriter (voir 5.4), puisque le cot irrductible de la
u
e
vritable sortie est dj` amorti par un tampon. Et bien, il se trouve que le cot de lapplication
e
ea
u
de lencodage des char vers les byte suit lui-aussi le mod`le dun cot constant important
e
u
relativement au cot proportionnel au nombre de caract`res encods. Le tampon (de char)
u
e
e
introduit en amont du FileWriter par new BueredWriter (new FileWriter (name2))
a alors pour fonction damortir ce cot irrductible de lencodage.
u
e

Quelques classes de biblioth`que


e

En gnral une biblioth`que (library) est un regroupement de code susamment structur et


e e
e
e
document pour que dautres programmeurs puissent sen servir. La biblioth`que associe ` un
e
e
e a
langage de programmation est diuse avec le compilateur, syst`me dexcution etc. La majeure
e
e
e
partie de des programmes disponibles dans la biblioth`que dun langage est normalement crite
e
e
dans ce langage lui-mme.
e
La premi`re source de documentation des classes de la biblioth`que de Java est bien entendu
e
e
la documentation en ligne4 de cette derni`re, ou comme on dit la Application Programmer Intere
face Specication. Nous donnons donc des extraits de cette documentation assortis de quelques
commentaires personnels. Cette section entend surtout vous aider ` prendre la bonne habitude
a
de consulter la documentation du langage que vous utilisez. En particulier, la version web du
polycopi comporte des liens vers la documentation en ligne.
e
On objectera que la biblioth`que est norme. Mais en pratique on sy retrouve assez vite :
e
e
3

Encodage et dcodage font aussi quil ne faut pas crire Cp et Cat comme nous lavons fait (avec des ux
e
e
de char). Cest inutilement inecace et mme dangereux certains dcodages pouvant parfois chouer, il aurait
e
e
e
mieux valu employer les ux de byte.
4
http://java.sun.com/j2se/1.5.0/docs/api

208

ANNEXE B. MORCEAUX DE JAVA

La documentation en ligne est organise de faon systmatique, La page daccueil est une
e
c
e
liste de liens vers les pages des packages, la page dun package est une liste de liens vers les
pages des classes, qui contiennent (au dbut) une liste de liens vers les champs, mthodes
e
e
etc.
Les classes que nous utilisons appartiennent ` trois packages seulement.
a
Les descriptions sont parfois un peu cryptiques (car elles sont compl`tes), mais on arrive
e
vite ` en extraire lessentiel.
a
Il est vain dobjecter que la documentation est en anglais. Cest comme ca.

6.1

Package java.lang, biblioth`que ( standard )


e
(
)

Ce package regroupe les fonctionnalits les plus basiques de la la biblioth`que. Il est import
e
e
e
par dfaut (pas besoin de import java.lang.*).
e
6.1.1

Classes des scalaires

`
A chaque type scalaire ( int , char etc.), correspond une classe (Integer, Character etc.)
qui permet surtout de transformer les scalaires en objets. Une transformation que le compilateur
sait automatiser (voir II.2.3).
Prenons lexemple de la classe Integer le pendant objet du type scalaire int . Deux mthodes
e
permettent le passage dun scalaire ` un objet et rciproquement.
a
e
La mthode valueOf transforme le scalaire en objet.
e
public static Integer valueOf(int i)
La mthode rciproque intValue extrait le int cach dans un objet Integer.
e
e
e
public int intValue()
La classe Integer a bien dautres mthodes, comme par exemple
e
La mthode parseInt permet de (( lire )) un entier.
e
public static int parseInt(String s) throws NumberFormatException
Si la cha s contient la reprsentation dcimale dun entier, alors parseInt renvoie
ne
e
e
cet entier (comme un int ). Si la cha s ne reprsente pas un int , alors lexception
ne
e
NumberFormatException est lance.
e
6.1.2

Un peu de maths

Les fonctions mathmatiques usuelles sont dnies dans la classe Math. Il ny a pas dobjets
e
e
Math. Cette classe ne sert qu` la structuration. Elle ore des mthodes (statiques).
a
e
La valeur absolue abs, disponible en quatre versions.
public
public
public
public

static
static
static
static

int abs(int a)
long abs(long a)
float abs(float a)
double abs(double a)

Les fonctions maximum et minimum, max et min, galement disponibles en quatre versions.
e
public static int max(int a, int b)
public static int min(int a, int b)
.
.
.
Les divers arrondis des double, par dfaut oor, par exc`s ceil et au plus proche round.
e
e

`
6. QUELQUES CLASSES DE BIBLIOTHEQUE

209

public static double floor(double a)


public static double ceil(double a)
public static long round(double a)
On note que floor et ceil renvoient un double. Tandis que round renvoie un long. Les
mthodes existent aussi pour les oat et round( oat a) renvoie un int . Cela peut
e
sembler logique si on consid`re que les valeurs double et long dune part, et oat et
e
int dautre part occupent le mme nombres de bits (64 et 32). Mais en fait ca ne r`gle
e

e
pas vraiment le probl`me des ottants trop grands pour tre arrondis vers un entier (voir
e
e
la documentation).
Et bien dautres mthodes, comme la racine carre sqrt, lexponentielle exp, le logarithme
e
e
naturel log, etc.
6.1.3

Les cha
nes

Les cha
nes sont des objets de la classe String. Une cha est tout simplement une squence
ne
e
nie de caract`res. En Java les cha
e
nes sont non-mutables : on ne peut pas changer les caract`res
e
dune cha
ne. Les cha
nes sont des objets normaux, ` un peu de syntaxe spcialise pr`s. Les
a
e
e
e
constantes cha
nes scrivent entre doubles quotes, par exemple "coucou". Si on veut mettre
e
un double quote (( " )) dans une cha
ne, il fait le citer en le prcdant dune barre oblique
e e
inverse (backslash). System.err.print("Je suis un double quote : \"") ache Je suis
un double quote : " sur la console. En outre, la concatnation de deux cha
e
nes s1 et s2 , scrit
e
avec loprateur + comme s1 +s2 .
e
Les autres oprations classiques sur les cha
e
nes sont des mthodes normales.
e
La taille ou longueur dune cha s, cest-`-dire le nombre de caract`res quelle contient
ne
a
e
sobtient par s.length()
public int length()
Le caract`re dindice index sextrait par la mthode charAt.
e
e
public char charAt(int index)
Comme pour les tableaux, les indices commencent ` zro.
a e
On extrait une sous-cha par la mthode substring.
ne
e
public String substring(int beginIndex, int endIndex)
beginIndex est lindice du premier caract`re de la sous-cha et endIndex est lindice du
e
ne,
caract`re qui suit immdiatement la sous-cha La longueur de la sous-cha extraite est
e
e
ne.
ne
donc endIndex-beginIndex. Si le couple dindices ne dsigne pas une sous-cha lexe
ne
ception IndexOutOfBoundsException est lance. Les r`gles de dnition du couple
e
e
e
dindices dsignant une sous-cha valide (voir la documentation) conduisent ` la partie
ne
a
cularit bien sympathique que s.substring(s.length(), s.length()) renvoie la cha
e
ne
vide "".
La mthode equals est lgalit structurelle des cha
e
e
e
nes, celle qui regarde le contenu des
cha
nes.
public boolean equals(Object anObject)
Il faut remarquer que largument de la mthode est un Object. Il faut juste le savoir, cest
e
tout. La classe String tant une sous-classe de Object (voir 2.3), la mthode sapplique
e
e
en particulier au seul cas utile dun argument de classe String.
La mthode compareTo permet de comparer les cha
e
nes.
public int compareTo(String anotherString)

210

ANNEXE B. MORCEAUX DE JAVA


Compare la cha avec une autre selon lordre lexicographique (lordre du dictionnaire).
ne
Lappel (( s1 .compareTo(s2 ) )) renvoie un entier r avec :
r < 0, si s1 est strictement infrieure ` s2 .
e
a
r = 0, si les deux cha
nes sont les mmes.
e
r > 0, si s1 est strictement plus grande t s2 .
Lordre sur les cha
nes est induit par lordre des codes des caract`res, ce qui fait que lon
e
obtient lordre du dictionnaire stricto-sensu seulement pour les mots sans accents.
Il y a beaucoup dautres mthodes, par exemple toLowerCase et toUpperCase pour mie
nusculer et majusculer.
public String toLowerCase()
public String toUpperCase()
Cette fois-ci les caract`res accentus sont respects.
e
e
e

6.1.4

Les fabricants de cha


nes

On ne peut pas changer une cha par exemple changer un caract`re individuellement. Or,
ne,
e
on veut souvent construire une cha petit ` petit, on peut le faire en utilisant la concatnation
ne
a
e
mais il y a alors un prix ` payer, car une nouvelle cha est cre ` chaque concatnation. En
a
ne
ee a
e
fait, lorsque lon construit une cha de gauche vers la droite (pour achage dun tableau par
ne
ex.) on a besoin dun autre type de structure de donnes : le StringBuilder (ou StringBuer
e
essentiellement quivalent). Un objet StringBuilder contient un tampon de caract`re interne,
e
e
qui peut changer de taille et dont les lments peuvent tre modis. Les membres les plus utiles
ee
e
e
de cette classe sont :
Le constructeur sans argument.
public StringBuilder ()
Cre un nouveau fabricant dont le tampon a la capacit initiale par dfaut (16 caract`res).
e
e
e
e
Les mthodes append surcharges. Il y en a treize, six pour les scalaires, et les autres pour
e
e
divers objets.
public StringBuilder append(int i)
public StringBuilder append(String str)
public StringBuilder append(Object obj)
Toutes ces mthodes ajoutent la reprsentation sous forme de cha de leur argument `
e
e
ne
a
la n du tampon interne, dont la capacit est augment silencieusement si ncessaire.
e
e
e
Dans le cas de largument objet quelconque (le dernier), cest la cha obtenue par
ne
e
a
obj.toString() qui est ajoute, ou "null" si obj est null . Les autres mthodes sont l`,
e
soit pour des raisons decacit (cas de String), soit pour les tableaux dont le toString()
e
est inutilisable, soit parce que largument est de type scalaire (cas de int ). Toutes les
mthodes append renvoient lobjet StringBuilder dont on appelle la mthode, ce qui est
e
e
absurde, cet objet restant le mme. Je crois quil en est ainsi an dautoriser par exemple
e
lcriture sb.append(i).append(:) ; au lieu de sb.append(i) ; sb.append(:) ;
e
(o` i est une variable par exemple de type int ).
u
Et bien videmment, une mthode toString rednie.
e
e
e
public String toString()
Copie le contenu du tampon interne dans une nouvelle cha Contrairement aux tampons
ne.
de chier, le tampon nest pas vid. Pour vider le tampon, on peut avoir recours ` la
e
a
mthode setLength avec largument zro.
e
e
On trouvera un exemple dutilisation classique dun objet StringBuilder en I.2.1 : une cration,
e
des tas de append(...), et un toString() nal.

`
6. QUELQUES CLASSES DE BIBLIOTHEQUE
6.1.5

211

La classe System

Cette classe System est un peu fourre-tout. Elle posss`de trois champs et quelques mthodes
e
e
assez utiles.
Les trois ux standards.
public static final InputStream in
public static final PrintStream out
public static final PrintStream err
Ces trois ux sont des ux doctets (voir la section sur les entres-sorties 5). Notez que
e
les champs sont dclars nal , on ne peut donc pas les changer (bizarrement il existe
e
e
quand mme des mthode pour changer les trois ux standard). Les deux ux de sortie
e
e
orent des sorties formates par une mthode print bien pratique, car elle est surcharge
e
e
e
et fonctionne pour toutes les valeurs.
La sortie out est la sortie normale du programme, tandis que la sortie err est la sortie
derreur. En fonctionnement normal ces deux ux semblent confondus car aboutissant
tous deux dans la mme fentre. Mais on peut rediriger la sortie out, par exemple vers
e
e
un chier (>chier). Les messages derreurs sachent alors au lieu de se mlanger avec la
e
sortie normale du programme dans chier.
Une mthode pour arrter imdiatement le programme.
e
e
e
public static void exit(int status)
Lentier status est le code de retour qui vaut conventionellement zro dans le cas dune
e
excution normale, et autre chose pour une excution qui sest mal passe, En Unix, le
e
e
e
code est accessible juste apr`s lexcution comme $status ou $?, selon les shells.
e
e
Deux mthodes donnent acc`s ` un temps absolu. La seconce permet de mesurer le temps
e
e a
dexcution par une soustractions.
e
public static long currentTimeMillis()
public static long nanoTime()
La mthode currentTimeMillis renvoie les milisecondes coules depuis le 1er janvier
e
e
e
1970. Il sagit donc du temps ordinaire. La mthode nanoTime() renvoie les nanosecondes
e
(109 ) depuis une date arbitraire. Il sagit du temps pass dans le code du programme, ce
e
qui est dirent du temps ordinaire surtout dans un syst`me dexploitation multi-tches
e
e
a
comme Unix.

6.2

Package java.io, entres-sorties


e

Ce package regroupe sattaque principalement ` la communication avec le syst`me de chiers


a
e
de la machine. Nous ne passons en revue ici que les ux de caract`res, qui correspondent aux
e
chiers texte (voir 5).
6.2.1

Classe des lecteurs

Les objets de la classe Reader sont des ux de caract`res ouverts en lecture. Un tel ux
e
est simplement une squence de caract`res que lon lit les un apr`s les autres, et qui nit par se
e
e
e
terminer.
La mthode la plus signicative est donc celle qui permet de lire le caract`re suivant du
e
e
ux.
public int read() throws IOException
Cette mthode renvoie un int qui est le caract`re en attente dans le ux dentre, ou 1, si
e
e
e
le ux est termin. On notera que read renvoie un int et non pas un char prcisment pour
e
e e

212

ANNEXE B. MORCEAUX DE JAVA


pouvoir identier la n du ux facilement. En outre et comme annonc par sa dclaration
e
e
elle peut lever lexception IOException en cas derreur (possible d`s que lon sintresse
e
e
au chiers).
Quand la n dun ux est atteinte, il faut fermer le ux.
public void close() throws IOException

Cette opration lib`re les ressources mobilises lors de la cration dun ux dentre
e
e
e
e
e
(numros divers lies au syst`me de chiers, tampons internes). Si on ne ferme pas les
e
e
e
uxs dont on ne sert plus, on risque de ne pas pouvoir en ouvrir dautres, en raison de
lpuisement de ces ressources.
e
La classe Reader sert seulement ` dcrire une fonctionnalit et il nexiste pas ` proprement
a e
e
a
parler dobjets de cette classe.
En revanche, il existe diverses sous-classes (voir 2.3) de Reader qui permettent de construire
concr`tement des objets, qui peuvent se faire passer pour des Reader. Il sagit par entre autres
e
des classes StringReader (lecteur sur une cha
ne), InputStreamReader (lecteur sur un ux
doctets) et FileReader (lecteur sur un chier). On peut donc par exemple obtenir un Reader
ouvert sur un chier /usr/share/dict/french par
Reader in = new FileReader ("/usr/share/dict/french") ;
et un Reader sur lentre standard par
e
Reader in = new InputStreamReader (System.in) ;
6.2.2

Lecteur avec tampon

Un objet BueredReader est dabord un Reader qui groupe les appels au ux sous-jacent
et stocke les caract`res lus dans un tampon, an damliorer les performances (voir 5.4). Mais
e
e
un BueredReader ore quelques fonctionnalits en plus de celles des Reader.
e
Le constructeur le plus simple prend un Reader en argument.
public BufferedReader(Reader in)
Construit un ux dentre bueris ` partir dun lecteur quelconque.
e
ea
La classe BueredReader est une sous-classe de la classe Reader de sorte quun objet BueredReader poss`de une mthode read.
e
e
public int read() throws IOException
Cette mthode nest pas hrite mais rednie, an de grer le tampon interne.
e
e e
e
e
La lecture dune ligne se fait par une nouvelle mthode, que les Reader ne poss`dent pas.
e
e
public String readLine() throws IOException
Renvoie une ligne de texte ou null en n de ux. La mthode fonctionne pour les trois
e
e
conventions de n de ligne (\n, \r ou \r suivi de \n) et le ou les caract`res (( n
de ligne )) ne sont pas renvoys. La mthode fonctionne encore pour la derni`re ligne dun
e
e
e
ux qui ne serait pas termine par une n de ligne explicite.
e

6.3
6.3.1

Package java.util : trucs utiles


Les sources pseudo-alatoires
e

Une source pseudo-alatoire est une suite de nombres qui ont lair dtre tirs au hasard. Ce
e
e
e
qui veut dire que les nombres de la suite obissent ` des crit`res statistiques censs exprimer
e
a
e
e
le hasard (et en particulier luniformit, mais aussi un aspect chaotique pas vident ` spcier
e
e
a e
prcisment). Une fois la source construite (un objet de la classe Random), diverses mthodes
e e
e

`
7. PIEGES ET ASTUCES

213

permettent dobtenir divers scalaires ( int , double, etc.) que lon peut considrer comme tirs au
e
e
hasard. On a besoin de tels nombres (( tirs au hasard )) par exemple pour raliser des simulations
e
e
(voir II.1.1) ou produire des exemples destiner ` tester des programmes.
a
Les constructeurs.
public Random()
public Random(long seed)
La version avec argument permet de donner une semence de la source pseudo-alatoire,
e
deux sources qui ont la mme semence se comportant ` lidentique. La version sans argue
a
ment laisse la biblioth`que choisir une semence comme elle lentend. La semence choisie
e
change ` chaque appel du constructeur et on suppose quelle est choisie de faon la plus
a
c
arbitraire et non-reproductible possible (par exemple ` partir de lheure quil est).
a
Les nombres pseudo-alatoires sont produits par diverses mthodes (( next )). Distinguons
e
e
particuli`rement,
e
Un entier entre zro (inclus) et n (exclu).
e
public int nextInt(int n)
Cette mthode est pratique pour tirer un entier au pseudo-hasard sur un intervalle
e
[a. . . b[, par a + rand.nextInt(b-a).
Un ottant entre 0.0 (inclus) et 1.0 (exclu).
public float nextFloat()
public double nextDouble()
Ces mthodes sont pratiques pour dcider selon une probabilit quelconque. Du style
e
e
e
rand.nextDouble() < 0.75 vaut true avec une probabilit de 75 %.
e

Pi`ges et astuces
e

Cette section regroupe des astuces de programmation ou corrige des erreurs frquemment
e
rencontres.
e

7.1

Sur les caract`res


e

Les codes des caract`res de 0 a 9 se suivent, de sorte que les deux trucs suivants
e
`
sappliquent. Pour savoir si un caract`re c est un chire (arabe) :
e
0 <= c && c <= 9
Il existe aussi une mthode Character.isDigit(char c) qui tient compte de tous les chires
e
possibles (en arabe cest-`-dire indiens, etc.). Pour rcuprer la valeur dun chire :
a
e
e
int i = c - 0 ;
Ici encore il y a des mthodes statiques qui donnent les valeurs de chires dans la classe
e
Character.

7.2

Oprateurs
e

Java poss`de un oprateur ternaire (` trois arguments) lexpression conditionnelle ec ? et :


e
e
a
ef . Lexpression ec est de type boolean, tandis que le types de et et ef sont identiques. Lexpression conditionnelle vaut et si ec vaut true, et ef si ec vaut false . De sorte que lon peut acher
un boolen ainsi :
e
// b est un boolean, on veut afficher + si b est vrai et - autrement.
System.out.println(b ? + : -) ;

214

7.3

ANNEXE B. MORCEAUX DE JAVA

Connecteurs ( paresseux )
(
)

Les oprateurs || et && sont respectivement la disjonction (ou logique) et la conjonction (et
e
logique), ils sont de la varit dite squentielle (gauche-droite) ou parfois paresseuse : si valuer
ee
e
e
la premi`re condition sut pour dterminer le rsultat, alors la seconde condition nest pas
e
e
e
value. Plus prcisment, si dans e1 || e2 (resp. e1 && e2 ) e1 vaut true (resp. false ) , alors
e
e
e e
e2 nest pas value, et la disjonction (resp. conjonction) vaut true (resp. false). Cest un idiome
e
e
de proter de ce comportement pour crire des conditions concises. Par exemple, pour savoir si
e
la liste xs contient au moins deux lments, on peut crire
ee
e
. . . xs != null && xs.next != null . . .
Cette expression nchoue jamais, cas si xs vaut null alors lacc`s xs.next nest pas tent.
e
e
e
On remarque que les connecteurs && et || peuvent sexprimer ` laide dun seul oprateur :
a
e
loprateur ternaire e1 ? e2 : e3 . La conjonction e1 && e2 sexprime comme e1 ? e2 : false
e
et la disjonction e1 || e2 comme e1 ? true : e2

Bibliographie
[1] J. L. Bentley et M. D. McIlroy, Engineering a Sort Function, Software - Practice and
Experience 23,11 (1993), 12491265.
[2] T. H. Cormen, C. E. Leiserson et R. L. Rivest, Introduction a lalgorithmique, Dunod,
`
1994.
[3] J. E. F. Friedl, Ma
trise des expressions rguli`res, OReilly, 2001.
e
e
[4] D. E. Knuth, Fundamental Algorithms. The Art of Computer Programming, vol. 1, Addison
Wesley, 1968.
[5] D. E. Knuth, Seminumerical Algorithms. The Art of Computer Programming, vol. 2, Addison Wesley, 1969.
[6] D. E. Knuth, Sorting and Searching. The Art of Computer Programming, vol. 3, Addison
Wesley, 1973.
[7] R. Sedgewick, Algorithms, (2nd edition), Addison-Wesley, 1988.
[8] R. Uzgalis, Hashing Concepts and the Java Programming Language, Rap. Tech., University
of Auckland, New Zealand, 1996.

215

Index
!= (oprateur), voir galit
e
e
e
&& (oprateur), voir et logique
e
== (oprateur), voir galit
e
e
e
|| (oprateur), voir ou logique
e
acc`s
e
direct, 9
squentiel, 9, 198
e
add (mthode des les)
e
en liste, 59
en tableau, 58
adresse, 9, 179, 180
algorithme, 167
allocation (mmoire), 7
e
alphabet, 89
anctre, 82
e
append (mthode)
e
itratif, 20
e
rcursif, 20
e
arbre, 82
de syntaxe abstraite, 144
AVL, 124
binaire, 87
complet, 88, 103
de recherche, 117
tass, 96
e
couvrant, 87
de dcision, 91
e
de drivation, 138
e
de Fibonacci, 125
de slection, 101
e
de syntaxe abstraite, 92
enracin, 82
e
quilibr, 124
e
e
libre, 82
ordonn, 83, 116
e
arcs, 81
arte, 82
e
arit, 82
e
association, 65
automate, 155
transition, 162

ni dterministe, 155
e
ni non-dterministe, 158
e
reprsentation Java, 163
e
table de transition, 156
balise, 131
biblioth`que, 207
e
break (mot-cl), 189, 190
e
buer, voir tampon
BueredReader (classe), 212
catch (mot-cl), 196
e
champ, 173, 177
chemin
simple, 82
circuit, 81
class gnrique, 55, 76
e e
code, 89
prxe, 103
e
complet, 103
collision, 69
Complexit
e
dans le pire cas, 168
en moyenne, 168
concatnation
e
des cha
nes, 11, 209
des listes, 20, voir append et nappend
des mots, 89, 135
contenu, 113
continue (mot-cl), 190
e
copy (mthode)
e
itratif, 15
e
cot amorti, 53
u
dbordement de la pile, 30
e
descendant, 82
dictionnaire, 118
distance, 87
do (mot-cl), 190
e
double rotation, 126
droite, 126
galit, 181
e
e
216

INDEX
physique, 181
structurelle, 182
else (mot-cl), 189
e
empreinte mmoire, 64
e
encapsulage, 33, 34, 68, 145
encodage, 206
encoding, voir encodage
enfant, 82
entre
e
standard, 198
erreur de type, 182
et logique, 214
exception, 53, 194
attraper, voir try, 196
dclarer, voir throws, 198
e
dnir, 197
e
lancer, voir throw, 197
expression, 186
expression conditionnelle, 213, 214
extends (mot-cl), 176, 197
e
extrmit, 81
e
e
facteur de charge, 71
feuille, 82
chier, 198
binaire, 198
fermer, 200
ouvrir, 200
texte, 198, 206
FIFO, 45
le, 45, 115
de priorit, 96
e
FileReader (classe), 212
lle, 82
ls, 82
droit, 113
gauche, 113
nal (mot-cl), 185
e
ux, 198
bueris, 202
e
for (mot-cl), 190
e
fort, 83
e
fusion
de listes ordonnes, 101
e
des listes tries, 27, voir merge
e
garbage collection, 13
gestion automatique de la mmoire, 7, 13
e
Grand , 168
Grand , 168
Grand O, 168

217
graphe, 81
connexe, 82
non orient, 81
e
orient, 81
e
hachage, 69
HashMap (classe), 76
hauteur
dun arbre, 82
dun arbre binaire, 87
moyenne, 124
hritage, 176, 179
e
idiome, 9
boucle innie, 41, 190
boucle vide, 21
oprateur logique squentiel, 24
e
e
oprateur logique squentiel, 25, 214
e
e
parcours de liste, 9
if (mot-cl), 189
e
implements (mot-cl), 67
e
import (mot-cl), 191
e
inxe, 187
initialisation dire, 14, 31
ee
initialisation par dfaut, 186, 192
e
InputStreamReader (classe), 212
insertion
dans un arbre, 98
dans une liste trie, 25
e
insertionSort (mthode), 24
e
instruction, 186
interface, 66, 68
interface (mot-cl), 66
e
Landau, 168
langage rgulier, 137
e
lettre, 89
lex`me, 205
e
LIFO, 45
liste
boucle, 36
e
circulaire, 36
doublement cha ee, 41, 61
n
simplement cha ee, 7
n
longueur
dun chemin, 81
dun mot, 89
dun tableau, 193
dune cha
ne, 209
dune liste, 10

218
main (mthode), 174
e
mem (mthode)
e
itratif, 11
e
rcursif, 11
e
membre (dune classe), 173
m`re, 82
e
merge (mthode)
e
itratif, 31
e
rcursif, 28
e
mergeSort (mthode), 28
e
mot, 89
nappend (mthode)
e
itratif, 21
e
rcursif, 20
e
nud, 81
interne, 82
nombres de Catalan, 88
notation
inxe, 49, 92, 94
postxe, 47
nremove (mthode)
e
itratif, 18
e
rcursif, 18
e
null (mot-cl), 178
e
objet, 177, 180
ordre
fractionnaire, 91
inxe, 90
prxe, 90
e
suxe, 90
origine, 81
ou logique, 11, 24, 214
overloading, voir surcharge
overriding, voir rednition de mthode
e
e
package, 175
parcours, 115
darbre, 89
en largeur, 90, 115
en profondeur, 90
inxe, 90
postxe, 90
prxe, 90, 115
e
suxe, 90
parent, 82
partition, 83
p`re, 82
e
pile, 45, 115
pointeur, 180

INDEX
de pile, 51
pop (mthode)
e
en liste, 55
en tableau, 51
porte lexicale, 188
e
postxe, 187
prxe, 89, 187
e
prxe (dun mot), 135
e
prxiel, 89
e
private (mot-cl), 175, 175
e
profondeur
dun nud, 87
protected (mot-cl), 175
e
public (mot-cl), 173, 175
e
push (mthode)
e
en liste, 55
en tableau, 51
queue, voir le
a
` deux bouts, 61
racine, 82, 87, 113
Random (classe), 212
Reader (classe), 211
rcursion
e
terminale, 23
rednition de mthode, 179
e
e
rfrence, 180
ee
remove (mthode)
e
itratif, 16
e
rcursif, 12
e
remove (mthode des les)
e
en liste, 59
en tableau, 58
return (mot-cl), 188
e
reverse (mthode)
e
itratif, 21
e
rcursif, 22
e
rotation, 126
scalaire, 180
signature, 186, 198
sondage
linaire, 73
e
par double hachage, 75
sortie
derreur standard, 198
standard, 198
sous-arbre, 82
droit, 87
gauche, 87

INDEX
sous-classe, 179, 197, 199, 200
sous-typage, 179, 183, 201
spcication de visibilit, 175
e
e
Stack (classe), 55
stack, voir pile
stack overow, voir dbordement de la pile
e
static (mot-cl), 173, 185
e
String (classe), 209
StringBuer (classe), 210
StringBuilder (classe), 11, 210
structure
dynamique, 7
fonctionnelle, voir non-mutable
imprative, voir mutable
e
inductive, 7
mutable, 18, 20
non-mutable, 12, 20
persistante, voir non-mutable
squentielle, 9
e
structure de bloc, 188
suxe (dun mot), 135
suppression
dune cl, 120
e
dans un arbre, 98
surcharge, 177, 179, 210
switch (mot-cl), 189
e
tableau, 192
taille
dun arbre, 115
dun tableau, 193
dune cha
ne, 209
tampon, 202, 210
tas, 96, 97
this (mot-cl), 178, 186, 191
e
throw (mot-cl), 38, 53, 197
e
throws (mot-cl), 53, 54, 198
e
token, voir lex`me
e
toString, 10
rednition, 179
e
tri
par comparaison, 91
par tas, 100
tri (des listes)
fusion, 28
par insertion, 24
try (mot-cl), 54, 196
e
type, 182
abstrait, 63
Union-Find, 83

219
uniq (mthode), 24
e
valeur, 180
variable, 180
variable dinstance, 177
while (mot-cl), 189
e

Vous aimerez peut-être aussi