队列就像食堂打饭的队伍,先来的人先打到饭(FIFO原则),后来的人只能排在队尾。但在计算机内存中,用数组实现的普通队列会遇到"假溢出"问题——明明数组前面有空位,却因为队尾指针跑到数组末尾而无法继续插入新元素。这就好比食堂窗口前明明有空位,却因为队伍排到了墙角而无法继续加人。
循环队列通过将数组首尾相连形成环形结构,完美解决了这个问题。今天我们就深入探讨循环队列的实现细节,分享我在实际项目中的使用心得,并提供一个可直接复用的C++实现方案。
普通顺序队列在进行多次入队和出队操作后,会出现"假溢出"现象。假设队列容量为5:
code复制初始状态:[ , , , , ] front=0, rear=0
入队A,B,C:[A,B,C, , ] front=0, rear=3
出队A,B:[ , ,C, , ] front=2, rear=3
入队D,E,F:[ , ,C,D,E] front=2, rear=0 (此时rear循环回到起点)
虽然数组前两个位置空闲,但如果不采用循环结构,插入F时就会因rear超出数组范围而报错。循环队列通过取模运算让指针能够"绕回"数组开头:
cpp复制rear = (rear + 1) % MaxSize; // 当rear到达末尾时,会回到0
循环队列最精妙也最容易出错的就是如何区分队空和队满状态。常见有三种实现方式:
牺牲一个存储单元法(本文示例采用的方法)
front == rear(rear + 1) % MaxSize == front计数法(维护size变量)
cpp复制struct {
ElemType data[MaxSize];
int front, rear;
int size; // 新增计数器
} SqQueue;
size == 0size == MaxSize标记法(记录最后一次操作类型)
cpp复制struct {
ElemType data[MaxSize];
int front, rear;
bool lastOpIsPush; // 最后一次操作是入队
} SqQueue;
front == rear && !lastOpIsPushfront == rear && lastOpIsPush实际项目中,牺牲一个存储单元的方法最为常用,因为现代计算机内存充足,用少量空间换取代码简洁性是值得的。
cpp复制#include <iostream>
using namespace std;
#define ElemType int // 元素类型可随时替换
#define MaxSize 50 // 根据实际需求调整
typedef struct {
ElemType data[MaxSize]; // 存储数组
int front, rear; // 队首和队尾指针
} SqQueue;
这里使用宏定义MaxSize而不是直接写数字50,是为了方便后续修改队列容量。在实际工程中,建议使用constexpr替代宏:
cpp复制constexpr int MaxSize = 50; // C++11推荐方式
cpp复制void InitQueue(SqQueue &Q) {
Q.rear = Q.front = 0; // 初始时队首队尾都指向0
}
注意这里使用引用参数&Q,避免结构体拷贝。这是C++特有的特性,C语言中需要使用指针。
cpp复制bool EnQueue(SqQueue &Q, ElemType x) {
if ((Q.rear + 1) % MaxSize == Q.front) // 队满检查
return false;
Q.data[Q.rear] = x; // 新元素放入队尾
Q.rear = (Q.rear + 1) % MaxSize; // 队尾指针循环后移
return true;
}
这里有一个常见陷阱:(Q.rear + 1) % MaxSize不能简化为Q.rear + 1,否则无法实现循环效果。
cpp复制bool DeQueue(SqQueue &Q, ElemType &x) {
if (Q.rear == Q.front) // 队空检查
return false;
x = Q.data[Q.front]; // 获取队首元素
Q.front = (Q.front + 1) % MaxSize; // 队首指针循环后移
return true;
}
注意出队操作通过引用参数x返回被删除的元素值,这是C++中常用的输出参数方式。
cpp复制int main() {
SqQueue Q;
InitQueue(Q);
cout << "入队1-10:" << endl;
for (int i = 1; i <= 10; ++i) {
EnQueue(Q, i);
cout << i << " ";
}
cout << endl;
cout << "出队前5个元素:" << endl;
ElemType x;
for (int i = 0; i < 5; ++i) {
DeQueue(Q, x);
cout << x << " ";
}
cout << endl;
cout << "再入队11-15:" << endl;
for (int i = 11; i <= 15; ++i) {
EnQueue(Q, i);
cout << i << " ";
}
cout << endl;
cout << "全部出队:" << endl;
while (DeQueue(Q, x)) {
cout << x << " ";
}
cout << endl;
return 0;
}
这个测试案例展示了循环队列的完整生命周期:初始化→连续入队→部分出队→再次入队→全部出队。
固定大小的循环队列在实际工程中往往不够灵活。我们可以实现动态扩容版本:
cpp复制bool EnQueue_Dynamic(SqQueue &Q, ElemType x) {
// 检查是否需要扩容
if ((Q.rear + 1) % MaxSize == Q.front) {
ElemType* newData = new ElemType[MaxSize * 2];
if (!newData) return false; // 内存分配失败
// 将原数据复制到新数组
int i = 0;
while (Q.front != Q.rear) {
newData[i++] = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
}
delete[] Q.data;
Q.data = newData;
Q.front = 0;
Q.rear = i;
MaxSize *= 2;
}
// 正常入队操作
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
return true;
}
注意:动态扩容会带来性能开销,在实时性要求高的场景慎用。通常建议初始化时设置合理的MaxSize。
在多线程环境下使用队列时,需要添加互斥锁:
cpp复制#include <mutex>
typedef struct {
ElemType data[MaxSize];
int front, rear;
std::mutex mtx; // 互斥锁
} ThreadSafeQueue;
bool SafeEnQueue(ThreadSafeQueue &Q, ElemType x) {
std::lock_guard<std::mutex> lock(Q.mtx);
if ((Q.rear + 1) % MaxSize == Q.front)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
return true;
}
虽然队列通常只关心队首和队尾元素,但有时需要遍历所有元素:
cpp复制void TraverseQueue(const SqQueue &Q) {
if (Q.front == Q.rear) {
cout << "队列为空" << endl;
return;
}
int i = Q.front;
cout << "队列元素:";
while (i != Q.rear) {
cout << Q.data[i] << " ";
i = (i + 1) % MaxSize;
}
cout << endl;
}
典型错误实现:
cpp复制// 错误的队满判断
if (Q.rear + 1 == Q.front) // 缺少取模运算
return false;
正确做法必须使用取模运算:
cpp复制if ((Q.rear + 1) % MaxSize == Q.front)
计算公式:
cpp复制int QueueLength(const SqQueue &Q) {
return (Q.rear - Q.front + MaxSize) % MaxSize;
}
这个公式在rear在front前面和后面时都能正确计算元素个数。
| 特性 | 循环队列 | 链式队列 |
|---|---|---|
| 内存布局 | 连续内存,缓存友好 | 内存分散,可能缓存缺失 |
| 固定大小 | 是,需预先分配 | 否,动态增长 |
| 实现复杂度 | 中等(需处理循环逻辑) | 简单(直接操作指针) |
| 适用场景 | 已知最大容量/性能敏感场景 | 容量不确定/频繁动态变化场景 |
当队列存储的是指针类型时,出队操作需要特别注意:
cpp复制typedef struct {
char* data[MaxSize]; // 存储字符串指针
int front, rear;
} StrQueue;
bool DeQueue_Str(StrQueue &Q, char* &x) {
if (Q.rear == Q.front)
return false;
x = Q.data[Q.front]; // 返回指针
Q.data[Q.front] = nullptr; // 清空槽位
Q.front = (Q.front + 1) % MaxSize;
return true;
}
// 使用示例
StrQueue strQ;
InitQueue(strQ);
char* str = new char[100];
EnQueue(strQ, str);
// 出队后需要手动释放内存
char* outStr;
if (DeQueue_Str(strQ, outStr)) {
// 使用outStr...
delete[] outStr; // 必须释放内存!
}
现代CPU缓存对性能影响极大。我们可以优化结构体布局:
cpp复制// 优化前:结构体包含数据和指针
typedef struct {
ElemType data[MaxSize]; // 数据块
int front, rear; // 控制块
} SqQueue;
// 优化后:分离热数据(hot)和冷数据(cold)
struct QueueData {
ElemType data[MaxSize]; // 频繁访问的热数据
};
struct QueueCtrl {
int front, rear; // 较少访问的冷数据
};
class OptimizedQueue {
private:
QueueData* data; // 热数据单独分配
QueueCtrl ctrl; // 控制数据
};
这种布局可以提高缓存命中率,因为front/rear的访问频率通常远低于实际数据。
对于高频场景,可以提供批量操作接口减少函数调用开销:
cpp复制// 批量入队
int EnQueueBatch(SqQueue &Q, const ElemType* items, int count) {
int enqueued = 0;
while (enqueued < count && (Q.rear + 1) % MaxSize != Q.front) {
Q.data[Q.rear] = items[enqueued];
Q.rear = (Q.rear + 1) % MaxSize;
enqueued++;
}
return enqueued; // 返回实际入队数量
}
// 批量出队
int DeQueueBatch(SqQueue &Q, ElemType* buffer, int maxCount) {
int dequeued = 0;
while (dequeued < maxCount && Q.front != Q.rear) {
buffer[dequeued] = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
dequeued++;
}
return dequeued; // 返回实际出队数量
}
对于频繁创建销毁的队列,可以使用内存池技术:
cpp复制class QueuePool {
private:
vector<SqQueue*> pool;
public:
SqQueue* Allocate() {
if (!pool.empty()) {
SqQueue* q = pool.back();
pool.pop_back();
InitQueue(*q); // 重置状态
return q;
}
return new SqQueue;
}
void Deallocate(SqQueue* q) {
pool.push_back(q);
}
~QueuePool() {
for (auto q : pool) delete q;
}
};
这种技术特别适合实时系统,可以避免频繁的内存分配释放操作。
循环队列是实现生产者-消费者模型的理想选择:
cpp复制#include <thread>
#include <chrono>
SqQueue taskQueue;
std::mutex queueMutex;
const int MaxTasks = 100;
void producer() {
for (int i = 0; i < 150; ++i) {
std::lock_guard<std::mutex> lock(queueMutex);
if (!EnQueue(taskQueue, i)) {
cout << "队列已满,等待..." << endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
--i; // 重试
} else {
cout << "生产任务: " << i << endl;
}
}
}
void consumer() {
for (int i = 0; i < 150; ++i) {
std::lock_guard<std::mutex> lock(queueMutex);
ElemType task;
if (!DeQueue(taskQueue, task)) {
cout << "队列为空,等待..." << endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
--i; // 重试
} else {
cout << "消费任务: " << task << endl;
}
}
}
int main() {
InitQueue(taskQueue);
std::thread prod(producer);
std::thread cons(consumer);
prod.join();
cons.join();
return 0;
}
在网络编程中,循环队列常用于缓冲接收到的数据包:
cpp复制struct Packet {
uint32_t srcIP;
uint32_t dstIP;
uint16_t srcPort;
uint16_t dstPort;
uint8_t* payload;
size_t length;
};
#define MAX_PACKETS 1024
typedef struct {
Packet packets[MAX_PACKETS];
int front, rear;
std::mutex mtx;
} PacketQueue;
void ProcessPacket(PacketQueue &queue) {
while (true) {
Packet pkt;
{
std::lock_guard<std::mutex> lock(queue.mtx);
if (!DeQueue(queue, pkt)) {
continue; // 无数据包,继续轮询
}
}
// 处理数据包...
cout << "处理来自" << pkt.srcIP << "的数据包" << endl;
delete[] pkt.payload; // 释放内存
}
}
游戏引擎常用循环队列处理输入事件:
cpp复制enum GameEventType { KEY_PRESS, MOUSE_CLICK, COLLISION };
struct GameEvent {
GameEventType type;
union {
struct { int keyCode; } key;
struct { int x, y, button; } mouse;
struct { EntityID a, b; } collision;
} data;
};
#define MAX_EVENTS 256
typedef struct {
GameEvent events[MAX_EVENTS];
int front, rear;
} EventQueue;
void ProcessEvents(EventQueue &queue) {
GameEvent evt;
while (DeQueue(queue, evt)) {
switch (evt.type) {
case KEY_PRESS:
HandleKeyPress(evt.data.key.keyCode);
break;
case MOUSE_CLICK:
HandleMouseClick(evt.data.mouse.x, evt.data.mouse.y);
break;
case COLLISION:
HandleCollision(evt.data.collision.a, evt.data.collision.b);
break;
}
}
}
完善的测试应该覆盖以下边界条件:
cpp复制void TestQueue() {
SqQueue Q;
InitQueue(Q);
// 测试空队列出队
ElemType x;
assert(!DeQueue(Q, x));
// 填满队列
for (int i = 0; i < MaxSize - 1; ++i) {
assert(EnQueue(Q, i));
}
// 测试队满
assert(!EnQueue(Q, 100));
// 清空队列
for (int i = 0; i < MaxSize - 1; ++i) {
assert(DeQueue(Q, x));
assert(x == i);
}
// 测试队空
assert(!DeQueue(Q, x));
// 测试循环特性
for (int i = 0; i < MaxSize * 2; ++i) {
assert(EnQueue(Q, i));
assert(DeQueue(Q, x));
assert(x == i);
}
cout << "所有测试通过!" << endl;
}
使用工具检测内存问题:
Valgrind (Linux/Mac):
bash复制valgrind --leak-check=full ./your_program
AddressSanitizer (GCC/Clang):
bash复制g++ -fsanitize=address -g your_code.cpp
使用性能分析工具定位热点:
cpp复制#include <chrono>
void Benchmark() {
SqQueue Q;
InitQueue(Q);
const int N = 1000000;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
EnQueue(Q, i);
DeQueue(Q, i);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
cout << N << "次操作耗时: " << duration.count() << "ms" << endl;
}
对于高性能场景,可以考虑无锁(lock-free)队列:
cpp复制#include <atomic>
template<typename T, size_t N>
class LockFreeQueue {
std::atomic<size_t> front{0}, rear{0};
T data[N];
public:
bool Enqueue(const T& item) {
size_t currRear = rear.load(std::memory_order_relaxed);
size_t nextRear = (currRear + 1) % N;
if (nextRear == front.load(std::memory_order_acquire))
return false;
data[currRear] = item;
rear.store(nextRear, std::memory_order_release);
return true;
}
bool Dequeue(T& result) {
size_t currFront = front.load(std::memory_order_relaxed);
if (currFront == rear.load(std::memory_order_acquire))
return false;
result = data[currFront];
front.store((currFront + 1) % N, std::memory_order_release);
return true;
}
};
实现带优先级的队列系统:
cpp复制#define PRIORITY_LEVELS 3
class PriorityQueue {
SqQueue queues[PRIORITY_LEVELS];
public:
PriorityQueue() {
for (int i = 0; i < PRIORITY_LEVELS; ++i)
InitQueue(queues[i]);
}
bool Enqueue(int priority, ElemType x) {
if (priority < 0 || priority >= PRIORITY_LEVELS)
return false;
return EnQueue(queues[priority], x);
}
bool Dequeue(ElemType &x) {
// 按优先级从高到低检查
for (int i = 0; i < PRIORITY_LEVELS; ++i) {
if (DeQueue(queues[i], x))
return true;
}
return false; // 所有队列都为空
}
};
实现可持久化到磁盘的队列:
cpp复制#include <fstream>
bool SaveQueue(const SqQueue &Q, const string& filename) {
std::ofstream out(filename, std::ios::binary);
if (!out) return false;
out.write(reinterpret_cast<const char*>(&Q.front), sizeof(Q.front));
out.write(reinterpret_cast<const char*>(&Q.rear), sizeof(Q.rear));
int count = (Q.rear - Q.front + MaxSize) % MaxSize;
int index = Q.front;
for (int i = 0; i < count; ++i) {
out.write(reinterpret_cast<const char*>(&Q.data[index]), sizeof(ElemType));
index = (index + 1) % MaxSize;
}
return out.good();
}
bool LoadQueue(SqQueue &Q, const string& filename) {
std::ifstream in(filename, std::ios::binary);
if (!in) return false;
in.read(reinterpret_cast<char*>(&Q.front), sizeof(Q.front));
in.read(reinterpret_cast<char*>(&Q.rear), sizeof(Q.rear));
int count = (Q.rear - Q.front + MaxSize) % MaxSize;
int index = Q.front;
for (int i = 0; i < count; ++i) {
in.read(reinterpret_cast<char*>(&Q.data[index]), sizeof(ElemType));
index = (index + 1) % MaxSize;
}
return in.good();
}
理解循环队列后,可以进一步学习:
这些高级队列结构在操作系统、数据库、分布式系统中都有广泛应用。循环队列作为基础,其设计思想和实现技巧是理解这些复杂结构的基石。