VZWiWkUnulrQ54cUEdR6 19 94373e95cd5e1b663eb7dfd045b1d81e file


19MB taille 1 téléchargements 185 vues
Algorithmique et programmation en Java Cou[s et exe[cices cor[igés 4e édition

Illustration de couverture :

Abstract background - Spheres © Dreaming Andy - Fotolia.com

d'enseignement supérieur, provoquant une Le pictogramme qui figure ci-contre mérite une explication. Son objet est baisse brutale des achats de livres et de d'alerter le lecteur sur la menace que revues, au point que la possibilité même pour représente pour l'avenir de l'écrit, les auteurs de créer des œuvres nouvelles et de les faire éditer cor­ particulièrement dans le domaine DANGER de l'édition technique et universi­ rectement est aujourd'hui menacée. taire, le développement massif du Nous rappelons donc que toute photocopillage. reproduction, partielle ou totale, Le Code de la propriété intellec­ de la présente publication est tuelle du 1er juillet 1992 interdit interdite sans autorisation de LE PHOTOCœLLAGE l'auteur, de son éditeur ou du en effet expressément la photoco­ TUE LE LIVRE pie à usage collectif sans autori­ Centre français d'exploitation du sation des ayants droit. Or, cette pratique droit de copie (CFC, 20, rue des s'est généralisée dans les établissements Grands Augustins, 75006 Paris).

@)

"O 0 c: ::J

0 '­ a. 0 u

-

© Dunod, 2000, 2004, 2010, 2014 5 rue Laromiguière, 75005 Paris www.dunod.com ISBN 978-2-10-071452-0 Le Code de la propriété intellectuelle n'autorisant, aux termes de l'article L. 122- 5, 2° et 3° a), d'une part, que les «copies ou reproductions strictement réservées à l'usage privé du copiste et non destinées à une utilisation collective» el, d'autre part, que les analyses et les courtes citations dans un but d'exemple et d'illustration, « 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 » (art. L. 1224). Cette représentation ou reproduction, par quelque procédé que ce soit, constitue­ rait donc une contrefaçon sanctionnée par les ar ticles L. 335-2 et suivants du Code de la propriété intellectuelle.

à Maud

-0 0 c: =i 0 "Cl. 0 u

XV



ACTIONS ÉLÉMENTAIRES

15

2.1

Lecture d'une donnée

15

2.2

Exécution d'une routine prédéfinie

16

2.3

Écriture d'un résultat

17

2.4

Affectation d'un nom à un objet

17

2.5

Déclaration d'un nom

18

2.5.1

Déclaration de constantes

18

2.5.2

Déclaration de variables

19

2.6

Règles de déduction

19

2.6.1

L'affectation

19

2.6.2

L'appel de procédure

20

2.7

Le programme sinus écrit en Java

20

2.8

Exercices

22

CHAPITRE 3



TYPES ÉLÉMENTAIRES

23

3.1

Le type entier

24

3.2

Le type réel

25

VIII

3.3

Le type booléen

28

3.4

Le type caractère

29

3.5

Constructeurs de types simples

31

3.5.1

Les types énumérés

31

3.5.2

Les types intervalles

32

3.6

-0 0 c: =i 0 "Cl. 0 u

Algorithmique et programmation en Java

Exercices

32

CHAPITRE 4 •EXPRESSIONS

35

4.1

36

Évaluation

4.1.1

Composition du même opérateur plusieurs fois

36

4.1.2

Composition de plusieurs opérateurs différents

36

4.1.3

Parenthésage des parties d'une expression

37

4.2

Type d'une expression

37

4.3

Conversions de type

38

4.4

Un exemple

38

4.5

Exercices

41

CHAPITRE 5 •ÉNONCÉS STRUCTURÉS

43

5.1

Énoncé composé

43

5.2

Énoncés conditionnels

44

5.2.1

Énoncé choix

44

5.2.2

Énoncé si

46

5.3

Résolution d'une équation du second degré

47

5.4

Exercices

50

CHAPITRE 6 •PROCÉDURES ET FONCTIONS

53

6.1

Intérêt

53

6.2

Déclaration d'une routine

54

6.3

Appel d'une routine

55

6.4

Transmission des paramètres

56

6.4.1

Transmission par valeur

57

6.4.2

Transmission par résultat

57

6.5

Retour d'une routine

57

6.6

Localisation

58

6.7

Règles de déduction

60

6.8

Exemples

61

6.9

Exercices

65

CHAPITRE 7 •PROGRAMMATION PAR OBJETS

67

7.1

67

Objets et classes

7 .1.1

Création des objets

68

7.1.2

Destruction des objets

69

7.1.3

Accès aux attributs

69

IX

Table des matières

7.1.4

Attributs de classe partagés

70

7.1.5

Les classes en Java

70

7.2

Les méthodes

71

7.2.1

Accès aux méthodes

72

7.2.2

Constructeurs

72

7.2.3

Constructeurs en Java

73

7.2.4

Les méthodes en Java

73

7.3

Assertions sur les classes

75

7.4

Exemples

76

7.4.1

Équation du second degré

76

7.4.2

Date du lendemain

79

7.5

Exercices

82

CHAPITRE 8 •ÉNONCÉS ITÉRATIFS

85

8.1

Forme générale

85

8.2

L'énoncé tantque

86

8.3

L'énoncé répéter

87

8.4

Finitude

88

8.5

Exemples

88

8.5.1

Factorielle

88

8.5.2

Minimum et maximum

89

8.5.3

Division entière

89

8.5.4

Plus grand commun diviseur

90

8.5.5

Multiplication

91

8.5.6

Puissance

91

8.6

Exercices

92

CHAPITRE 9 •LES TABLEAUX

95

-0 0 c: =i 0 "Cl. 0 u

Algorithmique et programmation en Java

10.4.2 Un tri interne simple

109

10.4.3 Confrontation de modèle

110

10.5 Complexité des algorithmes

1 14

10.6 Exercices

116

CHAPITRE 11 •LES TABLEAUX À PLUSIEURS DIMENSIONS

119

11.1 Déclaration

119

11.2 Dénotation d'un composant de tableau

120

11.3 Modification sélective

120

11.4 Opérations

121

11.5 Tableaux à plusieurs dimensions en Java

121

11.6 Exemples

122

11.6.1 Initialisation d'une matrice

122

11.6.2 Matrice symétrique

122

11.6.3 Produit de matrices

123

11.6.4 Carré magique

124

11.7 Exercices

126

CHAPITRE 12 •HÉRITAGE

13 1

12.1 Classes héritières

131

12.2 Redéfinition de méthodes

134

12.3 Recherche d'un attribut ou d'une méthode

135

12.4 Polymorphisme et liaison dynamique

136

12.5 Classes abstraites

137

12.6 Héritage simple et multiple

138

12.7 Héritage et assertions

139

12.7.1 Assertions sur les classes héritières

139

12.7.2 Assertions sur les méthodes

139

12.8 Relation d'héritage ou de clientèle

139

12.9 L'héritage en Java

140

CHAPITRE 13 • FONCTIONS ANONYMES

143

13.1 Paramètres fonctions

144

13.1.1 Fonctions anonymes en Java

145

13.1.2 Foreach et map

147

13.1.3 Continuation

148

13.2 Fonctions anonymes en résultat

150

13.2.1 Composition

150

13.2.2 Curryfication

15 1

13.3 Fermeture

152

13.3.1 Fermeture en Java

153

13.3.2 Lambda récursives

154

13.4 Exercices

155

Table des matières

CHAPITRE 14



LES EXCEPTIONS

1 57

14.2 Traitement d'une exception

158

14.3 Le mécanisme d'exception de Java

159

14.3.1 Traitement d'une exception

159

14.3.2 Émission d'une exception

160

CHAPITRE 15



1 61 LES FICHIERS SÉQUENTIELS

164

15.2 Notation

164

15.3 Manipulation des fichiers

1 65

15.3.1 Écriture

165

15.3.2 Lecture

166 1 67

15.4.1 Fichiers d'octets

167

15.4.2 Fichiers d'objets élémentaires

169

15.4.3 Fichiers d'objets structurés

173

15.5 Les fichiers de texte

1 73

15.6 Les fichiers de texte en Java

1 74

15.7 Exercices

178

CHAPITRE 16

@ ....... ..c: O'l ·c >Cl. 0 u

163

15.1 Déclaration de type

15.4 Les fichiers de Java

,..-1 0 N

1 57

14.1 Émission d'une exception

14.4 Exercices

-0 0 c: =i 0 "­ Cl. 0 u

Cet ouvrage doit beaucoup à de nombreuses personnes. Tout d' abord, aux auteurs des algorithmes et des techniques de programmation qu'il présente. Il n'est pas possible de les citer tous ici, mais les références à leurs principaux textes sont dans la bibliographie. À Olivier Lecarme et Jean-Claude Boussard, mes professeurs à l'université de Nice qui m'ont enseigné cette discipline au début des années 1 980. Je tiens tout particulièrement à remercier ce dernier qui fut le tout premier lecteur attentif de cet ouvrage alors qu'il n'était encore qu'une ébauche, et qui m' a encouragé à poursuivre sa rédaction. À Carine Fédèle qui a bien voulu lire et corriger ce texte à de nombreuses reprises, qu'elle en soit spécialement remercier. Enfin, à mes collègues et mes étudiants qui m'ont aidé et soutenu dans cette tâche ardue qu'est la rédaction d'un livre. Enfin, je remercie toute l'équipe Dunod, Carole Trochu, Jean-Luc Blanc, et Romain Hen­ nion, pour leur aide précieuse et leurs conseils avisés qu'ils m'ont apportés pour la publication des quatre éditions de cet ouvrage.

Sophia Antipolis, avril 2014.

Chapitre 1

Introduction

Les informaticiens, ou les simples usagers de l'outil informatique, utilisent des systèmes informatiques pour concevoir ou exécuter des programmes d' application. Nous considére­ rons qu'un environnement informatique est formé d'une part d'un ordinateur et de ses équi­ pements externes, que nous appellerons environnement matériel, et d'autre part d'un système d'exploitation avec ses programmes d'application, que nous appellerons environnement logi­ ciel. Les programmes qui forment le logiciel réclament des méthodes pour les construire, des langages pour les rédiger et des outils pour les exécuter sur un ordinateur.

-0 0 c: =i 0 "­ Cl. 0 u

données- - - -�

langage machine

FIGURE 1.2 Traduction

- - -�

résultats

en langage machine.

- Nous avons vu qu'un langage de programmation définü un ordinateur fictif. La seconde méthode consiste à simuler le fonctionnement de cet ordinateur fictif sur l'ordinateur réel par interprétation des instructions du langage de programmation de haut niveau. Le logiciel qui effectue cette interprétation s'appelle un interprète. L'interprétation directe des instructions du langage est en général difficilement réalisable. Une pre­ mière phase de traduction du langage de haut niveau vers un langage intermédiaire de plus bas niveau est d'abord effectuée. Remarquez que cette phase de traduction

Chapitre

8

1



Introduction

comporte les mêmes phases d'analyse qu'un compilateur. L' interprétation est alors faite sur le langage intermédiaire. C'est la technique d'implantation du langage JAVA (voir la figure 1 .3), mais aussi de beaucoup d'autres langages. Un programme source JAVA est d'abord traduit en un programme objet écrit dans un langage intermédiaire, appelé JAVA pseudo-code (ou byte-code). Le programme objet est ensuite exécuté par la machine virtuelle JAVA, JVM (Java Virtual Machine).

programme source Java

compilateur

langage

interprète

intermédiaire

JVM

- - - - - - résultats

données

FIGURE 1.3 Traduction

-0 0 c: =i 0 "= sur des opérandes de type caractère. Les caractères peuvent être dénotés par la valeur hexadécimale de leur ordinal, précédée par la lettre u et le symbole \ . Par exemple, ' \u004 1 ' est le caractère d'ordinal 65, c'est-à-dire la lettre A. Cette notation particulière trouve tout son intérêt lorsqu'il s'agit de dénoter des caractères graphiques. Par exemple, les caractères ' \ u 1 2cc' et '\u1 356' représentent les lettres éthiopiennes � et 7·, les caractères ' \u2200' et ' \u2208' représentent les symboles mathéma­ tiques \/ et E, et ' \u2708' est le symbole +. Notez que puisqu'en JAVA les caractères UNICODE sont codés sur 1 6 bits, l' intervalle valide va donc de ' \uOOOO' à '\uFFFF'. La déclaration suivante introduit trois nouvelles variables de type caractère : char lett r e , marqu e , symbole; 4. Cette norme I S O définit un jeu unjversel d e caractères. UNICODE et ISO/CEi 10646 sont étroitement liés.

3.5

Constructeurs de types simples

31

Les caractères UNICODE peuvent être normalement utilisés dans la rédaction des pro­ grammes JAVA pour dénoter des noms de variables ou de fonctions. Afin d'accroître la li­ sibilité des programmes, il est fortement conseillé d'utiliser les caractères accentués, s'ils sont nécessaires. Il est aussi possible d' utiliser toutes sortes de symboles, et la déclaration de constante suivante est tout à fait valide : final double

n

=

3 . 1415926;

Si l a saisie directe du symbole 7r n'est pas possible, i l sera toujours possible d'employer la notation hexadécimale. final double \ u 0 3 C O

3 . 1415926;

CONSTRUCTEURS D E TYPES S I M PLES

3.5

Les types énumérés et intervalles permettent de construire des ensembles de valeurs parti­ culières. L'intérêt de ces types est de pouvoir spécifier précisément le domaine de définition des variables utilisées dans le programme. Certains langages de programmation proposent de tels constructeurs et permettent de nommer les types élémentaires construits. Dans cette section, nous présentons succinctement une notation algorithmique pour définir des types énumérés et intervalles qui nous serviront par la suite. Nous présentons également les types énumérés de JAVA, introduits dans sa version 5.0. Les types intervalles n'existent pas en JAVA.

3.5.1

Les types énumérés

Une façon simple de construire un type est d'énumérer les éléments qui le composent. On indique le nom du type suivi, entre accolades, des valeurs de l'ensemble à construire. Ces valeurs sont des noms de constantes ou des constantes prises dans un même type élémentaire. L'exemple suivant montre la déclaration de trois types énumérés. -0 0 c: =i 0 ,.-1

couleurs =

vert , b l eu ,

voye l l e s =

'a ' ,

nbpremi e r s

( 1,

3,

5,

'e ' ,

gri s ,

rouge,

11, 13 ) 'o ' , ' u ' , ' i ',

j aune

7,

'y'

)

"­ Cl. 0 u

TYPE D'U N E EXPRESSION

On vient de voir qu'une expression calcule un résultat. Ce résultat est typé et définit, par voie de conséquence, le type de l'expression. Ainsi, si p et q sont deux booléens, l'expression p ou q est une expression booléenne puisqu'elle produit un résultat booléen. Notez bien qu'une expression peut très bien être formée à partir d'opérateurs manipulant des objets de types différents, et dans ce cas le parenthésage peut servir à délimiter sans ambiguïté les opérandes de même type dont la composition forme un résultat d'un autre type, lui-même opérande d'un autre opérateur dans la même expression. Par exemple, l' expression booléenne suivante :

( i � max É lément s )

et ( c ourant # 0 )

est formée par les deux opérandes booléens de l'opérateur de conjonction eux-mêmes com­ posés à partir de deux opérandes numériques reliés par des opérateurs de relation à résultat booléen.

38

4.3

Chapitre

4 • Expressions

CONVERSIONS D E TYPE

Les objets, mais pas tous, peuvent être convertis d'un type vers un autre. Généralement, on distingue deux types de conversion. Les conversions implicites, pour lesquelles l'opérateur décide de la conversion à faire en fonction de la nature de l'opérande ; les conversions expli­ cites, pour lesquelles le programmeur est responsable de la mise en œuvre de la conversion à l'aide d'une notation adéquate. Dans les langages fortement typés, les conversions implicites sont, pour des raisons de sécurité de programmation, peu nombreuses. Dans un langage comme PASCAL, il n'existe qu'une seule conversion implicite des entiers vers les réels. Toutefois, il existe des exceptions, comme le langage C, qui définit de nombreuses conversions implicites. En revanche, les langages non typés, de par leur nature, ont en général un nombre de conversions implicites important, et il n'est pas toujours simple pour le programmeur de déterminer rapidement le type du résultat de l'évaluation d'une expression. > Les conversions de type en J AVA

Les conversions de type implicites sont relativement nombreuses, en partie dues à l' exis­ tence de plusieurs types entiers et réels. Ces conversions, appelées également promotions, transforment implicitement un objet de type T en un objet d'un type T', lorsque le contexte l'exige. Par exemple, l'évaluation de l' expression 1 + 4.76 provoquera la conversion impli­ cite de l 'entier 1 en un réel double précision 1 .0, suivie d'une addition réelle. Les promotions possibles sont données par la table 4.2. type

float long int short char

-0 0 c: =i 0

TABLE 4.2

"

début

{ Pi } � {Q1 }

:::::>

{ P2 } � {Q2} . . . {Pn } � {Qn}

:::::>

:

{Q}

{ Pi } E1 { Qi } { P2 } E2 { Q2 } . . . { Pn } En { Qn } fin { Q }

La notation { P } g { Q} exprime que le conséquent Q se déduit de 1 ' antécédent P par l'application de l'énoncé E. S'il n'y a pas d'énoncé, la notation {P} =? {Q} indique que Q se déduit directement de P. La règle de déduction précédente spécifie que si la pré-condition

{ P } :::::> {P1 } � {Q1 } :::::> {P2 } � {Q2} . . . {Pn } � {Qn } :::::> { Q } est vérifiée alors le conséquent Q se déduit de l'antécédent P par application de l'énoncé composé. > L'énoncé composé en J AVA

Les parenthéseurs sont représentés par les accolades ouvrantes et fermantes. La plupart des langages de programmation utilise un séparateur entre les énoncés, qui doit être considéré comme un opérateur de séquentialité. En JAVA, il n'y a pas à proprement parlé de séparateur d'énoncé. Toutefois, un point-virgule est nécessaire pour terminer un énoncé simple.

5.2

É NONCÉS CON D ITION N E LS

Les actions qui forment les programmes que nous avons écrits jusqu'à présent sont exé­ cutées systématiquement une fois. Les langages de programmation proposent des énoncés conditionnels qui permettent d'exécuter ou non une action selon une décision prise en fonc­ tion d'un choix. Le critère de choix est en général la valeur d'un objet d'un type élémentaire discret. -0 0 c: =i 0 "­ Cl. 0 u



..c Ol ï::: >0. 0 u

Chapitre 6

Procédures et fonctions

Dans une approche de la programmation organisée autour des actions, les programmes sont d' abord structurés à l'aide de procédures et de fonctions. À l' instar de C ou PASCAL, les langages qui suivent cette approche sont dits procéduraux. Nous avons vu précédemment comment utiliser une procédure ou une fonction prédéfinie ; dans ce chapitre, nous étudierons comment les construire et le mécanisme de transmission des paramètres lorsqu' elles sont appelées.

6.1 -0 0 c: =i 0 "­ Cl. 0 u

DÉCLARATION D'U N E ROUTI N E

Le rôle de l a déclaration d'une routine est de lier un nom unique à une suite d'énoncés sur des objets formels ne prenant des valeurs effectives qu'au moment de 1' appel de cette rou­ tine. Le nom est un identificateur de procédure ou de fonction. Cette déclaration est toujours formée d'un en-tête et d'un corps.

> L' en-tête L' en-tête de la routine, appelé aussi signature ou encore prototype, spécifie : le nom de la routine ; - les paramètres formels et leur type ; - le type du résultat dans le cas d'une fonction. La déclaration d'une procédure prend la forme suivante : :

{An t é cédent une affirma t i on } { Conséquent : une affirma t i o n } {Rôle : une a ffi rma t i on donnant l e rôle de l a procédure} procédure NomP r o c ( [ ] ) 1 . Ibidem.

6.3

55

Appel d'une routine

Notez que les crochets indiquent que ]a liste des paramètres formels est facultative. Tous 1es en-têtes de procédures doivent contenir des affirmations décrivant 1eur antécédent, 1eur conséquent ou leur rôle. L'en-tête correspondant à la déclaration de la procédure qui calcule les racines d'une équation du second degré peut être :

{An t é cédent { Conséquen t {Rôle

a#O, b et c rée l s, coeffi ci en t s de l ' équ a t i on du second degré, ax2 +bx+c} (x- (rl +i x i l ) ) (x- (r2+i x i 2 ) ) = 0 ) cal cule les ra cines d' une équa t i on du second degré}

procédure Éq2degré ( données a,

b,

résultats r l ,

c

:

il,

réel

r2 ,

i2

:

rée l )

La procédure s' appelle Éq2degré, possède sept paramètres formels a , b , c, qui sont les données de la procédure, et r 1 , i 1 , r 2 et i 2 , qui sont les résultats qu'elle calcule. Tous ces paramètres sont de type réel. Une fonction est une routine qui représente une valeur résultat qui peut intervenir dans une expression. La déclaration d' une fonction précise en plus le type du résultat renvoyé :

{An t é cédent { Conséquent {Rôle fonction

une affirma t i on } une affirma t i o n } une a ffirma t i on donnant le rôle de l a fon c t i o n } NomF o n c ( [
  • ] ) : type - r é s u l t at

    L'en-tête suivant déclare une fonction qui retourne la racine carrée d'un entier naturel :

    {An t é cédent x � 0) { Conséquen t : rac2 y'xJ : cal cu l e la ra cine carrée de l ' en tier n a t urel x } {Rôle =

    fonction rac2 ( donnée x

    :

    naturel )

    :

    réel

    � Le corps

    -0 0 c: =i 0 "­ Cl. 0 u

    1,

    3,

    4, 2

    6, :

    5,

    7,

    8,

    10 ,

    12

    9,

    11

    : max +- 3 0

    si b i s sext i l e ( a )

    : max f- 3 1 alors max f- 2 9

    sinon maxf- 2 8 finsi finchoix

    {max = n ombre de j ours dans l e mois m} ( j � l ) et ( j�max)

    çava ffinsi finsi finproc

    {da t e Val i de }

    Enfin, écrivons la procédure é c r i reDate : {An técédent {Conséquent

    j , m, a représen t e n t une da t e val i de} l a da t e est écri t e sur l a sorti e s tandard a vec l e mois en t outes l e t t res}

    0)

    6.9

    Exercices

    65

    procédure é c r i reDat e ( données j , m,

    a

    ent i e r )

    é c r i re ( j , ' ) choix m parmi '--' '

    1

    é c r i re ( " j anvier " )

    2

    é c r i r e ( " févri e r " )

    3

    é c r i r e ( " mar s " )

    4

    é c r i r e ( " av r i l " )

    5

    é c r i r e ( " mai " )

    6

    écrire ( " juin " )

    7

    é c r i r e ( " j u i l l et " )

    8

    é c r i r e ( " août " )

    9

    é c r i re ( " septembre " )

    10

    é c r i r e ( " octobre " )

    11

    é c r i r e ( " novembre " )

    12

    é c r i r e ( " décemb re " )

    finchoix é c r i r e ( ' '-' ' , finproc

    6.9

    a)

    { écri reDa t e }

    EXERCICES

    Écrivez en JAVA et testez la fonction b i s s e xt i l e qui teste s i une année passée en paramètre est bissextile ou non.

    Exercice 6.1.

    Écrivez en JAVA et testez la fonction max2 qui renvoie le maximum de deux entiers passés en paramètres. Exercice 6.2.

    Exercice 6.3.

    En utilisant la fonction max2 précédente, écrivez une fonction max3 qui ren­ voie le maximum de trois entiers passés en paramètres.

    Écrivez la fonction f ahrenhei t qui prend en paramètre un réel représentant une température en degrés Celcius et qui renvoie sa conversion en degrés Fahrenheit.

    Exercice 6.4. -0 0 c: =i 0 "=0 ) { Il calcul des racines réelles if (b> O ) rl = - ( b+Mat h . sqrt ( � ) ) / ( 2 * a ) ; else r l = (Math . s qrt ( � ) -b) / ( 2 * a ) ; Il rl e s t l a racine l a pl us grande en valeur absolue r2 = Math . abs ( r l ) < E ? 0 : c / ( a * r l ) ; i l =i2=0 ; Il (x - rl ) (x - r2) = 0

    78

    Chapitre

    else

    *

    Programmation par objets

    cal cul des racines compl exes

    rl = r2 = -b/ ( 2 * a ) ; i l =Math . s qrt ( � ) / ( 2 * a ) ; -

    I**



    { Il

    } Il

    7

    +

    (x - (rl

    ixil) )

    i2=-i l ;

    (x - (r2

    +

    ix i2) )

    =

    0

    Conséquen t : renvoie une représent a t i on sous forme d ' une chaîne de caractères des deux ra cines de l ' équa t i on

    *I

    public String t oS t r ing ( ) {

    return

    " r 1 =w ( " + rl + " r 2 =w ( "

    + r2 +

    + il + " )\n"

    +

    + i2 + " ) " ;

    fin classe Éq2Degré

    Il

    Vous notez la présence de la méthode t o S t ri n g , qui permet la conversion d'un objet É q2Degré en une chaîne de caractères 6 . Celle-ci peut être utilisée implicitement par cer­ taines méthodes, comme la procédure System . out . p r i n t l n qui n'accepte en paramètre qu' une chaîne de caractères. Si le paramètre n'est pas de type S t r i ng, deux cas se pré­ sentent : soit il est d'un type de base, et alors il est converti implicitement ; soit le paramètre est un objet, et alors la méthode t o S t r i n g de l'objet est appelée afin d'obtenir une repré­ sentation sous forme de chaîne de caractères de 1'objet courant. Nous pouvons écrire la classe T e s t , contenant la méthode main, pour tester la classe É q2Degré.

    import j ava . i o . * ; class T e s t public static void ma i n ( St ring [ ] args ) throws IOExcept i o n -0 0 c: =i 0 " L'énoncé

    tantque non B faire E2 E 1 fintantque E tantque non B faire E f intantque

    E1

    répéter de JAVA

    Cet énoncé s'écrit de la façon suivante : do E while ( B ) ;

    Notez que la sémantique du prédicat d'achèvement est à l'opposé de celle de la forme algorithmique. L'itération s'achève lorsque le prédicat B prend la valeur faux. La règle de déduction est donc légèrement modifiée :

    si { P } E { Q } alors { P } do

    { Q et B ) E { Q ) while ( B ) ; { Q et

    et E

    non

    B)

    88

    Chapitre

    8.4

    8



    Énoncés itératifs

    FIN ITU D E

    Lorsqu'un programme exécute une boucle, il faut être certain que le processus itératif s'achève ; sinon on dit que le programme boucle. La finitude des énoncés itératifs est une propriété fondamentale des programmes informatiques. L'achèvement d'une boucle ne peut se vérifier par son exécution sur l'ordinateur. Démontrer la finitude d'une boucle se fait de façon analytique : on cherche une fonction f (X) où X représente des variables mises en jeu dans le corps de la boucle et qui devront nécessairement être modifiées par le processus itératif. Cette fonction est positive et décroît strictement vers une valeur particulière. Pour cette valeur, par exemple zéro, on en déduit que B est vérifié, et que la boucle s'achève.

    8.5

    EXE M PLES

    La première partie de ce chapitre était assez théorique. Il est temps de montrer l'utilisation des énoncés répétitifs sur des exemples concrets.

    8.5.1

    Factorielle

    Nous désirons écrire la fonction f a c t o r i e l l e qui calcule la factorielle d'un entier natu­ rel passé en paramètre. Rappelons que la factorielle d'un entier naturel n est égale à :

    n!

    =

    1

    X

    2

    X

    3X ...

    X

    (i - 1 )

    X

    i

    X . . . X n

    Une première façon de procéder est de calculer une suite croissante de produits de 1 à n. Peut-on déterminer immédiatement l'invariant de boucle ? Au ie produit, c'est-à-dire à la je itération qu'a-t-on calculé ? Réponse il et ce sera notre invariant. L'algorithme et la démonstration de sa validité sont donnés ci-dessous :

    -0 0 c: =i 0 "­ Cl. 0 u

    2. HÉRON L'ANCIEN, mathématicien et mécanicien grec du 1°' siècle.

    Chapitre 9

    Les tableaux

    Les tableaux définissent un nouveau mode de structuration des données. Un tableau est un agrégat de composants, objets élémentaires ou non, de même type et dont 1' accès à ses composants se fait par un indice calculé.

    9.1

    -0 0 c: =i 0 "­ Cl. 0 u

    L'environnement JAVA définit deux classes, S t r i n g et S t r i ngBu i l der, pour créer et manipuler des chaînes de caractères. Les objets de type S t r i n g sont des chaînes constantes, i.e., une fois créées, elles ne peuvent plus être modifiées. Au contraire, les chaînes de type St r i n gBu i l de r peuvent être modifiées dynamiquement. Ces deux classes offrent une multitude de services que nous ne décrirons pas ici. Nous nous contenterons de présenter quelques fonctions classiques de la classe S t ring . La méthode length renvoie le nombre de caractères contenus dans la chaîne courante. La méthode charAt renvoie le caractère dont la position dans la chaîne courante est passée en paramètre. La position du premier caractère est zéro. La méthode i ndexOf renvoie la première position du caractère passé en paramètre dans la chaîne courante. La méthode compareTo compare la chaîne courante avec une chaîne passée en paramètre. La comparaison, selon l'ordre lexicographique, renvoie zéro si les chaînes sont identiques, une valeur entière négative si l'objet courant est inférieur au paramètre, et une valeur entière positive si l'objet courant lui est supérieur.

    1 02

    Chapitre

    9



    Les tableaux

    De plus, le langage définit l'opérateur + qui concatène deux chaînes 3 , et d'une façon générale deux objets munis de la méthode t o S t r ing. Le fragment de code suivant déclare deux variables de type S t ring et met en évidence les méthodes données plus haut : S t r i n g c l = new S t r ing ( " b o n j o u r._. " ) ;

    // ou String cl

    "bonj our ";

    S t ring c2 ; c2 = c l + " à ._. t o u s " ; S y s t em . out . p r i nt l n ( c 2 ) ; S y s t em . out . println ( c 2 . l ength ( ) ) ; S y s t em . out . print l n ( c 2 . charAt ( 3 ) ) ; S y s t em . out . println ( c 2 . indexüf ( ' o ' ) ) ; S y s tem . out . p r i n t l n ( c l . compa reTo ( " à._. t o u s " ) ) ;

    L'exécution de ce fragment de code produit les résultats suivants : bon j o u r à t o u s 14

    j 1

    -12 6

    // // // //

    l ongueur de l a chaine "bonjour l e qua trième caractère posi t i on du premier ' o ' "bon jour " < "� t o u s "



    tous "

    Le résultat de la comparaison paraît surprenant puisque la lettre a, même accentuée, précède la lettre b dans 1' alphabet français. En fait, la comparaison suit 1 ' ordre des carac­ tères dans le jeu UNICODE. Toutefois, JAVA définit la classe C o l l a t o r (du paquetage j ava . t ext) qui connaît les règles de comparaisons spécifiques à différentes langues inter­ nationales. À la création d'une instance de type C o l l at o r, on indique la langue désirée, puis on utilise la méthode compare à laquelle on passe en paramètre les deux chaînes à comparer. Le résultat de la comparaison suit la même convention que celle de compareTo. La comparaison suivante renvoie une valeur positive. C o l l a t o r f r = C o l l at o r . get i n s t ance ( Lo c a l e . FRENCH ) ; -0 0 c: =i 0 "­ Cl. 0 u

    Chapitre 1 0

    L'énoncé itératif pour

    1 0. 1

    FORM E G É N ÉRALE

    Il arrive souvent que l'on ait besoin de faire le même traitement sur toutes les valeurs d'un type donné. Par exemple, on désire afficher tous les caractères contenus dans le type caractère du langage de programmation avec lequel on travaille. Beaucoup de langages de programmation proposent une construction adaptée à ce besoin spécifique, appelée énoncé itératif pour. Une forme générale de cette construction est un énoncé qui possède la syntaxe suivante : -0 0 c: =i 0 "­ Cl. 0 u

    Le principe de la sélection ordinaire est de rechercher le minimum de la liste, de le pla­ cer en tête de liste et de recommencer sur le reste de la liste. En utilisant la liste d'entiers précédente, le déroulement de cette méthode donne les étapes suivantes. À chaque étape, le minimum trouvé est souligné.

    1 11

    53

    1

    914

    827

    302

    631

    785

    230

    11

    5 67

    350

    914

    827

    302

    631

    785

    230

    53

    5 67

    350

    302

    631

    785

    230

    914

    5 67

    350

    302

    631

    785

    827

    914

    567

    350

    11

    53

    1

    11

    53

    230

    1

    11

    53

    230

    302

    1

    11

    53

    230

    302

    350

    1

    11

    53

    230

    302

    350

    5 67

    1

    631

    1

    827

    631

    785

    827

    914

    5 67

    350

    785

    827

    914

    5 67

    631

    827

    914

    785

    631

    785

    827

    914

    827

    11

    53

    230

    302

    350

    5 67

    11

    53

    230

    302

    350

    5 67

    631

    785

    1

    11

    53

    230

    302

    350

    5 67

    631

    785

    827

    914

    1

    914

    L' algorithme de tri suit un processus itératif dont l'invariant spécifie, d'une part, qu' à la je étape la sous-liste formée des éléments de t [ 1 ] à t [ i -1 ] est triée, et, d'autre part, que tous ses éléments sont inférieurs ou égaux aux éléments t [ i ] à t [ n ] On en déduit que le nombre d'étapes est n - 1 . .

    110

    Chapitre

    Algorithme Tri

    10



    L'énoncé itératif pour

    p a r s é lection ordinaire

    {Rôle : Trie par sélec t i on ordinaire en ordre croi ssant } les n valeurs d ' un tabl eau t } { pourtout i de 1 à n - 1 faire { In variant : l e s o us-tableau de t [l ] à t [i - 1 ) est trié et ses élémen t s son t inférieurs o u éga ux a ux élémen t s t [i ] à t [n ] } min +-- i

    { chercher l ' indi ce du minimum sur l ' in t erva l l e [i , n ] } à n faire

    pourtout j de i + l si t [ j ]

    < t [ mi n ]

    alors min +--

    j finsi

    finpour

    {échanger t [i ] et t [min ] } t [i]

    B

    t [m i n ]

    finpour

    { l e tabl eau de t [l ]

    à

    t [n ] est t ri é }

    Programmation en J AVA

    La procédure suivante programme l'algorithme de tri par sélection ordinaire. Remarquez les déclarations des variables min et aux dans le corps de la boucle for la plus externe. I** *

    Rôle : Trie par sél ect i on ordinaire en ordre croissan t les val eurs du tabl eau t

    *I public void s é lecti onOrdinaire ( int for

    ( int i = O ; Il Il Il

    i ­ Cl. 0 u

    Si, par exemple, nous recherchons le mot noir dans le texte anoraks noirs, les différentes comparaisons et les déplacements d produits lors des échecs sont donnés ci-dessous : a

    n

    o

    r

    n

    0

    i

    r

    n

    0

    i

    a

    k

    n

    s

    o

    i

    r

    s

    échec ==> d r

    n

    0

    i

    échec ==> d

    r

    n

    0

    i

    r

    échec ==> d

    = = =

    1 4 3

    succès

    Après le premier échec, l'alignement des deux lettres o provoque un déplacement d'un caractère. Après le deuxième échec, et puisqu'il n'y a pas de a dans le mot, la fenêtre est ici.

    3. Il en existe une troisième qui tient compte de suffixes déjà reconnus dans le mot, mais qui ne sera pas évoquée

    112

    Chapitre 1 0



    L'énoncé itératif pour

    placée immédiatement après cette lettre. La comparaison entre le n et le r échoue. La lettre n est présente dans le mot, 1 ' ajustement produit un déplacement de trois caractères. Enfin, le mot et la fenêtre sont identiques. L'algorithme donné ci-dessous renvoie l' indice dans le texte du premier caractère du mot recherché s'il est trouvé, sinon il renvoie la valeur 1 Nous représentons le mot et le texte par deux tableaux de caractères. La variable p os indique la position courante dans le texte. Nous traiterons le calcul de la valeur du déplacement plus loin. -

    Algorithme Boyer-Moore variables pos ,

    i,

    .

    j de type nat u r e l

    variable d i f férent de type booléen

    pos

    +---

    1

    tantque pos



    l gt exte- l gmo t + l faire

    d i f f é rent +--- faux

    { On compare

    à

    partir de l a fin du mot e t de l a fen ê t re }

    i +--- lgmot j +--- p o s + l gmot - 1 répéter si mot [ i ] =text e [ j ]

    alors

    { égal i t é � on poursui t l a comparaison} i +--- i - 1 j +--- j - 1 sinon

    { di fféren ce � on s ' arrê t e } di f f é rent

    +---

    vrai

    finsi jusqu' à i = O ou di f fé rent si i=O alors

    { l a comparaison a réussi, renvoyer la pos i t i on }

    rendre pos sinon

    { échec : dépl acer l a fenêtre vers l a droi t e } p o s +--- pos + { va l e ur d u déplacement }

    finsi

    -0 0 c: =i 0 "­ Cl. 0 u

    { composant de type t ableau [ [l , n ] J de réels } { composant de type réel }

    mat r i c e [ l , 4 ]

    {composant de type tableau [bool éen, [ 1 , n ] ] de réel s } ' f ' , vr a i ] {composant de type tableau [ [1 , n ] J de réel s } ' f ' , vr a i , 3 J { composant de type rée l }

    table [ ' f ' ] table [ table [

    Les indices sont des expressions dont les résultats des évaluations doivent appartenir au type de l'indice associé.

    1 1 .3

    M O DI FICATION S É LECTIVE

    Les remarques faites sur la modification sélective d'un composant d'un tableau à une di­ mension (voir la section 9.3 page 97) s' appliquent de façon identique à un composant de tableau à plusieurs dimensions. Le fait qu'un composant de tableau soit lui-même de type tableau n'introduit aucune règle particulière.

    Opérations

    1 1.4

    1 1 .4

    121

    OPÉRATIONS

    Les opérations sur les tableaux à plusieurs dimensions sont identiques à celles sur les tableaux à une dimension (voir la section 9.4 page 97).

    1 1 .5

    TABLEAUX À PLU S I E U RS DIM ENSIONS E N JAVA

    Les tableaux à plusieurs dimensions sont traités comme des tableaux de tableaux. Le nombre de dimensions peut être quelconque et les règles pour leur déclaration et leur création sont semblables à celles données dans la section 9 5 page 97. On déclare un tableau t à n dimensions dont les composants sont de type Tc de la façon suivante : .

    Tc

    [] [] []

    ...

    []

    t;

    La création des composantes du tableau t est explicitée à l'aide de l'opérateur new. Pour chacune des dimensions, on indique son nombre de composants :

    La déclaration de la matrice de réels à m lignes et n colonnes de la section 1 1 . 1 s'écrit en JAVA comme suit : double

    [ ] [ ] mat r i ce = new double

    [m] [ n ] ;

    L'accès aux éléments de la matrice se fait par une double indexation, dénotée mat r i c e [ i J [ j J , où i et j sont deux indices définis, respectivement, sur les intervalles [ 0 , m- 1 ] et [ 0 , n - 1 ] .

    -0 0 c: =i 0 "­ Cl. 0 u

    REDÉFINITION D E M ÉTHODES

    Lorsqu'une classe héritière désire modifier l ' implémentation d'une méthode d'une classe parent, i l lui suffit de redéfinir cette méthode. La redéfinition d'une méthode est nécessaire si on désire adapter son action à des besoins spécifiques. Imaginons, par exemple, que la classe Rectangle possède une méthode d'affichage, la classe Carré peut redéfinir cette méthode pour l'adapter à ses besoins . classe Rectangle

    {Rôle : a ffiche une description du rectangl e couran t } procédure a f f i ch e r ( ) é c r ire ( " re c t angle de l argeu r " , " e t de l ongueu r " finproc finclasse Rectangle

    largeur, ,

    longueu r )

    12.3

    Recherche d'un attribut ou d'une méthode

    135

    classe Carré hérite de Re ctangle

    {Rôle : a ffiche une description du carré couran t } procédure a f f i cher ( )

    é c r i r e ( " carré de côté égal à " ,

    largeur )

    finproc

    finclasse Carré

    Dans le fragment de code suivant : variable c type C a r r é créer C a r r é ( S ) variable r type Re ctangle créer Re ctangle ( 2 , 4 )

    r . a f f i cher ( ) c . a f f i cher ( )

    il est clair que c'est la méthode a f f i cher de la classe Rectangle qui s'applique à l'objet r et celle de la classe Carré qui s' applique à l'objet c . Notez que les redéfinitions permettent de changer la mise en œuvre des actions, tout en préservant leur sémantique. Ainsi, la mé­ thode a f f i ch er de la classe Carré ne devra pas calculer, par exemple, la surface d'un carré. On peut remarquer que la classe Carré hérite des méthodes changerLargeur et changerLongueur qui, utilisées individuellement, peuvent mettre en cause l'invariant de classe qui assure que la longueur et largeur d'un carré doivent être égales. Certains lan­ gages de programmation ont des mécanismes qui permettent de limiter les attributs de la classe mère hérités par la sous-classe. En l'absence de tels mécanismes, il sera nécessaire de redéfinir les méthodes changerLargeur et changerLongueur afin qu'elles modifient simultanément la largeur et la longueur du carré afin de garantir l ' invariant de classe.

    -0 0 c: =i 0 "­ Cl. 0 u

    FIGURE 12.4 Graphe d'héritage CercledansCarré.

    Lorsqu'une classe ne possède qu'une seule classe parente, l'héritage est simple. En re­ vanche, si une classe peut hériter de plusieurs classes parentes différentes, l'héritage est alors multiple. Avec l'héritage multiple, les relations d'héritage entre les classes ne définissent plus une simple arborescence, mais de façon plus générale un graphe, appelé graphe d'héritage 3 . L'héritage multiple introduit une complexité non négligeable dans le choix de la méthode à appliquer en cas de conflit de noms ou d'héritage répété. Pour le programmeur, le choix d'une méthode à appliquer peut ne pas être évident. C'est pour cela que certains langages de programmation, comme JAVA 4, ne le permettent pas. 3. Voir les chapitres 19 et 20 qui décrivent les types abstraits graphe et arbre.

    4. Toutefois, ce langage définit la notion d' inte1face qui permet de mettre en œuvre une forme particulière de l'héritage multiple.

    12.7

    Héritage et assertions

    1 2.7

    1 39

    H É RITAG E ET ASSERTIONS

    Le mécanisme d'héritage introduit de nouvelles règles pour l a définition des assertions des classes héritières et des méthodes qu'elles comportent.

    1 2. 7 . 1

    Assertions sur les classes héritières

    L'invariant d'une classe héritière est la conjonction des invariants de ses classes parentes et de son propre invariant. Dans notre exemple, l'invariant de la classe Carré est celui de la classe Rect angle, i.e. la largeur et la longueur d'un rectangle doivent être positives ou nulles, et de son propre invariant, i.e. ces deux longueurs doivent être égales.

    1 2. 7 .2

    Assertions sur les méthodes

    Les règles de définition des antécédents et des conséquents sur les méthodes doivent être complétées dans le cas particulier de la redéfinition. Nous prendrons ici les règles données par B . MEYER [Mey97]. Une assertion A est plus forte qu'une assertion B, si A implique B. Inversement, nous dirons que B est plus faible. Lors d'une redéfinition d'une méthode m, que nous appellerons ' m , i 1 faudra que : ( 1 ) l'antécédent de m' soit plus faible ou égal que celui de m ; (2) le conséquent de m' soit plus fort ou égal que celui de m.

    -0 0 c: =i 0 "­ Cl. 0 u

    L'émission explicite d'une exception est produite grâce à l ' instruction throw. Le type de l'exception peut être prédéfini dans 1' API, ou défini par le programmeur.

    @

    if

    Émission d'une exception

    ( une s it u a t i on a n o rmale ) throw new Ari thmet icExcept i on ( ) ;

    if

    ( une s i t u a t i o n a n o rma l e ) throw new MonExcept ion ( " un mess a g e " ) ; ......

    Notez qu'une méthode (ou un constructeur) qui émet explicitement une exception doit également le signaler dans son en-tête avec le mot-clé throws, sauf si l'exception dérive de la classe RuntimeExcept i on.

    14.4

    Exercices

    1 4.4

    161

    EXERCICES

    Exercice 14.1.

    Complétez le code ci-dessous pour obliger 1 'utilisateur à fournir un entier.

    public static void rnain ( S t ring [ ] int i

    =

    0;

    =

    boolean nonLu

    s) {

    true;

    do try

    S y stem . out . println ( " un ...... e n t i e r ...... : " ) ; i= Stdi nput . re a d l n i n t ( ) ;

    ( IOException e ) {

    catch

    while

    (

    .

    .

    .

    .

    .

    .

    .

    .

    .

    .

    .

    .

    .

    .

    ) ;

    S y s t ern . out . print l n ( " e n t i e r ...... l u

    ......

    : ...... " + i ) ;

    Après chaque échec de lecture, la méthode l i reEnt i e r de la page 1 60 essaie une nouvelle lecture sans limiter le nombre de tentatives. Ceci peut être une source majeure de problèmes, si, par exemple, la saisie du nombre à lire est faite automatiquement par un programme qui produit systématiquement une valeur erronée. Modifiez la méthode l i reEnt i e r afin de limiter le nombre de tentatives, et de transmettre l'exception à l'envi­ ronnement d'appel si aucune des tentatives n'a réussi. Exercice 14.2.

    Écrivez une méthode qui calcule l'inverse d'un nombre réel x quelconque, i.e. l /x. Lorsque x est trop petit, l' opération produit une division par zéro, mais la méthode devra renvoyer dans ce cas la valeur zéro.

    Exercice 14.3.

    Exercice 14.4. -0 0 c: =i 0 "­ Cl. 0 u

    for

    ( int i=O ;

    i Cl.

    8

    throws IOExcept i o n ,

    D a t a i nputStream f

    S t r ing f2 )

    EOFExcept i o n { =

    new D a t a i np u t S t ream (new F i l e i nput St ream ( f l ) ) ; D a t a i nputStream g

    =

    new D a t a i npu t S t r e am (new F i l e i nput St ream ( f 2 ) ) ; Dat aOutput S t r e am h

    =

    new Dat aOutput S t r e am ( new F i l eOutput St ream ( nomF i ch ) ) ; int x ,

    Il

    y;

    l i re l e premier en t i er de cha cun des fi ch i ers

    try { x catch

    =

    f . readint ( ) ;

    ( EOFException e )

    }

    -+

    Il

    fdf (f)

    ::::}

    recopier g sur h

    { Il

    fdf (g)

    ::::}

    recopier

    recop i e r ( g , h ) ; return; try catch

    y

    =

    g . readint ( ) ;

    (EOFExcept i o n e )

    X

    -+

    et f sur h

    1 72

    Chapitre 1 5 • Les fichiers séquentiels

    h . writ e i nt ( x ) ; recopi e r ( f , h ) ; return;

    }

    Il les fichiers h et g con t i ennent tous l e s deux a u moins un en t i er while

    (true)

    Il met t re dans h min (f, g) et passer au s u i van t if ( xy ::::} écrire y sur h

    el se

    h . w r i t e int ( y ) ; try { y = g . r e a d i nt ( ) ; catch

    (EOFExcep t i o n e ) 11

    -+

    fdf (g) ::::} recopi er x e t f sur h

    h . w r i t e i nt ( x ) ; recopier ( f , h ) ; return;

    }

    Il

    -0 0 c: =i 0 "­ Cl. 0 u



    ..c Ol ï::: >0. 0 u

    Chapitre 1 6

    Récursivité

    Les fonctions récursives jouent un rôle très important en informatique. En 1 936, avant même l' avènement des premiers ordinateurs électroniques, le mathématicien A. CHURCH 1 avait émis la thèse 2 que toute fonction calculable, c'est-à-dire qui peut être résolue selon un algorithme sur une machine, peut être décrite par une fonction récursive.

    -0 0 c: =i 0 "­ a. 0 u

    FIGURE 16.3 Courbes de Hilbert de niveau 5. 5. DAVID HILBERT, mathématicien allemand ( 1 862-1943), proposa cette courbe en 1890. Ce type de figure géométrique est plus connu aujourd'hui sous Je nom de fractale.

    190

    Chapitre 1 6



    Récursivité

    Si l'on possède une fonction tracer qui trace un segment de droite dans le plan à partir de la position courante jusqu'au point de coordonnées (x,y), la procédure A s'écrit : A de la courbe de Hilbert de n i veau i h longueur du segment qui relie les courbes de ni veau i - 1 }

    {Rôle : tracer l a partie

    procédure A ( donnée i : naturel) si i>O alors D (i-1)

    X � x-h

    tracer ( x , y )

    A (i-1)

    y � y-h

    trace r ( x , y )

    A (i-1)

    X � x+h

    tracer ( x , y )

    B (i-1) finsi finproc

    {A}

    L'écriture des procédures B,

    1 6.2

    c

    et D, bâties sur ce modèle, est immédiate.

    RÉCURSIVITÉ DES OBJETS

    À ]'instar de la récursivité des actions, un objet récursif est un objet qui contient un ou plusieurs composants du même type que lui. La récursivité des objets peut être également indirecte. Imaginons que l'on veuille représenter la généalogie d'un individu. En plus de son identité, il est nécessaire de connaître son ascendance, c'est-à-dire l 'arbre généalogique de sa mère et de son père. Il est clair que le type Arbre Généal ogique que nous sommes amenés à définir est récursif : classe Arbre Généalogique prénom type chaîne de caractères mère, père type Arbre Généalogique finclasse

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    {Arbre Généalogique}

    Conceptue11ement, une tel1e déclaration décrit une structure infinie, mais en généra] ]es langages de programmation ne permettent pas cette forme d' auto-inclusion des objets. Con­ trairement à la récursivité des actions, celle des objets ne crée pas « automatiquement » une infinité d'incarnations d'objets. C'est au programmeur de créer explicitement chacune de ces incarnations et de les relier entre elles. La caractéristique fondamentale des objets récursifs est leur nature dynamique. Les objets que nous avons étudiés jusqu'à présent possédaient tous une taille fixe. La taille des objets récursifs pouITa, quant à e11e, varier au cours de 1 'exécution du programme. Pour mettre en œuvre la récmsivité des objets, les langages de programmation proposent des outils qui permettent de créer dynamiquement un objet du type voulu et d' accéder à cet objet. Des langages comme C ou PASCAL ne permettent pas une définition directement récursive des types, mais ] 'autorisent au moyen de pointeurs. Ils mettent à la disposition du program­ meur des fonctions d' allocation mémoire, comme rnalloc et new, pour créer dynamique­ ment les incarnations des objets auxquelles il accède par l'intermédiaire des pointeurs.

    16.2

    191

    Récursivité des objets

    \\\\

    \\\\

    \\\\

    R \\ �---+\\ ---

    \\\\

    FIGURE 16.4 La généalogie de Lo u ise . Pour de nombreuses raisons, mais en particulier pour des raisons de fiabilité de construc­ tion des programmes, les langages de programmation modernes ont abandonné la notion de pointeur. C'est le cas de JAVA qui permet des définitions d'objet vraiment récursives. Ainsi, le type Arbre Généalogique sera déclaré en JAVA comme suit : class ArbreGénéalogique

    String prénom;

    Il défi n i t i on de l ' ascendance ArbreGénéalogique mère, père;

    Il le constructeur ArbreGénéalogiqu e ( String s )

    {

    prénom=s ;

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    Il ArbreGénéalogique

    Cette définition est possible en JAVA car les attdbuts mère et père sont des références à des objets de type ArbreGénéalog ique et non pas l ' objet lui-même. L'arbre généalogique de Louise, donné par la figure 16.4, est produit par la séquence d'instructions suivante : ArbreGénéalogique ag; ag

    =

    new ArbreGénéalogique ( " Lo u i se " ) ;

    ag . mère

    =

    new ArbreGénéalogique ( " Mahaba " ) ;

    ag . père = new ArbreGénéalogique ( " Pa u l " ) ;

    new ArbreGénéalogique ( " Monique " ) ;

    ag . mère . mère ag . père . mère ag . père . père

    =

    =

    new ArbreGénéalogique ( " Maud" ) ;

    new ArbreGénéalogique ( " Dhakwan " ) ;

    ag . père . père . père

    =

    new ArbreGénéalogique ( " J a c q u e s " ) ;

    Chacun des ascendants, lorsqu'il est connu, est créé grâce à l'opérateur new et est relié à sa propre ascendance par l'intermédiaire des références. Celles-ci sont représentées sur

    192

    Chapitre 16



    Récursivité

    espace libre

    lzone globale1 FIGURE 16.5 Organisation de la mémoi re

    .

    la figure 16.4 par des flèches. Le symbole de la terre des électriciens indique la fin de la récursivité. En JAVA, il correspond à une référence égale à la constante null. Les supports d'exécution des langages de programmation placent les objets dynamiques dans une zone spéciale, appelée tas. La figure 16.5 montre l'organisation classique de la mé­ moire lors de l'exécution d'un programme. La zone globale est une zone de taille fixe qui contient des constantes et des données globales du programme. La pile d'évaluation, dont la taille varie au cours de l'exécution du programme, contient l'empilement des zones locales des routines appelées avec leur pile d'évaluation des expressions. Enfin, le tas contient les objets alloués dynamiquement par le programme. Le tas croît en direction de la pile d'éva­ luation. S'ils se rencontrent, le programme s'arrête faute de place mémoire pour s'exécuter.

    'O 0 c ::J 0 V T"l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    Selon les langages, la suppression des objets dynamiques, c'est-à-dire la libération de la place mémoire qu'ils occupent dans le tas, peut être à la charge du programmeur, ou laisser au support d'exécution. La suppression des objets dynamiques est une source de nombreuses erreurs, et il est préférable que le langage automatise la destruction des objets qui ne servent plus. C'est le choix fait par JAVA. Dans les prochains chapitres, nous aurons souvent l'occasion de mettre en pratique des définitions d'objet récursif. De nombreuses structures de données que nous aborderons se­ ront définies de façon récursive et les algorithmes qui les manipuleront seront eux-mêmes naturellement récursifs .

    1 6.3

    EXERCICES

    Exercice 16.1. Programmez en JAVA les algorithmes fibonacci et ToursdeHanoï nés à la page 183.

    don­

    Éctivez en JAVA et de façon récursive la fonction puissance qui élève un nombre réel à la puissance n (entière positive ou nulle). Note : lorsque n est pair, pensez à l'élévation au carré. Exercice 16.2.

    16.3

    193

    Exercices

    Exercice 16.3.

    En vous inspirant de la procédure écrireChiffres, écrivez de façon récursive et itérative la fonction converti rRornain qui retourne la représentation romaine d'un entier. On rappelle que les nombres romains sont découpés en quatre tranches : miJliers, centaines, dizaines et unités (dans cet ordre). Dans chaque tranche, on écrit de zéro à quatre chiffres et jamais plus de trois chiffres identiques consécutifs. Les tranches nulles ne sont pas représentées. Les chiffres romains sont 1 = 1, V = 5, X = 10, L = 50, C = 100, D = 500, et lVf = 1000. Par exemple, 49 = X LIX, 703 = DCCIII et 2000 = lVflVf. Éclivez une procédure qui engendre les n! permutations de n éléments a1, an. La tâche consistant à engendrer les n! permutations des éléments a1 , an peut être décomposée en n sous-tâches de génération de toutes les permutations de a1, an- l suivies de an, avec échange de ai et an dans la ie sous-tâche.

    Exercice 16.4. ·

    ·

    ·

    ,

    ·

    ·

    ·

    ·

    ,

    ·

    ·

    ,

    Écrivez un programme qui recopie sur la sottie standard un fichier de texte. Lorsqu'il reconnaît dans le fichier une directive de la forme ! nom-de-fichier, il inclut le contenu du fichier en lieu et place de la directive. Évidemment, un fichier inclus peut contenir une ou plusieurs directives d'inclusion. Exercice 16.5.

    Exercice 16.6.

    Soit une fonction continue .f définie sur un intervalle [a, b]. On cherche à trouver un zéro de .f, c'est-à-dire un réel x E [a, b] tel que f (x) = O. Si la fonction admet plusieurs zéros, n'importe lequel fera l'affaire. S'il n'y en a pas, il faudra le signaler. Dans le cas où f(a) . .f(b) < 0, on est sûr de la présence d'un zéro. Lorsque f(a).f (b) > 0, il faut rechercher un sous-intervalle [a, ,8], tel que f (a).f (,8) < O. L'algorithme procède par dichotomie, c'est-à-dire qu'il va diviser l'intervalle de recherche en deux moitiés à chaque étape. Si l'un des deux nouveaux intervalles, par exemple [a, ,8], est tel que f (a).f (,8) < 0, on sait qu'il contient un zéro puisque la fonction est continue : on poursuivra alors la recherche dans cet intervalle.

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    En revanche, si les deux demi-intervalles sont tels que f a le même signe aux deux extré­ mités, la solution, si elle existe, sera dans l'un ou l'autre de ces deux demi-intervalles. Dans ce cas, on prendra arbitrairement l'un des deux demi-intervalles pour continuer la recherche ; en cas d'échec on reprendra le deuxième demi-intervalle qui avait été provisoirement négligé. Écrivez de façon récursive l'algorithme de recherche d'un zéro, à E près, de la fonction .f. Exercice 16.7.

    À paitir de la petite grammaire d'expression donnée à la page

    189, écrivez

    en JAVA un évaluateur d'expressions arithmétiques infixes. Pour simplifier, vous ne traiterez pas la notion de variable dans facteur.

    -0 0 c ::J 0 «:!" T""l 0 N @ ...... ..c Ol ï:: >0.. 0 u

    Chapitre 1 7

    Structures de données

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    Le terme structure de données désigne une composition de données unies par une même sémantique. Mais, cette sémantique ne se réduit pas à celle des types (élémentaires ou struc­ turés) des langages de programmation utilisés pour programmer la structure de données. Dès le début des années 1970, C.A.R. HOARE [Hoa72] mettait en avant l'idée qu'une donnée représente avant tout une abstraction du monde réel définie en terme de structures abstraites, et qui n'est d'ailleurs pas nécessairement mise en œuvre à l'aide d'un langage de program­ mation patiiculier. D'une façon plus générale, un programme peut être lui-même modélisé en termes de données abstraites munies d'opérations abstraites. Ces réflexions ont conduit à définir une structure de données comme une donnée abstraite, dont le comportement est modélisé par des opérations abstraites. C'est à partir du milieu des années 1970 que la théorie des types abstraits algébriques est appai·ue pour déc1ire les structures de données. Définis en termes de signature, les types abstraits doivent d'une part, garantir leur indépendance vis-à-vis de toute mise en œuvre particulière, et d'autre part, offrir un support de preuve de la validité de leurs opérations. Dans ce chapitre, nous verrons comment spécifier une structure de données à l'aide d'un type abstrait, et comment l'implémenter dans un langage de programmation particulier comme JAVA. Dans les chapitres suivants, nous présenterons plusieurs structures de données fondamentales, que tout informaticien doit connaître. Il s'agit des structures linéaires, de la structure de graphe, des structures arborescentes et des tables.

    196

    Chapitre

    1 7. 1

    17



    Structures de données

    DÉFINITION D'UN TYPE ABSTRAIT

    Un type abstrait est décrit par sa signature qui comprend : - une déclaration des ensembles définis et utilisés ; - une description fonctionnelle des opérations ; - une description axiomatique de la sémantique des opérations. Dans ce qui suit, nous définirons (partiellement) les types abstraits EntierNaturel et En­ semble. Le premier décrit l'ensemble N des entiers naturels et le second des ensembles d'élé­ ments quelconques. Déclaration des ensembles

    Cette déclaration indique le nom du type abstrait à définir, ainsi que certaines constantes qui jouent un rôle particulier. La notation :

    EntierNaturel. 0 E EntierNaturel déclare le type abstrait EntierNaturel des entiers naturels, qui possède un élément particulier dénoté O. La déclaration ensembliste de ce11ains types abstraits nécessite de mentionner d'autres types abstraits. Ces derniers sont introduits par le mot-clé utilise. Le type abstrait Ensemble utilise deux autres types abstraits, booléen et E. Il possède la définition suivante :

    Ensemble utilise E, booléen. 0 E Ensemble

    'O 0 c ::J 0 V T"l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    où E définit les éléments d'un ensemble, et 0 un ensemble vide. Remarquez que les types abstraits utilisés n'ont pas nécessairement besoin d'être au préalable entièrement définis. Pour la spécification du type abstrait Ensemble de ce chapitre, seule la connaissance des deux éléments vrai et faux de l'ensemble des booléens nous est utile. Description fonctionnelle

    La définition fonctionnelle présente les signatures des opérations du type abstrait. Pour chaque opération, sa signature indique son nom et ses ensembles de départ et d'arrivée. L'en­ semble des entiers naturels peut être décrit à l'aide de l'opération suce qui, pour un entier naturel, fournit son successeur. Il est également possible de définir les opérations arithmé­ tiques + et x . suce

    + X

    EJntierNaturel -t EJntierNaturel EJntierNaturel x EJntierNaturel -t EJntierNaturel EJntierNaturel x EJntierNaturel -t EJntierNaturel

    Si on munit le type abstrait Ensemble des opérations est-vide ?, E, ajouter et union, le type abstrait contiendra les signatures suivantes :

    17. 1

    Définition d'un type abstrait

    t-vide?

    es

    E

    ajouter enleve r umon

    197

    Ensemble Ensemble Ensemble Ensemble Ensemble

    ---7 ---7

    x

    E E E

    x

    Ensemble

    ---7

    x x

    ---7 ---7

    booléen booléen Ensemble Ensemble Ensemble

    Description axiomatique

    La définition axiomatique décrit la sémantique des opérations du type abstrait. Il est clair que les définitions ensembliste et fonctionnelle précédentes ne suffisent pas à exprimer ce qu'est le type EntierNaturel ou le type Ensemble. Le choix des noms des opérations nous éclaire, mais il existe par exemple une infinité de fonctions de N dans N, et pour l'instant, rien ne distingue réellement la sémantique des opérations + et x . Il faut donc spécifier de façon formelle les propriétés des opérations du type abstrait, ainsi que leur domaine de définition lorsqu'elles correspondent à des fonctions partielles, comme enlever. Pour cela, on utilise des axiomes qui mettent en jeu les ensembles et les opérations. Pour le type EntierNaturel, nous pouvons utiliser les axiomes proposés par G. PEANO 1 au siècle dernier. ( 1 ) Vx E EntierNaturel, 3 x', succ(x)

    = x'

    (2) Vx, x' E EntierNaturel, x =/= x' => succ(x) =/= succ(x') (3) � x E EntierNaturel, succ(x) = 0 (4) Vx E EntierNaturel, x + 0 = x (5) Vx, y E EntierNaturel, x + succ(y) = succ(x + y) (6) Vx E EntierNat11,rel, x x 0 = 0 (7) Vx, y E EntierNaturel, x x succ(y) = x + x x y

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    Le premier axiome indique que tout entier naturel possède un successeur. Le second, que deux entiers naturels distincts possèdent deux successeurs distincts. Le troisième axiome pré­ cise que 0 n'est le successeur d'aucun entier naturel. Enfin, les quatre derniers spécifient les opérations + et x . Grâce à ces axiomes, il est démontré que tous les théorèmes de l'arithmé­ tique de G. PEANO sont vrais pour les entiers naturels. Les axiomes suivants décrivent les opérations du type abstrait Ensemble : ( 1 ) est-vide?(0) = vrai

    (2) Vx E E, Ve E Ensemble, est-vide?(ajouter(e, x)) = faux (3) Vx E E, x E 0 = faux (4) Vx, y E E, Ve E Ensemble, x = y => y E ajouter(e, x) = vrai x =!= y => y E ajouter ( e , x) = y E e (5) Vx, y E E, Ve E Ensemble, x = y => y E enlever(e,x) = faux x =/= y => y E enlever(e,x) = y E e 1 . GIUSEPPE PEANO, mathématicien italien ( 1 858-1932).

    198

    Chapitre

    E E, Ve E Ensemble, x tJ. e ::::} � e' E Ensemble, e'

    17



    Structures de données

    (6) \fx

    =

    enlever( e, x) (7) x E union( e, e') ::::} x E e ou x E e' Notez que l'axiome (5) impose la présence dans l'ensemble de l'élément à retirer. La fonction enlever est une fonction partielle, et cet axiome en précise le domaine de définition. Une des principales difficultés de la définition axiomatique est de s'assurer, d'une part, de sa consistance, c'est-à-dire qu'il n'y a pas d' axiomes qui se contredisent, et d'autre part, de sa complétude, c'est-à-dire que les axiomes définissent entièrement le type abstrait. [FGS90] distingue deux types d'opérations : les opérations internes, qui rendent un résultat de l'en­ semble défini, et les observateurs, qui rendent un résultat de l'ensemble prédéfini, et propose de tester la complétude d'un type abstrait en vérifiant si l'on peut déduire de ses axiomes le résultat de chaque observateur sur son domaine de définition. Pour garantir la consistance, il suffit alors de s'assurer que chacune de ces valeurs est unique.

    1 7.2

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    L'IMPLÉMENTATION D'UN TYPE ABSTRAIT

    L'implémentation est la façon dont le type abstrait est programmé dans un langage particu­ lier. Il est évident que l'implémentation doit respecter la définition formelle du type abstrait pour être valide. Certains langages de programmation, comme ALPHARD ou EIFFEL, incluent des outils qui permettent de spécifier et de vérifier automatiquement les axiomes, c'est-à-dire de contrôler si les opérations du type abstrait respectent, au cours de leur utilisation, ses pro­ priétés algébriques. L'implémentation consiste donc à choisir les structures de données concrètes, c'est-à-dire des types du langage d'écriture pour représenter les ensembles définis par le type abstrait, et de rédiger le corps des différentes fonctions qui manipuleront ces types. D'une façon géné­ rale, les opérations des types abstraits correspondent à des routines de petite taille qui seront donc faciles à mettre au point et à maintenir. Pour un type abstrait donné, plusieurs implémentations possibles peuvent être dévelop­ pées. Le choix d'implémentation du type abstrait variera selon l'utilisation qui en est faite et aura une influence sur la complexité des opérations. Le concept de classe des langages à objets facilite la programmation des types abstraits dans la mesure où chaque objet porte ses propres données et les opérations qui les mani­ pulent. Notez toutefois que les opérations d'un type abstrait sont associées à l'ensemble, alors qu'elles le sont à l'objet dans le modèle de programmation à objets. La majorité des langages à objets permet même de conserver la distinction entre la définition abstraite du type et son implémentation grâce aux notions de classe abstraite ou d' inteJjace. En JAVA, ]'interface suivante représente la définition fonctionnelle du type abstrait Entier Naturel : public interface EntierNaturel

    {

    public EntierNaturel suce { ) ; public EntierNaturel plus ( EntierNaturel n ) ; public EntierNaturel mult ( EntierNaturel n ) ;

    17.2

    L'implémentation d'un type abstrait

    199

    Dans la mesure où le langage JAVA n' autorise pas la surcharge des opérateurs, les symboles + et * n'ont pu être utilisés et les opérations d'addition et de multiplication ont été nommées. La définition fonctionnelle du type abstrait Ensemble correspondra à la déclaration de l'interface générique suivante : public interface Ensemble public boolean estVide ( } ; public boolean dans ( E x ) ; public void a j outer (E x ) ; public void enleve r ( E x)

    throws ExceptionÉ lémentAbsent ;

    public Ensemble union (Ensemble x ) ;

    Notez que la méthode enlever émet une exception si l'élément x à retirer n'est pas pré­ sent dans l'ensemble. L'exception traduit l'axiome (5) du type abstrait. D'une façon générale, les exceptions serviront à la définition des fonctions partielles des types abstraits. La définition du type abstrait Ensemble n'impose aucune restriction sur la nature des élé­ ments des ensembles. Ceux-ci peuvent être différents ou semblables. Les opérations d'ap­ partenance ou d'union doivent s'appliquer aussi bien à des ensembles d'entiers qu'à des en­ sembles de Rectangle, ou encore des ensembles d'ensembles de chaînes de caractères. L'implémentation du type abstrait doit être alors générique, c'est-à-dire qu'elle doit per­ mettre de manipuler des éléments de n'importe quel type. Souvent, il est même souhaitable d' imposer que tous les éléments soient d'un type donné. De nombreux langages de program­ mation (ADA, C++, EIFFEL, etc.) incluent dans leur définition la notion de généricité et proposent des mécanismes de construction de types génériques. Depuis sa version 5.0, JAVA inclut la généricité. JAVA offre la définition de types géné­ riques auxquels on passe en paramètre le type désiré. On ne va pas décrire ici tous les détails de cette notion du langage JAVA. Le lecteur intéressé pouITa se reporter à [GR l l ] . lei, la déclaration de l'interface Ensemble est paramétrée sur le type des éléments de l'ensemble. 'O 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    La mise en œuvre des opérations dépendra des types de données choisis pour implémen­ ter le type abstrait. Pour un ensemble, il est possible de choisir un arbre ou une liste, eux­ mêmes implémentés à l'aide de tableau ou d'éléments chaînés. L'implémentation de l'inter­ face Ensemble aura par exemple la forme suivante : public class EnsembleListe implements Ensemble Liste l ; public EnsembleL i s t e ( )

    Il Le constructeur

    public boolean estVide ( )

    {

    public boolean dans ( E x)

    {

    200

    Chapitre

    public void a j outer ( E x )

    17



    Structures de données

    {

    public void enlever (E x ) throws ExceptionÉ lémentAbsent

    Il fin classe EnsembleListe

    Dans les déclarations de l'interface Ensemble et de la classe EnsembleListe, le nom E est une sorte de paramètre formel qui permet de paramètrer l'interface et la classe sur un type donné. Le compilateur pourra ainsi contrôler que tous les éléments d'un ensemble sont de même type. Notez que dans la partie implémentation d'un type abstrait, le programmeur devra bien prendre soin d'interdire l'accès aux. données concrètes, et de rendre publiques les opérations du type abstrait.

    1 7.3

    UTILISATION DU TYPE ABSTRAIT

    Puisque la définition d'un type abstrait est indépendante de toute implémentation par­ ticulière, l' utilisation du type abstrait devra se faire exclusivement par l'intermédiaire des opérations qui lui sont associées et en aucun cas en tenant compte de son implémentation. D' ailleurs, certains langages de progranunation peuvent vous l'imposer, mais ce n'est mal­ heureusement pas le cas de tous les langages de programmation et c'est alors au programmeur de faire preuve de rigueur !

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    Les en-têtes des fonctions et des procédures du type abstrait et les affirmations qui dé­ finissent leur rôle représentent l'interface entre l'utilisateur et le type abstrait. Ceci permet évidemment de manipuler le type abstrait sans même que son implémentation soit définje, mais aussi de rendre son utilisation indépendante vis-à-vis de tout changement d'implémen­ tation. Des déclarations de vaiiables de type E n s emble pourront s'écrire en JAVA comme suit : Ensemble el = new EnsembleListe ( ) ; Ensemble e 2

    =

    new EnsembleListe ( ) ;

    Ensemble e3

    =

    new EnsembleListe ( ) ; Ensemble e4

    =

    new EnsembleListe ( ) ;

    Dans ces déclarations, el est un ensemble d' Integer, e2 un ensemble de Rectangle, et e3 un ensemble d'ensembles de String. En revanche, pour la dernière déclaration, il n'y a pas de contrainte sur le type des éléments de l'ensemble e 4 et ces éléments pounont donc être de type quelconque. L'utilisation du type abstrait se fera exclusivement par l'intermédiaire des méthodes défi­ nies dans son interface. Par exemple, les ajouts suivants seront valides quelle que soit l'im­ plémentation choisie pour le type En semble.

    17.3

    Utilisation du type abstrait

    201

    el . a jouter ( l 2 3 ) ; e 2 . a jouter (new Rectangle ( 2 , 5 ) ) ; e3 . a j outer ( new EnsembleLi ste ( ) ) ; e 4 . a jouter ( 1 2 3 . 4 5 ) ;

    L'exemple suivant montre l'éc1iture d'une méthode générique qui permet de manipuler le type générique des éléménts d'un Ensemble. void uneMéthode (Ensemble e)

    {

    E x; if

    ( e . dans ( x ) )

    Il x E e

    Enfin pour conclure, on peut ajouter que si le type abstrait est juste et validé, il y a plus de chances que son utilisation, exclusivement à l'aide de ses fonctions, soit elle aussi juste.

    -0 0 c ::J 0 V T"'l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    -0 0 c ::J 0 «:!" T""l 0 N @ ...... ..c Ol ï:: >0.. 0 u

    Cha pitre 1 8

    Structures linéaires

    Les structures linéaires sont un des modèles de données les plus élémentaires et utilisés dans les programmes info1matiques. Elles organisent les données sous forme de séquence non ordonnée d'éléments accessibles de façon séquentielle. Tout élément d'une séquence, sauf le dernier, possède un successeur. Une séquence s constituée de n éléments sera dénotée comme suit :

    et la séquence vide : -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    s =



    Les opérations d'ajout et de suppression d'éléments sont les opérations de base des struc­ tures linéaires. Selon la façon dont procèdent ces opérations, nous distinguerons différentes sortes de structures linéaires. Les listes autorisent des ajouts et des suppressions d'éléments n'importe où dans la séquence, alors que les piles, les files et les dèques ne les permettent qu'aux extrémités. On considère que les piles, les files et les dèques sont des fom1es particu­ lières de liste linéaire. Dans ce chapitre, nous commencerons par présenter la forme générale, puis nous étudierons les trois formes particulières de liste.

    1 8. 1

    LES LISTES

    La liste définit une forme générale de séquence. Une liste est une séquence finie d'éléments repérés selon leur rang. S'il n'y a pas de relation d'ordre sur l'ensemble des éléments de la séquence, il en existe une sur le rang. Le rang du premier élément est 1 , le rang du second

    204

    Chapitre 1 8



    Structures linéaires

    est 2, et ainsi de suite. L'ajout et la suppression d'un élément peut se faire à n'importe quel rang valide de la liste.

    18.1.1

    Définition abstraite

    Ensembles

    .Ciste est 1 'ensemble des listes linéaires non ordonnées dont les éléments appaitiennent à un ensemble E quelconque. L'ensemble des entiers représente le rang des éléments. La constante listevide est la liste vide . .Ciste utilise E, naturel et entier listevide E .Ciste Description fonctionnelle

    Le type abstrait .Ciste définit les quatre opérations de base suivantes :

    longueur ième suppnmer ajouter

    .Ciste .Ciste .Ciste .Ciste

    x x x

    ---t

    entier entier entier

    x

    E

    nat ur el

    ---t

    E

    ---t

    .Ciste .Ciste

    ---t

    L'opération longueur renvoie le nombre d'éléments de la liste. L'opération ième retourne l'élément d'un rang donné. Enfin, supprimer (resp. ajouter) supprime (resp. ajoute) un élé­ ment à un rang donné. Description axiomatique

    Les axiomes suivants déclivent les quatre opérations applicables sur les listes. La longueur d'une liste vide est égale à zéro. L'ajout d'un élément dans la liste augmente sa longueur de un, et sa suppression la réduit de un. 'O 0 c ::J 0 V T"l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    Vl E .Ciste, et Ve E E (1) longueur(listevide) = 0 (2) Vr, 1 ( r ( longueur(l), longueur(supprimer(l, r)) = longueur(l) - 1 (3) Vr, 1 ( r ( longueur(l) + 1, longueur(ajouter(l, r, e)) = longueur(l) + 1 L'opération ième renvoie l'élément de rang r, et n'est définie que si le rang est valide. (4)

    Vr, r < l et r > longueur(l), � e, e = ième(l , r)

    L'opération supprimer retire un élément qui appartient à la liste, c'est-à-dire dont le rang est compris entre un et la longueur de la liste. Les axiomes suivants indiquent que le rang des éléments à droite de l' élément supprimé est décrémenté de un. (5) Vr, 1 ( r ( longueur(l ) et l ( i < r ième(supprimer(l, r) , i) (6) Vr, 1 ( r ( longueur(l) et r ( i ( longueur(l) - 1, ième(supprimer(l, r), i) = ième(l, i + 1)

    =

    ième(l,i)

    18. 1

    (7)

    Les listes

    205

    Vr, r < 1 et r > longueur ( l), � l', l'

    =

    supprimer( l, r)

    L'opération ajouter insère un élément à un rang compris entre un et la longueur de la liste plus un. Le rang des éléments à la droite du rang d'insertion est incrémenté de un. (8) Vr, 1 :::;; r :::;; long ueur (l) + 1 et 1 :::;; i < r, ième(ajouter(l, r, e ) , i) = ième(l, i)

    Vr, 1 :::;; r :::;; longu eur (l) + l et r = i, ième(ajouter(l, r, e), i ) = e (10) Vr, 1 :::;; r :::;; longu eur (l) + 1 et r < i :::;; lon gueur ( l ) + 1, ième(ajouter(l, r, e ) , i) = ième(l, i 1) (11) Vr, r < l et r > longueur ( l) + 1 , � l', l' = aj outer (l , r, e) (9)

    -

    18.1.2

    L'implémentation en Java

    La description fonctionnelle du type abstrait .liste est traduite en JAVA par l'inte1face générique suivante : public interface Liste

    {

    public int l on gu eu r ( ) ; public E ième ( int r ) throws RanginvalideException; public void suppr ime r ( int r ) throws RanginvalideException;

    public void a jou ter ( int r,

    E e)

    throws RanginvalideExcept i o n ;

    Les méthodes d'accès aux éléments de la liste peuvent lever une exception si elles tentent d'accéder à un rang invalide. Cette exception, RanginvalideException, est simplement définie par la déclaration : -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    public class RanginvalideException extends RuntimeException { public RanginvalideException ( ) super ( ) ;

    Afin d'insister sur l'indépendance du type abstrait vis-à-vis de son implémentation, nous présenterons successivement deux sortes d'implémentation. La première utilise des tableaux et la seconde des structures chaînées. Les tableaux offrent une représentation contiguë des éléments, et permettent un accès direct aux éléments qui les composent, mais ont comme principal inconvénient de fixer la taille de Ja structure de données. Par exemple, si pour repré­ senter une liste, on déclare un tableau de cent composants alors quelle que soit la longueur ef­ fective de la liste, l'encombrement mémoire utilisé sera celui des cent éléments. Au contraire, les structures chaînées sont des structures dynamiques qui permettent d'adapter la taille de la structure de données au nombre effectif d'éléments. L'espace mémoire nécessaire pour mé­ moriser un élément est plus important, et le temps d'accès aux éléments est en général plus coûteux parce qu'il a lieu de façon indirecte.

    206

    Chapitre 1 8



    Structures linéaires

    Utilisation d'un tableau

    La méthode qui vient en premier à l'esprit, lorsqu'on mémorise les éléments d'une liste dans un tableau, est de conserver systématiquement le premier élément à la première place du tableau, et de ne faire varier qu'un indice de fin de liste. La figure 1 8 . 1 montre la séquence de cinq entiers < 5, -13, 23, 182, 100 > placée dans un tableau nommé élément s . L'attribut lg, qui indique la longueur de la liste, donne également l'indice de fin de liste. 0

    éléments

    5

    2 1-131 23

    38 00 1 21 1 4

    élément s . length-1

    1 1

    lg=S

    FIGURE 18.1 Une liste dans un tableau. L'algorithme de l'opération ième est très simple, puisque le tableau permet un accès direct à l'élément de rang r. La complexité de cet algmithme est donc 0(1). Notez que pour accéder à un élément de la liste l ' antécédent de l'opération doit être vérifié. Algorithme ième ( r )

    {Antécédent

    1

    � r � longueur (l ) J rendre élément s [ r - 1 ] {les valeurs débutent :

    à

    l ' indice

    0)

    L'opération de suppression d'un élément de la liste provoque un décalage des éléments qui se situent à droite du rang de suppression. Pour une liste de n éléments, la complexité de cette opération est O(n), et l'algorithme qui la décrit est le suivant : Algorithme supprimer ( r )

    {Antécédent

    :

    1 � r � longueur (l) }

    pourtout i de r à lg faire élément s [ i - 1 ]

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    f--

    élément s [ i ]

    finpour lg f--

    lg-1

    L'opération d'ajout d'un élément e au rang r consiste à décaler d'une position vers la droite tous les éléments à partir du rang r. Le nouvel élément est inséré au rang r. Dans la plupart des langages de programmation, une déclaration de tableau fixe sa taille à la compi­ lation. Quel que soit le constructeur choisi, un objet, instance de la classe L i steTableau, possédera un nombre d'éléments fixe. Ceci contraint la méthode a j outer à vérifier si le ta­ bleau éléments dispose d'une place libre avant d'ajouter un nouvel élément. Comme pour ] 'opération de suppression, la complexité de cet algorithme est O(n). L'algorithme est le suivant : Algorithme

    a j outer ( r , e ) {Antécédent : 1 � r � longueur (l) +l et longueur (l ) +l ­ a. 0 u

    : --queue;

    Notez que l'indice de tête ne précède pas nécessairement l'indice de queue, et qu'au gré des ajouts et des suppressions l'indice de tête peut être aussi bien inférieur que supé­ rieur à l 'indice de queue. La figure 1 8.2 donne deux dispositions possibles de la séquence < 5 20 4 9 45 > dans le tableau éléments.

    éléments





    t êt e

    5

    queue

    20

    4

    9

    45

    queue

    tête

    FIGURE 18.2 Gestion circu laire d'un tableau.

    Dans le cas général de la suppression ou de l'ajout d'un élément qui n'est pas situé à l'une des extrémités de la liste, le décalage d'une partie des éléments est nécessaire comme dans

    18. 1

    Les listes

    209

    la méthode de gestion du tableau précédente. Ce décalage est lui-même circulaire et se fait modulo la taille du tableau. L'indice d'un élément de rang r est égal à tête+r-1. Nous définissons la classe générique ListeTableauCirculaire en remplaçant les méthodes ième, s upprimer et a j outer par celles données ci-dessous : public E ième ( int r ) throws RanginvalideException { if

    ( r lg )

    throw new RanginvalideExcep t i on ( ) ;

    return é l ément s [ ( tê t e +r- 1 )

    public void supprimer ( int r )

    % é lément s . length ] ;

    throws RanginvalideException

    { if

    ( r lg )

    throw new RanginvalideExcept i on ( ) ;

    Il supprimer l e dernier él ément =

    que ue== O ? é lément s . length-1

    :

    --que ue ;

    el se

    Il supprimer le premier é l ément

    i f (r==l) tête

    =

    t ê te ==éléments . len gt h- 1 ? 0

    ++tête;

    :

    { Il décaler les él ément s

    else

    for

    ( int i=tête+r;

    i O élément s [ ( i - 1 )

    % élément s . length] élément s [ i % é l ément s . lengt h ] ;

    queue

    queue==O

    ? élément s . length-1

    :

    --queue;

    lg--;

    public void a j out er ( int r ,

    E e)

    throws RanginvalideException

    {

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    if

    ( l g==élément s . length) throw new ListePle ineExcept i on ( ) ;

    if

    ( r lg + l )

    throw new RanginvalideExcept i on ( ) ; if

    ( r ==lg+l )

    {

    Il ajouter en queue

    é lément s [ queue ] =e ;

    queue = queue==éléme nt s . length-1 ? 0

    ++queue;

    el se if

    ( r == l )

    { Il ajouter en t ê t e

    tête = tête==O ? élément s . length-1

    --tête;

    é lément s [ tête ] =e ; else

    { Il décaler les éléments for

    ( int i=lg+tête;

    i >= r+ t êt e ;

    i--)

    Il i > o élément s [ i % élément s . length ] = é lémen t s [ ( i - 1 )

    % élément s . lengt h ] ;

    Il tête+r-1 est l ' indice d ' insertion

    210

    Chapitre 1 8



    Structures linéaires

    é lément s [ tête+r- l ] =e ;

    queue = queue==élément s . length-1 ? 0

    ++queue ;

    lg++;

    Utilisation d'une structure chaînée

    Une structure chaînée est une structure dynamique formée de nœuds reliés par des liens. Les figures 18.3 et 18.4 montrent les deux types de structures chaînées que nous utiliserons pour représenter une liste. Dans cette figure, les nœuds sont représentés par des boîtes rec­ tangulaires, et les liens par des flèches. tête

    tête

    FIGURE 18.3 Liste avec chaînage simple. tête

    queue

    99

    queue

    tête

    /77/77

    9m

    ����. ----tl__:ïj 9

    FIGURE 18.4 Liste avec chaînage double.

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    Avec la première organisation, chaque élément est relié à son successeur par un simple lien et l ' accès se fait de la gauche vers la droite à partir de la tête de liste qui est une référence sur le premier nœud. Si sa valeur est égale à null, la liste est considérée comme vide. Dans la seconde organisation, chaque élément est relié à son prédécesseur et à son succes­ seur, permettant un parcours de la liste dans ]es deux sens depuis Ja tête ou la queue. La tête est une référence sur le premier nœud et la queue sur le dernier. La liste est vide lorsque la tête et la queue sont toutes deux égales à la valeur null. Nous représenterons un lien par la dasse générique L i e n qui définit simplement un attri­ but suivant de type Lien pour désigner le nœud suivant. Cette classe possède une méthode qui renvoie la valeur de cet attribut et une autre qui la modifie. Le constructeur par défaut ini­ tialise l ' atttibut suivant à ]a valeur null. public class Lien { protected Lien suivant; protected Lien suivant ( }

    { return suivant;

    protected void suivant ( Lien s )

    Il fin classe Lien

    {

    suivant=s;

    } }

    18. 1

    211

    Les listes

    Les nœuds sont représentés par la classe générique Noeud qui hérite de l a classe Lien et l'étend par l'ajout de l'attribut valeur qui désigne la valeur du nœud, i.e. la valeur d'un élément de la séquence. Cette classe possède deux constructeurs qui initialisent un nœud avec la valeur d'un élé­ ment particulier, et avec celJe d'un lien sur un autre nœud. La méthode valeur renvoie la valeur du nœud, alors que la méthode changerValeur change sa valeur. La méthode noeudSui vant renvoie ou modifie le nœud suivant. public class Noeud extends Lien protected E valeur; public Noeud(E e)

    { valeur=e;

    }

    { valeur=e; suivant ( s ) ;

    public Noeud ( E e , Noeud s ) public E valeur ( )

    { return valeur;

    }

    public void changerValeur ( E e )

    { valeur=e;

    public Noeud noeudSuivant ( )

    { return

    public void noeudSuivant ( Noeud s )

    }

    (Noeud)

    suivant ( ) ;

    { suivant ( s ) ;

    Avec cette structure chaînée, les opérations ième, supprimer, et a j outer nécessitent toutes un parcours séquentiel de la liste et possèdent donc une complexité égale à O(n). On atteint le nœud de rang r en appliquant r-1 fois l'opération noeudSuivant à partir de la tête de liste. Nous noterons ce nœud noeudSui vantr - l (tête ) . L'algotithme de l'opération ième s'écrit : Algorithme ième ( r )

    {Antécédent : 1 � r � longueur ( l ) }

    l rendre noeudSuivant r - ( t ê t e ) . valeur ( )

    Comme le montre la figure 18.5, la suppression d'un élément de rang r consiste à affecter au lien qui le désigne la valeur de son lien suivant. La flèche en pointillé représente le lien avant la suppression. Notez que si r est égal à un, il faut modifier la tête de liste.

    �l�ivant suivant noeud de rang r

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    FIGURE 18.S Suppression du nœud de rang r. Algorithme supprimer ( r )

    {Antécédent : 1 � r � longueur ( l ) } si r=l alors tête +-- noeudSuivant (têt e ) sinon

    2 noeudSuivant r - (têt e ) . suivant +--noeudSuivant r ( t êt e )

    finsi lg

    +--

    lg-1

    Chapitre 1 8

    212



    Structures linéaires

    L'ajout d'un élément e au rang r consiste à créer un nouveau nœud n initialisé à la valeur e , puis à relier le nœud de rang r-1 à n, et enfin à relier le nœud n au nœud de rang r. Si l ' élément est ajouté en queue de liste, son suivant est la valeur null. Comme précédemment, si r = l , il faut modifier la tête de liste. noeud de rang r

    � =4---�..._____.__

    suivant

    n

    FIGURE 18.6 Ajout d'un nœud au rang r. Algorithme a j outer ( r ,

    e)

    {Antécédent : 1 � r � longueur ( l ) + l } n +-- créer Noeud ( e ) 2 noeudSuivant r - ( t ê t e ) . s uivant +--n l n . suivant +-- noeudSuivant r- ( t ê t e ) l g +-- lg+ 1

    Nous pouvons donner l'écriture complète de la classe ListeChaînée. public class List eChaînée implements List e { protected int lg; protected Noeud t ê t e ; public int l ongueur ( )

    { return lg ;

    }

    public E ième ( int r ) throws RanginvalideException if

    'O 0 c ::J 0 V T""l 0 N @ ..... .c Ol ï::: >­ a. 0 u

    ( r< l

    1 1

    r>lg)

    throw new RanginvalideException ( ) ; Noeud n=tê t e ; for

    ( int i = l ; i < r ;

    i + + ) n=n . noeudSuivant ( ) ;

    Il n désigne l e nœud de rang r return n . valeur ( ) ; public void supprimer ( int r ) if

    ( r< l

    1 1

    throws Ranginva l i deExcept ion

    r>lg)

    throw new RanginvalideException ( ) ; if

    ( r==l)

    Il s uppression en t ê t e de l i s t e

    tête=tête . noeudSuivant ( ) ; else { Il cas général , r>l Noeud p=null, for

    ( int i = l ;

    q=t ête;

    i­ a. 0 u

    Le double chaînage est défini par la classe générique LienDouble qui étend la classe Lien en lui ajoutant un second lien, l'attribut précédent, dont la définition est semblable à l'attribut suivant. La déclaration de cette classe est la suivante : public class LienDouble extends Lien protected Lien précé de nt ; protected Lien précédent ( )

    { return précédent ;

    protected void précédent (Lien s )

    { précédent=s;

    }

    Il fin classe LienDouble

    Chaque nœud d'une structure doublement chaînée est représenté par la classe générique Noeud2 suivante qui étend la classe LienDouble. public class Noeud2 extends LienDouble protected E valeur;

    { valeur=e; } Noeud2 (E e, Noeud2 p , Noeud2 s )

    public Noeud2 ( E e) public

    valeur=e; précédent ( p ) ; public E valeur ( )

    {

    suivant ( s ) ;

    { return valeur;

    public void changerValeur ( E e ) public Noeud2 noeudSuivant ( )

    }

    { valeur=e; { return

    (Noeud2 ) suivant ( ) ;

    }

    Chapitre 18

    214

    public void noeudSuivant (Noeud2 s ) public Noeud2 noeudPrécédent ( )

    {

    suivant ( s ) ;

    { return

    public void noeudPrécédent (Noeud2 s )



    Structures linéaires

    }

    (Noeud2 < E > )

    précédent ( ) ;

    { précédent ( s ) ;

    }

    Une liste doublement chaînée possède une tête qui désigne le premier élément de la liste, et une queue qui indique le dernier élément. L'opération ième est identique à celle qui utilise une liste simplement chaînée. Sa com­ plexité est O(n). La suppression d'un élément de rang r nécessite de mettre à jour le lien précédent du nœud de rang r + l s'il existe (voir la figure 18.7). La complexité est O(n). Notez que la suppression du dernier élément de la liste est une simple mise à jour de l'attribut queue et sa complexité est 0(1). suivant

    noeud de rang r

    pr

    suivant

    é cé dent

    FIGURE 18.7 Suppression d u nœud de rang r. L'ajout d'un élément est semblable à celui dans une liste simplement chaînée. Mais là aussi, il faut mettre à jour le lien précédent comme le montre la figure 18.8. noeud de rang r

    Of\

    -0 0 c ::J 0 V T""l 0 N

    précédent

    @

    ..... ..c Ol ï::: >­ a. 0 u

    suivant

    �-

    '-+---'----+ suivant

    n

    FIGURE 18.8 Ajout du nœud de rang r. La classe suivante donne l'écriture complète de l'implémentation d'une liste avec une structure doublement chaînée. public class List eChaînéeDouble implements Liste { protected int l g ; protected Noeud2 < E > tête, public int longueu r ( ) public E ième (int r ) if

    ( r l g ) throw new Ranginval i deExcept i on ( ) ;

    18. 1

    215

    Les listes

    Noeud2 n=têt e ; for

    ( int i=l ;

    i< r; i++)

    n=n. noeudSuivant ( ) ;

    Il n désigne l e noeud2 de rang r return n . valeur ( ) ; public void supprimer (int r ) if

    ( r< l

    1 1

    if

    ( lg==l)

    11

    throws RanginvalideException

    throw new RanginvalideExcept ion ( ) ;

    r>lg)

    un seul élément =? r=l

    t ê t e=queue=null; else

    Il a u moins 2 éléments if (r==l) Il suppression en tête de l i s t e tête=têt e . noeudSuivant ( ) ;

    el se if

    ( r==lg)

    Il suppression du dernier élément de la l i s t e queue=queue . noeudPrécédent ( ) ; queue . noeudSuivant (null ) ;

    { Il cas général, r > 1 et r < l g

    else

    Noeud2 < E > q=tête, p=null ; for

    ( int i=l ;

    i­ a. 0 u

    Une des propriétés fondamentales des structures linéaires est que chaque élément, hom1is le dernier, possède un successeur. Il est alors possible d'énumérer tous les éléments d'une liste grâce à une fonction suce dont la signature est définie comme suit :

    suce

    E

    -t

    E

    Pour une liste l, cette fonction est définie par l'axiome suivant :

    Vr E [1, longu eur ( l ) [, succ(ième(l, r))

    =

    ième ( l , r +

    1)

    Notez que la liste n'est pas le seul type abstrait dont on peut énumérer les composants. Nous veITons que nous pomTons énumérer les éléments des types abstraits que nous étudie­ rons par la suite en fonction de Jeurs particularités. L'implémentation en JAVA

    Nous représenterons une énumération par l'interface JAVA Iterator du paquetage java . ut i l . Cette interface possède des méthodes de manipulation de l 'énumération.

    18. 1

    217

    Les listes

    E n particulier, l a méthode next qui renvoie l'élément suivant de l'énumération (ou l'ex­ ception NoSuchElementException si cet élément n'existe pas) et la méthode hasNext indique si la fin de l'énumération a été atteinte ou pas. L'inte1face définit aussi la méthode remove, mais nous ne la traiterons pas. Une liste définira la méthode i terator qui renvoie l'énumération de ses éléments. Cette méthode s'écrit de la façon suivante : public Iterator iterator ( ) return new Liste É numé ration ( } ;

    La programmation de la classe Li steÉnumération dépend de l'implémentation de la liste. Chaque classe qui implémente le type abstrait Liste définira une classe privée lo­ cale ListeÉnumération qui donne une implantation particulière de l'énumération. Par exemple dans la classe ListeTableau, on définira : private class ListeÉ numération implements Iterator private int courant; private Liste É numérat ion ( ) courant=O ; public boolean hasNext ( ) return courant ! =l g ; public E next ( ) if

    throws NoSuchElementException

    ( hasNext ( ) ) return élément s [ courant ++ ] ;

    Il pas de suivant throw new NoSuchElementExcept i on ( ) ;

    } Il List eÉnuméra t i on

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    L'attribut courant désigne l'indice du prochain élément de l'énumération dans le tableau. Sa valeur initiale est égale à zéro. Pour la classe Li steTableauCirculaire, le calcul de l'élément suivant se fait modulo la longueur du tableau. private class ListeÉ numération implements Iterator { private int courant, nbÉ nurn; private Liste É numération ( ) courant = t ê t e ; nbÉ num = O ; public boolean hasNext ( ) return nbÉ num ! = l g ; public E next ( ) if

    throws NoSuchElementException

    ( hasNext ( ) ) E e

    =

    {

    élérnent s [ courant ] ;

    courant = courant == élément s . length - 1 ? 0

    ++courant ;

    Chapitre 1 8

    218



    Structures linéaires

    nbÉ num++; return e ; }

    Il pas de suivant throw new NoSuchElementExcept i on ( ) ; }

    Il ListeÉnumération

    L'attribut nbÉnum est nécessaire pour identifier la fin de la liste, car si les indices de tête et de queue sont égaux, la liste peut être aussi bien vide que pleine. Pour les classes qui implémentent les listes à l'aide de structures chaînées, il suffit de conserver une référence sur l'élément courant. L'accès au successeur se fait à l'aide de la méthode noeudSui vant. La première fois, sa valeur est égale à la tête de liste. private class ListeÉ numération implements Iterator { private Noeud courant ; private Liste É numérat ion ( ) courant

    =

    tête;

    public boolean hasNext ( ) return courant ! =null; public E next ( ) throws NoSuchEl ementExcept ion if ( hasNext ( ) )

    {

    E e = courant . valeur ( ) ; courant = courant . noeudSuivant ( ) ; return e ;

    } Il pas de s u i vant throw new NoSuchElementExcept i on ( ) ;

    Il ListeÉnumération

    -0 0 c ::J 0 V T""l 0 N @ ..... ..c Ol ï::: >­ a. 0 u

    L'algorithme de parcours donné plus haut s'écrit en JAVA de la façon suivante. On crée d'abord l'énumération, puis on traite les éléments un à un grâce à la fonction next. public void parcours (Liste 1 ) Iterator énum=l . iterator ( ) ; while

    (énum. hasNext ( ) ) t r a i ter ( énum . next ( ) ) ;

    La manipulation d'une énumération s'applique en général à tous ses éléments. Il peut être en particulier très risqué de modifier la structure de données (e.g. la liste) au cours d'un parcours dans la mesure où cela peut corrompre l 'énumération. Une seconde façon de traiter tous les éléments d'une liste est d'utiliser un énoncé adapté, s'il existe dans le langage de programmation. JAVA propose l'énoncéjoreach avec lequel la méthode de parcours précédent se récrit simplement : public void parcours (Liste 1 ) for

    (E x

    :

    1)

    traiter ( x ) ;

    {

    18.2

    Les piles

    219

    Avec la version 8, JAVA propose l'interfacefonctionnelle 1 générique Iterable avec la méthode i terator qui renvoie l'énumération de l'objet courant et la méthode par défaut forEach qui prend en paramètre une fonction anonyme (une lambda) à appliquer à l 'énumé­ ration de l' objet courant. L'interface L i s te, ainsi toutes les autres interfaces qui décrivent les structures de données présentées par la suite dans ce livre, devront implémentées l'interface Iterable : public interface Liste extends Iterable {

    La méthode forEach définie par défaut (mais il est bien évidement possible de la redéfi­ nir) possède la forme suivante : public default void forEach ( Consumer