Vous êtes sur la page 1sur 126

1 Algorithmique et structures de données

Chapitre 7

Les fichiers séquentiels

7.1 Notion de fichier et définitions


La structure de données dont nous envisageons maintenant l’étude est la structure de fichier
séquentiel. L’accès séquentiel à des fichiers garde encore à l’heure actuelle une grande
importance pratique dans les problèmes de gestion par exemple. Nous allons définir de façon
abstraite les objets formant un fichier séquentiel comme une collection d’informations et les
propriétés fonctionnelles de ces objets comme les caractéristiques formelles de cette
collection. Cette démarche abstraite facilite la conception des algorithmes et des modèles de
programmes manipulant ces objets.
On distingue deux types de fichiers : les fichiers séquentiels et les fichiers à accès aléatoire.
Dans un fichier à accès séquentiel, les valeurs ne peuvent être accédées que dans le même
ordre dans lequel ils ont été stockés. Pour traiter un tel fichier, on doit se déplacer à travers les
éléments successifs dans le même ordre que leurs emplacements respectifs dans la mémoire.
Par contre, les valeurs dans un fichier aléatoire, aussi appelé fichier à accès direct, peuvent
être accédées dans n’importe quel ordre voulu par le programmeur.
Définition d’un fichier séquentiel
Un fichier séquentiel sur un ensemble de valeurs E est une suite finie d’éléments de E, munie
de certaines propriétés. Considérons par exemple :
 un tiroir de fiches signalitiques de livres dans une bibliothèque,
 la suite des enregistrements sur un cédérom,
 la file des clients d’une banque attendant devant un guichet.
Dans tous ces exemples, on peut définir :
a) Un ensemble de places ou positions P totalement ordonné puisque l’on peut distinguer le
premier enregistrement que l’on peut écouter sur un cédérom, le deuxième client qui passe
devant le guichet, la dernière fiche du tiroir.
b) Un ensemble de valeurs E : l’ensemble des fiches du tiroir, des clients de la banque, des
morceaux de musique enregistrés.
c) Dans tous les exemples précédents, l’opérateur humain est capable de détecter la fin de la
bande, le dernier client, le dernière fiche.
d) Fonctionnellement, on peut définir :
 une correspondance entre une place de P et un élément de E,
 un moyen de détecter la dernière place : une marque particulière peut être associée à
cette place dans le cas du traitement du fichier par un automate,
 un moyen d’accès à la première place de l’ensemble P.

Dr Ndi Nyoungui André


2 Algorithmique et structures de données

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.

7.1.2 Définitions formelles


Nous allons formaliser la notion de fichier séquentiel pour faciliter son utilisation dans
l’écriture des algorithmes.
Fichier séquentiel
Définition
Soient :
 Un ensemble quelconque E de valeurs appelé ensemble des valeurs du fichier
séquentiel,
 Un ensemble fini P, totalement ordonné, appelé l’ensemble des places du fichier
séquentiel.
Un fichier séquentiel à valeurs dans E est une application f de P dans E. L’ensemble P des
places du fichier peut être représenté par une suite d’emplacements mémoire consécutifs
destinés au stockage des composantes du fichier.

7.1.3 Les attributs d’un fichier séquentiel

Un fichier séquentiel est défini par les attributs suivants :


 Le type du fichier ou type des éléments du fichier,
 Un nom qui permet de le désigner et de le distinguer des autres fichiers,
 Un état, qui est défini à tout instant et qui peut évoluer au cours du temps.
7.1.4 Définition de types de fichiers
Le type d’un fichier définit le type des éléments du fichier. Le type d’un fichier est défini en
utilisant les mêmes règles que pour la définition des autres types de données. La définition
d’un type de fichier s’effectue de la manière suivante :
type
tfichier = fichier de télément ;

Dr Ndi Nyoungui André


3 Algorithmique et structures de données

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.

Quelques exemples de déclaration de variables fichiers.


variable
f : fentiers ;
employés : femployé ;

7.1.6 Accès à un fichier séquentiel


La référence à un fichier implique deux informations :
 l’identificateur de fichier déclaré dans l’algorithme ;
 le nom système (au niveau du système d’exploitation) du fichier dans le support de
stockage (disquette, disque dur, cédérom,…).
Ainsi pour accéder à un fichier disque, il faut établir une relation entre l’identificateur du
fichier (variable manipulée dans l’algorithme) et le nom système du fichier (au niveau du
système d’exploitation). Cette relation est établie au moyen d’une instruction d’assignation
dont la forme générale est la suivante :
assigner(f, nomfichier) ;
où f est l’identificateur de fichier déclaré dans le programme et nomfichier est le nom système
du fichier sur le disque. Une fois l’opération d’assignation effectuée, l’accès aux éléments du
fichier dans l’algorithme se fait à travers l’identificateur de fichier f.
On remarquera que contrairement aux tableaux, un fichier est un récipient permanent de
données ayant une image dans un support de mémoire secondaire (disque dur ou disquette).

Dr Ndi Nyoungui André


4 Algorithmique et structures de données

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.

Figure 7.1. Illustration de la tête de lecture/écriture

Marque de fin de 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) ;

Dr Ndi Nyoungui André


5 Algorithmique et structures de données

Si le fichier existe déjà sur le disque, son contenu est effacé.


 Ouverture d’un fichier en lecture
L'ouverture d'un fichier permet de le rendre utilisable en lecture et d'initialiser les indicateurs.
Elle s’exprime par :
assigner(f, nomfichier) ;
relire(f) ;

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 :

Figure 7.2. Illustration de l’ouverture d’un fichier

tête de lecture/écriture

 Opération de lecture dans un fichier


Le fichier f étant supposé ouvert, cette opération a pour effet de lire le contenu de l'élément
pointé par tête(f) et de l'affecter à une variable. Elle s’exprime de la manière suivante :
lire(f, élément) ;
Son effet se traduit de la manière suivante :
lire(f, élément) :
si fin(f) alors
écrire('Erreur')
sinon
début
élément  [élément pointé par tête(f)]
avancer(f)
fin.

Dr Ndi Nyoungui André


6 Algorithmique et structures de données

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

 Opération d’écriture dans un fichier


Le fichier f étant supposé ouvert en écriture, cette opération a pour effet d'écrire la valeur d’un
élément dans l’emplacement mémoire désignée par tête(f). Elle s’exprime par :
écrire(f, élément) ;
Son effet peut se traduire de la manière suivante :
écrire(f, élément) :
si mode(f) = lecture seule alors
écrire('Erreur')
sinon
si fin(f) alors
début
ajouter une place à la fin du fichier
avancer(f)
ranger élément dans la place pointée par tête(f)
fin
sinon
début
ranger élément dans la place pointée par tête(f)
avancer(f)
fin.
Cette primitive peut être schématisée par :
Avant l’écriture :

Dr Ndi Nyoungui André


7 Algorithmique et structures de données

tête de lecture/écriture

Après l’écriture :

val

tête de lecture/écriture

 Fermeture d’un fichier


Le fichier f étant supposé ouvert, l’opération de fermeture consiste à rendre impossible les
opérations de lecture ou d'écriture dans le fichier. Elle s’exprime par :
fermer(f) ;
Le fichier f est maintenant fermé ; aucune opération de lecture ni d’écriture ne peut plus lui
être appliquée.
 Suppression d’un fichier 
Le fichier f étant supposé ouvert, cette opération consiste à supprimer le fichier système qui a
été assigné à f au moment de son ouverture. Elle s’exprime par :
supprimer(f) ;
Le fichier assigné à f est maintenant détruit du disque ; son contenu est définitivement perdu.

7.2 Algorithmes traitant un seul fichier

Les schémas d'énumération

Un schéma d'énumération d'un fichier est un schéma dans lequel :


 On accède à chaque élément du fichier une seule fois;
 A chaque élément rencontré, on applique un traitement spécifique.
Le traitement appliqué à l'élément courant est appelé traitement courant alors que celui qui
est appliqué à la marque de fin de fichier est appelé traitement final.
On distingue deux types de schémas d'énumération en fonction du « traitement final » :

SCHEMA N° 1 : Ici le traitement courant est différent du traitement final.


début
initialisation ;
tantque non fin(f) faire

Dr Ndi Nyoungui André


8 Algorithmique et structures de données

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

Exemple 1. Impression des éléments d’un fichier séquentiel.


procédure afficherfichier(nomfichier : chaîne) ;
variable
élément : télément ;
f : fichier de télément ;
début
relire(f, nomfichier) ;
tantque non fin(f) faire
début
lire(f, élément) ;
afficher(élément);
fin ;
fermer(f) ;
fin ;
où afficher est une procédure qui permet d’afficher un élément du fichier.
Exemple 2. Calcul de la somme des éléments d’un fichier de nombres réels. On admet que la
somme des éléments d’un fichier vide est égale à zéro.
fonction somme(nomfichier : chaîne) : réel ;
variable
nombre, total : réel ;
f : fichier de réel ;
début
relire(f, nomfichier) ;
total  0 ;
tantque non fin(f) faire

Dr Ndi Nyoungui André


9 Algorithmique et structures de données

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;

Exemple 4. Création d’un fichier de n éléments de type télément.


procédure créerfichier(nomfichier : chaîne ; n : entier) ;
variable
élément : télément ;
f : fichier de télément ;
compteur : entier ;

Dr Ndi Nyoungui André


10 Algorithmique et structures de données

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) ;

Dr Ndi Nyoungui André


11 Algorithmique et structures de données

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).

7.2.3 Recherche de la plus grande valeur contenue dans un fichier


Soit un fichier non vide f, tel que l’ensemble des valeurs soit muni d’une relation d’ordre
total. On veut écrire une procédure qui retourne la plus grande valeur contenue dans le fichier.
La procédure retourne également un indicateur booléen égal à faux si le fichier est vide et à
vrai dans le cas contraire.
procédure plusgrand(nomfichier : chaîne ; var grand : télément ; var trouvé : booléen) ;
variable
élément : télément ;
f : fichier de télément;
début
relire(f, nomfichier) ;
si fin(f) alors
trouvé  faux
sinon
début
trouvé  vrai ;
lire(f, grand) ;
tantque non fin(f) faire
début
lire(f, élément) ;
si grand < élément alors
grand  élément ;
fin ;
fin ;
fermer(f) ;
fin ;

7.2.4 Accès à un élément dans un fichier


Ce problème fondamental est habituellement formulé de deux manières :

Dr Ndi Nyoungui André


12 Algorithmique et structures de données

 on donne un entier k et on recherche alors le kème élément du fichier. On parle de


recherche par position.
 on donne une valeur appartenant à l’ensemble des valeurs du fichier et on recherche le
premier ou le dernier élément qui a cette valeur dans le fichier. On parle de recherche
par valeur ou recherche associative.

7.2.4.1 Accès par position (accès au kème élément)


Première version
Le raisonnement consiste à parcourir le fichier pour amener la kème place du fichier en face
de la tête de lecture/écriture, la boucle étant contrôlée par la condition (i < k) et non fin(f).
Ceci n’est possible que si k  [1..n], où n est le nombre de places du fichier. Nous allons donc
utiliser un indicateur booléen pour déterminer si le kème élément existe ou non dans le
fichier. Si le parcours se termine avant la fin du fichier, il ne reste plus qu’à lire la valeur de
l’élément courant et à positionner l’indicateur.
L’algorithme est alors le suivant :
procédure accèsk(nomfichier : chaîne; k : entier; var trouvé : booléen; var elem : télément) ;
var
i : entier ;
é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 non fin(f) alors
début
lire(f, elem) ;
trouvé  vrai ;
fin ;
fermer(f) ;
fin;
Deuxième version
Le raisonnement est à peu près le même que dans la version précédente sauf que dans cette
version, on va parcourir le fichier pour amener la tête de lecture/écriture à la place k + 1, la
boucle étant contrôle par la condition (i  k) et non fin(f). Si le parcours se termine avec
l’égalité i = k + 1, on est sûr que le dernier élément lu correspond à la kème place. Il ne nous
donc plus qu’à tester l’égalité elem = élément pour conclure.
L’algorithme est alors le suivant :
procédure accèsk(nomfichier: chaîne ; k: entier ; var trouvé: booléen; var elem: télément) ;
var
i : entier ;

Dr Ndi Nyoungui André


13 Algorithmique et structures de données

é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;

Dr Ndi Nyoungui André


14 Algorithmique et structures de données

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 ;

Dr Ndi Nyoungui André


15 Algorithmique et structures de données

â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é.

7.2.5 Fichier ordonné (ou fichier trié)


Définition. Soit f un fichier dont l’ensemble des valeurs est muni d’une relation d’ordre total.
On dit que le fichier f est trié par ordre croissant si tous les couples d’éléments consécutifs de
f vérifient la relation d’ordre élément(i-1)  élément(i). Le fichier f sera dit trié par ordre
décroissant si tous les couples d’ éléments consécutifs de f vérifient la relation d’ordre
élément(i)  élément(i-1).
On admettra que le fichier vide et que tout fichier ne contenant qu’un seul élément sont des
fichiers triés.
On peut donc donner la définition suivante :
 un fichier vide est trié,
 un fichier contenant un seul élément est trié,
 un fichier f est trié si élément(i-1)  élément(i), pour toute place i.
7.2.5.1 Algorithme vérifiant qu’un fichier est trié
La méthode consiste à parcourir le fichier en comparant à chaque étape l’élément courant à
son prédécesseur. Le fichier est trié si on atteint la fin du fichier et que le dernier élément est
supérieur à l’avant dernier élément.
L’algorithme est alors le suivant :
fonction fichiertrié(nomfichier : chaîne) : booléen ;
var
précédent, courant : télément ;
f : fichier de télément;
début
relire(f, nomfichier) ;
si fin(f) alors
fichiertrié  vrai
sinon
début
lire(f, précédent) ;
si fin(f) alors

Dr Ndi Nyoungui André


16 Algorithmique et structures de données

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 ;

On peut simplifier l’initialisation en affectant précédent à courant. En effet, cela permet à


l’initialisation de donner la même valeur aux deux variables. La relation précédent  courant
est alors vraie et, si le fichier a plus d’un élément, on entre dans l’itération, sinon la fonction
prend la valeur vrai.
Cette technique, souvent utilisée en informatique, permet de simplifier les algorithmes.
L’algorithme est alors le suivant :
fonction fichiertrié(nomfichier : chaîne) : booléen ;
var
précédent, courant : télément ;
f : fichier de télément;
début
relire(f, nomfichier) ;
si fin(f) alors
fichiertrié  vrai
sinon
début
lire(f, précédent) ;
courant  précédent ;
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 ;
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.

Dr Ndi Nyoungui André


17 Algorithmique et structures de données

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é.

7.2.5.2 Algorithme d’accès à un élément dans un fichier ordonné


Dans le cas d’un accès associatif, la relation d’ordre définie sur l’ensemble des valeurs permet
d’éviter un parcours complet des éléments du fichier pour conclure à la non présence d’un
élément. En général, l’accès associatif dans un fichier ordonné est plus rapide que dans le cas
d’un fichier non ordonné. On s’efforce, dans la pratique, d’utiliser, chaque fois que cela est
possible, un fichier ordonné.
La méthode consiste à parcourir le fichier en comparant à chaque fois l’élément courant à la
valeur recherchée. La boucle étant contrôlée par la conjonction entre non fin(f) et la relation
courant < valeur. Ceci permet d’arrêter la recherche dès que la valeur de l’élément courant
devient supérieur à valeur.
L’algorithme est alors le suivant :
fonction accèstrié(nomfichier : chaîne ; valeur : télément) : booléen ;
var
élément : télément ;
f : fichier de télément;
début
relire(f, nomfichier) ;
si fin(f) alors
accèstrié  vrai
sinon
début
lire(f, élément) ;
tantque non fin(f) et (élément < valeur) faire
lire(f, élément) ;
accèstrié  valeur = élément ;
fin ;
fermer(f) ;
fin ;

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

Dr Ndi Nyoungui André


18 Algorithmique et structures de données

action itérative. Nous allons maintenant étudier quelques algorithmes classiques traitant
plusieurs fichiers à la fois.

7.3 Algorithmes traitant plusieurs fichiers

Ces algorithmes prennent des éléments dans un ou plusieurs fichiers et construisent un ou


plusieurs autres fichiers. Ils ont de nombreuses applications en gestion et en programmation
système. Tous ces algorithmes sont construits à partir du parcours des éléments de un ou
plusieurs fichiers appelés fichiers pilotes, fichiers sources, fichiers privilégiés ou fichiers
directeurs. Ces algorithmes s’expriment tous par une séquence d’actions itératives.
7.3.1 Algorithme de création, par copie, d’un fichier
La méthode consiste tout simplement à effectuer un parcours du fichier directeur et à copier
chaque élément du fichier directeur dans le fichier de sortie.
L’algorithme est alors le suivant :
procédure copiefichier(origine, copie : chaîne) ;
var
élément : télément ;
f, g : fichier de télément;
début
relire(f, origine) ;
réécrire(g, copie) ;
tantque non fin(f) faire
début
lire(f, élément) ;
écrire(g, élément) ;
fin ;
fermer(f) ; fermer(g) ;
fin ;

7.3.2 Concaténation de deux fichiers


La concaténation de deux fichiers f et g est un fichier h obtenu en copiant les éléments de g à
la suite des éléments de f. On note h = f || g la concaténation des fichiers f et g.
La méthode consiste simplement à copier les éléments de f dans h, puis à compléter h par les
éléments de g.
L’algorithme est alors le suivant :
procédure concaténation(source1, source2, sortie : chaîne) ;
var
f, g, h : fichier de télément;
élément : télément ;
début
reliref(f, source1)
relire(g, source2) ;
réécrire(h, sortie) ;
{copie de f dans h}
tantque non fin(f) faire
début

Dr Ndi Nyoungui André


19 Algorithmique et structures de données

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 ;

7.3.3 Éclatement d'un fichier en plusieurs autres fichiers


On veut construire m fichiers f1, …, fm à partir d’un seul fichier directeur f en utilisant un
critère d’éclatement critère(). Ce critère est une application totale définie sur l’ensemble des
valeurs du fichier directeur et permet de classer de façon unique chaque élément du fichier
directeur. La solution consiste à effectuer un parcours du fichier directeur f et, pour chaque
élément rencontré on calcule critère(élément), puis suivant la valeur obtenue on range
l’élément dans le fichier de sortie adéquat. L’algorithme est en principe cohérent et se termine
puisqu’il est dirigé par le fichier directeur f.
Première version. Utilisation des instructions si…alors…sinon imbriquées

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) ;
si critère(élément) = valeur1 alors
écrire(f1, élément)
sinon
si critère(élément) = valeur2 alors
écrire(f2, élément)
sinon
.
.
.
si critère(élément) = valeurm alors
écrire(fm, élément) ;
fin ;
fermer(f) ;
fermer(f1) ;
...

Dr Ndi Nyoungui André


20 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


21 Algorithmique et structures de données

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 ;

Dr Ndi Nyoungui André


22 Algorithmique et structures de données

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 ;

Dr Ndi Nyoungui André


23 Algorithmique et structures de données

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.

7.3.4 Fusion de plusieurs fichiers en un seul

Ce problème consiste à construire, à partir de plusieurs fichiers séquentiels, un seul fichier


suivant un certain critère de fusion. Nous prenons comme exemple celui de l’interclassement
de deux fichiers.
Interclassement de deux fichiers triés
Soient deux fichiers f et g à valeurs dans un même ensemble V et munis de la même relation
d’ordre. On veut construire un fichier h trié contenant tous les éléments de f et de g.
Les deux fichiers étant triés, on commence par parcourir les deux fichiers f et g en parallèle.
On sélectionne alors deux éléments cf et cg dont il faut écrire le plus petit dans le fichier h.
Ensuite, on remplace le plus petit par son élément suivant. Et ainsi de suite tant qu’il reste des
éléments à traiter.
Lorsque un des fichiers devient vide, on doit copier les éléments restants du fichier non vide.
L’algorithme comprend donc deux parties :
 le parcours en parallèle des deux fichiers f et g,
 le traitement des éléments restants du fichier non vide.
L’algorithme de fusion est alors le suivant :
procédure fusion(nomf, nomg, nomf : chaîne) ;
variable
cf, cg : télément ;
f, g, h: fichier de télément ;
début
relire(f, nomf) ;
relire(g, nomg) ;
réécrire(h, nomh) ;
si non fin(f) et non fin(g) alors
début
lire(f, cf) ;
lire(g, cg) ;
tantque non fin(f) et non fin(g) faire
si cf  cg alors
début
écrire(h, cf) ;
lire(f, cf) ;
fin

Dr Ndi Nyoungui André


24 Algorithmique et structures de données

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).

7.4 Algorithmes de mise à jour des fichiers


Il s’agit d’insérer, de supprimer ou de modifier des éléments dans un fichier. Dans un but de
simplification, on supposera que la mise à jour ne porte que sur un seul élément à la fois.
Comme pour les accès, l’élément peut être désigné soit par sa position soit par sa valeur.
D’autre part, le fichier peut trié ou non.
Étant donné que nous supposons qu’un fichier ne peut être ouvert que soit en lecture soit en
écriture, tous ces algorithmes nécessitent la création d’un nouveau fichier.

7.4.1 Insertion d’un élément dans un fichier


7.4.1.1 Aucun critère n’est donné
On dispose d’un fichier f et d’une valeur que l’on souhaite insérer dans le fichier. Aucun
critère n’étant spécifié on a deux possibilités simples : insérer l’élément en tête du fichier ou
bien l’ajouter à la fin du fichier.
Dans le premier cas, l’approche consiste à créer un nouveau fichier g, à ranger dans g la
valeur à insérer puis à recopier dans g tous les éléments de f.
L’algorithme est alors le suivant :
procédure insertête(ancien, nouveau : chaîne ; elem : télément) ;

Dr Ndi Nyoungui André


25 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


26 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


27 Algorithmique et structures de données

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 ;

7.4.1.3 Insertion associative


7.4.1.3.1 Insertion après une valeur
On veut insérer la valeur elem après ou avant (on choisit après) la valeur val dans un fichier.
Dans le cas où il y a plusieurs occurrences de la valeur val, on insérera après la première
occurrence de cette valeur.
Première version. L’approche consiste donc à rechercher la première occurrence de la valeur
val dans le fichier tout en recopiant chaque élément rencontré dans le fichier de sortie g.
Après la recopie de la première occurrence de la valeur val dans g, on insère la valeur à
insérer avant de continuer la copie du reste des éléments de f.
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 ;
f, g : fichier de télément ; 
début
relire(f, ancien) ;
réécrire(g, nouveau) ;
lire(f, élément) ;
écrire(g, élément) ;
tantque non fin(f) et (élément <> val) faire
début
lire(f, élément)
écrire(g, élément) ;
fin ;
possible  élément = val ;
si élément = val alors
début
écrire(g, elem) ;
tantque non fin(f) faire
début

Dr Ndi Nyoungui André


28 Algorithmique et structures de données

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 ;

7.4.1.3.2 Insertion dans un fichier trié


On veut ajouter un élément dans un fichier trié f de telle sorte que l’on obtienne un fichier trié
g : g contiendra tous les éléments de f et l’élément à ajouter.
On peut remarquer que dans ce cas, le problème de l’insertion est, d’un point de vue logique,
toujours possible car on trouve toujours une place appartenant à l’intervalle [1..n+1] où on
peut insérer le nouvel élément.
Première version

L’algorithme se décompose en trois parties :


 copier tous les éléments inférieurs à elem dans le fichier de sortie,
 insérer elem dans le fichier de sortie,
 copier le reste des éléments du fichier directeur dans le fichier de sortie.

Dr Ndi Nyoungui André


29 Algorithmique et structures de données

L’algorithme est alors le suivant :


procédure insertrié(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) ;
si fin(f) alors
écrire(g, elem)
sinon
début
lire(f, élément) ;
tantque non fin(f) et (élément < elem) faire
début
écrire(g, élément) ;
lire(f, élément) ;
fin ;
si élément  elem alors
début
écrire(g, elem) ;
écrire(g, élément) ;
fin 
sinon
début
écrire(g, élément) ;
écrire(g, elem) ;
fin ;
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
On utilise une seule itération avec un cas particulier pour l’insertion à la fin du fichier.
L’algorithme est alors le suivant :
procédure insertrié(ancien, nouveau : chaîne ; elem : télément) ;
variable
élément : télément;
f, g : fichier de télément ;
inférieur : booléen ;
début

Dr Ndi Nyoungui André


30 Algorithmique et structures de données

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é.

7.4.2 Suppression d’un élément dans un fichier

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.

7.4.2.1 Suppression par position


Première version. L’approche consiste à recopier dans le fichier de sortie g tous les éléments
du fichier d’entrée f en prenant soin de ne pas recopier le kème élément dans le fichier de
sortie.
L’algorithme se décompose alors en trois parties :
 copie des k - 1 premiers éléments de f dans g,
 lecture du kème élément (sans copie),
 copie des n - k derniers éléments de f dans g.
L’algorithme est alors le suivant :
procédure supprimek(ancien, nouveau : chaîne ; k : entier ; var possible : booléen) ;
variable

Dr Ndi Nyoungui André


31 Algorithmique et structures de données

é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) ;

Dr Ndi Nyoungui André


32 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


33 Algorithmique et structures de données

é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) ;

Dr Ndi Nyoungui André


34 Algorithmique et structures de données

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

Nous avons donné de nombreux exemples de procédures et de fonctions permettant de


manipuler les fichiers séquentiels.
L’accès à un élément nécessite l’énumération de tous les éléments qui le précèdent, ce qui le
rend particulièrement coûteux en temps de calcul. Si n est le nombre de places du fichier, le
coût d’accès à un élément est proportionnel à n. D’autre part, la mise à jour d’un fichier
nécessite toujours la construction d’un nouveau fichier, ce qui est également particulièrement
coûteux en temps de calcul et en espace mémoire.
L’utilisation des fichiers séquentiels pour le stockage des informations est encore très
répandue en informatique bien que les primitives disponibles soient particulièrement pauvres.

Dr Ndi Nyoungui André


35 Algorithmique et structures de données

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.

7.6 Études de cas

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é.

Gestion des étudiants

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 disposera des définitions de types suivantes :


constante
nbmax = 40 ;
type
tcours = article
Code : chaîne ;
Intitulé : chaîne ;
Note : réel ;
fin ;
vcours : vecteur[1..nbmax] de tcours ;
tfilière = article
Code : chaîne ;
Dénomination : chaîne ;
nbcours : entier ;
listecours : vcours ;
fin ;
tétudiant = article
Matricule : chaîne ;
Nom : chaîne ;
Sexe : (Féminin, Masculin) ;
filière : tfilière ;
fin ;
fétudiant = fichier de tétudiant ;

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.

1°/ Écrire une procédure qui crée un fichier de n étudiants.


2°/ Écrire une procédure qui imprime la liste générale des étudiants inscrits.

Dr Ndi Nyoungui André


36 Algorithmique et structures de données

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.

On disposera des définitions de types suivantes :


type
tenseignant = article
matricule : entier ;
nom : chaîne ;
statut : chaîne ;
département : chaîne ;
présent : booléen ;
grade : chaîne;
nbcours : entier;

Dr Ndi Nyoungui André


37 Algorithmique et structures de données

ancienneté : entier ;
fin ;
fenseignant = fichier de tenseignant ;

1°/ Écrire un algorithme pour créer un fichier d’enseignants.


1°/ Écrire un algorithme pour afficher, le matricule, le nom et le grade de chacun des
enseignants ayant un statut donné et qui sont maintenant en congé.
2°/ Écrire un algorithme pour afficher, le matricule, le nom, le statut, le grade et la position de
chacun des enseignants affectés à un département donné.
3°/ Écrire un algorithme qui délivre le nombre d’enseignants ayant un grade donné et qui sont
actuellement en congé.
4°/ Écrire un algorithme qui délivre le nombre des assistants les plus anciens ainsi que leur
nombre d’années d’ancienneté.
5°/ Écrire un algorithme qui délivre le numéro matricule et le nom du premier enseignant
ayant un grade donné.
6°/ Écrire un algorithme qui retourne le département d’affectation d’un enseignant de
matricule donné.
7°/ Écrire un algorithme qui vérifie qu’une personne de matricule donné est bien dans le
fichier et qu’elle est actuellement en poste. Si ces conditions sont remplies, un nouveau fichier
est créé où cette personne est mise en congé.
8°/ Écrire une procédure pour supprimer un enseignant de matricule donné dans le fichier des
personnels.
9°/ Écrire un algorithme qui éclate le fichier des personnels en deux fichiers : un contenant les
personnels en poste et un autre les personnels en congé.

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 ;

Dr Ndi Nyoungui André


38 Algorithmique et structures de données

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.

6°/ On dispose maintenant des structures de données suivantes :

Dr Ndi Nyoungui André


39 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


40 Algorithmique et structures de données

Chapitre 8

Les listes linéaires chaînées

8.1 Introduction

Structures de données dynamiques et statiques

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.

Dr Ndi Nyoungui André


41 Algorithmique et structures de données

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.

Gestion dynamique de la mémoire


Imaginons que la mémoire soit représentée par un réservoir de cellules. Les deux primitives
nouveau et libérer permettent de gérer ce réservoir. Les procédures nouveau et libérer peuvent
être définies de la manière suivante :
La procédure nouveau permet à chaque appel d’obtenir une nouvelle cellule dont l’adresse
sera retournée dans p.
procédure nouveau(var p : pélément) ;
début
si réservoir non vide alors
p  adresse d’une nouvelle cellule
sinon
p  nil ;
fin ;

Dr Ndi Nyoungui André


42 Algorithmique et structures de données

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.

On peut illustrer l’allocation dynamique de la mémoire comme l’affectation d’une valeur à un


emplacement mémoire référencé par un pointeur et la libération de cet emplacement mémoire
pour d’autres utilisations par le diagramme suivant :
variable
p : pélément ;

???
p
nouveau(p) ;

???
p p^
p^  10 ;

10

p p^
libérer(p) ;

???
p

On utilise la notation p^ pour désigner la variable que le pointeur p référence. La valeur


effective de p est bien entendu une adresse. Le processus peut être résumé de la manière
suivante. Initialement, le pointeur p est indéfini. L’appel de procédure nouveau(p) alloue de la
mémoire pour p^ et affecte à p l’adresse de p^. L’affectation p^  10 affecte la valeur 10 à
p^, l’emplacement mémoire référencé par p. Enfin, libérer(p) libère l’emplacement mémoire
référencé par p pour une ré-allocation ultérieure et rend p indéfini.

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

Dr Ndi Nyoungui André


43 Algorithmique et structures de données

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 ;

Utilisation des pointeurs


Les valeurs des pointeurs sont des adresses et sont par conséquent représentées dans le
système de représentation interne utilisée pour de telles valeurs. Par contre, les variables
référencées par des pointeurs peuvent être utilisées comme toutes les autres variables de
même type. Par exemple, on peut lire ou écrire les valeurs référencées par les pointeurs de la
manière suivante :
lire(p^) ;
écrire(p^) ;

Dr Ndi Nyoungui André


44 Algorithmique et structures de données

Soient deux pointeurs p et q pointant respectivement sur les valeurs a et b.

a b
p p^ q q^

L’exécution de l’instruction q  p ; permet d’obtenir :

a b
p p^, q^ q

Par contre, l’exécution de l’instruction q^  p^ ; permet d’obtenir :

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.

8.2 Les structures linéaires chaînées


Les pointeurs sont habituellement utilisés pour référencer des articles plutôt que de simples
types de données scalaires. En outre, les pointeurs sont utilisés pour construire des structures
chaînées. Pour construire une structure linéaire chaînée, on a besoin d’articles qui contiennent
non seulement des données mais aussi un champ qui est un pointeur sur un autre article de
même type. De tels articles sont souvent appelés des articles auto-référentiels.
On aura donc un couple composé d’une partie donnée et d’une partie adresse que nous
appellerons cellule ou élément. La partie donnée, comme dans le cas des vecteurs ou des
fichiers pourra être simple ou structurée. On notera qu’une cellule est connue par son adresse.

Dr Ndi Nyoungui André


45 Algorithmique et structures de données

Figure 8.1. représentation d’une cellule


CEL

partie donnée partie adresse


p
a
r
CELLULE

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.

Dr Ndi Nyoungui André


46 Algorithmique et structures de données

Définition d’une liste linéaire chaînée


Une liste linéaire chaînée (ou plus simplement liste chaînée) est une collection linéaire
d’articles auto-référentiels, appelés cellules, reliés par des pointeurs de lien. Le terme liste
chaînée trouve son origine dans le fait que les différentes cellules d’une telle liste forment une
chaîne que l’on peut suivre, jeton par jeton, cellule par cellule. On accède à une liste chaînée
par l’intermédiaire d’un pointeur sur la première cellule de la liste. Cette adresse doit se
trouver dans une variable que nous appellerons très souvent liste. Les cellules suivantes sont
accessibles par le champ pointeur stocké dans chacune des cellules. Le pointeur de liaison de
la dernière cellule d’une liste reçoit la valeur nil afin d’indiquer la fin de la liste. Les données
sont stockées de manière dynamique dans une liste chaînée : chaque cellule est créée dès que
nécessaire. Une cellule peut contenir des données de n’importe quel type.
Les listes de données peuvent être stockées sous forme de vecteurs mais les listes chaînées
offrent plusieurs avantages. Une liste chaînée est appropriée lorsque le nombre d’éléments à
représenter dans la structure de données n’est pas immédiatement prévisible. Les listes
chaînées sont dynamiques, de sorte que la longueur d’une liste peut croître ou décroître en
fonction des nécessités.
Par convention, on notera [liste] la suite des éléments contenus dans la liste dont l’adresse de
la première cellule se trouve dans la variable liste.
Par exemple, la liste de données (a, b, c, d) est représentée par la liste linéaire chaînée :
liste

a b c d

et [liste] = (a, b, c, d).


On notera que :
 si liste = nil alors [liste] est vide ; [liste] = (),
 il y a autant de cellules que d’éléments dans la liste,
 le champ suivant d’une cellule contient l’adresse de la cellule suivante,
 il suffit de connaître l’adresse de la première cellule rangée dans la variable liste pour
avoir accès à tous les éléments en utilisant les notations liste^.donnée, liste^.suivant,
liste^.suivant^.donnée, … On rappelle que liste^.donnée et liste^.suivant ne sont
définis que si liste est différent de nil,
 par rapport aux vecteurs, il y a dissociation entre l’ordre logique et l’ordre physique
des éléments,

Création d’une liste linéaire chaînée

a) Passage d’une représentation fichier à une représentation liste chaînée


On se proposer de créer une liste chaînée à partir d’un fichier d’éléments de type télément.

Première version

Dr Ndi Nyoungui André


47 Algorithmique et structures de données

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

L’exécution de la boucle tantque conduit à l’ajout du nouvel élément en tête de liste :

nouveau(courant) ;

courant dernier

Dr Ndi Nyoungui André


48 Algorithmique et structures de données

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 ;

Dr Ndi Nyoungui André


49 Algorithmique et structures de données

f : fichier de télément ;


début
relire(f , nomfichier) ;
si fin(f) alors
liste  nil
sinon
début
nouveau(liste) ;
lire(f, élément) ;
liste^.donnée  élément ;
dernier  liste ;
{Lire les autres éléments et les ajouter dans la liste}
tantque non fin(f) faire
début
lire(f, élément) ;
nouveau(courant) ;
dernier^.suivant  courant;
dernier  courant ;
courant^.donnée  élément ;
fin ;
dernier^.suivant  nil ;
fin ;
fermer(f) ;
fin;
Pour obtenir les éléments du fichier dans le même ordre, on doit créer la liste à partir de son
premier élément. Pour ce faire, on doit d’abord prévoir le cas du premier élément qui permet
la création de la première cellule de la liste dont l’adresse doit être préservée car elle
permettra , comme nous l’avons vu, l’accès à toutes les cellules créées. Ensuite, pour chaque
nouvel élément du fichier, on crée une nouvelle cellule que l’on insère en fin de liste et non
plus en tête.
Ainsi, au départ, on crée la première cellule de la liste. Ensuite, on procède à l’insertion des
éléments restants dans la liste. Pour chaque élément, on procède de la manière suivante : on
crée dynamiquement une nouvelle cellule en utilisant la procédure nouveau. L’instruction
« dernier^.suivant  courant » permet au champ suivant de dernière cellule insérée de pointer
sur la nouvelle cellule. La nouvelle cellule est ainsi reliée à la queue de la liste. La valeur de
courant est affectée à dernier tel que dernier continue à pointer sur la dernière cellule de la
liste. La figure ci-dessous illustre ce processus.
Création de la première cellule :

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 :

Dr Ndi Nyoungui André


50 Algorithmique et structures de données

liste

??
dernier courant

nouveau(courant) ;

liste

dernier courant
dernier^.suivant  courant ;

liste

dernier courant

dernier  courant ;
liste

dernier courant

Dr Ndi Nyoungui André


51 Algorithmique et structures de données

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 :

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) ;
nouveau(liste) ;
dernier  liste ;
tantque non fin(f) faire
début
lire(f, élément) ;
nouveau(courant) ;
dernier^.suivant  courant;
dernier  courant ;
courant^.donnée  élément;
fin ;
dernier^.suivant  nil ;
fermer(f) ;
fin ;

b) Passage d’une représentation vecteur à une représentation liste chaînée

Dr Ndi Nyoungui André


52 Algorithmique et structures de données

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 ;

c) Création d’une liste chaînée à partir d’un ensemble quelconque d’éléments


On se propose de créer une liste chaînée à partir d’éléments lus directement au clavier. Le
raisonnement presque est identique à celui que nous avons présenté précédemment.
Première version

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 ;

Dr Ndi Nyoungui André


53 Algorithmique et structures de données

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 ;

Parcours d’une liste chaînée


Une liste étant donnée, il s’agit de parcourir les différents éléments de la liste. Ce parcours
peut se faire de la gauche vers la droite ou de la droite vers la gauche. Pour chaque élément de
la liste, un traitement particulier est appliqué.
Schéma récursif
Pour l’écriture des algorithmes sous forme récursive, on utilise la définition suivante d’une
liste chaînée :
Une liste linéaire chaînée est :
 soit une liste vide (liste = nil),
 soit composée d’une cellule (la première) chaînée à une sous-liste (obtenue après
suppression de la première cellule).

Parcours de gauche à droite


procédure parcoursgd(liste : pélément) ;

Dr Ndi Nyoungui André


54 Algorithmique et structures de données

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 ;

8.4 Algorithmes sur les listes chaînées

On se propose d’écrire des algorithmes de traitement d’une liste chaînée en commençant


d’abord par l’écriture, sous forme itérative et récursive, des éléments d’une liste.
a) Écriture des éléments d’une liste chaînée
L’écriture de gauche à droite d’une liste chaînée correspondra au premier parcours alors que
l’écriture de droite à gauche correspondra au deuxième parcours.
Écriture de gauche à droite
Schéma récursif

Dr Ndi Nyoungui André


55 Algorithmique et structures de données

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 ;

Écriture de droite à gauche


On utilise le deuxième parcours, parcoursdg, qui permet d’obtenir l’écriture des éléments à
partir du dernier élément de la liste.
L’algorithme est le suivant :
procédure écrirelistedg(liste : pélément) ;
début
si liste  nil alors
début
écrirelistegd(liste^.suivant) ;
écrire(liste^.donnée) ;
fin ;
fin ;

Exercices d’apprentissage

Exercice 8.3
Que fait la procédure suivante ?
procédure parcoursliste(liste : pélément) ;
début

Dr Ndi Nyoungui André


56 Algorithmique et structures de données

si liste  nil faire


début
écrire(liste^.donnée) ;
parcoursliste(liste^.suivant) ;
écrire(liste^.donnée) ;
fin ;
fin ;
Exercice 8.4
Que fait la procédure suivante ?
procédure parcoursliste(liste : pélément) ;
début
si liste  nil faire
début
parcoursliste(liste^.suivant) ;
écrire(liste^.donnée) ;
parcoursliste(liste^.suivant) ;
fin ;
fin ;

b) Calcul de la longueur (nombre d’éléments) d’une liste chaînée


On définit la longueur d’une liste comme étant le nombre d’éléments significatifs de la liste.
Éléments significatifs parce que dans le cas d’une liste avec une sentinelle en tête, le nombre
d’éléments significatifs est égal au nombre de cellules de la liste moins un.
Schéma itératif
On utilise la version itérative du parcours de gauche à droite en remplaçant l’instruction
traiter(liste) par l’instruction n  n + 1. Au départ, on initialise n à zéro.
L’algorithme est alors le suivant :
fonction longueurliste(liste : pélément) : entier ;
variable
courant : pélément ;
n : entier ;
début
courant  liste ;
n  0 ;
tantque courant  nil faire
début
n  n + 1 ;
courant  courant^.suivant ;
fin ;
longueurliste  n ;
fin ;
Schéma récursif
Le raisonnement est le suivant : lorsqu’on entre dans la fonction la première fois, la valeur du
paramètre liste est comparée à nil. Si elle est en effet égale à nil, on sait que la liste est vide et
on retourne la valeur zéro. Si par contre la liste n’est pas vide, on invoque encore la fonction

Dr Ndi Nyoungui André


57 Algorithmique et structures de données

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 ;

c) Calcul du nombre d’occurrences d’un élément donné dans une liste


Schéma itératif
On utilise le parcours de gauche à droite. Pour chaque cellule rencontrée, on incrémente le
compteur d’occurrences de 1 si la donnée de la cellule courante est égale à élément.
L’algorithme est alors le suivant :
fonction nombrefois(liste : pélément ; élément : télément) : entier ;
variable
courant : pélément ;
compte : entier ;
début
courant  liste ;
compte  0 ;
tantque courant  nil faire
début
si courant^.donnée = élément alors
compte  compte + 1 ;
courant  courant^.suivant ;
fin ;
nombrefois  compte ;
fin ;
Schéma récursif
Le raisonnement est le suivant : lorsqu’on entre dans la fonction la première fois, on compare
la valeur du paramètre à nil. Si elle est en effet égale à nil, on sait que la liste est vide, et on
retourne la valeur zéro. Si elle n’est pas nil, on compare liste^.donnée à élément. S’il y a
égalité, on sait que l’on a trouvé une occurrence de élément ; le nombre d’occurrences de
élément dans la liste est donc égale à un plus le nombre d’occurrences de élément dans la liste
dont le pointeur de tête est liste^.suivant. S’il n’y pas égalité ; le nombre d’occurrences de
élément dans liste est tout simplement égal au nombre d’occurrences de élément dans la liste
dont le pointeur de tête se trouve dans liste^.suivant.
L’algorithme est alors le suivant :
fonction nombrefois(liste : pélément ; élément : télément) : entier ;
début
si liste = nil alors

Dr Ndi Nyoungui André


58 Algorithmique et structures de données

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.

8.4.4 Accès dans une liste chaînée


Comme nous l’avons vu dans les chapitres précédents, on distingue deux sortes d’accès :
l’accès par position et l’accès par valeur.
a) Accès par position dans une liste chaînée

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 ;

Dr Ndi Nyoungui André


59 Algorithmique et structures de données

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

On peut réécrire cet algorithme en faisant décrémenter le compteur à chaque itération. On


initialise le compteur à k et on s’arrête lorsque le compteur prend la valeur 1 ou lorsqu’on
atteint la fin de la liste.
L’algorithme devient alors le 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  k ;
tantque (i > 1) et (courant  nil) faire
début
i  i - 1 ;
courant  courant^.suivant ;
fin ;
trouvé  (i = 1) et (courant  nil)
pk  courant ;
fin ;
On peut éliminer le paramètre trouvé en établissant la convention suivante :
 pk contient l’adresse du kème élément s’il existe ;
 pk contient la valeur nil si le kème élément n’existe pas.
On peut alors écrire une fonction qui retourne l’adresse du kème élément s’il existe et nil s’il
n’existe pas.
fonction pointeurk(liste : pélément ; k : entier) : pélément;
variable
courant : pélément ;
i : entier ;
début
courant  liste ;
i  1 ;

Dr Ndi Nyoungui André


60 Algorithmique et structures de données

tantque (i < k) et (courant  nil) faire


début
i  i + 1 ;
courant  courant^.suivant ;
fin ;
si courant  nil alors
pointeurk  courant
sinon
pointeurk  nil;
fin ;
On peut réécrire la fonction ci-dessus sous forme récursive. Le raisonnement est fondé sur la
constatation suivante : chercher le kème (k > 1) élément dans une liste revient à cherche le (k-
1)ème élément dans liste^.suivant.
On obtient alors l’algorithme suivant :
fonction pointeurk(liste : pélément ; k : entier) : pélément;
début
si liste = nil alors
pointeurk  liste
sinon
si k = 1 alors
pointeurk  liste
sinon
pointeurk  pointeurk(liste^.suivant, k-1);
fin ;
On remarque pour k = 1 et liste = nil, le résultat de la fonction pointeurk prend la même
valeur ; celle contenue dans la variable liste. On peut donc combiner ces deux conditions par
le connecteur « ou » car elles sont indépendantes l’une de l’autre, et réaliser ainsi une mise en
facteur de l’instruction « pointeurk  liste ».
L’algorithme devient alors :
fonction pointeurk(liste : pélément ; k : entier) : pélément;
début
si (liste = nil) ou (k = 1) alors
pointeurk  liste
sinon
pointeurk  pointeurk(liste^.suivant, k-1);
fin ;

b) Accès associatif dans une liste chaînée


Schéma itératif
On peut utiliser le même raisonnement que dans le cas des fichiers séquentiels.
L’algorithme est alors le suivant :
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 ;

Dr Ndi Nyoungui André


61 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


62 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


63 Algorithmique et structures de données

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 ;

Dr Ndi Nyoungui André


64 Algorithmique et structures de données

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 ;

On peut mettre en facteur l’instruction « pointeurval  nil » en utilisant le connecteur « ou


sinon ». On obtient alors la version suivante :
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  nil

Dr Ndi Nyoungui André


65 Algorithmique et structures de données

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.

8.4.5 Recherche d’une sous-liste dans une liste

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].

Rechercher la sous-liste [sliste] dans la liste [liste]


revient à chercher si [sliste] est un préfixe d’une
sous-liste 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.

Recherche d’une sous-liste

Schéma itératif

L’algorithme consiste à parcourir la liste en examinant à chaque étape si sliste est un préfixe
la sous-liste restante.

Dr Ndi Nyoungui André


66 Algorithmique et structures de données

L’algorithme est alors le suivant :


fonction recherchesliste(liste, sliste : pélément) : booléen ;
variable
p : pélément ;
début
p  liste ;
tantque (p  nil) et non préfixe(p, sliste) faire
p  p^suivant ;
recherchesliste  p  nil ;
fin ;

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 ;

Écriture de la fonction préfixe

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

L’algorithme est le suivant :


fonction préfixe(liste, sliste : pélément) : booléen ;
début

Dr Ndi Nyoungui André


67 Algorithmique et structures de données

si liste = nil alors


préfixe  faux
sinon
si liste^.donnée  sliste^.donnée alors
préfixe  faux
sinon
si sliste^.suivant = nil alors
préfixe  vrai
sinon
préfixe  préfixe(liste^.suivant, sliste^.suivant) ;
fin ;
A chaque appel, la condition sliste  nil est vérifiée. On note, comme cela est très souvent le
cas, que le schéma récursif est plus concis que le schéma itératif correspondant.
On peut donner une autre version sans la condition sliste  nil et en utilisant un ou sinon.
L’algorithme est alors le suivant :
fonction préfixe(liste, sliste : pélément) : booléen ;
début
si sliste = nil alors
préfixe  vrai
sinon
si (liste = nil) ou sinon (liste^.donnée  sliste^.donnée) alors
préfixe  faux
sinon
si sliste^.suivant = nil alors
préfixe  vrai
sinon
préfixe  préfixe(liste^.suivant, sliste^.suivant) ;
fin ;

8.5 Algorithmes de mise à jour d’une liste chaînée


Ces algorithmes ont une grande importance pratique en raison de leur facilité de mise en
œuvre. Nous verrons en effet que la mise à jour d’une liste n’entraîne que la modification
d’un ou deux pointeurs sans recopie ni décalage des éléments non concernés par la mise à
jour. Nous écrivons d’abord les algorithmes d’insertion et nous terminons par les algorithmes
de suppression. Dans les deux cas, on distinguera les cas particuliers (insertion en tête de liste,
insertion en fin de liste, suppression en tête de liste) du cas général (insertion ou suppression
par position ou associative).
8.5.1 Insertion dans une liste chaînée
a) Insertion d’un élément en tête de liste
C’est un algorithme très important que nous utiliserons très souvent pour réaliser les autres
insertions. D’autre part, on notera que c’est un cas très particulier car l’insertion en tête
modifie l’adresse de la liste. On peut schématiser cet algorithme comme suit :

Dr Ndi Nyoungui André


68 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


69 Algorithmique et structures de données

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.

fonction dernier(liste : pélément) : pélément ;


début
si liste^.suivant = nil alors
dernier  liste
sinon
dernier  dernier(liste^.suivant) ;

Dr Ndi Nyoungui André


70 Algorithmique et structures de données

fin ;
Nous pouvons maintenant écrire l’algorithme d’insertion en fin de liste.

Schéma itératif

procédure insertfin(var liste : pélément ; élément : télement) ;


variable
courant, der : pélément ;
début
si liste = nil alors
insertête(liste, élément)
sinon
début
der  dernier(liste) ;
nouveau(courant) ;
courant^.donnée  élément ;
courant^.suivant  nil ;
der^.suivant  courant ;
fin ;
fin ;
On remarque que les quatre dernières instructions correspondent à l’appel de la procédure
insertête(der^.suivant, élément). En effet, insérer en fin de liste est équivalent à insérer en tête
de la liste qui est pointée par le dernier élément.
L’algorithme devient alors :
procédure insertfin(var liste : pélément ; élément : télément) ;
variable
der : pélément ;
début
si liste = nil alors
insertête(liste, élément)
sinon
début
der  dernier(liste) ;
insertête(der^.suivant, élément) ;
fin ;
fin ;
Schéma récursif
On peut également donner une version récursive de la procédure insertfin en utilisant la
procédure insertête.
L’algorithme est alors le suivant :
procédure insertfin(liste : pélément ; élément : télément) ;
début
si liste = nil alors
insertête(liste, élément)
sinon
insertfin(liste^.suivant, élément)

Dr Ndi Nyoungui André


71 Algorithmique et structures de données

fin ;
c) Insertion d’un nouvel élément à la kième place

On peut schématiser le problème de la manière suivante :


liste

(k-1) (k)

(b) (a)

p élément à insérer

Cas particulier (k = 1) : insertion en tête de liste

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) ;

Dr Ndi Nyoungui André


72 Algorithmique et structures de données

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 ;

Dr Ndi Nyoungui André


73 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


74 Algorithmique et structures de données

courant  pointeurval(liste, val) ;


si courant = nil alors
possible  faux
sinon
début
possible  vrai ;
insertête(courant^.suivant, élément) ;
fin ;
fin ;
On peut également écrire cet algorithme sous forme récursive.
Schéma récursif
procédure inseraprès(liste : pélément ; val, élément : télément ; var possible : booléen) ;
début
si liste = nil alors
possible  faux
sinon
si liste^.donnée = val alors
début
possible  vrai ;
insertête(liste^.suivant, élément) ;
fin ;
sinon
insertaprès(liste^.suivant, val, élément, possible) ;
fin ;
Exercices d’apprentissage
Exercice 8.16
Écrire, sous forme itérative et sous forme récursive, l’algorithme d’insertion d’un élément
avant la première occurrence d’une valeur.
Exercice 8.17
Écrire, sous forme itérative et sous forme récursive, l’algorithme d’insertion d’un élément
après chaque occurrence d’une valeur.
Exercice 8.18
Écrire, sous forme itérative et sous forme récursive, l’algorithme d’insertion d’un élément
avant chaque occurrence d’une valeur.
8.5.1.5 Insertion dans une liste triée
Cet algorithme est très important et, comme nous l’avons vu dans le cas des vecteurs, on peut
toujours réaliser cette insertion d’un point de vue logique. On considère que la liste est la
concaténation de deux sous-listes [la] et [lb] telles que :
[liste] = [la] || [lb] et [la] < élément  [lb]
Nous avons choisi de mettre l’inégalité au sens large à droite afin de minimiser le temps de
parcours de la liste alors que, dans le cas des vecteurs, nous avions choisi de mettre l’inégalité
à gauche pour minimiser le nombre de décalages à effectuer.

Dr Ndi Nyoungui André


75 Algorithmique et structures de données

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

[<p] < elem, précédent = D<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

Dr Ndi Nyoungui André


76 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


77 Algorithmique et structures de données

liste

(b) (a)

sliste

L’algorithme est alors le suivant :


procédure insertsliste(liste, sliste : pélément ; val : télément ; var possible : booléen) ;
variable
pval, der : pélément ;
début
pval  pointeurval(liste, val) ;
si pval = nil alors
possible  faux
sinon
début
der  dernier(sliste) ;
possible  vrai;
der^.suivant  pval^.suivant;
pval^.suivant  sliste ;
fin ;
fin ;
Exercice 8.20
On suppose que l’on a les définitions de type suivantes :
type
pétudiant = ^tétudiant ;
tétudiant = article
nom : chaîne ;
prénom : chaîne ;
suivant : pétudiant ;
fin ;
Écrire une procédure qui prend en entrée un fichier d’étudiants et les insère dans une liste
chaînée par ordre alphabétique. Si deux étudiants ont le même nom, on les classe par rapport à
leurs prénoms.
8.5.2 Suppression d’un élément dans une liste chaînée

Dr Ndi Nyoungui André


78 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


79 Algorithmique et structures de données

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

Au préalable, on aura pris soin de traiter le cas de la suppression du premier élément.


L’algorithme est alors le suivant :
procédure supprimek(var liste : pélément ; k : entier ;  var possible : booléen) ;
variable
pk, 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 alors
début
pk  précédent^.suivant ;
si pk  nil alors
début
possible  vrai ;
précédent^.suivant  pk^.suivant ;
libérer(pk) ;
fin ;
fin ;
fin ;
fin ;

En utilisant la procédure supprimetête, on obtient la version suivante :


procédure supprimek(var liste : pélément ; k : entier ;  var possible : booléen) ;
variable

Dr Ndi Nyoungui André


80 Algorithmique et structures de données

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 ;

8.5.2.3 Suppression associative


Les algorithmes sont semblables aux précédents et nous donnons l’algorithme correspondant à
la suppression de la première occurrence de la valeur val dans une liste. Il faut également
définir une variable booléenne possible qui nous permettra de savoir si la suppression a été
réalisée ou non.
Schéma récursif

Dr Ndi Nyoungui André


81 Algorithmique et structures de données

procédure supprimeval(var liste : pélément ; val : télément;  var possible : booléen) ;


début
si liste  nil alors
possible  faux 
sinon
si liste^.donnée = val alors
début
supprimetête(liste) ;
possible  vrai;
fin
sinon
supprimeval(liste^.suivant, val, possible) ;
fin ;
Schéma itératif
Après avoir traité le cas de la suppression du premier élément, on utilise le raisonnement
suivant :
Pour supprimer la cellule contenant la première occurrence de la valeur val, il faut connaître
l’adresse de la cellule précédente. Pour déterminer cette adresse, on peut, comme dans le cas
des insertions, utiliser une variable auxiliaire précédent ou utiliser les variables courant et
courant^.suivant pour disposer à chaque étape de l‘adresse de la cellule courante et de celle de
la cellule précédente. Nous choisissons la deuxième alternative.
L’algorithme est alors le suivant :
procédure supprimeval(var liste : pélément ; val : télément;  var possible : booléen) ;
variable
p : pélément ;
début
possible  faux ;
si liste  nil alors
si liste^.donnée = val alors
début
supprimetête(liste) ;
possible  vrai;
fin
sinon
début
p  liste ;
tantque (p  nil) et alors (p^.suivant^.donnée  val) faire
p  p^.suivant ;

si (p^.suivant  nil) et alors (p^.suivant^.donnée = val) alors


début
possible  vrai ;
supprimetête(p^.suivant) ;
fin ;
fin ;
fin ;

Dr Ndi Nyoungui André


82 Algorithmique et structures de données

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 ?

8.6 Autres exemples d’algorithmes sur les listes


a) Concaténation de deux listes
À partir de deux listes chaînées [liste1] et [liste2] possédant des informations de même type,
on désire construire une liste concaténation des listes [liste1] et [liste2], notée liste1 || liste2.
Il suffit de réaliser la liaison (a) selon le schéma ci-dessous et de définir liste :
liste1

liste
(a)

liste2

Dr Ndi Nyoungui André


83 Algorithmique et structures de données

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.

L’algorithme est alors le suivant :


procédure concaliste(liste1, liste2 : pélément ; var liste : pélément) ;
variable
der : pélément ;
début
si liste1 = nil alors
liste  liste2
sinon
début
liste  liste1 ;
si liste2  nil alors
début
der  dernier(liste1) ;
der^.suivant  liste2
fin ;
fin ;
fin ;
Remarque
Les listes [liste1], [liste2] et [liste] ne sont plus indépendantes ; en effet, une mise à jour de
[liste] a des répercussions sur [liste1) et/ou [liste2]. Nous donnons, ci-après, une autre version
de concaliste permettant la création d’une liste concaténation indépendante des listes initiales
[liste1] et [liste2]. Nous utilisons pour cela une procédure de création, par copie, d’une liste
chaînée.

b) Algorithme de création par copie d’une liste chaînée


Il faut créer une nouvelle liste d’adresse copie telle que :
[copie] = [liste], copie  liste.
Nous allons utiliser les deux parcours que nous avons précédemment.
Schéma récursif
Première version
On construit [copie] à partir du premier élément de [liste]. Il faut donc faire, à chaque pas une
insertion à la fin de la liste en cours de construction.
Au départ, il faut initialiser copie à nil (liste vide). Cette initialisation ne peut être effectuée
qu’à l’appel de la procédure.
L’algorithme est alors le suivant :
procédure copieliste(liste : pélément ; var copie : pélément) ;

Dr Ndi Nyoungui André


84 Algorithmique et structures de données

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;

Dr Ndi Nyoungui André


85 Algorithmique et structures de données

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 ;

c) Création de la liste miroir d’une liste


Définition de la liste miroir
 la liste miroir d’une liste vide est vide,
 la liste miroir d’une liste est égale la liste formée des éléments de la liste de départ
enregistrés dans l’ordre inverse.
En nous inspirant des algorithmes de copie d’une liste, nous allons donner ci-dessous deux
versions récursives permettant de construire la liste miroir d’une liste.
Première version

Dr Ndi Nyoungui André


86 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


87 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


88 Algorithmique et structures de données

La structure de pile est utilisée pour sauvegarder temporairement des informations en


respectant leur ordre d’entrée, et les réutiliser en ordre inverse. Une pile est liste linéaire, elle
peut donc être représentée de façon contiguë ou chaînée.
1. Écrire une procédure empiler qui a pour effet d’insérer un élément dans une pile.
2. Écrire une procédure dépiler qui a pour effet de supprimer l’élément au sommet de la
pile et de retourner sa valeur dans une variable.
3. Écrire une fonction pilevide qui vérifie qu’une pile est vide.
4. Écrire une procédure initpile qui initialise une pile.
5. Écrire une procédure sommetpile qui retourne la valeur de l’élément qui se trouve au
sommet de la pile.
Exercice 8.28
Les files d’attente ou queues. Une queue est semblable à une file d’attente à une caisse d’un
supermarché. La première personne en ligne est servie en premier et les autres clients qui
entrent à la fin de la queue attendent d’être servis à leur tour. Les cellules sont retirées
uniquement à la tête de la queue et insérés uniquement à la fin. C’est pourquoi on désigne
souvent la queue comme étant une structure de données « premier arrivé, premier servi ». En
anglais on dira « first-in, first-out ou en abrégé FIFO ».
Les queues ont de nombreuses applications dans les systèmes informatiques. La plupart des
ordinateurs n’ont qu’un seul processeur, de sorte qu’un seul utilisateur de ce processeur peut
être servi à la fois. Les entrées des autres utilisateurs doivent être placées dans une queue.
Chacun des entres avance progressivement vers le début de la queue, à mesure que les
utilisateurs sont servis. L’entrée qui se présente à l’avant de la queue est celle qui est sur le
point d’obtenir le service.
Une queue est définie à tout moment par sa tête et par sa queue.
1. Écrire une procédure ajoutqueue qui permet d’ajouter un élément dans une queue.
2. Écrire une procédure suppqueue qui permet de supprimer un élément dans une queue
et de retourner la valeur de l’élément supprimer.
3. Écrire une fonction queuevide qui vérifie qu’une queue est vide.
4. Écrire une procédure initqueue qui initialise une queue.

Dr Ndi Nyoungui André


89 Algorithmique et structures de données

8.8 Études de cas

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 ;

On suppose que tous les auteurs ont des noms différents.


1°/ Écrire un algorithme pour créer une liste chaînée d’ouvrages à partir d’un fichier.
2°/ Écrire , sous forme itérative et sous forme récursive, un algorithme qui imprime le nom de
l’auteur, le nom de l’éditeur, le titre de l’ouvrage, la discipline et l’année d’édition de tous les
ouvrages enregistrés.
3°/ Écrire, sous forme itérative et sous forme récursive, une fonction retourne le nombre
d’ouvrages présents dans la liste.
4°/ Écrire, sous forme itérative et sous forme récursive, une fonction qui retourne le nombre
d’ouvrages écrits par un auteur donné.
5°/ Écrire, sous forme itérative et sous forme récursive, une fonction qui retourne un pointeur
sur la kème cellule de rang k de la liste. La fonction retourne nil si la cellule de rang k
n’existe pas dans la liste.
6°/ Écrire, sous forme itérative et sous forme récursive, une fonction qui retourne un pointeur
sur le dernier livre d’une discipline donnée contenu dans une liste. La fonction retourne nil si
aucun live de mathématiques ne figure dans la liste.
7°/ On suppose que la liste est triée par ordre alphabétique sur les noms des auteurs. Écrire,
sous forme itérative et sous forme récursive, une fonction qui délivre un pointeur sur le
premier ouvrage écrit par un auteur donné. La fonction retourne nil si cet auteur ne figure pas
dans la liste.

Dr Ndi Nyoungui André


90 Algorithmique et structures de données

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 ;

Dr Ndi Nyoungui André


91 Algorithmique et structures de données

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 :

 tous les vols ont des numéros différents.


 toutes les compagnies ont des noms différents.
 le nombre des passagers pour un vol est limité par le nombre de places disponibles.
 le nombre vols proposés par une compagnie est illimité.
 la liste des compagnies est ordonnée par ordre alphabétique sur les noms des
compagnies.

Parcours de liste et comptage d’éléments


1°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre de
compagnies qui n’ont pas de vol programmé.
2°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre de
compagnies ayant un seul vol programmé.
3°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre total de
vols programmés.
4°/ Écrire, sous itérative et sous forme récursive, une fonction qui retourne le nombre de vols
programmés par la compagnie de nom nomcomp.
5°/ Un crash s’est produit. On désire connaître le nom de la compagnie concernée. Écrire une
fonction qui retourne un pointeur sur la compagnie concernée.

Mises à jour d’éléments

Dr Ndi Nyoungui André


92 Algorithmique et structures de données

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

Amougou Hamadou Kamgang

On dispose des définitions suivantes :


type
pfamille = ^tfamille ;
tfamille = article
nom : chaîne ;
suivant : pfamille ;
fin ;
1°/ Écrire, sous forme itérative et sous forme récursive, une fonction retourne le nombre de
familles dans une liste.
2°/ Écrire, sous forme itérative et sous forme récursive, une fonction qui détermine si une
famille de nom donné est présente dans la liste.
3°/ Écrire, sous forme itérative et sous forme récursive, une procédure qui insère une nouvelle
famille dans la liste.
4°/ Écrire, sous forme itérative et sous forme récursive, une procédure qui supprime une
famille de nom donné de la liste.
Deuxième partie : liste à plusieurs niveaux

Dr Ndi Nyoungui André


93 Algorithmique et structures de données

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.

Parcours de liste et comptage d’éléments

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.

Dr Ndi Nyoungui André


94 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


95 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


96 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


97 Algorithmique et structures de données

Chapitre 9

Les structures dynamiques non


linéaires

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

De nombreuses informations peuvent être représentées sous forme arborescente : arbre


généalogique, dictionnaire, structure syntaxique, etc.
Un type structuré dans un langage de programmation peut être représenté sous la forme d’une
arborescence.
Par exemple le type étudiant :
type
tdate = article
jour, mois, année : entier ;
fin ;
tfilière = article
nomfil : chaîne ;
niveau : entier ;
fin ;
tuniversité = article
nomuni : chaîne ;
faculté : chaîne ;
option : tfilière ;
fin ;

Dr Ndi Nyoungui André


98 Algorithmique et structures de données

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

inscription matricule identité

nomuni option faculté nom prénom datenais lieunais

nomfil niveau jour mois année

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

Dr Ndi Nyoungui André


99 Algorithmique et structures de données

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

dans la grammaire G = (V, T, E, P) où les ensembles V et T sont définis par


V = {E, I} et T = {a, b, c, +, *, (, )},
et l’ensemble des productions est : E  I, E  E + E, E  E * E, E  (E), I  a|b|c.
Une expression arithmétique peut être représentée sous la forme d’un arbre. Par exemple, en
utilisant la priorité des opérateurs usuels, l’expression (c * d div (a mod b)) + b – c * d mod a
admet pour représentation :

+ mo
d

div b * a

* mo c d
d

c d a b

Dr Ndi Nyoungui André


100 Algorithmique et structures de données

9.1.2 Définitions, notations et représentation


Nous avons remarqué dans le chapitre précédent que les schémas récursifs simplifiaient
beaucoup l’écriture des algorithmes sur les listes chaînées. Nous utilisions, pour ce faire, la
définition récursive suivante d’une liste chaînée :
Une liste chaînée est :
 soit vide,
 soit constituée d’une cellule chaînée à une liste.
Il en est de même pour les arbres, où on peut donner la définition suivante :
Une arbre est :
 soit vide,
 soit constitué d’un élément auquel sont chaînés un ou plusieurs arbres.
9.1.2.1 Terminologie

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

9.1.2.2 Arbre n-aire, arbre binaire


Lorsqu’un arbre admet, pour chaque nœud, au plus n fils, l’arbre est dit n-aire. Si n est égal à
deux, l’arbre est dit binaire. Étant donné que tout arbre général (n-aire) admet un équivalent
binaire, nous allons étudier uniquement les arbres binaires. Pour les arbres binaires, on parlera
de fils gauche et de fils droit ainsi que de sous-arbre gauche et de sous-arbre droit.
On écrira très souvent les algorithmes sur les arbres sous forme récursive qui est beaucoup
plus concise et naturelle que sous forme itérative. Nous donnerons ensuite quelques règles
simples de transformation d’un schéma récursif en un schéma itératif. Pour l’écriture des
algorithmes récursifs sur les arbres binaires, on utilisera la définition récursive suivante :
Un arbre binaire est :
 soit vide,

Dr Ndi Nyoungui André


101 Algorithmique et structures de données

 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

peut être dénoté


A(B(C(( ), ( )), D(( ), ( ))), E(( ), F(( ), ( ))))
où () est la dénotation d’un arbre vide.
On peut simplifier cette expression en supprimant les arbres vides. On obtient alors
A(B(C, D), E( ,F))
qui est une notation linéaire parenthésée plus complexe.
On remarquera que c’est une généralisation des notations utilisées pour les expressions
préfixées et les listes linéaires.
Numérotation de Dewey
Pour tracer un parcours ou, plus généralement, un algorithme sur un arbre, on peut numéroter
chaque nœud comme suit :
 la racine est numérotée 1,
 le sous-arbre gauche est numéroté 1,
 le sous-arbre droit est numéroté 2,
 les numéros sont séparés par des points.

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

Dr Ndi Nyoungui André


102 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


103 Algorithmique et structures de données

permet d’accéder à la donnée contenue dans la racine, racine^.gauche permet d’accéder au


sous-arbre gauche et racine^.droite permet d’accéder au sous-arbre droit.

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.

Arbre binaire complet

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é

Dr Ndi Nyoungui André


104 Algorithmique et structures de données

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)

ou (nœud^.droite = nil, nœud^.gauche  nil, nœud^.gauche^.donnée  nœud^.donnée)

ou (nœud^.droite  nil, nœud^.gauche = nil, nœud^.donnée  nœud^.droite^.donnée).


On admettra également qu’un arbre vide ou réduit à un seul élément est ordonné. Tous les
sous-arbres d’un arbre ordonné sont ordonnés.
On peut également écrire :
[nœud^.gauche]  nœud^.donnée  [nœud^.droite]
Dans toutes les applications que nous envisagerons, nous choisirons de ne laisser l’inégalité
au sens large que d’un seul côté, par exemple à droite, d’où :
[nœud^.gauche] < nœud^.donnée  [nœud^.droite]

9.2 Parcours d’un arbre binaire


Ce problème est analogue à celui du parcours séquentiel d’une liste chaînée où nous avons
défini deux parcours récursifs : traitement des éléments de « gauche à droite » ou de « droite à
gauche ». Les algorithmes de parcours sont très importants car ils servent de base à l’écriture
de très nombreux algorithmes. Il y a plusieurs manières de parcourir un arbre suivant l’ordre
dans lequel on énumère un nœud et ses sous-arbres gauche et droite. Nous nous limitons ici
aux trois parcours classiques : préfixé, infixé et postfixé.
Parcours préfixé (ou en préordre)

Cette méthode consiste à parcourir l’arbre dans l’ordre :


traitement de la racine
parcours du sous-arbre gauche
parcours du sous-arbre droit
Parcours infixé (appelé aussi projectif ou symétrique)
Cette méthode consiste à parcourir l’arbre dans l’ordre :
parcours du sous-arbre gauche
traitement de la racine

Dr Ndi Nyoungui André


105 Algorithmique et structures de données

parcours du sous-arbre droit


Parcours postixé (appelé aussi ordre terminal)
Cette méthode consiste à parcourir l’arbre dans l’ordre :
parcours du sous-arbre gauche
parcours du sous-arbre droit
traitement de la racine

9.2.1 Parcours préfixé d’un arbre binaire


Schéma récursif
Par définition du parcours préfixé, l’algorithme est le suivant :
procédure parcourspréfixé(racine : pélément) ;
début
si racine  nil alors
début
traiter(racine) ;
parcourspréfixé(racine^.gauche) ;
parcourspréfixé(racine^.droite) ;
fin ;
fin ;

Transformation du schéma récursif en schéma itératif


Nous allons effectuer cette transformation en deux étapes. Chacune de ces étapes consiste à
supprimer un appel récursif.
La première transformation est du même type que celle que nous avons utilisée dans le cas
des listes linéaires chaînées où nous n’avions, à chaque exécution de la fonction ou de la
procédure, qu’un seul appel récursif. Cette transformation a pour objet de remplacer un appel
récursif conditionnel par un schéma itératif équivalent.
La deuxième transformation est plus complexe et nécessite l’utilisation d’une pile afin de
sauvegarder l’environnement de l’appel récursif qui est indispensable à la poursuite de
l’algorithme au retour de celui-ci.

Première transformation
Pour tout schéma récursif de la forme (I) ci-dessous

procédure récursif(x : télément) ;


début
si condition(x) alors
début
(x) ;
récursif(f(x)) ;
fin ;
fin ;
le schéma itératif équivalent est :

Dr Ndi Nyoungui André


106 Algorithmique et structures de données

procédure itératif(x : télément) ;


variable
y : télément ;
début
y  x ;
tantque condition(y) faire
début
(y) ;
y  f(y) ;
fin ;
fin ;

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 ;

Application à la procédure préfixé


(racine) correspond à :
traiter(racine) ;
parcourspréfixé(racine^.gauche) ;
f(racine) correspond à :

Dr Ndi Nyoungui André


107 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


108 Algorithmique et structures de données

Cette deuxième transformation peut s’appliquer à tout algorithme de la forme :


procédure itérécursif(x : télément) ;
début
tantque condition(x) faire
début
1(x) ;
itérécursif(f(x)) ;
2(x) ;
fin ;
fin ;
pour obtenir :
procédure itératif(x : télément) ;
variable
y : télément ;
début
initpilevide ;
y  x ;
tantque condition(x) ou non pilevide faire
début
tantque condition(y) faire
début
1(y) ;
empiler(y) ;
y  f(y) ;
fin ;
dépiler(y) ;
2(x) ;
fin ;
fin ;

Cette transformation peut également s’appliquer à une fonction de la même forme.

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.

9.2.2 Parcours infixé d’un arbre binaire


Schéma récursif
D’après la définition du parcours infixé on obtient :
procédure parcoursinfixé(racine : pélément) ;
début

Dr Ndi Nyoungui André


109 Algorithmique et structures de données

si racine  nil alors


début
parcoursinfixé(racine^.gauche) ;
traiter(racine) ;
parcoursinfixé(racine^.droite) ;
fin ;
fin ;
Schéma itératif
On peut appliquer la première transformation pour supprimer le deuxième appel récursif :
(racine) correspond à :
parcoursinfixé(racine^.gauche) ;
traiter(racine) ;
f(racine) correspond à :
racine^.droite ;
L’algorithme devient alors :
procédure parcoursinfixé1(racine : pélément) ;
variable
rac : pélément ;
début
rac  racine ;
tantque rac  nil faire
début
parcoursinfixé1(rac^.gauche) ;
traiter(rac) ;
rac  rac^.droite ;
fin ;
fin ;
On peut maintenant appliquer la deuxième transformation :
1(rac) est vide
f(rac) correspond à :
rac^.gauche ;
2(rac) correspond à :
traiter(rac) ;
rac  rac^.droite ;
L’algorithme est alors le suivant :
procédure parcoursinfixé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
empiler(rac) ;

Dr Ndi Nyoungui André


110 Algorithmique et structures de données

rac  rac^.gauche ;
fin ;
dépiler(rac) ;
traiter(rac) ;
rac  rac^.droite ;
fin ;
fin ;

9.2.3 Parcours postfixé d’un arbre binaire


Schéma récursif
D’après la définition du parcours postfixé on obtient :
procédure parcourspostfixé(racine : pélément) ;
début
si racine  nil alors
début
parcourspostfixé(racine^.gauche) ;
parcourspostfixé(racine^.droite) ;
traiter(racine) ;
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 ;

Dr Ndi Nyoungui André


111 Algorithmique et structures de données

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 ;

9.3 Algorithmes sur les arbres binaires


Nous allons mettre en œuvre la définition d’un arbre binaire et les différents parcours d’arbres
binaires ci-dessus pour écrire quelques algorithmes sur les arbres binaires.

9.3.1 Calcul de la taille d’un arbre binaire

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.

fonction taillearbre(racine : pélément) : entier ;


début
si racine = nil alors
taillearbre  0
sinon
taillearbre  1 + taillearbre(racine^.gauche) + taillearbre(racine^.droite)
fin ;
Schéma itératif
On peut effectuer, entre autres, un parcours préfixé, infixé ou postfixé de l’arbre. On utilise le
schéma itératif correspondant dans lequel il suffit de remplacer l’instruction traiter(racine) par
n  n + 1 en ayant pris soin d’initialiser n à zéro. Nous donnons ci-dessous la version qui
correspond au parcours préfixé.

Dr Ndi Nyoungui André


112 Algorithmique et structures de données

fonction taillearbre(racine : pélément) : entier ;


variable
n : entier ;
rac : pélément ;
début
initpilevide(pile) ;
rac  racine ;
n  0 ;
tantque (rac  nil) ou non pilevide faire
début
tantque rac  nil faire
début
n  n + 1 ; 
empiler(rac) ;
rac  rac^.gauche ;
fin ;
dépiler(rac) ;
rac  rac^.droite ;
fin ;
taillearbre  n ;
fin ;

9.3.2 Calcul du nombre de feuilles d’un arbre binaire


Rappelons qu’une feuille est un nœud terminal, c’est-à-dire un nœud qui n’a pas de fils. On
peut déterminer si un nœud est une feuille avec la fonction suivante :
fonction feuille(nœud : pélément) : booléen ;
début
feuille  (nœud^.gauche = nil) et (nœud^.droite = nil) ;
fin ;

Schéma récursif

En utilisant la fonction feuille() ci-dessus, une version récursive de l’algorithme de calcul du


nombre de feuilles d’un arbre binaire est la suivante :

fonction nbfeuille(racine : pélément) ;


début
si racine = nil alors
nbfeuille  0
sinon
si feuille(racine) alors
nbfeuille  1
sinon
nbfeuille  nbfeuille(racine^.gauche) + nbfeuille(racine^.droite) ;
fin ;

Schéma itératif

Dr Ndi Nyoungui André


113 Algorithmique et structures de données

Il suffit d’utiliser le schéma itératif du parcours préfixé en remplaçant traiter(rac) par si


feuille(rac) alors nb  nb + 1 et en initialisant nb avec la valeur zéro.
L’algorithme est alors le suivant :
fonction nbfeuille(racine : pélément) : entier ;
variable
nb : entier ;
rac : pélément ;
début
initpilevide ;
rac  racine ;
nb  0 ;
tantque (rac  nil) ou non pilevide faire
début
tantque rac  nil faire
début
si feuille(rac) alors
nb  nb + 1 ; 
empiler(rac) ;
rac  rac^.gauche ;
fin ;
dépiler(rac) ;
rac  rac^.droite ;
fin ;
nbfeuille  nb ;
fin ;

9.3.3 Vérifier qu’un arbre binaire n’est pas dégénéré


Rappelons qu’un arbre est dégénéré si aucun nœud ne possède plus de un fils. On souhaite
écrire une procédure qui vérifie qu’un arbre est normal (ni dégénéré ni vide), c’est-à-dire qui
cherche s’il existe au mois un nœud qui possède deux fils.
Schéma récursif
fonction arbrenormal(racine : pélément) : booléen ;
début
si racine = nil alors
arbrenormal faux
sinon
si (racine^.gauche  nil) et (racine^.droite  nil) alors
arbrenormal  vrai
sinon
si racine^.gauche = nil alors
arbrenormal  arbrenormal(racine^.droite)
sinon
arbrenormal  arbrenormal(racine^.gauche) ;
fin ;
Schéma itératif

Dr Ndi Nyoungui André


114 Algorithmique et structures de données

On va transformer le schéma récursif à l’aide de la première transformation. En effet, on sort


de la fonction après chacun des appels récursifs, il n’y a donc aucune action à exécuter.
Dans le schéma récursif, on a deux cas de base :
 [racine] est vide,
 racine a deux fils.
Dans le schéma itératif, on aura donc une itération avec deux conditions :
 [racine] est non vide : racine  nil,
 racine n’a pas deux fils : (racine^.gauche = nil) ou (racine^.droite = nil).
L’algorithme est alors le suivant :
fonction arbrenormal(racine : pélément) : entier ;
variable
rac : pélément ;
début
initpilevide ;
rac  racine ;
tantque (rac  nil) et alors ((rac^.gauche = nil) ou (rac^.droite = nil)) faire
si rac^.gauche = nil alors
rac  rac^.droite 
sinon
rac  rac^.gauche ;
arbrenormal  rac  nil ;
fin ;
Exercice 9.1
Écrire, sous forme récursive et sous forme itérative, un algorithme qui vérifie qu’un arbre
binaire est ordonné.
9.3.4 Recherche associative dans un arbre binaire

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

Dr Ndi Nyoungui André


115 Algorithmique et structures de données

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 ».

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) ou sinon (recherche(racine^.gauche)) alors
recherche  vrai
sinon
recherche  recherche(racine^.droite, valeur) ;
fin ;
Schéma itératif
On ne peut pas utiliser la première transformation. En effet, si la valeur recherchée n’est pas
présente dans le sous-arbre gauche, on est amené à effectuer deux appels récursifs. Nous
allons donc écrire une transformation utilisant une pile. Il faut effectuer un parcours (par
exemple préfixé) de l’arbre et s’arrêter dès qu’on trouve la valeur val. Pour cela, on ajoute une
variable booléenne trouvé qui permettra de s’arrêter en cas de succès.
L’algorithme est alors le suivant :
fonction recherche(racine : pélément ; val : télément) : booléen ;
variable
rac : pélément ;
trouvé : booléen ;
début
initpilevide ;
rac  racine ;
trouvé  faux ;
tantque ((rac  nil) ou (non pilevide)) et non trouvé faire
début
tantque rac  nil faire
début
trouvé  rac^.donnée = val; 
empiler(rac) ;
rac  rac^.gauche ;
fin ;
dépiler(rac) ;
rac  rac^.droite ;
fin ;
recherche  trouvé ;

Dr Ndi Nyoungui André


116 Algorithmique et structures de données

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.

9.4 Arbres binaires ordonnés


Rappelons qu’un arbre ordonné est tel que la valeur associée à chaque nœud est supérieure à
toutes les valeurs du sous-arbre gauche et inférieure ou égale à toutes les valeurs du sous-
arbre droit :
[nœud^.gauche] < nœud^.donnée  [nœud^.droite]
9.4.1 Recherche dans un arbre binaire ordonné
On veut écrire une fonction qui délivre un pointeur sur la première occurrence d’une valeur
dans un arbre binaire ordonné.
Version itérative
On commence par la racine de l’arbre. Si la valeur stockée dans la racine est égale à la valeur
recherchée alors on a trouvé. Si la valeur stockée dans la racine est inférieure à la valeur

Dr Ndi Nyoungui André


117 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


118 Algorithmique et structures de données

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

Dr Ndi Nyoungui André


119 Algorithmique et structures de données

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 :

procédure créerfeuille(var feuille : pélément ; élément : télément) ;


début
nouveau(feuille) ;
feuille^.donnée  élément ;
feuille^.gauche  nil ;
feuille^.droite  nil ;
fin ;
Schéma récursif
On peut donner une expression récursive de la procédure d’insertion en se basant sur la
définition récursive d’un arbre binaire ordonné. L’algorithme inspecte le nœud racine de
l’arbre. Si la racine est nil, alors on crée un arbre formé d’une seule feuille. Sinon si élément
est supérieur à la donnée stockée dans la racine, on effectue l’insertion dans le sous-arbre dont
la racine est pointée par le pointeur droit. Si élément est inférieur à la donnée stockée dans la
racine, on effectue l’insertion dans le sous-arbre dont la racine est pointée par le pointeur
droit.

Dr Ndi Nyoungui André


120 Algorithmique et structures de données

L’algorithme est alors le suivant :


procédure insère(var racine : pélément ; élément : télément) ;
début
si racine = nil alors
créerfeuille(racine, élément)
sinon
si élément  racine^.donnée alors
insère(racine^.droite, élément)
sinon
insère(racine^.gauche, élément) ;
fin ;

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

Dr Ndi Nyoungui André


121 Algorithmique et structures de données

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.

Dr Ndi Nyoungui André


122 Algorithmique et structures de données

L’algorithme est alors le suivant :


procédure créervecteur(racine : pélément ; var liste : vélément ; var i : entier) ;
début
si racine  nil alors
début
créervecteur(racine^.gauche, liste, i) ;
liste[i]  racine^.donnée ;
i  i +1 ;
créervecteur(racine^.droite, liste, i) ;
fin ;
fin ;

9.4.4 Suppression d’un élément dans un arbre binaire ordonné


On se propose d’écrire une procédure pour supprimer un nœud dans un arbre binaire ordonnée
de telle sorte que l’arbre reste ordonné. Prenons un exemple. À partir de l’arbre ci-dessous on
désire supprimer le nœud (appelé fils) contenant la valeur 5.

2
0

1 2
0 5
fils
5 1
2

3 8

1 4 6 9

[fils^.gauche] [fils^.droite]

On obtient par exemple l’arbre ordonné suivant :

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 ;

procédure supprime(var racine : pélément ; elem : télément) ;

Dr Ndi Nyoungui André


125 Algorithmique et structures de données

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.

9.6 Études de cas

Dr Ndi Nyoungui André


126 Algorithmique et structures de données

Dr Ndi Nyoungui André