yangshun / tech-interview-handbook

💯 Curated coding interview preparation materials for busy software engineers

Home Page:https://www.techinterviewhandbook.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add solution to a leetcode problem.

raivatshah opened this issue · comments

Problem: https://leetcode.com/problems/sum-root-to-leaf-numbers/

Solution Explanation Blog Post: https://medium.com/leetcode-solutions/summing-root-to-leaf-numbers-in-a-binary-tree-353f33c8565e

Solution Explanation Text:

Summing Root to Leaf Numbers in a Binary Tree

leetcode1

Sum Root to Leaf Numbers is an interesting problem from Leetcode. The problem is of medium difficulty and is about binary trees. This post presents and explains the solution along with the thought process.

I assume that you’re familiar with Python and the concept of binary trees. If you’re not, you can read this article to get started.

The Problem

Given a binary tree whose nodes contain values 0-9, we have to find the sum of all numbers formed by root-to-leaf paths. A leaf is a node that doesn’t have any child nodes. In a binary tree, a root-to-leaf path is always unique. Here below is the expected behavior of the solution required:

leetcode2

In the tree on the left, the output is 25. 25 is the sum of 12 and 13, which are the two numbers formed when starting from 1 and visiting every leaf. In the tree on the right, the output is 1026 as it is sum of the three numbers 495, 491 and 40.

The Observations and Insights

  • We notice that we traverse the tree from the root to the leaf, visiting some leaves before many of the direct children of the root itself. This suggests that a depth-first search might be more useful here, and especially the one which starts visits the root first.

  • We notice that the building of numbers is incremental and similar of sorts: the only difference between 495 and 491 is the last digit. If we remove the 5 and insert a 1 in its place, we have the next number. A number is essentially made of up all digits in its ancestor nodes and thus shares many digits with siblings and nodes within the same sub-tree.

  • Finally, we notice that this problem involves a tree so a recursive solution is possible and natural.

The Solution

We can do a pre-order traversal of the tree where we incrementally build up a number and exploit the fact that numbers formed by nodes in the same sub-tree have common digits for common ancestors. When we’re done forming numbers in a sub-tree, we can back-track and go to another sub-tree.

Let’s create a Solution class to encompass our solution. We can have an array attribute to store all the root-to-leaf numbers formed and an array attribute to store the current list of digits as we traverse the tree.

class Solution: 
    curr = [] # stores digits in the current path 
    numbers = [] # stores numbers formed by root-to-leaf paths
    def sum_numbers(self, root: TreeNode) -> int:

The method signature given to us in the problem has one argument: root , which is of the type TreeNode . A TreeNode class is as follows (from Leetcode):

class TreeNode:
     def __init__(self, val=0, left=None, right=None):
         self.val = val
         self.left = left
         self.right = right

As highlighted earlier, we need to build a number for each root-to-leaf path so that we can compute the sum of all numbers. To keep our code modular, let’s define another method to do this (instead of doing it within the sum_numbers method). This method will be responsible for filling our numbers array with all the root-to-leaf numbers. Let’s call it get_root_to_leaf_nums. It should take an argument of type TreeNode as well and return nothing (since it is modifying state by filling up the numbers array.

Our Solution class looks like:

class Solution: 
    curr = []
    numbers = []
    def sum_numbers(self, root: TreeNode) -> int:
    def get_root_to_leaf_nums(self, root: TreeNode): 

We can think of the get_root_to_leaf_nums method recursively and process each node differently based on whether it is a leaf or a not a leaf.

  • If it is a leaf, we want to add the value to our curr array, create a number based on the digits in the curr array and add the number to the numbers array. Since the node is a leaf, we will backtrack from here to the previous node and therefore want to delete the current node’s value from the curr array.

  • If it is not a leaf, we want to add the value to our curr array, and then continue traversing the left and right sub-trees. When done, we want to remove the current node’s value from the curr array as we backtrack.

Thus, our get_root_to_leaf_nums method will be as follows:

def get_root_to_leaf_nums(self, root: TreeNode): 
        if root: 
            self.curr.append(str(root.val))
            if not root.left and not root.right:
                self.numbers.append(self.convert_to_num(self.curr))
                self.curr.pop()
            else: 
                self.get_root_to_leaf_nums(root.left)
                self.get_root_to_leaf_nums(root.right)
                self.curr.pop()

We check if the root is not a None and simply do nothing if it is None as we are not concerned with empty sub-trees. We need a convert_to_num method that can convert the curr array representing digits of the number to an integer:

def convert_to_num(self, arr) -> int:
        cum = int("".join([str(x) for x in arr]))
        return int(cum)

We basically convert each int in the curr array to a string, concatenate the string and then typecast the result back to an int.

Now, in our main method, we first want to fill the numbers array and then simply return its sum. We need to clear the numbers array at the end because Leetcode will typically run this through a lot of testcases and subsequent testcases will fail if the numbers array contains solutions numbers from a previous testcase.

Finally, this is how our solution looks:

class Solution: 
    curr = []
    numbers = []
    def sum_numbers(self, root: TreeNode) -> int:
        #get all numbers from root to leaf paths
        self.pre_order(root)
        # sum all numbers
        ans = sum(self.numbers)
        self.numbers.clear()
        return ansdef get_root_to_leaf_nums(self, root: TreeNode): 
        if root: 
            self.curr.append(str(root.val))
            if not root.left and not root.right:
                self.numbers.append(self.convert_to_num(self.curr))
                self.curr.pop()
            else: 
                self.get_root_to_leaf_nums(root.left)
                self.get_root_to_leaf_nums(root.right)
                self.curr.pop()def convert_to_num(self, arr) -> int:
        cum = int("".join([str(x) for x in arr]))
        return int(cum)

The Algorithmic Complexity

When solving a problem, it is important to analyze its algorithmic complexity not only to estimate its performance but also to identify areas for improvement and reflect on our problem solving skills.

Time:

Our solution is a modification of the depth-first-search pre-order traversal where we visit all nodes exactly once. Thus, our runtime is simply O(N) where N represents the number of nodes in the given tree. I can’t think of a solution that will be better than O(N) because to construct a number from digits, I need to know all the digits.

We also incur a small cost to sum the numbers in the numbers list and to convert the curr array to an integer, but these costs are insignificant when compared to the cost of visiting each node (the number of numbers formed will be less than total number of nodes).

Space:

In terms of storage, we store the numbers formed and the current list of digits, which is not that significant. However, what is significant is the recursion call stack that builds up as our get_root_to_leaf_nums calls itself. These calls “build-up” as one waits for another to finish.

Generally, the maximum call stack will be dependent upon the height of the binary tree (since we start backtracking after we visit a leaf), giving a complexity of O(H) where H is the height of the binary tree. In the worst case, the binary tree is skewed in either direction and thus H = N. Therefore, the worst case space complexity is O(H).

You can read this article to know more about recursion call stacks.

The Conclusion

I hope this post helped! Please do let me know if you have any feedback, comments or suggestions by responding to this post.

Hey @raivatshah, what is this supposed to be? Are you trying to contribute to the handbook? If so, please open a pull request rather than an issue. That said, as with the rest of the algorithmic content in the handbook, it is probably unnecessary for such a verbose walkthrough of a relatively simple tree question.

It also seems like all the content here was pasted over from your linked medium post. Are you just trying to cross-post your article here for visibility?

Ah got it. Btw @raivatshah I just took a look at the solution writeup and I think there are some gaps.

  1. The full solution doesn't seem to compile. For instance, self.pre_order(root) isn't defined anywhere.

  2. The suggested solution is doing a string concatenation of the parent digits for each leaf node which makes the time complexity analysis not quite right. Consider a perfect binary tree with O(log n) height and O(n) number of leaf nodes. The concatenation function would be performing a concatenation of O(log(n)) size, O(n) times which makes the time complexity O(n log n).

  3. The suggested solution seems a little unnecessarily complicated. E.g., it looks like it's storing a bunch of tree traversal state in class attributes when it might be more natural to store them as instance attributes or simply pass them around as arguments. In any case, this is a simple tree recursion problem, so something like this would have sufficed:

def sol(root, agg_value=0):
    if not root:
        return 0
    agg_value = agg_value * 10 + root.val
    if not root.left and not root.right:
        return agg_value
    return sum(sol(c, agg_value) for c in (root.left, root.right) if c)

Hi @louietyj thanks so much for spotting the mistakes. I've corrected them and opened a PR here: #166.

Closed via #166