You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

720 lines
19 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 银行业务模拟系统
成员:费良荣、冯国平、武杭凯、李聪颖
**摘要**:本项目针对银行业务系统运行的问题,实现客户和窗口的功能、管理客户、队列的功能和计算一天中客户在银行逗留的平均时间的功能。为了有效地存储和处理银行的开门时间、关门时间、营业窗口数目、银行营业的工作记录、客户的到达时间、离开时间等数据,采用了线性表、队列等数据结构和排序、随机数等数据结构知识。
项目开发过程中采用 Kanban看板进行任务管理和分工协作并使用 Git 对程序代码和文档进行版本管理。任务分工情况如下:
| 任务 | 设计 | 开发 | 测试 |
| ---- | ---- | ---- | ---- |
| C1 银行业务模拟,统计一天内客户在银行逗留的平均时间 | 费良荣 | 冯国平 | 武杭凯 |
| C2 比较事件发生先后 | 冯国平 | 李聪颖 | 费良荣 |
| C3 银行开门 | 李聪颖 | 武杭凯 | 冯国平 |
| C4 插入事件 | 武杭凯 | 冯国平 | 李聪颖 |
| C5 客户进门 | 费良荣 | 费良荣 | 李聪颖 |
| C6 客户离开 | 冯国平 | 武杭凯 | 费良荣 |
| C7 求长度最短队列 | 李聪颖 | 冯国平 | 武杭凯 |
| C8 链表初始化 | 费良荣 | 费良荣 | 冯国平 |
| C9 在第i个位置之前插入元素e | 武杭凯 | 李聪颖 | 费良荣 |
| C10 判断链表是否为空 | 冯国平 | 武杭凯 | 李聪颖 |
| C11 删除链表中第一个结点并以q返回 | 费良荣 | 李聪颖 | 冯国平 |
| C12 返回链表头结点 | 武杭凯 | 费良荣 | 费良荣 |
| C13 已知p指向线性链表中的一个结点返回p所指结点中元素的值 | 费良荣 | 冯国平 | 李聪颖 |
| C14 打印事件链表 | 武杭凯 | 费良荣 | 冯国平 |
| C15 遍历链表  | 李聪颖 | 武杭凯 | 费良荣 |
| C16 链队列的初始化 | 费良荣 | 冯国平 | 李聪颖 |
| C17 入队 | 费良荣 | 武杭凯 | 冯国平 |
| C18 出队 | 李聪颖 | 费良荣 | 武杭凯 |
| C19 返回队列的长度 | 冯国平 | 李聪颖 | 武杭凯 |
| C20 获取队头元素 | 冯国平 | 武杭凯 | 李聪颖 |
| C21 判断队列是否为空 | 武杭凯 | 冯国平 | 李聪颖 |
| C22 打印队列 | 李聪颖 | 武杭凯 | 冯国平 |
| C23 遍历队列Q  | 费良荣 | 费良荣 | 李聪颖 |
工作量占比:
| 费良荣 | 冯国平 | 李聪颖 | 武杭凯 |
| ---- | ---- | ---- | ---- |
| 25 | 25 | 25 | 25 |
# 1. 系统分析
## 1.1 问题描述
设计一个银行业务模拟系统,模拟银行的业务运行并计算一天中客户在银行逗留的平均时间。银行有 $N$ 个窗口对外接待客户,从早晨银行开门起不断有客户进入银行。由于每个窗口在某个时刻只能接待一个客户,因此在客户人数多时需分别在各个窗口前排队,对于刚进入银行的客户,如果某个窗口的业务员正在空闲,则可上前办理业务;反之,若 $N$ 个窗口均有客户正在办理业务,新来的客户便会排在人数最少的队伍后面。
1通过人机交互的方式设定程序所需的参数银行的开门时间和关门时间营业窗口数目。
2客户的到达时间可通过人机交互、文件导入或随机生成的方式输入。
3保存银行营业的工作记录存储客户的到达时间、离开时间。
4显示出在某一天整个银行系统中客户在银行逗留的平均时间。
要求系统运行正常、功能完整;数据结构使用得当,算法有较高的效率;代码规范、可读性高,结构清晰;具备一定的健壮性、可靠性和可维护性。
## 1.2 可行性分析
解决此问题的核心数据结构是有序链表和队列
客户排队的过程是一个按照到达时间先到先接受服务的过程,这一过程可以通过队列实现。$N$ 个不同的窗口对应 $N$ 个队列,队列中每一个元素对应一个客户。
算法中处理的事件有两类:一类是客户到来事件,另一类是客户离开事件。客户到来事件发生的时刻随客户到来自然形成,客户到来后,使用排序算法找到最短的等待队列,将客户插入队列;客户离开事件发生的时刻由银行窗口为其办理业务的完成时间决定,业务办理完成后,客户从等待队列中出队,窗口为下一个客户(如果存在的话)服务。由于程序驱动是按事件发生时刻的先后顺序进行,则事件表应是有序表,其主要操作是插入和删除事件。
## 1.3 需求分析
### 1输入和输出
主要输入:客户到达时间、客户离开时间(主要作用:确定某个客户的逗留时间)
某天客户的数量
银行的开门时间、银行的关门时间、营业窗口数目
主要输出:某一天整个银行系统中客户在银行逗留的平均时间
系统每次随机生成的是
1当前顾客顾客的柜台被服务时间durtime
2当前顾客和下一个顾客到达的间隔时间intertime
### 2数据字典
某个客户逗留时间=客户离开时间-客户到达时间
假设当前事件发生的时刻为occurtime,则下一个客户到达事件发生的时刻为occurtime+intertime
注:每个顾客在银行的等待时间取决于队列里前一个节点的离开时间,而不是自己的到达时间+服务时间。
### 3数据文件
1通过人机交互的方式设定程序所需的参数银行的开门时间和关门时间营业窗口数目。
2系统每次随机生成的是
当前顾客顾客的柜台被服务时间durtime
当前顾客和下一个顾客到达的间隔时间intertime
3客户的到达时间可通过人机交互、文件导入或随机生成的方式输入。
### 4参数设定
通过人机交互的方式设定程序所需的参数:银行的开门时间和关门时间,营业窗口数目。
客户的到达时间可通过人机交互、文件导入或随机生成的方式输入。
### 5管理客户、队列的功能
管理客户、队列的功能
主要作用:类似银行进门处的接待员,引导客户在哪里排队
# 2. 系统设计
## 2.1 概要设计
大体分为三大模块:
(1)客户
void Bank_Simulation(int CloseTime);//银行业务模拟,统计一天内客户在银行逗留的平均时间
int cmp(Event a, Event b);//比较事件发生先后
void OpenForDay();//银行开门
void OrderInsert(EventList L, Event en, int(*cmp)(Event a, Event b));//插入事件
void CustomerArrived();//客户进门
void CustomerDepature();//客户离开
int Minimum(LinkQueue Q[5]);//求长度最短队列
(2)链表
Status InitList(LinkList& L);//链表初始化
Status ListInsert_L(LinkList& L, int i, ElemType e);//在第i个位置之前插入元素e
Status ListEmpty(LinkList L);//判断链表是否为空
Status DelFirst(LinkList L, LNode*& q);//删除链表中第一个结点并以q返回
LNode* GetHead(LinkList L);//返回链表头结点
ElemType GetCurElem(LNode* p);//已知p指向线性链表中的一个结点返回p所指结点中元素的值
void PrintEventList();//打印事件链表
Status ListTraverse(LinkList& L);//遍历链表 
(3)链队列
Status InitQueue(LinkQueue& Q);//链队列的初始化
Status EnQueue(LinkQueue& Q, QElemType e);//入队
Status DeQueue(LinkQueue& Q, QElemType& e);//出队
int QueueLength(LinkQueue Q);//返回队列的长度
Status GetHead(LinkQueue Q, QElemType& e);//获取队头元素 注:由于参数个数不同,发生函数重载
Status QueueEmpty(LinkQueue Q);//判断队列是否为空
void PrintQueue();//打印队列
Status QueueTraverse(LinkQueue Q);//遍历队列Q 
## 2.2 数据结构设计
采用了链表和链队列
### 1链表结构
Status InitList(LinkList& L);//链表初始化
Status ListInsert_L(LinkList& L, int i, ElemType e);//在第i个位置之前插入元素e
Status ListEmpty(LinkList L);//判断链表是否为空
Status DelFirst(LinkList L, LNode*& q);//删除链表中第一个结点并以q返回
LNode* GetHead(LinkList L);//返回链表头结点
ElemType GetCurElem(LNode* p);//已知p指向线性链表中的一个结点返回p所指结点中元素的值
void PrintEventList();//打印事件链表
Status ListTraverse(LinkList& L);//遍历链表 
### 2链队列结构
Status InitQueue(LinkQueue& Q);//链队列的初始化
Status EnQueue(LinkQueue& Q, QElemType e);//入队
Status DeQueue(LinkQueue& Q, QElemType& e);//出队
int QueueLength(LinkQueue Q);//返回队列的长度
Status GetHead(LinkQueue Q, QElemType& e);//获取队头元素 注:由于参数个数不同,发生函数重载
Status QueueEmpty(LinkQueue Q);//判断队列是否为空
void PrintQueue();//打印队列
Status QueueTraverse(LinkQueue Q);//遍历队列Q 
###
## 2.3 算法设计
### 1链表初始化算法
链表初始化
{
L = (LinkList)malloc(sizeof(LNode));
if (!L){
exit(OVERFLOW);}
L->next = NULL;
return OK;
}
### 2链表判空算法
判断链表是否为空
{
if (L->next)
{return FALSE;}
else{
return TRUE;
}
}
### (3) 打印事件链表算法
{ 
printf("Current Eventlist is:\n");
ListTraverse(ev);
}
### (4) 遍历链表算法
{
LNode* p = L->next;
if (!p) {
printf("List is empty.\n");
return ERROR;
}
while (p != NULL) {
printf("OccurTime:%d,Event Type:%d\n", p->data.OccurTime, p->data.NType);
p = p->next;
}
printf("\n");
return OK;
}
### (5) 链队列初始化算法
{
Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
if (!Q.front)
{exit(OVERFLOW);}
Q.front->next = NULL;
return OK;
}
### (6) 队列判空算法
{
if (Q.front == Q.rear)
{
return TRUE;
}
return FALSE;
}
### (7) 打印队列算法
{
int i;
for (i = 1; i <= 4; i++) {
printf("窗口 %d 有 %d 个客户:", i, QueueLength(q[i]));
QueueTraverse(q[i]);
}
printf("\n");
}
### (8) 遍历队列算法
{
QNode* p = Q.front->next;
if (!p) {
printf("--Is empty.\n");
return ERROR;
}
while (p) {
printf("(到达时刻 %d min 办理业务需要花费 %d min) ", p->data.ArrivalTime, p->data.Duration);
p = p->next;
}
printf("\n");
return OK;
}
# 3. 系统实现
项目采用 C 语言编程实现,在 VS Code 集成开发环境IDE中用 GCC 进行编译。系统采用模块化设计,程序结构清晰
void Bank_Simulation(int CloseTime);//银行业务模拟,统计一天内客户在银行逗留的平均时间
int cmp(Event a, Event b);//比较事件发生先后
void OpenForDay();//银行开门
void OrderInsert(EventList L, Event en, int(*cmp)(Event a, Event b));//插入事件
void CustomerArrived();//客户进门
void CustomerDepature();//客户离开
int Minimum(LinkQueue Q[5]);//求长度最短队列
Status InitList(LinkList& L);//链表初始化
Status ListInsert_L(LinkList& L, int i, ElemType e);//在第i个位置之前插入元素e
Status ListEmpty(LinkList L);//判断链表是否为空
Status DelFirst(LinkList L, LNode*& q);//删除链表中第一个结点并以q返回
LNode* GetHead(LinkList L);//返回链表头结点
ElemType GetCurElem(LNode* p);//已知p指向线性链表中的一个结点返回p所指结点中元素的值
void PrintEventList();//打印事件链表
Status ListTraverse(LinkList& L);//遍历链表 
Status InitQueue(LinkQueue& Q);//链队列的初始化
Status EnQueue(LinkQueue& Q, QElemType e);//入队
Status DeQueue(LinkQueue& Q, QElemType& e);//出队
int QueueLength(LinkQueue Q);//返回队列的长度
Status GetHead(LinkQueue Q, QElemType& e);//获取队头元素 注:由于参数个数不同,发生函数重载
Status QueueEmpty(LinkQueue Q);//判断队列是否为空
void PrintQueue();//打印队列
Status QueueTraverse(LinkQueue Q);//遍历队列Q 
## 3.1 核心数据结构的实现
主要为有序链表和队列两种数据类型
配合程序代码加以说明。如下:
```cpp
typedef struct
{
int OccurTime;//事件发生时刻
int NType;//事件类型0表示到达事件1-4表示四个窗口的离开事件
}Event, ElemType;
typedef struct LNode
{
ElemType data;
struct LNode* next;
}LNode, * LinkList;
typedef LinkList EventList;
typedef struct
{
int ArrivalTime;//到达时刻
int Duration;//办理事务所需事件
}QElemType;
typedef struct QNode
{
QElemType data;
struct QNode* next;
}QNode, * QueuePtr;
typedef struct
{
QueuePtr front;//队头指针
QueuePtr rear;//队尾指针
}LinkQueue;
EventList ev;//事件表
Event en;//事件
LinkQueue q[5];//四个客户队列
QElemType customer;//客户记录
int TotalTime, CustomerNum, CloseTime;
```
## 3.2 核心算法的实现
程序代码加以说明。如下:
```cpp
void Bank_Simulation(int CloseTime)//银行业务模拟,统计一天内客户在银行逗留的平均时间
{
OpenForDay();//开始营业
LNode* p;
while (!ListEmpty(ev))
{
DelFirst(GetHead(ev), p);
printf("********action********\n");
en = GetCurElem(p);
if (en.NType == 0)
{
CustomerArrived();
}
else
{
CustomerDepature();
}
PrintQueue();
PrintEventList();
}
printf("The Average Time is %f\n", (float)TotalTime / CustomerNum);
}
int cmp(Event a, Event b)//比较事件发生先后
{
if (a.OccurTime > b.OccurTime) return 1;
if (a.OccurTime = b.OccurTime) return 0;
if (a.OccurTime < b.OccurTime) return -1;
}
void OpenForDay()//银行开门
//初始化操作
{
TotalTime = 0;//初始化累计时间为0
CustomerNum = 0;//初始化客户数为0
InitList(ev);//初始化事件链表为空表
en.OccurTime = 0;
en.NType = 0;//设定第一个客户到达事件
OrderInsert(ev, en, cmp);
for (int i = 1; i <= 4; i++)
{
InitQueue(q[i]);//将四个银行窗口队列初始化
}
}
void OrderInsert(EventList L, Event en, int(*cmp)(Event a, Event b))//插入事件
//事件插入函数,将不同事件按发生时间递增排序
{
LNode* p = L;
int i = 1;
while (p->next && cmp(en, p->next->data) > 0)//找到事件发生时间所在事件链表中的位置
{
p = p->next;
i++;
}
ListInsert_L(ev, i, en);//插入该事件
}
void CustomerArrived()//客户进门
//处理客户到达事件en.NType=0
{
CustomerNum++;
int durtime = rand() % 30 + 1;//客户处理事务时间
int intertime = rand() % 8;//下一个客户到达的时间间隔
int t = en.OccurTime + intertime;//下一个客户到达的时刻
if (t < CloseTime)//如果他在营业时间内进来
{
printf("一个新客户在银行营业%2dmin后进来办理业务花费了%2dmin下一个客户过了%2dmin后进来\n", en.OccurTime, durtime, intertime);
OrderInsert(ev, { t, 0 }, cmp);//插入客户进门事件NType=0为到达事件
}
int i = Minimum(q);//客户找最短队开始排队
EnQueue(q[i], { en.OccurTime, durtime });
if (QueueLength(q[i]) == 1)
{
OrderInsert(ev, { en.OccurTime + durtime,i }, cmp);//队列长度为1时设定一个离开事件
}
}
void CustomerDepature()//客户离开
{
int i = en.NType;
DeQueue(q[i], customer);//删除第i队列的排头客户
TotalTime += en.OccurTime - customer.ArrivalTime;//累计客户逗留时间
if (!QueueEmpty(q[i])) {
GetHead(q[i], customer);
OrderInsert(ev, { en.OccurTime + customer.Duration, i }, cmp);//插入事件
}
}
int Minimum(LinkQueue Q[5])//求长度最短队列
{
int minLength = QueueLength(Q[1]);
int i = 1;
for (int j = 2; j < 5; j++)
{
if (minLength > QueueLength(Q[j]))
{
minLength = QueueLength(Q[j]);
i = j;
}
}
return i;
}
Status InitList(LinkList& L)//链表初始化
{
L = (LinkList)malloc(sizeof(LNode));
if (!L)
{
exit(OVERFLOW);
}
L->next = NULL;
return OK;
}
Status ListInsert_L(LinkList& L, int i, ElemType e)//在第i个位置之前插入元素e
{
LinkList p = L;
int j = 0;
while (p && j < i - 1)//注意是i-1,因为要找被插入元素的前一个元素
{
p = p->next;
j++;
}
if (!p || j > i - 1)
{
return ERROR;
}
LinkList s = (LinkList)malloc(sizeof(LNode));
if (!s)
{
exit(OVERFLOW);
}
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
Status ListEmpty(LinkList L)//判断链表是否为空
//空表头指针和头结点仍然存在但头结点指向NULL
{
if (L->next)
{
return FALSE;
}
else
{
return TRUE;
}
}
Status DelFirst(LinkList L, LNode*& q)//删除链表中第一个结点并以q返回
{
if (!L->next)
{
return ERROR;
}
q = L->next;
L->next = q->next;
return OK;
}
LNode* GetHead(LinkList L)//返回链表头结点
{
return L;
}
ElemType GetCurElem(LNode* p)//已知p指向线性链表中的一个结点返回p所指结点中元素的值
{
return p->data;
}
void PrintEventList()//打印事件链表 
{ 
printf("Current Eventlist is:\n");
ListTraverse(ev);
}
Status ListTraverse(LinkList& L) //遍历链表  
{
LNode* p = L->next;
if (!p) {
printf("List is empty.\n");
return ERROR;
}
while (p != NULL) {
printf("OccurTime:%d,Event Type:%d\n", p->data.OccurTime, p->data.NType);
p = p->next;
}
printf("\n");
return OK;
}
Status InitQueue(LinkQueue& Q)//链队列的初始化
{
Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
if (!Q.front)
{
exit(OVERFLOW);
}
Q.front->next = NULL;
return OK;
}
Status EnQueue(LinkQueue& Q, QElemType e)//入队
{
QNode* p = (QueuePtr)malloc(sizeof(QNode));
if (!p)
{
exit(OVERFLOW);
}
p->data = e;
p->next = NULL;
Q.rear->next = p;
Q.rear = p;
return OK;
}
Status DeQueue(LinkQueue& Q, QElemType& e)//出队
{
if (Q.front == Q.rear)
{
return ERROR;
}
QNode* p = Q.front->next;
e = p->data;
Q.front->next = p->next;
if (Q.rear == p)//注意这里要考虑到,当队列中最后一个元素被删后,队列尾指针也丢失了,因此需对队尾指针重新复制(指向头结点)
{
Q.rear = Q.front;
}
free(p);
return OK;
}
int QueueLength(LinkQueue Q)//返回队列的长度
{
int count = 0;
QNode* p = Q.front->next;
while (p) {
p = p->next;
count++;
}
return count;
}
Status GetHead(LinkQueue Q, QElemType& e)//获取队头元素
{
if (Q.front == Q.rear)
{
return ERROR;
}
e = Q.front->next->data;
}
Status QueueEmpty(LinkQueue Q)//判断队列是否为空
{
if (Q.front == Q.rear)
{
return TRUE;
}
return FALSE;
}
void PrintQueue()//打印队列
{
//打印当前队列  
int i;
for (i = 1; i <= 4; i++) {
printf("窗口 %d 有 %d 个客户:", i, QueueLength(q[i]));
QueueTraverse(q[i]);
}
printf("\n");
}
Status QueueTraverse(LinkQueue Q)//遍历队列Q  
{
QNode* p = Q.front->next;
if (!p) {
printf("--Is empty.\n");
return ERROR;
}
while (p) {
printf("(到达时刻 %d min 办理业务需要花费 %d min) ", p->data.ArrivalTime, p->data.Duration);
p = p->next;
}
printf("\n");
return OK;
}
```
# 4. 系统测试
![测试图](cheshi.svg)
# 5. 总结
本次项目综合运用线性表、队列、排序、随机数等数据结构知识,模拟银行业务系统的运行情况,掌握和提高分析、设计、实现及测试程序的综合能力。
个人小结:
成员1(费良荣)
成员2(冯国平)
成员3(李聪颖):
成员4(武杭凯)
# 参考文献
[1] 严蔚敏, 吴伟民. 数据结构C语言版. 北京: 清华大学出版社, 2007.
[2] 作者:回到唐朝当少爷 https://www.bilibili.com/read/cv15955830 出处bilibili