
简介基于QT实现的医院排队叫号系统源自同济大学20级计算机科学与技术专业的数据结构课程设计适合正在准备课程设计、毕业设计或初学Qt/C的开发者。项目以队列结构为核心通过PatientQueue实现候诊患者入队、出队与顺序叫号医生端与B超检查等模块配合模拟从取号、排队到叫号、检查的完整就诊流程代码模块划分清晰界面可运行便于直接复现或二次开发。压缩包共30个文件、约6MB包含6个cpp与5个h源码、UI界面描述文件、Qt工程与资源配置、实验报告PDF、运行截图及动态演示素材jpg/gif等图片资源可用于界面展示与效果预览帮助快速评估运行结果。已有132人学习浏览适合作为数据结构课程设计、期末大作业或工程实训的复现蓝本也可在此基础上扩展预约挂号、分诊统计等功能。 作为一个带过几届课程设计、也自己当年在数据结构课上摸爬滚打过的老学长我太清楚医院排队叫号系统这类题目在每个学期的课设题目清单里有多常见了。说它经典是因为它看似简单实则把数据结构课里的重点内容几乎全串了一遍而且做出来之后演示效果非常直观——一个带着真实业务逻辑、界面又能跑起来的系统远比单纯在控制台里打印一个链表要有说服力得多。这篇博文我就以基于QT的医院排队叫号系统这个典型的同济大学20级计科数据结构课程设计为例完整复盘一下这套系统怎么做、为什么这样做、以及课设答辩时最容易被人问倒的几个点。不管你是正在为课设头秃的学生还是想用QT做一套带界面的小系统练手的开发者这篇文章应该都能帮你少走不少弯路。1. 为什么医院排队叫号系统是数据结构课设的黄金题目先说一个很多人没想透的问题课程设计选题到底选什么样的题才算好我见过太多人选了特别炫酷的题目比如做一个图像识别系统、做一个网络爬虫结果做到了中期发现核心难点全在算法调优和第三方库上跟数据结构本身半毛钱关系都没有最后答辩的时候老师问你这里面用到了什么数据结构支支吾吾答不上来分数自然不会好看。医院排队叫号系统恰好站在了另一个极端——它的业务逻辑足够清晰数据结构应用足够密集又不会难到让人做不完。具体来说这套系统里至少要处理这样几件事取号患者到院后取一个排队号码号码要按顺序递增这背后是队列的入队操作。叫号医生看完一个患者系统从等待队列中取出下一个患者这背后是队列的出队操作。多队列管理一个科室通常有好几个诊室每个诊室前都排着一队人这背后是队列数组或者队列哈希表。优先级急诊患者要插队优先就诊这背后是优先队列或者堆结构。过号处理叫到号的患者不在医生会先跳过过号患者回来后要重新安排这背后涉及队列元素的删除和重新插入。你看就这么一个看似不就是排队吗的系统实际上把线性表、队列、优先队列、甚至树和哈希表全都串起来了。老师看到这样的选题第一反应就是这个学生是真的在用心做课设而不是随便找个题目糊弄过去。再从技术栈的角度看QT作为C最主流的GUI框架之一跟数据结构课程的衔接非常自然。你在课上学过的std::queue、std::priority_queue、手写的链表节点、二叉堆都可以直接搬到QT项目里用不需要额外学习Python的list或者Java的Collection框架——学习成本几乎为零但展示效果直接拉满。2. 取号与叫号的核心队列的选择比你想的更讲究2.1 用STL的queue还是自己写链式队列这是第一个要做的决策。很多学生图省事直接#include queue然后std::queuePatient q;觉得完事了。如果你只是想跑通功能这样没问题但如果你想让课设有点含金量我强烈建议你至少自己实现一个链式队列。原因不只是老师喜欢看到手写数据结构这么表面。链式队列在叫号系统里有一个实实在在的优势它支持任意位置的元素删除。你想想看患者拿到号之后可能等得不耐烦走了也可能过号回来了。如果用std::queue它只允许从队头弹出元素你想把一个排在中途的患者移除唯一的方法是把它前面的所有患者全部pop()出来暂存移除目标后再重新入队——这个操作的时间复杂度是O(n)而且代码写起来非常丑陋万一中途程序崩溃暂存的患者信息就全丢了。手写链式队列就灵活多了。你给每个节点加一个prev指针做成双向链表结构的队列这样删除任意一个节点都是O(1)操作。我在课设里就是这么设计的struct PatientNode { int number; // 排队号码如 A001 QString name; // 患者姓名 int priority; // 优先级0普通 1急诊 PatientNode* prev; PatientNode* next; PatientNode(int num, const QString n, int pri) : number(num), name(n), priority(pri), prev(nullptr), next(nullptr) {} }; class LinkedQueue { private: PatientNode* head; // 队头指向虚拟头节点方便删除操作 PatientNode* tail; // 队尾 int size; public: LinkedQueue(); ~LinkedQueue(); void enqueue(PatientNode* node); // 尾插 PatientNode* dequeue(); // 头删 bool removeByNumber(int number); // 按号码移除任意节点 void display(); // 遍历队列 int getSize() const; };2.2 多队列的存储结构设计医院的叫号不是一条大长队排到底而是分科室、分诊室的。以普通内科为例假设有3个诊室同时开诊每个诊室前面都应该有一队患者。这就要用到队列数组LinkedQueue queues[3]; // 0号诊室、1号诊室、2号诊室取号的时候系统根据当前各诊室的排队人数把新患者分配到这个科室下排队人数最少的诊室队列中——这就是一个简单的负载均衡算法虽然简单但非常实用答辩的时候还能顺带讲讲这个设计思路。不过这里也有一个细节如果你想根据科室来动态创建队列而不是写死3个那就需要哈希表 队列的组合结构。用QHashQString, LinkedQueue*key是科室名value是对应的队列指针。患者取号时根据选择的科室找到对应的队列然后入队。这个设计的扩展性很好新增一个科室只需要往哈希表里插一个键值对不用改任何其他代码。我当时课设就采用了哈希表管理科室队列答辩时老师追问为什么不用数组我的回答是数组需要预先知道科室的固定数量而且科室ID和数组下标的映射关系不够直观哈希表的key直接用科室名称字符串代码可读性更高查找复杂度也是O(1)。这个回答直接让老师点了点头。3. 急诊优先与过号重排从队列到优先队列的距离3.1 优先队列怎么处理插队需求医院里最核心的一个需求就是急诊患者可以插队。有的学生一看插队直接就在普通队列中间插入一个节点——这叫插队但它是无序插队如果急诊患者来得多了队尾的普通患者可能永远轮不到。正确的做法是使用优先队列。在数据结构课上优先队列最常见的实现方式是二叉堆Binary HeapSTL里的std::priority_queue就是基于堆实现的。但是在叫号系统这个场景里直接堆std::priority_queue有一个硬伤它不支持遍历。你想象一下护士叫号的时候屏幕上要显示当前等待A001、A002、A003...这就是一次完整的队列遍历。priority_queue做不到因为它内部是一个堆你只能top()拿到最高优先级元素剩下的元素是乱序存放在底层容器里的。所以我的做法是系统维护两条队列——一条普通队列链式队列一条急诊队列也是链式队列。取号的时候患者选择普通还是急诊分别进入不同的队列。叫号的时候系统先看急诊队列是否为空不为空就先叫急诊否则叫普通队列的队头。这种设计在数据结构上其实是一个双队列 优先级调度的模型比单纯用堆更贴近真实业务也更好解释。你把它画成一张图两个队列并行出队时急诊队列优先这就是一个典型的两级调度。如果你想让课设更出彩一点还可以再加一个优先级数的概念普通号优先级为10急诊号优先级为5数字越小越优先每个节点存一个priority字段。这样以后如果出现危重急诊和普通急诊的区别只需要调整priority的数值不用改代码结构。这就是数据结构和现实业务分离的设计思想。3.2 过号患者的二次入队状态管理过号处理是一个特别容易被忽略、但又特别体现数据结构功力的细节。先说业务规则护士叫号A003请到3号诊室就诊叫了三遍没人来护士点击过号按钮A003就被标记为过号状态。过了一会儿A003的患者匆匆忙忙跑回来了护士要把他重新安排进去。安排进去有两种策略策略一插到当前队头之后。这相当于在链式队列的头部第二个位置插入一个节点。为什么是这个位置因为有一个人已经在被叫号了不能让过号患者抢了正在就诊的人的位置但过号患者确实比后面排队的普通患者更有资格先看所以插在队头之后是最合理的。策略二直接排到队尾。这个策略对普通患者更公平但对过号患者不太友好可能会引发医患矛盾现实中医院通常不会这么做。从数据结构实现的难易程度来看策略一其实非常简单。如果你用的是LinkedQueue队头是虚拟头节点那么在队头之后插入就是void insertAfterHead(PatientNode* node) { node-next head-next; if (head-next) { head-next-prev node; } else { tail node; // 队列本来就是空的 } head-next node; node-prev head; size; }这里有一个关键的设计细节虚拟头节点dummy head真的太好用了。没有虚拟头节点的时候你在队头插入要判断队列是否为空插入位置是不是队头等等各种特殊情况有了虚拟头节点insertAfterHead的逻辑就变得非常统一不需要任何额外的条件判断。这个经验是我写了很多次链表之后才悟出来的建议所有做链表相关课设的同学都采用这种写法。再说状态管理。患者从取号到就诊完成一共经历这么几个状态等待中 - 已叫号等待中- 就诊中 - 已完成以及两个特殊状态过号、已取消。我在课设里用了一个枚举类型来管理enum PatientStatus { STATUS_WAITING 0, // 等待中 STATUS_CALLED 1, // 已被叫号等待进入诊室 STATUS_VISITING 2, // 就诊中 STATUS_COMPLETED 3, // 已完成 STATUS_MISSED 4, // 过号 STATUS_CANCELLED 5 // 已取消 };每个患者节点都维护一个status字段。护士点击过号按钮不是直接把节点从队列里删掉而是把状态改成STATUS_MISSED同时把它从队列中移除。这里移除但不销毁很重要——因为你后面可能需要统计今天有多少患者过号了、过号患者等待的平均时间是多少这些数据这些数据在数据结构上是怎么来的是从一个历史记录链表或者已完成患者列表里遍历统计出来的。我当时额外维护了一个QListPatientNode* completedList每次患者完成就诊就把节点从队列移出挂到这个列表里。这样做的好处是主队列的元素始终是真正在等待的人而完成列表记录了全部历史数据。答辩的时候你还能展示今日接诊量统计、过号率统计这些看起来特别像真实产品的功能加分效果很明显。4. 界面层与数据结构的解耦QT信号槽的正确打开方式4.1 先画界面骨架再连信号槽很多人用QT做课设一上来就拖控件把界面画得漂漂亮亮结果发现按钮跟数据结构的联动完全不知道怎么搞。正确的顺序应该是先想清楚数据流再画界面。以这套叫号系统为例它的数据流是这样的取号端患者选择科室 - 点击取号按钮 - 生成新节点 - 插入对应科室的队列 - 更新叫号显示屏大屏。叫号端医生/护士选择科室 - 点击下一个按钮 - 从对应队列出队一个节点 - 更新状态为就诊中 - 更新叫号显示屏。显示屏只读显示当前叫到的号码和等待队列中接下来的几个号码。这三个界面取号端、叫号端、显示屏可以做成三个独立窗口也可以合并成一个主窗口加两个子窗口。我建议用QMainWindow作为主框架取号端和叫号端分别用两个QDockWidget停靠在左右两侧显示屏用一整块QLabel居中显示大号字体。这样操作端和显示端同屏可见演示的时候非常直观。4.2 信号槽怎么和队列操作联动你需要明确一个原则所有的数据层操作都必须在数据类的成员函数中完成界面只负责调用和刷新。也就是说界面上不要出现任何直接修改节点指针的代码。具体实现时我会建一个HospitalSystem类它持有所有科室的队列哈希表、完成列表、以及各种统计字段。这个类对外暴露的方法就是取号、叫号、过号、取消等业务操作。界面层通过QT的信号槽机制跟这个类通信。举个具体的信号槽连接例子。假设叫号端有一个呼叫下一个按钮它应该这样做// 叫号端窗口类 connect(ui-btnNextPatient, QPushButton::clicked, this, []() { // 从系统类中取出一个患者 PatientNode* nextPatient system-callNext(selectedDept); if (nextPatient nullptr) { QMessageBox::information(this, 提示, 当前队列为空无需叫号); return; } // 更新显示屏 emit patientCalled(nextPatient-number, nextPatient-name); // 刷新等待列表 refreshWaitingList(); });这里用lambda表达式捕获this指针可以很方便地调用当前窗口的成员函数。其实QT5之后的信号槽写法已经很现代了不需要再写古老的SIGNAL/SLOT宏。但这里有一个新手特别容易踩的坑如果你在lambda里直接操作了PatientNode的指针然后又刷新了界面界面里显示的信息极有可能是野指针数据。因为PatientNode是从链式队列里dequeue()出来的此时这个节点已经不在队列里了但它还没被销毁挂到了完成列表或者历史列表里所以指针本身是有效的。但如果你在出队操作里写了delete node那这个指针就立刻悬空了刷新界面的时候必然崩溃。所以记住一句话节点从队列移除不等于销毁节点。从队列移除只是脱离队列这种数据结构的管理节点本身的生命周期应该由历史列表或者单独的内存管理器来负责。这个设计思路在我做课设的时候帮我避免了无数次崩溃代码里注释也写得很清楚Do not delete the node here, its now owned by historyList.4.3 叫号显示屏的刷新策略叫号显示屏主要展示两块内容当前叫号号码、接下来3到5个等待号码。刷新方式有两种方式一定时器轮询。每隔500ms遍历一次主队列更新显示屏。这个方式简单粗暴但效率低而且如果队列里数据量大会频繁造成无意义的刷新。方式二事件驱动。当队列发生任何变化入队、出队、过号、取消时对应窗口主动发射一个queueChanged信号显示屏收到信号后再重新渲染内容。这个方式更优雅也更符合QT的信号槽设计哲学。我强烈推荐方式二。虽然代码量稍微多一点但演示的时候你会明显感觉到取号端一点按钮显示屏瞬间就跳到下一个号码了完全没有任何卡顿和延迟。这个瞬间响应的体验在演示评分的时候是有隐性加分的。老师会认为你的系统是实时的而不是轮询模拟出来的。显示屏的刷新函数可以这样写void DisplayWindow::refreshDisplay() { // 获取当前科室队列中前5个等待号码 QListint nextNumbers system-getNextNumbers(deptName, 5); // 当前叫号栏 ui-lblCurrentNumber-setText(nextNumbers.isEmpty() ? --- : QString(A%1).arg(nextNumbers.first())); // 接下来等待栏 QStringList waitingList; for (int i 1; i nextNumbers.size(); i) { waitingList QString(A%1).arg(nextNumbers[i]); } ui-lblWaitingList-setText(waitingList.join( )); }其实getNextNumbers就是一次链队列的遍历时间复杂度O(n)。因为每次显示只取前5个所以可以加一个计数器遍历到第5个就提前break这又是一个可以跟老师讲的小优化点。5. 课设答辩必问考点复杂度分析、边界情况与数据结构的选择理由5.1 复杂度分析是答辩的必答题答辩的时候老师十有八九会问你这个系统里各个操作的时间复杂度是多少我把每个核心操作整理成一张表你可以直接背下来也能帮助自己真正理解操作数据结构时间复杂度说明取号入队链式队列尾插O(1)维护tail指针直接在尾部插入叫号出队链式队列头删O(1)通过head-next拿到第一个有效节点过号任意位置删除双向链式队列O(1)需要按号码找到节点此时要遍历所以严格说是O(n)急诊优先叫号双队列调度O(1)先查急诊队列头不涉及遍历按号码查找患者链表遍历O(n)如果引入哈希表辅助索引可降到O(1)显示屏等待列表链表遍历取前k个O(k)提前breakk通常为5近似O(1)其中过号那栏我特意做了说明真正删除节点本身是O(1)但删除之前你得先找到这个节点。如果链表是无序的查找就是O(n)。如果你想让按号码查找也变成O(1)可以额外维护一个QHashint, PatientNode*作为索引号码到指针直接映射。这样删除操作就变成了先用哈希表找到节点再在链表里删除整体也是O(1)。但是这个哈希表需要在每个节点入队、出队、删除时同步更新相当于用空间换时间增加了一点维护复杂度。我个人建议课设里做到O(n)查找就够了但要在答辩时主动提一句如果需要优化到O(1)可以通过哈希索引实现我的代码里预留了这个扩展点。这句话能展现出你真正理解复杂度优化而不是背了个结论。5.2 边界情况课设答辩的隐藏考点除了复杂度老师还特别喜欢问边界情况。我在课设现场就被问过一次如果你的某个诊室队列为空这时候护士点了叫号按钮程序会怎么样如果你在代码里直接dequeue()一个空队列轻则返回空指针然后界面崩溃重则直接Segmentation Fault。所以所有涉及出队的操作都必须先检查队列是否为空。我在HospitalSystem::callNext函数里就写了一段防护逻辑PatientNode* HospitalSystem::callNext(const QString deptName) { LinkedQueue* deptQueue queues.value(deptName, nullptr); if (deptQueue nullptr) return nullptr; // 科室不存在 if (deptQueue-isEmpty()) return nullptr; // 队列为空 // 先看急诊队列是否有患者 LinkedQueue* emergencyQueue emergencyQueues.value(deptName, nullptr); if (emergencyQueue !emergencyQueue-isEmpty()) { return emergencyQueue-dequeue(); } return deptQueue-dequeue(); }还有几个其他容易被问到的边界情况我建议你在答辩前自己过一遍多个诊室共用一个显示屏时号码重复怎么办比如3号诊室和1号诊室同时在叫号屏幕上显示的号码怎么区分我的做法是每个诊室用字母前缀区分A代表内科1诊室B代表内科2诊室。这样就不会混淆了。系统刚启动时历史队列是空的界面会不会显示异常所有初始化为空字符串或者---不能显示空白或乱码。患者取号后系统突然断电重启存队列里的数据怎么办课程设计不用做到数据持久化但如果老师问起来你可以回答正式版本会将队列数据序列化到本地文件启动时反序列化恢复然后在代码里简单加一个saveToFile和loadFromFile函数用QFile和QDataStream写就行。5.3 为什么这些数据结构选择是合理的答辩时老师可能会追问你为什么用链式队列而不是循环队列你要是只回答因为STL里有queue分数就低了。正确的回答思路是这样循环队列的底层是数组需要预先分配固定大小如果患者数量超过容量就需要扩容操作。而且循环队列删除中间元素特别麻烦——你要把所有后面的元素往前挪一位O(n)开销。医院排队场景中患者数量是动态变化的而且存在过号、取消等操作需要频繁删除非队头元素所以链式队列双向链表实现更合适入队出队O(1)删除任意节点也只需要O(1)。这个回答有三个层次先说数组的局限再说场景的特点动态变化支持任意位置删除最后对比复杂度。这就是一个典型的数据结构选型论证的回答模板放在任何涉及链表vs数组的讨论里都能用。同理如果老师问为什么优先队列不用堆你可以回答std::priority_queue虽然入队出队复杂度更优O(log n)但它无法遍历无法支持叫号系统中显示等待列表的需求。所以我在工程实现上选择双链式队列 优先级调度来模拟优先队列的行为保证了遍历能力和实现简洁性。这个回答展示了你在理论最优和工程可行之间的取舍能力是课设答辩里的高分答案。6. 课设过程中我踩过的四个经典坑6.1 信号槽里改了数据但界面不刷新这个问题太典型了。你在取号端调用了system-enqueue()数据确实进去了但是显示屏完全没反应。排查半天发现你根本没有发射queueChanged信号或者信号发射了但没有连接到显示屏的刷新槽函数上。QT信号槽有一个很隐蔽的坑如果你手写connect时槽函数的参数写错了或者用lambda捕获了错误的变量程序不会编译报错但运行时会静默失败。排查这个问题的办法是在槽函数里加一个qDebug() refresh triggered看看槽函数到底有没有被调用。如果被调用但没有刷新界面那就是UI组件指针的问题如果没被调用那就是信号没发射或连接没建立。这个排查思路拿一句大白话总结就是先确认信号有没有发出去再确认槽函数有没有接收到最后才检查界面更新的代码本身。我当时为这个问题折腾了一个晚上最后发现只是connect时忘了传Qt::QueuedConnection——在不跨线程的情况下默认的AutoConnection其实就够了所以这个不算核心问题但排查过程确实让人印象深刻写在这里给大家参考。6.2 中文字符串编码问题QT的QString内部是UTF-16编码而Windows下控制台的默认编码是GBK所以你如果用QString::fromLocal8Bit(患者姓名)在某些系统上可能会乱码。我当时的做法是源代码文件统一用UTF-8保存字符串字面量用QStringLiteral宏包裹或者在项目文件.pro里加上一句msvc { QMAKE_CXXFLAGS /utf-8 }这样在MSVC编译器下就能避免常量字符串中有无法识别的字符之类的编译错误。用MinGW的话一般没有这个问题。6.3 程序启动时报no qt platform plugin could be initialized这大概是QT新手最常遇到的崩溃提示了热搜词里也确实有这个。这个报错的意思是程序找不到QT的平台插件比如qwindows.dll。绝大多数发生在你把自己编译好的exe拷贝到别的电脑上运行或者用QT Creator直接运行时环境变量没配置好的时候。解决方案有两种一是把Qt安装目录下的platforms文件夹里面装着qwindows.dll拷到你的exe所在目录下的platforms子文件夹里。注意目录结构必须是exe所在的文件夹/platforms/qwindows.dll放错位置照样报错。二是用QT自带的部署工具windeployqt在命令行里执行windeployqt 你的程序名.exe它会自动把你依赖的所有DLL和插件都复制到exe同目录下。这是正式发布时最推荐的方式。你这个课程设计如果用QT打包后发给老师演示一定要先在一台没有装QT的电脑上测试一下能不能跑起来不然演示现场蓝屏崩溃就尴尬了。6.4 指针生命周期管理混乱导致的内存泄漏课设代码写完了功能也跑通了但是一运行内存占用就哗哗往上涨。这通常是节点创建了但没有正确释放导致的。解决思路明确每一个PatientNode的所有权。我的约定是节点创建后所有权归HospitalSystem。入队后队列持有节点的使用引用通过指针但所有权仍在HospitalSystem。出队、过号后节点挂到completedList或missedList所有权不变。HospitalSystem析构时统一遍历所有队列和列表释放所有节点。听起来有点绕但核心就是一句话不要让多个模块同时负责delete同一个指针。谁创建谁负责最终销毁。界面上只是借用指针来显示数据没有销毁权。这样下来内存泄漏基本可以杜绝。写在最后一个让课设质变的小技巧如果你做完基础版还有余力我强烈建议你加一个模拟演示模式。就是在系统里加一个定时器每隔几秒自动执行一次取号 - 叫号 - 过号 - 完成的完整流程配合界面上滚动的号码和变化的等待人数根本不需要真人操作整个流程自动跑起来像真实的医院大厅一样。我当时最后一天加了这个功能答辩现场演示的时候老师盯着大屏幕上自动跳动的号码看了好久然后问了我一个问题这个自动演示的逻辑用的是哪种数据结构我回答说是事件队列每个定时器tick产生一个排队事件系统按顺序处理这些事件——这又变相展示了一次队列的应用。课程设计说到底不是看你堆了多少功能而是看你能不能把课上学过的数据结构知识在一个真实的业务场景里用得恰到好处。医院排队叫号系统恰好是这样一个完美的载体模型成熟、需求清晰、数据结构密集、演示效果好。希望这篇复盘对你有帮助做完之后你会发现自己对链表和队列的理解比刷十道题都深刻。本文还有配套的精品资源点击获取