Exercice 1 — QCM : vocabulaire des listes chaînées
Pour chaque question, une seule réponse est correcte.
1. Dans une liste chaînée, chaque nœud contient :
- A. uniquement une valeur
- B. une valeur et une référence vers le nœud suivant
- C. une valeur, une référence vers le suivant et une référence vers le précédent
- D. une valeur et son indice dans la liste
Correction publiée après la séance.
2. Pour représenter une liste vide, on utilise :
- A. un nœud dont la valeur est 0
- B. un nœud dont la valeur est
None - C. un objet
ListeChaineedont l’attributpremiervautNone - D. la valeur
[]
Correction publiée après la séance.
3. Quelle est la complexité de l’ajout en tête d’une liste chaînée ?
- A. \(O(n)\) car il faut décaler tous les éléments
- B. \(O(1)\) car on crée un nœud et on met à jour une référence
- C. \(O(n)\) car il faut parcourir la liste pour trouver la tête
- D. \(O(\log n)\) car on utilise une recherche dichotomique
Correction publiée après la séance.
4. On considère la liste chaînée 5 → 3 → 8 → None. Quelle expression Python permet d’accéder à la valeur 8 ?
- A.
ma_liste.premier.element - B.
ma_liste.premier.suivant.element - C.
ma_liste.premier.suivant.suivant.element - D.
ma_liste.ieme_element(3)
Correction publiée après la séance.
5. Par rapport au type list de Python (tableau dynamique), la liste chaînée est plus performante pour :
- A. l’accès à un élément par son indice
- B. les insertions et suppressions en tête
- C. la recherche d’un élément
- D. le calcul de la longueur
Correction publiée après la séance.
Exercice 2 — Exercice guidé : manipuler une liste chaînée pas à pas
On rappelle les classes vues en cours :
class Noeud:
def __init__(self, element, suivant=None):
self.element = element
self.suivant = suivant
class ListeChainee:
def __init__(self):
self.premier = None
def ajout_en_tete(self, element):
nouveau = Noeud(element)
nouveau.suivant = self.premier
self.premier = nouveau
def ajout_en_queue(self, element):
"""Insère element en queue de liste"""
if self.premier == None:
self.ajout_en_tete(element)
else:
courant = self.premier
while courant.suivant != None:
courant = courant.suivant
dernier = Noeud(element)
courant.suivant = dernier
On exécute les instructions suivantes :
ma_liste = ListeChainee()
ma_liste.ajout_en_tete(7)
ma_liste.ajout_en_tete(3)
ma_liste.ajout_en_tete(9)
Dessiner l’état de la liste chaînée après chaque instruction
ajout_en_tete. On représentera chaque nœud par une case contenant la valeur et une flèche vers le suivant.Indice
ajout_en_tetecrée un nœud dont le suivant est l’ancien premier nœud, puis en fait le nouveau premier : chaque nouvelle valeur se place devant les autres.Quelle est la valeur de
ma_liste.premier.element?Indice
premierdésigne le nœud de tête, celui du dernier ajout en tête.Quelle est la valeur de
ma_liste.premier.suivant.suivant.element?Indice
Chaque
.suivantavance d’un nœud : partir de la tête du dessin et avancer deux fois.On ajoute maintenant
ma_liste.ajout_en_tete(1). Dessiner le nouvel état de la liste.
Correction publiée après la séance.
Exercice 3 — Exercice guidé : compléter des méthodes à trous
On reprend les classes Noeud et ListeChainee. Compléter les méthodes suivantes en remplaçant les ... par le code approprié.
a) Méthode __len__ qui renvoie le nombre d’éléments de la liste :
def __len__(self):
courant = ... # partir du premier nœud
cpt = 0
while courant:
courant = ... # passer au nœud suivant
cpt += 1
return ... # renvoyer le compteur
Indice
self.premier est le premier nœud et courant.suivant le nœud qui suit courant. La boucle s’arrête quand courant vaut None, c’est-à-dire après le dernier nœud.
b) Méthode __repr__ qui renvoie une représentation sous la forme "9 → 3 → 7" :
def __repr__(self):
elements = []
courant = self.premier
while courant:
elements.append(str(...)) # ajouter la valeur du nœud
courant = ... # passer au suivant
return " → ".join(elements)
Indice
La valeur d’un nœud est son attribut element ; on avance de nœud en nœud comme dans __len__.
c) Méthode contient qui renvoie True si element est dans la liste, False sinon :
def contient(self, element):
courant = self.premier
while courant:
if ... == element: # comparer avec la valeur du nœud
return True
courant = ... # passer au suivant
return ... # élément non trouvé
Indice
Comparer courant.element à element. Si la boucle se termine sans l’avoir trouvé, l’élément n’est pas dans la liste.
Correction publiée après la séance.
Exercice 4 — Accès et suppression en tête
Écrire la méthode
supprime_en_tete(self)qui supprime le premier élément de la liste et renvoie sa valeur. Si la liste est vide, la méthode doit lever une exceptionValueError("liste vide").Indice
Si
self.premiervautNone, la liste est vide : lever l’exception avecraise ValueError("liste vide"). Sinon, garder la valeur du premier nœud, faire du nœud suivant la nouvelle tête, puis renvoyer la valeur gardée.Code à copier puis compléter
def supprime_en_tete(self): if self.premier == None: raise ValueError("liste vide") valeur = self.premier.element self.premier = ... return ...Tester votre méthode sur la liste
9 → 3 → 7:- appeler
supprime_en_tete()une première fois : quelle valeur est renvoyée ? Quel est l’état de la liste ? - appeler
supprime_en_tete()deux fois de plus : quelles valeurs sont renvoyées ? - un dernier appel : que se passe-t-il ?
Indice
Construire la liste avec trois appels à
ajout_en_tete, en ajoutant 7, puis 3, puis 9, et afficher la liste après chaque suppression. Le dernier appel porte sur une liste vide : relire ce que la méthode doit faire dans ce cas.- appeler
Correction publiée après la séance.
Exercice 5 — Insertion en position quelconque
Écrire la méthode inserer(self, element, i) qui insère element en position i dans la liste (les positions sont comptées à partir de 0).
Indications :
- si
i == 0, il suffit d’utiliserajout_en_tete; - sinon, parcourir la liste jusqu’au nœud de position
i - 1(le futur prédécesseur) ; - créer un nouveau nœud dont le suivant est l’ancien nœud en position
i; - mettre à jour le suivant du nœud en position
i - 1.
Tester avec la liste 9 → 3 → 7 :
inserer(5, 0)doit donner5 → 9 → 3 → 7;inserer(1, 2)doit donner5 → 9 → 1 → 3 → 7.
Indice
Les indications donnent la marche à suivre. Pour atteindre le nœud de position i - 1, partir de self.premier et avancer i - 1 fois avec courant = courant.suivant.
Code à copier puis compléter
def inserer(self, element, i):
if i == 0:
self.ajout_en_tete(element)
else:
courant = self.premier
for _ in range(i - 1):
courant = ...
courant.suivant = Noeud(element, ...)
Correction publiée après la séance.
Exercice 6 — Comparaison de complexité
Compléter le tableau suivant en indiquant la complexité de chaque opération pour les deux structures :
| Opération | list Python (tableau) | Liste chaînée |
|---|---|---|
Accès à l’élément d’indice i | ||
| Ajout en tête | ||
| Ajout en queue | ||
| Suppression en tête | ||
| Suppression en queue | ||
| Recherche d’un élément |
Indice
Le tableau Python accède directement à la case d’indice i, mais doit décaler tous les éléments quand on insère ou supprime au début. La liste chaînée n’accède directement qu’à son premier nœud : atteindre un autre nœud demande de parcourir la liste depuis la tête.
Correction publiée après la séance.
Exercice 7 — Synthèse : ajouter en queue en temps constant
Dans la classe ListeChainee de l’exercice 2, la méthode ajout_en_queue parcourt toute la liste pour atteindre le dernier nœud. On écrit une nouvelle classe, ListeAvecDernier, qui garde en plus une référence dernier vers le dernier nœud. Quand la liste est vide, premier et dernier valent tous les deux None.
a) Quelle est la complexité de la méthode ajout_en_queue de la classe ListeChainee ? Pourquoi ?
Indice
Que fait la boucle while avant d’accrocher le nouveau nœud ? Combien de tours fait-elle pour une liste de \(n\) éléments ?
b) Compléter les méthodes de la classe ListeAvecDernier, pour que ajout_en_queue s’exécute en temps constant et que dernier désigne toujours le dernier nœud :
class ListeAvecDernier:
def __init__(self):
self.premier = None
self.dernier = None
def ajout_en_queue(self, element):
nouveau = Noeud(element)
if self.dernier == None:
self.premier = ...
else:
self.dernier.suivant = ...
self.dernier = ...
def ajout_en_tete(self, element):
nouveau = Noeud(element, self.premier)
self.premier = nouveau
if ...:
self.dernier = ...
def supprime_en_tete(self):
"""Retire le premier nœud d'une liste non vide et renvoie son élément."""
element = self.premier.element
self.premier = ...
if ...:
self.dernier = ...
return element
Indice
Si la liste n’a pas de dernier nœud, elle est vide : un nœud ajouté, en tête comme en queue, est alors à la fois le premier et le dernier. Après une suppression en tête, la liste est devenue vide quand premier vaut None : il n’y a plus de dernier nœud non plus.
c) Dans supprime_en_tete, que se passerait-il si l’on ne remettait pas dernier à None quand la liste devient vide ? Dérouler la suite d’opérations suivante.
l = ListeAvecDernier()
l.ajout_en_queue(5)
l.supprime_en_tete()
l.ajout_en_queue(8)
Indice
Après la suppression du seul nœud, premier vaut None. Vers quel nœud dernier pointe-t-il encore ? Que fait alors ajout_en_queue(8) ?
d) Garder une référence vers le dernier nœud ne rend pas la suppression en queue plus rapide. Pourquoi ?
Indice
Pour retirer le dernier nœud, quel nœud faut-il modifier ? Peut-on l’atteindre depuis dernier ?
Correction publiée après la séance.