一、前言
在近期的编程练习中,我练习了一组电梯调度题目。这些题目以渐进式难度设计,尤其是每阶段的最后一题,对逻辑思维和面向对象设计能力提出了较高挑战。作为对面向对象编程(OOP)尚不熟练的开我,在解题过程中深刻体会到从「功能实现」到「设计模式」的思维转变。题量适中但逻辑密度大,尤其是电梯调度规则的细节处理,需要反复推敲状态转移和队列操作的合理性。
习题5的电梯🛗题目是单类设计,所有功能都基本集中在一个电梯类中,难点可能在于如何在大类中设计调度算法,比如处理同方向优先的请求,方向如何转换等。
习题6的电梯🛗题目引入了单一职责原则,也需要迭代,需要拆分多个Elevator,Controller,RequestQueue等多个类,这个难点可能在于职责的划分(但给了大致具体的类图),另外,需要单独过滤请求重复的过滤。
习题7的电梯🛗问题引入乘客类,取消请求类,外部请求需要转换成乘客对象,处理时将目的楼层加入内部队列,这里我觉得难点在于乘客类的转换,这个题目更多的要处理源和目的楼层是否有效,是否合理,控制类调度相差不大。
二、设计与分析
习题集5电梯
设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
枚举类Direction:定义了电梯的三种运行状态UP\DOWN\IDLE。
内部请求类InnerQueue:该类用于表示电梯内部的请求,包含一个整数类型的 floor 属性,表示请求到达的楼层。
外部请求类OutQueue:该类用于表示电梯外部的请求,包含一个整数类型的 floor 属性和一个 Direction 类型的 direction 属性,分别表示请求的楼层和请求的方向。
电梯类Elevator:该类是整个电梯模拟系统的核心类,包含了电梯的基本属性和调度方法。
主类Main:从标准输入读取用户输入的电梯最低楼层、最高楼层和一系列请求,直到用户输入 end 为止。
- 代码行数(Lines):285
- 语句数量(Statements):126
- 代码结构相关(Percent Branch Statements):分支语句所占百分比,为 22.2% ,反映代码中使用如if - else 、switch等分支结构的比例情况。
- 方法调用语句数量(Method Call Statements):52
- 含注释的代码行所占百分比(Percent Lines with Comments):27.4%
- 类和接口的数量(Classes and Interfaces):2
- 平均每个类的方法数(Methods per Class):7.50
- 平均每个方法的语句数(Average Statements per Method): 7.93
- 最复杂方法所在的行号(Line Number of Most Complex Method): 222
- 最复杂方法的名称(Name of Most Complex Method):Main.main()
- 最大复杂度(Maximum Complexity): 9
//衡量方法逻辑复杂程度,数值越高逻辑越复杂 - 平均复杂度(Average Complexity):9.00
显然,较高的复杂度与代码块深度,让代码逻辑错综复杂,极大地提升了阅读难度。理解代码的功能与流程,往往需耗费更多时间精力。就当前代码而言,最为复杂的是 Main.main() 方法 ,其内部充斥着大量条件判断、循环嵌套以及方法调用,致使逻辑繁杂难辨。
汲取此次经验,往后开发时,我会审慎使用此类复杂构建。一方面,极力规避过度复杂的逻辑堆砌;另一方面,针对重复调用语句进行优化,减少不必要的嵌套结构。通过这些举措,力求提升代码可读性与可维护性 。
习题集6电梯
类图:
电梯运行规则与前阶段单类设计相同,但要处理如下情况:
乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行
乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>
类图见下图:
- 代码行数(Lines):300
- 语句数量(Statements):184
- 代码结构相关(Percent Branch Statements):分支语句所占百分比,为 18.5% ,反映代码中使用如if - else 、switch等分支结构的比例情况。
- 方法调用语句数量(Method Call Statements):95
- 含注释的代码行所占百分比(Percent Lines with Comments):0.0%
- 类和接口的数量(Classes and Interfaces):7
- 平均每个类的方法数(Methods per Class):4.43
- 平均每个方法的语句数(Average Statements per Method): 4.16
- 最复杂方法所在的行号(Line Number of Most Complex Method): 158
- 最复杂方法的名称(Name of Most Complex Method):Controller.handleStop()
- 最大复杂度(Maximum Complexity): 10
//衡量方法逻辑复杂程度,数值越高逻辑越复杂 - 平均复杂度(Average Complexity):2.65
分析:平均下来每个类承担的功能相对较为丰富,不过也需要关注是否存在功能职责不清晰,导致方法在类中分布混乱的情况。main 方法作为程序入口,通常会承担较多初始化、流程调度等工作,但最大复杂度达到 10 ,说明该方法内逻辑交织复杂,可能存在大量条件判断、循环嵌套等情况,对代码的可读性、可维护性构成挑战。这表明代码中最复杂的部分逻辑相当繁琐,修改或扩展这部分代码时,出错风险较高。但是整体代码平均复杂程度还算比较低,但由于存在最大复杂度较高的方法,仍需对代码结构和逻辑进行优化,平衡各部分复杂度。
开始时一直有运行超时,猜想可能是代码太复杂的关系,改了几次,把总体平均代码复杂程度拉低了,但是最大复杂程度还是只增不减,我猜想可能是main主类中太多if语句嵌套的结果
习题集7电梯
代码结构分析
枚举类 Direction:UP、DOWN、 IDLE
类 Passenger:表示乘客,包含起始楼层和目标楼层两个属性,提供了获取这些属性的方法。
类 RequestQueue:用于管理内部请求和外部请求,提供了添加请求、检查队列是否为空、将外部请求的目的地添加到内部请求队列等方法。
类 Elevator:表示电梯,包含最低楼层、最高楼层、当前楼层和运行方向等属性,提供了获取和设置这些属性的方法。
类 Controller:处理电梯的请求和移动逻辑,包括确定电梯的运行方向、判断是否应该在指定楼层停靠、查找下一个要到达的楼层、打开和关闭电梯门以及电梯的移动操作。
类 Main:程序的入口点,负责读取用户输入,创建电梯、请求队列和控制器对象,并调用控制器的 processRequests 方法来处理请求。
- 代码行数(Lines):307
- 语句数量(Statements):146
- 代码结构相关(Percent Branch Statements):分支语句所占百分比,为 21.9% ,反映代码中使用如if - else 、switch等分支结构的比例情况。
- 方法调用语句数量(Method Call Statements):105
- 含注释的代码行所占百分比(Percent Lines with Comments):10.1%
- 类和接口的数量(Classes and Interfaces):2
- 平均每个类的方法数(Methods per Class):9.00
- 平均每个方法的语句数(Average Statements per Method): 7.44
- 最复杂方法所在的行号(Line Number of Most Complex Method): 14
- 最复杂方法的名称(Name of Most Complex Method):Passenger.getDestination
- 最大复杂度(Maximum Complexity): 1
- 平均复杂度(Average Complexity):1.00
代码分析: - 性能问题:在 findNextFloor 方法中,使用了遍历请求队列的方式来查找下一个要到达的楼层,当请求队列较长时,性能可能会受到影响。
- 代码重复:在 shouldStop 和 findNextFloor 方法中,有部分代码逻辑重复,例如对内部请求和外部请求的遍历(可能导致代码运行超时)
- 注释不够详细:虽然代码中有一些注释,但部分注释不够详细,例如 Controller 类中的一些方法的注释可以进一步完善,以帮助其他开发者更好地理解代码。
三、三次电梯问题学习心得
这三次电梯逐渐模块化,让我在学习中更加了解面向对象的特点,把职责分离清晰,将电梯系统拆解为Elevator(状态管理)、RequestQueue(请求队列)、Controller(逻辑控制)等独立模块,每个类只负责单一职责,如:Elevator仅管理电梯的当前楼层、运行方向等状态;Controller专注于请求处理和移动逻辑,避免代码混杂。模块化能显著提升代码的可读性和维护性,后续扩展功能(如多电梯调度)时,只需修改或新增模块,无需大幅调整整体结构。
虽然代码中未显式使用接口,但通过类的封装(如RequestQueue提供统一的请求管理方法),隐藏了内部实现细节(使用LinkedList存储请求)。
我们可以调度策略的合理性,我感觉我还是没太了解电梯问题的深层内部逻辑,当前代码采用 “简单先来先服务 + 方向优先” 的策略,可能是有问题的,因为最后代码也总是超时,可以考虑在 findNextFloor 方法中重新引入外部请求的方向判断,确保电梯在同方向上处理所有可能的请求后再转向。对于请求移除的问题,可能需要使用更有效的数据结构,如使用集合来自动去重,或者在添加请求时进行重复检查,避免无效请求进入队列。
优化思路:
引入 “捎带请求” 逻辑,在同向运行时尽可能处理沿途请求,减少折返次数;
使用电梯调度算法(如 SCAN、LOOK 算法),按楼层顺序批量处理同方向请求,提升效率。
目前使用LinkedList存储请求,遍历查找下一楼层的时间复杂度为 O (n)。
对内部请求和外部请求分别按楼层排序(如使用TreeSet或有序列表),查找最近楼层时可通过二分法降低时间复杂度;
对外部请求,按方向分组(上行请求、下行请求),避免遍历无关请求。
四、踩坑心得
1. 逻辑移除不清楚
当我输入如下楼层时:
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
但是我却只能输出
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
少了6层一下的部分,找了很长时间的错误,发现问题出在remove移除内外部请求上,原来的代码是把移除内外请求放到了一个函数中,
点击查看代码
// 移除指定楼层的请求
private void removeRequests(int floor) {
listin.remove(Integer.valueOf(floor));
// 移除外部请求
listout.removeIf(request -> request.floor == floor && request.Direction.equals(Direction));
}
点击查看代码
public boolean isStop(int floor) {
boolean shouldStop = false;
// 处理内部请求(无条件停靠)
if (!innerQueue.isEmpty() && innerQueue.get(0) == floor) {
innerQueue.remove(0);
shouldStop = true;
}
// 处理外部请求
if (!outerQueue.isEmpty() && outerQueue.get(0).floor == floor) {
OutQueue req = outerQueue.get(0);
// 情况1:方向匹配时直接停靠
if (direction == req.direction) {
outerQueue.remove(0);
shouldStop = true;
}
// 情况2:方向不匹配,但内部请求为空或者满足特殊条件
else if (innerQueue.isEmpty() ||
(direction == Direction.UP && !innerQueue.isEmpty() && innerQueue.get(0) < currentFloor) ||
(direction == Direction.DOWN && !innerQueue.isEmpty() && innerQueue.get(0) > currentFloor)) {
outerQueue.remove(0);
shouldStop = true;
// 需要转向
direction = req.direction;
}
}
return shouldStop;
}
}
2. 代码逻辑错误循环
在解决完这个错误后,运行发现超时,我将此代码放在IDEA上进行各种测试,发现如果向上述这种请求不算很多的还好,如果一长串的内外请求那么代码运行之后可能会一直在某层之间循环,我发现错误主要集中在findNextFloor中,仅根据楼层位置(高于 / 低于当前楼层)决定是否加入上行 / 下行路径。修改后,电梯会先到达请求楼层,再在停靠时判断方向是否匹配或是否需要转向。但是我感觉这种改变它没有注意外部请求的逻辑方向问题,我觉得逻辑可能依旧是错误的,在转向方向可能还是不太明晰的
点击查看代码
public int findNextFloor() {
int nextFloor = currentFloor;
int upNext = maxFloor + 1; // 初始设为无效楼层(上方最远)
int downNext = minFloor - 1; // 初始设为无效楼层(下方最远)
// 处理内部请求(所有楼层)
for (int floor : innerQueue) {
if (floor > currentFloor) {
upNext = Math.min(upNext, floor);
} else if (floor < currentFloor) {
downNext = Math.max(downNext, floor);
}
}
// 处理外部请求(仅按楼层位置,不限制方向)
for (OutQueue req : outerQueue) {
if (req.floor > currentFloor) {
upNext = Math.min(upNext, req.floor); // 无论方向,高层请求加入上行路径
} else if (req.floor < currentFloor) {
downNext = Math.max(downNext, req.floor); // 无论方向,低层请求加入下行路径
}
}
// 根据当前方向选择下一个楼层
if (direction == Direction.UP) {
nextFloor = (upNext <= maxFloor) ? upNext : currentFloor;
} else if (direction == Direction.DOWN) {
nextFloor = (downNext >= minFloor) ? downNext : currentFloor;
} else { // 静止时优先处理上行请求
if (upNext <= maxFloor) {
nextFloor = upNext;
} else if (downNext >= minFloor) {
nextFloor = downNext;
}
}
return nextFloor;
}
点击查看代码
1
20
<3>
<4,DOWN>
<1>
end
点击查看代码
1
20
<5>
<3,UP>
<7>
<5,UP>
<2>
<6,DOWN>
<16,DOWN>
<4,UP>
<14,UP>
<9>
<3>
<2,UP>
<3,DOWN>
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: 8 Direction: UP
Current Floor: 9 Direction: UP
Current Floor: 10 Direction: UP
Current Floor: 11 Direction: UP
Current Floor: 12 Direction: UP
Current Floor: 13 Direction: UP
Current Floor: 14 Direction: UP
Current Floor: 15 Direction: UP
Current Floor: 16 Direction: UP
Current Floor: 15 Direction: DOWN
Current Floor: 14 Direction: DOWN
Current Floor: 13 Direction: DOWN
Current Floor: 12 Direction: DOWN
Current Floor: 11 Direction: DOWN
Current Floor: 10 Direction: DOWN
Current Floor: 9 Direction: DOWN
Current Floor: 8 Direction: DOWN
Current Floor: 7 Direction: DOWN
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
Current Floor: 2 Direction: DOWN
Open Door # Floor 2
Close Door
Current Floor: 3 Direction: UP
Current Floor: 4 Direction: UP
Current Floor: 5 Direction: UP
Current Floor: 6 Direction: UP
Current Floor: 7 Direction: UP
Current Floor: 8 Direction: UP
Current Floor: 9 Direction: UP
Open Door # Floor 9
Close Door
Current Floor: 10 Direction: UP
Current Floor: 11 Direction: UP
Current Floor: 12 Direction: UP
Current Floor: 13 Direction: UP
Current Floor: 14 Direction: UP
Current Floor: 15 Direction: UP
Current Floor: 16 Direction: UP
Open Door # Floor 16
Close Door
Current Floor: 15 Direction: DOWN
Current Floor: 14 Direction: DOWN
Current Floor: 13 Direction: DOWN
Current Floor: 12 Direction: DOWN
Current Floor: 11 Direction: DOWN
Current Floor: 10 Direction: DOWN
Current Floor: 9 Direction: DOWN
Current Floor: 8 Direction: DOWN
Current Floor: 7 Direction: DOWN
Current Floor: 6 Direction: DOWN
Current Floor: 5 Direction: DOWN
Current Floor: 4 Direction: DOWN
Current Floor: 3 Direction: DOWN
Open Door # Floor 3
Close Door
Current Floor: 2 Direction: DOWN
Current Floor: 3 Direction: UP
Current Floor: 4 Direction: UP
Open Door # Floor 4
Close Door
Current Floor: 5 Direction: UP
Current Floor: 6 Direction: UP
Current Floor: 7 Direction: UP
Current Floor: 8 Direction: UP
Current Floor: 9 Direction: UP
Current Floor: 10 Direction: UP
Current Floor: 11 Direction: UP
Current Floor: 12 Direction: UP
Current Floor: 13 Direction: UP
Current Floor: 14 Direction: UP
Open Door # Floor 14
Close Door
Current Floor: 13 Direction: DOWN
Current Floor: 12 Direction: DOWN
Current Floor: 11 Direction: DOWN
Current Floor: 10 Direction: DOWN
Current Floor: 9 Direction: DOWN
Current Floor: 8 Direction: DOWN
Current Floor: 7 Direction: DOWN
Current Floor: 6 Direction: DOWN
Current Floor: 5 Direction: DOWN
Current Floor: 4 Direction: DOWN
Current Floor: 3 Direction: DOWN
Current Floor: 2 Direction: DOWN
Open Door # Floor 2
Close Door
Current Floor: 3 Direction: UP
Open Door # Floor 3
Close Door
五、总结
在最近几次电梯问题的学习中,我对正则表达式的实际应用有了更深入的掌握。通过正则表达式,能够精准匹配输入格式(如内部请求<\\d+>
或外部请求<\\d+,\\s*(UP|DOWN)>
),有效过滤无效数据(如楼层超出范围、方向拼写错误)。例如,用matches("<\\d+>")
校验内部请求格式,用replaceAll("[<>]", "")
提取楼层数字,这种“先校验再处理”的逻辑极大提升了程序准确,避免因数据导致的逻辑混乱。
关于数据结构的使用,ArrayList
和List
的差异让我体会到接口编程的灵活性。初期直接使用ArrayList
存储请求,虽能实现基本功能,但在查找最近楼层时需遍历整个列表(时间复杂度O(n))。后来改用List
接口引用TreeSet
实现类,利用其自动排序特性,将查找复杂度降至O(log n)。同时,通过List
的多态性,可轻松切换不同实现类(如LinkedList
处理频繁增删场景),体现了“面向接口编程”的优势。核心难点在于运动逻辑与转向策略的设计,这要求在代码中维护清晰的状态(如Direction.UP/DOWN/IDLE
),并在每次移动后重新评估队列状态。
提前绘制类图是理清逻辑的关键。通过类图明确Elevator
(状态管理)、RequestQueue
(请求存储)、Controller
(逻辑调度)的职责边界,避免代码逻辑混杂。例如,Elevator
仅负责楼层移动和开关门,RequestQueue
处理请求的合法性与去重,Controller
集中实现调度算法,这种模块化设计让代码结构清晰,后期扩展(如多电梯调度)时只需修改Controller
逻辑,无需改动底层类。总的来说,这次电梯程序锻炼了逻辑思维模式,帮助让我从C语言过度到面向对象设计。