Retour à la page d'accueilNSI · Terminale (maths complémentaires) · Changer de matière
Commencer gratuitement
Terminale (maths complémentaires) · NSI
Structures de données, bases de données et algorithmique · Structures de données

Piles, files et arbres binaires

Quand tu appuies sur le bouton « page précédente » de ton navigateur, la dernière page visitée revient en premier. Quand une imprimante du lycée reçoit vingt travaux, elle les traite dans l'ordre d'arrivée. Ces deux comportements ne sont pas les mêmes : comment les décrire précisément, et comment les programmer ? Dans ce chapitre, tu vas voir deux structures de données linéaires (la pile et la file), puis une structure à plusieurs niveaux : l'arbre binaire.

La pile : dernier arrivé, premier sorti

Imagine une pile d'assiettes. Tu poses une assiette sur le dessus, et tu reprends toujours celle du dessus. On ne touche pas à celles du dessous.

Une structure se décrit par son interface : la liste des opérations autorisées, sans dire comment elles sont codées.

Opération Effet
Pile() crée une pile vide
empiler(x) place x au sommet
depiler() retire le sommet et le renvoie
est_vide() renvoie True si la pile est vide

Une implémentation possible en Python utilise une liste comme attribut : le sommet est la fin de la liste.

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 pourrait écrire une autre implémentation (avec des maillons chaînés, par exemple) : l'interface resterait la même. C'est toute la différence entre interface et implémentation.

Entraîne-toi★☆☆Pas commencé
On considère une pile initialement vide. On effectue successivement les opérations suivantes : empiler(4), empiler(7), empiler(2), depiler(), empiler(9), depiler(). a)…

On considère une pile initialement vide. On effectue successivement les opérations suivantes : empiler(4), empiler(7), empiler(2), depiler(), empiler(9), depiler().

  1. a) Donner le contenu de la pile après chaque opération (le sommet étant indiqué).
  1. b) Donner la valeur renvoyée par chacun des deux appels à depiler().
  1. c) Indiquer le principe (LIFO ou FIFO) suivi par une pile.
Résoudre
Entraîne-toi★★☆Pas commencé
On souhaite vérifier qu'une expression contenant des parenthèses, par exemple ((3+2)(4-1)), est bien parenthésée, à l'aide d'une pile. a) Décrire l'algorithme : que…

On souhaite vérifier qu'une expression contenant des parenthèses, par exemple ((3+2)*(4-1)), est bien parenthésée, à l'aide d'une pile.

  1. a) Décrire l'algorithme : que fait-on à la lecture d'une parenthèse ouvrante, puis d'une parenthèse fermante ? Quelle condition finale garantit que l'expression est bien parenthésée ?
  1. b) Appliquer l'algorithme à ((3+2)*(4-1)) en donnant la taille de la pile après la lecture de chaque parenthèse.
  1. c) Appliquer l'algorithme à (5*(2+1))*3) et indiquer à quel moment l'erreur est détectée.
Résoudre

La file : premier arrivé, premier sorti

À la boulangerie, le premier client arrivé est le premier servi. On entre par la queue, on sort par la tête.

Opération Effet
File() crée une file vide
enfiler(x) ajoute x à la queue
defiler() retire l'élément de tête et le renvoie
est_vide() renvoie True si la file est vide
class File:

    def __init__(self):

        self.contenu = []

    def enfiler(self, x):

        self.contenu.append(x)      # ajout à la queue

    def defiler(self):

        return self.contenu.pop(0)  # retrait de la tête

    def est_vide(self):

        return len(self.contenu) == 0
Une file contenant Lina, Hugo et Jade : Lina est en tête, Jade en queue ; on défile par la tête et on enfile par la queue. Lina Hugo Jade tête queue defiler() enfiler()

Une file avec deux piles

On peut implémenter une file avec deux piles entree et sortie : l'interface de la file reste la même, seule l'implémentation change.

  • enfiler(x) : on empile x dans entree.

  • defiler() : si sortie est vide, on transfère tout entree dans sortie (en dépilant puis empilant : l'ordre est alors inversé), puis on dépile sortie.

Pourquoi ça marche ? Le plus ancien élément est au fond de entree ; après le transfert, il se retrouve au sommet de sortie. Le transfert est coûteux (autant d'opérations que d'éléments), mais chaque élément n'est transféré qu'une seule fois : il est empilé au plus deux fois et dépilé au plus deux fois au total.

Entraîne-toi★☆☆Pas commencé
On considère une file initialement vide. On effectue successivement : enfiler("Anna"), enfiler("Hugo"), enfiler("Léa"), defiler(), enfiler("Paul"), defiler(). a) Donner…

On considère une file initialement vide. On effectue successivement : enfiler("Anna"), enfiler("Hugo"), enfiler("Léa"), defiler(), enfiler("Paul"), defiler().

  1. a) Donner le contenu de la file après chaque opération (en précisant la tête et la queue).
  1. b) Donner les valeurs renvoyées par les deux appels à defiler().
  1. c) Indiquer le principe (LIFO ou FIFO) suivi par une file.
Résoudre
Entraîne-toi★★☆Pas commencé
Dans un service d'urgences de l'hôpital de Lille, les patients sont pris en charge dans l'ordre d'arrivée. On modélise la salle d'attente par une file. Arrivent dans…

Dans un service d'urgences de l'hôpital de Lille, les patients sont pris en charge dans l'ordre d'arrivée. On modélise la salle d'attente par une file. Arrivent dans l'ordre : Martin, Dubois, Petit, Roux. Martin est pris en charge, puis Lemaire arrive, puis deux patients sont pris en charge.

  1. a) Donner l'état de la file à la fin.
  1. b) Indiquer quels patients ont été pris en charge, dans l'ordre.
  1. c) Écrire en Python une classe File avec les méthodes enfiler, defiler et est_vide, en utilisant une liste Python comme attribut.
Résoudre
Entraîne-toi★★★Pas commencé
On implémente une file à l'aide de deux piles entree et sortie. Pour enfiler, on empile dans entree. Pour défiler, si sortie est vide, on transfère tous les éléments de…

On implémente une file à l'aide de deux piles entree et sortie. Pour enfiler, on empile dans entree. Pour défiler, si sortie est vide, on transfère tous les éléments de entree vers sortie (en dépilant puis empilant), puis on dépile sortie.

  1. a) Exécuter la séquence : enfiler 1, 2, 3 ; défiler ; enfiler 4 ; défiler ; défiler. Donner l'état des deux piles après chaque opération.
  1. b) Montrer que l'ordre FIFO est bien respecté.
  1. c) Un défilement peut coûter $n$ opérations élémentaires. Montrer néanmoins que, pour $n$ éléments enfilés puis tous défilés, le coût total est en $O(n)$.
Résoudre

Arbres binaires : vocabulaire

Un dossier contient des sous-dossiers, qui contiennent d'autres sous-dossiers : cette organisation hiérarchique ne se représente pas bien avec une liste. On utilise un arbre.

Arbre binaire de racine S : S a pour fils T et W ; T a pour fils U et V ; W a un seul fils droit X ; X a pour fils gauche Y. STW UVXY
  • La racine est le nœud tout en haut (ici S).

  • Un fils (gauche ou droit) est un nœud directement sous un autre, son père. T et W sont les fils de S.

  • Une feuille est un nœud sans fils : ici U, V et Y.

  • La taille est le nombre de nœuds : ici 7.

  • La hauteur est le nombre de niveaux : ici 4 (chemin S, W, X, Y). Convention : la racine seule a une hauteur de 1, et l'arbre vide a une hauteur de 0.

En Python, un arbre se représente par une classe Noeud ; l'absence de fils est notée None :

class Noeud:

    def __init__(self, valeur):

        self.valeur = valeur

        self.gauche = None

        self.droit = None

def taille(arbre):

    if arbre is None:

        return 0

    return 1 + taille(arbre.gauche) + taille(arbre.droit)

def hauteur(arbre):

    if arbre is None:

        return 0

    return 1 + max(hauteur(arbre.gauche), hauteur(arbre.droit))

La structure de l'arbre est récursive, donc les fonctions qui le parcourent le sont aussi : on traite le cas de l'arbre vide, puis on combine les résultats des deux sous-arbres.

Entraîne-toi★☆☆Pas commencé
On considère l'arbre binaire suivant : la racine est A ; A a pour fils gauche B et pour fils droit C ; B a pour fils gauche D et pour fils droit E ; C a pour unique fils…

On considère l'arbre binaire suivant : la racine est A ; A a pour fils gauche B et pour fils droit C ; B a pour fils gauche D et pour fils droit E ; C a pour unique fils droit F ; D, E et F sont des feuilles.

  1. a) Donner la taille de cet arbre.
  1. b) Donner sa hauteur (en convenant que la racine seule a une hauteur de 1).
  1. c) Donner la liste des feuilles.
Résoudre

Taille et hauteur : un encadrement

Un arbre peut être « maigre » (une chaîne) ou « touffu » (tous les niveaux remplis). Quelle hauteur $h$ peut avoir un arbre de taille $n$ ?

À gauche un arbre en chaîne de 3 nœuds, de hauteur 3 ; à droite un arbre complet de 7 nœuds, de hauteur 3. n = 3, h = 3 n = 7, h = 3

Retiens l'idée : un arbre bien équilibré a une hauteur proportionnelle à $\log_2 n$, un arbre en chaîne a une hauteur proportionnelle à $n$. C'est ce qui rendra la recherche très rapide... ou pas.

Entraîne-toi★★★Pas commencé
Un arbre binaire de hauteur $h$ (la racine seule ayant pour hauteur 1) a une taille $n$. a) Montrer que $h \le n \le 2^h - 1$. b) En déduire un encadrement de $h$ en…

Un arbre binaire de hauteur $h$ (la racine seule ayant pour hauteur 1) a une taille $n$.

  1. a) Montrer que $h \le n \le 2^h - 1$.
  1. b) En déduire un encadrement de $h$ en fonction de $n$ et justifier que $h \ge \lceil \log_2(n+1) \rceil$.
  1. c) Un arbre binaire de recherche de 1 000 nœuds a au minimum quelle hauteur ? Donner la hauteur maximale et expliquer dans quel cas elle est atteinte.
Résoudre

Parcourir un arbre

Pour afficher ou traiter tous les nœuds, il faut choisir un ordre. Prenons cet arbre :

Arbre complet de racine 15 ; fils 9 et 20 ; 9 a pour fils 4 et 12 ; 20 a pour fils 18 et 25. 15920 4121825

Les trois parcours en profondeur sont récursifs. Ils ne diffèrent que par le moment où on traite la racine :

def infixe(arbre):

    if arbre is not None:

        infixe(arbre.gauche)      # 1. sous-arbre gauche

        print(arbre.valeur)       # 2. racine

        infixe(arbre.droit)       # 3. sous-arbre droit
Parcours Ordre Résultat sur l'arbre
Préfixe racine, gauche, droit 15, 9, 4, 12, 20, 18, 25
Infixe gauche, racine, droit 4, 9, 12, 15, 18, 20, 25
Suffixe gauche, droit, racine 4, 12, 9, 18, 25, 20, 15

Le parcours en largeur visite les nœuds niveau par niveau, de gauche à droite : 15, 9, 20, 4, 12, 18, 25. Il utilise une file :

def largeur(arbre):

    f = File()

    f.enfiler(arbre)

    while not f.est_vide():

        n = f.defiler()

        if n is not None:

            print(n.valeur)

            f.enfiler(n.gauche)

            f.enfiler(n.droit)

Le nœud visité est le plus ancien de la file ; ses fils sont ajoutés à la queue, donc ils passent après tous les nœuds du niveau actuel : c'est le FIFO qui impose l'ordre par niveaux.

Entraîne-toi★★☆Pas commencé
Un arbre binaire a pour racine 10. Le fils gauche de 10 est 6, son fils droit est 14. Le fils gauche de 6 est 3 ; le fils droit de 6 est 8. Le fils gauche de 14 est 12 ;…

Un arbre binaire a pour racine 10. Le fils gauche de 10 est 6, son fils droit est 14. Le fils gauche de 6 est 3 ; le fils droit de 6 est 8. Le fils gauche de 14 est 12 ; 14 n'a pas de fils droit.

  1. a) Dessiner (décrire) l'arbre et donner sa taille et sa hauteur (on convient qu'un arbre réduit à sa seule racine a pour hauteur 1).
  1. b) Donner les parcours préfixe, infixe et suffixe.
  1. c) Donner le parcours en largeur.
  1. d) Montrer que cet arbre est un arbre binaire de recherche.
Résoudre

Arbres binaires de recherche

L'arbre précédent a une propriété particulière : à gauche de chaque nœud, il n'y a que des valeurs plus petites, à droite, que des plus grandes.

Rechercher une valeur

On compare la valeur cherchée à la racine : si elle est égale, on a trouvé ; si elle est plus petite, on cherche à gauche ; sinon, à droite. À chaque étape, on élimine un sous-arbre.

def rechercher(arbre, v):

    if arbre is None:

        return False

    if v == arbre.valeur:

        return True

    if v < arbre.valeur:

        return rechercher(arbre.gauche, v)

    return rechercher(arbre.droit, v)

Chercher 12 dans l'arbre précédent : $12 < 15$ → gauche ; $12 > 9$ → droite ; $12 = 12$ : trouvé en visitant 3 nœuds (15, 9 puis 12).

Insérer une valeur

On suit le même chemin que pour la recherche, jusqu'à arriver sur un emplacement vide (None) : c'est là qu'on crée le nouveau nœud, qui sera une feuille.

Le parcours infixe trie

Dans un ABR, le parcours infixe visite le sous-arbre gauche (valeurs plus petites que la racine), puis la racine, puis le sous-arbre droit (plus grandes). Cette propriété vaut à tous les niveaux, donc les valeurs sont affichées dans l'ordre croissant. Vérifie sur l'exemple : 4, 9, 12, 15, 18, 20, 25.

Hauteur et efficacité

Une recherche dans un ABR coûte au plus la hauteur $h$ de l'arbre. Si l'arbre est équilibré, $h$ est de l'ordre de $\log_2 n$ : en 1 000 nœuds, une dizaine de comparaisons : $h \ge \lceil \log_2(1001) \rceil = 10$ car $2^9 = 512 < 1001 \le 2^{10} = 1024$, donc un ABR de 1 000 nœuds a une hauteur minimale de 10 et une hauteur maximale de 1 000 (chaîne). Ici, $\log_2$ sert simplement à compter les niveaux. Mais si on insère des valeurs déjà triées (1, 2, 3, ...), chaque nouvelle valeur devient le fils droit de la précédente : l'arbre est une chaîne, de hauteur $n$, et la recherche coûte $n$ comparaisons.

Reviens au début : une page web précédente se retrouve avec une pile, les travaux d'impression avec une file, et un annuaire trié se parcourt vite avec un ABR bien équilibré.

Entraîne-toi★★★Pas commencé
On représente un arbre binaire de recherche par une classe Noeud possédant les attributs valeur, gauche et droit (ces deux derniers valant None en l'absence de fils). a)…

On représente un arbre binaire de recherche par une classe Noeud possédant les attributs valeur, gauche et droit (ces deux derniers valant None en l'absence de fils).

  1. a) Écrire une fonction récursive inserer(arbre, v) qui insère la valeur v dans l'arbre et renvoie la racine.
  1. b) Dessiner l'arbre obtenu en insérant successivement 8, 3, 10, 1, 6, 14, 4, puis donner son parcours infixe.
  1. c) Justifier que le parcours infixe d'un arbre binaire de recherche fournit les valeurs triées dans l'ordre croissant.
  1. d) Donner la hauteur de l'arbre obtenu si l'on insère 1, 2, 3, 4, 5, 6, 7 dans cet ordre et commenter le coût d'une recherche.
Résoudre

L'essentiel

  • Une pile est LIFO : empiler, depiler, est_vide. Une file est FIFO : enfiler, defiler, est_vide. On décrit une structure par son interface, indépendante de l'implémentation.

  • Un arbre binaire a une racine, des nœuds, des feuilles, des sous-arbres gauche et droit. Sa taille est le nombre de nœuds, sa hauteur le nombre de niveaux (racine seule : 1).

  • Encadrement : $h \le n \le 2^h - 1$, donc $h \ge \lceil \log_2(n+1) \rceil$.

  • Parcours en profondeur : préfixe (racine, gauche, droit), infixe (gauche, racine, droit), suffixe (gauche, droit, racine). Parcours en largeur : niveau par niveau, avec une file.

  • Dans un ABR, la recherche et l'insertion suivent un chemin depuis la racine ; le parcours infixe donne les valeurs triées. Le coût dépend de la hauteur.

Voir tous les exercices de cette partie