Arbres & ABR
Arbres binaires · ABR · Parcours · Récursivité · Complexité
🎯 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
Noeudet 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 Noneen premier
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)
| Terme | Définition | Exemple ci-dessus |
|---|---|---|
| Racine | Nœud sans parent (sommet de l'arbre, point d'entrée unique) | Le nœud A |
| Feuille | Nœud sans enfant (degré sortant = 0) | Nœuds D, G, F |
| Nœud interne | Nœud avec au moins un enfant | Nœuds B, C, E |
| Profondeur d'un nœud | Nombre d'arêtes entre la racine et ce nœud | prof(A)=0, prof(B)=1, prof(G)=3 |
| Hauteur de l'arbre | Longueur du plus long chemin racine → feuille (en arêtes) | 3 (chemin A→B→E→G) |
| Taille | Nombre total de nœuds | 7 nœuds |
| Sous-arbre | Nœud et tous ses descendants | Sous-arbre raciné en B : {B, D, E, G} |
| Arbre binaire | Arbre où chaque nœud a au plus 2 enfants (fils gauche et fils droit) | L'arbre ci-dessus est binaire |
| Arbre vide | Arbre sans aucun nœud — cas de base de la récursivité | Représenté par None en Python |
Propriétés importantes
- Un arbre binaire de hauteur
hcontient au maximum 2h+1 − 1 nœuds - Un arbre binaire de hauteur
hcontient au minimum h + 1 nœuds (chemin dégénéré) - La hauteur minimale d'un arbre de
nnœ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)
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
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
| Parcours | Ordre | Résultat (arbre A) | Implémentation | Application typique |
|---|---|---|---|---|
| Préfixe | N → G → D | A B D E C F G | Récursif | Copie, sérialisation, répertoires |
| Infixe | G → N → D | D B E A F C G | Récursif | Tri sur ABR (ordre croissant) |
| Postfixe | G → D → N | D E B F G C A | Récursif | Évaluation expression, suppression |
| Largeur | Niveau / niveau | A B C D E F G | Itératif + file | Plus 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
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 :
Insertion : 5, 3, 7, 2, 4, 6, 8
5
/ \
3 7
/ \ / \
2 4 6 8
hauteur = 2
recherche : O(log n)
Insertion : 1, 2, 3, 4, 5
1
\
2
\
3
\
4
\
5
hauteur = n-1
recherche : 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
📊 Complexité algorithmique
Parcours
Tous les parcours visitent chaque nœud exactement une fois :
Opérations sur un ABR
| Opération | ABR équilibré | Pire cas (dégénéré) |
|---|---|---|
| Recherche | O(log n) | O(n) |
| Insertion | O(log n) | O(n) |
| Taille | O(n) (toujours — visite tous les nœuds) | |
| Hauteur | O(n) (toujours) | |
| Nb feuilles | O(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
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)
- Dessinez l'arbre (schéma ASCII).
- Quelle est la hauteur de cet arbre ?
- Quelle est la taille (nombre de nœuds) ?
- Listez les feuilles.
- Donnez le résultat du parcours infixe.
- 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é.
On utilise la classe Noeud vue en cours. Écrivez les fonctions Python suivantes :
nb_feuilles(noeud)→ nombre de feuilles.est_present(noeud, valeur)→Truesi la valeur est dans l'arbre (arbre quelconque).prefixe_liste(noeud)→ liste des valeurs dans l'ordre préfixe (sansprint).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
Un compilateur représente l'expression (3 + 5) × (8 − 2) par un arbre binaire :
× / \ + − / \ / \ 3 5 8 2
Partie A — Lecture
- Hauteur et taille de cet arbre ?
- Parcours préfixe (notation polonaise préfixée) ?
- Parcours postfixe (NPI — notation polonaise inversée) ?
Partie B — Code Python
- Construisez cet arbre en Python avec la classe
Noeud. - É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 ✓
É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 Noned'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