1. 为什么顺序队列明明"能存数据",却总被人嫌弃浪费空间
队列这东西,大家都不陌生。生活里买奶茶排队、食堂打饭、银行叫号,全是队列的活生生例子。在计算机里,队列就是**先进先出(FIFO, First In First Out)**的线性表:一端负责进(队尾rear),一端负责出(队头front),数据排着队走,谁先来谁先走,规则简单粗暴。
但问题是,队列用顺序存储实现的时候,有个让人非常难受的毛病——假溢出。
什么叫假溢出?我先给你还原一个场景。
假设你开了一个长度为5的数组当队列,初始时front和rear都指向下标0。你现在依次入队A、B、C、D、E,rear一路加到了5,此时rear == MaxSize,数组满了,不能再入队了,这没问题。
但你紧接着出队A、B、C、D,front从0走到了4。这时候数组里明明只剩E一个元素,可rear已经在数组的最后一个位置上了。如果你再想入队F,程序一看 rear == MaxSize,直接报"队列已满"——可你数组下标0到3的位置全是空的啊!
这就是假溢出:数组有空间,但rear指针已经跑到末尾,逻辑上判满了。解决办法有两种:
- 数据搬移:每次入队前检查,如果rear到顶但front前面还有空位,把整个队列整体往前搬。代价是O(n)的时间开销,频繁搬移性能差。
- 循环队列:把数组在逻辑上首尾相接,让rear到顶后自动"绕"回下标0,继续利用前面空出来的位置。
循环队列,就是从方案2里长出来的东西,也是这篇要讲的重点。
它本质上就是一个头尾相接的顺序存储结构,靠取模运算让指针"转圈圈"。你可能觉得这有什么好讲的?不就是 (rear + 1) % MaxSize 嘛。但真正动手写的时候,坑还挺多的——判空判满的两种方案怎么选、队列长度公式怎么推、最后一个存储单元到底能不能用、遍历的时候边界在哪……这些细节没搞清楚,笔试面试一写代码就露馅。
这篇围绕循环队列,把顺序存储这条线彻底讲透。从底层设计到完整可运行的C语言实现,再到实际工程里的应用场景(线程池的任务队列、BFS的辅助队列,全都跟它脱不开关系),一步步拆给你看。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 循环队列的核心设计:一个数组,两个指针,转圈圈
2.1 怎么让数组"首尾相接"
数组本身是线性的,物理内存上不可能真的弯成一个环。所以循环队列的做法是:在逻辑上把array[0]和array[MaxSize-1]看成相邻的位置,指针在向后移动时,如果已经到了数组末尾,就通过取模运算跳回数组开头。
入队时rear指针的移动,从普通的 rear++ 变成:
c复制rear = (rear + 1) % MaxSize;
出队时front指针同理:
c复制front = (front + 1) % MaxSize;
用生活化的类比,这就好比一个环形跑道,跑完一圈自动回到起点,不用掉头。取模运算在这里干的事,就是"到边界自动回绕",保证任何时刻front和rear都在 [0, MaxSize-1] 这个合法区间内。
2.2 判空和判满:循环队列最容易翻车的点
队列初始时,front = rear = 0,此时队列为空,这是共识。
但问题来了:队列满了之后,front 和 rear 是什么关系?
你往一个长度为5的数组里连续入队5个元素,rear先加1,再取模。当最后一个元素入队后,rear会从4变成0,而这个0恰好等于front。所以队列满时,front == rear,和队列空时一模一样。
这就尴尬了:同一个条件,既判空又判满,程序无法区分。
解决这个问题,工程上有三种主流方案,我一个个说清楚,因为笔试题里三种都有可能考到。
方案一:牺牲一个存储单元
这是最经典、教材最常用、面试最常考的一种。主动浪费数组的一个元素空间,让"rear的下一个位置是front"时判定为满。具体条件:
c复制// 判断队满
(rear + 1) % MaxSize == front
// 判断队空
front == rear
说得直白一点:数组长度为MaxSize,但队列最多只能存 MaxSize - 1 个元素。空一个位子出来,专门用来打破"空满同条件"的僵局。这个空位不存数据,它存在的意义就是让rear永远追不上front——最多只能"挨着"它。
我这个队列可以存多少元素?数组开了MaxSize个格子,实际能用的是MaxSize-1个。这个"浪费一格"的设计,从一开始就要想清楚,不然后面写代码容易绕晕。
方案二:增设size计数器
结构体里多维护一个 int size,入队时size++,出队时size--。判空判满直接看size:
c复制// 判断队空
size == 0
// 判断队满
size == MaxSize
这个方案的好处是不浪费存储空间,数组里每一个格子都能用来存数据。代价是结构体多一个字段,每次入队出队要多维护一次size。逻辑上更简单直观,很多实际项目里喜欢用这种。
方案三:增设tag标志位
结构体里加一个 int tag,入队操作后tag置1,出队操作后tag置0。因为front和rear相等时,只有两种情况:要么是入队后追平(队满),要么是出队后追平(队空)。用tag记录"最后一次操作是入队还是出队",就能区分了:
c复制// 判断队满
front == rear && tag == 1
// 判断队空
front == rear && tag == 0
这个方案同样不浪费存储空间,但多一个字段,每次操作都要维护tag值。笔试里出现的频率比方案二低一些,但作为扩展知识了解不亏。
下面用一张表把这三种方案放在一起对比,方便你记忆:
| 方案 | 判空条
