OO第三单元JML总结
OO第三单元JML作业总结
一、实现规格所采取的设计策略
本单元的作业旨在实现一个社交关系模拟系统。
- 首先,由于异常类的功能基本相同,所以先完成几个异常类。
- 然后,结合官方包代码中的类名和各个类的代码规格可以确定MyNetwork类是代码量最大,功能最复杂的类,是最上层的类,所以先完成除MyNetwork类的其他几个类,最后实现MyNetwork类。
- 在实现各个类的过程中,先结合类名和方法名猜各个方法的实际意义,然后结合给出的
JML规格进一步理解并且实现。如果JML规格非常复杂,比如sendIndirectMessage方法的规格有40+行,就把规格分成几部分,分别考虑其实际意义然后实现,保证完全按照JML的规格实现。 - 另外,在实现
JML规格的过程中,较多地使用了HashMap容器,并且采用并查集、堆优化Dijkstra等算法尽量减小方法的时间复杂度。
二、基于JML规格来设计测试的方法和策略
本单元虽然推荐我们使用Junit进行测试,但是在本单元第一次作业使用了Junit后,感觉并不是很好用(应该还是太弱了不会用),而且第二次第三次作业很多方法的规格非常复杂,就不想搞Junit测试了Orz。
主要的测试还是以黑盒对拍测试为主。随机生成数据为主,主要对于程序整体的功能正确性进行测试,手工构造数据为辅,主要对于JML规格中时间复杂度较高的方法进行测试,卡时间。
虽然前两次作业并没有出问题,但是第三次作业的评测机数据生成写的不太好,导致有一个异常类的一处小笔误没有检查出来导致强测爆炸了QAQ,还是有些后悔没有用Junit全面测试一下的。
三、容器的选择和使用
在本单元的三次作业中主要采用了HashMap和ArrayList容器,在堆优化Dijkstra算法中还使用了PriorityQueue容器。
-
MyPerson类
HashMap<Person, Integer> friends // <acquaintance, value>:由于acquaintance与value是一一对应的,所以使用HashMap来存储。ArrayList<Message> messages:考虑到向MyPerson添加Message时需要考虑插入顺序的问题(需要头插),getReceivedMessages方法也与插入的顺序有关,所以采用ArrayList来存储,添加时使用arraylist.add(int index,E element)方法即可。
-
MyGroup类
ArrayList<Person> people:最初考虑使用HashMap来存储,但是考虑到MyGroup类中的方法基本不涉及到对这个容器中Person的查询操作,都是遍历,不需要以键值对的形式进行存储,所以直接使用了ArrayList。
-
MyNetwork类
-
由于Person、Group、Message和Emoji都具有一个独一无二的
id属性,并且MyNetwork类中增删改查的操作很多,所以都采用HashMap进行存储。 -
HashMap<Integer, Person> people; //<person.id, person> -
HashMap<Integer, Group> groups; //<group.id, group> -
HashMap<Integer, Message> messages; //<message.id, message> -
HashMap<Integer, Integer> emojis; //<emoji.id, emoji.heat> -
另外由于采用了并查集进行优化,所以还有两个容器。
-
HashMap<Integer, Integer> father; //<person.id, fatherId>:把每个人的id和其父结点的id作为键值对进行存储。 -
HashMap<Integer, Integer> rank; //<person.id, rank>:把每个人的id和其对应的rank作为键值对进行存储。
-
四、性能问题
出现性能问题的原因是算法的时间复杂度过高,在本单元的三次作业中有四个方法有可能会出现性能问题:MyNetwork类中的queryBlockSum方法和sendIndirectMessage方法,这两个方法分别对应图论中的求连通块数和求最短路问题;MyGroup类中的getValueSum方法和getAgeVar方法。
-
queryBlockSum方法(求图的连通块数):最初这个方法是用BFS算法实现的,功能上确实没什么问题,但是和同学对拍的过程中就发现程序运行时间很长,并且如果手工构造数据,时间会特别长QAQ。后来使用并查集进行优化,实现并查集的过程中还实现了路径压缩和按秩合并进行进一步的优化。这样一来,只需要在addRelation方法中进行操作,queryBlockSum方法的复杂度就降低到了O(n),只需要遍历所有的结点,父结点的个数也就是连通块数:@Override public int queryBlockSum() { int count = 0; for (int eachId : people.keySet()) { //单重循环,看一共有几个父结点 if (father.get(eachId) == eachId) { count++; } } return count; }啰嗦一句,
并查集的实现有递归和非递归两种实现方法,为了防止爆栈,建议使用非递归实现。//递归实现 private int find(int id) { if (id == father.get(id)) { return id; } else { father.replace(id, find(father.get(id))); return father.get(id); } } //非递归实现 private int find(int id) { while (id != father.get(id)) { father.replace(id, father.get(father.get(id))); id = father.get(id); } return id; } //并没有压缩到最优 -
sendIndirectMessage方法(求图中两结点的最短路):由于两个人之间的value值只能为非负值,所以这个方法可以使用Dijkstra算法实现,而朴素的Dijkstra算法的时间复杂度是O(n2),考虑到数据最长可以达到10000行,所以采取堆优化,可以把时间复杂度降低到O(mlogn),避免出现性能问题。并且由于java内置了优先队列PriorityQueue容器,所以堆优化的实现非常简单,只需要重写Edge的compareTo方法。PriorityQueue<Edge> queue = new PriorityQueue<>();public class Edge implements Comparable<Edge> { private Person to; private int value; /*————————————————*/ @Override public int compareTo(Edge o) { return value - o.getValue(); } } -
getValueSum方法和getAgeVar方法:这两个方法的处理就不像前两个方法那样涉及算法的问题了。只需要维护两个变量valueSum、ageVar即可,初始化为0,在向Group加人和删人的时候进行修改即可(我还设置了ageSum变量,便于求年龄平均值):private int valueSum; private int ageSum; private int ageVar;@Override public void addPerson(Person person) { ageSum += person.getAge(); people.add(person); ageVar = 0; int ageAverage = getAgeMean(); for (Person eachPerson : people) { valueSum += 2 * person.queryValue(eachPerson); ageVar += (eachPerson.getAge() - ageAverage) * (eachPerson.getAge() - ageAverage); } }
五、作业架构设计
-
本单元的架构设计已经由官方包代码给出,我们只需要按照
JML规格实现即可,不需要考虑太多,除了要求实现的类,仅在第三次作业中为了实现堆优化Dijkstra算法添加了Edge类。 -
图模型构建与维护策略:
-
构建:其实图模型的构建
JML规格已经为我们实现了,Network就是一张图,每个Person就是一个结点,为两个Person添加关系也就是连接两个结点,两个人之间的value值就是边的权值,Person的acquaintance其实就是邻接表。在完成作业的过程中,除实现了JML规格规定的图模型以外,因为使用了并查集,所以构建了一个新的图,新图的结点与Network的结点相同。 -
维护:对于已有的图模型,要做的就是严格按照给出的
JML规格实现各个类及其方法即可。对于新构建的图,具体操作如下://addPerson方法中 father.put(person.getId(), person.getId()); //将父节点设置为自己 //addRelation方法中 //对加关系的两个Person进行合并操作(Merge) int father1 = find(id1); int father2 = find(id2); if (rank.get(father1) <= rank.get(father2)) { father.replace(father1, father2); } else { father.replace(father2, father1); } if (rank.get(father1).equals(rank.get(father2)) && father1 != father2) { rank.replace(father2, rank.get(father2) + 1); } //find方法(为实现并查集)中 //路径压缩,具体实现见性能问题部分
-

浙公网安备 33010602011771号