Comment utiliser cette page

Cette page suit toujours le même plan : le vocabulaire, la définition, quelques usages concrets, un exemple unique (repris dans toute la page), puis les deux façons courantes d'implémenter un graphe en Python — la liste d'adjacence et la matrice d'adjacence — avant la conversion entre les deux et la complexité.

Utilisation de la démonstration
La démonstration avance uniquement quand vous cliquez sur « Suivant » ou « Précédent » : rien ne bouge tout seul. Un bouton « Lecture automatique » est disponible si vous préférez laisser les étapes défiler seules, à une vitesse que vous choisissez.
📄 Le code de cette page, en fichier téléchargeable
Le fichier src/graphes.py, dans le sous-dossier src à côté de cette page, contient tout le code présenté ici (les deux implémentations, les fonctions de conversion, et tous les tests). Après l'avoir téléchargé, on l'exécute dans un terminal, dans le dossier où il a été enregistré, avec python3 graphes.py (Python doit être installé sur l'ordinateur). Le fichier affiche « OK » pour chaque test s'il n'y a pas d'erreur.
Un graphe, comme les piles, les files ou les arbres
Un graphe est une structure de données, au même titre que les piles, les files ou les listes. Les arbres binaires, vus dans la fiche « Les arbres binaires », sont d'ailleurs des graphes particuliers (un graphe orienté, sans cycle, où chaque sommet a au plus un père).

1. Vocabulaire à connaître avant de commencer

Chaque mot ci-dessous est utilisé exactement dans ce sens dans toute la page.

MotDéfinition précise
GrapheUne structure formée de deux ensembles : un ensemble V de sommets, et un ensemble E de liaisons entre certains de ces sommets.
SommetUn élément du graphe (aussi appelé nœud, ou vertex en anglais).
ArcUne liaison orientée entre deux sommets : elle a une extrémité initiale (le départ) et une extrémité terminale (l'arrivée), qui ne jouent pas le même rôle.
ArêteUne liaison non orientée entre deux sommets : contrairement à un arc, les deux sommets jouent le même rôle, il n'y a pas de sens de parcours.
Graphe orientéUn graphe dont les liaisons sont des arcs : le sens compte. On dit que B est un successeur de A s'il existe un arc de A vers B.
Graphe non orientéUn graphe dont les liaisons sont des arêtes : le sens ne compte pas. On dit que A et B sont voisins s'il existe une arête entre eux (et alors B est voisin de A si et seulement si A est voisin de B).
Poids (ou coefficient)Une valeur numérique attachée à un arc ou une arête, par exemple une distance ou une probabilité.
Graphe pondéréUn graphe dont chaque arc (ou arête) porte un poids.
Diagramme sagittalLa représentation dessinée d'un graphe : les sommets sont des points ou des cercles, les arcs des flèches, les arêtes des segments.
Liste d'adjacence (ou de successeurs)Une implémentation d'un graphe où chaque sommet est associé à la liste des sommets qui lui sont reliés.
Matrice d'adjacenceUne implémentation d'un graphe sous la forme d'un tableau à double entrée : une ligne et une colonne par sommet.
Matrice symétriqueUne matrice où, pour tout i et tout j, le terme ligne i colonne j est égal au terme ligne j colonne i. La matrice d'adjacence d'un graphe est symétrique si et seulement si le graphe est non orienté.

2. Qu'est-ce qu'un graphe ?

Définition
Un graphe est une structure constituée de deux ensembles : un ensemble fini V de sommets, et un ensemble E de liaisons entre certains des sommets de V.

Chaque liaison possède une extrémité initiale et une extrémité terminale. Lorsque cette distinction compte, on parle d'arc et le graphe est dit orienté ; lorsqu'elle ne compte pas, on parle d'arête et le graphe est dit non orienté.

Orienté ou non orienté : comment choisir ?
On choisit un graphe orienté quand la relation n'est pas forcément réciproque : par exemple, sur un réseau social, A peut suivre B sans que B ne suive A. On choisit un graphe non orienté quand la relation est toujours réciproque dans les deux sens : par exemple, une route entre deux villes se parcourt dans les deux sens.

3. À quoi sert un graphe ?

Un graphe permet de représenter des relations entre des objets. Par exemple, un réseau de transport pondéré, où chaque arête porte la distance (en km) entre deux villes :

Un réseau social se représente par un graphe orienté (la relation « suit » n'est pas forcément réciproque), et un réseau informatique (ordinateurs, imprimante, routeurs reliés par des câbles) se représente naturellement par un graphe non orienté, où chaque sommet est un appareil et chaque arête une connexion physique.

4. L'exemple utilisé dans toute la suite de cette page

On reprend le même graphe à 8 sommets dans toute la suite de la page : sa version orientée (avec des arcs) pour la section sur la liste d'adjacence, et sa version non orientée (les mêmes sommets, mais reliés par des arêtes) pour la section sur la matrice d'adjacence — pour bien voir la différence entre les deux notions sur un exemple identique par ailleurs.

4.1 — Version orientée

V = {1, 2, 3, 4, 5, 6, 7, 8}

E = {(1,2), (1,5), (1,8), (2,3), (3,1), (3,4), (4,8), (5,6), (7,6), (8,7)}

4.2 — Version non orientée

Les mêmes 8 sommets, mais avec des arêtes (sans sens de parcours) plutôt que des arcs :

E = {{1,2}, {1,3}, {1,5}, {1,8}, {2,3}, {3,4}, {4,8}, {5,6}, {6,7}, {7,8}}

5. Implémentation 1 : la liste d'adjacence

On utilise un dictionnaire Python : chaque sommet est une clé, et la valeur associée est la liste de ses successeurs (pour un graphe orienté) ou de ses voisins (pour un graphe non orienté).

5.1 — Les opérations primitives

OpérationEffet
creer_graphe()crée un graphe vide
est_dans(G, v)renvoie True si v est un sommet du graphe G, sinon False
ajouter_sommet(G, v)ajoute un sommet v au graphe G
supprimer_sommet(G, v)supprime le sommet v du graphe G
ajouter_arc(G, v1, v2)ajoute un arc de v1 vers v2
supprimer_arc(G, v1, v2)supprime l'arc de v1 vers v2
successeurs(G, v)renvoie la liste des sommets extrémités terminales des arcs d'extrémité initiale v
La sémantique compte
Le sens précis d'une opération dépend de l'implémentation choisie. Par exemple, selon les cas, ajouter_sommet peut nécessiter de préciser directement ses arcs, ou bien laisser la liste vide et ajouter les arcs séparément ensuite avec ajouter_arc — c'est ce deuxième choix qui est fait ci-dessous.

5.2 — Le code Python

def creer_graphe():
    return {}

def est_dans(G, v):
    return v in G

def ajouter_sommet(G, v):
    G[v] = []

def supprimer_sommet(G, v):
    del G[v]
    for sommet in G:
        if v in G[sommet]:
            G[sommet].remove(v)

def ajouter_arc(G, v1, v2):
    G[v1].append(v2)

def supprimer_arc(G, v1, v2):
    G[v1].remove(v2)

def successeurs(G, v):
    if est_dans(G, v):
        return G[v]
    else:
        return []
Et pour un graphe pondéré ?
Pour conserver un poids sur chaque arc (comme dans l'exemple du réseau de transport), on stocke des couples (sommet, poids) dans chaque liste, par exemple {'Paris': [('Brest', 592), ('Lyon', 546)], ...}. La fonction successeurs devient alors return [sommet for sommet, poids in G[v]] si on veut uniquement les noms, ou return G[v] si on veut garder les poids.

5.3 — Construire pas à pas la liste d'adjacence de l'exemple

Principe, en une phrase
On ajoute d'abord les 8 sommets, avec une liste de successeurs vide chacun ; puis on ajoute un par un les 10 arcs, ce qui allonge la liste du sommet de départ de chaque arc.
  1. Ajouter les 8 sommets 1 à 8 : chacun démarre avec une liste de successeurs vide.
  2. Ajouter l'arc (1,2) : 2 rejoint la liste de 1.
  3. Ajouter l'arc (1,5) : 5 rejoint la liste de 1.
  4. Ajouter l'arc (1,8) : 8 rejoint la liste de 1.
  5. Ajouter l'arc (2,3) : 3 rejoint la liste de 2.
  6. Ajouter l'arc (3,1) : 1 rejoint la liste de 3.
  7. Ajouter l'arc (3,4) : 4 rejoint la liste de 3.
  8. Ajouter l'arc (4,8) : 8 rejoint la liste de 4.
  9. Ajouter l'arc (5,6) : 6 rejoint la liste de 5.
  10. Ajouter l'arc (7,6) : 6 rejoint la liste de 7.
  11. Ajouter l'arc (8,7) : 7 rejoint la liste de 8.
arc (ou sommet) qui vient d'être ajouté
Étape 1 / 1
La ligne de code correspondant à l'étape en cours est surlignée ci-dessous.
G = creer_graphe()for v in [1, 2, 3, 4, 5, 6, 7, 8]:    ajouter_sommet(G, v)ajouter_arc(G, 1, 2)ajouter_arc(G, 1, 5)ajouter_arc(G, 1, 8)ajouter_arc(G, 2, 3)ajouter_arc(G, 3, 1)ajouter_arc(G, 3, 4)ajouter_arc(G, 4, 8)ajouter_arc(G, 5, 6)ajouter_arc(G, 7, 6)ajouter_arc(G, 8, 7)

On retrouve, une fois toutes ces instructions exécutées, exactement le graphe de la section 4.1 : G[1] == [2, 5, 8], G[2] == [3], G[3] == [1, 4], G[4] == [8], G[5] == [6], G[6] == [], G[7] == [6], G[8] == [7].

6. Implémentation 2 : la matrice d'adjacence

On utilise cette fois une matrice carrée (un tableau à double entrée) : les lignes représentent les sommets de départ, les colonnes les sommets d'arrivée. Une entrée nulle indique qu'il n'y a pas de liaison entre ces deux sommets ; une valeur non nulle indique l'existence d'un arc (ou d'une arête), avec une éventuelle pondération.

6.1 — Un premier exemple, pondéré et orienté

Sur un petit graphe à 3 sommets A, B, C, avec des arcs pondérés (y compris des boucles, un arc d'un sommet vers lui-même) : A→A (0,1), A→B (0,5), A→C (0,4), B→B (0,35), B→C (0,65), C→A (0,8), C→C (0,2). On dispose les sommets dans l'ordre alphabétique en ligne et en colonne, et on part d'une ligne i vers une colonne j :

ABC
A0.10.50.4
B00.350.65
C0.800.2

En Python, une matrice se représente comme un tableau de tableaux (une liste de listes) :

graph_proba = [[0.1, 0.5, 0.4],
               [0,   0.35, 0.65],
               [0.8, 0,   0.2]]

Cette matrice n'est pas symétrique (par exemple, ligne A colonne C vaut 0.4, mais ligne C colonne A vaut 0.8) : c'est normal, ce graphe est orienté.

6.2 — Construire la matrice du graphe non orienté de l'exemple

On applique maintenant la même idée à la version non orientée de l'exemple de la section 4.2. Comme le graphe est non orienté, chaque arête {i, j} remplit deux cases symétriques : ligne i colonne j, et ligne j colonne i.

ArêteCase remplie à 1
{1,2}ligne 1 col 2, et ligne 2 col 1
{1,3}ligne 1 col 3, et ligne 3 col 1
{1,5}ligne 1 col 5, et ligne 5 col 1
{1,8}ligne 1 col 8, et ligne 8 col 1
{2,3}ligne 2 col 3, et ligne 3 col 2
{3,4}ligne 3 col 4, et ligne 4 col 3
{4,8}ligne 4 col 8, et ligne 8 col 4
{5,6}ligne 5 col 6, et ligne 6 col 5
{6,7}ligne 6 col 7, et ligne 7 col 6
{7,8}ligne 7 col 8, et ligne 8 col 7

Toutes les autres cases restent à 0. On obtient la matrice suivante (les sommets 1 à 8 sont dans l'ordre, en ligne comme en colonne) :

On reconnaît un graphe non orienté au premier coup d'œil
Cette matrice est symétrique : la case ligne i colonne j vaut toujours la même chose que la case ligne j colonne i. C'est le cas si et seulement si le graphe est non orienté — un simple coup d'œil à la matrice de la section 6.1 (non symétrique) suffit à voir qu'elle représente un graphe orienté.

6.3 — Le code Python

def creer_matrice(n):
    return [[0] * n for _ in range(n)]

def est_dans_matrice(matrice, i):
    return 0 <= i < len(matrice)

def ajouter_arc_matrice(matrice, i, j, poids=1):
    matrice[i][j] = poids

def supprimer_arc_matrice(matrice, i, j):
    matrice[i][j] = 0

def successeurs_matrice(matrice, i):
    n = len(matrice)
    return [j for j in range(n) if matrice[i][j] != 0]

Ici, les sommets sont numérotés de 0 à n-1 (l'indice dans la matrice), plutôt que d'avoir un nom quelconque comme avec un dictionnaire : c'est la principale différence pratique entre les deux implémentations.

7. Passer d'une représentation à l'autre

On peut convertir une matrice d'adjacence en liste d'adjacence (par sommets numérotés), et inversement.

def matrice_vers_listes_succ(matrice):
    listes_succ = {i: [] for i in range(len(matrice))}   # Initialisation des listes de successeurs
    for i in range(len(matrice)):
        for j in range(len(matrice[i])):
            if matrice[i][j] == 1:      # S'il y a une arête de i vers j
                listes_succ[i].append(j)
    return listes_succ

def listes_succ_to_matrice(listes_succ):
    n = len(listes_succ)                 # Nombre de sommets
    matrice = [[0] * n for _ in range(n)]  # Initialisation de la matrice
    for i in listes_succ:
        for j in listes_succ[i]:
            matrice[i][j] = 1             # Ajout des arêtes
    return matrice

Par exemple, avec la petite matrice [[0, 1, 0], [0, 0, 1], [1, 0, 0]] (un cycle 0 → 1 → 2 → 0), matrice_vers_listes_succ renvoie {0: [1], 1: [2], 2: [0]}.

Et pour un graphe pondéré ?
Quand deux sommets sont reliés par un arc, le coefficient correspondant de la matrice est égal au poids de l'arc (et non à 1). Il suffit d'adapter le test if matrice[i][j] == 1 en if matrice[i][j] != 0, et de conserver le poids : listes_succ[i].append((j, matrice[i][j])).

8. Récapitulatif des complexités

n désigne le nombre de sommets du graphe, et m le nombre d'arcs (ou d'arêtes).

OpérationListe d'adjacence (dictionnaire)Matrice d'adjacence
est_dans(G, v)O(1)O(1) (test d'indice)
ajouter_sommet(G, v)O(1)O(n) : il faut agrandir toutes les lignes d'une case, et ajouter une ligne
supprimer_sommet(G, v)O(n + m) : il faut retirer v de la liste de chaque autre sommetO(n²) : il faut décaler toute une ligne et toute une colonne
ajouter_arc / supprimer_arcO(1) (ajout) à O(n) (suppression, à cause de list.remove)O(1) : on modifie une seule case
successeurs(G, v)O(1) : la liste existe déjàO(n) : il faut parcourir toute la ligne

En résumé : la liste d'adjacence est en général plus efficace pour ajouter ou retirer des sommets, et pour obtenir directement les successeurs d'un sommet. La matrice, elle, permet de tester en O(1) si un arc précis existe entre deux sommets donnés, et son inconvénient (le nombre de cases, n²) devient négligeable quand le graphe est très dense (beaucoup d'arcs par rapport au nombre de sommets).

9. Points clés à retenir

Prêt(e) pour la suite ? → Fiche « Les arbres binaires » · Retour à l'accueil