队列
文章目录
声明
本篇是数据结构系列的第二章,主要内容是明确队列的常用实现形式以及使用队列解决常见问题的方法。
本系列基于代码源wls的数据结构系列课程,主要内容包括栈、队列、链表、树、堆、单调栈、单调队列、哈希、字典树、并查集持续更新,欢迎关注。
所有例题均可在Home - Daimayuan Online Judge的数据结构课程题单中提交。
概念
排队这个现象非常常见,队伍具有一个最显著的特点:所有人从队尾加入队列,从队首完成相应任务后离开队列
队列这种数据结构允许在队首(front)进行删除操作,在队尾(rear)实现插入操作
核心元素:大小、队首、队尾
基本操作及实现
与栈类似,队列也有相应的基本操作:
- push(x):把x从队尾塞进队首
- pop():移除队首元素
- size():返回队列元素个数
- front():返回队首元素
基本实现并不难,完全类比栈的实现
使用数组来模拟队列
定义
定义出队列的三个要素
int c[101];
int front = 1, rear = 0;
将front初始化为1和rear初始化为0是为了存放第一个元素时++rear,rear指向下标为1的位置,然后将c[rear] = x;
这样队列的下标就是从1开始,队列的范围就是从front到rear
插入
c[++rear] = x;
相当于两句话:
++rear;
c[rear] = x;
跟栈中的q[++top]含义完全相同
删除
由于队列的范围是从front到rear,所以将front后移一位就代表着删除队首元素
front++;
size
元素个数当然是rear - front + 1
rear - front + 1;
front
直接就是c[front]
c[fornt];
empty
判断队列是否为空,自然是front <= rear
上述基本操作只要熟悉就可以很好的理解下列例题啦
例1:队列
实现思路
判断操作类型,做出对应操作即可
数据处理
定义队列c,队首front,队尾rear,m记录操作次数,str记录操作类型
数据结构和输入定义为全局变量是一个好习惯
int c[100001], front = 1, rear, m;
char str[10];
判断并操作
对于每一次操作,读入操作类型并做出相应的操作
query k表示从队首向后数第k个元素的值,所以就是
c[front + k - 1]
减1是因为从前往后数第一个是队首,第二个是front + 2 - 1,以此类推
注意这里和栈一样,不需要判断整个字符串相不相等,只需要判断关键位置的字符是什么即可
while (m--) {
scanf("%s", str);
if (str[1] == 'o')
front++;
else {
if (str[2] == 's') {
scanf("%d", &temp);
c[++rear] = temp;
} else {
scanf("%d", &temp);
printf("%d\n", c[front + temp - 1]);
}
}
}
整体代码
#include <bits/stdc++.h>
using namespace std;
int c[100001], front = 1, rear, m;
char str[10];
int main() {
scanf("%d", &m);
int temp;
while (m--) {
scanf("%s", str);
if (str[1] == 'o')
front++;
else {
if (str[2] == 's') {
scanf("%d", &temp);
c[++rear] = temp;
} else {
scanf("%d", &temp);
printf("%d\n", c[front + temp - 1]);
}
}
}
return 0;
}
本题主要目的是熟悉队列的基本操作,所以没有什么难度
例2:队列练习
实现思路
模拟整个题述过程,每次输出队首元素即可。
数据处理
由于每次操作都会增加两个元素,所以一共会存放2k + 1个元素,由于下标是从1开始,队列的大小应该是2k + 2即20002
int c[200002], front = 1, rear, x, k;
核心操作
对于每一次操作,都向队尾添加两个元素,然后输出队首,并移除队首元素
while (k--) {
c[++rear] = c[front] * 2;
c[++rear] = c[front] * 2 + 1;
printf("%d\n", c[front]);
front++;
}
整体代码
#include <bits/stdc++.h>
using namespace std;
int c[200002], front = 1, rear, x, k;
int main() {
scanf("%d%d", &x, &k);
c[++rear] = x;
while (k--) {
c[++rear] = c[front] * 2;
c[++rear] = c[front] * 2 + 1;
printf("%d\n", c[front]);
front++;
}
return 0;
}
思想
对队首元素进行操作(判断或运算)后移除或加到队尾
例3:数字统计
实现思路
队列中存放数组中小于等于a[i] - 5的值,所以每次读入一个a[i],将队列中所有小于a[i] - 5的值都删掉,因为a[i]是单增的,所以所有小于a[i] - 5的值必然都在队列中靠近队首的位置。
数据处理
int c[200002], front = 1, rear, n, a;
核心操作
读入一个a后,“但凡队列中有元素且队首元素小于a - 5,就将队首元素移除”
这句话如何实现呢?
for (; c[front] < a - 5 && rear >= front; front++);
这种没有循环体的for能够很好的实现**“但凡”**这个关键词,这样写只需要一行,而且便于理解。阅读的时候只需要抓住但凡这个关键词就好
后边只需要输出队列大小和把a塞进队列就可以了
printf("%d ", rear - front + 1);
c[++rear] = a;
整体代码
#include <bits/stdc++.h>
using namespace std;
int c[200002], front = 1, rear, n, a;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", &a);
for (; c[front] < a - 5 && rear >= front; front++);
printf("%d ", rear - front + 1);
c[++rear] = a;
}
return 0;
}
思想
确定队列存放的内容,再思考如何实现
例4:排队买票
实现思路
如果队首元素买完票就存入当前时间,如果没买完就将其放到队尾。
数据处理
根据这个思路,用a[i]表示第i个人需要买多少票,t[i]表示第i个人什么时候买完票,队列里存放每个人的编号
由于一共最多要进行a[i] * n次减1,所以队列大小设为a[i] * n + 1
int c[1000001], front = 1, rear;
int n, a[1001], t[1001];
将每个人的编号放入队列
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
c[++rear] = i;
scanf("%d", &a[i]);
}
核心运算
在队列中还有元素时,首先curTime++,并将队首元素需要买的票减1,代表其买了票,如果a[c[front]] == 0了,就记录当前时间,即t[c[frfont]] = curTime,如果不是,就将其放到队尾,即c[++rear] = a[front],最后++front,因为无论其票买没买完都要将其移除队列,所以最后再++front
curTime++;
a[c[front]]--;
if (a[c[front]] <= 0)
t[c[front]] = curTime;
else
c[++rear] = c[front];
front++;
最后输出t数组即可
整体代码
#include <bits/stdc++.h>
using namespace std;
int c[1000001], front = 1, rear;
int n, a[1001], t[1001];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
c[++rear] = i;
scanf("%d", &a[i]);
}
int curTime = 0;
while (rear >= front) {
curTime++;
a[c[front]]--;
if (a[c[front]] <= 0)
t[c[front]] = curTime;
else
c[++rear] = c[front];
front++;
}
for (int i = 1; i <= n; ++i) {
printf("%d ", t[i]);
}
return 0;
}
思想
对队首进行处理后进行判断并作出相应的操作
由于进行了模拟,所以需要定义当前量记录状态
例5:约瑟夫环
实现思路
用curNum记录队首元素数到了几,如果数到了m就输出、移除、将curNum归0,没数到就将队首元素放到队尾
数据处理
用队列保存学生编号
int c[1000001], front = 1, rear;
int n, m;
将所有学生塞入队列
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
c[++rear] = i;
}
核心运算
逻辑如上所述,由于无论curNum数没数到m都需要将队首元素移除,所以把++front写到后面,这点与上一题一致
int curNum = 0;
while (rear >= front) {
curNum++;
if (curNum == m) {
printf("%d ", c[front]);
curNum = 0;
} else
c[++rear] = c[front];
++front;
}
将变量定义在循环外是一个很好的习惯,这样会减少很多时间开销
整体代码
#include <bits/stdc++.h>
using namespace std;
int c[1000001], front = 1, rear;
int n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
c[++rear] = i;
}
int curNum = 0;
while (rear >= front) {
curNum++;
if (curNum == m) {
printf("%d ", c[front]);
curNum = 0;
} else
c[++rear] = c[front];
++front;
}
return 0;
}
循环队列
数组模拟队列时,队列中的那些元素一旦被移出队列,其所在的空间便无法再利用。
可以当rear、front移动到数组末端时将其移回数组头
定义
const int size = 100;
int c[size + 1], front = 1, rear = size;
此处rear = size仅仅是为了当第一次进行入队操作时++rear能使rear变为1,从而将第一个元素存放到第一个位置,当然rear初始化为0也是可以的
size一定要大于队列的最大长度,因为如果size小于要存的元素数量,元素存不下,则必然会有元素被覆盖,如果等于元素数量,存放完后会出现front == rear 的情况,那此时队列中到底是有1个元素还是队列是满的无法判断。
基本操作
push(x)
与普通队列的唯一不同就是普通队列是rear++,循环队列是rear = rear % size + 1,这样写的好处是当rear == size 时,rear % size 等于0,此时rear就被赋值为1
rear = rear % size + 1;
c[rear] = x
pop()
操作与上述类似
front = front % size + 1;
size()
如果rear小于front,就说明元素存到了front到size和1到rear两部分,元素数量就是size - front + 1 + r
if(rear < front)
len = size - front + 1 + r;
else
len = rear - front + 1;
stl
stl提供了队列模板,不过由于很慢而且数组实现非常简单灵活,所以一般不会使用
定义
queue<元素类型> 队列名;
操作
直接调用即可
queue<int> c;
- c.push(x)
- c.pop()
- c.size()
- c.empty()
- c.front()
这些都可以使用点操作符调用,跟栈类似