Liste Chaînée

Les listes chaînées sont très utilisés en algorithmie lorsque l'on a uniquement besoin de parcourir une suite d'éléments et que l'on souhaite pouvoir supprimer ou ajouter un élément quelconque de cette liste en temps constant.

Pour réussir à supprimer/ajouter un élément en temps constant on va découper la liste en éléments indépendants, appelés maillons, collés les uns aux autres.

Maillon

Commençons par définir un Maillon :

structure Maillon<Type>:
    attributs:
        valeur: Type

        suivant: Maillon  ∅
        précédent: Maillon 

Chaque maillon contient :

Un maillon est une liste chaînée de 1 élément :

M  Maillon {valeur: 1}

L  M

Chaîne de maillons

Si on veut ajouter un maillon à la chaîne, on le peut le rajouter avant ou après l'élément comme le montre l'exemple suivant qui crée une chaîne de deux maillons :

M  Maillon {valeur: 1}

# ajout d'un maillon après M
M2  Maillon {valeur: 3}
M2.précédent   M
M.suivant   M2

On peut même insérer un élément :

# insert d'un élément après M

M3  Maillon {valeur: 2}

M3.précédent  M
M3.suivant  M.suivant

M.suivant  M3

À la fin de l'exemple on a une liste chaînée commençant au maillon M et constitué de 3 maillons.

À retenir

Il n'y a pas de différence entre un maillon et la chaîne qui commence par lui.

De la remarque précédente on peut en déduire l'algorithme permettant de déterminer le nombre de maillons (ie la longueur) après un maillon donné :

algorithme taille(L: Maillon<T>)  entier:
    t  0
    tant que L :
        t  t + 1
        L  L.suivant
    rendre t

Structure

On a coutume de définir la liste chaînée via un type récursif :

Définition

Une liste chaînée est soit :

  • un Maillon isolé
  • un Maillon suivi d'une liste chaînée

On peut écrire cette structure en ajoutant juste deux méthodes à notre maillon :

structure Maillon<Type>:
    attributs:
        valeur: Type

        suivant: Maillon  ∅
        précédent: Maillon méthodes:
        fonction append(L: Maillon<Type>) :  # ajoute L après self
            si L : 
                L.précédent  self
            self.suivant  L

        fonction pop()  Maillon<Type>:  # supprime self de la liste 

            L  self.suivant 

            self.suivant si L :
                L.précédent rendre L

Écrivez un algorithme récursif permettant de concaténer deux listes chaînées avec une complexité égale à la taille de la première liste.

corrigé

algorithme concatène(L1: Maillon<T>, L2: Maillon<T>)  Maillon<T>:
    si L1 ==:
        rendre L2
    rendre L1.append(concatène(L1.suivant, L2))

complexité

Le principal intérêt des liste chaînée est :

L'insertion et la suppression d'un maillon se fait en $\mathcal{O}(1)$ opérations

Ce qui en fait une structure très malléable. En revanche ceci à un coût :

Pour accéder au $i$ème élément d'une liste chaînée, il faut la parcourir à une complexité de $\mathcal{O}(i)$.

Utilisation

On utilise les listes chaînées lorsque l'on doit très souvent insérer ou supprimer des éléments à l'intérieur d'une liste ou lorsque l'on construit une liste par étapes.

Stockage sur disque

Les listes chaînées sont utilisées pour stocker des fichiers sur un disque physique.

Algorithmes récursifs

C'est cette dernière utilisation fait que cette structure est plébiscité par les approches récursives où l'on construit petit à petit nos listes. Voyons ça avec quelques exercices qui reprennent avec des listes chaînées des exercices que l'on a déjà vu avec des tableaux, vous verrez que les algorithmes sont de complexité linéaire ce qui n'était pas le cas avec des tableaux.

Donnez un algorithme récursif et sa complexité de recherche de la valeur maximale dune liste chaînée d'entiers.

corrigé

L'algorithme suivant est clairement correct :

algorithme maximum(L: Maillon<entier>)  entier:
    si L.suivant ==:
        rendre L.valeur
    rendre max(L.valeur, maximum(L.suivant))

L'équation de sa complexité est : $C(n) = \mathcal{O}(1) + C(n-1)$ où $n$ es la taille de la liste. Cette équation a pour solution $C(n) = \mathcal{O}(n)$

Donnez un algorithme récursif et sa complexité permettant de supprimer une valeur d'ue liste.

corrigé

L'algorithme suivant est clairement correct :

algorithme supprime(L: Maillon<T>, v: T)  Maillon<T>:
    si L ==:
        rendresi L.valeur == v:
        rendre supprime(L.suivant, v)
    rendre L.append(supprime(L.suivant, v))

L'équation de sa complexité est : $C(n) = \mathcal{O}(1) + C(n-1)$ où $n$ es la taille de la liste. Cette équation a pour solution $C(n) = \mathcal{O}(n)$.

Donnez un algorithme récursif et sa complexité permettant de retourner une liste chaînée

corrigé

Utilisons la concaténation :

algorithme retourne(L: Maillon<T>)  Maillon<T>:
    si L ==:
        rendre ∅
    L2  L.pop()
    rendre concaténation(retourne(L2), L)

La complexité vaut :

$C(n) = \mathcal{O}(n) + C(n-1)$ où $n$ es la taille de la liste. ce qui donne une complexité de $\mathcal{O}(n^2)$ ce qui est un peut trop.

La complexité importante est due au fait que l'on fait trop de concaténations inutiles. Pour palier ça, utilisons les accumulateurs !

algorithme retourne(L: Maillon<T>, acc: Maillon<T>)  Maillon<T>:
    si L ==:
        rendre acc
    L2  L.pop()
    L.append(acc)
    rendre retourne(L2, L)

Et on retourne la liste en commençant par un accumulateur vide. Par exemple si on cherche à retourne la liste [1, 2, 3] :

retourne([1, 2, 3],)  = 
retourne([2, 3], [1])   =
retourne([3], [2, 1])   =
retourne([], [3, 2, 1]) = [3, 2, 1]

La complexité est maintenant à nouveau de $C(n) = \mathcal{O}(1) + C(n-1)$ donc bien linéaire en la taille de la liste chaînée.