【前端算法学习】数据结构之“队列”

回顾

上一章,我们学习了“栈”这个数据结构,我们在JS中可以用push()pop()来模拟入栈和出栈。

这一章我们将学习“队列”这个数据结构,同时我们也会使用JS代码来介绍、模拟实现队列的操作。

什么是队列

队列与我们上一章学习的栈十分相似;但是与栈不同的是,队列遵循FIFO( “先进先出” first in first out)原则的一组有序的项。队列在尾部添加元素,并从顶部移除元素。最新添加的元素就处于队列的顶部。

在现实中,队列就好比我们排队打饭,或者排队办理银行业务;排在第一位的人会先接受服务

排队图片

JS中如何实现队列?

在大致理解队列这个数据结构之后,我们来看看如何在JS模拟队列的行为吧!

与上一章相似,下面我们用TS,模拟队列的操作行为。

接下来需要声明一些队列可用的方法。

  • enqueue(element(s)):向队列尾部添加一个(或多个)新的项。

    使用数组的push方法模拟

  • dequeue():移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。

    队列遵循先出,移除数组中的第一项,可以联想到数组的shift方法

  • front():返回队列中第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息——与Stack类的peek方法非常类似)。

  • isEmpty():如果队列中不包含任何元素,返回true,否则返回false

  • size():返回队列包含的元素个数,与数组的length属性类似。

完整的Queue类

根据上一节的想法,我们使用TS实现了完整的Queue类,用于模拟队列的操作。

class Queue {
  item: any[] = []

  // 向队尾添加一个或多个元素
  _enqueue(element: any) {
    this.item.push(element)
  }

  // 移除队列的第一项
  _dequeue() {
    this.item.shift()
  }

  // 返回队列的第一个元素
  _front(): any {
    return this.item[0]
  }

  // 判断队列是否为空
  _isEmpty(): boolean {
    return this.item.length === 0
  }

  // 返回队列的长度
  _size(): number {
    return this.item.length
  }

  // 清空队列
  _clear(){
    this.item = []
  }

  //打印队列内容
  _print(){
    console.log(...this.item)
  }
}

简单的使用Queue

首先我们实例化我们的Queue类,并验证类中的方法是否可用。

const queue = new Queue()
console.log(queue._isEmpty()) //输出 true

接下来,添加一些元素(添加"John"和"Jack"两个元素——你可以向队列添加任何类型的 元素):

queue.enqueue("John");
queue.enqueue("Jack");
queue.enqueue("Camila");

再执行一些其他的命令:

queue._print();
console.log(queue._size()); //输出3
console.log(queue._isEmpty()); //输出false
queue._dequeue();
queue._dequeue();
queue._print(); // 输出 Camila

如果打印队列的内容,就会得到John、Jack和Camila这三个元素。因为我们向队列添加了 三个元素,所以队列的大小为3(当然也就不为空了)。

这里我们进行了三个元素的入列,以及两次出列,最后,再次打印队列内容时,就只剩Camila一个元素了。前两个入列的元素出列了,最后 入列的元素也将是最后出列的。也就是说,我们遵循了先进先出原则。

优先队列

队列大量应用在我们的生活中,我们在上一节实现的默认队列也会有一些修改版本。

优先队列就是其中一个修改版本。元素的添加和移除都是基于优先级的。用一个现实中的例子来表示就是:飞机的头等舱与商务舱的乘客登机的优先级是要高于经济舱的乘客的。在我们国家,军人在办理银行或者医院的业务时,也是拥有较高的优先级的。

实现一个优先队列,有两种选项:

  1. 设置优先级,然后在正确的位置添加元素;
  2. 或者用入列操作添加元素,然后按照优先级移除它们。
// 优先队列
class PriorityQueue {
  item: any[] = []

  // 向队尾添加一个或多个元素
  _enqueue(element: any, priority: number) {
    const queueElement = {
      element,
      priority
    }
    if (this._isEmpty()) {
      // 队列为空,直接添加
      this.item.push(queueElement)
    } else {
      // 队列不为空,需要判断优先级
      let added = false
      for (let i = 0; i < this.item.length; i++) {
        if (queueElement.priority < this.item[i].priority) {
          // 优先级小于当前项,插入到当前项前面
          this.item.splice(i, 0, queueElement)
          added = true
          break
        }
      }
      if(!added){
        // 优先级大于所有项,插入到队列末尾
        this.item.push(queueElement)
      }
    }
  }

  // 移除队列的第一项
  _dequeue() {
    this.item.shift()
  }

  // 返回队列的第一个元素
  _front(): any {
    return this.item[0]
  }

  // 判断队列是否为空
  _isEmpty(): boolean {
    return this.item.length === 0
  }

  // 返回队列的长度
  _size(): number {
    return this.item.length
  }

  // 清空队列
  _clear() {
    this.item = []
  }

  //打印队列内容
  _print() {
    console.log(...this.item)
  }
}

默认的Queue类和PriorityQueue类实现上的区别是,要向PriorityQueue添加元素,需要创建一个特殊的元素queueElement。这个元素包含了要添加到队列的元素(它可以是任意类型) 及其在队列中的优先级。其余的方法与Queue类类似。

我们在这里实现的优先队列称为最小优先队列,因为优先级的值较小的元素被放置在队列最 前面(1代表更高的优先级)。最大优先队列则与之相反,把优先级的值较大的元素放置在队列最 前面。

const priorityQueue = new PriorityQueue();
priorityQueue._enqueue("John", 2);
priorityQueue._enqueue("Jack", 1);
priorityQueue._enqueue("Camila", 1);
priorityQueue._print();

在运行上面的代码之后,我们发现,入队时,会根据等级进行处理;如果要添加元素的priority值大于任何已有的元素,把它添加到队列的末尾就行了,如果要要添加的元素等级,小于之前我们添加的,那么久与已经入队的元素等级进行逐一对比,直到找到合适的位置,插入进队列。

以上代码是一个使用PriorityQueue类的示例。在下图中可以看到每条命令的结果

wlrk24

小结

这一章我们学习了队列这种数据结构。我们实现了自己的队列算法,学习了如何通过 enqueue方法和dequeue方法添加和移除元素。我们还学习了两种非常著名的特殊队列的实现: 优先队列和循环队列(使用击鼓传花游戏的实现)。

在下一章中,我们将学习链表,一种比数组更复杂的数据结构。但在这之前,也许你想去看看有什么算法题,是可以用队列这一数据结构来实现解题的。

posted @ 2023-06-25 16:39  捡破烂的小z  阅读(50)  评论(0编辑  收藏  举报