本篇文章主要是
《学习JavaScript数据结构与算法(第3版)》作者: [巴西]洛伊安妮·格罗纳,第5章节提供,本文仅学习并记录笔记。( ps: 有些代码与图书源码出入,本人只是稍微改进了一两行,问题不大,注释类是本人对其代码的解读,不会照搬图书,不懂或建议可评论,一起学习进步)
本篇文章内容有:
-
数据结构 - 队列
-
数据结构 - 双端队列
-
算法 - 击鼓传花
-
算法 - 检测是否是回文
队列
数据结构:
遵循先进先出(FIFO)原则的一组有序项。队列尾部添加新元素,并从顶部移除元素。 最新添加的元素必须排在队列的末尾。
应用场景:
排队买票、批量打印
队列方法
enqueue:向队列尾部添加一个(或多个)新的项dequeue:移除队列的第一项(即排在队列最前面的项)并返回被移除的元素
队列不做任何变动(不移除元素,只返回元素信息)。该方法在其他语言中也可以叫front方法
peek:返回队列的第一个元素——最先被添加,也将是最先被移除的元素。isEmpty:如果队列中不包括任何元素,则返回true,有元素则返回falsesize:返回队列的元素个数(与数组的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,有元素则返回falsesize:返回队列的元素个数(与数组的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版)源码查看代码案例。