数据结构课设:银行排队系统链队列与平均逗留时间实现
发布时间:2026/10/27 20:13:10 锦皓数字建站

简介《银行排队系统实验报告》是一份面向数据结构课程学习者和C语言初学者的一份doc实验文档围绕多窗口银行排队场景帮助读者把队列结构、随机事件模拟与时间统计落到可运行的程序设计中。文档共1个doc文件压缩包约191KB内容从设计要求、实验目的、仪器与编译环境切入梳理主菜单、顾客到达、顾客离开、业务查询、排队查询和系统查询等模块并结合结构体、链队列、头文件调用与time函数说明实现思路。文档还呈现VIP与普通客户分流、业务编号、平均逗留时间计算等细节可作为课程实验报告撰写、C语言课程设计参考和队列应用练习的样本。已有902人学习适合需要理解银行排队模拟流程、对照模块设计与代码组织方式的学生查阅。1. 三个窗口、一条队伍和一个必须算准的平均逗留时间数据结构课设里银行排队系统的实验报告几乎是最容易被写成“能跑就行”的题目——菜单能跳、编号能排、屏幕能刷看起来就完事了。但答辩时被追问一句“平均逗留时间怎么算出来的”很多人当场就卡住了。真正要交付的东西有三件一套自洽的排队模型、一组经得起对照测试的运行数据、一段能解释清楚“为什么等候区用链队列而不是循环队列”的设计说明。这套方案面向两类读者正在做数据结构课程设计、需要一个能直接落笔的骨架的人和要给新人布置练手题、希望两周就能讲透的带教人员。下面按需求、选型、架构、实现、避坑、验收往下走。2. 需求拆解与选型3 个窗口、2 类客户、5 个菜单项怎么落到数据结构上题目里那句“假设有 n 个窗口对外接待业务”其实把架构定死了n 个窗口是固定数量的并发服务台客户人数是不固定的所以窗口和等候区必须用两种完全不同的存储结构。这一步想清楚后面的代码基本是顺着写下来的。2.1 把业务流程翻译成三张表窗口表、等候队列、顾客档案模拟程序最难的不是语法是“哪些信息该跟着顾客走”。把一天的业务拆开看其实只有三个实体实体存储结构关键字段作用窗口定长数组win_id[WIN1]窗口号、当前顾客编号0 表示空闲回答“哪个窗口在办谁”等候队列带头结点的链队列队头、队尾回答“谁在我前面”顾客档案结构体数组cust[]编号、到达时刻、是否 VIP、占用窗口回答“这个人从进来到现在多久了”第三张表是最容易被漏掉的。原始版本的实现里顾客编号只是一个整数在数组和链表之间传来传去等到用户点“离开”要结算时根本找不到这个人是什么时候进来的——平均逗留时间自然算不出来。把顾客编号当作主键、额外维护一张档案表是这套系统能不能自圆其说的分水岭。#include stdio.h #include malloc.h #include stdlib.h #include time.h #define WIN 3 /* 对外窗口数就是题目里的 n改这一个数字就能扩缩容 */ #define MAXC 500 /* 顾客档案表容量按一天最大客流估超了要报错而不是越界 */ /* 顾客档案一条记录跟着一个顾客走完全程 */ typedef struct { int id; /* 顾客编号普通用户从 1 开始自增 */ int arrive; /* 到达时刻秒级时间戳结算逗留时间的唯一依据 */ int is_vip; /* 1 VIP0 普通卡 */ int win; /* 占用的窗口号0 表示还在队列里-1 表示已离开 */ } Customer; Customer cust[MAXC 1]; int cust_top 0; /* 已发放的最大编号 */ int win_id[WIN 1]; /* win_id[i] 存第 i 号窗口正在办的顾客编号0 为空闲 */win_id用下标 1..WIN 而不是 0..WIN-1纯粹是为了跟界面上“1 号柜台、2 号柜台”的说法对齐少一层-1的心算。这个下标从 1 开始的习惯在实验报告里写一句“与柜台编号保持一致避免展示层做偏移换算”评阅时是加分项。2.2 等候区为什么必须是链队列循环队列差在哪很多人第一反应是用数组做循环队列毕竟教材上刚讲完。但放到这个场景里循环队列会立刻暴露两个麻烦。第一个是容量。循环队列必须预设MAXSIZE而题目说的是“客户人数众多时需排队”——人数上限未知。设小了队满要处理溢出要么丢客户要么拒绝服务逻辑分支立刻翻倍设大了白白占内存而且实验报告里还要解释这个数字是怎么来的。链队列没有这个问题来一个人malloc一个结点内存上限就是机器的内存上限。第二个是判满代价。循环队列的经典写法要牺牲一个存储单元来区分队空和队满即(rear1)%MAXSIZE front表示满。这个约定在报告里必须解释一遍而链队列只要front rear就是空一句话说完。/* 链队列结点只存顾客编号不存整个 Customer 结构体 */ typedef struct Lnode { int data; /* 顾客编号相当于档案表的下标 */ struct Lnode *next; /* C 里必须写 struct Lnode *不能只写 Lnode * */ } Lnode; typedef struct { Lnode *front; /* 头结点front-next 才是真正的队首 */ Lnode *rear; /* 指向最后一个结点入队 O(1) */ } LinkQueue; LinkQueue Q; void InitQueue(LinkQueue *Q) { Q-front Q-rear (Lnode *)malloc(sizeof(Lnode)); if (Q-front NULL) { printf(内存不足程序退出\n); exit(1); } Q-front-next NULL; /* 带头结点空队列时 front rear */ } void EnQueue(LinkQueue *Q, int id) { Lnode *s (Lnode *)malloc(sizeof(Lnode)); if (s NULL) { printf(内存不足程序退出\n); exit(1); } s-data id; s-next NULL; Q-rear-next s; /* 挂到队尾 */ Q-rear s; /* 尾指针后移一位 */ } /* 出队成功返回 1 并把编号写进 *id空队返回 0把判断权交给调用方 */ int DeQueue(LinkQueue *Q, int *id) { Lnode *p; if (Q-front Q-rear) return 0; p Q-front-next; *id p-data; Q-front-next p-next; if (Q-rear p) Q-rear Q-front; /* 出的是最后一个结点尾指针要回退 */ free(p); return 1; }DeQueue里那句尾指针回退是链队列最容易写漏的一行。漏了它队列最后一个元素出队后rear仍然指向一块已经free掉的内存下一次EnQueue就是往野指针上写数据——这类问题在单次演示里往往不出现答辩现场连点几下才崩属于典型的“血泪经验”。另外注意返回值设计出队失败返回 0 而不是直接exit(1)。空队列在模拟里是正常状态窗口办完事没人排队不是错误用exit处理等于把正常流程当异常。2.3 事件驱动的两条路手动敲菜单还是随机到达题目要求“从早晨银行开门起不断有客户进入”这句话有两种实现路线实验报告里必须明确写选了哪条、为什么。第一条是手动事件驱动也就是课程要求的交互式做法用户在菜单里敲 1 表示有人到达、敲 2 表示有人离开每次操作触发一次状态变更。优点是每个事件都能单独观察、单独截图放进报告调试时可以把某一帧状态原样复现缺点是没法真正“跑一天”一个人工点出来的序列不具备统计意义。第二条是随机事件驱动用rand()生成下一位客户的到达间隔和服务时长把事件按发生时刻排序放进事件表或者直接按时间轴顺序推进程序自己跑完一整天最后一性次输出平均逗留时间、平均队长、窗口利用率。这条路线才符合“模拟”的本意但实现量翻倍且中途没法演示单步过程。比较稳妥的做法是两者都要主流程走手动事件驱动保证可演示另外写一个AutoRun(days)函数挂在隐藏菜单项下面内部用随机间隔批量调用Arrive/Leave最后打印统计。这样报告里既有操作截图又有“连续模拟 8 小时、共服务 XXX 人、平均逗留 XX 秒”的硬数据。/* 自动跑批按泊松式到达间隔推进只在本机演示用不参与菜单交互 */ void AutoRun(int total) { int i, t 0; srand((unsigned)time(NULL)); for (i 1; i total; i) { t rand() % 30 10; /* 相邻两位客户到达间隔 10~39 秒 */ Arrive(cust_top, (rand() % 100) 20 ? 1 : 0); /* 约两成是 VIP */ /* 每隔若干人清空一次窗口模拟业务办完 */ if (i % 7 0) CloseSomeWindow(); } }rand() % 30 10里的 30 和 10 要写进报告的参数说明这是按“一位柜员平均 1~3 分钟办一笔、网点日均百来人”估出来的量级不是随便填的。参数说明写清楚评阅才会认为你理解自己写的每一行。3. 总体设计与模块划分一个主循环加六个函数的职责边界原始版本的main函数把所有东西都塞进了switch里菜单打印、VIP 认证、密码输入、评分统计、系统查询全挤在同一个函数体里接近两百行。这种写法能跑但报告里画不出调用关系图答辩时也很难说清模块边界。3.1 主循环只做三件事打印、取值、分发主循环的职责应该被压到最小显示菜单、读入选择、把控制权交给对应的处理函数。所有业务逻辑都下沉到MenuXxx()里主函数里不出现任何一个malloc、不出现任何一次scanf读具体业务数据。int main(void) { int choice; InitWindows(); /* 窗口表全部置 0 */ InitQueue(Q); /* 队头队尾指向同一个头结点 */ for (;;) { ShowMenu(); /* 打印菜单 当前时刻 */ if (scanf(%d, choice) ! 1) { /* 输入非数字清掉脏数据重来 */ while (getchar() ! \n); continue; } switch (choice) { case 1: MenuArrive(); break; /* 顾客到达 */ case 2: MenuLeave(); break; /* 顾客离开 服务评分 */ case 3: ShowServing(); break; /* 在办业务查询 */ case 4: ShowWaiting(); break; /* 排队情况查询 */ case 5: return 0; /* 退出 */ case 6: MenuAdmin(); break; /* 系统统计菜单上不打印内部口令进入 */ default: printf(输入有误请重新选择\n); } } }case 5直接return 0而不是break是因为原版在return后面还跟了getch(); system(cls);这类永远执行不到的语句属于典型的复制粘贴残留。退出路径就应该干净利落一行。case 6是隐藏项菜单上只打印 1~5但内部人员知道输入 6 能进统计界面。这个“隐藏菜单”的设计在报告里可以写成一个小亮点——它体现了权限分离的思想普通客户看不到运营数据。3.2 模块清单与调用关系把功能拆成六个模块每个模块对外只暴露一个入口函数模块入口函数依赖的数据是否改变状态到达处理MenuArrive→Arrive窗口表、等候队列、顾客档案是离开处理MenuLeave→Leave窗口表、等候队列、顾客档案是评分处理Score评分累计数组是在办查询ShowServing窗口表否排队查询ShowWaiting等候队列否运营统计MenuAdmin顾客档案、累计量否这张表放在报告第三章比画一张箭头满天飞的图有用得多。它清楚地表达了“查询类操作不改变状态”这一约定——而原始版本里的print2()在遍历队列时用了Lnode *s Q.front-next;而没有修改任何指针这一点是对的值得单独写一句“查询接口保持只读避免遍历过程中意外改动队头”。3.3 初始化与全局状态InitWindows和InitQueue不能省原始版本在main开头调了Initshuzu()和Initqueue()思路是对的但写在了while(1)之前的变量声明之后中间还夹着double sum 0;这种声明。在老编译器上声明必须集中在块首混写会直接编译不过。稳妥的写法是让初始化函数自己遍历void InitWindows(void) { int i; for (i 1; i WIN; i) win_id[i] 0; /* 0 表示该窗口空闲 */ cust_top 0; total_stay 0; done_cnt 0; } void InitQueue(LinkQueue *Q) { Q-front Q-rear (Lnode *)malloc(sizeof(Lnode)); if (Q-front NULL) { printf(内存不足\n); exit(1); } Q-front-next NULL; }注意InitWindows顺带把total_stay和done_cnt一起清零了。统计量必须跟状态量在同一个地方初始化否则改天加了“重新开张”功能很容易只清窗口不清累计值第二天的平均值被第一天的数据污染。4. 关键实现到达、离开、查询与平均逗留时间四个核心算法这四个函数写对了整个系统就立住了。剩下的菜单、界面、评分都是外围装饰。4.1 到达事件有空窗口就直接上没空位才入队到达事件的核心判断只有一句窗口没坐满就坐窗口坐满了才进队列。原始版本用if (L.len n)判断逻辑是对的但L.len表示的是“数组里元素的个数”如果中途有人离开、数组中间出现空洞L.len就不等于实际在办人数了。/* 找一个空闲窗口返回窗口号全忙则返回 0 */ int FindFreeWin(void) { int i; for (i 1; i WIN; i) if (win_id[i] 0) return i; return 0; } /* 到达事件id 由调用方分配is_vip 表示客户类型 */ void Arrive(int id, int is_vip) { int w; if (id MAXC) { printf(档案表已满本次到达被丢弃\n); return; } cust[id].id id; cust[id].arrive (int)time(NULL); /* 关键打时间戳后面结算全靠它 */ cust[id].is_vip is_vip; cust[id].win 0; w FindFreeWin(); if (w 0) { /* 有空窗口直接占用 */ win_id[w] id; cust[id].win w; printf(请 %d 号顾客到 %d 号窗口办理\n, id, w); } else { /* 窗口占满进等候队列 */ EnQueue(Q, id); printf(请 %d 号顾客在等候区等待前面还有 %d 人\n, id, QueueLen(Q)); } }FindFreeWin用一层循环找空闲窗口时间复杂度 O(WIN)。窗口数是个位数这个开销可以忽略没必要为了“优化”去维护一个空闲窗口链表——那种优化在报告里要多解释两百字收益却是零。VIP 的处理放在到达事件里而不是单独开一条队列。原始版本让 VIP 直接进“VIP 区办理”不占普通窗口。如果要做真正的优先队列正确做法是把等候区改成两条链队列VIP 队列 普通队列出队时优先从 VIP 队列取/* 优先出队VIP 队列非空先出 VIP否则出普通客户 */ int DeQueuePri(LinkQueue *vipQ, LinkQueue *normQ, int *id) { if (DeQueue(vipQ, id)) return 1; return DeQueue(normQ, id); }这个改动只多三行但把“VIP 优先”从一个界面上的说法变成了数据结构层面的事实。报告里配上这段优先级队列的知识点就算用上了。4.2 离开事件从队头补位顺手把逗留时间结掉离开是整个系统里状态变更最密集的一步要做四件事校验、结算、补位、标记。顺序不能乱。/* 离开事件结算逗留时间并返回秒数编号非法返回 -1 */ int Leave(int id) { int w, next_id, stay; if (id 1 || id cust_top) return -1; /* 编号根本没发过 */ if (cust[id].win 0) return -1; /* 还在排队或已离开不能走 */ w cust[id].win; stay (int)time(NULL) - cust[id].arrive; /* 逗留时间 离开时刻 - 到达时刻 */ total_stay stay; done_cnt; /* 队头补位队列非空就让下一个人顶上刚空出来的窗口 */ if (DeQueue(Q, next_id)) { win_id[w] next_id; cust[next_id].win w; printf(%d 号顾客请到 %d 号窗口\n, next_id, w); } else { win_id[w] 0; /* 没人排队窗口置空 */ } cust[id].win -1; /* -1 已离开防止重复结算 */ return stay; }cust[id].win 0这个判断挡掉了两类错误一是用户在排队阶段就点了离开二是同一个人被点了两次离开。没有这个判断同一个人会被结算两次total_stay虚高平均值直接被拉偏——而且这种偏差在演示时完全看不出来只有对账才会发现。补位这一步的顺序也有讲究必须先取出队头再改窗口表。如果先win_id[w] 0再出队中间那一瞬间窗口是空闲的但队列里还有人虽然程序跑得飞快看不出来但如果哪天加了多线程或者中断就是一个典型的竞态。4.3 平均逗留时间公式一行坑在后面平均逗留时间的定义很朴素平均逗留时间 Σ(每位已离开顾客的离开时刻 - 到达时刻) / 已离开顾客人数落到代码里就是两个累计量total_stay和done_cnt输出时相除。原始版本写的是allsum A[0]而A[0]在整个程序里从没被赋过值恒等于 0——所以无论怎么操作平均值永远是 0.00。这个 bug 在演示时毫无破绽因为屏幕上确实打印了“平均用时 0.00”看起来像是“今天还没人办完”。void ShowAdmin(void) { double avg 0.0; int i, busy 0; for (i 1; i WIN; i) if (win_id[i] ! 0) busy; if (done_cnt 0) avg (double)total_stay / done_cnt; printf(已完成业务%d 人\n, done_cnt); printf(当前在办%d 人等候区%d 人\n, busy, QueueLen(Q)); printf(累计逗留时间%d 秒\n, total_stay); printf(平均逗留时间%.2f 秒\n, avg); }三个细节值得写进报告done_cnt为 0 时必须单独处理否则除零total_stay用int累加秒数一天 86400 秒量级完全够用不必上double输出用%.2f因为秒级时间戳本身精度就到秒多打几位是假精度。如果想让数据更有说服力可以再加分类型统计VIP 平均逗留 vs 普通平均逗留。两组数字放在一起VIP 通道的价值立刻就能用数据说明而不是靠嘴讲。int vip_stay, vip_done, norm_stay, norm_done; /* 分流统计 */ /* 在 Leave 里结算时按类型分别累加 */ if (cust[id].is_vip) { vip_stay stay; vip_done; } else { norm_stay stay; norm_done; }4.4 柜台评分五个变量变成两个数组原始版本给 1~5 号柜台各写了sum1..sum5、i1..i5、ave1..ave5十五个全局变量switch里五个几乎一模一样的分支。这种写法的问题不是丑是改不动——窗口数从 3 改成 5 要动十几处改漏一处就是脏数据。float score_sum[WIN 1]; /* score_sum[w]w 号柜台评分总和 */ int score_cnt[WIN 1]; /* score_cnt[w]w 号柜台被评分次数 */ void Score(int w, int s) { if (w 1 || w WIN) { printf(柜台号不存在请重新输入\n); return; } if (s 1 || s 5) { printf(评分必须在 1 到 5 之间\n); return; } score_sum[w] s; score_cnt[w]; printf(%d 号柜台平均满意度 %.2f共 %d 次评价\n, w, score_sum[w] / score_cnt[w], score_cnt[w]); }校验边界写成w WIN而不是w 5这是判断“校验条件和实际资源是否对齐”的第一道关口。原始版本的guitai()提示写“请输入 1-3 号柜台”校验条件却写成y 1 || y 5用户填 4 能过校验然后落进switch的case 4给一个根本不存在的柜台记了一笔评分。演示时不明显对账时五个柜台的平均分加起来除不尽才发现多出来一个幽灵柜台。5. 避坑与排查五个让实验报告直接扣分的实现问题下面这几条都是这类系统里反复出现的现象、原因、解决一条条列清楚验收前逐条过一遍。5.1 平均逗留时间永远是 0.00现象不管怎么操作系统查询界面里的“平均用时”始终打印 0.00但累计时间和人数看起来都在涨。原因结算时写的是allsum A[0]而A[]这个数组在程序里只被初始化为全 0后续没有任何一处给它赋值。真正含时间信息的是顾客的到达时刻但它从来没被保存过——顾客编号在数组和链表之间流转时只带了编号本身。解决给顾客建档案表在到达时写入time(NULL)离开时用当前时间减到达时间得到单次逗留累加到total_stay最后除以done_cnt。同时在ShowAdmin里对done_cnt 0做保护。改完之后用“到达 1 人 → 等 5 秒 → 离开”这个最小用例验证平均值应该落在 5 秒上下对不上就是时间戳没打上。5.2 VIP 密码输到第五位程序直接崩现象VIP 认证界面提示“请输入五位密码”输满五位时程序闪退或者输出乱码。原因缓冲区声明成char ch[3]只够放两个字符加一个结束符而循环里ch[k] getch(); k;一直写到用户按空格为止五位密码要写 5 个字节直接越过数组边界踩到相邻变量。解决按最大可能的密码长度声明并显式限制下标。密码是 5 位就写char ch[6]循环条件加k 5char ch[6]; /* 5 位密码 1 个结束符 */ int k 0; while (k 5) { char c (char)getch(); if (c \r || c \n) break; /* 回车结束输入 */ ch[k] c; printf(*); /* 回显星号不显示明文 */ } ch[k] \0;顺带一提getch()输入密码不回显是对的但必须自己补上printf(*)否则用户完全不知道有没有输进去体验极差。5.3 柜台号填 4 也能通过校验现象只有 3 个窗口但离开时填 4 号柜台照样能提交评分系统还煞有介事地打印“4 号柜台平均满意度”。查统计时发现多出一个不该存在的柜台。原因校验函数里的上界写死成 5if (y 1 || y 5)与实际窗口数#define n 3脱节。提示语里写的是 1-3代码里放行到 5两处各改各的谁也没跟谁对齐。解决所有涉及窗口数量的地方统一用宏WIN校验写w 1 || w WIN数组下标也写[WIN 1]。改窗口数只需要动#define一行。验收时专门测一次“填 4”和“填 0”两个都必须被挡住。5.4 换台机器编译就报unknown type name Lnode现象在自己机器上跑得好好的拷到实验室机器上编译报Lnode类型未知一行行红字往上滚。原因结构体定义写成了struct Lnode { int data; Lnode *next; };。C 语言里结构体内部引用自身必须写完整的struct Lnode *因为此时typedef还没生效甚至连typedef都没写。部分编译器的扩展能容忍这种写法换一个严格一点的环境立刻原形毕露。解决统一写struct Lnode *next;或者把typedef拆成两步typedef struct Lnode Lnode; /* 先声明别名 */ struct Lnode { /* 再定义结构体 */ int data; Lnode *next; /* 此时别名已生效可以简写 */ };第二种写法在报告里更漂亮也更容易讲清楚“为什么这里能简写”。5.5 输错一次菜单就自己连环刷新现象菜单提示“请输入”手滑打了个字母屏幕开始疯狂刷菜单CtrlC 才停得下来。原因scanf(%d, choice)遇到非数字会立即返回 0并且把那个字符留在输入缓冲区里。switch走到default打印一句提示循环回到开头scanf又读到同一个字符又失败——死循环。如果代码里还混用了getch()两者对缓冲区的处理方式不同残留的回车会让下一次scanf读到空行菜单看起来“自己跳了一下”。解决所有scanf都判断返回值失败就清空缓冲区if (scanf(%d, choice) ! 1) { while (getchar() ! \n); /* 把这一行剩下的脏字符全吃掉 */ continue; }getch()和scanf尽量别在同一个输入流程里混用。要用就统一全用scanf或者全用getch 手动转数字。6. 验收与答辩用三组对照数据把系统讲透系统跑起来只是及格线能拿出一组自洽的对照数据才是真正的交付。答辩前我一般会准备三组场景每组都记录操作序列和期望输出现场照着点一遍。场景操作序列期望结果验证点空窗直上连续到达 3 人3 人分别落 1/2/3 号窗口队列为空窗口分配逻辑满窗排队到达 5 人前 3 人上窗口第 4、5 人进队列查询显示“前面 2 人”入队与计数补位结算到达 5 人 → 1 号窗口离开 → 查在办队头补进 1 号窗口在办仍为 3 人队列剩 1 人出队补位 逗留时间第三组要盯着秒表算从第一个人到达按下回车到点离开中间隔了多少秒系统打印的逗留时间应该和秒表差不超过 1 秒。这个手工对账动作比任何口头解释都有说服力。答辩时最常被问的三个问题提前把答案写进报告的“设计说明”一节讲的时候直接翻到那一页第一个是“为什么队列用链式不用顺序”。答案分两层人数上限未知链队列天然不受容量约束出队就是摘链头不需要移动元素也不需要处理循环队列那个“牺牲一个单元判满”的约定。第二个是“平均逗留时间的时间复杂度”。单次到达是 O(WIN)单次离开是 O(WIN) 找窗口加 O(1) 出队整体统计只做一次除法是 O(1)。如果非要抠FindFreeWin的线性扫描可以换成维护空闲窗口栈做到 O(1)但窗口数只有个位数这个优化没有实际意义——会被追问“为什么不做”答“收益小于复杂度成本”即可。第三个是“如果窗口数改成 n5 要改哪里”。正确答案是只改一处#define WIN 5其余代码全部自适应。如果能当场改完重新编译跑一遍这个问题的分量就彻底吃下了。从那以后我每次带这类课设都会在提交前强制走一遍“改一个宏、跑三组数据、秒表对一次账”的流程宁可多花二十分钟也不要在答辩现场被一句“这个平均值怎么是 0”问住。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。