LeetCode 99 - Recover Binary Search Tree
The problem gives us the root of a binary search tree, but exactly two nodes in the tree have had their values swapped accidentally. Our task is to restore the tree so that it once again satisfies the binary search tree property, without modifying the tree structure itself.
Difficulty: 🟡 Medium
Topics: Tree, Depth-First Search, Binary Search Tree, Binary Tree
Solution
Problem Understanding
The problem gives us the root of a binary search tree, but exactly two nodes in the tree have had their values swapped accidentally. Our task is to restore the tree so that it once again satisfies the binary search tree property, without modifying the tree structure itself.
A binary search tree, or BST, has a very important property:
- Every node in the left subtree contains a value smaller than the current node.
- Every node in the right subtree contains a value larger than the current node.
One of the most useful consequences of this property is that an inorder traversal of a valid BST produces values in strictly increasing order.
The input is the root node of the corrupted BST. The output is not returned directly. Instead, we must modify the tree in place by swapping the incorrect values back to their proper locations.
The constraints tell us that the tree contains between 2 and 1000 nodes. This is small enough that even an O(n) traversal is completely acceptable. However, the follow up asks whether we can solve the problem using constant extra space, which hints that the intended optimal solution avoids storing the entire traversal.
There are several important edge cases to keep in mind:
- The swapped nodes may be adjacent in inorder traversal.
- The swapped nodes may be far apart in inorder traversal.
- One swapped node may be the root.
- The tree may contain only two nodes.
- The BST property is guaranteed to be violated by exactly two swapped nodes, so we do not need to handle arbitrary corruption.
A naive implementation can fail when the swapped nodes are not adjacent, because there may be two inversion points instead of one. Correct handling requires carefully identifying both misplaced nodes.
Approaches
Brute Force Approach
The most straightforward solution is to perform an inorder traversal of the tree and store all nodes in an array.
Since inorder traversal of a valid BST should produce sorted values, we can compare the collected sequence with a sorted version of itself. The two nodes whose values differ are the swapped nodes. Once identified, we swap their values back.
This approach works because sorting reveals the correct order of values, and only two positions are incorrect.
However, this solution requires additional memory proportional to the number of nodes because we store the traversal. While acceptable for the given constraints, it does not satisfy the follow up requirement asking for constant extra space.
Optimal Approach
The key observation is that inorder traversal of a BST must be sorted.
If exactly two nodes are swapped, the inorder sequence will contain one or two inversions.
For example:
Correct inorder:
1 2 3 4 5
Swapped inorder:
1 4 3 2 5
Here:
4 > 3is an inversion3 > 2is another inversion
The first misplaced node is the larger value from the first inversion.
The second misplaced node is the smaller value from the last inversion.
We can detect these inversions during inorder traversal without storing the entire traversal. We only need:
prev, the previously visited nodefirst, the first incorrect nodesecond, the second incorrect node
After traversal completes, swapping first.val and second.val restores the BST.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n log n) | O(n) | Store inorder traversal and compare with sorted array |
| Optimal | O(n) | O(h) | Detect inversions during inorder traversal, where h is tree height |
The recursive solution uses O(h) stack space due to recursion. A Morris Traversal version can achieve true O(1) auxiliary space.
Algorithm Walkthrough
- Perform an inorder traversal of the tree.
Inorder traversal visits nodes in the order:
left -> root -> right
For a valid BST, this produces a sorted sequence. 2. Keep track of the previously visited node.
During traversal, store a pointer called prev.
If the BST is valid, we should always have:
prev.val < current.val
- Detect inversions.
If we encounter:
prev.val > current.val
then the BST ordering is violated. 4. Record the first misplaced node.
The first time an inversion appears:
first = prevsecond = current
The larger node from the inversion is definitely misplaced. 5. Update the second misplaced node if another inversion occurs.
If another inversion appears later:
- update
second = current
This handles the case where swapped nodes are not adjacent. 6. Continue traversal until all nodes are processed.
We must traverse the entire tree because the second incorrect node may appear later. 7. Swap the values of the two misplaced nodes.
After traversal finishes:
first.val, second.val = second.val, first.val
This restores BST ordering.
Why it works
A valid BST produces a strictly increasing inorder sequence. Swapping two values introduces ordering violations. If the swapped nodes are adjacent, there will be one inversion. If they are separated, there will be two inversions.
The first incorrect node is always the larger value from the first inversion. The second incorrect node is always the smaller value from the last inversion. Swapping these two values restores the sorted inorder order, which restores the BST property.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from typing import Optional
class Solution:
def recoverTree(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
first = None
second = None
prev = None
def inorder(node: Optional[TreeNode]) -> None:
nonlocal first, second, prev
if not node:
return
inorder(node.left)
if prev and prev.val > node.val:
if first is None:
first = prev
second = node
prev = node
inorder(node.right)
inorder(root)
if first and second:
first.val, second.val = second.val, first.val
The implementation follows the inorder traversal strategy discussed earlier.
The variables first and second store the two misplaced nodes. The variable prev keeps track of the previously visited node during traversal.
The recursive inorder function traverses the tree in sorted order. At each node, we compare the current node's value with the previous node's value.
If prev.val > node.val, we have detected an inversion.
During the first inversion:
first = prev
second = node
If another inversion appears later, only second is updated.
After traversal completes, the two incorrect values are swapped back.
This solution modifies the tree directly and does not create a new tree.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func recoverTree(root *TreeNode) {
var first *TreeNode
var second *TreeNode
var prev *TreeNode
var inorder func(node *TreeNode)
inorder = func(node *TreeNode) {
if node == nil {
return
}
inorder(node.Left)
if prev != nil && prev.Val > node.Val {
if first == nil {
first = prev
}
second = node
}
prev = node
inorder(node.Right)
}
inorder(root)
if first != nil && second != nil {
first.Val, second.Val = second.Val, first.Val
}
}
The Go implementation is structurally very similar to the Python version.
Since Go does not support nested functions with nonlocal, we use closure variables directly inside the recursive function.
Go uses nil instead of Python's None for null references.
Pointers are used naturally because tree nodes are referenced through *TreeNode.
Worked Examples
Example 1
Input:
1
/
3
\
2
Inorder traversal produces:
3, 2, 1
This is not sorted.
| Current Node | Prev Node | Inversion? | First | Second |
|---|---|---|---|---|
| 3 | None | No | None | None |
| 2 | 3 | Yes | 3 | 2 |
| 1 | 2 | Yes | 3 | 1 |
After traversal:
first = 3
second = 1
Swap values:
1 <-> 3
Result:
3
/
1
\
2
Example 2
Input:
3
/ \
1 4
/
2
Inorder traversal:
1, 3, 2, 4
Violation occurs because 3 > 2.
| Current Node | Prev Node | Inversion? | First | Second |
|---|---|---|---|---|
| 1 | None | No | None | None |
| 3 | 1 | No | None | None |
| 2 | 3 | Yes | 3 | 2 |
| 4 | 2 | No | 3 | 2 |
Swap:
3 <-> 2
Correct BST:
2
/ \
1 4
/
3
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Every node is visited exactly once |
| Space | O(h) | Recursive call stack uses height of tree |
The traversal processes each node exactly one time, giving linear time complexity.
The recursive implementation uses stack space proportional to the tree height. In the worst case of a completely skewed tree, this becomes O(n). In a balanced tree, it is O(log n).
A Morris Traversal implementation can reduce auxiliary space to true O(1).
Test Cases
# Helper functions for testing
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def inorder(root):
if not root:
return []
return inorder(root.left) + [root.val] + inorder(root.right)
# Test 1: Example 1
root = TreeNode(1)
root.left = TreeNode(3)
root.left.right = TreeNode(2)
Solution().recoverTree(root)
assert inorder(root) == [1, 2, 3] # adjacent violations
# Test 2: Example 2
root = TreeNode(3)
root.left = TreeNode(1)
root.right = TreeNode(4)
root.right.left = TreeNode(2)
Solution().recoverTree(root)
assert inorder(root) == [1, 2, 3, 4] # non-adjacent swap
# Test 3: Two-node tree
root = TreeNode(2)
root.left = TreeNode(1)
root.val, root.left.val = root.left.val, root.val
Solution().recoverTree(root)
assert inorder(root) == [1, 2] # minimum tree size
# Test 4: Root involved in swap
root = TreeNode(2)
root.left = TreeNode(3)
root.right = TreeNode(1)
Solution().recoverTree(root)
assert inorder(root) == [1, 2, 3] # root swapped
# Test 5: Large skewed tree
root = TreeNode(1)
root.right = TreeNode(3)
root.right.left = TreeNode(2)
Solution().recoverTree(root)
assert inorder(root) == [1, 2, 3] # skewed structure
# Test 6: Adjacent inorder swap
root = TreeNode(3)
root.left = TreeNode(2)
root.right = TreeNode(4)
root.left.left = TreeNode(1)
root.val, root.left.val = root.left.val, root.val
Solution().recoverTree(root)
assert inorder(root) == [1, 2, 3, 4] # adjacent swapped nodes
# Test 7: Non-adjacent inorder swap
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(6)
root.left.left = TreeNode(1)
root.left.right = TreeNode(5)
root.right.left = TreeNode(3)
root.right.right = TreeNode(7)
Solution().recoverTree(root)
assert inorder(root) == [1, 2, 3, 4, 5, 6, 7] # distant swapped nodes
| Test | Why |
|---|---|
| Example 1 | Verifies handling of two inversions |
| Example 2 | Verifies non-adjacent swapped nodes |
| Two-node tree | Smallest valid input size |
| Root involved in swap | Ensures root corruption is handled |
| Large skewed tree | Tests unbalanced tree behavior |
| Adjacent inorder swap | Verifies single inversion handling |
| Non-adjacent inorder swap | Verifies double inversion handling |
Edge Cases
Adjacent Swapped Nodes
If the swapped nodes are adjacent in inorder traversal, there will only be one inversion instead of two.
For example:
1 3 2 4
A buggy implementation may expect two violations and fail to identify the correct nodes.
This implementation handles the case correctly because the first inversion already identifies both misplaced nodes.
Non-Adjacent Swapped Nodes
If the swapped nodes are far apart, there will be two inversions.
For example:
1 5 3 4 2 6
The correct nodes are 5 and 2.
A naive implementation might incorrectly swap 5 and 3, because it only examines the first inversion.
This implementation updates second every time an inversion is found, ensuring the final incorrect node is captured properly.
Root Node Is Swapped
Sometimes one of the swapped nodes is the root.
Since inorder traversal processes nodes purely by ordering and not by position, the algorithm does not care whether a node is the root, leaf, or internal node.
The comparison logic works uniformly across the entire tree, so root corruption is naturally handled without any special cases.