第32期 | yocto-queue 队列 链表

269 阅读5分钟

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

前言

yocto-queue作用

针对 先进先出 这种场景,如果是大型数组,我们应该使用链表而不是数组,因为Array#shift()它具有线性时间复杂度 O(n)而Queue#dequeue()具有恒定时间复杂度 O(1), 从而优化该场景下的性能

在javascript中,是没有链表这个数据结构的yocto-queue便是用js对链表的实现

前置知识

什么是链表

链表是物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,由一系列可动态生成的结点(地址)组成

链表与数组的区别

1、链表是链式存储结构,数组是顺序存储结构

2、链表通过指针连接元素与元素,而数组则是把所有元素按顺序进行存储

3、链表的插入和删除元素比较简单,不需要移动元素,且较为容易实现长度的扩充,但是查询元素比较困难数组是查询比较快,但是删除和增加会比较麻烦。

什么是迭代器&生成器

迭代器

在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值

迭代器对象可以通过重复调用 next()显式地迭代。 迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。 在产生终止值之后,对 next()的额外调用应该继续返回{done:true}

function makeRangeIterator() {
  
    // 迭代器1
    // value 表示当前值
    // done 表示是否是最后一个迭代器
    // next方法 返回下一个迭代器
    let iterator = {
        value: "1",
        done: false,
        next: function () { return iterator2 } // 返回迭代器2
    }
    
    // 迭代器2
    const iterator2 = {
        value: "2",
        done: false,
        next: function () { return iterator3 } // 返回迭代器3
    }
    
    // 迭代器3
    const iterator3 = {
        value: "3",
        done: false,
        next: function () { return iteratorDone } // 返回迭代器结束
    }
    
    // 迭代器结束
    // 迭代器结束时 done 字段为 true
    const iteratorDone = {
        done: true,
        next: function () { return iteratorDone }
    }
    return {
        next: function () {
            const result = iterator
            iterator = iterator.next()
            return result
        }
    }
}

使用方式:

let it = makeRangeIterator();
console.log(it.next()) // {value: 0, done: false}
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {"done":true}

生成器

生成器函数使用 function*语法编写,配合yield字段生成迭代器

yield关键字实际返回一个对象,它有两个属性,valuedonevalue属性是对yield表达式求值的结果,而donefalse,表示生成器函数尚未完全完成

我们再使用生成器实现一遍上面的代码

function* makeRangeIterator(end = Infinity) {
    yield '1';
    yield '2';
    yield '3';
}
var a = makeRangeIterator(3)
console.log(a.next()) // {value: 0, done: false}
console.log(a.next()) // {value: 1, done: false}
console.log(a.next()) // {value: 2, done: false}
console.log(a.next()) // {"done":true}

源码

框架

class Node {}

export default class Queue {

	#head; // 指向头指针
	#tail; // 指向当前节点
	#size; // 当前队列元素个数
        
        // 构造函数
        constructor () {}
        
        // 向队列中添加一个值
        enqueue (value) {}
  
        // 删除队列中的下一个值
        dequeue () {}
  
        // 清空队列
        clear () {}
  
        // 获取当前链表大小
        get size () {}
        
        *[Symbol.iterator] () {}
}

node类

class Node {
	value; // 当前节点值
	next;  // 指向后一个节点

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

这里使用到了设计模式中的工厂模式,使用node类实例化出来的对象,都有valuenext这两个字段

constructor 构造方法

constructor() {
	this.clear();
}

构造函数只是调用了一下清空队列的方法

这里的clear()方法主要起设置初始值的作用,比如上面虽然定义了size字段,但是并未赋值

clear()方法中有一段this.#size = 0正好可以给size赋初始值,提高代码复用

API

enqueue 入栈

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 出栈

dequeue () {
	const current = this.#head;
  
        // 如果头部节点不存在则不执行
	if (!current) {
		return;
	}
  
        // 将头部节点指向后一个节点(先入先出)
	this.#head = this.#head.next;
  
	this.#size--;	 // 实时记录链表大小
	return current.value;
}

clear 清空

clear () {
	this.#head = undefined;
	this.#tail = undefined;
	this.#size = 0; // 重置链表大小
}

size 获取大小

get size () {
	return this.#size;
}

这里使用了get关键字

主要是为了让实例对象获取链表大小时,可以通过实例对象.size直接获取当前链表大小

Symbol.iterator

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

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

看到这里有的可能会比较诧异,这段代码有什么用?你说enqueue dequeue 这些还能理解,它们是队列的API函数,但是这Symbol.iterator是个什么鬼?上面使用时也没看到哪里有调用它啊?

那我我们先注释掉Symbol.iterator代码部分,打印试一下

export default class Queue {
        // 其余代码... 
        
        // *[Symbol.iterator] () {
                // 内部代码...
        // }
}

const queue = new Queue();
queue.enqueue('🦄');
queue.enqueue('🌈');
queue.enqueue('🦄');

for (let i of queue) {
    console.log(i)
}
// TypeError: queue is not iterableat 

循环时时浏览器就直接 抛出了异常


然后我们再取消Symbol.iterator注释了,再打印试一下

export default class Queue {
        // 其余代码... 
        
        // 取消注释 Symbol.iterator
	*[Symbol.iterator] () {
		 // 内部代码...
	}
}

const queue = new Queue();
queue.enqueue('🦄');
queue.enqueue('🌈');
queue.enqueue('🦄');

for (let i of queue) {
    console.log(i)
}
// 🦄🌈🦄

这里是可以正常打印的

到这里们不难看出Symbol.iterator作用就是为了让实例化出来的对象可以被 for...of 循环

然后我们再来解析源码

*[Symbol.iterator] () {
  
  // 当前节点指向头部链表头部节点
	let current = this.#head;
  
  
	while (current) {
    // 使用 yield 生成迭代器
		yield current.value;
     
    // 当前节点指向后一个节点
    // 如果后面没有节点了,将停止循环
		current = current.next;
	}
}

总结

通过yocto-queue源码的学习,加深了数据结构中链表的印象,也学习了Symbol.iterator的应用场景,s收获还是比较多吧

参考