1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
9. Interfaces and Polymorphism
10. Inheritance
11. Additional Java Features
12. Collections and Generics
13. Linked Data
14. Iterating over Data Structures
15. Stacks and Queues
16. Trees and their Iterators
17. Binary Search Trees
16. Trees and their Iterators

16. Trees and their Iterators

So far, the data structures we have studied have organized their elements linearly. In arrays and lists, this linear structure is made explicit to the client using indices to refer to elements by their position. While elements in stacks and queues cannot be indexed (since these ADTs do not offer random access to their elements), their underlying representation in memory still places them in a natural order based on when they were added to the data structure. While linear structures are straightforward to reason about (largely due to the linear arrangement of computer memory), they are not the natural choice for modeling all types of data. For example,

All of these are examples of hierarchical data, where some elements can be viewed as the parents or ancestors of other child or descendant elements. In the genealogical example, this terminology is quite literal; an individual’s ancestors are their lineage. In a file system, a directory or file’s parent is the directory in which it is contained. In a type system, a superclass is the parent of all of its subclasses. These hierarchical data can be visualized as trees (for example, family trees or file trees) where parent elements are visualized above and connecting down to their children. In today’s lecture, we’ll introduce terminology to describe trees, with a focus on binary trees. We’ll also look at some representations of trees. Finally, we’ll see how we can extend the idea of iterators to non-linear data. In upcoming lectures, we’ll see how we can impose additional constraints on binary trees to obtain other useful data structures with good performance guarantees.

Tree Terminology

Similar to a linked list, a tree consists of a collection of linked objects, often called nodes, that each store a single data element. While a linked list arranges these nodes linearly, the connections in a tree can branch out, such as in the following example that has nodes labeled with Characters.

In this structure, there is one node, \(a\), that has no incoming connections. This is called the root of the tree. Every other node has exactly one incoming connection. In addition, each node can have one or more outgoing connections to other nodes. We can reach all of the nodes by following these outgoing connections starting from the root. Together, these properties connect the nodes in an acyclic branching structure.

Definition: Tree, Node, Root

A tree is a linked data structure consisting of individual data elements called nodes. The nodes are all connected together in an acyclic manner, with one distinguished node called the tree's root having no incoming connections and all other nodes having exactly one incoming connection.

We typically draw the connections in a tree oriented downward with the root node in the highest position. We draw the connections as downward arrows oriented away from the root. Adopting the terminology from family trees, we call the start of each arrow the parent node and the nodes that it connects to its children. For example, the node \(d\) in the above tree has parent \(a\) and children \(g\), \(h\), and \(i\). The nodes at the “bottom” of the tree that have no outgoing connections (i.e., that have no children) are called its leaves. The example tree drawn above has seven leaves: \(c\), \(e\), \(h\), \(i\), \(j\), \(k\), and \(l\).

Definition: Parent, Child, Leaf

Within a tree, a parent node has outgoing connections to its child nodes. A leaf node has no outgoing connections.

In your computer’s file tree, the root is the topmost directory in the file system. The connections model containment, so a directory is the parent of all of the other directories or files that it contains. The leaf nodes of this tree are the files and any empty directories, since neither of these contain other files/directories. By starting at the root directory in your computer’s file explorer, you can access all of the nodes in this tree by clicking on the correct nested sequence of directories.

Remark:

The definition that we have given for trees is specific to data structures. In particular, this definition includes a distinguished vertex called the root and oriented connections that all point away from the root. In discrete math or graph theory, trees are a related but distinct concept that typically describe nodes with undirected acyclic connections. Mathematicians may use other terms such as a rooted tree or an arborescence for the objects we have just described.

Subtrees

If we select any node in a tree and consider only the nodes that can be reached via connections from that node, we will find that we are left with another, likely smaller, tree. We call this the subtree rooted at this node.

Definition: Subtree

Given a tree and any node in this tree, which we'll denote by \(v\), the subtree rooted at \(v\) consists of the node \(v\) and all nodes reachable via one or more connections starting at \(v\). The nodes within a subtree also have a tree structure.

For example, the subtree rooted at \(d\) in our above tree is

\(d\) is the root of its subtree, and the leaves of this subtree are exactly the nodes that were leaves in the original tree. Using this notion of subtrees, we can give the following recursive description of a tree, that we will return to later in the lecture.

From this recursive lens, we can “collapse” our view of the tree we’ve been considering as

As one more piece of useful terminology, we can generalize the notion of parents and children to discuss a node’s ancestors and descendants.

Definition: Ancestor, Descendant

Within a tree, a node \(u\) is an ancestor of a node \(v\) if \(v\) is a member of \(u\)'s subtree. In this case, we say that \(v\) is a descendant of \(u\)

In a family tree, your ancestors not only include your parents, but also your grandparents, great-grandparents, etc. Your descendants not only include your children, but also your grandchildren, great-grandchildren, etc. In our running example, the node \(k\) has ancestors \(g\), \(d\), and \(a\), and the node \(b\) has descendants \(e\), \(f\), and \(j\). Every non-root node in a tree has the root as an ancestor, and every non-leaf node has at least one leaf as a descendant.

Measurements of Trees

Now, we’ll introduce some more terms that describe various measurements of trees. These measurements will be useful when we try to bound the number of steps required to carry out different operations on trees. The first measurement, size, should be familiar from our discussion of lists.

Definition: Size

The size of a tree is the number of nodes that it contains.

The size of our example tree is 12. Size on its own is not enough to describe the structure of a tree, as different branching structures can result in many differently shaped trees all with the same size. For example, the following 3 trees all have size 7.

The first of these trees is very “short” and “wide”, with its root directly connecting to its six leaves. The last of these trees is very “tall” and “narrow”, and its nodes essentially form a linked chain from its root to its single leaf. The middle tree falls in between these extremes. We can capture these notions of the “dimension” of a tree with the following terms.

Definition: Height

The height of a tree is the maximum number of connections that must be traversed to connect its root to one of its leaves.

Our tree labeled with characters has height 3. One of the longest paths from its root to a leaf is \(a \to d \to g \to l\), which includes 3 connections. The minimum height of a non-empty tree is 0, and this is possible only in a tree consisting of a single node. Any tree with multiple nodes must have height at least 1. The maximum height of a tree is one less than its size. This is realized by a tree with nodes forming a single linked chain, such as the third example above. When we want to talk about the “level” of a particular node within a tree, we use the related property, depth.

Definition: Depth

The depth of a node in a tree is the number of connections that must be traversed to reach that node from the tree's root.

Note that there is always exactly one path from the root to any node in the tree, so the depth of a node is unambiguous. The depth of the root node is 0, and the maximum depth of any node is the tree’s height. We can group all of the nodes in a tree into levels based on their depths as shown below.

Finally, arity (sometimes called degree) gives a notion of how “widely” branched out a tree is.

Definition: Arity

The arity of a tree is the maximum number of outgoing connections of any of its nodes.

The arity of a tree with more than one node is at least 1 (and exactly 1 in the degenerate case of a linked list) and at most the size of the tree minus 1 (if all other nodes are children of the root). While a general tree has no restriction on its arity, we often impose an upper bound when we design a tree data structure. The most common upper bound is arity 2, giving a binary tree.

Definition: Binary Tree

A binary tree is a tree with arity 2. Each of its non-leaf nodes has either 1 or 2 children.

Typically in a binary tree, we distinguish between its two possible children, labeling one as its left child and one as its right child. This means that a node in a binary tree can have no children (if it is a leaf), just a left child, just a right child, or both a left and a right child. Later in today’s lecture (when we discuss tree traversals) and next lecture (when we discuss binary search trees), we’ll see examples where this left-right distinction can be significant.

Representing Binary Trees

Now that we have introduced a lot of terminology to discuss trees, we’ll transition to thinking about how we can model them in our code. For the remainder of our discussion of trees, we will focus our attention on binary trees. You can find some pointers on implementing general trees in the lecture exercises.

As a linked structure (i.e., a structure that decentralizes the storage of its data elements and includes connections between some pairs of elements), we can use a similar approach as we did for linked lists, defining an auxiliary static nested Node class. In the case of a binary tree, each node needs to store its data element and (up to) two references to other Nodes, a left reference and a right reference. The outer class, which we’ll call a LinkedBinaryTree would need only to store a reference to the Node object at the root of the tree. Then, the other nodes could be reached by following the correct sequence of links. Overall, this would result in a class structure:

LinkedBinaryTree.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class LinkedBinaryTree<T> {
  /** The node at the root of this tree */
  private Node<T> root; 

  // ... LinkedBinaryTree methods
  
  /** A node in this binary tree. */
  private static class Node<T> {
    
    /** The element stored in this node. */
    T data;

    /** A reference to the left child of this node. */
    Node<T> left;

    /** A reference to the right child of this node. */
    Node<T> right;

    // ... Node constructor
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class LinkedBinaryTree<T> {
  /** The node at the root of this tree */
  private Node<T> root; 

  // ... LinkedBinaryTree methods
  
  /** A node in this binary tree. */
  private static class Node<T> {
    
    /** The element stored in this node. */
    T data;

    /** A reference to the left child of this node. */
    Node<T> left;

    /** A reference to the right child of this node. */
    Node<T> right;

    // ... Node constructor
  }
}

Such a class design is suitable, but the branching structure of trees will make many operations (such as adding and locating new data elements) more complicated than their linked list counterparts. Since there is now a possibility that we could select the “wrong direction” as we traverse the links in the tree, we would need a way to backtrack and navigate down the other option. We can use a stack to help with the bookkeeping of this backtracking (as we will see later today when we discuss tree iterators). However, there is another tool we can use that leads to a more elegant approach for coding trees: recursion.

Recall that earlier we gave a recursive description of a tree, it is a data structure that stores an element (at its root) along with one or more child subtrees. We can use this description to design a recursive BinaryTree data structure that stores a root element and left and right fields that are references to other BinaryTree objects. The methods on BinaryTrees will also be recursive. Soon, we’ll discuss how to formulate operations in terms of (non-recursive) work on the root and a recursive call on its subtree(s). By using recursion, we allow the runtime stack to handle all of the bookkeeping and backtracking for us. Now that we’ve described the high-level recursive design of our BinaryTrees, let’s dig down further into more of the design decisions.

A BinaryTree Abstract Class

We will choose to model our BinaryTree type using an abstract class. This will allow us to leverage inheritance to extract some behaviors common to all binary trees (such as iterators) while leaving some implementation details up to the designer of the particular subclass. We’ll discuss one concrete subclass today and another more involved subclass (binary search trees) next lecture, which will motivate our choice of abstract methods.

In our recursive view of trees, any BinaryTree instance must store three things, its root element, its left subtree, and its right subtree. We’ll store the root element directly in the BinaryTree class (with generic type T) since this is consistent between different binary tree implementations. However, we will delegate the responsibility for declaring the left and right fields to the concrete subclass. The reason for this is static typing. Within the BinaryTree class, our left and right fields would need to have type BinaryTree. However, a subclass would want its left and right fields to have its own type in order to define and make use of its own recursive methods (without the need for casting). There is no mechanism for changing the type of a field in a subclass, so it’s better to delegate this responsibility. To provide access to the left and right subtrees from within the BinaryTree superclass, we can require that its subclasses provide left() and right() accessor methods by declaring these as abstract in the BinaryTree class. Overall, this leads to the following class design:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public abstract class BinaryTree<T> {
  /** The element at the root of this tree. */
  T root;

  /** Returns the left subtree of this tree or null if this tree does not have a left child. */
  abstract BinaryTree<T> left();

  /** Returns the right subtree of this tree or null if this tree does not have a right child. */
  abstract BinaryTree<T> right();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public abstract class BinaryTree<T> {
  /** The element at the root of this tree. */
  T root;

  /** Returns the left subtree of this tree or null if this tree does not have a left child. */
  abstract BinaryTree<T> left();

  /** Returns the right subtree of this tree or null if this tree does not have a right child. */
  abstract BinaryTree<T> right();
}

Here, we see another demonstration of the power of subtype substitution rules. Most likely, our concrete BinaryTree subclass will have left and right fields which have a strict subtype of BinaryTree. Nevertheless, these fields can still be returned by the left() and right() and utilized within the BinaryTree methods. Our BinaryTree abstract class will define all of its computations in terms of root, left(), and right().

Let’s pause to think about the appropriate visibility modifiers for these BinaryTree members. Recall that this is a question of proper encapsulation of our class. As is typical, we do not want the root field to be public because this would allow the client to modify it, possibly in violation of some class invariant. We also don’t want root to be private, as this would cut off access to it from the subclasses, who may want to directly inspect the elements of the tree. Thus, we’ll select protected visibility. Similarly, we will not want to give our client direct access to the left() and right() subtrees. Calling (public) methods on these subtrees could invalidate the class invariant of the “main” tree (as will be the case for binary search trees); we want the client to interact with the tree only from its root to avoid this possibility. Said differently, we want to encapsulate the BinaryTree’s recursive structure. To do so, we cannot make left() and right() public. We also cannot make them private; since they are abstract, they will need to be visible from the subclass, which must provide their definition. Again, protected is the appropriate choice.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public abstract class BinaryTree<T> {

  /** The element at the root of this tree. */
  protected T root;

  /** Returns the left subtree of this tree or null if this tree does not have a left child. */
  protected abstract BinaryTree<T> left();

  /** Returns the right subtree of this tree or null if this tree does not have a right child. */
  protected abstract BinaryTree<T> right();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public abstract class BinaryTree<T> {

  /** The element at the root of this tree. */
  protected T root;

  /** Returns the left subtree of this tree or null if this tree does not have a left child. */
  protected abstract BinaryTree<T> left();

  /** Returns the right subtree of this tree or null if this tree does not have a right child. */
  protected abstract BinaryTree<T> right();
}
Remark:

This provides another reason why modeling with an abstract class was preferable to an interface. Beyond the ability to give base definitions for some common behaviors (which can also be achieved by default interface methods), abstract classes allow for protected visibility of members. Interfaces primarily allow public method declarations. To achieve proper encapsulation, we must leverage inheritance, and to achieve the proper polymorphic behavior of our subtree classes, we are best off making the base BinaryTree class abstract.

Immutable Binary Trees

Before we start to define these methods, it will help to provide a basic BinaryTree subclass that we can use to visualize tree computations. We’ll do this by defining an ImmutableBinaryTree subclass. As an immutable class, we will not need to worry about allowing a client to modify the contents or arrangement of the tree after it has been constructed. Therefore, the constructor will be the only method to modify (or more appropriately, initialize) the fields, so we can mark the fields as final. We can directly return these fields from the left() and right() methods.

ImmutableBinaryTree.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ImmutableBinaryTree<T> extends BinaryTree<T> {
  /** The left subtree of this tree. */
  private final ImmutableBinaryTree<T> left;

  /** The right subtree of this tree. */
  private final ImmutableBinaryTree<T> right;

  /**
    * Constructs a binary tree with the given `root` value and `left` and `right` subtrees.
    */
  public ImmutableBinaryTree(T root, ImmutableBinaryTree<T> left, ImmutableBinaryTree<T> right) {
      this.root = root;
      this.left = left;
      this.right = right;
  }

  @Override
  protected BinaryTree<T> left() {
      return left;
  }

  @Override
  protected BinaryTree<T> right() {
      return right;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ImmutableBinaryTree<T> extends BinaryTree<T> {
  /** The left subtree of this tree. */
  private final ImmutableBinaryTree<T> left;

  /** The right subtree of this tree. */
  private final ImmutableBinaryTree<T> right;

  /**
    * Constructs a binary tree with the given `root` value and `left` and `right` subtrees.
    */
  public ImmutableBinaryTree(T root, ImmutableBinaryTree<T> left, ImmutableBinaryTree<T> right) {
      this.root = root;
      this.left = left;
      this.right = right;
  }

  @Override
  protected BinaryTree<T> left() {
      return left;
  }

  @Override
  protected BinaryTree<T> right() {
      return right;
  }
}

How do we instantiate this class to represent a particular tree? For example, suppose that we want to represent the following tree of Characters:

We will need to construct 4 ImmutableBinaryTrees for this representation, one representing the subtree rooted at each of the nodes \(a, b, c, d\). Since the child subtrees are an argument to the constructor of the parent, we’ll need to construct these 4 trees from the bottom up. First, we construct the subtree rooted at \(d\), which stores 'd' as its root and has neither a left nor a right subtree (so null in both of these fields).

1
ImmutableBinaryTree dTree = new ImmutableBinaryTree('d', null, null);
1
ImmutableBinaryTree dTree = new ImmutableBinaryTree('d', null, null);

Similarly, the subtree rooted at \(c\) has no child subtrees.

1
ImmutableBinaryTree cTree = new ImmutableBinaryTree('c', null, null);
1
ImmutableBinaryTree cTree = new ImmutableBinaryTree('c', null, null);

The subtree rooted at \(b\) has the node with \(d\) as its right child, so dTree as its right subtree.

1
ImmutableBinaryTree bTree = new ImmutableBinaryTree('b', null, dTree);
1
ImmutableBinaryTree bTree = new ImmutableBinaryTree('b', null, dTree);

Finally, subtree rooted at \(a\) (i.e., our entire desired tree) has bTree as its left subtree and cTree as its right subtree.

1
ImmutableBinaryTree tree = new ImmutableBinaryTree('a', bTree, cTree);
1
ImmutableBinaryTree tree = new ImmutableBinaryTree('a', bTree, cTree);

We can compact this instantiation into a single, nested constructor call as follows:

1
2
3
4
ImmutableBinaryTree tree = new ImmutableBinaryTree('a',
  new ImmutableBinaryTree('b', null, 
    new ImmutableBinaryTree('d', null, null)),
  ImmutableBinaryTree('c', null, null));
1
2
3
4
ImmutableBinaryTree tree = new ImmutableBinaryTree('a',
  new ImmutableBinaryTree('b', null, 
    new ImmutableBinaryTree('d', null, null)),
  ImmutableBinaryTree('c', null, null));

As is common in linked structures, the object diagram (drawn here with abbreviated class names for space reasons) for this tree closely resembles the tree structure itself.

Therefore, we will often eschew the formality of object diagrams and use node diagrams similar to the original tree picture above for our visualizations.

Recursive Methods on Trees

Let’s develop some methods for the BinaryTree class that leverage the recursive structure of trees. To start, let’s add a size() method that returns the total number of elements stored in the tree. One can imagine an iterative definition of this method that increments a counter while traversing over the nodes in the tree, backtracking when necessary to explore all of the branches. As we noted above, we can more easily accomplish this traversal recursively. Since left() and right() both return BinaryTrees, we can make the recursive calls left().size() and right().size() from within the size() method. These calls will return the number of nodes in the left subtree of the root and the right subtree of the root, respectively. How do we “combine” these return values to compute the size of the entire tree? To help see this, let’s label the sizes of all subtrees within a tree to spot the pattern.

We see that the size of each subtree is one more than the sum of the sizes of its child subtrees. This is because every node in a subtree is either (1) its root node, (2) in the left subtree of its root, or (3) in the right subtree of its root. Thus, the return value of size() should be 1 + left.size() + right.size(). The only caveat of this is that we need to make sure that the left and right subtrees exist (i.e., are not null) before we recurse on them, leading to the following method definition.

BinaryTree.java

1
2
3
4
5
6
/**
 * Returns the number of elements stored in this tree.
 */
public int size() {
  return 1 + (left() == null ? 0 : left().size()) + (right() == null ? 0 : right().size());
}
1
2
3
4
5
6
/**
 * Returns the number of elements stored in this tree.
 */
public int size() {
  return 1 + (left() == null ? 0 : left().size()) + (right() == null ? 0 : right().size());
}

This null-checking provides a natural base case for the method; leaf nodes have a null left subtree and a null right subtree, so calling size() on them results in no recursive calls and returns 1. Generally, when developing a recursive method on a tree, we ask,

How do I combine the answer from the left() subtree, the answer from the right() subtree, and the value at the root to obtain the answer for the whole tree?

As a second example, let’s add a height() method to our BinaryTree class that returns the height of the tree. Take some time to think about the above question, which we can specialize to ask, “How does the height of a tree relate to the height of its child subtrees?” Use your answer to complete the definition of this method, and compare your answer with our definition below.

height() definition

We can label the heights of all of the subtrees of the following tree as follows:
We see that height of a tree is 1 more than the height of its taller child subtree. When one or more of these subtrees is missing, we can imagine that their height is -1 to ensures that the subtrees of leaf nodes have height 0. This leads to the following definition.

BinaryTree.java

1
2
3
4
5
6
7
/**
 * Returns the height (i.e., maximum length of a path from root to leaf) of this binary tree.
 */
  public int height() {
      return 1 + Math.max((left() == null ? -1 : left().height()),
              (right() == null ? -1 : right().height()));
  }
1
2
3
4
5
6
7
/**
 * Returns the height (i.e., maximum length of a path from root to leaf) of this binary tree.
 */
  public int height() {
      return 1 + Math.max((left() == null ? -1 : left().height()),
              (right() == null ? -1 : right().height()));
  }

A more complicated recursive method, a toString() helper method that allows us to visualize the structure of a tree as a multiline String, is provided with the lecture release code. Here, the insight is that a String representation of the entire tree includes the String representations of both of its subtrees (padded with the appropriate amount of spacing) below and connecting to the String representation of the tree’s root. This method will be helpful for us in the next lecture, when we visualize operations on binary search trees. You can find more practice developing recursive methods on trees in the lecture exercises.

Binary Tree Iterators

As our final topic for today, we’ll consider how to iterate over a binary tree. While the linear structure of a list presented one natural traversal order (the index order), the branching structure of trees presents some ambiguity. Should we “visit” the root node before, during, or after traversing the subtrees? Should we traverse all the way down one path to a leaf node before backtracking or traverse the nodes level by level? We’ll present some of the most common traversals here and include others in the lecture exercises.

The traversal strategies that we’ll consider are recursive and consist of three steps.

The three traversal strategies we’ll consider, pre-order, in-order, and post-order traversals, differ in the order of these steps. We’ll always recurse on the left subtree before recursing on the right subtree. Then, the name of the traversal indicates when we “visit” the root relative to these recursive traversals.

Definition: Pre-Order Traversal

In a pre-order traversal, we visit the root node before traversing either subtree. Therefore, the pre-order traversal procedure is summarized by the steps:

  1. Visit the root.
  2. Recursively perform a pre-order traversal of the left subtree (if it exists).
  3. Recursively perform a pre-order traversal of the right subtree (if it exists).

The following animation walks through a pre-order traversal of the binary tree from the preceding section.

previous

next

Next, we’ll introduce in-order traversals.

Definition: In-Order Traversal

In an in-order traversal, we visit the root node between traversing the left and right subtrees (in that order). Therefore, the in-order traversal procedure is summarized by the steps:

  1. Recursively perform an in-order traversal of the left subtree (if it exists).
  2. Visit the root.
  3. Recursively perform an in-order traversal of the right subtree (if it exists).

Next lecture, we’ll see that an in-order traversal visits the elements of a binary search tree in sorted order. The following animation walks through an in-order traversal of our binary tree.

previous

next

Finally, we’ll introduce post-order traversals.

Definition: Post-Order Traversal

In a post-order traversal, we visit the root node after traversing its child subtrees. Therefore, the post-order traversal procedure is summarized by the steps:

  1. Recursively perform a post-order traversal of the left subtree (if it exists).
  2. Recursively perform a post-order traversal of the right subtree (if it exists).
  3. Visit the root.

The following animation walks through a post-order traversal of our binary tree.

previous

next

We can define three different iterators to produce the elements of a binary tree according to these three traversal orders, a pre-order iterator, an in-order iterator, and a post-order iterator. Each of these will be accessed by the client through a public method of the BinaryTree class that constructs and returns an instance of a static nested iterator class. We’ll work through the design of these iterator classes in the following sections.

BinaryTree.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BinaryTree<T> {
  /** Return a pre-order iterator over the elements in this binary tree. */
  public Iterator<T> preorderIterator() {
    return new PreorderIterator<>(this);
  }

  /** Return an in-order iterator over the elements in this binary tree. */
  public Iterator<T> inorderIterator() {
    return new InorderIterator<>(this);
  }

  /** Return a post-order iterator over the elements in this binary tree. */
  public Iterator<T> postorderIterator() {
    return new PostorderIterator<>(this);
  }

  /** A pre-order iterator over the elements in this binary tree. */
  private static class PreorderIterator<T> implements Iterator<T> { ... }

  /** An in-order iterator over the elements in this binary tree. */
  private static class InorderIterator<T> implements Iterator<T> { ... }

  /** A post-order iterator over the elements in this binary tree. */
  private static class PostorderIterator<T> implements Iterator<T> { ... }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BinaryTree<T> {
  /** Return a pre-order iterator over the elements in this binary tree. */
  public Iterator<T> preorderIterator() {
    return new PreorderIterator<>(this);
  }

  /** Return an in-order iterator over the elements in this binary tree. */
  public Iterator<T> inorderIterator() {
    return new InorderIterator<>(this);
  }

  /** Return a post-order iterator over the elements in this binary tree. */
  public Iterator<T> postorderIterator() {
    return new PostorderIterator<>(this);
  }

  /** A pre-order iterator over the elements in this binary tree. */
  private static class PreorderIterator<T> implements Iterator<T> { ... }

  /** An in-order iterator over the elements in this binary tree. */
  private static class InorderIterator<T> implements Iterator<T> { ... }

  /** A post-order iterator over the elements in this binary tree. */
  private static class PostorderIterator<T> implements Iterator<T> { ... }
}

Unlike the other tree methods that we have written, tree iterator methods do not admit a natural recursive definition that leverages the runtime stack to track the state of the traversal. This is because we must return control to the client after each call to next(), and this will remove any call frames of our recursive methods. Instead, we’ll need to use fields to keep track of the iterator’s current position. All three iterators we’ll design will maintain a stack of BinaryTrees (which we can think of as a stack of their root nodes) with the invariant that the top node in the stack is the next() one to be returned. Therefore, the hasNext() methods of these iterators will simply check whether this stack is empty. We’ll need to do more work, specialized to each iterator to properly initialize this stack and update it during each next() call.

Implementing a Pre-order Iterator

In a pre-order iterator, the first element that is returned by next() is the root element, and a subtree’s root will always be returned before any nodes in its subtrees. To meet our class invariant on the stack, we’ll initialize it to contain the entire tree in the constructor. Within next(), we’ll store the node we are visiting and then push its children (if they exist) onto the stack to be traversed next. Because of the LIFO ordering of the stack, we must push() these trees in the opposite order to how we want them traversed, so right() followed by left().

BinaryTree.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class PreorderIterator<T> implements Iterator<T> {
  /**
   * Keeps track of state of the tree traversal. While `stack` is non-empty, 
   * the root of its top element the `next()` element to be returned.
   */
  private final Deque<BinaryTree<T>> stack;

  /** Constructs a new pre-order iterator over the given `tree`. */
  public PreorderIterator(BinaryTree<T> tree) {
    stack = new LinkedList<>();
    if (tree != null && tree.root != null) {
      stack.push(tree);
    }
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public T next() {
    assert hasNext();
    BinaryTree<T> visiting = stack.pop();
    if (visiting.right() != null) {
      stack.push(visiting.right()); // push right first so stack visits it second
    }
    if (visiting.left() != null) {
      stack.push(visiting.left());
    }
    return visiting.root;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class PreorderIterator<T> implements Iterator<T> {
  /**
   * Keeps track of state of the tree traversal. While `stack` is non-empty, 
   * the root of its top element the `next()` element to be returned.
   */
  private final Deque<BinaryTree<T>> stack;

  /** Constructs a new pre-order iterator over the given `tree`. */
  public PreorderIterator(BinaryTree<T> tree) {
    stack = new LinkedList<>();
    if (tree != null && tree.root != null) {
      stack.push(tree);
    }
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public T next() {
    assert hasNext();
    BinaryTree<T> visiting = stack.pop();
    if (visiting.right() != null) {
      stack.push(visiting.right()); // push right first so stack visits it second
    }
    if (visiting.left() != null) {
      stack.push(visiting.left());
    }
    return visiting.root;
  }
}

Note that the stack field has static type Deque, the Java library’s stack interface and is initialized to a LinkedList reference, since the LinkedList class implements Deque. The following animation visualizes the state of the stack over the lifetime of a PreorderIterator.

previous

next

The stack can grow as large as the number of levels in the tree during the lifetime of the iterator. Therefore, this pre-order iterator will utilize \(O(\textrm{height})\) memory.

Implementing an In-order Iterator

The in-order iterator is a bit more involved. While the pre-order iterator initialized the stack with the root and “stepped down” at most one level in the tree for each call to next(), pop()ping one tree and push()ing its child subtrees, an in-order iterator may need to walk down multiple levels of the tree. For example, during initialization, the in-order iterator needs to get the “leftmost” leaf to the top of the stack, as this is the first element that must be returned. To find this “leftmost” leaf, we’ll need to repeatedly follow left references in the tree until we reach a leaf, push()ing onto the stack as we go.

We’ll call this operation a left “cascade” and carry it out with a private recursive helper method cascadeLeft().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * Walks down the left children of `tree` until a leaf node is reached, 
 * pushing all subtrees onto the stack in the process.
 */
private void cascadeLeft(BinaryTree<T> tree) {
  if (tree != null && tree.root != null) {
    stack.push(tree);
    cascadeLeft(tree.left());
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * Walks down the left children of `tree` until a leaf node is reached, 
 * pushing all subtrees onto the stack in the process.
 */
private void cascadeLeft(BinaryTree<T> tree) {
  if (tree != null && tree.root != null) {
    stack.push(tree);
    cascadeLeft(tree.left());
  }
}

Within the next() method, we’ll again store the node we are visiting. After we return its element, the subsequent call to next() should begin traversal of its right subtree (if it exists). To prepare for this, we need to call cascadeLeft() on visiting.right().

BinaryTree.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private static class InorderIterator<T> implements Iterator<T> {
  /**
   * Keeps track of state of the tree traversal. While `stack` is non-empty, 
   * the root of its top element the `next()` element to be returned.
   */
  private final Deque<BinaryTree<T>> stack;

  /** Constructs a new in-order iterator over the given `tree`. */
  public InorderIterator(BinaryTree<T> tree) {
    stack = new LinkedList<>();
    cascadeLeft(tree);
  }

  /**
   * Walks down the left children of `tree` until a leaf node is reached, 
   * pushing all subtrees onto the stack in the process.
   */
  private void cascadeLeft(BinaryTree<T> tree) {
    if (tree != null && tree.root != null) {
      stack.push(tree);
      cascadeLeft(tree.left());
    }
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public T next() {
    assert hasNext();
    BinaryTree<T> visiting = stack.pop();
    if (visiting.right() != null) {
      cascadeLeft(visiting.right());
    }
    return visiting.root;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private static class InorderIterator<T> implements Iterator<T> {
  /**
   * Keeps track of state of the tree traversal. While `stack` is non-empty, 
   * the root of its top element the `next()` element to be returned.
   */
  private final Deque<BinaryTree<T>> stack;

  /** Constructs a new in-order iterator over the given `tree`. */
  public InorderIterator(BinaryTree<T> tree) {
    stack = new LinkedList<>();
    cascadeLeft(tree);
  }

  /**
   * Walks down the left children of `tree` until a leaf node is reached, 
   * pushing all subtrees onto the stack in the process.
   */
  private void cascadeLeft(BinaryTree<T> tree) {
    if (tree != null && tree.root != null) {
      stack.push(tree);
      cascadeLeft(tree.left());
    }
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public T next() {
    assert hasNext();
    BinaryTree<T> visiting = stack.pop();
    if (visiting.right() != null) {
      cascadeLeft(visiting.right());
    }
    return visiting.root;
  }
}

The following animation visualizes the state of the stack over the lifetime of an InorderIterator.

previous

next

Again, the stack can grow as large as the number of levels in the tree during the lifetime of the iterator. Therefore, this in-order iterator will utilize \(O(\textrm{height})\) memory.

Implementing a Post-order Iterator

The most intricate iterator is the post-order iterator. Again, we will need a cascade() helper method to initialize the state of the stack. However, this time, the cascade must always terminate at a leaf node; all children are always visited before their parent in a post-order traversal. This means that the cascade will need to recurse right in the case that a node does not have a left child.

Beyond this, we will need to check whether the node we are visiting in next() is the left() or right() child of its parent. We can access the parent via stack.peek() since a node will always sit directly above its parent on the stack. When visiting is the left() child, we will need to cascade() on stack.peek().right() (its parent node’s right subtree, if it exists) to ensure that this gets traversed before the parent. The full code for the post-order iterator is given below.

BinaryTree.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static class PostorderIterator<T> implements Iterator<T> {
  /**
   * Keeps track of state of the tree traversal. While `stack` is non-empty, 
   * the root of its top element the `next()` element to be returned.
   */
  private final Deque<BinaryTree<T>> stack;

  /** Constructs a new post-order iterator over the given `tree`. */
  public PostorderIterator(BinaryTree<T> tree) {
    stack = new LinkedList<>();
    if (tree != null && tree.root != null) {
      cascade(tree);
    }
  }

  /**
    * Walks down the left children of `tree` until a leaf node is reached, 
    * pushing all subtrees onto the stack in the process. If any node has a 
    * right child but no left child, the cascade proceeds to the right.
    */
  private void cascade(BinaryTree<T> tree) {
    if (tree != null && tree.root != null) {
      stack.push(tree);
      cascade(tree.left() == null ? tree.right() : tree.left());
    }
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public T next() {
    assert hasNext();
    BinaryTree<T> visiting = stack.pop();
    if (!stack.isEmpty() && stack.peek().left() == visiting) {
      // we were exploring left subtree of `visiting`'s parent `stack.peek()`
      // must explore the parent's right subtree before returning it
      if (stack.peek().right() != null) {
        cascade(stack.peek().right());
      }
    }
    return visiting.root;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static class PostorderIterator<T> implements Iterator<T> {
  /**
   * Keeps track of state of the tree traversal. While `stack` is non-empty, 
   * the root of its top element the `next()` element to be returned.
   */
  private final Deque<BinaryTree<T>> stack;

  /** Constructs a new post-order iterator over the given `tree`. */
  public PostorderIterator(BinaryTree<T> tree) {
    stack = new LinkedList<>();
    if (tree != null && tree.root != null) {
      cascade(tree);
    }
  }

  /**
    * Walks down the left children of `tree` until a leaf node is reached, 
    * pushing all subtrees onto the stack in the process. If any node has a 
    * right child but no left child, the cascade proceeds to the right.
    */
  private void cascade(BinaryTree<T> tree) {
    if (tree != null && tree.root != null) {
      stack.push(tree);
      cascade(tree.left() == null ? tree.right() : tree.left());
    }
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public T next() {
    assert hasNext();
    BinaryTree<T> visiting = stack.pop();
    if (!stack.isEmpty() && stack.peek().left() == visiting) {
      // we were exploring left subtree of `visiting`'s parent `stack.peek()`
      // must explore the parent's right subtree before returning it
      if (stack.peek().right() != null) {
        cascade(stack.peek().right());
      }
    }
    return visiting.root;
  }
}

The following animation visualizes the state of the stack over the lifetime of an PostorderIterator.

previous

next

For the third time, the stack can grow as large as the number of levels in the tree during the lifetime of the iterator. Therefore, this post-order iterator will utilize \(O(\textrm{height})\) memory. Starting in the next lecture, we will use the foundational binary trees that we have just developed as building blocks for more complicated data structures with stronger invariants and performance guarantees.

Main Takeaways:

  • Trees connect data in a branching structure from its single root node to one or more leaf nodes. Trees provide a good way to model hierarchical structures such as genealogies, file systems, and grammatical expressions.
  • It is often useful to view trees from a recursive perspective, where each non-leaf parent node has one or more child subtrees. Recursive methods on trees typically make recursive calls on their child subtrees and combine these results with their root value.
  • The size of a tree is its number of nodes, and its height is one less than its number of levels. Its arity bounds the number of children its nodes can have. Binary trees have arity 2.
  • Pre-order, in-order, and post-order iterators provide different ways for traversing the elements of a tree. We use stacks to keep track of the state of these iterators between next() calls.

Exercises

Exercise 16.1: Check Your Understanding
(a)
What is the maximum number of nodes in a binary tree with height 3?
Check Answer
(b)
What is the maximum number of nodes in a binary tree with height 3?
Check Answer
Consider the following binary tree.
(c)
Which nodes are at the same depth as f?
Check Answer
(d)
The subtrees rooted at which nodes have size 3?
Check Answer
(e)

What are the pre-order, in-order, and post-order traversal orders of this binary tree?

Pre-order traversal

Pre-order: a, b, c, e, f, d, g, h

In-order traversal

In-order: e, c, f, b, d, h, g, a

Post-order traversal

Post-order: e, f, c, h, g, d, b, a

Exercise 16.2: Mutable Binary Trees
Our ImmutableBinaryTree prevented clients from changing the children of a tree after instantiation. Often, we want to change the structure of a tree by adding or removing nodes. To support this, define a new MutableBinaryTree that extends BinaryTree. This class should include 2 extra methods, setLeft() and setRight(), to change the structure of the tree.
1
2
/** A mutable binary tree. */
public class MutableBinaryTree<T> extends BinaryTree<T> { ... }
1
2
/** A mutable binary tree. */
public class MutableBinaryTree<T> extends BinaryTree<T> { ... }

Exercise 16.3: Binary Trees with Parent Pointers
Currently, our binary trees only store pointers to their children. This is sufficient for top-down traversal. However, if we ever want to go up, finding the parent of a particular node could take \(O(N)\) time, where \(N\) is the size of the tree. Consider the following binary tree subclass where each node additionally stores a pointer to its parent.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/** A mutable binary tree with parent pointers. */
public class ParentedBinaryTree<T> extends BinaryTree<T> {
  /** The parent of a node. `null` if the root of the tree. */
  private ParentedBinaryTree<T> parent;

  /** The left subtree of this tree. */
  private ParentedBinaryTree<T> left;

  /** The right subtree of this tree. */
  private ParentedBinaryTree<T> right;

  // constructors and methods
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/** A mutable binary tree with parent pointers. */
public class ParentedBinaryTree<T> extends BinaryTree<T> {
  /** The parent of a node. `null` if the root of the tree. */
  private ParentedBinaryTree<T> parent;

  /** The left subtree of this tree. */
  private ParentedBinaryTree<T> left;

  /** The right subtree of this tree. */
  private ParentedBinaryTree<T> right;

  // constructors and methods
}
(a)
Complete the implementation of this class. Note that this tree should be mutable.
(b)

While recursion makes methods like size() and height() elegant, it also requires us to traverse the entire tree structure for each method call. This new parent pointer allows us to support cached computations of these methods. Suppose we add the following new field to our class:

1
2
3
4
5
6
public class ParentedBinaryTree<T> extends BinaryTree<T> {
  /** The (cached) size of this subtree. -1 if the cache is invalid. */
  private int size;

  // other fields and methods
}
1
2
3
4
5
6
public class ParentedBinaryTree<T> extends BinaryTree<T> {
  /** The (cached) size of this subtree. -1 if the cache is invalid. */
  private int size;

  // other fields and methods
}
Override size() to utilize this cache. What is the best-case and worst-case runtime for this method?
(c)
This cached result is only valid as long as we make no modifications to the subtree. However, if we ever add to this mutable tree, we must invalidate some caches. When you add a node to this tree, which nodes’ cached size value are now invalid? Update your methods that modify this tree to properly invalidate caches and re-establish the class invariant.
Exercise 16.4: More Recursive BinaryTree Methods
Implement each of the following methods according to its specifications. Unless specified, assume these methods are defined within the BinaryTree class.
(a)
1
2
/** Returns whether `target` is in this (sub)tree. */
public boolean contains(T target) { ... }
1
2
/** Returns whether `target` is in this (sub)tree. */
public boolean contains(T target) { ... }
(b)
1
2
/** Returns the number of times `target` appears in this (sub)tree. */
public int frequencyOf(T target) { ... }
1
2
/** Returns the number of times `target` appears in this (sub)tree. */
public int frequencyOf(T target) { ... }
(c)
1
2
/** Returns the minimum depth of any leaf node in this tree. */
public int minDepthLeaf() { ... }
1
2
/** Returns the minimum depth of any leaf node in this tree. */
public int minDepthLeaf() { ... }
When we "mirror" a binary tree, each of its node's left and right children are swapped.
(d)

Define the following mirror() within the ImmutableBinaryTree class.

1
2
/** Returns a mirrored version of this tree. */
public ImmutableBinaryTree<T> mirror() { ... }
1
2
/** Returns a mirrored version of this tree. */
public ImmutableBinaryTree<T> mirror() { ... }
Exercise 16.5: General Trees
We modeled each node (or subtree) in a binary tree using two fields that stored pointers to its left and right subtree. A node in a general tree can have an arbitrary number of children, so we cannot have a separate field for each one. Rather, we can store the pointers to all of these children in a CS2110List. Consider the following class:
1
2
3
4
5
6
7
8
/** A general tree. */
public class GeneralTree<T> {
  /** The element at the root of this tree. */
  private T root;

  /** The children of this tree. */
  private CS2110List<GeneralTree<T>> children; 
}
1
2
3
4
5
6
7
8
/** A general tree. */
public class GeneralTree<T> {
  /** The element at the root of this tree. */
  private T root;

  /** The children of this tree. */
  private CS2110List<GeneralTree<T>> children; 
}
(a)

Define a constructor for this class. Does it matter which implementation of CS2110List we choose?

1
2
/** Creates a general tree with value `root` and `children`. */
public GeneralTree(T root, CS2110List<GeneralTree<T>> children) { ... }
1
2
/** Creates a general tree with value `root` and `children`. */
public GeneralTree(T root, CS2110List<GeneralTree<T>> children) { ... }
(b)

Implement a method to add a child to a node.

1
2
/** Adds `child` to this node. */
public void addChild(GeneralTree<T> child) { ... }
1
2
/** Adds `child` to this node. */
public void addChild(GeneralTree<T> child) { ... }
Finally, develop the following methods for the GeneralTree. Your solution will likely involve a mix of iteration and recursion.
(c)
1
2
/** Returns the number of elements stored in this tree. */
public int size() { ... }
1
2
/** Returns the number of elements stored in this tree. */
public int size() { ... }
(d)
1
2
3
4
/**
 * Returns the height (i.e., maximum length of a path from root to leaf) of this binary tree.
 */
public int height() { ... }
1
2
3
4
/**
 * Returns the height (i.e., maximum length of a path from root to leaf) of this binary tree.
 */
public int height() { ... }
Exercise 16.6: Recursive Traversals
We implemented our traversals using Iterators in the lecture notes. This allows the clients to perform any actions of their choice before visiting the next() node. Suppose instead that we just want to visualize, i.e. print, the elements in each traversal order. In this case, the entire traversal can be performed in one method call. Use recursion to provide a succinct definition of these traversal printing methods.
(a)
1
2
/** Prints the pre-order traversal of the nodes in `tree`. */
public static <T> void printPreorder(BinaryTree<T> tree) { ... }
1
2
/** Prints the pre-order traversal of the nodes in `tree`. */
public static <T> void printPreorder(BinaryTree<T> tree) { ... }
(b)
1
2
/** Prints the in-order traversal of the nodes in `tree`. */
public static <T> void printInorder(BinaryTree<T> tree) { ... }
1
2
/** Prints the in-order traversal of the nodes in `tree`. */
public static <T> void printInorder(BinaryTree<T> tree) { ... }
(c)
1
2
/** Prints the post-order traversal of the nodes in `tree`. */
public static <T> void printPostorder(BinaryTree<T> tree) { ... }
1
2
/** Prints the post-order traversal of the nodes in `tree`. */
public static <T> void printPostorder(BinaryTree<T> tree) { ... }

Exercise 16.7: Level Order Traversal
A level order traversal of a binary tree iterates through the nodes level by level from left to right. For instance, the level order traversal of the tree used throughout Section 16.3 is the letters in alphabetical order. Implement a LevelOrderIterator.

Hint: use a queue to keep track of the order of nodes to be visited.
Exercise 16.8: Leaf Only Iterator
Design an iterator that iterates over only the leaves of a binary tree. The order in which the leaves are yielded is up to you, but ensure this is clearly communicated in the specification.

Exercise 16.9: Reconstructing Trees from Traversals
Each traversal produces a linear representation of a tree. Can we go the other way?
(a)

Given the following pre-order and in-order traversals, draw the binary tree that produced these.

  • Pre-order Traversal: [A, B, D, E, C, F]
  • In-order Traversal: [D, B, E, A, F, C]
(b)

Given some arbitrary pre-order and in-order traversal of a binary tree, implement a method to return this binary tree. It can be shown that this binary tree is unique.

1
2
3
4
5
6
/** 
 * Returns the binary tree that corresponds to `preorder` and `inorder`.
 * Requires `preorder.size() == inorder.size()` and `preorder` and `inorder`
 * are traversals of the same binary tree.
 */
public static <T> BinaryTree<T> buildFromPreIn(CS2110List<T> preorder, CS2110List<T> inorder) { ... }
1
2
3
4
5
6
/** 
 * Returns the binary tree that corresponds to `preorder` and `inorder`.
 * Requires `preorder.size() == inorder.size()` and `preorder` and `inorder`
 * are traversals of the same binary tree.
 */
public static <T> BinaryTree<T> buildFromPreIn(CS2110List<T> preorder, CS2110List<T> inorder) { ... }
(c)
Write another method to return the binary tree that corresponds to a pair of in-order and post-order traversals.
(d)
It might seem that knowing both the pre-order and post-order traversals of a binary tree would also be enough to reconstruct it. Show that this is not the case by giving an example of pre-order and post-order sequences that can be generated by more than one valid binary tree.
Exercise 16.10: Reverse Polish Notation
When we write normal arithmetic expressions (called infix notation), we rely on parentheses and operator precedence rules to determine the order of evaluation. As we've seen in the lecture notes, dealing with these parentheses can be very cumbersome. Instead, compilers, interpreters, and some calculators use Reverse Polish Notation (also called postfix notation), where every operator comes after its operands. So instead of writing 3 + 4 * 2, we write: 3 4 2 * +.
(a)

Given a well-formed postfix arithmetic expression in the form of a string only containing digits and the four common operators (+, -, *, /) with each token separated by a space, evaluate the expression.

1
2
3
4
5
/** 
 * Evaluate a RPN expression. `expr` contains only digits and +, -, *, /, with
 * each token separated by a space, and is in valid postfix notation.
 */
public static double evalRPN(String expr) { ... }
1
2
3
4
5
/** 
 * Evaluate a RPN expression. `expr` contains only digits and +, -, *, /, with
 * each token separated by a space, and is in valid postfix notation.
 */
public static double evalRPN(String expr) { ... }
(b)
Unary functions, such as \(\ln()\) and \(\sin()\), are easily integrated into postfix notation. For instance, the infix expression \(\sin(3 + 4)\) can be written as \(3\;4 + \sin()\). Edit evalRPN to support the unary functions, \(\sin()\), \(\cos()\), \(\exp()\), and \(\ln()\). All of these functions are supported by the Math class.