简介大二下数据结构课程设计——银行排队系统项目包面向正在学习栈、队列等线性结构的学生解决如何用数据结构模拟银行多窗口排队、VIP优先服务等实际问题。项目基于C实现包含主要源码、可执行程序、工程配置文件及说明文档压缩包共8个文件体积约334KB轻量易用。已有2865人浏览学习适合用于课程作业参考或课后练习。内容紧密结合课堂理论普通客户按到达顺序进入队列先进先出VIP客户通过栈的压入/弹出操作优先插队服务还可模拟多窗口分行处理动态调整服务策略。通过阅读源码和运行程序可以直观看到队列FIFO与栈LIFO在实际场景中的配合使用并进一步理解如何用程序实现数据结构的业务逻辑。资源文件类型以cpp、txt为主附带exe可执行文件与cbp工程文件便于直接运行和二次修改。1. 银行排队系统用栈实现先破掉“该用队列”的直觉大二下的数据结构作业“银行排队系统”乍看起来很矛盾排队明明是天经地义的先进先出题目却点名要你写“排队系统栈”。很多人第一版代码就是把客户挨个 push 进一个栈运行起来才发现最后取号的客户反而第一个被叫号当场翻车。问题不在栈写得不好而在模型没翻转栈是后进先出队列是先进先出两者在逻辑上是逆序关系。这道作业真正要练的是能不能用两个栈把 LIFO 硬掰成 FIFO。这篇笔记按建模、实现、避坑、验证的顺序展开最后给一个答辩时能多讲三分钟的封装写法。适合正在赶这份作业的在校生也适合复习栈和队列考点的考研人照着敲一遍。2. 把 LIFO 掰成 FIFO栈排队系统的模型翻转2.1 作业里“排队系统栈”到底在说什么“排队系统栈”不是一种新数据结构而是“用栈实现的排队系统”的缩写本质是数据结构的组合应用。老师给这道题的目的不是让你调库而是逼你回答一个问题容器本身不支持先进先出怎么用一组操作还原出先进先出的语义栈对外只暴露三个能力压入、弹出、看栈顶。这三个能力在时间轴上天生只回放最近的事件。想让历史事件按原顺序回放唯一办法是把已经逆序的数据再压进第二个栈里从头再来一遍。两次“后进先出”叠加等于一次“先进先出”逆序的逆序是原顺序。这就是双栈模拟队列的全部原理也是这道作业最核心的建模结论。一个容易混淆的点是银行叫号顺序是 FIFO但柜台客户的“办理顺序”并不严格要求 FIFO。如果作业没有额外说明默认要求就是先到先服务。所以不要为了迎合栈而把出队顺序改成后到先服务那是未完成建模不是设计方案。2.2 双栈模拟队列倒一次就是排队两个栈分别叫 inStack 和 outStack业务上可以理解为“新客户暂存区”和“叫号待发区”。入队永远只压 inStack出队之前先检查 outStack如果 outStack 为空就把 inStack 里的元素全部弹出并按弹出顺序压进 outStack。这个动作叫“倒栈”。倒完栈后从 outStack 栈顶弹出第一个客户就是整个队列里最早来的那一位。这里两条规则缺一不可第一倒栈必须“全部倒完”不能倒一半就出队第二必须等 outStack 空了才能再倒。举一个最小例子客户 1、2、3 依次入队倒栈后 outStack 里从栈顶到栈底依次是 1、2、3。弹出 1 是第一个客户。这时来了一位新客户 4入队压进 inStack。下一次叫号时 outStack 还没空直接从 outStack 弹出 2顺序依然正确。等到 outStack 空了而 inStack 里有 4下一次叫号前才会把 4 单独倒过来。整个过程任意时刻出队顺序都与入队顺序一致。从抽象层面看双栈模拟队列就是把“排队”拆成两段操作先进来的人先进暂存区等到真正要叫号时再整体翻转到待发区。两次翻转抵消了栈的逆序特性对外表现和一个真正的队列完全一样。2.3 容量、窗口数与初始参数怎么定实现前先定几个参数这部分也直接对应实验报告里的“需求分析”。参数常见取值说明MAX_SIZE100单个顺序栈的容量队列最多同时在等 MAX_SIZE 位客户窗口数1队列只管叫号分配窗口的逻辑可留到扩展编号起点1id_counter 从 1 递增便于手工核对数据文件queue_log.txt退出前导出当前等待队列MAX_SIZE 设成 100 对绝大多数课程评测足够。两个栈分别占 MAX_SIZE 个元素空间可能出现一个栈满载、另一个栈全空的极端情况所以队列的实际容量上限是 MAX_SIZE而不是两个栈容量之和。窗口数在基础版本里可以不接入调度逻辑service 函数里打印“请 xx 号客户到 xx 号窗口”即可。如果要做得更完整再加一个窗口状态数组有空闲窗口才叫号。注意MAX_SIZE 不要拍脑袋写 1000。顺序栈是连续内存数组开得越大出队倒栈时的数据搬运量也越大对课程演示没有帮助。3. 用C语言跑通银行排队系统结构体、双栈与菜单3.1 结构体设计与栈的基本操作代码采用静态数组顺序栈不用链栈。原因很简单作业规模小顺序栈能少写内存管理代码避免在课设里引入 malloc 和 free 的隐患。链栈可以在实验报告“改进方向”里提一句但主程序跑通为王。#include stdio.h #include stdlib.h #include string.h #define MAX_SIZE 100 // 单个栈容量也是队列容量上限 #define MAX_NAME 20 // 客户姓名长度上限 typedef struct { int id; // 取号编号从 1 递增 char name[MAX_NAME]; // 客户姓名 int service_time; // 预计办理时长单位分钟 } Customer; typedef struct { Customer data[MAX_SIZE]; int top; // 栈顶下标-1 表示空栈 } Stack; void initStack(Stack* s) { s-top -1; } int isStackEmpty(Stack* s) { return s-top -1; } int isStackFull(Stack* s) { return s-top MAX_SIZE - 1; } void push(Stack* s, Customer c) { if (isStackFull(s)) { printf(暂存区已满无法入队\n); return; } s-data[(s-top)] c; } Customer pop(Stack* s) { Customer fail; fail.id -1; fail.name[0] \0; fail.service_time -1; if (isStackEmpty(s)) { printf(暂存区为空弹出失败\n); return fail; } return s-data[(s-top)--]; } Customer peek(Stack* s) { Customer fail; fail.id -1; fail.name[0] \0; fail.service_time -1; if (isStackEmpty(s)) { return fail; } return s-data[s-top]; }结构体里把客户建模为编号、姓名、办理时长三个字段。id 必须独立于数组下标因为在双栈搬运过程中数组下标一直在变只有 id 能稳定标识一位客户。pop 失败时返回 id 为 -1 的哨兵 Customer主程序判断 id 小于 0 就能识别失败比只打印一条错误信息更可靠。3.2 入队出队与叫号核心业务函数有了栈的基本操作再封装队列层。队列层对外只暴露四个函数初始化、入队、出队、统计人数。这一层是和栈的唯一接触面。typedef struct { Stack inStack; // 新客户先进这里 Stack outStack; // 叫号时从这里出 } Queue; void initQueue(Queue* q) { initStack((q-inStack)); initStack((q-outStack)); } void enqueue(Queue* q, Customer c) { push((q-inStack), c); } // 核心只有 outStack 为空才把 inStack 整体倒过来 Customer dequeue(Queue* q) { if (isStackEmpty((q-outStack))) { while (!isStackEmpty((q-inStack))) { push((q-outStack), pop((q-inStack))); } } return pop((q-outStack)); } int countQueue(Queue* q) { return q-inStack.top 1 q-outStack.top 1; } void service(Queue* q) { if (isStackEmpty((q-inStack)) isStackEmpty((q-outStack))) { printf(当前没有等待客户\n); return; } Customer c dequeue(q); if (c.id -1) { printf(出队失败请检查栈状态\n); return; } printf(请 %d 号客户到窗口办理预计时长 %d 分钟\n, c.id, c.service_time); }dequeue 是整个系统的核心顺序不能反先判 outStack 空不空空了才倒栈倒栈要一次倒完。如果把“倒栈”放到 enqueue 里做新客户入队时会反复搬运数据顺序虽然对但每次入队都是 O(n) 操作作业避坑时会被老师追问复杂度。现在这样设计每个客户最多被压两次、弹两次dequeue 均摊 O(1)。service 的职责是判断队列是否真空再安全调用 dequeue。3.3 总控菜单与文件导出主程序做一个循环菜单。菜单不追求花哨但每个分支都要能独立运行退出前自动保存等待队列到文件方便实验报告直接贴截图。void saveQueue(Queue* q, const char* filename) { FILE* fp fopen(filename, w); if (fp NULL) { printf(文件打开失败\n); return; } fprintf(fp, Bank Queue Log \n); // 出队顺序outStack 从栈顶到栈底然后 inStack 从栈底到栈顶 for (int i q-outStack.top; i 0; i--) { fprintf(fp, %d %s %d\n, q-outStack.data[i].id, q-outStack.data[i].name, q-outStack.data[i].service_time); } for (int i 0; i q-inStack.top; i) { fprintf(fp, %d %s %d\n, q-inStack.data[i].id, q-inStack.data[i].name, q-inStack.data[i].service_time); } fclose(fp); printf(排队记录已保存到 %s\n, filename); } int main() { Queue bankQueue; initQueue(bankQueue); int choice 0; int idCounter 1; while (1) { printf(\n 银行排队系统(双栈版) \n); printf(1. 客户取号入队\n); printf(2. 叫号办理\n); printf(3. 查看队首\n); printf(4. 显示等待人数\n); printf(5. 保存排队记录\n); printf(6. 退出并保存\n); printf(请选择: ); if (scanf(%d, choice) ! 1) { break; } switch (choice) { case 1: { Customer c; c.id idCounter; printf(请输入姓名: ); scanf(%s, c.name); printf(请输入办理时长(分钟): ); scanf(%d, c.service_time); enqueue(bankQueue, c); printf(%d 号客户入队成功\n, c.id); break; } case 2: service(bankQueue); break; case 3: { Customer c; if (!isStackEmpty(bankQueue.outStack)) { c peek(bankQueue.outStack); } else if (!isStackEmpty(bankQueue.inStack)) { c bankQueue.inStack.data[0]; } else { printf(队列为空\n); break; } printf(队首客户: %d号 %s 时长%d分钟\n, c.id, c.name, c.service_time); break; } case 4: printf(当前等待客户总数: %d\n, countQueue(bankQueue)); break; case 5: saveQueue(bankQueue, queue_log.txt); break; case 6: saveQueue(bankQueue, queue_log.txt); printf(已保存并退出\n); return 0; default: printf(无效选择请重新输入\n); } } return 0; }case 3 的队首查找逻辑最容易写错单独解释一下如果 outStack 非空栈顶就是下一个被叫号的人如果 outStack 空而 inStack 非空inStack 最底部才是一开始就排队的客户所以取 data[0]。两个都空才是队列为空。这段代码里两个栈的 top 同时参与计算手工推一遍就明白为什么顺序是“out 顶到底 in 底到顶”。文件导出的顺序和队首逻辑一致保证日志读回来就是实际叫号顺序。4. 银行排队系统避坑记录5个让作业翻车的细节4.1 单栈直接出队最后一个变成第一个现象客户 1、2、3 依次取号第一次叫号直接叫了 3 号。原因单栈 LIFOpop 一定拿最后 push 的元素。这是全作业最常见的翻车现场。解决不要想着“再遍历一遍找到第一个”而是老老实实用双栈。先建模再写代码这一段血泪经验值一个通宵。简单的判别方法写完后跑三组入队出队看输出顺序是不是 1、2、3。4.2 使用 Visual Studio 时 scanf 编译报 C4996现象代码在 Dev-C 里好好的放到 Visual Studio 编译直接报错error C4996: scanf: This function or variable may be unsafe。原因VS 默认禁用 scanf 系列函数要求替换成 scanf_s。解决在源文件最顶部、所有 #include 之前加一行#define _CRT_SECURE_NO_WARNINGS。不要改用 scanf_s因为 scanf_s 是 VS 特有的老师用 gcc 环境评阅时反而编译失败。Dev-C、Code::Blocks 没有这个问题。注意#define _CRT_SECURE_NO_WARNINGS必须出现在所有头文件之前放在#include stdio.h后面仍然报错。4.3 中文字符串写进文件变成乱码现象printf 输出中文正常用记事本打开 queue_log.txt 却是一堆乱码。原因Dev-C 等老环境默认用 GBK 保存源码printf 写出的也是 GBK 字节电脑默认按 UTF-8 打开文本文件时就会乱码。解决文件内容头一行用英文比如 “ Bank Queue Log ”客户姓名本身如果是中文截图贴进实验报告即可。不要在程序里折腾编码转换C 语言课程作业不值得为编码花时间。4.4 空队列时叫号程序读栈底垃圾数据现象没有任何客户时按“叫号办理”程序不提示“没有客户”反而打印出一串负数 id偶尔直接崩溃。原因service 里只判断了 outStack 空没判断 inStack 也空dequeue 返回的哨兵 Customer 没有在 service 里处理。解决dequeue 返回后必须检查 c.id -1另外查看队首的分支也要先判断两个栈都空。这种问题最难排查表面看“能跑”实际结果全错。如果已经崩了用 gdb 跑一次backtrace看调用栈就能发现是哪个函数没做空判断——栈回溯比 printf 大法高效得多。4.5 链栈的 malloc 不释放或者重复 free现象抄了网上的链栈模板跑完服务台数据显示内存泄漏手动加 free 后又出现“double free”崩溃。原因链栈每个节点都是 malloc 出来的退出前必须逐个释放但释放逻辑写错同一个节点被 free 两次就是未定义行为表现非常玄学。解决课程作业直接用静态数组顺序栈一个 free 都不用写。如果坚持用链栈写一个 destroyStack在主函数 return 前调用一次并保证任何分支都只调用一次。5. 从能跑到能答辩排队系统的验证用例与实验报告5.1 手工验证用例四组操作覆盖全部边界程序写完只能算能跑交之前必须做顺序验证。下面的用例拿笔照样推一遍再对照程序输出。操作序列期望输出验证点enq(1), enq(2), deq1基础 FIFO 顺序enq(1), deq, enq(2), deq1 然后 2第一批倒栈后第二批顺序不被打乱enq(1), enq(2), deq, enq(3), deq, deq1、2、3outStack 非空时新客户不提前enq(1), enq(2), enq(3), deq, deq, deq1、2、3整批倒栈一次完整输出第一组验证最基础的双栈翻转。第二组验证“倒一半再来新客户”不会插队。第三组是最容易出问题的场景outStack 里还有 2新来的 3 只进 inStack必须等 outStack 空了再倒。第四组验证连续出队时只倒一次栈后续都直接弹出。5.2 合法出栈序列判定一道跟本题强相关栈算法题答辩时老师喜欢顺着栈往下追问给定入栈序列 1 到 n怎么判断一个输出序列是合法的出栈序列这是数据结构题库里的常客也直接检验你对栈行为的理解。判定的经典思路是模拟用一个辅助栈遍历目标输出序列如果栈顶不是当前目标就从入栈序列里继续压入如果入栈序列全部压完栈顶还不是目标说明该序列不合法。// 判断长度为 n 的出栈序列是否合法 // out 数组存放待判定序列入栈序列为 1..n int isValidPopSequence(int out[], int n) { int aux[n]; // 辅助栈 int top -1; int in 1; // 下一个待压入的元素 for (int i 0; i n; i) { // 栈顶不是目标时继续压入 while (in n (top -1 || aux[top] ! out[i])) { aux[top] in; } if (top -1 || aux[top] ! out[i]) { return 0; // 入栈序列全部用完还没匹配上 } top--; // 匹配成功弹出 } return 1; }参数 n 是序列长度out 是待判定的出栈序列。函数返回 1 表示合法序列。例如输入序列 1、2、3输出序列 3、1、2 会返回 0因为 3 弹出后栈里只剩下 21 在更底下拿不到 1。这个结论可以写进实验报告的“算法扩展”一节能明显提升报告层次。5.3 数据结构实验报告的四块内容实验报告不是代码粘贴。按下面的结构写老师想扣分都难找理由。第一块是问题建模把“先到先服务”抽象成先进先出队列再把队列用两个栈实现画一个数据流向示意outStack 和 inStack 的关系一句话讲清。第二块是结构设计给出 Customer、Stack、Queue 三个结构体定义配上核心函数列表和复杂度分析。出队最坏 O(n)均摊 O(1)入队 O(1)这点必须写。第三块是测试结果直接放 5.1 的四组用例输出截图再放 queue_log.txt 的截图。第四块是结果分析写“用队列实现排队只需要五分钟用栈实现让我理解了抽象层级”这是真实的作业体会比空喊“通过本次实验我收获很多”有用得多。6. 一个更稳的封装双栈队列接口与答辩加分写法到这一步双栈逻辑已经能跑。如果想在答辩时多讲三分钟把队列层再往抽象提一级对外只暴露 QueueInit、QueuePush、QueuePop、QueueEmpty 四个接口主程序完全不知道内部有栈的存在。typedef struct { Stack inStack; Stack outStack; } Queue; void QueueInit(Queue* q) { initStack((q-inStack)); initStack((q-outStack)); } void QueuePush(Queue* q, Customer c) { push((q-inStack), c); } Customer QueuePop(Queue* q) { if (isStackEmpty((q-outStack))) { while (!isStackEmpty((q-inStack))) { push((q-outStack), pop((q-inStack))); } } return pop((q-outStack)); } int QueueEmpty(Queue* q) { return isStackEmpty((q-inStack)) isStackEmpty((q-outStack)); }这样封装之后main 函数调用的只有“入队”“出队”“判空”和调用标准库里的队列没有区别。答辩时就能自然讲出抽象数据类型的概念上层只关心干什么不关心怎么存。把 Queue 类型单独放到 queue.h 里Stack 的相关实现放在 stack.c就是一个标准的分层设计。我当时做这道题时就栽在“直接存取栈顶”这个动作上。老师问为什么 case 3 要分别看 outStack 和 inStack我支支吾吾半天。后来把队列操作全部封装起来对外只留四个接口内部怎么折腾都不影响上层逻辑才真正想明白这道题在教什么。这个封装习惯后来写 Linux 驱动和设备驱动时一直在用数据结构作业其实是在给工程基本功打底。希望这篇笔记能让你少走一点我当时走过的弯路也希望你能在答辩时把双栈翻转讲得比当年的我更清楚——希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
C#.net物联网网关开发:从Modbus采集到MQTT上云与组态实践 简介:面向C#/.NET开发者的跨平台物联网网关完整源代码,基于.NET6打造,核心解决工业现场设备接入与数据上云问题。通过浏览器可视化配置即可接入PLC、扫码枪、CNC、数据库、串口设备、上位机、OPC Server、MQTT Server等,并支持与T… · 2026/9/26 21:44:53
SpringBoot+Vue前后端分离项目部署实战:从环境搭建到排错指南 拿到这套"在线教育系统信息管理系统"源码的时候,我的第一反应其实是有点复杂的——标题里明明白白写着SpringBoot后端Vue前端MySQL,又标注了"可直接运行",但做过这类项目的人都知道,所谓"可直接运行&quo… · 2026/9/26 21:44:39
CLI与Agent结合实战:从命令行工具到智能体能力编排 1. 从“CLI-Anything”说起:命令行工具正在被重新定义第一次看到“CLI-Anything”这个标题,我脑子里蹦出来的不是某个具体工具,而是一种趋势判断:命令行界面正在从“人敲命令”变成“人描述意图,Agent 去敲命令”。过去… · 2026/9/26 21:44:32
wordpress显示缩略图摘要怎么选不踩坑3个实战案例 wordpress显示缩略图摘要怎么选不踩坑3个实战案例 自己不会代码想做网站,是不是每次看到那种“左边一张精美缩略图,右边几行摘要文字”的列表页,心里既痒又慌?怕改乱了样式,怕代码报错,更怕花了钱请人做,结果对方收你高价还做得慢。其实,… · 2026/9/26 22:23:49
C#反编译实战:ILSpy、dnSpy与de4dot还原程序集与混淆对抗 简介:ILSpy是一款免费开源的.NET反编译器,以MIT许可证发布,面向需要查看程序集内部实现、逆向分析或学习代码技巧的C#开发人员。它由开发过著名SharpDevelop的iCSharpCode团队打造,初衷正是为了完全替代收费的Reflector࿰… · 2026/9/26 22:23:49
CRM系统不只是客户管理:DeskcommCRM的沟通留痕与工单协作全解析 1. 从名字说起:DeskcommCRM到底在解决什么问题做客户管理的团队,迟早会遇到一个绕不开的尴尬期:客户资料散落在销售个人的Excel里、企业微信聊天记录里、客服工单系统里,甚至还有几张写在便利贴上的电话号码。每次要跟一个重点客户… · 2026/9/26 22:23:49
戴尔R7515实战:Debian 12.5下Mellanox网卡RoCE配置指南 这台戴尔PowerEdge R7515在我机柜里待了快半年,最近终于腾出时间认真折腾——装Debian 12.5,再把Mellanox ConnectX-5网卡的驱动、固件和RoCE功能全部调通。我写这篇东西的初衷很简单:R7515是单路EPYC平台里性价比很能打的一台2U服务器&#… · 2026/9/26 22:23:35
金翔云ASP进销存本地部署实战指南 简介:金翔云WEB进销存系统是一套面向中小企业管理者的轻量级云端进销存解决方案,聚焦库存、销售、采购、账务及基础权限管控等核心业务场景,帮助非IT背景用户快速实现业务数字化,降低本地部署与运维门槛。资源包共1841个文件&… · 2026/9/26 22:23:35
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46