backend / algorithmes-structures-donnees
Piles et files
Explication
Ce que vous allez apprendre
- Différencier LIFO (pile) et FIFO (file) et associer chacune à un cas d'usage concret
- Expliquer pourquoi restreindre l'accès à une ou deux extrémités garantit des opérations en O(1)
- Repérer pourquoi
list.pop(0)en Python est un piège de performance pour implémenter une file - Utiliser
collections.dequecomme structure de référence pour une file efficace - Appliquer la technique du "deque d'indices" pour résoudre un problème de maximum en fenêtre glissante
Dans quel contexte ?
Un développeur implémente le bouton "annuler" (Ctrl+Z) d'un éditeur de texte collaboratif : chaque action de l'utilisateur est empilée, et cliquer sur "annuler" dépile la dernière action pour l'inverser — un usage naturel de pile (LIFO). Au même moment, le système de notifications de la même application doit envoyer les alertes dans leur ordre exact d'arrivée : la première notification créée doit être livrée avant les suivantes, ce qui impose une file (FIFO). Confondre les deux structures produirait un "annuler" qui défait la mauvaise action, ou des notifications reçues dans le désordre.
D'abord, deux disciplines d'accès opposées
Une pile, c'est une pile d'assiettes : on ne peut poser ou retirer qu'au sommet, donc le dernier élément posé est le premier retiré, LIFO. Une file, c'est une file d'attente à la caisse : le premier arrivé est le premier servi, FIFO.
| Structure | Ajout | Retrait | Ordre | Structure Python recommandée |
|---|---|---|---|---|
| Pile (Stack) | O(1) au sommet | O(1) au sommet | LIFO — dernier entré, premier sorti | list (append / pop) |
| File (Queue) | O(1) en fin | O(1) en tête | FIFO — premier entré, premier sorti | collections.deque |
Une clarification importante
Ce ne sont pas deux variantes d'une même idée : elles modélisent deux logiques d'ordre totalement opposées, et le choix de l'une ou l'autre découle directement du problème à résoudre.
Pourquoi restreindre l'accès est en fait une force
Interdire l'accès libre semble une contrainte, mais c'est justement ce qui rend ces structures rapides et prévisibles : toutes les opérations se font en O(1), sans jamais parcourir la collection.
Ce que chaque structure capture naturellement
La pile capture une notion d'imbrication, comme des parenthèses ou des appels de fonctions : le dernier contexte ouvert doit être le premier refermé. La file capture une notion de traitement séquentiel équitable : une tâche arrivée en premier ne doit pas attendre indéfiniment derrière des arrivées plus récentes.
Piège fréquent
Une liste Python n'est performante en O(1) qu'à une seule extrémité, la fin. L'utiliser comme file avec pop(0) semble correcte fonctionnellement mais coûte O(n) à chaque appel, car tout le reste doit être décalé — sur une file de 100 000 éléments, cela transforme un traitement censé être instantané en un calvaire quadratique.
La solution : la bonne structure pour le bon usage
collections.deque est conçue spécifiquement pour offrir O(1) aux deux extrémités — c'est elle qu'il faut utiliser pour une file, jamais une liste Python classique.
Astuce
deque(maxlen=N) crée automatiquement une file à taille bornée : dès qu'un nouvel élément est ajouté au-delà de la limite, le plus ancien est éjecté tout seul. Pratique pour garder "les 10 dernières mesures" d'un capteur sans gérer la purge manuellement.
Pour aller plus loin
La technique du "deque d'indices" pour le maximum en fenêtre glissante illustre une idée puissante : maintenir une structure qui ne garde que les candidats potentiellement utiles, en éliminant activement ceux qui ne pourront plus jamais être la réponse.
Vers la suite
Ce principe d'élimination active reviendra en algorithmique gloutonne. La prochaine leçon change de sujet et s'attaque à un mécanisme fondamental : la récursivité, une fonction qui s'appelle elle-même.
Commandes & code
Piles (Stack) et files (Queue)
from collections import deque
# --- Pile (LIFO : Last In, First Out) implémentée sur une liste Python ---
class Pile:
def __init__(self):
self._donnees: list = []
def empiler(self, valeur) -> None: # push, O(1) amorti
self._donnees.append(valeur)
def depiler(self): # pop, O(1)
if self.est_vide():
raise IndexError("Pile vide")
return self._donnees.pop()
def sommet(self): # peek, O(1)
if self.est_vide():
raise IndexError("Pile vide")
return self._donnees[-1]
def est_vide(self) -> bool:
return len(self._donnees) == 0
# Application : vérifier que des parenthèses/accolades/crochets sont bien équilibrés
def parentheses_valides(expression: str) -> bool:
correspondance = {')': '(', ']': '[', '}': '{'}
pile = Pile()
for caractere in expression:
if caractere in "([{":
pile.empiler(caractere)
elif caractere in ")]}":
if pile.est_vide() or pile.depiler() != correspondance[caractere]:
return False
return pile.est_vide()
assert parentheses_valides("({[]})") is True
assert parentheses_valides("({[}])") is False
# Application : évaluer une expression postfixée (notation polonaise inverse)
def evaluer_postfixe(tokens: list[str]) -> float:
pile = Pile()
operations = {
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: a / b,
}
for token in tokens:
if token in operations:
b = pile.depiler() # ordre important : b est dépilé avant a
a = pile.depiler()
pile.empiler(operations[token](a, b))
else:
pile.empiler(float(token))
return pile.depiler()
assert evaluer_postfixe(["3", "4", "+", "2", "*"]) == 14.0 # (3+4)*2
# --- File (FIFO : First In, First Out) implémentée avec deque (O(1) aux deux extrémités) ---
class File:
def __init__(self):
self._donnees: deque = deque()
def enfiler(self, valeur) -> None: # enqueue, O(1)
self._donnees.append(valeur)
def defiler(self): # dequeue, O(1) -- deque.popleft() vs list.pop(0) qui est O(n)
if self.est_vide():
raise IndexError("File vide")
return self._donnees.popleft()
def premier(self):
if self.est_vide():
raise IndexError("File vide")
return self._donnees[0]
def est_vide(self) -> bool:
return len(self._donnees) == 0
# --- File à double extrémité (Deque) : ajout/retrait aux deux bouts en O(1) ---
d = deque()
d.append(1) # ajoute à droite
d.appendleft(0) # ajoute à gauche
d.pop() # retire à droite
d.popleft() # retire à gauche
# Application : fenêtre glissante -- maximum de chaque sous-tableau de taille k
def max_fenetre_glissante(tableau: list[int], k: int) -> list[int]:
# O(n) au total grâce à un deque d'indices maintenu en ordre décroissant de valeur
resultat = []
fenetre = deque() # stocke des INDICES, pas des valeurs
for i, valeur in enumerate(tableau):
while fenetre and tableau[fenetre[-1]] <= valeur:
fenetre.pop() # retire les indices dont la valeur est dominée
fenetre.append(i)
if fenetre[0] <= i - k:
fenetre.popleft() # l'indice est sorti de la fenêtre
if i >= k - 1:
resultat.append(tableau[fenetre[0]])
return resultat
assert max_fenetre_glissante([1, 3, -1, -3, 5, 3, 6, 7], 3) == [3, 3, 5, 5, 6, 7]Résumé
- Pile (LIFO) :
append/popen fin de liste Python, O(1). Usages : parenthésage, undo, DFS itératif. - File (FIFO) :
deque.append/popleft, O(1) aux deux bouts. Unelist.pop(0)classique est O(n), à éviter pour une file. collections.dequeest la structure Python idéale pour pile ET file (double extrémité efficace).- Le pattern "deque d'indices" résout le maximum en fenêtre glissante en O(n) au lieu de O(n*k) naïvement.
Exercices pratiques
Mission : réparer le vérificateur de code qui plante sur des fichiers volumineux
Objectif : Diagnostiquer un mauvais choix de structure pour une file, puis produire un vérificateur de parenthèses robuste.
Contexte
Un éditeur de code doit vérifier en direct que chaque (, [ et { ouvert est bien refermé dans le bon ordre, et un système de file d'attente doit traiter les fichiers soumis dans leur ordre d'arrivée exact. Un développeur junior a implémenté la file d'attente avec une list Python et pop(0), et sur un pic de 50 000 fichiers en attente, le serveur se met à ramer fortement.
Tu dois d'abord corriger la structure de la file, puis réfléchir à la logique du vérificateur de parenthèses lui-même.