算法母题30道(第3篇):栈与队列(11-15)

系列说明:本系列共6篇文章,涵盖30道经典算法母题,使用Java实现
适用场景:算法学习、面试准备、编程能力提升


📋 30道算法母题总览

第1篇:数组与字符串基础(1-5)

  1. 两数之和
  2. 三数之和
  3. 盛最多水的容器
  4. 接雨水
  5. 最长无重复字符子串

第2篇:链表操作(6-10)

  1. 反转链表
  2. 合并两个有序链表
  3. 链表中倒数第k个节点
  4. 判断链表是否有环
  5. 合并K个有序链表

第3篇:栈与队列(11-15)⭐ 本篇

  1. 有效的括号
  2. 用栈实现队列
  3. 最小栈
  4. 滑动窗口最大值
  5. 每日温度

第4篇:二叉树遍历(16-20)

  1. 二叉树的前序遍历
  2. 二叉树的中序遍历
  3. 二叉树的后序遍历
  4. 二叉树的层序遍历
  5. 二叉树的最大深度

第5篇:动态规划(21-25)

  1. 爬楼梯
  2. 最长递增子序列
  3. 最长公共子序列
  4. 背包问题(0-1背包)
  5. 编辑距离

第6篇:搜索与排序(26-30)

  1. 二分查找
  2. 快速排序
  3. 归并排序
  4. 岛屿数量(DFS/BFS)
  5. 全排列(回溯)

第11题:有效的括号

题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串 s,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合
  2. 左括号必须以正确的顺序闭合
  3. 每个右括号都有一个对应的相同类型的左括号

示例

输入:s = "()"
输出:true

输入:s = "()[]{}"
输出:true

输入:s = "(]"
输出:false

输入:s = "([)]"
输出:false

输入:s = "{[]}"
输出:true

💡 解题思路

栈的应用

  1. 遍历字符串中的每个字符
  2. 如果是左括号,压入栈中
  3. 如果是右括号:
    • 检查栈是否为空(空则无效)
    • 弹出栈顶元素,检查是否匹配
    • 不匹配则返回 false
  4. 遍历结束后,检查栈是否为空(空则有效)

日常应用

  • 代码编辑器:括号匹配检查
  • HTML/XML 解析:标签配对验证
  • 数学表达式:公式合法性检查
  • 编译器:语法分析的基础

🔧 代码解决的问题

  • 括号配对验证
  • 嵌套结构的合法性检查
  • 字符串的语法验证

💻 Java实现代码

import java.util.Stack;

public class ValidParentheses {
    
    /**
     * 有效的括号 - 栈
     * @param s 括号字符串
     * @return 是否有效
     */
    public boolean isValid(String s) {
        // 奇数长度的字符串必然无效
        if (s.length() % 2 != 0) {
            return false;
        }
        
        Stack<Character> stack = new Stack<>();
        
        for (char c : s.toCharArray()) {
            // 左括号入栈
            if (c == '(' || c == '[' || c == '{') {
                stack.push(c);
            } 
            // 右括号匹配
            else {
                // 栈为空,说明没有对应的左括号
                if (stack.isEmpty()) {
                    return false;
                }
                
                char top = stack.pop();
                
                // 检查是否匹配
                if (c == ')' && top != '(') return false;
                if (c == ']' && top != '[') return false;
                if (c == '}' && top != '{') return false;
            }
        }
        
        // 栈为空说明所有括号都匹配
        return stack.isEmpty();
    }
    
    /**
     * 优化的版本 - 使用 HashMap
     */
    public boolean isValidOptimized(String s) {
        if (s.length() % 2 != 0) {
            return false;
        }
        
        Stack<Character> stack = new Stack<>();
        
        // 右括号到左括号的映射
        java.util.Map<Character, Character> map = new java.util.HashMap<>();
        map.put(')', '(');
        map.put(']', '[');
        map.put('}', '{');
        
        for (char c : s.toCharArray()) {
            if (map.containsKey(c)) {
                // 右括号
                if (stack.isEmpty() || stack.pop() != map.get(c)) {
                    return false;
                }
            } else {
                // 左括号
                stack.push(c);
            }
        }
        
        return stack.isEmpty();
    }
    
    // 测试方法
    public static void main(String[] args) {
        ValidParentheses solution = new ValidParentheses();
        
        String[] testCases = {"()", "()[]{}", "(]", "([)]", "{[]}"};
        boolean[] expected = {true, true, false, false, true};
        
        for (int i = 0; i < testCases.length; i++) {
            boolean result = solution.isValid(testCases[i]);
            System.out.println("输入: \"" + testCases[i] + "\"");
            System.out.println("输出: " + result + " (期望: " + expected[i] + ")");
            System.out.println();
        }
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(n)

    • n 是字符串的长度
    • 遍历一次字符串,每次栈操作为 O(1)
  • 空间复杂度:O(n)

    • 最坏情况下,所有字符都是左括号
    • 栈的深度为 n

第12题:用栈实现队列

📝 题目描述

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty)。

示例

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2]
myQueue.peek();  // return 1
myQueue.pop();   // return 1, queue is [2]
myQueue.empty(); // return false

💡 解题思路

双栈法

  1. 使用两个栈:stackIn(入队栈)和 stackOut(出队栈)
  2. push 操作:直接压入 stackIn
  3. pop/peek 操作
    • 如果 stackOut 为空,将 stackIn 的所有元素倒入 stackOut
    • stackOut 弹出/查看栈顶元素
  4. empty 操作:两个栈都为空时队列为空

核心思想

  • stackIn 负责接收新元素(后进)
  • stackOut 负责提供旧元素(先出)
  • 元素从 stackIn 转移到 stackOut 时,顺序反转,实现 FIFO

日常应用

  • 消息队列:缓冲和处理消息
  • 任务调度:按顺序处理任务
  • 打印队列:文档按提交顺序打印
  • 数据流处理:先进先出的数据处理

🔧 代码解决的问题

  • 用 LIFO 结构实现 FIFO
  • 摊还分析(Amortized Analysis)
  • 数据结构的设计模式

💻 Java实现代码

import java.util.Stack;

class MyQueue {
    private Stack<Integer> stackIn;
    private Stack<Integer> stackOut;
    
    /** Initialize your data structure here. */
    public MyQueue() {
        stackIn = new Stack<>();
        stackOut = new Stack<>();
    }
    
    /** Push element x to the back of queue. */
    public void push(int x) {
        stackIn.push(x);
    }
    
    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        // 如果 stackOut 为空,转移元素
        if (stackOut.isEmpty()) {
            transferElements();
        }
        return stackOut.pop();
    }
    
    /** Get the front element. */
    public int peek() {
        // 如果 stackOut 为空,转移元素
        if (stackOut.isEmpty()) {
            transferElements();
        }
        return stackOut.peek();
    }
    
    /** Returns whether the queue is empty. */
    public boolean empty() {
        return stackIn.isEmpty() && stackOut.isEmpty();
    }
    
    /** 将 stackIn 的元素转移到 stackOut */
    private void transferElements() {
        while (!stackIn.isEmpty()) {
            stackOut.push(stackIn.pop());
        }
    }
    
    // 测试方法
    public static void main(String[] args) {
        MyQueue queue = new MyQueue();
        
        queue.push(1);
        System.out.println("push(1)");
        
        queue.push(2);
        System.out.println("push(2)");
        
        System.out.println("peek(): " + queue.peek());  // 返回 1
        System.out.println("pop(): " + queue.pop());    // 返回 1
        System.out.println("empty(): " + queue.empty()); // 返回 false
        System.out.println("pop(): " + queue.pop());    // 返回 2
        System.out.println("empty(): " + queue.empty()); // 返回 true
    }
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */

⏱️ 复杂度分析

时间复杂度

  • push:O(1) - 直接压栈
  • pop:摊还 O(1) - 每个元素最多入栈和出栈两次
  • peek:摊还 O(1) - 同 pop
  • empty:O(1) - 检查两个栈

空间复杂度:O(n)

  • 两个栈总共存储 n 个元素

第13题:最小栈

📝 题目描述

设计一个支持 push、pop、top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) —— 将元素 x 推入栈中
  • pop() —— 删除栈顶的元素
  • top() —— 获取栈顶元素
  • getMin() —— 检索栈中的最小元素

示例

输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3
minStack.pop();
minStack.top();      --> 返回 0
minStack.getMin();   --> 返回 -2

💡 解题思路

辅助栈法

  1. 使用两个栈:dataStack(数据栈)和 minStack(最小值栈)
  2. push 操作
    • 元素压入 dataStack
    • 如果 minStack 为空或新元素 ≤ 当前最小值,也压入 minStack
  3. pop 操作
    • dataStack 弹出元素
    • 如果弹出的元素等于 minStack 的栈顶,也弹出 minStack
  4. getMin 操作:返回 minStack 的栈顶元素

关键点

  • minStack 的栈顶始终是当前数据栈的最小值
  • 相等元素也要压入 minStack(处理重复最小值)

🎯 日常应用

  • 股票交易:记录最低价格
  • 游戏开发:跟踪最低分数
  • 监控系统:记录最小指标值
  • 算法优化:需要频繁查询最小值的场景

🔧 代码解决的问题

  • O(1) 时间获取最小值
  • 空间优化的技巧
  • 栈的扩展应用

Java实现代码

import java.util.Stack;

class MinStack {
    private Stack<Integer> dataStack;
    private Stack<Integer> minStack;
    
    /** Initialize your data structure here. */
    public MinStack() {
        dataStack = new Stack<>();
        minStack = new Stack<>();
    }
    
    /** Push element x onto stack. */
    public void push(int val) {
        dataStack.push(val);
        
        // 如果 minStack 为空或新元素小于等于当前最小值
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }
    
    /** Removes the element on top of the stack. */
    public void pop() {
        if (dataStack.isEmpty()) {
            throw new RuntimeException("Stack is empty");
        }
        
        int popped = dataStack.pop();
        
        // 如果弹出的元素是最小值,也要弹出 minStack
        if (popped == minStack.peek()) {
            minStack.pop();
        }
    }
    
    /** Get the top element. */
    public int top() {
        if (dataStack.isEmpty()) {
            throw new RuntimeException("Stack is empty");
        }
        return dataStack.peek();
    }
    
    /** Retrieve the minimum element in the stack. */
    public int getMin() {
        if (minStack.isEmpty()) {
            throw new RuntimeException("Stack is empty");
        }
        return minStack.peek();
    }
    
    // 测试方法
    public static void main(String[] args) {
        MinStack minStack = new MinStack();
        
        minStack.push(-2);
        System.out.println("push(-2)");
        
        minStack.push(0);
        System.out.println("push(0)");
        
        minStack.push(-3);
        System.out.println("push(-3)");
        
        System.out.println("getMin(): " + minStack.getMin());  // -3
        System.out.println("top(): " + minStack.top());        // -3
        
        minStack.pop();
        System.out.println("pop()");
        
        System.out.println("getMin(): " + minStack.getMin());  // -2
        System.out.println("top(): " + minStack.top());        // 0
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

⏱️ 复杂度分析

  • 时间复杂度

    • push:O(1)
    • pop:O(1)
    • top:O(1)
    • getMin:O(1)
  • 空间复杂度:O(n)

    • 最坏情况下,所有元素都压入 minStack(递减序列)
    • 最好情况下,只有最小值压入 minStack(递增序列)

第14题:滑动窗口最大值

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]

解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

💡 解题思路

单调队列(双端队列)

  1. 维护一个双端队列 deque,存储元素的索引
  2. 队列中的元素对应的值从大到小排列(单调递减)
  3. 遍历数组:
    • 移除队列中超出窗口范围的索引(队首)
    • 移除队列中小于当前元素的索引(队尾),保持单调性
    • 将当前元素索引加入队尾
    • 当窗口形成后(i >= k-1),队首就是最大值

为什么用单调队列?

  • 队首始终是最大值
  • 被遮挡的小元素永远不会成为最大值,可以直接丢弃
  • 时间复杂度从 O(nk) 优化到 O(n)

日常应用

  • 股票分析:滑动窗口内的最高价
  • 信号处理:峰值检测
  • 图像处理:局部最大值提取
  • 性能监控:时间段内的峰值指标

🔧 代码解决的问题

  • 窗口内的极值查询
  • 避免重复比较
  • 线性时间复杂度的优化

💻 Java实现代码

import java.util.Deque;
import java.util.LinkedList;

public class SlidingWindowMaximum {
    
    /**
     * 滑动窗口最大值 - 单调队列
     * @param nums 输入数组
     * @param k 窗口大小
     * @return 每个窗口的最大值数组
     */
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return new int[0];
        }
        
        int n = nums.length;
        int[] result = new int[n - k + 1];
        // 双端队列,存储索引
        Deque<Integer> deque = new LinkedList<>();
        
        for (int i = 0; i < n; i++) {
            // 1. 移除超出窗口范围的索引(队首)
            if (!deque.isEmpty() && deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            
            // 2. 维护单调递减队列(移除队尾小于当前元素的索引)
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            }
            
            // 3. 将当前元素索引加入队尾
            deque.offerLast(i);
            
            // 4. 当窗口形成后,记录最大值(队首)
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        
        return result;
    }
    
    // 测试方法
    public static void main(String[] args) {
        SlidingWindowMaximum solution = new SlidingWindowMaximum();
        
        int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
        int k = 3;
        
        System.out.println("输入数组: " + java.util.Arrays.toString(nums));
        System.out.println("窗口大小: " + k);
        
        int[] result = solution.maxSlidingWindow(nums, k);
        System.out.println("窗口最大值: " + java.util.Arrays.toString(result));
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(n)

    • 每个元素最多入队和出队一次
    • 均摊时间复杂度为 O(1)
  • 空间复杂度:O(k)

    • 双端队列最多存储 k 个元素

第15题:每日温度

题目描述

给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

解释:
第0天(73度) -> 第1天(74度) 更高,间隔1天
第1天(74度) -> 第2天(75度) 更高,间隔1天
第2天(75度) -> 第6天(76度) 更高,间隔4天
第3天(71度) -> 第5天(72度) 更高,间隔2天
...

💡 解题思路

单调栈

  1. 维护一个单调栈,存储温度的索引
  2. 栈中温度对应的值从大到小排列(单调递减)
  3. 遍历温度数组:
    • 如果当前温度 > 栈顶索引对应的温度:
      • 弹出栈顶索引
      • 计算天数差:当前索引 - 栈顶索引
      • 记录结果
    • 将当前索引压入栈中
  4. 栈中剩余的索引对应没有更高温度的天数(默认为 0)

核心思想

  • 栈中存储的是等待更高温度的天数索引
  • 当遇到更高温度时,就找到了等待天的答案

🎯 日常应用

  • 股票预测:找到下一个更高价格的日期
  • 销售分析:预测销量增长的时间
  • 天气预:温度变化趋势分析
  • 排队系统:等待服务的时间预测

代码解决的问题

  • "下一个更大元素"问题
  • 单调栈的经典应用
  • 避免暴力搜索的 O(n²) 复杂度

💻 Java实现代码

import java.util.Stack;

public class DailyTemperatures {
    
    /**
     * 每日温度 - 单调栈
     * @param temperatures 温度数组
     * @return 等待更高温度的天数数组
     */
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] result = new int[n];
        // 单调栈,存储索引
        Stack<Integer> stack = new Stack<>();
        
        for (int i = 0; i < n; i++) {
            // 当当前温度高于栈顶索引对应的温度时
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                int prevIndex = stack.pop();
                result[prevIndex] = i - prevIndex;
            }
            
            // 将当前索引入栈
            stack.push(i);
        }
        
        // 栈中剩余的索引对应没有更高温度的天数(默认为0,无需处理)
        return result;
    }
    
    /**
     * 暴力法(不推荐,仅作对比)
     */
    public int[] dailyTemperaturesBruteForce(int[] temperatures) {
        int n = temperatures.length;
        int[] result = new int[n];
        
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (temperatures[j] > temperatures[i]) {
                    result[i] = j - i;
                    break;
                }
            }
        }
        
        return result;
    }
    
    // 测试方法
    public static void main(String[] args) {
        DailyTemperatures solution = new DailyTemperatures();
        
        int[] temperatures = {73, 74, 75, 71, 69, 72, 76, 73};
        
        System.out.println("温度数组: " + java.util.Arrays.toString(temperatures));
        
        int[] result = solution.dailyTemperatures(temperatures);
        System.out.println("等待天数: " + java.util.Arrays.toString(result));
        
        System.out.println("\n详细解释:");
        for (int i = 0; i < temperatures.length; i++) {
            System.out.printf("第%d天(%d度) -> 等待%d天\n", 
                i, temperatures[i], result[i]);
        }
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(n)

    • 每个元素最多入栈和出栈一次
    • 均摊时间复杂度为 O(1)
  • 空间复杂度:O(n)

    • 栈的最大深度为 n(递减序列)

对比暴力法

  • 暴力法:O(n²) 时间复杂度
  • 单调栈:O(n) 时间复杂度
  • 对于大规模数据,单调栈优势明显

📊 总结与对比

栈与队列技巧总结

题目 核心技巧 关键数据结构 时间复杂度 空间复杂度
有效的括号 栈匹配 Stack O(n) O(n)
用栈实现队列 双栈转换 2×Stack 摊还O(1) O(n)
最小栈 辅助栈 2×Stack O(1) O(n)
滑动窗口最大值 单调队列 Deque O(n) O(k)
每日温度 单调栈 Stack O(n) O(n)

通用解题模板

1. 括号匹配模板
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
    if (是左括号) {
        stack.push(c);
    } else {
        if (stack.isEmpty() || !匹配) return false;
        stack.pop();
    }
}
return stack.isEmpty();
2. 单调栈模板(下一个更大元素)
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < nums.length; i++) {
    while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
        int prevIndex = stack.pop();
        result[prevIndex] = i - prevIndex;
    }
    stack.push(i);
}
3. 单调队列模板(滑动窗口最大值)
Deque<Integer> deque = new LinkedList<>();
for (int i = 0; i < nums.length; i++) {
    // 移除超出窗口的元素
    if (!deque.isEmpty() && deque.peekFirst() <= i - k) {
        deque.pollFirst();
    }
    // 维护单调性
    while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
        deque.pollLast();
    }
    deque.offerLast(i);
    
    if (i >= k - 1) {
        result[i - k + 1] = nums[deque.peekFirst()];
    }
}
4. 双栈实现队列模板
class MyQueue {
    Stack<Integer> stackIn, stackOut;
    
    public void push(int x) { stackIn.push(x); }
    
    public int pop() {
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.pop();
    }
}

📚 下一篇预告

第4篇:二叉树遍历(16-20)

  • 二叉树的前序遍历
  • 二叉树的中序遍历
  • 二叉树的后序遍历
  • 二叉树的层序遍历
  • 二叉树的最大深度

如果觉得这篇文章对您有帮助,欢迎:

  • 👍 点赞支持
  • 💬 留言讨论
  • ⭐ 收藏备用

有任何算法问题,欢迎在评论区交流!

更多推荐