题目集5~7的总结性Blog

前言

经过了前面三次的电梯调度程序的迭代作业,在这里做一个总结,电梯调度的问题确实是我目前做过的最复杂的一个程序作业。(也可能是本人做得少),在前期的时候甚至不知道如何下手。。。第一次体会到了从做一些简单的考察语法的题到系统性,综合性和逻辑性都挺大的一个程序作业的差距。不过肝了一个周末后,确实是有点效果。虽然程序依然存在逻辑问题,再解决完第一次的电梯题后,第二次的电梯迭代则是要求我们使用解耦合的思想,将这些类全部剥离开来,包括实体类和控制类,这也是我第一次感受到解耦合的重要作用。第三次的话加了一些新的要求,不过有了前一次的代码基础,把第二次代码做了一点修改就过了。果然,一句话与诸位共勉:千淘万漉虽辛苦,吹尽狂沙始到金

复杂度介绍

鉴于本人水平有限,先来介绍一下复杂度分析,以便后续对程序的分析。

1.复杂度(通常指圈复杂度,Cyclomatic Complexity):是衡量代码逻辑复杂程度的重要指标,由 Thomas J. McCabe 提出,用于量化代码中独立逻辑路径的数量,反映代码中分支结构(如 if-else、switch)和循环(如 for、while)的复杂程度。数值越高,代码逻辑越复杂,理解、测试和维护的难度越大。一般认为,圈复杂度 ≤ 10 的函数较易维护;若 > 20,则需考虑重构。

2.计算公式:圈复杂度 = 代码中判断节点(分支、循环等)的数量 + 1。例如,一个函数中有 2 个 if 语句和 1 个 for 循环,其圈复杂度为 2+1+1=4(每个判断节点 +1,初始 +1)。

3.平均复杂度:通常指一个文件、类或项目中所有函数(或方法)的圈复杂度(Cyclomatic Complexity,用于衡量代码逻辑分支数量,反映代码逻辑复杂程度)的平均值。若该值较低(如小于 5),通常表示代码中函数的逻辑相对简单,结构清晰,易于理解、测试和维护。若该值较高(如大于 10),则意味着代码整体逻辑较复杂,可能存在较多的条件判断(if-else、switch)、循环等结构,维护和调试的难度会增加。

设计与分析

第一次电梯设计

题目概述

设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
请编写一个Java程序,设计一个电梯类,包含状态管理、请求队列管理以及调度算法,并使用一些测试用例,模拟不同的请求顺序,观察电梯的行为是否符合预期,比如是否优先处理同方向的请求,是否在移动过程中处理顺路的请求等。为了降低编程难度,不考虑同时有多个乘客请求同时发生的情况,即采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求),具体运行规则详见输入输出样例。

分析

第一次的电梯设计只要求设计电梯类和测试类,首先用户输入数据,这时考虑使用正则表达式来读取字符串中有用的数据,创建数组把数据存储在队列中。其中,外部请求由于需要考虑方向和请求楼层,故还需创建一个数组存储方向。接下来就是电梯类的设计。首先,根据电梯的运行规则,它只会处理队头数据,这是这个题目的一个关键点。现在我们需要模拟电梯的运行,电梯默认停在一楼,状态是静止,判断方向,这时读取队头数据,方向向上运行,每运动一层,考虑是否停靠,如果有符合条件的内部请求或者同向的外部请求,电梯停靠,停靠时会清除队头数据,把这层楼的队头请求全部清除。接下来判断方向(电梯停就要判断方向),如果同方向还有请求,电梯就继续按照原方向运行。是不是觉得好像搞定了,一写代码发现测试样例能过,但是测试点过不了,回头分析,这时考虑还是不是很周到,忽略了一些特殊情况,比如在电梯什么时候会接反方向的外部请求,接了反方向的外部请求后方向是否会改变,这些题目中都没有提及,这时我们可以通过一些案例进行模拟。为此,我们根据一些测试数据进行分析。比如输入
1
20
<3>
<4,DOWN>
<1>
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
Open Door # Floor 4
Close Door
Current Floor: 3 Direction: DOWN
Current Floor: 2 Direction: DOWN
Current Floor: 1 Direction: DOWN
Open Door # Floor 1
Close Door
这个样例是我设计的,为了解决电梯什么时候会接反方向的外部请求,接了反方向的外部请求后方向是否会改变。这时我们很容易发现,如果接了外部请求不反向,我们的程序就会进入死循环。这时就体现了模拟的重要性。自己设计数据然后通过自己的程序进行测试,最终解决问题。

SourceMonitor报表

以上是关于电梯题目的思路逻辑方面的概述,接下来给出给出SourceMonitor的报表进行程序方面的分析。image
image
最大复杂度超过了10,所以其实存在不易于维护的部分,但是平均复杂度只有2.8,说明整体的复杂度并不高。

类图

从代码的最大复杂度和平均复杂度可以看出均满足要求。行数适中,最复杂的方法是电梯里的添加请求类。设计好的程序,给出类图:image
Main类负责读取输入,初始化电梯并处理请求直至输入 “end”。内部类Elevator通过属性(如currentFloor当前楼层、InnerQueue内部队列、OuterQueue外部队列等)管理电梯状态与请求。addRequest方法通过正则匹配区分内外部请求并加入对应队列;determineDirection根据队列头请求与当前楼层关系确定运行方向;moveOneFloor实现电梯移动;checkStopConditions优先处理内部请求,再根据方向处理外部请求以判断是否停靠;start方法驱动电梯循环运行,直至无未处理请求。Direction和State枚举分别定义电梯运行方向(上、下、停)与状态(移动、空闲等)。整体通过队列管理请求,结合方向判断与停靠逻辑,实现电梯的基本调度功能。

电梯关键运行逻辑代码:

点击查看代码
public void start() {
            System.out.println("Current Floor: 1 Direction: UP");
            while (hasPendingRequests()) {
                // 仅在停止状态重新计算方向
                if (state==State.IDLE&&!flag) {
                    determineDirection();
                }
                if(flag){
                    directionReverse();
                }
                flag=false;
                // 移动后立即检查是否停靠
                moveOneFloor();
                if (checkStopConditions()) {
                    processStop();
                    state = State.IDLE; // 停靠后进入空闲状态
                } else {
                    state = State.MOVING; // 移动中保持运行状态
                }
            }
        }

在编写过程中最复杂的就是就是判断方向和检查是否停靠的方法,

点击查看代码
// 方向判断方法(不考虑外部请求方向,仅楼层差方向)
        private void determineDirection() {
            // 端点层强制转向
            if (currentFloor == maxFloor) {
                direction = Direction.DOWN;
                return;
            } else if (currentFloor == minFloor) {
                direction = Direction.UP;
                return;
            }

            // 获取队列头请求方向
            Direction innerDir = getHeadInnerDirection();
            Direction outerDir = getHeadCtoODirection();

            // 方向维持逻辑
            if (direction == Direction.UP) {
                if (innerDir == Direction.UP || outerDir == Direction.UP) {
                    return; // 有同向请求则维持方向
                }
            } else if (direction == Direction.DOWN) {
                if (innerDir == Direction.DOWN || outerDir == Direction.DOWN) {
                    return; // 有同向请求则维持方向
                }
            }

            // 方向反转条件
            if (direction != Direction.STOP) {
                directionReverse(); // 无同向请求时反转方向
            }
        }
		// 停靠检查方法
        private boolean checkStopConditions() {
            boolean shouldStop = false;

            // 第一优先级:内部请求必停
            if (front1 < rear1 && currentFloor == InnerQueue[front1]) {
                dequeue1();
                shouldStop = true;
                // 方向保持不变
                if(front2 < rear2 && currentFloor == OuterQueue[front2]){
                    dequeue2();
                }
            }

            // 第二优先级:外部请求处理
            if (!shouldStop && front2 < rear2 && currentFloor == OuterQueue[front2]) {
                Direction reqDir = outerDirections[front2];

                // 顺向请求必停
                if (reqDir == direction) {
                    dequeue2();
                    shouldStop = true;
                }
                // 反向请求处理
                else {
                    // 检查当前方向是否还有请求
                    if (!hasInnerInCurrentDirection()) {
                        dequeue2();
                        shouldStop = true;
                        flag=true;
                    }
                }
            }

            return shouldStop;
        }

倒不是说这两个方法有多难写,只是很难去get题目中真正的电梯运行的逻辑。只能去一点一点去试。

心得

第一次迭代所以代码都写在电梯类中,代码显得很臃肿,同时维护起来比较困难,在代码的一些方法中,大量堆积if判断,使得阅读代码困难。同时,注释较少也需要改进。

第二次电梯设计

题目概述

必须包含但不限于设计电梯类、乘客请求类、队列类以及控制类,电梯运行规则与前阶段单类设计相同,但要处理如下情况:
1.乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入.
2.继续执行乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>

类图

image

类图分析

Elevator 与 Direction、State:通过属性关联,电梯的方向和状态由这两个枚举定义。
Controller 与 Elevator、RequestQueue:通过组合关系,持有电梯和请求队列的引用,实现对电梯和请求的控制。
RequestQueue 与 ExternalRequest:管理外部请求链表,处理外部请求数据。
ExternalRequest 与 Direction:外部请求的方向由 Direction 枚举定义。
类图通过清晰的职责划分与协作关系,构建了一个可扩展的电梯控制系统模型:Main 启动程序,Controller 协调控制,Elevator 管理自身状态,RequestQueue 处理请求,ExternalRequest 封装外部请求信息,Direction 和 State 提供方向与状态定义,各组件协同实现电梯的运行逻辑。

SourceMonitor报表图

image

电梯运行关键程序

点击查看代码
// 电梯运行主循环
    public void start() {
        // 获取队列头请求方向
        System.out.println("Current Floor: 1 Direction: UP");
        while (hasPendingRequests()) {
            // 仅在停止状态重新计算方向
            Direction innerDir = getHeadInnerDirection();
            Direction outerDir = getHeadCtoODirection();
            if (elevator.getState()==State.IDLE&&!flag) {
                determineDirection();
            }
            if(flag){
                directionReverse();
                if(innerDir==Direction.STOP&&outerDir==Direction.STOP){
                    elevator.setDirection(Direction.STOP);
                }
            }
            if(elevator.getDirection()==Direction.STOP){
                processStop();
                break;
            }
            flag=false;
            // 移动后立即检查是否停靠
            moveOneFloor();
            if (checkStopConditions()) {
                processStop();
                elevator.setState(State.IDLE);  // 停靠后进入空闲状态
            } else {
                elevator.setState( State.MOVING);// 移动中保持运行状态
            }
        }
    }
}

关键点

这题逻辑和第一题完全一致,只是需要把第一题全部放在电梯类的代码进行解耦,分成各种类,使用的时候还要考虑将直接调用的属性转化为调用getter和setter方法。这些事做完之后,处理重复请求和不符合要求的楼层请求就相对简单多了。

心得

通过优化代码,平均复杂度已经减到了1.6,最大复杂度也减到了12,但是还是大于标准,需要继续优化,同时,这次迭代将代码拆分成各个类,提高了阅读性和可复用性。

第三次电梯设计

题目概述

对之前电梯调度程序再次进行迭代性设计,加入乘客类(Passenger),取消乘客请求类,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客类、队列类以及控制类。
电梯运行规则与前阶段相同,但有如下变动情况:
乘客请求输入变动情况:外部请求由之前的<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层>
对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)

类图

image
该类图中,Main 作为程序入口启动系统,Controller 通过组合关系持有 Elevator 和 RequestQueue,协调二者工作,Elevator 的 direction 属性依赖 Direction 枚举(定义上下等方向)、state 属性依赖 State 枚举(定义运行或空闲状态)来确定自身状态与行为,RequestQueue 管理着封装了乘客起始与目标楼层信息的 Passenger 请求,为 Controller 提供数据支撑以实现电梯调度,如此各组件紧密协作,Main 触发程序运行,Controller 统筹,Elevator 依据枚举定义执行操作,RequestQueue 组织乘客请求,共同构建起电梯系统的运行逻辑。

SourceMonitor报表:

image
image
image

总的来说,电梯程序进一步解耦,各部分彼此联系,较比前两次更加完善。但是最大复杂度依然很大,一方面说明程序确实有比较复杂的部分,同时也要考虑是否能够优化。

电梯运行过程代码

点击查看代码
// 电梯运行主循环
    public void start() {
        // 获取队列头请求方向
        System.out.println("Current Floor: 1 Direction: UP");
        while (hasPendingRequests()) {
            // 仅在停止状态重新计算方向
            Direction innerDir = getHeadInnerDirection();
            Direction outerDir = getHeadCtoODirection();
            if (elevator.getState()==State.IDLE&&!flag) {
                determineDirection();
            }
            if(flag){
                directionReverse();
                if(innerDir==Direction.STOP&&outerDir==Direction.STOP){
                    elevator.setDirection(Direction.STOP);
                }
            }
            if(elevator.getDirection()==Direction.STOP){
                processStop();
                break;
            }
            flag=false;
            // 移动后立即检查是否停靠
            moveOneFloor();
            if (checkStopConditions()) {
                processStop();
                elevator.setState(State.IDLE);  // 停靠后进入空闲状态
            } else {
                elevator.setState( State.MOVING);// 移动中保持运行状态
            }
        }
    }
}

关键点:

这次迭代的关键其实是在于将<\d+,[A-Z]+>改成<\d+,\d+>,然后外部请求方向无疑就是多算了一步,把目标楼层和请求楼层做差便可。这样一来,问题解决。

踩坑心得

主要问题

从第一次电梯到第三次,分析其中错误的或者未能通过测试的代码,主要有以下几点:
1.运行超时
2.答案错误
3.类设计不清晰
这些问题主要集中在第一次电梯的设计过程,后两次迭代主要是类设计的问题,也就是解耦的能力,耦合性低的代码具有更好的复用性,这也是为什么需要不断迭代改进的原因。

点击查看代码
  public void start() {
            System.out.println("Current Floor: 1 Direction: UP");
            while (hasPendingRequests()) {
                // 仅在停止状态重新计算方向
                if (state==State.IDLE&&!flag) {
                    determineDirection();
                }
                if(flag){
                    directionReverse();
                }
                flag=false;
                // 移动后立即检查是否停靠
                moveOneFloor();
                if (checkStopConditions()) {
                    processStop();
                    state = State.IDLE; // 停靠后进入空闲状态
                } else {
                    state = State.MOVING; // 移动中保持运行状态
                }
            }
        }
这是第一次电梯运行的代码逻辑。拿来说的原因在于这个flag,这个flag是什么含义呢?它出现在检查是否停靠的函数里。
点击查看代码
 // 停靠检查方法
        private boolean checkStopConditions() {
            boolean shouldStop = false;

            // 第一优先级:内部请求必停
            if (front1 < rear1 && currentFloor == InnerQueue[front1]) {
                dequeue1();
                shouldStop = true;
                // 方向保持不变
                if(front2 < rear2 && currentFloor == OuterQueue[front2]){
                    dequeue2();
                }
            }

            // 第二优先级:外部请求处理
            if (!shouldStop && front2 < rear2 && currentFloor == OuterQueue[front2]) {
                Direction reqDir = outerDirections[front2];

                // 顺向请求必停
                if (reqDir == direction) {
                    dequeue2();
                    shouldStop = true;
                }
                // 反向请求处理
                else {
                    // 检查当前方向是否还有请求
                    if (!hasInnerInCurrentDirection()) {
                        dequeue2();
                        shouldStop = true;
                        flag=true;
                    }
                }
            }

            return shouldStop;
        }
如果没有这个flag判断会怎么样?在处理反向请求的时候可能会进入死循环。那我是如何发现这个错误的呢,其实就是通过模拟各种情况,自己编写测试案例运行,最终发现运行的逻辑错误 如何处理重复请求,其实有两种思路,一是在入队的时候就过滤掉:
点击查看代码
 public static void main(String[] args) {

        int minFloor;
        int maxFloor;
        Scanner sc = new Scanner(System.in);
        minFloor = Integer.parseInt(sc.nextLine());
        maxFloor = Integer.parseInt(sc.nextLine());
        Elevator  elevator = new Elevator(minFloor, maxFloor);
        RequestQueue queue=new RequestQueue();
        Controller controller=new Controller(elevator,queue);
        while(true) {
            String input  = sc.nextLine().trim();
            if(input.equalsIgnoreCase("end")) break;
            if(input.matches("<\\d+>")) { // 内部请求
                int floor = Integer.parseInt(input.replaceAll("[<>]", ""));
                if(queue.getRear1()>=1){
                    if(floor== queue.getInnerQueueR()){
                        continue;
                    }}
                if(elevator.validateFloor(floor)) queue.enqueue1(floor);
            }
            else if(input.matches("<\\d+,\\d+>")) { // 外部请求
                String[] parts = input.replaceAll("[<>]", "").split(",");
                int sourcefloor = Integer.parseInt(parts[0]);
                int destinationfloor = Integer.parseInt(parts[1]);
                Passenger passenger=new Passenger(sourcefloor,destinationfloor);
                Direction dir;
                if(sourcefloor-destinationfloor<0){
                    dir=Direction.UP;
                }
                else{
                     dir =Direction.DOWN;
                }
                if(queue.getRear2()>=1){
                    if(sourcefloor==queue.getOuterQueueR().getSourceFloor()&&destinationfloor==queue.getOuterQueueR().getDestinationFloor()){
                        continue;
                    }}
                if(elevator.validateFloor(sourcefloor)&&elevator.validateFloor(destinationfloor)) {
                    queue.enqueue2(passenger, dir);
                }
            }
        }

        controller.start();
        // 请求处理核心逻辑
    }

内部请求修改代码:
点击查看代码
if(floor== queue.getInnerQueueR()){
                        continue;
                    }

外部请求修改代码:

点击查看代码
if(sourcefloor==queue.getOuterQueueR().getSourceFloor()&&destinationfloor==queue.getOuterQueueR().getDestinationFloor()){

                        continue;
                    }

二是在全部入队之后再消除掉重复的。我这里选用的是第一钟方式。

其他细节

1.end是不区分大小写的,这里可以用忽视大小写的input.equalsIgnoreCase("end");这条语句来实现。DOWN和UP是必须大写的。
2.正则表达式在处理字符串信息方面有着得天独厚的优势。但是里面有许多要记的符号和其用法。这里用到的是<\\d+>;
3.属性必须是私有的,使用的时候必须调用getter和setter方法,这体现了封装性,不能因为方便把属性设为公有,这种做法虽然可以节省很多时间,但是没有达到训练的效果。
4.设计实体类和控制类,目的是让其解耦,提高代码的可复用性,同时要学会使用自己写过的类似的代码,不用重复造轮子,这样能提高代码的编写效率。
5.第一层电梯的方向一定向上,这时在电梯运行的循环之前就需要先打印Current Floor: 1 Direction: UP

总结

从这次作业中可以学到面向对象编程的类设计与职责划分,如 Elevator 类管理电梯状态与操作,Controller 类协调电梯与请求队列,体现单一职责原则;掌握类间关系(组合、依赖),如 Controller 组合 Elevator 和 RequestQueue,Elevator 依赖 Direction 和 State 枚举定义方向与状态;学会枚举类型的应用,清晰界定电梯运行方向(如 UP、DOWN)和状态(如 MOVING、IDLE);理解数据结构在请求管理中的应用,如 RequestQueue 对乘客请求的组织与处理同时善用模拟测试,可以更快帮助我们找到我们代码的逻辑漏洞。从三次的练习让我对java有了不一样的认识,第一次体会到有难度,有挑战性的程序任务。同时我期待的第三次的迭代,以为会改变电梯运行规则,比如从只靠虑队头到全局考虑。又或者考虑时间顺序,先后顺序而不是队头。因为在刚做这个题目的时候就有这方面的想法。但是第三次的迭代只是改变了以下外部请求的输入方式,其实本质没有任何改变。只需把目标楼层和请求楼层做差就能知道其方向,这样又转化成立前面的题目的模式。不过不管怎么说,第三次进一步添加了乘客类,虽然难度不高,但是处处体现了单一职责原则。我想这也是老师们想要我们掌握的最核心的东西。写这段总结的时候已经是凌晨,倦意袭来,最后想用一句我喜欢的诗句结束:
“宝剑锋从磨砺出,梅花香自苦寒来。”

posted @ 2025-04-16 17:15  太妙了  阅读(110)  评论(0)    收藏  举报