二叉搜索树的后序遍历序列
- 背景
- 题目描述
- 题解
背景
每次重复刷到这题都没有思路,看答案也总需要理解一会,但是下次又忘了,哈哈哈,因此记录一下思路.
题目描述
牛客地址:
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) {// 剩一个节点的时候 返回 trueif (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。
备注:大部分文字转载自牛客精华题解,有兴趣的可以去看一下。