luyanwei24201920

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

目录

一、引言

1. 背景信息

在现代建筑里,电梯作为垂直出行的关键工具,面对楼层增多与人流量增大的状况,高效调度以缩短乘客等待时间、提升运行效率成了关键问题,电梯调度算法应运而生。

早期 FCFS、SSTF 等算法各有缺陷,LOOK 算法在此基础上诞生,旨在平衡效率与公平性。它让电梯如双向扫描磁头,依请求队列动态调整方向,只在有请求楼层间运行以减少空驶。而单例电梯调度针对小型建筑或特定场所的一部电梯情况,LOOK 算法会依电梯运行方向与楼层请求,选最近目标楼层服务,减少往返次数与等待时间 。

注:本次电梯调度程序遵守简化的LOOK算法。

2. 题目信息

设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:
电梯默认停留在1层,状态为静止。当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动。当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。
电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。

请编写一个Java程序,设计一个电梯类,包含状态管理、请求队列管理以及调度算法,并使用一些测试用例,模拟不同的请求顺序,观察电梯的行为是否符合预期,比如是否优先处理同方向的请求,是否在移动过程中处理顺路的请求等。为了降低编程难度,不考虑同时有多个乘客请求同时发生的情况,即采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求),具体运行规则详见输入输出样例。

输入样例 输出样例

二、电梯调度分析

1. 过程分析

(1)在输入队列的信息之后,首先根据数据输入的顺序将输入的信息分为内部和外部两个队列。

(2)根据当前楼层信息与两个队头的楼层信息判断运动方向,楼层的判断遵守以下几点原则:

I.就近原则
II.每次处理一个数据(串行处理)
III.电梯会优先处理与目前运动方向相同的请求

(3)电梯在保持前方向的情况下,会前往较近的楼层,例如:
若当前电梯信息NowFloor: 5 Direction: UP
内外队列请求为: <7,8> <9>
则此时电梯会前往较近的7层。
(4)判断楼层信息时要同时考虑电梯的运动方向和乘客请求的运动方向且考虑方向优先于就近原则,例如:
若当前电梯信息NowFloor: 5 Direction: UP
内外队列请求为: <7,4> <9>
则此时电梯不会前往较近的7层,而会直接前往9层,因为外部请求的方向为Down。
(5)每次到达目的楼层之后,要对该楼层进行出队操作,即List.removeFirst(),当两个队头表示同一楼层时(且外部队列的方向要与目前电梯移动方向相同),要对两个队头进行同时出队的操作。
(6)当所有队头楼层均不能使得电梯沿当前方向运行时,电梯要改变方向。

2. 调度难点解决

(1)读入数据

通过Scanner类从标准输入读取数据进入一个List,使用正则表达式对List里的数据进行匹配和分类,并将匹配结果存储到不同的LinkedList中。在读取过程中,通过判断是否遇到结束标志来终止输入。

匹配方法:String regex1 = "<(.*?,(UP|DOWN))>";String regex2 = "<(.*?)>";

终止方法:end / END
示例:
输入数据:
1
20
<3,UP>
<5>
<6,DOWN>
<7>
<3>
end
队列信息:

(2)处理逻辑

当任意一队列不为空时,将目前的楼层(默认为1)与ListIn和ListOut队列的队头楼层比较:
分为四种情况
① 请求均高于当前楼层: 向上移动,判断外部请求的方向,若外部请求与电梯运动方向为同向,则按照就近原则前往较近的楼层;反之,到达内部请求楼层。

② 请求均低于当前楼层:向下移动,判断外部请求的方向,若外部请求与电梯运动方向为同向,则按照就近原则前往较近的楼层;反之,到达内部请求楼层。

③ ListIn请求高于当前楼层,ListOut请求低于当前楼层:向上或向下移动,前往与目前运动方向同向的楼层。

④ ListOut请求高于当前楼层,ListIn请求低于当前楼层:向上或向下移动,前往与目前运动方向同向的楼层。

注意:当ListIn队列或ListOut队列任意一队列为空时,只需串行处理非空队列的数据,无需一直保持处理单一运动方向的数据。
电梯程序在两个队列均为空时停止

三、优化方案(迭代)

第一次题目:

UML图

Queue类分析

属性

  • headtail:类型为int且初始值都为1 ,用于标识队列的头部和尾部位置,帮助管理队列元素的进出顺序。
  • nowfloor:类型为int且初始值为0 ,用于记录电梯当前所在楼层,与电梯调度场景相契合。
  • isup:类型为boolean且初始值为true ,用于表示电梯的运行方向(向上或向下),true代表向上运行方向。
  • state:类型为int且初始值为0 ,用于表示电梯的状态,如停止、运行中等,通过不同的整数值来区分具体状态。

方法

  • Getter方法getisup() 用于获取isup属性的值。
  • Setter方法setup(boolean up) 用于设置isup属性的值。
  • 业务方法processRequests() 从名称推测,用于处理电梯请求队列的核心方法,负责调度电梯响应各种请求,实现电梯的运行逻辑。
  • 构造方法Queue() 是类的构造函数,用于在创建Queue类实例时进行初始化操作,比如对上述属性进行默认值设置等。
  • 其他方法:一系列以setget开头的方法,如setNowfloor(int nowfloor)getNowfloor() 等,用于设置和获取相应属性的值。

Main类分析

  • main(String args[]):这是Java程序的入口方法,程序启动时会从这里开始执行。在这个方法中,创建Queue类的实例,并调用其相关方法来启动电梯调度程序,协调整个程序的执行流程。

缺点反思:

由于本次题目为第一次接触LOOK算法,仅完成了单例电梯调度的逻辑设计,没有将不同的功能拆分开来使其成为单一的设计类,即电梯类的功能过于集中;
使用了大量的if-else语句,同时造成代码的冗余;
实际上代码的逻辑并不完善,在设计整个问题时仍然存在漏洞没有解决(在后续的题目中逐渐完善);
在开始阶段,由于题目的难度大,产生了畏难情绪,没有在第一时间开始认真思考题目,而是等到了题目集开始延时后才认真思考题目的逻辑,造成了完成的时间较晚。

第二次题目:

迭代信息

在最初的电梯调度程序中,电梯类承担了过多职责,这违背了单一职责原则(SRP),导致代码的可维护性和可扩展性较差。本次迭代设计旨在重新梳理类结构,通过将不同职责分离到独立的类中,提升程序的质量。目标是设计出包含电梯类、乘客请求类、队列类以及控制类等的程序架构,各尽其职,协同实现高效的电梯调度功能。
本次题目还增加了错误数据的处理,连续相同数据的剔除,使得逻辑上更完备。
类图如下:

类设计思路

按照题目提供的类图的信息,完成Elevator类、ExternalRequest类、Controller类、ReqestQueue类和两个列举类State、Direction。
其中我优先设计了与其他类调用关系简单的类,注意了类与类之间的调用关系,如:ExternalRequest类、RequestQueue类,即UML图的“末端”,这样会更加高效,不易出错。

主要的设计类:

Elevator类

里面含有主要的电梯的属性:最大楼层、最小楼层、电梯方向、电梯状态,与电梯相关的属性,及一些Getter、Setter方法。

Controller类

里面包含主要的电梯移动逻辑实现方法,电梯方法调用,读入内部外部队列的方法的调用,。
遗憾的是,我没有将整个电梯移动的逻辑拆分成不同的方法,而是全部打包成一个controller类里面的processRequest方法,这一点在第三次题目得到了改善。

SourceMonintr分析

雷达图

柱状图


从上图可知
最复杂的方法是Controller.getIn () 即完成对数据的读入的方法,包含了正则表达式筛选和对相同数据的过滤;
最高的复杂度的值为10,因为我在完成电梯的移动逻辑时,大量地使用了嵌套if-else循环,这也是雷达图里超出合理范围的数据,最大代码深度同理,除了逻辑判断的方法复杂,其他的方法则比较简单,使得平均复杂度比较低,在代码里还有注释水平较低的问题。
每个方法的平均语句数:7.41 ,意味着平均每个方法包含 7.41 条语句。这个数值适中,如果方法语句数过多,方法可能过于复杂,难以维护和测试;过少则可能功能过于简单分散。
平均复杂度:1.37 ,说明整体方法复杂度不算高,大部分方法逻辑相对简单,但存在个别复杂度较高的方法(最大复杂度为 10 ),需要关注优化。

第三次题目

迭代信息

在本次题目中,乘客请求输入变动情况:外部请求由之前的<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层>,对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)。

新增加了一个Passenger类用于处理电梯中源楼层与目的楼层,在每一次对外部队列进行出队操作后,都会将目的楼层add到内部队列里。

SourceMonitor分析


整个的代码复杂度较高,同时代码深度高,主要是因为处理运动逻辑时方法的职责不单一,条件嵌套多;
于是,我尝试把逻辑判断的代码打包成不同的方法,同时给代码增加一些注释。

改进后:


语句数量从302减少至170,分支数量略有提升,类和结构数量从8减少至4,可以发现代码复杂度、代码深度等数据出现了大幅度的降低,可见写代码过程中SRP和注释的重要性。

反思

对于本题,在经历了前两次的调试编写后,原以为第三次题目的通过会非常简单,但是事与愿违,第一个测试点花费了我大量的时间,更是几度想要放弃,但幸运的是我坚持了下来,完成了题目。


错误的原因在两个方面:
一个方面是电梯移动情况讨论的不完全(这在我使用了大量的循环嵌套的逻辑里是致命的)比如:在每一次判断电梯移动方向时,我的逻辑只考虑了内外部队列高于或低于目前楼层并且给出相应输出的结果,而没有考虑队列请求与目前楼层相同的情况,这使得程序出现了死循环。
另一个方面,对于程序的出队逻辑还不清晰,在前面的题目里,每当两个队头出现相同目的楼层时,我都进行了同时出队的操作,而实际还要判断外部楼层与当前电梯运动方向是否一致,一致时才能同时出队。

四、总结与复盘

总的来说:三次题目难度逐渐递增,在整个迭代的过程中,我对于单例电梯LOOK算法的理解逐渐加深,在设计电梯的移动逻辑上逐渐地完善。
踩坑:主要是前期准备不足,花在设计方法的分工和逻辑的时间较少,导致后期花费了大量地时间修改代码的逻辑,填补代码漏洞。然后由于代码长度过长,无法提交,还花费时间重新将部分代码带包成方法,最后加注释。

学习收获

  1. 学习单一职责原则SRP:深切理解其在类设计中的关键作用,在设计程序时,将复杂职责拆分,类功能明晰且内聚,极大提升代码可读性,一处修改不易波及其他类,我按照题目的类图设计程序。
  2. 增强代码复用:各单一职责类复用性强,如ElevatorQuest队列类能灵活用于他处,开发效率提高,出错风险降低。
  3. 学会使用注释:增加注释率,可以提高代码的可读性,也可以降低程序的复杂程度。
  4. 完善代码逻辑:在每一次开始编程之前要首先进行更加完备的思考,首先完善整个代码的逻辑,构思每一个类和方法的职责,最终才开始敲代码。

少许建议

希望老师对于每一个教学情境下的题目提供更多工程应用场景,由算法或者架构方法的实际应用场景引入可以调动学生的兴趣,同时能丰富学生的工程知识和素养。

posted on 2025-04-19 22:30  lu_yanwei  阅读(258)  评论(0)    收藏  举报