pexels-photo-34861773-34861773.jpg

Binary tree

Let’s get the basics of binary tree, right..

Tree:

— An tree is made of Nodes(Parent Node & Child Nodes).
— Root Node is the which don’t have any Parent Node. leaf Node is the Node which don’t have any child Nodes.
— These nodes are nothing but the containers inside which, data is stored.

Binary Tree:
— It is an tree in which Parent Node can have atmost 2 Child Nodes.
— It has only one root Node.
— Exactly one path between root Node to any other Node.
— Edge case : A tree with 0 Nodes is also called as Binary tree.
— Be careful about this situation.
— Usually, when you see below diagram, then it’s easy to identify that it’s an Binary Tree.
— But, below is also Binary tree.. Don’t believe? See the next picture which is just a bit rotated version of this Tree. Then it looks like Binary tree 🙂

{2837b6cd 6368 4366 9da7 29bd7bf9286e}
{2dd5d083 0272 439c 9d70 1e1420b47206}
Represent the below binary tree, programatically

{d6275038 1f65 49ea a3b2 15d14ff8046a}

Solution:
We know how to represent an Linked List programatically. This is not much of an difference from that..
In Linked List, each node consists of an 2 variables named val & next.
In case of Binary Tree, each node consists of 3 variables named val, left & right.

class Node:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")
e = Node("e")
f = Node("f")

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#          a
#       /    \
#      b      c
#    /  \      \
#   d   e       f
Traversing binary tree in depth first way

Problem:
Write a function, depth_first_values, that takes in the root of a binary tree.
The function should return a list containing all values of the tree in depth-first order.

{d6275038 1f65 49ea a3b2 15d14ff8046a}

Solution:
What is Depth First Traversal?
— Let’s see how the depth first travel will be in case of above binary tree.
— Will start the traversal of tree, with root node.
— Then which ever path we take from root node, that needs to be traversed as depth as possible, before traversing lateral movement.
— In the above case; it will be: a → b → d → e → c → f
— In order to traverse an binary tree in depth first manner, the data structure needed is Stack
Stack data structure:
— Stack data structure is used for traversing the Binary Tree in depth first order. So, we need to know how Stack data structure works.

{32a3fee1 5967 42af 973a c5f47fdfeb9c}

— Stack is the name given to an data structure which behaves in above way.
Coming to python, stack data structure is list & this is how above picture can be turned into code.
— The last element you push into stack will be the one coming out, when you want to pop an element from stack.

my_stack = []
my_stack.append(1)  # 1
my_stack.append(2)  # 1, 2
my_stack.append(3)  # 1, 2, 3
my_stack.pop()      # 3

Recursive approach:

— We already know that any recrusive problem is composed of two steps. base case & recursive step.
— Leap of faith concept needs to be understood before we write the recursive step.
When we make recursive calls, it’s as if we are taking recursive leap of faith(We will assume that when we make recursive all on the child,
we will get back the result that we are supposed to get).
— Once we get the result from left child tree & right child tree, we have to think how to frame the final result & return.
This is how return [ root.val, *left_values, *right_values ], in this case.

{e20a705e 998a 4b10 bb06 ec4513a14780}
def depth_first_values(root):
    # Base case.
    if not root:
        return []

    # Recursive step
    left_values = depth_first_values(root.left)
    right_values = depth_first_values(root.right)
    return [root.val, *left_values, *right_values]

Iterative approach:

def depth_first_values(root):
    # This is just an edge case.
    # If the root node is empty, which means if the tree ifself is empty,
    # Then, the result is [].
    if not root:
        return []

    # We have to start the traversal from root of the tree.
    # Hence, i am inserting root Node into the stack.
    stack = [root]
    values = []

    # All iterative approaches will usually has the while loop.

    # Inside while loop, this is the repeated work will be doing.
    # Will pop out an node & treat it as curr_node.
    # We can do whatever work on that curr_node needs to be done,
    # as per the problem.
    # In this case, we are just appending the curr_node to the list.
    # Then, will append the childs of the curr_node to list,
    # after checking if those nodes are present.

    # At the end of the loop, just return the values.
    while stack:
        curr_node = stack.pop()
        values.append(curr_node.val)
        if curr_node.right:
            stack.append(curr_node.right)
        if curr_node.left:
            stack.append(curr_node.left)
    return values
Traversing binary tree in breadth first way

Problem:
Write a function, breadth_first_values, that takes in the root of a binary tree.
The function should return a list containing all values of the tree in breadth-first order.

{d6275038 1f65 49ea a3b2 15d14ff8046a}

Solution:
What is Breadth First Traversal?
— Like the name indicates, it’s traversal of the tree in breadth manner. a → b → c → d → e → f
— In order to traverse an binary tree in breadth first manner, the data structure needed is queue
Queue data structure
— Queue data structure is used for traversing the Binary Tree in breadth first order. So, we need to know how Queue data structure works.
— Think about the queue data structure working analogous to people standing in the grocery queue line.

{9a745870 f802 4c47 958d 2d464324655f}

— Queue is the name given to an data structure which behaves in above way. Coming to python, queue data structure is list & this is how above picture can be turned into code.
— The first element you push into queue will be the one coming out, when you want to pop an element from queue.

my_queue = []
my_queue.append(1)  # 1
my_queue.append(2)  # 1, 2
my_queue.append(3)  # 1, 2, 3
my_queue.pop(0)     # 1

Recursive approach:

Only works in cool way in case of Depth first search 🙂

Iterative approach:

def breadth_first_values(root):
    if root is None:
        return []
    queue = [root]
    result = []
    while queue:
        curr_node = queue.pop(0)
        result.append(curr_node.val)
        if curr_node.left:
            queue.append(curr_node.left)
        if curr_node.right:
            queue.append(curr_node.right)
    return result
Tree sum

Problem:
Write a function, tree_sum, that takes in the root of a binary tree that contains number values.
The function should return the total sum of all values in the tree.

# test_00
a = Node(3)
b = Node(11)
c = Node(4)
d = Node(4)
e = Node(-2)
f = Node(1)

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#       3
#    /    \
#   11     4
#  / \      \
# 4   -2     1

tree_sum(a) # -> 21

Solution:

Depth first search — Recursive approach:

def tree_sum(root):
    if root is None:
        return 0
    return root.val + tree_sum(root.left) + tree_sum(root.right)

Breadth first search — Iterative approach:

from collections import deque

def tree_sum(root):
    if not root:
        return 0
    queue = deque([root])
    total_sum = 0
    while queue:
        node = queue.popleft()
        total_sum += node.val
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
    return total_sum
Tree includes

Problem:
Write a function, tree_includes, that takes in the root of a binary tree and a target value. The function should return a boolean indicating whether or not the value is contained in the tree.

# test_00
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")
e = Node("e")
f = Node("f")

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#      a
#    /   \
#   b     c
#  / \     \
# d   e     f

tree_includes(a, "e") # -> True

Solution:

Depth first search — Recursive approach:

def tree_includes(root, target):
    if not root:
        return False

    if root.val == target:
        return True

    return tree_includes(root.left, target) or tree_includes(root.right, target)

Breadth first search — Iterative approach:

from collections import deque

def tree_includes(root, target):
    if not root:
        return False
    queue = deque([root])
    while queue:
        node = queue.popleft()
        if node.val == target:
            return True
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
    return False
Tree min value

Problem:
Write a function, tree_min_value, that takes in the root of a binary tree that contains number values. The function should return the minimum value within the tree. You may assume that the input tree is non-empty.

# test_00
a = Node(3)
b = Node(11)
c = Node(4)
d = Node(4)
e = Node(-2)
f = Node(1)

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#       3
#    /    \
#   11     4
#  / \      \
# 4   -2     1
tree_min_value(a) # -> -2

Solution:

Depth first search — Recursive approach:

def tree_min_value(root):
    if root is None:
        return float("inf")

    smallest_left_value = tree_min_value(root.left)
    smallest_right_value = tree_min_value(root.right)
    return min(root.val, smallest_left_value, smallest_right_value)

Breadth first search — Iterative approach:

def tree_min_value(root):
    queue = [root]
    smallest = float("inf")
    while queue:
        current = queue.pop(0)
        if current.val < smallest:
            smallest = current.val
        if current.left is not None:
            queue.append(current.left)
        if current.right is not None:
            queue.append(current.right)
    return smallest
max root to leaf path sum

Problem:
Write a function, max_path_sum, that takes in the root of a binary tree that contains number values. The function should return the maximum sum of any root to leaf path within the tree.
You may assume that the input tree is non-empty.

# test_00
a = Node(3)
b = Node(11)
c = Node(4)
d = Node(4)
e = Node(-2)
f = Node(1)

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#       3
#    /    \
#   11     4
#  / \      \
# 4   -2     1

max_path_sum(a) # -> 18

Solution:

Depth first search — Recursive approach:

def max_path_sum(root):
  if root is None:
    return float("-inf")

  # Edge case:
  # When you are at leaf node. 
  # The return value of this function will be like:
  # 5 + (-infinity) + (-infinity) = (-infinity)
  # You need to catch this before return statement 
  # & that's why you need this. 
  if root.left is None and root.right is None:
    return root.val

  return root.val + max(max_path_sum(root.left), max_path_sum(root.right))
Tree path finder

Problem:
Write a function, path_finder, that takes in the root of a binary tree and a target value. The function should return an array representing a path to the target value. If the target value is not found in the tree, then return None. You may assume that the tree contains unique values.

# test_00
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")
e = Node("e")
f = Node("f")

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#      a
#    /   \
#   b     c
#  / \     \
# d   e     f

path_finder(a, 'e') # -> [ 'a', 'b', 'e' ]

Solution:

Depth first search — Recursive approach — O(n2)

def path_finder(root, target):
    if root is None:
        return None
    if root.val == target:
        return [root.val]
    left_path = path_finder(root.left, target)
    if left_path is not None:
        return [root.val, *left_path]
    right_path = path_finder(root.right, target)
    if right_path is not None:
        return [root.val, *right_path]
    return None

Depth first search — Recursive approach — with append — O(n)

def path_finder(root, target):
    result = _path_finder(root, target)
    if result is None:
        return None
    else:
        return result[::-1]

def _path_finder(root, target):
    if root is None:
        return None
    if root.val == target:
        return [root.val]
    left_path = _path_finder(root.left, target)
    if left_path is not None:
        left_path.append(root.val)
        return left_path
    right_path = _path_finder(root.right, target)
    if right_path is not None:
        right_path.append(root.val)
        return right_path
    return None
How high

Problem:
Write a function, how_high, that takes in the root of a binary tree. The function should return a number representing the height of the tree.
The height of a binary tree is defined as the maximal number of edges from the root node to any leaf node.
If the tree is empty, return -1.

# test_00
a = Node('a')
b = Node('b')
c = Node('c')
d = Node('d')
e = Node('e')
f = Node('f')

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#      a
#    /   \
#   b     c
#  / \     \
# d   e     f

how_high(a) # -> 2

Solution:

Depth first search — Recursive approach:

def how_high(root):
    if root is None:
        return -1

    left_height = how_high(root.left)
    right_height = how_high(root.right)
    return 1 + max(left_height, right_height)

Breadth first search — Iterative approach:

from collections import deque

def how_high(root):
    queue = deque([])
    if root is not None:
        queue.append((root, 0))
    max_depth = -1
    while queue:
        node, depth = queue.pop()
        max_depth = max(max_depth, depth)
        if node.left is not None:
            queue.append((node.left, depth + 1))
        if node.right is not None:
            queue.append((node.right, depth + 1))
    return max_depth
Bottom right value

Problem:
Write a function, bottom_right_value, that takes in the root of a binary tree. The function should return the right-most value in the bottom-most level of the tree.You may assume that the input tree is non-empty.

# test_00
a = Node(3)
b = Node(11)
c = Node(10)
d = Node(4)
e = Node(-2)
f = Node(1)

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#       3
#    /    \
#   11     10
#  / \      \
# 4   -2     1

bottom_right_value(a) # -> 1

Solution:

Breadth first search — Iterative approach:

from collections import deque

def bottom_right_value(root):
  queue = deque([ root ])
  current = None
  while queue:
    current = queue.popleft()
    if current.left is not None:
      queue.append(current.left)
    if current.right is not None:
      queue.append(current.right)
  return current.val
All tree paths

Problem:
Write a function, all_tree_paths, that takes in the root of a binary tree. The function should return a 2-Dimensional list where each subarray represents a root-to-leaf path in the tree.
The order within an individual path must start at the root and end at the leaf, but the relative order among paths in the outer list does not matter.
You may assume that the input tree is non-empty.

# test_00
a = Node('a')
b = Node('b')
c = Node('c')
d = Node('d')
e = Node('e')
f = Node('f')

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#      a
#    /   \
#   b     c
#  / \     \
# d   e     f

all_tree_paths(a) # ->
# [ 
#   [ 'a', 'b', 'd' ], 
#   [ 'a', 'b', 'e' ], 
#   [ 'a', 'c', 'f' ] 
# ]

Solution:
Base case 1, what should be returned when base case was hit
Initially, it is tempting to think the answer for base case is just the value of leaf node inside an array. But, that’s not true.
Think of the base case itself as an input. That’s the key.. For example; what should be returned if the tree given to you is just an node d? It should be [[D]] which tells us that their is only one path from root node to leaf node, which is [ ] & since the question is asking us to keep all the paths inside an [ ], which results in [[D]].

{8b46fad5 be0a 4c6f 9808 0009fa0e0fce}

Base case 2, what should be returned when base case was null
So, after reading the base case 1, you may think that in case of null node, you should return [[]].
But, NO.When will that inside [ ] comes into picture? When you have an path. Right?
When there is no path itself, then why will the inside [ ] will come in first place? That’s why, the return value in this case is [ ].

{e1ed0d90 8255 4957 b909 86043a14d6b6}

Depth first search — Recursive approach:

Observe the 2 base cases. You must write the base case 1 first & then the base case 2. The reason being, If the node is None, then you should catch that first & return an result for that case. Otherwise, if you allow that None to pass through, it will result in error at root.left.

def all_tree_paths(root):
    paths = _all_tree_paths(root)
    for path in paths:
        path.reverse()
    return paths

def _all_tree_paths(root):
    # base case 1
    if root is None:
        return []
    # base case 2
    if root.left is None and root.right is None:
        return [[root.val]]
    all_paths = []
    left_sub_paths = _all_tree_paths(root.left)
    for path in left_sub_paths:
        path.append(root.val)
        all_paths.append(path)
    right_sub_paths = _all_tree_paths(root.right)
    for path in right_sub_paths:
        path.append(root.val)
        all_paths.append(path)
    return all_paths
Tree levels

Problem:
Write a function, tree_levels, that takes in the root of a binary tree. The function should return a 2-Dimensional list where each sublist represents a level of the tree.

# test_00
a = Node('a')
b = Node('b')
c = Node('c')
d = Node('d')
e = Node('e')
f = Node('f')

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#      a
#    /   \
#   b     c
#  / \     \
# d   e     f

tree_levels(a) # ->
# [
#   ['a'],
#   ['b', 'c'],
#   ['d', 'e', 'f']
# ]

Solution:
What is level?
— level is all the nodes which are at the same distance(in units of edges) from the root node.
— root starts with level 0, the reason being as per definition, how many edges away is the root node, when you calculate the distance from root node?
0 edges. Let’s follow this subsequentially down the tree & you will end up with.

{d2547250 cd28 43fb a890 bc60b6edd2fc}

Breadth first search — Iterative approach:
— As per the definition of level, it’s all the nodes which are at the same distance from the root node. So, when you are doing depth first traversal, will note down the level of each node.
— Breadth first traversal → queue data structure → when we are inserting the node values into queue, will insert the corresponding level of the node also, along with the value.
— When we are popping an value from queue, will pop the node value as well as corresponding level.
That level we pop out, helps us to identify to prepare the result like [ [3], [11, 4], [4, 10, 1] ]
— This is the whole logic of the problem.

def tree_levels(root):
    # Base case
    if root is None:
        return []

    # Iterative step
    levels = []
    queue = [(root, 0)]
    while queue:
        current, level_num = queue.pop(0)
        # When length of levels at which you are is == level of current popped up node,
        # It means, you are at this level for the first time, hence add the node value
        # to new list. Else, just add the element at an index which is equal to current
        # level_num
        if len(levels) == level_num:
            levels.append([current.val])
        else:
            levels[level_num].append(current.val)

        # When you are inserting the child node into queue, the level will be level_num + 1,
        # because, child node will be at +1 level to that of parent level.
        if current.left is not None:
            queue.append((current.left, level_num + 1))

        # When you are inserting the child node into queue, the level will be level_num + 1,
        # because, child node will be at +1 level to that of parent level.
        if current.right is not None:
            queue.append((current.right, level_num + 1))

    return levels

Depth first search — Recursive approach:

def tree_levels(root):
    levels = []
    _tree_levels(root, levels, 0)
    return levels

def _tree_levels(root, levels, level_num):
    if root is None:
        return
    if level_num == len(levels):
        levels.append([root.val])
    else:
        levels[level_num].append(root.val)
    _tree_levels(root.left, levels, level_num + 1)
    _tree_levels(root.right, levels, level_num + 1)
Level averages

Problem:
Write a function, level_averages, that takes in the root of a binary tree that contains number values. The function should return a list containing the average value of each level.

# test_00
a = Node(3)
b = Node(11)
c = Node(4)
d = Node(4)
e = Node(-2)
f = Node(1)

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#       3
#    /    \
#   11     4
#  / \      \
# 4   -2     1

level_averages(a) # -> [ 3, 7.5, 1 ] 

Solution:
— First thing to note is : you can’t do this solution same as that of above & just return the levels[-1].
The reason being: Implementation of leaf_list is that, it only returns the values from the deepest level of the tree,
which is not the same as the list of all leaf nodes. ❌
— You are essentially running a Breadth-First Search (BFS) for level order traversal and grouping nodes by level.
— Your final line, return levels[-1], returns the last list in your levels array, which contains all the nodes at the maximum depth.
This only works correctly if all leaf nodes happen to be at the same maximum depth.
If the tree is unbalanced (meaning some leaf nodes are at shallower depths), your solution will miss them.

Depth first search — Recursive approach:

from statistics import mean

def level_averages(root):
    levels = []
    fill_levels(root, levels, 0)
    avgs = []
    for level in levels:
        avgs.append(mean(level))
    return avgs

def fill_levels(root, levels, level_num):
    if root is None:
        return
    if level_num == len(levels):
        levels.append([root.val])
    else:
        levels[level_num].append(root.val)
    fill_levels(root.left, levels, level_num + 1)
    fill_levels(root.right, levels, level_num + 1)

Breadth first search — Iterative approach:

from statistics import mean
from collections import deque

def level_averages(root):
    levels = []
    fill_levels(root, levels)
    avgs = []
    for level in levels:
        avgs.append(mean(level))
    return avgs

def fill_levels(root, levels):
    if root is None:
        return
    queue = deque([(root, 0)])
    while queue:
        current, level_num = queue.popleft()
        if len(levels) == level_num:
            levels.append([current.val])
        else:
            levels[level_num].append(current.val)
        if current.left is not None:
            queue.append((current.left, level_num + 1))
        if current.right is not None:
            queue.append((current.right, level_num + 1))
    return levels
Leaf list

Problem:
Write a function, leaf_list, that takes in the root of a binary tree and returns a list containing the values of all leaf nodes in left-to-right order.

# test_00
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")
e = Node("e")
f = Node("f")

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

#      a
#    /   \
#   b     c
#  / \     \
# d   e     f

leaf_list(a) # -> [ 'd', 'e', 'f' ] 

Solution:

Depth first search — Iterative approach:

def leaf_list(root):
    if root is None:
        return []
    leaves = []
    stack = [root]
    while stack:
        current = stack.pop()
        if current.left is None and current.right is None:
            leaves.append(current.val)
        if current.right is not None:
            stack.append(current.right)
        if current.left is not None:
            stack.append(current.left)
    return leaves

Depth first search — Recursive approach:

def leaf_list(root):
    leaves = []
    _leaf_list(root, leaves)
    return leaves
def _leaf_list(root, leaves):
    if root is None:
        return
    if root.left is None and root.right is None:
        leaves.append(root.val)
    _leaf_list(root.left, leaves)
    _leaf_list(root.right, leaves)