队列是一种先进先出(FIFO)的有序结构,它在队尾添加新元素,在顶部移除元素。在生活中我们最常见的就是排队了,谁在前就先服务谁。在程序中也可以经常看到它的身影,比如JS的事件队列。所以,学习队列对我们理解生活和计算机十分有帮助。
我们根据队列先进先出的特征,在JS中来模拟实现这个结构。我会创建一个Queue
类,里面会包含以下方法:
接下来我会使用ES6的语法来实现它们。
实现Queue
类
首先创建一个Queue
类,使用一个对象来模拟队列。在构造函数中,会有一个count
属性和lowestCount
属性,它们的作用分别是控制队列的长度以及追踪当前队列的第一个元素。
class Queue {
constructor () {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
}
实现 enqueue
方法
这个方法实现很简单,就是往队列的后一项增加一个元素,使用count
作为下标。
enqueue (element) {
this.items[this.count++] = element
}
实现 isEmpty
方法
我们使用一个对象来实现的队列,就那么不能简单像数组那样直接返回this.count
的值,因为这个下标是代表的一个元素,比如从前面移除了一个元素,我们需要让lowestCount
往后移一位来跟踪第一个元素,后面的元素的序号是不能变的。如果当前lowestCount
和count
相等的话说明当前队列为空了。可能不是很容易理解,我画张图吧。
为了容易理解,我画一个类似数组的结构。这下我们可以很容易看到,只要this.lowest和this.count相等就表明队列已经空了(好好理解一下)。这下代码就出来了:
isEmpty () {
return this.count - this.lowestCount === 0;
}
(我相信会有人看出来,其实我本人就是不容易理解所以画了图,嘿嘿。)
实现 size
方法
有了上面的理解,这个实现就简单了。
size () {
return this.count - this.lowestCount;
}
实现 dequeue
方法
要删除一个元素需要先判断队列是否为空,有了上面的方法作铺垫就好说了。分三步走:
this.lowestCount
返回当前第一个元素,并且存储到一个变量result
中delete
操作符来删除一个对象中的属性this.lowestCount
往后一位,并且返回这个result
dequeue () {
if (this.isEmpty()) {
return undefiend;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
实现 peek
方法
这个方法是返回队列第一个元素,不用多说了吧,先判断为空,再利用this.lowestCount
返回第一个元素。
peek () {
if (this.isEmpty()) {
return undefiend;
}
return this.items[this.lowestCount];
}
实现 clear
方法
这个方法也很简单,令属性全部设为初始值就可以了。
clear () {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
实现 toString
方法
这个方法主要将队列每个元素转化为字符串,每个对象都有默认的toString
方法,我们自己实现一下。实现方法其实也不是特别难,借助了递归思想。
toString() {
if (this.isEmpty()) {
return '';
}
let objString = `${this.items[this.lowestCount]}`;
for (let i = this.lowestCount + 1; i > 0; i--) {
objString = `${objString},${this.items[i]}`
}
return objString;
}
整个代码如下:
class Queue {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
enqueue(element) {
this.items[this.count++] = element;
}
dequeue() {
if (this.empty()) {
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
peek() {
if (this.empty()) {
return undefined;
}
return this.items[this.lowestCount];
}
empty() {
return this.count - this.lowestCount === 0;
}
size() {
return this.count - this.lowestCount;
}
toString() {
if (this.empty()) {
return '';
}
let objString = `{this.items[this.lowestCount]}`;
for (let i = this.lowestCount + 1; i < this.count; i++) {
objString = `${objString},${this.items[i]}`;
}
return objString;
}
}
这种队列和上面的队列不一样,它遵循了队列和栈的的特征,既可以先进先出,又可以后进先出,是一种很特殊的结构。这对我们解决问题提供了极大的帮助。还是拿排队买票来说,排在第一位的可以先服务,最后一个如果不想排了,它也可以先离开。下面我们来实现这个双向队列。根据这个队列的特征,主要是以下方法:
可以看出,大部分方法我们在上面的普通队列已经实现的,剩余的主要是跟栈的特性有关的方法了。
我们来实现这些剩余的方法吧。
实现 addFont
方法
这个方法稍微有点复杂。我们有三种情况需要考虑:
addBack
无疑。this.lowestCount
大于0,直接添加在第一项即可this.lowestCount
为0,则需要把队列整体往后移addFont (element) {
if (this.isEmpty()) {
ths.addBack(element);
} else if (this.lowestCount > 0) {
this.items[--lowestCount] = element
} else {
for (let i = count; i > 0; i--) {
this.items[i] = this.items[i - 1];
}
this.count++;
this.lowestCount = 0;
this.items[0] = element;
}
}
实现 removeBack
方法
这个跟栈的移除是一样的,实现很简单。
removeBack() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[--this.count];
delete this.items[this.count];
return result;
}
实现 peekBack
方法
peekBack() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.count - 1];
}
整体代码如下:
class Deque {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
addFont(element) {
// the queue is empty
if(this.isEmpty()) {
this.addBack(element)
} else if (this.lowestCount > 0) {
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(element) {
this.items[this.count++] = element;
}
removeFont() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
removeBack() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[--this.count];
delete this.items[this.count];
return result;
}
peekFont() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.lowestCount];
}
peekBack() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.count - 1];
}
isEmpty() {
return this.count - this.lowestCount === 0;
}
size() {
return this.count - this.lowestCount;
}
clear() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
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;
}
}
这个游戏相信大家都玩过,有可能不是击鼓,或许是挨个挨个传递某样东西(我们以前可能是随便乱扔,为了实现队列,就严谨一点吧),还有一个类似的游戏叫抢椅子,这个大家肯定玩过,就不多介绍了。在一定的时间后,东西在谁手里谁就会被淘汰,然后进入下一轮。直到只剩下一个人,那个人就是赢家。
我们可以用一个队列来把参与者放进去,然后循环队列,将第一个取出放在最后一个,时间一到,谁在第一个就被淘汰掉,然后接着游戏直到队列只有一个人为止。这个算法实现其实还是挺简单的,我们创建一个hotPotato
函数,传递一个队列和一个模拟时间的数字。
function hotPotato(players, time) {
// 1. 创建一个Queue实例
const queue = new Queue()
// 2. 淘汰者
const eliminatedPlayers = []
// 3. 将玩家加入队列
players.forEach(player => {
queue.enqueue(player)
})
// 4. 循环队列,直到只剩下一个人
while (queue.size() > 1) {
// 规定时间到,淘汰一个人
for (let i = 0; i < time; i++) {
queue.enqueue(queue.dequeue())
}
eliminatedPlayers.push(queue.dequeue())
}
// 5. 得到赢家
let winner = queue.dequeue()
// 6. 返回结果
return {
winner,
eliminatedPlayers
}
}
我们测试一下:
const players = ['老曹', '老刘', '老王', '老蔡', '老孔'];
const [, , time] = process.argv; // 获取用户输入
const { eliminatedPlayers, winner } = hotPotato(players, ~~time);
eliminatedPlayers.forEach(loser => {
console.log(`${loser} has been eliminated in the hotPotato game`);
})
console.log(`the winner is ${winner}`);
在控制台输入并得到结果:
还是挺有意思的吧!下面我们看另一个问题:
回文是正反都能读通的字母、数字组成的字符串序列,如racecar、madam。
实现回文检查器有很多方法,但是这里我们借助了双向队列的思想来实现它,而且会十分简单,只需要将字符串放入一个双向队列中,并且不停取出第一个和最后一个元素比较,直到队列只剩下一个元素为止,如果期间有不同的就直接返回false
,否则为true
。我们用一个polindromeChecker
函数来实现吧。
下面是思路:
Deque
对象function polindromeChecker(str) {
if (typeof str === undefined || typeof str === null || (typeof str !== null && str.length === 0) {
return false;
}
const deque = new Deque();
const newString = str.toLocaleLowerCase().match(/[0-9a-z]+/g).join('');
let isEqual = true;
let firstName, lastName;
for (let i = 0; i < newString.length; i++) {
deque.addBack(newString.charAt(i))
}
while (deque.size() > 1) {
firstName = deque.removeFont();
lastName = deque.removeBack();
if (firstName !== lastName) {
isEqual = false;
}
}
return isEqual;
}
加入测试用例:
console.log('a', panlindRomeChecker('a'));
console.log('aa', panlindRomeChecker('aa'));
console.log('kayak', panlindRomeChecker('kayak'));
console.log('Was it a car or a cat I saw ?', panlindRomeChecker('Was it a car or a cat I saw ?'));
全部都通过了,虽然在检查字符串那一块还有瑕疵,不过还是正常实现我们需要的回文检查器。
怎么样?是不是感觉使用队列解决问题是不是很简单。
在JS中,Event Loop 模型用到了事件队列,里面存放了各种事件的处理函数,等调用栈为空后然后执行队列的第一个函数。详细请看这篇文章,我个人觉得自从看了这篇 MDN的并发模型与事件循环文章后,你会对JS执行机制会有更多的领悟。之后我也会单独去写一篇文章来讲述这个概念。
这篇文章算是大概写完了,看过《学习JavaScript数据结构与算法》(第3版)这本书的朋友应该会对这篇文章很熟悉,我是按照书上的结构写的文章,不过我是在看完书后合上书再来写的文章,代码也是一个一个打的,算是对知识的一个吸收吧,我的数据结构与算法上学时没怎么听过课,现在回头“还债”中。最后再提一下,一定要去看MDN的并发模型与事件循环文章,太棒了。
【1】《学习JavaScript数据结构与算法》(第3版)
【2】并发模型与事件循环