【问题标题】:Recursive evaluate() in expression tree class表达式树类中的递归评估()
【发布时间】:2014-12-04 19:23:39
【问题描述】:

我是 Java 新手,正在尝试将评估方法添加到我的课程中。 ExpTree 类和它的测试程序给了我。我按照我在课堂上学到的代码编写了代码,但不知道为什么它不起作用。

evaluate() 方法,它返回 ExpTree 的算术评估。这应该以递归方式完成,因此您将需要 2 种方法来完成它。在导致除以 0 或取模的情况下,它应该抛出一个带有描述性字符串的新 ArithmeticException。如果树为空,evaluate() 还应该抛出一个带有描述性字符串的新 ArithmeticException。

这是我的代码:

// This will implement an "Expression Tree" which stores an arithmetic expression

import java.util.*;

public class ExpTree
{
    //-------data
    private ExpNode root;

    //-------constructor
    public ExpTree()
    {
        root = null;
    }

    //constructor where a string is passed in.  It is parsed and stored
    public ExpTree(String expString)
    {
        //declare StringTokenizer, Stacks, and other variables used in parsing
        StringTokenizer tokenizer = new StringTokenizer (expString, "()+-*/%", true);
        String token;
        ExpNode operator, leftOperand, rightOperand;
        Stack<ExpNode> operators = new Stack<ExpNode>();
        Stack<ExpNode> operands = new Stack<ExpNode>();

        //break up expString into tokens
        while (tokenizer.hasMoreTokens())
        {
            token = tokenizer.nextToken();

            // if the current token is a left paren, ignore it
            if (token.equals ("("))
                ;

            // if the current token is an operator, put it on the
            // operator stack
            else if ((token.equals ("+")) || (token.equals ("-")) ||
                        (token.equals ("*")) || (token.equals ("/"))  || (token.equals ("%")))
                operators.push (new ExpNode(token));

            //if the current token is a right paren, pop the operators stack
            //to get the operator, pop the operands stack twice to get the two
            //operands (stored as expression trees).   Then make the two operands
            //children of the operator and push back on the operands tree.
            else if (token.equals (")"))
            {
                operator = operators.pop();

                rightOperand = operands.pop();
                leftOperand = operands.pop();

                operator.setLeft(leftOperand);
                operator.setRight(rightOperand);

                operands.push(operator);
            }

            //otherwise, the token should be a number - put it in the operands stack
            else
                operands.push (new ExpNode(token));

        } // while (tokenizer.hasMoreTokens())

        //when finished parsing, the operands stack should contain the fully-built
        //expression tree.
        if (!operands.isEmpty())
            root = operands.pop();
    }

    //-------methods

    //isEmpty()
    public boolean isEmpty()
    {
        return (root == null);
    }

    //printTree methods - prints the tree in RNL order, with indents.  Called from "outside"
    public void printTree()
    {
        if (root == null)
            System.out.println("The tree is empty");
        else
            printTree(root, 0);    //start with the root with 0 indentations
    }

    //recursive, private version of printTree
    private void printTree(ExpNode subTree, int indents)
    {
        //if there is a right side, handle it first (with 1 more indent)
        if (subTree.getRight() != null)
            printTree(subTree.getRight(), indents+1);

        //then print the node itself (first move over the right amount of indents)
        System.out.println("\n\n\n");
        for (int i=0; i<indents; i++)
            System.out.print("\t");
        System.out.println(subTree);

        //if there is a left side, handle it first (with 1 more indent)
        if (subTree.getLeft() != null)
            printTree(subTree.getLeft(), indents+1);
    }

    //inorder traversal - starts the recursive calls to print inorder
    public String inOrder()
    {
        return inOrder(root);
    }

    //inorder traversal - recursive left side of tree, print node, right side of tree
    private String inOrder(ExpNode theTreeToTraverse)
    {
        if (theTreeToTraverse == null)
            return "";   //don't try to do anything if tree is null

        //else build up a String to return.  It will involve recursive calls
        String returnString = "";
        if (theTreeToTraverse.getLeft() != null)
        {
            returnString += "(" + inOrder(theTreeToTraverse.getLeft());
        }
        returnString += theTreeToTraverse;
        if (theTreeToTraverse.getRight() != null)
        {
            returnString += inOrder(theTreeToTraverse.getRight()) + ")";
        }

        return returnString;
    }

    //public version of evaluate  
    public double evaluate(){
    if (root == null)       //Am I null?
    throw new ArithmeticException("The tree is empty, nothing to be evaluated!");

    else                    //You handle it!
        return recursiveEvaluate(root);  
    }

    //Recursive version of evaluate
    private double recursiveEvaluate(ExpNode subTree){
        //If subTree is empty
        if (subTree == null)
            return 0;

    //What are you subTree? A number? An operator? 
    else if(subTree.getData().equals("+"))
        return recursiveEvaluate(subTree.getLeft()) +
                recursiveEvaluate(subTree.getRight()) ;

    else if(subTree.getData().equals("-"))
        return recursiveEvaluate(subTree.getLeft()) -
                recursiveEvaluate(subTree.getRight()) ;

    else if(subTree.getData().equals("*"))
        return recursiveEvaluate(subTree.getLeft()) *
                recursiveEvaluate(subTree.getRight()) ;

    else if(subTree.getData().equals("/")){
        double right = recursiveEvaluate(subTree.getRight());

    if(right == 0.0)
        throw new ArithmeticException("Divide by zero is undefined!");
            return recursiveEvaluate(subTree.getLeft()) / right;
    }

    else if(subTree.getData().equals("%")){
        double right = recursiveEvaluate(subTree.getRight());

    if(right == 0.0)
        throw new ArithmeticException("Mod by zero exception");
            return recursiveEvaluate(subTree.getLeft()) % right;
    }

    //Converting String type to double
    else
        return Double.parseDouble(subTree.getData());

    }

    //Public version of numPlus
    public int numPlus(){
        return recursiveNumPlus(root);
    }

    //Recursive version of numPlus
    private int recursiveNumPlus(ExpNode subTree){
        if (subTree == null)
            return 0;

    //If you are a '+' sign
    if(subTree.getData().equals("+"))
        return recursiveNumPlus(subTree.getLeft()) +
                recursiveNumPlus(subTree.getRight()) + 1;

    else
        return recursiveNumPlus(subTree.getLeft()) +
                recursiveNumPlus(subTree.getRight());
    }

}

//***************************************************************************
// ExpNode holds a "node" for an ExpTree.
class ExpNode
{
    //data
    private String data;
    private ExpNode left;
    private ExpNode right;

    //constructor
    public ExpNode(String el)
    {
        data = el;
        left = right = null;
    }

    //methods
    //toString() - this is how an ExpNode represents itself as a String
    public String toString()
    {
        return data;
    }

    //getLeft - returns the reference to the left subTree
    public ExpNode getLeft()
    {
        return left;
    }

    //getRight - returns the reference to the right subTree
    public ExpNode getRight()
    {
        return right;
    }

    //getData - returns the data (could be an operator or a number, so returns as a String)
    public String getData()
    {
        return data;
    }

    //setLeft - sets the left subTree to whatever is passed in
    public void setLeft(ExpNode newNode)
    {
        left = newNode;
    }

    //setRight - sets the right subTree to whatever is passed in
    public void setRight(ExpNode newNode)
    {
        right = newNode;
    }

}

【问题讨论】:

  • 树实际存储了什么样的东西,评估它意味着什么?
  • 只是数字、int 和 double。这意味着如果树中有任何数学运算,此方法必须对其进行评估!例如,如果我们通过 3 + 5,我们应该得到 8 作为输出!
  • 如果你能展示一个完整的课程会更容易提供帮助。 recursiveEvaluateExpNode 的成员吗? getLeftgetRight 是否返回另一个 ExpNode?如果是这样,您的代码应该看起来像return this.getLeft().evaluate() + this.getRight().evaluete()。在这种情况下,您不需要两个功能。或者你可能应该实现 Visitor Pattern
  • 所以是数字和运算符?数字有不同的数据类型?看起来您已经编写了一些很好的代码来计算树中的节点数,但您也需要以某种方式查看数字和运算符。
  • @5gon12eder 实际上,Ava 的结构方式与您的建议相同。问题是缺少很多东西——不是recursiveEvaluate 是否应该有ExpNode 作为参数。

标签: java recursion methods expression-trees evaluate


【解决方案1】:

解决问题的面向对象方法是为每种节点定义一个专用类型。为了保持这个答案的长度合理并避免做作业,我将只展示一个仅涉及加法和乘法的整数表达式的最小示例。

第一步是定义表达式节点必须提供的内容。为此,我们定义了接口ExprNode。如果您还没有在课堂上学习多态性(这让我感到惊讶),您可能现在想停止阅读并在了解后回来。

我们想要评估节点,因此我们将添加一个evaluate 方法,该方法应该返回以该节点为根的子表达式的值。我们将把它的实现推迟到特定的节点类,因为它们最清楚如何评估自己。

我们还想格式化表达式,所以我们将添加另一种方法来格式化中缀表示法的子表达式。

public interface ExprNode {
    int evaluate();
    String asInfixString();
}

现在,让我们看看我们需要哪些节点。当然,任何表达式的叶子都会包含数字,所以我们最好开始为它们定义一个类。 ValueNode的实现真的很简单,不是说微不足道的。

public final class ValueNode implements ExprNode {

    private final int value;

    public ValueNode(final int value) {
        this.value = value;
    }

    @Override
    public int evaluate() {
        return this.value;
    }

    @Override
    public String asInfixString() {
        return String.valueOf(this.value);
    }
}

接下来,我们有两个二元运算+*。各个类的实现也很简单。

public final class PlusNode implements ExprNode {

    private final ExprNode lhs;
    private final ExprNode rhs;

    public PlusNode(final ExprNode lhs, final ExprNode rhs) {
        this.lhs = lhs;
        this.rhs = rhs;
    }

    @Override
    public int evaluate() {
        return this.lhs.evaluate() + this.rhs.evaluate();
    }

    @Override
    public String asInfixString() {
        return String.format("(%s) + (%s)",
                             this.lhs.asInfixString(),
                             this.rhs.asInfixString());
    }
}
public final class TimesNode implements ExprNode {

    private final ExprNode lhs;
    private final ExprNode rhs;

    public TimesNode(final ExprNode lhs, final ExprNode rhs) {
        this.lhs = lhs;
        this.rhs = rhs;
    }

    @Override
    public int evaluate() {
        return this.lhs.evaluate() * this.rhs.evaluate();
    }

    @Override
    public String asInfixString() {
        return String.format("(%s) * (%s)",
                             this.lhs.asInfixString(),
                             this.rhs.asInfixString());
    }
}

有了它,我们可以优雅地构建表达式树,打印和评估它们。这是表达式2 * (3 + 4) 的示例。

ExprNode expr = new TimesNode(
        new ValueNode(2),
        new PlusNode(new ValueNode(3), new ValueNode(4)));
System.out.println(expr.asInfixString() + " = " + expr.evaluate());

它将打印(2) * ((3) + (4)) = 14

因此,对于您的ExprTree,您只需检查root != null 是否为return root.evaluate()

如果我们想要更多表达式怎么办?

显然,我们将定义ExprNode 的另一个子类型。例如,我们可以定义更多的二元运算符来处理减法和除法,另一个一元节点用于一元减号等等。这些类中的每一个都必须实现ExprNode 规定的接口,因此任何子类都可以以相同的方式使用,封装了它如何评估自身的逻辑。

如果我们想要更多操作怎么办?

例如,我们可能希望以后缀表示法格式化表达式。为了能够做到这一点,我们可以将另一个方法 asPostfixString 添加到 ExprNode。然而,这有点尴尬,因为这意味着我们必须去编辑到目前为止我们已经实现的所有子类,添加新的操作。

这是一个相当基本的问题。如果你布局一个复合结构来封装节点中的操作,那么它使用起来很优雅,添加新的节点类型很简单,但很难添加模式操作。如果您在每个操作中都使用案例选择(有点像在您的代码中),那么添加新操作会更简单,但操作代码会变得复杂,并且很难添加更多节点类型(所有操作的代码都需要被改变)。这种困境被称为tyranny of the dominant model decompositionvisitor pattern 是一种突破的尝试。

无论如何,如果你正在学习一个基本的 Java 课程,我认为你应该学习的是实现一个多态树,并在节点中定义操作,如上所示。

【讨论】:

  • 非常感谢。这太棒了!非常有帮助和清晰!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-16
  • 2020-07-10
相关资源
最近更新 更多