zoukankan      html  css  js  c++  java
  • 面向对象编程 之 第二单元总结

    面向对象编程 之 第二单元总结


    本次单元的三次作业内容主题依据电梯调度依次展开。本文将主要围绕代码的设计策略、功能性能设计平衡、度量分析、自动化测试以及总结感想六方面对此次作业进行阐述。
    (1)从多线程的协同和同步控制方面,分析和总结自己三次作业的设计策略。 (2)从功能设计和性能设计的平衡方面,更细和总结自己**第三次作业**架构设计的可扩展性。 (3)基于度量分析自己的程序结构。 (4)分析自己程序的bug。 (5)分析自己发现别人程序bug所采用的策略。 (6)心得体会。


    1.电梯设计策略 & 架构设计分析 (1) & (2)


    1.1 单电梯调度

    1.1.1 需求分析 & 设计策略


      第一次作业需要完成的任务为单部多线程可稍带(ALS)电梯的模拟。笔者将其抽象成一个生产者-多消费者模型。生产者即乘客请求(Request),唯一的消费者即单部电梯(Elevator)。“托盘”,即我们的调度器(Scheduler),是用来盛放乘客请求并加入到电梯内。我认为,调度器本质上负责管理输入线程输入的请求。它是一个由请求队列组成的协调区,只负责乘客请求对应电梯、乘客间排列的规划,完成电梯间调度,而不关心电梯内部调度的策略

      由于第一次作业电梯数量为1,调度器仅仅需要将输入请求拆分为电梯内可以执行的简单任务(Task),并启动电梯线程并且分配(notify)给电梯请求。在这里,我们的电梯负责电梯的正常运行,仅仅执行移动、开门、关门、装货和卸货等等功能,而不关心电梯的运行方向;决定电梯内部调度的逻辑在任务分配器(Task Container)内实现,它是一个盛放着一部电梯的所有当前任务的容器,负责电梯内部调度的逻辑。

      电梯捎带算法选择了在ALS基础上的C-SCAN调度算法。C-SCAN算法规定电梯单向移动,直到达到无请求的最大/小楼层后,改变方向继续进行循环扫描。


    1.1.2 功能和性能设计平衡


      至此,我们第一次作业设计逻辑也比较清晰,并且有着较好的扩展性

    • 调度器(Scheduler):对输入线程传递的请求进行翻译,拆分为电梯可以处理的“进入”和“退出”的简单任务。

    • 任务(Task):电梯内部可以处理的请求的基本单位,有人员ID,目标楼层,进出选项,是否激活和附带任务等等属性。

    • 任务分配器(Task Container):一个盛放着一部电梯的所有当前任务的容器,负责电梯内部调度的逻辑。

    • 单部电梯(Elevator):负责电梯的正常运行,仅仅执行移动、开门、关门、装货和卸货等等功能;内部配置响应的Task Container。

    • 状态(State):电梯状态,有上升(up)、下降(down)和空闲(still)三种运行方向。


    ​ 第一次作业的设计总体来说可扩展性较强。其突出优点之一是实现了电梯内部调度和电梯基本功能的分离,更加模块化,也有利于采用多种调度方式(如LOOK算法,SCAN算法)等等来进一步地优化内部调度的性能。此外,设计能够较容易地扩展到多电梯,仅仅需要在调度器中实现平衡电梯间调度的算法,以决定处理请求的电梯即可。最后,在调度器阶段拆分为简单任务,届时后续实现分功能多部电梯的联合调度,电梯内部几乎无需改动。

      但不可否认这样的设计也有缺点。笔者后来反思第一次作业的设计时,发现C-SCAN并没有考虑到卸货任务的存在(虽然没有出错,但没有特判会导致任务平均等待时间增长)。此外,C-SCAN并非较优的调度算法,贪心策略反而在强测的随机数据中反而表现更好,C-SCAN在有些情况下表现并不优越,比如在电梯刚刚从4楼转为5楼,但此时4楼又来了一个请求,请求的平均等待时间则会增大。


    1.2 多电梯调度

    1.2.1 需求分析 & 设计策略


      第二次作业在单电梯的基础上需要模拟一个多线程实时电梯系统,从标准读入中读取模拟电梯数目,动态建立电梯,并且规定了电梯的最大满载量。可以预见,电梯还会有更多个性化的设置,例如开关门时间、停靠楼层、运行速度等等。

    ​ 那么在第一次作业的基础上,问题进化为多生产者-多消费者模型。多个乘客请求如何分配给多部电梯(即消费者)?此时,作为输入请求的收发装置,调度器单独成为一个线程,负责协调电梯间的任务分配,那么我采取的电梯间调度算法如下:

    ​ 首先电梯分为3类,空闲电梯(“电梯空闲”状态)上升电梯(“上升”状态),下降电梯(“下降”状态)。当外部输入请求发生时,我们根据请求发生的楼层和输入请求的方向(向上或向下)从上面的三种状态的电梯中选择两种,相同类型的电梯通过优先级(与当前楼层和目的楼层的差值,电梯当前任务数量有关)进行挑选。上升请求优先检查上升电梯,下降请求同理。如果没有符合条件的电梯,寻找任意一部空闲电梯。如果一部电梯空闲,那么这部电梯可以响应发生在所有楼层的请求。同时,还有一点需要注意的细节,即“上升电梯”不能响应在比它当前楼层低的楼层发生的请求,比如一部上升电梯正处于8楼,此时7楼传入了一个上升请求,那么这部电梯不应当响应这个请求;“下降电梯”也同理。

    ​ 那么可能存在一个请求,此时没有符合携带该请求的电梯的情况。例如,当所有电梯都处于4楼,并且状态为“上升”,那么此时若有人在3楼按动按钮(不论上下)发出请求,每一部电梯不会此请求。在这种请求无法被立即响应的情况下,我们将该请求放入等待队列,并且要求调度器每次循环都会检查等待队列里是否有可被分配的请求,调度合适的电梯。并且给每个在等待队列的请求设置等待时间,经过非常不完全的测试,选取了一个中值5000s,一旦有请求超过时间限制仍然无法被处理,类似地分配给最高优先级的电梯

    即*电梯间调度*有以下原则:
    
    1. 优先级原则:优选选择距离最近/任务量最小的的电梯。

    2. 携带原则:上升请求可以挑选空闲电梯和上升电梯,下降请求可以挑选空闲电梯和下降电梯;上升电梯不能响应比电梯当前楼层低的上升请求,下降电梯不能响应比电梯当前楼层高的下降请求。

    3. 超时原则:不能处理的请求将被加入等待队列,定时检查和清理。一旦超时,强行分配给最高优先级的电梯。

      此外,还需要向Task Container告知最大载客量以避免电梯出现满载状况。


      1.2.2 功能和性能设计平衡

      在第一次作业的基础上,笔者将Scheduler类单独开设一个线程,用于注册电梯和分配请求。并且设置等待队列服务于电梯间的调度策略。除此以外,更改了电梯里的满载条件,避免了人员溢出现象。

      显然,第一次作业相对较好的设计提供了后续偷懒的便利。相较于第一次作业,这次各个类的功能几乎没有变化,基本架构设计没有变化,只是实现了电梯间调度策略。此外,调度优先级的设置兼顾电梯间的分配效率和性能,从而电梯陷入极端承载情况的概率极低。

      但成也调度器,败也调度器。实际上,一个总等待队列并不是一个很好的设计,更利于扩展功能的应该是每个电梯都有一个等待队列。此外,调度器没有较好地确定优先级的权重系数(如等待时限的设置等等),也是调度性能的一大瓶颈。电梯的缺陷则在于直接增加满载量限制,而不是将功能交给控制任务进出的Task Container,这一点在后续得到了更改。


    1.3 分功能多电梯调度

    1.3.1 需求分析 & 设计策略


    第三次作业模拟分功能多电梯,多部电梯拥有不同的个性化设置,例如可停靠楼层,运行时间,最大载客量等等。此外,在请求中还加入了订阅电梯的电梯请求。
    
    由于此次任务需要对乘客请求进行拆分,而乘客请求的“激活状态”需要实时更新。此次模型应用**观察者模式(Observer Pattern**)。调度器的实现采用观察者模式,订阅者即多部电梯,通过调度器的调度分配将新的请求加入到**电梯对应的请求队列**之中。调度器既负责**输入请求的调度分配**又能**实时地订阅新的电梯**。
    

      即Scheduler先从输入线程得到请求,然后调度器将其分发到电梯,如果需要换乘的话,将第二部分的请求加入到等待队列中,等待二次分配。

    ​ 电梯内部调度基本不变,外部调度采用“随机+优先级”的调度策略。新需求衍生出的不可直达请求需要拆分,在这里,我采用了非常简单的调度方式:

    • 可直达请求:乘客等待可直达电梯即可。(电梯随机选择)。
    • 不可直达请求:拆成两个可直达请求,通过电梯的可停靠楼层静态分配请求的换乘楼层。

    1.2.2 功能和性能设计平衡


    新增的类&变化的类:
    
    • 调度器(Scheduler):对输入线程传递的请求实时调度,将请求传给调度的电梯内。实时订阅新的电梯。
    • 请求(MyRequest):与输入请求类似,用于拆分请求。
    • 优先级(PriorityIndex):用于处理调度器的优先级。
    • 任务队列(TaskQueue):请求队列,用于盛放未激活的输入请求
    • 任务(Task):电梯内部可以处理的请求的基本单位,有人员ID,目标楼层,进出选项,是否激活和附带任务等等属性。
    • 任务分配器(Task Container):一个盛放着一部电梯的所有当前任务的容器,负责电梯内部调度的逻辑。
    • 单部电梯(Elevator):负责电梯的正常运行,仅仅执行移动、开门、关门、装货和卸货等等功能;内部配置响应的Task Container和Task Queue。
    • 状态(State):电梯状态,有上升(up)、下降(down)和空闲(still)三种运行方向。

    为了避免轮询导致的CPU时间超时,每一部电梯配置了相应的请求队列。其他类的功能设计几乎没有太大变化,仅仅更改了Scheduler的拆分和调度算法和电梯内部运行逻辑,即每次循环除了检查**任务分配器**之外,还要检查**任务队列**。
    
    这样的设计总体没有太大问题,但是性能却非常差劲。经过反思总结,以下可能是造成我在强测中正确性得到保证、但是性能分几乎为0的原因:
    
    1. 调度器内缺少一个总请求队列,这是功能设计上的重大缺陷。拆分请求后立即分别加入两部可达电梯的请求队列中,没有很好地利用后来到达的电梯请求。
    2. 面对不可直达请求,由于时间精力有限,笔者采用了傻瓜的静态拆分。这样的调度算法显然不科学,可以考虑根据不同电梯的运行速度、任务量、当前楼层选择更合适的电梯。
    3. 电梯是否应该提前等待?当一个电梯在执行第一段请求的时,第二个电梯是否可以提前运行到中转楼层进行等待?我想未必,或者这也是性能更差的原因。

    2.度量分析

    2.1 类图分析


    第一次作业:

      第一次作业结构较为清晰,使用生产者-单消费者模式和单例模式。一共有2个线程,即主线程创建电梯线程和Scheduler对象。


      第二次作业与第一次作业的类图一模一样,唯一区别是Scheduler变成独立的线程。这依然是一个生产者-消费者模式的例子。


    第三次作业:

    ​ 第三次作业采用观察者模式。在前一次作业的基础上,程序增加了TaskQueue,用于服务未激活的任务。缺陷在于Request和PriorityIndex的作用较为单一,并且前者实际上可以用一个方法代替。

      可以看到,第一次任务的设计基本决定了之后的架构,三次作业没有太大改变。可以看到,对于合理的设计,每次作业的工作量并不大,仅仅是修修补补。


    2.2 规模分析

    第一次作业的度量分析


    ElementaryTest.main(String[]) 3.0 2.0 3.0
    Elevator.addRequest(PersonRequest) 1.0 1.0 1.0
    Elevator.closeDoor() 1.0 2.0 2.0
    Elevator.Elevator(long,long,long) 1.0 1.0 1.0
    Elevator.finishTasks(Vector) 1.0 3.0 3.0
    Elevator.moveDownFloor() 1.0 1.0 1.0
    Elevator.moveUpFloor() 1.0 1.0 1.0
    Elevator.openDoor() 1.0 1.0 1.0
    Elevator.printFloor(String) 1.0 1.0 1.0
    Elevator.run() 3.0 6.0 7.0
    RequestParser.addElevator(Elevator) 1.0 1.0 1.0
    RequestParser.parseRequest(PersonRequest) 1.0 3.0 3.0
    RequestParser.RequestParser() 1.0 1.0 1.0
    Task.getDstFloor() 1.0 1.0 1.0
    Task.getId() 1.0 1.0 1.0
    Task.getRelevantTask() 1.0 1.0 1.0
    Task.isGetIn() 1.0 1.0 1.0
    Task.isMarching() 1.0 1.0 1.0
    Task.setMarching() 1.0 1.0 1.0
    Task.Task(int,int,boolean,Task) 1.0 1.0 1.0
    TaskContainer.addTask(PersonRequest) 1.0 3.0 3.0
    TaskContainer.getCurrentTasks(int) 1.0 6.0 6.0
    TaskContainer.getDirection(int,State) 5.0 9.0 9.0
    TaskContainer.isEnd() 1.0 1.0 1.0
    TaskContainer.TaskContainer() 1.0 1.0 1.0
    Total 33.0 51.0 53.0
    Average 1.32 2.04 2.12

    Task Container的`getDirection`方法ev(G)过高的原因是因为这里要根据电梯方向的状态机判断下一个运行方向,嵌套了5层if else判断,问题不大。耦合度高是因为该方法在Elevator类的run方法中被调用频率过高。

    第二次作业的度量分析:

    ElementaryTest.init(int) 1.0 2.0 2.0
    ElementaryTest.main(String[]) 3.0 2.0 3.0
    Elevator.addRequest(PersonRequest) 1.0 2.0 3.0
    Elevator.closeDoor() 1.0 2.0 2.0
    Elevator.Elevator(String,long,long,long,int,AtomicBoolean) 1.0 1.0 1.0
    Elevator.finishTasks(Vector) 3.0 5.0 5.0
    Elevator.getCurFloor() 1.0 1.0 1.0
    Elevator.getCurState() 1.0 1.0 1.0
    Elevator.getPriorityValue(int) 1.0 1.0 3.0
    Elevator.isFull() 1.0 1.0 1.0
    Elevator.moveDownFloor() 1.0 1.0 2.0
    Elevator.moveUpFloor() 1.0 1.0 2.0
    Elevator.openDoor() 1.0 1.0 1.0
    Elevator.printFloor(String) 1.0 1.0 1.0
    Elevator.run() 3.0 6.0 7.0
    Scheduler.addElevator(Elevator) 1.0 1.0 1.0
    Scheduler.addRequest(PersonRequest) 1.0 1.0 1.0
    Scheduler.close() 1.0 1.0 1.0
    Scheduler.getBestElevator(PersonRequest) 3.0 12.0 12.0
    Scheduler.getInstance(AtomicBoolean) 1.0 1.0 3.0
    Scheduler.parseBufferRequest() 1.0 6.0 6.0
    Scheduler.parseRequest(PersonRequest) 2.0 1.0 2.0
    Scheduler.putRequest(int,PersonRequest) 1.0 3.0 3.0
    Scheduler.run() 1.0 5.0 7.0
    Scheduler.Scheduler(AtomicBoolean) 1.0 1.0 1.0
    Task.getDstFloor() 1.0 1.0 1.0
    Task.getId() 1.0 1.0 1.0
    Task.getRelevantTask() 1.0 1.0 1.0
    Task.isGetIn() 1.0 1.0 1.0
    Task.isMarching() 1.0 1.0 1.0
    Task.setMarching() 1.0 1.0 1.0
    Task.setUnMarching() 1.0 1.0 1.0
    Task.Task(int,int,boolean,Task) 1.0 1.0 1.0
    TaskContainer.addTask(PersonRequest) 1.0 3.0 3.0
    TaskContainer.addWaitingTask(Task) 1.0 2.0 2.0
    TaskContainer.getCurrentTasks(int) 1.0 6.0 6.0
    TaskContainer.getDirection(int,State) 5.0 13.0 13.0
    TaskContainer.isEnd() 1.0 1.0 1.0
    TaskContainer.size() 1.0 1.0 1.0
    TaskContainer.TaskContainer() 1.0 1.0 1.0
    Total 53.0 95.0 107.0
    Average 1.325 2.375 2.675

    Task Container的`getDirection`方法原因如第一次作业。此外,Scheduler的`getBestElevator`方法耦合度过高是因为每次循环需要三次检查请求队列,所以被调用次数过多;圈复杂度过高是为了实现优化,循环遍历所有电梯,根据电梯间调度逐一选择,这个方法写得十分冗杂且不美观,不宜阅读,可以考虑新建一个类专门盛放调度优化得逻辑。

    第三次作业

    ElementaryTest.init() 1.0 6.0 6.0
    ElementaryTest.main(String[]) 3.0 4.0 5.0
    Elevator.addRequest(PersonRequest) 1.0 2.0 3.0
    Elevator.addTask(Task) 3.0 2.0 4.0
    Elevator.closeDoor() 1.0 3.0 3.0
    Elevator.Elevator(String,long,long,long,int,TreeSet,AtomicBoolean) 1.0 1.0 1.0
    Elevator.finishTasks(ArrayList) 1.0 4.0 4.0
    Elevator.getCurFloor() 1.0 1.0 1.0
    Elevator.getCurState() 1.0 1.0 1.0
    Elevator.getName() 1.0 1.0 1.0
    Elevator.getStopFloors() 1.0 1.0 1.0
    Elevator.getTaskNum() 1.0 1.0 1.0
    Elevator.getTaskQueue() 1.0 1.0 1.0
    Elevator.moveDownFloor() 1.0 1.0 2.0
    Elevator.moveFloor(int) 1.0 3.0 3.0
    Elevator.moveUpFloor() 1.0 1.0 2.0
    Elevator.openDoor() 1.0 1.0 1.0
    Elevator.printFloor(String) 1.0 1.0 1.0
    Elevator.run() 7.0 13.0 16.0
    PriorityIndex.compareTo(PriorityIndex) 1.0 1.0 1.0
    PriorityIndex.getEleNum() 1.0 1.0 1.0
    PriorityIndex.getreqNum() 1.0 1.0 1.0
    PriorityIndex.PriorityIndex(int,int) 1.0 1.0 1.0
    Request.getFrom() 1.0 1.0 1.0
    Request.getInTask() 1.0 1.0 1.0
    Request.getOutTask() 1.0 1.0 1.0
    Request.getRelevantRequest() 1.0 1.0 1.0
    Request.getTo() 1.0 1.0 1.0
    Request.isActive() 1.0 1.0 1.0
    Request.Request(int,int,int,Request,boolean,boolean) 1.0 2.0 3.0
    Request.setActive(boolean) 1.0 1.0 1.0
    Scheduler.addElevator(Elevator) 1.0 1.0 1.0
    Scheduler.addRequest(PersonRequest) 1.0 1.0 1.0
    Scheduler.addTask(int,int,int,PersonRequest) 1.0 1.0 1.0
    Scheduler.close() 1.0 1.0 1.0
    Scheduler.divRequest(PersonRequest,ArrayList) 7.0 16.0 19.0
    Scheduler.getBestElevator(PersonRequest) 3.0 15.0 15.0
    Scheduler.getInstance(AtomicBoolean) 1.0 1.0 3.0
    Scheduler.parseBufferRequest() 3.0 6.0 6.0
    Scheduler.parseRequest(PersonRequest) 3.0 3.0 4.0
    Scheduler.putRequest(int,PersonRequest) 1.0 1.0 1.0
    Scheduler.putTask(int,ArrayList) 1.0 1.0 1.0
    Scheduler.putTask(int,Task) 1.0 1.0 1.0
    Scheduler.run() 1.0 8.0 8.0
    Scheduler.Scheduler(AtomicBoolean) 1.0 1.0 1.0
    Scheduler.startThread(int) 1.0 3.0 3.0
    Scheduler.toSplit(PersonRequest,ArrayList) 3.0 3.0 4.0
    Task.equals(Object) 1.0 1.0 4.0
    Task.getDstFloor() 1.0 1.0 1.0
    Task.getId() 1.0 1.0 1.0
    Task.getRelevantTask() 1.0 1.0 1.0
    Task.hashCode() 1.0 1.0 1.0
    Task.isGetIn() 1.0 1.0 1.0
    Task.isMarching() 1.0 1.0 1.0
    Task.setMarching() 1.0 1.0 1.0
    Task.setRelevantActive() 1.0 2.0 2.0
    Task.setUnMarching() 1.0 1.0 1.0
    Task.Task(int,int,boolean,boolean,Task) 1.0 1.0 1.0
    TaskContainer.addTask(PersonRequest) 1.0 3.0 3.0
    TaskContainer.addWaitingTask(Task) 1.0 2.0 2.0
    TaskContainer.getCurrentTasks(int,int,int) 1.0 9.0 10.0
    TaskContainer.getDirection(int,State) 9.0 15.0 17.0
    TaskContainer.hasActiveTask(int) 4.0 3.0 4.0
    TaskContainer.hasOnTask(int) 4.0 3.0 4.0
    TaskContainer.isEnd() 1.0 1.0 1.0
    TaskContainer.releaseTask() 1.0 1.0 1.0
    TaskContainer.size() 1.0 1.0 1.0
    TaskContainer.TaskContainer() 1.0 1.0 1.0
    TaskQueue.getReadyTasks() 1.0 5.0 5.0
    TaskQueue.getTask(int) 4.0 3.0 6.0
    TaskQueue.getTaskNum() 1.0 1.0 1.0
    TaskQueue.isEnd() 1.0 2.0 2.0
    TaskQueue.kill() 1.0 1.0 1.0
    TaskQueue.putTask(ArrayList) 1.0 2.0 2.0
    Total 115.0 189.0 217.0
    Average 1.554054054054054 2.554054054054054 2.9324324324324325

    Task Container的`getDirection`方法,Scheluder的`getBestElevator`原因如第二次作业。Elevator中`run`方法是因为要同时检查TaskQueue和TaskContainer,并且实现第三次作业性能(详情见1.3.2)的第三条原则,导致if-else分支过多,并且内部控制分支过多。Scheduler的`divRequest`由于要进行潜在的三次拆分(中间,上下),if-else分支多,控制分支多;并且要被`run`方法多次调用,所以圈耦合度高。Elevator里的`getCurrentTasks`方法圈耦合度过高是因为要进行满载与否的判断,而耦合度过高是因为在`run`中被调用过多。
    

    2.4 UML协作图分析

      使用starUML进行绘制。


    第一次作业

    第一次作业直接传递请求。task container是elevator内部的属性。


    第二次作业

    与第一次作业类似,唯一把scheduler类改为线程类。


    第三次作业

    第三次作业中,增加了taskqueue类。从scheduler类获取请求,然后交付给elevator。


    2.4 设计原则分析

     SRP(单一责任原则)、OCP(开放封闭原则)、LSP(里氏替换原则)、ISP(接口分离原则)和DIP(依赖倒置原则)。

      SRP:单一责任。笔者认为总体都实现了,但第二次的电梯满载量的检查方法不应该放在电梯类,第三次进行了修改。

      OCP:所有的扩展应该是基于现有代码的基础上进行增加新的方法,而不是直接修改某个方法内部的逻辑。三次作业基本没有大的架构修改。

      LSP:子类应该包括父类的所有属性。不涉及父子类的问题。

      ISP:核心是一个个接口不应该和太多功能相关。不涉及接口问题。

      DIP:要求模块之间尽可能依赖于抽象,而不是模块之间的依赖,抽象不能依赖于细节。从第二次作业开始,这个原则并没有很好地实现,因为电梯的实时运行状态需要依赖于调度器,而调度器又依赖于电梯。


    3.错误分析


    第一次作业:互测与强测均无错误。

    第二次作业:强测错了一个点。经检查,发现是Scheduler里时限设置的太高,导致请求迟迟未被处理。此外,在写第三次作业时,发现了一个没检测出来的bug,即在输出”someone has come out“之前就已经将任务的下一连带任务设置为”激活“。

    第三次作业:互测一个点RTLE。经检查,是电梯出现了震荡,没有及时停止。增加了end标志位,及时停止。


    4.发现别人bug的策略

     与上一单元一样,仍然是手搓自动测试。根据模拟给的输入输出jar包接口,用线程的休眠实现定时输入,借助maven插件制作新输入输出jar包。利用大批量的自动化测试,随机生成请求输入,根据数据合法性规则,检查输出的操作是否合法。

    ​ 此外,吸取上一单元的教训,还针对性地设计输入数据,尤其是检查满载问题。


    5.心得体会

    好架构省事省力。

  • 相关阅读:
    Spring ContextLoaderListener
    判断整数是否是对称数
    jstl缺包时的报错
    Spring初始化日志
    C# 网络编程之最简单浏览器实现
    Java实现 蓝桥杯 算法训练 Anagrams问题
    Java实现 蓝桥杯 算法训练 出现次数最多的整数
    Java实现 蓝桥杯 算法训练 出现次数最多的整数
    Java实现 蓝桥杯 算法训练 字串统计
    Java实现 蓝桥杯 算法训练 字串统计
  • 原文地址:https://www.cnblogs.com/daytripper/p/12706950.html
Copyright © 2011-2022 走看看