前言
本人是一个刚入行的菜鸡前端程序员,写这个文章的目的只是为了记录自己学习的笔记与成果,如有不足请大家多多指点。
在上一篇文章我们已经学习了栈。本篇文章我们将了解队列,队列和栈非常类似,但是使用了与先进后出不同的原则。我们同样要学习双端队列的工作原理。双端队列是一种将栈原则和队列的原则混合在一起的数据结构。
队列数据结构
队列是遵循先进先出(FIFO -> first in - first out)原则的一组有序的项。队列在尾部添加元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。
在现实中,最常见的例子就是排队。

创建队列
我们需要创建自己的类来表示一个队列。先从最基本的声明类开始。
class Queue {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
}
首先需要一个用于存储队列中元素的数据结构。我们可以使用数组,就像上一篇文章的 Stack 类那样。但是,为了写出一个在获取元素时更高效的数据结构,我们将使用一个对象来存储我们的元素。你会发现Queue类和Stack类非常类似,只是添加元素和移除元素的原则不同。同时我们声明一个count属性来帮助我们控制队列的大小。由于我们将要从前端移除元素,同样需要一个变量来帮助我们追踪第一个元素。因此,声明一个lowestCount变量。
接下来需要声明一些队列可用的方法
- enqueue(element(s)): 向队列尾部添加一个(或多个)新的项。
- dequeue(): 移除队列中的第一项(即排在队列最前面的项)并返回被移除的元素。
- peek(): 返回队列中第一个元素——最先被添加,也将是最先被移除的元素。
- isEmpty(): 如果队列中不包含任何元素,返回true,否则返回false。
- size(): 返回队列中包含的元素个数,与数组的 length 属性类似。
向队列中添加元素 - enqueue()
enqueue(element) {
this.items[this.count] = element;
this.count++;
}
items属性是一个JavaScript对象,它是一个键值对的集合。要向队列中加入一个元素的话,我们要把count变量作为items对象中的键,对应的元素作为它的值。将元素加入队列后,我们将count变量加1。
从队列中移除元素 - dequeue()
该方法负责从队列中移除项。由于队列遵循先进先出原则,先添加的项也是最先被移除的项。
dequeue(){
if(this.isEmpty()) {
return undefined; //如果队列为空,返回undefined
}
const result = this.items[this.lowestCount]; //暂存队列头部的值
delete this.items[this.lowestCount]; // 删除
this.lowestCount++; //lowestCount属性加1,继续追踪头部元素
return result; //返回被移除的元素
}
查看队列头元素 - peek()
peek() {
if(this.isEmpty()) {
return undefined
}
return this.items[this.lowestCount]
}
检查队列是否为空并获取它的长度 - isEmpty(), size()
isEmpty() {
return this.count - this.lowestCount === 0;
}
队列中的长度就是 count 与 lowestCount 之间的差值。
所以要实现size方法,我们只需要返回这个差值即可
size() {
return this.count - this.lowestCount;
}
可以像下面这样重新实现isEmpty方法
isEmpty() {
return this.size() === 0;
}
清空队列
clear() {
this.items = {};
this.count = 0;
this.lowestCount = 0;
}
创建 toString 方法
toString() {
if(this.isEmpty()) {
return '';
}
let objString = `${this.items[this.lowestCount]}`;
for(let i = this.lowestCount + 1; i < this.count; i++) {
objString = `${objString},${this.items[i]}`;
}
return objString;
}
双端队列数据结构
双端队列是一种允许我们同时从前端和后端添加和移除元素的特殊队列。
双端队列在现实生活中的例子有电影、餐厅中排队的队伍等。举个例子,一个刚买了票的人如果只是还需要问一些简单的信息,就可以直接回到队伍的头部。另外,在队伍末尾的人如果赶时间或者有事,他可以直接离开队伍。
创建Deque类
class {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
}
既然双端队列是一种特殊的队列,我们可以看的其构造函数中的部分代码吗和队列相同、包括相同的内部属性和以下方法:isEmpty、clear、size和toString。
由于双端队列允许在两端添加和移除元素,还会有下面几个方法。
- addFront(element): 该方法在双端队列前端添加新的元素。
- addBack(element): 该方法在双端队列的后端添加新的元素(实现方法和Queue类中的enqueue方法相同)。
- removeFront(): 该方法从双端队列的前端移除第一个元素(实现方法和Queue类中的dequeue方法相同)。
- removeBack(): 该方法从双端队列后端移除第一个元素(实现方法和Stack类中的pop方法一样)。
- peekFront(): 该方法返回双端队列前端的第一个元素(实现方法和Queue类中的peek方法一样)
- peekBack(): 该方法返回双端队列后端的第一个元素(实现方法和Stack类中的peek方法一样)
向双端队列前端添加元素
由于已经实现了部分方法,我们将只专注于 addFront 方法的逻辑。
addFront(element) {
if (this.isEmpty()) {
this.addBack(element)
} else if (this.lowestCount > 0 ) {
this.lowestCount--;
this.items[this.lowestCount] = element;
} else {
for (let i = this.count; i > 0; i-- ) {
this.items[i] = this.items[i - 1];
}
this.count++;
this.lowestCount = 0;
this.items[0] = element;
}
}
要将一个元素添加到双端队列的前端,存在三种场景。
第一种是这个双端队列是空的。在这种情况下,我们可以执行 addBack 方法。
第二种场景是一个元素已经被从双端队列的前端移除,也就是说lowestCount 属性大于等于1.这种情况下,我们只需要将lowestCount 属性减1并将新元素的值放在这个键的位置上即可。
第三种场景是 lowestCount 为 0 的情况。我们可以设置一个负值的键,同时更新用于计算双端队列长度的逻辑,使其也能包含负键值。
使用队列和双端队列来解决问题
循环队列——击鼓传花游戏
由于队列经常被应用在计算机领域和我们的现实生活种,就出现了一些队列的修改版本,这其中有一种叫做循环队列。循环队列的一个例子就是击鼓传花游戏。在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈、结束游戏。重复这个过程,直到只剩下一个孩子(胜者)。
在下面这个示例中,我们要实现一个模拟的击鼓传花游戏。
function hotPotato(elementsList, num) {
cont queue = new Queue(); //存储参加游戏的人物
cont 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 {
elimnated: elimitatedList;
winner: queue.dequeue();
}
}
双端队列——回文检查器
回文就是正反都能读通的单词、词组、数或一系列字符的序列,例如 madam 或 racecar 。
function palindromeChecker(aString) {
if(aString === undefined || aString === null || (aString !== null && aString.length === 0)) {
return false; // 需要检查传入的字符串是否合法
}
const deque = new Deque();
const lowerString = aString.toLocaleLowerCase().split('').join('');
let isEqual = true;
let firstChar, lastChar;
for (let i = 0; i < lowerString.length; i++) {
deque.addback(lowerString.charAt[i]) //charAt()返回指定位置的字符
}
while (deque.size() > 1 && isEqual) {
firstChar = deque.removeFront();
lastChar = deque.removeBack();
if (firstChar !== lastChar) {
isEqual = false;
}
}
return isEqual;
}
小结
本篇文章介绍了队列这种数据结构。我们实现了自己的队列算法,学习了如何通过 enqueue 方法和 dequeue 方法并遵循先进先出原则来添加和移除元素。我们同样学习了双端队列数据结构,如何将元素添加到双端队列的前端和后端,以及如何将元素从双端队列的前端和后端移除。
我们也讨论了如何用队列和双端队列数据结构解决两个经典的问题:击鼓传花(使用一个修改过的队列:循环队列) 和回文检查(使用双端队列)。
下一篇,我们将学习链表。这是一种比数组更复杂的数据结构。