1、题目描述
二叉搜索树中的两个节点被错误地交换。
请在不改变其结构的情况下,恢复这棵树。
示例 1:
示例 2:
进阶:
- 使用 O(n) 空间复杂度的解法很容易实现。
- 你能想出一个只使用常数空间的解决方案吗?
2、我的代码
/**
* 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 swap(TreeNode* x, TreeNode* y){
if(x == nullptr || y == nullptr)
return;
int tmp = x->val;
x->val = y->val;
y->val = tmp;
}
void recoverTree(TreeNode* root) {
TreeNode* x = nullptr;
TreeNode* y = nullptr;
TreeNode* pre = nullptr;
stack<TreeNode*> nodeStack;
while(!nodeStack.empty() || root != nullptr){
while(root != nullptr){
nodeStack.push(root);
root = root->left;
}
root = nodeStack.top();
nodeStack.pop();
if(pre != nullptr && pre->val > root->val){
y = root;
if(x == nullptr)
x = pre;
else
break;
}
pre = root;
root = root->right;
}
swap(x,y);
}
};
3、网上好的解法
方法二:迭代中序遍历
算法:
在这里,我们通过迭代构造中序遍历,并在一次遍历中找到交换的节点。
迭代顺序很简单:尽可能的向左走,然后向右走一步,重复一直到结束。
若要找到交换的节点,就记录中序遍历中的最后一个节点 pred(即当前节点的前置节点),并与当前节点的值进行比较。如果当前节点的值小于前置节点 pred 的值,说明该节点是交换节点之一。
交换的节点只有两个,因此在确定了第二个交换节点以后,可以终止遍历。这样,就可以直接获取节点(而不仅仅是它们的值),从而实现 O(1) 的交换时间,大大减少了步骤 3 所需的时间。
class Solution {
public void swap(TreeNode a, TreeNode b) {
int tmp = a.val;
a.val = b.val;
b.val = tmp;
}
public void recoverTree(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque();
TreeNode x = null, y = null, pred = null;
while (!stack.isEmpty() || root != null) {
while (root != null) {
stack.add(root);
root = root.left;
}
root = stack.removeLast();
if (pred != null && root.val < pred.val) {
y = root;
if (x == null) x = pred;
else break;
}
pred = root;
root = root.right;
}
swap(x, y);
}
}
方法三:递归中序遍历
算法:
方法二的迭代可以转换为递归方式。
递归中序遍历很简单:遵循 Left->Node->Right 方向,即对左子节点进行递归调用,然后判断该节点是否被交换,然后对右子节点执行递归调用。
class Solution {
TreeNode x = null, y = null, pred = null;
public void swap(TreeNode a, TreeNode b) {
int tmp = a.val;
a.val = b.val;
b.val = tmp;
}
public void findTwoSwapped(TreeNode root) {
if (root == null) return;
findTwoSwapped(root.left);
if (pred != null && root.val < pred.val) {
y = root;
if (x == null) x = pred;
else return;
}
pred = root;
findTwoSwapped(root.right);
}
public void recoverTree(TreeNode root) {
findTwoSwapped(root);
swap(x, y);
}
}
4、自己的代码可以优化的地方
内存消耗多,执行效率也不好
5、优化代码至简无可简
6、我的思考
解题思路
本题使用树的中序遍历,利用Morris遍历方法,实现常数空间复杂度
中序遍历 + 双指针找逆序对
二叉搜索树的中序遍历结果可以等效于一个升序数组
因此先用数组举例,如果原始的二叉搜索树为
1, 2, 3, 4, 5
如果将其中2,4两个元素进行交换,变成
1, 4, 3, 2, 5
那么我们可以使用双指针的方法,检查这个数组里的逆序对,将逆序对找出来就可以解决问题
观察数组
- 第一对逆序对4, 3,是索引小的那个是被交换元素
- 第二对逆序对3, 2,是索引大的那个是被交换元素
所以我们在遇到逆序对的时候,如果是第一次遇见,则存储索引小的那个,如果不是,则存储索引大的那个
if(pre != NULL && cur->val < pre->val){
if(s1 == NULL) s1 = pre; // 索引小的已经找到,就不再改变
s2 = cur;
}
为什么这里s2用的不是else?
因为存在两个相邻元素交换的情况,所以我们在第一次遇见的把索引大的那个也存下来
- 如果后面没有逆序对了,那就是这一对
- 如果后面还有逆序对,会覆盖s2(因为有if判断,所以s1不会被覆盖)
Morris遍历
通过前面的分析,使用递归的方法可以很容易地完成本题。
首先明确:在二叉搜索树中,如果一个结点有前驱结点,那么前驱结点的右指针只有两种情况(可以自己找两个二叉搜索树验证一下,此处不再证明)
- 是空的
- 是这个结点本身(即前驱是它的父结点)
所以我们可以把前驱结点的右指针这一特性利用起来,从而降低空间复杂度。Morris遍历算法的步骤如下:
检查当前结点的左孩子:
- 如果当前结点的左孩子为空,说明要不没有前驱,要不前驱是它的父结点,所以进行检查,然后进入右孩子。
- 如果当前结点的左孩子不为空,说明左子树里肯定有它的前驱,那就找到这个前驱
- 如果前驱结点的右孩子是空,说明还没检查过左子树,那么把前驱结点的右孩子指向当前结点,然后进入当前结点的左孩子。
- 如果当前结点的前驱结点其右孩子指向了它本身,说明左子树已被检查过,就直接进行检查,然后把前驱结点的右孩子设置为空,恢复原树,再进入右孩子。
复杂度分析
时间复杂度:每个结点访问了2次,因此时间复杂度为O(n)
空间复杂度:只使用了常数空间,因此空间复杂度为O(1)
参考资料:
望月从良 《面试算法:二叉树的Morris遍历算法》
https://www.jianshu.com/p/484f587c967c
/**
* 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:
// s1 存小索引那个结点,s2存大索引那个结点,pre存前驱结点
TreeNode *s1 = NULL, *s2 = NULL, *pre = NULL;
void recoverTree(TreeNode* root) {
TreeNode* cur = root; // 游标
while(cur != NULL){
if(cur->left != NULL){ // 进入左子树
// 找到cur的前驱结点,分两种情况
// 1、cur的左子结点没有右子结点,那cur的左子结点就是前驱
// 2、cur的左子结点有右子结点,就一路向右下,走到底就是cur的前驱
TreeNode* predecessor = cur->left;
while(predecessor->right != NULL && predecessor->right != cur){
predecessor = predecessor->right;
}
// 前驱还没有指向自己,说明左边还没有遍历,将前驱的右指针指向自己,后进入前驱
if(predecessor->right == NULL){
predecessor->right = cur;
cur = cur->left;
}else{
// 前驱已经指向自己了,直接比较是否有逆序对,然后进入右子树
if(pre != NULL && cur->val < pre->val){
if(s1 == NULL) s1 = pre;
s2 = cur;
}
pre = cur;
predecessor->right = NULL;
cur = cur->right;
}
}else{ // 左子树为空时,检查是否有逆序对,然后进入右子树
if(pre != NULL && cur->val < pre->val){
if(s1 == NULL) s1 = pre;
s2 = cur;
}
pre = cur;
cur = cur->right;
}
}
// 进行交换
int t = s1->val;
s1->val = s2->val;
s2->val = t;
return;
}
};