经典算法恢复二叉搜索树

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;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值