I'm trying leetcode problem 572.
Given two non-empty binary trees s and t, check whether tree t has exactly the same structure and node values with a subtree of s. A subtree of s is a tree consists of a node in s and all of this node's descendants. The tree s could also be considered as a subtree of itself.
Since, tree's a great for recursion, I thought about splitting the cases up.
a) If the current tree s is not the subtree t, then recurse on the left and right parts of s if possible
b) If tree s is subtree t, then return True
c) if s is empty, then we've exhausted all the subtrees in s and should return False
def isSubtree(self, s: TreeNode, t: TreeNode) -> bool:
if not s:
return False
if s == t:
return True
else:
if s.left and s.right:
return any([self.isSubtree(s.left, t), self.isSubtree(s.right, t)])
elif s.left:
return self.isSubtree(s.left, t)
elif s.right:
return self.isSubtree(s.right, t)
else:
return False
However, this for some reason returns False even for the cases where they are obviously True
Ex:
My code here returns False, but it should be True. Any pointers on what to do?
This'll simply get through:
class Solution:
def isSubtree(self, a, b):
def sub(node):
return f'A{node.val}#{sub(node.left)}{sub(node.right)}' if node else 'Z'
return sub(b) in sub(a)
References
For additional details, you can see the Discussion Board. There are plenty of accepted solutions with a variety of languages and explanations, efficient algorithms, as well as asymptotic time/space complexity analysis1, 2 in there.
You need to change second if statement.
In order to check if tree t is subtree of tree s, each time a node in s matches the root of t, call check method which determines whether two subtrees are identical.
if s.val == t.val and check(s, t):
return True
A check method is look like this.
def check(self, s, t):
if s is None and t is None:
return True
if s is None or t is None or s.val != t.val:
return False
return self.check(s.left, t.left) and self.check(s.right, t.right)
While other code work well, your code will much simpler in else statement like the following. You don't need to check whether left and right nodes are None because first if statement will check that.
else:
return self.isSubtree(s.left, t) or self.isSubtree(s.right, t)
Related
I am looking at the LeetCode problem "100. Same Tree":
Given the roots of two binary trees p and q, write a function to check if they are the same or not.
Two binary trees are considered the same if they are structurally identical, and the nodes have the same value.
My code passes some test cases and fails others.
My specific problem is with the test case [1,2] and [1,null,2]. Python uses None not null. When I use my local code editor (visual studio code) it produces the expected output. I know I could implement this in other ways but I don't understand why this solution won't work on LeetCode.
Here is my code:
class Solution:
def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
answer1 = []
answer2 = []
self.helperFunction(p, q, answer1, answer2)
for i in range(len(answer1)):
if answer1[i] != answer2[i]:
return False
return True
def helperFunction(self, p, q, answer1, answer2):
if p != None and q != None:
self.helperFunction(p.left, q.left, answer1, answer2)
answer1.append(p.val)
answer2.append(q.val)
self.helperFunction(p.right, q.right, answer1, answer2)
Your code assumes that both trees have the same shape. If however one tree has a child where the other doesn't, your code never visits that child, and this will give false positives when the rest of the tree is the same. The mere presence of such a child (that does not exist in the other tree) would be enough indication to abort the process and return False.
As to the algorithm itself. It is a pity that you do this:
answer1.append(p.val)
answer2.append(q.val)
...when you could actually compare these two values immediately (instead of doing this at the very end). Why not check p.val == q.val and if that is not true, stop looking any further? On the other hand, if they are equal, there is no reason to append those values to lists either. It just means "so far so good", and you can continue...
Concluding:
Return True when at both sides there is no node (None)
Return False when on one side you have a node that is not there on the other side
Return False when the values of corresponding nodes are different
Return True if and only when the recursion also returns True for both children.
Implemented:
class Solution:
def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
return bool(not p and not q or
p and q and p.val == q.val and self.isSameTree(p.left, q.left)
and self.isSameTree(p.right, q.right))
Let's look at your "answer1" and "answer2" variables for the following input:
input:
[1,2]
[1,null,2]
output:
answer1 = answer2 = [1]
as you can see, your program incorrectly stops comparing the nodes of the two trees once one of the values "p" or "q" is null in your helperFunction. This is due to the following line of code:
if p != None and q != None:
A simple fix for your bug could be this:
def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
answer1 = []
answer2 = []
self.helperFunction(p, q, answer1, answer2) # changed
return answer1 == answer2
def helperFunction(self, p, q, answer1, answer2):
if p != None and q != None:
self.helperFunction(p.left, q.left, answer1, answer2)
self.helperFunction(p.right, q.right, answer1, answer2)
if p is None:
answer1.append(None)
else:
answer1.append(p.val)
if q is None:
answer2.append(None)
else:
answer2.append(q.val)
I'm interested in handling DFS in undirected (or directed) graphs where cycles exist, such that the risk of entering an infinite-loop is non-trivial.
Note: This question is not about the cycle-detection problem(s) on LeetCode. Below is an iterative approach:
g = {'a':['b','c'],
'b':['a','f'],
'c':['a','f','d'],
'd':['c','e'],
'e':['d'],
'f':['c','b'],
'g':['h'],
'h':['g']
}
def dfs(graph, node, destination):
stack = [node]
visited = []
while stack:
current = stack.pop()
if current == destination:
return True
visited.append(current)
next_nodes = list(filter(lambda x: x not in visited + stack, graph[current]))
stack.extend(next_nodes)
return False
dfs(g,'h', 'g')
>>> True
dfs(g,'a', 'g')
>>> False
My question is, does such a recursive approach exist? And if so, how can it be defined in python?
If you're not interested in detecting if there are any loops or not and just interested in avoiding infinite loops (if any), then something like the following recursive implementation would work for you:
def dfs(graph, node, destination, visited=None):
if visited is None:
visited = set()
if node == destination:
return True
visited.add(node)
return any(
dfs(graph, neighbor, destination, visited=visited)
for neighbor in graph[node]
if neighbor not in visited
)
Note that a generator expression is used inside any, so it's evaluated in a lazy manner (one by one), and the whole any(...) expression returns True early as soon as a solution (i.e. a path to the destination) is found without checking the other neighbors and paths, so no extra recursive calls are made.
def is_ancestor(node, middle):
# search down
if node.data == middle.data:
return True
if node.left:
is_ancestor(node.left, middle)
if node.right:
is_ancestor(node.right, middle)
return False
I am using this function to recursively check is a node is an ancestor of middle.
Let's say we have a tree that looks like
5
/
2
\
4
and I say that node is the node that points to 5 and middle is 2.
When calling is_ancestor(node_with_5, node_with_2), I am expecting to recursively move the node down both to left and right and return True whenever it finds the middle.
However, my current function gives me False even though it will find the middle in the first recursion call.
Any help?
You would have to make some little changes à la:
def is_ancestor(node, middle):
if node is middle: # data could coincide, compare nodes directly
return True
if node.left and is_ancestor(node.left, middle):
return True # do actually return something
if node.right and is_ancestor(node.right, middle):
return True # do actually return something
return False
You could get the entire logic in an even more concise way:
def is_ancestor(node, middle):
if node is None:
return False
if node is middle:
return True
return is_ancestor(node.left, middle) or is_ancestor(node.right, middle)
I have one question regarding the space complexity analysis of the solution of the leetcode problem: 100. Same Tree
The problem:
Given two binary trees, write a function to check if they are the same or not. Two binary trees are considered the same if they are structurally identical and the nodes have the same value.
The solution code:
from collections import deque
class Solution:
def isSameTree(self, p, q):
"""
:type p: TreeNode
:type q: TreeNode
:rtype: bool
"""
def check(p, q):
# if both are None
if not p and not q:
return True
# one of p and q is None
if not q or not p:
return False
if p.val != q.val:
return False
return True
deq = deque([(p, q),])
while deq:
p, q = deq.popleft()
if not check(p, q):
return False
if p:
deq.append((p.left, q.left))
deq.append((p.right, q.right))
return True
My question:
The space complexity analysis of the solution in Leetcode says that it's O(N) for completely unbalanced tree, to keep a deque. However, I think the space complexity is definitely not O(N). I use the following example to show why: Assume that root = 1, root.left = 2, root.left.left = 3, root.left.left.left = 4, N is None. Below are the different stages of deque. For each stage, we popleft one item and add two except when we meet N.
[1]->[2,N]->[N,3,N]->[3,N]->[N,4,N]->[4,N]->[N,N,N]
As you can see from the deque above, the size never reaches N (where N is the number of nodes)
I can tell the space complexity of this algorithm is not O(N) (Am I right?), but I can't tell for sure what it is.
I was asked the following question in a job interview:
Given a root node (to a well formed binary tree) and two other nodes (which are guaranteed to be in the tree, and are also distinct), return the lowest common ancestor of the two nodes.
I didn't know any least common ancestor algorithms, so I tried to make one on the spot. I produced the following code:
def least_common_ancestor(root, a, b):
lca = [None]
def check_subtree(subtree, lca=lca):
if lca[0] is not None or subtree is None:
return 0
if subtree is a or subtree is b:
return 1
else:
ans = sum(check_subtree(n) for n in (subtree.left, subtree.right))
if ans == 2:
lca[0] = subtree
return 0
return ans
check_subtree(root)
return lca[0]
class Node:
def __init__(self, left, right):
self.left = left
self.right = right
I tried the following test cases and got the answer that I expected:
a = Node(None, None)
b = Node(None, None)
tree = Node(Node(Node(None, a), b), None)
tree2 = Node(a, Node(Node(None, None), b))
tree3 = Node(a, b)
but my interviewer told me that "there is a class of trees for which your algorithm returns None." I couldn't figure out what it was and I flubbed the interview. I can't think of a case where the algorithm would make it to the bottom of the tree without ans ever becoming 2 -- what am I missing?
You forgot to account for the case where a is a direct ancestor of b, or vice versa. You stop searching as soon as you find either node and return 1, so you'll never find the other node in that case.
You were given a well-formed binary search tree; one of the properties of such a tree is that you can easily find elements based on their relative size to the current node; smaller elements are going into the left sub-tree, greater go into the right. As such, if you know that both elements are in the tree you only need to compare keys; as soon as you find a node that is in between the two target nodes, or equal to one them, you have found lowest common ancestor.
Your sample nodes never included the keys stored in the tree, so you cannot make use of this property, but if you did, you'd use:
def lca(tree, a, b):
if a.key <= tree.key <= b.key:
return tree
if a.key < tree.key and b.key < tree.key:
return lca(tree.left, a, b)
return lca(tree.right, a, b)
If the tree is merely a 'regular' binary tree, and not a search tree, your only option is to find the paths for both elements and find the point at which these paths diverge.
If your binary tree maintains parent references and depth, this can be done efficiently; simply walk up the deeper of the two nodes until you are at the same depth, then continue upwards from both nodes until you have found a common node; that is the least-common-ancestor.
If you don't have those two elements, you'll have to find the path to both nodes with separate searches, starting from the root, then find the last common node in those two paths.
You are missing the case where a is an ancestor of b.
Look at the simple counter example:
a
b None
a is also given as root, and when invoking the function, you invoke check_subtree(root), which is a, you then find out that this is what you are looking for (in the stop clause that returns 1), and return 1 immidiately without setting lca as it should have been.