OO第三单元总结

OO第三单元总结

一、设计策略

第一次作业

第一次作业比较简单,要求通过官方提供的带有JML接口实现自己的类,并实现规定的异常类,大多数实现可根据所给规格简单实现。

1. 容器选择

在MyPerson中,由于一个person对应一个value,所以通过HashMap<Person, Integer>作为acquaintance合并进行储存。在MyNetwork中,一个person对应一个id,固还是通过Hash<Integer, Perosn>进行储存。

2. 性能分析

本次作业中相对较难的方法为isCircle方法,其余方法的实现比较容易。对于isCircle方法,即判断两个人是否有关系,可以采用简单的dfs或bfs,但是由于这样算法的时间复杂度是O(n^2),虽然在这次强测中不会被卡掉,但在互测和之后的作业中则有可能会因此超时,因此,由于作业中只有addRelation而没有delRelation,便采用了路径压缩的并查集方法,时间复杂度为常数级

实现上,通过增加一个HashMap<Integer,Integer>fa,在addPerson方法中增加person的id对应的键值,并将其对应的值设置为自己的id。在addRelation方法中,对新增关系的两个id进行merge,具体方法如下,其中find采用了路径压缩。

private int find(int id) {
    if (id == fa.get(id)) {
        return id;
    } else {
        fa.put(id, find(fa.get(id)));
        return fa.get(id);
    }
}
​
private void merge(int id1, int id2) {
    fa.put(find(id1), find(id2));
}

本次作业中另一个较难的容易出现性能问题的方法queryBlockSum也是利用了并查集所用到的fa,查找总共有多少个不同的父节点,实现上利用了hashSet中元素的唯一性,时间复杂度为O(n)具体实现如下。

public int queryBlockSum() {
    HashSet<Integer> hashSet = new HashSet<>();
    for (int i : people.keySet()) {
        hashSet.add(find(i));
    }
    return hashSet.size();
}

当然也可在添加一个blockSum的变量,在addPerson和addRelation时进行维护,在查询时返回blockSum即可,但由于O(n)的算法已经不会超时了,就没有去更改了。

3. 异常类实现

异常类需要记录异常发生的次数和对应id触发异常的次数,因此采用了静态int变量cnt存储异常发生的次数,静态HashMap存储对应id触发异常的次数。

第二次作业

第二次作业相对于第一次作业,增加了需要实现的接口Group和Message,还有对应的异常类和新增的一些方法。异常类的实现与第一次作业相同,不在赘述。

1. 容器选择

MyPerson中由于对接收到的messages有顺序的要求,因此采用了List,在MyNetwork中,由于一个id对应一个Message,因此采用了HashMap,MyGroup中的people同理也采用了HashMap。

2. 性能分析

对于新增的方法,实现起来较为简单,但如果完全按照JML来写可能会导致超时,因此对于一些get方法采用了预处理的方式以降低时间复杂度。

实现上,通过新增变量保存对应的valueSum、ageSum(年龄和)、ageVarSum(年龄平方和)。在addPerson的时候进行增加,在delPerson进行减少,在get时经过对应的简单的计算即可。valueSum的更新需要遍历一遍Group中的people进行更新。因此,总体以O(n)时间复杂度维护, O(1)时间复杂度进行回答。需要的注意的是在addRelation时,需要对相应的valueSum进行更新。具体的三个get方法如下。

public int getValueSum() {
    return valueSum * 2;
}
​
public int getAgeMean() {
    return people.size() == 0 ? 0 : ageSum / people.size();
}
​
public int getAgeVar() {
    if (people.size() == 0) {
        return 0;
    } else {
        return (ageVarSum - 2 * ageSum * getAgeMean() + people.size() * getAgeMean() * getAgeMean()) / people.size();
    }
}

 

第三次作业

第三次作业需要实现三个继承自Message的接口,对应的异常类和一些新增的方法。三个新接口的实现较为简单,异常类的实现也和之前一样。

1. 容器选择

本次作业需要新增int[] emojiIdList和int[] emojiHeatList,由于一个emoji对应一个id,又对应一个heat,因此只用一个HashMap,键值为emoji的id,并映射它的heat。

2. HashMap的删除

本次作业的方法中deleteColdEmoji需要对元素进行删除,这些元素我都储存在HashMap中。对于HashMap中元素的删除,如果采用简单的遍历判断并进行remove,会产生错误,抛出java.util.ConcurrentModificationException的异常大概是因为在遍历HashMap的元素过程中删除了当前所在元素,下一个待访问的元素的指针也由此丢失了。于是通过迭代器进行删除,代码如下。

for (Iterator<Map.Entry<Integer, Integer>> it = emojis.entrySet().iterator(); it.hasNext(); ) {
    Map.Entry<Integer, Integer> item = it.next();
    if (item.getValue() < limit) {
        it.remove();
    }
}

 

3. 性能分析

本次作业最难的最容易出现性能问题的地方应该就是sendIndirectMessage中的求图的最短路径的算法了,考虑到时间复杂度,O(n^3)的Floyd算法必然不行,O(n^2)的算法应该也不可取,这样基本的Dijkstra算法也行不通。因此就采用了O(nlogn)的堆优化dijkstra算法。

实现上,新建一个类Edge存储id和距离,并利用java自带的PriorityQueue<Edge>来实现小根堆。其中为实现小根堆,需传入一个Comparator<Edge>,如下所示。

private static Comparator<Edge> comp = new Comparator<Edge>() {
    @Override
    public int compare(Edge c1, Edge c2) {
        return (int) (c1.getValue() - c2.getValue());
    }
};

 

二、测试

本次作业主要通过评测机生成随机数据进行对拍,没有用到具体的专门针对的工具,所以这里简单介绍一下主要的基于JML规格的测试工具。

OpenJML

OpenJML最基本的功能是堆JML注释的完整性进行检查,包括经典的类型检查、变量可见性与可写性等等。通过命令行使用OpenJML时,可以通过-check(缺省)指定类型检查。对于规格内容的检查,需要使用-esc参数。

openjml [-check] options files

不过OpenJML环境很难配,不支持高版本java,功能十分有限。

JMLUnitNG

JMLUnitNG可以根据规格自动生成测试用例,主要是针对边界数据的测试。如在参数为 int 的情况下,会自动生成 INT_MAX ,0 , INT_MIN 进行测试,对象则直接传入null。数据覆盖度较低,因此应该主要可以用来检查极端数据下是否又异常。

JUnit

JUnit是java语言的单元测试框架,Junit测试是程序员测试,即所谓白盒测试,通过自己编写测试代码进行自动测试,可以保证很高的覆盖率(不过覆盖率高也不能保证没有bug)。而且Junit环境很好配,使用体验很好。对于代码的重构等可以打打提高编程效率。

对拍工具

本次的评测机通过python进行编写,采用了随机生成指令,并通过对拍验证正确性。不过由于是纯随机,数据较弱,只能找出一些明显的bug。对于hack策略还是通过下载他人的代码,查看他们方法的时间复杂度,有针对性地进行测试数据地构造。

BUG分析

本单元作业中三次中、强、互测均为发现bug。

在自测中第一次作业发现了异常类的输出写错一个字母的bug(差点酿成大错),第二次作业在addRelation时忘了对预处理的值进行更新。第三次没有发现bug。

对于同屋其他人的bug,主要是时间复杂度的问题。在第二次作业中一位同学的qgvs采用了时间复杂度为O(n^2)的方法,因此针对该点构造数据卡掉。其它地方没能找到bug。

三、体会与感想

通过本单元作业的练习与实验,我对JML格式有了较为清晰的认识,可以根据JML判断出程序大致的功能是什么,也能够编写一些简单的JML注释了。

本单元的作业相对于前两单元难度较为简单,重点在于对细节的实现是否充分。而且实现时要实现规格但又不能完全拘泥于规格,否则可能会导致较高的时间复杂度,使性能下降。因此,本单元除了对JML格式的考察,也重点考察了在保持规格的情况下如何有效地提升程序的性能,对算法能力有一定的要求。不过由于本单元涉及的算法并不算难,主要是图论的算法,在数据结构课上也有学过一些,在编写的过程中通过百度学习也能熟练掌握。

 

 

 

 

 

 

posted @ 2021-05-27 17:17  19lcw  阅读(84)  评论(0)    收藏  举报