Chapitre 5 Modèles et structures de données
Malgré leur appellation voisine, une "liste" et une
"liste chaînée"
sont des concepts très différents. Une liste est une abstraction
mathématique ou un modèle de données. Une liste chaînée est une
structure de données. En particulier, c'est la structure de données
que nous utilisons habituellement en Pascal et dans d'autres
langages similaires pour représenter les listes abstraites dans les
programmes. Il existe d'autres langages pour lesquels il n'est pas
nécessaire d'utiliser une structure de données pour représenter des
listes abstraites. Par exemple, la liste (a1, a2, ... ,an)
peut être représentée directement dans le langage Lisp, et d'une
façon similaire dans le langage Prolog...
Aho & Ullman [1]
Un modèle de données est une abstraction utilisée pour décrire
des problèmes. Cette abstraction spécifie
les valeurs qui peuvent être attribuées aux objets, et les opérations
valides sur ces objets.
La structure de données, quant à elle, est une construction du
langage de programmation, permettant de représenter un modèle de
données.
Dans ce chapitre,
nous allons illustrer la différence entre ces deux notions en parlant
assez longuement
de la liste (modèle de données), et des structures de données
qui permettent habituellement de la représenter.
Cela nous donnera aussi l'occasion d'introduire la notion
d'interface, telle qu'elle est définie en Java.
Nous verrons aussi plus rapidement quelques autres modèles de données
classiques.
5.1 Les listes
La liste est un modèle de données qui permet de décrire une séquence
d'objets contenant chacun une valeur d'un type donné.
À la base, on peut définir le type L = liste de V à partir d'un
type V donné, comme une suite finie d'éléments de V.
Les opérations basiques que l'on peut définir sur une telle liste sont :
|
vide : |
L |
® |
Bool |
indique que la liste est vide |
tete : |
L |
® |
V |
donne la valeur de la tête de liste |
succ : |
L |
® |
L |
donne la suite de la liste |
adjTete : |
L × V |
® |
L |
ajoute une valeur en
tête de liste |
|
En plus de ces opérations de base, on peut souhaiter définir des
opérations plus complexes -- qui s'expriment en termes de combinaison de
ces opérations de base -- comme l'adjonction ou la suppression à un
endroit quelconque, la recherche de l'existence d'une valeur dans la
liste, voire l'adjonction et la suppression en queue ou le tri...
5.1.1 Représentation par un tableau
Le premier type de représentation auquel on pense est bien entendu le
tableau, que nous avons déjà vu.
Si nous représentons une liste par un tableau T[1 ... n], par
exemple, nous pouvons réaliser les opérations de base comme suit :
|
vide : |
T[1 ... n] |
® |
Bool |
|
tete : |
T[1 ... n>0] |
® |
x Î V |
x = T[1] |
succ : |
T[1 ... n>0] |
® |
U[1 ... n-1] |
" i Î
[1 ... n-1] U[i] = T[i+1] |
adjTete : |
T[1 ... n] × x Î V |
® |
U[1 ... n+1] |
ì
í
î |
|
U[1] = x |
" i Î [2 ... n+1] |
U[i] = T[i-1] |
|
|
|
5.1.2 Représentation par une liste chaînée
La liste chaînée est une structure de données que l'on retrouve
fréquemment en informatique.
Elle nécessite de représenter chaque élément de la liste par un
couplet (val, succ), désignant respectivement la valeur au point
courant et le "pointeur" sur le chaînon suivant.
En Java, cela s'écrit très aisément puisque, comme nous l'avons vu,
toutes les variables déclarées d'un type défini par une classe sont
des références.
Prenons par exemple le cas d'une liste d'entiers.
La classe qui va représenter un "maillon de la chaîne" pourra
s'écrire :
public class ElementListInt {
public int val;
public ElementListInt succ;
// Constructeur
ElementListInt(int x) {
val = x;
succ = null;
}
}
Nous pouvons maintenant définir la classe représentant la liste
chaînée d'entiers.
Vous remarquerez que je n'ai pas mis dans l'interface de cette classe
les opérations tete et succ, que l'on peut effectuer
directement par l'accès à tete.val et tete.succ, ces
variables d'instance de ElementListInt ayant été déclarées
publiques :
public class ListInt {
public ElementListInt tete;
// Constructeur -- liste vide
ListInt() {
tete = null;
}
// Méthodes
public boolean vide() {
return tete == null;
}
public void adjTete(int x) {
// Créer le nouvel élément
ElementListInt nouveau = new ElementListInt(x);
// Le raccorder au reste
nouveau.succ = tete;
tete = nouveau;
}
}
Notez la facilité de "chaînage" pour l'adjonction en tête : on
crée le nouveau maillon, on lui attribue une valeur, et on l'accroche
au reste de la chaîne.
5.1.3 Comparaison entre les deux représentations
On remarquera tout de suite que d'un point de vue pratique,
l'adjonction en tête n'est pas une opération très efficace avec la
représentation tableau, puisqu'elle nécessite un décalage vers la
droite de toutes les valeurs du tableau.
Il en serait de même de l'insertion ou de la suppression en une position
quelconque.
Ces opérations sont bien plus immédiates avec la liste chaînée.
En revanche, celle-ci nécessite plus de place en mémoire, puisque
chaque élément de la liste requiert à la fois un emplacement mémoire
pour la valeur et un autre emplacement pour le chaînage.
Il est très fréquent de devoir ainsi faire des choix entre temps de
calcul et encombrement mémoire.
D'autres facteurs peuvent intervenir, notamment le type et la
fréquence des opérations que l'on veut effectuer.
Si par exemple on a besoin de trier la liste régulièrement, la
représentation par tableau présente l'avantage de permettre l'emploi
d'algorithmes de tri plus efficaces.
Si en revanche les opérations d'adjonction/suppression en un endroit
quelconque sont fréquentes, la représentation par liste chaînée
présente des avantages indéniables.
5.1.4 Application : l'interface
ListeDeComptes
Il est temps de revenir à notre exemple de gestion bancaire.
Nous avions décidé de représenter les comptes d'une agence bancaire
par un tableau d'objets de type CompteBancaire.
Mais à la réflexion, il est probablement dommage de se lier à ce type
de représentation ; nous venons de voir que dans certains cas,
d'autres types de représentation peuvent être plus appropriés.
Pour se détacher de la représentation, tout en garantissant un
ensemble de services, nous allons recourir à un nouveau concept en Java :
l'interface.
De manière informelle, une interface peut être considérée comme une
déclaration de classe -- on parlera en fait plutôt de signature de type
-- sans comportement associé, c'est-à-dire sans code, ni variables
d'instance. L'utilisation d'interfaces permet de spécifier un
comportement -- on parle aussi de contrat -- que les objets d'un type
donné doivent assurer, sans prendre aucune décision sur les structures
de données à mettre en oeuvre pour représenter concrètement cette
interface. On favorise ainsi l'abstraction de données, puisqu'on se
situe au niveau du modèle de données.
Définissons donc l'interface ListeDeComptes, dont nous
attendons le comportement suivant :
-
trouver un compte connaissant le nom du client,
- ajouter un compte,
- supprimer un compte,
- afficher l'état de tous les comptes,
- appliquer un traitement quotidien à tous les comptes.
Le mot clé interface introduit cette
interface, dont la définition se met dans un fichier à part, comme
pour les classes -- ici le fichier ListeDeComptes.java :
// Interface ListeDeComptes - version 1.0
/**
* Interface représentant une liste de comptes et les opérations
* que l'on souhaite effectuer sur cette liste
* @author Karl Tombre
*/
public interface ListeDeComptes {
// Récupérer un compte à partir d'un nom donné
public CompteBancaire trouverCompte(String nom);
// Ajout d'un nouveau compte
public void ajout(CompteBancaire c);
// Suppression d'un compte connaissant le nom du client
public void supprimer(CompteBancaire c);
// Afficher l'état de tous les comptes
public void afficherEtat();
// Traitement quotidien de tous les comptes
public void traitementQuotidien();
}
Vous noterez la syntaxe très proche de celle d'une classe, avec les
deux différences fondamentales suivantes, dues au fait que nous sommes
au niveau du modèle de données et non de sa représentation concrète :
-
nous ne définissons aucune variable,
- nous ne donnons que la signature des méthodes, sans aucune
information sur la manière de les exécuter.
L'intérêt d'une telle interface, c'est qu'elle définit un type valide ;
nous pouvons donc maintenant écrire notre programme de gestion
bancaire en termes plus abstraits, puisque la variable
monAgence peut maintenant être déclarée de type
ListeDeComptes.
Cependant, une interface ne comporte aucune variable d'instance et
aucun constructeur, on ne peut donc pas l'instancier.
Il faut donc bien au moins une classe concrète qui implante
l'interface ainsi spécifiée,
si on veut disposer d'objets de ce type.
Reprenons notre classe AgenceBancaire ; elle met déjà en
oeuvre toutes les opérations de l'interface
ListeDeComptes, sauf la suppression.
Grâce au mot clé implements, nous allons
déclarer que cette classe met en oeuvre l'interface :
public class AgenceBancaire implements ListeDeComptes {
...
}
Mais si nous essayons de la compiler après avoir simplement ajouté
cette déclaration, nous avons le message d'erreur suivant :
javac AgenceBancaire.java
AgenceBancaire.java:11: class AgenceBancaire must be declared abstract.
It does not define void supprimer(CompteBancaire) from interface ListeDeComptes.
public class AgenceBancaire implements ListeDeComptes {
^
1 error
Que se passe-t-il ? En fait, en déclarant AgenceBancaire
implements ListeDeComptes, nous nous sommes engagés à
remplir le contrat exprimé par l'interface.
Or nous n'avons pas pour l'instant dit comment réaliser la méthode
supprimer ; le compilateur nous suggère donc de rendre la
classe abstraite, supposant que nous voulons définir des sous-classes
dans lesquelles cette méthode serait définie.
Il se trompe... Nous allons plutôt ajouter la méthode supprimer
à la classe AgenceBancaire, dont la nouvelle version est donnée
ci-après :
// Classe AgenceBancaire - version 1.2
/**
* Classe représentant une agence bancaire et les méthodes qui lui
* sont associées.
* @author Karl Tombre
*/
public class AgenceBancaire implements ListeDeComptes {
private CompteBancaire[] tabComptes; // le tableau des comptes
private int capacitéCourante; // la capacité du tableau
private int nbComptes; // le nombre effectif de comptes
// Constructeur -- au moment de créer une agence, on crée un tableau
// de capacité initiale 10, mais qui ne contient encore aucun compte
AgenceBancaire() {
tabComptes = new CompteBancaire[10];
capacitéCourante = 10;
nbComptes = 0;
}
// Méthode privée utilisée pour incrémenter la capacité
private void augmenterCapacité() {
// Incrémenter la capacité de 10
capacitéCourante += 10;
// Créer un nouveau tableau plus grand que l'ancien
CompteBancaire[] tab = new CompteBancaire[capacitéCourante];
// Recopier les comptes existants dans ce nouveau tableau
for (int i = 0 ; i < nbComptes ; i++) {
tab[i] = tabComptes[i];
}
// C'est le nouveau tableau qui devient le tableau des comptes
// (l'ancien sera récupéré par le ramasse-miettes)
tabComptes = tab;
}
// Les méthodes de l'interface
// Ajout d'un nouveau compte
public void ajout(CompteBancaire c) {
if (nbComptes == capacitéCourante) { // on a atteint la capacité max
augmenterCapacité();
}
// Maintenant je suis sûr que j'ai de la place
// Ajouter le nouveau compte dans la première case vide
// qui porte le numéro nbComptes !
tabComptes[nbComptes] = c;
// On prend note qu'il y a un compte de plus
nbComptes++;
}
// Récupérer un compte à partir d'un nom donné
public CompteBancaire trouverCompte(String nom) {
boolean trouvé = false; // rien trouvé pour l'instant
int indice = 0;
for (int i = 0 ; i < nbComptes ; i++) {
if (nom.equalsIgnoreCase(tabComptes[i].nom())) {
trouvé = true; // j'ai trouvé
indice = i; // mémoriser l'indice
break; // plus besoin de continuer la recherche
}
}
if (trouvé) {
return tabComptes[indice];
}
else {
return null; // si rien trouvé, je rend la référence nulle
}
}
// Afficher l'état de tous les comptes
public void afficherEtat() {
// Il suffit d'afficher l'état de tous les comptes de l'agence
for (int i = 0 ; i < nbComptes ; i++) {
tabComptes[i].afficherEtat();
}
}
// Traitement quotidien de tous les comptes
public void traitementQuotidien() {
for (int i = 0 ; i < nbComptes ;i++) {
tabComptes[i].traitementQuotidien();
}
}
// Suppression d'un compte
public void supprimer(CompteBancaire c) {
boolean trouvé = false; // rien trouvé pour l'instant
int indice = 0;
for (int i = 0 ; i < nbComptes ; i++) {
if (tabComptes[i] == c) { // attention comparaison de références
trouvé = true; // j'ai trouvé
indice = i; // mémoriser l'indice
break; // plus besoin de continuer la recherche
}
}
if (trouvé) {
// Décaler le reste du tableau vers la gauche
// On "écrase" ainsi le compte à supprimer
for (int i = indice+1 ; i < nbComptes ; i++) {
tabComptes[i-1] = tabComptes[i];
}
// Mettre à jour le nombre de comptes
nbComptes--;
}
else {
// Message d'erreur si on n'a rien trouvé
System.out.println("Je n'ai pas trouvé ce compte");
}
}
}
Notez bien la manière dont on "efface" le compte en décalant d'une
position vers la gauche toutes les valeurs du tableau après la
position du compte à supprimer.
Notez aussi que nous fondons la recherche du compte à supprimer sur la
comparaison des références (opérateur ==) et non sur la
comparaison des contenus, ce qui nécessiterait une opération plus
complexe.
Modifions maintenant notre programme de gestion bancaire de deux
manières :
-
la variable monAgence est maintenant déclarée de type
ListeDeComptes, donc en termes d'interface -- bien entendu,
l'instanciation se fait à partir d'une classe concrète, à savoir
AgenceBancaire ;
- l'option de suppression d'un compte est proposée à l'utilisateur.
// Classe Banque - version 4.2
/**
* Classe contenant un programme de gestion bancaire, utilisant
* un objet de type AgenceBancaire
* @author Karl Tombre
* @see ListeDeComptes, CompteBancaire, Utils
*/
public class Banque {
public static void main(String[] args) {
ListeDeComptes monAgence = new AgenceBancaire();
while (true) { // boucle infinie dont on sort par un break
String monNom = Utils.lireChaine("Donnez le nom du client (rien=exit) : ");
if (monNom.equals("")) {
break; // si on ne donne aucun nom on quitte la boucle
}
else {
// Vérifier si le compte existe
CompteBancaire monCompte = monAgence.trouverCompte(monNom);
if (monCompte == null) {
// rien trouvé, on le crée
System.out.println("Ce compte n'existe pas, nous allons le créer");
String monAdresse = Utils.lireChaine("Adresse = ");
String type =
Utils.lireChaine("Compte de [D]épôt (défaut) ou d'[E]pargne ? ");
char choixType = type.charAt(0);
// Créer le compte de type demandé
if (choixType == 'e' || choixType == 'E' ||
choixType == 'é' || choixType == 'É') {
monCompte = new CompteEpargne(monNom, monAdresse, 0.00015);
}
else {
monCompte = new CompteDepot(monNom, monAdresse, 0.00041);
}
// L'ajouter aux comptes de l'agence
monAgence.ajout(monCompte);
}
String choix =
Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [S]upprimer ? ");
boolean supprimer = false; // pour savoir si on a supprimé
// Récupérer la première lettre de la chaîne saisie
char monChoix = choix.charAt(0);
if (monChoix == 's' || monChoix == 'S') {
monAgence.supprimer(monCompte);
supprimer = true;
}
else if (monChoix == 'c' || monChoix == 'C') {
int montant = Utils.lireEntier("Montant à créditer = ");
monCompte.créditer(montant);
}
else if (monChoix == 'd' || monChoix == 'D') {
int montant = Utils.lireEntier("Montant à débiter = ");
monCompte.débiter(montant);
}
else {
System.out.println("Choix invalide");
}
if (!supprimer) {
// Si on n'a pas supprimé, afficher l'état du compte
monCompte.afficherEtat();
}
else {
// Sinon afficher l'état global de l'agence
System.out.println("Nouvel état des comptes de l'agence");
monAgence.afficherEtat();
}
}
}
// Quand on sort de la boucle, afficher l'état global de l'agence
System.out.println("Voici le nouvel état des comptes de l'agence");
monAgence.afficherEtat();
System.out.println("-----------------------------------");
System.out.println("On applique un traitement quotidien");
System.out.println("-----------------------------------");
monAgence.traitementQuotidien();
monAgence.afficherEtat();
}
}
Une trace d'exécution de ce nouveau programme montre la possibilité de
supprimer un compte :
Donnez le nom du client (rien=exit) : \emph{Karl}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Sornéville}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{D}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{D}
Montant à débiter = \emph{10000}
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -10000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Luigi}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Rome}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{E}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{C}
Montant à créditer = \emph{100000}
Compte d'épargne
Compte numéro 2 ouvert au nom de Luigi
Adresse du titulaire : Rome
Le solde actuel du compte est de 100000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Jacques}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Laxou}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{D}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{C}
Montant à créditer = \emph{23000}
Compte de dépôts
Compte numéro 3 ouvert au nom de Jacques
Adresse du titulaire : Laxou
Le solde actuel du compte est de 23000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Luigi}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{S}
Nouvel état des comptes de l'agence
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -10000 euros.
************************************************
Compte de dépôts
Compte numéro 3 ouvert au nom de Jacques
Adresse du titulaire : Laxou
Le solde actuel du compte est de 23000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Luigi}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Bari}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{E}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{C}
Montant à créditer = \emph{100000}
Compte d'épargne
Compte numéro 4 ouvert au nom de Luigi
Adresse du titulaire : Bari
Le solde actuel du compte est de 100000 euros.
************************************************
Donnez le nom du client (rien=exit) :
Voici le nouvel état des comptes de l'agence
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -10000 euros.
************************************************
Compte de dépôts
Compte numéro 3 ouvert au nom de Jacques
Adresse du titulaire : Laxou
Le solde actuel du compte est de 23000 euros.
************************************************
Compte d'épargne
Compte numéro 4 ouvert au nom de Luigi
Adresse du titulaire : Bari
Le solde actuel du compte est de 100000 euros.
************************************************
-----------------------------------
On applique un traitement quotidien
-----------------------------------
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -10004 euros.
************************************************
Compte de dépôts
Compte numéro 3 ouvert au nom de Jacques
Adresse du titulaire : Laxou
Le solde actuel du compte est de 23000 euros.
************************************************
Compte d'épargne
Compte numéro 4 ouvert au nom de Luigi
Adresse du titulaire : Bari
Le solde actuel du compte est de 100014 euros.
************************************************
Nous allons maintenant illustrer l'intérêt d'avoir utilisé une
interface, en définissant une autre classe qui met également en
oeuvre l'interface ListeDeComptes, mais cette fois-ci au
moyen d'une liste chaînée1.
Définissons tout d'abord la classe ChainonCompte, qui définit un
"maillon" de la chaîne, sur le modèle de ce que nous avons fait pour
ElementListInt :
public class ChainonCompte {
public CompteBancaire val;
public ChainonCompte succ;
// Constructeur à partir d'un compte bancaire
ChainonCompte(CompteBancaire c) {
val = c;
succ = null;
}
}
La classe qui met en oeuvre l'interface ListeDeComptes s'écrit
ensuite assez aisément :
// Classe AgenceBancaireBis - version 1.0
/**
* Classe représentant une agence bancaire et les méthodes qui lui
* sont associées, au moyen d'une liste chaînée
* @author Karl Tombre
*/
public class AgenceBancaireBis implements ListeDeComptes {
private ChainonCompte tete; // la tête de liste
// Constructeur -- au moment de créer une agence, on crée une liste vide
AgenceBancaireBis() {
tete = null;
}
// Les méthodes de l'interface
// Ajout d'un nouveau compte
public void ajout(CompteBancaire c) {
// On va l'ajouter en tête, c'est le plus facile
ChainonCompte n = new ChainonCompte(c);
n.succ = tete;
tete = n;
}
// Récupérer un compte à partir d'un nom donné
public CompteBancaire trouverCompte(String nom) {
ChainonCompte courant = tete; // chaînon courant
while ((courant != null) && (!nom.equalsIgnoreCase(courant.val.nom()))) {
courant = courant.succ; // aller au suivant
}
if (courant != null) { // on en a trouvé un
return courant.val;
}
else {
return null; // si rien trouvé, je rend la référence nulle
}
}
// Afficher l'état de tous les comptes
public void afficherEtat() {
// Il suffit d'afficher l'état de tous les comptes de l'agence
ChainonCompte courant = tete;
while (courant != null) {
courant.val.afficherEtat();
courant = courant.succ;
}
}
// Traitement quotidien de tous les comptes
public void traitementQuotidien() {
ChainonCompte courant = tete;
while (courant != null) {
courant.val.traitementQuotidien();
courant = courant.succ;
}
}
// Suppression d'un compte
public void supprimer(CompteBancaire c) {
if ((tete != null) && (tete.val == c)) {
// Cas particulier de la suppression en tête
tete = tete.succ;
}
else {
ChainonCompte courant = tete; // chaînon courant
ChainonCompte prev = null; // celui qui précède le courant
while ((courant != null) && (courant.val != c)) {
prev = courant; // mémoriser le chaînon d'où un vient
courant = courant.succ; // aller au suivant
}
if (courant != null) {
// Si on a trouvé quelque chose
prev.succ = courant.succ; // on le court-circuite
}
else {
// Message d'erreur si on n'a rien trouvé
System.out.println("Je n'ai pas trouvé ce compte");
}
}
}
}
NB : je vous conseille d'étudier attentivement la manière
dont sont effectuées des opérations telles que la recherche ou la
suppression. Elles sont caractéristiques des opérations sur les listes
chaînées.
Le moment "magique" approche...
Nous revenons maintenant au programme de gestion bancaire, et nous
nous contentons de modifier la classe instanciée pour créer l'objet
référencé par la variable monAgence :
ListeDeComptes monAgence = new AgenceBancaireBis();
Et le programme fonctionne de la même manière, bien que la
représentation interne des données soit tout à fait différente !
Nous voyons bien que nous avons gagné un niveau d'abstraction en
utilisant l'interface au lieu d'utiliser directement la classe...
Mais avons-nous vraiment le même fonctionnement ?
En fait, l'opération d'affichage des états nous donne les
comptes dans l'ordre inverse de celui donné avec une instance de
AgenceBancaire. Cela est dû au fait que l'adjonction d'un
nouveau compte se fait en tête de liste dans la classe
AgenceBancaireBis, alors qu'elle se fait en queue du tableau
dans AgenceBancaire.
Voici les dernières lignes de la trace d'exécution de la nouvelle
version du programme :
-----------------------------------
On applique un traitement quotidien
-----------------------------------
Compte de dépôts
Compte numéro 4 ouvert au nom de Luigi
Adresse du titulaire : Bari
Le solde actuel du compte est de 98765 euros.
************************************************
Compte d'épargne
Compte numéro 3 ouvert au nom de Jacques
Adresse du titulaire : Laxou
Le solde actuel du compte est de 123 euros.
************************************************
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -12350 euros.
************************************************
5.2 Les piles
Une pile est une liste particulière, sur laquelle on ne permet
l'adjonction et la suppression qu'en tête. Ainsi, c'est toujours le
dernier ajouté qui sera le premier sorti (on parle parfois de LIFO :
Last In First Out).
Les opérations de la pile sont souvent appelées empiler et
dépiler, ou en anglais push et pop.
Ce modèle de données trouve beaucoup d'applications pratiques quand il
s'agit par exemple de mémoriser des contextes en programmation
structurée (notion de pile des appels) ou des opérandes en attente
d'opérateur, etc. En particulier, le calcul arithmétique avec notation
suffixée (dite aussi polonaise inversée) se gère aisément au moyen
d'une pile.
On peut bien entendu représenter une pile avec toutes les structures
de données qui permettent de représenter des listes ou des tableaux.
Nous donnons ci-après une esquisse d'une classe PileEntiers
permettant d'empiler et de dépiler des entiers.
Nous avons choisi une représentation par un tableau surdimensionné, mais vous savez
maintenant comment le transformer en un tableau extensible...
Nous utilisons aussi un traitement d'exception pour le cas où on
essaie de dépiler d'une pile déjà vide.
Nous reviendrons plus tard sur cette notion d'exceptions et nous
reprendrons cet exemple en le détaillant plus
(§ 6.4.2).
import java.util.*;
public class PileEntiers {
private int[] tab;
private int premierLibre;
// Constructeur
PileEntiers() {
tab = new int[1000]; // surdimensionné
premierLibre = 0;
}
public boolean pileVide() {
return (premierLibre == 0);
}
public void empiler(int x) {
tab[premierLibre] = x;
++premierLibre;
}
public int depiler() throws EmptyStackException {
if (! pileVide()) {
--premierLibre;
return tab[premierLibre];
}
else {
throw new EmptyStackException();
}
}
}
5.3 Les arbres
Si la liste permet de représenter une séquence d'éléments, l'arbre
offre des moyens d'organisation plus puissants.
Chaque noeud d'un arbre "contient" une valeur, et "pointe"
sur les fils du noeud.
L'arbre binaire est l'archétype des arbres ; chaque noeud a
deux fils, et c'est un modèle de données qui permet en particulier de
représenter les ensembles de manière efficace.
Nous y reviendrons au § 6.5.1 ; contentons-nous pour
l'instant de donner la classe qui représente les noeuds des arbres
binaires d'entiers :
public class NoeudArbre {
public int val;
public NoeudArbre filsGauche;
public NoeudArbre filsDroit;
// Constructeur
NoeudArbre(int x) {
val = x;
filsGauche = null;
filsDroit = null;
}
}
- 1
- De même que pour le tableau extensible,
un vrai programmeur Java préférerait recourir directement à la
classe LinkedList, disponible dans la bibliothèque de base
Java, et qui met en oeuvre une liste chaînée générique. Mais une
fois de plus, il nous semble utile
d'avoir fait soi-même ce genre de manipulation au moins une
fois...