牛客题解 | 二叉搜索树的后序遍历序列

题目

题目链接

题目的主要信息:
  • 题目给出我们一个一维数组sequence
  • 该数组需要我们判定数组sequence中的元素是否符合一个二叉搜索树的后序遍历顺序
  • 如果该数组sequence可以是一种二叉搜索树的后序遍历顺序,则返回true
  • 如果该数组sequence非二叉搜索树的后序遍历顺序,则返回false
举一反三:

学习完本题的思路你可以解决如下题目:

单调栈相关

JZ9. 用两个栈实现队列

JZ30. 包含min函数的栈

JZ31. 栈的压入、弹出序列

后序遍历相关

JZ37. 序列化二叉树

方法一:单调栈(推荐使用)

知识点:栈

栈是一种仅支持在表尾进行插入和删除操作的线性表,这一端被称为栈顶,另一端被称为栈底。元素入栈指的是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;元素出栈指的是从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

思路:单调栈
首先我们需要按照题目给出列表的逆序进行遍历,这样我们的遍历视角转成了 [ 根 − 右子树 − 左子树 ] [根-右子树-左子树] [右子树左子树]。假定原输入列表的顺序为 [ t n , t n 1 , . . . , t 2 , t 1 ] [t_n, t_{n_1},...,t_2, t1] [tn,tn1,...,t2,t1],则该逆序后列表内容 [ t 0 , t 1 , t 2 , . . . , t n ] [t_0, t_1, t_2,...,t_n] [t0,t1,t2,...,tn]有如下特性:

  1. t i < t i + 1 t_i < t_{i+1} ti<ti+1时,说明节点 t i + 1 t_{i+1} ti+1 t i t_i ti的右子节点
  2. t i > t i + 1 t_i > t_{i+1} ti>ti+1时,说明节点 t i + 1 t_{i+1} ti+1是已经遍历过的所有节点中比 t i + 1 t_{i+1} ti+1大的且最接近 t i + 1 t_{i+1} ti+1的节点的左子节点,设 t i + 1 t_{i+1} ti+1对应的该父节点为 r o o t root root同时要满足 t i + 1 , t i + 2 , . . . , t n t_{i+1},t_{i+2},...,t_n ti+1,ti+2,...,tn都要比 r o o t root root
  3. 根据我们推理的以上性质(加粗部分),用单调栈工具进行操作,如果能找到不符合以上性质的矛盾点,则返回false,如果上述性质都满足,则返回true

具体做法:

  • step 1:首先处理特殊情况,sequence为空的情况,返回false
  • step 2:维护一个单调栈s,不断迭代一个根节点root(初始化为INT_MAX
  • step 3:通过循环反向遍历给定的列表,模拟逆序操作
  • step 4:循环内,如果当前遍历节点大于root,则返回false
  • step 5:如果当前节点小于栈顶节点且栈非空,则内循环更新root,并不断退栈,这一过程在找到当前节点的父节点
  • step 6:内循环结束后,将数字进入单调栈

图示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Java实现代码:

import java.util.*;
public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        // 处理序列为空情况
        if(sequence.length == 0) return false;
        Stack<Integer> s = new Stack<>();
        int root = Integer.MAX_VALUE;
        // 以根,右子树,左子树顺序遍历
        for(int i = sequence.length - 1; i >= 0; i--) {
            // 确定根后一定是在右子树节点都遍历完了,因此当前sequence未遍历的节点中只含左子树,左子树的节点如果>root则说明违背二叉搜索的性质
            if(sequence[i] > root) return false;
            // 进入左子树的契机就是sequence[i]的值小于前一项的时候,这时可以确定root
            while(!s.isEmpty() && s.peek() > sequence[i]) {
                root = s.pop();
            }
            // 每个数字都要进一次栈
            s.add(sequence[i]);
        }
        return true;
    }
}

C++实现代码:

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        // 处理序列为空情况
        if(sequence.size() == 0) return false;
        
        stack<int> s;
        int root = INT_MAX;
        // 以根,右子树,左子树顺序遍历
        for(int i = sequence.size() - 1; i >= 0; i--) {
            // 确定根后一定是在右子树节点都遍历完了,因此当前sequence未遍历的节点中只含左子树,左子树的节点如果>root则说明违背二叉搜索的性质
            if(sequence[i] > root) return false;
            // 进入左子树的契机就是sequence[i]的值小于前一项的时候,这时可以确定root
            while(!s.empty() && s.top() > sequence[i]) {
                root = s.top();
                s.pop();
            }
            // 每个数字都要进一次栈
            s.push(sequence[i]);
        }
        return true;
    }
};

Python实现代码:

class Solution:
    def VerifySquenceOfBST(self , sequence: List[int]) -> bool:
        # 特殊情况处理
        if not sequence: return False
        # 初始化
        s, root = [], 999999
        # 倒序遍历
        for i in range(len(sequence) -1, -1, -1):
            # 存在左子树大于父节点root的情况,违背二叉搜索树性质
            if sequence[i] > root: return False;
            # 找当前遍历节点的父节点
            while s and sequence[i] < s[-1]:
                root = s.pop()
            s.append(sequence[i])
        return True

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) n n n代表sequence长度,各个节点均入栈、 出栈一次,因此时间代价和序列长度呈线性关系
  • 空间复杂度: O ( n ) O(n) O(n),最差情况下单调栈存储所有节点,使用 O ( n ) O(n) O(n)空间
方法二:(扩展思路)

知识点:二叉树递归

递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。

而二叉树的递归,则是将某个节点的左子树、右子树看成一颗完整的树,那么对于子树的访问或者操作就是对于原树的访问或者操作的子问题,因此可以自我调用函数不断进入子树。

思路:

递归函数的参数包含三个信息:遍历序列,树的起点索引位置,树的重点索引位置。

对于后序遍历的二叉搜索树来讲,终点位置就是根,然后从后往前遍历找到的第一个小于终点位置的节点,就是在序列中划分左右子树的分割位置。

只要能够一直划分下去,直到递归最后是单个节点,则说明应该返回true,否则返回false

具体做法:

  • step 1:首先对于给定列表长度为0的特殊情况返回false
  • step 2:递归函数中返回条件为 l>=r,返回true
  • step 3:递归函数中确定根节点为sequence[r],然后从后往前遍历找到左右子树分割点,进行继续递归

Java实现代码:

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 l, int r) {
        // 剩一个节点的时候 返回 true
        if(l >= r) return true;
        int j;
        int mid = sequence[r];
        
        // 找到左子树和右子树的分界点,j代表左子树的最后一个索引位置
        for(j = r; j >= l; j--) {
            int cur = sequence[j];
            if(cur < mid) break;
        }
        
        // 判断所谓的左子树中是否又不合法(不符合二叉搜索树)的元素
        for(int i = j; i >= l; i--) {
            int cur = sequence[i];
            if(cur > mid) return false;
        }
        return order(sequence, l, j) && order(sequence, j+1, r-1);
    }
}

C++实现代码:

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        if(sequence.size() == 0) return false;
        return order(sequence, 0, sequence.size() - 1);
    }
    bool order(vector<int>& sequence, int l, int r) {
        // 剩一个节点的时候 返回 true
        if(l >= r) return true;
        int j;
        int mid = sequence[r];
        
        // 找到左子树和右子树的分界点,j代表左子树的最后一个索引位置
        for(j = r; j >= l; j--) {
            int cur = sequence[j];
            if(cur < mid) break;
        }
        
        // 判断所谓的左子树中是否又不合法(不符合二叉搜索树)的元素
        for(int i = j; i >= l; i--) {
            int cur = sequence[i];
            if(cur > mid) return false;
        }
        return order(sequence, l, j) && order(sequence, j+1, r-1);
    }
};

Python实现代码:

class Solution:
    def VerifySquenceOfBST(self , sequence: List[int]) -> bool:
        if not len(sequence): return False
        def order(l, r):
            if l >= r: return True
            # i 从边界l开始先遍历所有小于根节点的数字
            i = l
            while sequence[i] < sequence[r]: i += 1
            # i 继续遍历所有大于根节点的数字
            j = i
            while sequence[i] > sequence[r]: i += 1
            # 判断是否i到达了此范围内的边界重点r,并继续递归
            return i == r and order(l, j-1) and order(j, r-1)
        return order(0, len(sequence) - 1)

复杂度分析:

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2),递归操作每次排除掉一个根节点,因此递归次数是 O ( n ) O(n) O(n),而最差情况下树退化成链表,每次递归内又要遍历所有节点,还是 O ( n ) O(n) O(n),因此最终代价就是 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n),最差情况下树退化成链表,递归深度就是 O ( n ) O(n) O(n)
练习赛142是一场编程竞赛,通常包含多个算法题目,涵盖如数组、字符串、链表、动态规划等常见数据结构与算法知识点。针对这类比赛的解题思路和方法,可以从以下几个方面进行分析: ### 题目类型与解题策略 1. **数组相关问题** - 常见的题目包括查找数组中出现次数超过一半的数字、寻找缺失的数字、求解最大子数组和等。 - 解题方法包括使用哈希表统计频率、摩尔投票法(适用于多数元素问题)、双指针技巧或前缀和优化。 2. **链表操作** - 链表题目可能涉及反转链表、判断链表是否有环、找出两个链表的相交节点等。 - 例如,在找两个链表相交点的问题中,可以先计算各自长度,然后让长链表先走差值步数,再同步遍历比较节点地址[^3]。 3. **字符串处理** - 包括最长回文子串、无重复字符的最长子串等。 - 可采用滑动窗口、动态规划或中心扩展法等策略。 4. **树与图** - 树相关的题目可能涉及二叉树的遍历、路径和、最近公共祖先等问题。 - 图论问题可能需要使用深度优先搜索(DFS)、广度优先搜索(BFS)或拓扑排序等算法。 5. **动态规划** - 动态规划常用于解决背包问题、最长递增子序列、编辑距离等。 - 关键在于定义状态转移方程,并通过迭代或记忆化搜索进行求解。 6. **贪心算法** - 适用于区间调度、活动选择、硬币找零等问题。 - 贪心策略的核心在于每一步都做出局部最优选择。 ### 示例代码:摩尔投票法解决“多数元素”问题 ```python def majorityElement(nums): count = 0 candidate = None for num in nums: if count == 0: candidate = num count += (1 if num == candidate else -1) return candidate ``` 该算法时间复杂度为 O(n),空间复杂度为 O(1),非常适合处理大规模输入的数据集[^2]。 ### 提升解题能力的建议 - **刷题积累经验**:在 LeetCode、Codeforces、AtCoder 等平台上持续练习,熟悉各种题型。 - **学习经典算法**:掌握常见的算法模板,如二分查找、归并排序、快速选择等。 - **阅读官方题解与讨论区**:了解不同解法的优劣,尤其是最优解的时间复杂度分析。 - **模拟比赛训练**:定期参加在线编程比赛,提升实战能力和代码调试速度。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值