目录
问题描述:
假设某银行有四个窗口对外接待客户,从银行开门到关门之前起不断有客户进入银行。由于每个窗口只能接待一个客户,因此在客户人数众多时需在每个窗口前顺次排队,对于刚进入银行的客户,如果某个窗口的业务员正在空闲,则可上前办理业务,反之,若四个窗口均有客户所占,他便会排在队伍最短的队伍后面。假设事件表中最早发生的事件是新客户到达,则随之应得到两个时间(均由随机生成数函数得到):(1)是本客户办理业务所需时间(2)是下一个客户将到达银行的时间间隔。现在,需要编制一个程序以模拟银行的这种业务活动并求出一天中客户在银行逗留的平均时间。
解题思路:
(以下是听完懒猫老师的《数据结构》相关课程后的一些自己的代码实现和相关笔记(我用的C语言),老师讲的真的超级细致!)
(1)数据结构
1.银行窗口
共四个银行窗口,并且由客户的先来后到进行排队,且满足先进先出的原则,所以这里要采用四个队列来进行存储。但是由于当天进入到银行的客户人数不确定,循环队列大小是固定的,无法满足这一需求,所以这里要采用链式队列,每个结点代表一个在队列中的客户,客户和链表的结构体如下:
typedef struct client {
int arrivalTime;//客户达到银行的时间
int duration;//办理业务所需时间
} Client;
typedef Client QElemType;
//定义队列结点类型,即待执行指令
typedef struct QNode {
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
//对队列进行基本操作的指针
typedef struct {
QueuePtr front;//头指针
QueuePtr rear;//尾指针
} LinkQueue;
链表中的数据域数据结构类型是定义的Clinet,分别包含客户达到银行的时间和办理业务所需时间,这两者相加得到的就是这位顾客离开的时间。
队列中每个元素标识排队等侯的客户,队尾元素为最迟进入银行的客户,而队头元素则表示正被银行业务员接待的客户。只有下列五种情况发生会促使队列发生变化:(1)一种情况是新的客户进入银行,他将加入元素最少的队列而成为该队列新的队尾元素;(2)另四种情况是某个正被业务员接待的客户办理完业务离开银行。
2.事件列表
因为银行处理事件的原则是时间的前后,所以需要建立一个事件链表,根据事件的执行时间先后来进行排序储存,结构定义如下:
//定义事件链表的结点类型
typedef struct evnode {
int occurTime;//事件发生时间
int nType;//事件类型,-1代表到达事件,0-3代表四个窗口离开事件
struct evnode *next;//指针域
} evNode, *evPtr;
包含时间开始发生的时间和事件的四种类型。
(2)功能实现
1)比较四个队列中的个数,将新到客户加入到元素个数最少的队列中成为新的队尾元素。若该队列原为空,则刚插入的队尾元素也是队头元素,此时应设定一个事件——刚进入银行的客户办理完业务离开银行的事件,并插入事件表;(因为队头元素可以通过客户结点的 达到银行的时间+办理业务所需时间=离开时间,得到这位客户离开事件中所需的时间部分;而如果不是队头元素,不知道其他队列中加上本队列中有多少事件要在前面执行,所以暂时还不能添加离开事件,等排到了就能添加了);
2)设定一个新的事件,如果下一个客户即将到达银行的事件,就插入事件表;若最早发生的事件是某个队列中的客户离开银行,则模拟程序需要做的工作是:
1.该客户出队列,并计算他在银行逗留时间;
2.当该队列非空时,计算新的队头元素将离开银行的时间,并由此设立一个新的离开事件,并插入事件表。
3)银行逗留的平均时间=所有客户在银行逗留时间的和/客户人数
具体流程可以参考下下图:(也来自懒猫老师喔!)
(3)随机数生成
哈哈我以前没有用过就专门记录一下!
这里用的是void rand(void)函数,头文件是“stdlib.h”。rand()会返回一随机数值,范围在0至RAND_MAX间,RAND_MAX定义在stdlib.h中。 在调用此函数产生随机数前,必须先利用srand()设好随机数种子,如果未设随机数种子,rand()在调用时会自动设随机数种子为1,一般用for语句来设置种子的个数。例如下面是0-100的随机数。
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int i;
printf("Ten random numbers from 0 to 99\n\n");
for(i=0; i<10; i++)
printf("%d\n", rand() % 100);
return 0;
}
而在本题中,我们需要自定义随机数范围,即从X到Y,有Y-X+1个数,所以要产生从X到Y的数,要写成:
rand()%(Y-X+1)+X
以下是我自己设定的事件,也可以进行更改
1)是本客户办理业务所需时间:0-30min
2)是下一个客户将到达银行的时间间隔:0-10min
3)银行从开门(即接待第一位客户起算)到关门时间:40min
随机数生成函数:
int random1() {//生成上一个客户需求的处理时间
int n;
srand( (unsigned)time( NULL ) ); //初始化随机数
n = rand() % 30;//生成0-30之间的随机数
// printf("%d", n);
return n;
}
int random2() {//生成下一个客户到来的间隔时间
int n;
srand( (unsigned)time( NULL ) ); //初始化随机数
n = rand() % 10;//生成0-10之间的随机数
// printf("%d", n);
return n;
}
完整代码:
(这里分成三个包,分别是链式队列结构定义和基本操作包,名:链队列.h;事件链表结构定义和基本操作包,名:链表操作.h;还有银行排队算法的主要实现函数包,名:银行排队算法.c,三个包互相调用就能实现完整代码)
(1)链队列.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct client {
int arrivalTime;//客户达到银行的时间
int duration;//办理业务所需时间
} Client;
typedef Client QElemType;
//定义队列结点类型,即待执行指令
typedef struct QNode {
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
typedef struct {
QueuePtr front;//头指针
QueuePtr rear;//尾指针
} LinkQueue;
int InitQueue(LinkQueue *Q) {
QueuePtr node;
node = (QueuePtr)malloc(sizeof(QNode));
if (node != NULL) {
node->next = NULL; //创建头指针
Q->front = Q->rear = node;
return 1;
} else
return 0;
}
void DestroyQueue(LinkQueue *Q) {
QueuePtr temp;
while (Q->front != Q->rear) {
temp = Q->front->next;
free(Q->front);
Q->front = temp;
}
free(Q->rear);
}
void ClearQueue(LinkQueue *Q) { //保留队列头指针和队列尾指针,其他结点销毁
QueuePtr p, q;
Q->rear = Q->front; //将尾指针归位到头指针
q = Q->front->next; //队列头
while (q != NULL) {
p = q->next;
free(q);
q = p;
}
Q->front->next = NULL;//天哪一定要记得把头节点next域设为NULL,不然还是被释放了的q,就乱指了
}
int QueueEmpty(LinkQueue Q) { //是否为空,为空返回,非空返回0
if (Q.front->next == NULL)
return 1;
else
return 0;
}
int QueueLength(LinkQueue Q) { //返回队列长度
QueuePtr p = Q.front->next;
if (p == NULL) {//刚初始化完的队列长度为0
return 0;
}
int count = 1;
while (p != Q.rear) {
count++;
p = p->next;
}
return count;
}
QElemType GetHead(LinkQueue Q) { //若队列非空,返回队列首的值
if (QueueEmpty(Q) != 1)
return Q.front->next->data;
else
printf("队列为空!\n");
}
void EnQueue(LinkQueue *Q, QElemType e) { //将e元素插入队列
QueuePtr node;
node = (QueuePtr)malloc(sizeof(QNode));
if (node != NULL) {
node->next = NULL;
node->data = e;
Q->rear->next = node;
Q->rear = node;
} else
printf("EnQueue error!\n");
}
QElemType DeQueue(LinkQueue *Q) { //删除队头元素,并返回其值
QElemType e;
QueuePtr q;
if (QueueEmpty(*Q) != 1) {
q = Q->front->next;
e = Q->front->next->data;
Q->front->next = q->next;
if (Q->rear == q)//队头等于队尾,防止尾指针丢失
Q->rear = Q->front;
free(q);
return e;
} else
printf("队列为空!\n");
}
void QueueTraverse(LinkQueue Q) { //从队列头到尾遍历队列元素并输出
QueuePtr p = Q.front->next;
// printf("从队列头到尾遍历队列元素并输出:\n");
if (QueueEmpty(Q) == 1) {
printf("队列为空!\n");
return;
}
while (p != NULL) {
if (p != Q.rear)
printf("(%d %d) ", p->data.arrivalTime, p->data.duration);
else
printf("(%d %d)\n", p->data.arrivalTime, p->data.duration);
p = p->next;
}
}
(2)链表操作.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义事件链表的结点类型
typedef struct evnode {
int occurTime;//事件发生时间
int nType;//事件类型,-1代表到达事件,0-3代表四个窗口离开事件
struct evnode *next;//指针域
} evNode, *evPtr;
//evPtr head;
//这是一个有头节点的链表
evPtr EventList(evPtr head) { //初始化链表
head = (evPtr)malloc(sizeof(evNode));
if (head != NULL) {
head->next = NULL;
return head;
} else {
printf("Init error!\n");
}
}
void DestoryEventList(evPtr head) { //销毁链表
evPtr p = head->next;
while (p != NULL) {
free(head);
head = p;
p = p->next;
}
free(head);
}
int isEmpty(evPtr head) { //判断链表是否为空
if (head->next == NULL || head == NULL)
return 1;
else
return 0;
}
void addNode(evPtr head, int occurTime, int nType) { //添加结点,遍历,时间从小到大排列
evPtr node, p, q ;
int temp;
node = (evPtr)malloc(sizeof(evNode));
node->next = NULL;
node->occurTime = occurTime;
node->nType = nType;
if (head->next == NULL) {
head->next = node;
} else {
p = head->next;
q = head;
while (p != NULL) {
if (occurTime < p->occurTime) { //插入结点
node->next = q->next;
q->next = node;
return;
} else {
q = p;
p = p->next;
}
}//遍历完也没有插进去,就添加到末尾
q->next = node;
}
}
void deleteNode(evPtr head) { //在已经排列好顺序的链表里,只用删首任务就行
evPtr Q;
Q = head->next;
head->next = Q->next;
free(Q);
}
void display(evPtr head) {
if (isEmpty(head) == 0) {
evPtr Q;
Q = head->next;
while (Q != NULL) {
if (Q->next == NULL)
printf("(%d,%d)\n", Q->nType, Q->occurTime);
else
printf("(%d,%d) ", Q->nType, Q->occurTime);
Q = Q->next;
}
} else {
printf("链表为空!\n");
}
}
(3)银行排队算法.c
#include "链队列.h"
#include "链表操作.h"
#include <time.h>
#define CLOSE_TIME 40
int random1();
int random2();
int findMin(LinkQueue *);
double simulation() {
double totalTime = 0;
double customerNum = 0; //客户人数
int durTime, interTime;
//定义队列数组queue,下标0至3分别代表4个排队窗口队列
LinkQueue queue[4];
for (int i = 0; i < 4; i++)
InitQueue(&queue[i]);//四个银行窗口初始化
//建立事件链表对象,设定第一个客户到达的事件
evPtr evlist;
evlist = EventList(evlist);
//初始化事件列表,加入第一个结点,起始时间为0,第一个事件为-1(新客户到达事件)
evNode evItem;
evItem.nType = -1;
evItem.occurTime = 0;
evItem.next = NULL;
addNode(evlist, 0, -1);
evPtr head = evlist;
int count = 1, count1 = 1;
printf("初始化事件列表为:");
display(head);
//存储生成的随机数据方便查看
//扫描并处理事件列表
while (isEmpty(head) != 1) { //事件链表非空
//删除事件链表中第一个结点存入evItem
printf("------------------第%d次循环------------------\n", count1++);
evlist = head->next;
evItem.nType = evlist->nType;
evItem.occurTime = evlist->occurTime;
printf("显示当前事件表:");
display(head);
printf("删除事件链表中第一个结点并存入evItem,值为[%d,%d]\n", evItem.nType, evItem.occurTime);
deleteNode(head);
//总共只有两大类事件,客户到达和客户离开
if (evItem.nType == -1) {
/* --是新客户到达事件-- */
// 客户人数加1
customerNum++;
// 输入随机数durTime 和interTime,(durTime代表当前客户服务时间,
// interTimed代表下一个客户到来的间隔时间)
// printf("是新客户到达事件,请输入durTime,interTime:");
// scanf("%d %d", &durTime, &interTime);
durTime = random1();
interTime = random2();
printf("第%d组随机数据为:(服务时间:%d,间隔时间:%d)\n", count++, durTime, interTime);
if (evItem.occurTime + interTime < CLOSE_TIME) { //下一个客户来时银行还没关门
//而已经来的顾客无论需要处理多久都会把任务处理完
//设定下一位客户到达的事件插入事件表
evItem.nType = -1;
evItem.occurTime = evItem.occurTime + interTime;
evItem.next = NULL;
printf("把下一位客户到达的事件插入事件表后的新表:");
addNode(head, evItem.occurTime, evItem.nType);
display(head);
evItem.occurTime -= interTime;//后面入队列时还要用,就把值还原回去
}
//寻找最短队列
int di;
di = findMin(queue);
printf("新客户进入%d号窗口\n", di + 1);
//将当前正在处理的到来事件放入最短排队窗口队列
Client current;
current.arrivalTime = evItem.occurTime;
current.duration = durTime;
EnQueue(&queue[di], current);
//如果当前客户到达的窗口没有人在排队(他是第一个客户)
//就可以将本客户离开的事件进行预测,并加入事件链表
if (QueueLength(queue[di]) == 1) {
printf("本客户是%d号窗口的队首客户,添加离开事件\n", di + 1);
evItem.nType = di;
evItem.occurTime = evItem.occurTime + durTime;
evItem.next = NULL;
addNode(head, evItem.occurTime, evItem.nType);
printf("把队首客户离开的事件插入事件表后的新表:");
display(head);
}
} else {
/* --是客户离开事件-- */
//获得客户所在的窗口号(队列号)
//服务总时间累加
Client del;
del = DeQueue(&queue[evItem.nType]);
totalTime += del.duration;
printf("是老客户离开事件,将要离开的客户是%d窗口的(%d,%d)\n", evItem.nType + 1, del.arrivalTime, del.duration);
//如果还有人在排队,则把队头客户的离开事件插入事件表
if (QueueLength(queue[evItem.nType]) != 0) { //相当于后面排队的人向前走一步
printf("还有人在排队,往前走一步并添加离开事件\n");
evItem.nType = evItem.nType;
evItem.occurTime += del.arrivalTime + del.duration;
evItem.next = NULL;
addNode(head, evItem.occurTime, evItem.nType);
printf("把队首客户离开的事件插入事件表后的新表:");
display(head);
}
}
printf("四个窗口的排队现状:\n");
for (int i = 0; i < 4; i++) {
printf("%d号窗口:", i + 1);
QueueTraverse(queue[i]);
}
}
return totalTime / customerNum;
}
int findMin(LinkQueue *queue) {
int minlen = QueueLength(queue[0]);
int di = 0; //保存最短队伍窗口编号
for (int i = 1; i < 4; i++) {
if (QueueLength(queue[i]) < minlen) {
di = i;
minlen = QueueLength(queue[i]);
}
}
return di;
}
int random1() {//生成上一个客户需求的处理时间
int n;
srand( (unsigned)time( NULL ) ); //初始化随机数
n = rand() % 30;//生成0-30之间的随机数
// printf("%d", n);
return n;
}
int random2() {//生成下一个客户到来的间隔时间
int n;
srand( (unsigned)time( NULL ) ); //初始化随机数
n = rand() % 10;//生成0-10之间的随机数
// printf("%d", n);
return n;
}
int main() {
printf("银行排队算法测试:\n");
double percent;
percent = simulation();
printf("通过计算本次离散随机事件,得银行处理个人业务的平均时间为:%0.4f分钟\n", percent);
}
测试输出:
因为题目要求时间是随机生成的,不好核对答案,这里只需要把我上面银行排队算法.c包的生成随机数语句注释掉,并且取消掉上面的手动输入时间的注释,就可以核对下面这一组答案啦!
(一下这组数据和参考答案的数据均来自懒猫老师的课程)
输入:
分别输入时间(durTime,interTime)代替随机数:
(23 4),(3 1),(11 3),(29 2),(18 4),(13 5),(20 30)
输出:
初学小白,有错误欢迎指正喔!^-^