资讯详情 数据结构电梯模拟:优先队列与调度策略实现
📅 2026/10/17 15:05:28
简介这份资源是面向计算机专业学生的数据结构课程设计报告以电梯模拟系统为课题适合正在准备毕业设计或课程设计、需要参考完整实现思路与文档规范的学习者。报告围绕五层教学楼自动电梯系统展开涵盖乘客进出、等候队列、电梯状态转换等核心逻辑并给出抽象数据类型定义、系统分析与概要设计过程。压缩包内共1个doc文件约523KB为完整的课程设计报告文档包含任务书、目录、概述、系统分析、概要设计、详细设计、运行测试与总结等章节可直接作为撰写模板与算法设计参考。已有139人学习下载。读者可从中获取电梯调度中链队列与栈的具体应用方式、时间单位驱动的模拟时序设计、以及从问题分析到编码测试的完整软件开发流程对理解数据结构的选择与算法实现具有实际参考价值。1. 数据结构电梯模拟从课程设计到能跑起来的调度内核很多人做「数据结构电梯模拟」这个题第一反应是画个界面、几个按钮、电梯上下动一动就交差。但真正让这个题有价值的地方是藏在界面背后的调度逻辑——它本质上是一个带约束的实时请求调度问题而不是动画演示。我见过太多版本界面做得花哨一跑多楼层多请求就乱序、卡死、饿死某个方向的乘客。这篇笔记就围绕这个题把需求拆清楚、把数据结构选型讲透、把可复现的实现路径写出来顺带把几个必调的参数和常见翻车点讲明白。适合正在做课程设计的学生也适合想拿它练手调度算法的开发者。这个题的核心诉求其实就三条请求怎么存、电梯怎么选下一个目标、状态怎么推进。存请求决定了你能不能高效地插入和取出选目标决定了调度策略是「先来先服务」还是「顺路优先」状态推进决定了模拟是离散事件驱动还是固定时间步。把这三件事想清楚代码量并不大但边界条件特别多稍不注意就会出现电梯在两个楼层之间反复横跳的玄学现象。下面按「先立住模型、再动手实现、最后排坑」的顺序展开。2. 电梯调度到底在模拟什么请求模型与状态机设计2.1 把现实电梯抽象成三个集合做模拟的第一步不是写代码而是把现实场景抽象成可计算的对象。我一般会定义三个核心集合楼层集合、请求集合、电梯状态集合。楼层集合就是 1 到 N 的整数N 通常取 5 到 20太小体现不出调度差异太大调试时眼累。请求集合里每个元素至少包含四个字段请求楼层、目标楼层、请求方向、到达时间。注意「请求方向」这个字段它是区分「上行请求」和「下行请求」的关键很多初学者只存楼层不存方向结果电梯到了那一层却不知道该往哪走。电梯状态集合则要记录当前楼层、当前方向上行/下行/空闲、当前载客目标列表、门开关状态。这里有个容易忽略的点——电梯的「方向」和「请求的方向」是两回事。电梯正在上行不代表它只响应上行请求它可能在到达某个上行请求的目标层后转而响应更高层的下行请求。把这两个方向分开建模后面调度逻辑才不会绕晕。抽象完之后整个模拟就变成一个循环时间推进 → 产生新请求 → 调度器选目标 → 电梯移动 → 到达处理 → 更新状态。这个循环用离散事件驱动比固定时间步更准因为电梯在两楼层之间的耗时是固定的没必要每毫秒都算一次。2.2 为什么用优先队列而不是普通列表请求存哪里直接决定调度效率。用普通列表存请求每次选下一个目标都要遍历全部请求时间复杂度 O(n)请求一多模拟跑起来就明显卡顿。常见做法是用两个优先队列一个存上行请求一个存下行请求队列内部按楼层排序。这样取「当前方向上最近的请求」就是 O(1) 或 O(log n)。具体来说上行请求队列按楼层升序排列下行请求队列按楼层降序排列。电梯当前上行时就看上行队列的队首如果上行队列空了再检查下行队列里有没有比当前楼层低的请求有就掉头。这个「掉头判断」是调度器的灵魂写错了就会出现电梯空跑或者乘客等不到的情况。提示优先队列的排序键不要只用楼层最好带上请求到达时间做次级排序。同一楼层多个请求时先到的先服务避免后到的请求因为楼层相同而插队。用 Python 实现的话可以用heapq模块但要注意heapq是小顶堆下行请求需要存负数楼层来模拟大顶堆。下面这段代码就是请求队列的初始化逻辑import heapq class ElevatorScheduler: def __init__(self, total_floors): self.total_floors total_floors # 上行请求小顶堆楼层小的先出 self.up_queue [] # 下行请求存负数小顶堆模拟大顶堆楼层大的先出 self.down_queue [] self.current_floor 1 self.direction 0 # 0 空闲1 上行-1 下行 def add_request(self, floor, target, direction): direction: 1 表示上行请求-1 表示下行请求 if direction 1: heapq.heappush(self.up_queue, (floor, target)) else: heapq.heappush(self.down_queue, (-floor, target))这段代码里up_queue和down_queue分别管理两个方向的请求。add_request根据请求方向入队上行存正楼层下行存负楼层。参数floor是请求所在楼层target是目标楼层direction是请求方向。注意这里没有做去重实际项目中同一楼层同一方向的重复请求应该在入队前合并否则电梯会在同一层停多次。2.3 状态机的四个状态与转移条件电梯的状态机我一般设计成四个状态空闲、上行、下行、开门中。空闲状态是电梯没有待处理请求时的状态此时它停在当前楼层不动。上行和下行是移动状态开门中是到达目标层后的短暂停留状态。状态转移的条件必须写清楚否则模拟会出现「电梯到了目标层却不停」或者「没到目标层就开门」的 bug。转移规则是这样的空闲状态下如果上行队列非空转入上行否则如果下行队列非空转入下行。上行状态下如果当前楼层等于上行队列队首楼层转入开门中开门结束后如果该请求的目标楼层高于当前楼层继续上行否则检查下行队列。下行状态对称处理。开门中状态持续一个固定时间单位然后回到上行或下行或空闲。这里的关键参数是「开门时间」和「楼层间移动时间」。我一般设开门时间为 2 个时间单位楼层间移动为 1 个时间单位。这两个参数直接影响模拟的总耗时调的时候要保证开门时间大于 0否则电梯会在同一层反复开关门。3. 用 Python 把调度内核跑通从请求入队到电梯移动3.1 最小可运行版本的目录与依赖这个模拟不需要任何第三方库标准库的heapq和time就够了。我一般会建一个单文件脚本结构分成三部分调度器类、模拟循环、结果输出。调度器类负责请求管理和状态转移模拟循环负责推进时间结果输出负责打印每步的状态变化。这样拆分的好处是调试时可以直接调用调度器的方法不用跑整个循环。依赖方面Python 3.7 以上即可没有版本坑。如果你要用图形界面可以加tkinter但建议先把命令行版本跑通再套界面否则界面会掩盖逻辑错误。下面是一个最小可运行版本的骨架import heapq class Elevator: def __init__(self, total_floors): self.total_floors total_floors self.current_floor 1 self.direction 0 self.up_queue [] self.down_queue [] self.passengers [] # 当前载客的目标楼层列表 self.time 0 def add_request(self, floor, target, direction): if direction 1: heapq.heappush(self.up_queue, (floor, target)) else: heapq.heappush(self.down_queue, (-floor, target)) def step(self): 推进一个时间单位 self.time 1 if self.direction 0: self._pick_direction() elif self.direction 1: self._move_up() elif self.direction -1: self._move_down() def _pick_direction(self): if self.up_queue: self.direction 1 elif self.down_queue: self.direction -1 def _move_up(self): self.current_floor 1 if self.up_queue and self.current_floor self.up_queue[0][0]: floor, target heapq.heappop(self.up_queue) self.passengers.append(target) print(f时间 {self.time}: 在 {floor} 层接客目标 {target} 层) if not self.up_queue and not self.passengers: self.direction 0 def _move_down(self): self.current_floor - 1 if self.down_queue and self.current_floor -self.down_queue[0][0]: floor, target heapq.heappop(self.down_queue) self.passengers.append(target) print(f时间 {self.time}: 在 {floor} 层接客目标 {target} 层) if not self.down_queue and not self.passengers: self.direction 0这段代码里step方法是模拟的核心每次调用推进一个时间单位。_pick_direction在空闲时决定方向_move_up和_move_down处理移动和接客。参数total_floors是总楼层数current_floor是当前楼层direction是方向。注意_move_up里判断接客的条件是current_floor self.up_queue[0][0]这里假设请求楼层就是接客楼层实际场景中请求楼层和目标楼层可能不同需要根据业务调整。3.2 请求入队与去重的三个参数请求入队时有三个参数必须想清楚请求楼层、目标楼层、请求方向。请求楼层是乘客按按钮的楼层目标楼层是乘客想去的地方请求方向是乘客的意图方向。这三个参数决定了请求进入哪个队列。如果乘客在 5 楼想去 10 楼请求方向是上行进入上行队列如果乘客在 10 楼想去 5 楼请求方向是下行进入下行队列。去重逻辑是很多人忽略的。同一楼层同一方向的多个请求应该合并成一个否则电梯会在同一层停多次。合并的规则是如果上行队列里已经有楼层 5 的请求新来的楼层 5 上行请求就不再入队只更新目标楼层列表。这个逻辑可以用一个字典来辅助键是楼层值是目标楼层集合。def add_request_dedup(self, floor, target, direction): 带去重的请求入队 if direction 1: for i, (f, t) in enumerate(self.up_queue): if f floor: # 已存在同楼层请求合并目标 self.up_queue[i] (f, t) # 实际应用中可以存目标列表 return heapq.heappush(self.up_queue, (floor, target)) else: for i, (f, t) in enumerate(self.down_queue): if -f floor: return heapq.heappush(self.down_queue, (-floor, target))这段代码演示了去重的基本思路遍历队列检查是否已有同楼层请求。参数floor和target含义同上direction决定检查哪个队列。注意这里为了演示简化了目标合并实际项目中可以用列表存多个目标。去重能显著减少电梯的停靠次数提升模拟效率。3.3 电梯移动与到达判断的边界处理电梯移动时最容易出问题的是到达判断。电梯从 3 楼到 5 楼中间经过 4 楼如果 4 楼有请求电梯应该停。但如果 4 楼的请求方向与电梯当前方向相反电梯不应该停而是继续走到 5 楼再掉头。这个「顺路才停」的逻辑是调度器的核心写错了就会出现电梯频繁掉头、乘客等待时间暴涨。我一般会在移动方法里加一个判断只有当请求方向与电梯当前方向一致或者电梯已经到达队列尽头时才停靠。具体实现时可以在_move_up里检查self.up_queue[0][0] self.current_floor同时检查self.direction 1。如果方向不一致就把请求暂存等电梯掉头后再处理。注意电梯到达顶层或底层时必须强制掉头否则会出现数组越界或死循环。顶层和底层的判断要放在移动之前避免电梯冲出楼层范围。边界处理还包括「空队列时的行为」。如果上行队列和下行队列都空了电梯应该停在当前楼层方向设为空闲。这个判断要放在每次移动之后否则电梯会在空闲状态下继续移动浪费模拟时间。4. 调度策略选型先来先服务、顺路优先与最短寻道4.1 三种策略的适用场景对比调度策略决定了电梯的响应顺序常见的有三种先来先服务、顺路优先、最短寻道优先。先来先服务就是按请求到达时间排序谁先来先服务谁实现最简单但效率最低容易出现电梯来回跑的情况。顺路优先是电梯沿着当前方向一直走沿途响应同方向请求走到尽头再掉头这是现实电梯最常用的策略效率高但实现复杂。最短寻道优先是每次选离当前楼层最近的请求不管方向响应快但可能导致某个方向的请求饿死。策略实现难度平均等待时间适用场景先来先服务低高请求少、楼层少的教学演示顺路优先中低多楼层、多请求的真实模拟最短寻道中中请求分布均匀、追求响应速度选型时如果只是课程设计演示先来先服务够用如果要体现调度算法的价值顺路优先是首选。最短寻道适合做对比实验但不建议作为主策略因为饿死问题在模拟中很难解释清楚。4.2 顺路优先的实现方向锁与掉头条件顺路优先的核心是「方向锁」电梯一旦确定方向就沿着这个方向一直走直到没有同方向请求为止。方向锁的实现需要在调度器里加一个direction_locked标志电梯移动时检查这个标志只有到达尽头或同方向队列为空时才解锁。掉头条件有两个一是当前方向队列为空二是电梯到达顶层或底层。两个条件满足任意一个就掉头。掉头时电梯方向取反然后从反方向队列里取请求。这个逻辑用代码实现如下def _move_up_with_lock(self): self.current_floor 1 # 沿途检查是否有同方向请求 while self.up_queue and self.up_queue[0][0] self.current_floor: floor, target heapq.heappop(self.up_queue) self.passengers.append(target) print(f时间 {self.time}: 在 {floor} 层接客目标 {target} 层) # 到达顶层或上行队列为空掉头 if self.current_floor self.total_floors or not self.up_queue: self.direction -1 if self.down_queue else 0这段代码里_move_up_with_lock在移动后检查同方向请求沿途接客。参数total_floors用于判断顶层up_queue和down_queue用于判断是否掉头。注意while循环处理同一楼层的多个请求避免遗漏。掉头时优先检查下行队列如果有请求就转向下行否则设为空闲。4.3 参数调优开门时间与移动时间的比例开门时间和移动时间的比例直接影响模拟的总耗时和乘客等待时间。我一般设移动时间为 1开门时间为 2这样电梯停靠一次的代价相当于移动两层。如果开门时间设得太短电梯会频繁停靠总耗时反而增加如果设得太长乘客等待时间会明显上升。调参时可以用一个简单的实验固定请求序列分别设开门时间为 1、2、3跑完模拟后比较总耗时和平均等待时间。通常开门时间为 2 时综合表现最好。这个参数没有绝对最优值取决于请求的分布。请求集中在少数楼层时开门时间可以短一些请求分散时开门时间要长一些避免频繁启停。提示模拟的总耗时和平均等待时间是两个不同的指标。总耗时关注电梯的效率平均等待时间关注乘客的体验。调参时要同时看这两个指标不能只优化一个。5. 避坑与排查电梯模拟里最容易翻车的五个地方5.1 电梯在两层之间反复横跳现象电梯从 3 楼到 4 楼又回到 3 楼再上 4 楼循环不停。原因掉头条件写错了电梯到达 4 楼后上行队列为空但下行队列有 3 楼的请求电梯掉头下行到了 3 楼又发现上行队列有 4 楼请求再次掉头。解决掉头时不要只看队列是否为空还要看请求方向是否与当前方向一致。如果下行队列的请求方向是上行电梯不应该掉头去接而是等上行请求处理完再掉头。5.2 同一楼层重复停靠现象电梯在同一楼层停多次每次开门时间都消耗掉。原因请求入队时没有去重同一楼层同一方向的多个请求分别入队。解决在add_request里加去重逻辑用字典或遍历检查同楼层请求合并目标楼层。去重后电梯在同一楼层只停一次。5.3 电梯冲出楼层范围现象电梯从顶层继续上行楼层数超出total_floors导致数组越界或逻辑错误。原因移动前没有检查边界或者掉头条件没有覆盖顶层和底层。解决在_move_up和_move_down开头加边界判断如果current_floor total_floors且方向为上强制掉头如果current_floor 1且方向为下强制掉头。5.4 乘客目标楼层被忽略现象电梯接了乘客但乘客的目标楼层没有加入停靠列表导致乘客一直待在电梯里。原因接客时只处理了请求楼层没有把目标楼层加入passengers列表。解决在接客逻辑里把target加入passengers并在移动时检查current_floor是否在passengers中如果在就停靠并移除。5.5 模拟循环无法终止现象模拟跑了几万步还在继续没有结束条件。原因请求队列永远不为空或者电梯状态没有正确更新。解决在模拟循环里加终止条件当所有队列为空且passengers为空时跳出循环。同时检查请求入队逻辑确保请求被正确消费。6. 进阶技巧用事件驱动替代时间步进时间步进模拟的缺点是每一步都要检查所有状态效率低。事件驱动模拟只在关键事件发生时推进时间比如请求到达、电梯到达楼层、开门结束。这样模拟速度更快也更接近真实电梯的运行逻辑。实现事件驱动的关键是维护一个事件优先队列按事件时间排序每次取出最早的事件处理。import heapq class EventDrivenElevator: def __init__(self, total_floors): self.total_floors total_floors self.current_floor 1 self.direction 0 self.events [] # (时间, 事件类型, 参数) self.time 0 def schedule_event(self, delay, event_type, **kwargs): heapq.heappush(self.events, (self.time delay, event_type, kwargs)) def run(self): while self.events: self.time, event_type, kwargs heapq.heappop(self.events) if event_type request: self._handle_request(**kwargs) elif event_type arrive: self._handle_arrive(**kwargs) elif event_type open_door: self._handle_open_door(**kwargs) def _handle_request(self, floor, target, direction): # 处理请求安排到达事件 travel_time abs(floor - self.current_floor) self.schedule_event(travel_time, arrive, floorfloor, targettarget) def _handle_arrive(self, floor, target): self.current_floor floor self.schedule_event(2, open_door, floorfloor, targettarget) def _handle_open_door(self, floor, target): print(f时间 {self.time}: 在 {floor} 层开门目标 {target} 层) # 安排下一个请求或移动事件这段代码里schedule_event把事件按时间加入优先队列run循环取出最早事件处理。参数delay是延迟时间event_type是事件类型kwargs是事件参数。事件驱动的好处是时间跳跃推进不用一步步模拟。注意_handle_request里计算旅行时间时假设电梯直接前往请求楼层实际场景中要考虑顺路停靠需要更复杂的路径计算。事件驱动的难点在于事件之间的依赖关系。比如电梯到达楼层后开门事件和下一个移动事件有先后顺序必须保证开门结束后再移动。我一般会在开门事件处理完后再安排移动事件延迟设为开门时间。这样事件队列里始终只有必要的事件不会出现时间倒流或事件冲突。调试事件驱动模拟时建议打印每个事件的时间和类型观察事件序列是否符合预期。如果发现事件时间不单调递增说明事件调度逻辑有问题需要检查schedule_event的延迟计算。这个技巧我踩过坑当时事件时间倒流导致模拟结果完全错乱后来加了时间戳校验才定位到问题。最后说个习惯我每次写完调度器都会先用固定请求序列跑一遍手动核对每一步的楼层和方向确认逻辑无误后再加随机请求。这个笨办法帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取