Rohan Crossland and Alex Lopez
Spencer Racca-Gwozdzik and Brett Garon
Brendan Bell and Ella Slattery
Mackenzie Leibee and Noah Zimmer
Devan Meyer and Lucas Takiff
Gabriel Guinn and Noah McCullough
Bonacic Bonacic and Jacob Endrina
Lucas Calaff and Cian Monaghan
Asa Bonner and Grey Roppolo
Jinwoo Choi and Noah Sprenger
Stephen Rice and Ethan Spence
Expressions like these can be represented by expression trees, in which the internal nodes contain operators, and the operands are at the leaves. The last expression above can be represented by the expression tree to the right.3 2.17 + 5.8 x * 3.14159 (3.14 - 1) * 2.0 (10 + foo) * (30 - bar + 50)
Expression trees are a convenient way to represent arithmetic expressions since the structure of the tree encodes the order in which the operators should be applied — no parentheses are needed. Operators at higher levels in the tree are applied only after those at lower levels, so the multiplication at the top of the tree above is the last operator to be applied. Note that its right subtree correctly shows that the subtraction should occur before the addition (we take the minus and plus operators from left to right in the original expression).
They're also convenient because an expression in tree form can be easily evaluated, using recursion: The root can evaluate its left and right subtrees, then combine the results as appropriate. For example, the multiplication at the top of the tree would evaluate its left and right subtrees, then multiply the two results and return the product. The process is recursive since the addition in the left subtree similarly asks that its two subtrees be evaluated, then adds the results, etc. A representation of an expression tree as a String can be produced by an in-order traversal of the tree like the one we did in class.
Previously we saw how we could use a stack to evaluate an expression, but the plan here is to represent a formula that could potentially contain the names of variables, and re-evaluate the expression any time the variables change.
"*"
, for example, and the leaves could be things like "10"
and "foo"
. That would store all the necessary information, but using it during an evaluation would be difficult. We would have to parse a node's string to see what kind of information it contained, and then convert it to a numeric value or operator as necessary.
Instead, we'll create a custom tree implementation just for this project that uses different node classes for each of the three kinds of nodes in the tree: Nodes that represent constant values (e.g. 6
), nodes representing variables (e.g. foo
), and nodes representing arithmetic operators (e.g. +
). Using a different node class for each kind of node also means that we can take an object-oriented approach to implementing the evaluation of a tree: Each node class can have a node-specific evaluate
method! A numeric value node can just return its value when it's asked to evaluate itself, and a variable node can look up its current value in a Map and return it. An operator node can call evaluate on its left and right subtrees, then combine those values appropriately. For example, a +
operator node would call evaluate on its left and its right subtrees, sum the values from those calls, and return the result.
Put another way, the nodes in our expression trees won't just store information, they'll do things as well. Each node in the tree will know how to evaluate itself. We'll make sure each kind of node implements toString
as well, so that we can print trees. The node classes will each implement the Evaluable
interface shown below, so that the code we write to use these expression trees is assured that every node supports the following operations:
package arithmeticExpression; import java.util.Map; public interface Evaluable { public double evaluate(Map<String, Double> env); public String toString(); }
Classes implementing this Evaluable
interface must have an evaluate
method, and override the toString
method. Do not modify the interface above to include any new methods.
The different node classes will contain different fields — for example, operator nodes will need to contain references to their left and right subtrees, but value nodes won't since they don't have children. Regardless of the node type, each will have to implement
evaluate
in the appropriate manner: Value nodes can
simply return their value, variable nodes will look up the variable's
value in the Map
that's passed to evaluate
,
and operator nodes will apply their operator to their subtrees' values.
Each will also implement toString
as
appropriate (variables return their names, operator nodes
return strings from their left subtree + operator + subtree, etc.). The output from toString
should be a fully parenthesized expression, formatted such that it could be successfully passed to the constructor described in the next section.
Expression
classExpression
class to encapsulate our expression trees, just like the book writes a
BinaryTree
class to hide the details of their linked tree
nodes. The Expression
class will contain some methods that involve the entire tree, not just individual nodes, and will have a constructor that produces complete expression trees from arithmetic expressions passed in as strings. The documentation for the Expression
class is here,
and gives you the full list of methods the class must implement.
Expression
class must build a
tree from an infix expression string. This is a challenge, since the
operator precedence and parenthesized subexpressions must be handled
properly — you can't just process the operators in the order
you find them in the input string. Luckily, you can stitch together
some code and algorthms from Chapter 3 to make this easier.
First, within the constructor you can convert parenthesized infix expressions to postfix
expression strings using the InfixToPostfixParens
class
from the book. (You do not need to understand how this code works.) This might not seem like a step in the right direction,
but at least the parenthesized subexpressions will be processed away and,
more importantly, the operators will now appear in the order in which
they need to be applied.
For example, the infix expression 5 * (1 + 2) - 6
would be
transformed into the postfix expression 5 1 2 + * 6 -
. In
the original expression, the parentheses tell us that the +
should be performed first, precedence says that the *
is
next, followed by the -
. Lo and behold, that's the order
in which they appear in the postfix string!
We can also borrow the stack-based algorithm we wrote in class for evaluating postfix expressions. It said we should push operands onto the stack until we came to an operator, then apply it to the top two items on the stack and push the result. You can use this same algorithm to build expression trees! Have the stack hold references to tree nodes: When you encounter a value or a variable, make a new tree node and push it onto the stack. When an operator is encountered, build a larger tree out of the operator and the two subtrees at the top of the stack, then push this new tree onto the stack. A reference to the full expression tree will be on the stack when we're done!
One word of warning though: The convert
method in
InfixToPostfixParens
gets confused unless there are
spaces separating the operators, operands, and parentheses.
We'll place these same restrictions on the input to the
Expression
constructor. Thus, the expression above would
actually have to be written as "5 * ( 1 + 2 ) - 6"
,
with spaces around the parentheses.
arithmeticExpression
. This will make it easier to plug this code in as part of your final assignment. Your project structure will look something like this:
Evaluable
. Before moving on, it wouldn't hurt to verify that you can stitch nodes together and get the evaluate
and toString
methods to work.
Expression
class to encapsulate your
expression trees. It must support the four methods shown in
the documentation:
The constructor, evaluate
, getVariables
,
and toString
. Your code should throw an
IllegalArgumentException
if, during evaluation, a
variable can't be found in the Map (either because the Map does not contain the variable, or because the Map argument is null).
Expression
class are
shown below. Note that the last three demonstrate exceptions being thrown.
> Expression e = new Expression("5.2"); > e.toString() "5.2" (String) > e.evaluate(null) 5.2 (double) > e = new Expression("1 + 2 * 3 - 4"); > e.toString() "( ( 1.0 + ( 2.0 * 3.0 ) ) - 4.0 )" (String) > e.evaluate(null) 3.0 (double) > e = new Expression("1 + 2 * ( 3 - 4 )"); > e.toString() "( 1.0 + ( 2.0 * ( 3.0 - 4.0 ) ) )" (String) > e.evaluate(null) -1.0 (double) > e = new Expression("1 + 5 / 3"); > e.toString() "( 1.0 + ( 5.0 / 3.0 ) )" (String) > e.evaluate(null) 2.666666666666667 (double) > e.getVariables() <object reference> (HashSet<String>) > e.getVariables().toString() "[]" (String) > import java.util.HashMap; > HashMapenv = new HashMap (); > env.put("foo", 17.8); > e = new Expression("foo"); > e.evaluate(env) 17.8 (double) > e = new Expression("foo + 1"); > e.toString() "( foo + 1.0 )" (String) > e.evaluate(env) 18.8 (double) > e = new Expression("foo + 1 * ( foo + 2.5 )"); > e.toString() "( foo + ( 1.0 * ( foo + 2.5 ) ) )" (String) > e.evaluate(env) 38.1 (double) > e.getVariables().toString() "[foo]" (String) > e = new Expression("java + puppies / java * foo + 1.5"); > e.toString() "( ( java + ( ( puppies / java ) * foo ) ) + 1.5 )" (String) > e.getVariables().toString() "[java, foo, puppies]" (String) > e.evaluate(env) Exception: java.lang.IllegalArgumentException (java) > e.evaluate(null) Exception: java.lang.IllegalArgumentException (java) > e = new Expression("1 + 2 3"); Exception: arithmeticExpression.InfixToPostfixParens$SyntaxErrorException (Too many operands in input expression)
arithmeticExpression
, it should be contained within a folder with that same name. Please zip up the arithmeticExpression
folder containing your code and submit the .zip archive via Canvas as you've done previously.