Chapitre 02

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 10

Université A/MIRA-Béjaia

Faculté des sciences exactes


Département mathématique/MI

Algorithmique et Structures de Données 2

Chapitre 2 : Les listes chaînées.

Edité par : GHILAS Hamza


Chapitre 02 : Les listes chaînées
1. Introduction
2. Le type pointeur
3. Gestion dynamique de la mémoire
4. Les listes chaînées
5. Opérations sur les listes chaînées

1. Introduction :
Les structures de données statiques (les tableaux) ont l’inconvénient du gaspillage de la
mémoire et parfois d’insuffisance de la mémoire. La solution c’est les structures de données
dynamiques ou l’espace mémoire est alloué au fur et à mesure par l’algorithme. Les cellules de
la mémoire centrale (RAM) sont regroupées en octet et chaque octet est numéroté par un
numéro unique appelé adresse mémoire. Les adresses mémoire sont stockées dans des variables
de type pointeur.

2. Le type pointeur
Le type pointeur est destiné pour contenir une adresse mémoire. Une variable de type pointeur
est stockée dans une case mémoire et contient l’adresse d’une autre case mémoire.

Exemple : Si on considère un pointeur P contenant l’adresse mémoire @004, la situation en mémoire


centrale est représentée ci-dessous. On dit que P pointe la case mémoire à l’adresse @004.

M.C
@001
P = @004 @002
@003
@004
@005
@006
@007
2.1. Déclaration

Pour déclarer une variable de type pointeur, il faut préciser le type de la case mémoire à pointer. La
syntaxe pour déclarer un pointeur est la suivante

syntaxe
Algorithmique Langage C
Var id_pointeur : ↑type; type *id_pointeur;
Exemples
Var P : ↑entier; int *P;
Type pointeur = ↑Etudiant; typedef struct Etudiant Etudiant;
Etudiant = enregistrement Etudiant *q ;
Nom: chaine[30]; struct Etudiant{
Prenom : chaine[30]; char Nom[30];
Moyenne : reel; char Prenom[30];
Var q : pointeur; float Moyenne ;};
Dans l’exemple ci-dessus, P est un pointeur destiné à contenir une adresse mémoire d’une case de type
entier et q est destiné à contenir l’adresse d’une variable de type Etudiant. Il est possible de déclarer
un pointeur vers un type qui n’est pas encore défini, car la taille d’une case mémoire de type pointeur
est la même quel que soit le type pointé (un pointeur est codé généralement sur 4 octets).

2.2. Manipulation des pointeurs en algorithmique

Les variables de type pointeur sont appelées ‘variables dynamiques’ l’espace mémoire nécessaire est
réservé dans la partie action de l’algorithme. C’est au programmeur de gérer les variables dynamiques
en utilisant deux opérations prédéfinies :

 Allouer(P) : permet de réserver une case mémoire est sans adresse est stocker dans le pointeur
P fourni en paramètre.
 Liberer(P) : permet de libérer la case mémoire pointé par le pointeur P et sa valeur devient
NIL
L’espace mémoire réservé avec la procédure Allouer reste occupé même après terminaison du
programme. Dans un algorithme il faut libérer tous les objets pointeurs en utilisant la procédure
liberer() pour ne pas saturer la mémoire de la machine.

Après déclaration d’une variable de type pointeur, celle-ci ne contient aucune adresse et elle ne pointe
vers aucune case mémoire. Sa valeur est la valeur par défaut ‘NIL’ et signifie que le pointeur ne pointe
nul part. Pour réserver la case mémoire d’un pointeur il faut utiliser la procédure prédéfinie Allouer(P).
Pour accéder et manipuler le contenue de la case mémoire pointée par P en écrit P↑

Remarque : la valeur ‘NIL’ d’un pointeur est très importante. Le test Si (P = NIL) permet de savoir
si le pointeur contient une adresse.

Exemples :
Var p : ↑entier;
allouer(p);
p↑  25 ;

L’instruction allouer(p); permet de réserver un espace mémoire pour un entier et son adresse est
stockée dans le pointeur p. La case mémoire pointé par le pointeur p est désignée par p↑.
Algorithme pointeur ;
Type Etudiant = enregistrement;
Nom: chaine[30];
Prenom : chaine[30];
Moyenne : reel;
Var q : ↑Etudiant;
Debut
| Allouer(q);
| Avec (q↑) faire
| | Lire(nom,prenom,moyenne);
| finAvec;
| ecrire(q↑.nom, q↑.prenom);
Fin.

2.3. Manipulation des pointeurs en Langage C

L'opérateur unaire d'indirection * permet d'accéder directement à la valeur de l'objet pointé. Ainsi,
si p est un pointeur vers un entier i, *p désigne la valeur de i.
int main()
{ int i = 3;
int *p;
p = &i;
printf("*p = %d \n",*p);
}
L’opérateur & renvoie l’adresse mémoire d’une variable, le pointeur p contient l’adresse de i. Alors, le
programme affiche *p = 3.
Les opérations possibles sur un pointeur sont :
 L’addition d’un entier à un pointeur.
 La soustraction d’un entier d’un pointeur
Le type des pointeurs est important pour leur arithmétique. Si i est un entier et p est un pointeur sur un
objet de type type, l'expression p + i désigne un pointeur sur un objet de type type dont la valeur est
égale à la valeur de p incrémentée de i * sizeof(type). Il en va de même pour la soustraction d'un
entier à un pointeur.
Exemples :
main()
{
int i = 3;
int *p1, *p2;
p1 = &i;
p2 = p1 + 1;
printf("p1 = %ld \t p2 = %ld\n",p1,p2);
}
Le programme affiche p1 = 4831835984 p2 = 4831835988.
Par contre, le même programme avec des pointeurs sur des objets de type double :
main()
{
double i = 3;
double *p1, *p2;
p1 = &i;
p2 = p1 + 1;
printf("p1 = %ld \t p2 = %ld\n",p1,p2);
}
Affiche p1 = 4831835984 p2 = 4831835992.

2.3.1. Allocation dynamique

Avant de manipuler un pointeur, et notamment de lui appliquer l’opérateur d’indirection *, il


faut l’initialiser. Sinon, par défaut, la valeur du pointeur est égale à une constante symbolique
notée NULL définie dans <stdio.h>. En général, cette constante vaut 0. Dans ce cas le
pointeur ne contient aucune adresse ainsi que le test p == NULL permet de savoir si le pointeur
p pointe vers un objet. Pour pouvoir utiliser la variable *p pointée par p, il faut lui réserver un
espace mémoire. Cette opération s’appelle allocation dynamique, elle se fait en langage C avec
la fonction malloc prédéfinie dans <stdlib.h>, sa syntaxe est :
Pointeur = malloc(nombre-octets) ;
Cette fonction retourne un pointeur de type char* pointant vers un objet de taille nombre-
octets. Pour initialiser des pointeurs vers des objets qui ne sont pas de type char, il faut convertir
le type de la sortie de la fonction malloc à l’aide d’un cast. L’argument nombre-octets est
souvent donné à l’aide de la fonction sizeof(type) qui renvoie le nombre d’octets utilisés
pour stocker un objet.
Ainsi, pour initialiser un pointeur vers un entier, on écrit :
#include <stdlib.h>
int *p;
p = (int*)malloc(sizeof(int));
Enfin, lorsque l’on n’a plus besoin de l’espace-mémoire alloué dynamiquement (c’est-`a-dire
quand on n’utilise plus le pointeur p), il faut libérer cette case mémoire. Ceci se fait à l’aide de
l’instruction free qui a pour syntaxe
free(nom-du-pointeur);
A toute instruction malloc doit être associée une instruction free.

2.3.2. Pointeurs et tableaux

Tout tableau en C est en fait un pointeur constant dont la déclaration est :


int tab[10];
tab est un pointeur constant (non modifiable) dont la valeur est l’adresse du premier élément
du tableau. Autrement dit, tab a pour valeur &tab[0]. On peut donc utiliser un pointeur initialisé
à tab pour parcourir les éléments du tableau.
int tab[5] = {1, 2, 6, 0, 7};
main(){
int i; int *p;
p = tab;
for (i = 0; i < 5; i++){
printf(" %d \n",*p);
p++;
}}
Grace à l’allocation dynamique, il est possible de créer des tableaux dont la taille est connue à
l’exécution du programme.
#include <stdlib.h>
main(){
int n;
int *tab;
scanf(″%d″,&n);
...
tab = (int*)malloc(n * sizeof(int));
...
free(tab);
}

3. Les listes chainées


Une liste chainée est une suite d’enregistrement telle que chaque enregistrement contient
l’adresse mémoire d’un autre enregistrement. Il existe plusieurs types de liste à savoir : les
listes linéaires avec chainage simple, les listes doublement chainées et des listes particulières
comme les piles et les files d’attente.
3.1. Les listes linéaires simples
Une liste linéaire chainée est une suite d’enregistrements, chaque enregistrement contient
l’adresse de l’enregistrement suivant dans la liste. La liste est référencée par l’adresse du
premier enregistrement appelé tête de liste. La figure ci-après illustre un exemple de liste
contenant des valeurs entières.

Le pointeur L contenant l’adresse du premier enregistrement est appelé tête liste. Le dernier
enregistrement d’une liste chainée ne contient aucune adresse.
Dans la suite de ce document les opérations élémentaires sur une liste chainée
sont implémentées à savoir :
 La création
 La suppression d’un élément.
 La suppression d’une liste (vider la liste).
 Affichage les éléments de la liste.
3.2. Déclaration

Un élément d’une liste chainé est un enregistrement contenant des champs pour des
informations à traiter et un champ de type pointeur pour stocker l’adresse de l’élément suivant
dans la liste.
syntaxe
Algorithmique Langage C
Type nom_element = enregistrement struct nom_element {
info: type ; type info;
suivant:↑nom_element; struct nom_element *suivant;
Fin ; };

Exemples
Type liste : ↑Etudiant ; typedef struct Etudiant Etudiant;
Etudiant = enregistrement typedef Etudiant* liste ;
Nom: chaine[30]; struct Etudiant{
Prenom : chaine[30]; char Nom[30];
Moyenne : reel; char Prenom[30];
Suivant : liste ; float Moyenne ;
Var L: liste; liste suivant;
};
Liste L ;

Dans ce qui suit, une liste de nombre entier est utilisée pour la définition des procédures
permettant de réaliser les opérations de base. Considérons la structure de donné pour
implémenter une liste de nombre entiers.
Type liste : ↑Element;
Element = enregistrement
Val: entier;
Suivant : liste ;
Fin ;
3.3. Création d’une liste chainée

Lors de la déclaration d’une variable de type liste, sa valeur est NIL. Initialement une liste ne
contient aucune adresse mémoire, les enregistrements sont créés et ajoutés à la liste par
l’algorithme. La création d’une liste peut se faire par ajout en fin de liste ou au début de la
liste.

Pour ajouter un élément à une liste chainée, il faut d’abord allouer un espace mémoire pour
l’enregistrement et initialiser les champs information
allouer(p);
lire(p↑.Val);

a. Ajout en début de liste

Une fois l’élément à ajouter est créé, il faut le rattacher à la liste.

(1) p↑.suivant ← L;
(2) L ← p;

Ci-dessous la procédure permettant de créer une liste d’entier avec ajout en début de liste.
Procedure Creer_debut(var L: liste) ;
Var p :liste;
rep :caractere ;
Debut
Repeter
Allouer(p); // création du nouvel élément
Lire(p↑.val); // lire la valeur de l’élément
p↑.suivant←L;// attacher le nouvel enregistrement à la liste
L←p ; // modifier la tête de la liste
Ecrire('voulez vous continuer o/n') ;
Lire(rep) ;
Jusqua(rep='n');
Fin.

b. Ajout en fin de liste

L’ajout d’un élément en fin de liste se fait comme suit :


Il faut maintenir un pointeur q sur le dernier élément de la liste pour réaliser le lien de
chainage :

q↑.suivant ← p;

Ci-dessous la procédure permettant de créer une liste d’entier avec ajout en fin de liste.
Procedure Creer_fin(var L: liste) ;
Var p,q :liste;
rep :caractere ;
Debut
Allouer(L); // création du premier élément
Lire(L↑.val);
q←L; // positionner le pointeur q sur la fin de la liste
Ecrire('voulez vous continuer o/n') ;
Lire(rep) ;
Tantque(rep = 'o')
Allouer(p); // création du nouvel élément
Lire(p↑.val); // lire la valeur de l’élément
q↑.suivant←p;// attacher le nouvel enregistrement à la liste
q←p ; // déplacer le pointeur q en avant pour pointer le dernier
élément. le pointeur q doit pointer toujours le dernier élément.
Ecrire('voulez vous continuer o/n') ;
Lire(rep) ;
finTQ;
Fin.

3.4. Suppression d’un élément


La suppression d’un élément dans une liste est réelle, la case mémoire réservé à l’élément est
libérer après suppression. La suppression ne se fait pas de la même manière lorsque l’élément
à supprimer est au début de la liste ou au milieu.

La suppression du premier élément de la liste se fait comme suit


1. p←L ; // Positionner un pointeur sur le premier élément
2. L ←L↑.suivant ; // Modifier le lien de chainage
3. Liberer(p) ; // Suppression

Si l’élément à supprimer n’est pas le premier de la liste, il faut positionner un pointeur p sur
l’élément à supprimer et un autre pointeur q sur l’élément le précédant et procéder comme
suit :
1. q↑.suivant ← p↑.suivant ; // modifier les liens de chainage
2. liberer(p) ; // suppression
La procédure supprimer ci-dessous permet de supprimer une valeur v d’une liste d’entier L
(on suppose qu’il y’a une seule occurrence de la valeur v dans la liste L).
Procedure supprimer(var L: liste, v:entier) ;
Var p,q :liste;
Debut
Si(L↑.Val=v) alors
p←L;
L ←L↑.suivant ;
Liberer(p);
Sinon
q←L;
p←L↑.suivant ;
Tantque(p↑.val <> v)&&(p<> NIL)
q←p ;
p←p↑.suivant;
finTQ;
Si(p<>NIL)Alors
q↑.suivant ← p↑.suivant ;
liberer(p) ;
finsi;
finsi;
Fin;

3.5. Suppression de la liste :


Pour supprimer une liste chainée, il faut supprimer ses éléments un par un. L’instruction liberer(L)
ne supprime que le premier élément de la liste et l’accès aux autres éléments sera impossible.
1. p←L;
2. L ←L↑.suivant ;
3. Liberer(p);
Ces instructions permettent de supprimer le premier élément de la liste, il suffit de les répéter jusqu’à ce
que la liste soit vide (L=NIL). Ci-dessous la procédure vider_liste permettant de supprimer tous les
éléments d’une liste.
Procedure vider_liste(var L: liste) ;
Var p :liste;
Debut
Tantque(L<> NIL)
p←L;
L ←L↑.suivant ;
Liberer(p);
finTQ;
Fin;

3.6. Affichage d’une liste chainée :

Les éléments d’une liste chainée ne sont pas indexés comme les éléments d’un tableau. Pour
parcourir une liste chainée on procède comme suit :

1. P←L; Positionner un pointeur P en tête de liste.


2. P←P↑.suivant; Déplacer le pointeur P jusqu’au à la fin de la liste
Ci-dessous la procédure afficher liste

Procedure afficher_liste(var L: liste) ;


Var p :liste;
Debut
p←L;
Tantque(p <> NIL)
Ecrire(p↑.valeur) ;
p ←p↑.suivant ;
finTQ;
Fin;

Remarque : le support de cours ne peut pas remplacer le cours en présentiel.

Vous aimerez peut-être aussi