队列-经典应用案例

这里来简单举几个经典的场景如 "击鼓传花", "字符串回文监测" 等来加深对队列这个结构的直观认识.

单端队列-击鼓传花

这里先介绍一种队列的变种叫 循环队列, 即元素从队首出队后, 立即又进行从队尾入队, 类似行程了一个 , 与之对应的一个经典游戏就是 "击鼓传花", 英文叫 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

我自己用队列还是蛮多的, 主要是对于那种数据下载的任务, 把它整成一个异步任务, 然后用户的数据下载任务排队处理.

posted @ 2024-04-21 21:16  致于数据科学家的小陈  阅读(92)  评论(0编辑  收藏  举报