Structures de données et programmation orientée objet
Quand tu cliques sur « Retour » dans ton navigateur, tu reviens à la page précédente, puis à celle d'avant, et ainsi de suite. Quand tu envoies trois documents à l'imprimante du lycée, le premier envoyé est le premier imprimé. Ces deux comportements ne se programment pas de la même façon : ce sont deux structures de données différentes. Pourquoi le « Retour » suit-il la règle inverse de l'imprimante ? Tu auras la réponse dans la première section, puis tu verras comment organiser des objets, des associations, des hiérarchies et des réseaux.
Dans l'épreuve de spécialité NSI, ces notions servent presque toujours de support aux exercices. On te demande de les utiliser, de les implémenter en Python et de raisonner sur leur fonctionnement.
Piles et files : interface et implémentation
Une structure de données organise des données et fixe les opérations permises. On distingue deux niveaux :
C'est la réponse à notre gancho : le bouton « Retour » dépile la dernière page visitée (pile), alors que l'imprimante traite les documents dans l'ordre d'arrivée (file).
En Python, on implémente souvent une pile avec une liste dont le sommet est la fin : append pour empiler, pop() pour dépiler. Pour une file, on retire au début avec pop(0).
class Pile:
def __init__(self):
self.contenu = []
def empiler(self, x):
self.contenu.append(x)
def depiler(self):
return self.contenu.pop()
def est_vide(self):
return len(self.contenu) == 0
On utilise une liste Python p comme une pile, le sommet de la pile étant la fin de la liste. On exécute les instructions suivantes. 1. Donner le contenu de la liste p…
On utilise une liste Python p comme une pile, le sommet de la pile étant la fin de la liste. On exécute les instructions suivantes.
p = []
p.append(4)
p.append(7)
a = p.pop()
p.append(2)
p.append(9)
b = p.pop()
c = p.pop()
-
Donner le contenu de la liste
paprès chacune des instructionsp.append(...)etp.pop(). -
Donner les valeurs de
a,betc, puis le contenu final dep. -
Indiquer si une pile obéit à la règle FIFO ou à la règle LIFO.
-
Citer une opération dont on a besoin pour savoir si une pile est vide avant d'effectuer un dépilement.
On implémente une file à l'aide de la classe suivante. On exécute les instructions suivantes. 1. Donner les valeurs de a et b, puis le contenu de f.contenu à la fin. 2.…
On implémente une file à l'aide de la classe suivante.
class File:
def __init__(self):
self.contenu = []
def enfiler(self, x):
self.contenu.append(x)
def defiler(self):
return self.contenu.pop(0)
def est_vide(self):
return len(self.contenu) == 0
On exécute les instructions suivantes.
f = File()
f.enfiler(5)
f.enfiler(8)
f.enfiler(3)
a = f.defiler()
f.enfiler(1)
b = f.defiler()
-
Donner les valeurs de
aetb, puis le contenu def.contenuà la fin. -
Justifier que cette structure respecte la règle FIFO.
-
Écrire une fonction
somme(f)qui renvoie la somme des éléments d'une filefde nombres, en n'utilisant que les méthodesenfiler,defileretest_vide, et telle que la file retrouve son contenu initial à la fin de l'appel. -
Expliquer pourquoi on distingue l'interface d'une file de son implémentation.
On dispose d'une structure de pile dont l'interface est la suivante : Pile() crée une pile vide, empiler(x), depiler() (qui renvoie le sommet et le retire) et…
On dispose d'une structure de pile dont l'interface est la suivante : Pile() crée une pile vide, empiler(x), depiler() (qui renvoie le sommet et le retire) et est_vide().
class Pile:
def __init__(self):
self.contenu = []
def empiler(self, x):
self.contenu.append(x)
def depiler(self):
return self.contenu.pop()
def est_vide(self):
return len(self.contenu) == 0
On évalue une expression en notation polonaise inversée, donnée sous forme de liste de chaînes. Un nombre est empilé ; un opérateur parmi "+", "-", "*" dépile deux valeurs b puis a (dans cet ordre) et empile le résultat de a opérateur b.
-
Évaluer à la main
["8", "3", "-", "4", "*"]en indiquant le contenu de la pile après chaque élément lu. -
Évaluer de même
["5", "1", "2", "+", "4", "*", "+", "3", "-"]. -
Écrire une fonction
evaluer(expr)qui renvoie la valeur d'une expression valide, en n'utilisant que l'interface dePile. -
On remplace l'implémentation de
Pilepar la suivante, fondée sur des cellules chaînées.
class Cellule:
def __init__(self, valeur, suivante):
self.valeur = valeur
self.suivante = suivante
class Pile:
def __init__(self):
self.sommet = None
...
a) Écrire les méthodes `empiler`, `depiler` et `est_vide` de cette nouvelle implémentation.
b) Justifier que la fonction `evaluer` n'a pas besoin d'être modifiée.
Classes, attributs et méthodes
La programmation orientée objet regroupe dans un même objet des données (les attributs) et les fonctions qui les manipulent (les méthodes).
class Cercle:
def __init__(self, rayon):
self.rayon = rayon # attribut
def diametre(self): # méthode
return 2 * self.rayon
c = Cercle(4) # création d'un objet
print(c.rayon) # accès à un attribut : 4
print(c.diametre()) # appel d'une méthode : 8
Le programme de terminale ne demande ni héritage ni polymorphisme : on écrit une classe, on crée des objets, on accède aux attributs et aux méthodes.
Une classe est aussi la manière naturelle d'implémenter une structure : la classe Pile ci-dessus a un attribut contenu et des méthodes empiler, depiler, est_vide. Si on remplaçait la liste par une autre représentation, l'interface resterait la même : c'est exactement la distinction entre interface et implémentation.
On considère la classe suivante. 1. Indiquer les attributs de la classe Rectangle. 2. On exécute r = Rectangle(5, 3). Donner la valeur de r.aire(). 3. Écrire une méthode…
On considère la classe suivante.
class Rectangle:
def __init__(self, largeur, longueur):
self.largeur = largeur
self.longueur = longueur
def aire(self):
return self.largeur * self.longueur
-
Indiquer les attributs de la classe
Rectangle. -
On exécute
r = Rectangle(5, 3). Donner la valeur der.aire(). -
Écrire une méthode
perimetre(self)qui renvoie le périmètre du rectangle, puis donner sa valeur pourr. -
Écrire une instruction qui crée un carré de côté 4 à l'aide de la classe
Rectangle, et donner son aire.
Dictionnaires
Une liste se repère par des indices (0, 1, 2…). Un dictionnaire associe à une clé une valeur : on retrouve la valeur grâce à la clé.
age = {"Emma": 17, "Lucas": 18}
age["Emma"] # 17
age["Jade"] = 16 # ajout d'une association
age["Lucas"] = 19 # modification
len(age) # 3
"Emma" in age # True
for nom in age: # parcours des clés
print(nom, age[nom])
for nom, a in age.items(): # parcours des couples (clé, valeur)
print(nom, a)
Les clés sont uniques : affecter une valeur à une clé existante remplace l'ancienne. La recherche d'une clé dans un dictionnaire est bien plus rapide que la recherche d'un élément dans une longue liste, car on n'a pas à parcourir tous les éléments.
Un professeur enregistre les notes de quatre élèves dans un dictionnaire Python. 1. Donner le contenu du dictionnaire notes après exécution de ces instructions. 2.…
Un professeur enregistre les notes de quatre élèves dans un dictionnaire Python.
notes = {"Léa": 14, "Hugo": 11, "Inès": 16}
notes["Hugo"] = 13
notes["Tom"] = 9
-
Donner le contenu du dictionnaire
notesaprès exécution de ces instructions. -
Donner la valeur de
len(notes)et celle denotes["Inès"]. -
Écrire une boucle qui calcule la moyenne des notes du dictionnaire, puis donner sa valeur.
-
Indiquer ce que produit l'instruction
notes["Zoé"]et proposer une façon de l'éviter.
Arbres binaires et arbres binaires de recherche
Un arbre représente une hiérarchie (dossiers d'un ordinateur, tournoi, arbre généalogique). Dans un arbre binaire, chaque nœud a au plus deux fils : un fils gauche et un fils droit.
Pour l'arbre de la figure, la taille est 6, la hauteur est 3 et il y a 3 feuilles (1, 4 et 12).
Une pile de $h$ nœuds en file indienne donne $n = h$ ; un arbre « plein » donne $n = 2^h - 1$ (par exemple 7 nœuds pour $h = 3$).
Parcours d'un arbre
-
Parcours en profondeur : on descend le plus loin possible avant de remonter. Selon le moment où on traite le nœud, on distingue trois ordres :
-
préfixe : nœud, puis sous-arbre gauche, puis sous-arbre droit ;
-
infixe : sous-arbre gauche, puis nœud, puis sous-arbre droit ;
-
suffixe : sous-arbre gauche, puis sous-arbre droit, puis nœud.
-
Parcours en largeur : niveau par niveau, de gauche à droite (on utilise une file).
Pour l'arbre de la figure : préfixe 5, 2, 1, 4, 9, 12 ; infixe 1, 2, 4, 5, 9, 12 ; suffixe 1, 4, 2, 12, 9, 5 ; en largeur 5, 2, 9, 1, 4, 12.
En Python, on représente un nœud par un objet, et l'arbre vide par None. Les fonctions sont naturellement récursives.
class Noeud:
def __init__(self, cle):
self.cle = cle
self.gauche = None
self.droit = None
def taille(a):
if a is None:
return 0
return 1 + taille(a.gauche) + taille(a.droit)
def hauteur(a):
if a is None:
return 0
return 1 + max(hauteur(a.gauche), hauteur(a.droit))
def infixe(a):
if a is not None:
infixe(a.gauche)
print(a.cle)
infixe(a.droit)
Arbre binaire de recherche (ABR)
Le parcours infixe d'un ABR donne les clés dans l'ordre croissant. L'arbre de la figure en est un. Pour chercher une clé, on compare à la racine puis on va à gauche ou à droite : on élimine un sous-arbre à chaque étape. Si l'arbre est équilibré, la recherche coûte de l'ordre de $\log_2 n$ comparaisons, contre $n$ pour un arbre « en peigne ».
def rechercher(a, cle):
if a is None:
return False
if cle == a.cle:
return True
if cle < a.cle:
return rechercher(a.gauche, cle)
return rechercher(a.droit, cle)
L'insertion suit le même chemin et crée la nouvelle feuille à la place du None rencontré.
On considère l'arbre binaire dont les nœuds portent des entiers, décrit ainsi. La racine porte la clé 8. Son fils gauche porte 3 et son fils droit porte 10. Le nœud 3 a…
On considère l'arbre binaire dont les nœuds portent des entiers, décrit ainsi.
-
La racine porte la clé 8.
-
Son fils gauche porte 3 et son fils droit porte 10.
-
Le nœud 3 a pour fils gauche 1 et pour fils droit 6.
-
Le nœud 6 a pour fils gauche 4 et pour fils droit 7.
-
Le nœud 10 n'a pas de fils gauche et a pour fils droit 14.
-
Le nœud 14 a pour fils gauche 13 et n'a pas de fils droit.
-
Les nœuds 1, 4, 7 et 13 n'ont aucun fils.
On convient que la hauteur d'un arbre réduit à sa racine est égale à 1.
-
Déterminer la taille de l'arbre, sa hauteur et le nombre de ses feuilles.
-
Donner la liste des clés obtenues par un parcours infixe, puis par un parcours suffixe.
-
Donner la liste des clés obtenues par un parcours en largeur.
-
Justifier que cet arbre est un arbre binaire de recherche.
-
Pour un arbre binaire de taille $n = 9$, donner un encadrement de la hauteur $h$ de la forme $a \leqslant h \leqslant b$, puis vérifier que l'arbre étudié le respecte.
On représente un arbre binaire de recherche par des objets de la classe Noeud. L'arbre vide est représenté par None. 1. Compléter la fonction inserer, qui insère une clé…
On représente un arbre binaire de recherche par des objets de la classe Noeud. L'arbre vide est représenté par None.
class Noeud:
def __init__(self, cle):
self.cle = cle
self.gauche = None
self.droit = None
def inserer(a, cle):
if a is None:
return ...
if cle < a.cle:
a.gauche = ...
elif cle > a.cle:
a.droit = ...
return a
-
Compléter la fonction
inserer, qui insère une clé dans un arbre binaire de recherche et renvoie la racine. Une clé déjà présente n'est pas insérée de nouveau. -
On insère successivement, dans un arbre initialement vide, les clés 12, 5, 18, 3, 9, 15, 20.
a) Décrire l'arbre obtenu (fils gauche et fils droit de chaque nœud) et donner sa hauteur, avec la convention qu'un arbre réduit à sa racine a pour hauteur 1.
b) On insère ensuite 7, puis 10. Donner la nouvelle hauteur et la liste des clés obtenue par un parcours infixe.
-
On insère les mêmes sept clés dans l'ordre 3, 5, 9, 12, 15, 18, 20. Donner la hauteur de l'arbre obtenu et expliquer l'effet de l'ordre d'insertion sur le coût d'une recherche.
-
Écrire une fonction récursive
hauteur(a)qui renvoie la hauteur d'un arbre (0 pour l'arbre vide).
Graphes
Un graphe modélise des liens entre objets : métro, réseau social, pages web reliées par des liens.
Deux représentations
Pour le graphe de la figure, avec les sommets rangés dans l'ordre 0, 1, 2, 3 :
-
Listes de successeurs (dictionnaire) :
g = {0: [1], 1: [2], 2: [0, 3], 3: []}. -
Matrice d'adjacence : on met 1 à la ligne $i$, colonne $j$, s'il y a un arc de $i$ vers $j$, sinon 0.
Les prédécesseurs d'un sommet se lisent dans sa colonne (ou en cherchant dans quelles listes il apparaît) : ceux de 2 sont [1]. Pour un graphe non orienté, la matrice est symétrique, et la somme des degrés vaut deux fois le nombre d'arêtes (chaque arête compte pour deux extrémités).
def matrice(g, n):
m = [[0] * n for _ in range(n)]
for s in g:
for t in g[s]:
m[s][t] = 1
return m
Parcours d'un graphe
Pour ne pas tourner indéfiniment à cause des cycles, on mémorise les sommets déjà vus.
-
Parcours en largeur : on visite d'abord les voisins du départ, puis leurs voisins, etc. On utilise une file. Il donne un chemin comportant un nombre minimal d'arêtes (ou d'arcs).
-
Parcours en profondeur : on suit un chemin le plus loin possible, puis on revient en arrière. On utilise la récursivité (ou une pile).
def largeur(g, depart):
vus = [depart]
file = [depart]
while len(file) > 0:
s = file.pop(0)
for t in g[s]:
if t not in vus:
vus.append(t)
file.append(t)
return vus
def profondeur(g, s, vus=None):
if vus is None:
vus = []
vus.append(s)
for t in g[s]:
if t not in vus:
profondeur(g, t, vus)
return vus
On considère le graphe orienté à cinq sommets numérotés de 0 à 4, représenté par ses listes de successeurs. 1. Écrire la matrice d'adjacence de ce graphe, les sommets…
On considère le graphe orienté à cinq sommets numérotés de 0 à 4, représenté par ses listes de successeurs.
g = {0: [1, 2], 1: [3], 2: [3], 3: [4], 4: [1]}
-
Écrire la matrice d'adjacence de ce graphe, les sommets étant rangés dans l'ordre 0, 1, 2, 3, 4.
-
Donner la liste des prédécesseurs du sommet 3.
-
Montrer que ce graphe contient un cycle et le préciser.
-
Donner l'ordre de visite des sommets lors d'un parcours en largeur depuis le sommet 0, puis lors d'un parcours en profondeur depuis le sommet 0. On visite les successeurs d'un sommet par ordre croissant.
-
Écrire une fonction
vers_matrice(g, n)qui prend en paramètres un dictionnaire de listes de successeurs et le nombre de sommets, et renvoie la matrice d'adjacence sous forme de liste de listes.
Un graphe non orienté est représenté par le dictionnaire suivant. 1. Écrire la matrice d'adjacence de ce graphe, les sommets étant rangés dans l'ordre A, B, C, D, E. 2.…
Un graphe non orienté est représenté par le dictionnaire suivant.
g = {"A": ["B", "C"],
"B": ["A", "D"],
"C": ["A", "D"],
"D": ["B", "C", "E"],
"E": ["D"]}
-
Écrire la matrice d'adjacence de ce graphe, les sommets étant rangés dans l'ordre A, B, C, D, E.
-
Donner le degré de chaque sommet. Vérifier que la somme des degrés est égale à deux fois le nombre d'arêtes.
-
Donner un chemin de A à E comportant un nombre minimal d'arêtes, ainsi que l'ordre de visite d'un parcours en largeur depuis A (voisins pris dans l'ordre de la liste).
-
On considère la fonction suivante.
def a_cycle(g, s, parent, vus):
vus.add(s)
for v in g[s]:
if v not in vus:
if a_cycle(g, v, s, vus):
return True
elif v != parent:
return True
return False
a) Dérouler l'appel `a_cycle(g, "A", None, set())` et donner la valeur renvoyée.
b) On supprime l'arête entre C et D. Donner la nouvelle valeur renvoyée par l'appel précédent.
c) Expliquer le rôle du paramètre `parent`.
L'essentiel
-
Interface : les opérations offertes ; implémentation : leur programmation concrète. Une pile est LIFO (
empiler,depiler), une file est FIFO (enfiler,defiler). -
Une classe regroupe des attributs et des méthodes ;
__init__initialise l'objet etselfle désigne. -
Un dictionnaire associe une clé unique à une valeur :
d[cle],cle in d,len(d). -
Un arbre binaire a une racine, des feuilles et des sous-arbres ; sa hauteur $h$ et sa taille $n$ vérifient $h \le n \le 2^h - 1$. Parcours préfixe, infixe, suffixe et en largeur. Dans un ABR, le parcours infixe est croissant.
-
Un graphe se représente par une matrice d'adjacence ou par des listes de successeurs. Parcours en largeur (file) et en profondeur (récursivité ou pile) avec une liste de sommets vus.
-
Au bac : lis toujours l'interface donnée, suis les valeurs pas à pas dans un tableau, et justifie par écrit.
Retour au début : le bouton « Retour » fonctionne comme une pile (LIFO), l'imprimante comme une file (FIFO).