剑指 Offer 33. 二叉搜索树的后序遍历序列

127 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情

Leetcode : leetcode-cn.com/problems/er…

GitHub : github.com/nateshao/le…

/**
 * 栈
 * @param postorder
 * @return
 */
public boolean verifyPostorder(int[] postorder) {
    // 单调栈使用,单调递增的单调栈
    Deque<Integer> stack = new LinkedList<>();
    int pervElem = Integer.MAX_VALUE;
    // 逆向遍历,就是翻转的先序遍历
    for (int i = postorder.length - 1; i >= 0; i--) {
        // 左子树元素必须要小于递增栈被peek访问的元素,否则就不是二叉搜索树
        if (postorder[i] > pervElem) {
            return false;
        }
        while (!stack.isEmpty() && postorder[i] < stack.peek()) {
            // 数组元素小于单调栈的元素了,表示往左子树走了,记录下上个根节点
            // 找到这个左子树对应的根节点,之前右子树全部弹出,不再记录,因为不可能在往根节点的右子树走了
            pervElem = stack.pop();
        }
        // 这个新元素入栈
        stack.push(postorder[i]);
    }
    return true;
}

剑指 Offer 26. 二叉搜索树的后序遍历序列

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

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

       5
      / \
     2   6
   / \
1   3

示例 1:

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

示例 2:

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

解题思路:

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

  • 二叉搜索树定义:

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

方法一:递归

Go

package main

func main() {

}
func verifyPostorder(postorder []int) bool {
   if len(postorder) < 2 {
      return true
   }

   index := len(postorder) - 1   // 区分左右子树:左子树上的值全都比根节点小,右子树上的值全都比根节点大
   rootValue := postorder[index] // 用来记录根节点的值

   for k, v := range postorder {
      // 当出现第一个大于根节点的值时,这个值往后全是右子树
      if index == len(postorder)-1 && v > rootValue {
         index = k
      }
      // 在右子树中出现小于根节点的值时,则该树不是二叉搜索树
      if index != len(postorder)-1 && rootValue > v {
         return false
      }
   }
   return verifyPostorder(postorder[:index]) && verifyPostorder(postorder[index:len(postorder)-1])
}

image.png

  • 根据二叉搜索树的定义,可以通过递归,判断所有子树的 正确性 (即其后序遍历是否满足二叉搜索树的定义) ,若所有子树都正确,则此序列为二叉搜索树的后序遍历。
递归解析:
  • 终止条件: 当 i ≥ j,说明此子树节点数量 ≤ 1 ,无需判别正确性,因此直接返回 true ;

  • 递推工作:

    1. 划分左右子树:遍历后序遍历的[i,j] 区间元素,寻找第一个大于根节点的节点,索引记为m 。此时,可划分出左子树区间[i,m- 1]、 右子树区间[m,j- 1]、 根节点索引 j。
    2. 判断是否为二叉搜索树:
      • 左子树区间[i, m- 1]内的所有节点都应< postorder[j]。 而第1. 划分左右子树步骤已经保证左子树区间的正确性,因此只需要判断右子树区间即可。
      • 右子树区间[m,j- 1]内的所有节点都应> postorder[j] 。实现方式为遍历,当遇到≤ postorder[j] 的节点则跳出;则可通过 p = j 判断是否为二叉搜索树。
  • 返回值:所有子树都需正确才可判定正确,因此使用 与逻辑符 &&连接。

    1. p=j :判断此树否正确。
    2. recur(i,m- 1) :判断 此树的左子树 否正确。
    3. recur(m,j-1) :判断 此树的右子树 是否正确。

复杂度分析:

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

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

package com.nateshao.sword_offer.topic_26_verifyPostorder;

import java.util.Stack;

/**
 * @date Created by 邵桐杰 on 2021/11/30 14:22
 * @微信公众号 千羽的编程时光
 * @个人网站 www.nateshao.cn
 * @博客 https://nateshao.gitee.io
 * @GitHub https://github.com/nateshao
 * @Gitee https://gitee.com/nateshao
 * Description:
 */
public class Solution {
    public static void main(String[] args) {
        int[] postorder = {1, 3, 2, 6, 5};
        int[] postorder2 = {1, 6, 3, 2, 5};// false
        int[] postorder3 = {1, 6, 3, 2, 5};// false
        System.out.println("递归:(postorder) = " + verifyPostorder(postorder));
        System.out.println("递归:(postorder2) = " + verifyPostorder(postorder2));
    }
    
    public static boolean verifyPostorder(int[] postorder) {
        return recur(postorder, 0, postorder.length - 1);
    }

    public static boolean recur(int[] postorder, int i, int j) {
        if (i >= j) return true;
        int p = i;
        while (postorder[p] < postorder[j]) p++;
        int m = p;
        while (postorder[p] > postorder[j]) p++;
        return p == j && recur(postorder, i, m - 1) && recur(postorder, m, j - 1);
    }
    /**
     * 递归:(postorder) = true
     * 递归:(postorder2) = false
     */
}

方法二:栈

后序遍历 倒序: [ 根节点 | 右子树 | 左子树 ] 。类似 先序遍历的镜像 ,即先序遍历为 “根、左、右” 的顺序,而后序遍历的倒序为 “根、右、左” 顺序。

算法流程:

  1. 初始化: 单调栈stack,父节点值root = +∞(初始值为正无穷大, 可把树的根节点看为此无穷大节点的左孩子) ;
  2. 倒序遍历postorder: 记每个节点为ri;
    1. 判断: 若ri > root,说明此后序遍历序列不满足一 叉搜索树定义,直接返回false ;
    2. 更新父节点root: 当栈不为空 且ri < stack:peek()时,循环执行出栈,并将出栈节点赋给root
    3. 入栈: 将当前节点ri入栈;
  3. 若遍历完成,则说明后序遍历满足二叉 搜索树定义,返回true。

package com.nateshao.sword_offer.topic_26_verifyPostorder;

import java.util.Stack;

/**
 * @date Created by 邵桐杰 on 2021/11/30 14:22
 * @微信公众号 千羽的编程时光
 * @个人网站 www.nateshao.cn
 * @博客 https://nateshao.gitee.io
 * @GitHub https://github.com/nateshao
 * @Gitee https://gitee.com/nateshao
 * Description: 二叉搜索树的后序遍历序列
 */
public class Solution {
    public static void main(String[] args) {
        int[] postorder3 = {1, 6, 3, 2, 5};// false
        System.out.println("栈:(postorder3) = " + verifyPostorder2(postorder3));
    }

    /**
     * 方法二:栈
     *
     * @param postorder
     * @return
     */
    public static boolean verifyPostorder2(int[] postorder) {
        Stack<Integer> stack = new Stack<>();
        //初始化: 单调栈 stackstack ,父节点值 root = +∞ (初始值为正无穷大,可把树的根节点看为此无穷大节点的左孩子);
        int root = Integer.MAX_VALUE;
        // 倒序遍历 postorder:记每个节点为 i
        for (int i = postorder.length - 1; i >= 0; i--) {
            // 若 ri>root,说明此后序遍历序列不满足二叉搜索树定义,直接返回 false ;
            if (postorder[i] > root) return false;
            // 更新父节点root:当栈不为空且 stack.peek()>ri时,循环执行出栈,并将出栈节点赋给 root
            while (!stack.isEmpty() && stack.peek() > postorder[i])
                root = stack.pop();
            stack.add(postorder[i]); // 入栈: 将当前节点 ri
        }
        return true; //遍历完成,则说明后序遍历满足二叉搜索树定义,返回 true 。
    }
    /**
     * 递归:(postorder) = true
     * 递归:(postorder2) = false
     * 栈:(postorder3) = false
     */
}

参考链接:leetcode-cn.com/problems/er…