Skip to content

循环队列

循环队列是一种利用 固定大小 的数组实现队列操作的数据结构,但它采用了**“环形”思想**,使得数组的末尾与开头相连,从而充分利用了所有存储空间。

普通队列与循环队列的差异

  1. 普通队列:使用数组来实现,随着元素不断出队,数组前面部分就空闲了(特别是像 Java、C++ 这一类有标准数组数据结构的语言)。当然,可以将后面的元素往前面移动,但是这里又涉及到了移动相关的操作。
  2. 循环队列:利用环形结构,队尾到达数组末尾之后,新的入队操作会自动写入到数组起始的位置。

循环队列特点

  1. 固定容量与环形结构

    • 固定容量
    • 环形思想:实际上都是通过模运算来计算新入队的元素的下标位置
  2. 指针管理:通常使用两个变量:

    • 队头指针(front):指向队列的第一个元素

    • 元素计数(rear/count):记录当前队列有多少个元素,方便后面做模运算

    • 模运算,使得指针在数组边界“循环”,避免了普通队列中出队后前部空间无法再利用的问题。

  3. 高效操作

具体示例

假设有一个大小为 5 的队列,我们依次对队列执行以下操作:

普通队列#

基于固定数组,未使用循环策略

  1. 入队操作:将元素 A、B、C 依次入队

    [A, B, C, _, _]
  2. 出队操作:出队两次,把 A 和 B 出队

    [_, _, C, _, _]

    现在随着 A 和 B 的出队,前面两个位置就空出来了

  3. 再次入队操作:入队 D 和 E 元素

    [_, _, C, D, E]

    现在这个结构就没有办法再入队新元素,已经满了。除非将后面的元素全部往前面移动:

    [C, D, E, _, _]

循环队列#

同样大小为 5 的循环队列,进行相同的操作:

  1. 入队操作:将元素 A、B、C 依次入队

    [A, B, C, _, _]
    • frontIndex:0
    • count:3
    • maxSize:5
  2. 出队操作:出队两次,把 A 和 B 出队

    [_, _, C, _, _]

    这里就会更新队首指针:新队首指针 = (旧队首指针 + 1) % maxSize

    • A元素出队的时候:新队首指针 = (0 + 1) % 5 = 1
    • B元素出队的时候:新队首指针 = (1 + 1) % 5 = 2
  3. 再次入队操作:之后新元素入队,放入位置的计算公式 (队首指针 + count) % maxSize

    • 新元素 D 入队:(2 + 1) % 5 = 3

      [_, _, C, D, _]
      • frontIndex:0
      • count : 2
    • 新元素 E 入队:(2 + 2) % 5 = 4

      [_, _, C, D, E]
      • count:3
    • 新元素 F 入队:(2 + 3) % 5 = 0

      [F, _, C, D, E]
      • count: 4
    • 新元素G 入队:(2 + 4) % 5 = 1

      [F, G, C, D, E]
      • count: 5

通过循环队列这种计算方式,就不需要将后面的元素全部往前面移动,可以通过模运算计算出每次新元素应该入队的正确位置。

代码实现

class CircularQueue {
constructor(maxSize = 10) {
this.maxSize = maxSize
this.queue = new Array(maxSize)
// 队首指针
this.frontIndex = 0
// 队列元素计数
this.count = 0
}
// 一些辅助方法:size、isEmpty、isFull
size() {
return this.count
}
isEmpty() {
return this.count === 0
}
isFull() {
return this.count === this.maxSize
}
// 新元素入队
enqueue(item) {
if (this.isFull()) {
// 进入此分支,说明队列已满
throw new Error('队列已满')
}
// 计算新元素应该存储的索引位置
let index = (this.frontIndex + this.count) % this.maxSize
this.queue[index] = item
this.count++
}
// 元素出队
dequeue() {
if (this.isEmpty()) {
return undefined
}
// 取出队首的元素
let item = this.queue[this.frontIndex]
this.queue[this.frontIndex] = undefined
// 更新存储队首位置的变量
this.frontIndex = (this.frontIndex + 1) % this.maxSize
this.count--
return item
}
// 获取队首的元素,但是不移除
front() {
return this.isEmpty() ? undefined : this.queue[this.frontIndex]
}
// 获取队尾的元素,但是不移除
// 队尾元素的索引计算公式:(frontIndex + count - 1) % maxSize
back() {
if (this.isEmpty()) return undefined
let index = (this.frontIndex + this.count - 1) % this.maxSize
return this.queue[index]
}
clear() {
// 清空队列,全部还原
this.queue = new Array(this.maxSize)
this.frontIndex = 0
this.count = 0
}
print() {
// 显示当前队列信息
// 1. 当前数组里面的元素 2. 元素的正确顺序
console.log(this.queue)
// 还需要显示元素正确的顺序
for (let i = 0; i < this.count; i++) {
// 计算正确的下标,然后打印出来
let index = (this.frontIndex + i) % this.maxSize
console.log(this.queue[index] + ' ')
}
}
}