前端数据结构与算法之队列(js实现)

1,586 阅读4分钟

队列是一种先进先出的线性表,通常用链表或者数组来实现。队列只能在队尾插入元素,只能在队首删除元素。

队列示意图:

duilie.jpg

队列的数据结构

本文使用数组来实现队列的结构,但是你要知道相当于是给队列开辟了一块连续的内存空间。

队列的数据结构包含一个队首索引,一个队尾索引,队列长度和一个数据域。

/**
 * 队列的数据结构,初始化队列时需要确定队列的容量
 * @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 也好或者什么方式也好,来跟消息队列服务进行通信,就能保存或者读取数据了。