队列(queue)是一种遵循先入先出规则的线性数据结构。
队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。

队列常用操作
队列实现
基于链表的实现
链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。
/* 基于链表实现的队列 */
class LinkedListQueue {
#front; // 头节点 #front
#rear; // 尾节点 #rear
#queSize = 0;
constructor() {
this.#front = null;
this.#rear = null;
}
/* 获取队列的长度 */
get size() {
return this.#queSize;
}
/* 判断队列是否为空 */
isEmpty() {
return this.size === 0;
}
/* 入队 */
push(num) {
// 在尾节点后添加 num
const node = new ListNode(num);
// 如果队列为空,则令头、尾节点都指向该节点
if (!this.#front) {
this.#front = node;
this.#rear = node;
// 如果队列不为空,则将该节点添加到尾节点后
} else {
this.#rear.next = node;
this.#rear = node;
}
this.#queSize++;
}
/* 出队 */
pop() {
const num = this.peek();
// 删除头节点
this.#front = this.#front.next;
this.#queSize--;
return num;
}
/* 访问队首元素 */
peek() {
if (this.size === 0) throw new Error('队列为空');
return this.#front.val;
}
/* 将链表转化为 Array 并返回 */
toArray() {
let node = this.#front;
const res = new Array(this.size);
for (let i = 0; i < res.length; i++) {
res[i] = node.val;
node = node.next;
}
return res;
}
}
基于数组的实现
在数组中删除首元素的时间复杂度为 ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题
使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。
- 入队操作:将输入元素赋值给
rear索引处,并将size增加 1 。 - 出队操作:只需将
front增加 1 ,并将size减少 1 。
在不断进行入队和出队的过程中,front 和 rear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。
对于环形数组,我们需要让 front 或 rear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,
/* 基于环形数组实现的队列 */
class ArrayQueue {
#nums; // 用于存储队列元素的数组
#front = 0; // 队首指针,指向队首元素
#queSize = 0; // 队列长度
constructor(capacity) {
this.#nums = new Array(capacity);
}
/* 获取队列的容量 */
get capacity() {
return this.#nums.length;
}
/* 获取队列的长度 */
get size() {
return this.#queSize;
}
/* 判断队列是否为空 */
isEmpty() {
return this.#queSize === 0;
}
/* 入队 */
push(num) {
if (this.size === this.capacity) {
console.log('队列已满');
return;
}
// 计算队尾指针,指向队尾索引 + 1
// 通过取余操作实现 rear 越过数组尾部后回到头部
const rear = (this.#front + this.size) % this.capacity;
// 将 num 添加至队尾
this.#nums[rear] = num;
this.#queSize++;
}
/* 出队 */
pop() {
const num = this.peek();
// 队首指针向后移动一位,若越过尾部,则返回到数组头部
this.#front = (this.#front + 1) % this.capacity;
this.#queSize--;
return num;
}
/* 访问队首元素 */
peek() {
if (this.isEmpty()) throw new Error('队列为空');
return this.#nums[this.#front];
}
/* 返回 Array */
toArray() {
// 仅转换有效长度范围内的列表元素
const arr = new Array(this.size);
for (let i = 0, j = this.#front; i < this.size; i++, j++) {
arr[i] = this.#nums[j % this.capacity];
}
return arr;
}
}
队列典型应用
- 淘宝订单。购物者下单后,订单将加入队列中,系统随后会根据顺序处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。
- 各类待办事项。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等,队列在这些场景中可以有效地维护处理顺序。
- 打印队列:计算机打印多个文件的时候,需要排队打印。
- 线程队列:当开启多线程时,当新开启的线程所需的资源不足时就先放入线程队列,等待 CPU 处理。
击鼓传花
分析:传入一组数据集合和设定的数字 number,循环遍历数组内元素,遍历到的元素为指定数字 number 时将该元素删除,直至数组剩下一个元素。
// 利用队列结构的特点实现击鼓传花游戏求解方法的封装
function passGame(nameList, number) {
// 1、new 一个 Queue 对象
const queue = new Queue();
// 2、将 nameList 里面的每一个元素入队
for (const name of nameList) {
queue.enqueue(name);
}
// 3、开始数数
// 队列中只剩下 1 个元素时就停止数数
while (queue.size() > 1) {
// 不是 number 时,重新加入到队尾
// 是 number 时,将其删除
for (let i = 0; i < number - 1; i++) {
// number 数字之前的人重新放入到队尾(即把队头删除的元素,重新加入到队列中)
queue.enqueue(queue.dequeue());
}
// number 对应这个人,直接从队列中删除
// 由于队列没有像数组一样的下标值不能直接取到某一元素,
// 所以采用,把 number 前面的 number - 1 个元素先删除后添加到队列末尾,
// 这样第 number 个元素就排到了队列的最前面,可以直接使用 dequeue 方法进行删除
queue.dequeue();
}
// 4、获取最后剩下的那个人
const endName = queue.front();
// 5、返回这个人在原数组中对应的索引
return nameList.indexOf(endName);
}
双向队列
在队列中,我们仅能删除头部元素或在尾部添加元素。双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
双向队列常用操作
双向队列实现
基于双向链表的实现
双向链表的头节点和尾节点视为双向队列的队首和队尾,同时实现在两端添加和删除节点的功能。
/* 双向链表节点 */
class ListNode {
prev; // 前驱节点引用 (指针)
next; // 后继节点引用 (指针)
val; // 节点值
constructor(val) {
this.val = val;
this.next = null;
this.prev = null;
}
}
/* 基于双向链表实现的双向队列 */
class LinkedListDeque {
#front; // 头节点 front
#rear; // 尾节点 rear
#queSize; // 双向队列的长度
constructor() {
this.#front = null;
this.#rear = null;
this.#queSize = 0;
}
/* 队尾入队操作 */
pushLast(val) {
const node = new ListNode(val);
// 若链表为空,则令 front 和 rear 都指向 node
if (this.#queSize === 0) {
this.#front = node;
this.#rear = node;
} else {
// 将 node 添加至链表尾部
this.#rear.next = node;
node.prev = this.#rear;
this.#rear = node; // 更新尾节点
}
this.#queSize++;
}
/* 队首入队操作 */
pushFirst(val) {
const node = new ListNode(val);
// 若链表为空,则令 front 和 rear 都指向 node
if (this.#queSize === 0) {
this.#front = node;
this.#rear = node;
} else {
// 将 node 添加至链表头部
this.#front.prev = node;
node.next = this.#front;
this.#front = node; // 更新头节点
}
this.#queSize++;
}
/* 队尾出队操作 */
popLast() {
if (this.#queSize === 0) {
return null;
}
const value = this.#rear.val; // 存储尾节点值
// 删除尾节点
let temp = this.#rear.prev;
if (temp !== null) {
temp.next = null;
this.#rear.prev = null;
}
this.#rear = temp; // 更新尾节点
this.#queSize--;
return value;
}
/* 队首出队操作 */
popFirst() {
if (this.#queSize === 0) {
return null;
}
const value = this.#front.val; // 存储尾节点值
// 删除头节点
let temp = this.#front.next;
if (temp !== null) {
temp.prev = null;
this.#front.next = null;
}
this.#front = temp; // 更新头节点
this.#queSize--;
return value;
}
/* 访问队尾元素 */
peekLast() {
return this.#queSize === 0 ? null : this.#rear.val;
}
/* 访问队首元素 */
peekFirst() {
return this.#queSize === 0 ? null : this.#front.val;
}
/* 获取双向队列的长度 */
size() {
return this.#queSize;
}
/* 判断双向队列是否为空 */
isEmpty() {
return this.#queSize === 0;
}
/* 打印双向队列 */
print() {
const arr = [];
let temp = this.#front;
while (temp !== null) {
arr.push(temp.val);
temp = temp.next;
}
console.log('[' + arr.join(', ') + ']');
}
}
基于数组的实现
与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列
/* 基于环形数组实现的双向队列 */
class ArrayDeque {
#nums; // 用于存储双向队列元素的数组
#front; // 队首指针,指向队首元素
#queSize; // 双向队列长度
/* 构造方法 */
constructor(capacity) {
this.#nums = new Array(capacity);
this.#front = 0;
this.#queSize = 0;
}
/* 获取双向队列的容量 */
capacity() {
return this.#nums.length;
}
/* 获取双向队列的长度 */
size() {
return this.#queSize;
}
/* 判断双向队列是否为空 */
isEmpty() {
return this.#queSize === 0;
}
/* 计算环形数组索引 */
index(i) {
// 通过取余操作实现数组首尾相连
// 当 i 越过数组尾部后,回到头部
// 当 i 越过数组头部后,回到尾部
return (i + this.capacity()) % this.capacity();
}
/* 队首入队 */
pushFirst(num) {
if (this.#queSize === this.capacity()) {
console.log('双向队列已满');
return;
}
// 队首指针向左移动一位
// 通过取余操作实现 front 越过数组头部后回到尾部
this.#front = this.index(this.#front - 1);
// 将 num 添加至队首
this.#nums[this.#front] = num;
this.#queSize++;
}
/* 队尾入队 */
pushLast(num) {
if (this.#queSize === this.capacity()) {
console.log('双向队列已满');
return;
}
// 计算队尾指针,指向队尾索引 + 1
const rear = this.index(this.#front + this.#queSize);
// 将 num 添加至队尾
this.#nums[rear] = num;
this.#queSize++;
}
/* 队首出队 */
popFirst() {
const num = this.peekFirst();
// 队首指针向后移动一位
this.#front = this.index(this.#front + 1);
this.#queSize--;
return num;
}
/* 队尾出队 */
popLast() {
const num = this.peekLast();
this.#queSize--;
return num;
}
/* 访问队首元素 */
peekFirst() {
if (this.isEmpty()) throw new Error('The Deque Is Empty.');
return this.#nums[this.#front];
}
/* 访问队尾元素 */
peekLast() {
if (this.isEmpty()) throw new Error('The Deque Is Empty.');
// 计算尾元素索引
const last = this.index(this.#front + this.#queSize - 1);
return this.#nums[last];
}
/* 返回数组用于打印 */
toArray() {
// 仅转换有效长度范围内的列表元素
const res = [];
for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {
res[i] = this.#nums[this.index(j)];
}
return res;
}
}
双向队列应用
双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。
我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push 到栈中,然后通过 pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 10 步)。当栈的长度超过 10 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。
优先队列的应用
生活中类似优先队列的场景:
- 优先排队的人,优先处理。 (买票、结账、WC)。
- 排队中,有紧急情况(特殊情况)的人可优先处理。
优先级队列主要考虑的问题:
- 每个元素不再只是一个数据,还包含优先级。
- 在添加元素过程中,根据优先级放入到正确位置。
优先队列的实现
// 优先队列内部的元素类
class QueueElement {
constructor(element, priority) {
this.element = element;
this.priority = priority;
}
}
// 优先队列类(继承 Queue 类)
export class PriorityQueue extends Queue {
constructor() {
super();
}
// enqueue(element, priority) 入队,将元素按优先级加入到队列中
// 重写 enqueue()
enqueue(element, priority) {
// 根据传入的元素,创建 QueueElement 对象
const queueElement = new QueueElement(element, priority);
// 判断队列是否为空
if (this.isEmpty()) {
// 如果为空,不用判断优先级,直接添加
this.items.push(queueElement);
} else {
// 定义一个变量记录是否成功添加了新元素
let added = false;
for (let i = 0; i < this.items.length; i++) {
// 让新插入的元素进行优先级比较,priority 值越小,优先级越大
if (queueElement.priority < this.items[i].priority) {
// 在指定的位置插入元素
this.items.splice(i, 0, queueElement);
added = true;
break;
}
}
// 如果遍历完所有元素,优先级都大于新插入的元素,就将新插入的元素插入到最后
if (!added) {
this.items.push(queueElement);
}
}
}
// dequeue() 出队,从队列中删除前端元素,返回删除的元素
// 继承 Queue 类的 dequeue()
dequeue() {
return super.dequeue();
}
// front() 查看队列的前端元素
// 继承 Queue 类的 front()
front() {
return super.front();
}
// isEmpty() 查看队列是否为空
// 继承 Queue 类的 isEmpty()
isEmpty() {
return super.isEmpty();
}
// size() 查看队列中元素的个数
// 继承 Queue 类的 size()
size() {
return super.size();
}
// toString() 将队列中元素以字符串形式返回
// 重写 toString()
toString() {
let result = "";
for (let item of this.items) {
result += item.element + "-" + item.priority + " ";
}
return result;
}
}
测试代码
const priorityQueue = new PriorityQueue();
// 入队 enqueue() 测试
priorityQueue.enqueue("A", 10);
priorityQueue.enqueue("B", 15);
priorityQueue.enqueue("C", 11);
priorityQueue.enqueue("D", 20);
priorityQueue.enqueue("E", 18);
console.log(priorityQueue.items);
//--> output:
// QueueElement {element: "A", priority: 10}
// QueueElement {element: "C", priority: 11}
// QueueElement {element: "B", priority: 15}
// QueueElement {element: "E", priority: 18}
// QueueElement {element: "D", priority: 20}
// 出队 dequeue() 测试
priorityQueue.dequeue();
priorityQueue.dequeue();
console.log(priorityQueue.items);
//--> output:
// QueueElement {element: "B", priority: 15}
// QueueElement {element: "E", priority: 18}
// QueueElement {element: "D", priority: 20}
// isEmpty() 测试
console.log(priorityQueue.isEmpty()); //--> false
// size() 测试
console.log(priorityQueue.size()); //--> 3
// toString() 测试
console.log(priorityQueue.toString()); //--> B-15 E-18 D-20