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. 性能分析
实现上,新建一个类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格式的考察,也重点考察了在保持规格的情况下如何有效地提升程序的性能,对算法能力有一定的要求。不过由于本单元涉及的算法并不算难,主要是图论的算法,在数据结构课上也有学过一些,在编写的过程中通过百度学习也能熟练掌握。

浙公网安备 33010602011771号