Vous êtes sur la page 1sur 8

Constructeurs et destructeur d’arbre

binaire
I. Constructeur par défaut d’arbre binaire
Lorsqu’une variable de type arbre binaire est déclarée dans un programme, par exemple de la
manière suivante

ArbreBinaire A;

le pointeur racine de A n’est pas initialisé et pointe donc n’importe où en mémoire. Ceci est
dangereux et doit absolument être évité. Nous souhaitons faire en sorte que lors de la déclaration
d’une variable de type ArbreBinaire son pointeur racine soit automatiquement initialisé à un
pointeur nul, c'est-à-dire que l’arbre soit initialisé à un arbre vide.

Il faut pour cela écrire ce qu’on appelle un constructeur d’arbre binaire. C’est une fonction qui sera
appelée automatiquement dès qu’un arbre binaire sera créé. Rappelons que le nom d’un
constructeur est simplement le nom de la classe.

ArbreBinaire::ArbreBinaire()
{
racine = nullptr;
}

On a plutôt pour habitude d’initialiser les champs d’un objet (comme ici le champ racine) dans ce
qu’on appelle la liste d’initialisation du constructeur. Cette liste se met directement à la suite de
l’entête du constructeur de la manière suivante :

ArbreBinaire::ArbreBinaire() : racine{nullptr}
{}

Il faut également penser à ajouter l’entête de ce constructeur dans la partie publique de la classe
ArbreBinaire (voir la section VI).

II. Constructeur de nœud


Comme pour l’arbre binaire, on souhaite que lors de la création d’un nœud ses deux pointeurs fg et
fd soient automatiquement initialisés à des pointeurs nuls. On souhaite de plus pouvoir préciser la
valeur à laquelle le champ info du nœud doit être initialisé. Par exemple, pour déclarer un nœud
dont le champ info vaut 10, on veut pouvoir écrire

Noeud N{10};

Il faut pour cela écrire un constructeur de nœud qui prend un entier en paramètre. Ce constructeur
s’écrit de la manière suivante :

Dominique Schmitt L3 Informatique et Mathématiques 1


Noeud::Noeud(int i) : info{i}, fg{nullptr}, fd{nullptr}
{}

Comme pour toute autre fonction, on peut écrire différents constructeurs de nœuds à condition que
leurs listes de paramètres soient différentes. Si on n’écrit pas de constructeur de nœud par défaut
(c’est-à-dire un constructeur sans paramètre), il n’est plus possible de créer un nœud sans préciser sa
valeur. Par exemple, la déclaration

Noeud N;

génèrera une erreur de compilation.

L’entête du constructeur de nœud doit également être ajouté dans la classe Noeud. Nous choisissons
de placer cet entête dans la partie privée de la classe Noeud. On peut en effet considérer que
l’utilisateur de la classe ArbreBinaire n’a pas de raison de créer lui-même des nœuds. Pour lui, l’arbre
binaire est une structure de données dans laquelle il veut insérer des valeurs, en l’occurrence des
entiers. Il conviendra donc de lui fournir une fonction qui permet d’insérer un entier dans un arbre.

III. Constructeur par recopie d’arbre binaire


Lors de sa déclaration, une variable entière peut être initialisée avec une autre variable entière qui
existe déjà. C’est le cas pour la variable j dans l’exemple qui suit.

int i=3; // équivalent à int i{3};



int j=i; // équivalent à int j{i};

On souhaite pouvoir faire la même chose avec des arbres binaires, de la manière suivante :

ArbreBinaire A;
… // initialisation de A
ArbreBinaire B=A; // équivalent à ArbreBinaire B{A};

La construction d’un arbre à partir d’un arbre qui existe déjà se fait grâce au constructeur par
recopie d’arbre binaire. Si nous n’écrivons pas nous même ce constructeur par recopie, il est généré
automatiquement et effectue une simple copie champ par champ. Or, un objet ArbreBinaire ne
contient qu’un seul champ, le champ racine. Le champ racine de A est donc recopié dans le champ
racine de B, ce qui a pour effet de faire pointer le pointeur racine de B au même endroit que là où
pointe le pointeur racine de A (voir la figure qui suit).

Dominique Schmitt L3 Informatique et Mathématiques 2


L’arbre binaire B n’est donc pas une copie de A, mais contient exactement les mêmes nœuds que A.
Toute modification ultérieure d’un des arbres entrainera la modification de l’autre. Pour que B soit
vraiment une copie de A, nous devons écrire explicitement le constructeur par recopie d’arbre
binaire.

Commençons par donner une définition récursive de la recopie d’un arbre.

Décomposition
Recopier un arbre A dans un arbre B revient à recopier la racine de A dans la racine de B, à recopier le
fils gauche de A dans le fils gauche de B, et à recopier le fils droit de A dans le fils droit de B.

Cas de base
Si A est vide, B doit aussi être vide.

Comme la fonction qui implémente cette définition va devoir être appelée récursivement sur des
sous-arbres, elle va devoir avoir en paramètre un pointeur rA sur la racine de A et un pointeur rB sur
la racine de B.

Recopier la racine de A dans la racine de B signifie qu’il faut créer en mémoire un nouveau nœud qui
contiendra la même information que la racine de A. Une manière de créer ce nœud consiste à
déclarer une variable de type Noeud de la manière suivante :

Noeud N{rA->info};

Dominique Schmitt L3 Informatique et Mathématiques 3


Le problème est que la variable N est alors une variable locale de la fonction dans laquelle elle est
déclarée et qu’elle disparait dès que cette fonction s’arrête. Cela ne nous convient pas ici puisque
nous voulons créer un arbre qui reste en mémoire même quand la fonction de recopie s’arrête. Le
nœud doit donc être créé en allouant dynamiquement la mémoire de la manière suivante :

new Noeud{rA->info}

Remarquez qu’un nœud créé de cette manière n’a pas de nom. En fait l’opérateur new renvoie
l’adresse où le nœud a été créé en mémoire. Cette adresse doit être récupérée dans un pointeur qui
pointera alors sur le nœud créé.

D’où la fonction récursive qui recopie le sous arbre de racine rA dans le sous arbre de racine rB.

// On suppose que le sous-arbre de racine rB ne contient pas encore


// de noeuds
void ArbreBinaire::copie(Noeud *&rB, Noeud *rA)
{
if (rA == nullptr)
rB = nullptr;
else
{
rB = new Noeud{rA->info};
copie(rB->fg,rA->fg);
copie(rB->fd,rA->fd);
}
}

Attention
Le pointeur rB est modifié par la fonction afin qu’il pointe sur le nouveau nœud créé. Il doit donc être
passé par référence à la fonction (le & dans l’entête de la fonction).

Lorsque le nœud est créé par l’instruction new Noeud{rA->info}, le constructeur de nœud que
nous avons écrit dans la section précédente est automatiquement appelé. Or, ce constructeur
initialise les pointeurs fg et fd du nœud à des pointeurs nuls. Lorsque la fonction copie est appelée
récursivement, les pointeurs rB->fg et rB->fd qui lui sont passés en paramètre sont donc des
pointeurs nuls. L’instruction rB = nullptr est alors inutile, à condition de supposer que le
pointeur rB initial est aussi un pointeur nul.

// On suppose que rB est un pointeur nul


void ArbreBinaire::copie(Noeud *&rB, Noeud *rA)
{
if (rA != nullptr)
{
rB = new Noeud{rA->info};
copie(rB->fg,rA->fg);
copie(rB->fd,rA->fd);
}
}

Dominique Schmitt L3 Informatique et Mathématiques 4


Le constructeur par recopie d’arbre binaire peut maintenant être écrit en utilisant la fonction copie
précédente. Comme tout constructeur, son nom est celui de la classe (ici ArbreBinaire). Comme
il s’agit d’un constructeur par recopie, il a en paramètre l’arbre à partir duquel l’arbre à construire
doit être créé.

ArbreBinaire::ArbreBinaire(const ArbreBinaire &A) : racine{nullptr}


{
copie(racine,A.racine);
}

IV. Destructeur d’arbre


Supposons qu’un arbre est déclaré et créé dans une fonction comme dans l’exemple qui suit.

void f()
{
ArbreBinaire A;
… // création des noeuds de l’arbre A
… // manipulation de l’arbre
}

La variable A est une variable locale de la fonction f. Lorsque la fonction s’arrête cette variable est
automatiquement supprimée de la mémoire. En revanche, comme les nœuds de l’arbre ont été
alloués dynamiquement, ils ne sont pas supprimés. Pour que la suppression des nœuds de l’arbre A
soit faite automatiquement au moment où la variable A est supprimée de la mémoire, il faut écrire
un destructeur d’arbre binaire. C’est une fonction qui est appelée automatiquement dès qu’un objet
de type ArbreBinaire disparait de la mémoire.

Décomposition
Détruire les nœuds d’un arbre binaire revient à détruire les nœuds de ses deux sous arbres puis à
détruire sa racine.

Cas de base
Si l’arbre est vide, il n’y a rien à détruire.

// Détruit les noeuds du sous-arbre de racine r


void ArbreBinaire::detruit(Noeud *r)
{
if (r != nullptr)
{
detruit(r->fg);
detruit(r->fd);
delete r;
}
}

L’instruction delete r détruit ce qui est pointé par r. Dans notre cas, elle détruit le nœud pointé
par r.

Dominique Schmitt L3 Informatique et Mathématiques 5


Le destructeur d’arbre peut maintenant être écrit en utilisant la fonction detruit. Le nom d’un
destructeur est le nom de la classe précédé d’un ~.

ArbreBinaire::~ArbreBinaire()
{
detruit(racine);
}

V. Surcharge de l’opérateur d’affectation


On souhaite pouvoir recopier un arbre dans un autre à n’importe quel moment dans un programme
comme dans l’exemple qui suit.

ArbreBinaire A,B;
… // création des noeuds des arbres A et B
… // manipulation des arbres
B=A;

Il faut pour cela écrire un opérateur = pour les arbres binaires. On dit qu’on surcharge l’opérateur
d’affectation (en effet, l’opérateur = est déjà défini sur d’autres types de variables tels que les
entiers). La différence fondamentale avec le constructeur par recopie vu dans la section III est que
l’arbre B existe déjà au moment où on écrit B=A. L’arbre B contient donc éventuellement déjà des
nœuds. Ces nœuds doivent être détruits avant de recopier A dans B.

Pour recopier un arbre A dans un arbre B il suffit de créer un arbre auxiliaire Aux par recopie à partir
de l’arbre A, puis d’échanger les pointeurs racine de Aux et de B.

Comme Aux est une variable locale de la fonction, Aux est détruit lorsque la fonction s’arrête.
Comme à ce moment-là l’arbre Aux contient les nœuds qui étaient auparavant dans l’arbre B, ces
nœuds sont détruits par le destructeur d’arbre qui est appelé automatiquement sur Aux.

La fonction qui permet de surcharger l’opérateur d’affectation doit s’appeler operator=. Dans son
entête apparaissent trois arbres. L’arbre auquel la méthode est appliquée (celui qui est avant les ::)
correspond à l’arbre qui sera à gauche du signe =. L’arbre qui est dans la parenthèse correspond à

Dominique Schmitt L3 Informatique et Mathématiques 6


l’arbre qui sera à droite du signe =. Enfin, la fonction renvoie la référence d’un arbre. Cela permet
d’enchainer les signes = de la manière suivante A=B=C. L’arbre qui doit être renvoyé par la fonction
est celui auquel la méthode est appliquée et qui n’a donc pas de nom. Cependant, toute méthode
d’une classe dispose d’un pointeur appelé this qui pointe sur l’objet auquel la méthode est
appliquée. L’objet lui-même est désigné par *this. D’où la fonction :

ArbreBinaire& ArbreBinaire::operator=(const ArbreBinaire &A)


{
ArbreBinaire Aux=A;
swap(racine,Aux.racine);
return *this;
}

VI. Actualisation des classes ArbreBinaire et Noeud


Pour intégrer l’ensemble des méthodes écrites dans ce cours, les classes Noeud et ArbreBinaire
doivent être complétées de la manière suivante :

// Définition d’un noeud


class Noeud
{
friend class ArbreBinaire;
private:
// Données privées de la classe
int info; // l’information stockée dans le noeud
Noeud *fg, *fd; // un pointeur sur son fils gauche et
// un pointeur sur son fils droit

// Méthodes privées de la classe

// Constructeur qui construit un noeud de valeur i


Noeud(int i);
};

// Définition d’un arbre binaire


class ArbreBinaire
{
public:
// Méthodes publiques de la classe

// Constructeur d’arbre vide


ArbreBinaire();

// Constructeur par recopie d’arbre


ArbreBinaire(const ArbreBinaire &A);

Dominique Schmitt L3 Informatique et Mathématiques 7


// Destructeur d’arbre
~ArbreBinaire();

// Surcharge de l’opérateur d’affectation


ArbreBinaire& operator=(const ArbreBinaire &A);

// Affiche les noeuds de l’arbre auquel elle est appliquée


void affiche() const;

// Renvoie la hauteur de l’arbre auquel elle est appliquée


int hauteur() const;

private:
// Données privées de la classe
Noeud *racine; // un pointeur sur la racine

// Méthodes privées de la classe

// Recopie le sous-arbre de racine rA dans le sous-arbre de


// racine rB. On suppose que rB est un pointeur nul
void copie(Noeud *&rB, Noeud *rA);

// Détruit les noeuds du sous-arbre de racine r


void detruit(Noeud *r);

// Affiche les noeuds du sous-arbre de racine r


void affiche(Noeud *r) const;

// Renvoie la hauteur du sous-arbre de racine r


int hauteur(Noeud *r) const;
};

Dominique Schmitt L3 Informatique et Mathématiques 8

Vous aimerez peut-être aussi