什么是循环队列?如何解决「假溢出」并判断队空队满?
简化版
普通数组队列出队后,队头前面的空间没法再用,明明还有空位却提示满了,这叫假溢出。循环队列让队头队尾指针取模回绕((index + 1) % capacity),把数组当成首尾相接的环,空出来的位置能循环利用。判空判满常用留一个空位:rear == front 为空,(rear + 1) % capacity == front 为满。
详细版
用数组 + 两个指针 front(队头)、rear(队尾),入队写 rear 位置后 rear = (rear+1)%cap,出队读 front 位置后 front = (front+1)%cap。
问题:rear == front 既可能是「空」也可能是「满」,无法区分。三种常见解法:
- 牺牲一个空位(最常用):故意不让队列填满,留一格空。
- 队空:
front == rear - 队满:
(rear + 1) % cap == front - 实际能存
cap − 1个元素。
- 队空:
- 额外记一个
size计数:size == 0为空、size == cap为满,能存满cap个,但多维护一个变量。 - 加一个布尔标志 / 用
count:思路同上。
class MyCircularQueue {
int[] a; int front, rear, cap;
MyCircularQueue(int k) { cap = k + 1; a = new int[cap]; } // 多留一格
boolean enQueue(int v) {
if (isFull()) return false;
a[rear] = v; rear = (rear + 1) % cap; return true;
}
boolean deQueue() {
if (isEmpty()) return false;
front = (front + 1) % cap; return true;
}
boolean isEmpty() { return front == rear; }
boolean isFull() { return (rear + 1) % cap == front; }
}
完整版教学
一、假溢出是怎么来的
用固定数组做队列,队头出队时如果只是 front++,那么数组左边被出队腾出的格子再也用不到了。反复入队出队后,rear 到了数组末尾、front 前面却空着一堆——这时入队会判「越界/满」,但实际还有空间。这种「有空位却报满」就是假溢出,本质是「线性数组用一次就作废前面的空间」。
二、循环队列如何消除假溢出
把数组想象成一个环:指针走到末尾就用取模 % cap 回到开头。这样队头出队腾出的前面格子,队尾入队时能绕回来复用,空间循环利用,不再有假溢出。核心动作就是指针 +1 后对容量取模。
三、判空判满为什么是难点
回绕之后,front == rear 出现在两种时刻:一开始(空),或者刚好绕一圈填满(满)。同一个条件对应两种状态,必须想办法区分:
- 留一个空位:让「满」时
rear停在front前一格,于是「满」是(rear+1)%cap == front,「空」才是front == rear。代价是浪费一格(能存cap−1个)。 - 额外计数
size:直接判size==0/size==cap,不浪费空间但多一个变量要同步维护。
两种都对,面试说清楚你选哪种、以及为什么就行。
四、为什么值得用循环队列
- 定长、高效:数组连续内存、缓存友好,入队出队都是 O(1)。
- 常见于底层:环形缓冲区(ring buffer)广泛用于 IO 缓冲、日志、生产者-消费者、网络收发缓冲、音视频流——都是「固定容量、循环复用」的场景。
- 相比链表队列,省去了频繁分配/回收节点的开销。
五、对比链表实现的队列
链表队列天然不需要处理假溢出(动态分配),但每个节点有指针开销、缓存不友好、频繁 new 节点。循环队列用固定数组,容量上限确定的场景下更快更省。两者按「容量是否固定、是否在意内存/缓存」来选。
六、常见误区与追问
| 设计方式 | 判空条件 | 判满条件 | 特点 |
|---|---|---|---|
| 浪费一个槽位 | front == rear | (rear + 1) % capacity == front | 逻辑简单,实际容量少 1 |
维护 size | size == 0 | size == capacity | 容量可用满,多维护一个变量 |
| 维护标志位 | 看标志 | 看标志 | 容易写错,面试较少推荐 |
易错点:循环队列的数组下标会回绕,但元素顺序没有乱;真正的队头和队尾由
front/rear语义决定,不由物理下标大小决定。
例如数组容量为 5,采用浪费一个槽位的设计,最多只能放 4 个元素。若 front=3,rear=2,队列并不是空,而是已经发生回绕;此时队列里的逻辑区间可能是 3,4,0,1。判断满要看 (rear+1)%5 == front,而不是看 rear > front。
- 误区:rear 小于 front 就说明队列错了。 循环队列允许下标回绕,
rear < front只是说明尾部已经绕到数组前面。 - 误区:数组队列只要 rear 到末尾就满了。 如果前面有出队留下的空位,普通线性队列会假溢出,循环队列可以复用这些位置。
- 误区:判空和判满都用
front == rear。 这样无法区分空和满,所以要浪费一个槽位、维护 size 或维护额外标志。 - 追问:为什么循环队列适合固定容量缓冲区? 它用固定数组复用空间,不需要频繁移动元素或分配节点。
- 追问:入队和出队为什么是 O(1)? 只改一个位置和一个指针,取模回绕不依赖元素数量。
- 追问:链表队列和循环队列怎么选? 容量固定且追求缓存友好时用循环数组;容量不确定且不想预分配时可用链表。
七、加强记忆
假溢出 = 线性数组队列出队后前面空间作废;循环队列用指针 +1 取模回绕复用空间。判空判满要打破 front==rear 的二义性:常用留一格空位(空 front==rear、满 (rear+1)%cap==front)或额外记 size。适合环形缓冲等定长场景。