Tout ce qui ressemble à un réseau — pages web, comptes amis, villes reliées par des routes, paquets dépendant d’autres paquets — se modélise comme un graphe. Le parcours en largeur, ou BFS pour breadth-first search, est l’algorithme fondamental qui visite tous les sommets d’un graphe couche par couche, en partant d’un sommet source. Il sert à compter les degrés de séparation, à trouver le plus court chemin en nombre d’arêtes, à explorer un labyrinthe, ou à détecter une composante connexe. Ce tutoriel construit une représentation simple d’un graphe en Python, implémente BFS de zéro avec une deque, et bâtit la version qui reconstitue le chemin entre deux sommets.
Pour la vue d’ensemble des structures de données et des algorithmes, lire d’abord le guide principal Algorithmes et structures de données : repères 2026 pour devs.
Prérequis
- Python 3.10 ou plus récent
- Connaître les dictionnaires Python, les ensembles (
set) et les listes - Avoir lu, idéalement, le tutoriel Coder une table de hachage en Python de cette série pour comprendre pourquoi le dictionnaire est la bonne structure pour la liste d’adjacence
- Niveau attendu : intermédiaire
- Temps estimé : 50 minutes pour suivre, 90 minutes avec les variantes
Étape 1 — Modéliser un graphe en Python
Un graphe est un ensemble de sommets reliés par des arêtes. Pour le représenter en mémoire, on a deux options principales : la matrice d’adjacence, un tableau V × V de booléens où la case (i, j) vaut True si i et j sont reliés ; et la liste d’adjacence, un dictionnaire qui à chaque sommet associe la liste de ses voisins. La matrice est simple mais dévore O(V²) de mémoire, ce qui devient prohibitif au-delà de quelques milliers de sommets. La liste d’adjacence ne stocke que ce qui existe vraiment — O(V + E) — et reste compacte tant que le graphe n’est pas dense.
En pratique, on choisit presque toujours la liste d’adjacence pour les graphes du monde réel. Un réseau social a quelques milliards d’utilisateurs mais chaque utilisateur a au mieux quelques milliers de connexions — le graphe est creux. Une matrice serait monstrueuse, une liste d’adjacence tient sans peine. La classe Python ci-dessous utilise un dictionnaire qui, à chaque sommet, mappe une liste de voisins. Les sommets peuvent être n’importe quel type hachable : chaînes, entiers, tuples.
class Graph:
def __init__(self):
self._adj = {}
def add_vertex(self, v):
if v not in self._adj:
self._adj[v] = []
def add_edge(self, u, v):
"""Ajoute une arête non orientée entre u et v."""
self.add_vertex(u)
self.add_vertex(v)
self._adj[u].append(v)
self._adj[v].append(u)
def neighbors(self, v):
return self._adj.get(v, [])
def __contains__(self, v):
return v in self._adj
def __iter__(self):
return iter(self._adj)
def __len__(self):
return len(self._adj)
L’opération add_edge ajoute la relation dans les deux sens parce qu’on modélise un graphe non orienté. Pour un graphe orienté (relations à sens unique : suivi sur Twitter, dépendance de paquet, lien hypertexte), il suffirait de retirer le second append. Tester l’API : g = Graph(); g.add_edge("A", "B"); g.add_edge("B", "C"); print(g.neighbors("B")) doit afficher ['A', 'C']. La complexité de l’ajout d’une arête est O(1) en moyenne, parce que dict et list.append sont en O(1) amorti.
Étape 2 — Construire un graphe de test
Avant d’écrire BFS, on prépare un petit graphe sur lequel on pourra valider visuellement le résultat. On choisit une topologie qui force l’algorithme à traiter plusieurs branches : six sommets nommés A à F, avec quelques arêtes qui créent des chemins multiples. C’est le genre de cas test qui permet de vérifier non seulement que l’algorithme finit, mais qu’il visite les sommets dans le bon ordre et qu’il n’oublie personne.
g = Graph()
edges = [
("A", "B"), ("A", "C"),
("B", "D"), ("B", "E"),
("C", "E"),
("E", "F"),
]
for u, v in edges:
g.add_edge(u, v)
Représenté à plat, le graphe ressemble à : A est relié à B et C ; B est relié à A, D, E ; C est relié à A et E ; D à B uniquement ; E à B, C, F ; F à E uniquement. Il y a deux chemins distincts entre A et E (par B ou par C), ce qui sert à vérifier que BFS trouve bien le plus court — deux arêtes — sans se laisser piéger par celui qu’il rencontre en premier. Le sommet F est terminal et ne se visite qu’à la dernière vague.
Étape 3 — Implémenter BFS
BFS exploite trois structures auxiliaires : un ensemble visited qui mémorise les sommets déjà rencontrés (pour éviter les boucles infinies sur les graphes avec cycles), une file queue qui stocke les sommets à explorer ensuite (FIFO — premier entré, premier sorti, pour préserver l’ordre par couches), et une liste order qui retient l’ordre de visite. La file s’implémente en Python avec collections.deque, qui offre popleft() en O(1) là où list.pop(0) serait en O(n).
from collections import deque
def bfs(graph, start):
"""Parcours en largeur depuis start. Renvoie l'ordre de visite."""
if start not in graph:
return []
visited = {start}
queue = deque([start])
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph.neighbors(node):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return order
Le déroulement sur le graphe de l’étape 2 avec start=A donne : on part avec visited={A}, queue=[A]. On retire A, on l’ajoute à order, ses voisins B et C n’ont pas été visités donc on les marque et les empile : visited={A,B,C}, queue=[B,C]. Vague suivante : on retire B (premier entré), on découvre D et E, on les empile : queue=[C,D,E]. Puis C, qui ne révèle rien de nouveau (E déjà visité). Puis D (rien). Puis E, qui découvre F. Puis F. Résultat final : ['A', 'B', 'C', 'D', 'E', 'F'] — les sommets sont visités exactement par couches de distance croissante depuis A.
Étape 4 — Calculer le plus court chemin
BFS ne trouve pas seulement les sommets accessibles : il garantit que la distance, en nombre d’arêtes, est minimale. Cette propriété tient parce que l’algorithme visite tous les sommets à distance 1 avant de toucher ceux à distance 2, etc. Pour reconstituer le chemin, on enregistre pour chaque sommet le sommet par lequel on l’a découvert — c’est le tableau parent. Une fois la cible atteinte, on remonte la chaîne des parents jusqu’à la source.
def shortest_path_bfs(graph, start, target):
"""Plus court chemin en nombre d'arêtes. None si non joignable."""
if start not in graph or target not in graph:
return None
if start == target:
return [start]
visited = {start}
queue = deque([start])
parent = {start: None}
while queue:
node = queue.popleft()
for neighbor in graph.neighbors(node):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = node
if neighbor == target:
return _reconstruct_path(parent, target)
queue.append(neighbor)
return None
def _reconstruct_path(parent, target):
path = []
cur = target
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path
Sur le graphe de l’étape 2, shortest_path_bfs(g, "A", "F") renvoie ['A', 'B', 'E', 'F'] — trois arêtes, ce qui est bien le plus court. La reconstruction part de F, lit parent[F]=E, parent[E]=B, parent[B]=A, parent[A]=None, ce qui donne F→E→B→A, qu’on inverse pour obtenir le bon ordre. Notez qu’on aurait aussi pu passer par C : A→C→E→F est de longueur 3 également. BFS renvoie celui qu’il a découvert en premier, ce qui dépend de l’ordre des voisins dans la liste d’adjacence. Pour le plus court chemin pondéré (avec des coûts différents par arête), il faut Dijkstra, pas BFS.
Étape 5 — Calculer toutes les distances depuis une source
Une variante utile renvoie la distance de chaque sommet à la source, pas seulement vers une cible donnée. Le code est presque identique au BFS de base, sauf qu’on remplace l’ordre de visite par un dictionnaire de distances. C’est l’algorithme qu’on utilise pour les degrés de séparation sur un réseau social, ou pour calculer les distances dans un labyrinthe à grille (où les sommets sont les cases libres et les arêtes relient les cases adjacentes).
def distances_from(graph, start):
"""Distance (en arêtes) de chaque sommet accessible à start."""
if start not in graph:
return {}
dist = {start: 0}
queue = deque([start])
while queue:
node = queue.popleft()
for neighbor in graph.neighbors(node):
if neighbor not in dist:
dist[neighbor] = dist[node] + 1
queue.append(neighbor)
return dist
Sur le graphe de l’étape 2 avec start=A, on obtient {'A': 0, 'B': 1, 'C': 1, 'D': 2, 'E': 2, 'F': 3}. Les distances correspondent exactement à ce qu’on lit visuellement sur le diagramme. Si un sommet n’apparaît pas dans le dictionnaire renvoyé, c’est qu’il n’est pas joignable depuis la source — il appartient à une autre composante connexe du graphe. Cette propriété fait de BFS l’outil naturel pour énumérer les composantes connexes : on lance BFS depuis chaque sommet non encore visité, et chaque exécution révèle une composante.
Étape 6 — Vérification
Un test de bout en bout vérifie le parcours, le plus court chemin, et les distances sur le graphe préparé. On ajoute un cas isolé (un sommet sans arête) pour valider la gestion des sommets inaccessibles, et un cas où start n’existe pas dans le graphe.
if __name__ == "__main__":
g = Graph()
edges = [("A","B"),("A","C"),("B","D"),("B","E"),("C","E"),("E","F")]
for u, v in edges:
g.add_edge(u, v)
g.add_vertex("Z") # sommet isolé
# Parcours
visited_order = bfs(g, "A")
assert set(visited_order) == {"A", "B", "C", "D", "E", "F"}
assert visited_order[0] == "A"
# Plus court chemin
path = shortest_path_bfs(g, "A", "F")
assert path == ["A", "B", "E", "F"] or path == ["A", "C", "E", "F"]
assert shortest_path_bfs(g, "A", "Z") is None
assert shortest_path_bfs(g, "A", "A") == ["A"]
# Distances
dist = distances_from(g, "A")
assert dist == {"A": 0, "B": 1, "C": 1, "D": 2, "E": 2, "F": 3}
# Sommet inexistant
assert bfs(g, "INCONNU") == []
print("Tous les tests passent.")
Lancez python bfs.py et vous devez voir Tous les tests passent.. Si une assertion échoue, le test indique précisément ce qui est faux. Le cas shortest_path_bfs(g, "A", "Z") renvoie None parce que Z est isolé : on l’a ajouté comme sommet mais sans aucune arête, donc aucun parcours depuis A ne peut l’atteindre. La gestion correcte de ce cas est ce qui distingue une implémentation propre d’une implémentation qui plante à la première composante déconnectée.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Boucle infinie sur un graphe avec cycles | Pas de jeu visited, ou marquage après dépilement au lieu de marquage à l’enfilement | Marquer visited.add(neighbor) au moment où on l’empile, pas quand on le retire |
BFS très lent avec list.pop(0) | list.pop(0) est en O(n), ce qui rend le parcours global O(V × n) | Utiliser collections.deque et popleft() qui est en O(1) |
| Plus court chemin incorrect avec poids | BFS suppose toutes les arêtes de coût 1 | Pour des arêtes pondérées, utiliser Dijkstra (avec heapq) |
KeyError en parcours | Voisin référencé qu’on n’a pas ajouté comme sommet | Soit utiliser graph.neighbors(node) qui renvoie [] par défaut, soit ajouter explicitement les sommets |
| Distances erronées sur graphe orienté | add_edge qui ajoute dans les deux sens alors que la relation est unidirectionnelle | Distinguer la classe pour graphes orientés et celle pour graphes non orientés |
BFS contre DFS : quand utiliser lequel
Le parcours en profondeur (DFS) explore aussi systématiquement, mais en suivant chaque branche jusqu’au bout avant de remonter. La différence d’implémentation tient à une seule structure : DFS utilise une pile (LIFO) à la place de la file, ou tout simplement la récursion. Le résultat fonctionnel est différent : DFS plonge en profondeur, BFS ratisse en largeur. Les deux ont la même complexité O(V + E) sur une liste d’adjacence, mais ils ne servent pas aux mêmes problèmes.
BFS est l’outil pour le plus court chemin non pondéré, les distances en arêtes, le calcul de niveaux dans une hiérarchie. DFS est l’outil pour la détection de cycles, le tri topologique d’un DAG, la recherche de composantes fortement connexes (algorithmes de Tarjan ou Kosaraju). Si vous écrivez un crawler web qui doit prioriser les pages les plus proches du point de départ, c’est BFS ; si vous résolvez un Sudoku ou explorez un arbre de jeux, c’est DFS avec backtracking. Avoir les deux en tête, et savoir quand basculer, fait partie de l’outillage de base.
Au-delà : Dijkstra et A*
Quand les arêtes ont des poids différents (distance kilométrique, latence réseau, coût financier), BFS ne donne plus le plus court chemin. L’algorithme de Dijkstra étend l’idée en remplaçant la file FIFO par une file de priorité (min-heap), ce qui permet d’explorer toujours le sommet de coût total le plus faible. Sa complexité avec un tas binaire (le module heapq de Python) est en O((V + E) log V). C’est l’algorithme derrière Google Maps, dans une version très optimisée avec préférences hiérarchiques.
L’algorithme A* va plus loin en ajoutant une heuristique qui estime la distance restante jusqu’à la cible, ce qui guide la recherche vers le but plutôt que d’explorer dans toutes les directions. Sur des grilles 2D (jeux vidéo, robotique), A* avec heuristique de Manhattan ou euclidienne explore des dizaines de fois moins de sommets que Dijkstra pur. Mais A* nécessite une heuristique admissible (ne surestime jamais), ce qui restreint son usage à des domaines où la géométrie aide.
Cas d’usage concrets dans le métier
Les développeurs sous-estiment la fréquence à laquelle ils manipulent des graphes sans le nommer. Le DOM d’une page HTML est un arbre, donc un graphe particulier — tout outil qui le parcourt (jQuery, querySelectorAll, scrapers) fait du BFS ou du DFS sans le dire. Le système de fichiers est un arbre, et la commande find de Unix exécute un DFS. Les dépendances de paquets npm, pip ou cargo forment un graphe orienté acyclique, et l’algorithme qui détermine l’ordre d’installation est un tri topologique — lui-même bâti sur DFS. Comprendre BFS et DFS, c’est lire ces outils et leurs comportements avec les bons mots.
Côté applicatif, BFS résout des problèmes qu’on rencontre en pratique : un labyrinthe à grille modélisable comme un graphe où chaque case libre est un sommet et chaque déplacement vers une case voisine une arête, le calcul du nombre de degrés de séparation entre deux profils LinkedIn ou Facebook, le crawling d’un site web où l’on veut explorer en priorité les pages les plus proches de la racine, ou encore la propagation d’une infection ou d’une rumeur dans un réseau social. À chaque fois, l’invariant est le même : on veut visiter par couches en partant d’une source, et on veut s’arrêter à une distance maximale ou à une cible donnée.
Performance et limites pratiques
BFS est en O(V + E) en temps et O(V) en mémoire pour la file et l’ensemble visited. Sur un graphe de 10 millions de sommets et 100 millions d’arêtes (taille typique d’un réseau social moyen), une exécution Python pure prend de l’ordre de 30 à 60 secondes — ce qui est gérable pour des analyses ponctuelles, mais inacceptable en latence d’API. Pour les vrais réseaux, on passe en C++ (graphes BGL), en Rust (petgraph) ou on délègue à une base de données graphe (Neo4j, ArangoDB) qui implémente BFS en interne avec des indices optimisés.
Une autre limite pratique concerne la mémoire. Sur un graphe énorme dont la liste d’adjacence ne tient pas en RAM, BFS classique échoue. Les variantes « external memory BFS » lisent et écrivent par blocs sur disque, en regroupant les sommets par couche pour limiter les accès aléatoires. Les frameworks comme Apache Giraph ou Pregel de Google distribuent BFS sur plusieurs machines en se basant sur le modèle « think like a vertex » : chaque sommet calcule localement son état et envoie des messages à ses voisins à chaque super-étape. Ces architectures dépassent largement le cadre de ce tutoriel mais valent d’être connues comme horizon.
Tutoriels suivants
- Implémenter une liste chaînée en Python étape par étape — les bases de la manipulation de pointeurs.
- Coder une table de hachage en Python étape par étape — structure utilisée pour la liste d’adjacence et l’ensemble visited.
- Programmer le tri rapide (quicksort) en Python — algorithme classique par diviser-pour-régner.
Pour aller plus loin
- Retour au guide principal : Algorithmes et structures de données : repères 2026 pour devs
- Documentation officielle Python —
collections.deque - Documentation officielle Python — module
heapq(pour Dijkstra) - Wikipedia — Breadth-first search
- Wikipedia — Dijkstra’s algorithm
- MIT OpenCourseWare 6.006 — cours BFS (slides + vidéo)
- Cormen, Leiserson, Rivest, Stein — Introduction to Algorithms (chapitre 22)