
刷编程题库的时候很多人都会遇到“模拟电梯”这类题目。题目编号可能叫《113. 模拟电梯7》也可能叫“电梯调度模拟”“电梯运行仿真”但不管名字怎么变核心考察点几乎一致能不能用代码描述一台电梯从启动、加速、停靠、开关门到响应新请求的完整状态变化。这类题看起来亲切因为谁每天都会坐电梯真动手写起来又容易卡住因为电梯的运行不是简单的“按楼层排序”它涉及方向、状态、请求优先级和边界条件。很多初学者把电梯写成一个冒泡排序或者写出一段在最高层和最低层之间反复横跳的死循环代码根本原因不是语法不熟而是没有先在逻辑层把“电梯模型”讲清楚。这篇文章不打算只给一份代码而是以“模拟电梯”题为切入点完整拆解电梯模拟器的设计过程。你会看到电梯问题的核心难点到底是什么如何用面向对象思维设计电梯模型一套基于“电梯算法”的调度逻辑怎么写完整的 Java 代码实现和运行效果常见的死循环、请求丢失、边界错乱问题如何排查。如果你正在准备机试、课程设计或者想把状态机建模能力练扎实这篇内容建议收藏跟着从头写一遍。1. 这道题到底想考什么“模拟电梯”不是一道普通的算法题它本质上是一道“状态机 调度策略 边界处理”的综合题。很多人一看到题目就开始写排序其实跑偏了。电梯的难点可以拆成三个层次。第一层是状态建模。电梯不是一维数组它同时拥有“当前楼层”“运行方向”“运行状态”“门状态”这些属性。方向有上行、下行、静止状态有运行中、停靠中、开关门中。这些状态之间存在约束关系例如电梯不可能在开门状态下移动也不可能同时向上和向下运行。第二层是请求处理。电梯的请求来自两个方向乘客在楼层按键产生的“外部请求”以及乘客在轿厢内选楼层产生的“内部请求”。模拟题目通常会把这些请求简化成一条条输入但你要在内部把它们区分开因为电梯经过一个楼层时要看这个楼层是否有当前方向的请求而不是把所有请求一股脑处理掉。第三层是运行策略。电梯在某一时刻可能同时面对多个方向的请求应该先响应哪一个“电梯算法”给出的思路是电梯沿着当前方向继续运行先处理同方向请求当同方向没有请求时再判断是否需要换向。这个策略是所有电梯调度器的起点。如果你理解透了这三点那么“模拟电梯7”只是换了输入格式和输出要求核心逻辑完全可以复用。2. 电梯模拟器的核心概念与调度原理2.1 状态机一切电梯模型的基础电梯的状态可以抽象为以下几个维度维度取值范围说明运行方向UP / DOWN / IDLEIDLE 表示没有请求时的静止状态运行状态MOVING / STOPPEDSTOPPED 表示当前停靠门状态OPEN / CLOSED模拟题里可以简化为一个状态字段当前楼层整数范围在 1 到 maxFloor 之间在实际代码中方向是最核心的状态因为调度逻辑完全依赖它。建议使用枚举而不是字符串常量这样在 switch 判断和参数传递时更安全。2.2 请求的分类与去重电梯请求可以分为两类上行请求乘客在某个楼层按下“上”按钮说明想去更高楼层下行请求乘客在某个楼层按下“下”按钮说明想去更低楼层。这里要注意一个常见误区同一个楼层同时可能有上行和下行请求它们是两个不同的请求需要分开存储。例如5 楼有人要上去同时 5 楼也有人要下去电梯经过 5 楼时如果方向是上行应该只处理上行请求下行请求留到换向后处理。另外同一楼层同一方向可能有多人请求模拟的时候只需要记录一次不需要重复响应。所以用 Set 结构去重比用 List 更合理。2.3 电梯算法同向优先边界换向电梯调度用得最多的基础算法是 SCAN也叫“电梯算法”。它的核心思想是电梯沿当前方向运行处理该方向上的所有请求当前方向没有请求时判断另一端是否还有请求如果有则换向继续运行如果所有请求都处理完回到 IDLE 状态。用一句话概括就是“走到头再回头”。这种算法的优点是实现简单、逻辑清晰缺点是响应时间不稳定极端场景下可能让某个请求等待较长时间。但在编程模拟题中SCAN 已经足够也是考官最想看到的调度逻辑。3. 系统设计与数据结构选择3.1 类设计为了让代码清晰建议把电梯模拟拆成三个类Direction方向枚举Elevator电梯实体维护楼层位置、方向、边界ElevatorController调度控制器负责接收请求、判断方向、执行移动Main主程序负责读取输入并驱动模拟循环。这种拆分的价值在于电梯实体不关心调度策略控制器不关心楼层边界职责分离后后续扩展多电梯、不同调度策略都更容易。3.2 为什么请求集合用 TreeSet在调度过程中电梯反复要做两件事判断当前方向上是否还有请求找到当前方向上的下一个请求楼层。如果用 ArrayList每次都需要遍历而且还要考虑去重和删除代码会很别扭。这里推荐使用TreeSetInteger它自带自然排序并且提供两个很关键的方法ceiling(floor)返回大于等于指定楼层的最小请求floor(floor)返回小于等于指定楼层的最大请求。电梯向上运行时用ceiling判断上方是否还有请求向下运行时用floor判断下方是否还有请求。每次到达楼层后直接remove即可时间复杂度也比较理想。3.3 调度主流程每一次“模拟步”可以拆成三个阶段服务当前楼层移除当前楼层在当前方向上能处理的请求重新决策方向根据剩余请求和当前方向决定下一步方向执行移动按方向移动一层。关键点在第 2 步。很多人的死循环都出在这里如果方向判定条件写反电梯会一直在两三个楼层之间来回走。后面会给出完整代码并解释判定逻辑。4. 环境准备与项目结构本文的示例使用 Java 编写需要的环境非常简单JDK 8 及以上版本任意文本编辑器或 IDE命令行终端。不使用 Maven 或 Gradle也没有第三方依赖方便机试环境直接编译运行。项目目录结构如下elevator-simulator/ ├── Direction.java ├── Elevator.java ├── ElevatorController.java └── Main.java也可以全部写在同一个.java文件里但建议分开逻辑更清晰。5. 完整代码实现5.1 方向枚举Direction.javapublic enum Direction { UP, DOWN, IDLE }IDLE代表静止状态当没有请求时电梯会自动回到这个状态。在判断方向时加入IDLE可以避免“电梯没有方向还在移动”的 bug。5.2 电梯实体Elevator.javapublic class Elevator { private int currentFloor; private int minFloor; private int maxFloor; private Direction direction; public Elevator(int startFloor, int minFloor, int maxFloor) { this.currentFloor startFloor; this.minFloor minFloor; this.maxFloor maxFloor; this.direction Direction.IDLE; } public int getCurrentFloor() { return currentFloor; } public int getMinFloor() { return minFloor; } public int getMaxFloor() { return maxFloor; } public Direction getDirection() { return direction; } public void setDirection(Direction direction) { this.direction direction; } public void moveUp() { if (currentFloor maxFloor) { currentFloor; } } public void moveDown() { if (currentFloor minFloor) { currentFloor--; } } }这里把移动动作封装成moveUp()和moveDown()方法内部做边界检查。这样控制器就不用关心楼层边界代码更安全。5.3 调度控制器ElevatorController.java这是整个模拟器的核心请求存储、方向决策、移动触发都在这里完成。import java.util.TreeSet; public class ElevatorController { private final Elevator elevator; private final TreeSetInteger upRequests new TreeSet(); private final TreeSetInteger downRequests new TreeSet(); public ElevatorController(Elevator elevator) { this.elevator elevator; } public void request(int floor, Direction direction) { if (floor elevator.getMinFloor() || floor elevator.getMaxFloor()) { System.out.println(非法楼层请求已忽略 floor); return; } if (direction Direction.UP) { upRequests.add(floor); } else if (direction Direction.DOWN) { downRequests.add(floor); } else { System.out.println(非法方向请求已忽略 floor); } } public boolean hasPendingRequests() { return !upRequests.isEmpty() || !downRequests.isEmpty(); } public void step() { int currentFloor elevator.getCurrentFloor(); // 1. 服务当前楼层 boolean served upRequests.remove(currentFloor); served | downRequests.remove(currentFloor); if (served) { System.out.println( 电梯停靠 currentFloor 层开门上下客); } // 2. 重新判断方向 decideDirection(); // 3. 执行移动 Direction direction elevator.getDirection(); if (direction Direction.UP) { elevator.moveUp(); } else if (direction Direction.DOWN) { elevator.moveDown(); } } private void decideDirection() { int currentFloor elevator.getCurrentFloor(); Direction currentDirection elevator.getDirection(); if (upRequests.isEmpty() downRequests.isEmpty()) { elevator.setDirection(Direction.IDLE); return; } if (currentDirection Direction.IDLE) { boolean hasRequestAbove (!upRequests.isEmpty() upRequests.last() currentFloor) || (!downRequests.isEmpty() downRequests.last() currentFloor); elevator.setDirection(hasRequestAbove ? Direction.UP : Direction.DOWN); return; } if (currentDirection Direction.UP) { boolean hasRequestAbove (!upRequests.isEmpty() upRequests.last() currentFloor) || (!downRequests.isEmpty() downRequests.last() currentFloor); if (!hasRequestAbove) { elevator.setDirection(Direction.DOWN); } } else if (currentDirection Direction.DOWN) { boolean hasRequestBelow (!upRequests.isEmpty() upRequests.first() currentFloor) || (!downRequests.isEmpty() downRequests.first() currentFloor); if (!hasRequestBelow) { elevator.setDirection(Direction.UP); } } } }这段方向的判断逻辑需要重点解释一下。以电梯当前向上为例判断条件并不是“是否有上行请求”而是“是否有任何请求在当前楼层上方”。只要上方还有请求电梯就继续向上走。这个设计呼应了 SCAN 算法的思想电梯向一个方向运行时会顺路处理所有方向相反的请求所以上行途中的下行请求同样会在这个阶段被服务掉直到上方再没有请求才换向。有人可能会问如果电梯正在向下运行但下方已经没有请求了同时上方还有请求方向会立刻切换成 UP。这个逻辑通过 else 分支实现了。但这套算法追求的是简单和正确并不保证响应时间最短。5.4 主程序Main.javaimport java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner new Scanner(System.in); System.out.println(请输入总楼层数和初始楼层空格分隔); int maxFloor scanner.nextInt(); int startFloor scanner.nextInt(); Elevator elevator new Elevator(startFloor, 1, maxFloor); ElevatorController controller new ElevatorController(elevator); System.out.println(请输入请求数量); int n scanner.nextInt(); System.out.println(请输入 n 条请求每行格式楼层 方向(UP/DOWN)); for (int i 0; i n; i) { int floor scanner.nextInt(); String dir scanner.next(); controller.request(floor, Direction.valueOf(dir.toUpperCase())); } int step 0; while (controller.hasPendingRequests()) { controller.step(); System.out.println(执行一步后电梯在第 elevator.getCurrentFloor() 层方向 elevator.getDirection()); step; if (step 200) { System.out.println(步数超过200疑似死循环已终止); break; } } System.out.println(模拟结束共执行 step 步); } }主程序做了一个“步数保护”超过 200 步强制结束。这个设计对于模拟类程序非常有价值。一旦调度逻辑写错电梯陷入死循环如果不设置上限程序会一直跑下去测试时很难发现。6. 运行结果与效果验证编译和运行命令如下javac Direction.java Elevator.java ElevatorController.java Main.java java Main使用下面的输入10 1 4 5 UP 8 DOWN 3 UP 4 DOWN含义是总共 10 层电梯从 1 层出发有 4 条请求5 楼有人要上行8 楼有人要下行3 楼有人要上行4 楼有人要下行。程序运行后的关键过程应该是电梯从 1 层出发先向上到达 3 层服务上行请求开门继续向上到达 4 层服务下行请求开门继续向上到达 5 层服务上行请求开门继续向上到达 8 层服务下行请求开门所有请求处理完毕电梯变为 IDLE。跑通这个用例说明基础调度逻辑没有问题。建议多测试几组边界用例电梯初始楼层就是最高层请求都在下方所有请求都在同一层请求楼层重复请求楼层超出建筑范围。这些都是机试常见的隐藏用例特别值得提前验证。7. 常见问题与排查思路问题现象可能原因排查方式解决方案电梯只向上永远不向下向上方向判断缺少对反向请求的检查导致上方无请求时没有触发换向在方向判断分支打印当前剩余请求集合使用“是否存在上方请求”替代“是否存在同向请求”电梯死循环在两个楼层之间来回走服务请求后没有及时 remove或换向条件自相矛盾在 step 方法开始和结束分别打印当前楼层和请求集合确保 remove 在移动前执行方向判断条件只能有一个出口请求楼层处理了两次使用了 List 存储请求没有去重打印每次 remove 结果改用 TreeSet 存储请求电梯到了最高层还继续向上moveUp 缺少边界检查在 moveUp 里打印当前楼层和 maxFloor移动方法内部增加上下限判断换向后漏掉反方向请求电梯只在上行集合里找请求没有检查下行集合检查 decideDirection 里的判断条件判断方向时同时检查 upRequests 和 downRequests输入非法楼层后程序崩溃request 方法没有做边界校验在 request 入口增加日志增加楼层范围和方向合法性判断这些问题里死循环是最常见的。排查时不要看完整输出应该看最后几步的日志如果电梯一直在 A 层和 A1 层之间来回走说明换向条件和移动条件冲突了优先检查decideDirection。8. 最佳实践与工程建议8.1 用枚举代替魔法值方向字段不要用字符串 up/down也不要直接用 1/-1 表示。使用枚举可以让代码自解释还能避免拼写错误。尤其在 Controller 的请求入口Direction.valueOf()直接提供了输入校验能力。8.2 请求集合选型很重要TreeSet 在这个模拟场景里几乎是量身定做的。它天然去重、自动排序配合ceiling和floor方法可以很容易地找到当前位置上下方的请求。如果用 ArrayList去重要手动判断找“最近请求”要遍历代码量会明显增加而且容易出错。8.3 模拟循环要加步数保护机试环境不会让你的死循环跑太久但也别指望系统能给你明确报错。自己加上步数保护一旦超过合理上限就强制退出并打印当前请求状态能省去大量调试时间。8.4 将调度策略与电梯实体分离这个设计看似简单但在后续扩展时价值很大想改成多电梯调度只需要增加多个 Elevator 实例并让 Controller 管理多个请求队列想换成最短寻道优先算法只需要重写decideDirection想加开关门时间只需要在step中增加时间字段。所以在写模拟器时不要把所有逻辑塞到 Main 方法里。实体、调度、输入输出分开是工程化模拟器的基础。8.5 在真实系统设计中的应用电梯模拟看似是教学题但它的建模思路在真实系统中很常见。电梯调度器对应着任务调度系统请求队列对应消息队列的待处理任务方向决策对应负载均衡策略换向逻辑对应任务消费的优先级切换。掌握电梯模拟其实是在掌握“一个有状态的服务如何高效处理乱序请求”这一类问题。9. 总结与后续扩展方向“模拟电梯”类题目真正的价值不在于把某一道题的样例跑通而在于训练一种思维把现实世界中的设备状态、事件请求、运行规则翻译成代码中的状态字段、集合操作和条件分支。本文给出了一个单电梯的 SCAN 调度实现完整的代码结构分成 Direction、Elevator、ElevatorController、Main 四部分。你可以直接编译运行也可以在理解思路后尝试以下扩展把单电梯改成多电梯并设计简单的“空闲电梯优先”策略加入电梯内部请求模拟区分内呼和外呼在每次停靠时累加时间比较不同调度策略的总运行时间把控制台输出改成 GUI 可视化。如果你能把上面的扩展独立完成说明状态机建模和调度逻辑已经真正掌握了。下次再看到任何编号的“模拟电梯”题目都可以从容地把这套核心模型迁移过去只要针对输入输出格式做适配就够了。