前端涨薪功法:为什么需要队列数据结构 ?

149 阅读5分钟

一、JavaScript队列简介:理解队列及其应用

1.1 什么是队列?

队列是一种基本的数据结构,用于按照特定顺序存储数据。队列遵循先进先出(FIFO)的原则,即最先加入队列的元素最先被移除。队列的主要操作包括:

  • 入队(enqueue):向队列末尾添加一个元素。
  • 出队(dequeue):从队列头部移除一个元素。

队列在软件开发和Web应用程序中有很多实际用途。以下是一些常见的应用场景:

  • 任务调度:队列可以用于排队执行任务,确保任务按照预定顺序执行。
  • 消息传递:队列可用于传递消息或事件,帮助解耦生产者和消费者之间的依赖关系。
  • 缓冲:队列可用于减轻服务器压力,通过限制同时处理的请求数量来提高系统的稳定性。

二、使用数组实现简单队列

2.1 基于数组的队列实现

在JavaScript中,可以使用数组实现简单队列。以下是一个队列的基本实现:

class Queue {
  constructor() {
    this.items = [];
  }

  enqueue(element) {
    this.items.push(element);
  }

  dequeue() {
    if (this.isEmpty()) {
      return null;
    }
    return this.items.shift();
  }

  isEmpty() {
    return this.items.length === 0;
  }
}

这个简单的队列实现包括两个主要操作:

  • enqueue:向队列末尾添加一个元素。
  • dequeu:从队列头部移除一个元素。

2.2 基本原理

基于数组实现的队列主要利用了数组的pushshift操作。push操作用于向数组的末尾添加元素,而shift操作用于从数组的头部删除元素。这两个操作分别实现了入队和出队的功能。

然而,这种实现存在一些性能问题。shift操作需要将数组中的所有元素向前移动一位,导致时间复杂度为O(n)。在接下来的文章中,将介绍如何使用链表实现队列以提高性能。

三、使用链表进行高效的插入和删除

3.1 链表实现队列

为了提高队列的插入和删除效率,可以考虑使用链表实现。链表中的每个节点包含数据和指向下一个节点的引用。以下是一个基于链表的队列实现:

class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

class LinkedListQueue {
  constructor() {
    this.front = null;
    this.rear = null;
  }

  enqueue(element) {
    const newNode = new Node(element);
    if (this.isEmpty()) {
      this.front = this.rear = newNode;
    } else {
      this.rear.next = newNode;
      this.rear = newNode;
    }
  }

  dequeue() {
    if (this.isEmpty()) {
      return null;
    }
    const removedNode = this.front;
    this.front = this.front.next;
    if (this.front === null) {
      this.rear = null;
    }
    return removedNode.data;
  }

  isEmpty() {
    return this.front === null;
  }
}

3.2 链表与数组的比较

  1. 性能:链表在插入和删除元素时具有更优的性能,因为它不需要移动其他元素或重新分配内存。数组在访问元素时具有更快的性能,因为它允许通过索引直接访问元素。
  2. 空间要求:链表通常需要更多的内存空间,因为它们需要存储指向下一个节点的指针。然而,链表可以在内存中分散存储,而数组需要连续的内存块。
  3. 易用性:数组在许多编程语言和库中都有内置支持,因此它们通常更容易使用。链表可能需要额外的实现工作,但它们提供了更多的灵活性。

四、双端队列(Deque)

image.png

双端队列是一种特殊的队列数据结构,允许从队列的两端添加和删除元素。在JavaScript中,我们可以使用数组来实现双端队列数据结构。下面是一个使用数组实现双端队列的示例代码:

class Deque {
  constructor() {
    this.items = [];
  }

  isEmpty() {
    return this.items.length === 0;
  }

  addFront(element) {
    this.items.unshift(element);
  }

  addBack(element) {
    this.items.push(element);
  }

  removeFront() {
    if (this.isEmpty()) {
      return null;
    }

    return this.items.shift();
  }

  removeBack() {
    if (this.isEmpty()) {
      return null;
    }

    return this.items.pop();
  }

  peekFront() {
    if (this.isEmpty()) {
      return null;
    }

    return this.items[0];
  }

  peekBack() {
    if (this.isEmpty()) {
      return null;
    }

    return this.items[this.items.length - 1];
  }

  size() {
    return this.items.length;
  }
}

在这个实现中,我们使用一个数组来存储双端队列中的元素。addFront方法将一个元素添加到队列的前面,addBack方法将一个元素添加到队列的后面。removeFront方法从队列的前面删除一个元素,removeBack方法从队列的后面删除一个元素。peekFrontpeekBack方法分别返回队列的前面和后面的元素,但不修改队列。isEmpty方法检查队列是否为空,size方法返回队列中元素的数量。

使用这个实现,我们可以创建一个新的双端队列对象并使用它来执行常见的队列操作:

const deque = new Deque();

deque.addBack(1);
deque.addBack(2);
deque.addFront(0);

console.log(deque.peekFront()); // 0
console.log(deque.peekBack()); // 2

deque.removeFront();
deque.removeBack();

console.log(deque.size()); // 1
console.log(deque.peekFront()); // 1

双端队列在实际项目中有很多应用,例如在react,vue,webpack,lodash等框架或库中。

React使用了双端队列来实现一个叫做Fiber的算法。Fiber是React的新的渲染引擎,它可以将渲染任务分解为多个小的单元,并在空闲时间执行。Fiber使用了一个叫做workInProgressRoot的双端队列来存储当前需要渲染的组件,它可以从两端操作这个队列,从而实现优先级调度和中断恢复的功能。

Vue使用了双端队列来实现一个叫做nextTick的函数。nextTick是Vue的一个核心API,它可以将一个函数推迟到下一个事件循环中执行,从而保证数据更新后再执行。nextTick使用了一个叫做callbacks的双端队列来存储需要延迟执行的函数,它可以根据不同的环境选择不同的方式来触发这个队列,例如Promise,MutationObserver,setTimeout等。

Webpack使用了双端队列来实现一个叫做Tapable的库。Tapable是Webpack的核心库之一,它提供了一种插件机制,可以让开发者在Webpack的各个阶段注入自定义的逻辑。Tapable使用了一个叫做_taps的双端队列来存储插件函数,它可以根据不同的类型来执行这些函数,例如同步,异步,并行等。