Les Listes Chaînées - TP
Les Listes Chaînées - TP
Les Listes Chaînées - TP
Il s'adresse à toute personne ayant suivi les cours de M@teo jusqu'aux pointeurs. Ce tutoriel
accompagné des exercices que je vous proposerai sont à mon avis un excellent entraînement, en
ce qui concerne les pointeurs, entre autres, mais fera appel à toutes vos connaissances du langage
C.
Et à la fin, que saurai-je faire ?
Le but de ce tutoriel est de vous initier aux listes chaînées, une autre façon d'implémenter un
conteneur, la plus courante étant les tableaux. A la fin de ce cours, vous serez capables de coder
votre propre bibliothèque permettant la création et la manipulation de listes simplement
chaînées. Les listes doublement chainées seront introduites pour terminer afin que vous puissiez
améliorer votre bibliothèque.
Je suis sûr que vous êtes prêts : nous allons donc commencer !
• Dans un tableau, la taille est connue, l'adresse du premier élément aussi. Lorsque vous
déclarez un tableau, la variable contiendra l'adresse du premier élément de votre tableau.
Comme le stockage est contigu, et la taille de chacun des éléments connue, il est possible
d'atteindre directement la case i d'un tableau.
• Dans une liste chaînée, la taille est inconnue au départ, la liste peut avoir autant
d'éléments que votre mémoire le permet.
Il est en revanche impossible d'accéder directement à l'élément i de la liste chainée.
Pour ce faire, il vous faudra traverser les i-1 éléments précédents de la liste.
• Pour déclarer une liste chaînée, il suffit de créer le pointeur qui va pointer sur le premier
élément de votre liste chaînée, aucune taille n'est donc à spécifier.
• Il est possible d'ajouter, de supprimer, d'intervertir des éléments d'une liste chaînée sans
avoir à recréer la liste en entier, mais en manipulant simplement leurs pointeurs.
Chaque élément d'une liste chaînée est composé de deux parties :
#include <stdlib.h>
struct element
int val;
};
#include <stdlib.h>
struct element
int val;
};
return 0;
Il est important de toujours initialiser la liste chaînée à NULL. Le cas échéant, elle sera considérée
comme contenant au moins un élément. C'est une erreur fréquente. A garder en mémoire donc. De
manière générale, il est plus sage de toujours initialiser vos pointeurs.
Manipuler les listes chainées (1/2)
Maintenant que nous savons comment déclarer une liste chaînée, il serait intéressant d'apprendre à
ajouter des éléments dans cette liste, ainsi que de lire ce qu'elle contient. C'est ce que nous allons
étudier dans cette première partie sur la manipulation des listes chaînées. Je vous invite à essayer par
vous-mêmes de programmer ces quelques fonctions basiques permettant de manipuler les listes. Dans
tous les cas (ou presque), nous renverrons la nouvelle liste, c'est-à-dire un pointeur sur element
contenant l'adresse du premier élément de la liste.
Ajouter un élément
Lorsque nous voulons ajouter un élément dans une liste chaînée, il faut savoir où l'insérer. Les deux
ajouts génériques des listes chaînées sont les ajouts en tête, et les ajouts en fin de liste. Nous allons
étudier ces deux moyens d'ajouter un élément à une liste.
Ajouter en tête
Lors d'un ajout en tête, nous allons créer un élément, lui assigner la valeur que l'on veut ajouter, puis
pour terminer, raccorder cet élément à la liste passée en paramètre. Lors d'un ajout en tête, on devra
donc assigner à nxt l'adresse du premier élément de la liste passé en paramètre. Visualisons tout ceci
sur un schéma :
nouvelElement->val = valeur;
nouvelElement->nxt = liste;
return nouvelElement;
}
C'est l'ajout le plus simple des deux. Il suffit de créer un nouvel élément puis de le relier au début de la
liste originale. Si l'original est , (vide) c'est NULL qui sera assigne au champ nxt du nouvel element. La
liste contiendra dans ce cas-là un seul élément.
nouvelElement->val = valeur;
nouvelElement->nxt = NULL;
if(liste == NULL)
return nouvelElement;
else
{
element* temp=liste;
while(temp->nxt != NULL)
temp = temp->nxt;
temp->nxt = nouvelElement;
return liste;
Comme vous pouvez le constater, nous nous déplaçons le long de la liste chaînée grâce au pointeur
temp. Si l'élément pointé par temp n'est pas le dernier (temp->nxt != NULL), on avance d'un cran (temp
= temp->nxt) en assignant à temp l'adresse de l'élément suivant. Une fois que l'on est au dernier
élément, il ne reste plus qu'à le relier au nouvel élément.
Si vous pensez avoir bien saisi ces deux fonctions, je vous invite à passer à la partie suivante, dans
laquelle je vais vous proposer quelques exercices. Le premier sera fondamental puisqu'il nous
permettra d'afficher le contenu d'une liste chaînée.
Exercices (1/2)
Exercice n°1
Vous allez maintenant pouvoir vous tester. Votre mission, si vous l'acceptez, est de coder la fonction
afficherListe. Vous devrez parcourir la liste jusqu'au bout et afficher toutes les valeurs qu'elle contient.
Voici son prototype :
Exercice n°2
Un deuxième exercice utilisant trois fonctions que nous avons vues jusqu'à présent :
• ajouterEnTete
• ajouterEnFin
• afficherListe
Vous devez écrire le main permettant de remplir et afficher la liste chaînée ci-dessous. Vous ne devrez
utiliser qu'une seule boucle for.
10 9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9 10
Aucune autre directive pour cet exercice qui nécessite peut-être un peu de logique, mais question
technique vous devriez être au point.
Une simple boucle suffit. Au début, la liste est vide. Vous ajoutez un premier élément égal à 1, puis un
deuxième 1. Après un premier passage, votre liste contient deux éléments 1. Au deuxième passage,
nous allons ajouter un élément 2 en tête, puis un élément 2 en fin pour obtenir 2 1 1 2. Il suffit alors de
répéter l'opération dix fois.
Exercice n°3
L'exercice suivant est le plus simple des trois, mais il me faut vous le montrer. Écrivez une fonction qui
renvoie 1 si la liste est vide, et 0 si elle contient au moins un élément. Son prototype est le suivant :
Vous vous demandez sûrement l'intérêt d'écrire une telle fonction... eh bien quand vous codez une
bibliothèque, le mieux est de "masquer" le fonctionnement interne de vos codes. L'utilisateur n'est pas
censé savoir qu'une liste vide sera égale à NULL, ni même que le type llist est un pointeur. Lui, il déclare
une llist sans savoir comment elle est créée, puis l'utilise (ajoute ou supprime des éléments, trie sa liste,
etc...) grâce aux fonctions de la bibliothèque. Dans certains cas, il lui faudra tester si la liste est vide, il
utilisera par exemple :
if(estVide(ma_liste))
else
afficherListe(ma_liste);
Si la liste est NULL, il ne contient aucun élément, elle est donc vide. Sinon, c'est qu'elle contient au
minimum un élément.
Nous voilà au bout de cette première série d'exercices. Dans la section suivante, nous allons voir plein
de fonctions plus complexes permettant de manipuler nos listes chaînées !
if(liste != NULL)
free(liste);
return aRenvoyer;
else
return NULL;
if(liste == NULL)
return NULL;
if(liste->nxt == NULL)
free(liste);
return NULL;
while(tmp->nxt != NULL)
ptmp = tmp;
tmp = tmp->nxt;
ptmp->nxt = NULL;
free(tmp);
return liste;
element *tmp=liste;
while(tmp != NULL)
if(tmp->val == valeur)
return tmp;
tmp = tmp->nxt;
return NULL;
int i = 0;
if(liste == NULL)
return 0;
/* Sinon, tant qu'il y a encore un élément ayant la val = valeur */
/* On incrémente */
liste = liste->nxt;
i++;
return i;
int i;
liste = liste->nxt;
if(liste == NULL)
return NULL;
else
#define ERREUR -1
if(liste == NULL)
return 0;
/* Sinon, il y a un élément (celui que l'on est en train de traiter)
return nombreElements(liste->nxt)+1;
if(liste == NULL)
return NULL;
if(liste->val == valeur)
free(liste);
pointant sur une liste qui ne contient plus aucun élément ayant la valeur recherché
e */
return tmp;
else
return liste;
Exercices (2/2)
Exercice n°1
Voilà... Maintenant, vous êtes des as en matière de liste chaînées, j'en suis sûr. Je vous propose donc
quelques exercices. Le premier sera de coder de manière itérative la fonction nombreElements dont je
vous rappelle le prototype :
C'est un exercice qui ne devrait pas vous poser de problèmes : bonne chance.
int nb = 0;
/* On parcours la liste */
while(tmp != NULL)
/* On incrémente */
nb++;
tmp = tmp->nxt;
return nb;
C'est aussi simple que ça. On parcourt simplement la liste tant que l'on n'est pas arrivé au bout, et on
incrémente le compteur nb que l'on renvoie pour finir.
Exercice n°2
Nous allons maintenant écrire une fonction permettant d'effacer complètement une liste chaînée de la
mémoire. Je vous propose d'écrire avec un algorithme itératif dans un premier temps, puis une
seconde fois grâce à un algorithme récursif. Dans le premier cas, vous devez parcourir la liste, stocker
l'élément suivant, effacer l'élément courant et avancer d'une case. A la fin la liste est vide, nous
retournerons NULL.
element* tmpnxt;
while(tmp != NULL)
tmpnxt = tmp->nxt;
free(tmp);
tmp = tmpnxt;
return NULL;
if(liste == NULL)
return NULL;
else
{
liste effacée */
element *tmp;
tmp = liste->nxt;
free(liste);
return effacerListe(tmp);
Et voilà : vous en savez maintenant un peu plus sur ce que sont les listes chaînées. Vous pourrez
améliorer ceci en utilisant les listes doublement chaînées, qui sont en fait une liste d'éléments qui
pointent à la fois sur l'élément suivant, mais aussi sur l'élément précédent, ce qui vous permet de
revenir en arrière plus facilement qu'avec les listes simplement chaînées. Vous pouvez compléter votre
bibliothèque avec des fonctions de tri, de recherche de minimum, de maximum et bien d'autre choses...
Passons maintenant à un petit QCM, afin de vérifier que vous avez tout saisi !
Eh bien il semblerait que vous êtes au point. Comme vous avez pu le constater, le choix d'utiliser ou non
les listes chaînées est fait lors de l'implémentation de votre algorithme dans un langage. Vous devrez
toujours faire des compromis.