二叉搜索树的后序遍历序列

二叉搜索树的后序遍历序列

背景

每次重复刷到这题都没有思路,看答案也总需要理解一会,但是下次又忘了,哈哈哈,因此记录一下思路.

题目描述

牛客地址:

https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd

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

数据范围:
节点数量 0≤n≤1000 ,节点上的值满足 1≤val≤105,保证节点上的值各不相同
要求:
空间复杂度 O(n) ,时间时间复杂度 O(n2)
提示:
1.二叉搜索树是指父亲节点大于左子树中的全部节点,但是小于右子树中的全部节点的树。
2.该题我们约定空树不是二叉搜索树
3.后序遍历是指按照 “左子树-右子树-根节点” 的顺序遍历
4.参考下面的二叉搜索树,
示例 1
在这里插入图片描述
示例1
输入:[1,3,2]
返回值:true
说明:是上图的后序遍历 ,返回true
示例2
输入:[3,1,2]
返回值:false
说明:
不属于上图的后序遍历,从另外的二叉搜索树也不能后序遍历出该序列 ,因为最后的2一定是根节点,前面一定是孩子节点,可能是左孩子,右孩子,根节点,也可能是全左孩子,根节点,也可能是全右孩子,根节点,但是[3,1,2]的组合都不能满足这些情况,故返回false
示例3
输入:[5,7,6,9,11,10,8]
返回值:true

题解

思路:
首先根据题意我们需要需要想到两点:
第一点:题干给的是后序遍历的排序数组,二叉树的后序遍历顺序 是 左结点-右结点-根结点,因此,最后一个结点一定是根结点,这点很重要.
第二点:题干给的是二叉搜索树,二叉搜索树的特点是,根结点的左子树的所有结点一定比根结点小,根结点的右子树的所有结点一定根结点大.

通过这两个特点,然后来分析给出的后序遍历数组,如果我们能想到,利用某种方式来验证二叉树的左子树一定比根结点小,二叉树的右子树一定比根结点大.那么这个数组就符合要求,反之则不符合.

通过这个思路,我们首先肯定需要找到根结点,然后才能进行左右子树的判断.因为是后序遍历,所以数组的最后一位肯定是根结点.根结点找到了,我们还需要找到左右子树,找到左右子树后,我们再对左右子树进行验证即可.下面讨论两种寻找左右子树的方法.

我们先举个例子,比如给出树 [1,3,2,5,4]
在这里插入图片描述
那么数组的最后一位,也就是4是树的根结点,接下来我们验证左子树[1,3,2]和右子树[5]是否满足要求即可.但是到这里还没结束,我们还需要对2这个左子树的子结点进行验证.如果子结点后面还有子结点,我们可能还需要继续进行验证.由此我们可能有了一种思路,递归.

方法一:递归
我们首先根据最后一位4,可以把前面的[1,3,2,5]分成两组,比4小的左子树[1,3,2],比4大的右子树[5],接着再进行验证左子树的元素是否都比4小,右子树的元素是否都比4大.如果有不满足的,则验证失败.

验证完后,我们还需要对两个子树进行递归验证,右子树[5]已经没有子树了,因此满足,要求,左子树[1,3,2]又可以以根结点2来进行划分,且它的左子树为[1],右子树为[3].划分完后继续验证,很明显都满都要求.接下来我们上代码:

import java.util.*;
public class Solution {
    public boolean VerifySquenceOfBST(int[] sequence) {
        if (sequence.length == 0) return false;
        return order(sequence, 0, sequence.length - 1);
    }

    public boolean order(int[] sequence, int start, int end) {
        // 剩一个节点的时候 返回 true
        if (start >= end) return true;
        int j;
        //根结点一定在最后一个
        int root = sequence[end];

        //且左子树一定在剩下结点的左半部分,右子树一定在剩下结点的右半部分
        // 从后往前找到左子树和右子树的分界点,找到比root小的第一个即为分界点
        for (j = end; j >= start; j--) {
            int cur = sequence[j];
            //当找到小于根的时候表明找到了分界点了
            if (cur < root) break;
        }

        //找到分界点后,从分界点开始,判断所谓的左子树中是否有不合法(不符合二叉搜索树)的元素
        //这里只需要验证左子树,因为我们是从后往前找比root小的,如果右子树存在比root小的,那么
        //我们找的分界点会出错,也会导致最终验证不通过.
        for (int i = j; i >= start; i--) {
            int cur = sequence[i];
            if (cur > root) return false;
        }
        return order(sequence, start, j) && order(sequence, j + 1, end - 1);
    }
}

算法中有一点需要我们格外注意的,就是每次递归方法中我们只验证了左子树,没有验证右子树.这是为什么呢?
因为我们是从后往前找比root小的分界点,如果右子树存在比root小的,那么我们找的分界点会出错,也会导致最终验证不通过.

时间复杂度:𝑂(𝑛2),递归操作每次排除掉一个根节点,因此递归次数是O(n),而最差情况下树退化成链表,每次递归内又要遍历所有节点,还是O(n),因此最终代价就是O(n2)
空间复杂度:O(n),最差情况下树退化成链表,递归深度就是O(n)

方法二:逆向后序遍历+单调栈
相比于上面一种解法,这种解效率更高,但是理解更困难.
我们都知道,从后往前遍历,即顺序变成了根->右子树->左子树。
由于右子树>根>左子树,所以遍历序列过程有下降时,说明当前已经来到了左子树,利用单调栈找到大于当前值的最小值,该值即为局部树中的根节点。
初始时,令根节点root无穷大,则当前树为该根节点的左子树。遍历过程中,逐步缩小root的值,因为所有的操作(主要就是对结点是否合规的判断)都是在当前根节点root的左子树中进行的,所以保证遍历的值小于root即可满足判断条件,否则为false;
代码入下:

import java.util.Stack;
public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        // 处理序列为空情况
        if(sequence.length == 0) return false;
        //首先定义根结点为最大值,这说数组组成的树是root的左子树.
        int root = Integer.MAX_VALUE;
        Stack<Integer> stack = new Stack<>();
        // 以根,右子树,左子树顺序遍历
        for(int i= sequence.length-1;i>=0;i--){
         //这里判断的sequence[i]一定是左子结点,因为如果是右子结点就不会去更新root,由下面的stack.peek()>sequence[i]保证.
         //且一开始数组组成的树它就是左子树,因此可以直接判断.
         //所以确定根后一定是在右子树节点都遍历完了,因此当前sequence未遍历的节点中只含左子树,左子树的节点如果>root则说明违背二叉搜索的性.
         //这里不用判断整个左子树,如果是左子树的左子树,后面会利用单调栈来重新确定root,然后再循环判断.
            if(sequence[i]>root) return false;
            // 进入左子树的契机就是sequence[i]的值小于前一项的时候,这时可以确定root
            //且会循环的出栈取root,直到找到比root还小的值停止,那么上一个值一定是sequence[i]的直接根结点
            while(!stack.isEmpty() && stack.peek()>sequence[i]){
                 //找到根结点后,就能确定sequence[i]的直接根结点了.
                 //且sequence[i-1]也一定是root的左子结点.
                root = stack.pop();
            }
            //每个数字都要进一次栈,且进栈的顺序一定是单调递增的,因为进栈的都是栈底结点的右子结点
            //因为左子结点需要找自己的直接根结点,找根结点的过程中会出栈.
            //所以这里用单调栈的意义就是为了保存右子结点,为后续左子结点找根结点提供方式.
            stack.add(sequence[i]);
        }
        return true;
    }
}

复杂度分析:
时间复杂度:O(n),遍历数组序列。
空间复杂度:O(n),入栈最大数目n。

备注:大部分文字转载自牛客精华题解,有兴趣的可以去看一下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值