leetcode:99. 恢复二叉搜索树

题目来源

题目描述

在这里插入图片描述
在这里插入图片描述

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 recoverTree(TreeNode* root) {

    }
};

题目解析

三个指针:找到这两个错误节点

用三个指针:

  • first、second分别指向第一个和第二个错乱位置的节点
  • pre指向当前节点的中序遍历的前一个节点
  • 如果对所有的节点值都不一样的搜索二叉树进行中序遍历,那么出现的节点值会一直升序。如果,如果有两个节点位置错了,就会出现降序
  • 如果出现了两次降序
    • 第一个错误的节点为第一次降序时较大的节点
    • 第二个错误的节点为第二次降序时较小的节点
    • 举个例子:
      • 正确的中序遍历顺序: {1、2、3、4、5}
      • 遍历得到的中序遍历:{1、5、3、4、2}
      • 第一次降序为5->3,所以第一个错误的节点为5
      • 第二次降序为4->2,所以第二个错误的节点为2
      • 把5和2交换即可恢复
  • 如果出现了一次降序
    • 第一个错误的节点为本次降序时较大的节点
    • 第二个错误的节点为本次降序时较小的节点
    • 举个例子:
      • 正确的中序遍历顺序: {1、2、3、4、5}
      • 遍历得到的中序遍历:{1、2、4、3、5}
      • 只有一次降序4->3,所以第一个错误节点为4,第二个错误节点为3
      • 把4和3交换过来即可恢复
  • 寻找两个错误节点的过程可以总结为:第一个错误节点为第一次降序是较大的节点;第二个错误节点为最后一次降序是较小的节点
  • 所以,关键点中序遍历,实现如下:

在这里插入图片描述

即:中序遍历BST,依次访问的节点值是递增的,错误的BST会破坏递增性,从而能定位出错误。错误有两种

  • 错误1:出现了两对不满足前小后大,需要交换第一对的第一个元素与第二对的第二个元素。
  • 错误2:只出现一对不满足前小后大,交换这一对元素即可。

在这里插入图片描述

  • 只用比较前后访问的节点值,prev 保存上一个访问的节点,当前访问的是 root 节点。
  • 每访问一个节点,如果prev.val>=root.val,就找到了一对“错误对”。
  • 检查一下它是第一对错误对,还是第二对错误对。
  • 遍历结束,就确定了待交换的两个错误点,进行交换。
class Solution {
    TreeNode *pre = NULL, *first = NULL, *second = NULL;
    void inorder(TreeNode* root){
        if(root == NULL){
            return;
        }

        inorder(root->left);
        if (pre != NULL && pre->val > root->val) {
            if (first == NULL) first = pre;
            second = root;
        }
        pre = root;
        inorder(root->right);
    }
public:
    void recoverTree(TreeNode* root) {
        inorder(root);
        swap(first->val, second->val);
    }
};

在这里插入图片描述
其迭代写法

class Solution {
public:
    void recoverTree(TreeNode* root) {
        TreeNode *pre = NULL, *first = NULL, *second = NULL, *p = root;
        stack<TreeNode*> st;
        while (p || !st.empty()) {
            while (p) {
                st.push(p);
                p = p->left;
            }
            p = st.top(); st.pop();
            if (pre) {
                if (pre->val > p->val) {
                    if (!first) first = pre;
                    second = p;
                }
            }
            pre = p;
            p = p->right;
        }
        swap(first->val, second->val);
    }
};

使用栈写法:

class Solution {
public:
    std::vector<TreeNode*> getTwoErrNodes(TreeNode* root) {
        std::vector<TreeNode*> errs(2, nullptr);
        if(root == nullptr){
            return errs;
        }
        
        std::stack<TreeNode*> stack;
        TreeNode *prev = nullptr, *curr = root;
        while (!stack.empty() || curr != nullptr){
            if(curr != nullptr){
                stack.push(curr);
                curr = curr->left;
            }else{
                curr = stack.top(); stack.pop();
                if(prev != nullptr && prev->val > curr->val){
                    errs[0] = errs[0] == nullptr ? prev : errs[0];
                    errs[1] = curr;
                }
                prev = curr;
                curr = curr->right;
            }
        }
        return errs;
    }
};

在结构上交换这两个错误节点

如果想要在结构上交换这两个错误节点,需要先找到这两个错误节点的父节点。

  • 假设第一个错误节点是e1,e1的父节点是e1P,e1的左孩子为e1L,e1的右孩子为e1R
  • 假设第二个错误节点是e2,e1的父节点是e2P,e2的左孩子为e2L,e2的右孩子为e2R

在结构上交换两个节点,实际上是把两个节点互换环境。即:

  • e2成为e1P的孩子,e1L和e1R成为e2的孩子节点
  • e1成为e2P的孩子,e2L和e2R成为e1的孩子节点

但是,注意要处理很多特殊情况:

  • 比如,如果 e1 是头节点,则意味着 e1P 为 null,那么让 e2 成为 e1P 的孩子节点时,关于 e1P的任何 left 指针或 right 指针操作都会发生错误,因为 e1P 为 null 则根本没有 Node 类型节点的结构。
  • 再如,如果 e1 本身就是 e2 的左孩子节点,即 e1==e2L,那么让 e2L 成为 e1 的左孩子节点时,e1 的 left 指针将指向 e2L,将会指向自己,这会让整棵二叉树发生严重的结构错误。

换句话说,我们必须理清楚 e1 及其上下环境之间的关系,e2 及其上下环境之间的关系,以及两个环境之间是否有联系。有以下三个问题和一个特别注意是必须关注的。

  • 情况一:e1和e2是否有一个头结点?如果有,谁是头
  • 情况二:e1和e2是否相邻?如果相邻,谁是谁的父节点
  • 情况三:e1和e2分别是各自父节点的左孩子还是右孩子

特别注意:因为是在中序遍历时先找到e1,后找到e2,所以e1 一定不是e2 的右孩子节点,e2 也一定不是e1 的左孩子节点。

以上三个问题与特别注意之间相互影响,情况非常复杂。经过仔细整理,共有 14 种情况,每一种情况在调整 e1 和 e2 各自的拓扑关系时都有特殊处理。

  1. e1 是头节点,e1 是e2 的父节点,此时e2 只可能是e1 的右孩子节点。
  2. e1 是头节点,e1 不是e2 的父节点,e2 是e2P 的左孩子节点。
  3. e1 是头节点,e1 不是e2 的父节点,e2 是e2P 的右孩子节点。
  4. e2 是头节点,e2 是e1 的父节点,此时e1 只可能是e2 的左孩子节点。
  5. e2 是头节点,e2 不是e1 的父节点,e1 是e1P 的左孩子节点。
  6. e2 是头节点,e2 不是e1 的父节点,e1 是e1P 的右孩子节点。
  7. e1 和e2 都不是头节点,e1 是e2 的父节点,此时e2 只可能是e1 的右孩子节点,e1 是e1P 的左孩子节点。

我们先来找到e1和e2的父节点

std::vector<TreeNode*> getTwoErrParents(TreeNode* root, TreeNode *e1, TreeNode* e2){
        std::vector<TreeNode*> parents(2, nullptr);
        if(root == nullptr){
            return parents;
        }
        std::stack<TreeNode*> stack;
        TreeNode *prev = nullptr, *curr = root;
        while (!stack.empty() || curr != nullptr){
            if(curr != nullptr){
                stack.push(curr);
                curr = curr->left;
            }else{
                curr = stack.top(); stack.pop();
                if(curr->left == e1 || curr->right == e1){
                    parents[0] = curr;
                }

                if(curr->left == e2 || curr->right == e2){
                    parents[1] = curr;
                }
                curr = curr->right;
            }
        }
        return parents;
    }

递归

用中序遍历树,并将所有的节点都存到一个一维向量中,然后对存节点值的一维向量排序,再将拍好的数组按照顺序赋给节点。

class Solution {

    void inorder(TreeNode* root, std::vector<TreeNode *> &list,  std::vector<int> &vals){
        if(root == NULL){
            return;
        }
        inorder(root->left, list, vals);
        list.push_back(root);
        vals.push_back(root->val);
        inorder(root->right, list, vals);
    }
public:
    void recoverTree(TreeNode* root) {
        std::vector<TreeNode *>list;
        std::vector<int> vals;
        inorder(root, list, vals);
        sort(vals.begin(), vals.end());
        for (int i = 0; i < vals.size(); ++i) {
            list[i]->val = vals[i];
        }
    }
};

这种最一般的解法可以针对任意个数目的节点错乱的情况

Morris 遍历

这道题的真正符合要求的解法应该用的 Morris 遍历,这是一种非递归且不使用栈,空间复杂度为 O(1) 的遍历方法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值