S01 — Structures de données
Listes, piles, files, dictionnaires et listes chaînées
🎯 Introduction
Partie 1 — Premier chapitre de Terminale : sans classes
Prérequis : variables, conditions, boucles, fonctions, listes, tuples et dictionnaires de Première. Tu peux suivre le cours, les activités, les exercices et le QCM sans avoir étudié la programmation orientée objet.
Utiliser liste.append(x) ou F.popleft(), c’est appeler une opération déjà fournie par Python. Cela ne demande pas de savoir écrire une classe. Les mots class, self et __init__ sont réservés à la partie 2, à lire plus tard.
Les structures de données permettent d'organiser et de stocker les informations en mémoire. Le choix d'une structure adaptée est crucial pour l'efficacité d'un algorithme.
✅ Ce qu'il faut absolument savoir pour le bac
- Spécifier une structure de données par son interface (la liste des opérations qu'elle propose)
- Distinguer interface et implémentation : deux implémentations différentes peuvent respecter la même interface
- Distinguer les structures par les opérations qui les caractérisent (LIFO, FIFO, accès par clé)
- Choisir une structure de données adaptée à la situation à modéliser
- Distinguer la recherche d'une valeur dans une liste et dans un dictionnaire
- Implémenter une pile et une file avec une liste Python (et savoir pourquoi
dequepour une file) - Ne pas confondre le type abstrait liste (au sens Lisp,
cons/car/cdr) et les listes Python, qui sont en réalité des tableaux dynamiques — ce sont des « faux amis »
🎮 Cours ↔ TP2 — Piles et files
Observe ici les opérations, puis programme-les dans NSI Arcade : Bataille et PyLand pour FIFO, Hanoï pour LIFO. Les aides sont facultatives et les premières missions ne demandent ni classes ni récursion.
Ouvrir le TP2 — Piles et files →🖨️ Du chapitre S01 à l’objet réel : NSI Maker
Fabrique des disques en Python et OpenSCAD, puis utilise-les pour manipuler trois piles de Hanoï et le tri de crêpes de l’exercice 6. Une file FIFO interactive fait aussi le lien avec les activités sur les files.
Explorer la passerelle piles & files →🔁 Rappels
Nous avons vu l'année dernière comment étaient codées les données au sein d'un ordinateur, et nous avons utilisé des types simples et composés de Python. De nombreux algorithmes « classiques » manipulent des structures de données plus complexes que de simples nombres.
Les types simples
Un objet de type simple ne stocke qu'une donnée.
| Type | Exemples |
|---|---|
entier (int) | 1, 12, -4 |
nombre flottant (float) | 1.1, 12.0, -25E2 |
chaîne de caractères (str) | "Du texte", 'Un autre texte' |
booléen (bool) | True, False |
Les types construits
Les types construits permettent de stocker des collections de données.
| Type | Exemple | Accès |
|---|---|---|
tableau (list) | l = [1, 12, -4] | l[2] → -4 (par indice) |
dictionnaire (dict) | d = {'nom': 'Gaston', 'age': 25} | d['age'] → 25 (par clef) |
Méthodes d'itération
On peut itérer sur une liste par les valeurs ou par les index ; sur un dictionnaire, par les clés, les valeurs, ou les deux à la fois.
# Itération sur les valeurs d'une liste
for val in ma_liste:
print(val)
# Itération sur les index (on a aussi accès à la valeur)
for i in range(len(ma_liste)):
val = ma_liste[i]
print("indice:", i, "valeur:", val)
# Itération sur les clés d'un dictionnaire
for key in D: # équivalent à : for key in D.keys():
print(key)
# Itération sur les valeurs
for value in D.values():
print(value)
# Itération sur les paires clé/valeur
for key, value in D.items():
print(key, '=>', value)
📋 Listes et tableaux
Le type abstrait liste (au sens Lisp)
Le langage Lisp (John McCarthy, 1958 — Lisp signifie list processing) a introduit la notion de liste comme type abstrait : une liste L se décompose en sa tête (car, le dernier élément ajouté) et sa queue (cdr, le reste de la liste). Cinq opérations suffisent à la spécifier :
| Opération | Rôle |
|---|---|
vide() | renvoie une liste vide (souvent notée nil) |
estVide(L) | teste si la liste L est vide |
cons(x, L) | construit une nouvelle liste en ajoutant x en tête de L |
car(L) | renvoie le dernier élément ajouté (la tête), sans modifier L |
cdr(L) | renvoie une liste contenant tous les éléments de L sauf la tête |
Exemple (les instructions s'enchaînent) :
L = vide() # L est vide
estVide(L) # renvoie True
L1 = cons(12, L) # L1 contient 12
estVide(L1) # renvoie False
L1 = cons(15, L1) # L1 contient 15 et 12
L1 = cons(1, cons(11, L1)) # L1 contient 1, 11, 15 et 12 (on peut enchaîner les cons)
car(L1) # renvoie 1 — L1 reste inchangée
L2 = cdr(L1) # L2 contient 11, 15 et 12 — L1 reste inchangée
On implémente facilement ce type abstrait en Python avec des tuples :
def vide(): return None
def estVide(L): return L is None
def cons(x, L): return (x, L)
def car(L): return L[0]
def cdr(L): return L[1]
Faux amis
Ce type abstrait liste n'a rien à voir avec les « listes Python » (list) manipulées ci-dessous : les listes Python sont en réalité des tableaux dynamiques. Même nom, deux notions différentes — à ne jamais confondre le jour du bac.
En pratique : les listes Python (tableaux dynamiques)
En Python, le type list est un tableau dynamique : les éléments sont stockés de façon contiguë en mémoire, et Python gère automatiquement le redimensionnement.
| Opération | Complexité | Exemple |
|---|---|---|
| Accès par indice | O(1) | tab[i] |
| Ajout en fin | O(1) amorti | tab.append(x) |
| Insertion en début | O(n) | tab.insert(0, x) |
| Suppression en fin | O(1) | tab.pop() |
| Recherche d'une valeur | O(n) | x in tab |
| Longueur | O(1) | len(tab) |
# Création et opérations courantes
tab = [3, 1, 4, 1, 5]
tab.append(9) # [3,1,4,1,5,9]
tab.insert(0, 0) # [0,3,1,4,1,5,9] — O(n) !
tab.pop() # retire 9, O(1)
tab.pop(0) # retire 0, O(n) !
# Compréhension de liste
carres = [x**2 for x in range(5)] # [0,1,4,9,16]
pairs = [x for x in tab if x % 2 == 0]
📚 Piles — LIFO
🍽️ Une pile d’assiettes
Sommet en haut : on pose et on retire seulement l’assiette du dessus. Dans le code ci-dessous, le sommet est à droite.
1, puis 2, puis 3 ont été ajoutés. Prédis le prochain retrait.
Limite visuelle : 6 éléments. Une structure vide ne permet pas de retrait.💡 Pourquoi ce n’est pas le même ordre ?
Le dernier élément ajouté occupe le sommet : 3 sort avant 2, puis 1. Empiler 4 puis dépiler rend donc 4.
Une pile (stack) suit le principe LIFO : Last In, First Out (dernier entré, premier sorti). Comme une pile d'assiettes : la seule assiette directement accessible est la dernière posée.
L'interface d'une pile se résume à quatre opérations : estVide(P), push(P, val) (empiler), pop(P) (dépiler — renvoie le sommet et le supprime) et taille(P).
Exemple — soit une pile P composée de 12, 14, 8, 7, 19 et 22 (le sommet est 22). Pour chaque ligne ci-dessous, on repart de la pile d'origine :
| Instruction | Effet |
|---|---|
pop(P) | renvoie 22 ; P devient 12, 14, 8, 7, 19 (sommet : 19) |
push(P, 42) | P devient 12, 14, 8, 7, 19, 22, 42 (sommet : 42) |
pop(P) × 6 | estVide(P) renvoie True |
après un pop(P) | taille(P) renvoie 5 |
Cas d'usage réels
Pile d’appels de fonctions (récursion : découverte facultative ci-dessous), annulation dans un éditeur (Ctrl+Z), navigation web (bouton Retour), vérification des parenthèses.
# Implémentation d'une pile avec une liste Python
pile = []
# Empiler (push)
pile.append('A') # pile = ['A']
pile.append('B') # pile = ['A','B']
pile.append('C') # pile = ['A','B','C']
# Dépiler (pop) — retire le DERNIER élément
sommet = pile.pop() # sommet = 'C', pile = ['A','B']
# Consulter sans dépiler (peek)
if pile: # vérifier non-vide avant !
sommet = pile[-1] # 'B' sans modifier la pile
💡 Découvrir la récursion — pas encore étudiée
Une fonction est récursive lorsqu’elle s’appelle elle-même pour traiter un problème plus petit. Il faut un cas d’arrêt et s’en rapprocher à chaque appel.
def compte_a_rebours(n):
# Précondition : n est un entier positif ou nul.
if n == 0:
print("Départ !")
return
print(n)
compte_a_rebours(n - 1)
compte_a_rebours(3) # affiche 3, 2, 1, puis Départ !Trace : appel avec 3 → 2 → 1 → 0 ; puis les appels se terminent dans l’ordre inverse. Les appels en attente forment une pile. Sans arrêt, ou sans diminution de n, les appels finissent par provoquer une erreur RecursionError.
Pour Hanoï, résoudre avec n disques se ramène à déplacer n−1 disques, déplacer le plus grand, puis déplacer à nouveau les n−1 petits. Commence par les déplacements manuels : la résolution récursive est un prolongement guidé, pas un prérequis.
Définir l’interface avec des fonctions
Dans les activités, push(P, x) correspond à empiler(P, x) et pop(P) à depiler(P). Le sommet est à droite dans notre liste Python.
def pileVide():
return []
def estVide(P):
return len(P) == 0
def empiler(P, valeur):
P.append(valeur)
def depiler(P):
# Précondition : P n'est pas vide.
return P.pop()
def taille(P):
return len(P)
P = pileVide()
empiler(P, 34)
empiler(P, 76)
if not estVide(P):
a = depiler(P) # a = 76 ; P = [34]
Exécute chaque exemple dans son propre fichier : le nom estVide sera aussi utilisé pour les listes chaînées, avec une représentation différente.
🚌 Files — FIFO
🎢 Une file devant une attraction
Sortie à gauche ← visiteurs dans l’ordre d’arrivée ← entrée à droite. Le nouveau visiteur rejoint la fin.
1, puis 2, puis 3 ont été ajoutés. Prédis le prochain retrait.
Limite visuelle : 6 éléments. Une structure vide ne permet pas de retrait.💡 Pourquoi ce n’est pas le même ordre ?
L’ancienneté décide : 1 sort avant 2, puis 3. Enfiler 4 ne lui donne pas le droit de passer devant.
Une file (queue) suit le principe FIFO : First In, First Out (premier entré, premier sorti). Comme une file d'attente : on ajoute à une extrémité (enqueue, enfiler) et on retire à l'autre (dequeue, défiler).
L'interface d'une file : estVide(F), enqueue(F, val) (enfiler), dequeue(F) (défiler — renvoie l'élément le plus ancien et le supprime) et taille(F).
Exemple — soit une file F composée de 12, 14, 8, 7, 19 et 22, où 22 est le premier élément entré et 12 le dernier. Pour chaque ligne ci-dessous, on repart de la file d'origine :
| Instruction | Effet |
|---|---|
enqueue(F, 42) | F devient 42, 12, 14, 8, 7, 19, 22 (le plus ancien reste 22, le plus récent devient 42) |
dequeue(F) | F devient 12, 14, 8, 7, 19 (22 est retiré, le plus ancien devient 19) |
dequeue(F) × 6 | estVide(F) renvoie True |
après un dequeue(F) | taille(F) renvoie 5 |
Une file avec une liste : possible, mais moins efficace
Une liste avec append et pop(0) respecte FIFO, mais retirer en tête coûte O(n). collections.deque permet des ajouts à droite et des retraits à gauche en O(1).
from collections import deque
file = deque() # file vide
# Enfiler (enqueue) — ajouter en queue
file.append('A') # file = deque(['A'])
file.append('B') # file = deque(['A','B'])
# Défiler (dequeue) — retirer en tête — O(1) !
premier = file.popleft() # premier = 'A'
Définir l’interface avec des fonctions
Dans cette représentation Python, la sortie (le plus ancien) est à gauche et l’entrée à droite. Le dessin précédent utilise l’orientation inverse : la règle FIFO reste la même.
from collections import deque
def fileVide():
return deque()
def estVide(F):
return len(F) == 0
def enqueue(F, valeur):
F.append(valeur)
def dequeue(F):
# Précondition : F n'est pas vide.
return F.popleft()
def taille(F):
return len(F)
F = fileVide()
enqueue(F, 67)
enqueue(F, 34)
if not estVide(F):
a = dequeue(F) # a = 67 ; F contient encore 34
Exécute chaque exemple dans son propre fichier : le nom estVide sera aussi utilisé pour les listes chaînées, avec une représentation différente.
🗝️ Dictionnaires
Un dictionnaire (table de hachage) associe des clés à des valeurs. Les clés doivent être hashables (immuables : str, int, tuple).
| Opération | Complexité | Exemple |
|---|---|---|
| Accès/Modification | O(1) moy. | d[cle] |
| Test d'appartenance | O(1) moy. | cle in d |
| Insertion | O(1) moy. | d[cle] = val |
| Suppression | O(1) moy. | del d[cle] |
| Parcours | O(n) | for k, v in d.items() |
# Création
notes = {'Alice': 18, 'Bob': 14, 'Chloe': 16}
# Accès (KeyError si absent → utiliser get)
n = notes.get('David', 0) # renvoie 0 par défaut
# Test d'appartenance
if 'Alice' in notes:
print(notes['Alice']) # 18
# Parcours
for nom, note in notes.items():
print(f"{nom} : {note}")
# Compréhension de dictionnaire
maj = {k.upper(): v for k, v in notes.items()}
# setdefault / defaultdict
from collections import defaultdict
freq = defaultdict(int)
for c in "bonjour": freq[c] += 1
Depuis Python 3.7
Les dictionnaires préservent l'ordre d'insertion. C'est garanti par le langage.
🔗 Type abstrait et représentation concrète
Les listes, les piles et les files vues plus haut sont des types abstraits : des « vues de l'esprit », indépendantes de tout support informatique. Les implémenter, c'est les traduire dans un langage compréhensible par un ordinateur — et il existe plusieurs façons concrètes de le faire, selon le langage.
Deux grandes familles de représentation :
- Tableau : une suite contiguë de cases mémoire. Simple et rapide en accès direct, mais de taille fixe : insérer un élément suppose de recréer un tableau plus grand et d'y recopier les données.
- Tableau dynamique : un tableau dont la taille peut varier, ce qui facilite l'insertion. Les « listes Python » sont des tableaux dynamiques — pas le type abstrait liste vu plus haut (encore ces fameux faux amis).
- Liste chaînée : chaque élément (nœud) occupe deux cases mémoire, une pour la valeur et une pour l'adresse de l'élément suivant. Les éléments ne sont pas contigus en mémoire, mais l'insertion y est très simple : il suffit de rediriger quelques pointeurs.
| Opération | Liste chaînée | Tableau Python |
|---|---|---|
| Accès par indice | O(n) | O(1) |
| Insertion en tête | O(1) | O(n) |
| Insertion en queue | O(1) si pointeur fin | O(1) amorti |
| Suppression en tête | O(1) | O(n) |
| Recherche | O(n) | O(n) |
Une représentation avec des tuples, sans classes
Comme dans l’activité 6, une cellule est un couple (valeur, suite) ; None termine la chaîne. cons crée une nouvelle cellule sans modifier la suite existante. Il faut une liste non vide pour appeler car ou cdr.
def vide():
return None
def estVide(L):
return L is None
def cons(x, L):
return (x, L)
def car(L):
# Précondition : L n'est pas vide.
return L[0]
def cdr(L):
# Précondition : L n'est pas vide.
return L[1]
L = cons(12, cons(5, cons(32, vide())))
courant = L
while not estVide(courant):
print(car(courant)) # affiche 12, puis 5, puis 32
courant = cdr(courant)
Exemple : (12, (5, (32, None))). Parcourir cette chaîne signifie suivre successivement les seconds éléments des couples. Une autre implémentation, avec des classes, sera étudiée après la POO.
⚠️ Pièges classiques au bac
pop()sur une liste vide → IndexError ! Toujours testerif pile:avant.list.insert(0, x)est O(n), pas O(1). Pour une file, utiliserdeque.- Un dictionnaire avec doublon de clé :
{'a':1,'a':2}→{'a':2}. La deuxième valeur écrase. - Les listes sont mutables :
b = ane copie pas ! Utiliserb = a.copy()oub = a[:]. - L'accès dict par clé absente → KeyError. Toujours utiliser
d.get(cle)oucle in d.
📝 Activités
Neuf activités de prise en main, à faire dans l'ordre : elles s'appuient les unes sur les autres.
Soit la suite d'instructions suivantes (elles s'enchaînent) :
L = vide()
L = cons(12, cons(5, cons(32, L)))
a = car(L)
L1 = cdr(L)
L1 = cons(42, cons(23, L1))
Donnez le contenu des listes L et L1, et la valeur de a.
✅ Correction
L = vide() # L devient une liste vide
L = cons(12, cons(5, cons(32, L))) # L devient [12, 5, 32]
a = car(L) # a = 12, le premier élément de L
L1 = cdr(L) # L1 devient [5, 32]
L1 = cons(42, cons(23, L1)) # L1 devient [42, 23, 5, 32]
Contenu de L : [12, 5, 32] · Contenu de L1 : [42, 23, 5, 32] · Valeur de a : 12
Soit une pile P composée des éléments suivants : 15, 11, 32, 45 et 67 (le sommet de la pile est 67). Quel est l'effet de l'instruction pop(P) ?
✅ Correction
L'élément au sommet (67) est retiré. La pile devient : [15, 11, 32, 45].
Soit une pile P initialement vide. Soit les instructions suivantes :
push(P, 34)
push(P, 76)
push(P, 43)
a = pop(P)
push(P, 42)
b = taille(P)
Donnez le contenu de la pile P, la valeur de a et la valeur de b.
✅ Correction
push(P, 34) # P devient [34]
push(P, 76) # P devient [76, 34]
push(P, 43) # P devient [43, 76, 34]
a = pop(P) # a = 43, P devient [76, 34]
push(P, 42) # P devient [42, 76, 34]
b = taille(P) # b = 3
Contenu de P : [42, 76, 34] · a = 43 · b = 3
Soit une file F composée des éléments suivants : 1, 12, 24, 17, 21 et 72 (le premier élément entré est 72 ; le dernier entré est 1). Quel est l'effet de l'instruction enqueue(F, 25) ?
✅ Correction
File F, du plus ancien au plus récent : [72, 21, 17, 24, 12, 1]. L'élément 25 est ajouté à la fin. La file devient : [72, 21, 17, 24, 12, 1, 25].
Soit une file F initialement vide. Soit les instructions suivantes :
enqueue(F, 67)
enqueue(F, 34)
enqueue(F, 78)
a = dequeue(F)
enqueue(F, 23)
b = taille(F)
Donnez le contenu de la file F, la valeur de a et la valeur de b.
✅ Correction
enqueue(F, 67) # F devient [67]
enqueue(F, 34) # F devient [67, 34]
enqueue(F, 78) # F devient [67, 34, 78]
a = dequeue(F) # a = 67, F devient [34, 78]
enqueue(F, 23) # F devient [34, 78, 23]
b = taille(F) # b = 3
Contenu de F : [34, 78, 23] · a = 67 · b = 3
Soit le programme Python suivant :
def vide(): return None
def estVide(L): return L is None
def cons(x, L): return (x, L)
def car(L): return L[0]
def cdr(L): return L[1]
Quel est le but de ce programme ? Vérifiez à l'aide de ce programme que vos réponses à l'activité 1 étaient correctes.
✅ Correction
Le programme implémente le type abstrait liste vu en cours, avec des tuples Python : vide() renvoie une liste vide (None), estVide(L) teste si L est vide, cons(x, L) ajoute x en tête, car(L) renvoie la tête, cdr(L) renvoie le reste. En exécutant ce programme avec les instructions de l'activité 1, on retrouve bien L = [12, 5, 32], a = 12 et L1 = [42, 23, 5, 32].
Python propose une implémentation des piles. Après avoir étudié la documentation officielle (partie 5.1.1), écrivez un programme vérifiant vos réponses à l'activité 3.
✅ Correction
P = []
P.append(34)
P.append(76)
P.append(43)
a = P.pop() # a = 43
P.append(42)
b = len(P) # b = 3
Une pile Python, c'est simplement une list : push = append, pop = pop, taille = len.
Python propose une implémentation des files. Après avoir étudié la documentation officielle (partie 5.1.2), écrivez un programme vérifiant vos réponses à l'activité 5.
✅ Correction
from collections import deque
F = deque()
F.append(67)
F.append(34)
F.append(78)
a = F.popleft() # a = 67
F.append(23)
b = len(F) # b = 3
Écrivez une fonction Python permettant de déterminer le nombre d'éléments d'une liste. Puis, sans utiliser len(), écrivez-la pour le type abstrait liste, pour une pile et pour une file — sans les modifier.
✅ Correction
# Avec len() — le plus simple
def taille(L): return len(L)
# Sans len(), sur le type abstrait liste (cons/cdr)
def taille(L):
compteur = 0
while L is not None:
compteur += 1
L = cdr(L) # passe à l'élément suivant
return compteur
Pour une pile ou une file, il n'existe pas d'équivalent de cdr qui laisse la structure intacte : il faut la vider dans une structure temporaire pour compter, puis la restaurer.
def taille_pile(P):
compteur = 0
temporaire = []
while P:
compteur += 1
element = P.pop()
temporaire.append(element)
while temporaire:
P.append(temporaire.pop()) # restaure l'ordre initial
return compteur
def taille_file(F): # F est une deque
compteur = 0
temporaire = deque()
while F:
compteur += 1
element = F.pop()
temporaire.append(element)
while temporaire:
F.append(temporaire.pop()) # restaure l'ordre initial
return compteur
Dans les deux cas : on vide entièrement la structure dans une structure temporaire en comptant au passage, puis on la restaure élément par élément — ce qui remet tout dans l'ordre d'origine, sans jamais utiliser len().
🏋️ Exercices
Six exercices, du plus simple au sujet 0 du bac NSI (tri d'une pile de crêpes).
L = vide()
L = cons(2, cons(15, cons(23, L)))
L1 = cdr(L)
a = car(L1)
L1 = cons(4, cons(3, L1))
Donnez le contenu des listes L et L1, et la valeur de a.
✅ Correction
L est initialisé avec [2, 15, 23]. L1 = cdr(L) = [15, 23]. a = car(L1) = 15. Enfin, deux éléments sont ajoutés à L1, qui devient [4, 3, 15, 23].
L : [2, 15, 23] · L1 : [4, 3, 15, 23] · a : 15
push(P, 4)
push(P, 7)
a = pop(P)
b = taille(P)
c = pop(P)
push(P, 3)
push(P, 2)
d = taille(P)
P est initialement vide. Donnez son contenu final et les valeurs de a, b, c, d.
✅ Correction
On empile 4 puis 7. a = pop(P) = 7, il reste [4]. b = taille(P) = 1. c = pop(P) = 4, la pile est vide. On empile 3 puis 2 : pile finale [3, 2], d = taille(P) = 2.
P : [3, 2] · a : 7 · b : 1 · c : 4 · d : 2
enqueue(F, 6)
enqueue(F, 3)
a = dequeue(F)
enqueue(F, 9)
b = taille(F)
enqueue(F, 17)
c = dequeue(F)
enqueue(F, 2)
d = taille(F)
F est initialement vide. Donnez son contenu final et les valeurs de a, b, c, d.
✅ Correction
On enfile 6 puis 3. a = dequeue(F) = 6, F devient [3]. On enfile 9 : F = [3, 9], b = taille(F) = 2. On enfile 17 : F = [3, 9, 17]. c = dequeue(F) = 3. On enfile 2 : F = [9, 17, 2], d = taille(F) = 3.
F : [9, 17, 2] · a : 6 · b : 2 · c : 3 · d : 3
pile = []
tab = [5, 8, 6, 1, 3, 7]
pile.append(5)
pile.append(10)
pile.append(8)
pile.append(15)
for i in tab:
if i > 5:
pile.pop()
Donnez l'état de pile après l'exécution.
✅ Correction
Après les append : pile = [5, 10, 8, 15]. On parcourt tab : 5 → rien ; 8 > 5 → on retire 15 (pile = [5, 10, 8]) ; 6 > 5 → on retire 8 (pile = [5, 10]) ; 1 → rien ; 3 → rien ; 7 > 5 → on retire 10 (pile = [5]).
État final : [5]
from collections import deque
file = deque([])
tab = [2, 78, 6, 89, 3, 17]
file.append(5)
file.append(10)
file.append(8)
file.append(15)
for i in tab:
if i > 50:
file.popleft()
Donnez l'état de file après l'exécution.
✅ Correction
Après les append : file = [5, 10, 8, 15]. On parcourt tab : 2 → rien ; 78 > 50 → on retire 5 (file = [10, 8, 15]) ; 6 → rien ; 89 > 50 → on retire 10 (file = [8, 15]) ; 3, 17 → rien.
État final : [8, 15]
On dispose de 4 fonctions : pileVide(), estVide(P), empiler(P, val), depiler(P) (renvoie et retire le sommet).
1) P contient 8, 5, 2, 4 (empilés par le haut, sommet = 4). Quel est le contenu de Q après :
Q = pileVide()
while not estVide(P):
empiler(Q, depiler(P))
2a) Complétez hauteur_pile(P), qui renvoie le nombre d'éléments de P sans la modifier durablement :
def hauteur_pile(P):
Q = pileVide()
n = 0
while not(estVide(P)):
???
x = depiler(P)
empiler(Q, x)
while not(estVide(Q)):
???
empiler(P, x)
return ???
2b) Écrivez max_pile(P, i) : position (1 = sommet) de l'élément maximum parmi les i derniers empilés ; P retrouve son état d'origine.
3) Écrivez retourner(P, j) : inverse l'ordre des j derniers éléments empilés (utiliser deux piles auxiliaires), ne renvoie rien.
4) « Tri des crêpes » : on modélise une pile de crêpes par une pile d'entiers (le diamètre). On veut réordonner la pile de la plus grande (en bas) à la plus petite (en haut), en ne disposant que d'une « spatule » qui retourne toutes les crêpes au-dessus d'une position donnée. Écrivez tri_crepes(P) à l'aide des fonctions précédentes.
✅ Correction
1) Q contient les mêmes éléments que P, dans l'ordre inverse : si P = [8, 5, 2, 4], alors Q = [4, 2, 5, 8].
2a)
def hauteur_pile(P):
Q = pileVide()
n = 0
while not(estVide(P)):
n += 1 # incrémente le compteur
x = depiler(P)
empiler(Q, x)
while not(estVide(Q)):
x = depiler(Q) # restaure P
empiler(P, x)
return n
On dépile P en comptant, en empilant chaque élément dans Q, puis on restaure P depuis Q.
2b)
def max_pile(P, i):
Q = pileVide()
max_value = None
position = 1
max_position = 1
for j in range(i):
x = depiler(P)
empiler(Q, x)
if max_value is None or x > max_value:
max_value = x
max_position = position
position += 1
while not estVide(Q):
empiler(P, depiler(Q))
return max_position
On parcourt les i derniers éléments de P, on repère le maximum et sa position, puis on restaure P.
3)
def retourner(P, j):
Q = pileVide()
R = pileVide()
for _ in range(j):
empiler(Q, depiler(P))
while not estVide(Q):
empiler(R, depiler(Q))
while not estVide(R):
empiler(P, depiler(R))
Deux piles auxiliaires suffisent : dépiler deux fois de suite (P→Q puis Q→R) inverse deux fois l'ordre, donc P→Q→R→P... en s'arrêtant à R on obtient l'ordre inversé une seule fois pour les j derniers éléments.
4)
def tri_crepes(P):
n = hauteur_pile(P)
for i in range(n, 1, -1):
max_pos = max_pile(P, i)
retourner(P, max_pos) # amène la plus grande crêpe restante au sommet
retourner(P, i) # puis tout en bas des i premières
À chaque tour, on cherche la plus grande crêpe parmi les i non encore triées, on la ramène au sommet (retourner(P, max_pos)), puis on la fait descendre tout en bas du paquet en cours de tri (retourner(P, i)) — exactement le principe de la « spatule ».
📌 Fiche synthèse
LISTE (tableau)
- Accès[i] → O(1)
- append → O(1) amorti
- insert(0,x) → O(n)
- Recherche → O(n)
PILE (LIFO)
- push = append() → O(1)
- pop() → O(1)
- peek = tab[-1] → O(1)
- Usage : récursion, Ctrl+Z
FILE (FIFO)
- enqueue = append() → O(1)
- dequeue = popleft() → O(1)
- Utiliser deque !
- Usage : BFS, impression
DICTIONNAIRE
- Accès/Insert → O(1) moy.
- Clé in dict → O(1)
- Clés hashables
- Ordre depuis Python 3.7
🧠 QCM
Choisissez le nombre de questions et le niveau, puis lancez.
📦 Partie 2 — À lire après avoir étudié la POO
Cette partie n’est pas à travailler au début de l’année. Termine d’abord la partie sans classes. Reviens ici après le chapitre sur la programmation orientée objet, lorsque tu connais classe, instance, attribut, méthode, self et __init__.
J’ai étudié la POO : ouvrir les implémentations avec classes
Le contrat LIFO ou FIFO ne change pas. Une classe regroupe désormais la structure stockée dans un attribut et les opérations dans des méthodes.
| Partie 1 : fonctions | Après la POO : méthodes |
|---|---|
P = pileVide() | P = Pile() |
empiler(P, 34) | P.empiler(34) |
depiler(P) | P.depiler() |
💡 Que fait raise ? Et NotImplementedError ?
raise déclenche une exception : Python interrompt l’exécution normale et cherche un bloc except capable de la traiter. Sans traitement, le programme s’arrête avec un message d’erreur. Ce n’est ni un affichage (print), ni une valeur renvoyée (return).
def retirer(file):
if not file:
raise ValueError("La file est vide")
return file.pop(0)
try:
retirer([])
except ValueError as erreur:
print(erreur) # affiche La file est videDans les squelettes, raise NotImplementedError("Mission à compléter") est un repère volontaire. Remplace cette ligne par ton algorithme lorsque tu programmes la fonction. Le message ne signifie pas que Python est mal installé.
ValueError signale ici une opération refusée ; IndexError apparaît notamment avec un retrait invalide dans une liste. Respecte le contrat de chaque activité : dans PyLand, retirer d’une file vide renvoie None, tandis que la Bataille demande une exception.
La classe Pile
class Pile:
def __init__(self): self._data = []
def est_vide(self): return len(self._data) == 0
def empiler(self, x): self._data.append(x)
def depiler(self):
if self.est_vide(): raise IndexError("pile vide")
return self._data.pop()
def sommet(self): return self._data[-1]La classe File
from collections import deque
class File:
def __init__(self): self._data = deque()
def est_vide(self): return len(self._data) == 0
def enfiler(self, x): self._data.append(x)
def defiler(self):
if self.est_vide(): raise IndexError("file vide")
return self._data.popleft()Des nœuds et une liste chaînée
Les attributs valeur et suivant jouent ici les rôles des deux éléments du tuple de la partie 1.
class Noeud:
def __init__(self, valeur):
self.valeur = valeur
self.suivant = None
class ListeChainee:
def __init__(self):
self.tete = None
def inserer_tete(self, val): # O(1)
n = Noeud(val)
n.suivant = self.tete
self.tete = n
def parcourir(self):
courant = self.tete
while courant is not None:
print(courant.valeur)
courant = courant.suivantÀ vérifier : deux instances doivent garder des contenus indépendants ; empiler puis dépiler restitue la dernière valeur ; enfiler puis défiler respecte l’ordre d’arrivée. Les retraits sur une structure vide lèvent une erreur.