+ All Categories
Home > Documents > [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

[John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Date post: 20-Jan-2016
Category:
Upload: nehadnina
View: 108 times
Download: 0 times
Share this document with a friend
384
STRUCTURES DE DONNÉES EN JAVA J. R. HUBBARD Professeur de mathématiques et d’informatique à l’université de Richmond (Virginie, USA) Traduit de l’américain par Virginie Maréchal
Transcript
Page 1: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

STRUCTURES DEDONNÉES EN JAVA

J. R. HUBBARDProfesseur de mathématiques et d’informatique

à l’université de Richmond (Virginie, USA)

Traduit de l’américain par Virginie Maréchal

lims Hubbard 22/06/06 12:33 Page 1

Page 2: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Original edition copyright © 2001 by The McGraw-Hill Companies, Inc. All rights reserved.L’édition originale de cet ouvrage est parue sous le titre :

Schaum’s Outline of Data Structures with Java.

© Dunod, Paris, 2003, pour l’édition française. Tous droits réservés.ISBN : 2-10-0006937-3

Toute représentation ou reproduction intégrale ou partielle faite sans le consentement de l’auteur ou de ses ayants droit ou ayants cause

est illicite selon le Code de la propriété intellectuelle (Art L 122-4) et constitue une contrefaçon réprimée par le Code pénal. • Seules sont

autorisées (Art L 122-5) les copies ou reproductions strictement réservées à l’usage privé du copiste et non destinées à une utilisation

collective, ainsi que les analyses et courtes citations justifiées par le caractère critique, pédagogique ou d’information de l’œuvre à laquelle

elles sont incorporées, sous réserve, toutefois, du respect des dispositions des articles L 122-10 à L 122-12 du même Code, relatives à la

reproduction par reprographie.

Ce pictogramme mérite une explication.Son objet est d’alerter le lecteur sur la menace que représente pour l’avenirde l’écrit, particulièrement dansle domaine de l’édition tech-nique et universitaire, le dévelop-pement massif du photo-copillage.

Le Code de la propriété intellectuelle du 1er juillet 1992interdit en effet expressément laphotocopie à usage collectifsans autorisation des ayants droit. Or,cette pratique s’est généralisée dans les

établissements d’enseignement supérieur,provoquant une baisse brutale des achatsde livres et de revues, au point que la

possibilité même pour les auteursde créer des œuvres nouvelles etde les faire éditer correctementest aujourd’hui menacée.

Nous rappelons donc quetoute reproduction, partielle outotale, de la présente publicationest interdite sans autorisation duCentre français d’exploitation du

droit de copie (CFC, 20 rue des Grands-Augustins, 75006 Paris).

lims Hubbard 22/06/06 12:33 Page 2

Page 3: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Sommaire

Avant-propos IX

Chapitre 1 Caractéristiques de base du langage java 1

1.1 Programmation orientée objet

1

1.2 Langage de programmation Java

1

1.3 Variables et objets

2

1.4 Types primitifs

3

1.5 Contrôle du flux

5

1.6 Classes

7

1.7 Modificateurs

9

1.8 Classe

String

10

1.9 Classe

Math

13

Questions de révision

16

Réponses

17

Exercices d’entraînement

18

Solutions

20

Chapitre 2 Caractéristiques de base des tableaux 25

2.1 Propriétés des tableaux

25

2.2 Copier un tableau

27

2.3 Classe

Arrays

28

2.4 Algorithme de recherche séquentielle

31

2.5 Algorithme de recherche binaire

33

2.6 Classe

Vector

35

Questions de révision

38

Réponses

39

Exercices d’entraînement

39

Solutions

44

Chapitre 3 Java avancé 57

3.1 Héritage

57

3.2 Polymorphisme

58

3.3 Conversion des types

60

3.4 Classe

Object

62

3.5 Classes abstraites

64

Page 4: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

IV

Structures de données en Java

3.6 Interfaces

67

3.7 Paquetages

70

3.8 Gérer les exceptions

70

Questions de révision

72

Réponses

72

Exercices d’entraînement

73

Solutions

74

Chapitre 4 Récursivité 77

4.1 La base et la partie récursive de la récursivité

78

4.2 Tracer un appel récursif

79

4.3 Algorithme récursif de recherche binaire

80

4.4 Coefficients binomiaux

82

4.5 Algorithme d’Euclide

83

4.6 Preuve inductive de correction

83

4.7 Analyse de la complexité des algorithmes récursifs

84

4.8 Programmation dynamique

85

4.9 Les tours de Hanoi

85

4.10 Récursivité mutuelle

87

Questions de révision

88

Réponses

89

Exercices d’entraînement

89

Solutions

92

Chapitre 5 Collections 99

5.1 Framework de collections Java

99

5.2 Interface

Collection

100

5.3 Classe

AbstractCollection

101

5.4 Classe

Bag

102

5.5 Interface

Iterator

109

Questions de révision

109

Réponses

110

Exercices d’entraînement

110

Solutions

111

Chapitre 6 Piles 115

6.1 Classe Java

Stack

115

6.2 Applications des piles

118

6.3 Supprimer la récursivité

121

Questions de révision

122

Réponses

122

Exercices d’entraînement

123

Solutions

125

Page 5: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Sommaire

V

Chapitre 7 Files 129

7.1 Framework des files

129

7.2 Implémentation contiguë

132

7.3 Implémentation chaînée

134

7.4 Applications utilisant les files

136

Questions de révision

142

Réponses

142

Exercices d’entraînement

142Solutions 144

Chapitre 8 Listes 1498.1 Interface java.util.List 1498.2 Implémenter l’interface java.util.List 1518.3 Classes AbstractList et AbstractSequentialList 1518.4 Itérateurs de listes 1538.5 Classe ArrayList 1548.6 Classe LinkedList 1558.7 Itérateurs de listes indépendants 164Questions de révision 165Réponses 165Exercices d’entraînement 166Solutions 167

Chapitre 9 Arbres 1719.1 Terminologie des arbres 1729.2 Arbres décisionnels et diagrammes de transition 1749.3 Arbres ordonnés 1779.4 Algorithmes de parcours des arbres ordonnés 178Questions de révision 180Réponses 181Exercices d’entraînement 183Solutions 183

Chapitre 10 Arbres binaires 18710.1 Terminologie 18710.2 Compter les arbres binaires 18810.3 Arbres binaires complets 18910.4 Identité, égalité et isomorphisme 19010.5 Arbres binaires parfaits 19210.6 Algorithmes de parcours des arbres binaires 19410.7 Arbres d’expression 19610.8 Classe BinaryTree 19810.9 Implémenter les algorithmes de parcours 203

Page 6: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

VI Structures de données en Java

10.10 Forêts 205Questions de révision 206Réponses 207Exercices d’entraînement 208Solutions 210

Chapitre 11 Arbres de recherche 21711.1 Arbres de recherche multidirectionnels 21711.2 Arbres équilibrés ou arbres-B 21911.3 Arbres binaires de recherche 22211.4 Caractéristiques des arbres binaires de recherche

en matière de performances 22311.4 Arbres AVL 22411.6 Classe AVLTree 225Questions de révision 227Réponses 228Exercices d’entraînement 228Solutions 229

Chapitre 12 Tas et files de priorité 23312.1 Tas 23312.2 Mappage naturel 23312.3 Insérer des éléments dans un tas 23412.4 Supprimer un élément d’un tas 23512.5 Classe PriorityQueue 23612.6 Interface Java Comparator 23712.7 Implémentation directe 239Questions de révision 243Réponses 243Exercices d’entraînement 244Solutions 245

Chapitre 13 Algorithmes de tri 25113.1 Méthode Java Arrays.sort() 25213.2 Tri par permutation 25213.3 Tri par sélection 25413.4 Tri par insertion 25513.5 Tri Shell 25613.6 Tri par fusion 25813.7 Tri par segmentation 26013.8 Tri vertical 26313.9 Rapidité des algorithmes de tri par comparaison 26813.10 Tri digital 26813.11 Tri panier 270

Page 7: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Sommaire VII

Questions de révision 272Réponses 274Exercices d’entraînement 275Solutions 277

Chapitre 14 Tables 28714.1 Interface Java Map 28714.2 Classe HashMap 28814.3 Codes de hachage Java 28914.4 Tables de hachage 29014.5 Performances des tables de hachage 29214.6 Algorithmes de résolution des collisions 29314.7 Chaînage séparé 29514.8 Applications 29614.9 Classe TreeMap 298Questions de révision 300Réponses 300Exercices d’entraînement 301Solutions 302

Chapitre 15 Ensembles 30515.1 Ensembles mathématiques 30515.2 Interface Java Set 30615.3 Classe Java AbstractSet 30615.4 Classe Java HashSet 30715.5 Classe Java TreeSet 309Questions de révision 311Réponses 311Exercices d’entraînement 311Solutions 312

Chapitre 16 Graphes 31316.1 Graphes simples 31316.2 Terminologie des graphes 31316.3 Chemins et cycles 31416.4 Graphes isomorphes 31616.5 Matrice d’adjacence d’un graphe 31816.6 Matrice d’incidence d’un graphe 31916.7 Liste d’adjacences d’un graphe 31916.8 Digraphes 32016.9 Chemins d’un digraphe 32116.10 Digraphes pondérés et graphes 32216.11 Chemins et cycles eulériens et hamiltoniens 32316.12 Algorithme de Dijkstra 324

Page 8: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

VIII Structures de données en Java

16.13 Algorithmes de parcours des graphes 328Questions de révision 332Réponses 332Exercices d’entraînement 333Solutions 338

Annexes A. Mathématiques de base 345

B. De C++ à Java 366

C. Environnements de développement Java 368

Index 371

Page 9: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Avant-propos

Comme tous les livres de notions fondamentales proposés dans la série Schaum’s, celui-ci est en premierlieu destiné aux personnes qui souhaitent étudier seules, de préférence en complément d’un cours sur lesstructures de données. Il constitue également une excellente référence pour les programmeurs Java plusexpérimentés.

Cet ouvrage comprend plus de 260 exemples et exercices d’entraînement. L’auteur est convaincu queles principes des structures de données peuvent être acquis plus aisément grâce à un large éventaild’exemples bien structurés et accompagnés d’explications précises. Ce livre est conçu de façon à appor-ter cette aide au lecteur.

Le code source des exemples et les exercices peuvent être téléchargés depuis le site web de l’auteurhttp://www.mathcs.richmond.edu/~hubbard/Books.html. Toutes les corrections et les addenda sont égale-ment disponibles sur ce site.

Je souhaiterais remercier tous mes amis, collègues, étudiants dont les critiques constructives m’ontété d’une aide précieuse. Je tiens tout particulièrement à remercier ma femme, Anita Hubbard, pour sesconseils, ses encouragements et les exemples créatifs qu’elle a apportés à ce livre. La plupart des idéesoriginales que vous y trouverez sont d’elle.

JOHN R. HUBBARD

Richmond, [email protected]

Page 10: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie
Page 11: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 1

Caractéristiques de basedu langage java

Ce chapitre est consacré aux fonctionnalités de base du langage de programmation Java nécessaires à laconception et l’implémentation des structures de données.

1.1 PROGRAMMATION ORIENTÉE OBJETJava est un langage de programmation orienté objet parce qu’il présente les caractéristiques suivantes :

• Chaque élément de donnée est encapsulé dans un objet.

• Chaque instruction exécutable est effectuée par un objet donné.

• Chaque objet est l’instanciation d’une classe ou est un tableau.

• Chaque classe est définie dans le cadre d’une hiérarchie d’héritage unique.

La hiérarchie d’héritage unique de Java est une structure arborescente dont la racine est la classeObject (reportez-vous à la section 3.4). Même si cette structure peut vous faire penser que Java est unlangage simple, ne vous fiez pas aux apparences car elle comprend les bibliothèques de classes Java 1.3qui sont composées de 2 130 classes et de 23 901 membres.

Ce sont ces caractéristiques orientées objet qui font de Java un langage particulièrement adapté à laconception et à l’implémentation des structures de données. L’architecture de ses collections (reportez-vous au chapitre 4) offre une plate-forme idéale de construction des structures de données.

1.2 LANGAGE DE PROGRAMMATION JAVAUn programme Java est une collection d’un ou de plusieurs fichiers texte. Au moins l’un de ces fichierscontient une classe publique unique avec une méthode unique dont la signature est la suivante :

public static void main(String[] args)

Chaque fichier du programme doit comprendre une classe ou bien une interface publique et estappelé X.java, X correspondant au nom de la classe ou de l’interface publique unique. Chaque fichierX.java est compilé dans un fichier classe appelé X.class. Le programme est ensuite lancé en exécu-tant le fichier X.class qui contient la méthode main(String[]) comprenant les instructions à exécuter.

Page 12: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

2 Caractéristiques de base du langage java

Cette programmation peut être effectuée à partir de la ligne de commande ou bien d’un environnementIDE (reportez-vous à l’annexe C).

1.3 VARIABLES ET OBJETSDans le cadre de tous les langages de programmation,l’accès aux données est effectué via les variables. Avec Java,une variable est soit une référence à un objet, soit l’un deshuit types primitifs, comme illustré dans le diagramme.

S’il s’agit d’une référence, sa valeur est null ou bienelle contient l’adresse d’un objet instancié.

Chaque objet appartient à une classe unique qui définitles propriétés et les opérations de ses objets de la mêmemanière qu’un type primitif définit les propriétés et les opé-rations de ses variables.

Une variable est créée par une déclaration spécifiant sontype et sa valeur initiale facultative. Un objet est créé parl’opérateur new qui appelle son constructeur de classe.

Exemple 1.1 Créer des variables et des objets

public class Ex0101 public static void main(String[] args) boolean flag=true; char ch=’B’; short m; int n=22; float x=3.14159F; double y; String str; String nada=null; String pays = new String("France"); System.out.println("flag = " + flag); System.out.println("ch = " + ch); System.out.println("n = " + n); System.out.println("x = " + x); System.out.println("nada = " + nada); System.out.println("pays = " + pays);

Ce programme déclare neuf variables, puis il imprime les six qui ont été initialisées. Les six premiè-res variables sont de type primitif et les trois dernières sont des références aux objets String.Il contient un seul objet qui est de type String et s’appelle pays (techniquement, pays est le nomde la variable faisant référence à l’objet String).

flag = truech = Bn = 22x = 3,14159nada = nullpays = France

Types Java

Types de référencestype Tableau

Types numériquesboolean

Types entiersbyte

double

type Interfacetype Classe

Types primitifs

Types à virgule flottantefloat

shortintlongchar

Page 13: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Types primitifs 3

La figure présentée contient les neuf variables et le seulobjet.Chacune de ces variables a un nom et un type. Si letype est une classe, la variable est une référence à uneinstance (c’est-à-dire à un objet) de cette classe. Lesvariables non initialisées sont vides, tandis que lesautres contiennent une valeur. Une variable de réfé-rence ne peut contenir qu’une seule valeur, c’est-à-direla référence dessinée sous forme d’un point noir. Sicette référence n’est pas null, le point est composéd’une flèche pointant vers l’objet auquel il fait réfé-rence. En Java, un objet ne peut pas exister si unevariable de référence ne pointe pas vers lui.

1.4 TYPES PRIMITIFSLe langage Java définit les huit types primitifs suivants :

• boolean false ou true• caractère Unicode 6 bits char, soit ’B’ ou ’π’• entier 8 bits byte : –128 à 127• entier 16 bits short : –32768 à 32767• entier 32 bits int : –2147483648 à 2147483647• entier 64 bits long : –9223372036854775808 à 9223372036854775807• nombre décimal à virgule flottante 32 bits float : (plus ou moins) 1.4E-45F à 3.4E38F• nombre décimal à virgule flottante 64 bits double : (plus ou moins) 4.9E-324 à 1.8E308

Remarquez que les littéraux de type float doivent être précédés du suffixe F qui les différencie desvaleurs de type double. Chaque littéral Unicode peut être exprimé sous la forme '\uxxxx', x corres-pondant à un chiffre hexadécimal. Par exemple, 'B' correspond à '\u0042' et 'π' à '\u03C0'.

Outre leur valeur numérique, les variables à virgule flottante peuvent également contenir l’une destrois valeurs spéciales suivantes : NEGATIVE_INFINITY, POSITIVE_INFINITY et NaN (Not a Number,c’est-à-dire « n’est pas un nombre »).

Ces valeurs spéciales sont obtenues à la suite de mauvaises opérations arithmétiques.

Exemple 1.2 Valeurs spéciales des types à virgule flottante

public class Ex0102 public static void main(String[] args) double x=1E200; // x=10000...0 (1 suivi de 200 zéros) System.out.println("x = " + x); System.out.println("x*x = " + x*x); System.out.println("(x*x)/x = " + (x*x)/x); System.out.println("(x*x)/(x*x) = " + (x*x)/(x*x));

x = 1.0E200x*x = Infinity(x*x)/x = Infinity(x*x)/(x*x) = NaN

flagboolean

chchar

mshort

nint

xfloat

ydouble

str nada

pays

true "B"

22

3.14159

String

String

String

"France"

Page 14: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

4 Caractéristiques de base du langage java

La valeur de x*x est Infinity parce que (10200)(10200) == 10400, soit une valeur supérieure aumaximum de 1.3(10308) pour le type double.D’un point de vue algébrique, (xx)/x = x, sauf lorsque x est fini. En effet, Infinity divisé par unevaleur finie non négative sera toujours Infinity.En dernier lieu, la valeur Infinity divisée par Infinity ne produit pas de valeur finie, mais unevaleur indéterminée sur le plan algébrique, d’où la valeur Java NaN.

Pour chacun des huit types primitifs, Java définit une classe enveloppe qui fournit les services orien-tés objets nécessaires à la manipulation des types primitifs. Par exemple, la classe enveloppe du typedouble est Double, et celle du type int est Integer.

Exemple 1.3 Utiliser une classe enveloppe

public class Ex0103 public static void main(String[] args) String s = "2.7182818284590"; System.out.println("s = " + s); Double x = new Double(s); System.out.println("x = " + x); double y = x.doubleValue(); System.out.println("y = " + y); s += 45; System.out.println("s = " + s); y += 45; System.out.println("y = " + y); int n = x.intValue(); System.out.println("n = " + n); n = Integer.parseInt("3A9",16); System.out.println("n = " + n);

Le constructeur Double(s) crée l’objet x de type Double à partir de l’objet s de type String etil stocke la valeur numérique 2.7182818284590 dans l’objet x. La deuxième instruction println()appelle implicitement la méthode Double.toString() qui reconvertit cette valeur numérique enchaîne à imprimer. L’appel x.doubleValue() renvoie la valeur numérique stockée affectée à y.L’opérateur += est ensuite appliqué à l’objet s de type String et à la variable y de type double,créant ainsi des résultats complètement différents. Dans le cas des chaînes, += signifie « insérer »alors qu’il signifie « additionner » lorsqu’il s’agit de numéros.Puis, la méthode Double.intValue() tronque la valeur numérique stockée de l’entier 2 et l’uti-lise pour initialiser n. En dernier lieu, Integer.parseInt("3A9",16) renvoie la valeur int (auformat décimal) pour l’entier dont la représentation hexadécimale est 3A9, soit 3(162) + 10(16) + 9= 937.Remarquez que l’opérateur + convertit automatiquement le nombre y en son équivalent de typechaîne lorsqu’il est combiné à une chaîne, comme c’est le cas pour l’appel System.out.println("y = " + y).

s = 2.7182818284590x = 2.718281828459y = 2.718281828459s = 2.718281828459045y = 47.718281828459n = 2n = 937

Page 15: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Contrôle du flux 5

1.5 CONTRÔLE DU FLUXLe langage Java supporte les instructions if, if..else et switch, ainsi que l’opérateur d’expressionconditionnelle ?..:.

Exemple 1.4 Utiliser les instructions conditionnelles

Le programme suivant génère des entiers aléatoires dans un intervalle de 0 à 99, puis il utilise les ins-tructions conditionnelles afin de les classer :

public class Ex0104 public static void main(String[] args) int n = (int)Math.round(100*Math.random()); System.out.println("n = " + n); if (n>25 && n<75) System.out.println(n + " est compris entre 25 et 75"); else System.out.println(n + " n’est pas compris entre 25 et 75"); switch ((int)n/20) case 0: System.out.println(n + " < 20"); case 1: System.out.println(n + " < 40"); case 2: System.out.println(n + " < 60"); break; case 3: System.out.println(n + " < 80"); break; default: System.out.println(n + " >= 80"); System.out.println(n + (n%2>0 ? " est impair" : " est pair"));

Remarquez que l’absence d’instruction break entre case 0 et case 1 permet au contrôle de tra-verser chacun de ces cas jusqu’à la case 2 et d’exécuter les trois instructions println().L’expression (n%2>0 ? " est impair" : " est pair") est évaluée à la chaîne " estimpair" si la condition n%2>0 est vraie (c’est-à-dire si n n’est pas divisible par 2). Dans le cascontraire, elle est évaluée à la chaîne " est pair" (lorsque n est divisible par 2).Java supporte les instructions while, do..while et for.

Exemple 1.5 Utiliser les boucles

Le programme teste empiriquement le théorème des nombres premiers de Gauss : si p(n) est le nom-bre de nombres premiers inférieurs à n, le rapport p(n)(ln n)/n se rapproche de 1.0 lorsque n devientillimité. Il compte le nombre p(n) de nombres premiers pour chaque n impair situé dans l’intervalle3 à 1 000 000. Au fur et à mesure de l’augmentation de p(n), chaque fois qu’un multiple de 5 000 estpassé, le programme imprime les valeurs de n, p(n), lnn (le logarithme naturel) et le rapport p(n)(lnn)/n, soit un nombre proche de 1.0.

public class Ex0105 public static void main(String[] args) System.out.println("n\tp(n)\tln(n)\t\t\tp(n)*ln(n)/n"); final String DASHES18="\t------------------"; System.out.println("------\t-----" + DASHES18 + DASHES18);

n = 1919 n’est pas compris entre 25 et 7519 < 2019 < 4019 < 6019 est impair

Page 16: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

6 Caractéristiques de base du langage java

int p=1; // p = nombre de nombres premiers qui sont <= n for (int n=3; n<1000000; n += 2) int d=3; while (d<=Math.sqrt(n) && n%d>0) d += 2; if (n%d==0) continue; ++p; if (p%5000>0) continue; double ln=Math.log(n); System.out.println(n + "\t" + p + "\t" + ln + "\t" + p*ln/n); System.out.println("------\t-----" + DASHES18 + DASHES18);

La boucle for est itérée 499 999 fois, soit pour chaque valeur de n = 3, 5, 7, 9, …, 999999. Le blocde la boucle teste si n est un nombre premier en le divisant par d = 3, 5, 7, 9, etc. S’il trouve un nom-bre d qui divise n de façon à ce que le reste n%d soit égal à zéro, la première instruction continueest exécutée, en commençant par l’itération suivante de la boucle for.En revanche, si aucun diviseur de n n’est trouvé, cela signifie qu’il s’agit d’un nombre premier et lecompteur p est alors incrémenté.

Java supporte l’utilisation de sous-programmes appelés méthodes.

Exemple 1.6 Utiliser des méthodes

Le programme suivant crée la même sortie que celui de l’exemple précédent même s’il utilise uneméthode séparée pour déterminer si chaque entier n est un nombre premier.

public class Ex0106 public static void main(String[] args) System.out.println("n\tp(n)\tln(n)\t\t\tp(n)*ln(n)/n"); final String DASHES18="\t------------------"; System.out.println("------\t-----" + DASHES18 + DASHES18); int p=1; // p = nombre de nombres premiers <= n for (int n=3; n<1000000; n += 2) if (isPrime(n)) ++p; if (p%5000>0) continue;

n c(n) ln(n) c(n)*ln(n)/n------ ------ ------------------ --------------------48619 5000 10.79176967999097 1.109830486023054104743 10000 11.559265009775785 1.1035835339617717163847 15000 12.006688344529985 1.0991981859170432224743 20000 12.322712806131365 1.0966048158235286287137 25000 12.567714732761953 1.0942263390613152350381 30000 12.766776412829921 1.093105198012728414991 35000 12.936012112230687 1.091012633835611479939 40000 13.08141429147497 1.0902564110418174545749 45000 13.209914442069696 1.0892299388420983611957 50000 13.324417297588104 1.0886726761511107679279 55000 13.428787220525182 1.087304770394617746777 60000 13.52352189210366 1.0865510232990836814309 65000 13.610095179831822 1.0863888114819662882389 70000 13.690388280841916 1.0860597533048737951193 75000 13.765472265206315 1.085384795609801------ ------ ------------------- ------------------

Page 17: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classes 7

double ln=Math.log(n); System.out.println(n + "\t" + p + "\t" + ln + "\t" + p*ln/n); System.out.println("------\t-----" + DASHES18 + DASHES18);

private static boolean isPrime(int n) int d=3; while (d<=Math.sqrt(n) && n%d>0) d += 2; if (n%d==0) return false; return true;

La méthode isPrime(s) encapsule le code qui teste si n est un nombre premier. Elle renvoiefalse si un diviseur d de n est trouvé, et true dans le cas contraire.Remarquez que cette méthode est déclarée comme étant privée et statique. Elle doit être privée parcequ’elle n’est pas destinée à être utilisée hors de sa classe, et statique parce qu’elle doit être appeléedepuis la méthode statique main() (reportez-vous à la section 1.7).

1.6 CLASSESUne classe est une implémentation d’un type de données abstrait. Les objets sont créés grâce à l’instan-ciation des classes. La définition d’une classe spécifie le type de données géré par ses objets et les opéra-tions que ces derniers peuvent effectuer. Ces spécifications correspondent aux membres de la classe.

Comme illustré dans la figure, quatre types de membres peuvent com-poser une classe.

Un champ est une variable qui contient des données. Une méthode estune fonction qui effectue des opérations. À l’intérieur d’elle-même, uneclasse peut également définir des classes et des interfaces locales internes.

En outre, la figure précédente contient quatre types courants de métho-des spécialisées : les constructeurs, les accesseurs, les mutateurs et les uti-litaires. Un constructeur est une fonction qui crée les objets de la classe. Ceprocessus est qualifié d’instanciation et les objets obtenus d’instances de laclasse. Le constructeur est également chargé d’initialiser les champs desobjets qu’il crée.

Un accesseur est une fonction en lecture seule qui peut renvoyer la valeur d’un champ de classe sanspermettre sa modification externe. Dans le cadre de Java, un accesseur qui accède à un champ X est géné-ralement appelé getX(). C’est la raison pour laquelle les accesseurs sont souvent qualifiés de getters.

Un mutateur est en lecture-écriture et permet aux appeleurs externes de modifier la valeur d’unchamp. Dans le cadre de Java, un mutateur qui modifie un champ X est généralement appelé setX().C’est la raison pour laquelle les mutateurs sont souvent qualifiés de setters.

Un utilitaire est une fonction privée utilisée de façon interne par d’autres méthodes de la classe.

Exemple 1.7 Classe Point

Le programme suivant définit une classe dont les instances représentent des points d’un plan eucli-dien :

public class Point protected double x, y;

public Point(double x, double y)

membrechampméthodeet

constructeurns r

mutateurtaccesseurce

interfaceclasse

utilitairet

Page 18: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

8 Caractéristiques de base du langage java

this.x = x; this.y = y;

public double getX() return x; public double getY() return y;

public Point getLocation() return new Point(x,y);

public void setLocation(double x, double y) this.x = x; this.y = y;

public void translate(double dx, double dy) x += dx; y += dy;

public boolean equals(Object object) if (object == this) return true; if (object.getClass() != this.getClass()) return false; Point point = (Point)object; return (x == point.x && y == point.y);

public int hashCode() return (new Double(x)).hashCode() + (new Double(y)).hashCode();

public String toString() return new String("(" + (float)x + "," + (float)y + ")");

Cette classe a trois champs (x, y et origin), un constructeur et cinq accesseurs (getX(), getY(),getLocation(), hashCode() et toString()), et un mutateur (translate(double,double)).Le pilote test de cette classe est le suivant :

public class Ex0107 public static void main(String[] args) Point p = new Point(2,3); System.out.println("p = " + p); System.out.println("p.hashCode() = " + p.hashCode()); Point q = p.getLocation(); compare(p,q); q.translate(5,-1); compare(p,q); q = p; compare(p,q); private static void compare(Point p, Point q) System.out.println("q = " + q);

Page 19: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Modificateurs 9

System.out.println("q.hashCode() = " + q.hashCode()); if (q.equals(p)) System.out.println("q est égal à p"); else System.out.println("q n’est pas égal à p"); if (q == p) System.out.println("q == p"); else System.out.println("q != p");

Ce pilote test comprend la méthode utilitaire compare(Point,Point) qui facilite la vérification.Le point p a la valeur hashCode() –2 146 959 360.Ce nombre n’a aucune signification intrinsèque, ilest simplement utilisé comme numéro d’identifica-tion et est calculé à partir des valeurs hashcode()correspondantes des deux objets de coordonnées dupoint (x et y).Le point q est défini comme la copie de p. Il a doncla même valeur hashCode() et la méthodeequals(Object) renvoie true. Cependant, le testq == p est évalué à false parce que p et q sont desobjets différents. En fait, l’opérateur d’égalité ==teste plus l’identité que l’égalité, ce qui est vrai uni-quement lorsque vous avez deux références différen-tes au même objet.Une fois que q est traduit à l’emplacement (7,2), laméthode equals(Object) renvoie false.En dernier lieu, lorsque la référence p est affectée àla référence q, l’égalité q == p est évaluée à trueparce qu’il ne reste plus qu’un seul objet Point, soitle point d’origine (2,3). L’autre point (7,2) a été récu-péré par le ramasse-miettes, c’est-à-dire qu’il a étédétruit au moment où il a perdu sa variable de référence.

1.7 MODIFICATEURSLes classes, les interfaces et leurs membres peuvent être déclarés à l’aide des modificateurs public,protected, private, package, abstract, static et final. Le tableau suivant résume leur signi-fication.

p = (2.0,3.0)p.hashCode() = -2146959360q = (2.0,3.0)q.hashCode() = -2146959360q est égal à pq != pq = (7.0,2.0)q.hashCode() = -2145648640q n’est pas égal à pq != pq = (2.0,3.0)q.hashCode() = -2146959360q est égal à pq == p

xdouble

p 2.0Point

ydouble

3.0

xdouble

q 2.0Point

ydouble

3.0

xdouble

p 2.0Point

ydouble

3.0

qPoint

xdouble

q 7.0Point

ydouble

2.0

Page 20: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

10 Caractéristiques de base du langage java

Les modificateurs public, protected et private sont qualifiés de modificateurs d’accès parcequ’ils déterminent l’emplacement à partir duquel il est possible d’accéder à la classe ou au membre.Généralement, public signifie accessible depuis n’importe quel emplacement, protected signifieaccessible uniquement depuis la classe et ses sous-classes et private signifie accessible uniquementdepuis la classe elle-même.

Un membre est déclaré comme abstract lorsqu’il est incomplet. C’est pourquoi une méthodeabstract est une méthode dont l’implémentation n’est pas incluse. Une classe abstract comporte aumoins une méthode abstract. Les interfaces sont abstract par défaut, c’est pourquoi elles n’utili-sent pas de modificateur. Les champs ne peuvent pas être abstract.

Une classe final ne peut pas être divisée en sous-classes (reportez-vous à la section 3.1). Unchamp final est simplement une constante. Une méthode final ne peut pas être remplacée dans unesous-classe.

Un champ static appartient à la classe elle-même ; il ne génère pas de copie séparée de chacunede ses instances. De la même façon, une méthode static est liée à la classe et non à ses objets. Si X estune classe avec un champ statique x et une méthode statique y(), X.x et X.y() permettent d’y accéderet sont indépendants des objets. La classe java.lang.Math (reportez-vous à la section 1.9) illustrel’utilisation des méthodes static.

1.8 CLASSE StringUne chaîne (string) est un objet qui contient une séquence de caractères généralement utilisée pourtraiter du texte.

Java fournit une classe String qui permet la création et le traitement des chaînes. Cette classecontient plus de 50 méthodes, dont plus de 10 constructeurs. Vous trouverez les méthodes les plus cou-ramment utilisées dans la définition de classe suivante.

Bien que les chaînes soient des objets et non des types primitifs, elles sont similaires à ces dernierssur certains points. Le système reconnaît les littéraux de chaînes tels que "bleu" et "automne", de lamême façon qu’il reconnaît des littéraux numériques tels que 8388608 et 3,14159. En outre, les réfé-rences de chaîne peuvent être affectées à ces littéraux de la façon suivante :

String couleur="bleu";

Modificateur Interface Classe Classe imbriquée Champ Méthode

public Accessible depuis n’importe quelle classe.

protected Accessible uniquement depuis cette classe et ses sous-classes.

private Accessible uniquement depuis cette classe.

abstract Sans objetContient au moins une méthode abstract.

Sans objet

Son implémentation n’est pas définie ; seuls sa signature et son type de renvoi sont déclarés.

final Sans objetNe peut pas être décomposée en sous-classes.

Sa valeur ne peut pas être modifiée.

Ne peut pas être remplacée par une sous-classe.

static Sans objet Sans objetN’est pas une classe interne.

Une seule instance existe pour tous les objets de la classe.

Une seule instance existe pour tous les objets de la classe.

Page 21: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe String 11

Étant donné que les littéraux sont uniques, toute autre référence à "bleu" sera évaluée comme égaleà la référence couleur.

À l’instar des types numériques, les chaînes sont les seules classes d’objets susceptibles d’être mani-pulées par des opérateurs (reportez-vous à l’exemple 1.8).

public final class String public char charAt() public boolean endsWith(String suffix) public boolean equals(Object object) public int indexOf(char ch) public int indexOf(char ch, int start) public int indexOf(String str) public int indexOf(String str, int start) public int lastIndexOf(char ch) public int lastIndexOf(char ch, int start) public int lastIndexOf(String str) public int lastIndexOf(String str, int start) public int length() public String replace(char ch, char ch2) public boolean startsWith(String prefix) public boolean startsWith(String prefix, int start) public String() public String(char[] chars) public String(char[] chars, int start, int len) public String substring(int start) public String substring(int start, int stop) public char[] toCharArray() public String toLowerCase() public String toUpperCase() public String trim()

Par ailleurs, la classe String définit les deux opérateurs de concaténation + et += illustrés dansl’exemple suivant.

Exemple 1.8 Tester la classe String

public class Ex0108 public static void main(String[] args) String s="ABCDEFG"; System.out.println("s = \"" + s + "\""); s = s + "HIJK"; System.out.println("s = \"" + s + "\""); s += "LMNOP"; System.out.println("s = \"" + s + "\""); System.out.println("s.length() = " + s.length()); System.out.println("s.charAt(6) = " + s.charAt(6)); System.out.println("s.indexOf(’G’) = " + s.indexOf(’G’)); System.out.println("s.indexOf(’Z’) = " + s.indexOf(’Z’)); System.out.println("s.indexOf(’G’,8) = " + s.indexOf(’G’,8)); System.out.println("s.indexOf(\"GHIJ\") = " + s.indexOf("GHIJ")); if (s.startsWith("DE")) System.out.println("s.startsWith(\"DE\")"); else System.out.println("s ne commence pas par \"DE\""); if (s.startsWith("DE",3)) System.out.println("s.startsWith(\"DE\",3)"); else System.out.println("s ne commence pas par \"DE\" après 3 chars");

Page 22: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

12 Caractéristiques de base du langage java

if (s.endsWith("IJK")) System.out.println("s.endsWith(\"IJK\")"); else System.out.println("s ne finit pas par \"IJK\""); if (s.endsWith("NOP")) System.out.println("s.endsWith(\"NOP\")"); else System.out.println("s ne finit pas par \"NOP\""); s += "DABBADABBADO"; System.out.println("s = \"" + s + "\""); s = s.replace(’B’,’T’); System.out.println("s = \"" + s + "\""); s = s.substring(7,10); System.out.println("s = \"" + s + "\""); s = s.toLowerCase(); System.out.println("s = \"" + s + "\""); s = " W XY Z "; System.out.println("s = \"" + s + "\""); System.out.println("s.length() = " + s.length()); s = s.trim(); System.out.println("s = \"" + s + "\""); System.out.println("s.length() = " + s.length());

Remarquez que les objets String sont immuables. C’est pourquoi la seule méthode de modificationd’une chaîne consiste à lui affecter la valeur de renvoi d’une méthode, par exemple replace(char,char) qui permet de renvoyer une nouvelle chaîne.

Avec Java, le système d’exploitation de l’ordinateur doit gérer un pool de littéraux de chaînes ets’assurer qu’il n’existe qu’une seule occurrence de tout littéral de chaîne utilisé, quel que soit l’environ-nement d’exécution. Ce procédé est similaire à celui qui s’applique aux littéraux numériques, à savoirqu’il n’existe qu’un seul nombre 27. C’est la raison pour laquelle l’opérateur d’égalité == fonctionnetoujours « correctement » pour les chaînes.

Exemple 1.9 Tester si les littéraux String sont uniques

public class Ex0109 public static void main(String[] args)

s = "ABCDEFGHIJK"s = "ABCDEFGHIJKLMNOP"s.length() = 16s.charAt(6) = Gs.indexOf(’G’) = 6s.indexOf(’Z’) = -1s.indexOf(’G’,8) = -1s.indexOf("GHIJ") = 6s ne commence pas par "DE"s.startsWith("DE",3)s ne finit pas par "IJK"s.endsWith("NOP")s = "ABCDEFGHIJKLMNOPDABBADABBADO"s = "ATCDEFGHIJKLMNOPDATTADATTADO"s = "HIJ"s = "hij"s = " W XY Z "s.length() = 12s = "W XY Z"s.length() = 7

Page 23: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Math 13

String s1="ABCDEFG"; System.out.println("s1 = \"" + s1 + "\""); System.out.println("(s1 == \"ABCDEFG\") = " + (s1 == "ABCDEFG")); System.out.println("(s1 == \"ABCD\"+\"EFG\") = " + (s1 == "ABCD"+"EFG")); String s2="ABCDEFG"; // fait de s2 un synonyme de s1 System.out.println("s2 = \"" + s2 + "\""); System.out.println("(s1 == s2) = " + (s1 == s2)); s2 = new String("ABCDEFG"); // s2 fait maintenant référence à // un objet séparé System.out.println("s2 = \"" + s2 + "\""); System.out.println("(s1 == s2) = " + (s1 == s2)); System.out.println("s1.equals(s2) = " + s1.equals(s2));

Dans ce programme, vous pouvez constater qu’il n’existe qu’un seul littéral de chaîne "ABCDEFG"quel que soit son format. C’est pourquoi si deux références différentes (c’est-à-dire des référencesavec des noms différents) sont affectées au littéral, elles doivent être égales, c’est-à-dire que l’opéra-teur d’égalité == est évalué à true. Remarquez cependant que deux chaînes différentes peuventavoir la même valeur de littéral. Le cas échéant, l’opérateur d’égalité est évalué à false, mais laméthode equals() renvoie tout de même true. Comparez ce résultat à celui de l’exemple 1.7.

Les propriétés spéciales suivantes permettent de différencier la classe String de toutes les autresclasses :

• Les objets String sont immuables (en lecture seule) ; leur valeur ne peut pas être modifiée.• Les littéraux String sont gérés dans un pool de chaînes par le système d’exploitation.• La classe String définit des opérateurs spéciaux + et +=.• Il est possible d’accéder à la longueur grâce à la méthode length() au lieu du champ length,

comme dans le cas des tableaux, ou bien grâce à la méthode size() utilisée par les objets Collection.

1.9 CLASSE MathLa classe Java Math définit les constantes et les méthodes mathématiques qui implémentent les fonctionsmathématiques courantes. Sa définition dans le paquetage java.util est la suivante :

public final class Math public static final double E=2.7182818284590452354; public static final double PI=3.14159265358979323846; public static double abs(double x) // valeur absolue public static native double atan(double x) // arctangent public static native double ceil(double x) // plafond public static native double cos(double x) public static native double exp(double x) // base e

s1 = "ABCDEFG"(s1 == "ABCDEFG") = true(s1 == "ABCD"+"EFG") = trues2 = "ABCDEFG"(s1 == s2) = trues2 = "ABCDEFG"(s1 == s2) = falses1.equals(s2) = true

Page 24: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

14 Caractéristiques de base du langage java

public static native double floor(double x) public static native double log(double x) // base e public static native double max(double x, double y) public static native double min(double x, double y) public static native double pow(double x, double y) // puissance public static synchronized double random() public static long round(double x) public static native double sin(double x) public static native double sqrt(double x) // racine carrée public static native double tan(double x)

Remarquez que tous ces membres sont static. Cela signifie qu’ils sont appelés à l’aide du préfixeMath au lieu de l’objet Math.

Exemple 1.10 Tester la classe Math

public class Ex0110 public static void main(String[] args) final double PI=Math.PI; final double E=Math.E; System.out.println("E = " + E); System.out.println("Math.exp(1.0) = " + Math.exp(1.0)); System.out.println("PI = " + PI); System.out.println("4*Math.atan(1.0) = " + 4*Math.atan(1.0)); System.out.println("Math.cos(2*PI) = " + Math.cos(2*PI)); System.out.println("Math.sin(PI/2) = " + Math.sin(PI/2)); System.out.println("Math.tan(PI/4) = " + Math.tan(PI/4)); System.out.println("Math.log(E) = " + Math.log(E)); System.out.println("Math.abs(-13.579) = " + Math.abs (-1.579)); System.out.println("Math.floor(13.579) = " + Math.floor(13.579)); System.out.println("Math.ceil(13.579) = " + Math.ceil(13.579)); System.out.println("Math.round(13.579) = " + Math.round(13.579)); System.out.println("Math.pow(25.0,0.5) = " + Math.pow(25.0,0.5)); System.out.println("Math.sqrt(25.0) = " + Math.sqrt(25.0)); System.out.println("Math.random() = " + Math.random()); System.out.println("Math.random() = " + Math.random());

Notez l’erreur d’arrondi dans le calcul de Math.tan(PI/4) ; la valeur correcte serait très exacte-ment 1.0.

E = 2.718281828459045Math.exp(1.0) = 2.7182818284590455PI = 3.1415926535897934*Math.atan(1.0) = 3.141592653589793Math.cos(2*PI) = 1.0Math.sin(PI/2) = 1.0Math.tan(PI/4) = 0.9999999999999999Math.log(E) = 1.0Math.abs(-13.579) = 13.579Math.floor(13.579) = 13.0Math.ceil(13.579) = 14.0Math.round(13.579) = 14Math.pow(25.0,0.5) = 5.0Math.sqrt(25.0) = 5.0Math.random() = 0.9279776738566742Math.random() = 0.4493770111566855

Page 25: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Math 15

Notez également que la méthode Math.round(double) renvoie un entier long au lieu d’un nom-bre décimal à virgule flottante double comme toutes les autres méthodes Math.

La méthode Math.random() renvoie des nombres décimaux à virgule flottante de type doublegénérés de façon aléatoire et distribués uniformément dans un intervalle de 0.0 à 1.0. L’exemple suivantpermet de tester cette distribution.

Exemple 1.11 Tester la méthode Math.random()

Ce programme génère 50 000 nombres aléatoires dans un intervalle de 0.0 à 1.0, puis il compte com-bien d’entre eux entrent dans les 100 sous-intervalles divisés de façon égale selon une longueur de0.01. Il utilise un tableau frequency[] pour cumuler ces comptes. Par exemple, le nombre aléa-toire x= 0.7294416115902632 est situé dans l’intervalle 0.72 x < 0.73, c’est pourquoi il est comptéen incrémentant frequency[72].

public class Ex0111 public static void main(String[] args) final int SUBINTERVALS=100; final int TOTAL=50000; int[] frequency = new int[SUBINTERVALS]; for (int k=0; k<SUBINTERVALS; k++) frequency[k] = 0; for (int j=0; j<TOTAL; j++) double x = Math.random(); // 0.0 < x < 1.0 x *= SUBINTERVALS; // 0.0 < x < 100.0 x = Math.floor(x); // 0.0 <= x <= 99.0 long m = Math.round(x); // 0 <= m <= 99 int k = (int)m; // 0 <= k <= 99 ++frequency[k]; // compter k for (int i=0; i<SUBINTERVALS; i++) System.out.print(frequency[i]+(i%10==9?"\n":" "));

Si les 50 000 nombres étaient distribués uniformément et exactement, nous devrions en avoir 500dans chaque sous-intervalle. Étant donné que ces 100 comptes de fréquence sont proches de 500,nous obtenons la preuve empirique que la méthode Math.random() crée effectivement une distri-bution uniforme.

Exemple 1.12 Tester la méthode Math.sqrt()

Ce programme teste la méthode Math.sqrt() en comparant ses résultats à une méthode de racinecarrée définie localement et en mettant au carré sa sortie.

public class Ex0112 public static void main(String[] args)

515 506 515 454 495 512 502 542 506 514475 529 483 504 476 496 471 524 508 489501 499 477 498 500 498 517 482 438 492491 480 566 487 512 485 489 461 524 504500 527 482 506 498 475 519 517 466 509473 503 497 483 522 468 505 510 488 532499 533 499 488 511 468 506 476 510 498552 504 507 478 512 521 484 479 480 487514 489 511 512 464 487 499 506 497 526511 501 522 468 524 505 508 533 530 504

Page 26: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

16 Caractéristiques de base du langage java

for (double x=50.0; x<60.0; x++) double y=Math.sqrt(x); double z=sqrt(x); System.out.println(y + "\t" + y*y); System.out.println(z);

private static double sqrt(double x) final double EPSILON=1E-14; if (x <= 0) return 0.0; double y=1.0; while (Math.abs(y*y-x) > EPSILON) y = (y+x/y)/2; return y;

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

1.1 Pourquoi le langage de programmation Java est-il qualifié d’orienté objet ?

1.2 Quels sont les éléments nécessaires à l’écriture d’un programme Java ?

1.3 Qu’est-ce qu’un type primitif ?

1.4 Qu’est-ce qu’un littéral ?

1.5 Quels sont les trois littéraux non numériques des deux types à virgule flottante ?

1.6 Qu’est-ce qu’une référence d’objet ?

1.7 Qu’est-ce qu’un objet ?

1.8 Qu’est-ce qu’une classe ?

1.9 Qu’est-ce qu’une instance ?

7.0710678118654755 50.000000000000017.07106781186547557.14142842854285 51.000000000000017.141428428542857.211102550927978 51.999999999999997.2111025509279797.280109889280518 53.07.2801098892805187.3484692283495345 54.07.34846922834953457.416198487095663 55.07.4161984870956637.483314773547883 56.07.4833147735478837.54983443527075 57.07.549834435270757.615773105863909 58.000000000000017.6157731058639097.681145747868608 58.999999999999997.681145747868609

Page 27: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 17

1.10 Qu’est-ce qu’un champ ?

1.11 Qu’est-ce qu’une méthode ?

1.12 Qu’est-ce que la signature d’une méthode ?

1.13 Qu’est-ce qu’un constructeur ?

1.14 Qu’est-ce qu’un membre static ?

1.15 Qu’est-ce qu’un modificateur d’accès ?

1.16 Que signifie private ?

1.17 Que signifie protected ?

1.18 Qu’est-ce qu’une classe enveloppe ?

1.19 Qu’est-ce qui différencie la classe String de toutes les autres classes de la bibliothèque Javastandard ?

1.20 À quoi sert la méthode Math.random() ?

RÉPONSES¿RÉPONSES

1.1 Java est qualifié de langage de programmation orienté objet parce que toutes les données et lesopérations sont encapsulées dans des objets. En outre, les classes des objets sont définies dansune hiérarchie d’héritage unique.

1.2 Un programme Java doit contenir une classe public définie dans un fichier appelé X.java, Xcorrespondant au nom de la classe. Cette classe doit comprendre une méthode déclarée de lafaçon suivante :

• public static void main(String[] args)

1.3 Les types primitifs sont les suivants : boolean, char, byte, short, int, long, float oudouble.

1.4 Un littéral est une constante anonyme ; un symbole qui représente une valeur constante du type.Par exemple, 4, 3.14 et "France" sont des littéraux.

1.5 Les trois littéraux non numériques des deux types à virgule flottante sont NEGATIVE_INFI-NITY, POSITIVE_INFINITY et NaN.

1.6 Une référence d’objet est une variable dont la valeur est null ou correspond à l’adresse d’unobjet.

1.7 Un objet est un bloc contigu de stockage mémoire typé par une classe et auquel vous accédez viaune variable de référence pour cette classe. Il peut être composé de plusieurs classes qui contien-nent des données et peut avoir des méthodes qui effectuent des opérations.

1.8 Une classe est une méthodologie de création d’objets. La définition d’une classe spécifie leschamps et les méthodes de chacune de ses instances.

1.9 Une instance de classe est un objet du type de cette classe. Le processus de création d’une ins-tance est qualifié d’instanciation de la classe.

1.10 Un champ est une variable membre d’une classe. Lorsque la classe est instanciée, les champsobtenus pour l’objet contiennent les données de ce dernier.

1.11 Une méthode est une fonction membre d’une classe. Lorsque cette dernière est instanciée, lesméthodes de l’objet obtenu effectuent certaines opérations pour ce dernier.

Page 28: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

18 Caractéristiques de base du langage java

1.12 La signature d’une méthode est la partie de sa définition nécessaire à la compilation des instruc-tions qui l’appellent. Par exemple, la signature de la méthode main() est la suivante :

• main(String[])

1.13 Un constructeur est une méthode qui instancie sa classe. Il porte le même nom que la classe elle-même et n’a aucun type de renvoi. Il est appelé par l’opérateur new.

1.14 Un membre static s’applique à toute la classe et non à une instance. Il est appelé à l’aide dunom de la classe au lieu de celui de l’instance. Par exemple, la méthode sqrt(double) de laclasse Math est statique. Elle est appelée sous la forme Math.sqrt(). Un champ static n’aqu’une valeur de donnée quel que soit le nombre d’objets existant dans la classe, même s’il n’yen a aucun.

1.15 Un modificateur d’accès est l’un des trois mots-clés Java public, protected ou private quipermettent de modifier une classe, un champ, une méthode ou une interface lorsque ces élémentssont définis.

1.16 Le modificateur d’accès private signifie que l’entité définie sera accessible uniquement à par-tir de la classe dans laquelle elle est définie.

1.17 Le modificateur d’accès protected signifie que l’entité définie sera accessible uniquementdepuis la classe dans laquelle elle est définie ou depuis les sous-classes qui en découlent.

1.18 Une classe enveloppe a pour but de fournir des constantes et des méthodes destinées au traite-ment de l’un des huit types primitifs. Par exemple, la classe Double est une classe enveloppe dutype primitif double.

1.19 La classe String se distingue des autres classes parce que ses instances sont immuables (enlecture seule), que ses littéraux sont uniques et qu’elle a deux opérateurs + et += destinés à laconcaténation.

1.20 La méthode Math.random() renvoie des nombres aléatoires à virgule flottante de type doublequi sont distribués uniformément dans un intervalle de 0.0 à 1.0.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

1.1 Écrivez et testez la méthode suivante :

• public static String monthName(int month)• // condition préalable : 0 < n et n < 13• // exemple : monthName(3) renvoie "Mars"

1.2 Écrivez et testez la méthode suivante :

• public static int daysInMonth(int month, int year)• // condition préalable : 0 < n et n < 13• // exemple : daysInMonth(2,2000) renvoie 29

1.3 Écrivez et testez la méthode suivante :

• public static int numberOfDigits(int n)• // exemple : numDigits(-8036800) renvoie 7

1.4 Écrivez et testez la méthode suivante :

• public static int sumOfDigits(int n)• // exemple : sumOfDigits(-8036800) renvoie 25

Page 29: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 19

1.5 Écrivez et testez la méthode suivante :

• public static int reverseDigits(int n)• // exemple : reverseDigits(-8036800) renvoie 572038

1.6 Écrivez et testez la méthode suivante :

• public static double round(double x, int precision)• // exemple : round(803.505692,4) renvoie 803.1057

1.7 Écrivez et testez la méthode suivante :

• public static String signedToBinary(int n)• // examples : signedToBinary( 1289) renvoie "010100001001"• // signedToBinary(-1289) renvoie "101011110111"

1.8 Écrivez et testez la méthode suivante :

• public static String unsignedToBinary(int n)• // condition préalable : n >= 0• // exemple : unsignedToBinary(1289) renvoie "10100001001"

1.9 Écrivez et testez la méthode suivante :

• public static int binaryToSigned(String code)• // conditions préalables : chaque caractère du code est ’0’ ou ’1’;• code.length <= 32• // exemple : binaryToSigned("010100001001") renvoie 1289• // binaryToSigned("101011110111") renvoie -1289

1.10 Écrivez et testez la méthode suivante :

• public static int binaryToUnsigned(String code)• // conditions préalables : chaque caractère du code est ’0’ ou ’1’;• code.length <= 32• // exemple : binaryToUnsigned("010100001001") renvoie 1289• // binaryToUnsigned("101011110111") renvoie 2807

1.11 Écrivez et testez la méthode suivante :

• public static String format(String s, int len, int d)• // exemples : format("tomate",9,d) pour d = -1,0, 1 renvoie• // respectivement :• // "tomate " , " tomate " , " tomate"

1.12 Écrivez et testez la méthode suivante :

• public static int randomInt(int start, int stop)• // conditions préalables : début < arrêt;• // renvoie des entiers distribués uniformément• // dans l’intervalle début à fin -1 (inclus)

1.13 Implémentez ce constructeur par défaut pour la classe Point (reportez-vous à l’exemple 1.7) :

• public Point()• // construit un point représentant l’origine (0,0)

1.14 Ajoutez un objet public static final à la classe Point afin de représenter l’origine (0,0).

1.15 Implémentez le constructeur de copie suivant pour la classe Point (reportez-vous à l’exem-ple 1.7) :

• public Point(Point point)• // construit un point ayant les mêmes coordonnées que• // le point donné

Page 30: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

20 Caractéristiques de base du langage java

1.16 Implémentez la méthode suivante pour la classe Point (reportez-vous à l’exemple 1.7) :

• public double distance(Point point)• // renvoie la distance euclidienne de ce point au point donné

1.17 Implémentez la méthode suivante pour la classe Point (reportez-vous à l’exemple 1.7) :

• public double magnitude()• // renvoie la distance euclidienne de ce point à l’origine

1.18 Implémentez la méthode suivante pour la classe Point (reportez-vous à l’exemple 1.7) :

• public double amplitude()• // renvoie la mesure en radians de l’angle polaire du point

1.19 Implémentez le constructeur suivant pour la classe Point (reportez-vous à l’exemple 1.7) :

• public void setPolar(double r, double theta)• // déplace ce point vers les coordonnées polaires données (r,theta)

1.20 Implémentez la méthode suivante pour la classe Point (reportez-vous à l’exemple 1.7) :

• public static Point polar(double r, double theta)• // renvoie le point dont les coordonnées polaires sont (r,theta)

1.21 Implémentez la méthode suivante pour la classe Point (reportez-vous à l’exemple 1.7) :

• public void expand(double dr)• // développe ce point par le facteur dr

1.22 Implémentez la méthode suivante pour la classe Point (reportez-vous à l’exemple 1.7) :

• public void rotate(double theta)• // fait pivoter le point dans le sens des aiguilles d’une montre• // par theta radians

SOLUTIONS¿SOLUTIONS

1.1 • public static String monthName(int month)• switch (month)• case 1: return "Janvier";• case 2: return "Février";• case 3: return "Mars";• case 4: return "Avril";• case 5: return "Mai";• case 6: return "Juin";• case 7: return "Juillet";• case 8: return "Août";• case 9: return "Septembre";• case 10: return "Octobre";• case 11: return "Novembre";• case 12: return "Décembre";• • return "";•

1.2 • public static int daysInMonth(int month, int year)• if (month==4 || month==6 || month==9 || month==11) return 30;• if (month==2)

Page 31: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 21

• if (year%400==0 || year%100!=0 && year%4==0) return 29;• else return 28;• return 31;•

1.3 • public static int numberOfDigits(int n)• if (n<0) n = -n;• int count=0;• while (n>0)• n /= 10;• ++count;• • return count;•

1.4 • public static int sumOfDigits(int n)• if (n<0) n = -n;• int sum=0;• while (n>0)• sum += n%10;• n /= 10;• • return sum;•

1.5 • public static int reverseDigits(int n)• if (n==0) return 0;• int sign = (n<0?-1:1);• if (n<0) n = -n;• int reverse=0;• while (n>0)• reverse = 10*reverse + n%10;• n /= 10;• • return sign*reverse;•

1.6 • public static double round(double x, int precision)• double pow10 = Math.pow(10,precision);• return Math.round(x*pow10)/pow10;•

1.7 • public static String signedToBinary(int n)• if (n==0) return "0";• if (n>0) return "0" + unsignedToBinary(n); // exercice 1.8• int mod=1;• while(mod+2*n<0)• mod *= 2;• return unsignedToBinary(mod+n);•

1.8 • public static String unsignedToBinary(int n)• // Condition préalable : n > 0• String code="";• while (n > 0)• code = "" + (n%2) + code; // ajouter le bit suivant • // à gauche du code• n /= 2; // supprimer le bit courant le moins significatif• • return code ;•

Page 32: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

22 Caractéristiques de base du langage java

1.9 • public static int binaryToSigned(String code)• int len = code.length();• int unsigned = binaryToUnsigned(code); // exercice 1.10• if (code.charAt(0) == ’0’) return unsigned;• return unsigned - (int)Math.pow(2,len);•

1.10 • public static int binaryToUnsigned(String code)• int n = code.length();• int answer = 0;• for (int i=0; i<n; i++)• answer = answer*2 + (code.charAt(i)==’1’ ? 1 : 0);• return answer;•

1.11 • public static String format(String s, int len, int d)• int spaces = (len - s.length());• if (spaces <= 0) return s;• String formatS = "";• int leftSpaces = (d<0)? 0 : ((d>0)? spaces : spaces/2);• int rightSpaces = spaces - leftSpaces;• for (int i=0; i<leftSpaces; i++)• formatS += " ";• formatS += s ; // s vient après les espaces à gauche• for (int i=0; i<rightSpaces; i++)• formatS += " ";• return formatS;•

1.12 • static java.util.Random random = new java.util.Random();• public static int randomInt(int start, int stop)• return start + random.nextInt(stop-start);•

1.13 Voici un exemple de constructeur par défaut pour la classe Point :

• public Point()• this.x = 0;• this.y = 0;•

1.14 Voici un exemple de champ public static final pour la classe Point :

•public static final Point ORIGIN = new Point();

1.15 Voici un exemple de constructeur de copie pour la classe Point :

• public Point(Point q)• this.x = q.x;• this.y = q.y;

1.16 Voici un exemple de méthode de distance pour la classe Point :

• public double distance(Point point)• double dx = this.x - point.x;• double dy = this.y - point.y;• return Math.sqrt(dx*dx+dy*dy);•

1.17 Voici un exemple de méthode de grandeur pour la classe Point :

• public double magnitude()• return distance(ORIGIN);•

Page 33: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 23

1.18 Voici un exemple de méthode d’amplitude pour la classe Point :

• public double amplitude()• return Math.atan(y/x);•

1.19 Voici un exemple de méthode de la classe Point pour les coordonnées polaires :

• public void setPolar(double r, double theta)• this.x = r*Math.cos(theta);• this.y = r*Math.sin(theta);•

1.20 Voici un autre exemple de méthode de la classe Point pour les coordonnées polaires :

• public static Point polar(double r, double theta)• double x = r*Math.cos(theta);• double y = r*Math.sin(theta);• return new Point(x,y);•

1.21 Voici un exemple de méthode de développement pour la classe Point :

• public void expand(double dr)• x *= dr;• y *= dr;•

1.22 Voici un exemple de méthode de rotation pour la classe Point :

• public void rotate(double theta)• double xx = x;• double yy = y;• double sin = Math.sin(theta);• double cos = Math.cos(theta);• x = xx*cos - yy*sin;• y = xx*sin + yy*cos;•

Page 34: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie
Page 35: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 2

Caractéristiques de basedes tableaux

Un tableau est un objet composé d’une séquence d’éléments numérotés de même type. Ces éléments sontnumérotés à partir de 0 et peuvent être référencés par leur numéro grâce à l’opérateur d’index []. Lestableaux sont beaucoup utilisés en raison de leur efficacité.

2.1 PROPRIÉTÉS DES TABLEAUXDans le cadre du langage de programmation Java, les tableaux présentent les propriétés suivantes :

1. Les tableaux sont des objets.2. Ils sont créés dynamiquement (au moment de l’exécution).3. Ils peuvent être affectés à des variables de type Object.4. Toutes les méthodes de la classe Object peuvent être appelées sur un tableau.5. Un objet tableau contient une séquence de variables qui sont toutes du même type.6. Les variables sont qualifiées de composants du tableau.7. Si le type de composant est T, le tableau a également le type T[].8. Une variable de type tableau contient une référence à l’objet tableau.9. Le type de composant peut également avoir le type tableau.

10. Un élément de tableau est un composant dont le type n’est pas un tableau.11. Un type d’élément peut être primitif ou de référence.12. La longueur d’un tableau correspond à son nombre de composants.13. La longueur d’un tableau est paramétrée lors de sa création et ne peut pas être modifiée par la suite.14. Pour être accessible, la longueur d’un tableau doit être définie comme une variable d’instance

public final.15. Les tableaux doivent être indexés par des valeurs intégrales dans un intervalle de 0..length –1.16. Une exception ArrayIndexOutOfBoundsException est lancée si la propriété 15 n’est pas res-

pectée.17. Les variables de type short, byte ou char peuvent être utilisées comme index.18. Les tableaux peuvent être copiés à l’aide de la méthode Object.clone().

Page 36: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

26 Caractéristiques de base des tableaux

19. Il est possible de tester l’égalité des tableaux à l’aide de la méthode Arrays.equals().

20. Les objets tableau implémentent Cloneable et java.io.Serializable.

La propriété 3 découle de la propriété 1. Bien que le type tableau ne soit pas une classe, il se com-porte comme une extension de la classe Object. La propriété 7 démontre que le type tableau est diffé-rent du type classe (reportez-vous à la première figure du chapitre 1). En fait, il s’agit d’un type dérivé :pour chaque type de classe T, il existe un type de tableau correspondant T[]. En outre, pour chacun deshuit types primitifs, il existe un type de tableau correspondant.

La propriété 9 permet la création de tableaux de tableaux. Techniquement parlant, Java autorise lestableaux multidimensionnels uniquement pour les types primitifs. Cependant, dans le cas des objets, iln’y a pas énormément de différence entre des objets et un tableau de tableaux. En effet, les tableaux étanteux-mêmes des objets, un tableau de tableaux est, par définition, un tableau d’objets. En outre, certainsobjets composants peuvent également être différents d’un tableau (reportez-vous à l’exemple 2.1).

En raison de la propriété 13, l’affectation de null à une valeur de composant de référence n’affecteen rien la longueur du tableau ; null reste une valeur valide pour le composant de référence.

Exemple 2.1 Quelques définitions de tableaux

Vous trouverez ci-après des exemples de définitions de tableaux :

public class Ex0201 public static void main(String[] args) float x[]; x = new float[100]; args = new String[10]; boolean[] isPrime = new boolean[1000]; int fib[] = 0, 1, 1, 2, 3, 5, 8, 13 ; short[][][] b = new short[3][8][5]; double a[][] = 1.1,2.2, 3.3,4.4, null, 5.5,6.6, null ; a[4] = new double[66]; a[4][65] = 3.14; Object[] objects = x, args, isPrime, fib, b, a ;

La première ligne déclare x[] comme tableau de floats mais n’alloue aucun de ces derniers. Ladeuxième ligne définit x[] comme ayant 100 composants de type float.

La troisième ligne déclare args[] comme tableau d’objets String. Remarquez les deux méthodes(équivalentes) de déclaration d’un tableau : les crochets peuvent être le suffixe de l’identificateur detype ou bien de celui du tableau. La quatrième ligne définit args[] avec 10 composants String.

La cinquième ligne définit isPrime[] comme tableau de 1 000 variables boolean.

La sixième ligne définit fib[] comme tableau de 8 int initialisés aux 8 valeurs listées. Ainsi,fib[4] a la valeur 3 et fib[7] a la valeur 13.

La huitième ligne définit a[][] comme tableau de cinq composants, chacun d’entre eux étant untableau d’éléments de type double. Seuls trois des cinq tableaux composants sont alloués. La lignesuivante alloue un tableau de 66 éléments de type double à a[4] et la dernière ligne affecte 3.14 àson dernier élément.

La dernière ligne définit le tableau objects avec six composants, chacun d’entre eux étant untableau. Les composants des quatre premiers tableaux composants sont des éléments, c’est-à-direqu’ils ne sont pas des tableaux. Mais les composants b et a ne sont pas des éléments parce qu’ils sontégalement des tableaux. Les éléments du tableau objects comprennent 2, 5 et 13 (composants ducomposant fib), null (composant du composant a), ainsi que 2.2 et 3.14 (composants des compo-sants du composant a).

Page 37: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Copier un tableau 27

Le tableau a[][] défini dans l’exemple 2.1 est qualifié de tableau extensif parce qu’il s’agit d’untableau bidimensionnel composé de lignes de différentes longueurs.

Dans le cadre de Java, l’élément d’un tableau peut être de type primitif, ou bien de type tableau ouréférence. Les tableaux les plus simples sont bien évidemment ceux qui sont composés d’éléments detype primitif, par exemple x[], isPrime[] et fib[] dans l’exemple 2.1. Ces tableaux présententl’avantage de pouvoir être triés.

2.2 COPIER UN TABLEAUÉtant donné qu’un tableau est un objet, il peut être copié en appelant la méthode Object.clone(),comme illustré dans l’exemple suivant.

Exemple 2.2 Copier un tableau

public class Ex0202 public static void main(String[] args) int[] a = 22, 44, 66, 88 ; print(a); int[] b = (int[])a.clone(); // copier a[] dans b[] print(b); String[] c = "AB", "CD", "EF" ; print(c); String[] d = (String[])c.clone(); // copier c[] dans d[] print(d); c[1] = "XYZ"; // modifier c[], mais pas d[] print(c); print(d);

public static void print(int[] a) for (int i=0; i<a.length; i++) System.out.print(a[i] + " "); System.out.println();

public static void print(Object[] a) for (int i=0; i<a.length; i++) System.out.print(a[i] + " "); System.out.println();

Le tableau a[] contient quatre éléments int. Le tableau b[] est une copie de a[]. De la mêmefaçon, le tableau d[] est une copie du tableau c[], chacun d’entre eux contenant trois élémentsString. Dans les deux cas, la copie est obtenue en appelant la méthode clone(). Étant donné que cetteméthode renvoie une référence à un Object, le transtypage doit être effectué vers le type du tableaucopié, c’est-à-dire int[] ou String[].

22 44 66 8822 44 66 88AB CD EFAB CD EFAB XYZ EFAB CD EF

Page 38: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

28 Caractéristiques de base des tableaux

La dernière partie de l’exemple illustre le fait que le tableau cloné d[] soit une copie séparée dec[] : la modification de c[1] en "XYZ" n’affecte en aucun cas la valeur "CD" de d[1].

2.3 CLASSE ArraysLa classe java.util.Arrays définit les méthodes suivantes :

public static List asList(Object[])public static int binarySearch(...)public static boolean equals(...)public static void fill(...)public static void sort(...)

Dans le cas présent, les points de suspension entre parenthèses indiquent que la méthode est surchar-gée pour différents types de paramètre, à la fois primitifs et de référence. Par exemple, il existe neuf ver-sions de la méthode binarySearch(…) : une pour le tableau de chaque type primitif à l’exception dutype boolean, et deux pour les tableaux de type Object[].

Notez que toutes les méthodes de la classe Arrays sont static et sont donc appelées à l’aide dupréfixe Arrays au lieu du nom de l’instance de classe. En fait, il n’est pas possible d’instancier la classeArrays parce que son constructeur est déclaré comme private.

Exemple 2.3 Tester les méthodes de la classe Arrays

import java.util.Arrays;

public class Ex0203 public static void main(String[] args) char[] a = new char[64]; Arrays.fill(a,’H’); String s = new String(a); System.out.println("s = \"" + s + "\""); Object[] objects = new Object[8]; Arrays.fill(objects,2,5,"Java"); System.out.println("objects = " + Arrays.asList(objects)); int[] x = 77, 44, 99, 88, 22, 33, 66, 55 ; int[] y = (int[])x.clone(); System.out.print("x = "); print(x); System.out.print("y = "); print(y); System.out.println("Arrays.equals(x,y) = " + Arrays.equals(x,y)); System.out.println("y.equals(x) = " + y.equals(x)); y[4] = 0; System.out.print("y = "); print(y); System.out.println("Arrays.equals(x,y) = " + Arrays.equals(x,y)); System.out.print("x = "); print(x); Arrays.sort(x); System.out.print("x = "); print(x); int i = Arrays.binarySearch(x,44); System.out.println("Arrays.binarySearch(x,44) = " + i); i = Arrays.binarySearch(x,47); System.out.println("Arrays.binarySearch(x,47) = " + i); private static void print(int[] a) System.out.print(" " + a[0]); for (int i=1; i<a.length; i++) System.out.print(", " + a[i]); System.out.println(" ");

Page 39: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Arrays 29

Les trois premières lignes construisent un objet String avec plusieurs instances d’un même carac-tère. Nous définissons d’abord a comme tableau composé de 64 char. Nous utilisons ensuite laméthode Arrays.fill() pour le remplir à l’aide du caractère voulu, ’H’ dans le cas présent.Nous utilisons ensuite le constructeur String approprié de façon à créer un objet String avec lemême contenu.

Puis, nous construisons un tableau appelé Object et composé de huit références. Nous entrons leséléments indexés de 2 à 4 avec l’objet "Java" de type String. Remarquez que les paramètresentiers 2 et 5 sont utilisés pour indiquer le sous-intervalle de valeurs à remplir. Le premier entier, 2,est l’index de départ, et le second est l’index du premier élément suivant qui ne sera pas modifié. Ceprotocole de définition des sous-intervalles est souvent utilisé dans les bibliothèques standard. C’estpourquoi le nombre d’éléments du sous-intervalle est toujours égal à la différence entre les deuxparamètres, dans le cas présent 5 – 2 = 3 éléments ont été modifiés.

Il n’existe pas de méthode toString() pour les tableaux. Contrairement à d’autres objets, ilspeuvent donc être imprimés directement. Cependant, la méthode Arrays.toList() crée un objetList qui peut être passé à la méthode System.out.println() parce qu’il a une méthodetoString(). Il s’agit là d’un procédé simple permettant d’imprimer un tableau d’objets.

La partie suivante de l’exemple crée deux tableaux int, x et y, en utilisant la méthode Object.clone() pour copier x dans y (reportez-vous à la section 2.2). Les méthodes Arrays.equals()et Object.equals() sont ensuite appelées afin de vérifier l’égalité. Remarquez que seule la pre-mière de ces méthodes renvoie la bonne réponse. Nous modifions ensuite y[4] de façon à vérifier sila méthode Arrays.equals() fonctionne correctement lorsque les deux tableaux ne sont paségaux.

La méthode Arrays.sort() permet de trier x. Nous pouvons ensuite utiliser la méthode Arrays.binarySearch() pour rechercher un int donné dans x. Si cet élément se trouve dans le tableau,la méthode renvoie son index, c’est-à-dire 2 dans notre exemple. Dans le cas contraire, la méthoderenvoie un entier négatif afin de signaler que l’élément ne se trouve pas dans le tableau. L’algorithmede recherche binaire générique est défini dans la section 2.5.

Remarquez que la méthode Arrays.binarySearch() ne fonctionnera pas correctement si letableau n’a pas été trié auparavant.

La méthode Arrays.equals() est destinée à être utilisée à la place de la méthode Object.equals() pour les tableaux. Cette deuxième méthode renvoie true uniquement lorsque les deuxréférences concernent le même objet, c’est-à-dire que l’identité est testée au lieu de l’égalité.

Afin d’illustrer plus en détail les algorithmes de tableaux présentés dans ce livre, nous allons main-tenant définir une autre classe Arrays. Pour éviter un conflit de nom, cette classe sera définie dans unpaquetage séparé appelé schaums.dswj.

s = "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH"objects = [null, null, Java, Java, Java, null, null, null]x = 77, 44, 99, 88, 22, 33, 66, 55 y = 77, 44, 99, 88, 22, 33, 66, 55 Arrays.equals(x,y) = truey.equals(x) = falsey = 77, 44, 99, 88, 0, 33, 66, 55 Arrays.equals(x,y) = falsex = 77, 44, 99, 88, 22, 33, 66, 55 x = 22, 33, 44, 55, 66, 77, 88, 99 Arrays.binarySearch(x,44) = 2Arrays.binarySearch(x,47) = -4

Page 40: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

30 Caractéristiques de base des tableaux

Exemple 2.4 Classe utilitaire Arrays

package schaums.dswj;import java.util.Random;

public class Arrays private static Random random = new Random();

public static int load(int start, int range) return random.nextInt(range) + start;

public static void load(int[] a, int start, int range) int n=a.length; for (int i=0; i<n; i++) a[i] = random.nextInt(range) + start;

public static void print(int[] a) for (int i=0; i<a.length; i++) System.out.print(" " + (i>9?"":" ") + i); System.out.print("\n " + a[0]); for (int i=1; i<a.length; i++) System.out.print(", " + a[i]); System.out.println(" ");

Initialement, cette classe test n’a que trois méthodes : deux méthodes load() permettant d’initiali-ser les variables et les tableaux à l’aide d’entiers aléatoires et une méthode print() permettantd’imprimer des tableaux d’entiers. Les méthodes load() limitent les valeurs entières du début del’intervalle à load –1. Cela facilite la création des valeurs copiées. La méthode print() annote laliste du tableau à l’aide d’une ligne des numéros d’index afin de faciliter la recherche de chaque élé-ment indexé.Le pilote test suivant illustre le fonctionnement de cette classe :

import schaums.dswj.Arrays;

public class Testing private static final int SIZE = 16; private static final int START = 40; private static final int RANGE = 20; private static int[] a = new int[SIZE];

public static void main(String[] args) Arrays.load(a,START,RANGE); Arrays.print(a); Arrays.load(a,START,RANGE); Arrays.print(a);

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 49, 56, 46, 43, 40, 57, 47, 43, 43, 43, 46, 57, 47, 53, 44, 46

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 58, 55, 40, 45, 56, 46, 59, 59, 57, 45, 46, 42, 52, 47, 42, 54

Page 41: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithme de recherche séquentielle 31

Ce programme génère deux tableaux aléatoires composés de 16 éléments chacun. Ces éléments setrouvent dans l’intervalle 40 à 59 et sont donc susceptibles d’avoir des copies. Par exemple, dans lepremier tableau, a[2] = a[10] = 46.

2.4 ALGORITHME DE RECHERCHE SÉQUENTIELLELa recherche séquentielle (également appelée recherche linéaire) est l’algorithme de recherche le plussimple, mais également le moins efficace. Cet algorithme étudie simplement chaque élément de façonséquentielle. Il commence donc par le premier élément et continue jusqu’à ce qu’il trouve l’élément cléou qu’il atteigne la fin du tableau.

Par exemple, si vous cherchez quelqu’un dans un train en marche, vous utilisez une rechercheséquentielle, comme illustré dans la figure suivante :

Algorithme 2.1 Recherche séquentielle

(Condition préalable : s = s0, s1, s2, . . ., sn – 1 est une séquence de n valeurs ordinales du même typeque x.)(Condition postérieure : l’index i est renvoyé quand si = x, ou –1 est renvoyé.)

1. Effectuez les étapes 2 et 3 n fois pour i = 0 jusqu’à n – 1.

2. (Invariant : aucun des éléments de la séquence suivante s0..si – 1 n’est égal à x.)

3. Si si = x, renvoyer i.

4. Renvoyer –1.

Dans l’algorithme 2.1 (et dans tous les algorithmes de ce livre), nous préciserons les conditions préa-lables et postérieures afin d’indiquer très exactement ce que fait l’algorithme. En outre, nous utiliseronsdes invariants de boucle afin de démontrer l’exactitude de l’algorithme. Nous proposerons également unexemple Java de mise en pratique de chaque algorithme.

Exemple 2.5 Recherche séquentielle

public static int sequentialSearch(int[] a, int x) // Conditions postérieures : renvoie i; si i >= 0, alors a[i] == x; // sinon i == -1; for (int i=0; i<a.length; i++) // étape 1 // INVARIANT : si a[k]==x alors i <= k < a.length; // étape 2 // INVARIANT : a[k] != x, pour 0 <= k < i; // étape 2 if (a[i]==x) return i; // étape 3 return -1; // étape 4

Théorème 2.1 : la recherche séquentielle est correcte.Démonstration : si n = 0, la séquence est vide et la boucle n’est pas du tout exécutée. Seule l’étape 4 estexécutée et elle renvoie –1 immédiatement. Cela respecte les conditions postérieures puisque x ne peutpas être égal à l’un des éléments parce qu’il n’y en a aucun.Si n = 1, la boucle n’est itérée qu’une seule fois, avec i = 0. Lors de cette itération, s0 = x ou s0 ≠ x. Si s0= x, 0 est renvoyé et la condition postérieure est respectée. Si s0 ≠ x, la boucle s’arrête, l’étape 4 est exé-cutée et –1 est renvoyé, ce qui respecte la condition postérieure puisque le seul élément de la séquencen’est pas égal à x.

0 1 3 4

? ? ? ?

5 6 7 8 9

Page 42: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

32 Caractéristiques de base des tableaux

Supposons que n > 1. Lors de la première itération de la boucle, i = 0 et l’invariant de boucle de l’étape 2est vide et donc true parce que la sous-séquence s0..si – 1 est vide. Ensuite, au cours de l’étape 3, s0 =x ou s0 ≠ x. Si s0 = x, 0 est renvoyé et la condition postérieure est respectée. Si s0 ≠ x, la boucle continue.S’il y a une deuxième itération (c’est-à-dire, si s0 ≠ x), i = 1 et l’invariant de boucle de l’étape 2 est encoretrue parce que la sous-séquences0..si – 1 = s0 et que s0 ≠ x.Supposons que, lors de la kième itération de la boucle, l’invariant de cette dernière soit true, c’est-à-dire qu’aucun des éléments de la sous-séquence s0..sk – 1 soit égal à x. Au cours de cette itération, àl’étape 3, si = x ou si ≠ x. Si si = x, k est renvoyé et la condition postérieure est respectée. Si si ≠ x, la bou-cle continue.D’après le principe d’induction mathématique (reportez-vous à la section A.4), après chaque itération deboucle, l’algorithme se termine par une condition postérieure true ou bien c’est l’invariant de boucle del’itération suivante qui sera true. Ainsi, si l’algorithme ne se termine au cours d’aucune itération, l’inva-riant de boucle du cas i = n sera true après la dernière itération, lorsque i = n –1. En effet, aucun élémentde la sous-séquence s0..sn – 1 n’est égal à x. À ce stade, –1 est renvoyé et la condition postérieure estrespectée.

Le théorème suivant utilise la notation O(). Reportez-vous à la section A.3 pour revoir ces basesmathématiques.

Théorème 2.2 : la recherche séquentielle est exécutée en une durée O(n).Démonstration : si x se trouve dans la séquence, disons à x = si avec i < n, la boucle est itérée i fois.Dans ce cas, la durée d’exécution est proportionnelle à i, soit O(n) puisque i < n. En revanche, si x ne setrouve pas dans la séquence, la boucle est itérée n fois, d’où une durée d’exécution proportionnelle à n,soit O(n).

Exemple 2.6 Tester la recherche séquentielle

import schaums.dswj.Arrays;

public class Testing private static final int SIZE = 16; private static final int START = 40; private static final int RANGE = 20; private static int[] a = new int[SIZE];

public static void main(String[] args) Arrays.load(a,START,RANGE); Arrays.print(a); test(); test(); test(); test();

public static void test() int x = Arrays.load(START,RANGE); System.out.print("Recherche de x = " + x + ":\t"); int i = Arrays.sequentialSearch(a,x); if (i >= 0) System.out.println("a[" + i + "] = " + a[i]); else System.out.println("i = " + i + " ==> x introuvable");

Page 43: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithme de recherche binaire 33

Ce programme teste l’algorithme de recherche séquentielle quatre fois sur le tableau généré parload(). Le premier test recherche x = 51 et le trouve à a[6]. Le deuxième recherche x = 47 et letrouve à a[11]. Le troisième recherche x = 56 et échoue, c’est pourquoi il renvoie –1. Le quatrièmerecherche x = 50 et le trouve à a[4]. Remarquez que cette valeur se trouve également à a[10].La recherche séquentielle s’arrête dès qu’elle trouve l’élément recherché et elle ignore le reste dutableau.

Chaque algorithme de tableau est ajouté à la classe schaums.dswj.Arrays afin d’en faciliterl’accès. Ainsi, pour tester la recherche séquentielle, nous importons cette classe, puis nous appelons laméthode de recherche Arrays.sequentialSearch().

2.5 ALGORITHME DE RECHERCHE BINAIRELa recherche binaire est la procédure standard de recherche dans uneséquence triée. Elle est beaucoup plus efficace que la recherche séquen-tielle, mais les éléments doivent être triés pour qu’elle soit utilisable.Elle divise à plusieurs reprises la séquence en deux, puis limite à chaquefois la recherche à la moitié contenant l’élément.

Ce type de recherche est notamment utile lorsque vous souhaitezrechercher un mot dans le dictionnaire.

Algorithme 2.2 Recherche binaire

(Condition préalable : s = s0, s1, s2, …, sn – 1 est une séquence triéede valeurs ordinales n du même type que x.)(Condition postérieure : l’index i est renvoyé si si = x ou bien –1 est renvoyé.)

1. Supposons que ss soit une sous-séquence de s, initialement paramétrée de façon à être égale à s.2. Si la sous-séquence ss est vide, renvoyer –1.3. (Invariant : si x se trouve dans la séquence initiale s, il doit se trouver dans la sous-séquence ss.)4. Supposons que si soit l’élément central de ss.5. Si si = x, renvoyer son index i.6. Si si < x, répétez les étapes 2 à 7 sur la sous-séquence située au-dessus de si.7. Répétez les étapes 2 à 7 sur la sous-séquence située sous si.

Notez que la condition préalable de l’algorithme 2.2 ne peut être appliquée qu’à une séquence triée.La recherche binaire est implémentée dans la classe java.util.Arrays (reportez-vous à la sec-

tion 2.3). Vous en trouverez ci-après une version annotée :

Exemple 2.7 Recherche binaire

public static int binarySearch(int[] a, int x) // Condition préalable : a[0] <= a[1] <= ... <= a[a.length-1]; // Conditions postérieures : renvoie i; si i >= 0, alors a[i] == x; // sinon i == -1;

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 49, 44, 49, 41, 50, 59, 51, 48, 48, 41, 50, 47, 41, 58, 46, 48 Recherche de x = 51: a[6] = 51Recherche de x = 47: a[11] = 47Recherche de x = 56: i = -1 ==> x introuvableRecherche de x = 50: a[4] = 50

?

?

?

?

Page 44: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

34 Caractéristiques de base des tableaux

int lo=0, hi=a.length-1; while (lo <= hi) // étape 1 // INVARIANT : si a[j]==x alors lo <= j <= hi; // étape 3 int i = (hi + lo)/2; // étape 4 if (a[i] == x) return i; // étape 5 else if (a[i] < x) lo = i+1; // étape 6 else hi = i-1; // étape 7 return -1; // étape 2

Théorème 2.3 : la recherche binaire est correcte.Démonstration : l’invariant de boucle est true à la première itération parce que la sous-séquence cou-rante est identique à la séquence initiale. À chaque autre itération, la sous-séquence courante a été définiedans l’itération précédente comme la moitié de la sous-séquence précédente qui restait après l’omissionde la moitié qui ne contenait pas x. Ainsi, si x se trouvait dans la séquence initiale, il doit se trouver dansla sous-séquence courante. L’invariant de boucle est par conséquent true à chaque itération.Pour chaque itération, i est renvoyé lorsque si = x ou bien la sous-séquence est réduite de plus de 50 %.Étant donné que la séquence initiale ne contient qu’un nombre fini d’éléments, la boucle ne peut pascontinuer indéfiniment. C’est pourquoi l’algorithme s’arrête soit en renvoyant i depuis la boucle, soit àl’étape 6 ou 7 lorsque –1 est renvoyé. Si i est renvoyé depuis la boucle, si = x. Si tel n’est pas le cas, laboucle se termine lorsque grand (hi) < petit (lo), c’est-à-dire lorsque la sous-séquence est vide. Dans cecas, nous savons par l’invariant de boucle que si ne se trouve pas dans la séquence initiale.

Théorème 2.4 : la recherche binaire est exécutée en une durée O(lgn).Démonstration : dans la démonstration du théorème 2.3, nous avons vu que le nombre d’itérations estau maximum égal au nombre de fois (plus 1) où n peut être divisé par deux. Ce nombre correspond aulogarithme binaire intégral lgn (reportez-vous à l’annexe A pour plus d’informations).

Exemple 2.8 Tester la recherche binaire

import schaums.dswj.Arrays;

public class Ex0208 private static final int SIZE = 16; private static final int START = 40; private static final int RANGE = 20; private static int[] a = new int[SIZE];

public static void main(String[] args) Arrays.load(a,START,RANGE); Arrays.print(a); test(); java.util.Arrays.sort(a); Arrays.print(a); test(); test(); test();

public static void test() int x = Arrays.load(START,RANGE); System.out.print("Recherche de x = " + x + ":\t"); int i = Arrays.binarySearch(a,x); if (i >= 0) System.out.println("a[" + i + "] = " + a[i]); else System.out.println("i = " + i + " ==> x introuvable");

Page 45: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Vector 35

Ce programme teste l’algorithme de recherche binaire quatre fois sur le tableau généré par load().Le premier test recherche x = 50 et échoue bien que a[7] = 50 parce que le tableau n’est pas encoretrié.Une fois que java.util.Arrays.sort(a) a trié le tableau, le deuxième test trouve x = 50 àa[7].Le troisième test recherche x = 57 et le trouve à a[5]. Remarquez qu’il ne s’agit pas de l’occurrencela plus à gauche de cette valeur qui apparaît aussi à a[4]. En fait, la recherche binaire n’est passéquentielle, c’est pourquoi il n’est pas facile de savoir quel index sera renvoyé par l’algorithmelorsqu’une valeur apparaît plusieurs fois (reportez-vous à la question de révision 2.9).Le quatrième test recherche x = 58 et échoue, c’est pourquoi il renvoie –1.

2.6 CLASSE VectorMathématiquement, un vecteur est simplement une séquence finie de nombres. Géométriquement, nouspensons généralement à un vecteur bidimensionnel (x, y) comme représentant un point dans un plan et àun vecteur tridimensionnel (x, y, z) comme représentant un point dans l’espace. Les vecteurs composésd’un nombre n fini d’éléments sont courants en algèbre linéaire : (x1, x2, …, xn) . Ce procédé requiertl’utilisation des index.

Dans le cadre de Java, un vecteur est identique à un tableau, à l’exception des points suivants :

• Un vecteur est une instance de la classe java.util.Vector.

• Il est possible de modifier la longueur d’un vecteur.

La classe Vector a été largement réécrite dans Java 1.2. Elle est définie de la façon suivante dans lepaquetage java.util :

public class Vector extends AbstractList implements List public boolean add(Object object) public void add(int index, Object object) public boolean addAll(Collection collection) public boolean addAll(int index, Collection collection) public void addElement(Object object) public void clear() public Object clone() public boolean contains(Object object) public boolean containsAll(Collection collection) public void copyInto(Object[] objects) public Object elementAt(int index) public boolean equals(Object object) public Object firstElement() public Object get(int index) public int hashCode() public int indexOf(Object object) public int indexOf(Object object, int index) public void insertElementAt(Object object, int index)

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 48, 57, 46, 51, 55, 51, 55, 45, 47, 52, 57, 47, 50, 42, 59, 45 Recherche de x = 50: i = -1 ==> x introuvable 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 42, 45, 45, 46, 47, 47, 48, 50, 51, 51, 52, 55, 55, 57, 57, 59 Recherche de x = 50: a[7] = 50Recherche de x = 47: a[5] = 47Recherche de x = 58: i = -1 ==> x introuvable

Page 46: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

36 Caractéristiques de base des tableaux

public boolean isEmpty() public Object lastElement() public int lastIndexOf(Object object) public int lastIndexOf(Object object, int index) public Object remove(int index) public boolean remove(Object object) public boolean removeAll(Collection collection) public void removeAllElements() public boolean removeElement(Object object) public void removeElementAt(int index) public boolean retainAll(Collection collection) public Object set(int index, Object object) public void setElementAt(Object object, int index) public int size() public List subList(int start, int stop) public Object[] toArray() public Object[] toArray(Object[] objects) public String toString() public Vector() public Vector(Collection collection)

Les méthodes add permettent d’insérer de nouveaux éléments dans le vecteur. Les méthodesadd(object) et addElement(object) ont le même effet, mais la première renvoie true (oufalse en cas d’échec). Si l’emplacement du ou des nouveaux éléments n’est pas spécifié, la fin du vec-teur est utilisée par défaut.

La méthode clear() réduit le vecteur de façon à ce que sa taille soit égale à zéro, c’est-à-dire qu’ilsoit vide. Remarquez qu’un vecteur vide est différent d’une référence null puisque l’objet vecteurexiste encore.

La méthode clone() copie tout le vecteur, et donc l’objet qu’il contient.La méthode contains détermine si les objets donnés sont des éléments du vecteur.La méthode copyInto copie les éléments du vecteur dans le tableau donné.Les méthodes elementAt() et get() sont semblables à l’opérateur d’index pour les tableaux

puisqu’elles renvoient l’élément vecteur à l’index donné.La méthode equals() teste l’égalité d’un autre objet et renvoie true si et uniquement si l’objet

donné est un vecteur dont les éléments sont égaux au vecteur appelé.Les méthodes firstElement() et lastElement() renvoient simplement le premier et le dernier

élément du vecteur.La méthode hashCode() renvoie un numéro d’identification pour l’objet vecteur.Les méthodes indexOf utilisent la recherche séquentielle pour retrouver et renvoyer l’index d’un

objet donné du vecteur. Elles renvoient –1 si l’objet n’est pas trouvé. La version à deux paramètres com-mence sa recherche à l’index donné.

La méthode insertElementAt() insère l’objet donné dans le vecteur à l’index donné. Tous leséléments existants sont alors déplacés vers l’avant à partir de cet index.

Les méthodes lastIndexOf fonctionnent comme indexOf(), mais elles exécutent la rechercheséquentielle à l’envers.

Les méthodes remove suppriment du vecteur un segment composé d’un ou de plusieurs éléments.Les éléments existants qui se trouvaient au-delà du segment supprimé sont alors déplacés vers l’arrière,ce qui provoque un résultat inverse à celui de la méthode insertElementAt(). Remarquez le pointsuivant : bien qu’il existe une seule méthode insert, six méthodes remove (public) sont à votre dis-position. Les méthodes remove(index) et removeElementAt(index) ont les mêmes conséquences,mais la première renvoie l’objet supprimé (ou null en cas d’échec), tandis que la dernière renvoie true(ou false en cas d’échec).

Page 47: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Vector 37

Les méthodes set(index,object) et setElementAt(object,index) remplacent toutes lesdeux l’élément à l’index donné par l’objet donné. Elles ont le même effet, sauf que la première renvoiel’objet supprimé (ou null en cas d’échec).

La méthode subList() renvoie un objet List contenant les objets du vecteur de l’index start àl’index stop -1. C’est pourquoi la taille de l’objet List obtenu est stop – start.

Les méthodes toArray() renvoient un tableau Object[] qui contient les mêmes éléments que levecteur. La version sans paramètres crée le tableau renvoyé et a donc la même taille que le vecteur. Laversion avec le paramètre Object[] effectue la même opération si la taille du tableau donné est infé-rieure à la taille du vecteur. Si tel n’est pas le cas, elle copie les éléments du vecteur au début du tableaudonné, puis elle attribue la valeur null aux composants restants.

La méthode toString() renvoie une chaîne qui représente le contenu du vecteur.

Exemple 2.9 Tester la classe java.util.Vector

import java.util.*;public class Ex0209 private static Vector v = new Vector(); private static Vector w = new Vector(); public static void main(String[] args) String[] villes = "Austin", "Boston", "Milan", "Moscou" ; v.addAll(Arrays.asList(villes)); System.out.println("v = " + v); v.add("Paris"); System.out.println("v = " + v); w = (Vector)v.clone(); System.out.println("w = " + w); System.out.println("w.equals(v) = " + w.equals(v)); v.set(3,"Ottawa"); System.out.println("v = " + v); System.out.println("w = " + w); System.out.println("w.equals(v) = " + w.equals(v)); v.insertElementAt("Londres",3); System.out.println("v = " + v); System.out.println("w = " + w); System.out.println("w.equals(v) = " + w.equals(v)); w.removeElementAt(1); w.removeElementAt(3); w.remove("Milan"); System.out.println("w = " + w); v.addAll(5,w); System.out.println("v = " + v); System.out.println("v.indexOf(\"Austin\") = " + v.indexOf("Austin")); System.out.println("v.indexOf(\"Austin\",2) = " + v.indexOf("Austin",2)); System.out.println("v.indexOf(\"Dublin\") = " + v.indexOf("Dublin"));

Page 48: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

38 Caractéristiques de base des tableaux

Le tableau villes permet de créer le vecteur v en passant l’objet List créé par la méthodeArrays.asList() au constructeur Vector qui prend l’objet Collection comme paramètre(une List est une Collection). La méthode System.out.println() appelle la méthodetoString() du vecteur pour l’imprimer.

L’appel v.add("Paris") ajoute la nouvelle chaîne à la fin du vecteur et augmente ainsi sa taille.L’appel v.clone() renvoie une copie du vecteur v, mais comme type Object. C’est pourquoi ildoit être transtypé en Vector afin d’être affecté à la référence w.

La méthode equals() montre que v et w sont égaux. L’appel v.set(3,"Ottawa") modifie v,mais pas w et vérifie si les deux vecteurs sont différents, mais égaux.

Après avoir supprimé les trois éléments de w, l’appel v.addAll(5,w) insère des copies de tous leséléments de w dans v en commençant à la cinquième position (c’est-à-dire juste après les cinq pre-miers éléments). En dernier lieu, la méthode indexOf() permet de rechercher les éléments de v.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

2.1 Quelle est la différence entre un composant et un élément de tableau ?

2.2 Que signifie exactement le fait que Java n’autorise pas les tableaux multidimensionnels ?

2.3 Qu’est-ce qu’une exception ArrayIndexOutOfBoundsException et en quoi son utilisationdifférencie-t-elle Java d’autres langages de programmation tels que le C et le C++ ?

2.4 Quels sont les types valides pour les index de tableaux ?

2.5 En quoi cette définition est-elle erronée ?

•Arrays arrays = new Arrays();

2.6 Quelle est la méthode d’impression d’un tableau d’objets la plus simple ?

2.7 Si la recherche binaire est nettement plus rapide que la recherche séquentielle, quel est l’intérêtd’utiliser cette dernière ?

2.8 Que se passe-t-il si la recherche séquentielle est appliquée à un élément qui apparaît plusieursfois dans le tableau ?

2.9 Que se passe-t-il si la recherche binaire est appliquée à un élément qui apparaît plusieurs foisdans le tableau ?

v = [Austin, Boston, Milan, Moscou]v = [Austin, Boston, Milan, Moscou, Paris]w = [Austin, Boston, Milan, Moscou, Paris]w.equals(v) = truev = [Austin, Boston, Milan, Ottawa, Paris]w = [Austin, Boston, Milan, Moscou, Paris]w.equals(v) = falsev = [Austin, Boston, Milan, Londres, Ottawa, Paris]w = [Austin, Boston, Milan, Moscou, Paris]w.equals(v) = falsew = [Austin, Moscou]v = [Austin, Boston, Milan, Londres, Ottawa, Austin, Moscou, Paris]v.indexOf("Austin") = 0v.indexOf("Austin",3) = 5v.indexOf("Dublin") = -1

Page 49: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 39

2.10 Quelle est la différence entre l’appel de la méthode clear() sur un objet Vector et l’affecta-tion de la valeur null à ce dernier ?

RÉPONSES¿RÉPONSES

2.1 Un composant de tableau peut être de type primitif, ou bien de type référence ou tableau. Un élé-ment de tableau est un composant qui n’est pas un type de tableau. C’est pourquoi, dans le lan-gage Java, les composants de a[] sont les lignes des tableaux et les éléments de a[][] sont desvariables de type double.

2.2 Un tableau multidimensionnel comporte plusieurs index. Un tableau Java n’a qu’une seule varia-ble d’index. Cependant, étant donné qu’un composant indexé par la variable peut être un tableau(avec un index), le tableau initial semble avoir plusieurs index.

2.3 Un objet ArrayIndexOutOfBoundsException est une exception qui est lancée dès que vousessayez d’utiliser une valeur inférieure à zéro ou bien supérieure ou égale à la longueur dutableau comme index sur le tableau. Grâce à ce procédé, le programmeur contrôle mieux lesconséquences d’une telle erreur d’exécution. Dans des langages tels que le C++, ce type d’erreurprovoque généralement le plantage du programme.

2.4 Un index de tableau peut être de type byte, char, short ou int.

2.5 La classe Array ne peut pas être instanciée parce que son constructeur est déclaré commeprivate.

2.6 La méthode la plus simple pour imprimer un tableau d’objets consiste à le passer à la méthodeArrays.toList() qui crée un objet List pouvant être imprimé directement à l’aide de laméthode System.out.println().

2.7 La recherche binaire ne peut pas fonctionner si le tableau n’a pas été trié au préalable.

2.8 Si la recherche séquentielle est appliquée à un élément qui apparaît plusieurs fois dans le tableau,elle renvoie l’index le plus proche du début du tableau.

2.9 Si la recherche binaire est appliquée à un élément qui apparaît plusieurs fois dans le tableau, elleest susceptible de renvoyer n’importe quel index ; cela dépend de l’emplacement des index parrapport aux multiples du point médian des sous-intervalles. Par exemple, si la recherche binaireest appliquée à un tableau de 10 000 éléments et que vous recherchez un élément répété auxemplacements 0-99, elle renverra l’index 77 à la 77e itération.

2.10 L’appel v.clear() fait de v un vecteur vide qui reste un objet non null. L’affectation v = nulldétruit l’objet. Dans le premier cas, l’expression v.size() serait évaluée à 0 ; dans le deuxième,elle lancerait une exception.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

2.1 Exécutez un programme test afin d’illustrer comment la méthode Arrays.fill() gère untableau d’objets.

2.2 Exécutez un programme test afin d’illustrer comment la méthode Arrays.equals() gèreun tableau d’objets.

Page 50: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

40 Caractéristiques de base des tableaux

2.3 Exécutez un programme test afin d’illustrer comment la méthode Arrays.equals() gère untableau de tableaux.

2.4 S’il faut 50 minutes à une recherche séquentielle pour parcourir un tableau de 10 000 éléments,combien de temps mettrait-elle pour effectuer la même opération sur un tableau de 20 000 élé-ments en utilisant le même ordinateur ?

2.5 S’il faut 5 minutes à une recherche binaire pour parcourir un tableau de 1 000 éléments, combiende temps mettrait-elle pour effectuer la même opération sur un tableau de 1 000 éléments en uti-lisant le même ordinateur ?

2.6 La recherche par interpolation est similaire à la recherche binaire. Cependant, contrairement àcette dernière, elle choisit si au cours de l’étape 4 de façon à ce que la proportion des élémentsinférieurs à si dans la sous-séquence ss corresponde à une répartition uniforme. Par exemple, sivous recherchez le nom Byrd dans un annuaire de 2600 pages, vous ouvrirez d’abord celui-ci auxalentours de la page 200 en supposant logiquement qu’1/13e des noms précède celui que vousrecherchez. La recherche par interpolation peut être exécutée en O(lglgn). En tenant compte deces informations, s’il vous a fallu 5 minutes pour exécuter votre recherche sur un tableau de1 000 éléments, combien de temps mettrez-vous pour un tableau de 1 000 000 éléments en utili-sant le même ordinateur ?

2.7 Exécutez un pilote test pour la méthode de recherche binaire présentée dans l’exemple 2.7 sur untableau de 10 000 éléments et comptez le nombre d’itérations.

2.8 Écrivez et testez la méthode suivante :

• private static boolean isSorted(int[] a)• // renvoie true si a[0] <= a[1] <= ... <= a[a.length-1]

2.9 Écrivez et testez la méthode suivante :

• private static int minimum(int[] a)• // renvoie l’élément minimum de a[]

2.10 Écrivez et testez la méthode suivante :

• private static double mean(double[] a)• // renvoie la valeur moyenne de tous les éléments de a[]

2.11 Écrivez et testez la méthode suivante :

• private static int[] withoutDuplicates(int[] a)• // renvoie un tableau avec les mêmes éléments que ceux de a[],• // mais sans copies

2.12 Écrivez et testez la méthode suivante :

• private static Object[] withoutDuplicates(Object[] a)• // renvoie un tableau avec les mêmes composants que ceux de a[], • // mais sans copies

2.13 Écrivez et testez la méthode suivante :

• private static void reverse(int[] a)• // inverse les éléments de a[]

2.14 Écrivez et testez la méthode suivante :

• private static Object[] concatenate(Object[] a, Object[] b)• // renvoie un tableau contenant tous les éléments de a[]• // suivis de tous les éléments de b[]

Page 51: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 41

2.15 Écrivez et testez la méthode suivante :

• private static void shuffle(Object[] a)• // permute de façon aléatoire les éléments de a[]

2.16 Écrivez et testez la méthode suivante :

• private static int[] tally(String string)• // renvoie un tableau a[] de 26 entiers qui compte les fréquences• // des lettres (insensibles à la casse) de la chaîne donnée

2.17 Écrivez et testez la méthode suivante :

• private static double innerProduct(double[] x, double[] y)• // renvoie le produit interne algébrique (la sommes des produits• // des composants) des deux tableaux donnés sous forme de • // vecteurs (algébriques)

2.18 Écrivez et testez la méthode suivante :

• private static double[][] outerProduct(double[] x, double[] y)• // renvoie le produit externe algébrique des deux tableaux donnés• // sous forme de vecteurs (algébriques) : p[i][j] = a[i]*b[j]

2.19 Écrivez et testez la méthode suivante :

• private static double[][] product(double[][] a, double[][] b)• // renvoie le produit matrice des deux tableaux donnés :• // p[i][j] = Sum(a[i][k]*b[k][j]:k)

2.20 Écrivez et testez la méthode suivante :

• private static void transpose(double[][] a)• // transpose le tableau donné sous forme de matrice :• // a[i][j] <-- a[j][i]

2.21 Écrivez et testez la méthode suivante :

• private static int[][] pascal(int size)• // renvoie le triangle de Pascal avec une taille donnée

2.22 Le crible d’Ératosthène est un tableau d’éléments boolean dont le iième élément est true si etseulement si i est un nombre premier. Utilisez l’algorithme suivant pour calculer et imprimer uncrible de taille 1 000 :

Algorithme 2.3 Crible d’Ératosthène

(Condition préalable : p est un tableau de n bits.) (Condition postérieure : p[i] est true si et seulement si i est un nombre premier.)

1. Initialiser p[0] et p[1] à false et tous les autres p[i] à true.

2. Répéter l’étape 3 pour chaque i de 3 à n en incrémentant par 2.

3. S’il existe un nombre premier ≤ à la racine carrée de i qui divise i, paramétrer p[i] à false.

2.23 Répétez l’exercice 2.22 en utilisant l’objet java.util.Vector.

2.24 Répétez l’exercice 2.22 en utilisant l’objet java.util.BitSet.

2.25 Définissez et testez la classe Primes avec les méthodes suivantes :

• private Primes()• public static void setLast(int last) // définit le dernier• public static void setLast() // définit dernier=1

Page 52: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

42 Caractéristiques de base des tableaux

• public static void sizeSize(int size) // définit la taille • // de bitset• public static void sizeSize() // définit la taille de bitset=1000• public static boolean isPrime(int n) // true si n est un• // nombre premier• public static int next() // nombre premier suivant • // le dernier• public static void printPrimes() // imprime le crible

Utilisez l’implémentation BitSet du crible d’Ératosthène de l’exercice 2.24 et les définitionssuivantes :

• public class Primes• private static final int SIZE = 1000;• private static int size = SIZE;• private static BitSet sieve = new BitSet(size);• private static int last = 1;

en incluant cet initialisateur statique qui implémente le crible d’Ératosthène :

• static• for (int i=2; i<SIZE; i++)• sieve.set(i);• for (int n=2; 2*n<SIZE; n++)• if (sieve.get(n))• for (int m=n; m*n<SIZE; m++)• sieve.clear(m*n);•

2.26 Ajoutez la méthode suivante à la classe Primes, puis testez-la :

• public static String factor(int n)• // condition préalable : n > 1• // renvoie la factorisation des nombres premiers de n;• // exemple : facteur(4840) renvoie "2*2*2*5*11*11"

2.27 Christian Goldbach (1690–1764) a supposé en 1742 que chaque nombre pair supérieur à 2 est lasomme de deux nombres premiers. Écrivez un programme qui teste la conjecture de Goldbachpour tous les nombres pairs inférieurs à 100. Utilisez la classe Primes de l’exercice 2.25. Les10 premières lignes de la sortie obtenue devraient ressembler aux lignes suivantes :

2.28 D’après les travaux de Pierre de Fermat (1601–1665), il existe un nombre infini de nombres pre-miers de forme pour un entier p. Ces nombres sont qualifiés de nombres premiersde Fermat. Par exemple, 5 est un nombre premier de Fermat parce qu’il a la forme . Écri-vez un programme capable de rechercher tous les nombres premiers de Fermat se trouvant dansl’intervalle du type int. Utilisez la classe Primes de l’exercice 2.25 et la méthode Math.pow.

•••••••••••

4 = 2+26 = 3+38 = 3+510 = 3+7 = 5+512 = 5+714 = 3+11 = 7+716 = 3+13 = 5+1118 = 5+13 = 7+1120 = 3+17 = 7+1322 = 3+19 = 5+17 = 11+11

n 22p

1+=221

1+

Page 53: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 43

Les cinq premières lignes de votre sortie devraient ressembler aux lignes suivantes :

2.29 Charles Babbage (1792–1871) a obtenu la première bourse jamais accordée en 1823 lorsqu’il apersuadé le gouvernement britannique de lui accorder 1 000 livres pour qu’il puisse construire samachine de calcul mécanique. Dans sa demande de bourse, Babbage donnait la formule x2 + x +41 comme exemple d’une fonction que sa machine serait en mesure de calculer. Cette fonctionprésentait un intérêt particulier pour les mathématiciens parce qu’elle crée un nombre inhabituelde nombres premiers. Les nombres premiers de forme n = x2 + x + 41 pour un entier x sont qua-lifiés de nombres premiers de Babbage. Écrivez un programme capable de retrouver tous lesnombres premiers de Babbage inférieurs à 10 000. Utilisez la classe Primes de l’exercice 2.25.Les cinq premières lignes de la sortie obtenue devraient ressembler aux lignes suivantes :

2.30 Deux entiers impairs consécutifs qui sont tous les deux premiers sont qualifiés de nombres pre-miers jumeaux. La conjecture des nombres premiers jumeaux consiste à dire qu’il existe un nom-bre infini de nombres premiers jumeaux. Écrivez un programme capable de rechercher tous lesnombres premiers jumeaux inférieurs à 1 000. Utilisez la classe Primes de l’exercice 2.25.Les cinq premières lignes de la sortie obtenue devraient ressembler aux lignes suivantes :

2.31 Testez la supposition selon laquelle il existe au moins un nombre premier entre chaque paire denombres aux carrés consécutifs (les nombres au carré sont 1, 4, 9, 16, 25, …). Utilisez la classePrimes de l’exercice 2.25. Les cinq premières lignes de la sortie obtenue devraient ressembleraux lignes suivantes :

2.32 Le moine minime Marin Mersenne (1588–1648) entreprit en 1644 l’étude de nombres sous laforme n = 2p – 1, p étant un nombre premier. Il était convaincu que la plupart de ces nombres nétaient premiers. Ces nombres sont maintenant appelés Nombres premiers de Mersenne. Écrivezun programme capable de rechercher tous les nombres premiers de Mersenne pour p < 30.

••••••

2^2^0 + 1 = 32^2^1 + 1 = 52^2^2 + 1 = 172^2^3 + 1 = 2572^2^4 + 1 = 65537

••••••

0 41 est un nombre premier1 43 est un nombre premier2 47 est un nombre premier3 53 est un nombre premier4 61 est un nombre premier

••••••

3 55 711 1317 1929 31

••••••

1 < 2 < 44 < 5 < 99 < 11 < 1616 < 17 < 2525 < 29 < 36

Page 54: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

44 Caractéristiques de base des tableaux

Utilisez la classe Primes de l’exercice 2.25. Les cinq premières lignes de la sortie obtenuedevraient ressembler aux lignes suivantes :

2.33 Un nombre est dit palindromique s’il ne varie pas lorsqu’il est inversé, c’est-à-dire s’il reste lemême lorsque les chiffres qui le composent sont inversés. Par exemple, 3456543 est palindromi-que. Écrivez un programme capable de vérifier chacun des 10 000 premiers nombres premiers etd’imprimer ceux qui sont palindromiques. Utilisez la classe Primes de l’exercice 2.25.

SOLUTIONS¿SOLUTIONS

2.1 Le programme test de la méthode java.util.Arrays.fill() appliquée aux tableauxd’objets est le suivant :

• import java.util.Arrays;• public class Pr0201• public static void main(String[] args)• Object[] a = new Object[4];• Double x = new Double(Math.PI);• Arrays.fill(a,x);• for (int i=0; i<a.length; i++)• System.out.println("a[" + i + "] = " + a[i]);• Arrays.fill(a,"Eclair au chocolat !");• for (int i=0; i<a.length; i++)• System.out.println("a[" + i + "] = " + a[i]);• •

2.2 Le programme test de la méthode java.util.Arrays.equals() appliquée aux tableauxd’objets est le suivant :

• import java.util.Arrays;• public class Pr0202• public static void main(String[] args)• Double x = new Double(Math.PI);• Object[] a = new Object[4];• Arrays.fill(a,x);• Object[] b = new Object[4];• Arrays.fill(b,x);• System.out.println("b.equals(a) = " + b.equals(a));• System.out.println("Arrays.equals(a,b) = "• + Arrays.equals(a,b));• Arrays.fill(a,"Eclair au chocolat !");• Arrays.fill(b,"Eclair au chocolat !");• System.out.println("b.equals(a) = " + b.equals(a));• System.out.println("Arrays.equals(a,b) = "• + Arrays.equals(a,b));• •

••••••

2 2^2-1 = 3 est un nombre premier3 2^3-1 = 7 est un nombre premier5 2^5-1 = 31 est un nombre premier7 2^7-1 = 127 est un nombre premier11 2^11-1 = 2047 est un nombre premier

Page 55: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 45

2.3 Le programme test de la méthode java.util.Arrays.equals() appliquée à des tableaux detableaux est le suivant :

• import java.util.Arrays;• public class Pr0203• public static void main(String[] args)• double[] x = Math.E, Math.PI ;• String[] s = "Nord", "Est", "Sud", "Ouest" ;• Vector[] y = new Vector[0];• Object[] a = x, s, null, y ;• Object[] b = x, s, null, y ;• System.out.println("b.equals(a) = " + b.equals(a));• System.out.println("Arrays.equals(a,b) = "• + Arrays.equals(a,b));• •

2.4 La recherche séquentielle est exécutée de façon linéaire, ce qui signifie que sa durée est propor-tionnelle au nombre d’éléments. Par conséquent, le traitement d’un tableau deux fois plus grandprendrait deux fois plus de temps, soit 20 minutes.

2.5 La recherche binaire est exécutée de façon logarithmique, c’est pourquoi la mise au carré de lataille du tableau ne fait que doubler la durée du traitement. Ainsi, un tableau composé de 1 0002

éléments sera traité en deux fois plus de temps, soit 10 minutes.

2.6 La recherche par interpolation a une durée d’exécution hyperlogarithmique. Par conséquent, lamise au carré de la taille du tableau n’affecte en aucun cas cette durée d’exécution de la recher-che qui sera d’environ 2 minutes.

2.7 Le programme test de l’algorithme de recherche séquentielle est le suivant :

• public class Pr0207• private static final int N=16;• private static int[] a = new int[N];• private static final int RANGE=2*N;• private static final int START=10;• private static Random random = new Random();• public static void main(String[] args)• load(a);• print(a);• int x = random.nextInt(RANGE) + START;• System.out.println("x = " + x);• int i = sequentialSearch(a,x);• System.out.println("recherche(a,x) = " + i);• if (i >= 0) System.out.println("a[" + i + "] = " + a[i]);• • private static void load(int[] a)• for (int i=0; i<a.length; i++)• a[i] = random.nextInt(RANGE) + START;• • private static void print(int[] a)• for (int i=0; i<a.length; i++)• System.out.print(" " + (i>9?"":" ") + i);• System.out.print("\n " + a[0]);• for (int i=1; i<a.length; i++)• System.out.print(", " + a[i]);• System.out.println(" ");• • public static int sequentialSearch(int[] a, int x)• // Recherche séquentielle :• for (int i=0; i<a.length; i++)

Page 56: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

46 Caractéristiques de base des tableaux

• if (a[i]==x) return i;• return -1;• •

2.8 Le programme test de la méthode isSorted(int[]) est le suivant :

• public class Pr0208• private static final int SIZE = 16;• private static int[] a = new int[SIZE];• public static void main(String[] args)• schaums.dswj.Arrays.load(a,40,20);• schaums.dswj.Arrays.print(a);• System.out.println("isSorted(a) = " + isSorted(a));• java.util.Arrays.sort(a);• schaums.dswj.Arrays.print(a);• System.out.println("isSorted(a) = " + isSorted(a));• • private static boolean isSorted(int[] a)• if (a.length<2) return true;• for (int i=1; i<a.length; i++)• if (a[i]<a[i-1]) return false;• return true;• •

2.9 Le programme test de la méthode minimum(int[]) est le suivant :

• public class Pr0209• private static final int SIZE = 8;• private static int[] a = new int[SIZE];• public static void main(String[] args)• schaums.dswj.Arrays.load(a,20,80);• schaums.dswj.Arrays.print(a);• System.out.println("minimum(a) = " + minimum(a));• • private static int minimum(int[] a)• int min = a[0];• for (int i=1; i<a.length; i++)• if (a[i]<min) min = a[i];• return min;• •

2.10 Le programme test de la méthode mean(double[]) est le suivant :

• public class Pr0210• private static final int SIZE = 4;• private static double[] a = new double[SIZE];• public static void main(String[] args)• schaums.dswj.Arrays.load(a,20,10);• schaums.dswj.Arrays.print(a);• System.out.println("mean(a) = " + mean(a));• • private static double mean(double[] a)• double sum=0.0;• for (int i=0; i<a.length; i++)• sum += a[i];• return sum/a.length;• •

Page 57: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 47

2.11 Le programme test de la méthode withoutDuplicates(int[]) est le suivant :

• public class Pr0211• private static final int SIZE = 16;• private static int[] a = new int[SIZE];• public static void main(String[] args)• schaums.dswj.Arrays.load(a,10,8);• schaums.dswj.Arrays.print(a);• int[] b = withoutDuplicates(a);• schaums.dswj.Arrays.print(b);• • private static int[] withoutDuplicates(int[] a)• if (a.length<2) return a; // il n’y a aucune copie• int x = a[0]; // utiliser cette valeur comme marqueur factice• for (int i=1; i<a.length-1; i++)• for (int j=i+1; j<a.length; j++)• if (a[j]==a[i]) a[j] = x;• int count=0; // compter les copies• for (int i=1; i<a.length; i++)• if (a[i]==x) ++count;• if (count==0) return a; // il n’y a aucune copie• int[] b = new int[a.length-count];• b[0] = x;• int shift=0;• for (int i=1; i<a.length; i++)• if (shift>0 && a[i] != x) b[i-shift] = a[i];• else if (a[i]==x) ++shift;• else b[i] = a[i];• return b;• •

2.12 Le programme test de la méthode withoutDuplicates(Object[]) est le suivant :

• public class Pr0212• private static final int SIZE = 16;• private static Object[] a = new Object[SIZE];• public static void main(String[] args)• schaums.dswj.Arrays.load(a,10,8);• schaums.dswj.Arrays.print(a);• Object[] b = withoutDuplicates(a);• schaums.dswj.Arrays.print(b);• • private static Object[] withoutDuplicates(Object[] a)• if (a.length<2) return a; // il n’y a aucune copie• Object x = a[0]; // utiliser cet objet comme marqueur factice• for (int i=1; i<a.length-1; i++)• for (int j=i+1; j<a.length; j++)• if (a[j].equals(a[i]))• a[j] = x;• int count=0; // compter les copies• for (int i=1; i<a.length; i++)• if (a[i].equals(x)) ++count;• if (count==0) return a; // il n’y a aucune copie• Object[] b = new Object[a.length-count];• b[0] = x;• int shift=0;• for (int i=1; i<a.length; i++)• if (shift>0 && !a[i].equals(x)) b[i-shift] = a[i];• else if (a[i].equals(x)) ++shift;• else b[i] = a[i];

Page 58: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

48 Caractéristiques de base des tableaux

• return b;• •

2.13 Le programme test de la méthode reverse(int[]) est le suivant :

• public class Pr0213• private static final int SIZE = 16;• private static int[] a = new int[SIZE];• public static void main(String[] args)• schaums.dswj.Arrays.load(a,60,40);• schaums.dswj.Arrays.print(a);• reverse(a);• schaums.dswj.Arrays.print(a);• • private static void reverse(int[] a)• if (a.length<2) return;• for (int i=0; i<a.length/2; i++)• schaums.dswj.Arrays.swap(a,i,a.length-1-i);• •

2.14 Le programme test de la méthode concatenate(Object[],Object[]) est le suivant :

• public class Pr0214• private static final int SIZE = 8;• private static Object[] a = new Object[SIZE];• private static Object[] b = new Object[SIZE];• public static void main(String[] args)• schaums.dswj.Arrays.load(a,10,40);• schaums.dswj.Arrays.load(b,60,40);• schaums.dswj.Arrays.print("a",a);• schaums.dswj.Arrays.print("b",b);• Object[] c = concatenate(a,b);• schaums.dswj.Arrays.print("a",a);• schaums.dswj.Arrays.print("b",b);• schaums.dswj.Arrays.print("c",c);• • private static Object[] concatenate(Object[] a, Object[] b)• Object[] c = new Object[a.length+b.length];• for (int i=0; i<a.length; i++)• c[i] = a[i];• for (int i=0; i<b.length; i++)• c[i+a.length] = b[i];• return c;• •

2.15 Le programme test de la méthode shuffle(Object[]) est le suivant :

• public class Pr0215• private static final int SIZE = 16;• private static Object[] a = new Object[SIZE];•• public static void main(String[] args)• schaums.dswj.Arrays.load(a,10,90);• schaums.dswj.Arrays.print(a);• shuffle(a);• schaums.dswj.Arrays.print(a);• • private static void shuffle(Object[] a)

Page 59: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 49

• Random random = new Random();• for (int i=0; i<a.length; i++)• schaums.dswj.Arrays.swap(a,i,random.nextInt(a.length));• •

2.16 Le programme test de la méthode tally(String) est le suivant :

• public class Pr0216• public static void main(String[] args)• String string="Bienvenue dans le nouveau millénaire";• System.out.println(string);• int[] t = tally(string);• for (int i=0; i<26; i++)• System.out.println("Fréquence de " + (char)(’A’+i)• + " = " + t[i]);• • private static int[] tally(String s)• int[] frequency = new int[26];• for (int i=0; i<s.length(); i++)• char ch = Character.toUpperCase(s.charAt(i));• if (Character.isLetter(ch))• ++frequency[(int)ch - (int)’A’]; // compter ch• • return frequency;• •

2.17 Le programme test de la méthode innerProduct(double[],double[]) est le suivant :

• public class Pr0217• public static void main(String[] args)• double[] x = 1.1, 2.2, 3.3, 4.4 ;• double[] y = 2.0, 0.0, 1.0, -1.0 ;• System.out.println("innerProduct(x,y) = "• + innerProduct(x,y));• • private static double innerProduct(double[] x, double[] y)• double sum=0.0;• for (int i=0; i<x.length && i<y.length; i++)• sum += x[i]*y[i];• return sum;• •

2.18 Le programme test de la méthode outerProduct(double[],double[]) est le suivant :

• public class Pr0218• public static void main(String[] args)• double[] x = 1.1, 2.2, 3.3, 4.4 ;• double[] y = 2.0, 0.0, -1.0 ;• double[][] z = outerProduct(x,y);• for (int i=0; i<x.length; i++)• for (int j=0; j<y.length; j++)• System.out.print("\t" + z[i][j]);• System.out.println();• • • private static double[][] outerProduct(double[] x, double[] y)• double[][] z = new double[x.length][y.length];• for (int i=0; i<x.length; i++)

Page 60: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

50 Caractéristiques de base des tableaux

• for (int j=0; j<y.length; j++)• z[i][j] = x[i]*y[j];• return z;• •

2.19 Le programme test de la méthode product(double[][],double[][]) est le suivant :

• public class Pr0219• public static void main(String[] args)• double[][] x = 1.0, 2.0 ,• 3.0, 4.0 ;• double[][] y = 20.0, -10.0 ,• 10.0, 20.0 ;• double[][] z = product(x,y);• for (int i=0; i<x.length; i++)• for (int j=0; j<y.length; j++)• System.out.print("\t" + z[i][j]);• System.out.println();• • • private static double[][] product(double[][] x, double[][] y)• double[][] z = new double[x.length][y[0].length];• for (int i=0; i<x.length; i++)• for (int j=0; j<y[0].length; j++)• double sum=0.0;• for (int k=0; k<x[0].length; k++)• sum += x[i][k]*y[k][j];• z[i][j] = sum;• • return z;• •

2.20 Le programme test de la méthode transpose(double[][]) est le suivant :

• public class Pr0220• public static void main(String[] args)• double[][] x = 1.0, 2.0, 3.0 , 4.0, 5.0, 6.0 ;• double[][] y = transpose(x);• for (int i=0; i<y.length; i++)• for (int j=0; j<y[0].length; j++)• System.out.print("\t" + y[i][j]);• System.out.println();• • • private static double[][] transpose(double[][] x)• double[][] y = new double[x[0].length][x.length];• for (int i=0; i<x[0].length; i++)• for (int j=0; j<x.length; j++)• y[i][j] = x[j][i];• return y;• •

2.21 Le programme test qui renvoie le triangle de Pascal est le suivant :

• public class Pr0221• private static final int N=9;• public static void main(String[] args)• int[][] p = pascal(N);

Page 61: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 51

• for (int i=0; i<N; i++)• for (int j=0; j<N; j++)• System.out.print("\t" + p[i][j]);• System.out.println();• • • private static int[][] pascal(int n)• int[][] p = new int[n][n];• for (int j=0; j<n; j++)• p[j][0] = p[j][j] = 1;• for (int i=2; i<n; i++)• for (int j=1; j<i; j++)• p[i][j] = p[i-1][j-1] + p[i-1][j];• return p;• •

2.22 Le crible d’Ératosthène est le suivant :

• public class Pr0222• private static final int SIZE=1000;• private static boolean[] sieve = new boolean[SIZE];• public static void main(String[] args)• initializeSieve();• printSieve();• • private static void initializeSieve()• for (int i=2; i<SIZE; i++)• sieve[i] = true;• for (int n=2; 2*n<SIZE; n++)• if (sieve[n])• for (int m=n; m*n<SIZE; m++)• sieve[m*n] = false;• • private static void printSieve()• int n=0;• for (int i=0; i<SIZE; i++)• if (sieve[i]) System.out.print((n++%10==0?"\n":"\t")+i);• System.out.println("\n" + n • + " nombres premiers inférieurs à "+ SIZE);• •

2.23 Le crible d’Ératosthène avec un objet java.util.Vector est le suivant :

• import java.util.Vector;• public class Pr0223• private static final int SIZE=1000;• private static Vector sieve = new Vector(SIZE);• public static void main(String[] args)• initializeSieve();• printSieve();• • private static void initializeSieve()• sieve.add(Boolean.FALSE);• sieve.add(Boolean.FALSE);• for (int i=2; i<SIZE; i++)• sieve.add(Boolean.TRUE);• for (int n=2; 2*n<SIZE; n++)• if (((Boolean)sieve.get(n)).booleanValue())• for (int m=n; m*n<SIZE; m++)

Page 62: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

52 Caractéristiques de base des tableaux

• sieve.set(m*n,Boolean.FALSE);• •• private static void printSieve()• int n=0;• for (int i=0; i<SIZE; i++)• if (((Boolean)sieve.get(i)).booleanValue())• System.out.print((n++%10==0?"\n":"\t")+i);• System.out.println("\n" + n • + " nombres premiers inférieurs à " + SIZE);• •

2.24 Le crible d’Ératosthène avec un objet java.util.BitSet :

• import java.util.BitSet;• public class Pr0224• private static final int SIZE=1000;• private static BitSet sieve = new BitSet(SIZE);• public static void main(String[] args)• initializeSieve();• printSieve();• • private static void initializeSieve()• for (int i=2; i<SIZE; i++)• sieve.set(i);• for (int n=2; 2*n<SIZE; n++)• if (sieve.get(n))• for (int m=n; m*n<SIZE; m++)• sieve.clear(m*n);• • private static void printSieve()• int n=0;• for (int i=0; i<SIZE; i++)• if (sieve.get(i))• System.out.print((n++%10==0?"\n":"\t")+i);• System.out.println("\n" + n • + " nombres premiers inférieurs à " + SIZE);• •

2.25 La classe Primes est définie de la façon suivante :

• package schaums.dswj;• import java.util.*;• public class Primes• private static final int SIZE = 1000;• private static int size = SIZE;• private static BitSet sieve = new BitSet(size);• private static int last = 1;• static• for (int i=2; i<SIZE; i++)• sieve.set(i);• for (int n=2; 2*n<SIZE; n++)• if (sieve.get(n))• for (int m=n; m*n<SIZE; m++)• sieve.clear(m*n);• • private Primes()• •

Page 63: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 53

• public static void setLast(int n)• last = n;• • public static void setLast()• last = 1;• • public static void setSize(int n)• size = n;• • public static void setSize()• size = 1000;• • public static boolean isPrime(int n)• return sieve.get(n);• • public static int next()• while (++last<size)• if (sieve.get(last)) return last;• return -1;• • public static void printPrimes()• int n=0;• for (int i=0; i<SIZE; i++)• if (sieve.get(i))• System.out.print((n++%10==0?"\n":"\t")+i);• System.out.println("\n" + n • + " nombres premiers inférieurs à " + SIZE);• • •• import schaums.dswj.Primes;• public class Pr0225• public static void main(String[] args)• Primes.printPrimes();• for (int n=1; n<=10; n++)• System.out.println(n + ".\t" + Primes.next());• •

2.26 Le programme test de la méthode de factorisation des nombres premiers est le suivant :

• import schaums.dswj.Primes;• import java.util.Random;• public class Pr0226• private static final int N=10;• private static final int RANGE=2000;• private static Random random = new Random();• public static void main(String[] args)• for (int i=0; i<N; i++)• int n = random.nextInt(RANGE);• System.out.println(n + " = " + Primes.factor(n));• • •

La méthode est ajoutée de la façon suivante à la classe Primes :

• public static String factor(int n)• String primes="";• int p = next();• while (n>1)

Page 64: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

54 Caractéristiques de base des tableaux

• if (n%p==0)• primes += (primes.length()==0?"":"*") + p;• n /= p;• • else p = next();• if (p == -1)• primes += " DEBORDEMENT";• break;• • • setLast();• return primes;•

2.27 Le programme test de la conjecture de Goldbach est le suivant :

• public class Pr0227• public static void main(String[] args)• final int N=10000;• Primes.setSize(N);• System.out.println("4 = 2+2");• for (int n=6; n<100; n += 2)• System.out.print(n);• for (int p=3; p<=n/2; p += 2)• if (Primes.isPrime(p) && Primes.isPrime(n-p))• System.out.print(" = "+p+"+"+(n-p));• System.out.println();• • •

2.28 Le programme de recherche des nombres premiers de Fermat est le suivant :

• public class Pr0228• public static void main(String[] args)• final int N=10000;• Primes.setSize(N);• for (int p=0; p<5; p++)• int n = (int)Math.pow(2,Math.pow(2,p)) + 1;• if (Primes.isPrime(n))• System.out.println("p = "+p+", n = 2^2^p = "+n);• • •

2.29 Le programme de recherche des nombres premiers de Babbage est le suivant :

• public class Pr0229• public static void main(String[] args)• final int N=10000;• Primes.setSize(N);• for (int x=0; x<50; x++)• System.out.print(x);• int n = x*x + x + 41;• if (Primes.isPrime(n))• System.out.println("\t"+n+" is prime");• else System.out.println();• • •

Page 65: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 55

2.30 Le programme de recherche des nombres premiers jumeaux est le suivant :

• public class Pr0230• public static void main(String[] args)• final int N=1000;• Primes.setSize(N);• int n=Primes.next();• while (n<0.9*N)• if (Primes.isPrime(n+2))• System.out.println(n + "\t" + (n+2));• n = primes.next();• • •

2.31 Le programme de recherche des nombres premiers entre les racines carrées est le suivant :

• public class Pr0231• public static void main(String[] args)• final int N=10000;• Primes.setSize(N);• for (int n=1; n<100; n++)• for (int i=n*n+1; i<(n+1)*(n+1); i++)• if (Primes.isPrime(i))• System.out.println(n*n + " < " + i + " < " + (n+1)*(n+1));• break;• • •

2.32 Le programme de recherche des nombres premiers de Mersenne est le suivant :

• public class Pr0232• public static void main(String[] args)• final int N=10000;• Primes.setSize(N);• for (int p = Primes.next(); p<30; p = Primes.next())• int n = (int)Math.round(Math.pow(2,p)) - 1;• System.out.print(p + "\t2^" + p + "-1 = " + n);• if (Primes.isPrime(n)) • System.out.println(" est un nombre premier ");• else System.out.println(" n’est pas un nombre premier ");• • •

2.33 Le programme de recherche des nombres premiers palindromiques est le suivant :

• public class Pr0233• public static void main(String[] args)• final int N=10000;• Primes.setSize(N);• for (int i=0; i<N; i++)• int p = Primes.next();• if (isPalindromic(p)) System.out.print(p+"\t");• • System.out.println();• •• private static boolean isPalindromic(int n)• if (n<0) return false;• int p10=1;

Page 66: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

56 Caractéristiques de base des tableaux

• // faire de p10 la puissance 10 la plus importante < n• while (p10<n)• p10 *= 10;• p10 /= 10;• while (n>9)• if (n/p10 != n%10) return false;• n /= 10; // supprimer de n le chiffre le plus à droite• p10 /= 10;• n %= p10; // supprimer de n le chiffre le plus à gauche• • return true; // les entiers composés d’un chiffre sont • // palindromiques• •

Page 67: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 3

Java avancé

3.1 HÉRITAGEL’héritage des classes représente l’un des concepts centraux de la programmation orientée objet. Nousdisons que la classe Y hérite de la classe X si elle étend cette dernière en utilisant ses champs et sesméthodes en plus de la définition de ses propres champs et méthodes. Dans ce cas, nous disons égale-ment que Y est une sous-classe de X ou bien une classe enfant de X qui est alors qualifiée de classe parentou superclasse. D’un point de vue conceptuel, vous obtenez ainsi des instances de la classe Y qui sontplus spécialisées et des instances de la classe X qui sont plus générales.

Exemple 3.1 Étendre la classe Point

Le programme suivant définit PointColore comme sous-classe de la classe Point déjà vue dansl’exemple 1.7 :

public class PointColore extends Point private String couleur="noir";

public PointColore(double x, double y, String couleur) super(x,y); // appelle le constructeur parent this.couleur = couleur;

public String getCouleur() return couleur;

public void setCouleur(String couleur) this.couleur = couleur;

public boolean equals(Object object) if (object == this) return true; if (object.getClass() != this.getClass()) return false; if (object.hashCode() != this.hashCode()) return false; PointColore point = (PointColore)object; return (x == point.x && y == point.y && couleur == point.couleur);

Page 68: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

58 Java avancé

public int hashCode() return (new Double(x)).hashCode() + (new Double(y)).hashCode() + couleur.hashCode();

public String toString() return new String("(" + x + "," + y + ", " + couleur + ")");

Cette sous-classe définit son propre constructeur, un nouvel accesseur getCouleur() et un nou-veau mutateur setCouleur(String). Elle remplace les méthodes equals(Object), hash-Code() et toString() définies dans la classe parent. Remarquez que les champs x et y de laclasse Point doivent être déclarés comme protected au lieu de private afin d’être accessiblesdepuis la sous-classe PointColore. Voici le pilote test de cette sous-classe :

public class Ex0301 public static void main(String[] args) PointColore p = new PointColore(2,3,"vert"); System.out.println("p = " + p); PointColore q = new PointColore(2,3,"vert"); System.out.println("q = " + q); if (q == p) System.out.println("q == p"); else System.out.println("q != p"); if (q.equals(p)) System.out.println("q est égal à p"); else System.out.println("q n’est pas égal à p"); q.setCouleur("rouge"); System.out.println("q = " + q); if (q.equals(p)) System.out.println("q est égal à p"); else System.out.println("q n’est pas égal à p");

3.2 POLYMORPHISMEGrâce à l’héritage, l’instance d’une sous-classe peut-être considérée comme une instance de sa classeparent lorsqu’elle est passée sous forme d’un argument à une méthode. Cette capacité de prendre enapparence plusieurs formes est qualifiée de polymorphisme.

Exemple 3.2 Utiliser le polymorphisme

public class Ex302 public static void main(String[] args) PointColore p = new PointColore(2,3,"vert"); System.out.println("p = " + p); Point q = new Point(-2,0); System.out.println("q = " + q); System.out.println("distance(p,q) = " + distance(p,q));

p = (2.0,3.0, vert)q = (2.0,3.0, vert)q != pq est égal à pq = (2.0,3.0, rouge)q n’est pas égal à p

Page 69: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Polymorphisme 59

private static double distance(Point p, Point q) double dx = p.getX() - q.getX(); double dy = p.getY() - q.getY(); return Math.sqrt(dx*dx + dy*dy);

L’objet p est une instance de la classe PointColore. Mais la méthode distance(Point,Point)permet son passage au premier paramètre sous forme d’une instance de la classe parent Point.C’est pourquoi p semble avoir plusieurs formes (PointColore et Point), c’est-à-dire qu’il sembleêtre polymorphique.

Toutes les classes Java, qu’elles fassent partie dela bibliothèque standard ou qu’elles soient créées parle programmeur, résident dans une hiérarchie d’héri-tage de grande taille. Cette hiérarchie se présentesous la forme d’une structure arborescente avec laclasse java.lang.Object à sa racine. Chaqueautre classe se trouve sous une classe parent unique.Ainsi, chaque classe Java est directement ou indirec-tement une sous-classe de la classe Object.

L’arbre ci-contre illustre l’organisation de 40 des1 462 classes qui constituent la bibliothèque stan-dard Java 1.2 (soit moins de 3 % d’entre elles).

Vous pouvez notamment constater que Integerest une sous-classe de Number, qui est une sous-classe de la classe Object.

Cet arbre qui illustre la hiérarchie Java vous per-met également de comprendre comment fonctionnele polymorphisme. Étant donné que les instances dechaque classe enfant peuvent être passées à un para-mètre dont le type est une classe ancêtre, il suffitsimplement de suivre les chemins qui vont desfeuilles à la racine Object pour voir à quoi ressem-ble un objet polymorphique. Par exemple, un objetFrame peut être passé à un paramètre de typeFrame, Window, Container, Component ouObject.

En raison de cette hiérarchie Java, le polymor-phisme signifie que tout objet peut être passé à unparamètre de type Object et que tout objet peutappeler n’importe quelle méthode définie dans laclasse Object (puisqu’aucune d’entre elles n’estprivate).

p = (2.0,3.0, vert)q = (-2.0,0.0)distance(p,q) = 5.0

Object

AbstractCollection

AbstractList

ArrayList

AbstractSequentialList

LinkedList

Vector

Stack

AbstractSet

HashSet

TreeSet

AbstractMap

HashMap

TreeMap

Arrays

BitSet

Collections

Dictionary

Hashtable

Properties

WeakHashMap

Reader

InputStreamReader

FileReader

Component

Button

Window

Date

File

Math

Number

Integer

Double

Container

Panel

Frame

Random

System

StringTokenizer

String

Page 70: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

60 Java avancé

3.3 CONVERSION DES TYPESDans le cadre de Java, chaque expression a un type qui peut être converti à l’aide de différents mécanismes.

L’opérateur de concaténation + de type String convertit automatiquement les autres types enString, comme dans l’exemple suivant :

int n = 44;String s = "n = " + n;

Dans l’expression "n = " + n, l’opérateur + convertit int n en objet String "44". Ce procédé estqualifié de conversion de chaîne.

Si le type de la classe est différent de String, la méthode toString() de cette classe est appeléepour effectuer la conversion. Cette opération peut être héritée de la classe Object.

Lorsque plusieurs types numériques sont utilisés dans une expression numérique, les valeurs de typeles plus petites sont automatiquement converties en valeurs de type plus grandes, par exemple :

double x = 3.14159/4;

L’expression 3.14159/4 contient la valeur double 3.14159 et la valeur 4 de type int ; c’est pour-quoi la valeur int est convertie en type double avant l’opération de division. Il s’agit d’une promotionnumérique.

Le même processus de conversion a lieu lorsqu’un type plus petit est affecté à un type plus grand,comme dans l’exemple suivant :

double x = 44;

Cette initialisation affecte la valeur 44 de type int à la variable x de type double. Avant l’affecta-tion, la valeur int est convertie dans son équivalent de type double. Il s’agit de la conversion de l’affec-tation. Cette technique peut également être utilisée entre des types de référence :

Exemple 3.3 Conversion de l’affectation

class X int a; class Y extends X int b;

public class Ex0303 public static void main(String[] args) X x = new X(); System.out.println("x.getClass() = "+ x.getClass()); Y y = new Y(); System.out.println("y.getClass() = "+ y.getClass()); x = y; // conversion de l’affectation de x en type Y

Conversions primitives élargissantes

float

long

int

shortchar

byte

Conversions primitives rétrécissantes

double

float

long

int

short

bytechar

Page 71: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Conversion des types 61

System.out.println("x.getClass() = "+ x.getClass());

L’affectation x = y attribue le type le plus petit Y au type le plus grand X et convertit ainsi x entype Y.

Le polymorphisme décrit dans la section 2.3 est qualifié de conversion de l’appel de méthode parcequ’un type est converti dans un autre lorsqu’il est passé à une méthode appelée. Ce type de conversion aégalement lieu avec les types primitifs. Par exemple :

int n = 44;double y = Math.sqrt(n); // conversion de l’appel de méthode

Dans le cas présent, la valeur 44 de type int est convertie en double lorsque la méthodeMath.sqrt(double) est appelée parce que son paramètre est de type double.

Pour finir, il reste à préciser que les types peuvent être convertis explicitement à l’aide de l’opérateurde conversion de type.

Exemple 3.4 Transtyper les types de référence

class X public String toString() return "Je suis un X."; class Y extends X public String toString() return "Je suis un Y.";

public class Ex0304 public static void main(String[] args) Y y = new Y(); System.out.println("y: " + y); X x = y; System.out.println("x: " + x); Y yy = (Y)x; // transtype x en type Y System.out.println("yy: " + yy); X xx = new X(); System.out.println("xx: " + xx); yy = (Y)xx; // Erreur d’exécution : xx ne peut pas être // transtypé en y System.out.println("yy: " + yy);

La conversion yy = (Y)x convertit x en type Y. Cette opération fonctionne parce que la valeur de xpeut être interprétée pour le type Y étant donné qu’elle provient initialement de y. La conversionyy = (Y)xx convertit xx en type Y. La compilation a lieu mais, au moment de l’exécution, l’excep-tion ClassCastException est lancée parce que la valeur de xx ne peut pas être interprétée pourle type Y.

x.getClass() = class Xy.getClass() = class Yx.getClass() = class Y

y: Je suis un Y.x: Je suis un Y.yy: Je suis un Y.xx: Je suis un X.Exception in thread "main" java.lang.ClassCastException: Xat Testing.main(Testing.java)

Page 72: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

62 Java avancé

Le transtypage de l’exemple 3.4 illustre des conversions rétrécissantes de types de référence se sol-dant par une réussite ou par un échec. L’exemple suivant vous permettra de constater qu’il est égalementpossible d’effectuer des conversions rétrécissantes de types primitifs réussies ou non.

Exemple 3.5 Transtyper les types primitifs

public class Ex0305 public static void main(String[] args) double x = 44.0; System.out.println("x = " + x); float y = (float)x; // transtypage rétrécissant de double en float System.out.println("y = " + y); int n = (int)y; // transtypage rétrécissant de float en int System.out.println("n = " + n); short m = (short)n; // transtypage rétrécissant de int en short System.out.println("m = " + m); n = 65536 + 4444; System.out.println("n = " + n); m = (short)n; // transtypage rétrécissant de int en short System.out.println("m = " + m); y = (float)m; // transtypage élargissant de short en float System.out.println("y = " + y); y = (float)n; // transtypage élargissant de int en float System.out.println("y = " + y);

Les trois premiers transtypages rétrécissants sont réussis parce que la valeur 44 est appropriée pourles types les plus petits. Cependant, la valeur int 69980 (65536 + 4444) n’est pas transtypée enshort : la valeur erronée 4444 est affectée à m. En effet, le rétrécissement de int en short créetoujours le reste issu de la division de la valeur int par 65 536, soit 216. De la même façon, le transtypage élargissant y = (float)m de short en float est réussi tandisque le transtypage élargissant y = (float)n de int en float échoue parce que la valeur int69980 comprend trop de chiffres significatifs pour le type float. Remarquez que cette valeurnumérique est identique à celle obtenue en rétrécissant int en short.

3.4 CLASSE ObjectLa classe Object est la classe ancêtre dominante en Java. Toutes les autres classes en sont dérivées.Chaque méthode de la classe Object est héritée par toutes les autres classes. Vous trouverez ci-après laliste des méthodes les plus importantes :

public class Object protected Object clone() public boolean equals(Object) public final Class getClass()

x = 44.0y = 44.0n = 44m = 44n = 69980m = 4444y = 4444.0y = 69980.0

Page 73: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Object 63

protective int hashCode() public Object() public String toString()

La plupart de ces méthodes seront remplacées par des sous-classes.La méthode clone() renvoie une copie de l’objet. Si la sous-classe ne remplace pas cette méthode,

elle se contente d’effectuer une copie champ par champ.

Exemple 3.6 Clonage incorrect

class Widget implements Cloneable int n; Widget w; Widget(int n) this.n = n;

public Object clone() throws CloneNotSupportedException return super.clone(); public String toString() return "(" + n + "," + w.n + ")";

public class Ex0206 public static void main(String[] args) throws CloneNotSupportedException Widget x = new Widget(44); x.w = new Widget(66); System.out.println("x = " + x); Widget y = (Widget)x.clone(); System.out.println("y = " + y); x.n = 55; x.w.n = 77; System.out.println("x = " + x); System.out.println("y = " + y);

r

Le widget x est construit avec le champ ninitialisé à 44. Puis le champ w est instan-cié directement dans la deuxième ligne,ce qui permet d’initialiser x.w.n à 66.Le widget y est construit comme clonede x. La méthode Object.clone()copie simplement les champs n et w de x,sans instancier d’autre widget pour y.w.C’est la raison pour laquelle le widget référencé par x.w n’est pas copié.Lorsque x.n prend la valeur 55, cela n’affecte en aucun cas y.n qui conserve sa valeur 44. Maislorsque x.w.n prend la valeur 77, il s’agit également d’une valeur de y.w.n, c’est pourquoi y n’estpas complètement indépendant de x. Le clonage est donc incomplet.

xx = (44,66)yy = (44,66)xx = (55,77)yy = (44,77)

nint

w

44

Widget

yWidget

nint

w

44

Widget

xWidget

nint

w

66

Widget

Page 74: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

64 Java avancé

Exemple 3.7 Clonage correct

Essayons maintenant de remplacer la méthode clone() de l’exemple 3.6 par le programme suivant :

public Object clone() throws CloneNotSupportedException Widget newWidget = new Widget(this.n); Widget y = newWidget; Widget t = this.w; while (t != null) y.w = new Widget(t.n); y = y.w; t = t.w; return newWidget;

Ce programme ne copie pas uniquementle widget, mais également chaque wid-get associé au widget this. C’est la rai-son pour laquelle aucun widget clonén’est complètement indépendant de cewidget. Par conséquent, la modificationd’un widget n’a aucun effet sur l’autre.

La méthode equals() doit être remplacée de la même façon que la méthode clone(). Tous leséléments liés correspondants doivent être comparés (reportez-vous à l’exemple 1.7 et à l’exerciced’entraînement 3.4).

La méthode getClass() renvoie un objet Class qui représente la classe instanciée par l’objetthis. Elle est particulièrement utile lorsque vous souhaitez tester l’égalité des objets (reportez-vous àl’exemple 1.7 et à l’exercice d’entraînement 3.4).

La méthode hashCode() renvoie un int qui fonctionne comme un code d’identification de l’objet.Les hash codes de différents objets sont susceptibles de varier. Ceux des objets composés doivent êtrecalculés à partir des hash codes de leurs composants (reportez-vous à l’exemple 1.7 et à l’exerciced’entraînement 3.5).

La méthode toString() renvoie un type String qui affiche le contenu de l’objet (reportez-vousaux exemples 1.7 et 3.6).

3.5 CLASSES ABSTRAITESUne classe abstraite comprend au moins une méthode abstraite. Une méthode abstraite ne comporte pasd’implémentation, mais uniquement une déclaration d’en-tête. C’est le mot-clé abstract permetd’identifier les classes et les méthodes abstraites.

Exemple 3.8 Classe Sequence déclarée comme abstract

public abstract class Sequence protected int length=0;

public int getLength()

xx = (44,66)yy = (44,66)xx = (55,77)yy = (44,66)

nint

w

44

Widget

yWidget

nint

w

55

Widget

xWidget

nint

w

77

Widget

nint

w

66

Widget

Page 75: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classes abstraites 65

return length;

public abstract void append(Object object); // ajoute l’objet donné à la fin de la séquence

public int count(Object object) int c=0; for (int i=0; i<getLength(); i++) if (object.equals(get(i))) ++c; return c;

public abstract Object get(int index); // renvoie l’objet à l’index donné, // ou null s’il n’est pas dans la séquence

public int indexOf(Object object) for (int i=0; i<length; i++) if (object.equals(get(i))) return i; return -1;

public abstract Object remove(int index); // supprime et renvoie l’objet à l’index donné, // ou renvoie null si index >= longueur (length);

public abstract boolean remove(Object object); // supprime la première occurrence de l’objet; // renvoie thrue si l’opération est réussie;

public abstract Object set(int index, Object object); // renvoie l’objet à l’index donné après l’avoir remplacé par // l’objet donné, ou null l’opération est réussie

public String toString() if (length==0) return "()"; String s = "(" + get(0); for (int i=1; i<length; i++) s += "," + get(i); return s + ")";

Cette classe contient cinq méthodes abstraites et quatre méthodes concrètes, c’est-à-dire non abstrai-tes. Elle comporte également un champ. Remarquez que chaque déclaration de méthode abstraite se termine par un point-virgule.

Le fait de définir une classe abstraite vous permet d’avoir recours à différentes implémentations decertaines méthodes qui sont terminées dans les sous-classes.

Exemple 3.9 Sous-classe ArraySequence de la classe Sequence

public class ArraySequence extends Sequence protected Object[] a; protected int capacity=16; // INVARIANT : a[i] != null pour 0 <= i < length;

public ArraySequence()

Page 76: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

66 Java avancé

a = new Object[capacity];

public void append(Object object) if (length==capacity) // doubler la capacité Object[] tmp = a; capacity *= 2; a = new Object[capacity]; for (int i=0; i<length; i++) a[i] = tmp[i]; a[length++] = object;

public Object get(int index) if (index >= length) return null; return a[index];

public Object remove(int index) if (index >= length) return null; Object x = a[index]; for (int i=index; i<length; i++) a[i] = a[i+1]; --length; return x;

public boolean remove(Object object) int i=indexOf(object); if (i == -1) return false; remove(i); return true;

public Object set(int index, Object object) if (index >= length) return null; Object x = a[index]; a[index] = object; return x;

Cette sous-classe implémente la structure de la séquence comme s’il s’agissait d’un tableaud’Objects. Elle fournit un constructeur par défaut et implémente les cinq méthodes abstraites de laclasse Sequence.En voici le pilote test :

public class Ex0309 public static void main(String[] args) ArraySequence s = new ArraySequence(); System.out.println("s = " + s); s.append("Chili"); s.append("Chine"); s.append("Congo"); s.append("Egypte"); s.append("Chine"); s.append("Inde"); s.append("Italie"); s.append("Chine");

Page 77: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Interfaces 67

System.out.println("s = " + s); System.out.println("s.getLength() = " + s.getLength()); System.out.println("s.count(\"Chine\") = " + s.count("Chine")); System.out.println("s.get(5) = " + s.get(5)); System.out.println("s.indexOf(\"Chine\") = " + s.indexOf("Chine")); System.out.println("s.remove(5) = " + s.remove(5)); System.out.println("s.remove(\"Chine\") = " + s.remove("Chine")); System.out.println("s.indexOf(\"Chine\") = " + s.indexOf("Chine")); System.out.println("s.set(2,\"Japon\") = " + s.set(2,"Japon")); System.out.println("s = " + s); System.out.println("s.getLength() = " + s.getLength());

Cet exemple utilise un tabeau pour implémenter la classe Sequence définie dans l’exemple 3.8,mais d’autres implémentations sont possibles. Par exemple, nous aurions pu utiliser une structurechaînée similaire à celle de la classe Widget définie dans l’exemple 3.6. Vous pouvez choisir ulté-rieurement la méthode d’implémentation.

3.6 INTERFACESUn type de données abstrait est une description du comportement du type de données sans détails sur sonmode d’implémentation. Dans le cadre de Java, un type de données est une classe et un type de donnéesabstrait est une interface.

Une interface est similaire à une classe abstraite, sauf qu’elle n’a pas d’implémentations de métho-des, ni de champs qui ne soient pas final. Elle sert de projet ou de contrat ; tout objet de classe quiimplémente une interface est capable de faire ce que l’interface spécifie via ses méthodes. Par consé-quent, les autres méthodes peuvent déclarer leurs paramètres de façon à avoir des types d’interface, destypes de classe et des types primitifs. Le compilateur autorise le passage d’un objet à ce genre de para-mètre tant qu’il s’agit d’une classe qui implémente l’interface, même dans le cas d’une classe abstraite.

Exemple 3.10 Interface Sequence

Le programme suivant illustre le cas d’une interface qui pourrait être utilisée à la place de la classeabstraite définie dans l’exemple 3.8 :

public interface Sequential public abstract void append(Object object); // ajoute l’objet donné à la fin de la séquence

public int count(Object object); // renvoie le nombre d’occurrences de l’objet // donné dans la séquence

s = ()s = (Chili,Chine,Congo,Egypte,Chine,Inde,Italie,Chine)s.getLength() = 8s.count("Chine") = 3s.get(5) = Indes.indexOf("Chine") = 1s.remove(5) = Indes.remove("Chine") = trues.indexOf("Chine") = 3s.set(2,"Japon") = Egyptes = (Chili,Congo,Japon,Chine,Italie,Chine)s.getLength() = 6

Page 78: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

68 Java avancé

public abstract Object get(int index); // renvoie l’objet à l’index donné, // ou null s’il n’est pas dans la séquence

public int getLength(); // renvoie le nombre d’éléments de la séquence

public int indexOf(Object object); // renvoie le nombre d’éléments précédant // la première occurrence de l’objet donné dans la séquence, // ou -1 si l’objet donné n’est pas dans la séquence

public abstract Object remove(int index); // supprime et renvoie l’objet à l’index donné, // ou renvoie null si l’index >= longueur (length);

public abstract boolean remove(Object object); // supprime la première occurrence de l’objet; // renvoie thrue si l’opération est réussie;

public abstract Object set(int index, Object object); // renvoie l’objet à l’index donné après l’avoir remplacé par // l’objet donné, ou null si l’opération échoue

public String toString(); // renvoie une chaîne qui affiche le contenu de la séquence

Remarquez que les interfaces ne contiennent pas de code exécutable. Par définition, elles ne peuventcontenir que des déclarations de méthodes et des définitions de constantes.

Remarquez également que, à l’instar de la bibliothèque Java standard, nous utilisons des formesnominatives pour les noms de classes et des adjectifs pour ceux des interfaces. Il est ainsi plus aisé de serappeler qu’une classe est un type, tandis qu’une interface est une description de comportement. Parexemple, l’interface Sequential spécifie comment les instances de types qui l’implémentent doiventse comporter, c’est-à-dire comme des structures de données séquentielles.

Les interfaces peuvent être utilisées comme les classes pour déclarer des variables et des paramètres,de la façon suivante :

public static void print(Sequential sequence) for (int i=0; i<sequence.getLength(); i++) System.out.println(i + ": " + sequence.get(i));

Ce code est compilé et aucune implémentation de l’interface Sequential n’est nécessaire. Cepen-dant, pour être utile, une interface doit être implémentée. Pour cela, il suffit de définir une classe compre-nant la définition de toutes les déclarations de méthodes spécifiées dans l’interface et d’ajouter la clauseimplements à la déclaration de classe.

Exemple 3.11 Implémenter l’interface Sequential à l’aide de la classe ArraySequence

public class ArraySequence implements Sequential protected int length=0; protected Object[] a; protected int capacity=16; // INVARIANT : a[i] != null pour 0 <= i < length;

public ArraySequence() a = new Object[capacity];

Page 79: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Interfaces 69

public void append(Object object) if (length==capacity) // doubler la capacité Object[] tmp = a; capacity *= 2; a = new Object[capacity]; for (int i=0; i<length; i++) a[i] = tmp[i]; a[length++] = object;

public int count(Object object) int c=0; for (int i=0; i<getLength(); i++) if (object.equals(get(i))) ++c; return c;

public Object get(int index) if (index >= length) return null; return a[index];

public int getLength() return length;

public int indexOf(Object object) for (int i=0; i<length; i++) if (object.equals(get(i))) return i; return -1;

public Object remove(int index) if (index >= length) return null; Object x = a[index]; for (int i=index; i<length; i++) a[i] = a[i+1]; --length; return x;

public boolean remove(Object object) int i=indexOf(object); if (i == -1) return false; remove(i); return true;

public Object set(int index, Object object) if (index >= length) return null; Object x = a[index]; a[index] = object; return x;

public String toString() if (length==0) return "()"; String s = "(" + get(0); for (int i=1; i<length; i++) s += "," + get(i); return s + ")";

Page 80: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

70 Java avancé

Remarquez que l’exemple 3.11 utilise le même code que celui des exemples 3.8 et 3.9. Cela vous per-met de constater que la classe abstraite et l’interface se différencient essentiellement par l’emplacement ducode : dans une interface, tout le code exécutable doit se trouver dans l’implémentation, tandis que la classeabstraite peut avoir une partie du code exécutable dans l’implémentation et le reste dans la sous-classe.

Les interfaces sont moins souples que les classes abstraites parce qu’elles ne peuvent pas inclure decode exécutable, mais elles sont également plus souples qu’elles car plusieurs interfaces peuvent êtreimplémentées par une seule classe. Java ne permet pas l’héritage multiple, c’est-à-dire une classe quiétend plusieurs autres classes.

3.7 PAQUETAGESUn paquetage est une collection de classes et d’interfaces. Chaque nom de classe ou d’interface doit êtreunique dans un paquetage, mais le même nom peut être utilisé pour différentes classes ou interfaces depaquetages différents. Par exemple, il existe deux classes Object dans la bibliothèque standard Java : lapremière se trouve dans le paquetage java.lang et l’autre dans java.org.omg.CORBA. D’un pointde vue technique, ces deux noms sont différents parce qu’ils s’écrivent ainsi lorsqu’ils sont complets :java.lang.Object et java.org.omg.CORBA.Object. En d’autres termes, le nom du paquetageauquel la classe appartient est en fait le préfixe du nom de la classe.

Les paquetages sont organisés en hiérarchies arborescentes, exactementcomme les classes, les interfaces et les répertoires de fichiers (dossiers).En fait, les programmeurs Java mappent généralement chaque paquetagedans un répertoire unique avec la hiérarchie de paquetage correspondant àcelle du répertoire. Ainsi, par exemple, tous les fichiers de classe du paque-tage java.awt.event seraient stockés dans le répertoire java/awt/event/ (ou dossier java\awt\event\ sous Windows). La hiérarchiede certains sous-paquetages Java est illustrée dans l’arbre ci-contre.

La plupart des environnements de développement Java (reportez-vousà l’annexe C) conseillent l’utilisation des paquetages. Ils mappent leschamps du nom de paquetage au chemin du sous-répertoire vers le réper-toire dans lequel le paquetage est stocké. Par exemple, un paquetageappelé books.schaums.dswj.util aurait un chemin d’accès Win-dows appelé books\schaums\dswj\util.java.

3.8 GÉRER LES EXCEPTIONSLorsque vous développez vos programmes, n’oubliezpas que le moindre détail peut être source d’erreur.Une exception est une erreur d’exécution qui provo-que le transfert de l’exécution du programme de lasource de la condition vers un gestionnaire d’excep-tions. La source de la condition est qualifiée de lan-ceur d’exception et le gestionnaire d’attrapeur.

Les erreurs d’exécution sont généralement encap-sulées sous forme d’instances de la classe Throwa-ble et de ses sous-classes. Les deux sous-classes quien dérivent immédiatement sont les classes Error etException. La hiérarchie d’héritage suivante illus-tre en détail la classification des exceptions en fonc-tion des conditions qui les provoquent.

awt

event

image

renderablebeans

beancontextio

util

jar

lang

ref

math

zip

reflect

java

applet

color

Object

Throwable

Error

ThreadDeath

LinkageError

IllegalArgumentException

VirtualMachineError

ArithmeticException

Exception

ClassNotFoundException

IOException

NoSuchFieldException

FileNotFoundException

EOFException

NoSuchMethodException

RuntimeException

NullPointerException

IndexOutOfBoundsException

Page 81: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Gérer les exceptions 71

Les deux exemples suivants vous indiquent deux méthodes de gestion d’une exception : en l’attra-pant à l’aide de l’instruction try..catch, ou en la passant à l’appelant de l’environnement courant grâceà l’insertion de la clause throw dans la méthode courante.

Exemple 3.12 Une exception attrapée

public class Ex0312 public static void main(String[] args) try int n = Integer.parseInt("Abacadabra !"); System.out.println("n = " + n);

catch (Exception e) System.out.println("e = " + e);

L’appel Integer.parseInt("Abacadabra!") échoue parce que la chaîne passée ne représentepas un entier. La méthode lance un objet NumberFormatException qui est attrapé par la méthodemain(). Les instructions exécutables du bloc catch sont exécutées lorsque l’exception est attrapée.

Exemple 3.13 Une exception non attrapée

public class Ex0313 public static void main(String[] args) System.out.println("Essayez ça..."); int n = Integer.parseInt("Abacadabra !"); System.out.println("n = " + n);

Ce programme essaie d’effectuer le même appel de méthode que celui de l’exemple 3.12, mais horsd’un bloc try. C’est la raison pour laquelle le programmeur perd tout contrôle de l’exécution unefois l’exception lancée. En d’autres termes, le système plante et imprime un message d’erreur.

La classe NumberFormatException est un descendant de la classe RuntimeException. Seulesles extensions de cette dernière peuvent être gérées aisément, comme c’est le cas dans l’exemple 3.13.Toutes les autres exceptions (ou tout au moins la plupart d’entre elles) doivent être attrapées par laméthode à l’origine de l’erreur ou bien par une méthode déjà appelée sur la pile appelante.

Les exceptions qui doivent être attrapées (c’est-à-dire qui ne sont pas des extensions de la classeRuntimeException) sont qualifiées d’exceptions vérifiées.

e = java.lang.NumberFormatException: Abacadabra !

Essayez ça...Exception in thread "main" java.lang.NumberFormatException:Abacadabra !at java.lang.Integer.parseInt(Integer.java:409)at java.lang.Integer.parseInt(Integer.java:458)at Testing.main(Testing.java:12)

Page 82: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

72 Java avancé

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

3.1 Qu’est-ce que l’héritage ?

3.2 Qu’est-ce que le polymorphisme ?

3.3 Qu’est-ce que le transtypage ?

3.4 Qu’est-ce que la promotion ?

3.5 Pourquoi la classe Object est-elle qualifiée de « mère de toutes les classes » en Java ?

3.6 Qu’est-ce qu’une méthode abstract ?

3.7 Qu’est-ce qu’une classe abstract ?

3.8 Quelle est la différence entre une classe abstract et une interface ?

3.9 Quelle est la différence entre une sous-classe et une extension de classe ?

3.10 Quelle est la différence entre une classe abstract et une classe concrète ?

3.11 Quelle est la différence entre une classe qui étend une autre classe et une classe qui implémenteune interface ?

3.12 Qu’est-ce qu’un bloc try ?

3.13 Quelle est la différence entre une exception vérifiée et une exception non vérifiée ?

RÉPONSES¿RÉPONSES

3.1 L’héritage décrit les relations entre une classe et une autre classe qui l’étend. Cette extensionconsiste à ajouter des champs et/ou des méthodes supplémentaires, voire d’autres méthodes quiremplacent celles définies dans la première classe. Cette dernière est appelée classe parent ousuperclasse et la classe qui en est dérivée est appelée classe enfant ou sous-classe.

3.2 Le polymorphisme fait référence à un objet qui semble se comporter comme un autre. Cet objetsimilaire est en fait le même objet qui est considéré comme un membre de sa classe parent ouancêtre.

3.3 Le transtypage correspond à l’utilisation de la valeur d’un type comme s’il s’agissait d’un autretype. Les objets ne peuvent pas modifier leurs types, mais leurs valeurs peuvent être affectées àdes objets d’un autre type tant que deux types se trouvent sur le même chemin racine-vers-feuilledans l’arbre d’héritage. Par exemple :

• String string1 = "ABCDE";• Object object = (Object)string1; // transtypage d’une chaîne • // comme objet• String string2 = (String)object; // transtypage d’un objet • // comme chaîne

3.4 La promotion est le transtypage appliqué aux types primitifs. Les valeurs des types numériquespeuvent être attribuées aux variables de type numérique supérieur. Par exemple :

• int n = 44; // promotion d’un int en float• float x = n; // promotion d’un float en double• double y = x;

Page 83: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 73

3.5 Dans le cadre du langage Java, la classe Object est la seule sans classe parent. Elle se trouve àla racine de l’arbre d’héritage.

3.6 Une méthode abstract est simplement la déclaration de la fonction, c’est-à-dire les modifica-teurs, le type de renvoi et la signature de la méthode

3.7 Une classe abstract a au moins une méthode abstract.

3.8 Une classe abstract fonctionne comme une interface, mais elle peut en outre avoir des défini-tions de champs et des méthodes implémentées. Les méthodes d’une classe abstract qui nesont pas implémentées doivent être déclarées à l’aide du mot-clé abstract. En revanche, uneinterface est une classe sans implémentation, c’est-à-dire qu’elle contient uniquement un listingdes signatures de méthodes, sans instructions exécutables ni définitions de champs.

3.9 Il n’y a pas de différence entre une sous-classe et une extension de classe, ces deux expressionssont synonymes.

3.10 Une classe abstract est composée d’au moins une méthode abstract (c’est-à-dire implé-mentée) alors que toutes les méthodes d’une classe concrète doivent être implémentées.

3.11 Lorsqu’une classe en étend une autre, elle hérite des méthodes de champs de cette dernière, àl’exception des méthodes qu’elle remplace. Lorsqu’une classe implémente une interface, elledoit définir toutes les méthodes dont les signatures sont spécifiées dans l’interface.

3.12 Le bloc try est le bloc d’instructions qui suit le mot-clé try d’une instruction try..catch.C’est à cet endroit que les méthodes qui lancent les exceptions vérifiées doivent être appelées àmoins que la méthode appelante ne lance elle-même l’exception plus haut dans l’arbre appelant.

3.13 Une exception vérifiée est une exception qui doit être appelée par la méthode où elle a eu lieu oupar une autre méthode située plus haut dans l’arbre appelant. Les méthodes qui lancent les excep-tions vérifiées peuvent être appelées uniquement depuis les blocs try ou depuis d’autres métho-des lançant les exceptions. Les exceptions non vérifiées sont uniquement celles qui sont dérivéesde la classe RuntimeException.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

3.1 Étendez la classe Point (reportez-vous à l’exemple 1.7) afin de créer une classe Point3D enajoutant une troisième coordonnée. Remplacez les méthodes suivantes :

• public boolean equals(Object object);• public int hashCode();• public String toString();

et ajoutez les méthodes suivantes :

• private double z;• public Point3D(double x, double y);• public Point3D(double x, double y, double z);• public Point3D(Point3D q);• public double getZ();• public void setLocation(double x, double y, double z);• public void translate(double dx, double dy, double dz);

Page 84: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

74 Java avancé

3.2 Modifiez la classe Point3D de l’exercice précédent afin d’intégrer les ajouts effectués précé-demment dans la classe Point. Remplacez les méthodes suivantes :

• public double magnitude();• public void expand(double dr);

et ajoutez les membres suivants :

• public static final Point3D ORIGIN;• public Point3D();• public double distance(Point3D point);

3.3 Essayez de deviner ce que le programme suivant imprimera. Exécutez-le afin de vérifier si vousaviez raison.

• class X• protected int a;• protected int b=22;• public X()• System.out.print("X(): " + this);• System.out.print(" a = " + a + ", b = " + b);• a = 33;• b = 44;• System.out.println(" a = " + a + ", b = " + b);• • public String toString() return "Je suis un X."; • • class Y extends X• protected int c=55;• public Y()• System.out.print("Y(): " + this);• System.out.print(" a = " + a + ", b = " + b + ", c = " + c);• a = 66;• b = 77;• c = 88;• System.out.println(" a = " + a + ", b = " + b + ", c = " + c);• • public String toString() return "Je suis un Y."; • • public class Testing• public static void main(String[] args)• Y y = new Y();• •

3.4 Implémentez la méthode equals(Object) pour la classe Widget de l’exemple 3.6.

3.5 Implémentez la méthode hashCode() pour la classe Widget de l’exemple 3.6.

SOLUTIONS¿SOLUTIONS

3.1 Vous obtenez la classe Point3D suivante en étendant la classe Point :

• public class Point3D extends Point• protected double z;• public Point3D(double x, double y)• this.x = x;

Page 85: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 75

• this.y = y;• this.z = 0;• • public Point3D(double x, double y, double z)• this.x = x;• this.y = y;• this.z = z;• • public Point3D(Point3D q)• this.x = q.x;• this.y = q.y;• this.z = q.z;• • public double getZ()• return z;• • public void setLocation(double x, double y, double z)• this.x = x;• this.y = y;• this.z = z;• • public void translate(double dx, double dy, double dz)• x += dx;• y += dy;• z += dz;• • public boolean equals(Object object)• if (object == this) return true;• if (object.getClass() != this.getClass()) return false;• if (object.hashCode() != this.hashCode()) return false;• Point3D point = (Point3D)object;• return (x == point.x && y == point.y && z == point.z);• • public int hashCode()• return (new Double(x)).hashCode() + (new Double(y)).hashCode()• + (new Double(z)).hashCode();• • public String toString()• return new String("(" + (float)x + "," + (float)y• + "," + (float)z + ")");• •

3.2 Vous ajoutez les membres à la classe Point3D de la façon suivante :

• public static final Point3D ORIGIN = new Point3D();• public Point3D()• this.x = 0;• this.y = 0;• this.z = 0;• • public double distance(Point3D point)• double dx = this.x - point.x;• double dy = this.y - point.y;• double dz = this.z - point.z;• return Math.sqrt(dx*dx+dy*dy+dz*dz);• • public double magnitude()• return distance(ORIGIN);•

Page 86: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

76 Java avancé

• public void expand(double dr)• x *= dr;• y *= dr;• z *= dr;•

3.3 L’ordre des événements est le suivant :

1. Paramétrage des champs à l’aide des valeurs par défaut.

2. Appel du constructeur Y().

3. Appel du constructeur X().

4. Appel du constructeur Object.

5. Initialisation des champs X.

6. Exécution du constructeur X().

7. Initialisation des champs Y.

8. Exécution du constructeur Y().

Vous obtenez alors la sortie suivante :

3.4 La méthode equals(Object) est implémentée de la façon suivante pour la classe Widget :

• public boolean equals(Object object)• if (object == this) return true;• if (object.getClass() != this.getClass()) return false;• Widget y = (Widget)object;• Widget t = this;• while (y != null || t != null)• if (y == null || t == null) return false;• if (y.n != t.n) return false;• y = y.w;• t = t.w;• • return true;•

3.5 La méthode hashCode() est implémentée de la façon suivante pour la classe Widget :

• public int hashCode()• int hc = (new Integer(n)).hashCode();• Widget tw = this.w;• while (tw != null)• hc += (new Integer(tw.n)).hashCode();• tw = tw.w;• • return hc;•

•••

X(): Je suis un Y. a = 0, b = 22 a = 33, b = 44Y(): Je suis un Y. a = 33, b = 44, c = 55 a = 66, b = 77, c = 88

Page 87: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 4

Récursivité

Une fonction est dite récursive lorsqu’elle s’appelle elle-même. Cette puissante technique de program-mation crée des répétitions sans utiliser de boucles while, do..while ou for. C’est pourquoi elle estcapable de produire des résultats substantiels à partir de très peu de code. En outre, la récursivité vouspermettra de trouver des solutions d’une grande élégance à un certain nombre de problèmes. Attentioncependant, lorsqu’elle est mal utilisée, cette subtilité informatique peut créer un code totalement ineffi-cace. Le code récursif provient généralement des algorithmes récursifs.

Exemple 4.1 Fonction factorielle

La fonction factorielle est définie mathématiquement de la façon suivante :

Cette définition est récursive parce que la factorielle revient à droite de l’équa-tion. La fonction est définie par elle-même. Le tableau ci-contre contient lesdix premières valeurs de la fonction factorielle. La première, 0! est définie parla moitié supérieure de la définition 0! = 1 (pour n = 0). Toutes les autresvaleurs sont définies par la moitié inférieure de la définition, c’est-à-dire :

• Pour n = 1, 1! = n! = n(n – 1)! = 1(1 – 1)! = 1(0)! = 1(1) = 1.• Pour n = 2, 2! = n! = n(n – 1)! = 2(2 – 1)! = 2(1)! = 2(1) = 2.• Pour n = 3, 3! = n! = n(n – 1)! = 3(3 – 1)! = 3(2)! = 3(2) = 6.• Pour n = 4, 4! = n! = n(n – 1)! = 4(4 – 1)! = 4(3)! = 4(6) = 24.• Pour n = 5, 5! = n! = n(n – 1)! = 5(5 – 1)! = 5(4)! = 5(24) = 120.

Comme vous pouvez le constater, cette fonction croît rapidement.

Exemple 4.2 Implémenter récursivement la fonction factorielle

Lorsqu’une fonction est définie récursivement, son implémentation est généralement une traductiondirecte de sa définition récursive. Les deux parties qui constituent la définition récursive de la fonc-tion factorielle sont directement traduites en deux instructions Java :

public static int f(int n) if (n==0) return 1; // base return n*f(n-1); // partie récursive

n n!

0 1

1 1

2 2

3 6

4 24

5 120

6 720

7 5 040

8 40 310

9 362 880

n n( )n! 1, if n = 0

1– !, if n 0>

=

Page 88: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

78 Récursivité

Vous trouverez ci-après un pilote test simple pour la fonction factorielle :

public static void main(String[] args) for (int n=0; n<10; n++) System.out.println("f("+n+") = "+f(n));

Ce programme est censé imprimer les mêmes valeurs que celles du tableau précédent.

Exemple 4.3 Implémenter itérativement la fonction factorielle

Il est également facile d’implémenter la fonction factorielle itérativement :

public static int f(int n) int f=1; for (int i=2; i<=n; i++) f *= i; return f;

Remarquez que l’en-tête de la fonction est identique à celui de l’exemple 4.2, seul le corps diffère.Cela nous permet d’utiliser le même pilote test pour les deux implémentations. Le résultat obtenudoit être identique dans les deux cas.

4.1 LA BASE ET LA PARTIE RÉCURSIVE DE LA RÉCURSIVITÉ

Pour fonctionner correctement, chaque fonction récursive doit être composée d’une base et d’une partierécursive. La base arrête la récursivité et c’est dans la partie récursive que la fonction s’appelle elle-même.

Exemple 4.4 Base et partie récursive de la fonction factorielle

Dans la méthode Java qui implémente la fonction factorielle de l’exemple 4.2, la base et la partierécursive sont associées à des commentaires. La partie récursive appelle la méthode et passe unevaleur plus petite de n. Ainsi, si vous commencez par la valeur positive 5, les valeurs des appels sui-vants seront 4, 3, 2, 1 et 0.Lorsque 0 est passé, la base est exécutée, ce qui met un terme à la récursivité etprovoque le début de la chaîne des renvois, soit 1, 1, 2, 6, 24 et 120.

Exemple 4.5 Nombres de Fibonacci

Les nombres de Fibonacci sont les suivants : 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ….Chaque nombre suivant le deuxième est la somme des deux nombres précédents.Il s’agit d’une définition récursive naturellement :

Les 15 premières valeurs de la séquence de Fibonacci sont présentées dans letableau ci-contre.Les deux premières valeurs F0 et F1 sont définies par les deux premières parties dela définition : F0 = 0 (pour n= 0) et F1 = 1 (pour n = 1). Ces deux parties constituentla base de la récursivité.

n Fn

0 01 12 13 24 35 56 87 138 219 3410 5511 8912 14413 23314 377

Fn

0, if n = 0

1, if n = 1

Fn 1– Fn 2– , if n > 1+

=

Page 89: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tracer un appel récursif 79

Toutes les autres valeurs sont définies par la partie récursive de la définition :

• Pour n = 2, F2 = Fn = Fn – 1 + Fn – 2 = F(2) – 1 + F(2) – 2 = F1 + F0 = 1 + 0 = 1.

• Pour n = 3, F3 = Fn = Fn – 1 + Fn – 2 = F(3) – 1 + F(3) – 2 = F2 + F1 = 1 + 1 = 2.

• Pour n = 4, F4 = Fn = Fn – 1 + Fn – 2 = F(4) – 1 + F(4) – 2 = F3 + F2 = 2 + 1 = 3.

• Pour n = 5, F5 = Fn = Fn – 1 + Fn – 2 = F(5) – 1 + F(5) – 2 = F4 + F3 = 3 + 2 = 5.

• Pour n = 6, F6 = Fn = Fn – 1 + Fn – 2 = F(6) – 1 + F(6) – 2 = F5 + F4 = 5 + 3 = 8.

• Pour n = 7, F7 = Fn = Fn – 1 + Fn – 2 = F(7) – 1 + F(7) – 2 = F6 + F5 = 8 + 5 = 13.

Exemple 4.6 Implémenter récursivement la fonction Fibonacci

public static int fib(int n) if (n < 2) return n; // base return fib(n-1) + fib(n-2); // partie récursive

Vous trouverez ci-après un pilote test simple de la méthode de Fibonacci :

public static void main(String[] args) for (int n=0; n<16; n++) System.out.println("fib(" + n + ") = " + fib(n));

Les valeurs obtenues devraient être identiques à celles du tableau de l’exemple 4.5.

4.2 TRACER UN APPEL RÉCURSIFLe traçage de l’exécution d’une méthode permet généralement de le rendre plus clair.

Exemple 4.7 Tracer la fonction factorielle récursive

La figure suivante illustre comment vous pouvez tracer l’appel f(5) de la fonction factorielle récur-sive définie dans l’exemple 4.2 :

L’appel commence dans la fonction main() et la valeur 5 est passée à la fonction f(). La valeurdu paramètre n est alors 5, f(4) est appelée et la valeur 4 est passée à la fonction f(). La valeur duparamètre n est alors 4, f(3) est appelée et la valeur 3 est passée à la fonction f(). Le processuscontinue (récursivement) jusqu’à ce que l’appel f(1) soit effectué depuis l’appel f(2). La valeurdu paramètre n est alors 1. Cette valeur est renvoyée immédiatement sans qu’aucun autre appel nesoit effectué. L’appel f(2) renvoie ensuite 2*1=2 à l’appel f(3). L’appel f(3) renvoie à son tour3*2=6 à l’appel f(4) qui renvoie 4*6=24 à l’appel f(5). En dernier lieu, l’appel f(5) renvoie lavaleur 120 à main().Le traçage de cet exemple indique que l’appel f(n) de l’implémentation récursive de la fonctionfactorielle générera n – 1 appels récursifs. Cette technique est clairement inefficace comparée àl’implémentation itérative de l’exemple 4.3.

Exemple 4.8 Tracer la fonction Fibonacci récursive

La fonction Fibonacci (voir l’exemple 4.6) est plus récursive que la fonction factorielle de l’exem-ple 4.2 dans la mesure où elle inclut deux appels récursifs. Vous pouvez en constater les conséquences

4n

f(4)3

65n

main()5

1205n

f(5)4

243n

f(3)2 1n

f(1)

22n

f(2)1

1

Page 90: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

80 Récursivité

dans la trace de l’appel fib(5) illustrée ci-après. L’appel commence dans la fonction main() et lavaleur 5 est passée à la fonction fib(). La valeur du paramètre n est alors 5, fib(4) et fib(3)sont ensuite appelées et les valeurs 3 et 4 sont respectivement passées. Chacun de ces appels effectueensuite deux appels récursifs jusqu’aux appels de base f(1) et f(0) qui renvoient tous les deux 1.Les appels récursifs renvoient ensuite la somme des deux valeurs qui leur ont été renvoyées pour quela valeur 8 soit finalement renvoyée à main().

4.3 ALGORITHME RÉCURSIF DE RECHERCHE BINAIRENous avons déjà vu l’algorithme de recherche binaire non récursif dans la section 2.5. Vous vous rappe-lez certainement qu’il utilise la stratégie « Diviser pour mieux régner » en fractionnant la séquence endeux et en poursuivant l’analyse dans la moitié contenant l’élément recherché. Ce procédé est naturelle-ment récursif.

Algorithme 4.1 Recherche binaire récursive

(Condition préalable : s = s0, s1, …, sn – 1 est une séquence triée de n valeurs ordinales du mêmetype que x.)(Condition postérieure : l’index i est renvoyé lorsque si = x ; dans le cas contraire, –1 est renvoyé.)

1. Si la séquence est vide, renvoyer –1.2. Supposons que si soit l’élément central de la séquence.3. Si si = x, renvoyer son index i.4. Si si < x, appliquer l’algorithme sur la sous-séquence se trouvant au-dessus de si.5. Appliquer l’algorithme à la sous-séquence de ss se trouvant au-dessus de si.

D’après la condition préalable de l’algorithme 4.1, la séquence doit être triée.

Exemple 4.9 Recherche binaire récursive

public static int search(int[] a, int lo, int hi, int x) // Condition préalable : a[0] <= a[1] <= ... <= a[a.length-1]; // Conditions postérieures : renvoie i; si i >= 0, alors a[i] == x; // sinon i == -1;

5n

main()5

8

0n

fib(0)

1n

fib(1)1

2n

fib(2)

1 0

11n

fib(1)2

3n

fib(3)

2 1

1

0n

fib(0)

1n

fib(1)1

2n

fib(2)

1 0

1

4n

fib(4)

5n

fib(5)

3 2

3 2 1n

fib(1)2

3n

fib(3)

2 1

1

0n

fib(0)

1n

fib(1)1

2n

fib(2)

1 0

1

4

5 3

3

Page 91: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithme récursif de recherche binaire 81

if (lo>hi) return -1; // étape 1 int i = (lo+hi)/2; // étape 2 if (a[i] == x) return i; // étape 3 else if (a[i] < x) return search(a,i+1,hi,x); // étape 4 else return search(a,lo,i-1,x); // étape 5

Théorème 4.1 : la durée d’exécution de la recherche binaire récursive est égale à O(lgn).Démonstration : l’argument qui sous-tend ce théorème est globalement identique à celui du théo-rème 2.4. La durée d’exécution est proportionnelle au nombre d’appels récursifs effectués. Chaque appeltraite une sous-séquence correspondant à la moitié de la précédente. C’est la raison pour laquelle le nom-bre d’appels récursifs est égal au nombre de fois où n peut être divisé par 2, soit lg n.

Exemple 4.10 Tester la recherche binaire récursive

import schaums.dswj.Arrays;public class Ex0410 private static final int SIZE = 16; private static final int START = 40; private static final int RANGE = 20; private static int[] a = new int[SIZE];

public static void main(String[] args) Arrays.load(a,START,RANGE); java.util.Arrays.sort(a); Arrays.print(a); test(); test(); test(); test();

public static void test() int x = Arrays.load(START,RANGE); System.out.print("Recherche de x = " + x + ":\t"); int i = search(a,x); if (i >= 0) System.out.println("a[" + i + "] = " + a[i]); else System.out.println("i = " + i + " ==> x introuvable");

public static int search(int[] a, int x) return search(a,0,a.length-1,x);

public static int search(int[] a, int lo, int hi, int x) if (lo>hi) return -1; // base int i = (lo+hi)/2; if (a[i] == x) return i; else if (a[i] < x) return search(a,i+1,hi,x); else return search(a,lo,i-1,x);

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 45, 45, 45, 45, 48, 48, 49, 50, 51, 52, 53, 54, 54, 58, 58, 59 Recherche de x = 46: i = -1 ==> x introuvableRecherche de x = 48: a[5] = 48Recherche de x = 54: a[11] = 54Recherche de x = 57: i = -1 ==> x introuvable

Page 92: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

82 Récursivité

Ce test est similaire à celui de l’exemple 2.8.Vous constaterez que, pour avoir la signature de méthode search(int[],int) avec uniquementdeux paramètres, vous devez « envelopper » la méthode récursive dans une méthode pilote nonrécursive afin de la démarrer.

4.4 COEFFICIENTS BINOMIAUXLes coefficients binomiaux sont le résultat de l’expansion d’une expression binomiale de la forme(x + 1)n. Par exemple,

(x + 1)6 =x6 + 6x5 + 15x4 = 20x3 + 15x2 + 6x +1

Les 7 coefficients générés ici sont 1, 6, 15, 20, 15, 6 et 1.Le mathématicien Pascal (1623-1662) a découvert une relation récursive dans les coefficients bino-

miaux. En les organisant dans un triangle, il a trouvé que chaque nombre interne est la somme des deuxnombres se trouvant directement au-dessus de lui :

Par exemple, 15 = 5 + 10.Supposons que c(n,k) indique le coefficient du numéro de ligne n et du numéro de colonne k (en

comptant à partir de 0). Si nous prenons l’exemple de c(6,2) = 15, la relation de récurrence de Pascal peutêtre exprimée de la façon suivante :

c(n, k) = c(n–1, k–1) + c(n–1, k), for 0 < k < n

soit, lorsque n = 6 et k = 2, c(6,2) = c(5,1) + c(5,2).

Exemple 4.11 Implémenter récursivement la fonction des coefficients binomiaux

public static int c(int n, int k) if (k==0 || k==n) return 1; // base return c(n-1,k-1) + c(n-1,k); // récursivité

La base de la récursivité couvre les parties gauche et droite du triangle, avec k = 0 et k = n.Les coefficients binomiaux sont identiques aux nombres de combinaison utilisés en mathématiquescombinatoires et calculés explicitement par la formule suivante :

Dans ce contexte, la combinaison est souvent écrite sous la forme suivante : En outre, vous la lirez en disant que « n choisit k ».

Par exemple, si n = 8 et k = 3, vous obtenez la combinaison suivante :

11 1

1 2 11 3 3 1

1 4 6 4 11 5 10 10 5 1

1 6 15 20 15 6 11 7 21 35 35 21 7 1

1 8 28 56 70 56 28 8 1

Colonne 2

Ligne 6

= =c n k,( ) n!k! n k–( )!----------------------- n

1---

n 1–2

------------ n 2–

3------------

…n k– 1+

k---------------------

c n k,( ) nk

=

83

8 1⁄( ) 7 2⁄( ) 6 3⁄( ) 56= =

Page 93: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithme d’Euclide 83

Exemple 4.12 Implémenter itérativement la fonction des coefficients binomiaux

Cette version implémente la formule explicite donnée précédemment. L’expression de droite estcomposée de coefficients k, c’est pourquoi le calcul est effectué par une boucle répétée un nombre kde fois :

public static int c(int n, int k) if (n<2 || k==0 || k==n) return 1; int c=1; for (int j=1; j <= k; j++) c = c*(n-j+1)/j; return c;

4.5 ALGORITHME D’EUCLIDEL’algorithme d’Euclide calcule le plus grand diviseur commun de deux entierspositifs. Il apparaît comme Proposition 2 du livre VII composant les Élémentsd’Euclide (v. 300 av. J.-C.) et est probablement l’algorithme récursif le plusancien. Tel qu’il a été initialement formulé par Euclide, il consiste à soustraireplusieurs fois le nombre n le plus petit du plus grand nombre m jusqu’à ce quela différence d obtenue soit plus petite que n. Répétez ensuite les mêmes éta-pes en remplaçant n par d et m par n. Continuez jusqu’à ce que les deux nom-bres soient égaux. Le nombre obtenu est alors le plus grand diviseur commundes deux nombres initiaux.

Cet exemple applique l’algorithme d’Euclide pour rechercher le plusgrand diviseur commun de 494 et 130, c’est-à-dire 26. Ceci est correct puisque494 = 26⋅19 et 130 = 26⋅5.

Exemple 4.13 Implémenter récursivement l’algorithme d’Euclide

Chaque étape de cet algorithme ne fait que soustraire le plus petit nombre du plus grand en appelantrécursivement gcd(m,n-n) ou gcd(m-n,n) :

public static int gcd(int m, int n) if (m==n) return n; // base else if (m<n) return gcd(m,n-m); // récursivité else return gcd(m-n,n); // récursivité

Par exemple, l’appel gcd(494,130) appelle récursivement gcd(364,130), qui appelle récursive-ment gcd(234,130), qui appelle récursivement gcd(104,130), qui appelle récursivementgcd(104,26), qui appelle récursivement gcd(78,26), qui appelle récursivement gcd(52,26),qui appelle récursivement gcd(26,26), qui renvoie 26. Cette valeur est ensuite renvoyée successi-vement jusqu’à l’appel initial gcd(494,130) qui la renvoie à sa routine d’appel.

4.6 PREUVE INDUCTIVE DE CORRECTIONLes fonctions récursives sont généralement démontrées à l’aide du principe d’induction mathématique(voir l’annexe B). Selon ce principe, il est possible de prouver qu’une séquence infinie d’instructions estvraie en vérifiant (i) si la première instruction est vraie, et (ii) en en déduisant que toutes les autres pro-positions de la séquence sont vraies. L’étape (i) est qualifiée d’étape de base et (ii) d’étape inductive. Lasupposition selon laquelle les propositions précédentes sont vraies est qualifiée d’hypothèse inductive.

494–130

364–130

234–130

104130

–10426

104–26

78–26

52–26

26

Page 94: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

84 Récursivité

Théorème 4.2 : la fonction factorielle récursive est correcte.Démonstration : pour prouver que l’implémentation récursive de la fonction factorielle est correcte(voir l’exemple 4.1), nous devons tout d’abord vérifier la base. L’appel f(0) renvoie la valeur correcte 1en raison de la première ligne :

if (n < 2) return 1;

Nous supposons ensuite que la fonction renvoie la valeur correcte pour tous les entiers inférieurs à unnombre n > 0. Puis la deuxième ligne :

return n*f(n-1);

renverra la valeur n! correcte parce que, si nous nous en tenons à l’hypothèse inductive, l’appel f(n-1)renverra (n – 1)! et n! = n (n – 1).Remarquez que nous utilisons ici le principe fort d’induction mathématique (également qualifié dedeuxième principe). Dans cette version, l’hypothèse inductive nous permet de supposer que toutes lespropositions précédentes sont vraies. Dans le cadre du principe faible (ou premier principe), nous pou-vons uniquement supposer que seule la proposition précédente est vraie. Cependant, étant donné que cesdeux principes sont équivalents (c’est-à-dire qu’ils permettent tous les deux d’établir la preuve d’uneproposition générale), il est généralement préférable d’appliquer la méthode d’induction forte.

Théorème 4.3 : l’algorithme d’Euclide est correct.Démonstration : Pour prouver que l’algorithme d’Euclide (voir l’exemple 4.13) est correct, nous pou-vons utiliser l’induction forte. Si m et n sont égaux, ce nombre est leur plus grand diviseur commun. Lafonction renvoie donc la valeur correcte dans ce cas en raison de la ligne de code suivante :

if (m == n) return n;

Si m et n ne sont pas égaux, la fonction renvoie gcd(m,n-m) ou gcd(m-n,n). Pour constater que cettevaleur est également correcte, nous avons simplement besoin de voir que les trois paires (m,n), (m,n-m)et (m-n,n) auront toujours le même plus grand diviseur commun. Il s’agit là d’un théorème de la théoriedes nombres présenté dans l’annexe B.

4.7 ANALYSE DE LA COMPLEXITÉ DES ALGORITHMES RÉCURSIFS

L’analyse de la complexité d’un algorithme récursif dépend de sa relation de récurrence. Généralement,la meilleure technique consiste à utiliser T(n) comme nombre d’étapes nécessaires à l’application d’unalgorithme pour un problème de taille n. La partie récursive de l’algorithme se traduit en relation derécurrence sur T(n). Sa solution est ensuite la fonction de complexité de l’algorithme.

Théorème 4.4 : la fonction factorielle récursive a une durée d’exécution égale à O(n).Démonstration : supposons que T(n) soit le nombre d’appels récursifs effectués à partir de l’appel ini-tial f(n) de la fonction (voir l’exemple 4.2). T(0) = T(1) = 0 parce que si n < 2, aucun appel récursif n’alieu. Si n > 1, la ligne

return n*f(n-1);

est exécutée et effectue l’appel récursif f(n - 1). Le nombre total d’appels récursifs est 1 plus le nombred’appels effectués depuis f(n - 1), ce qui se traduit de la façon suivante dans la relation de récurrence :

T(n) = 1 + T(n – 1)

La solution de cette récurrence est la suivante :

T(n) = n – 1, pour n > 0

Page 95: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Programmation dynamique 85

Nous arrivons à cette conclusion en deux étapes : tout d’abord, nous trouvons la solu-tion, puis nous utilisons l’induction pour prouver qu’elle est correcte.La technique la plus simple de recherche d’une solution pour la relation de récurrenceest de créer un tableau de valeurs et de rechercher un schéma. Dans le cas présent, larelation de récurrence nous indique que chaque valeur de T(n) est supérieure de 1 à lavaleur précédente. La solution f(n) = n – 1 est donc plutôt évidente.Maintenant, pour prouver que T(n) = n – 1 chaque fois que n > 0, nous supposons quef(n) = n – 1 et nous appliquons le principe faible de l’induction mathématique. Lasituation de base est lorsque n = 1. Dans ce cas, T(n) = T(1) = 0 et f(n) = f(1) = (1) – 1= 0. Pour l’étape inductive, nous supposons que T(n) = f(n) pour n > 0, puis nous endéduisons que T(n + 1) = f(n + 1) :

T(n + 1) = 1 + T(n) = 1 + f(n) = 1 + (n – 1) = nf(n + 1) = (n + 1) – 1 = n

Voilà qui termine notre démonstration.Maintenant que nous avons déterminé que la fonction de complexité pour l’implémentation récursive dela fonction factorielle T(n) = n – 1, nous pouvons en déduire que la durée d’exécution de cette implémen-tation sera proportionnelle à la taille de son argument n. S’il faut 3 millisecondes pour calculer 8!, il fau-dra environ 6 millisecondes pour calculer 16!.

4.8 PROGRAMMATION DYNAMIQUEDans la plupart des cas, la récursivité est rendue inefficace par ses nombreux appels de fonctions. C’estla raison pour laquelle il est souvent préférable d’avoir recours à une implémentation itérative lorsqu’ellen’est pas trop complexe.

Une troisième possibilité s’offre également à vous, elle consiste à implémenter la relation de récur-rence en stockant les valeurs calculées précédemment dans un tableau au lieu de les recalculer grâce auxappels récursifs de fonctions. Cette méthode est qualifiée de programmation dynamique.

Exemple 4.14 Implémenter la fonction Fibonacci à l’aide de la programmation dyna-mique

public static int fib(int n) if (n<2) return n; int[] f = new int[n]; f[0] = 0; f[1] = 1; for (int i=2; i<n; i++) // stocker les nombres de Fibonacci f[i] = f[i-1] + f[i-2]; return f[n-1] + f[n-2];

Cette implémentation utilise un tableau dynamique f[] d’entiers longs n pour stocker les premiersnombres n de Fibonacci.

4.9 LES TOURS DE HANOINous venons de voir des exemples importants de fonctions qui sont définies plus naturellement et quisont plus aisément compréhensibles si vous utilisez la récursivité. Dans certains cas, la récursivité estd’ailleurs la seule méthode acceptable, comme nous allons le voir pour le jeu des tours de Hanoi.

n T (n)

0

1

2

3

4

5

6

0

0

1

2

3

4

5

Page 96: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

86 Récursivité

Ce jeu est composé d’un plateau sur lequel se trou-vent trois tours (appelées A, B et C) et une séquence den disques avec un trou au centre. Les disques sontempilés par taille décroissante (c’est-à-dire d’abord lerayon de 8 cm, puis celui de 7 cm, 6 cm, 5 cm, etc.) sur la tour A. La règle est simple : aucun disque nepeut être placé sur un disque plus petit de la même tour. L’objectif est de déplacer tous les disques de latour A vers la tour C, un disque à la fois, en respectant la règle énoncée précédemment.

La solution générale est naturellement récursive :

• Partie I : déplacer les disques n – 1 les plus petits de la tour A vers la tour B.• Partie II : déplacer le disque restant de la tour A vers la tour C.• Partie III : déplacer les disques n – 1 les plus petits de la tour B vers la tour C.

Les étapes 1 et 3 sont récursives puisque vous appliquez la solution complète aux disques n – 1. Labase de cette solution récursive est le cas n = 0, c’est-à-dire, ne faites rien.

La solution du cas du disque n = 1 est la suivante :

1. Déplacer le disque de la tour A vers la tour C.La solution du cas des disques n = 2 est la suivante :

1. Déplacer le disque supérieur de la tour A vers la tour B.2. Déplacer le deuxième disque de la tour A vers la tour C.3. Déplacer le disque supérieur de la tour B vers la tour C.

La solution du cas des disques n = 3 est la suivante :

1. Déplacer le disque supérieur de la tour A vers la tour C.2. Déplacer le deuxième disque de la tour A vers la tour B.3. Déplacer le disque supérieur de la tour C vers la tour B.4. Déplacer le disque restant de la tour A vers la tour C.5. Déplacer le disque supérieur de la tour B vers la tour A.6. Déplacer le deuxième disque de la tour B vers la tour C.7. Déplacer le disque supérieur de la tour A vers la tour C.

Dans cet exemple, les étapes 1 à 3 constituent la première partie de la solution générale, l’étape 4 ladeuxième partie et les étapes 5 à 7 la troisième partie.

Étant donné que la solution générale récursive requiert la substitution de différents noms de tours, ilest préférable d’utiliser des variables. Ainsi, si nous appelons cet algorithme en trois étapes hanoi(n,x,y,z),nous obtenons la solution suivante :

• Partie I : déplacer les disques n –1 les plus petits de la tour x vers la tour z.• Partie II : déplacer le disque restant de la tour x vers la tour y.• Partie III : déplacer les disques n – 1 les plus petits de la tour z vers la tour y.

Vous trouverez l’implémentation complète de cette solution générale dans l’exemple suivant.

Exemple 4.15 Les tours de Hanoi

public class Ex0415 public static void main(String[] args) runHanoi(4,’A’,’B’,’C’);

public static void runHanoi(int n, char x, char y, char z) if (n==1) // base

A B C

Page 97: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Récursivité mutuelle 87

System.out.println("Déplacer le disque supérieur de la tour " + x + " vers la tour " + z); else // récursivité runHanoi(n-1,x,z,y); runHanoi(1,x,y,z); runHanoi(n-1,y,x,z);

Dans le cas de quatre disques, la solution est créée par l’appel :

runHanoi(4,’A’,’B’,’C’);

Vous obtenez alors la sortie suivante :

Déplacer le disque supérieur de la tour A vers la tour BDéplacer le disque supérieur de la tour A vers la tour CDéplacer le disque supérieur de la tour B vers la tour CDéplacer le disque supérieur de la tour A vers la tour BDéplacer le disque supérieur de la tour C vers la tour ADéplacer le disque supérieur de la tour C vers la tour BDéplacer le disque supérieur de la tour A vers la tour BDéplacer le disque supérieur de la tour A vers la tour CDéplacer le disque supérieur de la tour B vers la tour CDéplacer le disque supérieur de la tour B vers la tour ADéplacer le disque supérieur de la tour C vers la tour ADéplacer le disque supérieur de la tour B vers la tour CDéplacer le disque supérieur de la tour A vers la tour BDéplacer le disque supérieur de la tour A vers la tour CDéplacer le disque supérieur de la tour B vers la tour C

4.10 RÉCURSIVITÉ MUTUELLELorsqu’une fonction s’appelle elle-même, il s’agit d’unerécursivité directe. Mais il existe aussi une récursivitéindirecte, quand une fonction appelle d’autres fonctions,qui appellent d’autres fonctions, qui finissent par appelerla fonction initiale. La récursivité mutuelle est la forme laplus courante de ce type de récursivité ; elle a lieu lorsquedeux fonctions s’appellent mutuellement.

Exemple 4.16 Fonctions sinus et cosinus calculées par récursivité mutuelle

Les fonctions trigonométriques sinus et cosinus peuvent être définies de plusieurs façons et diversalgorithmes vous permettent de calculer leurs valeurs. Bien qu’elle ne soit pas la plus efficace, laméthode la plus simple consiste à utiliser la récursivité mutuelle. Elle est basée sur les identitéssuivantes :

sin2θ = 2sincosθcos2θ = 1 – 2(sinθ)2

et sur les deux polynômes de Taylor :

sinx ≈ x – x3/6

cosx ≈ 1 – x2/2

qui sont des approximations de petites valeurs de x.

f()

f() g()

f()

g()

h()

Page 98: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

88 Récursivité

public class Ex0416 public static void main(String[] args) for (double x=0. ; x<1.0; x += 0.1) System.out.println(s(x) + "\t" + Math.sin(x)); for (double x=0. ; x<1.0; x += 0.1) System.out.println(c(x) + "\t" + Math.cos(x)); public static double s(double x) if (-0.005 < x && x < 0.005) return x - x*x*x/6; // base return 2*s(x/2)*c(x/2); // récursivité public static double c(double x) if (-0.005 < x && x < 0.005) return 1.0 - x*x/2; // base return 1 - 2*s(x/2)*s(x/2); // récursivité

Ce programme fonctionne parce que x est divisé par 2 à chaque appel récursif. Il finit ainsi par attein-dre le critère de base (-0.005 < x && x < 0.005) qui arrête la récursivité. Lorsque x est trèspetit, les polynômes de Taylor donnent des résultats précis à 15 décimales près.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

4.1 Une fonction récursive doit être composée d’une étape de base et d’une étape récursive. Expli-quez en quoi consistent ces étapes et leur rôle essentiel dans la récursivité.

4.2 Combien d’appels récursifs de la fonction factorielle récursive seront-ils générés par l’appelf(10) (voir l’exemple 4.2) ?

0.0 0.00.09983341664635367 0.099833416646828150.1986693307941265 0.198669330795061220.29552020665442036 0.29552020666133960.3894183423068936 0.38941834230865050.4794255385990945 0.4794255386042030.5646424733830799 0.56464247339503540.6442176872361944 0.6442176872376910.7173560908968648 0.71735609089952270.7833269096232099 0.78332690962748330.8414709848016061 0.84147098480789641.0 1.00.9950041652780733 0.99500416527802580.9800665778414311 0.98006657784124160.9553364891277464 0.9553364891256060.9210609940036278 0.92106099400288510.8775825618931635 0.87758256189037280.8253356149178575 0.82533561490967830.7648421872857489 0.76484218728448850.6967067093499022 0.69670670934716550.6216099682760496 0.62160996827066450.5403023058779364 0.5403023058681398

Page 99: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 89

4.3 Combien d’appels récursifs de la fonction récursive Fibonacci seront-ils générés par l’appelfib(6) (voir l’exemple 4.6) ?

4.4 Quels sont les avantages et les inconvénients de l’implémentation d’une solution récursive aulieu d’une solution itérative ?

4.5 Quelle est la différence entre la récursivité directe et la récursivité indirecte ?

RÉPONSES¿RÉPONSES

4.1 La base d’une fonction récursive est son point de départ lors de sa définition et son étape finalelorsqu’elle est appelée récursivement ; c’est ce qui termine la récursivité. Quant à la partie récur-sive, il s’agit de l’affectation qui comprend la fonction à droite de l’opérateur d’affectation et quiprovoque l’appel de la fonction par elle-même ; c’est ce qui crée les répétitions. Par exemple,dans le cas de la fonction factorielle, la base est n! = 1 si n = 0 et la partie récursive est n! =n(n – 1) si n > 0.

4.2 L’appel factorial(10) générera 10 appels récursifs.

4.3 L’appel f(6) de la fonction Fibonacci générera 14 + 8 = 22 appels récursifs parce qu’il appellef(5) et f(4) qui génèrent respectivement 14 et 8 appels récursifs.

4.4 Une solution récursive est souvent plus facile à comprendre que son équivalent itératif. Cepen-dant, la récursivité s’exécute beaucoup plus lentement que l’itération.

4.5 La récursivité directe a lieu quand une fonction s’appelle elle-même. La récursivité indirecte alieu lorsque des fonctions s’appellent entre elles.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

4.1 Écrivez et testez une fonction récursive qui renvoie la somme des carrés des premiers entierspositifs n.

4.2 Écrivez et testez une fonction récursive qui renvoie la somme des premières puissances n d’unebase b.

4.3 Écrivez et testez une fonction récursive qui renvoie la somme des premiers éléments n d’untableau.

4.4 Écrivez et testez une fonction récursive qui renvoie le maximum des premiers éléments n d’untableau.

4.5 Écrivez et testez une fonction récursive qui renvoie le maximum des premiers éléments n d’untableau en n’utilisant pas plus de n lg appels récursifs.

4.6 Écrivez et testez une fonction récursive qui renvoie la puissance xn.

4.7 Écrivez et testez une fonction récursive qui renvoie la puissance xn, en n’utilisant pas plus de2 n lg appels récursifs.

4.8 Écrivez et testez une fonction récursive qui renvoie le logarithme binaire entier d’un entier n(c’est-à-dire le nombre de fois où n peut être divisé par 2).

Page 100: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

90 Récursivité

4.9 Écrivez et testez une fonction booléenne récursive qui détermine si une chaîne est un palindrome(c’est-à-dire une chaîne de caractères qui reste identique lorsque vous en inversez les lettres).

4.10 Écrivez et testez une fonction récursive qui renvoie une chaîne contenant la représentationbinaire d’un entier positif.

4.11 Écrivez et testez une fonction récursive qui renvoie une chaîne contenant la représentation hexa-décimale d’un entier positif.

4.12 Écrivez et testez une fonction récursive qui imprime toutes les permutations des n premierscaractères d’une chaîne. Par exemple, l’appel print("ABC",3) imprimerait

• ABC• ACB• BAC• BCA• CBA• CABr

4.13 Implémentez itérativement la fonction Fibonacci (sans utiliser de tableau).

4.14 Implémentez la fonction d’Ackermann récursive :

a(0, n) = 1

a(1, 0) = 2

a(m, 0) = m + 2, if m >1

a(m, n) = a(a(m – 1, n), n – 1), if m > 0 et n > 0

4.15 Prouvez la relation de récurrence de Pascal.

4.16 Tracez l’implémentation récursive de l’algorithme d’Euclide (voir l’exemple 4.6) pour l’appelgcd(385,231).

4.17 Implémentez l’algorithme d’Euclide itérativement.

4.18 Implémentez l’algorithme d’Euclide récursivement en utilisant l’opérateur de reste entier % aulieu de la soustraction répétée.

4.19 Implémentez l’algorithme d’Euclide itérativement en utilisant l’opérateur de reste entier % aulieu de la soustraction répétée.

4.20 Utilisez l’induction mathématique pour prouver que l’implémentation récursive de la fonctionFibonacci est correcte (voir l’exemple 4.6).

4.21 Utilisez l’induction mathématique pour prouver que la fonction récursive de l’exercice 4.4 estcorrecte.

4.22 Utilisez l’induction mathématique pour prouver que la fonction récursive de l’exercice 4.5 estcorrecte.

4.23 Utilisez l’induction mathématique pour prouver que la fonction récursive de l’exercice 4.8 estcorrecte.

4.24 Utilisez l’induction mathématique pour prouver que la fonction récursive de l’exercice 4.12 estcorrecte.

4.25 Le domaine calculable d’une fonction est l’ensemble des entrées pour lesquelles cette fonctionpeut créer des résultats corrects. Déterminez empiriquement le domaine calculable de la fonctionfactorielle implémentée dans l’exemple 4.2.

4.26 Déterminez empiriquement le domaine calculable de la fonction sum(b,n) implémentée dansl’exercice 4.2 en utilisant b = 2.

Page 101: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 91

4.27 Déterminez empiriquement le domaine calculable de la fonction Fibonacci implémentée dansl’exemple 4.3.

4.28 Déterminez empiriquement le domaine calculable de la fonction récursive des coefficients bino-miaux de l’exemple 4.11.

4.29 Le programme des tours de Hanoi réalise 7 déplacements de disques pour 3 disques. Combien dedéplacements doivent être effectués si vous disposez de :

a. 5 disques ?

b. 6 disques ?

c. n disques ?

4.30 Démontrez la formule dérivée de l’exercice précédent.

4.31 Déterminez empiriquement le domaine calculable de la fonction d’Ackermann de l’exer-cice 4.14.

4.32 Créez l’arbre d’appels récursifs pour l’appel hanoi(4,'A', 'B', 'C') de l’exemple 4.15.

4.33 Modifiez le programme de l’exemple 4.16 afin d’obtenir des résultats plus précis en réduisant lesbases de façon à ce que la récursivité se poursuive jusqu’à ce que |x| < 0,00005.

4.34 Modifiez le programme de l’exemple 4.16 afin d’obtenir des résultats plus précis en utilisant desapproximations plus précises :

sin x ≈ x – x3/6 + x5/120 = x(1 – x2(1 – x2/20))

cos x ≈ 1 – x2/2 + x4/24 = 1 – x2/2⋅(1 – x2/12)

4.35 Utilisez la récursivité mutuelle pour implémenter les fonctions de sinus et de cosinus hyperboli-ques. Utilisez les formules suivantes :

Comparez vos résultats aux valeurs correspondantes des fonctions sinh et cosh définies en termesde fonction exponentielle par les formules suivantes :

4.36 Implémentez la fonction tangente récursivement en utilisant les formules suivantes :

4.37 Implémentez une fonction récursive qui évalue un polynôme a0 +a1x + a2x2 +… + anxn, les coef-ficients n + 1 ai étant passés à la fonction dans un tableau en même temps que le degré n.

2θsinh 2 θsinh θcosh=

2θcosh 1 2 θsinh( )2+=

xsinh x x3 6/+≈

xcosh 1 x2 2/+≈

xsinh ex e x––( )2

-----------------------=

xcosh ex e x–+( )2

-----------------------=

2xtan2 xtan

1 xtan( )2–---------------------------=

xtan x13--- x3+≈

Page 102: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

92 Récursivité

SOLUTIONS¿SOLUTIONS

4.1 Cette fonction récursive renvoie la somme des premiers carrés n :

• public static int sum(int n)• if (n==0) return 0; // base• return sum(n-1) + n*n; // récursivité•

4.2 Cette fonction récursive renvoie la somme des premières puissances n d’une base b :

• public static double sum(double b, int n)• if (n==0) return 1; // base• return 1 + b*sum(b,n-1); // récursivité•

Remarquez que cette solution implémente la méthode de Horner : 1 + b*(1 + b*(1 + … + b)).

4.3 Cette fonction récursive renvoie la somme des premiers éléments n d’un tableau :

• public static double sum(double[] a, int n)• if (n==0) return 0.0; // base• return sum(a,n-1) + a[n-1]; // récursivité•

4.4 Cette fonction récursive renvoie le maximum des premiers éléments n d’un tableau :

• public static double max(double[] a, int n)• if (n==1) return a[0]; // base• double m = max(a,n-1); // récursivité• if (a[n-1] > m) return a[n-1];• else return m;•

4.5 Cette fonction récursive renvoie le maximum des premiers éléments n d’un tableau et n’effectuepas plus de n lg appels récursifs :

• public static double max(double[] a, int lo, int hi)• if (lo>=hi) return a[lo];• int mid=(lo+hi)/2; // index central• double m1=max(a,lo,mid); // récursivité sur a[lo..mid]• double m2=max(a,mid+1,hi); // récursivité sur a[mid+1..hi]• return (m1>m2?m1:m2); // maximum de m1,m2•

4.6 Cette fonction récursive renvoie la puissance xn :

• public static double pow(double x, int n)• if (n==0) return 1.0; // base• return x*pow(x,n-1); // récursivité•

4.7 Cette fonction récursive renvoie la puissance xn et n’effectue pas plus de n lg appels récursifs :

• public static double pow(double x, int n)• if (n==0) return 1.0; // base• double p = pow(x,n/2);• if (n%2==0) return p*p; // récursivité (n pair)• else return x*p*p; // récursivité (n impair)•

Page 103: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 93

4.8 Cette fonction récursive renvoie le logarithme binaire entier de n :

• public static int lg(int n)• if (n==1) return 0; // base• return 1 + lg(n/2); // récursivité•

4.9 Cette fonction récursive détermine si une chaîne est un palindrome :

• public static boolean isPalindrome(String s)• int len = s.length();• if (len<2) return true;• if (s.charAt(0) != s.charAt(len-1)) return false;• if (len==2) return true;• return isPalindrome(s.substring(1,len-1)); // récursivité•

4.10 Cette fonction récursive convertit du décimal en binaire :

• public static String binary(int n)• String s;• if (n%2 == 0) s = "0";• else s = "1";• if (n < 2) return s; // base• return binary(n/2) + s; // récursivité•

4.11 Cette fonction récursive convertit du décimal en hexadécimal :

• public static String hexadecimal(int n)• String s = hex(n%16);• if (n<16) return s; // base• return hexadecimal(n/16) + s; // récursivité• • String hex(int n)• if (n==0) return "0";• if (n==1) return "1";• if (n==2) return "2";• if (n==3) return "3";• if (n==4) return "4";• if (n==5) return "5";• if (n==6) return "6";• if (n==7) return "7";• if (n==8) return "8";• if (n==9) return "9";• if (n==10) return "A";• if (n==11) return "B";• if (n==12) return "C";• if (n==13) return "D";• if (n==14) return "E";• else return "F";•

4.12 Cette fonction récursive imprime des permutations :

• public static void print(String str)• print("",str);• •• public static void print(String left, String right)• int n=right.length();• if (n==0) return;

Page 104: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

94 Récursivité

• if (n==1)• System.out.println(left+right);• return;• • StringBuffer s = new StringBuffer(right);• for (int i=0; i<n; i++)• char temp = s.charAt(i);• s.setCharAt(i,s.charAt(0));• s.setCharAt(0,temp);• print(left+temp,s.substring(1,n));• •

4.13 L’implémentation itérative de la fonction Fibonacci est la suivante :

• public static int fib(int n)• if (n<2) return n;• int f0=0, f1=1, f=f0+f1;• for (int i=2; i<n; i++)• f0 = f1;• f1 = f;• f = f0 + f1;• • return f;•

4.14 Et voici la fonction d’Ackermann :

• public static int ackermann(int m, int n)• if (m==0) return 1; // base• if (n==0)• if (m==1) return 2; // base• else return m + 2; // base• return ackermann(ackermann(m-1,n), n-1); // récursivité•

4.16 Considérez la relation c(8,3 = 56 = 35 + 21 = c(7,3) + c(7,2) issue de l’expansion de (x + 1)8 :

(x + 1)8 = (x + 1)(x + 1)7

= (x + 1)(x7 + 7x6 + 21x5 + 35x4 + 35x3 + 21x2 + 7x + 1)

= x8 + 7x7 + 21x6 + 35x5 + 35x4 + 21x3 + 7x2 + x

+ x7 + 7x6 + 21x5 + 35x4 + 35x3 + 21x2 + 7x + 1

= x8 + 8x7 + 28x6 + 56x5 + 70x4 + 56x3 + 28x2 + 7x + 1

Le coefficient c(8,3) est pour le terme x5 qui est 35x5 +21x5 + 56x5. La somme 35x5 +21x5 estissue de x(35x4) et 1(21x5). Ces coefficients sont donc 35 = c(7,3) et 21 = c(7,2).La preuve générale est basée sur le même argument : c(n,k) est le coefficient du terme xk dansl’expansion de (x + 1)n. Étant donné que (x + 1)n = (x + 1)(x + 1)n – 1, ce terme vient de la somme

(x)(c(n – 1, k – 1) xk – 1) + (1)(c(n – 1, k)xk) = (c(n – 1, k – 1) + c(n – 1, k)) xk

Par conséquent, c(n, k) = c(n – 1, k – 1) + c(n – 1, k).

4.16 La trace de l’appel gcd(616,231) est la suivante :

385,231

77

385m

gcd(385,231)

231n154,231

77

154m

gcd(154,231)

231n154,77

77

154m

gcd(154,77)

77n77,77

77

77m

gcd(77,77)

77n

385m

main()

231n

Page 105: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 95

4.17 L’implémentation itérative de l’algorithme d’Euclide est la suivante :

• public static int gcd(int m, int n)• while (m != n) // INVARIANT : gcd(m,n)• if (m < n) n -= m;• else m -= n;• return n;•

4.18 L’implémentation récursive de l’algorithme d’Euclide utilisant l’opérateur de reste est la sui-vante :

• public static int gcd(int m, int n)• if (m==0) return n; // basis• if (n==0) return m; // basis• else if (m<n) return gcd(m,n%m); // récursivité• else return gcd(m%n,n); // récursivité•

4.19 L’implémentation itérative de l’algorithme d’Euclide utilisant l’opérateur de reste est la sui-vante :

• public static int gcd(int m, int n)• while (n>0) // INVARIANT : gcd(m,n)• int r = m%n;• m = n;• n = r;• • return m;•

4.20 Pour prouver que l’implémentation récursive de la fonction Fibonacci est correcte, vérifiezd’abord la base. Les appels fib(0) et fib(1) renvoient les valeurs correctes 0 et 1 en raisonde la première ligne

• if (n < 2) return n;

Nous supposons ensuite que la fonction renvoie les valeurs correctes pour tous les entiers infé-rieurs à n > 1. La deuxième ligne

• return fib(n-1) + fib(n-2);

renverra la valeur correcte n! parce que, si nous suivons l’hypothèse inductive, les appelsfib(n-1) et fib(n-2) renvoient les valeurs correctes pour Fn – 1 et Fn – 2 respectivement, et Fn= Fn – 1 + Fn – 2 par définition. Remarquez que, dans le cas présent, la base requiert la vérificationdes deux premières étapes de la séquence parce que la relation de récurrence Fn = Fn – 1 + Fn – 2s’applique uniquement pour n > 1.

4.21 Si n = 1, la base est exécutée et renvoie a[0] qui est l’élément maximum parce qu’il s’agit duseul élément. Si n > 1, la fonction calcule correctement le maximum m des premiers élémentsn–1 (selon l’hypothèse inductive). Si (a[m – 1] > m) est vraie, a[m – 1] est renvoyé. Cet élémentest le plus grand puisqu’il est plus grand que le plus grand de tous les autres éléments. Dans lecas contraire, si la condition (a[n-1] >m) est fausse, m est renvoyé et il s’agit de l’élément leplus grand puisque a[m – 1] n’est pas plus grand.

4.22 Si n = 1, la base et exécutée et renvoie a[0] qui est l’élément maximum parce qu’il s’agit du seulélément. Si n > 1, la fonction calcule correctement les valeurs maximum m1 et m2 des premièreet deuxième parties du tableau (selon l’hypothèse inductive). L’un de ces deux nombres est cor-rect pour tout le tableau et le plus grand d’entre eux est renvoyé.

Page 106: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

96 Récursivité

4.23 Si n = 1, la base est exécutée et renvoie 0 qui est le nombre de fois où n peut être divisé par 2. Sin > 1, la fonction calcule correctement le nombre de fois où n/2 peut être divisé par 2 (selonl’hypothèse inductive). Ce nombre est inférieur de 1 au nombre de fois où n peut être divisépar 2. C’est pourquoi la valeur renvoyée, 1 + lg(n/2) est correcte.

4.24 Nous démontrons d’abord la conjecture selon laquelle l’appel print(left,right) imprimeran! chaînes distinctes ayant toutes le même préfixe left, avec n = right.length(). Si n = 1,la méthode imprime left+right et renvoie les données, soit 1! chaîne (distincte). Supposonsque, lorsque right.length() = n – 1, l’appel print(left,right) imprime (n – 1)! Chaî-nes distinctes ayant toutes la chaîne de préfixe left. Ensuite, lorsque right.length() = n, laboucle for effectue n appels du type print(left+temp,ss), avec temp comme caractèredistinct et ss = s.substring(1,n). Étant donné que la longueur de s.substring(1,n)est égale à n – 1, chacun de ces appels imprime (n – 1)! chaînes distinctes ayant toutes la mêmechaîne de préfixe left+temp. C’est pourquoi la boucle imprime (n)(n – 1)! chaînes distinctesayant toutes la même chaîne de préfixe left. Notre supposition est donc démontrée grâce àl’induction mathématique. Nous pouvons en déduire que l’appel print(str) imprimera n!permutations distinctes des caractères de la chaîne str, n étant sa longueur. Puisqu’il s’agit trèsexactement du nombre total de permutations de la chaîne, nous pouvons dire que la méthode estcorrecte.

4.25 En ce qui concerne la fonction factorielle implémentée dans l’exemple 4.2, le dépassementd’entier a lieu pour le type de renvoi long avec n = 13 sur l’ordinateur de l’auteur. C’est pour-quoi le domaine calculable de cette fonction est 0 ≤ n ≤ 12.

4.26 En ce qui concerne la fonction sum(b,n) implémentée dans l’exercice 4.2 avec b = 2, le dépas-sement des nombres à virgule flottante a lieu pour le type de renvoi double avec n = 1,023 surl’ordinateur de l’auteur. Le domaine calculable de cette fonction est donc 0 ≤ n ≤ 1,022.

4.27 En ce qui concerne la fonction Fibonacci implémentée dans l’exemple 4.4, le temps système desappels récursifs nuit considérablement aux performances d’exécution après n = 36 sur l’ordina-teur de l’auteur. Le domaine calculable de cette fonction est donc environ 0 ≤ n ≤ 40.

4.28 En ce qui concerne la fonction des coefficients binomiaux de l’exemple 4.7, le temps système desappels récursifs nuit considérablement aux performances d’exécution après n = 25 sur l’ordina-teur de l’auteur. Le domaine calculable de cette fonction est donc environ 0 ≤ n ≤ 30.

4.29 Le programme des tours de Hanoi effectue :

a. 31 déplacements pour 5 disques ;

b. 63 déplacements pour 6 disques ;

c. 2n – 1 déplacements pour n disques.

4.30 Nous utiliserons l’induction mathématique pour démontrer que le programme des tours de Hanoieffectue 2n – 1 déplacements de disques. La base est établie dans l’exemple 4.15. Pour déplacern + 1 disques, vous devez effectuer 2n – 1 déplacements qui vous permettront de transférer tousles disques sauf le dernier dans la tour B (d’après l’hypothèse inductive). Vous aurez ensuitebesoin d’effectuer un déplacement pour transférer le dernier disque dans la tour C et de 2n – 1déplacements supplémentaires pour transférer les disques restants de la tour B à la tour C au-des-sus du dernier disque dont nous venons de parler. Le total est donc de (2n – 1) + 1 + (2n – 1) =2n + 1 – 1.

4.31 En ce qui concerne la fonction d’Ackermann de l’exercice 4.14, des exceptions sont lancées pourm = 17 lorsque n = 2, pour m = 5 lorsque n = 3, pour m = 4 lorsque n = 4 et pour m = 3 lorsque n= 5. Le domaine calculable de cette fonction est donc limité à 0 ≤ m ≤ 16 lorsque n = 2, 0 ≤ m ≤ 4lorsque n = 3, 0 ≤ m ≤ 3 lorsque n = 4 et à 0 ≤ m ≤ 2 lorsque n = 5.

Page 107: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 97

4.32 L’arbre d’appels de l’exemple 4.15 est le suivant :

4.33 Voici un exemple d’implémentation récursive plus précise des fonctions sinus et cosinus :

• public class Pr0433• public static void main(String[] args)• for (double x=0. ; x<1.0; x += 0.1)• System.out.println(s(x) + "\t" + Math.sin(x));• for (double x=0. ; x<1.0; x += 0.1)• System.out.println(c(x) + "\t" + Math.cos(x));• • public static double s(double x)• if (Math.abs(x) < 0.00005) return x - x*x*x/6;• return 2*s(x/2)*c(x/2);• • public static double c(double x)• if (Math.abs(x) < 0.00005) return 1.0 - x*x/2;• return 1 - 2*s(x/2)*s(x/2);• •

4.34 Voici un autre exemple d’implémentation récursive plus précise des fonctions sinus et cosinus :

• public class Pr0434• public static void main(String[] args)• for (double x=0. ; x<1.0; x += 0.1)• System.out.println(s(x) + "\t" + Math.sin(x));• for (double x=0. ; x<1.0; x += 0.1)• System.out.println(c(x) + "\t" + Math.cos(x));• • public static double s(double x)• if (Math.abs(x) < 0.005) return x*(1 - x*x/6*(1-x*x/20));• return 2*s(x/2)*c(x/2);• •• public static double c(double x)• if (Math.abs(x) < 0.005) return 1.0 - x*x/2*(1-x*x/12);• return 1 - 2*s(x/2)*s(x/2);• •

h(4,A,B,C) h(1,A,B,C)

h(3,A,C,B)

h(3,B,A,C)

h(1,A,C,B)

h(1,B,A,C)

h(2,A,B,C)

h(2,C,A,B)

h(2,B,C,A)

h(2,A,B,C)

h(1,B,A,C)

h(1,A,B,C)

h(1,C,A,B)

h(1,B,C,A)

h(1,A,B,C)

h(1,C,B,A)

h(1,A,C,B)

h(1,B,A,C)

h(1,C,B,A)

h(1,A,C,B)

h(1,A,C,B)

h(1,B,A,C)

Page 108: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

98 Récursivité

4.35 L’implémentation récursive mutuelle des fonctions sinus et cosinus hyperboliques est la suivante :

• public class Pr0435• public static void main(String[] args)• for (double x=0. ; x<1.0; x += 0.1)• System.out.println(s(x) + "\t" + sinh(x));• for (double x=0. ; x<1.0; x += 0.1)• System.out.println(c(x) + "\t" + cosh(x));• •• public static double s(double x)• if (Math.abs(x) < 0.005) return x + x*x*x/6;• return 2*s(x/2)*c(x/2);• •• public static double c(double x)• if (Math.abs(x) < 0.005) return 1.0 + x*x/2;• return 1 + 2*s(x/2)*s(x/2);• •• public static double sinh(double x)• return (Math.exp(x) - Math.exp(-x))/2.0;• •• public static double cosh(double x)• return (Math.exp(x) + Math.exp(-x))/2.0;• •

4.36 L’implémentation récursive de la fonction tangente est la suivante :

• public static double t(double x)• if (-0.005 < x && x < 0.005) return x + x*x*x/3; // base• return 2*t(x/2)/(1 - t(x/2)*t(x/2)); // récursivité•

4.37 L’évaluation récursive d’une fonction polynomiale est la suivante :

• public static double p(double[] a, double x)• // renvoie a[0] + a[1]*x + a[2]*x*x + ...• return p(a,x,0);• •• private static double p(double[] a, double x, int k)• // renvoie a[k] + a[k+1]*x + a[k+2]*x*x + ...• if (k == a.length) return 0; // base• return a[k] + x*p(a,x,k+1); // récursivité•

Page 109: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 5

Collections

Une collection est un conteneur d’objets. Java 1.2 a formalisé ce concept avec son framework de collec-tions composées d’interfaces et de classes.

5.1 FRAMEWORK DE COLLECTIONS JAVA

Le paquetage java.util définit le framework suivant composé de huit interfaces pour les collections :

Interface Description

Collection Collection d’éléments.

List Séquence d’éléments.

Set Collection d’éléments uniques.

SortedSet Collection triée d’éléments uniques.

Map Collection de paires (clé, valeur) avec des clés obligatoirement uniques.

SortedMap Collection triée de paires (clé, valeur) avec des clés obligatoirement uniques.

Iterator Objet capable de parcourir une collection.

ListIterator Objet capable de parcourir une séquence.

Page 110: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

100 Collections

Le graphique suivant illustre les relations entre les interfaces et les classes qui les implémentent :

Les lignes en pointillés connectent les interfaces situées à droite du graphique aux classes de la partiegauche qui les implémentent directement. Par exemple, la classe Hashtable implémente l’interfaceMap. Dans les classes comme dans les interfaces, l’héritage est indiqué par des lignes continues. Parexemple, la classe Stack étend la classe Vector et l’interface SortedSet étend l’interface Set.

Remarquez que chacune des quatre interfaces principales (Collection, List, Set et Map) estimplémentée par une classe abstract correspondante (AbstractCollection, AbstractList,AbstractSet et AbstractMap).

Remarquez également que le nom de la plupart des classes de collection concrètes est composé de lastructure de données utilisée et de l’interface implémentée. Ainsi, la classe ArrayList utilise untableau pour implémenter l’interface List, la classe HashSet utilise une table de hachage pour implé-menter l’interface Set et la classe TreeMap utilise un arbre pour implémenter l’interface Map.

Ce chapitre est consacré aux implémentations directes des interfaces Collection et Iterator.L’implémentation des six autres interfaces sera abordée dans les chapitres 7, 13 et 14.

5.2 INTERFACE CollectionComme vous avez pu le constater dans le graphique précédent, l’interface Collection définit le fra-mework de toutes les classes conteneurs Java. Elle est définie de la façon suivante dans le paquetagejava.util :

public interface Collection public boolean add(Object object); public boolean addAll(Collection collection); public void clear(); public boolean contains(Object object); public boolean containsAll(Collection collection); public boolean equals(Object object); public int hashCode();

Object

AbstractCollection

AbstractList

ArrayList

AbstractSequentialList

LinkedList

Vector

Stack

AbstractSet

HashSet

TreeSet

AbstractMap

HashMap

TreeMap

Arrays

BitSet

Collections

Dictionary

Hashtable

Properties

Collection

List

Set

SortedSet

Map

SortedMap

Map

WeakHashMap

Iterator

ListIterator

Page 111: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe AbstractCollection 101

public boolean isEmpty(); public Iterator iterator(); public boolean remove(Object object); public boolean removeAll(Collection collection); public boolean retainAll(Collection collection); public int size(); public Object[] toArray(); public Object[] toArray(Object[] objects);

5.3 CLASSE AbstractCollectionLa classe AbstractCollection est une implémentation partielle de l’interface Collection. Elleimplémente autant d’éléments que possible sans spécifier la structure de stockage utilisée par la collec-tion. Elle est définie de la façon suivante dans le paquetage java.util :

public class AbstractCollection implements Collection public boolean add(Object object) public boolean addAll(Collection collection) public void clear() public boolean contains(Object object) public boolean containsAll(Collection collection) public boolean isEmpty() abstract public Iterator iterator() public boolean remove(Object object) public boolean removeAll(Collection collection) public boolean retainAll(Collection collection) abstract public int size() public Object[] toArray() public Object[] toArray(Object[] objects) public String toString()

Notez que cette classe implémente les 15 méthodes spécifiées par l’interface Collection, àl’exception de equals() et hashCode() qui sont définies dans la classe Object (reportez-vous à lasection 3.4) et qui seront remplacées par des extensions de la classe AbstractCollection. En outre,la classe AbstractCollection remplace la méthode toString() de la classe Object.

Si AbstractCollection ne remplace pas les méthodes equals() et hashCode() de la classeObject, c’est parce que celles-ci ne peuvent pas être complètement définies si vous ignorez quellestructure de données implémentera le stockage des éléments du conteneur. En revanche, Abstract-Collection est en mesure de remplacer la méthode toString() de la classe Object, comme illustrédans l’exemple suivant.

Exemple 5.1 Méthode toString() définie dans la classe AbstractCollection

public String toString() if (isEmpty()) return "[]"; Iterator it = iterator(); String str = "[" + it.next(); while (it.hasNext()) str += ", " + it.next(); return str + "]";

Cette méthode utilise l’itérateur renvoyé par la méthode iterator() de la classe afin de parcourirla collection et d’obtenir une image chaîne de tous ses éléments. Pour travailler sur l’instance d’unesous-classe concrète de la classe AbstractCollection, il suffit de se baser sur cette implémenta-

Page 112: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

102 Collections

tion correcte de la méthode iterator(). C’est la raison pour laquelle AbstractCollectiondéfinit une méthode iterator() abstraite : de cette façon, elle sera toujours implémentée par unesous-classe concrète.La méthode size() de la classe AbstractCollection est définie comme étant abstraite pour lamême raison.Cette classe est utilisée par d’autres méthodes implémentées dans le code que nous venons de voir(exemple 5.2).

Exemple 5.2 Méthode isEmpty() définie dans la classe AbstractCollection

public boolean isEmpty() return size() == 0;

5.4 CLASSE BagUn sac (bag), également qualifié de multi-ensemble, est une collection d’éléments susceptibles de conte-nir des copies. En effet, les ensembles standard n’autorisent pas les copies. Dans l’exemple suivant, vousverrez comment utiliser un tableau d’Objects pour implémenter une classe Bag.

Exemple 5.3 Classe Bag

public class Bag extends AbstractCollection private Object[] objects; private int size=0; // nombre d’objets dans le sac private static final int CAPACITY=16; // capacité par défaut

private void resize(int capacity) // augmente la taille de objects[] jusqu’à une capacité donnée // reportez-vous à l’exemple 5.4

public Bag() // construit un sac vide avec la capacité par défaut objects = new Object[CAPACITY];

public Bag(int capacity) // construit un sac vide avec la capacité donnée // reportez-vous à l’exercice d’entraînement 5.1

public Bag(Collection collection) // construit un sac contenant les objets de // la collection donnée en doublant sa capacité // reportez-vous à l’exemple 5.5

public Bag(Object[] objects) // construit un sac contenant les objets du // tableau donné en doublant sa capacité // reportez-vous à l’exercice d’entraînement 5.2

public boolean add(Object object) // ajoute l’objet donné au sac // reportez-vous à l’exemple 5.6

public boolean addAll(Collection collection) // ajoute tous les objets de la collection donnée dans le sac // reportez-vous à l’exercice d’entraînement 5.3

Page 113: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Bag 103

public void clear() // supprime tous les objets du sac // reportez-vous à l’exercice d’entraînement 5.4

public boolean contains(Object object) // renvoie true si l’objet donné // est égal à un objet du sac // reportez-vous à l’exemple 5.7

public boolean containsAll(Collection collection) // renvoie true si chaque objet de la collection donnée // est égal à un objet du sac, c’est-à-dire // si la collection donnée est un sous-ensemble du sac; // reportez-vous à l’exercice d’entraînement 5.5

private static int frequency(Collection x, Object object) // renvoie le nombre d’objets de la collection donnée // qui sont égaux à l’objet donné // reportez-vous à l’exercice d’entraînement 5.6

public boolean equals(Object object) // renvoie true si l’objet donné est un sac // et a le même contenu que le sac donné // reportez-vous à l’exemple 5.8

public int hashCode() // renvoie le code de hachage du sac, c’est-à-dire la somme // de tous les codes de hachage de ses éléments // reportez-vous à l’exercice d’entraînement 5.7

public boolean isEmpty() // renvoie true si le sac est vide return size == 0;

public Iterator iterator() // renvoie un itérateur sur le sac // reportez-vous à l’example 5.12

public boolean remove(Object object) // supprime l’un des objets donnés du sac; // renvoie true si le sac a été modifié; // reportez-vous à l’exercice d’entraînement 5.8

public boolean removeAll(Object object) // supprime la totalité de l’objet donné du sac; // renvoie true si le sac a été modifié; // reportez-vous à l’exemple 5.9

public boolean removeAll(Collection collection) // supprime du sac tous les objets se trouvant également dans // la collection donnée, réduisant ainsi ce sac à son // intersection théorique avec cette collection; // renvoie true si le sac a été modifié; // reportez-vous à l’exercice d’entraînement 5.9

public boolean retainAll(Collection collection) // supprime du sac tous les objets ne se trouvant pas dans la // collection donnée, réduisant ainsi le sac au complément théorique // de cette collection;

Page 114: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

104 Collections

// renvoie true si le sac a été modifié; // reportez-vous à l’exercice d’entraînement 5.10

public int size() // renvoie le nombre d’objets contenus dans le sac; return size;

public Object[] toArray() // renvoie un tableau dont les éléments se trouvent dans le sac; // reportez-vous à l’exemple 5.10

public Object[] toArray(Object[] objects) // renvoie un tableau dont les éléments se trouvent dans le sac // s’il est plus grand que le tableau object[] donné; // sinon, le tableau donné est renvoyé après suppression // de tous ses éléments, puis il est chargé avec les éléments du sac // et null pour combler les vides; // reportez-vous à l’exercice d’entraînement 5.11

public String toString() // renvoie une chaîne avec le contenu du sac // reportez-vous à l’exercice d’entraînement 5.12

Exemple 5.4 Implémenter la méthode Bag.resize(int)

Cette méthode utilitaire privée est intégrée afin de faciliter l’expansion du tableau objects[] lors-que de nouveaux éléments sont ajoutés au sac.

private void resize(int capacity) // augmente la taille du tableau objects[] jusqu’à la // capacité donnée if (capacity <= this.capacity) return; Object[] temp = objects; objects = new Object[capacity]; for (int i=0; i<size; i++) objects[i] = temp[i];

Si la capacity donnée n’est pas plus importante que la capacity courante, la méthode renvoie letableau sans rien changer. Dans le cas contraire, elle copie tout le tableau objects dans un tableautemp, réaffecte la référence objects à un nouveau tableau de taille équivalente à la capacitydonnée, tableau dans lequel elle copie le tableau temp.

Exemple 5.5 Implémenter un constructeur Bag(Collection)

public Bag(Collection collection) // construit un sac contenant les objets de // la collection donnée en doublant sa capacité objects = new Object[2*collection.size()]; for (Iterator it = collection.iterator(); it.hasNext(); ) objects[size++] = it.next();

Pour créer un Bag contenant les éléments de la collection donnée, cette méthode alloue d’abordle tableau objects avec le double des composants de cette collection (afin de permettre l’augmen-tation ultérieure du tableau).

Page 115: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Bag 105

Elle utilise ensuite un itérateur pour parcourir la collection et copie chaque variable de référencede ses objets dans le nouveau tableau objects du sac. Notez que les objets qui composent la collec-tion ne sont pas copiés dans notre exemple qui ne propose qu’une nouvelle structure de données deréférences à ces objets.Étant donné que la collection donnée implémente l’interface Collection, vous aurez nécessai-rement une méthode iterator() qui renverra un objet itérateur capable de parcourir cette collec-tion. En outre, puisque cet objet itérateur implémente l’interface java.util.Iterator (reportez-vous à la section 5.5), vous retrouverez nécessairement les méthodes next() et hasNext() utili-sées dans notre exemple. Elles permettent l’utilisation d’une boucle for afin de parcourir la col-lection et d’accéder à chacun de ses éléments via la méthode next().

Exemple 5.6 Implémenter la méthode Bag.add(Object)

public boolean add(Object object) // ajoute l’objet donné au sac if (size == objects.length) resize(2*objects.length); objects[size++] = object; return true;

Si le sac est plein, la méthode private resize() est appelée en premier lieu afin de doubler lacapacité du tableau objects[]. L’objet donné est ensuite ajouté à la séquence d’éléments se trou-vant déjà dans le tableau.Lorsque vous utilisez l’interface Collection, n’oubliez pas que la méthode add(Object) doitrenvoyer une valeur boolean afin d’indiquer si l’opération d’ajout est réussie ou non. Avec cetteimplémentation, l’ajout sera toujours réussi (à moins que l’ordinateur ne se retrouve à cours demémoire), c’est pourquoi true sera automatiquement renvoyé. Nous verrons dans d’autres applica-tions que la méthode add(Object) peut parfois renvoyer false.Voici un pilote test de la méthode add(Object) :

public class Testing public static void main(String[] args) String[] food = "fromage", "jambon", "rhum", "thé" ; Bag foodBag = new Bag(food); System.out.println(foodBag); foodBag.add("figue"); System.out.println(foodBag);

Notez que ce programme teste également le constructeur Bag(Object[]).

Exemple 5.7 Implémenter une méthode Bag.contains(Object)

Cette méthode implémente l’algorithme de recherche séquentielle (reportez-vous à la section 2.4)afin de retrouver l’object donné dans le sac. Elle effectue une recherche dans le tableauobjects[] et renvoie true dès que l’un de ses éléments est égal à l’object donné. Si la boucle setermine une fois que tous les éléments ont été vérifiés, la méthode renvoie false :

public boolean contains(Object object) // renvoie true si l’objet donné // est égal à un objet du sac

fromage, jambon, rhum, thé fromage, jambon, rhum, thé, figue

Page 116: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

106 Collections

for (int i=0; i<size; i++) if (object.equals(objects[i])) return true; return false;

Remarquez que cette méthode utilise la méthode Object.equals() afin de tester l’égalité. Cetappel renvoie true uniquement si les deux références object et objects[i] concernent toutesles deux le même objet.Voici le pilote test de la méthode contains(Object) :

public class Testing public static void main(String[] args) String[] food = "fromage", "jambon", "rhum", "thé" ; Bag foodBag = new Bag(food); System.out.println(foodBag); if (foodBag.contains("figue")) System.out.println("figue"); else System.out.println("pas de figue"); if (foodBag.contains("jambon")) System.out.println("jambon"); else System.out.println("pas de jambon");

Notez que les deux occurrences du littéral chaîne doivent faire référence au même objet se trouvanten mémoire.

Exemple 5.8 Implémenter une méthode Bag.equals(Object)

public boolean equals(Object object) // renvoie true si l’objet donné est un sac // et a le même contenu que le sac donné if (object == this) return true; if (object.getClass() != this.getClass()) return false; if (object.hashCode() != this.hashCode()) return false; Collection collection = (Collection)object; if (collection.size() != this.size()) return false; if (!collection.containsAll(this)) return false; if (!this.containsAll(collection)) return false; for (int i=0; i<size; i++) Object x = objects[i]; if (frequency(collection,x) != frequency(this,x)) return false; return true;

Exemple 5.9 Implémenter une méthode Bag.removeAll(Object)

public boolean removeAll(Object object) // supprime tous les objets donnés du sac; // renvoie true si le sac a été modifié; boolean modified=false; for (int i=0; i<size; i++) if (object.equals(objects[i])) objects[i] = objects[--size];

fromage, jambon, rhum, thé pas de figuejambon

Page 117: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Bag 107

modified = true; return modified;

Exemple 5.10 Implémenter une méthode Bag.toArray()

public Object[] toArray() // renvoie un tableau dont les éléments sont dans le sac; Object[] objects = new Object[size]; for (int i=0; i<size; i++) objects[i] = this.objects[i]; return objects;

Exemple 5.11 Tester la classe Bag

Voici un pilote test de la classe Bag définie dans l’exemple 5.3 :

public class Ex0511 public static void main(String[] args) String[] etats = "Maine", "Idaho", "Iowa", "Ohio", "Utah" ; Bag myCollection = new Bag(etats); print(myCollection); myCollection.add("Alaska"); print(myCollection); if (myCollection.remove("Ohio")) print(myCollection); else System.out.println("Object \"Ohio\" introuvable."); Iterator it = myCollection.iterator(); while (it.hasNext()) String s = (String)it.next(); System.out.println("\ts = \""+s+"\""); if (s.charAt(0) == ’I’) it.remove(); System.out.println("\tObjet \""+s+"\" a été supprimé."); print(myCollection); private static void print(Bag collection) System.out.println("size() = " + collection.size()); Object[] objects = collection.toArray(); for (int i=0; i<objects.length; i++) System.out.println("\tobjects[" + i + "] = " + objects[i]); if (collection.contains("Iowa")) System.out.println("\tContient \"Iowa\""); else System.out.println("\tNe contient pas \"Iowa\""); String[] etats = "Maine", "Idaho", "Iowa", "Ohio", "Utah" ; Bag newCollection = new Bag(etats); if (newCollection.containsAll(collection)) System.out.println("\tnewC contient myC"); else System.out.println("\tnewC ne contient pas myC"); if (collection.containsAll(newCollection)) System.out.println("\tmyC contient newC"); else System.out.println("\tmyC ne contient pas newC"); if (collection.equals(newCollection)) System.out.println("\tmyC égal à newC"); else System.out.println("\tmyC n’est pas égal à newC"); if (newCollection.equals(collection)) System.out.println("\tnewC est égal à myC");

Page 118: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

108 Collections

else System.out.println("\tnewC n’est pas égal à myC");

size() = 5objects[0] = Maineobjects[1] = Idahoobjects[2] = Iowaobjects[3] = Ohioobjects[4] = UtahContains "Iowa"newC contient myCmyC contient newCmyC est égal à newCnewC est égal à myCsize() = 6objects[0] = Maineobjects[1] = Idahoobjects[2] = Iowaobjects[3] = Ohioobjects[4] = Utahobjects[5] = AlaskaContient "Iowa"newC ne contient pas myCmyC contient newCmyC n’est pas égal à newCnewC n’est pas égal à myCsize() = 5objects[0] = Maineobjects[1] = Idahoobjects[2] = Iowaobjects[3] = Alaskaobjects[4] = UtahContient "Iowa"newC ne contient pas myCmyC ne contient pas newCmyC n’est pas égal à newCnewC n’est pas égal à myCs = "Maine"s = "Idaho"Objet "Idaho" supprimé.s = "Utah"s = "Iowa"Objet "Iowa" supprimé.s = "Alaska"size() = 3objects[0] = Maineobjects[1] = Utahobjects[2] = AlaskaNe contient pas "Iowa"newC ne contient pas myCmyC ne contient pas newCmyC n’est pas égal à newCnewC n’est pas égal à myC

Page 119: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Interface Iterator 109

5.5 INTERFACE IteratorUn itérateur est un objet qui parcourt une structure de données et en vérifie chaque élément une seulefois.

Étant donné que le temps est linéaire, un itérateur linéarise la structure qu’il parcourt. Plusieursitérateurs peuvent vérifier une structure simultanément, chacun d’entre eux étant en mesure d’identifierun élément de la structure et d’en permettre l’accès.

Voici l’interface Iterator telle qu’elle est définie dans le paquetage java.util :

public interface Iterator public boolean hasNext(); public Object next(); public void remove();

Les itérateurs sont généralement implémentés comme des classes internes anonymes définies dans laméthode iterator() de la classe de collection associée, comme illustré dans l’exemple suivant.

Exemple 5.12 Implémenter la méthode Iterator dans la classe Bag

Voici maintenant l’implémentation de la méthode iterator() pour la classe Bag définie dansl’exemple 5.3 :

public Iterator iterator() // renvoie un itérateur sur le sac return new Iterator() // définition du constructeur incorporée :

private int cursor=0;

public boolean hasNext() return cursor<size;

public Object next() if (cursor>=size) return null; return objects[cursor++];

public void remove() objects[--cursor] = objects[--size]; objects[size] = null;

; // notez le point-virgule obligatoire

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

5.1 Qu’est-ce que le framework de collections Java ?

5.2 Pourquoi les méthodes iterator() et size() sont-elles déclarées comme abstract dans ladéfinition de la classe AbstractCollection ?

5.3 Qu’est-ce qu’un itérateur ?

Page 120: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

110 Collections

RÉPONSES¿RÉPONSES

5.1 Le framework de collections Java est un groupe d’interfaces et de classes défini dans le paque-tage java.util. Il facilite la définition des classes de structure de données via l’héritage.

5.2 Les méthodes iterator() et size() sont déclarées comme abstract dans la classe Abs-tractCollection parce qu’elles sont utilisées par d’autres méthodes telles que toString()et isEmpty() implémentées dans cette classe. Ces autres méthodes peuvent ainsi être utiliséesdans des sous-classes concrètes.

5.3 Un itérateur est un objet qui passe séquentiellement d’un composant à l’autre via une structurede collection et qui fournit l’accès à ces éléments.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

5.1 Implémentez le constructeur suivant pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public Bag(int capacity)• // construit un sac vide avec la capacité donnée

5.2 Implémentez le constructeur suivant pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public Bag(Object[] objects)• // construit un sac contenant les objets du• // tableau donné en doublant sa capacité

5.3 Implémentez le constructeur suivant pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public boolean addAll(Collection collection)• // ajoute tous les objets de la collection donnée• // dans le sac

5.4 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public void clear()• // supprime tous les objets du sac

5.5 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public boolean containsAll(Collection collection)• // renvoie true si chaque objet de la collection donnée• // est égal à un objet du sac ; c’est-à-dire,• // si la collection donnée est un sous-ensemble du sac ;

5.6 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• private static int frequency(Collection x, Object object)• // renvoie le nombre d’objets de la collection donnée• // qui sont égaux à l’objet donné

5.7 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public int hashCode()• // renvoie un code de hachage pour le sac qui est la somme• // des codes de hachage de ses éléments

Page 121: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 111

5.8 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public boolean remove(Object object)• // supprime l’un des objets donné du sac;• // renvoie true si le sac a été modifié;

5.9 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public boolean removeAll(Collection collection)• // supprime du sac tous les objets se trouvant également dans la• // collection donnée, réduisant ainsi le sac à son• // intersection théorique avec cette collection;• // renvoie true si le sac a été modifié;

5.10 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public boolean retainAll(Collection collection)• // supprime du sac tous les objets ne se trouvant pas dans la• // collection donnée, réduisant ainsi le sac• // à son complément théorique par rapport à la collection;• // renvoie true si le sac a été modifié;

5.11 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public Object[] toArray(Object[] objects)• // renvoie un tableau dont les éléments se trouvent dans le sac• // s’il est plus long que le tableau object[] donné;• // sinon, le tableau donné est renvoyé après suppression de• // tous ses éléments, puis il est chargé avec les éléments du sac• // avec null pour combler les vides;

5.12 Implémentez la méthode suivante pour la classe Bag (reportez-vous à l’exemple 5.3) :

• public String toString()• // renvoie une chaîne avec le contenu du sac

SOLUTIONS¿SOLUTIONS

5.1 L’implémentation du constructeur (int) pour la classe Bag est la suivante :

• public Bag(int capacity)• // construit un sac vide avec la capacité donnée• objects = new Object[capacity];•

5.2 L’implémentation du constructeur Bag(Object[]) pour la classe Bag est la suivante :

• public Bag(Object[] objects)• // construit un sac contenant les objets du• // tableau donné en doublant sa capacité• this.objects = new Object[2*objects.length];• for (int i=0; i<objects.length; i++)• this.objects[size++] = objects[i];•

5.3 L’implémentation de la méthode addAll(Collection) pour la classe Bag :

• public boolean addAll(Collection collection)• // ajoute tous les objets de la collection donnée dans le sac

Page 122: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

112 Collections

• resize(2*collection.size());• for (Iterator it = collection.iterator(); it.hasNext(); )• objects[size++] = it.next();• return true;•

5.4 L’implémentation de la méthode clear() pour la classe Bag est la suivante :

• public void clear()• // supprime tous les objets du sac• for (int i=0; i<size; i++)• objects[i] = null;• size = 0;•

5.5 L’implémentation de la méthode containsAll(Collection) pour la classe Bag est la sui-vante :

• public boolean containsAll(Collection collection)• // renvoie true si chaque objet de la collection donnée• // est égal à un objet du sac, c’est-à-dire• // si la collection donnée est un sous-ensemble du sac;• for (Iterator it = collection.iterator(); it.hasNext(); )• if (!this.contains(it.next())) return false;• return true;•

5.6 L’implémentation de la méthode frequency(Collection,Object) pour la classe Bag est lasuivante :

• private static int frequency(Collection x, Object object)• // renvoie le nombre d’objets de la collection donnée• // étant égaux à l’objet donné• int count=0;• for (Iterator it = x.iterator(); it.hasNext(); )• if (object.equals(it.next())) ++count;• return count;•

5.7 L’implémentation de la méthode hashCode() pour la classe Bag est la suivante :

• public int hashCode()• // renvoie un code de hachage pour le sac égal à la somme• // de tous les codes de hachage de ses éléments• int code=0;• for (int i=0; i<size; i++)• code += objects[i].hashCode();• return code;•

5.8 L’implémentation de la méthode remove(Object) pour la classe Bag est la suivante :

• public boolean remove(Object object)• // supprime l’un des objets donnés du sac;• // renvoie true si le sac a été modifié;• for (int i=0; i<size; i++)• if (object.equals(objects[i]))• objects[i] = objects[--size];• return true;• • return false;•

Page 123: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 113

5.9 L’implémentation de la méthode removeAll(Collection) pour la classe Bag est la sui-vante :

• public boolean removeAll(Collection collection)• // supprime du sac tous les objets se trouvant également• // dans la collection donnée, réduisant ainsi l’intersection• // théorique avec cette collection;• // renvoie true si le sac a été modifié;• boolean modified=false;•• for (Iterator it = collection.iterator(); it.hasNext(); )• if (this.remove(it.next())) modified = true;• return modified;•

5.10 L’implémentation de la méthode retainAll(Collection) pour la classe Bag est la sui-vante :

• public boolean retainAll(Collection collection)• // supprime du sac tous les objets ne se trouvant pas dans• // la collection donnée, réduisant ainsi le sac• // à son complément théorique par rapport à la collection;• // renvoie true si le sac a été modifié;• boolean modified=false;•• for (int i=0; i<size; i++)• if (!collection.contains(objects[i]))• remove(objects[i]);• modified = true;• • return modified;•

5.11 L’implémentation de la méthode toArray(Object[]) pour la classe Bag est la suivante :

• public Object[] toArray(Object[] objects)• // renvoie un tableau dont les éléments sont dans le sac• // s’il est plus grand que le tableau object[] donné;• // sinon, le tableau donné est renvoyé après suppression• // de tous ses éléments, puis il est chargé avec les éléments• // du sac et null pour combler les vides;• if (size > objects.length) objects = this.toArray();• • for (int i=0; i<size; i++)• objects[i] = this.objects[i];• for (int i=size; i<objects.length; i++)• objects[i] = null;• • return objects;•

5.12 L’implémentation de la méthode toString() pour la classe Bag est la suivante :

• public String toString()• // renvoie une chaîne avec le contenu du sac• String s=" ";• if (size>0) s += this.objects[0];• for (int i=1; i<size; i++)• s += ", " + this.objects[i];• return s + " ";•

Page 124: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie
Page 125: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 6

Piles

Une pile est un conteneur qui implémente le protocole dernier entré, premier sorti(LIFO, Last-in-First-Out). Cela signifie que le seul objet accessible dans le conteneurest le dernier à avoir été entré. Pour comprendre de quoi il retourne, imaginez quevous souhaitiez lire le dernier livre d’une pile. Avant de l’atteindre, vous devrez retirertous les livres qui se trouvent au-dessus.

6.1 CLASSE JAVA StackComme nous l’avons déjà vu dans le diagramme de la section 5.1, leframework de collections Java comprend une classe Stack qui estdéfinie de la façon suivante dans le paquetage java.util :

public class Stack extends Vector public boolean empty() return size()==0;

public Object peek() if (size()==0) throw new EmptyStackException(); return elementAt(size()-1);

public Object pop() Object object = peek(); removeElementAt(size()-1); return object;

public Object push(Object object) addElement(object); return object;

public int search(Object object) int i=lastIndexOf(object); if (i<0) return -1; // l’objet n’est pas dans la pile return size() - i;

BrésilCanadaFranceMexiqueRussieSuèdeCanada

Object

AbstractCollection

AbstractList

Vector

Stack

Page 126: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

116 Piles

public Stack()

La classe Stack est une sous-classe de Vector (reportez-vous au chapitre 2). Elle implémente unepile sous forme de vecteur, le dernier élément de ce dernier se trouvant en haut de la pile. La méthodeempty() renvoie true si et seulement si le nombre d’éléments de la pile est égal à zéro.

La méthode peek() renvoie l’objet situé en haut de la pile sans le supprimer. Elle utilise la méthodeVector.elementAt(int) pour accéder au dernier élément du vecteur. Si la pile est vide, l’exceptionEmptyStackException est lancée.

La méthode pop() renvoie l’objet du haut de la pile après l’avoir supprimé. Elle utilise la méthode Vector.removeElementAt(int). Étant donné que cette méthode appelle la méthode peek(),

elle lance également une exception EmptyStackException si la pile est vide.La méthode push(Object) insère l’objet donné en haut de la pile. Elle utilise la méthode Vec-

tor.addElementAt(Object) qui ajoute l’élément à la fin du vecteur.La méthode search(Object) renvoie la position de l’objet donné dans la pile ou –1 si l’objet n’est

pas dans la pile. Elle appelle la méthode Vector.lastIndexOf(Object) qui utilise la méthodeequals() pour comparer l’objet donné à ceux du vecteur.

Si plusieurs objets sont égaux à l’objet donné, c’est la position du dernier objet de la pile qui est ren-voyée, c’est-à-dire celle de l’objet le plus près du haut de la pile. Les numéros des positions sont calculésà l’aide de l’indexation de base 1, c’est pourquoi l’élément supérieur de la pile est à la position 1.

Exemple 6.1 Visualiser une pile vide

public class Ex0601 public static void main(String[] args) java.util.Stack stack = new java.util.Stack(); System.out.println("stack.size() = " + stack.size()); System.out.println("stack.peek() = " + stack.peek());

Ce programme illustre le lancement d’une exception EmptyStackException si la méthodepeek() est appelée sur une pile vide.

Exemple 6.2 Gérer une exception EmptyStackException

import java.util.Stack;

public class Test public static void main(String[] args) Stack stack = new Stack(); print(stack); private static void print(Stack stack) System.out.println("stack.size() = " + stack.size()); try System.out.println("stack.peek() = " + stack.peek());

stack.size() = 0java.util.EmptyStackException at java.util.Stack.peek(Stack.java:86) at Ex0601.main(Ex0601.java:10)

Page 127: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Java Stack 117

catch(java.util.EmptyStackException e) System.out.println(e + " : La pile est vide.");

Ce programme illustre un procédé simple de gestion d’une exception EmptyStackException.

Exemple 6.3 Tester les méthodes push() et pop()

import java.util.Stack;

public class Ex0603 public static void main(String[] args) Stack stack = new Stack(); stack.push("Angleterre"); stack.push("Canada"); stack.push("France"); stack.push("Mexique"); stack.push("Russie"); stack.push("Danemark"); stack.push("Angleterre"); stack.push("Turquie"); print(stack); System.out.println("stack.search(\"Angleterre\") = " + stack.search("Angleterre")); System.out.println("stack.pop() = " + stack.pop()); System.out.println("stack.pop() = " + stack.pop()); print(stack); System.out.println("stack.search(\"Angleterre\") = " + stack.search("Angleterre")); private static void print(Stack stack) System.out.println(stack); System.out.println("stack.size() = " + stack.size()); try System.out.println("stack.peek() = " + stack.peek()); catch(java.util.EmptyStackException e) System.out.println(e + " : La pile est vide.");

stack.size() = 0java.util.EmptyStackException : La pile est vide.

[Angleterre, Canada, France, Mexique, Russie, Danemark, Angleterre, Turquie]stack.size() = 8stack.peek() = Turquiestack.search("Angleterre") = 2stack.pop() = Turquiestack.pop() = Angleterre[Angleterre, Canada, France, Mexique, Russie, Danemark]stack.size() = 6stack.peek() = Danemarkstack.search("Angleterre") = 6

Page 128: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

118 Piles

Après avoir poussé les huit objets (toutes les chaînes) dans la pile, ce programme appelle sa méthodelocale print(Stack) afin d’afficher les données relatives à la pile. Il recherche ensuite "Angle-terre", retire "Turquie" du sommet de la pile, puis appelle à nouveau print(Stack). Notezque search(Object) renvoie 2, ce qui signifie qu’il existe une copie de "Angleterre" endeuxième position à partir du haut de la pile (une autre copie se trouve en bas de la pile).La méthode pop() est ensuite appelée deux fois, ce qui permet de supprimer la première occurrencede "Turquie", puis "Angleterre" de la pile. L’appel final de print(Stack) indique que lapile est composée de six éléments, avec "Danemark" en haut et "Angleterre" en sixième posi-tion à partir du haut.Notez que l’appel System.out.println(stack) invoque la méthode Vector.toString()qui renvoie une chaîne affichant une liste d’éléments délimités par des crochets [].

6.2 APPLICATIONS DES PILESBien que la structure de données stack soit l’une des plus simples, elle est fondamentale dans certainesapplications importantes, notamment dans les exemples qui vont suivre.

Une expression arithmétique est qualifiée de notation suffixée (ou de notation polonaise inversée,RPN) si chaque opérateur est placé après ses opérandes. Par exemple, l’expression suffixée de 3*(4 + 5)est la suivante : 3 4 5 + * (l’expression 3*(4 + 5) est qualifiée d’expression infixée). Les expressionssuffixées sont plus faciles à traiter par une machine que les expressions infixées et ce sont les calculatri-ces RPN qui peuvent effectuer ce type de traitement.

Exemple 6.4 Utiliser une calculatrice RPN

Ce programme analyse les expressions suffixées et effectue les calculs indiqués. Il a recours à deuxpiles, la première pour cumuler les opérateurs, et la seconde pour cumuler les opérandes :

import java.util.Stack;import java.io.*;

public class Ex0604 public static void main(String[] args) boolean quit=false; String input; double x, y, z; Stack operands = new Stack(); while (!quit) input = getString("RPN> "); switch (input.charAt(0)) case ’Q’: quit = true; break;

case ’+’: y = Double.parseDouble((String)operands.peek()); operands.pop(); x = Double.parseDouble((String)operands.peek()); operands.pop(); z = x + y; System.out.println("\t" + x + "+" + y + " = " + z); operands.push(new Double(z).toString()); break;

case ’-’: y = Double.parseDouble((String)operands.peek());

Page 129: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Applications des piles 119

operands.pop(); x = Double.parseDouble((String)operands.peek()); operands.pop(); z = x - y; System.out.println("\t" + x + "-" + y + " = " + z); operands.push(new Double(z).toString()); break;

case ’*’: y = Double.parseDouble((String)operands.peek()); operands.pop(); x = Double.parseDouble((String)operands.peek()); operands.pop(); z = x * y; System.out.println("\t" + x + "*" + y + " = " + z); operands.push(new Double(z).toString()); break;

case ’/’: y = Double.parseDouble((String)operands.peek()); operands.pop(); x = Double.parseDouble((String)operands.peek()); operands.pop(); z = x / y; System.out.println("\t" + x + "/" + y + " = " + z); operands.push(new Double(z).toString()); break;

default: operands.push(input);

private static String getString(String prompt) System.out.print(prompt); InputStreamReader iSReader = new InputStreamReader(System.in); BufferedReader bReader = new BufferedReader(iSReader); String input=""; try input = bReader.readLine(); catch(IOException e) System.out.println(e); return input;

Ce programme traite l’expression suffixée 3 4 5 + * 10 / 1 – qui représente l’expressioninfixée 3*(4 + 5)/10 - 1. Chaque calcul intermédiaire est imprimé, c’est-à-dire : 4 + 5 = 9,3*9 = 27, 27/10 = 2,7 et 2,7 - 1 = 1,7.À chaque itération de la boucle while, le programme imprime l’invite RPN>, puis il lit une chaîned’entrée. Il utilise le premier caractère de cette chaîne afin de décider quelle opération effectuer. S’ils’agit d’un Q, le programme s’arrête. En revanche, s’il s’agit d’un opérateur +, -, * ou /, il procèdeà l’opération correspondante. S’il s’agit d’une autre valeur, le programme suppose que l’entrée estun opérande numérique. Dans ce cas, il utilise stringstream pour envoyer la valeur numériquedans la variable x, puis il la pousse dans la pile d’opérandes. Lorsque l’un des quatre opérateurs estentré, il dépile les deux derniers nombres de la pile d’opérandes, il effectue l’opération arithmétiquedemandée, il imprime le résultat, puis il le pousse dans la pile d’opérandes.

Page 130: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

120 Piles

Généralement, les êtres humains préfèrent les notations infixées à celles suffixées pour les opérationsarithmétiques. C’est pourquoi nous vous proposons l’exemple suivant qui convertit une expressioninfixée donnée en une expression suffixée.

Exemple 6.5 Convertir une expression infixée en expression suffixée

import java.util.Stack;import java.io.*;

public class Ex0605 public static void main(String[] args) try Stack stack = new Stack(); InputStreamReader reader = new InputStreamReader(System.in); StreamTokenizer tokens = new StreamTokenizer(reader); tokens.ordinaryChar(’/’); // sinon ce serait un commentaire tokens.eolIsSignificant(true); // false est la valeur par défaut int tokenType; System.out.print("Entrez une expression infixée : "); while ((tokenType=tokens.nextToken()) != StreamTokenizer.TT_EOL) char ch = (char)tokenType; if (tokenType==StreamTokenizer.TT_NUMBER) System.out.print(tokens.nval + " "); else if (ch==’+’ || ch==’-’ || ch==’*’ || ch==’/’) stack.push(new Character(ch)); else if (ch==’)’) System.out.print((Character)stack.pop()+" "); while (!stack.empty()) System.out.print((Character)stack.pop()+" ");

catch (Exception e) System.out.println(e);

RPN> 3RPN> 4RPN> 5RPN> + 4.0+5.0 = 9.0RPN> * 3.0*9.0 = 27.0RPN> 10RPN> / 27.0/10.0 = 2.7RPN> 1RPN> - 2.7–1 = 1.7000000000000002RPN> Q

Entrer une expression infixée : (80 - 30)*(40 + 50/10)80.0 30.0 - 40.0 50.0 10.0 / + *

Page 131: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Supprimer la récursivité 121

L’entrée est analysée par un objet tokens de type StreamTokenizer associé à l’objet Input-StreamReader. ordinaryChar(’/’) est appelée et reconnaît l’opérateur de division « / »comme caractère. La méthode eolIsSignificant(true) est ensuite appelée de façon à ce quela constante de classe TT_EOL puisse contrôler la boucle d’analyse et la terminer lorsque le caractèrede fin de ligne est détecté.À chaque itération de la boucle while, l’objet tokens obtient l’unité suivante du flux d’entrée,extrait sa représentation char et l’insère dans la variable ch. Les actions suivantes de cet objetdépendent de sa nature : un opérande numérique, l’un des quatre opérateurs arithmétiques ou lecaractère parenthèse droite ’)’. S’il s’agit d’un opérande numérique, il est imprimé immédiatementparce que les opérandes figurent devant les opérateurs dans les notations suffixées.S’il s’agit d’un opérateur arithmétique, il est poussé dans la pile. Et enfin, s’il s’agit du caractèreparenthèse droite, l’opérateur situé en haut de la pile est extrait est imprimé.Attention, dans le cas présent, l’expression d’entrée doit être mise entre parenthèses. Pensez égale-ment à insérer un espace devant l’opérateur de soustraction pour qu’il soit reconnu comme opérateurde soustraction binaire et non comme opérateur de négation unaire.

6.3 SUPPRIMER LA RÉCURSIVITÉLe système d’exploitation d’un ordinateur exécute une fonction récursive grâce à une pile qui lui permetde stocker l’état d’exécution courant chaque fois qu’il effectue un appel récursif. Par la suite, chaque foisque l’appel récursif provoque le renvoi d’une valeur, il dépile l’état de la pile d’exécution afin de pouvoirrecommencer là où il en était. Étant donné que les piles sont utilisées par le système d’exploitation pourexécuter une fonction récursive, il est évident que le programmeur doit être capable de réécrire une fonc-tion récursive de façon à utiliser une pile explicite au lieu d’effectuer des appels récursifs.

Exemple 6.6 Implémenter itérativement les tours de Hanoi

Ce programme équivaut au programme de récursivité présenté dans l’exemple 4.15 :

public class Ex0606 public static void main(String[] args) hanoi(3,’A’,’B’,’C’); // jouer avec 3 disques

private static void hanoi(int n, char x, char y, char z) java.util.Stack stack = new java.util.Stack(); stack.push(new Quad(n,x,y,z)); while (!stack.empty()) Quad quad = (Quad)stack.pop(); n = quad.n; x = quad.a; y = quad.b; z = quad.c; if (n == 1) System.out.println ("Déplacer le disque supérieur de la tour " + quad.a + " vers la tour " + quad.c); else stack.push(new Quad(n-1,y,x,z)); stack.push(new Quad(1,x,y,z)); stack.push(new Quad(n-1,x,z,y));

Page 132: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

122 Piles

class Quad public int n; public char a, b, c; public Quad(int n, char a, char b, char c) this.n = n; this.a = a; this.b = b; this.c = c;

Chaque appel récursif intégré à la version récursive de la méthode hanoi(int, char, char,char) est remplacé par un appel stack.push(Quad) et chaque retour d’un appel récursif est rem-placé par un appel stack.pop(Quad).La classe Quad contient un quadruple composé d’un entier et de trois caractères.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

6.1 Pourquoi les piles sont-elles qualifiées de structures LIFO ?

6.2 Peut-on qualifier les piles de structures :

a. LILOb. FILO

6.3 Définissez les expressions suivantes :

a. notation préfixée ;b. notation infixée ;c. notation suffixée.

6.4 Dans le cadre des notations suffixées, quelle expression suivante peut être considérée commevraie :

a. x y + z + = x y z + +b. x y + z - = x y z – +c. x y – z + = x y z + –d. x y – z – = x y z – –

RÉPONSES¿RÉPONSES

6.1 Les piles sont qualifiées de structures LIFO parce que le dernier élément inséré dans la pile esttoujours le premier à en être supprimé. LIFO est l’acronyme de l’expression anglaise Last-In-First-Out, c’est-à-dire dernier entré, premier sorti.

Déplacer le disque supérieur de la tour A vers la tour CDéplacer le disque supérieur de la tour A vers la tour BDéplacer le disque supérieur de la tour C vers la tour BDéplacer le disque supérieur de la tour A vers la tour CDéplacer le disque supérieur de la tour B vers la tour ADéplacer le disque supérieur de la tour B vers la tour CDéplacer le disque supérieur de la tour A vers la tour C

Page 133: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 123

6.2 a. Une pile ne peut pas être une structure LILO (Last-In-Last-Out, Dernier entré, dernier sorti),c’est-à-dire le contraire du protocole Dernier entré, premier sorti.

b. Une pile peut être une structure FILO (First-In-Last-Out, Premier entré, dernier sorti) qui estidentique au protocole Dernier entré, premier sorti.

6.3 a. La notation préfixée des expressions arithmétiques place les opérateurs binaires devant leursdeux opérandes. Par exemple, « x + 2 » s’écrit sous la forme « + x 2 » en notation préfixée. Enmathématiques, la notation fonctionnelle standard utilise ce format préfixé, par exemple pourf(x), sin x, etc.

b. La notation infixée des expressions arithmétiques place les opérateurs binaires entre leurs opé-randes. Il s’agit du format standard des expressions arithmétiques telles que « x + 2 ».

c. La notation suffixée des expressions arithmétiques place les opérateurs binaires après leursdeux opérandes. Par exemple, l’expression « x + 2 » s’écrit « x 2 + » en notation suffixée. Enmathématiques, la fonction factorielle utilise la notation suffixée : n!.

6.4 a. Vraie parce que (x + y) + z = x + (y + z).b. Vraie parce que (x + y) – z = x + (y – z).c. Fausse parce que (x – y) + z x - (y + z).d. Fausse parce que (x – y) – z x – (y – z).

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

6.1 Tracez le code suivant en faisant apparaître le contenu de la pile après chaque appel :

• Stack stack = new Stack();• stack.push(new Character(’A’));• stack.push(new Character(’B’));• stack.push(new Character(’C’));• stack.pop();• stack.pop();• stack.push(new Character(’D’));• stack.push(new Character(’E’));• stack.push(new Character(’F’));• stack.pop();• stack.push(new Character(’G’));• stack.pop();• stack.pop();• stack.pop();

6.2 Convertissez les expressions préfixées suivantes en expressions infixées :

a. – / + * a b c d e

b. / – a b * c + d e

c. / a + b * c – d e

6.3 Convertissez les expressions préfixées de l’exercice 6.2 en expressions suffixées.

6.4 Convertissez les expressions infixées suivantes en expressions préfixées :

a. (a + b) – (c / (d + e))b. a / ((b / c) * (d – e))c. (a / (b / c)) * (d – e)

Page 134: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

124 Piles

6.5 Convertissez les expressions infixées de l’exercice 6.4 en expressions suffixées.

6.6 Convertissez les expressions suffixées suivantes en expressions préfixées :

a. a b + c d – / e +

b. a b c + d e – * –

c. a b c d e / / / /

6.7 Convertissez les expressions suffixées de l’exercice 6.6 en expressions infixées.

6.8 Écrivez la méthode suivante en utilisant uniquement le constructeur et les méthodes push(),peek(), pop() et empty() de la classe Stack :

• private static void reverse(Stack stack);• // inverse le contenu de la pile donnée

6.9 Écrivez la méthode suivante en utilisant uniquement le constructeur et les méthodes push(),peek(), pop() et empty() de la classe Stack :

• private static Stack reversed(Stack stack);• // renvoie une nouvelle pile contenant les mêmes éléments• // que ceux de la pile donnée, mais dans l’ordre inverse

6.10 Écrivez la méthode suivante en utilisant uniquement le constructeur et les méthodes push(),peek(), pop() et empty() de la classe Stack :

• public Object penultimate(Stack stack);• // renvoie le deuxième élément de la pile donnée en partant du haut

6.11 Écrivez la méthode suivante en utilisant uniquement le constructeur et les méthodes push(),peek(), pop() et empty() de la classe Stack :

• public Object bottom();• // renvoie l’élément inférieur de la pile

6.12 Écrivez la méthode suivante en utilisant uniquement le constructeur et les méthodes push(),peek(), pop() et empty() de la classe Stack :

• public Object popBottom();• // supprime et renvoie l’élément situé en bas de la pile

6.13 Écrivez la méthode reverse() de l’exercice 6.8 en utilisant les méthodes Vector héritées.

6.14 Écrivez la méthode reverse() de l’exercice 6.9 en utilisant les méthodes Vector héritées.

6.15 Écrivez la méthode penultimate() de l’exercice 6.10 en utilisant les méthodes Vector héri-tées.

6.16 Écrivez la méthode bottom() de l’exercice 6.11 en utilisant les méthodes Vector héritées.

6.17 Écrivez la méthode popBottom() de l’exercice 6.12 en utilisant les méthodes Vector héritées.

6.18 Modifiez l’exemple 6.5 de façon à ce qu’il utilise une pile de valeurs primitives char des réfé-rences Object et implémentez votre propre classe Stack dans ce but. Utilisez un tableau pourstocker les valeurs char.

Page 135: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 125

SOLUTIONS¿SOLUTIONS

6.1 La trace est la suivante :

6.2 a. (a * b + c) / (d – e)

b. (a – b) / (c * ( d + e ))c. a / (b + (c * (d – e)))

6.3 a. a b * c + d / e –

b. a b – c d e + * /c. a b c d e – * + /

6.4 a. (a + b) – (c / (d + e)) = – + a b / c + d e

b. a / ((b / c) * (d – e)) = / a * / b c – d ec. (a / (b / c)) * (d – e) = * / a / b c – d e

6.5 a. (a + b) – (c / (d + e)) = a b + c d e + / –

b. a / ((b / c) * (d – e)) = a b c / d e – * /c. (a / (b / c)) * (d – e) = a b c / / * d e – *

6.6 a. (a + b) / (c – d) + e

b. a – (b + c) * (d – e)c. a / (b / (c / (d / e)))

6.7 a. + / + a b – c d e

b. – a * + b c – d ec. / a / b / c / d e

6.8 La méthode suivante inverse le contenu d’une pile :

• private static void reverse(Stack stack)• Stack tempStack1 = new Stack();

s.pop() s.pop()EDA

DA

s.push('G')

GEDA

s.push('F') s.pop()

FEDA

EDA

EDA

s.pop() s.push('D')

ADA

s.pop() BA

s.push('B') s.push('C')BA

CBA

s.push('A')

A

s.push('E')

Page 136: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

126 Piles

• while(!stack.empty())• tempStack1.push(stack.pop());• Stack tempStack2 = new Stack();• while(!tempStack1.empty())• tempStack2.push(tempStack1.pop());• while(!tempStack2.empty())• stack.push(tempStack2.pop());•

6.9 La méthode suivante inverse le contenu d’une pile :

• private static Stack reversed(Stack stack)• Stack tempStack = new Stack();• Stack newStack = new Stack();• while(!stack.empty())• Object x = stack.pop();• tempStack.push(x);• newStack.push(x);• • while(!tempStack.empty())• stack.push(tempStack.pop());• return newStack;•

6.10 La méthode suivante renvoie le deuxième élément d’une pile en partant du haut :

• private static Object penultimate(Stack stack)• Object x1 = stack.pop();• Object x2 = stack.pop();• stack.push(x2);• stack.push(x1);• return x2;•

6.11 La méthode suivante renvoie l’élément inférieur d’une pile :

• private static Object bottom(Stack stack)• Object x = null;• Stack tempStack = new Stack();• while(!stack.empty())• x = stack.pop();• tempStack.push(x);• • while(!tempStack.empty())• stack.push(tempStack.pop());• return x;•

6.12 La méthode suivante supprime et renvoie l’élément inférieur d’une pile :

• private static Object popBottom(Stack stack)• Stack tempStack = new Stack();• Object x = null;• if (!stack.empty()) x = stack.pop();• while(!stack.empty())• tempStack.push(x);• x = stack.pop();• • while(!tempStack.empty())• stack.push(tempStack.pop());• return x;•

Page 137: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 127

6.13 La méthode suivante utilise les méthodes Vector pour inverser le contenu d’une pile :

• private static void reverse(Stack stack)• Stack copiedStack = (Stack)stack.clone();• stack.clear();• while(!copiedStack.empty())• stack.push(copiedStack.pop());•

6.14 La méthode suivante utilise les méthodes Vector pour renvoyer le contenu inversé d’une pile :

• private static Stack reversed(Stack stack)• Stack copiedStack = (Stack)stack.clone();• Stack newStack = new Stack();• while(!copiedStack.empty())• newStack.push(copiedStack.pop());• return newStack;•

6.15 La méthode suivante utilise les méthodes Vector pour renvoyer le deuxième élément à partir duhaut de la pile :

• private static Object penultimate(Stack stack)• if (stack.size()<2) return null;• return stack.elementAt(stack.size()-2);•

6.16 La méthode suivante utilise les méthodes Vector pour renvoyer l’élément inférieur d’une pile :

• private static Object bottom(Stack stack)• return stack.firstElement();•

6.17 La méthode suivante utilise les méthodes Vector pour supprimer et renvoyer l’élément inférieurd’une pile :

• private static Object popBottom(Stack stack)• return stack.remove(0);•

6.18 L’exemple 6.5 modifié à l’aide d’une pile de valeurs primitives char est le suivant :

• import java.io.*;• class CharStack• private char[] s = new char[1000];• private int top=-1;• public boolean empty() return top<0; • public char peek() return s[top]; • public char pop() return s[top--]; • public void push(char ch) s[++top] = ch; • •• public class Tmp• public static void main(String[] args)• try• CharStack stack = new CharStack();• InputStreamReader reader = new InputStreamReader(System.in);• StreamTokenizer tokens = new StreamTokenizer(reader);• tokens.ordinaryChar(’/’);• tokens.eolIsSignificant(true);• int tokenType;• System.out.print("Enter an infix expression: ");

Page 138: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

128 Piles

• while ((tokenType=tokens.nextToken())• != StreamTokenizer.TT_EOL)• char ch = (char)tokenType;• if (tokenType==treamTokenizer.TT_NUMBER)• System.out.print(tokens.nval + " ");• else if (ch==’+’ || ch==’-’ || ch==’*’ || ch==’/’)• stack.push(ch);• else if (ch==’)’)• System.out.print(stack.pop()+" ");• • while (!stack.empty())• System.out.print(stack.pop()+" ");• • catch (Exception e)• System.out.println(e);• • •

Page 139: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 7

Files

Une file est un conteneur qui implémente le protocole Premier entré, premiersorti (FIFO, First-In-First-Out). Cela signifie que le seul objet accessibledans le conteneur est celui qui a été inséré en premier. Pour comprendre dequoi il retourne, imaginez un groupe de personnes qui font la queue pour allervoir un film : la prochaine personne à entrer dans la salle de cinéma sera celle qui est arrivée avant toutesles autres.

7.1 FRAMEWORK DES FILESUn framework est un jeu d’interfaces et de classes abstraites associées via l’extension et l’implémenta-tion. En outre, il offre les diverses possibilités d’implémentations des types de données abstraites. Le fra-mework de collections Java (reportez-vous à la section 5.1) est composé de listes, de jeux et de mappes.Si la bibliothèque standard Java ne propose pas de framework de files à proprement parler, le frameworkde collections vous indique tout de même au programmeur comment en construire un.

À l’instar des interfacesList et Set, l’interface Queueest une sous-interface de Col-lection (reportez-vous à lasection 5.2) et elle hérite doncdes 15 spécifications de métho-des de cette dernière :

public interface Collection public boolean add(Object);public boolean addAll(Collection);public void clear();public boolean contains(Object);public boolean containsAll(Collection);public boolean equals(Object);public int hashCode();public boolean isEmpty();public Iterator iterator();public boolean remove(Object);public boolean removeAll(Collection);public boolean retainAll(Collection);public int size();

Object

AbstractCollection

AbstractQueue

ArrayQueue

LinkedQueue

Collection

Queue

Page 140: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

130 Files

public Object[] toArray();public Object[] toArray(Object[]);

Exemple 7.1 Une interface queue

L’interface suivante est destinée aux files qui ajoutent les quatre méthodes traditionnelles définissantleur comportement :

public interface Queue extends Collection public Object dequeue();public Object enqueue(Object object);public Object getBack();public Object getFront();

Dans le cas présent, dequeue signifie supprimer l’objet situé en tête de file, et enqueue insérer lenouvel objet en queue de la file.

Exemple 7.2 Utiliser une file

Le diagramme ci-contre illustre l’évolution d’unefile suite aux appels des méthodes enqueue(Object) et dequeue() dans l’ordre indiquéci-après :

q.enqueue("Amin");q.enqueue("Bush");q.enqueue("Chen");q.enqueue("Diaz");q.dequeue();q.dequeue();q.enqueue("Ford");q.enqueue("Gore");q.dequeue();

Dans le cas présent, les éléments situés en têtede file se trouvent dans la colonne de gauche,ceux situés en queue se trouvent dans la colonnede droite. C’est la raison pour laquelle, après letroisième appel de la méthode dequeue(), unappel de la méthode getFront() renverrait"Diaz" et un appel de la méthode getBack()renverrait "Gore".

Sur le modèle du framework de collections Java(reportez-vous à la section 5.1), nous implémentonsl’interface Queue comme sous-classe abstraite de laclasse AbstractCollection, le tableau concernéet les implémentations liées devenant ensuite dessous-classes de cette classe de base :

Exemple 7.3 Une classe AbstractQueue

import java.util.*;

public abstractclass AbstractQueue extends AbstractCollection implements Queue

Chen Diaz Ford

Gore

Diaz

Chen

Bush

Amin

Amin

Amin Bush

ChenAmin Bush

DiazChenBush

DiazChen

Chen Diaz Ford

GoreDiaz Ford

q.enqueue("Gore")

q.dequeue()

q.enqueue("Ford")

q.dequeue()

q.enqueue("Diaz")

q.dequeue()

q.enqueue("Bush")

q.enqueue("Amin")

q.enqueue("Chen")

Page 141: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Framework des files 131

protected AbstractQueue()

public abstract Object dequeue();

public abstract Object enqueue(Object object);

public boolean equals(Object object) if (object == this) return true; if (!(object instanceof AbstractQueue)) return false; AbstractQueue abstractQueue = (AbstractQueue) object; if (abstractQueue.size() != size()) return false; return containsAll(abstractQueue);

public abstract Object getBack();

public abstract Object getFront();

public int hashCode() int n = 0; for (Iterator it = iterator(); it.hasNext(); ) Object object = it.next(); if (object != null) n += object.hashCode(); return n;

public abstract Iterator iterator();

public abstract int size();

Cette classe abstraite implémente le constructeur par défaut et remplace les méthodes equals(Object) et hashCode() de la classe Object. Les six autres méthodes sont déclarées commeabstraites ; c’est pourquoi les sous-classes concrètes sont chargées de leur implémentation. L’inter-face Queue requiert la présence des méthodes dequeue(), enqueue(Object), getBack() etgetFront(), tandis que la classe de base AbstractCollections requiert l’implémentation desméthodes iterator() et size().Étant une sous-classe de AbstractCollections, AbstractQueue hérite des méthodes concrè-tes suivantes (reportez-vous à la section 5.3) :

public boolean addAll(Collection);public void clear();public boolean contains(Object);public boolean containsAll(Collection);public boolean isEmpty();public boolean remove(Object);public boolean removeAll(Collection);public boolean retainAll(Collection);public Object[] toArray();public Object[] toArray(Object[]);public String toString();

La méthode isEmpty() utilise la méthode size(), contrairement aux dix autres méthodes concrè-tes qui utilisent la méthode iterator(). C’est la raison pour laquelle les sous-classes doiventimplémenter les méthodes size() et iterator().

Page 142: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

132 Files

7.2 IMPLÉMENTATION CONTIGUËLes tableaux, c’est-à-dire le procédé contigu, constituent la méthode la plus simple d’implémentationd’une file.

Exemple 7.4 Classe ArrayQueue

import java.util.*;

public class ArrayQueue extends AbstractQueue protected Object[] objects; protected int front=0; protected int back=0; protected int capacity=16; // INVARIANTS : objects[i] == null pour 0 <= i < front; // objects[i] != null pour tête <= i < fin; // objects[i] == null pour fin <= i < capacité;

public ArrayQueue() objects = new Object[capacity];

public ArrayQueue(int capacity) this.capacity = capacity; objects = new Object[capacity];

public Object dequeue() if (isEmpty()) throw new NoSuchElementException("La file est vide"); Object object = objects[front++]; if (2*front>=capacity) // décaler vers la gauche for (int i=0; i<size(); i++) objects[i] = objects[i+front]; back -= front; front = 0; return object;

public Object enqueue(Object object) if (back>=capacity) Object[] temp = objects; capacity *= 2; // doubler la capacité objects = new Object[capacity]; for (int i=0; i<back-front; i++) objects[i] = temp[i+front]; back -= front; front = 0; objects[back++] = object; return object;

public Object getBack() if (isEmpty()) throw new NoSuchElementException("La file est vide"); return objects[back-1];

Page 143: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Implémentation contiguë 133

public Object getFront() if (isEmpty()) throw new NoSuchElementException("La file est vide"); return objects[front];

public Iterator iterator() return new Iterator() // classe interne anonyme private int cursor=front; public boolean hasNext() return cursor<back; public Object next() if (cursor>=back) throw new NoSuchElementException(); return objects[cursor++]; public void remove() throw new UnsupportedOperationException(); ;

public int size() return back-front;

Le tableau objects est composé des éléments de la file. Comme l’indique le commentaire d’inva-riant de classe, seul le sous-tableau objects[front..(back-1)] est utilisé ; tous les autres élé-ments sont null.La classe contient deux constructeurs : le premier est un constructeur par défaut qui définit la capa-cité initiale du tableau à 16, tandis que le second laisse à l’utilisateur le soin de déterminer cettecapacité initiale.Les méthodes getFront() et getBack() renvoient respectivement objects[front] etobjects[back-1]. Quant à la méthode size(), elle renvoie back-front, soit le nombre d’élé-ments figurant dans le sous-tableau objects[front..(back-1)].La méthode enqueue(Object) insère l’objet donné dans objects[back], puis elle incrémentel’index back. Si objects[back] n’existe pas, c’est-à-dire si back a atteint la fin du tableau, unnouveau tableau deux fois plus grand est créé avant l’insertion. Les éléments back-front sontensuite copiés dans ce tableau, en commençant par l’index 0.La méthode dequeue() supprime l’objet situé au niveau de objects[front], puis incrémentel’index front. Suite à cette suppression, si l’index front se trouve dans la deuxième moitié finaledu tableau (c’est-à-dire si au moins la moitié des éléments du tableau sont inutilisés), les élémentsback-front sont tous décalés vers le début du tableau de façon à commencer au niveau de l’index0.En dernier lieu, la méthode iterator() renvoie un itérateur qui vous permettra de parcourir la file.Étant donné que celle-ci est implémentée comme un sous-tableau, l’itérateur doit simplement suivreun index, nommé cursor, qui traverse ce sous-tableau. L’itérateur renvoyé est créé par le construc-teur d’une classe interne anonyme qui définit les trois méthodes nécessaires à l’interface Iterator.Le curseur est initialisé au niveau de l’index front, puis il est incrémenté après chaque appel de lafonction next() qui renvoie objects[cursor]. Si la fonction next() est appelée alors quehasNext() est false (c’est-à-dire une fois que le curseur a atteint la fin de la file), une exceptionNoSuchElementException est lancée. La méthode remove() lance ensuite automatiquementune exception UnsupportedOperationException parce qu’une file interdit la suppression deses éléments par une méthode autre que dequeue().

Page 144: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

134 Files

Exemple 7.5 Tester la classe ArrayQueue

Le pilote test suivant nous permettra d’implémenter notre file :

public class TestQueue public static void main(String[] args) ArrayQueue q = new ArrayQueue(); System.out.println(q); q.enqueue("Amin"); System.out.println(q); q.enqueue("Bush"); System.out.println(q); q.enqueue("Chen"); System.out.println(q); q.enqueue("Diaz"); System.out.println(q); q.dequeue(); System.out.println(q); q.dequeue(); System.out.println(q); q.enqueue("Ford"); System.out.println(q); q.enqueue("Gore"); System.out.println(q); q.dequeue(); System.out.println(q); System.out.println("q.getFront() = " + q.getFront()); System.out.println("q.getBack() = " + q.getBack()); System.out.println("q.size() = " + q.size()); for (java.util.Iterator it=q.iterator(); it.hasNext(); ) System.out.println("\tit.next() = " + it.next()); for (;;) // forcer une exception q.dequeue(); System.out.println(q);

7.3 IMPLÉMENTATION CHAÎNÉEL’implémentation chaînée est très certainement plus efficace que l’implémentation contiguë parce qu’elleélimine principalement les risques de dépassement de capacité de la file. Cependant, étant donné que cetype d’implémentation a recours aux pointeurs, il reste plus complexe que l’implémentation contiguë.

[][Amin][Amin, Bush][Amin, Bush, Chen][Amin, Bush, Chen, Diaz][Bush, Chen, Diaz][Chen, Diaz][Chen, Diaz, Ford][Chen, Diaz, Ford, Gore][Diaz, Ford, Gore]q.getFront() = Diazq.getBack() = Goreq.size() = 3 it.next() = Diaz it.next() = Ford it.next() = Gore[Ford, Gore][Gore][]java.util.NoSuchElementException: la file est vide at LinkedQueue.dequeue(LinkedQueue.java:26) at TestQueue.main(TestQueue.java, Compiled Code)Exception in thread "main"

Page 145: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Implémentation chaînée 135

Exemple 7.6 Classe LinkedQueue

import java.util.*;import AbstractQueue;

public class LinkedQueue extends AbstractQueue private static class Node Object object; Node next, previous; Node() this.next = this.previous = this; Node(Object object, Node next, Node previous) this.object = object; this.next = next; this.previous = previous; private Node header = new Node(); private int size = 0;

public Object dequeue() if (isEmpty()) throw new NoSuchElementException("la file est vide"); Object object = header.next.object; header.next = header.next.next; header.next.previous = header; --size; return object;

public Object enqueue(Object object) Node p = header.previous; // dernier élément de la file header.previous = p.next = new Node(object,header,p); ++size; return object;

public Object getBack() if (isEmpty()) throw new NoSuchElementException("la file est vide"); return header.previous.object;

public Object getFront() if (isEmpty()) throw new NoSuchElementException("la file est vide"); return header.next.object;

public Iterator iterator() return new Iterator() // classe interne anonyme private Node cursor=header; public boolean hasNext() return cursor.next != header; public Object next() if (cursor.next==header) throw new NoSuchElementException(); cursor = cursor.next; return cursor.object; public void remove() throw new UnsupportedOperationException();

Page 146: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

136 Files

;

public LinkedQueue()

public int size() return size;

7.4 APPLICATIONS UTILISANT LES FILESLes files sont naturellement utilisées lorsque la fréquence desdemandes utilisateur pour certains services risque de dépassercelle à laquelle ces services peuvent être effectués. Imaginez, parexemple, des voitures qui font la queue à un péage, comme illus-tré dans le diagramme composé de quatre voitures et de troiscabines de péage.

La situation est similaire avec un réseau local sur lequel denombreux ordinateurs partagent uniquement quelques impriman-tes : les travaux d’impression s’accumulent parfois dans la filed’attente. Il en va de même aux caisses d’un supermarché ou bienchez le coiffeur.

Exemple 7.7 Simulation d’un système client/serveur

Le programme suivant vous propose une simulation d’unsystème général client/serveur, les clients pouvant être desvoitures, des travaux d’impression ou bien des personnes, etles serveurs respectivement des cabines de péage, des impri-mantes ou bien des coiffeurs. L’exécution de cette simulationcréerait la sortie suivante :

Le travail #1 arrive au temps 2 avec 7 pages.La file contient maintenant 1 travail : [#1(7)]L’imprimante A(89%,84%) commence le travail #1 au temps 2.La file est maintenant vide.Le travail #2 arrive au temps 10 avec 39 pages.La file contient maintenant 1 travail : [#2(39)]L’imprimante B(97%,91%) commence le travail #2 au temps 10.La file est maintenant vide.L’imprimante A(89%,84%) termine le travail #1 au temps 11.Le travail #3 arrive au temps 18 avec 36 pages.La file contient maintenant 1 travail : [#3(36)]L’imprimante A(89%,87%) commence le travail #3 au temps 18.La file est maintenant vide.Le travail #4 arrive au temps 44 avec 126 pages.La file contient maintenant 1 travail : [#4(126)]L’imprimante C(106%,102%) commence le travail #4 au temps 44.La file est maintenant vide.L’imprimante B(97%,91%) termine le travail #2 au temps 53.

Page 147: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Applications utilisant les files 137

Dans cet exemple, les travaux d’impression correspondent aux clients, et les imprimantes aux serveurs.Ce cas de figure est composé de quatre serveurs, à savoir les imprimantes A, B, C et D, chacuned’entre elles se caractérisant par une vitesse d’impression différente exprimée sous forme d’un pour-centage. Ainsi, l’imprimante A a une vitesse d’impression de 89 %, soit environ 0,89 page parseconde. En outre, chaque travail d’impression est associé à une vitesse d’impression. Ainsi, l’impri-mante A présente une vitesse d’impression égale à 84 % pour le travail #1, soit 0,84 page parseconde. La rapidité d’une même imprimante varie d’un travail à l’autre pour des raisons diverses,notamment en fonction du trafic sur le réseau. Ainsi, l’imprimante B, dont la vitesse d’impressionest en moyenne égale à 0,97 page par seconde, imprime 0,91 page par seconde pour le travail #2, et0,95 page par seconde pour le travail #6.D’autre part, les travaux d’impression arrivent dans la file de façon aléatoire. Dans le cas présent, letravail #1 a été inséré au temps 2 (soit 2 secondes après l’heure de début), le travail #2 au temps 10et le travail #3 au temps 18.Si les imprimantes sont encore occupées lorsqu’un nouveau travail arrive, celui-ci est intégré à la filed’attente. C’est ce qui se produit pour le travail #8 qui arrive au temps 127, au moment où les quatre

L’imprimante A(89%,87%) termine le travail #3 au temps 60.Le travail #5 arrive au temps 78 avec 170 pages.La file contient maintenant 1 travail : [#5(170)]L’imprimante A(89%,92%) commence le travail #5 au temps 78.La file est maintenant vide.Le travail #6 arrive au temps 113 avec 172 pages.La file contient maintenant 1 travail : [#6(172)]L’imprimante B(97%,95%) commence le travail #6 au temps 113.La file est maintenant vide.Le travail #7 arrive au temps 121 avec 40 pages.La file contient maintenant 1 travail : [#7(40)]L’imprimante D(128%,124%) commence le travail #7 au temps 121.La file est maintenant vide.Le travail #8 arrive au temps 127 avec 30 pages.La file contient maintenant 1 travail : [#8(30)]Le travail #9 arrive au temps 136 avec 41 pages.La file contient maintenant 2 travaux : [#8(30), #9(41)]Le travail #10 arrive au temps 140 avec 20 pages.La file contient maintenant 3 travaux : [#8(30), #9(41), #10(20)]Le travail #11 arrive au temps 147 avec 31 pages.La file contient maintenant 4 travaux : [#8(30), #9(41), #10(20), #11(31)]L’imprimante D(128%,124%) termine le travail #7 au temps 154.L’imprimante D(128%,126%) commence le travail #8 au temps 155.La file contient maintenant 3 travaux : [#9(41), #10(20), #11(31)]Le travail #12 arrive au temps 160 avec 63 pages.La file contient maintenant 4 travaux : [#9(41), #10(20), #11(31), #12(63)]L’imprimante C(106%,102%) termine le travail #4 au temps 168.L’imprimante C(106%,104%) commence le travail #9 au temps 169.La file contient maintenant 3 travaux : [#10(20), #11(31), #12(63)]L’imprimante D(128%,126%) termine le travail #8 au temps 179.L’imprimante D(128%,118%) commence le travail #10 au temps 180.La file contient maintenant 2 travaux : [#11(31), #12(63)]L’imprimante D(128%,118%) termine le travail #10 au temps 197.L’imprimante D(128%,138%) commence le travail #11 au temps 198.La file contient maintenant 1 travail : [#12(63)]L’imprimante C(106%,104%) termine le travail #9 au temps 209.L’imprimante C(106%,96%) commence le travail #12 au temps 210.La file est maintenant vide.

Page 148: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

138 Files

imprimantes sont utilisées. Avant cela, il restait toujours au moins une imprimante disponible quipouvait commencer l’impression du travail dès son arrivée :

• L’imprimante A commence l’impression du travail #1 dès son arrivée au temps 2.

• L’imprimante B commence l’impression du travail #2 dès son arrivée au temps 10.

• L’imprimante A commence l’impression du travail #3 dès son arrivée au temps 18.

• L’imprimante C commence l’impression du travail #4 dès son arrivée au temps 44.

• L’imprimante A commence l’impression du travail #5 dès son arrivée au temps 78.

• L’imprimante B commence l’impression du travail #6 dès son arrivée au temps 113.

• L’imprimante D commence l’impression du travail #7 dès son arrivée au temps 121.

Le travail #8 doit donc attendre 28 secondes avant que l’imprimante D commence son impression autemps 155. Il est placé dans la file d’attente, comme tous les autres travaux qui se présentent au coursde ces 28 secondes. C’est pourquoi notre file contient les travaux #8, #9, #10 et #11 au temps 155.Dans l’exemple de sortie que nous venons de voir, chaque travail d’impression est identifié par sonnuméro ID et par sa taille. Ainsi, #1(7) signifie que le travail #1 est composé de 7 pages à imprimer.De la même manière, #8(30) indique que 30 pages doivent être imprimées pour le travail #8.

Pour qu’une simulation soit efficace, vous devez utiliser des nombres générés de façon aléatoire.Dans le cas présent, nous avons eu recours à trois générateurs de nombres aléatoires : le premier agénéré les vitesses d’impression moyennes de chaque imprimante, le second la vitesse d’impressionréelle de chaque travail imprimé par une imprimante donnée, et le troisième l’intervalle de temps quisépare l’arrivée des travaux.Ces générateurs sont des instances de l’extension suivante de la classe java.util.Random :

public class Random extends java.util.Random private double mean; private double standardDeviation; public Random(double mean) this.mean = mean; this.standardDeviation = mean;

100 200 300

#1

#2

A

B

C

D

#3

#4

#5

#6

#7 #8

#9

#10 #11

#12

#8

#9

#10

#11

#12

0

100

Page 149: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Applications utilisant les files 139

public Random(double mean, double standardDeviation) this.mean = mean; this.standardDeviation = standardDeviation; public double nextGaussian() double x = super.nextGaussian(); // x = normal(0.0, 1.0) return x*standardDeviation + mean; public double nextExponential() return -mean*Math.log(1.0 - nextDouble()); public int intNextExponential() return (int)Math.ceil(nextExponential());

La méthode nextGaussian() renvoie des nombres aléatoires qui sont traditionnellement affectésavec la moyenne et la déviation standard données. Elle appelle et remplace la méthode synonymedans la classe java.util.Random, ce qui renvoie des nombres aléatoires normalement attribuésavec une moyenne égale à 0.0 et une déviation standard de 1.0. La méthode nextExponential()renvoie des nombres aléatoires qui sont attribués de façon exponentielle avec la moyenne donnée.Vous obtenez ainsi une répartition juste des intervalles qui séparent les arrivées. Cette méthodepermet également de générer la taille des travaux qui détermine la durée de l’impression.Chaque travail d’impression est une instance de la classe suivante :

public class Client private static final int MEAN_JOB_SIZE = 100; private static Random randomJobSize = new Random(MEAN_JOB_SIZE); private static int nextId = 0; private int id, jobSize; private Server server;

public Client(int time) id = ++nextId; jobSize = randomJobSize.intNextExponential(); print(id,time,jobSize);

public double getJobSize() return jobSize;

public void beginService(Server server, int time) this.server = server; printBegins(server,id,time);

public void endService(int time) printEnds(server,id,time); server = null;

public String toString() return "#" + id + "(" + (int)Math.round(jobSize) + ")";

private static void print(int job, int time, double size) System.out.println("Le travail #" + job + " arrive au temps " + time + " avec " + (int)Math.round(size) + " pages.");

private static void printBegins(Server server, int job, int time)

Page 150: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

140 Files

System.out.println("L’imprimante " + server + " commence le travail #" + job + " au temps " + time + ".");

private static void printEnds(Server server, int job, int time) System.out.println("L’imprimante " + server + " termine le travail #" + job + " au temps " + time + ".");

Le générateur de nombres aléatoires randomJobSize génère la taille des travaux affectée de façonexponentielle avec une moyenne de 100 pages. Il est déclaré comme étant statique parce qu’uneseule instance est nécessaire à la création de toutes les tailles de travaux. De la même façon, staticint nextId permet de générer les numéros d’identification de tous les travaux.Le constructeur utilise le compteur nextId pour définir l’id du travail, puis le générateur ran-domJobSize afin de définir jobSize. Il imprime ensuite une ligne de sortie annonçant que le tra-vail est arrivé.La méthode beginService() affecte la référence server à l’imprimante qui l’a appelée, puiselle imprime une ligne de sortie indiquant que l’impression a commencé. De la même façon, laméthode endService() annule la référence server après avoir imprimé une ligne de sortie signa-lant que l’impression est terminée.Chaque imprimante est représentée par une instance de la classe suivante :

public class Server private static Random randomMeanServiceRate = new Random(1.00,0.20); private static char nextId = ’A’; private Random randomServiceRate; private char id; private double meanServiceRate, serviceRate; private Client client; private int timeServiceEnds;

public Server() id = (char)nextId++; meanServiceRate = randomMeanServiceRate.nextGaussian(); randomServiceRate = new Random(meanServiceRate,0.10);

public void beginServing(Client client, int time) this.client = client; serviceRate = randomServiceRate.nextGaussian(); client.beginService(this,time); int serviceTime = (int)Math.ceil(client.getJobSize()/serviceRate); timeServiceEnds = time + serviceTime;

public void endServing(int time) client.endService(time); this.client = null;

public int getTimeServiceEnds() return timeServiceEnds;

public boolean isFree() return client == null;

public String toString() int percentMeanServiceRate = (int)Math.round(100*meanServiceRate);

Page 151: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Applications utilisant les files 141

int percentServiceRate = (int)Math.round(100*serviceRate); return id + "(" + percentMeanServiceRate + "%," + percentServiceRate + "%)";

Le générateur de nombres aléatoires randomMeanServiceRate génère normalement les vitessesavec une moyenne de 100.0 et une déviation standard de 20.0. Il crée la vitesse meanServiceRatepour chaque imprimante soit, dans notre exemple, une vitesse d’impression de 89 % pour l’impri-mante A, de 97 % pour l’imprimante B, de 106 % pour l’imprimante C et de 128 % pour l’impri-mante D. De la même façon, le générateur de nombres aléatoires randomServiceRate génèrenormalement les vitesses d’impression de chaque travail soit, dans notre exemple, 84 % pour le tra-vail #1, 87 % pour le travail #3 et 92 % pour le travail #5. Ces vitesses ont été calculées à partir d’uneaffectation normale définie en fonction d’une moyenne égale à 89 % (pour l’imprimante A). Ladéviation standard est fixée à 10 % pour la répartition de chaque imprimante.La méthode beginServing() affecte la référence client au travail client en cours d’impression.Elle obtient la vitesse serviceRate extraite du générateur randomServiceRate, puis elleenvoie le message beginService au travail d’impression client. Ensuite, l’affectation int ser-viceTime = (int)Math.ceil(client.getJobSize()/serviceRate); calcule la duréed’impression du travail (exprimée en secondes) en divisant la taille de ce travail (soit le nombre depages) par la vitesse d’impression (soit le nombre de pages par seconde). Le plafond entier de cettedivision est ensuite ajouté à l’heure courante de façon à initialiser le champ timeServiceEnds del’objet Server.La classe Main est la suivante :

public class ClientServerSimulation private static final int NUMBER_OF_SERVERS = 4; private static final double MEAN_INTERARRIVAL_TIME = 20.0; private static final int DURATION = 100; private static Server[] servers = new Server[NUMBER_OF_SERVERS]; private static Queue clients = new ArrayQueue(); private static Random random = new Random(MEAN_INTERARRIVAL_TIME);

public static void main(String[] args) for (int i=0; i<NUMBER_OF_SERVERS; i++) servers[i] = new Server(); int timeOfNextArrival = random.intNextExponential(); for (int t=0; t<DURATION; t++) if (t == timeOfNextArrival) clients.enqueue(new Client(t)); print(clients); timeOfNextArrival += random.intNextExponential(); for (int i=0; i<NUMBER_OF_SERVERS; i++) if (servers[i].isFree()) if(!clients.isEmpty()) servers[i].beginServing((Client)clients.dequeue(),t); print(clients); else if (t == servers[i].getTimeServiceEnds()) servers[i].endServing(t);

private static void print(Queue queue) int size=queue.size(); if (size==0) System.out.println("La file est maintenant vide.");

Page 152: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

142 Files

else System.out.println("La file contient maintenant " + size + "job" + (size>1?"s: ":": ") + queue);

La simulation de l’exemple 7.7 est qualifiée de simulation en temps réel parce que sa boucle princi-pale est effectuée une fois pour chaque mouvement de l’horloge. Par opposition, une simulation événe-mentielle a lieu lorsque la boucle principale est effectuée une fois pour chaque événement : à l’arrivée dutravail, au début ou à la fin du service. Les simulations événementielles sont généralement plus simples,même si tous les serveurs doivent alors obligatoirement travailler à la même vitesse.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

7.1 Pourquoi les files sont-elles qualifiées de structures FIFO, c’est-à-dire Premier entré, premier sorti ?

7.2 Peut-on qualifier les files de structures :

a. LILOb. FILO

7.3 Quels sont les avantages et les inconvénients de l’implémentation chaînée d’une file par rapportà une implémentation contiguë ?

RÉPONSES¿RÉPONSES

7.1 Les files sont qualifiées de structures FIFO parce que le premier élément inséré dans une file esttoujours le premier à en être supprimé.

7.2 a. Une file peut être une structure LILO (Last-In-Last-Out, Dernier entré, dernier sorti), c’est-à-dire la même chose que le protocole Premier entré, premier sorti.

b. Une file ne peut pas être une structure FILO (First-In-Last Out, Premier entré, dernier sorti)qui est le contraire du protocole Premier entré, premier sorti.

7.3 L’implémentation chaînée présente l’avantage d’éliminer principalement les risques de dépasse-ment de capacité de la file. En effet, le nombre d’appels de la méthode enqueue() est unique-ment limité par la quantité de mémoire disponible pour l’opérateur new. En revanche, elle utilisedes pointeurs qui la rendent plus complexe que l’implémentation contiguë.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

7.1 Tracez le code suivant en indiquant le contenu de la file q après chaque appel :

• ArrayQueue q;• q.enqueue("A");• q.enqueue("B");• q.enqueue("C");• q.dequeue();• q.dequeue();

Page 153: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 143

• q.enqueue("D");• q.enqueue("E");• q.enqueue("F");• q.dequeue();• q.enqueue("G");• q.dequeue();• q.dequeue();• q.dequeue();

Implémentez la méthode des exercices 7.2 à 7.7 en utilisant uniquement le constructeur et lesméthodes enqueue(), dequeue() et isEmpty() de la classe ArrayQueue :

7.2 • public static void reverse(ArrayQueue queue)• // inverse le contenu de la file donnée;

7.3 • private static ArrayQueue reversed(ArrayQueue queue);• // renvoie une nouvelle file qui contient les mêmes éléments• // que la file donnée, mais dans l’ordre inverse

7.4 • public static Object second(ArrayQueue queue)• // renvoie le deuxième élément à partir de la tête de file

7.5 • public static Object last();• // renvoie le dernier élément de la file

7.6 • public static Object removeLast();• // supprime et renvoie le dernier élément de la file

7.7 • public static ArrayQueue merge(ArrayQueue q1, ArrayQueue q2);• // fusionne les deux files données en les alternant• // tant que cela est possible, et renvoie la file combinée obtenue

7.8 Modifiez le programme de l’exemple 7.7 pour qu’il calcule et imprime les statistiques suivantes :

a. le nombre moyen de travaux dans le système ;b. le nombre moyen de travaux dans la file ;c. la durée moyenne de présence d’un travail dans le système ;d. la durée moyenne d’attente d’un travail dans la file.Théoriquement, les réponses obtenues devraient être approximativement égales à ces résultatssuivants :a. L = n/(st – n)b. (nL)/(st)c. tL

d. (nL)/savec n = taille moyenne du travail, s = nombre de serveurs et t = intervalle moyen entre les arrivées.Par exemple, si n = 100, s = 6 et t = 20, les réponses obtenues seront approximativement égales à :a. 5 travaux dans le système ;b. 4,17 travaux dans la file ;c. 100 secondes dans le système ;d. 83,3 secondes dans la file.

7.9 Modifiez le programme de l’exercice 7.8 de façon à ce qu’il calcule et imprime également lesstatistiques suivantes :

a. la durée de service moyenne de chaque serveur ;b. la durée de service moyenne de tous les travaux ;c. le pourcentage d’inactivité de chaque serveur.

Page 154: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

144 Files

SOLUTIONS¿SOLUTIONS

7.1 La trace est la suivante :

7.2 La méthode suivante inverse le contenu de la file :

• private static void reverse(ArrayQueue queue)• Stack tempStack = new Stack();• while(!queue.isEmpty())• tempStack.push(queue.dequeue());• while(!tempStack.empty())• queue.enqueue(tempStack.pop());•

7.3 La méthode suivante renvoie le contenu inversé d’une file :

• private static ArrayQueue reversed(ArrayQueue queue)• ArrayQueue newQueue = new ArrayQueue();• Stack stack = new Stack();• while(!queue.isEmpty())• Object x = queue.dequeue();• stack.push(x);• newQueue.enqueue(x);• • while(!newQueue.isEmpty())• queue.enqueue(newQueue.dequeue());• while(!stack.empty())

q.enqueue('B')q.enqueue('A') AA A B

q.dequeue()q.enqueue('C') B CA B C

q.enqueue('D')q.dequeue() C DC

q.enqueue('F')q.enqueue('E') C D E

q.enqueue('G')q.dequeue()

C D E F

D E F D E F G

q.dequeue()q.dequeue() F GE F G

q.dequeue() G

Page 155: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 145

• newQueue.enqueue(stack.pop());• return newQueue;•

7.4 La méthode suivante renvoie le deuxième élément à partir de la tête de file :

• private static Object second(ArrayQueue queue)• ArrayQueue tempQueue = new ArrayQueue();• Object x = queue.dequeue();• tempQueue.enqueue(x);• x = queue.dequeue();• tempQueue.enqueue(x);• while(!queue.isEmpty())• tempQueue.enqueue(queue.dequeue());• while(!tempQueue.isEmpty())• queue.enqueue(tempQueue.dequeue());• return x;•

7.5 La méthode suivante renvoie le dernier élément d’une file :

• private static Object last(ArrayQueue queue)• ArrayQueue tempQueue = new ArrayQueue();• while(!queue.isEmpty())• tempQueue.enqueue(queue.dequeue());• Object x = null;• while(!tempQueue.isEmpty())• x = tempQueue.dequeue();• queue.enqueue(x);• • return x;•

7.6 La méthode suivante supprime et renvoie le dernier élément d’une file :

• private static Object removeLast(ArrayQueue queue)• ArrayQueue tempQueue = new ArrayQueue();• int count=0;• while(!queue.isEmpty())• tempQueue.enqueue(queue.dequeue());• ++count;• • Object x = null;• for (int i=0; i<count-1; i++)• queue.enqueue(tempQueue.dequeue());• return tempQueue.dequeue();•

7.7 La méthode suivante fusionne deux files :

• public static ArrayQueue merged(ArrayQueue q1, ArrayQueue q2)• ArrayQueue newQueue = new ArrayQueue();• while(!q1.isEmpty() && !q2.isEmpty())• newQueue.enqueue(q1.dequeue());• newQueue.enqueue(q2.dequeue());• • while(!q1.isEmpty())• newQueue.enqueue(q1.dequeue());• while(!q2.isEmpty())• newQueue.enqueue(q2.dequeue());• return newQueue;•

Page 156: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

146 Files

7.8 Le programme de l’exemple 7.7 peut être modifié de la façon suivante :

Ajoutez tout d’abord ce code à la classe Client :

• private int id, jobSize, tArrived, tBegan, tEnded;•• public Client(int time)• id = ++nextId;• jobSize = randomJobSize.intNextExponential();• tArrived = time;• •• public double getJobSize()• return jobSize;• •• public int getWaitTime()• return tBegan - tArrived;• •• public int getServiceTime()• return tEnded - tBegan;• •• public void beginService(Server server, int time)• this.server = server;• tBegan = time;• •• public void endService(int time)• tEnded = time;• server = null;•

Ajoutez ensuite ce code à la classe Server :

• public Client getClient()• return client;•

En dernier lieu, ajoutez ce code à la classe main :

• private static int totalTime, queueTime, totalNumberOfJobs;• private static int[] numberOfJobs = new int[NUMBER_OF_SERVERS];• public static void main(String[] args)• for (int i=0; i<NUMBER_OF_SERVERS; i++)• servers[i] = new Server();• int timeOfNextArrival = random.intNextExponential();• for (int t=0; t<DURATION; t++)• if (t == timeOfNextArrival)• clients.enqueue(new Client(t));• timeOfNextArrival += random.intNextExponential();• • for (int i=0; i<NUMBER_OF_SERVERS; i++)• Server server = servers[i];• if (server.isFree())• if(!clients.isEmpty())• server.beginServing((Client)clients.dequeue(),t);• • • else // le serveur sert un client• ++totalTime; // compter le temps dans le système

Page 157: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 147

• if (t == server.getTimeServiceEnds())• Client client = server.getClient();• server.endServing(t);• int waitTime = client.getWaitTime();• ++numberOfJobs[i]; // compter uniquement les• // projets terminés• • • • totalTime += clients.size();• queueTime += clients.size();• • for (int i=0; i<NUMBER_OF_SERVERS; i++)• totalNumberOfJobs += numberOfJobs[i];• printStatistics();• •• private static void printStatistics()• System.out.println("Intervalle moyen entre les arrivées : " +• MEAN_INTERARRIVAL_TIME);• System.out.println("Temps moyen de service : "• + Client.MEAN_JOB_SIZE);• System.out.println("Nombre de serveurs : " + NUMBER_OF_SERVERS);• System.out.println("Nombre moyen de travaux dans le système : "• + (float)totalTime/DURATION);• System.out.println("Nombre moyen de travaux dans la file : "• + (float)queueTime/DURATION);• System.out.println("Durée moyenne de présence du travail dans le• système : "• + (float)totalTime/totalNumberOfJobs);• System.out.println("Durée moyenne de présence du travail dans la• file : "• + (float)queueTime/totalNumberOfJobs);•

7.9 En ce qui concerne la seconde modification du programme de l’exemple 7.7, ajoutez le code sui-vant :

• private static int totalTime, queueTime, totalNumberOfJobs,• totalServiceTime;• private static int[] serviceTime = new int[NUMBER_OF_SERVERS];• private static int[] numberOfJobs = new int[NUMBER_OF_SERVERS];• public static void main(String[] args)• for (int i=0; i<NUMBER_OF_SERVERS; i++)• servers[i] = new Server();• serviceTime[i] = numberOfJobs[i] = 0;• • int timeOfNextArrival = random.intNextExponential();• for (int t=0; t<DURATION; t++)• if (t == timeOfNextArrival)• clients.enqueue(new Client(t));• timeOfNextArrival += random.intNextExponential();• • for (int i=0; i<NUMBER_OF_SERVERS; i++)• Server server = servers[i];• if (server.isFree())• if(!clients.isEmpty())• server.beginServing((Client)clients.dequeue(),t);• •

Page 158: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

148 Files

• else // le serveur sert un client• ++totalTime; // compter le temps dans le système• if (t == server.getTimeServiceEnds())• Client client = server.getClient();• server.endServing(t);• int waitTime = client.getWaitTime();• serviceTime[i] = client.getServiceTime();• ++numberOfJobs[i]; // compter uniquement les• // travaux terminés• • • • totalTime += clients.size();• queueTime += clients.size();• • for (int i=0; i<NUMBER_OF_SERVERS; i++)• totalServiceTime += serviceTime[i];• totalNumberOfJobs += numberOfJobs[i];• • printStatistics();• •• private static void printStatistics()• System.out.println("Intervalle moyen entre les arrivées : " +• MEAN_INTERARRIVAL_TIME);• System.out.println("Temps moyen de service : "• + Client.MEAN_JOB_SIZE);• System.out.println("Nombre de serveurs : " + NUMBER_OF_SERVERS);• System.out.println("Nombre moyen de travaux dans le système : "• + (float)totalTime/DURATION);• System.out.println("Nombre moyen de travaux dans la file : "• + (float)queueTime/DURATION);• System.out.println("Durée moyenne de présence du travail dans• le système : "• + (float)totalTime/totalNumberOfJobs);• System.out.println("Durée moyenne de présence du travail dans• la file : "• + (float)queueTime/totalNumberOfJobs);• System.out.println("Durée totale de présence de tous les travaux• dans le système : "• + totalTime);• System.out.println("Durée totale de présence de tous les travaux• dans la file : "• + queueTime);• System.out.println("Durée : " + DURATION);• System.out.println("Nombre total de travaux terminés : "• + totalNumberOfJobs);• System.out.println("Durée de service moyenne du serveur :");• for (int i=0; i<NUMBER_OF_SERVERS; i++)• System.out.println("\t" + serveurs[i] + ": "• + (float)serviceTime[i]/numberOfJobs[i]);• System.out.println("Durée moyenne de service pour tous les • travaux : "• + (float)totalServiceTime/totalNumberOfJobs);• System.out.println("Pourcentage d’inactivité du serveur :");• for (int i=0; i<NUMBER_OF_SERVERS; i++)• System.out.println("\t" + serveurs[i] + ": "• + (float)(100.0*(DURATION - serviceTime[i]))/DURATION + "%");•

Page 159: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 8

Listes

Une liste est un conteneur séquentiel capable d’insérer etde supprimer des éléments localement de façon constante,c’est-à-dire indépendamment de la taille du conteneur. Il est préférable d’utiliser cette structure de don-nées pour les applications qui n’ont pas besoin d’un accès aléatoire. Pour comprendre de quoi il retourne,imaginez les wagons d’un train : pour en supprimer un, il suffit de le déconnecter des wagons le tou-chant, puis de reconnecter ces derniers entre eux.

Dans le cadre de Java, l’interface List est une extension de l’interface Collection, c’est pourquoielle fait partie, de même que son implémentation, du framework de collections Java 1.2 (reportez-vousà la section 5.1) défini dans le paquetage java.util. L’interface List fournit un type de donnéesabstrait à la notion de séquence en termes mathématiques. Ainsi, les implémentations de List Java secomportent comme des conteneurs qui supportent un accès indexé à leurs éléments.

8.1 INTERFACE java.util.ListL’interface List est définie de la façon suivante dans le paquetage java.util :

public interface List extends Collection public boolean add(Object object);public void add(int index, Object object);public boolean addAll(Collection collection);public boolean addAll(int index, Collection collection);public void clear();public boolean contains(Object object);public boolean containsAll(Collection collection);public boolean equals(Object object);public Object get(int index);public int hashCode();public int indexOf(Object object);public boolean isEmpty();public Iterator iterator();public int lastIndexOf(Object object);public ListIterator listIterator();public ListIterator listIterator(int index);public Object remove(int index);public boolean remove(Object object);public boolean removeAll(Collection collection);public boolean retainAll(Collection collection);

Page 160: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

150 Listes

public Object set(int index, Object object);public int size();public List subList(int start, int stop);public Object[] toArray();public Object[] toArray(Object[] objects);

Vous remarquerez que ce code utilise un type d’itérateur spécial nommé ListIterator ; il s’agitd’un itérateur bidirectionnel.

Exemple 8.1 Utiliser l’interface List

import java.util.*;

public class Ex0801 public static void main(String[] args) String[] strings; strings = new String[]"Adams","Tyler","Grant","Hayes","Nixon"; List list = Arrays.asList(strings); System.out.println("list = " + list); System.out.println("list.size() = " + list.size()); System.out.println("list.getClass().getName() = " + list.getClass().getName()); System.out.println("list.get(1) = " + list.get(1)); System.out.println("list.contains(\"Tyler\") = " + list.contains("Tyler")); List sublist = list.subList(2,5); System.out.println("sublist = " + sublist); Object object = list.get(1); System.out.println("sublist.get(1) = " + sublist.get(1)); System.out.println("sublist.contains(\"Tyler\") = " + sublist.contains("Tyler")); list.remove(3);

Ce programme a recours à un tableau String pour construire l’objet List. La méthode get-Class() indique que cet objet est une instance de la classe ArrayList.Les méthodes get()et sublist() utilisent les numéros d’index comme arguments. L’appel delist.sublist(2,5) renvoie un objet List qui contient les éléments des numéros 2 à 4 de la liste.L’appel list.remove(3) lance une exception UnsupportedOperationException parce quela classe ArrayList n’implémente pas la méthode remove().

L’exception lancée dans l’exemple 8.1 vous permet de constater que toutes les méthodes spécifiéesdans l’interface List ne sont pas systématiquement implémentées. Lorsqu’elles ne le sont pas, elles doi-vent envoyer une exception UnsupportedOperationException.

list = [Adams, Tyler, Grant, Hayes, Nixon]list.size() = 5list.getClass().getName() = java.util.Arrays$ArrayListlist.get(1) = Tylerlist.contains("Tyler") = truesublist = [Grant, Hayes, Nixon]sublist.get(1) = Hayessublist.contains("Tyler") = falseException in thread "main" java.lang.UnsupportedOperationExceptionat java.util.AbstractList.remove(AbstractList.java:174)at Testing.main(Testing.java:23)

Page 161: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Implémenter l’interface java.util.List 151

8.2 IMPLÉMENTER L’INTERFACE java.util.ListDans l’exemple 8.1, vous avez pu découvrir l’une des classes du framework de collections Java 1.2 quiimplémente l’interface List : la classe ArrayList. Cette interface peut également être implémentéepar les trois classes suivantes : AbstractList, LinkedList et Vector.

La classe AbstractList joue, pour les listes, un rôle identique à celui de AbstractCollectionpour les collections générales : étant une implémentation partielle de l’interface correspondante, ellelimite la quantité de code nécessaire aux implémentations personnalisées. En outre, il est impossible del’instancier directement parce qu’elle est définie comme abstraite. Nous reviendrons sur l’utilisationde cette classe dans la section 8.3.

La classe LinkedList offre la structure et la fonctionnalité de toutes les classes de listes chaînées.Nous la détaillerons dans la section 8.6.

La classe ArrayList implémente l’interface List à l’aide d’un tableau contigu. Nous l’aborderonsplus en détail dans la section 8.5.

Quant à la classe Vector, elle est similaire à la classe ArrayList, comme nous l’avons déjà vudans la section 2.6.

8.3 CLASSES AbstractList ET Abstract-SequentialList

La classe AbstractList fournit l’ossature des implémentations complètes de la classe List qui ontrecours à un tableau afin de stocker les éléments d’une liste. De la même façon, la classe Abstract-SequentialList fournit l’ossature des implémentations complètes de la classe List qui ont recours àune structure chaînée non contiguë pour stocker les éléments d’une liste. Ces deux procédés de stockagesont fondamentalement opposés. En effet, la structure contiguë d’un tableau fournit un accès aléatoireimmédiat, mais nécessite un certain nombre de transferts de données (insertions et suppressions), tandisque la structure chaînée est capable de gérer des insertions et des suppressions immédiates, ce qui signi-fie qu’elle ne permet pas l’utilisation des index et donc l’accès immédiat (accès aléatoire). Le tableausuivant résume les différences entre ces deux procédés :

Si votre application requiert un accès rapide à une séquence statique, il est préférable d’utiliser la classeArrayList, de l’étendre ou bien d’étendre sa classe parent AbstractList. En revanche, si votre applica-tion requiert une séquence dynamique, c’est-à-dire avec des insertions et des suppressions fréquentes, pensezà utiliser la classe LinkedList, à l’étendre ou bien à étendre sa classe parent AbstractSequentialList.

Implémentation partielleImplémentation

complèteStructure

de donnéesget(), set()

add(), remove()

AbstractList ArrayList tableau O(1) O(n)

AbstractSequentialList LinkedList liste chaînée O(n) O(1)

Object

AbstractCollection

AbstractList

ArrayList

AbstractSequentialList

LinkedList

Collection

List

Iterator

ListIteratorVector

Page 162: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

152 Listes

Pour information, sachez que la classe LinkedList n’implémente pas la méthode get(int)directement.

Vous trouverez ci-après une liste des méthodes de la classe AbstractList définies dans le paque-tage java.util :

public abstract class AbstractList extends AbstractCollection implements List protected AbstractList() public boolean add(Object object); public void add(int index, Object object); public boolean addAll(Collection collection); public boolean addAll(int index, Collection collection); public void clear(); public boolean equals(Object object); abstract public Object get(int index); public int hashCode(); public int indexOf(Object object); public Iterator iterator(); public int lastIndexOf(Object object); public ListIterator listIterator(); public ListIterator listIterator(int index); public Object remove(int index); public void removeRange(int start, int stop); public Object set(int index, Object object); public List subList(int start, int stop);

La méthode get() étant déclarée comme abstraite, toutes les extensions concrètes de cette classedevront l’implémenter.

L’implémentation des méthodes set(), add() et remove() est relativement aisée :

throw new UnsupportedOperationException();

Cette implémentation vous permet de comprendre pourquoi ces méthodes sont facultatives. En effet,elles peuvent éventuellement être remplacées par une extension selon l’efficacité de leur structure dedonnées (reportez-vous au tableau de cette section). Une autre alternative est à votre disposition à cestade : l’extension peut également laisser au moins l’une de ces implémentations. Dans ce cas, l’exceptionest lancée intentionnellement si une application utilise une méthode autre que celle qui lui était associée.

Vous trouverez ci-après une liste des méthodes de la classe AbstractSequentialList définiesdans le paquetage java.util :

public abstract class AbstractSequentialList extends AbstractList protected AbstractSequentialList() public void add(int index, Object object); public boolean addAll(Collection c); public boolean addAll(int index, Collection c); public Object get(int index); public Iterator iterator(); public int lastIndexOf(Object object); abstract public ListIterator listIterator(int index); public Object remove(int index); public Object set(int index, Object object);

Les méthodes listIterator() et size() sont déclarées comme abstraites, c’est pourquoi cha-que extension concrète de la classe devra les implémenter. N’oubliez pas que la méthode size() abs-traite est héritée de la classe AbstractCollection (reportez-vous à la section 5.3).

Page 163: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Itérateurs de listes 153

8.4 ITÉRATEURS DE LISTESUn itérateur est un objet capable de se déplacer d’un élément à l’autre dans une collection. Il remplacel’indice des tableaux ou des vecteurs. De la même manière que vous utilisez naturellement les indicespour traiter les tableaux, vous utiliserez les itérateurs pour traiter les listes.

Java 1.2 définit l’interface Iterator et la sous-interface ListIterator dans le paquetagejava.util ; ces deux éléments font donc partie du framework de collections Java. L’interface Itera-tor spécifie le comportement d’un itérateur unidirectionnel dans une structure de données généralelinéaire (reportez-vous à la section 5.5). En revanche, l’extension ListIterator spécifie le comporte-ment d’un itérateur bidirectionnel dans une liste doublement chaînée.

L’interface ListIterator, telle qu’elle est définie dans le paquetage java.util, est la suivante :

public interface ListIterator extends Iterator public void add(Object object); public boolean hasNext(); public boolean hasPrevious(); public Object next(); public int nextIndex(); public Object previous(); public int previousIndex(); public void remove(); public void set(Object object);

Exemple 8.2 Utiliser un itérateur de liste

import java.util.*;public class Ex0802 public static void main(String[] args) String[] planets = new String[]"Neptune","Terre","Mars","Pluton"; List list = Arrays.asList(planets); System.out.println("list = " + list); ListIterator it = list.listIterator(); System.out.println("it.next() = " + it.next()); System.out.println("it.next() = " + it.next()); System.out.println("it.next() = " + it.next()); System.out.println("it.next() = " + it.next()); System.out.println("it.previous() = " + it.previous()); System.out.println("it.previous() = " + it.previous()); it.add("Saturne");

list = [Neptune, Terre, Mars, Pluton]it.next() = Neptuneit.next() = Terreit.next() = Marsit.next() = Plutonit.previous() = Plutonit.previous() = MarsException in thread "main" java.lang.UnsupportedOperationExceptionat java.util.AbstractList.add(AbstractList.java:153)at java.util.AbstractList$ListItr.add(AbstractList.java:503)at Testing.main(Testing.java:20)

Page 164: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

154 Listes

Ce programme illustre le fonctionnement d’un itérateur bidirectionnel. Vous avez également puconstater que la méthode add() n’est pas implémentée pour les objets ListIterator qui itèrentsur les objets ArrayList. C’est pourquoi l’appel it.add("Saturne") déclenche le lancementde l’exception UnsupportedOperationException.

8.5 CLASSE ArrayListLa classe ArrayList utilise un tableau pour implémenter la classe AbstractList. Elle est définie dela façon suivante dans le paquetage java.util :

public class ArrayList extends AbstractList implements List public boolean add(Object object) public void add(int index, Object object) public boolean addAll(Collection collection) public boolean addAll(int index, Collection collection) public ArrayList() public ArrayList(int initialCapacity) public ArrayList(Collection collection) public void clear() public Object clone() public boolean contains(Object object) public void ensureCapacity(int capacity) public Object get(int index) public int indexOf(Object object) public boolean isEmpty() public int lastIndexOf(Object object) public Object remove(int index) public Object set(int index, Object object) public int size() public Object[] toArray() public Object[] toArray(Object[] objects) public void trimToSize()

Les deux méthodes ensureCapacity(int) et trimToSize() gèrent plus spécifiquement lacapacité privée d’un objet ArrayList. L’entier obtenu correspond à la taille réelle du tableau qui stockeles éléments de l’objet ArrayList. Sur les 21 méthodes implémentées par cette classe, seules deux,ainsi que les constructeurs, ne constituent pas une personnalisation de la méthode correspondante dansl’une des classes ancêtres AbstractList, AbstractCollection ou Object.

Exemple 8.3 Utiliser ArrayList pour stocker les données classées

Le fichier texte suivant nommé Villes.txt liste les neuf villes les plusimportantes en 1990, par ordre décroissant de leur nombre d’habitants (Tokyoétant donc la ville la plus peuplée). Le programme qui suit est destiné à lireces données dans ArrayList, puis à utiliser les méthodes indexOf(),remove() et add() afin de mettre à jour les données pour l’année 1995.

import java.io.*;import java.util.*;

public class Ex0803 private static ArrayList list = new ArrayList(); public static void main(String[] args) load("Villes.txt"); System.out.println(list); list.remove(list.indexOf("Seoul"));

TokyoMexicoSao PaoloSeoulNew YorkOsakaBombay

Villes.txt

Page 165: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe LinkedList 155

list.remove(list.indexOf("Osaka")); list.add(5,"Shanghai"); list.add(6,"Los Angeles"); System.out.println(list);

private static void load(String filename) try File file = new File(filename); FileReader reader = new FileReader(file); BufferedReader in = new BufferedReader(reader); String ville = in.readLine(); while (ville != null) list.add(ville); ville = in.readLine(); catch(Exception e) System.out.println(e);

8.6 CLASSE LinkedListLa classe LinkedList utilise une liste chaînée afin d’implémenter la classe AbstractSequential-List. Elle est définie de la façon suivante dans le paquetage java.util :

public class LinkedList extends AbstractSequentialList implements List public boolean add(Object object) public void add(int index, Object object) public boolean addAll(Collection collection) public boolean addAll(int index, Collection collection) public void addFirst(Object object) public void addLast(Object object) public void clear() public Object clone() public boolean contains(Object object) public Object getFirst() public Object getLast() public int indexOf(Object object) public int lastIndexOf(Object object) public LinkedList() public LinkedList(Collection collection) public ListIterator listIterator(int index) public Object remove(int index) public boolean remove(Object object) public Object removeFirst() public Object removeLast() public Object set(int index, Object object) public int size() public Object[] toArray() public Object[] toArray(Object[] objects)

[Tokyo, Mexico, Sao Paolo, Seoul, New York, Osaka, Bombay, Calcutta, Buenos Aires][Tokyo, Mexico, Sao Paolo, New York, Bombay, Shanghai, Los Angeles, Calcutta, Buenos Aires]

Page 166: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

156 Listes

Parmi les 24 méthodes définies dans la classe LinkedList, seules les neuf qui apparaissent ici engras ne sont pas des redéfinitions de la méthode correspondante dans l’une des classes ancêtres Abs-tractSequentialList, AbstractList, AbstractCollection ou Object.

Exemple 8.4 Classe Ring

Le programme suivant permet de définir une classe destinée aux listes circulaires doublement liées.Cette classe est similaire à java.util.LinkedList, à l’exception du fait que les méthodesnext() et previous() sont capables de passer d’un bout à l’autre de la liste en cercle.

package schaums.dswj;import java.util.*;

public class Ring extends java.util.AbstractSequentialList private Node header; private int size = 0;

public Ring()

public Ring(List list) super(); addAll(list);

public ListIterator listIterator(int index) return new RingIterator(index);

public int size() return size;

private static class Node Object object; Node previous, next;

Node(Object object, Node previous, Node next) this.object = object; this.previous = previous; this.next = next; Node(Object object) this.object = object; this.previous = this.next = this;

private class RingIterator implements ListIterator private Node next, lastReturned; private int nextIndex;

RingIterator(int index) if (index<0 || index>size) throw new IndexOutOfBoundsException("Index : " + index); next=(size==0?null:header); for (nextIndex=0; nextIndex<index; nextIndex++) next = next.next;

Page 167: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe LinkedList 157

public boolean hasNext() return size > 0;

public boolean hasPrevious() return size > 0;

public Object next() if (size==0) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex = (nextIndex==size-1?0:nextIndex+1); return lastReturned.object;

public Object previous() if (size==0) throw new NoSuchElementException(); next = lastReturned = next.previous; nextIndex = (nextIndex==0?size-1:nextIndex-1); return lastReturned.object;

public int nextIndex() return nextIndex;

public int previousIndex() return (nextIndex==0?size-1:nextIndex-1);

public void add(Object object) if (size==0) next = header = new Node(object); nextIndex = 0; else Node newNode = new Node(object,next.previous,next); newNode.previous.next = next.previous = newNode; lastReturned = null; ++size; nextIndex = (nextIndex==size-1?0:nextIndex+1);

public void remove() if (lastReturned==null) throw new IllegalStateException(); if (next==lastReturned) next = lastReturned.next; else nextIndex = (nextIndex==0?size-1:nextIndex-1); lastReturned.previous.next = lastReturned.next; lastReturned.next.previous = lastReturned.previous; lastReturned = null; --size;

public void set(Object object) if (lastReturned==null) throw new IllegalStateException(); lastReturned.object = object;

Page 168: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

158 Listes

Étant une sous-classe de la classe AbstractSequentialList, cette classe Ring doit obligatoire-ment implémenter les méthodes listIterator(int) et size(). En outre, puisque la méthodelistIterator(int) doit renvoyer un objet ListIterator, notre classe doit également implé-menter les neuf méthodes de l’interface (reportez-vous à la section 8.4) grâce à la classe interneRingIterator.La classe Ring définit deux champs privés (header et size), quatre méthodespubliques et deux classes privées (Node et RingIterator).Une liste chaînée est une séquence de nœuds chaînés, c’est-à-dire d’objets distinctsqui contiennent les données de la liste, à raison d’un composant par nœud. C’est laraison pour laquelle toute implémentation d’une liste chaînée requiert l’utilisation d’une classe Nodeinterne avec un champ destiné aux données et un autre destiné au chaînage.Dans le cas de notre implémentation, le champ de données du nœud est une réfé-rence Object nommée object. Étant donné que ces listes seront doublementchaînées, la classe Node est composée de deux champs de chaînage nommés pre-vious et next. Quelle que soit la structure chaînée utilisée (une liste, un arbre, ungraphe, etc.), chaque champ doit être une référence au même type que la classe denœud elle-même. Ainsi, dans cette implémentation, previous et next sont des références à Node.Le champ header de la classe Ring pointe vers le premier nœud de la liste. Vous remarquerez quecette implémentation n’utilise aucun nœud d’en-tête factice. C’est pourquoi le nombre de nœuds esttoujours égal au nombre réel de composants de la liste stocké dans le champ size de cette classe.La classe interne RingIterator implémente l’interface java.util.Lin-kedList. Elle est composée de trois champs : next et lastReturned, quisont des références aux nœuds de listes, et l’entier nextIndex, qui assure lesuivi du numéro d’index du nœud recherché par l’itérateur. Le champ nextpointe vers le nœud suivant de la liste, soit le prochain à être consulté si l’ité-rateur est avancé. Le champ lastReturned pointe vers le nœud qui a été renvoyé par l’appel pré-cédent de la méthode next() ou previous(). Il recherche le nœud concerné par un appel de laméthode remove() ou set().L’interface ListIterator spécifie si les méthodes hasNext() et hasPrevious() devraientrenvoyer true lorsque la liste contient plus de composants dans la direction correspondante. En cequi concerne la classe Ring, qui n’a ni début, ni fin, ces deux méthodes renverront toujours true,sauf si la liste est vide.Les méthodes next() et previous() avancent l’itérateur jusqu’au nœud suivant dans la directionindiquée. L’instruction next = next.next; est utilisée lorsqu’il s’agit d’avancer. Vous la com-prendrez mieux si vous pensez qu’une référence à un objet est en fait l’adresse de cet objet dans lamémoire principale. Ainsi, cette instruction affecte l’adresse stockée dans next.next à la variablede référence next. Le diagramme suivant illustre l’exemple de 12 objets et de leur adresse mémoireréelle au cours de l’exécution du pilote test de l’exemple 8.5.

header

size 8

Ring

next

object 'B'

Node

previous

next

nextIndex 5

RingIterator

lastReturned

size 8

nextNode

previous

0x15c8518

0x15c8538

0x15c8528

object 0x15c8530

nextNode

previous

0x15c8528

0x15c8288

0x15c8538

object 0x15c8540

nextNode

previous

0x15c8538

0x15c8298

0x15c8288

object 0x15c8290

nextNode

previous

0x15c8288

0x15c84e8

0x15c8298

object 0x15c8820

Ring

0x15c6d50

header 0x15c8298

nextIndex 2

RingIterator

nextlastReturned

0x15c8538

0x15c8288

0x15c8640

value 'B'

Character

0x15c8290

value 'C'

Character

0x15c8540

value 'D'

Character

0x15c8530

value 'A'

Character

0x15c8820

Page 169: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe LinkedList 159

Dans le cas présent, l’adresse stockée dans next.next est 0x15c8528, et celle stockée dans nextest 0x15c8538. Ainsi, l’affectation next = next.next; a modifié la valeur de la référencenext de 0x15c8538 en 0x15c8528. En d’autres termes, le champ next de l’itérateur ne recher-che plus le troisième nœud, mais le quatrième. Vous remarquerez que cette représentation des objetsest plus simple que la suivante, dans laquelle chaque référence d’objet (adresse mémoire) est symbo-lisée par une flèche pointant vers le référent :

Prêtez attention à l’utilisation de l’opérateur d’expression conditionnelle pour incrémenter et dedécrémenter le champ nextIndex, respectivement dans les méthodes next() et previous().Par exemple, l’expression nextIndex = (nextIndex==size-1?0:nextIndex+1); attribuela valeur 0 au champ ou bien elle l’incrémente selon la valeur de sa taille. Lorsque celle-ci est égaleà size-1, c’est que l’itérateur se trouve au niveau du dernier élément de la liste et qu’il devra passerau premier élément si vous l’avancez, c’est-à-dire à l’élément dont l’index est 0. Dans le cas contraire,le champ sera simplement incrémenté.

Les méthodes nextIndex() et previousIndex() renvoient respectivement le dernier élément etl’élément précédent. Ce dernier élément est stocké dans le champ nextIndex de l’itérateur. L’élé-ment précédent doit lui être inférieur de 1, sauf si la valeur de nextIndex est égale à 0, auquel casl’index précédent doit être égal à size-1. Voilà qui illustre parfaitement le fait qu’une liste circu-laire chaînée soit consultée en boucle.

La méthode add() insère l’objet donné comme nouveau composant de la liste. Le diagramme sui-vant illustre le fonctionnement de ce code :

Node newNode = new Node(object,next.previous,next);newNode.previous.next = next.previous = newNode;

Les pointeurs qui seront amenés à changer sont signalés en pointillés. Remarquez comment le poin-teur lastReturned est invalidé (c’est-à-dire devient null) après l’insertion. Cela vous indiqueque la dernière opération effectuée par l’itérateur n’était pas un appel de next(), ni de pre-vious(). En outre, vous pouvez en déduire que, ni la méthode add(), ni la méthode remove() nepeuvent être appelées si vous n’avez pas d’abord appelé une nouvelle fois la méthode next() ouprevious(). Si vous oubliez cette étape, une exception UnsupportedOperationExceptionsera lancée, comme nous l’avons déjà vu dans l’exemple 8.2.

value 'C'

Character

value 'D'

Character

value 'A'

Character

header

size 8

Ring

next

object

Node

previous

next

nextIndex 2

RingIterator

lastReturned

value 'B'

Character

next

object

Node

previous

next

object

Node

previous

next

object

Node

previous

Page 170: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

160 Listes

Exemple 8.5 Tester la classe Ring

import java.util.*;import schaums.dswj.Ring;

public class Ex0808 private static final int SIZE=8; private static Ring ring = new Ring(); public static void main(String[] args) ListIterator it = ring.listIterator(); for (int n=0; n<SIZE; n++) it.add(new Character((char)(’A’+n))); System.out.println(ring); it = ring.listIterator(); // l’initialiser à nouveau printNext(it,10); printPrevious(it,4);

next

nextIndex 4

RingIterator

lastReturned

Avant :

next

object 'C'

Node

previous

next

object 'D'

Node

previous

next

object 'X'

Node

previous

next

nextIndex 4

RingIterator

lastReturned

Après :

next

object 'C'

Node

previous

next

object 'D'

Node

previous

next

object 'X'

Node

previous

Page 171: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe LinkedList 161

System.out.println(ring); it = ring.listIterator(); // l’initialiser à nouveau printNext(it,2); it.remove(); System.out.println(ring); printNext(it,3); it.remove(); System.out.println(ring); printPrevious(it,2); it.remove(); System.out.println(ring); printPrevious(it,3); it.remove(); System.out.println(ring);

private static void printNext(ListIterator it, int n) for (int i=0; i<n; i++) System.out.println("it.nextIndex() = " + it.nextIndex() + "\tit.next() = " + it.next()); private static void printPrevious(ListIterator it, int n) for (int i=0; i<n; i++) System.out.println("it.previousIndex() = " + it.previousIndex() + "\tit.previous() = " + it.previous());

Afin de vous faciliter la tâche, ce programme utilise deux méthodes d’impression : d’une part,printNext() appelle un nombre de fois donné nextIndex() et next(), et d’autre part, print-Previous() effectue la même opération pour previousIndex() et previous().

[A, B, C, D, E, F, G, H]it.nextIndex() = 0 it.next() = Ait.nextIndex() = 1 it.next() = Bit.nextIndex() = 2 it.next() = Cit.nextIndex() = 3 it.next() = Dit.nextIndex() = 4 it.next() = Eit.nextIndex() = 5 it.next() = Fit.nextIndex() = 6 it.next() = Git.nextIndex() = 7 it.next() = Hit.nextIndex() = 0 it.next() = Ait.nextIndex() = 1 it.next() = Bit.previousIndex() = 1 it.previous() = Bit.previousIndex() = 0 it.previous() = Ait.previousIndex() = 7 it.previous() = Hit.previousIndex() = 6 it.previous() = G[A, B, C, D, E, F, G, H]it.nextIndex() = 0 it.next() = Ait.nextIndex() = 1 it.next() = B[A, C, D, E, F, G, H]it.nextIndex() = 1 it.next() = Cit.nextIndex() = 2 it.next() = Dit.nextIndex() = 3 it.next() = E[A, C, D, F, G, H]it.previousIndex() = 2 it.previous() = Dit.previousIndex() = 1 it.previous() = C[A, D, F, G, H]it.previousIndex() = 0 it.previous() = Ait.previousIndex() = 4 it.previous() = Hit.previousIndex() = 3 it.previous() = G[A, D, F, H]

Page 172: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

162 Listes

Tout d’abord, ce programme charge l’anneau à l’aide de huit objets Character :

Il utilise ensuite ses méthodes d’impression afin de parcourir l’anneau dans le sens des aiguillesd’une montre et dans le sens inverse, ce qui vous permet de comprendre comment les numérosd’index sont reliés aux nœuds.

Après avoir réinitialisé l’itérateur, ce code avance à deux reprises de façon à ce que le champ last-Returned pointe vers le nœud B. Ensuite, l’appel it.remove() supprime le nœud B, commel’illustre la sortie de la méthode println(ring) suivante. De la même façon, si vous avancezencore trois fois, l’appel it.remove() supprime le nœud E.

Vous avez probablement remarqué que, juste avant cet appel de it.remove(), l’appel précédentavait renvoyé l’index 3. Avant la première suppression, le nœud E était associé à l’index 4 mais, à cestade (c’est-à-dire après la suppression du nœud B), il ne reste plus que trois nœuds le précédant (A,C et D) ; son index est donc désormais égal à 3. L’index d’un élément séquentiel est toujours égal aunombre d’éléments le précédant.

L’appel suivant de it.remove() intervient après deux appels de it.previous(). À ce stade, lenœud supprimé (le nœud C) était associé à l’index 1. En dernier lieu, à la suite des trois appels sup-plémentaires de it.previous(), l’itérateur bidirectionnel parcourt la liste en boucle et c’est lenœud G qui se trouve supprimé.

Exemple 8.6 Problème de Josephus

Ce problème est inspiré d’une histoire attribuée à Joseph ben Matthias (Josephus). Ce dernier avaitconclu un pacte de suicide avec 40 soldats alors qu’ils étaient assiégés par les forces romaines biensupérieures (en 67 ap. J.-C.). Il avait proposé que chaque homme tue son voisin et s’était arrangépour être le dernier. C’est ainsi qu’il put raconter cette histoire.

A

B

C

D

E

F

G

H

header

size 7

Ring

ring

next

nextIndex 4

RingIterator

it

lastReturned

Page 173: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe LinkedList 163

Notre classe Ring va nous permettre de simuler une solution à ce problème :

import java.util.*;import schaums.dswj.*;

public class Ex0806 public static void main(String[] args) Ring ring = new Ring(); ListIterator it = ring.listIterator(); int N = get("Entrez le nombre de soldats"); for (int k=0; k<N; k++) it.add(new Character((char)(’A’+k))); System.out.print(N + " soldats : "); System.out.println(ring); while (ring.size() > 1) Object killer = it.next(); System.out.println(killer + " tue " + it.next()); it.remove(); System.out.println("Le seul survivant est " + it.next());

Ce programme utilise la méthode suivante de saisie des entiers :

public static int get(String prompt) int n=0; try InputStreamReader reader = new InputStreamReader(System.in); BufferedReader in = new BufferedReader(reader); System.out.print(prompt + ": "); String input = in.readLine(); n = Integer.parseInt(input);

catch(Exception e) System.out.println(e); return n;

Il est défini dans la classe schaums.dswj.IO.

Notez que la classe Ring ne peut stocker que des objets, au même titre que toutes les structures dedonnées des collections Java. C’est pourquoi nous devons envelopper chaque entier dans un objetInteger afin de le stocker dans l’anneau.

Entrez le nombre de soldats : 88 soldats : [A, B, C, D, E, F, G, H]A tue BC tue DE tue FG tue HA tue CE tue GA tue ELe seul survivant est A

A

E

CG

B

D

H

F

Page 174: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

164 Listes

8.7 ITÉRATEURS DE LISTES INDÉPENDANTSIl est possible d’utiliser plusieurs itérateurs sur une même liste chaînée tant qu’ils n’essaient pas de lamodifier.

Exemple 8.7 Exemple de simulation de promenades aléatoires

import java.util.*;

public class Ex0807 private static final int SIZE=26; private static LinkedList list = new LinkedList(); private static Random random = new Random();

public static void main(String[] args) initializeList(); ListIterator it1 = list.listIterator(); ListIterator it2 = list.listIterator(); moveForward(it1); moveForward(it2); moveBackward(it1); moveBackward(it2); moveForward(it1); moveForward(it2);

private static void initializeList() ListIterator it = list.listIterator(); for (int k=0; k<SIZE; k++) it.add(new Character((char)(’A’+k)));

private static void moveForward(ListIterator it) int n = random.nextInt(SIZE-it.previousIndex()); for (int i=0; i<n; i++) System.out.print(it.next()); System.out.println();

private static void moveBackward(ListIterator it) int n = random.nextInt(it.nextIndex()); for (int i=0; i<n; i++) System.out.print(it.previous()); System.out.println();

Ce programme vous permet de simuler deux promenades aléatoires sur une même liste chaînée. Ilutilise les méthodes locales moveForward() et moveBackward() pour représenter les prome-nades. Chaque appel déplace un nombre aléatoire de pas dans la direction donnée. Les méthodes

ABCDEFGHIJKLMNOPQRABCDERQPONEDNOPDEFGHIJKLMNO

Page 175: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 165

previousIndex() et nextIndex() de ListIterator évitent au programme d’atteindre la finde la liste et de la dépasser.Au cours de cette exécution, l’itérateur it1 se déplace d’abord vers l’avant de 18 éléments jusqu’àR. Ensuite, l’itérateur it2 se déplace indépendamment de 5 éléments vers l’avant jusqu’à E. Puisl’itérateur it1 se déplace vers l’arrière de 5 éléments, de R à N, et l’itérateur it2 se déplace indépen-damment de 2 éléments vers l’arrière, soit de E à D. Attention, lorsqu’un itérateur inverse son sens dedéplacement, il compte à nouveau l’élément courant. En dernier lieu, it1 se déplace vers l’avantde 3 éléments, de N à P, et l’itérateur it2 se déplace indépendamment de 12 éléments vers l’avant,soit de D à O.La figure suivante illustre la position des deux itérateurs après leur deuxième séquence de déplace-ments :

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

8.1 Qu’est-ce qui caractérise l’interface List par rapport à l’interface Collection ?

8.2 En quoi les classes AbstractCollection et AbstractList se différencient-elles ?

8.3 En quoi les classes AbstractList et AbstractSequentialList se différencient-elles ?

8.4 En quoi les interfaces Iterator et ListIterator se différencient-elles ?

8.5 En quoi les classes ArrayList et LinkedList se différencient-elles ?

8.6 En quoi les objets ArrayList et Vector se différencient-ils ?

8.7 Dans quelles conditions est-il préférable d’utiliser un objet ArrayList plutôt qu’un objetLinkedList, et inversement ?

RÉPONSES¿RÉPONSES

8.1 L’interface List comprend les 10 méthodes suivantes qui fonctionnent avec des index :

• public void add(int index, Object object);• public boolean addAll(int index, Collection collection);• public Object get(int index);• public int indexOf(Object object);• public int lastIndexOf(Object object);• public ListIterator listIterator();• public ListIterator listIterator(int index);• public Object remove(int index);• public Object set(int index, Object object);• public List subList(int start, int stop);

8.2 La classe AbstractList implémente les méthodes de l’interface List, dont les 10 méthodesd’index listées dans la réponse 8.1 qui ne se trouvent pas dans la classe AbstractCollection.

HGFEDCBA PONMLKJI

it2 it1

Page 176: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

166 Listes

8.3 La classe AbstractSequentialList est conçue de façon à être utilisée comme classe debase des classes de listes chaînées. Elle spécifie les méthodes abstraites listIterator() etsize() qui doivent être implémentées par une sous-classe concrète quelle qu’elle soit.

8.4 La classe ListIterator étend la classe Iterator sur le même modèle que la classe Abs-tractSequentialList avec AbstractList (reportez-vous à la réponse 8.2). Les objetsIterator standard sont des itérateurs unidirectionnels qui sont répétés sur des listes detableaux, alors que les objets ListIterator sont des itérateurs bidirectionnels qui sont répétéssur des listes chaînées.

8.5 Contrairement aux instances de la classe ArrayList qui utilisent un stockage contigu et indexédont l’accès est direct (à l’aide des tableaux), les instances de la classe LinkedList utilisent unstockage d’accès chaîné (séquentiel). Pour résumer, les listes de tableaux vous offrent donc unaccès plus rapide, tandis que les listes chaînées vous permettent d’effectuer plus rapidement vosmodifications (insertions et suppressions)

8.6 Les objets ArrayList et Vector ne présentent que peu de différences dans la mesure où ilspermettent tous les deux un accès direct indexé. La classe ArrayList faisant partie du fra-mework de collections Java, elle a fait son apparition récemment avec Java 1.2, c’est probable-ment pourquoi elle remporte la faveur des programmeurs. Quant à la classe Vector, elle a beauproposer deux fois plus de méthodes, celles-ci sont souvent redondantes et prêtent par consé-quent à confusion.

8.7 Il est préférable d’utiliser un objet ArrayList si vous prévoyez de nombreuses recherches, etun objet LinkedList si vous prévoyez des insertions et/ou des suppressions fréquentes (voir laréponse 8.6).

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

8.1 Implémentez la méthode suivante :

• public static void loadRandomLetters(LinkedList list, int n)• // remplit la liste de n majuscules générées de façon aléatoire

8.2 Écrivez une méthode qui utilise un itérateur pour imprimer le contenu d’une liste chaînée à rai-son d’un objet par ligne.

8.3 Écrivez une méthode qui utilise un itérateur pour imprimer le contenu d’une liste chaînée àl’envers, toujours à raison d’un objet par ligne.

8.4 Écrivez la méthode suivante :

• public static void exchange(LinkedList list, int i, int j)• // intervertit les éléments au niveau des index i et j

8.5 Modifiez la solution au problème de Josephus (reportez-vous à l’exemple 8.6) pour qu’elle uti-lise également un paramètre skip afin de générer la sortie. La valeur de skip est une constanteentière non négative qui spécifie quelle personne doit être tuée par chaque soldat. Par exemple, siskip = 2, A tue D, ce qui signifie qu’il ignore B et C, puis E tue H, etc. La solution initiale cor-respondait au cas spécial où skip = 0.

8.6 Modifiez le programme des promenades aléatoires présenté dans l’exemple 8.7 de façon à ce queles itérateurs se déplacent autour de l’objet Ring et n’utilisent plus une liste chaînée (voirl’exemple 8.5). Modifiez les méthodes pour que les déplacements soient effectués en boucleautour de l’anneau.

Page 177: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 167

8.7 Modifiez le programme des promenades aléatoires présenté dans l’exemple 8.7 de façon à ce queles itérateurs se déplacent autour de l’objet Ring et n’utilisent plus une liste chaînée (voir l’exer-cice précédent). Faites également en sorte que la direction change de façon aléatoire.

SOLUTIONS¿SOLUTIONS

8.1 • public static void loadRandomLetters( LinkedList list, int n )• list.clear();• while (0 < n--)• list.add("" + (char)(’A’ + (int)(Math.random()*26)));•

8.2 • public static void printForward(LinkedList list )• for (ListIterator itr=list.listIterator(); itr.hasNext(); )• System.out.println(itr.next());•

8.3 • public static void printBackward(LinkedList list)• ListIterator itr=list.listIterator(list.size());• while (itr.hasPrevious())• System.out.println(itr.previous());•

8.4 • public static void exchange(LinkedList list, int i, int j)• Object ithObj = list.get(i);• Object jthObj = list.get(j) ;• list.set(i,jthObj);• list.set(j,ithObj);•

8.5 La solution générale au problème de Josephus est la suivante :

• public class Pr0805• public static void main(String[] args)• Ring ring = new Ring();• ListIterator it = ring.listIterator();• int n = get("Entrez le nombre de soldats");• int skip = get("Entrez le nombre de soldats à ignorer");• for (int k=0; k<n; k++)• it.add(new Character((char)(’A’+k)));• System.out.print(n + " soldats : ");• System.out.println(ring);• while (ring.size() > 1)• Object killer = it.next();• for (int i=0; i<skip; i++)• it.next();• System.out.println(killer + " tue " + it.next());• it.remove();• • System.out.println("Le seul survivant est " + it.next());• •

8.6 Le programme des promenades aléatoires peut-être modifié de la façon suivante pour que les ité-rateurs se déplacent autour d’un objet Ring :

Page 178: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

168 Listes

• import java.util.*; // définit la classe ListIterator• import schaums.dswj.*; // définit la classe Ring•• public class Pr0806• private static final int SIZE=10;• private static final int MAX_WALK=20;• private static Ring ring = new Ring();• private static Random random = new Random();•• public static void main(String[] args)• initializeRing();• ListIterator it1 = ring.listIterator();• ListIterator it2 = ring.listIterator();• moveForward(it1);• moveForward(it2);• moveBackward(it1);• moveBackward(it2);• moveForward(it1);• moveForward(it2);• •• private static void initializeRing()• ListIterator it = ring.listIterator();• for (int k=0; k<SIZE; k++)• it.add(new Character((char)(’A’+k)));• •• private static void moveForward(ListIterator it)• int n = random.nextInt(MAX_WALK);• for (int i=0; i<n; i++)• System.out.print(it.next());• System.out.println();• •• private static void moveBackward(ListIterator it)• int n = random.nextInt(MAX_WALK);• for (int i=0; i<n; i++)• System.out.print(it.previous());• System.out.println();• •

8.7 Le programme des promenades aléatoires peut être modifié de la façon suivante pour que les ité-rateurs se déplacent autour de l’objet Ring dans des directions aléatoires :

• import java.util.*; // définit la classe ListIterator• import schaums.dswj.*; // définit la classe Ring•• public class Pr0807• private static final int SIZE=10;• private static final int MAX_WALK=20;• private static Ring ring = new Ring();• private static Random random = new Random();•• public static void main(String[] args)• initializeRing();• ListIterator it1 = ring.listIterator();• ListIterator it2 = ring.listIterator();• move(it1);• move(it2);

Page 179: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 169

• move(it1);• move(it2);• move(it1);• move(it2);• •• private static void initializeRing()• ListIterator it = ring.listIterator();• for (int k=0; k<SIZE; k++)• it.add(new Character((char)(’A’+k)));• •• private static void move(ListIterator it)• int n = random.nextInt(MAX_WALK);• boolean forward = (random.nextInt(2)==1);• if (forward)• for (int i=0; i<n; i++)• System.out.print(it.next());• else• for (int i=0; i<n; i++)• System.out.print(it.previous());• System.out.println();• •

Page 180: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie
Page 181: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 9

Arbres

Un arbre est un conteneur non linéaire qui modéliseune relation hiérarchique dans laquelle tous les élé-ments peuvent avoir plusieurs successeurs (qualifiésd’enfants) et un prédécesseur unique (le parent), saufdans le cas de la racine qui est le seul élément sansparent. Les arbres sont couramment utilisés en infor-matique, notamment pour les structures des systè-mes de fichiers. Dans le cadre de Java, vous y aurezsouvent recours pour les structures d’héritage desclasses, la durée d’exécution de l’appel des métho-des lors de l’exécution d’un programme, la classifi-cation des types et la définition syntaxique de celangage de programmation lui-même.

ObjectAbstractCollection

AbstractList

ArrayList

AbstractSequentialListLinkedList

VectorStack

AbstractSetHashSetTreeSet

AbstractMapHashMapTreeMap

ArraysBitSetCollectionsDictionary

HashtableProperties

WeakHashMap

Type

Type de référenceType Array (tableau)

Type numériqueboolean

Type entierbyte

double

Type InterfaceType Class (classe)

Type primitif

Type à virgule flottantefloat

shortintlongchar

Page 182: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

172 Arbres

9.1 TERMINOLOGIE DES ARBRESLa définition récursive d’un arbre non ordonné est la suivante :

Définition : Un arbre est soit un ensemble vide, soit une paire (r, S), r étant un nœud et S un ensembled’arbres disjoints ne contenant pas r.

Le nœud r est qualifié de racine de l’arbre T et les éléments de l’ensemble S sont ses sous-arbres. Cetensemble S peut bien évidemment être vide. La restriction selon laquelle aucun sous-arbre ne peutcontenir de racine s’applique récursivement, à savoir que r ne peut se trouver dans aucun sous-arbre, nidans aucun sous-arbre d’un sous-arbre, et ainsi de suite.

D’après cette définition, le deuxième composant d’un arbre peut être un ensemble de sous-arbres, cequi signifie que l’ordre des sous-arbres n’a absolument aucune importance.

Exemple 9.1 Arbres non ordonnés égaux

Les deux arbres suivants sont égaux :

L’arbre de gauche a une racine a et deux sous-arbres B et C, avec B = (b, ), C = (c, D). D est lesous-arbre D = (d, ). L’arbre de droite a la même racine a et le même ensemble de sous-arbres B, C= C, B, c’est pourquoi (a, B, C) = (a, C, B).

Les éléments qui composent un arbre sont qualifiés de nœuds. Techni-quement, chaque nœud est le composant d’un seul sous-arbre, à savoirl’arbre dont il est la racine. Cependant, un arbre est indirectement com-posé de sous-arbres imbriqués et chaque nœud est considéré comme unélément de tous les arbres dans lesquels il est imbriqué. C’est pourquoi a,b, c et d sont tous considérés comme des nœuds de l’arbre A. De la mêmefaçon, c et d sont tous les deux des nœuds de l’arbre C.

La taille d’un arbre correspond au nombre de nœuds qu’il contient.Ainsi, l’arbre A de la figure précédente a une taille égale à 4 et l’arbre C aune taille égale à 2. Un arbre dont la taille est égale à 1 est qualifié de sin-gleton, comme c’est le cas des arbres B et D de notre figure. Un arbre videest un arbre de taille 0 ; il est signalé à l’aide du symbole ∅. Vous remar-querez que ce type d’arbre est unique puisque tous les arbres vides sontégaux.

Si T = (x, S) est un arbre, x est la racine de T et S est son ensemble de sous-arbres S = T1, T2, …, Tn.Chaque sous-arbre Tj est lui-même un arbre ayant sa propre racine rj. Dans ce cas, le nœud r est qualifiéde parent de chaque nœud rj qui est donc considéré comme l’enfant de r. En général, nous disons quedeux nœuds sont adjacents si l’un est le parent de l’autre.

Un nœud sans enfant est une feuille et un nœud composé d’au moins un enfant est qualifié de nœudinterne.

=b

a

d

c b

a

d

c

D

CB

A

b

a

d

c

Page 183: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Terminologie des arbres 173

Le chemin est une séquence de nœuds (x0, x1, x2…, xm) dans laquelle les nœuds de chaque paireayant des indices adjacents (xi – 1, xi) sont des nœuds adjacents. Par exemple, (a, b, c, d) est le chemin del’arbre que nous venons de voir, mais cela n’est pas le cas de (a, d, b, c). La longueur d’un chemin cor-respond au nombre m de ses paires adjacentes.

Nous pouvons déduire de cette définition que les arbres sont acycliques, c’est-à-dire qu’aucun che-min ne peut contenir le même nœud plusieurs fois.

Le chemin joignant la racine pour le nœud x0 d’un arbre est le chemin (x0, x1, x2…, xm) dans lequelxm est la racine de l’arbre. Le chemin joignant la racine à un nœud feuille est qualifié de chemin feuille-vers-racine.

Théorème 9.1 : chaque nœud d’un arbre comporte un seul chemin de racine.Pour consulter la démonstration de ce théorème, reportez-vous à l’exercice d’entraînement 9.1.

La profondeur d’un nœud correspond à la longueur de son chemin jusqu’à la racine. Il est évidentque la profondeur de la racine d’un arbre est, par définition, égale à 0. Nous pouvons également faireréférence à la profondeur du sous-arbre d’un arbre en parlant de la profondeur de sa racine.

Le niveau d’un arbre est l’ensemble de nœuds se trouvant à une profondeur donnée.La hauteur d’un arbre correspond à la profondeur la plus grande parmi tous les nœuds. Si nous repre-

nons l’arbre de répertoires Windows présenté en début de chapitre, nous pouvons constater que sa hau-teur est égale à 4. Par définition, la hauteur d’un singleton est égale à 0 et celle d’un arbre vide à –1.

Un nœud y est considéré comme l’ancêtre d’un nœud x s’il se trouve sur le chemin de x qui rejoint laracine. Vous remarquerez que la racine d’un arbre est donc l’ancêtre de chaque autre nœud du mêmearbre.

De la même manière, un nœud x est considéré comme le descendant d’un autre nœud y si y est unancêtre de x. Pour chaque nœud y d’un arbre, l’ensemble contenant y et tous ses descendants formentdonc le sous-arbre de racine y. Si S est un sous-arbre de T, nous pouvons dire que T est le superarbre de S.

La longueur de chemin d’un arbre correspond à la somme de la longueur de tous ses chemins partantde la racine. Il s’agit en fait d’une somme pondérée pour laquelle vous ajoutez chaque niveau multipliépar le nombre de nœuds qu’il contient. La longueur du chemin de l’arbre représenté ci-après est donc lasuivante : 1⋅3 + 2⋅4 + 3⋅8 = 35.

Exemple 9.2 Propriétés d’un arbre

La racine de l’arbre ci-contre est le nœud a. Les six nœudsa, b, c, e, f et h sont des nœuds internes., les neuf autresnœuds étant des feuilles. Le chemin (m, h, c, a) constituele chemin feuille-vers-racine et il a une longueur égale à3. Le nœud b a une profondeur de 1 et le nœud m de 3. Leniveau 2 est composé des nœuds e, f, g et h.La hauteur de cet arbre est égale à 3. Les nœuds a, c et hsont tous des ancêtres du nœud m et le nœud k est un des-cendant du nœud c, mais pas du nœud b. Le sous-arbre deracine b est composé des nœuds b, e, i et j.Le degré d’un nœud correspond à son nombre d’enfants.

Dans l’exemple précédent, b a un degré égal à 1, d a un degréde 0 et h de 5. L’ordre d’un arbre est le degré maximum parmitous ses nœuds.

Un arbre est complet lorsque tous ses nœuds internes ontle même degré et que toutes ses feuilles se trouvent au mêmeniveau. L’arbre suivant est complet. Il a un degré égal à3 et estcomposé de 40 nœuds au total.

e

kj

b

i

f

a

g

d

o

h

c

l nm

Page 184: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

174 Arbres

Théorème 9.2 : l’arbre complet d’ordre d et de hauteur h comporte nœuds.

Pour consulter la démonstration de ce théorème, reportez-vous à l’exercice d’entraînement 9.1.

Corollaire 9.1 : la hauteur d’un arbre complet d’ordre d et de taille n est h = logd (nd – n + 1) – 1.

Corollaire 9.2 : le nombre de nœuds d’un arbre de hauteur h est au maximum égal à ,d étant le degré maximum parmi tous les nœuds.

9.2 ARBRES DÉCISIONNELS ET DIAGRAMMES DE TRANSITION

Un arbre décisionnel résume tous les résultats susceptibles d’être obtenus à la suite d’un traitement enplusieurs étapes. Chaque nœud interne de ce type d’arbre est associé à une question et les branches qui leconnectent à ses enfants sont associées aux différentes réponses possibles à cette question.

Exemple 9.1 Retrouver la fausse pièce

Supposons que vous deviez retrouver la fausse pièce parmi cinq pièces qui semblent identiques.Cette fausse pièce se distingue des autres uniquement parce qu’elle est plus légère. La seule solutionqui s’offre à vous consiste donc à peser un sous-ensemble de pièces et à le comparer à un autre. Resteà savoir comment sélectionner les sous-ensembles de pièces.Dans l’arbre décisionnel ci-après, le symbole ~ signifie que le poids des deux opérandes est com-paré. Ainsi, par exemple, a, b ~ d, e signifie que le poids des pièces a et b est comparé à celui despièces d et e.

Un diagramme de transition est un arbre, ou un graphe (voir le chapitre 12), dont les nœuds internesreprésentent divers états (situations) pouvant être obtenus au cours d’un traitement en plusieurs éta-pes. Comme dans le cadre d’un arbre décisionnel, chaque feuille correspond à un résultat différent.Chaque branche indique la probabilité (condition) d’occurrence de l’événement enfant, si l’événe-ment parent a eu lieu.

dh 1+ 1–d 1–

---------------------

dh 1+ 1–d 1–

---------------------

a, b ≈ d, e

a ≈ b

c

a

b

d

e

a, b = d, e

a, b < d, e

a, b > d, e

d ≈ e

a<b

a>b

d<e

d>e

Page 185: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Arbres décisionnels et diagrammes de transition 175

Exemple 9.4 Jeu de craps

Le craps se joue avec des dés et deux joueurs, X et Y. Tout d’abord, X lance la paire de dés. Si lasomme de ces derniers est égale à 7 ou à 11, X gagne. En revanche, si elle est égale à 2, 3 ou 12, c’estY qui gagne. Dans tous les autres cas, la somme est qualifiée de « point » qui doit être à nouveauatteint au cours du lancer de dés suivant. Donc, si aucun joueur ne sort gagnant du premier lancer, lesdés sont jetés jusqu’à ce que la somme du point sorte à nouveau ou jusqu’à ce qu’un 7 sorte. Dans cedeuxième cas de figure, Y l’emporte. Dans le cas contraire, c’est X qui gagne lorsque la somme dupoint sort.Le diagramme de transition suivant modélise le fonctionnement du jeu de craps :

Lorsque vous lancez une paire de dés, 36 résultats différents peuvent sortir (6 possibilités pour lepremier dé multipliées par 6 autres pour le second lancé avec le premier). Parmi ces 36 résultatspossibles, un seul lancer peut créer une somme égale à 2 (1 + 1), 2 lancers différents peuvent créerune somme égale à 3 (1 + 2 ou 2 + 1) et un seul lancer peut créer une somme égale à 12 (6 + 6).Il y a donc 4 chances sur 36 d’obtenir un 2, un 3 ou un 12, soit une probabilité de 4/36 = 1/9. De lamême façon, il existe 6 possibilités d’obtenir un 7 et 2 possibilités d’obtenir un 11, soit une proba-bilité de 8/36 = 2/9. Les autres probabilités du premier niveau de l’arbre sont calculées de la mêmefaçon.

Afin d’illustrer le calcul des probabilités au deuxième niveaude l’arbre, nous allons prendre l’exemple du point 4. Si lelancer suivant est égal à 4, X gagne. S’il est égal à 7, c’est Yqui l’emporte. Dans les autres cas, X doit continuer à lancerles dés. Le diagramme de transition ci-contre résume cestrois possibilités :

X

Y4

1/32/3

X

Y5

2/53/5

X

Y6

5/116/11

X

Y8

5/116/11

X

Y9

2/53/5

X

Y10

1/32/3

7 ou 11, X gagne

2, 3, ou 12, donc

5/36

5/36

1/9

1/9

1/12

1/12

1/9

2/9

donc

Y gagne

4

74

1/121/6

3/4

Page 186: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

176 Arbres

Les probabilités 1/12, 1/6 et 3/4 sont calculées de la façon suivante :

P(4) = 3/36 = 1/12

P(7) = 6/36 = 1/3

P(2, 3, 5, 6, 8, 9, 10, 11 ou 12) = 27/36 = 3/4

Une fois que le point 4 a été établi avec le premier lancer, la probabilité que X gagne au deuxièmelancer est de 1/12, et la probabilité qu’il doive lancer les dés une troisième fois est de 3/4. La proba-bilité que X gagne au troisième lancer est donc de (3/4)(1/12) et celle qu’il aille au quatrième lancerde (3/4)(3/4). Toujours sur le même modèle, la probabilité que X gagne au quatrième lancer est de(3/4)(1/12) + (3/4)(3/4)(1/12), etc. Si nous additionnons ces probabilités partielles, nous pouvonsconclure que la probabilité de voir X gagner à chaque lancer une fois le point 4 établi avec le premierlancer est la suivante :

Ce calcul applique la formule des séries géométriques (voir la section A.10 de l’annexe A).

Si la probabilité de voir X gagner une fois le point 4 établi au premier lancer est de 1/3, la probabilitéde voir Y l’emporter dans ce cas doit être de 2/3. Les autres probabilités du deuxième niveau sont cal-culées de la même façon.

Nous pouvons maintenant calculer la probabilité de voir X gagner à partir du diagramme de transi-tion principal :

La probabilité de voir X gagner est donc de 49,29 % et celle de voir Y l’emporter de 50,71 %.

La méthode stochastique peut donc être analysée par un diagramme de transition, c’est-à-direqu’elle peut être décomposée en séquences d’événements dont les probabilités conditionnelles peuventêtre calculées. Le jeu de craps illustre parfaitement le traitement stochastique puisque le nombre d’évé-nements susceptibles de se produire est illimité. Comme dans l’analyse de l’exemple 9.2, la plupart des

1 4/-----------

+P41

12------ 3

4---

112------ 3

4---

2 112------ 3

4---

3 112------ 3

4---

4 112------ 3

4---

5 112------ …+ + + + +=

112------

134---–

------------=

1 12/=

13---=

P29--- 1

12------ P4( )

19--- P5( )

536------ P6( )

536------ P8( )

19--- P9( )

112------ P10( )+ + + + + +=

29--- 1

12------ 1

3---

19--- 2

5---

536------ 5

11------

536------ 5

11------

19--- 2

5---

112------ 1

3---

+ + + + + +=

244495---------=

0.4929=

Page 187: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Arbres ordonnés 177

traitements stochastiques peuvent être reformulés en un traitement fini équivalent applicable aux ordi-nateurs.

Remarquez que, contrairement aux autres modèles d’arbres, les arbres de transition et décisionnelssont généralement dessinés de la gauche vers la droite de façon à suggérer le mouvement dépendant dutemps d’un nœud à l’autre.

9.3 ARBRES ORDONNÉSLa définition récursive d’un arbre ordonné est la suivante :

Un arbre ordonné est soit vide, soit constitué d’une paire T(x, S), le premier composant x étant unnœud et le second composant S une séquence d’arbres disjoints ordonnés qui ne contiennent pas x.

Le nœud r est qualifié de racine de l’arbre T et les éléments de l’ensemble S sont ses sous-arbres. Cetensemble S peut bien évidemment être vide. La restriction selon laquelle aucun sous-arbre ne peutcontenir de racine s’applique récursivement, à savoir que r ne peut se trouver dans aucun sous-arbre, nidans aucun sous-arbre d’un sous-arbre, et ainsi de suite.

Comme vous pouvez le constater, cette définition est identique à celle des arbres non ordonnés, à uneexception près : les sous-arbres des arbres non ordonnés sont contenus dans une séquence et non dans unensemble. Ainsi, si deux arbres sont composés des mêmes sous-ensembles, ils sont considérés commeétant égaux. En revanche, dans le cas des arbres ordonnés, ils ne peuvent être égaux que si leurs sous-arbres égaux sont dans le même ordre

Exemple 9.5 Arbres ordonnés inégaux

Les deux arbres ordonnés suivants ne sont pas égaux :

En effet, l’arbre de gauche a une racine a et deux sous-arbres B et C, avec B = (b, ∅) et C = (c, (D)).En outre, D est le sous-arbre D = (d, ∅). Quant à l’arbre de droite, il a la même racine a et les mêmessous-arbres. Cependant, ces sous-arbres ne sont pas dans le même ordre, c’est pourquoi les séquen-ces ne sont pas égales : (B, C) ≠ (C, B).

Si vous respectez la définition au pied de la lettre, vous prendrez conscience d’une subtilité à laquellepeu de programmeurs prêtent attention, comme illustré dans l’exemple suivant :

Exemple 9.6 Arbres ordonnés inégaux, deuxième exemple

Les deux arbres T1 = (a, (B, C)) et T2 = (a, (B, ∅, C)) sont des arbres différents,même s’ils seraient probablement dessinés tous les deux de la façon suivante : En fait, techniquement parlant, T1 est un arbre dont l’ordre est égal à 2, tandis queT2 a un ordre égal à 3.

La terminologie des arbres non ordonnés s’applique également aux arbres ordonnés. Deux termessupplémentaires vous seront cependant utiles : le premier enfant et le dernier enfant d’un nœud dans unarbre ordonné. Afin de vous faciliter la tâche, vous pouvez considérer que vous avez affaire à un arbregénéalogique dans lequel les enfants sont classés par âge, du plus vieux au plus jeune.

a

b c

d

a

c b

d

a

b c

Page 188: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

178 Arbres

9.4 ALGORITHMES DE PARCOURS DES ARBRES ORDONNÉSUn algorithme de parcours est une méthode de traitement d’une structure de données qui applique uneopération spécifique à chaque élément de cette structure. Par exemple, si l’opération consiste à imprimer lecontenu de l’élément, le parcours imprime chaque élément de la structure. Le fait d’appliquer l’opérationà un élément est qualifié de visite de cet élément. Lors de l’exécution de l’algorithme de parcours, chaqueélément de la structure est donc visité dans un ordre qui dépend de l’algorithme sélectionné. Les algo-rithmes de parcours d’un arbre général les plus courants sont au nombre de trois.

L’algorithme de parcours en largeur visite la racine, puis chaque élément du premier niveau, avantde visiter chaque élément du deuxième niveau, etc., en visitant systématiquement chaque élément d’unniveau avant de passer au niveau suivant. En fait, si l’arbre est composé de façon habituelle avec sa racineen haut et les feuilles près du bas, le parcours en largeur s’effectue de gauche à droite, puis de haut enbas, comme si vous lisiez un texte en français.

Exemple 9.7 Parcours en largeur

Le parcours en largeur d’un arbre visite les nœuds dans l’ordre a, b, c, d, e, f, g, h, i, j, k, l, m, commeillustré dans la figure suivante :

Algorithme 9.1 Parcours en largeur d’un arbre ordonné

Pour parcourir un arbre ordonné non vide :

1. Initialisez une file d’attente.

2. Mettez la racine en attente.

3. Répétez les étapes 4 à 7 jusqu’à ce que la file d’attente soit vide.

4. Retirez le nœud x de la queue.

5. Visitez x.

6. Mettez tous les enfants de x en attente dans l’ordre.

L’algorithme de parcours préfixe visite d’abord la racine, puis parcourt de façon préfixe chaquesous-arbre récursivement.

Exemple 9.8 Parcours préfixe

Le parcours préfixe de l’arbre présenté dans l’exemple 9.8 visiterait les nœuds dans l’ordre suivant :a, b, e, h, i, f, c, d, g, j, k, l, m.

g

c

l

f

mki jh

b d

e

a

Page 189: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithmes de parcours des arbres ordonnés 179

Vous remarquerez que le parcours préfixe d’un arbre peut être obtenu en effectuant des cercles, c’est-à-dire en commençant par la racine, puis en visitant chaque nœud la première fois que vous le ren-contrez sur votre gauche.

Algorithme 9.2 Parcours préfixe d’un arbre ordonné

Pour parcourir un arbre ordonné non vide :

1. Visitez la racine.2. Effectuez un parcours préfixe de chaque sous-arbre dans l’ordre.

L’algorithme de parcours postfixe parcourt de façon postfixe chaque arbre récursivement, puis ilvisite la racine.

Exemple 9.9 Parcours postfixe

Le parcours postfixe de l’arbre présenté dans l’exemple 9.8 visiterait les nœuds dans l’ordre suivant :h, i, e, f, b, c, j, k, l, m, g, d, a.

Algorithme 9.3 Parcours postfixe d’un arbre ordonné

Pour parcourir un arbre ordonné non vide :

1. Effectuez un parcours préfixe récursif de chaque sous-arbre dans l’ordre.2. Visitez la racine.

Remarquez que le parcours en largeur et le parcours pré-fixe visitent systématiquement la racine de chaque sous-arbreavant de visiter les autres nœuds. En revanche, le parcourspostfixe visite toujours la racine de chaque sous-arbre aprèsavoir visité tous ses autres nœuds. En outre, le parcours pré-fixe visite toujours le nœud le plus à droite en dernier tandisque le parcours postfixe visite toujours le nœud le plus à gau-che en premier. Les parcours préfixe et postfixe sont récursifset peuvent également être implémentés itérativement à l’aided’une pile. Le parcours en largeur est implémenté itérative-ment à l’aide d’une file.

g

c

l

f

mki jh

b d

e

a

g

c

l

f

mki jh

b d

e

a

Page 190: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

180 Arbres

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

9.1 Dans le cadre de Java, toutes les classes forment un seul arbre communément appelé arbred’héritage Java. Mettez en pratique les notions que nous venons d’étudier et répondez aux ques-tions suivantes concernant cet arbre :

a. Quelle est la taille de l’arbre d’héritage Java dans Java 1.3 ?b. Quelle est la racine de cet arbre ?c. Quel type de nœud représente la classe final de cet arbre ?

9.2 Indiquez si les affirmations suivantes sont vraies ou fausses :

a. La profondeur d’un nœud est égale au nombre de ses ancêtres.b. La taille d’un sous-arbre est égale au nombre de descendants de la racine du sous-arbre.c. Si x est un descendant de y, la profondeur de x est plus importante que celle de y.d. Si la profondeur de x est plus importante que celle de y, cela signifie que x est un descendant

de y.e. Un arbre est un singleton si et seulement si sa racine est une feuille.f. Chaque feuille d’un sous-arbre est également la feuille de son superarbre.g. La racine d’un sous-arbre est également la racine de son superarbre.h. Le nombre d’ancêtres d’un nœud est égal à sa profondeur.i. Si R est un sous-arbre de S et que S est un sous-arbre de T, alors R est un sous-arbre de T.j. Un nœud est une feuille si et seulement si son degré est égal à 0.k. Dans tous les arbres, le nombre de nœuds internes doit être inférieur à celui des nœuds feuilles.l. Un arbre est complet si et seulement si toutes ses feuilles se trouvent au même niveau.m.Chaque sous-arbre d’un arbre binaire complet est complet.n. Chaque sous-arbre d’un arbre binaire parfait est parfait. Reportez-vous à la section 10.5 pour

plus d’informations sur ce sujet.

9.3 Pour chacun des cinq arbres suivants, indiquez quels sont les nœuds des feuilles, les enfants dunœud C, la profondeur du nœud F, tous les nœuds du niveau 3, la hauteur de l’arbre et son ordre.

9.4 Pour l’arbre ci-contre, retrouvez les éléments suivants :

a. tous les ancêtres du nœud F ;b. tous les descendants du nœud F ;c. tous les nœuds du sous-arbre dont la racine est F ;d. tous les nœuds feuilles.

9.5 Combien de nœuds y a-t-il dans un arbre complet avec :

a. un ordre égal à 3 et une hauteur égale à 4 ?b. un ordre égal à 4 et une hauteur égale à 3 ?c. un ordre égal à 10 et une hauteur égale à 4 ?d. un ordre égal à 4 et une hauteur égale à 10 ?

9.6 Donnez l’ordre de visite de l’arbre présenté dans l’exemple 9.2si vous effectuez un parcours :

a. en largeur ;b. préfixe ;c. postfixe.

B

A

D

C

E F

H

G

JI

K L

Page 191: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 181

9.7 Quel parcours visite toujours :

a. la racine en premier ?

b. le nœud le plus à gauche en premier ?

c. la racine en dernier ?

d. le nœud le plus à droite en dernier ?

9.8 Quel ordre de parcours se comporte comme s’il s’agissait de lire un texte de gauche à droite,ligne par ligne ? Quel algorithme de parcours lit les colonnes verticalement de gauche à droite ?

9.9 Quel algorithme de parcours est utilisé dans l’arbre d’appel de l’exercice 4.31 ?

RÉPONSES¿RÉPONSES

9.1 Dans le cadre de l’arbre d’héritage Java :

a. La taille de l’arbre de Java 1.3 est égale à 1 730.

b. La classe Objet se trouve à la racine de l’arbre.

c. La classe final est un nœud feuille.

L M N

GF

A

B E

H J

G H J

K

K

H J K

GFGF

F

H J

C

D

A

B

E

C

O

L M N O

L M N

L M N

O

O

P Q

D

B EC D

B E

E

C D B C D

A

P Q

F

R

A

TT

K

G

M NL

H

O

J

S

Q

R S

T U

V W X

Y Z

A

P

P

Q R S

P Q R S

a. b. c.

d.

e.

Page 192: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

182 Arbres

9.2 a. Vraie.b. Fausse. La taille contient un élément en plus parce que la racine du sous-arbre se trouve dans

le sous-arbre, mais n’est pas un descendant d’elle-même.c. Vraie.d. Fausse.e. Vraie.f. Vraie.g. Fausse.h. Vraie.i. Vraie.j. Vraie.k. Fausse.l. Fausse.m.Vraie.n. Vraie.

9.3 a. Les nœuds feuilles sont L, M, N, H, O, P, Q, ; les enfants du nœud C sont G et H ; le nœud F aune profondeur de 2 ; les nœuds du niveau 3 sont L, M, N, O, P et Q ; la hauteur de l’arbre estde 3 ; l’ordre de l’arbre est égal à 4.

b. Les nœuds feuilles sont C, E, G, O, P, Q, R et S ; le nœud C n’a aucun enfant ; le nœud F a uneprofondeur de 2 ; les nœuds du niveau 3 sont L, M, N et O ; la hauteur de l’arbre est de 4 ;l’ordre de l’arbre est égal à 4.

c. Les nœuds feuilles sont C, E, G, J, L, N, O, P, W, Y et Z ; le nœud C n’a aucun enfant ; le nœudF a une profondeur de 2 ; les nœuds du niveau 3 sont H, J et K ; la hauteur de l’arbre est de 9 ;l’ordre de l’arbre est égal à 3.

d. Les nœuds feuilles sont G, H, K, L, N, O, P, Q, R, S et T ; le seul nœud enfant de C est E ; lenœud F a une profondeur de 3 ; les nœuds du niveau 3 sont F, G, H et J ; la hauteur de l’arbreest de 5 ; l’ordre de l’arbre est égal à 5.

e. Les nœuds feuilles sont D, E, L, N, P, Q, R, S et T ; le nœud C n’a aucun enfant ; le nœud F aune profondeur de 1 ; les nœuds du niveau 3 sont K, L, M, N et O ; la hauteur de l’arbre estde 4 ; l’ordre de l’arbre est égal à 5.

9.4 a. Les ancêtres de F sont C et A.b. Les descendants de F sont I, K et L.c. Les nœuds du sous-arbre dont la racine est F sont F, I, K et L.d. Les nœuds feuilles sont D, H, J, K et L.

9.5 a. (35 – 1)/2 = 121 nœudsb. (44 – 1)/3 = 85 nœudsc. (105 – 1)/9 = 11 111 nœudsd. (411 – 1)/3 = 1 398 101 nœuds

9.6 a. Parcours en largeur : A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P.b. Parcours préfixe : A, B, E, I, J, C, F, G, H, K, L, M, N, O, P, D.c. Parcours postfixe : I, J, E, B, F, G, K, L, M, N, O, P, H, C, D, A.

9.7 a. Les parcours en largeur et préfixe visitent toujours la racine en premier.b. Le parcours postfixe visite toujours le nœud le plus à gauche en premier.c. Le parcours postfixe visite toujours la racine en dernier.d. Le parcours préfixe visite toujours le nœud le plus à droite en dernier.

Page 193: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 183

9.8 Le parcours en largeur traverse l’arbre de gauche à droite et de haut en bas. Le parcours infixe litchaque colonne de gauche à droite (pour obtenir plus d’informations sur ce parcours, consultezl’exemple 10.6).

9.9 Le parcours infixe est utilisé dans l’exercice d’entraînement 4.31.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

9.1 Démontrez le théorème 9.1.

9.2 Démontrez le théorème 9.2.

9.3 Démontrez le corollaire 9.1.

9.4 Démontrez le corollaire 9.2.

9.5 Déduisez la formule permettant de calculer la longueur du chemin d’un arbre complet d’ordre det de hauteur h.

9.6 Supposons que deux joueurs de craps considèrent 3 comme valeur possible d’un point. Danscette version, le joueur Y gagne si son premier lancer fait sortir 2 ou 12. Utilisez un diagrammede transition pour analyser cette version du jeu et calculez la probabilité de voir X l’emporter.

9.7 Le paradoxe de St-Pétersbourg est une stratégie de pari qui semble garantir la victoire. Il peutêtre appliqué à tous les jeux binomiaux pour lesquels vous avez autant de chances de gagner quede perdre à chaque tentative, et pour lesquels le montant parié peut varier. Prenons l’exemple dujeu « pile ou face » : supposons qu’un parieur parie la somme qu’il veut et qu’il gagne la sommepariée si c’est face qui sort ou qu’il perde cette somme si c’est pile qui sort. La stratégie de St-Pétersbourg consiste à continuer à jouer jusqu’à ce que face sorte et à doubler le pari chaque foisque ce côté ne sort pas. Ainsi, si la séquence de lancer de la pièce est P, P, P, F et que le pari semonte à 1 euro, que le parieur perd, qu’il parie encore 2 euros, qu’il perd, puis 4 euros et qu’ilperd, et enfin 8 euros et qu’il gagne, il aura gagné –1 + –2 + –4 + 8 = 1 euro. Étant donné queface doit sortir à un moment ou à un autre, le parieur gagnera nécessairement 1 euro, quel quesoit le nombre de fois où il devra lancer la pièce.

Dessinez le diagramme de transition de cette stratégie en indiquant les gains du parieur à chaqueétape du jeu. Expliquez ensuite le défaut de cette stratégie.

9.8 Vous devez retrouver la fausse pièce parmi un ensemble de sept pièces apparemment identiques.La seule caractéristique qui les différencie est la légèreté de la fausse pièce par rapport auxautres. La seule solution consiste donc à tester le poids d’un sous-ensemble de pièces par rapportà un autre. Comment allez-vous sélectionner ces sous-ensembles (voir l’exemple 9.3) ?

SOLUTIONS¿SOLUTIONS

9.1 S’il n’existe aucun chemin reliant le nœud donné x à la racine de l’arbre, la définition est faussepuisque x doit être la racine d’un sous-arbre pour constituer un élément de l’arbre. S’il existe plusd’un chemin de x à la racine, x est un élément de plusieurs sous-arbres distincts, ce qui va égale-ment à l’encontre de la définition de l’arbre selon laquelle les sous-arbres doivent être disjoints.

Page 194: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

184 Arbres

9.2 Si l’arbre est vide, sa hauteur est h = –1 et son nombre de nœuds n = 0. Dans ce cas, la formuleest correcte :

n = (d(h) + 1 – 1)/(d – 1) = (d(–1) + 1 – 1)/(d – 1) = (d0 – 1)/(d – 1) = (1 – 1)/(d – 1) = 0

Si l’arbre est un singleton, sa hauteur est h = 0 et son nombre de nœuds n = 1. Dans ce cas, laformule suivante est toujours correcte :

n = (d(h) + 1 – 1)/(d – 1) = (d(0) + 1 – 1)/(d – 1) = (d – 1)/(d – 1) = 1

Supposons maintenant que cette formule soit correcte pour tout arbre complet de hauteur h–1,avec h ≥ 0. Supposons que T soit un arbre complet de hauteur h. Par définition, il est composéd’un nœud racine et d’un ensemble de sous-arbres. En outre, étant donné que T est complet, il esten fait composé d’une nœud racine et d’un ensemble de d sous-arbres ayant chacun une hauteurh–1. Ainsi, par hypothèse inductive, nous pouvons déduire que le nombre de nœuds de chaquesous-arbre est :

ns = (d(h – 1) + 1 – 1)/(d – 1) = (dh – 1)/(d – 1)

Le nombre total de nœuds dans T est donc égal à :

D’après le principe d’induction mathématique (reportez-vous à la section A.4), nous pouvons endéduire que la formule doit être correcte pour tous les arbres complets quelle que soit leur hau-teur.

9.3 Cette démonstration est purement algébrique :

9.4 Supposons que T soit un arbre d’ordre d et de hauteur h. T peut être intégré dans l’arbre complet

de mêmes degré et hauteur. Cet arbre complet est composé très exactement de nœuds,

c’est pourquoi son sous-arbre T est composé au plus de ce même nombre de nœuds.

9.5 La longueur du chemin d’un arbre complet de degré d et de hauteur h est calculée de la façon sui-vante :

n 1 d( ) nS( )+=

1 ddh 1–d 1–--------------

+=

d 1–d 1–------------ dh 1+ d–

d 1–---------------------+=

dh 1+ 1–d 1–

-----------------------=

ndh 1+ 1–

d 1–---------------------=

n d 1–( ) dh 1+ 1–=

dh 1+ n d 1–( ) 1+=

nd n– 1+=

h 1+ logd nd n– 1+( )=

h logd nd n– 1+( ) 1–=

dh 1+ 1–d 1–

---------------------

d

d 1–( )2------------------ hdh 1+ h 1+( )d– 1+[ ]

Page 195: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 185

Par exemple, la longueur de chemin de l’arbre complet situé dans la section 9.2 est égale à 102.

9.6 La version du craps avec 3 comme point est la suivante :

La probabilité de voir X gagner dans le cadre de cette version est de 0,5068 ou de 50,68 %.

9.7 L’analyse du diagramme arborescent du paradoxe de St-Pétersbourg est illustrée ci-après. Ledéfaut de cette stratégie est qu’il existe une possibilité distincte (c’est-à-dire une probabilité posi-tive) que pile sorte suffisamment de fois à la suite pour que le montant parié dépasse l’enjeu duparieur. Après n piles successifs, le parieur doit miser 2n euros. Par exemple, si pile sort 20 foisde suite, le pari suivant doit être d’un million d’euros !

X

Y4

1/32/3

X

Y5

2/53/5

X

Y6

5/116/11

X

Y8

5/116/11

X

Y9

2/53/5

X

Y10

1/32/3

7 ou 11, donc X

2 ou 12, donc Y

5/36

5/36

1/9

1/9

1/12

1/12

1/9

1/18

X

Y3

1/43/4

1/18

gagne

gagne

F

P F

P F

P F

P

–3€ – 4€ = –7€

–1€ – 2€ = –3€

–1€

–3 + 4€ = +1€

–1€ + 2€ = +1€

+1€

–7€ – 8€ = –15€

–7€ + 8€ = +1€

Pari 1€

Gain 1€

Gain 1€

Gain 1€

Gain 1€

Pari 2€

Pari 4€

Pari 8€

Page 196: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

186 Arbres

9.8 L’arbre décisionnel suivant illustre tous les résultats possibles après exécution de l’algorithmepermettant de résoudre le problème des 7 pièces :

X = a, b, cY = e, f, g

X = aY = c

X = eY = g

d

X < Y

X = Y

X > Y

aX < Y

c

X > Y

eX < Y

g

X > Y

bX = Y

fX = Y

Page 197: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 10

Arbres binaires

10.1 TERMINOLOGIELa définition récursive d’un arbre binaire est la suivante :

Définition : Un arbre binaire est soit un ensemble vide, soit un triplet T = (x, L, R), x étant un nœudet L et R étant des arbres binaires disjoints ne contenant pas x.

Le nœud x est la racine de l’arbre T, et les sous-arbresL et R sont respectivement le sous-arbre de gauche et lesous-arbre de droite de T. Si vous comparez cette défini-tion à celle des arbres ordonnés présentée dans la sec-tion 9.3, vous constaterez rapidement qu’un arbre binaireest simplement un arbre ordonné dont l’ordre est égal à 2.Attention cependant, un sous-arbre gauche vide est diffé-rent d’un sous-arbre droit vide (reportez-vous à l’exem-ple 9.6). C’est pourquoi les deux arbres binaires de lafigure ci-dessus ne sont pas identiques.

L’équivalent non récursif de notre première définition est le suivant :

Définition : Un arbre binaire est un arbre ordonné au sein duquel chaque nœud interne a un degréégal à 2.

Dans cette définition plus simple, les nœuds feuillessont considérés comme des nœuds factices dont l’uniquebut est de définir la structure de l’arbre. Dans le cadred’applications réelles, les nœuds internes contiennent desdonnées alors que les nœuds feuilles sont soit des nœudsvides identiques, soit un nœud vide seul, soit une simpleréférence null. Cette définition vous semblera peut-êtreinefficace et plus complexe de prime abord, mais elle esten fait beaucoup plus facile à implémenter.

a

b c

d

a

b c

d

* *

=

a

b c

d* * *

a

b c

d

Page 198: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

188 Arbres binaires

Sauf indication contraire, nous nous référerons dans ce livre à la première définition des arbres binai-res. C’est pourquoi certains nœuds internes pourront n’avoir qu’un seul enfant, à gauche ou bien à droite.

La définition des termes taille, chemin, longueur de chemin, profondeur de nœud, niveau, hauteur,nœud interne, ancêtre, descendant, sous-arbre et superarbre est identique qu’il s’agisse d’un arbrebinaire ou d’un arbre général. Vous pouvez donc vous reporter au chapitre précédent si vous souhaitezobtenir plus d’informations sur ces notions.

Exemple 10.1 Caractéristiques d’un arbre binaire

L’arbre binaire suivant a une taille égale à 10 et une hauteur égaleà 3. Le nœud a est sa racine et le chemin qui connecte le nœud hau nœud b a une longueur égale à 2. Le nœud b se trouve auniveau 1, le nœud h au niveau 3. b est un ancêtre de h et h est undescendant de b. La partie en grisé constitue le sous-arbre detaille 6 et de hauteur 2. Sa racine est le nœud b.

10.2 COMPTER LES ARBRES BINAIRES

Exemple 10.2 Tous les arbres binaires de taille 3

Il existe 5 types différents d’arbres binaires de taille n = 3 :

Quatre d’entre eux ont une hauteur égale à 2 et le cinquième a une hauteur égale à 1.

Exemple 10.3 Tous les arbres binaires de taille 4

Il existe 14 types différents d’arbres binaires de taille n = 4 :

Dix d’entre eux ont une hauteur égale à 3 et les quatre restants ont une hauteur égale à 2.

Exemple 10.4 Tous les arbres binaires de taille 5

Pour trouver tous les arbres binaires de taille 5, il vous suffit d’appliquer leur définition. Si t est unarbre binaire de taille 5, il doit être composé d’un nœud racine et de deux sous-arbres dont la sommedes tailles est égale à 4. Il existe 4 possibilités : le sous-arbre de gauche contient 4, 3, 2, 1 ou 0nœuds.Comptez d’abord tous les arbres binaires de taille 5 dont le sous-arbre de gauche est de taille 4.Comme nous l’avons déjà vu dans l’exemple 10.4, il existe 14 possibilités différentes pour ce sous-arbre gauche. Cependant, pour chacun de ces 14 choix, il n’y a aucune autre option étant donné quele sous-arbre droit doit être vide. Nous dénombrons donc 14 arbres binaires différents de taille 5 dontle sous-arbre gauche est de taille 4.Nous comptons ensuite tous les arbres binaires de taille 5 dont le sous-arbre gauche est de taille 3.Comme nous l’avons déjà vu dans l’exemple 10.3, il existe 5 possibilités différentes pour ce sous-arbre.

ji m

d

cb

a

h

fe

Page 199: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Arbres binaires complets 189

Cependant, pour chacun de ces 5 choix, il n’y a aucune autre option étant donné que le sous-arbredroit doit être un singleton. Nous dénombrons donc 5 arbres binaires différents de taille 5 dont lesous-arbre gauche est de taille 3.Puis, nous comptons tous les arbres binaires de taille 5 dont le sous-arbre gauche est de taille 2. Iln’existe que deux possibilités différentes pour ce sous-arbre. Cependant, pour chacun de ces deuxchoix, nous avons 2 fois 2 possibilités différentes pour le sous-arbre droit étant donné qu’il doit éga-lement être de taille 2. Nous obtenons donc 2 × 2 = 4 arbres binaires différents de taille 5 dont lesous-arbre gauche est de taille 2.En suivant le même raisonnement, nous pouvons déduire qu’il existe 5 arbres binaires différents detaille 5 dont le sous-arbre gauche est de taille 1, et qu’il existe 14 arbres binaires différents de taille5 dont le sous-arbre gauche est de taille 0. Le nombre total d’arbres binaires de taille 5 est donc de :14 + 5 + 4 + 5 + 14 = 42.

10.3 ARBRES BINAIRES COMPLETSUn arbre binaire est considéré comme complet si toutes ses feuilles sont au même niveau et que chacunde ses nœuds internes a deux enfants.

Exemple 10.5 Arbre binaire complet de hauteur 3

L’arbre illustré ci-contre est un arbre binaire complet dehauteur 3. Remarquez qu’il est composé de 15 nœuds,dont 7 nœuds internes et 8 feuilles.

Théorème 10.1 : l’arbre binaire complet de hauteur h a l =2h feuilles et m = 2h – 1 nœuds internes.Démonstration : l’arbre binaire complet de hauteur h = 0 estun nœud feuille seul ; il a donc n = 1 nœud, qui est une feuille.Par conséquent, étant donné que 2h + 1 – 1 = 20 + 1 – 1 = 21 – 1 = 2 – 1 = 1,2h – 1 = 20 – 1 = 1 – 1 = 0 et2h = 20 = 1, les formules sont correctes lorsque h = 0. De façon plus générale, supposons que h > 0 et que,hypothèse inductive, les formules soient vraies pour tous les arbres binaires complets de hauteur infé-rieure à h.Prenons ensuite un arbre binaire complet de hauteur h. Chaque sous-arbre a une hauteur h – 1 et nous luiappliquons les formules suivantes : lL = lR = 2h – 1 et mL = mR = 2h – 1 – 1. Il s’agit respectivement du nom-bre de feuilles du sous-arbre gauche, puis de celui du sous-arbre droit, du nombre de nœuds internes dansle sous-arbre gauche, puis dans le sous-arbre droit.Ensuite,

l = lL + lR = 2h – 1 + 2h – 1 = 2. 2h – 1 = 2h

etm = mL + mR + 1 = (2h – 1 – 1) + (2h – 1 – 1) + 1 = 2 · 2h – 1 – 1 = 2h – 1.

Par conséquent, d’après le deuxième principe d’induction mathématique, les formules doivent être vraiespour les arbres binaires complets de hauteur h ≥ 0. CQFD.

En regroupant simplement les formules de m et l, nous obtenons le premier corollaire.

Corollaire 10.1 : l’arbre binaire complet de hauteur h comprend un total de n = 2h + 1 – 1 nœuds.En résolvant la formule n = 2h + 1 – 1 pour h, nous obtenons le corollaire suivant.

Corollaire 10.2 : l’arbre binaire complet composé de n nœuds a une hauteur h = lg(n + 1) – 1.

lkji onm

d

cb

a

h

gfe

Page 200: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

190 Arbres binaires

Notez que la formule de ce corollaire est correcte même lorsque n = 0. En effet, l’arbre binaire videa une hauteur h = lg(n + 1) – 1 = lg(0 + 1) – 1 = lg(1) – 1 = 0 – 1 = -1.Le corollaire suivant applique le corollaire 10.1 et le fait qu’un arbre binaire complet de hauteur h aitplus de nœuds que n’importe quel autre arbre binaire de hauteur h.

Corollaire 10.3 : dans tout arbre binaire de hauteur h,

h + 1 ≤ n ≤ 2h + 1 – 1 et lg n ≤ h ≤ n – 1

correspondant au nombre de ses nœuds.

10.4 IDENTITÉ, ÉGALITÉ ET ISOMORPHISMEDans un ordinateur, deux objets sont identiques s’ils occupent le même espace mémoire et qu’ils ont parconséquent la même adresse. Dans le cadre du langage de programmation Java, cette approche de l’éga-lité est reflétée dans l’utilisation de l’opérateur d’égalité. Ainsi, si x et y sont des références aux objets,la condition (x == y) est vraie si, et seulement si, x et y font tous les deux références au même objet.

Cependant, en mathématiques, deux objets sont généralement considérés comme égaux s’ils ont lesmêmes valeurs. En java, cette distinction est gérée grâce à la méthode equals() définie dans la classeObject (reportez-vous à la section 3.4) et héritée par chaque classe. En général, les sous-classes la redé-finissent.

Exemple 10.6 Tester l’égalité des chaînes

public class Ex1006 static public void main(String[] args) String x = new String("ABCDE"); String y = new String("ABCDE"); System.out.println("x = " + x); System.out.println("y = " + y); System.out.println("(x == y) = " + (x == y)); System.out.println("x.equals(y) = " + x.equals(y));

Dans le cas présent, les deux objets x et y (ou, pour être plus précis, les deux objets qui sont référen-cés par les variables x et y) sont différents et occupent donc des espaces mémoire différents ; c’estpourquoi ils ne sont pas égaux dans le sens d’identiques. (x == y) est donc évaluée à false.Cependant, d’un point de vue mathématique, ils ont tous les deux la même valeur et sont par consé-quent égaux : x.equals(y) est donc évaluée à true.

Dans le contexte du langage de programmation Java, cette distinction entre l’égalité d’identité etl’égalité mathématique existe uniquement pour les variables de référence (c’est-à-dire pour les objets).Ainsi, pour toutes les variables de type primitif, l’opérateur d’égalité teste l’égalité mathématique.

Les structures de données comportent à la fois un contenu et une structure. C’est pourquoi il arriveque deux structures de données aient un contenu égal, c’est-à-dire identique, mais organisé différem-ment. Par exemple, deux tableaux peuvent contenir les mêmes numéros 22, 44 et 88, mais dans un ordredifférent.

x = ABCDEy = ABCDE(x == y) = falsex.equals(y) = true

Page 201: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Identité, égalité et isomorphisme 191

Exemple 10.7 Tester l’égalité des tableaux

import java.util.Arrays;public class Testing static public void main(String[] args) int[] x = 44, 22, 88 ; int[] y = 88, 44, 22 ; System.out.println("Arrays.equals(x,y) = " + Arrays.equals(x,y)); Arrays.sort(x); Arrays.sort(y); System.out.println("Arrays.equals(x,y) = " + Arrays.equals(x,y));

Deux structures de données sont dites isomorphes si elles présentent la même structure, c’est-à-diresi elles ont la même taille et qu’elles contiennent les mêmes données. Ces données peuvent ensuite êtreréorganisées de façon à rendre les deux structures égales. Cette opération est possible si et seulement siil existe une correspondance unilatérale entre les nœuds des deux structures pour préserver leur conti-guïté. La définition exacte de l’isomorphisme est la suivante :

Définition : Deux structures X et Y sont dites isomorphes s’il existe une fonction f qui fait correspon-dre chaque élément x de X à un élément unique y = f(x) de Y de façon à ce que chaque y de Y correspondeà un élément unique x de X, et que les deux éléments x1 et x2 soient adjacents dans X si et seulement sif(x) et f(x) sont adjacents dans Y.

Une fonction f ayant les propriétés décrites dans cette définition est qualifiée d’isomorphisme entreles deux structures.

Vous remarquerez que cette définition ne suppose pas que les deux structures contiennent les mêmesdonnées. En effet, l’isomorphisme ne dépend pas du contenu, mais il décrit uniquement la structure sous-jacente. Dans le cas des tableaux, des listes et des autres structures de données linéaires, les données peu-vent être réorganisées (triées) pour que les deux structures soient égales d’un point de vue mathématique.En revanche, avec des structures de données non linéaires telles que les arbres et les graphes, cetteréorganisation n’est pas toujours possible. Par exemple, si deux graphes ont la même taille et le mêmecontenu, mais qu’ils sont structurés différemment, il est impossible de réorganiser leur contenu de façonà le rendre égal. Ils ne sont donc pas isomorphes.

Exemple 10.8 Arbres isomorphes

Arrays.equals(x,y) = falseArrays.equals(x,y) = true

Arbre 1 Arbre 2 Arbre 3

gf

b

a

e

c d

t u

s

p

v

rq

e f

d

a

g

cb

Page 202: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

192 Arbres binaires

Les arbres 1 et 2 sont isomorphes, mais pas égaux. L’arbre 3 n’est pas isomorphe (et par conséquentpas égal) aux deux autres parce qu’il n’a que trois feuilles, tandis que les deux autres arbres sontcomposés de quatre feuilles.Cette distinction vous permet de déduire relativement aisément qu’il n’existe aucun isomorphismeentre l’arbre 1 et l’arbre 3.Étant donné qu’ils sont ordonnés les arbres 1 et 2 ne sont pas isomorphes parce que les sous-arbresles plus à gauche de leur racine ont une taille différente. En effet, le sous-arbre le plus à gauche del’arbre 1 est composé de trois nœuds (b, e et f), tandis que celui de l’arbre 2 n’est composé que dedeux nœuds (q et t). Là encore, cette distinction vous permet de déduire relativement aisément qu’iln’existe aucun isomorphisme entre l’arbre 1 et l’arbre 3.

Les arbres binaires sont des arbres ordonnés, c’est-à-dire que l’ordre des deux enfants de chaquenœud fait partie de la structure de l’arbre binaire lui-même.

Exemple 10.9 Arbres binaires non isomorphes

Dans le cas présent, l’arbre binaire 1 n’est pas isomorphe à l’arbre binaire 2, de la même façon queles arbres ordonnés de l’exemple 10.8 ne sont pas isomorphes. En effet, l’ordre de leurs sous-arbresn’est pas égal. Cependant, s’il s’agissait d’arbres non ordonnés, ils seraient isomorphes.Précisons que même dans un contexte d’arbres non ordonnés, aucun d’entre eux ne serait isomorpheà l’arbre 3 parce que celui-ci ne comporte que cinq nœuds tandis que les autres en ont six.

10.5 ARBRES BINAIRES PARFAITSUn arbre binaire parfait est soit un arbre binaire complet, soit un arbre complet comportant un segmentde feuilles manquantes à droite du niveau inférieur.

Exemple 10.10 Arbre binaire parfait de hauteur 3

Le premier arbre est parfait et le deuxième est un arbre binaire complet qui a été obtenu en ajoutant5 feuilles à droite du niveau 3.

Théorème 10.2 : dans un arbre binaire parfait de hauteur h,

h + 1 ≤ n ≤ 2h + 1 – 1 et h = lg n

n correspondant au nombre de ses nœuds.

fe

b

a

d

c

d e

c

a

f

b

d e

c

a

b

Arbre binaire 2 Arbre 3Arbre binaire 1

Page 203: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Arbres binaires parfaits 193

Exemple 10.11 Autres arbres binaires parfaits

Voici trois autres exemples d’arbres binaires parfaits :

Les arbres binaires parfaits sont importants parce qu’ils peuvent être simplement et naturellementstockés sous forme de tableaux. Pour cela, il vous suffit de procéder à un mappage naturel (reportez-vousà la section 12.2) qui affectera les numéros d’index aux nœuds de l’arbre par niveau, comme illustré dansla figure ci-après. L’index 1 est affecté à la racine puis, pour chaque nœud i, 2i est affecté à son enfantgauche et 2i + 1 à son enfant de droite (le cas échéant). Vous pouvez ainsi affecter un entier positif uniqueà chaque nœud. L’intérêt de ce mappage naturel est qu’il permet aisément de calculer les index detableau des enfants et du parent d’un nœud à partir de l’index de ce dernier.

Algorithme 10.1 Mappage naturel d’un arbre binaire parfait dans un tableau

Pour parcourir un arbre binaire parfait stocké dans un tableau à la suite de son mappage naturel :

1. Le parent du nœud stocké à l’emplacement i est stocké à l’emplacement i/2 ;

2. L’enfant gauche du nœud stocké à l’emplacement i est stocké à l’emplacement 2i ;

3. L’enfant droit du nœud stocké à l’emplacement i est stocké à l’emplacement 2i + 1.

Par exemple, le nœud e est stocké à l’index i = 5 dans le tableau ; son nœud parent b est stocké àl’index i/2 = 5/2 = 2 ; son nœud enfant gauche j est stocké à l’emplacement 2i = 25 = 10 ; et sonnœud enfant droit k est stocké à l’index 2i + 1 = 25 + 1 = 11.

La signification du terme « parfait » devrait maintenant être claire. La propriété qui définit la perfec-tion est précisément la fonction qui garantit que le mappage naturel stockera ses nœuds dans un tableausans trous.

Exemple 10.12 Arbre binaire incomplet

L’arbre binaire de l’exemple 10.1 est incomplet.Le mappage naturel de ses nœuds dans un tableau laisse des trous, comme illustré dans la figure sui-vante :

ea b dc f g h i j k l1 2 3 4 5 6 7 8 9 100 11 12

9 11108

4

2

1

lkji

d

cb

a

h

gfe

12

7

3

5 6

Page 204: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

194 Arbres binaires

Attention : certains auteurs utilisent l’expression « arbre binaire quasi-parfait » pour faire référenceà un arbre binaire parfait et « arbre binaire parfait » pour faire référence à un arbre binaire complet.

10.6 ALGORITHMES DE PARCOURS DES ARBRES BINAIRES

Les algorithmes de parcours utilisés pour les arbres généraux, c’est-à-dire le parcours préfixe, postfixe eten largeur (reportez-vous à la section 9.4), s’appliquent également aux arbres binaires. En outre, lesarbres binaires supportent un quatrième algorithme, celui du parcours infixe. Ces quatre algorithmes sontdéveloppés ci-après.

Algorithme 10.2 Parcours en largeur d’un arbre binaire

Pour parcourir un arbre binaire non vide :1. Initialisez une file d’attente.2. Mettez la racine en attente dans la file.3. Répétez les étapes 4 à 7 jusqu’à ce que la file d’attente soit vide.4. Retirez le nœud x de la file.5. Visitez x.6. Le cas échéant, mettez l’enfant gauche de x dans la file d’attente.7. Le cas échéant, mettez l’enfant droit de x dans la file d’attente.

Exemple 10.13 Parcours en largeur d’un arbre binaire

Le parcours en largeur d’un arbre binaire complet de hauteur 3 est le suivant :

Les nœuds sont visités dans l’ordre suivant : A, B, C, D, E, F, G, H, I, J, K, L, M, N, O.

jih

d

cb

a

g

fe

ea b dc f g h j1 2 3 4 5 6 7 8 9 100 12 1311

i

MLKJIH

FE

A

C

GD

ON

B

Page 205: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithmes de parcours des arbres binaires 195

Algorithme 10.3 Parcours préfixe d’un arbre binaire

Pour parcourir un arbre binaire non vide :

1. Visitez la racine.

2. Si le sous-arbre de gauche n’est pas vide, effectuez-en un parcours préfixe.

3. Si le sous-arbre de droite n’est pas vide, effectuez-en un parcours préfixe.

Exemple 10.14 Parcours préfixe d’un arbre binaire

La figure ci-après illustre le parcours préfixe d’un arbre binaire complet de hauteur 3.

Les nœuds sont visités dans l’ordre A, B, D, H, I, E, J, K, C, F, L, M, G, N, O, C.

Remarquez que le parcours préfixe d’un arbre binaire peut s’effectuer en cercles, en commençant parla racine et en visitant chaque nœud la première fois que vous le rencontrez sur votre gauche.

Algorithme 10.4 Parcours postfixe d’un arbre binaire

Pour parcourir un arbre binaire non vide :

1. Si le sous-arbre de gauche n’est pas vide, effectuez-en un parcours postfixe.

2. Si le sous-arbre de droite n’est pas vide, effectuez-en un parcours postfixe.

3. Visitez la racine.

Exemple 10.15 Parcours postfixe d’un arbre binaire

La figure ci-après illustre le parcours postfixe d’un arbre binaire complet de hauteur 3 :

J K L M N O

E F

A

B

D G

H I

C

MLKJIH

FE

A

C

GD

ON

B

MLKJIH

FE

A

C

GD

ON

B

Page 206: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

196 Arbres binaires

Les nœuds sont visités dans l’ordre suivant : H, I, D, J, K, E, B, L, M, F, N, O, G, C, A.

Le parcours préfixe visite la racine en premier lieu tandis que le parcours postfixe la visite en dernier.Il existe également une troisième possibilité qui consiste à visiter la racine entre le parcours des deuxsous-arbres. Il s’agit du parcours infixe.

Algorithme 10.5 Parcours infixe d’un arbre binaire

Pour parcourir un arbre binaire non vide :

1. Si le sous-arbre de gauche n’est pas vide, effectuez-en un parcours préfixe.

2. Visitez la racine.

3. Si le sous-arbre de droite n’est pas vide, effectuez-en un parcours préfixe.

Exemple 10.6 Parcours infixe d’un arbre binaire

La figure ci-après illustre le parcours infixe d’un arbre binaire complet de hauteur 3 :

Les nœuds sont visités dans l’ordre suivant : H, D, I, B, J, E, K, A, L, F, M, C, N, G, O.

10.7 ARBRES D’EXPRESSIONUne expression arithmétique telle que (5 – x)*y + 6/(x + z) est une combinaison d’opérateursarithmétiques (+, -, *, /, etc.), d’opérandes (5, x, y, 6, z, etc.), et de parenthèses permettant de rempla-cer la priorité des opérations. Chaque expression peut être représentée par un arbre binaire unique dontla structure est déterminée par la priorité des opérations dans l’expression. Ce type d’arbre est qualifiéd’arbre d’expression.

Exemple 10.17 Un arbre d’expression

Vous trouverez ci-après l’arbre représentant l’expression suivante :

(5 – x)*y + 6/(x + z)

L’algorithme récursif de construction d’un arbre d’expression est le suivant :

MLKJIH

FE

A

C

GD

ON

B

x

y

+

*

5

/

6 +

x z

Page 207: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Arbres d’expression 197

Algorithme 10.5 Construire un arbre d’expression

L’arbre d’expression d’une expression donnée peut être construit récursivement à l’aide des règlessuivantes :

1. L’arbre d’expression d’un seul opérande est un nœud racine unique le contenant.

2. Si E1 et E2 sont des expressions représentées par les arbres T1 et T2 et si op est un opérateur, alorsl’arbre de l’expression E1 op E2 est composé d’un nœud racine contenant op et des deux sous-arbres T1 et T2.

Une expression peut être représentée de trois façons selon l’algorithme de parcours utilisé. Le par-cours préfixe crée une représentation préfixe, le parcours infixe une représentation infixe et le parcourspostfixe une représentation postfixe de l’expression. Cette dernière porte également le nom de notationpolonaise inversée ou de notation RPN (pour Reverse Polish Notation), comme nous l’avons déjà vudans la section 6.2.

Exemple 10.18 Les trois représentations d’une expression

Les trois représentations de l’expression de l’exemple 10.17 sont les suivantes :

• Préfixe : +*-5xy/6+xz

• Infixe : 5-x*y+6/x+z

• Postfixe : 5x-y*6xz+/+

Généralement, la syntaxe d’une fonction utilise la représentation préfixe. L’expression de l’exemple10.17 pourrait ainsi être évaluée de la façon suivante :

somme(produit(différence(5, x), y), division(6, somme(x, z)))

Certaines calculatrices scientifiques proposent la notation RPN. Pour utiliser cette notation, vousdevez entrer les deux opérandes avant l’opérateur.

Une expression est évaluée en appliquant l’algorithme suivant à sa représentation postfixe.

Algorithme 10.7 Évaluer une expression à partir de sa représentation postfixe

Pour évaluer une expression dont la représentation est postfixe, vous devez analyser cette représenta-tion de gauche à droite :

1. Créez une pile d’opérandes.

2. Répétez les étapes 3 à 9 jusqu’à ce que vous atteigniez la fin de la représentation.

3. Lisez le symbole t suivant de la représentation.

4. S’il s’agit d’un opérande, poussez sa valeur dans la pile.

5. Si tel n’est pas le cas, répétez les étapes 6 à 9 :

6. Dépilez a.

7. Dépilez b.

8. Évaluez c = a t b.

9. Poussez c dans la pile.

10. Renvoyez l’élément supérieur de la pile.

Exemple 10.9 Évaluer une expression à partir de sa représentation postfixe

L’évaluation de l’expression de l’exemple 10.18 en remplaçant x par 2, y par 3 et z par 1 est la sui-vante :

Page 208: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

198 Arbres binaires

10.8 CLASSE BinaryTreeLa classe BinaryTree est destinée aux arbres binaires et implémente directement la définition récur-sive de ces derniers. Étant donné qu’elle étend la classe AbstractCollection (reportez-vous à la sec-tion 5.3), elle demeure cohérente avec le framework de collections Java :

import java.util.*;

public class BinaryTree extends java.util.AbstractCollection protected Object root; protected BinaryTree left, right, parent; protected int size;

public BinaryTree()

op a cb

x

y

z

2

3

1 op a cb 5

op a cb 52

op a cb 35 – 3

op a cb 33

op a cb 9 933 *

op a cb

69

12

op a cb

3

9

op a cb

69 op a cb

69

2

5 – 2 3

62 +

+

31

op a cb 9 112op a cb 92

/6 3 11

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

*5 x y– 6 x z + / +

3

2

Page 209: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe BinaryTree 199

public BinaryTree(Object root) this.root = root; size = 1;

public BinaryTree(Object root, BinaryTree left, BinaryTree right) this(root); if (left != null) this.left = left; left.parent = this; size += left.size(); if (right != null) this.right = right; right.parent = this; size += right.size();

public boolean equals(Object object) if (!(object instanceof BinaryTree)) return false; BinaryTree tree = (BinaryTree)object; return ( tree.root.equals(root) && tree.left.equals(left) && tree.right.equals(right) && tree.parent.equals(parent) && tree.size == size);

public int hashCode() return root.hashCode() + left.hashCode() + right.hashCode() + size;

public Iterator iterator() return new java.util.Iterator() // classe interne anonyme private boolean rootDone; private java.util.Iterator lit, rit; // itérateurs public boolean hasNext() return !rootDone || lit != null && lit.hasNext() || rit != null && rit.hasNext(); public Object next() if (rootDone) if (lit != null && lit.hasNext()) return lit.next(); if (rit != null && rit.hasNext()) return rit.next(); return null; if (left != null) lit = left.iterator(); if (right != null) rit = right.iterator(); rootDone = true; return root; public void remove() throw new UnsupportedOperationException(); ;

public int size() return size;

Page 210: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

200 Arbres binaires

L’objet BinaryTree peut être représenté de la façon suivante :Il est composé de cinq champs : une référence Object nom-

mée root, un champ int nommé size et trois références Bina-ryTree nommées left, right et parent. La définition récursive des arbres binaires supposeuniquement l’existence des champs root, left et right, les deux autres champs étant simplementinclus afin de simplifier une partie du code.

Le constructeur composé d’un paramètre crée un singleton, c’est-à-dire un arbre binaire avec un seulnœud. Le constructeur composé de trois paramètres crée un arbre binaire récursivement à partir des deuxautres arbres binaires qui peuvent être null.

La classe java.util.AbstractCollection requiert l’implémentation des quatre méthodesdéfinies ici.

La méthode equals(Object) remplace la version par défaut définie dans la classe Object(reportez-vous à la section 3.4). Pour qu’un Object soit égal à l’objet BinaryTree, il doit représenterune instance de la classe BinaryTree et tous ses champs doivent être égaux aux cinq champs corres-pondants de l’objet BinaryTree.

La méthode hashCode() remplace également la version par défaut définie dans la classe Object.Elle renvoie simplement un entier calculé à partir des codes de hachage de ses quatre objets membres etson champ de taille.

La méthode iterator() remplace la version par défaut (vide) définie dans la classe Abstract-Collection. Son rôle est de construire un objet itérateur capable de parcourir l’objet BinaryTree. Pourcela, elle crée sa propre classe interne anonyme Iterator à l’aide de la construction Java return new :

return new Iterator() // classe interne anonyme private boolean rootDone; private Iterator lit, rit; //itérateurs public boolean hasNext() ... public Object next() ... public void remove() ..;

Le corps de cette classe anonyme est défini entre les accolades qui suivent l’appel du constructeurIterator(). Comme vous pouvez le constater, ce bloc doit être suivi d’un point-virgule parce qu’ilconstitue en fait la fin d’une instruction return. Attention, bien que la construction complète ressembleà une définition de méthode, elle n’en est pas une : il s’agit en fait d’une définition de classe complèteinsérée dans une instruction return.

Pour renvoyer un objet Iterator, cette classe anonyme doit implémenter l’interface Iterator(reportez-vous à la section 5.5), d’où la nécessité de définir les trois méthodes suivantes :

public boolean hasNext() ...public Object next() ...public void remove() ..

Cette implémentation est récursive. La méthode hasNext() appelle les méthodes hasNext() desitérateurs sur les deux sous-arbres, et la méthode next() appelle les méthodes next() de ces deux ité-rateurs nommés lit et rit. L’autre variable locale est en fait un indicateur nommé rootDone qui saitsi l’objet racine a déjà été visité par l’itérateur.

La méthode hasNext() renvoie true, sauf si les trois composants de l’arbre ont été visités, àsavoir la racine, le sous-arbre de gauche et celui de droite. Pour cela, elle utilise les itérateurs lit et ritrécursivement.

La méthode next() utilise également les itérateurs lit et rit récursivement. Si la racine a déjà étévisitée, l’itérateur visite le nœud suivant dans le sous-arbre de gauche s’il y en a un, ou bien le nœud sui-vant du sous-arbre de droite s’il existe. En revanche, si la racine n’a pas encore été visitée, il s’agit dupremier appel de l’itérateur sur ce sous-arbre, c’est pourquoi la méthode initialise lit et rit, paramètrel’indicateur rootDone, puis renvoie la racine.

size

BinaryTree

root

right

parent

left1

"B"

Page 211: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe BinaryTree 201

La méthode remove() n’est pas implémentée parce qu’aucune méthode ne permet de supprimeraisément un nœud interne d’un arbre binaire.

Exemple 10.20 Tester la classe BinaryTree

Le pilote test de la classe BinaryTree définie précédemment est le suivant :

public class Ex1020 static public void main(String[] args) BinaryTree e = new BinaryTree("E"); BinaryTree g = new BinaryTree("G"); BinaryTree h = new BinaryTree("H"); BinaryTree i = new BinaryTree("I"); BinaryTree d = new BinaryTree("D",null,g); BinaryTree f = new BinaryTree("F",h,i); BinaryTree b = new BinaryTree("B",d,e); BinaryTree c = new BinaryTree("C",f,null); BinaryTree tree = new BinaryTree("A",b,c); System.out.println("arbre = " + tree);

Ce programme crée l’arbre binaire suivant, puis il appelle indirectement sa méthode toString()héritée de la classe AbstractCollection :

arbre = [A, B, D, G, E, C, F, H, I]

1 "H"1 "G" 1 "I"

1 "E"2 "D" 3 "F"

9 "A"

4 "B" 4 "C"

HG I

D

CB

A

FE

Page 212: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

202 Arbres binaires

Cette figure vous propose deux vues du même arbre, le plus grand étant plus détaillé et représentantchaque référence d’objet à l’aide d’une flèche.

Étant donné qu’elle étend la classe AbstractCollection, la classe BinaryTree hérite automa-tiquement des méthodes suivantes qui sont définies à l’aide des méthodes iterator() et size() :

public boolean isEmpty()public boolean contains(Object object)public Object[] toArray()public Object[] toArray(Object[] objects)public String toString()public boolean add(Object object)public boolean addAll(Collection collection)public void clear()public boolean containsAll(Collection collection)public boolean remove(Object object)public boolean removeAll(Collection collection)public boolean retainAll(Collection collection)

Cependant, les méthodes non constantes lanceront une exception UnsupportedOperation-Exception parce qu’elles appellent d’autres méthodes qui ne sont pas implémentées, à savoir add()et Iterator.remove().

Exemple 10.21 Tester la méthode contains() sur un arbre binaire

Cet exemple construit un arbre identique à celui de l’exemple 10.20, puis il teste la méthodecontains() sur l’arbre et ses sous-arbres :

public class Ex1020 static public void main(String[] args) BinaryTree e = new BinaryTree("E"); BinaryTree g = new BinaryTree("G"); BinaryTree h = new BinaryTree("H"); BinaryTree i = new BinaryTree("I"); BinaryTree d = new BinaryTree("D",null,g); BinaryTree f = new BinaryTree("F",h,i); BinaryTree b = new BinaryTree("B",d,e); BinaryTree c = new BinaryTree("C",f,null); BinaryTree a = new BinaryTree("A",b,c); System.out.println("a = " + a); System.out.println("a.contains(\"H\") = " + a.contains("H")); System.out.println("b = " + b); System.out.println("b.contains(\"H\") = " + b.contains("H")); System.out.println("c = " + c); System.out.println("c.contains(\"H\") = " + c.contains("H"));

Les sous-arbres b et c sont mis en évidence dans la figure ci-contre :

a = [A, B, D, G, E, C, F, H, I]a.contains("H") = trueb = [B, D, G, E]b.contains("H") = falsec = [C, F, H, I]c.contains("H") = true

HG I

D

CB

A

FE

cb

Page 213: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Implémenter les algorithmes de parcours 203

10.9 IMPLÉMENTER LES ALGORITHMES DE PARCOURSL’itérateur renvoyé par la méthode iterator() suit l’algorithme de parcours préfixe (voir l’algo-rithme 10.5) afin de parcourir l’arbre binaire. Dans le programme suivant, la classe BinaryTree a étémodifiée de façon à implémenter les quatre algorithmes de parcours d’un arbre binaire :

public class BinaryTree extends java.util.AbstractCollection private Object root; private BinaryTree left, right, parent; private int size; // Inclure le même code que celui de la section 10.8

abstract public class Iterator protected boolean rootDone; protected Iterator lit, rit; // itérateurs public boolean hasNext() return !rootDone || lit != null && lit.hasNext() || rit != null && rit.hasNext(); abstract public Object next();

public void remove() throw new UnsupportedOperationException();

public class PreOrder extends Iterator public PreOrder() if (left != null) lit = left.new PreOrder(); if (right != null) rit = right.new PreOrder();

public Object next() if (!rootDone) rootDone = true; return root; if (lit != null && lit.hasNext()) return lit.next(); if (rit != null && rit.hasNext()) return rit.next(); return null;

public class InOrder extends Iterator public InOrder() if (left != null) lit = left.new InOrder(); if (right != null) rit = right.new InOrder();

public Object next() if (lit != null && lit.hasNext()) return lit.next(); if (!rootDone) rootDone = true; return root; if (rit != null && rit.hasNext()) return rit.next(); return null;

Page 214: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

204 Arbres binaires

public class PostOrder extends Iterator

public PostOrder() if (left != null) lit = left.new PostOrder(); if (right != null) rit = right.new PostOrder();

public Object next() if (lit != null && lit.hasNext()) return lit.next(); if (rit != null && rit.hasNext()) return rit.next(); if (!rootDone) rootDone = true; return root; return null;

public class LevelOrder extends Iterator ArrayQueue queue = new ArrayQueue(); public boolean hasNext() return (!rootDone || !queue.isEmpty());

public Object next() if (!rootDone) if (left != null) queue.enqueue(left); if (right != null) queue.enqueue(right); rootDone = true; return root; if (!queue.isEmpty()) BinaryTree tree = (BinaryTree)queue.dequeue(); if (tree.left != null) queue.enqueue(tree.left); if (tree.right != null) queue.enqueue(tree.right); return tree.root; return null;

Tout d’abord, nous définissons une classe interne abstraite intitulée Iterator qui servira de classede base aux quatre classes d’itérateurs concrètes. Cette classe déclare les trois mêmes champs (roo-tDone, rit et lit) que la classe d’itérateur anonyme définie précédemment. Pour information, il n’yaura aucun conflit de nom entre cette classe Iterator et la classe java.util.Iterator hors de laclasse BinaryTree parce que vous y ferez référence sous la forme BinaryTree.Iterator (repor-tez-vous à l’exemple 10.22). D’ailleurs, nous avons déjà évité un conflit de nom au sein de la classeBinaryTree en incluant le nom du paquetage java.util comme composant de la classe java.util.Iterator.

Les méthodes hasNext() et remove() sont implémentées dans la classe abstraite Iterator de lamême façon que dans la classe d’itérateur anonyme. Cependant, la méthode next() est déclarée commeétant abstraite parce que chacun des algorithmes de parcours l’implémente différemment.

La classe PreOrder définit les itérateurs lit et rit comme itérateurs PreOrder dans sonconstructeur afin de s’assurer que le parcours récursif suivra l’algorithme de parcours préfixe. En effet,cet algorithme (reportez-vous à la section Algorithme 10.3) spécifie que la racine doit être visitée enpremier, puis que le même algorithme doit être appliqué récursivement au sous-arbre de gauche, puis ausous-arbre de droite. D’où l’utilité des trois instructions if :

Page 215: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Forêts 205

if (!rootDone) rootDone = true; return root;if (lit != null && lit.hasNext()) return lit.next();if (rit != null && rit.hasNext()) return rit.next();

Les classes PreOrder, InOrder et PostOrder se différencient uniquement par leur définition desitérateurs récursifs rit et lit dans les constructeurs, ainsi que par l’ordre des trois instructions if dansla méthode next(). En ce qui concerne la classe InOrder, la racine est visitée entre les deux parcoursrécursifs. Dans la classe PostOrder, la racine est visitée après les deux parcours récursifs. Comme vousavez pu le deviner, « Pre » signifie « avant », « in » signifie « entre » et « post » signifie « après ».

La classe de parcours en largeur LevelOrder est nettement différente des trois autres classes deparcours parce qu’elle utilise une file au lieu d’être récursive.

Exemple 10.22 Tester les algorithmes de parcours

public class Ex1022 static public void main(String[] args) BinaryTree e = new BinaryTree("E"); BinaryTree g = new BinaryTree("G"); BinaryTree h = new BinaryTree("H"); BinaryTree i = new BinaryTree("I"); BinaryTree d = new BinaryTree("D",null,g); BinaryTree f = new BinaryTree("F",h,i); BinaryTree b = new BinaryTree("B",d,e); BinaryTree c = new BinaryTree("C",f,null); BinaryTree tree = new BinaryTree("A",b,c); System.out.println("arbre = " + tree); BinaryTree.Iterator it; System.out.print("Parcours préfixe : "); for (it = tree.new PreOrder(); it.hasNext(); ) System.out.print(it.next() + " "); System.out.print("\nParcours infixe : "); for (it = tree.new InOrder(); it.hasNext(); ) System.out.print(it.next() + " "); System.out.print("\nParcours posfixe : "); for (it = tree.new PostOrder(); it.hasNext(); ) System.out.print(it.next() + " "); System.out.print("\nParcours en largeur : "); for (it = tree.new LevelOrder(); it.hasNext(); ) System.out.print(it.next() + " "); System.out.println();

10.10 FORÊTSUne forêt est une liste d’arbres.

arbre = [A, B, D, G, E, C, F, H, I]Parcours préfixe : A B D G E C F H IParcours infixe : D G B E A H F I CParcours postfixe : G D E B H I F C AParcours en largeur : A B C D E F G H I

Page 216: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

206 Arbres binaires

Exemple 10.23 Une forêt

La figure suivante illustre le cas d’une forêt constituée de trois arbres :

L’algorithme suivant indique comment une forêt peut être représentée par un seul arbre binaire.

Algorithme 10.7 Mappage naturel d’une forêt dans un arbre binaire

1. Mappez la racine du premier arbre dans celle de l’arbre binaire.

2. Si le nœud X est mappé dans le nœud X’ et que le nœud Y est le premier enfant de X, mappez Ydans l’enfant gauche de X’.

3. Si le nœud X est mappé dans X’ et que le nœud Z est le frère de X, mappez Z dans l’enfant droitde X’. Les racines des arbres sont considérées comme des frères.

Exemple 10.24 Mapper une forêt dans un arbre binaire

Le mappage de la forêt illustrée dans l’exemple 10.23 est le suivant :

Regardons plus précisément les nœuds C, F et D. Dans la forêt originale, l’enfant le plus ancien de Cétait F et son frère suivant était D ; dans l’arbre binaire obtenu, C a donc F pour enfant gauche et Dpour enfant droit.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

10.1 Combien de nœuds feuilles dénombre-t-on dans un arbre binaire complet de hauteur h = 3 ?

10.2 Combien de nœuds internes dénombre-t-on dans un arbre binaire complet de hauteur h = 3 ?

LK M

E F

A

DB

HG J

C

TS

Q

RPO

N

K M

E

A

B

G J

C

S

Q

RO

N

K M

E

A

B

G J

C

S

Q

RO

N

D P D P

F H T F H T

LL

Page 217: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 207

10.3 Combien de nœuds dénombre-t-on dans un arbre binaire complet de hauteur h = 3 ?

10.4 Combien de nœuds feuilles dénombre-t-on dans un arbre binaire complet de hauteur h = 9 ?

10.5 Combien de nœuds internes dénombre-t-on dans un arbre binaire complet de hauteur h = 9 ?

10.6 Combien de nœuds dénombre-t-on dans un arbre binaire complet de hauteur h = 9 ?

10.7 Quel est l’intervalle de hauteurs possibles pour un arbre binaire avec n = 100 nœuds ?

10.8 Pourquoi est-il impossible d’effectuer un parcours infixe d’un arbre général ?

10.9 Indiquez si les affirmations suivantes sont vraies ou fausses :

a. Si toutes les feuilles d’un arbre binaire se trouvent au même niveau, cet arbre est complet.

b. Si un arbre binaire est composé de n nœuds et a une hauteur h, alors h ≥ lg n.

c. Un arbre binaire ne peut pas avoir plus de 2d nœuds au niveau de la profondeur d.

d. Si chaque sous-arbre correct d’un arbre binaire est complet, l’arbre lui-même doit égalementêtre complet.

RÉPONSES¿RÉPONSES

10.1 Un arbre binaire complet de hauteur 3 a l = 23 = 8 feuilles.

10.2 Un arbre binaire complet de hauteur 3 a m = 23 – 1 = 7 nœuds internes.

10.3 Un arbre binaire complet de hauteur 3 a n = 23+1 – 1 = 24 – 1 = 16 – 1 = 15 nœuds.

10.4 Un arbre binaire complet de hauteur 9 a l = 29 = 512 feuilles.

10.5 Un arbre binaire complet de hauteur 9 a m = 29 – 1 = 512 – 1 = 511 nœuds internes.

10.6 Un arbre binaire complet de hauteur 9 a n = 29+1 – 1 = 210 – 1 = 1024 – 1 = 1023 nœuds.

10.7 D’après le corollaire 10.3, dans tout arbre binaire :

lg n ≤ h ≤ n – 1

Ainsi, dans un arbre binaire composé de 100 nœuds :

lg 100 ≤ h ≤ 100 – 1 = 99

Étant donné que :

lg 100 = (log 100)/(log 2) = 6,6 = 6

la hauteur doit être située entre 6 et 99, 6 ≤ h ≤ 99.

10.8 L’algorithme de parcours infixe des arbres binaires visite récursivement la racine entre le par-cours du sous-arbre gauche et celui du sous-arbre droit. Cela suppose donc l’existence de deuxsous-arbres (même vides) à chaque nœud (non vide). Or, dans un arbre général, un nœud peutavoir un nombre illimité de sous-arbres ; il n’existe donc aucune méthode algorithmique qui per-mette de généraliser le parcours infixe.

10.9 a. Vraie.

b. Vraie.

c. Vraie.

d. Fausse.

Page 218: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

208 Arbres binaires

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

10.1 Pour chacun des arbres binaires suivants, dessinez son équivalent en fonction de la deuxièmedéfinition selon laquelle chaque nœud interne doit avoir deux enfants :

10.2 Donnez l’ordre de visite de cet arbre binaire selon les parcours suivants :

a. parcours en largeur ;b. parcours préfixe ;c. parcours infixe ;d. parcours postfixe.

10.3 Donnez l’ordre de visite de l’arbre binaire de taille 10 figurant dansl’exemple 10.2 selon les parcours suivants :

a. parcours en largeur ;b. parcours préfixe ;c. parcours infixe ;d. parcours postfixe.

10.4 Donnez l’ordre de visite de cet arbre binaire selonles parcours suivants :

a. parcours en largeur ;b. parcours préfixe ;c. parcours infixe ;d. parcours postfixe.

10.5 Dessinez le tableau obtenu à la suite d’un map-page naturel destiné à stocker l’arbre binaire del’exercice 10.1.

h

d

cb

a

g

fe

d

b

a

cd

cb

a

e

ji on

d

cb

a

gfe

a. b.

c. d.

B

A

D

C

E F G

H JI K

K L

M O

E F

A

B

D

G J

C

H

N

Page 219: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 209

10.6 Dessinez le tableau obtenu à la suite d’un mappage naturel destiné à stocker l’arbre binaire del’exemple 10.1.

10.7 Dessinez le tableau obtenu à la suite d’un mappage naturel destiné à stocker l’arbre binaire del’exercice 10.4.

10.8 Si les nœuds d’un arbre binaire sont numérotés en fonction de leur mappage naturel et que l’opé-ration de visite imprime le numéro du nœud, quel algorithme de parcours imprimera les nombresdans l’ordre ?

10.9 Dessinez l’arbre d’expression de a * (b + c) * (d * e + f).

10.10 Écrivez les représentations préfixe et postfixe des expressions de l’exercice 10.8.

10.11 Dessinez l’arbre d’expression de chacune des expressions préfixes de l’exercice 6.2.

10.12 Dessinez l’arbre d’expression de chacune des expressions infixes de l’exercice 6.4.

10.13 Dessinez l’arbre d’expression de chacune des expressions postfixes de l’exercice 6.6.

10.14 Quelles sont les limites du nombre n de nœuds pour un arbre binaire de hauteur 4 ?

10.15 Quelles sont les limites de la hauteur h d’un arbre binaire composé de 7 nœuds ?

10.16 Quelle est la forme de l’arbre binaire le plus haut pour un nombre donné de nœuds ?

10.17 Quelle est la forme de l’arbre binaire le plus bas (c’est-à-dire avec la hauteur la plus petite) pourun nombre donné de nœuds ?

10.18 Vérifiez la définition récursive des arbres binaires (expliquée en début dechapitre) pour l’arbre binaire ci-contre :

10.19 Dessinez les 42 arbres binaires de taille n = 5.

10.20 Combien y a-t-il d’arbres binaires différents de taille n = 6 ?

10.21 Déduisez une relation de récurrence pour le nombre f(n) d’arbres binairesde taille n.

10.22 Démontrez que, pour tout n ≤ 8, la fonction f(n) calculée dans l’exercice 10.21 crée la mêmeséquence que la formule explicite suivante :

Par exemple,

10.23 Démontrez le corollaire 10.3.

10.24 Démontrez le théorème 10.2.

10.25 Dessinez la forêt représentée par l’arbre binaire ci-contre :

10.26 Déduisez une formule explicite pour le nombre f(h) d’arbresbinaires parfaits de hauteur h.

10.27 Déduisez une formule explicite pour le nombre f(h) d’arbresbinaires complets de hauteur h.

10.28 Démontrez que chaque sous-arbre d’un arbre binaire complet est également complet.

10.29 Démontrez que chaque sous-arbre d’un arbre binaire parfait est également parfait.

C E F

B D

A

( )( )f n( )

2nn

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

n! n 1+( )!------------------------ 2n( ) 2n 1 2n 2–( )… 2n 3+( ) 2n 2+( )

n( ) n 1–( ) n 2–( ) n 3– … 2( ) 1( )----------------------------------------------------------------------------------------------------= = –

f 4( )

84

5-------- 8!

4!5!---------- 8( ) 7( ) 6( )

4( ) 3( ) 2( ) 1( )-------------------------------- 8( ) 7( )

4---------------- 14= = = = =

F G

D E

A

B C

Page 220: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

210 Arbres binaires

Implémentez chacune des méthodes suivantes dans la classe BinaryTree :

10.30 • public int leaves();• // renvoie le nombre de feuilles dans l’arbre

10.31 • public int height();• // renvoie la hauteur de l’arbre

10.32 • public int level(Object object);• // renvoie -1 si l’objet donné n’est pas dans l’arbre;• // sinon, renvoie son niveau dans l’arbre;

10.33 • public void reflect();• // intervertit les enfants de chaque nœud de l’arbre

10.34 • public void defoliate();• // supprime toutes les feuilles de l’arbre

SOLUTIONS¿SOLUTIONS

10.1 Les arbres correspondant à la deuxième définition d’un arbre binaire sont les suivants :

10.2 a. Parcours en largeur : A, B, C, D, E, F, G, H, I, J, K.

b. Parcours préfixe : A, B, D, E, H, I, C, F, J, G, K.

c. Parcours infixe : D, B, H, E, I, A, F, J, C, G, K.

d. Parcours postfixe : D, H, I, E, B, J, F, K, G, C, A.

* *

*

*

*

* *

* *h

d

cb

a

g

fe

*

*

* *

*

d

b

a

c

* *

* *

* *

d

cb

a

e

*

* * * *

* * *

* * * *

ji on

d

cb

a

gfe

a. b.

c. d.

Page 221: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 211

10.3 a. Parcours en largeur : A, B, C, D, E, F, H, I, J,M.

b. Parcours préfixe : A, B, D, H, I, E, J, C, F, M.

c. Parcours infixe : H, D, I, B, J, E, A, F, M, C.

d. Parcours postfixe : H, I, D, J, E, B, M, F, C, A.

10.4 a. Parcours en largeur : A, B, C, D, E, F, G, H, J, K, L, M, N, O.

b. Parcours préfixe : A, B, D, G, M, H, C, E, J, N, F, K, O, L.

c. Parcours infixe : G, M, D, H, B, A, N, J, E, C, K, O, F, L.

d. Parcours postfixe : M, G, H, D, B, N, J, E, O, K, L, F, C, A.

10.5 La figure suivante illustre le mappage naturel de l’arbre binaire donné :

10.6 La figure suivante illustre le mappage naturel de l’arbre binaire de l’exemple 10.1 :

10.7 La figure suivante illustre le mappage naturel de l’arbre binaire donné :

10.8 Le parcours en largeur imprime les nombres du mappage naturel dans l’ordre.

10.9 L’arbre d’expression est le suivant :

10.10 La représentation préfixe est *a*+bc+*def, et la représentation postfixe est abc+de*f+**.

A B DC F G H K1 2 3 4 5 6 7 8 9 100 11 12

E14 1513

JI

A B DC F H M1 2 3 4 5 6 7 8 9 100 11 12

E13

JI

A B DC F G H K L M1 2 3 4 5 6 7 8 9 100 11 12

E23 24 2514 15 16 17 18 19 20 21 2213 2926 27 28

J N O

d

f

+

a *

*

*

e

c

+

b

Page 222: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

212 Arbres binaires

10.11 Les arbres d’expression des expressions préfixes de l’exercice 6.2 sont les suivants :

10.12 Les arbres d’expression des expressions infixes de l’exercice 6.4 sont les suivants :

10.13 Les arbres d’expression des expressions postfixes de l’exercice 6.6 sont les suivants :

10.14 Dans un arbre binaire de hauteur h = 4, 5 n 31.

10.15 Dans un arbre binaire avec n = 7 nœuds, 2 h 6.

10.16 Pour un nombre donné de nœuds, l’arbre binaire le plus haut est une séquence linéaire.

10.17 Pour un nombre donné de nœuds, l’arbre binaire le plus bas est un arbre binaire parfait.

10.18 Pour vérifier la définition récursive d’un arbre donné, nous remarquons d’abord que les feuillesC, E et F sont des arbres binaires parce que chaque singleton est conforme à la définition récur-sive des arbres binaires (ses sous-arbres de gauche et de droite sont tous les deux vides). Ils’ensuit que le sous-arbre de racine B est un arbre binaire parce qu’il s’agit d’un triplet (X, L, R),avec X = B, L = Ø et R = C. De la même façon, il s’ensuit que le sous-arbre de racine D est unarbre binaire parce qu’il s’agit d’un triplet (X, L, R), avec X = D, L = E et R = F. En dernier lieu,

a

c

+

e/

-

*

d

b

a

c

*

/

/

b

a

/

+

*

d e

e

+

d

-c

b

a. b. c.

a

+

-

b

/

/

b

*

*

cd e d e

/

*

d e

-

b c

+c

a

a /

a. b. c.

-

a

/

/

/

d e

-

b

*

+

c d e

/c

b

c.

a

/

+

+

e

b a b

a

a. b.

--

Page 223: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 213

il s’ensuit que l’arbre entier est conforme à la définition récursive parce qu’il s’agit d’un triplet(X, L, R), avec X = A, L comme arbre binaire de racine B et L comme arbre binaire de racine D.

10.19 Les 42 arbres binaires de taille n = 5 sont les suivants :

10.20 Il existe 132 arbres binaires différents de taille 6 :

142 + 114 + 25 + 52 + 141 + 142 = 132

10.21 Un arbre binaire non vide est composé d’une racine X, d’un sous-arbre gauche L et d’un sous-arbre droit R. Supposons que n soit la taille de l’arbre binaire, que nL = |L| = la taille de L et quenR = |R| = la taille de R. Ensuite, n = 1 + nL + nR. Il existe donc n valeurs différentes possiblespour la paire (nL, nR) : (0, n – 1), (1, n – 2), …, (n – 1,0). Par exemple, si n = 6 (comme dansl’exercice 10.16), les seules possibilités sont (0, 5), (1, 4), (2, 3), (3, 2), (4, 1) ou (5, 0). Dans lecas de (0, n – 1), L est vide et |R| = n – 1 ; il existe donc f(0) x f(n – 1) arbres binaires différents.Dans le cas de (1, n – 2), L est un singleton et |R| = n – 2 ; il existe donc f(1) x f(n – 2) arbresbinaires différents. Le même principe peut être appliqué à chaque cas. Le nombre total d’arbresbinaires différents de taille est n est donc le suivant :

f(n) = 1f(n – 1) + 1f(n – 2) + 2f(n – 3) + 5f(n – 4) +…+ f(i – 1) f(n – i) +…+ f(n – 1)1

Cette formule condensée est la suivante :

f n( ) f i 1–( ) f n 1–( )⋅i 1=

n

∑=

Page 224: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

214 Arbres binaires

10.22 Il s’agit des nombres catalans :

10.23 Pour une hauteur donnée h > 0, l’arbre binaire ayant le plus de nœuds est l’arbre binaire complet.Le corollaire 10.2 établit que ce nombre est n = 2h + 1 – 1. Par conséquent, dans tout arbre binairede hauteur h, le nombre n de nœuds doit être conforme à n 2h + 1 – 1. Toujours pour une hauteurh donnée, l’arbre binaire ayant le moins de nœuds est celui dans lequel chaque nœud interne a unseul enfant ; cet arbre linéaire a n = h + 1 nœuds parce que chaque nœud a très exactement unenfant, à l’exception de la feuille seule. Nous pouvons en déduire que, dans tout arbre binaire dehauteur h, le nombre n de nœuds doit être conforme à n ≥ h + 1. La deuxième paire d’inégalitéssuit la première en résolvant h.

10.24 Supposons que T soit un arbre parfait de hauteur h et de taille n. Supposons que T1 soit le sous-arbre complet obtenu en supprimant le niveau le plus bas des feuilles de T, et que T2 soit le super-arbre complet obtenu en remplissant le niveau le plus bas des feuilles de T. Ensuite, T1 a une hau-teur h – 1 et T2 une hauteur h. D’après le corollaire 10.1, n1 = |T1| = 2h – 1 et n2 = |T2| = 2h + 1 – 1.Maintenant, n1 < n n2, donc 2h – 1 = n1 < n n2 = 2h + 1 – 1 et 2h = n1 + 1 n n2 < n2 + 1 = 2h + 1.Ainsi, h ≤ lg n < h + 1, et donc h = lg n.

10.25 La forêt créée par l’arbre binaire donné est obtenue en inversant la mappe naturelle de la façonsuivante :

10.26 f(h) = 1

10.27 f(h) = h

10.28 Théorème : chaque sous-arbre d’un arbre binaire complet est également complet.

n n + 1

0 1 1 1 1

1 2 2 1 1·1 = 1

2 6 3 2 1·1+1·1 = 2

3 20 4 5 1·2+1·1+2·1 = 5

4 70 5 14 1·5+1·2+2·1+5·1 = 14

5 252 6 42 1·14+1·5+2·2+5·1+14·1 = 42

6 924 7 132 1·42+1·14+2·5+5·2+14·1+42·1 = 32

7 3432 8 429 1·132+1·42+2·14+5·5+14·2+42·1+132·1 = 429

8 12 870 9 1430 1·149+1·132+2·42+5·14+14·5+42·2+132·1+429·1 = 1430

2nn

2nn

n 1+( )⁄ f i 1–( )∑ f n i–( )⋅

F G

D E

A

B C

F

A

B GD E

C A

B G

F

D E

C

Page 225: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 215

Démonstration : supposons que T soit un arbre de recherche binaire et que S soit un sous-arbrede T. Supposons également que x soit un élément de S et que L et R soient les sous-arbres gaucheet droit de x dans S. Ensuite, étant donné que S est un sous-arbre de T, x est également un élémentde T, et L et R sont les sous-arbres gauche et droit de x dans T. Donc, y ≤ x ≤ z pour chaque y ∈ Let chaque z ∈ R parce que T a la propriété des arbres binaires de recherche. Par conséquent, S acette même propriété.

10.29 Théorème : chaque sous-arbre d’un arbre binaire parfait est également parfait.Démonstration : supposons que T soit un arbre de recherche binaire et que S soit un sous-arbrede T. Supposons également que x soit un élément de S et que L et R soient les sous-arbres gaucheet droit de x dans S. Ensuite, étant donné que S est un sous-arbre de T, x est également un élémentde T, et L et R sont les sous-arbres gauche et droit de x dans T. Donc, y ≤ x ≤ z pour chaque y ∈ Let chaque z ∈ R parce que T a la propriété des arbres binaires de recherche. Par conséquent, S acette même propriété.

10.30 • public int leaves()• if (this == null) return 0;• int leftLeaves = (left==null ? 0 : left.leaves());• int rightLeaves = (right==null ? 0 : right.leaves());• return leftLeaves + rightLeaves;•

10.31 • public int height()• if (this == null) return -1;• int leftHeight = (left==null ? -1 : left.height());• int rightHeight = (right==null ? -1 : right.height());• return 1 + (leftHeight<rightHeight ? rightHeight : leftHeight);•

10.32 • public int level(Object object)• if (this == null) return -1;• if (object == root) return 0;• int leftLevel = (left==null ? -1 : left.level(object));• int rightLevel = (right==null ? -1 : right.level(object));• if (leftLevel<0 && rightLevel<0) return -1;• return 1 + (leftLevel<rightLevel ? rightLevel : leftLevel);•

10.33 • public void reflect()• if (this == null) return;• if (left != null) left.reflect();• if (right != null) right.reflect();• BinaryTree temp=left;• left = right;• right = temp;•

10.34 • public void defoliate()• if (this == null) return;• if (left == null && right == null)• root = null;• return;• • if (left != null && left.left==null && left.right==null)• left = null;• else left.defoliate();• if (right != null && right.left==null && right.right==null)• right = null;• else right.defoliate();•

Page 226: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie
Page 227: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 11

Arbres de recherche

Les structures arborescentes sont utilisées pour stocker les données parce que leur organisation permetd’accéder plus efficacement à ces dernières. Quant aux arbres de recherche, ils gèrent les données qu’ilscontiennent de façon ordonnée.

11.1 ARBRES DE RECHERCHE MULTIDIRECTIONNELSLa définition récursive d’un arbre de recherche multidirectionnel est la suivante :

Définition : Un arbre de recherche multidirectionnel d’ordre m est soit un ensemble vide, soit unepaire (k, S) dans laquelle le premier composant est une séquence k = (k1, k2, …, kn – 1) de n – 1 clés et ledeuxième composant une séquence S = (S0, S1, S2, …, Sn – 1) de n arbres de recherche multidirectionnelsd’ordre m, avec 2 ≤ n ≤ m et s0 ≤ k1 ≤ s1 ≤ …≤ kn – 1 ≤ sn – 1 pour chaque si ∈ Si.

Cette définition est similaire à celle de l’arbre générale que nous avons déjà vue dans la section 9.1.Un arbre de recherche multidirectionnel d’ordre m peut être considéré comme un arbre d’ordre m consti-tué de séquences de clés ayant la propriété d’ordre décrite ci-dessus.

Exemple 11.1 Arbre de recherche à 5 directions

23 2513 19 39 45 5548 50

27 33 38 47

65 6761 64 73 75 8278 81

59 60 70

87

77 85

57 72

Page 228: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

218 Arbres de recherche

L’arbre multidirectionnel illustré ci-après est d’ordre m = 5. Il est composé de trois nœuds internesde degré 5 (chacun d’entre eux comportant quatre clés), de trois nœuds internes de degré 4 (chacund’entre eux comportant 3 clés), de quatre nœuds internes de degré 3 (chacun d’entre eux comportantdeux clés) et d’un nœud interne de degré 2 (comportant 1 clé).Le nœud racine se compose de deux clés et de trois enfants. Les quatre clés du premier enfant sontinférieures à k1 = 57 et les trois clés du deuxième enfant sont situées entre k1 = 57 et k2 = 72. Lesdeux clés du troisième enfant sont supérieures à k1 = 72. En fait, les 13 clés du premier sous-arbresont inférieures à 57, les sept clés du deuxième sous-arbre sont situées entre 57 et 72 et les huit clésdu troisième sous-arbre sont supérieures à 72.

L’arbre multidirectionnel fait partie de la catégorie des arbres de recherche parce qu’il joue le rôled’un index multiniveau permettant d’effectuer des recherches dans des listes de taille importante. Pourtrouver une valeur de clé, commencez par la racine et descendez l’arbre jusqu’à ce que vous trouviez laclé ou que vous atteigniez une feuille. Effectuez une recherche binaire de la clé à chaque nœud. Si vousne la trouvez pas dans un nœud, la recherche s’arrête entre deux valeurs de clés adjacentes (avec k0 = –∞et kn = ∞). Vous devez alors suivre le lien reliant ces deux clés et le prochain nœud. Si vous atteignez unefeuille, c’est que la clé ne se trouve pas dans l’arbre.

Supposons que vous recherchiez la valeur de clé 66. Vous commencerez à la racine de l’arbre, puisvous suivrez le lien central (parce que 57 ≤ 66 < 72) jusqu’au nœud central composé de 3 clés. Vous sui-vrez ensuite le troisième lien (parce que 60 ≤ 66 < 70) jusqu’au nœud du bas composé de 4 clés. Voussuivrez ensuite le troisième lien (parce que 65 ≤ 66 < 67) jusqu’au nœud feuille. Vous serez alors enmesure de conclure que la clé 66 ne se trouve pas dans l’arbre.

Pour insérer une clé dans un arbre de recherche multidirectionnel, vous devez d’abord appliquerl’algorithme de recherche. Si la recherche se termine au niveau d’un nœud feuille, les deux clés qui enca-drent le nœud parent recherchent l’emplacement d’insertion de la nouvelle clé. C’est pourquoi vousdevez insérer cette dernière dans le nœud interne, entre les deux clés qui l’encadrent. Si cette insertionporte à m le nombre de clés, c’est-à-dire si la limite de m – 1 clés par nœud est dépassée en raison de sonajout, fractionnez le nœud en deux parties après avoir remonté la clé centrale jusqu’à son nœud parent. Sice déplacement porte à m le nombre de clés du nœud parent, répétez l’opération de fractionnementjusqu’à temps que vous atteigniez la racine si nécessaire. Le fractionnement de la racine crée une nou-velle racine, ce qui augmente la hauteur de l’arbre d’un niveau.

Exemple 11.2 Insérer des éléments dans un arbre à 5 directions

Pour insérer 66 dans l’arbre de recherche de l’exemple 11.1, effectuez d’abord une recherche en sui-vant les étapes que nous venons de détailler. Vous atteindrez alors le nœud feuille marqué d’un Xdans la figure suivante :

23 2513 19 39 45 5548 50

27 33 38 47

65 6761 64 73 75 8278 81

59 60 70

87

77 85

57 72

X

Page 229: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Arbres équilibrés ou arbres-B 219

Insérez la nouvelle clé 66 dans le dernier nœud parent entre les clés 65 et 67 qui l’encadrent, commeillustré ci-après :

Le nœud contient désormais 5 clés et dépasse donc la limite de 4 clés qui caractérise les arbres à5 directions. Ce nœud est donc fractionné, ce qui fait remonter la clé centrale 65 jusqu’au nœudparent :

En général, le fractionnement d’un nœud n’est pas fréquent, en particulier si m est important. Parexemple, si m = 50, environ 2 % des nœuds dépasseront leur limite au cours des recherches. Celasignifie que, dans ce cas, un fractionnement du niveau inférieur se révélerait nécessaire uniquementpour 2 % des insertions. En outre, un fractionnement du deuxième niveau en partant du niveau infé-rieur (c’est-à-dire un fractionnement double) ne serait nécessaire que pour 2 % de 2 % des inser-tions, ce qui équivaut à une probabilité de 0,0004. Si nous poursuivons notre raisonnement, laprobabilité d’un troisième fractionnement serait de 0,000008. Comme vous pouvez le constater, uneracine a très peu de chances d’être fractionnée. Étant donné qu’il s’agit de la seule façon de fairecroître l’arbre verticalement, celui-ci a tendance à rester très court et très large, ce qui réduit considé-rablement la durée des recherches.

11.2 ARBRES ÉQUILIBRÉS OU ARBRES-BUn arbre-B d’ordre m est en fait un arbre de recherche multidirectionnel qui présente les caractéristiquessupplémentaires suivantes :

1. Sa racine est composée de deux enfants minimum.

2. Tous ses autres nœuds internes sont composés de m/2 enfants au minimum.

3. Tous les nœuds feuilles se trouvent au même niveau.

23 2513 19 39 45 5548 50

27 33 38 47

65 6661 64 73 75 8278 81

59 60 70

87

77 85

57 72

67

23 2513 19 39 45 5548 50

27 33 38 47

6661 64 73 75 8278 81

59 60 65

87

77 85

57 72

67

70

Page 230: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

220 Arbres de recherche

Ces critères rendent l’arbre plus équilibré et, par conséquent, plus efficace. Ils simplifient égalementles algorithmes d’insertion et de suppression. Les arbres-B servent d’index aux ensembles de données detaille importante stockés sur disque. Dans une base de données relationnelle, les données sont organiséesen séquences d’enregistrements distinctes qualifiées de tables. Chaque table est susceptible d’être stoc-kée sous forme d’un fichier de données séquentiel dans lequel les enregistrements sont numérotéscomme les éléments d’un tableau. D’autre part, le système de base de données a également la possibilitéd’accéder aux enregistrements directement à l’aide de leur adresse sur le disque. Quelle que soit la solu-tion choisie, chaque enregistrement est accessible directement sur le disque via une structure d’adres-sage. Il vous suffit donc d’avoir l’adresse de l’enregistrement sur le disque pour pouvoir y accéderdirectement (c’est-à-dire en lisant le disque une seule fois). Par conséquent, la « clé » qui est stockéedans un arbre-B est en fait une paire clé/adresse qui contient la valeur de clé réelle de l’enregistrement(par exemple, un numéro de sécurité sociale dans le cas d’enregistrements de données privées ou unISBN dans le cas d’un livre), ainsi que son adresse sur le disque. Dans l’exemple suivant, seule la valeurde clé apparaîtra ; l’adresse sur le disque qui lui est normalement associée sera implicite.

Exemple 11.3 Arbre-B

La figure suivante illustre le cas d’un arbre-B d’ordre 5. Chacun de ses nœuds internes est composéde 3, 4 ou 5 enfants et toutes ses feuilles se trouvent au niveau 3.

Algorithme 11.1 Rechercher un élément dans un arbre-B.

Pour rechercher un enregistrement comportant une clé k à l’aide d’un arbre-B d’ordre m :

1. Si l’arbre est vide, renvoyez null.2. Supposons que x soit la racine.3. Répétez les étapes 4 à 6 jusqu’à ce que x soit un nœud feuille.4. Appliquez la recherche binaire (algorithme 2.2) au nœud x pour la clé ki, avec ki – 1 < k ≤ ki (pour

k0 = –∞ et km = ∞).5. Si ki = k, procédez à l’extraction de l’enregistrement du disque et renvoyez-le.6. Supposons que x soit la racine du sous-arbre Si.7. Renvoyez null.

Comme vous pouvez le constater, ce processus est similaire à la recherche d’un thème dans l’indexd’un livre. Chaque page de l’index est associée à un mot ou à une lettre correspondant aux thèmesqu’elle contient. Ce mot ou cette lettre jouent le rôle des clés des nœuds internes dans un arbre derecherche. Quant au numéro de la page figurant en regard du thème, il joue le rôle de l’adresse dunom du fichier qui vous mènera aux données sur le disque. La dernière étape de notre processusconsiste à rechercher la page dans le livre ou bien le fichier sur le disque. Nous pourrions poussercette analogie encore plus loin si l’index du livre avait son propre index. En effet, chaque niveauinterne d’un arbre multidirectionnel est similaire à un autre niveau d’index.

23 2513 19 39 45 5548 50

33 47

65 6761 64 73 75 8278 80

66 77

87

88 95

57 81

90 92 96 98

Page 231: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Arbres équilibrés ou arbres-B 221

Algorithme 11.2 Insérer des éléments dans un arbre-B

Pour insérer un enregistrement de clé k à l’aide de l’index d’un arbre-B d’ordre m :

1. Si l’arbre est vide, créez un nœud racine composé de deux feuilles factices, insérez k à ce niveauet renvoyez true (afin d’indiquer que l’insertion est réussie).

2. Supposons que x soit la racine.

3. Répétez les étapes 4 à 6 jusqu’à ce que x soit un nœud feuille.

4. Appliquez l’algorithme 2.2 de recherche au nœud x pour la clé ki, avec ki – 1 < k ≤ ki (pourk0 = –∞ et km = ∞).

5. Si ki = k, renvoyez false (afin d’indiquer que l’insertion a échoué parce qu’un enregistrementavec la clé k existe déjà et que les clés doivent être uniques).

6. Supposons que x soit la racine du sous-arbre Si.

7. Ajoutez l’enregistrement sur le disque.

8. Insérez k (accompagnée de l’adresse de l’enregistrement sur le disque) dans x entre ki – 1 et ki.

9. Ajoutez un nœud feuille factice à x.

10. Si le degré (x) = m, répétez les étapes 11 à 13 jusqu’à ce que le degré (x) < m.

11. Supposons que kj soit la clé centrale du nœud x.

12. Supposons que u et v soient les moitiés droite et gauche de x après la suppression de kj de x.

13. Si x est la racine, créez un nouveau nœud racine comportant kj et les sous-arbres u et v.

14. Dans le cas contraire, insérez kj dans le nœud parent de x et attachez les sous-arbres u et v.

15. Renvoyez true.

L’algorithme de suppression des arbres-B est similaire à leur algorithme d’insertion.

Les trois algorithmes que nous venons d’étudier ont une durée d’exécution proportionnelle à lahauteur de l’arbre. D’après le corollaire 9.1, nous pouvons déduire que la hauteur de l’arbre est pro-portionnelle à logm(n). D’après le théorème A.6, nous pouvons déduire que cette hauteur est propor-tionnelle à lgn. Nous pouvons donc en déduire le théorème suivant :

Théorème 11.1 : dans un arbre-B, la recherche, l’insertion et la suppression ont une durée d’exécu-tion égale à O(lgn).

23 2513 19 30

33 47

x

23

2513 19 30

33 47

u

kj

23

2513 19 30

33 47

u

v

v

Page 232: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

222 Arbres de recherche

11.3 ARBRES BINAIRES DE RECHERCHEUn arbre binaire de recherche est un arbre binaire dont leséléments comprennent un champ clé de type ordinal ayant lapropriété suivante : si k est la valeur de clé d’un nœud, k ≥ xpour chaque clé x du sous-arbre situé à gauche du nœud, etk ≤ y pour chaque clé y du sous-arbre situé à droite du nœud.Grâce à cette propriété (qualifiée de propriété BST enanglais), le parcours symétrique d’un arbre binaire de recher-che crée des éléments dans l’ordre croissant. Vous applique-rez cette propriété pour chaque insertion dans l’arbre.

Algorithme 11.3 Insérer des éléments dans un arbre binaire de recherche

Pour insérer un élément avec la valeur de clé k dans un arbre binaire de recherche, vous devez procé-der de la façon suivante :

1. Si l’arbre est vide, insérez le nouvel élément à la racine. Le renvoyer.2. p recherche la racine.3. Si k est inférieure à la clé stockée au niveau indiqué par p et que le nœud signalé par p n’a pas

d’enfant à gauche, insérez le nouvel élément comme enfant gauche de p. Le renvoyer.4. Si k est inférieure à la clé stockée au niveau de p et que le nœud signalé par p a un enfant à gau-

che, p recherche son enfant gauche. Retournez ensuite à l’étape 3.5. Si le nœud au niveau de p n’a pas d’enfant droit, insérez le nouvel élément comme enfant droit

de p. Le renvoyer.6. p recherche son enfant droit. Retournez ensuite à l’étape 3.

Exemple 11.4 Insérer des éléments dans un arbre binaire de recherche

Appliquez l’algorithme 11.3 pour insérer un élément declé M dans l’arbre binaire précédent. L’étape 1 lance l’ité-rateur p à la racine K. Étant donné que M est supérieur à K(c’est-à-dire que M suit K dans l’alphabet) et que le nœudK a un enfant à droite, l’algorithme passe à l’étape 6 etréinitialise l’itérateur p au niveau du nœud P, puisretourne à l’étape 3. Ensuite, M étant inférieur à P (c’est-à-dire que M précède P dans l’alphabet) et le nœud Payant un enfant à gauche, l’algorithme passe à l’étape 4et réinitialise l’itérateur p au nœud N avant de retourner àl’étape 3. Puis, étant donné que M est également inférieur à N mais que le nœud N n’a pas d’enfant àgauche, l’algorithme passe à l’étape 5 et insère le nouvel élément comme enfant gauche du nœud Net le renvoie.

Exemple 11.5 Créer un arbre binaire de recherche

La séquence d’arbres suivante illustre la création d’un arbre binaire de recherche en insérant laséquence d’entrée 44, 77, 55, 22, 99, 33, 88 :

E

K

B

P

G N

A

S

FD QH

E

K

B

P

G N

A

S

FD QH M

p

44

22 77

Insérer 5544

22

7744Insérer Insérer 44

Page 233: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Caractéristiques des arbres binaires de recherche 223

Si un arbre binaire de recherche est équilibré, vos recherches seront plus efficaces. Comme dans lecas d’une recherche binaire, vous avez besoin de O(lg n) étapes pour trouver un élément dans unarbre binaire de recherche équilibré. Cependant, si vous ne spécifiez aucune autre restriction, vousrisquez d’obtenir un arbre fortement déséquilibré, surtout lorsque les éléments sont insérés dans unordre trié. Dans ce cas, l’arbre devient une liste linéaire et transforme l’algorithme de recherche enrecherche séquentielle O(n).

Exemple 11.6 Arbre binaire de recherche déséquilibré

Dans cet exemple, nous allons utiliser les mêmes données d’entrée que dans l’exemple11.2, mais dans un ordre différent : 99, 22, 88, 33, 77, 55, 44. Nous obtenons alorsl’arbre ci-contre :

11.4 CARACTÉRISTIQUES DES ARBRES BINAIRES DE RECHERCHE EN MATIÈRE DE PERFORMANCES

Les fonctions insert() et search() commencent à la racine de l’arbre et descendent lelong des feuilles en effectuant une comparaison à chaque niveau. Dans ces conditions, ladurée d’exécution d’un algorithme est proportionnelle à h + 1, h correspondant à la hauteurde l’arbre. La fonction search() peut s’arrêter avant d’atteindre une feuille, mais elle nepeut pas effectuer plus de h + comparaisons.

Théorème 11.2 : dans un arbre binaire de recherche de taille n, les fonctions insert() etsearch() ont besoin de O(lg n) comparaisons dans le meilleur des cas.Démonstration : dans le meilleur des cas, l’arbre binaire est totalement équilibré et presque complet.C’est pourquoi, si l’on reprend le corollaire 10.2, h + 1 ≈ lg(n + 1) = O(lg n).

Corollaire 11.1 : Dans un arbre binaire de recherche de taille n, les fonctions insert() etsearch() ont besoin de O(n) comparaisons dans le pire des cas.Démonstration : Dans le pire des cas, l’arbre est linéaire et h + 1 = n = O(n).

Théorème 11.3 : Dans un arbre binaire de recherche de taille n, les fonctions insert() etsearch() ont besoin de O(2lnn) ≈ O(1.39lg n) comparaisons dans la plupart des cas.La démonstration de ce résultat dépasse la portée de cet ouvrage.

44

22 77

33 55 99

88

Insérer 88

44

22 77

33 55 99

Insérer 33

44

22 77

55 99

Insérer 99

44

22 77

55

Insérer 22

99

22

88

33

77

55

44

Page 234: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

224 Arbres de recherche

11.4 ARBRES AVLLe problème de déséquilibre illustré dans l’exemple 11.3 peut être évité en imposant certaines restric-tions aux nœuds de l’arbre binaire de recherche. C’est là qu’intervient l’arbre AVL, un arbre binaire telque les hauteurs des deux sous-arbres diffèrent de 1 au maximum.

Dans le cadre des arbres AVL, l’algorithme d’insertion s’assure que chaque nœud est équilibré àl’aide d’un nombre d’équilibre défini comme la hauteur du sous-arbre de droite moins celle du sous-arbre de gauche. Pour que l’arbre soit équilibré, chaque nombre d’équilibre doit être égal à 1, 0 ou –1. Cetype d’arbre a été nommé d’après ses inventeurs, Adelson-Velskii-Landis. La figure suivante illustre ladifférence entre un arbre AVL et un arbre non AVL.

Ainsi, l’arbre de gauche n’est pas un arbre AVL parce qu’il est déséquilibré au niveau du nœud C,son nombre d’équilibre étant égal à 2, soit une différence de profondeur qui ne se trouve pas dans la four-chette autorisée. Cet arbre est également déséquilibré au niveau du nœud G.

En revanche, l’arbre de droite illustre parfaitement l’arbre AVL parce que chaque numéro d’équilibreest égal à –1, 0 ou 1.

Les arbres AVL sont liés aux nombres de Fibonacci Fm (reportez-vous à la section A.10).

Théorème 11.4 : si un arbre AVL a une taille n et une hauteur h, n ≥Fh + 2 – 1.Démonstration : supposons que T soit un arbre AVL de hauteur h et de taille n. Si h = –1, alors n = 0 (cequi signifie que l’arbre est vide) et Fh + 2 – 1 = F(0) + 2 – 1 = F2 – 1 = 1 – 1 = 0 = n. Si h = 0, alors n = 1(ce qui signifie que l’arbre est un singleton) et Fh + 2 – 1 = F(1) + 2 – 1 = F3 – 1 = 1 – 1 = 0 = n. Nousvenons d’établir la base de notre démonstration récursive. Supposons maintenant que h > 1 et que, parhypothèse inductive, l’inégalité soit vraie pour tous les arbres AVL de hauteur < h. Supposons que T soitun arbre AVL de hauteur h et de taille n. Le nombre d’équilibre à la racine T doit alors être égal à –1, 0ou 1. Supposons que TL et TR soient les sous-arbres gauche et droit de T, que hL et hR soient leurs hauteursrespectives et que nL et nR soient leurs tailles respectives. Il s’ensuit que soit hL = h –1, soit hR = h –1 etque hL ≥h – 2 et hR ≥h – 2. Ainsi, dans les deux cas,

Par conséquent, l’hypothèse inductive nous permet de déduire que

Corollaire 11.2 : la hauteur d’un arbre AVL est égale à O(1.44 lgn).Démonstration : d’après le corollaire A.3, Fh + 2 = Θ(φh + 2) = Ω(φh). Ainsi, n ≥ Fh + 2 – 1 = Ω(Fh + 2)= Ω(φh). Ainsi, φh = O(n), donc h = O(logφn) = O(1.44 lgn).

L

KJI

GF

C

A

H

ED

B

10

1-1

0

01

0

1

1

-1

1ON

ML

GF

C

A

KJIH

ED

B

00

1

0

0

0

00

00

2

2

0

00

Ce n’est pas un arbre AVL C’est un arbre AVL

FhLFhG

+ Fh 1– Fh 2–+≥ Fh=

n 1 nL nR+ + 1 FhL 2+1–( ) FhR 2+

1–( )+ + Fh 2+ 1–≥ ≥=

Page 235: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe AVLTree 225

Corollaire 11.3 : dans le cadre d’un arbre AVL, la durée d’exécution des recherches, des insertions etdes suppressions est plus lente de 44 % au maximum par rapport à celle de ces mêmes opérations effec-tuées dans un arbre complètement équilibré.

11.6 CLASSE AVLTreeCette classe destinée aux arbres AVL étend la classe BinaryTree définie dans la section 10.8 :

public class AVLTree extends BinaryTree protected AVLTree left, right; protected int balance; protected java.util.Comparator comp; public AVLTree(java.util.Comparator comp) this.comp = comp; public AVLTree(Object root, java.util.Comparator comp) this.root = root; this.comp = comp;

public boolean add(Object object) AVLTree temp = attach(object); if (temp != this) left = temp.left; right = temp.right; balance = temp.balance; return true;

public AVLTree attach(Object object) if (root == null) // l’arbre est vide root = object; return this; if (comp.compare(object,root)<0) // insérer dans le sous-arbre // gauche if (left == null) left = new AVLTree(object,comp); ++size; --balance; else int lb = left.balance; left = left.attach(object); if (left.balance != lb && left.balance != 0) --balance; if (balance < -1) if (left.balance > 0) left = left.rotateLeft(); return rotateRight(); else // insérer dans le sous-arbre droit if (right == null) right = new AVLTree(object,comp); ++size; ++balance; else int rb = right.balance;

Page 236: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

226 Arbres de recherche

right = right.attach(object); if (right.balance != rb && right.balance != 0) ++balance; if (balance > 1) if (right.balance < 0) right = right.rotateRight(); return rotateLeft(); return this;

private AVLTree rotateRight() // reportez-vous à l’exercice 11.6 private AVLTree rotateLeft() AVLTree x = this, y = right, z = y.left; x.right = z; y.left = x; int xb = x.balance, yb = y.balance; if (yb < 1) --x.balance; y.balance = ( xb>0 ? yb-1 : xb+yb-2 ); else if (yb < xb) x.balance -= yb+1; --y.balance; else y.balance = xb-2; return y;

Exemple 11.7 Créer un arbre AVL

La figure suivante illustre l’insertion de G, M, T, D et P dans un arbre AVL vide :

T

M

G

0

1

2G0

M

G

0

1

T

M

G0

0

0

add(G)

rotateLeft()

add(M) add(T)

rotateLeft()

add(D)

T

M

E

D

G

0

1

0

-2

-2

T

M

D

G

0

0

-1

-1

add(E) T

M

D

E

G

-1

0

0

-2

-2

rotateRight()

G

T

M

D

E

00

0

1

0add(P)

G P

T

M

D

E

000

-1

0

0

X

X

X

Page 237: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 227

La première rotation se produit avec l’insertion de T. En effet, la racine est alors égale à 2, ce qui vaà l’encontre de la propriété des arbres AVL. Avec la rotation à gauche autour de la racine x, Mdevient le parent de son parent précédent G. La rotation suivante a lieu après l’insertion de E. Cetterotation autour du parent D étend la partie G-D-E, mais laisse le nœud G déséquilibré puisqu’il estégal à –2. D’où la nécessité d’une autre rotation dans la direction opposée.

Comme vous pouvez le constater, les rotations sont particulièrement efficaces : grâce à des modifica-tions locales des références et des différences de profondeur, elles rétablissent un équilibre presqueparfait pour l’arbre.

Exemple 11.8 Insérer d’autres éléments dans le même arbre AVL

La figure suivante illustre le cas d’insertions supplémentaires dans le même arbre AVL. Nous allonsdonc intégrer W après U, V et Z.

Comme vous pouvez le constater, nous devons procéder à une double rotation au cours de laquelleun sous-arbre non trivial est déplacé. Le sous-arbre contenant U est décalé de son parent V vers sonparent T. Vous remarquerez que la propriété des arbres binaires de recherche est conservée.

Bien qu’il soit légèrement compliqué, l’algorithme d’insertion des arbres AVL est extrêmement effi-cace. Les rotations qui garantissent l’équilibre des arbres ne constituent que des modifications loca-les de certaines références et des nombres d’équilibre.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

11.1 Quels sont les avantages et les inconvénients d’un arbre binaire de recherche ?

11.2 Quels sont les avantages et les inconvénients d’un arbre AVL ?

UP00

Z

WT

V

M

GD

E

0

1000

0

1

0

rotateRight()

rotateLeft()

W

ZU

VP

T

M

G

E

-10

1000

2

2

0

0Z

WU

VP

T

M

GD

E

10

2000

2

2

0

0

X

X

Page 238: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

228 Arbres de recherche

RÉPONSES¿RÉPONSES

11.1 L’inconvénient d’un arbre binaire de recherche est qu’il est parfois complètement déséquilibré ettransforme ainsi la recherche de cas en algorithme O(n). En revanche, il est très efficace pour lesinsertions et les suppressions.

11.2 Un arbre AVL présente l’avantage d’être toujours équilibré, ce qui garantit la rapidité d’exécutionO(lg n) de l’algorithme binaire de recherche. Cependant, vous devrez effectuer des rotations com-plexes utilisées par les algorithmes d’insertion et de suppression nécessaires à l’équilibre de l’arbre.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

11.1 Décrivez ce qui se passe lorsqu’un nouvel enregistrement comportant la clé 16 est inséré dansl’arbre à 5 directions décrit dans l’exemple 11.1.

11.2 Définissez deux autres méthodes d’organisation des 7 clés de l’exemple 11.5 qui vous permet-tront de créer le même arbre binaire de recherche.

11.3 Décrivez une méthode de tri des tableaux d’objets à l’aide d’un arbre binaire de recherche, puisdéterminez la complexité de votre algorithme.

11.4 Parmi les arbres binaires suivants, lesquels sont des arbres binaires de recherche ?

11.5 Vérifiez l’affirmation suivante du corollaire 11.2 : logφn = 1.44 lgn.

11.6 Implémentez la méthode rotateRight() dans la classe AVLTree.

11.7 Démontrez que chaque sous-arbre d’un arbre binaire est également un arbre binaire.

11.8 Démontrez que chaque sous-arbre AVL est également un arbre AVL.

11.9 Voici une liste d’abréviations postales des 10 premiers états américains à avoir ratifié leur consti-tution : DE, PA, NJ, GA, CT, MA, MD, SC, NH, VA. Créez un arbre AVL pour l’insertion dechacune de ces chaînes.

a.

B

A

D

C

FE

H

G

J NI MK L O

b.

D

H

B

L

F J

A

N

EC MKG I O

c.

C

F

A

H

E G N

DB K O

d.

N

O

L

R

Q

K

S

M P T

Page 239: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 229

SOLUTIONS¿SOLUTIONS

11.1 Pour insérer un nouvel enregistrement composé d’une clé 16 dans l’arbre, votre recherche ini-tiale doit commencer par le premier nœud feuille, comme illustré dans la figure suivante :

Étant donné qu’il s’agit d’un arbre de recherche à 5 directions, ce premier nœud feuille a dépasséson nombre d’équilibre limite et a dû être fractionné en deux nœuds feuilles, ce qui a transféré laclé centrale 19 jusqu’au nœud parent :

Cependant, maintenant que le nœud parent a également dépassé la limite du nombre d’équilibre,il est fractionné, ce qui déplace sa clé centrale jusqu’à son nœud parent :

23 2513 16 39 45 5548 50

27 33 38 47

65 6761 64 73 75 8278 81

59 60 70

87

77 85

57 72

19

23 2513 16 39 45 5548 50

27 33 38 47

65 6761 64 73 75 8278 81

59 60 70

87

77 85

57 72

19

23 2513 16 39 45 5548 50

27

33

38 47

65 6761 64 73 75 8278 81

59 60 70

87

77 85

57 72

19

Page 240: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

230 Arbres de recherche

11.2 Vous pourriez classer les 7 clés de l’exemple 11.5 de la façon suivante et obtenir le même arbrebinaire de recherche que celui de cet exemple :

a. 44, 22, 33, 77, 55, 99, 88 ;

b. 44, 22, 77, 33, 55, 99, 88.

11.3 Un tableau d’objets peut être trié grâce à l’insertion d’objets dans un arbre binaire de recherchepuis à l’utilisation d’un parcours infixe pour copier ces objets dans le tableau. La propriété desarbres binaires de recherche veut que le parcours infixe visite les éléments dans l’ordre. Ainsi, sivous avez recours à un arbre AVL, chaque insertion a une durée d’exécution égale à O(lgn), c’estpourquoi la création d’un arbre composé de n éléments sera exécutée en une durée de O(n lgn).Le parcours infixe qui en découle a également une complexité O(n lgn), donc l’algorithme com-plet trie le tableau en une durée égale à O(n lgn).

11.4 Tous ces arbres sont des arbres binaires de recherche, à l’exception de l’arbre a.

11.5 logφ n = (logφ2) (lgn) = (ln2 / lnφ) (lgn) = (0,693148/0,481212) (lgn) = (1,4404) (lgn) = 1,44 lgn

11.6 • private AVLTree rotateRight()• AVLTree x = this;• AVLTree y = left;• AVLTree z = y.left;• x.left = z;• y.left = x;• int xb = x.balance;• int yb = y.balance;• if (yb > 1)• ++x.balance;• y.balance = ( xb<0 ? yb+1 : xb+yb+2 );• • else if (yb > xb)• x.balance += yb-1;• ++y.balance;• • else y.balance = xb+2;• return y;•

11.7 Théorème : chaque sous-arbre d’un arbre binaire de recherche est lui-même un arbre binairede recherche.Démonstration : supposons que T soit un arbre binaire de recherche et S un sous-arbre de T. x estun élément de S, et L et R sont respectivement les sous-arbres gauche et droit de x dans S. Étantdonné que S est un sous-arbre de T, x est également un élément de T, et L et R sont les sous-arbresgauche et droit de x dans T. Donc, y ≤ x ≤ z lorsque y ∈ L et lorsque z ∈ R.Donc S a aussi la propriété de l’arbre binaire de recherche.

11.8 Théorème : chaque sous-arbre d’un arbre AVL est lui-même un arbre AVL.Démonstration : nous venons de démontrer dans la solution précédente que chaque sous-arbred’un arbre binaire de recherche est lui-même un arbre binaire de recherche. À cette démonstra-tion s’ajoute le fait que si S est un sous-arbre d’un arbre AVL T, chaque nœud de S se trouve éga-lement dans T. Le nombre d’équilibre de chaque nœud est alors égal à –1, 0 ou 1.

11.9 La solution est illustrée par les arbres suivants :

Page 241: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 231

DE DE0

PA1

0

PA

DE NJ

0

2

NJ

-1

PA

DE

0

2

PA

1

NJ

DErR

0

0

DE0

PA

NJ GA

0

1

-1

GA

DE0

PA

NJ CT

00

0

-1

CT GA

DE0

PA

NJr_l

MA

10

1

-2

CT GA

DE0

PA

NJ

0

MA

0-1

-1

-2

DE MA

GA0

PA

NJ rRrL

CT0

0

-1

0

CT

DE

00

0

MA PA

NJ

GA

0

-1

1

CT

DE

0

01

0

MD

MA PA

NJ

GAMD

0

SC

SC

0

NH

NH rL

0

-1

1

CT

DE

0

11

0

MD

MA PA

NJ

GA

0

SC

0

-1

2

CT

DE

1

12

-1

MD

MA PA

NJ

GA

0

SC

0

-1

1

CT

DE

0

10

0

NH

MD PA

NJ

GA

MA0

rLVA

0

PA1

SC

0

-1

2

CT

DE

0

20

1

NH

MD PA

NJ

GA

MA0

0

VA

0

VA

0

-1

1

CT

DE

0

00

0

NH

MD SC

NJ

GA

MA0

Page 242: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie
Page 243: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 12

Tas et files de priorité

12.1 TASUn tas est un arbre binaire parfait dont les éléments ont des clés conformes à la propriété suivante destas : les clés se trouvant le long du chemin racine-vers-feuille sont décroissantes. Par exemple, l’arbre ci-après est un tas parce qu’il s’agit d’un arbre binaire parfait et que les éléments situés le long de ses che-mins racine-vers-feuille sont décroissants :

77 66 44 22 ;77 66 44 41 ;77 66 60 58 ;77 66 60 25 ;77 55 33 29 ;77 55 55.

Remarquez que les tas pourraient contenir des arbres généalogiques puisque leur propriété signifieque chaque parent est plus vieux que son/ses enfant(s).

Les tas permettent d’implémenter les files de priorité (voir la section 12.5) et l’algorithme de tri ver-tical (voir la section 13.8).

12.2 MAPPAGE NATURELChaque arbre binaire parfait a un mappage naturel dans un tableau, comme nous l’avons déjà vu dansl’algorithme 10.1. Par exemple, le tas de la section 12.1 est mappé dans le tableau ci-dessus. Le mappagenaturel est obtenu à partir du parcours en largeur de l’arbre. Dans le tableau final, le parent de l’élémentsitué à l’index i est à l’index i/2 et les enfants de cet élément sont aux index 2i et 2i + 1. Par exemple,l’élément 60 est à l’index i = 5, son parent est l’élément 66 situé à l’index i/2 = 2 et ses enfants sont leséléments 58 et 25 aux index 2i = 10 et 2i + 1 = 11.

Le mappage naturel entre un arbre binaire parfait etun tableau est une correspondance bidirectionnelle. Pourremapper les éléments d’un tableau dans un arbre binaireparfait, il vous suffit de numéroter les nœuds de l’arbreconsécutivement en suivant un parcours en largeur et encommençant par le numéro 1 à la racine. Vous copiezensuite l’élément du tableau situé à l’index i dans le nœudnuméroté i. Si l’arbre obtenu présente la propriété de tas,vous pouvez dire que le tableau a également cette propriété.

77

66 55

44 3360 55

22 295841 25

3366 44 555577 60 5822 2541 2911 1210876543210 9

11 12108

7654

32

1

9

Page 244: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

234 Tas et files de priorité

Exemple 12.1 Déterminer si un tableau présente la propriété de tas

Pour déterminer si un tableau a la propriété de tas, nousdevons d’abord le mapper dans un arbre binaire, puisvérifier le chemin racine-vers-feuille.Le chemin 88, 66, 44, 51 n’est pas décroissant parceque 44 < 51. Cet arbre n’a donc pas la propriété de taset, par conséquent, le tableau non plus.Un tableau avec la propriété de tas est partiellement

ordonné. Cela signifie que la plupart des clés plus importan-tes sont placées avant les plus petites et, plus précisément,que chaque sous-tableau tas-chemin est trié dans un ordredécroissant, le sous-tableau tas-chemin étant une sous-séquence d’éléments du tableau dans lequel chaque numérod’index correspond à la moitié de son successeur. Par exem-ple, le sous-tableau a[1], a[2], a[5], a[11], a[22], a[45], a[90], a[180] serait unsous-tableau du tableau a[] de 200 éléments. L’algorithme de tri vertical exploite cette caractéristiquepour obtenir une méthode rapide et efficace de tri des tableaux.

12.3 INSÉRER DES ÉLÉMENTS DANS UN TASDans un tas, les éléments sont insérés à côté de la feuille la plus à droite du niveau le plus bas. La pro-priété de tas est ensuite restaurée en faisant remonter l’élément dans l’arbre jusqu’à ce qu’il soit plus« jeune » que son parent, c’est-à-dire que sa clé soit plus importante. À chaque itération, l’enfant est per-muté avec son parent.

Exemple 12.2 Insérer un élément dans un tas

La clé 75 serait insérée de la façon suivante dans le tas :

5566 33 757788 44 5130 2210876543210

77

33 55 75

30 22 51

44

66

88

9

88

66 77

33 5544 75

30 5122 75

88

66 77

33 5544 75

30 5122

88

66 77

33 5575 75

30 5122 44

75

88

75 77

33 5566 75

30 5122 44

Page 245: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Supprimer un élément d’un tas 235

L’élément 75 est ajouté dans l’arbre comme la dernière nouvelle feuille. Il est ensuite permuté avecson élément parent 44 parce que 75 > 44. Puis, il est permuté avec son élément parent 66 parce que75 > 66. La propriété de tas est maintenant rétablie puisque le nouvel élément 75 est inférieur à sonparent et supérieur à ses enfants.

Remarquez que l’insertion ne concerne que les nœuds situés le long d’un chemin racine-vers-feuille.

12.4 SUPPRIMER UN ÉLÉMENT D’UN TASL’algorithme de suppression d’un élément du tas élimine toujours l’élément racine de l’arbre. Pour cela,le dernier élément feuille est déplacé dans l’élément racine, puis la propriété de tas est rétablie en faisantdescendre le nouvel élément racine dans l’arbre jusqu’à ce qu’il soit plus « vieux » que ses enfants, c’est-à-dire que sa clé soit inférieure à celle de ses enfants.

Exemple 12.3 Supprimer un élément d’un tas

L’élément racine (clé 88) serait supprimé du tas de la façon suivante :

La dernière feuille (clé 44) est supprimée et copiée dans la racine ; elle remplace la racine précédente(clé 88) qui est supprimée. Ensuite, pour rétablir la propriété de tas, l’élément 44 permute avec leplus grand de ses deux enfants (77). Cette étape est répétée jusqu’à ce que l’élément 44 ne soit plusinférieur à ses enfants. Dans le cas présent, 44 redevient finalement une feuille.

Remarquez que la suppression touche uniquement les nœuds situés le long d’un seul chemin racine-vers-feuille, ce qui nous donne le résultat suivant d’après le théorème 10.2 :

Théorème 12.1 : les insertions dans un tas et les suppressions d’éléments de ce tas ont une duréed’exécution égale à O(lgn).

88

75 77

33 5566 75

30 5122 44

44

75 77

33 5566 75

30 5122

77

75 44

33 5566 75

30 5122

77

75 75

33 5566 44

30 5122

Page 246: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

236 Tas et files de priorité

12.5 CLASSE PriorityQueueUne pile est un conteneur LIFO, c’est-à-dire que le dernier élément entré est le premier sorti. Une file estun conteneur FIFO, c’est-à-dire que le premier élément entré est le premier sorti. Une file de priorité estun conteneur BIFO, c’est-à-dire que l’élément avec la priorité la plus élevée est le premier à sortir. Eneffet, chaque élément porte un numéro de priorité dans le contexte des files de priorité.

Ce type de file est largement utilisé dans les systèmes informatisés. Par exemple, si plusieurs ordina-teurs sont connectés à une même imprimante sur un réseau local, les travaux d’impression qui sont dansune file d’attente sont généralement mis temporairement dans une file de priorité où les travaux les pluspetits sont prioritaires.

La plupart du temps, les files de priorité sont implémentées comme des tas puisque leur structure dedonnées conserve toujours l’élément avec la clé la plus importante à la racine. En outre, les tas permet-tent d’insérer et de supprimer efficacement des éléments (reportez-vous à la section 12.7). Cependant, leframework de collections Java vous propose une autre structure arborescente qui se révélera presqueaussi efficace, à savoir la classe java.util.TreeSet. Grâce à elle, vous pourrez aisément implémen-ter la classe PriorityQueue Java à l’aide de l’interface Comparator, comme démontré dans l’exem-ple suivant.

Exemple 12.4 Classe PriorityQueue

La classe suivante est destinée aux files de priorité des objets :

import java.util.*;

public class PriorityQueue extends TreeSet public PriorityQueue() super();

public PriorityQueue(Comparator comparator) super(comparator);

public void push(Object object) add(object);

public Object top() return first();

public Object pop() Object object = first(); remove(object); return object;

Cette sous-classe exploite la fonctionnalité de la classe TreeSet afin d’insérer et de supprimer deséléments efficacement en O(lgn).

Page 247: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Interface Java Comparator 237

12.6 INTERFACE JAVA ComparatorUne file de priorité doit être capable de comparer ses éléments en fonction de la priorité qui leur a étéaffectée. C’est le rôle du framework de collections Java qui utilise pour cela un objet Comparator.

Exemple 12.5 Classe Person et ses propres classes Comparator

La classe Person implémentée ci-après contient des individus, leur date de naissance (yob, corres-pondant à l’anglais year of birth) et leur date de décès (yod correspondant à l’anglais year of death).Sa méthode toString() imprimera ces informations au format suivant :

Pascal(1623-1662)

Cette classe est composée de deux constructeurs car vous ne connaîtrez pas toujours la date de décèsd’un individu qui peut d’ailleurs être encore en vie.

public class Person private String name; private int yob, yod;

public Person(String name, int yob) this.name = name; this.yob = yob;

public Person(String name, int yob, int yod) this(name,yob); this.yod = yod;

public int getYob() return yob;

public int getYod() return yod;

public String toString() return name + "(" + yob + (yod>0?"-"+yod:"") + ")";

Vous trouverez ci-après l’exemple d’une classe Comparator destinée à comparer les instances de laclasse Person en fonction des dates de naissance.

class YobComparator implements java.util.Comparator public int compare(Object o1, Object o2) if (o1==null || !(o1 instanceof schaums.dswj.Person)) return -1; Person p1 = (Person)o1; if (o2==null || !(o2 instanceof Person)) return 1; Person p2 = (Person)o2; if (p1.getYob() < p2.getYob()) return -1; if (p1.getYob() > p2.getYob()) return 1; return 0;

Page 248: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

238 Tas et files de priorité

Une instance de cette classe renvoie –1 si la date de naissance de la première personne est antérieureà celle de la seconde. Elle renvoie 1 dans le cas contraire. Il s’agit du protocole généralement utilisédans les classes Comparator : un nombre négatif signifie « inférieur à », 0 signifie « égal à » et unnombre positif signifie « supérieur à ». Par ailleurs, comme vous avez pu le constater, l’interfaceComparator requiert la signature suivante :

public int compare(Object o1, Object o2)

C’est pourquoi toute implémentation doit vérifier la classe intrinsèque à laquelle appartient chaqueargument grâce à l’opérateur instanceof. En outre, les arguments doivent être transtypés en objetsPerson de façon à pouvoir accéder aux champs yob.

Exemple 12.6 Tester la classe PriorityQueue contenant un comparateur

public class Ex1206 static public void main(String[] args) PriorityQueue yobQueue = new PriorityQueue(new YobComparator()); yobQueue.push(new Person("Barrow",1630)); yobQueue.push(new Person("Fermat",1601)); yobQueue.push(new Person("Pascal",1623)); yobQueue.push(new Person("Wallis",1616)); System.out.println(yobQueue); while (!(yobQueue.isEmpty())) System.out.println("pop()=" + yobQueue.pop() + ": " + yobQueue);

Ce programme instancie l’objet yobQueue de PriorityQueue en lui associant un YobCompara-tor. Le constructeur TreeSet qui prend un argument Comparator gère cette opération de liaison.Ensuite, la méthode add() implémentée par la classe TreeSet est utilisée afin d’ajouter quatreobjets Person dans la file de priorité.Puis, le programme teste la méthode pop() à plusieurs reprises jusqu’à ce que la file de priorité soitvide. À chaque itération, il imprime tout le contenu de cette file à l’aide de la méthode héritée Tree-Set.toString() qui appelle la méthode Person.toString().

Il est possible d’utiliser la même classe PriorityQueue avec différents comparateurs. Pour cela, ilvous suffit de passer un autre objet comparateur à son constructeur.

Exemple 12.7 Tester la classe PriorityQueue avec un autre comparateur

public class Ex1207 static public void main(String[] args) PriorityQueue yodQueue = new PriorityQueue(new YodComparator()); yodQueue.add(new Person("Barrow",1630,1677)); yodQueue.add(new Person("Fermat",1601,1665)); yodQueue.add(new Person("Pascal",1623,1662)); yodQueue.add(new Person("Wallis",1616,1703)); System.out.println(yodQueue);

[Fermat(1601), Wallis(1616), Pascal(1623), Barrow(1630)]pop()=Fermat(1601): [Wallis(1616), Pascal(1623), Barrow(1630)]pop()=Wallis(1616): [Pascal(1623), Barrow(1630)]pop()=Pascal(1623): [Barrow(1630)]pop()=Barrow(1630): []

Page 249: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Implémentation directe 239

class YodComparator implements java.util.Comparator public int compare(Object o1, Object o2) if (o1==null || !(o1 instanceof Person)) return -1; Person p1 = (Person)o1; if (o2==null || !(o2 instanceof Person)) return 1; Person p2 = (Person)o2; if (p1.yod<p2.yod) return -1; if (p1.yod>p2.yod) return 1; return 0;

La classe PriorityQueue a recours aux méthodes de la classe TreeSet pour conserver l’équili-bre des éléments d’un arbre binaire de recherche. La méthode TreeSet.toString() dont ellehérite renvoie une représentation chaînée des éléments dans un ordre croissant déterminé par le com-parateur qui lui est associé.

Le comparateur de l’exemple 12.6 utilise le champ yob, c’est pourquoi Wallis(1616) s’afficheavant Pascal(1623). En revanche, le comparateur de l’exemple 12.7 utilise le champ yod, c’est pour-quoi Wallis(1616-1703) s’affiche après Pascal(1623-1662).

12.7 IMPLÉMENTATION DIRECTEL’implémentation de la classe PriorityQueue illustrée dans la section 12.5 a recours au framework decollections Java ; c’est pourquoi les instances de cette classe doivent stocker des objets. Dans l’exemplesuivant, vous allez pouvoir vous familiariser avec une implémentation des types primitifs.

Exemple 12.8 Interface IntPriorityQueueInterface

Dans cet exemple, vous constaterez que cinq méthodes sont nécessaires à l’implémentation d’unefile de priorité pour des entiers :

public interface IntPriorityQueueInterface public int top(); public int pop(); public void push(int x); public int size(); public String toString();

Exemple 12.9 Classe IntPriorityQueue

Cette classe utilise un tas pour implémenter l’interface définie dans l’exemple 12.8 :

public class IntPriorityQueue implements IntPriorityQueueInterface private final int CAPACITY=16; private int capacity, size; private int[] ints;

public IntPriorityQueue() capacity = CAPACITY; ints = new int[capacity+1];

[Pascal(1623-1662), Fermat(1601-1665), Barrow(1630-1677), Wallis(1616-1703)]

Page 250: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

240 Tas et files de priorité

public IntPriorityQueue(int capacity) this.capacity = capacity; ints = new int[capacity+1];

public int top() return ints[1];

public int pop() int x = ints[1]; ints[1] = ints[size--]; heapifyDown(); return x;

public void push(int x) if (size==capacity) resize(); ints[++size] = x; heapifyUp();

public int size() return size;

public String toString() if (size==0) return "[]"; String string = "[" + ints[1]; for (int i=2; i<=size; i++) string += "," + ints[i]; return string + "]";

private void heapifyUp() for (int j=size, i; j>0; j = i) i = j/2; if (ints[j]<ints[i]) swap(ints,i,j);

private void heapifyDown() for (int i=1, j; i<=size/2; i = j) j = 2*i; if (j<size && ints[j]>ints[j+1]) ++j; if (ints[j]<ints[i]) swap(ints,i,j);

private void resize() int[] temp = new int[capacity+1]; for (int i=1; i<=capacity; i++) temp[i] = ints[i]; capacity *= 2; ints = new int[capacity+1]; for (int i=1; i<=size; i++) ints[i] = temp[i];

Page 251: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Implémentation directe 241

private void swap(int[] a, int i, int j) int tmp=a[i]; a[i] = a[j]; a[j] = tmp;

Cette implémentation utilise un tableau pour stocker la file de priorité sous forme d’un tas. Le champsize stocke le nombre d’éléments présents dans cette file dans ints[1..size];. Cela signifieque l’implémentation utilise une indexation basée sur 1 et non sur 0 afin de simplifier les opérationsde tas. Il s’agit donc d’un mappage naturel (reportez-vous à l’algorithme 10.1 et à la section 12.2).Afin de faciliter la croissance du tableau, cette implémentation a recours au protocole de doublementde capacité, ce qui signifie qu’elle utilise un champ séparé, nommé capacity, pour déterminer lataille réelle du tableau ints[]. Étant donné que nous utilisons une indexation de base 1, la tailleréelle sera toujours égale à capacity+1. Le champ capacity est initialisé à l’aide de la capacitéconstante qui est égale à 16 dans le cas présent, mais pourrait être représentée par n’importe quelentier positif. Ainsi, le tableau ints[] commence à la taille 17, ce qui signifie que vous pouvezinsérer jusqu’à 16 éléments dans la file de priorité. Si la méthode push() est appelée lorsque size== capacity, la méthode private resize() est appelée afin de doubler la capacité du tas.L’implémentation de push() est conforme à l’algorithme de la section 12.3 et celle de pop() estconforme à l’algorithme de la section 12.4. Ces deux implémentations utilisent respectivement lesméthodes heapifyUp() et heapifyDown().

Exemple 12.10 Tester la classe IntPriorityQueue

Ce programme teste la classe IntPriorityQueue implémentée dans l’exemple 12.9 :

public class Ex1210 public static void main(String[] args) IntPriorityQueue queue = new IntPriorityQueue(); queue.push(50); queue.push(95); queue.push(70); queue.push(30); queue.push(90); queue.push(25); queue.push(55); queue.push(35); queue.push(80); queue.push(60); queue.push(40); queue.push(20); queue.push(10); queue.push(75); queue.push(45); queue.push(65); System.out.println(queue.size() + ": " + queue); queue.push(85); System.out.println(queue.size() + ": " + queue); queue.pop(); System.out.println(queue.size() + ": " + queue); while(queue.size()>0) System.out.println("queue.pop() = " + queue.pop()); System.out.println(queue.size() + ": " + queue);

Page 252: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

242 Tas et files de priorité

Après avoir instancié queue comme objet IntPriorityQueue, ce programme appelle sa méthodepush() afin d’insérer 17 éléments dans la file de priorité. Puis, la méthode println() appelle laméthode toString() de IntPriorityQueue afin d’imprimer les éléments en fonction de leurordre de stockage dans le tableau ints[]. Par exemple, la première ligne affichée

indique que la file est composée de 16 éléments, puis elle imprime ints[1..16]. Nous pouvons endéduire que la propriété de tas peut être vérifiée. Par exemple, le chemin racine-vers-feuilleints[1,2,4,8,16] correspond à la séquence croissante [10,35,50,65,95], et le cheminracine-vers-feuille ints[1,3,6,13] correspond à la séquence croissante [10,20,25,70](reportez-vous à l’exercice d’entraînement 12.7 si vous souhaitez consulter une trace complète de cetexemple).Quant à la deuxième ligne affichée, elle indique que la méthode private resize() fonctionneparfaitement. En effet, le dernier appel de la méthode push()

queue.push(85);

essaie d’insérer un élément alors que le tableau ints[] est plein (size == capacity). C’estpourquoi push() appelle resize() afin de doubler la capacité du tableau ints[] avant l’inser-tion.L’opération suivante appelle pop() dans le but de supprimer un élément de la file de priorité. Ellesupprime l’élément ayant la priorité la plus élevée (l’entier le plus petit), à savoir l’élément 10. Étantdonné que la file de priorité est implémentée sous forme d’un tas, l’élément le plus petit se trouve àla racine de ce dernier, c’est-à-dire à l’index 1 du tableau ints[]. L’algorithme de suppression(reportez-vous à la section 12.4) déplace le dernier élément dans la racine, puis il effectue une opé-ration heapifyDown() afin de rétablir la propriété de tas. L’élément 85 se trouve alors poussé enbas du tas. C’est pourquoi le chemin racine-vers-feuille ints[1,3,6,13] n’est plus égal à[85,20,25,30], mais à [20,25,30,85].En dernier lieu, une boucle while permet de supprimer tous les autres éléments de la file de prioritéen appelant pop() 16 fois. Vous remarquerez que les éléments sont dépilés dans un ordre croissant :20, 25, 30, 35, …, 95.

16: [10,35,20,50,40,25,45,65,80,90,60,70,30,75,55,95]17: [10,35,20,50,40,25,45,65,80,90,60,70,30,75,55,95,85]16: [20,35,25,50,40,30,45,65,80,90,60,70,85,75,55,95]queue.pop() = 20queue.pop() = 25queue.pop() = 30queue.pop() = 35queue.pop() = 40queue.pop() = 45queue.pop() = 50queue.pop() = 55queue.pop() = 60queue.pop() = 65queue.pop() = 70queue.pop() = 75queue.pop() = 80queue.pop() = 85queue.pop() = 90queue.pop() = 950: []

16: [10,35,20,50,40,25,45,65,80,90,60,70,30,75,55,95]

Page 253: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 243

La sortie de l’exemple 12.10 vous suggère une méthode efficace de tri d’une séquence consistant àpousser les données dans une file de priorité, puis à les dépiler. Si la file de priorité est implémentée àl’aide d’un tas, chaque appel de pop() est exécuté en O(lgn). Ainsi, le dépilement des n éléments estexécuté en O(n lgn).

Les n insertions sont exécutées en O(n), ce qui signifie que tout le processus de tri est exécuté en O(nlgn). Il s’agit de l’algorithme de tri vertical que nous aborderons plus en détail dans la section 13.8.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

12.1 Quelles sont les deux applications principales des tas ?

12.2 Quelle est la durée d’exécution des opérations d’insertion d’éléments dans un tas et de leur sup-pression ?

12.3 Pourquoi une file de priorité est-elle qualifiée de conteneur BIFO ?

12.4 Quelle est la différence entre une file et une file de priorité ?

12.5 Pourquoi les tas sont-ils utilisés lors de l’implémentation des files de priorité ?

12.6 Au cours du mappage naturel d’un arbre binaire dans un tableau a[], pourquoi devez-vous com-mencer à a[1] et non à a[0] ?

12.7 L’implémentation de la classe IntPriorityQueue reconstruit le tableau de stockage en dou-blant sa capacité lorsque la méthode push() est appelée et que le tableau est plein. Serait-il parconséquent possible de reconstruire le tableau en divisant sa capacité en deux lorsque la méthodepop() est appelée et que le tableau est seulement à moitié plein ?

RÉPONSES¿RÉPONSES

12.1 Les tas sont utilisés pour implémenter les files de priorité et le tri vertical (voir la section 13.8).

12.2 Les insertions et les suppressions effectuées dans le cadre d’un tas sont excessivement efficacespuisqu’elles ont une durée d’exécution égale à O(lgn).

12.3 Une file de priorité est un conteneur BIFO (Best-In-First-Out, première priorité, premier élémentsorti), c’est-à-dire que l’élément avec la priorité la plus élevée est le premier à sortir.

12.4 Les éléments sont retirés de la file dans l’ordre de leur insertion, c’est-à-dire premier entré, pre-mier sorti. En revanche, les éléments d’une file de priorité doivent avoir un champ clé ordinal quidéterminera la priorité en fonction de laquelle ils seront supprimés.

12.5 Les tas sont utilisés lors de l’implémentation des files de priorité parce qu’ils permettent O(lg n)insertions et suppressions. En effet, les fonctions push() et pop() sont implémentées selon unparcours du chemin racine-vers-feuille via le tas. Ces chemins ne peuvent pas être plus longs quela hauteur de l’arbre, c’est-à-dire lg n au maximum.

12.6 Le mappage naturel commence à a[1] et non à a[0] afin de faciliter le parcours ascendant etdescendant du tas (de l’arbre). Si vous attribuez le numéro 1 à la racine et que vous poursuivezséquentiellement par un parcours en largeur, le numéro de n’importe quel nœud parent k seraégal à k/2 et les numéros de ses enfants seront égaux à 2k et 2k + 1.

Page 254: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

244 Tas et files de priorité

12.7 La reconstruction du tableau de IntPriorityQueue lorsque la méthode push() est appeléeest nécessaire. En revanche, diviser par deux la capacité d’un tableau à moitié plein lorsque laméthode pop() est appelée n’est pas indispensable. Cette opération vous fera parfois économi-ser de l’espace de stockage, mais vous risquez surtout de faire augmenter sa durée d’exécution etsa complexité.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

12.1 Parmi les arbres binaires suivants, lesquels sont des tas ?

12.2 Parmi les tableaux suivants, lesquels ont la propriété de tas ?

12.3 Dessinez les tas après insertion de ces clés dans l’ordre suivant :

44, 66, 33, 88, 77, 55, 22

12.4 Dessinez les tableaux résultant du mappage naturel de chaque tas obtenu dans l’exercice 12.3.

12.5 Démontrez que chaque sous-arbre d’un tas est un tas.

12.6 Ajoutez la méthode suivante dans la classe IntPriorityQueue de l’exemple 12.9, puis testez-la :

• public boolean isHeap()• // renvoie true si et seulement si ints[] • // répond à la propriété de tas

12.7 Tracez l’exécution du programme de l’exemple 12.10 en illustrant le tas comme arbre binaire etcomme tableau après chaque modification.

12.8 Révisez la simulation d’une structure client/serveur de l’exemple 7.7 en remplaçant la filed’attente simple par une file de priorité. Définissez une classe Comparator pour la classeClient de façon à ce que les travaux d’impression les plus courts aient la priorité la plus élevée.

55

88

77 66

44 33

55

33 77

22 6644 88

88

77 66

55 3344 22

b. c.88

66 44

33 7755 33

a.

e. f.

55

88

77 66

44 33

d. 99

88 66

55 3344 22

77

7766 33 334488 5576543210

b. c.a.

3377 55 226688 4476543210

5544 22 667788 3376543210

3377 44 22558876543210

e. f.d.

4466 22 557788 3376543210

5577 33 662288 4476543210

Page 255: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 245

SOLUTIONS¿SOLUTIONS

12.1 a. Cet arbre n’est pas un tas parce que le chemin racine-vers-feuille 88, 44, 77 n’est pasdécroissant (44 < 77).

b. Cet arbre est un tas.

c. Cet arbre n’est pas un tas parce que le chemin racine-vers-feuille 55, 33, 44 n’est pasdécroissant (33 < 44) et que le chemin racine-vers-feuille 55, 77, 88 n’est pas décroissant(55 < 77 < 88).

d. Cet arbre n’est pas un tas parce qu’il n’est pas parfait.

e. Cet arbre est un tas.

f. Cet arbre n’est pas un tas parce qu’il n’est pas binaire.

12.2 a. Ce tableau n’a pas la propriété de tas parce que le chemin racine-vers-feuille a[1], a[3],a[6] = 88, 44, 77 n’est pas décroissant (44 < 77).

b. Ce tableau a la propriété de tas.

c. Ce tableau a la propriété de tas.

d. Ce tableau n’a pas la propriété de tas parce que ses éléments de données ne sont pas contigus ;il ne représente pas un arbre binaire parfait.

e. Ce tableau a la propriété de tas.

f. Ce tableau n’a pas la propriété de tas parce que le chemin racine-vers-feuille a[1], a[3],a[6] = 88, 22, 55 n’est pas décroissant (22 < 55) et que le chemin racine-vers-feuillea[1], a[3], a[7] = 88, 22, 66 n’est pas décroissant (22 < 66).

12.3 L’insertion des clés 44, 66, 33, 88, 77, 55, 22 dans un tas se présente de la façon suivante :

444466

44

66

66

44

3366

44 33

88

66

44 33

88

66

88 33

44

88

66 33

44

88

77 33

44

55

55

77

88

66 33

44 77 66

88

77 33

44 66

33

88

77 55

44 66

88

77 55

44 3366 22

22

Page 256: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

246 Tas et files de priorité

12.4 Les tableaux des tas de l’exercice 12.3 sont les suivants :

12.5 Théorème : chaque sous-arbre d’un tas est également un tas.

Démonstration : supposons que T soit un tas et que S soit un sous-arbre de T. Par définition, Test un arbre binaire parfait avec la propriété de tas. Donc, d’après le théorème de l’exer-cice 10.21, S est également un arbre binaire parfait. Supposons que x soit la racine de S et que psoit un chemin racine-vers-feuille dans S. Ensuite, x est un élément de T puisque S est un sous-arbre de T, et il existe un chemin unique q dans T qui va de x à la racine de T. En outre, p est unchemin de T qui connecte x à une feuille de T puisque S est un sous-arbre de T. Supposons que q–1

représente l’inverse du chemin q et que q–1p représente la concaténation de q–1 et de p dans T.Ensuite, q–1p est un chemin racine-vers-feuille dans T. Nous pouvons en déduire que les élémentssitués sur le chemin q–1p doivent être décroissants puisque T a la propriété de tas. Par conséquent,les éléments le long de p sont décroissants. S a donc la propriété de tas.

12.6 • public boolean isHeap()• for (int leaf=size/2+1; leaf<=size; leaf++)• for (int j=leaf, i; j>0; j = i)• i = j/2;• if (ints[j]<ints[i]) return false;• • return true;•

77 443388 66543210

44 4410

66 6644210

4466210

33 88

66 443388 77543210

66 44338843210

88 44336643210

44 88336643210

44 33663210

3377 44 225588 6676543210

3377 445588 666543210

5577 443388 666543210

22

77

55

q

p

xS

T

Page 257: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 247

12.7 La trace de l’exemple 12.10 est la suivante :

30

95

50

70

4

2

1

3

95 90

50

30

25

70

4 5 6

2

1

3

4513

7510

1013

60 201413

4010

35

95 90

50

25

70 55

30

8

4 5 6 7

2

1

3

95 80 60

50 90

35

25

70 55

30

8 10

2

1

3

6516

95 85

65 80 90 60

50 40

35

85

70 30 75 55

25 45

20

4 5 76

2

1

3

16 17

95 85

65 80 90 60

50 40

35

10

70 30 75 55

25 45

20

10 12 1411 13 15

2

1

3

16 17

95 80 90

50 40

35

25

70 55

30

8 98 9 10

2

1

3

95 80 90

50 60

35

25

70 55

30

10

2

1

3

60 701413

95 80 90

50 40

35

20

30 55

25

10

2

1

3

3013

60 701413

95 80 90

50 40

35

10

25 55

20

8 98 9

8 9

10

2

1

3

5513

7510

3013

60 701413

95 80 90

50 40

35

10

25 45

20

10

2

1

3

95

65 80 90 60

50 40

35

20

70 85 75 55

30 45

25

16

9

5 6 74

5 6 745 6 74

5 6 745 6 74

5 6 745 6 74

10 12 1411 13 15

2

1

3

8 910 12 1411 13 158 9

5 6 74

8 9

Page 258: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

248 Tas et files de priorité

12.8 Le programme révisé de simulation d’une structure client/serveur est le suivant :

• public class Pr1208• private static final int NUMBER_OF_SERVERS = 4;• private static final double MEAN_INTERARRIVAL_TIME = 5.0;• private static final int DURATION = 200;• private static Server[] servers = new Server[NUMBER_OF_SERVERS];• private static PriorityQueue clients• = new PriorityQueue(new ClientComparator());•• private static Random random• = new Random(MEAN_INTERARRIVAL_TIME);•• public static void main(String[] args)• for (int i=0; i<NUMBER_OF_SERVERS; i++)• servers[i] = new Server();• int timeOfNextArrival = random.intNextExponential();• for (int t=0; t<DURATION; t++)• if (t == timeOfNextArrival) // nouvelle arrivée• clients.push(new Client(t));• timeOfNextArrival += random.intNextExponential();• System.out.println(t + "\t" + clients);• • for (int i=0; i<NUMBER_OF_SERVERS; i++) // traiter • // les serveurs• if (servers[i].isFree())• if(!clients.isEmpty())• servers[i].beginServing((Client)clients.pop(),t);• • else if (t == servers[i].getTimeServiceEnds())• servers[i].endServing(t);• • • •• public class Server• private static Random randomMeanServiceRate• = new Random(1.00,0.20);• private static char nextId = ’A’;• private Random randomServiceRate;• private char id;• private double meanServiceRate, serviceRate;• private Client client;• private int timeServiceEnds;•• public Server()• id = (char)nextId++;• meanServiceRate = randomMeanServiceRate.nextGaussian();• randomServiceRate = new Random(meanServiceRate,0.10);• •• public Client getClient()• return client;• •• public void beginServing(Client client, int time)• this.client = client;• serviceRate = randomServiceRate.nextGaussian();• client.beginService(this,time);• int serviceTime = (int)Math.ceil(client.getJobSize()/serviceRate);

Page 259: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 249

• timeServiceEnds = time + serviceTime;• System.out.println(time + "\t" + id + " commence à servir "• + client);• •• public void endServing(int time)• client.endService(time);• System.out.println(time + "\t" + id + " finit de servir "• + client);• this.client = null;• •• public int getTimeServiceEnds()• return timeServiceEnds;• •• public boolean isFree()• return client == null;• •• public String toString()• int percentMeanServiceRate• = (int)Math.round(100*meanServiceRate);• int percentServiceRate = (int)Math.round(100*serviceRate);• return id + "(" + percentMeanServiceRate + "%,"• + percentServiceRate + "%)";• • •• public class ClientComparator implements java.util.Comparator• public int compare(Object o1, Object o2)• if (o1==null || !(o1 instanceof schaums.dswj.Client))• return -1;• Client c1 = (Client)o1;• if (o2==null || !(o2 instanceof Client)) return 1;• Client c2 = (Client)o2;• if (c1.getJobSize() < c2.getJobSize()) return -1;• if (c1.getJobSize() > c2.getJobSize()) return 1;• return 0;• • •• public class Client• public static final int MEAN_JOB_SIZE = 20;• private static Random randomJobSize = new Random(MEAN_JOB_SIZE);• private static int nextId = 0;• private int id, jobSize, tArrived, tBegan, tEnded;• private Server server;•• public Client(int time)• id = ++nextId;• jobSize = randomJobSize.intNextExponential();• tArrived = time;• System.out.println(time + "\t" + this + " arrive");• •• public double getJobSize()• return jobSize;• •

Page 260: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

250 Tas et files de priorité

• public int getWaitTime()• return tBegan - tArrived;• •• public int getServiceTime()• return tEnded - tBegan;• •• public void beginService(Server server, int time)• this.server = server;• tBegan = time;• •• public void endService(int time)• tEnded = time;• server = null;• •• public String toString()• return "#" + id + "(" + (int)Math.round(jobSize) + ")";• •• private static void print(int job, int time, double size)• System.out.println("Le travail #" + job + " arrive au temps "• + time + " avec " + (int)Math.round(size) + " pages.");• •• private static void printBegins(Server server, int job, int time)• System.out.println("L’imprimante " + server • + " commence le travail #" + job + " au temps " + time • + ".");• •• private static void printEnds(Server server, int job, int time)• System.out.println("L’imprimante " + server • + " termine le travail #" + job + " au temps " + time • + ".");• •

Page 261: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 13

Algorithmes de tri

Dans le chapitre 2, nous avons vu que toute recherche dans un tableau est beaucoup plus efficace si leséléments ont été triés, ce qui semblera évident à toute personne ayant déjà cherché un numéro de télé-phone dans un annuaire ou un mot dans un dictionnaire. Ce chapitre résume les 10 algorithmes les pluscouramment utilisés afin de trier une structure de données linéaire telle qu’un tableau ou une liste.Lorsqu’il s’agit de trier un tableau d’entiers, tous les algorithmes de tri sont implémentés à l’aide de lamême signature Java :

public static void sort(int[] a)

Cette signature peut aisément être modifiée pour trier un tableau d’un autre type primitif ou bien untableau d’objets implémentant l’interface Comparable. Les exercices figurant en fin de chapitre vouspermettront de vous familiariser avec les processus de tri des listes et d’autres structures. Le pseudo-codeet le code Java comprennent des conditions préalables, des conditions postérieures et des invariants deboucle permettant de prouver que les algorithmes sont corrects et d’analyser leur complexité. Les tris parpermutation ont recours à la méthode générale suivante :

private static void swap(int[] a, int i, int j) // CONDITIONS PREALABLES : 0 <= i < a.length; 0 <= j < a.length; // CONDITION POSTERIEURE : a[i] et a[j] sont permutés;if (i == j) return;int temp=a[j];a[j] = a[i];a[i] = temp;

Ce méthode ne fait qu’intervertir les i-ième et j-ième éléments du tableau.Dans le pseudo-code, nous utiliserons la notation s = s0, s1, …, sn – 1 pour une séquence de n élé-

ments. La notation sp… sq correspond à la sous-séquence sp, sp + 1, …, sq d’éléments de sp à sq. Dansles commentaires de code en Java, nous représenterons cette sous-séquence de la façon suivante :s[p..q]. Par exemple, s3…s7 et s[3..7] correspondent à la sous-séquence s3, s4, s5, s6, s7.

Sauf indication contraire, le terme « trié » fera systématiquement référence aux éléments de laséquence organisés selon un ordre croissant : s0 ≤ s1 ≤ s2 ≤ … ≤ sn – 1.

Page 262: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

252 Algorithmes de tri

13.1 MÉTHODE JAVA Arrays.sort()La bibliothèque de classes standard Java définit une méthode sort() dans la classe java.util.Arrays. Pour être exact, elle définit 18 versions surchargées de cette méthode, notamment quatre pourles tableaux d’objets et deux pour les tableaux de type primitif à l’exception de boolean. La signaturedes deux méthodes sort() des tableaux d’entiers est la suivante :

public static void sort(int[] a)public static void sort(int[] a, int p, int q)

La première signature trie la totalité du tableau et la seconde trie uniquement le sous-tableaua[p..q].

Exemple 13.1 Utiliser la méthode Arrays.sort()

public static void main(String[] args) int[] a = 77, 44, 99, 66, 33, 55, 88, 22 ; print(a); java.util.Arrays.sort(a); print(a);

private static void print(int[] a) for (int i=0; i<a.length; i++) System.out.print(a[i]+" "); System.out.println();

La méthode Arrays.sort() implémente l’algorithme 13.6 de tri par segmentation.

13.2 TRI PAR PERMUTATIONLe tri par permutation effectue n – 1 itérations dans une séquence de n élé-ments. À chaque itération, il compare les éléments adjacents par paire degauche à droite et intervertit les paires qui ne sont pas dans l’ordre. Les élé-ments les plus importants se trouvent ainsi déplacés vers la droite. Ce tri estégalement qualifié de tri à bulles parce que, si vous essayez de vous représen-ter les différents éléments dans une seule colonne, vous aurez l’impressionque chaque itération fait remonter les éléments les plus grands comme lesbulles d’une boisson gazeuse.

Algorithme 13.1 Tri par permutation.

(Condition préalable : s = s0, s1, …, sn – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : tout la séquence s est triée.)

1. Effectuez les étapes 2 à 4 de i = n – 1 à 1.2. Effectuez l’étape 3 de j = 1 à i.3. Si les deux éléments sj – 1 et sj ne sont pas dans l’ordre, permutez-les.4. (Invariants : la sous-séquence sj…sn – 1 est triée et si = maxs0...si.)

77 44 99 66 33 55 88 2222 33 44 55 66 77 88 99

Page 263: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri par permutation 253

Exemple 13.2 Tri par permutation

public static void sort(int[] a) // CONDITION POSTERIEURE : a[0] <= a[1] <= ... <= a[a.length-1]; for (int i=a.length-1; i>0; i--) // étape 1 for (int j=1; j<=i; j++) // étape 2 if (a[j-1]>a[j]) swap(a,j-1,j); // étape 3 // INVARIANTS : a[i] <= a[i+1] <= ... <= a[a.length-1]; // a[i] >= a[j] pour tout j < i;

Exemple 13.3 Tracer le tri par permutation

Vous trouverez ci-après le tri par permutation d’un tableau composé des entiers 77, 44, 99, 66, 33,55, 88, 22 :

Théorème 13.1 : le tri par permutation est correct.Reportez-vous à la solution de l’exercice d’entraînement 13.14 pour consulter la démonstration de cethéorème.

Théorème 13.2 : le tri par permutation a une durée d’exécution égale à O(n2).Reportez-vous à la solution de l’exercice d’entraînement 13.15 pour consulter la démonstration de cethéorème.

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7]

77 44 99 66 33 55 88 22

44 77

66 99

33 99

55 99

88 99

22 99

66 77 99

33 77 99

55 77 99

22 88 99

33 66 88 99

55 66 88 99

22 77 88 99

33 44 77 88 99

22 66 77 88 99

22 55 66 77 88 99

22 44 55 66 77 88 99

22 33 44 55 66 77 88 99

Page 264: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

254 Algorithmes de tri

13.3 TRI PAR SÉLECTIONLe tri par sélection est similaire au tri par permuta-tion. Il effectue n – 1 itérations dans une séquencede n éléments, en déplaçant à chaque passage le plusimportant des éléments non triés afin de le mettre à sa place. Cependant, cet algorithme de tri est plusefficace que le précédent parce qu’il ne déplace aucun élément au cours de sa recherche de l’élément leplus important. Il n’effectue donc qu’un seul échange à chaque itération après avoir trouvé l’élément quin’est pas dans l’ordre. Il porte ce nom parce qu’il sélectionne le plus important des éléments non triésrestants à chaque itération et qu’il l’insère à l’emplacement correct.

Algorithme 13.2 tri par sélection.

(Condition préalable : s = s0, s1, …, sn – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : tout la séquence s est triée.)

1. Effectuez les étapes 2 à 4 de i = n – 1 à 1.2. Recherchez l’index j de l’élément le plus important parmi s0...si..3. Intervertissez si et sj.4. (Invariants : la sous-séquence sj…sn – 1 est triée et si = maxs0...si.)

Exemple 13.4 Tri par sélection

public static void sort(int[] a) // CONDITION POSTERIEURE : a[0] <= a[1] <= ... <= a[a.length-1]; for (int i=a.length-1; i>0; i--) // étape 1 int j=0; // étape 2 for (int k=1; k<=i; k++) if (a[k] > a[j]) j = k; // INVARIANT : a[j] >= a[k] pour tout k <= i; swap(a,i,j); // étape 3 // INVARIANTS : a[i] >= a[k] pour tout k <= i; // a[i] <= a[i+1] <= ... <= a[a.length-1];

Exemple 13.5 Tracer le tri par sélection

Vous trouverez ci-après la trace du tri par sélection appliqué au tableau précédent composé desentiers 77, 44, 99, 66, 33, 55, 88, 22 :

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7]

77 44 99 66 33 55 88 22

22 99

88 99

55 77 88 99

33 66 77 88 99

33 55 66 77 88 99

22 44 55 66 77 88 99

22 33 44 55 66 77 88 99

Page 265: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri par insertion 255

Théorème 13.3 : le tri par sélection est correct.Reportez-vous à la solution de l’exercice d’entraînement 13.19 pour consulter la démonstration de cethéorème.

Théorème 13.4 : le tri par sélection a une durée d’exécution égale à O(n2).Reportez-vous à la solution de l’exercice d’entraînement 13.20 pour consulter la démonstration de cethéorème.

Bien que le tri par permutation et le tri par sélection aient la même fonction de complexité, ledeuxième d’entre eux est exécuté plus rapidement, comme l’illustrent les deux traces. En effet, le tri parpermutation a effectué 18 échanges, tandis que celui par sélection n’en a réalisé que sept. En fait, le tripar sélection présente l’avantage d’intervertir des éléments éloignés les uns des autres en une seule opé-ration quand le tri par permutation doit en effectuer plusieurs (reportez-vous à l’exercice 11.8).

13.4 TRI PAR INSERTIONComme les deux algorithmes précédents, l’algo-rithme de tri par insertion effectue n – 1 itérationsdans une séquence de n éléments. À chaque itéra-tion, il insère l’élément suivant dans le sous-tableausitué à gauche qui est donc trié. Une fois le dernierélément inséré, la totalité du tableau est triée.

Algorithme 13.3 Tri par insertion.

(Condition préalable : s = s0, s1, …, sn – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : tout la séquence s est triée.)

1. Effectuez les étapes 2 à 4 de i = n – 1 à 1. 2. Stockez l’élément si dans un espace temporaire.3. Recherchez l’index j le moins important pour lequel sj >= si.4. Remontez d’une position la sous-séquence sj…si – 1 jusqu’à sj + 1…si.5. Copiez la valeur stockée de si dans sj.6. (Invariant : la sous-séquence s0…si est triée).

Exemple 13.6 Tri par insertion

public static void sort(int[] a) // CONDITION POSTERIEURE : a[0] <= a[1] <= ... <= a[a.length-1]; for (int i=1; i<a.length; i++) // étape 1 int temp=a[i], j; // étape 2 for (j=i; j>0 && a[j-1]>temp; j--) // étape 3 a[j] = a[j-1]; // étape 4 a[j] = temp; // étape 5 // INVARIANT : a[0] <= a[1] <= ... <= a[i];

Exemple 13.7 Tracer le tri par insertion

Vous trouverez ci-après la trace du tri par insertion appliqué au tableau précédent composé desentiers 77, 44, 99, 66, 33, 55, 88, 22 :

Page 266: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

256 Algorithmes de tri

Théorème 13.5 : le tri par insertion est correct.Reportez-vous à la solution de l’exercice d’entraînement 13.23 pour consulter la démonstration de cethéorème.

Théorème 13.6 : le tri par sélection a une durée d’exécution égale à O(n2).Reportez-vous à la solution de l’exercice d’entraînement 13.24 pour consulter la démonstration de cethéorème.

Théorème 13.7 : le tri par sélection a une durée d’exécution égale à O(n) sur une séquence triée.Reportez-vous à la solution de l’exercice d’entraînement 13.25 pour consulter la démonstration de cethéorème.

13.5 TRI SHELLLe théorème 13.7 suggère que si la séquence estpresque triée, l’algorithme de tri par insertion a unedurée d’exécution de O(n), ce qui est vrai. Le tri Shell exploite cette caractéristique pour créer un algo-rithme généralement exécuté en O(n1.5). En effet, il applique plusieurs fois le tri par insertion aux sous-séquences d’incrément telles que s0, s3, s6, s9, …, sn – 2 et s1, s4, s7, s10, …, sn – 1 qui représententdeux des trois sous-séquences d’incrément 3 possibles.

Algorithme 13.4 Tri Shell.

(Condition préalable : s = s0, s1, …, sn – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : tout la séquence s est triée.)

1. Définissez d = 1 ;2. Répétez l’étape 3 jusqu’à ce que 9d > n ;3. Définissez d = 3d + 1 ;4. Effectuez les étapes 5 à 6 jusqu’à ce que d = 0 ;5. Appliquez l’algorithme de tri par insertion à chacune des d sous-séquences d’incrément d de s ;6. Définissez d = d/3.

Exemple 13.8 Tri Shell

Supposons que s soit composée de n = 200 éléments. La boucle de l’étape 2 serait alors répétée troisfois, ce qui ferait augmenter d = 1 à d =4, 13 et 40.

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7]

77 44 99 66 33 55 88 22

44 77

66 77 99

33 44 66 77 99

55 66 77 99

88 99

22 33 44 55 66 77 88 99

Page 267: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri Shell 257

La première itération de la boucle ayant lieu pendant l’étape 4 appliquerait le tri par insertion à cha-cune des 40 sous-séquences d’incrément 40 s0, s40, s80, s120, s160, s1, s41, s81, s121, s161, s2, s42,s82, s122, s162, …, s39, s79, s119, s159, s199. L’étape 6 réduirait alors d à 13, puis la deuxième itérationde la boucle de l’étape 4 appliquerait le tri par insertion à chacune des treize sous-séquences d’incré-ment 13 s0, s13, s26, s39, s52, s65, …, s194, s1, s14, s27, s40, s53, s66, …, s195, …, s12, s25, s38, s51, s64,s77, …, s193. L’étape 6 réduirait ensuite d à 4 et la troisième itération de la boucle de l’étape 4 appli-querait le tri par insertion à chacune des quatre sous-séquences d’incrément 4 s0, s4, s8, s12, …,s196, s1, s5, s9, s13, . . ., s197, s2, s6, s10, s14, . . ., s198 et s3, s7, s11, s15, . . ., s199. L’étape 6 rédui-rait ensuite d à 1 et la quatrième itération de boucle de l’étape 4 appliquerait le tri par insertion àtoute la séquence. Pour résumer, ce processus appliquerait le tri par insertion 58 fois : 40 fois auxsous-séquences de taille n1 = 5, 13 fois aux sous séquences de taille n2 = 15, 4 fois aux sous-séquen-ces de taille n3 = 50 et une fois à toute la séquence de taille n4 = 200.De prime abord, vous pourriez penser qu’il est plus long d’utiliser le tri par insertion dans le cadre dutri Shell plutôt que d’appliquer le tri par insertion directement à toute la séquence en une seule fois.Et effectivement, un calcul direct du nombre total de comparaisons nécessaires dans l’exemple 13.8à l’aide de la fonction de complexité n2 crée le résultat suivant :

qui est nettement supérieur à

n2 = 2002 = 40 000

Cependant, après la première itération de l’étape 4, les sous-séquences suivantes sont presque triées.Le nombre réel de comparaisons serait donc égal à :

ce qui est largement plus intéressant que 40 000.

Théorème 13.8 : le tri Shell a une durée d’exécution égale à O(n1.5).Vous remarquerez que, pour n = 200, n1.5 = 2001.5 = 2 829, soit un résultat nettement meilleur quen2 = 2002 = 40 000.

Exemple 13.9 Tri Shell

public static void sort(int[] a) int d=1, j, n=a.length; // étape 1 while (9*d<n) // étape 2 d = 3*d + 1; // étape 3 while (d>0) // étape 4 for (int i=d; i<n; i++) // étape 5 int temp=a[i]; j=i; while (j>=d && a[j-d]>temp) a[j] = a[j-d]; j -= d; a[j] = temp; d /= 3; // étape 6

40 n12( ) 13 n2

2( ) 4 n32( ) 1 n4

2( )+ + + 40 52( ) 13 152( ) 4 502( ) 1 2002( )+ + + 53 925= =

40 n12( ) 13 n2( ) 4 n3( ) 1 n4( )+ + + 40 52( ) 13 15( ) 4 50( ) 1 200( )+ + + 1 595= =

Page 268: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

258 Algorithmes de tri

13.6 TRI PAR FUSIONLe tri par fusion applique la stratégie « diviser pourmieux régner » lorsqu’il met une séquence en ordre.En effet, il commence par sous-diviser la séquenceen sous-séquences composées de singletons. Puis, ilfusionne successivement les sous-séquences parpaire jusqu’à ce qu’une nouvelle séquence uniquesoit à nouveau formée. Chaque fusion conservant l’ordre précédent, chaque sous-séquence fusionnée esttriée. Une fois la dernière fusion terminée, toute la séquence est triée.

Bien qu’il puisse être implémenté itérativement, l’algorithme de tri est naturellement récursifpuisqu’il consiste à diviser la séquence en deux, à trier chaque moitié, puis à les fusionner à nouveau enconservant leur ordre. Vous obtenez votre base lorsque la sous-séquence ne contient qu’un seul élément.

Algorithme 13.5 tri par fusion.

(Condition préalable : s = s0, s1, …, sn – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : tout la séquence s est triée.)

1. Si n > 1, effectuez les opérations 2 à 5.2. Fractionnez s en deux sous-séquences : a = s0…sm – 1 et b = sm…sn – 1, avec m = n/2.3. Triez a.4. Triez b.5. Reformez s en fusionnant a et b, ce qui préservera l’ordre de la séquence.

Exemple 13.10 Tri par fusion

public static void sort(int[] a) // CONDITION POSTERIEURE : a[0] <= a[1] <= ... <= a[a.length-1]; if (n>1) sort(a,0,a.length); // étape 1

public static void sort(int[] a, int k, int n) // CONDITIONS PREALABLES : 2 <= n <= k+n <= a.length; // CONDITION POSTERIEURE : a[k] <= a[k+1] <= ... <= a[k+n-1]; if (n<2) return; sort(a,k,n/2); // étapes 2 et 3 sort(a,k+n/2,n-n/2); // étape 4 merge(a,k,n); // étape 5

public static void merge(int[] a, int k, int n) // CONDITIONS PREALABLES : 1 <= n <= k+n <= a.length; // a[k] <= a[k+1] <= ... <= a[k+n/2-1]; // a[k+n/2] <= ... <= a[k+n-1]; // CONDITION POSTERIEURE : a[k] <= a[k+1] <= ... <= a[k+n-1]; int[] temp = new int[n]; int i=0, lo=k, hi=k+n/2; while (lo<k+n/2 && hi<k+n) temp[i++] = ( a[lo]<= a[hi] ? a[lo++] : a[hi++] ); // INVARIANT : temp[0..i] est trié while (lo<k+n/2) temp[i++] = a[lo++]; // INVARIANT : temp[0..i] est trié while (hi<k+n) temp[i++] = a[hi++]; // INVARIANT : temp[0..i] est trié

Page 269: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri par fusion 259

for (i=0; i<n; i++) a[k+i] = temp[i];

La méthode principale sort() trie la totalité du tableau en appelant la méthode surchargée sort()à l’aide des paramètres de l’index k de départ et de la longueur n du sous-tableau. Cette méthode àtrois paramètres trie le sous-tableau en commençant par la moitié gauche, puis en passant à la moitiédroite avant de procéder à la fusion de ces deux parties.La méthode merge() fusionne les deux moitiés s[k..m-1] et s[m..k+n-1] dans un tableautemporaire, m étant l’index central m = k + n/2. À chaque itération, la première boucle whilecopie le plus petit des deux éléments a[lo] et a[hi]. L’opérateur de post-incrémentation avanceautomatiquement l’index de l’élément copié. Une fois que tous les éléments d’une moitié ont étécopiés, la première boucle while s’arrête, puis l’une des deux autres boucles while copie les élé-ments restants dans temp[]. En dernier lieu, tous les éléments sont copiés à nouveau dans a[].

Exemple 13.11 Tracer le tri par fusion

Vous trouverez ci-après une trace du tri par fusion appliqué à notre tableau de huit entiers :

La figure ci-contre illustre la méthode de division, puis de fusion des sous-tableaux.

Théorème 13.9 : le tri par fusion est exécuté en O(n lgn).Démonstration : en général, le tri par fusion fonctionne en divisant à plusieurs reprises le tableau endeux jusqu’à ce que les éléments soient des singletons, puis en fusionnant ces éléments par paire jusqu’àce qu’il ne reste plus qu’un seul élément, comme illustré dans le diagramme suivant. Le nombre d’itéra-tions de la première partie est égal au nombre de fois où n peut être divisé par deux, c’est-à-dire lg n – 1.En ce qui concerne le nombre et la taille des éléments, la deuxième partie du processus inverse la pre-mière. Par conséquent, la deuxième partie se déroule également en lg n – 1 étapes. C’est pourquoi la tota-lité de l’algorithme est effectuée en O(lg n) étapes, chacun d’entre elles comparant tous les éléments n.Le nombre total de comparaisons est donc égal à O(n lg n).

Théorème 13.10 : le tri par fusion est correct.Démonstration : notre démonstration se base sur les conditions préalables et postérieures données dansle code. Dans la méthode principale sort(), le tableau est déjà trié si sa longueur est égale à 0 ou 1. Sitel n’est pas le cas, la condition postérieure de la méthode sort() à trois paramètres garantit que letableau sera trié une fois qu’elle aura terminé parce que c’est tout le tableau qui lui est passé. Cettecondition postérieure est identique à celle de la méthode merge() qui est appelée en dernier. C’estpourquoi il ne nous reste plus qu’à démontrer que la condition postérieure de cette méthode est vraie.Cette condition est issue des trois invariants de boucle. En effet, une fois que les boucles sont terminées,

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7]

77 44 99 66 33 55 88 22

44 77

66 99

66 77

22 44

22 33 44 44 55 66 77 88

4477 6699 5533 2288

77 44 99 66 33 55 88 22

77 44 99 66 33 55 88 22

77 44 99 66 33 55 88 22

44 77 66 99 33 55 22 88

44 66 77 99 22 33 55 88

22 33 44 55 66 77 88 99

Page 270: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

260 Algorithmes de tri

le tableau temp[] est trié et il est copié dans a[], son ordre étant toujours le même. Il ne nous restedonc qu’à vérifier les trois invariants de boucle. Supposons que, au cours du traitement de l’une des troisboucles while, un élément y soit copié dans temp[] alors qu’il est inférieur à un élément x copié aupa-ravant. Cela signifierait que x a été copié de l’autre moitié du tableau puisque les deux moitiés avaientdéjà été triées et que cette opération a eu lieu au cours de la première boucle while. Sans être trop pré-cis, nous pouvons donc supposer que y était dans la première moitié et x dans la seconde. Maintenant, siy < x, tous les éléments de a[k] à y doivent également être inférieurs à x. Cependant, l’affectation

temp[i++] = ( a[lo]<=a[hi] ? a[lo++] : a[hi++] );

copie systématiquement l’élément le plus petit en premier lieu ; elle doit donc avoir copié tous les élé-ments de a[k] à y dans temp[] avant d’avoir copié x. Cela va à l’encontre de notre supposition et véri-fie par conséquent l’invariant de boucle.En mettant en œuvre sa stratégie « diviser pour mieux régner », l’algorithme de tri par fusion obtient unedurée d’exécution de O(n lgn), soit une amélioration considérable par rapport au O(n2) des algorithmesprécédents. Cette stratégie consiste à :

1. Fractionner la séquence en deux sous-séquences.

2. Trier chaque sous-séquence séparément.3. Fusionner les deux sous-séquences de façon à reformer une seule séquence.

Le tri par fusion effectue la première étape de la façon la plus simple et la plus équilibrée qui soit : il frac-tionne la séquence en son milieu. Si cette première étape est réalisée différemment, vous obtiendrezd’autres algorithmes de tri. La stratégie « diviser pour mieux régner » est également utilisée dans l’algo-rithme de recherche binaire (algorithme 2.2). La méthode la plus simple de fractionnement de laséquence de façon déséquilibrée consiste à insérer tous les éléments, sauf le dernier, dans la premièresous-séquence et d’insérer uniquement le dernier élément restant dans la seconde. Vous obtenez ainsi laversion récursive du tri par insertion (reportez-vous à l’exercice d’entraînement 13.22). Un autreméthode de fractionnement non équilibrée consiste à insérer uniquement l’élément le plus importantdans la deuxième sous-séquence et de laisser ainsi tous les autres éléments dans la première. Vous obte-nez ainsi la version récursive du tri par sélection (reportez-vous à l’exercice d’entraînement 13.18). Vousremarquerez que cette méthode fait de l’étape 3 un véritable jeu d’enfant puisqu’il vous suffit d’associerl’élément le plus important à la fin de la première sous-séquence. Une quatrième méthode de fractionnement de la séquence est également à votre disposition : elle consisteà diviser la séquence de façon à ce que chaque élément de la première sous-séquence soit inférieur à cha-que élément de la seconde. Cette condition était bien évidemment vraie dans le cas précédent qui nous aconduit au tri par sélection récursif. Cependant, lorsque vous obtenez cette propriété et que vos deuxsous-séquences ont la même taille, vous utilisez un nouvel algorithme O(n lgn) connu sous le nom de tripar segmentation.

13.7 TRI PAR SEGMENTATIONComme le tri par fusion, le tri par segmentationest récursif et requiert une fonction auxiliaireavec plusieurs boucles. En outre, il a égalementune complexité O(n lg n). Cependant, dans la plu-part des cas, il est plus rapide que le tri par fusion.

Il départage le tableau en deux parties séparées par un seul élément supérieur à tous les éléments dela partie gauche et inférieur à tous les éléments de la partie droite. Cette technique vous permet de vousassurer que l’élément seul, qualifié d’élément pivot, est à la place voulue. L’algorithme applique ensuitela même méthode aux deux éléments séparés. Il est donc naturellement récursif et très rapide.

yx

k lo hi

0 i

ya

temp

x

Page 271: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri par segmentation 261

Algorithme 13.6 Tri par segmentation.

(Condition préalable : s = s0, s1, …, sn – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : tout la séquence s est triée.)

1. Si n > 1, effectuez les étapes 2 à 5.

2. Appliquez la partition rapide (voir l’algorithme 13.7) à s afin d’obtenir l’élément pivot si.

3. (Invariant : l’élément pivot si se trouve à son emplacement trié.)

4. Appliquez le tri par segmentation à a = s0, s1, …, si – 1.

5. Appliquez le tri par segmentation à b = si + 1, si + 2, …, sn – 1.

Algorithme 13.7 Partition rapide.

(Condition préalable : s = sk, sk + 1, sk + 2, …, sk + n – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : k ≤ j et sp ≤ sj ≤ sq pour k ≤ p < j < q < k + n.)

1. Définissez x = sk (l’élément pivot).

2. Définissez i = k et j = k + n.

3. Répétez les étapes 4 à 7 tant que i < j.

4. Continuez à incrémenter i tant que si < x.

5. Continuez à décrémenter j tant que sj > x.

6. Permutez si et sj.

7. (Invariant : sp ≤ x ≤ sq pour k ≤ p < i et j < q < k + n.)

8. Permutez sk et sj.

9. (Invariant : sp ≤ sj ≤ sq pour k ≤ p < j < q < k + n.)

Exemple 13.12 Tri par segmentation

public static void sort(int[] a) // CONDITION POSTERIEURE: a[0] <= a[1] <= ... <= a[a.length-1]; if (a.length>1) sort(a,0,a.length); // étape 1 algorithme 13.6

public static void sort(int[] a, int k, int n) // CONDITIONS PREALABLES : 0 <= k <= k+n <= a.length; // CONDITION POSTERIEURE : a[k] <= a[k+1] <= ... <= a[k+n-1]; if (n<2) return; int pivot = a[k]; // étape 1 algorithme 13.7 int i = k; int j = k+n; while (i < j) // étape 3 algorithme 13.7 while (i+1<k+n && a[++i]<pivot) ; // étape 4 algorithme 13.7 while (a[--j]>pivot) ; // étape 5 algorithme 13.7 if (i < j) swap(a,i,j); // étape 3 algorithme 13.7 // INVARIANT : a[p]<=pivot<=a[q] pour k<=p=<i<j=<q<k+n // étape 7 algorithme 13.7

swap(a,k,j); // étape 8 algorithme 13.7 // INVARIANT : a[p]<=a[j]<=a[q] pour k<p<j<q<k+n // étape 9 algorithme 13.7 sort(a,k,j-k); // étape 4 algorithme 13.6 sort(a,j+1,k+n-j-1); // étape 5 algorithme 13.6

Page 272: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

262 Algorithmes de tri

Exemple 13.13 Tracer le tri par segmentation

Vous trouverez ci-après la trace du tri par segmentation de notre tableau composé de huit entiers :

L’algorithme 13.7 sélectionne l’élément pivot comme dernier élément de la séquence. Il fonctionne-rait tout aussi bien si le pivot était inséré comme premier élément ou comme élément central. Vousobtiendrez des performances légèrement meilleures si vous sélectionnez la moyenne de ces trois élé-ments.

La méthode Arrays.sort() implémente le tri par segmentation et sélectionne le pivot qui est égalà la moyenne des trois éléments s0, sn/2, sn – 1 quand n ≤ 40 et celui qui est égal à la moyenne des 9éléments à intervalle égal lorsque n > 40. Elle a recours au tri par insertion (algorithme 13.3) lorsquen < 7.

Théorème 13.11 : le tri par segmentation est exécuté en O(n lgn) dans le meilleur des cas.Démonstration : le meilleur des cas correspond aux situations dans lesquelles les valeurs de la séquencesont distribuées uniformément de façon aléatoire, chaque appel de l’algorithme de tri par segmentationcréant ainsi un fractionnement équilibré de la séquence. Le tri fractionné divise alors la séquence en deuxsous-séquences de longueur presque égale. Comme pour la recherche binaire (algorithme 2.2) et le tripar fusion (algorithme 13.5), cette sous-division répétée a besoin de lgn étapes pour arriver à une seulesous-séquence (voir la figure suivante).

Par conséquent, nous avons effectué O(lgn) appels de l’algorithme de tri par segmentation qui est exé-cuté en O(n). La durée d’exécution totale de ce tri est donc égale à O(n lgn).

Théorème 13.12 : le tri par segmentation est exécuté en O(n2) dans le pire des cas.

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7]

77 44 99 66 33 55 88 22

22 99

55 77

33 66 77

33 55 77 88

22 44 55 77 88

22 33 55 77 88 99

22 33 44 55 66 77 88 99

lg n

n

Page 273: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri vertical 263

Démonstration : le pire des cas correspond aux situations dans lesquelles la séquence est déjà triée (outriée à l’envers). Le tri par segmentation sélectionne alors systématiquement le dernier élément (ou lepremier si la séquence est triée à l’envers), créant ainsi le fractionnement le moins déséquilibré possible :une partie contient n – 2 éléments et l’autre 1 seul. Ce type de division sera répété O(n) fois jusqu’à ceque les deux sous-séquences ne comportent plus qu’un seul élément. Par conséquent, nous avons effec-tué O(n) appels de l’algorithme de tri par segmentation qui est exécuté en O(n). La durée d’exécutiontotale de ce tri est donc égale à O(n2).Vous remarquerez que, dans le pire des cas, l’algorithme de tri par segmentation devient algorithme de tripar sélection (algorithme 13.2) parce que chaque appel du tri par segmentation revient à sélectionnerl’élément le plus important de la sous-séquence qui lui est passée. Ainsi, en fait, le théorème 13.12 est uncorollaire du théorème 13.4.

Théorème 13.13 : le tri par segmentation a en moyenne une durée d’exécution égale à O(n lgn).La démonstration de ce théorème implique des notions bien plus complexes que celles présentées dansce livre d’initiation.

Théorème 13.14 : le tri par segmentation est correct.Démonstration : l’invariant qui se trouve dans la démonstration de la boucle while indique que tous leséléments situés à gauche de a[i] sont inférieurs ou égaux à l’élément pivot, et que tous les élémentssitués à droite de a[j] sont supérieurs ou égaux au pivot. Cette affirmation est vraie parce que chaqueélément supérieur au pivot et situé à gauche de a[i] a changé de place avec un élément inférieur aupivot et situé à droite de a[j]. Et inversement, chaque élément inférieur au pivot et situé à droite dea[j] a changé de place avec un élément supérieur au pivot et situé à gauche de a[i]. Lorsque cetteboucle se termine, j ≤ i, ce qui signifie que tous les éléments qui sont supérieurs au pivot ont été dépla-cés à droite de a[i] et que tous les éléments inférieurs au pivot ont été déplacés à gauche de a[i]. Ils’agit de l’invariant présent à l’étape 7 de l’algorithme de partition rapide. Après la permutation del’étape 8, tous les éléments qui sont supérieurs à a [i] se trouvent sur sa droite, et tous les éléments quisont inférieurs à a[i] se trouvent sur sa gauche. Il s’agit toujours de l’invariant présent à l’étape 7 del’algorithme de partition rapide, invariant identique à celui de l’étape 3 du tri par segmentation. Parconséquent, le tri indépendant du segment de droite et du segment de gauche vous permet de trier toutela séquence.

13.8 TRI VERTICALPar définition, un tas est partiellement trié parce que chaque chaîne linéaire de la racine à la feuille esttriée, d’où l’algorithme de tri vertical. Comme tous les algorithmes de tri, il s’applique à un tableau (ouà un vecteur), mais la structure sous-jacente du tas représentée par le tableau est utilisée afin de définircet algorithme.À l’instar du tri par fusion et du tri par segmentation, le tri vertical utilise une fonction auxiliaire appeléedepuis la fonction sort() et a une fonction de complexité O(n lg n). Cependant, contrairement à cesdeux algorithmes, le tri vertical n’est pas récursif.

Le tri vertical consiste essentiellement à charger n éléments dans un tas, puis à les décharger. D’aprèsle théorème 12.1, chaque élément est chargé en O(lgn) et déchargé en O(lgn). La durée d’exécution pourn éléments est donc égale à O(n lgn).

Algorithme 13.8 Tri vertical.

((Condition préalable : s = s0, s1, …, sn – 1 est une séquence de n valeurs ordinales.)(Condition postérieure : tout la séquence s est triée.)

1. Effectuez les opérations 2 à 3 pour i = n/2 – 1 jusqu’à 0.

Page 274: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

264 Algorithmes de tri

2. Appliquez l’algorithme de la fonction heapify à la sous-séquence si, si + 1, …, sn – 1.

3. (Invariant : dans s, chaque chemin racine-vers-feuille est décroissant.)

4. Effectuez les opérations 5 à 7 pour i = n –1 jusqu’à 1.

5. Permutez si et s0.

6. (Invariant : la sous-séquence si, si + 1, …, sn – 1 est triée.)

7. Appliquez l’algorithme de la fonction heapify à la sous-séquence s0, s1, …, si – 1.

Algorithme 13.9 heapify(Conditions préalables : ss = si, si + 1, …, sj – 1 est une sous-séquence des valeurs ordinales de j – iavec 0 ≤ i < j ≤ n ; et les deux sous-séquences si + 1, …, sj – 1 et si + 2, …, sj – 1 ont la propriété de tas.)(Condition postérieure : ss a la propriété de tas.)

1. Suposons que t = s2i + 1.

2. Supposons que sk = max s2i + 1, s2i + 2, donc k = 2i + 1 ou 2i + 2, l’index de l’enfant le plusimportant.

3. Si t < sk, effectuez les étapes 4 à 6.

4. Définissez si = sk.

5. Définissez i = k.

6. Si i < n/2 et si < maxi < max s2i + 1, s2i + 2, répétez les étapes 1 à 4.

7. Définissez sk = t.

Ces algorithmes se distinguent des méthodes du chapitre 12 sur deux points. D’une part, les tas sontdans l’ordre inverse et chaque chemin racine-vers-feuille est décroissant. Et d’autre part, ces algorithmesutilisent une indexation basée sur 0. Grâce à l’ordre inversé, la fonction heapify laissera systématique-ment l’élément le plus important à la racine de la sous-séquence. En outre, l’utilisation d’une indexationbasée sur 0 et non sur 1 permet d’avoir recours à une méthode sort() cohérente avec toutes les autresméthodes sort(), même si cela signifie que votre code sera légèrement plus compliqué.

Exemple 13.14 Tri vertical

public static void sort(int[] a) // CONDITION POSTERIEURE : a[0] <= a[1] <= ... <= a[a.length-1]; for (int i=a.length/2-1; i>=0; i--) // étape 1 heapify(a,i,n); // étape 2 for (int i=a.length-1; i>0; i--) // étape 4 swap(a,0,i); // étape 5 heapify(a,0,i); // étape 7 private static void heapify(int[] a, int i, int j) int tmp=a[i], k; // étape 1 while (2*i+1<j) k=2*i+1; if (k+1<j && a[k+1] > a[k]) ++k; // a[k] est l’enfant le // plus important if (tmp>=a[k]) break; // étape 3 a[i] = a[k]; // étape 4 i = k; // étape 5 a[k] = tmp; // étape 7

Vous remarquerez que la méthode heapify() utilisée ici équivaut à la méthode heapifyDown()de l’exemple 12.9.

Page 275: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri vertical 265

Exemple 13.15 Tracer le tri vertical

Vous trouverez ci-après la trace du tri vertical du tableau 77, 44, 99, 66, 33, 55, 88, 22, 44 :

Chaque zone qui apparaît en grisé correspond au résultat de l’appel de fonction heapify(). Cetappel a lieu une fois au cours de l’étape 2, puis n – 2 fois au cours de l’étape 7. La fonction sort()commence par convertir le tableau de façon à ce que l’arbre binaire sous-jacent complet soit trans-formé en tas. Pour cela, la fonction heapify() est appliquée à chaque sous-arbre non trivial, c’est-à-dire aux sous-arbres composés de plus d’un élément et dont la racine est située au-dessus du niveaude la feuille. Dans le tableau, les feuilles sont stockées aux emplacements a[n/2] à a[n]. C’estpourquoi la première boucle for de la fonction sort() applique heapify() aux élémentsa[n/2-1] en remontant jusqu’à a[0] (c’est-à-dire la racine de l’arbre sous-jacent). Vous obtenezalors un tableau dont l’arbre correspondant a la propriété de tableau, comme illustré dans la figureci-après :

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

77 44 99 66 33 55 88 22 44

66 44

99 66 88 44 33 55 77 22 44

44 99

88 66 77 44 33 55 44 22

22 88

77 66 55 44 33 22 44

44 77

66 44 55 44 33 22

22 66

55 44 22 44 33

33 55

44 44 22 33

33 44

44 33 22

22 44

33 22

22 33

99

8866

44 33

22 44

55 77

0

87

3 4 5 6

1 2

44228

777

556

335

444

883

662

9910

Page 276: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

266 Algorithmes de tri

La deuxième boucle for, qui est en fait la boucle principale, effectue maintenant n – 1 itérations,chacune de ces dernières réalisant les deux opérations suivantes : elle permute l’élément racine etl’élément a[i], puis elle applique la fonction heapify() au sous-arbre des éléments a[0:i-1].Ce sous-arbre est composé de la partie encore non triée du tableau. Avant l’exécution de swap() àchaque itération, le sous-tableau a[0:i] a la propriété de tas, c’est pourquoi a[i] est son élémentle plus important. Cela signifie que swap() insère l’élément a[i] à l’emplacement correct.

Les sept premières itérations de la boucle for créent le résultat illustré par les sept figures suivantes :

88

7766

44 33

22 99

55 44

0

87

3 4 5 6

1 2

99228

447

556

335

444

773

662

8810

77

5566

44 33

88 99

22 44

0

87

3 4 5 6

1 2

99888

447

226

335

444

553

662

7710

66

5544

44 33

88 99

22 77

0

87

3 4 5 6

1 2

99888

777

226

335

444

553

442

6610

55

2244

44 33

88 99

66 77

0

87

3 4 5 6

1 2

99888

777

666

335

444

223

442

5510

44

2244

33 55

88 99

66 77

0

87

3 4 5 6

1 2

99888

777

666

555

334

223

442

4410

440

Page 277: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri vertical 267

Le tableau (et son arbre binaire correspondant imaginaire) est divisé en deux parties : la première estle sous-tableau a[0:i-1] qui a la propriété de tas, et la seconde le sous-tableau restant a[i:n-1]dont les éléments se trouvent à l’emplacement correct. Cette deuxième partie apparaît en grisé dansl’illustration précédente. Chaque itération de la boucle for principale décrémente la taille de la pre-mière partie et incrémente celle de la seconde. Ainsi, une fois que la boucle a terminé le processus,la première partie est vide et la seconde, triée, constitue la totalité du tableau. Cette analyse vient denous permettre de démontrer que le tri vertical fonctionne.

Théorème 13.15 : le tri vertical est correct.

Reportez-vous à la solution 13.1 pour consulter la démonstration de ce théorème.

Théorème 13.16 : le tri vertical est exécuté en O(n lgn).

Démonstration : chaque appel de la fonction heapify() est effectué en lg n étapes au maximum parcequ’il est uniquement itéré le long d’un chemin allant de l’élément courant jusqu’à une feuille. Le pluslong chemin de ce type pour un arbre binaire complet de n éléments est lg n. La fonction heapify() estappelée n/2 dans la première boucle for et

n – 1 fois dans la seconde boucle for, ce qui équivaut à une durée inférieure à (3n/2) lg n, qui est propor-tionnelle à n lg n.

Si vous utilisez l’algorithme de tri comme le traitement d’un flux au cours duquel les éléments sont insé-rés dans un tableau de façon aléatoire, puis en sont extraits une fois qu’ils ont été triés, le tri vertical peutêtre considéré comme un juste milieu entre le tri par sélection et le tri par insertion. En effet, le tri parsélection est réalisé cours de l’étape de suppression, une fois que les éléments ont été stockées dansl’ordre non trié dans lequel ils arrivent. Quant au tri par insertion, il est effectué au cours de l’étaped’insertion afin que les éléments puissent être extraits du tableau dans l’ordre trié dans lequel ils ont étéstockés. Le tri vertical présente l’avantage supplémentaire d’effectuer un tri partiel en insérant les élé-ments dans un tas, puis en finissant l’opération de tri lorsque les éléments sont supprimés du tas. Cettesolution intermédiaire est nettement plus efficace puisqu’elle est exécutée en O(n lg n) au lieu de O(n2).

44

2233

44 55

88 99

66 77

0

87

3 4 5 6

1 2

99888

777

666

555

444

223

332

4410

22

4433

44 55

88 99

66 77

0

87

3 4 5 6

1 2

99888

777

666

555

444

443

332

2210

Page 278: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

268 Algorithmes de tri

13.9 RAPIDITÉ DES ALGORITHMES DE TRI PAR COMPARAISON

Théorème 13.17 : aucun algorithme de tri permettant la réorganisation du tableau en comparant seséléments ne peut avoir de fonction de complexité meilleure que O(n lg n) dans le pire des cas.Démonstration : prenons l’exemple d’un arbre de décision couvrant tous les résultats possibles del’algorithme pour un tableau de taille n. Étant donné que l’algorithme réorganise le tableau en comparantses éléments, chaque nœud de l’arbre de décision représente une condition de la forme suivante : (a[i]< a[j]). Puisque ce type de condition peut créer deux résultats différents, true ou false, nous avonsaffaire à un arbre binaire. En outre, l’arbre couvrant toutes les réorganisations possibles, il doit avoir aumoins n! feuilles. Ainsi, d’après le corollaire 10.3, la hauteur de l’arbre de décision doit être au moinségale à lg(n!). Dans le pire des cas, le nombre de comparaisons effectuées par l’algorithme est identiqueà celui de l’arbre de décision. Ainsi, la fonction de complexité de l’algorithme dans le pire des cas doitêtre O(lg(n!)).Maintenant, d’après la formule de Stirling (voir le théorème A.11),

donc

Dans le cas présent, « log » signifie logarithme binaire lg = log2. La fonction de complexité de l’algo-rithme dans le pire des cas doit donc être O(n lg n).

Le théorème 13.17 s’applique uniquement aux algorithmes de tri par comparaison, c’est-à-dire auxalgorithmes qui trient les éléments en comparant leurs valeurs, puis en modifiant leurs emplacementsrelatifs en fonction du résultat de ces comparaisons. Tous les algorithmes que nous venons de voir fontpartie de cette catégorie. Nous allons maintenant étudier deux autres types d’algorithmes qui n’ont pasrecours aux comparaisons.

13.10 TRI DIGITALLa méthode de tri digital implique l’utilisation d’un tableau lexicographique de taille constante commetype d’élément de la séquence, c’est-à-dire d’un type chaîne ou d’un type entier. Supposons que r soit labase de numération associée à l’élément du tableau (r = 26 pour les chaînes de caractères ASCII, r = 10pour les entiers décimaux, r = 2 pour les chaînes de bits), et supposons que w soit la largeur constante dutableau lexicographique. Si nous prenons l’exemple d’un numéro de sécurité sociale français, d = 10 etw = 13.

Exemple 13.16 Trier des livres par ISBN

Tous les livres publiés depuis 1970 se voient attribuer un numéro unique qualifié de numéro ISBNpour International Standard Book Number. Ce numéro est généralement imprimé en bas du verso dela couverture d’un livre. Par exemple, l’ISBN de cet ouvrage est 0071361286. Une dernière préci-sion, les ISBN sont généralement composés de quatre groupes de chiffres séparés par des traitsd’union de la façon suivante : 0-07-136128-6. Le dernier chiffre est utilisé dans un but de vérifica-tion ; il est formé en additionnant les neuf autres. Étant donné qu’il peut s’agir de n’importe quel

n! 2n ne---

n

n!( )log 2n ne---

n

nn( )log≈log≈ n nlog=

Page 279: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri digital 269

chiffre de 0 à 10 ou bien de la lettre X, nous obtenons la base r = 11 et la largeur d = 10 puisque lenuméro est composé de 10 chiffres.

Algorithme 13.10 Méthode de tri digital.

(Condition préalable : s = s0, s1, s2, …, sn – 1 est une séquence de n entiers ou chaînes de caractèreavec une base de numération r et une largeur w.)(Condition postérieure : la séquence s est triée par ordre alphabétique.)

1. Répétez l’étape 2 pour d = 0 et ce jusqu’à w – 1.2. Appliquer un algorithme de tri stable à la séquence s en triant uniquement le nombre d. Un algo-

rithme de tri est dit stable s’il conserve l’ordre relatif des éléments avec des clés égales. Parexemple, le tri par insertion est un algorithme stable.

Exemple 13.17 Trier les ISBN à l’aide de la méthode de tri digital

Voici les trois premières itérations du tri digital appliqué à une séquence de 21 ISBN :

Cette figure illustre l’importance de la stabilité qui permet de conserver l’ordre des itérations précé-dentes. Par exemple, après la première itération, 8838650527 précède 0830636528 parce que7 < 8. Ces deux clés ont la même valeur 2 comme deuxième chiffre en partant de la droite (d = 1).Ainsi, lors de la deuxième itération, qui trie uniquement le chiffre 1, ces deux clés sont évaluéescomme étant égales. Cependant, leur ordre ne doit pas être modifié parce que 27 < 28. La stabilitégarantit le respect de cet ordre.Les colonnes qui ont été traitées apparaissent en grisé. Après la troisième itération,les sous-séquences de 3 chiffres situées les plus à droite sont triées : 109 < 13X< 286 < 373 < 453, etc. Vous remarquerez que X correspond à la valeur 10,13X signifiant 130 + 10 = 140 d’un point de vue numérique.Une fois les sept itérations terminées, la séquence a l’aspect suivant :

Exemple 13.18 Méthode de tri digital

Cette méthode suppose que les constantes RADIX et WIDTH ont déjà été définies.Par exemple, dans le cas de tableaux d’entiers :

public static final int RADIX=10;public static final int WIDTH=10;

public static void sort(int[] a) // CONDITION POSTERIEURE : a[0] <= a[1] <= ... <= a[a.length-1]; for (int d=0; d<WIDTH; d++) // étape 1 sort(a,d); // étape 2

private static void sort(int[] a, int d) // CONDITION POSTERIEURE : a[] est trié de façon stable pour d;

0071342109007052713X00713612860070308373007135345388386504540071353461083062847988386505270830636528

007134210988386505270830636528007052713X007135345388386504540071353461007030837308306284790070308683

007030837300713534610071342109007135345300703086830071361286007052713X083063652808306284798838650527

0071353461007030837300713534530070308683883865045497420805850071361286883865052708306365280830628479

0830628479083063652800703083730070308683007052713X00713534610071342109007135345300713612868838650454

Page 280: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

270 Algorithmes de tri

int[] c = new int[RADIX]; for (int i=0; i<a.length; i++) ++c[digit(d,a[i])]; // compter les valeurs de a[] for (int j=1; j<RADIX; j++) c[j] += c[j-1]; // c[j] == nbre éléments dans a[] qui sont <= j int[] tmp = new int[a.length]; for (int i=a.length-1; i>=0; i--) tmp[--c[digit(d,a[i])]] = a[i]; for (int i=0; i<a.length; i++) a[i] = tmp[i];

private static int digit(int d, int x) // c’est-à-dire que le chiffre(3,1234567890) renvoie 7; return x/(int)Math.pow(10,d)%RADIX;

La deuxième méthode de tri est qualifiée de tri de comptage.

Théorème 13.18 : la méthode de tri digital est exécutée en O(n).Démonstration : l’algorithme a des itérations WIDTH et traite les n éléments trois fois lors de chaque ité-ration. La durée d’exécution est donc proportionnelle à WIDTH*n et elle est constante. Bien que O(n) soitthéoriquement mieux que O(n lgn), cette méthode est rarement plus rapide que les algorithmes de tri O(nlgn), c’est-à-dire ceux de tri par fusion, par segmentation et vertical. En effet, elle perd beaucoup detemps à extraire les chiffres et à copier les tableaux.

13.11 TRI PANIERLe tri panier fait également partie de la catégorie des tris par distribution parce qu’il répartit les élémentsdans des paniers en fonction d’un critère déterminé, puis qu’il applique un autre algorithme de tri à cha-que panier. Il est similaire au tri par segmentation sur un point : tous les éléments du panier i sont supé-rieurs ou égaux à tous les éléments du panier i – 1, et inférieurs ou égaux à tous les éléments du panieri + 1. Cependant, alors que le tri par segmentation sépare la séquence en deux paniers, le tri panier lasépare en n paniers.

Algorithme 13.11 Tri panier.

(Condition préalable : s = s0, s1, s2, …, sn – 1 est une séquence de n valeurs ordinales avec unevaleur minimum (min) et une valeur maximum (max) connues.)(Condition postérieure : la séquence s est triée.)

1. Initialisez un tableau de n paniers (collections).2. Répétez l’étape 3 pour chaque si de la séquence.3. Insérez si dans le panier j, avec j = rn, r = (si – min)/(max + 1 – min).4. Triez chaque panier.5. Répétez l’étape 6 pour j de 0 à n – 1.6. Insérez à nouveau les éléments du panier j séquentiellement dans s.

Exemple 13.19 Trier des numéros d’identification composés de 9 chiffres à l’aide dutri panier

Supposons que vous ayez 1 000 numéros d’identification composés de 9 chiffres. Vous allez paramétrer1 000 tableaux de type int, puis répartir les nombres à l’aide de la formule j = rn, r = (si – min)/(max + 1 – min) = (si – 0)/(109+ 1 – 0) si/109. Ainsi, par exemple, le numéro d’identification

Page 281: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tri panier 271

666666666 serait inséré dans le panier numéro j, avec j = rn = (666666666/109)(103) =666,666666 = 666. De la même façon, le numéro d’identification 123456789 serait inséré dans lepanier numéro 123, et le numéro d’identification 666543210 serait inséré dans le panier 666.Chaque panier serait alors trié. Vous remarquerez que le nombre d’éléments présents dans chaquepanier est en moyenne égal à 1, c’est pourquoi le choix de cet algorithme de tri n’influera pas sur ladurée d’exécution.En dernier lieu, les éléments sont recopiés dans s en commençant par le panier numéro 0.

Exemple 13.20 Tri panier

public static void sort(int[] a) // CONDITION POSTERIEURE : a[0] <= a[1] <= ... <= a[a.length-1]; int min=minimum(a); int max=maximum(a); int n=a.length; Bucket[] bucket = new Bucket[n]; // étape 1 for (int j=0; j<n; j++) bucket[j] = new Bucket(); for (int i=0; i<n; i++) // étape 2 int j = n*(a[i]-min)/(max+1-min)); bucket[j].add(a[j]); // étape 3 int i=0; for (int j=0; j<n; j++) Bucket bucketj=bucket[j]; bucketj.sort(); // étape 4

000284791000550376002846113004479198004760115004766632005503276

000284791000550376

002846113

Panier #0

Panier

#

#

#

1

Panier 2

Panier

#

3

004479198004760115004766632

005503276

Panier #4

Panier 5

Panier #6

0123456

Page 282: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

272 Algorithmes de tri

for (int k=0; k<bucketj.size(); k++) // étape 5 a[i++] = bucketj.get(k);

Ce programme requiert l’implémentation de l’interface suivante :

public interface Bucket public void add(int x); // ajoute x à la fin du panier public int get(int k); // renvoie l’élément k du panier public int size(); // renvoie le nombre d’éléments

Il doit également implémenter deux méthodes locales :

public int minimum(int[] a); // renvoie la plus petite valeur de a[]public int maximum(int[] a); // renvoie la plus grande valeur de a[]

Théorème 13.19 : le tri panier a une durée d’exécution égale à O(n).Démonstration : l’algorithme est composé de trois boucles parallèles, chacune étant répétée n fois. Ladernière boucle contient une boucle interne, mais elle n’effectue en moyenne qu’une seule itération. Lesméhodes minimum() et maximum() sont également exécutées toutes les deux en n étapes. C’est la rai-son pour laquelle le nombre d’étapes exécutées est proportionnel à 5n.

À l’instar de la méthode de tri digital, l’algorithme de tri panier O(n) est beaucoup plus lent que lesalgorithmes de tri O(n lgn) dans la pratique.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

13.1 Pourquoi le tri par permutation est-il lent ?

13.2 La démonstration du théorème 13.2 conclut que le tri par permutation effectue n(n – 1)/2 compa-raisons. Comment en arrive-t-on à la conclusion que sa fonction de complexité est O(n2) ?

13.3 Pourquoi les algorithmes de tri O(n) tels que la méthode de tri digital et le tri panier sont-ils pluslents que les algorithmes de tri O(n lg n) tels que le tri par fusion, le tri par segmentation et le trivertical ?

13.4 Le tri par fusion applique la stratégie « diviser pour mieux régner » afin de trier un tableau. Eneffet, il divise ce dernier en plusieurs parties et s’applique récursivement à chacune d’entre elles.Quels autres algorithmes de tri ont également recours à cette stratégie ?

13.5 Quels algorithmes de tri sont aussi efficaces sur une liste chaînée que sur les tableaux ?

13.6 Quels algorithmes de tri ont une complexité moyenne différente de leur complexité dans le piredes cas ?

13.7 Quels algorithmes de tri ont une complexité moyenne différente de leur complexité dans lemeilleur des cas ?

13.8 Pourquoi la version non récursive d’un algorithme de tri récursif est-elle généralement plus effi-cace ?

13.9 Quels sont les points communs entre les algorithmes de tri par segmentation et par fusion ?

13.10 Dans quelles conditions est-il préférable d’appliquer le tri par fusion plutôt que les deux autresalgorithmes O(n lg n) ?

Page 283: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 273

13.11 Dans quelles conditions le tri par segmentation est-il similaire au tri par sélection ?

13.12 Dans quelles conditions est-il préférable d’appliquer le tri par segmentation plutôt que les deuxautres algorithmes O(n lg n) ?

13.13 Quels sont les points communs entre le tri vertical et les tris par sélection et par insertion ?

13.14 Quel algorithme l’API Java utilise-t-elle pour implémenter ses méthodes java.util.Arrays.sort() ?

13.15 Un algorithme de tri est considéré comme stable s’il préserve l’ordre des éléments égaux. Parmiles algorithmes de tri que nous avons vus, lesquels ne sont pas stables ?

13.16 Parmi les neuf algorithmes de tri que nous venons de voir, lesquels ont besoin d’un espace sup-plémentaire pour les tableaux ?

13.17 Parmi les neuf algorithmes que nous venons de voir, lesquels seraient les mieux adaptés à unfichier externe d’enregistrements ?

13.18 Le tri par fusion est parallélisable, c’est-à-dire que plusieurs étapes peuvent être effectuéessimultanément, indépendamment les unes des autres, à condition que l’ordinateur ait plusieursprocesseurs susceptibles d’être exécutés en parallèle. Ce procédé fonctionne pour le tri par fusionparce que plusieurs parties différentes du tableau peuvent être sous-divisées ou fusionnées indé-pendamment des autres parties. Quels autres algorithmes de tri décrits dans ce chapitre sont éga-lement parallélisables ?

13.19 Imaginez un site web avec un applet Java pour chaque algorithme de tri. Ce site indique le fonc-tionnement de l’algorithme en affichant l’animation d’une exécution test appliquée à un tableaua[] de 256 nombres aléatoires dans un intervalle de 0.0 à 1.0. Cette animation affiche à chaqueitération de la boucle principale un graphique bidimensionnel composé de 256 points (x, y), unpour chaque élément du tableau, avec x = i+1 et y = a[i]. Chaque graphique correspond aurésultat obtenu à la moitié du processus de tri pour chacun des algorithmes suivants :

• Tri par sélection

• Tri par insertion

• Tri par fusion

• Tri par segmentation

• Tri vertical

• Méthode de tri digital

Associez chacun des graphiques suivants à l’algorithme de tri qui l’a créé :

Page 284: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

274 Algorithmes de tri

RÉPONSES¿RÉPONSES

13.1 Le tri par permutation est lent parce qu’il n’opère que localement. Chaque élément ne bouge qued’une place à la fois. Par exemple, l’élément 99 de l’exemple 13.1 est déplacé par six appels dif-férents de la fonction swap() pour arriver à la place voulue, soit a[8].

13.2 Le passage de n(n – 1)/2 à O(n2) se justifie de la façon suivante :

a. Pour les valeurs importantes de n (c’est-à-dire n > 1000), n(n – 1)/2 est presque identiqueà n2/2.

b. Une fonction de complexité est utilisée uniquement pour les comparaisons. Par exemple, com-bien de temps prendrait le tri d’un tableau deux fois plus grand ? Pour cette analyse, les fonc-tions proportionnelles sont équivalentes. En outre, étant donné que n2/2 est proportionnel à n2,nous pouvons laisser tomber la division 1/2 et simplifier notre conclusion qui devient O(n2).

13.3 Les algorithmes de tri O(n) (méthode de tri digital et tri panier) sont plus lents que les algorith-mes de tri O(n lg n) (tri par fusion, par segmentation et vertical) parce que, malgré une duréed’exécution proportionnelle à n, la constante de la proportion est en grande partie due à des trai-tements supplémentaires. Ainsi, pour les deux algorithmes de tri O(n), tous les éléments doiventêtre copiés dans une liste de files ou de tableaux à chaque itération, puis recopiés dans laséquence initiale.

13.4 Les algorithmes de tri par fusion et par segmentation, ainsi que celui de tri panier mettent enœuvre la stratégie « diviser pour mieux régner ».

13.5 Les algorithmes de tri par permutation, par sélection, par insertion, par fusion et par permutationsont aussi efficaces sur des listes chaînées que sur des tableaux.

13.6 Le tri par permutation et le tri panier sont nettement plus lents dans le pire des cas.

13.7 Le tri par insertion, le tri Shell et la méthode de tri digital sont nettement plus rapides dans lemeilleur des cas.

13.8 La récursivité présente l’inconvénient d’implémenter de nombreux appels récursifs de méthodes.

13.9 Ces algorithmes mettent tous les deux en œuvre la stratégie « diviser pour mieux régner », maisde façon différente. Ainsi, le tri par permutation commence par effectuer une division O(lgn) dela séquence, puis il trie récursivement chaque sous-séquence indépendamment. Quant au tri parfusion, il procède dans l’ordre inverse. En effet, il commence par effectuer deux appels récursifs,puis il fusionne les deux parties en une durée O(lgn). Ces deux algorithmes effectuent donc O(n)opérations un nombre O(lgn) de fois, d’où une complexité égale à O(n lgn).

13.10 Le tri par fusion est mieux adapté au tri des listes chaînées et des fichiers externes.

13.11 Le tri par segmentation inverse le tri par sélection dans le pire des cas, c’est-à-dire lorsque laséquence est déjà triée.

13.12 Le tri par segmentation est mieux adapté au tri des tableaux de taille importante comportant destypes primitifs.

13.13 Le tri par sélection peut être considéré comme un processus de tri des sorties. En effet, vous insé-rez les éléments dans un tableau selon l’ordre dans lequel ils vous sont donnés, puis vous sélec-tionnez à plusieurs reprises l’élément suivant le plus important. À l’inverse, le tri par sélectionpeut être considéré comme un processus de tri des entrées. En effet, vous insérez chaque élémentdans le tableau à son emplacement trié correct, puis vous supprimez tous les éléments selonl’ordre du tableau. Ainsi, le tri par sélection insère les éléments dans le tableau en une durée O(n)et il les supprime en O(n2), tandis que le tri par insertion insère les éléments dans le tableau enO(n2) et les supprime en O(n). Le résultat obtenu dans les deux cas est un algorithme O(n2).

Page 285: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 275

Quant au tri vertical, il peut être considéré comme un processus de tri mi-entrées, mi-sorties puis-que vous insérez les éléments dans un tableau tout en conservant la propriété de tas (partielle-ment trié) et que vous sélectionnez ensuite le premier élément (en fait le plus petit) afin derétablir la propriété de tas. Les opérations d’insertion et de suppression ont toutes les deux unedurée d’exécution égale à O(n lgn), soit une durée d’exécution totale de O(n lgn).

13.14 L’API Java applique le tri par fusion pour implémenter les méthodes Arrays.sort() auxtableaux d’objets, et le tri par segmentation pour implémenter ses méthodes Arrays.sort()aux tableaux composés de types primitifs.

13.15 Les tris Shell et vertical, ainsi que le tri par segmentation sont instables.

13.16 Le tri par fusion, le tri digital et le tri panier requièrent un stockage supplémentaire dans letableau.

13.17 Les tris par permutation, par sélection, par insertion, par fusion et par segmentation pourraientêtre appliqués à des fichiers externes d’enregistrements.

13.18 Les tris par fusion et par segmentation, ainsi que les tris Shell et panier seraient exécutés nette-ment plus rapidement sur un ordinateur parallèle.

13.19 Chaque algorithme est indiqué sous le graphique qu’il a créé :

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

13.1 Si un algorithme O(n2), par exemple le tri par permutation, celui par sélection ou celui par inser-tion, prend 3,1 millisecondes pour traiter un tableau de 200 éléments, combien de temps pren-drait le traitement d’un tableau similaire de :

a. 400 éléments ?b. 40 000 éléments ?

13.2 Si un algorithme O(n lg n), par exemple le tri par fusion, le tri par segmentation ou le tri vertical,prend 3,1 millisecondes pour traiter un tableau de 200 éléments, combien de temps prendrait letraitement d’un tableau similaire de 40 000 éléments ?

13.3 Le tri par insertion est exécuté de façon linéaire sur un tableau déjà trié, mais que se passe-t-illorsque le tableau est trié à l’envers ?

Tri par fusion Tri vertical Tri digital

Tri par segmentation Tri par sélection Tri par insertion

Page 286: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

276 Algorithmes de tri

13.4 Comment le tri par permutation procède-t-il dans le cas :

a. d’un tableau déjà trié ?

b. d’un tableau trié à l’envers ?

13.5 Comment le tri par sélection procède-t-il dans le cas :

a. d’un tableau déjà trié ?

b. d’un tableau trié à l’envers ?

13.6 Comment le tri par fusion procède-t-il dans le cas :

a. d’un tableau déjà trié ?

b. d’un tableau trié à l’envers ?

13.7 Comment le tri par segmentation procède-t-il dans le cas :

a. d’un tableau déjà trié ?

b. d’un tableau trié à l’envers ?

13.8 Comment le tri vertical procède-t-il dans le cas :

a. d’un tableau déjà trié ?

b. d’un tableau trié à l’envers ?

13.9 Les tris par permutation, par sélection et par insertion sont tous des algorithmes O(n2). Lequeld’entre eux est le plus rapide et lequel est le plus lent ?

13.10 Le tri par fusion, le tri par segmentation et le tri vertical sont tous des algorithmes O(n lg n).Lequel d’entre eux est le plus rapide et lequel est le plus lent ?

13.11 Tracez le tri du tableau suivant :

• int a[] = 44, 77, 55, 99, 66, 33, 22, 88, 77

pour chacun des algorithmes suivants :

a. Tri par permutation

b. Tri par sélection ;

c. Tri par insertion ;

d. Tri par fusion ;

e. Tri par segmentation ;

f. Tri vertical.

13.12 Modifiez le tri par permutation de façon à ce qu’il trie le tableau en ordre décroissant.

13.13 Modifiez le tri par permutation de façon à ce qu’il soit capable de s’arrêter dès que le tableau esttrié.

13.14 Démontrez le théorème 13.1.

13.15 Démontrez le théorème 13.2.

13.16 Le tri cocktail shaker améliore légèrement le tri par permutation dans la mesure où il alterne lamigration des éléments les plus importants vers la fin du tableau avec la remontée des élémentsles plus petits vers le haut de ce tableau. En tenant compte de cette définition, implémentezl’algorithme de tri cocktail shaker et déterminez s’il est plus efficace qu’un simple tri par inser-tion.

13.17 Modifiez le tri par sélection (algorithme 13.2) pour qu’il utilise le plus petit élément de si..sn – 1au cours de l’étape 2.

13.18 Réécrivez le tri par sélection récursivement.

13.19 Démontrez le théorème 13.3.

13.20 Démontrez le théorème 13.4.

Page 287: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 277

13.21 Modifiez le tri par insertion de façon à ce qu’il trie le tableau indirectement. Vous devrez utiliserun tableau d’index distinct dont les valeurs correspondront aux index des éléments de donnéesréels. Ce tri indirect réorganisera donc le tableau d’index sans modifier le tableau de données.

13.22 Réécrivez le tri par insertion récursivement.

13.23 Démontrez le théorème 13.5.

13.24 Démontrez le théorème 13.6.

13.25 Démontrez le théorème 13.7.

13.26 Modifiez le tri par segmentation de façon à ce qu’il sélectionne sont pivot comme dernier élé-ment et non comme premier élément de la sous-séquence.

13.27 Modifiez le tri par segmentation de façon à ce qu’il sélectionne un pivot égal à la moyenne destrois éléments suivants : le premier, le dernier et celui du milieu.

13.28 Modifiez le tri par segmentation de façon à ce qu’il inverse le tri par insertion lorsque la taille dutableau est inférieure à 8.

13.29 Étant donné que le tri vertical a une durée d’exécution égale à O(n lgn), pourquoi n’est-il pasappliqué plus souvent que le tri par segmentation qui est exécuté en O(n2) dans le pire des cas ?

13.30 Étant donné que le tri vertical a une durée d’exécution égale à O(n lgn) et qu’il ne requiert aucunespace de tableau supplémentaire, pourquoi n’est-il pas appliqué plus souvent que le tri parfusion qui ne nécessite qu’un espace de stockage double pour le tableau ?

13.31 Démontrez le théorème 13.15.

13.32 L’algorithme de tri Las Vegas appliqué au tri d’un jeu de cartes est le suivant :

1. Battre les cartes de façon aléatoire.

2. Si le jeu n’est pas trié, répéter l’étape 1.

Déterminez la fonction de complexité de cet algorithme de tri.

SOLUTIONS¿SOLUTIONS

13.1 L’algorithme O(n2) prendrait :

a. 12,4 millisecondes (4 fois plus longtemps) pour traiter un tableau de 400 éléments ;

b. 124 secondes (40 000 fois plus longtemps) pour traiter un tableau de 40 000 éléments, soitenviron 2 minutes. Cette réponse peut être calculée mathématiquement de la façon suivante.Le temps de traitement t est proportionnel à n2, il y a donc une constante c pour laquellet = c·n2. S’il faut t = 3,1 millisecondes pour trier n = 200 éléments, alors (3,1 millisecondes) =c·(200 éléments)2, alors c = (3,1 millisecondes)/(200 éléments)2 = 0,0000775 millisecondes/élément2. Ensuite, pour n = 40 000, t = c·n2 = (0,0000775 millisecondes/élément2) (40 000 élé-ments)2 = 124 000 millisecondes = 124 secondes.

13.2 L’algorithme O(n lg n) prend 1,24 secondes (400 fois plus longtemps) pour traiter un tableaude 40 000 éléments. Cette réponse peut être calculée mathématiquement de la façon suivante.Le temps de traitement t est proportionnel à n lg n, il existe donc une constante c pour laquellet = cn lg n. S’il faut t = 3,1 millisecondes pour trier n = 200 éléments, (3,1 millisecondes) =c(200)lg(200), donc c = (3,1 millisecondes)/(200lg(200)) = 0,0155/lg(200). Ensuite, pourn = 40 000, t = cn lg n = (0,0155/lg(200))(40 000lg(40 000)) = 620(lg(40 000)/lg(200)). Main-tenant, 40 000 = 2002, donc lg(40 000) = lg(2002) = 2lg200. Ainsi, lg(40 000)/lg(200) = 2, donct = 6202 millisecondes = 1 240 millisecondes = 1,24 secondes.

Page 288: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

278 Algorithmes de tri

13.3 Le tri par insertion n’est pas du tout efficace lorsqu’il est appliqué à un tableau trié à l’enversparce que chaque nouvel élément inséré force tous les éléments sur sa gauche à bouger d’uneplace sur la droite.

13.4 Le tri par permutation tel qu’il est implémenté dans l’algorithme 13.1 est insensible à l’entrée,c’est-à-dire qu’il effectuera le même nombre n(n – 1)/2 de comparaisons quel que soit l’ordre ini-tial des éléments du tableau. Le fait que le tableau soit déjà trié ou non, ou qu’il soit trié àl’envers n’a donc aucune importance ; cette méthode reste très lente.

13.5 Le tri par sélection est également insensible aux entrées, ce qui signifie qu’il aura besoin à peuprès du même temps pour trier des tableaux de même taille, quel que soit leur ordre initial.

13.6 Le tri par fusion est également insensible aux entrées, ce qui signifie qu’il aura besoin à peu prèsdu même temps pour trier des tableaux de même taille, quel que soit leur ordre initial.

13.7 Le tri par segmentation est en revanche plutôt sensible aux entrées. Tel qu’il est implémenté dansl’algorithme 13.5, il devient un algorithme O(n2) dans les cas spéciaux où le tableau est déjà trié,quel que soit l’ordre de tri. En effet, l’élément pivot sera toujours une valeur extrême dans lesous-tableau et le partage fractionnera donc le sous-tableau de façon très inégale, demandantainsi n étapes au lieu de lg n.

13.8 Le tri vertical est légèrement sensible aux entrées, mais pas énormément. La fonction hea-pify() aura généralement besoin de moins de lg n itérations.

13.9 Le tri par permutation est plus lent que le tri par sélection et, dans la plupart des cas, le tri parinsertion est légèrement plus rapide.

13.10 Le tri par fusion est plus lent que le tri vertical et, dans la plupart des cas, le tri par segmentationest plus rapide.

a. La trace du tri par permutation est la suivante :

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

44 77 55 99 66 33 22 88 77

55 77

66 99

33 99

22 99

88 99

77 99

66 77

33 77

22 77

77 88

33 66

22 66

33 55

Page 289: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 279

b. La trace du tri par sélection est la suivante :

c. La trace du tri par insertion est la suivante :

d. La trace du tri par fusion est la suivante :

22 55

33 44

22 44

22 33

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

44 77 55 99 66 33 22 88 77

22 44

33 77

44 55

55 99

77 99

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

44 77 55 99 66 33 22 88 77

55 77

66 77 99

33 44 55 66 77 99

22 33 44 55 66 77 99

88 99

77 88 99

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

44 77 55 99 66 33 22 88 77

44 55 77 99

33 66

77 88

22 33 66 77 88

22 33 44 55 66 77 77 88 99

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

(suite)

Page 290: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

280 Algorithmes de tri

e. La trace du tri par segmentation est la suivante :

f. La trace du tri vertical est la suivante :

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

44 77 55 99 66 33 22 88 77

22 99

77 99

22 44

33 77

44 55

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

44 77 55 99 66 33 22 88 77

99 77

88 77

99 44

88 44

77 44

77 99

88 77

44 88

77 44

77 44

22 77

77 22

66 22

33 77

66 33

44 33

22 66

55 22

33 22

22 55

44 22

Page 291: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 281

13.12 Le tri par permutation permettant de trier dans un ordre décroissant est le suivant :

• public static void sort(int[] a)• for (int i=a.length-1; i>0; i--)• for (int j=1; j<=i; j++)• if (a[j-1]<a[j]) swap(a,j-1,j);•

13.13 Le tri par permutation « intelligent » est le suivant :

• public static void sort(int[] a)• boolean sorted=false;• for (int i=a.length-1; i>0; i--)• for (int j=1; j<=i; j++)• sorted = true;• if (a[j-1] > a[j])• swap(a,j-1,j);• sorted = false;• • • if (sorted) return;• •

13.14 Théorème : le tri par permutation est correct.Démonstration : l’invariant de la boucle permet de démontrer que le tri par permutation trieréellement le tableau. Après la première itération de la boucle i principale, l’élément le plusimportant doit avoir été transféré à la dernière position. Quel que soit l’endroit où il a commencé,il doit être transféré progressivement complètement à droite parce que chaque comparaison per-met de le déplacer à droite. De la même façon, le deuxième élément le plus important doit avoirété transféré de la deuxième position avant la fin au cours de la deuxième itération de la boucleprincipale i. C’est pourquoi les deux éléments les plus importants se trouvent aux bons emplace-ments. Ce raisonnement démontre que l’invariant de la boucle est vrai à la fin de chaque itérationde la boucle principale i. Cependant, après la dernière itération, les éléments n-1 les plus impor-tants doivent être aux bons emplacements. Cela oblige l’énième élément le plus important (c’est-à-dire le plus petit élément) à se trouver également au bon emplacement. Par conséquent, letableau doit être trié.

13.15 Théorème : le tri par permutation est exécuté en O(n2).Démonstration : la fonction de complexité O(n2) signifie que, pour les valeurs importantes de n,le nombre d’itérations de la boucle a tendance à être proportionnel à n2. Il s’ensuit qu’un tableaude taille importante faisant le double d’un autre tableau, devrait être quatre fois plus long à trier.La boucle interne j itère n – 1 fois lors de la première itération de la boucle externe i, n – 2 foislors de la deuxième itération de la boucle i, n – 3 fois lors de la troisième itération de la boucle i,etc. Par exemple, si n = 7, 6 comparaisons sont effectuées au cours de la première itération de laboucle i, 5 au cours de la deuxième itération, 4 au cours de la troisième, etc., ce qui nous mèneà un nombre total de comparaisons de 6 + 5 + 4 + 3 + 2 + 1 = 21. En général, le nombre total decomparaisons est de

(n – 1) + (n – 2) + (n – 3) + … + 3 + 2 + 1

33 44

22 33

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]

(suite)

Page 292: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

282 Algorithmes de tri

Cette somme peut être exprimée de la façon suivante : n(n – 1)/2 (reportez-vous au théorème B.8pour plus d’informations). Pour des valeurs importantes de n, cette expression est presque égaleà n2/2, qui est proportionnel à n2.

13.16 Le tri cocktail shaker est le suivant :

• public static void sort(int[] a)• for (int i=a.length; i>0; i -= 2)• for (int j=1; j<i; j++)• if (a[j-1] > a[j]) swap(a,j-1,j);• for (int j=i-2; j>0; j--)• if (a[j-1] > a[j]) swap(a,j-1,j);• •

13.17 La modification du tri par sélection de façon à ce qu’il utilise le plus petit élément de chaquesous-séquence est la suivante :

• public static void sort(int[] a)• for (int i=0; i<a.length-1; i++)• int j=i;• for (int k=i+1; k<a.length; k++)• if (a[k] < a[j]) j = k;• swap(a,i,j);• •

13.18 Le tri par sélection récursif est le suivant :

• public static void sort(int[] a)• sort(a,a.length);• • public static void sort(int[] a, int n)• if (n<2) return;• int j=0;• for (int k=1; k<n; k++)• if (a[k] > a[j]) j = k;• swap(a,n-1,j);• sort(a,n-1);•

13.19 Théorème : le tri par sélection est correct.Démonstration : c’est l’invariant de boucle qui prouve l’exactitude de ce théorème. C’est pour-quoi, comme dans le cas du tri par permutation, nous aurons uniquement besoin de vérifier lesinvariants de boucle. À la première itération de la boucle principale (étape 1), a[i] est le dernierélément du tableau, c’est pourquoi l’index k de la boucle interne parcourt chaque élément situéaprès a[0]. La valeur de l’index j commence à 0, puis elle est modifiée chaque fois que k trouveun élément plus important. Étant donné que j est systématiquement réinitialisé à l’index de l’élé-ment le plus important, a[j] sera l’élément le plus important du tableau lorsque la boucleinterne se terminera. Cela vérifie le premier invariant de la boucle. À chaque itération successivede la boucle externe, l’index k parcourt le segment du tableau restant non trié. Ainsi, pour lamême raison que précédemment, a[j] sera l’élément le plus important du segment restant lors-que la boucle interne se terminera. Cela vérifie que le premier invariant de boucle est vrai à cha-que itération de la boucle externe. Étant donné que swap(a,i,j) ne fait que permuter a[i] eta[j], le deuxième invariant de boucle découle du premier. Le troisième invariant de boucledécoule du second par induction mathématique. Au cours de la première itération de la boucleprincipale, la boucle interne détermine que a[j] est l’élément le plus important du tableau. Laméthode swap(a,i,j) insère cet élément au niveau du dernier emplacement de a[i], c’est

Page 293: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 283

pourquoi a[i] doit être >= à la totalité du tableau a[j]. Avant la iième itération de la boucleprincipale, nous savons par hypothèse inductive que le sous-tableau a[i+1..n-1] est trié etque toutes les valeurs du sous-tableau a[0..i] sont plus petites que a[i+1]. Ensuite, après laiième itération, a[i] est l’un des éléments les plus petits. Par conséquent, a[i] <= a[i+1]<= ...<= a[n-1].

13.20 Théorème : le tri par sélection est exécuté en O(n2).Démonstration : cette fois encore, notre démonstration sera globalement identique à celle duthéorème correspondant pour le tri par permutation. À la première itération de la boucle externe i,la boucle interne j itère n – 1 fois. À la deuxième itération, elle itère n – 2 fois. Cette progressionse poursuit et crée un total de :

(n –1) + (n – 2) + … + 2 + 1 = n(n – 1)/2

13.21 Le test du tri par insertion indirect est le suivant :

• public static void main(String[] args)• int[] a = 77, 44, 99, 66, 33, 55, 88, 22 ;• int[] index = 0,1,2,3,4,5,6,7;• print(a,index);• sort(a,index);• print(a,index);• • public static void sort(int[] a, int[] index)• for (int i=1; i<a.length; i++)• int temp=index[i], j;• for (j=i; j>0 && a[index[j-1]]>temp; j--)• index[j] = index[j-1];• index[j] = temp;• • • private static void print(int[] a, int[] index)• for (int i=0; i<a.length; i++)• System.out.print(a[index[i]]+" ");• System.out.println();•

13.22 Le tri par insertion récursif est le suivant :

• public static void sort(int[] a)• sort(a,a.length);• • public static void sort(int[] a, int n)• if (n<2) return;• sort(a,n-1);• int temp=a[n-1], j;• for (j=n-1; j>0 && a[j-1]>temp; j--)• a[j] = a[j-1];• a[j] = temp;•

13.23 Théorème : le tri par insertion est correct.Démonstration : à la première itération de la boucle principale i, a[1] est comparé à a[0] etils sont intervertis si nécessaire. Par conséquent, a[0] <= a[1] après la première itération. Sinous supposons que l’invariant de la boucle est vrai avant la kième itération, il doit égalementêtre vrai après la fin de cette itération puisque a[k+1] a été inséré entre les éléments qui lui sontinférieurs ou égaux et ceux qui lui sont supérieurs. D’après le principe d’induction mathéma-tique, l’invariant de la boucle est donc vrai pour tout k.

13.24 Théorème : le tri par insertion est exécuté en O(n2).

Page 294: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

284 Algorithmes de tri

Démonstration : elle est identique à celle des algorithmes de tri par permutation et par sélection.À la première itération de la boucle externe i, la boucle interne j se répète une fois. À ladeuxième itération, elle se répète une ou deux fois en fonction du résultat de l’expression a[1]< a[2]. À la troisième itération, la boucle interne j est répétée trois fois au maximum, là aussien fonction du nombre d’éléments à gauche de a[3] supérieurs à a[3]. Le traitement se pour-suit sur ce modèle de façon à ce que, à la kième itération de la boucle externe, la boucle internesoit répétée k fois au maximum. Par conséquent, le nombre total d’itérations est le suivant :

1 + 2 + 3 + … + (n – 2) + (n – 1) = n(n – 1)/2

13.25 Théorème : le tri par insertion est exécuté en O(n) lorsque la séquence est déjà triée.Démonstration : dans ce cas, la boucle interne est répétée une seule fois pour chaque itérationde la boucle externe. Le nombre total d’itérations de la boucle interne est donc le suivant :

1 + 1 + 1 + … +1 + 1 = n – 1

13.26 Le tri par segmentation pivotant au niveau du dernier élément est le suivant :

• public static void sort(int[] a)• if (a.length>1) sort(a,0,a.length);• •• public static void sort(int[] a, int k, int n)• if (n<2) return;• int pivot = a[k+n-1];• int i = k-1;• int j = k+n-1;• while (i < j)• while (a[++i]<pivot) ;• while (j>0 && a[--j]>pivot) ;• if (i < j) swap(a,i,j);• • swap(a,i,k+n-1);• sort(a,k,i-k);• sort(a,i+1,k+n-i-1);•

13.27 Le tri par segmentation dont le pivot est égal à la moyenne de trois éléments est le suivant :

• private static void setMedian(int[] a, int i, int j, int k)• // CONDITION POSTERIEURE : either a[i] <= a[j] <= a[k]• // ou a[k] <= a[j] <= a[i]• // c’est-à-dire, a[j] est la moyenne de a[i],a[j],a[k]• if (a[i] <= a[k] && a[k] <= a[j]) swap(a,j,k);• else if (a[j] <= a[i] && a[i] <= a[k]) swap(a,j,i);• else if (a[j] <= a[k] && a[k] <= a[i]) swap(a,j,k);• else if (a[k] <= a[i] && a[i] <= a[j]) swap(a,j,i);• •• public static void sort(int[] a)• if (a.length>1) sort(a,0,a.length);• •• public static void sort(int[] a, int k, int n)• // CONDITIONS PREALABLES : 0 <= k <= k+n <= a.length;• if (n<2) return;• setMedian(a,k+n/2,k,k+n-1); // a[k] = moyenne• int pivot = a[k];• int i = k;• int j = k+n;• while (i < j)

Page 295: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 285

• while (i+1<k+n && a[++i]<pivot) ;• while (a[--j]>pivot) ;• if (i < j) swap(a,i,j);• • swap(a,k,j);• sort(a,k,j-k);• sort(a,j+1,k+n-j-1);•

13.28 Le tri par segmentation qui applique le tri par insertion lorsque n < 8 est le suivant :

• public static void sort(int[] a)• if (a.length>1) sort(a,0,a.length);• •• public static void sort(int[] a, int k, int n)• // CONDITIONS PREALABLES : 0 <= k <= k+n <= a.length;• if (n<2) return;• if (n<8)• insertionSort(a,k,n);• return;• • int pivot = a[k];• int i = k;• int j = k+n;• while (i < j)• while (i+1<k+n && a[++i]<pivot) ;• while (a[--j]>pivot) ;• if (i < j) swap(a,i,j);• • swap(a,k,j);• sort(a,k,j-k);• sort(a,j+1,k+n-j-1);• •• public static void insertionSort(int[] a, int k, int n)• // CONDITIONS POSTERIEURES : a[k] <= a[k+1] <= ... <= a[n-1];• for (int i=k+1; i<n; i++)• int temp=a[i], j;• for (j=i; j>k && a[j-1]>temp; j--)• a[j] = a[j-1];• a[j] = temp;• •

13.29 Le tri vertical n’est généralement pas préféré au tri par segmentation parce qu’il est plus lent enmoyenne.

13.30 Le tri vertical n’est généralement pas préféré au tri par fusion parce qu’il n’est pas stable.

13.31 Théorème : le tri vertical est correct.Démonstration : la condition postérieure de Heapify établit l’invariant de boucle de l’étape 3.Grâce à cette condition, la racine s0 est l’élément maximum de la sous-séquence. L’étape 5 insèrece maximum à la fin de la sous-séquence. Ainsi, lorsque la boucle se termine à l’étape 4, laséquence est triée. L’algorithme Heapify rétablit la propriété de tas à tout le segment ss enappliquant la méthode heapifyDown() depuis sa racine.

13.32 Le tri Las Vegas a une complexité O(nn). Il existe n! permutations différentes d’un jeu de n cartes.Lorsque vous les battez, cela revient à sélectionner une permutation au hasard. Une seule des n!permutations est correcte, c’est-à-dire qu’une seule d’entre elles laissera les cartes dans l’ordre.

Page 296: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

286 Algorithmes de tri

Le nombre de mélanges aléatoires nécessaires avant d’arriver à l’ordre voulu est donc égal à n!.Ensuite, chaque permutation a besoin de n – 1 comparaisons pour vérifier si elle est correcte.C’est pourquoi la complexité totale de l’algorithme est égale à O(nn!). En dernier lieu, d’après laformule de Stirling (reportez-vous au corollaire A.2), O(nn!) = O(nn).

Page 297: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 14

Tables

Une table (également qualifiée de mappe, table de conversion, tableau associatif oudictionnaire) est un conteneur qui permet un accès direct par type d’index. Elle fonc-tionne comme un tableau ou un vecteur, à l’exception du fait que la variable d’indexne doit pas nécessairement être un entier. En fait, vous pouvez imaginer qu’il s’agitd’un dictionnaire, la variable d’index étant le mot consulté et l’élément qui l’indexeétant sa définition.

Une table est une séquence de paires. Le premier composant d’une paire est une clé qui sert d’indexdans la table et généralise l’indice entier utilisé dans les tableaux. Le deuxième composant est la valeurdu composant clé qui contient les informations recherchées. Si nous reprenons l’exemple du diction-naire, la clé est le mot consulté et la valeur correspond à la définition de ce mot (et à tout ce qui est listépour lui).

Vous pouvez également parler d’une mappe parce que les clés sont mappées à leurs valeurs commeune fonction mathématique de type f(clé) = valeur. Vous pouvez aussi qualifier les tables de tableauxassociatifs parce qu’elles peuvent être implémentées à l’aide de deux tableaux parallèles, l’un contenantles clés et l’autre les valeurs.

14.1 INTERFACE JAVA MapL’interface Map est définie de la façon suivante dansle paquetage java.util :

public interface Map int size(); boolean isEmpty(); boolean containsKey(Object key); boolean containsValue(Object value); Object get(Object key); Object put(Object key, Object value); Object remove(Object key); void putAll(Map map); void clear(); public Set keySet(); public Collection values(); public Set entrySet(); public interface Entry Object getKey();

Dictionnaire

Object

AbstractMap

HashMap

TreeMap

Map

SortedMap

WeakHashMap

Page 298: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

288 Tables

Object getValue(); Object setValue(Object value); boolean equals(Object o); int hashCode(); boolean equals(Object o); int hashCode();

14.2 CLASSE HashMapSi vous regardez la hiérarchie de classe présentée dans la figure précédente, vous constaterez que Javadéfinit quatre implémentations de son interface Map, à savoir les classes AbstractMap, HashMap,TreeMap et WeakHashMap.

Exemple 14.1 Dictionnaire allemand-anglais

Ce programme utilise la classe HashMap afin de créer un dictionnaire allemand-anglais :

public class Ex1401 public static void main(String[] args) Map map = new HashMap(); map.put("Tag","day"); map.put("Hut","hat"); map.put("Uhr","clock"); map.put("Rad","wheel"); map.put("Ohr","ear"); map.put("Tor","gate"); System.out.println("map=" + map); System.out.println("map.size()=" + map.size()); System.out.println("map.keySet()=" + map.keySet()); System.out.println("map.values()=" + map.values()); System.out.println("map.get(\"Uhr\")=" + map.get("Uhr")); System.out.println("map.remove(\"Rad\")=" + map.remove("Rad")); System.out.println("map.get(\"Rad\")=" + map.get("Rad")); System.out.println("map=" + map); System.out.println("map.size()=" + map.size());

La méthode put() insère les paires clé/valeur dans la table. Par exemple, map.put("Tag","day"); insère la paire clé/valeur ("Tag","day"), "Tag" correspondant à la clé et "day" à lavaleur.Le premier appel de println() invoque la méthode HashMap.toString() et imprime la totalitéde l’objet map. Le deuxième appel de println() invoque la méthode HashMap.size() et indi-que que l’objet Map comporte six éléments clé/valeur. L’appel suivant de println() invoque la

map=Rad=wheel, Uhr=clock, Ohr=ear, Tor=gate, Hut=hat, Tag=daymap.size()=6map.keySet()=[Rad, Uhr, Ohr, Tor, Hut, Tag]map.values()=[wheel, clock, ear, gate, hat, day]map.get("Uhr")=clockmap.remove("Rad")=wheelmap.get("Rad")=nullmap=Uhr=clock, Ohr=ear, Tor=gate, Hut=hat, Tag=daymap.size()=5

Page 299: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Codes de hachage Java 289

méthode HashMap.keySet() qui renvoie un objet Set contenant toutes les clés (c’est-à-dire lessix mots allemands). L’appel suivant de println() invoque la méthode HashMap.values() quirenvoie un objet Collection contenant toutes les valeurs (c’est-à-dire les six mots anglais).L’appel suivant de println() invoque la méthode HashMap.get() qui renvoie la valeur d’uneclé donnée. Cet appel renvoie la valeur "clock" de la clé "Uhr". L’appel suivant de println()invoque la méthode HashMap.remove() qui supprime la paire ("Rad","wheel"). La réussitede cette opération est confirmée par l’appel suivant puisque map.get("Rad") renvoie null, indi-quant ainsi qu’il n’existe aucune paire clé/valeur dans map dont la clé serait "Rad". Les deux derniè-res lignes impriment à nouveau la totalité de map. Comme vous pouvez le constater, la paire("Rad","wheel") a bien été supprimée.

L’ordre de stockage des paires clé/valeur dans l’objet HashMap de cet exemple semble aléatoire. Eneffet, il ne correspond pas à l’ordre d’insertion de ces paires, comme le prouve l’exemple suivant.

Exemple 14.2 Les objets Java HashMap sont des tables de hachage

Ce programme crée deux objets HashMap indépendants, puis il y insère les mêmes paires clé/valeurque dans l’exemple précédent, mais dans un ordre différent :

public class Ex1402 public static void main(String[] args) Map map1 = new HashMap(); map1.put("Tor","gate"); map1.put("Rad","wheel"); map1.put("Tag","day"); map1.put("Uhr","clock"); map1.put("Hut","hat"); map1.put("Ohr","ear"); System.out.println("map1=" + map1); Map map2 = new HashMap(); map2.put("Rad","wheel"); map2.put("Uhr","clock"); map2.put("Ohr","ear"); map2.put("Tag","day"); map2.put("Tor","gate"); map2.put("Hut","hat"); System.out.println("map2=" + map2);

L’ordre de stockage des paires clé/valeur dans la table HashMap est reflété dans la sortie obtenueaprès exécution de la méthode toString(). En effet, il est identique à celui de notre exemple pré-cédent et ne dépend donc pas de l’ordre d’insertion des données. Vous remarquerez également quecet ordre est identique à celui de l’exemple 14.1.

14.3 CODES DE HACHAGE JAVAL’ordre de stockage des paires clé/valeur dans la table HashMap dépend uniquement de la capacité decette dernière et de la valeur du code de hachage des objets. N’oubliez pas que, comme nous l’avons déjàvu à la section 3.4, chaque objet de Java est associé à un code de hachage intrinsèque qui est calculé àpartir des données réelles stockées dans l’objet. C’est ce code qui est renvoyé par la méthodeObject.hashCode() pour chaque objet.

map1=Rad=wheel, Uhr=clock, Ohr=ear, Tor=gate, Hut=hat, Tag=daymap2=Rad=wheel, Uhr=clock, Ohr=ear, Tor=gate, Hut=hat, Tag=day

Page 300: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

290 Tables

Exemple 14.3 Codes de hachage de certains objets chaîne

Ce programme imprime les codes de hachage intrinsèques des objets String stockés dans les pro-grammes précédents :

public class Ex1403 public static void main(String[] args) printHashCode("Rad"); printHashCode("Uhr"); printHashCode("Ohr"); printHashCode("Tor"); printHashCode("Hut"); printHashCode("Tag");

private static void printHashCode(String word) System.out.println(word+": "+word.hashCode());

Comme vous pouvez le constater, les six codes obtenus sont des entiers de 5 chiffres proches parceque les objets String ont tous une longueur de 3.

14.4 TABLES DE HACHAGEUne table de hachage utilise une fonction spéciale, appelée fonction de hachage, pour calculer l’empla-cement des valeurs des données à partir des valeurs de leurs clés au lieu de stocker ces dernières dans latable. Étant donné que la durée de recherche ne dépend pas de la taille de la table, les tables de hachageaccèdent généralement plus vite aux données.

Java définit la classe Hashtable dans son paquetage java.util, mais il s’agit surtoutd’une amélioration de la classe HashMap. En effet, une table HashMap peut effectuer exac-tement les mêmes opérations qu’un objet Hashtable et elle présente l’avantage supplé-mentaire d’être cohérente avec le reste du framework de collections Java. En général, unetable de hachage ressemble à la figure suivante, à savoir qu’il s’agit d’un tableau d’objetsindexés par leur valeur de hachage.

Attention, cette structure implique une correspondance exacte entre l’intervalle définidans la fonction de hachage et celui des valeurs d’index du tableau. Pour cela, il vous suffitd’utiliser un modulo égal à la taille du tableau.

Exemple 14.4 Mapper des clés dans une table de hachage de taille 11

Ce programme imprime les valeurs du code de hachage pour les objets String qui seront stockésdans une table de hachage de taille 11 :

public class Ex1404 private static final int MASK = 0x7FFFFFFF; // 2^32-1 private static final int CAPACITY = 11;

Rad: 81909Uhr: 85023Ohr: 79257Tor: 84279Hut: 72935Tag: 83834

OhrRadUhrHutTag

Tor

0

1

2

3

4

5

6

7

8

9

10

Page 301: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Tables de hachage 291

public static void main(String[] args) printHash("Rad"); printHash("Uhr"); printHash("Ohr"); printHash("Tor"); printHash("Hut"); printHash("Tag");

private static void printHash(String word) System.out.println("hash(" + word + ") = " + hash(word));

private static int hash(Object object) return (object.hashCode() & MASK) % CAPACITY;

Les valeurs de la fonction de hachage sont calculées par l’instructionreturn (object.hashCode() & MASK) % CAPACITY;, CAPACITY étant égal à 11 et MASKà 2147483647, soit 0x7FFFFFFF sous sa forme hexadécimale. L’opération n & MASK ne fait quesupprimer le signe de l’entier n. Vous devez procéder ainsi en Java avant d’utiliser l’opérateurmodulo afin de calculer un index de tableau parce que ce langage, contrairement au C++, peut créerun résultat négatif pour m % CAPACITY si m est négatif. Ainsi, le résultat renvoyé par la fonctionhash() de cet exemple sera nécessairement compris entre 0 et 10. Les cinq premières chaînes ontles valeurs d’index 3, 4, 2, 8 et 5 ; elles seront donc stockées à ces emplacements dans la table dehachage. Cependant, la sixième chaîne ("Tag") a également une valeur d’index de 3, ce qui crée unconflit avec "Rad", qui est déjà stocké dans le composant 3. Dans ce genre de situation, vous appli-querez l’algorithme qui consiste à insérer le nouvel élément dans le prochain composant disponible,c’est-à-dire le composant 6 dans cet exemple puisque "Uhr" est déjà stocké dans le composant 4 et"Hut" dans le composant 5.Cet algorithme de résolution des collisions est qualifié de hachage avec essais linéaires.

La classe HashMap utilise la fonction de hachage comme dans l’exemple 14.4 pour implémenter sesaccesseurs : containsKey(), get(), put(), remove() et entrySet(). Elle définit la taille initialede la table de hachage à 101. Grâce à ces explications, vous êtes maintenant en mesure de comprendrepourquoi les six chaînes des exemples précédents étaient stockées dans l’ordre indiqué.

Exemple 14.5 Mapper les clés dans une table de hachage de taille 101

Le programme suivant est identique à celui de l’exemple 14.4 mais, cette fois, la capacité de la tablede hachage est définie à 101 au lieu de 11 :

public class Ex1405 private static final int MASK = 0x7FFFFFFF; // 2^32-1 private static final int CAPACITY = 101;

public static void main(String[] args) printHash("Rad");

hash(Rad) = 3hash(Uhr) = 4hash(Ohr) = 2hash(Tor) = 8hash(Hut) = 5hash(Tag) = 3

Page 302: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

292 Tables

printHash("Uhr"); printHash("Ohr"); printHash("Tor"); printHash("Hut"); printHash("Tag");

private static void printHash(String word) System.out.println("hash(" + word + ") = " + hash(word));

private static int hash(Object object) return (object.hashCode() & MASK) % CAPACITY;

Comme vous pouvez le constater, les éléments sont finalement stockés dans l’ordre inverse de leuraccessibilité.

14.5 PERFORMANCES DES TABLES DE HACHAGELes performances d’une table de hachage de taille 101 contenant seulement six éléments seront excellen-tes. Dans ces conditions, les risques de collisions sont peu probables, c’est pourquoi les fonctionsd’accès sont immédiates et la durée d’exécution est égale à O(1). Il s’agit d’un accès direct, comme dansle cas des tableaux. En revanche, les performances d’une table de hachage de taille 101 contenant 100éléments seront plutôt décevantes parce que de nombreuses collisions auront lieu au cours du processusde stockage des éléments. Par exemple, supposons que la chaîne "Lob" doive subir 60 collisions avantqu’un composant libre soit trouvé, cela signifie que vous aurez besoin de procéder à 60 tests avant detrouver ce composant chaque fois que vous essaierez d’accéder à la chaîne. Ce type de performance estproche de O(n), c’est-à-dire une performance à peine meilleure que celle d’une liste chaînée.

Pour résoudre ce problème, vous devrez éviter de trop remplir la table de hachage en la redimension-nant chaque fois qu’elle atteint une taille plafond. Afin de savoir où en est votre table, vous devrez tenircompte des deux paramètres suivants : la taille de la table, c’est-à-dire le nombre réel d’éléments qu’ellecontient, et sa capacité, c’est-à-dire le nombre de composants qu’elle comporte. Le rapport entre cesdeux paramètres est qualifié de facteur de charge. Dans le premier exemple de cette section, notre tableavait une taille égale à 6 et une capacité égale à 101, d’où un facteur de charge de 6/101 = 5,94 %. Dansle deuxième exemple, la taille de la table était de 100, d’où un facteur de chargé égal à 100/101 =99,01 %.

La classe HashMap redimensionne automatiquement sa table de hachage lorsque le facteur decharge atteint une valeur plafond. Cette dernière peut être définie au moment de la création de la table dehachage à l’aide du constructeur public HashMap(int initialCapacity, float loadFactor)qui permet également le paramétrage de la capacité initiale. Si vous utilisez un constructeur qui ne prendaucun de ces deux arguments, les valeurs par défaut de la capacité et du plafond de charge seront utili-sées, c’est-à-dire 101 et 75 % respectivement.

hash(Rad) = 99hash(Uhr) = 82hash(Ohr) = 73hash(Tor) = 45hash(Hut) = 13hash(Tag) = 4

Page 303: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithmes de résolution des collisions 293

14.6 ALGORITHMES DE RÉSOLUTION DES COLLISIONSL’algorithme de résolution des collisions que nous avons abordé dans les exemples précédents est quali-fié de hachage avec essais linéaires. Lorsqu’un nouvel élément doit être inséré dans un composant detable déjà utilisé, cet algorithme consiste à incrémenter l’index jusqu’à ce qu’un composant vide soittrouvé. Cela implique que vous devrez parfois retourner au début de la table de hachage.

Exemple 14.6 Essais linéaires

Le programme suivant vient compléter celui de l’exemple 14.4. Il garde un suivi des composants detable utilisés et de la valeur du facteur de charge après chaque hachage.

public class Ex1406 private static final int MASK = 0x7FFFFFFF; // 2^32-1 private static final int CAPACITY = 11; private static int size=0; private static boolean[] used = new boolean[CAPACITY];

public static void main(String[] args) printHash("Rad"); printHash("Uhr"); printHash("Ohr"); printHash("Tor"); printHash("Hut"); printHash("Tag"); printHash("Eis"); printHash("Ast"); printHash("Zug"); printHash("Hof"); printHash("Mal");

private static void printHash(String word) System.out.println("hash(" + word + ") = " + hash(word) + ", charge = " + 100*size/CAPACITY + "%");

private static int hash(Object object) ++size; int h = (object.hashCode() & MASK) % CAPACITY; while (used[h]) System.out.print(h + ", "); h = (h+1)%CAPACITY; used[h] = true; return h;

hash(Rad) = 3, charge = 9%hash(Uhr) = 4, charge = 18%hash(Ohr) = 2, charge = 27%hash(Tor) = 8, charge = 36%hash(Hut) = 5, charge = 45%3, 4, 5, hash(Tag) = 6, charge = 54%5, 6, hash(Eis) = 7, charge = 63%3, 4, 5, 6, 7, 8, hash(Ast) = 9, charge = 72%9, hash(Zug) = 10, charge = 81%3, 4, 5, 6, 7, 8, 9, 10, hash(Hof) = 0, charge = 90%2, 3, 4, 5, 6, 7, 8, 9, 10, 0, hash(Mal) = 1, charge = 100%

Page 304: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

294 Tables

Le champ size contient le nombre d’éléments hachés dans la table. Le tableau used[] indiquequels composants sont occupés dans cette même table. La méthode printHash() imprime l’indexde la table de hachage et le facteur de charge correspondant sous forme d’un pourcentage. Lorsqueles essais linéaires sont effectués, chaque test successif des numéros d’index est imprimé. Comme nous l’avons déjà vu dans l’exemple 14.4, la collision a lieu au moment de l’insertion de"Tag". Ce programme indique que trois collisions se sont produites, au niveau des numéros d’index3, 4 et 5, avant qu’un emplacement de hachage libre soit trouvé à l’index 6. Après cette insertion, latable est remplie à 54 %.Chaque élément suivant entrera également en collision avec les autres. Bien évidemment, les colli-sions deviennent plus fréquentes au fur et à mesure du remplissage de la table. Ainsi, le dernier élé-ment "Hut" rencontre 10 collisions. Cela signifie qu’à ce stade, chaque fois que vous accéderez à cetélément, vous devrez rechercher 11 éléments avant de le trouver, d’où un processus exécuté en O(n).

Un autre algorithme de résolution des collisions, plus efficace que les essais linéaires, est à votre dis-position : il s’agit de la méthode quadratique qui ignore certains éléments au cours des tests, créant ainsiune répartition plus uniforme des composants utilisés et moins de blocs importants. Cette méthode amé-liorera les performances de vos programmes puisqu’elle implique des chaînes d’essais plus courtes. Vousremarquerez que l’index revient au début de la liste au moment de l’insertion de "Mal" : 2, 3, 4, 5, 6, 7,8, 9, 10, 0, 1.

Exemple 14.7 Méthode quadratique

Ce programme ajoute à celui de l’exemple précédent la fonction hash() modifiée :

private static int hash(Object object) ++size; int h = (object.hashCode() & MASK) % CAPACITY; if (used[h]) int h0=h; int jump=1; while (used[h]) System.out.print(h + ", "); h = (h0+jump*jump)%CAPACITY; // incrément au carré ++jump; used[h] = true; return h;

Cette méthode se différencie essentiellement par ses essais successifs sur les numéros d’index effec-tués dans la boucle while dès qu’une collision a lieu. En outre, au lieu d’avoir recours à une recher-che linéaire, elle utilise un incrément au carré. Par exemple, lorsque l’insertion de "Ast" est enconflit avec l’index 3, l’essai linéaire poursuit ses vérifications aux index 4, 5, 6, 7, 8 et 9, commec’est le cas dans l’exemple 14.6. En revanche, avec la méthode quadratique, seuls les index 3, 4, 7 et1 ( = 12 mod 11) sont testés en ignorant 1, 4 et 9 (12, 22 et 32). Dans le cadre d’une méthode linéaire,

hash(Rad) = 3, charge = 9%hash(Uhr) = 4, charge = 18%hash(Ohr) = 2, charge = 27%hash(Tor) = 8, charge = 36%hash(Hut) = 5, charge = 45%3, 4, hash(Tag) = 7, charge = 54%5, hash(Eis) = 6, charge = 63%3, 4, 7, hash(Ast) = 1, charge = 72%hash(Zug) = 9, charge = 81%

Page 305: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chaînage séparé 295

50 % d’essais minimum sont nécessaires. Cependant, même si la méthode quadratique amélioreconsidérablement les performances de l’algorithme précédent, elle risque de créer une boucle infinie,comme illustré dans l’exemple 14.7 avec l’insertion suivante. En effet, si nous prenons l’exemple dela chaîne "Hof" initialement hachée à l’index 3, l’algorithme d’essais linéaires a trouvé une cellulevide à l’index 0 ( = 11 mod 11) après huit collisions, tandis que la séquence d’essais quadratiques surle même élément est identique à celle de la chaîne "Ast" : 3, 4, 7, 1, 8, 6, 6, 8 1, 7, 4, 3, 4, etc. Cerésultat est calculé à partir de la séquence quadratique non modulée 3, 4, 7, 12, 19, 28, 39, 52, 67, 84,103, 124, 147, etc., qui continue indéfiniment et ne teste que les six index 3, 4, 7, 1, 8 et 6, parailleurs déjà utilisés.Ainsi, bien que la table ne soit remplie qu’à 81 %, l’insertion échoue, ce qui n’aurait pas eu lieu avecles essais linéaires.

14.7 CHAÎNAGE SÉPARÉAu lieu de chercher à définir un algorithme de résolution des collisions plus efficace, il est préférabled’éviter complètement les collisions en autorisant l’insertion de plusieurs éléments par composant detable. Cette méthode est qualifiée de chaînage séparé parce qu’elle a recours aux listes chaînées pourstocker plusieurs éléments. Dans ce contexte, les composants de tables sont généralement qualifiés decompartiments.

Exemple 14.8 Chaînage séparé

Votre classe HashTable peut être en partie définie de la façon suivante à l’aide du chaînage séparé :

public class HashTable private static final int MASK = 0x7FFFFFFF; // 2^32-1 private static int capacity=101; private static int size=0; private static float load=0.75F; private static LinkedList[] buckets;

HashTable() buckets = new LinkedList[capacity]; for (int i=0; i<capacity; i++) buckets[i] = new LinkedList();

HashTable(int capacity, float load) this(); this.capacity = capacity; this.load = load;

Object put(Object key, Object value); int h=hash(key); LinkedList bucket=buckets[h]; Object oldValue=null; for (ListIterator it = bucket.iterator(); it.hasNext(); ) Map.Entry entry = it.next(); if (entry.getKey().equals(key)) break; if (entry.getKey().equals(key)) oldValue = entry.setValue(value); else bucket.add(new Entry(key,value)); return oldValue;

// d’autres méthodes...

Page 306: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

296 Tables

Comme vous pouvez le constater, la fonction put() joue deux rôles différents : si la table comportedéjà une entrée associée à la clé donnée, elle se contente de modifier la valeur de cette entrée ; dansle cas contraire, elle ajoute simplement une nouvelle entrée avec la paire clé/valeur. La classejava.util.HashMap utilise un chaînage séparé similaire à celui-ci dans l’exemple 14.8.

14.8 APPLICATIONSLes tables sont fréquemment utilisées dans les programmations de systèmes. En outre, il s’agit des blocsprincipaux qui constituent les bases de données relationnelles. L’exemple suivant illustre leur utilisationdans le cadre de la programmation d’une application.

Exemple 14.9 Concordance

Une concordance est une liste de mots apparaissant dans un document texte avec le numéro deslignes sur lesquelles figurent ces mots. Vous pourriez la comparer à l’index d’un livre qui listerait lesnuméros de ligne au lieu des numéros de page. Les concordances sont particulièrement utiles pourl’analyse de documents lorsque vous recherchez la fréquence de certains mots et des associations quine sont pas évidentes si vous lisez directement le document.

Ce programme crée une concordance pour le texte anglais ci-dessus extrait du Jules César de Sha-kespeare. La première partie de la concordance obtenue se trouve à droite. Elle est créée grâce àl’utilisation d’une vue Set de la concordance, puis à une itération de l’ensemble qui permet d’impri-mer un élément par ligne. Chacun de ces éléments est un objet Map Entry composé de Key, quicorrespond au mot dans le texte (en lettres majuscules), et de Value, qui correspond à la liste des

Shakeaspeare.txt Ex1409.out

Friends, Romans countrymen, lend me your ears !I come to bury Caesar, not to praise him.The evil taht men do lives after them,The good is oft interred with their bones ;So let it be with Caesar. The noble BrutusHath told you Caesar was ambitious ;If it were so, it was a grievous fault ;And grievously hath Caesar answer’d it.Here, under leave of Brutus and the rest, --For Brutus is an honourable man ;So are they all, all honourable men.Come I to speak in Caesar’s funeral.He was my friend, faithful and just to me.But Brutus says he was ambitious ;And Brutus is an honourable man.He hath brought manycaptives home to Rome.Whose ransoms did the general coffers fill :Did this in Caesar seem ambitious ?When that the poor have cried, Caesar hath wept ;Ambition should be made of sterner stuff.Yet Brutus says he was ambitious ;And Brutus is an honourable man.You all did see that on the LupercalI thrice presented him with a kingly crown,Which he did thrice refuse : was this ambition ?Yet Brutus says he was ambitious ;And, sure, is an honourable man.I speak not to disprove that Brutus spoke,But here I am to speak what I do know.You all did love him once, not without cause.What cause withholds you, then, to mourn for him ?O judgement ! thou art fled to brutish beasts,And men have lost their reason !

STUFF=20THE=3, 4, 5, 9, 17, 19, 23GRIEVOUS=7GRIEVOUSLY=8WHOSE=17REASON=33AND= 8, 9, 13, 15, 22, 27, 33FAULT=7KINGLY=24COUNTRYMEN=1MOURN=31FRIENDS=1GOOD=4LEAVE=9ROME=16CROWN=24SHOULD=20INTERRED=4WEPT=19FOR=10, 31FRIEND=13BUT=14, 29BRUTUS=5, 9, 10, 14, 15, 21, 22, 26, 28MAN=10, 15, 22, 27CAUSE=30, 31SURE=27PRESENTED=24YOU=6, 23, 30, 31SEE=23BONES=4LIVES=3REFUSE=25HERE=9, 29

Page 307: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Applications 297

numéros de lignes où ce mot apparaît. Par exemple, le mot man figure aux lignes 10, 15, 22 et 27, laligne 10 étant

For Brutus is an honourable man;

Le programme complet est le suivant :

import java.io.*;import java.util.*;

public class Ex1409 private static HashMap concordance; private static File file; private static FileReader reader; private static BufferedReader in;

public static void main(String[] args) new Ex1409("Shakespeare.txt"); load(); dump(); Ex1409(String filename) try concordance = new HashMap(); file = new File(filename); reader = new FileReader(file); in = new BufferedReader(reader); catch(Exception e) System.out.println(e);

private static void load() String line=null; StringTokenizer parser=null; int lineNumber=0; try while ((line=in.readLine()) != null) ++lineNumber; parser = new StringTokenizer(line,",.;:()-!?’ "); while (parser.hasMoreTokens()) String word = parser.nextToken().toUpperCase(); String listing = (String)concordance.get(word); if (listing==null) listing = "" + lineNumber; else listing += ", " + lineNumber; concordance.put(word,listing); catch(Exception e) System.out.println(e);

private static void dump() Set set = concordance.entrySet(); for (Iterator it=set.iterator(); it.hasNext(); ) System.out.println(it.next());

La méthode main() instancie la classe Ex1409 et passe la chaîne "Shakespeare.txt" à sonconstructeur. Ce dernier instancie la classe HashMap et crée un objet concordance. Il instancieégalement l’objet qui lit le fichier d’entrée, à savoir in. La fonction main() appelle load() quicharge la concordance à partir du fichier texte. Cette méthode utilise l’objet parser de la classe

Page 308: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

298 Tables

StringTokenizer afin d’analyser le texte, puis elle extrait chaque mot à l’aide de la méthodenextToken(). Elle assure le suivi des numéros de ligne grâce au compteur lineNumber qui estincrémenté chaque fois qu’une nouvelle ligne est lue. Chaque itération de la boucle while externelit une ligne, tandis que chaque itération de la boucle while interne lit un mot. L’instructionString listing = (String)concordance.get(word); recherche le mot en tant que clédans le cadre de la concordance. Si elle le trouve, elle affecte sa valeur correspondante à la chaînelisting. Dans le cas contraire, listing est null et se voit affecter une chaîne avec le numéro deligne. Si listing n’est pas null, le numéro de ligne est concaténé au listing existant. Dans ces deuxcas, l’instruction concordance.put(word,listing); insère ensuite le nouveau listing dans latable de hachage, sous forme d’une nouvelle entrée ou bien sous forme d’une entrée mise à jour.Vous remarquerez que l’objet parser est créé par l’appel de constructeur parser = new String-Tokenizer(line,",.;:()-!? "); qui utilise le constructeur composé de deux arguments pourla classe StringTokenizer, le second argument étant une chaîne composée de tous les caractèresdélimiteurs. En listant la virgule ’,’, le point ’.’ et le point-virgule ’;’, etc., nous pouvonsexclure ces caractères des mots stockés. En dernier lieu, main() appelle dump() qui imprime laconcordance. Étant donné qu’il est impossible de parcourir directement la classe HashMap (repor-tez-vous à la section 14.1), nous devons d’abord insérer la concordance dans un objet Set. Cela nouspermettra d’obtenir une « vue » différente de la table de hachage, c’est-à-dire un ensemble dont leséléments correspondent à des entrées de table. Ensuite, grâce à la méthode iterator() de l’inter-face Set, nous pouvons aisément parcourir cette dernière à l’aide d’une boucle for en accédant àchaque entrée de façon séquentielle. La méthode next() de l’itérateur renvoie une entrée sousforme d’un Object dont le type intrinsèque est Map.Entry. D’ailleurs, comme vous pouvez leconstater dans la définition de l’interface Map présentée au début de ce chapitre, Entry est une inter-face interne de Map. C’est pourquoi, lorsqu’elle est passé à println(), la méthode Map.Entry.toString() est appelée, créant ainsi une chaîne de sortie similaire à l’exemple suivant :

BRUTUS=5, 9, 10, 14, 15, 21, 22, 26, 28

Le signe égal '=' sépare la clé de l’entrée et sa valeur. Dans le cas présent, la clé est constituée parla chaîne "BRUTUS" et la valeur par la chaîne "5, 9, 10, 14, 15, 21, 22, 26, 28".Les concordances permettent d’analyser un texte. Grâce à l’exemple que nous venons de voir, vouspouvez déterminer que cette scène de Shakespeare est plus consacrée à Brutus et à l’utilisation deson homonyme « brutish » qu’à César.

La sortie du programme de l’exemple 14.9 illustre un inconvénient majeur des tables de hachage :elles ne sont pas ordonnées. Afin d’obtenir une sortie par ordre alphabétique de la concordance, nousdevons d’abord trier cette dernière.

14.9 CLASSE TreeMapLa classe TreeMap étend la classe AbstractMap et implémente l’interface SortedMap (reportez-vousà la section 14.1). Il s’agit en fait d’un arbre binaire de recherche et non d’une table de hachage. Cepen-dant, elle a encore recours aux mappes et à leurs entrées clé/valeur. Étant une structure arborescente, elleperd son temps d’accès égal à O(1), mais présente l’avantage d’être ordonnée.

Exemple 14.10 Concordance ordonnée

Ce programme se différencie de celui de l’exemple 14.9 par le fait que la classe HashMap a été rem-placée par TableMap dans le constructeur :

Ex1410(String filename) try concordance = new TableMap();

Page 309: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe TreeMap 299

file = new File(filename); reader = new FileReader(file); in = new BufferedReader(reader); catch(Exception e) System.out.println(e);

Ce programme crée une concordance stockée sous forme d’arbre binaire de recherche ordonné ;c’est pourquoi votre sortie est maintenant triée :

A=7, 24AFTER=3ALL=11, 11, 23, 30AM=29AMBITION=20, 25AMBITIOUS=6, 14, 18, 21, 26AN=10, 15, 22, 27AND=8, 9, 13, 15, 22, 27, 33ANSWER=8ARE=11ART=32BE=5, 20BEASTS=32BONES=4BROUGHT=16BRUTISH=32BRUTUS=5, 9, 10, 14, 15, 21, 22, 26, 28BURY=2BUT=14, 29CAESAR=2, 5, 6, 8, 12, 18, 19CAPTIVES=16CAUSE=30, 31COFFERS=17COME=2, 12COUNTRYMEN=1CRIED=19CROWN=24D=8DID=17, 18, 23, 25, 30DISPROVE=28DO=3, 29EARS=1EVIL=3FAITHFUL=13FAULT=7FILL=17FLED=32FOR=10, 31FRIEND=13FRIENDS=1FUNERAL=12GENERAL=17GOOD=4GRIEVOUS=7GRIEVOUSLY=8HATH=6, 8, 16, 19HAVE=19, 33HE=13, 14, 16, 21, 25, 26HERE=9, 29HIM=2, 24, 30, 31

Page 310: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

300 Tables

Cet exemple prouve non seulement la dichotomie fondamentale entre les tables de hachage nonordonnées linéaires et les arbres binaires de recherche ordonnés quadratiques, mais illustre égalementl’efficacité du framework de collections Java. En effet, étant donné que les classes HashMap et TreeMapimplémentent toutes les deux la même interface Map, nous pouvons les permuter en changeant simple-ment le nom du constructeur appelé pour créer la mappe.

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

14.1 Quelle est la différence entre une table et un vecteur ?

14.2 Pourquoi une table est-elle également qualifiée de mappe ?

14.3 Pourquoi une table est-elle également qualifiée de tableau associatif ?

14.4 Pourquoi une table est-elle également qualifiée de dictionnaire ?

14.5 Qu’est-ce qu’une concordance ?

14.6 Qu’est-ce qu’une table de hachage ?

14.7 Quelle est la différence entre les classes Java Hashtable et HashMap ?

14.8 Les deux premiers exemples de ce chapitre ont démontré que l’ordre d’insertion des élémentsdans une table de hachage n’a aucune importance s’il n’y a pas de collisions. Mais que se passe-t-il si des collisions ont lieu ?

14.9 Quels sont les avantages et les inconvénients de la méthode quadratique par rapport à la méthodedes essais linéaires ?

14.10 Dans quelles conditions est-il préférable d’utiliser une classe HashMap plutôt qu’une classeTreeMap, et inversement ?

RÉPONSES¿RÉPONSES

14.1 Un vecteur fournit un accès direct à ses éléments grâce à des index entiers. Une table fournit unaccès direct à ses éléments grâce à un champ clé de type ordinal, c’est-à-dire de type int,double, string, etc.

14.2 Une table est également qualifiée de mappe parce qu’elle agit comme une fonction mathéma-tique et mappe chaque valeur de clé à un élément unique.

14.3 Une table est également qualifiée de tableau associatif parce qu’elle fonctionne comme untableau (voir la réponse 14.1) dans lequel chaque valeur de clé est associée à son élément unique.Comme les fonctions mathématiques, elle mappe chaque valeur de clé à un élément unique.

14.4 Une table est également qualifiée de dictionnaire parce qu’elle est utilisée comme un dictionnairede langue standard, c’est-à-dire pour rechercher des éléments au même titre que dans un diction-naire.

14.5 Une concordance est une liste de mots trouvés dans un document texte et associés aux numérosdes lignes dans lesquelles ils apparaissent.

Page 311: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 301

14.6 Une table de hachage est une table qui utilise une fonction spéciale afin de calculer l’emplace-ment des valeurs des données à partir des valeurs des clés au lieu de stocker ces clés dans la table.

14.7 Elles sont presque identiques. La classe HashMap constitue une amélioration de la classe Hash-table, ce qui lui permet de rester cohérente avec le framework de collections Java.

14.8 Même si des collisions ont lieu, l’ordre d’insertion n’a aucune importance.

14.9 La méthode quadratique crée généralement moins de collisions parce que les essais qu’elle effec-tue ignorent les emplacements vides de la fourchette d’index. Cependant, contrairement auxessais linéaires, elle risque de provoquer des boucles infinies même lorsque la table n’est paspleine.

14.10 Un objet HashMap est une table de hachage implémentée avec un chaînage séparé et un plafondde charge par défaut égal à 75 %. Il offre donc un temps d’accès presque égal à O(1) pour lesinsertions, les suppressions et les recherches. En revanche, un objet TreeMap est un arbre derecherche binaire équilibré implémenté comme un arbre rouge et noir. C’est pourquoi il offre untemps d’accès presque égal à O(lgn) pour les insertions, les suppressions et les recherches.HashMap est donc plus rapide, alors que TreeMap présente l’avantage d’être ordonné.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

14.1 Exécutez un programme similaire à celui de l’exemple 14.1 vous permettant d’insérer 16 entréesdans le dictionnaire allemand-anglais :

• map.put("Ast","gate");• map.put("Eis","ice");• map.put("Hof","court, yard, farm");• map.put("Hut","hat");• map.put("Lob","praise");• map.put("Mal","mark, signal");• map.put("Mut","courage");• map.put("Ohr","ear");• map.put("Ost","east");• map.put("Rad","wheel");• map.put("Rat","advice, counsel");• map.put("Tag","day");• map.put("Tor","gate");• map.put("Uhr","clock");• map.put("Wal","whale");• map.put("Zug","procession, train");

14.2 Modifiez la classe Concordance pour qu’elle filtre et qu’elle élimine lesmots communs (pronoms, adverbes, etc.) dont la liste n’apporte rien àl’analyse du document. Stockez ces mots communs dans un fichierséparé que vous nommerez MotsCommuns.txt et qui sera similaire àcelui de la figure suivante :

14.3 Modifiez le programme de l’exemple 14.1 de façon à ce qu’il stocke lesmots par ordre alphabétique. Il devra charger les mêmes données quecelles de l’exercice 14.1, puis imprimer le contenu de la table par ordrealphabétique.

14.4 Implémentez une classe FrequencyTable destinée à créer une liste demots et leur fréquence d’apparition dans un fichier texte donné.

AAFTERALLAMANANDAREBEBROUGHTBUTCOMEDIDDOFORHATHHAVEHEHEREHIMIIFINISIT

MotsCommuns.txt

Page 312: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

302 Tables

SOLUTIONS¿SOLUTIONS

14.1 Vous insérerez 16 nouvelles entrées dans le dictionnaire allemand-anglais de la façon suivante :

• import java.util.*;• public class Testing• public static void main(String[] args)• Map map = new HashMap(11);• map.put("Ast","gate");• map.put("Eis","ice");• map.put("Hof","court, yard, farm");• map.put("Hut","hat");• map.put("Lob","praise");• map.put("Mal","mark, signal");• map.put("Mut","courage");• map.put("Ohr","ear");• map.put("Ost","east");• map.put("Rad","wheel");• map.put("Rat","advice, counsel");• map.put("Tag","day");• map.put("Tor","gate");• map.put("Uhr","clock");• map.put("Wal","whale");• map.put("Zug","procession, train");• System.out.println("map=" + map);• System.out.println("map.keySet()=" + map.keySet());• System.out.println("map.size()=" + map.size());• •

14.2 La classe Concordance capable de filtrer les mots courants est la suivante :

• import java.io.*;• import java.util.*;• public class Pr1403• private static Map concordance;• private static Set words;• private static File file;• private static FileReader reader;• private static BufferedReader in;• public static void main(String[] args)• loadSet("MotsCourants.txt");• dumpSet();• loadMap("Shakespeare.txt");• dumpMap();• • private static void loadSet(String filename)• try• words = new HashSet();• file = new File(filename);• reader = new FileReader(file);• in = new BufferedReader(reader);• String line=null;• StringTokenizer parser=null;• while ((line=in.readLine()) != null)• parser = new StringTokenizer(line,",.;:()-!?’ ");• String word = parser.nextToken().toUpperCase();• words.add(word);

Page 313: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 303

• • • catch(Exception e) System.out.println(e); • • private static void dumpSet()• for (Iterator it=words.iterator(); it.hasNext(); )• System.out.println(it.next());• • private static void loadMap(String filename)• try• concordance = new TreeMap();• file = new File(filename);• reader = new FileReader(file);• in = new BufferedReader(reader);• String line=null;• StringTokenizer parser=null;• int lineNumber=0;• while ((line=in.readLine()) != null)• ++lineNumber;• parser = new StringTokenizer(line,",.;:()-!?’ ");• while (parser.hasMoreTokens())• String word = parser.nextToken().toUpperCase();• if (!words.contains(word))• String listing = (String)concordance.get(word);• if (listing==null) listing = "" + lineNumber;• else listing += ", " + lineNumber;• concordance.put(word,listing);• • • • • catch(Exception e) System.out.println(e); • • private static void dumpMap()• Set set = concordance.entrySet();• for (Iterator it=set.iterator(); it.hasNext(); )• System.out.println(it.next());• •

14.3 Vous pouvez trier le dictionnaire allemand-anglais de la façon suivante :

• import java.util.*;• public class Pr1202• private static Map map;• public static void main(String[] args)• map = new TreeMap();• load();• dump();• • private static void load()• map.put("Ast","gate");• map.put("Eis","ice");• map.put("Hof","court, yard, farm");• map.put("Hut","hat");• map.put("Lob","praise");• map.put("Mal","mark, signal");• map.put("Mut","courage");• map.put("Ohr","ear");• map.put("Ost","east");• map.put("Rad","wheel");

Page 314: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

304 Tables

• map.put("Rat","advice, counsel");• map.put("Tag","day");• map.put("Tor","gate");• map.put("Uhr","clock");• map.put("Wal","whale");• map.put("Zug","procession, train");• • private static void dump()• Set set = map.entrySet();• for (Iterator it=set.iterator(); it.hasNext(); )• System.out.println(it.next());• •

14.4 La table de fréquence peut être implémentée de la façon suivante :

• import java.io.*;• import java.util.*;• public class Pr1404• private static Map concordance;• private static File file;• private static FileReader reader;• private static BufferedReader in;• public static void main(String[] args)• loadMap("Shakespeare.txt");• dumpMap();• • private static void loadMap(String filename)• try• concordance = new TreeMap();• file = new File(filename);• reader = new FileReader(file);• in = new BufferedReader(reader);• String line=null;• StringTokenizer parser=null;• int lineNumber=0;• while ((line=in.readLine()) != null)• ++lineNumber;• parser = new StringTokenizer(line,",.;:()-!?’ ");• while (parser.hasMoreTokens())• String word = parser.nextToken().toUpperCase();• String frequency = (String)concordance.get(word);• if (frequency==null) frequency = "1";• else• int n=Integer.parseInt(frequency);• ++n;• frequency = "" + n;• • concordance.put(word,frequency);• • • • catch(Exception e) System.out.println(e); • • private static void dumpMap()• Set set = concordance.entrySet();• for (Iterator it=set.iterator(); it.hasNext(); )• System.out.println(it.next());• •

Page 315: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 15

Ensembles

15.1 ENSEMBLES MATHÉMATIQUESUn ensemble est une collection d’éléments uniques. Sa taille correspond au nombre d’éléments qui lecomposent. L’ensemble de taille 0 est qualifié d’ensemble vide ; il est représenté par le symbole ∅.

En mathématiques, les ensembles sont déterminés par la liste de leurs éléments présentée de la façonsuivante :

A= 44, 77, 22

ou par une liste basée sur un modèle donné :

B = 2, 4, 6, 8, 10, 12, . . .

ou bien à l’aide d’une condition :

C = n ∈ Z | n est pair = n ∈ Z : n est impair

Le symbole suivant correspond à l’ensemble de tous les entiers :

Z = 0, 1, –1, 2, –2, 3, –3, . . .

Le symbole de l’opérateur de transfert de données (|) et les deux-points (:) sont lus « tel que ». Ainsi,la définition de l’ensemble C ci-dessus serait lue « l’ensemble de tous les entiers n tels que n est pair »,ce qui signifie simplement l’ensemble de tous les entiers pairs.

Les trois opérations principales susceptibles d’être effectuées sur un ensemble sont l’union, l’inter-section et le complément relatif (également qualifié de soustraction). L’union des ensembles A et B estindiquée et définie de la façon suivante :

A ∪ B = x : x ∈ A ou x ∈ B

L’intersection des ensembles A et B est indiquée et définie de la façon suivante :

A ∩ B = x : x ∈ A et x ∈ B

Quant au complément relatif de l’ensemble B dans l’ensemble A, il est indiqué et défini de la façonsuivante :

A - B = x : x ∈ A mais (x ∉ B)

L’opérateur relationnel principal susceptible d’être appliqué aux ensembles est l’opérateur de sous-ensemble : A est un sous-ensemble de B si et seulement si chaque élément de A est également un élémentde B. Cette condition est signalée de la façon suivante : A ⊆ B. Vous remarquerez que chaque ensembleest un sous-ensemble de lui-même et que l’ensemble vide est un sous-ensemble de chaque ensemble.

Page 316: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

306 Ensembles

Exemple 15.1 Opérations théoriques sur les ensembles

Supposons que A = 1, 2, 3, 4, 5 et B = 4, 5, 6, 7. Alors A ∪ B = 1, 2, 3, 4, 5, 6, 7, A ∩ B =4, 5 et A – B = 1, 2, 3.Supposons que C = 2, 3, 4, alors C est un sous-ensemble de A, mais pas de B.

15.2 INTERFACE JAVA SetDans le cadre du framework de collections Java (reportez-vous à la collection 5.1), l’implémentation desensembles est parallèle à celles des tables que nous venons d’étudier au chapitre 14. Ce framework com-prend notamment les classes et les interfaces suivantes :

L’interface java.util.Set est une sous-interface de Collection ; elle hérite donc des métho-des de cette dernière :

public boolean add(Object);public boolean addAll(Collection);public void clear();public boolean contains(Object);public boolean containsAll(Collection);public boolean equals(Object);public int hashCode();public boolean isEmpty();public Iterator iterator();public boolean remove(Object);public boolean removeAll(Collection);public boolean retainAll(Collection);public int size();public Object[] toArray();public Object[] toArray(Object[]);

Aucune autre méthode n’est ajoutée à l’interface Set.

15.3 CLASSE JAVA AbstractSetLa classe java.util.AbstractSet est une implémentation partielle de l’interface Set. À l’instardes autres classes du framework de collections, elle est conçue de façon à limiter les efforts nécessairesà la construction d’une classe complète. Elle implémente la plupart des méthodes requises par l’interfaceSet :

public abstract class AbstractSet extends AbstractCollection implements Set protected AbstractSet() public boolean add(Object); public boolean addAll(Collection);

Object

AbstractCollection

AbstractSet

HashSet

TreeSet

AbstractMap

HashMap

TreeMap

Collection

Set

SortedSet

Map

SortedMap

Page 317: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Java HashSet 307

public void clear(); public boolean contains(Object); public boolean containsAll(Collection); public boolean equals(Object); public int hashCode(); public boolean isEmpty(); public abstract Iterator iterator(); public boolean remove(Object); public boolean removeAll(Collection); public boolean retainAll(Collection); public abstract int size(); public Object[] toArray(); public Object[] toArray(Object[]);

15.4 CLASSE JAVA HashSetLa classe Java HashSet est globalement identique à la classe HashMap, si ce n’est que ses objets sontdes composants simples constitués d’éléments d’un ensemble au lieu des composants doubles constituésde paires clé/valeur. À l’instar de la classe HashMap, la classe HashSet a recours aux valeurs hash-Code() intrinsèques des objets pour les stocker dans une table de hachage. Ainsi, l’accès à ses éléments(à l’aide des méthodes add(), contains() et remove()), a une durée d’exécution presque constante(c’est-à-dire que les algorithmes ont une complexité O(1)). Cette classe est définie de la façon suivantedans le paquetage java.util :

public class HashSet extends AbstractSet implements Set public HashSet(); public HashSet(Collection collection); public HashSet(int capacity, float threshold); public HashSet(int capacity); public Iterator iterator(); public int size(); public boolean isEmpty(); public boolean contains(Object object); public boolean add(Object object); public boolean remove(Object object); public void clear(); public Object clone();

Exemple 15.2 Table de hachage HashSet destinée aux chaînes

Le programme suivant crée une table de hachage HashSet nommée pays, puis il la charge à l’aidede sept chaînes. Il utilise ensuite un itérateur pour la parcourir afin d’imprimer son contenu

import java.util.*;public class Ex1502 public static void main(String[] args) Set pays = new HashSet(); load(pays); dump(pays); private static void load(Set set) set.add("Cuba"); set.add("Iran"); set.add("Iraq"); set.add("Laos"); set.add("Russie");

Page 318: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

308 Ensembles

set.add("Chine"); set.add("Togo"); private static void dump(Set set) for (Iterator it=set.iterator(); it.hasNext(); ) System.out.println(it.next()); System.out.println("set.size() = " + set.size());

Comme vous pouvez le constater, l’ordre des éléments de la table de hachage est totalement aléa-toire. Les index réels des éléments sont calculés par une fonction de hachage privée.

Exemple 15.3 Modifier la table de hachage HashSet composée de chaînes

Ce programme illustre l’utilisation des méthodes add(), contains() et remove() sur les ensem-bles HashSet. Il reprend les mêmes données que celles de l’exemple précédent :

import java.util.*;public class Ex1503 public static void main(String[] args) Set pays = new HashSet(); load(pays); report(pays,"Iraq"); System.out.println("pays.remove(\"Iraq\") = " + pays.remove("Iraq")); report(pays,"Iraq"); report(pays,"Fiji"); System.out.println("pays.remove(\"Fiji\") = " + pays.remove("Fiji")); System.out.println("pays.add(\"Fiji\") = " + pays.add("Fiji")); report(pays,"Fiji"); System.out.println("pays.add(\"Fiji\") = " + pays.add("Fiji")); report(pays,"Fiji"); private static void report(Set set, String string) System.out.println("\t\t" + set.size() + " éléments : " + set); System.out.println("\t\tset.contains(" + string + ") = " + set.contains(string)); private static void load(Set set) set.add("Cuba"); set.add("Iran"); set.add("Iraq"); set.add("Laos"); set.add("Russie"); set.add("Chine"); set.add("Togo");

ChineRussieIraqIranCubaTogoLaosset.size() = 7

Page 319: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Classe Java TreeSet 309

La méthode remove() signale si l’opération est réussie ou non. C’est pourquoi l’appel pays.remove("Fiji") renvoie false. En effet, à ce stade de l’exécution du programme, "Fiji"n’était pas encore un élément de l’ensemble. La méthode add() signale également si l’opération estréussie ou non. L’accès aux éléments de l’ensemble (via les méthodes add(), contains() etremove() est donc exécuté en une durée logarithmique, c’est-à-dire par des algorithmes de com-plexité O(lgn).

15.5 CLASSE JAVA TreeSetLa classe Java TreeSet est globalement identique à la classe HashMap, si ce n’est que ses objets sontdes composants simples constitués d’éléments d’un ensemble au lieu des composants doubles constituésdes paires clé/valeurs. À l’instar de la classe TreeMap, la classe TreeSet stocke ses objets dans unarbre binaire de recherche équilibré (un arbre rouge et noir). C’est pourquoi l’accès à ses éléments (àl’aide des méthodes add(), contains() et remove()) est exécuté en une durée logarithmique, c’est-à-dire par des algorithmes de complexité O(lgn). Cette classe est définie dans le paquetage java.utilde la façon suivante :

public class TreeSet extends AbstractSet implements Set public TreeSet(); public TreeSet(Collection collection); public TreeSet(SortedSet set); public TreeSet(Comparator comparator); public Iterator iterator(); public int size(); public boolean isEmpty(); public boolean contains(Object object); public boolean add(Object object); public boolean remove(Object object); public void clear(); public Object clone(); public Comparator comparator(); public Object first(); public Object last(); public SortedSet subSet(Object start, Object stop); public SortedSet headSet(Object stop); public SortedSet tailSet(Object start);

7 éléments : [Chine, Russie, Iraq, Iran, Cuba, Togo, Laos]set.contains(Iraq) = truepays.remove("Iraq") = true6 éléments : [Chine, Russie, Iran, Cuba, Togo, Laos]set.contains(Iraq) = false6 éléments : [Chine, Russie, Iran, Cuba, Togo, Laos]set.contains(Fiji) = falsepays.remove("Fiji") = falsepays.add("Fiji") = true7 éléments : [Chine, Fiji, Russie, Iran, Cuba, Togo, Laos]set.contains(Fiji) = truepays.add("Fiji") = false7 éléments : [Chine, Fiji, Russie, Iran, Cuba, Togo, Laos]set.contains(Fiji) = true

Page 320: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

310 Ensembles

Ce programme implémente non seulement les méthodes de la classe HashSet, mais également desméthodes supplémentaires requises par l’interface SortedSet. Il s’agit notamment des cinq dernièresméthodes de la liste précédente qui renvoient le premier et le dernier éléments, ainsi que trois types desegments de sous-ensembles. La classe HashSet utilise la méthode equals() de ses éléments pourrechercher les éléments donnés, ce qui est largement suffisant puisque l’ordre n’a pas d’importance dansune table de hachage. En revanche, l’ordre jouant un rôle essentiel dans les arbres de recherche, la classeTreeSet utilise soit la méthode compareTo() de ses éléments, soit un objet Comparator. Si le qua-trième constructeur listé dans le programme ci-dessus est appelé, l’objet Comparator est utilisé (repor-tez-vous à la section 12.6).

Exemple 15.4 Utiliser un ensemble TreeSet de chaînes

Ce programme illustre l’utilisation des méthodes first(), last(), headSet(), subSet() ettailSet() pour les ensembles TreeSet :

import java.util.*;public class Ex1504 public static void main(String[] args) SortedSet pays = new TreeSet(); load(pays); System.out.println(pays); System.out.println("pays.first() = " + pays.first()); System.out.println("pays.last() = " + pays.last()); String s1="Fiji"; String s2="Laos"; System.out.println("pays.headSet(" + s1 + ") = " + pays.headSet(s1)); System.out.println("pays.subSet(" + s1 + "," + s2 + ") = " + pays.subSet(s1,s2)); System.out.println("pays.tailSet(" + s2 + ") = " + pays.tailSet(s2)); private static void load(Set set) set.add("Laos"); set.add("Cuba"); set.add("Iraq"); set.add("Togo"); set.add("Iran"); set.add("Russie"); set.add("Tchad"); set.add("Chine"); set.add("Guam"); set.add("Fiji");

Vous remarquerez cette fois encore que, dès qu’un intervalle d’éléments est spécifié, l’élément dedébut est le premier du segment, et l’élément de fin est celui qui se trouve après le dernier élément dusegment. Par exemple, l’appel pays.subSet("Fiji","Laos") renvoie le segment qui com-mence par "Fiji" et se termine par "Iraq" qui se trouve juste avant "Laos".

[Tchad, Cuba, Fiji, Guam, Iran, Iraq, Laos, Russie, Chine, Togo]pays.first() = Tchadpays.last() = Togopays.headSet(Fiji) = [Tchad, Cuba]pays.subSet(Fiji,Laos) = [Fiji, Guam, Iran, Iraq]pays.tailSet(Laos) = [Laos, Russie, Chine, Togo]

Page 321: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 311

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

15.1 Que se passe-t-il lorsque vous essayez d’ajouter (add()) dans un ensemble un élément qui yfigure déjà ?

15.2 Que se passe-t-il lorsque vous essayez de supprimer (remove()) d’un ensemble un élément quin’y figure plus ?

15.3 Quels sont les avantages et les inconvénients de HashSet par rapport à TreeSet ?

RÉPONSES¿RÉPONSES

15.1 Toute tentative d’ajout d’un élément en double dans un ensemble via la méthode add() renverrafalse sans que l’ensemble soit modifié (reportez-vous à l’exemple 15.3).

15.2 Toute tentative de suppression d’un élément inexistant dans un ensemble via la méthoderemove() renverra false sans que l’ensemble soit modifié (reportez-vous à l’exemple 15.3).

15.3 Un objet HashSet est une table de hachage implémentée avec un chaînage séparé et un seuil dechargement par défaut égal à 75 %. C’est pourquoi il offre un temps d’accès presque égal à O(1)pour les insertions, les suppressions et les recherches. Un objet TreeSet est un arbre de recher-che binaire équilibré implémenté sur le modèle d’un arbre rouge et noir. C’est pourquoi il offreun temps d’accès égal à O(lgn) pour les insertions, les suppressions et les recherches. Nous pou-vons en déduire qu’une table de hachage HashSet présente l’avantage d’être plus rapide, etl’objet TreeSet celui d’être ordonné.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

15.1 Implémentez l’opération théorique suivante sur les ensembles pour des objets TreeSet :

• public static Set union(Set a, Set b)• // renvoie un nouvel ensemble contenant chaque élément de• // l’ensemble a, chaque élément de l’ensemble b et rien d’autre.

15.2 Implémentez l’opération théorique suivant sur les ensembles pour des objets TreeSet :

• public static Set intersection(Set a, Set b)• // renvoie un nouvel ensemble contenant uniquement les éléments• // qui se trouvent dans les ensembles a et b

15.3 Implémentez l’opération théorique suivante sur les ensembles pour des objets TreeSet :

• public static Set complement(Set a, Set b)• // renvoie un nouvel ensemble contenant uniquement les éléments• // qui se trouvent dans l’ensemble a, mais pas dans l’ensemble b

Page 322: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

312 Ensembles

15.4 Implémentez l’opération théorique suivante sur les ensembles pour des objets TreeSet :

• public static isSubset(Set a, Set b)• // renvoie true si et seulement si chaque élément de l’ensemble a• // est également un élément de l’ensemble b

SOLUTIONS¿SOLUTIONS

15.1 L’opération d’union pour les objets TreeSet est la suivante :

• public static Set union(Set a, Set b)• Set c = new TreeSet(a);• for (Iterator it=b.iterator(); it.hasNext(); )• c.add(it.next());• return c;•

15.2 L’opération d’intersection pour les objets TreeSet est la suivante :

• public static Set intersection(Set a, Set b)• Set c = new TreeSet(a);• for (Iterator it=a.iterator(); it.hasNext(); )• String x=(String)it.next();• if (!b.contains(x)) c.remove(x);• • return c;•

15.3 L’opération de soustraction (complément relatif) pour les objets TreeSet est la suivante :

• public static Set complement(Set a, Set b)• Set c = new TreeSet(a);• for (Iterator it=b.iterator(); it.hasNext(); )• c.remove(it.next());• return c;•

15.4 L’opération de sous-ensemble pour les objets TreeSet est la suivante :

• public static boolean isSubset(Set a, Set b)• for (Iterator it=a.iterator(); it.hasNext(); )• if (!b.contains(it.next())) return false;

return true;

Page 323: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chapitre 16

Graphes

16.1 GRAPHES SIMPLESUn graphe simple est un couple G = (V, E), dans lequel V et E sont des ensembles finis et chaque élémentE est un sous-ensemble de V composé de deux éléments (c’est-à-dire une paire non ordonnée d’élémentsdistincts de V). Les éléments de V sont qualifiés de sommets (ou nœuds) et les éléments de E sont quali-fiés d’arêtes (ou arcs). Si e ∈ E, alors e = a, b pour a, b ∈ V. Dans ce cas, vous pouvez représenter eplus simplement de la façon suivante : e = ab = ba. Nous disons que l’arête e connecte les sommets a etb, que e est incident à a et b, que a et b sont incidents sur e, que a et b sont les points terminaux de l’arêtee et que a et b sont adjacents. La taille d’un graphe correspond au nombre d’éléments qui composent sonensemble de sommets.

Exemple 16.1 Graphe simple

La figure ci-contre illustre le cas d’un graphe simple (V, E) de taille 4. Son ensem-ble de sommets est V = a, b, c, d, et son ensemble d’arêtes E = ab, ac, ad, bd,cd. Ce graphe est donc composé de quatre sommets et de cinq arêtes.

Par définition, une arête est une ensemble composé exactement de deux éléments. C’est pourquoiune boucle ne peut pas être une arête : elle ne comprend qu’un sommet. Nous pouvons en déduire que ladéfinition d’un graphe simple exclut les boucles.

D’autre part, étant donné que E est un ensemble, une arête ne peut pas être listée plusieurs fois (lesensembles n’autorisent pas la répétition des membres qui les composent). Nous pouvons en déduire quela définition d’un graphe simple exclut également les arêtes répétées.

En revanche les graphes généraux (qui ne sont pas nécessairement simples) autorisent les boucles etles répétitions d’arêtes.

16.2 TERMINOLOGIE DES GRAPHESSi G = (V, E) est un graphe et G′ = (V′, E′), avec V′ ⊆ V et E′ ⊆ E, alors G′ est un sous-graphe de G. Cha-que graphe est un sous-graphe maximal de lui-même.

a b

c d

Page 324: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

314 Graphes

Exemple 16.2 Sous-graphes

Le graphe G1 = (V1, E1) a un ensemble de sommets V1 = a, b, d et son ensemble de sommets E1 =ad, bd est un sous-graphe non maximal du graphe de l’exemple 16.1. Ce sous-graphe est de taille 3.Le graphe G2 = (V2, E2) a un ensemble de sommets V2 = a, b, c, d et son ensemble de sommets E2= ad, ac, cd est un sous-graphe maximal du graphe de l’exemple 16.1. Ce sous-graphe est de taille 4.

Le degré (ou la valence) d’un sommet correspond au nombre de sommets incidents. Ainsi, dans legraphe de l’exemple 16.1, le sommet a a un degré 3 et le sommet b a un degré 2. En revanche, dansle sous-graphe de l’exemple 16.2, les sommets a et b ont tous les deux un degré égal à 1.Un point isolé est un sommet de degré 0.

Théorème 16.1 : la somme des degrés des sommets d’un graphe composé de m arêtes est égale à 2m.Démonstration : chaque sommet ajoute la valeur 1 au degré de chacun des deux sommets qui le déter-minent. Par conséquent, la contribution totale de m arêtes est égale à 2m.

Un graphe complet est un graphe simple dans lequel chaque couple de sommets est connecté par unearête. Pour un nombre donné de n sommets, il n’existe qu’un seul graphe complet de cette taille. Nousfaisons donc référence au graphe complet d’un ensemble de sommets donné.

Exemple 16.3 Graphe complet à quatre sommets

La figure ci-contre illustre le cas d’un graphe complet de l’ensemble V = a, b, c, d,dont l’arête E = ab, ac, ad, bc, bd, cd.Vous remarquerez que les graphes des exemples précédents sont des sous-graphesde celui-ci.

Théorème 16.2 : le nombre d’arêtes du graphe complet composé de n sommets est égal à n(n–1)/2.Démonstration : pour chacun des n sommets, il existe n – 1 sommets dont ils peuvent être adjacents. Ily a donc n(n – 1) paires ordonnées de sommets et, par conséquent, n(n – 1)/2 paires non ordonnées parceque chaque paire non ordonnée peut être ordonnée de deux façons différentes : (a, b) ou (b, a) dans le casd’une pairea, b.

Par exemple, le nombre d’arêtes du graphe complet à quatre sommets de l’exemple 16.3 est le sui-vant : n(n –1)/2 = 4(4 –1)/2 = 6.

Corollaire 16.1 : le nombre d’arêtes d’un graphe de n sommets est égal à m ≤ n(n – 1)/2.

16.3 CHEMINS ET CYCLESLe chemin du sommet a au sommet b d’un graphe correspond à une séquence de sommets (a0a1, a1a2,ak – 1ak), avec a0 = a and ak = b et à une séquence de sommets (e1, e2,…, ek) pour lesquelles si ei connecteun sommet au sommet ai, le sommet suivant ei + 1 connecte le sommet ai à un autre sommet de façon àformer une chaîne de sommets connectés de a à b. La longueur d’un chemin est égale au nombre k desommets le formant.

a b

c d

G2a b

d

G1

a b

c d

Page 325: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chemins et cycles 315

Bien qu’un chemin soit une séquence d’arêtes, il induit naturellement une séquence de sommetsadjacents connectés par ces arêtes. Nous pouvons donc noter le chemin (a0a1, a1a2, ak – 1ak) plus simple-ment sous la forme a0a1a2…ak – 1ak tant que chaque paire (ai – 1, ai) est une arête valide du graphe.

Si p = a0a1a2…ak – 1ak est un chemin du graphe, nous pouvons dire que p est un chemin de a0 à ak (ouinversement de ak à a0), que p connecte a0 à ak et que a0 et ak sont connectés par p. Les sommets a0 et aksont également qualifiés de points terminaux ou points nodaux du chemin.

Un chemin élémentaire est un chemin qui ne contient pas plusieurs fois le même sommet.

Exemple 16.4 Chemins de graphes

Ce graphe illustre le cas d’un chemin élémentaire abcfde delongueur 5. Écrit plus formellement, il s’agit du chemin (ab,bc, cf, fd, de). En revanche, le chemin abefdbc de longueur 6n’est pas élémentaire parce que le sommet b apparaît deuxfois. C’est également le cas du chemin abefa de longueur 4dans lequel le sommet a apparaît deux fois.La séquence abf ne constitue pas un chemin étant donné que bfn’est pas une arête. De la même manière, la séquence abbn’est pas un chemin parce que bb n’est pas une arête.En dernier lieu, aba est un chemin de longueur 2 et ab un che-min de longueur 1.

Un graphe est dit connexe si, pour toute paire de sommets distincts, il existe un chemin les reliant.Dans le cas contraire, le graphe est dit non non connexe. Tous les graphes que nous venons de voir étaientconnexes.

Exemple 16.5 Graphe non connexe

Ce graphe de taille 12 n’est pas connexe.

Le composant connexe d’un graphe est un sous-grapheconnexe maximal, c’est-à-dire un sous-graphe non contenudans un autre sous-graphe fortement connexe.

Le graphe de l’exemple 16.5 comporte 5 composantsconnexes de taille 3, 1, 4, 2 et 2.

Théorème 16.3 : chaque graphe correspond à une union d’un ensemble unique de composantsconnexes.

Un chemin fermé a pour dernier sommet le premier. Un cycle est un chemin fermé de longueur égaleà 3 au minimum et dont tous les sommets internes sont distincts.

Exemple 16.6 Cycles d’un graphe

Le chemin abefa du graphe figurant dans l’exemple 16.4 est un cycle. En revanche, le chemin abe-dbcfa n’est pas un cycle parce qu’il ne s’agit pas d’un chemin (il contient deux fois le sommet b).Le chemin abef n’est pas un cycle non plus parce qu’il n’est pas fermé.Quant au chemin aba, il ne présente pas les caractéristiques d’un cycle puisque sa longueur est seu-lement égale à 2.

Un graphe est dit acyclique s’il ne contient aucun cycle.Parmi ceux que nous venons de voir, seul le graphe de l’exemple 16.2 est acyclique.

f

e

c

d

a b

Page 326: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

316 Graphes

Un graphe acyclique porte également le nom de forêt libre et un graphe acyclique connexe d’arbrelibre. Vous constaterez qu’un arbre (reportez-vous au chapitre 9) est un arbre libre dans lequel un nœudest désigné comme étant la racine. Dans le cadre des graphes, un arbre est donc qualifié d’arbre enracinéet défini comme graphe acyclique connexe avec un sommet désigné comme sa racine.

L’arbre maximal d’un graphe est un sous-graphe maximal acyclique connexe.

Exemple 16.7 Arbre maximal

La figure suivante représente un graphe et son arbre maximal.

16.4 GRAPHES ISOMORPHESLes graphes G = (V, E) et G′ = (V′, E′) sont isomorphes si la fonction f affecte à chaque sommet x de V unsommet y = f(x) dans V′ de façon à ce que les trois conditions suivantes soient respectées :

1. f est appliquée unilatéralement : chaque x de V se voit affecter un sommet différent y = f(x) dans V′.2. f est surjective : chaque y de V′ est affecté à x dans V.

3. f préserve l’adjacence : si x1, x2 est une arête de E, alors f(x1), f(x2) est une arête de E.

Deux graphes sont dits isomorphes si ce sont les mêmes graphes, mais dessinés différemment. D’unpoint de vue mathématique, cela signifie qu’ils présentent la même structure (topologie) ; d’un point devue graphique, cela implique qu’ils peuvent être pivotés dans tous les sens tant que les connexions entreles arêtes ne sont pas rompues.

Exemple 16.8 Graphes isomorphes

Les deux graphes suivants sont isomorphes :

a b

d

e

f

h

g

a b

d c

ef

h g c

Page 327: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Graphes isomorphes 317

L’isomorphisme est indiqué par l’étiquetage des sommets. Vous pouvez ainsi vérifier que le sommetx1 est adjacent au sommet x2 dans le premier graphe, et que les sommets correspondants sont égale-ment adjacents dans le second. Par exemple, le sommet a est adjacent aux sommets b, d, e, et f (maispas aux sommets c, g ou h) dans les deux graphes.

Afin de démontrer théoriquement que deux graphes sont isomorphes, il est nécessaire de trouver unisomorphisme entre eux, ce qui revient à les étiqueter avec les mêmes étiquettes pour que l’adjacences’applique de la même façon. Les chances de découvrir ainsi un isomorphisme sont peu nombreusesétant donné qu’il existe n! possibilités différentes. Par exemple, on dénombre 8! = 40 320 façons diffé-rentes d’attribuer 8 étiquettes aux 8 sommets de chaque graphe illustré dans l’exemple 16.8. C’est pour-quoi l’algorithme suivant sera plus efficace :

1. Étiquetez arbitrairement les sommets d’un graphe en utilisant ici des entiers positifs.

2. Dans le second graphe, trouvez un sommet ayant le même degré que le sommet 1 du premier grapheet attribuez-lui le numéro 1 également.

3. Étiquetez les sommets adjacents au nouveau sommet 1 avec les numéros identiques à ceux des som-mets adjacents à l’autre sommet 1.

4. Répétez l’étape 3 pour chacun des autres sommets au fur et à mesure de leur étiquetage.

Si l’étape 3 est impossible à un certain stade du processus, retournez en arrière et essayez un étique-tage différent. Si cela ne fonctionne pas, essayez de prouver que les deux graphes ne sont pas isomor-phes.

Pour démontrer théoriquement que deux graphes ne sont pas isomorphes, vous devez prouver quechaque étiquetage parmi les n! possibilités différentes est incapable de préserver l’adjacence, ce qui n’estpas très réaliste. Le théorème suivant vous permettra de démontrer beaucoup plus aisément que deuxgraphes ne sont pas isomorphes.

Théorème 16.4 : tester l’isomorphisme de deux graphes.

Pour que deux graphes soient isomorphes, toutes les conditions suivantes doivent être respectées :

1. Ils doivent comporter le même nombre de sommets.

2. Ils doivent comporter le même nombre d’arêtes.

3. Ils doivent comporter le même nombre de composants connexes.

4. Ils doivent comporter le même nombre de sommets de chaque degré.

5. Ils doivent comporter le même nombre de cycles de chaque longueur.

Exemple 16.9 Démontrer que deux graphes ne sont pas isomorphes

Comparez les trois graphes suivants aux deux graphes isomorphes de l’exemple 16.8 :

G2G1 G3

Page 328: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

318 Graphes

Chacun de ces graphes est composé de 8 sommets, ils pourraient donc tous être isomorphes des deuxautres graphes. Le graphe G1 n’est pas isomorphe aux deux autres parce qu’il ne contient que 14 arêtes,contre 16 pour les graphes de l’exemple 16.8. Or, la condition 2 du théorème 16.5 indique que des gra-phes isomorphes doivent avoir le même nombre d’arêtes. Bien que le graphe G2 contienne 16 arêtes,il n’est pas isomorphe aux deux autres non plus parce qu’il comporte 2 composants connexes, contre1 pour les graphes de l’exemple 16.8. Or, la condition 3 du théorème 16.5 indique que des graphesisomorphes doivent avoir le même nombre de composants connexes.Bien que le graphe G3 soit composé de 16 arêtes et d’un seul composant connexe, il n’est pas iso-morphe aux deux autres non plus parce qu’il comporte des sommets de degré 3 et d’autres de degré5, alors que les deux graphes de l’exemple 16.8 ont des sommets de degré 4. Or, la condition 4 duthéorème 16.5 indique que des graphes isomorphes doivent avoir le même nombre de sommets dechaque degré.

Comme vous pouvez le constater, il n’est pas nécessaire de comparer chaque graphe de l’exemple16.9 aux deux graphes de l’exemple 16.8 simultanément ; une comparaison avec chaque graphe l’unaprès l’autre suffit amplement.

Théorème 16.5 : l’isomorphisme des graphes dans une relation d’équivalence.La relation d’isomorphisme entre les graphes respecte les trois propriétés d’une relation d’équivalence :

1. Chaque graphe est isomorphe à lui-même.2. Si G1 est isomorphe à G2, alors G2 est isomorphe à G1.3. Si G1 est isomorphe à G2 et que G2 est isomorphe à G3, alors G1 est isomorphe à G3.

Corollaire 16.2 : si G1 est isomorphe à G2 et qu’un autre graphe G n’est pas isomorphe à G1, alors Gn’est pas isomorphe à G2 non plus.

16.5 MATRICE D’ADJACENCE D’UN GRAPHELa matrice d’adjacence d’un graphe (V, E) est représentée par un tableau booléen bidimensionnel boo-lean[][] a; obtenu en ordonnant les sommets V = v0, v1, …, vn – 1 et en affectant true à a[i][j]si et seulement si le sommet vi est adjacent au sommet vj.

Exemple 16.10 Matrice d’adjacence

La matrice d’adjacence du graphe de l’exemple 16.1est illustrée ci-contre :Les matrices d’adjacence présentent les caractéristi-ques suivantes :

1. Une matrice est symétrique, c’est-à-dire quea[i][j]==a[j][i] est vraie pour tout i et tout j.

2. Le nombre d’entrées true est le double du nom-bre de sommets.

3. Un ordre différent de l’ensemble de sommets Vcréera des matrices d’adjacence différentes pourle même graphe.

Les matrices d’adjacence comportent souvent lesvaleurs 0 et 1 au lieu de true et false. Dans ce cas,la matrice d’adjacence de l’exemple 16.10 auraitl’aspect ci-contre :

b

d

a

c

b da c

T

F

T

T

F

T

T

F

F

T

T

F

T

T

F

T

a b

c d

b

d

a

c

b da c

1

0

1

1

0

1

1

0

0

1

1

0

1

1

0

1

Page 329: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Matrice d’incidence d’un graphe 319

16.6 MATRICE D’INCIDENCE D’UN GRAPHELa matrice d’incidence d’un graphe (V, E) est un tableau bidimensionnel int[][] a; obtenu en clas-sant les sommets V = v0, v1, …, vn – 1 et les arêtes E = e0, e1, …, em – 1, puis en affectant 1 à a[i][j]si le sommet vi est incident à l’arête ej, et 0 dans le cas contraire.

Exemple 16.11 Matrice d’incidence

La matrice d’incidence du graphe de l’exemple 16.1est illustrée ci-contre :La première ligne indique que le sommet a est inci-dent aux arêtes 1, 2 et 3 ; la deuxième ligne indiqueque le sommet b est incident aux arêtes 1 et 4, etc.

Quel que soit le nombre de sommets et d’arêtes d’un graphe, il y aura systématiquement deuxvaleurs 1 dans chaque colonne d’une matrice d’incident. Pour comprendre la raison de cette propriété,consultez la réponse à la question de révision 16.6.

16.7 LISTE D’ADJACENCES D’UN GRAPHELa liste d’adjacences d’un graphe (V, E) contient un élément pour chaque sommet du graphe, cet élémentcontenant également une liste des sommets qui lui sont adjacents. La liste secondaire de chaque sommetest qualifiée de liste des arêtes.

Exemple 16.12 Liste d’adjacences

La liste d’adjacences du graphe de l’exemple 16.1est illustrée ci-contre :La liste des arêtes d’un sommet a contient trois élé-ments, un pour chacune des trois arêtes incidentes à a ;la liste d’incidences du sommet b a deux éléments,un pour chacune des deux arêtes incidentes à b, etc.

Vous constaterez que chaque élément de la listed’arêtes correspond à une entrée true unique dans lamatrice d’incidence du graphe. Par exemple, les trois élé-ments de cette liste pour le sommet c correspondent auxtrois true de la troisième ligne (celle du sommet c) dansla matrice d’incidence de l’exemple 16.11. La présenta-tion sous forme de tableau de tous les nœuds composantla liste d’arêtes est identique à celle de la matrice d’inci-dence. Le diagramme ci-dessus est plutôt complexe. Eneffet, bien qu’il représente les pointeurs (c’est-à-dire lesréférences) qui seraient stockés dans les listes d’arêtes,les auteurs simplifient souvent sa représentation en listantdirectement le sommet référencé au lieu des référencesindirectes comme illustré ci-contre :

Vous remarquerez que la liste d’arêtes n’est pasordonnée ; son ordre n’a en effet aucune importance.

b

d

a

c

1

1

0

0

0

1

0

1

0

1

1

0

1

0

1

0

a b

c d0

0

1

1

a

b

c

d

a

b

c

d

b

a

d

c

c

d

a

b

d

a

Page 330: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

320 Graphes

16.8 DIGRAPHESUn digraphe (ou graphe orienté) est un couple G = (V, E), V étant un ensemble fini et E un ensemble depaires ordonnées d’éléments de V. Comme dans le cas des graphes non orientés, les éléments de V sontqualifiés de sommets (ou nœuds) et les éléments de E sont qualifiés d’arêtes (ou arcs). Si e ∈ E, alors e =(a, b) pour a, b ∈ V. Dans ce cas, il est possible de noter e plus simplement sous la forme e = ab. Nousdisons alors que l’arête e part du sommet a et arrive au sommet b. Le degré sortant d’un sommet corres-pond au nombre d’arêtes qui en partent, et le degré entrant au nombre d’arêtes qui y arrivent.

Contrairement à la définition du graphe, celle du digraphe autorise naturellement une arête à arriverau sommet dont elle part. Ce type d’arête est qualifié de boucle. Un digraphe simple n’autorise pas lesboucles.

Exemple 16.13 Digraphe

Le digraphe suivant a un ensemble de sommets V = a, b, c, d et un ensembled’arêtes E = ab, ad, bd, ca, dc.Le sommet a a un degré sortant de 2 et un degré entrant de 1. Les sommets b et cont tous les deux un degré sortant de 1 et un degré entrant de 1. Le sommet d a undegré sortant de 1 et un degré entrant de 2.

Théorème 16.6 : Si G est un digraphe composé de m arêtes, alors la somme de tous ses degrés sor-tants est égale à m et la somme de tous ses degrés entrants est égale à m.Démonstration : chaque arête ajoute la valeur 1 au total de tous les degrés sortants et 1 au total de tousles degrés entrants. Chaque total doit donc être égal à m.

Un digraphe complet est composé d’une arête orientée partant de chaque sommet et allant verschaque sommet.

Exemple 16.14 Digraphe complet à six sommets

Le graphe suivant est un digraphe complet à six sommets. Il est com-posé de 15 arêtes orientées doubles ; le nombre total d’arêtes unidi-rectionnelles est donc égal à 30, soit n(n – 1) = 6(6 –1) = 6(5) = 30.

Théorème 16.7 : le nombre d’arêtes d’un digraphe complet à n som-mets est égal à n(n – 1).Démonstration : d’après le théorème 16.2, il existe n(n – 1)/2 arêtes nonorientées, soit n(n – 1)/2 arêtes orientées doubles. C’est pourquoi le nom-bre total d’arêtes orientées unidirectionnelles est le double de ce nombre.

Corollaire 16.3 : le nombre d’arêtes d’un digraphe à n sommets est égal à m n(n – 1).

Chaque digraphe est composé d’un graphe incorporé qui est obtenu en convertissant chaque arêteorientée en arête non orientée, puis en supprimant les arêtes et les boucles en double. D’un point de vuemathématique, cela revient à convertir chaque paire ordonnée (x, y) des sommets de E en un ensemblex, y, puis à supprimer tous les ensembles de taille 1 (c’est-à-dire les singletons).

Exemple 16.15 Graphe incorporé dans un digraphe

Le graphe incorporé du digraphe de l’exemple 16.13 est le graphe del’exemple 16.1.

a b

c d

a b

c d

Page 331: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chemins d’un digraphe 321

La matrice d’adjacence d’un digraphe (V, E) est un tableau booléen bidimensionnel boolean[][]a; obtenu en classant les sommets V = v0, v1, …, vn – 1, puis en affectant la valeur true à a[i][j] siet seulement si une arête part du sommet vi et arrive au sommet vj.

Exemple 16.16 Matrice d’adjacence d’un digraphe

La matrice d’adjacence du graphe de l’exemple 16.13 est la suivante :Comme vous pouvez le constater dans cette matrice, le nombre

d’entrées true est égal au nombre d’arêtes. En outre, comme dans le casdes graphes non orientés, un ordre différent de l’ensemble de sommets Vcréera des matrices d’adjacence différentes pour le même digraphe.

La matrice d’incidence d’un digraphe (V, E) est un tableau d’entiersbidimensionnel int[][] a; obtenu en classant les sommets V = v0, v1, …, vn – 1 et les arêtes E = e0,e1, …, em – 1, puis en affectant les valeurs 1 à a[i][j] et –1 à a[j][i] si une arête part du sommet viet arrive au sommet vj , et en affectant 0 dans tous les autres cas.

Exemple 16.17 Matrice d’incidence d’undigraphe

La matrice d’incidence du digraphe de l’exemple16.13 est illustrée ci-contre :La première ligne indique que les deux arêtespartent du sommet a et que l’une d’entre elles yarrive.La dernière valeur 1 se trouve dans la ligne du sommet d, dans la dernière colonne. La seule autreentrée qui ne soit pas égale à 0 dans cette colonne est la valeur –1 de la ligne du sommet c, ce quisignifie que cette arête part du sommet d et arrive au sommet c.

La liste d’adjacences d’un digraphe (V, E) contient un élémentpour chaque sommet du graphe, chacun de ces éléments comportantégalement une liste des arêtes qui partent de ce sommet. Cette listed’adjacences est identique à celle d’un graphe mais, cette fois, lesliens ne sont pas dupliqués à moins que les arêtes ne soient bidirec-tionnelles entre deux sommets.

Exemple 16.18 Liste d’adjacences d’un digraphe

La liste d’adjacences du digraphe de l’exemple 16.13 est illustréeci-contre :La liste d’arêtes du sommet a comporte deux éléments, un pourchacune des deux arêtes qui partent de ce sommet : ab et ad.

16.9 CHEMINS D’UN DIGRAPHELe chemin du sommet a au sommet b d’un digraphe correspond à une séquence de sommets (a0a1, a1a2,ak – 1ak), avec a0 = a and ak = b Comme dans le cas des chemins non orientés des graphes non orientés,les chemins orientés sont généralement abrégés par leur chaîne de sommets de la façon suivante : p =a0a1a2…ak – 1ak. Quelle que soit la notation utilisée, nous disons que le chemin part du sommet a etarrive au sommet b.

Un chemin est dit fermé s’il arrive au sommet dont il part. Un chemin élémentaire est un chemin quine contient pas plusieurs fois le même sommet. Un cycle est un chemin fermé dont tous les sommetsinternes sont distincts.

b

d

a

c

b da c

F

F

F

T

F

T

F

F

F

F

T

F

T

T

F

F

b

d

a

c

-1

1

0

0

0

1

-1

0

1

0

-1

0

0

-1

0

1

a b

c d

1

23 4

5

0

0

1

-1

a

b

c

d

Page 332: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

322 Graphes

Exemple 16.19 Chemins orientés

Dans le digraphe de l’exemple 16.13, adcabdc est un chemin de longueur 6 qui n’est pas fermé. Enrevanche, le chemin abdcacda est fermé, mais il ne s’agit pas pour autant d’un cycle parce que lessommets internes d (et c) sont répétés. Le chemin dcab est élémentaire et non fermé. Le chemincabdc est un cycle de longueur 4 et le chemin dcad est un cycle de longueur 3.

Vous remarquerez que des cycles différents peuvent parcourir les mêmes sommets. Ainsi, adca etcadc sont des cycles distincts du digraphe de l’exemple 16.13.

Un digraphe est fortement connexe s’il existe un chemin entre chaque paire de sommets. À l’inverse,il est faiblement connexe si son graphe incorporé est connexe. Un digraphe qui n’est pas faiblementconnexe est dit non connexe.

Exemple 16.20 Digraphes fortement et faiblement connexes

Le digraphe G1 est fortement connexe (et donc également faiblement connexe). Le digraphe G2 estfaiblement connexe, mais il n’est pas fortement connexe parce qu’aucun chemin n’arrive au sommet x.Quant au digraphe G3, il est non connexe.

16.10 DIGRAPHES PONDÉRÉS ET GRAPHESUn digraphe pondéré est un couple (V, w) dans lequel V est un ensemble fini de sommets et w une fonc-tion qui affecte à chaque paire (x, y) un entier positif ou le symbole de l’infini ∞. La fonction w est qua-lifiée de fonction pondérée et sa valeur w(x, y) peut être interprétée comme le coût (ou le temps, ouencore la distance) de déplacement direct de x à y. La valeur w(x, y) = ∞ indique qu’il n’existe aucunearête entre x et y.

Un graphe pondéré est un digraphe pondéré (V, w) dont la fonction w est symétrique, c’est-à-dire quew(x, y) = w(x, y) pour tout x, y ∈ V. De la même façon qu’un digraphe comporte un graphe incorporé, toutdigraphe pondéré comporte un graphe pondéré incorporé (V, w). La fonction pondérée de ce dernier peutêtre définie de la façon suivante : w′(x,y) = minw(x,y), w(y,x), w étant la fonction pondérée du digraphepondéré. L’ensemble de sommets du graphe incorporé peut être défini comme E = (x,y) : w(x,y) < ∞.

Les propriétés que nous avons déjà énumérées pour les graphes et les digraphes s’appliquent égale-ment aux graphes et aux digraphes pondérés. En outre, de nouvelles propriétés peuvent être ajoutéesselon la fonction pondérée sous-jacente. Par exemple, la longueur du chemin pondéré est égale à lasomme du poids des arêtes du chemin. Et la distance la plus courte de x à y est égale à la longueur duchemin pondéré minimum parmi tous les chemins qui vont de x à y.

Exemple 16.21 Digraphe pondéré et ses structures incorporées

La figure suivante illustre un digraphe pondéré et son graphe pondéré incorporé, son digraphe incor-poré et son graphe incorporé. Les pondérations sont indiquées sur les arêtes.

G2G1 G3

x

Page 333: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Chemins et cycles eulériens et hamiltoniens 323

Dans le graphe G1, la longueur du chemin pondéré cabd est égale à |cabd| = 2 + 3 + 2 = 7 et la dis-tance la plus courte de c à d est de 6 (le long du chemin cad). Cependant, dans le graphe G2, ladistance la plus courte est égale à 1 (le long du chemin cd). Vous remarquerez que G3 est identiqueau graphe de l’exemple 16.13 et que G4 est identique au graphe de l’exemple 16.1. Les matricesd’adjacence, d’incidence et de listes du graphe G1 sont les suivantes :

16.11 CHEMINS ET CYCLES EULÉRIENS ET HAMILTONIENS

Un chemin eulérien d’un graphe comprend chaque arête une seule fois. Un cycle eulérien est un cheminfermé qui comprend chaque arête une seule fois. Quant au graphe eulérien, il comporte un cycle eulérien.

Vous constaterez que les chemins et les cycles eulériens n’ont pas besoin d’avoir des sommetsdistincts ; il ne s’agit donc pas de chemins stricts.

Exemple 16.22 Chemins et cycles eulériens

Dans le graphe suivant, le chemin fermé acedabefdbcfa est uncycle eulérien ; vous êtes donc en présence d’un graphe eulérien.Vous remarquerez que chaque sommet du graphe a un degré 4et que ses 12 arêtes sont réparties en trois cercles. Commel’indique le théorème suivant, ces deux propriétés garantissentsystématiquement qu’un graphe est eulérien.

Théorème 16.8 : graphes eulériensSi G est un graphe connexe, les conditions suivantes sont vraies :

1. G est eulérien.2. Chaque sommet a un degré pair.3. L’ensemble de toutes les arêtes de G peut être divisé en cycles.

a b

c d

3

242

1

a b

c d

3

242

1

a b

c d

a b

c d

Un digraphe pondéré

G2G1 G3 G4

Son graphe pondéré incorporé

Son digraphe incorporé

Son graphe incorporé

b

d

a

c

b da c

∞∞

2

3

∞∞

∞∞

1

2

4

∞∞

b

d

a

c

-3

3

0

0

0

4

-4

0

2

0

-2

0

0

-2

0

2

0

0

1

-1

a 3 4

b 2

c 5

d 1

f

e

c

d

a b

Page 334: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

324 Graphes

Le chemin hamiltonien d’un graphe comprend chaque sommet une seule fois. Un cycle hamiltonienest un cycle qui comprend chaque sommet une seule fois. Un graphe hamiltonien est un graphe qui com-porte un cycle hamiltonien.

Malheureusement, il n’existe pas de théorème similaire au théorème 16.8 pour les graphes hamil-toniens. Il s’agit d’ailleurs de l’un des plus gros problèmes encore insolubles à l’heure actuelle dans ledomaine de l’informatique.

Exemple 16.23 Graphes hamiltoniens

Le graphe de gauche est hamiltonien, contrairement à celui de droite qui admet un chemin hamilto-nien, mais pas de cycle hamiltonien.

16.12 ALGORITHME DE DIJKSTRAL’algorithme de Dijkstra trouve le chemin le plus cours d’un sommet v0 à un autre sommet du digraphe.Une fois qu’il a terminé, la longueur de la distance la plus courte de v0 à v est stockée dans le sommet vet le chemin le plus court de v0 à v est enregistré dans les pointeurs de v et les autres sommets du chemin(voir l’exemple 16.24). L’algorithme utilise une file de priorité : il l’initialise à l’aide de tous les som-mets, puis il retire un sommet de la file à chaque itération.

Algorithme 16.1 Algorithme Dijkstra

(Condition préalable : G = (V,w) est un graphe pondéré de sommet initial v0.)

(Condition postérieure : chaque sommet v de V stocke la distance la plus courte de v0 à v et une réfé-rence au sommet précédent sur ce chemin le plus court.)

1. Initialisez le champ de distance à 0 pour v0 et à ∞ pour chacun des sommets.

2. Insérez dans la file de priorité Q tous les sommets, la priorité la plus élevée étant attribuée à lavaleur la plus basse du champ de distance.

3. Répétez les étapes 4 à 10 jusqu’à ce que Q soit vide.

4. (Invariant : les champs de distance et de référence de chaque sommet ne se trouvant pas dans Qsont corrects.)

5. Retirez de la file de priorité le sommet ayant la priorité la plus élevée afin de l’insérer dans x.

6. Effectuez les étapes 7 à 10 pour chaque sommet y adjacent à x et se trouvant dans la file depriorité.

7. Supposons que s soit la somme du champ de distance de x et du poids de l’arête allant de x à y.

8. Si s est inférieur au champ de distance de y, effectuez les opérations 9 à 10 ; dans le cas contraire,retournez à l’étape 3.

9. Affectez s au champ de distance de y.

10. Affectez x au champ de référence de y.

Page 335: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithme de Dijkstra 325

Exemple 16.24 Tracer l’algorithme de Dijkstra

La trace de l’algorithme 16.1 sur un graphe de 8 sommets est la suivante :

À chaque itération, les sommets qui se trouvent encore dans la file de priorité sont mis en grisé et lesommet x est étiqueté. Les champs de distance de chaque sommet sont indiqués comme étant adja-cents au sommet et les pointeurs sont dessinés sous forme de flèches. Au cours de la première itéra-tion, le sommet ayant la plus grande priorité est x = A parce que son champ de distance est égal à 0 etque tous les autres sont infinis. Les étapes 7 à 10 sont répétées 3 fois, une fois pour chaque voisin deA y = B, C et D. Les valeurs de s calculées pour ces voisins sont de 0 + 4 = 4, 0 + 6 = 6 et 0 + 1 = 1,soit des nombres inférieurs à la valeur infinie courante du champ de distance correspondant. C’estpourquoi ces trois valeurs sont affectées et les pointeurs des trois voisins définis de façon à pointervers A. Lors de la deuxième itération, le sommet ayant la priorité la plus élevée dans la file de prioritéest x = D qui a un champ de distance de 1. Les étapes 7 à 10 sont à nouveau répétées trois fois, unefois pour chaque voisin non visité de D y = B, F et G. Les valeurs de s calculées pour ces voisins sontde 1 + 4 = 5, 1 + 2 = 3 et 1 + 6 = 7, soit des nombres inférieurs à la valeur courante du champ dedistance correspondant. C’est pourquoi ces trois valeurs sont affectées et les pointeurs sont définisde façon à pointer vers D. Remarquez comment ces nouvelles opérations ont modifié le champ dedistance et le pointeur du sommet B.

Lors de la troisième itération, le sommet de la file de priorité ayant la priorité la plus élevée est x = Favec un champ de distance égal à 3. Les étapes 7 à 10 sont à nouveau répétées trois fois, une foispour chaque voisin de F non visité y = C, G et H. Les valeurs de s calculées pour ces trois voisinssont égales à 3 + 1 = 4, 3 + 3 = 6 et 3 + 5 = 8, soit des nombres inférieurs à la valeur courante. C’estpourquoi elles sont affectées et les pointeurs sont définis de façon à pointer vers F. Remarquez com-ment ces opérations modifient à nouveau le champ de distance et le pointeur du sommet B.

∞∞

∞0

∞∞GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6

A

∞4

60

∞1GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6D

A

GD

A C F H

B E

Ax

∞4

50

3

71GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6D

A F

∞4

40

8

3

61GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6

B

F

GD

C F H

B E

G

C F H

B E

x

x

Page 336: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

326 Graphes

Lors de la quatrième itération, le sommet de la file de priorité ayant la priorité la plus élevée est x =B avec un champ de distance de 4. Les étapes 7 à 10 sont répétées deux fois pour y = C et E. Lesvaleurs de s calculées pour ces deux sommets sont 4 + 3 = 7 et 4 + 5 = 9. Ce dernier nombre étantinférieur à la valeur courante infinie de E, son champ de distance se voit affecter la valeur 9 et sonpointeur est défini de façon à pointer vers B. Cependant, étant donné que la valeur 7 n’est pas infé-rieure au champ de distance courant de C, son champ n’est pas modifié. L’algorithme poursuit avecles itérations restantes, pour x = C, E, G et H de la même façon :

Le résultat final vous indique par exemple que le chemin le plus court de A à E est ADFCE. Il a unelongueur de 6.

94

4

8

3

61GD

C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6

B

D

C

64

40

8

3

61GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6D

A C

E

GD

C H

B E

GD

A C H

E

A

D

A x

D

x

0A

64

40

7

3

61D

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6 GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6

64

40

7

3

61

H

D

A C F H

B

4 3

6

1 4

5

2

1

2 3

1

5

3

6

xE

G

H

x G

GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6

64

40

7

3

61GD

A C F H

B E

4 3

6

1 4

5

2

1

2 3

1

5

3

6

64

40

7

3

61

HHx

Page 337: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithme de Dijkstra 327

Exemple 16.25 Implémenter de l’algorithme de Dijkstra en Java

Le programme Java suivant implémente l’algorithme 16.1 en définissant une classe Network dontles instances représentent des digraphes pondérés :

public class Network Vertex start;

private class Vertex Object object; Edge edges; Vertex nextVertex; boolean done; int dist; Vertex back;

private class Edge Vertex to; int weight; Edge nextEdge;

public Network() if (start != null) start.dist = 0; for (Vertex p = start.nextVertex; p != null; p = p.nextVertex) p.dist = Integer.MAX_VALUE; // infini

public void findShortestPaths() // implémente l’algorithme de Dijkstra : for (Vertex v = start; v != null; v = closestVertex()) for (Edge e = v.edges; e != null; e = e.nextEdge) Vertex w = e.to; if (!w.done && v.dist+e.weight < w.dist) w.dist = v.dist+e.weight; w.back = v; v.done = true;

private Vertex closestVertex() // renvoie le sommet avec la distance minimum parmi les // sommets restants : Vertex v = null; int minDist = Integer.MAX_VALUE; for (Vertex w = start; w != null; w = nextVertex) if (!w.done && w.dist < minDist) v = w; minDist = w.dist; return v;

Dans le cadre de cette implémentation, nous avons utilisé une méthode de recherche simpleclosestVertex() au lieu d’une file de priorité. Cette méthode est moins efficace puisqu’elle estexécutée en O(n) au lieu de O(lgn) pour la file de priorité.

Page 338: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

328 Graphes

16.13 ALGORITHMES DE PARCOURS DES GRAPHESLes chemins créés par l’algorithme de Dijkstra créent un arbre couvrant minimal pour le graphe. Cet arbrecouvrant a la longueur pondérée totale minimale pour le graphe, c’est-à-dire qu’aucun autre arbre n’a delongueur totale plus petite. Cet arbre est créé en largeur d’abord, en considérant les sommets adjacents ausommet courant à chaque itération. Il s’agit de l’une des deux méthodes de parcours d’un graphe.

L’algorithme de recherche en largeur d’abord est globalement identique à l’algorithme de Dijkstra sivous ne tenez pas compte des champs de distance.

Algorithme 16.2 Algorithme de recherche en largeur d’abord

(Conditions préalables : G = (V,E) est un graphe ou un digraphe dont le sommet initial est v0 ; chaquesommet a un champ booléen visité initialisé à false ; T est un ensemble vide d’arêtes ; L est uneliste vide de sommets).(Condition postérieure : L liste les sommets en largeur d’abord et T est un arbre couvrant construit enlargeur pour G.)

1. Initialisez la file vide Q en vue du stockage temporaire des sommets.2. Insérez v0 dans Q.3. Répétez les étapes 4 à 6 tant que Q n’est pas vide.4. Retirez les sommets de Q et insérez-les dans x.5. Ajoutez x à L.6. Effectuez l’étape 7 pour chaque sommet y adjacent à x.7. Si y n’a pas été visité, effectuez les étapes 8 à 9.8. Ajoutez l’arête xy à T.9. Insérez y dans Q.

Exemple 16.26 Tracer l’algorithme de recherche en largeur d’abord

Vous trouverez ci-après la trace de l’algorithme 16.2 pour le graphe suivant :

Dans le cas présent, le sommet de départ est v0 = A.Le parcours de recherche en largeur obtenu est renvoyé dans la listeL = (A, B, E, C, F, D, G) et l’arbre minimal de recherche en largeurcréé est renvoyé dans l’ensemble T = AB, AE, BC, BF, CD, CD.

Q x L y T

A A A B AB

B E AB,AE

B,E B C AB, AE, BC

E, C A, B F AB, AE, BC, BF

E, C, F E A, B, E

C, F C A, B, E, C D AB, AE, BC, BF, CD

F, D G AB, AE, BC, BF, CD, CG

F, D, G F A, B, E, C, F

D, G D A, B, E, C, F, D

G G A, B, E, C, F, D, G

D

G

BA

F

C

E

D

G

BA

F

C

E

Page 339: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithmes de parcours des graphes 329

L’algorithme de recherche en profondeur d’abord utilise une pile au lieu d’une file.

Algorithme16.3 Algorithme de recherche en profondeur d’abord

(Conditions préalables : G = (V,E) est un graphe ou un digraphe de sommet initial v0 ; chaque som-met a un champ booléen visité initialisé à false ; T est un ensemble vide d’arêtes ; L est une listevide de sommets.)(Condition postérieure : L liste les sommets en profondeur d’abord et T est un arbre couvrantconstruit en profondeur pour G.)

1. Initialisez une pile vide S en vue du stockage temporaire des sommets.2. Ajoutez v0 à L.3. Poussez v0 dans S.4. Indiquez que v0 a été visité.5. Répétez les étapes 6 à 8 tant que S n’est pas vide.6. Supposons que x soit l’élément supérieur de S.7. Si des sommets adjacents de x n’ont pas encore été visités, effectuez les étapes 9 à 13.8. Sinon, dépilez S et retournez à l’étape 5.9. Supposons que y soit un sommet non visité adjacent à x.

10. Ajoutez l’arête xy à T.11. Ajoutez y à L.12. Poussez y dans S.13. Indiquez que y a été visité.

Exemple 16.27 Tracer l’algorithme de rechercheen profondeur d’abord

Vous trouverez ci-après la trace de l’algorithme 16.3 pour legraphe ci-contre identique à celui de l’exemple 16.26 :

Dans le cas présent, le sommet de départ est v0 = A.

L S x y T

A A A B AB

A, B A, B B C AB,BC

A, B, C A, B, C C D AB, BC, CD

A, B, C, D A, B, C, D D G AB, BC, CD, DG

A, B, C, D, G A, B, C, D, G G

A, B, C, D D

A, B, C C F AB, BC, CD, DG, CF

A, B, C, D, G, F A, B, C, F F

A, B, C C

A, B B E AB, BC, CD, DG, CF, BE

A, B, C, D, G, F, E A, B, E E

A, B B

A A

D

G

BA

F

C

E

Page 340: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

330 Graphes

Le parcours de recherche en profondeur obtenu est renvoyé dans laliste L = (A, B, C, D, G, F, E) et l’arbre minimal de recherche enprofondeur créé est renvoyé dans l’ensemble T = AB, BC, CD,DG, CF, BE.

Étant donné que le parcours en profondeur d’abord utilise une pile, il a une version récursivenaturelle.

Algorithme 16.4 algorithme récursif de recherche en profondeur d’abord

(Conditions préalables : G = (V,E) est un graphe ou un digraphe de sommet initial x ; chaque sommeta un champ visité booléen initialisé à false ; T est un ensemble global d’arêtes ; L est une listeglobale de sommets.)(Conditions postérieures : L liste les sommets en profondeur et T est un arbre couvrant construit enprofondeur pour G.)

1. Indiquez que x a été visité.2. Ajoutez x à L.3. Répétez les étapes 4 à 5 pour chaque sommet y non visité adjacent à v.4. Ajoutez l’arête xy à T.5. Appliquez l’algorithme de recherche en profondeur au sous-graphe avec le sommet initial y.

Exemple 16.28 Tracer l’algorithme récursif de recher-che en profondeur

Vous trouverez ci-après la trace de l’algorithme 16.4 pour le grapheci-contre identique à celui de l’exemple 16.26 :

Dans le cas présent, le sommet de départ est v0 = A.Le résultat obtenu est bien évidemment identique à celui de l’exemple 16.27. La seule différencenotable est que la pile explicite S a été remplacée par la pile du système qui assure le suivi des appelsrécursifs.

L x y T

A A B AB

A, B B C AB,BC

A, B, C C D AB, BC, CD

A, B, C, D D G AB, BC, CD, DG

A, B, C, D, G G

D

C F AB, BC, CD, DG, CF

A, B, C, D, G, F F

C

B E AB, BC, CD, DG, CF, BE

A, B, C, D, G, F, E E

B

A

D

G

BA

F

C

E

D

G

BA

F

C

E

Page 341: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Algorithmes de parcours des graphes 331

Exemple 16.29 Implémenter les algorithmes de parcours des graphes

Le programme Java suivant implémente les deux algorithmes de parcours pour la classe Networkque nous avons déjà vue dans le cadre de l’exemple 16.25 :

public class Network Vertex start;

private class Vertex Object object; Edge edges; Vertex nextVertex; boolean visited;

private class Edge Vertex to; int weight; Edge nextEdge;

public static void visit(Vertex x) System.out.println(x.object);

public void breadthFirstSearch() if (start == null) return; Vector queue = new Vector(); visit(start); start.visited = true; queue.addElement(start); while (!queue.isEmpty()) Vertex v = queue.firstElement(); queue.removeElementAt(0); for (Edge e = v.edges; e != null; e = e.nextEdge) Vertex w = e.to; if (!w.visited) visit(w); w.visited = true; queue.addElement(w);

public void depthFirstSearch() if (start != null) depthFirstSearch(start);

public void depthFirstSearch(Vertex x) visit(x); x.visited = true; for (Edge e = x.edges; e != null; e = e.nextEdge) Vertex w = e.to; if (!w.visited) depthFirstSearch(w);

Ce programme utilise la version récursive de la recherche en profondeur qui requiert l’utilisationd’une méthode depthFirstSearch() sans aucun paramètre afin de lancer la méthode récursivedepthFirstSearch().

Page 342: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

332 Graphes

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

16.1 Quelle est la différence entre un graphe et un graphe simple ?

16.2 Dans un graphe non orienté, une arête peut-elle être un chemin ?

16.3 Quelle est la différence entre des sommets connexes et des sommets adjacents ?

16.4 En utilisant uniquement la définition de l’isomorphisme des graphes, est-il plus facile de démon-trer que deux graphes sont isomorphes ou bien qu’ils ne le sont pas ? Pourquoi ?

16.5 Les cinq conditions exposées dans le théorème 16.4 sont-elles suffisantes pour démontrer quedeux graphes sont isomorphes ?

16.6 Pourquoi la définition naturelle d’un graphe simple interdit-elle les boucles tandis que la défini-tion naturelle d’un digraphe les autorise ?

16.7 Les affirmations suivantes sont-elles vraies ou fausses :

a. Si un graphe a n sommets et n(n – 1)/2 arêtes, il est nécessairement complet.b. La longueur d’un chemin doit être inférieure à la taille du graphe.c. La longueur d’un cycle doit être égale au nombre de sommets distincts qu’il contient.d. Si la matrice d’incidence d’un graphe a n lignes et n(n – 1)/2 colonnes, le graphe est nécessai-

rement complet.e. Dans la matrice d’incidence d’un digraphe, la somme des entrées de chaque ligne est égale au

degré entrant du sommet.f. La somme de toutes les entrées d’une matrice d’incidence d’un graphe est égale à 2|E|.g. La somme de toutes les entrées d’une matrice d’incidence d’un digraphe est toujours égale à 0.

16.8 Un graphe (V, E) est qualifié de dense si |E| = Θ(|V|2), et de creux si |E| = O(|V|).

a. Parmi les trois types de représentations que nous avons vues (matrice d’adjacence, matriced’incidence et liste d’adjacences), laquelle serait la mieux adaptée à un graphe dense ?

b. Laquelle serait la mieux adaptée à un graphe creux ?

RÉPONSES¿RÉPONSES

16.1 Un graphe est dit simple lorsqu’il ne contient aucune boucle ni aucune répétition d’arêtes.

16.2 Non, dans un graphe non orienté, une arête ne peut pas être un chemin. En effet, une arête estnécessairement composée d’un ensemble de deux éléments (c’est-à-dire une paire non ordon-née), tandis qu’un chemin est une séquence (c’est-à-dire une liste ordonnée de sommets).

16.3 Deux sommets sont dits connexes s’il existe un chemin entre eux. Deux sommets sont dits adja-cents s’ils forment une arête.

16.4 En utilisant uniquement la définition de l’isomorphisme des graphes, il est aisé de démontrer quedeux graphes sont isomorphes parce qu’il suffit de trouver un isomorphisme et de le vérifier. Enrevanche, pour démontrer à partir de cette définition que deux graphes ne sont pas isomorphes, ilest nécessaire de vérifier si chacune des n! fonctions unilatérales n’est pas un isomorphisme.

16.5 Non, les cinq conditions du théorème 16.4 ne suffisent pas pour que deux graphes soient consi-dérés comme isomorphes. Des graphes non isomorphes peuvent tout à fait répondre à ces carac-téristiques (reportez-vous à l’exercice d’entraînement 16.7).

Page 343: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 333

16.6 La définition naturelle d’un graphe interdit les boucles parce que l’arête d’un graphe est unensemble composé de deux éléments qui doivent être distincts. En revanche, d’après la définitionnaturelle d’un digraphe, une arête est une paire ordonnée, ce qui signifie que les deux compo-sants peuvent être identiques.

16.7 a. Vraieb. Vraie.c. Vraie.d. Vraie.e. Fausse.f. Vraie.g. Vraie.

16.8 La matrice d’adjacence convient mieux à un graphe dense parce qu’elle est compacte et permetun accès direct rapide. En revanche, c’est la liste d’adjacences qui est la mieux adaptée à un gra-phe creux parce qu’elle permet d’insérer et de supprimer aisément des arêtes.

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

16.1 Déterminez les propriétés suivantes de ce graphe :

a. sa taille n ;b. son ensemble de sommets V ;c. son ensemble d’arêtes E ;d. le degré d(x) de chaque sommet x ;e. un chemin de longueur 3 ;f. un chemin de longueur 5 ;g. un cycle de longueur 4 ;h. un arbre maximal ;i. sa matrice d’adjacence ;j. sa matrice d’incidence ;k. sa liste d’adjacences.

16.2 Déterminez les propriétés suivantes de ce digraphe :

a. sa taille n ;b. son ensemble de sommets V ;c. son ensemble d’arêtes E ;d. le degré entrant id(x) de chaque sommet x ;e. le degré sortant od(x) de chaque sommet x ;f. un chemin de longueur 3 ;g. un chemin de longueur 5 ;h. un cycle de longueur 4 ;i. sa matrice d’adjacence ;j. sa matrice d’incidence ;k. sa liste d’adjacences.

a

d

c

eb f

f

cb

ed

a

Page 344: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

334 Graphes

16.3 Dessinez un graphe complet de n sommets pour n = 2, 3, 4, 5 et 6.

16.4 Déterminez si le graphe G1 ci-après est eulérien ou hamiltonien.

16.5 Déterminez si le graphe G2 ci-après est eulérien ou hamiltonien.

16.6 Pour chacun des sous-graphes suivants du graphe présenté dans l’exemple 16.7, indiquez s’il estconnexe, acyclique et/ou maximal.

G1 G2

a. b.

c. d.

e. f.

Page 345: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 335

16.7 Trouvez deux graphes non isomorphes pour lesquels les cinq conditions du théorème 16.4 sontvraies.

16.8 Démontrez le corollaire 16.2.

16.9 Décrivez la matrice d’adjacence d’un graphe complet composé de n sommets.

16.10 Décrivez la matrice d’incidence d’un graphe complet composé de n sommets.

16.11 Supposons que G1 soit un graphe représenté par sa liste d’adjacences suivante :

a. Dessinez G1.b. S’agit-il d’un graphe orienté ?c. S’agit-il d’un graphe fortement connexe ?d. S’agit-il d’un graphe faiblement connexe ?e. S’agit-il d’un graphe acyclique ?f. Dessinez sa matrice d’adjacence.

g. h.

i. j.

k. l.

ABCDEF

FCBACE

BD

Page 346: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

336 Graphes

16.12 Supposons que G2 soit un graphe dont la matrice d’adjacence est la suivante :

a. Dessinez G2.b. S’agit-il d’un graphe simple ?c. S’agit-il d’un digraphe ?d. S’agit-il d’un digraphe fortement connexe ?e. S’agit-il d’un digraphe faiblement connexe ?f. S’agit-il d’un digraphe acyclique ?

16.13 En vous basant sur le digraphe pondéré G2 suivant :

a. Dessinez la matrice d’adjacence de ce graphe.b. Dessinez la liste d’adjacences de ce graphe.c. Ce graphe est-il connexe ? Justifiez votre réponse.d. Ce graphe est-il acyclique ? Justifiez votre réponse.

16.14 Parmi les graphes suivants, lesquels sont isomorphes ? Vous remarquerez qu’ils ont tous unetaille égale à 10.

2

341

2

4

3

A

E

CD

B

G20 1 0 1 01 0 1 0 10 1 0 1 01 1 1 0 10 1 1 0 1

G1 G2

G3 G4

G5

G6

G7

Page 347: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 337

16.15 En vous basant sur les graphes G1 (graphe que nous avons déjà vu dans l’exemple 16.22) et G2suivants :

a. Déterminez s’ils sont isomorphes.b. Trouvez un cycle eulérien dans G2 ou bien expliquez pourquoi il n’en comporte pas.c. Trouvez un cycle hamiltonien dans G2 ou bien expliquez pourquoi il n’en comporte pas.

16.16 Un graphe de type roue composé de n sommets est un graphe de taille n + 1avec un n-cycle dans lequel chacun des n sommets est également adjacent àun sommet central commun unique. Par exemple, le graphe de type rouesuivant est composé de 6 sommets.

En gardant ces propriétés présentes à l’esprit, décrivez :a. la matrice d’adjacence d’un graphe de type roue composé de n sommets ;b. la matrice d’incidence d’un graphe de type roue composé de n sommets ;c. la liste d’adjacences d’un graphe de type roue composé de n sommets.

16.17 Tracez l’algorithme de Dijkstra (algorithme 16.1) du graphe G8 ci-après en illustrant son cheminle plus court et la distance qui sépare le nœud A de chaque autre nœud.

16.18 Tracez l’algorithme de Dijkstra (algorithme 16.1) du graphe G9 ci-après en illustrant son cheminle plus court et la distance qui sépare le nœud A de chaque autre nœud.

16.19 Comme nous l’avons déjà vu, il existe quatre algorithmes standard de parcours des arbres binai-res : le parcours préfixe, le parcours infixe, le parcours postfixe et le parcours en largeur. Si vousconsidérez qu’un arbre binaire est un graphe acyclique connexe, quel parcours d’arbre obtien-drez-vous si vous effectuez une recherche :

a. en profondeur d’abord ;b. en largeur d’abord.

G1 G2

4

2

1

5

3

3

4

4

2

1

2

1

A

B

C

D

E

F

G

GH F

IA J

CB D

E

5 1

2 1

5

3

3

3

6

4

31 2 1

1 12

G9G8

5

Page 348: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

338 Graphes

16.20 Appliquez l’algorithme de parcours indiqué sur legraphe ci-contre :

Donnez l’ordre des sommets visités et dessinezl’arbre maximal obtenu.a. Tracez la recherche en largeur d’abord.b. Tracez la recherche en profondeur d’abord, en

commençant par le nœud A et en imprimantl’étiquette de chaque nœud une fois qu’il a étévisité.

16.21 Pour le digraphe pondéré G1 ci-contre :

a. Dessinez la matrice d’adjacence.b. Dessinez la matrice d’incidence.c. Dessinez la liste d’adjacences.

SOLUTIONS¿SOLUTIONS

16.1 a. n = 6b. V = a, b, c, d, e, fc. E = ab, bc, bd, cd, ce, de, cf, dfd. d(a) = 1, d(b) = 3, d(e) = d(f) = 2, d(c) = d(d) = 4.e. Le chemin abcd a une longueur égale à 3.f. Le chemin abcfde a une longueur égale à 5.g. Le cycle bcedb a une longueur égale à 4.h. L’arbre maximal est illustré ci-contre :

i. La matrice d’adjacence est la suivante :

ED F

BA C

G

PO Q

LK M

H

N

JI

G1 A

D C

3

2

5

B

176

4

a

d

c

eb f

a b

d

c

e f

b

d

a

c

b da c

F

F

F

F

F

T

T

T

T

F

T

F

T

F

F

T

fe

fe

F

F

T

T

F

F

T

T

F

F

F

F

T

T

T

T

F

F

F

F

Page 349: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 339

j. La matrice d’incidence est la suivante :

k. La liste d’adjacences est illustrée ci-contre :

16.2 a. n = 6b. V = a, b, c, d, e, fc. E = ad, ba, bd, cb, cd, ce, cf, de, ec, fed. id(a) = id(b) = id(c) = id(f) = 1, id(d) = id(e) = 3.e. od(a) = od(d) = od(e) = od(f) = 1, od(b) = 2, od(c) = 4.f. Le chemin adec a une longueur égale à 3.g. Le chemin fecbad a une longueur égale à 5.h. Le cycle adcba a une longueur égale à 4.i. L’arbre maximal est illustré ci-contre :

j. La matrice d’adjacence est la suivante :

k. La matrice d’incidence est la suivante :

l. La liste d’adjacences est illustrée ci-contre :

16.3 Les graphes complets sont les suivants :

a

b

c

d

b

a

c

b

d

c

d

c

e

e

e

f c d

b

d

f

f

b

d

a

c

1

0

0

1

0

0

1

1

0

1

0

0

0

1

1

fe

0

0

0

1

0

0

0

1

0

0

0

0

0

0

0

0

0

1

1

0

0

0

1

0

0

0

1

0

0

1

1

0

1

f

cb

ed

a

f

cb

ed

a

b

d

a

c

b da c

T

F

F

F

F

F

F

T

F

F

F

F

T

T

F

T

fe

fe

F

F

T

T

F

F

F

T

F

F

F

F

F

T

F

F

T

F

F

F

a

b

c

d

b

a

c

e

d

d

e

e

f e

d

f

b

d

a

c

1

-1

0

1

-1

0

0

1

0

-1

0

-1

0

0

1

fe

0

0

-1

1

0

0

0

1

0

0

0

0

0

0

0

0

0

0

0

-1

0

0

0

1

0

0

1

0

-1

0

0

-1

0 0

0

0

-1

0

0

0

0

0

1

1

-1

Page 350: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

340 Graphes

16.4 Le graphe G1 ne peut pas être eulérien parce qu’il est composé desommets de degré impair, mais son cycle hamiltonien illustré dans lediagramme ci-contre vérifie qu’il est hamiltonien :

16.5 Le graphe G2 n’est ni eulérien, ni hamiltonien.

16.6 a. Non connexe, cyclique et maximal.b. Non connexe, acyclique et maximal.c. Non connexe, cyclique et maximal.d. Non connexe, cyclique et non maximal.e. Connexe, acyclique et maximal.f. Connexe, acyclique et maximal.g. Connexe, cyclique et maximal.h. Connexe, acyclique et non maximal.i. Non connexe, cyclique et maximal.j. Connexe, cyclique et non maximal.k. Non connexe, acyclique et non maximal.l. Connexe, acyclique et non maximal.

16.7 Ces deux graphes ne sont pas isomorphes parce quecelui de gauche est un 4-cycle contenant 2 sommetsde degré 2, contrairement à celui de droite. Pour-tant, les cinq conditions du théorème 16.4 sontrespectées.

16.8 Supposons que G1 et G2 soient isomorphes et que Gne soit pas isomorphe à G1. D’après le théorème16.5, G2 est également non isomorphe à G1. Si G est isomorphe à G2, d’après la partie 3 du théo-rème 16.5, G doit également être isomorphe à G1. Ainsi, puisque G est non isomorphe à G1, il nepeut pas être isomorphe à G2 non plus.

16.9 La matrice d’adjacence du graphe complet composé de n sommets est une matrice de n par mavec la valeur false à chaque entrée de la diagonale, et la valeur true pour toutes les autresentrées.

16.10 La matrice d’incidence Mn du graphe completcomposé de n sommets est illustrée ci-contre :

Elle est composée de n lignes et de n(n – 1)/2colonnes (voir le théorème 16.2).Si n = 2, il s’agit de la matrice 2-par-1 contenanttrue dans les deux entrées.Si n > 2, il s’agit de la matrice A concaténée hori-zontalement à la matrice obtenue de Mn – 1 eninsérant une ligne de valeurs false au-dessus.

16.11 a. Le digraphe G1 est présenté page suivante.b. Oui, il s’agit d’un digraphe puisqu’il contient

au moins une arête bilatérale.c. Non, le digraphe n’est pas fortement connexe

parce qu’il n’existe pas de chemin entre C et D.d. Oui, le digraphe est faiblement connexe parce que son graphe incorporé non orienté est

connexe.e. Non, le digraphe n’est pas acyclique puisqu’il contient le cycle AFEDA.

1

1

0

0

0

1

0

1

0

1

1

0

0 0

1

0

1

1

1

0

0

1

11

1

0

1

0

1

1

1

0

0

1

1

0

1

0

0

1

0

0

0

0

0

0

1

0

0

1

0

1

1

0

0

0

1

0

1

0

1

1

0

0 0 0

1 1 1 1 0 0 0 0 0 0

1

0

1

1

1

0

0

1

1

M5 =

M4 =

M3 =M2 =

Page 351: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 341

f. La matrice d’adjacence du digraphe est la suivante :

16.12 a. Le digraphe G2 est illustré ci-dessus :

b. Non, il ne s’agit pas d’un digraphe simple parce qu’il contient une boucle.

c. Oui, il s’agit d’un digraphe puisque sa matrice d’adjacence n’est pas symétrique.

d. Oui, ce digraphe est fortement connexe.

e. Oui, ce digraphe est faiblement connexe.

f. Non, ce digraphe n’est pas acyclique puisqu’il contient le cycle ADB.

16.13 Pour le graphe donné, la matrice d’adjacence et la liste d’adjacences sont les suivantes :

Il n’est pas connexe parce qu’il n’existe aucun chemin de B à A. Il n’est pas acyclique non plusparce qu’il contient le cycle BECDB.

16.14 Parmi les sept graphes suivants :

0 0 0 0 0 1

0 0 1 0 0 0

0 1 0 0 0 0

1 1 0 0 0 0

0 0 1 1 0 0

0 0 0 0 1 0

A

E

CD

B

A

D

E

BF

C

G2G1

∞ 3 4 1 ∞∞ ∞ ∞ ∞ 2

∞ ∞ ∞ 3 ∞∞ 2 3 ∞ ∞∞ ∞ 4 ∞ ∞

3 B 4 C

2 E

1 D

3 D

2 B 3 C

4 C

D

E

B

C

A

G1 G2

e

a

b

d

i

c

f g

hj

e

a

b

d

i

c

f g

hj

p

Page 352: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

342 Graphes

G1 est isomorphe à G2 : l’isomorphisme est indiqué par les étiquettes des sommets a-j. G3 est iso-morphe à G4 : l’isomorphisme est indiqué par les étiquettes des sommets p-y. G6 ne peut pas êtreisomorphe à un autre graphe puisqu’il est composé de 25 arêtes et que tous les autres n’en ontque 20. G3 (et par conséquent G4) ne peut pas être isomorphe à un autre graphe parce qu’il estcomposé d’une pyramide de quatre 3-cycles adjacents (pqr, prs, pst et ptq), ce qu’aucun autregraphe n’a, à l’exception de G6. G6 ne peut être isomorphe à aucun autre graphe parce qu’il estcomposé d’une chaîne de trois 4-cycles adjacents (ABCD, CEFG et FHIJ), contrairement à tousles autres graphes. De la même façon, G7 ne peut être isomorphe à aucun autre graphe parce qu’ilcomporte une chaîne de quatre 3-cycles adjacents (PQS, QSR, SRT et RTU), contrairement à tousles autres graphes, à l’exception de G6.

16.15 a. Ces deux graphes sont isomorphes : leur bijection est définie par les étiquettes des sommets,comme illustré ci-après.

G3

G4

G5

G6

G7

q

p

ts

r

x

y

uv

wq

t

s

r

x

y

u

v

wp

R

UTS

Q P

H

J

I

D

C

B

A

G

F

E

G1 G2A

B C

DE

F

A

B C

DE

F

Page 353: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Révision et entraînement 343

b. Le cycle eulérien de G2 est ABCDEBFCADFEA.c. Le cycle hamiltonien de G2 est ABCDFEA.

16.16 a. La matrice d’adjacence d’un graphe de type roue est similaire à la matrice A suivante.

b. La matrice d’incidence d’un graphe de type roue est similaire à la matrice B de la figure pré-cédente (lorsque n = 4). Elle sera généralement composée de n 1 et de n 0 sur la premièreligne. Sous cette ligne se trouvera la matrice d’identité (uniquement des valeurs 1 sur la diago-nale et des 0 ailleurs) suivie de la matrice au carré remplie de 1 sur la diagonale et la sous-dia-gonale. Comparez cette solution récursive à l’exercice 16.10.

c. La liste d’adjacences d’un graphe de typeroue est similaire à celui-ci.

La liste des arêtes du premier sommet (c’est-à-dire le sommet central) comporte n nœudsd’arêtes, soit un pour chaque autre sommet.Chaque autre liste de sommets est composéede trois nœuds d’arêtes, un pointant vers lesommet central (étiqueté a dans l’exemple sui-vant) et un autre pour chacun de ses voisins.

16.17 La trace de l’algorithme de Dijkstra pour le graphe G8 est la suivante :

16.18 La trace de l’algorithme de Dijkstra pour le graphe G9 est illustrée ci-dessus.

T

F

T

T

F

T

F

T

T

T

T

F

F

T

F

T

F

T

T

F

F

T

F

F

T

T

F

F

F

F

F

T

T

F

F

T

A =

a

b

c

d

d

c

f

e

b

f

c

e

f b

b

c

d

e

d

a

a

a

a

a

e f

0

1

0

0

1

0

0

0

0

0

0

1

0

0

0

0

1

1

0

0

1

0

0

1

0

0

1

1

1

1 1 1 1 0 0 0 0

0

1

0

B =

4

2

1

5

3

3

4

4

2

1

2

1

A

B

C

D

E

F

G

0

2

5

3

6

5

4

GH F

IA J

CB D

E

5 1

2 1

5

3

3

3

6

4

31 2 1

1 12

8

4

5

3

4

1

764

G8 G9

5

Page 354: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

344 Graphes

16.19 a. Si la recherche en profondeur d’abord est appliquée à un arbre, vous effectuez un parcourspréfixe.

b. Si la recherche en largeur d’abord est appliquée à un arbre, vous effectuez un parcours en lar-geur.

16.20 a. La recherche en largeur d’abord visite ABDECHFIGKLJMONPQ, comme illustré par l’arbremaximal sur la figure suivante (à gauche).

b. La recherche en profondeur d’abord visite ABCFEIHDKLMJGN, comme illustré par l’arbremaximal sur la figure suivante (à droite).

16.21 La matrice d’adjacence et la liste d’adajcences du graphe donné sont les suivantes :

ED F

BA C

G

PO Q

LK M

H

N

JI

ED F

BA C

G

PO Q

LK M

H

N

JI

A

B

C

D

3 6

1 5

2

7 4

A

0 3 0 6

0 0 1 5

0 0 0 2

7 0 0 4

=

A B

D C

3

2

5 176

4

Page 355: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Annexes

A. Mathématiques de base

Étant une science, l’informatique est basée sur des principes théoriques fondamentaux qui sont dérivés etappliqués à l’aide des mathématiques. Dans cette annexe, vous trouverez un résumé des concepts mathé-matiques nécessaires à l’étude des structures de données.

A.1 FONCTIONS PLANCHER ET PLAFONDLes fonctions plancher et plafond renvoientl’entier le plus proche d’un nombre réel donné.Le plancher de x, noté sous la forme x estl’entier le plus grand qui ne soit pas supérieur à x. Le plafond de x, noté sous la forme x est l’entier leplus petit qui ne soit pas inférieur à x. Vous pouvez également dire que le plafond et le plancher de x sontles entiers les plus proches respectivement à gauche et à droite de x.

Exemple A.1 Fonctions plancher et plafond

Si x = 2,71828, alors x = 2 et x = 3.Supposons que Z soit l’ensemble de tous les entiers et que R soit l’ensemble de tous les nombresréels. Les fonctions plancher et plafond mappent alors R dans Z, c’est-à-dire que chaque fonctionrenvoie un entier correspondant à un nombre décimal. Dans le théorème suivant, nous supposeronségalement que N est l’ensemble de tous les nombres naturels : N = n ∈ Z n ≥ 1 = 1, 2, 3, ….

Théorème A.1 : les fonctions plancher et plafond ont les propriétés suivantes pour tous les nombresréels x :

a) x = maxm ∈ Z m ≤ x.b) x ≤ x < x + 1 et x - 1 < x ≤ xc) x – 1 < x ≤ x ≤ x < x + 1d) Si n ∈ Z et que n ≤ x < n + 1, alors n = x. Si n ∈ Z et que n – 1 < x ≤ n, alors n = x.e) Si x ∈ Z, alors x = x = x.f) Si x ∉ Z, alors x < x < x.g) -x = -x et -x = -x.h) x + 1 = x + 1 et x + 1 = x + 1.

Reportez-vous à l’exercice d’entraînement A.2 pour consulter une démonstration de ce théorème.

1 2 3 4

x 1- x x x 1+<< x≤ ≤

Page 356: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

346 Annexes

A.2 LOGARITHMESLe logarithme de base b d’un nombre x est l’exposant d’une base b donnée qui crée la valeur x. Parexemple, le logarithme en base 10 de 1000 est 3 (log101000 = 3) parce que 3 est l’exposant de 10, ce quidonne comme résultat la valeur 1000 : 103 = 1000.

Dans le cadre des sciences sociales, c’est la base 10 qui est utilisée et log x au lieu de log10x ; il s’agitdu logarithme commun. En revanche, les physiciens et les mathématiciens préfèrent utiliser la base e (=2,718281828459) et écrire ln x au lieu de logex ; il s’agit du logarithme naturel. En informatique, lesscientifiques choisissent souvent d’utiliser la base 2 et d’écrire lg x au lieu de log2x ; il s’agit du loga-rithme binaire.

À l’instar des fonctions mathématiques, les logarithmes sont les inverses des fonctions exponentiel-les :

Par exemple, log2256 = 8 parce que 28 = 256. Cette équivalence peut être considérée comme unedéfinition des logarithmes pour toutes les bases b. Les propriétés suivantes découlent de cette définition.

Théorème A.2 : lois des logarithmes

Reportez-vous à l’exercice A.3 pour consulter une démonstration de ce théorème.

Exemple A.2 Appliquer les lois des logarithmes

Théorème A.3 : le logarithme binaire a les propriétés suivantes :

a) Si p ∈ Z et que 2p < n < 2p + 1, alors p = lgn et p + 1 = lgn.b) Si n ∈ Z, alors lg(n + 1) = lgn + 1.

Reportez-vous à l’exercice A.4 pour consulter une démonstration de ce théorème.

L’entier lgn est qualifié de logarithme binaire intégral de n. Il s’agit en fait du nombre de fois où npeut être divisé par 2 avant d’atteindre 1. Par exemple, le logarithme binaire intégral de 1 000 est 9parce que 1 000 peut être divisé par 2 10 fois et donner les résultats suivants : 500, 250, 125, 62, 31,15, 7, 3, 1.

Exemple A.3 Tester le logarithme binaire intégral

public class ExA01 public static void main(String[] args) System.out.println("iLg(1) = " + iLg(1));

y = logb x 0 ⇔ b y = x

a) logb(b y) = y

b) b = xc) logb uv = logb u + logb vd) logb u/v = logb u – logb v

e) logb uv = v logb uf) logb x = (log c x)/(log c b) = (logb c) (log c x)

logbx

log2 256 = log2 (28) = 8

log2 1 000 = (log10 1 000)/(log10 2) = 3/0,30103 = 9,966log2 1 000 000 000 000 = log2 10004 = 4(log2 1000) = 4(9,966) = 39,86

Page 357: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 347

System.out.println("iLg(2) = " + iLg(2)); System.out.println("iLg(3) = " + iLg(3)); System.out.println("iLg(4) = " + iLg(4)); System.out.println("iLg(10) = " + iLg(10)); System.out.println("iLg(100) = " + iLg(100)); System.out.println("iLg(1000) = " + iLg(1000)); System.out.println("iLg(10000) = " + iLg(10000));

public static int iLg(int n) int count=0; while (n > 1) n /= 2; ++count; return count;

A.3 CLASSES DE COMPLEXITÉEn informatique, les algorithmes sont classés selon leurs fonctions de complexité. Ces dernières décri-vent la durée d’exécution des algorithmes qui dépend de la taille des problèmes à résoudre. Par exemple,le tri par permutation a une complexité O(n2) parce que, si vous l’utilisez pour trier un tableau deux foisplus grand, le traitement sera quatre fois plus long : (2n2) = 4n2. Le symbole O() signifie Ordre et O(n2)Ordre n au carré.

Cette notation peut être définie avec précision en termes de limites. Si f et g sont des fonctions crois-santes sur des entiers positifs, supposons que L(f,g) exprime la limite de la façon suivante :

Cette constante peut être égale à 0, à n’importe quel nombre positif ou bien être infinie. Par exemple,si f(n) = n2 et que g(n) = n, alors L(f,g) = parce que n2/n = n3/2. Grâce à cette notation, nous pouvonsdéfinir les trois classes de complexité standard suivantes :

Dans le cas présent, M est un ensemble de toutes les fonctions croissantes positives sur des entierspositifs.

iLg(1) = 0iLg(2) = 1iLg(3) = 1iLg(4) = 2iLg(10) = 3iLg(100) = 6iLg(1000) = 9iLg(10000) = 13

L f g,( ) f n( )g n( )-----------

n ∞→lim=

o(g) = f ∈ M | L(f, g) < ∞O(g) = f ∈ M | 0 ≤ L(f, g) < ∞Θ(g) = f ∈ M | 0 < L(f, g) < ∞Ω(g) = f ∈ M | 0 < L(f, g) ≤ ∞ω(g) = f ∈ M | L(f, g) = ∞

Page 358: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

348 Annexes

Théorème A.4 : les trois ensembles o(g), Θ(g) etω(g) divisent M, c’est-à-dire que chaque fonction de Mse trouve dans un et un seul de ces trois ensembles.

Théorème A.5 :

Le diagramme précédent illustre les relations entre les cinq classes de complexité.

Cependant, bien que les cinq classes de complexité soient des ensembles de fonctions, il est pluscourant d’écrire f(n) = O(g(n)) que l’expression plus précise f ∈ O(g), et f(n) ≠ O(g(n)) plutôt quef ∉ O(g).

Exemple A.4 Classes de complexité

• n lg n = O(n2) mais n lg n ≠ Θ(n2) parce que (n lg n)/(n2) = (lg n)/n → 0 ;• n lg n = Θ(n lg n) parce que (n lg n)/(n log n) = (log2n)/(log10n) = log102, qui est une constante

positive ;• n lg n = Ω( ) mais n lg n ≠ O( ) parce que (n lg n)/( ) = lg n → ∞.

Théorème A.6 : pour toute base b > 1, logbn = O(lgn).Démonstration : d’après le théorème A.2(f), logbn = c lg n, avec c = 1/lgb = logb2.

Attention, la limite L(f, g) n’existe pas systématiquement. Dans ce cas, nous supposons que L(f,g)représente l’ensemble de toutes les limites des sous-séquences de la séquence f(n)/g(n). Ensuite, les iné-galités de L(f, g) sont interprétées comme étant obligatoires pour chaque élément de l’ensemble.

Exemple A.5 Coefficient oscillant

Supposons que f(n) = 2n et que g(n) = 4n/2. Ensuite, la séquence f(n)/g(n) = 1, 2, 1, 2, 1, 2, 1, 2,1, 2, … n’a aucune limite (unique), c’est pourquoi le symbole représenterait l’ensemble 1, 2.Étant donné que chaque élément x de cet ensemble (c’est-à-dire x = 1 et x = 2) est conforme aux iné-galités 0 < x < ∞, il s’ensuit que f = Θ(g) (et g = Θ(f)).

A.4 PREMIER PRINCIPE D’INDUCTION MATHÉMATIQUELe premier principe d’induction mathématique, également appelé induction faible, est souvent utiliséafin de démontrer des formules relatives aux nombres naturels.

Théorème A.7 : premier principe d’induction mathématique.Si P(1), P(2), P(3), P(4), … est une séquence de propositions avec les deux propriétés suivantes, alorstoutes les propositions sont vraies :

1. P(1) est vraie.2. Chaque proposition peut être déduite de ses prédécesseurs.

Ω(g)O(g)ω(g)θ(g)o(g)

O(g) = o(g) ∪ Θ(g)Θ(g) = O(g) ∩ Ω(g)Ω(g) = Θ(g) ∪ ω(g)

n n n n

Page 359: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 349

Exemple A.6 Utiliser l’induction faible

Démontrez l’inégalité 2n ≤ (n + 1)! Pour tout n ≤ 1.Cette formule affirme la séquence suivante de propositions :

P(1) : 21 ≤ 2!

P(2) : 22 ≤ 3!

P(3) : 23 ≤ 4!

P(4) : 24 ≤ 5!

etc.

Les quatre premières propositions sont sans aucun doute vraies parce que :

21 = 2 ≤ 2 = 2!

22 = 4 ≤ 6 = 3!

23 = 8 ≤ 24 = 4!

24 = 16 ≤ 120 = 5!

Toutes les autres formules pourraient aussi être vérifiées directement de cette façon, mais elles sontbeaucoup trop nombreuses pour que vous arriviez à toutes les vérifier.La première partie du théorème A.7 requiert la vérification de P(1), ce que nous avons fait dans leparagraphe précédent. Pour vérifier la deuxième partie de ce théorème, vous devez soustraire P(n) deP(n – 1). Supposons donc que P(n – 1) soit vraie pour n > 1. Cela signifie que nous supposons éga-lement que 2n – 1 n! est vraie pour n. Pour déduire P(n) de cette supposition, nous devons rechercherles relations entre P(n – 1) et P(n) ; c’est-à-dire essayer de relier les deux formules :

P(n – 1) : 2n – 1 ≤ n!

P(n) : 2n ≤ (n + 1)!

La proposition P(n) peut être réécrite de la façon suivante :

P(n) : (2)(2n – 1) ≤ (n + 1)(n!)

L’évolution de P(n – 1) à P(n) est maintenant évidente : la partie gauche de l’inégalité est multipliéepar 2, tandis que la partie droite est multipliée par n + 1. Tant que n + 1 > 2, l’augmentation de gau-che est inférieure à celle de droite. Et nous savons que n + 1 > 2 puisque n > 1. Nous arrivons donc àl’implication suivante :

2n – 1 ≤ n! ⇒ 2n ≤ (n + 1)!

soit,

P(n – 1) ⇒ P(n)

C’est ce qui est spécifié dans la deuxième partie du théorème A.7. Nous pouvons donc conclure queP(n) et vraie pour tout n ≥ 1.La première partie de ce théorème est qualifiée de base de la démonstration et la seconde partied’étape inductive. Quant à la supposition de la deuxième partie selon laquelle P(n) est vraie pour n,il s’agit de l’hypothèse inductive.

A.5 DEUXIÈME PRINCIPE D’INDUCTION MATHÉMATIQUELe deuxième principe d’induction mathématique, également qualifié d’induction forte, est presque iden-tique au premier principe ; seule l’étape inductive (la deuxième partie) diffère.

Page 360: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

350 Annexes

Théorème A.8 : deuxième principe d’induction mathématique.Si P(1), P(2), P(3), P(4), … est une séquence de propositions avec les deux propriétés suivantes, alorstoutes les propositions sont vraies :

1. P(1) est vraie.

2. Chaque proposition peut être déduite de tous ses prédécesseurs.

L’étape inductive (deuxième partie) signifie que P(n) peut être déduite de la supposition selonlaquelle toutes les propositions précédentes P(1), P(2), P(3), …, P(n – 1) sont vraies.

Exemple A.7 Démonstration du théorème fondamental de l’arithmétique

Théorème A.9 : théorème fondamental de l’arithmétique.Chaque entier positif a une représentation unique comme produit des exposants des nombres premiers :

p1n1p2

n2p3n3pk

nk

p1, p2, p3, … étant les nombres premiers : p1 = 2, p2 = 3, p3 = 5, etc., et chaque nj étant un entier non négatif.Par exemple, l’entier positif 23 115 456 correspond à la représentation unique 26 34 50 73 110 131.La démonstration applique le deuxième principe d’induction mathématique à la séquence de proposi-tions :

P(1) = 1 a une représentation unique comme produit des nombres premiers.

P(2) = 2 a une représentation unique comme produit des nombres premiers.

P(3) = 3 a une représentation unique comme produit des nombres premiers.

P(4) = 4 a une représentation unique comme produit des nombres premiers.

etc.

Ces quatre premières propositions sont vraies parce que :

1 = 20

2 = 21

3 = 31

4 = 22

La première supposition vérifie la base de l’induction. Afin de vérifier la démarche inductive, nous sup-posons que toutes les propositions P(1), P(2), P(3), …, P(n – 1) sont vraies pour n > 1.Si n a un facteur premier, appelez-le p et supposez que m = n/p. Ensuite, étant donné que m est un entierpositif inférieur à n, l’hypothèse inductive nous permet de déduire que P(m) doit être vraie, c’est-à-direque m a une représentation unique comme produit des exposants premiers. Il en va de même pour n parceque n = p·m. C’est pourquoi P(n) est vraie dans ce cas.En revanche, si n n’a pas de facteurs premiers, il doit être un nombre premier lui-même et a donc certai-nement une représentation unique comme produit des exposants premiers : n = n1. C’est pourquoi P(n)est vraie dans ce cas.La démonstration est terminée puisque nous avons déduit P(n) à partir de P(1), P(2), P(3), …, P(n – 1).

A.6 SÉRIES GÉOMÉTRIQUESUne série géométrique est une somme dans laquelle chaque terme est égal au précédent multiplié par unevaleur fixe. Par exemple, 10 + 30 + 90 + 270 + 810 + 2430 + … est une série géométrique parce que cha-que terme est égal à 3 fois son prédécesseur. Le multiplicateur 3 est qualifié de raison de la série.

Page 361: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 351

Théorème A.10 : somme d’une série géométrique finie.

Dans le cas présent, a est le premier terme de la série, r est la raison et n le nombre de termes composantla somme.

Exemple A.8 Rechercher la somme d’une série géométrique finie

10 + 30 + 90 + 270 + 810 + 2 430 = 10(1 – 36)/(1 – 3) = 10(1 – 729)/(-2) = 10(-728)/(-2) = 3 640

Théorème A.11 : somme d’une série géométrique infinie.

Cette formule est valide uniquement pour –1 < r < 1.

Exemple A.9 Rechercher la somme d’une série géométrique infinie

6 + 3 + 3/2 + 3/4 + 3/8 + … = 6/(1 – 1/2) = 6/(1/2) = 12

A.7 FORMULES DE SOMMATION

Théorème A.12 : somme des n premiers entiers positifs.

Remarquez que le paramètre n est égal au nombre determes de la somme.Pour vous souvenir plus facilement de cette formule,mémorisez la figure suivante qui est composée de deuxtriangles de points. Ces deux triangles, le premier étantblanc et le second gris, contiennent le même nombre depoints, à savoir S = 1 + 2 + 3 + … + n. Lorsqu’ils sontréunis, ils forment un rectangle de largeur égale à npoints et de hauteur égale à n + 1 points. Ainsi, le nom-bre total de points est égal à n(n + 1), soit le double de lataille de la somme S.

Exemple A.10 Rechercher la somme d’une séquence arithmétique

1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 8(9)/2 = 36

Théorème A.13 : somme des n premiers carrés.

Remarquez que cette somme est un entier même si la partie droite est une fraction.

a ar ar2 ar3 … arn 1–+ + + + + a 1 rn–( )1 r–

----------------------=

a ar ar2 ar3 …+ + + + a1 r–-----------=

1 2 3 … n+ + + + n n 1+( )2

--------------------=

12 22 32 … n2+ + + + n n 1+( ) 2n 1+( )6

-----------------------------------------=

Page 362: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

352 Annexes

Exemple A.11 Rechercher une somme de carrés

1 + 4 + 9 + 16 + 25 + 36 + 49 = 7(8)(15)/6 = 140

A.8 NOMBRES HARMONIQUESLes nombres harmoniques sont définis par la formule suivante :

Les six premiers nombres harmoniques sont illustrés dans le tableau ci-contre :La séquence harmonique a beau croître très lentement, cette croissance est

illimitée, comme le démontrent le théorème suivant et son corollaire.

Théorème A.14 : la séquence harmonique est asymptomatiquement logarithmique.La limite suivante : est une constante positive.

Corollaire A.1 : les nombres harmoniques croissent logarithmiquement.

Hn = Θ(ln n)

L’exemple suivant démontre empiriquement que le théorème A.14 est vrai.

Exemple A.12 Constante d’Euler

Afin de tester la supposition selon laquelle la limite est une constante positive, nousallons exécuter le programme suivant qui imprimera les valeurs de Hn – lnn pour toutes les valeursde n qui sont des exposants de 2 inférieures à 10 000 000 :

public class ExA10 public static void main(String[] args) double hn=0.0; // nombres harmoniques int pow2=1; // exposants de 2 for (int n=1; n<1e7; n++) hn += 1.0/n; // énième nombre harmonique if (n==pow2) // n’imprimer que les exposants de 2 double ln=Math.log(n); // logarithme naturel de n double difn = hn - ln; // s’approche de la constante d’Euler System.out.println(n+"\t"+hn+"\t"+ln+"\t"+difn); pow2 *= 2;

1 1,0 0,0 1,02 1,5 0,6931471805599453 0,80685281944005474 2,083333333333333 1,3862943611198906 0,6970389722138 2,7178571428571425 2,0794415416798357 0,63841560117716 3,3807289932289937 2,772588722239781 0,60814027098932 4,05849519543652 3,4657359027997265 0,59275929263664 4,7438909037057675 4,1588830833596715 0,585007820346128 5,433147092589174 4,852030263919617 0,581116828669256 6,124344962817281 5,545177444479562 0,579167518337512 6,81651653454972 6,238324625039508 0,578191909510

n Hn

1 1.000000

2 1.500000

3 1.833333

4 2.083333

5 2.283333

6 2.450000

+= =Hn1k---

k 1=

n

Σ 112--- 1

3--- 1

4--- 1

5--- … 1

n---+ + + + +

H nln–( )n ∞→lim

Hn nln–( )n ∞→lim

Page 363: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 353

Au fur et à mesure de la croissance de n, la différence Hn – ln n se rapproche de la constante0,5772157, plus connue sous le nom de constante d’Euler et désignée par la lettre gamma grecque :

On ignore encore si γ est un nombre rationnel.

A.9 FORMULE DE STIRLINGLa fonction factorielle est souvent utilisée dans le cadre des analyses informatiques. Dans ce contexte,l’analyse concerne la grandeur des expressions contenant n! au fur et à mesure de la croissance de n.Cependant, ces valeurs sont difficiles à obtenir directement parce qu’elles sont trop grandes comme, parexemple, 70! > 10100. La formule de Stirling est une méthode pratique qui permet d’obtenir une approxi-mation des factorielles de grande taille.

Théorème A.15 : formule de Stirling.

Dans le cas présent, e et π sont les constantes mathématiques e = 2,71828 et π = 3,14159.Le programme suivant illustre le fait que ces liaisons plus serrées s’appliquent :

Exemple A.13 Approximation de Stirling

public class ExA12 public static void main(String[] args) final double E=Math.E; // e = 2,71828 final double PI=Math.PI; // pi = 3,14159 final double INF=Double.POSITIVE_INFINITY; // = 1.8E308 double f=3628800.0; // 10! = 3,628,800 int n=10; while (f != INF) double s = Math.sqrt(2*n*PI)*Math.pow(n/E,n); double ss = Math.sqrt(2*n*PI)*Math.pow(n/E,n+1.0/(12*n));

1024 7,509175672278132 6,931471805599453 0,5777038666782048 8,202078771817716 7,6246189861593985 0,5774597856584096 8,89510389696629 8,317766166719343 0,5773377302468192 9,588190046095265 9,010913347279288 0,57727669881516384 10,281306710008463 9,704060527839234 0,57724618216932768 10,974438632012168 10,39720770839918 0,57723092361265536 11,667578183235785 11,090354888959125 0,577223294276131072 12,360721549112862 11,78350206951907 0,577219479593262144 13,053866822328144 12,476649250079015 0,577217572249524288 13,747013049214582 13,16979643063896 0,5772166185751048576 14,440159752936799 13,862943611198906 0,5772161417372097152 15,133306695078193 14,556090791758852 0,5772159033194194304 15,826453756428641 15,249237972318797 0,5772157841098388608 16,51960087738358 15,942385152878742 0,577215724504

γ Hn nln–( )n ∞→lim 0 5772157…,= =

1n!

2nπ-------------- e

n--- n

11

2n------+<<

2nπ ne---

n

n! 2nπ ne---

n 1 12n( ) /+

≤ ≤

Page 364: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

354 Annexes

System.out.println(n+"\t"+s+"\t"+f+"\t"+ss); for (int i=n+1; i<=n+10; i++) f *= i; n += 10;

Vous constaterez que l’approximation de Stirling est précise à 1 % près et que cette précision relativeaugmente parallèlement à la croissance de n. Par exemple, pour n = 100, n! = 9,3326(10178) etl’approximation de Stirling est égale à 9,3248(10178), soit une erreur absolue de 0,0078 (10178),c’est-à-dire une erreur relative de 0,08 %.

Corollaire A.2 : n! = o(nn).Démonstration : d’après la formule de Stirling, il s’agit du rapport :

La limite de l’expression se trouvant de la partie droite de l’équation est 0.

A.10 LES NOMBRES DE FIBONACCILes nombres de Fibonacci sont 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, … Chaque nombre sui-vant le second est égal à la somme des deux nombres précédents :

Les dix premiers nombres de Fibonacci sont illustrés dans le tableau ci-contre :

10 3598695,6187410373 3628800,0 3637971,795993722520 2,4227868467611351E18 2,43290200817664E18 2,4430176532620851E1830 2,6451709592296516E32 2,6525285981219103E32 2,6628732015735442E3240 8,142172644946236E47 8,159152832478977E47 8,187911721520889E4750 3,036344593938168E64 3,0414093201713376E64 3,0511169215892805E6460 8,3094383149767E81 8,320987112741392E81 8,345226643189724E8170 1,1964320047337557E100 1,197857166996989E100 1,2010678721362897E10080 7,14949447318118E118 7,156945704626378E118 7,174726163602993E11890 1,4843409438918685E138 1,4857159644817607E138 1,4891588486241144E138100 9,32484762526942E157 9,33262154439441E157 9,352904468745705E157110 1,587042784164154E178 1,5882455415227421E178 1,5914981328581203E178120 6,68485904870404E198 6,689502913449124E198 6,702464725447769E198130 6,462711405582573E219 6,466855489220472E219 6,4787535645487E219140 1,3454001771051692E241 1,346201247571752E241 1,3485604820896678E241150 5,7102107404794024E262 5,7133839564458505E262 5,7229480213358916E262160 4,7122686933249974E284 4,714723635992059E284 4,7222810411380736E284170 7,25385893454291E306 7,257415615307994E306 7,268579978559315E306

n!nn

--------- 2nπ ne---

1 12n( )/

1e--

n≤

n Fn

0 0

1 1

2 1

3 2

4 3

5 5

6 8

7 13

8 21

9 34

10 55

Fn

0, si n = 0

1, si n = 1

Fn 1– Fn 2– , si n > 1+

=

Page 365: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 355

A.11 NOMBRE D’ORLe nombre d’or est la constante mathématique suivante :

En fait, il s’agit de la solution apportée au problème suivant de la Grèce antique : comment détermi-ner le point C sur un segment AB de façon à ce que AB/AC = AC/AB. Ce point intermédiaire était appelésection dorée, divine proportion ou nombre d’or.

Théorème A.16 : nombre d’or.Si C est le nombre d’or de AB, alors AB/AC = φ.La démonstration (voir l’exercice A.11) prouve que φ est l’une des racines de l’équation quadratiquex2 = x + 1. L’autre racine, ψ = (1 – )/2 = –0,618, est qualifiée de conjugué du nombre d’or. Ces deuxformes du nombre d’or ont des propriétés inhabituelles.

Théorème A.17 : quelques propriétés du nombre d’orSi φ = (1 + )/2 et que ψ = (1 – )/2, alors

φ2 = φ + 1

ψ2 = ψ + 11/φ = φ – 11/ψ = ψ – 1φ + ψ = 1

φ - ψ =

Le nombre d’or est lié aux nombres de Fibonacci et, par conséquent, à l’informatique en raison du résul-tat suivant.

Théorème A.18 : formule explicite des nombres de Fibonacci.

La formule du théorème A.18 est remarquable parce que, bien que φ, ψ et soient tous des nombresirrationnels, les nombres de Fibonacci sont des entiers positifs.

Corollaire A.3 : la fonction Fibonacci est asymptotiquement exponentielle.

Fn = Θ(φn)

A.12 ALGORITHME D’EUCLIDEL’algorithme d’Euclide calcule le plus grand multiple commun de deux entiers positifs. Par exemple, lePGCD de 494 et 130 est 26 parce que les diviseurs de 494 sont 1, 2, 13, 19, 26, 38, 247, 494 et ceux de130 sont 1, 2, 5, 10, 13, 26, 130.

φ 1 5+2

---------------- 1,618≈=

A C B

5

5 5

5

Fnφn Ψn–

5------------------=

5

Page 366: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

356 Annexes

Algorithme A.1 Algorithme d’Euclide.

La version itérative est la suivante :

public static long gcd(long a, long b) while (b>0) long r=a%b; a = b; b = r; // INVARIANT : gcd(a,b) est constant return a;

La version récursive est la suivante :

public static long gcd(long a, long b) if (b==0) return a; // INVARIANT : gcd(a,b) est constant return gcd(b,a%b);

Par exemple, la trace suivante illustre pourquoi gcd(494,130) renvoie 26 :

L’analyse suivante dépend du résultat issu de la théorie des nombres :

Théorème A.19 : Si a > b > 0 et r = a%b > 0 et d | b, alors d | a ⇔ d | r.Dans le cas présent, le symbole « | » signifie « d divise b » et le symbole « % » signifie « le reste de ladivision de a par b ».

Corollaire A.4 : si a > b > 0 et r = a%b > 0, alors (d | a) ∧ (d | b) ⇔ (d | b) ∧ (d | r).

Corollaire A.5 : si a > b > 0 et r = a%b > 0, alors gcd(a, b) = gcd(b, r).Par exemple, gcd(494,130) = gcd(130,104) = gcd(104,26) = gcd(26,0) = 26.

Théorème A.20 : l’algorithme d’Euclide est correct.L’invariant de la boucle est Qk : pgcd(ak, bk) = pgcd(a,b), ak et bk étant les valeurs de a et de b à la k-ièmeitération.

Théorème A.21 : l’algorithme d’Euclide a une durée d’exécution logarithmique.L’algorithme d’Euclide exécute logϕa divisions au maximum, φ étant le nombre d’or et n = maxa, b.

A.13 NOMBRES CATALANSLes nombres catalans sont définis récursivement par la formule suivante :

a 494

b 130

gcd(494,130)

a 130

b 104

gcd(130,104)

a 104

b 26

gcd(104,26)

a 26

b 0

gcd(26,0)

26262626

main()

( )C n( ) 1, if n = 0

C 0( )C n 1 C 1( )C n 2–( ) C 2( )C n 3–( ) C n 1–( )C 0( ), if n > 0+ + + +

=– …

Page 367: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 357

Les dix premiers nombres catalans figurent dans le tableau ci-contre :Cette séquence est similaire à la séquence de Fibonacci. Elle est définie

récursivement et croît de façon exponentielle.

Exemple A.14 Implémenter la fonction des nombres catalans

public static long cat(int n) if (n < 2) return 1; long sum=cat(n-1); for (int i=1; i<n-1; i++) sum += cat(i)*cat(n-1-i); return sum;

La séquence des nombres catalans a été découverte en appliquant ledeuxième principe d’induction mathématique au problème suivant :

Exemple A.15 Compter les triangulations d’un polygone

Un polygone est une région plane limitée par des segments de droite qui ne se croisent pas. Un poly-gone est dit convexe si toute droite passant par deux sommets consécutifs laisse tous les autres som-mets dans le même demi-plan de frontière par rapport à cette droite.Les polygones suivants sont convexes :

La triangulation d’un polygone convexe est une sous-division de ce polygone en triangles dont tousles sommets sont également les sommets du polygone. La triangulation d’un polygone composé de scôtés est simplement un ensemble de s-3 diagonales qui ne se coupent pas. L’exemple suivant illustrele cas de quatre triangulations différentes dans un hexagone :

Les figures suivantes représentent deux triangulations d’un quadrilatère, cinq triangulations d’unpentagone et 14 triangulations d’un hexagone :

n C(n)

0 1

1 1

2 2

3 5

4 14

5 42

6 132

7 429

8 1430

9 4862

triangles = 3

quadrilatères = 4

pentagones = 5

hexagones = 6

Page 368: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

358 Annexes

Les notions que nous venons de voir nous permettent d’arriver aux suppositions suivantes.

Théorème A.22 : le nombre de triangulations dans un polygone composé de n + 2 côtés est égal auénième nombre catalan C(n).Démonstration : supposons que f(s) soit le nombre de triangulations d’un polygone composé de s côtés,nous devons donc prouver que f(n + 2) = C(n) pour tout n ≥ 1. Nous utiliserons pour cela l’inductionforte.

La base est l’instruction f(3) = C(1) = 1 et elle est vérifiée par le fait qu’il n’existe qu’une seule triangu-lation possible dans un triangle, à savoir lui-même.En ce qui concerne l’étape inductive, nous allons supposer que n > 1, f(k + 2) = C(k) pour tout k < n.Nous ignorons quelle est la valeur de n, mais nous pouvons supposer que n = 8. Il nous reste donc àdémontrer que f(10) = C(8), c’est-à-dire qu’un décagone a 1430 triangulations. La figure ci-dessus situéela plus à droite illustre un décagone composé d’un triangle qui le divise en deux parties : un hexagone etun pentagone. D’après notre hypothèse inductive, nous savons que f(6) = C(4) = 14 possibilités différen-tes de triangulation dans un hexagone, et que f(5) = C(3) = 5 possibilités différentes de triangulation dansun pentagone. Nous pouvons donc en déduire qu’il existe C(4) ⋅ C(3) = 14 ⋅ 5 = 70 possibilités différentesde triangulation dans ce décagone pour que le triangle en grise fasse partie de la triangulation.De la même façon, la deuxième figure indique qu’il existe f(8) ⋅ f(5) = C(4) ⋅ C(3) = 132 ⋅ 1 = 132 possibilités différentes de triangulation dans le décagone pour quele triangle en grisé fasse partie de la triangulation. Et enfin, la dernière figure indique qu’il existe f(9) =C(7) = 429 possibilités différentes de triangulation dans ce décagone pour que le triangle en grisé fassepartie de la triangulation. Les f(10) triangulations sont toutes divisées en cas distincts, chaque cas étantdéterminé par le triangle spécial qui a la même base que le décagone. Nous pouvons dénombrer huit de cescas, un pour chaque sommet A à H. Ainsi, le nombre total de triangulations dans un décagone est égal à :

f(10) = f(9) + f(3) ⋅ f(8) + f(4) ⋅ f(7) + f(5) ⋅ f(6) + f(6) ⋅ f(5) + f(7) ⋅ f(4) + f(8) ⋅ f(3) + f(9)

Étant donné que f(s) = C(n – 2) et C(0) = 1,

f(10) = C(7) + C(1) ⋅C(6) + C(2) ⋅C(5) + C(3) ⋅C(4) + C(4) ⋅C(3) + C(5) ⋅C(2) + C(6) ⋅C(1) + C(7) = C(8)

Le même argument indique que f(n +2) = C(n) pour tout n.La technique « diviser pour mieux régner » est utilisée dans la démonstration du théorème A.22. Commevous avez déjà pu le constater, cette stratégie est très pratique dans le domaine de l’informatique. Eneffet, une fois que vous avez analysé un problème pour toutes les tailles inférieures à n, elle consiste àprocéder à une analyse pour la taille n en la fractionnant en deux parties et en appliquant l’analyse précé-dente à chaque partie. Ensuite, l’analyse du problème complet revient à voir comment cela s’applique àdes problèmes plus petits. Nous avons déjà eu affaire à cette stratégie dans le cadre de la recherchebinaire (algorithme 2.2), du tri par fusion (algorithme 13.5) et du tri par segmentation (algorithme 13.6).La formule qui définit les nombres catalans est récursive, ce qui signifie que vous ne pouvez pas l’utiliserafin d’obtenir la valeur de C(n) à moins d’avoir obtenu au préalable les valeurs de C(k) pour tout k < n.En revanche, la formule qui suit est plus pratique et explicite.

f(6)=14 f(5)=5f(8)=132 f(9)=429

A

B

C

D E

F

G

H

E

G

f(3)=1

Page 369: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 359

Théorème A.23 :

Cette formule vous indique qu’il faut former la fraction (2n)/(n), puis ajouter à plusieurs reprises les fac-teurs par paire, un dans le numérateur et un dans le dénominateur, chacun étant inférieur d’1 à son prédé-cesseur jusqu’à ce que le facteur du dénominateur soit égal à 2.

Exemple A.16 Calculer les nombres catalans directement

L’application suivante du théorème A.23 confirme la valeur 4862 donnée dans le tableau de l’exem-ple A.13 :

Étant donné que n = 9 dans le cas présent, vous dénombrerez 8 facteurs dans le numérateur et 8 dansle dénominateur. Comme vous pouvez le constater, tous les facteurs du dénominateur s’annulent.

Théorème A.24 : nombre d’arbres binaires de taille n.Le nombre d’arbres binaires distincts de taille n est égal auénième nombre catalan C(n).Démonstration : supposons que f(n) soit le nombre d’arbresbinaires distincts de taille n. Alors, f(0) = 1 = C(0) parce qu’ilexiste très exactement 1 arbre binaire de taille 0, à savoirl’arbre vide. Les deux figures indiquent que f(1) = 1 = C(1), f(2) = 2 = C(2) et f(3) = 5 = C(3). Cela vérifie la base de la démonstration par induction forte (théorème A.8).Supposons maintenant que pour tout n > 0, f(k) = C(k) pour tout k < n. Supposons que T soit un arbrebinaire de taille n. Comme pour le théorème A.22, nous appliquerons la stratégie « diviser pour mieuxrégner ». Supposons que TL et TR soient les sous-arbres gauche et droit de T. Supposons également quenL et nR soient respectivement les tailles de TL et TR. Ensuite, étant donné que nL et nR sont tous les deuxinférieurs à n, nous pouvons appliquer l’hypothèse f(nL) = C(nL) et f(nR) = C(nR). Maintenant, n = 1 + nL+ nR parce que chaque nœud de T doit être la racine, ou bien être situé dans TL ou bien dans TR . Donc, nR= n – 1 – nL.Et nL peut être n’importe quel nombre k situé dans l’intervalle 0 ≤ k ≤ n – 1. Par conséquent, tous lesarbres binaires de taille n sont divisés en n classes, une pour chaque valeur de nL = k pour 0 ≤ k ≤ n – 1.Les deux figures illustrent deux arbres binaires de taille n = 8, un pourle cas où nL = 3, et un pour le cas où nL = 5. Pour le cas où nL = k, il existe C(nL) possibilités différentes d’arbresbinaires pour TL et C(nR) possibilités pour TR. Le nombre total de possibilités pour ce cas est donc deC(nL) ⋅ C(nR) = C(k) ⋅ C(n – 1 – n). D’où le nombre total d’arbres binaires égal à :

f(n)= C(0) ⋅ C(n – 1) + C(1) ⋅ C(n – 2) + C(2) ⋅ C(n – 3) + … + C(n – 1) ⋅ C(0) = C(n)

Nous pouvons donc en déduire que f(n) = C(n) pour tout n.

C n( ) 2n( )!n! n 1+( )!------------------------ 2n( ) 2n 1–( ) 2n 2–( )… n 2+( )

n( ) n 1–( ) n 2–( )… 2( )----------------------------------------------------------------------------= =

C 9( ) 18( ) 17( ) 16( ) 15( ) 14( ) 13( ) 12( ) 11( )9( ) 8( ) 7( ) 6( ) 5( ) 4( ) 3( ) 2( )

----------------------------------------------------------------------------------------- 17( ) 13( ) 11( ) 2( ) 4862= = =

f(1)=1 :

f(2)=2 :

f(3)=5 :

Page 370: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

360 Annexes

QUESTIONS DE RÉVISION?QUESTIONS DE RÉVISION

A.1 Une fonction f() est dite idempotente si f(f(x)) = f(x) pour tout x du domaine de f(x). Expliquezpourquoi les fonctions de plancher et de plafond sont idempotentes.

A.2 Qu’est-ce qu’un logarithme ?

A.3 Quelle est la différence entre l’induction faible et l’induction forte ?

A.4 Quand est-il préférable d’avoir recours à l’inductions forte ?

A.5 Qu’est-ce que la constante d’Euler ?

A.6 Pourquoi la formule de Stirling est-elle si pratique ?

A.7 Est-il préférable d’utiliser la définition récursive des nombres de Fibonacci (exemple 4.5) ou leurformule explicite (théorème A.18) ?

RÉPONSES¿RÉPONSES

A.1 Les fonctions de plancher et de plafond sont idempotentes parce qu’elles renvoient des valeursentières. En outre, d’après le théorème A.1, le plancher ou le plafond d’un entier est l’entier lui-même.

A.2 Un logarithme est l’exposant de la base donnée qui crée la valeur donnée.

A.3 Le premier principe de l’induction mathématique (induction faible) autorise l’hypothèse induc-tive d’après laquelle la proposition P(n) est vraie pour une valeur unique de n. En revanche, ledeuxième principe d’induction mathématique (induction forte) autorise l’hypothèse inductived’après laquelle toutes les propositions P(k) sont vraies pour tout k inférieur ou égal à une valeurde n.

A.4 Utilisez l’induction faible (premier principe) lorsque la proposition P(n) peut être directementassociée à son prédécesseur P(n – 1). Utilisez l’induction forte (deuxième principe) lorsque laproposition P(n) dépend de P(k) pour k < n –1.

A.5 La constante d’Euler est la limite de la différence (1 + 1/2 + 1/3 + … + 1/n) – ln n. Sa valeur estapproximativement égale à 0,5772.

A.6 La formule de Stirling est particulièrement pratique lorsque vous devez approximer n! pour des nde taille importante, par exemple n > 20.

A.7 La définition récursive des nombres de Fibonacci (exemple 4.5) est inutile lorsque n > 20 parceque le nombre d’appels récursifs croît de façon exponentielle. En revanche, la formule explicite(théorème A.18) est une solution très pratique lorsque n < 1475.

Page 371: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 361

EXERCICES D’ENTRAÎNEMENT?EXERCICES D’ENTRAÎNEMENT

A.1 Dessinez les graphes de :

a. y = xb. y = xc. y = x - x

d. y = x - xA.2 Démontrez le théorème A.1.

A.3 Démontrez le théorème A.2.

A.4 Démontrez le théorème A.3.

A.5 Vrai ou faux :

a. f = o(g) ⇔ g = ω(f)

b. f = O(g) ⇔ g = Ω(f)

c. f = Θ(g) ⇔ g = Θ (f)

d. f = O(g) ⇒ f = Θ (g)

e. f = Θ (g) ⇒ f = Ω (g)

f. f = Θ (h) ∧ g = Θ (h) ⇒ f + g = Θ (h)

g. f = Θ (h) ∧ g = Θ (h) ⇒ fg = Θ (h)

h. n2 = O(n lg n)

i. n2 = Θ (n lg n)

j. n2 = Ω (n lg n)

k. lg n = ω(n)

l. lg n = o(n)

A.6 Démontrez le théorème A.4.

A.7 Démontrez le théorème A.5.

A.8 Démontrez le théorème A.10.

A.9 Démontrez le théorème A.11.

A.10 Démontrez le théorème A.12.

A.11 Démontrez le théorème A.16.

A.12 Démontrez le théorème A.17.

A.13 Démontrez le théorème A.18.

A.14 Démontrez le corollaire A.3.

A.15 Exécutez un programme qui testera la formule du théorème A.18 en comparant les valeurs qu’ilcrée à celles de la définition récursive des nombres de Fibonacci.

A.16 Démontrez le théorème A.19.

A.17 Démontrez le corollaire A.4.

A.18 Démontrez le corollaire A.5.

A.19 Démontrez le théorème A.20.

A.20 Démontrez le théorème A.21.

Page 372: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

362 Annexes

SOLUTIONS¿SOLUTIONS

A.1 Les graphes (a) y = x, (b) y = x, (c) y = x – x et (c) y = x – x :

A.2 La démonstration du théorème A.1 est la suivante :

a. Les relations x = maxm ∈ Z | m ≤ x et x = minn ∈ Z | n ≥ x ne font que reformuler lesdéfinitions de x et x.

b. Supposons que m = x et n = x. Alors, par définition, m ≤ x < m + 1 et n – 1 < x ≤ n. Ensuite,x – 1 < m et n < x + 1. Ainsi, x – 1 < m ≤ x ≤ n < x + 1.

c. Les inégalités x - 1 < x < x + 1 ne font que résumer celles du point précédent.d. Supposons que n ∈ Z de telle façon que n ≤ x < n + 1, et supposons que A = m ∈ Z | m ≤ x.

Ensuite, n ∈ A et x = maxA, c’est pourquoi n ≤ x. Maintenant, si n < x, alors n + 1 ≤ xpuisque n et x dont entiers. Mais, par hypothèse, n + 1 ≤ x, d’où n = x. La démonstrationde la deuxième partie est analogue.

e. Supposons que x ∈ Z (c’est-à-dire que x soit un entier). Ensuite, n = x dans le point d que nousvenons de voir : x ≤ x < x + 1, donc x = x et x – 1 < x≤ x, donc x = x.

f. Supposons que x ∉ Z (c’est-à-dire que x ne soit pas un entier). Supposons que u = x – x et v= x – x. Ensuite, d’après c, u ≥ 0 et v ≥ 0. Toujours d’après c, x – 1 < x = x – u et v + x = x< x + 1, c’est pourquoi u < 1 et v < 1. Ainsi, 0 ≤ u < 1 et 0 ≤ v < 1. Cependant, u et v ne peuventpas être entiers : en effet, si c’était le cas, x le serait également parce que x = x+ u = x – v.Par conséquent, 0 < u < 1 et 0 < v < 1, d’où x = x + u > et x = x – v < x.

g. Supposons que n = – -x. Alors, (–n) = (-x). Par conséquent, d’après c, (–x) – 1 < (–n) ≤(–x), d’où x ≤ n < x + 1, d’où x ≤ n et n – 1 < x, d’où n – 1 < x ≤ n. Ainsi, d’après d, n = x etdonc – -x = x, d’où -x = -xLa deuxième identité suit la première si vous remplacez x par –x.

h. Supposons que n = x + 1. D’après c, (x + 1) – 1 < n ≤ (x + 1), d’où x – 1 < n – 1 ≤ x et x =(x + 1) – 1 < n. Par conséquent, n – 1 ≤ x < n, c’est-à-dire que (n – 1) ≤ x < (n – 1) + 1.

b

d

a

c

Page 373: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 363

Par conséquent, d’après d, (n – 1) = -x, et donc x + 1 = n = x + 1. La démonstration de ladeuxième identité est similaire.

A.3 Démonstration du théorème A.2 :

a. Supposons que x = by. Par définition, logb(by) = logb(x) = y.

b. Supposons que y = logb x. Par définition, .

c. Supposons que y = logb u et z = logb v. Ensuite, par définition, u = by et v = bz, donc uv =(by)(bz) = by+z, donc logb(uv) = y + z = logb u + logb v.

d. D’après la loi c que nous venons de voir, logb v + logb u/v = logb (v u/v) = logb u, donc logb u/v= logb u + logb u.

e. Supposons que y = logb u. Ensuite, par définition, u = by, donc uv = (by)v = bvy. Ensuite, par défi-nition, logb (uv) = v y = v logb u.

f. Supposons que y = logb x. Ensuite, par définition, x = by, donc logc x = logc (by) = y logcb,d’après la loi e que nous venons de voir. Par conséquent, logb x = y = (logc x)/( logcb).

A.4 La démonstration du théorème A.3 est la suivante :

a. Étant donné que 2p < n < 2p + 1, avec n, p ∈ Z, supposons que x = lg n. Ensuite, en prenant lelogarithme binaire de chaque partie de la double inégalité, nous avons p < x < p + 1. Ainsi,d’après le théorème A.1 (d), p = x = lg n. De la même façon, (p + 1) – 1 < x < (p + 1), doncp + 1 = lg n.

b. Supposons que x = lg (n + 1) et y = lg n. D’après le théorème A.1(c), y < y + 1, donc n = 2y

< 2y + 1. Mais maintenant, les deux parties de l’inégalité n < 2y + 1 sont des entiers, c’estpourquoi n + 1 ≤ 2y + 1. Par conséquent, x = lg (n+1) + 1. Mais également, x = lg (n + 1) > =lg n = y ≥ y, donc m – 1 < x ≤ m, avec m = y + 1 = lg n + 1. Par conséquent, d’après lethéorème A.1 (d), x = m, c’est-à-dire que lg(n + 1) = lg n + 1.

A.5 a. Vrai.

b. Vrai.

c. Vrai.

d. Faux.

e. Vrai.

f. Vrai.

g. Faux.

h. Faux.

i. Faux.

j. Vrai.

k. Faux.

l. Vrai.

A.6 La démonstration du théorème A.4 est la suivante :

Pour des fonctions données f et g dans M, l’expression L(f,g) est égale à 0, est positive ou bieninfinie. Ces trois cas qui s’excluent mutuellement déterminent respectivement si f est dans o(g),Θ(g) ou bien dans ω(g).

A.7 La démonstration du théorème A.5 est la suivante :

bxblog

by x= =

O(g) = f ∈ M | 0 L(f, g) < ∞= f ∈ M | L(f, g) = 0 ou 0 < L(f, g) < ∞= f ∈ M | L(f, g) = 0 ∪ f ∈ M | 0 < L(f, g) < ∞ = o(g) ∪ Θ(g)

Page 374: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

364 Annexes

A.8 La démonstration du théorème A.10 est la suivante :

Supposons que S = a + ar + ar2 + ar3 + … + arn – 1. Ensuite, rS = ar + ar2 + ar3 + ar4 … + arn,donc S – rS = a – arn, (1 – r)S = a(1 – rn), et ainsi S = a(1 – rn)/(1 – r).

A.9 La démonstration du théorème A.11 est la suivante :

Si –1 < r < 1, alors, au fur et à mesure de la croissance illimitée de n, rn décroît jusqu’à 0. Si nouslaissons rn = 0 dans la formule du théorème A.10, nous obtenons le théorème A.11.

A.10 La démonstration du théorème A.12 est la suivante :

Supposons que S = 1 + 2 + 3 + … + n. Alors, S = n + … + 3 + 2 + 1 également. Ajoutez ces deuxéquations en ajoutant les 2n termes à droite de la paire : (1 + n), (2 + (n – 1)), etc. Il existe n pai-res et chacune d’entre elles a la même somme n + 1. Par conséquent, le sommet total de la partiedroite est n (n + 1). Ensuite, étant donné que la somme de gauche est 2S, la valeur correcte de Sdoit être n(n + 1)/2.

A.11 La démonstration du théorème A.16 est la suivante :

Si r = AC/CB, alors r = AC/CB = AB/AC = (AC + CB)/AC = 1 + CB/AC = 1 + 1/r, donc r2 = r + 1.Si nous finissons le carré, nous obtenons (r – 1/2)2 = r2 – r + 1/4 = (r + 1) – r + 1/4 = 5/4, doncr – 1/2 = ±Et r = (1 ± )/2. Les formes décimales de ces racines sont

r1 = (1 + )/2 = φ = 1,6180339887498948482045868343656…

r2 = (1 – )/2 = ψ = –0,6180339887498948482045868343656…

étant donné que le nombre d’or φ est la seule solution positive, il doit s’agir du bon coefficient.

A.12 La démonstration du théorème A.14 est la suivante :

a. φ2 + 1 = φ + 1 parce que φ est une solution à l’équation x2 = x + 1.

b. ψ 2 = ψ + 1 parce que ψ est une solution à l’équation x2 = x + 1.

c. 1/φ = φ – 1 parce que 1 = φ2 – φ.

d. 1/ψ = ψ – 1 parce que 1 = ψ 2 – ψ .e. φ + ψ = (1 + )/2 + (1 – )/2 = 1.

f. φ – ψ = (1 + )/2 – (1 – )/2 = 2( )/2) = ).

A.13 La démonstration du théorème A.18 est la suivante, d’après l’hypothèse inductive :

Θ(g) = f ∈ M | 0 < L(f, g) < ∞= f ∈ M | L(f, g) > 0 et L(f, g) < ∞= f ∈ M | L(f, g) > 0 ∩ f ∈ M | L(f, g) < ∞ = Ω(g) ∩ O(g)

Ω(g) = f ∈ M | 0 < L(f, g) ∞= f ∈ M | 0 < L(f, g) < ∞ ou L(f, g) = ∞= f ∈ M | 0 < L(f, g) < ∞ ∪ f ∈ M | L(f, g) = ∞ = Θ(g) ∪ ω(g)

5 4⁄5

5

5

5 5

5 5 5 5

F0φ0 Ψ0–

5----------------- 1 1–

5------------ 0

5------- 0= = = =

F1φ1 Ψ1–

5----------------- φ Ψ–

5------------- 5

5------- 1= = = =

Page 375: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

A. Mathématiques de base 365

A.14 La démonstration du corollaire A.3 est la suivante :

Étant donné que –1 < ψ < 0, les exposants élevés de ψ sont négligeables. C’est pourquoi Fn = kφn, avec k = 1/ .

A.15 Le programme suivant teste la formule explicite des nombres de Fibonacci (théorème A.18) :

• public class PrA15• public static void main(String[] args)• final double SQRT5 = Math.sqrt(5.0);• final double PHI = (1 + SQRT5)/2;• final double PSI = (1 - SQRT5)/2;• long f0, f1=0, f2=1;• for (int n=2; n<32; n++)• f0 = f1;• f1 = f2;• f2 = f1 + f0;• float fn = (float)((Math.pow(PHI,n)-Math.pow(PSI,n))/SQRT5);• System.out.println(f2+"\t"+fn);• • •

A.16 La démonstration du théorème A.19 est la suivante :

Supposons que a > b > 0, que r = a%b > 0 et que d | b. Ensuite, a = qb + r pour q ∈ N, et b = kdpour k ∈ N. Ainsi, a = qb + r = qkd + r. Ensuite, d | a ⇒ a = md, pour m ∈ N ⇒ md = a = qkd +r ⇒ r = md – qkd = (m – qk)d ⇒ d | r. De la même façon, d | r ⇒ r = nd, pour n ∈ N ⇒ a = qkd+ r = qkd + nd = (qkd + n)d ⇒ d | a. Ainsi, d | a ⇔ d | r.

A.17 La démonstration du corollaire A.4 est la suivante :

Il s’agit d’une conséquence de l’argument logique (P∧Q) → (R↔S) ⇒ P → (Q∧R↔Q∧S), avecP = « a > b > 0 » et r = « a%b » > 0, Q = « d | b », R = « d | a », et R = « d | r ».

A.18 La démonstration du corollaire A.5 est la suivante :

Supposons que A = d ∈ N : d | a et d | b et supposons que B = q ∈ N : d | b et d | r. Ensuite,d’après le corollaire A.4, A = B. Par conséquent, maxA = maxB. Cependant, par définition, maxA= gcd(a,b) and maxB = gcd(b,r).

A.19 La démonstration du théorème A.20 est la suivante :

Le corollaire A.4 garantit que l’invariant de boucle est toujours vrai. La boucle s’arrête lorsqueb = 0 et renvoie a qui est le gcd(a,b). Par conséquent, la valeur renvoyée est également le gcd dela paire initiale (a,b).

A.20 La démonstration du théorème A.21 est la suivante :

À la k-ième itération, ak = qk bk + rk, avec qk = ak/bk ≥ 1 et rk = ak%bk. Ainsi, ak ≥ bk + rk. Mais bk= ak + 1 et rk = bk + 1 = ak + 2. Ainsi, ak ≥ ak + 1 + ak + 2. Si nous considérons maintenant la séquenceinverse xi avec x0 = am et xi = am – i : x0 ≥ 1, xi ≥ xi – 1 + xi – 2 pour tout i, par conséquent, x2 ≥ x1+ x0 ≥ 1 + 1 = 2, x3 ≥ x2 + x1 ≥ 2 + 1 = 3, etc. Ainsi, xi ≥ Fi + 1 (c’est-à-dire les nombres de Fibo-nacci). Par conséquent, a = xm ≥Fm + 1 ≥ ϕm, et donc logϕa ≥ m.

( )Fn 1+

φn 1+ Ψn 1+–

5------------------------------- φn φn 1–+( ) Ψn Ψ n 1–+–

5----------------------------------------------------------------= =

φn Ψ n+

5------------------ φn 1– Ψ n 1–+

5------------------------------+=

Fn Fn 1–+=

5

Page 376: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

B. De C++ à Java

Cette annexe illustre les correspondances entre les concepts du C++ et ceux de Java. Elle est destinée auxprogrammeurs C++ qui apprennent Java. Les différences fondamentales entre ces deux langages sont lessuivantes :

• En Java, toutes les instructions exécutables doivent être encapsulées dans des classes.

• Java utilise les éléments Objects au lieu des classes template.

• Java n’admet aucune fonction, ni aucune variable externes.

• Java utilise des références et non des pointeurs.

• Toutes les arguments sont passés par valeur.

• La définition C++ string* s = new string; équivaut à la définition Java String s = newString();.

• Java a recours à un procédé de ramasse-miettes automatique au lieu d’utiliser l’opérateur delete.

Vous trouverez ci-après une liste des principales correspondances entre Java et le C++ :bool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . booleanchar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique paswchar_t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . charshort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . shortint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . intlong . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . longunsigned char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . byteunsigned short . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasunsigned int . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasunsigned long . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasfloat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . floatdouble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . doubleenum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique passtring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stringconst . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . finalgoto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique paspointeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasréférence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique paspassage par valeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . passerpassage par référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasinline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasregister . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasnamespace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . packageflot entrées/sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasprintf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . System.out.println()scanf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasclass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . classstruct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . class

Page 377: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

B. De C++ à Java 367

membre de donnée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . champ, variable d’instancefonction membre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . méthodepublic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . publicprotected . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . protectedprivate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . privatestatic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . staticthis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . thisnew . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . newdelete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pastemplate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasoperator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasfriend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique passizeof . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pastypedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pastypeid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . getClass()virtual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . abstractvirtual class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . abstract classfonction virtual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . abstract methodclasse virtual pure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . interfacefichier d’en-tête . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . package#include . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . importvector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vectorstack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stackqueue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique pasdeque . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique paspriority_queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ne s’applique paslist . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . LinkedListmap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HashMapset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HashSet

Page 378: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

C. Environnementsde développement Java

Un environnement de développement est un ensemble des programmes informatiques qui permettent decréer un programme. Il est essentiellement composé des deux éléments suivants : un éditeur de texte etun compilateur. Par ailleurs, les environnements de développement intégré, ou IDE, comprennent égale-ment un débogueur, de la documentation sur le langage de programmation, ainsi que d’autres outils.

C.1 INVITE DE COMMANDE WINDOWSL’environnement de développement le plus simple ne vous coûte rien puisque vous pouvez utiliser l’édi-teur de texte qui est fourni avec votre système d’exploitation, puis télécharger la dernière version de Javadu site web de Sun Microsystems http://java.sun.com/.

Si vous travaillez sous Microsoft Windows, vous pouvez utiliser les éditeurs NotePad ou WordPad.Créez un fichier que vous nommerez Hello.java.

Enregistrez ce fichier au format Texte seulement sur le lecteur A:. Lancez ensuite l’invite de com-mandes Windows depuis Démarrer > Programmes > Accessoires et exécutez les commandesqui vous permettront notamment de paramétrer la variable de chemin d’accès PATH, si nécessaire, defaçon à ce que le système d’exploitation puisse trouver la commande du compilateur javac et la com-mande d’exécution java.

C.2 WEBGAIN VISUAL CAFEVisual Cafe a été initialement créé par Symantec Corp et est maintenant commercialisé par WebGain Inc.La version 4.0 a été lancée en août 2000.

Pour créer un programme d’application Java standard :

1. Sélectionnez New Project… dans le menu File (ou appuyez sur Ctrl+Shift+N), puis sélection-nez Win32 Console Application dans la boîte de dialogue New Project.

2. Cette opération vous permet de créer un nouveau projet et de définir une classe nommée SimplConde la façon suivante :

/* Classe stub java de base pour une application console Win32. */ public class SimplCon public SimplCon () static public void main(String args[]) System.out.println("Hello World");

Ce programme est une variation du programme classique « Hello, World! ».

Page 379: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

C. Environnements de développement Java 369

3. Pour l’exécuter, sélectionnez Execute dans le menu Project (ou appuyez sur Ctrl+F5). Cela vouspermettra de compiler et d’exécuter le code, puis de créer une sortie dans une fenêtre DOS classique.

4. Appuyez sur Entrée pour fermer la fenêtre DOS.

5. Pour enregistrer votre projet, cliquez sur la fenêtre correspondante nommée Project – Untitled,puis sélectionnez Enregistrer sous… dans le menu Fichier. Dans notre exemple, nous avonsenregistré le projet sous Project1.vep dans A:. Vous remarquerez que le fichier Simpl-Con.java est alors enregistré automatiquement dans le même répertoire.

6. Pour renommer votre classe principale, éditez les deux occurrences de la chaîne SimplCon dans lafenêtre du code source, puis sélectionnez Enregistrer sous… dans le menu Fichier. Dans lecas présent, nous avons renommé la classe principale Main. Cela signifie que vous avez maintenantdeux classes contenant chacune leur propre méthode main(), à savoir SimplCon et Main. Doublecliquez sur l’icône du fichier SimplCon dans la fenêtre du projet afin de l’afficher à nouveau dans sapropre fenêtre d’édition.

7. Un projet ne peut comporter qu’une seule classe main(), c’est-à-dire une seule classe dont laméthode main() est désignée comme cible de départ de l’exécution. Afin de savoir quelle méthodemain() est la cible de votre projet, modifiez la chaîne println() de la classe Main et remplacez-la par "Goodbye, World!". Appuyez ensuite sur Ctrl+F5 pour exécuter le projet ou bien sélec-tionnez Execute dans le menu Project. Vous devriez obtenir un message Hello World vousindiquant que la cible n’a pas été modifiée.

8. Pour changer de cible, sélectionnez Options… dans le menu Project afin d’afficher la fenêtreProject Options. Modifiez ensuite le listing sous MainClass en remplaçant SimplCon parMain, puis cliquez sur OK. Exécutez à nouveau votre projet. Cette fois, c’est le message Good-bye,World! qui devrait s’afficher.

9. Fermez maintenant la fenêtre SimplCon.java. Cliquez ensuite avec le bouton droit de la souris surl’icône de ce fichier dans la fenêtre Project, puis sélectionnez Couper dans le menu déroulant, oubien dans le menu Edition afin de supprimer la classe du projet et de n’y laisser que la nouvelleclasse Main.

10. Éditez maintenant la classe principale en ajoutant une autre instruction println de façon à ce quemain() soit similaire au code suivant:

static public void main(String args[]) System.out.println(args[0] + " **** " + args[1]); System.out.println("Goodbye, World!");

Ouvrez ensuite la fenêtre Project Options à nouveau, sélectionnez Options dans le menu Pro-ject, puis ajoutez le texte Hello World!!!! dans le champ Program Arguments avant decliquer sur OK. Exécutez votre projet une nouvelle fois. Cette fois, votre sortie devrait commencer parla ligne suivante :

Nous venons donc de voir comment utiliser les arguments des lignes de commande. Chaque chaînelistée figurant dans le champ Program Arguments devient alors l’un des éléments String duparamètre de tableau args[] de la méthode main().

11. Pour insérer une nouvelle classe dans votre projet, sélectionnez Class… dans le menu Insert.Nommez votre nouvelle classe Widget et entrez DSwJ.util dans le champ Package. Cliquezensuite sur le bouton Finish, puis sur Yes pour répondre à la question concernant la création d’unnouveau répertoire. Vérifiez ensuite ce qui a été créé dans votre répertoire de projet. Votre paquetage

Hello **** World!!!!Goodbye, World!

Page 380: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

370 Annexes

Java est devenu un chemin de répertoire, chacun de ses composants correspondant au nom d’un dos-sier imbriqué. Ainsi, le paquetage DSwJ.util a créé un dossier Util imbriqué dans un dossierDSwJ, et le nouveau fichier Widget.java a été inséré dans ce dossier interne.

12. Double cliquez sur le nouveau listing Widget dans la fenêtre Project afin d’ouvrir ce fichier decode source qui contient une présentation minimale de classe :

package DSwJ.util;public class Widget

13. Cliquez sur l’onglet Packages situé en bas dela fenêtre Project afin d’afficher la vue correspon-dante. Dans la partie supérieure de cette vue, vous trouverez le fichier Main.java listé sous lepaquetage Default Package,et suivi du fichier Widget.java listé dans le paquetageDSwJ.util. Sous ces fichiers, vous trouverez une très longue liste de tous les paquetages de labibliothèque standard Java. Faites dérouler la liste jusqu’à java.util et étendez cette entrée defaçon à ce qu’elle liste les classes qu’elle définit. Double-cliquez sur le listing Stack.java afind’ouvrir le fichier et d’afficher le code source.

14. Sélectionnez Class Browser dans le menu View ou bien appuyez sur Ctrl+Shift+C afind’ouvrir la fenêtre de navigateur de classes de Visual Café. Cette fenêtre contient trois sections : lelisting Classes, une sous-fenêtre Members et une sous-fenêtre d’édition. Cliquez avec le boutondroit de la souris dans le listing Classes, puis sélectionnez Collapse All afin de visualiser leslistings des dossiers. Faites défiler le listing de la même manière que pour l’étape 13 jusqu’au paque-tage java.util, étendez-le, puis double-cliquez sur l’entrée Stack. La sous-fenêtre Membersdoit maintenant contenir une liste de tous les membres de la classe java.util.Stack. Cliquez surla méthode push pour afficher son code source dans la sous-fenêtre d’édition.

Visual Cafe offre toutes les fonctionnalités habituellement disponibles dans d’autres environnementsIDE avancés :

• éditeur couleur ;• débogueur contrôlé par un bouton ;• aide en ligne exhaustive.

Il se distingue des autres environnements IDE grâce aux caractéristiques suivantes :

• des messages de compilateur utiles et significatifs ;• un assistant de classe ;• un code helper ;• un correcteur de syntaxe ;• un éditeur de hiérarchie ;• le support des macros définies par l’utilisateur.

Le système d’aide en ligne de Visual Café est particulièrement bien fait. La commande Java APIReference… dans le menu Help offre des listings complets de tous les paquetages, classes, champs etméthodes disponibles qui vont de la bibliothèque standard Java aux bibliothèques supplémentaires four-nies par WebGain. La fenêtre Class Hierarchy présente un arbre d’héritage gigantesque, de la classeObject jusqu’aux classes qui se trouvent au niveau le plus bas. Les rubriques Index of all Classes,Fields et Methods comportent plus de 20 000 listings. La rubrique Package Index liste les 95paquetages des bibliothèques Java, Sun et Symantec. Vous pouvez passer d’une rubrique à l’autre encliquant sur les onglets correspondants situés dans la partie supérieure de la fenêtre API Reference.Toutes ces rubriques utilisent des liens hypertextes afin de faciliter la navigation. Malheureusement, laréférence API n’est pas à jour puisque la version d’août 2000 ne contient aucune des classes Java 1.2telles que java.util.Arrays.

Page 381: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Index

Aabstract, mot-clé 64AbstractCollection, classe

101AbstractList, classe 151AbstractSequentialList,

classe 151AbstractSet, classe 306accesseur 7algorithme

d'Euclide 83, 355récursivité 83

de Dijkstra 324de parcours 178

d'un graphe 328en largeur 178, 194infixe 196postfixe 179, 195préfixe 178, 195

de recherchebinaire 33

récursive 80en largeur d'abord 328en profondeur d'abord

329séquentielle 31

de résolution des collisionshachage avec essais

linéaires 293méthode quadratique 294

de tridigital 268panier 270par fusion 258par insertion 255par permutation 252par segmentation 260par sélection 254rapidité 268

Shell 256stable 269vertical 263

récursif, complexité 84arbre 171

algorithme de parcours 178binaire 187

algorithme de parcours 194

complet 189d'expression 196de recherche 222

AVL 224déséquilibré 223équilibré 223propriété 222

incomplet 193parfait 192

chemin 173complet 173couvrant minimal 328de recherche 217

multidirectionnel 217décisionnel 174degré d'un nœud 173enfant 171équilibré ou B 219feuille 172hauteur 173libre 315longueur de chemin 173niveau 173nœud 172

interne 172profondeur 173

non ordonné 172égalité 172

ordonné 177inégalité 177

ordre 173parent 171

propriété 173racine 171sous-arbre 173superarbre 173taille 172

arêtes, graphe 313ArrayList, classe 154ArrayQueue, classe 132Arrays, classe 28Arrays.sort(), méthode 252

BBag, classe 102BinaryTree, classe 198boucle 5

graphe 320

Ccalculatrice RPN 118chaînage séparé 295champ 7

clé, arbre binaire de recher-che 222

chemin 314élémentaire 315eulérien 323fermé 315hamiltonien 324le plus court 324orienté 322

classe 7AbstractCollection 101AbstractList 151AbstractSequential-

List 151

Page 382: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

372 Structures de données en Java

classe (suite)AbstractSet 306abstraite 64ArrayList 154ArrayQueue 132Arrays 28Bag 102BinaryTree 198de complexité 347enfant 57enveloppe 4HashMap 288HashSet 307LinkedList 155LinkedQueue 135Math 13Object 62parent 57PriorityQueue 236Ring 156Stack 115String 10TreeMap 298TreeSet 309Vector 35

clonage 63code de hachage 289coefficient

binomial 82itérativité 83récursivité 82

oscillant 348collection 99Collection, interface 100Comparator, interface 237complexité 347concordance 296

ordonnée 298constante d'Euler 352constructeur 7contains(), méthode 202contrôle du flux 5conversion

de chaîne 60de l'affectation 60de l'appel de méthode 61de type 60

cycle 315eulérien 323hamiltonien 324

Ddegré

entrant 320sortant 320

dequeue, méthode 130diagramme de transition 174digraphe 320

chemin 321complet 320faiblement connexe 322fortement connexe 322liste d'adjacences 321matrice

d'adjacence 321d'incidence 321

pondéré 322simple 320

données classées 154

Eégalité 190

des tableaux 191enqueue, méthode 130ensemble 305

intersection 305soustraction 305taille 305union 305vide 305

environnement de développement 368

exception 70, 116gestion 116UnsupportedOperation-

Exception 150vérifiée 71

Ffacteur de charge 292file 129

de priorité 233, 236

utilisation 130, 136fonction

cosinus, récursivité mutuelle 87

de hachage 290factorielle 77

base 78itérativité 78partie récursive 78récursivité 77

Fibonaccirécursivité 79

plafond 345plancher 345récursive 77sinus, récursivité mutuelle

87forêt 205

libre 315formule

de sommation 351de Stirling 353

frameworkde collections 129des files 129

Ggénération de nombres

aléatoires 138graphe 313

acyclique 315algorithme de parcours 328arête 313chemin 314complet 314connexe 315cycle 314eulérien 323hamiltonien 324incorporé 320isomorphe 316orienté 320pondéré 322simple 313sommet 313

Page 383: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

Index 373

Hhachage

avec essais linéaires 291, 293

fonction de ~ 290table de ~ 290

HashMap, classe 288HashSet, classe 307héritage 57

hiérarchie 1

Iidentité 190implémentation

chaînée 134contiguë 132itérative

coefficient binomial 83fonction factorielle 78tours de Hanoi 121

récursivealgorithme d’Euclide 83coefficient binomial 82fonction factorielle 77nombre de Fibonaci 79

induction mathématique 83deuxième principe 349étape inductive 349hypothèse inductive 349premier principe 348

instanciation 7interface 67

Collection 100Comparator 237Iterator 109, 153List 149Map 287Queue 129Set 306

isomorphisme 190, 191, 316des arbres 191

itérateur 153bidirectionnel 150, 153de liste 153

indépendant 164

unidirectionnel 153Iterator, interface 109, 153

JJava

définition 1et le C++ 366

java.util, paquetage 99, 115

LLinkedList, classe 155LinkedQueue, classe 135List, interface 149liste 149

chaînée 151, 155, 295circulaire 156d'adjacences 319, 321des arêtes 319

ListIterator 150logarithme

binaire 346commun 346de base b 346lois des ~ 346naturel 346

MMap, interface 287mappage naturel 193, 206, 233Math, classe 13matrice

d'adjacence 318, 321d'incidence 319, 321

méthode 6abstraite 64, 131Arrays.sort() 252concrète 131contains() 202dequeue 130enqueue 130pop() 117

push() 117quadratique 294stochastique 176

modificateur 9d'accès 10

mot-clé, abstract 64mutateur 7

Nnœud, arbre 172nombre

catalan 356d'équilibre 224d'or 355de Fibonacci 78, 354harmonique 352

notationinfixée 118suffixée 118

OObject, classe 62objet 2opérateur

d'expression conditionnelle 159

d'index 25

Ppaire

clé/adresse 220clé/valeur 218, 287

paquetage 70java.util 99, 115

pile 115pivot 260point isolé 314polymorphisme 58pop(), méthode 117PriorityQueue, classe 236problème de Josephus 162

Page 384: [John_R._Hubbard]_Structures_de_données_en_Java(BookFi.org) - Copie

374 Structures de données en Java

programmationdynamique 85orientée objet 1

promotion numérique 60push(), méthode 117

QQueue, interface 129

Rracine, arbre 171raison, série géométrique 350recherche

binaire 33séquentielle 31

récursivité 77, 121algorithme de recherche

binaire 80base 78

fonction factorielle 78directe 87indirecte 87mutuelle 87

cosinus, fonction 87sinus, fonction 87

partie récursive 78fonction factorielle 78

rééquilibrage 224représentation d'une

expressioninfixe 197postfixe 197préfixe 197

Ring, classe 156

Ssérie géométrique 350

raison 350Set, interface 306simulation

en temps réel 142événementielle 142

simulation d'un système client/serveur 136

singleton 172sommet

degré 314graphe 313point isolé 314

sous-arbre 173sous-classe 57sous-ensemble 305sous-graphe maximal 313Stack, classe 115String, classe 10structure

contiguë 151non contiguë 151

superarbre 173superclasse 57

Ttable 287

clé 287de hachage 289, 290

et chaînes 307performance 292

valeur 287

tableau 25, 132, 150, 151composant 25copier 27élément 25extensif 27

tas 233propriété de ~ 233

tours de Hanoi 85implémentation itérative

121tracer un appel de fonction 79TreeMap, classe 298TreeSet, classe 309tri

digital 268panier 270par fusion 258par insertion 255par permutation 252par segmentation 260par sélection 254Shell 256vertical 233, 263

triangulation 357type primitif 3

UUnsupported Operation

Exception, exception 150utilitaire 7

Vvariable 2Vector, classe 35


Recommended