NSI · Terminale
Structures de données : piles, files, objets, arbres et graphes
Une structure de données organise des valeurs pour répondre à un besoin : retrouver par clé, traiter dans l’ordre d’arrivée, revenir au dernier élément ou représenter des relations. Apprends à choisir une interface, prévoir les observations et comparer deux implantations avec des tests.
Explications et exemples en accès libre. Ateliers, quiz et cartes avec l’abonnement.
Prévoir mon tempsExplications et exemples en accès libre
Une première étude des explications, schémas, exemples résolus et erreurs expliquées. Les activités, productions, quiz et cartes réservés à l’abonnement ne sont pas comptés ici.
- Comprendre2059 mots d’explication et 4 schémas
- 13 à 21 min
- Étudier les exemples et les erreurs18 cas, exemples et activités guidés
- 36 à 66 min
Étude du cours en accès libre, environ45 min à 1 h 30
Voir le calcul et le temps par étape
Le calcul suit automatiquement les explications et les tâches présentes dans le cours. Ses coefficients sont des repères de planification, pas des temps mesurés auprès d’élèves.
- Lecture active : 2059 mots, à raison de 160 à 220 mots par minute.
- Schémas : 4, avec 1 à 2 min pour lire chacun.
- Exemple guidé : 6 × 2 à 4 min.
- Suivre le code résolu et ses tests : 6 × 3 à 5 min.
- Comprendre une erreur expliquée : 6 × 1 à 2 min.
- Pile ou file : prévoir ce qui sort8 à 15 min
- Objets : un état propre à chaque instance8 à 15 min
- Choisir entre indice, clé et ordre d’attente7 à 13 min
- Arbres : compter les nœuds et les arêtes8 à 16 min
- Graphes : traduire les relations sans les inverser8 à 16 min
- Deux implantations, le même programme client7 à 13 min
Les sous-totaux sont arrondis à la minute, puis additionnés. La fourchette totale est élargie aux cinq minutes voisines. Les étapes ci-dessus comprennent leurs explications et exemples ; elles ne s’ajoutent pas une seconde fois au total.
Adapte ce repère à tes acquis et au soin apporté aux exercices. Les pauses, les reprises, la consultation des sources externes et les révisions suivantes s’ajoutent selon tes besoins.
Avec les ateliers, le projet, le quiz et les cartes : environ 2 h 45 à 4 h 45, à répartir sur plusieurs séances.
Objectifs du cours
Ce que tu vas savoir faire
- Prévoir les sorties d’une pile et d’une file à partir de leur contrat.
- Écrire une classe dont chaque instance possède son propre état.
- Distinguer indice, clé, tableau dynamique et liste chaînée pour choisir une structure.
- Décrire un arbre binaire et calculer sa taille, sa hauteur et ses feuilles.
- Passer d’un graphe à une matrice ordonnée et retrouver ses successeurs et prédécesseurs.
- Comparer deux files avec le même programme client et des tests discriminants.
Étape du cours · 8 à 15 min
Pile ou file : prévoir ce qui sort
Trois demandes fictives A, B, C arrivent dans cet ordre. Une file traite A d’abord : c’est FIFO, premier entré, premier sorti. Une pile rend C d’abord : c’est LIFO, dernier entré, premier sorti. La pile convient à un historique à remonter ; la file conserve l’ordre d’attente. Ni l’une ni l’autre ne choisit automatiquement la demande la plus urgente.
Un type abstrait décrit les valeurs manipulées et les opérations permises, avec leurs préconditions et leurs résultats. Son interface expose ces opérations au programme client. L’implantation décide comment les réaliser. Dire « empiler ajoute un élément au sommet » appartient au contrat ; dire « utiliser la dernière case d’une liste Python » appartient à une implantation.
Notre première pile est un tuple : creer_pile() rend (), empiler(p, x) rend une nouvelle pile, sommet(p) observe sans retirer, et depiler(p) rend le couple (valeur, nouvelle_pile). Sur le vide, les deux dernières opérations lèvent ValueError. La loi depiler(empiler(p, x)) == (x, p) exprime le comportement attendu sur les valeurs ordinaires utilisées ici.
Il faut réaffecter le résultat : appeler seulement empiler(p, 'A') laisse p inchangé. C’est un contrat fonctionnel, différent de la classe mutable de l’étape suivante. Le tuple ne change pas de cases, mais les objets qu’il référence peuvent être mutables. Ajouter à une liste stockée dedans reste possible : immuable ne signifie pas copie profonde.
Ce modèle privilégie une trace facile à lire. Concaténer ou découper un tuple de n éléments recopie des références : ces opérations coûtent O(n), même si leur écriture tient en une ligne. La conformité LIFO ne garantit donc pas l’efficacité. On pourra changer de stockage tout en préservant un contrat donné.
Voir pour comprendre
Mêmes arrivées, sorties différentes
A arrive avant B, puis C. Les listes ci-dessous donnent l’ordre des retraits.
Arrivée : A → B → C
Pile · LIFO
Ordre des sorties :
- Csort en premier
- Bsort en 2e
- Asort en 3e
File · FIFO
Ordre des sorties :
- Asort en premier
- Bsort en 2e
- Csort en 3e
Lis le schéma. Cache les sorties : quel élément serait retiré en premier dans chaque structure ?
La pile inverse ici l’ordre d’arrivée ; la file le conserve. Avec une seule valeur, cette différence resterait invisible.
Exemples résolus et erreurs expliquées
Trois arrivées, deux ordres de sortie
- Pars de
p0 = (). Aprèsp1 = empiler(p0, 'A'), p1 contient A et p0 reste vide. Les deux noms ne désignent pas le même état de pile. - Construis p2 en ajoutant B à p1, puis p3 en ajoutant C à p2. Le sommet de p3 est C ; observer ce sommet ne retire rien.
- Dépiler p3 rend
('C', ('A', 'B')). Pour poursuivre, conserve les deux résultats :valeur, p = depiler(p3). Puis p rend B et enfin A. - Sur une file alimentée par les mêmes arrivées, les retraits rendent A, B, C. Le test avec un seul élément ne distingue pas ces deux comportements ; deux valeurs différentes suffisent.
Conclusion. Le nom du conteneur ne suffit pas : écris l’ordre attendu et précise si l’opération modifie l’état ou en renvoie un nouveau.
Laboratoire de code
Languepython
ButConstruire une pile fonctionnelle et vérifier ses observations, y compris sur le vide.
Code solution
def creer_pile():
return ()
def est_vide(pile):
return len(pile) == 0
def empiler(pile, valeur):
return pile + (valeur,)
def sommet(pile):
if est_vide(pile):
raise ValueError('pile vide')
return pile[-1]
def depiler(pile):
if est_vide(pile):
raise ValueError('pile vide')
return pile[-1], pile[:-1]Tests
p0 = creer_pile()
p1 = empiler(p0, 'A')
p2 = empiler(p1, 'B')
assert est_vide(p0)
assert sommet(p2) == 'B'
assert depiler(p2) == ('B', ('A',))
assert p0 == ()
assert depiler(empiler(p1, 'X')) == ('X', p1)
for operation in (sommet, depiler):
try:
operation(())
except ValueError:
pass
else:
raise AssertionError('le vide doit être refusé')
p = ()
empiler(p, 'A')
assert p == ()
contenu = []
p = empiler((), contenu)
contenu.append('X')
assert sommet(p) is contenu
assert sommet(p) == ['X']Trace
- creer produit une pile vide dont est_vide vaut vrai
- empiler A puis B produit des états observables dont le sommet est B
- depiler rend B et une pile dont le sommet est A ; l'état p0 n'a pas été modifié
Clinique de bogue
Indice observéaprès avoir empilé A puis B, la fonction rend A : elle réalise une file au lieu d'une pile
CauseL'implantation retire la première case, alors que le contrat dernier entré, premier sorti exige l'extrémité où B vient d'être ajouté. Le défaut porte sur l'observation abstraite, pas sur le type tuple lui-même.
Étape du cours · 8 à 15 min
Objets : un état propre à chaque instance
Une classe réunit une description d’état et des méthodes pour agir dessus. p = Pile() crée une instance ; __init__ initialise cette nouvelle instance. L’attribut p._contenu porte ses données. La méthode p.empiler('A') utilise self pour désigner p ; dans cet exemple, cet appel équivaut à Pile.empiler(p, 'A').
Contrairement au tuple de l’étape précédente, cette pile se modifie sur place. empiler ne rend pas une nouvelle pile : sans return, son résultat est None. Écrire p = p.empiler('A') ferait donc perdre la référence à l’objet. depiler retire puis rend la valeur ; sommet rend la valeur sans changer l’ordre ni la taille.
Placer _contenu = [] directement dans le corps de la classe créerait une liste partagée par les instances qui l’utilisent. La créer dans __init__ donne à p et q deux listes distinctes. Ne confonds pas ce défaut avec q = p : cette affectation donne volontairement deux noms au même objet, sans créer une deuxième pile.
L’interface maintient un invariant : les valeurs sont rangées de la plus ancienne à la plus récente, et seul le sommet est retiré. Sur le vide, observer ou retirer lève ValueError sans changer l’état. Le préfixe _ signale un détail interne par convention ; il n’empêche pas techniquement le client de modifier cet attribut.
observer() renvoie un tuple des valeurs, pour examiner l’ordre sans donner directement la liste de rangement. Ce tuple est une copie superficielle des références : si une valeur est elle-même une liste, elle reste partagée. L’indépendance des conteneurs et l’indépendance des objets contenus sont deux questions différentes.
Voir pour comprendre
Deux instances, deux états
Les observations sont prises après chaque ligne ; observer ne retire aucun élément.
| Opération | p.observer() | q.observer() |
|---|---|---|
p, q = Pile(), Pile() | () | () |
p.empiler('A') | ('A',) | () |
q.empiler('B') | ('A',) | ('B',) |
p.depiler() | () | ('B',) |
Lis le schéma. Que devrait afficher la colonne p si la liste de rangement était partagée par erreur ?
L’état de q reste indépendant des opérations sur p. Une nouvelle instance se construit ; une affectation q = p ne copie pas l’objet.
Exemples résolus et erreurs expliquées
Deux piles ou deux noms ?
- Crée
p, q = Pile(), Pile(). Il y a deux appels au constructeur, donc deux instances et deux listes de rangement initialement vides. - Appelle
p.empiler('A'), puisq.empiler('B'). Les observations deviennent('A',)pour p et('B',)pour q : B ne doit pas apparaître chez p. - Appelle
p.depiler(). Le résultat est A, p est vide et q contient toujours B. Ces quatre états sont ceux du tableau. - Si tu écris ensuite
r = q, r et q désignent la même instance.r.empiler('C')change aussi ce qu’observe q. Ce partage vient de l’affectation, pas d’un attribut de classe.
Conclusion. Compte les constructions d’objets, puis suis les références. Un nom supplémentaire n’est pas une copie.
Laboratoire de code
Languepython
ButConstruire des instances indépendantes et observer leur état à travers une interface explicite.
Code solution
class Pile:
def __init__(self):
self._contenu = []
def est_vide(self):
return len(self._contenu) == 0
def empiler(self, valeur):
self._contenu.append(valeur)
def sommet(self):
if self.est_vide():
raise ValueError('pile vide')
return self._contenu[-1]
def depiler(self):
if self.est_vide():
raise ValueError('pile vide')
return self._contenu.pop()
def observer(self):
return tuple(self._contenu)Tests
p, q = Pile(), Pile()
assert p.empiler('A') is None
q.empiler('B')
assert p.observer() == ('A',)
assert q.observer() == ('B',)
assert p.depiler() == 'A'
assert p.est_vide() and q.sommet() == 'B'
for operation in (p.sommet, p.depiler):
try:
operation()
except ValueError:
pass
else:
raise AssertionError('le vide doit être refusé')
contenu = []
p.empiler(contenu)
vue = p.observer()
contenu.append('X')
assert vue[0] is contenu
assert p.sommet() == ['X']Trace
- p et q construisent deux listes internes distinctes
- empiler A puis B sur p laisse q vide et place B au sommet de p
- observer rend ('A',) après dépilement, sans fournir la liste mutable interne
Clinique de bogue
Indice observéempiler A dans une première instance fait apparaître A dans une seconde instance nouvellement créée
CauseUne liste placée comme attribut de classe est partagée tant que les instances utilisent cet attribut. Chaque appel à __init__ doit créer une nouvelle liste d’instance. Ce changement sépare les conteneurs, sans copier profondément les valeurs qu’on y empile.
Étape du cours · 7 à 13 min
Choisir entre indice, clé et ordre d’attente
Une liste abstraite représente une suite. Une implantation chaînée relie une cellule, qui contient une valeur, à la suivante. Dans ('A', ('B', None)), la tête vaut A, puis on suit un lien pour atteindre B. Ajouter en tête crée une cellule ; accéder à l’élément d’indice k oblige à suivre k liens. None représente ici la fin de chaîne.
Une list de CPython est un tableau dynamique de références, pas une liste chaînée. L’accès par indice est direct, tandis que retirer en tête avec pop(0) décale les références suivantes. Retirer à la fin évite ce décalage. Les coûts décrivent une implantation et un modèle d’opérations, pas la longueur du code ni un chronométrage universel.
Un dictionnaire associe une valeur à une clé. Avec catalogue = {'T2': 'dessiner', 'T7': 'tester'}, catalogue['T7'] vaut « tester » : T7 n’est pas un rang. in teste les clés, et réaffecter une clé remplace sa valeur. Dans une table de hachage, l’accès est en moyenne O(1) si le hachage répartit correctement les clés et si calculer leur hash et les comparer coûte O(1) ; ce n’est pas une garantie du pire cas.
Pour traiter des identifiants dans l’ordre d’arrivée, emploie une file et garde leurs descriptions dans un dictionnaire. Changer la description de T7 ne doit pas déplacer T7 dans l’attente. Une clé inconnue lève KeyError dans un accès direct : choisis et documente le comportement souhaité avant de le masquer par une valeur par défaut.
Notre File stocke les valeurs dans une liste avec un indice de tête. Défiler avance cet indice, libère la référence consommée puis compacte occasionnellement la liste. L’invariant est 0 <= _tete <= len(_donnees) ; les éléments actifs commencent à _tete. Une sortie peut coûter O(n) lors d’une copie, mais une longue suite d’opérations a un coût amorti O(1) par ajout ou retrait. observer() copie les n éléments actifs et reste O(n).
Exemples résolus et erreurs expliquées
Le rang change, la clé reste
- Le dictionnaire contient T2 et T7 avec leurs descriptions ; la file contient les clés dans cet ordre. Le premier élément attendu est T2, pas la plus petite valeur de texte.
- Remplace la description de T7 par « tester le cas vide ». L’affectation change une association du dictionnaire, sans appeler de méthode de la file.
- Défile une fois : tu obtiens T2. La file ne contient plus que T7, désormais en tête, mais la clé T7 et sa description n’ont pas changé.
- Dans une chaîne séparée
('A', ('B', None)), atteindre l’indice 1 exige de suivre un lien. Ni la clé T7 ni la valeur B ne constituent un indice de ce dictionnaire.
Conclusion. Choisis séparément comment retrouver une donnée et dans quel ordre la traiter.
Laboratoire de code
Languepython
ButImplanter une file FIFO, suivre une chaîne et distinguer les clés du rang dans l’attente.
Code solution
class File:
def __init__(self):
self._donnees = []
self._tete = 0
def est_vide(self):
return self._tete == len(self._donnees)
def enfiler(self, valeur):
self._donnees.append(valeur)
def tete(self):
if self.est_vide():
raise ValueError('file vide')
return self._donnees[self._tete]
def defiler(self):
valeur = self.tete()
self._donnees[self._tete] = None
self._tete += 1
if self.est_vide():
self._donnees = []
self._tete = 0
elif self._tete > 8 and 2 * self._tete >= len(self._donnees):
self._donnees = self._donnees[self._tete:]
self._tete = 0
return valeur
def observer(self):
return tuple(self._donnees[self._tete:])
def valeur_indice(chaine, indice):
# Précondition : chaîne finie de couples (valeur, suivant), terminée par None.
if type(indice) is not int or indice < 0:
raise ValueError('indice entier positif ou nul attendu')
courant = chaine
for _ in range(indice):
if courant is None:
raise IndexError('indice hors chaîne')
courant = courant[1]
if courant is None:
raise IndexError('indice hors chaîne')
return courant[0]Tests
f = File()
catalogue = {'T2': 'dessiner', 'T7': 'tester'}
for identifiant in ('T2', 'T7'):
f.enfiler(identifiant)
catalogue['T7'] = 'tester le cas vide'
assert f.tete() == 'T2'
assert catalogue['T7'] == 'tester le cas vide'
assert f.observer() == ('T2', 'T7')
assert [f.defiler(), f.defiler()] == ['T2', 'T7']
assert f.est_vide()
for i in range(30):
f.enfiler(i)
assert [f.defiler() for _ in range(25)] == list(range(25))
assert f.observer() == (25, 26, 27, 28, 29)
assert valeur_indice(('A', ('B', None)), 1) == 'B'
assert 'T7' in catalogue and 'tester le cas vide' not in catalogueTrace
- Enfiler T2 puis T7 conserve cet ordre, même quand la description de T7 change.
- Après 25 retraits dans la file des entiers 0 à 29, l’observation est (25, 26, 27, 28, 29).
- Dans la chaîne A → B → fin, l’indice 1 demande un lien ; la clé T7 appartient à une autre interface.
Clinique de bogue
Indice observéAprès les arrivées T2 puis T7, le premier retrait rend T7 au lieu de T2.
Causeappend suivi de pop sans indice retire du même côté : le résultat est LIFO. Pour respecter FIFO, il faut lire la tête logique puis l’avancer. Le test de deux éléments distincts révèle cette inversion ; un unique élément ne la révèle pas.
Étape du cours · 8 à 16 min
Arbres : compter les nœuds et les arêtes
Un arbre enraciné organise des nœuds à partir d’une racine. Chaque autre nœud possède un unique parent ; il n’y a pas de cycle et tous les nœuds sont accessibles depuis la racine. Une feuille n’a aucun enfant. Le sous-arbre enraciné en un nœud comprend ce nœud et ses descendants. Un arbre non vide de n nœuds possède n − 1 arêtes.
Dans un arbre binaire, chaque nœud possède deux emplacements distingués : gauche et droit. Chacun peut contenir un sous-arbre ou être vide. Un enfant gauche seul et un enfant droit seul ne donnent donc pas la même structure. Un arbre binaire n’est pas automatiquement un arbre binaire de recherche : ce dernier ajoute des règles d’ordre sur les clés.
La profondeur d’un nœud est ici le nombre d’arêtes depuis la racine ; la racine est à profondeur 0. La hauteur de l’arbre est la profondeur maximale. Un singleton a donc hauteur 0 ; nous donnons à l’arbre vide la hauteur −1 pour garder la même formule récursive. D’autres exercices comptent les nœuds : pour un arbre non vide, leur hauteur vaut la nôtre plus 1. Vérifie toujours la convention.
Le modèle Python emploie None pour le vide et (valeur, gauche, droite) pour un nœud. La taille vérifie 1 + taille(gauche) + taille(droite) ; la hauteur vérifie 1 + max(hauteur(gauche), hauteur(droite)). Les fonctions supposent une structure finie bien formée. Elles ne valident pas un objet arbitraire et la profondeur d’appels reste limitée par l’exécution Python.
Pour un arbre binaire non vide de hauteur h et de taille n, h + 1 <= n <= 2**(h + 1) - 1. Le chemin le plus long fournit la borne basse ; les niveaux pleins de 1, 2, 4… nœuds donnent la borne haute. À taille fixée, une chaîne donne la hauteur maximale n − 1. Ces bornes évitent d’accepter un résultat impossible.
Dire qu’un arbre est complet exige une définition. Dans le transfert, tous les niveaux sauf peut-être le dernier sont pleins et le dernier se remplit de gauche à droite. Un arbre parfait a tous ses niveaux pleins. L’exemple dessiné est complet, mais pas parfait : au dernier niveau, 1 et 6 occupent les deux premières places, puis les deux places sous 10 restent vides.
Voir pour comprendre
Une racine, cinq nœuds, trois feuilles
Les fils gauches sont dessinés à gauche et les fils droits à droite. Les emplacements vides ne sont pas dessinés.
Taille : 5 nœuds · hauteur : 2 arêtes
Feuilles : 1, 6, 10
- Profondeur 0 : 8
- Profondeur 1 : 3, 10
- Profondeur 2 : 1, 6
Lis le schéma. Le chemin 8 → 3 → 6 compte trois nœuds. Combien d’arêtes contient-il ?
La taille vaut 5 et la hauteur en arêtes vaut 2. La feuille 10 est moins profonde que 1 et 6, mais reste une feuille.
Exemples résolus et erreurs expliquées
Hauteur 2, taille 5 : pourquoi ?
- Repère la racine 8. Son enfant gauche est 3, qui porte 1 et 6 ; son enfant droit est 10, sans enfant. Il y a cinq nœuds, pas cinq arêtes.
- Pars des feuilles : leurs deux sous-arbres sont vides, de hauteur −1. Leur hauteur vaut donc
1 + max(-1, -1), soit 0. - Remonte vers 3 : ses enfants ont hauteur 0, donc 3 a hauteur 1. Remonte vers 8 : ses enfants ont hauteurs 1 et 0, donc 8 a hauteur 2.
- Les profondeurs sont 0 pour 8, 1 pour 3 et 10, 2 pour 1 et 6. La feuille 10 est à profondeur 1 mais son propre sous-arbre a hauteur 0 : ne confonds pas les deux mesures.
Conclusion. Compte les arêtes pour la hauteur choisie ici, les nœuds pour la taille et vérifie le vide séparément.
Laboratoire de code
Languepython
ButMesurer taille, hauteur, feuilles et niveaux d’un arbre binaire fini représenté par des tuples.
Code solution
def noeud(valeur, gauche=None, droite=None):
# Les enfants doivent déjà être des arbres finis bien formés.
return (valeur, gauche, droite)
def taille(arbre):
if arbre is None:
return 0
return 1 + taille(arbre[1]) + taille(arbre[2])
def hauteur(arbre):
if arbre is None:
return -1
return 1 + max(hauteur(arbre[1]), hauteur(arbre[2]))
def feuilles(arbre):
if arbre is None:
return []
if arbre[1] is None and arbre[2] is None:
return [arbre[0]]
return feuilles(arbre[1]) + feuilles(arbre[2])
def niveaux(arbre):
resultat = {}
def visiter(courant, profondeur):
if courant is not None:
resultat.setdefault(profondeur, []).append(courant[0])
visiter(courant[1], profondeur + 1)
visiter(courant[2], profondeur + 1)
visiter(arbre, 0)
return resultatTests
a = noeud(8, noeud(3, noeud(1), noeud(6)), noeud(10))
assert taille(a) == 5
assert hauteur(a) == 2
assert feuilles(a) == [1, 6, 10]
assert niveaux(a) == {0: [8], 1: [3, 10], 2: [1, 6]}
assert taille(None) == 0 and hauteur(None) == -1
assert hauteur(noeud(8)) == 0
assert 2 + 1 <= taille(a) <= 2**(2 + 1) - 1
assert niveaux(None) == {}Trace
- Les feuilles 1, 6 et 10 ont hauteur 0, même si elles ne sont pas à la même profondeur.
- Le nœud 3 a hauteur 1 ; la racine 8 a hauteur 1 + max(1, 0) = 2.
- La taille vaut 1 + 3 + 1 = 5 ; les cinq nœuds sont reliés par quatre arêtes.
Clinique de bogue
Indice observéLe singleton reçoit la hauteur 1 alors que le contrat de ce cours impose 0.
CauseLe cas vide renvoie 0 tout en conservant la formule 1 + max : on compte alors les nœuds du chemin. Cette convention est possible, mais incohérente avec les exemples et le contrat en arêtes. Le vide à −1 rétablit un singleton à 0 et toutes les mesures associées.
Étape du cours · 8 à 16 min
Graphes : traduire les relations sans les inverser
Un graphe décrit des sommets et leurs relations. Une arête non orientée relie deux sommets sans ordre ; un arc orienté va d’un sommet de départ vers un sommet d’arrivée. Avec A → B, B est successeur de A et A est prédécesseur de B. L’arc inverse n’existe que si on l’ajoute. Les graphes suivants utilisent des lettres fictives, sans personnes ni réseau réel.
On représente ici les successeurs par un dictionnaire d’ensembles. Dans {'A': {'B'}, 'B': {'C'}, 'C': set(), 'D': set()}, C n’a aucune sortie mais reçoit l’arc venant de B. D est isolé : aucune entrée ni sortie. Un ensemble évite les doublons ; ce modèle ne représente pas plusieurs arcs parallèles. Le laboratoire autorise un arc d’un sommet vers lui-même, appelé boucle.
Pour écrire une matrice, fixe un ordre des sommets, par exemple A, B, C, D. La case M[i][j] vaut 1 si un arc va du sommet de la ligne i au sommet de la colonne j, et 0 sinon. La ligne B contient donc un 1 dans la colonne C. Changer l’ordre permute les lignes et les colonnes ensemble ; une matrice sans ses étiquettes perd cette correspondance.
Une représentation non orientée doit être symétrique : B apparaît chez A si et seulement si A apparaît chez B. Dans une matrice orientée, une ligne nulle ne suffit pas pour conclure « isolé » : vérifie aussi la colonne. Les sommes d’une ligne et d’une colonne donnent les degrés sortant et entrant de nos graphes sans arcs multiples ; une boucle compte une fois dans chacun.
La matrice prend O(n²) cases pour n sommets ; tester une case est direct, mais énumérer les voisins exige de lire une ligne. Le dictionnaire d’ensembles utilise O(n + m) emplacements, à un facteur constant près pour un graphe non orienté. Parcourir un ensemble de successeurs visite seulement ces voisins. Il ne faut pas confondre récupérer une collection et parcourir son contenu.
Le code vérifie les étiquettes, les extrémités et la forme de la matrice. Une valeur booléenne n’est pas acceptée à la place d’un entier 0 ou 1. La conversion inverse recrée aussi les sommets isolés. Ces contrôles font partie du coût du programme fourni ; ils ne sont pas gratuits sous prétexte qu’une opération élémentaire est O(1). ajouter_arete modifie le graphe non orienté sur place et renvoie None ; une extrémité invalide est refusée avant toute écriture.
Voir pour comprendre
Le dessin et sa matrice racontent les mêmes arcs
Ordre des sommets : A, B, C, D. Deux arcs : A → B et B → C.
| De / vers | A | B | C | D |
|---|---|---|---|---|
| A | 0 | 1 | 0 | 0 |
| B | 0 | 0 | 1 | 0 |
| C | 0 | 0 | 0 | 0 |
| D | 0 | 0 | 0 | 0 |
Lis le schéma. Cherche le 1 correspondant à B → C, puis compare les colonnes C et D.
Une ligne nulle indique l’absence de sorties. D est isolé parce que sa colonne est aussi nulle ; C reçoit l’arc de B.
Exemples résolus et erreurs expliquées
C et D : deux lignes nulles, deux situations
- Fixe l’ordre
[A, B, C, D]. Place un 1 en ligne A, colonne B pour A → B, puis en ligne B, colonne C pour B → C. - Lis la ligne C : elle est nulle, donc aucun arc ne part de C. Lis maintenant la colonne C : le 1 venant de B prouve que C n’est pas isolé.
- Lis la ligne D et la colonne D : elles sont toutes deux nulles. D est isolé, mais reste un sommet et doit conserver sa ligne, sa colonne et sa clé.
- Dans l’ordre
[D, C, B, A], A → B se trouve en ligne d’indice 3, colonne d’indice 2. Le dessin peut rester identique ; seule la façon d’indexer la matrice a changé.
Conclusion. Traduis toujours « de qui vers qui ? » avant d’écrire les indices.
Laboratoire de code
Languepython
ButConstruire un graphe fictif, changer de représentation et conserver les sommets isolés.
Code solution
def verifier_sommets(sommets):
if type(sommets) not in (list, tuple):
raise ValueError('liste ou tuple de sommets attendu')
if any(type(s) is not str or not s for s in sommets):
raise ValueError('étiquettes textuelles non vides attendues')
if len(set(sommets)) != len(sommets):
raise ValueError('sommet répété')
def construire_graphe(sommets, aretes, oriente=False):
verifier_sommets(sommets)
if type(oriente) is not bool or type(aretes) not in (list, tuple):
raise ValueError('orientation booléenne et liste de relations attendues')
graphe = {s: set() for s in sommets}
for relation in aretes:
if type(relation) not in (list, tuple) or len(relation) != 2:
raise ValueError('deux extrémités attendues')
a, b = relation
if type(a) is not str or type(b) is not str or a not in graphe or b not in graphe:
raise ValueError('sommet inconnu')
graphe[a].add(b)
if not oriente:
graphe[b].add(a)
return graphe
def graphe_non_oriente(sommets, aretes):
return construire_graphe(sommets, aretes)
def verifier_graphe(graphe):
if type(graphe) is not dict:
raise ValueError('dictionnaire attendu')
verifier_sommets(list(graphe))
for voisins in graphe.values():
if type(voisins) is not set or not voisins <= graphe.keys():
raise ValueError('ensemble de sommets connus attendu')
def est_symetrique(graphe):
verifier_graphe(graphe)
return all(a in graphe[b] for a in graphe for b in graphe[a])
def matrice(graphe, ordre):
verifier_graphe(graphe)
verifier_sommets(ordre)
if set(ordre) != set(graphe):
raise ValueError('tous les sommets, une seule fois')
return [[int(b in graphe[a]) for b in ordre] for a in ordre]
def depuis_matrice(tableau, ordre, oriente=False):
verifier_sommets(ordre)
n = len(ordre)
if type(oriente) is not bool or type(tableau) not in (list, tuple) or len(tableau) != n:
raise ValueError('matrice carrée attendue')
if any(type(ligne) not in (list, tuple) or len(ligne) != n
or any(type(x) is not int or x not in (0, 1) for x in ligne)
for ligne in tableau):
raise ValueError('n lignes de n entiers 0 ou 1 attendues')
if not oriente and any(tableau[i][j] != tableau[j][i] for i in range(n) for j in range(n)):
raise ValueError('matrice non orientée asymétrique')
return {a: {b for j, b in enumerate(ordre) if tableau[i][j] == 1}
for i, a in enumerate(ordre)}
def ajouter_arete(graphe, a, b):
if not est_symetrique(graphe):
raise ValueError('graphe non orienté attendu')
if type(a) is not str or type(b) is not str or a not in graphe or b not in graphe:
raise ValueError('sommet inconnu')
# Nouveaux ensembles : un éventuel alias ne modifie pas un troisième sommet.
graphe[a] = graphe[a] | {b}
graphe[b] = graphe[b] | {a}Tests
ordre = ['A', 'B', 'C', 'D']
g = construire_graphe(ordre, [('A', 'B'), ('B', 'C')], True)
m = [[0,1,0,0], [0,0,1,0], [0,0,0,0], [0,0,0,0]]
assert matrice(g, ordre) == m
assert depuis_matrice(m, ordre, True) == g
assert g['C'] == set() and 'C' in g['B']
assert all('D' not in voisins for voisins in g.values())
u = graphe_non_oriente(ordre, [('A', 'B'), ('B', 'C')])
assert est_symetrique(u)
ajouter_arete(u, 'A', 'C')
assert 'C' in u['A'] and 'A' in u['C']
assert depuis_matrice([], []) == {}Trace
- A → B et B → C occupent les cases (ligne A, colonne B) et (ligne B, colonne C).
- C a une ligne nulle mais une colonne non nulle ; D a une ligne et une colonne nulles.
- La conversion inverse rend les quatre sommets, même D sans relation.
Clinique de bogue
Indice observéaprès ajout de l'arête A-B dans un graphe annoncé non orienté, B ne contient pas A parmi ses voisins
CauseAjouter seulement b dans les voisins de a crée une relation orientée. Pour un graphe non orienté, les deux observations doivent rester symétriques. Il faut aussi valider les extrémités avant toute écriture ; recréer les ensembles concernés évite de contaminer un troisième sommet par alias.
Étape du cours · 7 à 13 min
Deux implantations, le même programme client
Le programme client d’une file utilise enfiler, defiler, tete, est_vide et observer. Il ne lit ni _donnees ni _tete. Une seconde implantation peut alors employer deux piles : une pile d’entrée reçoit les arrivées, une pile de sortie fournit les retraits. Ici, chaque pile est réalisée par une liste avec append et pop à droite.
Quand la pile de sortie est vide, transfère toute la pile d’entrée vers elle. L’inversion fait remonter le plus ancien élément au sommet de la sortie. Si la sortie n’est pas vide, ne transfère rien : ses éléments sont arrivés avant ceux qui attendent encore dans l’entrée. Transférer trop tôt ferait passer un nouveau venu devant eux.
L’état abstrait observable correspond à reversed(sortie) suivi de entree. tete() peut provoquer un transfert interne, mais elle ne change ni l’ordre logique ni le nombre d’éléments. C’est un exemple où « observer sans modifier la file » ne signifie pas « aucune case interne ne change ». Le contrat parle de ce que le client est autorisé à observer.
Chaque élément est empilé à l’entrée, transféré au plus une fois, puis retiré de la sortie. Sur une suite d’opérations, cela justifie un coût amorti O(1) par opération de file, avec les coûts usuels amortis des listes Python. Un transfert isolé peut coûter O(n). Copier une observation de toute la file coûte toujours O(n), quelle que soit l’implantation étudiée.
Le banc de conformité reçoit une fabrique d’objets et joue les mêmes commandes. Il vérifie l’ordre, les observations répétées, les erreurs sur le vide et l’indépendance de deux instances. Réussir un corpus fini donne des preuves sur ces cas, pas sur tous les usages possibles. La substituabilité exige le même contrat complet ; la rapidité se compare dans une analyse séparée.
Exemples résolus et erreurs expliquées
Le transfert qu’il ne faut pas faire trop tôt
- Enfile A puis B : la pile d’entrée contient
[A, B], avec B au sommet, et la pile de sortie est vide. La file logique contient A puis B. - Demande un retrait : transfère B puis A vers la sortie, qui contient
[B, A]. Retire A au sommet. B reste en attente dans la sortie. - Enfile C : l’entrée contient
[C], la sortie[B]. La prochaine tête doit rester B. Transférer C maintenant le placerait devant B. - Retire B, puis demande un autre retrait. La sortie est désormais vide : transfère C et retire-le. Le client a bien observé A, B, C sans connaître les piles internes.
Conclusion. Deux représentations peuvent avoir le même état abstrait. Vérifie les observations et les erreurs, pas les noms d’attributs.
Laboratoire de code
Languepython
ButRéaliser une file avec deux piles et la vérifier au moyen d’une interface commune.
Code solution
class FileDeuxPiles:
def __init__(self):
self._entree = []
self._sortie = []
def est_vide(self):
return not self._entree and not self._sortie
def enfiler(self, valeur):
self._entree.append(valeur)
def _preparer_sortie(self):
if not self._sortie:
while self._entree:
self._sortie.append(self._entree.pop())
def tete(self):
self._preparer_sortie()
if self.est_vide():
raise ValueError('file vide')
return self._sortie[-1]
def defiler(self):
valeur = self.tete()
self._sortie.pop()
return valeur
def observer(self):
return tuple(reversed(self._sortie)) + tuple(self._entree)
def verifier_file(fabrique):
f, autre = fabrique(), fabrique()
assert f.est_vide() and f.observer() == ()
for operation in (f.tete, f.defiler):
try:
operation()
except ValueError:
pass
else:
raise AssertionError('ValueError attendue sur le vide')
assert f.est_vide() and f.observer() == ()
for valeur in ('A', 'B', 'C'):
assert f.enfiler(valeur) is None
assert f.tete() == f.tete() == 'A'
assert f.observer() == ('A', 'B', 'C')
assert f.defiler() == 'A'
f.enfiler('D')
assert f.observer() == ('B', 'C', 'D')
assert [f.defiler(), f.defiler(), f.defiler()] == ['B', 'C', 'D']
assert f.est_vide() and autre.est_vide()
for valeur in (None, False, 0):
f.enfiler(valeur)
assert f.tete() is valeur
assert f.defiler() is valeur
return TrueTests
assert verifier_file(FileDeuxPiles)
f = FileDeuxPiles()
f.enfiler('A')
f.enfiler('B')
assert f.defiler() == 'A'
f.enfiler('C')
assert f.tete() == 'B'
assert f.observer() == ('B', 'C')
assert [f.defiler(), f.defiler()] == ['B', 'C']Trace
- Après A puis B : entrée [A, B], sortie vide ; l’ordre logique est A puis B.
- Le premier retrait transfère B puis A, rend A et laisse B dans la sortie.
- C entre alors dans l’entrée ; tant que B attend en sortie, B doit sortir avant C.
Clinique de bogue
Indice observéAprès enfiler A, enfiler B, retirer A puis enfiler C, le retrait suivant rend C au lieu de B.
CauseUn transfert est déclenché alors que la pile de sortie contient encore des éléments plus anciens. Les nouveaux passent devant eux. La condition « seulement si la sortie est vide » préserve FIFO ; le scénario entrelacé révèle un défaut invisible quand on remplit tout avant de vider.
Poursuivre avec l’abonnement
Passer de la lecture à la pratique
Lis les six explications, les quatre schémas et les exemples résolus.
Vérifie tes choix avec six ateliers, dix questions corrigées, douze cartes et un projet Python à adapter.
10 questions · 12 cartes. Ta reprise et tes révisions sont enregistrées dans ce navigateur. Elles ne se synchronisent pas entre appareils.
Accéder à l’entraînement Déjà abonné ? Se connecterVérifier et prolonger
Sources du cours
Édition Maxdecours · Vérifié le .
Spécialité NSI, Terminale générale · programme du BO spécial du 25 juillet 2019
- Programme de l'enseignement de spécialité NSI de terminaleMinistère de l'Éducation nationale · consulté le 2026-09-06
- Types abstraits de données : présentationÉduscol · consulté le 2026-09-06
- Types abstraits de données : implantations et propositions de mise en œuvreÉduscol · consulté le 2026-09-06
- Généralités sur les arbresÉduscol · consulté le 2026-09-06
- Généralités sur les graphesÉduscol · consulté le 2026-09-06
- Représentation des graphesÉduscol · consulté le 2026-09-06
- Data StructuresPython Software Foundation · consulté le 2026-09-06
- Classes : instances, méthodes et attributs partagésPython Software Foundation · consulté le 2026-09-06
- Coûts des opérations sur les types natifs de CPythonPython Software Foundation · consulté le 2026-09-06
© 2026 Maxdecours.com · Comprendre et progresser