数据结构——循环队列
我们今天来接着了解队列的变形——循环队列:
什么是循环队列
循环队列主要是解决,顺序结构的队列在进行pop操作时,时间复杂度为O(n)的操作:
这是因为,我们默认0号位置就是队头,所以0号位置元素一旦被弹出,后面的元素必须得补上,不然就会出现没有队头的情况。
其实我们不必规定0号位置就是队头,0号位置被弹出之后,自然1号位置就是队头,我们可以用一个变量front记录我们队头的位置:
但是这样有会有一个问题,如果队列一直弹出,就会出现front越界:
这个时候,明明有很多的空位,但是无法完成push,因为下标越界,这就是我们所说的假溢出。
所以,我们可以某种方式,使得front越界之后,重新回到开头:
这就是循环队列循环的来历,但是这样也只能完成队列的push,无法完成pop,所以我们不能只要一个指针,我们需要两个指针,一个用来指向队头,一个用来指向队尾:
这就是循环队列的基本结构:
循环队列是一种特殊类型的队列数据结构,它利用固定大小的数组(通常是连续的内存区域)模拟无限循环的空间,从而有效地解决了普通顺序队列在使用过程中可能出现的“假溢出”问题。循环队列通过将数组的两端连接起来,形成一个逻辑上的环形结构,使得队列的头部和尾部可以循环地在数组范围内移动,而不是受限于数组的固定边界。
以下是循环队列的主要特点和运作机制:
-
结构:
-
插入(入队):
-
移除(出队):
-
空间利用率:
-
判断队列状态:
-
实现方式:
队满和队空的问题
现在有一个问题,我们一开始设置的队空的时候,队头指针和队尾指针都是指向的一个位置:
如果我们插入元素插满之后,rear会越界:
这个时候,rear越界,会重新回到0号位置:
这个时候队满和队空都是指向一个位置,无法区别,这时候我们有两种办法,一种是设定标志位flag,另一种是留一个空位,我们着重来介绍一下第二种方法:
留一个空位
为了区分队满和队空的区别,我们留一个空位,这个时候队满的条件就会变成:rear + 1 == front:
rear指向8,8 + 1 = 9,越界,此时取模 9 % 9 = 0,和front重合,说明队列已满。
因为这是循环队列,所以也会出现下面的情况:
这是因为rear已经转过了一圈,整合上面的两种情况,我们可以得到队满的判断条件:
上面公式可以包括这两种情况,若rear > front,取模完成循环,否则取模是无效操作。
元素个数
当rear > front时,直接rear - front就是元素个数:
当rear < front时:
此时元素个数分为两部分:一部分是_capacity - front ,另一部分是rear - 0
归并上面两个式子,可以得到元素个数:
了解了难点之后,我们就可以编写代码:
#pragma once
#include<iostream>
#include<cassert>
// 循环队列模板类
template<class T>
class CircQueue
{
public:
// 默认构造函数,初始化队列容量为10
CircQueue()
{
_data = new T[10];
_capacity = 10;
}
// 带参数构造函数,初始化队列容量为size+1
CircQueue(const size_t& size)
{
_data = new T[size + 1];
_capacity = size + 1;
}
// 判断队列是否为空
bool empty()
{
return front == rear;
}
// 判断队列是否已满
bool fullQueue()
{
return ((rear + 1) % _capacity) == front;
}
// 入队操作
void push(const T& data)
{
assert(!fullQueue()); // 断言队列不为满
_data[rear++] = data;
// rear取模循环
rear = rear % _capacity;
}
// 出队操作
T pop()
{
assert(!empty()); // 断言队列不为空
T data = _data[front];
front++;
// 循环
front = front % _capacity;
return data;
}
// 返回元素数量
size_t size()
{
return (rear - front + _capacity) % _capacity;
}
// 打印队列中的所有元素
void PrintCicQueue()
{
int index = front;
// 当rear在front之后(正常情况)
if (rear > front)
{
while (index < _capacity - 1)
{
std::cout << _data[index] << " ";
index++;
}
}
// 当front在rear之后或等于rear(队列元素跨越队尾回绕至队首)
else if (front >= rear)
{
if (index < _capacity - 1)
{
while (index != _capacity - 1)
{
std::cout << _data[index] << " ";
index++;
}
if (index == _capacity - 1)
{
index = index % (_capacity - 1);
while (index != rear)
{
std::cout << _data[index] << " ";
index++;
}
}
}
}
}
private:
T* _data; // 动态数组
int front = 0; // 头指针
int rear = 0; // 尾指针
size_t _capacity; // 队列容量
};
在这个循环队列模板类中,我们实现了以下功能:
- 默认构造函数和带参数构造函数,用于初始化队列。
empty()
函数,判断队列是否为空。fullQueue()
函数,判断队列是否已满。push()
函数,用于向队列中添加元素。pop()
函数,用于从队列中移除元素。size()
函数,返回队列中的元素数量。PrintCicQueue()
函数,打印队列中的所有元素。
我们可以测试一下:
#include"CircQueue.h"
int main()
{
CircQueue<int> cdeque(10);
cdeque.push(23);
cdeque.push(2);
cdeque.push(1);
cdeque.push(231);
cdeque.push(3);
cdeque.push(5);
cdeque.push(0);
cdeque.push(7);
cdeque.push(17);
cdeque.push(0);
cdeque.pop();
cdeque.pop();
cdeque.pop();
cdeque.push(188);
cdeque.push(23);
cdeque.push(6);
//cdeque.push(6);
cdeque.PrintCicQueue();
std::cout << "元素个数:"<< cdeque.size() << std::endl;
return 0;
}
记录元素个数
上面的代码一般来说是考试喜欢考的,如果是自己实现,完全可以自己记录元素个数:
#pragma once
#include<iostream>
#include<cassert>
// 循环队列模板类
template<class T>
class CircQueue
{
public:
// 默认构造函数,创建一个初始容量为10的循环队列
CircQueue()
: _size(0)
{
_data = new T[10];
_capacity = 10;
}
// 构造函数,创建一个指定容量的循环队列
CircQueue(const size_t& size)
: _size(0)
{
_data = new T[size];
_capacity = size;
}
// 入队操作:将数据添加到队列尾部
void push(const T& data)
{
assert(_size < _capacity); // 确保队列未满
_data[rear++] = data; // 将数据存入队尾
_size++; // 队列元素个数加一
// 假溢出处理:将rear指针回绕到队列开始位置
rear = rear % _capacity;
}
// 出队操作:从队列头部移除并返回数据
T pop()
{
assert(_size != 0); // 确保队列非空
T data = _data[front]; // 获取队首数据
front++; // 队首指针后移
// 假溢出处理:将front指针回绕到队列开始位置
front = front % _capacity;
_size--; // 队列元素个数减一
return data; // 返回移除的数据
}
// 返回队列中元素的个数
size_t size() const
{
return _size;
}
// 判断队列是否为空
bool empty() const
{
return _size == 0;
}
// 打印队列中的所有元素
void PrintCicQueue()
{
int index = front;
// 当rear在front之后(正常情况)
if (rear > front)
{
while (index < _capacity)
{
std::cout << _data[index] << " ";
index++;
}
}
// 当front在rear之后或等于rear(队列元素跨越队尾回绕至队首)
else if (front >= rear)
{
if (index < _capacity)
{
while (index != _capacity)
{
std::cout << _data[index] << " ";
index++;
}
if (index == _capacity)
{
index = index % _capacity;
while (index != rear)
{
std::cout << _data[index] << " ";
index++;
}
}
}
}
}
private:
// 存储队列元素的动态数组
T* _data;
// 当前队列中元素的个数
size_t _size;
// 队列的最大容量
size_t _capacity;
// 指向队列头部的指针
int front = 0;
// 指向队列尾部的指针
int rear = 0;
};
也是可以达到同样的效果: