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 -2^{31} −231 <= Node.val <= 2 31 − 1 2^{31 - 1} 231−1
进阶:使用 O ( n ) O(n) O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O ( 1 ) O(1) O(1) 空间的解决方案吗?
题解
关键:要联想到二叉搜索树在不同的 DFS 生成的数组序列的特点,以及两个节点被错误地交换后对原二叉搜索树造成了什么影响
方法一:显式中序遍历
思路
我们需要考虑两个节点被错误地交换后对原二叉搜索树造成了什么影响。对于二叉搜索树,我们知道如果对其进行中序遍历,得到的值序列是递增有序的,而如果我们错误地交换了两个节点,等价于在这个值序列中交换了两个值,破坏了值序列的递增性。
我们来看下如果在一个递增的序列中交换两个值会造成什么影响。假设有一个递增序列 a = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] a=[1,2,3,4,5,6,7] a=[1,2,3,4,5,6,7]。如果我们交换两个不相邻的数字,例如 2 2 2 和 6 6 6,原序列变成了 a = [ 1 , 6 , 3 , 4 , 5 , 2 , 7 ] a=[1,6,3,4,5,2,7] a=[1,6,3,4,5,2,7],那么显然序列中有两个位置不满足 a i < a i + 1 a_i<a_{i+1} ai<ai+1,在这个序列中体现为 6 > 3 6>3 6>3, 5 > 25 5>25 5>25,因此只要我们找到这两个位置,即可找到被错误交换的两个节点。如果我们交换两个相邻的数字,例如 2 2 2 和 3 3 3,此时交换后的序列只有一个位置不满足 a i < a i + 1 a_i<a_{i+1} ai<ai+1 。因此整个值序列中不满足条件的位置或者有两个,或者有一个。
至此,解题方法已经呼之欲出了:
- 找到二叉搜索树中序遍历得到值序列的不满足条件的位置。
- 如果有两个,我们记为 i i i 和 j j j( i < j i<j i<j 且 a i > a i + 1 & & a j > a j + 1 a_i>a_{i+1}\ \&\&\ a_j>a_{j+1} ai>ai+1 && aj>aj+1 ),那么对应被错误交换的节点即为 a i a_i ai 对应的节点和 a j + 1 a_{j+1} aj+1 对应的节点,我们分别记为 x x x 和 y y y。
- 如果有一个,我们记为 i i i,那么对应被错误交换的节点即为 a i a_i ai 对应的节点和 a i + 1 a_{i+1} ai+1 对应的节点,我们分别记为 x x x 和 y y y。
- 交换 x x x 和 y y y 两个节点即可。
实现部分,本方法开辟一个新数组 nums \textit{nums} nums 来记录中序遍历得到的值序列,然后线性遍历找到两个位置 i i i 和 j j j,并重新遍历原二叉搜索树修改对应节点的值完成修复,具体实现可以看下面的代码。
代码实现
/**
* 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 {
public:
void inorder(TreeNode* root, vector<int>& nums) {
if (root == nullptr) {
return;
}
inorder(root->left, nums);
nums.push_back(root->val);
inorder(root->right, nums);
}
pair<int,int> findTwoSwapped(vector<int>& nums) {
int n = nums.size();
int index1 = -1, index2 = -1;
for (int i = 0; i < n - 1; ++i) {
if (nums[i + 1] < nums[i]) {
index2 = i + 1;
if (index1 == -1) {
index1 = i;
} else {
break;
}
}
}
int x = nums[index1], y = nums[index2];
return {x, y};
}
void recover(TreeNode* r, int count, int x, int y) {
if (r != nullptr) {
if (r->val == x || r->val == y) {
r->val = r->val == x ? y : x;
if (--count == 0) {
return;
}
}
recover(r->left, count, x, y);
recover(r->right, count, x, y);
}
}
void recoverTree(TreeNode* root) {
vector<int> nums;
inorder(root, nums);
pair<int,int> swapped= findTwoSwapped(nums);
recover(root, 2, swapped.first, swapped.second);
}
};
复杂度分析
时间复杂度:
O
(
N
)
O(N)
O(N),其中
N
N
N 为二叉搜索树的节点数。中序遍历需要
O
(
N
)
O(N)
O(N) 的时间,判断两个交换节点在最好的情况下是
O
(
1
)
O(1)
O(1),在最坏的情况下是
O
(
N
)
O(N)
O(N),因此总时间复杂度为
O
(
N
)
O(N)
O(N)。
空间复杂度:
O
(
N
)
O(N)
O(N)。我们需要用
nums
\textit{nums}
nums 数组存储树的中序遍历列表。
方法二:隐式中序遍历
思路
方法一是显式地将中序遍历的值序列保存在一个 nums \textit{nums} nums 数组中,然后再去寻找被错误交换的节点,但我们也可以隐式地在中序遍历的过程就找到被错误交换的节点 x x x 和 y y y。
具体来说,由于我们只关心中序遍历的值序列中每个相邻的位置的大小关系是否满足条件,且错误交换后最多两个位置不满足条件,因此在中序遍历的过程我们只需要维护当前中序遍历到的最后一个节点 pred \textit{pred} pred,然后在遍历到下一个节点的时候,看两个节点的值是否满足前者小于后者即可,如果不满足说明找到了一个交换的节点,且在找到两次以后就可以终止遍历。
这样我们就可以在中序遍历中直接找到被错误交换的两个节点 x x x 和 y y y,不用显式建立 nums \textit{nums} nums 数组。
中序遍历的实现有迭代和递归两种等价的写法,在本方法中提供迭代实现的写法。使用迭代实现中序遍历需要手动维护栈。
代码实现
Leetcode 官方题解(迭代写法)
class Solution {
public:
void recoverTree(TreeNode* root) {
stack<TreeNode*> stk;
TreeNode* x = nullptr;
TreeNode* y = nullptr;
TreeNode* pred = nullptr;
while (!stk.empty() || root != nullptr) {
while (root != nullptr) {
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
if (pred != nullptr && root->val < pred->val) {
y = root;
if (x == nullptr) {
x = pred;
}
else break;
}
pred = root;
root = root->right;
}
swap(x->val, y->val);
}
};
我的(即,递归写法)
class Solution {
queue<TreeNode*> two_val;
TreeNode *big1 = nullptr, *small1 = nullptr, *big2 = nullptr, *small2 = nullptr; // 两对不是升序的值
// 如果是和相邻的值交换,则 big2 和 small2 都为 nullptr,交换 big1 和 small1 的 val
// 如果不是和相邻的值交换,则四个指针都不为 nullptr,交换 big1 和 small2 的 val
// 例如:
// 1. [6,2,3,4,5,1] big1 = 6, small1 = 2, big2 = 5, small2 = 1
// 2. [1,2,4,3,5,6] big1 = 4, small1 = 3, big2 = nullptr, small2 = nullptr
public:
void recoverTree(TreeNode* root) {
centerDFS(root);
int val;
if(big2 == nullptr){
val = big1->val;
big1->val = small1->val;
small1->val = val;
}else{
val = big1->val;
big1->val = small2->val;
small2->val = val;
}
}
bool centerDFS(TreeNode* root){ // 中序遍历二叉排序树
if(root == nullptr) return true;
if(centerDFS(root->left) == false) return false;
// 访问根结点
if(two_val.size() == 0) two_val.push(root);
else if(two_val.size() == 1){
two_val.push(root);
if(two_val.front()->val > two_val.back()->val){
big1 = two_val.front();
small1 = two_val.back();
}
}else if(two_val.size() == 2){
two_val.pop();
two_val.push(root);
if(two_val.front()->val > two_val.back()->val){
if(big1 == nullptr){
big1 = two_val.front();
small1 = two_val.back();
}else{
big2 = two_val.front();
small2 = two_val.back();
return false; // 后面不可能再有其它错误的值对了
}
}
}
if(centerDFS(root->right) == false) return false;
return true;
}
};
复杂度分析
时间复杂度:最坏情况下(即待交换节点为二叉搜索树最右侧的叶子节点)我们需要遍历整棵树,时间复杂度为
O
(
N
)
O(N)
O(N),其中
N
N
N 为二叉搜索树的节点个数。
空间复杂度:
O
(
H
)
O(H)
O(H),其中
H
H
H 为二叉搜索树的高度。中序遍历的时候栈的深度取决于二叉搜索树的高度。
方法三:Morris 中序遍历(进阶解法)
思路
仔细观察方法二,我们可以发现,空间复杂度主要是由栈实现的中序遍历所导致的,如果我们可以在使用常数的空间复杂度来遍历一棵二叉树,则可以再进一步的优化算法。此时我们可以想到一个算法——Morris 遍历算法,该算法能将非递归的中序遍历空间复杂度降为 O ( 1 ) O(1) O(1)。
Morris 的中序遍历规则整体步骤如下(假设当前遍历到的节点为 x x x):
- 如果 x x x 无左孩子,则访问 x x x 的右孩子,即 x = x . right x = x.\textit{right} x=x.right。
- 如果
x
x
x 有左孩子,则找到
x
x
x 的左子树上最右的节点(即左子树中序遍历的最后一个节点,也就是
x
x
x 在中序遍历生成序列中的前驱节点),我们记为
predecessor
\textit{predecessor}
predecessor。根据
predecessor
\textit{predecessor}
predecessor 的右孩子是否为空,进行如下操作。
- 如果 predecessor \textit{predecessor} predecessor 的右孩子为空,则将其右孩子指向 x x x,然后访问 x x x 的左孩子,即 x = x . left x = x.\textit{left} x=x.left。
- 如果 predecessor \textit{predecessor} predecessor 的右孩子不为空,则此时其右孩子指向 x x x,说明我们已经遍历完 x x x 的左子树,我们将 predecessor \textit{predecessor} predecessor 的右孩子置空,然后访问 x x x 的右孩子,即 x = x . right x = x.\textit{right} x=x.right。
- 重复上述操作,直至访问完整棵树。
其实整个过程我们就多做一步:将当前节点左子树中最右边的节点指向它,这样在左子树遍历完成后我们通过这个指向走回了 x x x,并且能再通过这个来判断是否已经遍历完成了左子树,而不用再通过栈来维护,省去了栈的空间复杂度。
了解完这个算法以后,其他地方与方法二并无不同,我们同样也是维护一个 pred \textit{pred} pred 变量去比较即可。
代码实现
class Solution {
public:
void recoverTree(TreeNode* root) {
TreeNode *x = nullptr, *y = nullptr, *pred = nullptr, *predecessor = nullptr;
while (root != nullptr) {
if (root->left != nullptr) {
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root->left;
while (predecessor->right != nullptr && predecessor->right != root) {
predecessor = predecessor->right;
}
// 让 predecessor 的右指针指向 root,继续遍历左子树
if (predecessor->right == nullptr) {
predecessor->right = root;
root = root->left;
}
// 说明左子树已经访问完了,我们需要断开链接
else {
if (pred != nullptr && root->val < pred->val) {
y = root;
if (x == nullptr) {
x = pred;
}
}
pred = root;
predecessor->right = nullptr;
root = root->right;
}
}
// 如果没有左孩子,则直接访问右孩子
else {
if (pred != nullptr && root->val < pred->val) {
y = root;
if (x == nullptr) {
x = pred;
}
}
pred = root;
root = root->right;
}
}
swap(x->val, y->val);
}
};
复杂度分析
时间复杂度:
O
(
N
)
O(N)
O(N),其中
N
N
N 为二叉搜索树的高度。Morris 遍历中每个节点会被访问两次,因此总时间复杂度为
O
(
2
N
)
=
O
(
N
)
O(2N)=O(N)
O(2N)=O(N)。
空间复杂度:
O
(
1
)
O(1)
O(1)。