栈与队列经典算法题详解

45 阅读6分钟

1. 最小栈(LeetCode 155)

问题分析

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

解题思路:双栈法

  • 使用两个栈:一个正常存储数据,另一个存储当前最小值
  • 最小栈的栈顶始终是数据栈中的最小值
  • 入栈时同步更新最小栈,出栈时同步移除

代码实现与优化

java

class MinStack {
    private Stack<Integer> dataStack;
    private Stack<Integer> minStack;

    public MinStack() {
        dataStack = new Stack<>();
        minStack = new Stack<>();
    }
    
    public void push(int val) {
        dataStack.push(val);
        // 最小栈为空或当前值 <= 最小栈顶时入栈
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }
    
    public void pop() {
        if (dataStack.isEmpty()) return;
        
        int popped = dataStack.pop();
        // 如果弹出的是最小值,同步移除最小栈顶
        if (popped == minStack.peek()) {
            minStack.pop();
        }
    }
    
    public int top() {
        if (dataStack.isEmpty()) throw new RuntimeException("Stack is empty");
        return dataStack.peek();
    }
    
    public int getMin() {
        if (minStack.isEmpty()) throw new RuntimeException("Stack is empty");
        return minStack.peek();
    }
}

复杂度分析

  • 时间复杂度:所有操作 O(1)
  • 空间复杂度:O(n)

2. 有效的括号(LeetCode 20)

问题分析

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

解题思路:栈匹配法

  • 遇到左括号入栈
  • 遇到右括号检查栈顶是否匹配
  • 最终栈为空则有效

代码实现

java

class Solution {
    public boolean isValid(String s) {
        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 != '(') ||
                    (c == ']' && top != '[') || 
                    (c == '}' && top != '{')) {
                    return false;
                }
            }
        }
        
        return stack.isEmpty();
    }
}

使用HashMap的优化版本

java

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        Map<Character, Character> map = new HashMap<>();
        map.put(')', '(');
        map.put(']', '[');
        map.put('}', '{');
        
        for (char c : s.toCharArray()) {
            if (!map.containsKey(c)) {
                // 左括号
                stack.push(c);
            } else {
                // 右括号
                if (stack.isEmpty() || stack.pop() != map.get(c)) {
                    return false;
                }
            }
        }
        
        return stack.isEmpty();
    }
}

3. 栈的压入、弹出序列

问题分析

判断第二个序列是否可能为第一个序列的弹出顺序。

解题思路:模拟出入栈

  • 遍历压入序列,模拟入栈操作
  • 每次入栈后检查栈顶是否等于弹出序列当前元素
  • 相等则出栈并移动弹出序列指针

代码实现

java

import java.util.Stack;

public class Solution {
    public boolean IsPopOrder(int[] pushA, int[] popA) {
        if (pushA == null || popA == null || pushA.length != popA.length) {
            return false;
        }
        
        Stack<Integer> stack = new Stack<>();
        int popIndex = 0;
        
        for (int i = 0; i < pushA.length; i++) {
            stack.push(pushA[i]);
            
            // 检查是否可以弹出
            while (!stack.isEmpty() && stack.peek() == popA[popIndex]) {
                stack.pop();
                popIndex++;
            }
        }
        
        return stack.isEmpty();
    }
}

4. 逆波兰表达式求值(LeetCode 150)

问题分析

根据逆波兰表示法(后缀表达式)求表达式的值。

中缀转后缀表达式方法

以 (1+2)*3+4 为例:

  1. 加括号明确优先级:(((1+2)*3)+4)
  2. 将运算符移到对应括号后面:(((12+)3*)4+)
  3. 去除括号:12+3*4+

解题思路:栈计算法

  • 遇到数字入栈
  • 遇到运算符弹出两个数字运算后结果入栈
  • 注意除法和减法的顺序

代码实现

java

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        
        for (String token : tokens) {
            if (isOperator(token)) {
                int b = stack.pop();  // 注意顺序:先弹出的是右操作数
                int a = stack.pop();  // 后弹出的是左操作数
                int result = calculate(a, b, token);
                stack.push(result);
            } else {
                stack.push(Integer.parseInt(token));
            }
        }
        
        return stack.pop();
    }
    
    private boolean isOperator(String token) {
        return token.equals("+") || token.equals("-") || 
               token.equals("*") || token.equals("/");
    }
    
    private int calculate(int a, int b, String operator) {
        switch (operator) {
            case "+": return a + b;
            case "-": return a - b;
            case "*": return a * b;
            case "/": return a / b;  // 题目保证除法为整数除法
            default: throw new IllegalArgumentException("Invalid operator");
        }
    }
}

5. 用队列实现栈(LeetCode 225)

问题分析

使用队列实现栈的 push、pop、top、empty 操作。

解题思路:双队列法

  • 入栈时直接加入非空队列
  • 出栈时将非空队列的前 n-1 个元素转移到空队列,弹出最后一个元素
  • 始终保持一个队列为空

代码实现

java

class MyStack {
    private Queue<Integer> queue1;
    private Queue<Integer> queue2;
    
    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }
    
    public void push(int x) {
        // 总是加入非空队列
        if (!queue1.isEmpty()) {
            queue1.offer(x);
        } else {
            queue2.offer(x);
        }
    }
    
    public int pop() {
        if (empty()) throw new RuntimeException("Stack is empty");
        
        if (!queue1.isEmpty()) {
            // 将 queue1 的前 n-1 个元素移到 queue2
            while (queue1.size() > 1) {
                queue2.offer(queue1.poll());
            }
            return queue1.poll();
        } else {
            // 将 queue2 的前 n-1 个元素移到 queue1
            while (queue2.size() > 1) {
                queue1.offer(queue2.poll());
            }
            return queue2.poll();
        }
    }
    
    public int top() {
        if (empty()) throw new RuntimeException("Stack is empty");
        
        int topValue;
        if (!queue1.isEmpty()) {
            // 将 queue1 的所有元素移到 queue2,记录最后一个
            while (!queue1.isEmpty()) {
                topValue = queue1.peek();
                queue2.offer(queue1.poll());
            }
        } else {
            // 将 queue2 的所有元素移到 queue1,记录最后一个
            while (!queue2.isEmpty()) {
                topValue = queue2.peek();
                queue1.offer(queue2.poll());
            }
        }
        return topValue;
    }
    
    public boolean empty() {
        return queue1.isEmpty() && queue2.isEmpty();
    }
}

优化版本(单队列法)

java

class MyStack {
    private Queue<Integer> queue;
    
    public MyStack() {
        queue = new LinkedList<>();
    }
    
    public void push(int x) {
        queue.offer(x);
        // 将新元素前面的所有元素重新入队,使其成为队首
        for (int i = 0; i < queue.size() - 1; i++) {
            queue.offer(queue.poll());
        }
    }
    
    public int pop() {
        return queue.poll();
    }
    
    public int top() {
        return queue.peek();
    }
    
    public boolean empty() {
        return queue.isEmpty();
    }
}

6. 用栈实现队列(LeetCode 232)

问题分析

使用栈实现队列的 push、pop、peek、empty 操作。

解题思路:双栈法

  • 输入栈:负责接收 push 操作
  • 输出栈:负责 pop 和 peek 操作
  • 当输出栈为空时,将输入栈所有元素转移到输出栈

代码实现

java

class MyQueue {
    private Stack<Integer> inStack;
    private Stack<Integer> outStack;
    
    public MyQueue() {
        inStack = new Stack<>();
        outStack = new Stack<>();
    }
    
    public void push(int x) {
        inStack.push(x);
    }
    
    public int pop() {
        if (empty()) throw new RuntimeException("Queue is empty");
        
        // 如果输出栈为空,转移所有元素
        if (outStack.isEmpty()) {
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
        }
        
        return outStack.pop();
    }
    
    public int peek() {
        if (empty()) throw new RuntimeException("Queue is empty");
        
        // 如果输出栈为空,转移所有元素
        if (outStack.isEmpty()) {
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
        }
        
        return outStack.peek();
    }
    
    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }
}

7. 设计循环队列(LeetCode 622)

问题分析

设计一个循环队列,支持 enQueue、deQueue、Front、Rear、isEmpty、isFull 操作。

循环队列设计要点

  • 使用数组实现,维护 front 和 rear 指针
  • 队列空:front == rear
  • 队列满:(rear + 1) % capacity == front
  • 浪费一个空间来区分空和满的状态

代码实现

java

class MyCircularQueue {
    private int[] data;
    private int front;  // 队首指针
    private int rear;   // 队尾指针(指向下一个插入位置)
    private int capacity;
    
    public MyCircularQueue(int k) {
        capacity = k + 1;  // 多分配一个空间用于判断满
        data = new int[capacity];
        front = 0;
        rear = 0;
    }
    
    public boolean enQueue(int value) {
        if (isFull()) return false;
        
        data[rear] = value;
        rear = (rear + 1) % capacity;
        return true;
    }
    
    public boolean deQueue() {
        if (isEmpty()) return false;
        
        front = (front + 1) % capacity;
        return true;
    }
    
    public int Front() {
        if (isEmpty()) return -1;
        return data[front];
    }
    
    public int Rear() {
        if (isEmpty()) return -1;
        // rear 指向下一个插入位置,队尾元素在 rear-1 位置
        return data[(rear - 1 + capacity) % capacity];
    }
    
    public boolean isEmpty() {
        return front == rear;
    }
    
    public boolean isFull() {
        return (rear + 1) % capacity == front;
    }
}

不使用浪费空间的版本

java

class MyCircularQueue {
    private int[] data;
    private int front;
    private int rear;
    private int size;  // 当前元素个数
    
    public MyCircularQueue(int k) {
        data = new int[k];
        front = 0;
        rear = 0;
        size = 0;
    }
    
    public boolean enQueue(int value) {
        if (isFull()) return false;
        
        data[rear] = value;
        rear = (rear + 1) % data.length;
        size++;
        return true;
    }
    
    public boolean deQueue() {
        if (isEmpty()) return false;
        
        front = (front + 1) % data.length;
        size--;
        return true;
    }
    
    public int Front() {
        if (isEmpty()) return -1;
        return data[front];
    }
    
    public int Rear() {
        if (isEmpty()) return -1;
        return data[(rear - 1 + data.length) % data.length];
    }
    
    public boolean isEmpty() {
        return size == 0;
    }
    
    public boolean isFull() {
        return size == data.length;
    }
}

算法技巧总结

栈的常见应用场景

  1. 括号匹配:使用栈检查嵌套结构
  2. 表达式求值:中缀转后缀,后缀表达式求值
  3. 函数调用:模拟递归调用栈
  4. 浏览器历史:前进后退功能

队列的常见应用场景

  1. BFS遍历:图的广度优先搜索
  2. 任务调度:CPU任务调度,消息队列
  3. 缓存实现:LRU缓存淘汰算法

设计模式总结

  1. 最小栈:双栈同步维护
  2. 栈实现队列:输入栈 + 输出栈
  3. 队列实现栈:双队列或单队列重排
  4. 循环队列:数组 + 指针模运算

复杂度分析要点

  • 栈和队列的基本操作通常为 O(1)
  • 注意边界条件:空栈/空队列操作
  • 循环队列注意指针回绕处理

这些题目涵盖了栈和队列的核心应用场景,掌握后能够解决大多数相关问题。建议重点理解双栈法和双队列法的设计思想,这在很多系统设计中都有应用。