模拟电梯调度算法详解:从状态机到Java实现
2026/8/27 3:06:19 网站建设 项目流程

刷编程题库的时候,很多人都会遇到“模拟电梯”这类题目。题目编号可能叫《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,也叫“电梯算法”。它的核心思想是:

  1. 电梯沿当前方向运行,处理该方向上的所有请求;
  2. 当前方向没有请求时,判断另一端是否还有请求;
  3. 如果有,则换向继续运行;
  4. 如果所有请求都处理完,回到 IDLE 状态。

用一句话概括就是“走到头再回头”。这种算法的优点是实现简单、逻辑清晰,缺点是响应时间不稳定,极端场景下可能让某个请求等待较长时间。但在编程模拟题中,SCAN 已经足够,也是考官最想看到的调度逻辑。

3. 系统设计与数据结构选择

3.1 类设计

为了让代码清晰,建议把电梯模拟拆成三个类:

  • Direction:方向枚举;
  • Elevator:电梯实体,维护楼层位置、方向、边界;
  • ElevatorController:调度控制器,负责接收请求、判断方向、执行移动;
  • Main:主程序,负责读取输入并驱动模拟循环。

这种拆分的价值在于:电梯实体不关心调度策略,控制器不关心楼层边界,职责分离后,后续扩展多电梯、不同调度策略都更容易。

3.2 为什么请求集合用 TreeSet

在调度过程中,电梯反复要做两件事:

  • 判断当前方向上是否还有请求;
  • 找到当前方向上的下一个请求楼层。

如果用 ArrayList,每次都需要遍历,而且还要考虑去重和删除,代码会很别扭。这里推荐使用TreeSet<Integer>,它自带自然排序,并且提供两个很关键的方法:

  • ceiling(floor):返回大于等于指定楼层的最小请求;
  • floor(floor):返回小于等于指定楼层的最大请求。

电梯向上运行时,用ceiling判断上方是否还有请求;向下运行时,用floor判断下方是否还有请求。每次到达楼层后直接remove即可,时间复杂度也比较理想。

3.3 调度主流程

每一次“模拟步”可以拆成三个阶段:

  1. 服务当前楼层:移除当前楼层在当前方向上能处理的请求;
  2. 重新决策方向:根据剩余请求和当前方向决定下一步方向;
  3. 执行移动:按方向移动一层。

关键点在第 2 步。很多人的死循环都出在这里:如果方向判定条件写反,电梯会一直在两三个楼层之间来回走。后面会给出完整代码并解释判定逻辑。

4. 环境准备与项目结构

本文的示例使用 Java 编写,需要的环境非常简单:

  • JDK 8 及以上版本;
  • 任意文本编辑器或 IDE;
  • 命令行终端。

不使用 Maven 或 Gradle,也没有第三方依赖,方便机试环境直接编译运行。

项目目录结构如下:

elevator-simulator/ ├── Direction.java ├── Elevator.java ├── ElevatorController.java └── Main.java

也可以全部写在同一个.java文件里,但建议分开,逻辑更清晰。

5. 完整代码实现

5.1 方向枚举:Direction.java

public enum Direction { UP, DOWN, IDLE }

IDLE代表静止状态,当没有请求时电梯会自动回到这个状态。在判断方向时加入IDLE,可以避免“电梯没有方向还在移动”的 bug。

5.2 电梯实体:Elevator.java

public 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 TreeSet<Integer> upRequests = new TreeSet<>(); private final TreeSet<Integer> 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.java

import 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. 电梯从 1 层出发,先向上;
  2. 到达 3 层,服务上行请求,开门;
  3. 继续向上,到达 4 层,服务下行请求,开门;
  4. 继续向上,到达 5 层,服务上行请求,开门;
  5. 继续向上,到达 8 层,服务下行请求,开门;
  6. 所有请求处理完毕,电梯变为 IDLE。

跑通这个用例,说明基础调度逻辑没有问题。

建议多测试几组边界用例:

  • 电梯初始楼层就是最高层,请求都在下方;
  • 所有请求都在同一层;
  • 请求楼层重复;
  • 请求楼层超出建筑范围。

这些都是机试常见的隐藏用例,特别值得提前验证。

7. 常见问题与排查思路

问题现象可能原因排查方式解决方案
电梯只向上,永远不向下向上方向判断缺少对反向请求的检查,导致上方无请求时没有触发换向在方向判断分支打印当前剩余请求集合使用“是否存在上方请求”替代“是否存在同向请求”
电梯死循环,在两个楼层之间来回走服务请求后没有及时 remove,或换向条件自相矛盾在 step 方法开始和结束分别打印当前楼层和请求集合确保 remove 在移动前执行,方向判断条件只能有一个出口
请求楼层处理了两次使用了 List 存储请求,没有去重打印每次 remove 结果改用 TreeSet 存储请求
电梯到了最高层还继续向上moveUp 缺少边界检查在 moveUp 里打印当前楼层和 maxFloor移动方法内部增加上下限判断
换向后漏掉反方向请求电梯只在上行集合里找请求,没有检查下行集合检查 decideDirection 里的判断条件判断方向时同时检查 upRequests 和 downRequests
输入非法楼层后程序崩溃request 方法没有做边界校验在 request 入口增加日志增加楼层范围和方向合法性判断

这些问题里,死循环是最常见的。排查时不要看完整输出,应该看最后几步的日志:如果电梯一直在 A 层和 A+1 层之间来回走,说明换向条件和移动条件冲突了,优先检查decideDirection

8. 最佳实践与工程建议

8.1 用枚举代替魔法值

方向字段不要用字符串 "up"/"down",也不要直接用 1/-1 表示。使用枚举可以让代码自解释,还能避免拼写错误。尤其在 Controller 的请求入口,Direction.valueOf()直接提供了输入校验能力。

8.2 请求集合选型很重要

TreeSet 在这个模拟场景里几乎是量身定做的。它天然去重、自动排序,配合ceilingfloor方法,可以很容易地找到当前位置上下方的请求。如果用 ArrayList,去重要手动判断,找“最近请求”要遍历,代码量会明显增加,而且容易出错。

8.3 模拟循环要加步数保护

机试环境不会让你的死循环跑太久,但也别指望系统能给你明确报错。自己加上步数保护,一旦超过合理上限就强制退出并打印当前请求状态,能省去大量调试时间。

8.4 将调度策略与电梯实体分离

这个设计看似简单,但在后续扩展时价值很大:

  • 想改成多电梯调度,只需要增加多个 Elevator 实例,并让 Controller 管理多个请求队列;
  • 想换成最短寻道优先算法,只需要重写decideDirection
  • 想加开关门时间,只需要在step中增加时间字段。

所以在写模拟器时,不要把所有逻辑塞到 Main 方法里。实体、调度、输入输出分开,是工程化模拟器的基础。

8.5 在真实系统设计中的应用

电梯模拟看似是教学题,但它的建模思路在真实系统中很常见。

电梯调度器对应着任务调度系统:请求队列对应消息队列的待处理任务;方向决策对应负载均衡策略;换向逻辑对应任务消费的优先级切换。掌握电梯模拟,其实是在掌握“一个有状态的服务如何高效处理乱序请求”这一类问题。

9. 总结与后续扩展方向

“模拟电梯”类题目真正的价值,不在于把某一道题的样例跑通,而在于训练一种思维:把现实世界中的设备状态、事件请求、运行规则,翻译成代码中的状态字段、集合操作和条件分支。

本文给出了一个单电梯的 SCAN 调度实现,完整的代码结构分成 Direction、Elevator、ElevatorController、Main 四部分。你可以直接编译运行,也可以在理解思路后尝试以下扩展:

  • 把单电梯改成多电梯,并设计简单的“空闲电梯优先”策略;
  • 加入电梯内部请求模拟,区分内呼和外呼;
  • 在每次停靠时累加时间,比较不同调度策略的总运行时间;
  • 把控制台输出改成 GUI 可视化。

如果你能把上面的扩展独立完成,说明状态机建模和调度逻辑已经真正掌握了。下次再看到任何编号的“模拟电梯”题目,都可以从容地把这套核心模型迁移过去,只要针对输入输出格式做适配就够了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询