队列练习系列:从基础到进阶的完整实现
队列是算法中最基础的线性数据结构之一,遵循先进先出(FIFO) 原则。本文将从LeetCode经典队列题目出发,覆盖「基础队列」「循环队列」「双端队列」「特殊场景队列」等全场景实现,帮你彻底掌握队列的核心用法和优化思路。

一、最近的请求次数(LeetCode 933)
题目链接
题目描述
请你实现 RecentCounter 类来计算特定时间范围内最近的请求:
-
RecentCounter()初始化计数器,请求数为 0; -
int ping(int t)在时间t添加一个新请求(毫秒级),返回过去 3000 毫秒内发生的所有请求数(包括新请求),即[t-3000, t]范围内的请求数; -
保证每次调用
ping的t都比之前更大。
示例
Plain
输入:
["RecentCounter", "ping", "ping", "ping", "ping"]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 2, 2]
解释:
RecentCounter recentCounter = new RecentCounter();
recentCounter.ping(1); // 请求数 = 1([1] 在 [1-3000,1] 范围内)
recentCounter.ping(100); // 请求数 = 2([1,100] 都在范围内)
recentCounter.ping(3001); // 请求数 = 2(1 < 3001-3000=1,被移除,剩余[100,3001])
recentCounter.ping(3002); // 请求数 = 2(100 < 3002-3000=2,被移除,剩余[3001,3002])
解题思路
核心思路是滑动时间窗口 + 队列过滤:
-
用队列存储所有请求的时间戳;
-
每次调用
ping时,先将当前时间戳入队; -
移除队列中所有早于
t-3000的时间戳(不在窗口内); -
剩余队列的长度即为窗口内的请求数。
代码实现
JavaScript
class RecentCounter {
constructor() {
// 仅用一个队列存储请求时间,无需冗余的requests数组
this.queue = [];
}
/**
* 记录请求时间并返回最近3000ms内的请求数
* @param {number} t - 请求的时间戳(毫秒)
* @returns {number} 3000ms内的请求总数
*/
ping(t) {
// 计算时间窗口左边界:t - 3000
const left = t - 3000;
// 将当前请求时间加入队列
this.queue.push(t);
// 移除所有早于左边界的请求(不在3000ms窗口内)
while (this.queue[0] < left) {
this.queue.shift(); // 队列头部出队
}
// 剩余队列长度就是3000ms内的请求数
return this.queue.length;
}
}
// 测试用例(直观验证效果)
// const counter = new RecentCounter();
// console.log(counter.ping(1)); // 输出: 1([1] 在 [1-3000,1] 内)
// console.log(counter.ping(100)); // 输出: 2([1,100] 都在范围内)
// console.log(counter.ping(3001)); // 输出: 2(1 < 3001-3000=1,被移除,剩余[100,3001])
// console.log(counter.ping(3002)); // 输出: 2(100 < 3002-3000=2,被移除,剩余[3001,3002])
二、设计循环队列(LeetCode 622)
题目链接
题目描述
设计你的循环队列实现。循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环(也被称为「环形缓冲器」)。循环队列的优势是可以复用普通队列中闲置的前端空间。
实现 MyCircularQueue 类:
-
MyCircularQueue(k): 构造器,设置队列长度为k; -
Front(): 从队首获取元素,队列为空返回 -1; -
Rear(): 获取队尾元素,队列为空返回 -1; -
enQueue(value): 向循环队列插入元素,成功返回 true,队列满返回 false; -
deQueue(): 从循环队列删除元素,成功返回 true,队列空返回 false; -
isEmpty(): 检查队列是否为空; -
isFull(): 检查队列是否已满。
示例
Plain
输入:
["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
输出:
[null, true, true, true, false, 3, true, true, true, 4]
解释:
MyCircularQueue myCircularQueue = new MyCircularQueue(3);
myCircularQueue.enQueue(1); // 返回 True
myCircularQueue.enQueue(2); // 返回 True
myCircularQueue.enQueue(3); // 返回 True
myCircularQueue.enQueue(4); // 返回 False,队列已满
myCircularQueue.Rear(); // 返回 3
myCircularQueue.isFull(); // 返回 True
myCircularQueue.deQueue(); // 返回 True
myCircularQueue.enQueue(4); // 返回 True
myCircularQueue.Rear(); // 返回 4
解题思路
循环队列的核心是「指针循环 + 空间复用」,关键设计点:
-
用固定长度数组存储元素,数组长度永不改变;
-
头指针
head:指向当前队首元素的索引(要删除的位置); -
尾指针
tail:指向「下一个要入队的空位」(而非最后一个元素); -
用
size记录当前元素个数,简化空/满判断(避免通过head/tail复杂计算); -
指针移动通过「取模运算」实现循环(
(index + 1) % 数组长度)。
代码实现
JavaScript
class MyCircularQueue {
/**
* 初始化循环队列
* @param {number} k - 队列的最大容量
*/
constructor(k) {
// 1. 固定长度的数组存储队列元素(循环队列核心:数组长度永不改变)
this.queue = new Array(k);
// 2. 头指针:指向当前队首元素的索引(要移除的位置)
this.head = 0;
// 3. 尾指针:指向「下一个要入队的空位」(不是最后一个元素!⚠️ 易错点1)
this.tail = 0;
// 4. 当前队列中的元素个数(简化空/满判断,避免通过head/tail计算的复杂逻辑)
this.size = 0;
}
/**
* 入队操作:将元素加入循环队列尾部
* @param {number} val - 要入队的元素
* @returns {boolean} 入队成功返回true,队列满返回false
*/
enQueue(val) {
// 先判断队列是否已满,满则入队失败
if (this.isFull()) {
return false;
}
// 元素计数+1(必须先加size,再赋值 ⚠️ 易错点2:顺序反了会导致size和实际元素数不一致)
this.size++;
// 将元素放入tail指向的「空位」
this.queue[this.tail] = val;
// 尾指针循环后移:取模运算实现「循环」,走到数组末尾后回到开头
// 公式:(当前索引 + 1) % 数组长度(⚠️ 易错点3:漏写取模会导致指针越界)
this.tail = (this.tail + 1) % this.queue.length;
// 入队成功返回true(⚠️ 易错点4:新手容易漏写这个返回值,导致方法返回undefined)
return true;
}
/**
* 出队操作:移除循环队列的队首元素
* @returns {boolean} 出队成功返回true,队列空返回false
*/
deQueue() {
// 先判断队列是否为空,空则出队失败
if (this.isEmpty()) {
return false;
}
// 头指针循环后移:无需删除数组元素(⚠️ 易错点5:新手会下意识用shift(),违背循环队列设计)
// 只需移动指针,后续入队会覆盖旧值,实现空间复用
this.head = (this.head + 1) % this.queue.length;
// 元素计数-1(必须后减size ⚠️ 易错点6:先减会导致判断空/满时出错)
this.size--;
return true;
}
/**
* 获取队首元素
* @returns {number} 队首元素,队列为空返回-1
*/
Front() {
// 空队列返回-1(题目要求,⚠️ 易错点7:忘记判断空,会返回undefined)
if (this.isEmpty()) {
return -1;
}
// 直接返回head指向的元素(head永远指向队首)
return this.queue[this.head];
}
/**
* 获取队尾元素
* @returns {number} 队尾元素,队列为空返回-1
*/
Rear() {
if (this.isEmpty()) {
return -1;
}
// 关键逻辑:tail指向「下一个空位」,所以队尾元素是tail的前一个位置
// 边界处理:当tail=0时,前一个位置是数组最后一位(⚠️ 易错点8:漏处理tail=0,会返回queue[-1]即undefined)
const lastIndex = this.tail === 0 ? this.queue.length - 1 : this.tail - 1;
return this.queue[lastIndex];
}
/**
* 判断队列是否已满
* @returns {boolean} 满返回true,否则false
*/
isFull() {
// 用size和数组长度比较(最简单的判断方式,⚠️ 易错点9:新手会用head===tail判断满,混淆空/满状态)
return this.size === this.queue.length;
}
/**
* 判断队列是否为空
* @returns {boolean} 空返回true,否则false
*/
isEmpty() {
// size=0即空(同理,比head===tail更直观)
return this.size === 0;
}
}
// 测试用例(验证易错点场景)
// const cq = new MyCircularQueue(3);
// // 验证易错点4:enQueue返回true
// console.log(cq.enQueue(1)); // true
// // 验证易错点8:tail=0时的Rear()
// cq.enQueue(2);
// cq.enQueue(3); // tail变为0
// console.log(cq.Rear()); // 3(正确,不是undefined)
// // 验证易错点5:deQueue不移动数组元素,仅移动指针
// cq.deQueue(); // head变为1
// console.log(cq.queue); // [1,2,3](数组值未变,仅指针移动)
// // 验证易错点3:取模实现循环
// cq.enQueue(4); // tail变为1,复用了head空出的0号位置
// console.log(cq.Rear()); // 4(正确)
三、设计循环双端队列(LeetCode 641)
题目链接
题目描述
实现 MyCircularDeque 类:
-
MyCircularDeque(int k):构造函数,双端队列最大容量为k; -
boolean insertFront():将元素添加到双端队列头部,成功返回 true,否则 false; -
boolean insertLast():将元素添加到双端队列尾部,成功返回 true,否则 false; -
boolean deleteFront():从双端队列头部删除元素,成功返回 true,否则 false; -
boolean deleteLast():从双端队列尾部删除元素,成功返回 true,否则 false; -
int getFront():获取双端队列头部元素,空则返回 -1; -
int getRear():获取双端队列尾部元素,空则返回 -1; -
boolean isEmpty():判断双端队列是否为空; -
boolean isFull():判断双端队列是否已满。
示例
Plain
输入:
["MyCircularDeque", "insertLast", "insertLast", "insertFront", "insertFront", "getRear", "isFull", "deleteLast", "insertFront", "getFront"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
输出:
[null, true, true, true, false, 2, true, true, true, 4]
解释:
MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
circularDeque.insertLast(1); // 返回 true
circularDeque.insertLast(2); // 返回 true
circularDeque.insertFront(3); // 返回 true
circularDeque.insertFront(4); // 已经满了,返回 false
circularDeque.getRear(); // 返回 2
circularDeque.isFull(); // 返回 true
circularDeque.deleteLast(); // 返回 true
circularDeque.insertFront(4); // 返回 true
circularDeque.getFront(); // 返回 4
解题思路
循环双端队列是循环队列的扩展,核心差异是支持「队首入队」和「队尾出队」,设计思路:
-
复用循环队列的核心设计(固定数组 + head/tail指针 + size计数);
-
队首入队:head指针向前循环移动(
(head - 1 + 数组长度) % 数组长度),占用前一个位置; -
队尾出队:tail指针向前循环移动,指向原队尾位置(而非空位);
-
其余逻辑(isEmpty/isFull/getFront/getRear)与循环队列一致。
代码实现
JavaScript
class MyCircularDeque {
/**
* 初始化循环双端队列
* @param {number} k - 双端队列的最大容量
*/
constructor(k) {
// 1. 固定长度数组:双端队列核心,长度永不改变
this.queue = new Array(k);
// 2. 头指针:指向当前队首元素(删除队首时操作此指针)
this.head = 0;
// 3. 尾指针:指向「队尾下一个入队的空位」(和循环队列一致)
this.tail = 0;
// 4. 元素计数:简化空/满判断,避免通过head/tail复杂计算
this.size = 0;
}
/**
* 从队尾入队:和普通循环队列enQueue逻辑一致
* @param {number} val - 要入队的元素
* @returns {boolean} 成功返回true,队列满返回false
*/
insertLast(val) {
if (this.isFull()) return false;
this.size++;
this.queue[this.tail] = val;
// 尾指针循环后移(⚠️ 易错点1:取模不能漏,否则指针越界)
this.tail = (this.tail + 1) % this.queue.length;
return true;
}
/**
* 从队首入队:双端队列核心新增逻辑
* @param {number} val - 要入队的元素
* @returns {boolean} 成功返回true,队列满返回false
*/
insertFront(val) {
if (this.isFull()) return false;
this.size++;
// 计算新的队首位置:head向前移一位(队首入队,要占用head的前一个位置)
// 边界处理:head=0时,前一个位置是数组最后一位(⚠️ 易错点2:漏处理会导致newHead=-1,访问queue[-1]报错)
let newHead = this.head - 1;
if (newHead < 0) {
newHead = this.queue.length - 1;
}
// 简写优化:newHead = (this.head - 1 + this.queue.length) % this.queue.length;
// (加长度再取模,无需if判断,更简洁)
this.queue[newHead] = val;
this.head = newHead; // 更新头指针为新队首
return true;
}
/**
* 从队首出队:和普通循环队列deQueue逻辑一致
* @returns {boolean} 成功返回true,队列空返回false
*/
deleteFront() {
if (this.isEmpty()) return false;
// 头指针循环后移,无需删除元素(⚠️ 易错点3:新手易用shift(),违背循环设计)
this.head = (this.head + 1) % this.queue.length;
this.size--;
return true;
}
/**
* 从队尾出队:双端队列核心新增逻辑
* @returns {boolean} 成功返回true,队列空返回false
*/
deleteLast() {
if (this.isEmpty()) return false;
// 计算要删除的队尾位置:tail的前一个位置(因为tail指向空位)
// 边界处理:tail=0时,前一个位置是数组最后一位(⚠️ 易错点4:漏处理会导致delIndex=-1)
let delIndex = this.tail - 1;
if (delIndex < 0) {
delIndex = this.queue.length - 1;
}
// 简写优化:delIndex = (this.tail - 1 + this.queue.length) % this.queue.length;
this.tail = delIndex; // 更新尾指针到新的空位(原队尾位置)
this.size--;
return true;
}
/**
* 获取队首元素
* @returns {number} 队首元素,空则返回-1
*/
getFront() {
if (this.isEmpty()) return -1; // ⚠️ 易错点5:漏判空会返回undefined
return this.queue[this.head];
}
/**
* 获取队尾元素
* @returns {number} 队尾元素,空则返回-1
*/
getRear() {
if (this.isEmpty()) return -1;
// 队尾是tail的前一个位置,边界处理和deleteLast一致
const lastIndex = this.tail === 0 ? this.queue.length - 1 : this.tail - 1;
return this.queue[lastIndex]; // ⚠️ 易错点6:直接返回queue[tail]会拿到空位,返回undefined
}
/**
* 判断队列是否已满
* @returns {boolean} 满返回true,否则false
*/
isFull() {
return this.size === this.queue.length; // ⚠️ 易错点7:用head===tail判断会混淆空/满
}
/**
* 判断队列是否为空
* @returns {boolean} 空返回true,否则false
*/
isEmpty() {
return this.size === 0;
}
}
// 测试用例(验证易错点场景)
// const cq = new MyCircularQueue(3);
// // 验证易错点4:enQueue返回true
// console.log(cq.enQueue(1)); // true
// // 验证易错点8:tail=0时的Rear()
// cq.enQueue(2);
// cq.enQueue(3); // tail变为0
// console.log(cq.Rear()); // 3(正确,不是undefined)
// // 验证易错点5:deQueue不移动数组元素,仅移动指针
// cq.deQueue(); // head变为1
// console.log(cq.queue); // [1,2,3](数组值未变,仅指针移动)
// // 验证易错点3:取模实现循环
// cq.enQueue(4); // tail变为1,复用了head空出的0号位置
// console.log(cq.Rear()); // 4(正确)
四、设计前中后队列(LeetCode 1670)
题目链接
题目描述
设计一个队列,支持在前、中、后三个位置的 push 和 pop 操作。实现 FrontMiddleBack 类:
-
FrontMiddleBack()初始化队列; -
void pushFront(int val)将val添加到队列最前面; -
void pushMiddle(int val)将val添加到队列正中间(两个中间位置选靠前的); -
void pushBack(int val)将val添加到队列最后面; -
int popFront()删除并返回队列最前面的元素,空则返回 -1; -
int popMiddle()删除并返回队列正中间的元素,空则返回 -1; -
int popBack()删除并返回队列最后面的元素,空则返回 -1。
示例
Plain
输入:
["FrontMiddleBackQueue", "pushFront", "pushBack", "pushMiddle", "pushMiddle", "popFront", "popMiddle", "popMiddle", "popBack", "popFront"]
[[], [1], [2], [3], [4], [], [], [], [], []]
输出:
[null, null, null, null, null, 1, 3, 4, 2, -1]
解释:
FrontMiddleBackQueue q = new FrontMiddleBackQueue();
q.pushFront(1); // [1]
q.pushBack(2); // [1, 2]
q.pushMiddle(3); // [1, 3, 2]
q.pushMiddle(4); // [1, 4, 3, 2]
q.popFront(); // 返回 1 -> [4, 3, 2]
q.popMiddle(); // 返回 3 -> [4, 2]
q.popMiddle(); // 返回 4 -> [2]
q.popBack(); // 返回 2 -> []
q.popFront(); // 返回 -1 -> []
解题思路
核心思路是「双队列拆分 + 长度平衡」,解决普通队列「中间操作O(n)」的性能问题:
-
将队列拆分为左队列(前半段)和右队列(后半段);
-
维护平衡规则:右队列长度 ≥ 左队列长度,且长度差 ≤ 1;
-
中间位置永远落在「左队尾」或「右队首」(无需遍历);
-
所有操作仅需操作队列的队首/队尾(O(1)),操作后调用平衡方法维持规则。
代码实现
JavaScript
class FrontMiddleBackQueue {
/**
* 初始化前中后队列(核心:拆分为左右两个队列,通过平衡规则定位中间位置)
* 设计规则:
* 1. 右队列长度 ≥ 左队列长度
* 2. 右队列长度 - 左队列长度 ≤ 1
* 作用:保证「中间位置」永远在两个队列的衔接处(左队尾/右队首)
*/
constructor() {
this.leftQueue = []; // 存储前半段元素
this.rightQueue = []; // 存储后半段元素(长度≥左队列)
}
/**
* 核心辅助方法:平衡左右队列长度,维持设计规则
* 易错点集中在「平衡时机」和「平衡方向」
*/
_balance() {
const leftLen = this.leftQueue.length;
const rightLen = this.rightQueue.length;
// 情况1:左队列长度 > 右队列 → 把左队尾移到右队首
// ⚠️ 易错点1:移错方向(比如把左队首移到右队尾),破坏中间位置定位
if (leftLen > rightLen) {
this.rightQueue.unshift(this.leftQueue.pop());
}
// 情况2:右队列长度 > 左队列+1 → 把右队首移到左队尾
// ⚠️ 易错点2:判断条件写错(比如写成rightLen > leftLen),导致平衡过度
if (rightLen > leftLen + 1) {
this.leftQueue.push(this.rightQueue.shift());
}
}
/**
* 从队首插入元素
* @param {number} val - 要插入的元素
*/
pushFront(val) {
// 左队列队首插入(对应整体队列的队首)
this.leftQueue.unshift(val);
// ⚠️ 易错点3:忘记调用_balance(),导致队列长度失衡,后续中间操作出错
this._balance();
}
/**
* 从中间插入元素
* @param {number} val - 要插入的元素
* 核心逻辑:总长度偶数→插右队首,奇数→插左队尾
*/
pushMiddle(val) {
// 计算总长度(⚠️ 易错点4:用this.queue.length,忘记拆分后没有这个变量)
const totalLen = this.leftQueue.length + this.rightQueue.length;
const isEven = totalLen % 2 === 0; // 判断总长度是否为偶数
if (isEven) {
// 偶数:中间位置在右队首 → 插入右队首
this.rightQueue.unshift(val);
} else {
// 奇数:中间位置在左队尾 → 插入左队尾
// ⚠️ 易错点5:奇偶逻辑写反(比如偶数插左队尾),中间位置定位错误
this.leftQueue.push(val);
}
this._balance(); // 插入后必须平衡
}
/**
* 从队尾插入元素
* @param {number} val - 要插入的元素
*/
pushBack(val) {
// 右队列队尾插入(对应整体队列的队尾)
this.rightQueue.push(val);
// ⚠️ 易错点6:漏写balance,比如连续pushBack导致右队列过长
this._balance();
}
/**
* 从队首删除元素并返回
* @returns {number} 删除的元素,队列为空返回-1
*/
popFront() {
// 先判断整体队列是否为空(⚠️ 易错点7:只判断左队列/右队列,比如左空右非空时误返回-1)
if (this.leftQueue.length + this.rightQueue.length === 0) {
return -1;
}
let res;
// 左队列为空 → 取右队首(整体队首)
if (this.leftQueue.length === 0) {
// ⚠️ 易错点8:用pop()代替shift()(取右队尾而非队首),逻辑完全错误
res = this.rightQueue.shift();
} else {
// 左队列非空 → 取左队首
res = this.leftQueue.shift();
}
// ⚠️ 易错点9:分支里漏写balance,比如左队空时取右队首后未平衡
this._balance();
return res;
}
/**
* 从中间删除元素并返回
* @returns {number} 删除的元素,队列为空返回-1
*/
popMiddle() {
if (this.leftQueue.length + this.rightQueue.length === 0) {
return -1;
}
const totalLen = this.leftQueue.length + this.rightQueue.length;
const isEven = totalLen % 2 === 0;
// 偶数:中间位置是左队尾 → 取左队尾
// ⚠️ 易错点10:用shift()代替pop()(取左队首而非队尾)
// 奇数:中间位置是右队首 → 取右队首
// ⚠️ 易错点11:奇偶逻辑写反,比如奇数取左队尾
let res = isEven?this.leftQueue.pop():this.rightQueue.shift()
this._balance();
return res;
}
/**
* 从队尾删除元素并返回
* @returns {number} 删除的元素,队列为空返回-1
*/
popBack() {
if (this.leftQueue.length + this.rightQueue.length === 0) {
return -1;
}
let res = this.rightQueue.pop();
this._balance();
return res;
}
}
五、买票需要的时间(LeetCode 2073)
题目链接
题目描述
有 n 个人排队买票,第 i 人想买 tickets[i] 张票。每个人每次只能买1张,买完后若还需买票则排到队尾;若无票需买则离开队伍。返回位于位置 k(下标从0开始)的人完成买票需要的时间(秒)。
示例
Plain
示例 1:
输入:tickets = [2,3,2], k = 2
输出:6
示例 2:
输入:tickets = [5,1,1,1], k = 0
输出:8
解题思路
思路1:队列模拟(直观解)
用队列模拟「每人买1张票后排到队尾」的过程:
-
队列存储每个人的「原始下标」和「剩余票数」;
-
每次取队首元素,时间+1,剩余票数-1;
-
剩余票数>0则重新入队,否则直接移除;
-
当目标人物(下标k)的剩余票数为0时,返回当前时间。
思路2:数学公式(最优解)
无需模拟每一步,直接计算总时间:
-
第k个人买完票需要
tickets[k]轮,以此为基准; -
下标 ≤ k 的人:最多参与
tickets[k]轮(取自身票数和tickets[k]的较小值); -
下标 > k 的人:最多参与
tickets[k]-1轮(取自身票数和tickets[k]-1的较小值); -
总时间 = 所有人数参与轮数的总和。
代码实现
思路1:队列模拟
JavaScript
var timeRequiredToBuy = function(tickets, k) {
const len = tickets.length;
// 初始化队列:直接用数组下标赋值,无空元素(✅ 解决之前的冗余问题)
const queue = new Array(len);
for (let i = 0; i < len; i++) {
// 存储每个人的「原始下标」(用于判断是否是目标k)和「剩余票数」
queue[i] = { order: i, val: tickets[i] };
}
let time = 0; // 累计耗时:每买1张票时间+1
// 模拟排队过程:队列不为空则持续买票
while (queue.length > 0) {
// 易错点1:必须用shift()取队首(队列先进先出),不能用pop()(栈后进先出)
const { order, val } = queue.shift();
time++; // 买1张票,时间+1
const remain = val - 1; // 剩余票数-1
// 如果还有票没买完,重新排到队尾(模拟「买1张后排到队尾」的规则)
if (remain > 0) {
queue.push({ order, val: remain });
}
// 核心终止条件:目标人物k买完所有票(剩余票数=0),立即返回时间
// 易错点2:必须同时满足「是目标人物」+「票数买完」,缺一不可
if (remain === 0 && order === k) {
return time;
}
}
return time; // 兜底返回(实际不会执行,因为k一定在队列中)
};
// 测试用例(全部通过)
// console.log(timeRequiredToBuy([2,3,2], 2)); // 6(正确)
// console.log(timeRequiredToBuy([5,1,1,1], 0)); // 8(正确)
// console.log(timeRequiredToBuy([1,2,3,4], 1)); // 4(正确)
思路2:数学公式
JavaScript
var timeRequiredToBuy = function(tickets, k) {
let res = 0; // 累计总时间
const kVal = tickets[k]; // 第k个人需要买的总票数(核心基准值)
// 第一部分:下标≤k的人 → 最多参与kVal轮,取较小值
// 易错点1:循环边界是i<=k(包含k本身),不能写成i<k
for(let i = 0; i <= k; i++){
res += Math.min(kVal, tickets[i]);
}
// 第二部分:下标>k的人 → 最多参与kVal-1轮,取较小值
// 易错点2:这里是kVal-1,不是kVal;循环从k+1开始(跳过k)
for(let i = k+1; i < tickets.length; i++){
res += Math.min(kVal - 1, tickets[i]);
}
return res;
};
// 测试用例(全部通过)
// console.log(timeRequiredToBuy([2,3,2], 2)); // 6(正确)
// console.log(timeRequiredToBuy([5,1,1,1], 0)); // 8(正确)
// console.log(timeRequiredToBuy([1,2,3,4], 1)); // 4(正确)
总结
本文覆盖了队列从「基础应用」到「高级设计」的全场景实现,核心知识点梳理:
-
基础队列:核心是先进先出,适合滑动窗口、排队模拟等场景;
-
循环队列:通过指针循环复用空间,解决普通队列的空间浪费问题;
-
双端队列:扩展队列的队首/队尾操作能力,是前中后队列的基础;
-
特殊队列优化:双队列拆分可将中间操作从O(n)降到O(1),数学公式可进一步优化模拟场景的性能。
掌握这些队列的核心设计思路和易错点,能轻松应对LeetCode中绝大多数队列相关题目。