Arbre binaire de recherche : définition et mise en oeuvre en C++

21 Nov 2021 21 Nov 2021 10483 vues ESSADDOUKI Mostafa 7 min de lecture
Introduction et syntaxe de base
1 Introduction au langage C++ 2 Entrée-sortie en C++ - cin et cout 3 Inférence de type avec le mot-clé auto en C++ 4 Classe std::string et les chaînes de caractères en C++ 5 Les structures conditionnelles (if et switch) en C++ (C++17 et C++20) 6 Les boucles en C++ (C++17 et C++20) 7 La gestion des fichiers en C++
Pointeurs et fonctions
8 Introduction aux pointeurs en C++ - Déclaration et interêts 9 Les références en C++ - déclaration et interêts 10 Les tableaux en C++ - Déclaration et interêts 11 Introduction aux fonctions en C++ 12 Passer des arguments à une fonction en C++ 13 Déclarer un paramètre const en C++ 14 Les fonctions Lambda en C++ 15 Fonctions utiles (Mathématiques et caractères) en C++
Programmation OO
16 Classes et objets en C++ 17 Spécificateurs d'accès en C++ 18 Constructeurs et destructeur d'une classe en C++ 19 Fonctions membres en C++ 20 Membres statiques d'une classe en C++ 21 Fonctions en ligne en C++ - inline 22 Fonctions et classes amies en C++ - friend 23 Surcharge des fonctions en C++ 24 Surcharge des opérateurs en C++ 25 Héritage en C++ 26 La gestion d'exceptions en C++ : déclaration, utilisation et personnalisation 27 fonctions et classes templates en C++ 28 Les nouveautés C++20 pour améliorer les templates en C++
Structures de données
29 Introduction aux structures de données 30 Les structures en C++ et la différence avec les structures en C 31 Les listes chaînées en C++ 32 Les piles en C++ 33 File d'attente en C++ 34 Arbre binaire de recherche : définition et mise en oeuvre en C++
La bibliothèque standard (STL)
35 Introduction à la bibliothèque de Template Standard STL 36 Les itérateurs en C++ - définition, déclaration et exemples 37 La classe array en C++ (bibliothèque STL) <array> 38 La classe vector de la bibliothèque STL <vector> 39 La classe deque en C++ ( Bibliothèque STL) 40 La classe list en C++ (bibliothèque STL) <list> 41 La classe stack (Pile) en C++ (bibliothèque STL) <stack> 42 La classe queue (File d'attente) en C++ (bibliothèque STL) <queue> 43 La file d'attente prioritaire (classe priority_queue) - Bibliothèque STL 44 Les ensembles en C++ (Classe set <set> - Bibliothèque STL) 45 Les dictionnaires en C++ : Classe map (Bibliothèque STL) 46 Introduction aux algorithmes de la bibliothèque STL (programmation compétitive) 47 Tri et méthodes associées en C++ - Bibliothèque STL 48 Recherche dichotomique et méthodes associées en C++ - Bibliothèque STL 49 Appliquer un prédicat ou une fonction aux éléments d'une séquence en C++ - Bibliothèque STL 50 Recherche dans une séquence et méthodes associées en C++ - Bibliothèque STL

Les listes chaînées, piles et files sont des collections linéaires. Un arbre est une collection non linéaire. Un cas particulier important est l'arbre binaire de recherche (ABR), dans lequel les valeurs du sous-arbre gauche sont inférieures à la racine, et celles du sous-arbre droit lui sont supérieures.

Arbre binaire de recherche (ABR)

Un ABR est un arbre binaire tel que pour tout nœud n : toutes les valeurs du sous-arbre gauche sont inférieures à la valeur de n, et toutes les valeurs du sous-arbre droit lui sont supérieures. Cette propriété est vérifiée récursivement pour chaque nœud.

Parcours d'arbre binaire

Le parcours d'un ABR exige que chaque nœud soit traité exactement une fois. Il existe trois types de parcours :

ParcoursOrdre de traitementPropriété
Infixe (in-order)Gauche → Racine → DroiteProduit les valeurs en ordre croissant
Préfixe (pre-order)Racine → Gauche → DroiteLa racine est traitée en premier
Postfixe (post-order)Gauche → Droite → RacineLa racine est traitée en dernier — utilisé pour la destruction

Parcours infixe

Le traitement de la racine se situe entre les deux sous-arbres. Le parcours infixe d'un ABR produit les valeurs dans l'ordre croissant.

Parcours préfixe

La racine est traitée en premier, suivie de tous les nœuds du sous-arbre gauche, puis du droit.

Parcours postfixe

Le traitement de la racine vient après les deux sous-arbres. C'est le parcours utilisé pour détruire un ABR.

Mise en œuvre

Classe ABR

Les méthodes de la classe sont divisées en deux catégories : les méthodes privées (aide récursive) et les méthodes publiques (interface simplifiée pour l'utilisateur).

#ifndef ABR_H
#define ABR_H
#include <iostream>
using namespace std;

template <typename T>
struct Noeud {
    T donnees;
    Noeud <T>* gauche;
    Noeud <T>* droit;
};

template <class T>
class ABR {
    private:
        Noeud <T>* racine;
        int compteur;
        Noeud <T>* CreerNoeud (const T& valeur);
        void detruire  (Noeud <T>* ptr);
        void inserer   (const T& valeur, Noeud <T>*& ptr);
        void infixe    (Noeud <T>* ptr) const;
        void prefixe   (Noeud <T>* ptr) const;
        void postfixe  (Noeud <T>* ptr) const;
        Noeud <T>* successeur   (Noeud <T>* ptr, Noeud <T>*& parent) const;
        Noeud <T>* predecesseur (Noeud <T>* ptr, Noeud <T>*& parent) const;
        void supprimer (Noeud <T>* ptr, Noeud <T>* parent);
        Noeud <T>* recherche (const T& valeur, Noeud <T>* ptr, Noeud <T>*& parent) const;
    public:
        ABR ();
        ~ABR ();
        void inserer    (const T& valeur);
        void detruire   ();
        void supprimer  (const T& valeur);
        Noeud <T>* recherche (const T& valeur) const;
        void infixe   () const;
        void prefixe  () const;
        void postfixe () const;
        int  taille   () const;
        bool estVide  () const;
};
#endif

Constructeur et destructeur

// Constructeur
template <class T>
ABR <T>::ABR() : racine(0), compteur(0) {}

// Destructeur — appelle detruire (parcours postfixe)
template <class T>
ABR <T>::~ABR () { detruire(racine); }

Détruire un ABR

La destruction utilise un parcours postfixe : on détruit d'abord le sous-arbre gauche, puis le droit, puis la racine.

template <class T>
void ABR <T>::detruire (Noeud <T>* ptr) {
    if (!ptr) { return; }
    detruire(ptr -> gauche);  // détruire sous-arbre gauche
    detruire(ptr -> droit);   // détruire sous-arbre droit
    delete ptr;                // détruire la racine
}

Insertion

L'insertion est récursive : si l'arbre est vide, on insère à la racine ; sinon on descend à gauche si la valeur est inférieure, à droite sinon.

template <class T>
void ABR <T>::inserer (const T& valeur, Noeud <T>*& ptr) {
    if (!ptr) {
        ptr = CreerNoeud(valeur);   // arbre vide : insérer comme racine
        return;
    } else if (valeur < ptr -> donnees) {
        inserer(valeur, ptr -> gauche); // insérer dans le sous-arbre gauche
    } else {
        inserer(valeur, ptr -> droit);  // insérer dans le sous-arbre droit
    }
}

Rechercher un nœud

La recherche est également récursive. Elle retourne le nœud trouvé (avec son parent) ou NULL si absent.

template <class T>
Noeud <T>* ABR <T>::recherche (const T& valeur, Noeud <T>* ptr, Noeud <T>*& parent) const {
    if (!ptr)                          return NULL;
    else if (ptr->donnees == valeur)   return ptr;
    else if (valeur < ptr->donnees)  { parent = ptr; return recherche(valeur, ptr->gauche, parent); }
    else                             { parent = ptr; return recherche(valeur, ptr->droit,  parent); }
}

Supprimer un nœud

La suppression couvre trois cas selon le nombre d'enfants du nœud à supprimer :

Cas 1 — Le nœud n'a pas d'enfants

Le lien du parent est mis à NULL et le nœud est supprimé.

Cas 2 — Le nœud a un seul enfant

Le nœud est supprimé et son enfant est directement relié au parent.

Cas 3 — Le nœud a deux enfants

On remplace la valeur du nœud par celle de son successeur (valeur minimale du sous-arbre droit) ou de son prédécesseur (valeur maximale du sous-arbre gauche), puis on supprime ce successeur/prédécesseur.

template <class T>
void ABR <T>::supprimer (Noeud <T>* ptr, Noeud <T>* parent) {
    if (ptr->gauche == 0 && ptr->droit == 0) {         // Cas 1 : feuille
        if (ptr != racine) {
            if (parent->gauche == ptr) parent->gauche = NULL;
            else                       parent->droit  = NULL;
        } else racine = NULL;
        delete ptr;
    } else if (ptr->gauche && ptr->droit) {             // Cas 3 : deux enfants
        Noeud <T>* pere = ptr;
        Noeud <T>* succ = successeur(ptr->droit, pere);
        int val = succ->donnees;
        supprimer(succ, pere);
        ptr->donnees = val;
    } else {                                             // Cas 2 : un enfant
        Noeud <T>* enfant = (ptr->gauche) ? ptr->gauche : ptr->droit;
        if (ptr != racine) {
            if (ptr == parent->gauche) parent->gauche = enfant;
            else                       parent->droit  = enfant;
        } else racine = enfant;
        delete ptr;
    }
}

Parcours (implémentations récursives)

template <class T>
void ABR <T>::infixe (Noeud <T>* ptr) const {
    if (!ptr) { return; }
    infixe(ptr -> gauche);
    cout << ptr -> donnees << '\t';
    infixe(ptr -> droit);
}

template <class T>
void ABR <T>::prefixe (Noeud <T>* ptr) const {
    if (!ptr) { return; }
    cout << ptr -> donnees << '\t';
    prefixe(ptr -> gauche);
    prefixe(ptr -> droit);
}

template <class T>
void ABR <T>::postfixe (Noeud <T>* ptr) const {
    if (!ptr) { return; }
    postfixe(ptr -> gauche);
    postfixe(ptr -> droit);
    cout << ptr -> donnees << '\t';
}
Méthodes publiques vs privées

Toutes les méthodes récursives ci-dessus sont privées. Les méthodes publiques (inserer(valeur), infixe(), etc.) appellent simplement leurs homologues privés avec les bons paramètres, masquant la récursion à l'utilisateur.

Programme principal

#include "abr.cpp"

int main() {
    ABR <int> arbre;
    arbre.inserer(40); arbre.inserer(50); arbre.inserer(14);
    arbre.inserer(16); arbre.inserer(5);  arbre.inserer(3);
    arbre.inserer(7);  arbre.inserer(55); arbre.inserer(45);

    cout << "Parcours infixe : \n";   arbre.infixe();
    cout << "\nParcours postfixe : \n"; arbre.postfixe();
    cout << "\nParcours prefixe : \n";  arbre.prefixe();

    if (arbre.recherche(3)  != 0) cout << "\nNoeud 3 : present";
    if (arbre.recherche(60) == 0) cout << "\nNoeud 60 : absent";

    arbre.supprimer(3);
    cout << "\nInfixe apres suppression de 3 : \n"; arbre.infixe();

    arbre.supprimer(40);
    cout << "\nInfixe apres suppression de 40 : \n"; arbre.infixe();
    return 0;
}
Sortie
Parcours infixe :
3       5       7       14      16      40      45      50      55

Parcours postfixe :
3       7       5       16      14      45      55      50      40

Parcours prefixe :
40      14      5       3       7       16      50      45      55

Noeud 3 : present
Noeud 60 : absent

Infixe apres suppression de 3 :
5       7       14      16      40      45      50      55
Infixe apres suppression de 40 :
5       7       14      16      45      50      55

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.