LeetCode 285 - Inorder Successor in BST
This problem asks us to find the in-order successor of a given node p inside a Binary Search Tree, abbreviated as BST. An in-order traversal of a BST visits nodes in sorted order: 1. Traverse the left subtree 2. Visit the current node 3.
Difficulty: 🟡 Medium
Topics: Tree, Depth-First Search, Binary Search Tree, Binary Tree
Solution
Problem Understanding
This problem asks us to find the in-order successor of a given node p inside a Binary Search Tree, abbreviated as BST.
An in-order traversal of a BST visits nodes in sorted order:
- Traverse the left subtree
- Visit the current node
- Traverse the right subtree
Because BSTs maintain the ordering property:
- Every node in the left subtree is smaller than the current node
- Every node in the right subtree is larger than the current node
the in-order traversal naturally produces values in ascending order.
The in-order successor of a node p is the node that appears immediately after p in this sorted traversal order. More specifically, it is the node with the smallest value strictly greater than p.val.
The input consists of:
root, the root node of a BSTp, a node that exists somewhere inside the tree
The output should be:
- The
TreeNoderepresenting the successor Noneornilif no successor exists
The constraints tell us that the tree can contain up to 10^4 nodes. This is large enough that we should avoid unnecessarily expensive operations if a more efficient BST-based solution exists.
The problem also guarantees:
- All node values are unique
palways exists in the tree
These guarantees simplify the logic because we never need to handle duplicates or missing target nodes.
Several edge cases are important:
pmay be the largest node in the BST, in which case no successor existspmay have a right subtree, which changes how the successor is foundpmay not have a right subtree, requiring us to search among ancestors- The tree may contain only one node
A naive implementation can easily fail when handling nodes without right children or when incorrectly tracking ancestor candidates.
Approaches
Brute Force Approach
The brute-force solution performs a complete in-order traversal of the BST and stores all visited nodes in a list.
Since in-order traversal of a BST produces nodes in sorted order, we can scan through the resulting list and locate node p. The next node in the list, if it exists, is the in-order successor.
This approach is straightforward and easy to reason about because it directly simulates the definition of in-order successor.
The algorithm works as follows:
- Perform an in-order DFS traversal
- Store nodes in an array
- Find the index of
p - Return the next node if one exists
Although correct, this solution is inefficient because it traverses the entire tree even when the answer could be determined much earlier using BST properties.
Optimal Approach
The key observation is that BST ordering allows us to search intelligently instead of traversing the entire tree.
There are two important cases:
- If
phas a right subtree, then the successor is the leftmost node inp.right - If
pdoes not have a right subtree, then the successor is one of its ancestors
While traversing from the root toward p, whenever we move left from a node, that node becomes a potential successor because it is larger than p. We keep track of the smallest such candidate.
This allows us to find the successor in O(h) time, where h is the tree height.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Full in-order traversal and array storage |
| Optimal | O(h) | O(1) | Uses BST properties to search efficiently |
Here, h is the height of the tree. In a balanced BST, this becomes O(log n).
Algorithm Walkthrough
Optimal BST Search Algorithm
- Initialize a variable called
successorasNone.
This variable stores the best candidate successor found so far. At any point during traversal, it represents the smallest node greater than p.val.
2. Start traversing from the root.
We move through the BST similarly to standard BST search logic.
3. Compare p.val with the current node's value.
If p.val is smaller than the current node's value, then the current node could be the successor because it is greater than p.
4. Update successor and move left.
Since we want the smallest value greater than p.val, we store the current node as a candidate and continue searching the left subtree for a potentially smaller valid successor.
5. Otherwise, move right.
If p.val is greater than or equal to the current node's value, then the successor cannot be in the left subtree or the current node itself. We move right to search for larger values.
6. Continue until traversal ends.
Once the traversal reaches None, the stored successor is the correct answer.
Why it works
The algorithm maintains an invariant:
successoralways stores the smallest node encountered so far whose value is greater thanp.val
Whenever we move left from a node, that node becomes a valid candidate because it is larger than p. Moving left searches for an even smaller valid candidate.
Whenever we move right, the current node cannot be the successor because its value is too small.
Because BST ordering guarantees structured value relationships, this process eventually finds the smallest node greater than p.val, which is exactly the in-order successor definition.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
from typing import Optional
class Solution:
def inorderSuccessor(
self,
root: TreeNode,
p: TreeNode
) -> Optional[TreeNode]:
successor = None
current = root
while current:
if p.val < current.val:
successor = current
current = current.left
else:
current = current.right
return successor
The implementation starts by initializing successor to None. This handles the case where no valid successor exists.
The variable current traverses the BST from the root downward.
Whenever p.val < current.val, the current node becomes a candidate successor because it is larger than p. However, there may still be a smaller valid successor in the left subtree, so the algorithm stores the candidate and moves left.
When p.val >= current.val, the current node cannot be the successor. The algorithm therefore moves right to search for larger values.
The loop continues until the traversal reaches a null pointer. At that point, the best candidate stored in successor is returned.
This implementation avoids recursion and auxiliary data structures, resulting in constant extra space usage.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func inorderSuccessor(root *TreeNode, p *TreeNode) *TreeNode {
var successor *TreeNode
current := root
for current != nil {
if p.Val < current.Val {
successor = current
current = current.Left
} else {
current = current.Right
}
}
return successor
}
The Go implementation mirrors the Python logic closely.
Instead of None, Go uses nil pointers. The variable successor is initialized as a nil pointer and updated whenever a better candidate is found.
Because Go uses explicit pointer types, all tree navigation occurs through *TreeNode references.
No special integer overflow handling is required because node values are well within Go's integer range.
Worked Examples
Example 1
Input:
root = [2,1,3]
p = 1
Tree structure:
2
/ \
1 3
We want the smallest value greater than 1.
| Step | Current Node | Successor | Action |
|---|---|---|---|
| 1 | 2 | None | 1 < 2, update successor to 2, move left |
| 2 | 1 | 2 | 1 >= 1, move right |
| 3 | None | 2 | Traversal ends |
Final answer:
2
Example 2
Input:
root = [5,3,6,2,4,null,null,1]
p = 6
Tree structure:
5
/ \
3 6
/ \
2 4
/
1
We want the smallest value greater than 6.
| Step | Current Node | Successor | Action |
|---|---|---|---|
| 1 | 5 | None | 6 >= 5, move right |
| 2 | 6 | None | 6 >= 6, move right |
| 3 | None | None | Traversal ends |
No valid successor exists.
Final answer:
null
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(h) | Traverses only one root-to-leaf path |
| Space | O(1) | Uses only a few pointer variables |
The algorithm follows a single traversal path through the BST. In a balanced tree, the height h is O(log n). In the worst case of a completely skewed tree, the height becomes O(n).
The solution uses constant auxiliary space because it stores only a few references regardless of tree size.
Test Cases
# Helper TreeNode class
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
solution = Solution()
# Test 1: Basic example with successor
root1 = TreeNode(2)
root1.left = TreeNode(1)
root1.right = TreeNode(3)
assert solution.inorderSuccessor(root1, root1.left).val == 2 # successor exists
# Test 2: Largest node has no successor
root2 = TreeNode(5)
root2.left = TreeNode(3)
root2.right = TreeNode(6)
root2.left.left = TreeNode(2)
root2.left.right = TreeNode(4)
root2.left.left.left = TreeNode(1)
assert solution.inorderSuccessor(root2, root2.right) is None # no successor
# Test 3: Node with right subtree
root3 = TreeNode(20)
root3.left = TreeNode(10)
root3.right = TreeNode(30)
root3.left.right = TreeNode(15)
assert solution.inorderSuccessor(root3, root3.left).val == 15 # left child successor
# Test 4: Single node tree
root4 = TreeNode(1)
assert solution.inorderSuccessor(root4, root4) is None # only node
# Test 5: Successor is ancestor
root5 = TreeNode(5)
root5.left = TreeNode(3)
root5.right = TreeNode(7)
root5.left.left = TreeNode(2)
root5.left.right = TreeNode(4)
assert solution.inorderSuccessor(root5, root5.left.right).val == 5 # ancestor successor
# Test 6: Skewed tree
root6 = TreeNode(1)
root6.right = TreeNode(2)
root6.right.right = TreeNode(3)
assert solution.inorderSuccessor(root6, root6.right).val == 3 # skewed right tree
# Test 7: Deep left traversal
root7 = TreeNode(50)
root7.left = TreeNode(30)
root7.left.left = TreeNode(20)
root7.left.right = TreeNode(40)
root7.left.right.right = TreeNode(45)
assert solution.inorderSuccessor(root7, root7.left.right).val == 45 # deeper subtree successor
| Test | Why |
|---|---|
[2,1,3], p=1 |
Standard successor lookup |
p=6 in second example |
Verifies handling when no successor exists |
| Node with right subtree | Ensures subtree logic works correctly |
| Single node tree | Smallest possible input |
| Successor is ancestor | Verifies ancestor tracking |
| Right-skewed tree | Tests worst-case height |
| Deep left traversal | Tests repeated candidate updates |
Edge Cases
One important edge case occurs when p is the maximum value in the BST. In this situation, no node has a larger value, so the correct answer is None. A buggy implementation might accidentally return the last ancestor visited. This implementation avoids that issue because successor is updated only when a node larger than p is encountered.
Another important case is when p has a right subtree. The successor should then be the leftmost node in that subtree. The iterative BST search naturally handles this because traversal continues into the right subtree and keeps updating smaller valid candidates while moving left.
A third important edge case is a highly skewed tree. For example, a tree shaped like a linked list can expose recursion depth problems in recursive implementations. This solution uses iterative traversal, so it avoids recursion stack growth entirely.
A fourth edge case occurs when the successor is an ancestor rather than a descendant. For example, if p = 4 in the tree [5,3,7,2,4], the successor is 5. The algorithm correctly handles this by remembering candidate ancestors whenever traversal moves left.