剑指 Offer 33. 二叉搜索树的后序遍历序列

一、题目 

题目链接:力扣

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

参考以下这颗二叉搜索树:

      5
     / \
   2   6
  / \
 1   3

示例 1:

输入: [1,6,3,2,5]
输出: false

示例 2:

输入: [1,3,2,6,5]
输出: true

提示:

数组长度 <= 1000


二、题解

1、思路

🦔 递归 + 分治

如果这题说的是判断该数组是不是某二叉搜索树的中序遍历结果,那么这道题就非常简单了,因为二叉搜索树的中序遍历结果一定是有序的,我们只需要判断数组是否有序就行了。但这道题要判断的是不是某二叉搜索树的后序遍历结果,这样就有点难办了。

二叉搜索树的特点是左子树的值<根节点<右子树的值。而后续遍历的顺序是:

左子节点→右子节点→根节点;

那么后续遍历的结果应该是:[ 左子树 | 右子树 | 根节点 ]

对于搜索树来说:

给出后序遍历,首先可以确定根节点,然后根据搜索树的定义,可以得到后序遍历中根节点的左子树和右子树位置(左子树全部小于根节点的值,右子树全部大于根节点的值)。

比如下面这棵二叉树,他的后续遍历是:[3,5,4,10,12,9]。

我们知道后续遍历的最后一个数字一定是根节点,所以数组中最后一个数字9就是根节点,我们从前往后找到第一个比9大的数字10,那么10后面的[10,12](除了9)都是9的右子节点,10前面的[3,5,4]都是9的左子节点,后面的需要判断一下,如果有小于9的,说明不是二叉搜索树,直接返回false。然后再以递归的方式判断左右子树。

🦔 单调栈

后序遍历:[ 左子树 | 右子树 | 根节点 ] 。

后序遍历倒序: [ 根节点 | 右子树 | 左子树 ] 。后序遍历的倒序就是将后序遍历的结果倒过来。类似 先序遍历的镜像。

主要思想:

  • 后序遍历倒序为先序遍历镜像,因此利用后序遍历倒序【先根、后右子树、最后左子树】的特点。
  • 从根节点开始遍历,首先遍历右子树,右子树遍历的过程,value是越来越大的,一旦出现了value小于栈顶元素value的时候,就表示要开始进入左子树了。
  • 进入左子树,找到左子树的根节点root,那么后序逆序未遍历过的所有元素都应该小于根节点root。【因为未遍历过的元素,要么是 root 的左子树节点(小于root),要么是root某个父节点的左子树节点,这两种情况的value都小于root,并且只有二叉搜索树才具有该性质】
  • 其实,直接使用后序遍历序列也可以,不过不好理解。

  1. (根右左后续倒序遍历)当前节点比上一个结点大(r_i  > r_i+1)。当前结点一定是上一个结点的右子结点,我们需要继续 压栈。/ 因为比r_i+1大的肯定都是他的右子树节点,如果还是挨着他的,肯定是在后续遍历中所有的右子节点最后一个遍历的,所以r_i 一定是r_i+1的右子节点。

  2. (根右左后续倒序遍历)当前节点比上一个元素小(r_i  < r_i+1)。当前结点r_i 一定是某一结点root的左孩子(开始进入root的左子树,r_i+1是root的右子树),其中root为已遍历过的逆序序列中大于当前结点且最接近(大于当前结点的第一个结点)当前结点的结点。

  3. 当遍历时遇到递减节点 r_i < r_i+1(上面第二种情况),若为二叉搜索树,则对于后序遍历节点 r_i 右边的任意节点(后序逆序未遍历过) r_x ∈ [r_i-1, r_i-2, …, r_1],必有节点值 r_x < root。因为,如果是二叉搜索树,节点 r_x 只可能为以下两种情况,且以下两种情况都满足r_x < root,具体情况如下图所示:
  • ① r_x 为 root 的左子树各节点;
  • ② r_x 为 root 的父节点或更高层父节点的左子树的各节点。
  • 在二叉搜索树中,以上两种情况 r_x 节点都应小于 root;
  • 注意:root为r_i 最近的父节点。

遍历 “后序遍历的倒序” 会多次遇到递减节点 r_i,若所有的递减节点 r_i,对应的父节点 root 都满足以上条件,则可判定为二叉搜索树。。。。假设不是搜索二叉树,那么可能后序逆序序列存在递增序列,但是遇到递减结点 r_i 后,不能保证 r_i 的父节点满足 r_x < root。

根据以上特点,考虑借助 单调栈 实现:

  1. 借助一个单调栈 stack 存储值递增的节点;
  2. 每当遇到值递减的节点 r_i  ,则通过出栈来更新节点 r_i 的父节点 root (出栈是用来更新父节点的);
  3. 每轮判断 r_i 和 root 的值关系:
  • 若 r_i > root 则说明不满足二叉搜索树定义,直接返回 false 。
  • 若 r_i < root 则说明满足二叉搜索树定义,则继续遍历。

巨难理解,敲!

2、代码实现

🦔 递归 + 分治

这里的递归说的是树,而不是数组,站在树的角度去理解。 

class Solution {
public:
    bool verifyPostorder(vector<int>& postorder) {
        if(postorder.size() == 0)return true;
        return recur(postorder, 0, postorder.size() - 1);
    }

private:
    // 该函数主要作用判断当前数组是否是搜索二叉树遍历的结果
    // 先判断根节点所在树,递归判断左右子树
    bool recur(vector<int>& postorder, int left, int right)
    {
        // 截止条件
        if(left >= right)return true;// left = right时,叶子节点肯定是,没必要再往下执行,但此处即使是>也不会出错

        // 找第一个最大
        int first_big = left;
        while(postorder[first_big] < postorder[right] && first_big < right)first_big++;

        // 找最后一个最大的下一个位置
        int last_big = first_big;
        while(postorder[last_big] > postorder[right] && last_big < right)last_big++;

        // 判断是否first_big后全部都大于postorder[right]
        int cur_status = false;
        if(last_big == right)cur_status = true;

        // 递归判读左子树、右子树后续遍历序列是否满足条件
        bool r_status = recur(postorder, first_big, right - 1);
        bool l_status = recur(postorder, left, first_big - 1);
        
        // 如果当前结点、左子树、右子树都满足条件
        return cur_status && l_status && r_status;
    }
};

🦔 单调栈

class Solution {
public:
    bool verifyPostorder(vector<int>& postorder) {
        stack<int> stk;
        int root = INT_MAX;
        for(auto i = postorder.rbegin(); i < postorder.rend(); ++i)
        {
            // 一个root会与多个*i判断,这里可以看K神的动画
            // 而且root越来越小,所以虽然后面的root没和当前root比较
            // 但是和更小的root进行了比较,满足r_x < root这个条件
            if(*i > root)return false;
            while(!stk.empty() && *i < stk.top())// 栈为空判断一定要放在前面,不然会访问空栈
            {
                root = stk.top();// 只要栈顶元素比*i大,那么就更新root并弹栈,最终root应该是栈中大于*i的第一个元素
                stk.pop();
            }
            stk.push(*i);
        }
        return true;
    }
};

第二次写的,可能更好理解一点:

class Solution {
public:
    bool verifyPostorder(vector<int>& postorder) {
        stack<int> stk;
        int root = INT_MAX;
        for(auto i = postorder.rbegin(); i < postorder.rend(); i++)
        {
            // 判断是否是二叉搜索树
            if(*i > root)return false;

            // 如果遍历右子树,压栈
            if(stk.empty() || *i > stk.top())stk.push(*i);
            else// 如果遍历左子树,弹栈更新root,
            {
                while(!stk.empty() && *i < stk.top())
                {
                    root = stk.top();
                    stk.pop();
                }
            }
        }
        return true;
    }
};

3、复杂度分析

🦔 递归 + 分治

时间复杂度:O(n2);

空间复杂度:O(n)。

🦔 单调栈

时间复杂度:O(n);

空间复杂度:O(n)。

4、运行结果

🦔 递归 + 分治

🦔 单调栈 


三、参考内容

力扣

力扣

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kashine

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值