Depth First Search - kjingers/Grokking-Notes GitHub Wiki

Binary Tree Path Sum

DFS using recursion.

def has_path(root, sum):

  # Return is root is False (when child doesn't exist)
  if root is None:
    return False

  # If leaf node and total sum = travesed sum, return True
  if root.val == sum and root.left is None and root.right is None:
    return True

  # Recursively Call function for left, then Right children
  # Pass in Sum - root.val
  return has_path(root.left, sum - root.val) or has_path(root.right, sum - root.val)

All Paths for a Sum

Lie above, we use a recursion for our DFS. But for this one, we don't return True when we get a valid path. Instead, we add the path to the allPaths list. I used a stack to keep track of the current Path. After each node is processed and performs the recusive call on its children, we pop that node's value from the stack.

Time Complexity for this is actually O(n*log(n)). We visit each node which is O(n), and for each leaf, we potentially need to copy the path, which is O(logN) - (this is the maximum number of nodes in a path)

def find_paths(root, sum):
  allPaths = []
  currentPath = deque()
  find_paths_recursive(root, sum, currentPath, allPaths)
  
  return allPaths

def find_paths_recursive(root, requiredSum, currentPath, allPaths):

  if root is None:
    return
  
  currentPath.append(root.val)

  if root.val == requiredSum and root.left is None and root.right is None:
    allPaths.append(list(currentPath))
  else:
    find_paths_recursive(root.left, requiredSum - root.val, currentPath, allPaths)
    find_paths_recursive(root.right, requiredSum - root.val, currentPath, allPaths)
  
  currentPath.pop()

Sum of Path Numbers

Recursive function adds to the path sum. If the node is a leaf, then return the pathSum to that point, else, continue with the left then right child.

def find_sum_of_path_numbers(root):
  
  if root is None:
    return None
  
 
  return find_sum_of_path_numbers_recursive(root, 0)


def find_sum_of_path_numbers_recursive(root, pathSum):

  if root is None:
    return 0

  pathSum = (pathSum * 10) + root.val

  # If this is a leaf node, then return the sum of the path
  if root.left is None and root.right is None:
    return pathSum

  # If not leaf, then call recursively on left and right
  return find_sum_of_path_numbers_recursive(root.left, pathSum) + find_sum_of_path_numbers_recursive(root.right, pathSum)

Path With Given Sequence

Just keep track of index of sequence array

def find_path(root, sequence):
  
  return find_path_recursive(root, sequence, 0)

def find_path_recursive(root, sequence, index):

  if root is None:
    return False

  # If our index is beyond the length of the sequence, or the current node.val
  # Does not match the next number in the sequence
  if root.val != sequence[index] or (index >= len(sequence)):
    return False

  # If leaf node and this node matches the last element of the list
  if root.left is None and root.right is None and (len(sequence) - 1) == index:
    return True
  
  return find_path_recursive(root.left, sequence, index + 1) or find_path_recursive(root.right, sequence, index + 1)

Count Paths for a Sum

Kinda similar to "All Paths for a Sum" above. For this one, we need to check sum paths too (doesn't have to start with root and doesn't have to end with leaf). So, as each node, we add all the subpaths that equal the sum. We use a for loop to iterate backwards starting with the current node, so that we don't count any path twice (we are checking all the paths that end at the current node).

def count_paths(root, S):
  return count_paths_recursive(root, S, [])

def count_paths_recursive(currentNode, S, currentPath):

  if currentNode is None:
    return 0

  # Append current node's value to current path
  currentPath.append(currentNode.val)

  pathCount = 0
  pathSum = 0

  # Find sum of all sub paths ending with current node
  for i in range(len(currentPath) - 1, -1, -1):
    pathSum += currentPath[i]

    if pathSum == S:
      pathCount += 1

  # Recursively call for left and right children
  # Add returned pathCounts
  pathCount += count_paths_recursive(currentNode.left, S, currentPath)
  pathCount += count_paths_recursive(currentNode.right, S, currentPath)

  # Remove this node's value from recursion stack
  del currentPath[-1]

  # Return all paths ending in either this node or any nodes in subtree
  return pathCount

Tree Diameter

This suggested to use a class, so that self.treeDiameter can remain global. This is so it can be updated at each node within the recursive call. We could also pass it in as the first element in a list so that it's passed by reference.

Once the calls get to a leaf node, its children are None and return 0. So the leaf node returns 1. At all nodes on up, the diameter passing through each node is checked (left + right) + 1. And then the maximum child path max(left, right) + 1 is returned to caller.

Rather than the number of edges, this wants us to calculate the number of nodes, which is one greater than the number of edges.

class TreeDiameter:

  def __init__(self):
    self.treeDiameter = 0

  def find_diameter(self, root):
    self.calculate_height(root)
    return self.treeDiameter

  def calculate_height(self, root):
    # If leaf, then 0
    if root is None:
      return 0

    left = self.calculate_height(root.left)
    right = self.calculate_height(root.right)

    self.treeDiameter = max(self.treeDiameter, left + right + 1)

    return max(left, right) + 1

Max Path Sum

Similar to above problem. Except instead of calculating height, we calculate sum. The main difference is if get_max_gain(left) or get_max_gain(right) returns a negative number, then we use 0 in the calculating. Because if the subtree is negative, then there is no reason to use them (terminate at current node).

def find_maximum_path_sum(root):
  max_path = float("-inf")

  def get_max_gain(node):

    nonlocal max_path

    if node is None:
      return 0

    # If left/right subtree is negative, then just make it 0
    # We won't use it if its negative
    max_gain_on_left = max(get_max_gain(node.left), 0)
    max_gain_on_right = max(get_max_gain(node.right), 0)

    # Calculate max Path Sum where this node is the root
    max_as_root = node.val + max_gain_on_right + max_gain_on_left

    # Update global max_path
    max_path = max(max_path, max_as_root)

    # Now we return this node's value, plus left OR right subtree, 
    # depending on which is greater (since we can only use one).
    # This is used for path including this node (but not as the root)
    return node.val + max(max_gain_on_left, max_gain_on_right)

  get_max_gain(root)
  return max_path