源码 | yocto-queue 库如何实现替代数组

681 阅读6分钟

本文正在参加「金石计划」

前言

接触这个库之前,对于数组类型的数据处理,我确实思考过 Array 方法的时间复杂度的问题,但是没有深入研究。

最近一直在川哥的【源码共读】活动中给出的开源库列表里学习,正好看到这个库,也顺便把之前没有研究的功能研究一下,最好能应用到实际开发中。

接下来的文章主要包括一下内容:

  • yocto-queue 库的用途
  • yocto-queue 库的代码分析
  • 对 yocto-queue 库实际应用的分析

yocto-queue 库介绍

yocto-queue 库的主要用途是,当数量级较大的数组,需要频繁的进行添加和删除的时候,可以用 yocto-queue 替代数组。

之所以可以用它进行替代,是因为 yocto-queue 库进行操作的时间复杂度是 O (1) ,而数组则是线性时间复杂度 O (n)

O(1) :表示算法执行的时间大小固定,不随输入数据N的大小而变化。

O(n) :表示算法执行的时间是与 n 呈线性关系的任意类型的集合,随着 n 值的增加,执行时间也会增加。

根据时间复杂度对比,不难发现前者的代码运行效率更高一些。

源码分析

源码中的代码量相对较少,读起来会比较轻松,看似可以琢磨的点少,其实不然。

代码中包含知识点主要包括类的属性链表与数组的对比队列、自定义迭代器等,容我细讲。

git 地址: yocto-queue

Node 类

node 类的作用是在新增操作中储存需要插入到队列中的元素。

class Node {
  value;
  next;

  constructor(value) {
    this.value = value;
  }
}

它有两个公有属性:

value:插入到队列中的元素。

next:下一个 Node 实例。

属性的两种写法

Node 类里面正好包含了这两种写法:

第一种,定义在 constructor() 方法里面的 this 上面;

第二种,定义在类的最顶层。(ES2022 新加的写法)

Queue 类

私有属性的“演化”

class Queue {
  #head;
  #tail;
  #size;
}

Queue 类里共包含三个私有属性:

#head:队列的头。

#tail:队列的尾。

#size:队列的长度。

在属性前面使用#,是 ES2022 新加入的写法,从此 class 正式有了私有属性。这个私有属性在 Queue 的外部是无法被使用的。

const queue = new Queue();
console.log(queue.#size);

外部使用 #size 属性,会抛出错误:

Private field '#size' must be declared in an enclosing class

在这之前 主要采用命名方式的不同区分私有属性和公有属性。比如声明一个只有 Queue 内部可用使用的常量_limit。

_limit = 10;

但是其实 Queue 的实例是可以访问到的。

const queue = new Queue();
console.log(queue._limit); // 10

还有将私有方法的名字命名为一个 Symbol 值的方式,可以阅读阮一峰大佬的书详细了解。

链表 vs 数组

数组:一种有序线性数据结构,用一串连续的内存空间进行数据存储。

链表:一种无序线性数据结构,用一串非连续的内存空间进行数据存储,通过链表中节点的指针次序维护线性链路。

两个数据类型在操作元素时的时间复杂度的区别如下:

时间复杂度数组链表
添加O (n)O (1)
删除O (n)O (1)
读取O (1)O (n)

队列

yocto-queue 库提供的主要功能就是用链表实现队列,然后进行数据的添加和删除。

队列是具有先进先出 (FIFO) 原则的有序集合。

enqueue 入队列

该方法会从队列中的队尾添加一个元素。

class Queue {
  enqueue(value) {
    const node = new Node(value);

    if (this.#head) {
      this.#tail.next = node;
      this.#tail = node;
    } else {
      this.#head = node;
      this.#tail = node;
    }

    this.#size++;
  }
}

通过队头 #head 的值判断,需要插入的元素存放在队列中的位置:

如果 #head 的值存在,则新添加的元素放在队尾;

如果 #head 的值不存在,则新添加的元素即为队列的队头;

且队列长度加一。

dequeue 出队列

该方法会从队列中的队头元素移除,并返回移除的元素。

class Queue {
  dequeue() {
    const current = this.#head;
    if (!current) {
      return;
    }

    this.#head = this.#head.next;
    this.#size--;
    return current.value;
  }
}

先定义 current 指向队头的元素,通过 current 值的判断进行下一步操作:

如果 current 不存在,则表示队列中已经没有数据。

如果 current 存在,则将队头的元素移除,再队列的长度减一,并返回移除的元素。

我们来调用 dequeue 方法之后,查看队列的值:

const queue = new Queue();
queue.enqueue(3);
queue.enqueue(4);
queue.enqueue(5);
console.log(...queue); // 4 5

从打印结果可以看出元素3已经被移除。

自定义迭代器

数组拥有默认的迭代器行为,Queue 类的实例对象理论也应该拥有迭代器行为。

使用 Symbol.iterator 可以创建自定义迭代器 创建方法很简单。

class Queue {
  *[Symbol.iterator]() {
    let current = this.#head;

    while (current) {
      yield current.value;
      current = current.next;
    }
  }
}

Queue 类定义的迭代器,从队列的头开始循环,把每一个元素都返回出来。

这样在查看队列数据的时候可以使用 ... 展开:

const queue = new Queue();
queue.enqueue(3);
queue.enqueue(4);
queue.enqueue(5);
console.log(...queue); // 3 4 5

yocto-queue 的实际应用

其实对于第三方库,除了学习之外,实际应用也同样重要。

如果我掌握了一种更佳的代码方案,我会联想到实际开发中的使用场景。

那么问题来了,实际开发中对大量级数据处理的场景,那么我怎么用 yocto-queue 处理这些数据?它并不支持现有数据的初始化。

它初始化了一个空队列,然后可以向里面插入数据,但是如果我本身就有一组数据,怎么进行操作呢?

现有数据的操作问题

这个问题,我来回想了几遍,都觉得对现有数组数据的处理才是这个库介绍中提供的最主要的功能。

仅按照说明文档中的使用,其实没有实际使用价值。

这是目前我对 yocto-queue 应用的最大的困惑

小结

目前,我并没有想到和找到,实际开发中如何使用 yocto-queue 库进行数据的操作。

欢迎大家留言讨论 💐💐💐

源码阅读的两个重要点

源码阅读的过程中,对实现方法和设计模式的分析、学习、吃透很重要。

遇到问题,提出疑问,寻找答案,这个过程同样重要。在这个过程里,除了锻炼了个人思考的能力,还可以帮助自信心的塑造

不要担心自己提的问题是多余的或者过于基础,由浅入深,在这个过程中可以实现经验的积累。而后,无论是看待问题的角度,还是提问的内容,都会变得精准。

总结

yocto-queue 库的源码阅读分析之后,收获如下:

  1. 了解 yocto-queue 库的用途,知道了它的使用场景
  2. 通过对 yocto-queue 库的源码分析,了解了它如何替代数组以及若干知识点回顾。
  3. 进一步探讨了如何在实际开发中使用它, 虽然没有得到定论,但是问题不一定有答案,过程中定会有收获。(收获了思路和错误的实现方案)。
  4. 额外的收获,关于源码阅读的两个重要点学习个人思考

我是 叶一一 ,非职业「传道授业解惑」的技术博主。

欢迎留言讨论、点赞 、收藏,续产出技术分享。