Return to the Lecture Notes Index

15-200 Lecture 21 (Wednesday, March 22, 2006)

Last time, we introduced the concept of Trees and learned about a special type of tree called a Heap. Today, we learned about another special type of Tree, called an Expression Tree. We also discussed various types of Tree Traversals, which are ways of walking through all the data in a Tree.

Expression Trees

Earlier in the semester, we discussed Expressions and how they could be interpreted using stacks. Expression trees are just another way of representing Expressions. Recall that Expressions consist of operators and operands, which are along with a well defined order of operations as to which operations should be done first. For the sake of simplicitiy, we've limited our discussion of Expressions to those that contain only Binary Operators, which are operators that take operands. These include familiar operators such as addition, subraction, mulitplication, etc...

Lets say we're given the expression "(5+2)*(9-3)". We can very naturally express this in the form of a binary tree.

			*
		      /   \  
	             +	   -
                    / \   / \
	           5   2 9   3

Different types of Trees are defined by different relationships between the parent and child nodes. In MaxHeaps, this relation was that the parent is always greater than its children. In Expression Trees, the Parent is an Operator, and its children are the Operands. Note that the only Nodes that contain Operands are the leaves, which have no children. However, we can think of any particular subtree taken as a whole to be an Operand, since it will be evaluated to a number.

For example, in the above Tree, the root is the multiplication operator *. The two children are its operands. Howver, these are themselves expression trees, so they must be evaluted before the multiplication is done. This is how the order of operations is preserved in the expression trees.

So, the left subtree of the * operator is the tree...

		+
	      /   \
	     5     2

In this subtree, the + is the root, and its two children are operands 5 and 2. So, we arrange the expression from left to right as...

	LEFT CHILD, OPERATOR, RIGHT CHILD
	    5           +          2

So this tree evaluates to 7, which is a valid operand. Likewise, the right subtree of the * operator is...

  		-
	      /   \
	     9     3

	LEFT CHILD, OPERATOR, RIGHT CHILD
	    9          -           3

Which evaluates to 6. Now, the entire expression tree simplifies to the following....

		*
	      /   \
	     7     6

	LEFT CHILD, OPERATOR, RIGHT CHILD
            7           *          6

So this tree, unambiguously evaluates to 42. Using this process, we could easily write a recursive method to evaluate an expression tree. The pseudocode would look something like this...


public int evaluate(TreeNode current){
	try{
		return Integer.parse(current.getValue());  // Try to return the value if its an operand.
	}catch(NumberFormatException e) {
		String operator = (String)current.getValue();
		if(operator.equals("*"))
			return evaluate(current.getLeftSubtree())* evaluate(current.getRightSubtree());
		// repeat for each operator
	}
}

Tree Traversals

Given an Expression tree, what if we wanted to simply print out the tree. The first question we need to ask is how do we want to print it. Earlier in the semester, we learned that an expression can be written in multiple ways. First, lets print it out in the familiar infix notation, where the operator comes between the operands.

Based on the relationship we described above, where the Node contains an operator, and its children are the operands, we know that we should print the left child first, then print the operator, and finally print the operand. This certainly makes sense for small trees.

		+
	      /   \
	     2     4

This clearly should be printed as "2+4". So we print the left child first, then the "+", and finally the right subtree. But what about for larger, more complicated Trees? Well, the solution is no different. We just recursively apply this method until we reach the leaves of the tree. As soon as we get a leaf, we just print that operand.


public void printInFix(TreeNode current){
	if(current == null) return;
	else if(current.getLeftSubtree()==null && current.getRightSubtree() == null)
		System.out.print(current.getValue());   // Node is leaf, so just print it
	else{
		printInFix(current.getLeftSubtree);    // print left child
		System.out.print(current.getValue());  // print operator
		printInFix(current.getRightSubtree);   // print right child
	}	
}

This algorithm is also known as a Tree Traversal. It walks through every element in the Tree exactly once, in a well defined order. In this case, it prints out in the normal order that we would see the expression written. We call this an Inorder Traversal.

We also discussed expressions in postfix order, where the operator is printed after the operand. For example, the above expression, which in infix was "2+4", would be "2,4,+" in postfix notation.

We'll approach the problem in a similar way. For the tree...


		+
	      /   \
	     2     4

We just print the left child first, then the right child, and then the operator.


public void printPostFix(TreeNode current){
	if(current == null) return;
	else if(current.getLeftSubtree()==null && current.getRightSubtree() == null)
		System.out.print(current.getValue());   // Node is leaf, so just print it
	else{
		printInFix(current.getLeftSubtree);    // print left child
		printInFix(current.getRightSubtree);   // print right child
		System.out.print(current.getValue()); // print operator
	}	
}

The only different is the order in which the elements are printed. In this case, since we want the operator to come after the operands, we just recursively print the left subtree, then recursively print the right subtree, and then print the operator. This is another type of Tree Traversal, called a PostOrder Traversal .

Finally, there is a third type of traversal, called a PreOrder Traversal. As you might imagine, this just involves visiting the value of the node first, then visiting the left subtree, and then the right subtree. This doesn't really have any particular importance with regards to expression trees, but it is still a type of traversal.

Although we have used the example of Expression Trees, you can use these traversals on any type of Binary Tree. PostOrder and PreOrder traversals can also be generalized to non-binary trees as well. When we discuss Binary Search Trees, we will see some of these traversals again.

Depth First Search and Breadth First Seach

What if we want to search a Tree for a particular item. We would want to traverse the tree in some way, each time checking the current Node to see if we find what we're looking for. Lets look again at an Preorder Traversal. Given a tree...


		A
	      /  \
	    B     C
          /  \   / \
         D   E  F   G

An preorder traversal will travel along the path A,B,D,E,C,F,G. We visit our current Node, then we try left, then we try right. We can use this traversal to search the tree. At any given node, we check to see if we have found what we're looking for. If not, we recursively search the left subtree. If we still dont find it, we search the right subtree.

This is called a depth first search. If you look at the order in which we search the Nodes, we search as deep as we can along one path, then backtrack, and try a different path, search this new path as deep as you can.

What if instead, we want to search all of the top levels first, then go deeper to the next level. In other words, in this tree we want to search in teh order A... B,C... D,E,F,G. We search the top level, A, then the second level, B and C, and finally we search the bottom level, D,E,F, and G. This type of search is called a Breadth First Search.

How would we implement this? Unlike the above traversals, its not as simple as just choosing which path to go down. We are essentially travelling down all paths simlutaneously. To do this, we can use a Queue. We enqueue the root to start. Next, we dequeue a Node and enqueue all of its children. We then repeat this step until the Queue is empty. Lets look at how this works in the above example.

First, we add A to our Queue. Next, we go into a loop. We then dequeue A, search A, then enqueue its children B and C. When we go through the loop two more times, we will search B and C as they are dequeued, and as we go we will enqueue D,E,F, and G in that order. The next 4 iterations of the loop, we will search those nodes. Since none of these nodes have any children, as soon as we dequeue G, the queue will be empty, and we know we have traversed the entire list.


public boolean DFS(TreeNode root, Object target){
	Queue q = new Queue();
	q.enqueue(root);
	while(!q.isEmpty){
		TreeNode current = q.dequeue();
		if(current.getValue().equals(target)) return true;  //check current node
		q.enqueue(current.getLeftSubtree());    //enqueue left child
		q.enqueue(current.getRightSubtree());   //enqueue right child
	}	
}