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

70 阅读4分钟

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

本篇是源码共读第32期 | yocto-queue队列链表,点击了解本期详情

1. 什么是链表

单项链表:

链表是一种在物理上非连接、非顺序读数据结构,由若干 节点(node)所组成;单向链表的每一个节点包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next 结构图:

image.png

链表的第一个节点被称为头节点(head),最后一个节点被称为尾节点(tail),尾节点的next指针指向空(null) 与数组按照下标来随机查找元素不同,对于链表的其中一个节点A,只能根据节点A的next指针来找到该节点的下一个节点B,再根据节点B的next指针找到下一个节点C...一级一级,单线传递。想让每个节点都能回溯到它的前置节点,可以使用双向链表。

双向链表

双向链表比单向链表复杂一点,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。

image.png

链表的存储方式

数组在内存中的存储方式是顺序存储,链表在内存中的存储方式是随机存储。 数组在内存中占用了连续完整的存储空间,而链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。

数组的内存分配方式图:

image.png 链表的内存分配方式图:

image.png 图中的箭头代表链表节点的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源码,学习了链表的实现方式,以及迭代器的使用。数组和队列两种数据结构的区别,和使用场景。