Université Mohammed V-Agdal École Supérieure de Technologie Salé STRUCTURE DE DONNEES … F. Guerouate
Chapitre-1 Listes chaînées
Définition … Introduction Les listes chaînées sont des structures dont le nombre d’éléments de même type peut varier au cours de l’exécution du programme. Les éléments consécutifs seront tout simplement chaînés entre eux. Il existe plusieurs types de listes chaînées dépendant de la manière dont on se déplace dans la liste, les listes chaînées simples, les listes doublement chaînées, les listes circulaires.
Les listes chaînées simples … Dans une liste chaînée, un élément est la donnée d’un couple (P, V) , place valeur. Sur une liste chaînée on peut définir trois fonctions : * Une fonction qui permet de retourner le 1er élément de la liste * Une fonction qui permet de retourner le successeur d’un élément dans la liste * Une fonction qui permet de retourner la valeur de l’élément si bien sur la place de ce dernier est connue Convention graphique Valeur de l’élément. Place de l ‘élément suivant dans la liste, place est de type pointeur. valeur place
Représentation graphique d’une liste chaînée dernier valeur ~= NIL premier element Pointeur sur l’élément suivant Déclaration du type pointeur struct elem { int valeur ; struct elem * suivant ; } ; typedef struct elem liste;
struct elem. suivant ; déclare un pointeur sur la données suivante struct elem *suivant ; déclare un pointeur sur la données suivante. « suivant » est le nom du pointeur (vous pouvez donc mettre n'importe quel nom) et c'est là qu'on découvre à quoi sert l'étiquette. En effet à ce stade le nom de la structure n'est pas encore donné. Toutefois le pointeur va pointer sur un nouvel élément, donc sur une instance de la structure que l'on est justement en train de définir. On utilise donc l'étiquette précédée du mot clef « struct ». element : il s'agit tout simplement du nom final donnée à la structure. Sur la ligne suivant : on déclare un nouveau type qui est un pointeur vers un element et que l'on appelle « liste ». Cette opération est facultative mais elle accrôit sensiblement la lisibilité du programme.
Accès aux valeurs de la liste … L'accès à la valeur d'une variable d'un élément donné se fait en reprenant les notations de structure composée. Exemple liste *l ; int a ; a=(*l).valeur ; liste *l ; int a ; a=l->valeur ; Ou L'accès au(x) pointeur(s) se fera de la même façon : l->suivant.
Initialiser la liste chaînée … Pour cela on utilise l'instruction « malloc » associé à « sizeof » : liste *l ; l=(liste*)malloc(sizeof(liste)); Notre liste est maintenant initialisée. « l » sera un pointeur sur le premier élément. On peut désormais affecter une valeur à cet élément. Il est important de se souvenir qu'il faudra réserver de la mémoire, donc faire un malloc, à chaque fois que l'on voudra insérer un nouvel élément, à l'emplacement de l'élément.
Saisie de plusieurs éléments … Admettons que l'utilisateur est entrée au clavier la valeur d'une variable entière «n» indiquant le nombre d'élément à créer. Le programme de saisie pourrait être celui-ci : Liste *l,*laux ; //on déclare également une variable auxiliaire qui va nous servir pour parcourir la liste l=(liste*)malloc(sizeof(element)) ;//on initialise c'est à dire on fait pointer l sur l'espace mémoire réservé pour le premier element laux=l ; //on fait pointer laux au même endroit que l
for(i=1 ;i<n+1 ;i++) { //on doit saisir n valeurs scanf("%d",laux->valeur) ; //saisie de la valeur //on fait pointer le pointeur suivant sur un nouvel espace réservé laux->suivant=(liste*)malloc(sizeof(element)) ; if(i==n) laux->suivant=NULL ; //on fait pointer le dernier élément sur un pointeur NULL afin d'avoir un programme plus « propre » laux=laux->suivant ; //on fait pointer laux sur le nouvel espace créé afin de saisir la valeur du prochain élément au prochain tour de boucle ; }
Attention Il ne faut surtout pas utiliser le pointeur principal « l » pour parcourir les éléments de la liste. Sinon on perdrai l'adresse du premier élément et donc de toute la liste chaînée. on utilise toujours un pointeur de parcours (liste auxiliaire) Il est important de savoir qu'il faut toujours faire l'allocation de mémoire AVANT d'ordonner au pointeur de pointer sur un élément.
Si on avait écrit : laux=laux->suivant ; laux=(liste*)malloc(sizeof(element)) ; de même que : laux=l ; l=(liste*)malloc(sizeof(element)) ; la compilation aurait fonctionné mais l'exécution non. En effet l'adressage du pointeur change au moment de l'allocation de la mémoire. Ceci représente une erreur type. Il faut y faire attention ! N'oubliez pas non plus que toute saisie d'un nouvel élément doit être précédée d'une allocation de mémoire.
En pratique, la saisie se fait rarement par une boucle mais par un appel successif d'une fonction définie par l'utilisateur qui saisie un élément en début ou en fin de liste suivant les cas. Il n'est pas obligatoire mais il est fortement recommandé de terminer la liste par le pointeur NULL (sauf en cas de liste cyclique, où le dernier pointeur pointe sur le premier élément). A noter l'existence de la fonction free() ; qui libère la place assignée par un malloc. Ex : free(l) ;
Les opérations sur les listes … Les opérations sur les listes sont très nombreuses, parmi elles : - Créer une liste vide - Tester si une liste est vide - Ajouter un élément à la liste · ajouter en début de liste (tête de liste) · ajouter à la fin de la liste (queue) · ajouter un élément à une position donnée · ajouter un élément après une position donnée . ajouter un élément avant une position donnée
- Afficher ou imprimer les éléments d'une liste - Ajouter un élément dans une liste triée (par ordre ascendant ou descendant) - Supprimer un élément d’une liste · supprimer en début de liste · supprimer en fin de liste · supprimer un élément à une position donnée . supprimer un élément avant ou après une position donnée.
Parcours d’une liste chaînée … Il existe une liste chaînée d’entiers, préalablement déclarée But : Afficher dans l’ordre de la liste tous les éléments de la liste : 5-10-15-20-25 5 10 15 20 25 l laux l.valeur l.suivant Remarque : Penser à dupliquer la valeur du premier pointeur (laux).
Algorithme Var *l, *laux : liste Début laux :=l TQ ( laux<>NULL ) FAIRE Sortir (laux->valeur) laux:= laux->suivant FTQ Remarque : Quand on cherche le dernier élément d‘une liste, le test à effectué est (laux->suivant) <>null. Le test laux<>null permet de parcourir indifféremment tous les éléments de la liste.
Ajout en tête de liste quand la liste n’est vide … Exemple : On suppose que l’on veut rajouter l’élément 6 au début de la liste suivante : 4 3 5 8 2 l a)- on commence par créer le nouveau nœud et lui donner son contenu 6 laux
b)- le suivant de p est l’ancienne tête de liste 6 4 3 5 8 2 laux l b)- on fait pointer le pointeur l vers la tête de liste 6 4 3 5 8 2 l
Les opérations a, b et c se traduisent par le pseudo-code suivant : Allouer (laux) Laux-> valeur := 6 Laux-> suivant := l l := laux Remarque : Si la liste est vide, cet algorithme convient aussi sous réserve d’avoir correctement initialiser premier
Ajout en fin de liste (liste non vide) … a)- on cherche le dernier élément de la liste, pour cela on utilise un pointeur de parcours. b)- création du nouveau bloc à insérer dans la liste.(nouveau) c)- lier le nouveau bloc à la liste.
q := l TQ (q ->Suivant <>nil) FAIRE q := q -> Suivant FTQ Allouer (nouveau) Entrer (nouveau ->Valeur) nouveau ->suivant := nil q ->suivant := nouveau
Dans cette situation, le pointeur nouveau n’est pas indispensable car le pointeur de parcours q pointe sur le dernier bloc.Comment ? . Allouer (q-> Suivant) q := q-> Suivant q-> Suivant := nil entrer (q -> valeur)
Ajout dans une liste chaînée … Ajout d’un élément après un nœud pointé par p p q La recherche a été effectuée est fournie le pointeur sur le bloc qui précède celui à insérer.
a)- création du nouveau bloc b)- mise à jour des liens. Allouer (nouveau) Entrer (nouveau -> Valeur) nouveau ->Suivant := laux-> suivant Laux->suivant := nouveau
Ajout d’un élément avant un nœud pointé par p 17 -6 9 34 Vers le nouvel élément Si la valeur du nouvel élément est -5, le successeur du prédécesseur de p (-6) devient le nouvel élément (-5), or nous ne pouvons pas remonter un pointeur à reculons, il faudrait repartir au début de la liste. Une solution serait d’insérer le nœud après p et transférer la valeur de nœud p dans le nouvel élément.
17 -6 -5 9 34 Noter qu’avec cette solution le pointeur p qui pointait vers le nœud contenant 9 avant l’insertion du nouvel élément, pointe vers le nœud contenant –5 après l’insertion du nouvel élément.
Suppression dans une liste chaînée … On ne peut pas détruire un élément dans une liste vide. Suppression en tête de liste -5 15 35 45 l Temp l := l-> Suivant Si on se content de cette instruction, le programme a logiquement éliminé de la liste le premier bloc, mais celui-ci est toujours en mémoire. De ce fait, il occupe inutilement de la place mémoire. Il faut donc libérer cette place mémoire.
La fonction free, en langage C, permet de libérer cette place mémoire La fonction free, en langage C, permet de libérer cette place mémoire. Cette fonction prend un paramètre en l’occurrence, le pointeur de bloc qui doit être supprimé. Temp := l l := l-> Suivant Libérer (Temp) (Temp ne pointe sur rien du tout)
Suppression en fin de liste a)- Parcours de la liste à le recherche du dernier élément (sous réserve d’avoir en mémoire l’adresse du dernier élément). b)- L’avant dernier bloc de la liste devient le dernier. c)- On libère le bloc à supprimer. dernier := l TQ (dernier-> Suivant) <> nil FAIRE l := dernier dernier := dernier-> Suivant FTQ l-> Suivant := nil Libérer (dernier)
Cet algorithme ne fonctionne pas si la liste est vide.
Suppression à un endroit quelconque de la liste a)- Recherche du bloc qui précède le bloc à supprimer. b)- Construire le lien qui unis le bloc qui précède le bloc à détruire et le bloc qui le succède. c)- Détruire. parcours := l TQ (parcours->suivant <> R) FAIRE parcours := parcours->Suivant FTQ parcours-> Suivant := R -> Suivant Libérer ( R )
LISTES PARTICULIERES …
LISTES BILATERES … Définition Dans les listes simplement chaînées, à partir d'un nœud donné, on ne peut accéder qu'au successeur de ce nœud. Dans une liste doublement chaînée (ou bilaterè), à partir d'un nœud donné, on peut accéder au nœud successeur et au nœud prédécesseur. Les éléments d’une liste bilatère contiennent 2 pointeurs : *un pointeur sur le bloc suivant *un pointeur sur le bloc précédent 30 35 34 Tête Courant Queue
Déclaration des types de données nécessaires struct elem { int valeur ; struct elem * suivant; struct elem *precedent; } ; typedef struct elem * liste;
Construction De La Liste A)- liste vide l : =NIL B)- Ajout du premier élément allouer(l) l->suivant:=NIL L->prec=NULL entrer (l->valeur) C)- Ajout entête de liste si la liste n’est pas vide. ALLOUER (laux) laux->suivant : =l l->précédent : = laux laux->précédent : = NIL laux->valeur : = 1 l : =laux
D)- Ajout en fin de liste dans les listes bilatères. But : insérer une valeur dans une liste triée en ordre croissant a)- Phase de parcours (à la recherche du dernier bloc). b)- Création du nouveau bloc (à insérer). d)- Mise à jour des liens.
laux : =l ; TQ laux->suivant <> NIL faire laux: =laux->suivant FTQ Allouer (nouveau) Entrer (nouveau-> Valeur) nouveau->suivant : = NIL nouveau->précédent : = laux Laux->suivant : = nouveau
E)- Ajout à un endroit qcq de la liste. 20 10 23 12 Courant
laux : = l TQ laux-> Valeur <>20 FAIRE laux : = laux->suivant FTQ Allouer (nouveau) Entrer (nouveau->valeur) nouveau->précédent : = laux nouveau ->suivant : = laux->suivant Laux->suivant ->précédent : = nouveau Laux->suivant : = nouveau
Destruction dans une liste bilatère … Suppression en tête de liste (*) toujours s’assurer qu’il existe 1 elt dans la liste. laux :=l l:= l->suivant //l->suivant->précédent : =NIL Dispose (laux) //Libérer (l-> Précédent) l->Précédent : = NIL
Suppression en fin de liste laux :=l TQ laux->suivant <>NIL faire laux :=laux->suivant FTQ Laux->précédent->suivant : =NIL Libérer(laux) Attention cet algorithme n’est valable que si la liste contient plus d’un élément.
Suppression à un endroit quelconque de la liste s->precedent>suivant=s-> suivant s ->suivant ->precedent: =s->precedent libérer (s).
LISTES CIRCULAIRES …
Définition Une liste où le pointeur NUL du dernier élément est remplacé par l’adresse du premier élément est appelée liste circulaire. Dans une liste circulaire tous les nœuds sont accessibles à partir de n’importe quel autre nœud. Une liste circulaire n’a pas de premier et de dernier nœud. Une liste circulaire peut être simplement chaînée ou doublement chaînée. Noter que la concaténation de deux listes circulaires peut se faire sans avoir à parcourir les deux listes.
Déclaration des types de données nécessaires typedef struct elem { int valeur ; struct elem * suivant;} element ; typedef element * anneau;