🌳

Arbres & ABR

Arbres binaires · ABR · Parcours · Récursivité · Complexité

Récursion Parcours O(log n) ABR

🎯 Introduction

Un arbre est une structure de données hiérarchique composée de nœuds reliés entre eux par des arêtes. C'est un graphe connexe sans cycle, orienté depuis un nœud particulier appelé racine. Les arbres sont omniprésents en informatique : systèmes de fichiers, compilateurs, bases de données, HTML…

✅ Ce qu'il faut maîtriser pour le bac

  • Vocabulaire : racine, feuille, nœud interne, hauteur, profondeur, taille, sous-arbre
  • Représentation Python avec la classe Noeud et la représentation par tuples
  • Les 4 parcours : préfixe, infixe, postfixe (récursifs) et largeur (file)
  • Fonctions récursives : taille, hauteur, nb_feuilles, recherche
  • Propriété ABR : gauche < nœud < droite
  • Recherche et insertion dans un ABR : O(h) → O(log n) si équilibré
  • ABR dégénéré : insertions triées → liste chaînée → O(n)
  • Toujours tester le cas de base if noeud is None en premier
💡
Lien avec les autres notions NSI : la quasi-totalité des algorithmes sur les arbres sont récursifs. Un arbre est aussi un cas particulier de graphe (acyclique connexe). La classe Noeud fait appel à la POO.

📖 Vocabulaire essentiel

Définitions illustrées

Voici les définitions illustrées sur l'arbre suivant :

          A          ← racine (profondeur 0)
         / \
        B   C        ← profondeur 1
       / \   \
      D   E   F    ← profondeur 2
         /
        G           ← feuille (profondeur 3)
TermeDéfinitionExemple ci-dessus
RacineNœud sans parent (sommet de l'arbre, point d'entrée unique)Le nœud A
FeuilleNœud sans enfant (degré sortant = 0)Nœuds D, G, F
Nœud interneNœud avec au moins un enfantNœuds B, C, E
Profondeur d'un nœudNombre d'arêtes entre la racine et ce nœudprof(A)=0, prof(B)=1, prof(G)=3
Hauteur de l'arbreLongueur du plus long chemin racine → feuille (en arêtes)3 (chemin A→B→E→G)
TailleNombre total de nœuds7 nœuds
Sous-arbreNœud et tous ses descendantsSous-arbre raciné en B : {B, D, E, G}
Arbre binaireArbre où chaque nœud a au plus 2 enfants (fils gauche et fils droit)L'arbre ci-dessus est binaire
Arbre videArbre sans aucun nœud — cas de base de la récursivitéReprésenté par None en Python
🚨
Hauteur vs Profondeur : la profondeur d'un nœud est mesurée depuis la racine (vers le bas). La hauteur d'un nœud est le plus long chemin depuis ce nœud vers une feuille. La hauteur de l'arbre = hauteur de la racine = profondeur maximale.

Propriétés importantes

  • Un arbre binaire de hauteur h contient au maximum 2h+1 − 1 nœuds
  • Un arbre binaire de hauteur h contient au minimum h + 1 nœuds (chemin dégénéré)
  • La hauteur minimale d'un arbre de n nœuds est ⌊log₂(n)⌋
  • Un arbre parfait (complet + tous les nœuds internes ont 2 fils) de hauteur h a exactement 2h+1 − 1 nœuds et 2h feuilles

🐍 Représentation en Python

La classe Noeud

En NSI, on représente un arbre binaire avec une classe Noeud :

class Noeud:
    """Représente un nœud d'un arbre binaire."""
    def __init__(self, valeur, gauche=None, droite=None):
        self.valeur = valeur    # donnée stockée dans le nœud
        self.gauche = gauche    # sous-arbre gauche (Noeud ou None)
        self.droite = droite    # sous-arbre droit  (Noeud ou None)

    def est_feuille(self):
        """Retourne True si le nœud est une feuille."""
        return self.gauche is None and self.droite is None
💡
None représente un arbre vide (pas un nœud sans valeur). C'est le cas de base incontournable de toutes les fonctions récursives.

Construction d'un arbre (des feuilles vers la racine)

# On construit toujours de bas en haut (des feuilles vers la racine)
#        8           ← ABR : gauche < racine < droite
#       / \
#      3   10
#     / \    \
#    1   6   14

n1  = Noeud(1)
n6  = Noeud(6)
n14 = Noeud(14)
n3  = Noeud(3, gauche=n1, droite=n6)
n10 = Noeud(10, droite=n14)
racine = Noeud(8, gauche=n3, droite=n10)

# Accès aux valeurs
print(racine.valeur)          # 8
print(racine.gauche.valeur)   # 3
print(n6.est_feuille())       # True
print(n3.est_feuille())       # False

Représentation alternative : tuples imbriqués

Certains sujets de bac utilisent des tuples (valeur, gauche, droite) ou None pour l'arbre vide :

# Même arbre sous forme de tuples (valeur, fils_gauche, fils_droit)
arbre = (8,
          (3, (1, None, None), (6, None, None)),
          (10, None, (14, None, None)))

# Accès : arbre[0]=racine, arbre[1]=gauche, arbre[2]=droite
print(arbre[0])       # 8 (racine)
print(arbre[1][0])   # 3 (fils gauche de la racine)
💡
Les deux représentations apparaissent au bac. La classe Noeud est la plus courante. Sachez passer de l'une à l'autre.

🔄 Parcours d'arbres

Parcourir un arbre signifie visiter tous ses nœuds exactement une fois. L'ordre de visite dépend du parcours choisi.

Pour tous les exemples, on utilise cet arbre de référence :

         A
        / \
       B   C
      / \   / \
     D   E F   G

Parcours en profondeur (DFS) — 3 variantes

Les parcours DFS explorent un sous-arbre entièrement avant de passer au suivant. L'ordre dépend de la position du Nœud (N) par rapport au sous-arbre Gauche (G) et au sous-arbre Droit (D).

1. Parcours préfixe (N → G → D)

Nœud d'abord, puis gauche, puis droite. Résultat : A B D E C F G

def prefixe(noeud):
    if noeud is None: return       # cas de base : arbre vide
    print(noeud.valeur)              # 1. Visiter le nœud
    prefixe(noeud.gauche)            # 2. Sous-arbre gauche
    prefixe(noeud.droite)            # 3. Sous-arbre droit

2. Parcours infixe (G → N → D)

Gauche, puis nœud, puis droite. Résultat : D B E A F C G

Propriété clé : sur un ABR, l'infixe donne les valeurs dans l'ordre croissant !

def infixe(noeud):
    if noeud is None: return
    infixe(noeud.gauche)             # 1. Sous-arbre gauche
    print(noeud.valeur)              # 2. Visiter le nœud
    infixe(noeud.droite)             # 3. Sous-arbre droit

3. Parcours postfixe (G → D → N)

Gauche, droite, puis nœud en dernier. Résultat : D E B F G C A

Application : évaluation d'arbres d'expression arithmétique (NPI), suppression de nœuds.

def postfixe(noeud):
    if noeud is None: return
    postfixe(noeud.gauche)           # 1. Sous-arbre gauche
    postfixe(noeud.droite)           # 2. Sous-arbre droit
    print(noeud.valeur)              # 3. Visiter le nœud
💡
Moyen mnémotechnique : le nom indique la position du Nœud (N) :
Préfixe → N en premier | Infixe → N entre les deux | Postfixe → N en dernier

Parcours en largeur — BFS (Breadth-First Search)

Visite les nœuds niveau par niveau. Ce parcours n'est pas récursif — il utilise une file (deque).

Résultat : A B C D E F G

from collections import deque

def largeur(racine):
    if racine is None: return
    file = deque([racine])           # on enfile la racine
    while file:
        noeud = file.popleft()       # on défile
        print(noeud.valeur)           # on visite
        if noeud.gauche is not None:
            file.append(noeud.gauche)
        if noeud.droite is not None:
            file.append(noeud.droite)

Tableau récapitulatif

ParcoursOrdreRésultat (arbre A)ImplémentationApplication typique
PréfixeN → G → DA B D E C F GRécursifCopie, sérialisation, répertoires
InfixeG → N → DD B E A F C GRécursifTri sur ABR (ordre croissant)
PostfixeG → D → ND E B F G C ARécursifÉvaluation expression, suppression
LargeurNiveau / niveauA B C D E F GItératif + filePlus court chemin, impression

⚙️ Opérations sur les arbres

Taille et hauteur

# ── TAILLE : nombre total de nœuds ──────────────────────────────
def taille(noeud):
    if noeud is None: return 0          # arbre vide → taille 0
    return 1 + taille(noeud.gauche) + taille(noeud.droite)
    # le 1 compte le nœud courant

# ── HAUTEUR : longueur du plus long chemin ──────────────────────
def hauteur(noeud):
    if noeud is None: return -1        # convention : arbre vide → -1
    return 1 + max(hauteur(noeud.gauche), hauteur(noeud.droite))
    # +1 pour l'arête vers ce nœud, max pour le chemin le plus long
🚨
Convention hauteur : certains sujets définissent hauteur(arbre vide) = 0 et hauteur(feuille) = 1. Lisez toujours l'énoncé. La logique reste identique, seule la constante de base change.

Compter les feuilles

def nb_feuilles(noeud):
    if noeud is None: return 0
    if noeud.gauche is None and noeud.droite is None:
        return 1                         # c'est une feuille
    return nb_feuilles(noeud.gauche) + nb_feuilles(noeud.droite)
    # on n'ajoute PAS 1 pour les nœuds internes

Recherche dans un arbre quelconque

def est_present(noeud, valeur):
    if noeud is None: return False
    if noeud.valeur == valeur: return True
    return est_present(noeud.gauche, valeur) or \
           est_present(noeud.droite, valeur)
    # Complexité : O(n) — on explore potentiellement tous les nœuds

🔍 Arbre Binaire de Recherche (ABR)

La propriété ABR

Un ABR vérifie la propriété fondamentale : pour tout nœud,

  • toutes les valeurs du sous-arbre gauche sont strictement inférieures à la valeur du nœud
  • toutes les valeurs du sous-arbre droit sont strictement supérieures
         8            ← ABR : 3 < 8 < 10
        / \
       3   10
      / \    \
     1   6   14

Vérification : 1 < 3 < 6  ✓   10 < 14  ✓   infixe → 1 3 6 8 10 14 (trié) ✓

Recherche dans un ABR — O(h)

La propriété ABR permet d'éliminer la moitié de l'arbre à chaque étape (comme la dichotomie) :

def recherche_abr(noeud, valeur):
    """Recherche dans un ABR — O(log n) si équilibré, O(n) au pire."""
    if noeud is None: return False      # non trouvé
    if valeur == noeud.valeur: return True  # trouvé !
    elif valeur < noeud.valeur:
        return recherche_abr(noeud.gauche, valeur)  # aller à gauche
    else:
        return recherche_abr(noeud.droite, valeur)  # aller à droite

Insertion dans un ABR

def insertion_abr(noeud, valeur):
    """Insère une valeur dans un ABR. Retourne la nouvelle racine."""
    if noeud is None:
        return Noeud(valeur)              # créer un nouveau nœud feuille
    if valeur < noeud.valeur:
        noeud.gauche = insertion_abr(noeud.gauche, valeur)
    elif valeur > noeud.valeur:
        noeud.droite = insertion_abr(noeud.droite, valeur)
    # si valeur == noeud.valeur : doublon ignoré
    return noeud                          # toujours retourner le nœud

# Exemple : construire un ABR par insertions successives
racine = None
for val in [8, 3, 10, 1, 6, 14]:
    racine = insertion_abr(racine, val)

ABR dégénéré : le pire cas

Si on insère des valeurs dans l'ordre croissant, l'ABR dégénère en liste chaînée :

✓ ABR équilibré — O(log n)
Insertion : 5, 3, 7, 2, 4, 6, 8
         5
        / \
       3   7
      / \ / \
     2 4 6 8
hauteur = 2
recherche : O(log n)
✗ ABR dégénéré — O(n)
Insertion : 1, 2, 3, 4, 5
1
 \
  2
   \
    3
     \
      4
       \
        5
hauteur = n-1
recherche : O(n) !
🚨
Piège classique au bac : "La recherche dans un ABR est toujours en O(log n)" est FAUX. C'est O(log n) uniquement si l'arbre est équilibré. Dans le pire cas (insertions triées), c'est O(n).

Vérifier qu'un arbre est un ABR

L'approche naïve (vérifier seulement les fils directs) est incorrecte. Il faut propager des bornes min/max :

def est_abr(noeud, mini=None, maxi=None):
    """Vérifie la propriété ABR en propageant les bornes."""
    if noeud is None: return True
    if mini is not None and noeud.valeur <= mini: return False
    if maxi is not None and noeud.valeur >= maxi: return False
    return (est_abr(noeud.gauche, mini, noeud.valeur) and
            est_abr(noeud.droite, noeud.valeur, maxi))

# En propageant : le fils gauche doit avoir val < noeud.valeur
# ET respecter toutes les bornes des ancêtres
💡
Astuce : le parcours infixe d'un ABR donne les valeurs dans l'ordre croissant. C'est un moyen rapide de vérifier visuellement qu'un arbre est un ABR.

📊 Complexité algorithmique

Parcours

Tous les parcours visitent chaque nœud exactement une fois :

O(n) Préfixe O(n) Infixe O(n) Postfixe O(n) Largeur

Opérations sur un ABR

OpérationABR équilibréPire cas (dégénéré)
RechercheO(log n)O(n)
InsertionO(log n)O(n)
TailleO(n) (toujours — visite tous les nœuds)
HauteurO(n) (toujours)
Nb feuillesO(n) (toujours)
💡

Complexité spatiale

  • Parcours DFS récursif : pile d'appels de profondeur h → O(h) → O(n) au pire
  • Parcours BFS itératif : file pouvant contenir jusqu'à n nœuds → O(n)

⛰️ Tas (Heap)

Un tas binaire est un arbre binaire complet qui respecte la propriété de tas : chaque nœud est ≥ (tas-max) ou ≤ (tas-min) à ses fils. Python implémente un tas-min avec heapq.

import heapq                          # tas-min en Python

tas = []
heapq.heappush(tas, 5)
heapq.heappush(tas, 2)
heapq.heappush(tas, 8)

minimum = heapq.heappop(tas)          # retire et renvoie le minimum : 2

lst = [3, 1, 4, 1, 5]
heapq.heapify(lst)                    # transforme une liste en tas : O(n)
🚨

Pièges classiques au bac

  • Oublier le cas de base if noeud is None: return → RecursionError
  • Confondre hauteur et profondeur — hauteur : depuis le nœud vers les feuilles ; profondeur : depuis la racine vers le nœud
  • ABR dégénéré : insertions triées → liste chaînée → recherche O(n), pas O(log n)
  • Convention hauteur : arbre vide = −1 (NSI) ou 0 selon l'énoncé — toujours vérifier
  • BFS : utiliser une file (deque), jamais la récursion
  • Vérifier un ABR : ne pas tester seulement les fils directs — propager les bornes
  • Infixe sur ABR : donne les valeurs triées — utile pour vérifier visuellement
  • Minimum d'un ABR : toujours à l'extrême gauche (nœud le plus à gauche)

🏋️ Exercices

Facile Lire un arbre construit en Python

On considère l'arbre binaire suivant :

n4 = Noeud(4)
n7 = Noeud(7)
n2 = Noeud(2, n4, n7)
n9 = Noeud(9)
n5 = Noeud(5, n9, None)
r  = Noeud(6, n2, n5)
  1. Dessinez l'arbre (schéma ASCII).
  2. Quelle est la hauteur de cet arbre ?
  3. Quelle est la taille (nombre de nœuds) ?
  4. Listez les feuilles.
  5. Donnez le résultat du parcours infixe.
  6. Cet arbre est-il un ABR ? Justifiez.
✅ Correction

1. Schéma :

       6
      / \
     2   5
    / \  /
   4   7 9

2. Hauteur : chemin le plus long = 6→2→4 (ou 6→2→7, ou 6→5→9) = 2 arêtes → hauteur = 2.

3. Taille : on compte les nœuds : 6, 2, 5, 4, 7, 9 → 6 nœuds.

4. Feuilles : nœuds sans enfant : 4, 7, 9.

5. Parcours infixe (G→N→D) : sous-arbre gauche de 6 → 4, 2, 7 puis racine 6, puis sous-arbre droit → 9, 5. Résultat : 4 → 2 → 7 → 6 → 9 → 5.

6. ABR : Non. Le parcours infixe (4, 2, 7, 6, 9, 5) n'est pas trié → ce n'est pas un ABR. De plus, le fils droit de 2 vaut 7 > racine 6, ce qui viole la propriété.

Intermédiaire Écriture de fonctions récursives

On utilise la classe Noeud vue en cours. Écrivez les fonctions Python suivantes :

  1. nb_feuilles(noeud) → nombre de feuilles.
  2. est_present(noeud, valeur) → True si la valeur est dans l'arbre (arbre quelconque).
  3. prefixe_liste(noeud) → liste des valeurs dans l'ordre préfixe (sans print).
  4. minimum_abr(noeud) → valeur minimale d'un ABR (sans parcourir tout l'arbre).
✅ Correction
# 1. Nombre de feuilles
def nb_feuilles(noeud):
    if noeud is None: return 0
    if noeud.gauche is None and noeud.droite is None: return 1
    return nb_feuilles(noeud.gauche) + nb_feuilles(noeud.droite)

# 2. Recherche (arbre quelconque)
def est_present(noeud, valeur):
    if noeud is None: return False
    if noeud.valeur == valeur: return True
    return est_present(noeud.gauche, valeur) or \
           est_present(noeud.droite, valeur)

# 3. Parcours préfixe retournant une liste
def prefixe_liste(noeud):
    if noeud is None: return []
    return ([noeud.valeur]
            + prefixe_liste(noeud.gauche)
            + prefixe_liste(noeud.droite))

# 4. Minimum d'un ABR (tout à gauche)
def minimum_abr(noeud):
    if noeud.gauche is None: return noeud.valeur  # nœud le plus à gauche
    return minimum_abr(noeud.gauche)               # descendre à gauche
Niveau bac Arbre d'expression arithmétique

Un compilateur représente l'expression (3 + 5) × (8 − 2) par un arbre binaire :

          ×
         / \
        +   −
       / \ / \
      3  5 8  2

Partie A — Lecture

  1. Hauteur et taille de cet arbre ?
  2. Parcours préfixe (notation polonaise préfixée) ?
  3. Parcours postfixe (NPI — notation polonaise inversée) ?

Partie B — Code Python

  1. Construisez cet arbre en Python avec la classe Noeud.
  2. Écrivez une fonction récursive evaluer(noeud) qui retourne 48.
✅ Correction

1. Hauteur = 2 (chemin ×→+→3). Taille = 7 nœuds.

2. Préfixe (N→G→D) : × + 3 5 − 8 2

3. Postfixe (G→D→N) : 3 5 + 8 2 − × (NPI — utilisé par les calculatrices HP)

# 4. Construction de l'arbre
f3 = Noeud(3); f5 = Noeud(5); f8 = Noeud(8); f2 = Noeud(2)
plus   = Noeud('+', f3, f5)
moins  = Noeud('-', f8, f2)
racine = Noeud('*', plus, moins)

# 5. Évaluation récursive
def evaluer(noeud):
    if noeud is None: return 0
    if noeud.gauche is None and noeud.droite is None:
        return noeud.valeur               # feuille → valeur numérique
    g = evaluer(noeud.gauche)
    d = evaluer(noeud.droite)
    if   noeud.valeur == '+': return g + d
    elif noeud.valeur == '-': return g - d
    elif noeud.valeur == '*': return g * d
    elif noeud.valeur == '/': return g / d

print(evaluer(racine))  # (3+5) × (8-2) = 8 × 6 = 48 ✓
Niveau bac Vérifier qu'un arbre est un ABR

Écrire une fonction est_abr(noeud, mini=None, maxi=None) qui vérifie qu'un arbre binaire respecte la propriété ABR en propageant des bornes.

✅ Correction
def est_abr(noeud, mini=None, maxi=None):
    if noeud is None: return True
    if mini is not None and noeud.valeur <= mini: return False
    if maxi is not None and noeud.valeur >= maxi: return False
    return (est_abr(noeud.gauche, mini, noeud.valeur) and
            est_abr(noeud.droite, noeud.valeur, maxi))

En descendant à gauche : on passe maxi=noeud.valeur (tout ce qui suit doit être < à cette valeur). En descendant à droite : on passe mini=noeud.valeur. Les bornes s'accumulent à chaque niveau.

📌 Fiche synthèse

VOCABULAIRE

  • Racine : nœud sans parent
  • Feuille : nœud sans enfant
  • Hauteur : max chemin → feuille
  • Profondeur : distance à la racine
  • None = arbre vide

PARCOURS

  • Préfixe : N → G → D
  • Infixe : G → N → D
  • Postfixe : G → D → N
  • Largeur : file deque
  • Infixe ABR → trié ✓

RÉCURSION

  • if noeud is None d'abord !
  • taille = 1 + t(g) + t(d)
  • hauteur = 1 + max(h(g), h(d))
  • hauteur(None) = −1
  • taille(None) = 0

ABR

  • gauche < racine < droite
  • Recherche / insertion : O(h)
  • Équilibré : h = O(log n)
  • Dégénéré : h = O(n) !
  • Minimum : tout à gauche

FORMULES

  • Arbre parfait h : 2^(h+1)−1 nœuds
  • h min de n nœuds : ⌊log₂(n)⌋
  • BFS, DFS : O(n)
  • DFS pile : O(h)
  • heapq → tas-min Python

PIÈGES

  • Oublier is None → RecursionError
  • ABR ≠ toujours O(log n)
  • Hauteur ≠ Profondeur
  • BFS : file, PAS récursion
  • Vérif ABR : propager bornes

🧠 QCM

50 questions · 3 niveaux · Tirage aléatoire

← Structures de données Graphes →