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
22 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