Introduction aux arbres binaires

27 Apr 2019 27 Apr 2019 13724 vues ESSADDOUKI Mostafa 10 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

Les arbres binaires

Objectifs

Comprendre la structure hiérarchique des arbres binaires, leur terminologie, leurs applications et maîtriser leur implémentation en Python et en C.

Définition — Arbre binaire

Un arbre est un graphe acyclique. Un arbre dans lequel chaque nœud a au plus 2 enfants est appelé un arbre binaire.

Contrairement aux tableaux, listes chaînées, piles et files d'attente, qui sont des structures de données linéaires, les arbres sont des structures de données hiérarchiques.

Structure d'un arbre binaire

Terminologie des arbres

Racine (Root)

Le nœud le plus haut de l'arbre. Dans l'exemple ci-dessus, le nœud A est la racine.

Parent et enfants

Les éléments qui sont directement sous un élément sont appelés ses enfants. L'élément directement au-dessus est appelé son parent.

Exemple : "D" est un enfant de "B" et "B" est le parent de "D".

Feuilles (Leaves)

Les éléments sans enfants sont appelés feuilles.

Exemple : D, E, F, G sont des feuilles.

Sous-arbre gauche / droit

Chaque nœud d'un arbre binaire peut avoir un enfant gauche (sous-arbre gauche) et un enfant droit (sous-arbre droit).

Vocabulaire
  • Niveau (level) : distance depuis la racine (racine = niveau 1)
  • Hauteur (height) : nombre maximum de nœuds sur un chemin racine → feuille
  • Profondeur (depth) : distance d'un nœud à la racine
  • Arbre complet (full binary tree) : chaque nœud a 0 ou 2 enfants
  • Arbre parfait (perfect binary tree) : tous les nœuds internes ont 2 enfants et toutes les feuilles sont au même niveau

Pourquoi utiliser des arbres ?

Avantages des arbres
  • Hiérarchie naturelle : Stocker des informations qui forment naturellement une hiérarchie (système de fichiers, organisation d'entreprise).
  • Accès/recherche optimisés : Les arbres binaires de recherche offrent une complexité \(O(\log n)\) pour la recherche, plus rapide qu'une liste chaînée.
  • Insertion/suppression efficaces : \(O(\log n)\) en moyenne, plus rapide que les tableaux.
  • Aucune limite de taille : Comme les listes chaînées, les nœuds sont liés dynamiquement via des pointeurs.

Principales applications des arbres

Applications

  • Données hiérarchiques : Système de fichiers, organisation d'entreprise, structure XML/JSON.
  • Recherche optimisée : Arbres binaires de recherche (BST), arbres AVL, arbres rouges-noirs.
  • Données triées : Manipulation efficace de listes triées.
  • Algorithmes de routage : Arbres de routage, spanning tree dans les réseaux.
  • Arbres de décision : Intelligence artificielle, apprentissage automatique.
  • Compression de données : Arbres de Huffman.
  • Expression arithmétique : Arbres d'expression pour l'évaluation et la simplification.

Représentation d'un arbre binaire

Un arbre est représenté par un pointeur vers le nœud le plus haut dans l'arbre (la racine). Si l'arbre est vide, la racine vaut NULL (ou None en Python).

Un nœud contient trois parties :

  1. Données : l'information stockée dans le nœud
  2. Pointeur vers l'enfant gauche
  3. Pointeur vers l'enfant droit

Représentation classique (nœuds chaînés)

class Noeud:
    def __init__(self, val):
        self.gauche = None
        self.droit = None
        self.val = val
struct Noeud {
    int val;
    struct Noeud *gauche;
    struct Noeud *droit;
};

Représentation Python avec liste (alternative compacte)

Représentation par liste imbriquée

En Python, on peut représenter un arbre binaire avec une liste de la forme :

[racine, sous_arbre_gauche, sous_arbre_droit]

Un nœud vide est représenté par une liste vide [].

Exemple : arbre de l'illustration
Arbre sous forme de liste
arbre = ['A',
         ['B',
          ['D', [], []],
          ['E', [], []]],
         ['C',
          ['F', [], []],
          ['G', [], []]]]
Structure
        A
       / \
      B   C
     / \ / \
    D  E F  G
Avantages : représentation compacte, facile à créer et à manipuler pour des petits arbres.
Fonctions d'accès Python
def val(arbre):
    """Retourne la valeur de la racine"""
    return arbre[0]

def gauche(arbre):
    """Retourne le sous-arbre gauche"""
    return arbre[1]

def droit(arbre):
    """Retourne le sous-arbre droit"""
    return arbre[2]

def est_vide(arbre):
    """Teste si l'arbre est vide"""
    return arbre == []

Parcours d'un arbre binaire

Il existe trois façons principales de parcourir un arbre binaire :

Parcours préfixe (NLR - Node, Left, Right)
  1. Visiter la racine
  2. Parcourir le sous-arbre gauche
  3. Parcourir le sous-arbre droit

Ordre pour l'exemple : A B D E C F G

Parcours infixe (LNR - Left, Node, Right)
  1. Parcourir le sous-arbre gauche
  2. Visiter la racine
  3. Parcourir le sous-arbre droit

Ordre pour l'exemple : D B E A F C G

Parcours suffixe (LRN - Left, Right, Node)
  1. Parcourir le sous-arbre gauche
  2. Parcourir le sous-arbre droit
  3. Visiter la racine

Ordre pour l'exemple : D E B F G C A

Implémentation des parcours

class Noeud:
    def __init__(self, val):
        self.val = val
        self.gauche = None
        self.droit = None

def parcours_prefixe(racine):
    """NLR : Racine → Gauche → Droit"""
    if racine:
        print(racine.val, end=" ")
        parcours_prefixe(racine.gauche)
        parcours_prefixe(racine.droit)

def parcours_infixe(racine):
    """LNR : Gauche → Racine → Droit"""
    if racine:
        parcours_infixe(racine.gauche)
        print(racine.val, end=" ")
        parcours_infixe(racine.droit)

def parcours_suffixe(racine):
    """LRN : Gauche → Droit → Racine"""
    if racine:
        parcours_suffixe(racine.gauche)
        parcours_suffixe(racine.droit)
        print(racine.val, end=" ")
# Représentation par liste : [valeur, gauche, droit]
def parcours_prefixe_liste(arbre):
    if arbre:
        print(arbre[0], end=" ")
        parcours_prefixe_liste(arbre[1])
        parcours_prefixe_liste(arbre[2])

def parcours_infixe_liste(arbre):
    if arbre:
        parcours_infixe_liste(arbre[1])
        print(arbre[0], end=" ")
        parcours_infixe_liste(arbre[2])

def parcours_suffixe_liste(arbre):
    if arbre:
        parcours_suffixe_liste(arbre[1])
        parcours_suffixe_liste(arbre[2])
        print(arbre[0], end=" ")
#include <stdio.h>
#include <stdlib.h>

struct Noeud {
    int val;
    struct Noeud *gauche;
    struct Noeud *droit;
};

// Parcours préfixe (NLR)
void parcours_prefixe(struct Noeud *racine) {
    if (racine != NULL) {
        printf("%d ", racine->val);
        parcours_prefixe(racine->gauche);
        parcours_prefixe(racine->droit);
    }
}

// Parcours infixe (LNR)
void parcours_infixe(struct Noeud *racine) {
    if (racine != NULL) {
        parcours_infixe(racine->gauche);
        printf("%d ", racine->val);
        parcours_infixe(racine->droit);
    }
}

// Parcours suffixe (LRN)
void parcours_suffixe(struct Noeud *racine) {
    if (racine != NULL) {
        parcours_suffixe(racine->gauche);
        parcours_suffixe(racine->droit);
        printf("%d ", racine->val);
    }
}

Construction d'un arbre binaire

Exemple de construction

Construisons l'arbre suivant :

        A
       / \
      B   C
     / \ / \
    D  E F  G
# Création de l'arbre avec la classe Noeud
racine = Noeud('A')
racine.gauche = Noeud('B')
racine.droit = Noeud('C')
racine.gauche.gauche = Noeud('D')
racine.gauche.droit = Noeud('E')
racine.droit.gauche = Noeud('F')
racine.droit.droit = Noeud('G')

print("Parcours préfixe : ", end="")
parcours_prefixe(racine)
print()
print("Parcours infixe : ", end="")
parcours_infixe(racine)
print()
print("Parcours suffixe : ", end="")
parcours_suffixe(racine)
print()
# Création de l'arbre avec la représentation par liste
arbre = ['A',
         ['B',
          ['D', [], []],
          ['E', [], []]],
         ['C',
          ['F', [], []],
          ['G', [], []]]]

print("Parcours préfixe : ", end="")
parcours_prefixe_liste(arbre)
print()
print("Parcours infixe : ", end="")
parcours_infixe_liste(arbre)
print()
print("Parcours suffixe : ", end="")
parcours_suffixe_liste(arbre)
print()
#include <stdio.h>
#include <stdlib.h>

struct Noeud* creer_noeud(int val) {
    struct Noeud* nouveau = (struct Noeud*)malloc(sizeof(struct Noeud));
    nouveau->val = val;
    nouveau->gauche = NULL;
    nouveau->droit = NULL;
    return nouveau;
}

int main() {
    // Création de l'arbre
    struct Noeud* racine = creer_noeud('A');
    racine->gauche = creer_noeud('B');
    racine->droit = creer_noeud('C');
    racine->gauche->gauche = creer_noeud('D');
    racine->gauche->droit = creer_noeud('E');
    racine->droit->gauche = creer_noeud('F');
    racine->droit->droit = creer_noeud('G');
    
    printf("Parcours préfixe : ");
    parcours_prefixe(racine);
    printf("\n");
    
    printf("Parcours infixe : ");
    parcours_infixe(racine);
    printf("\n");
    
    printf("Parcours suffixe : ");
    parcours_suffixe(racine);
    printf("\n");
    
    return 0;
}
Sortie
Parcours préfixe : A B D E C F G
Parcours infixe : D B E A F C G
Parcours suffixe : D E B F G C A

Comparaison des représentations

Représentation Avantages Inconvénients
Nœuds chaînés
(Python/C)
  • Accès direct aux enfants
  • Modifications faciles
  • Efficace en mémoire (pas de surcoût liste)
  • Plus verbeux à créer
  • Pas de représentation littérale simple
Liste imbriquée
(Python uniquement)
  • Représentation compacte et lisible
  • Création rapide en une expression
  • Idéal pour les petits exemples
  • Moins efficace pour les grandes structures
  • Modifications moins intuitives

L'essentiel en bref

Points clés à retenir
  • Un arbre binaire est une structure hiérarchique où chaque nœud a au plus deux enfants (gauche et droit).
  • Le nœud le plus haut est la racine ; les nœuds sans enfants sont des feuilles.
  • Les arbres sont utilisés pour représenter des données hiérarchiques (système de fichiers, arbres généalogiques, expressions arithmétiques).
  • Les trois parcours principaux sont :
    • Préfixe (NLR) : racine → gauche → droit
    • Infixe (LNR) : gauche → racine → droit
    • Suffixe (LRN) : gauche → droit → racine
  • Deux représentations courantes : nœuds chaînés (C, Python) et liste imbriquée (Python).
Conseils pratiques
  • Toujours vérifier si un nœud est NULL/None avant d'accéder à ses enfants.
  • Privilégier la représentation par nœuds chaînés pour les applications réelles.
  • Utiliser la liste imbriquée pour les tests rapides et les petits exemples.

Un peu d'histoire

Les arbres binaires ont été formalisés dans les années 1960 par Donald Knuth et d'autres pionniers de l'informatique. Ils sont au cœur de nombreux algorithmes fondamentaux et restent une structure incontournable en programmation.

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.