OO 第三单元总结

OO 第三单元总结

一、实现规格采取的设计策略

1、仔细阅读JML内容

本单元主要的考察点在于阅读理解提供的JML并根据自己的理解完成程序代码。JML语言表述的方法实现往往十分冗长。大量的requires字句和ensures字句使得一个本不复杂的方法看起来很复杂,大量的()嵌套使得表达式的层次难以理顺,所以需要我们耐心地一步步分析方法的规格。

2、根据需求选择恰当的容器

虽然在JML中将很多变量规格声明为int[],Person[]等数组的形式,但是在我们的实际操作中还要根据需求选择合适的容器,如ArrayList,HashMap等。(并且根据本人的实际经历这些最后十有八九都要被优化为HashMap…)

3、注意关注性能问题

官方提供的规格虽然完善,但是不会考虑具体实现时是否会出现性能的问题。所以一味地去“翻译”JML表达式而不根据自己的理解进行优化,很容易出现TLE的bug。(比如我…)例如Group中的getAgeMean()和getAgeVar():

    /*@ ensures \result == (people.length == 0? 0 : 
      @          ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
      @*/
    public /*@pure@*/ int getAgeMean();
​
    /*@ ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length; 
      @          (people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) / 
      @           people.length));
      @*/
    public /*@pure@*/ int getAgeVar();

如果真的在getAgeMean() 中遍历people得到result,那么调用getAgeVar()时就会出现一个二重循环,使得消耗的时间大大增加。

4、利用方法名帮助理解

因为JML的表达式总是表述的很复杂,这时候根据方法的名字就可以很快理解该方法的作用。如方法名叫做addPerson(),那么该方法往往是简单的向某一数组中加入一个person。

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

1、正确性测试

测试一些需要抛出异常的状况有没有正确抛出异常。重点测几个实现比较复杂的函数,看看是否结果正确。(然而强测还是很凄惨,因为自己造的数据不够强,又没有评测机)

2、性能测试

测试诸如qbs,qgam,qgav,sim等需要进行遍历或递归等算法的指令,容易出现CLE的问题。主要是分析是否能够优化自己的算法。

三、分析容器的选择和使用的经验

对于数组的实现我基本都是使用的HashMap(一开始我还试图用ArrayList,但最后都改成了HashMap),偶尔使用ArrayList也是为了它有序的特性。因为HashMap在查找元素时只需要调用get()既可,而放在ArrayList中就需要遍历整个list。除此之外,HashMap键值对的存储方式也使得数据的存储和调用方便了许多。

四、针对本单元容易出现的性能问题,总结分析原因如果自己作业没有出现,分析自己的设计为何可以避免

1、第一次作业

第一次作业主要是qbs的问题。

我对于isCirle()的实现有很大问题。当时我只是想到用递归的方法查找,就想不出什么更好的方法了。后来bug修复的时候知道了并查集的方法,顿时惊为天人。让我意识到解决性能问题的一大方法就是在每次ap,ar等操作时把当前的状态进行保存,在需要的时候通过一次遍历甚至一次调用就能解决所有问题。

除此之外,我在实现qbs时,只是向JML表达式里面写的那样简单粗暴的循环调用isCircle(),却没有考虑过其中的性能问题。但实际上我们增加一个成员变量qbs,在每一次ap,ar时都可以根据判断实时增减qbs的值,需要时直接返回既可。这样就大大减少了性能开销。

2、第二次作业

一是getAgeMean()和getAgeVar()的问题。当时写的时候我没有注意到getAgeVar()中每一次循环都要调用getAgeMean(),如果getAgeMean()中再循环计算就是一个二重循环了。然后就TLE了。

对此我的解决方法是增加一个成员变量ageSum,初始值为0,每次增加或删除person时修改ageSum的值,在调用getAgeMean()时只需返回ageSum除以people.size()的值既可。这样getAgeVar()中只有一重循环了。

二是qgvs的问题。我完成作业时是考虑到了如果遍历可能会超时的问题。然后我又想到第一次作业的解决方法,就试图每次记录下当前的getValueSum的值。然而我又忘记了person的value值是可以改变的,而且people中的person还可能被删除,结果就导致强测大片大片的WA。

3、第三次作业

主要是针对sim的问题。以下是我根据迪杰斯特拉算法的一部分代码实现:

            for (Person head : mark.keySet()) {
                HashMap<Person,Integer> list = wait.get(head.getId());
                int value = mark.get(head);
                for (Person p : list.keySet()) {
                    int listValue = list.get(p);
                    if (min > value + listValue) {
                        min = value + listValue;
                        nextPerson = p;
                    }
                }
            }
            mark.put(nextPerson,min);
            if (person2.equals(nextPerson)) {
                break;
            }

然而最后还是有两个点TLE了。我个人觉得如果在初始存放路径表时能够按照大小顺序排序的话,上述代码可以简化为一重循环,每个点可以直接取出相连点中路径最小的点。(然而我还没试过)

五、梳理自己的作业架构设计,特别是图模型构建与维护策略

整体架构设计都是按照JML规格完成的。

对于每个类:

1、MyPerson

    private int id;
    private String name;
    private int age;
    private HashMap<Person,Integer> acquaintance;
    private int socialValue;
    private ArrayList<Message> messages;
    private int money = 0;

其中acquaintance中的key值是有relation的人,value值是熟人对应的value值。

2、MyNetwork

    private HashMap<Integer,Person> people;
    private HashMap<Integer,Integer> relation = new HashMap<>();
    private HashMap<Integer,HashMap<Person,Integer>> searchRelation = new HashMap<>();
    private HashMap<Integer,Group> groups = new HashMap<>();
    private HashMap<Integer,Message> messages = new HashMap<>();
    private int qbs = 0;
    private HashMap<Integer,Integer> emojiIdList = new HashMap<>();

relation中key值代表一个person,value值代表person的父节点。addperson时同时在relation中添加一个value值为自身的person的id。addRelation时判断是否需要更改父节点:

            if (relation.get(id1).equals(relation.get(id2))) {
                return;
            }
            int father1 = find(id1,id1);
            int father2 = find(id2,id2);
            if (father1 != father2) {
                relation.put(father1,father2);
                qbs--;
            }

使用时通过调用find()判断是否拥有同一根节点,同时简化路径。以下为find()的实现:

    public int find(int personId, int id) {
        if (relation.get(id).equals(id)) {
            return id;
        } else {
            relation.put(personId,find(id,relation.get(id)));
            return relation.get(personId);
        }
    }

searchRelation中存放每一个person对应的acquaintance。用于辅助计算最短加权路径。迪杰斯特拉算法的实现在上一部分的第三次作业中已经提过,此处不再赘述。

qbs用于保存当前状态下的qbs值。初始为0。每次addPerson时加一,每次将父节点为自身的点(即根节点)设置为别的节点时,qbs减一。

3、MyGroup

    private int id;
    private HashMap<Person, Integer> people = new HashMap<>();
    private int ageMean = 0;

people保存了每个person和对应的age值。

ageMean保存当前状态下people的age之和。

4、MyMessage

    private int id;
    private int socialValue;
    private int type;
    private Person person1;
    private Person person2;
    private Group group;

都是JML中要求的规格。

5、各种异常

    private static int times = 0;
    private static HashMap<Integer, Integer> map = new HashMap<>();
    private int id1;
    private int id2;

times记录每个异常的触发次数。

map记录每个id抛出该异常的次数,不存在表示抛出次数为0。

id1,id2记录本次抛出异常的id。可能只有一个属性id。

posted @ 2021-05-31 12:57  anglerfish00  阅读(39)  评论(0)    收藏  举报