简介这是一份数据结构课程设计中的电梯模拟系统毕业设计报告适合计算机专业学生完成数据结构综合实践、课程设计或毕设选题参考。报告从课程设计任务书入手完整给出电梯模拟系统的需求分析、概要设计、详细设计与运行测试过程覆盖数据结构的选择与应用、算法设计与实现、C/C编码规范、程序调试等关键环节。系统模拟某校五层教学楼的单电梯运行通过链队列管理各楼层向上/向下等候乘客借助五个乘客栈存放不同目标层人员并设计开门、关门、等待、驶回一层候命等多状态调度机制同时以0.1秒为模拟时钟单位兼顾乘客最长容忍等待时间等约束。资源为单个doc文档约523KB已有139人学习文档内含任务要求、参考文献、章节目录及完整的系统设计说明对希望理解队列、栈在实际调度问题中的应用或需要撰写课程设计报告的读者具有直接参考价值。1. 电梯模拟课程设计为什么它是数据结构最值得复现的一次实战如果你正在做数据结构课程设计又不想选图书管理、学生选课那种“增删改查三件套”我强烈建议你试试电梯模拟。这份资源里写的是一个五层教学楼自动电梯的完整模拟程序电梯会在 0 到 4 层之间响应内外部请求乘客随机到达、排队、进出电梯超时还会放弃等待。整个系统用 C 控制台程序实现核心数据结构是五个链栈和五个链队列外加一个带时间片的状态机。它能帮你把栈、队列、状态转换、时间模拟这些抽象概念全部落到能跑的代码里。适合正在做课程设计、需要交报告和源码的计算机专业学生也适合想补数据结构实战的从业者。它最大的价值不是“电梯”这个题目本身而是它把数据结构选型和业务逻辑揉在一起的完整套路——拿到手你能直接照着复现也能在这个骨架上加自己的调度算法。2. 系统分析与数据结构选型链栈、链队列与七状态电梯模型2.1 电梯问题的需求拆解从楼层按钮到乘客忍耐时间先别急着写代码把需求逐条翻译成数据结构术语。题目设定是一个五层教学楼楼层编号 0 到 40 是地下层1 层是进出层也是电梯空闲时的候命层。每一层除了地下层有向上按钮每一层除了第四层有向下按钮电梯内五个目标层按钮。这些按钮对应变量CallUp[0..3]、CallDown[1..4]、CallCar[0..4]但实际代码里为了方便统一分配了长度 5索引 0 到 4 都能用。乘客的行为是随机的每个人有一个能容忍的最长等待时间超过就放弃。楼层里等待的乘客按方向分成两个队列向上队和向下队。电梯内的乘客按目标楼层分成五个栈目标楼层相同的人先进后出这符合“电梯到某一层时该层要下的乘客一起出”的场景——后进电梯的人站在门口先到自己的楼层先下但栈的顺序恰好能模拟这种“挤在门口”的现象。电梯自身有七个状态开门、已开门、关门、已关门、等待、上升、下降代码里用枚举goingup, goingdown, idle表示运动方向加上开门关门等动作函数来模拟完整生命周期。时间单位是 0.1 秒模拟时钟从 0 开始。有人进出时电梯每隔 40t 测试一次是否无人进出若无人就关门开关门各需 20t每人进出电梯需要 25t电梯在某层静止超过 300t 就自动回 1 层候命。这些时间参数不是随便定的它们直接影响调度效果如果开关门太快乘客可能来不及进出如果太慢电梯效率就低。代码里用wait(tt)函数延时clock()作为时间基准。2.2 为什么用链栈和链队列而不是数组教科书上线性表有顺序和链式两种存储方式这里选链式是出于两个原因一是乘客数量不确定链式结构可以动态伸缩用顺序表需要预先估计最大容量二是课程设计的评分点通常要求“设计抽象数据类型并实现”链栈和链队列的模板写法比直接开数组更能体现对指针和内存管理的掌握。代码里栈和队列是模板类LinkStackT和LinkQueueT各自有Push、Pop、EnQueue、DeQueue、StackEmpty、QueueEmpty等操作。电梯内的乘客栈用数组LinkStackman k[5]表示数组下标就是目标楼层楼层等待队列用LinkQueueman s[5]表示下标是当前楼层。这样设计的好处是判断“电梯该不该去某一层”时只需要检查对应栈或队列是否为空以及按钮是否被按下代码写起来非常直观。2.3 电梯状态机的核心逻辑向上、向下、静止的切换条件状态转换函数e4()是整个程序的心脏。它先扫描当前楼层以上的所有层判断是否有任何请求目标按钮、向上按钮、向下按钮、栈非空、队列非空有则置c1表示应该向上同样扫描下方楼层得到d1。然后根据当前状态nowstate决定下一步动作。这个判断有个容易忽略的点当前楼层本身的按钮请求怎么处理比如电梯在 2 层2 层恰好有人按了向上此时callup[2]1会被当成向下的障碍还是向上的理由代码里对当前层的处理是if(callup[j]1) c1;也就是说当前层的向上请求优先满足然后才扫描上方。这符合实际的电梯逻辑——先让本层要上楼的人进来再考虑去更高层。状态切换还考虑了边界条件电梯在最高层 4 层时不能再向上如果上方没有请求而下方有就转向向下电梯在 0 层时同理。静止条件有两个上下都没请求时进入idle或者电梯已经在目标最高/最低层且没有反向请求就停在原地等待。这个设计比简单的“有请求就动”更有真实感也避免电梯在两头反复横跳。3. 核心代码拆解乘客进出、时间模拟与状态转换的可执行细节3.1 乘客类的进出电梯逻辑从等待队列到目标层栈乘客类man有五个属性编号、起始楼层、目标楼层、进入系统时间、可容忍等待时间。inel()是乘客进入电梯的方法onwait()是乘客进入楼层等待队列的方法getout()是出电梯。这三个方法都会输出当前时间、下一个乘客到达时间、乘客编号和动作描述。void man::inel() { if ((m.intime m.giveuptime * TIME) clock() - delay) { cout clock() / TIME - delay / TIME , nexttime / TIME : number 号乘客等待超时已自动离开 endl; } else { k[outfloor].Push(*this); cout clock() / TIME - delay / TIME , nexttime / TIME : number 号乘客从 infloor 楼进入电梯。 endl; callcar[outfloor] 1; // 在电梯内按下目标楼层按钮 } }这里有一个容易被误读的参数intime是乘客到达系统的时间giveuptime是乘客能容忍的等待时间以 t 为单位giveuptime * TIME把等待时间换算成时钟滴答数。如果当前时刻超过了intime giveuptime * TIME说明乘客已经不耐烦离开。注意这个判断发生在乘客即将进入电梯时也就是说乘客在队列里排队时可能超时只有等到轮到他上电梯那一刻才检查这是一种“惰性检查”策略——减少每次时钟滴答都要扫描所有乘客的开销。onwait()则简单地把乘客挂到对应楼层的链队列里同时输出提示。这里没有在入队时检查超时因为超时判断只在inel()前做如果乘客一直排在队尾他可能已经超时但还没被处理直到电梯来开门时才会被清理。这是代码原作者的取舍逻辑上说得通但输出里会出现“某乘客入队后很长时间再被弹出时显示超时”的现象。3.2 时间模拟函数wait 与 shuru 的递归配合时间模块是整个程序最容易踩坑的地方。wait(tt)的作用是让模拟时钟走tt个单位时间它内部不是简单地Sleep()而是采用了一个循环循环过程中不断检查当前时刻是否到了下一个乘客到达的时间。如果到了就调用shuru()输入下一个乘客的信息然后继续等待余下的时间。int wait(long tt) { clock_t end; end clock() - delay; while (clock() end delay tt * TIME) { if ((clock() - delay) nexttime) { shuru(); // 在等待期间插入新乘客输入 } } return 0; }这段代码的巧妙之处在于用delay变量修正输入耗时。当用户在控制台输入乘客信息时程序的实际运行时间是走着的但模拟时间应该是“暂停”的。实现方法是输入前调用times()保存当前clock()并压栈输入结束后调用timer()把耗时加到delay上。这样一来所有动作判断用的都是clock() - delay就等同于排除了用户输入带来的误差。nexttime变量表示下一个乘客出现的模拟时刻。shuru()里会生成新的乘客并更新nexttime。由于shuru()本身可能再次调用wait()或者递归输入times()和timer()会嵌套使用代码里用一个LinkStackint biaozhi来保存每次times()的时钟值这样timer()能把嵌套的输入时间全部补偿进去。我见过很多人把这段写成全局变量数组其实用栈更安全因为输入的嵌套深度不确定。3.3 电梯动作函数家族e1 到 e8 各自负责什么开门e1()、关门e5()各耗 20t进出乘客各耗 25t这是题目规定的。e2()负责让当前楼层要下的乘客出电梯做法是循环弹出k[nowfloor]栈每弹出一个就wait(25)模拟乘客下梯所需时间。e3()负责让当前楼层等待队列里的乘客进入电梯但有个限制renshu MAXMAN最大载客量为 5。如果电梯满了即使这一层有人等待也只能下次再来接。int e2() { while (!k[nowfloor].StackEmpty()) { n k[nowfloor].Pop(); wait(25); n.getout(); renshu--; } return 0; }e3()的入栈逻辑要注意乘客进入电梯后会被放入k[outfloor]栈而不是k[nowfloor]。也就是说乘客在 2 楼上电梯、目标是 4 楼他会被放进k[4]栈。等到电梯到了 4 楼e2()直接从k[4]弹出他。这样设计使得每个楼层的“下车乘客”都集中在对应编号的栈里判断电梯是否需要在某层停靠时只需要检查k[i]是否非空即可。e6()是上升动作先输出“正在上升”wait(30)然后清除当前楼层按钮、楼层号加一、清除上一层的向上按钮最后输出到达新楼层。e7()是对称的下降动作。注意它在清除按钮时用了callup[nowfloor - 1] 0意思是电梯向上离开某层后该层的向上请求就被认为已经服务过。这是模拟电梯“经过即响应”的简化处理真实电梯还会在每层判断是否有人要下。3.4 手动补全主循环输入事件与状态事件的交替原报告没有给出完整的main函数但根据模块划分可以补出来。主程序维护一个事件循环每次比较下一个乘客到达时间nexttime和当前电梯动作的结束时间优先处理时间更早的事件。如果乘客先到就调用shuru()插入等候队列如果电梯动作先结束就调用e4()转换状态。int main() { chushihua(); cout 电梯模拟系统启动当前楼层1层 endl; shuru(); // 输入第一个乘客 while (true) { if (clock() - delay nexttime) { shuru(); } e4(); // 状态转换内部会调用 wait 处理动作耗时 if (/* 所有队列和栈都为空且没有按钮请求 */) { break; // 模拟结束 } } return 0; }这里有个细节e4()内部调用e6()或e7()时会执行wait(30)而wait()内部又会检查nexttime并插入新乘客所以“输入事件”和“状态事件”实际是交错处理的。如果e4()判断电梯应该静止它会调用e8()并继续循环此时如果已经没有剩余乘客程序会在下一个循环通过空队列判断退出。这个退出条件需要我们自己加否则程序会无限循环。4. 运行与测试把控制台输出变成可验证的时序序列4.1 编译环境的准备与源文件组织这份资源是 2013 年的课程设计代码用 Visual Studio 的stdafx.h头文件这是一种预编译头写法。在较新的 VS 版本里可以直接创建一个空项目把stdafx.h删掉或者换成#include iostream、#include ctime等标准头文件。原作者用的模板LinkStack和LinkQueue没有给出完整实现你需要自己补一个头文件。我一般这样组织文件main.cpp放主程序和电梯控制函数LinkStack.h和LinkQueue.h放模板类man.h放乘客类声明。模板类的实现要放在头文件里因为模板实例化是在编译期完成的。编译时注意 C 版本代码里没有用到 C11 之后的特性所以用 VS2010 或者 g 默认标准都能通过。LinkQueue模板至少要有EnQueue、DeQueue、QueueEmpty、QueueTranverse四个方法。QueueTranverse用于输出队列里的所有乘客编号原程序的diantizhuangtai()中用到它。LinkStack需要Push、Pop、StackEmpty、StackTranverse。这两个模板都是教科书标准实现照着严蔚敏书上的模板改写即可。4.2 按时间片验证输出序列的正确性运行程序后会看到类似这样的输出现在楼层是1 0,20:1号乘客进入1楼的等待队列。 0,20:电梯开门。 0,40:1号乘客从1楼进入电梯。 0,40:2号乘客进入1楼的等待队列。 0,60:电梯关门。 0,60:电梯正在上升中…… 0,90:电梯上升到了第2层 0,90:电梯开门。 0,115:1号乘客出电梯 0,115:电梯关门。关键是检查每个动作的时间戳是否与题目要求吻合。开门 20t 后应该有乘客进出的动作每次进出 25t。如果 40t 内无人进出电梯才关门那么从开门到关门至少 40t。你可以写个小脚本抓取输出中的所有时间戳计算相邻动作的时间差对照规则表验证。时间单位换算要小心输出里的时间显示是clock()/TIME其中TIME100也就是说 100 个时钟滴答等于 1 个显示时间单位。题目说“时间单位为 0.1 秒”所以clock()返回的毫秒数除以 100 后整数部分就是 0.1 秒数。wait(20)会等待 20 个显示单位即 2000 毫秒。但这个模拟不需要实时跑它只是用clock()做逻辑计时实际运行中一个人的进出瞬间就完成了但逻辑时间走了 25t。4.3 用边界测试找出逻辑漏洞测试时不要只跑随机数据要构造极端场景。第一所有乘客目标都是同一层比如都去 4 层观察电梯是否会在 4 层把所有栈清空。第二某一层只有向下按钮但没有向上按钮电梯是否会在该层停下第三乘客在电梯到达前超时离开观察队列清理是否正确。第四电梯载客满 5 人后若 1 层还有大量等待乘客电梯是否会在后续往返中继续接人。我测试时发现一个典型问题当电梯在某层停靠且完成了上下客它会调用e4()重新判断方向如果此时上下方都没有请求但该层等待队列里的人因为超时已经被清理电梯会进入idle状态。如果清理代码只发生在inel()中而乘客在队列里默默超时电梯无法感知就会一直静止。原代码对超时乘客的处理是“到进入电梯那一刻才检查”所以静止状态下队列里可能堆满了已经超时的人。解决办法是在e3()入栈前先遍历队列把所有超时的人弹出。这是一个真实存在的缺陷后面避坑章会细说。5. 避坑指南电梯模拟课程设计最常见的六个雷区5.1 现象电梯在某层反复开关门人没上来原因e3()里入栈的条件是!s[nowfloor].QueueEmpty() renshu MAXMAN但乘客进入电梯时调用的inel()可能会因为超时把人放走。如果队列里第一个乘客已经超时inel()直接输出“已离开”不会入栈但e3()的 while 循环只判断队列非空于是继续处理下一个。问题在于如果所有队列里的乘客都已超时e3()会空转一圈每个乘客都显示离开然后退出。如果队列里第一个乘客超时但第二个没超时e3()会先让第一个离开再把第二个放入电梯看起来像是“电梯开门只放走人却不上人”。解决在e3()进入循环前先扫描整个队列把超时乘客全部弹出。定义一个bool isTimeout(man m)函数比较m.intime m.giveuptime * TIME和clock() - delay超时的直接调用m.giveup()并从队列删除。5.2 现象程序运行到一半崩溃提示栈溢出或指针越界原因楼层编号是 0 到 4但callup、calldown、callcar数组声明长度为 5索引 0 到 4 都用。问题出在e6()中nowfloor后callup[nowfloor - 1] 0这行。当nowfloor从 0 上升到 1 时nowfloor - 1是 0没问题但当nowfloor从 4 试图再上升时e4()已经拦住了这个情况。真正的踩坑点是在e7()下降时nowfloor--之后如果变为负数calldown[nowfloor 1]访问的是前一层的下标但如果nowfloor原本是 0nowfloor--变成 -1calldown[0]没问题但nowfloor变成了非法楼层下一次判断就会越界。解决在e4()的下降分支里先判断nowfloor 0才能执行下降动作不能仅依赖d标志。另外建议在e6()和e7()开头加断言assert(nowfloor 0 nowfloor 4)。5.3 现象输入乘客后时间突然倒退或跳跃原因times()和timer()的嵌套使用有问题。times()把当前clock()压栈timer()把clock()与栈顶差值加到delay。如果在wait()内部调用了shuru()而shuru()又调用了wait()就会形成嵌套。此时如果某个timer()错误地弹出了另一个时间栈顶delay会变成负数或异常值。解决不要自己实现递归输入改用“时间事件”模式。将shuru()拆成两部分生成乘客信息以及把乘客放入等待队列。主循环每个滴答检查nexttime到了就执行入队操作不需要在wait()里调用shuru()。这样时间栈的深度始终为 1times()和timer()只围绕用户输入控制台耗时逻辑就清晰了。5.4 现象电梯明明在 1 层却去响应 0 层的请求然后又回到 1 层原因nowfloor 1是初始值表示电梯在 1 层。但题目说 1 层是进出层0 层是地下层。如果 0 层有人要上楼电梯需要先下降。e4()的判断逻辑是先检查当前层上方是否有请求再检查下方。如果上下都有请求默认优先向上。这会导致电梯在 1 层时上方 2 层有请求下方 0 层也有请求电梯优先向上。等上面处理完再下来0 层的人可能已经超时离开。解决这是调度策略问题不是 bug。要改的话可以设置“当前运行方向优先”规则如果电梯正在上升只响应上方的请求和中途同方向请求如果没有上方请求但下方有才转向。原代码的nowstate已经记录了当前方向但e4()在判断时没有完全利用它。你可以在c和d的计算中加入方向权重比如上升过程中d的计算只考虑下方是否有人已经按了向上按钮而不是所有按钮。5.5 现象电梯内人数超过 MAXMAN或者为负数原因renshu在e2()中出电梯时减一在e3()中进电梯时加一。但e3()的循环条件是renshu MAXMAN进入电梯的人可能是超时被放走的此时inel()里根本没有renshu而e3()循环体里却写了renshu。看原代码int e3() { while (!s[nowfloor].QueueEmpty() renshu MAXMAN) { n s[nowfloor].DeQueue(); wait(25); n.inel(); renshu; } return 0; }问题在于inel()内部已经根据超时与否决定是否真正入栈但renshu无条件执行。如果一个超时乘客离开renshu仍然增加导致电梯实际人数没变但计数虚高。反之如果某个乘客在inel()中超时但e2()出电梯时renshu--却发生了就会变成负数。解决把renshu移入inel()的成功分支。即只有乘客真的进入电梯栈时才增加renshu。同样e2()中先判断k[nowfloor]是否真的有乘客有才renshu--。5.6 现象程序无法退出一直输出“电梯静止”原因主循环缺少终止条件。原报告中的主程序模块只写了“处理输入事件和电梯状态转换事件”没有给出循环退出逻辑。最直接的退出条件是所有乘客队列、电梯内栈都为空且所有按钮请求都被清除。但如果在乘客全部结束后电梯停留在非 1 层而 1 层是候命层电梯应该自动回到 1 层这个“回 1 层”动作本身也需要时间期间可能又触发新的输入如果输入已经停止那么回 1 层后就应该结束。解决在主循环里维护一个totalPassenger计数每次shuru()时增加。当所有队列和栈为空且该计数值等于已处理的乘客总数就可以退出。更简单的方法是在shuru()里设置一个结束标志比如输入编号为 -1 的乘客时跳出循环。我在复现时采用后者交互更友好。6. 进阶优化让它从课程设计变成可演示的调度算法实验台你如果只想交一份课程设计做到上面那步已经够了。但如果你想把“电梯模拟”这个题目做得让答辩老师眼前一亮或者想在简历上写“我实现过一个电梯调度模拟系统”那下面这几个小技巧值得加进去。第一增加电梯数量。原题是单电梯改成双电梯时每个楼层按钮应该变成共享请求电梯调度需要决策哪一个电梯响应。常见做法是给每台电梯增加一个“当前位置、当前方向、剩余任务数”三元的代价函数哪个电梯代价低就分配给谁。这个可以用优先队列来实现正好把数据结构换成堆增加技术含量。第二把随机乘客生成改成泊松分布。原代码的乘客到达间隔是固定的如果你用rand()生成间隔到达模式会更接近真实场景。实现方式是把nexttime的增量从固定值改为(-log(1.0 - rand() / RAND_MAX)) * lambda其中lambda是平均到达率。这样你可以在报告中写“基于泊松过程的乘客流建模”听起来就比“固定间隔输入”高一个档次。第三把控制台输出改成文件日志再用 Python 画 Gantt 图。电梯的动作序列本质上是一堆时间戳事件你可以在每次动作后追加写一行到 CSV 文件包含时间、电梯楼层、动作类型、人数。然后用 matplotlib 画一条阶梯线表示电梯位置随时间的变化标出开门、关门、上升、下降。答辩时直接放这张图比满屏文字输出直观得多。我当时做的时候还加了乘客等待时间的直方图老师当场就问是不是用了额外库其实只用了几行 Python。第四把决定方向的那段c和d计算封装成一个独立函数int decideDirection(int floor, int nowstate)返回1向上、-1向下、0静止。这样你后续想改调度策略只需改这一个函数不用动电梯动作函数。比如实现“最长等待优先”策略计算每个方向所有等待乘客的总等待时间哪个方向总等待时间更长就去哪个方向。这比原代码里的“先向上优先”策略复杂很多但从数据结构角度看你只需要再加一个数组记录每层等待乘客的累积等待时长。最后一件事值得记住我后来每次做这类模拟项目都会先写一个“日志输出口”把每次状态变化都打印成时间,事件,楼层,人数的格式再写一个小脚本去校验时间轴是否连续。这样能在十分钟内发现逻辑漏洞而不是盯着控制台看半天看不出所以然。希望这个习惯也能帮到你至少别在答辩前一晚还在调renshu的加减问题。本文还有配套的精品资源点击获取