数据结构中还有个比较常用的就是队列,相比较于之前一篇介绍的栈的“后入先出”的特点,队列的特点就是“先进先出”
类比于栈,其实很容易就能知道队列的“先进先出”特点的具体意义,不过下面我还是会用图片的形式形象介绍下~在介绍之前,首先要知道队列的几个比较重要的方法,我们利用enqueue(x)向队列队尾添加元素x,dequeue()则是弹出队首元素,同时类似于栈的top指针,我们还使用head指针指向队首元素,tail指向队尾元素的后一个位置(这样指定的原因是为了进行队空的判断)。
1.队列初始状态,head=tail,这时队空 | 2.enqueue(3)向队尾添加元素,tail++,head不变 |
3.enqueue(7)向队尾添加元素,tail++,head不变 | 4.enqueue(8)向队尾添加元素,tail++,head不变 |
5.dequeue(),从队首弹出元素,head++,tail不变 | 6.enqueue(1)向队尾添加元素,tail++,head不变 |
7.dequeue(),从队首弹出元素,head++,tail不变 | 8.dequeue(),从队首弹出元素,head++,tail不变 |
这里我们只进行了队空的判断,即head=tail,至于队满,我们只需要判断tail指针,比如这里我们如果保证队列中最多只有7个元素,即元素下标最大为6,则此时tail=7,则表示队满。因此队满的判断是不一定的,是根据你分配的控件大小决定的。
小结下:
在一般队列中,我们使用head=tail判定队空
tai=指定的下标判定队满
此时队列中实际的元素个数=tail-head
其实通过上面的图示介绍,大家可能会发现一个问题,那就是,随着元素的插入和删除的进行,整个队列想数组中下标比较大的位置移动过去,从而就产生了队列的"单向移动性"。当元素被插入到队列中下标最大的位置上之后,队列的空间就被用光了,即便这时队列下标较小处还有空闲空间。这种现象就被称为队列的“假溢出”。
如果想要解决这个问题,有两个方法,其中一个方法就是每次执行dequeue()删除队首元素后,让队列中的所有元素整体向下标较小的一端移动。这时一个比较容易想到的方法,却又是比较复杂的方法,其时间复杂度为O(n)。
其实如果想解决这个方法,我们还有一种方法,那就是利用head,tail指针,因为每次插入和删除元素,我们都是通过这两个指针,所以每当head和tail移动到下标最大的地方时,就回头重新指向队首,也就是下标最小的地方,这样我们就形成了一种特殊的数据结构-----循环队列。循环队列的主要实现方式就是通过指针的循环,当指针达到最大下标时,从头开始指向队首,具体的插入和删除的操作我就不使用图示来演示了,其基本原理和一般的队列是一致的,都是满足“先进先出”的原则。
但是循环队列的一个注意点就是队空和队满的判断,我们还是规定head指针指向队首元素,tail指向队尾元素的后一个位置,现在我们考虑下面的两种情况:
1.队列中只有一个空闲位置(队满的临界情况) 图1.1 | 此时执行入队操作 图1.2 |
我们可以看到head=tail,此时队满~
2.队列中只有一个元素(队空的临界情况) 图2.1 | 此时执行出队操作 图2.2 |
这时我们可以看到head和tail仍然是相等的。
如果是这样的我们又怎么区分循环队列的队空和队满情况?
大家还记得我们上面提到过得利用tail的指向判断队满的情况吗,这里我们就利用tail,并且规定队列中最多只能存放queue_size-1个元素,其中queue_size是原本分配的空间个数,那么这样的话,我们图1.1中就是队满的情况,此时队空我们就仍然可以使用head=tail来进行判断。
这样的话我们通过浪费一个分配空间的方法实现了循环队列的队空和队满的判断。
小结下:
在循环队列中,我们使用head=tail判定队空
head=(tail+1)/queue_size判定队满
此时队列中实际的元素个数=(tail-head+queue_size)/queue_size
现在以一个例子作为这篇博文的结束:
现有名称为name[i]且处理事件为time[i]的n个任务按顺序排成一列,CPU通过循环调度法逐一处理这些任务,每个任务最多处理q ms(这个时间称为时间片)。如果q ms之后任务尚未处理完毕,那么该任务将被移动至队列最末尾,CPU开始执行下一个任务
输入: 输入形式如下
name[1] time[1]
name[2] time[2]
......
name[n] time[n]
第一行输入表示任务数的整数n与表示时间片的整数q,用1个空格隔开。
接下来n行输入个任务的信息。字符串name[i]与time[i]用一个空格隔开。
输出:按照任务完成的先后顺序输出各任务名以及各结束时间,任务名与对应结束时间用空格隔开,每一对任务名与结束时间占一行。
限制:1<=n<=100000
1<=q<=1000
1<=time[i]<=50000
1<=字符串name[i]的长度<=10
1<=name[i]的和<=1000000
输入示例: 输出示例:
5 100 p2 180
p1 150 p5 400
p2 80 p1 450
p3 200 p3 550
p4 350 p4 800
p5 20
分析:其实这是最简单的队列问题,每次从队首取出进程进行执行,在时间片内未执行完则放在队尾。当队列为空则执行结束~
#include<iostream>
#include<string>
#include<queue>
#include<algorithm>
using namespace std;
int main()
{
int n,q,t;
string name;
queue<pair<string,int> > Q; //注意这里使用pair后面两个> >要用空格隔开
cin>>n>>q;
for(int i=0;i<n;i++)
{
cin>>name>>t;
Q.push(make_pair(name,t)); //Q.push(pair(name,t));
}
pair<string ,int> u;
int elaps=0,a; //elaps记录当前已经使用的时间
while(!Q.empty())
{
u=Q.front();Q.pop();
a=min(u.second,q); //u.second取得pair对的第二个值,也就是时间,取两者小的值,作为本次时间片运行的时间
u.second-=a;
elaps+=a;
if(u.second>0) //进程u还未运行完,就添加到队尾
Q.push(u);
else
cout<<u.first<<" "<<elaps<<endl;
}
return 0;
}
PS:本来打算今天不写博客回老家,可是我妈太能磨蹭了,于是到下午才走。于是想到昨天看的队列的知识,就写下这篇博客~不说了