【源码共读】 | yocto-queue 队列 链表

283 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

【若川视野 x 源码共读】第32期 | yocto-queue 队列 链表点击了解本期详情一起参与

本章涉及

  • 数组和链表的异同
  • 数组和链表的异同
  • 实现一个队列
  • Symbol.iterator的理解
  • yocto-queue的适用场景

分析源码


源码地址:github.com/sindresorhu…

先看用法,这个库是实现了数据结构中的队列,队列具有先进先出(FIFO)的属性。


通常,我们在js中用数组来模拟队列的操作

const queue = []
queue.push()
queue.shift()

既然原生的数组就可以模拟出队列的操作,这个库的意义在哪里,有一个值得关注的点

image-20220909144813583

这个库实现了将时间复杂度降到O(1)

  • 使用链表来模拟队列的操作
  • Symbol.iterator实现自定义迭代器
  • 数组和链表的异同
    • 数组因为是连续地址存储,所以可以直接通过索引来访问
    • 链表不一定是连续的地址,但是便于插入删除操作
/*
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; // 指针指向下一个元素
		}
	}
}

测试用例


  • 项目依赖

  • "ava": "^3.15.0", // 测试库
    "tsd": "^0.17.0", // 检查TypeScript类型
    "xo": "^0.44.0"  //  格式化代码规范
    

覆盖实现的功能,我们主要挑迭代的测试来看

test('iterable', t => {
	const queue = new Queue();
	queue.enqueue('🦄');
	queue.enqueue('🌈');
	t.deepEqual([...queue], ['🦄', '🌈']);
});

使用解构的方式构建新的数组,查看是否与测试用例相等

总结


  • 链表和数组的区别

    • 链表的顺序是随机访问,所以通过指针next指向下一个元素访问
    • 数组的顺序是连续的,所以可以通过索引下标直接访问
    • 链表增删比较简单高效,通过指针的指向即可修改,时间复杂度为O(n)
    • 数组增删需要移动多个元素,取决于元素插入的位置,时间复杂度为O(1)
  • Symbol.iterator实现for...of....迭代

  • 适用场景

    • 数据量较大的时候,并进行增删操作时,链表的优势比较明显