cole Polytechnique
INF411
Les bases de la programmation
et de l'algorithmique
Jean-Christophe Fillitre
dition 2014
ii
Avant-propos
Ce polycopi est utilis pour le cours INF411 intitul Les bases de la programmation
et de l'algorithmique. Ce cours fait suite au cours INF311 intitul Introduction l'informatique et prcde le cours INF421 intitul Design and Analysis of Algorithms.
Ce polycopi reprend, dans sa premire partie, quelques lments d'un prcdent polycopi crit, en plusieurs itrations, par Jean Berstel, Jean-ric Pin, Philippe Baptiste,
Luc Maranget et Olivier Bournez. Je les remercie sincrement pour m'avoir donn accs
AT X de ce polycopi et m'avoir autoris en rutiliser une partie. D'autres
aux sources L
E
lments sont repris, et adapts, d'un ouvrage en cours de prparation avec mon collgue
Sylvain Conchon.
Je remercie galement chaleureusement les direntes personnes qui ont pris le temps
de relire tout ou partie de ce polycopi : Martin Clochard, Stefania Dumbrava, Lon
Gondelman, Franois Pottier, David Savourey.
On peut consulter la version PDF de ce polycopi, ainsi que l'intgralit du code Java,
sur le site du cours :
[Link]
L'auteur peut tre contact par courrier lectronique l'adresse suivante :
[Link]@[Link]
Historique
Version 1 : samedi 3 aot 2013
Version 2 : mercredi 30 juillet 2014
iv
Table des matires
I Prliminaires
1 Le langage Java
1.1
1.2
Programmation oriente objets
. . . . . . . . . . . . . . . . . . . . . . . .
1.1.1
Encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1.2
Champs et mthodes statiques . . . . . . . . . . . . . . . . . . . . .
1.1.3
Surcharge
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1.4
Hritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1.5
Classes abstraites . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
1.1.6
Classes gnriques
10
1.1.7
Interfaces
1.1.8
Rgles de visibilit
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
Modle d'excution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
1.2.1
Arithmtique des ordinateurs
. . . . . . . . . . . . . . . . . . . . .
13
1.2.2
Mmoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
1.2.3
Valeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2 Notions de complexit
2.1
2.2
2.3
12
. . . . . . . . . . . . . . . . . . . . . . . . . . .
23
Complexit d'algorithmes et complexit de problmes . . . . . . . . . . . .
23
2.1.1
La notion d'algorithme . . . . . . . . . . . . . . . . . . . . . . . . .
23
2.1.2
La notion de ressource lmentaire
. . . . . . . . . . . . . . . . . .
24
2.1.3
Complexit d'un algorithme au pire cas . . . . . . . . . . . . . . . .
24
2.1.4
Complexit moyenne d'un algorithme . . . . . . . . . . . . . . . . .
25
2.1.5
Complexit d'un problme . . . . . . . . . . . . . . . . . . . . . . .
26
Complexits asymptotiques
. . . . . . . . . . . . . . . . . . . . . . . . . .
27
2.2.1
Ordres de grandeur . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
2.2.2
Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
2.2.3
Notation de Landau
. . . . . . . . . . . . . . . . . . . . . . . . . .
28
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
2.3.1
Factorielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29
2.3.2
Tours de Hanoi
29
Quelques exemples
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
vi
II Structures de donnes lmentaires
31
3 Tableaux
33
3.1
3.2
3.3
3.4
Parcours d'un tableau
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
Recherche dans un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
3.2.1
Recherche par balayage . . . . . . . . . . . . . . . . . . . . . . . . .
35
3.2.2
Recherche dichotomique dans un tableau tri . . . . . . . . . . . . .
36
Mode de passage des tableaux . . . . . . . . . . . . . . . . . . . . . . . . .
38
Tableaux redimensionnables
. . . . . . . . . . . . . . . . . . . . . . . . . .
39
3.4.1
Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
40
3.4.2
Application 1 : Lecture d'un chier
41
3.4.3
Application 2 : Concatnation de chanes . . . . . . . . . . . . . . .
44
3.4.4
Application 3 : Structure de pile . . . . . . . . . . . . . . . . . . . .
44
3.4.5
Code gnrique
47
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4 Listes chanes
49
4.1
Listes simplement chanes . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
4.2
Application 1 : Structure de pile . . . . . . . . . . . . . . . . . . . . . . . .
54
4.3
Application 2 : Structure de le
54
4.4
. . . . . . . . . . . . . . . . . . . . . . . .
Modication d'une liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
4.4.1
Listes cycliques
59
4.4.2
Listes persistantes
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
60
4.5
Listes doublement chanes . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
4.6
Code gnrique
66
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5 Tables de hachage
69
5.1
Ralisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
70
5.2
Redimensionnement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
73
5.3
Code gnrique
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
74
5.4
Brve comparaison des tableaux, listes et tables de hachage . . . . . . . . .
75
6 Arbres
77
6.1
Reprsentation des arbres
. . . . . . . . . . . . . . . . . . . . . . . . . . .
77
6.2
Oprations lmentaires sur les arbres . . . . . . . . . . . . . . . . . . . . .
78
6.3
Arbres binaires de recherche . . . . . . . . . . . . . . . . . . . . . . . . . .
79
6.3.1
Oprations lmentaires
. . . . . . . . . . . . . . . . . . . . . . . .
80
6.3.2
quilibrage
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
84
6.3.3
Structure d'ensemble . . . . . . . . . . . . . . . . . . . . . . . . . .
90
6.3.4
Code gnrique
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91
6.4
Arbres de prxes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
92
6.5
Cordes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
96
7 Files de priorit
101
7.1
Structure de tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
7.2
Reprsentation dans un tableau
7.3
Reprsentation comme un arbre . . . . . . . . . . . . . . . . . . . . . . . . 107
7.4
Code gnrique
. . . . . . . . . . . . . . . . . . . . . . . . 102
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
vii
8 Classes disjointes
111
8.1
Principe
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
8.2
Ralisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
III Algorithmes lmentaires
117
9 Arithmtique
119
9.1
Algorithme d'Euclide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
9.2
Exponentiation rapide
9.3
Crible d'ratosthne
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
10 Programmation dynamique et mmosation
125
10.1 Mmosation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
10.2 Programmation dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
10.3 Comparaison
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
11 Rebroussement (backtracking )
131
12 Tri
137
12.1 Tri par insertion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
12.2 Tri rapide
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
12.3 Tri fusion
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
12.4 Tri par tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
12.5 Code gnrique
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
12.6 Exercices supplmentaires
. . . . . . . . . . . . . . . . . . . . . . . . . . . 149
13 Compression de donnes
151
13.1 L'algorithme de Human . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
13.2 Ralisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
IV Graphes
161
14 Dnition et reprsentation
163
14.1 Matrice d'adjacence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
14.2 Listes d'adjacence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
14.3 Code gnrique
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
15 Algorithmes lmentaires sur les graphes
15.1 Parcours de graphes
169
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
15.1.1 Parcours en largeur . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
15.1.2 Parcours en profondeur . . . . . . . . . . . . . . . . . . . . . . . . . 172
15.2 Plus court chemin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Annexes
185
A Lexique Franais-Anglais
185
viii
Bibliographie
187
Index
189
Premire partie
Prliminaires
Le langage Java
On rappelle ici certains points importants du langage de programmation Java. Ce chapitre n'est nullement exhaustif et suppose que le lecteur est dj familier du langage Java,
notamment grce au cours INF311. On pourra aussi lire l'excellent ouvrage
to Programming in Java
Introduction
de Sedgewick et Wayne [8].
1.1 Programmation oriente objets
Le concept central est celui de
classe . La dclaration d'une classe introduit un nouveau
type. En toute premire approximation, une classe peut tre vue comme un enregistrement.
class Polar {
double rho;
double theta;
}
rho et theta sont les deux champs de la classe Polar, de type double. On cre
instance particulire d'une classe, appele un objet , avec la construction new. Ainsi
Ici
une
Polar p = new Polar();
dclare une nouvelle variable locale
instance de la classe
Polar.
p,
de type
Polar,
dont la valeur est une nouvelle
L'objet est allou en mmoire. Ses champs reoivent des
valeurs par dfaut (en l'occurrence ici le nombre ottant
champs de
p,
et les modier, avec la notation usuelle
p.x.
0.0).
On peut accder aux
Ainsi on peut crire
[Link] = 2;
[Link] = 3.14159265;
double x = [Link] * [Link]([Link]);
[Link] = [Link] / 2;
Pour allouer de nouveaux objets en initialisant leurs champs avec des valeurs particulires,
constructeurs . Un
champs rho et theta en
autres que les valeurs par dfaut, on peut introduire un ou plusieurs
constructeur naturel pour la classe
Polar
arguments. On l'crit ainsi (dans la classe
prend les valeurs des
Polar)
Chapitre 1. Le langage Java
Polar(double r, double t) {
if (r < 0) throw new Error("Polar: negative length");
rho = r;
theta = t;
}
Ici, on ne se contente pas d'initialiser les champs. On vrie galement que
n'est pas
ngatif. Dans le cas contraire, on lve une exception. Ce constructeur nous permet d'crire
maintenant
Polar p = new Polar(2, 3.14159265);
Attention
Nous avions pu crire plus haut
new Polar()
sans avoir dni de constructeur.
En eet, toute classe possde un constructeur par dfaut, sans argument. Mais
une fois qu'un constructeur est ajout la classe
Polar,
le constructeur im-
plicite sans argument disparat. Dans l'exemple ci-dessus, si on tente d'crire
new Polar(), on obtient un message d'erreur du compilateur : The
constructor Polar() is undefined. Rien ne nous empche cependant de rinmaintenant
troduire un constructeur sans argument. Une classe peut en eet avoir plusieurs
constructeurs, avec des arguments en nombre ou en nature dirents. On parle de
surcharge.
La surcharge est explique plus loin.
1.1.1 Encapsulation
Supposons maintenant que l'on veuille maintenir l'invariant suivant pour tous les
objets de la classe
Polar
0 rho
Pour cela on dclare les champs
l'extrieur de la classe
Polar.
rho
0 theta < 2
theta privs,
et
de sorte qu'ils ne sont plus visibles
class Polar {
private double rho, theta;
Polar(double r, double t) { /* garantit l'invariant */ }
}
Si on cherche accder au champ
[Link]
pour un certain objet
rho
depuis une autre classe, en crivant par exemple
de la classe
Polar,
on obtient un message d'erreur du
compilateur :
[Link][Link] rho has private access in Polar
Les objets remplissent donc un premier rle d'encapsulation. La valeur du champ
peut nanmoins tre fournie par l'intermdiaire d'une
fournie par la classe
Polar
mthode , c'est--dire d'une fonction
et applicable tout objet de cette classe.
class Polar {
private double rho, theta;
...
double norm() { return rho; }
}
rho
1.1. Programmation oriente objets
Pour un objet
de type
Polar,
on appelle la mthode
norm
ainsi :
[Link]()
Navement, on peut voir cet appel de mthode comme un appel
norm
norm(p)
une
fonction
qui recevrait l'objet comme premier argument. Dans une mthode, cet argument
implicite, qui est l'objet sur lequel on appelle une mthode, est dsign par le mot-cl
this.
Ainsi on peut rcrire la mthode
norm
ci-dessus de la manire suivante :
double norm() { return [Link]; }
On explicite le fait que
rho
dsigne ici un champ de l'objet. En particulier, on vite une
confusion possible avec une variable locale ou un paramtre de la mthode. De la mme
manire, nous aurions pu crire le constructeur sous la forme
Polar(double r, double t) {
[Link] = r;
[Link] = t;
}
car, dans le constructeur,
this
dsigne l'objet qui vient d'tre allou. Du coup, on peut
mme donner aux paramtres du constructeur les mmes noms que les champs :
Polar(double rho, double theta) {
[Link] = rho;
[Link] = theta;
}
Il n'y a pas d'ambigut, puisque
rho
dsigne le paramtre et
[Link]
le champ. On
vite ainsi d'avoir trouver un nom dirent pour l'argument quand on programme,
le plus dicile est souvent de trouver des noms judicieux.
Attention
En revanche, il serait incorrect d'crire le constructeur sous la forme
Polar(double rho, double theta) {
rho = rho;
theta = theta;
}
Bien qu'accept par le compilateur, ces aectations sont sans eet : elles ne font
qu'aecter les valeurs des paramtres
rho
et
theta
aux paramtres
rho
et
theta
eux-mmes.
1.1.2 Champs et mthodes statiques
Il est possible de dclarer un champ comme
statique
et il est alors li la classe et non
aux instances de cette classe ; dit autrement, il s'apparente une variable globale.
class Polar {
double rho, theta;
static double two_pi = 6.283185307179586;
Chapitre 1. Le langage Java
De mme, une
mthode
peut tre
statique
et elle s'apparente alors une fonction tradi-
tionnelle.
static double normalize(double x) {
while (x < 0) x += two_pi;
while (x >= two_pi) x -= two_pi;
return x;
}
Ce qui n'est pas statique est appel
this
dynamique.
Dans une mthode statique, l'emploi de
n'est pas autoris. En eet, il n'existe pas ncessairement d'objet particulier ayant
t utilis pour appeler cette mthode. Pour la mme raison, une mthode statique ne peut
pas appeler une mthode dynamique. l'inverse, en revanche, une mthode dynamique
peut parfaitement appeler une mthode statique. Enn, on note que le point d'entre d'un
programme Java, savoir sa mthode
main,
est une mthode statique :
public static void main(String[] args) { ... }
1.1.3 Surcharge
Plusieurs mthodes d'une mme classe peuvent porter le mme nom, pourvu qu'elles
aient des arguments en nombre et/ou en nature dirents ; c'est ce que l'on appelle la
surcharge (en anglais overloading ). Ainsi on peut crire dans la classe Polar deux mthodes mult pour multiplier respectivement par un autre nombre complexe en coordonnes
polaires ou par un simple ottant.
class Polar {
...
void mult(Polar p) {
[Link] *= [Link]; [Link] = normalize([Link] + [Link]);
}
void mult(double f) {
[Link] *= f;
}
}
On peut alors crire des expressions comme
[Link](p)
ou encore
[Link](2.5).
La sur-
charge est rsolue par le compilateur, au moment du typage. Tout se passe comme si on
avait crit en fait deux mthodes avec des noms dirents
class Polar {
...
void mult_Polar(Polar p) {
[Link] *= [Link]; [Link] = normalize([Link] + [Link]);
}
void mult_double(double f) {
[Link] *= f;
}
}
1.1. Programmation oriente objets
puis les expressions
p.mult_Polar(p) et p.mult_double(2.5). Ce n'est donc rien d'autre
qu'une facilit fournie par le langage pour ne pas avoir introduire des noms dirents. On
peut surcharger autant les mthodes statiques que dynamiques, ainsi que les constructeurs
(voir notamment l'encadr page 4).
1.1.4 Hritage
B
Le concept central de la programmation oriente objet est celui d'hritage : une classe
peut tre dnie comme hritant d'une classe
A,
ce qui se note
class B extends A { ... }
Les objets de la classe
hritent alors de tous les champs et mthodes de la classe
A,
auxquels ils peuvent ajouter de nouveaux champs et de nouvelles mthodes. La notion
d'hritage s'accompagne d'une notion de
vue comme une valeur de type
appelle cela l'hritage
simple
A.
sous-typage
: toute valeur de type
peut tre
En Java, chaque classe hrite d'au plus une classe ; on
(par opposition l'hritage multiple, qui existe dans d'autres
langage orients objets, comme C++). La relation d'hritage forme donc un arbre
class
class
class
class
A
B
C
D
{ ... }
extends A { ... }
extends A { ... }
extends C { ... }
A
B C
D
Prenons comme exemple un ensemble de classes pour reprsenter des objets graphiques
(cercles, rectangles, etc.). On introduit en premier lieu une classe
Graphical reprsentant
n'importe quel objet graphique :
class Graphical {
int x, y;
/* centre */
int width, height;
void move(int dx, int dy) { x += dx; y += dy; }
void draw() { /* ne fait rien */ }
On a quatre champs, pour le centre et les dimensions maximales de l'objet, et deux m-
move pour dplacer l'objet et draw pour le dessiner. Pour reprsenter un rectangle,
hrite de la classe Graphical.
thodes,
on
class Rectangle extends Graphical {
On hrite donc des champs
x, y, width et height et des mthodes move et draw. On peut
crire un constructeur qui prend en arguments deux coins du rectangle :
Rectangle(int x1, int y1, int x2, int y2) {
this.x = (x1 + x2) / 2;
this.y = (y1 + y2) / 2;
[Link] = [Link](x1 - x2);
[Link] = [Link](y1 - y2);
}
Chapitre 1. Le langage Java
Graphical.
On peut utiliser directement toute mthode hrite de
On peut crire par
exemple
Rectangle p = new Rectangle(0, 0, 100, 50);
[Link](10, 5);
Pour le dessin, en revanche, on va
(en anglais on parle d'overriding )
rednir
la mthode
draw
dans la classe
Rectangle
class Rectangle extends Graphical {
...
void draw() { /* dessine le rectangle */ }
}
et le rectangle sera alors eectivement dessin quand on appelle
[Link]();
Type statique et type dynamique
La construction
new C(...)
ne peut tre modie par la suite ; on l'appelle le
revanche, le
type statique
C, et la classe de cet objet
type dynamique de l'objet. En
construit un objet de classe
d'une expression, tel qu'il est calcul par le compilateur,
peut tre dirent du type dynamique, du fait de la relation de sous-typage introduite
par l'hritage. Ainsi, on peut crire
Graphical g = new Rectangle(0, 0, 100, 50);
[Link](); // dessine le rectangle
Pour le compilateur,
Graphical, mais le rectangle est eectivement
draw de la classe Rectangle qui est excute.
a le type
sin : c'est bien la mthode
On procde de mme pour dnir des cercles. Ici on ajoute un champ
radius
des-
pour le
rayon, an de le conserver.
class Circle extends Graphical {
int radius;
Circle(int x, int y, int r) {
this.x = x;
this.y = y;
[Link] = r;
[Link] = [Link] = 2 * r;
}
void draw() { /* dessine le cercle */ }
}
Group, qui est simplement la
Graphical et contient une
cela la classe LinkedList de la
Introduisons enn un troisime type d'objet graphique,
runion de plusieurs objets graphiques. Un groupe hrite de
liste d'objets de la classe
bibliothque Java.
Graphical.
On utilise pour
1.1. Programmation oriente objets
class Group extends Graphical {
LinkedList<Graphical> group;
Group() {
[Link] = new LinkedList<Graphical>();
}
LinkedList est une classe gnrique, paramtre par le type des lments contedans la liste, ici Graphical. On indique ce type avec la notation <Graphical> juste
La classe
nus
aprs le nom de la classe gnrique. (La notion de classe gnrique est explique en dtail
un peu plus loin.) Initialement la liste est vide. On dnit une mthode
add pour ajouter
un objet graphique cette liste.
void add(Graphical g) {
[Link](g);
// + mise jour de x,y,width,height
}
Il reste rednir les mthodes
draw
et
move.
Pour dessiner un groupe, il faut dessiner
tous les lments qui le composent, c'est--dire tous les lments de la liste
g tant dessin en appelant sa propre mthode draw.
[Link] on utilise la construction for de Java :
chaque lment
liste
[Link],
Pour parcourir la
void draw() {
for (Graphical g : [Link])
[Link]();
}
Cette construction aecte successivement la variable
[Link]
g les dirents lments de la liste
et, pour chacun, excute le corps de la boucle. Ici le corps de la boucle est
[Link]. De mme, on rednit la mthode
mthode move de chaque lment, sans oublier
rduit une seule instruction, savoir l'appel
move
dans la classe
Group
en appelant la
de dplacer galement le centre de tout le groupe.
void move(int dx, int dy) {
this.x += dx;
this.y += dy;
for (Graphical g : [Link])
[Link](dx, dy);
}
L'essence de la programmation oriente objet est rsume dans ces deux mthodes. On
appelle la mthode
draw
(ou
move)
sur un objet
de type statique
Graphical,
sans sa-
voir s'il s'agit d'un rectangle, d'un cercle, ou mme d'un autre groupe. En fonction de la
nature de cet objet, le code correspondant sera appel. cet endroit du programme, le
compilateur ne peut pas le connatre. C'est pourquoi on parle d'appel
dynamique
de m-
thode. (D'une manire gnrale, statique dsigne ce qui est connu/fait au moment de
la compilation, et dynamique dsigne ce qui est connu/fait au moment de l'excution.)
10
Chapitre 1. Le langage Java
La classe Object
Une classe qui n'est pas dclare comme hritant d'une autre classe hrite de la classe
Object. Il s'agit d'une classe prdnie dans la bibliothque Java, de son vrai nom
[Link]. Par consquent, toute classe hrite, directement ou indirectement,
de la classe Object. Parmi les mthodes de la classe Object, dont toute classe hrite
donc, on peut citer notamment les trois mthodes
public boolean equals(Object o);
public int hashCode();
public String toString();
dont nous reparlerons par la suite. En particulier, certaines classes peuvent (doivent)
rednir ces mthodes de faon approprie.
1.1.5 Classes abstraites
Dans l'exemple donn plus haut d'une hirarchie de classes pour reprsenter des objets
graphiques, il n'y a jamais lieu de crer d'instance de la classe
Graphical.
Elle nous
sert de type commun tous les objets graphiques, mais elle ne reprsente pas un objet
particulier. C'est ce que l'on appelle une
mot-cl
abstract.
classe abstraite
et on peut l'indiquer avec le
abstract class Graphical {
...
}
new Graphical(). Du coup, plutt que d'crire dans
la classe graphical une mthode draw qui ne fait rien, on peut se contenter de la dclarer
Ainsi il ne sera pas possible d'crire
comme une mthode abstraite.
abstract void draw();
Le compilateur nous imposera alors de la rednir dans les sous-classes de
Graphical,
moins que ces sous-classes soient elles-mmes abstraites.
1.1.6 Classes gnriques
Une classe peut tre paramtre par une ou plusieurs classes. On parle alors de classe
gnrique. L'exemple le plus simple est srement celui d'une classe Pair pour reprsenter
1
une paire forme d'un objet d'une classe A et d'un autre d'une classe B . Il s'agit donc
d'une classe paramtre par les classes A et B. On la dclare ainsi :
class Pair<A, B> {
A et B, sont indiqus entre les symboles < et >. l'intrieur de la classe
A ou B comme tout autre nom de classe. Ainsi, on dclare deux champs
Les paramtres, ici
Pair, on utilise
fst et snd avec
1. D'une faon assez surprenante, une telle classe n'existe pas dans la bibliothque standard Java.
1.1. Programmation oriente objets
11
A fst;
B snd;
et le constructeur naturel avec
Pair(A a, B b) {
[Link] = a;
[Link] = b;
}
(On note qu'on ne rpte pas les paramtres dans le nom du constructeur, car ils sont
identiques ceux de la classe.) De la mme manire, les paramtres peuvent tre utiliss
dans les dclarations et dnitions de mthodes. Ainsi on peut crire ainsi une mthode
renvoyant la premire composante d'une paire, c'est--dire une valeur de type
A getFirst() { return [Link]; }
Pour utiliser une telle classe gnrique, il convient d'instancier
les paramtres formels
A et B par des paramtres eectifs, c'est--dire par deux expressions de type. Ainsi on peut
crire
Pair<Integer, String> p0 = new Pair<Integer, String>(89, "Fibonacci");
pour dclarer une variable
p0
contenant une paire forme d'un entier et d'une chane de
caractres. Une telle dclaration peut tre faite aussi bien dans la classe
Pair qu' l'ext-
rieur, dans une autre classe. Comme on le voit, la syntaxe pour raliser l'instanciation est,
sans surprise, la mme que pour la dclaration. Comme on le voit galement, l'instancia-
new Pair. On note que le premier paramtre a t instanci
par Integer et non pas int. En eet, seule une expression de type dnotant une classe
peut tre utilise pour instancier une classe gnrique, et int ne dsigne pas une classe.
La classe Integer de la bibliothque Java a justement pour rle d'encapsuler un entier
de type int dans un objet. La cration de cet objet est ajoute automatiquement par le
compilateur, ce qui nous permet d'crire 89 au lieu de new Integer(89). La bibliothque
Java contient des classes similaires Boolean, Long, Double, etc.
tion doit tre rpte aprs
Code statique gnrique
Pour crire un code statique gnrique, on doit prciser les paramtres aprs le mot
cl
static,
car ils ne concident pas ncessairement avec ceux de la classe. Ainsi, une
mthode qui change les deux composantes d'une paire s'crit
static<A, B> Pair<B, A> swap(Pair<A, B> p) {
return new Pair<B, A>([Link], [Link]);
}
L encore, on peut crire une telle dclaration aussi bien dans la classe
Pair
qu' l'ext-
rieur, dans une autre classe. Les paramtres d'un code statique ne sont pas ncessairement
les mmes que ceux de la classe gnrique. Par exemple on peut crire
static<C> Pair<C, C> twice(C a) { return new Pair<C, C>(a, a); }
2. On nous pardonnera cet anglicisme.
12
Chapitre 1. Le langage Java
pour renvoyer une paire forme de deux fois le mme objet. Lorsqu'on utilise une mthode
statique gnrique, l'instanciation est infre par le compilateur. Ainsi on crit seulement
Pair<String, Integer> p1 = [Link](p0);
Si cela est ncessaire, on peut donner l'instanciation explicitement, avec la syntaxe un
peu surprenante suivante :
Pair<String, Integer> p1 = Pair.<Integer, String>swap(p0);
1.1.7 Interfaces
contrat entre une fournisseur de
interface. Une interface est un ensemble de
Le langage Java fournit un mcanisme pour raliser un
code et son client. Ce mcanisme s'appelle une
mthodes. Voici par exemple une interface minimale pour une structure de pile contenant
des entiers.
interface
boolean
void
int
}
Stack {
isEmpty()
push(int x)
pop()
Elle dclare trois mthodes
isEmpty, push
et
pop.
Du ct du code client, cette interface
peut tre utilise comme un type. On peut ainsi crire une mthode
sum qui vide une pile
et renvoie la somme de ses lments de la manire suivante :
static int sum(Stack s) {
int r = 0;
while (![Link]()) r += [Link]();
return r;
}
Pour le compilateur, tout se passe comme si
isEmpty, push
et
pop. On peut
Stack.
Stack
tait une classe avec trois mthodes
appeler cette mthode avec toute classe qui dclare im-
plmenter l'interface
Ct fournisseur, justement, cette dclaration se fait l'aide du mot-cl
implements,
de la manire suivante :
class MyIntStack implements Stack {
..
}
Le compilateur va alors exiger la prsence des trois mthodes
les types attendus. Bien entendu, la classe
que celles de l'interface
Stack
MyIntStack
isEmpty, push et pop, avec
peut dclarer d'autres mthodes
et elles seront visibles. Une interface n'inclut pas le ou les
constructeurs. Une classe peut implmenter plusieurs interfaces.
Bien qu'il ne s'agit pas d'hritage, une relation de sous-typage existe qui permet
tout objet d'une classe implmentant l'interface
type
Stack.
Ainsi, on peut crire
Stack
d'tre considr comme tant de
1.2. Modle d'excution
13
Stack s = new MyIntStack();
L'intrt d'une telle dclaration est d'expliciter l'abstraction dont on a besoin. Ici, on
s et la suite du code se moque de savoir qu'il s'agit
MyIntStack. En particulier, on peut choisir de remplacer plus tard
une autre classe qui implmente l'interface Stack et le reste du code
arme j'ai besoin d'une pile
d'une valeur de type
MyIntStack
par
n'aura pas besoin d'tre modi.
Une interface peut tre gnrique. Voici un exemple d'interface gnrique fournie par
la bibliothque Java.
interface Comparable<K> {
int compareTo(K k);
}
On l'utilise pour exiger que les objets d'une classe donne soient comparables entre eux.
Des exemples d'utilisation se trouvent dans les sections 6.3.4, 7.4 12.5 ou encore 13.2.
1.1.8 Rgles de visibilit
Nous avons expliqu plus haut que le qualicatif
private peut tre utilis pour limiter
la visibilit d'un champ. Plus gnralement, il existe quatre niveaux de visibilit dirents
en Java :
private : visibilit limite la classe ;
protected : visibilit limites la classe
et ses sous-classes ;
aucun qualicatif : visibilit limite au paquetage, c'est--dire au dossier dans lequel
la classe est dnie ;
public
: visibilit illimite.
Ces qualicatifs de visibilit s'appliquent aussi bien aux champs qu'aux mthodes et aux
constructeurs. Pour des raisons de simplicit, ce polycopi omet le qualicatif
public
la
plupart du temps ; en pratique, il faudrait l'ajouter toute classe ou mthode d'intrt
gnral.
1.2 Modle d'excution
Pour utiliser un langage de programmation correctement, et ecacement, il convient
d'en comprendre le modle d'excution, c'est--dire la faon dont sont reprsentes les
valeurs qu'il manipule et le cot de ses direntes oprations, du moins un certain
niveau de dtails. Cette section dcrit quelques lments du modle d'excution de Java.
1.2.1 Arithmtique des ordinateurs
Le langage Java fournit plusieurs types numriques primitifs, savoir cinq types d'entiers (byte,
et
double).
short, char, int et long) et deux types de nombres virgule ottante (float
14
Chapitre 1. Le langage Java
Entiers.
Un entier est reprsent en base 2, sur
n chires appels bits . Ces chires sont
conventionnellement numrots de droite gauche :
bn1
b0 est
n vaut
bn2
bit de poids faible
b1
...
et le bit
b0
bn1
Le bit
appel le
le
Java,
8, 16, 32 ou 64. Par la suite, on utilisera la
bit de poids fort.
notation 1010102
Selon le type
pour dnoter
une suite de bits.
L'interprtation la plus simple de ces
bits est celle d'un entier
non sign
en base 2,
dont la valeur est donc
n1
X
bi 2i .
i=0
Les valeurs possibles vont de 0, c'est--dire
char de Java, qui est
216 1 = 65535.
reprsenter un entier sign,
le cas du type
00...002 ,
2n 1,
c'est--dire
11...112 .
C'est
un entier non sign de 16 bits. Ses valeurs possibles
vont donc de 0
Pour
on interprte le bit de poids fort
bn1
comme un bit
de signe, la valeur 0 dsignant un entier positif ou nul et la valeur 1 un entier strictement
n1
complment
ngatif. Plutt que de simplement mettre un signe devant un entier reprsent par les
bits restants, les ordinateurs utilisent une reprsentation plus subtile appele
deux .
Elle consiste interprter les
bits comme la valeur suivante :
n1
bn1 2
n2
X
bi 2i .
i=0
Les valeurs possibles s'tendent donc de
-dire
011...1112 .
2n1 ,
c'est--dire
100...0002 ,
2n1 1,
c'est-
On notera la dissymtrie de cette reprsentation, avec une valeur de
plus gauche de 0 qu' droite. En revanche, il n'y a qu'une seule reprsentation de 0,
savoir
00...002 .
En Java, les types
byte, short, int
et
long
dsignent des entiers signs,
sur respectivement 8, 16, 32 et 64 bits. Les plages de valeurs de ces types sont donc
7
7
15
15
31
31
63
63
respectivement 2 ..2 1, 2 ..2 1, 2 ..2 1 et 2 ..2 1.
Outre les oprations arithmtiques lmentaires, le langage Java fournit galement
des oprations permettant de manipuler directement la reprsentation binaire d'un entier,
n bits. L'opration est la ngation logique,
&, | et ^ sont respectivement le ET, le OU
c'est--dire d'un entier vu simplement comme
qui change les 0 et les 1, et les oprations
et le OU exclusif appliqus bit bit aux bits de deux entiers. Il existe galement des
oprations de
dcalage
des bits. L'opration
<< k
est un dcalage logique gauche, qui
k zros de poids faible. De mme, l'opration >>> k est un dcalage logique droite,
k zros de poids fort. Enn, l'opration >> k est un dcalage arithmtique
droite, qui rplique le bit de signe k fois. Ces oprations sont utilises dans le chapitre 11
insre
qui insre
pour reprsenter des ensembles de petite cardinalit l'aide d'entiers.
Il est important de signaler qu'un calcul arithmtique peut provoquer un
de capacit
et que ce dernier n'est pas signal, ni par le compilateur (qui ne pourrait pas
le faire de manire gnrale) ni l'excution. Ainsi, le rsultat de
1410065408. Le rsultat peut mme
200000 * 100000 est -1474836480.
est
dbordement
100000 * 100000
tre du mauvais signe. Ainsi le rsultat de
1.2. Modle d'excution
15
double
float
long
int
char short
byte
Figure 1.1 Conversions automatiques entre les types numriques de Java.
Nombres virgule ottante.
Les ordinateurs fournissent galement des nombres
nombres virgule ottante ou plus simplement ottants. Un tel nombre
est galement cod sur n bits, dont certains sont interprts comme un entier sign m
appel mantisse et d'autres comme un autre entier sign e appel exposant. Le nombre
e
ottant reprsent est alors m 2 .
En Java, le type float dsigne un ottant reprsent sur 32 bits. Ses valeurs s'tendent
38
38
45
de 3,4 10
3,4 10 , le plus petit ottant positif reprsentable tant 1,4 10
. Le
type double dsigne un ottant reprsent sur 64 bits. Ses valeurs s'tendent de 1,8
10308 1,8 10308 , le plus petit ottant positif reprsentable tant 4,9 10324 .
dcimaux, appels
Ce cours n'entre pas dans les dtails de cette reprsentation, qui est complexe, mais
plusieurs choses importantes se doivent d'tre signales. En premier lieu, il faut avoir
conscience que la plupart des nombres rels, et mme la plupart des nombres dcimaux,
ne sont pas reprsentables par un ottant. En consquence, les rsultats des calculs sont
arrondis et il faut en tenir compte dans les programmes que l'on crit. Ainsi, le nombre
n'est pas reprsentable et un simple calcul comme
qui n'est pas gal
173,2.
1732 0,1
0,1
donne en ralit un ottant
En particulier, une condition dans un programme ne doit
pas tester en gnral qu'un nombre ottant est nul, mais plutt qu'il est infrieur une
9
borne donne, par exemple 10 . En revanche, si les calculs sont arrondis, ils le sont d'une
manire qui est spcie par un standard, savoir le standard IEEE 754 [ ]. En particulier,
ce standard spcie que le rsultat d'une opration, par exemple la multiplication
0,1
1732
ci-dessus, doit tre le ottant le plus proche du rsultat exact.
Conversions automatiques.
Les valeurs des dirents types numriques de Java su-
bissent des conversions automatiques d'un type vers un autre dans certaines circonstances.
Ainsi, si une mthode doit renvoyer un entier de type
est de type
type
int.
char.
int,
return c o c
long un entier de
on peut crire
De mme on peut aecter une variable de type
La gure 1.1 illustre les direntes conversions automatiques de Java, chaque
trait de bas en haut tant vu comme une conversion et la conversion tant transitive. Les
conversions entre les types entiers se font sans perte. En revanche, les conversions vers les
types ottants peuvent impliquer un arrondi (on peut s'en convaincre par un argument
combinatoire, par exemple en constatant que le type
type
float).
long
contient plus de valeurs que le
Lorsque la conversion n'est pas possible, le compilateur Java indique une erreur. Ainsi,
on ne peut pas aecter une valeur de type
renvoyer une valeur de type
float
int
une variable de type
char
ou encore
dans une mthode qui doit renvoyer un entier.
16
Chapitre 1. Le langage Java
Lors d'un calcul arithmtique, Java utilise le plus petit type mme de recevoir le
rsultat du calcul, en eectuant une promotion de certaines des oprandes si ncessaire.
Ainsi, si on ajoute une valeur de type
type
int.
char
Plus subtilement, l'addition d'un
et une valeur de type
char
et d'un
short
int,
le rsultat sera de
sera de type
int.
1.2.2 Mmoire
Cette section explique comment la mmoire est structure et notamment comment les
objets y sont reprsents. Reprenons l'exemple de la classe
Polar de la section prcdente
class Polar {
double rho, theta;
}
et construisons un nouvel objet de la classe
Polar
dans une variable locale
Polar p = new Polar();
On a une situation que l'on peut schmatiser ainsi :
Polar
rho 0.0
theta 0.0
Les petites botes correspondent des zones de la mmoire. Les noms ct de ces botes
(p,
rho, theta) n'ont pas de relle incarnation en mmoire. En tant que noms, ils n'existent
que dans le programme source. Une fois celui-ci compil et excut, ces noms sont devenus
des
adresses
dsignant des zones de la mmoire, qui ne sont rien d'autre que des entiers.
Ainsi la bote
contient en ralit une adresse (par exemple 1381270477) laquelle on
trouve les petites botes dessines droite. Dans une est mentionne la classe de l'objet,
Polar. L encore, il ne s'agit pas en mmoire d'un nom, mais d'une reprsentation plus
bas niveau (en ralit une autre adresse mmoire vers une description de la classe Polar).
Dans d'autres botes on trouve les valeurs des deux champs rho et theta. L encore on a
ici
explicit le nom ct de chaque champ mais ce nom n'est pas reprsent en mmoire. Le
compilateur sait qu'il a rang le champ
rho une certaine distance de l'adresse de l'objet
rho.
et c'est tout ce dont il a besoin pour retrouver la valeur du champ
Notre schma est donc une simplication de l'tat de la mmoire, les zones mmoires
apparaissent comme des cases (les variables portent un nom) et les adresses apparaissent
comme des ches qui pointent vers les cases, alors qu'en ralit il n'y a rien d'autre que
des entiers rangs dans la mmoire des adresses qui sont elles-mmes des entiers. Par
ailleurs, notre schma est aussi une simplication car la reprsentation en mmoire d'un
objet Java est plus complexe : elle contient aussi des informations sur l'tat de l'objet.
Mais ceci ne nous intresse pas ici. Dans ce polycopi, on s'autorisera parfois mme ne
pas crire la classe dans la reprsentation d'un objet quand celle-ci est claire d'aprs le
contexte.
Tableaux
Un tableau est un objet un peu particulier, puisque ses direntes composantes ne
sont pas dsignes par des noms de champs mais par des indices entiers. Nanmoins l'ide
1.2. Modle d'excution
17
reste la mme : un tableau occupe une zone contigu de mmoire, dont une petite partie
dcrit le type du tableau et sa longueur et le reste contient les dirents lments. Dans
la suite, on reprsentera un tableau de manire simplie, avec uniquement ses lments
prsents horizontalement. Ainsi un tableau contient les trois entiers 1, 2 et 3 sera tout
simplement reprsent par
1 2 3
Allocation et libration
Comme expliqu ci-dessus, l'utilisation de la construction
allocation
new
de Java conduit une
de mmoire. Celle-ci se fait dans la partie de la mmoire appele le
tas
l'autre tant la pile, dcrite dans le paragraphe suivant. Si la mmoire vient s'puiser,
l'exception
OutOfMemoryError est leve. Au fur et mesure de l'excution du programme,
de la mmoire peut tre rcupre, lorsque les objets correspondants ne sont plus utiliss. Cette libration de mmoire n'est pas la charge du programmeur (contrairement
d'autres langages comme C++) : elle est ralise automatiquement par le
lector
Garbage Col-
(ou GC). Celui-ci libre la mmoire alloue pour un objet lorsque cet objet ne peut
plus tre rfrenc partir des variables du programme ou d'autres objets pouvant encore
tre rfrencs. La libration de mmoire est eectue incrmentalement, c'est--dire par
petites tapes, au fur et mesure de l'excution du programme. En premire approximation, on peut considrer que le cot de la libration de mmoire est uniformment rparti
sur l'ensemble de l'excution. En particulier, on peut s'autoriser penser que le cot
d'une expression
new se limite celui du code du constructeur, c'est--dire que le cot de
l'allocation proprement dite est constant.
Pile d'appels
Dans la plupart des langages de programmation, et en Java en particulier, les appels de
fonctions/mthodes obissent une logique dernier appel, premier sorti , c'est--dire
que, si une mthode
f appelle une mthode g, l'appel g terminera avant l'appel f. Cette
proprit permet au compilateur d'organiser les donnes locales un appel de fonction
(paramtres et variables locales) sur une pile. Illustrons ce principe avec l'exemple d'une
mthode rcursive
calculant la factorielle de
n.
static int fact(int n) {
if (n == 0) return 1;
return n * fact(n-1);
}
Si on value l'expression
fact(4),
alors le paramtre formel
matrialis quelque part en mmoire et recevra la valeur
va conduire l'valuation de
fact
fact(3).
4.
fact sera
de fact(4)
de la mthode
Puis l'valuation
n de la mthode
3. On comprend qu'on ne peut pas
fact(4), sinon la valeur 4 sera perdue,
De nouveau, le paramtre formel
doit tre matrialis pour recevoir la valeur
rutiliser le mme emplacement que pour l'appel
alors mme qu'il nous reste eectuer une multiplication par cette valeur l'issue de
fact(3). On a donc une seconde matrialisation en mmoire du paramtre n, et
ainsi de suite. Lorsqu'on en est au calcul de fact(2), on se retrouve donc dans la situation
l'appel
suivante :
3. La pile d'appels n'est pas lie la rcursivit mais la notion d'appels imbriqus, mais une fonction
rcursive conduit naturellement des appels imbriqus.
18
Chapitre 1. Le langage Java
fact(4) n
fact(3) n
fact(2) n
.
.
.
On visualise bien une structure de pile (qui crot ici vers le bas). Lorsqu'on parvient
fact(0), on atteint enn une instruction return, qui conclut l'appel
n contenant 0 est alors dpile et on revient l'appel fact(1).
On peut alors eectuer la multiplication 1 * 1 puis c'est l'appel fact(1) qui est termin
et la variable n contenant 1 qui est dpile. Et ainsi de suite jusqu'au rsultat nal.
La pile a une capacit limite. Si elle vient s'puiser, l'exception StackOverflowError
nalement l'appel
fact(0).
La variable
est leve. Par dfaut, la taille de pile est relativement petite, de l'ordre de 1 Mo, ce qui
correspond environ 10 000 appels imbriqus (bien entendu, cela dpend de l'occupation
sur la pile de chaque appel de fonction). On peut modier la taille de la pile avec l'option
-Xss
de la machine virtuelle Java. Ainsi
java -Xss100m Test
excute le programme
Test
avec 100 Mo de pile.
1.2.3 Valeurs
Il y a en Java deux grandes catgories de valeurs : les
valeurs primitives
et les
objets.
La distinction est en fait technique, elle tient la faon dont ces valeurs sont traites par
la machine, ou plus exactement sont ranges dans la mmoire. Une valeur primitive se
sut elle-mme ; il s'agit d'un entier, d'un caractre, ou encore d'un boolen. La valeur
d'un objet est une
adresse , dsignant une zone de la mmoire. On parle aussi de pointeur .
crivons par exemple
int x = 1 ;
int[] t = {1, 2, 3} ;
Les variables
et
sont deux cases, qui contiennent chacune une valeur, la premire
valeur tant primitive et la seconde un pointeur. Un schma rsume la situation :
x 1
t
1 2 3
En particulier, la valeur de la variable
est un pointeur vers l'objet en mmoire repr-
sentant le tableau, c'est--dire contenant ses lments. Si
et
sont deux variables, la
y = x se traduit par une copie de la valeur contenue dans la variable x dans la
y , que cette valeur soit un pointeur ou non. Ainsi, si on poursuit le code ci-dessus
construction
variable
avec les deux instructions suivantes
int y = x ;
int[] u = t ;
1.2. Modle d'excution
19
alors on obtient maintenant l'tat mmoire suivant :
y 1
x 1
1 2 3
En particulier, les deux variables
alias .
sont des
et
pointent vers le mme tableau. On dit que ce
On peut s'en convaincre en modiant un lment de
la modication s'observe galement dans
t.
Si par exemple on excute
et en vriant que
u[1] = 42;
alors
on obtient
x 1
y 1
1 42 3
ce que l'on peut observer facilement, par exemple en achant la valeur de
t et u ne sont pas lis jamais. Si on aecte t un nouveau
t = new int[] {4, 5};, alors on a la situation suivante
dant,
avec
x 1
y 1
Cepen-
tableau, par exemple
t
4 5
t[1].
1 42 3
dsigne toujours le mme tableau qu'auparavant.
null. Nous pouvons l'employer partout
null par le symbole .
possible de le drfrencer c'est--dire
Il existe un pointeur qui ne pointe nulle part :
o un pointeur est attendu. Dans les schmas nous reprsentons
Puisque
null
ne pointe nulle part il n'est pas
d'aller voir o il pointe. Toute tentative se traduit par une erreur l'excution, savoir
le dclenchement de l'exception
NullPointerException.
Toute valeur qui n'est pas initialise (variable, champ, lment de tableau) reoit une
valeur par dfaut. Le langage spcie qu'il s'agit de 0 dans le cas d'un entier, d'un caractre
ou d'un ottant,
false
dans le cas d'un boolen, et
null
dans les autres cas (objet ou
tableau).
galit des valeurs
L'oprateur d'galit
==
de Java teste l'galit de deux valeurs. De mme, l'opra-
teur != teste leur dirence. Pour des valeurs primitives, cela concide avec l'galit mathmatique. En revanche, pour deux objets, c'est--dire deux valeurs qui sont des pointeurs,
il s'agit tout simplement de l'galit de ces deux pointeurs. Autrement dit, le programme
int[] t = {1, 2, 3} ;
int[] u = t ;
int[] v = {1, 2, 3} ;
[Link]("t==u : " + (t == u) + ", t==v : " + (t == v)) ;
t==u : true, t==v : false. Les pointeurs t et u sont gaux parce qu'ils pointent
le mme objet. Les pointeurs t et v, qui pointent vers des objets distincts, sont
ache
vers
distincts. Cela peut se comprendre si on revient aux tats mmoire simplis.
20
Chapitre 1. Le langage Java
u
1 2 3
On dit parfois que
1 2 3
== est l'galit physique. L'galit physique donne parfois des rsultats
surprenants. Ainsi le programme suivant
String t = "coucou" ;
String u = "coucou" ;
String v = "cou" ;
String w = v + v ;
[Link]("t==u : " + (t == u) + ", t==w : " + (t == w)) ;
t==u : true, t==w : false, ce qui rvle que les chanes (objets) rfrencs par
u sont exactement les mmes (le compilateur les a partages), tandis que w est une
ache
et
autre chane.
"coucou"
w
"coucou"
La plupart du temps, un programme a besoin de savoir si deux chanes ont exactement
les mmes caractres et non si elles occupent la mme zone de mmoire. Il en rsulte
principalement qu'il ne faut pas tester l'galit des chanes, et des objets en gnral le
plus souvent, par
==. Dans le cas des chanes, il existe une mthode equals spcialise qui
equals est l'galit
compare les chanes caractre par caractre. On dit que la mthode
structurelle
des chanes. De manire gnrale, c'est la charge du programmeur Java que
d'crire une mthode pour raliser l'galit structurelle, si besoin est. Un exemple d'galit
structurelle est donn dans la section 5.3.
Passage par valeur
En Java, le mode de passage est
par valeur.
Cela signie que, lorsqu'une mthode
est appele, les valeurs des arguments eectifs de l'appel sont
copies
vers de nouveaux
emplacements mmoire. Mais il faut bien garder l'esprit qu'une telle valeur est soit une
valeur primitive, soit un pointeur vers une zone de la mmoire, comme expliqu ci-dessus.
En particulier, dans ce second cas, seul le
mthode
pointeur
suivante
static void f(int a, int[] b) {
a = 2;
b[2] = 7;
}
appele dans le contexte suivant :
int x = 1;
int[] y = {1, 2, 3};
f(x, y);
Juste avant l'appel
on a la situation suivante :
est copi. Considrons par exemple la
1.2. Modle d'excution
21
y
x 1
Juste aprs l'appel
on a deux nouvelles variables
qui ont reu respectivement les valeurs de
x 1
a 1
En particulier, les deux variables
aectations
a = 2
Aprs l'appel
f,
et
b[2] = 7
et
et
et
(les arguments formels de
1 2 3
sont des alias pour le mme tableau. Les deux
conduisent donc la situation suivante :
x 1
a 2
1 2 7
a et b sont dtruites, et on se retrouve donc dans
contenu du tableau y a t modi, mais pas celui de
la
la
x 1
1 2 7
Cet exemple utilise un tableau, mais la situation serait la mme si
la mthode
f),
les variables
situation nale suivante, o le
variable
1 2 3
f.
tait un objet et si
modiait un champ de cet objet : la modication persisterait aprs l'appel
Plus subtilement encore, si on remplace l'aectation
int[] {4, 5, 6},
b[2] = 7
dans
par
on se retrouve dans la situation suivante la n du code de
x 1
1 2 3
a 2
4 5 6
b = new
f:
Aprs l'appel, on se retrouve donc exactement dans la situation de dpart, c'est--dire
x 1
En particulier, le tableau
{4, 5, 6}
1 2 3
n'est plus nulle part rfrenc et sera donc rcupr
par le GC.
Un objet allou dans une mthode n'est pas systmatiquement perdu pour autant. En
eet, il peut tre renvoy comme rsultat (ou stock dans une autre structure de donnes).
Si on considre par exemple la mthode suivante
static int[] g(int a) {
int[] b = {a, a, a};
return b;
}
qui alloue un tableau dans une variable locale
b,
alors l'appel
g(1)
va conduire la
situation suivante
a 1
et c'est la
variables
1 1 1
valeur de b qui est renvoye, c'est--dire le pointeur vers le tableau. Bien que les
a et b sont dtruites, le tableau survivra (si la valeur renvoye par g est utilise
par la suite, bien entendu).
22
Chapitre 1. Le langage Java
Exercice 1.1.
que
1,
Expliquer pourquoi le programme suivant ne peut acher autre chose
quelle que soit la dnition de la mthode
int x = 1;
f(x);
[Link](x);
Notions de complexit
L'objet de la thorie de la complexit est de mesurer les ressources principalement
le temps et la mmoire utilises par un programme ou ncessaires la rsolution
d'un problme. Ce chapitre vise en donner les premires bases, en discutant quelques
algorithmes et leur ecacit. D'autres exemples sont donns dans les chapitres suivants,
notamment dans le chapitre 12 consacr aux tris.
2.1 Complexit d'algorithmes et complexit de problmes
2.1.1 La notion d'algorithme
La thorie de la
calculabilit,
ne des travaux fondateurs d'Emil Post et Alan Turing,
ore des outils pour formaliser la notion d'algorithme de faon trs gnrale.
Elle s'intresse en fait essentiellement discuter les problmes qui sont rsolubles
informatiquement, c'est--dire distinguer les problmes
rsolus informatiquement) des problmes
indcidables
dcidables
(qui peuvent tre
(qui ne peuvent avoir de solution
informatique). La calculabilit est trs proche de la logique mathmatique et de la thorie
de la preuve : l'existence de problmes qui n'admettent pas de solution informatique est
trs proche de l'existence de thormes vrais mais qui ne sont pas dmontrables.
La thorie de la
complexit
se focalise sur les problmes dcidables, et s'intresse aux
ressources (temps, mmoire, etc.) ncessaires la rsolution de ces problmes. C'est typiquement ce dont on a besoin pour mesurer l'ecacit des algorithmes considrs dans
ce cours : on considre des problmes qui admettent une solution, mais pour lesquels on
cherche une solution ecace.
Ces thories permettent de formaliser proprement la notion d'algorithme, en complte
gnralit, en faisant abstraction du langage utilis, mais cela dpasserait compltement
l'ambition de ce polycopi. Dans ce polycopi, on va prendre la notion suivante d'algorithme : un algorithme
est un programme Java qui prend en entre une donne
et
A(d). Cela peut par exemple tre un algorithme de tri, qui
d d'entiers, et produit en sortie cette mme liste trie A(d). Cela
peut aussi tre par exemple un algorithme qui prend en entre deux listes, i.e. un couple
d constitu de deux listes, et renvoie en sortie leur concatnation A(d).
produit en sortie un rsultat
prend en entre une liste
24
Chapitre 2. Notions de complexit
2.1.2 La notion de ressource lmentaire
On mesure toujours l'ecacit, c'est--dire la complexit, d'un algorithme en terme
d'une mesure lmentaire
valeur entire : cela peut tre le nombre d'instructions
eectues, la taille de la mmoire utilise, le nombre de comparaisons eectues, ou toute
autre mesure.
d, on sache clairement associer l'alA sur l'entre d, la valeur de cette mesure, note (A,d) : par exemple, pour un
algorithme de tri, si la mesure lmentaire est le nombre de comparaisons eectues,
(A,d) est le nombre de comparaisons eectues sur l'entre d (une liste d'entiers) par
l'algorithme de tri A pour produire le rsultat A(d) (cette liste d'entiers trie).
Il est clair que (A,d) est une fonction de l'entre d. La qualit d'un algorithme A n'est
donc pas un critre absolu, mais une fonction quantitative (A,.) des donnes d'entre
Il faut simplement qu'tant donne une entre
gorithme
vers les entiers.
2.1.3 Complexit d'un algorithme au pire cas
En pratique, pour pouvoir apprhender cette fonction, on cherche souvent valuer
taille : il y a souvent une fonction taille
d, un entier taille(d), qui correspond un paramtre
cette complexit pour les entres d'une certaine
qui associe chaque donne d'entre
naturel. Par exemple, cette fonction peut tre celle qui compte le nombre d'lments dans
la liste pour un algorithme de tri, la taille d'une matrice pour le calcul du dterminant,
la somme des longueurs des listes pour un algorithme de concatnation.
Pour passer d'une fonction des donnes vers les entiers, une fonction des entiers (les
tailles) vers les entiers, on considre alors la complexit
de l'algorithme
sur les entres de taille
(A,n) =
au pire cas
: la complexit
(A,n)
est dnie par
max
d | taille(d)=n
(A,d).
(A,n) est la complexit la pire sur les donnes de taille
parle de complexit d'algorithme en informatique, il s'agit de
Autrement dit, la complexit
n.
Par dfaut, lorsqu'on
complexit au pire cas, comme ci-dessus.
Si l'on ne sait pas plus sur les donnes, on ne peut gure faire plus que d'avoir cette
vision pessimiste des choses : cela revient valuer la complexit dans le pire des cas (le
meilleur des cas n'a pas souvent un sens profond, et dans ce contexte le pessimisme est
de loin plus signicatif ).
Exemple.
Considrons le problme du calcul du maximum : on se donne en entre une
liste d'entiers naturels
M = max1in ei ,
e1 ,e2 , ,en ,
avec
n 1,
et on cherche dterminer en sortie
c'est--dire le plus grand de ces entiers. Si l'entre est range dans un
tableau, la fonction Java suivante rsout le problme.
static int max(int T[]) {
int M = T[0];
for (int i = 1; i < [Link]; i++)
if (M < T[i]) M = T[i];
return M;
}
2.1. Complexit d'algorithmes et complexit de problmes
Si notre mesure lmentaire
tableau
T,
25
correspond au nombre de comparaisons entre lments du
nous en faisons autant que d'itrations de la boucle, c'est--dire exactement
n1. Nous avons donc (A,d) = n1 pour cet algorithme A. Ce nombre est indpendant
d de taille n, et donc (A,n) = n 1.
de la donne
2.1.4 Complexit moyenne d'un algorithme
Pour pouvoir en dire plus, il faut en savoir plus sur les donnes. Par exemple, qu'elles
sont distribues selon une certaine loi de probabilit. Dans ce cas, on peut alors parler de
complexit
de taille
en moyenne
(A,n) = E[(A,d) | d
o
(A,n) de l'algorithme A sur les entres
: la complexit moyenne
est dnie par
entre avec
taille(d) = n],
dsigne l'esprance (la moyenne). Si l'on prfre,
(A,n) =
(d)(A,d),
d | taille(d)=n
o
(d)
dsigne la probabilit d'avoir la donne
parmi toutes les donnes de taille
n.
En pratique, le pire cas est rarement atteint et l'analyse en moyenne peut sembler plus
sduisante. Mais il est important de comprendre que l'on ne peut pas parler de moyenne
sans loi de probabilit (sans distribution) sur les entres, ce qui est souvent trs dlicat
estimer en pratique. Comment anticiper par exemple les matrices qui seront donnes
un algorithme de calcul de dterminant ? On fait parfois l'hypothse que les donnes
sont quiprobables (lorsque cela a un sens, comme lorsqu'on trie
nombres entre
et
et o l'on peut supposer que les permutations en entre sont quiprobables), mais cela
est bien souvent totalement arbitraire, et pas rellement justiable. Enn, les calculs de
complexit en moyenne sont plus dlicats mettre en uvre.
Exemple.
Considrons le problme de la recherche d'un entier v parmi n entiers donns
e1 ,e2 , ,en , avec n 1. Plus prcisment, on cherche dterminer s'il existe un indice
1 i n avec ei = v . En supposant les entiers donns dans un tableau, l'algorithme
suivant rsout le problme.
static boolean contains(int[] T, int v) {
for (int i = 0; i < [Link]; i++)
if (T[i] == v) return true;
return false;
}
Sa complexit au pire cas en nombre d'instructions lmentaires est linaire en
la boucle est eectue
fois dans le pire cas. Supposons que les lments de
n, puisque
T sont des
nombres entiers distribus de faon quiprobable entre 1 et k (une constante). Il y a donc
k n tableaux. Parmi ceux-ci, (k 1)n ne contiennent pas v et, dans ce cas, l'algorithme
procde exactement
itrations. Dans le cas contraire, l'entier est dans le tableau et sa
premire occurrence est alors
avec une probabilit de
(k 1)i1
.
ki
26
Chapitre 2. Notions de complexit
Il faut alors procder
itrations. Au total, nous avons donc une complexit moyenne
de
X (k 1)i1
(k 1)n
C=
n
+
i.
kn
ki
i=1
Or
x,
n
X
ixi1 =
i=1
1 + xn (nx n 1)
(1 x)2
(il sut pour tablir ce rsultat de driver l'identit
Pn
i=0
xi =
1xn+1
) et donc
1x
n
(k 1)n
1
(k 1)n
n
C=n
.
+k 1
(1 + ) = k 1 1
kn
kn
k
k
k est trs grand devant n (on eectue par exemple une recherche dans un tableau
n = 1000 lments dont les valeurs sont parmi les k = 232 valeurs possibles de type
int), alors C n. La complexit moyenne est donc linaire en la taille du tableau, ce qui
ne nous surprend pas. Lorsqu'en revanche k est petit devant n (on eectue par exemple
une recherche parmi peu de valeurs possibles), alors C k . La complexit moyenne est
Lorsque
de
donc linaire en le nombre de valeurs, ce qui ne nous surprend pas non plus.
2.1.5 Complexit d'un problme
On peut aussi parler de la
complexit d'un problme
: cela permet de discuter de l'op-
timalit ou non d'un algorithme pour rsoudre un problme donn. On xe un problme
A qui rsout ce problme
est un algorithme qui rpond la spcication du problme P : pour chaque donne d, il
produit la rponse correcte A(d). La complexit du problme P sur les entres de taille n
: par exemple celui de trier une liste d'entiers. Un algorithme
est alors dnie par
(P,n) =
inf
max
A algorithme qui rsout P
d entre avec taille(d)=n
Autrement dit, on ne fait plus seulement varier les entres de taille
n,
(A,d).
mais aussi l'algo-
rithme. On considre le meilleur algorithme qui rsout le problme, le meilleur tant celui
avec la meilleure complexit au sens de la dnition prcdente, c'est--dire au pire cas.
C'est donc la complexit du meilleur algorithme au pire cas.
L'intrt de cette dnition est le suivant : si un algorithme
(P,n), i.e.
est tel que
(A,n) = (P,n)
pour tout
n,
possde la complexit
alors cet algorithme est clairement
optimal. Tout autre algorithme est moins performant, par dnition. Cela permet donc
de prouver qu'un algorithme est optimal.
Il y a une subtilit cache dans la dnition ci-dessus. Elle considre la complexit
du problme
P sur les entres de taille n.
En pratique, cependant, on crit rarement
un algorithme pour une taille d'entres xe, mais plutt un algorithme qui fonctionne
quelle que soit la taille des entres. Plus subtilement encore, cet algorithme peut ou non
procder diremment selon la valeur de
n.
Une dnition rigoureuse de la complexit
d'un problme se doit de faire cette distinction ; on parle alors de complexit uniforme et
non-uniforme. Mais ceci dpasse largement le cadre de ce cours.
2.2. Complexits asymptotiques
Exemple.
27
Reprenons l'exemple de la mthode
max
donn plus haut. On peut se poser
la question de savoir s'il est possible de faire moins de
n1
comparaisons. La rponse
est non ; dit autrement, cet algorithme est optimal en nombre de comparaisons. En effet, considrons la classe
maximum de
des algorithmes qui rsolvent le problme de la recherche du
lments en utilisant comme critre de dcision les comparaisons entre
lments. Commenons par noncer la proprit suivante : tout algorithme
de
est tel
que tout lment autre que le maximum est compar au moins une fois avec un lment
i0 le rang du maximum M renvoy par l'algorithme
T = e1 ,e2 , . . . ,en , c'est--dire ei0 = M = max1in ei . Raisonnons par
l'absurde : soit j0 6= i0 tel que ej0 ne soit pas compar avec un lment plus grand que lui.
L'lment ej0 n'a donc pas t compar avec ei0 le maximum. Considrons alors le tableau
T 0 = e1 ,e2 , . . . ,ej0 1 ,M + 1,ej0 +1 , . . . ,en obtenu partir de T en remplaant l'lment d'indice j0 par M + 1. L'algorithme A eectuera exactement les mmes comparaisons sur T
0
0
0
0
et T , sans comparer T [j0 ] avec T [i0 ] et renverra donc T [i0 ], ce qui est incorrect. D'o
qui lui est plus grand. En eet, soit
sur un tableau
une contradiction, qui prouve la proprit.
Il dcoule de la proprit qu'il n'est pas possible de dterminer le maximum de
n1
lments en moins de
du problme
comparaisons entre lments. Autrement dit, la complexit
du calcul du maximum sur les entres de taille
L'algorithme prcdent fonctionnant en
n1
est
(P,n) = n 1.
telles comparaisons, il est optimal pour
cette mesure de complexit.
2.2 Complexits asymptotiques
En informatique, on s'intresse le plus souvent l'ordre de grandeur (l'asymptotique)
des complexits quand la taille
des entres devient trs grande.
2.2.1 Ordres de grandeur
Dans le cas o la mesure lmentaire
est le nombre d'instructions lmentaires,
n, n log2 n,
croissantes, sur un processeur capable
intressons-nous au temps correspondant des algorithmes de complexit
n2 , n3 , ( 32 )n , 2n
et
n!
pour des entres de taille
d'excuter un million d'instructions lmentaires par seconde. Nous notons dans le
25
tableau suivant ds que le temps dpasse 10
annes (ce tableau est emprunt [4]).
n
n = 10
<1
n = 30
<1
n = 50
<1
n = 100
<1
n = 1000
<1
n = 10000
<1
n = 100000 < 1
n = 1000000 1s
Complexit
s
s
s
s
s
s
s
n log2 n
<1s
<1s
<1s
<1s
<1s
<1s
2s
20s
n2
<1s
<1s
<1s
<1s
1s
2 min
3 heures
12 jours
n3
<1s
<1s
<1s
1s
18 min
12 jours
32 ans
31 710 ans
( 32 )n
<1s
<1s
11 min
12,9 ans
2n
<1s
18 min
36 ans
1017 ans
n!
4s
1025
ans
28
Chapitre 2. Notions de complexit
2.2.2 Conventions
Ce type d'exprience invite considrer qu'une complexit en
( 32 )n , 2n
ou
n!
ne peut
pas tre considre comme raisonnable. On peut discuter de savoir si une complexit en
n158 est en pratique raisonnable, mais depuis les annes 1960 environ, la convention en
informatique est que oui : toute complexit borne par un polynme en
est considre
comme raisonnable. Si on prfre, cela revient dire qu'une complexit est raisonnable
d
ds qu'il existe des constantes c, d, et n0 telles que la complexit est borne par cn , pour
n > n0 . Des complexits non raisonnables sont par exemple nlog n , ( 23 )n , 2n et n!.
Cela ore beaucoup d'avantages : on peut raisonner un temps (ou un espace
mmoire, ou un nombre de comparaisons) polynomial prs. Cela vite par exemple de
prciser de faon trop ne le codage, par exemple comment sont codes les matrices pour
un algorithme de calcul de dterminant : passer du codage d'une matrice par des listes
un codage par tableau se fait en temps polynomial et rciproquement.
D'autre part, on raisonne souvent une constante multiplicative prs. On considre que
deux complexits qui ne dirent que par une constante multiplicative sont quivalentes :
3
3
par exemple 9n et 67n sont considrs comme quivalents. Ces conventions expliquent
que l'on parle souvent de complexit en temps de l'algorithme sans prciser nement la
mesure de ressource lmentaire
Dans ce polycopi, par exemple, on ne cherchera pas
prciser le temps de chaque instruction lmentaire Java. Dit autrement, on suppose
dans ce polycopi qu'une opration arithmtique ou une aectation entre deux variables
Java se fait en temps constant (unitaire) : cela s'appelle le modle
RAM.
2.2.3 Notation de Landau
En pratique, on discute d'asymptotique de complexits via la notation
Soient
et
de Landau.
deux fonctions dnies des entiers naturels vers les entiers naturels (comme
les fonctions de complexit d'algorithme ou de problmes). On dit que
f (n) = O(g(n))
si et seulement si il existe deux constantes positives
n0
et
telles que
n n0 ,f (n) Bg(n).
Ceci signie que
ne crot pas plus vite que
g.
En particulier
Par exemple, un algorithme qui fonctionne en temps
O(1)
O(1)
signie constant(e).
est un algorithme dont le
temps d'excution est constant et ne dpend pas de la taille des donnes. C'est donc un
ensemble constant d'oprations lmentaires (exemple : l'addition de deux entiers avec les
conventions donnes plus haut).
On dit d'un algorithme qu'il est linaire s'il utilise
O(n) oprations lmentaires. Il est
polynomial s'il existe une constante a telle que le nombre total d'oprations lmentaires
a
est O(n ) : c'est la notion de raisonnable introduite plus haut.
2.3 Quelques exemples
Nous donnons ici des exemples simples d'analyse d'algorithmes. De nombreux autres
exemples sont dans le polycopi.
2.3. Quelques exemples
29
2.3.1 Factorielle
Prenons l'exemple archi-classique de la fonction factorielle, et intressons-nous au
nombre d'oprations arithmtiques (comparaisons, additions, soustractions, multiplications) ncessaires son calcul. Considrons un premier programme calculant
n!
l'aide
d'une boucle.
static int fact(int n) {
int f = 1;
for (int i = 2; i <= n; i++)
f = f * i;
return f;
}
Nous avons
n1
itrations au sein desquelles le nombre d'oprations lmentaires est 3
(une comparaison, une multiplication et une addition), plus une dernire comparaison
pour sortir de la boucle. La complexit est donc
plexit de
fact
C(n) = 1 + 3(n 1) = O(n).
est donc linaire. Considrons un second programme calculant
La com-
n!,
cette
fois rcursivement.
static int
if (n ==
return
else
return
}
Soit
C(n)
fact(int n) {
0)
1;
n * fact(n-1);
le nombre d'oprations ncessaires pour calculer
nous avons alors
C(n) = 3 + C(n 1)
fact(n). Dans le cas rcursif,
car on eectue une comparaison, une soustraction
C(0) = 1, car on eectue
C(n) = 1 + 3n = O(n). La complexit de fact
(avant l'appel) et une multiplication (aprs l'appel). De plus
seulement une comparaison. On en dduit
est donc linaire.
Sur cet exemple, il est intressant de considrer galement la complexit en mmoire.
Pour la version utilisant une boucle, elle est constante, car limite aux deux variables
locales
et
i.
Pour la version rcursive, en revanche, elle est en
d'appels va contenir jusqu'
appels
fact,
O(n).
En eet, la pile
comme nous l'avons expliqu plus haut
page 17.
2.3.2 Tours de Hanoi
Le trs classique problme des tours de Hanoi consiste dplacer des disques de
diamtres dirents d'une tour de dpart une tour d'arrive en se servant d'une tour
intermdiaire. Les rgles suivantes doivent tre respectes : on ne peut dplacer qu'un
disque la fois, et on ne peut placer un disque que sur un disque plus grand ou sur un
emplacement vide.
Identions les tours par un entier. Pour rsoudre ce problme, il sut de remarquer
n de la tour ini vers dest, alors pour dplacer
ini vers dest, il sut de dplacer une tour de taille n de ini
ini vers dest et nalement la tour de hauteur n de temp vers
que, si l'on sait dplacer une tour de taille
une tour de taille
vers
temp,
dest.
n+1
de
un disque de
30
Chapitre 2. Notions de complexit
static void hanoi(int n, int ini, int temp, int dest){
if (n == 0) return; // rien faire
hanoi(n - 1, ini, dest, temp);
[Link]("deplace " + ini + " vers " + dest);
hanoi(n - 1, temp, ini, dest);
}
Notons
C(n) le nombre d'instructions lmentaires pour calculer hanoi(n, ini, temp, dest).
C(n + 1) 2C(n) + , o est une constante, et C(0) = 0. On en dduit
Nous avons
facilement par rcurrence l'ingalit
C(n) (2n 1).
La mthode
hanoi
Exercice * 2.1.
a donc une complexit exponentielle
O(2n ).
Quelle est la complexit de la mthode suivante qui calcule le
n-ime
lment de la suite de Fibonacci ?
static int fib(int n) {
if (n <= 1) return n;
return fib(n-2) + fib(n-1);
}
(C'est bien entendu une faon nave de calculer la suite de Fibonacci ; on expliquera plus
loin dans ce cours comment faire beaucoup mieux.)
Deuxime partie
Structures de donnes lmentaires
Tableaux
De manire simple, un tableau n'est rien d'autre qu'une suite de valeurs stockes dans
des cases mmoire contigus. Ainsi on reprsentera graphiquement le tableau contenant
la suite de valeurs entires
3,7,42,1,4,8,12
de la manire suivante :
42
12
La particularit d'une structure de tableau est que le contenu de la
lu ou modi
i-ime
case peut tre
en temps constant.
En Java, on peut construire un tel tableau en numrant ses lments entre accolades,
spars par des virgules :
int[] a = {3, 7, 42, 1, 4, 8, 12};
Les cases sont numrotes partir de 0. On accde la premire case avec la notation
a[0],
la seconde avec
a[1],
etc. Si on tente d'accder en dehors des cases valides du
tableau, par exemple en crivant
a[-1],
alors on obtient une erreur :
Exception in thread "main" [Link]: -1
On modie le contenu d'une case avec la construction d'aectation habituelle. Ainsi pour
mettre 0 la deuxime case du tableau
a,
on crira
a[1] = 0;
Si l'indice ne dsigne pas une position valide dans le tableau, l'aectation provoque la
mme exception que pour l'accs.
On peut obtenir la longueur du tableau
a avec l'expression [Link]. Ainsi [Link]
vaut 7 sur l'exemple prcdent. L'accs la longueur se fait en temps constant. Un tableau
peut avoir la longueur 0.
Il existe d'autres procds pour construire un tableau que d'numrer explicitement ses
lments. On peut notamment construire un tableau de taille donne avec la construction
new
int[] c = new int[100];
Ses lments sont alors initialiss avec une valeur par dfaut (ici 0 car il s'agit d'entiers).
34
Chapitre 3. Tableaux
3.1 Parcours d'un tableau
Supposons que l'on veuille faire la somme de tous les lments d'un tableau d'entiers
Un algorithme simple pour cela consiste initialiser une variable
a.
s 0 et parcourir tous
les lments du tableau pour les ajouter un par un cette variable. La mthode naturelle
for, pour aecter successivement
[Link] 1. Ainsi on peut crire la mthode sum
pour eectuer ce parcours consiste utiliser une boucle
une variable
les valeurs
0, 1,
. . .,
de la manire suivante :
static int sum(int[] a) {
int s = 0;
for (int i = 0; i < [Link]; i++)
s += a[i];
return s;
}
Cependant, on peut faire encore plus simple car la boucle
directement tous les
lments
du tableau
for de Java permet de parcourir
for(int x : a). Ainsi
avec la construction
le programme se simplie en
int s = 0;
for (int x : a)
s += x;
return s;
Ce programme eectue exactement
[Link]
additions, soit une complexit linaire.
Comme exemple plus complexe, considrons l'valuation d'un polynme
A(X) =
ai X i
0i<n
A sont stocks dans un tableau a, le coecient
ai tant stock dans a[i]. Ainsi le tableau [1, 2, 3] reprsente le polynme 3X 2 +2X+1.
On suppose que les coecients du polynme
Une mthode simple, mais nave, consiste crire une boucle qui ralise exactement la
somme ci-dessus.
static double evalPoly(double[] a, double x) {
double s = 0.0;
for (int i = 0; i < [Link]; i++)
s += a[i] * [Link](x, i);
return s;
}
Une mthode plus ecace pour valuer un polynme est d'utiliser la
mthode de Horner.
Elle consiste rcrire la somme ci-dessus de la manire suivante :
A(X) = a0 + X(a1 + X(a2 + + X(an2 + Xan1 ) . . . ))
Ainsi on vite le calcul des direntes puissances
ne faisant plus que des multiplications par
X.
X i,
en factorisant intelligemment, et en
Pour raliser ce calcul, il faut parcourir le
3.2. Recherche dans un tableau
35
tableau de la droite vers la gauche, pour que le traitement de la
multiplier par
la somme courante puis lui ajouter
a[i].
i-ime case de a consiste
s contient la
Si la variable
somme courante, la situation est donc la suivante :
A(X) = a0 + X( (ai + X(ai+1 + )))
| {z }
s
Ainsi la mthode de Horner s'crit
static double horner(double[] a, double x) {
double s = 0.0;
for (int i = [Link] - 1; i >= 0; i--)
s = a[i] + x * s;
return s;
}
On constate facilement que ce programme eectue exactement
[Link]
additions et
autant de multiplications, soit une complexit linaire.
Exercice 3.1.
crire une mthode qui prend un tableau d'entiers
Exercice 3.2.
crire une mthode qui renvoie un tableau contenant les
en argument et
renvoie le tableau des sommes cumules croissantes de a, autrement dit un tableau de
Pk
mme taille dont la k -ime composante vaut
i=0 a[i]. La complexit doit tre linaire.
Le tableau fourni en argument ne doit pas tre modi.
premires
valeurs de la suite de Fibonacci dnie par
F0 = 0
F1 = 1
Fn = Fn2 + Fn1
n 2.
pour
La complexit doit tre linaire.
Exercice 3.3.
crire une mthode
void shuffle(int[] a) qui mlange alatoirement
les lments d'un tableau en utilisant l'algorithme suivant appel mlange de Knuth
(Knuth
shue ),
est la taille du tableau :
pour
soit
de
n1
i (inclus)
i et j
un entier alatoire entre 0 et
changer les lments d'indices
On obtient un entier alatoire entre 0 et
k1
avec
(int)([Link]() * k ).
3.2 Recherche dans un tableau
3.2.1 Recherche par balayage
Supposons que l'on veuille dterminer si un tableau
On ne va pas ncessairement examiner
tous
contient une certaine valeur
v.
les lments du tableau, car on souhaite
interrompre le parcours ds que l'lment est trouv. Une solution consiste utiliser
return
ds que la valeur est trouve :
36
Chapitre 3. Tableaux
static boolean contains(int[] a, int v) {
for (int x : a)
if (x == v)
return true;
return false;
}
Dans la section 2.1.4, nous avons montr que la complexit en moyenne de cet algorithme
est linaire.
Exercice 3.4.
valeur de
apparat dans
Exercice 3.5.
contains qui renvoie l'indice
faire lorsque a ne contient pas v ?
crire une variante de la mthode
a,
le cas chant. Que
o la
crire une mthode qui renvoie l'lment maximal d'un tableau d'entiers.
On discutera des diverses solutions pour traiter le cas d'un tableau de longueur 0.
3.2.2 Recherche dichotomique dans un tableau tri
Dans le pire des cas, la recherche par balayage ci-dessus parcourt tout le tableau,
et eectue donc
comparaisons, o
est la longueur du tableau. Dans certains cas,
cependant, la recherche d'un lment dans un tableau peut tre ralise de manire plus
ecace. C'est le cas par exemple lorsque le tableau est tri. On peut alors exploiter l'ide
suivante : on coupe le tableau en deux par le milieu et on dtermine si la valeur
doit
tre recherche dans la moiti gauche ou droite. En eet, il sut pour cela de la comparer
avec la valeur centrale. Puis on rpte le processus sur la portion slectionne.
9 dans le tableau {1,3,5,6,9,12,14}.
Supposons par exemple que l'on cherche la valeur
La recherche s'eectue ainsi :
on cherche dans
on compare
x=9
on cherche dans
on compare
x=9
on cherche dans
on compare
x=9
a[0:7]
avec
a[3]=6
12
14
12
14
12
14
12
14
a[4:7]
avec
a[5]=12
a[4:4]
avec
a[4]=9
On note que seulement trois comparaisons ont t ncessaires pour trouver la valeur. C'est
une application du principe
diviser pour rgner .
Pour crire l'algorithme, on dlimite la portion du tableau
doit tre recherche l'aide de deux indices
valeurs strictement gauche de
de
suprieures
v,
et
d.
dans laquelle la valeur
sont infrieures
et les valeurs strictement droite
ce qui s'illustre ainsi
0
<v
On commence par initialiser les variables
d
?
et
On maintient l'invariant suivant : les
>v
avec 0 et
n
[Link]-1,
respectivement.
3.2. Recherche dans un tableau
37
static boolean binarySearch(int[] a, int v) {
int g = 0, d = [Link] - 1;
while (g <= d) {
Tant que la portion considrer contient au moins un lment
while (g <= d) {
on calcule l'indice de l'lment central, en faisant la moyenne de
et
d.
int m = (g + d) / 2;
Il est important de noter qu'on eectue ici une division
entire.
Qu'elle soit arrondie vers
g et d, ce qui
a[m] existe et qu'il est bien situ entre g et d. Si l'lment a[m] est
le bas ou vers le haut, on obtiendra toujours une valeur comprise entre
assure d'une part que
l'lment recherch, on a termin la recherche.
if (a[m] == v)
return true;
Sinon, on dtermine si la recherche doit tre poursuivie gauche ou droite. Si
a[m] < v,
on poursuit droite :
if (a[m] < v)
g = m + 1;
Sinon, on poursuit gauche :
else
d = m - 1;
Si on sort de la boucle
while,
c'est que l'lment ne se trouve pas dans le tableau, car
il ne reste que des lments strictement plus petits ( gauche de
grands ( droite de
d).
On renvoie alors
false
g)
ou strictement plus
pour signaler l'chec.
return false;
Le code complet est donn programme 1 page 38.
Java fournit une telle mthode
binarySearch
Note : La bibliothque standard de
[Link].
algorithme est au pire O(log n)
dans la classe
Montrons maintenant que la complexit de cet
est la longueur du tableau. En particulier, on eectue au pire un nombre logarithmique
de comparaisons. La dmonstration consiste tablir qu'aprs
on a l'ingalit
dg<
itrations de la boucle,
2k
k . Initialement, on a g = 0 et d = n 1 et
k = 0, donc l'ingalit est tablie. Supposons maintenant l'ingalit vraie au rang k et
g d. la n de la k + 1-ime itration, on a soit g = m+1, soit d = m-1. Dans le premier
La dmonstration se fait par rcurrence sur
cas, on a donc
g+d
g+d dg
n
n
+1 d
=
< k
= k+1
2
2
2
2 2
2
38
Chapitre 3. Tableaux
Programme 1 Recherche dichotomique dans un tableau tri
static boolean binarySearch(int[] a, int v) {
int g = 0, d = [Link] - 1;
while (g <= d) {
int m = (g + d) / 2;
if (a[m] == v)
return true;
if (a[m] < v)
g = m + 1;
else
d = m - 1;
}
return false;
}
Le second cas est laiss au lecteur. On conclut ainsi : pour
c'est--dire
d g 0.
k log2 (n),
on a
d g < 1,
On fait alors au plus une dernire itration.
La complexit de la recherche dichotomique est donc
recherche par balayage est
O(n).
O(log n),
alors que celle de la
Il ne faut cependant pas oublier qu'elles ne s'appliquent
pas dans les mmes conditions : une recherche dichotomique est exclue si les donnes ne
sont pas tries.
Exercice 3.6.
Montrer que la mthode
Exercice 3.7.
Pour un tableau de plus de
forme
(g+d)/2
binarySearch
230
termine toujours.
lments, le calcul de l'index
peut provoquer un dbordement de la capacit du type
int.
sous la
Comment y
remdier ?
3.3 Mode de passage des tableaux
Considrons le programme suivant, o une mthode
f reoit un tableau b en argument
et le modie
static void f(int[] b) {
b[2] = 42;
}
et o le programme principal construit un tableau
et le passe la mthode
int[] a = {0, 1, 2, 3};
f(a);
Il est important de comprendre que, pendant l'excution de la mthode
locale
est un alias pour le tableau
a.
Ainsi, l'entre de la mthode
f,
f,
on a
la variable
3.4. Tableaux redimensionnables
a
0 1 2 3
b
et, aprs l'excution de l'instruction
39
b[2] = 42;,
on a
0 1 42 3
b
En particulier, aprs l'appel la mthode
f, on a a[2] == 42. (Nous avions dj expliqu
cela section 1.2.3 mais il n'est pas inutile de le redire.)
Il est parfois utile d'crire des mthodes qui modient le contenu d'un tableau reu en
argument. Un exemple typique est celui d'une mthode qui change le contenu de deux
cases d'un tableau :
static void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
Un autre exemple est celui d'une mthode qui trie un tableau ; plusieurs exemples seront
donns dans le chapitre 12.
3.4 Tableaux redimensionnables
La taille d'un tableau Java est dtermine sa cration et elle ne peut tre modie
ultrieurement. Il existe cependant de nombreuses situations o le nombre de donnes
manipules n'est pas connu l'avance mais o, pour autant, on souhaite les stocker dans
un tableau pour un accs en lecture et en criture en temps constant. Dans cette section, nous prsentons une solution simple ce problme, connue sous le nom de
redimensionnable (resizable array
tableau
en anglais).
On suppose, pour simplier, qu'on ne s'intresse ici qu' des tableaux contenant des
lments de type
int.
On cherche donc dnir une classe
ResizableArray
fournissant
un constructeur
ResizableArray(int len)
pour construire un nouveau tableau redimensionnable de taille
len,
et (au minimum) les
mthodes suivantes :
int size()
void setSize(int len)
int get(int i)
void set(int i, int v)
La mthode
size
renvoie la taille du tableau. la dirence d'un tableau usuel, cette
taille peut tre modie
a posteriori
avec la mthode
setSize.
Les mthodes
get
et
set
sont les oprations de lecture et d'criture dans le tableau. Comme pour un tableau usuel,
elles lveront une exception si on cherche accder en dehors des bornes du tableau.
40
Chapitre 3. Tableaux
3.4.1 Principe
L'ide de la ralisation est trs simple : on utilise un tableau usuel pour stocker les
lments et lorsqu'il devient trop petit, on en alloue un plus grand dans lequel on recopie
les lments du premier. Pour viter de passer notre temps en allocations et en copies,
on s'autorise ce que le tableau de stockage soit trop grand, les lments au-del d'un
certain indice n'tant plus signicatifs. La classe
ResizableArray est donc ainsi dclare
class ResizableArray {
private int length;
private int[] data;
o le champ
data est le tableau de stockage des lments et length le nombre signicatif
d'lments dans ce tableau, ce que l'on peut schmatiser ainsi :
[Link]
[Link]
. . . lments . . .
. . . inutilis . . .
[Link]
On maintiendra donc toujours l'invariant suivant :
0 length [Link]
On note le caractre priv des champs
data et length, ce qui nous permettra de maintenir
l'invariant ci-dessus.
Pour crer un nouveau tableau redimensionnable de taille
il sut d'allouer un
sans oublier d'initialiser le champ
length
La taille du tableau redimensionnable est directement donne par le champ
length.
tableau usuel de cette taille-l dans
data,
len,
ga-
lement :
ResizableArray(int len) {
[Link] = len;
[Link] = new int[len];
}
int size() {
return [Link];
}
Pour accder au
i-ime
lment du tableau redimensionnable, il convient de vrier la
validit de l'accs, car le tableau
[Link] peut contenir plus de [Link] lments.
int get(int i) {
if (i < 0 || i >= [Link])
throw new ArrayIndexOutOfBoundsException(i);
return [Link][i];
}
L'aectation est analogue (voir page 42). Toute la subtilit est dans la mthode
qui redimensionne un tableau pour lui donner une nouvelle taille
il n'y a rien faire, si ce n'est mettre le champ
[Link],
il va
setSize
Plusieurs cas de
len est infrieure ou gale la taille de [Link],
length jour. Si en revanche len est plus
falloir remplacer data par un tableau plus grand. On
gure se prsentent. Si la nouvelle taille
grand la taille de
len.
commence donc par eectuer ce test :
3.4. Tableaux redimensionnables
41
void setSize(int len) {
int n = [Link];
if (len > n) {
len
doubler la
On alloue alors un tableau susamment grand. On pourrait choisir tout simplement
pour sa taille, mais on adopte une stratgie plus subtile consistant au moins
taille de
[Link]
(nous la justierons par la suite) :
int[] a = new int[[Link](len, 2 * n)];
On recopie alors les lments signicatifs de
[Link]
vers ce nouveau tableau
for (int i = 0; i < [Link]; i++)
a[i] = [Link][i];
puis on remplace
[Link]
par ce nouveau tableau :
[Link] = a;
L'ancien tableau sera ramass par le GC. Enn on met jour la valeur de
quel que soit le rsultat du test
len > n
[Link],
[Link] = len;
setSize. L'intgralit du code est donne programme 2 page 42.
Note : La bibliothque Java fournit une telle classe, dans [Link]<E>. Il s'agit
d'une classe gnrique, paramtre par la classe E des lments.
ce qui conclut le code de
Exercice 3.8.
Il peut tre souhaitable de rediminuer parfois la taille du tableau, par
exemple si elle devient grande par rapport au nombre d'lments eectifs et que le tableau
occupe beaucoup de mmoire. Modier la mthode
setSize
pour qu'elle divise par deux
la taille du tableau lorsque le nombre d'lments devient infrieur au quart de la taille du
tableau.
Exercice 3.9.
Ajouter une mthode
int[] toArray()
qui renvoie un tableau usuel
contenant les lments du tableau redimensionnable.
3.4.2 Application 1 : Lecture d'un chier
On souhaite lire une liste d'entiers contenus dans un chier, sous la forme d'un entier
par ligne, et les stocker dans un tableau. On ne connat pas le nombre de lignes du
chier. Bien entendu, on pourrait commencer par compter le nombre de lignes, pour
allouer ensuite un tableau de cette taille-l avant de lire les lignes du chier. Mais on peut
plus simplement encore utiliser un tableau redimensionnable. En supposant que le chier
s'appelle
[Link],
on commence par l'ouvrir de la manire suivante :
BufferedReader f = new BufferedReader(new FileReader("[Link]"));
42
Chapitre 3. Tableaux
Programme 2 Tableaux redimensionnables
class ResizableArray {
private int length; // nb d'lments significatifs
private int[] data; // invariant: 0 <= length <= [Link]
ResizableArray(int len) {
[Link] = len;
[Link] = new int[len];
}
int size() {
return [Link];
}
int get(int i) {
if (i < 0 || i >= [Link])
throw new ArrayIndexOutOfBoundsException(i);
return [Link][i];
}
void set(int i, int v) {
if (i < 0 || i >= [Link])
throw new ArrayIndexOutOfBoundsException(i);
[Link][i] = v;
}
void setSize(int len) {
int n = [Link];
if (len > n) {
int[] a = new int[[Link](len, 2 * n)];
for (int i = 0; i < [Link]; i++)
a[i] = [Link][i];
[Link] = a;
}
[Link] = len;
}
3.4. Tableaux redimensionnables
43
Puis on alloue un nouveau tableau redimensionnable destin recevoir les entiers que l'on
va lire. Initialement, il ne contient aucun lment :
ResizableArray r = new ResizableArray(0);
On crit ensuite une boucle dans laquelle on lit chaque ligne du chier avec la mthode
readLine
du
BufferedReader
while (true) {
String s = [Link]();
La valeur
null
signale la n du chier, auquel cas on sort de la boucle avec
break
if (s == null) break;
Dans le cas contraire, on tend la taille du tableau redimensionnable d'une unit, et on
stocke l'entier lu dans la dernire case du tableau :
int len = [Link]();
[Link](len + 1);
[Link](len, [Link](s));
Ceci conclut la lecture du chier. Pour tre complet, il faudrait aussi grer les exceptions
new FileReader d'une part et par [Link]()
(avec try catch), soit en les dclarant avec throws.
possiblement leves par
soit en les rattrapant
Exercice 3.10.
d'autre part,
void append(int v) la classe ResizableArray
v dans sa dernire case.
utilisant cette nouvelle mthode.
Ajouter une mthode
qui augmente la taille du tableau d'une unit et stocke la valeur
Simplier le programme ci-dessus en
Complexit.
Dans le programme ci-dessus, nous avons dmarr avec un tableau redi-
mensionnable de taille 0 et nous avons augment sa taille d'une unit chaque lecture
d'une nouvelle ligne. Si la mthode
setSize
avait eectu systmatiquement une allo-
cation d'un nouveau tableau et une recopie des lments dans ce tableau, la complexit
aurait t quadratique, puisque la lecture de la
cot
i,
i-ime
ligne du chier aurait alors eu un
d'o un cot total
1 + 2 + + n =
pour un chier de
n(n + 1)
= O(n2 )
2
lignes. Cependant, la stratgie de
setSize
est plus subtile, car
elle consiste doubler (au minimum) la taille du tableau lorsqu'il doit tre agrandi.
Montrons que le cot total est alors linaire. Supposons, sans perte de gnralit, que
n 2 et posons k = blog2 (n)c c'est--dire 2k n < 2k+1 . Au total, on aura eectu
k + 2 redimensionnements pour arriver un tableau data de taille nale 2k+1 . Aprs le
i-ime redimensionnement, pour i = 0, . . . ,k + 1, le tableau a une taille 2i et le i-ime
i
redimensionnement a donc cot 2 . Le cot total est alors
k+1
X
i=0
2i = 2k+2 1 = O(n).
44
Chapitre 3. Tableaux
Autrement dit, certaines oprations
setSize
ont un cot constant (lorsque le redimen-
sionnement n'est pas ncessaire) et d'autres au contraire un cot non constant, mais
la complexit totale reste linaire. Ramen l'ensemble des
oprations, tout se passe
comme si chaque opration d'ajout d'un lment avait eu un cot constant. On parle
complexit amortie pour dsigner la complexit
ensemble de n oprations. Dans le cas prsent, on
de
d'une opration en moyenne sur un
peut donc dire que l'extension d'un
tableau redimensionnable d'une unit a une complexit amortie
O(1).
3.4.3 Application 2 : Concatnation de chanes
Donnons un autre exemple de l'utilit du tableau redimensionnable. Supposons que
l'on souhaite construire une chane de caractres numrant tous les entiers de 0
la forme
"0, 1, 2, 3, ..., n".
sous
Si on crit navement
String s = "0";
for (int i = 1; i <= n; i++)
s += ", " + i;
alors la complexit est quadratique, car chaque concatnation de chanes, pour construire
s + ", " + i, a un cot proportionnel la longueur de s, c'est--dire proportionnel i. L encore, on peut avantageusement exploiter le principe du tableau redimensionnable pour construire la chane s. Il sut d'adapter le code de ResizableArray
le rsultat de
pour des caractres plutt que des entiers (ou encore en faire une classe gnrique voir
section 3.4.5 plus loin).
Plus simplement encore, la bibliothque Java fournit une classe
StringBuilder
pour
construire des chanes de caractres incrmentalement sur le principe du tableau redimensionnable. Une mthode
StringBuilder,
append
permet de concatner une chane la n d'un
d'o le code
StringBuilder buf = new StringBuilder("0");
for (int i = 1; i <= n; i++)
[Link](", " + i);
La chane nale peut tre rcupre avec
[Link]().
Cette fois, la complexit est
bien linaire, par un raisonnement identique au prcdent.
Exercice 3.11.
Ajouter une mthode
String toString() la classe ResizableArray,
qui renvoie le contenu d'un tableau redimensionnable sous la forme d'une chane de caractres telle que
"[3, 7, 2]".
On se servira d'un
StringBuilder
pour construire cette
chane.
3.4.4 Application 3 : Structure de pile
On va mettre prot la notion de tableau redimensionnable pour construire une
structure de pile.
Elle correspond exactement l'image traditionnelle d'une pile de cartes
ou d'assiettes pose sur une table. En particulier, on ne peut accder qu'au dernier lment
ajout, qu'on appelle le
B,
puis
sommet
de la pile. Ainsi, si on a ajout successivement
dans une pile, on se retrouve dans la situation suivante
A,
puis
3.4. Tableaux redimensionnables
45
C
B
A
B,
est empil sur
qu'on dpile
lui-mme empil sur
A.
On peut soit retirer
de la pile (on dit
C ), soit ajouter un quatrime lment D (on dit qu'on empile D). Si
A, il faut commencer par dpiler C puis B . L'image associe
on veut accder l'lment
une pile est donc dernier arriv, premier sorti (en anglais, on parle de LIFO pour
last in, rst out ).
Nous allons crire la structure de pile dans une classe
Stack, en fournissant les opra-
tions suivantes :
Stack() renvoie une nouvelle pile, initialement vide ;
la mthode pop() dpile et renvoie le sommet de la pile ;
la mthode push(v) empile la valeur v.
la mthode size() renvoie le nombre d'lments contenus dans la pile ;
la mthode isEmpty() indique si la pile est vide ;
la mthode top() renvoie le sommet de la pile, sans la modier.
Seules les oprations push et pop modient le contenu de la pile. Voici une illustration de
le constructeur
l'utilisation de cette structure :
Stack s = new Stack();
[Link](A);
[Link](B);
[Link](C);
C
B
A
int x = [Link]();
// x vaut C
B
A
Comme on l'aura devin, le tableau redimensionnable nous fournit exactement ce dont
nous avons besoin pour raliser une pile. En eet, il sut de stocker le premier lment
empil l'indice 0, puis le suivant l'indice 1, etc. Le sommet de pile se situe donc
l'indice
n1
est la taille du tableau redimensionnable. Pour empiler un lment, il
sut d'augmenter la taille du tableau d'une unit. Pour dpiler, il sut de rcuprer le
dernier lment du tableau puis de diminuer sa taille d'une unit.
Pour bien faire les choses, cependant, il convient d'encapsuler le tableau redimensionnable dans la classe
Stack, de manire garantir la bonne abstraction de pile. Pour cela,
il sut d'en faire un champ priv
class Stack {
private ResizableArray elts;
...
}
Ainsi, seules les mthodes fournies pourront en modier le contenu. Le code complet est
donn programme 3 page 46. Note : La bibliothque Java fournit une version gnrique
de la structure de pile, dans
[Link]<E>.
46
Programme 3 Structure de pile
(ralise l'aide d'un tableau redimensionnable)
class Stack {
private ResizableArray elts;
Stack() {
[Link] = new ResizableArray(0);
}
boolean isEmpty() {
return [Link]() == 0;
}
int size() {
return [Link]();
}
void push(int x) {
int n = [Link]();
[Link](n + 1);
[Link](n, x);
}
int pop() {
int n = [Link]();
if (n == 0)
throw new NoSuchElementException();
int e = [Link](n - 1);
[Link](n - 1);
return e;
}
int top() {
int n = [Link]();
if (n == 0)
throw new NoSuchElementException();
return [Link](n - 1);
}
}
Chapitre 3. Tableaux
3.4. Tableaux redimensionnables
Exercice 3.12.
crire une mthode
47
swap qui change les deux lments au sommet
Stack, puis comme une nouvelle mthode de
d'une pile, d'abord l'extrieur de la classe
la classe
Stack.
3.4.5 Code gnrique
Pour raliser une version gnrique de la classe
le type
ResizableArray, on la paramtre par
des lments.
class ResizableArray<T> {
private int length;
private T[] data;
Le code reste essentiellement le mme, au remplacement du type
int
par le type
aux
endroits opportuns. Il y a cependant deux subtilits. La premire concerne la cration
d'un tableau de type
T[].
Naturellement, on aimerait crire dans le constructeur
[Link] = new T[len];
mais l'expression
new T[len]
est refuse par le compilateur, avec le message d'erreur
Cannot create a generic array of T
C'est l une limitation du systme de types de Java. Il existe plusieurs faons de contourner
ce problme. Ici, on peut se contenter de crer un tableau de
avec un transtypage (cast en anglais) :
Object puis de forcer le type
[Link] = (T[])new Object[len];
Le compilateur met un avertissement (Unchecked
cast from Object[] to T[])
mais
il n'y a pas de risque ici car il s'agit d'un champ priv que nous ne pourrons jamais
confondre avec un tableau d'un autre type.
La seconde subtilit concerne la mthode
setSize,
dans le cas o la taille du tableau
est diminue. Jusqu' prsent, on ne faisait rien, si ce n'est mettre jour le champ
length.
Mais dans la version gnrique, il faut prendre soin de ne pas conserver tord des pointeurs
vers des objets qui pourraient tre rcuprs par le GC car inutiliss par ailleurs. Il faut
donc eacer les lments partir de l'indice
valeur
null.
void setSize(int len) {
int n = [Link];
if (len > n) {
... mme code qu'auparavant ...
} else {
for (int i = len; i < n; i++)
[Link][i] = null;
}
[Link] = len;
}
length.
On le fait en leur aectant la
48
Chapitre 3. Tableaux
On maintient donc l'invariant suivant :
i. [Link] i < [Link] [Link][i] = null
Dans la version non gnrique, nous n'avions pas ce problme, car les lments taient
des entiers, et non des pointeurs.
Listes chanes
Ce chapitre introduit une structure de donne fondamentale en informatique, la
chane. Il s'agit d'une structure dynamique
liste
au sens o, la dirence du tableau, elle est
gnralement alloue petit petit, au fur et mesure des besoins.
4.1 Listes simplement chanes
Le principe d'une liste chane est le suivant : chaque lment de la liste est reprsent
par un objet et contient d'une part la valeur de cet lment et d'autre part un pointeur
vers l'lment suivant de la liste. Si on suppose que les lments sont ici des entiers, la
dclaration d'une classe
Singly
pour reprsenter une telle liste chane est donc aussi
simple que
class Singly {
int element;
Singly next;
}
La valeur
null nous sert reprsenter la liste vide i.e. la liste ne contenant aucun lment.
Le constructeur naturel de cette classe prend en arguments les valeurs des deux champs :
Singly(int element, Singly next) {
[Link] = element;
[Link] = next;
}
Ainsi, on peut construire une liste contenant les trois entiers
variable
x,
1, 2
et
3,
stocke dans une
de la faon suivante :
Singly x = new Singly(1, new Singly(2, new Singly(3, null)));
On a donc construit en mmoire une structure qui a la forme suivante, o chaque lment
de la liste est un bloc sur le tas :
Singly
1
Singly
2
Singly
3
50
Chapitre 4. Listes chanes
Le bloc contenant la valeur 3 a t construit en premier, puis celui contenant la valeur 2,
tte
puis enn celui contenant la valeur 1. Ce dernier est appel la
trs particulier o
de la liste. Dans le cas
est la liste vide, on crit tout simplement
Singly x = null;
ce qui correspond une situation o
aucun
objet n'a t allou en mmoire :
Singly, une telle valeur n'en est pas pour autant un objet de la classe
Singly, avec un champ element et un champ next. Par consquent, il faudra pendre soin
Bien que de type
dans la suite de toujours bien traiter le cas de la liste vide, de manire viter l'exception
NullPointerException.
Ds la section suivante, nous verrons comment apporter une
solution lgante ce problme.
Parcours d'une liste
La nature mme d'une liste chane nous permet d'en parcourir les lments trs
facilement. En eet, si la variable
dsigne un certain lment de la liste, passer
l'lment suivant consiste en la simple aectation
lorsqu'on atteint la valeur
null.
x = [Link].
Le parcours s'arrte
Le schma d'une boucle parcourant tous les lments
d'une liste est donc le suivant :
while (x != null) {
...
x = [Link];
}
Comme premier exemple, considrons une mthode statique
un entier
apparat dans une liste
cessivement la valeur
contains qui dtermine si
s pour comparer suc-
donne. On parcourt la liste
avec tous les lments de la liste.
static boolean contains(Singly s, int x) {
while (s != null) {
Si l'lment courant est gal
contains.
x, on renvoie immdiatement true, ce qui achve la mthode
if ([Link] == x) return true;
Sinon, on passe l'lment suivant :
s = [Link];
Si on nit par sortir de la boucle, c'est que
alors
false
n'apparat pas dans la liste et on renvoie
return false;
Il est important de noter que ce code fonctionne correctement sur une liste vide, c'est--
s vaut null. En eet, on sort immdiatement de la boucle et on renvoie false,
le rsultat attendu. Le code de la mthode contains est donn programme 4
dire lorsque
ce qui est
page 51.
4.1. Listes simplement chanes
51
Programme 4 Listes simplement chanes
class Singly {
int element;
Singly next;
Singly(int element, Singly next) {
[Link] = element;
[Link] = next;
}
static boolean contains(Singly s, int x) {
while (s != null) {
if ([Link] == x) return true;
s = [Link];
}
return false;
}
Complexit.
contient
cot
n.
Quelle est la complexit de la fonction
contains ? Supposons que la liste
lments. Une recherche infructueuse implique un parcours total de la liste, de
Si en revanche la valeur
la position
i,
apparat dans la liste, avec une premire occurrence
alors on aura eectu exactement
tours de boucle. Si on suppose que
apparat avec quiprobabilit toutes les positions possibles dans la liste, le cot moyen
de sa recherche est donc
n+1
1X
i=
n i=1
2
ce qui est galement linaire. La structure de liste chane n'est donc pas particulirement
adapte la recherche d'un lment ; elle a d'autres applications, qui sont dcrites dans
les prochaines sections.
Exercice 4.1.
crire une mthode statique
int length(Singly s)
gueur de la liste
s.
Exercice 4.2.
crire une mthode statique
l'lment d'indice
exception si
qui renvoie la lon-
de la liste
s,
int get(Singly s, int i)
qui renvoie
l'lment de tte tant considr d'indice 0. Lever une
i ne dsigne pas un indice valide (par exemple IllegalArgumentException).
Achage
listToString qui conver"[1 -> 2 -> 3]". Comme
titre de deuxime exemple, crivons une mthode statique
tit une liste chane en une chane de caractres de la forme
nous l'avons expliqu plus haut (section 3.4.3), la faon ecace de construire une telle
52
Chapitre 4. Listes chanes
StringBuilder. On commence donc par allouer un tel objet, avec
"[" :
chane est d'utiliser un
une chane rduite
static String listToString(Singly s) {
StringBuilder sb = new StringBuilder("[");
Puis on ralise le parcours de la liste, ainsi qu'expliqu ci-dessus. Pour chaque lment,
on ajoute sa valeur, c'est--dire l'entier
" -> "
null.
[Link],
au
s'il ne s'agit pas du dernier lment de la liste,
StringBuilder, puis
c'est--dire si [Link]
la chane
n'est pas
while (s != null) {
[Link]([Link]);
if ([Link] != null) [Link](" -> ");
s = [Link];
}
Une fois sorti de la boucle, on ajoute le crochet fermant et on renvoie la chane contenue
dans le
StringBuilder.
return [Link]("]").toString();
L encore, ce code fonctionne correctement sur une liste vide, renvoyant la chane
Exercice 4.3.
Quelle est la complexit de la mthode
"[]".
listToString ? (Il faut ventuel
lement relire ce qui avait t expliqu dans la section 3.4.3.)
Tirage alatoire d'un lment
Comme troisime exemple de parcours d'une liste, considrons le problme suivant :
tant donne une liste non vide, renvoyer l'un de ses lments alatoirement, avec quiprobabilit. Bien entendu, on pourrait commencer par calculer la longueur
(exercice 4.1), puis tirer un entier
accder l'lment d'indice
de n'eectuer qu'un
seul
alatoirement entre 0 inclus et
de la liste
exclus, et enn
de la liste (exercice 4.2). Cependant, on va s'imposer ici
parcours de la liste. Ce n'est pas forcment absurde ; on peut
imaginer une situation o les lments ne peuvent tre tous stocks en mmoire car trop
nombreux, par exemple.
L'ide est la suivante. On parcourt la liste en maintenant un lment candidat
la victoire dans le tirage. l'examen du
i-ime
lment de la liste, on choisit alors
1
de remplacer ce candidat par l'lment courant avec probabilit . Une fois arriv la
i
n de la liste, on renvoie la valeur nale du candidat. crivons une mthode statique
randomElement
qui ralise ce tirage. On commence par vacuer le cas d'une liste vide,
pour lequel le tirage ne peut tre eectu :
static int randomElement(Singly s) {
if (s == null) throw new IllegalArgumentException();
Sinon, on initialise notre candidat avec une valeur arbitraire (ici 0) et on conserve l'indice
de l'lment courant de la liste dans une autre variable
index.
4.1. Listes simplement chanes
53
Programme 5 Tirage alatoire dans une liste
static int randomElement(Singly s) {
if (s == null) throw new IllegalArgumentException();
int candidate = 0, index = 1;
while (s != null) {
if ((int)(index * [Link]()) == 0) candidate = [Link];
index++;
s = [Link];
}
return candidate;
}
int candidate = 0, index = 1;
Puis on ralise le parcours de la liste. On remplace
probabilit
1/index.
candidate par l'lment courant avec
while (s != null) {
if ((int)(index * [Link]()) == 0) candidate = [Link];
Pour cela, on tire un entier alatoirement entre 0 inclus et
index
exclus et on le compare
[Link](), qui renvoie un ottant entre 0 inclus et 1 exclus,
et on multiplie le rsultat par index, ce qui donne un ottant entre 0 inclus et index
exclus. Sa conversion en entier, avec (int)(...), en fait bien un entier entre 0 inclus et
index exclus. On passe ensuite l'lment suivant, sans oublier d'incrmenter index.
0. Pour cela on utilise
index++;
s = [Link];
Une fois sorti de la boucle, on renvoie la valeur de
candidate.
return candidate;
On note qu'au tout premier tour de boucle qui existe car la liste est non vide
l'lment de la liste est ncessairement slectionn car
[Link]() < 1
et donc
(int)(1 * [Link]()) = 0. La valeur arbitraire que nous avions utilise pour initialiser la variable candidate ne sera donc jamais renvoye. On en dduit galement que
le programme fonctionne correctement sur une liste rduite un lment. Le code complet
est donn programme 5 page 53.
Exercice 4.4.
Montrer que, si la liste contient
1
est choisi avec probabilit .
n
lments avec
n 1,
chaque lment
54
Chapitre 4. Listes chanes
4.2 Application 1 : Structure de pile
Une application immdiate de la liste simplement chane est la structure de pile, que
nous avons dj introduite dans la section 3.4.4. En eet, il sut de voir la tte de la
liste comme le sommet de la pile, et les oprations
push
et
pop
se font alors en temps
constant. Exactement comme nous l'avions fait avec le tableau redimensionnable dans
la section 3.4.4, on va
encapsuler
la liste chane dans une classe
Stack,
de manire
garantir la bonne abstraction de pile. On en fait donc un champ priv
class Stack {
private Singly head;
...
}
Le code complet de la structure de pile est donn programme 6 page 55. Si on construit
une pile avec
s = new Stack(), dans laquelle on ajoute successivement les entiers 1, 2 et
[Link](1); [Link](2); [Link](3), alors on se retrouve dans la
3, dans cet ordre, avec
situation suivante :
Stack
head
Singly
3
Singly
2
Singly
1
Comme nous l'avons dj dit, la bibliothque Java fournit une version gnrique de la
structure de pile dans
Exercice 4.5.
[Link]<E>.
int size la classe Stack, contenant le nombre
int size() pour renvoyer sa valeur. Expliquer
priv.
Ajouter un champ priv
d'lments de la pile, et une mthode
pourquoi le champ
Exercice 4.6.
size
doit tre
crire une mthode dynamique publique
String toString()
pour la
Stack, qui renvoie le contenu d'une pile sous la forme d'une chane de caractres
"[1, 2, 3]" o 1 est le sommet de la pile. On pourra s'inspirer de la mthode
listToString donne plus haut.
classe
telle que
On pourra galement reprendre les exercices de la section 3.4.4 consistant utiliser la
structure de pile.
4.3 Application 2 : Structure de le
Une autre application de la liste simplement chane, lgrement plus subtile, est la
structure de
le.
Il s'agit d'une structure orant la mme interface qu'une pile, savoir
push et pop, mais o les lments sont renvoys par pop dans
insrs avec push, c'est--dire suivant une logique premier arriv,
deux oprations principales
l'ordre o ils ont t
4.3. Application 2 : Structure de le
Programme 6 Structure de pile
(ralise l'aide d'une liste simplement chane)
class Stack {
private Singly head;
Stack() {
[Link] = null;
}
boolean isEmpty() {
return [Link] == null;
}
void push(int x) {
[Link] = new Singly(x, [Link]);
}
int top() {
if ([Link] == null)
throw new NoSuchElementException();
return [Link];
}
int pop() {
if ([Link] == null)
throw new NoSuchElementException();
int e = [Link];
[Link] = [Link];
return e;
}
}
55
56
Chapitre 4. Listes chanes
rst in, rst out ).
premier sorti (en anglais, on parle de FIFO pour
Dit autrement, il
s'agit ni plus ni moins de la le d'attente la boulangerie.
On peut raliser une le avec une liste simplement chane de la manire suivante :
les lments sont insrs par
pop
push
au niveau du dernier lment de la liste et retirs par
au niveau du premier lment de la liste. Il faut donc conserver un pointeur sur le
dernier lment de la liste. Tout comme dans la section prcdente, on va encapsuler la
liste chane dans une classe
Queue.
Cette fois, il y a deux champs privs,
head
et
tail,
pointant respectivement sur le premier et le dernier lment de la liste.
class Queue {
private Singly head, tail;
...
}
Ainsi, si on construit une le avec
q = new Queue(), dans laquelle on ajoute [Link](1); [Link](2); [Link](3), alors
ment les entiers 1, 2 et 3, dans cet ordre, avec
on se retrouve dans la situation suivante
Queue
head
tail
Singly
1
Singly
2
Singly
3
o les insertions se font droite et les retraits gauche. Les lments apparaissent donc
chans dans le mauvais sens . Le code est plus subtil que pour une pile et mrite qu'on
s'y attarde. Le constructeur se contente d'initialiser les deux champs
null
Queue() {
[Link] = [Link] = null;
}
[Link] vaut null si et
[Link] vaut null. En particulier, la le est vide si et seulement [Link]
De manire gnrale, nous allons maintenir l'invariant que
seulement
vaut
null
boolean isEmpty() {
return [Link] == null;
}
Considrons maintenant l'insertion d'un nouvel lment, c'est--dire la mthode
commence par allouer un nouvel lment de liste
e,
push. On
qui va devenir le dernier lment de
la liste chane.
void push(int x) {
Singly e = new Singly(x, null);
Il faut alors distinguer deux cas, selon que la le est vide ou non. Si elle est vide, alors
[Link] et [Link] pointent dsormais tous les deux sur cet unique lment de liste :
4.3. Application 2 : Structure de le
57
if ([Link] == null)
[Link] = [Link] = e;
e la n de la liste existante, dont le dernier lment est
oublier de mettre ensuite jour le pointeur [Link] :
Dans le cas contraire, on ajoute
point par
[Link],
sans
else {
[Link] = e;
[Link] = e;
}
push. Pour le retrait d'un lment, on procde l'autre extrmit
de la liste, c'est--dire du ct de [Link]. On commence par vacuer le cas d'une liste
Ceci conclut le code de
vide :
int pop() {
if ([Link] == null)
throw new NoSuchElementException();
Si en revanche la liste n'est pas vide, on peut accder son premier lment, qui nous
donne la valeur renvoyer :
int e = [Link];
Avant de la renvoyer, il faut supprimer le premier lment de la liste, ce qui est aussi
simple que
[Link] = [Link];
[Link]
null :
Cependant, pour maintenir notre invariant sur
[Link]
null
si
[Link]
est devenu
et
[Link],
on va mettre
if ([Link] == null) [Link] = null;
Cette ligne de code n'est pas ncessaire pour la correction de notre structure de le.
push teste la
[Link] null
[Link]
et non celle de
[Link].
En eet, notre mthode
valeur de
Cependant, mettre
permet au GC de Java de rcuprer la cellule de
liste devenue maintenant inutile. Enn, il n'y a plus qu' renvoyer la valeur
return e;
Le code complet de la structure de le est donn programme 7 page 58. Note : La bibliothque Java fournit une version gnrique de la structure de le dans
Exercice 4.7.
[Link]<E>.
int size la classe Queue, contenant le nombre
d'lments de la pile, et une mthode int size() pour renvoyer sa valeur. Expliquer
pourquoi le champ size doit tre priv.
Ajouter un champ priv
58
Programme 7 Structure de le
(ralise l'aide d'une liste simplement chane)
class Queue {
private Singly head, tail;
Queue() {
[Link] = [Link] = null;
}
boolean isEmpty() {
return [Link] == null;
}
void push(int x) {
Singly e = new Singly(x, null);
if ([Link] == null)
[Link] = [Link] = e;
else {
[Link] = e;
[Link] = e;
}
}
int top() {
if ([Link] == null)
throw new NoSuchElementException();
return [Link];
}
int pop() {
if ([Link] == null)
throw new NoSuchElementException();
int e = [Link];
[Link] = [Link];
if ([Link] == null) [Link] = null;
return e;
}
Chapitre 4. Listes chanes
4.4. Modication d'une liste
59
4.4 Modication d'une liste
Bien que nous n'en ayons pas fait usage pour l'instant, on note que les listes chanes
a posteriori. En eet, rien ne nous interdit de modier la valeur
element ou du champ next d'un objet de la classe Singly. Si on modie le
champ element, on modie le contenu. On peut ainsi modier en place la liste 1 2 3
pour en faire la liste 1 4 3. Si on modie le champ next, on modie la structure de
la liste. On peut ainsi ajouter un quatrime lment la n de la liste 1 2 3 pour
en faire la liste 1 2 3 4 en allant modier le champ next de l'lment 3 pour le
peuvent tre modies
du champ
faire pointer faire un nouvel lment de liste.
4.4.1 Listes cycliques
En particulier, on peut modier la structure d'une liste pour en faire une
liste cyclique .
Considrons par exemple le code suivant
Singly s4 = new Singly(4, null);
Singly s2 = new Singly(2, new Singly (3, s4));
Singly s0 = new Singly(0, new Singly (1, s2));
qui construit la liste 5 lments suivante :
0
Si on modie le champ
l'lment
s2,
next
de son dernier lment
s4
(4.1)
pour qu'il pointe dsormais sur
c'est--dire
[Link] = s2;
alors on se retrouve dans la situation suivante :
Cette liste ne contient plus aucun pointeur
null,
(4.2)
mais seulement des pointeurs vers
d'autres lments de la liste. D'une manire gnrale, on peut montrer que toute liste
simplement chane est soit de la forme (4.1), c'est--dire une liste linaire se terminant
par
null,
soit de la forme (4.2), c'est--dire une pole frire avec un manche de
0
= 3).
longueur nie
=2
et
et une boucle de longueur nie
(dans l'exemple ci-dessus on a
Il est important de comprendre que les programmes que nous avons crits plus haut, qui
sont construits autour d'un parcours de liste, ne fonctionnent plus sur une liste cyclique,
car ils ne terminent plus dans certains cas. En eet, le critre d'arrt
s == null
ne sera
jamais vri. Si on voulait les adapter pour qu'ils fonctionnent galement sur des listes
cycliques, il faudrait tre mme de dtecter la prsence d'un cycle. Si on y rchit un
instant, on comprend que le problme n'est pas trivial.
On prsente ici un algorithme de dtection de cycle, d Floyd, et connu sous le nom
d'algorithme du livre et de la tortue. Comme son nom le suggre, il consiste parcourir
la liste deux vitesses direntes : la tortue parcourt la liste la vitesse 1 et le livre
parcourt la mme liste la vitesse 2. Si un quelconque moment, le livre atteint la n
60
Chapitre 4. Listes chanes
Programme 8 Algorithme de dtection de cycle de Floyd
dit algorithme du livre et de la tortue
static boolean hasCycle(Singly s) {
if (s == null) return false;
Singly tortoise = s, hare = [Link];
while (tortoise != hare) {
if (hare == null) return false;
hare = [Link];
if (hare == null) return false;
hare = [Link];
tortoise = [Link];
}
return true;
}
de la liste, elle est dclare sans cycle. Et si un quelconque moment, le livre et la tortue
se retrouvent la mme position, c'est que la liste contient un cycle. Le code est donn
programme 8 page 60. La seule dicult dans ce code consiste correctement traiter les
dirents cas o le livre (la variable
un
NullPointerException
hare)
peut atteindre la n de la liste, an d'viter
dans le calcul de
[Link].
Toute la subtilit de cet algorithme rside dans la preuve de sa terminaison. Si la liste
est non cyclique, alors il est clair que le livre nira par atteindre la valeur
la mthode renverra alors
false.
null
et que
Dans le cas o la liste est cyclique, la preuve est plus
dlicate. Tant que la tortue est l'extrieur du cycle, elle s'en approche chaque tape
de l'algorithme, ce qui assure la terminaison de cette premire phase (en au plus
tapes
avec la notation ci-dessus). Et une fois la tortue prsente dans le cycle, on note qu'elle ne
peut tre dpasse par le livre. Ainsi la distance qui les spare diminue chaque tape
de l'algorithme, ce qui assure la terminaison de cette seconde phase (en au plus
tapes).
n o
Incidemment, on a montr que la complexit de cet algorithme est toujours au plus
est le nombre d'lments de la liste. Cet algorithme est donc tonnamment ecace. Et
il n'utilise que deux variables, soit un espace (supplmentaire) constant.
En pratique, cet algorithme est rarement utilis pour adapter un parcours de liste au
cas d'une liste cyclique. Son application se situe plutt dans le contexte d'une liste
virtuelle dnie par un lment de dpart
l'lment qui suit
x0
et une fonction
telle que
f (x)
est
dans la liste. Un gnrateur de nombres alatoires est un exemple
de telle fonction. L'algorithme de Floyd permet alors de calculer partir de quel rang ce
gnrateur entre dans un cycle et la longueur de ce cycle.
4.4.2 Listes persistantes
Le caractre modiable d'une liste n'est pas sans danger. Supposons par exemple que
nous soyons parvenu la situation suivante aprs plusieurs tapes de constructions de
listes :
4.5. Listes doublement chanes
x
61
1
x pointe sur une liste 0 1 2 3 4 et la variable y pointe sur une liste
5 2 3 4 mais, dtail important, elles partagent une partie de leurs lments,
savoir la queue de liste 2 3 4. Si maintenant le dtenteur de la variable x dcide de
modier le contenu de la liste dsigne par x, par exemple pour remplacer la valeur 3 par
La variable
17, ou encore d'en modier la structure, pour faire reboucler l'lment 4 vers l'lment 2,
alors ce changement aectera la liste
galement. cet gard, c'est la mme situation
d'alias que nous avons dj voque avec les tableaux (voir page 19).
Il existe de nombreuses situations dans lesquelles on sait pertinemment qu'une liste
ne sera pas modie aprs sa cration. On peut donc chercher le garantir, comme un
invariant du programme. Une solution consisterait faire des champs
element
et
next
des champs privs et n'exporter que des mthodes qui ne modient pas les listes. Une
solution encore meilleure consiste dclarer les champs
element
et
next
comme
final.
Ceci implique qu'ils ne peuvent plus tre modis au-del du constructeur (le compilateur
le vrie), ce qui est exactement ce que nous recherchons.
class Singly {
final int element;
final Singly next;
...
}
Ds lors, une situation de partage telle que celle illustre ci-dessus n'est plus problmatique. En eet, la portion de liste partage ne peut tre modie ni par le dtenteur de
ni par celui de
y,
et donc son partage ne prsente plus aucun danger. Au contraire, il
permet mme une conomie d'espace.
Lorsqu'une structure de donnes ne fournit aucune opration permettant d'en modier
le contenu, on parle de structure de donnes
persistante
ou
immuable .
Un style de pro-
grammation qui ne fait usage que de structures de donnes persistantes est dit
purement
applicatif. L'un des intrts de ce style de programmation est une diminution signicative
du risque d'erreurs dans les programmes. En particulier, il devient beaucoup plus facile
de raisonner sur le code, en utilisant le raisonnement mathmatique usuel, sans avoir
se soucier constamment de l'tat des structures de donnes. Un autre avantage est la
possibilit d'un
partage
signicatif entre direntes structures de donnes, et une possible
conomie substantiel de mmoire. Nous reviendrons sur la notion de persistance dans le
chapitre 6 consacr aux arbres.
4.5 Listes doublement chanes
simplement chanes, c'est--dire o
chaque lment contient un pointeur vers l'lment suivant dans la liste. Rien n'exclut,
cependant, d'ajouter un pointeur vers l'lment prcdent dans la liste. On parle alors de
liste doublement chane. Une classe Doubly pour de telles listes, contenant toujours des
Jusqu' ici, nous avons dni et utilis des listes
entiers, possde donc les trois champs suivants
62
Chapitre 4. Listes chanes
class Doubly {
int element;
Doubly next, prev;
...
o
et
next est le pointeur vers l'lment suivant, comme pour les listes simplement chanes,
prev le pointeur vers l'lment prcdent. Une liste rduite un unique lment peut
tre alloue avec le constructeur suivant :
Doubly(int element) {
[Link] = element;
[Link] = [Link] = null;
}
Bien entendu, on pourrait aussi crire un constructeur naturel qui prend en arguments les
valeurs des trois champs. On ne le fait pas ici, et on choisit plutt un style de construction
de listes o de nouveaux lments seront insrs avant ou aprs des lments existants.
insertAfter(int v) qui ajoute un nouvel ldsign par this. On commence par construire le
crivons ainsi une mthode dynamique
ment de valeur
nouvel lment
v juste aprs l'lment
e, avec le constructeur
ci-dessus.
void insertAfter(int v) {
Doubly e = new Doubly(v);
Il faut maintenant mettre jour les dirents pointeurs
this
et
e.
next
et
prev
De manire vidente, on indique que l'lment qui prcde
pour lier ensemble
est
this.
[Link] = this;
this est e. Cependant, il y avait
[Link], et il convient alors
e et [Link].
Inversement, on souhaite indiquer que l'lment qui suit
peut-tre un lment aprs
this,
c'est--dire dsign par
de commencer par mettre jour les pointeurs entre
if ([Link] != null) {
[Link] = [Link];
[Link] = e;
}
Enn, on peut mettre jour
[Link], ce qui achve le code le la mthode insertAfter.
[Link] = e;
En utilisant le constructeur de la mthode
insertAfter, on peut construire la liste conte-
nant les entiers 1, 2, 3, dans cet ordre, en commenant par construire l'lment de valeur
1, puis en insrant successivement 3 et 2 aprs 1.
Doubly x = new Doubly(1);
[Link](3);
[Link](2);
4.5. Listes doublement chanes
63
On a donc allou trois objets en mmoire, qui se rfrencent mutuellement de la manire
suivante :
Doubly
1
Doubly
2
Doubly
3
Il est important de noter que la situation tant devenue compltement symtrique, il
n'y a pas forcment lieu de dsigner la liste par son premier lment (l'lment de
valeur 1 dans l'exemple ci-dessus). On pourrait tout aussi bien se donner un pointeur sur
le dernier lment. Cependant, une liste doublement chane tant, entre autres choses,
une liste simplement chane, on peut continuer lui appliquer le mme vocabulaire. Le
premier lment est donc celui dont le champ
champ
next
vaut
Exercice 4.8.
null.
prev
crire de mme une mthode
lment de valeur
juste avant
Suppression d'un lment.
this.
vaut
null
et le dernier celui dont le
insertBefore(v)
qui insre un nouvel
Une proprit remarquable des listes doublement chanes
e de la liste sans connatre rien d'autre que
sa propre valeur (son pointeur). En eet, ses deux champs prev et next nous donnent
l'lment prcdent et l'lment suivant et il sut de les lier entre eux pour que e soit
eectivement retir de la liste. crivons une mthode dynamique remove() qui supprime
l'lment this de la liste dont il fait partie. Elle est aussi simple que
est qu'il est possible de supprimer un lment
void remove() {
if ([Link] !=
[Link]
if ([Link] !=
[Link]
}
null)
= [Link];
null)
= [Link];
La seule dicult consiste correctement traiter les cas o
nuls, c'est--dire lorsque
this
[Link] ou [Link] sont
se trouve tre le premier ou le dernier lment de la liste
(voire les deux la fois). Pour eectuer une telle suppression dans une liste simplement
chane, il nous faudrait galement un pointeur sur l'lment qui prcde
Il est important de noter que la mthode
remove
dans la liste.
n'a pas l'eet escompt si elle est
applique au premier lment de la liste. Dans l'exemple ci-dessus, un appel
[Link]()
a eectivement pour eet de supprimer l'lment 1 de la liste, la rduisant une liste
x continue de pointer sur l'lment 1. Par
prev et next de cet lment n'ont pas t modis. Ainsi, un parcours
ne contenant plus que 2 et 3, mais la variable
ailleurs, les champs
de la liste
donnera toujours les valeurs 1, 2, 3.
Pour y remdier, plusieurs solutions peuvent tre utilises. On peut par exemple placer
aux deux extrmits de la liste deux lments ctifs, qui ne seront pas considrs comme
faisant partie de la liste et qui ne seront jamais supprims. On parle de
sentinelles . Mieux
encore, on peut encapsuler la liste doublement chane dans un objet qui maintient des
pointeurs vers son premier et son dernier lment, exactement comme nous l'avons fait
pour raliser des piles et des les avec des listes simplement chanes page 54.
64
Chapitre 4. Listes chanes
Programme 9 Listes doublement chanes
class Doubly {
int element;
Doubly next, prev;
Doubly(int element) {
[Link] = element;
[Link] = [Link] = null;
}
void insertAfter(int v) {
Doubly e = new Doubly(v);
[Link] = this;
if ([Link] != null) {
[Link] = [Link];
[Link] = e;
}
[Link] = e;
}
void remove() {
if ([Link] !=
[Link]
if ([Link] !=
[Link]
}
null)
= [Link];
null)
= [Link];
Le code complet des listes doublement chanes est donn programme 9 page 64.
La bibliothque Java fournit une classe gnrique de listes doublement chanes dans
[Link]<E>.
Application : le problme de Josephus.
Utilisons la structure de liste doublement
chane pour rsoudre le problme suivant, dit problme de Josephus. Des joueurs sont
placs en cercle. Ils choisissent un entier
et procdent alors une lection de la manire
suivante. Partant du joueur 1, ils comptent jusqu'
et liminent le
p-ime joueur, qui
p-ime joueur,
sort du cercle. Puis, partant du joueur suivant, ils liminent de nouveau le
et ainsi de suite jusqu' ce qu'il ne reste plus qu'un joueur. Si
joueurs au dpart, on note
J(n,p)
dsigne le nombre de
le numro du joueur ainsi lu. Avec
n=7
et
p=5
limine successivement les joueurs 5, 3, 2, 4, 7, 1 et le gagnant est donc le joueur 6
on
i.e.
J(7,5) = 6.
crivons une mthode statique
josephus(int n, int p)
qui calcule la valeur de
4.5. Listes doublement chanes
65
J(n,p) en utilisant une liste doublement chane cyclique, reprsentant le cercle des joueurs.
La mthode remove ci-dessus pourra alors tre utilise directement pour liminer un
joueur. On commence par crire une mthode statique circle(int n) qui construit une
liste doublement chane cyclique de longueur n. Elle commence par crer le premier
lment, de valeur 1 (on suppose ici n 1).
static Doubly circle(int n) {
Doubly l1 = new Doubly(1);
Puis elle ajoute successivement tous les lments
n, . . . ,2
juste aprs l'lment 1. En
procdant dans cet ordre, on aura bien au nal l'lment 2 juste aprs l'lment 1.
for (int i = n; i >= 2; i--) {
[Link](i);
La dicult consiste refermer correctement la liste, pour crer le cycle (le code crit
jusqu' prsent ne permet que de construire des listes termines par
n. l'issue
dans la boucle,
Pour cela, il sut de lier ensemble les lments 1 et
pas ais de retrouver l'lment
n.
On le fait donc
null de chaque ct).
de la boucle, il ne sera
lorsque
vaut
n.
if (i == n) { [Link] = [Link]; [Link] = l1; }
En ralit, ce test est positif ds le premier tour de boucle (et seulement au premier). Mais
le traiter l'extrieur de la boucle nous obligerait faire un cas particulier pour
n = 1.
l'issue de la boucle, on renvoie le premier lment.
return l1;
On passe maintenant la mthode
circle
pour construire le cercle
josephus.
Elle commence par appeler la mthode
des joueurs.
static int josephus(int n, int p) {
Doubly c = circle(n);
Puis elle eectue une boucle tant qu'il reste plus d'un joueur dans le cercle. On teste cette
condition en comparant
et
[Link].
while (c != [Link]) {
Il s'agit ici d'une comparaison
physique
(avec
!=),
qui compare les valeurs en tant que
pointeurs. chaque tour de boucle, on procde l'limination d'un joueur. Pour cela, on
p 1 fois dans le cercle, avec c = [Link], puis on limine le joueur c ainsi obtenu
[Link].
avance
avec
for (int i = 1; i < p; i++)
c = [Link];
[Link]();
Puis on passe l'lment suivant. Bien que
next
c vient d'tre supprim de la liste, son pointeur
dsigne toujours l'lment suivant dans la liste. On peut donc crire
66
Chapitre 4. Listes chanes
Programme 10 Le problme de Josephus
// construit la liste circulaire 1,2,...,n et renvoie l'lment 1
static Doubly circle(int n) {
Doubly l1 = new Doubly(1);
for (int i = n; i >= 2; i--) {
[Link](i);
if (i == n) { [Link] = [Link]; [Link] = l1; }
}
return l1;
}
static int josephus(int n, int p) {
Doubly c = circle(n);
while (c != [Link]) { // tant qu'il reste plus d'un joueur
for (int i = 1; i < p; i++)
c = [Link];
[Link](); // on limine le p-ime
c = [Link];
}
return [Link];
}
c = [Link];
Ceci achve la boucle
while.
Le gagnant est le dernier lment dans la liste.
return [Link];
Le code complet est donn programme 10 page 66. Pour plus de dtails concernant ce
problme, et notamment une solution analytique, on pourra consulter
matics
Concrete Mathe-
[3, Sec. 1.3].
Exercice 4.9.
Rcrire la mthode
josephus en utilisant une liste cyclique simplement
chane. Indication : dans la boucle interne, conserver un pointeur sur l'lment prcdent,
de manire pouvoir supprimer facilement le
Exercice 4.10.
Rcrire la mthode
p-ime
josephus
lment en sortie de boucle.
en utilisant un tableau d'entiers plutt
qu'une liste chane.
4.6 Code gnrique
crire une version gnrique des listes chanes est immdiat. Il sut de paramtrer les
classes
Singly
on crit donc
et
Doubly
par le type
des lments. Pour les listes simplement chanes,
4.6. Code gnrique
67
class Singly<E> {
E element;
Singly<E> next;
et pour les listes doublement chanes
class Doubly<E> {
E element;
Doubly<E> next, prev;
Le reste du code est le mme, au remplacement prs de
int par E aux endroits opportuns.
68
Chapitre 4. Listes chanes
Tables de hachage
Supposons qu'un programme ait besoin de manipuler un
ensemble
de chanes de ca-
ractres (des noms de personnes, des mot-cls, des URL, etc.) de telle manire que l'on
puisse, ecacement, d'une part ajouter une nouvelle chane dans l'ensemble, et d'autre
part chercher si une chane appartient l'ensemble. Avec les structures de donnes vues
jusqu' prsent, c'est--dire les tableaux et les listes, ce n'est pas facile. L'ajout peut
certes se faire en temps constant par exemple au dbut ou la n d'une liste ou la
n d'un tableau redimensionnable mais la recherche prendra un temps
semble contient
O(n)
si l'en-
lments. Bien entendu, on peut acclrer la recherche en maintenant
les chanes dans un tableau tri, mais c'est alors l'ajout qui prendra un temps
O(n)
dans
le pire des cas. Dans ce chapitre, nous prsentons une solution simple et ecace ce
problme : les tables de hachage.
L'ide est trs simple. Si les lments taient des entiers entre 0 et
directement un tableau de taille
m.
m 1, on utiliserait
Comme ce n'est pas le cas, on va se ramener cette
situation en utilisant une fonction f associant aux direntes chanes un entier dans
0..m 1. Bien entendu, il est impossible de trouver une telle fonction injective en gnral.
Il va donc falloir grer les collisions, c'est--dire les cas o deux ou plusieurs chanes
ont la mme valeur par f . Dans ce cas, on va les stocker dans un mme paquet . Si
la rpartition entre les dirents paquets est quilibre, alors chaque paquet ne contient
qu'un petit nombre de chanes. On peut alors retrouver rapidement un lment car il ne
reste plus qu' le chercher dans son paquet. Si on ralise chaque paquet par une simple
liste chane, ce qui convient parfaitement, une table de hachage n'est au nal rien d'autre
qu'un tableau de listes.
Considrons par exemple une table de hachage constitue de
m=7
paquets et conte-
nant les 5 chanes de caractres suivantes :
"", "We like", "the codes", "in", "Java."
Pour dnir la fonction
hachage,
f,
on commence par dnir une fonction
h,
fonction de
la fonction f
appele
associant un entier quelconque chaque lment, puis on dnit
comme
f (s) = h(s)
Ainsi, l'opration
modulo
garantit que la valeur de
que l'on prenne simplement pour
structure suivante :
mod
h(s)
m.
f
est bien dans
la longueur de la chane
s.
0..m 1.
Supposons
Alors on obtient la
70
Chapitre 5. Tables de hachage
0
1
2
3
4
5
6
""
"we like"
"the codes"
"in"
"Java."
"in", respectivement de
longueurs 9 et 2, car ces deux chanes ont pour image 2 par la fonction f . (Mais l'ordre dans
Ainsi le paquet 2 contient les deux chanes
"the codes"
et
lequel ces deux chanes apparaissent dans la liste peut varier suivant l'ordre d'insertion
des lments dans la table.)
5.1 Ralisation
HashTable. On commence par
Bucket, pour reprsenter les paquets
Ralisons une telle table de hachage dans une classe
introduire une classe de liste simplement chane,
(en anglais on parler de seau plutt que de paquet ).
class Bucket {
String element;
Bucket next;
Bucket(String element, Bucket next) {
[Link] = element;
[Link] = next;
}
}
La classe
HashTable
ne contient qu'un seul champ, savoir le tableau des dirents
paquets :
class HashTable {
private Bucket[] buckets;
Pour crire le constructeur, il faut se donner une valeur pour
m, c'est--dire un nombre de
paquets. Idalement, cette taille devrait tre du mme ordre de grandeur que le nombre
d'lments qui seront stocks dans la table. L'utilisateur pourrait ventuellement fournir
cette information, par exemple sous la forme d'un argument du constructeur, mais ce n'est
pas toujours possible. Considrons donc pour l'instant une situation simplie o cette
taille est une constante compltement arbitrairement, savoir
m = 17.
final private static int M = 17;
HashTable() {
[Link] = new Bucket[M];
}
(Nous verrons plus loin comment supprimer le caractre arbitraire de cette constante.) On
procde alors l'criture de la fonction de hachage proprement dite. Il y a de nombreuses
faons de la choisir, plus ou moins heureuses. De faon un peu moins nave que la simple
longueur de la chane, on peut chercher combiner les valeurs des dirents caractres de
la chane, comme par exemple
5.1. Ralisation
71
private int hash(String s) {
int h = 0;
for (int i = 0; i < [Link](); i++)
h = [Link](i) + 19 * h;
return (h & 0x7fffffff) % M;
}
Il s'agit ni plus ni moins de l'valuation (par la mthode de Horner) d'un polynme dont
les coecients seraient les caractres de
en un point compltement arbitraire, savoir
ici 19. Quoique l'on fasse, le plus important rside dans la dernire ligne, qui assure que
la valeur nale est bien un indice lgal dans le tableau
soin de masquer le bit de signe (avec
& 0x7fffffff)
buckets.
En particulier, on a pris
pour obtenir une valeur positive ou
nulle avant de prendre le modulo . En eet, l'opration de modulo
donne un rsultat
du mme signe que son premier argument, qui peut tre ngatif ici en cas de dbordement
de la capacit du type
int.
s dans la table de hachage. C'est d'une
paquet, avec la mthode hash, et on ajoute
On en arrive l'opration d'ajout d'une chane
simplicit enfantine : on calcule l'indice du
simplement
en tte de la liste correspondante.
void add(String s) {
int i = hash(s);
[Link][i] = new Bucket(s, [Link][i]);
}
Une variante consisterait vrier que
ne fait pas dj partie de cette liste (voir no-
tamment l'exercice 5.2). Mais la version ci-dessus a l'avantage de garantir une complexit
O(1)
dans tous les cas. Et on peut parfaitement tre dans une situation o on sait que
ne fait pas partie de la table par exemple parce qu'on a eectu le test d'appartenance
au pralable.
Pour raliser le test d'appartenance, justement, on procde de la mme faon, en
utilisant la mthode
hash
pour dterminer dans quel paquet la chane rechercher doit
se trouver, si elle est prsente. Pour chercher dans la liste correspondante, on ajoute par
exemple une mthode statique
contains
la classe
Bucket
static boolean contains(Bucket b, String s) {
for (; b != null; b = [Link])
if ([Link](s)) return true;
return false;
}
La mthode
contains
de la classe
HashTable
est alors rduite une seule ligne :
boolean contains(String s) {
return [Link]([Link][hash(s)], s);
}
Le code complet est donn programme 11 page 72.
1. En revanche, il ne serait pas correct d'crire [Link](h) % M car si h est gal au plus petit entier,
c'est--dire 231 = 2147483648, alors [Link](h) vaudra 2147483648 et le rsultat de hash sera
ngatif.
72
Chapitre 5. Tables de hachage
Programme 11 Tables de hachage
class Bucket {
String element;
Bucket next;
Bucket(String element, Bucket next) {
[Link] = element;
[Link] = next;
}
static boolean contains(Bucket b, String s) {
for (; b != null; b = [Link])
if ([Link](s)) return true;
return false;
}
}
class HashTable {
private Bucket[] buckets;
final private static int M = 17;
HashTable() {
[Link] = new Bucket[M];
}
private int hash(String s) {
int h = 0;
for (int i = 0; i < [Link](); i++)
h = [Link](i) + 19 * h;
return (h & 0x7fffffff) % M;
}
void add(String s) {
int i = hash(s);
[Link][i] = new Bucket(s, [Link][i]);
}
boolean contains(String s) {
return [Link]([Link][hash(s)], s);
}
5.2. Redimensionnement
Exercice 5.1.
73
Ajouter un champ priv
size la classe HashTable contenant le nombre
int size() renvoyant sa valeur. Pour
total d'lments de la table, ainsi qu'une mthode
size
quoi le champ
Exercice 5.2.
doit-il tre priv ?
Ajouter une mthode
void remove(String s)
pour supprimer un l-
s de la table de hachage. Discuter de la pertinence d'exclure les doublons l'intrieur
chaque paquet. Quel est l'impact sur la mthode add ?
ment
de
5.2 Redimensionnement
Le code que nous venons de prsenter est en pratique trop naf. Le nombre d'lments
contenus dans la table peut devenir grand par rapport la taille du tableau. Cette
charge
implique de gros paquets, qui dgradent les performances des oprations (ici seulement
contains).
de l'opration
quement,
mthode
dulo
Pour y remdier, il faut modier la taille du tableau
dynami-
en fonction de la charge de la table. On commence par modier lgrement la
hash
pour obtenir une valeur modulo
[Link]
et non plus mo-
return (h & 0x7fffffff) % [Link];
Puis on choisit une stratgie de redimensionnement. Par exemple, on peut choisir de
m du tableau ds que le nombre total d'lments dpasse m/2. On suppose
l'on a ajout un champ size la classe HashTable qui contient le nombre
doubler la taille
pour cela que
total d'lments (voir l'exercice 5.1 ci-dessus). Il sut alors d'ajouter une ligne au dbut
(ou la n) de la mthode
add pour appeler une mthode de redimensionnement resize
si ncessaire.
void add(String s) {
if ([Link] > [Link]/2) resize();
...
Tout le travail se fait dans cette nouvelle mthode
resize.
On commence par calculer la
nouvelle taille du tableau, comme le double de la taille actuelle :
private void resize() {
int n = 2 * [Link];
Puis on alloue un nouveau tableau de cette taille-l dans
[Link],
sans oublier de
conserver un pointeur sur son ancienne valeur :
Bucket[] old = [Link];
[Link] = new Bucket[n];
old vers
[Link]. On le fait en parcourant toutes les listes de old avec
pour chaque liste, tous ses lments avec une seconde boucle for :
Enn, on re-hache toutes les valeurs de l'ancien tableau
le nouveau tableau
une boucle
for (Bucket b : old)
for (; b != null; b = [Link]) {
int i = hash([Link]);
[Link][i] = new Bucket([Link], [Link][i]);
}
for
et,
74
Chapitre 5. Tables de hachage
hash a t modie pour rendre une valeur
[Link]. Le reste du code de la classe HashTable est inchang,
en particulier la mthode contains. Pour ce qui est de l'initialisation, on peut continuer
utiliser la constante arbitraire M pour allouer le tableau, car il sera agrandi si ncessaire.
Tout se passe correctement, car la mthode
modulo la taille de
Complexit.
Nous n'avons pas choisi la stratgie consistant doubler la taille du ta-
bleau par hasard. Exactement comme nous l'avons fait pour les tableaux redimensionnables (voir page 43), on peut montrer que l'insertion successive de
table de hachage aura un cot total
O(n).
Certains appels
add
lments dans la
sont plus coteux que
d'autres, et mme d'une complexit proportionnelle au nombre d'lments dj dans la
table, mais la
complexit amortie
de
add
reste
O(1).
La complexit de la recherche est plus dicile valuer, car elle dpend de la qualit
de la fonction de hachage. Si la fonction de hachage envoie tous les lments dans le
mme paquet c'est le cas par exemple si elle est constante alors la complexit de
contains
sera clairement
O(n).
Si au contraire la fonction de hachage rpartit bien les
lments dans les dirents paquets, alors la taille de chaque paquet peut tre borne
par une constante et la complexit de
contains
sera alors
O(1).
La mise au point d'une
fonction de hachage se fait empiriquement, par exemple en mesurant la taille maximale
et moyenne des paquets. Sur des types tels que des chanes de caractres, ou encore des
tableaux d'entiers, une fonction telle que celle que nous avons donne plus haut donne
des rsultats trs satisfaisants.
5.3 Code gnrique
Bien entendu, la structure de table de hachage que nous venons de prsenter s'adapte
facilement des lments autres que des chanes de caractres. Il sut pour cela de
hash) et d'autre part
equals de la classe String).
modier d'une part la fonction de hachage (ici notre mthode
l'galit utilise pour comparer les lments (ici la mthode
La bibliothque Java propose justement une version gnrique des tables de hachage, sous
[Link]<E> pour des ensembles dont les lments sont
[Link]<K, V> pour des dictionnaires associant
des valeurs de type K des valeurs de type V.
Dans les deux cas, il faut quiper les types E et K d'une fonction de hachage et d'une
galit adaptes. On le fait en rednissant les mthodes int hashCode() et boolean
equals(Object) hrites de la classe Object. Si par exemple on dnit une classe Pair
la forme d'une classe
d'un type
et d'une classe
pour des paires de chanes de caractres, de la forme
class Pair {
String fst, snd;
...
}
alors il conviendra de l'quiper d'une fonction de hachage d'une part, par exemple en
faisant la somme des valeurs de hachage des deux chanes
fst
et
public int hashCode() {
return [Link]() + [Link]();
}
snd
5.4. Brve comparaison des tableaux, listes et tables de hachage
75
et d'une galit structurelle d'autre part en comparant les deux paires membre membre.
Il y a l une subtilit : la mthode
argument de type
Object
equals
est dnie dans la classe
Object
avec un
et il faut donc respecter ce prol de mthode pour la rednir.
On doit donc crire
public boolean equals(Object o) {
Pair p = (Pair)o;
return [Link]([Link]) && [Link]([Link]);
}
(Pair)o est une conversion explicite car potentiellement non sre de la classe
Object vers la classe Pair. Si cette mthode equals n'est utilise que depuis le code de
HashSet<Pair> ou de HashMap<Pair, V>, on a la garantie que cette conversion n'chouera
jamais. En eet, le typage de Java nous garantit qu'un ensemble de type HashSet<Pair>
(resp. un dictionnaire de type HashMap<Pair, V>) ne pourra contenir que des lments
(resp. des cls) de type Pair.
o
Il convient d'expliquer soigneusement un pige dans lequel on aurait pu facilement
tomber. Naturellement, on aurait plutt crit la mthode suivante :
public boolean equals(Pair p) {
return [Link]([Link]) && [Link]([Link]);
}
Mais, bien qu'accept par le compilateur, ce code ne donne pas les rsultats attendus.
equals est maintenant surcharge et non plus rednie : il y a
deux mthodes equals, l'une prenant un argument de type Object et l'autre prenant un
argument de type Pair. Comme le code de HashSet et HashMap est crit en utilisant la
mthode equals ayant un argument de type Object (mme si cela peut surprendre), alors
c'est la premire qui est utilise, c'est--dire celle directement hrite de la classe Object.
Il se trouve qu'elle concide avec l'galit physique, c'est--dire avec l'opration ==, ce qui
n'est pas en accord avec l'galit structurelle que nous souhaitons ici sur le type Pair.
Quelle que soit la faon de rednir les mthodes hashCode et equals, il convient de
En eet, la mthode
toujours maintenir la proprit suivante :
xy, [Link](y ) [Link]() = y .hashCode()
Autrement dit, des lments gaux doivent tre rangs dans le mme seau.
5.4 Brve comparaison des tableaux, listes et tables de
hachage
On peut chercher comparer les structures de donnes que nous avons dj vues,
savoir les tableaux, les listes et les tables de hachage, du point de vue des oprations
ensemblistes que sont l'ajout d'un lment (add), l'accs au
i-ime
lment (get) et la
recherche d'un lment (contains). Les direntes complexits sont les suivantes :
tableau
tableau tri
liste
table de hachage
add
O(1) amorti
O(n)
O(1)
O(1) amorti
get
O(1)
O(1)
O(i)
contains
O(n)
O(log n)
O(n)
O(1)
76
Chapitre 5. Tables de hachage
Pour une table de hachage, l'accs au
i-ime
lment n'est pas dni. Il est cependant
possible de combiner les structures de table de hachage et de liste chane pour conserver,
ct d'une table de hachage, l'ordre d'insertion des lments. Les bibliothques Java
[Link]<E>
et
[Link]<E>
font cela.
Arbres
La notion d'arbre est dnie rcursivement. Un arbre est un ensemble ni de
tiquets par des valeurs, o un nud particulier
nuds,
racine de l'arbre et
sous-arbres ) de r. En
est appel la
les autres nuds forment des arbres disjoints appels les
ls
(ou
informatique, les arbres poussent vers le bas. Ainsi
A
B
C
E
D
F
reprsente un arbre de racine A ayant trois ls. Un nud qui ne possde aucun ls est
appel une
feuille .
Les feuilles de l'arbre ci-dessus sont B, E, F et G. La
hauteur
d'un
arbre est dnie comme le nombre de nuds le long du plus long chemin de la racine
une feuille (ou, de manire quivalente, comme la longueur de ce chemin, plus un). La
hauteur de l'arbre ci-dessus est donc trois.
La notion d'arbre
binaire
est galement dnie rcursivement. Un arbre binaire est
soit vide, soit un nud possdant exactement deux ls appels ls gauche et ls droit. Un
arbre binaire n'est pas un cas particulier d'arbre, car on distingue les sous-arbres gauche
et droit (on parle d'arbre positionnel). Ainsi, les deux arbres suivants sont distincts :
A
B
A
B
On montre facilement qu'un arbre binaire de hauteur
par rcurrence forte sur
hauteurs au plus
h.
Pour
h = 0,
c'est clair. Pour
1, d'o un total d'au plus 1 + 2h1
possde au plus
2h 1
nuds,
h > 0, on a deux sous-arbres de
1 + 2h1 1 = 2h 1 nuds.
6.1 Reprsentation des arbres
Considrons pour l'instant des arbres binaires uniquement, dont les nuds contiennent
des entiers. Une classe
Tree
pour reprsenter les nuds de tels arbres est donc
78
Chapitre 6. Arbres
class Tree {
int value;
Tree left, right;
}
o les champs
left
et
right
contiennent respectivement le ls gauche et le ls droit.
L'arbre vide est reprsent par
null.
Cette reprsentation n'est en rien dirente de
la reprsentation d'une liste doublement chane (voir page 64), aux noms des champs
prs. Ce qui change, c'est l'invariant de structure. Pour une liste doublement chane, la
structure impose par construction tait linaire : tout lment suivait son prcdent et
prcdait son suivant. Ici la structure impose par construction sera celle d'un arbre. En
Tree, et si B, D, E et F sont des entiers,
on peut construire un arbre avec l'expression new Tree(new Tree(new Tree(B, null,
null), D, null), E, new Tree(null, F, null)). On le dessine de faon simplie,
supposant dni le constructeur naturel de la classe
sans expliciter les objets comme des petites botes avec des champs ; cela prendrait trop
de place.
E
D
Comme nous l'avons expliqu plus haut avec les listes (voir section 4.4.2), on peut garantir
le caractre immuable d'un arbre, c'est--dire en faire une structure de donnes persistante,
en ajoutant simplement le qualicatif
final
ses trois champs :
class Tree {
final int value;
final Tree left, right;
}
Plus loin dans ce chapitre, nous montrerons d'autres faons de construire des arbres, dans
les sections 6.4 et 6.5.
6.2 Oprations lmentaires sur les arbres
La mthode la plus simple crire sur un arbre est srement celle qui compte le
nombre de ses lments. On procde naturellement rcursivement :
static int size(Tree t) {
if (t == null) return 0;
return 1 + size([Link]) + size([Link]);
}
Il est facile de montrer que sa complexit est proportionnelle au nombre de nuds de
l'arbre. Ce code est particulirement simple mais il possde nanmoins le dfaut d'un ventuel dbordement de pile, c'est--dire le dclenchement d'une exception
StackOverflowError,
si la hauteur de l'arbre est trs grande. (Les exercices 6.1 et 6.2 proposent de le vrier.)
6.3. Arbres binaires de recherche
79
Pour y remdier, il faudrait rcrire la mthode
size
avec une boucle. Mais, la di-
rence d'une liste chane pour laquelle nous aurions pu calculer la longueur l'aide d'une
boucle, on ne voit pas ici comment faire cela facilement. C'est en fait possible
mais nous
allons ignorer ce problme pour l'instant. Dans de nombreuses situations o les arbres
sont utiliss, leur hauteur est limite (voir par exemple la section 6.3.2 ci-dessous) il n'y
a donc pas lieu de se soucier d'un ventuel
Exercice 6.1.
un arbre
StackOverflowError.
crire une mthode statique
linaire gauche
contenant
Tree leftDeepTree(int n)
qui construit
nuds. Un arbre linaire gauche est un arbre o
chaque nud ne possde pas de ls droit.
Exercice 6.2.
Dterminer une valeur de
Exercice 6.3.
crire une mthode statique
n pour laquelle le rsultat de leftDeepTree(n)
provoque une exception StackOverflowError lorsqu'il est pass en argument la mthode
size.
d'un arbre.
int height(Tree t) qui renvoie la hauteur
Parcours.
De mme que nous avions crit des mthodes parcourant les lments d'une
liste chane, on peut chercher parcourir les lments d'un arbre, par exemple pour les
acher tous. Supposons par exemple que l'on veuille acher les lments de la gauche
vers la droite , c'est--dire d'abord les lments du ls gauche, puis la racine, puis les
lments du ls droit. L encore, il est naturel de procder rcursivement, et le parcours
est aussi simple que
static void inorderTraversal(Tree t) {
if (t == null) return;
inorderTraversal([Link]);
[Link]([Link]);
inorderTraversal([Link]);
}
Un tel parcours est appel un
parcours inxe
de l'arbre (inorder
traversal
en anglais). Si
on ache la valeur de la racine avant le parcours du ls gauche (resp. aprs le parcours
du ls droit) on parle de
parcours prxe
(resp.
postxe )
de l'arbre.
6.3 Arbres binaires de recherche
Si les lments qui tiquettent les nuds d'un arbre sont totalement ordonns c'est
le cas des entiers, par exemple alors il est possible de donner plus de structure un
arbre binaire en maintenant l'invariant suivant :
Pour tout nud de l'arbre, de valeur
sont plus petits que
x,
les lments situs dans le ls gauche
et ceux situs dans le ls droit sont plus grands que
1. Il est mme toujours possible de remplacer une fonction rcursive par une boucle.
x.
80
Chapitre 6. Arbres
arbre binaire de recherche. En particulier, on en dduit que les lments
On appelle cela un
apparaissent dans l'ordre croissant lorsque l'arbre est parcouru dans l'ordre inxe. Nous
allons exploiter cette structure pour crire des oprations de recherche et de modication
ecaces. Par exemple, chercher un lment dans un arbre binaire de recherche ne requiert
pas de parcourir tout l'arbre : il sut de descendre gauche ou droite selon la comparaison entre l'lment recherch et la racine de l'arbre. Dans ce qui suit, on considre
des arbres binaires de recherche
persistants
dont les valeurs sont des entiers. On se donne
donc la classe suivante pour les reprsenter.
class BST {
final int value;
final BST left, right;
}
6.3.1 Oprations lmentaires
Plus petit lment.
La structure d'arbre binaire de recherche permet notamment
d'obtenir facilement son plus petit lment. Il sut en eet de descendre le long de la
branche gauche, tant que cela est possible. La mthode
une boucle
while
getMin
ralise ce parcours, avec
static int getMin(BST b) {
while ([Link] != null) b = [Link];
return [Link];
}
Elle suppose que
n'est pas
null
et contient donc au moins un lment. Cette mthode
sera rutilise plus loin pour crire la mthode de suppression dans un arbre binaire de
recherche. Le cas de
null
y sera alors trait de faon particulire.
Recherche d'un lment.
La recherche d'un lment
consiste descendre dans
l'arbre jusqu' ce qu'on atteigne soit un nud contenant la valeur
le nud ne contient pas
x,
x,
soit
null.
Lorsque
la proprit d'arbre binaire de recherche nous indique de quel
ct poursuivre la descente. L encore, on peut crire cette descente sous la forme d'une
boucle
while.
static boolean contains(BST b, int x) {
while (b != null) {
if (x == [Link]) return true;
b = (x < [Link]) ? [Link] : [Link];
}
return false;
}
Exercice 6.4.
Rcrire la mthode
contains
rcursivement.
6.3. Arbres binaires de recherche
Insertion d'un lment.
cherche
81
x dans un arbre binaire de reb, en suivant le mme principe
immuables, on crit une mthode add qui
L'insertion d'un lment
consiste trouver l'emplacement de
que pour la recherche. Les arbres tant ici
dans
renvoie un nouvel arbre, c'est--dire
static BST add(BST b, int x) {
On va procder rcursivement. Si
nant uniquement
x.
est vide, on se contente de construire un arbre conte-
if (b == null)
return new BST(x);
Dans l'autre cas, on compare l'lment
la racine de
b,
et on poursuit rcursivement
l'insertion gauche ou droite lorsque la comparaison est stricte :
if (x < [Link])
return new BST(add([Link], x), [Link], [Link]);
if (x > [Link])
return new BST([Link], [Link], add([Link], x));
Il est important de noter que, aprs l'appel rcursif, on reconstruit le nud de l'arbre de
[Link]. Enn, dans le dernier cas, c'est--dire si x est gal la racine de b, on se
contente de renvoyer l'arbre b inchang, ce qui achve la mthode add.
valeur
return b;
On fait ici le choix de ne pas construire d'arbre contenant de doublon, mais on aurait trs
bien pu choisir de renvoyer au contraire un arbre contenant une occurrence supplmentaire
de
x.
Le choix que nous faisons ici est cohrent avec l'utilisation des arbres binaires de
recherche que nous allons faire plus loin pour raliser une structure d'ensemble.
Exercice 6.5.
les cas,
i.e.
crire la variante de la mthode
mme si
Exercice * 6.6.
y apparat dj.
add qui ajoute x dans l'arbre dans tous
Pourquoi est-il dicile d'crire la mthode
add avec une boucle while
plutt que rcursivement ? Le problme serait-il le mme si l'arbre n'tait pas immuable ?
Suppression d'un lment.
La suppression d'un lment x dans un arbre binaire de
b procde de la mme manire que pour l'insertion, c'est--dire par une descente
rcursive vers la position potentielle de x. Si b est vide, on se contente de renvoyer l'arbre
recherche
vide.
static BST remove(BST b, int x) {
if (b == null)
return null;
Sinon, on compare l'lment
x la racine de b, et on poursuit rcursivement la suppression
gauche ou droite lorsque la comparaison est stricte :
82
Chapitre 6. Arbres
if (x < [Link])
return new BST(remove([Link], x), [Link], [Link]);
if (x > [Link])
return new BST([Link], [Link], remove([Link], x));
Lorsqu'il y a galit, en revanche, on se retrouve confront une dicult : il faut supprimer la racine de l'arbre, c'est--dire renvoyer un arbre contenant exactement les lments
[Link] et [Link], mais il n'y a pas de moyen simple de raliser cette union. On souhaite autant que possible conserver [Link] ou [Link] inchang, pour limiter la quantit
de
de nuds reconstruire. La proprit d'arbre binaire de recherche nous suggre alors de
[Link], soit le plus petit
lment de [Link]. Vu que nous avons dj une mthode getMin, nous allons pencher
pour la seconde solution. Il convient de traiter correctement le cas o [Link] ne possde
aucun lment. Dans ce cas, il sut de renvoyer [Link].
placer la racine du nouvel arbre, soit le plus grand lment de
if ([Link] == null)
return [Link];
getMin([Link]) et o ce dernier est
[Link] avec une mthode removeMin que nous allons crire dans un instant.
Sinon, on construit un arbre dont la racine est
supprim de
return new BST([Link], getMin([Link]), removeMin([Link]));
remove. crivons maintenant une mthode removeMin. C'est un
remove, beaucoup plus simple, o l'on descend uniquement gauche,
Ceci achve le code de
cas particulier de
jusqu' trouver un nud n'ayant pas de sous-arbre gauche.
static BST removeMin(BST b) {
if ([Link] == null)
return [Link];
return new BST(removeMin([Link]), [Link], [Link]);
}
b dirent de null (dans le cas
contraire, [Link] provoquerait un NullPointerException), ce qui est bien garanti par
la mthode remove. Le code complet de la classe BST est donn programme 12 page 83.
Il est important de noter que cette mthode suppose
Exercice 6.7.
On peut amliorer l'ecacit des mthodes
add
et
remove
en renvoyant
directement l'arbre pass en argument lorsqu'il est inchang, c'est--dire quand
un lment dj prsent et quand
thodes
remove
add ajoute
supprime un lment absent. Rcrire les m-
add et remove en utilisant cette ide. On pourra lever une exception pour signaler
que l'arbre est inchang, en prenant soin de ne pas la rattraper chaque appel rcursif
mais uniquement au sommet de la mthode.
Exercice 6.8.
static int floor(BST b, int x) qui renvoie le
gal x, s'il existe, et lve une exception sinon.
Ajouter une mthode
plus grand lment de
infrieur ou
6.3. Arbres binaires de recherche
Programme 12 Arbres binaires de recherche
class BST {
final int value;
final BST left, right;
BST(BST left, int value, BST right) {
[Link] = left;
[Link] = value;
[Link] = right;
}
static boolean contains(BST b, int x) {
while (b != null) {
if ([Link] == x) return true;
b = (x < [Link]) ? [Link] : [Link];
}
return false;
}
static BST add(BST b, int x) {
if (b == null)
return new BST(null, x, null);
if (x < [Link])
return new BST(add([Link], x), [Link], [Link]);
if (x > [Link])
return new BST([Link], [Link], add([Link], x));
return b; // x dj dans b
}
static int getMin(BST b) { // suppose b != null
while ([Link] != null) b = [Link];
return [Link];
}
static BST removeMin(BST b) { // suppose b != null
if ([Link] == null)
return [Link];
else
return new BST(removeMin([Link]), [Link], [Link]);
}
static BST remove(BST b, int x) {
if (b == null)
return null;
if (x < [Link])
return new BST(remove([Link], x), [Link], [Link]);
if (x > [Link])
return new BST([Link], [Link], remove([Link], x));
if ([Link] == null)
return [Link];
return new BST([Link], getMin([Link]), removeMin([Link]));
}
}
83
84
Chapitre 6. Arbres
6.3.2 quilibrage
Telles que nous venons de les crire dans la section prcdente, les direntes oprations
sur les arbres binaires de recherche ont une complexit linaire, c'est--dire
O(n) o n est
le nombre d'lments contenus dans l'arbre. En eet, notre insertion peut tout fait
conduire un peigne c'est--dire un arbre de la forme
A
B
C
D
Il sut en eet d'insrer les lments dans l'ordre A, B, C, D. Une insertion dans l'ordre
inverse donnerait de mme un peigne, dans l'autre sens. Au-del de la dgradation des performances, un tel arbre linaire peut provoquer un dbordement de pile dans les mthodes
add ou remove, se traduisant par une exception StackOverflowError.
Dans cette section, nous allons quilibrer les arbres binaires de recherche, de manire
garantir une hauteur logarithmique en le nombre d'lments. Ainsi les direntes oprations auront une complexit O(log n) et le dbordement de pile sera vit. Il existe de
rcursives telles que
nombreuses manires d'quilibrer un arbre binaire de recherche. Nous optons ici pour une
solution connue sous le nom d'AVL (de leurs auteurs Adelson-Velsky et Landis [1]). Elle
consiste maintenir l'invariant suivant :
Pour tout nud, les hauteurs de ses sous-arbres gauche et droit dirent d'au
plus une unit.
crivons une nouvelle classe
AVL pour les arbres binaires de recherche quilibrs. On
BST, laquelle on ajoute un champ height contenant la
reprend la structure de la classe
hauteur de l'arbre :
class AVL {
final int value;
final AVL left, right;
final int height;
...
Le champ
height est dclar final car sa
valeur peut tre calcule dans le constructeur.
Pour traiter correctement le cas d'un arbre vide, on se donne la mthode suivante pour
renvoyer la hauteur d'un arbre :
static int height(AVL a) {
return (a == null) ? 0 : [Link];
}
Ds lors, on peut crire un constructeur qui calcule la hauteur de l'arbre en fonction des
hauteurs de ses sous-arbres
left
et
right.
6.3. Arbres binaires de recherche
85
AVL(AVL left, int value, AVL right) {
[Link] = left;
[Link] = value;
[Link] = right;
[Link] = 1 + [Link](height(left), height(right));
}
Il n'y a pas l de circularit malsaine : la mthode
dj construit
de la construction.
d'un arbre
height permet de renvoyer la hauteur
au moment
et le constructeur s'en sert pour calculer la hauteur
Les mthodes qui ne construisent pas d'arbres, mais ne font que les consulter, sont
BST (en remplaant partout BST par AVL, bien
getMin et contains. En revanche, pour les mthodes qui construisent des arbres, c'est--dire les mthodes add, removeMin et remove,
une modication est ncessaire. En eet, on ne peut plus utiliser le constructeur new AVL
exactement les mmes que dans la classe
videmment). C'est le cas des mthodes
sans risquer de violer l'invariant des AVL. On va donc remplacer l'utilisation du constructeur par une mthode
static AVL balance(AVL l, int v, AVL r) { ... }
qui se comportera comme un constructeur, au sens o elle renverra un nouvel arbre binaire
l, ceux de r ainsi que l'lment x, mais qui pourra
aussi eectuer des oprations de rquilibrage si ncessaire (en anglais, on parle de smart
constructor ). Illustrons l'ide de ce rquilibrage sur un exemple. Si on considre l'arbre
de recherche contenant les lments de
suivant (qui est bien un AVL)
E
F
D
B
(6.1)
et que l'on insre la valeur A avec la mthode d'insertion dans les arbres binaires de
recherche, alors on obtient l'arbre
E
D
B
A
(6.2)
qui n'est pas quilibr, puisque la dirence de hauteurs entre les sous-arbres gauche et
droit du nud E est maintenant de deux. Il est nanmoins facile de rtablir l'quilibre.
En eet, il est possible d'eectuer des transformations locales sur les nuds d'un arbre
qui conservent la proprit d'arbre binaire de recherche. Un exemple de telle opration
est la
rotation droite,
qui s'illustre ainsi :
86
Chapitre 6. Arbres
n
rotation droite
x>n
x<k
x<k
k<x<n
k<x<n
Cette opration remplace la racine
par la racine
sous-arbre contenant les lments compris entre
x>n
du sous-arbre gauche et dplace le
et
n.
On note que cette opration ne
modie que deux nuds dans la structure de l'arbre. De manire symtrique, on peut
eectuer une rotation gauche. Ainsi l'arbre (6.2) peut tre rquilibr en eectuant une
rotation droite sur le sous-arbre de racine D. On obtient alors l'arbre
E
B
qui est bien un AVL. Une simple rotation, gauche ou droite, ne sut pas ncessairement
rtablir l'quilibre. Si par exemple on insre maintenant C, on obtient l'arbre
E
B
D
C
qui n'est pas un AVL. On peut alors tenter d'eectuer une rotation droite la racine E
ou une rotation gauche au nud B, mais on obtient les deux arbres suivants
B
A
E
E
D
F
qui ne sont toujours pas des AVL. Cependant, celui de droite peut tre facilement rquilibr en eectuant une rotation droite sur la racine E. On obtient alors l'AVL
D
B
A
E
C
6.3. Arbres binaires de recherche
87
lv
lv
rotation droite
r
lr
ll
lr
ll
lrv
lv
lv
rotation gauche-droite
lrv
r
ll
lrr
lrr
ll
lrl
lrl
Figure 6.1 Rotations vers la droite dans un AVL.
Cette double opration s'appelle une
rotation gauche-droite. On a videmment l'opration
symtrique de rotation droite-gauche. Ces quatre oprations, savoir les deux rotations
simples et les deux rotations doubles, susent rquilibrer les AVL en toute circonstance. crivons maintenant le code de la mthode
commence par calculer les hauteurs
hl
et
hr
balance pour les mettre en uvre. On
des deux sous-arbres gauche et droit et on
considre en premier lieu le cas o le dsquilibre est caus par le sous-arbre gauche
static AVL balance(AVL l, int v, AVL r) {
int hl = height(l), hr = height(r);
if (hl > hr + 1) {
Une simple rotation droite sut lorsque le sous-arbre gauche
haut que son sous-arbre droit
lr
ll
de
est au moins aussi
AVL ll = [Link], lr = [Link];
int lv = [Link];
if (height(ll) >= height(lr))
return new AVL(ll, lv, new AVL(lr, v, r));
l ne peut tre null ici, car hl > hr+1. Cette rotation est illustre gure 6.1
En revanche, dans le cas o ll est moins haut que lr, il faut eectuer une
On note que
(en haut).
double rotation gauche-droite.
else {
AVL lrl = [Link], lrr = [Link];
int lrv = [Link];
88
Chapitre 6. Arbres
}
return new AVL(new AVL(ll, lv, lrl), lrv, new AVL(lrr, v, r));
L encore,
lr
ne peut tre
null,
car
height(lr) > 0.
La proprit d'AVL est bien
garantie, comme le montre la gure 6.1 (en bas). On notera que le dsquilibre peut tre
lrl ou lrr, indiremment, et que dans les deux cas la double rotation gauchel
est plus haut que r), ce qui achve ce premier cas. On traite de manire symtrique le cas
o r est la cause du dsquilibre
caus par
droite rtablit bien l'quilibre. Il n'y a pas d'autre cas possible de dsquilibre (lorsque
} else if (hr > hl + 1) {
...
(le code complet est donn page 89). Enn, si les hauteurs de
et
dirent d'au plus
un, on construit directement le nud sans rquilibrage :
} else
return new AVL(l, v, r);
ce qui achve le code de la mthode
Hauteur d'un AVL.
balance. Le code complet est donn programme 13.
Montrons qu'un AVL a eectivement une hauteur logarithmique
en son nombre d'lments. Considrons un AVL de hauteur h et cherchons encadrer son
h
nombre n d'lments. Clairement n 2 1, comme dans tout arbre binaire. Inversement,
quelle est la plus petite valeur possible pour
un sous-arbre de hauteur
h1
n?
Elle sera atteinte pour un arbre ayant
et un autre de hauteur
h2
(car dans le cas contraire
on pourrait encore enlever des lments l'un des deux sous-arbres tout en conservant la
Nh le plus petit nombre d'lments dans un AVL de hauteur
Nh = 1 + Nh1 + Nh2 , ce qui se rcrit Nh + 1 = (Nh1 + 1) + (Nh2 + 1).
proprit d'AVL). En notant
h,
on a donc
On reconnat l la relation de rcurrence dnissant la suite de Fibonacci. Comme on
N1 = 0 et N1 = 1, c'est--dire N0 + 1 = 1 et N1 + 1 = 2, onen dduit
Nh + 1= Fh+2 o (Fi ) est la suite de Fibonacci. On a l'ingalit Fi > i / 5 1 o
= 1+2 5 est le nombre d'or, d'o
n Fh+2 1 > h+2 / 5 2
a par ailleurs
En prenant le logarithme ( base 2) de cette ingalit, on en dduit la majoration recherche sur la hauteur
n:
1
log2 5
h <
log2 (n + 2) +
2
log2
log2
1,44 log2 (n + 2) 0,328
en fonction du nombre d'lments
Un AVL a donc bien une hauteur logarithmique en son nombre d'lments. Comme nous
l'avons dit plus haut, cela garantit une complexit
mais aussi l'absence de
O(log n) pour les toutes les oprations,
StackOverflowError.
La bibliothque Java propose des arbres binaires de recherche quilibrs (des arbres
[Link]<E> pour des
E et la classe [Link]<K, V> pour
des dictionnaires dont les cls sont de type K et les valeurs de type V. L aussi, la hauteur
rouges et noirs en l'occurrence [2]), savoir la classe
ensembles dont les lments sont de type
des arbres est logarithmique.
6.3. Arbres binaires de recherche
Programme 13 Arbres binaires de recherche quilibrs (AVL)
class AVL {
final int value;
final AVL left, right;
final int height;
AVL(AVL left, int value, AVL right) {
[Link] = left;
[Link] = value;
[Link] = right;
[Link] = 1 + [Link](height(left), height(right));
}
static int height(AVL a) {
return (a == null) ? 0 : [Link];
}
static AVL balance(AVL l, int v, AVL r) {
int hl = height(l), hr = height(r);
if (hl > hr + 1) {
AVL ll = [Link], lr = [Link];
int lv = [Link];
if (height(ll) >= height(lr))
return new AVL(ll, lv, new AVL(lr, v, r));
else {
AVL lrl = [Link], lrr = [Link];
int lrv = [Link];
return new AVL(new AVL(ll, lv, lrl), lrv, new AVL(lrr, v, r));
}
} else if (hr > hl + 1) {
AVL rl = [Link], rr = [Link];
int rv = [Link];
if (height(rr) >= height(rl))
return new AVL(new AVL(l, v, rl), rv, rr);
else {
AVL rll = [Link], rlr = [Link];
int rlv = [Link];
return new AVL(new AVL(l, v, rll), rlv, new AVL(rlr, rv, rr));
}
} else
return new AVL(l, v, r);
}
// le reste du code est identique celui de la classe BST,
// en remplaant BST par AVL et new BST(...) par balance(...)
89
90
Chapitre 6. Arbres
Programme 14 Structure d'ensemble ralise avec un AVL
class AVLSet {
private AVL root;
AVLSet() {
[Link] = null;
}
boolean isEmpty() {
return [Link] == null;
}
boolean contains(int x) {
return [Link]([Link], x);
}
void add(int x) {
[Link] = [Link](x, [Link]);
}
void remove(int x) {
[Link] = [Link](x, [Link]);
}
6.3.3 Structure d'ensemble
En utilisant la classe
AVL dcrite dans la section prcdente, crivons une classe AVLSet
pour reprsenter des ensembles d'entiers, avec l'interface suivante :
boolean isEmpty();
boolean contains(int x);
void add(int x);
void remove(int x);
Exactement comme nous l'avons fait prcdemment pour construire des structures de pile
et de le au dessus de la structure de liste chane, nous encapsulons un objet de type
AVL
dans cette nouvelle classe
AVLSet
class AVLSet {
private AVL root;
...
}
Le reste du code est immdiat ; il est donn programme 14 page 90.
6.3. Arbres binaires de recherche
Exercice 6.9.
Ajouter la classe
91
AVLSet un champ priv size contenant le nombre
int size() qui en renvoie la valeur. Modier
d'lments de l'ensemble et une mthode
les mthodes
add
et
remove
pour mettre jour la valeur de ce champ. Il faudra traiter
correctement le cas o l'lment ajout par
l'lment supprim par
remove
add
est dj dans l'ensemble et celui o
n'est pas dans l'ensemble. On pourra rutiliser l'ide de
l'exercice 6.7.
Exercice * 6.10.
Ajouter la classe
AVL une mthode AVL ofList(Queue<Integer> l)
entiers, suppose trie par ordre croissant, et ren-
qui prend en argument une liste de
voie un AVL contenant ces
partant de ses feuilles.
Exercice 6.11.
n entiers, en temps O(n). Indication : on construira l'arbre en
Dduire de l'exercice prcdent des mthodes ralisant l'union, l'inter-
section et la dirence ensembliste de deux AVL en temps
O(n + m),
et
sont les
nombres d'lments de chaque AVL.
6.3.4 Code gnrique
Pour crire une version gnrique des arbres binaires de recherche, par exemple des
AVL donns page 89, on paramtre le code par le type
des lments :
class AVL<E> {
final E value;
final AVL<E> left, right;
final int height;
Cela ne sut cependant pas. Le code a besoin de pouvoir comparer les lments entre eux,
contains
par exemple dans la mthode
ou la mthode
add.
Pour l'instant, nous avons
==, < et >. Pour comparer
exiger que la classe E fournisse
utilis une comparaison directe entre entiers, avec les oprateurs
des lments de type
E,
ce n'est plus possible. On va donc
une mthode pour comparer deux lments. Pour cela on utilise l'interface suivante :
interface Comparable<K> {
int compareTo(K k);
}
Le signe de l'entier renvoy par
compareTo
this se compare k. Une
[Link]<T>.
AVL implmente l'interface Comparable<E>,
indique comment
telle interface fait dj partie de la bibliothque Java, dans
On va exiger que le paramtre
de la classe
ce que l'on crit ainsi :
class AVL<E extends Comparable<E>> {
...
l'intrieur de la classe
de
[Link](y).
AVL,
on peut comparer deux lments
et
paramtre de type et sa contrainte (voir page 11). Ainsi la mthode
AVL
s'crit
en testant le signe
Pour crire une mthode statique, on doit prciser de nouveau le
contains de la classe
92
Chapitre 6. Arbres
static<E extends Comparable<E>> boolean contains(AVL<E> a, E x) {
while (a != null) {
int c = [Link]([Link]);
if (c == 0) return true;
a = (c < 0) ? [Link] : [Link];
}
return false;
}
6.4 Arbres de prxes
On s'intresse dans cette section une autre structure d'arbre, pour reprsenter des
ensembles de
mots
(ici du type
String
de Java). Dans ces arbres, chaque branche est
tiquete par une lettre et chaque nud contient un boolen indiquant si la squence de
lettres menant de la racine de l'arbre ce nud est un mot appartenant l'ensemble.
Par exemple, l'arbre reprsentant l'ensemble de mots {"if",
"in", "do", "done"}
est le
suivant :
false
false
false
true
true
o
true
n
false
e
true
Un tel arbre est appel un
arbre de prxes ,
plus connu sous le nom de
trie
en anglais.
L'intrt d'une telle structure de donne est de borner le temps de recherche d'un lment
dans un ensemble la longueur du mot le plus long de cet ensemble, quelque soit le nombre
de mots qu'il contient. Plus prcisment, cette proprit est garantie seulement si toutes
les feuilles d'un arbre de prxes reprsentent bien un mot de l'ensemble, c'est--dire si
elles contiennent toutes une valeur boolenne vrai. Cette
bonne formation
des arbres de
prxes sera maintenue par toutes les oprations dnies ci-dessous.
crivons une classe
HashMap
Trie
pour reprsenter de tels arbres. On utilise la bibliothque
pour reprsenter le branchement chaque nud par une table de hachage :
class Trie {
boolean word;
HashMap<Character, Trie> branches;
...
branches de la racine de l'arbre est une table de
hachage contenant deux entres, une associant le caractre 'i' au sous-arbre de gauche,
et une autre associant le caractre 'd' au sous-arbre de droite.
Ainsi, dans l'exemple ci-dessus, le champ
L'arbre de prxes vide est reprsent par un arbre rduit un unique nud o le
champ
word
vaut
false
et
branches
est un dictionnaire vide :
6.4. Arbres de prxes
93
Trie() {
[Link] = false;
[Link] = new HashMap<Character, Trie>();
}
Recherche d'un lment.
chane
crivons une mthode
contains
qui dtermine si une
appartient un arbre de prxes.
boolean contains(String s) {
La recherche consiste descendre dans l'arbre en suivant les lettres de
l'aide d'une boucle
for,
en se servant d'une variable
s.
On le fait ici
contenant le nud de l'arbre o
l'on se trouve chaque instant.
Trie t = this;
for (int i = 0; i < [Link](); i++) { // invariant t != null
t = [Link]([Link](i));
[Link] ne contient pas d'entre pour le i-ime caractre de s, alors la mthode
get ci-dessus renverra null. Dans ce cas, on conclut immdiatement que s n'appartient
pas t.
Si
if (t == null) return false;
Dans le cas contraire, on passe au caractre suivant. Si on sort de la boucle, c'est qu'on
est parvenu jusqu'au dernier caractre de
s.
Il sut alors de renvoyer le boolen prsent
dans le nud qui a t atteint.
return [Link];
Insertion d'un lment.
L'insertion d'un mot
dans un arbre de prxes consiste
descendre le long de la branche tiquete par les lettres de
s,
de manire similaire
au parcours eectu pour la recherche. C'est cependant lgrement plus subtil, car il faut
ventuellement crer de nouvelles branches dans l'arbre pendant la descente. Comme pour
la recherche, on procde la descente avec une boucle
mot et une variable
for
parcourant les caractres du
contenant le sous-arbre courant.
void add(String s) {
Trie t = this;
for (int i = 0; i < [Link](); i++) { // invariant t != null
char c = [Link](i);
Avant de suivre le branchement donn par le caractre
dans une variable locale
b.
c, on sauvegarde la table de hachage
HashMap<Character, Trie> b = [Link];
t = [Link](c);
94
Chapitre 6. Arbres
Si la branche correspondant au caractre
null.
n'existe pas, la mthode
get
aura renvoy
Il faut alors ajouter une nouvelle branche. On le fait en crant un nouvel arbre
d'une part, et en l'ajoutant la table
d'autre part.
if (t == null) {
t = new Trie();
[Link](c, t);
}
On peut alors passer au caractre suivant, car on a assur que
n'est pas
sorti de la boucle, il ne reste plus qu' positionner le boolen
prsence du mot
s.
true
null.
Une fois
pour indiquer la
}
[Link] = true;
Si le mot
tait dj prsent dans l'arbre, cette aectation est sans eet.
Le code complet est donn programme 15 page 95.
La structure d'arbre de prxes
peut tre gnralise toute valeur pouvant tre vue comme une suite de lettres, quelle
que soit la nature de ces lettres. C'est le cas par exemple pour une liste. C'est aussi le
cas d'un entier, si on voit ses bits comme formant un mot avec les lettres
et
1.
Dans ce
dernier cas, on parle d'arbre de Patricia [7].
Exercice 6.12.
Ajouter la classe
Trie
supprime l'occurrence de la chane
s,
Exercice * 6.13.
remove
branches vides,
i.e.
La mthode
une mthode
void remove(String s)
qui
si elle existe.
de l'exercice prcdent peut conduire des
ne contenant plus aucun mot, ce qui dgrade les performances de
la recherche. Modier la mthode
remove
pour qu'elle supprime les branches devenues
vides. Il s'agit donc de maintenir l'invariant qu'un champ
branches
ne contient jamais
une entre vers un arbre ne contenant aucun mot. Indication : on pourra procder rcursivement et se servir de la mthode suivante
boolean isEmpty() {
return ![Link] && [Link]();
}
qui teste si un arbre ne contient aucun mot supposer que l'invariant ci-dessus est
eectivement maintenu, bien entendu.
Exercice 6.14.
Optimiser la structure de
Trie pour que le champ branches des feuilles
null.
de l'arbre ne contiennent pas une table de hachage vide, mais plutt la valeur
Exercice 6.15.
La structure
Trie
est une structure de donnes modiable. Expliquer
pourquoi, la dirence des arbres binaires vus plus haut, on ne peut pas en faire facile-
final sur les deux
champs word et branches. (On peut cependant appliquer le qualicatif final au seul
champ branches ; mme si cela reste une structure modiable, quel en est l'intrt ?)
ment une structure persistante en ajoutant simplement le qualicatif
6.4. Arbres de prxes
Programme 15 Arbres de prxes
class Trie {
boolean word;
HashMap<Character, Trie> branches;
Trie() {
[Link] = false;
[Link] = new HashMap<Character, Trie>();
}
boolean contains(String s) {
Trie t = this;
for (int i = 0; i < [Link](); i++) { // invariant t != null
t = [Link]([Link](i));
if (t == null) return false;
}
return [Link];
}
void add(String s) {
Trie t = this;
for (int i = 0; i < [Link](); i++) { // invariant t != null
char c = [Link](i);
Map<Character, Trie> b = [Link];
t = [Link](c);
if (t == null) {
t = new Trie();
[Link](c, t);
}
}
[Link] = true;
}
95
96
Chapitre 6. Arbres
6.5 Cordes
On prsente ici une troisime structure d'arbre, les cordes. Il s'agit d'une structure
persistante pour reprsenter de grandes chanes de caractres ecacement, et permettre
notamment des oprations de concatnation et d'extraction de sous-chanes sans impliquer
de copies. La structure de corde s'appuie sur une ide trs simple : une corde n'est rien
d'autre qu'un arbre binaire dont les feuilles sont des chanes (usuelles) de caractres et
dont les nuds internes sont vus comme des concatnations. Ainsi l'arbre
App
"a ver"
App
"y long"
" string"
est une des multiples faons de reprsenter la chane
"a very long string". Deux consi-
drations nous poussent raner lgrement l'ide ci-dessus. D'une part, de nombreux
algorithmes auront besoin d'un accs ecace la longueur d'une corde, notamment pour
dcider de descendre dans le sous-arbre gauche ou dans le sous-arbre droit d'un nud
App.
Il est donc souhaitable d'ajouter la taille de la corde comme une dcoration de chaque
nud interne. D'autre part, il est important de pouvoir partager des sous-chanes entre
les cordes elles-mmes et avec les chanes usuelles qui ont t utilises pour les construire.
Ds lors, plutt que d'utiliser une chane complte dans chaque feuille, on va stocker plus
d'information pour dsigner un fragment d'une chane Java, par exemple sous la forme
de deux entiers indiquant un indice et une longueur. Pour reprsenter de tels arbres, on
pourrait imaginer la classe suivante
class Rope {
int length;
String word; int ofs; // feuille
Rope left, right;
// noeud interne
...
}
o le champ
length
est utilis systmatiquement, les deux suivants dans le cas d'une
feuille uniquement et les deux derniers dans le cas d'un nud interne uniquement. Cette
reprsentation a tout de mme le dfaut d'tre inutilement gourmande : deux champs sont
systmatiquement gchs dans chaque objet. Nous allons adopter une reprsentation plus
subtile, en tirant parti de l'hritage de classes fourni par Java.
On commence par crire une classe
Rope
reprsentant une corde quelconque, c'est-
-dire aussi bien une feuille qu'un nud interne. On y stocke la longueur de la corde,
puisque c'est l l'information commune aux deux types de nuds.
abstract class Rope {
int length;
}
Cette classe est dclare abstraite, ce qui signie qu'on ne peut construire d'objet de cette
type pour les cordes, mais les objets reprsentant eectivement
deux sous-classes de Rope, reprsentant respectivement les
classe. Elle va nous servir de
les cordes vont appartenir
feuilles et les nuds internes. On les dnit ainsi :
6.5. Cordes
97
class Str extends Rope {
String str;
int ofs;
}
class App extends Rope {
Rope left, right;
}
Str a donc trois champs, savoir length,
str et ofs, et un objet de la classe App a galement trois champs, savoir length, left
et right. Les classes Str et App ne sont pas abstraites et on les utilisera justement pour
construire des cordes. Les constructeurs sont immdiats. Pour la classe Str, ce n'est rien
Par le principe de l'hritage, un objet de la classe
d'autre que le constructeur naturel :
Str(String str, int ofs, int len) {
[Link] = str;
[Link] = ofs;
[Link] = len;
}
Pour la classe
App,
c'est lgrement plus subtil, car on
calcule
la longueur de la corde
comme la somme des longueurs de ses deux morceaux :
App(Rope left, Rope right) {
[Link] = left;
[Link] = right;
[Link] = [Link] + [Link];
}
Avec ces constructeurs, on peut dj construire des cordes. La corde donne en exemple
plus haut peut tre construite avec
Rope r = new App(new Str("a ver", 0, 5),
new App(new Str("y long", 0, 6),
new Str(" string", 0, 7)));
Exercice 6.16.
len
Modier le constructeur de la classe
dsignent bien une portion valide de la chane
s.
Str
pour qu'il vrie que
et
Dans le cas contraire, lever une
exception.
Exercice 6.17.
ofs
Ajouter la classe
Str
un constructeur qui prend uniquement une
chane en argument.
Accs un caractre.
char get(int i) qui renvoie le i-ime caractre d'une corde. On la dclare dans la classe Rope, car on veut pouvoir
accder au i-ime caractre d'une corde sans connatre sa nature. Ainsi, on veut pouvoir
crire [Link](3) avec r de type Rope comme dans l'exemple ci-dessus. Mais on ne peut
pas dnir get dans la classe Rope. Aussi on la dclare comme une mthode abstraite.
crivons maintenant une mthode
98
Chapitre 6. Arbres
abstract char get(int i);
Pour que le code soit maintenant accept par le compilateur, il faut dnir la mthode
get
dans les deux sous-classes
Str
et
App.
Dans la classe
Str,
c'est immdiat. Il sut de
ne pas oublier le dcalage.
char get(int i) {
return [Link]([Link] + i);
}
Dans la classe
App,
il faut dterminer si le caractre
gauche ou de droite et appeler
get
se trouve dans la sous-chane de
rcursivement.
char get(int i) {
return (i < [Link]) ?
[Link](i) : [Link](i - [Link]);
}
Toute la subtilit de la programmation oriente objet se trouve ici : on ne connat pas la
nature de
[Link]
et
[Link]
et pour autant on peut appeler leur mthode
get.
Le bon morceau de code sera appel, par la vertu de l'appel dynamique de mthode.
Exercice 6.18.
Modier le code des mthodes
get pour qu'il vrie que i dsigne bien
une position valide dans la corde. Dans le cas contraire, lever une exception.
Extraction de sous-chane.
sous-corde. Comme pour
On ajoute maintenant une mthode pour extraire une
get, on commence par la dclarer abstraite dans la classe Rope :
abstract Rope sub(int ofs, int len);
Puis on la dnit dans chacune des sous-classes. Dans la classe
Str,
c'est immdiat :
Rope sub(int ofs, int len) {
return new Str([Link], [Link] + ofs, len);
}
Dans la classe
App, c'est plus subtil. En eet, la sous-corde peut se retrouver soit entire-
ment dans la corde de gauche, soit entirement dans la corde de droite, soit cheval sur
les deux. On distingue ces trois cas en calculant la longueur de la portion de
commenant l'indice
ofs
[Link]
Rope sub(int ofs, int len) {
int llen = [Link] - ofs;
Si elle est plus grande que
[Link].
len,
c'est que le rsultat se trouve tout entier dans la corde
if (len <= llen)
return [Link](ofs, len);
Si elle est ngative ou nulle, c'est que le rsultat se trouve tout entier dans la corde
[Link].
6.5. Cordes
99
if (llen <= 0)
return [Link](-llen, len);
Sinon, c'est que le rsultat doit tre la concatnation d'une portion de
portion de
[Link],
rcursivement calcules avec
sub.
[Link] et d'une
return new App([Link](ofs, llen),
[Link](0, len - llen));
Exercice 6.19.
Modier le code des mthodes
sub
pour qu'il vrie que
ofs
et
len
dsignent bien une portion valide de la corde. Dans le cas contraire, lever une exception.
Exercice 6.20.
Ajouter des qualicatifs appropris (private,
champs des classes
Exercice 6.21.
Rope, Str
et
App.
sur les dirents
Ajouter une mthode
String toString()
qui renvoie la chane Java
dnie par une corde. Comme le faire ecacement l'aide d'un
Exercice 6.22.
final)
StringBuilder ?
Pour amliorer l'ecacit des cordes, on peut utiliser l'ide suivante :
ds que l'on cherche concatner deux cordes dont la somme des longueurs ne dpasse
pas une constante donne (par exemple 256 caractres) alors on construit directement
un nud de type
r)
Str
qui concatne deux cordes (this et
mthode
toString
App.
plutt qu'un nud
r)
crire une mthode
Rope append(Rope
en utilisant cette ide. On pourra rutiliser la
de l'exercice prcdent.
100
Chapitre 6. Arbres
Programme 16 Cordes
abstract class Rope {
int length;
abstract char get(int i);
abstract Rope sub(int ofs, int len);
}
class Str extends Rope {
String str;
int ofs;
Str(String str, int ofs, int len) {
[Link] = str;
[Link] = ofs;
[Link] = len;
}
char get(int i) {
return [Link]([Link] + i);
}
Rope sub(int ofs, int len) {
return new Str([Link], [Link] + ofs, len);
}
}
class App extends Rope {
Rope left, right;
App(Rope left, Rope right) {
[Link] = [Link] + [Link];
[Link] = left;
[Link] = right;
}
char get(int i) {
return (i < [Link]) ?
[Link](i) : [Link](i - [Link]);
}
Rope sub(int ofs, int len) {
int llen = [Link] - ofs;
if (len <= llen) // tout dans left
return [Link](ofs, len);
if (llen <= 0) // tout dans right
return [Link](-llen, len);
return new App([Link](ofs, llen),
[Link](0, len - llen));
}
}
Files de priorit
Dans le chapitre 4 nous avons vu comment la structure de liste chane permettait de
raliser facilement une structure de le. Dans ce chapitre, nous considrons maintenant
des les dans lesquelles les lments se voient associer des priorits. Dans une telle le,
dite
les de priorit
(en anglais
priority queue ),
les lments sortent dans l'ordre x par
leur priorit et non plus dans l'ordre d'arrive. L'interface que l'on cherche dnir va
donc ressembler quelque chose comme
boolean isEmpty();
int size();
void add(int x);
int getMin();
void removeMin();
Dans cette interface, la notion de minimalit concide avec la notion de plus grande priorit. Contrairement aux les, on prfre distinguer l'accs au premier lment et sa suppression, par deux oprations distinctes, pour des raisons d'ecacit qui seront expliques
plus loin. Ainsi, la mthode
thode
getMin renvoie l'lment le plus prioritaire de la le et la m-
removeMin le supprime. On trouvera des applications des les de priorits plus loin
dans les chapitres 12 et 13.
7.1 Structure de tas
Pour raliser une le de priorit, il faut recourir une structure de donnes plus
complexe que pour une simple le. Une solution consiste organiser les lments sous la
forme d'un
tas (heap
en anglais). Un tas est un arbre binaire o, chaque nud, l'lment
stock est plus prioritaire que les deux lments situs immdiatement au-dessous. Ainsi,
un tas contenant les lments
{3,7,9,12,21}, ordonns par petitesse, peut prendre la forme
suivante :
3
7
21
12
(7.1)
On note qu'il existe d'autres tas contenant ces mmes lments. Par dnition, l'lment
le plus prioritaire est situ la racine et on peut donc y accder en temps constant. Les
102
Chapitre 7. Files de priorit
deux sections suivantes proposent deux faons direntes de reprsenter un tel tas.
7.2 Reprsentation dans un tableau
Dans cette section, on choisit de reprsenter une le de priorit par un tas dont la
proprit supplmentaire est d'tre un arbre binaire complet, c'est--dire un arbre binaire
o tous les niveaux sont remplis, sauf peut-tre le dernier, qui est alors partiellement
rempli gauche. Nous pourrions rutiliser ce qui a introduit au chapitre prcdent pour
reprsenter un tel arbre. Mais il se trouve qu'un arbre binaire complet peut tre facilement
reprsent dans un tableau. L'ide consiste numroter les nuds de l'arbre de haut en
bas et de gauche droite, partir de 0. Par exemple, le rsultat de cette numrotation
sur le tas (7.1) donne l'tiquetage
3(0)
7(1)
21(3)
12(2)
9(4)
Cette numrotation permet de reprsenter le tas dans un tableau. Ainsi, le tas ci-dessus
correspond au tableau 5 lments suivant :
12 21
4
9
De manire gnrale, la racine de l'arbre occupe la case d'indice 0 et les racines des deux
sous-arbres du nud stock la case
2i + 2.
Inversement, le pre du nud
i sont stockes respectivement
i est stock en b(i 1)/2c.
aux cases
2i + 1
et
De cette structure de tas, on dduit les direntes oprations de la le de priorit
de la manire suivante. Le plus petit lment est situ la racine de l'arbre, c'est--dire
l'indice 0 du tableau. On y accde donc en temps constant. Pour ajouter un nouvel
lment dans un tas, on le place tout en bas droite du tas et on le fait remonter sa
place. Pour supprimer le plus petit lment, on le remplace par l'lment situ tout en bas
droite du tas, que l'on fait alors descendre sa place. Ses deux oprations sont dcrites
en dtail dans les deux sections suivantes. Ce que l'on peut dj comprendre, c'est que
leur cot est proportionnel la hauteur de l'arbre. Un arbre binaire complet ayant une
hauteur logarithmique, l'ajout et le retrait dans un tas ont donc un cot
O(log n)
est le nombre d'lments dans le tas.
Pour mettre en uvre cette structure de tas, il reste un petit problme. On ne connat
pas
a priori
la taille de la le de priorit. On pourrait xer l'avance une taille maximale
pour la le de priorit mais une solution plus lgante consiste utiliser un tableau
redimensionnable. De tels tableaux sont prsents dans le chapitre 3 et on va donc rutiliser
ici la classe
ResizableArray
prsente plus haut. Un tas n'est donc rien d'autre qu'un
objet encapsulant un tableau redimensionnable (ici dans un champ appel
class Heap {
private ResizableArray elts;
elts)
7.2. Reprsentation dans un tableau
103
Le constructeur se contente d'allouer un nouveau tableau redimensionnable, qui ne contient
initialement aucun lment :
Heap() {
[Link] = new ResizableArray(0);
}
Le nombre d'lments contenus dans le tas est exactement celui du tableau redimensionnable, d'o un code immdiat pour les deux mthodes
size
et
isEmpty
int size() {
return [Link]();
}
boolean isEmpty() {
return [Link]() == 0;
}
La mthode
getMin
renvoie la racine du tas, si elle existe, et lve une exception sinon.
Comme expliqu ci-dessus, la racine du tas est stocke l'indice 0.
int getMin() {
if ([Link]() == 0)
throw new NoSuchElementException();
return [Link](0);
}
Insertion d'un lment.
L'insertion d'un lment
tableau d'une case, y mettre la valeur
x,
dans un tas consiste tendre le
x jusqu' la bonne
x est plus petit que son pre,
puis faire remonter
position. Pour cela, on utilise l'algorithme suivant : tant que
c'est--dire la valeur situe immdiatement au dessus dans l'arbre, on change leurs deux
valeurs et on recommence. Par exemple, l'ajout de 1 dans le tas (7.1) est ralis en trois
tapes :
3
7
21
3
12
1 < 12
7
21
1
1
1<3
12
On commence donc par crire une mthode rcursive
7
21
3
9
12
moveUp(int x, int i)
qui insre
x dans le tas, en partant de la position i. Cette mthode suppose que l'arbre
i obtenu en plaant x en i est un tas. La mthode moveUp considre tout d'abord
le cas o i vaut 0, c'est--dire o on est arriv la racine. Il sut alors d'insrer x la
position i.
un lment
de racine
private void moveUp(int x, int i) {
if (i == 0) {
[Link](i, x);
S'il s'agit en revanche d'un nud interne, on calcule l'indice
stocke dans ce nud.
fi
du pre de
et la valeur
104
Chapitre 7. Files de priorit
} else {
int fi = (i - 1) / 2;
int y = [Link](fi);
Si
est suprieur
x,
il s'agit de faire remonter
puis en appelant rcursivement
moveUp
partir de
en descendant la valeur
fi.
la place
if (y > x) {
[Link](i, y);
moveUp(x, fi);
Si en revanche
est infrieur ou gal
x,
alors
a atteint sa place dnitive et il sut
de l'y aecter.
} else
[Link](i, x);
Ceci achve le code de
moveUp.
add
La mthode
procde alors en deux temps. Elle aug-
mente la taille du tableau d'une unit, en ajoutant une case la n du tableau, puis
appelle la mthode
moveUp
partir de cette case.
void add(int x) {
int n = [Link]();
[Link](n + 1);
moveUp(x, n);
}
Comme expliqu plus haut, la mthode
add a une complexit O(log n) o n est le nombre
d'lments de la le de priorit.
Exercice 7.1.
Rcrire la mthode
moveUp
Suppression du plus petit lment.
l'aide d'une boucle
while.
Supprimer le plus petit lment d'un tas est
lgrement plus dlicat que d'insrer un nouvel lment. La raison en est qu'il s'agit de
supprimer la racine de l'arbre et qu'il faut donc trouver par quel lment la remplacer.
L'ide consiste choisir l'lment tout en bas droite du tas, c'est--dire l'lment occupant la dernire case du tableau, comme candidat, puis le faire descendre dans le tas
jusqu' sa place, un peu comme on a fait monter le nouvel lment lors de l'insertion.
Supposons par exemple que l'on veuille supprimer le plus petit lment du tas suivant :
1
4
11
7
5
On remplace la racine, c'est--dire 1, par l'lment tout en bas droite, c'est--dire 8.
Puis on fait descendre 8 jusqu' ce qu'il atteigne sa place. Pour cela, on compare 8 avec
les racines
et
des deux sous-arbres. Si
et
sont tous les deux plus grands que 8, la
descente est termine. Sinon, on change 8 avec le plus petit des deux nuds
a et b, et on
continue la descente. Sur l'exemple, 8 est successivement chang avec 4 et 5 :
7.2. Reprsentation dans un tableau
105
4 < 8, 7
4
11
11
crivons une mthode rcursive
lment
5 < 11, 8
7
11
moveDown(int x, int i)
i.
7
8
qui ralise la descente d'un
sa place, en partant de l'indice
private void moveDown(int x, int i) {
int n = [Link]();
On commence par dterminer l'indice
du plus petit des deux ls du nud
i.
Il faut
soigneusement tenir compte du fait que ces deux nuds n'existent peut-tre pas.
int j = 2 * i + 1;
if (j + 1 < n && [Link](j + 1) < [Link](j))
j++;
Si le nud
existe, et qu'il contient une valeur plus petite que
On fait donc remonter la valeur situe l'indice
rcursivement partir de l'indice
j,
j,
x,
alors
la position
i,
doit descendre.
puis on procde
pour poursuivre la descente.
if (j < n && [Link](j) < x) {
[Link](i, [Link](j));
moveDown(x, j);
Sinon, c'est que la valeur
x a termin sa descente. Il sut donc de l'aecter la position i.
} else
[Link](i, x);
Ceci achve le code de la mthode
moveDown.
La mthode
removeMin
de suppression du
plus petit lment d'un tas s'en dduit alors facilement. On commence par traiter le cas
pathologique d'un tas vide.
void removeMin() {
int n = [Link]() - 1;
if (n < 0) throw new NoSuchElementException();
Puis on extrait la valeur
situe tout en bas droite du tas, c'est--dire la dernire
position du tableau, avant de diminuer la taille du tableau d'une unit, puis d'appeler la
mthode
moveDown
pour placer
sa place, en partant de la racine du tas, c'est--dire
de la position 0.
int x = [Link](n);
[Link](n);
if (n > 0) moveDown(x, 0);
Comme expliqu plus haut, la mthode
removeMin
le nombre d'lments de la le de priorit. Le code complet de la
programme 17 page 106.
O(log n) o n est
classe Heap est donn
a une complexit
106
Chapitre 7. Files de priorit
Programme 17 Structure de tas (dans un tableau)
class Heap {
private ResizableArray elts;
Heap() { [Link] = new ResizableArray(0); }
int size() { return [Link](); }
boolean isEmpty() { return [Link]() == 0; }
private void moveUp(int x, int i) {
if (i == 0) {
[Link](i, x);
} else {
int fi = (i - 1) / 2;
int y = [Link](fi);
if (y > x) {
[Link](i, y);
moveUp(x, fi);
} else
[Link](i, x);
}
}
void add(int x) {
int n = [Link]();
[Link](n + 1);
moveUp(x, n);
}
int getMin() {
if ([Link]() == 0) throw new NoSuchElementException();
return [Link](0);
}
private void moveDown(int x, int i) {
int n = [Link]();
int j = 2 * i + 1;
if (j + 1 < n && [Link](j + 1) < [Link](j))
j++;
if (j < n && [Link](j) < x) {
[Link](i, [Link](j));
moveDown(x, j);
} else
[Link](i, x);
}
void removeMin() {
int n = [Link]() - 1;
if (n < 0) throw new NoSuchElementException();
int x = [Link](n);
[Link](n);
if (n > 0) moveDown(x, 0);
}
}
7.3. Reprsentation comme un arbre
Exercice 7.2.
107
On peut utiliser la structure de tas pour raliser un tri ecace trs
facilement, appel
tri par tas
(en anglais
heapsort ).
L'ide est la suivante : on insre
tous les lments trier dans un tas, puis on les ressort successivement avec les mthodes
getMin et removeMin. crire une mthode void sort(int[] a) pour trier un tableau en
utilisant cet algorithme. (Le tri par tas est dcrit en dtail section 12.4.)
7.3 Reprsentation comme un arbre
Cette section prsente une autre reprsentation de la structure de tas, avec la particularit de proposer une fonction ecace de fusion de deux tas. Les tas sont ici directement
reprsents par des arbres binaires. Contrairement aux AVL, aucune information de nature assurer l'quilibrage n'est stocke dans les nuds. Nous verrons plus loin que ces tas
orent nanmoins une complexit amortie logarithmique. On parle de tas
auto-quilibrs.
skew heaps.
SkewHeap, qui encapsule un arbre binaire (le tas) et son nombre
la classe Tree de la section 6.1.
En anglais, ces tas s'appellent des
On introduit une classe
d'lments. On rutilise
class SkewHeap {
private Tree root;
private int size;
On maintiendra l'invariant que le champ
size
contient toujours le nombre d'lments de
root. Le constructeur est immdiat. On rappelle que l'arbre
null. Ce n'est pas gnant car le champ root est un champ priv
l'arbre stock dans le champ
vide est reprsent par
de la classe
SkewHeap.
SkewHeap() {
[Link] = null;
[Link] = 0;
}
Les mthodes
isEmpty
et
size
sont galement immdiates. On note qu'elles s'excutent
en temps constant.
boolean isEmpty() {
return [Link] == 0;
}
int size() {
return [Link];
}
isEmpty pourrait tout aussi bien
getMin renvoie le plus petit lment,
[Link]
est
null.
La mthode
tester si
mthode
c'est--dire la racine du tas. On prend
cependant soin de tester que l'arbre est non vide.
int getMin() {
if ([Link]()) throw new NoSuchElementException();
return [Link];
}
Enn la
108
Chapitre 7. Files de priorit
Opration de fusion.
thode
merge
Toute la subtilit de ces tas auto-quilibrs tient dans une m-
qui fusionne deux tas. On l'crit comme une mthode statique et prive qui
prend en arguments deux arbres
t1
et
t2,
supposs tre des tas, et renvoie leur fusion
sous la forme d'un nouvel arbre.
private static Tree merge(Tree t1, Tree t2) {
Si l'un des deux tas est vide, c'est immdiat.
if (t1 == null) return t2;
if (t2 == null) return t1;
Si en revanche aucun des tas n'est vide, on construit le tas rsultant de la fusion de la
manire suivante. Sa racine est clairement la plus petite des deux racines de
Supposons que la racine de
t1
t1
et
t2.
soit la plus petite.
if ([Link] <= [Link])
On doit maintenant dterminer les deux sous-arbres de
new Tree(..., [Link], ...).
merge sur deux des trois
Il y a plusieurs possibilits, obtenues en appelant rcursivement
arbres
[Link], [Link]
et
t2
et en choisissant de mettre le rsultat comme sous-arbre
gauche ou droit. Parmi toutes ces possibilits, on choisit celle qui change les deux sousarbres de
[Link]
t1,
de manire assurer l'auto-quilibrage. Ainsi,
et est fusionn avec
t2.
[Link]
prend la place de
return new Tree(merge([Link], t2), [Link], [Link]);
L'autre situation, o la racine de
est la plus petite, est symtrique.
else
return new Tree(merge([Link], t1), [Link], [Link]);
Ceci achve la mthode
Autres oprations.
et
t2
removeMin.
merge.
De cette opration
merge
on dduit facilement les mthodes
En eet, pour ajouter un nouvel lment
dernier avec un arbre rduit l'lment
x,
add
au tas, il sut de fusionner ce
sans oublier de mettre jour le champ
size.
void add(int x) {
[Link] = merge([Link], new Tree(null, x, null));
[Link]++;
}
Pour supprimer le plus petit lment, c'est--dire la racine du tas, il sut de fusionner
les deux sous-arbres gauche et droit. On commence par tester si le tas est eectivement
non vide.
int removeMin() {
if ([Link]()) throw new NoSuchElementException();
7.4. Code gnrique
109
Le cas chant, on conserve sa racine dans une variable
res (pour
merge.
la renvoyer comme
rsultat) et on fusionne les deux sous-arbres avec la mthode
int res = [Link];
[Link] = merge([Link], [Link]);
Enn, on met jour le champ
size
et on renvoie le plus petit lment
res.
[Link]--;
return res;
Le code complet de la classe
Exercice 7.3.
Ajouter la classe
qui ajoute au tas
Complexit.
this
est donn programme 18 page 110.
ShewHeap une mthode void merge(SkewHeap that)
that.
le contenu du tas
On peut montrer que l'insertion successive de
tas vide a un cot total
cot amorti
SkewHeap
log n.
O(n log n).
n lments en partant d'un
On peut donc considrer que chaque insertion a un
Il s'agit uniquement d'un cot amorti car il se peut nanmoins qu'une
O(n) dans le pire des cas. On
n lments d'un tas, avec une
application rpte de removeMin, a un cot total O(n log n). On peut donc considrer
de mme que chaque suppression a un cot amorti log n. L encore, il s'agit d'un cot
insertion particulire ait un cot plus grand, de l'ordre de
peut montrer galement que la suppression successive des
amorti, le pire des cas d'une suppression particulire pouvant tre suprieur.
7.4 Code gnrique
Pour raliser une version gnrique des les de priorit, on procde comme pour le
code gnrique des AVL (section 6.3.4), avec un paramtre de type
sur lequel on exige
une mthode de comparaison. On crit donc quelque chose comme
class Heap<E extends Comparable<E>> {
...
S'il s'agit d'une reprsentation dans un tableau, on utilise par exemple les tableaux redimensionnables gnriques de la section 3.4.5 (ou plus simplement la classe
Vector<E>
de
la bibliothque Java). S'il s'agit d'arbres binaires, on utilise des arbres gnriques, comme
au chapitre 6. Le reste du code est alors facilement adapt. Lorsqu'il s'agit de comparer
deux lments
et
y,
on n'crit plus
x < y
mais
[Link](y) < 0.
La bibliothque Java propose une telle structure de donnes gnrique dans la classe
[Link]<E>. Si la classe E implmente l'interface Comparable<E>, leur
mthode compareTo est utilise pour comparer les lments. Dans le cas contraire, l'utilisateur peut fournir un comparateur au moment de la cration de la le de priorit, sous
la forme d'un objet qui implmente l'interface
[Link]<T>
interface Comparator<T> {
int compare(T x, T y);
}
C'est alors la mthode
lments.
compare
de ce comparateur qui est utilise pour ordonner les
110
Chapitre 7. Files de priorit
Programme 18 Structure de tas (arbre auto-quilibr)
class SkewHeap {
private Tree root;
private int size;
SkewHeap() {
[Link] = null;
[Link] = 0;
}
boolean isEmpty() {
return [Link] == 0;
}
int size() {
return [Link];
}
int getMin() {
if ([Link]()) throw new NoSuchElementException();
return [Link];
}
private static Tree merge(Tree t1, Tree t2) {
if (t1 == null) return t2;
if (t2 == null) return t1;
if ([Link] <= [Link])
return new Tree(merge([Link], t2), [Link], [Link]);
else
return new Tree(merge([Link], t1), [Link], [Link]);
}
void add(int x) {
[Link] = merge([Link], new Tree(null, x, null));
[Link]++;
}
int removeMin() {
if ([Link]()) throw new NoSuchElementException();
int res = [Link];
[Link] = merge([Link], [Link]);
[Link]--;
return res;
}
Classes disjointes
Ce chapitre prsente une structure de donnes pour le problme des classes disjointes,
connue sous le nom de
union-nd.
Ce problme consiste maintenir dans une structure
de donnes une partition d'un ensemble ni, c'est--dire un dcoupage en sous-ensembles
disjoints que l'on appelle des classes . On souhaite pouvoir dterminer si deux lments
appartiennent la mme classe et runir deux classes en une seule. Ce sont ces deux
oprations qui ont donn le nom de structure
union-nd.
8.1 Principe
Sans perte de gnralit, on suppose que l'ensemble partitionner est celui des
entiers
{0,1, . . . ,n 1}.
On cherche construire une classe
UnionFind
avec l'interface
suivante :
class UnionFind {
UnionFind(int n)
int find(int i)
void union(int i, int j)
}
Le constructeur
UnionFind(n)
construit une nouvelle partition de
chaque lment forme une classe lui tout seul. L'opration
de l'lment
i,
find(i)
{0,1, . . . ,n 1}
dtermine la classe
sous la forme d'un entier considr comme l'unique reprsentant de cette
classe. En particulier, on dtermine si deux lments sont dans la mme classe en com-
find pour chacun. Enn, l'opration union(i,j) runit les
j , la structure de donnes tant modie en place.
parant les rsultats donns par
deux classes des lments
et
L'ide principale est de lier entre eux les lments d'une mme classe. Dans chaque
classe, ces liaisons forment des chemins qui mnent tous un unique reprsentant, qui est
le seul lment li lui-mme. La gure 8.1 montre un exemple o l'ensemble
{0,1, . . . ,7}
est partitionn en deux classes dont les reprsentants sont respectivement 3 et 4. Il est
possible de reprsenter une telle structure en utilisant des nuds allous en mmoire
individuellement (voir exercice 8.3). Cependant, il est plus simple et souvent plus ecace
d'utiliser un tableau qui lie chaque entier un autre entier de la mme classe. Ces liaisons
mnent toujours au reprsentant de la classe, qui est associ sa propre valeur dans le
tableau. Ainsi, la partition de la gure 8.1 est reprsente par le tableau suivant :
112
Chapitre 8. Classes disjointes
3
Figure 8.1 Une partition en deux classes de {0,1, . . . ,7}
0
7
3
find se contente de suivre les liaisons jusqu' trouver le reprsentant.
union commence par trouver les reprsentants des deux lments, puis lie
L'opration
L'opration
l'un des deux reprsentants l'autre. An d'atteindre de bonnes performances, on apporte
deux amliorations. La premire consiste
eectue par
find
compresser les chemins
pendant la recherche
: cela consiste lier directement au reprsentant tous les lments
trouvs sur le chemin parcouru pour l'atteindre. La seconde consiste maintenir, pour
chaque reprsentant, une valeur appele
rang
qui reprsente la longueur maximale que
pourrait avoir un chemin dans cette classe. Cette information est stocke dans un second
tableau et est utilise par la fonction
union
pour choisir entre les deux reprsentants
possibles d'une union. Cette structure de donnes est attribue McIlroy et Morris [ ] et
sa complexit a t analyse par Tarjan [ ].
8.2 Ralisation
Dcrivons maintenant le code de la structure
union-nd,
dans une classe
UnionFind
dont une instance reprsente une partition. Cette classe contient deux tableaux privs :
link
qui contient les liaisons et
rank
qui contient le rang de chaque classe.
class UnionFind {
private int[] link;
private int[] rank;
rank n'est signicative que pour des lments i qui sont des
pour lesquels link[i] = i. Initialement, chaque lment forme
L'information contenue dans
reprsentants, c'est--dire
une classe lui tout seul, c'est--dire est son propre reprsentant, et le rang de chaque
classe vaut 0.
UnionFind(int n) {
if (n < 0) throw new IllegalArgumentException();
[Link] = new int[n];
for (int i = 0; i < n; i++) [Link][i] = i;
[Link] = new int[n];
}
8.2. Ralisation
113
find calcule le reprsentant d'un lment i. Elle s'crit naturellement comme
p li i dans le tableau link.
i lui-mme, on a termin et i est le reprsentant de la classe.
La fonction
une fonction rcursive. On commence par calculer l'lment
Si c'est
int find(int i) {
if (i < 0 || i >= [Link])
throw new ArrayIndexOutOfBoundsException(i);
int p = [Link][i];
if (p == i) return i;
Sinon, on calcule rcursivement le reprsentant
de renvoyer
r.
r,
r de la classe avec l'appel find(p). Avant
i
on ralise la compression de chemins, c'est--dire qu'on lie directement
int r = [Link](p);
[Link][i] = r;
return r;
Ainsi, la prochaine fois que l'on appellera
entendu, il se trouvait peut-tre que
find
i,
sur
tait dj li
on trouvera
directement. Bien
et dans ce cas l'aectation est
sans eet.
L'opration
union
regroupe en une seule les classes de deux lments
commence par calculer leurs reprsentants respectifs
ri
et
rj.
et
j.
On
S'ils sont gaux, il n'y a
rien faire.
void union(int i, int j) {
int ri = [Link](i);
int rj = [Link](j);
if (ri == rj) return; // dj dans la mme classe
ri est strictement plus petit que
c'est--dire qu'on lie ri rj.
Sinon, on compare les rangs des deux classes. Si celui de
celui de
rj,
on fait de
rj
le reprsentant de l'union,
if ([Link][ri] < [Link][rj])
[Link][ri] = rj;
Le rang n'a pas besoin d'tre mis jour pour cette nouvelle classe. En eet, seuls les
chemins de l'ancienne classe de
ri
ont vu leur longueur augmente d'une unit et cette
nouvelle longueur n'excde pas le rang de
rj.
Si en revanche c'est le rang de
rj
qui est le
plus petit, on procde symtriquement.
else {
[Link][rj] = ri;
Dans le cas o les deux classes ont le mme rang, l'information de rang doit alors tre
mise jour, car la longueur du plus long chemin est susceptible d'augmenter d'une unit.
if ([Link][ri] == [Link][rj])
[Link][ri]++;
114
Chapitre 8. Classes disjointes
Il est important de noter que la fonction
union
utilise la fonction
des compressions de chemin, mme dans le cas o il s'avre que
mme classe. Le code complet de la classe
Complexit.
n
find et ralise donc
j sont dj dans la
et
est donn programme 19 page 115.
On peut montrer que, grce la compression de chemin et au rang associ
chaque classe, une suite de
contenant
UnionFind
oprations
find
union ralises sur une structure
O(m (n,m)), o est une fonction
et
lments s'excute en un temps total
qui crot extrmement lentement. Elle crot si lentement qu'on peut la considrer comme
constante pour toute application pratique vues les valeurs de
et
que les limites de
mmoire et de temps nous autorisent admettre ce qui nous permet de supposer un
temps amorti constant pour chaque opration. Cette analyse de complexit est complexe
et dpasse largement le cadre de ce livre. On en trouvera une version dtaille dans
Introduction to Algorithms
Exercice 8.1.
[2, chap. 22].
Ajouter la structure
union-nd
une mthode
int numClasses()
don-
nant le nombre de classes distinctes. On s'eorcera de fournir cette valeur en temps
constant, en maintenant la valeur comme un champ supplmentaire.
Exercice 8.2.
deux tableaux
Si les lments ne sont pas des entiers conscutifs, on peut remplacer les
rank
et
link
par deux tables de hachage. Rcrire la classe
Exercice 8.3.
UnionFind
en
utilisant cette ide.
Une autre solution pour raliser la structure
union-nd
consiste ne
pas utiliser de tableaux, mais reprsenter directement chaque lment comme un objet
contenant deux champs
rank
et
link.
Si
dsigne le type des lments, on peut dnir
la classe gnrique suivante :
class Elt<E> {
private E value;
private Elt<E> link;
private int rank;
...
}
Il n'est plus ncessaire de maintenir d'information globale sur la structure
union-nd, car
chaque lment contient toute l'information ncessaire. (Attention cependant ne pas
partager une valeur de type
Elt
Elt<E>
entre plusieurs partitions.) L'interface de la classe
est la suivante :
Elt(E x)
Elt<E> find()
void union(Elt<E> e)
Elt(E x) construit une classe contenant un unique lment, de valeur x.
On pourra choisir la convention qu'un pointeur link est null lorsqu'il s'agit d'un reprsentant. crire ce constructeur ainsi que les mthodes find et union.
Le constructeur
8.2. Ralisation
Programme 19 Structure de classes disjointes (union-nd )
class UnionFind {
private int[] link;
private int[] rank;
UnionFind(int n) {
if (n < 0) throw new IllegalArgumentException();
[Link] = new int[n];
for (int i = 0; i < n; i++) [Link][i] = i;
[Link] = new int[n];
}
int find(int i) {
if (i < 0 || i >= [Link])
throw new ArrayIndexOutOfBoundsException(i);
int p = [Link][i];
if (p == i) return i;
int r = [Link](p);
[Link][i] = r;
return r;
}
void union(int i, int j) {
int ri = [Link](i);
int rj = [Link](j);
if (ri == rj) return;
if ([Link][ri] < [Link][rj])
[Link][ri] = rj;
else {
[Link][rj] = ri;
if ([Link][ri] == [Link][rj])
[Link][ri]++;
}
}
115
116
Exercice 8.4.
Chapitre 8. Classes disjointes
On peut utiliser la structure
union-nd
pour construire ecacement un
labyrinthe parfait, c'est--dire un labyrinthe o il existe un chemin et un seul entre deux
cases. Voici un exemple de tel labyrinthe :
On procde de la manire suivante. On cre une structure
union-nd
dont les lments
sont les direntes cases. L'ide est que deux cases sont dans la mme classe si et seulement
si elles sont relies par un chemin. Initialement, toutes les cases du labyrinthe sont spares
les unes des autres par des murs. Puis on considre toutes les paires de cases adjacentes
(verticalement et horizontalement) dans un ordre alatoire. Pour chaque paire
c1 et c2 . Si elles sont identiques, on
c1 et c2 et on runit les deux classes
(c1 ,c2 )
on
compare les classes des cases
ne fait rien. Sinon, on
supprime le mur qui spare
avec
union.
crire un
code qui construit un labyrinthe selon cette mthode.
Indication : pour parcourir toutes les paires de cases adjacentes dans un ordre alatoire,
le plus simple est de construire un tableau contenant toutes ces paires, puis de le mlanger
alatoirement en utilisant le
mlange de Knuth
(exercice 3.3 page 35).
Justier que, l'issue de la construction, chaque case est relie toute autre case par
un unique chemin.
Troisime partie
Algorithmes lmentaires
Arithmtique
Ce chapitre regroupe un certain nombre d'algorithmes fondamentaux ayant pour
thme commun l'arithmtique.
9.1 Algorithme d'Euclide
Le plus clbre des algorithmes arithmtiques est trs certainement l'algorithme d'Euclide. Il permet de calculer le plus grand diviseur commun de deux entiers (dit pgcd ,
en anglais
et
v,
gcd
pour
greatest common divisor ).
tant donns deux entiers positifs ou nuls
l'algorithme d'Euclide rpte le calcul
(u,v) (v,u mod v)
jusqu' ce que
de
et
v.
(9.1)
v soit nul, et renvoie alors la valeur de u, qui est le pgcd des valeurs initiales
Le code est immdiat ; il est donn programme 20 page 119.
de cet algorithme est assure par la dcroissance stricte de
La terminaison
et le fait que
ailleurs positif ou nul. On a en eet l'invariant de boucle vident
u,v 0.
reste par
La correction
de l'algorithme repose sur le fait que l'instruction (9.2) prserve le plus grand diviseur
commun. Quand on parvient
le pgcd des valeurs initiales de
v = 0 on renvoie alors u c'est--dire gcd(u,0), qui est donc
u et v .
La complexit de l'algorithme d'Euclide est donne par le thorme de Lam, qui
stipule que si l'algorithme eectue
itrations pour
Programme 20 Algorithme d'Euclide
static int gcd(int u, int v) {
while (v != 0) {
int tmp = v;
v = u % v;
u = tmp;
}
return u;
}
u>v>0
alors
u Fs+1
et
v Fs
120
Chapitre 9. Arithmtique
Programme 21 Algorithme d'Euclide tendu
static int[] extendedGcd(int u0, int v0) {
int[] u = { 1, 0, u0 }, v = { 0, 1, v0 };
while (v[2] != 0) {
int q = u[2] / v[2];
int[] t = { u[0] - q * v[0], u[1] - q * v[1], u[2] - q * v[2] };
u = v;
v = t;
}
return u;
}
o
(Fn )
dtaille est donne dans
Exercice 9.1.
u
ou
s = O(log u).
est la suite de Fibonacci. On en dduit facilement que
The Art of Computer Programming
Une analyse
[5, sec. 4.5.3].
L'algorithme d'Euclide que l'on vient de prsenter suppose
est ngatif, il peut renvoyer un rsultat ngatif (l'opration
u,v 0.
Si
de Java renvoie
une valeur du mme signe que son premier argument). Modier le code de la mthode
gcd
pour qu'elle renvoie toujours un rsultat positif ou nul, quel que soit le signe de ses
arguments. Dans quel cas le rsultat vaut-il zro ?
Exercice 9.2.
Le rsultat de complexit donn ci-dessus suppose
dans le cas gnral, la complexit est
Algorithme d'Euclide tendu.
u > v.
Montrer que,
O(log(max(u,v))).
On peut facilement modier algorithme d'Euclide
pour qu'il calcule galement les coecients de Bzout, c'est--dire un triplet d'entiers
(r0 ,r1 ,r2 )
tels que
r0 u + r1 v = r2 = gcd(u,v).
v , mais avec deux triplets
d'entiers ~
u = (u0 ,u1 ,u2 ) et ~v = (v0 ,v1 ,v2 ). Initialement, on prend ~u = (1,0,u) et ~v = (0,1,v).
Pour cela, on ne travaille plus avec seulement deux entiers
et
Puis, exactement comme avec l'algorithme d'Euclide, on rpte l'instruction
(~u,~v ) (~v ,~u q~v )
jusqu' ce que
avec
q = bu2 /v2 c
(9.2)
v2 = 0, et on renvoie ~u. On appelle cela l'algorithme d'Euclide tendu. Une
traduction littrale de cet algorithme en Java, utilisant des tableaux pour reprsenter
les vecteurs
~u
et
~v ,
est donne programme 21 page 120. (L'exercice 9.3 en propose une
criture un peu plus ecace.) On se convainc facilement que la troisime composante du
vecteur renvoy est bien le pgcd de
et
v2
et
v.
avec les variables
u2 bu2 /v2 cv2 .
et
v,
u2
gcd
En eet, si on se focalise sur le calcul de
uniquement, on retrouve les mmes calculs que ceux eectus dans la mthode
la seule dirence que
u2 mod v2
est maintenant calcul par
Pour justier la correction de l'algorithme d'Euclide tendu, on note que
l'invariant suivant est maintenu :
u0 u + u1 v = u2
v0 u + v1 v = v2
9.2. Exponentiation rapide
121
Programme 22 Exponentiation rapide
static int exp(int x, int n) {
if (n == 0) return 1;
int r = exp(x * x, n / 2);
return (n % 2 == 0) ? r : x * r;
}
En particulier, la premire identit est exactement celle que l'on voulait pour le rsultat.
La complexit reste la mme que pour la mthode
gcd. Le nombre d'oprations eectues
chaque tour de boucle est certes suprieur, mais il reste born et le nombre d'itrations
est exactement le mme. La complexit est donc toujours
Exercice 9.3.
extendedGcd
Modier la mthode
O(log(max(u,v))).
pour qu'elle n'alloue pas de tableau
intermdiaire.
Exercice 9.4.
(mod m).
u,v,m trois entiers strictement positifs tels que gcd(v,m) = 1. On
v modulo m tout entier w tel que 0 w < m et u vw
mthode calculant le quotient de u par v modulo m.
Soient
appelle quotient de
crire une
par
9.2 Exponentiation rapide
L'algorithme d'exponentiation rapide (en anglais
n
calculer x en exploitant les identits suivantes :
exponentiation by squaring ) consiste
x2k = (x2 )k
x
= x(x2 )k
2k+1
Sa traduction en Java, pour
de type
int,
est immdiate. On peut crire par exemple le
code donn programme 22 page 121. Il existe de multiples variantes. On peut par exemple
n = 1 mais ce n'est pas vraiment utile. Voir aussi l'exercice 9.5.
xn , cet algorithme effectue un nombre de multiplications proportionnel log(n), ce qui est une amlioration
signicative par rapport l'algorithme naf qui eectue exactement n 1 multiplications.
faire un cas particulier pour
Quoiqu'il en soit, l'ide centrale reste la suivante : pour calculer
(On verra plus loin pourquoi on s'intresse uniquement aux multiplications, et pas aux
n lui-mme.) On peut s'en convaincre aisment en montrant
2k1 n < 2k , alors la mthode exp eectue exactement k
appel rcursif exp eectuant une ou deux multiplications, on
calculs faits par ailleurs sur
par rcurrence sur
que, si
appels rcursifs. Chaque
en dduit le rsultat ci-dessus.
Les applications de cet algorithme sont innombrables, car rien n'impose
type
int.
d'tre de
Ds lors qu'on dispose d'une unit et d'une opration associative, c'est--dire
M , alors on peut appliquer cet algorithme pour calculer xn avec x M et
d'un monode
n N. Donnons un exemple. Les nombres de la suite de Fibonacci (Fn ) vrient l'identit
suivante :
n
1 1
Fn+1 Fn
=
.
(9.3)
1 0
Fn
Fn1
122
Chapitre 9. Arithmtique
Autrement dit, on peut calculer
Fn en levant une matrice 22 la puissance n. Avec l'alO(log n) oprations arithmtiques,
gorithme d'exponentiation rapide, on peut le faire en
ce qui est une amlioration signicative par rapport un calcul direct en temps linaire .
Exercice 9.5.
crire une variante de la mthode
vantes :
exp
qui repose sur les identits sui-
x2k = (xk )2
x
= x(xk )2
2k+1
Y a-t-il une dirence d'ecacit ?
Exercice 9.6.
crire un programme qui calcule
Fn
en utilisant l'quation (9.3) et l'al-
22
int, avec la matrice identit, la multiplication et l'exponentiation rapide.
gorithme d'exponentiation rapide. On crira une classe minimale pour des matrices
coecients dans
9.3 Crible d'ratosthne
De nombreuses applications requirent un test de primalit (l'entier
n est-il premier ?)
ou le calcul exhaustif des nombres premiers jusqu' un certain rang. Le crible d'ratosthne est un algorithme qui dtermine, pour un certain entier
entiers
N.
n N.
Illustrons son fonctionnement avec
N = 23.
N,
la primalit de tous les
On crit tous les entiers de 0
On va liminer progressivement tous les entiers qui ne sont pas premiers d'o le
nom de
crible.
Initialement, on se contente de dire que 0 et 1 ne sont pas premiers.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Puis on dtermine le premier entier non encore limin. Il s'agit de 2. On limine alors
tous ses multiples, savoir ici tous les entiers pairs suprieurs 2.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Puis on recommence. Le prochain entier non limin est 3. On limine donc leur tour
tous les multiples de 3.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
On note que certains taient dj limins (les multiples de 6, en l'occurrence) mais ce
n'est pas grave. Le prochain entier non limin est 5. Comme
termin. En eet, tout multiple de 5, c'est--dire
au-del de 23 si
k 5.
k 5,
5 5 > 23
le crible est
est soit dj limin si
Les nombres premiers infrieurs ou gaux
k < 5,
soit
sont alors tous les
nombres qui n'ont pas t limins, c'est--dire ici 2, 3, 5, 7, 11, 13, 17, 19 et 23.
crivons une mthode
sieve
qui ralise le crible d'ratosthne l'aide d'un tableau
de boolens, qui sera renvoy au nal.
1. Attention cependant ne pas conclure htivement qu'on sait calculer Fn pour de grandes valeurs
de n. Les lments de la suite de Fibonacci croissent en eet de manire exponentielle. Si on a recours des
entiers en prcision arbitraire, le cot des oprations arithmtiques elles-mmes doit tre pris en compte,
et la complexit ne sera pas O(log n). Et dans le cas contraire, on aura rapidement un dbordement
arithmtique.
9.3. Crible d'ratosthne
123
Programme 23 Crible d'ratosthne
static boolean[] sieve(int max) {
boolean[] prime = new boolean[max + 1];
for (int i = 2; i <= max; i++)
prime[i] = true;
int limit = (int)([Link](max));
for (int n = 2; n <= limit; n++)
if (prime[n])
for (int m = n * n; m <= max; m += n)
prime[m] = false;
return prime;
}
static boolean[] sieve(int max) {
boolean[] prime = new boolean[max + 1];
Initialement, le tableau
prime contient la valeur false dans toutes ses cases (voir page 19).
true dans toutes les cases d'indice i 2.
On commence donc par crire
for (int i = 2; i <= max; i++)
prime[i] = true;
Puis on dtermine la limite au-del de laquelle il ne sera pas ncessaire d'aller. Il s'agit
de
b maxc,
que l'on peut calculer ainsi.
int limit = (int)([Link](max));
La boucle principale du crible parcourt alors les entiers de 2
limit,
et teste chaque
fois leur primalit.
for (int n = 2; n <= limit; n++)
if (prime[n])
Le cas chant, elle limine les multiples de
n,
l'aide d'une seconde boucle. La mme
n n > N nous permet de dmarrer
n n (plutt que 2n), les multiples plus petits ayant dj t limins.
raison qui nous permet d'arrter le crible ds que
cette limination
for (int m = n * n; m <= max; m += n)
prime[m] = false;
Il ne reste plus qu' renvoyer le tableau de boolens (des exercices plus bas proposent de
renvoyer plutt un tableau contenant les nombres premiers trouvs).
return prime;
124
Chapitre 9. Arithmtique
Le code complet est donn programme 23 page 123.
valuons la complexit du crible d'ratosthne. La complexit en espace est clairement
O(N ).
La complexit en temps nous amne considrer le cot de chaque itration de la
boucle principale. S'il ne s'agit pas d'un nombre premier, le cot est constant (on ne fait
N
car
rien). Mais lorsqu'il s'agit d'un nombre premier p, alors la boucle interne un cot
p
on considre tous les multiples de p (en fait, un peu moins car on commence l'itration
p2 , mais cela ne change pas l'asymptotique). Le cot total est donc
N+
XN
p
pN
o la somme est faite sur les nombres premiers. Un thorme d'Euler nous dit que
P
1
pN p ln(ln(N )) d'o une complexit N ln(ln(N )) pour le crible d'ratosthne.
Exercice 9.7.
de la mthode
L'entier 2 tant le seul nombre premier pair, on peut optimiser le code
sieve
en traitant part le cas des nombres pairs et en progressant de 2
en 2 partir de 3 dans la boucle principale. Mettre en uvre cette ide.
Exercice 9.8.
Le type
boolean[]
a une reprsentation mmoire un peu gourmande :
chaque boolen y est en eet reprsent par un octet, soit 8 bits l o un seul surait. La
bibliothque Java y remdie en proposant une classe
BitSet
o les tableaux de boolens
sont reprsents d'une manire plus compacte. crire une variante de la mthode
renvoyant un rsultat de type
Exercice 9.9.
BitSet
crire une mthode
plutt que
boolean[].
sieve
int[] firstPrimesUpto(int max) qui renvoie un
max, dans l'ordre crois
tableau contenant tous les nombres premiers infrieurs ou gaux
sant.
Exercice * 9.10.
int[] firstNPrimes(int n) qui renvoie un
pn dsigne le n-ime
pn < n log n + n log log n ds que n 6.
crire une mthode
tableau contenant les
premiers nombres premiers. Indication : si
nombre premier, on a l'ingalit
10
Programmation dynamique et
mmosation
La programmation dynamique et la mmosation sont deux techniques trs proches
qui s'appuient sur l'ide naturelle suivante : ne pas recalculer deux fois la mme chose.
Illustrons-les avec l'exemple trs simple du calcul de la suite de Fibonacci. On rappelle
que cette suite d'entiers
(Fn ) est dnie par :
F0 = 0
F1 = 1
Fn = Fn2 + Fn1
pour
n 2.
crire une mthode rcursive qui ralise ce calcul en suivant cette dnition est immdiat.
(On calcule ici avec le type
long car les nombres de Fibonacci deviennent rapidement trs
grands.)
static long fib(int n) {
if (n <= 1) return n;
return fib(n - 2) + fib(n - 1);
}
Mais c'est aussi trs naf. Ici le problme n'est pas li un ventuel dbordement de pile
mais au fait que, lors du calcul de
de
Fi .
Fn ,
on recalcule de nombreuses fois les mmes valeurs
Ainsi pour calculer ne serait-ce que
F5 ,
on va calculer deux fois
F3
et trois fois
F2 .
De manire plus gnrale, on peut montrer que la
mthode ci-dessus est de complexit
1+ 5
n
(c'tait l'objet de l'exercice 2.1). On
exponentielle, en O( ) o est le nombre d'or
2
peut l'observer empiriquement. Sur une machine de 2011, on observe qu'il faut 2 secondes
pour calculer
F42 , 3 secondes pour F43 , 5 secondes pour F44 , etc. On reconnat l justement
les nombres de la suite de Fibonacci. On extrapole qu'il faudrait 89 secondes pour calculer
F50
et ceci se vrie la demi seconde prs !
10.1 Mmosation
Puisqu'on a compris qu'on calculait plusieurs fois la mme chose, une ide naturelle
consiste stocker les rsultats dj calculs dans une table. Il s'agit donc d'une table
126
Chapitre 10. Programmation dynamique et mmosation
associant certains entiers i la valeur de Fi . Ds lors, on procde ainsi : pour calculer
fib(n) on regarde si la table possde une entre pour n. Le cas chant, on renvoie la valeur
correspondante. Sinon, on calcule fib(n), toujours comme fib(n-2)+fib(n-1), c'est-dire rcursivement, puis on ajoute le rsultat dans la table, avant de le renvoyer. Cette
technique consistant utiliser une table pour stocker les rsultats dj calculs s'appelle
la
mmosation
(en anglais
memoization, une terminologie forge par le chercheur Donald
Michie en 1968).
Mettons en uvre cette ide dans une mthode
fibMemo. On commence par introduire
une table de hachage pour stocker les rsultats dj calculs
static HashMap<Integer, Long> memo = new HashMap<Integer, Long>();
La mthode
fibMemo
a une structure identique la fonction
commence par traiter les cas de base
F0
et
fib.
En particulier, elle
F1 .
static long fibMemo(int n) {
if (n <= 1) return n;
On aurait pu ne pas faire de cas particulier en stockant les valeurs de
table ; c'est aaire de style uniquement. Lorsque
la table
memo
si la valeur de
fib(n)
n2
F0
et
F1
dans la
on commence par regarder dans
ne s'y trouve pas dj. Le cas chant, on la renvoie.
Long l = [Link](n);
if (l != null) return l;
On utilise ici le fait que la mthode
get
de la table de hachage renvoie la valeur
null
lorsqu'il n'y a pas d'entre pour la cl donne. Dans ce cas, justement, on calcule le rsultat
exactement comme pour la mthode
fib
c'est--dire avec deux appels rcursifs.
l = fibMemo(n - 2) + fibMemo(n - 1);
Puis on le stocke dans la table
memo,
avant de le renvoyer.
[Link](n, l);
return l;
Ceci conclut la mthode
calcul de
F50 ,
fibMemo.
fib. Le
fib). On
Son ecacit est bien meilleure que celle de
par exemple, est devenu instantan (au lieu de 89 secondes avec
fibMemo est linaire. Le calcul n'est pas compltement
Fn n'implique plus maintenant
que le calcul des valeurs de Fi pour i n une seule fois chacune. Le code complet de
la mthode fibMemo est donn programme 24 page 127. On notera que la table memo est
dnie l'extrieur de la mthode fibMemo, car elle doit tre la mme pour tous les appels
peut montrer que la complexit de
trivial mais, intuitivement, on comprend que le calcul de
rcursifs.
10.2. Programmation dynamique
127
Programme 24 Calcul de Fn par mmosation et par programmation dynamique
// mmosation
static HashMap<Integer, Long> memo = new HashMap<Integer, Long>();
static long fibMemo(int n) {
if (n <= 1) return n;
Long l = [Link](n);
if (l != null) return l;
l = fibMemo(n - 2) + fibMemo(n - 1);
[Link](n, l);
return l;
}
// programmation dynamique
static long fibDP(int n) {
long[] f = new long[n + 1];
f[1] = 1;
for (int i = 2; i <= n; i++)
f[i] = f[i - 2] + f[i - 1];
return f[n];
}
10.2 Programmation dynamique
Il peut sembler inutilement coteux d'utiliser une table de hachage pour mmoriser
les calculs. En eet, on ne va stocker au nal que les valeurs de
simple tableau de taille
n+1
Fi
i n et donc un
fibMemo ci-dessus
pour
sut. Si on voulait rcrire la mthode
avec cette ide, on pourrait par exemple remplir le tableau initialement avec la valeur
dans chaque case pour signier que la valeur n'a pas encore t calcule. Mais on
peut aussi procder diremment, en remplissant le tableau dans un certain ordre. En
l'occurrence ici, on voit bien qu'il sut de le remplir dans l'ordre croissant, car le calcul
de
Fi
ncessite le calcul des
Fj
pour
j < i.
Cette technique consistant utiliser une table
et la remplir progressivement avec les rsultats des calculs intermdiaires s'appelle la
programmation dynamique
dynamic programming, souvent abrg DP).
Mettons en uvre cette ide dans une mthode fibDP. On commence allouer un
bleau f de taille n + 1 destin contenir les valeurs des Fi .
(en anglais
ta-
static long fibDP(int n) {
long[] f = new long[n + 1];
Ce tableau peut tre allou
de
fibMemo,
l'intrieur
de la mthode
fibDP
car celle-ci, la dirence
ne va pas tre rcursive. Puis on remplit les cases du tableau
de bas en
haut, c'est--dire dans le sens des indices croissants, en faisant un cas particulier pour
f[1].
f[1] = 1;
128
Chapitre 10. Programmation dynamique et mmosation
for (int i = 2; i <= n; i++)
f[i] = f[i - 2] + f[i - 1];
Une fois le tableau rempli, il ne reste plus qu' renvoyer la valeur contenue dans sa dernire
case.
return f[n];
Ceci conclut la mthode
fibDP.
Comme pour
se voit facilement) et le calcul de
complet de la mthode
fibDP
F50 ,
fibMemo,
son ecacit est linaire (ici cela
par exemple, est galement instantan. Le code
est donn programme 24 page 127.
10.3 Comparaison
Le code des mthodes
fibMemo et fibDP peut nous laisser penser que la programmation
dynamique est plus simple mettre en uvre que la mmosation. Sur cet exemple,
c'est vrai. Mais il faut comprendre que, pour crire
fibDP,
nous avons exploit deux
informations capitales : le fait de savoir qu'il fallait calculer les
Fi
pour tous les
i n,
et
le fait de savoir qu'on pouvait les calculer dans l'ordre croissant. De manire gnrale, les
entres de la fonction calculer ne sont pas ncessairement des indices conscutifs, ou ne
sont mme pas des entiers, et les dpendances entre les diverses valeurs calculer ne sont
pas ncessairement aussi simples. En pratique, la mmosation est plus simple mettre
en uvre : il sut en eet de rajouter quelques lignes pour consulter et remplir la table
de hachage, sans modier la structure de la fonction.
Il existe cependant quelques rares situations o la programmation dynamique peut
tre prfre la mmosation. Prenons l'exemple du calcul des coecients binomiaux
C(n,k)
dont la dnition rcursive est la suivante :
C(n,0) = 1
C(n,n) = 1
C(n,k) = C(n 1,k 1) + C(n 1,k)
pour
0 < k < n.
On peut appliquer cette dnition aussi bien la mmosation que la programmation
dynamique. Dans le premier cas, on aura une table de hachage indexe par le couple
(n,k),
et dans le second cas, on aura une matrice indexe par n et k . Cependant, si on
5
5
cherche calculer C(n,k) pour n = 2 10 et k = 10 , alors il est probable qu'on va
dpasser les capacits mmoire de la machine (sur une machine de bureau raisonnable),
dans le premier cas en remplissant la table de hachage et dans le second cas en tentant
d'allouer la matrice. La raison est que la complexit est ici en temps
et en espace
en
O(nk).
Dans l'exemple ci-dessus, il faudrait stocker au minimum 15 milliards de rsultats.
Pourtant, y regarder de plus prs, le calcul des
ne ncessite que les valeurs des
de
C(n 1,k).
C(n,k) pour une certaine valeur de n
Ds lors on peut les calculer pour des valeurs
croissantes, sans qu'il soit utile de conserver toutes les valeurs calcules jusque l.
On le visualise mieux en dessinant le triangle de Pascal
10.3. Comparaison
129
1
1
1
.
.
.
10
10
1
1
et en expliquant que l'on va le calculer ligne ligne, en ne conservant chaque fois que
la ligne prcdente pour le calcul de la ligne suivante. Mettons cette ide en uvre dans
une mthode
taille
n+1
cnkSmartDP(int n, int k).
On commence par allouer un tableau
row
de
qui va contenir une ligne du triangle de Pascal.
static long cnkSmartDP(int n, int k) {
int[] row = new int[n+1];
(On pourrait se contenter d'un tableau de taille
k+1 ;
voir l'exercice 10.1.) Pour l'instant,
ce tableau ne contient que des valeurs nulles. On initialise sa toute premire case avec 1.
row[0] = 1;
Cette valeur ne bougera plus car la premire colonne du triangle de Pascal ne contient
que des 1. On crit ensuite une boucle pour calculer la ligne
du triangle de Pascal :
for (int i = 1; i <= n; i++)
Ce calcul va se faire en place dans le tableau
row,
sachant qu'il contient la ligne
i 1.
Pour ne pas se marcher sur les pieds, on va calculer les nouvelles valeurs de la droite vers
la gauche, car elles ne dpendent pas de valeurs situes plus droite. On procde avec
une seconde boucle :
for (int j = i; j >= 1; j--)
row[j] += row[j-1];
row[i] contient 0, ce qui nous dispense d'un cas particulier.
On s'arrte j = 1 car la valeur row[0] n'a pas besoin d'tre modie, comme expliqu
plus haut. Une fois qu'on est sorti de cette double boucle, le tableau row contient la ligne
n du triangle de Pascal, et on n'a plus qu' renvoyer row[k] :
On exploite ici le fait que
return row[k];
La complexit en temps reste
O(nk)
mais la complexit en mmoire n'est plus que O(n).
n = 2 105 et k = 105 , le rsultat est instantan
Dans l'exemple donn plus haut, avec
(on notera cependant qu'il provoque un dbordement arithmtique ; voir l'exercice 10.2).
La conclusion de cette petite exprience est que la programmation dynamique peut
parfois tre plus avantageuse que la mmosation, car elle permet un contrle plus n
des ressources mmoire. Mais dans les trs nombreuses situations o ce contrle n'est pas
ncessaire, la mmosation est plus simple mettre en uvre.
Exercice 10.1.
cnkSmartDP
colonne k.
Modier la mthode
triangle de Pascal au-del de la
pour ne pas calculer les valeurs du
130
Chapitre 10. Programmation dynamique et mmosation
Exercice 10.2.
Modier la mthode cnkSmartDP pour qu'elle renvoie un rsultat de
BigInteger. Il s'agit l d'une classe de la bibliothque Java reprsentant des entiers
en prcision arbitraire. Attention : la complexit n'est plus O(nk) car les additions ne sont
type
plus des oprations atomiques ; leur cot dpend de la taille des oprandes, qui grandit
vite dans le triangle de Pascal.
Exercice 10.3.
Modier la mthode
seulement deux entiers.
fibDP
pour qu'elle n'utilise plus de tableau, mais
11
Rebroussement (backtracking )
La technique du
rebroussement
(en anglais
backtracking )
consiste rsoudre un pro-
blme en parcourant un espace, possiblement inni, en prenant des dcisions et en faisant,
lorsque c'est ncessaire, machine arrire. Illustrons cette technique avec un exemple canonique : le problme des
reines. Il s'agit de placer
reines sur un chiquier
N N
de
telle sorte qu'aucune reine ne soit en prise avec une autre. Voici par exemple l'une des 92
solutions pour
N =8
q
q
q
q
q
q
On va procder de faon relativement brutale, par exploration de toutes les possibilits.
On fait cependant preuve d'un peu d'intelligence en remarquant qu'une solution comporte
ncessairement une et une seule reine sur chaque ligne de l'chiquier. Du coup, on va
chercher remplir l'chiquier ligne par ligne, en positionnant chaque fois une reine sans
qu'elle soit en prise avec les reines dj poses. Ainsi, si on a dj pos trois reines sur les
trois premires lignes de l'chiquier, alors on en vient chercher une position valide sur
la quatrime ligne :
q
q
? ? ? ? ? ? ? ?
Si on en trouve une, alors on place une reine cet endroit et on poursuit l'exploration
avec la ligne suivante. Sinon,
on fait machine arrire
sur l'un des choix prcdents, et
on recommence. Si on parvient remplir la dernire ligne, on a trouv une solution. En
procdant ainsi de manire systmatique, on ne ratera pas de solution.
Chapitre 11. Rebroussement (backtracking )
132
crivons une mthode
int[] findSolution(int n) qui met en uvre cette technique
et renvoie la solution trouve, le cas chant, ou lve une exception pour signaler l'absence
de solution. On commence par allouer un tableau
cols qui contiendra, pour chaque ligne
de l'chiquier, la colonne o se situe la reine place sur cette ligne.
static int[] findSolution(int n) {
int[] cols = new int[n];
n 1. Le cur de l'algorithme de rebroussement va tre crit dans une seconde mthode, findSolutionRec,
C'est donc un tableau de taille
n,
contenant des entiers entre 0 et
laquelle on passe le tableau d'une part et un indice indiquant la prochaine ligne de l'chiquier remplir d'autre part. Elle renvoie un boolen signalant le succs de la recherche.
Il sut donc de l'appeler et de traiter correctement sa valeur de retour.
if (findSolutionRec(cols, 0)) return cols;
throw new Error("no solution for n=" + n);
On en vient la mthode
findSolutionRec proprement dite. Elle est crite rcursivement.
Le cas de base correspond un chiquier o toutes les lignes ont t remplies. On renvoie
alors
true
pour signaler le succs de la recherche.
static boolean findSolutionRec(int[] cols, int r) {
if (r == [Link])
return true;
Dans le cas contraire, il faut essayer successivement toutes les colonnes possibles pour la
reine de la ligne
r.
Pour chacune, on enregistre le choix qui est fait dans le tableau
cols.
for (int c = 0; c < [Link]; c++) {
cols[r] = c;
Puis on teste que ce choix est cohrent avec les choix prcdents. Pour cela on va crire
check qui fait cette vrication. Si le test est positif, on apfindSolutionRec pour continuer le remplissage partir de la ligne
succs, on renvoie true immdiatement.
(plus loin) une mthode
pelle rcursivement
suivante. En cas de
if (check(cols, r) && findSolutionRec(cols, r + 1))
return true;
Dans le cas contraire, on poursuit la boucle avec la valeur suivante de
c.
Si on nit par
sortir de la boucle, c'est que toutes les colonnes ont t essayes sans succs. On signale
alors une recherche infructueuse.
}
return false;
Il nous reste crire la mthode
pour la ligne
check
qui vrie que le choix qui vient juste d'tre fait
est cohrent avec les choix prcdents. C'est une simple boucle sur les
premires lignes.
133
Programme 25 Le problme des N reines
static boolean check(int[] cols, int r) {
for (int q = 0; q < r; q++)
if (cols[q] == cols[r] || [Link](cols[q] - cols[r]) == r - q)
return false;
return true;
}
static boolean findSolutionRec(int[] cols, int r) {
if (r == [Link])
return true;
for (int c = 0; c < [Link]; c++) {
cols[r] = c;
if (check(cols, r) && findSolutionRec(cols, r + 1))
return true;
}
return false;
}
static int[] findSolution(int n) {
int[] cols = new int[n];
if (findSolutionRec(cols, 0))
return cols;
throw new Error("no solution for n=" + n);
}
static boolean check(int[] cols, int r) {
for (int q = 0; q < r; q++)
Pour chaque ligne
q,
on vrie que les deux reines ne sont sur la mme colonne et ni sur
une mme diagonale. Si c'est le cas, on choue immdiatement.
if (cols[q] == cols[r] || [Link](cols[q] - cols[r]) == r - q)
return false;
On notera l'utilisation de la valeur absolue pour viter de distinguer les deux types de
diagonales (montante et descendante). Si en revanche on sort de la boucle, alors on signale
une vrication positive.
return true;
Le code complet est donn programme 25 page 133. Il est important de bien comprendre
que ce programme s'interrompt la premire solution trouve. C'est le rle du second
return true
plac au milieu de la boucle de la mthode
findSolutionRec.
Chapitre 11. Rebroussement (backtracking )
134
Exercice 11.1.
Modier le programme prcdent pour qu'il dnombre toutes les so-
lutions. Les solutions ne seront pas renvoyes, mais seulement leur nombre total. Pour
1 N 9,
on doit obtenir les valeurs 1, 0, 0, 2, 10, 4, 40, 92, 352.
Optimisation.
Si on s'intresse au problme du dnombrement de toutes les solutions,
on ne connat pas ce jour de mthode qui soit fondamentalement meilleure que la
recherche exhaustive que nous venons de prsenter . Nanmoins, cela ne nous empche pas
de chercher optimiser le programme prcdent. Une ide naturelle consiste maintenir,
pour chaque ligne de l'chiquier, les colonnes sur lesquelles on peut encore placer une
reine. Ainsi, plutt que d'essayer systmatiquement les
colonnes de la ligne courante,
on peut esprer avoir en examiner beaucoup moins que
et, en particulier, faire
machine arrire plus rapidement.
Illustrons cette ide avec
N = 8.
Supposons qu'on
ait dj plac des reines sur les trois premires lignes.
Alors seules cinq colonnes (ici dessines en rouge)
doivent tre considres pour la quatrime ligne.
qqq
en prise avec les reines dj places le long d'une diagonale ascendante. Ces trois positions (ici en rouge)
qq
ne doivent pas tre considres.
en prise avec des reines dj places le long d'une dia-
gonale descendante. Ces deux positions (ici en rouge)
ne doivent pas tre considres.
ligne qui ne peuvent plus tre considres. Il ne reste
q
q
(2)
(3)
Au nal, ce sont donc six positions de la quatrime
(1)
q
q
De mme, deux positions de la quatrime ligne sont
drer, au lieu de 8 dans le programme prcdent.
q
q
Par ailleurs, trois positions de la quatrime ligne sont
nalement que deux positions (ici en rouge) consi-
(4)
Mettons en uvre cette ide dans un programme qui dnombre les solutions du problme des
reines. Il nous faut reprsenter les ensembles de colonnes considrer, ou
exclure, qui sont matrialises en rouge dans les gures ci-dessus. Comme il s'agit de
petits ensembles dont les lments sont dans
reprsenter par des valeurs de type
int,
{0, . . . ,N 1}, on peut avantageusement les
i indiquant la prsence de l'entier i dans
le bit
l'ensemble. Ainsi, dans la gure 1 ci-dessus, les cinq colonnes considrer sur la quatrime
ligne peuvent tre reprsentes par l'entier dont l'criture binaire est
111001012 ,
c'est--
dire 229 (l'usage est d'crire les bits de poids faible droite). De la mme manire, les
trois positions en prise par une diagonale ascendante (gure 2) correspondent l'entier
104 = 011010002 , et les deux positions en prise par une diagonale descendante (gure 3)
l'entier 9 = 000010012 . L'intrt de cette reprsentation est que les oprations sur les
entiers fournies par la machine vont nous permettre de calculer trs ecacement certaines
1. Si en revanche le problme est de trouver une solution, alors il existe un algorithme polynmial.
135
oprations ensemblistes . Ainsi, si
a, b
et
sont trois variables de type
int
contenant
respectivement les entiers 229, 104 et 9, c'est--dire les trois ensembles ci-dessus, alors
on peut calculer l'ensemble correspondant la gure 4 avec l'expression
L'oprateur
&
de Java est le ET bit bit et l'oprateur
ensemblistes, on vient de calculer
a\b\c.
Sur la base de cette ide, crivons une mthode rcursive
prend justement en arguments les trois entiers
a, b
et
a & b & c.
le NON bit bit. En termes
c.
countSolutionsRec
qui
static int countSolutionsRec(int a, int b, int c) {
L'entier
a dsigne les colonnes restant pourvoir, l'entier b (resp. c) les colonnes interdites
car en prise sur une diagonale ascendante (resp. descendante). La mthode renvoie le
nombre de solutions qui sont compatibles avec ces arguments. La recherche parvient son
terme lorsque
a devient vide, c'est--dire 0. On signale alors la dcouverte d'une solution.
if (a == 0) return 1;
a & b & c,
comme expliqu ci-dessus, dans une variable e. On initialise galement une variable f pour
Dans le cas contraire, on calcule les colonnes considrer avec l'expression
tenir le compte des solutions.
int e = a & b & c, f = 0;
Notre objectif est maintenant de parcourir tous les lments de l'ensemble reprsent
e, le plus ecacement possible. Nous allons parcourir les bits de e qui sont 1, et les
supprimer au fur et mesure, jusqu' ce que e devienne nul.
par
while (e != 0) {
Il existe une astuce arithmtique qui nous permet d'extraire exactement un bit d'un entier
non nul. Elle exploite la reprsentation en complment deux des entiers, en combinant
le ET bit bit et la ngation (entire) :
int d = e & -e;
Pour s'en convaincre, il faut se souvenir qu'en complment deux,
(Un excellent ouvrage plein de telles astuces est
Hacker's Delight
-e
est gal
[9].) Ce bit
e + 1.
e que
de
l'on vient d'extraire reprsente une colonne considrer. Il nous sut donc de procder
un appel rcursif, en mettant jour les valeurs de
a, b
et
en consquence.
f += countSolutionsRec(a - d, (b + d) << 1, (c + d) >> 1);
Le bit
a t retir de
a avec une
a), et a t
soustraction (il n'y a pas de retenue car
tait
c avec une addition (de mme il n'y
c). Les valeurs de b et c sont dcales
d'un bit, respectivement vers la gauche et vers la droite avec les oprateurs << et >>, pour
exprimer le passage la ligne suivante. Une fois le rsultat accumul dans f, on supprime
le bit d de e, pour passer maintenant au bit suivant de e.
ncessairement un bit de
a pas de retenue car
e -= d;
ajout
n'tait pas un bit de
et
ou
Chapitre 11. Rebroussement (backtracking )
136
Programme 26 Le problme des N reines (dnombrement)
static int countSolutionsRec(int a, int b, int c) {
if (a == 0) return 1;
int f = 0, e = a & b & c;
while (e != 0) {
int d = e & -e;
f += countSolutionsRec(a - d, (b + d) << 1, (c + d) >> 1);
e -= d;
}
return f;
}
static int countSolutions(int n) {
return countSolutionsRec((0 << n), 0, 0);
}
Une fois sorti de la boucle, il n'y a plus qu' renvoyer la valeur de
return f;
Le programme principal se contente d'un appel
de
f.
reprsentant l'ensemble
countSolutionsRec,
avec une valeur
{0, . . . ,N 1}
static int countSolutions(int n) {
return countSolutionsRec((0 << n), 0, 0);
}
Le code complet est donn programme 26 page 136. Si on compare les performances
de ce programme avec le prcdent (en supposant l'avoir adapt comme suggr dans
l'exercice 11.1), on observe un gain notable de performance. Ainsi pour
N = 14,
on
dnombre les 365 596 solutions en un quart de seconde, au lieu de prs de 8 secondes. Plus
intressant que le temps lui-mme est le nombre de dcisions prises. On le donne ici pour
les deux versions du dnombrement, pour quelques valeurs de
N.
10
11
12
13
14
version nave
version optimise
3,5 105
3,6 104
1,8 106
1,7 105
1,0 107
8,6 105
6,0 107
4,7 106
3,8 108
2,7 107
rapport
9,80
10,8
11,8
12,8
13,8
Comme on le constate empiriquement, le rapport augmente avec
comme
N.
N,
approximativement
12
Tri
Ce chapitre prsente plusieurs algorithmes de tri. On suppose pour simplier qu'on
trie des tableaux d'entiers (type
int[]),
dans l'ordre croissant. la n du chapitre,
nous expliquons comment gnraliser des lments d'un type quelconque. On note
le
nombre d'lments trier. Pour chaque tri prsent, on indique sa complexit en nombre
de comparaisons eectues et en nombre d'aectations.
On rappelle que la complexit optimale d'un tri eectuant uniquement des comparaisons d'lments est en
O(N log N ).
En eet, on peut visualiser un tel algorithme comme
un arbre binaire. Chaque nud interne reprsente une comparaison eectue, le sousarbre gauche (resp. droit) reprsentant la suite de l'algorithme lorsque le test est positif
(resp. ngatif ). Chaque feuille reprsente un rsultat possible, c'est--dire une permutation eectue sur la squence initiale. Si on suppose les
permutations possibles, donc au moins
moins gale
log N !.
N!
lments distincts, il y a
N!
feuilles cet arbre. Sa hauteur est donc au
Or le plus long chemin de la racine une feuille reprsente le plus
grand nombre de comparaisons eectues par l'algorithme sur une entre. Il existe donc
une entre pour laquelle le nombre de comparaisons est au moins
log(N !) N log N . Pour une preuve
The Art of Computer Programming [6, Sec. 5.3].
de Stirling, on sait que
consulter
log(N !).
Par la formule
plus dtaille, on pourra
12.1 Tri par insertion
Le tri par insertion est sans doute le tri le plus naturel. Il consiste insrer successivement chaque lment dans l'ensemble des lments dj tris. C'est ce que l'on fait
naturellement quand on trie un jeu de cartes ou un paquet de copies.
Le tri par insertion d'un tableau
sivement chaque lment
a[i]
s'eectue en place. Il consiste insrer succes-
dans la portion du tableau
a[0..i-1]
correspond la situation suivante :
i-1
. . . dj tri . . .
On commence par une boucle
for
a[i]
. . . trier . . .
pour parcourir le tableau :
static void insertionSort(int[] a) {
for (int i = 1; i < [Link]; i++) {
dj trie, ce qui
138
Chapitre 12. Tri
Programme 27 Tri par insertion
static void insertionSort(int[] a) {
for (int i = 1; i < [Link]; i++) {
int v = a[i], j = i;
for (; 0 < j && v < a[j-1]; j--)
a[j] = a[j-1];
a[j] = v;
}
}
Pour insrer l'lment
a[i] la bonne place, on utilise une seconde boucle qui dcale vers
a[i].
la droite les lments tant qu'ils sont suprieurs
int v = a[i], j = i;
for (; 0 < j && v < a[j-1]; j--)
a[j] = a[j-1];
Une fois sorti de la boucle, il reste positionner
donne par
a[i] sa place, c'est--dire la position
a[j] = v;
Le code complet est donn programme 27 page 138.
Complexit.
On note que la mthode
insertionSort
eectue exactement le mme
nombre de comparaisons et d'aectations. Lorsque la boucle
la position
i,
i k,
elle eectue
k+1
for insre l'lment a[i]
k vaut 0 et au pire k vaut
comparaisons. Au mieux
ce qui donne au nal le tableau suivant :
meilleur cas
moyenne
pire cas
N
N
N 2 /2
N 2 /2
comparaisons
aectations
N /4
N 2 /4
12.2 Tri rapide
Le tri rapide consiste appliquer la mthode
diviser pour rgner
: on partage les
lments trier en deux sous-ensembles, les lments du premier tant plus petits que
les lments du second, puis on trie rcursivement chaque sous-ensemble. En pratique, on
ralise le partage l'aide d'un lment
arbitraire de l'ensemble trier, appel
pivot.
Les deux sous-ensembles sont alors respectivement les lments plus petits et plus grands
que
p.
Le tri rapide d'un tableau s'eectue en place. On le paramtre par deux indices
indiquant la portion du tableau trier,
crire une mthode
l'lment
a[l]
partition
tant inclus et
et
exclus. On commence par
qui va dplacer les lments autour du pivot. On choisit
comme pivot, arbitrairement :
12.2. Tri rapide
139
static int partition(int[] a, int l, int r) {
int p = a[l];
On suppose ici
l < r,
ce qui nous autorise accder
a[l].
Le principe consiste alors
parcourir le tableau de la gauche vers la droite, entre les indices
avec une boucle
for.
l
p
L'indice
(inclus) et
(exclus),
chaque tour de boucle, la situation est la suivante :
<p
r
?
de la boucle dnote le prochain lment considrer et l'indice
partitionne
la portion dj parcourue.
int m = l;
for (int i = l + 1; i < r; i++)
a[i] est suprieur ou gal v, il n'y a rien faire. Dans le cas contraire, pour conserver
l'invariant de boucle, il sut d'incrmenter m et d'changer a[i] et a[m].
Si
if (a[i] < p)
swap(a, i, ++m);
(Le code de la mthode
swap
est donn page 140.) Une fois sorti de la boucle, on met le
pivot sa place, c'est--dire la position
m,
et on renvoie cet indice.
swap(a, l, m);
return m;
On peut bien entendu se dispenser de l'appel
swap
lorsque
l = m,
mais cela ne change
rien fondamentalement. On crit alors la partie rcursive du tri rapide sous la forme
d'une mthode
l r-1,
quickrec
qui prend les mmes arguments que la mthode
partition.
Si
il y a au plus un lment trier et il n'y a donc rien faire.
static void quickrec(int[] a, int l, int r) {
if (l >= r-1) return;
Sinon, on partitionne les lments entre
et
r.
int m = partition(a, l, r);
a[m] se retrouve
trier a[l..m[ et a[m+1..r[,
Aprs cet appel, le pivot
appels rcursifs pour
sa place dnitive. On eectue alors deux
quickrec(a, l, m);
quickrec(a, m + 1, r);
ce qui achve la mthode
quickrec.
Pour trier un tableau, il sut d'appeler
sur la totalit de ses lments.
static void quicksort(int[] a) {
quickrec(a, 0, [Link]);
}
Le code complet est donn programme 28 page 140.
quickrec
140
Programme 28 Tri rapide
static void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
// suppose l < r i.e. au moins un lment
static int partition(int[] a, int l, int r) {
int p = a[l];
int m = l;
for (int i = l + 1; i < r; i++)
if (a[i] < p)
swap(a, i, ++m);
swap(a, l, m);
return m;
}
static void quickrec(int[] a, int l, int r) {
if (l >= r-1) return;
int m = partition(a, l, r);
quickrec(a, l, m);
quickrec(a, m + 1, r);
}
static void quicksort(int[] a) {
quickrec(a, 0, [Link]);
}
Chapitre 12. Tri
12.2. Tri rapide
Complexit.
141
partition fait toujours exactement r l 1 comparaisons.
quicksort, notons C(N ) le nombre de comparaisons qu'elle eectue sur
une portion de tableau longueur N . Si la mthode partition a dtermin une portion de
longueur K et une autre de longueur N 1 K , on a donc au total
La mthode
Pour la mthode
C(N ) = N 1 + C(K) + C(N 1 K).
Le pire des cas correspond K = 0, ce qui donne C(N ) = N 1 + C(N 1), d'o
2
C(N ) N2 . Le meilleur des cas correspond une liste coupe en deux moitis gales,
c'est--dire K = N/2. On en dduit facilement C(N ) N log N . Pour le nombre de
comparaisons en moyenne, on considre que les
places nales possibles pour le pivot
sont quiprobables, ce qui donne
C(N ) = N 1 +
1
N
2
= N 1+
N
C(K) + C(N 1 K)
0KN 1
C(K).
0KN 1
Aprs un peu d'algbre (laisse au lecteur), on parvient
C(N )
C(N 1)
2
2
=
+
.
N +1
N
N + 1 N (N + 1)
Il s'agit d'une somme tlscopique, qui permet de conclure que
C(N )
2 log N
N +1
et donc que
C(N ) 2N log N .
partition eectue
swap que d'incrmentations de m. Le meilleur des cas est atteint lorsque
En ce qui concerne le nombre d'aectations, on note que la mthode
autant d'appels
le pivot est toujours sa place. Il n'y a alors aucune aectation. Il est important de
noter que ce cas ne correspond pas la meilleure complexit en termes de comparaisons
(qui est alors quadratique). En moyenne, toutes les positions nales pour le pivot tant
quiprobables, on a donc moins de
r l + 1 aectations (chaque appel swap ralise deux
aectations), d'o un calcul analogue au nombre moyen de comparaisons. Dans le pire
des cas, le pivot se retrouve toujours la position r-1. La mthode partition eectue
2(r l) aectations, d'o un total de N 2 aectations. Au nal, on obtient donc les
alors
rsultats suivants :
comparaisons
aectations
Amliorations.
meilleur cas
moyenne
pire cas
N log N
2N log N
2N log N
N 2 /2
N2
Choisir systmatiquement le premier lment de l'intervalle
a[l..r[
comme pivot n'est pas forcment une bonne ide. Par exemple, si le tableau est dj
tri, on se retrouvera avec une complexit quadratique. Il est prfrable de choisir le pivot
alatoirement parmi les valeurs de
a[l..r[. Une solution trs simple consiste mlanger le
142
Chapitre 12. Tri
tableau
avant
de commencer le tri rapide. L'exercice 3.3 propose justement une mthode
trs simple pour eectuer un tel mlange.
Toutefois, si les valeurs du tableau sont toutes identiques, cela ne sura pas. En eet,
le pivot se retrouvera une extrmit de l'intervalle et on aura toujours une complexit
quadratique. La solution ce problme consiste en une mthode
partition
un peu plus
subtile, propose dans l'exercice 12.1 ci-dessous. Avec ces deux amliorations, on peut
considrer en pratique que le tri rapide est toujours en
O(N log N ).
Comme on le voit, raliser un tri rapide en prenant soin d'viter un pire cas quadratique
n'est pas si facile que cela. Dans les sections suivantes, nous prsentons deux autres tris,
le tri par tas et le tri fusion, qui ont tous les deux une complexit
O(N log N ) dans le pire
des cas tout en tant plus simples raliser. Nanmoins, le tri rapide est souvent prfr
en pratique, car meilleur en temps que le tri par tas et meilleur en espace que le tri fusion.
Exercice 12.1.
Modier la fonction
partition
pour qu'elle spare les lments stric-
tement plus petits que le pivot ( gauche), les lments gaux au pivot (au milieu) et les
lments strictement plus grands que le pivot ( droite). Au lieu de deux indices
et
dcoupant le segment de tableau en trois parties, comme illustr sur la gure page 139, on
utilisera trois indices dcoupant le segment de tableau en quatre parties. (Un tel dcoupage en trois est aussi l'objet de l'exercice 12.8 plus loin.) La nouvelle fonction
doit maintenant renvoyer deux indices. Modier la fonction
quick_rec
partition
en consquence.
Exercice 12.2.
Pour viter le dbordement de pile potentiel de la mthode
quickrec,
une ide consiste eectuer d'abord l'appel rcursif sur la plus petite des deux portions,
puis remplacer le second appel rcursif par une boucle
ou
while
en modiant la valeur de
en consquence. Montrer que la taille de pile est alors logarithmique dans le pire
des cas.
Exercice 12.3.
Une ide classique pour acclrer un algorithme de tri consiste eec-
tuer un tri par insertion quand le nombre d'lments trier est petit,
i.e. devient infrieur
une constante xe l'avance (par exemple 5). Modier le tri rapide pour prendre en
insertionSort de la section prdeux indices l et r pour dlimiter la
compte cette ide. On pourra reprendre la mthode
cdente (gure 27) et la gnraliser en lui passant
portion du tableau trier.
12.3 Tri fusion
Comme le tri rapide, le tri fusion applique le principe
diviser pour rgner. Il partage les
lments trier en deux parties de mme taille, sans chercher comparer leurs lments.
Une fois les deux parties tries rcursivement, il les fusionne, d'o le nom de tri fusion.
Ainsi on vite le pire cas du tri rapide o les deux parties sont de tailles disproportionnes.
On va chercher raliser le tri en place, en dlimitant la portion trier par deux indices
l (inclus) et r (exclus). Pour le partage, il sut de calculer l'indice mdian m = l+2 r . On
trie alors rcursivement les deux parties dlimites par l et m d'une part, et m et r d'autre
part. Il reste eectuer la fusion. Il s'avre extrmement dicile de la raliser en place.
12.3. Tri fusion
143
Le plus simple est d'utiliser un second tableau, allou une et une seule fois au dbut du
tri.
merge
On commence par crire une mthode
guments deux tableaux,
a1
et
a2,
qui ralise la fusion. Elle prend en ar-
et les trois indices
l, m
et
r.
Les portions
a1[l..m[
et
a1[m..r[ sont supposes tries. L'objectif est de les fusionner dans a2[l..r[. Pour cela, on va
parcourir les deux portions de a1 avec deux variables i et j et la portion de a2 remplir
avec une boucle for.
static void merge(int[] a1, int[] a2, int l, int m, int r) {
int i = l, j = m;
for (int k = l; k < r; k++)
chaque tour de boucle, la situation est donc la suivante :
a1
tri
tri
a2
tri
k
Il faut alors dterminer la prochaine valeur placer en
des deux valeurs
a1[i]
et
a1[j].
a2[k].
Il s'agit de la plus petite
Il convient cependant de traiter correctement le cas o
il n'y a plus d'lment dans l'une des deux moitis. On dtermine si l'lment doit tre
pris dans la moiti gauche avec le test suivant :
if (i < m && (j == r || a1[i] <= a1[j]))
Dans les deux cas, on copie l'lment dans
a2[k] et on incrmente l'indice correspondant.
a2[k] = a1[i++];
else
a2[k] = a1[j++];
merge. La partie rcursive du tri fusion est matrialise par une
mthode rcursive mergesortrec qui prend en arguments deux tableaux a et tmp et deux
indices l et r dlimitant la portion trier. Elle trie a[l..r[ en se servant du tableau tmp
comme temporaire. Si le segment contient au plus un lment, c'est--dire si l r-1, il
Ceci achve la mthode
n'y a rien faire.
static void mergesortrec(int[] a, int[] tmp, int l, int r) {
if (l >= r-1) return;
Sinon, on partage l'intervalle deux moitis gales en calculant l'lment mdian
int m = (l + r) / 2;
(Le calcul de
(l + r) / 2
peut provoquer un dbordement de capacit arithmtique ;
voir exercice 3.7.) On trie alors rcursivement les deux portions
en utilisant
tmp
comme temporaire.
mergesortrec(a, tmp, l, m);
mergesortrec(a, tmp, m, r);
a[l..m[ et a[m..r[, toujours
144
Chapitre 12. Tri
Programme 29 Tri fusion
static void merge(int[] a1, int[] a2, int l, int m, int r) {
int i = l, j = m;
for (int k = l; k < r; k++)
if (i < m && (j == r || a1[i] <= a1[j]))
a2[k] = a1[i++];
else
a2[k] = a1[j++];
}
static void mergesortrec(int[] a, int[] tmp, int l, int r) {
if (l >= r-1) return;
int m = (l + r) / 2;
mergesortrec(a, tmp, l, m);
mergesortrec(a, tmp, m, r);
for (int i = l; i < r; i++)
tmp[i] = a[i];
merge(tmp, a, l, m, r);
}
static void mergesort(int[] a) {
mergesortrec(a, new int[[Link]], 0, [Link]);
}
On recopie ensuite tous les lments de la portion
proprement dite avec la mthode
merge
a[l..r[ dans tmp avant de faire la fusion
for (int i = l; i < r; i++)
tmp[i] = a[i];
merge(tmp, a, l, m, r);
mergesortrec. Le tri d'un tableau complet a consiste alors en un
mergesortrec, en allouant un temporaire de la mme taille que a :
Ceci achve la mthode
simple appel
static void mergesort(int[] a) {
mergesortrec(a, new int[[Link]], 0, [Link]);
}
Le code complet est donn programme 29 page 144. On peut encore amliorer l'ecacit de
ce code. Comme pour le tri rapide, on peut utiliser un tri par insertion quand la portion
trier devient susamment petite (voir exercice 12.3). Une autre ide, indpendante,
consiste viter la copie de
Complexit.
par
vers
tmp.
L'exercice 12.4 propose une solution.
C(N ) (resp. f (N )) le nombre total de comparaisons eectues
merge), on a l'quation
Si on note
mergesortrec
(resp.
C(N ) = 2C(N/2) + f (N )
12.3. Tri fusion
145
car les deux appels rcursifs se font sur deux listes de mme longueur
meilleur des cas, la mthode
merge
N/2.
Dans le
n'examine que les lments de l'une des deux listes
car ils sont tous plus petits que ceux de l'autre liste. Dans ce cas f (N ) = N/2 et donc
C(N ) 21 N log N . Dans le pire des cas, tous les lments sont examins par merge et
donc f (N ) = N 1, d'o C(N ) N log N . L'analyse en moyenne est plus subtile (voir
C(N ) N log N galement.
N aectations dans la mlment est copi de a1 vers a2) et N aectations eectues par
note A(N ) le nombre total d'aectations pour mergesort, on a
[6, ex 2 p. 646]) et donne
f (N ) = N 2 + o(1),
d'o
Le nombre d'aectations est le mme dans tous les cas :
merge (chaque
mergesortrec. Si on
thode
donc
A(N ) = 2A(N/2) + 2N,
d'o un total de
2N log N
aectations.
meilleur cas
comparaisons
aectations
Exercice 12.4.
1
N
2
log N
2N log N
Pour viter la copie de
moyenne
pire cas
N log N
2N log N
N log N
2N log N
a vers tmp dans la mthode mergesortrec, une
a tout en les dplaant vers le tableau tmp,
ide consiste trier les deux moitis du tableau
tmp vers a comme on le fait dj. Cependant, pour trier les lments de
a vers tmp, il faut, inversement, trier les deux moitis en place puis fusionner vers tmp. On
puis fusionner de
a donc besoin de deux mthodes de tri mutuellement rcursives. On peut cependant n'en
n'crire qu'une seule, en passant un paramtre supplmentaire indiquant si le tri doit tre
fait en place ou vers
tmp.
Modier les mthodes
mergesortrec
et
mergesort
en suivant
cette ide.
Exercice 12.5.
Le tri fusion est une bonne mthode pour trier des listes. Supposons
LinkedList<Integer>. crire tout
split qui prend en arguments trois listes l1, l2 et l3 et met la
moiti des lments de l1 dans l2 et l'autre moiti dans l3 (par exemple un lment
sur deux). La mthode split ne doit pas modier la liste l1. crire ensuite une mthode
merge qui prend en arguments deux listes l1 et l2, supposes tries, et renvoie la fusion de
par exemple que l'on souhaite trier des listes de type
d'abord une mthode
ces deux listes. Elle peut vider ses deux arguments de leurs lments. crire une mthode
rcursive
mergesort
qui prend une liste en argument et renvoie une nouvelle liste trie
contenant les mmes lments. Elle ne doit pas modier son argument.
Exercice * 12.6.
Le tri fusion permet galement de trier des listes
en place, c'est--dire
sans aucune allocation supplmentaire, ds lors que le contenu et la structure des listes
peuvent tre modis. Considrons par exemple les listes d'entiers du type
Singly
de
Singly split(Singly l) qui coupe la
liste l en son milieu, c'est--dire remplace le champ next qui relie la premire moiti la
seconde moiti par null et renvoie le premier lment de la seconde moiti. On pourra supposer que la liste l contient au moins deux lments. crire ensuite une mthode Singly
merge(Singly l1, Singly l2) qui fusionne deux listes l1 et l2, supposes tries, et renvoie le premier lment du rsultat. La mthode merge doit procder en place, sans allouer
de nouvel objet de la classe Singly. On pourra commencer par crire la mthode merge
la section 4.1. crire tout d'abord une mthode
146
Chapitre 12. Tri
while, an d'viter
mergesort qui prend une
mthode mergesort ne peut
rcursivement puis on en fera une version itrative avec une boucle
tout dbordement de pile. crire enn une mthode rcursive
liste en argument et la trie en place. Expliquer pourquoi la
pas provoquer de dbordement de pile.
12.4 Tri par tas
Le tri par tas consiste utiliser une le de priorit, comme celles prsentes au chapitre 7. Une telle structure permet l'ajout d'un lment et le retrait du plus petit lment.
L'ide du tri par tas est alors la suivante : on construit une le de priorit contenant tous
les lments trier puis on retire les lments un par un. L'opration de retrait donnant
le plus petit lment chaque fois, les lments sont sortis dans l'ordre croissant.
A priori,
n'importe quelle structure de le de priorit convient. En particulier, une
structure o les oprations d'ajout et de retrait ont un cot
ment un tri optimal en
O(N log N ).
O(log N )
donne immdiate-
Cependant, on peut raliser le tri par tas
en place,
en construisant la structure de tas directement l'intrieur du tableau que l'on cherche
trier. L'organisation du tas dans le tableau est exactement la mme que dans la section 7.1 : les ls gauche et droit d'un nud stock l'indice
aux indices
2i + 1
et
2i + 2.
i sont respectivement stocks
La seule dirence est que l'on va construire un tas pour la
relation d'ordre inverse, c'est--dire un tas o le plus grand lment se trouve la racine.
Pour construire le tas, on considre les lments du tableau de la droite vers la gauche.
chaque tour, on a une situation de la forme
k k+1
0
?
tas en construction
a[k+1..n[ contient la partie basse du tas en construction, c'est--dire une fort
i tels que k < i < 2k + 3. On fait alors descendre la valeur
place dans le tas de racine k. Une fois tous les lments parcourus, on a un
o la partie
de tas enracins aux indices
a[k]
sa
unique tas enracin en 0.
La seconde tape consiste alors dconstruire le tas. Pour cela, on change sa racine
en
a[0]
avec l'lment
ensuite la structure de tas
taille
n-1
a[n-1]. La valeur r se trouve alors
sur a[0..n-1[, en faisant descendre v sa
en
sa place. On rtablit
place dans un tas de
enracin en 0. Puis on rpte l'opration pour les positions
chaque tour
k,
etc.
on a la situation suivante
0
tas
c'est--dire un tas dans la portion
de la partie
n-1, n-2,
a[k..n[,
n
tri
a[0..k[, dont tous les lments sont plus petits que ceux
qui est trie.
Les deux tapes de l'algorithme ci-dessus utilisent la mme opration consistant faire
descendre une valeur jusqu' sa place dans un tas. On la ralise l'aide d'une mthode
rcursive
limite
moveDown qui prend en arguments le tableau a, un indice k, une valeur v et une
sur les indices.
static void moveDown(int[] a, int k, int v, int n) {
12.4. Tri par tas
147
h1 enracin en 2k+1 ds lors que 2k+1 < n, et
2k+2 ds lors que 2k+2 < n. L'objectif est de construire
un tas enracin en k, contenant v et tous les lments de h1 et h2 . On commence par
dterminer si le tas enracin en k est rduit une feuille, c'est--dire si le tas h1 n'existe
pas. Si c'est le cas, on aecte la valeur v a[k] et on a termin.
On fait l'hypothse qu'on a dj un tas
de mme un tas
h2
enracin en
int r = 2 * k + 1;
if (r >= n)
a[k] = v;
Sinon, on dtermine dans
traitant avec soin le cas o
r l'indice de la plus
h2 n'existe pas.
grande des deux racines de
h1
et
h2 ,
en
else {
if (r + 1 < n && a[r] < a[r + 1]) r++;
Si la valeur
a[k].
v est suprieure ou gale a[r], la descente est termine et il sut d'aecter
if (a[r] <= v)
a[k] = v;
Sinon, on fait remonter la valeur
rcursif sur la position
r.
a[r]
et on poursuit la descente de
avec un appel
else {
a[k] = a[r];
moveDown(a, r, v, n);
}
Ceci achve la mthode
moveDown.
La mthode de tri proprement dite prend un tableau
en argument
static void heapsort(int[] a) {
int n = [Link];
Elle commence par construire le tas de bas en haut par des appels
moveDown. On vite
b n2 c 1 (en
les appels inutiles sur des tas rduits des feuilles en commenant la boucle
eet, pour tout indice
strictement suprieur, on a
2k+1 n).
for (int k = n / 2 - 1; k >= 0; k--)
moveDown(a, k, a[k], n);
Une fois le tas entirement construit, on en extrait les lments un par un dans l'ordre
dcroissant. Comme expliqu ci-dessus, pour chaque indice
valeur
en
a[k]
puis on fait descendre
k,
on change
a[0]
avec la
sa place.
for (int k = n - 1; k >= 1; k--) {
int v = a[k];
a[k] = a[0];
moveDown(a, 0, v, k);
}
On note que la spcication de
moveDown nous permet d'viter d'aecter v en a[0] avant
d'entamer le descente. Le code complet est donn programme 30 page 148.
148
Programme 30 Tri par tas
static void moveDown(int[] a, int k, int v, int n) {
int r = 2 * k + 1;
if (r >= n)
a[k] = v;
else {
if (r + 1 < n && a[r] < a[r + 1])
r++;
if (a[r] <= v)
a[k] = v;
else {
a[k] = a[r];
moveDown(a, r, v, n);
}
}
}
static void heapsort(int[] a) {
int n = [Link];
for (int k = n / 2 - 1; k >= 0; k--)
moveDown(a, k, a[k], n);
for (int k = n - 1; k >= 1; k--) {
int v = a[k];
a[k] = a[0];
moveDown(a, 0, v, k);
}
}
Chapitre 12. Tri
12.5. Code gnrique
Complexit.
149
D'autre part,
moveDown. Le nombre
k est double chaque appel.
Considrons tout d'abord le cot de la mthode
d'appels rcursifs est major par
moveDown
log n,
puisque la valeur de
eectue au plus deux comparaisons et une aectation chaque
appel. Dans le pire des cas, on obtient un total de
2 log n comparaisons et log n aectations.
Pour le tri lui-mme, on peut grossirement majorer le nombre de comparaisons effectues dans chaque appel
3N log N
moveDown
par
2 log N ,
N log N
soit un total
C(N )
au pire gal
pour la premire tape et 2N log N
3
pour la seconde). De mme, le nombre total d'aectations est au pire N log N .
2
En ralit, on peut tre plus prcis et montrer notamment que la premire tape de
comparaisons (qui se dcompose en
l'algorithme, savoir la construction du tas, n'a qu'un cot linaire (voir par exemple [2,
Sec. 7.3]). Ds lors, seule la seconde partie de l'algorithme contribue la complexit
C(N ) 2N log N . Pour une analyse
The Art of Computer Programming [6, p. 152].
asymptotique et donc
renvoie
en moyenne du tri par tas, on
Ce tri s'eectue facilement en mmoire constante. En eet, la mthode
moveDown peut
tre rcrite sous forme d'une boucle mme si on ne risque pas ici un dbordement de
pile, la hauteur tant logarithmique.
12.5 Code gnrique
Pour crire un algorithme de tri gnrique, il sut de se donner un paramtre de type
K pour les lments, et d'exiger que ce type soit muni d'une relation de comparaison, c'est-dire implmente l'interface [Link]<T>, comme nous l'avons dj fait
pour les AVL (section 6.3.4) et les les de priorit (section 7.4). Ainsi le tri par insertion
(programme 27 page 138) s'crit dans sa version gnrique de la manire suivante :
static <K extends Comparable<K>> void insertionSort(K[] a) {
for (int i = 1; i < [Link]; i++) {
K v = a[i];
int j = i;
for (; 0 < j && [Link](a[j - 1]) < 0; j--)
a[j] = a[j - 1];
a[j] = v;
}
}
12.6 Exercices supplmentaires
Exercice 12.7.
crire une mthode
void twoWaySort(boolean[] a) qui trie en place
false < true. La complexit doit tre pro
un tableau de boolens, avec la convention
portionnelle au nombre d'lments.
Exercice 12.8.
(Le drapeau hollandais de Dijkstra) crire une mthode qui trie en place
un tableau contenant des valeurs reprsentant les trois couleurs du drapeau hollandais,
savoir
enum Color { Blue, White, Red }
150
Chapitre 12. Tri
On pourra procder de deux faons : soit en comptant le nombre d'occurrences de chaque
couleur, soit en n'eectuant que des changes dans le tableau. Dans les deux cas, la
complexit doit tre proportionnelle au nombre d'lments.
Exercice 12.9.
k valeurs
0, . . . ,k 1. crire une
O(max(k,N )) o N est la taille du
Plus gnralement, on considre le cas d'un tableau contenant
distinctes. Pour simplier, on suppose qu'il s'agit des entiers
mthode qui trie un tel tableau en place en temps
tableau.
13
Compression de donnes
La compression de donnes consiste tenter de rduire l'espace occup par une information. On l'utilise quotidiennement, par exemple en tlchargeant des chiers ou encore
sans le savoir en utilisant des logiciels qui compressent des donnes pour conomiser les
ressources. L'exemple typique est celui des formats d'image et de vido qui sont le plus
souvent compresss. Ce chapitre illustre la compression de donnes avec un algorithme
simple, savoir l'algorithme de Human [ ]. Il va notamment nous permettre de mettre
en pratique les arbres de prxes (section 6.4) et les les de priorit (chapitre 7).
Exercice 13.1.
Expliquer pourquoi on ne peut pas crire un programme de compression
de chiers qui parvienne systmatiquement diminuer strictement la taille du chier qu'il
compresse.
13.1 L'algorithme de Human
On suppose que le texte compresser est une suite de caractres et que le rsultat de
la compression est une suite de bits. L'algorithme de Human repose sur l'ide suivante :
si certains caractres du texte compresser apparaissent souvent, il est prfrable de
les reprsenter par un code court. Par exemple, dans le texte
tres
'i'
et
's'
de reprsenter le caractre
caractres
100
'm'
les carac-
'p'
'i' par la squence 0, le caractre 's' par la squence 11 et les
par des squences plus longues encore, par exemple respectivement
100011110111101011010.
Les squences pour les caractres 'i', 's', 'm' et 'p' n'ont pas t choisies au hasard.
et
101.
et
"mississippi",
apparaissent souvent, savoir quatre fois chacun. On peut ainsi choisir
Le texte compress sera alors
Elles ont en eet la proprit qu'aucune n'est un prxe d'une autre, permettant ainsi
un dcodage sans ambigut. On appelle cela un
code prxe .
Il se trouve qu'il est trs
facile de construire un tel code si les caractres considrs forment les feuilles d'un arbre
binaire. Prenons par exemple l'arbre suivant :
i
s
m
152
Chapitre 13. Compression de donnes
0
1 une descente vers la droite. Par construction,
Il sut alors d'associer chaque caractre le chemin qui l'atteint depuis la racine, un
dnotant une descente vers la gauche et un
un tel code est un code prxe. On a dj crois une telle reprsentation avec les arbres
prxes dans la section 6.4, mme si le problme n'tait pas pos en ces termes.
L'algorithme de Human permet de construire, tant donn un nombre d'occurrences
pour chacun des caractres, un arbre ayant la proprit d'tre le meilleur possible pour
cette distribution (dans un sens qui sera expliqu plus loin). La frquence des caractres
peut tre calcule avec une premire passe ou donne l'avance s'il s'agit par exemple
d'un texte crit dans un langage pour lequel on connat la distribution statistique des
caractres. Si on reprend l'exemple de la chane
"mississippi", les nombres d'occurrences
des caractres sont les suivantes :
m(1)
p(2)
s(4)
i(4)
L'algorithme de Human procde alors ainsi. Il slectionne les deux caractres avec les
nombres d'occurrences les plus faibles, savoir ici les caractres
'm'
et
'p',
et les runit
en un arbre auquel il donne un nombre d'occurrences gal la somme des nombres d'occurrences des deux caractres. On a donc la situation suivante :
(3)
m(1)
s(4)
i(4)
p(2)
Puis on recommence avec ces trois arbres , c'est--dire qu'on en slectionne deux ayant
les occurrences les plus faibles, ici 3 et 4, et on les runit en un nouvel arbre, ce qui donne
par exemple ceci :
i(4)
(7)
(3)
m(1)
s(4)
p(2)
Une dernire tape de ce procd nous donne au nal l'arbre suivant :
(11)
i(4)
(7)
(3)
m(1)
s(4)
p(2)
C'est l'arbre que nous avions propos initialement. Il se trouve qu'il est optimal, pour un
sens que nous donnons maintenant.
Optimalit.
Supposons que chaque caractre
ci
apparaisse avec la frquence
fi .
La
proprit de l'arbre construit par l'algorithme de Human est qu'il minimise la quantit
S=
fi di
i
o
di
est la profondeur du caractre
caractre
ci .
ci
dans l'arbre, c'est--dire la longueur du code du
Montrons-le par l'absurde, en supposant qu'il existe un arbre pour lequel la
13.2. Ralisation
somme
153
est strictement plus petite que celle obtenue avec l'algorithme de Human. On
T qui minimise le nombre n de caractres. Sans
c0 et c1 sont les deux caractres choisis initialement
choisit un tel arbre
perte de gnralit,
supposons que
par l'algorithme de
Human, c'est--dire deux caractres avec les frquences les plus basses. On peut supposer
que ces deux caractres sont des feuilles de
T,
car on n'augmente pas la somme
en les
changeant avec des feuilles. De mme, on peut supposer que ce sont deux feuilles d'un
mme nud, car on peut toujours les changer avec d'autres feuilles. Si on remplace alors
ce nud par une feuille de frquence
f0 +f1 , la somme S
diminue de
f0 +f1 . En particulier,
cette diminution ne dpend pas de la profondeur du nud. Du coup, on vient de trouver
un arbre meilleur que celui donn par l'algorithme de Human pour
n1
caractres, ce
qui est une contradiction.
13.2 Ralisation
On commence par introduire des classes pour reprsenter les arbres de prxes utiliss dans l'algorithme de Human. Qu'il s'agisse d'une feuille dsignant un caractre ou
d'un nud interne, tout arbre contient un nombre d'occurrences qui lui permettra d'tre
compar un autre arbre. On introduit donc une classe abstraite
HuffmanTree
pour
reprsenter un arbre, quelle que soit sa nature.
abstract class HuffmanTree implements Comparable<HuffmanTree> {
int freq;
public int compareTo(HuffmanTree that) {
return [Link] - [Link];
}
}
Le nombre d'occurrences est stock dans le champ
freq.
Cette classe implmente l'inter-
Comparable et sa mthode compareTo compare les valeurs stockes dans le champ
freq. Une feuille est reprsente par une sous-classe Leaf dont le champ c contient le
face
caractre qu'elle dsigne.
class Leaf extends HuffmanTree {
final char c;
Leaf(char c) {
this.c = c;
[Link] = 0;
}
}
Le nombre d'occurrences, hrit de la classe
HuffmanTree,
est x initialement zro.
Enn, un nud interne est reprsente par une seconde sous-classe
champs
left
et
right
contiennent les deux sous-arbres.
class Node extends HuffmanTree {
HuffmanTree left, right;
Node(HuffmanTree left, HuffmanTree right) {
[Link] = left;
Node
dont les deux
154
Chapitre 13. Compression de donnes
[Link] = right;
[Link] = [Link] + [Link];
Le constructeur calcule le nombre d'occurrences, l encore hrit de la classe
HuffmanTree,
comme la somme des nombres d'occurrences des deux sous-arbres.
crivons maintenant le code de l'algorithme de Human dans une classe
Cette classe contient l'arbre de prxes dans un champ
caractre dans un second champ
code,
tree
Huffman.
et le code associ chaque
sous la forme d'une table.
class Huffman {
private HuffmanTree tree;
private Map<Character, String> codes;
On va se contenter ici de construire des messages encods sous la forme de chanes de
caractres
'0'
et
'1' ;
en pratique il s'agirait de bits. C'est pourquoi la table
codes
associe de simples chanes aux caractres de l'alphabet.
On suppose que les frquences d'apparition des dirents caractres sont donnes initialement, sous la forme d'une collection de feuilles, c'est--dire d'une valeur
type
alphabet de
Collection<Leaf>. L'exercice 13.2 propose le calcul de ces frquences. Le construc-
teur prend alors la forme suivante :
Huffman(Collection<Leaf> alphabet) {
if ([Link]() <= 1) throw new IllegalArgumentException();
[Link] = buildTree(alphabet);
[Link] = new HashMap<Character, String>();
[Link]("", [Link]);
}
buildTree construit l'arbre de prxes partir de l'alphabet donn et la
traverse le parcourt pour remplir la table codes.
Commenons par le code de la mthode buildTree. Pour suivre l'algorithme prsent
La mthode
mthode
dans la section prcdente, qui slectionne chaque fois les deux arbres les plus petits, on
utilise une le de priorit. Ce peut tre la classe
la classe
[Link]
Heap
prsente au chapitre 7 ou encore
de la bibliothque Java.
HuffmanTree buildTree(Collection<Leaf> alphabet) {
Heap<HuffmanTree> pq = new Heap<HuffmanTree>();
Cette le de priorit contient des arbres, c'est--dire des valeurs de type
HuffmanTree.
On commence par la remplir avec toutes les feuilles contenues dans l'alphabet pass en
argument.
for (Leaf l: alphabet)
[Link](l);
Puis on applique l'algorithme de construction de l'arbre proprement dit. Tant que la le
contient au moins deux lments, on en retire les deux plus petits, que l'on fusionne en
un seul arbre qui est remis dans la le de priorit.
13.2. Ralisation
155
while ([Link]() > 1) {
HuffmanTree left = [Link]();
HuffmanTree right = [Link]();
[Link](new Node(left, right));
}
Lorsqu'on sort de la boucle, la le ne contient plus qu'un seul arbre, qui est le rsultat
renvoy.
return [Link]();
Une fois l'arbre construit, on peut remplir la table
codes. Il sut pour cela de parcourir
l'arbre, en maintenant le chemin depuis la racine, et de remplir la table chaque fois qu'on
atteint une feuille. crivons pour cela une mthode
Elle prend en arguments le chemin
une table
prefix,
traverse dans la classe HuffmanTree.
sous la forme d'une chane de caractres, et
remplir.
abstract class HuffmanTree ... {
...
abstract void traverse(String prefix, Map<Character, String> m);
}
On dnit ensuite cette mthode dans les deux sous-classes. Dans la classe
de remplir la table
en associant la chane
prefix
Leaf,
il sut
au caractre reprsent par la feuille.
class Leaf extends HuffmanTree {
...
void traverse(String prefix, Map<Character, String> m) {
[Link](this.c, prefix);
}
}
Dans la classe
Node, il s'agit de descendre rcursivement dans les deux sous-arbres gauche
et droit, en mettant jour le chemin chaque fois.
class Node extends HuffmanTree {
...
void traverse(String prefix, Map<Character, String> m) {
[Link](prefix + '0', m);
[Link](prefix + '1', m);
}
}
Ceci achve le code de la construction de l'arbre et du remplissage de la table.
Encodage et dcodage.
Il reste expliquer comment crire les deux fonctions qui
encode et dcode respectivement un texte donn. Commenons par la fonction d'encodage.
On utilise un
StringBuilder
pour construire le rsultat (voir page 44).
156
Chapitre 13. Compression de donnes
String encode(String msg) {
StringBuilder sb = new StringBuilder();
Pour chaque caractre de la chane encoder, on concatne son code au rsultat.
for (int i = 0; i < [Link](); i++)
[Link]([Link]([Link](i)));
Il n'y a plus qu' renvoyer la chane contenue dans
sb.
return [Link]();
Le dcodage est un peu plus subtil. La mthode
0 et de 1,
StringBuilder pour
decode reoit en arguments une chane
de caractres forme de
suppose avoir t encode avec cet objet. L encore,
on utilise un
construire le rsultat.
String decode(String msg) {
StringBuilder sb = new StringBuilder();
On va avancer progressivement dans le message encod, en dcodant les caractres un par
un. La variable
indique le caractre courant du message cod et on procde tant qu'on
n'a pas atteint la n du message.
int i = 0;
while (i < [Link]()) {
Pour dcoder un caractre, il faut descendre dans l'arbre
dsign par les
et les
du message, jusqu' atteindre une feuille. Une solution simple
consiste crire une mthode
arguments le message
[Link] en suivant le chemin
msg
find
dans la classe
et la position
i,
HuffmanTree
pour cela, qui prend en
et renvoie le caractre obtenu. On l'ajoute
alors la chane dcode.
char c = [Link](msg, i);
[Link](c);
i, il sut de l'augmenter de la longueur du code du caractre
qui vient juste d'tre dcod. Celle-ci est facilement obtenue grce la table [Link].
Pour mettre jour la variable
i += [Link](c).length();
Une autre solution aurait t de faire renvoyer cette longueur par la mthode
find mais il
n'est pas ais de renvoyer deux rsultats. Une fois sorti de la boucle, on renvoie la chane
construite.
return [Link]();
Il reste crire le code de la mthode
traverse
HuffmanTree
la mthode
find
qui descend dans l'arbre. Comme pour
plus haut, on commence par la dclarer dans la classe abstraite
13.2. Ralisation
157
abstract class HuffmanTree ... {
...
abstract char find(String s, int i);
}
puis on la dnit dans chacune des deux sous-classes. Dans la classe
Leaf,
il sut de
renvoyer le caractre contenu dans la feuille.
class Leaf extends HuffmanTree {
...
char find(String s, int i) {
return this.c;
}
}
Dans la classe
i-ime
Node,
il s'agit de descendre vers la gauche ou vers la droite, selon que le
caractre de la chane
vaut
'0'
ou
'1'.
class Node extends HuffmanTree {
...
char find(String s, int i) {
if (i == [Link]())
throw new Error("corrupted code; bad alphabet?");
return ([Link](i) == '0' ? [Link] : [Link]).find(s, i+1);
}
}
On prend soin de tester un ventuel dbordement au del de la n de la chane. Cela peut
se produire si on tente de dcoder un message qui n'a pas t encod avec cet arbre-l. Le
code complet est donn dans les programmes 31 et 32. Il est important de noter qu'une
implmentation raliste devrait, outre le fait d'utiliser des bits plutt que des caractres,
encoder galement l'arbre comme une partie du message ou, dfaut, la distribution des
caractres. Sans cette information, il n'est pas possible de dcoder.
Exercice 13.2.
crire une mthode statique
Collection<Leaf> buildAlphabet(String s)
qui calcule les nombres d'occurrences des dirents caractres d'une chane
s et les renvoie
sous la forme d'une collection de feuilles. Indication : on pourra utiliser une table de
hachage associant des feuilles des caractres. Une fois cette table remplie, sa mthode
values()
permet de renvoyer la collection de feuilles directement.
158
Chapitre 13. Compression de donnes
Programme 31 Algorithme de Human (structure d'arbre)
abstract class HuffmanTree implements Comparable<HuffmanTree> {
int freq;
public int compareTo(HuffmanTree that) {
return [Link] - [Link];
}
abstract void traverse(String prefix, Map<Character, String> m);
abstract char find(String s, int i);
}
class Leaf extends HuffmanTree {
final char c;
Leaf(char c) {
this.c = c;
[Link] = 0;
}
void traverse(String prefix, Map<Character, String> m) {
[Link](this.c, prefix);
}
char find(String s, int i) {
return this.c;
}
}
class Node extends HuffmanTree {
HuffmanTree left, right;
Node(HuffmanTree left, HuffmanTree right) {
[Link] = left;
[Link] = right;
[Link] = [Link] + [Link];
}
void traverse(String prefix, Map<Character, String> m) {
[Link](prefix + '0', m);
[Link](prefix + '1', m);
}
char find(String s, int i) {
if (i == [Link]())
throw new Error("corrupted code; bad alphabet?");
return ([Link](i) == '0' ? [Link] : [Link]).find(s, i+1);
}
}
13.2. Ralisation
Programme 32 Algorithme de Human (codage et dcodage)
class Huffman {
private HuffmanTree tree;
private Map<Character, String> codes;
Huffman(Collection<Leaf> alphabet) {
if ([Link]() <= 1) throw new IllegalArgumentException();
[Link] = buildTree(alphabet);
[Link] = new HashMap<Character, String>();
[Link]("", [Link]);
}
HuffmanTree buildTree(Collection<Leaf> alphabet) {
Heap<HuffmanTree> pq = new Heap<HuffmanTree>();
for (Leaf l: alphabet)
[Link](l);
while ([Link]() > 1) {
HuffmanTree left = [Link]();
HuffmanTree right = [Link]();
[Link](new Node(left, right));
}
return [Link]();
}
String encode(String msg) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < [Link](); i++)
[Link]([Link]([Link](i)));
return [Link]();
}
String decode(String msg) {
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < [Link]()) {
char c = [Link](msg, i);
[Link](c);
i += [Link](c).length();
}
return [Link]();
}
159
160
Chapitre 13. Compression de donnes
Quatrime partie
Graphes
14
Dnition et reprsentation
La structure de graphes est une structure de donnes fondamentale en informatique.
Un graphe est la donne d'un ensemble de
sommets
relis entre eux par des
artes .
On a
l'habitude de visualiser un graphe de la faon suivante :
V de sommets et d'un
paires de sommets. Si {x,y} E , on dit que les sommets
note x y . Cette relation d'adjacence tant symtrique, on
Plus formellement, un tel graphe est la donne d'un ensemble
ensemble
et
d'artes, qui sont des
sont adjacents et on
graphe non orient.
parle de
On peut galement dnir la notion de
semble de
d'artes .
graphe orient
en choisissant pour
un en-
couples de sommets plutt que de paires. On parle alors d'arcs plutt que
Si (x,y) E on dit que y est un successeur de x et on note x y . Voici un
exemple de graphe orient :
Un arc d'un sommet vers lui-mme, comme sur cet exemple, est appel une
boucle .
Le
degr entrant (resp. sortant) d'un sommet est le nombre d'arcs qui pointent vers ce sommet
(resp. qui sortent de ce sommet).
Les sommets comme les arcs peuvent porter une information ; on parle alors de
tiquet .
graphe
Voici un exemple de graphe orient tiquet :
1. Dans la suite, on utilisera systmatiquement le terme d'arc, y compris pour des graphes non orients.
164
Chapitre 14. Dnition et reprsentation
Il est important de noter que l'tiquette d'un sommet n'est pas la mme chose que le sommet lui-mme. En particulier, deux sommets peuvent porter la mme tiquette. Formellement, un graphe tiquet est donc la donne supplmentaire de deux fonctions donnant
respectivement l'tiquette d'un sommet de
et l'tiquette d'un arc de
E.
chemin du sommet u au sommet v est une squence x0 , . . . ,xn de sommets tels que
x0 = u, xn = v et xi xi+1 pour 0 i < n. Un tel chemin est de longueur n (il contient
n arcs).
Un
14.1 Matrice d'adjacence
On considre dans cette section le cas o les sommets sont reprsents par des entiers,
et plus prcisment par les entiers conscutifs
0, . . . ,N 1.
Dit autrement, on a
V =
{0, . . . ,N 1}. Le plus naturel pour reprsenter un tel graphe est sans doute une matrice
M , de taille N N o chaque lment Mi,j indique la prsence d'un arc entre les sommets
i et j . En supposant des graphes non tiquets, il sut d'utiliser une matrice de boolens :
class AdjMatrix {
int n; // les sommets sont 0,...,n-1
boolean[][] m;
}
(On conserve ici le nombre de sommets dans un champ
n,
mme si celui-ci est gal la
dimension de la matrice.) Le constructeur prend la valeur de
dimension
n n.
et alloue une matrice de
AdjMatrix(int n) {
this.n = n;
this.m = new boolean[n][n];
}
Cette matrice est initialise avec
false,
ce qui reprsente donc un graphe ne contenant
aucun arc. Les oprations d'ajout, de suppression ou de test de prsence d'un arc sont
immdiates. Pour des graphes orients, elles sont donnes programme 33 page 165. Elles
ont toutes une complexit
O(1)
c'est--dire un cot constant.
Une matrice d'adjacence occupe clairement un espace quadratique, c'est--dire en
O(N 2 ). C'est une reprsentation adapte aux graphes denses , c'est--dire aux graphes o
2
le nombre d'arcs E est justement de l'ordre de N .
Exercice 14.1.
Modier les matrices d'adjacence pour des graphes o les arcs sont
tiquets (par exemple par des entiers).
14.1. Matrice d'adjacence
165
Programme 33 Graphes orients reprsents par une matrice d'adjacence
class AdjMatrix {
int n; // les sommets sont 0,...,n-1
boolean[][] m;
AdjMatrix(int n) {
this.n = n;
this.m = new boolean[n][n];
}
boolean hasEdge(int x, int y) {
return this.m[x][y];
}
void addEdge(int x, int y) {
this.m[x][y] = true;
}
void removeEdge(int x, int y) {
this.m[x][y] = false;
}
166
Chapitre 14. Dnition et reprsentation
Exercice 14.2.
Le plus simple pour reprsenter des graphes non orients est de conser-
ver la mme structure que pour des graphes orients, mais en maintenant l'invariant que
pour chaque arc
removeEdge
ab
on a galement l'arc
b a.
Modier les oprations
addEdge
et
des matrices d'adjacence en consquence.
14.2 Listes d'adjacence
Dans le cas de graphes peu denses, une alternative aux matrices d'adjacence consiste
utiliser un tableau donnant, pour chaque sommet, la liste de ses successeurs. On parle
de
listes d'adjacence. Ainsi pour le graphe dessin page 163, ces listes sont [y] pour le
x, [y,t] pour le sommet y , etc. Il nous sut donc d'utiliser un tableau de listes.
sommet
Cependant, tester la prsence d'un lment dans une liste cote un peu cher, et donc
tester la prsence d'un arc dans le graphe le sera galement. Il semble plus opportun
d'utiliser par exemple une table de hachage pour reprsenter les successeurs d'un sommet
donn, par exemple avec la bibliothque
HashSet
de Java. Puisqu'on en est arriv
l'ide d'utiliser une table de hachage, o les sommets n'ont plus besoin d'tre les entiers
0, . . . ,N 1, autant pousser cette ide jusqu'au bout et reprsenter un graphe par une table
de hachage associant chaque sommet l'ensemble de ses successeurs, lui-mme reprsent
par une table de hachage. On s'aranchit ainsi compltement du fait que les sommets
sont les entiers
0, . . . ,N 1 et on peut mme s'aranchir facilement du fait que ce sont
des entiers ; voir l'exercice 14.3. On conserve cependant l'appellation de listes d'adjacence,
mme s'il n'y a pas de liste dans cette reprsentation.
Pour des graphes orients, le code est donn programme 34 page 167. On s'y attardera
un instant pour bien comprendre les petites dirences avec les matrices d'adjacence. En
particulier, il convient de traiter correctement les cas de gure o la table d'adjacence d'un
addEdge(x, y) suppose que x a dj t ajout
addVertex. On pourrait bien sr crire un code un peu plus dfensif
ou ajoutant automatiquement le sommet x dans le graphe si ncessaire. Concernant le
cot des oprations de test, d'ajout et de suppression d'un arc, elles restent en O(1)
sommet donn n'existe pas. Ici l'appel
comme sommet avec
(amorti) car il ne s'agit que d'oprations d'ajout et de suppression dans des tables de
hachage. Concernant le cot en espace, les listes d'adjacence ont une complexit optimale
de
O(N + E).
Exercice 14.3.
classe
crire une version gnrique de la classe
AdjList,
reprsentant les sommets.
Exercice 14.4.
Ajouter une mthode
paramtre par une
int nbEdges()
donnant le nombre d'arcs en
temps constant. Indication : maintenir le nombre d'arcs dans la structure de graphe,
en mettant jour sa valeur dans
Exercice 14.5.
addEdge
et
removeEdge.
Modier les listes d'adjacence pour des graphes o les arcs sont tiquets
(par exemple par des entiers). On pourra remplacer l'ensemble des successeurs du sommet
x par un
x y.
dictionnaire (HashMap) donnant pour chaque successeur
l'tiquette de l'arc
14.2. Listes d'adjacence
167
Programme 34 Graphes orients reprsents par des listes d'adjacence
class AdjList {
Map<Integer, Set<Integer>> adj;
AdjList() {
[Link] = new HashMap<Integer, Set<Integer>>();
}
void addVertex(int x) {
Set<Integer> s = [Link](x);
if (s == null) [Link](x, new HashSet<Integer>());
}
boolean hasEdge(int x, int y) {
Set<Integer> s = [Link](x);
return s != null && [Link](y);
}
void addEdge(int x, int y) {
[Link](x).add(y);
}
void removeEdge(int x, int y) {
Set<Integer> s = [Link](x);
if (s != null) [Link](y);
}
}
168
Chapitre 14. Dnition et reprsentation
Exercice 14.6.
Le plus simple pour reprsenter des graphes non orients est de conser-
ver la mme structure que pour des graphes orients, mais en maintenant l'invariant que
pour chaque arc
removeEdge
ab
on a galement l'arc
b a.
Modier les oprations
addEdge
et
des listes d'adjacence en consquence.
14.3 Code gnrique
La construction d'une structure de graphe gnrique, paramtre par un type de sommets
V,
est immdiate. Si on prend l'exemple des listes d'adjacence, une classe gnrique
de graphes est dclare ainsi :
class Graph<V> {
private Map<V, Set<V>> adj;
Si la ralisation utilise les classes
suppose donc que la classe
Exercice 14.7.
HashMap
et
HashSet
de la bibliothque standard, on
rednit correctement les mthodes
hashCode
crire la version gnrique du programme 34 page 167.
et
equals.
Pour les algorithmes sur les graphes que nous allons crire dans le chapitre suivant,
il est ncessaire de pouvoir accder l'ensemble des sommets du graphe d'une part et
l'ensemble des successeurs d'un sommet donn d'autre part. Plutt que d'exposer la table
de hachage qui contient la relation d'adjacence (ci-dessus on l'a d'ailleurs dclare comme
prive), il sut d'exporter les deux mthodes suivantes :
Set<V> vertices() {
return [Link]();
}
Set<V> successors(V v) {
return [Link](v);
}
Ds lors, pour parcourir tous les sommets d'un graphe
g,
il sut d'crire
for (V v: [Link]()) ...
et pour parcourir tous les successeurs d'un sommet
de
g,
il sut d'crire
for (V w: [Link](v)) ...
Le cot de ces parcours est respectivement
du sommet
v.
O(V )
et
O(d(v))
d(v)
est le degr sortant
15
Algorithmes lmentaires sur les
graphes
Graph<V>
On note V le
Dans ce chapitre, on utilise exclusivement la structure de graphes gnrique
introduite dans la section 14.3, o la classe
nombre de sommets et
dsigne le type des sommets.
le nombre d'arcs. La reprsentation du graphe tant par listes
d'adjacence, l'occupation mmoire du graphe est
O(V + E).
15.1 Parcours de graphes
Dans cette section, on prsente deux algorithmes qui permettent de parcourir tous les
sommets d'un graphe (qu'il soit orient ou non). Ces parcours, ou des variantes de ces
parcours, se retrouvent dans de trs nombreux algorithmes sur les graphes. Il convient
donc de les comprendre et de les matriser.
Avant mme de rentrer dans les dtails d'un parcours particulier, on peut dj comprendre que la prsence possible de
cycle
dans un graphe rend le parcours plus complexe
que celui d'une liste ou d'un arbre. Prenons l'exemple du graphe non orient suivant
3
(15.1)
Quel que soit son fonctionnement, un parcours qui dmarrerait du sommet 4 parviendra
un moment o un autre au sommet 5 et il ne doit pas entrer alors dans un boucle
innie du fait de la prsence du cycle
5 2 6.
Dans le cas d'un arbre, un tel cycle
n'est pas possible et nous avons pu crire le parcours inxe d'un arbre assez facilement
(voir page 79). L'arbre vide
null
tait la condition d'arrt du parcours rcursif. Dans le
cas d'une liste chane, nous avons voqu la possibilit de listes cycliques (section 4.4.1).
Nous avons certes donn un algorithme pour dtecter un tel cycle (l'algorithme du livre
et de la tortue, page 59), mais il exploite de faon cruciale le fait que chaque lment de
la liste ne possde qu'au plus un successeur. Dans le cas d'un graphe, ce n'est plus vrai.
1. Les exemples de ce chapitre sont inspirs de Introduction to Algorithms [2].
170
Chapitre 15. Algorithmes lmentaires sur les graphes
Nous allons donc devoir
marquer
les sommets atteints par le parcours, d'une faon
ou d'une autre. Nous utiliserons ici une table de hachage. Une autre solution consiste
modier directement les sommets, si le type
le permet.
15.1.1 Parcours en largeur
Le premier parcours que nous prsentons, dit parcours en largeur (en anglais
rst search
breadth-
ou BFS), consiste explorer le graphe en cercles concentriques en partant
d'un sommet particulier
une distance d'un arc de
s appel la source.
s, puis les sommets
On parcourt d'abord les sommets situs
situs une distance de deux arcs, etc. Sur
le graphe donn en exemple plus haut (15.1), et en partant du sommet 1, on explore en
premier lieu les sommets 1 et
5, directement relis au sommet 1, puis les sommets 2, 4
et 6, puis le sommet 3, puis enn le sommet 7. On le redessine ici avec les distances la
source (le sommet 1) indiques en exposant.
01
10
22
33
42
51
62
74
Pour mettre en uvre ce parcours, on va utiliser une table de hachage. Elle contiendra les
sommets dj atteints par le parcours, en leur associant de plus la distance la source.
On renverra cette table comme rsultat du parcours. On crit donc une mthode statique
avec le type suivant :
static <V> HashMap<V, Integer> bfs(Graph<V> g, V source) {
La table, appele ici
visited,
est cre au tout dbut de la mthode et on y met initia-
lement la source, avec la distance 0.
HashMap<V, Integer> visited = new HashMap<V, Integer>();
[Link](source, 0);
Le parcours proprement dit repose sur l'utilisation d'une
le,
dans laquelle les sommets
vont tre insrs au fur et mesure de leur dcouverte. L'ide est que la le contient,
chaque instant, des sommets situs distance
distance
d+1
de la source, suivis de sommets
sommets distance
sommets distance
d+1
C'est l la matrialisation de notre ide de cercles concentriques , plus prcisment des
deux cercles concentriques conscutifs en cours de considration. Cette proprit est cru-
ciale pour la correction du parcours en largeur . Ici on utilise la bibliothque
LinkedList
pour raliser la le. Initialement, elle contient uniquement la source.
Queue<V> q = new LinkedList<V>();
[Link](source);
2. Pour une preuve dtaille de la correction du parcours en largeur, on pourra consulter [2, chap. 23].
15.1. Parcours de graphes
171
Un autre invariant important est que tout sommet prsent dans la le est galement
prsent dans la table
visited.
On procde alors une boucle, tant que la le n'est pas
vide. Le cas chant, on extrait le premier lment
la source dans
visited.
v de la le et on rcupre sa distance d
while (![Link]()) {
V v = [Link]();
int d = [Link](v);
On examine alors chaque successeur
de
v.
for (V w : [Link](v))
visited,
distance d+1
S'il n'avait pas encore t dcouvert, c'est--dire s'il n'tait pas dans la table
alors on l'ajoute dans la le d'une part, et dans la table
visited
avec la
d'autre part.
if () {
[Link](w);
[Link](w, d+1);
}
Ceci achve la boucle sur les successeurs de
v.
On passe alors l'lment suivant de la
le, et ainsi de suite. Une fois sorti de la boucle principale, le parcours est achev et on
renvoie la table
visited.
}
return visited;
Le code complet est donn programme 35 page 172. Il s'applique aussi bien un graphe non
orient qu' un graphe orient.
La complexit est facile dterminer. Chaque sommet
est mis dans la le au plus une fois et donc examin au plus une fois. Chaque arc est
donc considr au plus une fois, lorsque son origine est examine. La complexit est donc
O(V + E),
ce qui est optimal. La complexit en espace est
O(V )
car la le, comme la
table de hachage, peut contenir (presque) tous les sommets dans le pire des cas.
On note qu'il peut rester des sommets non atteints par le parcours en largeur. Ce sont
les sommets
pour lesquels il n'existe pas de chemin entre la source et
v.
Sur le graphe
suivant, en partant de la source 1, seuls les sommets 1, 0, 3 et 4 seront atteints.
01
10
32
41
Dit autrement, le parcours en largeur dtermine l'ensemble des sommets accessibles depuis
la source, et donne mme pour chacun la distance minimale en nombre d'arcs depuis la
source.
Comme on l'a fait remarquer plus haut, la le a une structure bien particulire, avec
des sommets distance
d,
suivis de sommets distance
d + 1.
On comprend donc que la
172
Chapitre 15. Algorithmes lmentaires sur les graphes
Programme 35 Parcours en largeur (BFS)
class BFS {
static <V> HashMap<V, Integer> bfs(Graph<V> g, V source) {
HashMap<V, Integer> visited = new HashMap<V, Integer>();
[Link](source, 0);
Queue<V> q = new LinkedList<V>();
[Link](source);
while (![Link]()) {
V v = [Link]();
int d = [Link](v);
for (V w : [Link](v))
if () {
[Link](w);
[Link](w, d+1);
}
}
return visited;
}
}
structure de le n'est pas vraiment ncessaire. Deux sacs susent, l'un contenant les
d et l'autre les sommets distance d + 1. On peut les matrialiser par
listes. Lorsque le sac d vient s'puiser, on le remplit avec le contenu du
sommets distance
exemple par des
sac
d + 1,
qui est lui-mme vid. (On les change, c'est plus simple.) Cela ne change en
rien la complexit.
Exercice 15.1.
Modier la mthode
bfs
pour conserver le chemin entre la source et
chaque sommet atteint par le parcours. Une faon simple de procder consiste stocker,
pour chaque sommet atteint, le sommet qui a permis de l'atteindre, par exemple dans
une table de hachage. Le chemin est donc dcrit l'envers , du sommet atteint vers la
source.
Exercice 15.2.
En s'inspirant du parcours en largeur d'un graphe, crire une mthode
qui parcourt les nuds d'un
arbre
en largeur.
15.1.2 Parcours en profondeur
Le second parcours de graphe que nous prsentons, dit parcours en profondeur (en
anglais
depth-rst search
ou DFS) applique l'algorithme de rebroussement (backtracking )
vu au chapitre 11 : tant qu'on peut progresser en suivant un arc, on le fait, et sinon on
fait machine arrire. Comme pour le parcours en largeur, on marque les sommets atteints
15.1. Parcours de graphes
173
au fur et mesure de leur dcouverte, pour viter de tomber dans un cycle. Prenons
l'exemple du graphe suivant
et d'un parcours en profondeur qui dmarre du sommet 2. Deux arcs sortent de ce sommet,
24
et
25
et on choisit (arbitrairement) de considrer en premier l'arc
2 5.
On
passe donc au sommet 5. Aucun arc ne sort de 5 ; c'est une impasse. On revient alors
au sommet 2, dont on considre maintenant le second arc sortant,
peut que suivre l'arc
43
sommet 1 sortent deux arcs,
1 4.
2 4.
De 4, on ne
puis, de mme, de 3 on ne peut que suivre l'arc
10
et
1 4.
3 1.
Du
On choisit de suivre en premier lieu l'arc
Il mne un sommet dj visit, et on fait donc machine arrire. De retour sur 1,
on considre l'autre arc,
1 0,
qui nous mne 0. De l le seul arc sortant mne 3,
l encore dj visit. On revient donc 0, puis 1, puis 3, puis 4, puis enn 2. Le
parcours est termin. Si on redessine le graphe avec l'ordre de dcouverte des sommets en
exposant, on obtient ceci :
05
14
20
33
42
51
Comme pour le parcours en largeur, on va utiliser une table de hachage contenant
les sommets dj atteints par le parcours. Elle donnera l'ordre de dcouverte de chaque
sommet. Sans surprise, on choisit d'crire le parcours en profondeur comme une mthode
rcursive. Pour viter de lui passer la table et le graphe en arguments systmatiquement,
on va crire le parcours en profondeur dans une mthode dynamique, dans une classe dont
le constructeur reoit le graphe en argument :
class DFS<V> {
private final Graph<V> g;
private final HashMap<V, Integer> visited;
private int count;
DFS(Graph<V> g) {
this.g = g;
[Link] = new HashMap<V, Integer>();
[Link] = 0;
}
Le champ
count
est le compteur qui nous servira associer, dans la table
visited,
chaque sommet avec l'instant de sa dcouverte. Le parcours en profondeur proprement
dit est alors crit dans une mthode rcursive
Son code est d'une simplicit enfantine :
dfs
prenant un sommet
en argument.
174
Chapitre 15. Algorithmes lmentaires sur les graphes
void dfs(V v) {
if ([Link](v)) return;
[Link](v, [Link]++);
for (V w : [Link](v))
dfs(w);
}
v a dj t atteint, on ne fait rien. Sinon, on le marque comme dj atteint,
donnant le numro count. Puis on considre chaque successeur w, sur lequel on
Si le sommet
en lui
lance rcursivement un parcours en profondeur. On ne peut imaginer plus simple. Un
dtail, cependant, est crucial : on a ajout
dans la table
visited avant
de considrer
ses successeurs. C'est l ce qui nous empche de tourner indniment dans un cycle.
La complexit est
O(V + E),
par le mme argument que pour le parcours en largeur.
Le parcours en profondeur est donc galement optimal. La complexit en espace est lgrement plus subtile, car il faut comprendre que c'est ici la pile des appels rcursifs qui
contient les sommets en cours de visite (et joue le rle de la le dans le parcours en largeur). Dans le pire des cas, tous les sommets peuvent tre prsents sur la pile, d'o une
complexit en espace
O(V ).
Comme le parcours en largeur, le parcours en profondeur a dtermin l'ensemble des
sommets accessibles depuis la source
v.
Voici un autre exemple o le parcours en profon-
deur est lanc partir du sommet 1.
03
10
32
41
Les sommets 2 et 5 ne sont pas atteints. Dans de nombreuses applications du parcours en
profondeur, on souhaite parcourir
tous
les sommets du graphe, et non pas seulement ceux
qui sont accessibles depuis un certain sommet
dfs
v.
Pour cela, il sut de lancer la mthode
sur tous les sommets du graphe :
void dfs() {
for (V v : [Link]())
dfs(v);
}
(La surcharge nous permet d'appeler galement cette mthode
visit par un prcdent parcours, l'appel
dfs.) Pour un sommet dj
dfs(v) va nous redonner la main immdiatement,
et sera donc sans eet. Le code complet est donn programme 36 page 175. On l'a complt
par une mthode
getNum
qui permet de consulter le contenu de
visited
(une fois le
parcours eectu).
Comme on vient de l'expliquer, le parcours en profondeur est, comme le parcours en
largeur, un moyen de dterminer l'existence d'un chemin entre un sommet particulier, la
source, et les autres sommets du graphe. Si c'est l le seul objectif (par exemple, la distance minimale ne nous intresse pas), alors le parcours en profondeur est gnralement
plus ecace. En eet, son occupation mmoire (la pile d'appels) sera le plus souvent bien
15.1. Parcours de graphes
Programme 36 Parcours en profondeur (DFS)
class DFS<V> {
private final Graph<V> g;
private final HashMap<V, Integer> visited;
private int count;
DFS(Graph<V> g) {
this.g = g;
[Link] = new HashMap<V, Integer>();
[Link] = 0;
}
void dfs(V v) {
if ([Link](v)) return;
[Link](v, [Link]++);
for (V w : [Link](v))
dfs(w);
}
void dfs() {
for (V v : [Link]())
dfs(v);
}
int getNum(V v) {
return [Link](v);
}
175
176
Chapitre 15. Algorithmes lmentaires sur les graphes
infrieure celle du parcours en largeur. L'exemple typique est celui d'un arbre, o l'occupation mmoire sera limite par la hauteur de l'arbre pour un parcours en profondeur,
mais pourra tre aussi importante que l'arbre tout entier dans le cas d'un parcours en largeur. Le parcours en profondeur a beaucoup d'autres application, qui dpassent largement
le cadre de ce cours ; voir par exemple
Exercice 15.3.
Modier la classe
Introduction to Algorithms
[2].
DFS pour conserver le chemin entre la source et chaque
sommet atteint par le parcours. Une faon simple de procder consiste stocker, pour
chaque sommet atteint, le sommet qui a permis de l'atteindre, par exemple dans une table
de hachage. Le chemin est donc dcrit l'envers , du sommet atteint vers la source.
Exercice 15.4.
En quoi le parcours en profondeur est-il dirent/semblable du parcours
inxe d'un arbre dcrit page 79 ?
Exercice 15.5.
Rcrire la mthode
dfs
en utilisant une boucle
mthode rcursive. Indication : on utilisera une
pile
while
plutt qu'une
contenant des sommets partir
desquels il faut eectuer le parcours en profondeur. Le code doit ressembler celui du
parcours en largeur la pile prenant la place de la le mais il y a cependant une
dirence dans le traitement des sommets dj visits. Question subsidiaire : les sommets
sont-ils ncessairement numrots exactement comme dans la version rcursive ?
Exercice 15.6.
G ne contenant pas de cycle (on appelle cela un
DAG pour Directed Acyclic Graph ). Un tri topologique de G est une liste de ses sommets
compatible avec les arcs, c'est--dire o un sommet x apparat avant un sommet y ds lors
qu'on a un arc x y . Modier le programme 36 pour qu'il renvoie un tri topologique,
sous la forme d'une mthode List<V> topologicalSort(). On pourra introduire une
liste de type LinkedList dans laquelle le sommet v est ajout avec addFirst une fois que
l'appel dfs(v) est termin.
Soit un graphe orient
Exercice * 15.7.
Le parcours en profondeur peut tre modi pour dtecter la prsence
d'un cycle dans le graphe. Lorsque la mthode
ne sait pas
a priori
dfs
tombe sur un sommet dj visit, on
si on vient de trouver un cycle ; il peut s'agir en eet d'un sommet
dj atteint par un autre chemin, parallle. Il faut donc modier le marquage des sommets
pour utiliser non pas deux tats (atteint / non atteint) mais trois : non atteint / en cours
de visite / visit. Modier la classe
mthode
boolean hasCycle()
DFS
en consquence, par exemple en ajoutant une
qui dtermine la prsence d'un cycle.
Question subsidiaire : Dans le cas trs particulier d'une liste simplement chane, en
quoi cela est-il plus/moins ecace que l'algorithme du livre et de la tortue (page 59) ?
Exercice * 15.8.
On peut utiliser un parcours en profondeur pour construire un laby-
rinthe parfait c'est--dire un labyrinthe o il existe un chemin et un seul entre deux
cases dans une grille
n m.
Pour cela, on considre au dpart le graphe o toutes les
cases de la grilles sont relies leurs voisines :
15.2. Plus court chemin
177
Puis on eectue un parcours en profondeur partir d'un sommet quelconque (par exemple
celui en haut gauche, mais ce n'est pas important). Quand on parcourt les successeurs
d'un sommet, on le fait dans un ordre alatoire. Une fois le parcours eectu, le labyrinthe
est obtenu en considrant qu'on peut passer d'un sommet un autre si l'arc correspondant
a t emprunt pendant le parcours en profondeur.
15.2 Plus court chemin
On considre ici des graphes dont les arcs sont tiquets par des poids (par exemple
des entiers) et on s'intresse au problme de trouver le plus court chemin d'un sommet
un sommet, la longueur n'tant plus le nombre d'arcs mais la somme des poids le long du
chemin. Ainsi, si on considre le graphe
2
1
4
1
2
3
alors le plus court chemin du sommet 2 au sommet 0 est de longueur 5. Il s'agit du
chemin
2 4 3 1 0.
En particulier, il est plus court que le chemin
2 1 0,
de longueur 6, mme si celui-ci contient moins d'arcs. De mme le plus court chemin du
sommet 2 au sommet 5 est de longueur 2, en passant par le sommet 4.
L'algorithme que nous prsentons ici pour rsoudre ce problme s'appelle l'algorithme
de Dijkstra. C'est une variation du parcours en largeur. Comme pour ce dernier, on se
donne un sommet de dpart, la source, et on procde par cercles concentriques . La
dirence est ici que les rayons de ces cercles reprsentent une distance en terme de poids
total et non en terme du nombre d'arcs. Ainsi dans l'exemple ci-dessus, en partant de
la source 2, on atteint d'abord les sommets distance 1 ( savoir 4), puis distance 2
( savoir 3 et 5), puis distance 3 ( savoir 1), puis enn distance 5 ( savoir 0). La
dicult de mise en uvre vient du fait qu'on peut atteindre un sommet avec une certaine
distance, par exemple le sommet 5 avec l'arc
2 5,
plus court en empruntant d'autres arcs, par exemple
puis trouver plus tard un chemin
2 4 5.
On ne peut plus se
contenter d'une le comme dans le parcours en largeur ; on va utiliser une
le de priorit
(voir chapitre 7). Elle contiendra les sommets dj atteints, ordonns par distance la
source. Lorsqu'un meilleur chemin est trouv, le sommet est remis dans la le avec une
plus grande priorit, c'est--dire une distance plus petite .
Dcrivons le code Java de l'algorithme de Dijkstra. Il faut se donner le poids de chaque
arc. On pourrait envisager modier la structure de graphe pour qu'elle contienne le poids
3. Une autre solution consisterait utiliser une structure de le de priorit o il est possible de
modier la priorit d'un lment se trouvant dj dans la le. Bien que de telles structures existent, elles
sont complexes mettre en uvre et, bien qu'asymptotiquement meilleure, leur utilisation n'apporte pas
ncessairement un gain en pratique. La solution que nous prsentons ici est un trs bon compromis.
178
Chapitre 15. Algorithmes lmentaires sur les graphes
de chaque arc. De manire quivalente, on choisit ici de se donner plutt une fonction de
poids, comme un objet qui implmente l'interface suivante
interface Weight<V> {
int weight(V x, V y);
}
c'est--dire qui fournit une mthode
weight donnant le poids
x et y que lorsqu'il
mthode ne sera appele sur des arguments
arc entre
et
de l'arc
x y.
Cette
existe eectivement un
dans le graphe.
Pour raliser la le de priorit, on utilise la bibliothque Java
contenir des paires
(v,d)
est un sommet et
PriorityQueue. Elle va
sa distance la source. On reprsente
ces paires avec la classe
class Node<V> {
V node;
int dist;
}
Pour que ces paires soient eectivement ordonnes par la distance, et utilises en cons-
PriorityQueue, il faut que la classe Node implmente
Comparable<Node<V>>, et fournisse donc une mthode compareTo. Le code
quence par la classe
l'interface
est imm-
diat ; il est donn page 180.
On en vient au code de l'algorithme proprement dit. On l'crit comme une mthode
shortestPaths,
qui prend le graphe, la source et la fonction de poids en arguments, et
qui renvoie une table donnant les sommets atteints et leur distance la source.
static <V> HashMap<V, Integer>
shortestPaths(Graph<V> g, V source, Weight<V> w) {
On commence par crer un ensemble
visited
contenant les sommets pour lesquels on a
dj trouv le plus court chemin.
HashSet<V> visited = new HashSet<V>();
Puis on cre une table
distance
contenant les distances dj connues. On y met initia-
lement la source avec la distance 0. Les distances dans cette table ne sont pas forcment
optimales ; elles pourront tre amliores au fur et mesure du parcours.
HashMap<V, Integer> distance = new HashMap<V, Integer>();
[Link](source, 0);
Enn, on cre la le de priorit,
pq,
et on y insre la source avec la distance 0.
PriorityQueue<Node<V>> pq = new PriorityQueue<Node<V>>();
[Link](new Node<V>(source, 0));
Comme pour le parcours en largeur, on procde alors une boucle, tant que la le n'est
pas vide.
while (![Link]()) {
15.2. Plus court chemin
179
Le cas chant, on extrait le premier lment de la le. S'il appartient
visited, c'est que
l'on a dj trouv le plus court chemin jusqu' ce sommet. On l'ignore donc, en passant
directement l'itration suivante de la boucle.
Node<V> n = [Link]();
if ([Link]([Link])) continue;
Cette situation peut eectivement se produire lorsqu'un premier chemin est trouv puis
un autre, plus court, trouv plus tard. Ce dernier passe alors dans la le de priorit devant
le premier. Lorsque le chemin plus long nira par sortir de la le, il faudra l'ignorer. Si le
visited, c'est qu'on vient de dterminer le plus court chemin.
sommet visited.
sommet n'appartient pas
On ajoute donc le
[Link]([Link]);
v. La distance v en empruntant l'arc correspondant
est la somme de la distance [Link], c'est--dire [Link], et du poids de l'arc, donn par
la mthode [Link].
Puis on examine chaque successeur
for (V v: [Link]([Link])) {
int d = [Link] + [Link]([Link], v);
v. Soit c'est la premire fois qu'on
distance. Dans ce dernier cas, on peut ou non amliorer
[Link]. On regroupe les cas o distance doit tre mise
Plusieurs cas de gure sont possibles pour le sommet
l'atteint, soit il tait dj dans
la distance
en passant par
jour dans un seul test.
if ( || d < [Link](v)) {
[Link](v, d);
[Link](new Node<V>(v, d));
}
On a d'une part mis jour
distance
d.
distance et
d'autre part insr
v dans
la le avec la nouvelle
Une fois tous les successeurs traits, on ritre la boucle principale. Une fois
qu'on est sorti de celle-ci, tous les sommets atteignables sont dans
distance,
avec leur
distance minimale la source. C'est ce que l'on renvoie.
return distance;
Le code complet est donn programme 37 page 180. Le rsultat de l'algorithme de Dijkstra
sur le graphe donn en exemple plus haut, partir de la source 2, est ici dessin avec les
distances obtenues au nal pour chaque sommet en exposant :
05
1
32
2
1
1
13
1
41
4
1
1
20
3
52
180
Chapitre 15. Algorithmes lmentaires sur les graphes
Programme 37 Algorithme de Dijkstra (plus court chemin)
interface Weight<V> {
int weight(V x, V y);
}
class Node<V> implements Comparable<Node<V>> {
V node;
int dist;
Node(V node, int dist) {
[Link] = node;
[Link] = dist;
}
public int compareTo(Node<V> n) {
return [Link] - [Link];
}
}
class Dijkstra {
static <V> HashMap<V, Integer>
shortestPaths(Graph<V> g, V source, Weight<V> w) {
HashSet<V> visited = new HashSet<V>();
HashMap<V, Integer> distance = new HashMap<V, Integer>();
[Link](source, 0);
PriorityQueue<Node<V>> pq = new PriorityQueue<Node<V>>();
[Link](new Node<V>(source, 0));
while (![Link]()) {
Node<V> n = [Link]();
if ([Link]([Link])) continue;
[Link]([Link]);
for (V v: [Link]([Link])) {
int d = [Link] + [Link]([Link], v);
if ( || d < [Link](v)) {
[Link](v, d);
[Link](new Node<V>(v, d));
}
}
}
return distance;
}
}
15.2. Plus court chemin
181
Sur cet exemple, tous les sommets ont t atteints par le parcours. Comme pour les
parcours en largeur et en profondeur, ce n'est pas toujours le cas : seuls les sommets pour
lesquels il existe un chemin depuis la source seront atteints.
valuons la complexit de l'algorithme de Dijkstra, dans le pire des cas. La le de
priorit peut contenir jusqu'
lments, car l'algorithme visite chaque arc au plus une
fois, et chaque considration d'un arc peut conduire l'insertion d'un lment dans la le.
add et poll de la le de priorit ont un cot logarithmique
(c'est le cas pour la bibliothque PriorityQueue et pour les les de priorit dcrites au
chapitre 7), chaque opration sur la le a donc un cot O(log E), c'est--dire O(log V )
2
car E V . D'o un cot total O(E log V ).
En supposant que les oprations
Exercice 15.9.
Modier la mthode
shortestPaths
pour conserver le chemin entre la
source et chaque sommet atteint par le parcours. Une faon simple de procder consiste
stocker, pour chaque sommet atteint, le sommet qui a permis de l'atteindre, par exemple
dans une table de hachage. Le chemin est donc dcrit l'envers , du sommet atteint
vers la source. Attention : lorsqu'un chemin est amlior, il faut mettre jour cette table.
182
Chapitre 15. Algorithmes lmentaires sur les graphes
Annexes
Lexique Franais-Anglais
franais
anglais
voir pages
aectation
assignment
arbre
tree
77
arbre binaire
binary tree
77
arbre de prxes
trie
92
arc
(directed) edge
163
arte
edge
163
champ
eld
chemin
path
177
corde
rope
96
degr entrant
indegree
163
degr sortant
outdegree
163
feuille
leaf
77
le
queue (FIFO)
54
le de priorit
priority queue
101
ottant
oating-point number
13
graphe
graph
163
graphe (non) orient
(un)directed graph
163
graphe orient acyclique
directed acyclic graph (DAG)
176
graphe tiquet
labeled graph
inxe (parcours)
inorder traversal
79
liste
liste
49
liste d'adjacence
adjacency list
166
matrice d'adjacence
adjacency matrix
164
mmosation
memoization
125
mthode
method
parcours en largeur
breadth-rst search (BFS)
170
parcours en profondeur
depth-rst search (DFS)
172
pgcd
gcd
119
pile
stack (LIFO)
54, 44
plus court chemin
shortest path
177
programmation
dynamic
186
Chapitre A. Lexique Franais-Anglais
franais
dynamique
anglais
programming (DP)
voir pages
127
racine
root
77
rebroussement
backtracking
131
rednition
overriding
seau
bucket
70
sommet
vertex
163
surcharge
overloading
table de hachage
hash table
69
tableau
array
33
tableau
resizable array
39
redimensionnable
tas
heap
101
transtypage
cast
47
tri
sorting
137
tri en place
in-place sort
tri fusion
mergesort
142
tri par insertion
insertion sort
137
tri par tas
heapsort
146
tri rapide
quicksort
138
tri topologique
topological sort
176
Bibliographie
[1] G. M. Adel'son-Vel'ski
and E. M. Landis. An algorithm for the organization of information.
Soviet MathematicsDoklady,
3(5) :12591263, September 1962.
[2] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Cliord Stein.
troduction to Algorithms, Second Edition.
[3] R.L. Graham, D.E. Knuth, and O. Patashnik.
for Computer Science.
In-
The MIT Press, September 2001.
Concrete mathematics, a Foundation
Addison-Wesley, 1989.
[4] J. Kleinberg and E. Tardos.
Algorithm design.
Addison-Wesley Longman Publishing
Co., Inc. Boston, MA, USA, 2005.
[5] Donald E. Knuth.
The Art of Computer Programming, volume 2 (3rd ed.) : Seminu-
merical Algorithms.
[6] Donald E. Knuth.
and Searching.
Addison-Wesley Longman Publishing Co., Inc., 1997.
The Art of Computer Programming, volume 3 : (2nd ed.) Sorting
Addison Wesley Longman Publishing Co., Inc., 1998.
[7] Donald R. Morrison. PATRICIAPractical Algorithm To Retrieve Information Coded in Alphanumeric.
J. ACM,
15(4) :514534, 1968.
[8] Robert Sedgewick and Kevin Wayne.
Introduction to Programming in Java.
Wesley, 2008.
[9] Henry S. Warren.
Hackers's Delight.
Addison-Wesley, 2003.
Addison
188
BIBLIOGRAPHIE
Index
!= (oprateur), 19, 65
O, 28
& (oprateur), 14
<< (oprateur), 14
== (oprateur), 19, 75
>> (oprateur), 14
>>> (oprateur), 14
^ (oprateur), 14
(oprateur), 14
binary search,
bit, 14
36
boucle, 163
C++, 7, 17
calculabilit, 23
champ, 3
chemin
dans un graphe, 164
classe, 3
abstract,
10
abstraite, 10
adresse, 18
classes disjointes, 111
algorithme, 23
code prxe, 151
Comparable<T> ([Link].),
Comparator<T> ([Link].),
de Dijkstra, 177
de Human, 151
alias, 19, 21, 38, 61
complment deux, 14
arbre, 77
complexit, 23
auto-quilibr, 107
amortie, 44, 74
binaire, 77
asymptotique, 27
binaire de recherche, 79
d'un algorithme
de Patricia, 94
au pire cas, 24
en moyenne, 25
de prxes, 92
quilibr, 84
d'un problme, 26
compression, 151
arc, 163
arithmtique, 119
Arrays ([Link].),
constructeur, 3
37
corde, 96
arte, 163
crible d'ratosthne, 122
AVL, 84
cycle
backtracking,
dtection de, 59, 176
131
Bzout, 120
DAG, 176
BFS, 170
dbordement arithmtique,
14
91, 149
109
190
INDEX
dcalage, 14
Human
algorithme de, 151
dcidable, 23
hritage, 7
DFS, 172
Dijkstra
immuable, 61
algorithme de, 177
diviser pour rgner, 36, 138
implements,
galit, 19
interface, 12
indcidable, 23
physique, 20
Java, 3
structurelle, 20, 75
encapsulation, 4, 45, 54
ensemble, 69, 90, 92
ratosthne, 122
tiquette, 163
Euclide, 119
Euler, 124
exception, 82
exponentiation rapide, 121
feuille, 77
Fibonacci, 30, 35, 88, 120, 121, 125
le, 54
de priorit, 101
final,
61
ottant, 15
Floyd, Robert W., 59
Garbage Collector,
12
[Link].
Comparable<T>, 91, 149
[Link].
Arrays, 37
Comparator<T>, 109
HashMap<K, V>, 74
HashSet<E>, 74
LinkedHashMap<E>, 76
LinkedHashSet<E>, 76
LinkedList<E>, 64
PriorityQueue, 154
PriorityQueue<E>, 109
Queue<E>, 57
Stack<E>, 45, 54
TreeMap<K, V>, 88
TreeSet<E>, 88
Vector<E>, 41
Josephus, 64
voir GC
GC, 17, 21, 41, 47, 57
Knuth, Donald E.
gnricit, 10
Knuth shue,
gnrique, 74
graphe, 163
35
labyrinthe, 116, 176
dense, 164
Lam, thorme de, 119
listes d'adjacence, 166
Landau, notation de, 28
matrice d'adjacence, 164
livre, 59, 176
tri topologique, 176
LinkedHashMap<E> ([Link].),
LinkedHashSet<E> ([Link].),
LinkedList<E> ([Link].), 64
hachage, 69
HashMap<K, V> ([Link].),
HashSet<E> ([Link].), 74
hauteur d'un arbre, 77
heap
voir tas, 101
heapsort
voir tri par tas, 107
74
liste
chane, 49
cyclique, 59, 64
d'adjacence, 166
doublement chane, 61
simplement chane, 49
hritage, 96
[Link],
Horner
matrice
mthode de, 34, 71
35, 53
d'adjacence, 164
76
76
INDEX
191
sommet, 163
mlange
voir
Knuth shue,
d'une pile, 44
35
Stack<E> ([Link].), 45, 54
StackOverflowError, 18, 78, 79,
static, 5
StringBuilder, 44, 52
mmosation, 125
mesure lmentaire, 24
mthode, 4
null, 19
NullPointerException,
Object,
surcharge, 6, 75
19, 50, 60, 82
table de hachage, 69
10, 74, 75
tableau, 16, 33
objet, 3
OutOfMemoryError,
de gnriques, 47
17
parcours, 34
redimensionnable, 39, 102
paquetage, 13
Tarjan, Robert Endre, 112
parcours
tas, 17, 101
d'arbre, 79
this,
de graphe, 169
de liste, 50
en largeur d'un graphe, 170
en profondeur d'un graphe, 172
inxe, 79
transtypage, 47
TreeMap<K, V> ([Link].),
TreeSet<E> ([Link].), 88
tri, 137
Pascal, 128
complexit optimale, 137
persistance, 61
fusion, 142
pgcd, 119
par insertion, 137
pile
par tas, 107, 146
d'appels, 17
rapide, 138
structure de, 44, 54
topologique, 176
pointeur, 18
PriorityQueue ([Link].), 154
PriorityQueue<E> ([Link].), 109
private, 4, 13
programmation dynamique, 125
protected,
public, 13
tortue, 59, 176
voir arbre de prxes
union nd,
111
valeur, 18
par dfaut, 19
13
passage par, 20, 38
Queue<E> ([Link].),
racine
d'un arbre, 77
rebroussement, 131
recherche
dichotomique, 36
rednition, 8
reines
problme des
trie,
N,
131
sentinelle, 63
skew heap, 107
smart constructor,
85
57
Vector<E> ([Link].),
visibilit, rgles de, 13
41
88
84, 88