[数据结构]队列与队列的应用

(在阅读前,建议先了解的相关内容)

队列和栈同为线性结构中非常重要的两种,区别在于,栈是一种“后进先出”的数据结构,而队列“先进先出”。
考虑问题:

您需要写一种数据结构,维护一系列数,初始为空。定义其中一端为队头,另一端为队尾,要求提供三种操作:

(1)格式: 1 1 1 x x x,表示在队尾端处加入一个数 x x x;( O ( 1 ) O(1) O(1)

(2)格式: 2 2 2,表示输出队头端的数,并将其删除;( O ( 1 ) O(1) O(1)

(3)格式: 3 3 3,表示判断数据结构是否为空。( O ( 1 ) O(1) O(1)

(括号中的数表示操作的期望复杂度)
顾名思义,队列可以被形象地理解成一个队伍,在这里不存在插队的问题,队列始终是公平的,先排着的人就会排在前面,也当然就会先离开队伍。队列比栈略微复杂,因为队列的两端都可以进行相应的操作,而栈只在一端进行操作。

类比我们在栈的学习中曾经讲到的:应用带有一个指针的数组来模拟栈,而这个指针指向栈顶。那么我们就可以得到队列模拟的基本思路:使用一个带有两个指针的数组,分别指向队头和队尾的位置。

于是,在队尾端加入数时,便可以类似栈的模拟,将队尾向右移动一格,并将数字写在队尾所指的位置。(注意,模拟中队尾的位置靠右,队头靠左,否则表示队列为空) 当队头要删除数字时,我们也类似栈的模拟,将队头向右侧移动一格即可。

队列的应用中有一个重点,那就是在正常情况下,已经被用过且删除的内存(或数组下标)是不会被重复利用的。在栈中,如果我们删除一个元素再加入一个元素,那么新元素将会覆盖旧的元素,但是我们如果在队列中删除再加入,那么队列的整体位置就会向右移动一格,久而久之,即使队列中每个时刻的数都不多,也有可能超出内存限制。下面我们来考虑解决这个问题。
队列的模拟

循环队列思想

考虑上面的问题,假如增加一个条件:队列中每个时刻的元素个数不超过 1 0 5 10^5 105,但是总操作数却到达了 5 × 1 0 6 5\times 10^6 5×106的级别,那么如果我们按照上面的模拟方法,数组大小可能要开到 5 × 1 0 6 5\times 10^6 5×106,然而每时的数量却远小于这个水平,那么有没有办法压缩需要用的空间呢?

由于队列中最多只有这么多数,而且队列中当前存储的(在队头与队尾之间的)数都是连续的一段下标,因此当我们所用下标超过 1 0 5 10^5 105时,下标1(或者说0,总之是第一个下标)一定是无效值,所以我们只要令 1 0 5 10^5 105的下一个位置变为1(或0),使得整个队列形成一个循环,就可以减少内存的开销,只需要少量内存就可以实现这个功能。

以下是循环队列的模板。每次将头尾指针对最大数取模,即可实现上述循环思想。

#include <bits/stdc++.h>
using namespace std;
const int P=100000;
struct QUEUE_ {
	int val[P+10],h,t;
};
void init_ (QUEUE_ &x) {
	x.t=-1,x.h=0;
	return;
}
int push_ (QUEUE_ &x,int y) {
	x.t++;
	x.val[x.t%P]=y;
	return y;
}
int front_ (QUEUE_ x) {
	return x.val[x.h%P];
}
int pop_ (QUEUE_ &x) {
	x.h++;
	return x.val[(x.h-1)%P];
}
bool empty_ (QUEUE_ x) {
	return (x.h>x.t);
}
int main () {
	return 0;
}

接下来以几道题目为例,看看队列的应用:
POJ 2259 Team Queue
题目大意:n个小组进行排队,每个小组中有若干人,若一个人来到队伍中时队中已经有自己队的人,就会站在自己队的人的最后,否则则会站在整个队伍最后,给定一些入队指令和出队指令,输出出队顺序。

典型的队列问题…简单分析可以发现,在任何一个时刻,同一个小组的人一定会站在一起,所以相对可以确定的是所有小组所组成的“队列”,而这个队列中的每个元素就是小组,每个小组中又有若干人,他们又形成一个队列,也就是这是一个队列套队列的问题,大队列中的每个元素又分别是一个小队列。加入时,假如这一组存在在大队列中,直接加在这个小队列末尾,否则在大队列末尾加上一个小队列,里面包含这唯一的一个人。
出队时,直接将大队列中队头的小队列的队头出队,如果此时小队列已经空了,那么直接将这个小队列出队,标记为不存在。

有时做数据结构题的时候,用一个单独的数据结构不足以解决问题,就要考虑用多个相同或不同的结构共同解决。

再看一道实际的应用问题:
Luogu 1996 约瑟夫问题
题目大意:n个人站成一圈,顺时针1-n编号,从1开始顺时针报数,报到m的人离开,从他的下一个人报1继续游戏,请确定离开的顺序。

这题有多种解法,由于题目数据很水,暴力模拟也可以过。但是实际上相对比较好的有两种解法:队列法和双向链表法,这里讲前一种。(都可以参照洛谷题解)

我们将所有还在圈子里的人组成一个队列,按顺时针的顺序排好,并且队头就是下一个报数的人,同时记录一个变量 c n t cnt cnt,表示当前的人应该报哪个数字。
如果一个人应该报的是1到m-1之间的数,那么他不必出队,所以还会轮到他的,将他放到队尾去,表示当前队列里的人报完了又会到他,同时将 c n t + + cnt++ cnt++
如果一个人应该报到m,那么直接将他出队,并且将 c n t cnt cnt清为1。

来分析一下这个算法的时间复杂度。总共进行n轮,每轮报m个数,在这种解法里不会出现队头人不报数的情况,因此时间复杂度就是 O ( m n ) O(mn) O(mn)

由此可见,队列能解决的问题不只局限于线性问题和规定了两端出入的问题,这样的环形问题有时也可用队列解决。

双端队列

听着比较厉害的一种队列,但是实际上也蛮简单的…一般的队列和栈,无论是出还是入,都有唯一的方向。但是有时我们既需要在队列的队头增减元素,也需要在队尾增减元素,这时就不是一般的队列了,这种特殊的队列就是双端队列。

双端队列的实现和队列非常相似。按照上面的模拟方法,在队尾删除只需将队尾指针左移一格,而在队头加入可以将队头指针左移,如果超出了数组下标,那么我们只需要将最前面的元素的前一个元素定义成最后一个元素,类似于循环队列的思想,就可以很好的模拟双端队列了。常用的双端队列的应用是单调队列和deque优化的bfs。

最后值得一提的是,队列还在广度优先搜索(BFS)算法中有非常重要的作用,是这种算法的核心。

在C++ STL中,队列和双端队列都可以直接调用,分别在头文件<queue>和<deque>中,其中queue包含pushpop等常用功能,而deque还可以以下标方式进行随机访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值