队列是计算机科学中最基础的线性数据结构之一,核心遵循 先进先出(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); // []
二、经典题解 1:用栈实现队列(LeetCode 232)
这是队列的入门级经典题,要求仅使用两个栈实现队列的所有操作,核心考察栈(先进后出)和队列(先进先出)的特性转换,也是面试中常考的基础题。
1. 题目要求
实现一个队列,支持push(入队)、pop(出队)、peek(查看队头)、empty(判断为空)四个操作,且只能使用栈的push和pop方法。
2. 解题思路
栈是先进后出,队列是先进先出,直接用一个栈无法实现队列,因此我们需要两个栈配合:
-
入队栈(stackIn):专门负责接收新入队的元素,所有
push操作都往这个栈里加。 -
出队栈(stackOut):专门负责提供出队的元素。当需要出队 / 查看队头时,如果
stackOut为空,就把stackIn中的所有元素依次弹出并压入stackOut—— 此时stackOut的元素顺序会和原队列一致,从stackOut弹出元素就相当于队列的出队操作。
简单来说:用两个栈实现 “倒序”,把先进后出转换成先进先出。
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());
4. 复杂度分析
-
时间复杂度:
push和empty是 O (1);pop和peek是均摊 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. 解题思路:单调递减队列
单调队列的核心是维护一个队列,队列中的元素对应的数组值是单调递减的,且队列中存储的是元素的下标(方便判断元素是否在窗口内)。具体操作步骤:
-
入队维护单调性:当新元素进入窗口时,从队列尾部开始,移除所有比当前元素小的元素下标 —— 因为这些元素不可能成为后续窗口的最大值(当前元素更大,且在窗口中更靠后)。
-
入队当前元素下标:将当前元素的下标加入队列尾部。
-
移除窗口外的元素:检查队列头部的下标是否超出当前窗口的范围(即下标 ≤ 当前索引 - k),如果超出则从头部移除。
-
记录窗口最大值:当遍历的索引 ≥ 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 题目解析了队列的基础用法和进阶技巧(单调队列),核心要点总结:
-
队列的核心是先进先出,基础操作是
push(队尾入)和shift(队头出)。 -
LeetCode 232 用两个栈实现队列,核心是通过栈的 “倒序” 将先进后出转换为先进先出。
-
LeetCode 239 用单调递减队列解决滑动窗口最大值,核心是维护队列的单调性,快速找到窗口最大值,将时间复杂度优化到 O (n)。