☰
银行排队系统实验报告:数组加链队模拟全天排队
2026/10/7 11:02:17 网站建设 项目流程

简介:银行排队系统实验报告是一份面向数据结构课程设计与C语言编程实践的完整项目资料,适合计算机专业学生、课程设计参与者以及希望掌握队列实际应用的开发者学习。报告以模拟n个窗口的银行排队业务为切入点,完整覆盖顾客到达、顾客离开、业务办理查询、排队状态查看及系统信息查询等核心功能,并通过VIP与普通客户分流、随机到达/离开事件生成、平均逗留时间动态统计,将队列、链表、时间函数等知识点融入真实业务场景。资源包含1个doc文档,压缩包整体仅191KB,文档内除实验目的、设计要求、设计思路外,还给出了主函数菜单、VIP验证、入队出队等关键模块的源程序与运行界面,结构完整可直接参考复现。该资料已有901人学习,既能作为课程实验报告撰写模板,也可作为C语言队列编程的练习素材和答辩讲解蓝本。

1. 银行排队系统实验报告:一个数组加一条链队,把一整天的排队算清楚

银行排队系统实验报告,是数据结构课程设计里最常被翻牌子的题目之一:n 个窗口对外接待业务,客户从开门起源源不断地进来,人一多就得排队,程序要把一整天的排队活动模拟出来,最后算出客户在银行逗留的平均时间。这份报告用 C 语言把问题拆成了两个结构——正在办理业务的客户放进数组,排队等候的客户放进链队,再用一个 switch 菜单把到达、离开、查询、评分串起来。适合谁?正在做课程设计、代码跑不通或跑通了却讲不清设计思路的人。读完这篇,你能把全部函数复现出来,也知道答辩时老师最容易往哪个函数上追问。

2. 队列加数组的“双轨”设计:n 个窗口用数组,排队客户用链队

第一次拿到这个题目时,最简单的想法是把窗口和队列都用一个数组装。但很快会撞上两个问题:队列长度不定,数组定大了浪费,定小了溢出;客户离开之后,如果要把后面的人往前挪,数组元素要整体移动,效率难看。所以这套代码采用了“双轨”结构:窗口用线性表数组,队列用链队。

2.1 两个核心结构:A 数组管窗口,Q 链队管等候

#define n 3 // 线性表:1~n 号窗口,元素值为 0 表示当前窗口空闲 struct List { int A[n + 1]; // 窗口数组 int len; // 当前正在办理业务的客户数 } L; // 链表结点类型 struct Lnode { int data; Lnode *next; }; // 链式存储的等候队列 struct Linkqueue { Lnode *front; // 头结点指针 Lnode *rear; // 队尾指针 } Q;

窗口数组 A[1] 到 A[n] 分别对应 1 号到 n 号窗口,A[0] 特意空出来不参与业务,防止编号和下标混淆。len 表示正在办理业务的客户数,也就是当前被占用的窗口数。队列用带头结点的链式实现,front 指向头结点,rear 指向真正的队尾元素,空队判断条件是 front == rear。

选链队而不是顺序队,理由是排队人数在营业日内不可预知,链队每个节点都是运行时 malloc 出来的,队列多长都不会出现顺序队那种“队满”的情况。加上出队操作只需要改指针、free 一个节点,不需要像顺序队那样搬动元素。严蔚敏《数据结构(C 语言版)》里也讲了队列的链式存储是为“长度不确定”的场景准备的,这个案例用得很贴。

2.2 初始化函数:先把表和队列拉起来

void Initshuzu() { for (int i = 1; i <= n; i++) L.A[i] = 0; // 窗口置为空闲 L.len = 0; } void Initqueue() { Q.front = Q.rear = (Lnode *)malloc(sizeof(Lnode)); Q.front->next = NULL; }

Initshuzu 只负责窗口数组,len 初始为 0,表示开门时没有一个客户在办理。Initqueue 先动态分配一个头结点,让 front 和 rear 都指向它,头结点的 data 不存业务数据,只起锚点作用。有一个细节:Initqueue 里 malloc 的头结点,在整个程序运行期间都不释放,是故意留着的。

2.3 到达事件 daoda():有空窗口直接进,没空窗口进队

void daoda(int x) { int i = L.len + 1; if (L.len < n) { // 还有空闲窗口,直接占用 L.A[i] = x; i++; L.len++; } else { // 所有窗口都忙,进入排队队列 Enqueue(&Q, x); } }

x 是客户业务号,从 1 开始递增。为什么新客户放在 L.len + 1 的位置?因为数组是按顺序从 1 号位往后放的,当前有 len 个客户在办理,他们占据 A[1] 到 A[len],新客户自然要占 A[len + 1]。入口处判断 L.len < n,其实就是在问“还有空窗口吗”。有就直接办理,没有就进队排队。这个函数是整份报告的枢纽,后续所有离开、补位逻辑都围绕它转。

2.4 Enqueue 进队算法:尾插法新建节点

void Enqueue(Linkqueue *Q, int elem) { Lnode *s; s = (Lnode *)malloc(sizeof(Lnode)); // 新建节点 s->data = elem; s->next = NULL; Q->rear->next = s; // 挂到当前队尾后面 Q->rear = s; // rear 指向新队尾 }

这是标准的链队尾插法。参数 Q 用指针传递,因为函数内部要修改 Q->rear。elem 是要进队的客户编号。有一个容易忽略的点:malloc 之后没有判断返回值,如果内存不足 s 会是 NULL,下一行直接写 s->data 就会段错误。课程设计里问题不大,但要答得出“实际项目中应该判空”。

2.5 Dlqueue 出队算法:判空、摘头、free

int Dlqueue(Linkqueue *Q) { Lnode *t; int x; if (Q->front == Q->rear) { printf("队列为空!\n"); exit(1); } else { t = Q->front->next; // t 是真正的队头 Q->front->next = t->next; // 头结点跳过队头 x = t->data; free(t); // 释放队头节点 return x; } }

出队时先判断空队,空队直接 exit(1) 终止程序,这是课程设计里常见的“简单粗暴”写法。拿到队头节点后,通过修改头结点 next 指针把节点摘出来,读数据,再 free。注意 free 之后只是释放了堆内存,函数返回的 x 是数据副本,不受影响。

3. 到达与离开两条主流程:菜单按下后,数据这样流动

数据结构定义好了,接下来看主程序怎么把这些函数串起来。主函数的骨架是一个 while(1) 死循环套 switch,菜单有五到六个入口。这里直接看关键分支。

3.1 主函数 switch 骨架:五个菜单入口,一个隐藏通道

int main() { int c, x, v = 0, w = 0; Initshuzu(); Initqueue(); while (1) { printf("1 顾客到达\n"); printf("2 顾客离开\n"); printf("3 查看业务办理\n"); printf("4 查看排队情况\n"); printf("5 退出\n"); scanf("%d", &c); switch (c) { case 1: /* 顾客到达,见 3.2 */ case 2: /* 顾客离开,见 3.3 */ case 3: /* 查看业务办理 */ case 4: /* 查看排队情况 */ case 5: return 0; case 6: /* 系统查询,菜单里不显示 */ default: printf("输入有误!请重新输入:\n"); } } }

注意原代码是 void main(),在 VC6.0 里能被接受,但如果老师要求标准 C,int main() 更稳妥。菜单上只印 1 到 5,第 6 项“系统查询”故意不显示,给工作人员用。整体是一个典型的状态机:用户输数字,switch 分发,一个循环管全天。

3.2 顾客到达流程:普通客户直接取号,VIP 先过认证

case 1: { system("cls"); printf("请选择你的用户类型:VIP 用户请按 1;普通卡用户请按 2.\n"); int a; scanf("%d", &a); if (a == 1) { vip(a); // VIP 认证 getch(); } else { v++; // 业务号累加 printf("尊敬的普通卡用户,你的业务号为%d.\n", v); daoda(v); // 交给到达处理函数 } system("cls"); break; }

v 是普通客户计数器,每来一个普通客户就自增,业务号从 1 开始连续编号。这里的“VIP 通道”在代码里只做到了认证和提示,没有真正把 VIP 客户写进窗口数组或队列,属于演示性质。如果老师在答辩时问“VIP 优先级怎么体现”,你要能说清楚:目前只是 vip1 计数加提示语,真正要支持 VIP 优先,应该让认证通过的客户直接占一个窗口,或者把它插到队列头部。

3.3 顾客离开流程:出窗口、补队头、算时间三件事

case 2: { system("cls"); printf("请输入离开顾客的编号:\n"); scanf("%d", &x); likai(x); // 处理离开 mygrade(); // 满意度评分 w++; // 已离开人数 +1 allsum += A[0]; // 原意是累加逗留时间 getch(); system("cls"); break; }

这一段有动作有隐患。正常的离开流程应该是:按客户号把客户从窗口数组里清掉,如果队列有人在等,就把队头补进空窗口;接着弹出评分表让客户打分;最后把这个客户的逗留时间累加到 allsum。问题出在最后一行——allsum += A[0] 里的 A[0] 从来没被赋值过,永远是 0,所以这行实际等于什么都没加。后面避坑章会专门展开。

3.4 likai(x):清窗口,队列立刻补位

void likai(int x) { int i = 0; do { if (x > L.len) { printf("输入有误!\n请重新输入:"); scanf("%d", &x); } else { for (i = 0; i <= L.len; i++) { if (i == x) { printf("尊敬的%d 号顾客您好!\n", x); L.A[i] = 0; // 窗口置空 L.len--; // 占用窗口数减 1 if (Q.front != Q.rear) { int y = Dlqueue(&Q); // 排队第一人出队 L.A[i] = y; // 补进空窗口 L.len++; // 占用窗口数加回 } } } } } while (i == 0); }

设计意图是好的:窗口空出来后,若队列非空,立即让队头客户上位,保证“只要还有排队的人,就没有空闲窗口”。这个“空窗补位”的动作,就是数组和链队互相配合的关键一步。但实现里有几个细节会翻车。i 从 0 开始遍历到 L.len,用 i 同时充当数组下标和客户编号,这在一开始正好能对上,可一旦客户编号错位,下标和编号就会分家;do-while 的循环条件 while(i==0) 在输入非法时可能无限循环;x 超出 L.len 时只提示重输,没有限定重输次数。这些都是答辩时的高频问题。

3.5 两个查询界面:printl 看窗口,print2 看队列

void printl() { int i; printf("正在办理业务的顾客编号为:\n"); for (i = 1; i <= L.len; i++) { printf("%d ", L.A[i]); // 只打印 1 到 len,正好是窗口区 } printf("\n"); } void print2() { int i = 0; printf("正在等候办理业务的顾客编号为:"); Lnode *s = Q.front->next; // 从队头开始遍历 while (s != NULL) { printf("%d ", s->data); s = s->next; i++; } printf("\n您的前面一共有%d 人在排队,请您稍候!", i); printf("\n"); }

printl 遍历窗口数组,打印正在办理的客户号。print2 从队头节点开始,沿 next 指针走到队尾,边打印边计数,i 就是排队人数。这里可以看出数组的“查询正在办理”和链队的“查询排队人数”各有擅长,双轨设计的好处就在这——查询界面不用遍历整个队列才能知道谁在办理。

4. VIP 认证与柜台评分:从能跑到像样,还差这两块

第 3 章看的是两类客户共通的主流程,这一章把 VIP 认证和评分这两块独立模块拆开。它们在报告中占的篇幅不小,但代码质量参差不齐,老师通常会盯这两块。

4.1 vip() 的卡号密码认证

void vip(int x) { int i, a; a = x; char ch[3]; // 存密码的数组,只有 3 个字节 int k = 0; switch (a) { case 1: printf("请输入你的卡号:"); scanf("%d", &i); printf("请您输入五位密码:"); while (ch[k - 1] != ' ') { // 靠上次输入是否为空格判断结束 ch[k] = getch(); k++; printf("*"); } if (i == 100 && ch[0]=='1' && ch[1]=='1' && ch[2]=='1' && ch[3]=='1') { printf("\n尊敬的 VIP 用户您好,请您直接到 VIP 区办理业务!\n"); vip1++; } // 其余卡号 200~500 的 else if 分支同理 break; default: break; } }

这段代码在 VC6.0 里编译能过,但运行起来是玄学现场。先说数组,char ch[3] 只有 3 个字符的空间,密码要 5 位,ch[3]、ch[4] 已经越界。再说循环条件,ch[k - 1] != ' ' 的意思是“上一次读到的字符不是空格就继续读”,可这个循环里根本没有输入空格的机会,k 会一路自增直到数组越界,程序行为完全不可预测。正确的做法是固定读 5 次:

char ch[6]; int k = 0; while (k < 5) { ch[k] = getch(); putchar('*'); k++; } while (getch() != '\r'); // 如果还有回车要吞掉

这样密码长度固定为 5 位,数组给到 6 个字节留出 '\0' 位置,getch() 逐字符读取且不回显真实字符。

4.2 柜台评分:guitai、pingfeng、mygrade 三层结构

int guitai() { int y = 0; printf("请输入你所办理业务的柜台号(1-3):\n"); scanf("%d", &y); if (y < 1 || y > 5) { // 校验范围和提示不一致 printf("你输入的柜台号有误,请重新输入!\n"); printf("请输入你所办理业务的柜台号(1-3):\n"); scanf("%d", &y); } return y; } int pingfeng() { int y = 0; printf("请输入你评分(1-5):\n"); printf("评分标准:1 分非常不满意,5 分非常满意。\n"); scanf("%d", &y); if (y < 1 || y > 5) { printf("你输入评分有误,请重新输入!\n"); scanf("%d", &y); } return y; } void mygrade() { printf("为了改善服务质量,请你对我们的服务进行评价。谢谢!\n"); z = guitai(); y = pingfeng(); switch (z) { case 1: sum1 += y; i1++; ave1 = sum1 / i1; printf("%d 号柜台的平均满意度为 %0.2f。感谢你的评分!\n", z, ave1); break; case 2: sum2 += y; i2++; ave2 = sum2 / i2; printf("%d 号柜台的平均满意度为 %0.2f。感谢你的评分!\n", z, ave2); break; case 3: sum3 += y; i3++; ave3 = sum3 / i3; printf("%d 号柜台的平均满意度为 %0.2f。感谢你的评分!\n", z, ave3); break; default: printf("你的输入有误,请重新输入!\n"); } }

评分模块分成三层:guitai 负责柜台号输入校验,pingfeng 负责分数输入校验,mygrade 负责把分数累加到对应柜台的统计变量里。这个分层思路是好的,每层职责单一,也好改。坑在参数没对齐:n 定义成 3,guitai 却允许输到 5,mygrade 里还保留 case 4 和 case 5 的评分分支,属于窗口数改过、校验却漏改的典型症状。

4.3 VC6.0 环境下 scanf 和 getch 混用的坑

这份实验报告指明用 Microsoft Visual C++ 6.0 编译,那环境有个老毛病:scanf 读完后,输入缓冲区里常留着换行符,紧接着调 getch(),换行符会被 getch() 直接吞掉,导致密码输入少一位或界面一闪而过。常见做法是在 scanf 后面补一个 getchar() 清掉残留的换行符,或者干脆把整段密码输入改成 getch() 循环读取。写报告时如果提到“测试环境为 VC6.0”,最好也写一句“程序在 VC6.0 默认控制台下运行正常”,免得答辩时环境差异被老师拿出来说。

5. 银行排队系统实验报告避坑指南:五个常翻车的点,逐个排查

这份代码我在 VC6.0 和后来换到 Dev-C++、CodeBlocks 上各跑过几次,最常见的坑集中在五个地方。

5.1 平均逗留时间恒为 0:allsum += A[0] 是假累加

现象:主菜单输入 6 进入系统查询,显示“普通用户业务办理总共用时 0.00,平均用时 0.00”,不管离开多少人都是 0。

原因:离开流程里执行 allsum += A[0],可 A[0] 这个数组元素从初始化到运行结束都没有被赋值过,恒为 0。更本质的问题是,程序从头到尾没有记录客户的到达时间和离开时间,时间信息根本没进系统,怎么可能算得出逗留时间。

解决:按客户号记录时间戳。到达时记录一次,离开时再取一次当前时间做差值。

#include <time.h> time_t arrive_time[1000]; // 按业务号记录到达时刻 double total_used = 0.0; // 到达分支里 arrive_time[v] = time(NULL); // 离开分支里 total_used += difftime(time(NULL), arrive_time[x]); w++; average = total_used / w;

5.2 getch() 密码回显越界:数组只有 3 字节

现象:进入 VIP 认证或系统查询时,输入密码的过程不受控制,星号数量异常,甚至程序直接崩溃。

原因:密码要求五位,写在 char ch[3] 里必然越界;while(ch[k-1]!=' ') 的结束条件在上下文里永远不成立,k 不断自增,最终写穿数组。

解决:把存放密码的数组扩容到 char ch[6],循环固定读 5 位;如果想要更贴近真实体验,可以用回车键结束输入。

5.3 柜台号校验 1-5 与窗口数 n=3 矛盾

现象:运行界面明明只有 3 个窗口,评分界面却允许输入 4 号、5 号柜台,而且还能算出平均分。

原因:guitai() 里的范围判断写的是 if(y<1||y>5),mygrade() 的 switch 里保留了 case 4、case 5 的评分分支。这是从“5 窗口版本”改到“3 窗口版本”时,只改了宏定义 n,没改校验逻辑和评分逻辑。

解决:把 guitai() 的边界改成 if (y < 1 || y > n),并同步删掉 switch 里 4、5 号柜台分支。

5.4 likai() 用数组下标当客户编号,错位是迟早的事

现象:简单场景下输入编号 1 离开,能正常工作;一旦出现过补位,再按编号操作就会发现“编号对应的窗口是别人的号”。

原因:likai() 内部用 for(i=0;i<=L.len;i++) 逐个比对 i==x,把数组下标 i 直接当作客户编号 x。在客户按顺序进入、窗口数组顺序连续的早期场景下,业务号恰好等于数组下标,所以能蒙对。但 A[0] 永远空置,编号从 1 开始,下标从 0 开始,一出现空洞和补位,编号与下标的映射就断了。

解决:不要在数组里找下标等于 x,而是遍历窗口数组找值等于 x 的位置。

void likai(int x) { for (int i = 1; i <= n; i++) { if (L.A[i] == x) { L.A[i] = 0; L.len--; if (Q.front != Q.rear) { int y = Dlqueue(&Q); L.A[i] = y; L.len++; } return; } } printf("没有找到编号为 %d 的顾客\n", x); }

5.5 整个模拟没有随机到达,不满足“不断有客户进入”的要求

现象:实验要求“从早晨银行开门起不断有客户进入”,但实际运行程序时,每来一个客户都要手动按一次菜单,完全不是自动模拟。

原因:到达事件全靠人工触发,没有调用任何随机数生成函数,也没有时间驱动的机制。这是这份报告最容易被老师挑刺的地方之一。

解决:用 srand 和 rand 生成随机到达间隔。

#include <stdlib.h> #include <time.h> srand((unsigned)time(NULL)); int next_arrival = rand() % 10 + 1; // 1~10 分钟后下一位客户到达

在实际运行时,可以把“每 N 秒自动执行一次 daoda”放进主循环,到达间隔由 rand() 决定,这样才说得上是模拟。

6. 改进思路与一次完整实测:把报告写成闭环,答辩才不怕问

6.1 补上平均逗留时间的正确口径

要让实验结果站得住,平均逗留时间必须真有数据支撑。把客户看成结构体,id 配到达时间,离开时算差:

struct Customer { int id; time_t arrive_time; }; double total_stay = 0.0; int finished = 0; // 到达时记录 Customer c; c.id = v; c.arrive_time = time(NULL); // 离开时计算 double stay = difftime(time(NULL), c.arrive_time); total_stay += stay; finished++; average = total_stay / finished;

time(NULL) 返回的是秒级时间戳,对课程设计验收来说精度够用。如果要更细,就用 clock() 取 CPU 时间,或者直接以“模拟时间步长”为单位,每处理一个事件计 1 个时间单位。

6.2 用 3 个窗口完整跑一遍验证流程

拿到资源后,先别急着改代码,按下面这组用例原样跑一遍,确认原始行为:连续输入四次“1,普通卡用户”,前三位客户占用 1、2、3 号窗口,第四位进入队列;菜单输入 4,显示队列里有 1 人;菜单输入 2,离开编号 1,评分 5,此时 1 号窗口应补上业务号 4;再输入 3,显示正在办理 4、2、3;输入 6,卡号 000,密码 1111,查看统计人数和平均时间。

这组用例覆盖了窗口数组、链队、补位、评分、统计五个模块。如果最后平均用时显示 0.00,就是踩中了 5.1 的坑,正好拿它当报告里的“改进前 vs 改进后”对比材料。

6.3 报告里测试结果怎么写

写报告时,不要只贴代码,可以把上面的用例整理成一张测试表,列清楚:输入、操作、预期输出、实际输出、是否通过。再配一张简化流程图:主菜单 → 到达(VIP 认证 / 普通取号)→ 数组满则入队 → 离开(清窗、出队补位、评分)→ 查询。这样老师一眼就能看出你理解了文件里的每个函数和界面入口。

从那以后,我每次交课程设计,都会先做一遍这样的“全流程手动实测”,把每个菜单入口都走一遍,确认预期结果和实际输出一致,再截图放进报告。这比答辩现场被老师问住再补强得多,也几乎不会出现“代码能跑但报告说的话对不上”的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询