25. Binary Trees
A general tree is one where each node can have any number of children
An n-ary tree is one where each node has no more than \(n\) children
25.1. Binary Tree Definition
A binary tree is one where each node has no more than two (2) children
2-ary tree
Tree with a degree/arity of 2
If a node has children, they are referred to as the left and right children
Which are also referred to as the left and right subtrees
In the above example, B is the left subtree and C is the right subtree of A
In a binary tree, position matters
It matters if a subtree is the left or right
A recursive definition of a binary tree is
An empty tree
Or, a tree that has a root whose left and right subtrees are binary trees
Note
Based on this information, what would a unary tree be?
25.2. Traversals
With a simple linear structure, the order in which the nodes are traversed is rather natural
Start at one end and go to the other
For example, start at the head node and visit each node’s next until there are no more nodes
Other traversal orders could be defined for linear collections if desired, but that would be atypical
With a nonlinear data structure like a binary tree, the order to traverse the nodes in is not immediately obvious
There are a few common options to choose from
25.2.1. Pre-order
A pre-order traversal is a common order to traverse a binary tree
The general idea is
Start at the root
Access the node, then go to the left child, then the right child
To get more precise in a recursive definition
1Define PreOrderTraversal
2 If the node exists
3 Access the node
4 Call PreOrderTraversal on the left child node
5 Call PreOrderTraversal on the right child node
Notice that the root of the (sub)tree is accessed before (pre-) any recursive calls
25.2.2. In-order
An in-order traversal is another common traversal
The general idea is
Start at the root
Go to the left child, Access the node, then the right child
To get more precise in a recursive definition
1Define InOrderTraversal
2 If the node exists
3 Call InOrderTraversal on the left child node
4 Access the node
5 Call InOrderTraversal on the right child node
Notice that the root of the (sub)tree is accessed in between any recursive calls
25.2.3. Post-order
Take a wild guess at what this one will be
A post-order traversal is another traversal
The general idea is
Start at the root
Go to the left child, then the right child, then Access the node,
To get more precise in a recursive definition
1Define PostOrderTraversal
2 If the node exists
3 Call PostOrderTraversal on the left child node
4 Call PostOrderTraversal on the right child node
5 Access the node
Notice that the root of the (sub)tree is accessed after (post-) any recursive calls
25.2.4. Level-order
A level-order traversal is a little different when compared to the others
The search doesn’t work it’s way down each branch of the tree one by one
Instead, it traverses the breadth of the tree on the way down all branches
The idea is
Start at the root
Visit the nodes in each level from left to right
With this idea, there is no immediately obvious recursive definition of this traversal
An iterative definition of the traversal is perhaps simpler to derive
1Define LevelOrderTraversal
2 If the root node exists
3 Enqueue the root node to a queue
4
5 While the queue is not empty
6 Dequeue a node
7 Access the dequeued node
8
9 If the left child exists
10 Enqueue the left child to the queue
11
12 If the right child exists
13 Enqueue the right child to the queue
25.2.5. Iterative Pre/In/Post-Order
With the iterative level-order traversal, a queue was used
What would happen if a stack was used?
With the recursive pre-/in-/post-order traversals, a stack was used
The call stack
No directly instantiated stack data structure was used, but one could have been used
How would the level-order traversal need to be changed to do a pre-/in-/post-order traversal?
25.2.6. Traversal Analysis
Consider a binary tree with \(n\) nodes
If all \(n\) nodes are to be visited, what is the computational complexity of
pre-order traversal?
in-order traversal?
post-order traversal?
level-order traversal?
Intuitively, they’re all \(O(n)\) since all \(n\) nodes must be visited once and only once
If the question was changed slightly, consider a binary tree with height \(h\)
What is the computational complexity of the traversals?
Consider the relationship between the height of a binary tree and the number of nodes within the tree
\(O(2^{h})\)
25.3. Interface
What would a binary tree need to do?
Check if an element exists in the tree
Count the number of occurrences of a given element
Check if its empty
Get the size
Traverse the tree
Add elements
But where?
Remove elements
Which one, from where?
Adding/removing something to a stack and queue was more straightforward
Pushing and popping happened at the top of the stack
Enqueuing and dequeueing happen at opposite ends
For a binary tree, with add and remove, what is wanted/what it means will depend on the type of binary tree
Similar to the bag, elements must be able to be added and removed from the binary tree
But what what exactly add and remove means may differ depending on the specific type of binary tree
1import java.util.Iterator;
2import java.util.NoSuchElementException;
3
4public interface BinaryTree<T> extends Iterable<T> {
5
6 boolean add(T element);
7 boolean remove(T element);
8 boolean contains(T element);
9 int count(T target);
10 boolean isEmpty();
11 int size();
12 Iterator<T> iterator();
13 Iterator<T> preOrderIterator();
14 Iterator<T> inOrderIterator();
15 Iterator<T> postOrderIterator();
16 Iterator<T> levelOrderIterator();
17}
25.4. Linked Implementation
No binary tree implementation is being created; it will be inherited from for specific binary tree implementations
For example, a
BinarySearchTree
One way to implement a binary tree is with a collection of linked nodes
Use a size field to keep track of the number of elements within the tree
Use a field to reference the root node
Like how a reference was used to keep track of the top of a stack or the front of a queue
25.4.1. Binary Tree Node
Until now, the node class has only had a single successor
However, there is no rule saying that there can’t be more than one successor
Here, have the node contain:
A reference to some element
A reference to a left child
A reference to a right child
A new
Node
a standalone class, but this may cause confusion between singly linked nodes and nodes with two successorsA simple way to address this is to make the
Node
class a static nested class inside the specificBinaryTree
implementation
462 /**
463 * A node class for a linked binary tree structure. Each node contains a nullable reference to data of type T, and a
464 * reference to the left and right child nodes, which may be null references.
465 *
466 * @param <T> Type of the data being stored in the node
467 */
468 private static class Node<T> {
469 private T data;
470 private Node<T> left;
471 private Node<T> right;
472
473 private Node() {
474 this(null);
475 }
476
477 private Node(T data) {
478 this.data = data;
479 this.left = null;
480 this.right = null;
481 }
482
483 private T getData() {
484 return data;
485 }
486
487 private void setData(T data) {
488 this.data = data;
489 }
490
491 private Node<T> getLeft() {
492 return left;
493 }
494
495 private void setLeft(Node<T> left) {
496 this.left = left;
497 }
498
499 private Node<T> getRight() {
500 return right;
501 }
502
503 private void setRight(Node<T> right) {
504 this.right = right;
505 }
506 }
25.4.2. Linked Binary Tree
Although there will be no implementation of a general
BinaryTree
, general binary tree related methods can be discussed
25.4.2.1. size
Given some arbitrary binary tree with no
size
field, the number of elements can be counted recursivelyIf the current node exists, then the size of the (sub)tree it is the root of will be 1 + the size of the left subtree + the size of the right subtree
1public int size() {
2 return size(root);
3}
4
5private int size(Node<T> current) {
6 if (current == null) {
7 return 0;
8 } else {
9 return 1 + size(current.getLeft()) + size(current.getRight());
10 }
11}
Here a public setup method is used to start the private recursive method call on the root
What is the computational complexity of this
size()
method?\(O(n)\), where \(n\) is the number of nodes in the tree
25.4.2.2. contains
Given some arbitrary binary tree, a specific element can be searched for recursively
If the current element is what is being looking for, done
Otherwise, check the left subtree
If it wasn’t found in the left subtree, then check the right subtree
1public boolean contains(T needle) {
2 return contains(root, needle);
3}
4
5private boolean contains(Node<T> current, T needle) {
6 if (current == null) {
7 return false;
8 } else if (Objects.equals(current.getData(), needle)) {
9 return true;
10 } else {
11 return contains(current.getLeft(), needle) || contains(current.getRight(), needle);
12 }
13}
Mind the use of the short-circuit or in the above example
What is the computational complexity of this
contains()
method?\(O(n)\), where \(n\) is the number of nodes in the tree
Although the right subtree may not have been searched, the worst case scenario is considered
25.4.2.3. Traversals
Preorder traversal printing out the elements
1public void preOrder() {
2 preOrder(root);
3}
4
5private void preOrder(Node<T> current) {
6 if (current != null) {
7 System.out.println(current.getData());
8 preOrder(current.getLeft());
9 preOrder(current.getRight());
10 }
11}
An inorder traversal, but instead of printing out the contents, add them to some other collection
1public IndexedBag<T> inOrder() {
2 IndexedBag<T> sequence = new ArrayIndexedBag<>();
3 inOrder(root, sequence);
4 return sequence;
5}
6
7private void inOrder(Node<T> current, IndexedBag<T> sequence) {
8 if (current != null) {
9 inOrder(current.getLeft(), sequence);
10 sequence.add(current.getData());
11 inOrder(current.getRight(), sequence);
12 }
13}
25.5. For Next Time
Read Chapter 10 Sections 4 – 7
34 pages (mostly code though)
25.5.1. Playing Code
Download and play with the
BinaryTree
interface