Introduction : Pourquoi étudier les listes chaînées en NSI ?
Salut ! Si tu es en Terminale NSI, tu as déjà manipulé les listes natives de Python. Elles sont pratiques, mais sais-tu comment elles fonctionnent vraiment "sous le capot" ? Pour comprendre les structures de données, un pilier de l'informatique, il faut parfois les recréer soi-même. C'est là qu'intervient la liste chaînée. C'est une structure fondamentale, plus flexible que le tableau classique, que tu retrouveras dans de nombreux sujets de bac et projets. Dans cet article, on va plonger dans son implémentation complète en Python, pas à pas.
Le principe fondamental : des maillons et des chaînes
Contrairement à une liste Python qui stocke ses éléments dans un bloc de mémoire contigu, une liste chaînée les disperse. Chaque élément, appelé nœud (ou maillon), contient deux choses : la donnée utile (data) et une référence (un "lien") vers le nœud suivant. Le dernier nœud pointe vers une valeur spéciale, None, qui marque la fin de la chaîne.
Imagine une chasse au trésor où chaque indice te mène au suivant. C'est le principe ! Cette organisation a des conséquences majeures :
- Avantage : L'insertion ou la suppression d'un élément au milieu est très rapide (complexité O(1) si on a déjà la position), car il suffit de modifier quelques liens, sans avoir à décaler tous les autres éléments comme dans un tableau.
- Inconvénient : L'accès à un élément à un index donné est lent (complexité O(n)), car on doit parcourir la chaîne depuis le début. On parle d'accès séquentiel, et non aléatoire.
Implémentation pas à pas en Python
On va construire notre liste chaînée en deux temps : d'abord la classe pour le nœud, puis la classe pour la liste elle-même qui gérera ces nœuds.
Étape 1 : La classe Node (le nœud)
Chaque maillon de notre chaîne est un objet simple. On le définit avec un constructeur (__init__) qui initialise la donnée et le lien vers le suivant.
class Node:
"""Classe représentant un nœud (ou maillon) d'une liste chaînée."""
def __init__(self, data):
self.data = data # La valeur stockée
self.next = None # Le lien vers le nœud suivant. Par défaut, il n'y en a pas.C'est tout ! self.next sera soit un autre objet Node, soit None.
Étape 2 : La classe LinkedList (la liste)
Cette classe va gérer l'ensemble. Elle a juste besoin de connaître la tête (head), c'est-à-dire le premier nœud de la chaîne.
class LinkedList:
"""Classe représentant la liste chaînée elle-même."""
def __init__(self):
self.head = None # Au début, la liste est videÉtape 3 : Ajouter des méthodes essentielles
Une liste inerte ne sert à rien. Implémentons les opérations de base.
Ajout en fin de liste (append)
Il faut parcourir toute la liste jusqu'au dernier nœud (celui dont next est None) et lui attacher un nouveau nœud.
def append(self, data):
"""Ajoute un élément à la fin de la liste."""
new_node = Node(data)
if self.head is None: # Si la liste est vide
self.head = new_node
return
last_node = self.head
while last_node.next: # On avance jusqu'au dernier nœud
last_node = last_node.next
last_node.next = new_node # On attache le nouveau nœudAffichage de la liste (__str__)
Pour visualiser notre liste, on surcharge la méthode __str__. On parcourt et on accumule les données dans une chaîne.
def __str__(self):
"""Retourne une représentation lisible de la liste."""
nodes = []
current_node = self.head
while current_node:
nodes.append(str(current_node.data))
current_node = current_node.next
return " -> ".join(nodes) + " -> None"Insertion à une position donnée (insert)
C'est là que la liste chaînée montre sa force. On trouve le nœud à la position précédente, et on ajuste les liens.
def insert(self, data, position):
"""Insère un élément à l'index 'position' (en partant de 0)."""
if position == 0:
new_node = Node(data)
new_node.next = self.head
self.head = new_node
return
current_node = self.head
for i in range(position - 1):
if current_node is None:
raise IndexError("Position hors de la liste")
current_node = current_node.next
if current_node is None:
raise IndexError("Position hors de la liste")
new_node = Node(data)
new_node.next = current_node.next
current_node.next = new_nodeComparaison avec les listes natives de Python
Maintenant que tu as implémenté ta propre liste, voyons un tableau comparatif pour bien cerner les différences.
- Performance à l'insertion/suppression (milieu) :
Liste chaînée : Excellente (O(1) après recherche du point d'insertion).
Liste Python : Moyenne à mauvaise (O(n), car décalage des éléments). - Performance à l'accès par index (lecture) :
Liste chaînée : Mauvaise (O(n), parcours séquentiel obligatoire).
Liste Python : Excellente (O(1), accès direct). - Utilisation mémoire :
Liste chaînée : Légèrement plus gourmande à cause du stockage des références (next).
Liste Python : Plus efficace pour le stockage contigu de petits éléments.
En résumé, les listes natives de Python sont en réalité implémentées comme des tableaux dynamiques, pas comme des listes chaînées. Elles optimisent l'accès rapide, ce qui est le cas d'usage le plus fréquent.
Aller plus loin : listes doublement chaînées et applications
Notre liste est simplement chaînée : on ne peut la parcourir que dans un sens. Pour la NSI, tu dois aussi connaître le concept de liste doublement chaînée. Chaque nœud a alors un lien vers le suivant (next) ET vers le précédent (prev). Cela permet un parcours dans les deux sens et simplifie certaines suppressions, au prix d'une gestion un peu plus complexe.
Où retrouve-t-on ces structures ? Partout !
- L'historique de navigation de ton navigateur (précédent/suivant).
- La gestion de la mémoire dans certains systèmes.
- La file (deque du module
collectionsen Python) utilise une liste doublement chaînée pour des ajouts/suppressions rapides aux deux extrémités.
Implémenter une liste chaînée n'est pas qu'un exercice académique. C'est une porte d'entrée vers la compréhension profonde de l'algorithmique et de la gestion des données, des compétences centrales pour ton bac et tes futures études.
