
Les composantes de l'arbre seront donc soit des opérateurs (noeuds), soit des opérandes (feuilles). Nous allons choisir les déclarations suivantes :
typedef float type_feuille;
typedef struct s_noeud_2
{char op_c;
struct s_comp*fils1;
struct s_comp*fils2;
}type_noeud_2;
typedef struct s_noeud_1
{char op_c;
struct s_comp*fils;
}type_noeud_1;
typedef struct s_comp
{int arite;
union
{type_noeud_2 n2;
type_noeud_1 n1;
type_feuille f;
}val;
}composante;
typedef composante *lien;
Nous prévoyons donc deux types de noeuds : à arité 1 (avec
un fils) ou à arité 2 (deux fils). On aurait pu utiliser un seul
type (à arité 2) en mettant NULL dans le second fils en cas
d'arité 1. Une feuille par contre ne contient aucun lien. Une composante
de l'arbre contiendra donc un indicateur de son type (ici l'arité) et,
suivant ce type, soit une feuille, soit un noeud (d'arité 1 ou 2). Nous
allons écrire une fonction créant un tel arbre (à partir
d'une chaîne de caractères préalablement saisie, contenant
l'expression en notation polonaise préfixée
par exemple) :
{
lien x;
char c;
while(**deb==' '||**deb==',') (*deb)++;
if(**deb==0) /* on est arrivé en fin de chaîne */
{puts("erreur : il doit manquer des opérandes");
return(NULL);}
x=(composante*)malloc(sizeof(composante));
if(isdigit(**deb))
{
x->arite=0;
sscanf(*deb,"%f",&(x->val.f));
while(isdigit(**deb)||**deb=='.') (*deb)++;
}
else
{
c=toupper(*((*deb)++));
if(c=='*'||c=='/'||c=='+'||c=='-')
{
x->arite=2;
x->val.n2.op_c=c;
x->val.n2.fils1=saisie_prefixe(deb);
x->val.n2.fils2=saisie_prefixe(deb);
}
else if(c=='C'||c=='S')
{
x->arite=1;
x->val.n1.op_c=c;
x->val.n1.fils=saisie_prefixe(deb);
}
else printf("erreur, '%c'n'est pas un opérateur prévu\n",c);
}
return(x);
}
On peut remarquer que cette fonction est récursive.
La création de l'arbre à partir d'une expression postfixée
est similaire (scruter la chaîne dans l'autre sens par exemple), à
partir d'une expression infixée
c'est un peu plus dur, surtout si l'on ne veut pas imposer d'entrer toutes les
parenthèses, par exemple 1+2+3 au lieu de (1+(2+3)).L'utilisation d'un tel arbre devient maintenant très simple. On utilisera la récursivité. Pour évaluer l'expression, si c'est une feuille unique la valeur est celle de la feuille, si c'est un noeud, la valeur est obtenue en effectuant l'opération correspondante, après évaluation (récurrente, donc soit directement la valeur si feuille soit nouveau calcul) de ses fils :
float eval(lien x)
{
float r1,r2;
if(x->arite==0) return(x->val.f);
else if (x->arite==1)
{
if (x->val.n1.op_c=='C')
return(cos(eval(x->val.n1.fils)));
else if (x->val.n1.op_c=='S')
return(sin(eval(x->val.n1.fils)));
}
else
{
r1=eval(x->val.n2.fils1);
r2=eval(x->val.n2.fils2);
switch (x->val.n2.op_c)
{
case '+':return(r1+r2);
case '-':return(r1-r2);
case '*':return(r1*r2);
case '/':return(r1/r2);
}
}
/* si aucun return n'a été effectué jusqu'ici*/
puts("erreur (code opératoire inconnu par exemple)");
return(0);
}
Le parcours
des arbres (c'est à dire le passage par tous les noeuds et feuilles) se
fait d'habitude de manière récursive. On doit évidement
parcourir l'arbre pour afficher son contenu. On peut utiliser dans le cas des
arbres binaires trois possibilités de parcours : afficher la valeur du
noeud puis récursivement de ses deux fils, afficher le premier fils, la
valeur du noeud, puis le second fils, ou afficher les deux fils avant la valeur
du noeud. On obtient dans notre cas directement les notations
préfixé, infixée et postfixée (avec un petit
travail supplémentaire pour la gestion des parenthèses en
notation infixée, qu'on aurait pu simplifier si l'on avait
accepté des parenthèses surabondantes) :
void affiche_prefixe(lien x)
{
switch(x->arite)
{
case 0:printf("%6.1f ",x->val.f);break;
case 1:printf(" %C ",x->val.n1.op_c);
affiche_prefixe(x->val.n1.fils);
break;
case 2:printf(" %c ",x->val.n2.op_c);
affiche_prefixe(x->val.n2.fils1);
affiche_prefixe(x->val.n2.fils2);
}
}
void affiche_postfixe(lien x)
{
switch(x->arite)
{
case 0:printf("%6.1f ",x->val.f);break;
case 1:affiche_postfixe(x->val.n1.fils);
printf(" %C ",x->val.n1.op_c);
break;
case 2:affiche_postfixe(x->val.n2.fils1);
affiche_postfixe(x->val.n2.fils2);
printf(" %c ",x->val.n2.op_c);
}
}
void affiche_infixe(lien x)
{
switch(x->arite)
{
case 0:printf("%6.1f ",x->val.f);break;
case 1:printf(" %C( ",x->val.n1.op_c);
affiche_infixe(x->val.n1.fils);
putch(')');
break;
case 2:if(x->val.n2.fils1->arite!=0)putch('(');
affiche_infixe(x->val.n2.fils1);
if(x->val.n2.fils1->arite!=0)putch(')');
printf(" %c ",x->val.n2.op_c);
if(x->val.n2.fils2->arite!=0)putch('(');
affiche_infixe(x->val.n2.fils2);
if(x->val.n2.fils2->arite!=0)putch(')');
}
}
Notre exemple est un peu plus complexe que dans véritable arbre
binaire, puisque nous acceptions également des noeuds à un seul
fils.
typedef struct s_noeud *lien;
typedef struct s_noeud
{char *nom;
lien fils1;
lien fils2;
}type_noeud;
Il vaut mieux ne pas prévoir de type spécifique pour les
feuilles, l'arbre étant géré de manière
évolutive (une feuille sera donc un noeud avec ses deux fils valant le
pointeur NULL). Le nom contenu dans chaque noeud aurait pu être un
tableau statique de caractères, mais pour optimiser le coût
mémoire, nous utiliserons un tableau dynamique. Donc, pour allouer la
mémoire d'un nouveau noeud, deux malloc
seront nécessaires : un pour le noeud lui-même, et un pour le nom
qui lui est associé. Ecrivons une fonction de création d'un noeud
(on lui transmet le nom associé, elle retourne l'adresse allouée):
lien cree_feuille(char *nouv_nom)
{
lien x;
int taille;
x=(lien)malloc(sizeof(type_noeud));
taille=strlen(nouv_nom)+1; /* compter le \0 */
x->nom=(char*)malloc(taille);
strcpy(x->nom,nouv_nom);
x->fils1=NULL;
x->fils2=NULL;
return(x);
}
Supposons entrer dans l'ordre : Jean, Bernard, Lucie, Anne, Jacques,
Laurent, Maud, Luc, Julie, Elsa. Si vous créez progressivement l'arbre,
vous pourrez voir que toute nouvelle valeur trouve toujours sa place, les
feuilles se transformant petit à petit en noeuds, au fur et à
mesure de l'augmentation de la taille de l'arbre.

La fonction effectuant la saisie et la création de cet arbre devra donc, après la saisie du nouveau nom à insérer, rechercher sa position définitive. Cette recherche sera récursive: sur un noeud donné, si le nouveau nom est plus petit, rechercher dans la descendance du fils1, sinon rechercher dans la descendance du fils2. La recherche s'arrête quand on arrive à un pointeur NULL (qui sera remplacé par l'adresse du nouveau noeud). En cas d'égalité, si l'on désire une création stable, un nouveau nom sera placé derrière ceux déjà existants. :
void insert_feuille(lien racine, lien x)
{
while(racine!=NULL) /* ou boucle infinie */
{
if(strcmp(x->nom,racine->nom)<0)
if(racine->fils1==NULL){racine->fils1=x;return;}
else racine=racine->fils1;
else
if(racine->fils2==NULL){racine->fils2=x;return;}
else racine=racine->fils2;
}
}
lien saisie(void)
{
lien racine=NULL;
char txt[100];
do
{
printf("entrez un nouveau nom, @@@ pour finir : ");
gets(txt);
if(strcmp(txt,"@@@"))
if(racine==NULL)racine=cree_feuille(txt);
else insert_feuille(racine,cree_feuille(txt));
}
while(strcmp(txt,"@@@"));
return(racine);
}
On remarque l'efficacité de cette méthode : aucun
déplacement d'élément, le tri se fait par insertion, mais
avec recherche optimisée (du type dichotomie : l'espace de recherche est
réduit à chaque test). Pour N insertions, on fait donc N
recherches donc N*P lectures de composantes (P profondeur de l'arbre).Une fois l'arbre créé, on peut afficher la liste triée par ordre alphabétique par un simple parcours infixé (arbr_nom):
void affiche(lien x)
{
if(x!=NULL)
{
affiche(x->fils1);
printf("%s, ",x->nom);
affiche(x->fils2);
}
}
L'utilisation d'un arbre binaire
pour une liste triée permet donc une programmation relativement
aisée, des algorithmes rapides, mais moyennant un peu de place
mémoire supplémentaire. L'arbre va posséder
simultanément tous les avantages des autres structures de données:

En fait, plutôt que de créer cette liste hors des noeuds, le plus simple (et qui utilise autant de mémoire) est d'associer à chaque noeud l'adresse de son fils aîné, et de son frère cadet. Accéder à tous les fils revient donc à accéder au fils aîné puis à tous ses frères:

On peut remarquer qu'un tel arbre est un arbre binaire: chaque noeud possède deux liens. On peut donc traiter tout arbre sous forme d'un arbre binaire.
Une autre amélioration possible d'un arbre est de permettre un accès toujours séquentiel mais bidirectionnel : pour un arbre binaire, chaque noeud possède en plus l'adresse de son père, pour un arbre généralisé ceci revient à ce que chaque frère connaît son aîné, l'aîné connaissant leur père commun. Cet accès bidirectionnel est coûteux en mémoire, mais complique également les modifications (plus de liens à gérer), pour par contre accélérer les parcours dans l'arbre. On ne prévoira cette amélioration que lorsque l'on utilise de fréquentes remontées (l'utilisation d'algorithmes récursifs ne nécessite en général pas ces liens, le fait de quitter une fonction appelée pour gérer un fils fait automatiquement retrouver le père).