简介:这是一份数据结构课程设计中的电梯模拟系统毕业设计报告,适合计算机专业学生完成数据结构综合实践、课程设计或毕设选题参考。报告从课程设计任务书入手,完整给出电梯模拟系统的需求分析、概要设计、详细设计与运行测试过程,覆盖数据结构的选择与应用、算法设计与实现、C/C++编码规范、程序调试等关键环节。系统模拟某校五层教学楼的单电梯运行,通过链队列管理各楼层向上/向下等候乘客,借助五个乘客栈存放不同目标层人员,并设计开门、关门、等待、驶回一层候命等多状态调度机制;同时以0.1秒为模拟时钟单位,兼顾乘客最长容忍等待时间等约束。资源为单个doc文档,约523KB,已有139人学习;文档内含任务要求、参考文献、章节目录及完整的系统设计说明,对希望理解队列、栈在实际调度问题中的应用,或需要撰写课程设计报告的读者具有直接参考价值。
1. 电梯模拟课程设计:为什么它是数据结构最值得复现的一次实战
如果你正在做数据结构课程设计,又不想选图书管理、学生选课那种“增删改查三件套”,我强烈建议你试试电梯模拟。这份资源里写的是一个五层教学楼自动电梯的完整模拟程序,电梯会在 0 到 4 层之间响应内外部请求,乘客随机到达、排队、进出电梯,超时还会放弃等待。整个系统用 C++ 控制台程序实现,核心数据结构是五个链栈和五个链队列,外加一个带时间片的状态机。它能帮你把栈、队列、状态转换、时间模拟这些抽象概念全部落到能跑的代码里。适合正在做课程设计、需要交报告和源码的计算机专业学生,也适合想补数据结构实战的从业者。它最大的价值不是“电梯”这个题目本身,而是它把数据结构选型和业务逻辑揉在一起的完整套路——拿到手你能直接照着复现,也能在这个骨架上加自己的调度算法。
2. 系统分析与数据结构选型:链栈、链队列与七状态电梯模型
2.1 电梯问题的需求拆解:从楼层按钮到乘客忍耐时间
先别急着写代码,把需求逐条翻译成数据结构术语。题目设定是一个五层教学楼,楼层编号 0 到 4,0 是地下层,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 为什么用链栈和链队列而不是数组
教科书上线性表有顺序和链式两种存储方式,这里选链式是出于两个原因:一是乘客数量不确定,链式结构可以动态伸缩,用顺序表需要预先估计最大容量;二是课程设计的评分点通常要求“设计抽象数据类型并实现”,链栈和链队列的模板写法比直接开数组更能体现对指针和内存管理的掌握。
代码里栈和队列是模板类,LinkStack<T>和LinkQueue<T>,各自有Push、Pop、EnQueue、DeQueue、StackEmpty、QueueEmpty等操作。电梯内的乘客栈用数组LinkStack<man> k[5]表示,数组下标就是目标楼层;楼层等待队列用LinkQueue<man> s[5]表示,下标是当前楼层。这样设计的好处是,判断“电梯该不该去某一层”时,只需要检查对应栈或队列是否为空,以及按钮是否被按下,代码写起来非常直观。
2.3 电梯状态机的核心逻辑:向上、向下、静止的切换条件
状态转换函数e4()是整个程序的心脏。它先扫描当前楼层以上的所有层,判断是否有任何请求(目标按钮、向上按钮、向下按钮、栈非空、队列非空),有则置c=1表示应该向上;同样扫描下方楼层得到d=1。然后根据当前状态nowstate决定下一步动作。
这个判断有个容易忽略的点:当前楼层本身的按钮请求怎么处理?比如电梯在 2 层,2 层恰好有人按了向上,此时callup[2]==1会被当成向下的障碍还是向上的理由?代码里对当前层的处理是:if(callup[j]==1) c=1;,也就是说当前层的向上请求优先满足,然后才扫描上方。这符合实际的电梯逻辑——先让本层要上楼的人进来,再考虑去更高层。
状态切换还考虑了边界条件:电梯在最高层 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()会嵌套使用,代码里用一个LinkStack<int> 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++ 版本,代码里没有用到 C++11 之后的特性,所以用 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,其中TIME=100,也就是说 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原本是 0,nowfloor--变成 -1,calldown[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()。这样时间栈的深度始终为 1,times()和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的加减问题。
本文还有配套的精品资源,点击获取