题目描述:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true
,否则返回 false
。假设输入的数组的任意两个数字都互不相同。
示例 1:输入: [1,6,3,2,5] 输出: false
示例 2:输入: [1,3,2,6,5] 输出: true
解题思路:
后序遍历定义: [ 左子树 | 右子树 | 根节点 ] ,即遍历顺序为 “左、右、根” 。
二叉搜索树定义: 左子树中所有节点的值 < 根节点的值;右子树中所有节点的值 > 根节点的值;其左、右子树也分别为二叉搜索树。
方法一:递归分治
根据二叉搜索树的定义,可以通过递归,判断所有子树的 正确性 (即其后序遍历是否满足二叉搜索树的定义) ,若所有子树都正确,则此序列为二叉搜索树的后序遍历。
递归解析:
终止条件: 当 left >= right ,说明此子树节点数量 ≤1 ,无需判别正确性,因此直接返回 true ;
递推工作:
(1)划分左右子树: 遍历后序遍历的 [left,right] 区间元素,寻找 第一个大于根节点 的节点,索引记为 k 。此时,可划分出左子树区间 [left,k-1] 、右子树区间 [k,right-1]、根节点索引 right 。
(2)判断是否为二叉搜索树:
1) 左子树区间 [left,k-1] 内的所有节点都应 < postorder[right] 。而第 1.划分左右子树 步骤已经保证左子树区间的正确性,因此只需要判断右子树区间即可。
2)右子树区间 [k,right-1] 内的所有节点都应 > postorder[right] 。实现方式为遍历,当遇到 ≤postorder[right] 的节点则跳出;则可通过 k == right 判断是否为二叉搜索树。
(3)返回值: 所有子树都需正确才可判定正确,因此使用 与逻辑符 && 连接。
1. k == right: 判断 此树 是否正确。
2. recur(left, k - 1) : 判断 此树的左子树 是否正确。
3. recur(k, right - 1) : 判断 此树的右子树 是否正确。
class Solution {
public:
// 要点:二叉搜索树中根节点的值大于左子树中的任何一个节点的值,小于右子树中任何一个节点的值,子树也是
bool verifyPostorder(vector<int>& postorder)
{
//后序遍历:left -> right -> root
//二叉搜索树:left < root < right
//if(postorder.size() <= 2) return true;
int right = postorder.size() - 1;
return recur(postorder,0,right);
}
// 递归实现
bool recur(vector<int>& tree,int left,int right)
{
//如果left==right,就一个节点不需要判断了,如果left>right说明没有节点,
//也不用再看了,否则就要继续往下判断
if(left >= right) return true;
int k = left;
int rootVal = tree[right]; // 当前树的根节点的值
// 从当前区域找到第一个大于根节点的,说明后续区域数值都在右子树中
while(k < right && tree[k] < rootVal)
{
k++;
}
// 进行判断后续的区域是否所有的值都是大于当前的根节点,如果出现小于的值就直接退出
while(k < right && tree[k] > rootVal)
{
k++;
}
// 当前树遍历完所有节点没问题,就检查左右子树
return k == right && recur(tree,left,k-1) && recur(tree,k,right-1);
}
};
class Solution {
public:
// 要点:二叉搜索树中根节点的值大于左子树中的任何一个节点的值,小于右子树中任何一个节点的值,子树也是
bool verifyPostorder(vector<int>& postorder)
{
//后序遍历:left -> right -> root
//二叉搜索树:left < root < right
if(postorder.size() < 2) return true;
//int right = postorder.size() - 1;
return recur(postorder,0,postorder.size() - 1);
}
// 递归实现
bool recur(vector<int>& tree,int left,int right)
{
//如果left==right,就一个节点不需要判断了,如果left>right说明没有节点,
//也不用再看了,否则就要继续往下判断
if(left >= right) return true;
int k = left;
int rootVal = tree[right]; // 当前树的根节点的值
// 从当前区域找到第一个大于根节点的,说明后续区域数值都在右子树中
while(k < right && tree[k] < rootVal)
{
k++;
}
// 进行判断后续的区域是否所有的值都是大于当前的根节点,如果出现小于的值就直接返回false
for(int i=k;i<right;i++)
{
if(tree[i] < rootVal) return false;
}
// 当前树遍历完所有节点没问题,就检查左右子树
return recur(tree,left,k-1) && recur(tree,k,right-1);
}
};
方法二:辅助单调栈
后序遍历倒序: [ 根节点 | 右子树 | 左子树 ] 。类似 先序遍历的镜像 ,即先序遍历为 “根、左、右” 的顺序,而后序遍历的倒序为 “根、右、左” 顺序。
当遍历时遇到递减节点 r_i < r_{i+1} ,若为二叉搜索树,则对于后序遍历中节点 r_i右边的任意节点 ,必有节点值 <root。
遍历 “后序遍历的倒序” 会多次遇到递减节点 r_i,若所有的递减节点 r_i 对应的父节点 root都满足以上条件,则可判定为二叉搜索树。
根据以上特点,考虑借助 单调栈 实现:
1、借助一个单调栈 stackstack 存储值递增的节点;
2、每当遇到值递减的节点 r_i ,则通过出栈来更新节点 r_i 的父节点 rootroot ;
3、每轮判断 r_i 和 root 的值关系:
(1)若 r_i > root 则说明不满足二叉搜索树定义,直接返回 false。
(2)若 r_i < root 则说明满足二叉搜索树定义,则继续遍历。
算法流程:
1、初始化: 单调栈 stack ,父节点值 root = +∞ (初始值为正无穷大,可把树的根节点看为此无穷大节点的左孩子);
2、倒序遍历 postorder :记每个节点为 r_i
(1)判断: 若 r_i > root ,说明此后序遍历序列不满足二叉搜索树定义,直接返回 false;
(2)更新父节点 root: 当栈不为空 且 r_i < stack.top() 时,循环执行出栈,并将出栈节点赋给 root。
(3)入栈: 将当前节点 r_i 入栈;
3、若遍历完成,则说明后序遍历满足二叉搜索树定义,返回 true 。
翻转先序遍历又是root->right->left的,基于这样的性质和遍历方式,我们知道越往右越大,这样,就可以构造一个单调递增的栈,来记录遍历的元素。
为什么要用单调栈呢,因为往右子树遍历的过程,value是越来越大的,一旦出现了value小于栈顶元素value的时候,就表示要开始进入左子树了(如果不是,就应该继续进入右子树,否则不满足二叉搜索树的定义,不理解的请看下二叉搜索树的定义),但是这个左子树是从哪个节点开始的呢?
单调栈帮我们记录了这些节点,只要栈顶元素还比当前节点大,就表示还是右子树,要移除,因为我们要找到这个左孩子节点直接连接的父节点,也就是找到这个子树的根,只要栈顶元素还大于当前节点,就要一直弹出,直到栈顶元素小于节点,或者栈为空。栈顶的上一个元素就是子树节点的根。
接下来,数组继续往前遍历,之后的左子树的每个节点,都要比子树的根要小,才能满足二叉搜索树,否则就不是二叉搜索树。
class Solution {
public:
bool verifyPostorder(vector<int>& postorder)
{
// 单调栈使用,单调递增的单调栈
stack<int> stk;
int root = INT_MAX;
// 逆向遍历,就是翻转的先序遍历
for(int i = postorder.size() - 1 ; i >= 0 ; i--)
{
// 左子树元素必须要小于递增栈被top访问的元素,否则就不是二叉搜索树
if(postorder[i] > root) return false;
while(!stk.empty() && stk.top() > postorder[i])
{
// 数组元素小于单调栈的元素了,表示往左子树走了,记录下上个根节点
// 找到这个左子树对应的根节点,之前右子树全部弹出,不再记录,因为不可能在往根节点的右子树走了
root = stk.top();
stk.pop();
}
// 这个新元素入栈
stk.push(postorder[i]);
}
return true;
}
};