笔记:【若川视野 x 源码共读】 第32期 | yocto-queue 队列 链表

59 阅读6分钟

1、前言

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)
    

v2-7e1e0a35745f5243bb228de1346060b3_r.jpg 综上:

    对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组

    对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表

数组与链表的区别