一、🍊 队列的理论基础
1、概念
队列是先进先出,后进后出,就像排队结账,先排队的先结账,后面的人排到队列尾部。
2、操作特性
队列也是一种“操作受限”的线性表。最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。
3、使用场景
队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。
二、🍊 如何实现一个队列
- 跟栈一样,队列可以用数组来实现,也可以用链表来实现。
- 用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。 js代码如下:
class ArrayQueue {
// 数组:items,数组大小:capacity
constructor(capacity) {
this.items = new Array(capacity);
this.capacity = capacity;
// head表示队头下标,tail表示队尾下标
this.head = 0;
this.tail = 0;
}
// 入队
enqueue(item) {
// 如果tail == capacity 表示队列已经满了
if (this.tail === this.capacity) {
console.error('Queue is full');
return false;
}
this.items[this.tail] = item;
this.tail = (this.tail + 1) % this.capacity; // 循环队列的处理
return true;
}
// 出队
dequeue() {
// 如果head == tail 表示队列为空
if (this.head === this.tail) {
return null;
}
const ret = this.items[this.head];
this.head = (this.head + 1) % this.capacity; // 循环队列的处理
return ret;
}
}
// 使用示例
const queue = new ArrayQueue(3); // 创建一个容量为3的队列
console.log(queue.enqueue('item1')); // 输出: true
console.log(queue.enqueue('item2')); // 输出: true
console.log(queue.enqueue('item3')); // 输出: true
console.log(queue.enqueue('item4')); // 输出: false,队列已满
console.log(queue.dequeue()); // 输出: 'item1'
console.log(queue.dequeue()); // 输出: 'item2'
console.log(queue.dequeue()); // 输出: 'item3'
console.log(queue.dequeue()); // 输出: null,队列已空
- 使用
class
关键字来定义ArrayQueue
类,并使用constructor
方法来初始化队列; enqueue
方法用于向队列中添加元素,如果队列已满,则返回false
并输出错误信息。dequeue
方法用于从队列中移除元素,如果队列为空,则返回null
。- 第19行和39行代码使用了取模运算符
%
来处理循环队列
的情况,确保当head
或tail
到达数组末尾时能够回到数组的起始位置。
注意:上面的实现方式,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。如果想要继续添加,就必须移动数组,当队列的 tail 指针移动到数组的最右边后,如果有新的数据入队,我们可以将 head 到 tail 之间的数据,整体搬移到数组中 0 到 tail-head 的位置。上面入队函数的代码(11-21行代码修改如下):
// 入队操作,将item放入队尾
enqueue(item) {
// 如果队列已满,进行特殊处理
if (this.size === this.capacity) {
// 如果队列满且head为0,则无法继续入队
if (this.head === 0) {
return false;
}
// 数据搬移
for (let i = this.head; i < this.tail; i++) {
// 这里可以理解成数据搬移时head就是偏移量
this.items[i - this.head] = this.items[i];
}
// 更新head和tail
this.tail -= this.head;
this.head = 0;
}
// 在tail位置添加新元素
this.items[this.tail] = item;
// 更新tail和size
this.tail++;
this.size++;
return true;
}
三、🍊 循环队列
循环队列的存在,主要是为了解决上面代码中在 tail==n 时,会有数据搬移操作,这样入队操作性能就会受到影响。如下图:
写循环队列需要注意的点主要是确定队空和队满的条件。
- 队列为空的判断条件仍然是 head == tail
- 队满时,(tail+1)%n=head
注意:当队列满时,tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。
四、🍊 阻塞队列和并发队列
阻塞队列
其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。- 阻塞队列,在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列呢?线程安全的队列我们叫作
并发队列
。