Exercice 1 — QCM : piles et files
Pour chaque question, une seule réponse est correcte.
1. On empile successivement les lettres A, B, C, D dans une pile initialement vide. On dépile ensuite deux fois. Quelle est la lettre au sommet de la pile ?
- A. A
- B. B
- C. C
- D. D
Correction
B. On empile dans l’ordre A (fond), B, C, D (sommet). Le premier depiler renvoie D, le second renvoie C. Le sommet est maintenant B. L’erreur fréquente A (réponse du fond de pile) confond pile et file. L’erreur C oublie qu’on a dépilé deux fois.
2. On enfile les lettres A, B, C dans une file, puis on défile une fois. Quelle lettre est renvoyée ?
- A. C (la dernière enfilée)
- B. B (celle du milieu)
- C. A (la première enfilée)
- D. une erreur car la file est pleine
Correction
C. Une file est en mode FIFO (premier entré, premier sorti). Le premier élément enfilé est A, c’est donc le premier à être défilé. L’erreur A confond pile (LIFO) et file (FIFO).
3. Parmi les situations suivantes, laquelle correspond à une pile ?
- A. les clients qui font la queue à la boulangerie
- B. la fonction « annuler » (Ctrl-Z) d’un éditeur de texte
- C. les documents envoyés à une imprimante partagée
- D. les voitures qui passent à un péage dans l’ordre d’arrivée
Correction
B. La fonction Ctrl-Z annule la dernière action effectuée (LIFO). Les clients à la boulangerie (A), l’imprimante (C) et le péage (D) fonctionnent en FIFO : le premier arrivé est servi en premier.
4. Quelle opération est interdite sur une pile ?
- A. consulter l’élément au sommet sans le retirer
- B. empiler un nouvel élément
- C. accéder directement au troisième élément en partant du fond
- D. dépiler l’élément au sommet
Correction
C. Par définition, une pile ne donne accès qu’à son sommet. On ne peut pas accéder directement à un élément situé en profondeur sans dépiler les éléments au-dessus. A, B et D sont les opérations fondamentales d’une pile.
5. On implémente une pile à l’aide du type list de Python en utilisant append pour empiler et pop() pour dépiler. Quelle est la complexité de ces deux opérations ?
- A.
appenden \(O(n)\),pop()en \(O(n)\) - B.
appenden \(O(1)\) amorti,pop()en \(O(1)\) - C.
appenden \(O(1)\),pop()en \(O(n)\) - D.
appenden \(O(n)\),pop()en \(O(1)\)
Correction
B. Avec le type list de Python, append ajoute en fin de tableau en temps constant amorti et pop() (sans argument) retire en fin de tableau en \(O(1)\). L’erreur C correspondrait à pop(0) qui retire au début et nécessite un décalage.
Exercice 2 — Exercice guidé : dérouler empiler/dépiler pas à pas
On considère une pile p initialement vide. On exécute les instructions suivantes :
p.empiler(4)
p.empiler(7)
p.empiler(2)
a = p.depiler()
p.empiler(9)
b = p.depiler()
p.empiler(1)
c = p.depiler()
- Recopier et compléter le tableau suivant en dessinant l’état de la pile après chaque instruction (le sommet est en haut) et en indiquant la valeur des variables
a,betc:
| Instruction | État de la pile (bas → haut) | Variable |
|---|---|---|
empiler(4) | ||
empiler(7) | ||
empiler(2) | ||
depiler() | a = | |
empiler(9) | ||
depiler() | b = | |
empiler(1) | ||
depiler() | c = |
- Après ces instructions, quels éléments restent dans la pile ? Dans quel ordre ?
Correction
| Instruction | État de la pile (bas → haut) | Variable |
|---|---|---|
empiler(4) | 4 | |
empiler(7) | 4, 7 | |
empiler(2) | 4, 7, 2 | |
depiler() | 4, 7 | a = 2 |
empiler(9) | 4, 7, 9 | |
depiler() | 4, 7 | b = 9 |
empiler(1) | 4, 7, 1 | |
depiler() | 4, 7 | c = 1 |
- Il reste deux éléments dans la pile : 4 (au fond) et 7 (au sommet).
Principe : à chaque depiler, c’est le dernier élément empilé qui est retiré. C’est le fonctionnement LIFO (Last In, First Out).
Exercice 3 — Exercice guidé : compléter une classe Pile à trous
Compléter les méthodes de la classe Pile implémentée avec le type list de Python.
class Pile:
def __init__(self):
self.contenu = ... # créer une liste vide
def est_vide(self):
return ... # vrai si la liste est vide
def empiler(self, element):
... # ajouter en fin de liste
def depiler(self):
if self.est_vide():
raise ValueError("pile vide")
return ... # retirer et renvoyer le dernier élément
def sommet(self):
"""Renvoie l'élément au sommet sans le retirer."""
if self.est_vide():
raise ValueError("pile vide")
return ... # accéder au dernier élément
def __len__(self):
return ... # nombre d'éléments
Correction
class Pile:
def __init__(self):
self.contenu = []
def est_vide(self):
return len(self.contenu) == 0
def empiler(self, element):
self.contenu.append(element)
def depiler(self):
if self.est_vide():
raise ValueError("pile vide")
return self.contenu.pop()
def sommet(self):
if self.est_vide():
raise ValueError("pile vide")
return self.contenu[-1]
def __len__(self):
return len(self.contenu)
Points clés :
appendajoute en fin : le sommet de la pile est le dernier élément de la liste.pop()sans argument retire et renvoie le dernier élément (le sommet).self.contenu[-1]accède au dernier élément sans le retirer.est_videvérifie que la liste interne est vide.
Exercice 4 — Implémenter une file
En s’inspirant de l’exercice précédent, écrire une classe File implémentée avec le type list de Python.
La classe doit comporter les méthodes suivantes :
est_vide(self): renvoieTruesi la file est vide ;enfiler(self, element): ajoute un élément en fin de file ;defiler(self): retire et renvoie l’élément en début de file ;__len__(self): renvoie le nombre d’éléments.
Indication : pour retirer le premier élément d’une liste, on utilise pop(0).
Tester avec :
f = File()
f.enfiler("Alice")
f.enfiler("Bob")
f.enfiler("Charlie")
print(f.defiler()) # doit afficher Alice
print(f.defiler()) # doit afficher Bob
Correction
class File:
def __init__(self):
self.contenu = []
def est_vide(self):
return len(self.contenu) == 0
def enfiler(self, element):
self.contenu.append(element)
def defiler(self):
if self.est_vide():
raise ValueError("file vide")
return self.contenu.pop(0)
def __len__(self):
return len(self.contenu)
Test :
Alice
Bob
C’est bien le comportement FIFO : Alice a été enfilée en premier, elle est défilée en premier.
Remarque sur la complexité : enfiler est en \(O(1)\) (ajout en fin), mais defiler est en \(O(n)\) car pop(0) décale tous les éléments restants. Pour une file efficace, on préfère une implémentation avec deux références (premier et dernier) ou le module collections.deque.
Exercice 5 — Notation polonaise inversée (NPI)
La notation polonaise inversée (NPI) est une façon d’écrire les calculs sans parenthèses. On écrit d’abord les opérandes, puis l’opérateur. Par exemple :
- \((3 + 4)\) s’écrit
3 4 +; - \((3 + 4) \times 2\) s’écrit
3 4 + 2 *; - \(3 + 4 \times 2\) s’écrit
3 4 2 * +.
L’évaluation utilise une pile selon l’algorithme suivant :
- si le jeton est un nombre, on l’empile ;
- si le jeton est un opérateur, on dépile deux opérandes, on effectue le calcul, et on empile le résultat.
- Déroulé guidé. Évaluer pas à pas l’expression
5 3 + 8 2 - *en complétant le tableau :
| Jeton lu | Action | État de la pile (bas → haut) |
|---|---|---|
5 | empiler 5 | 5 |
3 | ||
+ | ||
8 | ||
2 | ||
- | ||
* |
Écrire une fonction
evaluer_npi(expression)qui prend en paramètre une chaîne de caractères (jetons séparés par des espaces) et renvoie le résultat du calcul en utilisant la classePile.Tester avec
evaluer_npi("5 3 + 8 2 - *")qui doit renvoyer 48.
Correction
| Jeton lu | Action | État de la pile (bas → haut) |
|---|---|---|
5 | empiler 5 | 5 |
3 | empiler 3 | 5, 3 |
+ | dépiler 3 et 5, calculer 5 + 3 = 8, empiler 8 | 8 |
8 | empiler 8 | 8, 8 |
2 | empiler 2 | 8, 8, 2 |
- | dépiler 2 et 8, calculer 8 − 2 = 6, empiler 6 | 8, 6 |
* | dépiler 6 et 8, calculer 8 × 6 = 48, empiler 48 | 48 |
Le résultat est 48, ce qui correspond à \((5 + 3) \times (8 - 2) = 8 \times 6 = 48\).
Attention à l’ordre des opérandes : pour la soustraction et la division, le premier opérande dépilé est à droite de l’opérateur.
def evaluer_npi(expression):
p = Pile()
for jeton in expression.split():
if jeton in "+-*/":
b = p.depiler() # opérande droit (dépilé en premier)
a = p.depiler() # opérande gauche
if jeton == "+":
p.empiler(a + b)
elif jeton == "-":
p.empiler(a - b)
elif jeton == "*":
p.empiler(a * b)
elif jeton == "/":
p.empiler(a / b)
else:
p.empiler(float(jeton))
return p.depiler()
evaluer_npi("5 3 + 8 2 - *")renvoie bien48.0.
Exercice 6 — Vérification du parenthésage
Un problème classique en compilation est de vérifier si les parenthèses et crochets d’une expression sont correctement appariés. Par exemple :
"(a + [b * c])"est valide ;"(a + [b * c)]"est invalide (croisement des délimiteurs) ;"(a + b"est invalide (parenthèse non fermée).
Algorithme : on parcourt les caractères un par un. Si on rencontre un ouvrant (( ou [), on l’empile. Si on rencontre un fermant () ou ]), on vérifie que le sommet de la pile correspond à l’ouvrant attendu. À la fin, la pile doit être vide.
Dérouler l’algorithme sur
"[a + (b * c)]"en montrant l’état de la pile après chaque ouvrant ou fermant.Écrire une fonction
parenthesage_valide(expression)qui renvoieTruesi le parenthésage est correct,Falsesinon.Tester sur les exemples suivants :
"()"→True"([a + b] * (c - d))"→True"(a + b]"→False"(("→False
Correction
- Déroulé pour
"[a + (b * c)]":
| Caractère | Action | Pile |
|---|---|---|
[ | empiler [ | [ |
a, , +, | ignorer | [ |
( | empiler ( | [, ( |
b, , *, , c | ignorer | [, ( |
) | dépiler ( → correspond à ) ✓ | [ |
] | dépiler [ → correspond à ] ✓ | (vide) |
Pile vide à la fin → expression valide.
def parenthesage_valide(expression):
p = Pile()
correspondance = {")": "(", "]": "["}
for c in expression:
if c in "([":
p.empiler(c)
elif c in ")]":
if p.est_vide():
return False
if p.depiler() != correspondance[c]:
return False
return p.est_vide()
Principe : trois causes d’invalidité :
- pile vide quand on rencontre un fermant (pas d’ouvrant correspondant) ;
- l’ouvrant dépilé ne correspond pas au fermant rencontré (croisement) ;
- pile non vide à la fin (ouvrant non fermé).
- Tests :
parenthesage_valide("()")→True✓parenthesage_valide("([a + b] * (c - d))")→True✓parenthesage_valide("(a + b]")→False✓ (dépile(mais attend[pour])parenthesage_valide("((")→False✓ (pile non vide à la fin)
Exercice 7 — Inversion d’une file avec une pile
On souhaite inverser l’ordre des éléments d’une file sans utiliser de tableau ni de liste auxiliaire, mais uniquement une pile.
Expliquer en quelques phrases le principe de la méthode.
Écrire une fonction
inverser_file(f)qui prend une filefen paramètre et inverse ses éléments sur place.Tester : si la file contient initialement
A ← B ← C(A en tête), après inversion elle doit contenirC ← B ← A(C en tête).
Correction
Principe : on défile tous les éléments de la file et on les empile un par un dans une pile. Puis on dépile tous les éléments de la pile et on les enfile un par un dans la file. Comme la pile inverse l’ordre (LIFO), les éléments se retrouvent dans l’ordre inverse dans la file.
def inverser_file(f):
p = Pile()
# Phase 1 : vider la file dans la pile
while not f.est_vide():
p.empiler(f.defiler())
# Phase 2 : vider la pile dans la file
while not p.est_vide():
f.enfiler(p.depiler())
Vérification pas à pas :
File initiale :
A ← B ← C(A en tête, C en queue).Phase 1 (file → pile) :
defiler()→ A, empiler A. Pile : Adefiler()→ B, empiler B. Pile : A, Bdefiler()→ C, empiler C. Pile : A, B, C (C au sommet)
Phase 2 (pile → file) :
depiler()→ C, enfiler C. File : Cdepiler()→ B, enfiler B. File : C ← Bdepiler()→ A, enfiler A. File : C ← B ← A
File finale :
C ← B ← A(C en tête). L’inversion est correcte. ✓
Exercice 8 — Synthèse : simuler une file d’impression
On modélise une file d’attente d’imprimante. Chaque document est représenté par un dictionnaire {"nom": ..., "pages": ...}.
Créer une classe
FileImpressionavec les méthodes :soumettre(self, nom, pages): ajoute un document à la file ;imprimer_suivant(self): retire le prochain document de la file, affiche"Impression de [nom] (X pages)"et renvoie le nombre de pages ;documents_en_attente(self): renvoie le nombre de documents restants ;pages_restantes(self): renvoie le nombre total de pages en attente.
Simuler le scénario suivant :
- Alice soumet « Rapport » (12 pages), puis Bob soumet « CV » (2 pages) ;
- l’imprimante imprime un document ;
- Charlie soumet « Thèse » (150 pages) ;
- l’imprimante imprime les deux documents restants.
Afficher le nombre total de pages imprimées.
Correction
class FileImpression:
def __init__(self):
self.file = File()
def soumettre(self, nom, pages):
self.file.enfiler({"nom": nom, "pages": pages})
def imprimer_suivant(self):
if self.file.est_vide():
print("Aucun document en attente.")
return 0
doc = self.file.defiler()
print(f"Impression de {doc['nom']} ({doc['pages']} pages)")
return doc["pages"]
def documents_en_attente(self):
return len(self.file)
def pages_restantes(self):
total = 0
# Parcours non destructif : on défile et ré-enfile
n = len(self.file)
for _ in range(n):
doc = self.file.defiler()
total += doc["pages"]
self.file.enfiler(doc)
return total
imprimante = FileImpression()
total = 0
imprimante.soumettre("Rapport", 12)
imprimante.soumettre("CV", 2)
# File : Rapport (12p) ← CV (2p)
total += imprimante.imprimer_suivant()
# Impression de Rapport (12 pages)
# File : CV (2p)
imprimante.soumettre("Thèse", 150)
# File : CV (2p) ← Thèse (150p)
total += imprimante.imprimer_suivant()
# Impression de CV (2 pages)
total += imprimante.imprimer_suivant()
# Impression de Thèse (150 pages)
print(f"Total : {total} pages imprimées")
# Total : 164 pages imprimées
- Le nombre total de pages imprimées est 164.
Remarque : les documents sont imprimés dans l’ordre d’arrivée (FIFO), ce qui est le comportement attendu d’une file d’impression.