1. 前言
-
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
-
这是源码共读的第32期,链接:yocto-queue 队列 链表
2. 前置知识
2.1 class用法
class的私有属性、私有方法。 静态属性、静态方法.
#
号用来定义私有属性、方法,之前只通过命名约定以下划线开头static
关键字用来定义静态属性、方法- 对于属性,可以使用
get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。外部设置无效
class Person {
static name = 'person'
#privateProp = 'privateProp'
constructor(age = 18) {
this.age = age
}
publicMethod() {
return this.#privateMethod() + '-' + 'publicMethod'
}
#privateMethod() {
return this.#privateProp
}
static staticMethod() {
return 'staticMethod'
}
get value() {
return this.age
}
set value(val) {
this.age = 111
}
}
let person = new Person()
console.log(
person.age, // 18
person.publicMethod(), //'privateProp-publicMethod'
(person.value = 12), //12
person.value, //111
Person.staticMethod(), //'staticMethod'
Person.name //'person'
)
2.2 队列、链表
- 队列
遵循先进先出(First-In-First-Out,FIFO)的原则,即最先进入队列的元素最先被移除。
实现一个队列:
class Queue {
constructor() {
this.items = [];
}
// 入队:将元素添加到队列末尾
enqueue(element) {
this.items.push(element);
}
// 出队:移除并返回队列的第一个元素
dequeue() {
if (this.isEmpty()) {
return null;
}
return this.items.shift();
}
// 返回队列的第一个元素(不删除)
front() {
if (this.isEmpty()) {
return null;
}
return this.items[0];
}
// 检查队列是否为空
isEmpty() {
return this.items.length === 0;
}
// 清空队列
clear() {
this.items = [];
}
// 返回队列的大小(元素个数)
size() {
return this.items.length;
}
// 将队列转换为字符串形式
toString() {
return this.items.toString();
}
}
// 创建一个队列实例
const queue = new Queue();
// 入队
queue.enqueue('apple');
queue.enqueue('banana');
queue.enqueue('cherry');
// 输出队列内容
console.log('队列内容:', queue.toString()); // 输出:队列内容:apple,banana,cherry
// 出队
const removedElement = queue.dequeue();
// 输出被移除的元素
console.log('被移除的元素:', removedElement); // 输出:被移除的元素:apple
// 输出队列内容
console.log('队列内容:', queue.toString()); // 输出:队列内容:banana,cherry
// 修改队列中的元素(根据索引位置)
queue.items[1] = 'blueberry';
// 输出修改后的队列内容
console.log('队列内容:', queue.toString()); // 输出:队列内容:banana,blueberry
// 查询队列中的元素(根据索引位置)
const element = queue.items[0];
// 输出查询结果
console.log('查询结果:', element); // 输出:查询结果:banana
// 清空队列
queue.clear();
// 检查队列是否为空
console.log('队列是否为空:', queue.isEmpty()); // 输出:队列是否为空:true
- 链表
链表是一种常见的数据结构,它由一系列节点(Node)组成,每个节点包含数据和一个指向下一个节点的引用(指针)。
与数组不同,链表的节点在内存中可以不连续地存储。每个节点都包含了它自己的值和一个指向下一个节点的引用,就像是一条链一样连接起来。最后一个节点的指针通常指向空值(null),表示链表的结束。
实现一个链表:
// 定义链表节点类
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
// 定义链表类
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
}
// 在链表末尾添加节点
append(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
}
// 在链表特定位置插入节点
insertAt(position, value) {
if (position < 0 || position >= this.size()) {
return false; // 插入位置非法,返回失败
}
const newNode = new Node(value);
if (position === 0) {
// 在链表头部插入节点
newNode.next = this.head;
this.head = newNode;
} else {
let current = this.head;
let previous = null;
let index = 0;
while (index < position) {
previous = current;
current = current.next;
index++;
}
newNode.next = current;
previous.next = newNode;
}
return true; // 插入成功,返回成功
}
// 删除链表特定位置的节点
removeAt(position) {
if (position < 0 || position >= this.size()) {
return null; // 删除位置非法,返回null
}
let current = this.head;
if (position === 0) {
// 删除链表头部节点
this.head = current.next;
if (position === this.size() - 1) {
this.tail = null; // 若链表只有一个节点,则删除后tail也需重置为null
}
return current.value;
}
let previous = null;
let index = 0;
while (index < position) {
previous = current;
current = current.next;
index++;
}
previous.next = current.next;
if (position === this.size() - 1) {
this.tail = previous; // 若删除的是尾节点,则更新tail指针
}
return current.value; // 返回被删除的节点数据
}
// 修改链表特定位置的节点
updateAt(position, newData) {
if (position < 0 || position >= this.size()) {
return false; // 修改位置非法,返回失败
}
let current = this.head;
let index = 0;
while (index < position) {
current = current.next;
index++;
}
current.value = newData;
return true; // 修改成功,返回成功
}
// 查询链表特定位置的节点数据
getAt(position) {
if (position < 0 || position >= this.size()) {
return null; // 位置非法,返回null
}
let current = this.head;
let index = 0;
while (index < position) {
current = current.next;
index++;
}
return current.value; // 返回节点数据
}
// 获取链表长度
size() {
let count = 0;
let current = this.head;
while (current) {
count++;
current = current.next;
}
return count;
}
// 将链表转换为字符串
toString() {
let result = "";
let current = this.head;
while (current) {
result += `${current.value} `;
current = current.next;
}
return result.trim();
}
}
// 创建一个链表实例
const linkedList = new LinkedList();
// 向链表中添加节点
linkedList.append(1);
linkedList.append(2);
linkedList.append(3);
// 输出链表内容
console.log("链表内容:", linkedList.toString()); // 输出:链表内容:1 2 3
// 在特定位置插入节点
linkedList.insertAt(1, 4);
// 输出链表内容
console.log("插入链表内容:", linkedList.toString()); // 输出:链表内容:1 4 2 3
// 删除特定位置的节点
const removedData = linkedList.removeAt(2);
// 输出被删除的节点数据
console.log("被删除的节点数据:", removedData); // 输出:被删除的节点数据:2
// 输出链表内容
console.log("链表内容:", linkedList.toString()); // 输出:链表内容:1 4 3
// 修改特定位置的节点
const updateResult = linkedList.updateAt(1, 5);
// 输出修改结果
console.log("修改结果:", updateResult); // 输出:修改结果:true
// 输出链表内容
console.log("链表内容:", linkedList.toString()); // 输出:链表内容:1 5 3
// 查询特定位置的节点数据
const getData = linkedList.getAt(2);
// 输出查询结果
console.log("查询结果:", getData); // 输出:查询结果:3
// 输出链表的长度
console.log("链表长度:", linkedList.size()); // 输出:链表长度:3
2.3 生成器 generator
JavaScript中的生成器(Generator)是一种特殊的函数,它可以用于迭代地生成一系列值。
用法如下:
- 定义生成器
生成器函数使用
function*
关键字来定义,并使用yield
关键字来指定每个生成的值。
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
// 创建生成器实例
const generator = myGenerator();
- 迭代生成器
生成器可以通过迭代器(Iterator)进行遍历。可以使用
for...of
循环来迭代生成器中的值,或者使用next()
方法逐个获取生成器中的值。注意:同时使用for...of和调用next()后,next不会打印,说明已经迭代完成
// 使用for...of循环遍历生成器中的值
for (const value of generator) {
console.log(value); // 输出:1 2 3
}
// 使用next()方法逐个获取生成器中的值
console.log(generator.next().value); // 输出:1
console.log(generator.next().value); // 输出:2
console.log(generator.next().value); // 输出:3
- 传递参数
生成器函数可以接受参数,并在每个
yield
语句中返回不同的值。可以通过调用next(value)
方法并传递参数来将其注入到生成器中。
function* greetingGenerator() {
const name = yield 'Hello';
yield `How are you, ${name}?`;
}
const generator = greetingGenerator();
console.log(generator.next().value); // 输出:Hello
console.log(generator.next('John').value); // 输出:How are you, John?
- 终止生成器
生成器可以通过调用
return(value)
方法来提前终止。这将使生成器立即返回,并将传递的值作为最后一个返还的值。
function* counterGenerator() {
let count = 0;
while (true) {
yield count++;
}
}
const generator = counterGenerator();
console.log(generator.next().value); // 输出:0
console.log(generator.next().value); // 输出:1
console.log(generator.return(100).value); // 输出:100
console.log(generator.next().done); // 输出:true
// for...of 遍历使用break跳出
function* counterGenerator() {
let count = 0;
while (true) {
yield count++;
}
}
const generator = counterGenerator();
for (let value of generator) {
console.log(value);
if (value >= 1) {
break; // 提前终止循环
}
}
3. 源码解析
yocto-queue使用链表来实现队列数据结构,以提供灵活的长度管理、高效的插入和删除操作等优势。
3.1 实现原理
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) { // 如果队列为空,返回 undefined
return;
}
this.#head = this.#head.next; // 将头部指针更新为下一个节点
this.#size--; // 队列元素个数减一
return current.value; // 返回当前头部节点的值
}
// 清空队列
clear() {
this.#head = undefined; // 将头部节点设为 undefined
this.#tail = undefined; // 将尾部节点设为 undefined
this.#size = 0; // 将元素个数设为 0
}
// 返回队列中元素的个数
get size() {
return this.#size;
}
// 迭代器方法,使用for...of循环遍历队列中的元素
* [Symbol.iterator]() {
let current = this.#head; // 从头部节点开始
while (current) {
yield current.value; // 返回当前节点的值
current = current.next; // 移动到下一个节点
}
}
}
过程总结:
-
Node
类定义队列中的节点。value
属性存储节点的值,next
属性指向下一个节点。 -
Queue
类是一个基于链表实现的队列。有以下属性和方法:
#head
:私有属性,指向队列的头部节点。#tail
:私有属性,指向队列的尾部节点。#size
:私有属性,表示队列中元素的个数。constructor()
:构造函数,初始化队列,调用clear()
方法。enqueue(value)
:将一个新元素添加到队列的末尾。它创建一个新节点,并根据当前的队列状态来更新头部和尾部节点的引用。dequeue()
:从队列的头部移除并返回一个元素。它更新头部节点的引用,并返回被移除的节点的值。clear()
:清空队列,将头部、尾部节点设置为 undefined,将元素个数设置为 0。size
:获取队列中元素的个数。[Symbol.iterator]()
:迭代器方法,通过yield
关键字,使用for...of
循环遍历队列中的元素。arrify迭代器参考
基本上了解了链表和队列的实现,源码看起来就轻松点,简单说就是使用链表实现了队列。
4. 总结
通过 yocto-queue源码学习知道了:
-
源码中队列为什么使用链表进行实现,相对于数组优势有:
-
动态扩展:如果你处理的数据量非常大并且不确定。链表能够随着元素的添加而动态地拓展其存储空间,而无需像数组那样预先分配空间大小。
-
高效的插入和删除:在大量数据中添加或移除元素时,链表比数组更具有优势,因为链表的插入和删除只涉及到少数几个指针的改变,无需移动大量元素。
-
避免内存浪费:由于链表的动态性,可以有效避免预分配的大块连续内存无法完全利用或超出的情况,从而避免内存浪费。
-
时间复杂度 | 出队和入队 | 解释 |
---|---|---|
数组 | O(n) | 如果队列没有满,那么这是一个O(1)的操作。但是,如果队列已满,我们需要增加数组的大小,这是一个O(n)的操作,其中n是队列的当前大小。出队:O(n)的操作,因为我们需要移动所有元素来填充被删除的空位。 |
链表 | O(1) | 始终是一个O(1)的操作,只是创建一个新节点并更新头/尾指针 |
- 了解了队列和链表的实现,学习了生成器的使用。
参考文章: