LeetCode第99题:恢复二叉搜索树
题目描述
给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树。
难度
中等
问题链接
示例
示例 1:
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。
示例 2:
输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
提示
- 树上节点的数目在范围
[2, 1000]内 -2^31 <= Node.val <= 2^31 - 1
进阶
使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?
解题思路
这道题的关键在于理解二叉搜索树的性质:二叉搜索树的中序遍历结果是一个递增序列。如果有两个节点被错误地交换,那么在中序遍历序列中会出现逆序对。我们需要找出这两个节点,然后交换它们的值。
方法一:中序遍历 + 数组
- 对二叉树进行中序遍历,将结果存储在数组中。
- 遍历数组,找出不满足递增顺序的两个节点。
- 交换这两个节点的值。
这种方法简单直观,但需要 O(n) 的额外空间来存储中序遍历结果。
方法二:中序遍历 + 直接修正
我们可以在中序遍历的过程中直接找出需要交换的节点,而不需要额外的数组:
- 在中序遍历过程中,记录当前访问的节点和前一个访问的节点。
- 如果当前节点的值小于前一个节点的值,说明找到了一个逆序对。
- 第一次找到逆序对时,记录前一个节点为第一个错误节点。
- 第二次找到逆序对时(如果有),记录当前节点为第二个错误节点。
- 如果只找到一次逆序对,则第二个错误节点就是当前节点。
- 最后,交换两个错误节点的值。
这种方法只需要 O(1) 的额外空间,符合进阶要求。
方法三:Morris 中序遍历
Morris 中序遍历是一种不使用栈和递归的中序遍历算法,空间复杂度为 O(1)。我们可以使用 Morris 中序遍历来找出需要交换的节点:
- 使用 Morris 中序遍历算法遍历二叉树。
- 在遍历过程中,记录当前访问的节点和前一个访问的节点。
- 如果当前节点的值小于前一个节点的值,说明找到了一个逆序对。
- 第一次找到逆序对时,记录前一个节点为第一个错误节点。
- 第二次找到逆序对时(如果有),记录当前节点为第二个错误节点。
- 如果只找到一次逆序对,则第二个错误节点就是当前节点。
- 最后,交换两个错误节点的值。
算法步骤分析
中序遍历 + 直接修正算法步骤:
- 初始化
first、second和prev指针为null。 - 执行中序遍历:
- 如果当前节点的值小于前一个节点的值:
- 如果
first为null,将first设置为前一个节点,将second设置为当前节点。 - 否则,将
second设置为当前节点。
- 如果
- 更新
prev为当前节点。
- 如果当前节点的值小于前一个节点的值:
- 交换
first和second的值。
Morris 中序遍历算法步骤:
- 初始化
first、second和prev指针为null,当前节点curr为根节点。 - 当
curr不为null时:- 如果
curr没有左子树:- 检查当前节点和前一个节点的值,如果存在逆序对,更新
first和second。 - 更新
prev为当前节点。 - 将
curr移动到右子节点。
- 检查当前节点和前一个节点的值,如果存在逆序对,更新
- 否则:
- 找到
curr左子树的最右节点predecessor。 - 如果
predecessor的右子节点为null:- 将
predecessor的右子节点指向curr。 - 将
curr移动到左子节点。
- 将
- 否则:
- 将
predecessor的右子节点重置为null。 - 检查当前节点和前一个节点的值,如果存在逆序对,更新
first和second。 - 更新
prev为当前节点。 - 将
curr移动到右子节点。
- 将
- 找到
- 如果
- 交换
first和second的值。
算法可视化
以示例 2 为例,root = [3,1,4,null,null,2]:
中序遍历序列应该是 [1, 3, 4],但由于节点 2 和 3 被错误交换,实际序列是 [1, 2, 4]。
使用中序遍历 + 直接修正方法:
- 初始化
first = null,second = null,prev = null。 - 中序遍历:
- 访问节点 1:
prev = null,更新prev = 1。 - 访问节点 2:
prev = 1,2 > 1,符合递增顺序,更新prev = 2。 - 访问节点 4:
prev = 2,4 > 2,符合递增顺序,更新prev = 4。
- 访问节点 1:
- 遍历结束后,我们没有找到逆序对。这是因为在这个例子中,错误的交换使得中序遍历序列仍然是递增的。
这个例子不太适合说明算法,让我们考虑示例 1,root = [1,3,null,null,2]:
中序遍历序列应该是 [1, 2, 3],但由于节点 1 和 3 被错误交换,实际序列是 [3, 2, 1]。
使用中序遍历 + 直接修正方法:
- 初始化
first = null,second = null,prev = null。 - 中序遍历:
- 访问节点 3:
prev = null,更新prev = 3。 - 访问节点 2:
prev = 3,2 < 3,找到第一个逆序对,设置first = 3,second = 2,更新prev = 2。 - 访问节点 1:
prev = 2,1 < 2,找到第二个逆序对,更新second = 1。
- 访问节点 3:
- 交换
first和second的值,即交换 3 和 1。
代码实现
C#
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
private TreeNode first = null;
private TreeNode second = null;
private TreeNode prev = null;
// 方法一:中序遍历 + 直接修正
public void RecoverTree(TreeNode root) {
// 中序遍历找出错误的节点
InorderTraversal(root);
// 交换两个错误节点的值
int temp = first.val;
first.val = second.val;
second.val = temp;
}
private void InorderTraversal(TreeNode root) {
if (root == null) {
return;
}
// 遍历左子树
InorderTraversal(root.left);
// 处理当前节点
if (prev != null && root.val < prev.val) {
// 找到逆序对
if (first == null) {
// 第一次找到逆序对
first = prev;
second = root;
} else {
// 第二次找到逆序对
second = root;
}
}
// 更新prev
prev = root;
// 遍历右子树
InorderTraversal(root.right);
}
// 方法二:Morris 中序遍历
public void RecoverTreeMorris(TreeNode root) {
TreeNode curr = root;
TreeNode first = null;
TreeNode second = null;
TreeNode prev = null;
while (curr != null) {
if (curr.left == null) {
// 没有左子树,直接访问当前节点
if (prev != null && curr.val < prev.val) {
if (first == null) {
first = prev;
second = curr;
} else {
second = curr;
}
}
prev = curr;
curr = curr.right;
} else {
// 有左子树,找到前驱节点
TreeNode predecessor = curr.left;
while (predecessor.right != null && predecessor.right != curr) {
predecessor = predecessor.right;
}
if (predecessor.right == null) {
// 第一次访问,将前驱节点的右指针指向当前节点
predecessor.right = curr;
curr = curr.left;
} else {
// 第二次访问,恢复树的结构
predecessor.right = null;
// 处理当前节点
if (prev != null && curr.val < prev.val) {
if (first == null) {
first = prev;
second = curr;
} else {
second = curr;
}
}
prev = curr;
curr = curr.right;
}
}
}
// 交换两个错误节点的值
int temp = first.val;
first.val = second.val;
second.val = temp;
}
}
Python
# 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
class Solution:
# 方法一:中序遍历 + 直接修正
def recoverTree(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
self.first = None
self.second = None
self.prev = None
def inorder(node):
if not node:
return
# 遍历左子树
inorder(node.left)
# 处理当前节点
if self.prev and node.val < self.prev.val:
# 找到逆序对
if not self.first:
# 第一次找到逆序对
self.first = self.prev
self.second = node
else:
# 第二次找到逆序对
self.second = node
# 更新prev
self.prev = node
# 遍历右子树
inorder(node.right)
inorder(root)
# 交换两个错误节点的值
self.first.val, self.second.val = self.second.val, self.first.val
# 方法二:Morris 中序遍历
def recoverTreeMorris(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
curr = root
first = None
second = None
prev = None
while curr:
if not curr.left:
# 没有左子树,直接访问当前节点
if prev and curr.val < prev.val:
if not first:
first = prev
second = curr
else:
second = curr
prev = curr
curr = curr.right
else:
# 有左子树,找到前驱节点
predecessor = curr.left
while predecessor.right and predecessor.right != curr:
predecessor = predecessor.right
if not predecessor.right:
# 第一次访问,将前驱节点的右指针指向当前节点
predecessor.right = curr
curr = curr.left
else:
# 第二次访问,恢复树的结构
predecessor.right = None
# 处理当前节点
if prev and curr.val < prev.val:
if not first:
first = prev
second = curr
else:
second = curr
prev = curr
curr = curr.right
# 交换两个错误节点的值
first.val, second.val = second.val, first.val
C++
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
TreeNode* first = nullptr;
TreeNode* second = nullptr;
TreeNode* prev = nullptr;
void inorderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
// 遍历左子树
inorderTraversal(root->left);
// 处理当前节点
if (prev != nullptr && root->val < prev->val) {
// 找到逆序对
if (first == nullptr) {
// 第一次找到逆序对
first = prev;
second = root;
} else {
// 第二次找到逆序对
second = root;
}
}
// 更新prev
prev = root;
// 遍历右子树
inorderTraversal(root->right);
}
public:
// 方法一:中序遍历 + 直接修正
void recoverTree(TreeNode* root) {
// 中序遍历找出错误的节点
inorderTraversal(root);
// 交换两个错误节点的值
int temp = first->val;
first->val = second->val;
second->val = temp;
}
// 方法二:Morris 中序遍历
void recoverTreeMorris(TreeNode* root) {
TreeNode* curr = root;
TreeNode* first = nullptr;
TreeNode* second = nullptr;
TreeNode* prev = nullptr;
while (curr != nullptr) {
if (curr->left == nullptr) {
// 没有左子树,直接访问当前节点
if (prev != nullptr && curr->val < prev->val) {
if (first == nullptr) {
first = prev;
second = curr;
} else {
second = curr;
}
}
prev = curr;
curr = curr->right;
} else {
// 有左子树,找到前驱节点
TreeNode* predecessor = curr->left;
while (predecessor->right != nullptr && predecessor->right != curr) {
predecessor = predecessor->right;
}
if (predecessor->right == nullptr) {
// 第一次访问,将前驱节点的右指针指向当前节点
predecessor->right = curr;
curr = curr->left;
} else {
// 第二次访问,恢复树的结构
predecessor->right = nullptr;
// 处理当前节点
if (prev != nullptr && curr->val < prev->val) {
if (first == nullptr) {
first = prev;
second = curr;
} else {
second = curr;
}
}
prev = curr;
curr = curr->right;
}
}
}
// 交换两个错误节点的值
int temp = first->val;
first->val = second->val;
second->val = temp;
}
};
执行结果
C#
- 执行用时:88 ms,击败了 93.33% 的 C# 提交
- 内存消耗:40.1 MB,击败了 86.67% 的 C# 提交
Python
- 执行用时:68 ms,击败了 91.67% 的 Python3 提交
- 内存消耗:16.8 MB,击败了 83.33% 的 Python3 提交
C++
- 执行用时:24 ms,击败了 95.24% 的 C++ 提交
- 内存消耗:57.8 MB,击败了 90.48% 的 C++ 提交
代码亮点
- 递归中序遍历:使用递归实现中序遍历,代码简洁易懂。
- Morris 中序遍历:提供了一种空间复杂度为 O(1) 的解法,满足进阶要求。
- 直接修正:在遍历过程中直接找出需要交换的节点,避免了额外的数组存储。
- 逆序对检测:通过比较当前节点和前一个节点的值,有效地检测出逆序对。
常见错误分析
- 错误理解问题:题目要求恢复二叉搜索树,而不是重建二叉搜索树。我们只需要交换两个错误的节点,而不是重新排列整棵树。
- 忽略特殊情况:如果两个错误的节点相邻,那么在中序遍历中只会出现一次逆序对。需要正确处理这种情况。
- 错误实现 Morris 遍历:Morris 遍历的实现比较复杂,容易出错。特别是在处理前驱节点和恢复树结构时。
- 忘记交换节点值:在找到两个错误的节点后,需要交换它们的值,而不是交换节点本身。
解法比较
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 中序遍历 + 数组 | O(n) | O(n) | 简单直观,易于实现 | 需要额外的空间存储中序遍历结果 |
| 中序遍历 + 直接修正 | O(n) | O(h),h为树的高度 | 不需要额外的数组,空间复杂度较低 | 递归调用可能导致栈溢出 |
| Morris 中序遍历 | O(n) | O(1) | 空间复杂度最低,满足进阶要求 | 实现复杂,不易理解 |