数据结构 | 循环队列

前言

队列是一种遵循先进先出(FIFO) 规则的线性结构,广泛应用于缓冲区管理、任务调度和消息通信等场景。传统实现方式中,链式队列 因节点内存分散导致缓存命中率低,且频繁的内存分配释放带来性能开销;顺序队列虽然缓存友好,但存在"假溢出"问题,空间利用率低。

循环队列 通过数组实现环形存储结构,完美解决了上述问题。它将数组首尾相连实现空间循环复用,保证入队、出队操作均为**O(1)**时间复杂度,兼具高性能和高空间利用率,成为工业级应用的首选方案。


一、循环队列的四大难点

循环队列的设计并非一蹴而就,而是在解决实际问题的过程中逐步迭代完善的,整个过程围绕效率、空间、状态判断、计数四大核心难点展开,每一个难点的解决,都让循环队列的设计更加完善。

难点 1:如何保证入队、出队操作时间复杂度为 O (1)

普通顺序队列的出队操作需要将所有元素向前挪动,时间复杂度为 O (n),在数据量较大时效率极差。想要实现高性能队列,必须让入队和出队操作不涉及任何元素移动。

解决方案 :放弃移动数据元素,仅通过队头指针(front)和队尾指针(rear) 的移动来标识队列的有效区间。入队操作仅向后移动队尾指针,出队操作仅向后移动队头指针,所有操作都仅需修改指针下标,时间复杂度稳定为O (1),彻底解决了普通顺序队列的效率瓶颈。

难点 2:如何解决队头指针后移带来的空间浪费问题

仅移动指针会导致数组头部的空间被闲置,随着不断的入队、出队操作,空闲空间会越来越多,最终引发假溢出。我们需要让这些空闲空间被重新利用,而不是永久浪费。

解决方案 :将数组逻辑上构造成环形结构 ,当队头 / 队尾指针移动到数组末尾时,通过取模运算自动回到数组起始位置。核心实现公式:指针 = (指针 + 1) % 队列最大容量,通过这种方式,数组空间可以被循环使用,从根本上杜绝了假溢出问题。

难点 3:如何解决判空与判满条件冲突的问题

当队列初始化、队列为空、队列满时,都会出现front == rear的情况,无法通过指针相等直接区分队列状态,这是循环队列设计中最经典的问题。

解决方案 :采用牺牲一个空闲格子的经典方案(最常用)。牺牲一个格子不存储数据,专门作为判满的标识:

  • 队列为空:front == rear
  • 队列为满:**(rear + 1) % 最大容量 == front**该方案无需额外变量,实现简洁、效率更高,完美区分了空队列和满队列的状态

难点 4:如何统一计算有效元素个数

循环队列的指针位置存在两种情况:rear 在 front 后方、rear 在 front 前方,两种情况的元素个数计算方式不同,需要一个通用公式适配所有场景。

解决方案 :推导通用计数公式:**有效元素个数 = (rear - front + 最大容量) % 最大容量**该公式兼容指针的所有位置关系,无论队列处于何种状态,都能精准计算出有效元素数量,是循环队列的核心公式之一。


二、循环队列结构体设计

循环队列的结构体设计是整个实现的核心,它围绕数据存储、指针标识、状态管理三大目标展开,结构简洁且功能完整,以下是详细设计思路:

  • 数据存储区 :使用动态数组ElemType* arr作为底层存储结构,相比静态数组,动态数组可以灵活分配内存空间,适配不同的容量需求,同时保持数组缓存友好的优势
  • 队头指针(front) :作为整型下标,指向队列中第一个有效元素,所有出队操作、获取队头元素操作都依赖该指针,是标识队列起始位置的核心变量
  • 队尾指针(rear) :作为整型下标,指向队列中下一个可插入数据的空闲位置,而非最后一个有效元素。这种设计让入队操作可以直接赋值,无需额外判断,简化了代码逻辑
  • 容量标识 :通过宏定义MAXSIZE指定队列最大容量,结合取模运算实现环形结构,同时配合牺牲一个格子的规则,实际可存储的有效元素数量为MAXSIZE - 1

三、核心代码精讲

我们基于 C++ 实现循环队列,封装为类结构,安全性更高、封装性更好。这里重点讲解入队、出队、获取队头、计算元素个数四大核心接口,先讲设计思路,再解析代码。

1. 核心结构定义

cpp 复制代码
#pragma once
#include <cassert>
#include <iostream>
using namespace std;

#define MAXSIZE 10    // 队列最大容量
typedef int ElemType; // 数据类型

class CircleQueue {
private:
    ElemType* _arr;   // 动态数组存储数据
    int _front;       // 队头:指向第一个有效元素
    int _rear;        // 队尾:指向下一个可插入位置
public:
    // 构造函数:初始化队列
    CircleQueue();
    // 析构函数:释放内存
    ~CircleQueue();

    // 核心接口
    bool push(const ElemType& val); // 入队
    bool pop();                     // 出队
    ElemType front();               // 获取队头元素
    int size();                     // 获取有效元素个数
    bool empty();                   // 判空
    bool full();                    // 判满
};

2. 构造与析构

cpp 复制代码
// 构造函数:初始化队列,分配数组空间
CircleQueue::CircleQueue() {
    _arr = new ElemType[MAXSIZE]; // 动态分配数组
    _front = 0;
    _rear = 0;
}

// 析构函数:释放动态数组
CircleQueue::~CircleQueue() {
    delete[] _arr;
    _arr = nullptr;
    _front = _rear = 0;
}

3. 入队操作(push)

设计思路

  1. 先判断队列是否已满,满则入队失败
  2. 未满则将数据存入队尾指针指向的位置
  3. 队尾指针向后移动一位,通过取模实现循环
cpp 复制代码
bool CircleQueue::push(const ElemType& val) {
    assert(_arr != nullptr);
    if (full()) return false;         // 队列满,拒绝入队
    _arr[_rear] = val;                // 数据存入队尾位置
    _rear = (_rear + 1) % MAXSIZE;    // 队尾指针循环后移
    return true;
}

4. 出队操作(pop)

设计思路

  1. 先判断队列是否为空,空则出队失败
  2. 非空则仅向后移动队头指针,无需删除数据(数据会被后续入队覆盖)
  3. 队头指针通过取模实现循环,保证空间复用
cpp 复制代码
bool CircleQueue::pop() {
    assert(_arr != nullptr);
    if (empty()) return false;         // 队列空,拒绝出队
    _front = (_front + 1) % MAXSIZE;   // 队头指针循环后移
    return true;
}

5. 获取队头元素(front)

设计思路

  1. 判断队列是否为空,空则抛出异常 / 提示错误
  2. 非空直接返回队头指针指向的元素,时间复杂度 O (1)
cpp 复制代码
ElemType CircleQueue::front() {
    assert(_arr != nullptr);
    assert(!empty());             // 空队列断言报错
    return _arr[_front];
}

6. 计算有效元素个数(size)

设计思路:使用通用公式适配所有指针位置,无需分支判断,简洁高效。

cpp 复制代码
int CircleQueue::size() {
    assert(_arr != nullptr);
    return (_rear - _front + MAXSIZE) % MAXSIZE;
}

7. 判空、判满辅助接口

cpp 复制代码
// 判空:front == rear
bool CircleQueue::empty() {
    return _front == _rear;
}
// 判满:牺牲一个格子,(rear+1)%MAXSIZE == front
bool CircleQueue::full() {
    return (_rear + 1) % MAXSIZE == _front;
}

四、STL容器适配器

C++ 标准库中提供了queue 容器适配器,它是一个封装好的队列,底层默认依赖deque容器,也可以指定list作为底层容器,使用简洁、安全性高,是工程开发中的首选。

1. 头文件

cpp 复制代码
#include <queue>

2. 核心使用接口

cpp 复制代码
int main() {
    // 定义整型队列
    queue<int> q;
    // 入队
    q.push(1);
    q.push(2);
    q.push(3);
    // 获取队头、队尾元素
    cout << "队头:" << q.front() << endl;
    cout << "队尾:" << q.back() << endl;
    // 出队
    q.pop();
    // 获取元素个数、判空
    cout << "元素个数:" << q.size() << endl;
    cout << "是否为空:" << q.empty() << endl;

    return 0;
}

3. 特点

  • 封装完善,无需关心底层实现
  • 满足先进先出规则,仅开放队尾入队、队头出队接口
  • 底层支持动态扩容,无容量限制

五、高频面试题

1. 循环队列相比链式队列和普通顺序队列,有哪些优势?

循环队列结合了数组和链表的优点:底层基于数组实现 ,CPU 缓存命中率高,运行效率远高于链式队列;通过环形结构实现空间循环复用,解决了普通顺序队列的假溢出问题,空间利用率极高;同时入队、出队操作时间复杂度均为 O (1),是高性能场景下的最优队列实现。

2. 循环队列为什么要牺牲一个格子?不牺牲可以吗?

循环队列中front==rear既可以表示空队列,也可以表示满队列,状态冲突无法区分。牺牲一个格子后,以(rear+1)%MAXSIZE==front作为判满条件,front==rear作为判空条件,完美解决冲突。不牺牲也可以实现,比如增加一个size变量记录元素个数,但会增加内存开销,且效率略低,因此工程中优先选择牺牲一个格子的方案。

3. 循环队列的入队、出队为什么不需要移动数据?

循环队列通过移动frontrear下标指针来标识有效数据区间 ,入队仅修改队尾指针,出队仅修改队头指针,全程不涉及任何数据元素的移动,所有操作都是对下标的修改,时间复杂度为 O (1),这是循环队列高性能的核心原因。

4. 循环队列有效元素个数的通用公式是什么?为什么要这样设计?

公式为:(rear-front+MAXSIZE)%MAXSIZE。因为循环队列的rear指针可能在front前方,也可能在后方,直接相减会出现负数,加上MAXSIZE可以保证结果为正,再通过取模运算适配环形结构,兼容所有场景,无需分支判断,简洁高效。

5. C++ STL 中的 queue 是什么?底层依赖什么容器?

STL 中的queue是容器适配器,并非原生容器,它封装了底层容器的接口,强制遵循先进先出规则。底层默认依赖**deque双端队列** ,也可以指定list作为底层容器。它屏蔽了底层实现细节,使用简单、安全性高,是实际开发中最常用的队列实现。

相关推荐
暴力求解21 小时前
数据结构---二叉树及堆的实现
数据结构·算法·二叉树
超梦dasgg21 小时前
并查集(Union-Find)详解 + Java 完整实现
java·数据结构·算法·图搜索
Shadow(⊙o⊙)21 小时前
Linux基础IO-1.0——open、close、read及write-深入手搓分析!
linux·运维·服务器·开发语言·c++·学习
小小de风呀21 小时前
de风——【从零开始学C++】(九)—vector的基本使用
开发语言·c++
AbandonForce21 小时前
从入门到入土:二分查找算法
数据结构·算法
L_090721 小时前
【C++】数据结构之哈希表(散列表)
数据结构·c++·散列表
仰泳之鹅21 小时前
【C语言】动态内存管理
c语言·数据结构·算法
LB211221 小时前
C++通讯录课设(西安石油大学)
开发语言·c++·算法
王老师青少年编程21 小时前
2026年全国青少年信息素养大赛初赛真题(算法应用主题赛C++初中组初赛真题1:文末附答案和解析)
c++·真题·全国青少年信息素养大赛·初赛·2026年·算法应用主题赛·初中组