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