• Aucun résultat trouvé

Les arbres

3.2 LES ARBRES BINAIRES

3.2.4 Parcours d’un arbre binaire

Un algorithme de parcours d’arbre est un procédé permettant d’accéder à chaque nœud de l’arbre. Un certain traitement est effectué pour chaque nœud (test, écriture, comp-tage, etc.), mais le parcours est indépendant de cette action et commun à des algo-rithmes qui peuvent effectuer des traitements très divers comme rechercher les enfants de Gontran, compter la descendance de Julie, compter le nombre de garçons, ajouter un fils à Paul, etc. On distingue deux catégories de parcours d’arbres : les parcours en profondeur et les parcours en largeur. Dans le parcours en profondeur, on explore branche par branche alors que dans le parcours en largeur on explore niveau par niveau.

3.2.4.a Les différentes méthodes de parcours en profondeur d’un arbre Il y a 6 types de parcours possibles (P : père, SAG : arbre gauche, SAD : sous-arbre droit). Nous ne considérons dans la suite de ce chapitre que les parcours gauche-droite. Les parcours droite-gauche s’en déduisent facilement par symétrie.

Ces parcours sont appelés parcours en profondeur car on explore une branche de l’arbre le plus profond possible avant de revenir en arrière pour essayer un autre chemin.

Figure 63 Les 6 types de parcours d’un arbre binaire.

Dans un parcours d’arbre gauche-droite, un nœud est visité trois fois :

• lors de la première rencontre du nœud, avant de parcourir le sous-arbre gauche.

• après parcours du sous-arbre gauche, avant de parcourir le sous-arbre droit.

• après examens des sous-arbres gauche et droit.

L’action à effectuer sur le nœud peut se faire lors de la visite (a), (b) ou (c).

gauche - droite droite - gauche

préfixé P . SAG . SAD P . SAD . SAG

infixé SAG . P . SAD SAD . P . SAG

postfixé SAG . SAD . P SAD . SAG . P

P

S AD S AG

(a) (b) (c)

Figure 64 Les 3 visites d’un nœud lors d’un parcours d’arbre binaire.

04Chap_03 Page 114 Samedi, 17. janvier 2004 10:37 10

3.2 • Les arbres binaires 115

© Dunod – La photocopie non autorisée est un délit.

3.2.4.b Parcours sur l’arbre binaire de l’arbre généalogique Parcours préfixé

Le premier type de parcours est appelé parcours préfixé. Il faut traiter le nœud lors de la première visite, puis explorer le sous-arbre gauche (en appliquant la même méthode) avant d’explorer le sous-arbre droit. Sur la Figure 58, Jonatan est traité avant son SAG (Pauline Sonia Paul) et avant son SAD (Gontran Antonine). La procédure se schématise comme suit :

• traitement de la racine

• traitement du sous-arbre gauche

• traitement du sous-arbre droit

Sur l’exemple, cela conduit au parcours de la Figure 65. Pour chaque nœud, on trouve le nom du nœud concerné, les éléments du SAG, puis les éléments du SAD.

Parcours infixé

Dans un parcours infixé, le nœud est traité lors de la deuxième visite, après avoir traité le sous-arbre gauche, mais avant de traiter le sous-arbre droit. La Figure 66 indique l’ordre de traitement des nœuds de l’arbre généalogique. Un nœud se trouve entre son SAG et son SAD. Jonatan par exemple se trouve entre son SAG (Pauline Sonia Paul) et son SAD (Antonine Gontran). La procédure se schématise comme suit :

• traitement du sous-arbre gauche

• traitement de la racine

• traitement du sous-arbre droit

Parcours postfixé

En parcours postfixé, le nœud est traité lors de la troisième visite, après avoir traité le SAG et le SAD. La Figure 67 indique par exemple que Jonatan est traité après son

Julie Jonatan Pauline Sonia Paul Gontran Antonine

Figure 65 Parcours préfixé de l’arbre généalogique de la Figure 58.

Pauline Sonia Paul Jonatan Antonine Gontran Julie

Figure 66 Parcours infixé de l’arbre généalogique de la Figure 58.

04Chap_03 Page 115 Samedi, 17. janvier 2004 10:37 10

116 3 Les arbres

SAG (Paul Sonia Pauline) et après son SAD (Antonine Gontran). La procédure à suivre est donnée ci-dessous :

• traitement du sous-arbre gauche

• traitement du sous-arbre droit

• traitement de la racine

Exercice 15 - Parcours d’arbres droite-gauche

Donner sur l’exemple de la Figure 58, les parcours préfixé, infixé, postfixé en parcours droite-gauche (voir Figure 63).

3.2.4.c Parcours sur l’arbre binaire de l’expression arithmétique

Sur l’arbre binaire de l’expression arithmétique, les parcours correspondent à une écriture préfixée, infixée ou postfixée de cette expression.

Parcours préfixé

L’opérateur est traité avant ses opérandes

Parcours infixé

L’opérateur se trouve entre ses deux opérandes.

L’expression infixée est ambiguë et peut être interprétée comme : a + (b * c) - d - e ou (a + b) * (c - d) - e

Il faut utiliser des parenthèses pour lever l’ambiguïté. C’est la notation habituelle d’une expression arithmétique dans les langages de programmation. En l’absence de parenthèses, des priorités entre opérateurs permettent aux compilateurs de choisir une des interprétations possibles.

Paul Sonia Pauline Antonine Gontran Jonatan Julie

Figure 67 Parcours postfixé de l’arbre généalogique de la Figure 58.

* + a b c d e

Figure 68 Parcours préfixé de l’expression arithmétique de la Figure 62.

a + b * c d e

Figure 69 Parcours infixé de l’expression arithmétique de la Figure 62.

04Chap_03 Page 116 Samedi, 17. janvier 2004 10:37 10

3.2 • Les arbres binaires 117

© Dunod – La photocopie non autorisée est un délit.

Parcours postfixé

L’opérateur se trouve après ses opérandes.

3.2.4.d Les algorithmes de parcours d’arbre binaire

Les fonctions de parcours découlent directement des algorithmes vus sur les exemples précédents. Le simple changement de la place de l’ordre d’écriture conduit à un traitement préfixé, infixé ou postfixé. La fonction toString(), passée en paramètre de prefixe() fournit une chaîne de caractères spécifiques de l’objet traité.

Cette chaîne est imprimée lors du printf. La fonction toString() est dépendante de l’application et passée en paramètre lors de la création de l’arbre. Par défaut, les objets référencés dans chaque nœud sont des chaînes de caractères.

Algorithme de parcours préfixé

// toString fournit la chaîne de caractères à écrire pour un objet donné static void prefixe (Noeud* racine, char* (*toString) (Objet*)) { if (racine != NULL) {

printf ("%s ", toString (racine->reference));

prefixe (racine->gauche, toString);

prefixe (racine->droite, toString);

} }

// parcours préfixé de l'arbre void prefixe (Arbre* arbre) {

prefixe (arbre->racine, arbre->toString);

}

Le déroulement de l’algorithme récursif prefixe() sur la Figure 62 où les pointeurs ont été remplacés pour l’explication par des adresses de 0 à 8 est schématisé sur la Figure 71. Les adresses des nœuds en allocation dynamique sont normalement quelconques et dispersées en mémoire. L’appel prefixe() avec le nœud racine 0 entraîne un appel récursif qui consiste à traiter le SAG, d’où un appel à prefixe() avec pour nouvelle racine 1 qui à son tour déclenche une cascade d’appels récursifs.

Plus tard, on fera un appel à prefixe() avec un pointeur sur SAD en 8. Le déroule-ment de l’exécution de l’algorithme est schématisé ligne par ligne, de haut en bas, et de gauche à droite.

a b + c d * e

Figure 70 Parcours postfixé de l’expression arithmétique de la Figure 62.

04Chap_03 Page 117 Samedi, 17. janvier 2004 10:37 10

118 3 Les arbres

Figure 71 Parcours préfixé de l’arbre binaire de la Figure 62.

Algorithme de parcours infixé

Le nœud racine est traité (écrit) entre les deux appels récursifs.

// toString fournit la chaîne de caractères à écrire pour un objet static void infixe (Noeud* racine, char* (*toString) (Objet*)) { if (racine != NULL) {

infixe (racine->gauche, toString);

printf ("%s ", toString (racine->reference));

infixe (racine->droite, toString);

} }

// parcours infixé de l'arbre void infixe (Arbre* arbre) {

infixe (arbre->racine, arbre->toString);

}

Algorithme de parcours postfixé

Le nœud racine est traité après les deux appels récursifs.

// toString fournit la chaîne de caractères à écrire pour un objet static void postfixe (Noeud* racine, char* (*toString) (Objet*)) { if (racine != NULL) {

postfixe (racine->gauche, toString);

postfixe (racine->droite, toString);

printf ("%s ", toString (racine->reference));

} }

// parcours postfixé de l'arbre void postfixe (Arbre* arbre) {

postfixe (arbre->racine, arbre->toString);

}

prefixe (0); racine = 0;

printf (-);

prefixe (4); racine = 4;

printf (b);

prefixe (NULL);

prefixe (NULL);

prefixe (5); racine = 5;

printf (-);

prefixe (7); racine = 7;

printf (d);

prefixe (NULL);

prefixe (NULL);

prefixe (8); racine = 8;

printf (e);

prefixe (NULL);

prefixe (NULL);

04Chap_03 Page 118 Samedi, 17. janvier 2004 10:37 10

3.2 • Les arbres binaires 119

© Dunod – La photocopie non autorisée est un délit.

Parcours préfixé avec indentation

Les écritures concernant les objets des nœuds visités sont décalées (indentées) pour mieux mettre en évidence la structure de l’arbre binaire. Le niveau est augmenté de 1 à chaque fois que l’on descend à gauche ou à droite dans l’arbre binaire.

// toString fournit la chaîne de caractères à écrire pour un objet // niveau indique l'indentation à faire

static void indentationPrefixee (Noeud* racine,

char* (*toString) (Objet*), int niveau) { if (racine != NULL) {

printf ("\n");

for (int i=1; i<niveau; i++) printf ("%5s", " ");

printf ("%s ", toString (racine->reference));

indentationPrefixee (racine->gauche, toString, niveau+1);

indentationPrefixee (racine->droite, toString, niveau+1);

} }

void indentationPrefixee (Arbre* arbre) {

indentationPrefixee (arbre->racine, arbre->toString, 1);

}

Résultats du parcours préfixé avec indentation :

L’exécution de la fonction indentationPrefixee() sur l’exemple de la Figure 62 conduit aux résultats suivants où la structure de l’arbre binaire est mise en évidence.

* + a b c d e

3.2.4.e Recherche d’un nœud de l’arbre

La fonction trouverNoeud() recherche récursivement le nœud contenant les infor-mations définies dans objet, dans l’arbre commençant en racine. C’est un parcours d’arbre interrompu (on s’arrête quand on a trouvé un pointeur sur l’objet concerné).

Si l’objet n’est pas dans l’arbre, la fonction retourne NULL.

Algorithme : la comparaison de deux objets est définie par la fonction comparer() passée en paramètre et spécifique des objets traités dans l’application. Cette fonction est définie lors de la création de l’arbre. Par défaut, il s’agit d’un arbre de chaînes de caractères.

La recherche de objet dans un arbre vide retourne la valeur NULL (pas trouvé). Si le nœud pointé par racine contient l’objet que l’on cherche alors le résultat est le

04Chap_03 Page 119 Samedi, 17. janvier 2004 10:37 10

120 3 Les arbres

pointeur racine ; sinon, on cherche objet dans le SAG ; si objet n’est pas dans le SAG, on le cherche dans le SAD.

static Noeud* trouverNoeud (Noeud* racine, Objet* objet,

int (*comparer) (Objet*, Objet*)) { Noeud* pNom;

if (racine == NULL) { pNom = NULL;

} else if (comparer (racine->reference, objet) == 0) { pNom = racine;

} else {

pNom = trouverNoeud (racine->gauche, objet, comparer);

if (pNom == NULL) pNom = trouverNoeud (racine->droite, objet,

comparer);

}

return pNom;

}

// recherche le noeud objet dans l'arbre

Noeud* trouverNoeud (Arbre* arbre, Objet* objet) {

return trouverNoeud (arbre->racine, objet, arbre->comparer);

}

Cette procédure est utile pour retrouver un nœud particulier de l’arbre et déclencher un traitement à partir de ce nœud. On peut par exemple appeler trouverNœud() pour obtenir un pointeur sur le nœud Jonatan de la Figure 58 et effectuer une énumération indentée à partir de ce nœud en appelant la fonction indentationPrefixee().

3.2.4.f Parcours en largeur dans un arbre

Une autre méthode de parcours des arbres consiste à les visiter étage par étage, comme si on faisait une coupe par niveau. Ainsi, sur l’arbre généalogique binaire de la Figure 58, le parcours en largeur est le suivant : Julie/Jonatan/Pauline-Gontran/Sonia-Antonine/Paul. Sur l’arbre binaire de l’expression arithmétique de la Figure 61, le parcours en largeur est : - * e + - a b c d. Ce parcours nécessite l’utilisation d’une file d’attente contenant initialement la racine. On extrait l’élément en tête de la file, et on le remplace par ses successeurs à gauche et à droite jusqu’à ce que la file soit vide. Dans les parcours d’arbres, on effectue plutôt des parcours en profondeur, bénéficiant ainsi des mécanismes automatiques de retour en arrière de la récursivité. Le parcours en largeur est effectué lorsque les résultats l’imposent comme pour le dessin d’un arbre binaire (voir § 3.2.8, page 127).

La fonction enLargeur() effectue un parcours en largeur des nœuds de l’arbre.

Exemple de résultats pour l’arbre généalogique : Parcours en largeur

Julie Jonatan Pauline Gontran Sonia Antonine Paul

04Chap_03 Page 120 Samedi, 17. janvier 2004 10:37 10

3.2 • Les arbres binaires 121

© Dunod – La photocopie non autorisée est un délit.

static void enLargeur (Noeud* racine, char* (*toString) (Objet*)) { Liste* li = creerListe();

insererEnFinDeListe (li, racine);

while (!listeVide (li) ) {

Noeud* extrait = (Noeud*) extraireEnTeteDeListe (li);

printf ("%s ", toString (extrait->reference));

if (extrait->gauche != NULL) insererEnFinDeListe (li,

extrait->gauche);

if (extrait->droite != NULL) insererEnFinDeListe (li,

extrait->droite);

} }

// parcours en largeur de l'arbre void enLargeur (Arbre* arbre) {

enLargeur (arbre->racine, arbre->toString);

}

La fonction EnLargeurParEtape() effectue un parcours en largeur des nœuds de l’arbre en effectuant un traitement en fin de chaque étage (aller à la ligne ici). Il faut utiliser 2 listes : une liste contenant les pointeurs sur les nœuds de l’étage courant, et une autre contenant les successeurs des nœuds courants qui deviendront étage courant à l’étape suivante.

Exemple de résultats :

Parcours en largeur par étage Julie

Jonatan

Pauline Gontran Sonia Antonine Paul

static void enLargeurParEtage (Noeud* racine, char* (*toString) (Objet*)) { Liste* lc = creerListe(); // liste courante

Liste* ls = creerListe(); // liste suivante insererEnFinDeListe (lc, racine);

while (!listeVide (lc)) { while (!listeVide (lc) ) {

Noeud* extrait = (Noeud*) extraireEnTeteDeListe (lc);

printf ("%s ", toString (extrait->reference));

if (extrait->gauche != NULL) insererEnFinDeListe (ls,

extrait->gauche);

if (extrait->droite != NULL) insererEnFinDeListe (ls,

extrait->droite);

void enLargeurParEtage (Arbre* arbre) {

enLargeurParEtage (arbre->racine, arbre->toString);

}

04Chap_03 Page 121 Samedi, 17. janvier 2004 10:37 10

122 3 Les arbres