☰
C语言链式队列实现银行排队系统:数据结构期末作业完整指南
2026/10/31 14:55:46 网站建设 项目流程

简介:这份数据结构期末作业资源面向计算机专业学生与C++初学者,围绕队列这一先进先出结构,实现带VIP优先级的银行排队系统模拟,帮助读者把课堂上的队列理论落到可运行的项目中。压缩包共9个文件,约1.03MB,以cpp源码、txt数据文件、cbp工程配置与exe可执行程序为主,另含编译中间文件与项目布局信息,构成一个可直接打开调试的Code::Blocks工程。已有3369人学习下载,说明其在同类课程作业中具有较高的参考价值。读者可从中获得双队列优先级调度的完整实现思路、用户信息文件的读取与解析方式、STL queue容器的实际用法,以及从源码到可执行文件的工程组织范例,便于对照理解多队列设计与控制流程逻辑,也可作为课程设计或期末提交的参考模板。

1. 银行排队系统:一份能直接跑通的数据结构期末作业

期末周前一周,隔壁宿舍的 A 同学抱着笔记本过来,说数据结构作业选了「银行排队系统」,结果网上搜到的代码要么跑不起来,要么只有一堆链表操作函数,连个像样的排队流程都没有。这其实是这类作业的通病:题目本身不难,难在把队列、链表、随机事件模拟这几块拼成一个能演示、能讲清楚、还能应付答辩的完整程序。这份银行排队系统作业就是冲着这个场景做的——它把客户到达、取号、多窗口叫号、等待时间统计串成一条完整链路,适合正在赶数据结构期末作业、需要一份能编译能跑、结构清晰可改的参考实现的同学。下面我按「它是什么、怎么跑、核心代码长什么样、怎么改」的顺序拆一遍,你拿到源码后照着走,基本不会翻车。

2. 项目结构与技术选型:为什么用 C 语言加链式队列

2.1 整体模块划分

这份作业的代码量不大,但模块切得比较干净,常见做法是分成四个部分:客户实体与队列结构定义、队列基本操作(入队、出队、判空、取队头)、银行模拟主循环(时间推进、随机到达、窗口服务)、统计输出(平均等待时间、最大等待时间、各窗口服务人数)。目录上一般长这样:

bank_queue/ ├── main.c # 程序入口,菜单与模拟调度 ├── queue.h # 队列结构与函数声明 ├── queue.c # 链式队列的入队/出队/判空实现 ├── customer.h # 客户结构体(编号、到达时间、开始服务时间) ├── simulate.c # 银行排队模拟核心逻辑 └── Makefile # 一键编译

这种拆法的好处是答辩时能指着文件说清楚「哪块是数据结构、哪块是业务模拟」,老师一问队列实现在哪,直接翻queue.c就行。如果你们课程要求单文件提交,把.c内容按顺序拼进main.c、头文件声明合并即可,逻辑不用动。

2.2 为什么选链式队列而不是数组队列

银行排队有个特点:客户数量事先不确定,高峰期可能一下来十几个人。数组队列要么开小了溢出,要么开大了浪费,还得处理循环队列的取模边界,对期末作业来说反而增加出错点。链式队列按需申请节点,入队出队都是 O(1),代码也短。常见做法是带头结点,这样入队和出队不用单独判断空队列,少一类玄学 bug。

// queue.h —— 链式队列结构定义 typedef struct Customer { int id; // 客户编号,按到达顺序递增 int arrive_time; // 到达时刻(单位:分钟) int start_time; // 开始接受服务的时刻 } Customer; typedef struct QNode { Customer data; // 数据域:一个客户 struct QNode *next; // 指针域:指向下一个排队客户 } QNode; typedef struct { QNode *front; // 队头指针,指向头结点 QNode *rear; // 队尾指针,指向最后一个客户节点 } LinkQueue;

front指向头结点而不是第一个客户,这是带头结点队列的关键约定;rear始终指向最后一个真实客户节点。入队时挂在rear后面再更新rear,出队时摘掉front->next。参数上arrive_time和start_time都用整型分钟,模拟时按分钟推进,避免浮点比较带来的精度问题。

2.3 编译环境与依赖

纯 C 标准库,不依赖第三方包,gcc 或 clang 都能编。Windows 下用 MinGW 或 Dev-C++ 也行,注意源码文件编码统一成 UTF-8,否则中文菜单在控制台可能乱码。Makefile 里一般就两条规则:

CC = gcc CFLAGS = -Wall -g -std=c99 bank: main.o queue.o simulate.o $(CC) $(CFLAGS) -o bank main.o queue.o simulate.o clean: rm -f *.o bank

-Wall一定要开,链式结构最容易出的就是「用了未初始化指针」这类警告,编译期拦下来比运行期段错误好查得多。-std=c99是为了用//注释和循环内声明变量,老编译器不加这个可能报错。

3. 核心模块拆解:队列操作与模拟主循环

3.1 链式队列入队与出队

队列是这份作业的数据结构骨架,入队出队写不对,后面模拟全是错的。带头结点的实现如下:

// queue.c —— 初始化、入队、出队 void InitQueue(LinkQueue *q) { q->front = q->rear = (QNode *)malloc(sizeof(QNode)); q->front->next = NULL; // 头结点不存数据 } int EnQueue(LinkQueue *q, Customer c) { QNode *p = (QNode *)malloc(sizeof(QNode)); if (!p) return 0; // 内存申请失败 p->data = c; p->next = NULL; q->rear->next = p; // 挂到队尾 q->rear = p; // 更新队尾指针 return 1; } int DeQueue(LinkQueue *q, Customer *c) { if (q->front == q->rear) return 0; // 空队列 QNode *p = q->front->next; // 第一个真实客户 *c = p->data; q->front->next = p->next; // 摘链 if (q->rear == p) q->rear = q->front; // 出队的是最后一个节点 free(p); return 1; }

DeQueue里那句if (q->rear == p)是血泪经验:如果出队的是队列里唯一一个客户,rear会变成野指针,下次入队直接崩。很多网上抄来的版本漏了这句,跑小数据看不出来,一跑长时间模拟就段错误。EnQueue返回 0/1 表示成功失败,调用方要判断,别默认一定成功。

3.2 银行模拟主循环:时间推进与随机到达

模拟的核心是「按分钟推进时钟」,每一分钟做三件事:按概率生成新客户、检查空闲窗口叫号、累计等待时间。常见做法是用rand()配合到达概率控制客流强度:

// simulate.c —— 单分钟模拟步进 #define WIN_NUM 3 // 窗口数量 #define ARRIVE_PROB 0.3 // 每分钟到达一个客户的概率 void SimulateStep(LinkQueue *q, int now, int win_free[], int *served) { // 1. 随机到达:概率命中则生成客户入队 if ((rand() % 100) < ARRIVE_PROB * 100) { Customer c; c.id = ++(*served); // 用已服务计数兼作编号会乱,实际用独立计数器 c.arrive_time = now; c.start_time = -1; EnQueue(q, c); } // 2. 有空窗口且有人排队,则叫号 for (int i = 0; i < WIN_NUM; i++) { if (win_free[i] == 0 && q->front != q->rear) { Customer c; DeQueue(q, &c); c.start_time = now; win_free[i] = 1 + rand() % 5; // 服务耗时 1~5 分钟 // 此处记录该客户的等待时间 now - c.arrive_time } } // 3. 所有忙碌窗口剩余时间减一 for (int i = 0; i < WIN_NUM; i++) if (win_free[i] > 0) win_free[i]--; }

win_free[i]存的是该窗口剩余服务分钟数,0 表示空闲,大于 0 表示还要忙几分钟。ARRIVE_PROB调大就是高峰期,调小就是闲时,答辩时现场改参数演示不同客流,比干讲有说服力。注意客户编号别用served兼作,服务计数和到达编号是两回事,混用会导致编号跳号,这是踩过的坑。

3.3 统计输出:平均等待时间怎么算

作业要求一般会问「平均等待时间」「最长等待时间」「各窗口服务人数」。等待时间在客户开始服务那一刻就能算出来:start_time - arrive_time。常见做法是在出队叫号时累加到一个全局统计结构里:

// 统计结构 typedef struct { int total_wait; // 累计等待分钟 int max_wait; // 最长等待 int count; // 已服务人数 int per_win[WIN_NUM]; // 各窗口服务人数 } Stat;

出队时stat.total_wait += now - c.arrive_time;,同时更新max_wait和per_win[i]。最后平均等待就是total_wait / count,注意count为 0 时要单独处理,否则除零直接崩。输出格式建议对齐成表格,答辩演示时一目了然。

4. 避坑与常见问题:跑不起来先看这几条

4.1 编译通过但运行直接段错误

现象:程序启动后输入菜单选项就崩。原因多半是队列没初始化就入队,front和rear是野指针。解决:main里第一件事就是InitQueue(&q),所有队列操作前确认已初始化。另外检查malloc返回值,虽然小作业很少失败,但养成判断习惯没坏处。

4.2 模拟跑完统计全是 0

现象:循环结束,平均等待时间打印 0。原因通常是统计变量没在出队分支里累加,或者count一直没自增。解决:在DeQueue成功返回后立刻更新统计,别放在窗口循环外面,否则叫号了但没统计。调试时可以在每次出队后打印一行id / arrive / start,肉眼核对。

4.3 中文菜单乱码

现象:Windows 控制台输出中文变问号或方块。原因:源码 UTF-8 而控制台默认 GBK。解决:要么把源码另存为 GBK,要么在main开头加system("chcp 65001");切到 UTF-8 代码页。Linux 和 macOS 终端一般没这问题,跨平台提交前在目标环境跑一遍。

4.4 随机结果每次一样

现象:多次运行统计数字完全相同。原因:没调srand,rand()默认种子固定。解决:main开头srand((unsigned)time(NULL));,记得#include <time.h>。如果老师要求结果可复现,可以改成固定种子并在注释里说明,答辩时讲清楚这是有意为之。

4.5 长时间模拟后内存持续增长

现象:跑几万分钟模拟,内存占用一直涨。原因:出队时free漏了,或者异常分支提前return没释放节点。解决:DeQueue里摘链后必须free(p),程序结束前写个DestroyQueue把头结点和残留节点全释放。用valgrind跑一遍,泄漏点一目了然。

5. 二次开发与改造思路:从作业到能演示的小系统

拿到这份代码后,最值得改的有三处,改完既能加分又不至于推翻重来。第一处是把「固定窗口数」改成运行时输入,WIN_NUM从宏改成变量,窗口状态数组用malloc动态分配,这样答辩时能现场演示 2 个窗口和 5 个窗口的排队差异。第二处是加一个「VIP 客户」插队逻辑,用两个队列分别存普通客户和 VIP,叫号时优先取 VIP 队列,这正好把「多队列调度」讲清楚,是数据结构课的高频加分点。第三处是把统计结果导出成 CSV,用fprintf按行写id,arrive,start,wait,再用表格软件画个等待时间分布图,演示效果直接上一个档次。

改造时我一般会先写一个最小验证:改完窗口数后,手动构造 3 个客户、2 个窗口的固定场景,把每一步的入队出队打印出来,和纸上推演对一遍,确认队列逻辑没被改坏,再放开随机模拟。下面这个片段是动态窗口初始化的常见写法:

// 按运行时窗口数分配状态数组 int win_num; printf("请输入窗口数量: "); scanf("%d", &win_num); int *win_free = (int *)calloc(win_num, sizeof(int)); // 全 0 表示空闲 // ... 模拟结束后 free(win_free);

calloc比malloc省事,直接把剩余服务时间清零。改完记得在所有提前退出的分支里也free,不然又是一处泄漏。

还有一个容易被忽略的点:客户编号和到达顺序要解耦。我见过不少改版把编号直接绑在循环变量上,加了 VIP 队列后编号就乱了,统计对不上。正确做法是维护一个独立的next_id计数器,每次生成客户自增,跟服务顺序无关。从那以后我每次改这类模拟程序,都强制先跑一遍「固定种子 + 小数据 + 逐步打印」的回归,确认队列和统计两条线都对得上,再放开随机参数。希望这份拆解能帮你把作业顺利跑起来,也留出足够的改造空间。

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

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

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

立即咨询