首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >LeetCode栈与队列全解析:从基础到高级应用

LeetCode栈与队列全解析:从基础到高级应用

作者头像
安全风信子
发布2025-11-13 14:26:25
发布2025-11-13 14:26:25
3790
举报
文章被收录于专栏:AI SPPECHAI SPPECH

一、栈与队列基础概念

1.1 栈的数据结构定义

栈(Stack)是一种遵循后进先出(LIFO, Last-In-First-Out)原则的线性数据结构。在栈中,元素的插入和删除操作都在一端进行,这一端被称为栈顶(Top)。

在LeetCode中,栈通常通过Java的Stack类或通过Deque接口的实现类(如ArrayDeque)来实现。以下是栈的基本操作:

代码语言:javascript
复制
// 使用Deque实现栈
Deque<Integer> stack = new ArrayDeque<>();

// 入栈操作
stack.push(1);
stack.push(2);
stack.push(3);

// 查看栈顶元素
int top = stack.peek(); // 返回3,但不删除

// 出栈操作
int popped = stack.pop(); // 返回3,并从栈中删除

// 检查栈是否为空
boolean isEmpty = stack.isEmpty();

// 获取栈的大小
int size = stack.size();
1.2 队列的数据结构定义

队列(Queue)是一种遵循先进先出(FIFO, First-In-First-Out)原则的线性数据结构。在队列中,元素的插入在一端(队尾,Rear)进行,而元素的删除在另一端(队头,Front)进行。

在Java中,队列通常通过Queue接口的实现类来实现,如LinkedListPriorityQueue等。以下是队列的基本操作:

代码语言:javascript
复制
// 使用LinkedList实现队列
Queue<Integer> queue = new LinkedList<>();

// 入队操作
queue.offer(1);
queue.offer(2);
queue.offer(3);

// 查看队头元素
int front = queue.peek(); // 返回1,但不删除

// 出队操作
int removed = queue.poll(); // 返回1,并从队列中删除

// 检查队列是否为空
boolean isEmpty = queue.isEmpty();

// 获取队列的大小
int size = queue.size();
1.3 栈与队列的应用场景

栈的常见应用场景:

  1. 括号匹配问题
  2. 表达式求值与转换
  3. 函数调用的系统栈
  4. 浏览器的前进后退功能
  5. 撤销操作

队列的常见应用场景:

  1. 任务调度
  2. 广度优先搜索(BFS)
  3. 生产者-消费者模式
  4. 缓冲区管理

二、栈的经典问题解析

2.1 有效的括号(LeetCode 20)

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

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

示例: 输入:s = “()” 输出:true

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

输入:s = “(]” 输出:false

解题思路

这是一个典型的栈应用问题。我们可以使用栈来跟踪括号的匹配情况:

  1. 遍历字符串中的每个字符
  2. 如果是左括号('(''{''['),则将其压入栈中
  3. 如果是右括号(')''}'']'),则检查栈顶元素是否为对应的左括号:
    • 如果是,则弹出栈顶元素,继续遍历
    • 如果不是,或者栈为空,则返回 false
  4. 遍历结束后,如果栈为空,则返回 true;否则,返回 false
代码语言:javascript
复制
public boolean isValid(String s) {
    Deque<Character> stack = new ArrayDeque<>();
    
    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();
}

时间复杂度:O(n),其中 n 是字符串的长度。我们需要遍历字符串中的每个字符。 空间复杂度:O(n),最坏情况下,栈可能需要存储所有的字符(当所有字符都是左括号时)。

2.2 最小栈(LeetCode 155)

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

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

示例: 输入: [“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”] [[],[-2],[0],[-3],[],[],[],[]]

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

解题思路

要在常数时间内检索到最小元素,我们可以使用一个辅助栈来跟踪当前栈中的最小值。辅助栈的栈顶元素始终是当前主栈中的最小元素。

具体操作如下:

  1. 主栈用于正常的 push、pop、top 操作
  2. 辅助栈用于跟踪最小值:
    • push 操作时,如果辅助栈为空或新元素小于等于辅助栈顶元素,则将新元素也压入辅助栈
    • pop 操作时,如果弹出的元素等于辅助栈顶元素,则辅助栈也需要弹出栈顶元素
    • getMin 操作时,直接返回辅助栈顶元素
代码语言:javascript
复制
class MinStack {
    private Deque<Integer> stack;     // 主栈
    private Deque<Integer> minStack;  // 辅助栈,用于跟踪最小值
    
    public MinStack() {
        stack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }
    
    public void push(int val) {
        stack.push(val);
        // 如果辅助栈为空或新元素小于等于辅助栈顶元素,则将新元素压入辅助栈
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }
    
    public void pop() {
        // 如果弹出的元素等于辅助栈顶元素,则辅助栈也需要弹出栈顶元素
        if (stack.peek().equals(minStack.peek())) {
            minStack.pop();
        }
        stack.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

时间复杂度:O(1),所有操作都是常数时间复杂度。 空间复杂度:O(n),最坏情况下,辅助栈可能需要存储所有的元素(当元素按递减顺序压入栈时)。

2.3 柱状图中最大的矩形(LeetCode 84)

题目描述:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例: 输入:heights = [2,1,5,6,2,3] 输出:10 解释:最大的矩形为图中红色区域,面积为 10

解题思路

这道题可以使用单调栈来解决。单调栈是一种特殊的栈,其中的元素保持单调递增或单调递减的顺序。

对于这道题,我们可以使用单调递增栈来寻找每个柱子左边和右边第一个比它小的柱子的位置,这样就可以计算以该柱子为高度的矩形的最大宽度。

具体步骤如下:

  1. 使用一个单调递增栈,栈中存储的是柱子的索引
  2. 遍历每个柱子的高度:
    • 如果当前柱子的高度大于栈顶柱子的高度,则将当前柱子的索引压入栈中
    • 如果当前柱子的高度小于等于栈顶柱子的高度,则弹出栈顶元素,并计算以该元素为高度的矩形的面积
  3. 遍历结束后,处理栈中剩余的元素
代码语言:javascript
复制
public int largestRectangleArea(int[] heights) {
    int n = heights.length;
    if (n == 0) {
        return 0;
    }
    if (n == 1) {
        return heights[0];
    }
    
    int maxArea = 0;
    Deque<Integer> stack = new ArrayDeque<>();
    
    // 遍历所有柱子
    for (int i = 0; i < n; i++) {
        // 当前柱子的高度小于等于栈顶柱子的高度,需要计算面积
        while (!stack.isEmpty() && heights[i] <= heights[stack.peek()]) {
            int h = heights[stack.pop()]; // 当前要计算的柱子的高度
            int w; // 宽度
            if (stack.isEmpty()) {
                w = i; // 如果栈为空,说明左边没有比它小的柱子,宽度为i
            } else {
                w = i - stack.peek() - 1; // 宽度为当前索引减去栈顶索引再减1
            }
            maxArea = Math.max(maxArea, h * w);
        }
        // 将当前柱子的索引压入栈中
        stack.push(i);
    }
    
    // 处理栈中剩余的元素
    while (!stack.isEmpty()) {
        int h = heights[stack.pop()];
        int w;
        if (stack.isEmpty()) {
            w = n;
        } else {
            w = n - stack.peek() - 1;
        }
        maxArea = Math.max(maxArea, h * w);
    }
    
    return maxArea;
}

时间复杂度:O(n),虽然有两层循环,但每个元素最多入栈和出栈一次。 空间复杂度:O(n),最坏情况下,栈可能需要存储所有的元素(当元素按递增顺序排列时)。

2.4 接雨水(LeetCode 42)

题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例: 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

解题思路

这道题可以使用单调栈来解决。我们可以使用单调递减栈来寻找每个凹槽的左右边界,从而计算每个凹槽能接多少雨水。

具体步骤如下:

  1. 使用一个单调递减栈,栈中存储的是柱子的索引
  2. 遍历每个柱子的高度:
    • 如果当前柱子的高度小于等于栈顶柱子的高度,则将当前柱子的索引压入栈中
    • 如果当前柱子的高度大于栈顶柱子的高度,则弹出栈顶元素,并计算以该元素为底部的凹槽能接多少雨水
代码语言:javascript
复制
public int trap(int[] height) {
    int n = height.length;
    if (n <= 2) {
        return 0; // 至少需要3个柱子才能形成凹槽
    }
    
    int totalWater = 0;
    Deque<Integer> stack = new ArrayDeque<>();
    
    // 遍历所有柱子
    for (int i = 0; i < n; i++) {
        // 当前柱子的高度大于栈顶柱子的高度,需要计算能接多少雨水
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
            int bottom = stack.pop(); // 凹槽的底部
            if (stack.isEmpty()) {
                break; // 如果栈为空,说明左边没有柱子,无法形成凹槽
            }
            int left = stack.peek(); // 凹槽的左边界
            int width = i - left - 1; // 凹槽的宽度
            int h = Math.min(height[left], height[i]) - height[bottom]; // 凹槽的高度
            totalWater += width * h; // 计算该凹槽能接的雨水量
        }
        // 将当前柱子的索引压入栈中
        stack.push(i);
    }
    
    return totalWater;
}

时间复杂度:O(n),虽然有两层循环,但每个元素最多入栈和出栈一次。 空间复杂度:O(n),最坏情况下,栈可能需要存储所有的元素(当元素按递减顺序排列时)。

三、队列的经典问题解析

3.1 用栈实现队列(LeetCode 232)

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

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

示例: 输入: [“MyQueue”,“push”,“push”,“peek”,“pop”,“empty”] [[],[1],[2],[],[],[]]

输出: [null,null,null,1,1,false]

解题思路

我们可以使用两个栈来实现队列:一个用于入队操作的栈(inStack),一个用于出队操作的栈(outStack)。

具体操作如下:

  1. push 操作:直接将元素压入 inStack
  2. poppeek 操作:
    • 如果 outStack 为空,则将 inStack 中的所有元素弹出并压入 outStack
    • 然后从 outStack 中弹出或查看栈顶元素
  3. empty 操作:当且仅当两个栈都为空时,队列为空
代码语言:javascript
复制
class MyQueue {
    private Deque<Integer> inStack;  // 用于入队操作的栈
    private Deque<Integer> outStack; // 用于出队操作的栈
    
    public MyQueue() {
        inStack = new ArrayDeque<>();
        outStack = new ArrayDeque<>();
    }
    
    public void push(int x) {
        inStack.push(x);
    }
    
    public int pop() {
        // 确保outStack不为空
        if (outStack.isEmpty()) {
            inToOut();
        }
        return outStack.pop();
    }
    
    public int peek() {
        // 确保outStack不为空
        if (outStack.isEmpty()) {
            inToOut();
        }
        return outStack.peek();
    }
    
    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }
    
    // 将inStack中的所有元素转移到outStack中
    private void inToOut() {
        while (!inStack.isEmpty()) {
            outStack.push(inStack.pop());
        }
    }
}

时间复杂度

  • push 操作:O(1)
  • poppeek 操作:均摊 O(1)。虽然在 inToOut 方法中有一个 O(n) 的循环,但每个元素最多被转移一次
  • empty 操作:O(1)

空间复杂度:O(n),需要存储所有的元素。

3.2 用队列实现栈(LeetCode 225)

题目描述:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的所有操作(push、top、pop、empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶
  • int pop() 移除并返回栈顶元素
  • int top() 返回栈顶元素
  • boolean empty() 如果栈为空,返回 true ;否则,返回 false

示例: 输入: [“MyStack”,“push”,“push”,“top”,“pop”,“empty”] [[],[1],[2],[],[],[]]

输出: [null,null,null,2,2,false]

解题思路

我们可以使用两个队列来实现栈:一个主队列(queue1),一个辅助队列(queue2)。

具体操作如下:

  1. push 操作:
    • 将元素添加到 queue2
    • queue1 中的所有元素出队并添加到 queue2
    • 交换 queue1queue2 的引用
  2. poptop 操作:直接从 queue1 中出队或查看队头元素
  3. empty 操作:当 queue1 为空时,栈为空
代码语言:javascript
复制
class MyStack {
    private Queue<Integer> queue1; // 主队列
    private Queue<Integer> queue2; // 辅助队列
    
    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }
    
    public void push(int x) {
        // 先将元素添加到queue2
        queue2.offer(x);
        // 将queue1中的所有元素添加到queue2
        while (!queue1.isEmpty()) {
            queue2.offer(queue1.poll());
        }
        // 交换queue1和queue2的引用
        Queue<Integer> temp = queue1;
        queue1 = queue2;
        queue2 = temp;
    }
    
    public int pop() {
        return queue1.poll();
    }
    
    public int top() {
        return queue1.peek();
    }
    
    public boolean empty() {
        return queue1.isEmpty();
    }
}

时间复杂度

  • push 操作:O(n),需要将 queue1 中的所有元素转移到 queue2
  • poptop 操作:O(1)
  • empty 操作:O(1)

空间复杂度:O(n),需要存储所有的元素。

3.3 滑动窗口最大值(LeetCode 239)

题目描述:给你一个整数数组 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. 使用一个单调递减队列,队列中存储的是元素的索引
  2. 遍历数组中的每个元素:
    • 移除队列中所有小于当前元素的索引(因为它们不可能是当前或未来滑动窗口的最大值)
    • 将当前元素的索引添加到队列中
    • 移除队列中所有不在当前滑动窗口范围内的索引
    • 如果已经形成了一个完整的滑动窗口,则将队列头部的元素(即当前滑动窗口的最大值)添加到结果数组中
代码语言:javascript
复制
public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    if (n == 0 || k == 0) {
        return new int[0];
    }
    
    int[] result = new int[n - k + 1];
    Deque<Integer> deque = new ArrayDeque<>(); // 单调递减队列,存储的是元素的索引
    
    for (int i = 0; i < n; i++) {
        // 移除队列中所有小于当前元素的索引
        while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
            deque.pollLast();
        }
        
        // 将当前元素的索引添加到队列中
        deque.offerLast(i);
        
        // 移除队列中所有不在当前滑动窗口范围内的索引
        while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
            deque.pollFirst();
        }
        
        // 如果已经形成了一个完整的滑动窗口,则记录当前的最大值
        if (i >= k - 1) {
            result[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    
    return result;
}

时间复杂度:O(n),虽然有两层循环,但每个元素最多入队和出队一次。 空间复杂度:O(k),队列中最多存储 k 个元素。

3.4 设计循环队列(LeetCode 622)

题目描述:设计你的循环队列实现。循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为"环形缓冲器"。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k) :构造器,设置队列长度为 k 。
  • Front :从队首获取元素。如果队列为空,返回 -1 。
  • Rear :获取队尾元素。如果队列为空,返回 -1 。
  • enQueue(value) :向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue() :从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty() :检查循环队列是否为空。
  • isFull() :检查循环队列是否已满。

示例

代码语言:javascript
复制
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1);  // 返回 true
circularQueue.enQueue(2);  // 返回 true
circularQueue.enQueue(3);  // 返回 true
circularQueue.enQueue(4);  // 返回 false,队列已满
circularQueue.Rear();      // 返回 3
circularQueue.isFull();    // 返回 true
circularQueue.deQueue();   // 返回 true
circularQueue.enQueue(4);  // 返回 true
circularQueue.Rear();      // 返回 4
解题思路

我们可以使用数组来实现循环队列。为了区分队列是空还是满的状态,我们可以使用以下两种方法之一:

  1. 使用一个额外的布尔变量来表示队列是空还是满
  2. 预留一个位置,当队尾指针的下一个位置是队首指针时,队列已满

这里我们使用第二种方法。具体实现如下:

  1. 使用一个大小为 k+1 的数组来存储元素
  2. 使用两个指针:front 指向队首元素的位置,rear 指向队尾元素的下一个位置
  3. front == rear 时,队列为空
  4. (rear + 1) % capacity == front 时,队列已满
代码语言:javascript
复制
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指向的是队尾元素的下一个位置,所以需要减1
        int realRear = (rear - 1 + capacity) % capacity;
        return data[realRear];
    }
    
    public boolean isEmpty() {
        return front == rear;
    }
    
    public boolean isFull() {
        return (rear + 1) % capacity == front;
    }
}

时间复杂度:所有操作都是 O(1)。 空间复杂度:O(k),需要一个大小为 k+1 的数组。

四、栈与队列的进阶问题

4.1 字符串解码(LeetCode 394)

题目描述:给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a2[4] 的输入。

示例: 输入:s = “3[a]2[bc]” 输出:“aaabcbc”

输入:s = “3[a2[c]]” 输出:“accaccacc”

解题思路

这道题可以使用栈来解决。我们需要两个栈:一个用于存储重复次数,一个用于存储字符串。

具体步骤如下:

  1. 遍历字符串中的每个字符:
    • 如果是数字,则解析完整的数字,并将其压入数字栈
    • 如果是字母,则将其添加到当前正在构建的字符串中
    • 如果是左括号 [,则将当前正在构建的字符串压入字符串栈,并重置当前字符串
    • 如果是右括号 ],则从字符串栈中弹出一个字符串,从数字栈中弹出一个数字,将当前字符串重复该数字次,然后将结果追加到弹出的字符串后面,并将结果设置为当前字符串
代码语言:javascript
复制
public String decodeString(String s) {
    Deque<Integer> numStack = new ArrayDeque<>(); // 存储重复次数
    Deque<StringBuilder> strStack = new ArrayDeque<>(); // 存储字符串
    StringBuilder currentStr = new StringBuilder(); // 当前正在构建的字符串
    int num = 0; // 当前的重复次数
    
    for (char c : s.toCharArray()) {
        if (Character.isDigit(c)) {
            // 解析数字
            num = num * 10 + (c - '0');
        } else if (c == '[') {
            // 遇到左括号,将当前的重复次数和字符串压入栈中
            numStack.push(num);
            strStack.push(currentStr);
            // 重置当前的重复次数和字符串
            num = 0;
            currentStr = new StringBuilder();
        } else if (c == ']') {
            // 遇到右括号,需要解码
            StringBuilder prevStr = strStack.pop(); // 弹出之前的字符串
            int repeat = numStack.pop(); // 弹出重复次数
            // 将当前字符串重复repeat次,并追加到prevStr后面
            for (int i = 0; i < repeat; i++) {
                prevStr.append(currentStr);
            }
            // 更新当前字符串
            currentStr = prevStr;
        } else {
            // 遇到字母,直接添加到当前字符串中
            currentStr.append(c);
        }
    }
    
    return currentStr.toString();
}

时间复杂度:O(n),其中 n 是解码后字符串的长度。在最坏情况下,如 2[2[2[a]]],解码后的字符串长度是指数级的,但每个字符最多被处理一次。 空间复杂度:O(n),需要存储解码过程中的中间状态。

4.2 简化路径(LeetCode 71)

题目描述:给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格绝对路径(以 '/' 开头),请你将其转化为更加简洁的规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。

示例: 输入:path = “/home/” 输出:“/home”

输入:path = “/…/” 输出:“/”

输入:path = “/home//foo/” 输出:“/home/foo”

输入:path = “/a/./b/…/…/c/” 输出:“/c”

解题思路

这道题可以使用栈来解决。我们首先将路径按照 '/' 分割成多个部分,然后遍历这些部分,根据不同的情况进行处理。

具体步骤如下:

  1. 将路径按照 '/' 分割成多个部分
  2. 创建一个栈来存储有效的目录名
  3. 遍历分割后的每个部分:
    • 如果是空字符串或 '.',则忽略(空字符串是由于多个连续的 '/' 导致的)
    • 如果是 '..',则从栈中弹出一个元素(如果栈不为空)
    • 否则,将该部分压入栈中
  4. 最后,将栈中的元素用 '/' 连接起来,并在开头加上 '/'
代码语言:javascript
复制
public String simplifyPath(String path) {
    // 将路径按照'/'分割成多个部分
    String[] components = path.split("/");
    Deque<String> stack = new ArrayDeque<>();
    
    for (String component : components) {
        // 忽略空字符串和'.'
        if (component.isEmpty() || component.equals(".")) {
            continue;
        }
        // 如果是'..',则从栈中弹出一个元素(如果栈不为空)
        if (component.equals("..")) {
            if (!stack.isEmpty()) {
                stack.pop();
            }
        } else {
            // 否则,将该部分压入栈中
            stack.push(component);
        }
    }
    
    // 将栈中的元素用'/'连接起来
    StringBuilder result = new StringBuilder();
    for (String dir : stack) {
        result.insert(0, "/" + dir);
    }
    
    // 如果结果为空,则返回"/"
    return result.length() > 0 ? result.toString() : "/";
}

时间复杂度:O(n),其中 n 是路径的长度。我们需要遍历路径中的每个字符,并对分割后的每个部分进行处理。 空间复杂度:O(n),最坏情况下,栈可能需要存储所有的目录名。

4.3 基本计算器 II(LeetCode 227)

题目描述:给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

整数除法仅保留整数部分。

示例: 输入:s = “3+2*2” 输出:7

输入:s = " 3/2 " 输出:1

输入:s = " 3+5 / 2 " 输出:5

解题思路

这道题可以使用栈来解决。我们可以遍历字符串,依次处理每个字符,并使用栈来存储中间结果。

具体步骤如下:

  1. 初始化一个栈,一个变量 num 用于存储当前正在解析的数字,一个变量 sign 用于存储当前的运算符(初始为 ‘+’)
  2. 遍历字符串中的每个字符:
    • 如果是数字,则更新 num
    • 如果是运算符或到达字符串末尾,则根据 sign 的值对 num 进行相应的操作,并将结果压入栈中,然后更新 signnum
  3. 最后,将栈中的所有元素相加,得到最终结果
代码语言:javascript
复制
public int calculate(String s) {
    Deque<Integer> stack = new ArrayDeque<>();
    int num = 0;
    char sign = '+'; // 初始运算符为'+'
    
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        
        // 如果是数字,更新num
        if (Character.isDigit(c)) {
            num = num * 10 + (c - '0');
        }
        
        // 如果是运算符或到达字符串末尾,处理之前的数字
        if ((!Character.isDigit(c) && c != ' ') || i == s.length() - 1) {
            switch (sign) {
                case '+':
                    stack.push(num);
                    break;
                case '-':
                    stack.push(-num);
                    break;
                case '*':
                    stack.push(stack.pop() * num);
                    break;
                case '/':
                    stack.push(stack.pop() / num);
                    break;
            }
            // 更新运算符和数字
            sign = c;
            num = 0;
        }
    }
    
    // 计算栈中所有元素的和
    int result = 0;
    while (!stack.isEmpty()) {
        result += stack.pop();
    }
    
    return result;
}

时间复杂度:O(n),其中 n 是表达式的长度。我们需要遍历表达式中的每个字符。 空间复杂度:O(n),最坏情况下,栈可能需要存储所有的操作数。

4.4 数据流的中位数(LeetCode 295)

题目描述:中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如:

  • [2,3,4] 的中位数是 3
  • [2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。

示例

代码语言:javascript
复制
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
解题思路

这道题可以使用两个堆来解决:一个最大堆(maxHeap)用于存储较小的一半元素,一个最小堆(minHeap)用于存储较大的一半元素。

具体操作如下:

  1. addNum 操作:
    • 如果 maxHeap 的大小等于 minHeap 的大小,则将新元素先添加到 maxHeap,然后将 maxHeap 的堆顶元素添加到 minHeap
    • 如果 maxHeap 的大小小于 minHeap 的大小,则将新元素先添加到 minHeap,然后将 minHeap 的堆顶元素添加到 maxHeap
  2. findMedian 操作:
    • 如果 maxHeap 的大小等于 minHeap 的大小,则中位数是两个堆顶元素的平均值
    • 如果 maxHeap 的大小小于 minHeap 的大小,则中位数是 minHeap 的堆顶元素
代码语言:javascript
复制
class MedianFinder {
    private PriorityQueue<Integer> maxHeap; // 存储较小的一半元素
    private PriorityQueue<Integer> minHeap; // 存储较大的一半元素
    
    public MedianFinder() {
        // 最大堆,默认是最小堆,所以需要传递一个比较器
        maxHeap = new PriorityQueue<>((a, b) -> b - a);
        // 最小堆
        minHeap = new PriorityQueue<>();
    }
    
    public void addNum(int num) {
        if (maxHeap.size() == minHeap.size()) {
            // 先添加到maxHeap,然后将maxHeap的堆顶元素添加到minHeap
            maxHeap.offer(num);
            minHeap.offer(maxHeap.poll());
        } else {
            // 先添加到minHeap,然后将minHeap的堆顶元素添加到maxHeap
            minHeap.offer(num);
            maxHeap.offer(minHeap.poll());
        }
    }
    
    public double findMedian() {
        if (maxHeap.size() == minHeap.size()) {
            // 两个堆大小相等,中位数是两个堆顶元素的平均值
            return (maxHeap.peek() + minHeap.peek()) / 2.0;
        } else {
            // 两个堆大小不相等,中位数是minHeap的堆顶元素
            return minHeap.peek();
        }
    }
}

时间复杂度

  • addNum 操作:O(log n),因为堆的插入和删除操作的时间复杂度是 O(log n)
  • findMedian 操作:O(1),因为堆的 peek 操作的时间复杂度是 O(1)

空间复杂度:O(n),需要存储所有的元素。

五、栈与队列的优化技巧总结

5.1 栈的优化技巧
  1. 单调栈:单调栈是一种特殊的栈,其中的元素保持单调递增或单调递减的顺序。单调栈常用于解决以下类型的问题:
    • 寻找每个元素左边或右边第一个比它大或小的元素
    • 计算柱状图中最大的矩形面积
    • 接雨水问题
  2. 辅助栈:辅助栈常用于优化一些需要额外信息的问题,如:
    • 最小栈问题:使用辅助栈跟踪当前栈中的最小值
    • 字符串解码问题:使用两个栈分别存储重复次数和字符串
  3. 栈的应用场景扩展:除了基本的先进后出操作外,栈还可以用于:
    • 递归的非递归实现
    • 表达式求值和转换
    • 括号匹配问题
5.2 队列的优化技巧
  1. 单调队列:单调队列是一种特殊的队列,其中的元素保持单调递增或单调递减的顺序。单调队列常用于解决滑动窗口最大值等问题。
  2. 优先队列:优先队列(也称为堆)是一种特殊的队列,其中的元素按照一定的优先级进行排序。优先队列常用于解决以下类型的问题:
    • 寻找第 k 大的元素
    • 数据流的中位数
    • 任务调度问题
  3. 循环队列:循环队列是一种特殊的队列,其队尾被连接在队首之后以形成一个循环。循环队列的主要优点是可以利用队列之前用过的空间。
5.3 栈与队列的转换
  1. 用栈实现队列:可以使用两个栈来实现队列,一个用于入队操作,一个用于出队操作。
  2. 用队列实现栈:可以使用两个队列来实现栈,一个主队列,一个辅助队列。
5.4 常见陷阱与注意事项
  1. 栈溢出:在使用栈时,需要注意避免栈溢出的问题,特别是在递归调用中。
  2. 空栈/队列检查:在进行栈的 pop 或 peek 操作,以及队列的 poll 或 peek 操作之前,一定要检查栈/队列是否为空,避免 NullPointerException。
  3. 单调栈/队列的维护:在使用单调栈或单调队列时,需要注意正确维护栈/队列的单调性。
  4. 循环队列的实现:在实现循环队列时,需要注意正确处理队首和队尾指针的更新,以及判断队列是空还是满的条件。

六、总结与展望

栈与队列是两种基础但重要的数据结构,它们在算法面试中经常被考察。通过本文的学习,我们了解了栈与队列的基本概念、常见操作和应用场景,并通过具体的LeetCode题目深入理解了这些数据结构的实际应用。

栈遵循后进先出(LIFO)原则,常用于解决括号匹配、表达式求值、单调栈等问题。队列遵循先进先出(FIFO)原则,常用于解决广度优先搜索、滑动窗口、优先队列等问题。此外,栈与队列之间还可以相互转换,用栈实现队列或用队列实现栈。

在实际应用中,我们需要根据问题的特点选择合适的数据结构,并掌握一些常见的优化技巧,如单调栈、单调队列、优先队列等。同时,我们也需要注意避免一些常见的陷阱,如栈溢出、空栈/队列检查等。

随着计算机科学的发展,栈与队列的应用也在不断扩展。例如,在系统设计中,栈可以用于实现函数调用、撤销操作等功能;队列可以用于实现消息队列、任务调度等功能。在人工智能领域,栈可以用于实现深度优先搜索;队列可以用于实现广度优先搜索。

通过不断地学习和实践,我们可以更好地理解和应用栈与队列,为解决实际问题提供有力的支持。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-12,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 一、栈与队列基础概念
    • 1.1 栈的数据结构定义
    • 1.2 队列的数据结构定义
    • 1.3 栈与队列的应用场景
  • 二、栈的经典问题解析
    • 2.1 有效的括号(LeetCode 20)
      • 解题思路
    • 2.2 最小栈(LeetCode 155)
      • 解题思路
    • 2.3 柱状图中最大的矩形(LeetCode 84)
      • 解题思路
    • 2.4 接雨水(LeetCode 42)
      • 解题思路
  • 三、队列的经典问题解析
    • 3.1 用栈实现队列(LeetCode 232)
      • 解题思路
    • 3.2 用队列实现栈(LeetCode 225)
      • 解题思路
    • 3.3 滑动窗口最大值(LeetCode 239)
      • 解题思路
    • 3.4 设计循环队列(LeetCode 622)
      • 解题思路
  • 四、栈与队列的进阶问题
    • 4.1 字符串解码(LeetCode 394)
      • 解题思路
    • 4.2 简化路径(LeetCode 71)
      • 解题思路
    • 4.3 基本计算器 II(LeetCode 227)
      • 解题思路
    • 4.4 数据流的中位数(LeetCode 295)
      • 解题思路
  • 五、栈与队列的优化技巧总结
    • 5.1 栈的优化技巧
    • 5.2 队列的优化技巧
    • 5.3 栈与队列的转换
    • 5.4 常见陷阱与注意事项
  • 六、总结与展望
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档