队列的结构

队列是遵循先进先出(FIFO,也称为先来先服务)原则的一组有序的项,与栈正好相反。典型的就是排队买票,先来的先买,后来的人只能站末尾,不允许插队。
队列两个基本操作:入队,放一个数据到队列尾部出队,从队列头部取一个元素。所以队列也是一种操作受限的线性表数据结构
image.png

JS实现队列

队列跟栈一样,也是一种抽象的数据结构。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列
Queue类和Stack类主要的区别在于dequeue方法和peek方法,这是由于先进先出和后进先出原则的不同所造成的。

数组实现队列

数组的实现方式是最简单的,通过shift和unshift方法,就能用数组模拟基本的队列数据结构。

  1. /*
  2. * @Author: xjp
  3. * @Date: 2022-06-24 11:20:07
  4. * @LastEditTime: 2022-06-24 11:21:14
  5. * @Description:
  6. */
  7. class Queue {
  8. constructor() {
  9. this.items = [] // 队列数组
  10. }
  11. // 入队
  12. enqueue(item) {
  13. this.items.unshift(item)
  14. return true
  15. }
  16. // 出队
  17. dequeue() {
  18. return this.items.splice(0, 1)
  19. }
  20. // 查看队列长度
  21. size() {
  22. return this.items.length
  23. }
  24. // 查看队列最后一个元素
  25. getTailItem() {
  26. const len = this.items.length
  27. if (len <= 0) {
  28. return null
  29. }
  30. return this.items[len - 1]
  31. }
  32. // 清空队列
  33. clearQueue() {
  34. this.items = []
  35. return true
  36. }
  37. }
  38. // 测试代码
  39. const q = new Queue()
  40. q.enqueue(1)
  41. q.enqueue(2)
  42. q.enqueue(3)
  43. console.log(q) // Queue { items: [ 3, 2, 1 ] }
  44. console.log(q.dequeue()) // -> 1 -> [3, 2]

对象实现队列

使用对象实现队列,获取元素更加高效!

/*
 * @Author: xjp
 * @Date: 2022-06-24 11:06:25
 * @LastEditTime: 2022-06-24 11:15:31
 * @Description: 
 */
class Queue {
  constructor() {
    this.head = 0 // 队头的key
    this.tail = 0 // 队尾的key
    this.items = {}
  }

  // 入队,向队尾添加新项,队尾的key++
  enqueue(item) {
    this.items[this.tail] = item // 放在最后
    this.tail++
  }

  // 出队,移除队头的元素,队头的key++
  dequeue() {
    if (this.isEmpty()) {
      return undefined
    }
    const result = this.items[this.head]
    delete this.items[this.head]
    this.head++
    return result
  }

  // 查看队列的第一个元素
  peek() {
    if (this.isEmpty()) {
      return undefined
    }
    return this.items[this.head]
  }

  // 查看队列是否为空
  isEmpty() {
    return this.head === this.tail
  }

  // 查看队列包含多少个元素
  size() {
    return this.tail - this.head
  }

  // 清空队列
  clear() {
    this.items = {}
    this.head = 0
    this.tail = 0
  }

  // toString方法,方便打印
  toString() {
    if (this.isEmpty()) {
      return ""
    }
    let objString = `${this.items[this.head]}`
    for (let i = this.head + 1; i < this.tail; i++) {
      objString = `${objString},${this.items[i]}`
    }
    return objString
  }
}

// 测试代码
const q = new Queue()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
console.log(q) // Queue { head: 0, tail: 3, items: { '0': 1, '1': 2, '2': 3 } }
console.log(q.toString()) // '1,2,3'
console.log(q.peek()) // 第一个元素:'1'
console.log(q.size()) // 队列个数:3
console.log(q.dequeue()) // 出队:'1'
console.log(q.size()) // 队列个数:2
console.log(q.peek()) // 第一个元素:'2'

链表实现队列

image.png

/**
 * 链表结点的类
 */
class Node {
  constructor(item) {
    this.item = item  // 添加的元素
    this.next = null // 指针
  }
}

class Queue {
  constructor() {
    this.length = 0 // 队列 size
    this.head = null // 队头的结点
    this.tail = null // 队尾的结点
  }

  // 入队(先将尾结点的next指向新添的节点,然后再用尾结点存储新添的结点)
  push(item) {
    const node = new Node(item)
    if (this.length == 0) { // 如果没有结点
      this.head = this.tail = node
    } else { // 否则,尾结点存储新添的结点
      this.tail.next = node; // ⭐顺序不能颠倒!!先将当前尾结点的next指向新结点
      this.tail = node // 再将新节点赋给尾结点
    }
    // 更新队列大小
    this.length++
    return true
  }

  // 出队(头结点取出来,再将头结点指向头结点的下一个结点)
  pop() {
    if (this.length == 0) {
      this.head = null // 队头结点
      this.tail = null // 队尾结点
      return this.head
    }
    const item = this.head.item
    // 更新队列头部结点
    this.head = this.head.next
    // 更新队列大小
    this.length--
    // 返回队列头部结点的值
    return item
  }

  // 获取长度(返回 length 即可)
  size() {
    return this.length
  }

  // 获取最后一个结点
  getTail() {
    if (this.length == 0) {
      return null
    }
    return this.tail
  }

  clear() {
    this.length = 0 // 队列 size
    this.head = null // 队头
    this.tail = null // 队尾
  }
}

const q = new Queue()
q.push(1)
console.log(q) 
// Queue {
//   length: 1,
//   head: Node { item: 1, next: null },
//   tail: Node { item: 1, next: null }
// }
q.push(2)
q.push(3)
q.push(4)
console.log(q) 
// Queue {
//   length: 4,
//   head: Node { item: 1, next: Node { item: 2, next: [Node] } },
//   tail: Node { item: 4, next: null }
// }
console.log(q.pop()) // 1
console.log(q)
// Queue {
//   length: 3,
//   head: Node { item: 2, next: Node { item: 3, next: [Node] } },
//   tail: Node { item: 4, next: null }
// }

双端队列

双端队列(deque,或称double-ended queue)是一种允许同时从前端和后端添加和移除元素的特殊队列。
双端队列在现实生活中的例子有电影院、餐厅中排队的队伍等。举个例子,一个刚买了票的人如果只是还需要再问一些简单的信息,就可以直接回到队伍的头部。另外,在队伍末尾的人如果赶时间,他可以直接离开队伍。
在计算机科学中,双端队列的一个常见应用是存储一系列的撤销操作。每当用户在软件中进行了一个操作,该操作会被存在一个双端队列中(就像在一个栈里)。当用户点击撤销按钮时,该操作会被从双端队列中弹出,表示它被从后面移除了。在进行了预先定义的一定数量的操作后,最先进行的操作会被从双端队列的前端移除。由于双端队列同时遵守了先进先出和后进先出原则,可以说它是把队列和栈相结合的一种数据结构

class Deque {
  constructor() {
    this.head = 0
    this.tail = 0
    this.items = {}
  }

  // 在双端队列前端添加新的元素,相当于插队:有三种场景
  addFront(element) {
    if (this.isEmpty()) { // 一、如果双端队列为空,则元素默认会被添加到双端队列的后端
      this.addBack(element)
    } else if (this.head > 0) { // 二、head > 0说明已经有元素从双端队列的前端被移除
      this.head--
      this.items[this.head] = element
    } else { // 三、head === 0,设置一个负值的键,同时更新用于计算双端队列长度的逻辑,使其也能包含负键值。
      for (let i = this.tail; i > 0; i--) { // 要在前端第一位添加一个新元素,就需要将所有元素后移一位来空出第一个位置
        this.items[i] = this.items[i - 1]
      }
      this.tail++
      this.items[0] = element // 将新元素添加到第一个位置
    }
  }

  // 在双端队列后端添加新的元素
  addBack(element) {
    this.items[this.tail] = element
    this.tail++
  }

  // 从双端队列前端移除第一个元素
  removeFront() {
    if (this.isEmpty()) {
      return undefined
    }
    const result = this.items[this.head]
    delete this.items[this.head]
    this.head++
    return result
  }

  // 从双端队列后端移除第一个元素
  removeBack() {
    if (this.isEmpty()) {
      return undefined
    }
    this.tail--
    const result = this.items[this.tail]
    delete this.items[this.tail]
    return result
  }

  // 返回双端队列前端的第一个元素
  peekFront() {
    if (this.isEmpty()) {
      return undefined
    }
    return this.items[this.head]
  }

  // 返回双端队列后端的第一个元素
  peekBack() {
    if (this.isEmpty()) {
      return undefined
    }
    return this.items[this.tail - 1]
  }

  isEmpty() {
    return this.size() === 0
  }

  clear() {
    this.items = {}
    this.tail = 0
    this.head = 0
  }

  size() {
    return this.tail - this.head
  }

  toString() {
    if (this.isEmpty()) {
      return ''
    }
    let objString = `${this.items[this.head]}`
    for (let i = this.head + 1; i < this.tail; i++) {
      objString = `${objString},${this.items[i]}`
    }
    return objString
  }
}

// 测试
const deque = new Deque()
console.log(deque.isEmpty()) // true
deque.addBack('A')
deque.addBack('B')
console.log(deque.toString()) // A,B
deque.addBack('C')
console.log(deque.toString()) // A,B,C
deque.addFront('D') 
console.log(deque.toString()) // D,A,B,C
deque.removeBack() 
console.log(deque.toString()) // D,A,B
deque.removeFront() 
console.log(deque.toString()) // A,B

数组实现循环队列

数组实现队列时候,当数据过多超出数组长度时会发生数据搬移操作,这对入队操作性能有所影响。循环队列就可以避免数据搬移:
image.pngimage.png
关键思路:循环队列满队**max**后不能继续添加,必须要pop出队才可以继续添加,出队时将队首head指向下一个,入队时将队尾tail指向下一个,保证循环的关键是每次出入队更新头尾用这个:**(this.tail/head + 1) % this.max**

class CircularQueue {
  constructor(maxSize) { // k 为队列数组长度
    this.items = Array(maxSize) // 存储队列
    this.head = 0 // 队头索引
    this.tail = 0 // 队尾索引
    this.max = maxSize
  }
  // 入队
  enqueue(value) {
    if (this.isFull()) {
      return false
    } else {
      this.items[this.tail] = value // 往队尾加入元素
      this.tail = (this.tail + 1) % this.max // ⭐更新队尾索引
      return true
    }
  }
  // 出队
  dequeue() {
    const item = this.items[this.head]
    this.items[this.head] = ''
    this.head = (this.head + 1) % this.max // ⭐更新队头索引
    return item
  }

  getHead() {
    return this.items[this.head] ? this.items[this.head] : -1
  }

  getTail() {
    if (this.isEmpty()) {
      return -1
    } else {
      const tailIndex = this.tail - 1 // 获取 index
      const cur = tailIndex < 0 ? this.max - 1 : tailIndex // 判断当前的 index
      return this.items[cur]
    }
  }
  // 队空
  isEmpty() {
    return this.head === this.tail && !this.items[this.head]
  }
  // 队满
  isFull() {
    return this.head === this.tail && !!this.items[this.head]
  }
}
const circularQueue = new CircularQueue(3) // 最多存三个元素
circularQueue.enqueue(1)
console.log(circularQueue) // CircularQueue { items: [ 1, <2 empty items> ], head: 0, tail: 1, max: 3 }
circularQueue.enqueue(2)
circularQueue.enqueue(3)
console.log(circularQueue.isFull()) // true
console.log(circularQueue) // CircularQueue { items: [ 1, 2, 3 ], head: 0, tail: 0, max: 3 }
console.log(circularQueue.enqueue(4)) // false,满了,不能继续添加
console.log(circularQueue) // CircularQueue { items: [ 1, 2, 3 ], head: 0, tail: 0, max: 3 }
console.log(circularQueue.getHead()) // 队首:1
console.log(circularQueue.getTail()) // 队尾:3
console.log(circularQueue.dequeue()) // 出队:1
console.log(circularQueue) // CircularQueue { items: [ '', 2, 3 ], head: 1, tail: 0, max: 3 }
circularQueue.enqueue(4)
console.log(circularQueue) // CircularQueue { items: [ 4, 2, 3 ], head: 1, tail: 1, max: 3 }
console.log(circularQueue.getHead()) // ⭐队首:2
console.log(circularQueue.getTail()) // ⭐队尾:4
console.log(circularQueue.dequeue()) // 出队:2
circularQueue.enqueue(5)
console.log(circularQueue) // CircularQueue { items: [ 4, 5, 3 ], head: 2, tail: 2, max: 3 }
console.log(circularQueue.getHead()) // ⭐队首:3
console.log(circularQueue.getTail()) // ⭐队尾:5

队列的应用

循环队列—击鼓传花

在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈、结束游戏。重复这个过程,直到只剩一个孩子(胜者)。

function hotPotato(elementsList, num) { // num表示击鼓传花游戏的每一轮的第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++) { // 每轮击鼓传花7次,每次把队列的第一个元素放到队尾,循环往复
      queue.enqueue(queue.dequeue())
    }
    elimitatedList.push(queue.dequeue()) // 一轮结束后,把队列的第一个元素放到淘汰列表
  }
  return {
    eliminated: elimitatedList,
    winner: queue.dequeue()
  }
}
const names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl']
const result = hotPotato(names, 7)
result.eliminated.forEach(name => {
  console.log(`${name}在击鼓传花游戏中被淘汰。`)
})
console.log(`胜利者: ${result.winner}`)

双端队列— 回文检查器

有不同的算法可以检查一个词组或字符串是否为回文:

  1. 最简单的方式是将字符串反向排列并检查它和原字符串是否相同。如果两者相同,那么它就是一个回文。
  2. 栈来解决
  3. 双端队列
    import Deque from "./deque.js"
    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))
    }
    while (deque.size() > 1 && isEqual) {
     // 将从前端移除一个元素,再从后端移除一个元素。如果是回文,移除的两个字符必须相同
     firstChar = deque.removeFront()
     lastChar = deque.removeBack()
     if (firstChar !== lastChar) {
       isEqual = false
     }
    }
    return isEqual
    }
    console.log(palindromeChecker("Step on no pets")) // true
    

    阻塞队列

    阻塞队列其实就是在队列基础上增加了阻塞操作。简单说就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
    image.png
    当“生产者”生产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满了。这个时候,生产者就阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。不仅如此,基于阻塞队列,还可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。
    image.png

    并发队列

    在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,线程安全的队列叫作并发队列。最简单直接的实现方式是直接在 push()、pop() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。
    基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。

    线程池请求排队

    当向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?一般有两种处理策略。第一种是非阻塞的处理方式,直接拒绝任务请求;另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理,这个时候就需要队列来实现先进先出。
    基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。
    而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说更加合理。不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。
    队列除了应用在线程池请求排队的场景,还可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。