一、题目
题目链接:力扣
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 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,并且只有二叉搜索树才具有该性质】
- 其实,直接使用后序遍历序列也可以,不过不好理解。
- (根右左后续倒序遍历)当前节点比上一个结点大(r_i > r_i+1)。当前结点一定是上一个结点的右子结点,我们需要继续 压栈。/ 因为比r_i+1大的肯定都是他的右子树节点,如果还是挨着他的,肯定是在后续遍历中所有的右子节点最后一个遍历的,所以r_i 一定是r_i+1的右子节点。
- (根右左后续倒序遍历)当前节点比上一个元素小(r_i < r_i+1)。当前结点r_i 一定是某一结点root的左孩子(开始进入root的左子树,r_i+1是root的右子树),其中root为已遍历过的逆序序列中大于当前结点且最接近(大于当前结点的第一个结点)当前结点的结点。
- 当遍历时遇到递减节点 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。
根据以上特点,考虑借助 单调栈 实现:
- 借助一个单调栈 stack 存储值递增的节点;
- 每当遇到值递减的节点 r_i ,则通过出栈来更新节点 r_i 的父节点 root (出栈是用来更新父节点的);
- 每轮判断 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、运行结果
🦔 递归 + 分治
🦔 单调栈