1、前言
- 本文参加了由 公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与
- 这是源码共读的第32期,链接: 第32期 | yocto-queue 队列 链表
2、开始
2.1项目地址
2.2 具体代码
/*
How it works:
`this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
*/
class Node {
value;
next;
constructor(value) {
this.value = value;
}
}
export default class Queue {
#head;
#tail;
#size;
constructor() {
this.clear();
}
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++;
}
dequeue() {
const current = this.#head;
if (!current) {
return;
}
this.#head = this.#head.next;
this.#size--;
return current.value;
}
clear() {
this.#head = undefined;
this.#tail = undefined;
this.#size = 0;
}
get size() {
return this.#size;
}
* [Symbol.iterator]() {
let current = this.#head;
while (current) {
yield current.value;
current = current.next;
}
}
}
2.3 源码分析
定义一个Node类并创建value与next属性
class Node {
value;
next;
constructor(value) {
this.value = value;
}
}
定义一个Queue类并创建 #head, #tail, #size属性
export default class Queue {
#head; // 链表的开始
#tail; // 链表的结尾
#size; // 链表的长度
constructor() {
// 创建新函数的时候先执行一次clear重置链表
this.clear();
}
// 插入链表元素
enqueue(value) {
// 创建一个新的链表元素里面包含value以及next
const node = new Node(value);
if (this.#head) { // 检查链表是否是第一次创建
// 这里我开始有点不太理解为什么要这么赋值
// 因为链表的next是指向下一个链表的 所以先给链表尾的next赋值新的元素 这样的话后续覆盖tail的时候因为是tail是复杂类型所以它的next指向不会改变
this.#tail.next = node; // 先给尾部tail next指向修改为新的链表元素
this.#tail = node; // 把尾部tail改为新的链表元素
**注: 这两个#tail不是同一个元素哦**
} else {
// 头与尾都是该链表元素
this.#head = node;
this.#tail = node;
}
this.#size++; // 长度加1
}
// 删除链表
dequeue() {
const current = this.#head; // 简单的赋值
if (!current) {
return; // 检查链表是否有值
}
this.#head = this.#head.next; // 直接把#head的next指向#head完成删除
this.#size--; // 长度减一
return current.value; // 返回删除的#head
}
// 清空链表
clear() {
this.#head = undefined;
this.#tail = undefined;
this.#size = 0;
}
// 获取链表长度
get size() {
return this.#size;
}
// Symbol.iterator 部署了该接口后执行该函数会返回一个遍历器对象 只要有该属性那么就表示数据是可以遍历的
// function* [Symbol.iterator](){} 这是generator函数 通过yield表达式定义内部不同状态 该函数还会返回一个可遍历对象
* [Symbol.iterator ) {
let current = this.#head;
while (current) { // while循环 直到检查到current为空
yield current.value; // 通过yield表达式 返回当前的链表元素
current = current.next; // 并把next指向赋值给current
}
}
}
2.4 个人思考
1. Iterator 与 Generator函数
Iterator
- 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
- iterator的作用有三个: 一是为各种数据结构,提供一个统一的,简便的访问接口; 二是使得数据结构的成员能够按照某种次序排列;三是 ES6 创造了一种新的遍历命令
for...of循环, Iterator 接口主要供for...of消费
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
- 上面代码定义了一个
makeIterator函数,它是一个遍历器生成函数,作用就是返回一个遍历器对象。对数组['a', 'b']执行这个函数,就会返回该数组的遍历器对象(即指针对象)it。 - Iterator 会返回一个遍历器对象,通过对象的next方法用来移动指针.开始时,指针指向数组的开始位置。然后,每次调用
next方法,指针就会指向数组的下一个成员。第一次调用,指向a;第二次调用,指向b。
Generator
-
Generator 函数是一个普通函数,但是有两个特征。一是,
function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。 -
Generator函数返回的是一个指向内部状态的指针对象也就是遍历器对象(Iterator Object), 需要调用next方法,使得指针移向下一个状态.也就是说每次调用
next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止.换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。 -
所以下面的代码的意思是,当你对链表进行遍历操作的时候他会调用Generator函数并返回遍历器对象,执行
next方法,使指针移向下一个状态.函数内部则是从头部或者停止的地方执行代码直到遇到yield表达式,并返回当前的链表元素// 该函数 * [Symbol.iterator ) { let current = this.#head; while (current) { // while循环 直到检查到current为空 yield current.value; // 通过yield表达式 返回当前的链表元素 current = current.next; // 并把next指向赋值给current } }
以上知识点皆来自阮一峰的ES6入门一书
2. 数组与链表的区别
数组
一. 数组的特点
1. 在内存中,数组是一块连续的区域
2. 数组需要预留空间
在使用前需要提前申请所占内存的大小,这样不知道需要多大的空间,就预先申请可能会浪费内存空间,即数组空间利用率低
ps:数组的空间在编译阶段就需要进行确定,所以需要提前给出数组空间的大小(在运行阶段是不允许改变的)
3. 在数组起始位置处,插入数据和删除数据效率低
插入数据时,待插入位置的的元素和它后面的所有元素都需要向后搬移
删除数据时,待删除位置后面的所有元素都需要向前搬移
4.随机访问效率很高,时间复杂度可以达到O(1)
5.数组开辟的空间,在不够使用的时候需要扩容,扩容的话,就会涉及到需要把旧数组中的所有元素向新数组中搬移
二. 数组的优点
随机访问性强,查找速度快,时间复杂度为O(1)
三. 数组的缺点
1.头插和头删的效率低,时间复杂度为O(N)
2.空间利用率不高
3.内存空间要求高,必须有足够的连续的内存空间
4.数组空间的大小固定,不能动态拓展
链表
一、链表的特点
1.在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续
2.链表中的元素都会两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址
每一个数据都会保存下一个数据的内存的地址,通过此地址可以找到下一个数据
3.查找数据时效率低,时间复杂度为O(N)
因为链表的空间是分散的,所以不具有随机访问性,如要需要访问某个位置的数据,需要从第一个数据开始找起,依次往后遍历,直到找到待查询的位置,故可能在查找某个元素时,时间复杂度达到O(N)
4.空间不需要提前指定大小,是动态申请的,根据需求动态的申请和删除内存空间,扩展方便,故空间的利用率较高
5.任意位置插入元素和删除元素效率较高,时间复杂度为O(1)
6.链表的空间是从堆中分配的
二、链表的优点
1.任意位置插入元素和删除元素的速度快,时间复杂度为O(1)
2.内存利用率高,不会浪费内存
3.链表的空间大小不固定,可以动态拓展
三、链表的缺点
随机访问效率低,时间复杂度为0(N)
综上:
对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组
对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表