Implémenter une structure — l'exemple de la file FIFO
Introduction
Le cours précédent a posé un principe : une interface est un contrat, une implémentation est un code qui le réalise — et un même contrat admet plusieurs implémentations. Le moment est venu d'en faire l'expérience sur un exemple canonique de Terminale : la file FIFO.
Vous allez voir deux classes, FileTableau et FileDeuxPiles, qui exposent la
même interface — quatre méthodes au comportement strictement identique du point
de vue de l'utilisateur — mais dont les performances diffèrent radicalement.
C'est l'illustration parfaite de l'item : « écrire plusieurs
implémentations d'une même structure de données ».
class, __init__,
self, méthode, encapsulation). Si l'un de ces termes vous échappe, retournez
au cours 1.L'interface d'une file FIFO
Une file (en anglais queue) est une structure linéaire au comportement FIFO — First In, First Out. L'analogie est immédiate : la file d'attente à la boulangerie. On entre par la queue, on sort par la tête, on ne double pas.
Son interface comporte typiquement quatre opérations :
| Méthode | Signature | Effet attendu |
|---|---|---|
enfiler(x) | File × Élément → File | Ajoute x à la queue de la file. |
defiler() | File → Élément | Retire et renvoie l'élément de tête ; lève une exception si la file est vide. |
est_vide() | File → Booléen | Renvoie True si la file est vide, False sinon. |
taille() | File → Entier | Renvoie le nombre d'éléments. |
Dans une file FIFO, le prochain élément à sortir est…
Implémentation 1 — avec un tableau (liste Python)
La représentation la plus naturelle utilise une liste Python comme stockage interne. Les éléments sont rangés dans l'ordre d'arrivée ; on enfile à la fin, on défile au début.
class FileTableau:
"""File FIFO implémentée par une liste Python."""
def __init__(self):
self._data = []
def enfiler(self, x):
self._data.append(x) # O(1) amorti
def defiler(self):
if not self._data:
raise IndexError("file vide")
return self._data.pop(0) # O(n) — décalage de tous les éléments
def est_vide(self):
return len(self._data) == 0
def taille(self):
return len(self._data)
Traçons un scénario complet pour vérifier la sémantique FIFO :
f = FileTableau()
f.enfiler(1)
f.enfiler(2)
f.enfiler(3)
print(f.defiler()) # 1 — le premier entré
print(f.defiler()) # 2
print(f.taille()) # 1
Analyse de complexité :
enfilerrepose surlist.append, qui s'exécute en amorti.defilerrepose surlist.pop(0), qui décale tous les éléments restants vers la gauche — coût en où est la taille courante.
Cette implémentation est simple à lire mais non optimale. Sur une file
de un million d'éléments, chaque defiler réalise un million d'écritures
mémoire — inacceptable en production.
Implémentation 2 — avec deux piles
Une astuce classique : utiliser deux piles pour simuler une file. La pile
d'entrée reçoit les enfiler ; la pile de sortie sert aux defiler.
Quand la sortie est vide, on transvase tout le contenu de l'entrée dedans, ce
qui inverse l'ordre — et le premier entré redevient le premier sorti.
class FileDeuxPiles:
"""File FIFO implémentée par deux piles (listes utilisées en LIFO)."""
def __init__(self):
self._entree = [] # pile : on append/pop en bout
self._sortie = []
def enfiler(self, x):
self._entree.append(x) # O(1)
def defiler(self):
if not self._sortie:
while self._entree:
self._sortie.append(self._entree.pop())
if not self._sortie:
raise IndexError("file vide")
return self._sortie.pop() # O(1) amorti
def est_vide(self):
return not self._entree and not self._sortie
def taille(self):
return len(self._entree) + len(self._sortie)
Traçons le même scénario qu'avant — l'observation extérieure est identique :
f = FileDeuxPiles()
f.enfiler(1)
f.enfiler(2)
f.enfiler(3)
print(f.defiler()) # 1
print(f.defiler()) # 2
print(f.taille()) # 1
Pour défiler avec deux piles, on transvase l'entrée vers la sortie uniquement quand…
Pourquoi est-ce plus rapide ?
À première vue, le transvasement coûte cher : il faut bien opérations pour basculer éléments. Mais regardez ce qui se passe sur une longue suite d'opérations.
Analyse amortie
Chaque élément, sur toute sa durée de vie, subit exactement quatre manipulations de pile :
- un
appenddans_entreeà sonenfiler; - un
popde_entreelors du transvasement ; - un
appenddans_sortielors du transvasement ; - un
popde_sortieà sondefiler.
Sur une suite de opérations, le coût total est en — soit amorti par opération.
C'est la beauté de la complexité amortie : on ne raisonne pas sur le pire cas d'une opération isolée (qui peut être un gros transvasement), mais sur le coût moyen observé sur une longue séquence d'opérations.
La promesse de l'abstraction
Les deux classes FileTableau et FileDeuxPiles exposent exactement la même
interface. Un code qui utilise f.enfiler(x) puis f.defiler() fonctionne
sans modification, quelle que soit la classe choisie. C'est la promesse de
l'abstraction : on peut changer le moteur sans changer la carrosserie.
# Ce code fonctionne identiquement avec FileTableau OU FileDeuxPiles
def traiter_taches(file_de_taches):
while not file_de_taches.est_vide():
tache = file_de_taches.defiler()
# ... traiter la tâche ...
f._data[0] au lieu de f.defiler(),
vous brisez ce contrat — et vous perdez la liberté de changer d'implémentation
sans casser le programme.Récapitulatif comparatif
| Critère | FileTableau | FileDeuxPiles |
|---|---|---|
| Stockage interne | une liste | deux listes (piles) |
enfiler | amorti | |
defiler | amorti | |
Pire cas d'un defiler isolé | avec = taille de l'entrée au moment du transvasement | |
| Lisibilité du code | très simple | demande l'astuce du transvasement |
Les deux respectent le contrat ; la deuxième est asymptotiquement meilleure. Le
choix d'implémentation dépend du contexte : si la file reste petite et que
la simplicité prime, FileTableau suffit ; si la performance compte,
FileDeuxPiles (ou mieux, collections.deque — voir ci-dessous).
Pour aller plus loin
Le module standard collections.deque fournit une file double —
à chaque bout — implémentée en C. C'est ce que vous utiliserez en pratique :
from collections import deque
f = deque()
f.append(1) # enfiler
f.append(2)
print(f.popleft()) # defiler → 1
Mais savoir réimplémenter une file à partir de structures plus primitives est exigible au baccalauréat — c'est précisément ce que vous venez de faire, deux fois.