队列-经典应用案例
这里来简单举几个经典的场景如 "击鼓传花", "字符串回文监测" 等来加深对队列这个结构的直观认识.
单端队列-击鼓传花
这里先介绍一种队列的变种叫 循环队列
, 即元素从队首出队后, 立即又进行从队尾入队, 类似行程了一个 圈
, 与之对应的一个经典游戏就是 "击鼓传花", 英文叫 hot potato
烫手的山芋.
在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈、结束游戏。重复这个过程,直到只剩一个孩子(胜者).
更为直接的是我司每年都会进行年会抽奖, 用的就是击鼓传花. 将所有人拉进来, 循环滚动, 中间叫停抽奖, 抽到奖的就移除用户池, 没抽中的继续.
这个 "击鼓传花" 的过程我们可以用队列进行模拟, 用队首表示 potato.
// 用数组模拟队列, 左是队首, 右是队尾
var queue = []
// 假设有 a, b, c, d, e 五个玩家, 先全部从队尾入队
[ a, b, c, d, e]
// 先滚动起来不淘汰哈
// 假设随机数字是 3
[d, e, a, b, c]
// 再假设数字是 4
[b, c, d, e, a]
// 可以看到 "循环" 的效果,
// 现在开始进行淘汰制, 直到最后剩下1人才结束, 一共需要4轮
// 初始入队, 并假设每轮的数字是恒定的 3
[ a, b, c, d, e]
// 第一轮
[d, e, a, b, c] => d 淘汰 => [e, a, b, c]
// 第二轮
[c, e, a, b] => c 淘汰 => [e, a, b]
// 第三轮
[e, a, b] => e 淘汰 =>[a, b]
// 第四轮
[a, b] => b 淘汰 => [a] => over
最终经过 4轮角逐, a 笑到了最后哦!
发现如果 传递次数固定
, 则是可以通过计算出来谁是最终赢家的, 这里我们用代码来实现一下:
class Queue {
constructor() {
this.count = 0
this.frontIndex = 0
this.obj = {}
}
// 元素入队
enqueue(item) {
this.obj[this.count] = item
this.count += 1
}
// 队列长度
size() {
return this.count - this.frontIndex
}
// 队列是否为空
isEmpty() {
this.count - this.frontIndex == 0
}
// 元素出队
dequeue() {
if (this.isEmpty()) return undefined
let frontItem = this.obj[this.frontIndex]
delete this.obj[this.frontIndex]
this.frontIndex += 1 // 更新队首元素索引
return frontItem
}
// 清空队列
clear() {
this.obj = {}
this.frontIndex = 0
this.count = 0
}
// 查看队首元素
peek() {
if (this.isEmpty()) return undefined
return this.obj[this.frontIndex]
}
// 查看全队列
toString() {
if (this.isEmpty()) return undefined
let objString = `${this.obj[this.frontIndex]}`
for (let i = this.frontIndex + 1; i < this.count; i++) {
objString = `${objString}, ${this.obj[i]}`
}
return objString
}
}
// 击鼓传花, 传递次数固定
function hotPotato(elementsList, num) {
const queue = new Queue()
// 元素全部入队
for (const item of elementsList) {
queue.enqueue(item)
}
// 开始奏乐, 开始舞, 直到剩下最后一个人
const loserList = []
while (queue.size() > 1) {
for (let i = 0; i < num; i++) {
// 先出队, 再入队, 等循环次数到了之后, 再进行真正 "离队删除"
let item = queue.dequeue()
queue.enqueue(item)
}
// 一轮到数, 进行真正的删除
loserList.push(queue.dequeue())
}
// 最后剩下的就是赢家
return {
winner: queue.dequeue(),
loserList: loserList
}
}
const names = ['a', 'b', 'c', 'd', 'e'];
const results = hotPotato(names, 3)
results.loserList.forEach(name => console.log(`${name} 在击鼓传花中被淘汰!`))
console.log('')
console.log('最终的 winner 是: ', results.winner);
PS F:\algorithms> node queue_case.js
d 在击鼓传花中被淘汰!
c 在击鼓传花中被淘汰!
e 在击鼓传花中被淘汰!
b 在击鼓传花中被淘汰!
最终的 winner 是: a
双端队列 - 回文检查器
回文是正反序列都是一样的词, 比如:
- madam
- 上海自来水来自海上, 山西采煤炭煤采西山
应用方面搜了一下说, 字符串算法的一个广阔的应用领域就是基因序列,比如有了回文串的识别算法,我们就可以在存储基因序列的时候进行压缩,节省时间和空间。
但我感觉更多作为一种游戏玩玩, 或者强行给自己代码上强度吧. 判断字符是否回文, 我们可以用双端队列来实现.
原理就是:
- 元素先入双端队列
- 循环: 队首 和 队尾 同时移除元素, 判断如果值相等, 则继续, 不相等则说明非回文, 退出程序
- 是回文的话, 最后双端队列里只会剩 1 或者 0 个元素 (单双数)
// 字符回文监测
// 假设字符是 madam
// 以数组来模拟队列, 假设左边是队尾
var deque = []
// 从依次从队尾入队
[m, a, d, a, m]
// 分别从两端出队
// 第一轮比较:
m == m => 继续: [a, d, a]
// 第二轮比较:
a == a => 继续: [d]
剩一个元素, ok 的, 这个是个回文哦!
用代码来实现就相对简单了.
class Deque {
constructor() {
this.count = 0
this.frontIndex = 0
this.obj = {}
}
// 队列长度
size() {
return this.count - this.frontIndex
}
// 队列是否为空
isEmpty() {
return this.count - this.frontIndex == 0
}
// 清空队列
clear() {
this.obj = {}
this.frontIndex = 0
this.count = 0
}
// 查看全队列
toString() {
if (this.isEmpty()) return this.obj;
let objString = `${this.obj[this.frontIndex]}`
for (let i = this.frontIndex + 1; i < this.count; i++) {
objString = `${objString}, ${this.obj[i]}`
}
return objString
}
// 队尾添加元素
addBack(item) {
this.obj[this.count] = item
this.count++
}
// 队尾删除元素
removeBack() {
if (this.isEmpty()) return undefined
this.count -= 1
let backItem = this.obj[this.count]
delete this.obj[this.count]
return backItem
}
// 队首添加元素
addFront(item) {
// 当为空队列时, 则从队尾加
if (this.isEmpty()) {
this.addBack(item)
} else if (this.frontIndex > 0) {
// 当队首元素已有过出队时, 此时 frontIndex > 0, 此时进行 减1替换即可
this.frontIndex -= 1
this.obj[this.frontIndex] = item
} else {
// 当队首元素未有过出队时, 此时 frontIndex = 0, 后面元素依次后挪
for (let i = this.count; i > 0; i--) {
this.obj[i] = this.obj[i-1]
}
// 移完后将队首元素的值和索引都更新, 队列长度加1
this.obj[0] = item
this.frontIndex = 0
this.count += 1
}
}
// 队首删除元素
removeFront() {
if (this.isEmpty()) return undefined
let frontItem = this.obj[this.frontIndex]
delete this.obj[this.frontIndex]
this.frontIndex += 1
return frontItem
}
}
// test
function callbackCheck(string) {
if (typeof string !== 'string') return false
if (string.length <= 1) return false
const deque = new Deque()
// 先字符入队
for (const char of string) {
deque.addBack(char.toLowerCase())
}
while (deque.size() > 1) {
firstChar = deque.removeFront()
lastChar = deque.removeBack()
if (firstChar !== lastChar) return false
}
return true
}
// test
console.log(callbackCheck(''));
console.log(callbackCheck('a'));
console.log(callbackCheck('madam'));
console.log(callbackCheck('上海自来水来自海上'));
console.log(callbackCheck('hello'));
console.log(callbackCheck('{}'));
PS F:\algorithms> node deque_case.js
false
false
true
true
false
false
我自己用队列还是蛮多的, 主要是对于那种数据下载的任务, 把它整成一个异步任务, 然后用户的数据下载任务排队处理.
耐心和恒心, 总会获得回报的.