DS MP - Arbres binaires de recherche équilibrés (AVL)

20 Mar 2026 20 Mar 2026 190 vues ESSADDOUKI Mostafa 14 min de lecture
Complexité algorithmique
1 Introduction à l'analyse des algorithmes 2 Notations asymptotiques 3 Opérations élémentaires et modèles de coût 4 Complexité temporelle et spatiale 5 Méthode de comptage des pas 6 Méthode de comptage des pas pour les boucles
Diviser pour régner & algorithmes de tri
7 Rappel sur l'approche récursive 8 Diviser pour régner 9 la recherche dichotomique 10 Tri par fusion 11 Tri rapide 12 Analyse des fonctions récursives
Les arbres binaires
13 Introduction aux arbres binaires 14 Définitions récursives des arbres 15 Propriétés des arbres binaires 16 Types d'arbre binaire 17 Parcours en profondeur des arbres binaires 18 Parcours en largeur des arbres binaires 19 Exercices corrigés sur les arbres - TD 1 20 Exercices corrigés sur les arbres - TD 2 21 Exercices corrigés sur les arbres - TD 3 22 DS - Arbres binaires de recherche équilibrés (AVL) 23 DS - Codage de Huffman - Compression de données
Algorithmes gloutons
24 Introduction aux algorithmes gloutons 25 Problème de la sélection d'activités 26 Problème de séquencement des tâches 27 Problème du Sac à Dos fraction
Programmation dynamique
28 Introduction à la programmation dynamique 29 Le concept de sous-structure optimale 30 Le concept de sous-problèmes superposés 31 Méthodes de la programmation dynamique 32 Différence entre la programmation dynamique, l'approche diviser pour régner, et les algorithmes gloutons 33 Calculer les nombres de catalan en C++ et Python 34 Calculer le coefficient binomial 35 Le nombre de façons pour construire un mur de dimension 4*N 36 Défi de conversion de mots 37 Décomposition de phrases à partir d'un dictionnaire 38 La collection de pièces dans un labyrinthe 39 Nombre de façons de regrouper les étudiants 40 Compter tous les chemins possibles dans une grille MxN
Méta heuristique
41 Algorithmes heuristiques et métaheuristiques 42 Algorithme de Recuit simulé 43 Algorithme Colonies de fourmis
Théorie des graphes
44 Introduction et notions fondamentales 45 Chemins, cycles et connexité 46 Représentations des graphes 47 Parcours de graphes 48 Algorithme de Dijkstra 49 Algorithme A* 50 Algorithme de Bellman-Ford 51 Algorithme Floyd-Warshall 52 Coloration des graphes
Base de données et SQL
53 Introduction au langage SQL 54 Concepts de base de SGBDR 55 Syntaxes de différentes instructions SQL 56 Création et suppression d'une base de données 57 Opérateurs SQL 58 Les contraintes en SQL 59 Création et suppression des tables en SQL 60 Insertion et modifications des enregistrements 61 Extraction des données - SELECT 62 Filtrer les données - WHERE 63 Recherche de motifs - LIKE 64 Trier les données - ORDER BY 65 Les jointures en SQL - JOIN 66 Fonctions d'agrégation en SQL - SUM, COUNT, AVG, MIN et MAX 67 Organiser des données identiques en groupes - GROUP BY et HAVING 68 Les sous-requêtes en SQL 69 Les fonctions SQL de manipulation de date 70 DS - langage SQL 71 Exercices corrigés de langage SQL
Introduction à l'intelligence artificielle
Introduction à la théorie des jeux
Préparation aux concours
72 Réseau de distribution d'eau 73 Arbre d'expression arithmétique 74 Exploration d'une grotte souterraine
Arbres binaires Rotations Structures de données Avancé
5 parties
10 questions
Python - CPGE
~90 min

Arbres binaires de recherche équilibrés (AVL)

On souhaite implémenter une structure de données permettant de gérer un dictionnaire de contacts, où chaque contact est identifié par un entier strictement positif (par exemple un identifiant unique).

On veut pouvoir :

  • insérer un nouvel identifiant,
  • rechercher un identifiant,
  • garantir que ces opérations restent efficaces, même après un très grand nombre d'insertions.

Pour cela, on utilise un arbre binaire de recherche vérifiant de plus une condition de bon équilibre précise.

  Représentation de l'arbre

Dans tout le problème, on utilisera la représentation suivante en Python :

  • un arbre vide est représenté par None,
  • un nœud est une liste de la forme :
[a, g, d]

où :

  • a est la valeur entière stockée dans le nœud (l'identifiant),
  • g est le sous-arbre gauche,
  • d est le sous-arbre droit,

eux-mêmes représentés de la même manière (ou None s'ils sont vides).
On suppose que l'arbre vérifie toujours la propriété d'arbre binaire de recherche :

  • 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 à la valeur du nœud.
0
Préliminaire 3 questions
Q1

Écrire une fonction hauteur(a) qui retourne la hauteur de l'arbre a. (La hauteur d'un arbre vide est -1 par convention)

Exemple

Arbre1
Arbre1
Entrée
arbre1 = [20, [12, None, None], [30, [25, None, None], [34, None, None]]]
Sortie
2
Solution — Hauteur
def hauteur(a):
    if a is None:
        return -1
    return 1 + max(hauteur(a[1]), hauteur(a[2]))
Q2

On définit le facteur d'équilibre d'un nœud (arbre non vide) a = [x, g, d] par :
$$\text{equilibre(a)} = \text{hauteur(g)} - \text{hauteur(d)}$$
Écrire une fonction equilibre(a) qui retourne le facteur d'équilibre du nœud a. Si a est vide, on pourra retourner 0 par convention.

Exemple

Entrée
arbre1 = [20, [12, None, None], [30, [25, None, None], [34, None, None]]]
Sortie
-1
Solution — Facteur d'équilibre
def equilibre(a):
    if a is None:
        return 0
    return hauteur(a[1]) - hauteur(a[2])
Q3

On dira qu'un arbre est bien équilibré si, pour tout nœud p de l'arbre, on a : $$-1 \leq \text{equilibre}(p) \leq 1.$$ Écrire une fonction est_bien_equilibre(a) qui retourne True si l'arbre a est bien équilibré, False sinon.

Exemple

Entrée
arbre1 = [20, [12, None, None], [30, [25, None, None], [34, None, None]]]
Sortie
True
Solution — Vérification équilibre
def est_bien_equilibre(a):
    if a is None:
        return True
    
    # Vérifier l'équilibre du nœud courant
    if abs(equilibre(a)) > 1:
        return False
    
    # Vérifier récursivement les sous-arbres
    return est_bien_equilibre(a[1]) and est_bien_equilibre(a[2])
1
Réorganisation locale 2 questions

Après une insertion, il est possible que certains nœuds deviennent trop « penchés » vers la gauche ou vers la droite. On veut alors réorganiser localement l'arbre autour de ce nœud, sans toucher au reste de l'arbre, tout en conservant :

  • la propriété d'arbre binaire de recherche ;
  • autant que possible, une bonne hauteur.

Pour cela, on utilise deux opérations élémentaires : rotation gauche et rotation droite.

Q4

Soit un nœud non vide a = [x, g, d] dont le sous-arbre droit d est lui-même non vide. On note d = [y, b, c].
La rotation gauche autour de a transforme :

  • l'ancienne racine locale a en fils gauche de d,
  • le sous-arbre b (gauche de d) en nouveau sous-arbre droit de a,
  • d devient la nouvelle racine locale.
Arbre1

Écrire une fonction rotation_gauche(a) qui effectue une rotation gauche autour de la racine a (non vide), en supposant que son sous-arbre droit est non vide. La fonction retourne le nouvel arbre.

Exemple

Arbre2
Rotation gauche
Entrée
arbre1 = [20, [12, None, None], [30, [25, None, None], [34, None, None]]]
Sortie
[30, [20, [12, None, None], [25, None, None]], [34, None, None]]
Solution — Rotation gauche
def rotation_gauche(a):
    # a = [x, g, d] avec d non vide
    x, g, d = a
    # d = [y, b, c]
    y, b, c = d
    
    # Nouvel arbre : [y, [x, g, b], c]
    return [y, [x, g, b], c]
Q5

De manière symétrique, soit un nœud non vide a = [x, g, d] dont le sous-arbre gauche g est lui-même non vide. On note g = [y, b, c].
La rotation droite autour de a transforme :

  • l'ancienne racine locale a en fils droit de g,
  • le sous-arbre c (droit de g) en nouveau sous-arbre gauche de a,
  • g devient la nouvelle racine locale.

Écrire une fonction rotation_droite(a) qui effectue une rotation droite autour de la racine a (non vide), en supposant que son sous-arbre gauche est non vide. La fonction retourne le nouvel arbre.

Arbre1

Exemple

Rotation droite
Entrée
[30, [20, [12, None, None], [25, None, None]], [34, None, None]]
Sortie
[20, [12, None, None], [30, [25, None, None], [34, None, None]]]
Solution — Rotation droite
def rotation_droite(a):
    # a = [x, g, d] avec g non vide
    x, g, d = a
    # g = [y, b, c]
    y, b, c = g
    
    # Nouvel arbre : [y, b, [x, c, d]]
    return [y, b, [x, c, d]]
2
Rééquilibrage autour d'un nœud 1 question

On considère maintenant un nœud a (arbre non vide) dont les sous-arbres gauche et droit sont déjà bien équilibrés, mais pour lequel a lui-même peut être déséquilibré.
On admet que, dans les situations qui nous intéressent, le déséquilibre maximal est :

  • equilibre(a) = 2 (trop penché à gauche),
  • ou equilibre(a) = -2 (trop penché à droite).

Dans chaque cas, il est possible de corriger le déséquilibre en utilisant une ou deux rotations au niveau de a et de son fils direct.

Rotation simple

Une rotation simple est utilisée lorsque le déséquilibre est "linéaire", c'est-à-dire que les noeuds sont alignés dans la même direction (tous à gauche ou tous à droite).
Il existe deux types de rotations simples comme vu dans la partie 1 : gauche et droite.

  Rotation droite:

Quand l'utiliser? Lorsque la balance d'un noeud est +2 (sous-arbre gauche trop lourd) et que la balance de son fils gauche est positive ou nulle (+1 ou 0). C'est le cas "Gauche-Gauche". Arbre1

  Rotation gauche :

Quand l'utiliser? Lorsque la balance d'un noeud est -2 (sous-arbre droit trop lourd) et que la balance de son fils droit est négative ou nulle (-1 ou 0). C'est le cas "Droit-Droit". Arbre1
Rotation double

Une rotation double est utilisée lorsque le déséquilibre est "croisé", c'est-à-dire que les noeuds sont alignés en zigzag (un à gauche, l'autre à droite, ou vice versa).
Il existe deux types de rotations doubles : gauche-droite et droite-gauche.

  Rotation gauche-droite :

Quand l'utiliser? Lorsque la balance d'un noeud est +2 (sous-arbre gauche trop lourd) et que la balance de son fils gauche est négative (-1). C'est le cas "Gauche-Droite". Arbre1

Le déséquilibre est à gauche mais intérieur → rotation double.

Étape 1 — Rotation simple gauche sur 10:
Arbre1
Étape 2 — Rotation simple droite sur 20:
Arbre1

  Rotation droite-gauche :

Quand l'utiliser? Lorsque la balance d'un noeud est -2 (sous-arbre droit trop lourd) et que la balance de son fils droit est positive (+1). C'est le cas "Droit-Gauche". Arbre1

Le déséquilibre est à droite mais intérieur → rotation double.

Étape 1 — Rotation simple droite sur 20:
Arbre1
Étape 2 — Rotation simple gauche sur 10:
Arbre1
Q6

Écrire une fonction reequilibrer(a) qui prend en entrée la racine d'un arbre non vide a.

  • On suppose que les sous-arbres gauche et droit de a sont déjà bien équilibrés (leur facteur d'équilibre est dans {-1, 0, 1}), mais que le facteur d'équilibre de a lui-même peut être 2 ou -2.
  • La fonction doit appliquer les rotations (simple ou double) nécessaires pour rétablir l'équilibre (c'est-à-dire ramener le facteur d'équilibre de la nouvelle racine locale dans {-1, 0, 1}) et retourner la nouvelle racine locale de l'arbre équilibré.

Cas à traiter :

  • Rotation droite simple : equilibre(a) = 2 et equilibre(a[1]) >= 0 (cas Gauche-Gauche)
  • Rotation gauche simple : equilibre(a) = -2 et equilibre(a[2]) <= 0 (cas Droit-Droit)
  • Rotation double gauche-droite : equilibre(a) = 2 et equilibre(a[1]) = -1
  • Rotation double droite-gauche : equilibre(a) = -2 et equilibre(a[2]) = 1

Exemple

Arbre1
Rotation double
Entrée
arbre2 = [20, [10, [2, None, None], [15, [12, None, None], [17, None, None]]], [26, None, None]]
Sortie
[15,
 [10, [2, None, None], [12, None, None]],
 [20, [17, None, None], [26, None, None]]]
Solution — Rééquilibrage
def reequilibrer(a):
    if a is None:
        return None
    
    eq = equilibre(a)
    
    # Cas Gauche-Gauche ou Gauche-Droit
    if eq > 1:
        if equilibre(a[1]) >= 0:
            # Cas Gauche-Gauche : rotation droite simple
            return rotation_droite(a)
        else:
            # Cas Gauche-Droit : double rotation
            a[1] = rotation_gauche(a[1])
            return rotation_droite(a)
    
    # Cas Droit-Droit ou Droit-Gauche
    if eq < -1:
        if equilibre(a[2]) <= 0:
            # Cas Droit-Droit : rotation gauche simple
            return rotation_gauche(a)
        else:
            # Cas Droit-Gauche : double rotation
            a[2] = rotation_droite(a[2])
            return rotation_gauche(a)
    
    # Déjà équilibré
    return a
3
Insertion et suppression équilibrée 3 questions

On veut maintenant insérer une nouvelle clé x dans l'arbre, de manière à :

  • respecter la propriété d'arbre binaire de recherche ;
  • corriger localement le déséquilibre éventuel.

On suppose que toutes les clés sont distinctes : si x est déjà présent, l'arbre ne doit pas être modifié.

Q7

Écrire une fonction inserer(a, x) qui, étant donné un arbre binaire de recherche a et une clé x, insère cette clé dans l'arbre tout en maintenant la propriété d'ordre d'un arbre binaire de recherche,

  • puis rééquilibre localement le nœud si cela est nécessaire.
  • La fonction doit retourner le nouvel arbre obtenu après insertion.

Exemple

Arbre1

Après insertion de 2, l'arbre devient déséquilibré :

Arbre1

Après rééquilibrage, l'arbre devient équilibré :

Arbre1
Entrée
arbre = [10, [5, [4, [3, None, None], None], [8, None, None]], [15, [12, None, None], [18, None, None]]]
x = 2
Sortie
[10,
 [5,
  [3,
   [2, None, None],
   [4, None, None]],
  [8, None, None]],
 [15, [12, None, None], [18, None, None]]]
Solution — Insertion équilibrée
def inserer(a, x):
    if a is None:
        return [x, None, None]
    
    val, g, d = a
    
    if x < val:
        g = inserer(g, x)
    elif x > val:
        d = inserer(d, x)
    else:
        # x déjà présent, pas de modification
        return a
    
    # Créer le nouveau nœud avec les sous-arbres modifiés
    nouveau = [val, g, d]
    
    # Rééquilibrer si nécessaire
    return reequilibrer(nouveau)

On se place maintenant dans le cas de la suppression d'une clé dans un arbre. Lorsqu'on supprime une valeur dans un arbre binaire ordonné, trois situations peuvent se présenter :

  • le noeud est une feuille;
  • le noeud possède un seul enfant;
  • le noeud possède deux enfants, auquel cas sa valeur doit être remplacée par celle de son successeur dans le sous-arbre droit (le plus petit élément de ce sous-arbre), puis ce successeur doit être supprimé.

Après cette suppression classique, il se peut que l'arbre perde sa propriété d'équilibre : il faut alors restaurer l'équilibre en appliquant une rotation simple ou double, selon la configuration des sous-arbres.

Q8

Écrire une fonction successeur(a) qui prend en paramètre un sous-arbre a non vide et qui retourne la plus petite valeur contenue dans ce sous-arbre.

Solution — Successeur
def successeur(a):
    if a is None:
        return None
    
    # Aller le plus à gauche possible
    while a[1] is not None:
        a = a[1]
    
    return a[0]
Q9

Écrire une fonction supprimer(a, x) qui :

  • recherche la clé x dans l'arbre a ;
  • la supprime en respectant les trois cas classiques de suppression dans un arbre ordonné ;
  • si nécessaire, rééquilibre localement le nœud courant en appliquant une rotation simple ou double ;
  • retourne l'arbre résultant.

Exemple

Arbre1
Entrée
arbre4 = [10, [5, [4, [3, None, None], None], [8, None, None]], [15, [12, None, None], [18, None, None]]]
x = 5
Sortie
[10,
 [4, [3, None, None], [8, None, None]],
 [15, [12, None, None], [18, None, None]]]
Solution — Suppression équilibrée
def supprimer(a, x):
    if a is None:
        return None
    
    val, g, d = a
    
    if x < val:
        g = supprimer(g, x)
    elif x > val:
        d = supprimer(d, x)
    else:
        # Cas 1 : nœud feuille
        if g is None and d is None:
            return None
        
        # Cas 2 : un seul enfant
        if g is None:
            return d
        if d is None:
            return g
        
        # Cas 3 : deux enfants
        # Remplacer par le successeur (plus petit du sous-arbre droit)
        succ = successeur(d)
        d = supprimer(d, succ)
        a = [succ, g, d]
        return reequilibrer(a)
    
    # Créer le nouveau nœud et rééquilibrer
    nouveau = [val, g, d]
    return reequilibrer(nouveau)
4
Applications 1 question

Une application très utile des arbres équilibrés : récupérer rapidement les valeurs dans un intervalle. On souhaite extraire uniquement les éléments compris entre deux bornes G et D.

Q10

Écrire une fonction récursive elements_intervalle(a, G, D) qui retourne une liste contenant toutes les valeurs v de l'arbre telles que G ≤ v ≤ D, dans l'ordre croissant.

Exploiter la structure équilibrée pour éviter un simple parcours complet.

Exemple

Arbre1
Recherche par intervalle
Entrée
arbre3 = [15, [10, [5, None, None], [12, None, None]], [20, [17, None, None], [26, None, None]]]
G = 14
D = 22
Sortie
[15, 17, 20]
Solution — Éléments dans l'intervalle
def elements_intervalle(a, G, D):
    if a is None:
        return []
    
    val, g, d = a
    resultat = []
    
    # Si val est dans l'intervalle, on pourra l'ajouter
    if G <= val <= D:
        # D'abord les éléments plus petits (gauche)
        resultat.extend(elements_intervalle(g, G, D))
        # Puis la valeur courante
        resultat.append(val)
        # Enfin les éléments plus grands (droite)
        resultat.extend(elements_intervalle(d, G, D))
    elif val < G:
        # Si val < G, tout ce qui est à gauche est aussi < G
        # Donc on cherche seulement à droite
        resultat.extend(elements_intervalle(d, G, D))
    else:  # val > D
        # Si val > D, tout ce qui est à droite est aussi > D
        # Donc on cherche seulement à gauche
        resultat.extend(elements_intervalle(g, G, D))
    
    return resultat

  Optimisation

Cette implémentation exploite la propriété d'ordre de l'arbre pour ne parcourir que les branches susceptibles de contenir des valeurs dans l'intervalle, ce qui est plus efficace qu'un parcours complet dans un grand arbre équilibré.

Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.