队列是一种先进先出的线性表,通常用链表或者数组来实现。队列只能在队尾插入元素,只能在队首删除元素。
队列示意图:
队列的数据结构
本文使用数组来实现队列的结构,但是你要知道相当于是给队列开辟了一块连续的内存空间。
队列的数据结构包含一个队首索引,一个队尾索引,队列长度和一个数据域。
/**
* 队列的数据结构,初始化队列时需要确定队列的容量
* @param {Number} length 队列的容量
*/
function Queue(length) {
this.head = 0;
this.tail = -1;
this.data = [];
this.length = length;
}
队列的基本算法
队列的基本算法不多,大致有以下几种:
/**
* 入队
* @param {*} queue 队列
* @param {*} element 入队的元素
* @returns
*/
function push(queue, element) {
// 队列满的时候,tail 是队列最后一个元素的索引,这时就不能再入队了。
if (queue.tail + 1 >= queue.length) {
return false;
}
queue.tail++;
queue.data[queue.tail] = element;
return true;
}
/**
* 输出队列,遍历队列
* @param {*} queue 队列
*/
function output(queue) {
// 队列的遍历跟数组的遍历一样的,从队头依次遍历到队尾
for (let i = queue.head; i <= queue.tail; i++) {
console.log(queue.data[i]);
}
}
/**
* 获取队首元素
* @param {*} queue 队列
* @returns 队首元素
*/
function front(queue) {
return queue.data[queue.head];
}
/**
* 弹出队首元素,删除队首元素,注意,这里只是把队首索引往后移动了一位,
* 因为在遍历队列和取队首元素的时候,都需要 head 索引来帮助。
* 但是这时老 head 还有数据,可以清楚掉也可以不清除掉,因为它已经没用了。
* @param {*} queue 队列
*/
function pop(queue) {
queue.head++;
}
/**
* 判断队列是否为空,空队列的 head 为 0,tail 为 -1
* @param {*} queue 队列
* @returns
*/
function empty(queue) {
return queue.head > queue.tail;
}
循环队列的数据结构
如果把普通的直线队列首尾连起来围成一个圈,就构成了循环队列。
循环队列的数据结构跟普通队列差不多,只是多了一个记录队列中元素个数的属性。
/**
* 循环队列的数据结构
* @param {Number} length 队列的容量
*/
function CircularQueue(length) {
this.head = 0;
this.tail = -1;
this.data = [];
this.length = length;
// 如果是循环队列才加上这个属性,用来保存循环队列中一共有多少个元素
this.count = 0;
}
循环队列的基本算法
循环队列的基本算法跟普通队列一样,还是那几个,只是在内部实现上有很大差别,尤其是要理解移动头尾索引时的(i + 1) % queue.length
这种取模的操作。
/**
* 循环队列入队操作
* @param {*} queue 队列
* @param {*} element 入队的元素
* @returns
*/
function push(queue, element) {
// 循环队列中的元素数量满了就不能入队
if (queue.count >= queue.length) {
return false;
}
// 因为是循环队列,尾部索引是值就不能简单的加 1 了,需要加 1 后再模队列长度,才是正确的索引
// 比如:length = 8,head = 1,tail = 7,tail 这时已经达到队尾了,如果再入队一个元素,
// 那么 (tail + 1) % 8 = 0,于是 tail 就跑到第一位去了,索引比 head 还小,
// 该元素也成功入队,这就是循环队列的特性。
queue.tail = (queue.tail + 1) % queue.length;
queue.data[queue.tail] = element;
queue.count++;
return true;
}
/**
* 弹出队首元素,删除队首元素,head 往后挪的时候也要注意到队尾后的取模。
* 例如:length = 8, head = 7, count = 3, tail = 1,head + 1 后就需要挪到 0 的位置。
* @param {*} queue 队列
*/
function pop(queue) {
queue.head = (queue.head + 1) % queue.length;
// 然后把元素数量减 1
queue.count--;
}
/**
* 输出队列,遍历队列
* @param {*} queue 队列
*/
function output(queue) {
// 循环队列的遍历比普通队列复杂些,就拿这个例子来说:
// length = 8,head = 1,tail = 0,count = 7; tail 比 head 小,
// 如果从 head 往下遍历到 length 处,就会发现 tail 不在那里,所以需要咱们做些处理。
// 从 head 处开始遍历
let i = queue.head;
while (i !== queue.tail) {
console.log(queue.data[i]);
i = (i + 1) % queue.length;
// 最后一次循环,输出队尾元素
if (i === queue.tail) {
console.log(queue.data[i]);
}
}
}
/**
* 判断循环队列是否为空,直接看 count 是否为 0 即可
* @param {*} queue 队列
* @returns
*/
function empty(queue) {
return queue.count === 0;
}
测试输出循环队列
const queue = new CircularQueue(8);
for (let i = 1; i <= 8; i++) {
push(queue, i);
}
pop(queue);
pop(queue);
pop(queue);
pop(queue);
push(queue, 9);
push(queue, 7);
console.log(queue);
output(queue);
队列的用途之消息队列
有关消息队列更详细的介绍请自行去查阅相关资料,本文只做简述。
消息队列,一般我们会简称它为MQ(Message Queue)。它出现的主要目的是为了解耦各项目间的依赖。也主要运用在后端场景下。
既然是队列,那么就有往它里面塞数据的一方,也就有从它里面读取数据的一方。 这时人们规定存数据的一方叫做生产者,取数据的一方叫做消费者。而消息队列就演化成为了一个集中化的数据存取中心。各个系统无论是通过 RPC 也好或者什么方式也好,来跟消息队列服务进行通信,就能保存或者读取数据了。