OOUnitThreeSummary

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

本单元的主要内容为JML规格的实现,在我们的官方包中给了我们需要实现类及函数的JML需求,通过本单元的学习,我对于JML语言格式逐渐熟悉,可以通过JML的语言格式实现自己的类、函数以及异常处理方法。而且本单元对于社交关系的模拟以及对于时间的较高要求也让我对图的学习和理解更加深刻,并学到了诸如Dijistra等算法。

在第一次作业中,我以为只要把规定的类和函数实现即可,只用了最简单、最低效的方法实现,对于每个函数和类的JML规格,我就仅仅进行了翻译工作,将JML一行行翻译成代码,而且使用的数据结构也没有什么自己的思考,基本上没有进行任何优化,虽然弱测和中测过的很顺利,但强测成绩很一般;而在第二次作业中,我将数据结构大部分都换成了性能较好的HashMap,而且对于JML规格也不再拘泥于其表述方式。在第二次作业的实现中对于比较耗时的queryBlockSum函数,我使用了HashSet的ArrayList来将可以连通的person放入到一个HashSet中,通过该HashSet的ArrayList来查询互不相同的块数(但我在这有点脑子短路,本来BlockSum的值就是该HashSet的ArrayList的容量,但我又遍历了people用isCircle算了一遍,性能直接爆炸),而且这次我对于java类库中的调用开销并不是很清楚,在细节上没有进行处理,也使性能下降了不少;在最后一次实验中,通过助教在研讨课上的指点,我将大部分函数修改了一番,使得不会出现一个函数中对一个数据结构同时有get和contain此类问题,此外,我在类中也多定义了一些变量,将相关数据暂存起来,在未改变人物以及关系的情况下可以直接进行读取,若人/关系改变更新即可。

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

在本单元前面的作业中,由于弱测和中测过的太过轻松,我就只是对于所实现的函数指令,自己构造了一些简单的数据进行了简单的测试,并未过多考虑复杂的性能问题,也因此吃了大亏,强测结果很差。在后面的实验中,由于前面吃了亏,这次我简单研究了python的随机生成数据,生成了几组近万行的随机数据,来初步测试我多个根据JML规格要求实现的函数的正确性,在随机数据测试过后,我又根据一些自己实现时有问题不太确定正确性的、比较复杂以及耗费性能较多的函数进行专项测试,例如在添加Person、Relation以及Message后,其余指令均用于最短路径的查询,通过这种单独的指令进行性能以及正确性的测试,并根据获得的结果进行修改优化。
对于随机数据的生成,我还停留在很基础的阶段,只能通过手动限定来编排指令种类和条数,再用Java评测机来测试相应数据,而且正确性的评定还需要其他人的帮助,与其他人的输出进行对比,远远没有达到自动评测的程度。

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

在本单元最初的作业中,我并没有考虑性能问题,用了许多的ArrayList ,例如在NetWork中 :

private ArrayList<Person> people;

public MyNetwork() {
	this.people = new ArrayList<>();
}

在这几次的作业中,contains的方法很多,许多exception的判断都需要判断包含关系,而ArrayList的contains方法就要对于整个ArrayList遍历,造成极大的开销,虽然在第一次作业中这种问题体现的并不明显,但后面随着数据越来越多,弊端就显而易见,很容易就会超时,造成CPU_TIME_LIMIT_EXCEED。

在后面的作业中,我对于选用的数据结构进行了优化,大部分容器选用了HashMap,这样对于许多包含contains以及查询的函数,复杂度便大大下降,而且,我用了一个HashSet的ArrayList来将可以联系的人存入同一个HashSet中,这样便可以直接查出BlockSum的个数,在MyNetwork中的容器如下:

private HashMap<Integer, Person> people; //id, person
private ArrayList<HashSet<Integer>> hashSets;
private HashMap<Integer, Group> groups; // id, group
private HashMap<Integer, Message> messages; // id, message

public MyNetwork() {
    people = new HashMap<>();
    hashSets = new ArrayList<>();
    groups = new HashMap<>();
    messages = new HashMap<>();
}

在第三次的作业中,对于容器的选用大致没有进行更改,只是对于新增的一些功能进行数据结构的补充,大部分也是用HashMap来进行实现的,MyNetwork对于所规定的成员变量,我选用容器实现如下:

    private HashMap<Integer, Person> people; //id, person
    private HashMap<Integer, Group> groups; // id, group
    private HashMap<Integer, Message> messages; // id, message
    private HashMap<Integer, Integer> emojiList;

    public MyNetwork() {
        people = new HashMap<>();
        groups = new HashMap<>();
        messages = new HashMap<>();
        emojiList = new HashMap<>();
    }

在这次作业的实现过程中,我深刻地认识到,本次作业中HashMap远比看起来很简单的ArrayList合适的多,ArrayList虽然使用起来很简单,但是本次多用到contains函数,ArrayList的函数复杂度要o(n),而HashMap最好可以达到o(1),远优于ArrayList,而且本单元要实现的作业的各种元素都有id属性,而且对于元素以及id的联系比较紧密,经常通过id来获取其元素本身,因此HashMap的键值对就十分契合本单元需要解决的问题。
在本次的容器选用过程中,我逐渐认识到,对于容器的选用要契合当前要解决的问题,而且不能贪图简单,对于没有接触过的容器也要去勇于学习,不仅可以提升当前程序的性能,更对我们以后的学习中终身受益。

四、针对本单元容易出现的性能问题,总结分析原因

本单元最容易出现的问题就是CTLE,即CPU_TIME_LIMIT_EXCEED,CPU运行超时,在我完成本单元几次作业的过程中,也出现了许多此类的问题,修改时也花费了我大量的时间。

性能问题一:数据结构选择问题。
在第一次作业中,我使用了以前最常用,因此也是对于我而言最简单、最熟悉的容器ArrayList,而该数据结构就导致了性能上的劣势。例如,对于在本单元程序中经常使用到的contains方法,ArrayList需要遍历整个List,单单一个contains方法时间复杂度便为o(n),而与此相比,若使用HashMap,对于contains的方法可以达到o(1),简直是天壤之别。

性能问题二:算法问题。
在第二次作业中,在研讨课中我听到有同学分享isCircle()方法使用了bfs,复杂度爆炸导致出现性能问题。我在第二次作业中并没有使用这种算法,而是用了HashSet的ArrayList来把相关联的人放入同一个HashSet,但是犯了一个上文提到过的一个致命性错误,仍然用遍历了people用isCircle()算出BlockSum个数,这与我本身设计的数据结构本就背道而驰(其实直接返回该ArrayList的size()即可),而queryBlockSum的遍历也使我的复杂度过高导致多个点出现CTLE。

性能问题三:函数调用问题
在研讨课上,助教为我们讲解了许多关于java类库中内部函数的具体实现,并指出了我们代码中存在的许多共性问题,其中很多都是我所犯下的错误。例如,在queryValue函数中:

public /*@pure@*/ int queryValue(int id1, int id2) throws
            PersonIdNotFoundException, RelationNotFoundException {
        if (contains(id1) && contains(id2) && getPerson(id1).isLinked(getPerson(id2))) {
            return getPerson(id1).queryValue(getPerson(id2));
        } else if (!contains(id1)) {
            throw new MyPersonIdNotFoundException(id1);
        } else if (contains(id1) && !contains(id2)) {
            throw new MyPersonIdNotFoundException(id2);
        } else {
            throw new MyRelationNotFoundException(id1, id2);
        }
    }

有着很多的contains函数(甚至在其他函数中还有很多get函数,由于第二次作业都修改过了因此只能找到第一次作业中的函数来举例),而我在应用时也没有多想,每次都调用了一次contains函数,而每一次contains都会调用一次hashMap中的getNode函数,这无形之中就使我的开销增大了好多倍,其实完全可以运用一个局部变量进行存储hashMap.get(id),然后每次判断其是否为空即可:

public /*@pure@*/ int queryValue(int id1, int id2) throws
            PersonIdNotFoundException, RelationNotFoundException {
        Person person1 = getPerson(id1);
        Person person2 = getPerson(id2);
        if (person1 != null && person2 != null && person1.isLinked(person2)) {
            return person1.queryValue(person2);
        } else if (person1 == null) {
            throw new MyPersonIdNotFoundException(id1);
        } else if (person2 == null) {
            throw new MyPersonIdNotFoundException(id2);
        } else {
            throw new MyRelationNotFoundException(id1, id2);
        }
    }

这样整个函数中只调用了一次hashMap中的getNode(),便可以实现函数的功能,大大降低了复杂度。

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

本次实验的主要内容集中在MyNetWork中,其余各个类都是实现自身的一些借口以供MyNetwork使用,而且各个类的接口都在MyNetwork有所体现,接下来,我主要通过MyNetwork的数据结构及函数分析梳理作业架构。
MyNetwork类属性如下:

    private HashMap<Integer, Person> people; //id, person
    private ArrayList<HashSet<Integer>> hashSets;
    private HashMap<Integer, Group> groups; // id, group
    private HashMap<Integer, Message> messages; // id, message
    private HashMap<Integer, Integer> emojiList;
    private HashMap<Integer, HashMap<Integer, Integer>> path; //p1, p2, value
    private HashMap<Integer, Boolean> pathUpdate;

其中,people、groups、messages、emojiList都是用HashMap所实现的JML规定规格,而HashSets为HashSet的一个ArrayList,其中每个HashSet中都储存着可以相互连通的人,每次添加关系时将人加入到HashSet中,添加关系/添加人时将含有相同人的HashSet合并,进行数据的更新,这样queryBlockSum便可直接o(1)查询ArrayList的大小即可。
而path中存储了当前p1到达其可联系的所有人的最短路径,pathUpdate为p1到其他人的最短路径是否已经更新,通过缓存的方式,当人物关系未改变时,可以直接通过path来读出数据,而在需要更新时重新算一次最短路径,再将最短路径存入path中即可,在MyGroup中我也进行了类似处理,将varAge和sumAge存储起来,在需要更新时重新计算,这样会在人物及关系未改变的情况下减少计算次数。

public class MyGroup implements Group {
    private int id;
    private HashMap<Integer, Person> people; // id person
    private int sumAge;
    private int valueSum;
    private int varAge;
    private boolean updated;
上一篇:判断某值是否在列表中存在List.Contains(Power Query 之 M 语言)


下一篇:高可用系列之Nginx