2. Algorithmique 4 - Listes

2.1. Qu'est-ce qu'une liste ?

La structure de liste est un conteneur qui stocke des objets et permet de les insérer ou de les supprimer en fonction de leur position :

  • en tête, c'est à dire en début de liste
  • en queue, c'est à dire en fin de liste
  • ou à une position donnée par la position de l'élément dans la liste

Une liste pour laquelle tout nouvel élément est ajouté en tête et tout élément est retiré en fin est appelée une file (queue en anglais), c'est une structure FIFO (First In, First Out).

Une liste pour laquelle tout nouvel élément est ajouté en queue et tout élément est retiré également en queue est appelée une pile (stack en anglais), c'est une structure LIFO (Last In, First Out).

Pile, File et leurs propriétés

2.2. Implantation d'une liste

On peut donner plusieurs implantation des listes :

Une liste possède plusieurs avantages :

Elle possède également des inconvénients :

2.3. Cas des listes simplement chaînées

On va utiliser la structure de données suivante pour implanter une liste simplement chaînée :

  1. #pragma once
  2.  
  3. // ----------------------------------------------
  4. //
  5. //      type de donnée stocké par la liste
  6. //
  7. //    il suffira de modifier ce type si on veut
  8. //      travailler sur une liste de personnes
  9. //
  10. // ----------------------------------------------
  11.  
  12. // définition du type de la valeur stockée par un élément de la liste
  13. using type_valeur = int;
  14.  
  15. // définition d'un élément (maillon) de la liste
  16. struct type_element_de_liste
  17. {
  18.     // pointeur sur l'élément suivant
  19.     type_element_de_liste *suivant;
  20.    
  21.     // valeur stockée par cet élément
  22.     type_valeur valeur;
  23. };
  24.  
  25. // définition d'une liste comme un pointeur sur un élément
  26. using liste = type_element_de_liste *;
  27.  
  28. // définition de la liste vide
  29. const liste liste_vide = nullptr;
  30.  

représentation d'un élément (maillon) d'une liste simplement chaînée

Une liste peut se réduire à l'utilisation d'éléments (maillons) chaînés entre eux, mais il est généralement préférable de définir un autre type liste qui stocke un pointeur sur le premier élément et le nombre d'éléments de la liste.

  1. typedef struct liste {
  2.  
  3.   // pointeur sur le premier élément de la liste
  4.   type_element_de_liste *premier;
  5.  
  6.   // nombre d'éléments dans la liste
  7.   int taille;
  8.  
  9. } liste;
  10.  

représentation d'une liste simplement chaînée

2.4. Variations sur les listes

Voici six exemples de l'implantation des listes sur le même thème issu du cours.

Ici, on ne dispose pas d'une structure de liste qui contient la taille de la liste et un pointeur sur le premier élément. Une liste est ici un pointeur sur un élément comme en cours.

2.4.1. Implantation à base de pointeurs de pointeurs

2.4.2. Implantation à base de pointeurs

Les deux dernières versions sont probablement les plus sympathiques ;-) car :

  1. on n'utilise que des pointeurs et non des pointeurs de pointeurs ou des références sur pointeur
  2. on n'a les mêmes prototypes (signatures) de sous-programme en itératif ou en récursif

2.4.3. Application

Voici un exemple qui reprend l'implantation itérative avec des listes pour une personne qui est un struct composé de deux chaînes : nom et prénom.

Il est nécessaire de redéfinir deux opérateurs pour une personne :

Voir le code liste de personnes.

2.4.4. Listes et récursion

La récursion sur les listes n'est pas une bonne chose en général.

En effet, définir un traitement récursif sur l'ajout en queue peut provoquer une saturation de la pile d'exécution du programme ou ralentir les traitements.

Heureusement, le compilateur gcc/g++ dérécursive la grande majorité des sous-programmes récursifs. Un simple test montre par exemple que sur AMD Ryzen 5600 G, si on utilise effectivement la récursion (il faut compiler avec g++ -O1), l'ajout en queue de $200\_000$ entiers dans une liste prend $50$ secondes alors que sans récursion on ne met plus que $18$ secondes.

Un autre test montre que si on utilise une structure de personne (nom, prénom et téléphone sous forme de string, comme en TP), l'ajout de la 43662 ième personne provoque une erreur de segmentation car la pile est saturée. En effet, on passe en paramètres de la fonction nom, prénom et téléphone à chaque appel de fonction.

En architecture 64 bits, la pile est initialement dans la méthode main à la valeur 238024d0_h . Elle descend jusqu'à 230047d0_h, soit une différence de 7fdd00_h $= 8\_379\_648_{10}$. Or la pile occupe 8 Mo soit $8\_388\_608$ octets, et, une fois dans la méthode main, on a déjà utilisé une partie de la pile. On a donc bien saturé la pile avec ces nombreux appels récursifs.