Aller au contenu

Chapitre 3 - Partie 2 : Interface et structures linéaires - listes, piles, files et dictionnaires


Une structure de données est une manière d'organiser des informations pour les manipuler efficacement. On la décrit par son interface : l'ensemble des opérations qu'elle propose, sans préciser comment elles sont réalisées.

L'implémentation est le choix concret de la représentation en mémoire et de l'algorithme de chaque opération.

  • On peut changer l'implémentation sans modifier le code qui utilise la structure ;
  • Plusieurs implémentations d'une même interface peuvent avoir des performances différentes ;
  • Cela favorise la modularité et la réutilisabilité du code.

Interface (ce qu'on peut faire) :

OpérationDescription
creer()Créer un compteur initialisé à 0
incrementer()Augmenter le compteur de 1
valeur()Lire la valeur actuelle
reinitialiser()Remettre le compteur à 0

Implémentation A (avec un entier) :

class CompteurEntier:
def __init__(self):
self.n = 0
def incrementer(self):
self.n += 1
def valeur(self):
return self.n
def reinitialiser(self):
self.n = 0

Implémentation B (avec une liste) :

class CompteurListe:
def __init__(self):
self.elements = []
def incrementer(self):
self.elements.append(1)
def valeur(self):
return len(self.elements)
def reinitialiser(self):
self.elements = []

Les deux implémentations offrent la même interface, mais diffèrent en performance et en mémoire.

Les implémentations de cette partie (listes chaînées, piles, files) reposent sur les classes vues au chapitre 1.


Une liste chaînée est une suite d'éléments où chaque élément (appelé maillon ou nœud) contient :

  • une valeur ;
  • une référence vers le maillon suivant (ou None si c'est le dernier).
┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│ 5 │ ──┼──>│ 8 │ ──┼──>│ 3 │ / │
└───┴───┘ └───┴───┘ └───┴───┘
tête queue

C'est une structure récursive : une liste chaînée est soit vide, soit un maillon suivi d'une liste chaînée.

OpérationTableau (list)Liste chaînée
Accès par indexO(1) - directO(n) - il faut parcourir
Insertion en têteO(n) - décalage de tous les élémentsO(1) - rapide
Insertion en finO(1) amortiO(n) sauf si on garde une référence vers la queue
Recherche d'une valeurO(n) - parcoursO(n) - parcours
class Maillon:
def __init__(self, valeur, suivant=None):
self.valeur = valeur
self.suivant = suivant
class ListeChainee:
def __init__(self):
self.tete = None
def est_vide(self):
return self.tete is None
def ajouter_en_tete(self, valeur):
nouveau = Maillon(valeur, self.tete)
self.tete = nouveau
def longueur(self):
compteur = 0
courant = self.tete
while courant is not None:
compteur += 1
courant = courant.suivant
return compteur
def contient(self, valeur):
courant = self.tete
while courant is not None:
if courant.valeur == valeur:
return True
courant = courant.suivant
return False
def __str__(self):
elements = []
courant = self.tete
while courant is not None:
elements.append(str(courant.valeur))
courant = courant.suivant
return " → ".join(elements)
lst = ListeChainee()
lst.ajouter_en_tete(3)
lst.ajouter_en_tete(8)
lst.ajouter_en_tete(5)
print(lst) # 5 → 8 → 3
print(lst.longueur()) # 3
print(lst.contient(8)) # True

La nature récursive de la liste chaînée se prête bien à des fonctions récursives :

def longueur_rec(maillon):
if maillon is None:
return 0
return 1 + longueur_rec(maillon.suivant)
def contient_rec(maillon, valeur):
if maillon is None:
return False
if maillon.valeur == valeur:
return True
return contient_rec(maillon.suivant, valeur)

Une pile (stack) suit le principe LIFO (Last In, First Out) : le dernier élément ajouté est le premier retiré. Comme une pile d'assiettes.

OpérationDescription
empiler(x)Ajouter x au sommet
depiler()Retirer et renvoyer l'élément du sommet
est_vide()Tester si la pile est vide
sommet()Consulter l'élément du sommet sans le retirer
class Pile:
def __init__(self):
self.elements = []
def empiler(self, x):
self.elements.append(x)
def depiler(self):
assert not self.est_vide(), "Pile vide"
return self.elements.pop()
def est_vide(self):
return len(self.elements) == 0
def sommet(self):
assert not self.est_vide(), "Pile vide"
return self.elements[-1]
def __str__(self):
return str(self.elements)
p = Pile()
p.empiler(10)
p.empiler(20)
p.empiler(30)
print(p.sommet()) # 30
print(p.depiler()) # 30 (le dernier ajouté)
print(p.depiler()) # 20
class PileChainee:
def __init__(self):
self.tete = None
def empiler(self, x):
self.tete = Maillon(x, self.tete)
def depiler(self):
assert self.tete is not None, "Pile vide"
valeur = self.tete.valeur
self.tete = self.tete.suivant
return valeur
def est_vide(self):
return self.tete is None
def sommet(self):
assert self.tete is not None, "Pile vide"
return self.tete.valeur

Deux implémentations différentes, même interface : c'est la distinction interface/implémentation en action.

  • Historique de navigation : le bouton « retour » dépile la dernière page visitée ;
  • Annulation (Ctrl+Z) : chaque action est empilée, annuler = dépiler ;
  • Évaluation d'expressions avec parenthèses ;
  • Appels de fonctions : chaque appel est empilé, le retour dépile.

Une file (queue) suit le principe FIFO (First In, First Out) : le premier élément ajouté est le premier retiré. Comme une file d'attente.

OpérationDescription
enfiler(x)Ajouter x à la fin
defiler()Retirer et renvoyer l'élément du début
est_vide()Tester si la file est vide
class FileTableau:
def __init__(self):
self.elements = []
def enfiler(self, x):
self.elements.append(x)
def defiler(self):
assert not self.est_vide(), "File vide"
return self.elements.pop(0)
def est_vide(self):
return len(self.elements) == 0

Une file peut être réalisée avec deux piles, ce qui donne un coût amorti en O(1) par opération :

class File:
def __init__(self):
self.entree = Pile()
self.sortie = Pile()
def enfiler(self, x):
self.entree.empiler(x)
def defiler(self):
if self.sortie.est_vide():
while not self.entree.est_vide():
self.sortie.empiler(self.entree.depiler())
return self.sortie.depiler()
def est_vide(self):
return self.entree.est_vide() and self.sortie.est_vide()

Le transfert inverse l'ordre : le premier entré se retrouve au sommet de la pile de sortie.

  • File d'impression : les documents s'impriment dans l'ordre d'envoi ;
  • Gestion de processus par le système d'exploitation (ordonnancement) ;
  • Parcours en largeur d'un graphe ou d'un arbre ;
  • Mémoire tampon (buffer) : données traitées dans l'ordre d'arrivée.

Un dictionnaire associe des clés à des valeurs. Chaque clé est unique et permet d'accéder directement à la valeur associée.

eleve = {
"nom": "Dupont",
"prenom": "Alice",
"age": 17,
"classe": "Terminale"
}
print(eleve["prenom"]) # "Alice"
eleve["moyenne"] = 15.5 # ajout d'une entrée
del eleve["age"] # suppression
OpérationDescriptionComplexité moyenne
d[cle]Accéder à la valeur associée à cleO(1)
d[cle] = valAjouter ou modifier une entréeO(1)
del d[cle]Supprimer une entréeO(1)
cle in dTester la présence d'une cléO(1)
OpérationListeDictionnaire
Recherche par valeurO(n) - parcours séquentielO(1) - par la clé
Accès par positionO(1) - par indexNon applicable
Ordre des élémentsPar position (index)Par insertion (Python 3.7+)

SituationStructure adaptéeRaison
Historique / annulationPile (LIFO)On revient à l'action la plus récente
File d'attente / impressionFile (FIFO)On traite dans l'ordre d'arrivée
Insertions/suppressions en tête fréquentesListe chaînéeO(1) en tête
Accès rapide par identifiantDictionnaireO(1) par clé
Accès rapide par positionTableau (list)O(1) par index

StructurePrincipeOpérations clésComplexité
Liste chaînéeMaillons reliés par des référencesAjout en tête, parcoursO(1) en tête, O(n) accès
PileLIFO (dernier entré, premier sorti)empiler, dépiler, sommetO(1)
FileFIFO (premier entré, premier sorti)enfiler, défilerO(1) amorti
DictionnairePaires clé-valeurAccès, ajout, suppressionO(1) en moyenne