Structures de données
Listes, piles, files, dictionnaires et listes chaînées
🎯 Introduction
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
- Distinguer les structures et leurs propriétés (LIFO, FIFO, accès par clé)
- Connaître les complexités O(1) / O(n) de chaque opération
- Implémenter une pile et une file avec une liste Python
- Utiliser les dictionnaires Python (création, accès, test d'appartenance)
- Comprendre les listes chaînées (nœuds, pointeurs)
- Savoir choisir la bonne structure selon le problème
🖨️ 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 →📋 Listes et tableaux
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 (stack) suit le principe LIFO : Last In, First Out (dernier entré, premier sorti). Comme une pile d'assiettes.
Cas d'usage réels
Pile d'appels de fonctions (récursion), 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
# Classe Pile propre
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]
🚌 Files — FIFO
Une file (queue) suit le principe FIFO : First In, First Out (premier entré, premier sorti). Comme une file d'attente.
N'utilise pas une simple liste Python pour une file !
list.insert(0, x) est O(n) car il décale tous les éléments. Utilise collections.deque pour des opérations O(1) aux deux extrémités.
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'
# Classe File propre
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()
🗝️ 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.
🔗 Listes chaînées
Dans une liste chaînée, chaque élément (nœud) contient une valeur et un pointeur vers le nœud suivant. Contrairement aux tableaux, les éléments ne sont pas contigus en mémoire.
| 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) |
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
⚠️ 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.
🏋️ Exercices
Écrire une fonction parentheses_ok(s) qui renvoie True si les parenthèses de la chaîne s sont bien équilibrées, False sinon. Exemple : "(a+(b*c))" → True, "(()" → False.
✅ Correction
def parentheses_ok(s):
pile = []
for c in s:
if c == '(':
pile.append(c)
elif c == ')':
if not pile:
return False # fermante sans ouvrante
pile.pop()
return len(pile) == 0 # vrai si tout est fermé
On utilise une pile : on empile chaque ( et on dépile à chaque ). Si on dépile sur une pile vide → faux. À la fin, la pile doit être vide.
Implémenter une file (FIFO) en utilisant uniquement deux piles. Les opérations enfiler(x) et defiler() doivent être correctes.
✅ Correction
class FileAvecDeuxPiles:
def __init__(self):
self.entree = [] # pile pour enfiler
self.sortie = [] # pile pour défiler
def enfiler(self, x):
self.entree.append(x)
def defiler(self):
if not self.sortie: # si sortie vide, transvaser
while self.entree:
self.sortie.append(self.entree.pop())
if not self.sortie:
raise IndexError("file vide")
return self.sortie.pop()
Astuce : on transvase la pile d'entrée dans la pile de sortie quand elle est vide. Cela inverse l'ordre, donnant l'effet FIFO. Complexité amortie O(1).
Écrire une fonction frequences(texte) qui prend une chaîne de caractères et renvoie un dictionnaire associant chaque mot à son nombre d'occurrences. Les mots sont séparés par des espaces, en ignorant la casse.
✅ Correction
def frequences(texte):
freq = {}
for mot in texte.lower().split():
freq[mot] = freq.get(mot, 0) + 1
return freq
# Variante avec defaultdict
from collections import defaultdict
def frequences2(texte):
freq = defaultdict(int)
for mot in texte.lower().split():
freq[mot] += 1
return dict(freq)
📌 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.