Tree Key Point Review
Tree is an important data structure.
There are multiple ways to implement, today we are going to talk about the most common 2 ways.
Implementation
HashMap
Hash map implementation is useful when given edges instead of a tree structure.
For example, if given edges: [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]
We can convert edges into a hash map like this:1
2
3
4
5
6
7
8{
0: [3],
1: [3],
2: [3],
3: [0, 1, 2, 4],
4: [3, 5],
5: [4]
}
Simply iterate through the edges and create this table:1
2
3
4
5
6from collections import defaultdict
graph = defaultdict(list)
for e in edges:
graph[e[0]].append(e[1])
graph[e[1]].append(e[0])
TreeNode Class
While the previos one is useful for all kinds of graph, this implemention is only useful for binary (or n-ary) trees, because the children number has to be fixed.
1 | class TreeNode: |
Operations and Time Complexity
To search or insert a node, the time complexity is O(h), where h is the height of the tree. The height is also at least O(logn).
Because this is not very efficien, we later introduce AVL Tree and Red-Black Tree.
To delete a node is less straightforward, because there are multiple cases.
If the node is a leaf, we simple delete it; if it has one child, we bypass the child. However, if there are 2 children, we replace the value with the largest value in its left subtree, and then delete that node.
This is to prevent the tree from breaking into 2 subtrees without access to one of them.
DFS
DFS, depth first search, is useful when finding paths or connected compartment.
There are 2 different kinds of implementation: recursive and iterative.
1 | # This code uses the 2nd implementation of tree above |
The non-recursive, or iterative way utilize a stack.1
2
3
4
5
6
7
8
9# This code uses the 1st implementation of tree above
stack = [root]
visit = []
while len(stack) > 0:
node = stack.pop()
visit.append(node)
for n in graph[node]:
if n not in visit:
stack.append(n)
Inorder, preorder and postorder are 3 ways of DFS.
- PreOrder traversal - visit the parent first and then left and right children;
- InOrder traversal - visit the left child, then the parent and the right child;
- PostOrder traversal - visit left child, then the right child and then the parent;
There are multiple ways, I’m only going to write some.
Preorder , recursion with side effects:
1 | def preorder(root): |
Inorder, recursion without side effects:1
2
3
4def inorder(root, visit):
if root == None:
return visit
return inorder(root.left, visit) + [root.val] + inorder(root.right, visit)
Iterative1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
res, stack = [], []
while True:
while root:
stack.append(root)
root = root.left
if not stack:
return res
node = stack.pop()
res.append(node.val)
root = node.right
Binary Tree Postorder Iterative
1 | def postorderTraversal(self, root): |
BFS
BFS, or Breadth First Search, is useful when finding shortest path.
The biggest difference is that instead of stack in DFS, BFS uses a queue.
1 | # This code uses the 1st implementation of tree above |
We can easily modify the code to find depth and path to a specific node.
1 | q = [(node, 1, [node])] |
Balanced
Balance meaning the height of every left and right subtree cannot differ more than 1.
We can check it recursively.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def isBalanced(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
def check_height(root):
if not root: return 0
left_height = check_height(root.left)
right_height = check_height(root.right)
if left_height == -1 or right_height == -1:
return -1
if abs(left_height - right_height) > 1: return -1
return max(left_height, right_height) + 1
return check_height(root) != -1