Saint Louis University |
Computer Science A220/P126
|
Dept. of Math & Computer Science |
Please see the general programming webpage for details about the programming environment for this course, guidelines for programming style, and details on electronic submission of assignments.
For this assignment, you are allowed to work with one other student if you wish (in fact, we suggest that you do so). If any student wishes to have a partner but has not been able to locate one, please let the instructor know so that we can match up partners. You will note that there are two distinct implementation approaches required for this assignment. It may be that the partnership divides the work by having one person do one implementation, and one person another (with consultation as needed). Alternatively both pieces could be developed side-by-side.
Please make sure you adhere to the policies on academic integrity in this regard.
The goal of this program will be to turn a parenthesized expression
such as:
((((3+1)*3)/((9-5)+2))-((3*(7-4))+6))
into a valid binary tree, which can then be used to evaluate the
underlying expression.
In terms of programming techniques, this assignment will explore the use of inheritence, defining a new class ExpressionTree which is a specialization of the more general BinaryTree class matching that of the text. You will not need to write any of the low-level code for representing the tree, but you will need to understand how to make use of the interface to the BinaryTree class in properly extending it for this purpose.
Parsers for languages such as C++ must certainly be able to perform such a task. Arithmetic expression appear in source code and must be evaluated in turn. Example 6.5 on pages 258--259 of the text discusses the use of such arithmetic expression trees.
In general, parsers deal with a variety of complications, as they allow unary operators (such as "-3"), and operators which take more than two (such as "4+8+2+12"). Furthermore, they enforce a prescribed precedence for operators in the case where the user does not properly parenthesize an expression (imagine "5 - 3 * 4 + 10").
We will consider a simple version of the problem by using only binary operators and by assuming strict parenthesization in expressions. In particular we will make the following recursive definition for a well-formed expression:
A string representing an integer is a well-formed expression (e.g. "5")
If expr1 is well-formed, expr2 is well-formed, and op is a binary operator ("+", "-", "*" or "/"), then the string "(expr1 op expr2)" is a well-formed expression.
This simple recursive definition allows us to represent arbitrarily complex arithmetic expression (and understanding this recursion will allow you to write a relative simple solution to this assignment!)
You may assume that your routine will be given WELL FORMED expressions. You do not need to be concerned with how to gracefully handle improperly formed expressions.
Examples of well-formed expressions include (one per line):
13
(5 - 8)
(3 * (4/2))
((((3+1)*3)/((9-5)+2))-((3*(7-4))+6))
To save you some effort, we provide a simple text-based driver, ExpressionDriver for this assignment. If entering input from the keyboard, the user is expected to enter a well-formed expression on a single line and press return. (the driver will not explicitly check the well-formed condition; but you may assume the user enters well-formed expressions).
The driver does the low-level input, breaking the line of input into an array of what we term tokens An individual token is either a parenthesis, an operator symbol, or a numeric value. Please see the Token.h file for a self-explanatory overview of that class. After breaking the expression into an array of tokens, the driver then asks you to construct an expression tree which corresponds to this input. After doing so, it calls additional routines to echo the original expression and to evaluate the expression. The user may then continue by typing another expression, again on a single line. To exit the program, enter "Q" and hit return.
As usual, if you prefer to type your input into a file for testing, you may give a filename to the driver as a single runtime argument.
Rather than having you create a class from scratch for representing an expression, we will have you make use of an existing BinaryTree class, modeled after the interface described in our text. Specifically, we have implemented such a class which includes all of the general Tree methods described in Chapter 6.1.2 as well as the specialized BinaryTree methods described in Chapter 6.3.1. In general, most of these methods combine to provide only an inspectable binary tree.
Please note that the constructor generates a new tree with a single external node which initially serves as the root. The element stored at the note will be a default object.
Since you will need the ability to modify your underlying tree, we have implemented the following update methods, some of which are discussed on page 294 of the text (though some are not):
replaceElement(const Position& p, const Object& element)
Replaces the position's current element with the provided element.
expandExternal(const Position& p)
Takes an external position p and converts
it to an internal node by creating two new (external) children.
A BoundaryViolationException will be thrown if p
is internal.
replaceExternalWithSubtree(const Position& p, BinaryTree& T2)
This method replaces the external position
v with a new subtree which is based upon the entire
contents of parameter T2. Please note that tree
T2 is itself destroyed by this action.
A BoundaryViolationException will be thrown if p
is internal.
removeAboveExternal(Position w)
Takes an external position w of
T, and deletes w and the parent of w from
the tree, promoting the sibling of w into the parents
place (see Figure 6.13 on page 276).
Rather than having you design a brand new class, presumably with a BinaryTree as a private data member, we have decided to use this assignment to explore a natural use of inheritence. You will be implementing a ExpressionTree class which is really a specialization and extension of the BinaryTree class. Please review Ch. 2.2.1 for a detailed discussion of inheritance in C++.
The ExpressionTree class is a specialization of the BinaryTree class because the original BinaryTree is templated to hold any type of Object as an element, whereas an expression tree is defined specifically to hold elements of type Token.
The ExpressionTree class extends the BinaryTree base class by supporting the following additional functionality. Your primary task is to implement each of these new functions.
This constructor takes the array of tokens, and generates an
ExpressionTree representing the original
expression. In particular, every internal node should
contain an element which is the token representing an
operator. Every external node should contain a token
which represents an integer. (The parentheses from the
original expression do not explicitly appear in the tree,
rather they provide the context for the expression.)
Please think about this task, and work on some examples, before you write any code. It is also possible to write a relatively simple recursive program which works perfectly. It is possible to implement this routine directly, and without recursion, but doing so may quickly become quite complicated.
The overloaded output operator should be used to print the
parenthesized version of the expression. It should be very easy
to do using an underlying recursive tree traversal procedure.
In fact, pseudo-code for the routine is given in
evaluate()
This routine should evaluate the underlying arithmetic
expression, returning the final results.
Again, this should be rather easy if using a recursive approach.
In fact, pseudo-code for the routine is given in
Note: because we are dealing with integer operands, we will interpret the division operator '/' as the integer division. So 5/2 = 2, and 4/7 = 0. This is consistent with C++'s behavior if you are using int values throughout an expression.
The files you need for this assignment can be downloaded here.
These include the following, though you will not need to modify, or even read, many of them. We will briefly discuss the purpose of each file:
ExpressionTree.h, ExpressionTree.cpp
These provide a stub for the ExpressionTree class which
you must implement.
BinaryTree.h, BinaryTree.tcc
These files define the base class, BinaryTree. You
should not be concerned with the low-level implementation of
this class. In fact, the access controls have been set in a way
which disallows your direct access to the low-level private
members. You must rely on the binary tree interface, as
discussed earlier in this assignment.
Token.h, Token.cpp
A very simple class to represent a "token" object (which is
either a parentheses, an operator or a numeric value).
ExpressionDriver.cpp
The main driver for the assignment.
VariousExceptions.h
There are a variety of exception classes involved in this
assignment. Rather than define each in its own file, we've
bundled them all together for convenience.
makefile
This makefile should allow you to rebuild your project by
simply typing 'make' rather than in invoking the compiler
directly.
You should submit:
ExpressionTree.h, ExpressionTree.cpp
readme - a brief summary of your program, and any further comments you wish to make to the grader. If you do the extra credit, please make this clear.
One of the beauties of binary trees are that they lead very nicely to the use of recursion. A subtree of a binary tree appears very much to be its own tree. In terms of implemenation however, there is one catch. If you call a method such as T.leftChild(v) for a given tree T and position v, it does not actually return to you an object of class BinaryTree; it returns to you a Position within the original tree T. If you look carefully at the many code examples in Chapter 6 of the text, they base their recursion on the concept of a subtree.
The representation of a subtree is generally modeled based upon
two references, one to the full tree and one to a position of that
tree which is treated as the root of a subtree. In many examples of
the text, such as
Of course the use of recursion is an implementation detail which is encapsulated from outside users. For example, in this assignment, we have asked for a routine evaluate(). Notice that this routine does not accept a parameter v to represent the position of the root of a subtree, even though such a parameter is needed for the recursive implementation. The solution is to have the public method serve as a wrapper for a private recursive procedure. Since the public method involves the full tree, you can simply translate this to start the recursive function with the root of the original tree as the parameter defining a subtree's root.
What we care most about in this program is that you get it working correctly. Some may be interested in thinking a bit about the efficiency of their routines. The straightforward implementations of the output operator and the evaluation are O(n) worst-case.
A straightforward recursive implementation of the constructor may lead to a worst case running time of n2, where n is the number of tokens in the expression. Though not required, we will point out that it is possible to implement the constructor in O(n) worst-case time. This can be done using a non-recursive implementation, and it can even be done using a recursive implementation, though it requires a good deal of thought into how to precisely define the recursion. Even the extra credit can be done in O(n) time with care.
For the original assignment, we guaranteed that expressions were well-formed, and fully parenthesized. For extra credit, we ask that you write an alternate form of the constructor which accomplishes the following
Note that our files declare a separate constructor for the extra credit, so that we can still grade your original constructor without fear that your attempt at extra credit caused your original working program to misbehave.
We define a more general grammer for expression as follows:
A string representing an integer is a general expression (e.g. "5")
If expr1 is a general expression, then the string: "(expr1)" is also a general expression.
If expr1 is a general expression, then the string: "-expr1" is also a general expression.
If expr1 is a general expression, then the string: "(expr1)" is also a general expression.
If expr1 and expr2 are both general expressions, and op is a binary operator ("+", "-", "*" or "/"), then the string: "expr1 op expr2" is a general expression.
In parsing general expression, you must follow C++'s rules for precedence of operators, namely that:
Parentheses have the highest precedence
Unary negation has precedence over all binary operators (it can be represented in the tree as zero minus the operand).
Operators "*" and "/" take precedence over operators "+" and "-".
Operators with equivalent precedence are evaluated from left to right.
If you compare this definition to the original, you will notice that it includes all original well-formed expression and many more. Examples of such general expressions include:
1+2-3+4 = (((1+2)-3)+4) = 4 (5)+7 = (5+7) = 12 5-3*4+10 = ((5-(3*4))+10) = 3 5-3*(4+10) = (5-(3*(4+10))) = -37 -(4+3) = (0-(4+3)) = -7 7*-4+3 = ((7*(0-4))+3) = -25
To test the extra credit version of the constructor, execute the
driver with two command-line arguments, the first of which is an
inputfile name and the second of which is "e" (or any argument in
reality). That is,
We will award up to 2 extra points for this task, though we are not in any way implying that this is a minor feat to accomplish. Please make sure that the readme file announces your success.