Retour à la page d'accueilNSI · Terminale (spé maths) · Changer de matière
Commencer gratuitement
Terminale (spé maths) · NSI
Préparer l'épreuve de spécialité NSI du bac · Épreuve de spécialité NSI du baccalauréat

Structures de données et programmation orientée objet

Quand tu cliques sur « page précédente » dans ton navigateur, comment celui-ci sait-il quelle page afficher ? Et dans une file d'impression au lycée, quel document sort en premier ? Derrière ces situations, il y a des structures de données : des façons d'organiser l'information avec des opérations bien précises. À l'épreuve de spécialité NSI, tu dois les reconnaître, les écrire en Python et les utiliser dans des algorithmes.

Interface, piles et files

Une structure de données se décrit de deux façons.

Deux structures linéaires à connaître :

Structure Règle Ajouter Retirer
Pile LIFO (Last In, First Out) : le dernier arrivé sort le premier empiler(x) depiler()
File FIFO (First In, First Out) : le premier arrivé sort le premier enfiler(x) defiler()

Dans les deux cas, on dispose aussi de est_vide(). Il faut l'appeler avant de retirer un élément : dépiler ou défiler une structure vide provoque une erreur.

Une pile où l'on empile et dépile par le haut, et une file où l'on enfile à droite et défile à gauche. Pile (LIFO)File (FIFO) 472 sommet 583 sortieentrée

En Python, une liste peut servir de pile : append ajoute à la fin, pop() retire la fin. Pour une file, on retire au début avec pop(0).

p = []

p.append(3)      # empiler 3

p.append(8)      # empiler 8

x = p.pop()      # dépiler : x vaut 8, p vaut [3]
Entraîne-toi★☆☆Pas commencé
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()
  1. Donner le contenu de la liste p après chacune des instructions p.append(...) et p.pop().

  2. Donner les valeurs de a, b et c, puis le contenu final de p.

  3. Indiquer si une pile obéit à la règle FIFO ou à la règle LIFO.

  4. Citer une opération dont on a besoin pour savoir si une pile est vide avant d'effectuer un dépilement.

Résoudre
Entraîne-toi★★☆Pas commencé
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()
  1. Donner les valeurs de a et b, puis le contenu de f.contenu à la fin.

  2. Justifier que cette structure respecte la règle FIFO.

  3. Écrire une fonction somme(f) qui renvoie la somme des éléments d'une file f de nombres, en n'utilisant que les méthodes enfiler, defiler et est_vide, et telle que la file retrouve son contenu initial à la fin de l'appel.

  4. Expliquer pourquoi on distingue l'interface d'une file de son implémentation.

Résoudre

Programmation orientée objet

Pour programmer une structure avec son interface, on utilise des classes.

class Cercle:

    def __init__(self, rayon):

        self.rayon = rayon          # attribut

    def diametre(self):             # méthode

        return 2 * self.rayon

c = Cercle(3)        # création : appel de __init__

print(c.rayon)       # 3  (accès à un attribut)

print(c.diametre())  # 6  (appel d'une méthode)

__init__ est le constructeur : il est appelé automatiquement à la création de l'objet et initialise les attributs. On accède aux attributs et aux méthodes avec la notation pointée objet.nom.

Une pile s'écrit très bien avec une classe : l'interface (empiler, depiler, est_vide) est cachée derrière les méthodes, et l'attribut contenu est l'implémentation. On pourrait la remplacer par une autre implémentation sans changer le code qui utilise la pile.

Application classique : l'évaluation d'une expression en notation polonaise inversée (opérateur après les opérandes), qui utilise une pile.

Entraîne-toi★☆☆Pas commencé
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
  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 perimetre(self) qui renvoie le périmètre du rectangle, puis donner sa valeur pour r.

  4. Écrire une instruction qui crée un carré de côté 4 à l'aide de la classe Rectangle, et donner son aire.

Résoudre
Entraîne-toi★★★Pas commencé
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.

  1. Évaluer à la main ["8", "3", "-", "4", "*"] en indiquant le contenu de la pile après chaque élément lu.

  2. Évaluer de même ["5", "1", "2", "+", "4", "*", "+", "3", "-"].

  3. Écrire une fonction evaluer(expr) qui renvoie la valeur d'une expression valide, en n'utilisant que l'interface de Pile.

  4. On remplace l'implémentation de Pile par 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.
Résoudre

Dictionnaires

Un dictionnaire associe à chaque clé une valeur. On accède à la valeur directement par sa clé, sans parcourir tout le contenu, ce qui est bien plus efficace qu'une recherche dans une liste.

d = {"Lucas": 12, "Emma": 17}

d["Emma"] = 18         # modification

d["Jade"] = 15         # ajout d'une nouvelle clé

print(len(d))          # 3

print("Lucas" in d)    # True

for nom in d:          # parcours des clés

    print(nom, d[nom])

for nom, v in d.items():   # parcours des couples (clé, valeur)

    print(nom, v)

Les clés sont uniques : affecter une valeur à une clé existante remplace l'ancienne valeur. Les listes de successeurs d'un graphe, les compteurs d'occurrences, les annuaires se représentent naturellement par des dictionnaires.

Entraîne-toi★☆☆Pas commencé
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
  1. Donner le contenu du dictionnaire notes après exécution de ces instructions.

  2. Donner la valeur de len(notes) et celle de notes["Inès"].

  3. Écrire une boucle qui calcule la moyenne des notes du dictionnaire, puis donner sa valeur.

  4. Indiquer ce que produit l'instruction notes["Zoé"] et proposer une façon de l'éviter.

Résoudre

Arbres binaires et arbres binaires de recherche

Un système de fichiers, un arbre généalogique, un tournoi à élimination directe : ces situations sont hiérarchiques, et un arbre les représente mieux qu'une liste.

Arbre binaire de racine 9, de fils 4 et 12, dont les feuilles sont 2, 6 et 15. 94122615

Mesures à connaître :

  • la taille est le nombre de nœuds (ici 6) ;

  • la hauteur est le nombre de niveaux ; avec la convention du sujet, un arbre réduit à sa racine a pour hauteur 1 (ici 3) ;

  • les feuilles sont 2, 6 et 15.

Pour un arbre de taille $n$ et de hauteur $h$, on a l'encadrement $h \leqslant n \leqslant 2^h - 1$. Chaque niveau contient au plus deux fois plus de nœuds que le précédent, d'où le maximum.

Les fonctions s'écrivent naturellement de façon récursive, l'arbre vide étant représenté par None :

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))

Parcours

Pour l'arbre ci-dessus :

Parcours Ordre Résultat
Préfixe racine, gauche, droit 9 4 2 6 12 15
Infixe gauche, racine, droit 2 4 6 9 12 15
Suffixe gauche, droit, racine 2 6 4 15 12 9
En largeur niveau par niveau (avec une file) 9 4 12 2 6 15

Arbre binaire de recherche

Le parcours infixe d'un ABR donne les clés dans l'ordre croissant (c'est le cas de l'arbre ci-dessus). On recherche une clé en descendant à gauche ou à droite selon la comparaison, et on insère de la même façon, jusqu'à trouver un emplacement vide :

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)

Pour insérer, la logique est la même : si l'arbre est vide, on crée un nœud ; sinon on remplace le sous-arbre concerné par le résultat de l'insertion dans celui-ci.

Entraîne-toi★★☆Pas commencé
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.

  1. Déterminer la taille de l'arbre, sa hauteur et le nombre de ses feuilles.

  2. Donner la liste des clés obtenues par un parcours infixe, puis par un parcours suffixe.

  3. Donner la liste des clés obtenues par un parcours en largeur.

  4. Justifier que cet arbre est un arbre binaire de recherche.

  5. 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.

Résoudre
Entraîne-toi★★★Pas commencé
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
  1. 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.

  2. 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.

  3. 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.

  4. Écrire une fonction récursive hauteur(a) qui renvoie la hauteur d'un arbre (0 pour l'arbre vide).

Résoudre

Graphes

Un réseau de métro, des amis sur un réseau social, des routeurs d'Internet : ce sont des graphes.

Graphe orienté de quatre sommets A, B, C, D : arcs A vers B, A vers C, B vers D, C vers D. ABCD

Représentations

Pour le graphe orienté ci-dessus, avec l'ordre A, B, C, D :

  • listes de successeurs (un dictionnaire) : g = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []} ;

  • matrice d'adjacence : la case de la ligne $i$ et de la colonne $j$ vaut 1 s'il y a un arc de $i$ vers $j$, sinon 0.

$$\begin{pmatrix}0&1&1&0\\0&0&0&1\\0&0&0&1\\0&0&0&0\end{pmatrix}$$

Les prédécesseurs de D sont B et C : on lit la colonne D de la matrice, ou on cherche D dans toutes les listes de successeurs. 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 deux fois).

Parcours

  • En largeur : on visite d'abord les voisins, puis les voisins des voisins, avec une file. Il donne un chemin comportant un nombre minimal d'arêtes.

  • En profondeur : on suit un chemin le plus loin possible avant de revenir en arrière, avec une pile ou la récursivité.

def largeur(g, depart):

    vus = [depart]

    file = [depart]

    while len(file) > 0:

        s = file.pop(0)

        for v in g[s]:

            if v not in vus:

                vus.append(v)

                file.append(v)

    return vus

def profondeur(g, s, vus=None):

    if vus is None:

        vus = []

    vus.append(s)

    for v in g[s]:

        if v not in vus:

            profondeur(g, v, vus)

    return vus

Pour repérer un cycle dans un graphe orienté, on fait un parcours en profondeur : si on rencontre un sommet déjà présent sur le chemin en cours, il y a un cycle.

Entraîne-toi★★☆Pas commencé
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]}
  1. Écrire la matrice d'adjacence de ce graphe, les sommets étant rangés dans l'ordre 0, 1, 2, 3, 4.

  2. Donner la liste des prédécesseurs du sommet 3.

  3. Montrer que ce graphe contient un cycle et le préciser.

  4. 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.

  5. É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.

Résoudre
Entraîne-toi★★★Pas commencé
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"]}
  1. Écrire la matrice d'adjacence de ce graphe, les sommets étant rangés dans l'ordre A, B, C, D, E.

  2. Donner le degré de chaque sommet. Vérifier que la somme des degrés est égale à deux fois le nombre d'arêtes.

  3. 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).

  4. 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`.
Résoudre

Retour au début : le bouton « page précédente » d'un navigateur fonctionne comme une pile (la dernière page visitée est la première retrouvée), et une file d'impression comme une file (premier arrivé, premier servi).

L'essentiel

  • Une structure se décrit par son interface ; l'implémentation (liste, classe) peut changer sans modifier l'interface.

  • Pile : LIFO (empiler, depiler, est_vide). File : FIFO (enfiler, defiler, est_vide).

  • Une classe regroupe des attributs et des méthodes ; __init__ initialise l'objet, self le désigne.

  • Un dictionnaire associe des valeurs à des clés uniques ; l'accès par clé est direct, et une clé absente provoque une KeyError.

  • Arbre binaire : taille, hauteur, feuilles ; $h \leqslant n \leqslant 2^h - 1$. Parcours préfixe, infixe, suffixe et en largeur. Dans un ABR, le parcours infixe est croissant, et recherche et insertion descendent à gauche ou à droite.

  • Graphe : sommets, arcs ou arêtes ; matrice d'adjacence, listes de successeurs et de prédécesseurs ; parcours en largeur (file) et en profondeur (pile ou récursivité).

Voir tous les exercices de cette partie