A binary tree is a tree data structure where every node has at most two children.
Each node can be of three categories - the root
which has no parent, the leaf
which have no children,
and the inner nodes, which have at least one child. A node can be defined as follows:
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
Here is an example binary tree:
A traversal can be defined as a strategy in which to visit every node of the tree. We can either go wide or go deep.
$\rightarrow$ For the case where we want to go deep first (depth-first traversal), we can define the following three recursive ways depending on when the current node is marked as visited.
- Pre-order traversal
def pre_order_traversal(node) -> None: if node is not None: print(node.val) pre_order_traversal(node.left) pre_order_traversal(node.right)
- In-order traversal
def in_order_traversal(node) -> None: if node is not None: in_order_traversal(node.left) print(node.val) in_order_traversal(node.right)
- Post-order traversal
def post_order_traversal(node) -> None: if node is not None: post_order_traversal(node.left) post_order_traversal(node.right) print(node.val)
$\rightarrow$ For the case where we want to go wide first (breadth-first search), we can use a queue for the traversal. For more details on queue, refer to this page. Note that this traversal basically prints the values on a per-level basis, hence it is also called as Level-order traversal.
from collections import deque
def bfs(root) -> None:
queue = deque([root])
while queue:
node = queue.popleft()
print(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
Binary Search tree
A binary search tree (BST) is a binary tree where for every node, the value of the left child is less than or equal to the current node value, and the value of the right child is greater than the current node value. Here is one example:
Note: An in-order
traversal of a BST will lead to values in sorted order.
Shortest paths
A BFS is very useful when computing shortest paths. Let us consider a binary matrix where the goal is to start at the top-leftmost position and reach the bottom-rightmost position in the shortest amount of steps. A cell can be visited only if the matrix value at that location is 0 and all the adjacent cells of the path are 8-directionally connected (share an edge or a corner)
from collections import deque
def shortest_path_binary(grid):
n, m = len(grid), len(grid[0])
# trivial case: if the starting location itself is invalid
if grid[0][0] == 1:
return -1
queue = deque()
queue.append((0, 0))
len_queue = deque([1])
seen = set((0, 0))
while queue:
node = queue.popleft()
prev_length = len_queue.popleft()
# logic for reaching the destination
if node == (n-1, m-1):
return prev_length
for x, y in [[0, 1], [0, -1], [1, 0], [-1, 0],
[-1, -1], [-1, 1], [1, -1], [1, 1]]:
neighbor = (node[0] + x, node[1] + y)
if (
(neighbor[0] >= 0) and # boundry condition
(neighbor[0] < n) and
(neighbor[1] >= 0) and
(neighbor[1] < m) and
(grid[neighbor[0]][neighbor[1]] == 1) and # visit condition
(neighbor not in seen) # avoid looping back
):
seen.add(neighbor)
queue.append(neighbor)
len_queue.append(prev_length + 1)
# if we reach here, we haven't found any path
return -1
from collections import deque
def shortest_path(start):
""" Shortest path from start to every other vertex.
"""
shortest_path_map = {start: 0}
queue = deque([start])
while queue:
curr_node = node.popleft()
for child, weight in zip(curr_node.children, curr_node.weights):
connceted_path_length = weight + shortest_path_map[curr_node]
if child not in shortest_path_map:
shortest_path_map[child] = connected_path_length
queue.append(child)
else:
if shortest_path_map[child] > connected_path_length:
shortest_path_map[chid] = connected_path_length
queue.append(child)
return shortest_path_map