Difficulty: Medium, Asked-in: Amazon, Google.
Key takeaway: an excellent problem to learn problem-solving using inorder traversal and BST augmentation (storing extra information inside BST nodes for solving a problem).
Given the root of a binary search tree and an integer k, write a program to return the kth largest value among all the node values in the given BST.
Input:
12
/ \
7 17
/ \ /
3 9 14
If k = 2, then 14 is the 2nd largest element.
If k = 4, then 9 is the 4th largest element.
If k = 5, then 7 is the 5th largest element. As we know, a binary search tree is a sorted version of the binary tree where each node value is greater than all the node values in the left subtree and greater than all the node values in the right sub-tree. So if we perform an in-order traversal, it will produce elements in sorted order.
So a basic idea would be:
inorder(root, sortedArray)
if root is null
return
inorder(root.left, sortedArray)
add root.data to sortedArray
inorder(root.right, sortedArray)
kthLargestBST(root, k)
if root is null
return INT_MIN
sortedArray = empty list
inorder(root, sortedArray)
n = size of sortedArray
return sortedArray[n - k]Time complexity = Time complexity of inorder traversal to store elements in a sorted order+ Accessing (n - k)th element from the sorted list = O(n) + O(1) = O(n). Space complexity = O(n).
The critical question is: can we solve the problem without using extra space? can we track the kth largest element using the in-order traversal? Think!
So one idea would be: while doing the reverse inorder traversal, we keep decreasing the value of k by one after accessing each node. When the k becomes equal to 0, we stop the traversal because the current node is our kth largest. So we return the value stored in the current node. This way, one could speed up the solution because there is no need to do complete inorder traversal.
kthLargestBST(root, k)
if root is null
return INT_MIN
// Visit the right subtree first
// to process values from largest to smallest
rightResult = kthLargestBST(root.right, k)
if k == 0
return rightResult
k = k - 1
if k == 0
return root.data
// Continue searching in the left subtree
return kthLargestBST(root.left, k)We are accessing nodes in decreasing order and decrementing k by 1. In general, traversal will return value after the k steps. So time complexity = k * time complexity of each step = k * O(1) = O(k).
Space complexity = O(h) for recursion call stack, where h is the height of the tree.
This idea is similar to the above approach, but instead of using recursive inorder traversal in reverse order, we are using iterative reverse traversal with the help of a stack. The idea here is: when we pop node from the stack, we decrement the value of k by 1 and check k is equal to 0 or not. If yes, we stop and return the value stored in the current node.
You can explore this blog: iterative tree traversal using stack.
kthLargestBST(root, k)
if root is null or k <= 0
return INT_MIN
stack = empty stack
current = root
// Reverse inorder visits nodes from largest to smallest
while stack is not empty or current is not null
if current is not null
push current onto stack
current = current.right
else
current = top of stack
pop stack
k = k - 1
if k == 0
return current.data
current = current.left
// k is greater than the number of nodes
return INT_MINAs mentioned in the analysis of the previous approach, the iterative traversal will also access nodes in decreasing order by decrementing the value of k. So iteration will stop after k number of steps. Time complexity = k * O(1) = O(k).
Space complexity = O(h) for recursion call stack, where h is the height of the tree.
The critical question is: What if the BST is often modified using insert or delete operations and we need to find the kth largest element frequently? Can we optimize the time complexity? Let's think!
We know from the BST property that the value stored in all the nodes in the right sub-tree of any node is larger than the value stored in that node. So for the given root in the BST, if we know the count of elements in its right sub-tree, we can easily define the rank or order of the root node. Suppose there are 5 elements in the right subtree, then the root element would be the 6th largest element in the tree.
Step 1: We define a new structure of the BST node where we use an extra parameter rightCount to store the node count in the right sub-tree.
class BSTNode
data
left
right
rightCount
BSTNode(value)
data = value
left = null
right = null
rightCount = 0Step 2: now we build a BST of the given input by inserting each element one by one. During the insertion, we can keep track of a number of elements in the right subtree and update the rightCount for each node.
Implementation code c++
insertBST(root, value)
if root is null
return a new BSTNode(value)
if value > root.data
root.right = insertBST(root.right, value)
root.rightCount = root.rightCount + 1
else if value < root.data
root.left = insertBST(root.left, value)
return rootStep 3: Now we define a function kthLargestBST(root, k) to return the kth largest.
We start from the root node by comparing k with the order or rank of the root node. What would be the rank of the root node? The answer is simple: If there are root-> rightCount nodes in the right subtree, then the root is (root-> rightCount + 1)th largest element in the tree.
Why are we passing k - rightCount - 1 as the input parameter? The answer: we are searching the kth largest in the left subtree and (rightCount + 1)number of elements are already greater than all the elements in the left subtree. So the rank of the kth largest element in the left subtree would be k - rightCount - 1.
kthLargestBST(root, k)
if root is null
return INT_MIN
// The root is ranked after all nodes in its right subtree
rootRank = root.rightCount + 1
if k == rootRank
return root.data
else if k < rootRank
return kthLargestBST(root.right, k)
else
return kthLargestBST(root.left, k - rootRank)kthLargestBST(root, k)
if root is null
return INT_MIN
current = root
while current is not null
currentRank = current.rightCount + 1
if k == currentRank
return current.data
else if k < currentRank
current = current.right
else
k = k - currentRank
current = current.left
// k is invalid or larger than the number of nodes
return INT_MINThe time complexity of insert and delete operations into a BST is O(h), where h is the height of the binary tree. h is O(logn) for the balanced tree and O(n) for a skewed tree.
Once our BST is ready with the rightCount of each node, we are searching the kth largest element by going left or right at each step based on the comparison. In other, we reduce the input size by the size of the left or right subtree at each step. So in the worst case, we will be traversing the tree's height and doing an O(1) operation at each step. So time complexity of finding kth largest = O(h).
Space complexity = storing rightCount inside each node = O(n). For recursive implementation, we also need to also consider the size of the recursion call stack which is O(h). But overall space complexity would be O(n) in both implementations.
Please write in the message below if you find anything incorrect, or you want to share more insight, or you know some different approaches to solve this problem. Enjoy learning, Enjoy algorithms!