Vous êtes sur la page 1sur 118

SUPPORT DE COURS

Année académique / Semestre 2023-2024/ SEMESTRE N° ….


Intitulé du cours Structures de données avancées 2

Classe / Niveau GL/ Niveau 2


Enseignant M. NDJE MAN Dieudonné François
Coordonnées Tél. : 654080512
Email : franoisndje@yahoo.fr
Volume horaire global 45 CM : 20 TD : 10 TP : 15
OBJECTIFS GÉNÉRAUX DU COURS :
Ce cours vise à fournir aux étudiants une compréhension approfondie des structures de
données avancées en utilisant le langage de programmation C.

OBJECTIFS SPECIFIQUES :

▪ Pour réussir ce cours, l'étudiant doit :


▪ Comprendre les structures et pointeurs
▪ Comprendre et manipuler les listes chainées
▪ Comprendre la gestion des fichiers
▪ Comprendre et implémenter une table de hachage
TABLE DES MATIERES
CHAPITRE 1 RAPPELS SUR LES STRUCTURES DE DONNEES .............................. 1
1.1 LA STRUCTURE .............................................................................................................. 1
1.1.1 DEFINITION ............................................................................................................... 1
1.1.2 MANIPULATION D’UNE STRUCTURE .......................................................... 4
1.1.3 STRUCTURE ET POINTEURS ........................................................................... 9
1.1.4 PORTEE ET DECLARATION ............................................................................ 14
1.1.5 PRINCIPE DES LISTES CHAINEES .............................................................. 16
1.2 EXERCICES DE FIN DE CHAPITRE ...................................................................... 21
CHAPITRE 2 LES FICHIERS .................................................................................................... 23
2.1 DEFINITION ..................................................................................................................... 23
2.1.1 EXTENSION DE FICHIER .................................................................................. 23
2.1.2 SYSTEME DE FICHIERS .................................................................................... 24
2.1.3 HIERARCHIE ........................................................................................................... 25
2.1.4 HIERARCHIE SOUS UNIX ................................................................................. 26
2.1.5 LE CHEMIN D'ACCES.......................................................................................... 27
2.1.6 METADONNEES..................................................................................................... 28
2.1.7 LES FLUX : UN PEU DE THEORIE ................................................................ 29
2.1.8 ÉCRITURE VERS UN FLUX DE TEXTE....................................................... 31
2.1.9 ÉCRITURE VERS UN FLUX BINAIRE .......................................................... 35
2.1.10 LECTURE DEPUIS UN FLUX BINAIRE ................................................... 37
2.1.11 RESUME ................................................................................................................ 38
2.2 EXERCICES DE FIN DE CHAPITRE ...................................................................... 38
2.2.1 EXERCICE 1 : OBJECTIF : MANIPULATION DE FICHIERS TEXTE
EN LANGAGE C ..................................................................................................................... 38
2.2.2 EXERCICE 2 : OBJECTIF : MANIPULATION DE FICHIERS
BINAIRES EN LANGAGE C .............................................................................................. 39
2.2.3 EXERCICE 3 : GESTION DYNAMIQUE DE LA MEMOIRE .................. 39
2.2.4 EXERCICE 4 : PROGRAMME QUI PERMET DE LIRE UN FICHIER
TEXTE 39
2.2.5 EXERCICE 2 : PROGRAMME QUI PERMET DE LIRE UN FICHIER
TEXTE ET D’EN COMPTER LE NOMBRE DE MOTS ............................................. 39
CHAPITRE 3 LES LISTES CHAINEES ................................................................................. 41
3.1 INTRODUCTION ............................................................................................................ 41
3.2 CONCEPT DE LISTE CHAINEE .............................................................................. 41
3.2.1 RAPPEL SUR LES LISTES CHAINEES ........................................................ 42
3.2.2 CONSTRUIRE UNE LISTE CHAINEE ........................................................... 43
3.2.3 GEREZ LA LISTE A L'AIDE DE FONCTIONS ........................................... 45
3.2.4 OPERATION SUR UNE LISTE CHAINEE ................................................... 46
3.3 CONCEPT DE PILE ET DE FILE ............................................................................. 50
3.4 LISTE CHAINEE SIMPLE (FILE) ............................................................................ 51
3.4.1 LA CONSTRUCTION DU PROTOTYPE D'UN ELEMENT DE LA FILE
53
3.4.2 OPERATIONS SUR LES FILES ....................................................................... 53
3.5 LISTE CHAINEE SIMPLE (PILE) ............................................................................ 60
3.5.2 OPERATIONS SUR LES PILES ....................................................................... 63
3.6 LISTE DOUBLEMENT CHAINEE ............................................................................ 71
3.6.1 INTRODUCTION .................................................................................................... 71
3.6.2 LA CONSTRUCTION DU PROTOTYPE D'UN ELEMENT DE LA
LISTE 73
3.6.3 OPERATIONS DE LISTE DOUBLEMENT CHAINEE .............................. 74
3.7 EXERCICES DE FIN DE CHAPITRE .................................................................... 104
3.7.1 EXERCICE 1 .......................................................................................................... 104
3.7.2 EXERCICE 2 .......................................................................................................... 105
3.7.3 EXERCICE 3 .......................................................................................................... 105
3.7.4 EXERCICE 4 .......................................................................................................... 105
3.7.5 EXERCICE 5 (Files)............................................................................................ 106
3.7.6 EXERCICE 6 (Piles et Files) .......................................................................... 107
CHAPITRE 4 TABLE DE HACHAGE ................................................................................... 108
4.1 INTRODUCTION .......................................................................................................... 108
4.2 CONCEPT ....................................................................................................................... 109
4.2.1 DEFINITION ET PRESENTATION ............................................................... 109
4.2.2 PRESENTATION .................................................................................................. 110
4.2.3 INTERET D'UNE TABLE DE HACHAGE .................................................... 110
CHAPITRE 1 RAPPELS SUR LES STRUCTURES DE
DONNEES
Les structures de données en C sont des moyens permettant de regrouper différentes
variables sous un même nom. Elles offrent la possibilité de créer des types de données
personnalisés en regroupant des données hétérogènes sous une seule entité. Les structures
de données sont un élément fondamental de la programmation en C, car elles permettent de
représenter et de manipuler des informations de manière organisée. On en distingue plusieurs
parmi lesquelles nous pouvons citer :
Les structures de données en C sont des composants essentiels pour organiser et manipuler
des données de manière efficace. Voici une exploration plus détaillée des structures de
données couramment utilisées en C :
▪ Tableau (Array) : structure de données linéaire qui stocke des éléments du même type
sous un seul nom. Les éléments sont accessibles à l'aide d'indices entiers.
▪ Structure (Struct) : structure permet de regrouper des éléments de différents types sous
un même nom. Elle est utile pour représenter des entités complexes.
▪ Liste chaînée (Linked List) : structure de données dynamique qui consiste en des
nœuds liés les uns aux autres. Chaque nœud contient une valeur et une référence
(pointeur) vers le nœud suivant.
▪ Pile (Stack) : structure de données linéaire basée sur le principe Last In, First Out
(LIFO). Les éléments sont ajoutés et retirés uniquement du sommet de la pile.
▪ File (Queue) : structure de données linéaire basée sur le principe First In, First Out
(FIFO). Les éléments sont ajoutés à l'arrière et retirés du devant de la file.
▪ Arbre (Tree) : structure de données hiérarchique constituée de nœuds. Chaque nœud
a un parent et zéro ou plusieurs enfants.
▪ Graphe (Graph) : structure de données composée de nœuds (sommets) et d'arêtes qui
les relient. Les graphes peuvent être dirigés ou non dirigés.
Ces structures de données fournissent des outils puissants pour résoudre une variété de
problèmes algorithmiques. Le choix de la structure de données dépend du problème
spécifique que vous cherchez à résoudre et des opérations que vous prévoyez d'effectuer
fréquemment sur les données.

1.1 LA STRUCTURE

1.1.1 DEFINITION
Une structure est un moyen de regrouper des variables de types différents sous un seul nom.
Cela permet de créer des enregistrements complexes.
• Chaque élément déclaré à l’intérieur de la structure est appelé un champ ou membre.

1
• Le nom donné à la structure est appelé étiquette de la structure.

1.1.1.1 SYNTAXE DÉCLARATIVE


Les syntaxes suivantes permettent de déclarer une structure en C :
struct NomDeLaStructure {
// Membres de la structure
TypeDeMembre1 NomMembre1;
TypeDeMembre2 NomMembre2;
// ... (autres membres)
};
Ou
typedef struct NomDeLaStructure {
// Membres de la structure
TypeDeMembre1 NomMembre1;
TypeDeMembre2 NomMembre2;
// ... (autres membres)
} NomAlias;
Exemple 1.1 : Déclaration d’une structure nommée Étudiant qui représente les informations
associées à un étudiant. La structure a trois membres :
▪ nom: Un tableau de caractères (chaîne de caractères) de taille 50, utilisé pour stocker
le nom de l'étudiant.
▪ age: Une variable de type entier (int), utilisé pour stocker l'âge de l'étudiant.
▪ moyenne: Une variable de type flottant (float), utilisé pour stocker la moyenne de
l'étudiant.
#include <stdio.h>
// Définition de la structure
struct Etudiant {
char nom[50];
int age;
float moyenne;
};
Ou
typedef struct Etudiant
{
char nom[50];
int age;
float moyenne;
} Etudiant;
Le tableau ci-dessous compare ces deux approches pour la définition d'une structure en
langage C : l'utilisation de typedef et la définition traditionnelle sans typedef.

2
1.1.1.2 REPRESENTATION EN MEMOIRE

En C, la représentation en mémoire d'une structure dépend de plusieurs facteurs, tels


que l'alignement de la mémoire et le type de données que la structure contient. La
mémoire est généralement organisée de manière à être efficace en termes d'accès et
d'utilisation.
La mémoire pourrait être organisée comme suit pour une instance de la structure
Etudiant :
---------------------------------------------
| nom (50 octets) | age (4 octets) | moyenne (4 octets) |
---------------------------------------------
Chaque membre de la structure occupe une certaine quantité d'octets en mémoire.
Dans cet exemple, le tableau de caractères `nom` occupe 50 octets, l'entier `age`
occupe 4 octets, et le nombre à virgule flottante (réel) `moyenne` occupe également 4
octets. La taille totale de la structure est donc de 58 octets.

3
1.1.1.2.1 ALIGNEMENT DE LA MEMOIRE

Les compilateurs C peuvent introduire des octets de bourrage 1pour garantir que
chaque membre de la structure commence à une adresse mémoire compatible avec
son type de données. La taille totale de la structure peut être plus grande en raison de
l'alignement de la mémoire.
Par exemple, si l'alignement de la mémoire pour les entiers est sur 4 octets, le
compilateur pourrait ajouter 2 octets de bourrage après le tableau de caractères `nom`
(50 octets) et 2 octets de bourrage après l'entier `age` (4 octets) pour s'assurer que la
`moyenne` commence à une adresse qui est un multiple de 4.
Ainsi, la représentation exacte en mémoire dépend des spécificités de la plateforme,
du compilateur et des options de compilation.
1.1.1.2.2 ACCES AUX MEMBRES DE LA STRUCTURE

Lorsque vous accédez aux membres d'une structure en C, le compilateur utilise l'offset
de chaque membre 2par rapport au début de la structure pour accéder directement à
la bonne position en mémoire. Par exemple, pour accéder à la valeur de l'âge dans
une instance de la structure `Etudiant` :
struct Etudiant etudiant;
int valeurAge = etudiant.age;
Le compilateur sait que l'âge est situé à un certain offset par rapport au début de la
structure et génère le code nécessaire pour accéder directement à cette position en
mémoire.

1.1.2 MANIPULATION D’UNE STRUCTURE


La manipulation d'une structure en langage C implique l'accès et la modification des membres
de la structure via diverses opérations telles que l'initialisation, la modification et l'affichage
des valeurs de la structure. Considérons la structure Etudiant créé ci-dessus. La manipuler
revient à effectuer les opérations ci-dessous :

1Les "octets de bourrage" sont des octets supplémentaires ajoutés à une structure de données pour
garantir un alignement correct des membres de la structure en mémoire. Les architectures matérielles
ont souvent des exigences d'alignement pour améliorer l'efficacité des accès aux données. Ainsi, les
compilateurs insèrent des octets de bourrage entre les membres d'une structure afin de s'assurer que
chaque membre commence à une adresse mémoire qui est une multiple de la taille de son type. Bien
que cela puisse augmenter la taille totale de la structure, cela permet d'optimiser les performances
d'accès aux données, en particulier sur des architectures spécifiques.

2 L'offset d'un membre dans une structure représente la distance, en octets, entre le début de la
structure et le début du membre spécifié. C'est l'index ou la position relative d'un membre par rapport
au début de la structure. 'offset est important lors de l'accès aux membres d'une structure, car il permet
au compilateur de calculer où se trouve chaque membre en mémoire par rapport au début de la structure

4
a. INITIALISATION ET AFFICHAGE
Objectif : Créer une variable de la structure et afficher ses valeurs.
Dans la fonction main, nous allons créer une variable de type struct Etudiant nommée
etudiant1 et la déclarer puis l’initialiser avec des valeurs spécifiques, par la suite, accéder aux
membres de la structure accéder aux membres en utilisant l’opérateur (point) et afficher les
informations correspondantes. Nous utiliserons la fonction strcpy pour attribuer une nouvelle
valeur à la chaîne de caractères nom de la structure etudiant2. Avant l'appel à strcpy, la chaîne
de caractères nom de etudiant2 était initialement vide car la structure etudiant2 a été déclarée
sans initialisation explicite. Enfin, les informations de cette structure seront affichées à l'aide
de la fonction printf.
Implémentation sans typedef
#include <stdio.h>
// Définition de la structure
struct Etudiant {
char nom[50];
int age;
float moyenne;
};
int main() {
// Déclaration et initialisation d'une variable de la structure Etudiant
struct Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};
// Affichage des informations
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);
// Autre manière d'initialiser
struct Etudiant etudiant2;
strcpy(etudiant2.nom, "SILINOU");
etudiant2.age = 20;
etudiant2.moyenne = 17.5;
// Accès aux membres et Affichage des informations de la première structure
printf("Informations de l'étudiant 1 :\n");
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n\n", etudiant1.moyenne);
// Accès aux membres et Affichage des informations de la deuxième structure
printf("Informations de l'étudiant 2 :\n");
printf("Nom: %s\n", etudiant2.nom);
printf("Age: %d\n", etudiant2.age);
printf("Moyenne: %.2f\n", etudiant2.moyenne);
return 0;
}

Implémentation avec typedef


#include <stdio.h>
#include <string.h> // Inclure la bibliothèque pour utiliser la fonction
strcpy
// Définition de la structure

5
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Déclaration et initialisation d'une variable de la structure
Etudiant
Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};
// Affichage des informations
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);
// Autre manière d'initialiser
Etudiant etudiant2;
strcpy(etudiant2.nom, "SILINOU");
etudiant2.age = 20;
etudiant2.moyenne = 17.5;
// Accès aux membres et Affichage des informations de la première
structure
printf("Informations de l'étudiant 1 :\n");
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n\n", etudiant1.moyenne);

// Accès aux membres et Affichage des informations de la deuxième


structure
printf("Informations de l'étudiant 2 :\n");
printf("Nom: %s\n", etudiant2.nom);
printf("Age: %d\n", etudiant2.age);
printf("Moyenne: %.2f\n", etudiant2.moyenne);

return 0;
}

Comparaison des deux codes

Si nous voulons afficher les informations de plusieurs étudiants, il faudrait créer un tableau de
structures Etudiant et initialiser chaque élément du tableau avec des données spécifiques.
6
Ensuite, écrire une boucle for qui va parcourir le tableau et afficher les informations pour
chaque étudiant.
#include <stdio.h>
// Définition de la structure
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Déclaration et initialisation d'un tableau de structures Etudiant
Etudiant etudiants[3] = {
{"SANI NGATCHUI", 20, 19.7},
{"NJOYA SAMUEL", 22, 19.5},
{"FOMO NDANDA", 21, 19.2}
};
// Affichage des informations pour chaque étudiant
for (int i = 0; i < 3; ++i) {
printf("Étudiant %d:\n", i + 1);
printf("Nom: %s\n", etudiants[i].nom);
printf("Age: %d\n", etudiants[i].age);
printf("Moyenne: %.2f\n", etudiants[i].moyenne);
printf("\n");
}

return 0;
}

b. MODIFICATION DES VALEURS


Objectif : Modifier les valeurs des membres d'une structure après son initialisation.
Dans le code ci-dessous nous allons modifier les valeurs de la structure initiale pour l’étudiant
1 et afficher les informations mises à jour en changeant le nom en "NJOYA SAMUEL", l'âge
est modifié à 21, et la moyenne est mise à jour à 19.5.
#include <stdio.h>
#include <string.h> // inclure la bibliothèque pour utiliser strcpy
// Définition de la structure
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Initialisation d'une structure Etudiant
Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};
// Modification des valeurs
strcpy(etudiant1.nom, "NJOYA SAMUEL");
etudiant1.age = 21;
etudiant1.moyenne = 19.5;
// Affichage des informations après modification
printf("Nom: %s\n", etudiant1.nom);

7
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);

return 0;
}
Exercice d'Application : Gestion des Étudiants
Objectif : Écrire un programme qui permet à l'utilisateur de saisir les informations de plusieurs
étudiants, puis affiche ces informations à l’écran.
Instructions :
1. Déclaration de la structure :
- Définissez une structure nommée `Etudiant` avec les membres suivants :
- nom (tableau de caractères de taille 50),
- age (entier),
- moyenne (nombre à virgule flottante).
2. Programme principal :
- Demandez à l'utilisateur de saisir le nombre d'étudiants (n).
- Déclarez un tableau de structures `Etudiant` avec une taille égale à n.
- Utilisez une boucle pour permettre à l'utilisateur de saisir les informations de chaque
étudiant (nom, âge, moyenne).
- Utilisez une autre boucle pour afficher les informations de chaque étudiant après leur saisie.
3. Exemple d'exécution :
- Le programme doit demander à l'utilisateur de saisir le nombre d'étudiants.
- Ensuite, pour chaque étudiant, le programme doit permettre à l'utilisateur de saisir le nom,
l'âge et la moyenne.
- Enfin, le programme doit afficher les informations de tous les étudiants.
Remarques :
- Assurez-vous de gérer correctement les espaces dans les noms des étudiants lors de la
saisie.
- Vous pouvez ajouter des vérifications pour vous assurer que les données saisies sont valides
(par exemple, l'âge ne peut pas être négatif).

Note 1.1 : Nous pouvons écrire la structure dans un autre fichier hors du code main () principal.
Pour l’appeler, vous devez séparer la déclaration de la structure de sa définition. Vous déclarez
la structure dans un fichier d'en-tête (header file), et vous définissez la structure dans un fichier
source (source file). Ensuite, vous incluez le fichier d'en-tête dans le fichier source où vous
souhaitez utiliser la structure.
Fichier d'en-tête (etudiant.h)
#ifndef ETUDIANT_H
#define ETUDIANT_H
// Définition de la structure

8
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;
#endif // ETUDIANT_H

Fichier source (main.c)


#include <stdio.h>
#include <string.h>
#include "etudiant.h" // Inclut le fichier d'en-tête
int main() {
// Déclaration d'une variable de type Etudiant
Etudiant etudiant1;
// Initialisation des membres de la structure
strcpy(etudiant1.nom, "SANI NGATCHUI");
etudiant1.age = 20;
etudiant1.moyenne = 18.5;
// Affichage des informations
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);
return 0;
}

1.1.3 STRUCTURE ET POINTEURS


Les structures et les pointeurs sont des concepts souvent associés. Une structure est un
moyen de regrouper des variables de types différents sous un même nom. Elle permet de
créer des types de données personnalisés regroupant des éléments apparentés. Un pointeur
est une variable qui contient l'adresse mémoire d'une autre variable. Il permet d'accéder et de
manipuler directement la mémoire, offrant des fonctionnalités avancées telles que la gestion
dynamique de la mémoire.

Les structures sont utilisées pour créer des objets complexes en regroupant plusieurs types
de données sous un seul nom, ce qui facilite la gestion et la manipulation de données
structurées. Les pointeurs sont utilisés pour des tâches avancées, telles que la gestion de la
mémoire dynamique, la manipulation d'adresses mémoire, et la création de structures de
données complexes.
Interaction entre Structures et Pointeurs
▪ Allocation Dynamique : Les pointeurs sont souvent utilisés avec des structures pour
allouer dynamiquement de la mémoire. Par exemple, l'allocation dynamique d'une
structure en C se ferait avec malloc ou calloc.
▪ Accès aux Éléments : Les pointeurs permettent d'accéder aux éléments d'une structure
de manière dynamique en utilisant l'opérateur de flèche (->). Par exemple, si p est un
pointeur vers une structure Point, p->x accède à la variable x de cette structure.

9
▪ Passage par Référence : Les pointeurs peuvent être utilisés pour passer des structures
par référence à des fonctions, permettant des modifications directes sur la structure
d'origine.
a. POINTEURS ET STRUCTURES
Objectif : Utiliser des pointeurs pour accéder et manipuler les membres d'une structure.
En nous servant de notre Structure Etudiant, nous allons utiliser un pointeur pour accéder aux
membres de la structure, ce qui va nous offrir une manière alternative d'interagir avec les
données de la structure. Le code ci-dessous illustre cela.
#include <stdio.h>
// Définition de la structure
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Initialisation d'une structure Etudiant
Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};

// Déclaration d'un pointeur vers la structure


Etudiant *ptrEtudiant = &etudiant1;
// Accès aux membres via le pointeur
printf("Nom: %s\n", ptrEtudiant->nom);
printf("Age: %d\n", ptrEtudiant->age);
printf("Moyenne: %.2f\n", ptrEtudiant->moyenne);

return 0;
}

1.1.3.1 ALLOCATION ET DESALLOCATION DE LA MEMOIRE


L'allocation de mémoire est le processus de réservation d'espace mémoire pour les données
nécessaires à l'exécution d'un programme. Elle peut être effectuée de deux manières :

▪ Allocation statique : la mémoire est allouée lors de la compilation du programme. Elle


est libérée automatiquement à la fin du programme.
▪ Allocation dynamique : la mémoire est allouée à l'exécution du programme. Elle doit
être libérée manuellement par le programmeur.
L'allocation et la désallocation de mémoire en langage C, en particulier en ce qui concerne les
structures et les pointeurs, peuvent être réalisées à l'aide des fonctions `malloc`, `calloc`,
`realloc` et `free`.
La désallocation de mémoire est le processus de libération de l'espace mémoire qui n'est plus
nécessaire. Elle est nécessaire pour éviter les fuites de mémoire, qui peuvent entraîner des
problèmes de performance et de stabilité. La désallocation de la mémoire allouée statiquement

10
est effectuée automatiquement par le système d'exploitation. La désallocation de la mémoire
allouée dynamiquement doit être effectuée manuellement par le programmeur.
Exemple : Le code ci-dessous crée dynamiquement un tableau d'entiers de taille 10 en utilisant
la fonction `malloc`, puis remplit ce tableau avec des valeurs correspondant à leurs indices à
l'aide d'une boucle. Enfin, il libère la mémoire allouée dynamiquement à l'aide de la fonction
`free` pour éviter les fuites de mémoire.
// Allocation de mémoire dynamique
int* tableau = (int*)malloc(10 * sizeof(int));

// Utilisation de la mémoire allouée


for (int i = 0; i < 10; i++) {
tableau[i] = i;
}
// Désallocation de la mémoire
free(tableau);
Le tableau ci-dessus permet de présenter les deux concepts.

Allocation Dynamique de Mémoire avec des Structures :

11
1. Allocation de mémoire pour une structure :
// Définition de la structure
typedef struct {
int x;
int y;
} Point;
// Allocation dynamique pour un objet de type Point
Point *dynamicPoint = (Point*)malloc(sizeof(Point));

Ici, `dynamicPoint` est un pointeur vers une structure `Point`, et `malloc` alloue de la mémoire
pour un objet de taille équivalente à celle de la structure `Point`.

2. Utilisation de la mémoire allouée :


if (dynamicPoint != NULL) {
dynamicPoint->x = 5;
dynamicPoint->y = 10;
}
On peut accéder aux membres de la structure via le pointeur `dynamicPoint` comme s'il
s'agissait d'une variable de type `Point`.
3. Désallocation de mémoire :
// Libération de la mémoire allouée
free(dynamicPoint);
Il est crucial de libérer la mémoire allouée avec `free` pour éviter les fuites mémoire.
Allocation Dynamique de Mémoire pour un Tableau de Structures :
1. Allocation de mémoire pour un tableau de structures :
// Allocation dynamique pour un tableau de 5 structures Point
Point *dynamicArray = (Point*)malloc(5 * sizeof(Point));

Cette fois, `dynamicArray` pointe vers le début d'un bloc de mémoire contenant un tableau de
5 structures `Point`.

2. Utilisation du tableau de structures :


if (dynamicArray != NULL) {
dynamicArray[0].x = 1;
dynamicArray[0].y = 2;
// ...
}
On peut accéder aux éléments du tableau de structures de manière similaire à un tableau
statique.
3. Désallocation de mémoire pour un tableau de structures :
// Libération de la mémoire allouée pour le tableau
free(dynamicArray);
Allocation Dynamique de Mémoire avec calloc :

12
La fonction `calloc` est similaire à `malloc`, mais elle initialise la mémoire allouée à zéro. Son
utilisation est la même que `malloc`.
Point *dynamicPoint = (Point*)calloc(1, sizeof(Point));
Allocation Dynamique de Mémoire avec realloc :
La fonction `realloc` permet de redimensionner une zone de mémoire précédemment allouée.
// Redimensionnement du tableau à 10 éléments
Point *resizedArray = (Point*)realloc(dynamicArray, 10 * sizeof(Point));

Remarque importante : Après toute utilisation, il est impératif de libérer la mémoire allouée
avec `free`. Ne pas le faire entraînerait des fuites de mémoire.
free(dynamicPoint);
free(resizedArray);

1.1.3.2 APPLICATION A LA STRUCTURE ETUDIANT


Dans la structure Etudiant que nous manipulons depuis, nous allons écrire un code qui définit
la structure Etudiant. Ensuite, il alloue dynamiquement de la mémoire pour une instance de
cette structure à l'aide de malloc. Après avoir vérifié que l'allocation a réussi, il utilise un
pointeur pour remplir les champs de la structure avec des valeurs spécifiques. Enfin, il affiche
ces informations et libère la mémoire allouée dynamiquement pour éviter les fuites de
mémoire. Si l'allocation échoue, un message d'échec est affiché..
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Définition de la structure Etudiant
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Allocation dynamique d'une structure Etudiant avec pointeur
Etudiant *etudiant1 = (Etudiant *)malloc(sizeof(Etudiant));
// Vérification de l'allocation
if (etudiant1 != NULL) {
// Utilisation de la structure via le pointeur
strcpy(etudiant1->nom, "BEYAS");
etudiant1->age = 21;
etudiant1->moyenne = 18.7;
// Affichage des informations
printf("Nom : %s, Age : %d, Moyenne : %.2f\n", etudiant1->nom,
etudiant1->age, etudiant1->moyenne);

// Libération de la mémoire allouée


free(etudiant1);
} else {
printf("Échec de l'allocation de mémoire.\n");

13
}
return 0;
}
Au final, l'allocation et la désallocation de mémoire en langage C avec des structures et des
pointeurs impliquent l'utilisation de `malloc`, `calloc`, `realloc` pour l'allocation, suivi de `free`
pour la libération de la mémoire allouée. Cela offre une gestion dynamique de la mémoire,
particulièrement utile pour des structures dont la taille peut varier pendant l'exécution du
programme. On peut donc dire que, les structures sont utilisées pour organiser des données,
tandis que les pointeurs permettent une manipulation plus avancée de la mémoire, notamment
avec les structures. Ensemble, ils offrent des moyens puissants de créer, organiser et
manipuler des données complexes en programmation.

1.1.4 PORTEE ET DECLARATION

1.1.4.1 PORTEE
Le choix d'utiliser une structure à l'intérieur ou à l'extérieur de la fonction main () dépend
généralement de la portée que vous souhaitez donner à cette structure dans votre programme.
Dans les exemples précédents, nous avons toujours placé notre définition de structure en
dehors de toute fonction. Cependant, sachez que celle-ci peut être circonscrite à un bloc de
sorte de limiter sa portée, comme pour les définitions de variables et les déclarations de
variables et fonctions.
Dans l’exemple ci-dessous, la structure Etudiant ne peut être utilisée que dans le bloc de la
fonction main() et les éventuels sous-blocs qui la composent.
#include <stdio.h>
int main(void) {
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;
Etudiant etudiant;
return 0;
}
Tableau comparatif illustrant les avantages de placer une structure à l'intérieur ou à l'extérieur
de la fonction main() :

14
Notez que le choix entre les deux dépendent du contexte spécifique de votre programme et
des principes de conception que vous souhaitez suivre. Il n'y a pas de solution unique, et la
meilleure approche dépend des besoins et de la structure de votre application.

1.1.4.2 DECLARATIONS
Jusqu’à présent, nous avons parlé de définitions de structures, toutefois, comme pour les
variables et les fonctions, il existe également des déclarations de structures. La déclaration
consiste à spécifier le nom de la structure et à indiquer au compilateur qu'elle existe, mais
sans fournir tous les détails de sa composition. La définition consiste à inclure la spécification
complète des membres de la structure et de leur type. Elle alloue également l'espace mémoire
nécessaire pour chaque instance de la structure.

L'inconvénient majeur d'utiliser des structures sans déclaration préalable est lié à la
déclaration et à l'allocation de la mémoire. En C, lorsque vous déclarez une structure, le
compilateur a besoin de connaître la taille de cette structure pour allouer l'espace mémoire
nécessaire. Si la structure contient des références à elle-même (comme dans le cas des
structures interdépendantes ou avec des membres pointeurs vers elles-mêmes), le
compilateur ne peut pas déterminer la taille de la structure tant que la définition complète de
la structure n'est pas connue.

Cela pose un problème lors de la création d'instances de ces structures ou lors de la


manipulation de ces structures dans votre programme. Si vous essayez de déclarer une
variable de la structure avant sa définition complète, le compilateur ne sait pas combien
d'espace mémoire allouer, ce qui entraîne une erreur de compilation.

15
Pour éviter cela, la plupart des programmeurs C utilisent des déclarations préalables
(prototypes) pour informer le compilateur de l'existence de la structure avant sa définition
complète. Par exemple, vous pourriez déclarer la structure avant la fonction main comme suit:
Une déclaration de structure est en fait une définition sans le corps de la structure.
// Déclaration préalable de la structure (sans la définition complète)
typedef struct Node Node;

int main() {
// Utilisation de la structure récursive
Node node1, node2, node3;
return 0;
}
// Définition complète de la structure
struct Node {
int data;
Node* next; // Membre pointeur vers la même structure
};

L’intérêt de cette déclaration vise à résoudre deux types de problèmes : les structures
interdépendantes3 et les structures comportant un ou des membres qui sont des pointeurs
vers elle-même4.

1.1.5 PRINCIPE DES LISTES CHAINEES


Une liste chaînée est une structure de données dynamique constituée d'éléments appelés
nœuds, où chaque nœud ou maillon ou élément contient des données et un pointeur vers le
nœud suivant dans la séquence.

▪ La tête d’une liste chainée est son premier nœud. La queue d’une liste peut se référer
soit au reste de la liste après la tête, soit au dernier nœud de la liste.

3Les structures interdépendantes se réfèrent à des situations où une structure contient des membres
qui sont du même type que la structure elle-même. Cela crée une dépendance récursive.

4 Les structures comportant un ou des membres qui sont des pointeurs vers elle-même se
réfèrent à une situation où une structure a un membre qui est un pointeur vers la même structure. La
principale différence avec les structures interdépendantes réside dans le fait que le membre pointeur
n'est pas obligatoirement du même type que la structure elle-même.

16
▪ Le champ de chaque nœud qui contient l’adresse du nœud suivant ou précédent est
généralement appelé lien ou pointeur. Le contenu est placé dans un ou plusieurs autres
champs appelés données, informations ou valeur.
Les listes chaînées permettent de stocker et d'organiser des données de manière flexible, en
allouant de la mémoire au fur et à mesure de l'ajout d'éléments.
De manière simple, les listes chaînées sont des structures de données fondamentales en
informatique, utilisées pour stocker et organiser des données de manière dynamique. Une liste
chaînée est constituée d'éléments appelés nœuds, et chaque nœud contient des données et
une référence (ou pointeur) vers le nœud suivant dans la séquence. Explorons le principe des
listes chaînées en détail :

1.1.5.1 NŒUD D'UNE LISTE CHAINEE


Un nœud dans une liste chaînée est une structure qui contient deux composants principaux
• Données : Le contenu du nœud, c'est-à-dire l'information que vous souhaitez
stocker.
• Pointeur (ou référence) : Une référence vers le nœud suivant dans la liste.
Voici à quoi ressemble généralement la structure d'un nœud en C :
#include <stdio.h>
// Définition de la structure Node
typedef struct Node {
int data; // Données du nœud
struct Node* next; // Pointeur vers le nœud suivant
} Node;

int main() {
// Utilisation de la structure Node
Node myNode;
myNode.data = 42;
myNode.next = NULL;
// Affichage des données du nœud
printf("Data: %d\n", myNode.data);
printf("Next: %p\n", (void*)myNode.next);
return 0;
}
1.1.5.2 STRUCTURE GLOBALE DE LA LISTE CHAINEE
Une liste chaînée est simplement une séquence de nœuds connectés les uns aux autres. Le
premier nœud s'appelle généralement la tête de liste, et le dernier nœud peut avoir un pointeur
vers `NULL` pour indiquer la fin de la liste. Visuellement, une liste chaînée ressemble à ceci :
H -> [data1|next] -> [data2|next] -> [data3|next] -> ... -> [dataN|NULL]
▪ H : Tête de liste (pointe vers le premier nœud).
▪ data1, data2, data3, ..., dataN : Données stockées dans chaque nœud.
▪ next :Pointeur vers le nœud suivant.

17
Figure II.1. Schéma de deux listes chainées en C
L'image ci-dessus montre deux exemples de listes chainées : une liste vide et une liste non
vide. La tête de liste est un élément important de la liste chainée. Elle est utilisée pour accéder
au début de la liste et pour parcourir la liste. On pourrait également expliquer que les listes
chainées sont des structures de données dynamiques. Cela signifie qu'elles peuvent être
modifiées à la volée, sans avoir à redimensionner la mémoire allouée à la liste.
❖ Liste vide
La liste vide est représentée par un seul nœud. Le nœud a un pointeur vers NULL, ce qui
signifie qu'il n'y a pas d'autre nœud dans la liste.
❖ Liste non vide
La liste non vide est représentée par deux nœuds. Le premier nœud est la tête de la liste. Il
contient la donnée 7777 et un pointeur vers le deuxième nœud. Le deuxième nœud est le
dernier nœud de la liste. Il contient un pointeur vers NULL, ce qui signifie qu'il n'y a pas d'autre
nœud après lui.
❖ Légende
La légende de l'image explique les différents éléments d'une liste chainée.
▪ Tête de liste : le nœud qui pointe vers le début de la liste.
▪ Donnée : la valeur stockée dans un nœud.
▪ Lien (Pointeur) : un pointeur vers le nœud suivant dans la liste.

1.1.5.3 PRINCIPES DE BASE

• Accès Séquentiel : L'accès aux éléments d'une liste chaînée se fait séquentiellement
à partir de la tête. Pour accéder à un élément spécifique, vous devez traverser la liste
depuis la tête jusqu'à cet élément.
• Insertion : Pour insérer un nouvel élément dans une liste chaînée, vous ajustez les
pointeurs dans les nœuds environnants pour inclure le nouveau nœud.

18
• Suppression : Pour supprimer un nœud, vous ajustez les pointeurs dans les nœuds
environnants pour contourner le nœud à supprimer, puis vous libérez la mémoire du
nœud.

1.1.5.4 EXEMPLE APPROFONDI


Imaginez que vous ayez une liste de choses à faire, telles que vous rendre à l'école, faire vos
devoirs et ranger votre chambre. Vous pouvez écrire cette liste sur un papier ou la mémoriser.
Si vous écrivez la liste sur un papier, chaque élément de la liste est séparé par une virgule.
Par exemple, votre liste pourrait ressembler à ceci :
- Aller à l'école,
- Faire vos devoirs,
- Ranger votre chambre.

Pour trouver un élément de la liste, vous devez parcourir tous les éléments jusqu'à ce que
vous trouviez celui que vous cherchez. Par exemple, pour trouver l'élément "Faire vos devoirs"
dans cet exemple, vous devriez parcourir la liste de la première à la deuxième virgule, car il
s'agit du deuxième élément de la liste.
Si vous mémorisez la liste, vous devez vous souvenir de l'ordre de tous les éléments. Pour
trouver un élément de la liste, vous devez simplement vous rappeler de son ordre. Par
exemple, pour trouver l'élément "faire vos devoirs", vous devriez vous rappeler que c'est le
deuxième élément de la liste.
Les listes chaînées sont un moyen de stocker des données en mémoire de manière similaire
à une liste écrite sur un papier. Chaque élément de la liste chaînée est appelé un nœud. Un
nœud contient deux informations :
- La valeur que vous voulez stocker
- Une référence vers le nœud suivant
La référence vers le nœud suivant est un pointeur qui indique l'emplacement du nœud suivant
dans la liste.
Imaginez que vous ayez une liste chaînée de nombres. Le premier nœud pourrait contenir le
nombre 1, le deuxième nœud pourrait contenir le nombre 2, et ainsi de suite. La référence vers
le nœud suivant serait le pointeur vers le nœud suivant dans la liste.
Pour trouver un élément dans une liste chaînée, vous devez commencer par le premier nœud.
Si le nœud contient la valeur que vous cherchez, vous avez trouvé l'élément. Sinon, vous
devez suivre la référence vers le nœud suivant. Vous continuez à suivre les références jusqu'à
ce que vous trouviez l'élément que vous cherchez ou jusqu'à ce que vous arriviez à la fin de
la liste.

19
CODE DU PROBLEME
// Structure représentant un nœud de la liste chaînée
typedef struct node {
char *value;
struct node *next;
} node;
// Fonction pour créer un nouveau nœud
node *create_node(char *value) {
node *new_node = malloc(sizeof(node));
new_node->value = value;
new_node->next = NULL;
return new_node;
}
// Fonction pour ajouter un nœud à la fin d'une liste chaînée
void append_node(node **head, node *new_node) {
if (*head == NULL) {
*head = new_node;
} else {
node *current_node = *head;
while (current_node->next != NULL) {
current_node = current_node->next;
}
current_node->next = new_node;
}
}

// Fonction pour trouver un nœud dans une liste chaînée


node *find_node(node *head, char *value) {
node *current_node = head;
while (current_node != NULL) {
if (strcmp(current_node->value, value) == 0) {
return current_node;
}
current_node = current_node->next;
}
return NULL;
}
// Fonction principale
int main(){
// Création de la liste chaînée
node *head = NULL;
node *node_1 = create_node("Aller à l'école");
node *node_2 = create_node("Faire vos devoirs");
node *node_3 = create_node("Ranger votre chambre");
append_node(&head, node_1);
append_node(&head, node_2);
append_node(&head, node_3);
// Recherche du nœud "Faire vos devoirs"
node *found_node = find_node(head, "Faire vos devoirs");
if (found_node != NULL) {
printf("Le nœud trouvé est : %s\n", found_node->value);
} else {
printf("Le nœud n'a pas été trouvé.\n");
}

20
return 0;
}
Note : Un pointeur simple, déclaré comme `node *new_node`, est utilisé pour stocker l'adresse
d'un objet ou d'une structure, tandis qu'un pointeur double, noté `**head` dans le contexte
d'une liste chaînée, est un pointeur vers un pointeur. Ce dernier est couramment employé pour
passer l'adresse d'un pointeur en tant qu'argument à une fonction, permettant ainsi de modifier
le pointeur original à l'intérieur de la fonction et de refléter ces changements à l'extérieur. Dans
le cas d'une liste chaînée, `**head` représente un pointeur vers le pointeur de début de la liste,
permettant des modifications dynamiques de la structure de la liste, telles que l'ajout ou la
suppression de nœuds, avec une mise à jour cohérente de la tête de liste.

1.1.5.5 AVANTAGES DES LISTES CHAINEES


▪ Taille Dynamique : Les listes chaînées permettent d'ajouter ou de supprimer des
éléments sans avoir besoin de spécifier la taille à l'avance, contrairement à certains
tableaux.
▪ Insertions et Suppressions Efficaces : Les opérations d'insertion et de suppression
peuvent être plus efficaces qu'avec d'autres structures de données, notamment pour
des opérations au milieu de la liste.

1.1.5.6 INCONVENIENTS DES LISTES CHAINEES


▪ Accès Séquentiel : L'accès direct à un élément nécessite une traversée séquentielle
depuis le début de la liste.
▪ Utilisation de la Mémoire : Chaque nœud nécessite un espace supplémentaire pour
stocker le pointeur, ce qui peut entraîner une surcharge mémoire par rapport à un
tableau statique.
Les listes chaînées sont largement utilisées dans de nombreuses applications et offrent une
flexibilité considérable en termes d'ajout et de suppression d'éléments. Elles sont
particulièrement utiles lorsque la taille de la structure de données peut changer fréquemment
pendant l'exécution du programme.

1.2 EXERCICES DE FIN DE CHAPITRE

1.2.1.1 PROBLEME 1 : MANIPULATION DES STRUCTURES EN C


Objectif : Écrire un programme en langage C qui gère une liste d'étudiants. Chaque étudiant
est représenté par une structure comprenant son nom, son âge et sa moyenne. Le programme
doit permettre à l'utilisateur d'ajouter de nouveaux étudiants, d'afficher la liste des étudiants,
et de libérer la mémoire allouée dynamiquement.
1. Déclarez la structure "Etudiant" avec les membres nécessaires.
2. Implémentez une fonction qui ajoute un nouvel étudiant à la liste. Cette fonction devrait
allouer dynamiquement de la mémoire pour le nouvel étudiant.

21
3. Implémentez une fonction qui affiche la liste des étudiants.
4. Dans la fonction principale (main), utilisez un menu pour permettre à l'utilisateur
d'ajouter des étudiants et d'afficher la liste.
5. N'oubliez pas de libérer la mémoire allouée dynamiquement à la fin du programme.

1.2.1.2 PROBLEME : GESTION D'UNE FILE EN C


Objectif : Écrire un programme en langage C qui simule le fonctionnement d'une file (queue).
La file doit être implémentée à l'aide d'une liste chaînée. Le programme doit permettre à
l'utilisateur d'enfiler et de défiler des éléments de la file.
1. Déclarez la structure "ElementFile" avec les membres nécessaires.
2. Implémentez une fonction qui ajoute un nouvel élément à la file (enfiler).
3. Implémentez une fonction qui retire un élément de la file (défiler).
4. Implémentez une fonction qui affiche le contenu actuel de la file.
5. Dans la fonction principale (main), utilisez un menu pour permettre à l'utilisateur
d'enfiler, de défiler et d'afficher la file.
6. N'oubliez pas de libérer la mémoire allouée dynamiquement à la fin du programme.

22
CHAPITRE 2 LES FICHIERS
L'utilisation et la manipulation de fichiers occupent une place cruciale dans le domaine de la
programmation en langage C. Les fichiers constituent un moyen essentiel de stocker et
d'organiser des données de manière persistante, permettant ainsi aux programmes de
communiquer avec le monde extérieur. Que ce soit pour la lecture de fichiers existants,
l'écriture de nouveaux contenus ou la modification de données déjà enregistrées, la gestion
des fichiers en langage C revêt une importance fondamentale. Cette interaction avec les
fichiers offre aux programmeurs une puissante flexibilité pour créer des applications robustes
et polyvalentes. La bibliothèque standard du langage C propose une panoplie de fonctions
dédiées à la manipulation de fichiers, permettant aux développeurs de concevoir des
programmes capables de lire et d'écrire des informations structurées de manière efficace.

Dans cette exploration du monde des fichiers en langage C, nous plongerons dans les
différentes techniques pour ouvrir, fermer, lire et écrire dans des fichiers. Nous aborderons
également les concepts fondamentaux tels que les modes d'ouverture, les pointeurs de
fichiers, ainsi que la gestion appropriée des erreurs liées à la manipulation des fichiers. En
comprenant ces aspects essentiels, les programmeurs seront mieux équipés pour créer des
applications qui tirent pleinement parti de la gestion de fichiers en langage C.

2.1 DEFINITION
En informatique, un fichier est un ensemble d’informations stockées sur un support, réuni sous
un même nom et manipulé comme une unité.

2.1.1 EXTENSION DE FICHIER

En informatique, une extension de fichier est un suffixe de nom de fichier fait pour identifier
son format. Elle est généralement composée de trois ou quatre caractères, séparés du nom
de fichier par un point. Par exemple, le fichier "exemple.txt" a l'extension ".txt".
Les extensions de fichiers sont utilisées par les systèmes d'exploitation et les applications pour
identifier le type de données contenues dans un fichier. Elles permettent de déterminer quel
programme est capable d'ouvrir et d'afficher un fichier, ainsi que l'icône à utiliser pour le
représenter.
Chaque fichier possède une organisation interne déterminée par le format des données qu'il
contient. Ces formats sont divers et spécifiques à chaque type de données, comprenant, par
exemple :
- Audio : Ogg, MP3, MP4, FLAC, Wave, etc. ;
- Vidéo : Ogg, WebM, MP4, AVI, etc. ;
- Documents : ODT, DOC et DOCX, XLS et XLSX, PDF, Postscript, etc.

23
Afin d'aider l'utilisateur à identifier le contenu d'un fichier, ces formats sont généralement
indiqués par une extension, un suffixe ajouté au nom du fichier. Il est crucial de noter que cette
extension est purement indicative et facultative, n'affectant en rien le contenu du fichier. Son
unique but est d'assister l'utilisateur dans la reconnaissance du type de contenu d'un fichier.

Les extensions de fichiers peuvent être définies par les développeurs de logiciels ou par des
organisations standardisées. Il existe des centaines de milliers d'extensions de fichiers
différentes, chacune correspondant à un type de données spécifique. Elles sont un élément
important de l'organisation des fichiers sur un ordinateur. Elles permettent de faciliter la
recherche et l'ouverture des fichiers, ainsi que de garantir que les fichiers sont ouverts avec le
programme approprié.

2.1.2 SYSTEME DE FICHIERS

En informatique, un système de fichiers est un ensemble de règles et de structures de données


qui organisent les fichiers et les répertoires sur un périphérique de stockage. Il fait référence
à la manière dont les fichiers sont nommés et organisés logiquement à des fins de stockage
et de récupération et permet de localiser et d'accéder aux fichiers, de les créer, de les modifier
et de les supprimer. Un système de fichiers est généralement organisé en une hiérarchie de
répertoires. Un répertoire est un ensemble de fichiers et de sous-répertoires. Le répertoire
racine est le répertoire parent de tous les autres répertoires.

Par exemple, les systèmes d'exploitation (ou OS) DOS, Windows, OS/2, Mac OS et Unix
possèdent tous un système de fichiers, où les fichiers sont enregistrés à des emplacements
précis selon une structure hiérarchique (ou arborescente). Un fichier est placé dans un
répertoire (appelé dossier sous Windows) ou dans un sous-répertoire, à l'emplacement
souhaité dans l'arborescence.
Le système de fichiers définit les conventions de nommage des fichiers, notamment le nombre
maximal de caractères, les caractères autorisés et, dans certains systèmes, la taille du suffixe
du nom de fichier. Il détermine en outre le format permettant de déterminer le chemin d'accès
à un fichier à travers la structure de répertoires.

L’exemple de schéma d'arborescence de fichiers ci-dessous montre un diagramme de la


structure d'un dossier nommé "Tom". Le dossier contient quatre sous-dossiers : "Data",
"Thesis", "Notes.txt" et "Tools". Le dossier "Data" contient deux fichiers texte : "One.txt" et
"Two.txt". Le dossier "Tools" contient trois fichiers texte : "Format", "Stats" et "Old".

24
Figure 2.1. Exemple de schéma d'arborescence de fichiers

2.1.3 HIERARCHIE
• Chaque fichier doit avoir un nom univoque
o éventuellement plusieurs…
• L’espace de nom est structuré en hiérarchie :
o notions de répertoire et de chemin
o notion de répertoire racine (point d’entrée de la hiérarchie)
o notions de chemin relatif et de répertoire courant

2.1.3.1 HIERARCHIE SOUS MS DOS / WINDOWS


Chaque périphérique possède sa propre racine, identifiée par une lettre.

25
Figure 2.2. Exemple de schéma d'arborescence de fichiers

2.1.4 HIERARCHIE SOUS UNIX


On a une hiérarchie unique ; chaque périphérique ancre sa racine dans un répertoire
(montage).

Figure 2.3. Exemple de schéma d'arborescence de fichiers


Les fichiers sont des unités de données qui contiennent des informations. Ils peuvent être de
différents types, tels que des fichiers texte, des fichiers binaires, des fichiers images, des
fichiers audios ou des fichiers vidéo.

Le langage C fournit une bibliothèque standard de fonctions pour accéder aux fichiers. Ces
fonctions permettent de lire, d'écrire, de créer, de modifier et de supprimer des fichiers. Pour
faciliter la localisation et la gestion des fichiers, ceux-ci sont classés et organisés sur leur

26
support conformément à un système de fichiers. Ce système permet à l'utilisateur de structurer
ses fichiers dans une arborescence de dossiers et de les localiser à partir d'un chemin d'accès
spécifique.

2.1.5 LE CHEMIN D'ACCES

Le chemin d'accès d'un fichier est une suite de caractères décrivant sa position dans le
système de fichiers. Il se compose au minimum du nom du fichier visé et au maximum de la
suite de tous les dossiers qu'il est nécessaire de traverser pour l'atteindre depuis le répertoire
racine. Les dossiers sont séparés par un caractère spécifique : "/" sous Unix et "\" sous
Windows.

La racine d'un système de fichier est le point de départ de l'arborescence des fichiers et
dossiers. Sous Unix, c'est le répertoire "/", tandis que sous Windows, chaque lecteur constitue
une racine, comme par exemple "C :". Un chemin d'accès absolu commence par la racine,
permettant d'atteindre le fichier depuis n'importe quelle position dans l'arborescence. Un
chemin d'accès relatif, ne commençant pas par la racine, n'est valide que depuis un point
spécifique dans la hiérarchie des fichiers.

Ainsi, pour accéder à un fichier nommé "texte.txt" dans le dossier "Documents" situé dans le
dossier "utilisateur" à la racine, le chemin d'accès absolu serait /utilisateur/documents/texte.txt
sous Unix et C:\utilisateur\documents\texte.txt sous Windows (supposant qu'il soit sur le
lecteur C:). Cependant, si nous sommes déjà dans le dossier "utilisateur", un chemin relatif
comme documents/texte.txt ou documents\texte.txt serait utilisable. L’explication ci-dessus est
représentée par la démarche ci-dessous.

Implémentation : "exemple.txt" situé dans le dossier "Documents" du répertoire utilisateur sur


les systèmes Windows et Linux :
▪ Sous Windows :
- Chemin absolu : `C:\Users\NomUtilisateur\Documents\exemple.txt`
- Chemin relatif (si le répertoire de travail actuel est `C:\Users\NomUtilisateur\`) :
`Documents\exemple.txt`
▪ Sous Linux :
- Chemin absolu : `/home/NomUtilisateur/Documents/exemple.txt`
- Chemin relatif (si le répertoire de travail actuel est `/home/NomUtilisateur/`) :
`Documents/exemple.txt`

NB : le chemin absolu est spécifié depuis la racine du système, tandis que le chemin relatif
est défini par rapport au répertoire de travail actuel. L'utilisation de l'un ou l'autre dépend du
contexte et des besoins spécifiques de localisation des fichiers ou répertoires dans une
hiérarchie de fichiers.

27
2.1.6 METADONNEES
Les métadonnées sont des données qui décrivent d'autres données. Elles fournissent des
informations supplémentaires sur les données réelles, telles que leur contenu, leur format, leur
provenance, leur utilisation ou leur signification. Elles peuvent être utilisées à diverses fins,
notamment :

▪ Améliorer la découverte et l'accès aux données : les métadonnées peuvent être


utilisées pour indexer les données et les rendre plus faciles à trouver. Par exemple, les
moteurs de recherche utilisent les métadonnées pour indexer les pages Web.
▪ Faciliter la gestion des données : les métadonnées peuvent être utilisées pour
organiser et structurer les données. Par exemple, les bibliothèques utilisent les
métadonnées pour organiser leurs collections de livres et de documents.
▪ Soutenir l'analyse des données : les métadonnées peuvent être utilisées pour enrichir
les données et fournir des informations contextuelles. Par exemple, les analystes de
données peuvent utiliser les métadonnées pour comprendre le contexte dans lequel
les données ont été collectées.

Les métadonnées peuvent être structurées ou non structurées. Les métadonnées structurées
sont organisées dans un format défini, tel que XML ou JSON. Les métadonnées non
structurées ne sont pas organisées dans un format défini, comme le texte libre ou les images.

Exemples de métadonnées

Pour une image : le titre, la description, les mots-clés, la date de prise de vue, le lieu de prise
de vue, le format du fichier.

Pour un document : le titre, l'auteur, le sujet, la date de création, le format du fichier.

Pour une base de données : les tables, les colonnes, les types de données, les contraintes.

Les métadonnées sont un élément essentiel de la gestion des données. Elles peuvent être
utilisées pour améliorer la découverte, l'accès, la gestion et l'analyse des données.

En outre, le système de fichiers conserve généralement des métadonnées pour chaque fichier,
incluant des informations telles que sa taille, ses droits d'accès, la date de dernier accès, la
date de dernière modification, etc. Ces données supplémentaires contribuent à une gestion
plus complète des fichiers.

28
2.1.7 LES FLUX : UN PEU DE THEORIE
La bibliothèque standard fournit différentes fonctions pour manipuler les fichiers, toutes
déclarées dans l’en-tête <stdio.h>. Toutefois, celles-ci manipulent non pas des fichiers,
mais des flux de données en provenance ou à destination de fichiers. Ces flux peuvent être
de deux types :

▪ des flux de textes qui sont des suites de caractères terminées par un caractère de fin
de ligne (\n) et formant ainsi des lignes ;
▪ des flux binaires qui sont des suites de multiplets.

2.1.7.1 POURQUOI UTILISER DES FLUX ?


Les flux de données en programmation C, manipulés à travers la bibliothèque standard et
déclarés dans l'en-tête <stdio.h>, sont essentiels pour la manipulation de fichiers. Ces flux
peuvent être de deux types : les flux de texte, constitués de suites de caractères formant des
lignes, et les flux binaires, constitués de suites de multiplets. L'utilisation de flux plutôt que la
manipulation directe de fichiers est motivée par deux raisons majeures. Premièrement, les
disparités entre les systèmes d'exploitation dans la représentation des fins de ligne nécessitent
une gestion complexe si l'on manipule directement les fichiers. Deuxièmement, les opérations
de lecture et d'écriture impliquent souvent des accès au disque dur, la mémoire la plus lente
de l'ordinateur. Pour pallier cela, la bibliothèque standard utilise la temporisation, soit par blocs,
soit par lignes, minimisant ainsi les accès au disque et améliorant les performances.

La bibliothèque standard fournit également trois flux par défaut lors du démarrage d'un
programme : stdin (entrée standard), stdout (sortie standard) et stderr (sortie d'erreur
standard). Ces flux facilitent les interactions avec l'utilisateur et la gestion des erreurs. Stdin
est utilisé pour récupérer les informations fournies par l'utilisateur, stdout pour transmettre des
informations à l'utilisateur (affichées généralement dans le terminal), et stderr pour transmettre
des messages d'erreurs ou des avertissements, privilégiant une temporisation rapide pour une
transmission efficace des informations d'erreur à l'utilisateur.

2.1.7.2 FICHIER : OUVERTURE ET FERMETURE D'UN FLUX

2.1.7.2.1 LA FONCTION FOPEN


FILE *fopen(char *chemin, char *mode);
La fonction fopen() permet d’ouvrir un flux. Celle-ci attend deux arguments : un chemin d’accès
vers un fichier qui sera associé au flux et un mode qui détermine le type de flux (texte ou
binaire) et la nature des opérations qui seront réalisées sur le fichier via le flux (lecture, écriture
ou les deux). Elle retourne un pointeur vers un flux en cas de succès et un pointeur nul en cas
d’échec.

29
Le mode
Le mode est une chaîne de caractères composée d’une ou plusieurs lettres qui décrit le type
du flux et la nature des opérations qu’il doit réaliser.

Cette chaîne commence obligatoirement par la seconde de ces informations. Il existe six
possibilités reprises dans le tableau ci-dessous.

Mode Type(s) d’opération(s) Effets


r Lecture Néant
r+ Lecture et écriture Néant
w Écriture Si le fichier n’existe pas, il est créé.
Si le fichier existe, son contenu est effacé.
w+ Lecture et écriture Idem
a Écriture Si le fichier n’existe pas, il est créé.
Place les données à la fin du fichier
a+ Lecture et écriture Idem

Par défaut, les flux sont des flux de textes. Pour obtenir un flux binaire, il suffit d’ajouter la lettre
b à la fin de la chaîne décrivant le mode.

EXEMPLE
Le code ci-dessous tente d’ouvrir un fichier nommé « texte.txt » en lecture seule dans le
dossier courant. Notez que dans le cas où il n’existe pas, la fonction fopen() retournera un
pointeur nul (seul le mode r permet de produire ce comportement).

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
FILE *fp = fopen("texte.txt", "r");
if (fp == NULL)
{
printf("Le fichier texte.txt n'a pas pu être ouvert\n");
return EXIT_FAILURE;
}
printf("Le fichier texte.txt existe\n");
return 0;
}

Le code est correct, mais il serait prudent de s'assurer que l’on ferme le fichier après l'avoir
ouvert. D’où intervient la fonction fclose() pour éviter des fuites de mémoire. Cela garantit que
le fichier est correctement fermé avant que le programme ne se termine.

2.1.7.2.2 LA FONCTION FCLOSE


int fclose(FILE *flux);

30
La fonction fclose() termine l’association entre un flux et un fichier. S’il reste des données
temporisées, celles-ci sont écrites. La fonction retourne zéro en cas de succès et EOF en cas
d’erreur.

NB : EOF est une constante définie dans l’en-tête <stdio.h> et est utilisée par les fonctions
déclarées dans ce dernier pour indiquer soit l’arrivée à la fin d’un fichier (nous allons y venir)
soit la survenance d’une erreur. La valeur de cette constante est toujours un entier négatif.
Nous pouvons désormais compléter l’exemple précédent comme suit.
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
FILE *fp = fopen("texte.txt", "r");

if (fp == NULL)
{
printf("Le fichier texte.txt n'a pas pu être ouvert\n");
return EXIT_FAILURE;
}
printf("Le fichier texte.txt existe\n");

if (fclose(fp) == EOF)
{
printf("Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

Veillez qu’à chaque appel à la fonction fopen() corresponde un appel à la fonction fclose().

2.1.8 ÉCRITURE VERS UN FLUX DE TEXTE

2.1.8.1 ÉCRIRE UN CARACTERE


int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);
int putchar(int ch);

Les fonctions putc() et fputc() écrivent un caractère dans un flux. Il s’agit de l’opération
d’écriture la plus basique sur laquelle reposent toutes les autres fonctions d’écriture. Ces deux
fonctions retournent soit le caractère écrit, soit EOF si une erreur est rencontrée. La fonction
putchar(), quant à elle, est identique aux fonctions putc() et fputc() si ce n’est qu’elle écrit dans
le flux stdout.

31
NB : Techniquement, putc() et fputc() sont identiques, si ce n’est que putc() est en fait le plus
souvent une macrofonction. Étant donné que nous n’avons pas encore vu de quoi il s’agit,
préférez utiliser la fonction fputc() pour l’instant.

L’exemple ci-dessous écrit le caractère « C » dans le fichier « texte.txt ». Étant donné que nous
utilisons le mode w, le fichier est soit créé s’il n’existe pas, soit vidé de son contenu s’il existe
(revoyez le tableau des modes à la section précédente si vous êtes perdus).
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("texte.txt", "w");

if (fp == NULL)
{
printf("Le fichier texte.txt n'a pas pu être ouvert\n");
return EXIT_FAILURE;
}
if (fputc('C', fp) == EOF)
{
printf("Erreur lors de l'écriture d'un caractère\n");
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
printf("Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

2.1.8.2 ÉCRIRE UNE LIGNE


int fputs(char *ligne, FILE *flux);
int puts(char *ligne);

La fonction fputs() écrit une ligne dans le flux flux. La fonction retourne un nombre positif ou
nul en cas de succès et EOF en cas d’erreurs. La fonction puts() est identique si ce n’est
qu’elle ajoute automatiquement un caractère de fin de ligne et qu’elle écrit sur le flux stdout.

Maintenant que nous savons comment écrire une ligne dans un flux précis, nous allons pouvoir
diriger nos messages d’erreurs vers le flux stderr afin que ceux-ci soient affichés le plus
rapidement possible.

L’exemple suivant écrit le mot « Bonjour » suivi d’un caractère de fin de ligne au sein du fichier
« texte.txt ».

32
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("texte.txt", "w");
if (fp == NULL)
{
fputs("Le fichier texte.txt n'a pas pu être ouvert\n", stderr);
return EXIT_FAILURE;
}
if (fputs("Bonjour\n", fp) == EOF)
{
fputs("Erreur lors de l'écriture d'une ligne\n", stderr);
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
fputs("Erreur lors de la fermeture du flux\n", stderr);
return EXIT_FAILURE;
}
return 0;
}
La norme garantit qu’une ligne peut contenir jusqu’à 254 caractères (caractère de fin de ligne
inclus). Aussi, veillez à ne pas écrire de ligne d’une taille supérieure à cette limite.

2.1.8.3 LECTURE DEPUIS UN FLUX DE TEXTE


2.1.8.3.1 RECUPERER UN CARACTERE
int getc(FILE *flux);
int fgetc(FILE *flux);
int getchar(void);

Les fonctions getc() et fgetc() sont les exacts miroirs des fonctions putc() et fputc() : elles
récupèrent un caractère depuis le flux fourni en argument. Il s’agit de l’opération de lecture la
plus basique sur laquelle reposent toutes les autres fonctions de lecture. Ces deux fonctions
retournent soit le caractère lu, soit EOF si la fin de fichier est rencontrée ou si une erreur est
rencontrée. La fonction getchar(), quant à elle, est identique à ces deux fonctions si ce n’est
qu’elle récupère un caractère depuis le flux stdin.

NB : Comme putc(), la fonction getc() est le plus souvent une macrofonction. Utilisez donc
plutôt la fonction fgetc() pour le moment.
L’exemple ci-dessous lit un caractère provenant du fichier texte.txt.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("texte.txt", "r");

33
if (fp == NULL)
{
fprintf(stderr, "Le fichier texte.txt n'a pas pu être
ouvert\n");
return EXIT_FAILURE;
}
int ch = fgetc(fp);
if (ch != EOF)
printf("%c\n", ch);
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

2.1.8.4 RECUPERER UNE LIGNE


char *fgets(char *tampon, int taille, FILE *flux);

La fonction fgets() lit une ligne depuis le flux flux et la stocke dans le tableau tampon. Cette
dernière lit au plus un nombre de caractères égal à taille diminué de un afin de laisser la place
pour le caractère nul, qui est automatiquement ajouté. Dans le cas où elle rencontre un
caractère de fin de ligne : celui-ci est conservé au sein du tableau, un caractère nul est ajouté
et la lecture s’arrête.

La fonction retourne l’adresse du tableau tampon en cas de succès et un pointeur nul si la fin
du fichier est atteinte ou si une erreur est survenue.

L’exemple ci-dessous réalise donc la même opération que le code précédent, mais en utilisant
la fonction fgets().

NB : Étant donné que la norme nous garantit qu’une ligne peut contenir jusqu’à 254 caractères
(caractère de fin de ligne inclus), nous utilisons un tableau de 255 caractères pour les contenir
(puisqu’il est nécessaire de prévoir un espace pour le caractère nul).

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char buf[255];
FILE *fp = fopen("texte.txt", "r");

if (fp == NULL)
{
fprintf(stderr, "Le fichier texte.txt n'a pas pu être
ouvert\n");

34
return EXIT_FAILURE;
}
if (fgets(buf, sizeof buf, fp) != NULL)
printf("%s\n", buf);
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

Toutefois, il y a un petit problème : la fonction fgets() conserve le caractère de fin de ligne


qu’elle rencontre. Dès lors, nous affichons deux retours à la ligne : celui contenu dans la chaîne
buf et celui affiché par printf(). Aussi, il serait préférable d’en supprimer un, de préférence celui
de la chaîne de caractères. Pour ce faire, nous pouvons faire appel à une petite fonction (que
nous appellerons chomp() en référence à la fonction éponyme du langage Perl) qui se
chargera de remplacer le caractère de fin de ligne par un caractère nul.
void chomp(char *s)
{
while (*s != '\n' && *s != '\0')
++s;

*s = '\0';
}

2.1.8.5 LA FONCTION FSCANF


La fonction fscanf() est identique à la fonction scanf() si ce n’est qu’elle récupère les données
depuis le flux fourni en argument (au lieu de stdin pour scanf()).

NB : Le flux stdin étant le plus souvent mémorisé par lignes, ceci vous explique pourquoi nous
lisions les caractères restant après un appel à scanf() jusqu’à rencontrer un caractère de fin
de ligne : pour vider le tampon du flux stdin.

La fonction fscanf() retourne le nombre de conversions réussies (voire zéro, si aucune n’est
demandée ou n’a pu être réalisée) ou EOF si une erreur survient avant qu’une conversion n’ait
eu lieu.

2.1.9 ÉCRITURE VERS UN FLUX BINAIRE


2.1.9.1 ÉCRIRE UN MULTIPLET
int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);

35
Comme pour les flux de texte, il vous est possible de recourir aux fonctions putc() et fputc().
Dans le cas d’un flux binaire, ces fonctions écrivent un multiplet (sous la forme d’un int converti
en unsigned char) dans le flux spécifié.

2.1.9.2 ÉCRIRE UNE SUITE DE MULTIPLETS


size_t fwrite(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fwrite() écrit le tableau référencé par ptr composé de nombre éléments de taille
multiplets dans le flux flux. Elle retourne une valeur égale à nombre en cas de succès et une
valeur inférieure en cas d’échec.

L’exemple suivant écrit le contenu du tableau tab dans le fichier binaire.bin. Dans le cas où un
int fait 4 octets, 20 octets seront donc écrits.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int tab[5] = { 1, 2, 3, 4, 5 };
const size_t n = sizeof tab / sizeof tab[0];
FILE *fp = fopen("binaire.bin", "wb");

if (fp == NULL)
{
fprintf(stderr, "Le fichier binaire.bin n'a pas pu être
ouvert\n");
return EXIT_FAILURE;
}
if (fwrite(&tab, sizeof tab[0], n, fp) != n)
{
fprintf(stderr, "Erreur lors de l'écriture du tableau\n");
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}

return 0;
}
NB : La ligne de code `const size_t n = sizeof tab / sizeof tab[0];` calcule le nombre d'éléments
dans le tableau `tab` en divisant la taille totale du tableau par la taille d'un élément individuel.
Cette opération utilise l'opérateur `sizeof` pour obtenir la taille en octets du tableau entier
(`sizeof tab`) et celle d'un élément unique (`sizeof tab[0]`). Le résultat de cette division est
attribué à la variable `n`, de type `size_t`, représentant ainsi le nombre d'éléments dans le

36
tableau. L'utilisation de cette approche permet d'écrire un code plus flexible, car elle ajuste
automatiquement le nombre d'éléments si la taille des éléments du tableau change, facilitant
ainsi la maintenance et l'adaptabilité du programme. Le tableau tab ici est déclaré avec cinq
éléments de type entier. En supposant que la taille d'un élément (int) soit de 4 octets, la taille
totale du tableau serait de 20 octets, et la taille d'un élément serait de 4 octets. Par conséquent,
la division 20 / 4 donnerait 5, et la variable n serait initialisée avec la valeur 5, représentant
ainsi le nombre d'éléments dans le tableau tab.
Si on a `int tab[6] = { 1, 2, 3, 4, 5, 6 };`, cela signifie que le tableau `tab` contient six éléments
de type entier. En supposant que la taille d'un élément (int) soit de 4 octets, la taille totale du
tableau serait de 24 octets (6 éléments * 4 octets/élément). La taille d'un élément individuel du
tableau serait toujours de 4 octets. Ainsi, le calcul `const size_t n = sizeof tab / sizeof tab[0];`
dans ce contexte donnerait 24 / 4, ce qui équivaut à 6. La variable `n` serait donc initialisée
avec la valeur 6, représentant le nombre d'éléments dans le tableau `tab`.

2.1.10 LECTURE DEPUIS UN FLUX BINAIRE


2.1.10.1 LIRE UN MULTIPLET
int getc(FILE *flux);
int fgetc(FILE *flux);

Lors de la lecture depuis un flux binaire, les fonctions fgetc() et getc() permettent de récupérer
un multiplet (sous la forme d’un unsigned char converti en int) depuis un flux.

2.1.10.2 LIRE UNE SUITE DE MULTIPLETS


size_t fread(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fread() est l’inverse de la fonction fwrite() : elle lit nombre éléments de taille
multiplets depuis le flux flux et les stocke dans l’objet référencé par ptr. Elle retourne une valeur
égale à nombre en cas de succès ou une valeur inférieure en cas d’échec.

Dans le cas où nous disposons du fichier binaire.bin produit par l’exemple de la section
précédente, nous pouvons reconstituer le tableau tab.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int tab[5] = { 0 };
const size_t n = sizeof tab / sizeof tab[0];
FILE *fp = fopen("binaire.bin", "rb");

if (fp == NULL)
{

37
fprintf(stderr, "Le fichier binaire.bin n'a pas pu être
ouvert\n");
return EXIT_FAILURE;
}
if (fread(&tab, sizeof tab[0], n, fp) != n)
{
fprintf(stderr, "Erreur lors de la lecture du tableau\n");
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}

for (unsigned i = 0; i < n; ++i)


printf("tab[%u] = %d\n", i, tab[i]);

return 0;
}
Résultat
tab[0] = 1
tab[1] = 2
tab[2] = 3
tab[3] = 4
tab[4] = 5

2.1.11 RESUME
L’extension d’un fichier fait partie de son nom et est purement indicative ;
Les fichiers sont manipulés à l’aide de flux afin de s’abstraire des disparités entre systèmes
d’exploitation et de recourir à la temporisation ;
Trois flux de texte sont disponibles par défaut : stdin, stdout et stderr ;
La fonction fopen() permet d’associer un fichier à un flux, la fonction fclose() met fin à cette
association ;
Les fonctions fputc(), fputs() et fprintf() écrivent des données dans un flux de texte ;
Les fonctions fgetc(), fgets() et fscanf() lisent des données depuis un flux de texte ;
Les fonctions fputc() et fwrite() écrivent un ou plusieurs multiplets dans un flux binaire ;
Les fonctions fgetc() et fread() lisent un ou plusieurs multiplets depuis un flux binaire.

2.2 EXERCICES DE FIN DE CHAPITRE


2.2.1 EXERCICE 1 : OBJECTIF : MANIPULATION DE FICHIERS TEXTE

EN LANGAGE C
1. Écrivez un programme en langage C qui crée un fichier texte nommé "donnees.txt" et y écrit
les entiers de 1 à 10, un entier par ligne.

38
2. Ensuite, lisez le contenu du fichier "donnees.txt" et affichez les entiers à l'écran.
3. Ajoutez une fonction au programme qui calcule la somme des entiers lus depuis le fichier
et affiche le résultat.

2.2.2 EXERCICE 2 : OBJECTIF : MANIPULATION DE FICHIERS

BINAIRES EN LANGAGE C
1. Créez une structure en langage C appelée "Etudiant" avec les champs suivants : nom
(chaîne de caractères), numéro d'étudiant (entier), et moyenne (nombre à virgule flottante).
2. Écrivez une fonction qui génère un tableau de structures "Etudiant" avec des données
fictives pour trois étudiants.
3. Écrivez ces structures dans un fichier binaire appelé "etudiants.dat".
4. Ensuite, lisez le contenu du fichier binaire "etudiants.dat" et affichez les informations des
étudiants à l'écran.
5. Ajoutez une fonction au programme qui calcule la moyenne générale des étudiants et affiche
le résultat.

2.2.3 EXERCICE 3 : GESTION DYNAMIQUE DE LA MEMOIRE


Objectif : Écrire un programme en langage C qui demande à l'utilisateur de saisir la taille d'un
tableau, alloue dynamiquement de la mémoire pour ce tableau, puis demande à l'utilisateur de
remplir le tableau avec des entiers. Enfin, le programme doit afficher la somme des éléments
du tableau et libérer la mémoire allouée.

2.2.4 EXERCICE 4 : PROGRAMME QUI PERMET DE LIRE UN FICHIER

TEXTE
Écrire un programme qui permet de lire un fichier texte et d’en afficher le contenu à l’écran, un
mot par ligne.
EXPLICATIONS
Pour réaliser cet exercice, il faudra utiliser les fonctions fopen(), fgets() et printf().
▪ La fonction fopen() permet d’ouvrir un fichier. Nous allons utiliser le mode r pour lire le
fichier en lecture seule.
▪ La fonction fgets() permet de lire une ligne depuis un fichier. Nous allons utiliser cette
fonction pour lire les mots du fichier un par un.
▪ La fonction printf() permet d’afficher des données à l’écran. Nous allons utiliser cette
fonction pour afficher les mots du fichier, un par ligne.

2.2.5 EXERCICE 2 : PROGRAMME QUI PERMET DE LIRE UN FICHIER

TEXTE ET D’EN COMPTER LE NOMBRE DE MOTS


Écrire un programme qui permet de lire un fichier texte et d’en compter le nombre de mots.

39
EXPLICATIONS

Pour réaliser cet exercice, il faudra utiliser les fonctions fopen(), fgets() et atoi().
▪ La fonction fopen() permet d’ouvrir un fichier. Nous allons utiliser le mode r pour lire le
fichier en lecture seule.
▪ La fonction fgets() permet de lire une ligne depuis un fichier. Nous allons utiliser cette
fonction pour lire les mots du fichier un par un.
▪ La fonction atoi() permet de convertir une chaîne de caractères en entier. Nous allons
utiliser cette fonction pour convertir chaque mot en entier.

40
CHAPITRE 3 LES LISTES CHAINEES

3.1 INTRODUCTION
Les listes chainées sont des structures de données dynamiques qui permettent de stocker des
données de manière non contiguë en mémoire. Elles sont composées d'éléments, appelés
nœuds, qui sont reliés entre eux par des pointeurs. Elles permettent de stocker et organiser
des données de manière dynamique, facilitant l'ajout, la suppression et la modification
d'éléments sans nécessiter une allocation statique de mémoire. Deux types courants de listes
chaînées sont les listes chaînées simples et les listes chaînées doubles, chacune avec ses
propres caractéristiques et avantages. Ces structures apparaissent naturellement dans un
certain nombre de problèmes ou de situations de tous les jours, et offrent une flexibilité accrue
par rapport aux tableaux statiques, car elles permettent l'insertion et la suppression efficaces
d'éléments sans nécessiter une allocation de mémoire préalable. En C, la mise en œuvre de
listes chaînées se base sur des nœuds qui contiennent les données ainsi que des pointeurs
vers les éléments suivants ou précédents de la liste.

3.2 CONCEPT DE LISTE CHAINEE


Un tableau peut être représenté en mémoire comme ceci :

Tableau de 4 cases en mémoire (représentation horizontale)


Les tableaux posent un problème parce qu'ils restent fixes. On ne peut pas les rendre plus
grands, sauf en en créant de nouveaux qui sont plus grands. De même, on ne peut pas ajouter
une case au milieu sans déplacer tous les autres éléments.

Il n'est pas possible d'augmenter la taille d'un tableau une fois qu'il est créé, et le langage C
ne fournit pas d'autres moyens de stockage de données. Cependant, il est envisageable de

41
créer un système personnalisé à partir de zéro. La clé réside dans la compréhension de la
marche à suivre, et c'est précisément ce que ce chapitre aidera à comprendre.

3.2.1 RAPPEL SUR LES LISTES CHAINEES


Une liste chaînée (ou liste liée) est une structure de données composées d’une séquence
d’éléments de liste. Chaque enregistrement d’une liste chaînée est souvent appelé élément,
nœud ou maillon. C’est un moyen d'organiser une série de données en mémoire. Cela consiste
à assembler des structures en les liant entre elles à l'aide de pointeurs. On pourrait les
représenter comme ceci :

Chaque élément peut contenir ce que l'on veut : un ou plusieurs int, double… En plus de cela,
chaque élément possède un pointeur vers l'élément suivant :

Souvenez-vous de la manière dont les éléments sont disposés les uns par rapport aux autres
: ils constituent une chaîne de pointeurs, d'où le terme "liste chaînée".
Chaque élément (ou maillon) M de la liste est composé :
▪ D’un contenu utile M.val (de n’importe quel type),
▪ D’un pointeur M.suiv pointant vers l’élément suivant de la séquence.

Le dernier élément de la liste possède un pointeur M.suiv vide :

NOTE 1 : La tête d’une liste est son premier nœud. La queue d’une liste peut se référer soit
au reste de la liste après la tête, soit au dernier nœud de la liste. Le champ de chaque nœud
qui contient l’adresse du nœud suivant ou précédent est généralement appelé lien ou pointeur.

42
Le contenu est placé dans un ou plusieurs autres champs appelés données, informations ou
valeur.

Une liste chainée L est entièrement définie par son maillon de tête L.tete, c’est à dire l’adresse
de son premier maillon.On peut également lui ajouter un attribut L.cur pour mémoriser
l’adresse d’un maillon « courant ».

NOTE 2 : Contrairement aux tableaux, les éléments d'une liste chaînée ne sont pas placés
côte à côte dans la mémoire. Chaque case pointe vers une autre case en mémoire, qui n'est
pas nécessairement stockée juste à côté.

3.2.2 CONSTRUIRE UNE LISTE CHAINEE


Nous allons essayer de créer une structure qui fonctionne sur le principe que nous venons de
découvrir.

3.2.2.1 CREEZ LA STRUCTURE DE LA LISTE DES ELEMENTS


Nous allons créer une liste chaînée de nombres entiers.
NB : On pourrait aussi bien créer une liste chaînée contenant des nombres décimaux ou même
des tableaux et des structures. Le principe des listes chaînées s'adapte à n'importe quel type
de données, mais ici, je propose de faire simple pour que vous compreniez bien le principe.
Chaque élément de la liste aura la structure suivante :
typedef struct Element Element;
struct Element
{
int nombre;
Element *suivant;
};
Cette structure contient

▪ Une donnée, ici un nombre de type int: on pourrait remplacer cela par n'importe quelle
autre donnée (un double, un tableau…). Cela correspond à ce que vous voulez stocker,
c'est à vous de l'adapter en fonction des besoins de votre programme.

Si on veut travailler de manière générique, l'idéal est de faire un pointeur sur void :void*. Cela
permet de faire pointer vers n'importe quel type de données.

▪ Un pointeur vers un élément du même type appelé suivant. C'est ce qui permet de lier
les éléments les uns aux autres : chaque élément sait où se trouve l'élément suivant
en mémoire. Souvenez-vous : les cases ne sont pas côte à côte en mémoire ; c'est la
grosse différence par rapport aux tableaux. Cela offre davantage de souplesse car on
peut plus facilement ajouter de nouvelles cases par la suite au besoin.

43
En revanche, il ne sait pas quel est l'élément précédent, il est donc impossible de revenir en
arrière à partir d'un élément avec ce type de liste. On parle de liste "simplement chaînée",
alors que les listes "doublement chaînées" ont des pointeurs dans les deux sens et n'ont pas
ce défaut. Elles sont néanmoins plus complexes.

3.2.2.2 CREEZ LA STRUCTURE DE CONTROLE


En plus de la structure qu'on vient de créer (que l'on dupliquera autant de fois qu'il y a
d'éléments), nous allons avoir besoin d'une autre structure pour contrôler l'ensemble de la liste
chaînée. Elle aura la forme suivante :
typedef struct Liste Liste;
struct Liste
{
Element *premier;
};
Cette structure Liste contient un pointeur vers le premier élément de la liste. En effet, il faut
conserver l'adresse du premier élément pour savoir où commence la liste. Si on connaît le
premier élément, on peut retrouver tous les autres en "sautant" d'élément en élément à l'aide
des pointeurs suivant.

NB : Une structure composée d'une seule sous-variable n'est en général pas très utile.
Néanmoins, je pense que l'on aura besoin d'y ajouter des sous-variables plus tard, je préfère
donc prendre les devants en créant ici une structure. On pourrait par exemple y stocker en
plus la taille de la liste, c'est-à-dire le nombre d'éléments qu'elle contient.

Nous n'aurons besoin de créer qu'un seul exemplaire de la structure Liste. Elle permet de
contrôler toute la liste :

N'oubliez pas le dernier élément de la liste. Il est essentiel de terminer la traversée de la


liste à un certain point. Pour cela, il faut indiquer au programme "Stop, c'est le dernier
élément". Il suffit de faire pointer le dernier élément de la liste vers NULL, en d'autres termes,
en mettant son pointeur suivant à NUL L :

44
Nous avons mis en place deux structures pour gérer une liste chaînée :

▪ Element : Représente un élément de la liste et peut être reproduit autant de fois que
nécessaire.
▪ Liste : Contrôle l'ensemble de la liste. Nous n'aurons besoin que d'une seule instance
de cette structure.

C'est un bon début, mais l'élément essentiel manque toujours : les fonctions qui vont
manipuler la liste chaînée. En effet, il n'est pas pratique de modifier manuellement le contenu
des structures à chaque besoin ! Il est préférable d'utiliser des fonctions qui automatisent
cette tâche.

3.2.3 GEREZ LA LISTE A L'AIDE DE FONCTIONS


À première vue, il semble évident que l'on aura besoin de fonctions pour :

▪ Initialiser la liste ;
▪ Ajouter un élément ;
▪ Supprimer un élément ;
▪ Afficher le contenu de la liste ;
▪ Supprimer la liste entière.

Il faut Commencer par initialiser la liste. La fonction d'initialisation est la première à être
appelée. Elle crée la structure de contrôle et le premier élément de la liste :
Liste *initialisation()
{
Liste *liste = malloc(sizeof(*liste));
Element *element = malloc(sizeof(*element));

if (liste == NULL || element == NULL)


{
exit(EXIT_FAILURE);
}
element->nombre = 0;
element->suivant = NULL;
liste->premier = element;

return liste;
}

45
EXPLICATIONS :
Nous commençons par créer la structure de contrôle pour la liste.

Le type de données est "Liste", et la variable associée est appelée "liste".

L'utilisation de majuscules permet de les distinguer.

Nous allouons dynamiquement la structure de contrôle avec un malloc. La taille à allouer est
calculée automatiquement avec sizeof(*liste). L'ordinateur saura qu'il doit allouer l'espace
nécessaire pour le stockage de la structure Liste.

Nous aurions également pu écrire sizeof(Liste), mais si nous décidons ultérieurement de


modifier le type du pointeur "liste", nous devrions également ajuster le sizeof.

Ensuite, nous allouons de la même manière la mémoire nécessaire pour stocker le premier
élément. Nous vérifions si les allocations dynamiques ont réussi. En cas d'erreur, le
programme s'arrête immédiatement en utilisant exit().

Si tout s'est bien déroulé, nous définissons les valeurs de notre premier élément :

▪ La donnée "nombre" est initialisée à 0 par défaut.


▪ Le pointeur "suivant" pointe vers NULL, car le premier élément de notre liste est
également le dernier pour le moment. Rappel : le dernier élément doit pointer vers
NULL pour signaler qu'il est en fin de liste.

Nous avons ainsi créé en mémoire une liste composée d'un seul élément :

La liste vient d'être initialisée.

3.2.4 OPERATION SUR UNE LISTE CHAINEE

3.2.4.1 AJOUTEZ UN ELEMENT


La première chose à savoir dans le cas de l’ajout d’un nouvel élément dans une liste chainée,
c’est où est-ce qu'on devrait le mettre.

On peut choisir. Pour le moment, de le mettre au début parce que c'est plus facile à
comprendre.

46
On doit créer une fonction qui peut mettre un nouveau élément au début de la liste. Prenons
un exemple : imaginons qu'il y ait déjà trois éléments dans la liste et qu'on veuille en ajouter
un nouveau au début :

Il va falloir adapter le pointeur premier de la liste ainsi que le pointeur suivant de notre nouvel
élément pour "insérer" correctement celui-ci dans la liste.

Le codeci-dessous représente une fonction d'insertion dans une liste chaînée. La fonction
prend en paramètres un pointeur vers une structure Liste et un entier représentant le nouveau
nombre à insérer. Elle crée un nouvel élément de liste (struct Element) en allouant
dynamiquement de la mémoire avec malloc, puis vérifie si la liste et le nouvel élément ont été
correctement alloués. Ensuite, elle assigne la valeur du nouvel entier à l'attribut "nombre" du
nouvel élément, et insère ce nouvel élément au début de la liste chaînée en mettant à jour les
pointeurs appropriés. Si la liste ou le nouvel élément n'ont pas pu être alloués, le programme
se termine avec un code d'erreur.
void insertion(Liste *liste, int nvNombre)
{
/* Création du nouvel élément */
Element *nouveau = malloc(sizeof(*nouveau));
if (liste == NULL || nouveau == NULL)
{
exit(EXIT_FAILURE);
}
nouveau->nombre = nvNombre;
/* Insertion de l'élément au début de la liste */
nouveau->suivant = liste->premier;
liste->premier = nouveau;
}
Nous avons ici choisi pour simplifier d'insérer l'élément en début de liste. Pour mettre à jour
correctement les pointeurs, nous devons procéder dans cet ordre précis :

47
1. Faire pointer notre nouvel élément vers son futur successeur, qui est l'actuel premier
élément de la liste.
2. Faire pointer le pointeur premier vers notre nouvel élément.
Cela aura pour effet d'insérer correctement notre nouvel élément dans la liste chaînée :

NB : On ne peut pas suivre ces étapes dans l'ordre inverse. Si vous faites d'abord pointer
premier vers notre nouvel élément, vous perdez l'adresse du premier élément de la liste.

3.2.4.2 SUPPRIMER UN ELEMENT


Dans une liste chainée, la suppression ne pose pas de difficulté supplémentaire. Il faut
cependant adapter les pointeurs de la liste dans le bon ordre pour ne perdre aucune
information.

Le code ci-dessous représente une fonction de suppression dans une liste chaînée. La
fonction prend en paramètre un pointeur vers une structure Liste. Elle commence par vérifier
si la liste existe. Si la liste existe et n'est pas vide (c'est-à-dire que le premier élément n'est
pas NULL), la fonction crée un pointeur vers l'élément à supprimer (aSupprimer), met à jour le
pointeur du premier élément de la liste pour pointer vers l'élément suivant, puis libère la
mémoire de l'élément à supprimer à l'aide de la fonction free(). Ainsi, la fonction supprime
l'élément en tête de la liste si la liste n'est pas vide. Si la liste est vide, aucun élément n'est
supprimé. Si la liste n'existe pas, le programme se termine avec un code d'erreur.
void suppression(Liste *liste)
{
if (liste == NULL)
{
exit(EXIT_FAILURE);
}
if (liste->premier != NULL)
{
Element *aSupprimer = liste->premier;
liste->premier = liste->premier->suivant;
free(aSupprimer);
}
}

48
Suppression d'un élément de la liste chaînée réussie.
Il faut bien comprendre qu'on doit faire les choses dans un ordre précis :
▪ Faire pointer premier vers le second élément.
▪ Supprimer le premier élément avec un free.
Si on fait l'inverse, on perd l'adresse du second élément.

3.2.4.3 AFFICHEZ LA LISTE CHAINEE


Pour afficher la liste chainée, il suffit de partir du premier élément et d'afficher chaque élément
un à un en "sautant" de bloc en bloc. Le code ci-dessous représente une fonction d'affichage
d'une liste chaînée. La fonction prend en paramètre un pointeur vers une structure Liste. Elle
commence par vérifier si la liste existe. Si la liste existe, la fonction initialise un pointeur (actuel)
sur le premier élément de la liste, puis utilise une boucle while pour parcourir la liste. À chaque
itération, elle affiche la valeur de l'élément actuel suivi de " -> " et met à jour le pointeur actuel
pour pointer vers l'élément suivant. La boucle continue jusqu'à ce que le pointeur actuel
devienne NULL, ce qui indique la fin de la liste. Enfin, la fonction affiche "NULL" pour indiquer
la fin de la liste. Si la liste n'existe pas, le programme se termine avec un code d'erreur. Cette
fonction permet d'afficher les éléments d'une liste chaînée de manière séquentielle.
void afficherListe(Liste *liste)
{
if (liste == NULL)
{
exit(EXIT_FAILURE);
}
Element *actuel = liste->premier;
while (actuel != NULL)
{
printf("%d -> ", actuel->nombre);
actuel = actuel->suivant;
}
printf("NULL\n");
}
▪ On part du premier élément et on affiche le contenu de chaque élément de la liste (un
nombre).
▪ On se sert du pointeur suivant pour passer à l'élément qui suit à chaque fois.

49
IMPLEMENTATION DE LA LISTE DANS LE MAIN
On peut tester la création de notre liste chaînée et son affichage avec un main. Le programme
principal commence par initialiser une liste chaînée à l'aide de la fonction `initialisation()`.
Ensuite, il insère les nombres 4, 8 et 15 dans la liste à l'aide de la fonction `insertion()`, suivi
d'une opération de suppression utilisant la fonction `suppression()` pour retirer le premier
élément de la liste. Enfin, la fonction `afficherListe()` est appelée pour afficher les éléments
restants de la liste après ces opérations. Le programme illustre ainsi le fonctionnement des
opérations d'insertion, suppression et affichage sur une liste chaînée, offrant un exemple
pratique de manipulation de données structurées dans le contexte des structures de données
en C.
int main()
{
Liste *maListe = initialisation();
insertion(maListe, 4);
insertion(maListe, 8);
insertion(maListe, 15);
suppression(maListe);
afficherListe(maListe);
return 0;
}
En plus du premier élément (que l'on a laissé ici à 0), on en ajoute trois nouveaux à cette liste.
Puis on en supprime un. Au final, le contenu de la liste chaînée sera donc : 8 -> 4 -> 0 -> NULL

3.3 CONCEPT DE PILE ET DE FILE


Imaginez que vous gérez une liste d'articles qui évolue avec le temps. Vous pouvez ajouter et
retirer des éléments de cette liste. Supposons que l'on ajoute toujours les nouveaux éléments
au début de la liste. Lorsqu'il s'agit de retirer des éléments, deux scénarios simples sont
intéressants à examiner :
▪ On retire toujours les éléments du début de la liste.
▪ On retire toujours les éléments de la fin de la liste.
Ces deux situations se produisent fréquemment dans la vie quotidienne. Dans le premier cas,
on parle d'une structure de pile, et dans le deuxième cas, d'une structure de file.

Par exemple, imaginez qu'un professeur ait donné un devoir à ses étudiants avec une limite
de temps. Juste avant la fin du temps imparti, certains étudiants commencent à rendre leurs
copies en avance. Le professeur décide de commencer à corriger les copies qu'il reçoit pour
gagner du temps. Il a une pile de copies sur son bureau, et voici comment cela fonctionne :

o Un étudiant termine son devoir et le remet au professeur, qui le place au-dessus


de la pile.
o Lorsqu'il a fini de corriger une copie, il prend la première copie de la pile pour
la corriger.
50
Cela correspond à ce qu'on appelle une pile. On décrit souvent ce comportement par
l'expression "dernier entré, premier sorti" (ou LIFO, Last In, First Out) : la dernière copie remise
est la première à être corrigée.

En revanche, lorsque vous faites la queue à la caisse d'un supermarché, cela correspond à
une file d'attente : les clients arrivent d'un côté de la file (au "début de la file"), et la caissière
est à l'autre bout (à la "fin de la file"). Lorsqu'elle a fini de traiter les achats d'un client, elle fait
avancer le client qui est à la fin de la file. C'est le comportement "premier entré, premier sorti"
(ou FIFO, First In, First Out).

Question : Imaginons un boulanger qui cuit du pain, le stocke, puis le vend à ses clients. Les
clients préfèrent avoir du pain le plus frais possible (juste sorti du four), et le boulanger ne peut
pas vendre du pain trop sec (qui est sorti du four depuis longtemps). Quels sont les avantages
d'une pile ou d'une file dans ce cas ? Quelle structure choisirait-il ?

o S'il utilise une pile, il vendra toujours le pain le plus frais possible à ses clients
en donnant à chaque client le pain en haut de la pile, c'est-à-dire celui qui vient
de sortir du four. Cependant, le pain en bas de la pile risque d'attendre trop
longtemps et pourrait devenir invendable.
o S'il utilise une file, il vendra toujours du pain un peu moins frais à ses clients,
mais le pain ne restera jamais trop longtemps chez le boulanger, évitant ainsi
de devenir trop sec.

En pratique, les boulangers n'appliquent généralement aucune de ces méthodes : ils préparent
le pain le matin pour la journée, s'assurant de vendre tout avant la fin de la journée. Ainsi, le
pain n'attend jamais plus d'un jour chez le boulanger.

3.4 LISTE CHAINEE SIMPLE (FILE)


Une File (ou Queue) est une structure de données linéaire dans laquelle les éléments sont
accessibles selon une discipline FIFO (“First-In First-Out”) : le premier élément inséré dans la
liste est le premier à en sortir.

51
▪ Insérer un élément dans la file est appelé enfiler (enqueue)
▪ Supprimer un élément de la file est appelé défiler (dequeue)

L’accès aux objets de la file se fait grâce à deux pointeurs, l’un pointant sur l’élément qui a été
inséré en premier et l’autre pointant sur celui inséré en dernier.

Les listes chaînées simples, souvent appelées files, sont constituées de nœuds
successivement liés les uns aux autres. Chaque nœud contient un élément de données et un
pointeur vers le nœud suivant dans la séquence. La file commence par un nœud appelé "tête"
et se termine par un nœud particulier dont le pointeur vers le nœud suivant est nul, indiquant
ainsi la fin de la liste.

La file est une structure de données, qui permet de stocker les données dans l'ordre FIFO
(First In First Out) - en français Premier Entré Premier Sorti). La récupération des données
sera faite dans l'ordre d'insertion.

52
L'image ci-dessus montre un diagramme d'une file qui permet de stocker des données dans
un ordre FIFO (premier entré, premier sorti). Les données sont stockées dans une liste
chaînée, où chaque élément de la liste contient les données elles-mêmes et un pointeur vers
l'élément suivant dans la liste. Le premier élément de la file est appelé le premier entré (First
In). Le dernier élément de la file est appelé le dernier entré (Last In).

Dans une file, les éléments sont placés les uns à côtés des autres on sort les éléments du plus
ancien vers le plus récent. Cela correspond à ce qui se passe dans une file d'attente : si on
prend l'exemple d’un garagiste, il reçoit les clients le matin et réceptionne leurs fiches dans
une file d'attente. Pour effectuer les travaux sur le véhicule, le garagiste va récupérer les fiches
en commençant par le premier client arrivé, c'est à dire la première fiche entrée dans la file.
C'est la loi du premier arrivé, premier servi ou FIFO (First In First out). C'est le comportement
classique d'une file d'attente.

3.4.1 LA CONSTRUCTION DU PROTOTYPE D'UN ELEMENT DE LA

FILE
Pour définir un élément de la file, le type struct sera utilisé, comprenant un champ "donnee" et
un pointeur "suivant". Il est essentiel que le pointeur suivant soit du même type que l'élément
lui-même, garantissant ainsi sa capacité à pointer vers l'élément. Ce mécanisme de pointage
via le pointeur suivant facilitera l'accès au prochain élément de la file.
typedef struct ElementListe {
char *donnee;
struct ElementListe *suivant;
}Element;
Pour assurer le contrôle de la file, il est recommandé de sauvegarder certains éléments clés
tels que le premier élément, le dernier élément et le nombre total d'éléments. Pour mettre en
œuvre cette gestion, une autre structure sera utilisée, bien que l'utilisation de variables ne soit
pas exclue. La composition de cette structure est la suivante :
typedef struct ListeRepere{
Element *debut;
Element *fin;
int taille;
} File;

3.4.2 OPERATIONS SUR LES FILES

3.4.2.1 INITIALISATION
La fonction initialisation initialise les pointeurs debut et fin avec NULL ainsi que la taille avec
la valeur 0. Cela doit être effectué avant toute autre opération sur la file.
void initialisation (File * suite){
suite->debut = NULL;
suite->fin = NULL;
suite->taille = 0;

53
}
3.4.2.2 INSERTION D'UN ELEMENT DANS LA FILE
L'algorithme d'insertion implique la déclaration d'éléments à insérer, l'allocation de mémoire
pour le nouvel élément, le remplissage des données, la mise à jour des pointeurs debut et fin,
et l'ajustement de la taille de la file.
Etapes
▪ Déclaration d'élément(s) à insérer
▪ Allocation de la mémoire pour le nouvel élément
▪ Remplir le contenu du champ de données
▪ Mettre à jour le pointeur debut vers le 1er élément (le début de file)
▪ Mettre à jour le pointeur fin (ça nous servira pour l'insertion vers la fin de la file)
▪ Mettre à jour la taille de la file
La 1ère image affiche le début de l'insertion, donc la liste a la taille 1 après l'insertion.

Dans la file, l'élément à récupérer c'est le 1er entré. Pour cela, l'insertion se fera toujours à la
fin de la file. Il s'agit de l'ordre normal de l'insertion (1er, 2ème, 3ème ...... etc.).

54
LA FONCTION

Le code ci-dessous représente une fonction "enfiler" qui prend en paramètres une file (File)
représentée par une structure, un élément courant (courant) et une donnée (donnee) à insérer
dans la file. La fonction crée un nouveau élément (nouveau_element) en allouant
dynamiquement de la mémoire pour la structure de l'élément ainsi que pour la donnée à
stocker. Ensuite, la donnée est copiée dans le nouveau élément. En fonction de la position du
courant (tête de la file ou un élément existant), le nouveau élément est inséré à la position
appropriée dans la file, et les liens sont mis à jour en conséquence. Enfin, la taille de la file est
mise à jour, et la fonction renvoie 0 en cas de succès ou -1 en cas d'échec (manque de
mémoire).
int enfiler(File *suite, Element *courant, char *donnee) {
// Création d'un nouveau_element et allocation mémoire
Element *nouveau_element;
if ((nouveau_element = (Element *)malloc(sizeof(Element))) == NULL)
return -1;
// Allocation mémoire pour la donnée
if ((nouveau_element->donnee = (char *)malloc(50 * sizeof(char))) ==
NULL) {
free(nouveau_element); // Libération de la mémoire en cas d'échec
return -1;
}
// Copie de la donnée dans le nouveau_element
strcpy(nouveau_element->donnee, donnee);

// Ajout du nouveau_element à la file


if (courant == NULL) {
// Cas où la file est vide

55
if (suite->taille == 0)
suite->fin = nouveau_element;
// Mise à jour des liens pour ajouter le nouveau_element en tête
nouveau_element->suivant = suite->debut;
suite->debut = nouveau_element;
} else {
// Cas où la file n'est pas vide
if (courant->suivant == NULL)
suite->fin = nouveau_element;
// Mise à jour des liens pour insérer le nouveau_element après le
courant
nouveau_element->suivant = courant->suivant;
courant->suivant = nouveau_element;
}
// Mise à jour de la taille de la file
suite->taille++;
return 0;
}
IMPLEMENTATION DE LA FONCTION DANS LE MAIN()

Dans l’exemple ci-dessous, la fonction main initialise une file, ajoute quelques éléments à
l'aide de la fonction enfiler, puis affiche le contenu de la file. Enfin, elle libère la mémoire
allouée pour les éléments de la file. Vous pouvez adapter ce code en fonction de vos besoins
spécifiques.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Structure pour un élément de la file
typedef struct Element {
char *donnee;
struct Element *suivant;
} Element;
// Structure pour la file
typedef struct File {
Element *debut;
Element *fin;
int taille;
} File;
// Prototype de la fonction d'insertion
int enfiler(File *suite, Element *courant, char *donnee);
int main() {
// Initialisation de la file
File maFile;
maFile.debut = NULL;
maFile.fin = NULL;
maFile.taille = 0;
// Exemple d'utilisation de la fonction enfiler
char donnee1[] = "Premier element";
char donnee2[] = "Deuxieme element";
char donnee3[] = "Troisieme element";
enfiler(&maFile, NULL, donnee1);
enfiler(&maFile, maFile.debut, donnee2);
enfiler(&maFile, maFile.fin, donnee3);
// Affichage des éléments de la file
Element *actuel = maFile.debut;

56
printf("Contenu de la file : ");
while (actuel != NULL) {
printf("%s -> ", actuel->donnee);
actuel = actuel->suivant;
}
printf("NULL\n");
// Libération de la mémoire allouée pour les éléments
actuel = maFile.debut;
while (actuel != NULL) {
Element *temp = actuel;
actuel = actuel->suivant;
free(temp->donnee);
free(temp);
}
return 0;
}

3.4.2.3 RETIRER UN ELEMENT DE LA FILE


La fonction "defiler" supprime l'élément vers lequel pointe le pointeur début, décrémente la
taille de la file, et libère la mémoire associée à cet élément. La fonction renvoie -1 en cas
d'échec et 0 sinon.
ETAPES :
▪ Le pointeur supp_elem contiendra l'adresse du 1er élément
▪ Le pointeur debut pointera vers le 2ème élément (après la suppression du 1er
élément, le 2ème sera au début de la file)
▪ La taille de la file sera décrémentée d'un élément

57
LA FONCTION

Le code suivant représente une fonction "défiler" qui prend en paramètre une file (File)
représentée par une structure. La fonction vise à retirer l'élément en tête de la file. Tout
d'abord, elle vérifie si la file n'est pas vide (taille égale à zéro), renvoyant -1 en cas contraire.
Ensuite, un pointeur "supp_element" est utilisé pour pointer vers l'élément en tête de la file.
Les liens de la file sont mis à jour pour pointer vers l'élément suivant comme nouveau début
de file. Ensuite, la mémoire allouée pour la donnée de l'élément et pour l'élément lui-même est
libérée à l'aide de la fonction "free". Enfin, la taille de la file est réduite et la fonction renvoie 0
pour indiquer le succès de l'opération.
int defiler (File * suite){
Element *supp_element;
if (suite->taille == 0)
return -1;
supp_element = suite->debut;
suite->debut = suite->debut->suivant;
free (supp_element->donnee);
free (supp_element);
suite->taille--;
return 0;
}
IMPLEMENTATION DE LA FONCTION DANS LE MAIN()

Dans ce code, le programme principal initialise une file vide et tente de retirer un élément,
affichant un message indiquant le succès ou l'échec de l’opération (La fonction "défiler" retire
l'élément en tête de la file.).
#include <stdio.h>
#include <stdlib.h>
// Structure de l'élément de la file
typedef struct Element {
// Données de l'élément
int donnee;
// Pointeur vers l'élément suivant dans la file
struct Element *suivant;
} Element;
// Structure de la file
typedef struct File {
// Pointeur vers le début de la file
Element *debut;
// Pointeur vers la fin de la file
Element *fin;
// Taille de la file (nombre d'éléments)
int taille;
} File;
// Déclaration de la fonction retirer
int defiler(File *suite);

int main() {
// Initialisation de la file
File maFile;
maFile.debut = NULL;

58
maFile.fin = NULL;
maFile.taille = 0;
// Exemple d'utilisation de la fonction retirer
int resultat = retirer(&maFile);
if (resultat == 0) {
printf("Retrait réussi.\n");
} else {
printf("La file est vide, impossible de retirer.\n");
}
return 0;
}

3.4.2.4 AFFICHAGE DE LA FILE


Pour afficher la file entière, il faut se positionner au début de la file (le pointeur debut le
permettra). Ensuite en utilisant le pointeur suivant de chaque élément, la file est parcourue du
1er vers le dernier élément. La condition d'arrêt est donnée par la taille de la file. La fonction
"affiche" parcourt la file du premier au dernier élément en utilisant les pointeurs suivants.
LA FONCTION

La procédure `affiche` ci-dessous prend en entrée un pointeur vers une structure de file (`File`)
et parcourt ses éléments à l'aide d'une boucle `for`. Pour chaque élément, elle affiche la
donnée associée (supposée être une chaîne de caractères) en utilisant la fonction `printf`. La
fonction utilise un pointeur `courant` pour traverser la file, en partant du début, et se déplace
à l'élément suivant à chaque itération. Ainsi, l'affichage résultant est une ligne de texte
montrant les données de chaque élément de la file, dans l'ordre où ils sont stockés, séparés
par des espaces.
void affiche(File *suite){
Element *courant;
int i;
courant = suite->debut;

for(i=0;i<suite->taille;++i){
printf(" %s ", courant->donnee);
courant = courant->suivant;
}
}
IMPLEMENTATION DE LA PROCEDURE DANS LE MAIN()
Le code suivant définit une structure de données de file (`File`) implémentée comme une liste chaînée
de structures élémentaires (`Element`). Il crée une file (`suite`) dans la fonction `main`, ajoute deux
éléments à la file avec des données spécifiques, puis affiche le contenu de la file à l'aide de la fonction
`affiche`.
#include <stdio.h>

typedef struct Element {


char *donnee;
struct Element *suivant;
} Element;

59
typedef struct File {
Element *debut;
int taille;
} File;

void affiche(File *suite) {


Element *courant;
int i;
courant = suite->debut;

for (i = 0; i < suite->taille; ++i) {


printf(" %s ", courant->donnee);
courant = courant->suivant;
}
}
int main() {
File suite;
suite.debut = NULL;
suite.taille = 0;
// Ajout d'éléments à la suite
suite.debut = malloc(sizeof(Element));
suite.debut->donnee = "élément 1";
suite.debut->suivant = NULL;
suite.taille++;
suite.debut->suivant = malloc(sizeof(Element));
suite.debut->suivant->donnee = "élément 2";
suite.debut->suivant->suivant = NULL;
suite.taille++;
// Affichage de la suite
affiche(&suite);
return 0;
}
En C, la création d'une liste chaînée simple implique la définition d'une structure de nœud et
la manipulation des pointeurs pour gérer les liaisons entre les éléments. Les opérations
courantes sur les listes chaînées simples incluent l'insertion en tête, l'insertion en fin de liste,
la suppression d'un élément spécifique et la traversée de la liste.

3.5 LISTE CHAINEE SIMPLE (PILE)


Une Pile (ou Stack) est une structure de données linéaire dans laquelle les éléments sont
accessibles selon une discipline LIFO (“Last-In First-Out”) : l’élément inséré en dernier est le
premier à sortir.

60
▪ Ajouter un élément dans la pile est appelé empiler ou pousser (push)
▪ Supprimer un élément de la pile est appelé dépiler (pop)

L’accès aux objets de la pile se fait grâce à un unique pointeur vers le sommet de la pile,
appelé top.

Les listes chaînées simples, souvent utilisées pour implémenter des piles, sont des structures
de données linéaires qui stockent des éléments de manière séquentielle. Une liste chaînée se
compose d'éléments appelés "nœuds", où chaque nœud contient une valeur et une référence
(ou un pointeur) vers le nœud suivant dans la séquence. Les piles, qui suivent le principe
"dernier entré, premier sorti" (LIFO - Last In, First Out), peuvent être implémentées
efficacement à l'aide de listes chaînées.

Une pile est une structure de données qui permet de stocker des éléments de manière
ordonnée. Les éléments sont ajoutés et supprimés au sommet de la pile, ce qui signifie que le
dernier élément ajouté est le premier élément à être supprimé. Par analogie, elle peut être
représentée par une pile d'assiettes. Les assiettes sont ajoutées au sommet de la pile, et la
première assiette à être retirée est la dernière à avoir été ajoutée.

Le schéma ci-dessous illustre une pile composée de trois éléments : "Donnée 1", "Donnée 2",
"Donnée 3". "Donnée 1" occupe la position supérieure de la pile, tandis que "Donnée 3" se
trouve à la base. Chaque élément est représenté par un rectangle, relié aux autres par des
lignes pour indiquer leurs relations(pointeurs). Pour ajouter un élément, il suffit de le placer au
sommet, et pour le supprimer, il doit être retiré de la base de la pile.

61
3.5.1.1 LA CONSTRUCTION DU PROTOTYPE D'UN ELEMENT DE LA PILE
La mise en place du prototype d'un élément de la pile implique l'utilisation du type struct.
Chaque élément de la pile comprendra un champ "donnee" et un pointeur "suivant". Il est
essentiel que le type du pointeur suivant soit le même que celui de l'élément pour garantir un
pointage correct. Ce pointeur "suivant" facilitera l'accès au prochain élément. La définition de
cette structure est réalisée avec le typedef de la struct ElementListe.
typedef struct ElementListe {
char *donnee;
struct ElementListe *suivant;
}Element;
Pour effectuer des opérations sur la pile, des informations clés seront sauvegardées,
notamment le premier élément et le nombre total d'éléments. Le premier élément, situé en
haut de la pile, permettra l'opération de récupération des données en haut de la pile. Une autre
structure, ListeRepere, sera utilisée pour cette fin, composée d'un pointeur "debut" contenant
l'adresse du premier élément et d'une variable "taille" indiquant le nombre total d'éléments.
typedef struct ListeRepere{
Element *debut;
int taille;
} Pile;
Notons que contrairement aux listes simplement chaînées, nous n'utilisons pas de pointeur
"fin" ici, car le pointeur "debut" suffit pour travailler uniquement au début de la liste. Peu importe
la position dans la liste, le pointeur "debut" pointe toujours vers le premier élément en haut de
la pile, et le champ "taille" indique le nombre d'éléments, quelle que soit l'opération effectuée
sur la pile.

62
3.5.2 OPERATIONS SUR LES PILES
3.5.2.1 INITIALISATION
La fonction d'initialisation doit être exécutée en premier avant toute autre manipulation de la
pile. Elle se caractérise par le prototype suivant : void initialisation(Pile *tas);
Cette opération initialise le pointeur "debut" avec la valeur NULL et la taille avec la valeur 0.
Voici l'implémentation de la fonction :
void initialisation(Pile *tas) {
tas->debut = NULL;
tas->taille = 0;
}

3.5.2.2 INSERTION D'UN ELEMENT DANS LA PILE


L'insertion d'un élément dans la pile s'effectue avec la fonction empiler, dont le prototype est
le suivant : int empiler(Pile *tas, char *donnee);
Le processus d'insertion consiste en la déclaration de l'élément à insérer, l'allocation de
mémoire pour le nouvel élément, le remplissage du champ de données, la mise à jour du
pointeur "debut" vers le nouvel élément (le sommet de la pile), et enfin, la mise à jour de la
taille de la pile.
ETAPES D’INSERTION D'UN ELEMENT DANS LA PILE
▪ Déclaration D'élément(S) A Insérer
▪ Allocation De La Mémoire Pour Le Nouvel Elément
▪ Remplir Le Contenu Du Champ De Données
▪ Mettre A Jour Le Pointeur Debut Vers Le 1er Elément (Le Haut De La Pile)
▪ Mettre A Jour La Taille De La Pile
La 1ère image montre le début de l'insertion, donc la liste a la taille 1 après l'insertion. La
caractéristique de la pile n'est pas très bien mise en évidence avec un seul élément, puisque
c'est le seul à récupérer.

En revanche la 2ème image nous permet d'observer le comportement de la pile.

63
La chose qu'il faut retenir, c'est que l'insertion se fait toujours en haut de la pile (au début de
la liste).

LA FONCTION

Le code ci-dessous implémente une fonction `empiler` qui prend en paramètres une structure
de pile (`Pile`) et une chaîne de caractères (`donnee`). Dans la fonction, un nouveau nœud
(`Element`) est créé dynamiquement à l'aide de l'allocation de mémoire (`malloc`). Si
l'allocation échoue, la fonction renvoie -1. Ensuite, un espace mémoire est alloué pour stocker
la chaîne de caractères dans le nouveau nœud. La fonction utilise la fonction `strcpy` pour
copier la chaîne de caractères fournie en paramètre dans le champ `donnee` du nouveau
nœud. Ensuite, le nouveau nœud est inséré au début de la pile en ajustant les pointeurs
appropriés, et la taille de la pile est incrémentée. Ainsi, la fonction réalise l'insertion d'un nouvel
élément contenant une chaîne de caractères au sommet de la pile spécifiée.

int empiler(Pile *tas, char *donnee) {


Element *nouveau_element;
if ((nouveau_element = (Element *)malloc(sizeof(Element))) == NULL)
return -1;

if ((nouveau_element->donnee = (char *)malloc(50 * sizeof(char))) ==


NULL)
return -1;
strcpy(nouveau_element->donnee, donnee);
nouveau_element->suivant = tas->debut;
tas->debut = nouveau_element;
tas->taille++;
}
IMPLEMENTATION DE LA FONCTION DANS LE MAIN()

64
Le code ci-dessous implémente une structure de pile (stack) à l'aide de listes chaînées. Il
définit une structure d'élément (`Element`) qui contient une chaîne de caractères (`donnee`)
et un pointeur vers l'élément suivant dans la pile (`suivant`). Il crée également une structure
de pile (`Pile`) qui contient un pointeur vers le premier élément de la pile (`debut`) et la taille
de la pile (`taille`). Le programme utilise une fonction `inserer` pour ajouter des éléments à la
pile, en allouant dynamiquement de la mémoire pour chaque élément et sa donnée. Ensuite,
il affiche le contenu de la pile et libère la mémoire allouée à la fin du programme. La pile est
mise en œuvre de manière dynamique, ce qui signifie qu'elle peut grandir ou diminuer en
fonction des ajouts et des suppressions d'éléments.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Définition de la structure Element
typedef struct Element {
char *donnee;
struct Element *suivant;
} Element;
// Définition de la structure Pile
typedef struct {
Element *debut;
size_t taille;
} Pile;
// Déclaration de la fonction inserer
int empiler(Pile *tas, char *donnee);
int main(void) {
// Création d'une pile
Pile maPile = {NULL, 0};
// Ajout d'éléments à la pile en utilisant la fonction inserer
if (inserer(&maPile, "Premier element") == -1) {
fprintf(stderr, "Erreur lors de l'ajout du premier element.\n");
return EXIT_FAILURE;
}
if (inserer(&maPile, "Deuxieme element") == -1) {
fprintf(stderr, "Erreur lors de l'ajout du deuxieme element.\n");
return EXIT_FAILURE;
}
// Affichage de la pile
Element *courant = maPile.debut;
printf("Contenu de la pile :\n");
while (courant != NULL) {
printf("%s\n", courant->donnee);
courant = courant->suivant;
}
// Libération de la mémoire à la fin du programme
courant = maPile.debut;
while (courant != NULL) {
Element *temp = courant;
courant = courant->suivant;
free(temp->donnee);
free(temp);
}
return EXIT_SUCCESS;

65
}
// Définition de la fonction empiler
int empiler(Pile *tas, char *donnee) {
Element *nouveau_element;
if ((nouveau_element = (Element *)malloc(sizeof(Element))) == NULL)
return -1;
if ((nouveau_element->donnee = (char *)malloc(50 * sizeof(char))) ==
NULL) {
free(nouveau_element);
return -1;
}
strcpy(nouveau_element->donnee, donnee);
nouveau_element->suivant = tas->debut;
tas->debut = nouveau_element;
tas->taille++;
return 0;
}

3.5.2.3 RETIRER UN ELEMENT DE LA PILE


La suppression d'un élément de la pile (dépilage) se réalise avec la fonction « depiler », il faut
tout simplement supprimer l'élément vers lequel pointe le pointeur debut. Cette opération ne
permet pas de récupérer la donnée en haut de la pile, mais seulement de la supprimer. Dont
le prototype est le suivant : int depiler(Pile *tas);
Cette opération supprime simplement l'élément pointé par le pointeur "debut", sans permettre
de récupérer la donnée en haut de la pile. La fonction renvoie -1 en cas d'échec et 0 sinon.
LES ETAPES
▪ Le Pointeur Supp_Elem Contiendra L'adresse Du 1er Elément
▪ Le Pointeur Debut Pointera Vers Le 2ème Elément (Après La Suppression Du 1er
Elément, Le 2ème Sera En Haut De La Pile)
▪ La Taille De La Pile Sera Décrémentée D'un Elément

66
LA FONCTION

Le code de la fonction "depiler" ci-dessous prend en paramètre un pointeur vers une structure
de pile (`Pile`). La fonction vise à retirer l'élément en haut de la pile. Elle commence par vérifier
si la pile est vide en examinant la variable "taille" de la pile. Si la pile est vide, la fonction
retourne -1 pour indiquer une erreur. Si la pile n'est pas vide, la fonction procède à la
suppression de l'élément en haut de la pile. Elle utilise un pointeur temporaire
(`supp_element`) pour stocker l'adresse de l'élément à supprimer, puis ajuste le pointeur de
début de la pile pour pointer vers l'élément suivant. Ensuite, la mémoire allouée pour la donnée
de l'élément est libérée, suivie de la libération de la mémoire de l'élément lui-même. Enfin, la
taille de la pile est décrémentée, et la fonction retourne 0 pour indiquer une suppression
réussie de l'élément en haut de la pile.
int depiler(Pile *tas) {
Element *supp_element;
if (tas->taille == 0)
return -1;
supp_element = tas->debut;
tas->debut = tas->debut->suivant;
free(supp_element->donnee);
free(supp_element);
tas->taille--;
return 0;
}
IMPLEMENTATION DE LA FONCTION DANS LE MAIN()

Le code ci-dessous implémente une structure de pile avec des fonctions associées, permettant
d'ajouter des éléments à la pile et de retirer l'élément situé au sommet. La pile est représentée
par une structure `Pile` contenant un pointeur vers le début et une variable de taille. Chaque
élément de la pile est une structure `Element` contenant un pointeur vers les données et un
pointeur vers l'élément suivant. Le programme principal démontre l'ajout d'éléments à la pile,
affiche son contenu avant et après le retrait, puis libère la mémoire allouée à la fin du
programme pour éviter les fuites de mémoire.
#include <stdio.h>
#include <stdlib.h>
// Structure pour un élément de la pile
typedef struct Element {
int *donnee;
struct Element *suivant;
} Element;

// Structure pour la pile


typedef struct {
Element *debut;
int taille;
} Pile;

// Fonction pour depiler un élément de la pile


int depiler(Pile *tas) {

67
Element *supp_element;
if (tas->taille == 0)
return -1;
supp_element = tas->debut;
tas->debut = tas->debut->suivant;
free(supp_element->donnee);
free(supp_element);
tas->taille--;
return 0;
}

int main() {
// Création d'une pile
Pile maPile;
maPile.debut = NULL; // Initialisation du début de la pile à NULL
maPile.taille = 0; // Initialisation de la taille de la pile à 0
// Exemple d'ajout de données à la pile (à adapter selon le type de
données)
int *donnee1 = malloc(sizeof(int));
*donnee1 = 42;
Element *element1 = malloc(sizeof(Element));
element1->donnee = donnee1;
element1->suivant = maPile.debut;
maPile.debut = element1;
maPile.taille++;
int *donnee2 = malloc(sizeof(int));
*donnee2 = 17;
Element *element2 = malloc(sizeof(Element));
element2->donnee = donnee2;
element2->suivant = maPile.debut;
maPile.debut = element2;
maPile.taille++;
// Affichage du contenu de la pile avant le retrait
printf("Contenu de la pile avant retrait : ");
Element *courant = maPile.debut;
while (courant != NULL) {
printf("%d ", *(courant->donnee));
courant = courant->suivant;
}
printf("\n");
// Retrait d'un élément de la pile
int resultatRetrait = depiler(&maPile);
if (resultatRetrait == 0) {
printf("Retrait réussi.\n");
} else {
printf("La pile est vide, impossible de retirer un élément.\n");
}
// Affichage du contenu de la pile après le retrait
printf("Contenu de la pile après retrait : ");
courant = maPile.debut;
while (courant != NULL) {
printf("%d ", *(courant->donnee));
courant = courant->suivant;
}
printf("\n");
// Libération de la mémoire utilisée par la pile (à faire à la fin du
programme)

68
courant = maPile.debut;
while (courant != NULL) {
Element *temp = courant;
courant = courant->suivant;
free(temp->donnee);
free(temp);
}
return 0;
}

3.5.2.4 AFFICHAGE DE LA PILE


Pour afficher la pile entière, la fonction affiche utilise le pointeur "debut" pour se positionner au
début de la pile. En parcourant la pile du premier au dernier élément à l'aide du pointeur
suivant, elle affiche les données. La condition d'arrêt est déterminée par la taille de la pile.
LA FONCTION

Le code de la procédure "affiche" ci-dessous prend en paramètre un pointeur vers une


structure de pile (Pile). La pile est représentée sous forme de liste chaînée, où chaque élément
de la pile est un nœud (Element) contenant une donnée (donnee) et un pointeur vers l'élément
suivant dans la pile (suivant). La fonction parcourt la pile à partir du début, affichant chaque
élément de la pile (représenté par la donnée) sur la sortie standard (stdout) à l'aide de la
fonction printf. Elle utilise une boucle for pour itérer à travers les éléments de la pile en utilisant
le pointeur "courant", et la variable "i" pour suivre le nombre d'éléments déjà affichés.

void affiche(Pile *tas) {


Element *courant;
int i;
courant = tas->debut;
for (i = 0; i < tas->taille; ++i) {
printf("\t\t%s\n", courant->donnee);
courant = courant->suivant;
}
}
IMPLEMENTATION DE LA PROCEDURE DANS LE MAIN()
Le code ci-dessous définit une structure de pile (stack) avec des opérations associées. Il utilise une
structure d'élément comprenant une donnée sous forme de chaîne de caractères et un pointeur vers
l'élément suivant. La pile est représentée par une structure comprenant un pointeur vers le premier
élément et une variable pour la taille. Trois fonctions sont implémentées : initialiserPile initialise une pile
vide, empiler ajoute un nouvel élément au sommet de la pile, et affiche affiche les éléments de la pile.
Dans la fonction principale, une pile est créée, initialisée, et alimentée avec trois éléments de test, puis
affichée.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Structure d'un élément de la pile


typedef struct Element {

69
char donnee[50]; // Changer la taille selon vos besoins
struct Element* suivant;
} Element;

// Structure de la pile
typedef struct Pile {
Element* debut;
int taille;
} Pile;
// Prototypes des fonctions
void initialiserPile(Pile* tas);
void empiler(Pile* tas, const char* donnee);
void affiche(Pile* tas);

int main() {
// Création d'une pile
Pile maPile;
initialiserPile(&maPile);
// Empilement de quelques éléments de test
empiler(&maPile, "Element 1");
empiler(&maPile, "Element 2");
empiler(&maPile, "Element 3");
// Affichage de la pile
affiche(&maPile);

return 0;
}
// Initialisation d'une pile vide
void initialiserPile(Pile* tas) {
tas->debut = NULL;
tas->taille = 0;
}
// Fonction pour empiler un élément
void empiler(Pile* tas, const char* donnee) {
Element* nouvelElement = (Element*)malloc(sizeof(Element));
if (nouvelElement == NULL) {
// Gestion d'erreur si l'allocation échoue
fprintf(stderr, "Erreur d'allocation de mémoire\n");
exit(EXIT_FAILURE);
}
strcpy(nouvelElement->donnee, donnee);
nouvelElement->suivant = tas->debut;
tas->debut = nouvelElement;
tas->taille++;
}
// Fonction pour afficher les éléments de la pile
void affiche(Pile* tas) {
Element* courant;
int i;
courant = tas->debut;
for (i = 0; i < tas->taille; ++i) {
printf("\t\t%s\n", courant->donnee);
courant = courant->suivant;
}
}

Tableau Comparatif Et Résumé Entre La File Et La Pile

70
3.6 LISTE DOUBLEMENT CHAINEE

3.6.1 INTRODUCTION
Après avoir vu les listes simplement chaînées, pour cette seconde partie, nous allons nous
attarder sur les listes doublement chaînées. Une liste doublement chaînée permet de gagner
en complexité sur l'insertion et la suppression par rapport à la liste simplement chaînée, en
plus de permettre un parcours des éléments à l'envers. Mais on utilise un pointeur
supplémentaire par élément. Il faut donc maintenir cohérentes deux fois plus de variables que
dans une liste simplement chaînée.

3.6.1.1 DEFINITION
Une liste doublement chaînée est une liste chaînée dans laquelle on peut accéder à l’élément
prédécesseur. C’est une structure de données qui représente une séquence d'éléments.
Chaque élément de la liste contient deux pointeurs : un vers l'élément suivant et un vers
l'élément précédent. Ici, chaque nœud possède des liens vers le nœud précédant et suivant.

71
Les nœuds dans les listes doublement chaînée se composent de trois éléments : l'un contient
les données et deux autres sont des pointeurs du nœud suivant et précédent. Ces deux
pointeurs aident à avancer ou à reculer à partir d'un nœud particulier.

La différence entre une liste doublement chainée et une liste simplement chainée est donc
que, dans une liste doublement chainée, chaque élément connait l'élément précédent et
l'élément suivant (la liaison entre les éléments se fait grâce à deux pointeurs (un qui pointe
vers l'élément précédent et un qui pointe vers l'élément suivant). En bref, le déplacement se
fait dans les deux directions, du premier vers le dernier élément et/ou du dernier vers le premier
élément.
Cela permet de simplifier l'insertion ou la suppression d'un élément donné, ainsi que le
parcours en sens inverse.

NB : Chaque liste chaînée est pourvue d'un nœud de tête et d'un nœud de queue. Le nœud
principal, également appelé nœud de tête, ne possède pas de nœud « précédent » (pointeur
précédent), tandis que le nœud de queue n'est pas relié à un nœud « suivant ».
Quelques termes essentiels relatifs à une liste chaînée double sont les suivants :
▪ Précédant : Chaque nœud est relié à son nœud précédent par le biais d'un pointeur ou
d'un lien. Ce lien est utilisé pour établir une connexion vers le nœud situé avant lui dans la
séquence.
▪ Suivant : Chaque nœud est relié à son nœud suivant par le biais d'un pointeur ou d'un lien.
Ce mécanisme permet d'établir une liaison vers le nœud suivant dans la séquence.
▪ Données : Ce terme fait référence à l'espace réservé dans un nœud destiné au stockage
d'informations. Les « données » peuvent englober diverses structures de données internes,
telles qu'une chaîne de caractères, un dictionnaire, un ensemble, un hashmap, etc. Ainsi,
les « Données » peuvent contenir des structures de données complexes.
Exemple d'une liste doublement chainée contenant les éléments 1, 2, 3, 4 et 5 :
[1] -> [2] -> [3] -> [4] -> [5]
Dans cet exemple, chaque élément contient un pointeur vers l'élément suivant. Le premier
élément, [1], pointe sur le deuxième élément, [2]. Le deuxième élément, [2], pointe sur le
troisième élément, [3], et ainsi de suite.

72
L'élément [5], le dernier de la liste, pointe sur NULL. Cela signifie qu'il n'y a pas d'élément
suivant.
Les opérations courantes sur les listes doublement chainées sont les suivantes :
▪ Insertion : pour insérer un élément à la fin de la liste, il suffit de créer un nouvel élément
et de pointer son pointeur suivant vers l'élément suivant de la liste.
▪ Suppression : pour supprimer un élément de la liste, il suffit de modifier les pointeurs
des éléments précédents et suivants de l'élément à supprimer.
▪ Parcours : pour parcourir la liste, il suffit de commencer par l'élément de tête et de
suivre le pointeur suivant de chaque élément jusqu'à ce que l'on atteigne NULL.

Les listes doublement chainées sont une structure de données versatile qui peut être utilisée
dans de nombreux contextes. Elles sont notamment utilisées dans les algorithmes de tri, de
recherche et de parcours.

3.6.2 LA CONSTRUCTION DU PROTOTYPE D'UN ELEMENT DE LA

LISTE
Pour définir un élément de la liste le type struct sera utilisé. L'élément de la liste contiendra un
champ donnee, un pointeur precedent et un pointeur suivant. Les pointeurs precedent et
suivant doivent être du même type que l'élément, sinon ils ne pourront pas pointer vers un
élément de la liste. Le pointeur "precedent" permettra l'accès vers l'élément précédent tandis
que le pointeur suivant permettra l'accès vers le prochain élément.

Le code ci-dessous définit une structure de données `dl_ElementListe`, représentant un


élément d'une liste doublement chaînée. Chaque élément contient un pointeur vers une chaîne
de caractères (`donnee`), ainsi que deux pointeurs vers des éléments adjacents dans la liste,
l'un vers l'élément précédent (`precedent`) et l'autre vers l'élément suivant (`suivant`). La
structure est ensuite renommée en tant que `dl_Element` à l'aide du typedef, permettant ainsi
de déclarer des variables de type `dl_Element` plus facilement.
typedef struct dl_ElementListe {
char *donnee;
struct dl_ElementListe *precedent;
struct dl_ElementListe *suivant;
}dl_Element;
Pour avoir le contrôle de la liste il est préférable de sauvegarder certains éléments : le premier
élément, le dernier élément, le nombre d'éléments. Pour réaliser cela une autre structure sera
utilisée (ce n'est pas obligatoire, des variables peuvent être utilisées).

Le code suivant définit une structure de données `dl_ListeRepere`, qui représente une liste
doublement chaînée repérée. La structure comporte trois membres : `debut` et `fin` sont des
pointeurs vers le premier et le dernier élément de la liste, respectivement, et `taille` est un

73
entier représentant le nombre d'éléments dans la liste. Cette structure fournit un moyen de
suivre les extrémités de la liste et de maintenir la taille de la liste de manière efficace. La
structure est renommée en tant que `dl_Liste` à l'aide du typedef pour faciliter la déclaration
de variables de ce type.
typedef struct dl_ListeRepere {
dl_Element *debut;
dl_Element *fin;
int taille;
}dl_Liste;
Le pointeur debut contiendra l'adresse du premier élément de la liste.
Le pointeur fin contiendra l'adresse du dernier élément de la liste.
La variable taille contient le nombre d'éléments.
Quelle que soit la position dans la liste, les pointeurs debut et fin pointent toujours
respectivement vers le 1er et le dernier élément.
Le champ taille contiendra le nombre d'éléments de la liste quelle que soit l'opération effectuée
sur la liste.

3.6.3 OPERATIONS DE LISTE DOUBLEMENT CHAINEE


Les opérations d'une liste doublement chaînée incluent l'ajout et la suppression de nœuds,
l'insertion et la suppression de nœuds et le parcours de la liste chaînée de haut en bas.

Pour l'insertion ainsi que pour la suppression, une seule fonction est largement suffisante si
elle est bien conçue en fonction des besoins.

3.6.3.1 INITIALISATION
Prototype de la procédure : void initialisation (Liste *liste);
Cette opération doit être faite avant toute autre opération sur la liste.
Elle initialise le pointeur debut et le pointeur fin avec le pointeur NULL, et la taille avec la valeur
0.
Le code ci-dessous définit une procédure `initialisation`, prenant en paramètre un pointeur
vers une structure de type `Liste`. La fonction initialise les membres de la structure en mettant
à zéro les pointeurs `debut` et `fin`, les faisant pointer vers NULL, et en attribuant la valeur 0
à la variable `taille`. En d'autres termes, la procédure prépare la liste pour une utilisation
ultérieure en la vidant et en réinitialisant sa taille à zéro.
void initialisation (Liste *liste){
liste->debut = NULL;
liste->fin = NULL;
liste->taille = 0;
}
3.6.3.2 INSERTION D'UN ELEMENT DANS LA LISTE

ALGORITHME D'INSERTION ET DE SAUVEGARDE DES ELEMENTS :

74
▪ Déclaration D'élément(S) A Insérer
▪ Allocation De La Mémoire Pour Le Nouvel Elément
▪ Remplir Le Contenu Du Champ De Données
▪ Mettre A Jour Les Pointeurs Vers L'élément Précédent Et L'élément Suivant
▪ Mettre A Jour Les Pointeurs Vers Le 1er Et Le Dernier Elément Si Nécessaire.
▪ Cas Particulier : Dans Une Liste Avec Un Seul Elément, Le 1er Est En Même
Temps Le Dernier.
▪ Mettre A Jour La Taille De La Liste
Pour ajouter un élément dans la liste il y a plusieurs situations :
1. Insertion dans une liste vide
2. Insertion au début de la liste
3. Insertion à la fin de la liste
4. Insertion avant un élément
5. Insertion après un élément

3.6.3.2.1 INSERTION DANS UNE LISTE VIDE


Prototype de la fonction : int ins_dans_liste_vide (dl_Liste * liste, char
*donnee);
La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.
❖ Étapes
▪ Allocation De La Mémoire Pour Le Nouvel Elément
▪ Remplir Le Champ De Données Du Nouvel Elément
▪ Le Pointeur Precedent Du Nouvel Elément Pointera Vers Null (Vu Que
L'insertion Est Faite Dans Une Liste Vide On Utilise L'adresse Du Pointeur
Debut Qui Vaut Null)
▪ Le Pointeur Suivant Du Nouvel Elément Pointera Vers Null (Vu Que
L'insertion Est Faite Dans Une Liste Vide On Utilise L'adresse Du Pointeur
Fin Qui Vaut Null)
▪ Les Pointeurs Debut Et Fin Pointeront Vers Le Nouvel Elément
▪ La Taille Est Mise A Jour

75
LA FONCTION

Le code ci-dessous définit une fonction `insertion_dans_liste_vide`, qui insère une nouvelle
donnée dans une liste doublement chaînée vide. La fonction alloue un nouvel élément de liste,
copie la donnée fournie, met à jour les pointeurs `precedent` et `suivant` pour indiquer le début
et la fin de la liste, incrémente la taille de la liste, puis renvoie 0 pour signaler une insertion
réussie. En cas d'échec de l'allocation mémoire, la fonction renvoie -1. Notez que la fonction
utilise une fonction `alloc` pour allouer de la mémoire, mais les détails de cette fonction ne
sont pas fournis dans le code donné.
int insertion_dans_liste_vide (dl_Liste * liste, char *donnee){
dl_Element *nouveau_element;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
nouveau_element->precedent = liste->debut;
nouveau_element->suivant = liste->fin;
liste->debut = nouveau_element;
liste->fin = nouveau_element;
liste->taille++;
return 0;
}
3.6.3.2.2 INSERTION AU DEBUT DE LA LISTE
Prototype de la fonction : int ins_debut_liste (dl_Liste * liste, char *donnee);
La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.
❖ Étapes
▪ Allocation De La Mémoire Pour Le Nouvel Elément
▪ Remplir Le Champ De Données Du Nouvel Elément
▪ Le Pointeur Precedent Du Nouvel Elément Pointe Vers Null
▪ Le Pointeur Suivant Du Nouvel Elément Pointe Vers Le 1er Elément
▪ Le Pointeur Precedent Du 1er Elément Pointe Vers Le Nouvel Elément

76
▪ Le Pointeur Debut Pointe Vers Le Nouvel Elément
▪ Le Pointeur Fin Ne Change Pas
▪ La Taille Est Incrémentée

LA FONCTION

Le code suivant présente la fonction `ins_debut_liste` qui vise à insérer une nouvelle donnée
au début d'une liste doublement chaînée. La fonction alloue un nouvel élément de liste, copie
la donnée fournie, met à jour les pointeurs `precedent` et `suivant` pour refléter la nouvelle
configuration de la liste, incrémente la taille de la liste, puis renvoie 0 pour indiquer une
insertion réussie. En cas d'échec de l'allocation mémoire, la fonction renvoie -1. Notons que
la fonction utilise une fonction `alloc` pour allouer de la mémoire, mais les détails de cette
fonction ne sont pas inclus dans le code fourni. Il est important de souligner que le code peut
provoquer des erreurs si la liste est vide (le pointeur `liste->debut` est NULL), car la tentative
d'accès à `liste->debut->precedent` dans ce cas peut entraîner un comportement indéfini.
int ins_debut_liste (dl_Liste * liste, char *donnee){
dl_Element *nouveau_element;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
nouveau_element->precedent = NULL;
nouveau_element->suivant = liste->debut;
liste->debut->precedent = nouveau_element;

77
liste->debut = nouveau_element;
liste->taille++;
return 0;
}

3.6.3.2.3 INSERTION A LA FIN DE LA LISTE


Prototype de la fonction : int ins_fin_liste (dl_Liste * liste, char *donnee);
La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.
❖ Étapes
▪ Allocation De La Mémoire Pour Le Nouvel Elément
▪ Remplir Le Champ De Données Du Nouvel Elément
▪ Le Pointeur Suivant Du Nouvel Elément Pointe Vers Null
▪ Le Pointeur Precedent Du Nouvel Elément Pointe Vers Le Dernier Elément (C'est
Le Pointeur Fin Qui Contient Pour L'instant Son Adresse
▪ Le Pointeur Suivant Du Dernier Elément Va Pointer Vers Le Nouvel Elément
▪ Le Pointeur Fin Pointe Vers Le Nouvel Elément
▪ Le Pointeur Debut Ne Change Pas
▪ La Taille Est Incrémentée

LA FONCTION

Le code ci-dessous implémente une fonction `ins_fin_liste`, conçue pour insérer une nouvelle
donnée à la fin d'une liste doublement chaînée. La fonction alloue un nouvel élément de liste,

78
copie la donnée fournie, met à jour les pointeurs `precedent` et `suivant` pour refléter la
nouvelle configuration de la liste, incrémente la taille de la liste, puis renvoie 0 pour signaler
une insertion réussie. En cas d'échec de l'allocation mémoire, la fonction renvoie -1. Il est
important de noter que le code peut entraîner des erreurs si la liste est vide (le pointeur `liste-
>fin` est NULL), car la tentative d'accès à `liste->fin->suivant` dans ce cas peut provoquer un
comportement indéfini. De plus, le fonctionnement de la fonction dépend de la disponibilité
d'une fonction `alloc` pour allouer de la mémoire, mais les détails de cette fonction ne sont pas
inclus dans le code fourni.
int ins_fin_liste (dl_Liste * liste, char *donnee){
dl_Element *nouveau_element;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
nouveau_element->suivant = NULL;
nouveau_element->precedent = liste->fin;
liste->fin->suivant = nouveau_element;
liste->fin = nouveau_element;
liste->taille++;
return 0;
}
3.6.3.2.4 INSERTION AVANT UN ELEMENT DE LA LISTE
Prototype de la fonction : int ins_avant (dl_Liste * liste, char *donnee, int
pos);
La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.
L'insertion s'effectuera avant une certaine position passée en argument à la fonction.
La position indiquée ne doit pas être ni le 1er ni le dernier élément. Dans ce cas il faut utiliser
les fonctions d'insertion au début et/ou à la fin de la liste.
❖ ÉTAPES
▪ Allocation De La Mémoire Pour Le Nouvel Elément
▪ Remplir Le Champ De Données Du Nouvel Elément
▪ Choisir Une Position Dans La Liste (L'insertion Se Fera Après La Position
Choisie)
▪ Le Pointeur Suivant Du Nouvel Elément Pointe Vers L'élément Courant.
▪ Le Pointeur Precedent Du Nouvel Elément Pointe Vers L'adresse Sur La Quelle
Pointe Le Pointeur Precedent D'élément Courant. (Une Explication Un Peu
Tordue Avec L'espoir Que L'image Vous Eclaira ;-)
▪ Le Pointeur Suivant De L'élément Qui Précède L'élément Courant Pointera Vers
Le Nouveau Elément
▪ Le Pointeur Precedent D'élément Courant Pointe Vers Le Nouvel Elément
▪ Le Pointeurs Fin Ne Change Pas
▪ Le Pointeur Debut Change Si Et Seulement La Position Choisie Est La Position1

79
▪ La Taille Est Incrémentée D'une Unité

LA FONCTION

Le code suivant présente une fonction nommée `ins_avant`, destinée à insérer une nouvelle
donnée avant la position spécifiée dans une liste doublement chaînée. La fonction
commence par allouer un nouvel élément de liste, puis copie la donnée fournie. Elle utilise
une boucle for pour atteindre l'élément à la position indiquée (`pos`). Ensuite, elle met à jour
les pointeurs `precedent` et `suivant` pour refléter l'insertion du nouvel élément. Si l'élément
à la position spécifiée a un prédécesseur, le pointeur `suivant` de ce prédécesseur est ajusté
pour pointer vers le nouvel élément. Si l'élément à la position spécifiée n'a pas de
prédécesseur (c'est-à-dire, il est le premier de la liste), le pointeur `debut` de la liste est mis
à jour pour pointer vers le nouvel élément. Enfin, la taille de la liste est augmentée, et la

80
fonction renvoie 0 pour indiquer une insertion réussie. En cas d'échec de l'allocation
mémoire, la fonction renvoie -1. Comme dans les exemples précédents, le fonctionnement
de la fonction dépend de l'existence d'une fonction `alloc` pour allouer de la mémoire, mais
les détails de cette fonction ne sont pas fournis dans le code donné.
int ins_avant (dl_Liste * liste, char *donnee, int pos){
int i;
dl_Element *nouveau_element, *courant;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
courant = liste->debut;
for (i = 1; i < pos; ++i)
courant = courant->suivant;
nouveau_element->suivant = courant;
nouveau_element-> precedent = courant->precedent;
if(courant->precedent == NULL)
liste->debut = nouveau_element;
else
courant->precedent->suivant = nouveau_element;
courant->precedent = nouveau_element;
liste->taille++;
return 0;
}

3.6.3.2.5 INSERTION APRES UN ELEMENT DE LA LISTE


Prototype de la fonction : int ins_apres (dl_Liste * liste, char *donnee, int pos);
La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.
L'insertion s'effectuera après une certaine position passée en argument à la fonction.
La position indiquée ne doit pas être le dernier élément. Dans ce cas il faut utiliser la fonction
d'insertion à la fin de la liste.
❖ ÉTAPES
▪ Allocation de la mémoire pour le nouvel élément
▪ Remplir Le Champ De Données Du Nouvel Elément
▪ Choisir Une Position Dans La Liste (L'insertion Se Fera Après La Position
Choisie)
▪ Le Pointeur Suivant Du Nouvel Elément Pointe Vers L'adresse Sur La Quelle
Pointe Le Pointeur Suivant D'élément Courant (Voir L'image)
▪ Le Pointeur Precedent Du Nouvel Elément Pointe Vers L'élément Courant.
▪ Le Pointeur Precedent De L'élément Qui Succède L'élément Courant Pointera
Vers Le Nouveau Elément
▪ Le Pointeur Suivant D'élément Courant Pointe Vers Le Nouvel Elément
▪ Le Pointeurs Debut Ne Change Pas
▪ Le Pointeur Fin Change Si Et Seulement La Position Choisie Est La Position Du
Dernier Elément (La Taille De Liste)

81
▪ La Taille Est Incrémentée D'une Unité

LA FONCTION

Le code suivant fournit une fonction "ins_apres" qui prend en paramètres une liste
doublement chaînée (dl_Liste), une donnée de type chaîne de caractères (donnee), et une
position (pos). La fonction insère un nouvel élément contenant la donnée fournie après
l'élément situé à la position spécifiée dans la liste. Le code commence par allouer de la
mémoire pour le nouvel élément et renvoie -1 en cas d'échec de l'allocation. Ensuite, il copie
la donnée dans le champ correspondant du nouvel élément. En utilisant une boucle for, la
fonction parcourt la liste jusqu'à atteindre la position spécifiée. Ensuite, elle ajuste les
pointeurs pour insérer le nouvel élément après l'élément à la position indiquée. Si l'élément
à la position spécifiée est le dernier élément de la liste, le pointeur "fin" de la liste est mis à
jour. Finalement, la taille de la liste est augmentée de 1, et la fonction renvoie 0 pour indiquer
le succès de l'insertion.

82
int ins_apres (dl_Liste * liste, char *donnee, int pos){
int i;
dl_Element *nouveau_element, *courant;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
courant = liste->debut;
for (i = 1; i < pos; ++i)
courant = courant->suivant;
nouveau_element->suivant = courant->suivant;5
nouveau_element->precedent = courant;
if(courant->suivant == NULL)
liste->fin = nouveau_element;
else
courant->suivant->precedent = nouveau_element;
courant->suivant = nouveau_element;
liste->taille++;
return 0;
}
3.6.3.2.6 SUPPRESSION D’UN ELEMENT DE LA LISTE
L'algorithme de suppression d'un élément de la liste utilise un pointeur temporaire pour
conserver l'adresse de l'élément à supprimer. L'emplacement de l'élément peut être n'importe
où dans la liste. Comparé aux listes simplement chaînées, où la suppression est limitée à un
élément spécifique, les listes doublement chaînées sont plus flexibles grâce à leurs deux
pointeurs qui permettent de suivre les éléments tant en amont qu'en aval.
Les étapes générales de suppression comprennent la libération de la mémoire occupée par
l'élément supprimé et la mise à jour de la taille de la liste. Les différentes situations de
suppression sont les suivantes :
1. Suppression au début de la liste
2. Suppression à la fin de la liste
3. Suppression avant un élément
4. Suppression après un élément
5. Suppression d'un élément
Toutefois, la suppression au début et à la fin de la liste doublement chaînées ainsi qu'avant ou
après un élément revient à la suppression à la position 0 (zéro) ou à la position N (N = nombre
d'éléments de la liste) ou ailleurs dans la liste.
Dans le cas des listes doublement chaînées la suppression à n'importe quelle position ne pose
pas des problèmes grâce aux pointeurs précédant et suivant, qui permettent de garder la
liaison entre les éléments de la liste.
C'est la raison pour laquelle nous allons créer une seule fonction.

5La ligne "courant->suivant->precedent = nouveau_element;" est utilisée pour mettre à jour les liens
précédents lors de l'insertion d'un nouvel élément dans une liste doublement chaînée. Cela assure la
continuité des liens dans la structure de la liste.

83
▪ Si Nous Voulons Supprimer L'élément Au Début De La Liste Nous Choisirons La
Position Zéro
▪ Si Nous Voulons Supprimer L'élément A La Fin De La Liste Nous Choisirons La
Position N (Le Nombre D'éléments)
▪ Si Nous Désirons Supprimer Un Elément Quelconque Alors On Choisit Sa
Position Dans La Liste
SUPPRESSION DANS LA LISTE
Prototype de la fonction : int supp(dl_Liste *liste, int pos);
La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.
Nous distinguons plusieurs situations :
▪ Suppression A La Position 1 Dans Une Liste Avec Un Seul Elément
▪ Suppression A La Position 1 Dans Une Liste Avec Plusieurs Eléments
▪ Suppression A La Dernière Position (Le Dernier Elément)
▪ Suppression Ailleurs Dans La Liste A Une Certaine Position
La suppression dans une liste vide n'a pas de sens.
❖ ÉTAPES
▪ La Position Choisie Est 1 (Le Cas De Suppression Du 1er Elément De La Liste)
▪ Le Pointeur Supp_Element Contiendra L'adresse Du 1er Elément
▪ Le Pointeur Debut Contiendra L'adresse Contenue Par Le Pointeur Suivant Du 1er
Elément Que Nous Voulons Supprimer (Si Ce Pointeur Vaut Null Alors Nous
Mettons A Jour Le Pointeur Fin Puisqu'on Est Dans Le Cas D'une Liste Avec Un
Seul Elément, Sinon Nous Faisons Pointer Le Pointeur Precedent Du 2ème Elément
Vers Null)

84
▪ La Position Choisie Est Egale Avec Le Nombre D'éléments De La Liste
▪ Le Pointeur Supp_Element Contiendra L'adresse Du Dernier Elément
▪ Nous Faisons Pointer Le Pointeur Suivant De L'avant Dernier Elément (C'est
L'élément Vers Le Quel Pointe Le Pointeur <Precedent> De Dernier Elément), Vers
Null
▪ Nous Mettons A Jour Le Pointeur Fin

▪ La Position Choisie Est Aléatoire Dans La Liste


▪ Le Pointeur Supp_Element Contiendra L'adresse De L'élément A Supprimer
▪ Le Pointeur Suivant D'élément Qui Précède L'élément A Supprimer Pointe Vers
L'adresse Contenu Par Le Pointeur Suivant D'élément A Supprimer
▪ Le Pointeur Precedent D'élément Qui Succède L'élément A Supprimer Pointe Vers
L'adresse Contenu Par Le Pointeur Precedent D'élément A Supprimer.

85
▪ La Taille De La Liste Sera Décrémentée D'un Elément

LA FONCTION
Le code ci-dessous fournit une fonction "supp" qui prend en paramètres une liste doublement
chaînée (dl_Liste) et la position d'un élément à supprimer (pos). La fonction commence par
vérifier si la taille de la liste est nulle, renvoyant -1 en cas de liste vide. Ensuite, elle gère trois
cas de suppression d'élément en fonction de la position spécifiée : suppression du premier
élément, suppression du dernier élément, et suppression d'un élément ailleurs dans la liste.
La fonction ajuste les pointeurs de début et de fin de la liste en conséquence. Après la
suppression de l'élément, elle libère la mémoire occupée par la donnée associée à l'élément
supprimé, puis libère la mémoire de l'élément lui-même. Enfin, la taille de la liste est
décrémentée, et la fonction renvoie 0 pour indiquer le succès de la suppression. Il convient de
noter une possible erreur de syntaxe dans le code : l'utilisation du double égal (==) au lieu de
l'opérateur d'affectation (=) lors de la mise à jour du pointeur "precedent" dans la branche
"else" après la suppression du premier élément.
int supp(dl_Liste *liste, int pos){
int i;
dl_Element *supp_element,*courant;

if(liste->taille == 0)
return -1;

if(pos == 1){ /* suppresion de 1er élément */


supp_element = liste->debut;
liste->debut = liste->debut->suivant;
if(liste->debut == NULL)
liste->fin = NULL;
else

86
liste->debut->precedent == NULL;
}else if(pos == liste->taille){ /* suppression du dernier élément */
supp_element = liste->fin;
liste->fin->precedent->suivant = NULL;
liste->fin = liste->fin->precedent;
}else { /* suppression ailleurs */
courant = liste->debut;
for(i=1;i<pos;++i)
courant = courant->suivant;
supp_element = courant;
courant->precedent->suivant = courant->suivant;
courant->suivant->precedent = courant->precedent;
}
free(supp_element->donnee);
free(supp_element);
liste->taille--;
return 0;
}
3.6.3.2.7 AFFICHAGE DE LA LISTE
Pour afficher l'intégralité de la liste, deux approches peuvent être adoptées : se positionner
soit au début, soit à la fin de la liste, en fonction du pointeur de début ou de fin. Ensuite, en
utilisant les pointeurs suivant ou précédent de chaque élément, la liste est parcourue soit du
premier au dernier élément, soit du dernier au premier élément. La condition d'arrêt du
parcours est déterminée par le pointeur suivant du dernier élément, qui prend la valeur NULL
lors de la lecture du début vers la fin de la liste. Dans le cas d'une lecture de la fin vers le début
de la liste, la condition d'arrêt est donnée par le pointeur précédent du premier élément, qui
prend la valeur NULL.
LES PROCEDURES
Les deux procédures suivantes, "affiche()" et "affiche_inv()", sont destinées à l'affichage des
éléments d'une liste doublement chaînée (dl_Liste) dans l'ordre direct et inverse,
respectivement. La fonction "affiche" parcourt la liste en partant du premier élément (debut) et
imprime les données de chaque élément séparé par des espaces, encadrées par des crochets.
La fonction "affiche_inv" parcourt la liste en partant du dernier élément (fin) et imprime les
données de chaque élément suivi de deux points, également encadrées par des crochets. Ces
fonctions utilisent une boucle while pour itérer à travers les éléments de la liste, en mettant à
jour le pointeur courant selon la direction de l'affichage. Les affichages sont suivis d'un saut
de ligne pour améliorer la lisibilité.
void affiche(dl_Liste *liste){ /* affichage en avançant */
dl_Element *courant;
courant = liste->debut; /* point du départ le 1er élément */
printf("[ ");
while(courant != NULL){
printf("%s ",courant->donnee);
courant = courant->suivant;
}

87
printf("]\n");
}

void affiche_inv(dl_Liste *liste){ /* affichage en reculant */


dl_Element *courant;
courant = liste->fin; /* point du départ le dernier élément */
printf("[ ");
while(courant != NULL){
printf("%s : ",courant->donnee);
courant = courant->precedent;
}
printf("]\n");
}

3.6.3.2.8 DESTRUCTION DE LA LISTE


Pour détruire l'intégralité de la liste, on a opté pour la suppression à la position 1 tant que la
taille de la liste est supérieure à zéro. Le code définit par la procédure `detruire()` ci-dessous
utilise une liste doublement chaînée (`dl_Liste`). Elle utilise une boucle pour supprimer tous
les éléments de la liste jusqu'à ce que la taille de la liste (`liste->taille`) soit réduite à zéro. La
fonction `supp` est probablement une fonction qui supprime un élément de la liste à une
position donnée.
void detruire(dl_Liste *liste){
while(liste->taille > 0)
supp(liste,1);
}

EXEMPLE COMPLET DE LA LISTE DOUBLEMENT CHAINEE

1. PREMIERE PARTIE
La première partie du code ci-dessous définit des opérations pour l'initialisation, l'insertion, la
suppression, l'affichage et la destruction d'une liste doublement chaînée (dl_Liste).
/* initialisation de la liste */
void initialisation (dl_Liste * liste);
dl_Element *alloc (dl_Element * nouveau_element);
/* INSERTION */
int ins_dans_liste_vide (dl_Liste * liste, char *donnee);
int ins_debut_liste (dl_Liste * liste, char *donnee);
int ins_fin_liste (dl_Liste * liste, char *donnee);
int ins_apres (dl_Liste * liste, char *donnee, int pos);
int ins_avant (dl_Liste * liste, char *donnee, int pos);
/* SUPPRESSION */
int supp(dl_Liste *liste, int pos);
void affiche (dl_Liste * liste);
/**************************/
void affiche_inv (dl_Liste * liste);
void detruire (dl_Liste * liste);
/* -------- FIN liste.h --------- */

88
2. DEUXIEME PARTIE
La seconde partie du code regroupe toutes les procédures et fonctions vues dans cette partie
du cours sur les listes doublement chainées. Il s’agit des fonctions et procédures pour
l'initialisation, l'insertion, la suppression, l'affichage, et la destruction d'une liste doublement
chaînée, avec une fonction de menu interactif pour choisir les opérations.
/***************************\
* dliste_function.h *
\***************************/
void initialisation (dl_Liste * liste){
liste->debut = NULL;
liste->fin = NULL;
liste->taille = 0;
}

int insertion_dans_liste_vide (dl_Liste * liste, char *donnee){


dl_Element *nouveau_element;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
nouveau_element->precedent = NULL;
nouveau_element->suivant = NULL;
liste->debut = nouveau_element;
liste->fin = nouveau_element;
liste->taille++;
return 0;
}

int ins_debut_liste (dl_Liste * liste, char *donnee){


dl_Element *nouveau_element;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
nouveau_element->precedent = NULL;
nouveau_element->suivant = liste->debut;
liste->debut->precedent = nouveau_element;
liste->debut = nouveau_element;
liste->taille++;
return 0;
}

int ins_fin_liste (dl_Liste * liste, char *donnee){


dl_Element *nouveau_element;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
nouveau_element->suivant = NULL;
nouveau_element->precedent = liste->fin;
liste->fin->suivant = nouveau_element;
liste->fin = nouveau_element;
liste->taille++;
return 0;
}

int ins_apres (dl_Liste * liste, char *donnee, int pos){

89
int i;
dl_Element *nouveau_element, *courant;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
courant = liste->debut;
for (i = 1; i < pos; ++i)
courant = courant->suivant;
nouveau_element->suivant = courant->suivant;
nouveau_element->precedent = courant;
if(courant->suivant == NULL)
liste->fin = nouveau_element;
else
courant->suivant->precedent = nouveau_element;
courant->suivant = nouveau_element;
liste->taille++;
return 0;
}

int ins_avant (dl_Liste * liste, char *donnee, int pos){


int i;
dl_Element *nouveau_element, *courant;
if ((nouveau_element = alloc (nouveau_element)) == NULL)
return -1;
strcpy (nouveau_element->donnee, donnee);
courant = liste->debut;
for (i = 1; i < pos; ++i)
courant = courant->suivant;
nouveau_element->suivant = courant;
nouveau_element-> precedent = courant->precedent;
if(courant->precedent == NULL)
liste->debut = nouveau_element;
else
courant->precedent->suivant = nouveau_element;
courant->precedent = nouveau_element;
liste->taille++;
return 0;
}

int supp(dl_Liste *liste, int pos){


int i;
dl_Element *supp_element,*courant;

if(liste->taille == 0)
return -1;

if(pos == 1){ /* suppresion de 1er élément */


supp_element = liste->debut;
liste->debut = liste->debut->suivant;
if(liste->debut == NULL)
liste->fin = NULL;
else
liste->debut->precedent == NULL;
}else if(pos == liste->taille){ /* suppression du dernier élément */
supp_element = liste->fin;
liste->fin->precedent->suivant = NULL;
liste->fin = liste->fin->precedent;

90
}else { /* suppression ailleurs */
courant = liste->debut;
for(i=1;i<pos;++i)
courant = courant->suivant;
supp_element = courant;
courant->precedent->suivant = courant->suivant;
courant->suivant->precedent = courant->precedent;
}
free(supp_element->donnee);
free(supp_element);
liste->taille--;
return 0;
}

void detruire(dl_Liste *liste){


while(liste->taille > 0)
supp(liste,1);
}

dl_Element *alloc (dl_Element * nouveau_element){


if ((nouveau_element = (dl_Element *) malloc (sizeof (dl_Element))) ==
NULL)
return NULL;
if ((nouveau_element->donnee = (char *) malloc (50 * sizeof (char)))
== NULL)
return NULL;
return nouveau_element;
}

int menu (dl_Liste *liste){


int choix;
if (liste->taille == 0){
printf ("1. Ajout du 1er element\n");
printf ("2. Quitter\n");
} else{
printf ("1. Ajout au debut de la liste\n");
printf ("2. Ajout a la fin de la liste\n");
printf ("3. Ajout avant la position specifie\n");
printf ("4. Ajout apres la position specifie\n");
printf ("5. Suppression à la position specifie\n");
printf ("6. Detruire la liste\n");
printf ("7. Quitter\n");
}
printf ("\n\nFaites votre choix : ");
scanf ("%d", &choix);
getchar();
if(liste->taille == 0 && choix == 2)
choix = 7;
return choix;
}
int supp(dl_Liste *liste, int pos);
void affiche(dl_Liste *liste){
dl_Element *courant;
courant = liste->debut;
printf("[ ");
while(courant != NULL){
printf("%s ",courant->donnee);

91
courant = courant->suivant;
}
printf("]\n");
}
void affiche_inv(dl_Liste *liste){
dl_Element *courant;
courant = liste->fin;
while(courant != NULL){
printf("%s : ",courant->donnee);
courant = courant->precedent;
}
printf("\n");
}
/* -------- FIN dliste_function.h --------- */

3. TROISIEME PARTIE
Le code de la dernière partie ci-dessous, utilise une liste doublement chaînée (`dl_Liste`)
avec des opérations d'initialisation, d'insertion, de suppression, d'affichage, et de destruction.
La fonction `menu` permet à l'utilisateur de choisir différentes opérations à effectuer sur la
liste en fonction de son état.

\**********************\
* dliste.c *
\**********************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dliste.h"
#include "dliste_function.h"

int main (void)


{

int choix = 0,pos;


char *donnee;
donnee = malloc(50);
dl_Liste *liste;
dl_Element *pilote = NULL;
liste = (dl_Liste *) malloc (sizeof(dl_Liste));

initialisation(liste);

while(choix != 7){
choix = menu(liste);
switch(choix){
case 1:
printf("Entrez un element : ");
scanf("%s",donnee);
getchar();
if(liste->taille == 0)
insertion_dans_liste_vide(liste,donnee);
else
ins_debut_liste (liste, donnee);
printf("%d elements: deb=%s,fin=%s ",
liste->taille,liste->debut->donnee,liste->fin->donnee);

92
affiche(liste);
break;
case 2:
printf("Entrez un element : ");
scanf("%s",donnee);
getchar();
ins_fin_liste (liste, donnee);
printf("%d elements: deb=%s,fin=%s ",
liste->taille,liste->debut->donnee,liste->fin->donnee);
affiche(liste);
break;
case 3:
if(liste->taille == 1){
printf("Utiliser l'insertion au debut ou a la fin (Entree
Menu : 1 ou 2)\n");
break;
}
printf("Entrez un element : ");
scanf("%s",donnee);
getchar();
do{
printf("Entrez la position : ");
scanf("%d",&pos);
}while (pos < 1 || pos > liste->taille);
getchar();
ins_avant(liste,donnee,pos);
printf("%d elements: deb=%s fin=%s ",
liste->taille,liste->debut->donnee,liste->fin->donnee);
affiche(liste);
break;
case 4:
if(liste->taille == 1){
printf("Utiliser l'insertion au debut ou a la fin (Entree Menu : 1
ou 2)\n");
break;
}
printf("Entrez un element : ");
scanf("%s",donnee);
getchar();
do{
printf("Entrez la position : ");
scanf("%d",&pos);
}while (pos < 1 || pos > liste->taille);
getchar();
ins_apres(liste,donnee,pos);
printf("%d elements: deb=%s,fin=%s ",
liste->taille,liste->debut->donnee,liste->fin->donnee);
affiche(liste);
break;
case 5:
do{
printf("Entrez la position : ");
scanf("%d",&pos);
}while (pos < 1 || pos > liste->taille);
getchar();
supp(liste,pos);
if(liste->taille != 0)

93
printf("%d elements: deb=%s,fin=%s ",
liste->taille,liste->debut->donnee,liste->fin->donnee);
else
printf("liste vide : %d elements",liste->taille);
affiche(liste);
break;
case 6:
detruire(liste);
printf("la liste a ete detruite : %d elements\n",liste->taille);
break;
}
}
return 0;
}
/* -------- FIN dliste.c --------- */

3.7 COMPLEXITE DES OPERATIONS SUR LES LISTES


DOUBLEMENT CHAINEES
La table récapitulative ci-dessous présente la complexité des principales opérations effectuées
sur une liste doublement chaînée. Pour la recherche au sein de la liste, la complexité demeure
linéaire (O(N)), nécessitant un parcours séquentiel des éléments. En revanche, les opérations
de recherche de l'élément suivant ou précédent, ainsi que les opérations d'insertion en début,
milieu ou fin de liste, affichent une complexité constante (O(1)). Cette caractéristique souligne
l'efficacité de la liste doublement chaînée pour les opérations d'insertion et de suppression, où
les références aux éléments suivants et précédents facilitent des modifications rapides. Ainsi,
la structure de la liste doublement chaînée est particulièrement avantageuse dans les
scénarios où des opérations dynamiques d'insertion et de suppression sont fréquemment
réalisées, même si la recherche linéaire reste une contrainte à prendre en compte.

3.8 LISTE POLYNOMIALES


La gestion des polynômes avec les listes chaînées consiste à représenter un polynôme en
utilisant une structure de données dynamique. Chaque terme du polynôme est stocké dans un

94
nœud de la liste chaînée, où chaque nœud contient le coefficient et l'exposant du terme, ainsi
qu'un pointeur vers le prochain nœud. Cette approche permet une flexibilité accrue dans la
manipulation des polynômes, car l'ajout, la suppression et la modification de termes peuvent
être réalisés efficacement en modifiant les liens entre les nœuds. Les opérations
mathématiques telles que l'addition, la soustraction et la multiplication de polynômes peuvent
également être implémentées de manière efficace en parcourant les listes chaînées
correspondantes. Ainsi, la gestion des polynômes avec les listes chaînées offre une solution
efficace et extensible pour manipuler des structures polynomiales complexes de manière
dynamique.
3.8.1 POLYNOME ET LISTE CHAINEE
3.8.1.1 POLYNOME
Un polynôme p(x) est une expression algébrique en variable x qui se présente sous la forme
(axn + bxn-1 + …. + jx+ k) , où a, b, c …., k appartiennent à la catégorie des
nombres réels et 'n' est un entier non négatif, appelé le degré du polynôme. Il est composé de
plusieurs termes, appelés monômes, qui sont reliés par des opérations d’addition ou de
soustraction. Par exemple, le polynôme 3x2−5x+2 est formé de trois monômes : 3x2, −5x
et 2.
Une caractéristique importante des polynômes est que chaque terme de l'expression
polynomiale se compose de deux parties :
▪ Le Coefficient
▪ L'exposant
Exemple :
Dans 10x2 + 26x, les coefficients sont 10 et 26, et les exposants sont 2 et 1.
Points à garder à l'esprit lors de la manipulation des polynômes :
Le signe de chaque coefficient et exposant est stocké dans le coefficient et l'exposant lui-
même.
Des termes supplémentaires ayant le même exposant sont possibles.
L'allocation de stockage pour chaque terme dans le polynôme doit être effectuée dans l'ordre
croissant et décroissant de leurs exposants.

3.8.1.2 LISTE CHAINEE


Une liste chainée est une structure de données qui permet de stocker une séquence
d’éléments, appelés nœuds, de manière dynamique. Chaque nœud contient une valeur et un
pointeur vers le nœud suivant dans la liste. Par exemple, la liste chainée [1 -> 2 -> 3 ->
NULL] est formée de trois nœuds : [1 ->], [2 ->] et [3 ->].
On peut représenter un polynôme par une liste chainée, en associant à chaque nœud un
monôme. Le coefficient et la puissance du monôme sont stockés dans la valeur du nœud, et

95
le pointeur vers le nœud suivant permet de passer au terme suivant du polynôme. Les
monômes sont ordonnés par ordre croissant de puissance dans la liste. Par exemple, le
polynôme 3x2−5x+2peut être représenté par la liste chainée [(2, 0) -> (-5, 1) -> (3,
2) -> NULL], où chaque paire (a, b) représente le monôme axb.
La gestion des polynômes avec les listes chainées consiste à définir des opérations sur les
polynômes, comme la création, l’affichage, l’évaluation, la dérivation, la somme, le produit,
etc., en utilisant les opérations sur les listes chainées, comme l’allocation, la libération,
l’insertion, la suppression, la recherche, etc.

Le figure ci-dessous illustre un algorithme combinatoire utilisé pour additionner deux


polynômes représentés par des listes chaînées de coefficients et de puissances. L'algorithme
parcourt les deux listes terme par terme, additionne les coefficients correspondants, et stocke
le résultat dans une nouvelle liste chaînée. La figure est divisée en deux parties, décrivant les
étapes initiales (lecture des données et initialisation des variables) et les étapes finales
(addition des coefficients et stockage du résultat). Les blocs spécifiques comprennent la
lecture des données, l'initialisation des variables, l'addition des coefficients, et le stockage du
résultat dans une nouvelle liste chaînée. Au final, l'algorithme réalise une addition polynomiale
en suivant un processus clairement défini.

3.8.1.3 REPRESENTATION DES POLYNOMES


Les polynômes peuvent être représentés de différentes manières. Voici quelques méthodes :
▪ À l'aide de tableaux
▪ À l'aide de listes chaînées

3.8.1.3.1 REPRESENTATION DES POLYNOMES A L'AIDE DE TABLEAUX


Il peut arriver que vous ayez besoin d'évaluer de nombreuses expressions polynomiales et
d'effectuer des opérations arithmétiques de base telles que l'addition et la soustraction avec
ces nombres. Pour cela, vous devrez trouver un moyen de représenter ces polynômes. La
méthode simple consiste à représenter un polynôme de degré 'n’, et à stocker les coefficients

96
des termes de degré n+1 du polynôme dans un tableau. Ainsi, chaque élément du tableau
contiendra deux valeurs : Le coefficient et L'exposant.

3.8.1.3.2 REPRESENTATION DES POLYNOMES A L'AIDE DE LISTES

CHAINEES
Un polynôme peut être considéré comme une liste ordonnée de termes non nuls. Chaque
terme non nul est un couple qui contient deux informations :
▪ La partie exponentielle
▪ La partie coefficient

3.8.1.3.2.1 ADDITION DE DEUX POLYNOMES A L'AIDE DE LISTES CHAINEES


L'addition de deux polynômes à l'aide de listes chaînées implique la fusion de deux structures
de données dynamiques représentant les polynômes. Chaque terme des polynômes est
stocké dans un nœud de la liste chaînée, comprenant le coefficient et l'exposant du terme,
ainsi qu'un pointeur vers le prochain nœud. Pour réaliser l'addition, les listes chaînées des
polynômes sont parcourues simultanément, en additionnant les coefficients des termes ayant
le même exposant. Les termes résultants sont ajoutés à une nouvelle liste chaînée
représentant le polynôme somme. Si un exposant est présent dans un seul des polynômes, le
terme correspondant est simplement ajouté à la liste somme. Cette approche permet une
manipulation efficace des polynômes, tout en évitant les opérations coûteuses associées à la
représentation statique des polynômes. Ainsi, l'addition de polynômes à l'aide de listes
chaînées offre une méthode flexible et optimale pour combiner des structures polynomiales
de manière dynamique.
Étant donné deux nombres polynomiaux représentés par une liste chaînée, écrivez une
fonction qui additionne ces listes, c'est-à-dire qui additionne les coefficients ayant les mêmes
puissances variables.
Exemple :
Entrée :
1er nombre = 5x^2 + 4x^1 + 2x^0
2e nombre = 5x^1 + 5x^0
Sortie :
5x^2 + 9x^1 + 7x^0
Entrée :
1er nombre = 5x^3 + 4x^2 + 2x^0
2e nombre = 5x^1 + 5x^0
Sortie :
5x^3 + 4x^2 + 5x^1 + 7x^0

97
L'image ci-dessous présente la procédure de sommation de deux polynômes représentés par
des listes chaînées, Liste 1 et Liste 2. La procédure commence par créer une nouvelle liste
chaînée vide appelée Liste résultante. En parcourant simultanément les deux listes d'origine,
chaque terme est additionné, et le résultat est stocké dans un nouveau nœud de la Liste
résultante. Cette étape se répète jusqu'à ce que les deux listes soient entièrement parcourues.
La procédure se conclut avec la Liste résultante contenant la somme des deux polynômes
d'origine. Les étapes détaillées comprennent la création de la Liste résultante, le parcours des
listes d'origine avec l'addition des coefficients et des puissances, et la création de nouveaux
nœuds pour stocker les résultats. En fin de procédure, la Liste résultante est renvoyée.

CODE DE L’EXEMPLE
Le code ci-dessous définit une structure de données `Node` pour représenter des termes de
polynômes, avec des champs pour le coefficient, la puissance et un pointeur vers le prochain
terme. Les fonctions incluses permettent de créer des nœuds pour les termes de polynômes,
d'additionner deux polynômes représentés par des listes chaînées, et d'afficher le résultat.
Dans la fonction `main`, deux polynômes sont créés, affichés, additionnés, et le résultat est
affiché. Le code facilite la manipulation des polynômes en utilisant une structure de données
chaînée pour représenter les termes, offrant une approche modulaire pour effectuer des
opérations sur les polynômes.
struct Node
{
int coeff; // Coefficient du terme
int pow; // Puissance du terme
struct Node *next; // Pointeur vers le prochain terme
};

98
void create_node(int x, int y, struct Node **temp)
{
struct Node *r, *z;
z = *temp;
if (z == NULL)
{
r = (struct Node *)malloc(sizeof(struct Node));
r->coeff = x;
r->pow = y;
*temp = r;
r->next = (struct Node *)malloc(sizeof(struct Node));
r = r->next;
r->next = NULL;
}
else
{
r->coeff = x;
r->pow = y;
r->next = (struct Node *)malloc(sizeof(struct Node));
r = r->next;
r->next = NULL;
}
}
void polyadd(struct Node *poly1, struct Node *poly2, struct Node *poly)
{
while (poly1->next && poly2->next)
{
if (poly1->pow > poly2->pow)
{
poly->pow = poly1->pow;
poly->coeff = poly1->coeff;
poly1 = poly1->next;
}
else if (poly1->pow < poly2->pow)
{
poly->pow = poly2->pow;
poly->coeff = poly2->coeff;
poly2 = poly2->next;
}
else
{
poly->pow = poly1->pow;
poly->coeff = poly1->coeff + poly2->coeff;
poly1 = poly1->next;
poly2 = poly2->next;
}
poly->next = (struct Node *)malloc(sizeof(struct Node));
poly = poly->next;
poly->next = NULL;
}
while (poly1->next || poly2->next)
{
if (poly1->next)
{
poly->pow = poly1->pow;
poly->coeff = poly1->coeff;

99
poly1 = poly1->next;
}
if (poly2->next)
{
poly->pow = poly2->pow;
poly->coeff = poly2->coeff;
poly2 = poly2->next;
}
poly->next = (struct Node *)malloc(sizeof(struct Node));
poly = poly->next;
poly->next = NULL;
}
}
void show(struct Node *node)
{
while (node->next != NULL)
{
printf("%dx^%d", node->coeff, node->pow);
node = node->next;
if (node->next != NULL)
printf(" + ");
}
}
int main()
{
struct Node *poly1 = NULL, *poly2 = NULL, *poly = NULL;
// Créer la première liste de 5x^2 + 4x^1 + 2x^0
create_node(5, 2, &poly1);
create_node(4, 1, &poly1);
create_node(2, 0, &poly1);
// Créer la deuxième liste de 5x^1 + 5x^0
create_node(5, 1, &poly2);
create_node(5, 0, &poly2);
printf("1er Nombre : ");
show(poly1);
printf("\n2e Nombre : ");
show(poly2);
poly = (struct Node *)malloc(sizeof(struct Node));
// Fonction pour additionner deux polynômes
polyadd(poly1, poly2, poly);
// Afficher la liste résultante
printf("\nPolynôme additionné : ");
show(poly);
return 0;
}

3.8.1.3.2.2 MULTIPLICATION DE DEUX POLYNOMES A L'AIDE DE LISTES

CHAINEES
La multiplication de deux polynômes à l'aide de listes chaînées implique une approche plus
complexe que l'addition. Pour effectuer cette opération, chaque terme du premier polynôme
doit être multiplié par chaque terme du deuxième polynôme, et les résultats sont additionnés
en fonction des exposants. On crée une nouvelle liste chaînée pour stocker les termes du

100
polynôme résultant. Pendant le processus de multiplication, on doit gérer les exposants et
coefficients de manière à éviter les doublons. Ainsi, lorsqu'un terme avec un exposant déjà
présent dans la liste résultante est atteint, le coefficient existant est mis à jour en additionnant
le produit du terme actuel. Cette méthode garantit que la nouvelle liste chaînée contienne tous
les termes nécessaires pour représenter le polynôme résultant de la multiplication. En
conclusion, la multiplication de polynômes à l'aide de listes chaînées implique une approche
itérative et une gestion minutieuse des exposants et coefficients pour obtenir un résultat
correct et efficace.
Soit deux polynômes sous forme de liste chaînée. La tâche consiste à trouver la multiplication
des deux polynômes.
Exemples :
Entrée : Poly1 : 3x^2 + 5x^1 + 6, Poly2 : 6x^1 + 8
Sortie : 18x^3 + 54x^2 + 76x^1 + 48
En multipliant chaque élément du premier polynôme avec les éléments du deuxième
polynôme, nous obtenons
18x^3 + 24x^2 + 30x^2 + 40x^1 + 36x^1 + 48
En ajoutant les valeurs avec la même puissance de x,
18x^3 + 54x^2 + 76x^1 + 48
Entrée : Poly1 : 3x^3 + 6x^1 - 9, Poly2 : 9x^3 - 8x^2 + 7x^1 + 2
Sortie : 27x^6 - 24x^5 + 75x^4 - 123x^3 + 114x^2 - 51x^1 - 18

Dans cette approche, nous multiplierons le 2e polynôme avec chaque terme du 1er
polynôme.
Stockeront la valeur multipliée dans une nouvelle liste chaînée.

101
Ensuite, nous ajouterons les coefficients des éléments ayant la même puissance dans le
polynôme résultant.
CODE DE L’EXEMPLE
Le code ci-dessous implémente l’opération de multiplication sur des polynômes. Il utilise une
structure de nœud pour représenter les termes du polynôme avec des coefficients et des
puissances. La fonction `addnode` ajoute un nouveau nœud à la fin de la liste chaînée
représentant un polynôme. La fonction `printList` affiche le polynôme. La fonction
`removeDuplicates` élimine les termes avec la même puissance en combinant leurs
coefficients. Enfin, la fonction `multiply` multiplie deux polynômes et stocke le résultat dans un
troisième polynôme, en utilisant les opérations sur les listes chaînées. Le programme principal
crée deux polynômes, les affiche, multiplie les deux polynômes, et affiche le résultat.
#include <stdio.h>
#include <stdlib.h>
// Structure de nœud contenant le coefficient et la puissance de la
variable
struct Node {
int coeff, power;
struct Node* next;
};
// Fonction pour ajouter un nouveau nœud à la fin de la liste
struct Node* addnode(struct Node* start, int coeff, int power)
{
// Créer un nouveau nœud
struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
newnode->coeff = coeff;
newnode->power = power;
newnode->next = NULL;
// Si la liste chaînée est vide
if (start == NULL)
return newnode;
// Si la liste chaînée a des nœuds
struct Node* ptr = start;
while (ptr->next != NULL)
ptr = ptr->next;
ptr->next = newnode;

return start;
}
// Fonction pour afficher la liste chaînée
void printList(struct Node* ptr)
{
while (ptr->next != NULL) {
printf("%dx^%d", ptr->coeff, ptr->power);
if (ptr->next != NULL && ptr->next->coeff >= 0)
printf("+");

ptr = ptr->next;
}
printf("%d\n", ptr->coeff);
}

102
// Fonction pour ajouter les coefficients de
// deux éléments ayant la même puissance
void removeDuplicates(struct Node* start)
{
struct Node *ptr1, *ptr2, *dup;
ptr1 = start;
/* Sélectionner les éléments un par un */
while (ptr1 != NULL && ptr1->next != NULL) {
ptr2 = ptr1;
// Comparer l'élément sélectionné
// avec le reste des éléments
while (ptr2->next != NULL) {

// Si la puissance de deux éléments est la même


if (ptr1->power == ptr2->next->power) {
// Ajouter leurs coefficients et le mettre dans le 1er élément
ptr1->coeff = ptr1->coeff + ptr2->next->coeff;
dup = ptr2->next;
ptr2->next = ptr2->next->next;
// Supprimer le 2ème élément
free(dup);
} else
ptr2 = ptr2->next;
}
ptr1 = ptr1->next;
}
}
// Fonction pour multiplier deux polynômes
struct Node* multiply(struct Node* poly1, struct Node* poly2,
struct Node* poly3)
{
// Créer deux pointeurs et stocker
// les adresses des 1er et 2ème polynômes
struct Node *ptr1, *ptr2;
ptr1 = poly1;
ptr2 = poly2;
while (ptr1 != NULL) {
while (ptr2 != NULL) {
int coeff, power;
// Multiplier le coefficient des deux
// polynômes et le stocker dans coeff
coeff = ptr1->coeff * ptr2->coeff;
// Ajouter la puissance des deux polynômes
// et la stocker dans power
power = ptr1->power + ptr2->power;
// Appeler la fonction addnode pour créer
// un nouveau nœud en passant trois paramètres
poly3 = addnode(poly3, coeff, power);
// Déplacer le pointeur du 2ème polynôme
// pour obtenir son terme suivant
ptr2 = ptr2->next;
}
// Déplacer le 2ème pointeur au
// point de départ du 2ème polynôme
ptr2 = poly2;
// Déplacer le pointeur du 1er polynôme
ptr1 = ptr1->next;
}

103
// Cette fonction sera appelée pour ajouter
// le coefficient des éléments ayant la même puissance
// à partir de la liste chaînée résultante
removeDuplicates(poly3);
return poly3;
}
// Fonction principale
int main()
{
struct Node *poly1 = NULL, *poly2 = NULL, *poly3 = NULL;
// Création du 1er polynôme : 3x^2 + 5x^1 + 6
poly1 = addnode(poly1, 3, 3);
poly1 = addnode(poly1, 6, 1);
poly1 = addnode(poly1, -9, 0);
// Création du 2ème polynôme : 9x^3 - 8x^2 + 7x^1 + 2
poly2 = addnode(poly2, 9, 3);
poly2 = addnode(poly2, -8, 2);
poly2 = addnode(poly2, 7, 1);
poly2 = addnode(poly2, 2, 0);
// Affichage du 1er polynôme
printf("1er Polynôme : ");
printList(poly1);
// Affichage du 2ème polynôme
printf("2ème Polynôme : ");
printList(poly2);
// Appel de la fonction multiply
poly3 = multiply(poly1, poly2, poly3);
// Affichage du polynôme résultant
printf("Polynôme Résultant : ");
printList(poly3);
return 0;
}

3.9 EXERCICES DE FIN DE CHAPITRE

3.9.1 EXERCICE 1
Considérons le code suivant, où tete pointe vers le début d'une liste chaînée :
typedef struct nœud {
int valeur;
struct nœud *lien;
} Nœud;
Complétez la fonction C suivante qui renvoie un pointeur vers le nœud qui précède (vient
avant) chercherNœud, s'il se trouve dans la liste chaînée. Si chercherNœudn'est pas trouvé
ou si la liste chaînée est vide, la fonction doit renvoyer NULL. Lisez attentivement la fonction :
vous devrez peut-être ajouter du code à plusieurs endroits.
Nœud *predecesseur(Nœud *tete, Nœud *chercherNœud) {
Nœud *courant;
courant = tete;
if (tete == chercherNœud) return NULL;
// COMPLETER LA LIGNE QUI SUIT:
while (…………………………………………………………………) {
if (courant->lien== chercherNœud)
return courant;
// ECRIRE LE CODE ICI:

104
}
return NULL;
}
3.9.2 EXERCICE 2
La structure C suivante est utilisée pour définir chaque nœud dans une liste chaînée :
typedef struct nœud {
int donnees;
struct nnœud *lien;
} Nœud;
Ecrivez une procédure C appelée printDuplicates() qui reçoit un pointeur vers le premier nœud
( tete) d'une liste chaînée en tant que paramètre. La fonction doit rechercher et imprimer les
entiers en double dans la liste chaînée. Par exemple, si la liste chaînée contient les entiers
6,3,3,6,7,4, alors printDuplicates() devrait afficher :
6
3
Remarque : Dans votre solution, vous pouvez supposer qu'un entier donné apparaît au plus
deux fois dans la liste chaînée.

3.9.3 EXERCICE 3
Créez une fonction qui transforme un arbre binaire donné en son miroir, comme illustré dans
la figure ci-dessous. La fonction échangera les parties gauche et droite de chaque nœud dans
l'arbre jusqu'aux feuilles. Vous avez le droit d'utiliser une seule variable locale supplémentaire,
mais aucune variable globale. Il n'est pas permis d'allouer de mémoire dynamique.

3.9.4 EXERCICE 4
Considérons une liste L représentant les étudiants d'une classe, avec chaque étudiant
caractérisé par son Matricule, Nom, Prénom et Moyenne.
1. Définissez une structure de données chaînée permettant de stocker la liste des
étudiants, et déclarez une liste vide.

105
2. Rédigez une fonction qui calcule la moyenne générale d'une classe L.
3. Élaborez un programme qui divise la liste L en deux sous-listes L1 et L2, de telle
manière que la première liste, L1, contienne uniquement les étudiants admis, et la
deuxième liste, L2, contienne les étudiants non admis.

3.9.5 EXERCICE 5 (Files)


Considérons le code d'une structure de File basée sur une liste chaînée, où initialiserFile()
initialise une file vide, et enfiler() ajoute un nouvel élément à la fin de la file.
typedef struct Element {
int donnee;
struct Element* suivant;
} Element;

typedef struct File {


Element* avant;
Element* arriere;
} File;
void initialiserFile(File* file) {
file->avant = NULL;
file->arriere = NULL;
}
void enfiler(File* file, int donnee) {
Element* nouvelElement = (Element*)malloc(sizeof(Element));
nouvelElement->donnee = donnee;
nouvelElement->suivant = NULL;

if (file->arriere == NULL) {
file->avant = nouvelElement;
file->arriere = nouvelElement;
} else {
file->arriere->suivant = nouvelElement;
file->arriere = nouvelElement;
}
}
// Fonction pour libérer la mémoire d'une liste chaînée
void freeListe(Element* actuel) {
Element* temp;

while (actuel != NULL) {


temp = actuel;
actuel = actuel->suivant;
free(temp);
}
}

1. Écrivez une procédure, nommée afficherFile(), qui affiche tous les éléments d'une file.
2. Élaborez une autre procédure, nommée defilerJusqua(), qui défile la file jusqu'à
atteindre l'élément spécifié (Elt). Notez que si l'élément Elt existe dans la file, il ne sera
pas défilé, mais restera dans la file. Cependant, si l'élément n'appartient pas à la file,
la fonction défilera tous les éléments de la file.

106
3.9.6 EXERCICE 6 (Piles et Files)
Considérons le code d’une structure de pile basée sur une liste chaînée, où initStack initialise
une pile vide, isEmpty vérifie si la pile est vide, push ajoute un élément au sommet de la pile,
et pop retire l'élément du sommet de la pile.
Écrivez une fonction inverser() pour inverser une pile `P1` contenant des nombres réels.
Choisissez judicieusement entre l'utilisation d'une pile ou d'une file pour cette opération.
Note : Pour l'exercice 4, il est suggéré de choisir une pile pour inverser les éléments, car
l'inversion d'une pile suit le principe de dernier entré, premier sorti (LIFO), ce qui est approprié
pour l'inversion.

3.9.7 EXERCICE 7 : GESTION DES POLYNOMES AVEC LES LISTES


CHAINEES
Le but de cet exercice est d’implémenter des opérations sur des polynômes par des listes
chaînées.
On représente un polynôme par une liste chaînée. Chaque cellule de la liste correspond à un
monôme, avec son coefficient et son exposant.
Par exemple, sur la figure ci-dessous, on représente la liste chaînée correspondant au
polynôme 5x4+2x2+3

On représente le Polynôme nul par une liste vide.


1. Définir la structure Poly qui a pour champs : le coefficient (réel), l'exposant (entier
positif).
2. Écrire une fonction de saisie au clavier d’un monôme.
3. Écrire une fonction qui ajout un monôme a la liste du polynôme, en traite tous les cas
possibles. Attention : le Polynôme doit être trié toujours en exposant décroissant.
4. Écrire une fonction qui revoie une copie du polynôme.
5. Écrire une fonction qui créer et renvoi la somme de deux polynômes.
6. Écrire une fonction qui créer et renvoi le polynôme dérivé.
7. Écrire une fonction qui affiche le polynôme.
.

107
CHAPITRE 4 TABLE DE HACHAGE

4.1 INTRODUCTION
Les tables de hachage, en informatique, sont des structures de données cruciales pour la
gestion efficace et rapide des données. En langage C, elles représentent un moyen puissant
d'optimiser la recherche, l'insertion et la suppression d'éléments dans un ensemble de
données. L'idée fondamentale derrière les tables de hachage est d'utiliser une fonction de
hachage pour mapper chaque élément à une position spécifique dans une table, souvent
appelée "bucket". Cette approche permet un accès direct aux éléments, réduisant ainsi le
temps de recherche par rapport à d'autres structures de données telles que les listes chaînées.
Les tables de hachage se démarquent particulièrement lorsqu'on les compare aux listes
chaînées. Alors que les listes chaînées offrent une flexibilité considérable pour l'ajout
dynamique d'éléments, elles peuvent devenir inefficaces en termes de recherche, surtout
lorsque la taille de la liste augmente. En revanche, les tables de hachage exploitent une
fonction de hachage pour répartir uniformément les éléments dans des emplacements
prédéterminés, minimisant ainsi les collisions6.
Les collisions surviennent lorsque deux éléments différents sont hashés vers la même position
dans la table. Pour résoudre ce problème, les tables de hachage utilisent souvent des
techniques telles que le chaînage, où chaque position de la table pointe vers une liste chaînée
contenant tous les éléments hashés à cette position. Cette approche permet une gestion
efficace des collisions tout en préservant la vitesse de recherche caractéristique des tables de
hachage.
Ainsi, en choisissant judicieusement une fonction de hachage et en traitant les collisions de
manière appropriée, les tables de hachage en C fournissent une solution optimale pour la
recherche rapide et la manipulation efficace des données, offrant des performances souvent
supérieures à celles des listes chaînées dans de nombreux scénarios d'application.

6
Collision : Dans une table de hachage, une collision est une situation dans laquelle deux clés
différentes ont la même valeur de hachage. Cela signifie qu'elles sont stockées dans la même alvéole
de la table.
Les collisions sont inévitables dans les tables de hachage, car la taille de la table est toujours
inférieure ou égale à l'ensemble des valeurs possibles des clés. Par exemple, si la table a une taille
de 10, et que les clés sont des nombres entiers, il y a une collision possible pour chaque paire de
nombres entre 0 et 9.

108
4.2 CONCEPT

4.2.1 DEFINITION ET PRESENTATION

4.2.1.1 DEFINITION
Une table de hachage est une structure de données qui associe des clés à des valeurs. C'est
donc une implémentation d’un tableau associatif (tableau dans lequel les indices peuvent être
autre chose que des petits entiers, aussi appelé dictionnaire) Elle utilise une fonction de
hachage pour transformer chaque clé en un index d’un tableau mais il peut y avoir des
collisions (= des clés différentes étant traduites en un même index) qu’il faut gérer.
SCHEMA D’UNE TABLE DE HACHAGE
Exemple 1 : on souhaite stocker {(Alice,Journaliste), (Bob,Mécanicien),
(Charles,Informaticien)}
▪ La fonction de hachage est le nombre de lettres dans le prénom
▪ Le tableau sous-jacent a 10 cases

Exemple 2 : on souhaite ajouter (David,Médecin)


▪ Une collision apparaît entre les clés « Alice » et « David »
▪ Dans cet exemple, on traite la collision avec une liste chaînée

109
4.2.2 PRESENTATION
Les listes chaînées ont un gros défaut lorsqu'on souhaite lire ce qu'elles contiennent : il n'est
pas possible d'accéder directement à un élément précis. Il faut parcourir la liste en avançant
d'élément en élément jusqu'à trouver celui qu'on recherche. Cela pose des problèmes de
performance dès que la liste chaînée devient volumineuse.

Les tables de hachage représentent une autre façon de stocker des données. Elles sont
basées sur les tableaux du langage C. Elles permettent de retrouver instantanément un
élément précis, que la table contienne 100, 1 000, 10 000 cases ou plus encore.

4.2.3 INTERET D'UNE TABLE DE HACHAGE


Les listes chaînées permettent d'ajouter ou de supprimer des cases à tout moment. Toutefois,
elles ont quand même un gros défaut : si on cherche à récupérer un élément précis de la liste,
il faut parcourir celle-ci en entier jusqu'à ce qu'on le retrouve.
Imaginons une liste chaînée qui contienne des informations sur des élèves : leur nom, leur âge
et leur moyenne. Chaque élève sera représenté par une structure Eleve.
Nous avons travaillé auparavant sur des listes chaînées qui contenaient des int . Cela dit, on
peut stocker ce qu'on veut dans une liste, même un pointeur vers une structure, comme je le
propose ici.
Si on veut retrouver les informations sur YANSO TCHAPTCHET, il va falloir parcourir toute la
liste en analysant chaque élément à partir du premier pour se rendre compte qu'il était à la fin
:

110
Bien entendu, si on avait cherché FOM NOTAM, cela aurait été beaucoup plus rapide puisqu'il
est au début de la liste. Néanmoins, pour évaluer l'efficacité d'un algorithme, on doit toujours
envisager le pire des cas. Et le pire, c'est YANSO TCHAPTCHET.
Ici, on dit que l'algorithme de recherche d'un élément a une complexité en O(n)
, car il faut parcourir toute la liste chaînée pour retrouver un élément donné, dans le pire des
cas où celui-ci est à la fin. Si la liste contient 9 éléments, il faudra 9 itérations au maximum
pour retrouver un élément.
Dans cet exemple, notre liste chaînée ne contient que quatre éléments. L'ordinateur retrouvera
YANSO TCHAPTCHET très rapidement. Mais imaginez que celui-ci se trouve à la fin d'une
liste chaînée de 10 000 éléments ! Ce n'est pas acceptable de devoir parcourir jusqu'à 10 000
éléments pour retrouver une information.
C'est là que les tables de hachage entrent en jeu.

4.2.3.1 TABLE DE HACHAGE


Les tableaux permettent un accès direct à un élément donné en utilisant son indice et non pas
de problème pour la recherche d'un élément dans une liste chaînée. Cela signifie que si vous
avez un tableau, vous pouvez accéder à n'importe quel élément en fournissant simplement
son indice, sans avoir à parcourir les éléments un par un. Cette caractéristique rend la
recherche plus efficace dans un tableau par rapport à une liste chaînée.
Pour accéder à l'élément d'indice 2 dans notre tableau tableau, il nous suffisait d'écrire ceci :
int tableau[4] = {12, 7, 14, 33};
printf("%d", tableau[2]);
Si on lui donne tableau[2], l'ordinateur va directement à la case mémoire où se trouve stocké
le nombre 14. Il ne parcourt pas les cases du tableau une à une.
Cependant comme nous l’avons dit dans le chapitre sur les listes chaînées, il est important de
noter que les tableaux ont leurs propres limitations, notamment en ce qui concerne l'ajout et
la suppression dynamiques d'éléments, ce qui est plus flexible avec les listes chaînées.

En effet, les listes chaînées sont plus flexibles. Les tableaux, eux, permettent un accès plus
rapide. Les tables de hachage constituent quelque part un compromis entre les deux.

111
Il y a un défaut important avec les tableaux dont on n'a pas beaucoup parlé jusqu'ici : les cases
sont identifiées par des numéros qu'on appelle des indices. Il n'est pas possible de demander
à l'ordinateur : "Dis-moi quelles sont les données qui se trouvent à la case « YANSO
TCHAPTCHET ».
Pour retrouver l'âge et la moyenne de YANSO TCHAPTCHET, on ne peut donc pas écrire :
tableau["YANSO TCHAPTCHET"];
Ce serait pourtant pratique de pouvoir accéder à une case du tableau rien qu'avec le nom ! Eh
bien avec les tables de hachage, c'est possible ;
Les tables de hachage ne font pas partie du langage C. Il s'agit simplement d'un concept. On
va réutiliser les briques de base du C que l'on connaît déjà pour créer un nouveau système
intelligent.
Puisque notre tableau doit forcément être numéroté par des indices, comment fait-on pour
retrouver le bon numéro de case si on connaît seulement le nom " YANSO TCHAPTCHET " ?
Un tableau reste un tableau, et celui-ci ne fonctionne qu'avec des indices numérotés.
Imaginez un tableau dans lequel chaque case a un indice et possède un pointeur vers une
structure de type Eleve :

Si on veut retrouver la case correspondant à Luc Doncieux, il faut transformer son nom en
indice du tableau, et donc associer chaque nom à un numéro de case :
▪ FOM NOTAL = 0 ;
▪ COSTY BANKOUE = 1 ;

112
▪ SANI NGATCHUI = 2 ;
▪ YANSO TCHAPTCHET = 3.
NB : On ne peut pas écrire tableau ["YANSO TCHAPTCHET"] comme nous l’avons fait
précédemment. Ce n'est pas valide en C.
Pour transformer cette chaine de caractères en numéro, il faut écrire une fonction qui prend
en entrée une chaîne de caractères, fait des calculs avec, puis retourne en sortie un numéro
correspondant à cette chaîne.
Ce numéro sera l'indice de la case dans notre tableau :

La fonction de hachage génère un indice correspondant au nom envoyé en paramètre

4.2.3.2 ECRITURE D’UNE FONCTION DE HACHAGE


La création d'une fonction de hachage correcte pose un défi particulier. Comment peut-on
convertir une chaîne de caractères en un nombre unique ?

Pour commencer, clarifions les choses : une table de hachage ne se limite pas à 4 cases
comme illustré dans mes exemples, mais plutôt à 100, 1 000, voire plus. Peu importe la taille
du tableau, la recherche de l'élément reste rapide.

On dit que c'est une complexité en O(1), car on peut trouver directement l'élément
recherché. La fonction de hachage renvoie un indice, et il suffit de "sauter" directement à la
case correspondante du tableau, éliminant ainsi la nécessité de parcourir toutes les cases.
Imaginons un tableau de 100 cases destiné à stocker des pointeurs vers des structures d'élèves.
Eleve* tableau[100];
Nous devons concevoir une fonction qui génère, à partir d'un nom, un nombre compris entre
0 et 99 (les indices du tableau). C'est là que la créativité est nécessaire. Bien qu'il existe des

113
méthodes mathématiques complexes pour "hacher" des données, nous pouvons ici simplifier
en proposant d'additionner les valeurs ASCII de chaque lettre du nom.
'Y'+'A'+'N'+'S'+'O'+' '+'T'+'C'+'H'+'A'+'P'+'T'+'C'+'H'+'E'+'T'
Cependant, un problème se pose : la somme des valeurs ASCII peut dépasser 100. Chaque lettre dans
la table ASCII peut être numérotée jusqu'à 255. On a donc vite fait de dépasser 100. Pour remédier à
cela, l'opérateur modulo % est utilisé pour obtenir un nombre entre 0 et 99.
Par exemple, si la somme fait 4 315, le reste de la division par 100 est 15. La fonction de hachage
retournera donc 15.
La fonction ci-dessous calcule une somme des valeurs ASCII des caractères dans la chaîne et renvoie
le reste de la division par 100 de cette somme, ce qui donne un résultat entre 0 et 99.:
int hachage(char *chaine)
{
int i = 0, nombreHache = 0;
for (i = 0 ; chaine[i] != '\0' ; i++)
{
nombreHache += chaine[i];
}
nombreHache %= 100;
return nombreHache;
}

Grâce à cette fonction de hachage, on sait dans quelle case du tableau placer les données.
Lorsqu'on souhaite y accéder ultérieurement pour récupérer les informations, il suffit de hacher
à nouveau le nom de la personne pour retrouver l'indice de la case du tableau où les données
sont stockées.
Il est recommandé de créer une fonction de recherche qui hachera la clé (le nom) et renverra
un pointeur vers les données recherchées.

114

Vous aimerez peut-être aussi