🗂️

S01 — Structures de données

Listes, piles, files, dictionnaires et listes chaînées

Structures Python Complexité O(1) / O(n)

🎯 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 deque pour 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 »

🔁 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.

TypeExemples
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.

TypeExempleAccè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érationRô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érationComplexitéExemple
Accès par indiceO(1)tab[i]
Ajout en finO(1) amortitab.append(x)
Insertion en débutO(n)tab.insert(0, x)
Suppression en finO(1)tab.pop()
Recherche d'une valeurO(n)x in tab
LongueurO(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.

Mettre en pratique dans le TP2 : Hanoï →

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 :

InstructionEffet
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) × 6estVide(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.

Mettre en pratique dans le TP2 : PyLand →

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 :

InstructionEffet
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) × 6estVide(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érationComplexitéExemple
Accès/ModificationO(1) moy.d[cle]
Test d'appartenanceO(1) moy.cle in d
InsertionO(1) moy.d[cle] = val
SuppressionO(1) moy.del d[cle]
ParcoursO(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érationListe chaînéeTableau Python
Accès par indiceO(n)O(1)
Insertion en têteO(1)O(n)
Insertion en queueO(1) si pointeur finO(1) amorti
Suppression en têteO(1)O(n)
RechercheO(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 tester if pile: avant.
  • list.insert(0, x) est O(n), pas O(1). Pour une file, utiliser deque.
  • Un dictionnaire avec doublon de clé : {'a':1,'a':2} → {'a':2}. La deuxième valeur écrase.
  • Les listes sont mutables : b = a ne copie pas ! Utiliser b = a.copy() ou b = a[:].
  • L'accès dict par clé absente → KeyError. Toujours utiliser d.get(cle) ou cle in d.

📝 Activités

Neuf activités de prise en main, à faire dans l'ordre : elles s'appuient les unes sur les autres.

Facile Activité 1 — Construire et décomposer une liste

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

Facile Activité 2 — Dépiler

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].

Intermédiaire Activité 3 — Empiler, dépiler, taille

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

Facile Activité 4 — Enfiler

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].

Intermédiaire Activité 5 — Enfiler, défiler, taille

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

Intermédiaire Activité 6 — Implémenter le type abstrait liste

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].

Facile Activité 7 — Les piles en Python

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.

Facile Activité 8 — Les files en Python

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
Intermédiaire Activité 9 — Compter sans len()

É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).

Facile Exercice 1 — Construire et décomposer une liste
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

Facile Exercice 2 — Une pile, cinq questions
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

Intermédiaire Exercice 3 — Une file, cinq questions
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

Intermédiaire Exercice 4 — Lire un programme (pile)
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]

Intermédiaire Exercice 5 — Lire un programme (file)
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]

Niveau bac Exercice 6 — Tri d'une pile de crêpes (sujet 0 du bac NSI)

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 : fonctionsAprè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 vide

Dans 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.

← Tous les chapitres Chapitre suivant : S02 — Bases de données →