Structures de données : les listes chaînées

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 13/01/2000 - Dernière mise à jour : 29/01/2001

Sommaire

Introduction
Définitions
Représentation interne des listes
Utilisation d'un vecteur
Utilisation de la mémoire dynamique
Autres opérations sur les listes
Généralisation
Structures de données et conteneurs
Généralités
Pile
Files
Autres listes
Listes doublement chaînées
Sentinelles
Listes circulaires
Listes multiples, listes de listes
Listes triées

Introduction

    On distingue quatre types de structures de données: les vecteurs, les structures séquentielles (listes linéaires), les structures arborescentes et les graphes. Nous allons étudier ici l'implémentation des listes linéaires en C++.

Définitions

    Une liste linéaire est une suite finie d'éléments repérés selon leur rang :
 
l = <e1, e2, ..., en>

    La notion d'ordre est fondamentale. Il ne s'agit pas d'un ordre sur les éléments eux-mêmes, c'est-à-dire sur leur valeur (par exemple un ordre alphabétique), mais sur les rangs de ces éléments. Par exemple on peut considérer une liste de processus à lancer sur une machine, ordonnée suivant les priorités décroissantes de ces applications, une liste de fenêtres à afficher à l'écran de la plus éloignée vers la plus proche de l'utilisateur (ordonnée selon l'axe z : Z-Order).

    Soit

    On peut donner d'une liste la définition suivante :

        Il existe k >= 0 tel que p = succk (tete (l)),
        Si n = 0, alors l est appelée une liste vide,
        Si n = 0, alors tete (l) est indéfini,
    succn (tete (l)) est indéfini.

Primitives de base

    Notons de façon formelle CListe le type d'une liste d'éléments de type CElement. Les types des listes et des éléments peuvent être des classes, des pointeurs ou des références, génériques ou pas, etc...

Accès à la tête : tete(l).
    La première primitive fondamentale est :
 
CElement CListe::GetTete (void) const;

    La fonction membre GetTete() de la classe CListe permet d'accéder au premier élément de la liste. Les spécifications formelles indiquées plus haut précisent que la tête n'est définie que si la liste n'est pas vide. La liste doit donc offrir l'une des deux fonctions membres suivantes :
 
bool CListe::EstVide (void) const;

ou
 
unsigned int CListe::GetNbreElements (void) const;

Remarques :

  1. La fonction EstVide() est probablement implémentée de la façon suivante :
  2. bool CListe::EstVide (void) const { return !GetNbreElements (); }
  3. A moins de parcourir à chaque fois la liste pour compter les éléments, ce qui est fort peu efficace, il est naturel que l'implémentation de la liste comporte une donnée membre m_NbreElements qui est le nombre d'éléments.
    L'utilisateur peut alors écrire :
 
if (!Liste.EstVide())
{
    CElement Courant = Liste.GetTete();
    // suite du traitement
}
Accès à l'élément suivant : succ(p,l)
    De nombreuses possibilités peuvent être envisagées, réalistes ou non. On peut par exemple considérer que c'est une fonctionnalité de la liste. En effet le successeur d'un élément n'a de sens que dans une liste donnée. Une première solution pourrait être :
 
CElement CListe::GetSuivant (CElement p);

Cela signifie clairement :

On peut aussi supposer que le passage d'un élément au suivant est une propriété de l'élément lui-même et définir une fonction :
 
CElement CElement::GetSuivant (void);

    Quoiqu'évidente, cette solution est quand même ambiguë car le suivant d'un élément n'a de sens qu'au sein d'une liste, qui semble disparaître ici, au moins explicitement.

    La dernière spécification de la liste est plus difficile à mettre en œuvre : elle précise qu'un élément n'a un successeur que s'il n'est pas le dernier. Il faut donc pouvoir tester si un élément est le dernier de la liste :
 
bool CListe::EstDernier (CElement p) const;

ce qui permettrait d'écrire :
 
if (!Liste.EstVide())
{
    for (CElement Courant = Liste.GetTete(); ; Courant = Liste.GetSuivant (Courant))
    {
        // traitement de l'élément courant

        if (Liste.EstDernier (Courant)) break;
    } 
}

Avec l'autre solution :
 
bool CElement::EstDernier (void) const;

et :
 
if (!Liste.EstVide())
{
    for (CElement Courant = Liste.GetTete(); ; Courant = Courant.GetSuivant())
    {
        // traitement de l'élément courant

        if (Courant.EstDernier ()) break;
    }
}

    Une alternative est possible si le rang d'un élément dans la liste est (ou peut être) connu. Par exemple :
 
unsigned int CListe::GetRang (CElement p) const;

permettrait d'écrire :
 
if (!Liste.EstVide())
{
    for (CElement Courant = Liste.GetTete(); ; Courant = Courant.GetSuivant())
    {
        // traitement de l'élément courant

        if (Liste.GetRang (Courant) = Liste.GetNbreElements()) break;
    }
}

Autres primitives
    De nombreuses autres primitives peuvent être implémentées à partir des deux premières, afin de faciliter l'utilisation de la liste. Ainsi :
succk(p,l)
CElement CListe::GetSuivantK (CElement p, unsigned int k) const
{
    CElement Courant = p;
    for (int i = 1, i <= k; ++i) Courant = GetSuivant (Courant);
    return Courant;

} // GetSuivantK ()

fonction à laquelle il faudrait ajouter toutes les validations nécessaires.

Accès par le rang :
CElement CListe::GetElement (unsigned int Rang) const
{
    CElement Courant = GetTete();
    for (int i = 1, i < Rang; ++i) p = GetSuivant (Courant);
    return Courant;

} // GetElement ()

Sommaire

Représentation interne des listes

    Considérons qu'un processus est une tâche (une activité du processeur) affectée d'une priorité. Dans les exemples qui suivent, nous considérerons la liste de processus suivante :
 
GestProcess(10), GestMem(7), GestDisk(6), CalcReact(3), Print(1)

classés par priorité décroissante (entre parenthèses) et dont les identificateurs sont explicites. Les trois premiers, processus du système d'exploitation, ont de fortes priorités. Le dernier, Print est une impression en tâche de fond de très faible priorité. CalcReact est un processus utilisateur. Nous utiliserons la classe CProcess suivante :
 
class CProcess
{
    CTask     m_Task;
    CPriorite m_Priorite;
    // ...

}; // CProcess

Sommaire

Utilisation d'un vecteur

    1) - Si l'ordre physique des éléments coïncide avec l'ordre logique dans la liste, la représentation contiguë dans le vecteur m_Liste est évidente, comme le montre la figure 1.

    figure 1
    Il faut conserver le nombre d'éléments à part (ou à la rigueur sous forme d'un drapeau). Une implémentation sans aucune validation est donnée figure 2.
 
class CListe
{
    CProcess m_Liste [CstTailleMax]; // CstTailleMax supposé défini
    unsigned m_NbreElements;

  public :
    // constructeur(s), destructeur et fonctions de mises à jour

    CProcess GetTete         (void) const { return m_Liste [0];     }
    unsigned GetNbreElements (void) const { return m_NbreElements;  }
    bool     EstVide         (void) const { return !m_NbreElements; }

}; // CListe

figure 2

    La construction, la destruction et le remplissage ne sont pas développés.

    La liste est dite propriétaire car elle inclut les éléments qu'elle ordonne : si la liste est détruite, les éléments disparaissent.

    Dans cette implémentation, le mécanisme de passage à l'élément suivant est évident, il suffit d'incrémenter d'une unité le rang physique (l'indice) de l'élément. Cela nécessite une fonction, que l'utilisateur n'a pas à connaître (fait seulement partie de l'implémentation), et qui suppose l'existence de l'opérateur == de comparaison de deux CProcess :
 
unsigned int CListe::GetIndice (const CProcess & Process) const
{
    int i = 0;
    for (; i < m_NbreElements && Process != m_Liste [i]; ++i);
    return i;

} // GetIndice()

Remarques :

    Ainsi les fonctions GetSuivant() et GetProcess() peuvent être écrites (sans tenir compte d'une validation nécessaire) :
 
CProcess CListe::GetSuivant (const CProcess & Process) const
{
    return m_Liste [GetIndice (Process) + 1];

} // GetSuivant ()

CProcess CListe::GetProcess (unsigned int Rang) const
{
    return m_Liste [Rang];

} // GetProcess ()

    Dans la pratique, une telle implémentation serait sans doute trop coûteuse, car elle nécessite de toujours recommencer la recherche à partir du début, ce qui est la définition même d'une liste, seulement accessible par son premier élément. La mise à jour de la liste (par exemple, le passage en urgence de CalcReact : priorité 10), nécessite des opérations d'autant plus coûteuses que la taille les éléments de la liste est importante et que le nombre d'éléments est grand. L'ajout d'un élément (par exemple GestModem de priorité 5) ou une suppression (de CalcReact par exemple) sont aussi des opérations coûteuses car elles entraînent le décalage de N/2 éléments en moyenne (en l'absence de toute autre astuce). Une solution consiste à utiliser une liste non propriétaire :

    2) - Une façon simple de limiter les transferts d'information liés à la maintenance de la liste est d'utiliser un vecteur de pointeurs vers des éléments, comme l'illustre la figure 3.


    figure 3
    Les modifications dans l'implémentation sont légères. La liste est alors dite non propriétaire puisque les éléments peuvent exister indépendamment de la liste. Néanmoins, si le vecteur est de très grande taille, le nombre d'opérations à réaliser en cas de mise à jour peut être prohibitif.

    3) - Si les ordres physique et logique ne coïncident pas, il faut alors que chaque élément conserve l'indice de son suivant, ce qui peut être réalisé par la liste MaListe, comme le montre la figure 4.


    figure 4
    Comme précédemment, les indices sont ici numérotés à partir de 0 et non de 1. De plus, le dernier élément de la liste a pour indice du suivant la valeur -1, valeur impossible qui joue le rôle de drapeau.

    Le premier élément de la liste n'étant pas le premier élément du tableau, il est nécessaire d'ajouter son indice comme donnée membre de la classe CListe, d'identificateur m_Tete.

    L'implémentation nécessite la construction d'une classe supplémentaire, CElement, comme le montre la figure 5.
class CElement
{
  public :
    int      m_Suivant;
    CProcess m_Process;

}; // CElement

class CListe
{
    CElement m_Liste [CstTailleMax]; // CstTailleMax supposé défini
    unsigned m_NbreElements;
    int      m_Tete;

    int GetIndice (const CProcess & Process) const;

  public :
    // constructeur(s), destructeur et fonctions de mises à jour

    unsigned GetNbreElements (void) const { return m_NbreElements;  }
    bool     EstVide         (void) const { return !m_NbreElements; }

    CProcess GetTete    (void)                     const;
    CProcess GetSuivant (const CProcess & Process) const;
    CProcess GetProcess (unsigned int Rang)        const;

}; // CListe

figure 5

    Les fonctions GetNbreElements() et EstVide() sont inchangées. En revanche, l'accès aux différents éléments de la liste est plus complexe (figure 6) :
 
CProcess CListe::GetTete (void) const
{
    return m_Liste [m_Tete].m_Process;

} // GetTete()

int CListe::GetIndice (const CProcess & Process) const
{
    int i = m_Tete;
    for (; i != -1 && Process != m_Liste[i].m_Process; i = m_Liste [i].m_Suivant);
    return i;

} // GetIndice()

CProcess CListe::GetSuivant (const CProcess & Process) const
{
    return m_Liste [m_Liste [GetIndice (Process)].m_Suivant].m_Process;

} // GetSuivant()

CProcess CListe::GetProcess (unsigned int Rang) const
{
    unsigned int i, j;

    for (i = m_Tete, j = 0; j < Rang; i = m_Liste [i].m_Suivant, ++j);
    return m_Liste [i].m_Process;

} // GetProcess()

figure 6

    Comme on le voit, l'accès est moins simple. Cependant la mise à jour de la liste (le passage en urgence de CalcReact : priorité 10), puis l'ajout d'un élément (le processus GestModem de priorité 5) ne nécessitent que des modifications mineures (figure 7).


figure 7

    L'inconvénient principal de l'utilisation d'un vecteur est la nécessité de fixer une fois pour toutes une taille maximale de la liste. La solution dynamique est donc parfois préférable.

Sommaire

Utilisation de la mémoire dynamique

    1) - La représentation en mémoire dynamique la plus simple correspond au schéma suivant (figure 8) :

figure 8

    Les déclarations correspondantes sont les suivantes (figure 9) :
 
class CElement
{
  public :
    CProcess m_Process;
    CElement * m_Suivant;

}; // CElement

class CListe
{
    unsigned   m_NbreElements;
    CElement * m_Tete;
    CElement * GetElement (const CProcess & Process) const;

  public :
    // constructeur(s), destructeur et fonctions de mises à jour

    unsigned GetNbreElements (void) const { return m_NbreElements;  }
    bool     EstVide        (void) const  { return !m_NbreElements; }

    CProcess GetTete    (void)                     const;
    CProcess GetSuivant (const CProcess & Process) const;
    CProcess GetProcess (unsigned int Rang)        const;

}; // CListe

figure 9

    La fonction membre GetIndice() est ici remplacée par GetElement() qui renvoie un pointeur vers l'élément correspondant au processus recherché. Les primitives de base peuvent être implémentées ainsi (figure 10) :
 
CProcess CListe::GetTete (void) const
{
    return m_Tete->m_Process;

} // GetTete()

CElement * CListe::GetElement (const CProcess & Process) const
{
    CElement * pElem = m_Tete
    for (; pElem && Process != pElem->m_Process; pElem = pElem->m_Suivant);
    return pElem;

} // GetElement()

CProcess CListe::GetSuivant (const CProcess & Process) const
{
    return GetElement (Process)->m_Suivant->m_Process;

} // GetSuivant ()

CProcess CListe::GetProcess (unsigned int Rang) const
{
    CElement * pElem = m_Tete;
    for (unsigned int j   = 0; j < Rang; pElem = pElem->m_Suivant, j++);

    return pElem->m_Process;

} // GetProcess()

figure 10

    Cette représentation possède toutes les propriétés désirées:

    La figure 11 illustre la mise à jour de la liste (le passage en urgence de CalcReact : priorité 10), suivie de l'ajout d'un élément (le processus GestModem de priorité 5). Les flèches grises montrent les chaînages avant modification.

figure 11

    La figure 12 illustre la suppression de GestMem. Comme précédemment, les flèches grises montrent les chaînages avant suppression. Il faut remarquer que, pour supprimer logiquement le processus GestMem de la liste, il suffit de modifier le chaînage de l'élément qui le précède, mais pas son propre chaînage vers le suivant. Bien entendu, cet élément peut devenir inaccessible et ce problème sera traité en travaux dirigés.


figure 12

    Le pointeur m_Suivant du dernier élément ne pointe sur aucun élément. Sa valeur est conventionnellement 0 : en C/C++, un pointeur nul correspond à une adresse invalide.

    La création d'un nouvel élément doit se dérouler en cinq étapes :

    Les étapes ne doivent pas impérativement être effectuées dans cet ordre. Certaines peuvent être regroupées en une seule.

    2) - Comme précédemment, il peut être avantageux d'utiliser une liste non propriétaire de l'information, dans laquelle chaque élément pointe à la fois sur l'information (ici le processus) et l'élément suivant, comme l'illustre la figure 13.


figure 13

    L'implémentation en est légèrement modifiée :
 
class CElement
{
  public :
    CProcess * m_Process;
    CElement * m_Suivant;
}; // CElement

class CListe
{
    unsigned   m_NbreElements;
    CElement * m_Tete;
    CElement * GetElement (const CProcess & Process) const;

  public :
    // constructeur(s), destructeur et fonctions de mises à jour

    unsigned GetNbreElements (void) const { return m_NbreElements;  }
    bool     EstVide         (void) const { return !m_NbreElements; }

    CProcess GetTete         (void)                     const;
    CProcess GetSuivant      (const CProcess & Process) const;
    CProcess GetProcess      (unsigned int Rang)        const;

}; // CListe

figure 14
CProcess CListe::GetTete (void) const
{
    return *(m_Tete->m_Process);

} // GetTete()

CElement * CListe::GetElement (const CProcess & Process) const
{
    CElement * pElem = m_Tete;
    for (; pElem && Process != *(pElem->m_Process);
         pElem = pElem->m_Suivant);
    return pElem;

} // GetElement()

CProcess CListe::GetSuivant (const CProcess & Process) const
{
    return *(GetElement (Process)->m_Suivant->m_Process);

} // GetSuivant()

CProcess CListe::GetProcess (unsigned int Rang) const
{
    CElement * pElem = m_Tete;
    
    for (unsigned int j = 0; j < Rang; ++j, pElem = pElem->m_Suivant);

    return *(pElem->m_Process);

} // GetProcess()

figure 15
Sommaire

Autres opérations sur les listes

    Pour spécifier complètement une liste, il est nécessaire d'ajouter quelques fonctions membres (méthodes) permettant de la maintenir, par exemple :     D'autres fonctionnalités peuvent être ajoutées au sein de la classe CListe afin de faciliter son utilisation, ou d'optimiser les traitements.

Sommaire

Généralisation

    Grâce à la généricité, la classe CListe que nous avons ébauchée plus haut est généralisable à presque tout autre type d'information, à condition que les opérations effectuées sur l'information soient licites. Nous avons déjà noté que la recherche d'une information ou de son rang dans la liste suppose que les opérateurs de comparaison == ou != soient accessibles (voir la fonction GetElement() par exemple). De plus, toutes les fonctions qui renvoient une valeur de type CProcess utilisent implicitement un constructeur par recopie. S'il n'existe pas, ou si les objets à recopier sont trop importants, il est préférable de renvoyer des références ou des pointeurs sur les informations, par exemple :
 
CProcess * CListe::GetSuivant (const CProcess & Process) const;

    La solution non propriétaire remédie à cet inconvénient, mais en apporte un autre : l'utilisateur, récupérant un pointeur sur l'information, peut l'altérer ou même la détruire directement sans passer par la liste.

    Pour rendre le code utilisable, il est nécessaire de le sécuriser : chaque opération doit être analysée et les causes potentielles d'erreurs testées. Comme toujours, deux attitudes peuvent être choisies :

    Outre que les traitants d'exceptions peuvent être clairement regroupés en des endroits particuliers des applications, cette technique supporte des traitements d'erreurs partiels étagés sur plusieurs niveaux.

    Les deux méthodes ont des avantages et des inconvénients, le principal en ce qui concerne les exceptions étant de ralentir l'exécution, mais son avantage étant une très grande lisibilité. Le principal inconvénient de l'autre méthode est de pousser l'utilisateur à supprimer les tests pour rendre le code plus lisible ... En tout état de cause, il est préférable de ne pas mélanger les deux techniques, si possible !

Sommaire

Structures de données et conteneurs

Généralités

    Il n'est pas toujours facile de bien faire la différence entre ces deux notions. Nous nous contenterons d'indiquer ici qu'une structure de données est une organisation logique qui ajoute des propriétés supplémentaires aux informations qu'elle contient. Ainsi, une liste, comme nous l'avons vu, ajoute une relation d'ordre aux informations. Une structure arborescente permet d'établir une hiérarchie entre les informations, un graphe introduit par exemple la notion de chemin pour aller d'une information à une autre.

    A l'intérieur de ces classes de structures de données existe une grande variété. Par exemple, les listes linéaires peuvent être simples, doubles, circulaires, les arbres peuvent être binaires, n-aires, parfaits, etc...

    Pour être programmées, ces différentes structures de données doivent utiliser les possibilités d'un langage de programmation. Une même structure peut utiliser des tableaux (array), de la mémoire dynamique ou autres. Un tableau peut aussi bien servir à implémenter une liste circulaire, un arbre binaire, un graphe, etc...

    Un conteneur  est un objet destiné à stocker et à restituer des informations, selon des règles particulières. Les conteneurs les plus simples sont les files et les piles. La façon dont un conteneur organise les informations qu'il stocke fait partie de l'implémentation et peut (doit ?) être cachée à l'utilisateur. Une pile peut être implémentée par une liste simple, elle-même implémentée par un tableau ou des éléments dynamiques chaînés. Certains conteneurs peuvent même faire intervenir simultanément plusieurs structures de données : un conteneur "arbre généalogique" est naturellement implémenté par une structure arborescente, mais l'ensemble des frères et sœurs (fratrie) correspond très bien à une liste circulaire.

Sommaire

Pile

    Une liste linéaire simple (un seul chaînage vers le suivant), permet d'implémenter facilement un conteneur pile. Il suffit de changer les noms des fonctions et de les faire porter sur le premier élément de la liste pour y reconnaître les opérations sur les piles :
 
EstVide()
Empile ()
Depile ()
Sommet ()
  inchangé
  remplace InsereEnTete()
  remplace SupprimeEnTete()
  remplace GetInfo (/* Rang = */ 0)

Sommaire

Files

    Moyennant une petite amélioration de la liste étudiée plus haut, il est maintenant possible d'obtenir toutes les caractéristiques d'une file (par exemple file d'attente : ordre FIFO). Les éléments sont déposés en queue de liste et retirés en tête. Le conteneur générique CFile propose donc les méthodes suivantes :
 
 
EstVide()
Depose ()
Retire ()
  inchangé
  remplace InsereEnQueue()
  remplace SupprimeEnTete()

Sommaire

Autres listes

    Dans les paragraphes précédents, nous avons porté notre intérêt sur les listes linéaires simples, ayant un premier et un dernier élément. D'autres structures de données voisines présentent un grand intérêt :

Listes doublement chaînées

    Il est parfois nécessaire de pouvoir accéder aux deux extrémités (tête et queue) de la liste, et de la parcourir dans les deux sens. Rien n'est plus coûteux que de remonter au précédent d'un élément dans une liste simple. De plus il est parfois intéressant de pouvoir ajouter ou supprimer des éléments en tête et en queue de liste. Ce double accès peut être facilement obtenu en ajoutant : Sommaire

Sentinelles

    L'insertion entre deux éléments de la liste doublement chaînée, que nous noterons A et B, nécessite la modification des données membres m_Suivant de l'élément A et m_Precedent de B (suivre sur la figure 16). L'insertion d'un élément en tête nécessite la modification des données membres m_Tete de la liste et m_Precedent du premier élément. L'insertion d'un élément en queue nécessite la modification des données membres m_Queue de la liste et m_Suivant du dernier élément. Enfin, l'insertion dans une liste vide nécessite la modification des données membres m_Tete  et m_Queue de la liste. La même complexité concerne l'initialisation des données membres m_Precedent et m_Suivant du nouvel élément, qui pointeront soit sur un autre élément, soit sur rien (en tête ou en queue).

figure 16

    L'algorithme de suppression présente aussi la même complexité. De plus, les cas particuliers qui doivent être envisagés sont relativement rares : si le rang de l'insertion est équiprobable, il y a une chance sur N (nombre d'éléments) qu'elle se fasse en tête par exemple.

    Une technique souvent utilisée consiste à unifier tous les cas, c'est-à-dire à forcer toute insertion ou suppression à être effectuée en milieu de liste. Elle nécessite d'ajouter à la liste deux éléments, appelés sentinelles, qui ont pour seul rôle de borner la liste. Ils sont physiquement présents, mais ne sont pas comptabilisés et ne contiennent pas d'information. Ils doivent être créés et chaînés entre eux dès la création de la liste (figure 17) et détruits lorsque la liste est détruite.


figure 17

    Tout au long de la vie de la liste, m_Tete pointe sur la première sentinelle, et m_Queue sur la seconde. Toutes les insertions et suppressions sont effectuées entre ces deux sentinelles (figure 18).


figure 18

    Les modifications entraînées sont minimes.

Sommaire

Listes circulaires

    Comme leur nom l'indique, les listes circulaires, simplement ou doublement chaînées n'ont ni tête ni queue, mais un point d'entrée, qui peut d'ailleurs varier dans le temps. Le parcours ou la recherche dans la liste s'arrête au plus tard lorsqu'on revient à l'élément de départ. L'exemple de fratrie cité ci-dessus peut être illustré par la figure 19 :

figure 19

    La présence de sentinelle(s) compliquerait le parcours de la liste, mais son absence nécessite de traiter séparément le cas de l'insertion dans une liste vide et de la suppression d'une liste limitée à un seul élément.

    Les listes circulaires sont très souvent utilisées dans les systèmes d'exploitation, par exemple pour établir un "tour de rôle" dans les processus qui se partagent l'accès au processeur (voir cours de Systèmes d'exploitation, 2ème année, chapitre "gestion des processus", technique de "round robin")

Sommaire

Listes multiples, listes de listes

    Des structures complexes peuvent être construites à partir des structures élémentaires. L'exemple ci-dessous montre un dictionnaire dans lequel les mêmes couples de mots français-anglais appartiennent à deux listes classées alphabétiquement (figure 20)

figure 20

    D'autres structures complexes seront étudiées en travaux dirigés.

Sommaire

Listes triées

    Dans les cas précédents, l'ordre des éléments était fixé selon un critère externe. Il est aussi possible de créer des listes chaînées triées selon un ordre particulier dépendant de tout ou partie de l'information : ordre alphabétique sur des chaînes de caractères, ordre croissant sur des nombres, etc... La valeur sur laquelle est effectué le tri est appelée clé (qui peut être la valeur elle-même). Deux approches différentes sont utilisées.

    Dans la première l'ordre est intrinsèque à la liste, c'est un invariant : la liste est toujours triée. Dès lors, cet ordre doit être respecté pendant toute la durée de vie de la structure. En particulier toutes les opérations qui font évoluer la liste (en particulier l'ajout) doivent maintenir cet ordre : les opérations essentielles sont la recherche, l'insertion ou la suppression d'un élément de clé donnée, l'utilisateur n'ayant pas en général à se préoccuper du rang auquel ces opérations sont effectuées. Pour maintenir l'ordre de la liste, l'utilisateur ne peut insérer une information à un rang donné. Une liste non propriétaire peut difficilement garantir qu'elle est ordonnée.

    L'approche de la bibliothèque standard du C++ (Standard Template Library ou STL) qui sera étudiée ultérieurement, est totalement différente : le tri est effectué par un algorithme, générique, qui peut être appliqué à différentes sortes de structures. L'ordre n'est pas une propriété intrinsèque de la liste, mais une propriété vérifiée à un instant donné. L'inconvénient majeur est d'obliger l'utilisateur à trier la liste chaque fois qu'il veut profiter de cette propriété si la liste a subi des transformations inconnues depuis son précédent tri. L'avantage est de permettre de trier la liste plusieurs fois dans la même application, selon plusieurs critères de tri différents.

Sommaire
 

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique