☰
C语言银行排队模拟系统:链队列、平均逗留时间与VIP分流实现
2026/10/27 14:34:23 网站建设 项目流程

简介:这份银行排队系统实验报告面向学习数据结构与C语言编程的本科生及实验课程学习者,围绕多窗口银行排队场景展开,要求模拟客户随机到达、按人数选择窗口排队,并统计一天中客户平均逗留时间,帮助读者把队列结构、时间处理与随机事件生成落实到可运行程序中。压缩包内共1个doc文件,大小约191KB,主要内容包括设计要求、实验目的、实验仪器与编译环境说明,以及设计思路、程序分析和主菜单、顾客到达、顾客离开、业务查询、排队查询、系统查询等模块的实现讲解,并附有关键源代码片段与运行界面说明。已有902人学习下载,适合作为课程实验参考、课程设计底稿或C语言综合练习材料,读者可据此理解队列初始化、入队出队、VIP与普通用户分流、平均等待时间计算等思路,并对照VC++ 6.0环境完成调试与功能扩展。

1. 从三个窗口到一天的平均等待:这套排队模拟到底要交付什么

三个窗口、一整天不断进来的客户、每个人办业务耗时还都不一样——银行排队系统实验报告这类课程设计,最容易写成一份只会打印菜单的实现:菜单能点、编号能加,等到要算「客户平均逗留时间」时,数据却怎么也对不上。这份方案要解决的正是这个落差。它要交付的不是一个花哨界面,而是一套能量化回答「客户平均等了多久、哪个柜台服务评价最高、VIP 通道有没有真正分流、系统跑了多久」的排队模拟程序。适合正在做数据结构课设、需要交一份完整实验报告的同学,也适合拿它当离散事件仿真入门练手的从业者。下面按需求、选型、模块、实现、排查、验收的顺序,把这份 C 语言实现的排队模拟完整拆一遍。

2. 需求拆解与数据结构选型:数组、链队列和一张事件表怎么分工

需求写起来只有一句话:有 n 个窗口对外接待,客户陆续进入,人多时要排队,最后算出一天中客户在银行逗留的平均时间。真动手才发现这句话里藏着三个必须提前定死的东西——窗口状态怎么存、等候队伍怎么存、时间从哪一刻算到哪一刻。这三件事定不下来,后面写的每个函数都在猜,改一处崩三处。选型上我倾向于「窗口用顺序表、队伍用链队列、时间戳跟着编号一起走」,理由是窗口数量固定且要按编号随机访问,队伍人数不定且只在两端进出,两类数据的访问模式正好相反,硬塞进同一种结构里迟早别扭。

2.1 三个窗口的状态用顺序表存,等候队伍用链队列接

窗口表的本质是一张「谁在哪个窗口」的映射。数量固定为 n,业务上需要频繁按窗口号定位(客户离开时要说清是几号窗口空出来了),所以顺序表是最省事的选择:下标即窗口号,值存当前在该窗口办理业务的顾客编号,0 表示空闲。用一个 len 字段记录当前占用的窗口数,比每次遍历数组去数非零元素要快,也让后面判断「窗口满没满」变成一次整数比较。

#define WINDOW_N 3 /* 窗口数量,改这一个宏就能改实验规模 */ typedef struct { int A[WINDOW_N + 1]; /* 下标 1..WINDOW_N 对应窗口号,值存顾客编号,0 表示空闲 */ int len; /* 当前正在办理业务的顾客数,注意它不是顾客编号 */ } WindowTable; WindowTable L; void InitWindowTable(void) { int i; for (i = 1; i <= WINDOW_N; i++) { L.A[i] = 0; /* 全部置空闲 */ } L.len = 0; }

这段初始化的关键在于「下标从 1 开始」。很多同学图省事用 0 号下标当第一个窗口,结果客户编号也从 0 开始发,两个 0 混在一起,离开事件永远找不到该清哪个位置。窗口号用 1..n、顾客编号从 1 开始递增、0 专门留给「空」,三套编号各管各的,日志打印出来一眼就能看懂。WINDOW_N 单独抽成宏,是为了后面做对比实验:窗口从 3 改成 5,只需要改这一行,不用去翻所有循环。

等候队伍则用带头结点的链队列。带头结点不是为了炫技,是为了让「空队列」和「只有一个元素的队列」在入队出队时代码完全一致,不用写 if 特判头指针。队列结点里除了顾客编号,还要带上到达时刻,这一点在下一节展开。

typedef struct Lnode { int data; /* 顾客编号 */ time_t arrive_at; /* 该顾客进入系统(入队或占窗口)的时刻 */ struct Lnode *next; } Lnode; typedef struct { Lnode *front; /* 头指针,指向头结点,不存业务数据 */ Lnode *rear; /* 尾指针,指向最后一个结点 */ } LinkQueue; LinkQueue Q; void InitQueue(LinkQueue *q) { q->front = q->rear = (Lnode *)malloc(sizeof(Lnode)); q->front->next = NULL; /* 头结点的 next 为空即队列为空 */ }

队列里存 time_t 而非 int,是因为逗留时间要用秒级差值算,用 time() 取一次再 difftime 相减最省心。如果只存编号,离开时就得回头去翻「这个编号是什么时候进来的」,等于把已经算过的时间又丢掉了。

2.2 事件与时间:逗留时间的口径先定死再写代码

「客户在银行逗留的平均时间」这句话,落到代码上有三种口径:从进大门到出大门、从开始排队到办完、从到窗口到办完。实验报告里必须自己选定一种并写清楚,否则算出来的数没法解释。我一般取第一种——客户进入系统的那一刻打时间戳,离开系统时取差值,这个口径最贴近「客户体验」这个词,也最容易在答辩时用一句话讲明白。

原稿里时间是用一个全局 start/end 配合 difftime 算的,问题在于这两个变量从头到尾没有被赋过值,difftime 拿到的是两个未初始化的内存,结果自然是 0。修正思路很直接:把时间戳挂到顾客身上,跟着他走完全程。

#include <time.h> /* 顾客到达:打时间戳后决定是直接占窗口还是进队列 */ void CustomerArrive(WindowTable *t, LinkQueue *q, int id) { time_t now = time(NULL); if (t->len < WINDOW_N) { /* 还有空窗口,直接办理 */ int pos = t->len + 1; t->A[pos] = id; t->len++; printf("顾客 %d 于窗口 %d 开始办理\n", id, pos); /* 直接占窗口的到达时间也要记,否则算不出他的逗留时长 */ record_arrive_time(id, now); } else { /* 窗口已满,进队等候 */ Lnode *s = (Lnode *)malloc(sizeof(Lnode)); s->data = id; s->arrive_at = now; s->next = NULL; q->rear->next = s; q->rear = s; printf("顾客 %d 进入等候队列\n", id); } }

这里有个容易被忽略的细节:直接占窗口的顾客也要记录到达时间。原稿只给排队的人记了时间,导致「不用排队的客户」逗留时间无从计算,平均值永远偏低。record_arrive_time 可以用一张按编号索引的数组实现,编号做下标,值为 time_t,编号范围可控(一天几百人),数组比哈希表省事得多。

排队顾客的时间戳存在结点里,离开时先出队拿到结点,再 difftime(now, s->arrive_at) 就是他在系统里待的秒数。把这个数累加进 allsum,离开人数累加进 count,平均逗留时间就是两者相除。口径一旦这么定下来,后面每个函数只需要遵守「进来打戳、出去算差、差值入账」这一条规矩,统计就不会乱。

2.3 主菜单背后的状态机:五个可见入口加一个隐藏入口

菜单看着只是 printf 加 switch,实际是整个系统的状态机入口。可见的五个选项分别对应到达、离开、查办理、查排队、退出,第六个「系统查询」按设计是内部人员使用,所以不在菜单上列出来,但分支照样留着——这种「隐藏入口」在课设里很常见,答辩时能讲出「权限分离」的设计意图,比单纯多一个功能更容易得分。

入口动作主要调用函数状态变化
1顾客到达vip / CustomerArrive窗口表或队列增长
2顾客离开likai / mygrade窗口表释放并补位,评分累加
3查看业务办理printl只读
4查看排队情况print2只读
5退出return结束主循环
6(隐藏)系统查询卡号密码校验 + 统计输出只读

主循环写成 while(1) 加 switch,每个分支处理完清屏回到菜单,这是控制台程序的常规写法。真正决定程序好不好用的是分支里的顺序:到达分支必须先判断用户类型再决定走 VIP 认证还是普通取号;离开分支必须先校验编号再动数据结构,否则一次误输入就能把窗口表改乱。

int choice; while (1) { ShowMenu(); /* 打印菜单并显示当前时刻 */ if (scanf("%d", &choice) != 1) { /* 输入非数字时清掉缓冲区,避免死循环 */ while (getchar() != '\n'); continue; } switch (choice) { case 1: HandleArrive(); break; case 2: HandleLeave(); break; case 3: printl(); break; case 4: print2(); break; case 5: return; case 6: HandleAdminQuery(); break; default: printf("输入有误,请重新选择\n"); } }

菜单里还要顺手打印当前时刻,原稿用 ctime 做这件事,作用是让操作者知道自己处在「一天的哪个位置」。这个细节对演示很友好:答辩时连续输入几次到达、离开,屏幕上的时间在走,评委能直观感受到这是个动态系统而不是静态表格。

3. 总体架构与模块划分:一个主循环、七个函数和一份全局状态

整套程序的结构可以概括为「一份全局状态 + 七个职责单一的函数 + 一个事件循环」。全局状态只有窗口表 L、链队列 Q、以及评分相关的几个累加器;七个函数分别是初始化、入队、出队、到达处理、离开处理、两个查询输出。初学课设最容易犯的错是把所有逻辑塞进 main,一个 switch 分支写八十行,改一个判断要在几百行里找。按职责切开的直接好处是每个函数都能单独测:先测入队出队对不对,再测到达离开,最后才拼菜单。

3.1 全局数据结构定义:窗口表 L、链队列 Q 与评分累加器

全局变量的取舍在课设里值得说一句。窗口表和队列作为系统的核心状态,全局声明能让七个函数都直接访问,省掉层层传参;代价是可测试性变差。折中做法是把「结构体本身」全局,把「操作」写成接收指针的函数,这样将来想搬到局部变量也只改初始化那几行。

/* ---- 系统核心状态 ---- */ WindowTable L; /* 窗口表 */ LinkQueue Q; /* 等候队列 */ /* ---- 统计量 ---- */ int normal_count = 0; /* 已服务的普通客户数 */ int vip_count = 0; /* 已服务的 VIP 客户数 */ int served_count = 0; /* 已离开客户总数,做平均时分母 */ double total_stay = 0.0; /* 所有客户逗留秒数之和 */ /* ---- 评分:按柜台号索引,1..5 号柜台 ---- */ double score_sum[6] = {0}; int score_cnt[6] = {0};

评分数组开到 6 而不是 5,同样是给 1 号下标留位置。用数组替代原稿里的 sum1..sum5、ave1..ave5 十个散装变量,好处是评分函数里那句 switch(z) 直接消失,变成一行 score_sum[z] += y。柜台从 3 个扩到 5 个、10 个,代码一行都不用改。这种「把重复变量收进数组」的改法是课设里性价比最高的重构,评委问「如果柜台数量变了怎么办」,你只需要指一下宏定义。

3.2 到达事件落地:daoda 与 Enqueue 的边界条件

到达事件要处理的分支只有两条:有空窗口就直接占,没空窗口就排队。听起来简单,边界却集中在两个地方——窗口表满没满的判断,以及队列为空时入队会不会把 rear 弄丢。

/* 到达事件:窗口有空则直接办理,否则入队等候 */ void daoda(int id) { if (L.len < WINDOW_N) { int pos = L.len + 1; /* 顺序表按 len 增长,天然不跳号 */ L.A[pos] = id; L.len++; PrintServing(id, pos); } else { Enqueue(&Q, id, time(NULL)); /* 时间戳随结点一起入队 */ } } void Enqueue(LinkQueue *q, int id, time_t t) { Lnode *s = (Lnode *)malloc(sizeof(Lnode)); if (s == NULL) { /* 内存分配失败必须有出口,不能直接解引用 */ printf("内存不足,入队失败\n"); return; } s->data = id; s->arrive_at = t; s->next = NULL; q->rear->next = s; /* 挂到队尾 */ q->rear = s; /* 尾指针后移,这两行的顺序不能反 */ }

参数 id 是顾客编号,由主循环里一个自增计数器发放,VIP 和普通客户共用同一套编号,便于后续查询和离开时定位。t 是到达时刻,只在入队时由调用方传入 time(NULL),函数内部不再取时间——这样测试时可以手动传一个固定时刻,验证算出来的逗留时间是否等于预期,不然每次跑出来的数都不一样,没法做回归。

这里的顺序陷阱值得单独提醒:先 q->rear->next = s 再 q->rear = s,反过来的话尾指针先指向新结点、新结点的 next 还没接上,队列就断了。这类指针顺序问题在实验室里肉眼看不出来,程序要等到第二次入队才崩,排查成本极高。

3.3 离开事件落地:likai 与 Dlqueue 的补位逻辑

离开事件是整个系统里逻辑密度最高的一段,因为它同时要做三件事:从窗口表里清掉这个人、如果队列非空就让队首补位、更新统计量。原稿的 likai 用「输入编号 x,然后在窗口表里找等于 x 的位置」的方式定位,问题在于它拿顾客编号当窗口下标用,先判断 x > L.len 就报错——L.len 是当前占用窗口数,不是顾客编号的最大值,第三个顾客在 2 号窗口办业务、len 是 2,输入 3 就被判成非法输入,直接卡死。

正确的做法是把「编号 → 位置」的查找和「位置 → 补位」分开写:

/* 离开事件:按顾客编号找到所在窗口,清位后让队首补上 */ void likai(int id) { int pos = -1, i; for (i = 1; i <= WINDOW_N; i++) { /* 在窗口表里找这个编号 */ if (L.A[i] == id) { pos = i; break; } } if (pos == -1) { /* 找不到说明编号输错或已离开 */ printf("未找到编号为 %d 的顾客,请重新输入\n", id); return; } L.A[pos] = 0; /* 先清位 */ L.len--; if (Q.front != Q.rear) { /* 队列非空,队首补位 */ int next = Dlqueue(&Q); L.A[pos] = next; L.len++; printf("顾客 %d 补位到窗口 %d\n", next, pos); } RecordDeparture(id); /* 统一更新统计量 */ }

这段代码有三个设计点。第一,查找与补位分离,找错了直接 return,不会把窗口表改到一半。第二,补位选择「队首补到刚空出的窗口」,这是最符合直觉的排队规则,也便于手算验收:3 个窗口满、队列里有 4、5、6,2 号窗口的人走了,进 2 号的一定是 4 号。第三,统计更新抽成 RecordDeparture 一个函数,所有离开都必须经过它,避免出现「有的分支加了、有的分支忘了」这种账目不平。

Dlqueue 出队时先取 t = front->next,再 front->next = t->next,最后 free(t)。取数据要在 free 之前,free 之后再读就是野指针,这个错误在控制台程序里有时能跑通、有时打印出乱码,属于典型的「玄学 bug」,源码级排查最快的方式是把 free 挪到函数最后一行。

3.4 查询模块:printl 与 print2 的输出契约

两个查询函数的输出格式要提前约定,因为它们是给使用者看的,也是答辩演示时最直观的部分。printl 遍历窗口表打印正在办理的顾客编号,遇到 0 就跳过或者打印「空闲」;print2 从队首走到队尾打印排队编号,并给出总人数。这两段代码本身很短,但输出契约值得写清楚:编号之间用什么分隔、空闲窗口显示什么、队列空时提示什么。

void printl(void) { int i; printf("正在办理业务的顾客编号:"); for (i = 1; i <= WINDOW_N; i++) { if (L.A[i] == 0) printf("[窗口%d 空闲] ", i); else printf("[窗口%d:%d] ", i, L.A[i]); } printf("\n"); } void print2(void) { int cnt = 0; Lnode *p = Q.front->next; printf("正在等候办理业务的顾客编号:"); while (p != NULL) { printf("%d ", p->data); p = p->next; cnt++; } printf("\n您的前面一共有 %d 人在排队\n", cnt); }

把窗口号和顾客编号一起打印出来,看起来比只印一串数字啰嗦,实际排查时省掉大量猜测:到达和离开连续操作三五次之后,光看编号列表根本不知道自己改的是哪个窗口。这种「输出带上下文」的习惯,在做演示型课设时回报很高,评委不用问「现在几个窗口在忙」,屏幕上写着。

4. 关键算法实现:VIP 双因子认证、满意度滚动平均与隐藏统计通道

有三个模块决定了这份课设「像不像真的银行系统」:VIP 客户的身份校验、柜台满意度评分、以及给内部人员看的统计查询。它们共同的特点是「输入不可控」——用户可能输错卡号、输错密码、输超范围的分数,代码必须有兜底路径。这三个模块也最容易出细节问题,因为涉及字符输入、循环计数和除法分母,任何一个没照顾到,程序就在演示现场卡住或打印出 0.00。

4.1 VIP 认证怎么写才不会被密码长度坑到

原稿的做法是用 getch() 逐字符读密码、边读边打星号,卡号和密码一一对应写死五个分支。思路没问题,坑在循环条件:while(ch[k-1] != ' ') 用空格当结束符,但用户输入密码时根本不会敲空格,k 会一路越界,写出数组之外;而提示语说「输入五位密码」,实际比较只比较了 ch[0] 到 ch[3] 四位。这种提示与实现不一致,在答辩时被追问一句「你到底几位密码」就很尴尬。

我的写法是固定长度读指定个数,读完就停,不依赖结束符:

#define PWD_LEN 4 #define VIP_NUM 5 /* VIP 卡号表与对应密码,数组化后增删卡号只改这两行 */ static const int vip_card[VIP_NUM] = {100, 200, 300, 400, 500}; static const char vip_pwd[VIP_NUM][PWD_LEN + 1] = { "1111", "2222", "3333", "4444", "5555" }; int vip_auth(void) { int card, i, k; char pwd[PWD_LEN + 1]; printf("请输入卡号:"); scanf("%d", &card); printf("请输入 %d 位密码:", PWD_LEN); for (k = 0; k < PWD_LEN; k++) { pwd[k] = (char)getch(); /* 逐位读,不回显 */ printf("*"); } pwd[PWD_LEN] = '\0'; /* 手动补串尾,否则后面的 strcmp 会越界 */ for (i = 0; i < VIP_NUM; i++) { if (card == vip_card[i] && strcmp(pwd, vip_pwd[i]) == 0) { vip_count++; printf("\n尊敬的 VIP 用户,请到 VIP 区办理业务\n"); return 1; } } printf("\n卡号或密码有误\n"); return 0; }

参数上 PWD_LEN 和 VIP_NUM 都抽成宏,密码长度从 4 位改成 6 位只需要改一个数字,提示语里的「%d 位密码」会自动跟着变,不会出现提示和实现不一致的情况。循环用 k < PWD_LEN 而不是判断结束符,输入几位就读几位,读满就停,越界问题从根上消失。校验逻辑用 strcmp 比较补过 \0 的字符数组,比逐位比较 ch[0]=='1' && ch[1]=='1' 干净得多,加一个 VIP 卡号也只是在数组里加一项。

认证通过后 VIP 客户的处理有两种设计:一种是直接占用一个专用 VIP 窗口,一种是和普通客户共用窗口但插到队首。原稿只做了计数和提示,没有真的进入排队结构,导致「VIP 有没有真正分流」这个统计问题答不上来。我一般让 VIP 直接走 daoda,但在 printl 的输出里用编号前缀区分,这样既保持了数据结构的统一,又能在统计时把 vip_count 和 normal_count 分开报。

4.2 柜台满意度:五个累加器与滚动平均的取舍

评分模块的需求是:客户离开时选柜台号、给 1 到 5 分,系统输出该柜台当前的平均分。原稿用五组变量分别累加,switch 里五段几乎一样的代码。改成数组之后,逻辑收敛成一个函数,加柜台只要改宏:

/* 记录一次评分,柜台号 1..MAX_DESK */ void add_score(int desk, int score) { if (desk < 1 || desk > MAX_DESK) { printf("柜台号超出范围\n"); return; } if (score < 1 || score > 5) { printf("评分必须在 1 到 5 之间\n"); return; } score_sum[desk] += score; score_cnt[desk] += 1; printf("%d 号柜台当前平均满意度:%.2f\n", desk, score_sum[desk] / score_cnt[desk]); }

两个校验都放在赋值之前,非法输入直接返回、不污染累加器,这是评分类功能的关键——一旦把 0 分或者 7 分录进去,平均分被拉偏,后面所有输出都是错的。除法的分母 score_cnt[desk] 在通过校验后必然大于 0,不会出现除零,这个顺序不能调换。

输出用 %.2f 保留两位,和实验报告里抄录的运行结果一致,省得截图时数值对不上。平均分用「总分/次数」实时算,而不是每次更新 ave 变量,好处是不用担心 ave 和 sum、i 之间失去同步——三个变量维护同一个量,早晚会有一个忘记更新。

4.3 隐藏的系统查询入口与统计口径

系统查询是给内部人员看的,原稿的做法是卡号填 000、密码 1111,输对了才打印统计。这个设计在课设里挺加分,因为它体现了「功能可见性」的考虑。但原稿的密码读取同样有越界和比较长度不一致的问题,而且统计口径也含糊:allsum 累加的是 A[0],而 A 数组从头到尾没有被写入过,所以平均值恒为 0。

修正后统计输出集中在一个函数里,口径写死:

void HandleAdminQuery(void) { int card; char pwd[PWD_LEN + 1]; int k; printf("请输入管理员卡号:"); scanf("%d", &card); printf("请输入 %d 位密码:", PWD_LEN); for (k = 0; k < PWD_LEN; k++) { pwd[k] = (char)getch(); printf("*"); } pwd[PWD_LEN] = '\0'; if (card != 0 || strcmp(pwd, "1111") != 0) { printf("\n验证失败\n"); return; } printf("\n已服务普通客户 %d 位,VIP 客户 %d 位\n", normal_count, vip_count); printf("累计逗留时间 %.2f 秒\n", total_stay); printf("平均逗留时间 %.2f 秒\n", served_count > 0 ? total_stay / served_count : 0.0); printf("系统已运行 %.2f 秒\n", difftime(time(NULL), g_start_time)); }

三个统计口径在这里定死:served_count 只统计真正走完「到达—离开」流程的人,中途退出不计;total_stay 用秒为单位累加,避免分钟和秒混用;平均值的分母为 0 时输出 0.00 而不是崩溃,用三目运算兜底。g_start_time 在 main 开头取一次 time(NULL),系统运行时长就是当前时刻减它,这个值做演示很有用——跑十分钟,屏幕上写着「系统已运行 600 秒」,比说什么都有说服力。

5. 常见问题排查:这份排队模拟最容易翻车的五个点

课程设计交上去之前,最好自己先照着下面这几条过一遍。这些不是理论上的风险,而是这类程序实际运行起来真的会踩到的坑,每一条我都见过不止一次。

5.1 平均逗留时间永远是 0.00

现象是系统查询界面的平均值一直显示 0.00,无论操作多少次、等多久都不变。原因有两层:一是统计累加用错了变量,累加的是一个从未被赋值过的数组元素;二是离开分支里 w++ 与 allsum 的更新顺序错乱,有的路径加了计数没加时间。解决办法是把「记录到达时间—计算差值—累加」收进一个函数,离开事件无论从哪个分支走都必须调用它,累加和计数写成相邻两行,中间不插任何逻辑。

void RecordDeparture(int id) { time_t t0 = get_arrive_time(id); /* 从编号索引表里取回到达时刻 */ if (t0 == 0) return; /* 没记录过到达时间,直接跳过 */ total_stay += difftime(time(NULL), t0); if (get_is_vip(id)) vip_count++; else normal_count++; served_count++; /* 计数与累加必须成对出现 */ }

5.2 密码输到一半程序就卡死或者越界

现象是输入 VIP 密码时,敲完几位程序不响应,或者继续敲下去整个窗口卡住,回车之后又正常了。原因是密码读取用了一个不存在于输入流的结束符做循环条件,循环次数完全失控,写到了数组外面。解决方式是改成已知长度的定次循环,读满 PWD_LEN 位就退出,随后手动补 \0;同时把提示语里的位数也改成由宏计算,避免提示四位、比较五位这种前后不一致。

5.3 离开事件把顾客编号当成窗口下标

现象是明明屏幕上显示 3 号顾客在办理业务,输入 3 之后提示「输入有误」,或者更糟——清掉了一个不该清的窗口。根因是判断条件写成了编号与窗口占用数比较,两者根本不是同一个量。正确做法是先在窗口表里遍历查找编号所在的下标,找到再用这个下标操作,找不到就明确提示并返回,绝不带着 -1 或不存在的下标继续往下走。

5.4 窗口数量改了,代码却有一半还在按旧的写

现象是把窗口数从 3 改成 5,结果 4、5 号窗口永远空着,或者输入 4 号柜台评分提示超范围。原因是这类数字在代码里散落成好几份:数组大小一份、循环上界一份、提示语里「1-3」一份、范围校验又一份。解决方式是把窗口数和柜台数抽成宏,数组大小、循环上界、输入提示、范围校验全部引用宏,改一个地方全篇生效。提示语里写死的「1-3」尤其要注意,它是给人看的,最容易和代码脱节。

5.5 队列空的时候出队,程序直接退出

现象是最后一个排队客户离开后,程序不是回到菜单,而是整个消失,命令行提示异常结束。原因是出队函数遇到空队列时调用了强制退出,而离开事件在补位前只判断了队列「非空」却没考虑并发场景下的时序。解决方式是把出队设计成「返回成功与否」的形式,空队列返回 0 由调用方决定怎么处理,函数内部永远不主动终止程序。控制器决定程序生死,工具函数只管干活,这条边界划清楚,调试时会省很多事。

6. 验收与答辩:用一组手算数据证明你的排队模拟是对的

交报告前最该做的一件事,是准备一张能手工推演的测试表。程序跑出来的数对不对,靠眼睛看日志是看不出来的,必须事先手算出期望值,再和屏幕上的输出逐条比对。下面这张表是我常用的最小验证集,五个用例覆盖了窗口占满、排队、补位、认证和统计五条主路径,全部跑一遍不超过五分钟。

编号操作序列期望结果检查点
T1连续到达 3 个普通客户窗口 1、2、3 各一人,队列空查询界面显示三人、排队 0 人
T2第 4 个客户到达进入队列,前面 1 人排队界面显示该编号、人数为 1
T3让窗口 2 的客户离开队首补到窗口 2,队列清空窗口 2 的编号变成原队首编号
T4VIP 卡号 200 配密码 2222认证通过,VIP 计数加一系统查询显示 VIP 一人
T5某客户到达后停留约 60 秒再离开平均逗留时间接近 60 秒平均值在 55 到 65 之间可接受

顺序有讲究:T1 到 T3 必须连着做,因为它们验证的是同一个状态在三次操作后的演化;T4 单独做,避免 VIP 计数干扰普通客户计数;T5 放在最后,因为它需要一个相对干净的统计环境。手算期望值的时候,把窗口表按「窗口号 → 顾客编号」写下来,每次操作后更新这张纸,比在脑子里推演可靠得多。

答辩环节通常三个问题出场率最高,提前把答案组织好能省不少临场紧张。第一个是「你的时间复杂度和空间复杂度」,回答要点:到达和离开里查找窗口是 O(n),n 是窗口数,窗口数是个小常数,所以实际是 O(1);入队出队 O(1);查询排队 O(m),m 是队列长度;空间上窗口表 O(n)、队列 O(m),总空间与同时在场的客户数成正比。第二个是「为什么窗口用数组、队伍用链表」,答案是访问模式不同,一个要按位置随机访问、一个只在两端进出,换成另一种结构都会浪费。第三个是「如果客户到达是随机的,你怎么模拟」,答案是加一个随机间隔生成器,每个事件之间 sleep 一段随机秒数,或者把到达事件做成按概率触发,不影响现有的数据结构。

真要上台演示,我建议按「先跑 T1 到 T3 展示结构演化,再跑 T4 展示权限,最后开隐藏入口展示统计」的顺序走,全程不超过三分钟,屏幕上的每一次变化都能对应到一张数据结构图。

从那以后我每次带这类课设,都会先逼着自己手算三条用例再允许写代码——因为数据结构的题目,代码写错的地方往往一眼能看出来,账算错的地方却要等到答辩前一晚才发现。把验收表提前到动手之前,比事后补十页调试日志都管用。希望帮到你。

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

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

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

立即咨询