【源码共读】yocto-queue 一个微型队列数据结构

简介: 【源码共读】yocto-queue 一个微型队列数据结构


yocto-queue是一个微型队列的数据结构,根据作者的介绍,如果在你一个数据量很大的数组上,大量的操作Array.pushArray.shift,那么你可以考虑使用yocto-queue来替代Array


因为Array.shift的时间复杂度是O(n),而Queue.dequeue的时间复杂度是O(1),这对于大量的数据来说,性能上的提升是非常明显的。


时间复杂度和空间复杂度


学习算法和数据结构的时候,我们经常会听到时间复杂度和空间复杂度,这两个概念是什么呢?


时间复杂度


时间复杂度是指一个算法执行所耗费的时间,它是一个函数,这个函数的变量是问题规模的函数;


通常会使用大O符号来表示时间复杂度,比如O(n)O(n^2)O(logn)等等,这就是大 O 表示法(Big O notation)。


O代表的是算法的渐进时间复杂度(asymptotic time complexity),也就是说,随着问题规模的增大,算法的执行时间的增长率和O中的函数相同,称作渐进时间复杂度。


O(1)表示算法的执行时间与问题规模无关,也就是说,不管问题规模有多大,算法执行所耗费的时间都是一样的,这种算法称为时间复杂度为常数阶的算法。


O(n)表示算法的执行时间与问题规模成正比,也就是说,随着问题规模的增大,算法执行所耗费的时间也随之增大,这种算法称为时间复杂度为线性阶的算法。


O(n^2)表示算法的执行时间与问题规模成平方比,也就是说,随着问题规模的增大,算法执行所耗费的时间呈二次方增长,这种算法称为时间复杂度为平方阶的算法。


通过上面的介绍,我们可以将O比喻成函数,O(1)就是一个常数函数,O(n)就是一个线性函数,O(n^2)就是一个平方函数,O(logn)就是一个对数函数。


说了这么多概念性的问题,不如直接来看看代码;


例如,我们有一个数组,我们要遍历这个数组,然后将数组中的每个元素都乘以2,那么我们可以这么写:

function multiplyBy2(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(arr[i] * 2);
  }
  return result;
}

这段代码的时间复杂度是O(n),因为我们遍历了数组,所以时间复杂度就是O(n)O代表这个函数,n代表问题规模,也就是数组的长度,组合起来就是O(n)


再直观一点,我们可以这么理解,当数组的长度为n时,我们需要执行n次循环,所以时间复杂度就是O(n),用代码表示就是:

function O(n) {
    for (let i = 0; i < n; i++) {
        // do something
    }
}
O(1); // O(1)
O(2); // O(2)
O(3); // O(3)
O(n); // O(n)

空间复杂度


空间复杂度是指一个算法执行所耗费的内存空间,它是一个函数,这个函数的变量是问题规模的函数;

和时间复杂度一样,空间复杂度也有O(1)O(n)O(n^2)O(logn)等等,它们的含义和时间复杂度一样,只不过它们是表示算法执行所耗费的内存空间的增长率。


当然空间复杂度计算的不是内存消耗,而是变量的个数,例如冒泡排序的空间复杂度是O(1),因为它只需要一个变量temp

function bubbleSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        const temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  return arr;
}

而快速排序的空间复杂度是O(logn),因为它需要一个变量pivot,而且它是递归的,所以空间复杂度是O(logn)

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const pivotIndex = Math.floor(arr.length / 2);
  const pivot = arr.splice(pivotIndex, 1)[0];
  const left = [];
  const right = [];
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return quickSort(left).concat([pivot], quickSort(right));
}

源码分析


上面了解了时间复杂度和空间复杂度的概念,那么我们开始正式分析yocto-queue的源码;


代码不多,可以直接通过github1s在线阅读,然后使用浏览器控制台来查看效果;


代码分析


先来看一下yocto-queue的使用方式:


  • 安装
npm install yocto-queue
  • 使用
import Queue from 'yocto-queue';
const queue = new Queue();
queue.enqueue('🦄');
queue.enqueue('🌈');
console.log(queue.size);
//=> 2
console.log(...queue);
//=> '🦄 🌈'
console.log(queue.dequeue());
//=> '🦄'
console.log(queue.dequeue());
//=> '🌈'

然后再来看一下yocto-queue的代码:

/*
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;
        }
    }
}

可以直接直接粘贴到浏览器控制台中运行


构造函数


首先来看一下Queue的构造函数:

class Queue {
  #head;
  #tail;
  #size;
  constructor() {
    this.clear();
  }
}


一个Queue类,它有三个私有属性:#head#tail#size


class中,如果属性名前面加上#,表示这个属性是私有属性,外部是无法访问的;


  • #head:指向队列的头部;
  • #tail:指向队列的尾部;
  • #size:队列的长度;


然后在构造函数中调用了this.clear()方法,这个方法的作用是清空队列,代码如下:

class Queue {
    clear() {
        this.#head = undefined;
        this.#tail = undefined;
        this.#size = 0;
    }
}

enqueue


接下来看一下enqueue方法:

class Queue {
    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++;
    }
}

enqueue方法的作用是向队列中添加一个元素;


首先创建一个Node实例,然后判断this.#head是否存在,如果存在,说明队列中已经有元素了,那么就把新创建的Node实例添加到队列的尾部;


如果this.#head不存在,说明队列中还没有元素,那么就把新创建的Node实例添加到队列的头部;

最后,队列的长度加一;


这里使用到了一个Node类,它的作用是用来保存队列中的元素的,代码如下:

class Node {
    value;
    next;
    constructor(value) {
        this.value = value;
    }
}
  • value:指向的是队列中的元素;
  • next:指向下一个Node实例;


dequeue


接下来看一下dequeue方法:

class Queue {
    dequeue() {
        const current = this.#head;
        if (!current) {
            return;
        }
        this.#head = this.#head.next;
        this.#size--;
        return current.value;
    }
}

dequeue方法的作用是从队列中移除一个元素;


首先获取队列的头部元素,然后判断current是否存在,如果不存在,说明队列中没有元素,那么就直接返回;


如果current存在,说明队列中有元素,那么就把队列的头部元素移除,然后把队列的长度减一;


最后,返回移除的元素;


图例


一个队列结构只会有两个操作:入队和出队,下面通过一个图例来说明一下;


入队时,会把新的元素添加到队列的尾部;


出队时,会把队列的头部元素移除;

image.png

上面的图例中,Node0表示队列的头部元素,Node_n表示队列的尾部元素;


Current0表示队列中的第一个元素,Current_n表示队列中的最后一个元素;


在队列中,只会关心头部和尾部的元素,头部的元素用于弹出队列,尾部的元素用于添加元素;


在上面的图例中,如果我们执行dequeue方法,那么就会把Node0移除,然后把Node1设置为队列的头部元素;

image.png


上面的dequeue方法执行完毕后,队列的头部元素就变成了Node1


Node0因为没有任何引用指向它,所以会被垃圾回收机制回收;


如果我们执行enqueue方法,那么就会把新的元素添加到队列的尾部:

image.png

上面的enqueue方法执行完毕后,队列的尾部元素就变成了newNode


迭代器


yocto-queue到最后还提供了一个迭代器,用于遍历队列中的元素;

class Queue {
    // ...
    *[Symbol.iterator]() {
        let current = this.#head;
        while (current) {
            yield current.value;
            current = current.next;
        }
    }
}

上面的代码中,使用了一个generator函数,它会返回一个迭代器;


迭代器中,会从队列的头部元素开始遍历,然后把每个元素的值返回出去;


迭代器的使用


迭代器是es6中的一个新特性,它可以用于遍历数据结构中的元素,使用起来也非常简单,只需要在数据结构上调用Symbol.iterator方法即可;

const obj = {
    [Symbol.iterator]() {
        let i = 0;
        return {
            next() {
                return {
                    value: i++,
                    done: i > 10
                };
            }
        };
    }
};
for (const item of obj) {
    console.log(item);
}

上面的代码中,我们定义了一个对象,它的Symbol.iterator方法返回了一个迭代器;


一个迭代器是一个对象,它有一个next方法,每次调用next方法,都会返回一个对象,这个对象中包含了当前元素的值和一个done属性,表示是否已经遍历完毕;


可以使用generator函数来简化上面的代码:

const obj = {
    *[Symbol.iterator]() {
        let i = 0;
        while (i < 10) {
            yield i++;
        }
    }
};
for (const item of obj) {
    console.log(item);
}

上面的代码中,我们使用了generator函数来简化迭代器的定义;


总结


yocto-queue是一个非常简单的队列实现,它的核心是使用了链表来存储数据;


链表的优点是插入和删除元素的时间复杂度是O(1),但是查找元素的时间复杂度是O(n),不过对于队列来说,查找元素的操作是不需要的;


对比于数组,每次插入和删除元素都需要移动元素,所以时间复杂度是O(n),但是查找元素的时间复杂度是O(1)


所以正如文章开头,作者提到的,对于一个大数组来实现队列,效率是非常低的,而应该使用链表来实现(因为这个库的核心实现就是链表,所以肯定优先推荐用这个库=);


通过阅读yocto-queue的源码,我们学习到了很多:


  • 时间复杂度、空间复杂度的概念
  • 链表的实现
  • 如何使用es6Symbol.iterator来实现可迭代对象;
  • 如何使用es6generator函数来实现迭代器;
  • 数组、队列、链表的区别;


目录
相关文章
|
6月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
179 1
|
4月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
219 3
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
9月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
373 77
|
7月前
|
存储 自然语言处理 数据库
【数据结构进阶】AVL树深度剖析 + 实现(附源码)
在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。
561 19
|
7月前
|
数据库 C++
【数据结构进阶】红黑树超详解 + 实现(附源码)
本文深入探讨了红黑树的实现原理与特性。红黑树是一种自平衡二叉搜索树,通过节点着色(红/黑)和特定规则,确保树的高度接近平衡,从而实现高效的插入、删除和查找操作。相比AVL树,红黑树允许一定程度的不平衡,减少了旋转调整次数,提升了动态操作性能。文章详细解析了红黑树的性质、插入时的平衡调整(变色与旋转)、查找逻辑以及合法性检查,并提供了完整的C++代码实现。红黑树在操作系统和数据库中广泛应用,其设计兼顾效率与复杂性的平衡。
1248 3
|
8月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
192 11
|
8月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
9月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
297 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
9月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
207 7

热门文章

最新文章