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,
- In genealogical data sets (that model the relationships among members of a family), we’d like a structure that allows individuals to be close to their parents and children. Since one person can have many children, some of these children could end up far from their parents in a linear structure.
- In a computer file system, one directory (i.e., folder) can contain many files or other directories. A linear structure does not afford an easy way to navigate between different levels of files in an explorer.
- In graphical applications, the engine needs to keep track of different components and how they interact to render them appropriately to the screen. When the user interacts with one component (such as a character sprite), this may affect the arrangement of multiple other components on the screen (e.g., if the character moves, this affects the position of an object they were holding as well). We need a data structure that allows us to model these connections.
- In Java’s class hierarchy, many different subclasses can all
extend
from the same superclass. There isn’t a natural linear order to arrange all of the classes in a way that illuminates these inheritance relationships.
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 Character
s.
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.
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\).
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.
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.
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.
- A node with no outgoing connections (i.e., a leaf node) is a tree.
- A node and its outgoing connections to one or more trees (its children’s subtrees) form a tree.
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.
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.
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.
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.
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.
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.
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 Node
s, 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
|
|
|
|
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 BinaryTree
s 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 BinaryTree
s, 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:
|
|
|
|
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.
|
|
|
|
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
|
|
|
|
How do we instantiate this class to represent a particular tree? For example, suppose that we want to represent the following tree of Character
s:
We will need to construct 4 ImmutableBinaryTree
s 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).
|
|
|
|
Similarly, the subtree rooted at \(c\) has no child subtrees.
|
|
|
|
The subtree rooted at \(b\) has the node with \(d\) as its right child, so dTree
as its right subtree.
|
|
|
|
Finally, subtree rooted at \(a\) (i.e., our entire desired tree
) has bTree
as its left subtree and cTree
as its right subtree.
|
|
|
|
We can compact this instantiation into a single, nested constructor call as follows:
|
|
|
|
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 BinaryTree
s, 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
|
|
|
|
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,
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
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.
- “Visit” the root node.
- Recursively traverse the left subtree of the root (using the same traversal strategy).
- Recursively traverse the right subtree of the root (using the same traversal strategy).
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.
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:
- Visit the root.
- Recursively perform a pre-order traversal of the left subtree (if it exists).
- 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.
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:
- Recursively perform an in-order traversal of the left subtree (if it exists).
- Visit the root.
- 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.
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:
- Recursively perform a post-order traversal of the left subtree (if it exists).
- Recursively perform a post-order traversal of the right subtree (if it exists).
- 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
|
|
|
|
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 BinaryTree
s (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
|
|
|
|
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()
.
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
f
?What are the pre-order, in-order, and post-order traversal orders of this binary tree?
Pre-order traversal
In-order traversal
Post-order traversal
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.
|
|
|
|
|
|
|
|
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:
|
|
|
|
size()
to utilize this cache. What is the best-case and worst-case runtime for this method?
BinaryTree
Methods
BinaryTree
class.
|
|
|
|
|
|
|
|
|
|
|
|
Define the following mirror()
within the ImmutableBinaryTree
class.
|
|
|
|
CS2110List
. Consider the following class:
|
|
|
|
Define a constructor for this class. Does it matter which implementation of CS2110List
we choose?
|
|
|
|
Implement a method to add a child to a node.
|
|
|
|
GeneralTree
. Your solution will likely involve a mix of iteration and recursion.
|
|
|
|
|
|
|
|
Iterator
s 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.
|
|
|
|
|
|
|
|
|
|
|
|
LevelOrderIterator
.Hint: use a queue to keep track of the order of nodes to be visited.
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]
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.
|
|
|
|
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.
|
|
|
|
evalRPN
to support the unary functions, \(\sin()\), \(\cos()\), \(\exp()\), and \(\ln()\). All of these functions are supported by the Math
class.