I wrote this algorithm for a coding challenge on HackerRank to determine if a give binary tree is a BST. Yet in some cases when the tree is not a BST my algorithm returns True anyway. I couldn't find what was wrong, everything seems ok, right? Or is there something I don't know about BSTs?
Node is defined as:
class node:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
My algorithm is
def checkBST(root):
if not root:
return True
else:
if root.left and root.left.data >= root.data:
return False
if root.right and root.right.data <= root.data:
return False
return checkBST(root.left) and checkBST(root.right)
A binary search tree has all the nodes on the left branch less than the parent node, and all the nodes on the right branch greater than the parent node.
So your code fails on a case like this:
5
/ \
4 7
/ \
2 6
It's not a valid BST because if you searched for 6, you'd follow the right branch from the root, and subsequently fail to find it.
Take a look at a picture below for which your code is giving an incorrect answer:
Where are you going wrong:
1. if an element exists on the left subtree if there exists a node with a value greater than root.
2. if an element exists on the right subtree if there exists a node with a value smaller than root.
You should try this approach :
if not root:
return True
else:
if root.left and maximumOfSubtree(root.left) >= root.data:
return False
if root.right and minimumOfSubtree(root.right) <= root.data:
return False
return checkBST(root.left) and checkBST(root.right)
so the problem is to determine whether the given tree is a BST or not.
the best way to find out is through an inorder traversal.
Do In-Order Traversal of the given tree and store the result in a temp array.
Check if the temp array is sorted in ascending order, if it is, then the tree is BST.
this can be the approach
def check_binary_search_tree_(root):
visited = []
def traverse(node):
if node.left: traverse(node.left)
visited.append(node.data)
if node.right: traverse(node.right)
traverse(root)
fc = {}
for i in visited:
if i in fc:
return False
else:
fc[i]=1
m = sorted(visited)
if visited==m:
return True
return False
refer to this for other methods https://www.geeksforgeeks.org/a-program-to-check-if-a-binary-tree-is-bst-or-not/
method 1 and method 2 are similar to your approach so it will help you to understand that too.
Related
I have looked at this code line by line maybe 100 times and I am stumped. Why doesn't this work????
Problem: input of [5,4,6,null,null,3,7] (this is a BST with 5 being the root and 4 and 6 being its left and right nodes)
returns True when it should return False (3 should be not be to the right of parent node 5). False should be returned at the first nested if statement in the while loop.
def isValidBST(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
# BFS method
current_node = root
queue = []
queue.append(current_node)
while len(queue) > 0:
current_node = queue.pop(0)
if current_node.left:
if (current_node.val > current_node.left.val) and (root.val > current_node.left.val):
queue.append(current_node.left)
else:
return False
if current_node.right:
if (current_node.val < current_node.right.val) and (root.val < current_node.right.val):
queue.append(current_node.right)
else:
return False
return True
The tree for which your code fails is:
5
/ \
4 6
/ \
3 7
When current_node is 6, your code checks that root.val > current_node.left.val, which means it checks that 5 > 3. This is true. And so it wrongly concludes there is no problem there, yet there is.
You may now think to just change the direction of that comparison, but there is a logical error in your approach: it is not enough to compare the root with every node in its subtree. For instance, in the following tree you should detect that 3 is violating the BST property, even though it is not in conflict with the root:
2
\
5
\
6
/
3
The conflict is with the value 5, which is neither the parent of 3, nor the root.
In conclusion, a BFS traversal is not really an ideal tool to verify a BST. Although it surely can be done, it is more natural to use DFS, as it allows to carry down the (accumulated) limitations that will apply to every node in the subtree.
I am looking at LeetCode problem 572. Subtree of Another Tree:
Given the roots of two binary trees root and subRoot, return true if there is a subtree of root with the same structure and node values of subRoot and false otherwise.
A subtree of a binary tree tree is a tree that consists of a node in tree and all of this node's descendants. The tree tree could also be considered as a subtree of itself.
Example 1
Input: root = [3,4,5,1,2], subRoot = [4,1,2]
Output: true
Example 2
Input: root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]
Output: false
My Code
class Solution:
def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
if not root and not subRoot:
return True
if root==None or subRoot==None:
return False
if root.val == subRoot.val and self.isSubtree(root.left,subRoot.left) and self.isSubtree(root.right,subRoot.right):
return True
return self.isSubtree(root.left,subRoot) or self.isSubtree(root.right,subRoot)
Input Failing:
Input
[3,4,5,1,null,2]
[3,1,2]
Output True
Expected False
The problem with your algorithm is that it allows the check to skip some nodes.
The failing test case represents these trees:
root: 3 subRoot: 3
/ \ / \
4 5 1 2
/ /
1 2
In the first call of your function, the roots have equal values, so then this recursive call is made:
root: 4 subRoot: 1
/
1
Now you can already see that here the subRoot is a sub tree of root, but this fact should not be used here, since we are half-way a tree matching procedure where the original roots already matched, so we are not allowed now to skip over the node 4, yet that is what will happen.
The real problem is that you are trying to cover two recursive needs in one recursive function:
find a node in root where subRoot matches with all its descendants
verify that all descendants match.
You need to split this into two independent recursive functions. For instance:
class Solution:
def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
def recurMatch(root, subRoot):
return not (subRoot or root) or (
subRoot and root and
root.val == subRoot.val and
recurMatch(root.left, subRoot.left) and
recurMatch(root.right, subRoot.right)
)
def recurSearch(root):
return root and (
recurMatch(root, subRoot) or
recurSearch(root.left) or
recurSearch(root.right)
)
return bool(recurSearch(root))
Faster?
The above solution will only perform "average" on LeetCode. You could improve by relying on fast string comparison: stringify both trees and check whether the second string occurs within the first. And the shorter the serialisation you can generate, the faster the algorithm will work.
Spoiler:
class Solution:
def isSubtree(self, root, subRoot):
def stringify(root):
return f"[{stringify(root.left) if root.left else ''}{chr(root.val+10128)}{stringify(root.right) if root.right else ''}]"
return stringify(subRoot) in stringify(root)
So I have been trying out questions from Hackerrank and there has been a question on checking whether a tree is BST or not. I have been using the level order traversal for this check. if node.left.value> node.value and node.right.value<node.value the Tree must not be a BST.
Apart from the condition where I haven't checked for a single root node, all the other test cases fail. Where have I been going wrong?
def checkBST(root):
# if root.left and root.right is None:
# return True
if not root:
return
flag=0
q=[root]
while len(q)>0:
temp=q.pop(0)
if temp.left is not None:
if temp.left.data> temp.data:
flag=1
break
else:
q.append(temp.left)
if temp.right is not None:
if temp.right.data<temp.data:
flag=1
break
else:
q.append(temp.right)
if flag==1:
return False
else:
return True
It is not enough to test that a direct child is in the correct relation to its parent. There are invalid BSTs for which all those comparison-tests pass. You need to ensure that in the whole left subtree of a node there is no node with a greater value than that node's value.
So a level order traversal is not really the right tool to do this. It will work better with an in-order traversal, since a BST will have an non-decreasing in-order traversal.
Most of the questions I've searched for regarding binary trees shows the implementation of binary search trees, but not binary trees. The terms of a complete binary tree are:
Either an empty tree or it has 1 node with 2 children, where each
child is another Binary Tree.
All levels are full (except for possibly the last level)
All leaves on the bottom-most level are
as far left as possible.
I've come up with a concept but it doesn't seem to running through the recursion properly -- Does anyone know what I'm doing wrong?
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def add(self, key):
if self.key:
if self.left is None:
self.left = Node(key)
else:
self.left.add(key)
if self.right is None:
self.right = Node(key)
else:
self.right.add(key)
else:
self.key = key
return (self.key)
The problem in your code is that you are adding the same value multiple times. You add the node, and then still recurse deeper, where you do the same.
The deeper problem is that you don't really know where to insert the node before you have reached the bottom level of the tree, and have detected where that level is incomplete. Finding the correct insertion point may need a traversal through the whole tree... which is defeating the speed gain you would expect to get from using binary trees in the first place.
I provide here three solutions, starting with the most efficient:
1. Using a list as tree implementation
For complete trees there is a special consideration to make: if you number the nodes by level, starting with 0 for the root, and within each level from left to right, you notice that the number of a node's parent is (k-1)/2 when its own number is k. In the other direction: if a node with number k has children, then its left child has number k*2+1, and the right child has a number that is one greater.
Because the tree is complete, there will never be gaps in this numbering, and so you could store the nodes in a list, and use the indexes of that list for the node numbering. Adding a node to the tree now simply means you append it to that list. Instead of a Node object, you just have the tree list, and the index in that list is your node reference.
Here is an implementation:
class CompleteTree(list):
def add(self, key):
self.append(key)
return len(self) - 1
def left(self, i):
return i * 2 + 1 if i * 2 + 1 < len(self) else -1
def right(self, i):
return i * 2 + 2 if i * 2 + 2 < len(self) else -1
#staticmethod
def parent(i):
return (i - 1) // 2
def swapwithparent(self, i):
if i > 0:
p = self.parent(i)
self[p], self[i] = self[i], self[p]
def inorder(self, i=0):
left = self.left(i)
right = self.right(i)
if left >= 0:
yield from self.inorder(left)
yield i
if right >= 0:
yield from self.inorder(right)
#staticmethod
def depth(i):
return (i + 1).bit_length() - 1
Here is a demo that creates your example tree, and then prints the keys visited in an in-order traversal, indented by their depth in the tree:
tree = CompleteTree()
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(" " * tree.depth(node), tree[node])
Of course, this means you have to reference nodes a bit different from when you would use a real Node class, but the efficiency gain pays off.
2. Using an extra property
If you know how many nodes there are in a (sub)tree, then from the bit representation of that number, you can know where exactly the next node should be added.
For instance, in your example tree you have 5 nodes. Imagine you want to add a 6 to that tree. The root node would tell you that you currently have 5 and so you need to update it to 6. In binary that is 110. Ignoring the left-most 1-bit, the rest of the bits tell you whether to go left or right. In this case, you should go right (1) and then finally left (0), creating the node in that direction. You can do this iteratively or recursively.
Here is an implementation with recursion:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.count = 1
def add(self, key):
self.count += 1
if self.left is None:
self.left = Node(key)
elif self.right is None:
self.right = Node(key)
# extract from the count the second-most significant bit:
elif self.count & (1 << (self.count.bit_length() - 2)):
self.right.add(key)
else:
self.left.add(key)
def inorder(self):
if self.left:
yield from self.left.inorder()
yield self
if self.right:
yield from self.right.inorder()
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(node.key)
3. Without extra property
If no property can be added to Node objects, then a more extensive search is needed to find the right insertion point:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def newparent(self):
# Finds the node that should serve as parent for a new node
# It returns a tuple:
# if parent found: [-1, parent for new node]
# if not found: [height, left-most leaf]
# In the latter case, the subtree is perfect, and its left-most
# leaf is the node to be used, unless self is a right child
# and its sibling has the insertion point.
if self.right:
right = self.right.newparent()
if right[0] == -1: # found inbalance
return right
left = self.left.newparent()
if left[0] == -1: # found inbalance
return left
if left[0] != right[0]:
return [-1, right[1]] # found inbalance
# temporary result in perfect subtree
return [left[0]+1, left[1]]
elif self.left:
return [-1, self] # found inbalance
# temporary result for leaf
return [0, self]
def add(self, key):
_, parent = self.newparent()
if not parent.left:
parent.left = Node(key)
else:
parent.right = Node(key)
def __repr__(self):
s = ""
if self.left:
s += str(self.left).replace("\n", "\n ")
s += "\n" + str(self.key)
if self.right:
s += str(self.right).replace("\n", "\n ")
return s
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
print(tree)
This searches recursively the tree from right to left, to find the candidate parent of the node to be added.
For large trees, this can be improved a bit, by doing a binary-search among paths from root to leaf, based on the length of those paths. But it will still not be as efficient as the first two solutions.
You can use the sklearn Decision trees, as they are able to be set up as binary decision trees as well. link to the documentation here.
You really need to augment your tree in some way. Since this is not a binary search tree, the only real information you have about each node is whether or not it has a left and right child. Unfortunately, this isn't helpful in navigating a complete binary tree. Imagine a complete binary tree with 10 levels. Until the 9th level, every single node has both a left child and a right child, so you have no way of knowing which path to take down to the leaves. So the question is, what information do you add to each node? I would add the count of nodes in that tree.
Maintaining the count is easy, since every time you descend down a subtree you know to add one to the count at that node. What you want to recognize is the leftmost imperfect subtree. Every perfect binary tree has n = 2^k - 1, where k is the number of levels and n is the number of nodes. There are quick and easy ways to check if a number is 1 less than a power of two (see the first answer to this question), and in fact in a complete binary tree every node has at most one child that isn't the root of a perfect binary tree. Follow a simple rule to add nodes:
If the left child is None, set root.left = Node(key) and return
Else if the right child is None, set root.right = Node(key) and return
If one of the children of the current node is the root of an imperfect subtree, make that node the current node (descend down that subtree)
Else if the sizes are unequal, make the node with the smaller subtree the current node.
Else, make the left child the current node.
By augmenting each node with the size of the subtree rooted there, you have all the information you need at every node to build a recursive solution.
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)