Vous êtes sur la page 1sur 109

Algorithmique

Partie C. Listes chaı̂nées

Université de Nantes
Département Informatique
2023/2024
Guillaume Cantin
guillaume.cantin@univ-nantes.fr
Retour sur les tableaux
Avantages et inconvénients des tableaux

I Avantages :
accès rapide par indice ;
stockage de plusieurs valeurs de façon consécutive (efficace).

I Inconvénients :
la mémoire est bloquée même pour une occupation partielle ;
la suppression d’un élément implique le décalage des éléments
suivants ;
l’insertion force le décalage des éléments suivants ;
le changement de taille nécessite de recopier tous les éléments.

1 / 57
1. Listes chaı̂nées
Listes chaı̂nées : définition

I Une liste chaı̂née est une structure de données :


homogène (les données sont de même type),
dynamique (autorisant un changement de taille).

I La représentation d’une liste chaı̂née comprend :


la tête, qui indique (pointe vers) le début de la liste,
les nœuds (ou maillons, cellules).

I Un nœud contient :
le champ de données (type primitif ou enregistrement),
le lien vers le nœud suivant.

2 / 57
Image intuitive

I On peut se représenter intuitivement une liste chaı̂née comme un


train :
la locomotive correspond à la tête de la liste,
les wagons contiennent les ressources (données),
les wagons sont liés les uns aux autres (pointeurs).

3 / 57
Image intuitive

I On peut toutefois imaginer un grand espace entre deux nœuds


consécutifs, selon l’état de la mémoire.

Schéma des emplacements de mémoire pour une liste chaı̂née

• octet libre, ◦ octet occupé, • • • • A • • • • liste chaı̂née.


4 / 57
Image intuitive

I On peut toutefois imaginer un grand espace entre deux nœuds


consécutifs, selon l’état de la mémoire.

Schéma des emplacements de mémoire pour une liste chaı̂née

• octet libre, ◦ octet occupé, • • • • A • • • • liste chaı̂née.


4 / 57
Listes chaı̂nées en algorithmique

I On définit un type t noeud (ou t maillon, ou t cellule) pour


représenter un nœud :

Type t_noeud = e n r e g i s t r e m e n t
t_data data
pointeur vers t_noeud suivant
fin e n r e g i s t r e m e n t

I Le type t data de la donnée data1 peut correspondre à un type


primitif, à un enregistrement...
I Exemples :

Type t_data = e n r e g i s t r e m e n t Type t_data = e n r e g i s t r e m e n t


chaine nom r é el absc
r é el taille r é el ordo
fin e n r e g i s t r e m e n t fin e n r e g i s t r e m e n t
t_data nain ← ( " Bombur " , 1 . 10 ) t_data cplx ← (1 , 1 )

1
Usage abusif du latin data (données), pluriel de datum (donnée). 5 / 57
Tête et queue d’une liste chaı̂née

I La tête de la liste est un pointeur vers le premier nœud :

Pointeur vers t_noeud t ê te

I La queue de la liste est très souvent repérée par un pointeur nul.

6 / 57
2. Insertion dans une liste chaı̂née
Insertion en tête

I On considère une liste chaı̂née. On souhaite ajouter un nœud en


tête de la liste :

7 / 57
Insertion en tête

I On considère une liste chaı̂née. On souhaite ajouter un nœud en


tête de la liste :

7 / 57
Insertion en tête

I On considère une liste chaı̂née. On souhaite ajouter un nœud en


tête de la liste :

I Schéma de l’insertion en tête :

7 / 57
Algorithme d’insertion en tête

I L’insertion en tête nécessite :


d’allouer un nouveau nœud,
d’y ranger la nouvelle donnée,
de chaı̂ner le nouveau nœud avec l’ancienne tête,
d’actualiser la tête.

// Proc é dure d ’insertion en t ê te


procédure insereTeteProc ( m pointeur vers t_noeud ancienN ,
e t_data nouvelleD )
v ariable : pointeur vers t_noeud nouveauN
début
nouveauN ← allocation ( t_noeud ) // allocation dynamique
m é moire ( nouveauN ). data ← nouvelleD
m é moire ( nouveauN ). suivant ← ancienN
ancienN ← nouveauN
fin

8 / 57
Algorithme d’insertion en tête

On peut également envisager une fonction d’insertion en tête, qui


retourne un pointeur vers la nouvelle tête.

// Fonction d ’insertion en t ê te
f o n c t i o n insereTeteFonc ( pointeur vers t_noeud ancienN ,
t_data nouvelleD ) : pointeur vers t_noeud
v ariable : pointeur vers t_noeud nouveauN ;
début
nouveauN ← allocation ( t_noeud ) // allocation dynamique
m é moire ( nouveauN ). data ← nouvelleD
m é moire ( nouveauN ). suivant ← ancienN
r e t o u r n e r nouveauN
fin
}

L’allocation dynamique permet de conserver l’emplacement réservé


pour le nouveau t noeud en fin de procédure ou fonction.
I Nous verrons plus loin comment effectuer la désallocation.
9 / 57
Insertion en tête en C++

// Structure t_noeud
struct t_noeud {
t_data data ;
t_noeud * suivant ;
};

// Proc é dure d ’ insertion en t ê te


void insereTeteProc ( t_noeud * & ancienN , t_data nouvelleD ){
t_noeud * nouveauN ;
nouveauN = new t_noeud ;
nouveauN - > data = nouvelleD ;
nouveauN - > suivant = ancienN ;
ancienN = nouveauN ;
}

// Fonction d ’ insertion en t ê te
t_noeud * insereTeteFonc ( t_noeud * teteListe , t_data nouvelleD ){
t_noeud * nouveauN ;
nouveauN = new t_noeud ;
nouveauN - > data = nouvelleD ;
nouveauN - > suivant = ancienN ;
return nouveauN ;
}

Syntaxe pour passer un pointeur en modification : * & ptr.


10 / 57
Construction d’une liste chaı̂née par insertions en tête succes-
sives

I On commence avec un pointeur nul, puis on répète les insertions


en tête.

procédure saisieListe ( m pointeur vers t_noeud L )


variables :
caract è re choix
t_data D
début
L ← NULL // On commence avec un pointeur nul
é crire " Voulez - vous saisir un nouveau noeud ( o / n ) ? "
lire choix
tant que ( choix = ’ o ’ ) faire
saisieDonnee ( D ) // Proc é dure appel é e pour saisir une donn é e en sortie
ins ereTeteProc (L , D ) // ou L ← insereTeteFonc (L , D )
é crire " Voulez - vous saisir un nouveau noeud ( o / n ) ? "
lire choix
fin tant que
fin

11 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

12 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

12 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

A A

12 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

A A A

12 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

A A A A

12 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

A A A A A

12 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

A A A A A A

12 / 57
Inconvénient des insertions en tête successives

I L’insertion en tête modifie la tête.

A A A A A A

I Avec cette méthode, la liste est  à l’envers  ...


Comment obtenir la liste  à l’endroit  ?

12 / 57
Insertion en queue

I L’insertion en queue nécessite de se déplacer le long de la chaı̂ne


en partant de la tête.

13 / 57
Insertion en queue

I Une fois arrivé en bout de liste :


on alloue un nouveau nœud,
on y range la nouvelle donnée,
on réalise le chaı̂nage avec le nouveau nœud.

14 / 57
Algorithme d’insertion en queue

// Proc é dure d ’insertion en queue


procédure insereQueueProc ( m pointeur vers t_noeud pTete ,
e t_data nouvelleD )
v a r i a b l e s : pointeur vers t_noeud nouveauN
pointeur vers t_noeud p
début
nouveauN ← allocation ( t_noeud )
m é moire ( nouveauN ). data ← nouvelleD
m é moire ( nouveauN ). suivant ← NULL
si ( pTete = NULL ) alors
pTete ← nouveauN // modification de pTete /!\
sinon
p ← pTete
tant que ( m é moire ( p ). suivant != NULL ) faire
p←m é moire ( p ). suivant
fin tant que
m é moire ( p ). suivant ← nouveauN
fin si
fin

Le statut en sortie du premier argument est nécessaire pour le cas


où la liste est vide.
15 / 57
Le parcours de la liste

Les instructions suivantes permettent un parcours complet de la


liste :
p ← pTete
tant que ( m é moire ( p ). suivant != NULL ) faire
p←m é moire ( p ). suivant )
fin tant que // p pointe sur la queue

La boucle de type tant que peut être traduite en C++ des deux
façons suivantes :
// solution 1
p = pTete
while (p - > suivant != nullptr ){
p = p - > suivant ;
}

// solution 2
for ( p = pTete ; p - > suivant != nullptr ; p =p - > suivant ){
// Rien de sp é cial , on se d é cale
}

16 / 57
Construction d’une liste chaı̂née par insertions en queue suc-
cessives

I On commence avec un pointeur nul, puis on répète les insertions


en queue.

procédure s ai s ie L i st e En d r oi t ( s pointeur vers t_noeud L )


variables :
caract è re choix
t_data D
début
L ← NULL // On commence avec un pointeur nul
é crire " Voulez - vous saisir un nouveau noeud ( o / n ) ? "
lire choix
tant que ( choix = ’ o ’ ) faire
saisieDonnee ( D ) // Proc é dure appel é e pour saisir une donn é e en sortie
i ns ereQueueProc (L , D ) // <- seule diff é rence avec la construction
// par insertions en t ê te successives
é crire " Voulez - vous saisir un nouveau noeud ( o / n ) ? "
lire choix
fin tant que
fin

17 / 57
Effet des insertions en queue successives

18 / 57
Effet des insertions en queue successives

18 / 57
Effet des insertions en queue successives

A A

18 / 57
Effet des insertions en queue successives

A A A

18 / 57
Effet des insertions en queue successives

A A A A

18 / 57
Effet des insertions en queue successives

A A A A A

18 / 57
Effet des insertions en queue successives

A A A A A A

18 / 57
Effet des insertions en queue successives

A A A A A A

I Avec cette méthode, la liste est  à l’endroit  ...

Est-il possible d’insérer autre part que en tête ou en queue ?

18 / 57
Insertion dans l’ordre

I S’il existe une relation d’ordre sur les données, on peut effectuer
une insertion qui maintient la liste ordonnée.
I Cela nécessite de se déplacer le long de la liste avec deux pointeurs
temporaires p et q, pour trouver la bonne position où insérer le
nouveau nœud.
Une fois cette position trouvée :
on alloue un nouveau nœud,
on réalise les chaı̂nages nécessaires.
I Si la liste est vide, ou si la liste ne contient qu’un nœud, le traite-
ment peut être différent.
On peut alors construire une liste de données ordonnées.

19 / 57
Schéma de l’insertion dans l’ordre

L 181 253 457 2014

20 / 57
Schéma de l’insertion dans l’ordre

L 181 253 457 2014

384
p q

L 181 253 457 2014

20 / 57
Schéma de l’insertion dans l’ordre

L 181 253 457 2014

384
p q

L 181 253 457 2014

384

p q

L 181 253 457 2014

20 / 57
Schéma de l’insertion dans l’ordre

L 181 253 457 2014

384
p q

L 181 253 457 2014

384

p q

L 181 253 457 2014

384

p q

L 181 253 457 2014

20 / 57
3. Parcours, chaı̂nage, fusion
Parcours

I Un parcours complet d’une liste a déjà été effectué pour l’insertion


en queue, moyennant un seul pointeur temporaire.
I Un parcours complet permet par exemple de compter le nombre
de nœuds dans une liste non vide.
I Le parcours peut aussi être partiel, par exemple pour réaliser une
insertion dans l’ordre, moyennant deux pointeurs temporaires.
I De façon générale, parcourir une liste permet d’effectuer une
recherche dans cette liste.

21 / 57
Recherche dans une liste

f o n c t i o n egaliteDonnees ( t_data D1 , t_data D2 ) : booléen

// Fonction de recherche d ’une donn é e dans une liste


f o n c t i o n rechercheDonnee ( pointeur vers t_noeud L , t_data D ) : booléen
variables :
booléen trouve
pointeur vers t_noeud p
début
si ( L = NULL ) faire // liste vide
trouve ← faux
sinon
p ← L
trouve ← egaliteDonnees ( m é moire ( p ). data , D );
tant que ( trouve = faux et m é moire ( p ). suivant != NULL ) faire
p ← m é moire ( p ). suivant
trouve ← egaliteDonnees ( m é moire ( p ). data , D )
fin tant que
fin si
r e t o u r n e r trouve
fin

Nombreuses variantes possibles (exemple : chercher une donnée


possédant un même attribut).
22 / 57
Chaı̂nage de deux listes

I On souhaite chaı̂ner deux listes L1, L2.

L1 data 1 data 2 data 3 L2 data 4 data 5

23 / 57
Chaı̂nage de deux listes

I On souhaite chaı̂ner deux listes L1, L2.

L1 data 1 data 2 data 3 L2 data 4 data 5

I Si p pointe vers le dernier nœud de L1, on peut chaı̂ner L1 et L2


en modifiant mémoire(p).suivant.

L1 data 1 data 2 data 3 L2 data 4 data 5

23 / 57
Chaı̂nage de deux listes

I On souhaite chaı̂ner deux listes L1, L2.

L1 data 1 data 2 data 3 L2 data 4 data 5

I Si p pointe vers le dernier nœud de L1, on peut chaı̂ner L1 et L2


en modifiant mémoire(p).suivant.

L1 data 1 data 2 data 3 L2 data 4 data 5

p L2

L1 data 1 data 2 data 3 data 4 data 5

23 / 57
Chaı̂nage de deux listes

I On souhaite chaı̂ner deux listes L1, L2.

L1 data 1 data 2 data 3 L2 data 4 data 5

I Si p pointe vers le dernier nœud de L1, on peut chaı̂ner L1 et L2


en modifiant mémoire(p).suivant.

L1 data 1 data 2 data 3 L2 data 4 data 5

p L2

L1 data 1 data 2 data 3 data 4 data 5

I /!\ Attention : cette opération modifie L1.


23 / 57
Chaı̂nage de deux listes

Pour préserver L1, on peut créer une nouvelle liste L3 qui reçoit
les éléments de L1 puis ceux de L2 (voir TD C, exercice 2).

L1 data 1 data 2 data 3 L2 data 4 data 5

L3 data 1 data 2 data 3 data 4 data 5

24 / 57
Chaı̂nage de deux listes

Pour préserver L1, on peut créer une nouvelle liste L3 qui reçoit
les éléments de L1 puis ceux de L2 (voir TD C, exercice 2).

L1 data 1 data 2 data 3 L2 data 4 data 5

L3 data 1 data 2 data 3 data 4 data 5

On peut compléter l’opération de chaı̂nage en supprimant les


éventuels doublons.

24 / 57
Fusion de deux listes ordonnées

I On suppose maintenant que L1 et L2 sont ordonnées. On souhaite


réaliser leur fusion, tout en préservant l’ordre des données.

L1 126 268 345 L2 197 205

25 / 57
Fusion de deux listes ordonnées

I On suppose maintenant que L1 et L2 sont ordonnées. On souhaite


réaliser leur fusion, tout en préservant l’ordre des données.

L1 126 268 345 L2 197 205

L3 126 197 205 268 345

25 / 57
Fusion de deux listes ordonnées

I On suppose maintenant que L1 et L2 sont ordonnées. On souhaite


réaliser leur fusion, tout en préservant l’ordre des données.

L1 126 268 345 L2 197 205

L3 126 197 205 268 345

Solution naı̈ve : chaı̂ner puis trier... méthode très coûteuse !

25 / 57
Fusion de deux listes ordonnées

I On suppose maintenant que L1 et L2 sont ordonnées. On souhaite


réaliser leur fusion, tout en préservant l’ordre des données.

L1 126 268 345 L2 197 205

L3 126 197 205 268 345

Solution naı̈ve : chaı̂ner puis trier... méthode très coûteuse !


Autre solution : insérer dans l’ordre les éléments de L2 dans une
copie de L1... mais les insertions dans l’ordre nécessitent de parcourir
la liste, ce qui peut augmenter le coût !
Bonne méthode : on parcourt  simultanément  L1 et L2. Le
plus petit des deux éléments est ajouté à L3, et on se décale dans
la liste où cet élément a été trouvé (voir TD C, exercice 3).
25 / 57
4. Suppression d’un élément,
suppression totale
Suppression de la tête

I La suppression de la tête nécessite de :


considérer un pointeur temporaire,
copier la tête dans le pointeur temporaire,
déplacer la tête,
désallouer le pointeur temporaire.
I  Désallouer un pointeur  signifie qu’on désalloue l’emplacement
mémoire vers lequel pointe le pointeur.

26 / 57
Suppression de la tête

L data 1 data 2 data 3 data 4

27 / 57
Suppression de la tête

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

27 / 57
Suppression de la tête

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

27 / 57
Suppression de la tête

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

27 / 57
Suppression de la tête

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

L data 1 data 2 data 3 data 4

L data 2 data 3 data 4

27 / 57
Suppression de la tête

procédure suppressionTete ( s pointeur vers t_noeud L)


variable :
pointeur vers t_noeud temp
début
si ( L != NULL ) faire
temp ← L
L ← m é moire ( L ). suivant
d é sallocation ( temp )
fin si
fin

I Il est important de décaler la tête avant de désallouer l’emplacement


mémoire sur lequel pointe le pointeur temporaire p ! (pourquoi ?
que se passerait-il sinon ? ...)

28 / 57
Suppression totale de la liste

I La suppression totale de la liste consiste à itérer la suppression de


la tête jusqu’à obtenir une liste vide.

procédure su pp res si onT ot ale ( s pointeur vers t_noeud L)


variable :
pointeur vers t_noeud p
début
tant que ( L != NULL ) faire
p ← L
L ← m é moire ( L ). suivant
d é sallocation ( p )
fin tant que
fin

I En fin d’opération, il est vivement conseillé de s’assurer que la


liste L a la valeur NULL.

29 / 57
Exemple d’exécution

I On considère l’algorithme principal suivant :

algorithme principal
variable :
pointeur vers t_noeud L
t_data nain
début
1 nain ← ( " Bombur " , 1 . 10 )
2 L ← NULL
3 insereTeteProc (L , nain )
4 é crire ( m é moire ( L ). data ). nom
5 é crire L , m é moire ( L ). suivant
6 sup pr ess io nTo ta le ( L )
fin

30 / 57
algorithme principal
variable :
pointeur vers t_noeud L
t_data nain
début
1 nain ← ( " Bombur " , 1 . 10 )
2 L ← NULL
3 insereTeteProc (L , nain )
4 é crire ( m é moire ( L ). data ). nom
5 é crire L , m é moire ( L ). suivant
6 sup pr ess io nTo ta le ( L )
fin

@1 @34 @73
Ligne L nain Remarques, affichages
début ? ? Allocations de L et nain
1 ? (”Bombur”, 1.10)
2 NULL (”Bombur”, 1.10)
3 @73 (”Bombur”, 1.10) ((”Bombur”, 1.10), NULL) Appel → allocation dynamique d’un t noeud.
4 @73 (”Bombur”, 1.10) ((”Bombur”, 1.10), NULL) Affiche : Bombur
5 @73 (”Bombur”, 1.10) ((”Bombur”, 1.10), NULL) Affiche : @73, NULL
6 NULL (”Bombur”, 1.10) Appel → désallocation dynamique du t noeud.
fin Désallocations de L et nain

31 / 57
Simulation de l’exécution

@1 @34 @73

Ligne L nain Remarques, affichages

début ? ? Allocations de L et nain

1 ? (”Bombur”, 1.10)

2 NULL (”Bombur”, 1.10)

3 @73 (”Bombur”, 1.10) ((”Bombur”, 1.10), NULL) Appel → allocation dynamique d’un t noeud.

4 @73 (”Bombur”, 1.10) ((”Bombur”, 1.10), NULL) Affiche : Bombur

5 @73 (”Bombur”, 1.10) ((”Bombur”, 1.10), NULL) Affiche : @73, NULL

6 NULL (”Bombur”, 1.10) Appel → désallocation dynamique du t noeud.

fin Désallocations de L et nain

I Il est important d’initialiser la liste avec L ← NULL.


I On vérifie que l’opération de libération attribue à nouveau la valeur
NULL à la liste L.

32 / 57
Suppression de la queue

I Pour supprimer la queue d’une liste chaı̂née, on doit :


s’assurer que la liste n’est pas vide,
parcourir la liste avec deux pointeurs p et q qui se suivent,
supprimer le dernier nœud,
adapter le dernier chaı̂nage.
I On doit distinguer deux cas, selon que la liste contient un seul
nœud ou plusieurs.

33 / 57
Suppression de la queue

I Algorithme de suppression de la queue :

procédure suppression Queue ( s pointeur vers t_noeud L )


variables
pointeurs vers t_noeud p , q
début
si ( L != NULL ) faire // Si la liste n ’est pas vide
p ← L
q ← m é moire ( p ). suivant
si ( q = NULL ) faire // S ’il n ’y a qu ’un seul noeud , on le d é truit
d é sallouer p
L ← NULL
sinon // Sinon , on se d é cale avec p jusqu ’ à l ’avant - dernier noeud
// et avec q jusqu ’au dernier .
tant que ( m é moire ( q ). suivant != NULL ) faire
p ← m é moire ( p ). suivant ;
q ← m é moire ( p ). suivant ;
fin tant que
d é sallouer ( q ) // On supprime le dernier noeud
m é moire ( p ). suivant ← NULL // On adapte le dernier chainage .
fin si
fin si
fin

34 / 57
Suppression de la queue

L data 1 data 2 data 3 data 4

35 / 57
Suppression de la queue

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3 data 4

35 / 57
Suppression de la queue

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3 data 4

35 / 57
Suppression de la queue

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3 data 4

35 / 57
Suppression de la queue

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3 data 4

35 / 57
Suppression de la queue

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3 data 4

p q

L data 1 data 2 data 3

35 / 57
Remarques

I Un seul pointeur temporaire peut suffire pour réaliser la suppres-


sion de la queue, mais la syntaxe est plus lourde.
I La suppression de la queue oblige un parcours complet, qui est
coûteux.
On peut diminuer le coût temporel de l’opération en considérant
un type qui stocke la queue :

Type ListeTeteQueue = e n r e g i s t r e m e n t
pointeur vers t_noeud tete
pointeur vers t_noeud queue
fin e n r e g i s t r e m e n t

I En C++, la syntaxe mémoire(p).suivant peut être traduite par


p->suivant, qui équivaut à (*p).suivant.
36 / 57
Suppression de la queue en C++

void s u p p ressionQueue ( t_noeud * & L ){


t_noeud *p , * q ;
if ( L != nullptr ){ // Si la liste n ’ est pas vide
p = L;
q = p - > suivant ;
if ( q == nullptr ){ // S ’ il n ’y a qu ’ un seul noeud , on le d é truit
delete p ;
L = nullptr ;
}
else { // Sinon , on se d é cale avec p jusqu ’à l ’ avant - dernier noeud
// et avec q jusqu ’ au dernier .
while (q - > suivant != nullptr ){
p = p - > suivant ;
q = p - > suivant ;
}
delete q ; // On supprime le dernier noeud .
p - > suivant = nullptr ; // On adapte le dernier chainage .
}
}
}

37 / 57
Exemple en C++ : des listes de caractères

struct t_car {
char carac ;
t_car * suivant ; // Ex é cution
}; Entrez un mot :
using mot = t_car *; infor
void insertionQueue ( mot & m , char c );
void saisie ( mot & m ); Entrez un mot :
mot concat ( mot m1 , mot m2 ); matique
void afficher ( mot m );
void afficher_complet ( mot m ); [0 x55d3b37e5850 ] i
void efface ( mot & m ); [0 x55d3b37e5870 ] n
[0 x55d3b37e5890 ] f
int main (){ [0 x55d3b37e58b0 ] o
mot m1 , m2 , m3 ; [0 x55d3b37e58d0 ] r
saisie ( m1 ); [0 x55d3b37e58f0 ] m
saisie ( m2 ); [0 x55d3b37e5910 ] a
m3 = concat ( m1 , m2 ); [0 x55d3b37e5930 ] t
afficher_c omplet ( m3 ); [0 x55d3b37e5950 ] i
efface ( m1 ); [0 x55d3b37e5970 ] q
efface ( m2 ); [0 x55d3b37e5990 ] u
efface ( m3 ); [0 x55d3b37e59b0 ] e
return 0;
}

38 / 57
Opérations d’insertion et de saisie

void insertionQueue ( mot & m , char c ){


mot nv , p ;
nv = new t_car ;
nv - > carac = c ;
nv - > suivant = nullptr ;
if ( m == nullptr ){
m = nv ;
} else {
p = m;
while (p - > suivant != nullptr ){
p = p - > suivant ;
}
p - > suivant = nv ;
}
}

void saisie ( mot & m ){


char c ;
m = nullptr ;
cout << " Entrez un mot : " << endl ;
cin >> c ;
while ( c != ’\ n ’ ){
insertionQueue (m , c );
cin . get ( c );
}
}
39 / 57
Concaténation

mot concat ( mot m1 , mot m2 ){


mot m , p ;
m = nullptr ;
if ( m1 == nullptr ){
m = m2 ;
} else {
p = m1 ;
while ( p != nullptr ){
insertionQueue (m , p - > carac );
p = p - > suivant ;
}
p = m2 ;
while ( p != nullptr ){
insertionQueue (m , p - > carac );
p = p - > suivant ;
}
}
return m ;
}

40 / 57
Affichage, libération

void afficher ( mot m ){


mot p ;
p = m;
while ( p != nullptr ){
cout << p - > carac ;
p = p - > suivant ;
}
cout << endl ;
}
void afficher_compl et ( mot m ){
mot p ;
p = m;
while ( p != nullptr ){
cout << " [ " << p << " ] " << p - > carac << endl ;
p = p - > suivant ;
}
cout << endl ;
}
void efface ( mot & m ){
mot p ;
while ( m != nullptr ){
p = m;
m = m - > suivant ;
delete p ;
}
}
41 / 57
Remarques

I En phase de développement, il peut être utile d’afficher les allo-


cations et les désallocations :

void insertionQueue ( mot & m , char c ){


...
nv = new t_car ;
cout << " Allocation . " << endl ;
...
}

void efface ( mot & m ){


...
delete p ;
cout << " D é sallocation . " << endl ;
...
}

42 / 57
5. Autres types de listes chaı̂nées
Listes doublement chaı̂nées

I On peut considérer des listes doublement chaı̂nées, définies à par-


tir du type suivant :

Type t_noeud = e n r e g i s t r e m e n t
t_data data
pointeur vers t_noeud suivant
pointeur vers t_noeud precedent
fin e n r e g i s t r e m e n t

Les opérations d’insertion nécessitent alors de définir deux chaı̂nages.


Exemple : insertion en tête dans une liste doublement chaı̂née.

43 / 57
Algorithme d’insertion en tête dans une liste doublement
chaı̂née

// Proc é dure d ’insertion en t ê te dans une liste doublement cha ı̂ n é e


procédure i n s e r e T e t e P r o c D o u b l e ( s pointeur vers t_noeud ancienN ,
e t_data nouvelleD )
v ariable : pointeur vers t_noeud nouveauN
début
nouveauN ← allocation ( t_noeud ) // allocation dynamique
m é moire ( nouveauN ). data ← nouvelleD
m é moire ( nouveauN ). suivant ← ancienN
m é moire ( nouveauN ). precedent ← NULL
si ancienN != NULL alors
m é moire ( ancienN ). precedent ← nouveauN
fin si
ancienN ← nouveauN
fin

On adapte de la même façon l’opération d’insertion en queue


(voir TD C, exercice 4).
44 / 57
Listes et cycles

I On peut considérer des listes circulaires (simplement ou double-


ment chaı̂nées).

Type ListeCirculaire = pointeur vers t_noeud

Dans une telle liste, le dernier nœud data 2

pointe vers la tête. L data 1

I La condition d’arrêt du parcours d’une


telle liste circulaire ne doit pas chercher data 3

un pointeur nul !
I On peut aussi rechercher des cycles dans une liste censée être non
circulaire (voir TD C, exercice 5).

45 / 57
6. Application des listes chaı̂nées
Piles et files

I Les listes chaı̂nées sont très utiles pour implémenter des structures
telles que les piles et les files.
Une pile est une structure qui répond au principe  dernier arrivé,
premier sorti  (en anglais LIFO pour last in, first out).
Une file est une structure qui répond au principe  premier arrivé,
premier sorti  (en anglais FIFO pour first in, first out).

sommet de la pile dernier

...

tête de la file

premier premier ... dernier

46 / 57
Piles et files

sommet de la pile dernier

...

tête de la file

premier premier ... dernier

I Sur ce schéma :
on entre dans la pile et on sort de la pile par le sommet,
on entre dans la file par la queue, on en sort par la tête.
I Ces structures seront étudiées en ASD1 puis en ASD2.
I On peut tout à fait implémenter les piles et les files en utilisant
des tableaux. 47 / 57
Exemple d’application : les polynômes

I On peut implémenter les polynômes en utilisant des listes chaı̂nées.


Exemple : soit P le polynôme défini par
P = 1 + 3x − 4x2 .
On crée une liste ordonnée contenant les monômes de P :

P 1 3x −4x2

On considère donc les types t monome et t polynome suivants :

type t_monome = e n r e g i s t r e m e n t
coeff : R é el
puiss : Entier
suivant : Pointeur vers t_monome
fin e n r e g i s t r e m e n t
type t_polynome = Pointeur vers t_monome

48 / 57
Opérations sur les polynômes

I On peut alors implémenter les opérations suivantes :


initialisation d’un polynôme,
saisie d’un monôme,
saisie d’un polynôme par insertions dans l’ordre successives,
affichage d’un polynôme,
somme de deux polynômes P + Q,
produit de deux polynômes P × Q,
composition de deux polynômes P ◦ Q,
destruction d’un polynôme,
etc...

49 / 57
Somme de deux polynômes

Fonction somme ( t_polynome P , t_polynome Q ) : t_polynome


variables
S : t_polynome
temp1 , temp2 : Pointeurs vers t_monome
début
temp1 ← P // temp1 pointe sur le premier mon ô me de P
temp2 ← Q // temp2 pointe sur le premier mon ô me de Q
init_poly ( S ) // on initialise S
tant que ( temp1 != NULL et temp2 != NULL ) faire
// on compare les degr é s des mon ô mes
si ( m é m ( temp1 ). puiss < m é m ( temp2 ). puiss ) alors
insereQueue (S , m é m ( temp1 ). coeff , m é m ( temp1 ). puiss )
temp1 ← m é m ( temp1 ). suivant // on se d é cale
sinon si ( m é m ( temp1 ). puiss > m é m ( temp2 ). puiss ) alors
insereQueue (S , m é m ( temp2 ). coeff , m é m ( temp2 ). puiss )
temp2 ← m é m ( temp2 ). suivant // on se d é cale
sinon // on a 2 mon ô mes de m ê me degr é
si ( m é m ( temp1 ). coeff + m é m ( temp2 ). coeff != 0) alors
// on effectue une addition
insereQueue (S , m é m ( temp1 ). coeff + m é m ( temp2 ). coeff , m é m ( temp2 ). puiss )
fin si
temp1 ← m é m ( temp1 ). suivant // on se d é cale
temp2 ← m é m ( temp2 ). suivant // on se d é cale
fin si sinon
fin tant que
... // ce n ’est pas tout à fait fini ...

50 / 57
Somme de deux polynômes

... // suite de l ’algorithme


si ( temp1 = NULL ) alors
// si on est arriv é à la fin de P
tant que ( temp2 != NULL ) faire
// mais qu ’on n ’est pas à la fin de Q
insereQueue (S , m é m ( temp2 ). coeff , m é m ( temp2 ). puiss )
temp2 ← m é m ( temp2 ). suivant // on se d é cale
fin tant que
fin si
si ( temp2 = NULL ) alors
// si on est arriv é à la fin de Q
tant que ( temp1 != NULL ) faire
// mais qu ’on n ’est pas à la fin de P
insereQueue (S , m é m ( temp1 ). coeff , m é m ( temp1 ). puiss )
temp1 ← m é m ( temp1 ). suivant // on se d é cale
fin tant que
fin si
retourner S
fin // de l ’op é ration

51 / 57
7. Récursivité
Récursivité

I Une fonction est dite récursive si elle s’appelle elle-même.


I Exemple. La fonction C++ suivante calcule la factorielle n! d’un
entier n, à partir de la relation de récurrence n! = (n − 1)! × n :

int F a c t o r i e l l e R e c u r s i v e ( int n ){
if ( n <= 1)
return 1;
else
return n * F a c t o r i e l l e R e c u r s i v e (n -1); // appel r é cursif
}

I Application : l’instruction FactorielleRecursive(5)


provoque l’appel à FactorielleRecursive(4),
qui provoque l’appel à FactorielleRecursive(3),
qui provoque l’appel à FactorielleRecursive(2),
qui provoque l’appel à FactorielleRecursive(1) !
52 / 57
Factorielle d’un entier

I Exemple d’exécution avec affichages supplémentaires :

# include < iostream >


using namespace std ;

int F a c t o r i e l l e R e c u r s i v e ( int n ){
if ( n <= 1){
return 1;
} else {
cout << " Appel de F a c t o r e l l e R e c u r s i v e ( " << n -1 << " ) " << endl ;
return n * F a c t o r i e l l e R e c u r s i v e (n -1); // appel r é cursif
}
}

int main (){


cout << F a c t o r i e l l e R e c u r s i v e (5) << endl ;
return 0;
}

// Ex é cution
guigui :∼/ home / ALGO$ ./ factorielle . out
Appel de F a c t o r i e l l e R e c u r s i v e (4)
Appel de F a c t o r i e l l e R e c u r s i v e (3)
Appel de F a c t o r i e l l e R e c u r s i v e (2)
Appel de F a c t o r i e l l e R e c u r s i v e (1)
120
53 / 57
Application aux listes chaı̂nées

I Les nœuds d’une liste chaı̂née ont une structure qui se prête na-
turellement à la programmation récursive.
I Exemple : algorithme récursif d’affichage.

procédure Aff ichageRecursif ( L : pointeur vers t_noeud )


si ( L != NULL ) alors
é crire m é m ( L ). data
L ← m é m ( L ). suivant ;
Aff ichage Recursif ( L ) // Appel r é cursif
sinon
é crire " Liste vide . "
fin

54 / 57
Algorithme récursif d’affichage d’une liste chaı̂née en C++

0x5616e95f5eb0 0x5616e95f5ee0 0x5616e95f5f10

L riri fifi loulou

void A f fi cha ge Rec ur sif ( t_noeud * L ){


if ( L != nullptr ){
cout << L << " : " << L - > donnee << endl ;
L = L - > suivant ;
cout << " Appel de Aff ic hag eR ecu rs if ( " << L << " ) " << endl ;
Af fi cha ge Rec ur sif ( L );
}
else {
cout << " Liste vide . " << endl ;
}
}
// Ex é cution de Aff ic hag eR ecu rs if ( L )
0 x5616e95f5eb0 : riri
Appel de Af fi cha ge Rec ur sif (0 x5616e95f5ee0 )
0 x5616e95f5ee0 : fifi
Appel de Af fi cha ge Rec ur sif (0 x5616e95f5f10 )
0 x5616e95f5f10 : loulou
Appel de Af fi cha ge Rec ur sif (0)
Liste vide .

55 / 57
Récursivité et listes chaı̂nées

I D’autres opérations sur les listes chaı̂nées peuvent être traitées


par une approche récursive :
la saisie,
la destruction,
la recherche,
etc...

56 / 57
Conclusion
Conclusion

I Les listes chaı̂nées sont construites avec une approche dynamique


différente des tableaux.

57 / 57
Conclusion

I Les listes chaı̂nées sont construites avec une approche dynamique


différente des tableaux.
I L’insertion et la suppression sont réalisées avec flexibilité.

57 / 57
Conclusion

I Les listes chaı̂nées sont construites avec une approche dynamique


différente des tableaux.
I L’insertion et la suppression sont réalisées avec flexibilité.
I En revanche, l’accès aux éléments nécessite un parcours de la liste.

57 / 57
Conclusion

I Les listes chaı̂nées sont construites avec une approche dynamique


différente des tableaux.
I L’insertion et la suppression sont réalisées avec flexibilité.
I En revanche, l’accès aux éléments nécessite un parcours de la liste.
I La gestion dynamique de la mémoire est une responsabilité confiée
au programmeur ou à la programmeuse.

57 / 57
Conclusion

I Les listes chaı̂nées sont construites avec une approche dynamique


différente des tableaux.
I L’insertion et la suppression sont réalisées avec flexibilité.
I En revanche, l’accès aux éléments nécessite un parcours de la liste.
I La gestion dynamique de la mémoire est une responsabilité confiée
au programmeur ou à la programmeuse.
I La structure récursive des nœuds d’une liste chaı̂née permet na-
turellement d’implémenter certaines opérations par récursivité.

57 / 57
Conclusion

I Les listes chaı̂nées sont construites avec une approche dynamique


différente des tableaux.
I L’insertion et la suppression sont réalisées avec flexibilité.
I En revanche, l’accès aux éléments nécessite un parcours de la liste.
I La gestion dynamique de la mémoire est une responsabilité confiée
au programmeur ou à la programmeuse.
I La structure récursive des nœuds d’une liste chaı̂née permet na-
turellement d’implémenter certaines opérations par récursivité.
I La récursivité peut parfois diminuer la clarté d’un algorithme.

57 / 57
Conclusion

I Les listes chaı̂nées sont construites avec une approche dynamique


différente des tableaux.
I L’insertion et la suppression sont réalisées avec flexibilité.
I En revanche, l’accès aux éléments nécessite un parcours de la liste.
I La gestion dynamique de la mémoire est une responsabilité confiée
au programmeur ou à la programmeuse.
I La structure récursive des nœuds d’une liste chaı̂née permet na-
turellement d’implémenter certaines opérations par récursivité.
I La récursivité peut parfois diminuer la clarté d’un algorithme.
I Les notions suivantes seront abordées en ASD1 puis en ASD2 :
complexité temporelle, piles, files, structures de données abstraites,
concrètes, programmation objet en C++, etc...
57 / 57

Vous aimerez peut-être aussi