Difficulty: Easy, Asked-in: Amazon, Microsoft, Ola.
Key takeaway: An excellent problem to learn problem-solving using both iterative and recursive approaches in the binary search tree.
Write a program to find the in-order predecessor of a given node x in a binary search tree (BST). An in-order predecessor of node x is the node that will be visited just before node x in the in-order traversal.
Problem note: If node x is visited first (leftmost node in BST), then the predecessor of node x is NULL. In other words, the in-order predecessor of the node with the minimum value does not exist in the BST.
If the left subtree of node x is present, the in-order predecessor of node x is a node with the maximum value in its left subtree. If the left subtree of node x doesn't exist, then there are two cases:
The critical question is: How can we move up in the BST? To implement this, we include a parent pointer in the BST node structure, which points to the parent of the given node.
BSTNode* bstMaximum(BSTNode* root)
{
while (root->right != NULL)
root = root->right;
return root;
}
BSTNode* inorderPredecessor(BSTNode* x)
{
if (x == NULL)
return NULL;
if (x->left != NULL)
return bstMaximum(x->left);
else if(x->parent != NULL && x->parent->right == x)
return x->parent;
else
{
BSTNode* p = x->parent;
BSTNode* curr = x;
while (p != NULL && p->left == curr)
{
curr = p;
p = p->parent;
}
return p;
}
}
def bstMaximum(root):
while root.right is not None:
root = root.right
return root
def inorderPredecessor(x):
if x is None:
return None
if x.left is not None:
return bstMaximum(x.left)
elif x.parent is not None and x.parent.right == x:
return x.parent
else:
p = x.parent
curr = x
while p is not None and p.left == curr:
curr = p
p = p.parent
return p
If the left child of node x is present, then the maximum value in the left subtree will be the predecessor. In the worst case, node x will be the root node and the node with minimum value in the left subtree is the deepest leaf node. Here we need to traverse the complete height of the tree. So the time complexity in the worst case = O(h).
If the left child of x is not present, then we traverse up to find the predecessor. In the worst case, x will be one of the deepest leaf nodes, and the root node will be the predecessor. Here, we need to traverse the complete height of the tree. So the time complexity in the worst case = O(h).
Overall, the time complexity will be O(h), which depends on the height of the tree. Since we are storing an extra parent pointer inside each node, the space complexity = O(n).
Storing a parent pointer inside the BST node helps us easily traverse up the tree, but it requires O(n) extra space. Now, the critical question is: Can we solve this problem without using a parent pointer? Let’s think!
The idea is: We maintain a pred pointer to track the predecessor and move down from the root to node x by comparing the root->key with the x->key.
By the end of this loop, we return the pred pointer.
BSTNode* bstMaximum(BSTNode* root)
{
while (root->right != NULL)
root = root->right;
return root;
}
BSTNode* inorderPredecessor(BSTNode* root, BSTNode* x)
{
if (root == NULL)
return NULL;
BSTNode* pred = NULL;
while (root != NULL)
{
if (x->key < root->key)
{
root = root->left;
}
else if (x->key > root->key)
{
pred = root;
root = root->right;
}
else
{
if (root->left != NULL)
pred = bstMaximum(root->left);
break;
}
}
return pred;
}
def bstMaximum(root):
while root.right is not None:
root = root.right
return root
def inorderPredecessor(root, x):
if root is None:
return None
pred = None
while root is not None:
if x.key < root.key:
root = root.left
elif x.key > root.key:
pred = root
root = root.right
else:
if root.left is not None:
pred = bstMaximum(root.left)
break
return pred
During the search process, we need to traverse the entire height of the tree in the worst case. So the time complexity in the worst case = O(h). Since there is no parent pointer and we use a constant amount of extra space, the space complexity = O(1).
In the above solution, we iteratively move down the tree to search for node x and update the predecessor. The critical question is: Can we think of implementing it using recursion?
The idea is to recursively move down to search for node x in the BST and keep updating the predecessor. When we find the node x and the left subtree of node x is present, we return the node with the maximum value in the left subtree of x. If the left subtree of node x doesn't exist, then the in-order predecessor is one of its ancestors, which has already been updated while searching.
BSTNode* bstMaximum(BSTNode* root)
{
while (root->right != NULL)
root = root->right;
return root;
}
BSTNode* inorderPredecessor(BSTNode* root, BSTNode* pred, BSTNode* x)
{
if (root == NULL)
return NULL;
if (root->key == x->key)
{
if (x->left != NULL)
return bstMaximum(x->left);
}
else if (root->key > x->key)
return inorderPredecessor(root->left, pred, x);
else
{
pred = root;
return inorderPredecessor(root->right, pred, x);
}
return pred;
}
def bstMaximum(root):
while root.right:
root = root.right
return root
def inorderPredecessor(root, pred, x):
if root is None:
return None
if root.key == x.key:
if x.left:
return bstMaximum(x.left)
elif root.key > x.key:
return inorderPredecessor(root.left, pred, x)
else:
pred = root
return inorderPredecessor(root.right, pred, x)
return pred
At each step of recursion, we are going one level down. So in the worst case, we will be traversing the complete height of the tree. So time complexity = O(h).
Space complexity will depend on the size of the call stack, which will be equal to the height of the recursion tree. So space complexity = O(h).
As we know, the in-order predecessor of node x is the node that will be visited just before node x in the in-order traversal. The critical question is: Can we solve it using an in-order traversal? Here's an idea!
We traverse the BST in an in-order manner and keep track of the previous node visited. When we find the target node, the previous node represents its in-order predecessor. To ensure that the changes to the previous node are propagated correctly across the recursive calls, we use a pointer prev and pass it as a reference during the recursive call.
BSTNode* inorder(BSTNode* root, BSTNode* x, BSTNode*& prev)
{
if (root == NULL)
return NULL;
BSTNode* leftResult = inorder(root->left, x, prev);
if (leftResult != NULL)
return leftResult;
if (x->key == root->key)
return prev;
prev = root;
return inorder(root->right, x, prev);
}
BSTNode* inorderPredecessor(BSTNode* root, BSTNode* x)
{
if (root == NULL || x == NULL)
return NULL;
BSTNode* prev = NULL;
return inorder(root, x, prev);
}
def inorder(root, x, prev):
if root is None:
return None
leftResult = inorder(root.left, x, prev)
if leftResult is not None:
return leftResult
if x.key == root.key:
return prev
prev[0] = root
return inorder(root.right, x, prev)
def inorderPredecessor(root, x):
if root is None or x is None:
return None
prev = [None]
return inorder(root, x, prev)
In the worst case, we will traverse each node only once using an in-order traversal. So, the time complexity will be O(n) in the worst case. Inside the code, we use constant extra space, but recursion will utilize a call stack with a size equal to the height of the tree. So, the space complexity is O(h).
If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy algorithms!