javascript 队列

924 阅读4分钟

之前在学习算法的时候学到了队列的一种概念, 在看到 juejin.cn/post/684490… 等等许多篇文章中全部是用数组去实现的, 对于这样的文章表示很不满.

队列的要求是: 数据插入队尾: 时间复杂度 O(1), 移除头部数据: 时间复杂度 O(1). 而其余文章中所用数组去实现, 移除头部的方法是 Array.prototype.shift, 这个方法的时间复杂度为 O(n), 并不满足队列的要求.

下面是对 Array.prototype.shift, Array.prototype.pop 两个方法性能的测试:

  1. 测试 pop, 浏览器瞬间打印时间(约9.1ms).
  // Array.prorotype.pop TEST:
  const arr = [];
  console.time();
  for (let i = 0; i < 99999; i++) {
      arr.push(i);
  }
  for (let i = 0; i < 99999; i++) {
      arr.pop();
  }
  setTimeout(() => {
    console.timeEnd();
  }, 0);  // default: 9.115234375ms
  1. 测试 shift, 浏览器瞬间打印时间(约432.9ms).
  const arr = [];
  console.time();
  for (let i = 0; i < 99999; i++) {
      arr.push(i);
  }
  for (let i = 0; i < 99999; i++) {
      arr.shift();
  }
  setTimeout(() => {
    console.timeEnd();
  }, 0);  // default: 432.95703125ms

可见时间差距还是很明显的, 但是时间都比较短, 为了避免误测, 扩大效果, 把测试数据改为 999999. 结论为:

  1. pop: default: 24.708251953125ms.(浏览器瞬间出现打印效果)
  2. shift: default: 46803.153076171875ms. (浏览器加载了很久才出现)

造成上述的原因是, 定义数组, 实际上是计算机给我们开辟了连续的一个储存空间, 如果删除末尾的元素, 直接删除即可, 如果删除头部的元素, 后面所有的元素都要往前挪一位, 来保证下标 index 相同. (详细的可以自行 google 或者百度).

所以为了保证队列的操作时间, 我们不能用数组实现队列, 那么如何实现呢? 虽然 JavaScript 中没有自己封装的队列, 但是 java 中有, 所以通过 google 搜索 java queue source code: introcs.cs.princeton.edu/java/43stac…

所以 Queue 的本质是用链表实现的, 下面就仿照 java 这段代码对链表进行实现:

class Queue {
  constructor() {
    this.length = 0; // 队列的长度.
    this.first = { next: null, item: null };; // 队列的头部.
    this.last = { next: null, item: null }; // 队列的尾部.
  }
 }

生成一个基本的队列结构, 用 next 指针把队列中的各个数据串起来, 用 item 表示队列的数据. 一个队列有下面几个方法: size(获取长度), isEmpty(是否为空), peek(查询队列头部的数据), enqueue(把数据压入队尾), dequeue(移出队列头部的数据). 我们从简到复实现上述方法.

  size() { // 获取队列长度.
    return this.length;
  }

  isEmpty() { // 判断队列是否为空.
    return this.length === 0;
  }

  peek() { // 查询队列头部的数据.
    if (this.isEmpty()) throw new Error('queue is empty!');
    return this.first.item;
  }

这几个方法都比较简单, 不在多说. 入队也是比较简单的, 就是把尾结点的 item 值设置成进入队列的值, next 设置成 { item: null, next: null }, 在把 last 指针指向 last.next 即可. 如果队列为空, 那么队列头部指针指向队列尾部.

  enqueue(item) {
    this.last.item = item; // 尾部的 item 值设置.
    this.last.next = { item: null, next: null }; // next 指针设置为 { item: null, next: null }
    if (this.isEmpty()) this.first = this.last; // 如果队列为空, 头指针指向尾指针.
    this.last = this.last.next; // 把尾指针指向尾指针的 next 属性.
    this.length++;
  }

对于指针指向不熟悉的同学, 可以对链表多做练习, 我自己刚开始也是绕来绕去. 出队也比较简单, 就把头指针指向他的 next 属性就好了.

  dequeue() {
    if (this.isEmpty()) throw new Error('queue is empty!');
    const { item } = this.first;
    this.first = this.first.next; // 头指针指向自己的 next 属性.
    this.length--;
    return item; // 返回出队的数据.
  }

至此, 队列的基本方法已经完成了. 我们可以使用 const queue = new Queue(); 来使用, 如果想初始化头部怎么办? 很简单, 只需要对构造函数进行改动即可:

 -- constructor() {
 ++ constructor(data) {
    this.length = 0;
    this.last = { next: null, item: null };
    this.first = { next: null, item: null };
 ++ if (data) this.enqueue(data);
  }

这时我们可以用 const queue = new Queue('data'); 来初始化队头.

完整代码:

class Queue {
  constructor(data) {
    this.length = 0;
    this.last = { next: null, item: null };
    this.first = { next: null, item: null };
    if (data) this.enqueue(data);
  }

  size() {
    return this.length;
  }

  isEmpty() {
    return this.length === 0;
  }

  peek() {
    if (this.isEmpty()) throw new Error('queue is empty!');
    return this.first.item;
  }

  enqueue(item) {
    this.last.item = item;
    this.last.next = { item: null, next: null };
    if (this.isEmpty()) this.first = this.last;
    this.last = this.last.next;
    this.length++;
  }

  dequeue() {
    if (this.isEmpty()) throw new Error('queue is empty!');
    const { item } = this.first;
    this.first = this.first.next;
    this.length--;
    return item;
  }
}

我在 从上到下打印二叉树 使用了自己封装的 Queue, 代码通过, 说明 enqueue 方法和 dequeue 方法是正确的, 时间复杂度方面我也分析过了, 是优与数组的, 至此, 队列的基本内容已经完成.

这是我写的第一篇文章, 难免会出现表述不清, 排序不够好的情况, 希望各位读者能够多多包容.

另因个人水平有限, 如果读者有更好的方法, 希望能够指出.