04、队列
队列
队列模型
像栈一样,队列(queue)也是表。然而使用队列时插入在一端进行而删除在另一端进行,遵守先进先出的规则。所以队列的另一个名字是(FIFO)。
队列的基本操作
入队(enqueue):它是在表的末端(队尾(rear)插入一个元素。
出队(dequeue): 出队他是删除在表的开头(队头(front))的元素。
注:下面模型只象征着输入输出操作
具体模型
队列的实现
下面给大家说一下队列的简单使用
#include <queue>
#include <iostream>
#include <string>
int main() {
// 创建不同类型的队列
std::queue<int> int_queue;
std::queue<std::string> string_queue;
std::queue<double> double_queue;
int_queue.push(10);
int_queue.push(20);
int_queue.push(30);
int_queue.push(40);
int_queue.push(50);
std::cout << "队列的大小: " << int_queue.size() << std::endl;
std::cout << "队首元素: " << int_queue.front() << std::endl;
std::cout << "队尾元素: " << int_queue.back() << std::endl;
while (!int_queue.empty()) {
std::cout << "出队: " << int_queue.front() << std::endl;
int_queue.pop();
}
string_queue.push("苹果");
string_queue.push("香蕉");
string_queue.push("橙子");
string_queue.push("葡萄");
std::cout << "队列的大小: " << string_queue.size() << std::endl;
std::cout << "队首元素: " << string_queue.front() << std::endl;
while (!string_queue.empty()) {
std::cout << "出队: " << string_queue.front() << std::endl;
string_queue.pop();
}
double_queue.push(3.14);
double_queue.push(2.718);
double_queue.push(1.414);
// 演示队列的其他操作
std::queue<int> demo_queue;
std::cout << "队列是否为空: " << (demo_queue.empty() ? "是" : "否") << std::endl;
demo_queue.push(100);
demo_queue.push(200);
demo_queue.push(300);
std::cout << "压入3个元素后,队列的大小: " << demo_queue.size() << std::endl;
std::cout << "队首元素: " << demo_queue.front() << std::endl;
demo_queue.pop();
std::cout << "出队1个元素后,队列的大小: " << demo_queue.size() << std::endl;
std::cout << "新的队首元素: " << demo_queue.front() << std::endl;
return 0;
}
我们想一下,既然我们只需要先入先出,顺序入队,那是否可以使用 vector 来实现队列呢?
必须可以
// 使用vector实现队列类
template<typename T>
class Queue {
private:
std::vector<T> data;
public:
// 入队操作
void push(const T& item) {
data.push_back(item);
}
// 出队操作
void pop() {
if (!empty()) {
data.erase(data.begin());
}
}
// 查看队首元素
T front() const {
if (!empty()) {
return data.front();
}
throw std::runtime_error("队列为空");
}
// 查看队尾元素
T back() const {
if (!empty()) {
return data.back();
}
throw std::runtime_error("队列为空");
}
// 判断队列是否为空
bool empty() const {
return data.empty();
}
// 获取队列大小
size_t size() const {
return data.size();
}
// 清空队列
void clear() {
data.clear();
}
};
循环队列
循环队列的出现就是为了解决队列的假溢出问题。
何为假溢出呢?我们运用数组实现队列时,数组长度为 5,我们放入了[1,2,3,4,5].
我们将 1,2 出队,此时如果继续加入 6 时,因为数组末尾元素已经被占用,再向后加则会溢出,但是我们的下标 0,和下标 1 还是空闲的。
所以我们把这种现象叫做“假溢出”。
例如,我们在学校里面排队洗澡一人一个格,当你来到澡堂发现头部有两个空格,但是其余格子已经满了,你是去前面洗,还是等其他格子的哥们洗完再洗?
肯定是去前面的格子洗。除非澡堂的所有格子都满了,我们才会等。
所以我们用来解决假溢出的方法就是后面满了,就再从头开始,也就是头尾相接的循环,我们把队列的这种头尾相接的顺序存储结构成为循环队列。
我们发现队列为空时 front == rear,队列满时也是 front == rear,那么问题来了,我们应该怎么区分满和空呢?
我们可以通过以下两种方法进行区分,
1.设置标记变量 flag; 当 front == rear 时且 flag = 0 时为空,当 front == rear 且 flag == 1 时为满
2.当队列为空时,front == rear, 当队列满时,我们保留一个元素空间,也就是说,队列满时,数组内还有一个空间(故意不放置内容)。
两种方式的具体介绍:
循环队列中,由于队头(front)和队尾(rear)指针会循环移动,当 front == rear 时可能表示队列空或满,需要通过特殊方式区分。常用两种两种常用方案:
- 标记变量法
- 增设一个布尔型标记变量
flag(初始为false)。 - 空队列:
front == rear且flag == false(初始状态或所有元素出队后)。 - 满队列:
front == rear且flag == true(当元素入队导致队尾追上队头时,将flag置为true)。 - 入队时,若操作后
front == rear,则flag = true;出队时,若操作后front == rear,则flag = false。 - 优点:队列空间可完全利用(数组大小为
n时,最多存储n个元素)。 - 缺点:需额外维护标记变量,逻辑稍复杂。
- 增设一个布尔型标记变量
- 预留空间法
- 不设标记,而是通过预留一个空元素空间区分状态。
- 空队列:
front == rear(初始状态或所有元素出队后)。 - 满队列:
(rear + 1) % maxSize == front(队尾指针的下一个位置是队头时,视为满)。 - 此时队列最大存储量为
maxSize - 1(maxSize为数组长度),始终保留一个空闲位置。 - 优点:无需额外变量,逻辑简单,实现方便。
- 缺点:浪费一个存储空间,当队列容量较小时,空间利用率略低。
两种方法各有侧重:标记变量法适合对空间利用率要求高的场景,预留空间法则更简洁直观,是实际开发中更常用的方案。
例:
暂时无法在飞书文档外展示此内容
然后我们再根据以下公式则能够判断队列满没满了。
(rear+1) % queue_size == front
queue_size 代表队列的长度,上图为 5。我们来判断上面两张图是否满。(4+1) % 5 = 0,则是满的,(1+1) % 5 = 2 则是没满的





