The Way of the Java/Trees
tree ADT data structure
This chapter presents a new data structure called a tree, some of its uses and two ways to implement it.
A possible source of confusion is the distinction between an ADT, a data structure, and an implementation of an ADT or data structure. There is no universal answer, because something that is an ADT at one level might in turn be the implementation of another ADT.
To help keep some of this straight, it is sometimes useful to draw a diagram showing the relationship between an ADT and its possible implementations. This figure shows that there are two implementations of a tree:
The horizontal line in the figure represents the barrier of abstraction between the ADT and its implementations.
A tree nodeEdit
node tree node cargo embedded reference implementation!Tree binary tree
Like lists, trees are made up of nodes. A common kind of tree is a binary tree, in which each node contains a reference to two other nodes (possibly null). The class definition looks like this:
verbatim public class Tree
Object cargo; Tree left, right;
Like list nodes, tree nodes contain cargo: in this case a generic Object. The other instance variables are named left and right, in accordance with a standard way to represent trees graphically:
The top of the tree (the node referred to by tree) is called the root. In keeping with the tree metaphor, the other nodes are called branches and the nodes at the tips with null references are called leaves. It may seem odd that we draw the picture with the root at the top and the leaves at the bottom, but that is not the strangest thing.
root node leaf node parent node child node level
To make things worse, computer scientists mix in yet another metaphor: the family tree. The top node is sometimes called a parent and the nodes it refers to are its children. Nodes with the same parent are called siblings, and so on.
Finally, there is also a geometric vocabulary for taking about trees. I already mentioned left and right, but there is also ``up (toward the parent/root) and down (toward the children/leaves). Also, all the nodes that are the same distance from the root comprise a level of the tree.
I don't know why we need three metaphors for talking about trees, but there it is.
The process of assembling tree nodes is similar to the process of assembling lists. We have a constructor for tree nodes that initializes the instance variables.
public Tree (Object cargo, Tree left, Tree right) this.cargo = cargo; this.left = left; this.right = right;
We allocate the child nodes first:
Tree left = new Tree (new Integer(2), null, null); Tree right = new Tree (new Integer(3), null, null);
We can create the parent node and link it to the children at the same time:
Tree tree = new Tree (new Integer(1), left, right);
This code produces the state shown in the previous figure.
The most natural way to traverse a tree is recursively. For example, to add up all the integers in a tree, we could write this class method:
public static int total (Tree tree) if (tree == null) return 0; Integer cargo = (Integer) tree.cargo; return cargo.intValue() + total (tree.left) + total (tree.right);
This is a class method because we would like to use null to represent the empty tree, and make the empty tree the base case of the recursion. If the tree is empty, the method returns 0. Otherwise it makes two recursive calls to find the total value of its two children. Finally, it adds in its own cargo and returns the total.
Although this method works, there is some difficulty fitting it into an object-oriented design. It should not appear in the Tree class because it requires the cargo to be Integer objects. If we make that assumption in Tree.java then we lose the advantages of a generic data structure.
On the other hand, this code accesses the instance variables of the Tree nodes, so it ``knows more than it should about the implementation of the tree. If we changed that implementation later (and we will) this code would break.
Later in this chapter we will develop ways to solve this problem, allowing client code to traverse trees containing any kinds of objects without breaking the abstraction barrier between the client code and the implementation. Before we get there, let's look at an application of trees.
tree!expression expression tree postfix infix
A tree is a natural way to represent the structure of an expression. Unlike other notations, it can represent the comptation unambiguously. For example, the infix expression 1 + 2 * 3 is ambiguous unless we know that the multiplication happens before the addition.
The following figure represents the same computation:
The nodes can be operands like 1 and 2 or operators like + and *. Operands are leaf nodes; operator nodes contain references to their operands (all of these operators are binary, meaning they have exactly two operands).
Looking at this figure, there is no question what the order of operations is: the multiplication happens first in order to compute the first operand of the addition.
Expression trees like this have many uses. The example we are going to look at is translation from one format (postfix) to another (infix). Similar trees are used inside compilers to parse, optimize and translate programs.
tree!traversal traverse recursion preorder postorder inorder
I already pointed out that recursion provides a natural way to traverse a tree. We can print the contents of an expression tree like this:
public static void print (Tree tree) if (tree == null) return; System.out.print (tree + " "); print (tree.left); print (tree.right);
In other words, to print a tree, first print the contents of the root, then print the entire left subtree, then print the entire right subtree. This way of traversing a tree is called a preorder, because the contents of the root appear before the contents of the children.
For the example expression the output is + 1 * 2 3. This is different from both postfix and infix; it is a new notation called prefix, in which the operators appear before their operands.
You might suspect that if we traverse the tree in a different order we get the expression in a different notation. For example, if we print the subtrees first, and then the root node:
public static void printPostorder (Tree tree) if (tree == null) return; printPostorder (tree.left); printPostorder (tree.right); System.out.print (tree + " ");
We get the expression in postfix (1 2 3 * +)! As the name of the previous method implies, this order of traversal is called postorder. Finally, to traverse a tree inorder, we print the left tree, then the root, then the right tree:
public static void printInorder (Tree tree) if (tree == null) return; printInorder (tree.left); System.out.print (tree + " "); printInorder (tree.right);
The result is 1 + 2 * 3, which is the expression in infix.
To be fair, I have to point out that I omitted an important complication. Sometimes when we write an expression in infix we have to use parentheses to preserve the order of operations. So an inorder traversal is not quite sufficient to generate an infix expression.
Nevertheless, with a few improvements, the expression tree and the three recursive traversals provide a general way to translate expressions from one format to another.
encapsulation client provider abstract class class!abstract
As I mentioned before, there is a problem with the way we have been traversing trees: it breaks down the barrier between the client code (the application that uses the tree) and the provider code (the Tree implementation). Ideally, tree code should be general; it shouldn't know anything about expression trees. And the code that generates and traverses the expression tree shouldn't know about the implementation of the trees. This design criterion is called object encapsulation to distinguish it from the encapsulation we saw in Section encapsulation, which we might call method encapsulation.
In the current version, the Tree code knows too much about the client. Instead, the Tree class should provide the general capability of traversing a tree in various ways. As it traverses, it should perform operations on each node that are specified by the client.
Visitable abstract class!Visitable
To facilitate this separation of interests, we will create a new abstract class, called Visitable. The items stored in a tree will be required to be visitable, which means that they define a method named visit that does whatever the client wants done to each node. That way the Tree can perform the traversal and the client can perform the node operations.
Here are the steps we have to perform to wedge an abstract class between a client and a provider:
Define an abstract class that specifies the methods the provider code will need to invoke on its components.
Write the provider code in terms of the new abstract class, as opposed to generic Objects.
Define a concrete class that belongs to the abstract class and that implements the required methods as appropriate for the client.
Write the client code to use the new concrete class.
The next few sections demonstrate these steps.
Defining an abstract classEdit
abstract class!defining interface
An abstract class definition looks a lot like a concrete class definition, except that it only specifies the interface of each method and not an implementation. The definition of Visitable is
verbatim public interface Visitable
public void visit ();
That's it! The word interface is Java's keyword for an abstract class. The definition of visit looks like any other method definition, except that it has no body. This definition specifies that any class that implements Visitable has to have a method named visit that takes no parameters and that returns void.
Like other class definitions, abstract class definitions go in a file with the same name as the class (in this case Visitable.java).
Implementing an abstract classEdit
abstract class!implementing Token class class!Token
If we are using an expression tree to generate infix, then ``visiting a node means printing its contents. Since the contents of an expression tree are tokens, we'll create a new concrete class called Token that implements Visitable
verbatim public class Token implements Visitable
public Token (String str) this.str = str;
public void visit () System.out.print (str + " ");
When we compile this class definition (which is in a file named Token.java), the compiler checks whether the methods provided satisfy the requirements specified by the abstract class. If not, it will produce an error message. For example, if we misspell the name of the method that is supposed to be visit, we might get something like, ``class Token must be declared abstract. It does not define void visit() from interface Visitable.
The next step is to modify the parser to put Token objects into the tree instead of Strings. Here is a small example:
String expr = "1 2 3 * +"; StringTokenizer st = new StringTokenizer (expr, " +-*/", true); String token = st.nextToken(); Tree tree = new Tree (new Token (token), null, null));
This code takes the first token in the string and wraps it in a Token object, then puts the Token into a tree node. If the Tree requires the cargo to be Visitable, it will convert the Token to be a Visitable object. When we remove the Visitable from the tree, we will have to cast it back into a Token.
As an exercise, write a version of printPreorder called visitPreorder that traverses the tree and invokes visit on each node in preorder.
Array implementation of treesEdit
tree!array implementation implementation!Tree ADT!Tree
What does it mean to ``implement a tree? So far we have only seen one implementation of a tree, a linked data structure similar to a linked list. But there are other structures we would like to identify as trees. Anything that can perform the basic set of tree operations should be recognized as a tree.
So what are the tree operations? In other words, how do we define the Tree ADT?
[constructor:] Build an empty tree.
[isEmpty:] Is this tree the empty tree?
[getLeft:] Return the left child of this node.
[getRight:] Return the left child of this node.
[getParent:] Return the parent of this node.
[getCargo:] Return the cargo object from this node.
[setCargo:] Assign a cargo object to this node (and create the node, if necessary).
In the implementation we have seen, the empty tree is represented by the special value null. getLeft and getRight are performed by accessing the instance variables of the node, as are getCargo and setCargo. We have not implemented getParent yet (you might think about how to do it).
There is another implementation of trees that uses arrays and indices instead of objects and references. To see how it works, we will start by looking at a hybrid implementation that uses both arrays and objects.
This figure shows a tree like the ones we have been looking at, although it is laid out sideways, with the root at the left and the leaves on the right. At the bottom there is an array of references that refer to the objects in the trees. The cargo objects are represented as null references.
You might notice that array index 1 refers to the root node and array index 0 is empty. The reason for that will become clear soon.
So now we have a tree where each node has a unique index. Furthermore, the indices have been assigned to the nodes according to a deliberate pattern, in order to achieve the following results:
The left child of the node with index has index .
The right child of the node with index has index .
The parent of the node with index has index (rounded down).
Using these formulas, we can implement getLeft, getRight and getParent just by doing arithmetic; we don't have to use the references at all!
Since we don't use the references, we can get rid of them, which means that what used to be a tree node is now just cargo and nothing else. That means we can implement the tree as an array of cargo objects; we don't need tree nodes at all.
Here's what one implementation looks like:
verbatim public class Tree
Object array; int i;
public Tree () array = new Object ; i = 1;
public Tree (Object array, int i) this.array = array; this.i = i;
No surprises so far. The instance variables are an array of Objects and an integer that indicates where in the array the new node is found.
The first constructor initializes the array with an arbitrary initial size (we can always resize it later). The second constructor assumes that the array of objects already exists. All nodes that comprise a tree will share the same array and have different values of i.
We use i to access the cargo of a given node:
public Object getCargo () if (i >= array.length) return null; return array[i];
public void setCargo (Object obj) if (i >= array.length) array = resize (array); array[i] = obj;
Both methods have to check the length of the array. If getCargo tries to access a nonexistent element of the array, it returns null to indicate an empty tree. If getCargo goes past the length of the array, it resizes the array to make room (see Section resize for a possible implementation of resize).
To check whether a node is an empty tree, we check whether the cargo is null.
public boolean empty () return (getCargo() == null);
The implementation of getLeft, getRight and getParent is just arithmetic:
public Tree getLeft () return new Tree (array, 2*i);
public Tree getRight () return new Tree (array, 2*i + 1);
public Tree parent () return new Tree (array, i/2);
Finally we are ready to build a tree. In another class (the client), we would write
Tree tree = new Tree (); tree.setCargo ("cargo for root");
The constructor builds an empty tree. Invoking setCargo puts the string "cargo for root" into the root node.
To add children to the root node:
tree.getLeft().setCargo ("cargo for left"); tree.getRight().setCargo ("cargo for right");
In the tree class we could provide a method that prints the contents of the tree in preorder.
public void print () if (isEmpty ()) return; System.out.println (array[i]); getLeft().print(); getRight().print();
We invoke this method from the client by passing the root as a parameter.
The output is
verbatim cargo for root cargo for left cargo for right verbatim
This implementation provides the basic operations that define a tree. As I pointed out, the linked implementation of a tree provides the same operations, but the syntax is different. As an exercise, modify the linked tree so that it implements the Tree ADT.
One problem with the array implementation is that the initial size of the array is arbitrary and it might have to be resized. This last problem can be solved by replacing the array with a Vector.
The Vector classEdit
Vector class class!Vector
The Vector is a built-in Java class in the java.util package. It is an implementation of an array of Objects, with the added feature that it can resize itself automatically, so we don't have to.
The Vector class provides methods named get and set that are similar to the getCargo and setCargo methods we wrote for the Tree class. You should review the other Vector operations by consulting the online documentation.
Before using the Vector class, you should understand a few concepts. Every Vector has a capacity, which is the amount of space that has been allocated to store values, and a size, which is the number of values that are actually in the vector.
The following figure is a simple diagram of a Vector that contains three elements, but it has a capacity of seven.
In general, it is the responsibility of the client code to make sure that the vector has sufficient size before invoking set or get. If you try to access an element that does not exist (in this case the elements with indices 3 through 6), you will get an ArrayIndexOutOfBounds exception.
The Vector methods add and insert automatically increase the size of the Vector, but set does not. The resize method adds null elements to the end of the Vector to get to the given size.
Most of the time the client doesn't have to worry about capacity. Whenever the size of the Vector changes, the capacity is updated automatically. For performance reasons, some applications might want to take control of this function, which is why there are additional methods for increasing and decreasing capacity.
Because the client code has no access to the implementation of a vector, it is not clear how we should traverse one. Of course, one possibility is to use a loop variable as an index into the vector:
for (int i=0; i<v.size(); i++) System.out.println (v.get(i));
There's nothing wrong with that, but there is another way that serves to demonstrate the Iterator class. Vectors provide a method named iterator that returns an Iterator object that makes it possible to traverse the vector.
The Iterator classEdit
Iterator class class!Iterator abstract class
Iterator is an abstract class in the java.util package. It specifies three methods:
[hasNext:] Does this iteration have more elements?
[next:] Return the next element, or throw an exception if there is none.
[remove:] Remove from the collection the last element that was returned.
The following example uses an iterator to traverse and print the elements of a vector.
Iterator iterator = vector.iterator (); while (iterator.hasNext ()) System.out.println (iterator.next ());
Once the Iterator is created, it is a separate object from the original Vector. Subsequent changes in the Vector are not reflected in the Iterator. In fact, if you modify the Vector after creating an Iterator, the Iterator becomes invalid. If you access the Iterator again, it will cause a ConcurrentModification exception.
In a previous section we used the Visitable abstract class to allow a client to traverse a data structure without knowing the details of its implementation. Iterators provide another way to do the same thing. In the first case, the provider performs the iteration and invokes client code to ``visit each element. In the second case the provider gives the client an object that it can use to select elements one at a time (albeit in an order controlled by the provider).
As an exercise, write a concrete class named PreIterator that implements the Iterator interface, and write a method named preorderIterator for the Tree class that returns a PreIterator that selects the elements of the Tree in preorder.
binary tree root node leaf node parent node child node level prefix preorder postorder inorder class variable encapsulation
[binary tree:] A tree in which each node refers to 0, 1, or 2 dependent nodes.
[root:] The top-most node in a tree, to which no other nodes refer.
[leaf:] A bottom-most node in a tree, which refers to no other nodes.
[parent:] The node that refers to a given node.
[child:] One of the nodes referred to by a node.
[level:] The set of nodes equidistant from the root.
[prefix notation:] A way of writing a mathematical expression with each operator appearing before its operands.
[preorder:] A way to traverse a tree, visiting each node before its children.
[postorder:] A way to traverse a tree, visiting the children of each node before the node itself.
[inorder:] A way to traverse a tree, visiting the left subtree, then the root, then the right subtree.
[class variable:] A static variable declared outside of any method. It is accessible from any method.
[binary operator:] An operator that takes two operands.
[object encapsulation:] The design goal of keeping the implementations of two objects as separate as possible. Neither class should have to know the details of the implementation of the other.
[method encapsulation:] The design goal of keeping the interface of a method separate from the details of its implementation.