队列是一种遵循先进先出(FIFO,First-In-First-Out) 原则的元素集合。这意味着最早添加的元素会最先被移除,就像超市排队结账时,顾客按到达顺序依次被服务一样。
Queue(队列)
尾部(Back) ← 队首(Front)
入队(enqueue) 出队(dequeue)
在这篇实操教程中,你将学习如何使用链表在 TypeScript 中实现队列。以下是我们将要覆盖的内容:
要学习本教程,你需要具备以下基础:
本教程提供了一个实操项目,帮助你逐步实现队列并跟进学习。请按以下步骤开始:
.
├── index.ts // 入口文件
├── examples // 示例目录(存放各队列的最终实现)
│ ├── 01-linked-list.ts // 链表示例
│ ├── 02-simple-queue.ts // 简单队列示例
│ ├── 03-circular-queue.ts // 循环队列示例
│ ├── 04-double-ended-queue.ts // 双端队列示例
│ └── 05-priority-queue.ts // 优先队列示例
└── playground // 实操目录(用于自己实现代码)
├── 01-linked-list.ts // 链表实操文件
├── 02-simple-queue.ts // 简单队列实操文件
├── 03-circular-queue.ts // 循环队列实操文件
├── 04-double-ended-queue.ts // 双端队列实操文件
└── 05-priority-queue.ts // 优先队列实操文件
队列是一种以先进先出(FIFO) 顺序管理元素的数据结构——最早进入队列的元素会最早被取出。
生活中的队列示例
打印机处理任务时,如果你发送 3 个文档打印,打印机将按接收顺序依次处理:第一个文档先打印,然后是第二个,最后是第三个。
编程中的队列应用
队列常用于需要按顺序处理任务的场景,例如:
队列的核心操作
所有队列都包含一组基础操作,本教程将重点实现以下常用操作:
实现队列的方式有多种,但本教程将使用基于链表的队列——因为链表在“尾部插入”和“头部删除”这两个队列核心操作上效率极高(时间复杂度为O(1)),无需像数组那样移动元素。
接下来,我们先简要了解链表的基础知识,再开始实现队列。
链表是一种存储元素的方式:每个元素(称为“节点”)包含两部分——实际数据和指向后一个节点的引用(或指针)。
与数组不同(数组元素在内存中连续存储),链表通过引用将节点连接成链状结构。
为什么用循环双向链表?
本教程将使用循环双向链表(Circular Doubly Linked List) 实现队列,它的特点是:
这种结构的优势在于:
已提供的循环双向链表实现
教程已在src/playground/01-linked-list.ts中提供了循环双向链表的代码,你可以直接使用。以下是核心代码及说明:
// 📁 src/playground/01-linked-list.ts
/**
* 双向链表的节点类
*/
export class NodeItem<T> {
value: T;
next: NodeItem<T> | null = null; // 指向后一个节点
prev: NodeItem<T> | null = null; // 指向前一个节点
constructor(value: T) {
this.value = value;
}
}
/**
* 循环双向链表类
*/
export class LinkedList<T> {
private head: NodeItem<T> | null = null; // 头节点
private tail: NodeItem<T> | null = null; // 尾节点
private currentSize: number = 0; // 当前节点数量
/**
* 向链表头部添加节点
* @param value 要添加的值
*/
prepend(value: T): void { /* 实现略 */ }
/**
* 向链表尾部添加节点
* @param value 要添加的值
*/
append(value: T): void { /* 实现略 */ }
/**
* 移除并返回头部节点的值
* @returns 头节点的值,为空时返回undefined
*/
deleteHead(): T | undefined { /* 实现略 */ }
/**
* 移除并返回尾部节点的值
* @returns 尾节点的值,为空时返回undefined
*/
deleteTail(): T | undefined { /* 实现略 */ }
/**
* 查看头部节点的值(不移除)
* @returns 头节点的值,为空时返回undefined
*/
getHead(): T | undefined { /* 实现略 */ }
/**
* 查看尾部节点的值(不移除)
* @returns 尾节点的值,为空时返回undefined
*/
getTail(): T | undefined { /* 实现略 */ }
/**
* 检查链表是否为空
* @returns 为空返回true,否则返回false
*/
isEmpty(): boolean { /* 实现略 */ }
/**
* 获取链表当前大小
* @returns 节点数量
*/
size(): number { /* 实现略 */ }
}
该链表提供了 8 个核心方法,恰好满足队列实现的需求。接下来,我们开始实现第一个队列——简单队列。
简单队列是最基础的队列类型,严格遵循 FIFO 原则:只能从尾部添加元素,从头部移除元素,就像火车站售票窗口前的队伍。
简单队列的实现
打开src/playground/02-simple-queue.ts,按以下代码实现简单队列(核心是复用上面的循环双向链表):
// 📁 src/playground/02-simple-queue.ts
import { LinkedList } from "./01-linked-list";
/**
* 基于循环双向链表实现的简单队列
*/
export class SimpleQueue<T> {
private list: LinkedList<T>; // 用链表存储队列元素
private maxSize?: number; // 可选的最大容量(不设置则无上限)
/**
* 构造函数
* @param maxSize 可选,队列的最大容量
*/
constructor(maxSize?: number) {
this.list = new LinkedList<T>();
this.maxSize = maxSize;
}
/**
* 入队:将元素添加到队列尾部
* @param item 要添加的元素
*/
enqueue(item: T): void {
if (this.isFull()) {
throw new Error("队列已满");
}
this.list.append(item); // 复用链表的append方法(添加到尾部)
}
/**
* 出队:移除并返回队列头部元素
* @returns 队首元素,为空时返回undefined
*/
dequeue(): T | undefined {
return this.list.deleteHead(); // 复用链表的deleteHead方法(删除头部)
}
/**
* 获取队首元素(不移除)
* @returns 队首元素,为空时返回undefined
*/
getFront(): T | undefined {
return this.list.getHead();
}
/**
* 获取队尾元素(不移除)
* @returns 队尾元素,为空时返回undefined
*/
getRear(): T | undefined {
return this.list.getTail();
}
/**
* 检查队列是否为空
* @returns 为空返回true,否则返回false
*/
isEmpty(): boolean {
return this.list.isEmpty();
}
/**
* 检查队列是否已满
* @returns 已满返回true,否则返回false
*/
isFull(): boolean {
return this.maxSize !== undefined && this.list.size() >= this.maxSize;
}
/**
* 预览队首元素(与getFront功能相同)
* @returns 队首元素,为空时返回undefined
*/
peek(): T | undefined {
return this.getFront();
}
/**
* 获取队列当前大小
* @returns 元素数量
*/
size(): number {
return this.list.size();
}
}
简单队列的工作原理
由于复用了链表的方法,简单队列的实现非常简洁,核心逻辑如下:
测试简单队列
实现完成后,在项目根目录运行以下命令测试代码是否正确:
npm run test:file 02
如果测试失败,可以参考src/examples/02-simple-queue.ts中的最终代码调试。
循环队列是一种固定容量的队列,尾部元素与头部元素“相连”形成闭环——当头部元素被移除后,其空间可以被新元素复用,就像自助餐台上循环补充的餐盘。
循环队列与简单队列的区别
循环队列的核心特点是必须指定最大容量(简单队列的最大容量是可选的),因此更适合需要控制内存或资源的场景(如实时系统、缓存缓冲区)。
循环队列的实现
打开src/playground/03-circular-queue.ts,实现代码如下:
// 📁 src/playground/03-circular-queue.ts
import { LinkedList } from "./01-linked-list";
/**
* 基于循环双向链表实现的循环队列
*/
export class CircularQueue<T> {
private list: LinkedList<T>;
private maxSize: number; // 循环队列必须有固定最大容量
/**
* 构造函数(必须传入最大容量)
* @param maxSize 队列的最大容量
*/
constructor(maxSize: number) {
this.list = new LinkedList<T>();
this.maxSize = maxSize;
}
/**
* 入队:添加元素到尾部
* @param item 要添加的元素
*/
enqueue(item: T): void {
if (this.isFull()) {
throw new Error("循环队列已满");
}
this.list.append(item);
}
/**
* 出队:移除并返回头部元素
* @returns 队首元素,为空时返回undefined
*/
dequeue(): T | undefined {
return this.list.deleteHead();
}
// 以下方法与SimpleQueue一致,略去注释
getFront(): T | undefined { return this.list.getHead(); }
getRear(): T | undefined { return this.list.getTail(); }
isEmpty(): boolean { return this.list.isEmpty(); }
isFull(): boolean { return this.list.size() >= this.maxSize; }
peek(): T | undefined { return this.getFront(); }
size(): number { return this.list.size(); }
}
循环队列的核心差异
与简单队列相比,循环队列只有 2 个关键区别:
测试循环队列
运行以下命令测试:
npm run test:file 03
双端队列(Deque,全称 Double-Ended Queue)是一种灵活的队列:允许从头部和尾部同时添加或移除元素,就像公交站的“双向排队”——人可以从队首或队尾上车/下车。
双端队列的实现
打开src/playground/04-double-ended-queue.ts,实现代码如下:
// 📁 src/playground/04-double-ended-queue.ts
import { LinkedList } from "./01-linked-list";
/**
* 基于循环双向链表实现的双端队列
*/
export class Deque<T> {
private list: LinkedList<T>;
private maxSize?: number; // 可选最大容量
/**
* 构造函数
* @param maxSize 可选,队列的最大容量
*/
constructor(maxSize?: number) {
this.list = new LinkedList<T>();
this.maxSize = maxSize;
}
/**
* 从头部入队
* @param item 要添加的元素
*/
enqueueFront(item: T): void {
if (this.isFull()) {
throw new Error("双端队列已满");
}
this.list.prepend(item); // 复用链表的prepend方法(添加到头部)
}
/**
* 从尾部入队
* @param item 要添加的元素
*/
enqueueRear(item: T): void {
if (this.isFull()) {
throw new Error("双端队列已满");
}
this.list.append(item); // 复用链表的append方法(添加到尾部)
}
/**
* 从头部出队
* @returns 队首元素,为空时返回undefined
*/
dequeueFront(): T | undefined {
return this.list.deleteHead();
}
/**
* 从尾部出队
* @returns 队尾元素,为空时返回undefined
*/
dequeueRear(): T | undefined {
return this.list.deleteTail(); // 复用链表的deleteTail方法(删除尾部)
}
// 以下方法与SimpleQueue一致,略去注释
getFront(): T | undefined { return this.list.getHead(); }
getRear(): T | undefined { return this.list.getTail(); }
isEmpty(): boolean { return this.list.isEmpty(); }
isFull(): boolean { return this.maxSize !== undefined && this.list.size() >= this.maxSize; }
peek(): T | undefined { return this.getFront(); }
size(): number { return this.list.size(); }
}
双端队列的核心特点
双端队列的关键在于双向操作能力,这使其同时具备“队列”和“栈”的特性:
适用场景包括: undo/redo 功能(前一步操作可从队尾撤销)、滑动窗口算法(两端添加/移除窗口元素)。
测试双端队列
运行以下命令测试:
npm run test:file 04
优先队列不遵循 FIFO 原则,而是按元素的优先级处理:优先级高的元素先出队,就像医院急诊室——重症患者优先于轻症患者被救治。
优先队列的实现
打开src/playground/05-priority-queue.ts,实现代码如下(核心是“插入时排序”):
// 📁 src/playground/05-priority-queue.ts
import { LinkedList, NodeItem } from "./01-linked-list";
/**
* 带优先级的元素接口
*/
interface PriorityItem<T> {
value: T; // 元素值
priority: number; // 优先级(数字越大,优先级越高)
}
/**
* 基于循环双向链表实现的优先队列
*/
export class PriorityQueue<T> {
private list: LinkedList<PriorityItem<T>>; // 存储带优先级的元素
private maxSize?: number;
constructor(maxSize?: number) {
this.list = new LinkedList<PriorityItem<T>>();
this.maxSize = maxSize;
}
/**
* 入队:按优先级插入元素(优先级高的靠前)
* @param value 元素值
* @param priority 优先级(数字越大优先级越高)
*/
enqueue(value: T, priority: number): void {
if (this.isFull()) {
throw new Error("优先队列已满");
}
const newItem: PriorityItem<T> = { value, priority };
// 若队列为空,直接插入头部
if (this.isEmpty()) {
this.list.prepend(newItem);
return;
}
// 遍历链表,找到插入位置(确保链表按优先级降序排列)
let current = this.list["head"]; // 访问链表的私有head属性
let count = 0;
while (current && current.value.priority >= priority && count < this.size()) {
current = current.next;
count++;
}
// 若遍历到队尾,直接添加到尾部
if (count === this.size()) {
this.list.append(newItem);
} else {
// 否则在当前位置插入新节点(手动维护链表的循环结构)
const newNode = new NodeItem(newItem);
newNode.next = current;
newNode.prev = current!.prev;
if (current!.prev) {
current!.prev.next = newNode;
} else {
this.list["head"] = newNode; // 若插入到头部,更新head
}
current!.prev = newNode;
// 维护循环:尾节点的next指向新head,新head的prev指向尾节点
this.list["tail"]!.next = this.list["head"];
this.list["head"]!.prev = this.list["tail"];
this.list["currentSize"]++; // 更新链表大小
}
}
/**
* 出队:移除并返回优先级最高的元素(队首)
* @returns 优先级最高的元素值,为空时返回undefined
*/
dequeue(): T | undefined {
// 队首元素就是优先级最高的,直接删除并返回其value
return this.list.deleteHead()?.value;
}
/**
* 获取优先级最高的元素(不移除)
* @returns 优先级最高的元素值
*/
getFront(): T | undefined {
return this.list.getHead()?.value;
}
/**
* 获取优先级最低的元素(不移除)
* @returns 优先级最低的元素值
*/
getRear(): T | undefined {
return this.list.getTail()?.value;
}
// 以下方法与其他队列一致,略去注释
isEmpty(): boolean { return this.list.isEmpty(); }
isFull(): boolean { return this.maxSize !== undefined && this.list.size() >= this.maxSize; }
peek(): T | undefined { return this.getFront(); }
size(): number { return this.list.size(); }
}
优先队列的核心逻辑
优先队列的关键在于入队时的排序:
适用场景包括:任务调度(高优先级任务先执行)、Dijkstra 最短路径算法(优先选择距离最近的节点)。
测试优先队列
运行以下命令测试:
npm run test:file 05
队列是处理“顺序任务”和“异步流程”的利器,但并非万能。正确判断适用场景是关键。
适合使用队列的场景
避免使用队列的场景
队列是一种基础但强大的数据结构,核心价值在于按顺序管理元素和支持异步处理。本教程通过循环双向链表实现了 4 种常见队列:
掌握队列的关键在于理解适用场景——既不要低估它在解耦和顺序处理中的作用,也不要在需要随机访问的场景中强行使用。
现在,你可以尝试在自己的项目中使用这些队列实现,解决实际开发中的顺序任务或异步问题了!祝你编码愉快!
本文是由葡萄城技术开发团队发布,转载请注明出处:葡萄城官网
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。