Vous êtes sur la page 1sur 199

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 :

http://www.enseignement.polytechnique.fr/informatique/INF411/
L'auteur peut tre contact par courrier lectronique l'adresse suivante :

Jean-Christophe.Filliatre@lri.fr

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

p.rho = 2;
p.theta = 3.14159265;
double x = p.rho * Math.cos(p.theta);
p.theta = p.theta / 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

p.rho

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 :

File.java:19: 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 :

p.norm()
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 this.rho; }


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) {
this.rho = r;
this.theta = 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) {


this.rho = rho;
this.theta = theta;
}
Il n'y a pas d'ambigut, puisque

rho

dsigne le paramtre et

this.rho

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) {
this.rho *= p.rho; this.theta = normalize(this.theta + p.theta);
}
void mult(double f) {
this.rho *= f;
}
}
On peut alors crire des expressions comme

p.mult(p)

ou encore

p.mult(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) {
this.rho *= p.rho; this.theta = normalize(this.theta + p.theta);
}
void mult_double(double f) {
this.rho *= 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;
this.width = Math.abs(x1 - x2);
this.height = Math.abs(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);


p.move(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

p.draw();

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);


g.draw(); // 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;
this.radius = r;
this.width = this.height = 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() {
this.group = 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) {
this.group.add(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.


this.group on utilise la construction for de Java :

chaque lment
liste

this.group,

Pour parcourir la

void draw() {
for (Graphical g : this.group)
g.draw();
}
Cette construction aecte successivement la variable

this.group

g les dirents lments de la liste

et, pour chacun, excute le corps de la boucle. Ici le corps de la boucle est

g.draw. 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 : this.group)
g.move(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
java.lang.Object. 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) {
this.fst = a;
this.snd = 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 this.fst; }


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>(p.snd, p.fst);
}
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 = Pair.swap(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 (!s.isEmpty()) r += s.pop();
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} ;
System.out.println("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 ;
System.out.println("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);
System.out.println(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 < T.length; 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 < T.length; 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);
System.out.println("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" java.lang.ArrayIndexOutOfBoundsException: -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 a.length. Ainsi a.length

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


a.length 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 < a.length; 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

a.length

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 < a.length; i++)
s += a[i] * Math.pow(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 = a.length - 1; i >= 0; i--)
s = a[i] + x * s;
return s;
}
On constate facilement que ce programme eectue exactement

a.length

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)(Math.random() * 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
a.length-1,

respectivement.

3.2. Recherche dans un tableau

37

static boolean binarySearch(int[] a, int v) {


int g = 0, d = a.length - 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

java.util.Arrays.
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 = a.length - 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 :

this.data

this.length

. . . lments . . .

. . . inutilis . . .

this.data.length

On maintiendra donc toujours l'invariant suivant :

0 length data.length
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) {
this.length = len;
this.data = new int[len];
}
int size() {
return this.length;
}
Pour accder au

i-ime

lment du tableau redimensionnable, il convient de vrier la

validit de l'accs, car le tableau

this.data peut contenir plus de this.length lments.

int get(int i) {
if (i < 0 || i >= this.length)
throw new ArrayIndexOutOfBoundsException(i);
return this.data[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

this.data,

il va

setSize

Plusieurs cas de

len est infrieure ou gale la taille de this.data,


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 = this.data.length;
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

this.data

(nous la justierons par la suite) :

int[] a = new int[Math.max(len, 2 * n)];


On recopie alors les lments signicatifs de

this.data

vers ce nouveau tableau

for (int i = 0; i < this.length; i++)


a[i] = this.data[i];
puis on remplace

this.data

par ce nouveau tableau :

this.data = 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

this.length,

this.length = len;

setSize. L'intgralit du code est donne programme 2 page 42.


Note : La bibliothque Java fournit une telle classe, dans java.util.Vector<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

numbers.txt,

on commence par l'ouvrir de la manire suivante :

BufferedReader f = new BufferedReader(new FileReader("numbers.txt"));

42

Chapitre 3. Tableaux

Programme 2  Tableaux redimensionnables


class ResizableArray {
private int length; // nb d'lments significatifs
private int[] data; // invariant: 0 <= length <= data.length
ResizableArray(int len) {
this.length = len;
this.data = new int[len];
}
int size() {
return this.length;
}
int get(int i) {
if (i < 0 || i >= this.length)
throw new ArrayIndexOutOfBoundsException(i);
return this.data[i];
}
void set(int i, int v) {
if (i < 0 || i >= this.length)
throw new ArrayIndexOutOfBoundsException(i);
this.data[i] = v;
}

void setSize(int len) {


int n = this.data.length;
if (len > n) {
int[] a = new int[Math.max(len, 2 * n)];
for (int i = 0; i < this.length; i++)
a[i] = this.data[i];
this.data = a;
}
this.length = 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 = f.readLine();
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 = r.size();


r.setSize(len + 1);
r.set(len, Integer.parseInt(s));

Ceci conclut la lecture du chier. Pour tre complet, il faudrait aussi grer les exceptions

new FileReader d'une part et par f.readLine()


(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++)
buf.append(", " + i);
La chane nale peut tre rcupre avec

buf.toString().

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();


s.push(A);

s.push(B);
s.push(C);

C
B
A

int x = s.pop();
// 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

java.util.Stack<E>.

46

Programme 3  Structure de pile


(ralise l'aide d'un tableau redimensionnable)

class Stack {
private ResizableArray elts;
Stack() {
this.elts = new ResizableArray(0);
}
boolean isEmpty() {
return this.elts.size() == 0;
}
int size() {
return this.elts.size();
}
void push(int x) {
int n = this.elts.size();
this.elts.setSize(n + 1);
this.elts.set(n, x);
}
int pop() {
int n = this.elts.size();
if (n == 0)
throw new NoSuchElementException();
int e = this.elts.get(n - 1);
this.elts.setSize(n - 1);
return e;
}
int top() {
int n = this.elts.size();
if (n == 0)
throw new NoSuchElementException();
return this.elts.get(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

this.data = 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

this.data = (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 = this.data.length;
if (len > n) {
... mme code qu'auparavant ...
} else {
for (int i = len; i < n; i++)
this.data[i] = null;
}
this.length = len;
}

length.

On le fait en leur aectant la

48

Chapitre 3. Tableaux

On maintient donc l'invariant suivant :

i. this.length i < this.data.length this.data[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) {


this.element = element;
this.next = 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 = x.next.

Le parcours s'arrte

Le schma d'une boucle parcourant tous les lments

d'une liste est donc le suivant :

while (x != null) {
...
x = x.next;
}
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 (s.element == x) return true;


Sinon, on passe l'lment suivant :

s = s.next;

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) {
this.element = element;
this.next = next;
}

static boolean contains(Singly s, int x) {


while (s != null) {
if (s.element == x) return true;
s = s.next;
}
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.

s.element,

au

s'il ne s'agit pas du dernier lment de la liste,

StringBuilder, puis
c'est--dire si s.next

la chane
n'est pas

while (s != null) {
sb.append(s.element);
if (s.next != null) sb.append(" -> ");
s = s.next;
}
Une fois sorti de la boucle, on ajoute le crochet fermant et on renvoie la chane contenue
dans le

StringBuilder.

return sb.append("]").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 * Math.random()) == 0) candidate = s.element;
index++;
s = s.next;
}
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 * Math.random()) == 0) candidate = s.element;
Pour cela, on tire un entier alatoirement entre 0 inclus et

index

exclus et on le compare

Math.random(), 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 = s.next;

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

Math.random() < 1

et donc

(int)(1 * Math.random()) = 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


s.push(1); s.push(2); s.push(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.

java.util.Stack<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() {
this.head = null;
}
boolean isEmpty() {
return this.head == null;
}
void push(int x) {
this.head = new Singly(x, this.head);
}
int top() {
if (this.head == null)
throw new NoSuchElementException();
return this.head.element;
}
int pop() {
if (this.head == null)
throw new NoSuchElementException();
int e = this.head.element;
this.head = this.head.next;
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 successiveq.push(1); q.push(2); q.push(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() {
this.head = this.tail = null;
}
this.head vaut null si et
this.tail vaut null. En particulier, la le est vide si et seulement this.head

De manire gnrale, nous allons maintenir l'invariant que


seulement
vaut

null

boolean isEmpty() {
return this.head == 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

this.head et this.tail pointent dsormais tous les deux sur cet unique lment de liste :

4.3. Application 2 : Structure de le

57

if (this.head == null)
this.head = this.tail = e;
e la n de la liste existante, dont le dernier lment est
oublier de mettre ensuite jour le pointeur this.tail :

Dans le cas contraire, on ajoute


point par

this.tail,

sans

else {
this.tail.next = e;
this.tail = e;
}
push. Pour le retrait d'un lment, on procde l'autre extrmit
de la liste, c'est--dire du ct de this.head. On commence par vacuer le cas d'une liste
Ceci conclut le code de
vide :

int pop() {
if (this.head == 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 = this.head.element;
Avant de la renvoyer, il faut supprimer le premier lment de la liste, ce qui est aussi
simple que

this.head = this.head.next;
this.head
null :

Cependant, pour maintenir notre invariant sur

this.tail

null

si

this.head

est devenu

et

this.tail,

on va mettre

if (this.head == null) this.tail = null;


Cette ligne de code n'est pas ncessaire pour la correction de notre structure de le.

push teste la
this.tail null

this.head

et non celle de

this.tail.

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.

java.util.Queue<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() {
this.head = this.tail = null;
}
boolean isEmpty() {
return this.head == null;
}
void push(int x) {
Singly e = new Singly(x, null);
if (this.head == null)
this.head = this.tail = e;
else {
this.tail.next = e;
this.tail = e;
}
}
int top() {
if (this.head == null)
throw new NoSuchElementException();
return this.head.element;
}

int pop() {
if (this.head == null)
throw new NoSuchElementException();
int e = this.head.element;
this.head = this.head.next;
if (this.head == null) this.tail = 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

s4.next = 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 = s.next;
while (tortoise != hare) {
if (hare == null) return false;
hare = hare.next;
if (hare == null) return false;
hare = hare.next;
tortoise = tortoise.next;
}
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

hare.next.

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) {
this.element = element;
this.next = this.prev = 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.

e.prev = this;
this est e. Cependant, il y avait
this.next, et il convient alors
e et this.next.

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 (this.next != null) {
e.next = this.next;
e.next.prev = e;
}
Enn, on peut mettre jour

this.next, ce qui achve le code le la mthode insertAfter.

this.next = 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);


x.insertAfter(3);
x.insertAfter(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 (this.prev !=
this.prev.next
if (this.next !=
this.next.prev
}

null)
= this.next;
null)
= this.prev;

La seule dicult consiste correctement traiter les cas o


nuls, c'est--dire lorsque

this

this.prev ou this.next 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

x.remove()

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) {
this.element = element;
this.next = this.prev = null;
}
void insertAfter(int v) {
Doubly e = new Doubly(v);
e.prev = this;
if (this.next != null) {
e.next = this.next;
e.next.prev = e;
}
this.next = e;
}
void remove() {
if (this.prev !=
this.prev.next
if (this.next !=
this.next.prev
}

null)
= this.next;
null)
= this.prev;

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

java.util.LinkedList<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--) {


l1.insertAfter(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) { l1.prev = l1.next; l1.next.next = 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

c.next.

while (c != c.next) {
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 = c.next, puis on limine le joueur c ainsi obtenu
c.remove.

avance
avec

for (int i = 1; i < p; i++)


c = c.next;
c.remove();
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--) {
l1.insertAfter(i);
if (i == n) { l1.prev = l1.next; l1.next.next = l1; }
}
return l1;
}
static int josephus(int n, int p) {
Doubly c = circle(n);
while (c != c.next) { // tant qu'il reste plus d'un joueur
for (int i = 1; i < p; i++)
c = c.next;
c.remove(); // on limine le p-ime
c = c.next;
}
return c.element;
}

c = c.next;

Ceci achve la boucle

while.

Le gagnant est le dernier lment dans la liste.

return c.element;

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) {
this.element = element;
this.next = 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() {
this.buckets = 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 < s.length(); i++)
h = s.charAt(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);
this.buckets[i] = new Bucket(s, this.buckets[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 = b.next)
if (b.element.equals(s)) return true;
return false;
}
La mthode

contains

de la classe

HashTable

est alors rduite une seule ligne :

boolean contains(String s) {
return Bucket.contains(this.buckets[hash(s)], s);
}
Le code complet est donn programme 11 page 72.

1. En revanche, il ne serait pas correct d'crire Math.abs(h) % M car si h est gal au plus petit entier,
c'est--dire 231 = 2147483648, alors Math.abs(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) {
this.element = element;
this.next = next;
}
static boolean contains(Bucket b, String s) {
for (; b != null; b = b.next)
if (b.element.equals(s)) return true;
return false;
}
}
class HashTable {
private Bucket[] buckets;
final private static int M = 17;
HashTable() {
this.buckets = new Bucket[M];
}
private int hash(String s) {
int h = 0;
for (int i = 0; i < s.length(); i++)
h = s.charAt(i) + 19 * h;
return (h & 0x7fffffff) % M;
}
void add(String s) {
int i = hash(s);
this.buckets[i] = new Bucket(s, this.buckets[i]);
}

boolean contains(String s) {
return Bucket.contains(this.buckets[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

this.buckets.length

et non plus mo-

return (h & 0x7fffffff) % this.buckets.length;


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 (this.size > this.buckets.length/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 * this.buckets.length;
Puis on alloue un nouveau tableau de cette taille-l dans

this.buckets,

sans oublier de

conserver un pointeur sur son ancienne valeur :

Bucket[] old = this.buckets;


this.buckets = new Bucket[n];
old vers
this.bucket. 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 = b.next) {
int i = hash(b.element);
this.buckets[i] = new Bucket(b.element, this.buckets[i]);
}

for

et,

74

Chapitre 5. Tables de hachage

hash a t modie pour rendre une valeur


this.buckets. 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

java.util.HashSet<E> pour des ensembles dont les lments sont


java.util.HashMap<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 this.fst.hashCode() + this.snd.hashCode();
}

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 this.fst.equals(p.fst) && this.snd.equals(p.snd);
}
(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 this.fst.equals(p.fst) && this.snd.equals(p.snd);
}
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, x.equals(y ) x.hashCode() = 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

java.util.LinkedHashSet<E>

et

java.util.LinkedHashMap<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(t.left) + size(t.right);
}
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(t.left);
System.out.println(t.value);
inorderTraversal(t.right);
}
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 (b.left != null) b = b.left;
return b.value;
}
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 == b.value) return true;
b = (x < b.value) ? b.left : b.right;
}
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 < b.value)
return new BST(add(b.left, x), b.value, b.right);
if (x > b.value)
return new BST(b.left, b.value, add(b.right, x));
Il est important de noter que, aprs l'appel rcursif, on reconstruit le nud de l'arbre de

b.value. 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 < b.value)
return new BST(remove(b.left, x), b.value, b.right);
if (x > b.value)
return new BST(b.left, b.value, remove(b.right, 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

b.left et b.right, mais il n'y a pas de moyen simple de raliser cette union. On souhaite autant que possible conserver b.left ou b.right inchang, pour limiter la quantit

de

de nuds reconstruire. La proprit d'arbre binaire de recherche nous suggre alors de

b.left, soit le plus petit


lment de b.right. Vu que nous avons dj une mthode getMin, nous allons pencher
pour la seconde solution. Il convient de traiter correctement le cas o b.right ne possde
aucun lment. Dans ce cas, il sut de renvoyer b.left.
placer la racine du nouvel arbre, soit le plus grand lment de

if (b.right == null)
return b.left;
getMin(b.right) et o ce dernier est
b.right 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(b.left, getMin(b.right), removeMin(b.right));


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 (b.left == null)
return b.right;
return new BST(removeMin(b.left), b.value, b.right);
}
b dirent de null (dans le cas
contraire, b.left 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) {
this.left = left;
this.value = value;
this.right = right;
}
static boolean contains(BST b, int x) {
while (b != null) {
if (b.value == x) return true;
b = (x < b.value) ? b.left : b.right;
}
return false;
}
static BST add(BST b, int x) {
if (b == null)
return new BST(null, x, null);
if (x < b.value)
return new BST(add(b.left, x), b.value, b.right);
if (x > b.value)
return new BST(b.left, b.value, add(b.right, x));
return b; // x dj dans b
}
static int getMin(BST b) { // suppose b != null
while (b.left != null) b = b.left;
return b.value;
}
static BST removeMin(BST b) { // suppose b != null
if (b.left == null)
return b.right;
else
return new BST(removeMin(b.left), b.value, b.right);
}
static BST remove(BST b, int x) {
if (b == null)
return null;
if (x < b.value)
return new BST(remove(b.left, x), b.value, b.right);
if (x > b.value)
return new BST(b.left, b.value, remove(b.right, x));
if (b.right == null)
return b.left;
return new BST(b.left, getMin(b.right), removeMin(b.right));
}
}

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 : a.height;
}
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) {


this.left = left;
this.value = value;
this.right = right;
this.height = 1 + Math.max(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 = l.left, lr = l.right;


int lv = l.value;
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 = lr.left, lrr = lr.right;
int lrv = lr.value;

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

java.util.TreeSet<E> pour des


E et la classe java.util.TreeMap<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) {
this.left = left;
this.value = value;
this.right = right;
this.height = 1 + Math.max(height(left), height(right));
}
static int height(AVL a) {
return (a == null) ? 0 : a.height;
}
static AVL balance(AVL l, int v, AVL r) {
int hl = height(l), hr = height(r);
if (hl > hr + 1) {
AVL ll = l.left, lr = l.right;
int lv = l.value;
if (height(ll) >= height(lr))
return new AVL(ll, lv, new AVL(lr, v, r));
else {
AVL lrl = lr.left, lrr = lr.right;
int lrv = lr.value;
return new AVL(new AVL(ll, lv, lrl), lrv, new AVL(lrr, v, r));
}
} else if (hr > hl + 1) {
AVL rl = r.left, rr = r.right;
int rv = r.value;
if (height(rr) >= height(rl))
return new AVL(new AVL(l, v, rl), rv, rr);
else {
AVL rll = rl.left, rlr = rl.right;
int rlv = rl.value;
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() {
this.root = null;
}
boolean isEmpty() {
return this.root == null;
}
boolean contains(int x) {
return AVL.contains(this.root, x);
}
void add(int x) {
this.root = AVL.add(x, this.root);
}

void remove(int x) {
this.root = AVL.remove(x, this.root);
}

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


java.lang.Comparable<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

x.compareTo(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 = x.compareTo(a.value);
if (c == 0) return true;
a = (c < 0) ? a.left : a.right;
}
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() {
this.word = false;
this.branches = 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 < s.length(); i++) { // invariant t != null
t = t.branches.get(s.charAt(i));
t.branches 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 t.word;

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 < s.length(); i++) { // invariant t != null
char c = s.charAt(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 = t.branches;


t = b.get(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();
b.put(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

}
t.word = 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 !this.word && this.branches.isEmpty();
}
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() {
this.word = false;
this.branches = new HashMap<Character, Trie>();
}
boolean contains(String s) {
Trie t = this;
for (int i = 0; i < s.length(); i++) { // invariant t != null
t = t.branches.get(s.charAt(i));
if (t == null) return false;
}
return t.word;
}

void add(String s) {
Trie t = this;
for (int i = 0; i < s.length(); i++) { // invariant t != null
char c = s.charAt(i);
Map<Character, Trie> b = t.branches;
t = b.get(c);
if (t == null) {
t = new Trie();
b.put(c, t);
}
}
t.word = 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) {


this.str = str;
this.ofs = ofs;
this.length = 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) {


this.left = left;
this.right = right;
this.length = left.length + right.length;
}
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 r.get(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 this.str.charAt(this.ofs + 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 < this.left.length) ?
this.left.get(i) : this.right.get(i - this.left.length);
}
Toute la subtilit de la programmation oriente objet se trouve ici : on ne connat pas la
nature de

this.left

et

this.right

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(this.str, this.ofs + 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

this.left

Rope sub(int ofs, int len) {


int llen = this.left.length - ofs;
Si elle est plus grande que

this.left.

len,

c'est que le rsultat se trouve tout entier dans la corde

if (len <= llen)


return this.left.sub(ofs, len);
Si elle est ngative ou nulle, c'est que le rsultat se trouve tout entier dans la corde

this.right.

6.5. Cordes

99

if (llen <= 0)
return this.right.sub(-llen, len);
Sinon, c'est que le rsultat doit tre la concatnation d'une portion de
portion de

this.right,

rcursivement calcules avec

sub.

this.left et d'une

return new App(this.left.sub(ofs, llen),


this.right.sub(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) {
this.str = str;
this.ofs = ofs;
this.length = len;
}
char get(int i) {
return this.str.charAt(this.ofs + i);
}
Rope sub(int ofs, int len) {
return new Str(this.str, this.ofs + ofs, len);
}
}
class App extends Rope {
Rope left, right;
App(Rope left, Rope right) {
this.length = left.length + right.length;
this.left = left;
this.right = right;
}
char get(int i) {
return (i < this.left.length) ?
this.left.get(i) : this.right.get(i - this.left.length);
}
Rope sub(int ofs, int len) {
int llen = this.left.length - ofs;
if (len <= llen) // tout dans left
return this.left.sub(ofs, len);
if (llen <= 0) // tout dans right
return this.right.sub(-llen, len);
return new App(this.left.sub(ofs, llen),
this.right.sub(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() {
this.elts = 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 this.elts.size();
}
boolean isEmpty() {
return this.elts.size() == 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 (this.elts.size() == 0)
throw new NoSuchElementException();
return this.elts.get(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) {
this.elts.set(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 = this.elts.get(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) {
this.elts.set(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
this.elts.set(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 = this.elts.size();
this.elts.setSize(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 = this.elts.size();
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 && this.elts.get(j + 1) < this.elts.get(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 && this.elts.get(j) < x) {


this.elts.set(i, this.elts.get(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
this.elts.set(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 = this.elts.size() - 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 = this.elts.get(n);
this.elts.setSize(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() { this.elts = new ResizableArray(0); }
int size() { return this.elts.size(); }
boolean isEmpty() { return this.elts.size() == 0; }
private void moveUp(int x, int i) {
if (i == 0) {
this.elts.set(i, x);
} else {
int fi = (i - 1) / 2;
int y = this.elts.get(fi);
if (y > x) {
this.elts.set(i, y);
moveUp(x, fi);
} else
this.elts.set(i, x);
}
}
void add(int x) {
int n = this.elts.size();
this.elts.setSize(n + 1);
moveUp(x, n);
}
int getMin() {
if (this.elts.size() == 0) throw new NoSuchElementException();
return this.elts.get(0);
}
private void moveDown(int x, int i) {
int n = this.elts.size();
int j = 2 * i + 1;
if (j + 1 < n && this.elts.get(j + 1) < this.elts.get(j))
j++;
if (j < n && this.elts.get(j) < x) {
this.elts.set(i, this.elts.get(j));
moveDown(x, j);
} else
this.elts.set(i, x);
}
void removeMin() {
int n = this.elts.size() - 1;
if (n < 0) throw new NoSuchElementException();
int x = this.elts.get(n);
this.elts.setSize(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() {
this.root = null;
this.size = 0;
}
Les mthodes

isEmpty

et

size

sont galement immdiates. On note qu'elles s'excutent

en temps constant.

boolean isEmpty() {
return this.size == 0;
}
int size() {
return this.size;
}
isEmpty pourrait tout aussi bien
getMin renvoie le plus petit lment,

this.root

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 (this.isEmpty()) throw new NoSuchElementException();
return this.root.value;
}

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 (t1.value <= t2.value)


On doit maintenant dterminer les deux sous-arbres de

new Tree(..., t1.value, ...).


merge sur deux des trois

Il y a plusieurs possibilits, obtenues en appelant rcursivement


arbres

t1.left, t1.right

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

t1.left

t1,

de manire assurer l'auto-quilibrage. Ainsi,

et est fusionn avec

t2.

t1.right

prend la place de

return new Tree(merge(t1.right, t2), t1.value, t1.left);


L'autre situation, o la racine de

est la plus petite, est symtrique.

else
return new Tree(merge(t2.right, t1), t2.value, t2.left);

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) {
this.root = merge(this.root, new Tree(null, x, null));
this.size++;
}
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 (this.isEmpty()) 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 = this.root.value;


this.root = merge(this.root.left, this.root.right);
Enn, on met jour le champ

size

et on renvoie le plus petit lment

res.

this.size--;
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

x.compareTo(y) < 0.

La bibliothque Java propose une telle structure de donnes gnrique dans la classe

java.util.PriorityQueue<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

java.util.Comparator<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() {
this.root = null;
this.size = 0;
}
boolean isEmpty() {
return this.size == 0;
}
int size() {
return this.size;
}
int getMin() {
if (this.isEmpty()) throw new NoSuchElementException();
return this.root.value;
}
private static Tree merge(Tree t1, Tree t2) {
if (t1 == null) return t2;
if (t2 == null) return t1;
if (t1.value <= t2.value)
return new Tree(merge(t1.right, t2), t1.value, t1.left);
else
return new Tree(merge(t2.right, t1), t2.value, t2.left);
}
void add(int x) {
this.root = merge(this.root, new Tree(null, x, null));
this.size++;
}

int removeMin() {
if (this.isEmpty()) throw new NoSuchElementException();
int res = this.root.value;
this.root = merge(this.root.left, this.root.right);
this.size--;
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();
this.link = new int[n];
for (int i = 0; i < n; i++) this.link[i] = i;
this.rank = 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 >= this.link.length)
throw new ArrayIndexOutOfBoundsException(i);
int p = this.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 = this.find(p);
this.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 = this.find(i);
int rj = this.find(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 (this.rank[ri] < this.rank[rj])


this.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 {
this.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 (this.rank[ri] == this.rank[rj])
this.rank[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();
this.link = new int[n];
for (int i = 0; i < n; i++) this.link[i] = i;
this.rank = new int[n];
}
int find(int i) {
if (i < 0 || i >= this.link.length)
throw new ArrayIndexOutOfBoundsException(i);
int p = this.link[i];
if (p == i) return i;
int r = this.find(p);
this.link[i] = r;
return r;
}

void union(int i, int j) {


int ri = this.find(i);
int rj = this.find(j);
if (ri == rj) return;
if (this.rank[ri] < this.rank[rj])
this.link[ri] = rj;
else {
this.link[rj] = ri;
if (this.rank[ri] == this.rank[rj])
this.rank[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)(Math.sqrt(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)(Math.sqrt(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 = memo.get(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.

memo.put(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 = memo.get(n);
if (l != null) return l;
l = fibMemo(n - 2) + fibMemo(n - 1);
memo.put(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 == cols.length)
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 < cols.length; 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] || Math.abs(cols[q] - cols[r]) == r - q)
return false;
return true;
}
static boolean findSolutionRec(int[] cols, int r) {
if (r == cols.length)
return true;
for (int c = 0; c < cols.length; 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] || Math.abs(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 < a.length; i++) {

dj trie, ce qui

138

Chapitre 12. Tri

Programme 27  Tri par insertion


static void insertionSort(int[] a) {
for (int i = 1; i < a.length; 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, a.length);
}
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, a.length);
}

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[a.length], 0, a.length);
}
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[a.length], 0, a.length);
}
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 = a.length;
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 = a.length;
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 java.lang.Comparable<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 < a.length; i++) {
K v = a[i];
int j = i;
for (; 0 < j && v.compareTo(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 this.freq - that.freq;
}
}
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;
this.freq = 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) {
this.left = left;

Node

dont les deux

154

Chapitre 13. Compression de donnes


this.right = right;
this.freq = left.freq + right.freq;

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 (alphabet.size() <= 1) throw new IllegalArgumentException();
this.tree = buildTree(alphabet);
this.codes = new HashMap<Character, String>();
this.tree.traverse("", this.codes);
}
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

java.util.PriorityQueue

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)


pq.add(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 (pq.size() > 1) {


HuffmanTree left = pq.removeMin();
HuffmanTree right = pq.removeMin();
pq.add(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 pq.getMin();

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) {
m.put(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) {
this.left.traverse(prefix + '0', m);
this.right.traverse(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 < msg.length(); i++)


sb.append(this.codes.get(msg.charAt(i)));
Il n'y a plus qu' renvoyer la chane contenue dans

sb.

return sb.toString();

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 < msg.length()) {
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

this.tree 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 = this.tree.find(msg, i);


sb.append(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 this.codes.

Pour mettre jour la variable

i += this.codes.get(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 sb.toString();

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 == s.length())
throw new Error("corrupted code; bad alphabet?");
return (s.charAt(i) == '0' ? this.left : this.right).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 this.freq - that.freq;
}
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;
this.freq = 0;
}
void traverse(String prefix, Map<Character, String> m) {
m.put(this.c, prefix);
}
char find(String s, int i) {
return this.c;
}
}
class Node extends HuffmanTree {
HuffmanTree left, right;
Node(HuffmanTree left, HuffmanTree right) {
this.left = left;
this.right = right;
this.freq = left.freq + right.freq;
}
void traverse(String prefix, Map<Character, String> m) {
this.left.traverse(prefix + '0', m);
this.right.traverse(prefix + '1', m);
}
char find(String s, int i) {
if (i == s.length())
throw new Error("corrupted code; bad alphabet?");
return (s.charAt(i) == '0' ? this.left : this.right).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 (alphabet.size() <= 1) throw new IllegalArgumentException();
this.tree = buildTree(alphabet);
this.codes = new HashMap<Character, String>();
this.tree.traverse("", this.codes);
}
HuffmanTree buildTree(Collection<Leaf> alphabet) {
Heap<HuffmanTree> pq = new Heap<HuffmanTree>();
for (Leaf l: alphabet)
pq.add(l);
while (pq.size() > 1) {
HuffmanTree left = pq.removeMin();
HuffmanTree right = pq.removeMin();
pq.add(new Node(left, right));
}
return pq.getMin();
}
String encode(String msg) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < msg.length(); i++)
sb.append(this.codes.get(msg.charAt(i)));
return sb.toString();
}

String decode(String msg) {


StringBuilder sb = new StringBuilder();
int i = 0;
while (i < msg.length()) {
char c = this.tree.find(msg, i);
sb.append(c);
i += this.codes.get(c).length();
}
return sb.toString();
}

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() {
this.adj = new HashMap<Integer, Set<Integer>>();
}
void addVertex(int x) {
Set<Integer> s = this.adj.get(x);
if (s == null) this.adj.put(x, new HashSet<Integer>());
}
boolean hasEdge(int x, int y) {
Set<Integer> s = this.adj.get(x);
return s != null && s.contains(y);
}
void addEdge(int x, int y) {
this.adj.get(x).add(y);
}
void removeEdge(int x, int y) {
Set<Integer> s = this.adj.get(x);
if (s != null) s.remove(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 this.adj.keySet();
}
Set<V> successors(V v) {
return this.adj.get(v);
}
Ds lors, pour parcourir tous les sommets d'un graphe

g,

il sut d'crire

for (V v: g.vertices()) ...


et pour parcourir tous les successeurs d'un sommet

de

g,

il sut d'crire

for (V w: g.successors(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>();


visited.put(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>();


q.add(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 (!q.isEmpty()) {
V v = q.poll();
int d = visited.get(v);
On examine alors chaque successeur

de

v.

for (V w : g.successors(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 (!visited.containsKey(w)) {
q.add(w);
visited.put(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>();
visited.put(source, 0);
Queue<V> q = new LinkedList<V>();
q.add(source);
while (!q.isEmpty()) {
V v = q.poll();
int d = visited.get(v);
for (V w : g.successors(v))
if (!visited.containsKey(w)) {
q.add(w);
visited.put(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;
this.visited = new HashMap<V, Integer>();
this.count = 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 (this.visited.containsKey(v)) return;
this.visited.put(v, this.count++);
for (V w : this.g.successors(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 : this.g.vertices())
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;
this.visited = new HashMap<V, Integer>();
this.count = 0;
}
void dfs(V v) {
if (this.visited.containsKey(v)) return;
this.visited.put(v, this.count++);
for (V w : this.g.successors(v))
dfs(w);
}
void dfs() {
for (V v : this.g.vertices())
dfs(v);
}

int getNum(V v) {
return this.visited.get(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>();


distance.put(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>>();


pq.add(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 (!pq.isEmpty()) {

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 = pq.poll();
if (visited.contains(n.node)) 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

visited.add(n.node);
v. La distance v en empruntant l'arc correspondant
est la somme de la distance n.node, c'est--dire n.dist, et du poids de l'arc, donn par
la mthode w.weight.
Puis on examine chaque successeur

for (V v: g.successors(n.node)) {
int d = n.dist + w.weight(n.node, v);
v. Soit c'est la premire fois qu'on
distance. Dans ce dernier cas, on peut ou non amliorer
n.node. 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 (!distance.containsKey(v) || d < distance.get(v)) {


distance.put(v, d);
pq.add(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) {
this.node = node;
this.dist = dist;
}
public int compareTo(Node<V> n) {
return this.dist - n.dist;
}
}
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>();
distance.put(source, 0);
PriorityQueue<Node<V>> pq = new PriorityQueue<Node<V>>();
pq.add(new Node<V>(source, 0));
while (!pq.isEmpty()) {
Node<V> n = pq.poll();
if (visited.contains(n.node)) continue;
visited.add(n.node);
for (V v: g.successors(n.node)) {
int d = n.dist + w.weight(n.node, v);
if (!distance.containsKey(v) || d < distance.get(v)) {
distance.put(v, d);
pq.add(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> (java.lang.),
Comparator<T> (java.util.),

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 (java.util.),

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

java.lang.
Comparable<T>, 91, 149
java.util.
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> (java.util.),
LinkedHashSet<E> (java.util.),
LinkedList<E> (java.util.), 64

hachage, 69

HashMap<K, V> (java.util.),


HashSet<E> (java.util.), 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

Math.random,

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> (java.util.), 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> (java.util.),


TreeSet<E> (java.util.), 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 (java.util.), 154


PriorityQueue<E> (java.util.), 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> (java.util.),
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> (java.util.),
visibilit, rgles de, 13

41

88

84, 88

Vous aimerez peut-être aussi