27

剑指offer 33——二叉搜索树的后序遍历序列

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU0NTk3NDMyMQ%3D%3D&%3Bmid=2247484161&%3Bidx=1&%3Bsn=033b423f117b27b08755391e3b3ecfdf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本题主要在于考察对二叉搜索树和后序遍历的理解。

原题

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

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

5
    / \
   2   6
  / \
 1   3

示例 1:

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

示例 2:

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

提示:

  • 数组长度 <= 1000

原题url:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/

解题

基本概念

首先介绍一些基本概念,方便后续做题。

  1. 后序遍历:[ 左子树 | 右子树 | 根节点 ] ,即遍历顺序为 “左、右、根” 。

  2. 二叉搜索树:左子树中所有节点的值 < 根节点的值;右子树中所有节点的值 > 根节点的值;其左、右子树也分别为二叉搜索树。

递归分治

既然本题只提供了后序遍历,那么我们就要在此基础之上下功夫了。

根据上面的提供的说明,后序遍历是,先左右子树再根节点,那么根是容易判断的,肯定在整个序列的最后。

而二叉搜索树节点值满足,左子树 < 根 < 右子树,因此我们就可以以根为基础,从后向前遍历,。

一开始的节点肯定都大于根,因为都在右子树上,一旦出现小于根的节点,说明就进入了左子树,那么之后所有的节点都应该小于根。然后再分别遍历左右子树,直至到叶子节点为止(即无左右子树的节点)。

按照上面的方法,就需要我们将后序遍历分成左右子树,不断递归遍历检查。

接下来看看代码:

class Solution {
    public boolean verifyPostorder(int[] postorder) {
        return checkTree(postorder, 0, postorder.length - 1);
    }

    private boolean checkTree(int[] postorder, int start, int end) {
        // 如果start >= end,说明已经寻找结束
        if (start >= end) {
            return true;
        }

        // 找到根
        int root = postorder[end];
        // 左子树开始的下标
        int leftStart = start;
        // 左子树结束的下标
        int leftEnd = leftStart;
                // 找到第一个大于根节点的值
        while (leftEnd < end && postorder[leftEnd] < root) {
            leftEnd++;
        }
        leftEnd--;

        // 右子树开始的下标
        int rightStart = leftEnd + 1;
        // 右子树结束的下标
        int rightEnd = end - 1;
        // 检查右子树是否都大于根节点
        for (int i = rightStart; i < end; i++) {
            if (postorder[i] > root) {
                continue;
            }

            return false;
        }

        // 继续检查左右子树
        return checkTree(postorder, leftStart, leftEnd) &&
                checkTree(postorder, rightStart, rightEnd);
    }
}

提交OK。

分析一下复杂度:

  • 时间复杂度 O(N^2) :每次调用 checkTree 方法减去一个根节点,因此递归占用 O(N) ;最差情况下(即当树退化为链表),每轮递归都需遍历树所有节点,占用 O(N ^ 2) 。

  • 空间复杂度 O(N) :最差情况下(即当树退化为链表),递归深度将达到 N 。

递增栈

既然上面分析出时间复杂度为 O(N^2) ,那么是否可以找到一种更高效的方法,只遍历一次序列,就可以解决问题呢?因为这样可以在时间复杂度上进行很大的优化。

这就需要再进一步结合搜索二叉树和后序遍历的特性了。(这个方法我是在网上看到的,感觉属于一种比较偏门的优化,一般很难出这种方法)

在我们从后向前遍历序列时,大致是经历了 根、右子树、左子树 ,而 左子树 < 根 < 右子树 ,那么一开始应该是单调递增的,我们可以将这些节点依次入栈。

当不满足 单调递增 调试时,一般是碰到了右子树中某一个左子树节点,或者真正的左子树,这时候可以将栈顶元素出栈,直到碰到比当前节点小的元素,那么将最后的栈顶元素设为 根节点

此时继续遍历,应该保证所有节点都小于根节点,因为此时已经进入左子树序列了。否则说明该序列不满足搜索二叉树的后序遍历。

重复以上步骤,如果遍历结束,说明满足搜索二叉树的后序遍历。

这么说可能比较难懂,直接上代码:

class Solution {
    public boolean verifyPostorder(int[] postorder) {
        // 单调递增栈
        Stack<Integer> stack = new Stack<>();
        int root = Integer.MAX_VALUE;
        // 倒序遍历
        for (int i = postorder.length - 1; i >= 0; i--) {
            if (postorder[i] > root) {
                return false;
            }
            // 如果当前栈不为空,且当前遍历的节点小于栈顶节点
            while (!stack.isEmpty() &&
                postorder[i] < stack.peek()) {
                // 栈顶节点压出,且更新根节点
                root = stack.pop();
            }
            // 当前节点入栈
            stack.push(postorder[i]);
        }
        return true;
    }
}

提交OK。

分析一下复杂度:

  • 时间复杂度 O(N) :遍历 postorder 所有节点,各节点均入栈 / 出栈一次,使用 O(N) 时间。

  • 空间复杂度 O(N) :最差情况下(即当树退化为链表),单调递增栈 stack 存储所有节点。

神奇的是,力扣给出的执行结果显示:递归分治方法消耗的时间更短。这点大家也可以研究研究是为什么。

总结

以上就是这道题目我的解答过程了,不知道大家是否理解了。本题主要在于考察对二叉搜索树和后序遍历的理解,递归分治是容易想出来的方法,但是后面那种单调递增栈确实很难想到,可以作为一种特殊思路进行理解。

有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。

https://death00.github.io/

公众号:健程之道

ziMRjij.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK