数据结构与算法-队列与双端队列

169 阅读4分钟

本篇文章主要是《学习JavaScript数据结构与算法(第3版)》作者: [巴西]洛伊安妮·格罗纳,第5章节提供,本文仅学习并记录笔记。

( ps: 有些代码与图书源码出入,本人只是稍微改进了一两行,问题不大,注释类是本人对其代码的解读,不会照搬图书,不懂或建议可评论,一起学习进步)

本篇文章内容有:

  • 数据结构 - 队列

  • 数据结构 - 双端队列

  • 算法 - 击鼓传花

  • 算法 - 检测是否是回文

队列

数据结构:

遵循先进先出(FIFO)原则的一组有序项。队列尾部添加新元素,并从顶部移除元素。 最新添加的元素必须排在队列的末尾。

应用场景:

排队买票、批量打印

队列方法

  • enqueue:向队列尾部添加一个(或多个)新的项
  • dequeue:移除队列的第一项(即排在队列最前面的项)并返回被移除的元素

队列不做任何变动(不移除元素,只返回元素信息)。该方法在其他语言中也可以叫front方法

  • peek:返回队列的第一个元素——最先被添加,也将是最先被移除的元素。
  • isEmpty:如果队列中不包括任何元素,则返回true,有元素则返回false
  • size:返回队列的元素个数(与数组的length相似)
  • clear:清空队列
  • toString: 队列转字符串

创建一个Queue类

也可以使用数组和对象来存储队列中的元素。为了获取元素更加高效,本篇选用方案是由对象创建队列结构。

实现代码如下:

// 创建队列类
class Queue {
  constructor() {
    // 控制队列的大小
    this.count = 0;
    // 追踪第一个元素
    this.lowestCount = 0;
    this.items = {};
  }

  // 向队列尾部添加一个(或多个)新的项
  enqueue(elements) {
    this.items[this.count] = elements;
    this.count++;
  }

  // 移除队列的第一项(即排在队列最前面的项)并返回被移除的元素
  dequeue() {
    if (this.isEmpty()) return;
    // 暂存删除元素的快照
    const result = this.items[this.lowestCount];
    // 删除第一项
    delete this.items[this.lowestCount];
    // 第一项的key更新
    this.lowestCount++;
    return result;
  }

  // 返回队列的第一个元素——最先被添加,也将是最先被移除的元素。
  // 队列不做任何变动(不移除元素,只返回元素信息)。
  peek() {
    if (this.isEmpty()) return;
    return this.items[this.lowestCount];
  }

  // 如果队列中不包括任何元素,则返回true,有元素则返回false
  isEmpty() {
    return this.count === 0;
  }

  // 返回队列的元素个数(与数组的length相似)
  size() {
    return this.count;
  }

  // 清空队列
  clear() {
    this.items = {};
  }

  // 队列转字符串
  toString() {
    if (this.isEmpty()) return "";
    let result = this.items[this.lowestCount];
    for (let i = this.lowestCount; i < this.count; i++) {
      result = `${result},${this.items[i]}`;
    }
    return result;
  }
}

双端队列

数据结构

是一种允许数据同时从前端和后端添加和移除元素的特殊队列。

由于双端队列同时遵守了先进先出后进先出原则,可以说是它把队列和栈相结合的一种数据结构。

应用场景

存储一系列的撤销操作

双端队列方法

  • addFront:向队列顶部部添加一个新的项
  • addBack:向队列尾部添加一个新的项
  • removeFront:移除队列的第一项(即排在队列最前面的项)并返回被移除的元素
  • removeBack:移除队列尾部元素并返回被移除的元素
  • peekFront:返回队列的第一个元素——最先被添加,也将是最先被移除的元素。
  • peekBack:返回队列的最后一个元素——最后被添加,也将是最后被移除的元素。
  • isEmpty:如果队列中不包括任何元素,则返回true,有元素则返回false
  • size:返回队列的元素个数(与数组的length相似)
  • clear:清空队列
  • toString: 队列转字符串

创建双端队列Deque

实现代码如下:

class Deque {
  constructor() {
    // 控制队列的大小
    this.count = 0;
    // 追踪第一个元素
    this.lowestCount = 0;
    this.items = {};
  }

  // 向队列头部添加一个新的项
  addFront(element) {
    // 队列为空,直接向队列尾部添加
    if (this.isEmpty()) {
      this.addBack(element);
    }
    // 删除过元素,顶部key大于0
    else if (this.lowestCount > 0) {
      this.lowestCount--;
      this.items[this.lowestCount] = element;
    }
    // 没有删除过元素,lowestCount还是默认值0,需要将其他元素全部往后移
    else {
      // 除了队列的顶部,其他往后移
      for (let i = this.count; i < 0; i--) {
        this.items[i] = this.items[i - 1];
      }
      //   添加队列顶部一个项
      this.items[0] = element;
      //   更新数据
      this.count++;
      this.lowestCount = 0;
    }
  }

  // 向队列尾部添加一个新的项
  addBack(element) {
    this.items[this.count] = element;
    this.count++;
  }

  // 移除队列的第一项(即排在队列最前面的项)并返回被移除的元素
  removeFront() {
    if (this.isEmpty()) return;
    // 暂存删除元素的快照
    const result = this.items[this.lowestCount];
    // 删除第一项
    delete this.items[this.lowestCount];
    // 第一项的key更新
    this.lowestCount++;
    return result;
  }

  // 移除队列的尾部
  removeBack() {
    if (this.isEmpty()) return;
    // 暂存删除元素的快照
    const result = this.items[this.count];
    // 删除第一项
    delete this.items[this.count];
    // 队列长度更新
    this.count--;
    return result;
  }

  // 返回队列顶部
  peekFront() {
    if (this.isEmpty()) return;
    return this.items[this.lowestCount];
  }

  // 返回队列顶部
  peekBack() {
    if (this.isEmpty()) return;
    return this.items[this.count];
  }

  // 如果队列中不包括任何元素,则返回true,有元素则返回false
  isEmpty() {
    return this.count === 0;
  }

  // 返回队列的元素个数(与数组的length相似)
  size() {
    return this.count;
  }

  // 清空队列
  clear() {
    this.items = {};
  }

  // 队列转字符串
  toString() {
    if (this.isEmpty()) return "";
    let result = this.items[this.lowestCount];
    for (let i = this.lowestCount; i < this.count; i++) {
      result = `${result},${this.items[i]}`;
    }
    return result;
  }
}

使用队列和双端队列解决问题

1. 击鼓传花游戏 - 循环队列

数人或几十人围成圆圈坐下,其中一人拿花;另有一人背着大家或蒙眼击鼓,鼓响时众人开始依次传花,至鼓停止为止。此时花在谁手中,谁就退出游戏,直到只剩下一个人,那个人就赢了。

主要利用循环队列,每次传花时,花到谁手里,谁就是队列顶部,每次循环就将队列顶部元素移除,并且将它保存到淘汰名单中,直到只剩下一个人。

实现代码如下

function hotPotato(elementsList, num) {
  const queue = new Queue();
  const elimitatedList = [];

  // 将元素项依次存入队列中
  for (let i = 0; i < elementsList.length; i++) {
    queue.enqueue(elementsList[i]);
  }

  // 直到队列元素只剩下一个
  while (queue.size() > 1) {
    // 循环了多少个元素,顺序循环替换
    for (let i = 0; i < num; i++) {
      queue.enqueue(queue.dequeue());
    }
    // 去除队列的尾部元素,并且记录在失败者的名单里
    elimitatedList.push(queue.dequeue());
  }

  return {
    elimitatedList,
    // 返回队列最后一个元素并清空queue队列
    winner: queue.dequeue(),
  };
}

核心代码queue.enqueue(queue.dequeue())是表示队列的尾部移除,并添加到队列顶部中,这时候队列尾部就变成了顶部,从而循环。循环了num次数后,就将队列顶部移除,也就是说,每次循环都会把队列顶部元素移除,直到只剩一个元素,跳出循环,输出结果,清空队列。

我们来测试一下代码运行结果:

const names = ["july", "lily", "lucas", "jack", "mary"];
// 循环项数最多12,最小2
const result = hotPotato(names, Math.random(10) + 2);

result.elimitatedList.forEach((n) => {
  console.log(`${n}在击鼓传花游戏中被淘汰。`);
});
console.log(`胜利者:${result.winner}`);

2. 回文检查器 - 双端队列

什么是回文?

回文是正反都能读通的单词、词组、数或一系列字符的系列,例如:madam,12321

而最简单检测回文的方法是反排序,反排序后和原文一样,就是回文

function isPalindrome(string) {
  // undefined / null / '' / 0 / false等均返回false
  if (!string) return false;
  string = String(string).toLocaleLowerCase(); // 统一小写后进行对比,如 Nan 转后是 naN
  return string.split("") === string.split("").reverse().join("");
}

接下来我们用一个双端队列来解决这个问题。 经由顶部与尾部进行对比,如果出现一对不相等,说明不是回文

function palindromeChecker(string) {
  if (string === undefined || string === null || !string.length) return false;
  const deque = new Deque();
  const lowerString = string.toLocaleLowerCase().split(" ").join("");
  let isEqual = true;
  let firstChar, lastChar;

  for (let i = 0; i < lowerString.length; i++) {
    // 将小写字符串一个一个的存入双端队列中
    queue.addBack(lowerString.charAt(i));
  }

  // 检测队列长度等于1时或检测队列顶部与尾部元素不相等时,跳出循环
  while (deque.size() > 1 && isEqual) {
    // 将顶部元素返回并删除
    firstChar = queue.removeFront();
    // 将尾部元素返回并删除
    lastChar = queue.removeBack();
    // 若出现顶部元素与尾部元素不不相等,说明不是回文,跳出循环
    if (firstChar !== lastChar) {
      isEqual = false;
    }
  }

  return isEqual;
}

如果你想要实现的全部方式,可以去学习JavaScript数据结构与算法(第3版)源码查看代码案例。