Algorithmique 2
Algorithmique 2
Algorithmique 2
Chapitre 7
On remarque enfin, dans tous ces exemples, qu’un fichier séquentiel peut être considéré
comme un support mobile défilant devant un repère fixe (par exemple, guichet de banque, tête
de lecture du lecteur de cédérom).
Le sens de défilement devant ce repère fixe (que nous appelons dans le schéma abstrait de
représentation d’un fichier la tête de lecture/écriture) est toujours le même. En effet,
lorsqu’un client est déjà passé devant le guichet, il ne lui est pas possible de repasser devant le
même guichet sans avoir à reprendre place à la queue de la file.
Nous avons donné un certain nombre de situations de la vie courante qui correspondent au
modèle de fichier séquentiel. En informatique, d’autre part, on utilise un certain nombre de
moyens de stockage d’informations, magnétiques ou optiques, dont le traitement par un
dispositif électromagnétique de lecture/écriture correspond au modèle de fichier séquentiel.
Ces dispositifs physiques (supports de fichiers dits séquentiels) sont les plus simples à réaliser
techniquement. Ils ont non seulement un intérêt historique, mais encore une grande
importance pratique dans les systèmes informatiques existants.
où tfichier est un identificateur de type de fichier, et télément est le type des éléments du
fichier ; télément peut être un type de données scalaires (entier, réel, caractère, booléen) ou un
type structuré (article, vecteur, chaîne de caractères).
Quelques exemples de définition de types de fichiers.
type
fentiers = fichier de entier ;
fréels = fichier de réel ;
temployé = article
matricule : entier ;
nom : chaîne ;
prénom : chaîne ;
salaire : réel ;
fin ;
femployé = fichier de temployé ;
7.1.5 Déclaration de fichiers
Une fois qu’un type de fichier est défini, on peut ensuite déclarer des fichiers de ce type. Pour
déclarer un fichier séquentiel f, on utilise une instruction de déclaration de la forme suivante :
var
f : tfichier ;
où tfichier est un identificateur de type de fichier préalablement défini.
Cette caractéristique permet donc aux fichiers de pouvoir conserver leur contenu à long
terme : un fichier est réutilisable après l’exécution du programme.
7.1.7 Opérations de base sur les fichiers
Un ensemble d'opérations ou fonctions (primitives) de base permettent d’accéder aux
éléments d’un fichier, notamment :
de créer ou de détruire des fichiers,
de consulter ou de modifier l'état d'un fichier,
de combiner des fichiers entre eux.
Interpréteur associé à un fichier séquentiel
La lecture et l'écriture dans un fichier étant séquentielles, on spécifie pour chaque fichier f une
tête de lecture/écriture tête(f) qui indique à tout moment l’élément de f auquel on peut accéder
directement. On a accès à un élément du fichier f que lorsque la place correspondant à cet
élément se trouve en face de la tête de lecture/écriture. L’élément qui se trouve en face de
tête(f) est appelé l’élément courant ou élément accessible. Un fichier étant une suite finie, on
introduit une marque spéciale qui permet au mécanisme d’interprétation de détecter la fin du
fichier.
tête de lecture/écriture
Les primitives d'accès aux fichiers séquentiels sont définies comme suit:
Indicateur de fin de fichier
Cet indicateur est une fonction booléenne notée fin(f) qui prend la valeur vrai lorsque tête(f)
se trouve en face de la marque de fin de fichier ; il prend la valeur faux dans le cas contraire.
Un fichier est vide si sa première place correspond à la marque de fin de fichier.
Création ou ouverture d’un fichier en écriture
Cette opération permet de créer un fichier initialement vide. fin(f) est mis à vrai. Elle
s’exprime de la manière suivante :
assigner(f, nomfichier) ;
réécrire(f) ;
ou tout simplement
réécrire(f, nomfichier) ;
ou tout simplement
relire(f, nomfichier) ;
Son effet est le suivant :
relire(f) :
si vide(f) alors
fin(f) = vrai
sinon
début
tête(f) <pointeur sur la première place de f>
fin(f) faux ;
end;
On peut schématiser cette primitive par :
tête de lecture/écriture
où avancer(f) est une opération qui fait avancer tête(f) d’une position vers la droite.
Cette primitive peut être schématisée par :
Avant la lecture :
tête de lecture/écriture
Après la lecture :
tête de lecture/écriture
tête de lecture/écriture
Après l’écriture :
val
tête de lecture/écriture
début
traiter(élément-courant) ;
avancer(f) ;
fin ;
traiter(élément-final) ;
fin.
SCHEMA N° 2: Ici le traitement courant est identique au traitement final.
début
initialisation ;
faire
traiter(élément-courant) ;
avancer(f) ;
tantque non fin(f) ;
fin.
Remarque. Étant donné qu’une tentative de lecture de la marque de fin de fichier provoque
une erreur d’exécution, on recommande d’utiliser le premier schéma dans tous les algorithmes
qui impliquent des opérations de lecture.
Exemples d’application
début
lire(f, nombre) ;
total total + nombre ;
fin ;
somme total ;
fermer(f) ;
fin;
On peut également exprimer l’algorithme ci-dessus sous forme de procédure.
procédure somme(nomfichier : chaîne ; var total : réel) ;
variable
nombre : réel ;
f : fichier de réel ;
début
relire(f, nomfichier) ;
total 0 ;
tantque non fin(f) faire
début
lire(f, nombre) ;
total total + nombre ;
fin ;
fermer(f) ;
fin;
Exemple 3. Calcul de la longueur d’un fichier (le nombre d’éléments d’un fichier).
fonction longueur(nomfichier : chaîne) : entier ;
variable
élément : télément ;
f : fichier de télément ;
compteur : entier ;
début
relire(f, nomfichier) ;
compteur 0 ;
tantque non fin(f) faire
début
lire(f, élément) ;
compteur compteur + 1 ;
fin ;
longueur compteur ;
fermer(f) ;
fin;
début
réécrire(f, nomfichier) ;
compteur 0 ;
tantque (compteur < n) faire
début
lire(élément) ;
écrire(f, élément);
compteur compteur + 1 ;
fin ;
fermer(f) ;
fin;
Exemple 5. Création d’un fichier à partir d’un vecteur liste de n éléments. On suppose que le
vecteur liste est formé d’éléments de même type que les éléments du fichier à créer.
procédure créerfichier(nomfichier : chaîne ; liste : vélément ; n : entier) ;
variable
f : fichier de télément;
i : entier ;
début
réécrire(f, nomfichier) ;
i 1 ;
tantque (i n) faire
début
écrire(f, liste[i]);
i i + 1 ;
fin ;
fermer(f) ;
fin;
Exemple 6. Création d’un fichier d’un nombre quelconque d’éléments de type télément.
procédure créerfichier(nomfichier: chaîne) ;
variable
élément : télément ;
f : fichier de télément;
ch : caractère ;
stop : booléen ;
début
réécrire(f, nomfichier) ;
stop faux ;
tantque non stop faire
début
lire(élément) ;
écrire(f, élément);
écrire('Avez-vous un élément à ajouter (O ou N) ? ');
lire(ch) ;
stop ch = 'N' ;
fin ;
fermer(f) ;
fin;
Exercices d’apprentissage
Exercice 7.1
Écrire une fonction qui délivre la somme des éléments de rang impair d’un fichier de nombres
entiers.
Exercice 7.2
Écrire une fonction qui délivre la différence entre la somme des éléments de rang pair et la
somme des éléments de rang impair d’un fichier de nombres réels.
Exercice 7.3
Écrire une fonction qui délivre la valeur du dernier élément d’un fichier
Exercice 7.4
Écrire une fonction qui délivre la somme du premier élément et du dernier élément d’un
fichier de nombres réels (par convention, si le fichier est vide la somme est nulle et si le
fichier ne contient qu’un seul élément, la somme est égale au double de l’unique élément).
élément : télément ;
f : fichier de télément;
début
relire(f, nomfichier) ;
trouvé faux ;
i 1 ;
tantque (i k) et non fin(f) faire
début
lire(f, élément) ;
i i + 1 ;
fin ;
si i = k +1 alors
début
elem élément ;
trouvé vrai ;
fin ;
fermer(f) ;
fin;
7.2.4.2 Accès par valeur ou accès associatif
On donne un fichier f et une valeur val appartenant à l’ensemble des valeurs du fichier f. On
veut savoir val est présente dans le fichier ou non.
Première version
L’approche consiste à lire le premier élément du fichier et à poursuivre le parcours du fichier
tant que le dernier élément lu est différent de val et l’on n’a pas encore atteint la fin du fichier.
Lorsque la boucle se termine, on vérifie si la dernière valeur lue est égale à val.
L’algorithme est alors le suivant :
fonction accèsval(nomfichier : chaîne; val : télément) : booléen ;
var
élément : télément ;
f : fichier de télément;
trouvé : booléen ;
début
relire(f, nomfichier) ;
si fin(f) alors
trouvé faux
sinon
début
lire(f, élément) ;
tantque (élément val) et non fin(f) faire
lire(f, élément) ;
trouvé élément = val ;
fin ;
accèsval trouvé ;
fermer(f)
fin;
Deuxième version
procédure accèsval(nomfichier : chaîne; val : télément) : booléen;
var
élément : télément ;
f : fichier de télément;
trouvé : booléen ;
début
relire(f, nomfichier) ;
trouvé faux ;
tantque non fin(f) et non trouvé faire
début
lire(f, élément) ;
si (élément = val) alors
trouvé vrai ;
fin ;
accèsval trouvé ;
fermer(f) ;
fin;
Exercices d’apprentissage
Exercice 7.5
Écrire une fonction qui calcule le nombre d’occurrences d’une valeur dans un fichier.
Exercice 7.6
Écrire une fonction qui calcule le rang de la dernière occurrence d’une valeur dans un fichier.
Exercice 7.7
Écrire une fonction qui vérifie qu’un fichier contient au moins n éléments.
Exercice 7.8
Écrire une fonction qui calcule le nombre d’occurrences d’une valeur comprises entre le i ème et
le jème éléments avec i < j.
Exercice 7.9
Écrire une fonction qui délivre le rang de la première occurrence d’une valeur dans un fichier.
Cette fonction retourne la valeur zéro si la valeur n’est pas présente dans le fichier.
Exercice 7.10
Écrire une fonction qui délivre le nombre d’occurrences de la valeur « val1 » entre les
premières occurrences des valeurs « val2 » et « val3 » dans cet ordre.
Exercices de compréhension
Exercice 7.11
On considère un fichier dont le type des éléments est défini par :
type
temployé = article
nom : chaîne ;
matricule : chaîne ;
âge : entier ;
sexe : (féminin, masculin) ;
fin ;
1. Écrire une fonction qui prend en entrée un fichier et délivre l’âge moyen des femmes
présentes dans le fichier.
2. Écrire une procédure qui prend en entrée un fichier et délivre le nombre d’hommes et
le nombre de femmes présents dans le fichier.
3. Écrire une fonction qui prend en entrée un fichier et retourne le nombre de personnes
mineures (moins de 18 ans) présentes dans le fichier.
4. Écrire une fonction qui prend en entrée un fichier et retourne le nom de la personne la
5. Écrire une procédure qui prend en entrée un fichier et une chaîne de caractères
représentant le matricule d’un employé et détermine si un employé ayant ce matricule
est présent dans la fichier. Dans le cas où l’employé est présent dans le fichier, la
procédure doit aussi retourner l’ensemble des informations concernant cet employé.
fichiertrié vrai
sinon
début
lire(f , courant) ;
tantque non fin(f) et (précédent courant) faire
début
précédent courant ;
lire(f, courant) ;
fin ;
fichiertrié précédent courant ;
fin ;
fin ;
fermer(f) ;
fin ;
Exercice de compréhension
Exercice 7.12
Écrire une fonction qui reçoit en entrée un fichier et vérifie que le fichier est trié dans l’ordre
croissant sans répétition.
Exercice 7.13
Écrire une fonction qui reçoit en entrée un fichier et vérifie que le fichier est trié dans l’ordre
décroissant.
Exercice 7.14
Écrire un algorithme qui délivre le nombre d’occurrences de la valeur val dans un fichier trié.
Exercice 7.15
Écrire un algorithme qui délivre le rang de la dernière occurrence de la valeur val dans un
fichier trié ou zéro si val ne se trouve pas dans le fichier.
Exercice 7.16
Écrire un algorithme qui recherche la valeur val après le rang i dans un fichier trié.
Conclusion
Nous avons donné quelques exemples d’algorithmes traitant un seul fichier, les plus courants
dans les applications informatiques. Il en existe beaucoup d’autres ne nécessitant qu’une seule
action itérative. Nous allons maintenant étudier quelques algorithmes classiques traitant
plusieurs fichiers à la fois.
lire(f, élément) ;
écrier(h, élément) ;
fin ;
{copie de g à la suite de f dans h}
tantque non fin(g) faire
début
lire(g, élément) ;
écrire(h, élément) ;
fin ;
fermer(f) ; fermer(g) ; fermer(h) ;
fin ;
fermer(fm) ;
fin ;
Deuxième version. Utilisation de l’instruction de sélection à choix multiples
procédure éclater(nomf, nomf1,.., nomfm : chaîne) ;
variable
élément : télément ;
f, f1, …, fm : fichier de télément;
début
relire(f, nomf) ;
réécrire(f1, nomf1) ;
...
réécrire(fm, nomfm) ;
tantque non fin(f) faire
début
lire(f, élément);
sélection critère(élément) de
valeur1 : écrire(f1, élément);
valeur2 : écrire(f2, élément) ;
…
valeurm : écrire(fm, élément)
fin ;
fin;
fermer(f) ;
fermer(f1) ;
...
fermer(fm) ;
fin ;
Exemples d’application
Exemple 1. On considère un fichier dont le type des éléments est défini par :
type
tpersonne = article
nom : chaîne ;
sexe : (féminin, masculin) ;
fin ;
a) Ecrire une procédure qui prend en entrée un fichier f et crée un nouveau fichier composé
uniquement de personnes de sexe féminin.
procédure éclater(source, sortie: chaîne) ;
variable
personne : tpersonne ;
f, g : fichier de tpersonne ;
début
relire(f, source) ;
réécrire(g, sortie) ;
tantque non fin(f) faire
début
lire(f, personne) ;
si personne.sexe = féminin alors
écrire(g, personne) ;
fin ;
fermer(f) ;
fermer(g) ;
fin ;
b) Ecrire une procédure qui prend en entrée un fichier f et l’éclate en deux fichiers, le premier
contenant des personnes de sexe masculin et le second des personnes de sexe féminin.
procédure éclater(source, nomg, nomh : chaîne) ;
variable
élément : tpersonne ;
f, g, h : fichier de tpersonne ;
début
relire(f, source) ;
réécrire(g, nomg) ;
réécrire(h, nomh) ;
tantque non fin(f) faire
début
lire(f, élément) ;
si élément.sexe = masculin alors
écrire(g, élément)
sinon
écrire(h, élément) ;
fin ;
fermer(f) ;
fermer(g) ;
fermer(h) ;
fin ;
Exemple 2. On considère un fichier d’étudiants dont le type des éléments est défini par :
type
tétudiant = article
matricule : chaîne ;
nom : chaîne ;
filière : (GBIO, MIP, GTE, GIN) ;
fin ;
Écrire une procédure qui prend en entrée un fichier d’étudiants et l’éclate en quatre nouveaux
fichiers, le premier composé des étudiants de la filière GBIO, le deuxième des étudiants de la
filière MIP, le troisième des étudiants de la filière GTE et le quatrième des étudiants de la
filière GIN.
Première version : Utilisation des instructions si…alors…sinon imbriquées.
procédure éclater(nomf, nomg1, nomg2, nomg3, nomg4 : chaîne) ;
variable
étudiant : tétudiant ;
f, g1, g2, g3, g4 : fichier de tétudiant ;
début
relire(f, nomf) ;
réécrire(g1, nomg1) ;
réécrire(g2, nomg2) ;
réécrire(g3, nomg3) ;
réécrire(g4, nomg4) ;
tantque non fin(f) faire
début
lire(f, étudiant) ;
si élément.filière = GBIO alors
écrire(g1, étudiant)
sinon
si élément.filière = MIP alors
écrire(g2, étudiant)
sinon
si étudiant.filière = GTE alors
écrire(g3, étudiant)
sinon
écrire(g4, étudiant);
fin ;
fermer(f) ;
fermer(g1) ;
fermer(g2) ;
fermer(g3) ;
fermer(g4)
fin ;
Deuxième version : Utilisation de l’instruction de sélection à choix multiples
procédure éclater(nomf, nomg1, nomg2, nomg3, nomg4 : chaîne) ;
variable
étudiant : tétudiant ;
f, g1, g2, g3 : fichier de tétudiant ;
début
relire(f, nomf) ;
réécrire(g1, nomg1) ;
réécrire(g2, nomg2) ;
réécrire(g3, nomg3) ;
réécrire(g4, nomg4) ;
tantque non fin(f) faire
début
lire(f, élément) ;
case étudiant.filière of
GBIO : écrire(g1, étudiant) ;
MIP : écrire(g2, étudiant) ;
GTE : écrire(g3, étudiant)
GIN : écrire(g3, étudiant) ;
fin ;
fin ;
fermer(f) ; fermer(g1) ; fermer(g2) ; fermer(g3) ; fermer(g4) ;
fin ;
Exercices d’apprentissage
Exercice 7.17
Écrire une procédure qui prend en entrée un fichier de nombres entiers et délivre en sortie
deux fichiers, le premier contenant les nombres pairs et le deuxième les nombres impairs.
Exercice 7.18
Écrire une procédure qui prend en entrée un fichier de nombres entiers et délivre en sortie
deux fichiers, le premier contenant les éléments de rang impair et le deuxième les éléments de
rang pair.
sinon
début
écrire(h, cg) ;
lire((g, cg) ;
fin ;
fin ;
tantque non fin(f) faire
début
lire(f, cf) ;
écrire(h, cf) ;
fin ;
tantque non fin(g) faire
début
lire(f, cf) ;
écrire(h, cf) ;
fin ;
fin ;
Exercices de recherche
Exercice 7.19
Écrire une procédure d’union de deux fichiers triés par ordre croissant sans répétition (le
fichier obtenu doit être trié sans répétition).
Exercice 7.20
Écrire une procédure d’intersection de deux fichiers triés par ordre croissant sans répétition (le
fichier obtenu doit être trié sans répétition).
var
élément : télément ;
f, g : fichier de télément ;
début
relire(f, ancien) ;
réécrire(g, nouveau) ;
écrire(g, elem) ;
tantque non fin(f) faire
début
lire(f, élément) ;
écrire(g, élément)
fin ;
fermer(f) ; fermer(g) ;
fin ;
Dans le deuxième cas, l’approche consiste à créer un nouveau fichier g, recopier dans g tous
les éléments f et à ranger dans g la valeur à insérer.
L’algorithme est alors le suivant :
procédure insertfin(ancien, nouveau : chaîne ; elem : télément) ;
variable
élément : télément ;
f, g : fichier de télément ;
début
relire(f, ancien) ;
réécrire(g, nouveau) ;
tantque non fin(f) faire
début
lire(f, élément) ;
écrire(g, élément)
fin ;
écrire(g, elem) ;
fermer(f) ;
fermer(g) ;
fin ;
7.4.1.2 Insertion par position
On donne la place k du nouvel élément. Il s’agit d’écrire une procédure d’insertion à la kème
place du nouvel élément. L’insertion n’est possible que si k [1..n+1], où n est le nombre de
places du fichier f.
Première solution. La première approche que l’on peut utiliser pour résoudre ce problème
consiste à créer un fichier g en procédant en trois étapes de la manière suivante :
copier les k - 1 premiers éléments de f dans g,
insérer le nouvel élément dans g,
copier les n – k + 1 derniers éléments de f dans g.
L’algorithme est alors le suivant :
procédure insertk(source, sortie : chaîne; k : entier; elem : télément ;var possible : booléen) ;
var
i : entier ;
élément : télément ;
f : fichier de télément ;
début
relire(f, ancien) ;
réécrire(g, nouveau) ;
i 1 ;
possible faux ;
tantque non fin(f) et (i < k) faire
début
lire(f, élément) ;
écrire(g, élément) ;
i i + 1 ;
fin ;
si i = k alors
début
écrire(g, elem) ;
tantque non fin(f) faire
début
lire(f, élément) ;
écrire(g, élément)
fin ;
possible vrai
fin ;
fermer(f) ; fermer(g) ;
fin ;
Deuxième solution. Le deuxième raisonnement consiste à constater que le fichier d’entrée et
le fichier de sortie sont presque identiques (à un élément près) ; d’où l’idée d’utiliser
l’algorithme de copie et d’y ajouter l’insertion du nouvel élément à la kème place. Au cours
de la copie, il faut détecter la place de chaque élément et lorsque cette place devient égale à k
copier la valeur elem dans le fichier de sortie.
On notera que l’on teste la valeur de la place suivie de la copie de l’élément, ce qui interdit à
l’intérieur de la boucle tantque l’insertion en fin de fichier. Il faut donc ajouter, après
l’exécution du tantque, une séquence permettant l’insertion, si nécessaire, en fin de fichier.
L’algorithme est alors le suivant :
procédure insertk(source, sortie: chaîne; k : entier; elem : télément; var possible : booléen) ;
variable
i : entier ;
élément : télément ;
f, g : fichier de télément ;
début
relire(f, ancien) ;
réécrire(g, nouveau) ;
i 1 ;
possible faux ;
tantque non fin(f) faire
début
si i = k alors
début
écrire(g, elem) ;
possible vrai ;
fin ;
lire(f, élément) ;
écrire(g, élément) ;
i i + 1 ;
fin ;
si i = k alors
début
écrire(g, elem) ;
possible vrai ;
fin ;
fermer(f) ; fermer(g) ;
fin ;
lire(f, élément) ;
écrire(g, élément) ;
fin ;
fin ;
fermer(f) ;
fermer(g) ;
fin ;
Deuxième version. En utilisant la procédure de copie, il suffit de copier la valeur elem après
la première occurrence de la valeur val.
L’algorithme est alors le suivant :
procédure insertval(ancien, nouveau: chaîne ; val, elem : télément ;
k : entier ; var possible : booléen) ;
variable
élément : télément ;
trouvé : booléen ;
f, g : fichier de télément ;
début
relire(f, ancien) ;
réécrire(g, nouveau) ;
trouvé faux ;
tantque non fin(f) faire
début
lire(f, élément)
écrire(g, élément) ;
si (élément = val) et (non trouvé) alors
début
trouvé vrai ;
écrire(g, elem) ;
fin ;
fin ;
possible trouvé;
fermer(f) ;
fermer(g) ;
fin ;
inférieur vrai ;
relire(f, ancien) ;
réécrire(g, nouveau) ;
tantque non fin(f) et (élément < elem) faire
début
lire(f, élément) ;
si (élément elem) et inférieur alors
début
inférieur faux ;
écrire(g, elem) ;
fin ;
écrire(g, élément) ;
fin ;
si inférieur alors
écrire(g, elem) ;
fermer(f) ;
fermer(g) ;
fin ;
Exercices d’apprentissage
Exercice 7.21
Écrire une procédure d’insertion de la valeur elem après chaque occurrence de la valeur val
dans un fichier.
Exercice 7.22
Écrire une procédure d’insertion de la valeur elem après la dernière occurrence de la valeur
val dans un fichier trié.
On peut, comme dans le cas des insertions, supprimer le kème élément (suppression par
position) ou supprimer un élément ayant une valeur particulière (suppression associative).
Dans le cas de la suppression associative, le fichier peut en plus être trié, ce qui permet
d’accélérer l’algorithme au cas où l’élément que l’on veut supprimer est absent du fichier.
élément : télément ;
f, g : fichier de télément ;
i : entier ;
début
i 1 ;
relire(f, ancien) ;
réécrire(g, nouveau) ;
possible faux ;
tantque non fin(f) et (i < k) faire
début
lire(f, élément) ;
écrire(g, élément) ;
i i + 1 ;
fin ;
si (i = k) et non fin(f) alors
début
lire(f, élément) ;
tantque non fin(f) faire
début
lire(f, élément) ;
écrire(g, élément) ;
fin ;
possible vrai ;
fin ;
fermer(f) ;
fermer(g) ;
fin ;
Deuxième version. Une deuxième version consiste à utiliser l’algorithme de copie d’un
fichier dans un autre en omettant de copier le kème élément.
L’algorithme est alors le suivant :
procédure supprimek(ancien, nouveau : chaîne ; k : entier ; var possible : booléen) ;
variable
élément : télément ;
f, g : fichier de télément ;
i : entier ;
début
i 1 ;
relire(f, ancien) ;
réécrire(g, nouveau) ;
tantque non fin(f) et (i < k) faire
début
lire(f, élément) ;
si (i k) alors
écrire(g, élément) ;
i i + 1 ;
fin ;
possible i > k;
fermer(f) ;
fermer(g) ;
fin ;
7.4.2.2 Suppression associative
On donne un fichier f et une valeur val appartenant à l’ensemble des valeurs du fichier. Le
problème consiste à écrire un algorithme pour supprimer la première occurrence de la valeur
val dans le fichier f. La suppression n’est possible que si val appartient à l’ensemble des
valeurs de f. On définit donc une variable booléenne possible qui indiquera si la suppression a
été effectuée ou non.
Première version. L’approche consiste à lire le premier élément du fichier directeur et à
parcourir le reste du fichier en comparant à chaque étape le dernier élément lu à val. Lorsque
la boucle se termine on vérifie si le dernier élément lu est égal à val. Dans l’affirmative, la
suppression est possible (possible reçoit la valeur vrai) et on copie le reste des éléments du
fichier d’entrée dans le fichier de sortie.
L’algorithme est alors le suivant :
procédure supprime(ancien, nouveau : chaîne ; val : télément ; var possible : booléen) ;
variable
élément : télément ;
f, g : fichier de télément ;
début
possible faux ;
relire(f, ancien) ;
réécrire(g, nouveau) ;
lire(f, élément) ;
tantque non fin(f) et (élément val) faire
début
écrire(g, élément) ;
lire(f, élément) ;
fin ;
si élément = va) alors
début
possible vrai;
tantque non fin(f) faire
début
lire(f, élément) ;
écrire(g, élément) ;
fin ;
fin ;
fermer(f) ;
fermer(g) ;
fin ;
Deuxième version. Le deuxième raisonnement consister à constater que le fichier directeur et
le fichier de sortie sont presque identiques (à un élément près), d’où l’idée d’utiliser la
procédure de copie en y ajoutant la suppression de la première occurrence de la valeur val.
L’algorithme est alors le suivant :
procédure supprime(ancien, nouveau : chaîne ; val : télément ; var possible : booléen) ;
variable
élément : télément ;
f, g : fichier de télément ;
trouvé : booléen ;
début
trouvé faux ;
relire(f, ancien) ;
réécrire(g, nouveau) ;
tantque non fin(f) faire
début
lire(f, élément) ;
si (élément = val) et (non trouvé) alors
trouvé vrai
sinon
écrire(g, élément) ;
fin ;
possible trouvé ;
fermer(f) ;
fermer(g) ;
fin ;
Troisième version. Afin de simplifier la procédure, on suppose que le fichier f est trié et non
vide. L’approche consiste alors à copier dans un premier temps dans g tous les éléments de f
qui sont inférieurs à val. Ensuite si la valeur val est présente, on la supprime et on recopie le
reste des éléments de f dans g.
L’algorithme est alors le suivant :
procédure supprime(ancien, nouveau : chaîne ; val : télément ; var possible : booléen) ;
variable
élément : télément ;
f, g : fichier de télément ;
début
possible faux ;
relire(f, ancien) ;
réécrire(g, nouveau) ;
lire(f, élément) ;
tantque non fin(f) et (élément < val) faire
début
écrire(g, élément) ;
lire(f, élément) ;
fin ;
si (élément = val) alors
début
possible vrai ;
tantque non fin(f) faire
début
lire(f, élément) ;
écrire(g, élément) ;
fin ;
fin ;
fermer(f);
fermer(g) ;
fin ;
Exercices d’apprentissage
Exercice 7.23
Écrire une procédure de suppression, dans un fichier, de tous les éléments ayant un rang
compris entre i et j (j i).
Exercice 7.24
Écrire une procédure de suppression de toutes les occurrences de la valeur val dans un fichier
trié f.
Exercice 7.25
Écrire une procédure de suppression de tous les espaces (caractère blanc) superflus dans une
fichier de caractères. Le fichier résultat ne doit jamais contenir deux espaces consécutifs.
Exercice 7.26
On considère un fichier d’étudiants dont le type des éléments est défini par :
type
toption = (GBIO, MIP, GTE, GIN) ;
tétudiant = article
nom : chaîne ;
matricule : chaîne ;
option : toption ;
fin ;
1. Écrire une procédure qui prend en entrée un fichier d’étudiants et supprime tous les
étudiants d’une filière donnée.
2. Écrire une procédure qui prend en entrée un fichier d’étudiants, une chaîne de
caractères représentant le matricule d’un étudiant et supprime l’étudiant qui possède ce
matricule.
On suppose maintenant que le fichier est trié par rapport à l’option.
3. Écrire une procédure qui prend en entrée un fichier d’étudiants et supprime tous les
étudiants d’une filière donnée.
7.5 Conclusion
Il existe cependant des primitives permettant d’effectuer des accès aléatoires dans un fichier
en offrant un mécanisme de contrôle de la tête de lecture/écriture permettant de la positionner
directement à l’endroit voulu par le programmeur.
Dans ce chapitre, nous appliquerons les algorithmes sur les fichiers séquentiels composés
d’articles ayant une structure complexe : gestion d’un fichier du personnel enseignant d’une
faculté (comptage des éléments vérifiant une ou plusieurs propriétés, recherche d’éléments
vérifiant une ou plusieurs propriétés, mise à jour de fichiers, éclatement de fichiers), la
facturation des bons de commandes (comptage, création de fichiers, mise à jour, édition de
factures), la gestion académique des étudiants ‘une faculté.
On veut écrire un programme pour générer les étudiants inscrits dans une faculté. Les
informations sur les étudiants sont stockés dans un fichier. Les informations retenues pour
chaque étudiant sont : le numéro matricule (différent pour chaque étudiant), le nom, le sexe, le
programme de formation de la filière concernée.
On suppose que chaque étudiant a un matricule différent et que tous les étudiants inscrits dans
la même filière ont le même nombre et la même liste de cours.
3°/ Écrire une fonction qui délivre le nombre d’étudiants inscrits dans une filière de code
donné.
4°/ Écrire une fonction qui délivre le nombre d’unités de valeurs validée par un étudiant de
matricule donné.
5°/ Écrire une procédure qui imprime la liste étudiants ayant validé la totalité de leurs unités
de valeurs dans une filière donnée.
6°/ Écrire une procédure qui imprime (matricule, nom, sexe, filière) de tous les étudiants
présents dans le fichier.
7°/ Écrire une procédure qui imprime la liste des garçons inscrits dans une filière de code
donné.
8°/ Écrire une procédure qui imprime la liste générale des garçons inscrits.
9°/ Écrire une procédure qui imprime la liste des filles inscrites dans une filière donnée.
10°/ Écrire une procédure qui imprime la liste générale des filles inscrites.
11°/ Écrire une procédure qui imprime le relevé des notes d’un étudiant de matricule donné.
12°/ Écrire une procédure qui éclate un fichier d’étudiants en deux fichiers suivant le sexe de
l’étudiant.
14°/ Écrire une procédure qui insère un nouvel étudiant dans le fichier.
15°/ Écrire une procédure qui supprime le kème élément du fichier.
16/° Écrire une procédure qui supprime un étudiant de matricule donné.
17°/ Écrire une procédure qui supprime tous étudiants d’une filière de code donné.
Personnel enseignant
On veut gérer le fichier du personnel enseignant d’un établissement universitaire. Pour chaque
enseignant les informations retenus sont les suivantes : matricule, nom (nom et prénoms) ;
statut (permanent ou vacataire) ; grade (professeur, maître de conférences, chargé de cours,
assistant) ; département d’affectation (Mathématiques, Physique, Mécanique, Chimie,
Informatique,…) ; nombre de cours enseignés, nombre d’années d’ancienneté ; position
(présent ou en congé). Le fichier sera trié par ordre croissant sur les matricules, supposés tous
différents.
ancienneté : entier ;
fin ;
fenseignant = fichier de tenseignant ;
Facturation
On envisage de simuler une application de facturation simplifiée pour une entreprise de vente
de matériels informatiques par Internet.
Chaque client émet un ou plusieurs bons de commande. À partir de ces bons de commande,
on établit une ou plusieurs factures par client.
Dans un premier temps, on dispose d’un fichier de commandes contenant tous les bons de
commande de l’ensemble des clients.
Une commande est composée d’un code client codeclient, de la quantité commandée
quantité et du prix unitaire de l’article commandé prixunit.
Une facture est composée du code client codeclient et du montant de la facteur montant.
On utilisera les définitions suivantes :
type
tcommande = article
codeclient, quantité : entier ;
prixunit : réel ;
fin ;
tfacture = article
codeclient : entier ;
montant : réel ;
fin ;
fcommande = fichier de tcommande;
ffacture = fichier de tfacture ;
1°/ On souhaite vérifier que le fichier de commandes est trié par ordre croissant sur le code
client. Écrire une fonction booléenne trié qui prend en argument un fichier de commandes et
retourne la valeur vrai si le fichier est trié et faux sinon
2°/ Si un client a commandé n articles, on dispose de n bons de commande pour ce client. On
désire établir une facture pour chaque bon de commande. Le fichier de commandes n’est pas
trié. Écrire une procédure de facturation qui construit un fichier de factures contenant toutes
les factures établies pour chaque bon de commande.
3°/ On suppose maintenant que le fichier de commandes est trié par ordre croissant sur le code
client et on souhaite connaître le nombre de clients différents dans le fichier de commande.
Écrire une fonction qui prend en argument un fichier de commandes et retourne le nombre de
clients différents présents dans le fichier de commandes.
4°/ Le fichier de commandes est toujours trié, et on veut établir une seule facture pour tous les
bons de commande d’un même client. Écrire une procédure de facturation qui prend en
argument un fichier de commandes et construit un fichier de factures contenant une facture
pour chaque client.
5°/ On dispose maintenant d’un fichier clients indiquant pour chaque client son adresse. Ce
fichier est trié sur codeclient. On utilisera les définitions supplémentaires suivantes :
type
tclient = article
codeclient : entier ;
adresse : chaîne ;
fin ;
fclient = fichier de tclient ;
En supposant que le fichier clients est une variable globale de type fclient trié sur codeclient,
codeclient une variable globale de type entier, écrire un algorithme qui prend en entrée un
fichier de clients, un code client et retourne l’adresse du client ayant cette adresse si celui est
présent dans le fichier. L’algorithme retourne aussi un booléen indiquant si la recherche a été
fructueuse ou non.
Remarque
Quand on invoque cet algorithme, on suppose que les premiers éléments de client ont déjà été
parcourus lors des appels précédents.
clients est donc déjà ouvert en lecture,
cet algorithme recherche l’adresse du client possédant le code codeclient,
l’algorithme doit tenir compte du fait que le fichier clients est trié sur le code client.
type
tfacturebis = article
codeclient : entier ;
adresse : chaîne50 ;
montant : réel ;
fin ;
terreur = article
codeclient : entier ;
message : chaîne ;
fin ;
fcommande = fichier de tcommande ;
ffacture = fichier de tfacture ;
ferreur = fichier de terreur ;
fclient = fichier de tclient ;
ffacturebis = fichier de tfacturebis ;
En supposant que le fichier commandes est une variable globale de type fcommande trié sur
codeclient, codeclient une variable globale de type entier, écrire un algorithme qui recherche
la première occurrence du prochain client sachant que le fichier de commandes est déjà ouvert
en lecture.
7°/ Écrire une procédure de facturation qui reçoit en entrée un fichier de commandes, un
fichier de clients trié sur codeclient et construit un fichier de factures contenant les factures
établies pour chaque client et un fichier d’erreurs contenant les messages d’erreurs
correspondant aux clients du fichier de commandes non présents dans le fichier de clients.
Cette procédure a pour objet d’établir une seule facture pour toutes les commandes d’un
même client. L’algorithme doit tenir compte du fait que les deux fichiers d’entrée sont triés.
8°/ On souhaite maintenant détecter les bons de commandes des clients afin de leur faire un
cadeau pour les fêtes de fin d’années. Pour cela on doit disposer du montant de toutes les
commandes de chaque client. On dispose de la structure suivante :
type
tclient = article
codeclient : entier ;
adresse : chaîne ;
montant : réel ;
fin ;
Écrire une procédure de facturation qui reçoit en entrée un fichier de commandes, un fichier
de client trié sur codeclient et construit un fichier de factures contenant les factures établies
pour chaque client, un fichier de facture2 et un fichier d’erreurs contenant les messages
d’erreurs correspondant aux clients du fichier de commandes non présents dans le fichier de
clients.
Chapitre 8
8.1 Introduction
Dans les structures de données que nous avons étudiées jusque là (à l’exception des fichiers)
le nombre d’éléments et les inter-relations entre ces éléments sont fixés au moment de la
création (déclaration) de la structure. Ceci signifie que l’organisation de la structure reste
statique pendant l’exécution de l’algorithme. On peut facilement modifier la valeur des
éléments dans une structure statique, mais on ne peut pas augmenter leur nombre ou changer
les inter-relations qui existent entre eux.
Considérons par exemple le cas des vecteurs. La taille d’un vecteur est fixée au moment de sa
déclaration. Si, pendant l’exécution du programme, on se rend compte que l’on a besoin d’un
vecteur beaucoup plus grand, il n’y a rien que l’on peut faire. Le vecteur doit être re-déclaré et
le programme recompilé avec la nouvelle taille. De même, la structure d’un vecteur est fixe.
Le premier élément précède le deuxième, le deuxième précède le troisième, etc. Si on veut
supprimer le deuxième élément tel que le troisième élément devienne maintenant le deuxième,
on devra décaler tous les éléments d’une position vers la gauche. On ne pourra pas
simplement supprimer le deuxième élément. Les vecteurs sont des structures statiques.
Les structures dynamiques, par contre, peuvent grossir ou se rétrécir pendant l’exécution du
programme. On peut, par exemple, insérer facilement de nouveaux éléments ou supprimer des
éléments existants dans une structure dynamique. Ceci signifie que l’on peut dynamiquement
modifier la taille de la structure et les inter-relations entre les éléments de la structure pendant
l’exécution du programme. De telles structures sont très utiles pour stocker des informations
qui changent continuellement, tel que dans un système de réservation. Un tel système donne
droit à des structures de données qui peuvent varier en taille et en organisation pendant leur
durée de vie.
Les structures de données dynamiques sont souvent construites en reliant les éléments de la
structure entre eux. Pour construire de telles structures, on a besoin que chaque élément
possède non seulement la capacité de stocker des données mais aussi une information sur la
manière dont les éléments sont reliés entre eux. Les liaisons entre les éléments de la structure
sont implémentées en utilisant des variables appelées pointeurs.
Les pointeurs
Avant d’étudier comment construire et utiliser de telles structures chaînées, nous allons
examiner la définition et l’utilisation des pointeurs. Un pointeur est une variable qui référence
une autre variable. Comme une variable est associée à un emplacement de la mémoire, les
pointeurs sont des variables qui référencent les emplacements mémoire. Ainsi, les valeurs des
pointeurs sont les adresses des emplacements de la mémoire centrale.
En algorithmique, la définition d’une variable pointeur doit spécifier le type de la variable que
le pointeur référence. Par exemple, la définition
type
pentier = ^entier ;
définit le type pentier comme un pointeur qui référence un emplacement mémoire réservé
pour stocker des entiers. La forme générale d’une définition de type pointeur est :
type
tpointeur = ^télément ;
où tpointeur est un identificateur de type pointeur et télément un type de données (simple ou
structurée) préalablement défini. Une fois qu’un type de pointeur a été défini, on peut déclarer
des variables pointeurs de ce type comme d’habitude. La déclaration :
variable
p, q : pentier ;
déclare p et q comme des pointeurs sur des entiers. Cette déclaration réserve de l’espace en
mémoire pour deux adresses, chacune ayant la possibilité de référencer des emplacements
mémoire contenant des entiers. Cependant, ces pointeurs sont encore indéfinis. Ils ne
référencent pas encore un emplacement mémoire spécifique. Le langage algorithmique offre
une procédure standard appelée nouveau, qui permet d’initialiser dynamiquement un pointeur
et d’allouer simultanément la mémoire pour la variable qu’il référence. Les expressions
nouveau(p) ;
nouveau(q) ;
allouent de la mémoire pour deux entiers et affectent aux pointeurs p et q les adresses de ces
emplacements mémoire. Pour affecter les valeurs, ou simplement accéder, aux emplacements
mémoire que p et q référencent, on utilise les notations p^ et q^ pour dénoter les
emplacements mémoire référencés par p et q respectivement.
Quand l’emplacement mémoire référencé par un pointeur p n’est plus nécessaire, une
procédure standard, appelée libérer, peut être utilisée pour rendre libre cette emplacement
mémoire pour d’autres utilisations.
La procédure libérer permet de rendre libre une cellule préalablement occupée. On peut
l’exprimer de la manière suivante :
procédure libérer(p : pélément) ;
début
rendre au réservoir la cellule pointée par p ;
fin ;
Ces deux primitives permettent d’obtenir ou de rendre une cellule à la mémoire au fur et à
mesure des besoins de l’algorithme. On parle alors de gestion dynamique de la mémoire
contrairement à la gestion statique des vecteurs.
???
p
nouveau(p) ;
???
p p^
p^ 10 ;
10
p p^
libérer(p) ;
???
p
On a occasionnellement besoin d’une variable pointeur qui ne pointe sur aucun emplacement
mémoire, mais qui est déjà définie. En algorithmique, la valeur d’un tel pointeur est
représentée par le mot réservé nil. L’exécution de l’instruction
p nil
affecte une valeur au pointeur p, mais p ne pointe sur aucun emplacement mémoire. Le
pointeur nil est schématisé de la manière suivante :
L’avantage d’un pointeur nil par rapport à un pointeur indéfini est que l’on peut tester pour
voir s’il est nil ou non. Le code ci-dessous détermine si le pointeur p pointe sur un
emplacement mémoire.
si p = nil alors
Écrire('Le pointeur est NIL')
sinon
Écrire('Le pointeur référence un emplacement mémoire') ;
Si p est indéfini, ce code conduit à une erreur d’exécution. La valeur nil peut être affectée à
n’importe quel pointeur, quelque soit le type de données qui est référencé par le pointeur.
On peut examiner la valeur d’un pointeur en déterminant simplement s’il est nil, ou en le
comparant à la valeur d’un autre pointeur de même type. Le code ci-dessous vérifie pour voir
si les pointeurs p et q ont la même valeur ; c’est-à-dire si p et q contiennent la même adresse
ou référencent le même emplacement mémoire.
si p = q alors
si p nil alors
Écrire('Les deux pointeurs sont égaux')
sinon
Écrire('Les deux pointeurs sont NIL')
sinon
Écrire('Les deux pointeurs sont différents') ;
En plus d’affecter une valeur à un pointeur en utilisant la procédure standard nouveau() ou en
lui affectant la valeur nil, la valeur d’un pointeur peut être affectée à un autre pointeur de
même type. Le code suivant affecte d’abord une nouvelle valeur au pointeur p, puis affecte la
même valeur au pointeur q de sorte que p et q référencent le même emplacement mémoire.
nouveau(p) ;
q p ;
a b
p p^ q q^
a b
p p^, q^ q
a a
p p^ q q^
Cette illustration montre combien il est important de savoir faire la différence entre la valeur
d’un pointeur (une adresse) et la valeur de la variable que ce pointeur référence (une donnée
de type quelconque mais approprié).
Question de cours
Exercice 8.1
Citez deux différences entre les structures de données statiques et les structures de données
dynamiques.
Exercice 8.2
Définir des pointeurs pour pointer sur des variables de type entier, réel, caractère, booléen,
vecteur ou article.
t
Définition d’une cellule i
On définit le type d’une cellule de la manière e
suivante :
type
télément = article
d
champ1 : Type1 ; o
.
. n
.
champn : Typen ; n
fin ;
é
pélément = ^tcellule ;
tcellule = article ;
e
donnée : télément ;
suivant : pélément ;
fin ;
Une variable p de type pélément peut être représentée schématiquement par :
p^
p^.donnée p^.suivant
L’adresse de la cellule est rangée dans la variable p. Par abus de langage, on dira : cellule
d’adresse p ou cellule pointée par p. La cellule contient deux champs : un champ de nom
donnée contenant une information et un champ de nom suivant contenant une adresse. Si p
ne contient pas la valeur nil, p^.donnée permet d’accéder à l’information de la cellule
d’adresse p et p^.suivant permet d’accéder à l’adresse contenue dans la cellule d’adresse p.
a b c d
Première version
Dans cette version, on crée la liste à partir de son dernier élément et du premier élément du
fichier. Les éléments sont donc enregistrés dans l’ordre inverse de lecture.
procédure créerliste(nomfichier : chaîne ; var liste : pélément) ;
variable
dernier, courant : pélément ;
élément : télément ;
f : fichier de télément ;
début
relire(f, nomfichier) ;
dernier nil ;
tantque non fin(f) faire
début
nouveau(courant) ;
lire(f, élément) ;
courant^.donnée élément ;
courant^.suivant dernier ;
dernier courant ;
fin ;
liste dernier;
fermer(f) ;
fin ;
La construction de la liste se fait de la manière suivante. Au départ, on met dernier à nil en
exécutant l’instruction,
dernier nil ;
dernier
On suppose qu’à une étape donnée on a une liste de la forme suivante et que l’on a encore des
éléments à ajouter dans la liste.
dernier
nouveau(courant) ;
courant dernier
courant^.donnée elem ;
val
courant dernier
courant^.suivant dernier ;
courant dernier
dernier courant ;
dernier
Ces étapes sont répétées pour tous les éléments du fichier. À la fin on exécute l’affectation
liste dernier, et la liste est créée.
liste
dernier
Deuxième version
Dans cette version, les éléments du fichier sont enregistrés dans l’ordre de lecture ; le premier
élément du fichier sera le premier élément de la liste.
procédure créerliste(nomfichier : chaîner ; var liste : pélément) ;
variable
dernier, courant : pélément ;
élément : télément ;
liste
dernier
O
n suppose maintenant que l’on une liste de la forme ci-dessous. Le processus d’insertion
d’une nouvelle cellule de la manière suivante :
liste
??
dernier courant
nouveau(courant) ;
liste
dernier courant
dernier^.suivant courant ;
liste
dernier courant
dernier courant ;
liste
dernier courant
Une fois que la cellule est créée et correctement liée à la liste, la valeur de l’élément à insérer
dans la liste peut ensuite être affectée au champ donnée de la nouvelle cellule par le moyen de
l’instruction :
courant^.donnée élément ;
Troisième version
Dans l’algorithme ci-dessus, on a eu à traiter différemment la première cellule des autres
cellules de la liste. Ceci parce qu’elle est pointée par liste au lieu du champ suivant de la
cellule précédente. On peut simplifier cet algorithme en incluant un élément factice en tête de
liste, appelé sentinelle de tête de liste. La figure ci-dessous présente une liste comprenant une
sentinelle en tête de liste.
liste
??
Les données contenues dans la sentinelle de tête ne sont pas significatives. Le champ suivant
de la sentinelle pointe sur le premier élément effectif de la liste. Une liste comprenant
uniquement une sentinelle de tête ayant le champ suivant égal à nil est considérée comme
vide. Si la liste utilise une sentinelle de tête, alors on peut simplifier la procédure de création
d’une liste chaînée de la manière suivante :
Il s’agit de créer une liste chaînée à partir des éléments d’un vecteur d’éléments de type
télément. A partir des éléments liste[i] d’un vecteur liste, on crée la liste à partir de son
dernier élément et du dernier élément du vecteur. On obtient ainsi les éléments dans le même
ordre.
procédure créerlistevecteur(liste : vélément ; n : entier ; var liste : pélément) ;
variable
dernier, courant : pélément ;
i : entier;
début
dernier nil ;
i n ;
tantque i 1 faire
début
nouveau(courant) ;
courant^.suivant dernier ;
dernier courant ;
courant^.donnée liste[i] ;
i i –1 ;
fin ;
liste dernier;
fin ;
Dans cette version, les éléments sont enregistrés dans l’ordre inverse de la lecture.
procédure créerliste(var liste : pélément) ;
variable
dernier, courant : pélément ;
stop : booléen ;
ch : caractère ;
début
dernier nil ;
stop faux ;
tantque non stop faire
début
nouveau(courant) ;
courant^.suivant dernier ;
dernier courant ;
lire(courant^.donnée) ;
écrire('Avez-vous un autre élément à ajouter (O ou N) ? ') ;
lire(ch) ;
stop ch = 'N' ;
fin ;
liste dernier ;
fin ;
Deuxième version
Dans cette version, les éléments sont enregistrés dans l’ordre de lecture.
procédure créerliste(var liste : pélément) ;
variable
dernier, courant : pélément ;
stop : booléen ;
ch : caractère ;
début
nouveau(liste) ;
lire(liste^.donnée) ;
liste^.suivant nil ;
dernier liste ;
écrire('Avez-vous un autre élément à ajouter (O ou N) ? ') ;
lire(ch) ;
stop ch = 'N' ;
tantque non stop faire
début
nouveau(courant) ;
dernier^.suivant courant ;
dernier courant ;
lire(courant^.donnée) ;
écrire('Avez-vous un autre élément à ajouter (O ou N) ? ') ;
lire(ch) ;
stop ch = 'N' ;
fin;
dernier^.suivant nil ;
fin ;
début
si liste nil alors
début
traiter(liste) ;
parcoursgd(liste^.suivant) ;
fin ;
fin ;
Parcours de droite à gauche
procédure parcoursdg(liste : pélément) ;
début
si liste nil alors
début
parcoursdg(liste^.suivant) ;
traiter(liste) ;
fin ;
fin ;
Schéma itératif
Le deuxième parcours n’est pas simple à obtenir sous forme itérative. Dans le cas général, il
est nécessaire de disposer d’une pile, comme nous le verrons dans les chapitres suivants. Nous
nous limiterons donc, pour le moment, au premier parcours qui correspond à ce que l’on
appelle une récursivité terminale.
procédure parcoursgd(liste : pélément) ;
variable
courant : pélément ;
début
courant liste ;
tantque courant nil faire
début
traiter(courant) ;
courant courant^.suivant ;
fin ;
fin ;
On utilise le parcours de gauche à droite. On écrit donc les éléments de la liste à partir du
premier élément.
L’algorithme est le suivant :
procédure écrirelistegd(liste : pélément) ;
début
si liste nil alors
début
écrire(liste^.donnée) ;
écrirelistegd(liste^.suivant) ;
fin ;
fin ;
Schéma itératif
On utilise la version itérative du parcours de la gauche vers la droite.
L’algorithme est alors le suivant :
procédure écrirelistegd(liste : pélément) ;
variable
courant : pélément ;
début
courant liste ;
tantque courant nil faire
début
écrire(courant^.donnée) ;
courant courant^.suivant ;
fin ;
fin ;
Exercices d’apprentissage
Exercice 8.3
Que fait la procédure suivante ?
procédure parcoursliste(liste : pélément) ;
début
mais cette fois avec la liste dont le pointeur de tête est liste^.suivant. La longueur de la liste
est alors fixée à un plus la longueur de la liste dont le pointeur de tête est liste^.suivant.
L’algorithme est alors le suivant :
fonction longueurliste(liste : pélément) : entier ;
début
si liste = nil alors
longueurliste 0
sinon
longueurliste 1 + longueurliste(liste^.suivant) ;
fin ;
nombrefois 0
sinon
si liste^.donnée = élément alors
nombrefois 1 + nombrefois(liste^.suivant, élément)
sinon
nombrefois nombrefois(liste^.suivant, élément) ;
fin ;
Exercices d’apprentissage
Exercice 8.5
Écrire un algorithme qui retourne la valeur de la dernière cellule d‘une liste linéaire chaînée.
Exercice 8.6
Écrire un algorithme qui détermine le plus grand élément dans une liste linéaire chaînée.
Exercice 8.7
Écrire un algorithme qui détermine le plus petit élément dans une liste linéaire chaînée.
Exercice 8.8
Écrire une fonction qui prend en entrée une liste chaînée et retourne la position du plus grand
élément de la liste. On suppose que le plus grand élément est unique et que la fonction
retourne zéro si la liste est vide.
Exercice 8.9
Écrire une fonction qui prend en entrée une liste chaînée et retourne la position du plus petit
élément de la liste. On suppose que le plus grand élément est unique et que la fonction
retourne zéro si la liste est vide.
Exercice 8.10
Écrire une procédure qui prend en entrée une liste chaînée et retourne le plus grand et le
second plus grand éléments de la liste.
Exercice 8.11
Écrire une fonction qui prend en entrée une liste chaînée de nombres réels et retourne la
somme des éléments de la liste.
Schéma itératif
On peut transposer l’algorithme que nous avons donné sur les fichiers séquentiels. On obtient
alors l’algorithme suivant :
procédure accesk(liste : pélément ; k : entier ;
var pk : pélément ; var trouvé : booléen) ;
variable
courant : pélément ;
i : entier ;
début
courant liste ;
i 1 ;
tantque (i < k) et (courant nil) faire
début
i i + 1 ;
courant courant^.suivant ;
fin ;
trouvé (i = k) et (courant nil)
pk courant ;
fin ;
Deuxième version
début
courant liste
tantque (courant nil) et alors (courant^.donnée élément) faire
courant courant^.suivant ;
trouvé courant nil ;
pv courant ;
fin ;
Ou en évitant d’utiliser le connecteur « et alors »
procédure accesvaleur(liste : pélément ; élément : télément ;
var pv : pélément ; var trouvé : booléen) ;
variable
courant : pélément ;
début
courant liste ;
trouvé faux ;
tantque (non trouvé ) et (courant nil) faire
début
si courant^.donnée = élément alors
trouvé vrai
sinon
courant courant^.suivant ;
fin ;
si trouvé alors
pv courant
sinon
pv nil;
fin ;
On peut également réécrire la fonction ci-dessus pour qu’elle retourne un pointeur sur la
première occurrence de élément et nil si élément n’est pas présent dans la liste. Ce qui permet
d’éliminer le paramètre trouvé. On obtient alors l’algorithme :
fonction pointeurval(liste : pélément ; élément : télément) : pélément ;
variable
courant : pélément ;
trouvé : booléen ;
début
courant liste ;
trouvé faux ;
tantque (non trouvé) et (courant nil) faire
début
si courant^.donnée = élément alors
trouvé faux
sinon
courant courant^.suivant ;
fin ;
si trouvé alors
pointeurval courant
sinon
pointeurval nil;
fin ;
Schéma récursif
En utilisant une convention analogue à celle que nous avons utilisée dans le cas de l’accès par
position, on peut écrire cet algorithme sous la forme d’une fonction récursive qui retourne
l’adresse de la première occurrence de élément s’il existe dans la liste et nil si élément
n’existe pas dans la liste.
L’algorithme est alors le suivant :
fonction pointeurval(liste : pélément ; élément : télément) : pélément ;
début
si liste = nil alors
pointeurval liste
sinon
si liste^.donnée = élément alors
pointeurval liste
sinon
pointeurval pointeurval(liste^.suivant, élément)
fin ;
On peut aussi mettre en facteur l’instruction « pointeurval liste » ; à l’aide du connecteur
« ou sinon » mais surtout pas à l’aide de « ou » à cause des problèmes d’incohérence qui
peuvent survenir.
L’algorithme devient alors :
fonction pointeurval(liste : pélément ; élément : télément) : pélément;
début
si (liste = nil) ou sinon (liste^.donnée = élément) alors
pointeurval liste
sinon
pointeurval pointeurval(liste^.suivant, élément)
fin ;
Exercice d’apprentissage
Exercice 8.12
On désire connaître l’adresse de la dernière occurrence d’une valeur dans une liste. Écrire
sous forme itérative et sous forme récursive la fonction qui délivre cette adresse ou nil si la
valeur n’est pas présente dans la liste.
c) Accès associatif dans une liste triée
Définition d’une liste triée
Une liste vide est triée,
Une liste composée d’un seul élément est triée,
Une liste de plus de deux éléments est triée si tous les éléments consécutifs vérifient la
relation : liste nil, liste^.suivant nil, liste^.donnée liste^.suivant^.donnée.
Une application immédiate de cette définition est la fonction suivante qui vérifie qu’une liste
est triée par ordre croissant.
Schéma récursif
Le raisonnement est le suivant : on départ au regarde si la liste est vide ou si elle est composée
d’une seule cellule. Dans l’un et dans l’autre cas, on retourne la valeur vrai. Si la liste
comprend plus de deux cellules, on compare l’élément courant à son suivant. Si l‘élément
courant est supérieur à son suivant, on retourne la valeur faux sinon on continue l’exploration
de la liste dont le pointeur de tête se trouve dans courant^.suivant.
L’algorithme est alors le suivant :
fonction listetrié(liste : pélément) : booléen ;
début
si liste = nil alors
listetrié vrai
sinon
si liste^.suivant = nil alors
listetrié vrai
sinon
si liste^.donnée > liste^.suivant^.donnée alors
listetriée faux
sinon
listetrié listetrié(liste^.suivant) ;
fin ;
Schéma itératif
L’approche consiste à parcourir la liste en comparant à chaque étape l’élément courant à son
suivant. Si courant^.donnée courant^.suivant^.donnée, on continue le parcours sinon on
arrête. La liste est triée si on sort de la boucle avec courant^.suivant = nil.
L’algorithme est alors le suivant :
fonction listetrié(liste : pélément) : booléen ;
variable
courant : pélément ;
début
si liste = nil alors
listetrié vrai
sinon
début
courant liste ;
tantque (courant^.suivant nil) et alors
(courant^.donnée courant^.suivant^.donnée faire
courant courant^.suivant ;
fin ;
listetrié courant^.suivant = nil ;
fin ;
ou en évitant d’utiliser le connecteur « et alors »
fonction listetrié(liste : pélément) : booléen ;
variable
courant : pélément ;
trié : booléen ;
début
si liste = nil alors
listetrié vrai
sinon
début
courant liste ;
trié vrai ;
tantque (courant^.suivant nil) et trié faire
si (courant^.donnée courant^.suivant^.donnée) alors
courant courant^.suivant
sinon
trié faux ;
fin ;
listetrié courant^.suivant = nil ;
fin ;
Nous pouvons maintenant écrire l’algorithme d’accès à une valeur dans une liste triée.
Schéma récursif
Le raisonnement est le suivant : si la liste est vide, on retourne la valeur faux sinon on
compare la valeur de la cellule courante à la valeur cherchée. Si elle est supérieure, on
retourne la valeur faux. Si elle sont égales, on retourne l’adresse de la cellule courante. Si elle
est strictement inférieure, on poursuit la recherche dans la liste dont le pointeur de tête se
trouve dans le champ suivant de la cellule courante.
L’algorithme est alors le suivant :
fonction pointeurval(liste : pélément ; élément : télément) : pélément ;
début
si liste = nil alors
pointeurval nil
sinon
si liste^.donnée < élément alors
pointeur pointeurval(liste^.suivant, élément)
sinon
si liste^.donnée > élément alors
pointeurval nil
sinon
pointeurval liste ;
fin ;
sinon
si liste^.donnée < élément alors
pointeur pointeurval(liste^.suivant, élément)
sinon
pointeurval liste ;
fin ;
Exercices d’apprentissage
Exercice 8.13
Écrire, sous forme récursive et sous forme itérative, un algorithme qui vérifie qu’une liste est
triée par ordre décroissant.
Exercice 8.14
Écrire, sous forme itérative, un algorithme de recherche de la première occurrence d’une
valeur dans une liste triée.
Exercice 8.15
Écrire, sous forme itérative et sous forme récursive, un algorithme de recherche de la dernière
occurrence d’une valeur dans une liste triée.
Ce problème est une généralisation de la recherche associative dans une liste linéaire : on ne
cherche plus une seule valeur dans une liste mais un ensemble de valeurs organisées en sous-
liste. On peut formaliser le problème de la manière suivante :
Soient une liste [liste] et une sous-liste [sliste]. On admettra que la sous-liste [sliste] n’est
jamais vide. La liste [sliste] est une sous-liste de la liste [liste] si [liste] est la concaténation de
trois sous-listes telles que :
[liste] = [listea] || [sliste] || [listeb].
Dans le cas où [listea] est vide on dit que [sliste] est un préfixe de [liste].
De même, la sous-liste [sliste] || [listeb] est un suffixe de [liste].
Supposons que nous disposions d‘une fonction préfixe() qui permet de déterminer si une sous-
liste [sliste] est un préfixe d’une liste [liste]. On écrira cette fonction après.
Schéma itératif
L’algorithme consiste à parcourir la liste en examinant à chaque étape si sliste est un préfixe
la sous-liste restante.
Schéma récursif
L’algorithme est le suivant :
fonction recherchesliste(liste, sliste : pélément) : booléen ;
début
si liste = nil alors
recherchesliste faux
sinon
si préfixe(liste, sliste) alors
recherchesliste vrai
sinon
recherchesliste recherchesliste(liste^.suivant, sliste) ;
fin ;
Il faut effectuer un parcours simultané des deux sous-listes en comparant les éléments situés à
la même position.
L’algorithme est alors le suivant :
fonction préfixe(liste, sliste : pélément) : booléen ;
variable
p, sp : pélément ;
début
p liste ;
sp liste ;
tantque (p nil) et (sp nil) et alors (p^.donnée = sp^.donnée) faire
début
p p^.suivant ;
sp sp^.suivant ;
fin ;
préfixe sp = nil ;
fin ;
Schéma récursif
liste
(b)
(a)
p
élément à
insérer
Il faut d’abord créer une cellule d’adresse p, par exécution de l’instruction nouveau(p).
Ensuite, le champ donnée reçoit la valeur de l’élément à insérer, pour terminer par la
réalisation des deux liaisons (a) et (b) dans cet ordre. On notera que les éléments suivants sont
décalés automatiquement d’une position (sans qu’on ait à les déplacer physiquement) et que
l’algorithme est également correct dans le cas où la liste est vide.
L’algorithme est alors la suivant :
procédure insertête(var liste : pélément ; élément : pélément) ;
variable
courant : pélément ;
début
nouveau(courant) ;
courant^.donnée élément ;
courant^.suivant liste ;
liste courant ;
fin ;
b) Insertion d’un élément en fin de liste
Schéma itératif
Si la liste est vide, on est ramené à une insertion en tête de liste. Dans le cas général, on
supposera que la liste n’est pas vide. On peut schématiser cette insertion de la manière
suivante :
liste der
(a)
élément à
p insérer
Après avoir créé une nouvelle cellule d’adresse p contenant la valeur de l’élément à insérer,
on doit effectuer la liaison (a). Pour pouvoir effectuer cette liaison, il faut connaître l’adresse
de la dernière cellule de la liste. Nous allons écrire une fonction appelée dernier qui délivre
l’adresse de la dernière cellule d’une liste.
Schéma itératif
Première version. On effectue un parcours de gauche à droite tous les éléments de la liste en
conservant à chaque étape l’adresse de la cellule précédente.
L’algorithme est alors le suivant :
fonction dernier(liste : pélément) : pélément ;
variable
courant, précédent : pélément ;
début
courant liste ;
précédent courant ;
tantque courant nil faire
début
précédent courant ;
courant courant^.suivant ;
fin ;
dernier précédent ;
fin ;
Deuxième version. Cette deuxième version consiste à ne pas utiliser la variable auxiliaire
précédent en remarquant tout simplement que les variables courant et courant^.suivant
contiennent les adresses de deux cellules consécutives;
L’algorithme est alors le suivant :
fonction dernier(liste : pélément) : pélément ;
variable
courant : pélément ;
début
courant liste ;
tantque courant^.suivant nil faire
courant courant^.suivant ;
dernier courant ;
fin ;
Schéma récursif
Aux deux versions itératives, on peut faire correspondre deux versions récursives. Nous
n’écrivons que la deuxième version et laissons au lecteur le soin d’écrire la première.
fin ;
Nous pouvons maintenant écrire l’algorithme d’insertion en fin de liste.
Schéma itératif
fin ;
c) Insertion d’un nouvel élément à la kième place
(k-1) (k)
(b) (a)
p élément à insérer
liste
(b)
(a)
Schéma itératif
Première version
L’insertion d’un élément à la kème position consiste à créer les liaisons (a) et (b) dans cet
ordre. Les éléments suivants seront alors automatiquement décalés d’une position sans qu’on
ait à les déplacer physiquement. Pour réaliser la liaison (b), il faut connaître l’adresse de la
cellule précédente. L’insertion n’est possible que si k [1..n+1], où n est le nombre
d’éléments de la liste. Il faudra prévoir d’abord l’insertion en tête de liste (k = 1) car elle
modifie l’adresse de la liste. Dans le cas général, on va se servir de la fonction pointeurk qui
délivre un pointeur sur le kème élément d’une liste.
L’algorithme est alors le suivant :
procédure insertionk(var liste : pélément ; k : entier ;
élément : télément ; var possible : booléen) ;
variable
courant, précédent : pélément ;
début
si (k = 1) alors
début
insertête(liste, élément) ;
possible vrai ;
fin
sinon
début
précédent pointeurk(liste, k-1) ;
si (précédent = nil) alors
possible faux
sinon
début
nouveau(courant) ;
courant^.donnée élément ;
courant^.suivant précédent^.suivant ;
précédent^.suivant courant ;
possible vrai ;
fin ;
fin ;
fin ;
Deuxième version
On peut également effectuer le raisonnement suivant : insérer un élément à la kème position
revient à insérer cet élément en tête de la liste dont le pointeur de tête se trouve dans le champ
adresse du (k-1)ème élément.
On peut aussi remarquer que l’exécution des quatre dernières instructions correspond à une
insertion en tête de la liste dont le pointeur de tête se trouve dans précédent^.suivant.
L’algorithme est alors le suivant :
procédure insertk(var liste : pélément ; k : entier ;
élément : télément ; var possible : booléen) ;
variable
précédent : pélément ;
début
si k = 1 alors
début
insertête(liste, élément) ;
possible vrai ;
fin
sinon
début
précédent pointeurk(liste, k-1) ;
si précédent = nil alors
possible faux
sinon
début
insertête(précédent^.suivant, élément) ;
possible vrai ;
fin ;
fin ;
fin ;
Schéma récursif
Nous utilisons le raisonnement correspondant à la deuxième version du schéma itératif.
L’algorithme est alors le suivant :
procédure insertk(var liste : pélément ; k : entier ;
élément : télément ; var possible : booléen) ;
variable
précédent : pélément ;
début
si k = 1 alors
début
insertête(liste, élément) ;
possible vrai ;
fin
sinon
si liste = nil alors
possible faux
sinon
insertk(liste^.suivant, k-1, élément, possible) ;
fin ;
8.5.1.4 Insertion d’un élément après la première occurrence d’une valeur
L’algorithme est assez simple : il suffit de connaître l’adresse de la cellule qui contient la
première occurrence de la valeur donnée pour pouvoir réaliser, d’après le schéma ci-dessous,
les liaisons (a) et (b) dans cet ordre.
liste
première
occurrence
val
(b) (a)
p élément à insérer
Schéma itératif
On utilise la fonction pointeurval pour connaître l’adresse de la cellule contenant la première
occurrence de la valeur. Ensuite, si cette valeur existe, il suffit d’effectuer une insertion en
tête de la liste dont le pointeur de tête se trouve dans le champ adresse de la cellule qui
contient la valeur.
L’algorithme est alors le suivant :
procédure insertaprès(liste : pélément ; val, élément : télément ; var possible : booléen) ;
variable
courant : pélément ;
début
L’algorithme consiste donc à parcourir la liste [la] et à insérer élément en tête de la liste [lb],
dont le pointeur de tête se trouve dans le champ adresse de la cellule d’adresse dernier(la).
Schéma récursif
La raisonnement est le suivant : lorsqu’on entre dans la procédure, on regarde si la liste est
vide ou si la donnée qui se trouve dans la première cellule est supérieure ou égale à la valeur à
insérer. Dans l’un ou dans l’autre cas, on procède à une insertion en tête de liste. Si par contre,
la donnée qui se trouve dans la première cellule est inférieure à la valeur à insérer, on appelle
encore la procédure mais cette fois avec liste^.suivant comme premier paramètre.
L’algorithme est alors le suivant :
procédure insertrié(var liste : pélément ; élément : télément) ;
début
si (liste = nil) ou sinon (liste^.donnée élément) alors
insertête(liste, élément)
sinon
insertrié(liste^.suivant, élément) ;
fin ;
Schéma itératif
Il faut effectuer un parcours séquentiel afin de déterminer les listes [la] et [lb]. On utilise une
variable auxiliaire qui devra contenir, lors du parcours, l’adresse de la cellule précédente. Le
raisonnement est alors identique à celui du schéma récursif. Supposons traités les premiers
éléments de la liste et qu’ils soient tous inférieurs à la valeur à insérer et que précédent pointe
sur la cellule précédente :
liste précédent p
L’itération doit porter sur deux conditions : l’une sur courant nil et l’autre sur élément >
courant^.donnée. On utilise le connecteur « et alors » pour éviter les problèmes de cohérence.
L’algorithme est alors le suivant :
Première version
procédure insertrié(var liste : pélément ; élément : télément) ;
variable
courant, précédent : pélément ;
début
si (liste = nil) ou sinon (liste^.donnée élément) alors
insertête(liste, élément)
sinon
début
précédent liste ;
courant liste^.suivant ;
tantque (courant nil) et alors (élément > courant^.donnée) faire
début
précédent courant ;
courant courant^.suivant ;
fin ;
insertête(précédent^.suivant, élément) ;
fin ;
fin ;
Deuxième version
Dans le cas d’une liste vide [p], on peut considérer par convention que D<p n’est pas défini et
que [<p] < élément est toujours vrai. À ce moment là, l’initialisation peut se limiter à courant
liste ; afin de préserver l’adresse de la première cellule de la liste.
Ensuite, on effectue un parcours de [liste] pour déterminer les listes [la] et [lb]. Enfin, on
réalise l’insertion en traitant les deux cas particuliers.
L’algorithme est alors les suivant :
procédure insertrié(var liste : pélément ; élément : télément) ;
variable
courant, précédent : pélément ;
début
courant liste ;
tantque (courant nil) et alors (élément > courant^.donnée) faire
début
précédent courant ;
courant courant^.suivant ;
fin ;
si courant = liste alors
insertête(liste, élément)
sinon
insertête(précédent^.suivant, élément)
fin ;
Exercice 8.19
Réécrire la procédure ci-dessus dans le cas d’une liste avec une sentinelle en tête de liste.
8.5.1.6 Insertion d’une sous-liste dans une liste
On vient de traiter le cas particulier de l’insertion d’un élément, c’est-à-dire l’insertion d’une
sous-liste de longueur un. On peut aisément généraliser cette insertion dans le cas d’une sous-
liste de longueur quelconque. Il faut alors établir les liaisons (a) et (b) dans le schéma ci-
dessous.
Les algorithmes sont semblables aux précédents. Il faut en plus chercher l’adresse du dernier
élément de la sous-liste afin d’établir la liaison (a). L’insertion peut s’effectuer par position ou
par rapport à l’occurrence d’une valeur. Nous choisissons le cas de l’insertion associative
après la première occurrence de la valeur val.
liste
(b) (a)
sliste
Comme pour les insertions, on a le cas particulier de la suppression de la première cellule qui
a pour conséquence de modifier l’adresse de la liste. Dans la cas général, il suffit, comme le
montre le schéma, de modifier le contenu d’un seul pointeur.
liste
élément à
supprimer
L’élément à supprimer peut être déterminé par sa position (suppression par position) ou par sa
valeur (suppression associative).
8.5.2.1 Suppression du premier élément
On suppose que la liste n’est pas vide. Le schéma est le suivant :
liste
élément à
supprimer
On suppose également que l’on n’a plus besoin de l’élément à supprimer et que l’on rend la
cellule supprimée au réservoir des cellules. Il faut donc préserver l’adresse de la tête de liste
avant d’effectuer la modification de cette adresse.
L’algorithme est alors le suivant :
procédure supprimetête(var liste : pélément) ;
variable
courant : pélément ;
début
courant liste ;
liste liste^.suivant ;
libérer(courant);
fin ;
8.5.2.2 Suppression par position
On veut supprimer le kème élément dans une liste linéaire chaînée et on peut, comme dans le
cas des insertions, donner une version itérative et une version récursive.
Schéma itératif
Comme pour l’insertion, il faut déterminer l’adresse de la cellule qui précède celle de la
cellule à supprimer ; c’est-à-dire l’adresse de la (k-1)ème cellule qui sera obtenue par un appel
de la fonction pointeurk. Ensuite, si la kème cellule existe, on modifie la valeur de l’adresse
contenue dans le champ suivant de la cellule d’adresse précédent comme indiqué sur le
schéma :
liste précédent pk
(k)
élément à
supprimer
précédent : pélément ;
début
si (liste nil ) et (k = 1) alors
début
supprimetête(liste) ;
possible vrai ;
fin
sinon
début
possible faux ;
précédent pointeurk(liste, k-1) ;
si (précédent nil) et alors (précédent^.suivant nil) alors
début
possible vrai ;
supprimetête(précédent^.suivant)
fin ;
fin ;
fin ;
Schéma récursif
Le raisonnement est le suivant : lorsqu’on entre dans la procédure, on regarde si la liste est
vide. Si elle est effet vide, on sait que l’on a rien à supprimer. Si par contre la liste n’est pas
vide, on regarde si k = 1, dans ce cas on procède à une insertion en tête de liste. Si k > 1, on
appelle encore la procédure, mais cette fois avec liste^.suivant comme premier paramètre et k
–1 comme deuxième paramètre. Ce qui nous conduira au bout d’un nombre fini d’étapes à
l’un des deux cas de base ci-dessus.
L’algorithme est alors le suivant :
procédure supprimek(var liste : pélément ; k : entier ; var possible : booléen) ;
début
si liste = nil alors
possible faux
sinon
si k = 1 alors
début
possible vrai ;
supprimetête(liste) ;
fin
sinon
supprimek(liste^.suivant, k - 1, possible)
fin ;
Exercices d’apprentissage
Exercice 8.21
Écrire sous forme récursive et sous forme itérative l’algorithme de suppression de toutes les
occurrences d’une valeur dans une liste.
Exercice 8.22
Écrire sous forme récursive et sous forme itérative l’algorithme de suppression de la première
occurrence d’une valeur dans une liste triée.
Exercice 8.23
Écrire sous forme récursive et sous forme itérative l’algorithme de suppression de toutes les
occurrences d’une valeur dans une liste triée.
Exercice 8.24
Que fait la procédure suivante ?
procédure Mystère(p : pélément) ;
variable
q : pélément ;
début
q p^.suivant ;
si q nil alors
début
p^ q^ ;
p^.suivant q^.suivant ;
libérer(q) ;
fin ;
fin ;
Pourquoi l’instruction si est-elle nécessaire dans cette procédure ?
liste
(a)
liste2
Cas particuliers :
[liste1] est vide, la liste concaténation est composée uniquement de [liste2].
[liste2] est vide, la liste concaténation est composée uniquement de [liste1].
[liste1] et [liste2] sont vides, la liste concaténation est également vide.
Dans le cas général, pour réaliser la liaison (a), il faut connaître l’adresse de la dernière cellule
de [liste1] qui sera obtenue par appel de la fonction dernier.
début
si liste nil alors
début
inserfin(copie, liste^.donnée);
copieliste(liste^.donnée, copie);
fin ;
fin ;
L’appel doit être de la forme :
copie nil ;
copieliste(liste, copie);
Deuxième version
On utilise le deuxième parcours qui consiste à construire [copie] à partir du dernier élément de
[liste]. Il faut donc faire, à chaque pas une insertion en tête de liste. On utilise pour cela la
procédure insertête.
L’algorithme est alors le suivant :
procédure copieliste(liste : pélément ; var copie : pélément) ;
début
si liste = nil alors
copie nil
sinon
début
copieliste(liste^.suivant, copie);
insertête(copie, liste^.donnée);
fin ;
fin ;
On notera qu’ici copie est un paramètre résultat et que l’initialisation est réalisée à l’intérieur
de la procédure, alors que, dans l’algorithme précédent, copie est un paramètre donnée-
résultat et l’initialisation doit être effectuée à l’appel de la procédure. Cette deuxième version
est plus efficace car on dispose du pointeur de tête de liste pour réaliser l’insertion. Dans le
cas précédent, il faut à chaque insertion effectuer un parcours de [copie] pour réaliser cette
insertion.
Schéma itératif
Nous donnons la traduction de la première version récursive.
L’algorithme est alors le suivant :
procédure copieliste(liste : pélément ; var copie : pélément) ;
variable
courant : pélément ;
début
courant liste ;
copie nil ;
tantque courant nil faire
début
inserfin(copie, courant^.donnée);
courant courant^.suivant;
fin ;
fin ;
On notera que l’initialisation est, cette fois, réalisée à l’intérieur de la procédure et que copie
est un paramètre résultat. On peut améliorer l’efficacité de l’algorithme en gérant l’adresse de
la dernière cellule de [copie].
L’algorithme est alors le suivant :
procédure copieliste(liste : pélément ; var copie : pélément) ;
variable
courant, dernier : pélément ;
début
courant liste ;
copie nil ;
si courant nil alors
début
insertête(copie, courant^.donnée) ;
der copie ;
courant courant^.suivant ;
tantque courant nil faire
début
insertfin(der, courant^.donnée) ;
der der^.suivant ;
courant courant^.suivant ;
fin ;
fin ;
fin ;
Nous pouvons donc donner maintenant une nouvelle version de l’algorithme de concaténation
de deux listes.
procédure concaliste(liste1, liste2 : pélément ; var liste : pélément) ;
variable
p1, p2 : pélément ;
début
copieliste(liste1, p1) ;
copieliste(liste2, p2) ;
concaliste(p1, p2, liste) ;
fin ;
Si on effectue un parcours de [liste] à partir de son premier élément, on doit construire la liste
miroir à partir de son dernier élément. On doit donc effectuer des insertions en tête de la liste
en cours de construction. Comme précédemment, l’initialisation de la liste miroir doit être
effectuée au moment de l’appel.
L’algorithme est alors le suivant :
procédure listemiroir(liste : pélément; var copie : pélément) ;
début
si liste nil alors
début
insertête(copie, liste^.donnée);
listemiroir(liste, copie);
fin ;
fin ;
Deuxième version
Cette deuxième version correspond à un parcours de droite à gauche de [liste]. On doit donc
construire la liste miroir à partir de son premier élément et utiliser des insertions en fin de la
liste miroir en cours de construction. Dans ce cas, l’initialisation doit être faite à l’intérieur de
la procédure.
L’algorithme est alors le suivant :
procédure listemiroir(liste : pélément ; var copie : pélément) ;
début
si liste = nil alors
copie nil
sinon
début
listemiroir(liste^.suivant, copie);
inserfin(copie, liste^.donnée);
fin ;
fin ;
8.7 Conclusion
Le coût d’un algorithme sur les listes linéaires chaînées est évaluée en fonction :
de la place mémoire occupée,
du nombre de pointeurs parcourus ou d’affectation de pointeurs.
La représentation chaînée est plus encombrante que la représentation vecteur et l’accès à un
élément est également moins rapide. Par contre, une mise à jour ne nécessitant aucune copie,
son coût est moins élevé que dans le cas de la représentation vecteur. On choisit une
représentation chaînée chaque fois que les mises à jour sont plus importantes que les
consultations et une représentation vecteur chaque fois que les consultations l’emportent sur
les mises à jour. Dans la pratique, on utilise très souvent des structures mixtes composées de
listes chaînées et de vecteurs.
Exercices de recherche
Exercice 8.25
Les listes circulaires ou anneaux. Les listes circulaires ou anneaux sont des listes linéaires
dans lesquelles le dernier élément pointe sur le premier. Il n’y a donc ni premier ni dernier,
mais il est nécessaire de garder un seul point d’entrée dans l’anneau, que nous appellerons
liste, pour faciliter l’écriture des algorithmes sur les anneaux.
Ces listes sont très utilisées en programmation système et dans les systèmes de gestion de
bases de données. Une liste circulaire permet de chaîner entre eux des éléments possédant une
même propriété. Il suffit alors de connaître un élément possédant cette propriété pour obtenir,
par parcours de la liste circulaire, tous les autres.
1. Écrire un algorithme pour créer une liste circulaire à partir des éléments d’un fichier.
2. Écrire un algorithme de parcours d’une liste circulaire.
3. Écrire un algorithme qui délivre l’adresse de la cellule de valeur val dans un anneau ou
nil si val n’est présente dans l’anneau.
4. Écrire un algorithme de suppression de la cellule de valeur val dans une liste
circulaire.
5. Écrire un algorithme d’insertion d’un nouvel élément avant une valeur val dans une
liste circulaire.
6. Écrire un algorithme d’insertion d’un nouvel élément après une valeur val dans une
liste circulaire.
Exercice 8.26
Une liste doublement chaînée est une liste dans laquelle chaque cellule a un pointeur sur la
cellule précédente et un pointeur sur la cellule suivante.
1. Écrire un algorithme d’insertion d’un élément avant une valeur val.
2. Écrire un algorithme d’insertion d’un élément après une valeur val
3. Écrire un algorithme d’insertion d’un élément à la kème place.
4. Écrire un algorithme d’insertion de la valeur elem après chaque occurrence de la
valeur val.
5. Écrire un algorithme de suppression d’une valeur val.
6. Écrire un algorithme de suppression de la kème cellule.
7. Écrire un algorithme de suppression de toutes les occurrences de la valeur val.
Exercice 8.27
Les piles. La structure de pile est une structure de liste particulière. Contrairement aux
fichiers et aux vecteurs, elle ne sert généralement pas à garder de façon plus ou moins
définitive des informations. On s’intéresse plutôt à la suite des états de la pile et on utilise le
fait que le dernier élément ajouté se trouve au sommet de la pile, afin de pouvoir être atteint le
premier.
On peut donner une image du fonctionnement de cette structure avec une pile d’assiettes : on
peut ajouter et enlever des assiettes au sommet de la pile ; toute insertion ou retrait d’une
assiette au milieu de la pile est une opération qui comporte des risques.
La stratégie de gestion d’une pile est « dernier arrivé, premier servi ». En anglais on dira
« last-in first-out ou en abrégé LIFO ».
Une pile est une liste linéaire telle que :
les insertions sont toujours effectuées en tête de liste,
les suppressions sont toujours effectuées en de tête de liste.
Dans ces études de cas, nous appliquerons les algorithmes classiques sur les listes chaînées à
quelques structures de données complexes.
Bibliothèque
On souhaite gérer le stock des livres d’une bibliothèque universitaire. Pour chaque ouvrage
les informations retenues sont le nom de l’auteur, le nom de l’éditeur, le titre de l’ouvrage, la
discipline et l’année d’édition.
Les structures de données utilisées sont les suivantes :
type
pouvrage = ^touvrage ;
touvrage = article
auteur : chaîne ;
éditeur : chaîne ;
titre : chaîne ;
discipline : chaîne ;
année : entier ;
suivant : pouvrage ;
fin ;
9°/ Écrire, sous forme itérative et sous forme récursive, une procédure qui insère un ouvrage à
la kème place dans la liste.
10°/ Écrire, sous forme itérative et sous forme récursive, un algorithme d’insertion d’un
ouvrage après le premier livre d’une discipline donnée rencontré dans la liste.
11°/ Écrire sous forme itérative un algorithme d’insertion d’un ouvrage avant le premier
ouvrage d’une discipline donnée rencontré dans la liste.
12°/ On suppose que la liste est trié par ordre alphabétique sur les noms des auteurs. Écrire,
sous forme itérative et sous forme récursive, une procédure d’insertion d’un ouvrage dans la
liste.
13°/ Écrire un algorithme de suppression du kème ouvrage enregistré dans de la liste.
14°/ Écrire un algorithme de suppression du premier livre de génie logiciel édité en l’an 2000
par un auteur de nom donné..
15°/ Écrire un algorithme de suppression de tous les livres écrits par un auteur donné contenus
dans la liste.
Agence de voyage
On souhaite automatiser la gestion des réservations dans une agence de voyage qui offre des
prestations dans le domaine du transport aérien. L’agence travaille pour cela avec un certain
nombre de compagnies aériennes. Les structures de données utilisées sont les suivantes :
1. Une liste chaînée des compagnies avec lesquelles l’agence travaille. Chaque compagnie
sera caractérisée par :
le nom de la compagnie
un pointeur sur la liste des vols offerts par cette compagnie.
2. Pour chaque compagnie, il existe une liste chaînée des vols prévus. Chaque vol est
caractérisé par :
le numéro du vol,
les villes de départ et de destination,
les heures de départ et d’arrivée,
le nombre de places,
un vecteur de booléens pour gérer la disponibilité des places.
un pointeur sur une liste de passagers ayant fait une réservation pour ce vol,
3. Pour chaque vol, il existe une liste chaînée des passagers ayant fait une réservation. Chaque
passager est caractérisé par :
le nom du passager,
le nom de la compagnie,
le numéro du vol,
le numéro de siège.
Les déclaration utilisées sont les suivantes :
type
pcompagnie = ^tcompagnie ;
ppassager = ^tpassager ;
pvol = ^tvol ;
tcompagnie = article
nomcomp : chaîne ;
listevols : pvol ;
suivant : pcompagnie ;
fin ;
tvol = article
numéro : chaîne ;
villedépart, villearrivée : chaîne ;
heuredépart, heurearrivée : entier ;
nbplaces : entier ;
listeplaces : vecteur[1..nbplaces] de booléen ;
listepass : ppassager ;
suivant : pvol ;
fin ;
tpassager = article
nompass : chaîne ;
nomcomp : chaîne ;
numvol : chaîne ;
siège : entier ;
suivant : ppassager ;
fin ;
On supposera que :
6°/ Écrire une procédure qui enregistre un passager de nom nompass dans le vol de numéro
numvol de la compagnie de nom nomcomp.
7°/ On suppose maintenant que la liste de passagers pour chaque vol est ordonnée par ordre
alphabétique. Écrire une procédure qui enregistre un passager de nom nompass dans le vol de
numéro numvol de la compagnie de nom nomcomp.
8°/ Écrire une procédure qui supprimer le passager de nom nompass dans le vol de numéro
numvol de la compagnie de nom nomcomp.
9°/ Écrire une procédure qui transfère le passager de nom nompass du vol de numéro numvil1
au vol de numéro numvol2 de la compagnie de nom nomcomp.
Familles
On souhaite gérer une population composée de plusieurs familles limitées à deux générations.
Dans un premier temps, on se limitera à des familles composées uniquement du nom de
famille. Dans un deuxième temps, les parents, les enfants et les voitures possédées par une
famille.
Première partie : Liste à un seul niveau
La liste est ordonnée par ordre alphabétique sur les noms de famille. La population
correspond au schéma suivant :
liste
On considère une population composée de plusieurs familles. En général, une famille est
composée de deux parents et d’un ou plusieurs enfants.
On peut avoir aussi les cas particuliers suivants :
un seul parent (pas de conjoint et pas d’enfant),
un seul parent et un ou plusieurs enfants,
aucun parent, mais un ou plusieurs enfants (cas du décès des deux parents),
deux parents, mais pas d’enfants,
une personne (parent ou enfant) appartient à une seule famille,
à chaque famille, on associe la liste des voitures, en nombre illimité, possédées par les
membres de la famille.
type
pfamille = ^tfamille ;
ppersonne = ^tpersonne ;
pvoiture = ^tvoiture ;
tfamille = article
nom : chaîne ;
parent : ppersonne ;
enfant : ppersonne ;
voiture : pvoiture ;
suivant : pfamille ;
fin ;
tpersonne = article
nom : chaîne ;
sexe : caractère ;
suivant : ppersonne ;
fin ;
tvoiture = article
marque : chaîne ;
numéro : chaîne ;
suivant : pvoiture ;
fin ;
On supposera que :
toutes les familles ont des numéros différents
tous les enfants d’une même famille ont des noms différents.
le nombre de parents est limité à deux.
le nombre des enfants est illimité.
la population est ordonnée par ordre alphabétique sur les noms de famille.
1°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre de
famille sans parents.
2°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre de
familles qui n’ont plus qu’un seul parent.
3°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre
d’enfants de la population.
4°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre
d’enfants d’une famille de nom donné.
5°/ Un vol de voiture a eu lieu. On désire connaître le propriétaire de la voiture. Écrire une
fonction qui retourne un pointeur sur le propriétaire d’une voiture de numéro donné.
Mises à jour d’éléments
6°/ Écrire une procédure qui insère un enfant de prénom et de sexe donnés dans une famille
de nom donné.
7°/ On suppose maintenant que la liste des enfants est ordonnée par ordre alphabétique. Écrire
une procédure qui insère un enfant de nom et de sexe donnés dans une famille de nom donné.
8°/ Écrire une fonction booléenne qui supprime une voiture de numéro donné. La fonction
retourne la valeur vrai si et seulement si une voiture ayant ce numéro existe et a été
supprimée.
9°/ Une famille achète une voiture à une autre famille. Écrire une procédure qui effectue le
transfert de la voiture entre les deux familles.
Location de voitures
Une entreprise de location de voitures a décidé d’informatiser la gestion des locations pour un
mois. Les voitures mises en location appartiennent à plusieurs propriétaires. Les structures de
données utilisées seront les suivantes :
1. Un vecteur vprop dans lequel figure les noms et adresses des propriétaires de voitures
mises en location. Dans ce vecteur, chaque propriétaire apparaît une seule fois, dans un ordre
quelconque. Un entier nbprop indique le nombre de propriétaires répertoriés. On suppose que
les propriétaires ont des noms différents.
2. Une liste chaînée, d’adresse liste, des voitures offertes en location. Chaque voiture sera
caractérisée dans la liste par :
la marque de la voiture,
le numéro de la voiture,
l’indice dans le vecteur des propriétaires,
un pointeur sur la liste des réservations (nil si aucune réservation n’est enregistrée).
La liste [liste] des voitures est triée uniquement sur les marques de voitures.
3. Pour chaque voiture, il existe une liste chaînée des locations. Les locations ne pouvant se
faire que par jours entiers, les dates de locations sont remplacées par des numéros de jour dans
le mois. Chaque location est caractérisée par :
le numéro du jour du début de la location,
le numéro du jour de fin de la location (si la location est pour une seule journée, ces
numéros sont identiques),
le nom du locataire.
Ces listes sont triées sur les dates de location. On ne peut pas avoir plus d’une réservation
pour une même voiture pour une journée donné. On suppose que les locataires ont tous des
noms différents, et qu’aucun n’a effectué plus d’une réservation le même mois.
Les structures de données utilisées sont les suivantes :
constante
taillemax = 100 ;
type
tpropriétaire = article
nom : chaîne ;
adresse : chaîne ;
fin ;
vectprop = vecteur[1..taillemax] de propriétaire ;
pvoiture = ^tvoiture ;
plocation = ^tlocation ;
tvoiture = article
marque : chaîne ;
numéro : chaîne ;
indprop : entier ;
reserv : plocation ;
suivant : pvoiture ;
fin ;
tlocation = article
jourdeb, jourfin : 1..31 ;
nomloc : chaîne ;
suivant : plocation ;
fin ;
variable
vprop : vectprop ;
nbprop : entier ;
liste : pvoiture ;
On précise que vprop, nbprop et liste sont des variables globales et peuvent donc ne pas
figurer dans les en-têtes des algorithmes.
1°/ Écrire une fonction qui retourne la position d’un propriétaire de nom donné dans vprop.
La fonction retourne zéro si aucun propriétaire de ce nom n’est présent dans la liste.
2°/ Écrire une fonction qui délivre un pointeur sur la réservation dont le nom du locataire est
nomloc, ou bien nil si aucune réservation n’est trouvée.
3°/ Écrire une fonction qui délivre le nombre de voitures qui appartiennent au propriétaire de
rang indice dans le vecteur vprop.
4°/ Écrire une fonction qui délivre le nombre de voitures qui appartiennent au propriétaire de
nom nomprop. La fonction retourne –1 si ce propriétaire est inconnu.
5°/ Écrire une fonction qui retourne le nombre total de jours de location enregistrés dans la
liste des réservations.
6°/ Écrire une fonction qui retourne le nombre de voitures qui ne sont pas louées au cours
d’un mois.
7°/ Écrire une procédure qui imprime la liste complète des voitures, avec pour chaque voiture,
la marque, le numéro et le nom du propriétaire.
8°/ Écrire une procédure qui imprime la liste des locataires pour un jour de numéro donné
numjour, avec pour chacun le nom du propriétaire ainsi que la marque et le numéro du
véhicule.
9°/ Écrire une fonction qui détermine si une voiture dont la liste de réservations a pour adresse
reserv est libre pendant un jour de numéro numjour.
10°/ Écrire une procédure qui imprime les noms et les adresses des propriétaires de voitures
d’une marque donnée qui sont libres un jour de numéro donné.
11°/ Écrire une procédure d’insertion d’un propriétaire de nom donné dans le vecteur vprop.
S’il y figure déjà, la procédure retourne son rang dans vprop. S’il n’y figure pas encore, la
procédure demandera son adresse, l’insérera dans vprop, et retournera l’indice d’insertion.
12°/ Écrire une procédure qui insère en tête de liste une nouvelle voiture dont on donne la
marque, le numéro et le nom du propriétaire.
13°/ Écrire une procédure qui insère en tête de liste une nouvelle voiture dont on donne la
marque, le numéro et le nom du propriétaire, après les autres voitures de même marque. Si le
nom du propriétaire est nouveau, les mises à jour nécessaires seront effectuées.
14°/ Écrire une procédure qui insère une nouvelle réservation en tête d’une liste de
réservations ; le nom du locataire et les numéros des jours de début et de fin sont donnés.
15°/ Écrire une fonction qui effectue la réservation pour un locataire de nom donné dans une
liste de réservations. La fonction retourne une valeur booléenne égale à vrai si la réservation a
été possible (la période souhaitée est libre) et faux sinon.
16°/ Écrire une fonction qui effectue une réservation pour le locataire de nom donné d’une
voiture de marque donnée, du jour jour1 au jour jour2. La fonction retourne une valeur
booléenne égale à vrai si la réservation a été possible (il existe une voiture de la marque
souhaitée, libre les jours souhaités) et faux sinon.
17°/ Écrire une fonction qui supprime la réservation au nom de nomloc, si elle existe, dans
une liste de réservations et délivre un booléen indiquant si la suppression a été effectué.
18°/ Écrire une procédure qui annule la réservation faite au nom de nomloc. S’il n’y a pas de
réservation à ce nom, la procédure imprime un message d’erreur.
19°/ Écrire une procédure qui essaie de prolonger d’un jour la location au nom de nomloc,
pour la même voiture. Des messages sont affichés dans tous les cas : prolongement possible,
prolongement impossible ou réservation inexistante.
Chapitre 9
9.1 Introduction
Dans le chapitre précédent nous avons vu que les structures chaînées peuvent être construites
à l’aide de pointeurs. Les structures que nous avons étudiées utilisent un seul pointeur pour
référencer la cellule suivante de la liste. Il en résulte que les éléments de la structure sont
organisées de façon linéaire, une après l’autre. Il est cependant possible de construire des
structures qui utilisent plusieurs pointeurs pour relier les éléments de la structure. De telles
structures de données sont appelées des arbres ou structures arborescentes.
Dans ce chapitre, nous nous bornerons à une étude élémentaire des structures arborescentes,
ou arbres. Nous commençons par présenter quelques exemples de structures arborescentes
pris dans divers domaines d’application de l’informatique (théorie des langages, compilation,
langage naturel…), puis nous donnons un certain nombre de définitions précises.
Nous étudierons ensuite les algorithmes généraux de parcours d’arbres, tant récursifs
qu’itératifs, et enfin des algorithmes d’application à différents types d’arbres.
9.1.1 Quelques exemples d’arbres
tidentité = article
nom, prénom : chaîne ;
datenais : tdate ;
lieunais : chaîne ;
fin ;
tétudiant = article
inscription : tuniversité;
matricule : chaîne ;
identité : tidentité ;
fin ;
étudiant
Une phrase dans un langage formel peut être représentée sous la forme d’une arborescence.
Par exemple, si on considère la grammaire dont les règles de production sont S aSb|SS|,
un arbre de dérivation de la phrase aabb est :
S S
a S b
a S b
Une expression arithmétique peut être représentée sous la forme d’un arbre. Par exemple, un
arbre de dérivation de l’expression arithmétique a + b * c est :
E + E
I E * E
a S I
b c
+ mo
d
div b * a
* mo c d
d
c d a b
Prenons un exemple. Soit l’arbre donnée à la figure ci-dessous. Chaque sommet ou nœud (A,
B, C, D, E, G) possède un certain nombre de fils (B et C pour le nœud A, D et E pour le nœud
B, G pour le nœud C, rien pour les nœuds D, E, G). Inversement, A est le père de B et de C, B
est le père de D et E, C est le père de G. Lorsqu’un nœud n’a pas de fils (ici D, E et G), on dit
que c’est une feuille de l’arbre, ou encore un nœud terminal. Le nœud particulier A qui n’a
pas de père est appelé la racine de l’arbre. Un arc reliant deux nœuds est appelé une branche.
Tous les nœuds, sauf la racine, n’ont qu’un seul père.
B C
D E G
soit composé d’un nœud auquel sont chaînés un sous-arbre gauche et un sous-arbre
droit.
Par analogie avec les listes linéaires chaînées, on notera [racine], l’ensemble des valeurs de
l’arbre dont le pointeur de départ se trouve dans la variable racine.
9.1.2.3 Représentation d’un arbre binaire
On peut représenter un arbre binaire sous forme graphique ou sous forme parenthésée. Par
exemple, l’arbre
B E
C D F
Par exemple, la notation de Dewey de l’arbre : A (B (C, D), E (, F (G, H (I, J)))) est la
suivante :
1A
1.1 B
1.1.1 C
1.1.2 D
1.2 E
1.2.2 F
1.2.2.1 G
1.2.2.2 H
1.2.2.2.1 I
1.2.2.2.2 J
Primitives d’accès
Il est possible de choisir comme primitives les opérations qui permettent d’accéder à la valeur,
au fils gauche et au fils droit d’un nœud. On peut également choisir un autre jeu de primitives
aussi naturelles, qui permettraient par exemple d’accéder à la valeur, aux fils et aux frères
d’un nœud.
Représentation chaînée d’un arbre binaire
Pour obtenir une représentation chaînée d’un arbre binaire, on utilise des nœuds de type :
type
pélément = ^tnœud;
tnœud = article
gauche : pélément ;
donnée : télément ;
droite : pélément ;
fin ;
La figure ci-dessous présente une illustration d’un arbre binaire.
racine
où racine est une variable de type pointeur contenant l’adresse du nœud racine de l’arbre :
c’est le point d’entrée de l’arbre. Si l’arbre n’est pas vide (racine nil), racine^.donnée
Comme pour les listes linéaires chaînées, on constate que l’accès à l’arbre est réalisé par une
variable pointeur que nous appellerons très souvent la racine de l’arbre et dont il ne faudra
pas perdre le contenu.
9.1.2.4 Définitions
Niveau
On dit que deux nœuds sont au même niveau dans un arbre, s’ils sont issus d’un même nœud
après le même nombre de filiations.
On peut donner la définition suivante :
le niveau de la racine de l’arbre est égal à un,
le niveau d’un nœud, autre que la racine, est égal au niveau de son père plus un.
On peut écrire des algorithmes de parcours d’arbres en passant par tous les nœuds situés à un
même niveau. Si on part de la racine et que l’on traite tous les nœuds au niveau 2, puis tous
les nœuds au niveau 3, etc. on dit qu’on utilise un parcours descendant par niveaux (ou en
largeur). On peut utiliser une méthode analogue avec un parcours ascendant par niveaux en
partant des nœuds ayant le plus grand niveau. C’est ce que nous étudierons à propos des
arbres complets.
Mot des feuilles d’un arbre binaire
Le mot des feuilles d’un arbre binaire est la chaîne formée, de gauche à droite, de la valeur
des feuilles de l’arbre.
Si chaque nœud autre qu’une feuille admet deux descendants et si toutes les feuilles sont au
même niveau, on dit que l’arbre binaire est complet. La taille de l’arbre est égale à … où k est
le niveau des feuilles.
Hauteur d’un nœud
Définition initiale
La hauteur d’un arbre est égale au maximum des niveaux des feuilles.
Définition récursive
la hauteur d’un arbre est égale à la hauteur de sa racine,
la hauteur d’un arbre vide est nulle,
la hauteur d’un nœud est égale au maximum des hauteurs du sous-arbre gauche et du
sous-arbre droit plus un.
Facteur d’équilibre
le facteur d’équilibre de chaque sous-arbre est associé à sa racine,
le facteur d’équilibre d’un nœud est égal à la hauteur du sous-arbre gauche moins la
hauteur du sous-arbre droit.
Arbre équilibré
Un arbre est dit équilibré si pour tout nœud p de cet arbre la valeur absolue du facteur
d’équilibre est inférieure ou égale à un : |facteur d’équilibre(p)| 1.
Arbre dégénéré
Un arbre est dit dégénéré si aucun nœud de cet arbre ne possède plus un descendant.
Arbre binaire ordonné
Un arbre binaire est dit ordonné si la chaîne infixée des valeurs correspondant au parcours
infixé est ordonnée.
Dans un arbre binaire ordonné, la valeur de chaque un nœud est telle que :
(nœud^.droite nil, nœud^.gauche nil,
nœud^.gauche^.donnée nœud^.donnée nœud^.droite^.donnée)
Première transformation
Pour tout schéma récursif de la forme (I) ci-dessous
On remarquera que cette transformation a déjà été utilisée dans les chapitres précédents, en
particulier pour le traitement des listes chaînées.
Par exemple, soit la procédure récursive suivante :
procédure écrireliste(liste : pélément) ;
début
si liste nil alors
début
écrire(liste^.donnée) ;
écrireliste(liste^.suivant) ;
fin ;
fin ;
Si on applique le transformation décrite ci-dessus en prenant :
(liste) écrire(liste^.donnée)
f(liste) liste^.suivant
on obtient la procédure itérative
procédure écrireliste(liste : pélément) ;
variable
p : pélément ;
début
p liste ;
tantque p nil faire
début
écrire(p^.donnée) ;
p p^.suivant ;
fin ;
fin ;
racine^.droite ;
On obtient alors l’algorithme suivant :
procédure parcourspréfixé1(racine : pélément) ;
variable
rac : pélément ;
début
rac racine ;
tantque rac nil faire
début
traiter(rac) ;
parcourspréfixé1(rac^.gauche) ;
rac rac^.droite ;
fin ;
fin ;
Deuxième transformation
La récursivité n’est pas encore complètement éliminée. Il reste encore un appel récursif. La
différence avec l’appel précédent provient du fait qu’il y a encore au moins une instruction à
exécuter après cet appel récursif. On a donc besoin d’une pile afin de préserver les valeurs
successives de racine pour pouvoir effectuer l’instruction suivante « racine racine^.droite)
au retour de chaque appel récursif. Dans notre cas, on parcourt tous les sous-arbres gauches,
en préservant, dans la pile, à chaque appel, l’accès aux sous-arbres droits correspondants. Le
parcours des sous-arbres droits doit s’effectuer tant qu’il reste un accès à un sous-arbre droit
dans la pile.
L’algorithme est alors le suivant :
procédure parcourspréfixé2(racine : pélément) ;
variable
rac : pélément ;
début
initpilevide ;
rac racine ;
tantque (rac nil) ou non pilevide faire
début
tantque rac nil faire
début
traiter(rac) ;
empiler(rac) ;
rac rac^.gauche ;
fin ;
dépiler(rac) ;
rac rac^.droite ;
fin ;
fin ;
On remarquera que la valeur nil n’est jamais empilée et que de ce fait racine^.gauche et
racine^.droite sont toujours définis.
Conclusion
Nous avons donnée deux transformations possibles (il peut en exister d’autres) permettant
d’éliminer les deux appels récursifs de la procédure parcourspréfixé afin d’obtenir un schéma
itératif équivalent qu’il n’est pas simple de donner a priori. Nous allons essayer d’utiliser ces
deux transformations dans le cas des parcours infixé et postfixé. On notera que le problème
dans le cas général de l’élimination de la récursivité est très complexe et que l’utilisation de la
deuxième transformation (à l’aide d’une pile) n’est pas triviale.
rac rac^.gauche ;
fin ;
dépiler(rac) ;
traiter(rac) ;
rac rac^.droite ;
fin ;
fin ;
Schéma itératif
On ne peut pas appliquer la première transformation car, après le deuxième appel récursif, il y
a une instruction à exécuter : traiter(racine). On ne peut pas non plus appliquer la deuxième
transformation.
On va alors appliquer une transformation différente mais qui met en jeu, comme dans le cas
de la deuxième, une pile. Cette pile devra contenir l’environnement des deux appels récursifs.
Au moment où on dépile, il faut savoir si on doit de nouveau empiler afin d’effectuer le
parcours d’un sous-arbre droit ou si, ce parcours ayant déjà été effectué, on doit traiter la
racine. On va donc empiler, en plus de la racine, un indicateur booléen qui nous permettra de
distinguer les deux cas au moment du dépilement.
Par convention, on empilera la valeur « vrai » avant d’effectuer le parcours d’un sous-arbre
gauche et la valeur « faux » avant d’effectuer le parcours d’un sous-arbre droit. Donc,
lorsqu’on dépile un élément, deux cas sont possibles :
l’indicateur a la valeur vrai, on doit de nouveau empiler la racine avec la valeur faux et
effectuer le parcours du sous-arbre droit,
l’indicateur a la valeur faux, on doit traiter la racine.
Après avoir traiter la racine, on donne à racine la valeur nil afin de récupérer, si la pile n’est
pas vide, l’élément qui se trouve au sommet de la pile.
L’algorithme est alors le suivant :
procédure parcourspostfixé1(racine : pélément) ;
variable
rac : pélément ;
indic : booléen ;
début
initpilevide ;
rac racine ;
tantque (rac nil) ou (non pilevide) faire
début
tantque rac nil faire
début
empiler(rac, vrai) ;
rac rac^.gauche ;
fin ;
dépiler(rac, indic) ;
traiter(rac) ;
si indic alors
début
empiler(rac, faux) ;
rac rac^.droite ;
fin
sinon
début
traiter(rac) ;
rac nil ;
fin ;
fin ;
fin ;
Schéma récursif
On utilise le fait que la taille d’un arbre est égale à un plus la taille du sous-arbre gauche plus
la taille du sous-arbre droit. La taille d’un arbre vide est égale à zéro.
Schéma récursif
Schéma itératif
On donne un arbre binaire et une valeur appartenant à l’ensemble des valeurs des éléments de
l’arbre. Le problème est d’écrire une fonction booléenne qui détermine si cette valeur est
présente dans la liste.
Schéma récursif
La raisonnement est le suivant : on commence par regarder si l’arbre est vide. Si l’arbre est
vide on retourne la valeur faux. Si l’arbre n’est pas vide, on regarde si la valeur stockée dans
la racine est égale à la valeur recherchée. Si oui on retourne la valeur vrai. Sinon on regarde si
la valeur recherchée se trouve dans le sous-arbre gauche et on retourne la vrai sinon on
continue la recherche dans le sous-arbre droit.
L’algorithme est alors le suivant :
fonction recherche(racine : pélément ; valeur : télément) : booléen ;
début
si racine = nil alors
recherche faux
sinon
si racine^.donnée = valeur alors
recherche vrai
sinon
si recherche(racine^.gauche, valeur) alors
recherche vrai
sinon
recherche recherche(racine^.droite, valeur) ;
fin ;
Cet algorithme peut aussi s’exprimer plus simplement en regroupant le second et le troisième
cas à l’aide du connecteur « ou sinon ».
fin ;
Une autre solution consisterait à effectuer une instruction de retour dès que l’on a trouvé
rac^.donnée = val, cela permet d’éviter de continuer à parcourir l’arbre. Il reste à effectuer
recherche faux si l’on a parcouru totalement l’arbre sans avoir trouvé val.
L’algorithme est alors le suivant :
fonction recherche(racine : pélément ; val : télément) : booléen ;
variable
rac : pélément ;
début
initpilevide ;
rac racine ;
trouvé faux ;
tantque ((rac nil) ou (non pilevide) faire
début
tantque rac nil faire
début
si rac^.donnée = val alors
recherche vrai ;
empiler(rac) ;
rac rac^.gauche ;
fin ;
dépiler(rac) ;
rac rac^.droite ;
fin ;
recherche faux ;
fin ;
Exercice 9.2
Transformer la fonction récursive de recherche d’un élément dans un arbre binaire afin qu’elle
délivre un pointeur sur la première occurrence de l’élément qu’elle trouve dans l’arbre.
Exercice 9.3
Transformer la fonction itérative de recherche d’un élément dans un arbre binaire afin qu’elle
délivre un pointeur sur la première occurrence de l’élément qu’elle trouve dans l’arbre.
recherchée, alors on suit le pointeur droit. Si elle est supérieure à la valeur recherchée, alors
on suit le pointeur gauche. Ce processus est répété jusqu’à ce l’on rencontre un pointeur nil ou
que l’on localise la valeur recherchée. Elle retourne un pointeur nil si la valeur recherchée
n’est pas présente dans la liste.
L’algorithme est alors le suivant :
fonction recherche(racine : pélément ; elem : télément) : pélément ;
variable
trouvé : booléen ;
courant : pélément ;
début
trouvé faux ;
courant racine ;
tantque (courant nil) et (non trouvé) faire
si courant^.donnée = elem alors
trouvé racine
sinon
si courant^.donnée > elem alors
courant courant^.gauche
sinon
courant courant^.droite ;
si trouvé alors
recherche courant
sinon
recherche nil ;
fin ;
Version récursive
La fonction ci-dessus peut être écrite sous forme récursive. En effet, un arbre binaire ordonné
peut être défini de façon récursive. On peut considérer un arbre ordonné comme étant soit
vide (il consiste un pointeur nil), soit composé d’un seul nœud (le nœud racine), soit composé
d’un nœud racine avec des pointeurs sur deux sous-arbres binaires ordonnés (le sous-arbre
gauche et le sous-arbre droit).
Pour donner une version récursive de la fonction ci-dessus, on utilise la définition récursive
d’un arbre binaire. Le raisonnement est le suivant : lorsqu’on entre dans la fonction, la valeur
du paramètre racine est comparée à nil. Si la valeur de racine est en effet égale à nil, alors on
sait que l’arbre est vide et que par conséquent la valeur recherchée ne s’y trouve pas, et on
retourne alors la valeur nil. Si par contre racine n’est pas nil, on regarde si la donnée stockée
dans la racine est égale à la valeur recherchée. Si les deux valeurs sont égales, on retourne la
valeur de racine. Sinon si la valeur recherchée est supérieure à la valeur stockée dans la
racine, on appelle encore la fonction, mais cette fois avec racine^.droite comme premier
paramètre (exploration du sous-arbre droit), sinon on appelle encore la fonction, mais cette
fois avec racine^.gauche comme premier paramètre (exploration du sous-arbre gauche).
L’algorithme est alors le suivant :
fonction recherche(racine : pélément ; elem : télément) : pélément ;
début
si racine = nil alors
recherche nil
sinon
si racine^.donnée = elem alors
recherche racine
sinon
si racine^.donnée > elem alors
recherche recherche(racine^.gauche, elem)
sinon
recherche recherche(racine^.droite, elem) ;
fin ;
On peut réécrire la fonction ci-dessus sous la forme d’un prédicat. L’arbre étant ordonné, on
peut, dans le cas des appels récursifs, choisir le sous-arbre dans lequel il faut effectuer la
recherche de la valeur donnée.
L’algorithme est alors le suivant :
fonction recherche(racine : pélément ; valeur : télément) : booléen ;
début
si racine = nil alors
recherche faux
sinon
si racine^.donnée = valeur alors
recherche vrai
sinon
si racine^.donnée > valeur alors
recherche recherche(racine^.gauche, valeur)
sinon
recherche recherche(racine^.droite, valeur)
fin ;
Exemple d’application
On considère un arbre binaire dont les nœuds sont définis par la structure
type
ppassager = ^tpassager ;
tpassager = article
nom : chaîne ;
siége : entier ;
gauche : ppassager ;
droite : ppassager ;
fin ;
En supposant que l’arbre est ordonné par rapport au champ nom, écrire une fonction qui prend
en entrée un passager et retourne son numéro de siège. La fonction retourne 0 si le passager
en question n’est pas présent dans la liste.
Solution
C’est une application directe de l’algorithme ci-dessus. On obtient alors :
fonction recherche(racine : ppassager ; pass : tpassager) : entier ;
début
si racine = nil alors
recherche 0
sinon
si racine^.nom = pass.nom alors
recherche pass.siège
sinon
si racine^.nom > nom alors
recherche recherche(racine^.gauche, pass)
sinon
recherche recherche(racine^.droite, pass)
fin ;
Exercice 9.4
Transformer la fonction récursive de recherche d’un élément dans un arbre binaire ordonné
afin qu’elle délivre un pointeur sur la première occurrence de l’élément rencontrée.
Exercice 9.5
Transformer la fonction itérative de recherche d’un élément dans un arbre binaire ordonné
afin qu’elle délivre un pointeur sur la première occurrence de l’élément rencontrée.
9.4.2 Insertion dans un arbre binaire ordonné
On souhaite insérer un élément dans un arbre binaire ordonné de telle sorte qu’il demeure
ordonné. Si l’arbre est vide (racine = nil) il suffit de créer un arbre contenant un seul élément.
Sinon, on parcourt l’arbre afin de trouver le nœud qui sera le père de l’élément que l’on désire
insérer. Une fois le nœud père trouvé, il suffit de créer une feuille contenant l’élément à
insérer, et l’attacher du bon côté du nœud père.
Ce nœud père est tel que :
(père^.donnée elem, père^.droite = nil) ou (père^.donnée > elem, père^.gauche = nil)
Il est toujours au bout de la filiation. C’est soit une feuille soit un nœud qui n’a qu’un seul
sous-arbre.
La création d’une feuille se fera au moyen de la procédure suivante :
Notons la parenté de cet algorithme avec l’insertion à la fin d’une liste linéaire. C’est
créerfeuille qui remplace insertête, et la différence principale réside dans le choix d’insérer à
gauche ou à droite de la racine, selon la valeur de élément:
procédure insère(var liste : pélément ; élément : télément) ;
début
si liste = nil alors
insertête(liste, élément)
sinon
insère(liste^.suivant, élément)
fin ;
Schéma itératif
La version itérative est un peu plus complexe : on doit traiter séparément au début le cas de
l’arbre vide, puis chercher le nœud père de celui que l’on veut insérer. Lorsqu’il est trouvé, on
crée la feuille correspondante, et on arrête l’itération au moyen du booléen stop.
L’algorithme est alors le suivant :
procédure insère(var racine : pélément ; elem : télément) ;
variable
stop : booléen ;
rac : pélément ;
début
si racine = nil alors
créerfeuille(racine, elem)
sinon
début
rac racine ;
stop faux ;
tantque non stop faire
si elem < rac^.donnée alors
si rac^.gauche = nil alors
début
créerfeuille(rac^.gauche, elem) ;
stop vrai ;
fin
sinon
rac rac^.gauche
sinon
si rac^.droite = nil alors
début
créerfeuille(rac^.droite, elem) ;
stop vrai ;
fin
sinon
rac rac^.droite ;
fin ;
fin ;
Exercices d’apprentissage
Exercice 9.6
Écrire une procédure qui crée un arbre binaire ordonné à partir des éléments d’un fichier.
9.4.3 Tri par arbre d’un vecteur
Nous donnons cet algorithme à titre d’exemple car il est d’un intérêt pratique très limité. Pour
trier un vecteur, on peut, à partir de ce vecteur, créer un arbre ordonné contenant toutes les
valeurs du vecteur. Il suffit d’effectuer ensuite un parcours infixé de l’arbre en rangeant les
valeurs de l’arbre dans le vecteur pour obtenir un vecteur trié.
procédure triarbre(var liste : vélément ; n : entier) ;
variable
i : entier ;
racine : pélément ;
début
créerarbre(liste, n, racine) ;
i 1 ;
créervecteur(racine, liste, i) ;
fin ;
Écriture de la procédure créerarbre
Il suffit d’initialiser l’arbre à nil, puis d’insérer successivement dans l’arbre tous les éléments
du vecteur à l’aide de la procédure d’insertion dans un arbre binaire ordonné.
L’algorithme est alors le suivant :
procédure créerarbre(liste : vélément ; n : entier ; var racine : pélément) ;
variable
i : entier ;
début
racine nil ;
pour i 1 haut n faire ;
insère(racine, liste[i]) ;
fin ;
Écriture de la procédure créevecteur
On va écrire cette procédure sous forme récursive en utilisant un parcours infixé. Le
paramètre i doit être passé par adresse ; en effet, s’il était passé par valeur, on rangerait un
nœud et son fils gauche au même emplacement dans le vecteur.
2
0
1 2
0 5
fils
5 1
2
3 8
1 4 6 9
[fils^.gauche] [fils^.droite]
2
0
1 2
0 5
8 1
2
6
9
3
Dr Ndi Nyoungui André
1 4
123 Algorithmique et structures de données
Nous avons mis le sous-arbre droit de fils en sous-arbre gauche du père de fils, et le sous-
arbre gauche de fils en sous-arbre gauche du plus petit élément de [fils^.droite].
C’est ce que nous ferons toujours, sauf bien entendu si le sous-arbre droit de fils est vide.
Dans ce cas, on se contentera de mettre le sous-arbre gauche de fils en sous-arbre gauche du
père de fils. Ainsi, la suppression du nœud appelé fils dans l’arbre suivant
2
0
1 2
0 5
fils
5 1
2
3
[fils^.gauche]
1 4
donnerait :
2
0
1 2
0 5
3 1
2
1 4
Si c’est la racine de l’arbre que l’on veut supprimer, on la remplacera par son sous-arbre droit
sauf si celui-ci est vide, et on rattachera son sous-arbre gauche au plus petit élément du sous-
arbre droit. Ainsi la suppression de la racine de l’arbre suivant
1 racine
0
5 1
2
Dr Ndi Nyoungui André
3 8 1 1
1 5
124 Algorithmique et structures de données
donnerait :
1
2
1 1
1 5
3 8
On remarque que les sous-arbres sont rattachés à gauche du père de fils. Ceci provient du fait
que fils est le fils gauche de son père. Si on avait supprimé un fils droit, on aurait dû rattacher
les sous-arbres à droite de ce père. On obtient ce résultat très facilement au moyen de l’appel
récursif dans la procédure supprime.
C’est la procédure suppnœud qui opère les rattachements des sous-arbres, c’est une extension
de la procédure supptête que nous avons employée dans les listes chaînées. Le pointeur sur le
plus petit élément d’un sous-arbre s’obtient au moyen de la boucle tantque dans suppnœud,
qui opère une descente vers l’élément le plus à gauche du sous-arbre.
procédure suppnoeud (nœud : pélément) ;
variable
p, q : pélément ;
début
p nœud ;
si nœud^.droite nil alors
début
q nœud^.droite ;
tantque q^.gauche nil faire
q q^.gauche ;
q^.gauche p^.gauche ;
nœud nœud^.droite
fin
sinon
nœud nœud^.gauche ;
fin ;
début
si racine nil alors
si racine^.donnée = elem alors
suppnoeud(racine)
sinon
si racine^.donnée > elem alors
supprime(racine^.gauche, elem)
sinon
supprime(racine^.droite, elem) ;
fin ;
9.5 Conclusion
Les arbres ont de très nombreuses applications en informatique. Nous n’en avons signalé que
quelques unes.
L’écriture fonctionnelle des algorithmes utilisant des schémas récursifs est bien adaptée au
traitement des arbres et permet d’effectuer simplement le travail d’analyse de l’algorithme. La
traduction directe de ces schémas n’est toutefois pas possible dans les langages qui ne
connaissent pas la récursivité, et d’autre part un programme utilisant des appels récursifs est
bien moins efficace que son équivalent non récursif (itératif). Nous avons pour cela proposé
des règles simples de transformation d’un algorithme récursif en un algorithme itératif
équivalent. Ces règles ne sont pas générales mais permettent quand même de résoudre un très
grand nombre de problèmes classiques.