队列进阶实战:栈模拟实现 + 滑动窗口最大值

82 阅读7分钟

队列是计算机科学中最基础的线性数据结构之一,核心遵循 先进先出(FIFO)原则,在算法题、实际开发(如任务调度、消息队列)中都有广泛应用。本文会从队列的基础概念讲起,结合代码示例解析队列的基本操作,再通过 LeetCode 232(用栈实现队列)和 LeetCode 239(滑动窗口最大值)两道经典题目,带你吃透队列的核心用法 —— 尤其是单调队列这种进阶技巧,全程通俗易懂,搭配图示辅助理解。

一、队列的核心概念

队列就像日常生活中排队买奶茶的队伍:先排队的人先买到奶茶离开(先入队的元素先出队),后排队的人只能在队尾等待(后入队的元素在队尾),这就是 先进先出(First In First Out,FIFO) 的核心特性。

1. 队列的基础操作

在 JavaScript 中,我们通常用数组模拟队列,核心只用到两个方法:

  • push:往队列尾部添加元素(入队),相当于新顾客排到队尾。

  • shift:从队列头部移除并返回元素(出队),相当于队头的顾客买完离开。

2. 基础代码示例

// 用数组模拟队列

const queue = [];

// 入队:往队尾添加元素

queue.push("珍珠奶茶");

queue.push("芋泥波波");

queue.push("杨枝甘露");

console.log("入队后队列:", queue); // ['珍珠奶茶', '芋泥波波', '杨枝甘露'](队头是珍珠奶茶)

// 出队:从队头移除元素,直到队列为空

while (queue.length > 0) {

 const item = queue.shift();

 console.log("出队元素:", item);

}

// 输出顺序:珍珠奶茶 → 芋泥波波 → 杨枝甘露(符合先进先出)

console.log("出队后队列:", queue); // []

ScreenShot_2025-11-23_223621_140.png

二、经典题解 1:用栈实现队列(LeetCode 232)

这是队列的入门级经典题,要求仅使用两个栈实现队列的所有操作,核心考察栈(先进后出)和队列(先进先出)的特性转换,也是面试中常考的基础题。

1. 题目要求

实现一个队列,支持push(入队)、pop(出队)、peek(查看队头)、empty(判断为空)四个操作,且只能使用栈的pushpop方法。

2. 解题思路

栈是先进后出,队列是先进先出,直接用一个栈无法实现队列,因此我们需要两个栈配合

  • 入队栈(stackIn:专门负责接收新入队的元素,所有push操作都往这个栈里加。

  • 出队栈(stackOut:专门负责提供出队的元素。当需要出队 / 查看队头时,如果stackOut为空,就把stackIn中的所有元素依次弹出并压入stackOut—— 此时stackOut的元素顺序会和原队列一致,从stackOut弹出元素就相当于队列的出队操作。

简单来说:用两个栈实现 “倒序”,把先进后出转换成先进先出

ScreenShot_2025-11-23_224653_086.png

3. 代码实现

var MyQueue = function () {
  this.stack1 = []
  this.stack2 = []
};

MyQueue.prototype.push = function (x) {
  this.stack1.push(x)
};

MyQueue.prototype.pop = function () {
  // 栈 1 倒到栈 2,再从栈 2 取值
  if (this.stack2.length == 0) {
    while(this.stack1.length > 0) {
      const top = this.stack1.pop()
      this.stack2.push(top)
    }
  }
  
  return this.stack2.pop()
};

MyQueue.prototype.peek = function () {
  if (this.stack2.length == 0) {
    while(this.stack1.length > 0) {
      const top = this.stack1.pop()
      this.stack2.push(top)
    }
  }
  return this.stack2[this.stack2.length - 1]
};

MyQueue.prototype.empty = function () {
  return !this.stack1.length && !this.stack2.length
};

const myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)

// console.log(myQueue.peek());
console.log(myQueue.pop());
console.log(myQueue.pop());

ScreenShot_2025-11-23_222348_055.png

4. 复杂度分析

  • 时间复杂度pushempty是 O (1);poppeek均摊 O (1)(每个元素最多被转移一次,后续操作直接从stackOut取)。

  • 空间复杂度:O (n),需要两个栈存储 n 个元素。

三、经典题解 2:滑动窗口最大值(LeetCode 239)

这道题是队列的进阶应用,难度为中等,核心考察单调队列(单调递减队列)的用法。如果用暴力解法会超时,而单调队列能把时间复杂度优化到 O (n),是处理滑动窗口问题的经典技巧。

1. 题目要求

给你一个整数数组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

2. 暴力解法的问题

暴力解法的思路很简单:遍历每个滑动窗口,逐个比较找出最大值。但时间复杂度是 O (n*k)(n 是数组长度,k 是窗口大小),当 n 和 k 很大时(比如 n=10^5,k=10^4),会直接超时。因此我们需要更高效的方法 ——单调队列

3. 解题思路:单调递减队列

单调队列的核心是维护一个队列,队列中的元素对应的数组值是单调递减的,且队列中存储的是元素的下标(方便判断元素是否在窗口内)。具体操作步骤:

  1. 入队维护单调性:当新元素进入窗口时,从队列尾部开始,移除所有比当前元素小的元素下标 —— 因为这些元素不可能成为后续窗口的最大值(当前元素更大,且在窗口中更靠后)。

  2. 入队当前元素下标:将当前元素的下标加入队列尾部。

  3. 移除窗口外的元素:检查队列头部的下标是否超出当前窗口的范围(即下标 ≤ 当前索引 - k),如果超出则从头部移除。

  4. 记录窗口最大值:当遍历的索引 ≥ k-1(窗口大小达到 k)时,队列头部的下标对应的元素就是当前窗口的最大值,加入结果数组。

4. 代码实现

let nums = [7, 2, 4], k = 2;

var maxSlidingWindow = function (nums, k) {
  const len = nums.length
  const res = []
  const dque = []  // 希望是一个递减的队列
  for (let i = 0; i < len; i++) {
    // 一个值进队列之前,得先将队列中从后往前依次比较,比我小的全退掉
    while (dque.length && nums[dque[dque.length - 1]] < nums[i]) {
      dque.pop()
    }

    dque.push(i) // 不存当前的值,而是存当前值的下标

    // 当大哥从窗户左侧出去
    while(dque.length && dque[0] <= i - k) {
      dque.shift()
    }

    if (i >= k - 1) {  // 开始找大哥
      res.push(nums[dque[0]])
    }

  }

  return res
};

5. 测试示例 & 分步解析

用示例nums = [1,3,-1,-3,5,3,6,7], k = 3分步解析:

  • i=0(元素 1):队列为空,push (0) → deque=[0]。i<2(k-1=2),不记录结果。

  • i=1(元素 3):nums [0]=1 < 3,pop (0) → deque 为空,push (1) → deque=[1]。i<2,不记录结果。

  • i=2(元素 - 1):nums [1]=3 > -1,push (2) → deque=[1,2]。i≥2,记录 nums [1]=3 → res=[3]。

  • i=3(元素 - 3):nums [2]=-1 > -3,push (3) → deque=[1,2,3]。检查队头 1 ≤ 3-3=0?否。记录 nums [1]=3 → res=[3,3]。

  • i=4(元素 5):nums [3]=-3 <5 → pop (3);nums [2]=-1 <5 → pop (2);nums [1]=3 <5 → pop (1);push (4) → deque=[4]。检查队头 4 ≤4-3=1?否。记录 nums [4]=5 → res=[3,3,5]。

  • 后续步骤:依次处理 i=5 到 i=7,最终 res=[3,3,5,5,6,7],与示例一致。

7. 复杂度分析

  • 时间复杂度:O (n),每个元素最多入队和出队各一次,没有嵌套循环。

  • 空间复杂度:O (k),队列中最多存储 k 个元素的下标(窗口大小)。

四、总结

本文从队列的基础概念出发,讲解了队列的核心操作,再通过两道经典 LeetCode 题目解析了队列的基础用法和进阶技巧(单调队列),核心要点总结:

  1. 队列的核心是先进先出,基础操作是push(队尾入)和shift(队头出)。

  2. LeetCode 232 用两个栈实现队列,核心是通过栈的 “倒序” 将先进后出转换为先进先出。

  3. LeetCode 239 用单调递减队列解决滑动窗口最大值,核心是维护队列的单调性,快速找到窗口最大值,将时间复杂度优化到 O (n)。