电梯问题分析总结blog
一、前言
题目集5-7题目集有难有易,既考察了我们对一些基本编程知识的考察,也考验和锻炼了我们的逻辑思维.这三次作业包含知识点广泛,包括正则表达式内容的考察(身份证号校验,验证码校验,QQ号校验等),同时也对类的设计进行了考察(点线类问题),要求我们对java的封装性,单一变量原则有深刻的了解.特别是电梯问题(基于电梯LOOK算法)的迭代,既要求对电梯运动的内部逻辑熟练掌握,还要实现类职能的不断优化改进.
二、设计与分析
第1次电梯分析
设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
请编写一个Java程序,设计一个电梯类,包含状态管理、请求队列管理以及调度算法,并使用一些测试用例,模拟不同的请求顺序,观察电梯的行为是否符合预期,比如是否优先处理同方向的请求,是否在移动过程中处理顺路的请求等。为了降低编程难度,不考虑同时有多个乘客请求同时发生的情况,即采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求)
输入格式:
第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。
电梯内乘客请求格式:<楼层数>
电梯外乘客请求格式:<乘客所在楼层数,乘梯方向>,其中,乘梯方向用UP代表上行,用DOWN代表下行(UP、DOWN必须大写)。
当输入“end”时代表输入结束(end不区分大小写)。
输出格式:
模拟电梯的运行过程,输出方式如下:
运行到某一楼层(不需要停留开门),输出一行文本:
Current Floor: 楼层数 Direction: 方向
运行到某一楼层(需要停留开门)输出两行文本:
Open Door # Floor 楼层数
Close Door
输入样例
1 20 <3,UP> <5> <6,DOWN> <7> <3> end
输出样例
Current Floor: 1 Direction: UP
Current Floor: 2 Direction: UP
Current Floor: 3 Direction: UP
Open Door # Floor 3
Close Door
Current Floor: 4 Direction: UP
Current Floor: 5 Direction: UP
Open Door # Floor 5
Close Door
Current Floor: 6 Direction: UP
Current Floor: 7 Direction: UP
Open Door # Floor 7
Close Door
Current Floor: 6 Direction: DOWN
Open Door # Floor 6
Close Door
Current Floor: 5 Direction: DOWN
Current Floor: 4 Direction: DOWN
Current Floor: 3 Direction: DOWN
Open Door # Floor 3
Close Door
在写这个代码时,我主要设计了Elevator类,ExternalRequests类和Main类,Elevator类中又包括了很多方法,包括determineDirection方法,move方法,shouldStop类等,将电梯问题的运行逻辑划拆分成不同方法,这样能提高代码的可读性和可维护性。
主要方法分析:
private void determineDirection() {
if (!internalRequests.isEmpty() && !externalRequests.isEmpty()) {
int infloor = internalRequests.get(0);
int exfloor = externalRequests.get(0).getFloor();
updateDirection(infloor, exfloor);
} else if (!internalRequests.isEmpty()) {
int infloor = internalRequests.get(0);
updateDirection(infloor, 0);
} else if (!externalRequests.isEmpty()) {
int exfloor = externalRequests.get(0).getFloor();
updateDirection(0, exfloor);
}
}
private void updateDirection(int infloor, int exfloor) {
boolean shouldCheckInFloor = infloor != 0;
boolean shouldCheckExFloor = exfloor != 0;
if ("UP".equals(currentDirection)) {
if ((shouldCheckInFloor && infloor < currentFloor) || (shouldCheckExFloor && exfloor < currentFloor)) {
currentDirection = "DOWN";
}
} else if ("DOWN".equals(currentDirection)) {
if ((shouldCheckInFloor && infloor > currentFloor) || (shouldCheckExFloor && exfloor > currentFloor)) {
currentDirection = "UP";
}
}
}
代码的核心逻辑是依据内部请求列表 internalRequests 和外部请求列表externalRequests 的情况,来决定电梯当前的运行方向 currentDirection。
-
当内部请求列表和外部请求列表都不为空时:
获取内部请求列表的第一个楼层 infloor 和外部请求列表的第一个楼层 exfloor。
若当前电梯方向为 "UP",并且 infloor 和 exfloor 都小于当前楼层 currentFloor,则将电梯方向改为 "DOWN"。 -
若当前电梯方向为 "DOWN",并且 infloor 和 exfloor 都大于当前楼层 currentFloor,则将电梯方向改为 "UP"。
-
当只有内部请求列表不为空时:
获取内部请求列表的第一个楼层 infloor。 -
若当前电梯方向为 "UP" 且 infloor 小于当前楼层 currentFloor,则将电梯方向改为 "DOWN"。
-
若当前电梯方向为 "DOWN" 且 infloor 大于当前楼层 currentFloor,则将电梯方向改为 "UP"。
-
当只有外部请求列表不为空时:
获取外部请求列表的第一个楼层 exfloor。 -
若当前电梯方向为 "UP" 且 exfloor 小于当前楼层 currentFloor,则将电梯方向改为 "DOWN"。
-
若当前电梯方向为 "DOWN" 且 exfloor 大于当前楼层 currentFloor,则将电梯方向改为 "UP"。
代码中将重复的方向判断逻辑提取到 updateDirection 方法中,避免了代码冗余,提高了代码的可维护性,但是也使用了大量的if-else分支,导致代码大量重复的判断逻辑
点击查看代码
private boolean exStop() {
// 检查外部请求列表是否为空
if (!externalRequests.isEmpty()) {
ExternalRequests firstExternal = externalRequests.get(0);
int externalFloor = firstExternal.getFloor();
// 检查第一个外部请求的楼层是否等于当前楼层
if (externalFloor == currentFloor) {
// 检查内部请求列表是否为空
if (!internalRequests.isEmpty()) {
String externalDirection = firstExternal.getDirection();
int firstInternalFloor = internalRequests.get(0);
// 判断是否满足停止条件
if (shouldStop(externalDirection, firstInternalFloor, externalFloor)) {
currentDirection = externalDirection;
externalRequests.remove(0);
return true;
}
} else {
// 内部请求列表为空,直接移除第一个外部请求并返回 true
externalRequests.remove(0);
return true;
}
}
}
return false;
}
// 封装判断是否停止的逻辑
private boolean shouldStop(String externalDirection, int firstInternalFloor, int externalFloor) {
return externalDirection.equals(currentDirection) ||
("UP".equals(externalDirection) && firstInternalFloor > externalFloor) ||
("DOWN".equals(externalDirection) && firstInternalFloor < externalFloor);
}
shouldStop 方法逻辑:该方法接收外部请求的方向、第一个内部请求的楼层和外部请求的楼层作为参数,判断是否满足停止条件。停止条件包括外部请求方向与当前电梯运行方向相同,或者外部请求方向为 "UP" 且第一个内部请求的楼层大于外部请求的楼层,或者外部请求方向为 "DOWN" 且第一个内部请求的楼层小于外部请求的楼层。
点击查看代码
while (!(data = input.next()).equalsIgnoreCase("End")) {
if (data.contains(",")) {
if (!data.matches("<\\d+,\\s*(UP|DOWN)>")) {
System.out.println("Wrong Format");
continue;
}
String[] parts = data.replaceAll("[<>]", "").split(",");
floor = Integer.parseInt(parts[0].trim());
direction = parts[1].trim().toUpperCase();
elevator.addExternalRequest(floor, direction);
} else {
if (!data.matches("<\\d+>")) {
System.out.println("Wrong Format");
continue;
}
floor = Integer.parseInt(data.replaceAll("[<>]", ""));
elevator.addInternalRequest(floor);
}
}
参数 | 值 | 说明 |
---|---|---|
代码行数 (Lines) | 179 | 文件总代码行数,包含空行和注释 |
可执行语句 (Statements) | 114 | 实际执行逻辑的语句数(不包含空行、注释) |
分支语句占比 | 13.2% | 分支语句占总语句的比例,逻辑复杂度参考指标 |
方法调用语句 | 66 | 代码中调用方法的语句数,反映模块间交互频率 |
注释行占比 | 3.4% | 含注释的代码行比例,注释严重不足 |
类数量 | 2 | 文件中定义的类 / 接口数量 |
优点分析
- 方法拆分合理,单一职责,平均每个方法仅 4.3 行代码,且类中方法数较多(10 个 / 类)
- 分支语句占比低,逻辑清晰
缺点分析
- 严重注释不足,注释行占比仅 3.4%,几乎无有效注释
- 局部复杂度超标,Elevator.updateDirection()复杂度高达 11,包含多层if-else嵌套或复杂条件组合
第2题次电梯分析
对之前电梯调度程序进行迭代性设计,目的为解决电梯类职责过多的问题,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客请求类、队列类以及控制类。电梯运行规则与前阶段单类设计相同,但要处理如下情况:
- 乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行
- 乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>
在这次电梯问题中,新增加了Controller类,并将Elevator和ExternalRequests作为其属性,将原来放在Elevator中的方法分离出来,使得代码更加符合单一变量原则
主要代码分析
点击查看代码
class Controller {
private Elevator elevator;
private RequestQueue requestQueue;
public Controller(Elevator elevator, RequestQueue requestQueue) {
this.elevator = elevator;
this.requestQueue = requestQueue;
}
public Elevator getElevator() {
return elevator;
}
public void setElevator(Elevator elevator) {
this.elevator = elevator;
}
public RequestQueue getQueue() {
return requestQueue;
}
public void setQueue(RequestQueue requestQueue) {
this.requestQueue = requestQueue;
}
public void processRequests() {
System.out.println("Current Floor: " + elevator.getCurrentFloor() + " Direction: " + elevator.getCurrentDirection());
while (!requestQueue.getInternalRequests().isEmpty() || !requestQueue.getExternalRequests().isEmpty()) {
move();
}
}
private void determineDirection() {
if (!requestQueue.getInternalRequests().isEmpty() && !requestQueue.getExternalRequests().isEmpty()) {
int infloor = requestQueue.getInternalRequests().get(0);
int exfloor = requestQueue.getExternalRequests().get(0).getFloor();
if ("UP".equals(elevator.getCurrentDirection())) {
if (infloor < elevator.getCurrentFloor() && exfloor < elevator.getCurrentFloor()) {
elevator.setCurrentDirection("DOWN");
}
}
if ("DOWN".equals(elevator.getCurrentDirection())) {
if (infloor > elevator.getCurrentFloor() && exfloor > elevator.getCurrentFloor()) {
elevator.setCurrentDirection("UP");
}
}
} else if (!requestQueue.getInternalRequests().isEmpty()) {
int infloor = requestQueue.getInternalRequests().get(0);
if ("UP".equals(elevator.getCurrentDirection()) && infloor < elevator.getCurrentFloor()) {
elevator.setCurrentDirection("DOWN");
} else if ("DOWN".equals(elevator.getCurrentDirection()) && infloor > elevator.getCurrentFloor()) {
elevator.setCurrentDirection("UP");
}
} else if (!requestQueue.getExternalRequests().isEmpty()) {
int exfloor = requestQueue.getExternalRequests().get(0).getFloor();
if ("UP".equals(elevator.getCurrentDirection()) && exfloor < elevator.getCurrentFloor()) {
elevator.setCurrentDirection("DOWN");
} else if ("DOWN".equals(elevator.getCurrentDirection()) && exfloor > elevator.getCurrentFloor()) {
elevator.setCurrentDirection("UP");
}
}
}
private void move() {
if ("UP".equals(elevator.getCurrentDirection())) {
elevator.setCurrentFloor(elevator.getCurrentFloor() + 1);
} else if ("DOWN".equals(elevator.getCurrentDirection())) {
elevator.setCurrentFloor(elevator.getCurrentFloor() - 1);
}
System.out.println("Current Floor: " + elevator.getCurrentFloor() + " Direction: " + elevator.getCurrentDirection());
if (shouldStop()) {
System.out.println("Open Door # Floor " + elevator.getCurrentFloor());
System.out.println("Close Door");
}
determineDirection();
}
private boolean shouldStop() {
boolean inis = inStop();
boolean exis = exStop();
return inis || exis;
}
private boolean inStop() {
if (!requestQueue.getInternalRequests().isEmpty() && requestQueue.getInternalRequests().get(0) == elevator.getCurrentFloor()) {
requestQueue.getInternalRequests().remove(0);
return true;
}
return false;
}
private boolean exStop() {
if (!requestQueue.getExternalRequests().isEmpty()) {
ExternalRequests firstExternal = requestQueue.getExternalRequests().get(0);
if (firstExternal.getFloor() == elevator.getCurrentFloor()) {
if (!requestQueue.getInternalRequests().isEmpty()) {
if (firstExternal.getDirection().equals(elevator.getCurrentDirection()) ||
("UP".equals(firstExternal.getDirection()) && requestQueue.getInternalRequests().get(0) > firstExternal.getFloor()) ||
("DOWN".equals(firstExternal.getDirection()) && requestQueue.getInternalRequests().get(0) < firstExternal.getFloor())) {
elevator.setCurrentDirection(firstExternal.getDirection());
requestQueue.getExternalRequests().remove(0);
return true;
}
} else {
requestQueue.getExternalRequests().remove(0);
return true;
}
}
}
return false;
}
}
1.初始化与入口
接收电梯(Elevator)和请求队列(RequestQueue)对象,通过 processRequests() 启动控制流程。
打印初始楼层和方向,循环处理请求直到内外请求队列为空。
2.电梯移动逻辑
move() 方法根据当前方向(UP/DOWN)改变楼层,打印实时状态。
调用 shouldStop() 判断是否停梯:若当前楼层有内部或外部请求,打印开关门信息。
移动后调用 determineDirection() 重新评估方向,避免空跑(例如:向上时若所有请求在下方,转为向下)。
3.停梯判断
inStop():检查内部请求队列首个请求是否为当前楼层,若是则移除请求并停梯。
exStop():检查外部请求队列首个请求是否为当前楼层,同时结合电梯方向和内部请求位置,决定是否停梯并移除请求(例如:外部请求方向与电梯一致,或内部请求在目标楼层上方 / 下方时停梯)。
4.方向调整
determineDirection():根据内外请求的首个楼层与当前楼层的关系调整方向。
若当前向上,但所有内外请求楼层都低于当前楼层,转为向下;反之同理。
单队列有请求时,根据请求楼层与当前楼层的高低调整方向(例如:向下时若请求在上方,转为向上)。
点击查看代码
class RequestQueue {
private ArrayList<Integer> internalRequests;
private ArrayList<ExternalRequests> externalRequests;
private Integer lastInternalRequest = null;
private ExternalRequests lastExternalRequest = null;
public RequestQueue() {
this.internalRequests = new ArrayList<>();
this.externalRequests = new ArrayList<>();
}
public ArrayList<Integer> getInternalRequests() {
return internalRequests;
}
public void setInternalRequests(ArrayList<Integer> internalRequests) {
this.internalRequests = internalRequests;
}
public ArrayList<ExternalRequests> getExternalRequests() {
return externalRequests;
}
public void setExternalRequests(ArrayList<ExternalRequests> externalRequests) {
this.externalRequests = externalRequests;
}
public void addInternalRequest(int floor, int minFloor, int maxFloor) {
if (floor < minFloor || floor > maxFloor) {
return;
}
if (lastInternalRequest != null && lastInternalRequest == floor) {
return;
}
internalRequests.add(floor);
lastInternalRequest = floor;
}
public void addExternalRequest(int floor, String direction, int minFloor, int maxFloor) {
if (floor < minFloor || floor > maxFloor) {
return;
}
ExternalRequests newRequest = new ExternalRequests(floor, direction);
if (lastExternalRequest != null && lastExternalRequest.equals(newRequest)) {
return;
}
externalRequests.add(newRequest);
lastExternalRequest = newRequest;
}
}
首先检查请求的楼层是否在有效范围内(minFloor 到 maxFloor),如果不在范围内则直接返回。
然后我创建一个新的 ExternalRequests 对象,包含请求的楼层和方向。
检查上一个外部请求是否与当前请求相同,如果相同则直接返回,避免重复添加。
如果以上条件都不满足,则将新的请求添加到 externalRequests 列表中,并更新 lastExternalRequest。
优点分析
- 使用两个列表分别管理内部请求和外部请求,数据结构清晰,便于后续的请求处理和管理。
- 通过记录上一个请求,避免了重复请求的添加,减少了不必要的请求处理,提高了效率。
缺点分析
- 代码中缺乏必要的注释,尤其是对于一些关键的逻辑和判断条件。
- 使用 ArrayList 存储请求,在频繁插入和删除操作时效率较低。可以考虑使用其他数据结构,如 LinkedList 或 TreeSet,根据具体的需求进行优化。
这次的主要问题是没有写注释, RequestQueue.addExternalRequest()该方法包含较多条件判断或循环,导致逻辑不够简洁,可读性下降。
第3次电梯分析
对之前电梯调度程序再次进行迭代性设计,加入乘客类(Passenger),取消乘客请求类,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客类、队列类以及控制类,电梯运行规则与前阶段相同,但有如下变动情况:
- 乘客请求输入变动情况:外部请求由之前的<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层>
- 对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)
这次电梯类取消乘客请求类并引入乘客类,对类功能及交互细节要求更精细。
缺点及改进方法
- 主方法过于复杂:
问题:Main.main() 方法行数达 305,复杂度为 10,深度为 5,违反 “单一职责原则”,导致代码难以维护、调试和扩展。
改进:将 main 方法中耦合的功能拆分到独立的类或方法中。例如,若 main 中包含输入处理、业务逻辑计算、输出展示等功能,可分别创建如 InputHandler、BusinessLogicProcessor、OutputDisplayer 等类,各自负责单一职责,降低复杂度。 - 注释不足:
问题:注释行占比仅 4.4%,代码可读性差,他人理解代码逻辑成本高。
改进:对关键类、方法、复杂逻辑段添加注释。例如,在每个方法开始处说明其功能、输入参数含义、返回值意义;对复杂算法或条件判断添加逻辑解释。 - 逻辑复杂度较高:
问题:分支语句占比 18.3%,结合主方法的高复杂度,可能存在过多嵌套或复杂条件判断,使代码逻辑难以追踪。
改进:审查代码中的条件判断和循环逻辑,通过重构简化。例如,使用策略模式替代部分复杂的条件判断;将多层嵌套的逻辑拆分为独立方法,提高可读性。 - 代码可维护性差:
问题:主方法庞大且复杂,一旦需求变更,修改代码容易引发连锁反应,增加出错风险。
改进:遵循面向对象设计原则,如依赖倒置原则、接口隔离原则等,提高代码的灵活性和可维护性。
三、踩坑心得
-
没有考虑边界问题,可能测试点过了,但是一旦范围不断扩大,边界问题逐渐暴露,容易运行超时和越界。
-
开始时写代码没有规划,没有考虑清楚电梯的运行规则就开始代码。
-
数据处理方面,请求格式校验存在漏洞,对非法字符组合处理不足,测试中发现大量请求未被正确过滤。
-
类设计结构上,ElevatorController 类职责较多,与其他类交互复杂,Passenger 类功能分散,方法存在代码冗余。
改进建议
-
下次在编写代码之前,要先对电梯系统的需求和运行规则进行详细分析,设计出合理的架构和流程。可以绘制流程图或者状态图,清晰地描述电梯的运行逻辑。
-
在处理请求时,添加异常处理机制,捕获并处理非法输入,避免程序崩溃。
-
找出类中重复的代码,将其提取成独立的方法或者类,提高代码的复用性,将不同方向的移动逻辑封装成单独的策略类
四、总结
这次电梯难题不仅是对java学习的考验,更是一场对心灵的考验,自己开始也因为题目难而产生退缩心理,内心烦躁,但是后来积极调整心态,不断发现问题。同时我通过这次实验也发现自己对正则表达式的使用,类与类之间的关系以及语法上都有所欠缺,之后要更加注重这方面的学习,不惧困难。在这个学习中巩固了关于ArrayList、LinkedList、和Queue的相关用法,能够较为熟练的使用debug的功能,学会了使用PowerDesigner与SourceMonitor两大软件,对自己编写的代码有了更加直观的认识。