What is an expression tree?
Exercise 1: Draw an expression tree for the expression 10/4 + 2*(5+1). Compute the values at each non-leaf node.
We will now look at a variety of issues related to expression trees:
Let's start with building the tree:
Exercise 2: Convert the expression 10/4 + 2*(5+1) into the form above, with complete parentheses.
We will build the tree as we go along in the parsing:
class ExprTreeNode { ExprTreeNode left, right; // The usual pointers. boolean isLeaf; // Is this a leaf? int value; // If so, we'll store the number here. char op; // If not, we need to know which operator. }
Algorithm: parse (e) Input: expression string e Output: root of expression subtree for e 1. node = make new tree node. // Bottom-out: it's a number 2. if e is a number 3. node.value = number extracted from string 4. return node 5. endif // Recurse 6. node.op = extract operator from e 7. node.left = parse (substring of e to the left of op) 8. node.right = parse (substring of e to the right of op) 9. return node
// We need this for the Stack class. import java.util.*; class ExprTreeNode { ExprTreeNode left, right; // The usual pointers. boolean isLeaf; // Is this a leaf? int value; // If so, we'll store the number here. char op; // If not, we need to know which operator. } class ExpressionTree { ExprTreeNode root; public void parseExpression (String expr) { root = parse (expr); } // This is the recursive method that does the parsing. ExprTreeNode parse (String expr) { ExprTreeNode node = new ExprTreeNode (); // Note: expr.charAt(0) is a left paren. // First, find the matching right paren. int m = findMatchingRightParen (expr, 1); String leftExpr = expr.substring (1, m+1); // Bottom-out condition: if (m == expr.length()-1) { // It's at the other end ⇒ this is a leaf. String operandStr = expr.substring (1, expr.length()-1); node.isLeaf = true; node.value = getValue (operandStr); return node; } // Otherwise, there's a second operand and an operator. // Find the left paren to match the rightmost right paren. int n = findMatchingLeftParen (expr, expr.length()-2); String rightExpr = expr.substring (n, expr.length()-1); // Recursively parse the left and right substrings. node.left = parse (leftExpr); node.right = parse (rightExpr); node.op = expr.charAt (m+1); return node; } int findMatchingRightParen (String s, int leftPos) { // Given the position of a left-paren in String s, // find the matching right paren. // Recognize the code? Stack<Character> stack = new Stack<Character>(); stack.push (s.charAt(leftPos)); for (int i=leftPos+1; i < s.length(); i++) { char ch = s.charAt (i); if (ch == '(') { stack.push (ch); } else if (ch == ')') { stack.pop (); if ( stack.isEmpty() ) { // This is the one. return i; } } } // If we reach here, there's an error. System.out.println ("ERROR: findRight: s=" + s + " left=" + leftPos); return -1; } int findMatchingLeftParen (String s, int rightPos) { // ... similar ... } int getValue (String s) { try { int k = Integer.parseInt (s); return k; } catch (NumberFormatException e) { return -1; } } } // end-ExpressionTree
Exercise 3: Show how the recursion works for the expression (((35)-((3)*((3)+(2))))/(4)). That is, draw the sequence of "stack snapshots" showing recursive calls to the parse() method.
Next, let's write code for computing the value of the expression after it's been parsed:
Algorithm: computeValue (node) Input: a root of a subtree Output: the value of the subtree 1. if node is a leaf 2. return the node's value // The number that the node represents 3. endif 4. leftValue = computeValue (node.left) 5. rightValue = computeValue (node.right) 6. result = apply node.op to leftValue and rightValue 7. return result
class ExpressionTree { // ... public int computeValue () { return compute (root); } int compute (ExprTreeNode node) { if (node.isLeaf) { return node.value; } // Otherwise do left and right, and add. int leftValue = compute (node.left); int rightValue = compute (node.right); if (node.op == '+') { return leftValue + rightValue; } else if (node.op == '-') { return leftValue - rightValue; } else if (node.op == '*') { return leftValue * rightValue; } else { return leftValue / rightValue; } } // ... }
public static void main (String[] argv) { // A test expression: String s = "(((35)-((3)*((3)+(2))))/(4))"; // Create a parser instance. ExpressionTree expTree = new ExpressionTree (); // Parse the expression (which creates the tree). expTree.parseExpression (s); // Compute the value and print. int v = expTree.computeValue (); System.out.println (s + " = " + v); }
Why did we do all this?
What are they?
Exercise 4: Convert the expression 10/4 + 2*(5+1) into postfix form.
Postfix expressions are evaluated using a stack:
Example: let's evaluate the expression 35 3 3 2 + * - 4 /
Exercise 5: Convert the expression 10/4 + 2*(5+1) into postfix form and show the steps in evaluating the expression using a stack. Draw the stack at each step.
Why is this useful?
push 35 push 3 push 3 push 2 add mult sub push 4 divide
Exercise 6: Convert the expression 10/4 + 2*(5+1) into postfix form and write down the push/arithmetic "instructions" corresponding to this expression.
Now let's write code to create postfix from an expression tree and then to evaluate it using a stack:
class ExpressionTree { ExprTreeNode root; public String convertToPostfix () { String str = postOrder (root); return str; } String postOrder (ExprTreeNode node) { String result = ""; if (node.left != null) { result += postOrder (node.left); } if (node.right != null) { result += " " + postOrder (node.right); } if (node.isLeaf) { result += " " + node.value; } else { result += " " + node.op; } return result; } } //end-ExpressionParser
class PostfixEvaluator { public int compute (String postfixExpr) { // Create a stack: all our operands are integers. Stack<Integer> stack = new Stack<Integer>(); // Use the Scanner class to help us extract numbers or operators: Scanner scanner = new Scanner (postfixExpr); while (scanner.hasNext()) { if (scanner.hasNextInt()) { // It's an operand ⇒ push it on the stack. int N = scanner.nextInt(); stack.push (N); } else { // It's an operator ⇒ apply the operator to the top two operands String opStr = scanner.next(); int b = stack.pop(); // Note: b is popped first. int a = stack.pop(); if (opStr.equals("+")) { stack.push (a+b); } else if (opStr.equals("-")) { stack.push (a-b); } else if (opStr.equals("*")) { stack.push (a*b); } else if (opStr.equals("/")) { stack.push (a/b); } } } // end-while // Result is on top. return stack.pop(); } } //end-PostfixEvaluator
Let's revisit our binary tree map:
class TribeInfo { // ... same as before ... } public class BinaryTreeMapExample3 { public static void main (String[] argv) { // Create an instance. BinaryTreeMap3 tree = new BinaryTreeMap3 (); // Put some key-value pairs inside. TribeInfo info = new TribeInfo ("Ewok", 3, "Endor"); tree.add ( new KeyValuePair (info.name, info) ); // ... add the remaining data ... } }
// Get all the key-value pairs: KeyValuePair[] tribes = tree.getAllKeyValuePairs (); // Print. for (int i=0; i < tribes.length; i++) { System.out.println (tribes[i]); }
How do we implement the method getAllKeyValuePairs()?
public class BinaryTreeMap3 { TreeNode root = null; int numItems = 0; KeyValuePair[] allPairs; // We will place the elements in this array. int currentIndex = 0; // ... other methods like add(), contains() ... etc public KeyValuePair[] getAllKeyValuePairs () { if (root == null) { return null; } allPairs = new KeyValuePair [numItems]; inOrderTraversal (root); return allPairs; } void inOrderTraversal (TreeNode node) { // Visit left subtree first. if (node.left != null) { inOrderTraversal (node.left); } // Then current node. allPairs[currentIndex] = node.kvp; currentIndex ++; // Now right subtree. if (node.right != null) { inOrderTraversal (node.right); } } } //end-BinaryTreeMap3
Exercise 7: Download BinaryTreeMap3.java and BinaryTreeMapExample3.java, compile and execute. You will notice that the output is sorted, whereas the input (the order in which the strings were added) was not. How/where did the sorting occur?
Using special purpose iterator classes:
import java.util.*; public class StringExample { public static void main (String[] argv) { // Make a tree (of strings) and add stuff to it. TreeSet<String> tribes = new TreeSet<String>(); tribes.add ("Ewok"); tribes.add ("Aqualish"); tribes.add ("Gungan"); tribes.add ("Amanin"); tribes.add ("Jawa"); tribes.add ("Hutt"); tribes.add ("Cerean"); // Get the data structure to return an object that does the iteration. Iterator iter = tribes.iterator (); // Now use the iterator object to perform iteration. while (iter.hasNext()) { String name = (String) iter.next (); // Note: a cast is required. System.out.println (name); } } }
Exercise 8: Why is a cast required above?
It turns out that we can use an iterator specialized to strings:
Iterator<String> iter = tribes.iterator (); while (iter.hasNext()) { String name = iter.next (); // No cast needed. System.out.println (name); }Note:
Let us develop a simple application of map's: count the number of occurences of words in a body of text:
The quick brown fox jumped over the lazy dog, after which the dog jumped on the fox and bit the fox, after which their friendship ended rather abruptly.For this example, we want the output to read something like:
4 the // 4 occurences of "the" in the text 3 fox // 3 occurences of "fox" in the text 2 jumped // ... etc 2 dog 2 after 2 which 1 quick 1 brown ...
Algorithm: wordCount Output: word counts 1. while more words 2. w = getNextWord() 3. if w is not in dictionary 4. add w to dictionary 5. set w's count to 1 6. else 7. increment w's count 8. endif 9. endwhile 10. Print counts for each word
import java.util.*; public class WordCount { public static void main (String[] argv) { countWordsInFileUsingTree ("testfile"); countWordsInFileUsingList ("testfile"); } static void countWordsInFileUsingTree (String fileName) { // Create an instance of the data structure. BinaryTreeMap3 tree = new BinaryTreeMap3 (); // Read a text file and extract the words into an array. String[] words = WordTool.readWords (fileName); System.out.println ("Read in " + words.length + " words"); // Put words into data structure. If a word repeats, increment its count. for (int i=0; iNote:// Increment count. KeyValuePair kvp = tree.getKeyValuePair (words[i]); Integer count = (Integer) kvp.value; kvp.value = new Integer (count+1); } } // Note use of array: KeyValuePair[] uniqueWords = tree.getAllKeyValuePairs (); System.out.println ("Found " + uniqueWords.length + " unique words"); // Sort the words by count. Arrays.sort (uniqueWords, new KeyValueComparator()); for (int i=0; i < uniqueWords.length; i++) { System.out.println (uniqueWords[i].value + " " + uniqueWords[i].key); } } static void countWordsInFileUsingList (String fileName) { // Our data structure is now a linked list. OurLinkedListMap list = new OurLinkedListMap (); // ... everything else is the same ... } } //end-WordCount
Exercise 9: Download BinaryTreeMap3.java, OurLinkedListMap.java, WordCount.java and testfile. Compile and execute WordCount to make sure it works. Then, perform word counts for this classic book and this one. You can comment out the linked-list version (since it does the same thing and does it slower). Find another free book on-line and apply WordCount to the book.
Exercise 10: Compare the performance between a tree, a hashtable and a linked list for the two large texts above. You will need to add code to use a hashtable for counting. Use OurHashMap.java as the hashtable.
Finally, let us explain how we used Java's sorting algorithm above:
KeyValuePair[] uniqueWords = tree.getAllKeyValuePairs (); // Sort the words by count. Arrays.sort (uniqueWords, new KeyValueComparator());
Let's look at some of the details:
public void sort (Object[] inputArray, Comparator comp)(Note: we have simplified it slightly to remove references to generic types).
public interface Comparator { public int compare (Object o1, Object o2); public boolean equals (Object obj); }(Again, we've simplified this by removing generic types).
class KeyValueComparator implements Comparator { // We decide how to compare two key-value pairs. Java's sort // algorithm will repeatedly call this as it compares elements. public int compare (KeyValuePair kvp1, KeyValuePair kvp2) { // Note: the .value variable is of type Object. That's why we need the cast. Integer count1 = (Integer) kvp1.value; Integer count2 = (Integer) kvp2.value; if (count1 > count2) { return -1; } else if (count1 < count2) { return 1; } else { return 0; } } // We're required to implement this as part of implementing // the interface. public boolean equals (Object obj) { return false; } } //end-KeyValueComparatorNote:
class KeyValueComparator implements Comparator<KeyValuePair> { public int compare (KeyValuePair kvp1, KeyValuePair kvp2) { // ... } }
Exercise 11: Add a counter to KeyValueComparator above to count how often the compare() method is called. What is the relation between this count and the size of the array being sorted?
Exercise 12: Examine the logic in compare(). What would happen if we switch the first two return statements to read:
if (count1 > count2) { return 1; } else if (count1 < count2) { return -1; } else { return 0; }Try it out and see what happens. Then explain what happened.