本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
本篇是源码共读第32期 | yocto-queue队列链表,点击了解本期详情
1. 什么是链表
单项链表:
链表是一种在物理上非连接、非顺序读数据结构,由若干 节点(node)所组成;单向链表的每一个节点包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next 结构图:
链表的第一个节点被称为头节点(head),最后一个节点被称为尾节点(tail),尾节点的next指针指向空(null) 与数组按照下标来随机查找元素不同,对于链表的其中一个节点A,只能根据节点A的next指针来找到该节点的下一个节点B,再根据节点B的next指针找到下一个节点C...一级一级,单线传递。想让每个节点都能回溯到它的前置节点,可以使用双向链表。
双向链表
双向链表比单向链表复杂一点,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。
链表的存储方式
数组在内存中的存储方式是顺序存储,链表在内存中的存储方式是随机存储。 数组在内存中占用了连续完整的存储空间,而链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。
数组的内存分配方式图:
链表的内存分配方式图:
图中的箭头代表链表节点的next指针
数组VS链表
数组查询快,插入慢;链表查询慢,插入快
数组的优势在于能够快速定位元素,对于读操作多、写操作少的场景来说,用数组更合适一点; 链表的优势在于能够灵活地进行插入和删除操作,如果频繁插入、删除元素,则链表更合适一点。
2. 源码
// index.js
// 定义节点类: 具有数据和指向下一个节点的指针
class Node(){
value;
next;
constructor(value){
this.value = value
}
}
export default class Queue(){
// 定义头部、尾部、节点数等私有变量,内部可以访问,不能删除;外部不能访问
// #head: 可以实现类似数组的 shift
// #tail: 可以实现类似数组的 push
// #size: 记录节点数
#head;
#tail;
#size;
// 构造器 先清空队列数据 首尾设为undefined size为0
constructor(){
this.clear()
}
// 入队: 新元素总是添加到队列的末尾
enquene(value){
// new 一个以入队值为数据的节点
const node = new Node(value)
// 若有头节点,把节点赋给表尾及表尾指针域next,即入队
// 若无头节点,则新节点既是头节点也是尾节点
// 节点数 +1
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 }
// 把队首元素删除,队首元素的next节点成为新的队首
this.$head = current.next;
this.$size--;
// 返回原head节点的值,即删除的数据
return current.value;
}
// 获取节点数
get size(){
return this.#size;
}
// 清空队列
clear(){
this.#head = undefined;
this.#tail = undefined;
this.#size = 0
}
// 为每一个对象定义了默认的迭代器,使队列实例可以使用... 展开或者for...of循环
*[Symbol.iterator]() {
// 迭代器将current 变量设置为队列的head属性。
let current = this.#head;
while(current){
// 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值
// 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式
// 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回到对象的value的属性值
// 如果该函数没有return语句,则返回的对象的value属性值为undefined
yield current.value
// current 变量更新为队列的下一个节点,循环继续
current = current.next
}
}
}
// 使用
import Queue from 'index.js'
let queue = new Queue()
queue.enqueue('test')
queue.enqueue('hello')
queue.enqueue('world')
console.log(queue.dequeue()) // 'test'
console.log(queue.size) // 2
console.log(...queue) // 'hello world'
总结: 通过阅读 yocto-queue源码,学习了链表的实现方式,以及迭代器的使用。数组和队列两种数据结构的区别,和使用场景。