队列应用银行排队问题模拟:计算客户的平均停留时间和等待时间以及每个客户的时间信息,两种方法实现
(上传的资源中有完整的源码哦!可以在CSDN的资源中搜索标题进行下载!)
第一种类似于买票排队,你总会到队列最短的窗口去排队,但往往会有其他队列办事速度快,队列长度很快变得比你所在队列的还短,但你改变自己的队列去当前较短的队列时,可能没过多久刚刚你在的队列又比你现在所处的队列短了,因为队短不代表等待时间短,你无法预测每个队列你需要等待的时间。所以在该种制度下,不同于买票排队的这种可以随便更换队列的随意性,我们在第一种算法中设定:每到达一个客户将其排在队列最短的队尾,且不管其它队列是否变的更短,甚至已经空闲,该客户也只能在已队列中等待前面的客户办理完业务自己才能办理业务,很明显这种算法效率不是最好的。一是时间利用率不高,而是无法保证先到达的客户的办理业务时间一定比后到达的客户早。
一、数据类型
首先需要两个数据结构:一个是有序事件链表,一个是队列。
1、事件链表
存储客户事件,包括到达事件和离开事件,其中到达事件的事件类型为0,1号窗口的离开事件类型为1,二号窗口的离开事件类型为2,三号窗口的离开事件类型为3,四号窗口的离开事件类型为4,由此就可以只使用事件类型就将到达事件和不同窗口的离开事件区分开来.
(1)链表数据结构:
1)每个事件项的数据结构为:
typedef struct{
int OccurTime;
int NType;
}Event,ElemType;
2)链表的结构:
typedef struct LinkList{
ElemType data;
struct LinkList*next;
}LinkList;
(2)链表数据操作
1)创建一个包含头结点空链表,返回指向头结点的指针
LinkList *InitList_L( );
初始化的结果为list指向头结点,list->next指向空。将list返回。
2)统计链表的元素的个数:并将统计结果返回
int CountList(LinkList *List_Head );
头结点的下一项即List_Headànext为链表的第一项,若该项为空,则链表长度为0,若该项不为空,从该项开始直达next指向空的项结束的项数的个数即为链表的长度。
3)判断链表是否为空,若为空,返回1,若不为空,返回0.
int ListEmpty(LinkList *List_Head );
头结点指向空,即List_Head指向的结点的后一项List_Head->指向空,或者使用CountList函数统计链表元素个数为0时,链表为空。
4)将元素e插入到链表中的位置i;即使e成为链表的第i个元素
intListInsert_L( LinkList *List_Head, int i, ElemType e );
首先为元素e创建一个指向链表结点指针node_i,并为该链指针分配链表类型LinkList的内存空间,将e的值赋给该指针指向的几点的数据项node_i->data:
若i为1,将node_i->next指向链表的第一项List_Head->next;将链表的头指针List_Head->next指向指针nodi_i.注意这两个表达式的顺序一定不能反过来。
node_i->next = List_Head->next;
List_Head->next = node_i;
若i不为1,找到第i- 1项,将当前链表的第i-1项的d的next的值赋给node_i的next,将node_i的值赋给第i-1项的next。操作结果是的node_i成为链表的第i项。若i值小于0若当前链表中没有第i-1项,说明要插入的位置为负数或者已经超过了链表长度+1,是一个错误的位置,此时提示错误。
5)删除链表中的第i个元素,并将该元素的值通过e输出
intListDelete_L( LinkList *List_Head, int i, ElemType *e )
若i值小于0若当前链表中没有第i项,说明要删除的元素位置在链表中不存在,是一个错误的位置,此时提示链表错误。
若i值为1,将List_Head->next指向List_Head->next->next;
若i不为1,将第i-1项的next,指向第i-1项的next项的next,并将第i项的空间释放。
6)将链表的元素按OccurTime的正序进行排列,即非递减
LinkList*Increse( LinkList *List_Head )
使用冒泡法对该单链表进行排序,需要三个指针,current指向当前项,pre指向当前项的前一项,nex指向当前项的后一项,使用冒泡法使得当前项比他之前的项都小,将当前项大于等于下一项时,交换这两项的位置。对于没有含义的链表排序时为了减少一次交换常常在此处判断条件时不使用等号,而在本程序中等号时也进行交换有特殊的含义:1当前事件的发生时间大于下一个事件的发生时间交换顺序: current->data.OccurTime > nex->data.OccurTime 2当前事件的发生时间等于下一个事件的发生时间且当前事件为到达事件时交换顺序,使得当一个离开事件和一个到达事件的时间相等时,离开事件的排序总是位于到达事件之前,使得之后离开事件先于到达事件被处理: ( current->data.OccurTime == nex->data.OccurTime &&( current->data.NType == 0 ) ) 3当两个离开事件的发生时间相等时,将队列号小的离开事件排在前面,即先处理队列号小的事件: ( current->data.OccurTime == nex->data.OccurTime &&nex->data.NType < current->data.NType && nex->data.NType !=0 )
2、窗口队列
假设有n个窗口队列,每次有客户到达时都把客户其分配到排队人数最少且窗口号较小的队列中。
(1) 队列数据结构
1)队列项的元素的数据结构
typedef struct{
int ArriveTime; //客户的到达事件
int Duration; //一个客户办理事务所需的时间
}QElemType;
2)队列链表的数据结构
typedef structQNode{
QElemType data;
struct QNode *next;
}QNode;
3)存储指向队列首尾项地址的指针:
QNode *front指向队列的头结点,QNode*front->next指向队列的第一个有效项,QNode *rear指向队列的最后一项,
typedef struct{
QNode *front;
QNode *rear;
}LinkQueue;
2、数据操作
1)构建一个包含头结点的空队列
LinkQueue *InitQueue( )
需要创建两个指针并为其分配空间:一个为LinkQueue*型,指向队列的第一项(非数据项)即存储 QNode *front和QNode *rear的项,一个为QNode*型,即为指向队列的第二项(非数据项)即头结点。初始化QNode *front和QNode *rear都指向头结点,头结点指向空。
2)判断队列是否为空
int QueueEmpty(LinkQueue *Q )
队列的头结点的Q->front->next指向队列的第一个结点,若该项为NULL;队列即为空。
3)计算队列的长度
int QueueLength(LinkQueue *Q )
即队列中包含的元素个数,不包括队列头即Q->front指向的元素,队列的第一个有效元素是Q->front->next,最后一个元素的next指向NULL
4)插入元素,由队列的性质决定应该在队尾插入元素
void InsertQueue(LinkQueue *Q, QElemType e )
5)删除元素,由队列的性质决定应该在队首删除元素
voidDeleteQueue( LinkQueue *Q, QElemType *e )
若队列非空,则进行元素的删除:1第一个结点的数据赋给e指向的单元2将队列的头结点指向第一个结点的下一个结点3释放第一个结点4如果删除队头结点后队列为空,将队尾指针指向头结点。
二、对事件链表和队列的处理
整个过程需要的变量:
Event en; //当前被处理的事件
QElemType customer; //客户在队列中的记录
int TotalTime; //累计客户逗留时间
int WaitTime; //累计客户等待时间
int CustomerNum ; //累计客户数
intCloseTime; //银行结束时间
intStartTime; //银行开始营业时间
LinkList *ev; //事件表
LinkQueue *q[5]; //4个客户队列,q[i]指向第i号窗口的队列
int leave_num= 0; //第leave_num个离开银行的客户
1、初始化:
void OpenForDay()
{
int i;
TotalTime = 0; //初始化累计时间为0
CustomerNum = 0; //初始化客户总数为0
WaitTime = 0; //初始化等待时间为0
ev = InitList_L( ); //初始化事件链表为空
en.OccurTime = 0; //设定第一个客户到达事件
en.NType = 0;
OrderInsert( ev, en ); //将第一个客户到达事件插入事件表
for( i = 1; i <= 4; i++ ) //初始化窗口队列
q[ i ] = InitQueue();
}
2、产生随机数的函数:
第一个参数代表当前客户的办理业务所需的时间,第二个参数代表下一个客户与当前客户的到达时间差。
void Random(int *duration, int *intertime );
{
srand( (unsigned)time( NULL ) ); //用时间作为种子对随机数进行操作
*duration = rand()%30 + 1; //任何一个客户的办理业务时间在1-30之间
*intertime = rand()%5+1; //任何两个客户到达的时间间隔不超过5分钟,1-5
Sleep(1000); //由于随机函数的产生机制导致在一秒以内产生的随机数都是相同的,因此在一次使用Random时需要进行延时
}
3、获得当前客户插入队列的队列号:
int Minium( int num1, int num2, int num3, int num4 );
比较四个数中的最小值,若第i个参数最小,则返回i,参数相等的情况下返回序号较小的参数的序号。调用时四个参数分别为第一个队列的长度,第二个队列的长度,第三个队列的长度,第四个队列的长度。调用结果是返回长度最短且队列号较小的队列的队列号。
4、模拟函数:初始化数据,包含主循环,打印结果
删除当前事件表中的第一个事件,并将该事件的参数赋给当前事件en:如果当前事件en为到达事件即en.NType == 0,则调用到达事件处理函数,否则当前事件为离开事件,则调用离开事件处理函数。参数CloseTime为对应的营业总时间的分钟数:即为(关门时间 – 开门时间) * 60
void Bank_Simulation( int CloseTime )
{
OpenForDay();
while(!ListEmpty( ev ) ){
ListDelete_L( ev, 1, &en );
if( en.NType== 0 ){
CustomerArrived( );
}
else
CustomerDepature();
}
printf("客户的平均停留时间是: %f minutes\n", (float) TotalTime/CustomerNum );
printf("客户的平均业务办理时间时间是: %f minutes\n", (float) TotalTime/CustomerNum -WaitTime/CustomerNum );
printf("客户的平均等待为其办理业务的时间是: %f minutes\n", (float) WaitTime/CustomerNum ); }
5、到达事件处理函数:
voidCustomerArrived( );
1)到达客户总数加1,利用随机数生成函数生成下一个到达事件,若到达事件的时间在CloseTime之前则用OrderInsert函数将其插入到事件表中。
2)生成当前到达事件的队列项customer并利用Minium函数找到合适的队列号用InsertQueue函数将其插入到队列中。
3)只有当当前到达事件位于队列的队首时,才在此函数中为其生成离开事件并用OrderInsert函数将其插入到事件表中,此时对应客户到达后无需等待即可办理业务的情况。因此对应的离开事件的发生时间为到达事件与办理业务时间之和:first_leave.OccurTime = customer.ArriveTime + customer.Duration;离开事件的类型为对应的队列号first_leave.NType = i。若当前到达事件不位于队首,则其需要等待位于他前面的最后一个客户离开后才能办理业务,其离开事件应该等于前一个客户的离开事件加上当前到达的客户办理业务的时间,因此应该在他前面的最后一个客户的离开事件处理函数中为其生成对应的离开事件。
基本思想就是只为位于队首的队列项生成离开事件。
6、离开事件处理函数:
voidCustomerDepature( )
1)利用当前事件的en.NType找到当前事件对应的队列项并将其在队列中除除并把该队列项的参数赋给customer。表示该客户已经办理完业务。
2)如果删除当前事件对应的队列项后,其所在的队列不为空,则为当前队首的队列项生成对应的离开事件。此时队首的队列项对应的离开事件的发生时间为当前离开事件的发生时间与当前位于队首的队列项的办理业务时间之和:depature.OccurTime = en.OccurTime +q[i]->front->next->data.Duration; 队首的队列项对应的离开事件的类型即为当前离开事件的事件类型。depature.NType = i。
( 与基本思想:只为位于队首的队列项生成离开事件对应)
3)离开序列号+1;
计算当前离开事件的并将其转换为24小时制:
到达时间:即为custome.ArriveTime
办理业务时间:即为customer.Duration
离开时间:即为当前事件的发生时间:en.OccurTime
停留时间:离开时间 – 到达时间,en.OccurTime--custome.ArriveTime
等待时间:停留时间 –办理业务时间en.OccurTime--custome.ArriveTime--customer.Duration
计算该离开事件发生后累计的总停留时间:
计算该离开事件发生后累计的总的等待时间
4)打印离开客户的:离开序列号,到达事件,业务窗口,办理业务时间,离开事件,停留时间,等待时间
7、将产生的事件插入按事件发生时间的顺序插入到事件表中
voidOrderInsert( LinkList *eventlist, Event cur_en )
{
if(ListEmpty( eventlist) ){
ListInsert_L(eventlist, 1, cur_en );
}
else{
ListInsert_L(eventlist, 1, cur_en );
Increse( eventlist );
}
}
当事件链表为空时,将第一个事件直接插入到第一个位置,当事件链表不为空时,先将事件插入到事件表的第一个位置然后将事件表按非递减的顺序排序。
三、处理过程
1、初始状态
事件表只有一项{0,0}队列为空。
2、第一步:处理第一个客户到达事件
(1)将第一个客户的到达事件从事件表中删除,事件表为空
(2)生成第二个客户到达事件,并将此事件有序插入到事件表中
(3)生成第一个客户到达事件的队列项,并为其分配队列窗口1,此时为其分配的队列窗口长度为1,即第一个客户到达之后无序等待即可办理业务,故
(4)为第一个客户生成离开事件,离开事件的发生时间即为到达事件的发生时间与半夜业务时间之和。将第一个客户的离开事件有序插入到事件表中。
此时,事件表中有两项,一项为第一个客户的离开事件,一项为第二个客户的到达事件。事件表中项数最多的情况是只有5项:一个到达事件项和四个离开事件项,且这四个离开事件对应的到达事件的发生事件在该到达事件项的发生时间之前。
3、第二步:
(1)第一个客户的离开事件位于事件表的第一项
(1)将第一个客户的离开事件从事件表中删除并将事件参数赋给当前事件en,利用第一个客户的离开事件的en.NType找到其对应的队列项1并将其在队列中删除并把该队列项的参数赋给customer。表示第一个客户已经办理完业务。此时第一个队列为空。没有需要生成的离开事件。
3)离开序列号+1;
计算当前离开事件的并将其转换为24小时制:
到达时间:即为custome.ArriveTime
办理业务时间:即为customer.Duration
离开时间:即为当前事件的发生时间:en.OccurTime
停留时间:离开时间 – 到达时间,en.OccurTime--custome.ArriveTime
等待时间:停留时间 –办理业务时间en.OccurTime--custome.ArriveTime--customer.Duration
计算该离开事件发生后累计的总停留时间:
计算该离开事件发生后累计的总的等待时间
4)打印第一个客户离开的:
离开序列号,到达时间,业务窗口,办理业务时间,离开时间, 停留时间, 等待时间
1 arr_h:arr_m 1 dur_m en.NType stop wait
(2)第二个客户的到达事件位于事件表的第一项
(1)将第二个客户的到达事件从事件表中删除,事件表为空
(2)生成第三个客户到达事件,并将此事件有序插入到事件表中
(3)生成第二个客户到达事件的队列项,并为其分配队列窗口2,此时为其分配的队列窗口长度为1,即第一个客户到达之后无序等待即可办理业务,故
(4)为第二个客户生成离开事件,离开事件的发生时间即为到达事件的发生时间与半夜业务时间之和。将第二个客户的离开事件有序插入到事件表中。
此时,事件表中有三项,一项为第一个客户的离开事件,一项为第二个客户的离开事件,一项为第三个客户的到达事件。
4、后续
执行过程与第二步相似:但值得一提的是事件表中项数最多的情况是只有5项:一个到达事件项和四个离开事件项,且这四个离开事件对应的到达事件的发生事件在该到达事件项的发生时间之前。因为此时四个离开事件对应的队列项已经占据了各个队列项的队首。
(1)此时若队首为客户到达事件时:
在事件表中删除该事件并将该事件的参数赋给当前事件en。用到达事件处理函数对en进行处理,在处理时只进行以下操作:
1)利用随机数生成函数生成下一个到达事件并用OrderInsert函数将其有序插入到事件表中。
2)生成当前到达事件的队列项customer并利用Minium函数找到合适的队列号用InsertQueue函数将其插入到队列中。
(2)此时若队首为客户离开事件时:
在事件表中删除该离开事件并将该事件的参数赋给当前事件e。用离开事件处理函数对en进行处理,在处理时进行以下操作:
1)利用当前事件的en.NType找到当前事件对应的队列项并将其在队列中除除并把该队列项的参数赋给customer。表示该客户已经办理完业务。
2)如果删除当前事件对应的队列项后,
1若其所在的队列不为空,则为当前队首的队列项生成对应的离开事件。
2若若其所在的队列为空,则没有需要生成的离开事件。
3)离开序列号+1;
计算当前离开事件的并将其转换为24小时制:
到达时间:即为custome.ArriveTime
办理业务时间:即为customer.Duration
离开时间:即为当前事件的发生时间:en.OccurTime
停留时间:离开时间 – 到达时间,en.OccurTime--custome.ArriveTime
等待时间:停留时间 –办理业务时间en.OccurTime--custome.ArriveTime--customer.Duration
计算该离开事件发生后累计的总停留时间:
计算该离开事件发生后累计的总的等待时间
4)打印离开客户的:离开序列号,到达事件,业务窗口,办理业务时间,离开事件,停留时间,等待时间
5、运行结果实例:
四、改进算法
此种算法对应的情况是:每次客户一到达,就为其分配队伍最短的窗口号,此时该客户只能在该窗口办理业务,但队列最短并不一定等待时间最短,因为可能该队列前面的客户办理业务所需的时间都很长,而其他队列虽然长,但办理业务的时间短,有可能其它窗口已经空闲了,但该客户也只能在该窗口等待为其办理业务。这显然不是最优的算法。造成这种现象的原因就是为在客户一到达时就为其分配窗口。更优的算法应该是现在银行的排队机制。每个窗口只有一个客户在办理业务,前1,2,3,4个到达的客户在办理业务,后面到达的客户先不为其分配窗口,所有的人都在等待区等待,当某一个窗口的客户离开时,将等待区的最先到达的客户分配到该窗口去办理业务。此时就需要两个事件表:一个到达事件表,一个离开事件表。
(红色加粗字体的地方为与之前的算法不同的地方)
1、数据项
Event en; //事件
QElemType customer; //当前事件对应的队列项,客户记录
int TotalTime; //累计客户逗留时间
int WaitTime; //累计客户等待时间
int CustomerNum ; //累计客户数
int CloseTime;//营业结束时间
int StartTime;//营业开始时间
LinkList *ev_arrive; //到达事件表,比之前的算法多一个到达事件表
LinkList *ev_leave; //离开事件表
LinkQueue *q[5]; //4个客户队列,q[i]指向第i号窗口的队列
int leave_num =0; //第leave_num个离开银行的客户
Event newestleave_en; //最新删除的离开事件项
2、初始化
void OpenForDay()
{
int i;
TotalTime = 0; //初始化累计时间为0
CustomerNum = 0; //初始化客户总数为0
WaitTime = 0; //初始等待时间为0
ev_arrive= InitList_L( ); //初始化到达事件链表为空
ev_leave = InitList_L( ); //初始化离开事件链表为空
en.OccurTime = 0; //设定第一个客户到达事件
en.NType = 0; //0表示到达事件
OrderInsert( ev_arrive, en ); //将第一个客户到达事件插入事件表
for( i = 1; i <= 4; i++ ) //初始化窗口队列
q[ i ] = InitQueue();
}
3、随机数生成函数:
分别生成到达客户的时间间隔和业务办理时间
voidRandom_duration( int *duration )
{
srand( (unsigned)time( NULL ) ); //用时间作为种子对随机数进行操作
*duration = rand()%30 + 1; //任何一个客户的办理业务时间在1-30之间
Sleep(1000);
}
voidRandom_intertime( int *intertime )
{
srand( (unsigned)time( NULL ) ); //用时间作为种子对随机数进行操作
*intertime = rand()%5+1; //客户到达的时间间隔不超过5分钟,1-5
Sleep(1000);
}
4、将事件cur_en有序的插入事件链表eventlist中
voidOrderInsert( LinkList *eventlist, Event cur_en )
{
if( ListEmpty( eventlist) ){
ListInsert_L( eventlist, 1, cur_en );
}
else{
ListInsert_L( eventlist, 1, cur_en);
Increse( eventlist );
}
}
当事件表为空时,将第一个事件直接插入到第一个位置,当事件表不为空时,先将事件插入到事件表的第一个位置然后将事件表按非递减的顺序排序。
将离开事件插入到离开事件表时,若两个离开事件的发生时间相等,应该将对应窗口号较小的离开事件排在前面。因此Increase函数中判断两个事件是否需要交换顺序的条件应为:
current->data.OccurTime> nex->data.OccurTime || ( current->data.OccurTime ==nex->data.OccurTime && current->data.NType >nex->data.NType))
5、模拟函数:
voidBank_Simulation( int CloseTime )
(1)对各项数据进行初始化
(2)当离开事件表和到达事件表都不为空时结束循环,否则
(3)当离开事件表为空时,则对到达事件列表的第一个到达事件进行处理:删除到达事件表的第一项,并将该项的参数赋给当前事件en,调用到达时间函数对en进行处理。
(只有第一个到达事件到达时或者离开事件表的所有离开事件的发生时间都小于到达事件表的第一个到达事件的到达时间时,离开事件表才为空)
(4)当到达事件表为空时,说明已经到了银行的最晚营业时间,没有客户再到达了,此时需要对离开事件表进行处理。删除当前离开事件表的第一项,并将对应的参数赋给当前事件en和最近离开事件,调用离开事件处理函数对en进行处理。
(5)当两个事件表都不为空时:
1)若离开事件表的第一项的发生事件小于等于到达事件的第一项的发生时间,则删除当前离开事件表的第一项,并将对应的参数赋给当前事件en和最近离开事件,调用离开事件处理函数对en进行处理。
2)若离开事件表的第一项的发生事件大于到达事件的第一项的发生时间:
1如果当前离开事件表的项数大于等:于4,说明没有空闲的窗口,需要先对离开事件表进行处理以便得到空闲的窗口分配给到达事件表的第一项。删除当前离开事件表的第一项,并将对应的参数赋给当前事件en和最近离开事件,调用离开事件处理函数对en进行处理。
2如果当前离开事件表的项数小于4,说明有空闲窗口,此时才可以对到达事件的第一项进行处理,除当前到达事件表中的第一项并将删除结果赋给当前事件项en,调用到达时间处理函数。
(6)循环结束后打印最终结果
注:在上述几步条件判断的过程中使得窗口有空闲时才对到达事件进行处理并为其分配窗口和产生离开事件。若窗口没有空闲即使到达事件的第一项的发生事件小于离开事件的第一项的发生时间也需要先对离开事件进行处理。因此在处理到达事件时也就代表窗口有空闲。
5、到达事件处理函数:
(1)到达客户总数加1,
(2)利用随机数生成函数生成下个客户的到达事件与当前客户的到达事件的时间间隔。生成下个客户的到达事件:下个客户的到达时间为:next.OccurTime = en.OccurTime + intertime; 事件类型为next.NType= 0。若到达事件的时间在CloseTime之前则用OrderInsert函数将其插入到达事件表。
(3)分配窗口号,此时必有空闲的窗口:因为只有当有空闲窗口时才会调用到达事件处理函数。
1)计算当前客户的队列项并将队列项分配到对应空闲的窗口队列
2)比较1)中为当前客户分配的窗口号与最近离开的客户的窗口号进行比较:若相等说明处最近离开的客户办理业务的窗口外其他窗口都没有空闲,若不等说明其他窗口有空闲。
1若相等当前客户的到达时间小于最近离开的客户的离开时间,则当前客户的离开事件的发生时间为:最近离开的客户的离开事件与当前客户办理业务所需时间之和。
2若相等且当前客户的到达时间大于最近离开的呼呼的离开事件,则当前客户的离开事件的发生时间为:当前客户的到达时间与办理业务时间之和。
3若不等则当前客户的离开事件的发生时间为:当前客户的到达时间与办理业务时间之和。
4当前客户的离开事件的事件类型等于为其分配的窗口号。将当前客户的离开事件有序的插入到离开事件表中。
6、离开事件处理函数
voidCustomerDepature( )
(1)删除当前离开事件对应的队列项,将该队列项删除并赋值给customer
(2)离开序列号加1
(3)计算离开事件的各项数据并打印
当前离开事件对应的到达时间即为队列项customer.ArrivrTime
当前离开客户的业务办理时间即为对应的队列项customer.Duration
当前离开客户的离开时间即为当前离开事件的en的发生时间en.OccurTime
当前离开客户的停留时间即为离开事件的发生时间减去到达时间:
( en.OccurTime - customer.ArriveTime )
当前离开的客户等待为其办理业务的时间即为停留时间减去办理业务的时间:en.OccurTime - customer.ArriveTime - customer.Duration
(4)计算当前客户离开时客户停留的总时间和客户等待为其办理业务的总时间。
7、运行结果:
平均等待时间和平均停留时间减少了20分钟左右。