Vous êtes sur la page 1sur 30

Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Université Sétif 1
Faculté des Sciences
Département d’informatique
Filière : Licence Académique
Module : Algorithmique et structure de données
Année universitaire : 2016-2017

CH2 - Les listes chaînées

Type structuré

Il est souvent pratique de regrouper logiquement plusieurs variables en une seule variable composée. On
parle alors de structure ou d’enregistrement.
Un type enregistrement ou type structuré est un type T de variable v obtenu en juxtaposant plusieurs
variables v1, v2, . . . ayant chacune un type T1,T2, . . .
 les différentes variables vi sont appelées champs de v
 elles sont repérées par un identificateur de champ
 si v est une variable de type structuré T possédant le champ ch, alors la variable v .ch est une variable
comme les autres :
(type, adresse, valeur)

Les types structurés en algorithmique


Syntaxe :
Déclaration de type structuré :
nom_de_type = structure:
nom_du_champ_1 : type_du_champ_1;
nom_du_champ_2 : type_du_champ_2;
...
Déclaration de variable structurée :
v: nom_de_type;

Les types structurés en C


Syntaxe :
Déclaration de type structuré :
struct nom_de_struct {
nom_du_champ_1 : type_du_champ_1;
nom_du_champ_2 : type_du_champ_2;
...
};
Déclaration de variable structurée :
struct nom_de_struct v;

Ou avec une définition de type :


typedef struct nom_de_struct nom_type;
nom_type v;

Exemple de déclaration de type structuré


struct s_date {
char nom_jour[9]; // lundi, mardi, ..., dimanche
int num_jour; // 1, 2, ..., 31
1
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

int mois; // 1, 2, ..., 12


int annee;
}
struct s_date date = {"vendredi", 21, 10, 2011};

Excercice :
Ecrire l’algorithme de l’exemple précédent

Corrigé :
S_date = structure:
Jour[9] : chaine_car;
Num_jour : entier;
Mois : entier ;
Année : entier ;

Déclaration de variable structurée :


date: S_date;

Chaînages dynamiques
Listes Dynamiques simplement chaînées (LDSC)
le pointeur p repère la LDSC

qui implante la suite (a1, a2, a3)


Dans le schéma, trois types sont à distinguer :
 le type des éléments de la suite :

 le type des pointeurs :

 le type des éléments de la liste dynamique,

composition des deux types précédents.

Déclaration des types element(cellule/maillon) et liste

Soit type_element le type des éléments de la suite.

Une liste chaînée est obtenue à partir du type element/cellule définie par
element = structure: // element ou cellule ou maillon
valeur : type_element // valeur ou val
suivant : pointeur : element // suivant ou next // pointeur : element ou ^element
liste = pointeur : element // ou ^element

En C :
struct element {
type_element val;
struct element * next;
};
typedef struct element elem; // element ou Cellule
typedef struct element *list; // Liste Chaînée

2
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Généralités sur les listes chainées

Lorsque vous créez un algorithme utilisant des conteneurs, il existe différentes manières de les implémenter,
la façon la plus courante étant les tableaux, que vous connaissez tous. Lorsque vous créez un tableau, les
éléments de celui-ci sont placés de façon contiguë en mémoire. Pour pouvoir le créer, il vous faut connaître
sa taille. Si vous voulez supprimer un élément au milieu du tableau, il vous faut recopier les éléments
temporairement, ré-allouer de la mémoire pour le tableau, puis le remplir à partir de l'élément supprimé. En
bref, ce sont beaucoup de manipulations coûteuses en ressources.

Une liste chaînée est différente dans le sens où les éléments de votre liste sont répartis dans la mémoire et
reliés entre eux par des pointeurs. Vous pouvez ajouter et enlever des éléments d'une liste chaînée à
n'importe quel endroit, à n'importe quel instant, sans devoir recréer la liste entière.

Qu'est-ce qu'une liste chaînée ?


C'est un système informatique qui permet la sauvegarde dynamique de données en mémoire tout comme des
variables ou tableaux mais sans se préoccuper de leur nombre et en rendant leur allocation plus transparente.
On dit liste chaînée car les données sont chaînées les unes avec les autres. On accède aux données à l'aide
d'un ou deux points d'entrées qui se situent la plus part du temps aux extrémités de la liste. Dans la pratique
ces points d'entrées seront des pointeurs soit sur le premier ou le dernier élément de la liste voir sur les deux
ou même mobile.

Les éléments de la liste sont chaînés entre eux à l'aide de pointeurs sur leurs éléments suivants ou précédents
voir sur les deux. Ces pointeurs doivent donc faire partie de l'élément. Ce qui nous orientera vers l'utilisation
d'une structure du langage C (struct). Cette structure aura donc la particularité d'avoir au moins un pointeur
sur des variables du même type qu'elle. Ce pointeur servira à relier les éléments de la liste entre eux. La
structure contiendra bien sûr aussi les données que l'on veut mémoriser.

Nous allons essayer de voir ceci plus en détail sur ces schémas :

Vous avez sur ce schéma la représentation que l'on pourrait faire d'un tableau et d'une liste chaînée. Chacune
de ces représentations possède ses avantages et inconvénients. C'est lors de l'écriture de votre programme
que vous devez vous poser la question de savoir laquelle des deux méthodes est la plus intéressante.

 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.
 Pour déclarer un tableau, il faut connaître sa taille.
3
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

 Pour supprimer ou ajouter un élément à un tableau, il faut créer un nouveau tableau et supprimer
l'ancien. Ce n'est en général pas visible par l'utilisateur, mais c'est ce que realloc va souvent faire.
L'adresse du premier élément d'un tableau peut changer après un realloc, ce qui est tout à fait logique
puisque realloc n'aura pas forcement la possibilité de trouver en mémoire la place nécessaire et
contiguë pour allouer votre nouveau tableau. realloc va donc chercher une place suffisante, recopier
votre tableau, et supprimer l'ancien.

 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'un 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 :

Figure 1 : représentation d'un élément d'une liste simplement chaînée.

 la valeur que vous voulez stocker,


 l'adresse de l'élément suivant, s'il existe.
S'il n'y a plus d'élément suivant, alors l'adresse sera NULL, et désignera le bout de la chaîne.

Voilà deux schémas pour expliquer comment se passent l'ajout et la suppression d'un élément d'une liste
chaînée. Remarquez le symbole en bout de chaîne qui signifie que l'adresse de l'élément suivant ne pointe
sur rien, c'est-à-dire sur NULL.

4
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Comme vous vous en doutez certainement maintenant, la liste chaînée est un type structuré. Nous en avons
terminé avec ces quelques généralités, nous allons pouvoir passer à la définition d'une structure de données
nous permettant de créer cette fameuse liste !

Un élément de la liste
Pour nos exemples, nous allons créer une liste chaînée de nombres entiers. Chaque élément de la liste aura la
forme de la structure suivante :
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.
typedef struct Element Element;
struct Element
{
int nombre;
Element *suivant;
};

Nous avons créé ici un élément d'une liste chaînée, correspondant à la fig. suivante que nous avons vue plus
tôt. Que contient cette structure ?
 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.
Comme je vous le disais plus tôt, 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.
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.
La structure de contrôle
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 lListe;
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.
5
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

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 (fig. suivante).

Le dernier élément de la liste


Notre schéma est presque complet. Il manque une dernière chose : on aimerait retenir le dernier élément de
la liste. En effet, il faudra bien arrêter de parcourir la liste à un moment donné. Avec quoi pourrait-on
signifier à notre programme « Stop, ceci est le dernier élément » ?
Il serait possible d'ajouter dans la structure Liste un pointeur vers le dernier Element. Toutefois, il y a
encore plus simple : il suffit de faire pointer le dernier élément de la liste vers NULL, c'est-à-dire de mettre
son pointeur suivant à NULL. Cela nous permet de réaliser un schéma enfin complet de notre structure de
liste chaînée (fig. suivante).

Initialiser la liste
La fonction d'initialisation est la toute première que l'on doit appeler. Elle crée la structure de contrôle et le
premier élément de la liste.
Je vous propose la fonction ci-dessous, que nous commenterons juste après, bien entendu :
Liste *initialisation()
{
Liste *liste = malloc(sizeof(*liste));
Element *element = malloc(sizeof(*element));

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


6
Module : Algorithmique et structure de données - Année universitaire : 2015-2016
{
exit(EXIT_FAILURE);
}

element->nombre = 0;
element->suivant = NULL;
liste->premier = element;

return liste;
}

On commence par créer la structure de contrôle liste.


Notez que le type de données est Liste et que la variable s'appelle liste. La majuscule permet de les
différencier.
On alloue 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 au
stockage de la structure Liste.
On aurait aussi pu écrire sizeof(Liste), mais si plus tard on décide de modifier le type du pointeur
liste, on devra aussi adapter le sizeof.
On alloue ensuite de la même manière la mémoire nécessaire au stockage du premier élément. On vérifie si
les allocations dynamiques ont fonctionné. En cas d'erreur, on arrête immédiatement le programme en
faisant appel à exit().
Si tout s'est bien passé, on définit les valeurs de notre premier élément :
 la donnée nombre est mise à 0 par défaut ;
 le pointeur suivant pointe vers NULL car le premier élément de notre liste est aussi le dernier pour
le moment. Comme on l'a vu plus tôt, le dernier élément doit pointer vers NULL pour signaler qu'il
est en fin de liste.
Nous avons donc maintenant réussi à créer en mémoire une liste composée d'un seul élément et ayant une
forme semblable à la fig. suivante :

Excercice
Ecrire l’algorithme équivalent du programme précédent

Déclaration en C d'une liste chainée

7
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Vous pouvez créer des listes chaînées de n'importe quel type d'éléments : entiers, caractères, structures,
tableaux, voir même d'autres listes chaînées... Il vous est même possible de combiner plusieurs types dans
une même liste.
Voici la déclaration d'une liste simplement chaînée d'entiers:

Code : C
1 #include <stdlib.h>
2
3 typedef struct element element;
4 struct element
5 {
6 int val;
7 struct element *nxt;
8 };
9
10 typedef element* llist;

Exercice : donner l’algorithmique équivalent au programme précédent.

Element = structure
Val : entier
nxt : pointeur :element
llist : pointeur : Element

On créer le type element qui est une structure contenant un entier (val) et un pointeur sur élément (nxt), qui
contiendra l'adresse de l'élément suivant. Ensuite, il nous faut créer le type llist (pour linked list = liste
chaînée) qui est en fait un pointeur sur le type element. Lorsque nous allons déclarer la liste chaînée, nous
devrons déclarer un pointeur sur element, l'initialiser à NULL, pour pouvoir ensuite allouer le premier
élément.
En C N'oubliez pas d'inclure stdlib.h afin de pouvoir utiliser la macro NULL. Comme vous allez le
constater, nous avons juste crée le type llist afin de simplifier la déclaration.

Voilà comment déclarer une liste chaînée (vide pour l'instant):

Code : C
1 #include <stdlib.h>
2
3 typedef struct element element;
4 struct element
5 {
6 int val;
7 struct element *nxt;
8 };
9
10 typedef element* llist;
11
12
13
14 int main(int argc, char **argv)
15 {
16 /* Déclarons 3 listes chaînées de façons différentes mais équivalentes */
17 llist ma_liste1 = NULL;
18 element *ma_liste2 = NULL;
19 struct element *ma_liste3 = NULL;
20
21

8
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

22 return 0;
23 }

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

Ici, les choses se compliquent un peu. Où va-t-on ajouter un nouvel élément ? Au début de la liste, à la fin,
au milieu ?
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 :

Code : C
1 llist ajouterEnTete(llist liste, int valeur)
2{
3 /* On crée un nouvel élément */
4 element* nouvelElement = malloc(sizeof(element));
5
6 /* On assigne la valeur au nouvel élément */

9
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

7 nouvelElement->val = valeur;
8
9 /* On assigne l'adresse de l'élément suivant au nouvel élément */
10 nouvelElement->nxt = liste;
11
12 /* On retourne la nouvelle liste, i.e. le pointeur sur le premier élément */
13 return nouvelElement;
14 }

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.

Explication en détail :

Nous devons créer une fonction capable d'insérer un nouvel élément en début de liste. Pour nous mettre en
situation, imaginons un cas semblable à la fig. suivante : la liste est composée de trois éléments et on
souhaite 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. Je vous propose pour cela ce code source que nous
analyserons juste après :
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;
}
10
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

La fonction insertion() prend en paramètre l'élément de contrôle liste (qui contient l'adresse du
premier élément) et le nombre à stocker dans le nouvel élément que l'on va créer.
Dans un premier temps, on alloue l'espace nécessaire au stockage du nouvel élément et on y place le
nouveau nombre nvNombre. Il reste alors une étape délicate : l'insertion du nouvel élément dans la liste
chaînée.
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 :
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.
On ne peut pas suivre ces étapes dans l'ordre inverse ! En effet, si vous faites d'abord pointer premier
vers notre nouvel élément, vous perdez l'adresse du premier élément de la liste ! Faites le test, vous
comprendrez de suite pourquoi l'inverse est impossible.
Cela aura pour effet d'insérer correctement notre nouvel élément dans la liste chaînée (fig. suivante) !

Excercice : ecrire l’algorithme équivalent au programme précédent

Ajouter en fin de liste

Cette fois-ci, c'est un peu plus compliqué. Il nous faut :

 tout d'abord créer un nouvel élément,


 lui assigner sa valeur,
 et mettre l'adresse de l'élément suivant à NULL.

En effet,, comme cet élément va terminer la liste nous devons signaler qu'il n'y a plus d'élément suivant.
Ensuite, il faut faire pointer le dernier élément de liste originale sur le nouvel élément que nous venons de
créer.

Pour ce faire, il faut créer un pointeur temporaire sur element qui va se déplacer d'élément en élément, et
regarder si cet élément est le dernier de la liste. Un élément sera forcément le dernier de la liste si NULL est
assigné à son champ nxt.

11
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Code : C
1 llist ajouterEnFin(llist liste, int valeur)
2{
3 /* On crée un nouvel élément */
4 element* nouvelElement = malloc(sizeof(element));
5
6 /* On assigne la valeur au nouvel élément */
7 nouvelElement->val = valeur;
8
9 /* On ajoute en fin, donc aucun élément ne va suivre */
10 nouvelElement->nxt = NULL;
11
12 if(liste == NULL)
13 {
14 /* Si la liste est videé il suffit de renvoyer l'élément créé */
15 return nouvelElement;
16 }
17 else
18 {
19 /* Sinon, on parcourt la liste à l'aide d'un pointeur temporaire et on
20 indique que le dernier élément de la liste est relié au nouvel élément */
21 element* temp=liste;
22 while(temp->nxt != NULL)
23 {
24 temp = temp->nxt;
25 }
26 temp->nxt = nouvelElement;
27 return liste;
28 }
29 }

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.

12
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Exercices (1/2)

Exercice n°1
Vous allez maintenant pouvoir vous tester. Ecrivez le code (programme en C/C++) la fonction
afficherListe. Vous devrez parcourir la liste jusqu'au bout et afficher toutes les valeurs qu'elle contient. Voici
son prototype :

Code : C
1 void afficherListe(llist liste);

Correction
Code : C
1 void afficherListe(llist liste)
2{
3 element *tmp = liste;
4 /* Tant que l'on n'est pas au bout de la liste */
5 while(tmp != NULL)
6 {
7 /* On affiche */
8 printf("%d ", tmp->val);
9 /* On avance d'une case */
10 tmp = tmp->nxt;
11 }
12 }

La seule chose à faire est de se déplacer le long de la liste chaînée grâce au pointeur tmp. Si ce pointeur tmp
pointe sur NULL, c'est que l'on a atteint le bout de la chaîne, sinon c'est que nous sommes sur un élément
dont il faut afficher la valeur.

Algorithme équivalent :

Exercice n°2
Un deuxième exercice utilisant trois fonctions que nous avons vues jusqu'à présent :

 ajouterEnTete
 ajouterEnFin
 afficherListe

Vous devez écrire la fonction main permettant de remplir et afficher la liste chaînée ci-dessous. Vous ne
devrez utiliser qu'une seule boucle for.

Code : Console
10 9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9 10

Correction
Code : C
1 int main(int argc, char **argv)
2{
3 llist ma_liste = NULL;
4 int i;
5
6 for(i=1;i<=10;i++)
7 {
8 ma_liste = ajouterEnTete(ma_liste, i);
13
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

9 ma_liste = ajouterEnFin(ma_liste, i);


10 }
11 afficherListe(ma_liste);
12
13 supprimerListe(ma_liste); // Libère les ressources, nous verrons cette fonction
14 plus tard.
15
16 return 0;
}

Exercice n°3
É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:
Code : C
1 int estVide(llist liste);

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:

Code : C
1 if(estVide(ma_liste))
2 {
3 printf("La liste est vide");
4 }
5 else
6 {
7 afficherListe(ma_liste);
8 }

Correction
Code : C
1 int estVide(lliste liste)
2{
3 if(liste == NULL)
4 {
5 return 1;
6 }
7 else
8 {
9 return 0;
10 }
11 }

Ou bien, en condensé :
Code : C
1 int estVide(llist liste)
2{
3 return (liste == NULL)? 1 : 0;
4}

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.
14
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Dans la section suivante, nous allons voir plein de fonctions plus complexes permettant de manipuler nos
listes chaînées !

15
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Manipuler les listes chainées (2/2)


Nous allons voir tout un tas de fonctions, pour supprimer des éléments, rechercher un élément...

Supprimer un élément en tête

Il s'agit là de supprimer le premier élément de la liste. Pour ce faire, il nous faudra utiliser la fonction free
que vous connaissez certainement. Si la liste n'est pas vide, on stocke l'adresse du premier élément de la liste
après suppression (i.e. l'adresse du 2ème élément de la liste originale), on supprime le premier élément, et on
renvoie la nouvelle liste. Attention quand même à ne pas libérer le premier élément avant d'avoir stocké
l'adresse du second, sans quoi il sera impossible de la récupérer.

Code : C
1 llist supprimerElementEnTete(llist liste)
2{
3 if(liste != NULL)
4 {
5 /* Si la liste est non vide, on se prépare à renvoyer l'adresse de
6 l'élément en 2ème position */
7 element* aRenvoyer = liste->nxt;
8 /* On libère le premier élément */
9 free(liste);
10 /* On retourne le nouveau début de la liste */
11 return aRenvoyer;
12 }
13 else
14 {
15 return NULL;
16 }
17 }

Explications en détail :

De même que pour l'insertion, nous allons ici nous concentrer sur la suppression du premier élément de la
liste. Il est techniquement possible de supprimer un élément précis au milieu de la liste, ce sera d'ailleurs un
des exercices que je vous proposerai à la fin.
La suppression ne pose pas de difficulté supplémentaire. Il faut cependant bien adapter les pointeurs de la
liste dans le bon ordre pour ne « perdre » aucune information.
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);
}
}

On commence par vérifier que le pointeur qu'on nous envoie n'est pas NULL, sinon on ne peut pas travailler.
On vérifie ensuite qu'il y a au moins un élément dans la liste, sinon il n'y a rien à faire.
16
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Ces vérifications effectuées, on peut sauvegarder l'adresse de l'élément à supprimer dans un pointeur
aSupprimer. On adapte ensuite le pointeur premier vers le nouveau premier élément, qui est
actuellement en seconde position de la liste chaînée.
Il ne reste plus qu'à supprimer l'élément correspondant à notre pointeur aSupprimer avec un free (fig.
suivante).

Cette fonction est courte mais sauriez-vous la réécrire ? Il faut bien comprendre qu'on doit faire les choses dans un ordre précis :

1. faire pointer premier vers le second élément ;


2. supprimer le premier élément avec un free.
Si on faisait l'inverse, on perdrait l'adresse du second élément !

Supprimer un élément en fin de liste

Cette fois-ci, il va falloir parcourir la liste jusqu'à son dernier élément, indiquer que l'avant-dernier élément
va devenir le dernier de la liste et libérer le dernier élément pour enfin retourner le pointeur sur le premier
élément de la liste d'origine.

Code : C
1 llist supprimerElementEnFin(llist liste)
2{
3 /* Si la liste est vide, on retourne NULL */
4 if(liste == NULL)
5 return NULL;
6
7 /* Si la liste contient un seul élément */
8 if(liste->nxt == NULL)
9 {
10 /* On le libère et on retourne NULL (la liste est maintenant vide) */
11 free(liste);
12 return NULL;
13 }
14
15 /* Si la liste contient au moins deux éléments */
16 element* tmp = liste;
17 element* ptmp = liste;
18 /* Tant qu'on n'est pas au dernier élément */
19 while(tmp->nxt != NULL)
20 {
21 /* ptmp stock l'adresse de tmp */
22 ptmp = tmp;
23 /* On déplace tmp (mais ptmp garde l'ancienne valeur de tmp */
24 tmp = tmp->nxt;
25 }

17
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

26 /* A la sortie de la boucle, tmp pointe sur le dernier élément, et ptmp sur


27 l'avant-dernier. On indique que l'avant-dernier devient la fin de la liste
28 et on supprime le dernier élément */
29 ptmp->nxt = NULL;
30 free(tmp);
31 return liste;
32 }

Rechercher un élément dans une liste

Le but du jeu cette fois est de renvoyer l'adresse du premier élément trouvé ayant une certaine valeur. Si
aucun élément n'est trouvé, on renverra NULL. L'intérêt est de pouvoir, une fois le premier élément trouvé,
chercher la prochaine occurrence en recherchant à partir de elementTrouve->nxt. On parcourt donc la liste
jusqu'au bout, et dès qu'on trouve un élément qui correspond à ce que l'on recherche, on renvoie son adresse.

Code : C
1 llist rechercherElement(llist liste, int valeur)
2{
3 element *tmp=liste;
4 /* Tant que l'on n'est pas au bout de la liste */
5 while(tmp != NULL)
6 {
7 if(tmp->val == valeur)
8 {
9 /* Si l'élément a la valeur recherchée, on renvoie son adresse */
10 return tmp;
11 }
12 tmp = tmp->nxt;
13 }
14 return NULL;
15 }

Compter le nombre d'occurrences d'une valeur

Pour ce faire, nous allons utiliser la fonction précédente permettant de rechercher un élément. On cherche
une première occurrence : si on la trouve, alors on continue la recherche à partir de l'élément suivant, et ce
tant qu'il reste des occurrences de la valeur recherchée. Il est aussi possible d'écrire cette fonction sans
utiliser la précédente bien entendu, en parcourant l'ensemble de la liste avec un compteur que l'on
incrémente à chaque fois que l'on passe sur un élément ayant la valeur recherchée. Cette fonction n'est pas
beaucoup plus compliquée, mais il est intéressant d'un point de vue algorithmique de réutiliser des fonctions
pour simplifier nos codes.

Code : C
1 int nombreOccurences(llist liste, int valeur)
2{
3 int i = 0;
4
5 /* Si la liste est vide, on renvoie 0 */
6 if(liste == NULL)
7 return 0;
8
9 /* Sinon, tant qu'il y a encore un élément ayant la val = valeur */
10 while((liste = rechercherElement(liste, valeur)) != NULL)
11 {
12 /* On incrémente */
13 liste = liste->nxt;
14 i++;
18
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

15 }
16 /* Et on retourne le nombre d'occurrences */
17 return i;
18 }

Recherche du i-ème élément

Pour le coup, c'est une fonction relativement simple. Il suffit de se déplacer i fois à l'aide du pointeur tmp le
long de la liste chaînée et de renvoyer l'élément à l'indice i. Si la liste contient moins de i élément(s), alors
nous renverrons NULL.

Code : C
1 llist element_i(llist liste, int indice)
2{
3 int i;
4 /* On se déplace de i cases, tant que c'est possible */
5 for(i=0; i<indice && liste != NULL; i++)
6 {
7 liste = liste->nxt;
8 }
9
10 /* Si l'élément est NULL, c'est que la liste contient moins de i éléments */
11 if(liste == NULL)
12 {
13 return NULL;
14 }
15 else
16 {
17 /* Sinon on renvoie l'adresse de l'élément i */
18 return liste;
19 }
20 }

Récupérer la valeur d'un élément

C'est une fonction du même style que la fonction estVide. Elle sert à "masquer" le fonctionnement interne à
l'utilisateur. Il suffit simplement de renvoyer à l'utilisateur la valeur d'un élément. Il faudra renvoyer un code
d'erreur entier si l'élément n'existe pas (la liste est vide), c'est donc à vous de définir une macro selon
l'utilisation que vous voulez faire des listes chaînées. Dans ce code, je considère qu'on ne travaille qu'avec
des nombres entiers positifs, on renverra donc -1 pour une erreur. Vous pouvez mettre ici n'importe quelle
valeur que vous êtes sûrs de ne pas utiliser dans votre liste. Une autre solution consiste à renvoyer un
pointeur sur int au lieu d'un int, vous laissant donc la possibilité de renvoyer NULL.

Code : C
1 #define ERREUR -1
2 int valeur(llist liste)
3 {
4 return ((liste == NULL)?ERREUR:(liste->val));
5 }

Compter le nombre d'éléments d'une liste chaîné

C'est un algorithme vraiment simple. Vous parcourez la liste de bout en bout et incrémentez d'un pour
chaque nouvel élément que vous trouvez. Cet algorithme n'ayant aucun intérêt au point où nous en sommes,
je vais en profiter pour vous faire découvrir un nouveau type d'algorithme. Jusqu'à maintenant, nous n'avons
19
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

utilisé que des algorithmes itératifs qui consistent à boucler tant que l'on n'est pas au bout. Nous allons voir
qu'il existe des algorithmes que l'on appellent récursifs et qui consistent en fait à demander à une fonction de
s'appeler elle-même.

Avant tout, je pense qu'un petit point d'explication s'impose.


Pour créer un algorithme récursif, il faut connaître la condition d'arrêt (ou condition de sortie) et la condition
de récurrence. Il faut en fait vous imaginer que votre fonction a fait son office pour les n-1 éléments
suivants, et qu'il ne reste plus qu'à traiter le dernier élément. Lisez le code suivant. N'ayez pas peur si ceci
vous semble obscur, ce n'est pas essentiel pour utiliser les listes chaînées. Maintenant, je suis sûr que vous
êtes au point et vous pouvez donc de manière itérative créer à peu près tout ce qui peut se faire.

Code : C
1 int nombreElements(llist liste)
2{
3 /* Si la liste est vide, il y a 0 élément */
4 if(liste == NULL)
5 return 0;
6
7 /* Sinon, il y a un élément (celui que l'on est en train de traiter)
8 plus le nombre d'éléments contenus dans le reste de la liste */
9 return nombreElements(liste->nxt)+1;
10 }

Effacer tous les éléments ayant une certaine valeur

Pour cette dernière fonction, nous allons encore une fois utiliser un algorithme récursif. Même si la
récursivité vous semble être une notion complexe (et ça l'est sûrement), elle simplifie grandement les
algorithmes dans certains cas, et dans celui-ci tout particulièrement. Je vous donne le code à titre indicatif,
vous pourrez vous même recoder cette fonction avec un algorithme itératif si vous le voulez.

Code : C
1 llist supprimerElement(llist liste, int valeur)
2 {
3 /* Liste vide, il n'y a plus rien à supprimer */
4 if(liste == NULL)
5 return NULL;
6
7 /* Si l'élément en cours de traitement doit être supprimé */
8 if(liste->val == valeur)
9 {
10 /* On le supprime en prenant soin de mémoriser
11 l'adresse de l'élément suivant */
12 element* tmp = liste->nxt;
13 free(liste);
14 /* L'élément ayant été supprimé, la liste commencera à l'élément suivant
15 pointant sur une liste qui ne contient plus aucun élément ayant la valeur
16 recherchée */
17 tmp = supprimerElement(tmp, valeur);
18 return tmp;
19 }
20 else
21 {
22 /* Si l'élement en cours de traitement ne doit pas être supprimé,
23 alors la liste finale commencera par cet élément et suivra une liste ne
24 contenant
25 plus d'élément ayant la valeur recherchée */
20
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

26 liste->nxt = supprimerElement(liste->nxt, valeur);


27 return liste;
}
}

21
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Exercices (2/2)

Exercice n°1
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:

Code : C
1 int nombreElements(llist liste);

Code : C
1 int nombreElements(llist liste)
2{
3 int nb = 0;
4 element* tmp = liste;
5
6 /* On parcours la liste */
7 while(tmp != NULL)
8 {
9 /* On incrémente */
10 nb++;
11 tmp = tmp->nxt;
12 }
13 /* On retourne le nombre d'éléments parcourus */
14 return nb;
15 }

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.

Code : C
1 llist effacerListe(llist liste)
2{
3 element* tmp = liste;
4 element* tmpnxt;
5
6 /* Tant que l'on n'est pas au bout de la liste */
7 while(tmp != NULL)
8 {
9 /* On stocke l'élément suivant pour pouvoir ensuite avancer */
10 tmpnxt = tmp->nxt;
11 /* On efface l'élément courant */
12 free(tmp);
13 /* On avance d'une case */
14 tmp = tmpnxt;
15 }
16 /* La liste est vide : on retourne NULL */
17 return NULL;
18 }

Code : C
22
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

1 llist effacerListe(llist liste)


2{
3 if(liste == NULL)
4 {
5 /* Si la liste est vide, il n'y a rien à effacer, on retourne
6 une liste vide i.e. NULL */
7 return NULL;
8 }
9 else
10 {
11 /* Sinon, on efface le premier élément et on retourne le reste de la
12 liste effacée */
13 element *tmp;
14 tmp = liste->nxt;
15 free(liste);
16 return effacerListe(tmp);
17 }
18 }

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'autres choses...

Passons maintenant à un petit QCM, afin de vérifier que vous avez tout saisi !

Q.C.M.
Une erreur s'est glissée dans le code suivant :
Code : C
1 typedef struct element element;
2 struct element
3 {
4 int val;
5 struct element nxt;
6 };

 Il manque un typedef Il manque une étoile Le code est juste

J'exécute le programme contenant le main suivant, que va-t-il se passer ?

Code : C
1 int main(int argc, char **argv)
2{
3 element* ma_liste;
4 ma_liste = ajouterEnTete(ma_liste, 12);
5 afficherListe(ma_liste);
6 return 0;
7}

 Rien du tout Ça ne compile pas : tu as mis element* à la place de llist pour déclarer
ma_liste Une erreur de segmentation
23
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

Aller plus loin

Nous venons de faire le tour des principales fonctions nécessaires à la gestion d'une liste chaînée :
initialisation, ajout d'élément, suppression d'élément, etc. Voici quelques autres fonctions qui manquent et
que je vous invite à écrire, ce sera un très bon exercice !
 Insertion d'un élément en milieu de liste : actuellement, nous ne pouvons ajouter des éléments
qu'au début de la liste, ce qui est généralement suffisant. Si toutefois on veut pouvoir ajouter un
élément au milieu, il faut créer une fonction spécifique qui prend un paramètre supplémentaire :
l'adresse de celui qui précèdera notre nouvel élément dans la liste. Votre fonction va parcourir la liste
chaînée jusqu'à tomber sur l'élément indiqué. Elle y insèrera le petit nouveau juste après.
 Suppression d'un élément en milieu de liste : le principe est le même que pour l'insertion en milieu
de liste. Cette fois, vous devez ajouter en paramètre l'adresse de l'élément à supprimer.
 Destruction de la liste : il suffit de supprimer tous les éléments un à un !
 Taille de la liste : cette fonction indique combien il y a d'éléments dans votre liste chaînée. L'idéal,
plutôt que d'avoir à calculer cette valeur à chaque fois, serait de maintenir à jour un entier
nbElements dans la structure Liste. Il suffit d'incrémenter ce nombre à chaque fois qu'on ajoute
un élément et de le décrémenter quand on en supprime un.
Je vous conseille de regrouper toutes les fonctions de gestion de la liste chaînée dans des fichiers
liste_chainee.c et liste_chainee.h par exemple. Ce sera votre première bibliothèque ! Vous
pourrez la réutiliser dans tous les programmes dans lesquels vous avez besoin de listes chaînées.

Source : https://openclassrooms.com/courses/les-listes-chainees-2

Ajout d'un élément dans une liste chaînée


1- Ajout en tête
Pour ajouter un élément au début de la liste on a les étapes suivantes:
 Créer une nouvelle cellule.
 Remplir la nouvelle valeur dans la partie information.
 Effectuer le chaînage.
La procédure correspondante est la suivante:

procédure inserer_au_debut(Pt: Ptr, v: entier)

var P: Ptr

24
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

début
allouer(P)
val(P) ← v
suivant(P) ← Pt

Pt ← P
fin

2- Ajout après P
Pour ajouter un nouvel élément après une cellule, on a les étapes suivantes:
 Rechercher la cellule pointée par P
 Créer une nouvelle cellule.
 Introduire la nouvelle valeur de cette cellule.
 Effectuer le chaînage c'est-à-dire pointer le suivant de la nouvelle cellule au suivant de la cellule
P. Ensuite pointer le suivant de P à la nouvelle cellule. La procédure correspondante est la
suivante

La procédure correspondante est la suivante:


procédure ajout_apres(tete, P: pointeur)

var P1, P2: pointeur


v: entier

début

P1 ← tete
tant que P1≠P faire
P1 ← suivant(P1)
fin tant que
lire(v)
allouer(P2)
val(P2) ← v
suivant(P2) ← suivant(P)
suivant(P) ← P2
fin

3- Ajout a vant P

25
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

procédure ajout_avant(tete, P: Ptr)

var P2, t: Ptr


val: info

début
t ← tete
tant que suivant(t)≠P faire
t ← suivant(t)
fin tant que
lire(v)
allouer(P2)
val(P2) ← v
suivant(t) ← P2
suivant(P2) ← P
fin

4- Ajout en fin de liste

procédure ajout_fin(tete: Ptr)

var P1, P2: Ptr


v: entier
début
P1 ← tete
tant que suivant(P1)

fin tant que


lire(v)
allouer(P2)
val(P2) ← v
suivant(P2) ← nil
Suivant(P1) ← P2
26
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

fin

procédure insertion(tete, Ptr, v: entier)

var P, P1, P2: Ptr

début
P ← tete
si v < val(P) alors
allouer(P1)
val(P1) ← v
suivant(P1) ← P1
tete ← P1
sinon
tant que (val(P)<v) et (suivant(P)≠nil) faire
P ← suivant(P)
fin tant que
si val(P) >= v alors
P1 ← tete
tant que suivant(P1) ≠ P faire
P1 ← suivant(P1)
fin tant que
allouer(P2)
val(P2) ← v
suivant(P1) ← P2
suivant(P2) ← P
sinon
si suivant(P) = nil
allouer(P2)
val(P2) ← v
suivant(P2) ← nil
suivant(P) ← P2
fin si
fin si
fin si
fin

E xerc ic e 2:
fonction compte(tête: Ptr, v: entier): entier

var P: Ptr
i: entier

début
P ← tete
t←0
tant que P ≠ nil faire
si val(P)=v alors
t ← t+1
fin si
27
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

P ← suivant(P)
fin tant
compte ← t
fin

E xerc ic e 3:
Créer un polynôme (donner dans l'ordre croissant)

Déclaration
type polynôme = pointeur : liste chaînée
liste chaîne = structure // ou enregistrement
coef: réel
deg: entier
suivant = polynôme
fin structure // ou enregistrement

procédure creer_polynome(tete: polynome)

début
tete ← nil
nouveau(P)
lire(val)
lire(val1)
coef(P) ← val
deg(P) ← val1
suivant(P) ← nul
tete ← P
repéter
lire(val)
lire(val1)
si val≠0 alors
si val1< degré(tete) alors
nouveau(P1)
coef(P1) ← val
deg(P1) ← val1
suivant(P1) ← tete
tete ← P1
sinon
P ← tete
tant que (deg(P)<val1) et (suivant(P) ≠ nil) faire
P ← suivant(P)
fin tant que
si deg(P)>val1 alors
P1 ← tete
tant que suivant(P1)≠P faire
P1 ← suivant(P1)
fin tant que
nouveau(P2)
coef(P2) ← val
degre(P2) ← val1
suivant(P2) ← P

28
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

suivant(P1) ← P2
sinon
si suivant(P)=nil alors
nouveau(P2)
coef(P2) ← val
deg(P2) ← val1
suivant(P2) ← nil
suivant(P) ← P2
fin si
fin si
fin si
fin si
jusqu'à val=0
fin

Suppression d'u n élément dans une liste chaînée


Utilisons la fonction libérer (en Pascal = dispose) pour libérer un emplacement occupé en
mémoire.
Remarque:
 La fonction "libérer" ne libère pas un emplacement nil et il libère une seule cellule à la fois.
 La fonction "libérer" ne libère qu'une seule cellule de la liste et non de la liste tout entière.
Suppression en tête de liste
Il s'agit de retirer l'élément en tête de la liste si la liste est vide, on ne retire aucun élément et on envoye un
message correspondant

procédure supp_tete(tete: Ptr)

var P: Ptr
P ← tete

si tete=nil alors
écrire('pas de suppression')
sinon
tete ← suivant(tete)
liberer(P)

fin si
fin

Suppression d'u n élément P de la liste


Pour supprimer un élément P de la liste, parcourir la liste chaînée du début de la liste jusqu'au précédent de
la cellule P, ensuite pointer le suivant du précédent de P ou suivant de P. Enfin libérer la cellule P.
La procédure correspondante est:

procédure supp_p(tête, P: Ptr)

29
Module : Algorithmique et structure de données - Année universitaire : 2015-2016

var P1: Ptr

début

si tete≠nil alors
P1 ← tete
tant que P#suivant(P1) faire
P1 ← suivant(P1)
fin tant que

suivant(P1) ← suivant(P)
// voir cas tete
liberer(P)
fin si
fin

Suppression en fin de liste


Pour supprimer un élément en fin de liste, il faut maintenir un pointeur à l'avant dernière cellule,
ensuite libérer la dernière cellule et enfin mettre le suivant de l'avant dernière cellule à nil. La
procédure correspondante est la suivante.

Procédure Supp_fin(tete: Ptr)

var P, P1: Ptr

début
P1 ← tete
P ← suivant(P1)
tant que suivant(P)≠nil faire
P1 ← suvant(P1)
P ← suivant(P)
fin tant que
suivant(P) ← nil
liberer(P)
fin

Source : http://www.mongosukulu.com/index.php/en/contenu/informatique -et-

reseaux/algorithme/663 -les-listes-chainees?showall=1

30

Vous aimerez peut-être aussi