NCHU_OOP_单部电梯调度程序
单部电梯调度程序
0. Index
呜呜呜,为什么要让我写Blog,文笔又不好,讲话又不清晰😭。
罢了,写就写,哼~😑也不是没写过,起码也是CSDN上过榜的作者,怎么着也是干这一行五年啦。下面是正文了嗷
1. 题干
PTA平台NCHU_OOP_题目集_单部电梯调度程序
1.1 原话(太长啦,咱不看)
设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
请编写一个Java程序,设计一个电梯类,包含状态管理、请求队列管理以及调度算法,并使用一些测试用例,模拟不同的请求顺序,观察电梯的行为是否符合预期,比如是否优先处理同方向的请求,是否在移动过程中处理顺路的请求等。为了降低编程难度,不考虑同时有多个乘客请求同时发生的情况,即采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求),具体运行规则详见输入输出样例。
1.2 简化👇
输入:
第一行输入电梯的最小楼层
第二行输入电梯的最大楼层
接下来若干行输入请求事件
- 在电梯内部按下的请求事件的格式为:
<楼层>- 在电梯外部按下的请求事件的格式为:
<楼层,方向>
最后一行输入end(不区分大小写)
造一部电梯,写一个算法,处理上面的请求事件。方向优先.
1.3 测试样例(有点长,你忍一下)
这份测试样例我很喜欢,点睛之笔,就是它让我茅塞顿开的
2. 分析
我觉得叭,本次程序设计重点在于对电梯调度算法的设计😃,其次是通过三次迭代式训练,强化多类设计能力,履行SRP。
虽然是三次,但是核心都是一样的,重点分析第二次就行啦,对于第一次和第三次的内容略作补充便是
画外不要你觉得,你觉得的都是错的🤭
2.1 多类设计
- Passenger类:表示乘客请求
- RequestQueue类:管理内部和外部请求队列
- Elevator类:表示电梯状态和属性
- Controller类:核心调度逻辑
- Main类:主程序入口
2.2 大脑调度中...🥵
2.2.1 迷雾重重😶🌫️
起初,按照老师的想法,电梯每到一层,都要判断在当前楼层是否要开门处理请求,以及判断接下来的运行方向,然后电梯按照运行方向移动到下一层,再循环往复的判断。
然后又结合方向优先原则,电梯在每一层判断运行方向的时候,优先处理和电梯当前方向同向的请求,那我寻思着🤔,这不就是让电梯从底走到顶,再从顶走到底就行了??
然后,老师又说处理事件请求要按顺序处理、串行处理,处理了前一个才能处理后一个。不是,玩啥啊😡,这不是和上面有分歧吗??那这样说的话我一个一个处理不就行了??
或者结合一下,按顺序处理请求,从底走到顶,再从顶走到底,再从底走到顶,再从顶走到底,哈哈哈哈🤣
试一下,😭😭😭大败而归。
2.2.2 初窥门径🤨
大概是长时间以来都没有同学能够完成题目🤣,接着老师在交流群里发了一些说明,没多久,又发了一份对某测试样例的完整解释
其中有一个很重要的点,就是双队列,内部请求为一个队列,外部请求为一个队列。电梯每次获取方向都是从两个队列的队首中各取一个用来判断方向。

欸!我有个想法😃😃,既然如此,我何必一层一层跑呢?到达一个楼层后,我只要通过一些算法获取到下一个要前往的目标楼层,然后一路循环输出不就行啦,反正中间是没有请求的,也没必要做重复无效的判断。
而至于这个算法,在细致一点就是从内部请求队列队首和外部请求队列队首各取一个,然后通过某种方法选择其中一个(特殊情况可能两个一并处理)
2.2.2 豁然开朗🤩
我们的组长大大😘发了一份他写的测试样例在群里,并附上了理论答案。(也就是上面的测试样例2),这份测试样例及答案,能够完美解释我前一次的想法,关键在于细节实现上。
我思考思考思考很久很久,尝试着琢磨明白其中的逻辑,有一个地方并没有采取方向优先,而是选择了距离短的,难道!还有距离原则!但是完全靠距离进行移动并不符合,那么什么时候取距离呢?什么取方向优先呢?
尝试着画条件分支图,越画越有感觉,突然就
我会了!!🤩
哈哈哈,因为紧接着要上大英课,我悄喵着🤫在课上把完整的4*8 = 32钟情况画出来了。
2.3 调度逻辑
以下是核心电梯调度逻辑:👇
- 内部同向
- 外部同向
- 外部志愿同向
取距离
- 外部志愿不同向
取内部
- 外部志愿同向
- 外部不同向
- 外部志愿同向
取内部
- 外部志愿不同向
取内部
- 外部志愿同向
- 外部同向
- 内部不同向
- 外部同向
- 外部志愿同向
取外部
- 外部志愿不同向
取距离
- 外部志愿同向
- 外部不同向
- 外部志愿同向
取内部
- 外部志愿不同向
取距离
- 外部志愿同向
- 外部同向
要解释也很简单,第一是要服从方向优先,如果两个都符合方向优先或都不符合方向优先,那么再判断距离,如果还是一样,很显然是直接一并处理掉。否则谁符合方向优先就选择谁。
你说,不是32种吗?这不是才8种吗?😶
那你细想,其实这里的每一种,都有4种小情况,下面附上我在英语课上画的笔记

当然,上面是内部队列和外部队列都不为空的时候🥱
如果有一个为空,那还选啥,直接取另一个队列的队首
如果两个都空了,哎呀呀,那不就结束了嘛,多简单呐
下附第一次设计时的类图

3. 代码实现
这个就没啥解释了哈,3,2,1,上链接:👇
你点呀点呀
import java.util.*;
enum Direction { UP, DOWN, IDLE }
// 乘客请求类
class Passenger {
private Integer sourceFloor = null;
private Integer destinationFloor = null;
private Direction direction = Direction.IDLE;
// 内部请求的构造方法
public Passenger(Integer destinationFloor) {
this.destinationFloor = destinationFloor;
}
// 外部请求的构造方法
public Passenger(Integer sourceFloor, Integer destinationFloor) {
this.sourceFloor = sourceFloor;
this.destinationFloor = destinationFloor;
direction = sourceFloor < destinationFloor ? Direction.UP : Direction.DOWN;
}
public Integer getSourceFloor() { return sourceFloor; }
public Integer getDestinationFloor() { return destinationFloor; }
public Direction getDirection() { return direction; }
}
// 请求队列类
class RequestQueue {
private LinkedList<Passenger> internalRequests = new LinkedList<>();
private LinkedList<Passenger> externalRequests = new LinkedList<>();
public RequestQueue() {
}
public LinkedList<Passenger> getInternalRequests() { return internalRequests; }
public LinkedList<Passenger> getExternalRequests() { return externalRequests; }
public void addInternalRequest(Passenger passenger) { internalRequests.add(passenger); }
public void addExternalRequest(Passenger passenger) { externalRequests.add(passenger); }
public Integer removeIRQueueFirst() { return internalRequests.removeFirst().getDestinationFloor(); }
public Integer removeERQueueFirst() { return externalRequests.removeFirst().getSourceFloor(); }
public boolean ifRepeated(Passenger passenger) {
if(passenger.getDestinationFloor() == null) {
return !internalRequests.isEmpty() && Objects.equals(internalRequests.getLast().getDestinationFloor(), passenger.getDestinationFloor());
}
else {
return !externalRequests.isEmpty() && Objects.equals(externalRequests.getLast().getDestinationFloor(), passenger.getDestinationFloor()) && Objects.equals(externalRequests.getLast().getSourceFloor(), passenger.getSourceFloor());
}
}
}
// 电梯类
class Elevator {
private int currentFloor;
private Direction direction;
private int maxFloor;
private int minFloor;
public Elevator(int minFloor, int maxFloor) {
this.currentFloor = 0;
this.direction = Direction.UP;
this.maxFloor = maxFloor;
this.minFloor = minFloor;
}
public int getCurrentFloor() { return currentFloor; }
public void setCurrentFloor(int currentFloor) { this.currentFloor = currentFloor; }
public Direction getDirection() { return direction; }
public void setDirection(Direction direction) { this.direction = direction; }
public boolean isValidFloor(int floor) { return minFloor <= floor && floor <= maxFloor; }
}
// 控制事件类|!核心!
class Controller {
private Elevator elevator;
private RequestQueue queue = new RequestQueue();
public Controller() {
}
public void setElevator(Elevator elevator) { this.elevator = elevator; }
public RequestQueue getQueue() { return queue; }
public void processRequests() {
while(!queue.getInternalRequests().isEmpty() || !queue.getExternalRequests().isEmpty()) {
Integer nextFloor = getNextFloor(); // 获取电梯要前往的楼层
// 目标楼层在现在电梯下面还是上面
if(nextFloor < elevator.getCurrentFloor()) {
elevator.setDirection(Direction.DOWN);
// 向下移动
for(int i = elevator.getCurrentFloor() - 1; i >= nextFloor; i--) {
System.out.println("Current Floor: " + i + " Direction: DOWN");
}
}
else {
elevator.setDirection(Direction.UP);
// 向上移动
for(int i = elevator.getCurrentFloor() + 1; i <= nextFloor; i++) {
System.out.println("Current Floor: " + i + " Direction: UP");
}
}
// 电梯到达目标楼层
elevator.setCurrentFloor(nextFloor);
openDoors();
}
}
// 核心方法!获取电梯要前往的楼层
public Integer getNextFloor() {
// 内部队列和外部队列都不为空
if(!queue.getInternalRequests().isEmpty() && !queue.getExternalRequests().isEmpty()) {
int IRFloor = queue.getInternalRequests().getFirst().getDestinationFloor();
int ERFloor = queue.getExternalRequests().getFirst().getSourceFloor();
Direction ERDirection = queue.getExternalRequests().getFirst().getDirection();
if(VectorDirection(IRFloor)) {
if(VectorDirection(ERFloor)) {
if(ERDirection == elevator.getDirection()) { // 内同外同外志同 ,取短
return getClosest(IRFloor, ERFloor);
}
else { // 内同外同外志不同 ,取内
return queue.removeIRQueueFirst();
}
}
else {
if(ERDirection == elevator.getDirection()) { // 内同外不同外志同 ,取内
return queue.removeIRQueueFirst();
}
else {
return queue.removeIRQueueFirst(); // 内同外不同外志不同 ,取内
}
}
}
else {
if(VectorDirection(ERFloor)) {
if(ERDirection == elevator.getDirection()) { // 内不同外同外志同 ,取外
queue.addInternalRequest(new Passenger(queue.getExternalRequests().getFirst().getDestinationFloor()));
return queue.removeERQueueFirst();
}
else { // 内不同外同外志不同 ,取短
return getClosest(IRFloor, ERFloor);
}
}
else {
if(ERDirection == elevator.getDirection()) { // 内不同外不同外志同 ,取内
return queue.removeIRQueueFirst();
}
else { // 内不同外不同外志不同,取短
return getClosest(IRFloor, ERFloor);
}
}
}
}
// 内部队列为空,外部队列不为空
else if(!queue.getInternalRequests().isEmpty()) {
return queue.removeIRQueueFirst();
}
// 外部队列为空,内部队列不为空
else if(!queue.getExternalRequests().isEmpty()){
queue.addInternalRequest(new Passenger(queue.getExternalRequests().getFirst().getDestinationFloor()));
return queue.removeERQueueFirst();
}
// 两个队列都为空
else {
return null;
}
}
public Integer getClosest(Integer a, Integer b) {
if(Math.abs(a - elevator.getCurrentFloor()) < Math.abs(b - elevator.getCurrentFloor())) {
queue.removeIRQueueFirst();
return a;
}
else if(Math.abs(a - elevator.getCurrentFloor()) > Math.abs(b - elevator.getCurrentFloor())) {
queue.addInternalRequest(new Passenger(queue.getExternalRequests().getFirst().getDestinationFloor()));
queue.removeERQueueFirst();
return b;
}
else {
queue.removeIRQueueFirst();
queue.addInternalRequest(new Passenger(queue.getExternalRequests().getFirst().getDestinationFloor()));
queue.removeERQueueFirst();
return a;
}
}
public void openDoors() {
System.out.println("Open Door # Floor " + elevator.getCurrentFloor());
System.out.println("Close Door");
}
// 获取从当前楼层到目标楼层的方向向量
public boolean VectorDirection(int destinationFloor) {
return (destinationFloor > elevator.getCurrentFloor() && elevator.getDirection() == Direction.UP) || (destinationFloor < elevator.getCurrentFloor() && elevator.getDirection() == Direction.DOWN);
}
public void addInternalRequest(Passenger passenger) { queue.addInternalRequest(passenger); }
public void addExternalRequest(Passenger passenger) { queue.addExternalRequest(passenger); }
}
public class A {
public static void main(String[] args) {
// 输入
Scanner sc = new Scanner(System.in);
Elevator elevator = new Elevator(sc.nextInt(), sc.nextInt());
Controller controller = new Controller();
controller.setElevator(elevator);
// 在外面定义,避免反复开辟空间
String input;
int floor;
while(!(input = sc.next()).equalsIgnoreCase("end")) {
int commaIndex = input.indexOf(",");
// 如果没有逗号,说明是内部请求
if(commaIndex == -1) {
floor = parseInt(input.substring(1, input.length() - 1));
// 输入楼层是合法的且没有和上一个请求重复
if(elevator.isValidFloor(floor) && !controller.getQueue().ifRepeated(new Passenger(floor))) {
controller.addInternalRequest(new Passenger(floor));
}
}
else {
int sourceFloor = parseInt(input.substring(1, commaIndex));
int destinationFloor = parseInt(input.substring(commaIndex + 1, input.length() - 1));
// 输入楼层是合法的且没有和上一个请求重复
if(elevator.isValidFloor(sourceFloor) && elevator.isValidFloor(destinationFloor) && !controller.getQueue().ifRepeated(new Passenger(sourceFloor, destinationFloor))) {
controller.addExternalRequest(new Passenger(sourceFloor, destinationFloor));
}
}
}
sc.close();
controller.processRequests();
}
// 字符串转整数,重写,节省时间
public static int parseInt(String s) {
int num = 0;
for (int i = 0; i < s.length(); i++) {
num = num * 10 + (s.charAt(i) - '0');
}
return num;
}
}
要阅读代码的话建议复制到IDE中阅读呀,这里多不方便呀,代码我已注入必要的注释惹
下附第三次UML类图设计(老师,呜呜呜,真的没有好看的UML工具吗,这个有一点点不好看呐😒)。相比第一次的类设计,第三次合理很多,基本达到了多类设计SRP规范。

3.1 过程琐事😡
3.1.1 队列
由于第一次迭代不允许使用现有的List及其泛型类,也不允许使用集合框架,所以我用数组去实现(链表不会,哈哈哈)
其次,虽然叫做队列,但是和队列并无关系,它不具有队列的任何性质(比如先进先出)
每处理一个事件,队首的请求都要删除,但是我是数组,删除元素的时间复杂度是O(n),我要优化
于是我引入队首指针(此指针非彼指针),它指向要处理的事件,实质上就是一个index,每处理掉一个事件,就把队首指针往后挪一位(index++),这样就替代了删除元素的效果,元素实际上并没有被删除,但是队首指针向后移动了就等价于删掉啦🤩
第二次可以用LinkedList了,就不再采用上面的方法了(但是因为访问权限问题,还是好麻烦😭)
3.1.2 性能
最开始写这题的时候,一直运行超时,于是我就极致的优化性能
- Scanner不用,我用
BufferedReader去读取,BufferedReader的速度是远快于Scanner的 - 正则表达式我不用,用
substring和indexOf手动解析,正则表达式也是非常的慢,正则表达式很好,匹配能力很强,但是这里还是用不上,先用indexOf做判断再substring就够用了 - Integer.parseInt我不用,
手写了一个parseInt方法,道理一样,优化性能,而且写这个也并不复杂 - 变量声明都放循环的外部,可以避免每次循环反复开辟内存空间带来的时间开销
很可惜,优化不过来,这些小优化抵不过面向对象本身的开销。思考良久后,我断定我的运行超时是因为逻辑错误导致了死循环😭
至于是什么逻辑错误我已经忘了,当时第一份代码不是按照这个逻辑写的🤫
3.1.3 非零返回
再然后,很折腾的就是“非零返回”,因为有C/C++的经验,C++里面非零返回是真的非零返回,但是Java里没这么一个概念😶🌫️,我一下子确实不能理解它报非零返回是什么意思
后面就是无头绪的摸索惹,发现Scanner没关(因为BufferedReader有些许麻烦,所以还是换回了Scanner),但是即使sc.close(),还是非零返回
打断点调试呐,没有办法啦,一步一步走,意外的中断了!又复现一边,发现是因为ifRepeated()方法里面没判断队列是否为空,导致了无法访问的问题
这种应该算是段错误吧,为什么是非零返回呢,真是不解
3.1.4 第三次
第三次迭代的题目,没有采用UP、DOWN的方法,而是改为目的楼层
- 在电梯内部按下的请求事件的格式为:
<楼层>- 在电梯外部按下的请求事件的格式为:
<楼层,目的楼层>
这个改动几乎没有影响(表面我的程序的可扩展性很好🤩🤩),对于外部请求事件,是要通过简单的大小判断,就可以知道它的请求志愿方向是向上还是向下,有了这个方向之后就可以像第二次一样运行了,顶多就是处理了这个外部请求之后,要把它的目标楼层添加到内部请求队列的队尾
3.1.5 类方法
因为思路和老师不一样,所以老师给的UML类图里的比较多方法我都用不上,但是又不敢不写,哎😐
3.2 其他重要算法
一个是VectorDirection(),这个用来判断电梯当前楼层到候选目标楼层之间的方向与电梯方向是否同向
另一个是getClosest(),用来判断并返回两个候选目标楼层中最近的、距离最短的一个
4. 代码质量分析
4.1 总评

| 重要参数 | 值 |
|---|---|
| 注释比例 | 11.9% |
| 每类方法数 | 9 |
| 平均每方法语句数 | 4.93 |
| 最大复杂度 | 6 |
| 最大深度 | 4 |
| 平均深度 | 1.19 |
| 平均复杂度 | 2.6 |
4.2 待改进
- 注释比例过低(11.9%)
注释不足可能导致代码可维护性和可读性差,好叭,我尽量去改正,看来我还不够啰嗦(主要是不知道怎么写注释才好看呐!要好看😒) - 分支语句百分比(20.3%)
条件分支过多可能增加代码复杂度和潜在缺陷风险,但是,这题它不就是这样吗???逻辑判断题不用if用什么??😑 - 最复杂方法(Controller.processRequests(),复杂度6)
Cyclomatic复杂度6超出行业推荐阈值(通常≤5),表明方法内部逻辑过于集中,不听不听不听,这要取决实际情况,说不定是它分析错了🙄 - 每类方法数(9.00)
单个类承载过多方法可能违反“单一职责原则”,成员数据都设置成了Private,那我要获取数据不得不添加更多方法呀,这题目问题,怪题目,老师的每类方法数比我还多😝 - 方法调用次数(processRequests()被调用13次)
高频调用可能暗示方法存在过度依赖或职责不清,但是!我只调用了1次!绝对是分析错了🤣
4.3 优点🤩🤩
- 平均每方法语句数(4.93), 方法长度适中,符合“单一职责原则”,便于阅读和维护。
- 最大块深度(4)与平均块深度(1.19),我的嵌套层级控制可十分合理,完全没有过深的条件嵌套导致的逻辑混乱。
- 类和接口数量(3),在电梯这种题目中,类的数量符合模块化设计要求,如果不符合就是老师给的类设计要求有问题。
4.4 基于题目分析
本次代码其实还是有漏洞,因为有些细节是为了通过题目拿到100分而设计的,在其他同学给我发的测试样例中,其实我这个程序是不通过的,别急哈,我主体核心逻辑绝对没问题,是从当前楼层到目标

4.5 IDEA代码检查
使用IDEA进行代码检查,还存在以下问题。问题的主要原因是为了方便阅读😑,我真的是为了增加可读性才这样写

5. 结束啦
撒花撒花😝
总的来说嘞,对于这次的题目,一旦想清楚了,其实非常简单,有的同学每一次都要换一个思路(也是个人物),有的同学一招鲜吃遍天(对,就是我)。
这个讲确实没有太多可讲的,核心逻辑我也已经放出来了,把中文译成Java代码即可。然后是移动时候循环输出的地方,要注意一下条件,不然会多输出或者少输出,这个没啥逻辑,缺啥补啥,为这个我特地把电梯默认楼层设为minfloor-1,哈哈哈😝

浙公网安备 33010602011771号