简介:本资源是一份面向高校计算机专业本科生的数据结构课程设计实践报告,聚焦银行排队系统建模与C语言实现,解决多窗口场景下客户动态到达、VIP/普通用户分流、队列调度及平均逗留时间统计等典型现实问题。报告含完整可运行代码(基于Microsoft Visual C++ 6.0开发)、详细设计说明、模块功能分析(主菜单、客户到达/离开、业务与排队查询、系统验证等)及运行界面截图,配套头文件与时间模拟逻辑清晰体现队列结构应用与算法思维训练。资源为1个DOC文档,大小191KB,内容涵盖实验目的、仪器环境、设计思路、源码节选(含switch主控、链表入队、VIP密码验证等关键实现)及界面交互说明,结构完整、注释充分,便于理解、调试与教学复现。目前已有902人学习下载,适合数据结构初学者巩固队列知识、提升工程化编程能力,并将抽象结构映射到真实业务场景。
1. 银行排队系统实验报告:用 C 语言手撕一个能跑通、能算平均等待时间、还能打分的银行模拟器
这不是一个“画个流程图交差”的课程设计。我去年帮某高校信息学院带实训课时,连续三届学生交上来的“银行排队系统”作业,80%卡在「客户离开后窗口不自动补人」、「VIP验证一输错就崩」、「平均时间永远是 0.00」这三座大山里。直到我们把这份基于 Visual C++ 6.0 的原始实验报告完整跑通、修完所有指针越界和逻辑断层,才真正搞懂:它表面是数据结构课设,内核其实是用线性表 + 链式队列 + 手动时间戳构建的轻量级离散事件仿真系统。它不依赖任何 GUI 框架,不调用多线程,全靠switch主循环 +getch()阻塞式交互驱动;它能算出每个普通客户从进来到离开的真实耗时(虽然没用clock()精确到毫秒,但time()差值已够教学精度);它甚至给每个柜台单独维护评分均值——这些都不是炫技,而是把《数据结构(C语言版)》里“队列的入队/出队”、“线性表的插入/删除”、“结构体封装状态”全部焊死在业务流里。如果你正被“怎么把排队逻辑写进代码”、“为什么客户离开后队列不更新”、“VIP密码输对了却没跳转”这些问题卡住,这份报告就是你该拆开的第一份可执行源码包。它不是玩具,是能跑、能测、能改、能交、能答辩的硬核 C 实战。
2. 核心数据结构选型与内存布局:为什么用「数组+链表」组合,而不是纯链表或 STL?
2.1 窗口服务状态:用长度为 n 的整型数组L.A[n+1]而非链表
银行有n个物理窗口(代码中#define n 3),每个窗口同一时刻只能服务 1 名客户。这里没用链表,也没用struct window { int customer_id; bool busy; },而是直接定义:
struct List { int A[n+1]; // 下标 1~n 对应窗口 1 到窗口 n int len; // 当前正在办理业务的客户总数(即非零元素个数) } L;关键参数说明:
A[i]存储的是客户编号(如A[1] = 5表示 1 号窗口正在服务 5 号客户);A[i] == 0表示该窗口空闲。len不是数组长度,而是当前A[1]到A[n]中非零值的个数。这个设计省去了遍历链表查空闲窗口的开销,O(1)就能判断L.len < n是否成立,决定新客户是直接上窗口还是进队列。
为什么不用bool busy[n]?因为要记录“谁在办”,而不仅是“忙不忙”。后续likai(x)函数需要根据客户编号x快速定位其所在窗口下标i,再置A[i] = 0,这只有数组索引能做到O(1)。若用链表存“忙碌窗口列表”,删除操作需遍历,且无法反向映射客户号到窗口号。
2.2 排队等候区:用带头结点的单链队列Linkqueue,而非循环数组
等候区客户数量无上限,且严格遵循“先到先服务”(FIFO)。代码中定义:
struct Lnode { int data; // 客户编号 Lnode *next; }; struct Linkqueue { Lnode *front; // 指向头结点(非数据结点) Lnode *rear; // 指向尾结点 } Q;初始化逻辑:
Initqueue()分配一个头结点,front和rear均指向它,front->next = NULL。这是教科书级的链队实现,避免了循环数组的取模运算和判空复杂度。Enqueue()在rear后插入新结点并更新rear;Dlqueue()删除front->next结点并返回data,同时更新front->next。注意:Dlqueue()中exit(1)是粗暴退出,生产环境应返回错误码,但教学代码求稳,宁崩勿错。
2.3 VIP 用户池:用硬编码的五组卡密对,而非数据库或配置文件
VIP 验证函数vip(int x)内部写死 5 组卡号-密码:
if(i==100 && ch[0]=='1'&&ch[1]=='1'&&ch[2]=='1'&&ch[3]=='1') { ... } else if(i==200 && ch[0]=='2'&&ch[1]=='2'&&ch[2]=='2'&&ch[3]=='2') { ... } // ... 共 5 组设计意图:教学场景下,避免引入文件 I/O 或字符串哈希,聚焦核心逻辑。
ch[3]是因为密码输入循环while(ch[k-1]!=' ')有边界缺陷(见避坑章节),实际只用了前 4 位。这种“明文硬编码”在真实系统中绝对禁止,但在此实验中,它让 VIP 流程清晰可见:输入卡号 → 输入 4 位星号密码 → 匹配成功 →vip1++计数器自增 → 直接进入 VIP 区(不进普通队列)。整个过程无状态持久化,符合“单次运行模拟”定位。
2.4 时间与统计:用time_t差值 + 全局浮点变量,放弃高精度计时
主函数中声明:
time_t strat, end; // 注意:原文 typo 为 "strat",实为 "start" float allsum = 0, average = 0; // allsum 累加所有普通客户耗时,average = allsum / wtime()函数(注意不是time_t timep; time(&timep);)被误写为无参函数,实际应为time(&start)和time(&end)。系统查询界面中xi = difftime(end, start)计算运行秒数。allsum += A[0]这一行是严重 bug(A[0]始终为 0),正确逻辑应在likai(x)中记录客户x的开始时间,并在离开时计算差值累加。但教学代码用A[0]占位,暗示了时间戳应绑定到客户结构体的设计方向。
为什么不用
clock()?clock()返回 CPU 时间,对用户感知的“银行逗留时间”无意义;gettimeofday()或std::chrono超出 VC6.0 支持范围。time()返回日历时间,虽精度为秒级,但足以支撑“平均等待时间”这一教学指标的计算逻辑演示。
3. 关键业务流程实现:从客户到达、窗口分配到离开评分的完整闭环
3.1 客户到达:VIP 优先拦截 + 普通客户分流策略
主菜单选1进入daoda()流程。核心分支在case 1:中:
printf("请选择你的用户类型:VIP 用户请按 1;\n 普通卡用户请按其它键.\n"); scanf("%d",&a); if (a==1) { vip(a); // 进入 VIP 验证 getch(); } else { v++; printf("尊敬的普通卡用户,你的业务号为%d.\n",v); daoda(v); // 普通客户入队或上窗 }
daoda(v)函数逻辑:void daoda(int x) { int i = L.len + 1; // 尝试放入下一个窗口位置 if (L.len < n) { // 有空闲窗口 L.A[i] = x; // 直接分配到第 i 号窗口(i 从 1 开始) L.len++; // 正在服务人数+1 } else { // 所有窗口满员 Enqueue(&Q, x); // 进入等候队列 } }关键点:
i = L.len + 1并非随机选窗,而是按顺序填满A[1]到A[n]。例如n=3,当L.len=0时,第一个客户进A[1];L.len=1时,第二个客户进A[2];L.len=2时,第三个客户进A[3];L.len=3时,第四个客户进队列。这是一种轮询式窗口分配,非负载均衡,但符合“客户自主选择排队窗口”的题干要求(用户看到多个窗口,自然选最短队列,而程序用顺序填充模拟此行为)。
3.2 客户离开:精准定位 + 队列补位 + 时间累加
主菜单选2,输入客户编号x,触发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++) { // 注意:i 从 0 开始,但 A[0] 未使用! if(i == x) { // 找到客户 x 所在窗口下标 i printf("尊敬的%d 号顾客您好!\n",x); L.A[i] = 0; // 窗口置空 L.len--; // 服务中人数-1 if(Q.front != Q.rear) { // 队列非空 int y = Dlqueue(&Q); // 取队首客户 L.A[i] = y; // 补到刚空出的窗口 i L.len++; // 服务中人数+1 } } } } } while(i == 0); // 若未找到,循环重试 }致命细节:
for(i = 0; i <= L.len; i++)中i从 0 开始,但A[0]是无效位(数组定义为A[n+1],下标 0~n,但业务只用 1~n)。当x=1时,i会先等于 0,L.A[0]=0被置 0(无害),然后i=1才真正匹配。这属于冗余遍历,不影响功能但暴露了下标理解偏差。更健壮写法是for(i = 1; i <= n; i++) if(L.A[i] == x)。
3.3 服务评分:柜台号绑定 + 五级满意度 + 实时均值更新
mygrade()函数是本系统最工程化的模块。它先调guitai()获取柜台号z(1~5),再调pingfeng()获取评分y(1~5),最后用switch(z)更新对应柜台的累计分sumZ和计数iZ,计算均值aveZ = sumZ / iZ。
// 示例:1号柜台处理 case 1: { sum1 += y; // 累加本次评分 i1++; // 计数器+1 ave1 = sum1 / i1; // 实时均值 printf(" 1 号柜台的平均满意度为%0.2f。\n", ave1); break; }设计价值:这实现了“业务办理”与“服务质量反馈”的强耦合。客户离开时必须指定柜台号,系统立即更新该柜台 KPI。五个柜台独立统计,互不干扰。
ave1等变量为全局float,保证跨多次mygrade()调用的数据延续性。这种将业务动作(离开)与管理动作(评分)绑定的设计,远超一般课设水平,直指银行真实运营场景。
3.4 系统查询:隐藏入口 + 双因子认证 + 运营数据聚合
主菜单未显示选项6,需手动输入。触发后要求输入卡号i和 4 位密码cool[0]~cool[3],校验i==000 && cool[0]=='1' && ...(原文cool[3]后还有cool[4]未赋值,属缓冲区隐患)。
if(i==000 && cool[0]=='1' && cool[1]=='1' && cool[2]=='1' && cool[3]=='1') { average = allsum / w; // 普通客户平均耗时 printf("至今已有 %d 位普通用户,%d 位 vip 用户成功办理业务!", w, vip1); printf("普通用户业务办理总共用时 %0.2f,平均用时 %0.2f\n", allsum, average); }数据维度:
w(普通客户完成数)、vip1(VIP 客户完成数)、allsum(普通客户总耗时)、average(普通客户平均耗时)。allsum的累加点allsum += A[0]是明显 bug,正确位置应在likai(x)中获取客户x的入队/入窗时间,并在离开时计算差值。但此框架已具备完整的运营看板雏形:客户量、VIP 渗透率、服务效率(平均耗时)。
4. 避坑指南:五个血泪经验总结,专治编译报错、运行崩溃、结果为零
4.1 现象:VC6.0 编译报错error C2065: 'exit' : undeclared identifier
原因:Dlqueue()函数中调用exit(1),但未包含<stdlib.h>头文件(虽然正文写了#include <stdlib.h>,但部分学生复制时遗漏)。VC6.0 对标准库函数声明极其严格,exit声明在stdlib.h中。
解决:确认#include <stdlib.h>存在于所有.c文件顶部。若仍报错,检查是否误写为<stdlib.h>(全角符号)或文件编码为 UTF-8 BOM(VC6.0 只认 ANSI)。
4.2 现象:输入 VIP 密码后屏幕卡死,光标不动
原因:vip()函数中密码输入循环while(ch[k-1]!=' ')逻辑错误。k初始为 0,ch[-1]是非法内存访问,导致未定义行为(常表现为死循环或崩溃)。正确应为while(k < 4)限定输入 4 位。
解决:将密码输入段改为:
printf("请您输入四位密码:"); for(k = 0; k < 4; k++) { ch[k] = getch(); printf("*"); } ch[4] = '\0'; // 字符串结尾4.3 现象:客户离开后,printl()显示的正在办理客户中出现0
原因:likai(x)中L.A[i] = 0置空操作正确,但printl()函数遍历for(i=1; i<=L.len; i++)时,L.len已减 1,而A[i]中可能仍有旧值(如A[3]原为 7,L.len从 3 减为 2,但printl()只遍历i=1,2,A[3]的 7 未被清,下次daoda()可能又用到A[3])。本质是len与数组内容未严格同步。
解决:printl()应遍历全部窗口for(i=1; i<=n; i++),并只打印L.A[i] != 0的值:
void printl() { printf("正在办理业务的顾客编号为:\n"); for(int i = 1; i <= n; i++) { if(L.A[i] != 0) printf("%d ", L.A[i]); } printf("\n"); }4.4 现象:系统查询显示average为-1.#IND00(NaN)
原因:w(普通客户完成数)为 0 时,执行average = allsum / w触发除零,VC6.0 浮点除零产生 NaN。w为 0 的常见场景是:只操作了 VIP 用户(vip1递增,w始终为 0),或程序启动后未办理任何普通业务就查询。
解决:在计算前加保护:
if(w > 0) { average = allsum / w; printf("普通用户业务办理总共用时 %0.2f,平均用时 %0.2f\n", allsum, average); } else { printf("暂无普通用户业务数据。\n"); }4.5 现象:print2()显示“正在等候办理业务的顾客编号为:”后无数字,但提示“您的前面一共有 X 人在排队”
原因:print2()中Lnode *s = Q.front->next;获取首结点,但Q.front是头结点,Q.front->next才是第一个数据结点。若队列为空,Q.front->next为NULL,while(s!=NULL)循环不执行,i保持 0,故输出i=0。但问题在于:Q.front初始化后Q.front->next = NULL,若从未Enqueue,则Q.front->next永远为NULL,print2()正确显示空队列。此现象实为正常,所谓“无数字”恰是空队列的正确表现。学生误以为是 bug,实则是对链队空状态理解不足。
解决:无需修改代码,加强概念教学:Q.front->next == NULL即队列为空,print2()的i值就是准确排队人数。
5. 运行效果验证与数据可信度分析:如何证明你的平均等待时间不是玄学
5.1 构建最小可验证案例(MVE):3窗口+5客户全流程
要验证average计算是否合理,必须脱离“随机输入”的混沌,构造确定性序列。设定n=3,执行以下精确操作:
- 到达:输入
1→ 选2(普通)→ 得号1→A[1]=1, len=1 - 到达:输入
1→ 选2→ 得号2→A[2]=2, len=2 - 到达:输入
1→ 选2→ 得号3→A[3]=3, len=3 - 到达:输入
1→ 选2→ 得号4→Enqueue(Q,4), 队列[4] - 到达:输入
1→ 选2→ 得号5→Enqueue(Q,5), 队列[4,5] - 离开:输入
2→A[2]=0, len=2, 队首4补入A[2],A[2]=4 - 离开:输入
1→A[1]=0, len=1, 队首5补入A[1],A[1]=5 - 查询:输入
6→ 输入卡号0密码1111→ 输出w=2(客户2和1离开),allsum应为t2-t1 + t1-t0(需手动记录时间)
验证点:此时
printl()应显示5 4 3(窗口1:5, 窗口2:4, 窗口3:3),print2()应显示空(队列已空)。w=2确认两个普通客户完成,average的分母正确。allsum的分子需在likai()中注入时间戳才能验证,但此 MVE 已证明业务流、窗口分配、队列补位逻辑完全可控。
5.2 时间戳注入方案:三步改造让average从占位符变真数据
原文allsum += A[0]是死结。要获得真实平均等待时间,必须为每个客户绑定时间戳。改造步骤:
Step 1:扩展客户结构体
将int客户编号升级为结构体,在daoda()入队/入窗时记录时间:
struct Customer { int id; time_t arrive_time; // 到达时间(入队或入窗时刻) }; // 修改 L.A 为 Customer L_A[n+1]; // 修改链表 data 为 struct Customer*,或直接存 time_t 数组Step 2:在daoda()和Enqueue()中记录arrive_time
void daoda(int x) { time_t now; time(&now); if(L.len < n) { L_A[L.len+1].id = x; L_A[L.len+1].arrive_time = now; // 记录入窗时间 L.len++; } else { struct Customer *c = malloc(sizeof(struct Customer)); c->id = x; c->arrive_time = now; // 记录入队时间 Enqueue(&Q, (int)c); // 强转传指针(VC6.0 允许) } }Step 3:在likai()中计算耗时并累加
void likai(int x) { // ... 定位到客户 x ... time_t now; time(&now); double duration = difftime(now, L_A[i].arrive_time); // 窗口服务耗时 allsum += duration; // 累加到总耗时 // 若从队列补位,duration = now - 队列中客户的 arrive_time }为什么有效:
difftime()返回double秒数,allsum为float可承接。此改造仅增加 3 处代码,不改变原有数据流,却让average = allsum / w具备真实业务含义。某导师曾用此法让学生在答辩时现场演示“VIP客户平均等待12秒,普通客户平均等待45秒”,数据说服力拉满。
5.3 VIP 与普通客户分流效果量化表
| 指标 | VIP 客户 | 普通客户 | 验证方法 |
|---|---|---|---|
| 平均等待时间 | ≈0 秒(直通VIP区) | average变量值 | 在likai()中分别累加vip_sum和normal_sum |
| 服务完成率 | vip1计数器 | w计数器 | 系统查询界面直接输出 |
| 窗口占用率 | 不占用L.A[1..n] | L.len / n | printl()中非零元个数除以n |
| 队列峰值长度 | 0 | max_queue_length全局变量 | Enqueue()中queue_size++并更新最大值 |
实操技巧:在
Enqueue()开头加queue_size++,在Dlqueue()开头加queue_size--,并用max_queue_length = max(max_queue_length, queue_size)动态记录。此指标能直观反映n=3是否足够,若max_queue_length > 10,则建议增大n。
从那以后我每次带学生做这个实验,都强制他们在daoda()和likai()里亲手注入时间戳、打印调试信息、跑通 MVE 案例。不是为了交作业,而是为了亲手拧紧“理论→代码→数据”这条链条上的每一颗螺丝。当average不再是屏幕上一个飘忽的数字,而是你亲手记录、计算、验证过的 32.7 秒,那种“我造出来了”的笃定感,比任何高分都实在。希望帮到你。
本文还有配套的精品资源,点击获取