• Aucun résultat trouvé

4 Arbres de syntaxe abstraite

a compter les notes ´egales `a i. Il suffit alors de lire les notes une par une et d’incr´ementer le compteur correspondant. Une fois ce travail accompli, le tri est termin´e : il y a T[0] notes

´egales `a 0, suivi de T[1] notes ´egales `a 1, etc. Cet algorithme est manifestement lin´eaire et ne fait aucune comparaison ! Pourtant, il ne contredit pas notre r´esultat. Nous avons en effet utilis´e implicitement une information suppl´ementaire : toutes les valeurs `a trier appartiennent `a l’intervalle [0,20]. Cet exemple montre qu’il faut bien r´efl´echir aux conditions particuli`eres avant de choisir un algorithme.

Seconde remarque, on constate exp´erimentalement que l’algorithme de tri rapide (QuickSort), dont la complexit´e dans le pire des cas est en O(n2), est le plus efficace en pratique. Comment est-ce possible ? Tout simplement parce que notre r´esultat ne concerne que la complexit´e dans le pire des cas. Or QuickSort est un algorithme enO(nlogn) en moyenne.

4 Arbres de syntaxe abstraite

4.1 Les expressions sont des arbres

Consid´erons une d´efinition des expressions arithm´etiques avec un œil neuf. Une expression arithm´etique eest :

ˆ un entier,

ˆ ou bien une op´eratione1ope2, o`u e1 ete2 sont des expressions arithm´etiques et opest un op´erateur (+,-,*et/).

L’œil neuf ne voit pas cette d´efinition comme celle de l’´ecriture usuelle (notation infixe) des expressions, et d’ailleurs il manque les parenth`eses. Il voit une d´efinition inductive, l’ensemble des expressions est solution de cette ´equation r´ecursive :

E =Z∪(E,+, E)∪(E,-, E)∪(E,*, E)∪(E,/, E)

Cette d´efinition inductive est une d´efinition d’arbre, les expressions sont des feuilles qui contiennent un entier ou des nœuds internes `a deux fils. Voir une expression comme un arbre ´evite toutes les ambigu¨ıt´es de la notation infixe. Par exemple, les deux arbres de la figure 15 disent clairement quels sont les arguments des op´erations + et * dans les deux cas. Alors qu’en notation infixe,

Fig. 15 – Deux arbres de syntaxe abstraite +

1 *

2 3

* +

1 2

3

pour bien se faire comprendre, il faut ´ecrire1+(2*3) et(1+2)*3.

D`es qu’un programme doit faire des choses un tant soit peu compliqu´ees avec les expressions arithm´etiques, il faut repr´esenter ces expressions par des arbres de syntaxe abstraite. Le terme ((abstraite))se justifie par opposition `a la syntaxe((concr`ete))qui est l’´ecriture des expressions, c’est-`a-dire ici la notation infixe. La production des arbres de syntaxes abstraite `a partir de la syntaxe concr`ete estl’analyse grammaticale(parsing), une question cruciale qui est ´etudi´ee dans le cours suivant INF 431.

4.2 Impl´ementation des arbres de syntaxe abstraite

Ecrivons une classe´ Exp des cellules d’arbre des expressions. Nous devons principalement distinguer cinq sortes de nœuds. Les entiers, qui sont des feuilles, et les quatre op´erations, qui ont deux fils. La technique d’impl´ementation la plus simple est de r´ealiser tous ces nœuds par des objets d’une seule classe Exp qui ont tous les champs n´ecessaires, plus un champ tag qui indique la nature du nœud.1Le champtagcontient un entier cens´e ˆetre l’une de cinq constantes conventionnelles.

class Exp {

final static int INT=0, ADD=1, SUB=2, MUL=3, DIV=4 ; int tag ;

// Utilis´e si tag == INT int asInt ;

// Utilis´es si tag ∈ {ADD, SUB, MUL, DIV}

Exp e1, e2 ;

Exp(int i) { tag = INT ; asInt = i ; } Exp(Exp e1, int op, Exp e2) {

tag = op ; this.e1 = e1 ; this.e2 = e2 ; }

}

Ainsi pour construire l’arbre de gauche de la figure 15, on ´ecrit : new Exp

(new Exp(1), ADD,

new Exp (new Exp(2), MUL, new Exp(3)))

C’est non seulement assez lourd, mais aussi source d’erreurs. On atteint ici la limite de ce qu’autorise la surcharge des constructeurs. Il est plus commode de d´efinir cinq m´ethodes statiques pour construire les divers nœuds.

static Exp mkInt(int i) { return new Exp (i) ; }

static Exp add(Exp e1, Exp e2) { return new Exp (e1, ADD, e2) ; } ..

.

static Exp div(Exp e1, Exp e2) { return new Exp (e1, DIV, e2) ; } Et l’expression d´ej`a vue, se construit par :

add(mkInt(1), mul(mkInt(2), mkInt(3))) Ce qui est plus concis, sinon plus clair.

Un exemple d’op´eration (( compliqu´ee )) sur les expressions arithm´etiques est le calcul de leur valeur. L’op´eration n’est compliqu´ee que si nous essayons de l’effectuer directement sur les notations infixes, car sur un arbreExpc’est tr`es facile.

1Une technique plus ´el´egante `a base d’h´eritage des objets est possible.

static int calc(Exp e) { switch (e.tag) {

case INT: return e.asInt ;

case ADD: return calc(e.e1) + calc(e.e2) ; case SUB: return calc(e.e1) - calc(e.e2) ; case MUL: return calc(e.e1) * calc(e.e2) ; case DIV: return calc(e.e1) / calc(e.e2) ; }

throw new Error ("calc : arbre Exp incorrect") ; }

L’instruction throw finale est n´ecessaire, car le compilateur n’a pas de moyen de savoir que le champtagcontient obligatoirement l’une des cinq constantes conventionnelles. En son absence, le programme est rejet´e par le compilateur. Pour satisfaire le compilateur, on aurait aussi pu renvoyer une valeur ((bidon )) par return 0, mais c’est nettement moins conseill´e. Une erreur est une erreur, en cas d’arbre incorrect, mieux vaut tout arrˆeter que de faire semblant de rien.

Dans cet exemple typique, il faut surtout remarquer le lien tr`es fort entre la d´efinition inductive de l’arbre et la structure r´ecursive de la m´ethode. La programmation sur les arbres de syntaxe abstraite est naturellement r´ecursive.

4.3 Traduction de la notation postfixe vers la notation infixe

Nous avons d´ej`a trait´e cette question de fa¸con incompl`ete, en ne produisant que des notations infixes compl`etement parenth´es´ees (exercice II.2). Nous pouvons maintenant faire mieux.

L’id´ee est d’abord d’interpr´eter la notation postfixe comme un arbre, puis d’afficher cet arbre, en tenant compte des r`egles usuelles qui permettent de ne pas mettre toutes les parenth`eses.

Pour la premi`ere op´eration il ne faut se poser aucune question, nous reprenons le calcul des expressions donn´ees en notation postfixe (voir II.1.2), en construisant un arbre au lieu de calculer une valeur. Nous avons donc besoin d’une pile d’arbres, ce qui est facile avec la classe des piles de la biblioth`eque (voir II.2.3).

static Exp postfixToExp(String [] arg) { Stack<Exp> stack = new Stack<Exp> () ; for (int k = 0 ; k < arg.length ; k++) {

Exp e1, e2 ;

String cmd = arg[k] ; i f (cmd.equals("+")) {

e2 = stack.pop() ; e1 = stack.pop() ; stack.push(add(e1,e2)) ;

} else i f (cmd.equals("-")) {

e2 = stack.pop() ; e1 = stack.pop() ; stack.push(sub(e1,e2)) ;

} else i f (cmd.equals("*")) {

e2 = stack.pop() ; e1 = stack.pop() ; stack.push(mul(e1,e2)) ;

} else i f (cmd.equals("/")) {

e2 = stack.pop() ; e1 = stack.pop() ; stack.push(div(e1,e2)) ;

} else {

stack.push(mkInt(Integer.parseInt(arg[k]))) ; }

}

return stack.pop() ; }

Examinons la question d’afficher un arbreExpsous forme infixe sans abuser des parenth`eses.

Tout d’abord, les parenth`eses autour d’un entier ne sont jamais utiles. Ensuite, on distingue deux classes d’op´erateurs, les additifs (+et-) et les multiplicatifs (*et/), les op´erateurs d’une classe donn´ee ont le mˆeme comportement vis `a vis du parenth´esage. Il y a cinq positions possibles : au sommet de l’arbre, et `a gauche ou `a droite d’un op´erateur additif ou multiplicatif. On examine ensuite l’´eventuel parenth´esage d’un op´erateur.

ˆ L’application des op´erateurs additifs doit ˆetre parenth´es´ee quand elle apparaˆıt comme se-cond argument d’un op´erateur additif (1-2+3s’interpr`ete comme(1-2)+3, il faut donc pa-renth´eser1-(2+3)), ou comme argument d’un op´erateur multiplicatif (consid´erer(1+2)*3 et1*(2+3)).

ˆ L’application des op´erateurs multiplicatifs doit ˆetre parenth´es´ee `a droite des op´erateurs multiplicatifs (mˆeme raisonnement que pour les additifs).

Ceci nous conduit `a regrouper les positions possible en trois classes (1) Sommet de l’arbre et `a gauche des additifs : ne rien parenth´eser.

(2) `A droite des additifs et `a gauche des multiplicatifs : ne parenth´eser que les additifs.

(3) `A droite des multiplicatifs : parenth´eser tous les op´erateurs.

On identifie les trois classes par 1, 2 et 3. On voit alors que les additifs sont `a parenth´eser pour les classes strictement sup´erieures `a 1, et les multiplicatifs pour les classes strictement sup´erieures `a 2. Ce qui conduit directement `a la m´ethode suivante qui prend en dernier argument un entier lvlqui rend compte de la position de l’arbree`a afficher dans la sortie out.

static void expToInfix(PrintWriter out, Exp e, int lvl) { switch (e.tag) {

case INT:

out.print(e.asInt) ; return ; case ADD: case SUB:

i f (lvl > 1) out.print(’(’) ; expToInfix(out, e.e1, 1) ;

out.print(e.tag == ADD ? ’+’ : ’-’) ; expToInfix(out, e.e2, 2) ;

i f (lvl > 1) out.print(’)’) ; return ;

case MUL: case DIV:

i f (lvl > 2) out.print(’(’) ; expToInfix(out, e.e1, 2) ;

out.print(e.tag == MUL ? ’*’ : ’/’) ; expToInfix(out, e.e2, 3) ;

i f (lvl > 2) out.print(’)’) ; return ;

}

throw new Error ("expToInfix : arbre Exp incorrect") ; }

La m´ethode expToInfix m´elange r´ecursion et affichage. Cela ne pose pas de difficult´e parti-culi`ere : pour afficher une op´eration il faut d’abord afficher le premier argument (r´ecursion) puis l’op´erateur et enfin le second argument (r´ecursion encore).

La sortie est unPrintWriterqui poss`ede une m´ethodeprintexactement commeSystem.out mais est bien plus efficace (voir B.5.5.1). Le code utilise une particularit´e de l’instructionswitch: on peut grouper les cas (ici des additifs et des multiplicatifs). Pour afficher l’op´erateur, on a re-cours `a l’expression conditionnelle (voir B.7.2). Par exemple e.tag == ADD ? ’+’ : ’-’vaut

’+’sie.tagest ´egal `aADDet’-’autrement — et ici((autrement))signifie n´ecessairement que e.tagest ´egal `a SUB puisque nous sommes dans un cas regroup´e du switchne concernant que ADD etSUB.

Voici finalement la m´ethode main de la classe Exp qui appelle l’affichage infixe sur l’arbre construit en lisant la notation postfixe

public static void main (String [] arg) {

PrintWriter out = new PrintWriter (System.out) ; Exp e = postfixToExp(arg) ;

expToInfix(out, e, 1) ;

out.println() ; out.flush() ; }

Les PrintWriter sont bufferis´es, il faut vider le tampon par out.flush() avant de finir, voir B.5.4. Reprenons l’exemple de la figure II.4.

% java Exp 6 3 2 - 1 + / 9 6 - ’*’

6/(3-2+1)*(9-6)

Ce qui est meilleur que l’affichage ((6/((3-2)+1))*(9-6)) de l’exercice II.2.