数据结构------循环队列
我们今天来接着了解队列的变形------循环队列:
什么是循环队列
循环队列主要是解决,顺序结构的队列在进行pop操作时,时间复杂度为O(n)的操作:
这是因为,我们默认0号位置就是队头,所以0号位置元素一旦被弹出,后面的元素必须得补上,不然就会出现没有队头的情况。
其实我们不必规定0号位置就是队头,0号位置被弹出之后,自然1号位置就是队头,我们可以用一个变量front记录我们队头的位置:
但是这样有会有一个问题,如果队列一直弹出,就会出现front越界:
这个时候,明明有很多的空位,但是无法完成push,因为下标越界,这就是我们所说的假溢出。
所以,我们可以某种方式,使得front越界之后,重新回到开头:
这就是循环队列循环 的来历,但是这样也只能完成队列的push,无法完成pop,所以我们不能只要一个指针,我们需要两个指针,一个用来指向队头,一个用来指向队尾:
这就是循环队列的基本结构:
循环队列是一种特殊类型的队列数据结构,它利用固定大小的数组(通常是连续的内存区域)模拟无限循环的空间,从而有效地解决了普通顺序队列在使用过程中可能出现的"假溢出"问题。循环队列通过将数组的两端连接起来,形成一个逻辑上的环形结构,使得队列的头部和尾部可以循环地在数组范围内移动,而不是受限于数组的固定边界。
以下是循环队列的主要特点和运作机制:
-
结构:
- 循环队列使用一个固定大小的数组(通常称为缓冲区)来存储元素。
- 有两个指针,分别表示队列的头部(front)和尾部(rear)。它们用来追踪队列中元素的插入和移除位置。
-
插入(入队):
- 当新元素被添加到队列尾部时,尾指针(rear)递增。
- 当尾指针到达数组的末尾时,不是停止插入(导致假溢出),而是将其绕回到数组的起始位置,继续在数组中下一个可用位置存放元素。这种绕回操作是通过取模运算(
rear = rear % capacity
)实现的,保证了尾指针始终在数组范围内循环移动。
-
移除(出队):
- 从队列头部移除元素时,头指针(front)递增。
- 同样,当头指针到达数组末尾时,也通过取模运算使其绕回到数组起始位置,这样就可以持续访问到队列中的下一个待出队元素。
-
空间利用率:
- 由于循环队列能够在数组范围内循环利用空间,即使队列未填满整个数组,也可以连续地进行入队和出队操作,避免了普通顺序队列因头尾指针接近而导致无法继续插入新元素(尽管数组中仍有空闲位置)的情况,即"假溢出"。
-
判断队列状态:
- 判断循环队列是否为空:
front == rear
。 - 判断循环队列是否已满:通常有两种策略:
- 如果要求队列元素间至少有一个空位作为分隔(区分满和空),则判断条件为
((rear + 1) % capacity) == front
。 - 如果允许队列满时尾部直接追上头部(即队列满时前后相邻),则判断条件为
(rear == front) && (_size == _capacity)
。
- 判断循环队列是否为空:
-
实现方式:
- 循环队列既可以基于数组实现,也可以借助单链表等其他线性数据结构实现。但最常见的是使用数组,因其能提供常数时间复杂度的随机访问能力,且空间连续,有利于缓存优化。
队满和队空的问题
现在有一个问题,我们一开始设置的队空的时候,队头指针和队尾指针都是指向的一个位置:
如果我们插入元素插满之后,rear会越界:
这个时候,rear越界,会重新回到0号位置:
这个时候队满和队空都是指向一个位置,无法区别,这时候我们有两种办法,一种是设定标志位flag,另一种是留一个空位,我们着重来介绍一下第二种方法:
留一个空位
为了区分队满和队空的区别,我们留一个空位,这个时候队满的条件就会变成:rear + 1 == front :
rear指向8,8 + 1 = 9,越界,此时取模 9 % 9 = 0,和front重合,说明队列已满。
因为这是循环队列,所以也会出现下面的情况:
这是因为rear已经转过了一圈,整合上面的两种情况,我们可以得到队满的判断条件:
(rear + 1)% _capacity == front
上面公式可以包括这两种情况,若rear > front,取模完成循环,否则取模是无效操作。
元素个数
当rear > front时,直接rear - front就是元素个数:
当rear < front时:
此时元素个数分为两部分:一部分是_capacity - front ,另一部分是rear - 0
rear - front + _capacity
归并上面两个式子,可以得到元素个数:
(rear - front + _capacity) % _capacity
了解了难点之后,我们就可以编写代码:
cpp
#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()
函数,打印队列中的所有元素。
我们可以测试一下:
cpp
#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;
}
记录元素个数
上面的代码一般来说是考试喜欢考的,如果是自己实现,完全可以自己记录元素个数:
cpp
#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;
};
也是可以达到同样的效果: