Explorer les Livres électroniques
Catégories
Explorer les Livres audio
Catégories
Explorer les Magazines
Catégories
Explorer les Documents
Catégories
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
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)
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 ;
Chaînages dynamiques
Listes Dynamiques simplement chaînées (LDSC)
le pointeur p repère la LDSC
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
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.
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.
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.
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).
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));
element->nombre = 0;
element->suivant = NULL;
liste->premier = element;
return liste;
}
Excercice
Ecrire l’algorithme équivalent du programme précédent
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;
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.
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.
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;
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) !
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
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
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 :
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
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 }
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 }
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 }
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 }
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.
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 }
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
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
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 };
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
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
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
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
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
fin
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
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
var P: Ptr
P ← tete
si tete=nil alors
écrire('pas de suppression')
sinon
tete ← suivant(tete)
liberer(P)
fin si
fin
29
Module : Algorithmique et structure de données - Année universitaire : 2015-2016
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
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
reseaux/algorithme/663 -les-listes-chainees?showall=1
30