牛客网新手入门130_90-91题_20260118

4 阅读7分钟

栈专题:两道经典栈算法题全解析(字典序最大出栈序列 + O (1) 最小值栈)

栈是算法中最基础的数据结构之一,本文结合「字典序最大的栈出栈序列」和「单车道停车场(O (1) 最小值栈)」两道经典题,从场景理解、核心原理、代码实现到复杂度分析,系统总结栈的核心用法和优化技巧。

一、第一道题:字典序最大的合法出栈序列

1. 题目核心场景

给定 1~n 的排列 P,按顺序入栈,可随时出栈,要求输出字典序最大的合法出栈序列(合法 = 按顺序入栈、仅栈顶出栈;字典序最大 = 序列前半部分尽可能大)。

2. 核心概念铺垫

(1)合法出栈序列
  • 入栈顺序不可打破:必须按 P [0]→P [1]→…→P [n-1] 入栈;
  • 出栈仅能弹栈顶:不能跳过栈顶直接弹栈底 / 中间元素。

例:P=[3,1,2,4],合法序列如 [4,2,1,3],非法序列如 [3,4,2,1](无法通过合法入栈出栈生成)。

(2)字典序

本质是「查字典规则」:从左到右逐个比较数字,第一个位置数字越大,字典序越大;若第一个相同,再比第二个,以此类推。

例:[4,2,1,3] > [3,4,2,1](第一个数字 4>3);[5,4,1] > [5,3,2](第二个数字 4>3)。

(3)贪心算法

每一步做「当前最优选择」,最终得到全局最优:本题中 “当前最优” = 弹出「栈顶 > 后续所有未入栈元素最大值」的元素(保证前面的数字尽可能大)。

(4)后缀最大值(suffixMax)
  • 定义:suffixMax[i] = 数组 P 中从 i 到末尾的最大值;
  • 作用:快速判断「后续未入栈元素的最大值」,避免遍历,保证算法效率。

3. 核心解题思路

  1. 预处理后缀最大值数组:从后往前遍历 P,计算每个位置的后缀最大值;
  1. 贪心 + 栈操作
    • 遍历 P,逐个入栈;
    • 入栈后循环检查:若栈顶 > 后续未入栈元素的最大值,弹出栈顶(加入结果);
  1. 处理剩余元素:遍历结束后,栈中剩余元素全部弹出(后续无新元素);
  1. 结果拼接:用 StringBuilder 高效拼接结果,避免字符串频繁拼接的性能损耗。

4. 完整代码实现

import java.util.Scanner;
import java.util.Deque;
import java.util.ArrayDeque;
public class Main {
    public static void main(String[] args) {
        // 输入处理(Scanner版,贴合新手模板)
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int[] P = new int[n];
        for (int i = 0; i < n; i++) {
            P[i] = in.nextInt();
        }
        // 步骤1:预处理后缀最大值数组
        int[] suffixMax = new int[n];
        suffixMax[n - 1] = P[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            suffixMax[i] = Math.max(P[i], suffixMax[i + 1]);
        }
        // 步骤2:贪心栈操作
        Deque<Integer> stack = new ArrayDeque<>();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            stack.push(P[i]); // 当前元素入栈
            // 循环检查栈顶是否可出栈
            while (!stack.isEmpty()) {
                int top = stack.peek();
                int nextMax = (i + 1 < n) ? suffixMax[i + 1] : -1;
                if (top > nextMax) {
                    sb.append(stack.pop()).append(" ");
                } else {
                    break;
                }
            }
        }
        // 步骤3:处理栈中剩余元素
        while (!stack.isEmpty()) {
            sb.append(stack.pop()).append(" ");
        }
        // 输出结果(去除末尾空格)
        System.out.println(sb.toString().trim());
        in.close();
    }
}

5. 复杂度分析

指标复杂度说明
时间复杂度O(n)每个元素最多入栈、出栈各 1 次,无嵌套循环;后缀最大值计算为 O (n)
空间复杂度O(n)后缀最大值数组、栈、StringBuilder 均占用 O (n) 空间(空间换时间)

6. 关键 API 说明

API作用场景
Deque.push()元素入栈顶模拟入栈操作
Deque.peek()获取栈顶(不弹出)判断栈顶是否可出栈
Deque.pop()弹出栈顶合法出栈时调用
StringBuilder.append()高效拼接字符串避免 n 规模下字符串拼接的性能损耗
Math.max()计算两个数的最大值构建后缀最大值数组

二、第二道题:O (1) 时间获取最小值的栈(单车道停车场)

1. 题目核心场景

模拟单车道停车场的车辆驶入 / 驶离,要求实现 4 个 O (1) 操作:

方法功能对应栈操作
push(int node)车辆 node 驶入(停最里面)入栈
pop()最门口车辆驶离出栈
top()返回最门口车号取栈顶
min()返回场内最小车号O (1) 查最小值

2. 核心概念铺垫

(1)访问修饰符(public/private)
  • public:修饰对外暴露的方法(push/pop/top/min),允许外部调用;
  • private:修饰内部变量(主栈 / 辅助栈),保护数据不被外部篡改;
  • 无修饰符(默认):仅同包可访问,外部调用会失败,不推荐。
(2)参数名 node

node 是 push 方法的参数名,仅代表「驶入车辆的编号」,可改为 carNum/x 等,不影响功能。

3. 核心解题思路:双栈设计

普通栈无法实现 O (1) 查最小值,引入「辅助最小值栈」与主栈同步操作:

  • 主栈:存储车辆编号(模拟停车场车辆);
  • 辅助栈:维护「当前主栈的最小值」,栈顶始终是主栈最小值。
关键规则
  1. push 同步:主栈入一个元素,辅助栈必入一个元素:
    • 辅助栈空 → 入当前元素;
    • 当前元素 ≤ 辅助栈顶 → 入当前元素(新最小值);
    • 当前元素 > 辅助栈顶 → 入辅助栈顶(最小值不变,维持栈长度)。
  1. pop 同步:主栈弹出一个元素,辅助栈同步弹出(保证双栈长度一致);
  1. min 查询:直接取辅助栈顶(O (1))。

4. 完整代码实现

import java.util.Stack;
public class Solution {
    // 主栈:存储车辆编号(private 保护内部数据)
    private Stack<Integer> mainStack = new Stack<>();
    // 辅助栈:维护当前最小值
    private Stack<Integer> minStack = new Stack<>();
    /**
     * 车辆驶入(入栈)
     * @param node 驶入车辆编号
     */
    public void push(int node) {
        mainStack.push(node);
        // 辅助栈同步维护最小值
        if (minStack.isEmpty() || node <= minStack.peek()) {
            minStack.push(node);
        } else {
            minStack.push(minStack.peek()); // 复用当前最小值
        }
    }
    /**
     * 最门口车辆驶离(出栈)
     */
    public void pop() {
        // 判空避免空栈异常
        if (!mainStack.isEmpty() && !minStack.isEmpty()) {
            mainStack.pop();
            minStack.pop();
        }
    }
    /**
     * 返回最门口车号(取栈顶)
     * @return 栈顶车辆编号
     */
    public int top() {
        return mainStack.peek();
    }
    /**
     * 返回场内最小车号(O(1))
     * @return 当前最小值
     */
    public int min() {
        return minStack.peek();
    }
}

5. 核心疑问解答

Q1:minStack.push(minStack.peek()) 为什么重复压入栈顶?

A:核心是「维持双栈长度一致」+「保留当前最小值」:

  • 主栈入一个元素,辅助栈必须入一个,否则 pop 后长度不一致;
  • 新元素不改变最小值时,重复压入栈顶,保证辅助栈顶始终是当前最小值。
Q2:pop 同步操作后,minStack 为何仍正确?

A:辅助栈的每个元素对应「主栈到该位置的最小值」,pop 时同步砍掉最后一个 “最小值标记”,剩余栈顶即为主栈剩余元素的最小值。

测试用例验证
操作主栈状态辅助栈状态top()min()
push(8)[8][8]88
push(4)[8,4][8,4]44
push(6)[8,4,6][8,4,4]64
push(2)[8,4,6,2][8,4,4,2]22
pop()[8,4,6][8,4,4]64
pop()[8,4][8,4]44

6. 复杂度分析

指标复杂度说明
时间复杂度O(1)所有方法均为单次栈操作,无循环
空间复杂度O(n)辅助栈最坏情况存储 n 个元素(空间换时间)

三、核心知识点总结(跨两道题)

1. 栈的核心特性

  • 先入后出(FILO):两道题均基于栈的这一核心特性(出栈仅能弹栈顶);
  • 栈的常用操作:push(入栈)、pop(出栈)、peek(取栈顶),均为 O (1) 时间。

2. 算法优化思路

  • 空间换时间:第一道题用后缀最大值数组避免遍历,第二道题用辅助栈实现 O (1) 查最小值;
  • 贪心策略:第一道题通过 “优先弹出当前最大元素” 实现字典序最大;
  • 同步操作:第二道题通过双栈同步,保证最小值查询的正确性。

3. 工程化技巧

  • 输入输出优化:第一道题用 Scanner(新手友好),大数据量可换 BufferedReader;
  • 字符串拼接:用 StringBuilder 替代 String 拼接,避免性能损耗;
  • 数据保护:用 private 修饰内部变量,public 暴露方法,符合面向对象设计原则;
  • 异常防护:第二道题 pop 时判空,避免空栈异常。

4. 举一反三

  • 字典序最大出栈序列:可扩展为 “字典序最小出栈序列”(贪心策略改为优先弹出最小元素);
  • O (1) 最小值栈:可扩展为 “O (1) 最大值栈”(辅助栈逻辑改为维护最大值)。

最后

栈是算法的基础数据结构,这两道题分别覆盖了「栈的贪心应用」和「栈的优化扩展」,掌握这两类场景后,大部分栈相关的算法题都能举一反三。建议结合测试用例手动模拟栈的操作过程,理解每一步的核心逻辑~