Intro
A common abstractdatatype representing connected, acyclic graphs.
Cyclic trees are called Graphs.
Remember All trees are graphs, but not all graphs are trees…
Often although not necessarily, Trees have a root node, and end nodes known as leaves.
Each node in the tree can be connected to one or more children but only one parent, except the root which has no parent.
Representing data in a tree can speed up your algorithms in many natural problems.
Two common ways to represent graphs:
- Adjacency Matrix
- matrix i,e, a 2D array of cells, each representing an edge
- Cell
i, jrepresents an edge from vertex i to j
- Adjacency Lists:
- linked lists – one for each vertex, showing all the neighbors of that vertex
There are many types of trees we’ve covered many in our comp3760 Algorithms course at BCIT.
Representing Trees
There are a few different ways of doing so. Some involve oop and others use fundamental data structures that can be found in most programming languages such as dictionaries and lists.
Node Class (OOP)
This is the typical representation you’ll encounter while studying and practicing.
Goes something like this:
# Python class for a typical 'Node' object
class Node(object):
def __init__(self, val):
self.val = val
self.left = None
self.right = NoArray of Arrays
Another way is using a 2-dimensional array (2D). Since each node has one parent, that’d be the outer array.
In this representation the structure is as follows:
- The value of a node is the first element in the array.
- The left subtree is the second element, which can be another array.
- the right subtree is the third element and can also be another array.
For example:
tree = [
'a', #root
['b', # left subtree
['d' [], []],
['e' [], []]
],
['c', # right subtree
['f' [], []],
[]
]
]// I hate this
Accessing
Since it’s an array (or Python list in this example) we can access elements using regular indexing. For example root = tree[0]
and the left subtree of the root’s left subtree would be tree[1][1].
Tables/Maps
A table illustrates the nested nature of a tree much better than an array. In this representation, each node has 3 keys, val, left, and right. Obviously, the left and right keys are in turn representing another table with the same keys…
This ends up looking like a nested object (it kinda is) but thanks to Map and Table implementations.
my_tree = {
'val': 'A',
'left': {
'val': 'B',
'left': {'val': 'D'},
'right': {'val': 'E'}
},
'right': {
'val': 'C',
'right': {'val': 'F'}
}
}Binary Trees
The most infamous type of trees in computer science. A binarytree is a tree that has left and right children.
In some Binary Trees, a node has a value, and the left child’s value is less than the parent’s and the right’s is greater. These are known as Binary Search Trees (BST)
// In all Binary Trees, the data must be ordered!
Terminology
- Complete Binary Tree: one in which every level except the last is filled, and all nodes of the last level are to the far left as possible.
- Balanced Binary Tree: one with a structure in which the height of the left and right sub-trees per node differ by 0 or 1.
Depth-First ( dfs )Traversal Methods
Unless specified, syntax isn't valid, only pseudocode!
In-order
Recursive
This is the standard recursive approach, simplified but the idea gets across:
//In-order implementation
void inorderTraversal(struct Node* node) {
if (node == NULL) {
return;
}
inorderTraversal(node->left);
printf("%d ", node->data);
inorderTraversal(node->right);
}Iterative
Simple Stack implementation, is time and space complexity.
def in_ord(root):
if root is null:
return
stack = new Stack()
curr = root
while curr or stack not empty:
# going down the tree
while curr:
# add to stack
stack.push(curr)
# go left
curr = curr.left
# curr is now null, end of subtree so pop from stack
curr = stack.pop()
# do something with "visited" node
print(curr)
# no more left subtree, check right
curr = curr.rightMorris’ Traversal Algorithm
More optimized for In order traversal as it’s time but space since it doesn’t need a Stack.
Found this example on GeeksForGeeks example
We want to traverse the tree and come back to the root after finishing the left subtree, but without a stack. Morris Traversal achieves this by temporarily modifying the tree
- For each node, check if it has a left child.
- If it does not have a left child, visit it and move to the right child.
- If it has a left child, find the inorder predecessor (rightmost node in the left subtree).
- Make the current node as the right child of its inorder predece(1) ssor (temporary link).
- Move to the left child.
- When you encounter a temporary link again, it means the left subtree is fully visited:
- Remove the temporary link.
- Visit the current node.
- Move to the right child.
def morris_inorder(root):
res = []
curr = root
while curr is not None:
if curr.left is None:
# If no left child, visit this node then go right
res.append(curr.data)
curr = curr.right
else:
# Find the inorder predecessor of curr
prev = curr.left
while prev.right is not None and prev.right != curr:
prev = prev.right
# Make curr the right child of its inorder predecessor
if prev.right is None:
prev.right = curr
curr = curr.left
else:
# Revert the changes made in the tree
prev.right = None
res.append(curr.data)
curr = curr.rightPre-order
Recursive approach
void preorderTraversal(struct Node* node) {
if (node == NULL) {
return;
}
printf("%d ", node->data);
preorderTraversal(node->left);
preorderTraversal(node->right);
}Iterative approach
Tree parameter is root and not node since iterative…
def pre_ord(root):
if root is null:
return
stack = new Stack(root)
while stack not empty:
curr = stack.pop()
# do something
print(curr)
if curr.right exists stack.push(curr.right)
if curr.left exists stack.push(curr.left) Now this implementation pushes the left sub-tree onto the stack just to push it on the next loop, let’s optimize slightly
def pre_ord(root):
if root is null:
return
stack = new Stack() #empty
curr = root
while curr not null and stack not empty:
while curr:
print(curr)
if curr.right exists stack.push(curr.right)
curr = curr.left
if stack not empty:
curr = stack.pop()
Post-order
Recursive
void postorderTraversal(struct Node* node) {
if (node == NULL) {
return;
}
postorderTraversal(node->left);
postorderTraversal(node->right);
printf("%d ", node->data);
}Iterative 2 Stacks
This implementation uses two stacks, one to traverse all the way through, and another to form the results.
def post_ord(root):
if root is null:
return
stack = new Stack(root) # this one init with root node
output = new Stack() # this one's empty
while stack not empty:
curr = stack.pop()
# add the current to output stack
output.push(curr)
if curr.left exists stack.push(curr.left)
if curr.right exists stack.push(curr.right)
# now that everything's been stacked, we pop the post order
while output not empty:
curr = output.pop()
# do something
print(curr)Keep in Mind
In-Order traversal of a Binary Tree is insufficient to uniquely serialize a tree. Pre-Order or Post-Order traversal is also required for this.