CS 10: Winter 2016

Lecture 12, January 29

Code discussed in lecture

Short Assignment 7

Short Assignment 7 is due Monday.

Layout Managers

Demonstrate the GUI in PS-3. Display the code of Editor.java. Note two things:

  1. JPanel objects are used to organize buttons. What is more, a JPanel is used to hold the three button JPanels.
  2. We have discussed the FlowLayout layout manager. The GUI for PS-3 uses two additional managers: GridLayout and BorderLayout. The first of these has two parameters: the number of rows and number of columns. It lays components out on a grid of this size. It is used for the JPanel holding the three button panels to get them to line up in a vertical column.

    A BorderLayout allows the user to put one thing at the top (NORTH), one at the bottom (SOUTH), one on the right side (EAST), one on the left side (WEST), and one in the middle (CENTER). The CENTER will expand to occupy all space not used by the other four. Here the JPanel holding the three rows of buttons goes in the NORTH and the canvas goes in the CENTER.

Rooted trees

We use rooted trees to represent hierarchical relations. Here is an example of a rooted tree:

A tree is built up from nodes. The node at the top of the tree is the root; in our example, the root is node 7. Each node has zero or more children, which are also nodes. For example, node 4 has two children, which are nodes 11 and 2. An edge connects a node with its child, for example the edge (4, 11). Nodes with no children are external nodes or leaves (such as nodes 9 and 10), and nodes with at least one child are called internal nodes (such as node 4). A child has exactly one parent (except for the root, which has no parent); for example, the parent of node 11 is node 4. Nodes with the same parent are siblings, such as nodes 11 and 2. A path in a tree is a sequence of unique nodes such that each node in the sequence has an edge to the nodes before and after it. In our example, one path consists of nodes 6, 8, 3, 7, 4. If there is a path from the root to node y such that node x appears on the path, then node x is an ancestor of node y, and node y is a descendant of node x. For example, node 3 is an ancestor of node 5, and node 5 is a descendant of node 3. A subtree rooted at a node consists of all descendents of that node, including the node itself. The subtree rooted at node 3 comprises nodes 3, 8, 6, 5, 9, 12, and 1.

Here are some examples of relations that can be represented by trees:

The book gives additional examples. We will see more examples as the course progresses.

The examples above have some cases where the order of children does not matter (the Java inheritance hierarchy, file systems, and organization charts). For an HTML document, however, the order does matter. In representing trees we end up imposing an order on children, whether it is important or arbitrary.

Implementing rooted trees

We will not use the tree code from our textbook, but feel free to read it. The book uses a different approach from what we have seen so far. We built our linked lists from Element objects, where Element is an inner class. We access the elements directly from within SentinelDLL or SLL and access fields with code such as current.next. We provide no access to Element objects from outside of the class, however. Instead, we have a current instance variable built into the class, or we provide an iterator. Java does the same thing.

In the textbook, they build up their lists, trees, graphs, etc. from Position objects. Position is an interface (in Position.java) with a single method: element. This method returns the data stored in the object. Their node classes for lists, trees, etc. implement the Position interface. They then let the user get a reference to one of these nodes via methods such as root in the Tree interface. Although root actually returns a reference to a Node object, its return type is Position.

Therefore, you can do only two things with a Position:

We call Position an opaque type. You can pass it around and use it to mark where you are in a data structure, but you're not permitted to access anything within it. From a software engineering point of view, this is the way to go.

This partially protects against the user calling methods on the Position object (although if the user knows that the Position is really a LinkedTree node he or she can cast it and call the functions). However, this approach complicates the tree code and hides its basic simplicity. Therefore we implemented our own tree code. We do not implement code for general trees (although we could do so), but concentrate on the common special case of binary trees.

Binary trees

A binary tree is a rooted tree in which each node has zero, one, or two children. We designate each child as either a left child or a right child, even when it's an only child. Here are two different binary trees:

They differ only in that node 5 is a left child in the tree on the left, and it's a right child in the tree on the right.

Binary trees come up in a multitude of applications: decision trees, expression trees, code trees (such as in Huffman encoding, which you'll be doing in Lab Assignment 4), and binary search trees. We'll see binary search trees later in the course, but the idea is that we store a value in each node, called the key, such that for every node x, the keys in its left subtree (the subtree rooted at its left child) are less than or equal to the key in node x, and the keys in its right subtree (the subtree rooted at its right child) are greater than or equal to the key in node x. Here is a binary search tree, with keys appearing inside the nodes:

Rather than having an inner class to represent the nodes and manipulating them via an outer class (as we did for linked lists), this time we make the tree nodes themselves more powerful and manipulate them from other classes. This code is in BinaryTree.java. The class BinaryTree has three instance variables:

The values of left or right are null if these children are absent. The book also keeps a reference to the parent node, which can be useful for certain applications. We will explore this further in sa7.

We access a binary tree through its root node. If we want to access a subtree, we access it through the root of the subtree. Thus, some of the methods in the BinaryTree class pertain to individual nodes, but we can always consider a node to be the root of a subtree.

Methods in the BinaryTree class

Here are the methods in the BinaryTree class. Although they are called on a BinaryTree object, many of them actually pertain to just the node at the root of a subtree, and that's what they're called on.

You might have noticed that although the BinaryTree class includes a height method, it does not include a depth method. Think about why there is not enough information in a BinaryTree object, as defined, to write a depth method.

Notice that several of the methods are recursive. For example, the size method. That's because we can characterize the size of a subtree recursively:

The size of a subtree is 1 (for the root of the subtree), plus the size of its left subtree, plus the size of its right subtree. The size of an empty subtree is 0.

The size and height methods use a special ternary operator ? : in Java. (Ternary means that it takes three operands.) The first operand, appearing before the question mark, is a boolean expression. If the boolean expression evaluates to true, then the value of the operator is the second expression, between the question mark and the colon. Otherwise, the value of the operator is the third expression, which follows the colon. In the expression hasLeft() ? left.size() : 0 in the size method, if the call hasLeft() returns true, the expression's value is the value returned by calling left.size(). Otherwise, hasLeft() returns false, and the expression's value is 0.

Similarly, we can characterize the height of a node recursively:

The height of a leaf is 0. The height of an internal node is 1 plus the maximum heights of its children.

You might recall that the equals method must take a reference to Object as its parameter, no matter what class it appears in. Therefore, the first thing that equals does is make sure that other references a BinaryTree object. Generic types in Java are designed strangely, and although you would think that the first line should be

if (other instanceof BinaryTree<E>) {

we have to put a question mark between the angle brackets instead. (I don't fully understand why, but it shows how difficult it is to design a programming language that is both easy to use and internally consistent.) After the instanceof check, we then cast other to a reference to BinaryTree<E>, this time using <E> and not <?>. The line @SuppressWarnings("unchecked") is another strange thing in Java; we could omit the line, but we'd get an annoying warning. Once we have cast the parameter, we have a complicated expression that checks for five requirements being met:

  1. This node has a left child if and only if other does.
  2. This node has a right child if and only if other does.
  3. The data in this node and other are equal, according to the equals method on the generic type E.
  4. If both nodes have left subtrees, then their left subtrees are equal.
  5. If both nodes have right subtrees, then their right subtrees are equal.

Notice how we rely on the || operator short-circuiting in the latter two tests. If hasLeft returns false, then do not call left.equals to check the left subtree, and ditto for the right subtree.

In the toStringHelper method, what's passed in is a string containing some number of spaces. These spaces precede each node in the subtree rooted at this. Each recursive call to toStringHelper increases the number of spaces by two. Although we usually think of processing the left subtree before the right subtree, in toStringHelper, we do the opposite so that when you look at the output with your head tipped to the left it looks like the structure of the binary tree. The toString method just gets things started off with an empty string.

We could have written the toStringHelper method in one line, using the ternary operator:

return (hasRight() ? right.toStringHelper(indent + "  ") : "")
    + (indent + data + "\n")
    + (hasLeft() ? left.toStringHelper(indent + "  ") : "");

In the fringe method, we create an empty ArrayList and pass it to addToFringe. This ArrayList has data added to it if the node is a leaf, and it's passed to the left and right subtrees otherwise. We could have done something like we did for toString, but that would require appending two ArrayList objects.

The BinaryTree class has a main method as a driver. It starts by creating as tree this tree:

Then it exercises the size, height, and fringe methods. Next, it traverses the tree using preorder, inorder, and postorder traversals (again, we'll see what these are later on). With the preorder and inorder traversals, it creates a copy of the tree in tree1 and exercises equals method, which returns true. After making a change to the data in the right child of the root of tree1, it runs the equals method again, this time getting back false. Finally, it makes another copy of tree in tree2, changes the left child of the root in tree2 to have no left child, and runs the equals method again, once again getting back false.