一.问题描述
假设某银行有四个窗口对外接待客户,从早晨银行开门起不断有客户进入银行。由于每个窗口只能接待一个客户,因此在客户人数众多时需在每个窗口前顺次排队,对于刚进入银行的客户,如果某个窗口的业务员正在空闲,则可上前办理业务,反之,若四个窗口均有客户所占,他便会排在队伍的后面。现在需要编写一个程序以模拟银行的这种业务活动,并计算一天中客户在银行逗留的平均时间。
为了计算这个平均值,我们要掌握每个客户到达银行和离开银行这两个时间,后者减去前者即为每个客户在银行的逗留事件。所有客户逗留时间的总和被一天内进入银行的客户数除便是所求的平均时间。
假设客户到达后即可办理业务,则他在银行的逗留时间即为他办理业务所需的时间;否则需加上他排队等候的时间。
二.数据结构分析
下面我们来讨论模拟程序所需的数据结构。
显然,需要四个队列以表示四个窗口前的客户队列,队列中每个元素标识排队等候的客户,队尾元素为最迟进入银行的客户,而队头元素则表示正被银行业务员接待的客户。
只有下列五种情况发生会促使队列发生变化:一种情况是新的客户进入银行,他将加入元素最少的队列而成为该队列新的队尾元素;另四种情况是某个正被业务员接待的客户办理完业务离开银行。
整个模拟程序就是按时间先后顺序一个接一个处理这些事件。这样一种模拟程序称作事件驱动模拟。
假设事件表中最早发生的事件是新客户到达,则随之应得到两个时间:
一是本客户办理业务所需时间;二是下一个客户将到达银行的时间间隔。
此时模拟程序应做的工作是:
1)比较四个队列中的个数,将新到客户加入到元素个数最少的队列中成为新的队尾元素。若该队列原为空,则刚插入的队尾元素也是队头元素,此时应设定一个事件——刚进入银行的客户办理完业务离开银行的事件,并插入事件表;
2)设定一个新的事件——下一个客户即将到达银行的事件,插入事件表;
若最早发生的事件是某个队列中的客户离开银行,则模拟程序需要做的工作是:
1)该客户出队列,并计算他在银行逗留时间;
2)当该队列非空时,计算新的队头元素将离开银行的时间,并由此设立一个新的离开事件,并插入事件表。
三.存储结构的实现
3.1 具体分析
由于队列中的最大长度无法预测,而且长度变化较大,故采用单链表做存储结构为宜。
每个结点表示一个排队等待的客户,Client它包含两个数据域:arrival_time 和 duration(分别表示客户到达银行的时间和办理业务所需时间)。
同时,为查找方便,设定LinkQueue队列类,包含三个属性front、rear和length(分别指示队头、队尾和队列中元素个数),设置队列对象数组,包含四个队列,每个队列反映0-3号窗口的排队情况。
3.2 代码实现
//定义银行客户的信息内容
typedef struct client{
int arrival_time;// 客户到达银行的时间
int duration;// 办理业务所需时间
}Client;
//队列节点类型
typedef struct node{
Client data;
struct node*next;
}Node;
class LinkQueue{
private:
Node *front,*rear;
int length;
public:
LinkQueue();
~LinkQueue();
void enQueue(Client c);
bool deQueue(Client *item);
bool getFront(Client *item);
bool is_Empty();
void clear_queue();
void display_Queue();
int queue_length();
};
LinkQueue::LinkQueue(){
front=new Node;
front->next=NULL;
rear=front;
length=0;
}
LinkQueue::~LinkQueue(){
clear_queue();
delete front;
}
void LinkQueue::enQueue(Client c){
Node *p=new Node;
p->data=c;
p->next=NULL;
rear->next=p;
rear=p;
length++;
}
bool LinkQueue::deQueue(Client *item){
if(is_Empty()){
return false;
}
else{
Node *p=front->next;
*item=p->data;
front->next=p->next;
delete p;
length--;
return true;
}
}
bool LinkQueue::getFront(Client *item){
if(is_Empty()){
return false;
}
else{
Node *p=front->next;
*item=p->data;
return true;
}
}
bool LinkQueue::is_Empty(){
if(length==0){
return true;
}
else{
return false;
}
}
void LinkQueue::clear_queue(){
Node *p=front->next;
for(int i=0;i<length;i++){
front->next=p->next;
delete p;
p=front->next;
}
rear=front;//rear 指向front
length=0;
}
void LinkQueue::display_Queue(){
Node *p=front->next;
for(int i=0;i<length;i++){
cout<<"Arrival time:"<<p->data.arrival_time<<"Duration:"<<p->data.duration<<endl;
p=p->next;
}
}
int LinkQueue::queue_length(){
return length;
}
3.3 事件表的结构
由于事件表需按事件发生的先后顺序排列,需经常进行插入动作,则也采用单链表做存储结构。
每个结点包含两个数据域:current_time和nType(分别表示事件发生的时间和事件的类型:-1表示新用户到来,0-3表示有客户离开1-4个窗口)。
我们需要定义一个链表处理类,来完成银行事件的处理流程,这个链表处理类是一个有序链表,最多5个结点,分别对应新到达客户,和四个不同的窗口队列的离开事件。
3.4 代码实现
typedef struct evnode{
int current_time;//事件发生的时间
int nType;//事件类型,-1表示到达事件,0-3表示四个窗口的离开事件
struct evnode *next;
}evNode;
class EventList{//事件列表中最多只有5个节点
private:
evNode *head;
public:
EventList();
~EventList();
bool is_Empty();
void add_Node(evNode event);//按照current_time从小到大排序
bool delete_Node(evNode *first_event);
void display_Node();
};
EventList::EventList(){
head=new evNode;
head->next=NULL;
}
EventList::~EventList(){
evNode *p=head->next;
while(p){
delete head;
head=p;
p=p->next;
}
delete head;
}
bool EventList::is_Empty(){
if(head->next==NULL){
return true;
}
else{
return false;
}
}
void EventList::add_Node(evNode event){//按照current_time从小到大排序
evNode *p=new evNode,*q=head->next;
*p=event;
while(q&&q->next->current_time<=event.current_time){
q=q->next;
}
p->next=q->next;
q->next=p;
}
bool EventList::delete_Node(evNode *first_event){
if(head->next){
evNode *p=head->next;
*first_event=*p;
head->next=p->next;
delete p;
return true;
}
else{
return false;
}
}
void EventList::display_Node(){
evNode *p=head->next;
while(p){
cout<<"current time:"<<p->current_time<<"type:"<<p->nType<<endl;
p=p->next;
}
}
四.银行业务模拟
1.
定义两个变量,记录客户总人数和总服务时长,后续通过这两个变量计算出平均服务时长。
2.
定义队列数组queue,下标0至3分别代表4个排队窗口队列
3.
建立事件链表对象,设定第一个客户到达的事件
其实为了驱动后续循环进行
4.
进入循环,开始扫描并处理事件列表
5.
将事件分成两类,一类是客户到来,一类是客户离开。
6.
如果是客户到来事件的话
1)客户人数加一
2)定义两个变量
durTime 和interTime
durTime代表当前进入银行的客户所需要银行职员为他服务的时间,interTime代表下一个客户到来的间隔时间
随机生成这两个时间
3)如果下一个客户到来的时间没有到银行的关门时间
设定下一位客户到来的事件并插入事件表中
4)
为当前客户找到排队人数最少的队列下标
int findMin(LinkQueue queue[],int n){
int i,min=0,a[100];
for(i=0;i<n;i++){
a[i]=queue[i].queue_length();
}
for(i=0;i<n;i++){
if(a[i]<a[min]){
min=i;
}
}
return min;
}
当前客户进入人数最少的队列
5)
如果当前客户到达的窗口没有人在排队(他是第一个客户)
设定第一个客户离开银行的事件,插入事件表
7.如果是客户离开事件
1)获得客户所在的窗口号(队列号)
2)将当前要离开的客户从队列中删除,并将当前客户信息存入client
3)计算客户在银行停留时间,把当前客户的服务时间累加到total_time中
4)如果还有人在排队 ,将此客户离开的事件插入到事件列表中
代码:
double silulation(){
//种随机数种子
srand((unsigned)time(NULL));
int total_time=0;
int customer_num=0;
//定义队列数组
LinkQueue queue[4];
//建立事件链表对象
EventList evList;
//定义客户对象
Client client;
//初始化事件列表
evNode evItem;
evItem={0,-1,NULL};//到达时间是0,事件类型为客户到达
evList.add_Node(evItem);//驱动循环开始
while(!evList.is_Empty()){//事件列表不为空
evList.delete_Node(&evItem);//取出事件
if(evItem.nType==-1){//新客户到达事件
customer_num++;//客户人数加一
evNode evTemp;
int durTime=rand()%50,interTime=rand()%20;
//durTime代表当前客户需要银行为他服务的时间,interTime代表下一个客户到来的间隔时间
if(evItem.current_time+interTime<CLOSE_TIME){//下一个客户到来的时间没有到银行的关门时间
//设定下一位客户到来的事件并插入事件表中
evTemp={evItem.current_time+interTime,-1,NULL};
evList.add_Node(evTemp);
}
//为当前客户找到排队人数最少的队列下标
int min=findMin(queue, 4);
//当前客户进入人数最少的队列
client.duration=durTime;
client.arrival_time=evItem.current_time;
queue[min].enQueue(client);//入队
//如果当前客户到达的窗口没有人在排队(他是第一个客户)
if(queue[min].queue_length()==1){
//设定第一个客户离开银行的事件,插入事件表
evTemp={evTemp.current_time+durTime,min,NULL};
evList.add_Node(evTemp);
}
}
else{//是客户离开事件
evNode evTemp;
Client client;
//获得客户所在的窗口号(队列号)
int win=evItem.nType;
//将当前要离开的客户从队列中删除,并将当前客户信息存入client
queue[win].deQueue(&client);
//计算客户在银行停留时间,把当前客户的服务时间累加到total_time中
total_time+=evItem.current_time-client.arrival_time;
if(queue[win].queue_length()!=0){//如果还有人在排队
queue[win].getFront(&client);
evTemp={evItem.current_time+client.duration,win,NULL};
evList.add_Node(evTemp);
}
}
}
return total_time*1.0/customer_num;
}
五.全部代码
注:本文章是学习bilibili懒猫老师的数据结构课程的笔记