Home

TD 6 - Expressions arithmétiques et arbre

Dans ce TD, il s'agit de manipuler des expressions arithmétiques représentées sous forme d'arbres de manière similaire à ce qui a été vu en amphi 5 (voir le transparent 37). Une expression pourra contenir des entiers, des opérations et aussi des variables. Les variables seront identifiées par un seul caractère, de 'a' à 'z'. Pour limiter les problèmes d'arithmétique, on n'utilisera que les 3 opérateurs +, - et *. Chaque expression constitue donc un arbre, ainsi 6 * ((3-2) + 1) * (9 - 6) * 2 peut-être représenté par l'arbre ci-dessous :

En terminologie d'arbre, les entiers et les variables sont des feuilles (ils n'ont pas de fils) et les opérations sont des noeuds internes.

1.  Codage

On va définir une classe Expr dont chaque objet correspond à un noeud d'une expression. On propose dans ce TD deux options de codage pour différencier les trois natures d'expression. La première plus simple consiste à ne définir qu'une seule classe avec un champ nature indiquant la nature (entier, opération ou variable). Si vous vous sentez à l'aise avec les interfaces et les méthodes non statiques, vous pouvez choisir la deuxième option consistant à définir Expr comme une interface avec trois classes différentes la réalisant.

1.1  Codage par une seule classe (« Option 1.1 »)

Un objet de la classe Expr contiendra un champ nature indiquant la nature de l'expression (entier, opération ou variable). Pour plus de lisibilité, on utilisera trois constantes INT, OP et VAR pour identifier ces trois natures. On pourra ainsi effectuer un traitement en fonction de la nature d'une expression e par la syntaxe :

  switch (e.nature) {
    case Expr.INT: ... break ;
    case Expr.OP: ... break ;
    case Expr.VAR: ... break ;
    default: throw new Error ("Nature inconnue : " + e.nature) ;
  }

Rappelons que dans une utilisation rationnelle du switch, toute suite de une ou plusieurs instructions d'un case doit se terminer soit par un break (qui sort du switch), soit par un return ... (qui sort de l'appel de fonction en cours).

Un objet de la classe Expr contiendra de plus un champ entier (utilisé dans le cas d'un entier), un champ caractère (utilisé dans le cas d'une variable), un autre champ caractère (utilisé dans le cas d'une opération) et deux champs expressions, ses fils gauche et droit.

La classe Expr aura 3 constructeurs :

Écrire la définition de la classe Expr avec les trois constructeurs requis. Voici deux exemples d'utilisation :

class Calcul
{
  public static void main (String[] args) {
    Expr e1 = new Expr( '*', new Expr('x'), new Expr(10));
    Expr e2 = new Expr( '*',
                        new Expr( '*', 
                                  new Expr( '*', 
                                            new Expr(6), 
                                            new Expr( '+', 
                                                      new Expr( '-',
                                                                new Expr(3),
                                                                new Expr(2)),
                                                      new Expr(1))),
                                  new Expr( '-',
                                            new Expr(9),
                                            new Expr(6))),
                        new Expr(2));
  }
}

1.2  Codage avec une interface (« Option 1.2 »)

Définir une interface Expr avec la syntaxe interface Expr { ... } (les méthodes à définir dans cette interface seront explicitées au fur et à mesure des questions suivantes). Définir trois classes ExprInt, ExprVar, et ExprOp implémentant cette interface (avec la syntaxe class ExprInt implements Expr { ... }). Chacune de ces classes aura un constructeur permettant de construire respectivement un entier, une variable ou une opération. On pourra ainsi construire les deux exemples ci-dessous :

class Calcul
{
  public static void main (String[] args) {
    Expr e1 = new ExprOp( '*', new ExprVar('x'), new ExprInt(10));
    Expr e2 = new ExprOp( '*',
                        new ExprOp( '*', 
                                  new ExprOp( '*', 
                                            new ExprInt(6), 
                                            new ExprOp( '+', 
                                                      new ExprOp( '-',
                                                                new ExprInt(3),
                                                                new ExprInt(2)),
                                                      new ExprInt(1))),
                                  new ExprOp( '-',
                                            new ExprInt(9),
                                            new ExprInt(6))),
                        new ExprInt(2));
  }
}

2.  Manipulation d'expressions

Dans cette partie, les signatures des méthodes (c'est à dire le type des variables prises en arguments ainsi que le type de ce qui est retourné) sont volontairement omises. Le premier travail pour chaque question consiste donc à deviner une signature correcte. Dans le cas de l'option 1.1, il est impératif d'utiliser un style similaire à l'utilisation du switch décrite plus haut.

On pourra au choix utiliser des méthodes statiques ou des méthodes non statiques (plus naturelles notamment pour l'option 1.2). Les indications en vert (comme ceci) ne concernent que ceux ayant choisi l'option 1.2 (avec interface).

Une solution par l'option 1.1.

3  Conversion en chaîne de caractères

Définir la méthode public String toString () de votre (ou vos) classe(s) qui renverra une chaîne codant l'expression sous forme préfixe. Rappelons que l'on peut convertir un entier i en chaîne de caractères par "" + i (de même pour un caractère c avec "" + c).

Pour une implémentation plus efficace, utiliser maintenant la classe StringBuffer. (Voir aussi les transparents 5 à 8 de l'amphi 6.) Écrire une méthode addToString qui ajoute une description de l'expression à un StringBuffer passé en argument. Ainsi, on pourra renommer la méthode toString précédente en toString2 et utiliser :

  public String toString () {
    StringBuffer sb = new StringBuffer () ;
    addToString (sb) ;
    return sb.toString () ;
  }

(Dans le cas de l'option 1.2 (avec interface), on remarquera qu'il n'est pas utile de modifier les méthodes toString de toutes les classes.)

Ceux qui ont utilisé l'option 1.1 jusqu'ici peuvent passer à l'option 1.2 ici. On pourra alors traiter en priorité dans la partie 2 les questions 2.1 (dans le cadre de toString comme ci-dessus), et 2.6.

Comparer la rapidité des deux méthodes toString sur le fichier bigexpr.txt contenant une expression sous forme préfixe. On utilisera comme au TD 3 des appels à TC.demarrerChrono() et System.err.println (TC.tempsChrono() + " millisecondes"). Si votre programme Calcul lit une expression au clavier, vous pouvez lire l'expression contenue dans bigexpr.txt par la commande :

java Calcul < bigexpr.txt

Une solution par l'option 1.2.

4.  Avant de partir

Déposez vos fichiers en utilisant la ligne de commande :

/users/profs/info/Depot/INF_421/deposer vos_fichiers_java TD_6 votre_groupe

vos_fichiers_java est la liste des fichiers que vous déposez (séparés par des espaces) et votre_groupe est votre numéro de groupe en informatique (entre 1 et 6).

5.  Supplément

Quelques exercices de plus.

Last modified: Thu Sep 30 09:51:38 CEST 2004