Avec une liste simplement chaînée, il est possible d'ajouter ou supprimer l'élément au début et (avec un peu d'effort) d'ajouter un élément à la fin. Mais afin de pouvoir supprimer rapidement l'élément à la fin d'une liste, celle-ci doit être doublement chaînée.
Contrairement au cas des listes simplement chaînées, Common Lisp n'a pas de fonctions prédéfinies pour les listes doublement chaînées. Nous devons donc commencer par définir un type. Nous allons utiliser une structure pour les cellules. Voici une définition possible :
Cette structure va conduire à la construction d'objets contenant des suites de références circulaires. L'affichage d'un tel objet est infini. Pour pallier à ce problème, nous fournissons à la structure une fonction d'affichage spécifique qui ne suit que le champ next et n'affiche pas le champ prev.
Comme avec des listes simplement chaînées, nous allons utiliser deux variables globales *start* et *end* qui pointent respectivement sur le premier et le dernier élément :
La figure 23.1 montre une telle liste.
Les fonctions pour récupérer le premier et le dernier élément sont simples :
Voici une première version de la fonction pour ajouter un élément au début :
La fonction pour ajouter un élément à la fin est symétrique. Chaque occurrence de *start* doit être remplacée par *end*, chaque occurrence de next par prev et chaque occurrence de prev par next :
Les deux fonctions pour supprimer un élément sont elles aussi symétriques :
Mais comme avec les listes simplement chaînées, ces fonctions sont inutilement compliquées et inefficaces. L'utilisation de sentinelles peut considérablement améliorer la situation.
L'idée est de faire en sorte que *start* pointe toujours sur la première sentinelle et que *end* pointe toujours sur la deuxième sentinelle. Le premier élément de la liste (s'il existe) se trouve donc dans la cellule qui suit celle pointée par *start* et le dernier élément (s'il existe) se trouve dans la cellule qui précède celle pointée par *end* (voir la figure 23.2).
Voici les expressions à évaluer pour initialiser *start* et *end*
Il convient de définir une fonction pour déterminer si la liste est vide :
Les fonctions pour récupérer le premier et le dernier éléments sont un peu plus compliquées :
Par contre, les fonctions pour ajouter un élément sont plus simples, car il n'y a pas de cas spécial à tester :
Ici, une sentinelle est transformée en cellule normale et la nouvelle cellule allouée sert de sentinelle. Il est aussi possible (mais un peu plus compliqué) de préserver la cellule sentinelle et d'utiliser la nouvelle cellule pour stocker l'élément rajouté. Voici finalement, les fonctions pour supprimer un élément :
Ici, la sentinelle est abandonnée et sera ultérieurement récupérée par le ramasse-miettes. La première (respectivement dernière) cellule est transformée en sentinelle.
Avec une liste doublement chaînée, il est possible d'introduire la notion de curseur (en Anglais : cursor). Il s'agit d'une position dans la liste permettant d'insérer ou de supprimer un élément. De plus, le curseur peut être avancé ou reculé d'une position à la fois.
Le curseur peut bien sûr indiquer un des éléments de la liste, mais cela pose un problème si la liste est vide. Une meilleure idée est que le curseur indique un espace entre deux éléments, car cette idée se généralise facilement à une liste vide. La figure 23.3 illustre cette idée.
En ce qui concerne l'implémentation, il est évidemment impossible de faire pointer un curseur entre deux éléments. Mais la solution est simple : il suffit de le faire pointer physiquement sur l'élément qui précède la position logique ou sur la première sentinelle, si la position est avant le premier élément. Sans la sentinelle, le nombre de cas spéciaux à tester serait considérable.
Il est possible d'abandonner les variables *start* et *end*, car il est toujours possible d'avancer ou de reculer le curseur. Mais elles peuvent servir, car on a souvent besoin de positionner le curseur au début ou à la fin de la liste. Sans ces variables, ces deux opérations seraient lentes : au lieu de .
Voici les expressions à évaluer pour initialiser une telle liste :
Il convient de définir deux fonctions pour déterminer si le curseur est au début ou à la fin de la liste :
On remarque une disymétrie entre ces deux fonctions, due au fait que le curseur pointe physiquement sur la cellule qui précède la position. On remarque aussi que le curseur peut être à la fois au début et à la fin de la liste, ce qui est le cas lorsque la liste est vide.
Voici les fonctions pour déplacer le curseur :
Il est clair que les fonctions pour mettre le curseur au début ou à la fin sont rapides (.
En ce qui concerne les fonctions d'insertion et de suppression, plusieurs conventions sont possibles. On peut insérer ou supprimer devant ou derrière le curseur.
Pour l'insertion, que ce soit avant ou après le curseur, une cellule doit être ajoutée entre la cellule sur laquelle pointe le curseur et celle d'après. La seule différence sera la position finale du curseur. Pour la suppression, il faut soit supprimer la cellule sur laquelle pointe le curseur, soit celle d'après.
Voici la fonction pour ajouter un élément après le curseur (à sa gauche) :
La seule différence entre add-before et add-after est que pour add-after le curseur ne sera pas affecté :
Voici la fonction pour supprimer l'élément suivant le curseur :
Finalement, voici la fonction pour supprimer l'élément après le curseur :
On pourrait maintenir les fonctions d'ajout et de suppression d'un élément au début ou à la fin de la liste, mais cela pose problème lorsqu'on supprime un élément sur lequel se trouve le curseur. C'est le cas si le curseur se trouve logiquement entre le premier et le deuxième élément (et donc physiquement sur le premier élément) et si le premier élément est supprimé. Il est alors nécessaire de tester la valeur du curseur pour le déplacer d'une position à gauche.
Ce problème est le même si on généralise cette technique à un nombre arbitraire de curseurs. Dans ce cas, il n'est pas pratique de tester l'ensemble des curseurs à chaque fois qu'un élément est supprimé.
Voici une autre technique possible. Il faut d'abord ajouter un champ dans la cellule comme ceci :
Le champ supplémentaire indique si la cellule a été supprimée. Un curseur qui pointe sur une cellule supprimée doit être ajusté. Mais au lieu de faire cet ajustement au moment où la cellule est supprimée, on le fait avant d'utiliser le curseur. De cette manière, on n'ajuste pas un curseur qui n'en a pas besoin. Dans la description qui suit, nous allons supposer que le curseur manipulé se trouve toujours dans la variable *cursor*, mais en général, on peut supposer qu'il peut y avoir plusieurs curseurs.
D'abord, avant de se servir d'un curseur quelconque, il est dorénavant nécessaire de s'assurer qu'il pointe sur une cellule non supprimée. On le fait en reculant (par le champ prev) jusqu'à ce que le curseur pointe sur une cellule non supprimée. Nous allons appelé cette procédure normalisation (voir la figure 23.4 pour une situation possible avant la normalisation et la figure 23.5 pour la situation après normalisation). Voici la fonction qui normalise le curseur :
Il est possible que plusieurs curseurs pointent sur la même cellule. Si le nombre de curseurs est grand, on peut simplifier la tâche des autres curseurs qui pointent sur les cellules de la chaîne de cellules supprimées, en transformant la chaîne en arbre peu profond. Pour cela, il faut modifier la fonction de normalisation de la manière suivante :
Après appel à la fonction collapse, toutes les cellules supprimées de la chaîne pointent directement sur une cellule non supprimée. La figure 23.6 montre la situation après appel à la nouvelle version de la fonction normalize.
Les fonctions de déplacement du curseur et les fonctions d'insertion doivent maintenant appeler normalize :
Les fonctions de suppression doivent maintenant être modifiées de la façon suivante :
Irene DURAND