Les structures de données statiques comme les tableaux peuvent limiter la flexibilité des applications.
Ces limitations entraînent des inefficacités lors des insertions et suppressions fréquentes.
Les listes chaînées offrent une alternative dynamique, permettant des modifications efficaces et flexibles des données.
Développez votre expertise en C et ouvrez la voie à des projets passionnants.
Introduction aux Listes Chaînées en C
Les listes chaînées sont des structures de données dynamiques qui permettent une gestion flexible des éléments. Contrairement aux tableaux qui ont une taille fixe, les listes chaînées peuvent croître et rétrécir dynamiquement en fonction des besoins de l’application. Elles sont particulièrement utiles lorsque vous devez effectuer des insertions et suppressions fréquentes, car elles offrent une performance supérieure à celle des tableaux pour ces opérations. Ce chapitre explore les concepts fondamentaux des listes chaînées, leur implémentation en C, et discute de leurs avantages et inconvénients par rapport à d’autres structures de données.
Définir les Listes Chaînées et leur Utilité
Liste Chaînée Simple :
Une liste chaînée simple est constituée de noeuds où chaque noeud contient une donnée et un pointeur vers le noeud suivant.
Chaque nœud contient deux éléments :
- Data :La donnée stockée.
- P_s1 :Un pointeur vers le nœud suivant.
Exemple du code
#include
#include // Pour malloc
struct s1 {
char *nom;
char *prenom;
int age;
struct s1 *p_s1;
} s;
int main() {
struct s1 *p_s = NULL;
p_s = (struct s1 *)malloc(sizeof(struct s1));
if (p_s == NULL) {
printf("Échec de l'allocation de mémoire.\n");
return 1;
}
// Initialisation des membres de la structure
p_s->nom = "Dupont";
p_s->prenom = "Jean";
p_s->age = 30;
p_s->p_s1 = NULL; // Pas de lien vers une autre structure
// Affichage des résultats
printf("Nom: %s\n", p_s->nom);
printf("Prénom: %s\n", p_s->prenom);
printf("Âge: %d\n", p_s->age);
// Libérer la mémoire allouée
free(p_s);
return 0;
}
- Exemple d’exécution de code sur Eclipse
Points à noter :
- L’allocation dynamique permet de créer une structure à l’exécution sans définir une taille fixe à la compilation.
- La bonne pratique est de libérer la mémoire allouée avec free pour éviter les fuites de mémoire.
Liste Chaînée Double :
Les listes chaînées doubles possèdent deux pointeurs dans chaque noeud : un vers le noeud suivant et un vers le noeud précédent.
Chaque nœud contient trois éléments :
- P_nexts1 :Un pointeur vers le nœud suivant.
- Data :La donnée stockée.
- P_prevs1 :Un pointeur vers le nœud précédent.
Exemple du code
#include
#include // Pour malloc
struct s1 {
char *nom;
char *prenom;
int age;
struct s1 *p_nexts1;
// Pointeur vers la structure suivante
struct s1 *p_prevs1;
// Pointeur vers la structure précédente
} s;
int main() {
struct s1 *p_s = NULL;
// Allocation de mémoire pour une structure s1
p_s = (struct s1 *)malloc(sizeof(struct s1));
if (p_s == NULL) {
printf("Échec de l'allocation de mémoire.\n");
return 1;
}
// Initialisation des membres de la structure
p_s->nom = "Martin";
p_s->prenom = "Luc";
p_s->age = 45;
p_s->p_nexts1 = NULL;
// Pas de structure suivante pour l'instant
p_s->p_prevs1 = NULL;
// Pas de structure précédente pour l'instant
// Affichage des résultats
printf("Nom: %s\n", p_s->nom);
printf("Prénom: %s\n", p_s->prenom);
printf("Âge: %d\n", p_s->age);
printf("Pointeur vers la structure suivante: %p\n", (void *)p_s->p_nexts1);
printf("Pointeur vers la structure précédente: %p\n", (void *)p_s->p_prevs1);
// Libération de la mémoire allouée
free(p_s);
return 0;
}
- Exemple d’exécution de code sur Eclipse
Points à noter :
- (0000000) ou NULL est affiché pour les pointeurs non initialisés (ici p_nexts1 et p_prevs1).
- Comme précédemment, la mémoire allouée est libérée pour éviter les fuites de mémoire.
- Le code met en place une base pour gérer une liste chaînée double, mais ici, il ne s’agit que d’une seule structure.
Structures de Données : Comparatif
Les tableaux et les listes chaînées sont deux structures de données fondamentales avec des caractéristiques distinctes.
- Le tableau suivant présente la Comparaison entre un tableaux et une liste des chaînées :
Critère | Tableau | Liste Chaînée Simple |
---|---|---|
Taille | Fixe | Dynamique |
Accès aux éléments | Accès direct par indice | Accès séquentiel |
Insertion/Suppression | Coûteux (déplacement) | Efficace (modification des pointeurs) |
Allocation mémoire | Continue (contiguë) | Fragmentée (non contiguë) |
- Le tableau suivant présente la Comparaison entre une liste des chaînée simples et double :
Critère | Liste Chaînée Simple | Liste Chaînée Double |
---|---|---|
Pointeurs | 1 (vers le suivant) | 2 (vers le suivant et le précédent) |
Parcours | Unidirectionnel | Bidirectionnel |
Complexité | Moins complexe | Plus complexe (gestion des deux pointeurs) |
Opérations sur Listes Chaînées en C
Les listes chaînées permettent d’effectuer plusieurs opérations courantes telles que l’insertion d’un élément au début, à la fin, ou au milieu de la liste, ainsi que la suppression d’un élément. Chacune de ces opérations présente des caractéristiques spécifiques et une complexité en termes de gestion des pointeurs.
Insertion au Début :
L’insertion au début d’une liste chaînée consiste à ajouter un nouvel élément juste avant le premier noeud existant. Cette opération est très rapide car elle ne nécessite pas de parcourir toute la liste.
Étapes :
- Créer un nouveau noeud.
- Assigner la donnée au nouveau noeud.
- Faire pointer le Next du nouveau noeud vers l’ancien premier noeud (l’ancien début de liste).
- Mettre à jour le pointeur head (tête de liste) pour qu’il pointe vers ce nouveau noeud.
Complexité : O(1) – La complexité est constante car l’opération ne nécessite qu’une modification locale des pointeurs.
Fonction d’insertion au Début d’une Liste Chaînée Simple :
void insertAtHead(Node **head, int newData) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = newData;
newNode->next = *head;
*head = newNode;
}
- Exemple d’exécution de code sur Eclipse
Explication des résultats :
- Avant la première insertion de 10, la liste est vide, donc le message « La liste est vide. » est affiché.
- Après l’insertion de 10, la liste contient un seul élément :10 -> NULL.
- Avant l’insertion de 20 , la liste contient 10 -> NULL, et après l’insertion , elle devient 20 -> 10 -> NULL.
- Avant l’insertion de 30 , la liste contient 20 -> 10 -> NULL, et après l’insertion , elle devient 30 -> 20 -> 10 -> NULL.
Ainsi, chaque insertion au début de la liste met à jour l’état de la liste en ajoutant un nouvel élément en tête.
Insertion à la Fin :
L’insertion à la fin de la liste chaînée consiste à ajouter un nouvel élément après le dernier noeud. Contrairement à l’insertion au début, elle nécessite de parcourir toute la liste pour atteindre le dernier élément.
Étapes :
- Créer un nouveau noeud.
- Assigner la donnée au nouveau noeud.
- Parcourir la liste jusqu’à trouver le dernier noeud (celui dont le next est NULL).
- Faire pointer le next du dernier noeud vers le nouveau noeud.
- Le next du nouveau noeud doit pointer vers NULL (indiquant la fin de la liste).
Complexité : O(n) – La complexité est linéaire car il faut parcourir la liste jusqu’à la fin.
- Fonction d’insertion à la Fin d’une Liste Chaînée Simple
void insertAtEnd(Node **head, int newData) {
Node *newNode = (Node *)malloc(sizeof(Node));
Node *last = *head;
newNode->data = newData;
newNode->next = NULL;
if (*head == NULL) {
*head = newNode;
return;
}
while (last->next != NULL) {
last = last->next;
}
last->next = newNode;
}
- Exemple d’exécution de code sur Eclipse
Explication des résultats :
- Avant la première insertion de 10, la liste est vide, donc le message « La liste est vide. » est affiché.
- Après l’insertion de 10, la liste contient un seul élément :10 -> NULL.
- Avant l’insertion de 20 , la liste contient 10 -> NULL, et après l’insertion , elle devient 10 -> 20 -> NULL.
- Avant l’insertion de 30 , la liste contient 10 -> 20 -> NULL, et après l’insertion , elle devient 10 -> 20 -> 30 -> NULL.
Insertion au Milieu :
L’insertion au milieu d’une liste chaînée consiste à insérer un nouveau noeud après un noeud donné, c’est-à-dire à une position spécifique dans la liste. Cela nécessite généralement d’abord de localiser le noeud après lequel l’insertion doit avoir lieu.
Étapes :
- Créer un nouveau noeud.
- Assigner la donnée au nouveau noeud.
- Parcourir la liste pour atteindre le noeud après lequel l’insertion doit avoir lieu.
- Faire pointer le next du nouveau noeud vers le noeud suivant celui où l’insertion se produit.
- Faire pointer le next du noeud actuel (celui où l’insertion a lieu) vers le nouveau noeud.
Complexité : O(n) – La complexité est linéaire car il faut parcourir la liste jusqu’à l’emplacement souhaité.
Fonction d’insertion au Milieu d’une Liste Chaînée Simple
void insertAfter(Node *prevNode, int newData) {
if (prevNode == NULL)
return;
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = newData;
newNode->next = prevNode->next;
prevNode->next = newNode;
}
- Exemple d’exécution de code sur Eclipse
Explication des résultats :
- Initialement , la liste est 10 -> 20 -> 30 -> NULL.
- Avant l’insertion de 15 après le nœud 10, la liste est encore 10 -> 20 -> 30 -> NULL.
- Après l’insertion de 15 après le nœud 10, la liste devient 10 -> 15 -> 20 -> 30 -> NULL.
Suppression :
La suppression d’un noeud implique de supprimer un élément de la liste et de réajuster les pointeurs pour ne pas briser la chaîne de noeuds. Selon la position du noeud à supprimer (début, milieu ou fin), l’opération diffère légèrement.
Étapes :
- Identifier le noeud à supprimer.
- Si le noeud est le premier, mettre à jour le pointeur head pour qu’il pointe vers le noeud suivant.
- Si le noeud est au milieu ou à la fin, parcourir la liste pour trouver le noeud précédent celui à supprimer.
- Faire pointer le next du noeud précédent vers le noeud suivant celui à supprimer.
- Libérer la mémoire du noeud supprimé.
Complexité : O(n) – La complexité est linéaire car il faut parcourir la liste pour localiser le noeud à supprimer.
Code de Suppression d’un Noeud dans une Liste Chaînée Simple
void deleteNode(Node **head, int key) {
Node *temp = *head, *prev = NULL;
if (temp != NULL && temp->data == key) {
*head = temp->next;
free(temp);
return;
}
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}
if (temp == NULL)
return;
prev->next = temp->next;
free(temp);
}
- Exemple d’exécution de code sur Eclipse
Explication des résultats :
- Initialement , la liste est 10 -> 20 -> 30 -> 40 -> NULL.
- Avant la suppression de 20 , la liste est 10 -> 20 -> 30 -> 40 -> NULL. Après suppression, elle devient 10 -> 30 -> 40 -> NULL.
- Lorsqu’on essaie de supprimer un nœud avec la clé 50 , qui n’existe pas, un message « Le nœud avec la clé 50 n’a pas été trouvé. » est affiché.
Démonstration Pratique
Voici un code qui effectue à la fois l’insertion et la suppression dans une liste chaînée. L’idée est d’insérer un nouveau nœud dans la liste et de supprimer simultanément un nœud avec une clé spécifique, si ce dernier existe.
#include
#include
// Définition de la structure Node
typedef struct Node {
int data;
struct Node *next;
} Node;
void printList(Node *node) {
if (node == NULL) {
printf("La liste est vide.\n");
return;
}
while (node != NULL) {
printf("%d -> ", node->data);
node = node->next;
}
printf("NULL\n");
}
// Fonction pour insérer un nouveau nœud au début de la liste
void insertAtHead(Node **head, int newData) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = newData;
newNode->next = *head;
*head = newNode;
}
// Fonction pour supprimer un nœud ayant une clé spécifique
void deleteNode(Node **head, int key) {
Node *temp = *head, *prev = NULL;
// Si le nœud à supprimer est le premier nœud
if (temp != NULL && temp->data == key) {
*head = temp->next;
// Change le pointeur head
free(temp);
// Libère la mémoire
return;
}
// Recherche du nœud à supprimer
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}
// Si le nœud n'a pas été trouvé dans la liste
if (temp == NULL)
return;
// Détache le nœud de la liste
prev->next = temp->next;
// Libère la mémoire du nœud supprimé
free(temp);
}
// Fonction pour effectuer l'insertion et la suppression en même temps
void insertAndDelete(Node **head, int insertData, int deleteKey) {
// Insertion d'un nouveau nœud au début de la liste
insertAtHead(head, insertData);
// Affichage de la liste après l'insertion
printf("Liste chaînée après insertion de %d :\n", insertData);
printList(*head);
// Suppression du nœud avec la clé deleteKey
deleteNode(head, deleteKey);
// Affichage de la liste après suppression
printf("Liste chaînée après suppression de %d :\n", deleteKey);
printList(*head);
}
// Fonction pour afficher les éléments de la liste
int main() {
// Déclaration d'un pointeur vers la tête de la liste
Node *head = NULL;
// Création de quelques nœuds manuellement
insertAtHead(&head, 30);
insertAtHead(&head, 20);
insertAtHead(&head, 10);
// Affichage initial de la liste
printf("Liste chaînée initiale :\n");
printList(head);
// Insertion et suppression simultanées
insertAndDelete(&head, 40, 20);
// Insertion de 40, suppression de 20
return 0;
}
Explication :
- Fonction insertAtHead :
Insère un nouveau nœud au début de la liste chaînée.
- Fonction deleteNode :
Supprime un nœud ayant une clé spécifique. Si le nœud à supprimer est le premier de la liste, il est détaché et libéré. Sinon, la fonction cherche dans la liste et détache le nœud trouvé avant de libérer la mémoire.
- Fonction insertAndDelete :
Combine à la fois l’insertion d’un nouveau nœud et la suppression d’un autre nœud avec une clé donnée. Après chaque opération (insertion et suppression), la fonction affiche l’état de la liste chaînée.
- Affichage :
La fonction printList est utilisée pour afficher l’état de la liste avant et après chaque opération.
Exemple d’exécution de code sur Eclipse
Conclusion sur les Listes Chaînées en C
Les listes chaînées sont des structures de données puissantes offrant une grande flexibilité pour gérer des collections dynamiques d’éléments. Bien qu’elles présentent certains inconvénients, comme un accès plus lent aux éléments, elles sont très efficaces pour les opérations d’insertion et de suppression. Comprendre leur fonctionnement et leur implémentation en C vous permettra de choisir la meilleure structure de données pour vos besoins en programmation.
Formez-vous gratuitement avec Alphorm !
Maîtrisez les compétences clés en IT grâce à nos formations gratuites et accélérez votre carrière dès aujourd'hui.
FAQ
Qu'est-ce qu'une liste chaînée simple ?
Comment fonctionne une liste chaînée double ?
Quels sont les avantages des listes chaînées par rapport aux tableaux ?
Comment insérer un élément dans une liste chaînée en C ?
Comment supprimer un élément d'une liste chaînée en C ?
Conclusion
Les listes chaînées offrent une flexibilité inégalée pour la gestion dynamique des données. Quelles autres structures de données pourraient compléter vos besoins en programmation?