Aller au contenu

Version pdf

Type abstrait Liste (Bac 🎯)⚓︎

Type abstrait Liste⚓︎

Point de cours 3

Le type abstrait Liste permet de créer une séquence d'éléments ordonnés par leur position dans la liste.

  • Un liste peut être vide.
  • Si la liste est non vide, contrairement aux types abstraits tableau ou dictionnaire, un seul élément de la liste est accessible directement, c'est la tête de liste
  • La queue de liste permet d'accéder aux éléments suivants. La queue de liste est une liste donc on accède au deuxième élément de la liste (s'il existe) en prenant la tête de la queue de liste. De même pour accéder au troisième élément on prend la tête de la queue de la queue de liste ...

L'accès aux éléments successifs de la liste n'est donc pas direct mais nécessite une répétition d'opérations identiques, appelée parcours de liste. On dit qu'une liste est une structure linéaire.

Une interface minimale du type abstrait Liste est la suivante :

Opération Signature Description
creer_liste creer_liste() Renvoie une liste vide
liste_vide liste_vide(lis) Renvoie un booléen indiquant si la liste lis est vide
inserer inserer(lis, elt) Insère elt en tête de la liste lis, renvoie une nouvelle liste ou modifie la liste en place
tete tete(lis) Renvoie l'élément en tête de liste
queue queue(lis) Renvoie la liste privée de l'élément en tête liste

Liste homogène

Dans tout le chapitre on se restreint à manipuler uniquement des listes dont tous les éléments sont de même type.

Méthode 2

Le parcours d'une liste est une répétition d'opérations tete pour lire la valeur de l'élément accessible en tête de liste et queue pour passer à l'élément suivant.

Cette répétition peut s'effectuer de façon itérative avec une boucle ou récursive.

Exemple 2

On peut écrire une fonction déterminant la longueur d'une liste de façon itérative ou récursive. Retenez bien ce modèle, qu'on peut décliner pour tous les parcours de liste.

Parcours de liste itératif

🐍 Script Python
def longueur(lis):
    lon = 0
    courant = lis  # élément en tête
    while not liste_vide(courant):
        # si besoin traitement sur tete(courant)
        lon = lon + 1 
        courant = queue(courant)
    return lon

Parcours de liste récursif

🐍 Script Python
def longueur_rec(lis):
    if liste_vide(lis):
        return 0
    return 1 + longueur_rec(queue(lis))

Liste chaînée⚓︎

Point de cours 4

On représente en général une liste comme une chaîne de cellules reliées par des flèches, on parle de liste chaînée.

Chaque cellule est un couple qui porte deux informations :

  • une valeur
  • un lien vers la cellule suivante

On peut définir le type abstrait Liste récursivement. Une liste est :

  • soit vide
  • soit une cellule portant une valeur appelée la tête de la liste, et un lien vers une autre liste appelée queue de la liste.

Exemple 3

On a représenté ci-dessous une liste avec une chaîne de trois cellules contenant les caractères "N", "S" et "I". Notez que l'élément en tête de liste est le dernier inséré.

liste

🐍 Script Python
liste = creer_liste()
inserer(liste, "I")
inserer(liste, "S")
inserer(liste, "N")

Méthode 3

Pour implémenter une liste chaînée, il faut une structure de données permettant de représenter une cellule. Voici deux solutions :

Implémentation avec tuples

Une cellule est représentée par un tuple, la première composante porte la valeur et la seconde un lien vers une autre cellule. La dernière cellule porte un lien vers un objet représentant une cellule vide, par exemple None ou (). On implémente alors la liste chaînée de l'exemple 2 par lis = ("N", ("S", ("I", None))).

🐍 Script Python
lis = ("N", ("S", ("I", None)))

alt

Implémentation avec une classe Cellule

Une cellule est représentée par un objet de la classe Cellule, qui possède deux attributs : valeur portant la valeur et suivant portant le lien vers la cellule suivante dans la liste chaînée. On implémente alors la liste chaînée de l'exemple 3 par :

alt

🐍 Script Python
class Cellule:

    def __init__(self, valeur, suivant=None):
        self.valeur = valeur
        self.suivant = suivant        

lis1 = Cellule("N", Cellule("S", Cellule("I", None)))

Complexité temporelle

Pour les deux implémentation de liste chaînée avec tuple ou classe Cellule, on a les mêmes complexités temporelles par rapport au nombre \(n\) d'éléments dans la liste. Le seul élément accessible directement est la tête, en temps constant. Le coût d'un parcours complet de la liste en suivant le chaînage est linéaire, proportionnel à la taille de la liste.

Opération Type complexité Notation de Landau
inserer, tete ou queue constante \(O(1)\)
parcours de toute la liste linéaire \(O(n)\)

Liste chaînée mutable⚓︎

Point de cours 5

  • Une structure de données est immuable si on ne peut la modifier une fois qu'elle est construite. Évidemment on peut toujours accéder aux données en lecture
  • Une structure de données est mutable si on peut la modifier une fois qu'elle est construite.
  • Cette distinction entre types immuable et mutable n'a de sens que pour des types structurés, les types simples comme les entiers, les flottants, les booléens sont immuables (1 reste égal à 1 !).

Exemple 4 types natifs mutables et immuables en Python

En Python, pour représenter une séquence de données on peut utiliser le type list des tableaux dynamiques qui est mutable ou le type tuple qui est immuable.

🐍 Script Python
>>> tup = (14, 10)
>>> tup[0]
14
>>> tup[0] = 11
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> tad = [14, 10]
>>> tad[0]
14
>>> tad[0] = 11

Quel est l'intérêt d'un type immuable ? Dans certains cas on a besoin de fixer certaines valeurs par exemple lorsqu'elles servent de clefs dans un dictionnaire implémenté par une table de hachage. Une fonction de hachage calcule à partir de la clef l'index de la case du tableau où est stockée la valeur donc la clef ne doit pas être modifiée pour que son image par la fonction de hachage reste inchangée. Ainsi un type mutable ne peut servir de clef dans un dictionnaire.

🐍 Script Python
>>> dico = dict()
>>> dico[tup] = 1
>>> dico
{(14, 10): 1}
>>> dico[tad] = 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Si dans l'implémentation d'une liste chaînée, une cellule implémentée par un tuple, la liste chaînée est donc immuable.

Exemple 6 listes chaînées immuables avec des tuples

Avec l'implémentation du type abstrait Liste par des cellules implémentées à l'aide de tuple, les opérations queue et inserer renvoient une nouvelle liste. Le type tuple étant immuable en Python, ces listes chaînées sont immuables.

On a également présenté une implémentation à l'aide d'une classe Cellule qui devrait donc permettre de construire des listes chaînées mutables, il nous manquait juste la possibilité de représenter une liste vide.

Méthode 4 : listes chaînées mutables

Pour construire des listes chaînées mutables, il est naturel d'utiliser le paradigme objet (POO). On reprend l'implémentation avec une classe Cellule mais on intègre cette fois toutes les opérations du type abstrait Liste comme méthode d'une classe Liste. Cette classe possède un seul attribut tete_liste qui pointe soit vers None (liste vide) ou vers la première cellule de la liste chaînée.

🐍 Script Python
class Cellule:

    def __init__(self, valeur, suivant=None):
        self.valeur = valeur
        self.suivant = suivant

class Liste:    
    
    def __init__(self):
        self.tete_liste = None
        
    def liste_vide(self):
        return self.tete_liste is None

    def inserer(self, valeur):
        self.tete_liste = Cellule(valeur, self.tete_liste)
    
    def tete(self):
        assert not self.liste_vide()
        return self.tete_liste.valeur
        
    def queue(self):
        assert not self.liste_vide()           
        liste_queue = Liste()
        liste_queue.tete_liste = self.tete_liste.suivant
        return liste_queue
            
    def modifier_tete(self, valeur):
        assert not self.liste_vide()
        self.tete_liste.valeur = valeur 

Effets de bord

Les listes chaînées mutables peuvent être modifiées après leur construction. Si deux listes chaînées mutables partagent les mêmes cellules, modifier une liste modifie l'autre par effet de bord, ce qui n'est pas toujours souhaité.

Exemple 5

On crée une liste mutable liste1 puis on associe la queue de liste1 à une variable liste2. Si on modifie la valeur de la cellule en tête de liste2 c'est aussi la cellule suivante de la tête de liste1qui est donc modifiée par effet de bord.

🐍 Script Python
>>> liste1 = Liste()
>>> liste1.inserer('P')
>>> liste1.inserer('T')
>>> liste1.inserer('T')
>>> liste1.inserer('H')
>>> print(liste1)
(H, (T, (T, (P, ()))))
>>> liste2 = liste1.queue()
>>> print(liste2)
(T, (T, (P, ())))
>>> liste2.modifier_tete('F')
>>> >>> print(liste2)
(T, (T, (P, ())))
>>> print(liste1)
(H, (F, (T, (P, ()))))

Avant la modification de liste2 :

alt

Après la modification de liste2 :

alt