BUAA_OO_第三单元作业总结

第三单元作业总结

BUAA OO 2021面向对象作业

19373469 陈纪源

一、分析实现规格的设计策略

(一)第一次作业

​ 在第一次作业中,由于刚开始遇到大量的JML规格约束,我选择的是先通读整个JML代码,了解每个函数、每个接口、每个异常类都在完成什么功能。在通读整个代码后发现整个社交网络的问题可以抽象成一个加边、加点、判断联通性的问题,对于不同的图操作对应使用不同的数据结构去维护它们。

​ 下面举一个JML规格的例子,函数isCircle()如下:

    /*@ public normal_behavior
      @ requires contains(id1) && contains(id2);
      @ ensures \result == (\exists Person[] array; array.length >= 2; 
      @                     array[0].equals(getPerson(id1)) && 
      @                     array[array.length - 1].equals(getPerson(id2)) &&
      @                      (\forall int i; 0 <= i && i < array.length - 1; 
      @                      array[i].isLinked(array[i + 1]) == true));
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id1);
      @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2);
      @*/
    public /*@pure@*/ boolean isCircle(int id1, int id2) throws PersonIdNotFoundException;

​ 如果我们直接按照规格所说,寻找那个Person[]数组,我们可能就需要写dfs,这样的复杂度是O(n)的且有着爆栈的风险(在后面迭代的时候)。

​ 在这里,我们在设计的时候可以想一想这个函数的本质,发现它其实相当于判断id1和id2的联通性,而我们的添加relation的操作是只有加边的没有删边的,因此我们在这里可以考虑使用并查集来维护这个联通性。

​ 在每个Person类中,加入如下的内容:

    private MyPerson root;
    private int size;

​ 分别表示这个并查集的根结点和所在并查集的大小(只有这个并查集的根结点的大小代表这个联通区域的大小)。

​ 下面的函数是寻找一个Person的根结点:

    public MyPerson getRoot() {
        MyPerson tem = this;
        while (!tem.root.equals(tem)) {
            tem = tem.root;
        }
        return tem;
    }

​ 有了并查集,这样在完成isCircle()函数就可以使用快速的并查集了,时间复杂度O(log n)

    public boolean isCircle(int id1, int id2) throws
            PersonIdNotFoundException {
        if (!contains(id1)) {
            throw new MyPersonIdNotFoundException(id1);
        } else if (!contains(id2)) {
            throw new MyPersonIdNotFoundException(id2);
        } else {
            MyPerson p1 = (MyPerson) getPerson(id1);
            MyPerson p2 = (MyPerson) getPerson(id2);
            MyPerson r1 = p1.getRoot();
            MyPerson r2 = p2.getRoot();
            return r1.equals(r2);
        }
    }

​ 总结一下,在实现规格时,要从规格的本质(即功能)出发,去设计相应的数据结构和算法去完成这个函数,切忌不可以完全照抄规格。

(二)第二次作业

​ 第二次作业在第一次作业的基础上,数据范围有所扩大,增加了Group和Message类。

​ 在迭代开发的时候,对于之前的JML规格,要十分小心,在设计时要保证之前的JML规格约束不受破坏,同时对于新的JML规格要通读了解和第一次作业之间的关系。

​ 在设计时要善于使用容器来存储一些数据。

​ 异常类由于数量较多,里面的计数功能我使用了一个计数器类存储异常次数。

public class Calculator {
    private TreeMap<Integer, Integer> cal;
    private int count;

​ 这样对于一种异常类,如下记录:

public class MyEqualMessageIdException extends EqualMessageIdException {
    private static Calculator calculator = new Calculator();
    private int id;

​ 通过一个静态类计数器来记录异常次数。

​ 在设计时要抽象公共的功能,来使用一个特定的类去管理,类与类之间解耦度要高。

(三)第三次作业

​ 第三次作业在第二次作业的基础上加入了不同类型的消息,在这次作业我采用了继承方式,对于消息类进行统一管理。

​ 这样在写添加消息的时候就可以不使用那么多的instance of了。

​ 同时最后一个sendIndirectMessage()函数需要把规格中对应的问题抽象出来,发现是一个单元最短路,可以使用dijkstra算法解决,进而避免过大的复杂度。

二、基于JML规格的测试的方法和策略

​ 基于JML规格进行测试,主要有如下的方法和策略:

  1. 构造repOK函数方法,验证JML的前置条件和后置条件是否满足。
  2. 使用一些开源软件(TestNG)方法来进行测试。
  3. 通过理解JML规格,构造边界条件的数据(包括数据边界和时间边界)。
  4. 使用对拍的方式进行测试,和其余同学进行测试(这样就能看大家对于JML的理解是不是一样的了)。

三、容器选择和使用

​ 本次作业中,我主要使用了TreeMap、HashMap、ArrayList还有静态数组四种容器,下面我来依次介绍这四种容器。

1. TreeMap

​ TreeMap是一种红黑树。

​ TreeMap支持插入某一元素、删除某一元素、整个key值的遍历。

​ TreeMap在使用时需要重载比较函数。

​ 它的基本操作都是O(log n)的,遍历是O(nlog n)的。

​ 一般如果需要快速查找某一键值对应的value,包括数据的动态插入删除,是可以使用TreeMap的,但是不建议在大规模需要遍历整个容器时使用TreeMap(毕竟复杂度高)

​ 本次作业中我在计数器类、people类(记录id到人的映射)、消息类(记录id到消息的映射)、表情类使用了TreeMap。

​ 基本上都是用来管理数据。

2. HashMap

​ HashMap是哈希表。

​ HashMap支持插入某一元素、删除某一元素、整个key值的遍历。

​ HashMap在使用时需要重载equal和hashcode函数。

​ 它的基本操作是期望O(n/m),遍历是O(n)的。(m是映射范围)

​ 和TreeMap一样,动态维护数据的插入和删除是可以使用HashMap的。

​ 可能时由于对于HashMap的不信任(或许是期望的原因),我只在维护边集里面使用了HashMap。

​ 其实从比较小的数据量来说,HashMap是会比TreeMap要更好的

3. ArrayList

​ ArrayList是动态数组。

​ ArrayList支持插入元素、删除元素、遍历整个数组。

​ 不像HashMap和TreeMap,它的复杂度都是O(n)的。

​ 但是它的常数会比TreeMap和HashMap要小(从Cache原理出发)

​ 在本次作业中,我对于groups和emojiMessages这两种相对来说比较小的数据规模使用了ArrayList来管理。

4. 静态数组

​ 静态数组相比于ArrayList,唯一的区别在于它的大小空间是开好的,因此常数会比ArrayList还要小。

​ 在本次作业中,我创建的边类中,我给点与点之间创建了邻接矩阵,使用了静态数组。

四、性能问题分析

​ 本单元最大的难点在于性能问题,但是我的代码并没有出现bug,下面我将来分析一下容易出现性能问题的函数。

(一)第一次作业

​ 在第一次作业中,容易出现性能问题的函数就是第一部分所介绍的isCircle函数,这一部分可以使用并查集来解决。

(二)第二次作业

​ 在第二次作业中,比较容易出现问题的是Group类中的getValueSum函数,这个函数在强测数据范围下如果每次都使用O(n^2)的算法是会TLE的。

​ 因此在这里我们需要使用查询O(1)、修改O(n)的算法去完成这个函数。

​ 首先我们先看这个value的JML:

    /*@ ensures \result == (\sum int i; 0 <= i && i < people.length; 
      @          (\sum int j; 0 <= j && j < people.length && 
      @           people[i].isLinked(people[j]); people[i].queryValue(people[j])));
      @*/
    public /*@pure@*/ int getValueSum();

​ 我们考虑一下这个value在什么时候会发生变化。

​ 当Group中的人没有发生改变时,这个value是不会发生改变的。

​ 当一个人从一个Group中移出/加入时,和TA相连的人的value是需要加进来了的,因此有以下代码:

    public void addPerson(Person person) {
        for (int i = 0; i < personArrayList.size(); i++) {
            valueSum = valueSum + 2 *
                    Relation.queryValue(((MyPerson) personArrayList.get(i)).getMapId(), currentId);
        }
    }
    public void delPerson(Person person) {
        for (int i = 0; i < personArrayList.size(); i++) {
            if (personArrayList.get(i).getId() == person.getId()) {
                personArrayList.remove(i);
                i--;
            } else {
                valueSum = valueSum -
                        2 * Relation.queryValue(((MyPerson)
                                personArrayList.get(i)).getMapId(), currentId);
            }
        }
    }

​ 最重要的是,当一条新的关系加入的时候,需要遍历所有的Group,去找到包含这条两个人的Group,加上新的权值。

    public void addRelation(int id1, int id2, int value) {
            for (Group group : groups) {
                MyGroup myGroup = (MyGroup) group;
                if (myGroup.contain(p1) && myGroup.contain(p2)) {
                    myGroup.addValue(2 * Relation.queryValue(p1.getMapId(), p2.getMapId()));
                }
            }
    }

(三)第三次作业

​ 第三次作业中最容易出现性能问题的就是sendIndirectMessage函数,这个函数本质上要求两个结点之间的最短路。

​ 这需要用dijkstra算法来解决这个问题。

​ 传统的O(n^2)的dijkstra是不行的(数据范围10000),我本地也出了能卡暴力dijkstra到50s的数据。

​ 需要使用堆优化的dijkstra去解决这个问题(使用pair或者新的类),时间复杂度O(n log n)。

​ 为了不大量使用Map,我将每一个人的id离散化到了[1, 5000]的范围内,这样就可以数组存这些值了。

​ 一个小优化,当我们的dijkstra找到终点之后直接break(而不是跑完),这样的剪枝可以快很多。

五、作业架构梳理

​ 主要依据第三次作业进行梳理。

(一)架构类图

(二)各类介绍

有关图的维护和构建,主要是由MyPerson,MyPerson,MyNetwork,MyMessage类管理的。

MyPerson

​ 存储网络中人的类,对应于图中的节点。

​ 里面包括人的各种信息(这里面的MapId是我把所有人的id映射到了[1,5000]的映射之后的id,这样方便我使用数组来跑一些算法,减小常数)

​ 还包括一些并查集的信息(包括该节点的father和该节点所组成的并查集树下方的大小)。

MyGroup

​ 存储网络中分的组的类,对应于图中的一些点组成的集合。

​ 里面包括使用该组的Value和和年龄和等。

​ 当Person类元素从Group类元素加入或者删除时,需要动态修改Value、年龄和、年龄平方和等。

​ 当然如果NetWork类中新加入一个Relation,也需要向包含这组Relation中两个人的Group中修改Value。

MyNetwork

​ 存储整个网络的类,对应于图中点和边的集合。

​ 存储包含了人的类、组的类、消息的类、表情消息的类的数据结构。

​ 同时还包含了跑dijkstra的dis数组和是否访问过的flag数组,维护删除冷表情的del数组和时间戳timeStack。

​ 里面实现的方法都是调用Person、Group类的方法,包括按照JML规格的其他逻辑,同时抛出对应的10个异常。

​ 从面向对象角度来说,我的这次MyNetwork类实际上是非常不面向对象的,因为它维护了很多本该不需要它来维护的方法(比如dijkstra、时间戳法等),最好还是去写一些额外的类来统一处理这些其他的算法。

​ 因此我的这次MyNetwork类非常长(500行),之后构建时需要再仔细思考。

Relation

​ 存储人与人之间的关系类,对应于图的邻接矩阵。

​ 使用邻接矩阵的原因是可以减小在dijkstra中的常数。

MapPerson

​ 做高id到[1,5000]映射的类。

​ 使用map来管理。

MyMessage

​ 管理传递的消息类,类似于图中边的各种属性。

​ 存储了消息的所有属性。

MyNoticeMessage

​ 继承了MyMessage类,增加了字符串的属性。

MyRedEnvelopeMessage

​ 继承了MyMessage类,增加了钱的属性。

MyEmojiMessage

​ 继承了MyMessage类,增加了表情的编号的属性。

Calculator

​ 计数器类,用来存储异常出现的次数。

My*Exception

​ 10个异常类,用来表示不同的异常。

Main

​ 整个工程的入口。

六、心得体会与反思

  • 通过第三次作业的磨练,对JML相关内容有了进一步的了解。
  • 更加认识到一个良好的架构,对于写代码的好处。
  • 对于社交网络模型有了更加深入的理解,大体对这个模型有了一个概念。
  • 对于规格化编程、契约化编程有了一个更加清醒的认识,意识到这种编程的重要性。
  • 明白了JML规格不是让我们照抄,而是要根据功能设计架构和使用方法。
posted @ 2021-05-30 16:31  生物圈的自传  阅读(76)  评论(1)    收藏  举报