Cours Structures de Données Complet 📦

Les 12 chapitres essentiels — arrays, listes chaînées, arbres, graphes, hash tables et files de priorité

12
Chapitres
15+
Structures
Python
Langage
A1→C2
Niveaux

CHAPITRE 01

Introduction

📦 Pourquoi les structures de données ?

Une structure de données est une façon d'organiser l'information en mémoire pour pouvoir y accéder et la modifier efficacement. Le choix de la structure impacte directement la performance. Chercher un nom dans une liste non triée de 1 million d'éléments prend O(n) = 1 million d'opérations. Dans une hash table, ça prend O(1) = 1 seule opération. Même algorithme, structure différente, 1 million de fois plus rapide.

Structure Accès Recherche Insertion Suppression Usage principal
Array O(1) O(n) O(n) O(n) Données indexées
Linked List O(n) O(n) O(1) O(1) Insertions/suppressions fréquentes
Hash Table O(1) O(1) O(1) Lookup rapide (dict, set)
Stack O(n) O(n) O(1) O(1) LIFO (undo, parenthèses)
Queue O(n) O(n) O(1) O(1) FIFO (BFS, tâches)
BST O(log n) O(log n) O(log n) O(log n) Données triées dynamiques
Heap O(1) min O(n) O(log n) O(log n) Min/max rapide, priorités

CHAPITRE 02

Arrays et listes

📋 Array (list en Python)
# Array = bloc contigu en mémoire
# Accès par index en O(1), recherche en O(n)

arr = [10, 20, 30, 40, 50]

# Accès — O(1)
arr[0] # 10
arr[1] # 50 (dernier)

# Ajout — O(1) amorti en fin, O(n) au début/milieu
arr.append(60) # [10,20,30,40,50,60] → O(1)
arr.insert(0, 5) # [5,10,20,…] → O(n) ! décale tout

# Suppression
arr.pop() # Retire le dernier → O(1)
arr.pop(0) # Retire le premier → O(n) ! décale tout
arr.remove(30) # Retire la valeur 30 → O(n)

# Slicing — O(k) où k = taille du slice
arr[1:3] # [20, 30]
arr[::1] # Inverse la liste

# Techniques courantes
sorted(arr) # Copie triée → O(n log n)
arr.sort() # Tri in-place → O(n log n)
len(arr) # Taille → O(1)
sum(arr) # Somme → O(n)
min(arr), max(arr) # Min/Max → O(n)

Piège courant : arr.insert(0, x) et arr.pop(0) sont O(n) car ils décalent tous les éléments. Si vous faites ça souvent → utilisez un deque (chapitre 4) qui est O(1) aux deux extrémités.

CHAPITRE 03

Stacks (piles)

📚 LIFO — Last In, First Out
# Stack = pile d'assiettes : on ajoute et retire par le dessus
# Push (empiler) et Pop (dépiler) en O(1)

# En Python, une list suffit comme stack
stack = [] stack.append(1) # Push → [1]
stack.append(2) # Push → [1, 2]
stack.append(3) # Push → [1, 2, 3]
stack.pop() # Pop → 3, stack = [1, 2]
stack[1] # Peek (voir le sommet) → 2

# Cas d'usage classique : vérifier des parenthèses
def parentheses_valides(s):
stack = [] paires = ': '
for c in s:
if c in '([{':
stack.append(c)
elif c in paires:
if not stack or stack.pop() != paires[c]:
return False
return len(stack) == 0

print(parentheses_valides(« () »)) # True
print(parentheses_valides(« () »)) # False

Utilisations des stacks : vérifier les parenthèses, évaluer des expressions mathématiques (notation polonaise), Ctrl+Z (undo/redo), call stack des langages de programmation, DFS (parcours en profondeur), conversion décimal→binaire.

CHAPITRE 04

Queues (files)

🚶 FIFO — First In, First Out
# Queue = file d'attente : premier arrivé, premier servi
# Enqueue (enfiler) et Dequeue (défiler) en O(1)

from collections import deque

queue = deque()
queue.append(« Alice ») # Enqueue → deque(['Alice'])
queue.append(« Bob ») # Enqueue → deque(['Alice', 'Bob'])
queue.append(« Claire ») # Enqueue → deque(['Alice', 'Bob', 'Claire'])
queue.popleft() # Dequeue → 'Alice' (premier arrivé)
queue[0] # Peek → 'Bob' (prochain)

# ⚠️ Ne PAS utiliser list pour une queue
# list.pop(0) est O(n) — deque.popleft() est O(1)

# Cas d'usage : BFS (parcours en largeur)
def bfs(graphe, depart):
visite = set([depart])
queue = deque([depart])
while queue:
noeud = queue.popleft()
for voisin in graphe[noeud]:
if voisin not in visite:
visite.add(voisin)
queue.append(voisin)
return visite

Utilisations des queues : BFS (parcours en largeur), file d'impression, serveur web (requêtes en attente), task queues (Celery, RabbitMQ, SQS), buffer de streaming. deque est aussi une stack efficace et supporte O(1) aux deux extrémités (appendleft, popleft, append, pop).

CHAPITRE 05

Listes chaînées

🔗 Nœuds liés par des pointeurs
# Linked List = chaque nœud contient une valeur + un pointeur
# Insertion/suppression O(1) si on a le nœud, accès O(n)

class Node:
def __init__(self, val, next=None):
self.val = val
self.next = next

class LinkedList:
def __init__(self):
self.head = None

def prepend(self, val): # O(1) — ajout au début
self.head = Node(val, self.head)

def append(self, val): # O(n) — ajout à la fin
if not self.head:
self.head = Node(val)
return
curr = self.head
while curr.next:
curr = curr.next
curr.next = Node(val)

def delete(self, val): # O(n) — trouver + supprimer
if self.head and self.head.val == val:
self.head = self.head.next
return
curr = self.head
while curr.next:
if curr.next.val == val:
curr.next = curr.next.next
return
curr = curr.next

# Technique d'entretien : Inverser une liste chaînée — O(n)
def inverser(head):
prev = None
curr = head
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
return prev # Nouvelle tête

En pratique, les listes chaînées sont rarement utilisées directement en Python ou JavaScript — les arrays dynamiques (list/Array) sont plus performants grâce au cache CPU (données contiguës en mémoire). Mais elles sont essentielles en entretien technique et servent de base aux stacks, queues et tables de hachage (chaining). Doubly linked list (prev + next) permet la suppression en O(1) quand on a le nœud.

CHAPITRE 06

Hash maps et sets

#️⃣ Dictionnaires et ensembles
# Hash map (dict) = clé → valeur, O(1) en moyenne
# Fonctionnement : hash(clé) → index dans un tableau interne

# Dict Python — O(1) get/set/delete/in
ages =
ages[« Claire »] = 24 # Insert O(1)
ages[« Alice »] # Lookup O(1) → 28
« Bob » in ages # Recherche O(1) → True
del ages[« Bob »] # Suppression O(1)
ages.get(« David », 0) # Défaut si absent → 0

# Set Python — collection sans doublons, O(1) add/in/remove
s =
s.add(4) #
s.add(2) # (pas de doublon)
3 in s # O(1) → True

# Pattern : compter les fréquences
from collections import Counter
mots = [« chat », « chien », « chat », « oiseau », « chat »] freq = Counter(mots) # Counter()

# Pattern : Two Sum — trouver 2 nombres qui font target
def two_sum(nums, target):
seen = # valeur → index
for i, n in enumerate(nums):
complement = target n
if complement in seen:
return [seen[complement], i] seen[n] = i
return []

# defaultdict — dict avec valeur par défaut
from collections import defaultdict
graph = defaultdict(list)
graph[« A »].append(« B ») # Pas de KeyError si « A » n'existe pas

Le dictionnaire est la structure de données la plus importante en programmation quotidienne. Quand vous voyez « trouver si X existe » ou « compter les occurrences » ou « grouper par clé » → pensez dict ou set immédiatement. C'est la différence entre un algorithme O(n²) (2 boucles) et O(n) (1 boucle + hashmap).

CHAPITRE 07

Arbres binaires (BST)

🌳 Binary Search Tree
# BST : pour chaque nœud, gauche < nœud < droite
# Recherche, insertion, suppression en O(log n) moyen

class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right

# Recherche dans un BST — O(log n)
def search(root, val):
if not root or root.val == val:
return root
if val < root.val:
return search(root.left, val)
return search(root.right, val)

# Insertion dans un BST — O(log n)
def insert(root, val):
if not root:
return TreeNode(val)
if val < root.val:
root.left = insert(root.left, val)
else:
root.right = insert(root.right, val)
return root

# 3 traversées — toutes O(n)
def inorder(root): # Gauche → Racine → Droite (TRIÉ !)
if not root: return [] return inorder(root.left) + [root.val] + inorder(root.right)

def preorder(root): # Racine → Gauche → Droite (copier l'arbre)
if not root: return [] return [root.val] + preorder(root.left) + preorder(root.right)

def postorder(root): # Gauche → Droite → Racine (supprimer l'arbre)
if not root: return [] return postorder(root.left) + postorder(root.right) + [root.val]

Inorder sur un BST donne les éléments triés. Un BST déséquilibré (type linked list) tombe à O(n). Les arbres auto-équilibrés (AVL, Red-Black) garantissent O(log n) — c'est ce qu'utilisent en interne TreeMap (Java), std::map (C++), et les index B-tree des bases de données.

CHAPITRE 08

Heaps et files de priorité

⬆️ Priority Queue
# Heap (tas) = arbre binaire où le parent est ≤ enfants (min-heap)
# Accès au minimum en O(1), insertion/suppression en O(log n)

import heapq

# Min-heap (Python heapq = toujours min-heap)
heap = [] heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 8)
heapq.heappush(heap, 1)

heap[0] # Peek min → 1, O(1)
heapq.heappop(heap) # Pop min → 1, O(log n)
heapq.heappop(heap) # Pop min → 2

# Max-heap : astuce — inverser le signe
max_heap = [] heapq.heappush(max_heap, 5)
heapq.heappush(max_heap, 2)
heapq.heappush(max_heap, 8)
heapq.heappop(max_heap) # → 8 (le plus grand)

# Top K éléments — O(n log k)
def top_k(arr, k):
return heapq.nlargest(k, arr)

# heapify — transformer une liste en heap O(n)
arr = [5, 3, 8, 1, 9] heapq.heapify(arr) # arr est maintenant un min-heap

Le heap est indispensable pour : trouver le min/max en O(1), Dijkstra (chemin le plus court), trouver les K plus grands/petits éléments, scheduler de tâches par priorité, merge de K listes triées. En entretien, dès que vous voyez « Kème plus grand » ou « top K » → pensez heap.

CHAPITRE 09

Hash tables (en profondeur)

🔧 Fonctionnement interne
# Hash table simplifiée

class HashTable:
def __init__(self, size=16):
self.size = size
self.buckets = [[] for _ in range(size)]

def _hash(self, key):
return hash(key) % self.size # Index dans le tableau

def set(self, key, value): # O(1) amorti
idx = self._hash(key)
for pair in self.buckets[idx]:
if pair[0] == key:
pair[1] = value
return
self.buckets[idx].append([key, value])

def get(self, key): # O(1) amorti
idx = self._hash(key)
for pair in self.buckets[idx]:
if pair[0] == key:
return pair[1] raise KeyError(key)

# Collision = 2 clés avec le même hash → même bucket
# Résolution : chaining (linked list dans le bucket)
# ou open addressing (chercher le prochain slot libre)

O(1) « amorti » signifie O(1) en moyenne. Pire cas O(n) si toutes les clés tombent dans le même bucket (collision extrême). En pratique, Python redimensionne automatiquement la table quand le load factor dépasse ~66%, ce qui maintient O(1). Seules les clés hashables (immuables) fonctionnent : int, str, tuple → oui. list, dict, set → non.

CHAPITRE 10

Graphes

🕸️ Représentations et types
# 1. Liste d'adjacence (dict) — la plus utilisée
graphe =

# 2. Matrice d'adjacence — pour graphes denses
# A B C D
# A [ 0, 1, 1, 0 ]
# B [ 1, 0, 0, 1 ]
# C [ 1, 0, 0, 0 ]
# D [ 0, 1, 0, 0 ]

# Graphe pondéré
graphe_p =

# Détecter un cycle (DFS + 3 couleurs)
def has_cycle(graphe):
WHITE, GRAY, BLACK = 0, 1, 2
color =
def dfs(node):
color[node] = GRAY
for voisin in graphe[node]:
if color[voisin] == GRAY: # Cycle !
return True
if color[voisin] == WHITE and dfs(voisin):
return True
color[node] = BLACK
return False
return any(dfs(n) for n in graphe if color[n] == WHITE)

Représentation Espace Vérifier arête Lister voisins Idéal pour
Liste d'adjacence O(V + E) O(degré) O(degré) Graphes épars (la plupart)
Matrice d'adjacence O(V²) O(1) O(V) Graphes denses, petits graphes

CHAPITRE 11

Tries (arbres de préfixes)

🔤 Recherche par préfixe en O(m)
# Trie = arbre où chaque nœud = une lettre
# Recherche/insertion en O(m) où m = longueur du mot
# Idéal pour : autocomplétion, correcteur orthographique, dictionnaire

class TrieNode:
def __init__(self):
self.children =
self.is_end = False

class Trie:
def __init__(self):
self.root = TrieNode()

def insert(self, word): # O(m)
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char] node.is_end = True

def search(self, word): # O(m) — mot exact
node = self._find(word)
return node is not None and node.is_end

def starts_with(self, prefix): # O(m) — préfixe
return self._find(prefix) is not None

def _find(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return None
node = node.children[char] return node

trie = Trie()
trie.insert(« python »)
trie.insert(« pytorch »)
trie.search(« python ») # True
trie.starts_with(« py ») # True (autocomplétion !)

CHAPITRE 12

Bonnes pratiques

🗺️ Choisir la bonne structure
Besoin Structure Python
Accès par index rapide Array list
Recherche rapide par clé Hash map dict
Vérifier l'existence Hash set set
LIFO (dernier entré, premier sorti) Stack list (append/pop)
FIFO (premier entré, premier sorti) Queue deque
Min/max rapide Heap heapq
Données triées dynamiques BST / Sorted list SortedList (sortedcontainers)
Autocomplétion, préfixes Trie Implémentation custom
Relations/connexions Graphe dict[list]
Comptage de fréquences Counter Counter
✅ Bonnes pratiques

✅ À FAIRE
dict/set pour lookup O(1) (pas de boucle !)
deque pour les queues (pas list.pop(0))
heapq pour min/max/top-K
Counter pour compter les fréquences
defaultdict pour éviter les KeyError
• Connaître la complexité de chaque opération
• Choisir la structure AVANT d'écrire l'algorithme
• Préférer les built-ins (optimisés en C)
• Profiler avant d'optimiser
• Pratiquer les patterns d'entretien

❌ À ÉVITER
if x in list dans une boucle → O(n²) (utiliser set)
list.pop(0) ou list.insert(0, x) → O(n)
• Créer sa propre hash table (utiliser dict)
• Linked list quand un array suffit
• Ignorer la complexité spatiale
• Trier pour chercher (hash map = O(n) vs sort O(n log n))
• Boucles imbriquées quand un dict suffit
• BST non équilibré (pire cas O(n))
• Confondre heap et sorted array
• Oublier que list.sort() est in-place

🧠 Quiz
list vs deque — quand utiliser quoi ?
list = accès par index O(1), append/pop en fin O(1). Mauvais pour pop(0) et insert(0) qui sont O(n). deque = O(1) aux deux extrémités (append, appendleft, pop, popleft). Mauvais pour accès par index O(n). Règle : si vous ajoutez/retirez au début → deque. Si vous accédez par index → list. Pour une queue (FIFO) → toujours deque.
Pourquoi « if x in set » est O(1) mais « if x in list » est O(n) ?
set utilise une hash table : hash(x) donne directement l'emplacement mémoire → O(1). list n'a pas de hash : Python doit parcourir chaque élément un par un pour comparer → O(n). C'est pour ça qu'une boucle avec if x in list donne O(n²) tandis que if x in set reste O(n).
Heap vs Sorted List — quelle différence ?
Heap : accès au min en O(1), insertion/suppression en O(log n), mais les autres éléments ne sont PAS triés. Sorted list : tous les éléments sont triés, mais insertion en O(n) (décalage). Utilisez un heap quand vous avez seulement besoin du min/max (Dijkstra, top-K). Utilisez une sorted list quand vous avez besoin de parcourir les données dans l'ordre (range queries).

Cours Structures de Données Complet — Arrays, hash tables, arbres, heaps, graphes et tries

Référence : LeetCode | NeetCode | VisuAlgo