BUAA_OO第三单元JML规格总结
一、实现规格的设计策略
JML规格中的绝大部分函数都可以直接按照规格实现,如基础的get,compare,add等;某些着重于搜索的函数并不能完全按照JML规格对整个数组遍历实现,可以考虑使用更适合的Hashmap存储并实现搜索;在实现一些较为复杂的功能时,JML给出的描述可能不能实现方法或时间复杂度太高,这时就需要先读懂函数功能要求,再自己实现方法。
比如第一次作业中的isCircle,可以利用dfs实现(但是在第二次作业中会CTLE),也可以使用并查集;比如qbs的实现让我认识到有些复杂的算法不能在查询函数中,qbs的特点是在添加人或添加关系时变化,其他操作不会发生变化,故可以不用把操作放在调用qbs查询的函数中,而可以将qbs设计为变量,每次add person或add relation的时候进行更新,查询时只返回一个值。因为同一个人\关系仅能添加一次,却可以查询无数次,故这样可以减少CPU运行时间。此后还有几个需要在发生变化时更新的函数。
在异常类中,由于异常类功能相近,都需要记录异常类发生次数与该id发生次数,故我额外设置了整体计数器来记录异常类发生的次数,避免大量的复制粘贴。
JML中也有许多复杂的描述,可以翻译成比较简单的意思,比如 增添新元素后,容器长度加一,旧有元素不变 应领会意思,适当忽略。
此外对于JML规格中优化的部分会在接下来的容器选择,性能优化的部分详细说,在此不多赘述。
二、基于JML规格设计测试的方法和策略
因为这次大家都是按照指定函数进行构造,规格设计极为相似,所以代码相对前几次也比较好理解,我们可以看其他人的代码,寻找不符合规格的地方进行hack。
在随机制造大数据的时候,确认结果准确性比较难,可以采取和别人对拍的方式来确认结果。
应注意构造一些边界数据,覆盖各种分支。
三、容器选择和使用的经验
Hashmap
刚一上手的时候,会习惯性想用ArrayList作为容器,但看到方法中有很多contains和通过id获取对应人\信息,可以看出对于这种有特定id的对象,更适合用Hashmap来获取对应的对象。
ArrayList
此外,我注意到在我添加关系时会更新每一个相关组的ValueSum,此时不知道person1、person2都属于哪个组,故可以再建立一份ArrayList作为容器进行遍历。
LinkedList
对于特殊需求,如Person的Messgae需要从头部开始加而非ArrayList擅长的尾部,且遍历时也只需考虑前四个,不需对遍历有太多优化,故选用了LinkedList作为容器。
MyPerson中的容器
private HashMap<Integer, Person> acquaintance;
private HashMap<Person, Integer> valueList;
private LinkedList<Message> messages;
MyNetwork中的容器
private HashMap<Integer, Group> groupHashMap;
private ArrayList<Group> groups = new ArrayList<>();
private HashMap<Integer, Message> messages;
private HashMap<Integer, Integer> unionFind = new HashMap<>();
private HashMap<Integer, Integer> emojiIdList = new HashMap<>();
private HashMap<Integer, Integer> emojiHeatList = new HashMap<>();
MyGroup中的容器
private HashMap<Integer, Person> people;
private ArrayList<Person> personArrayList;
四、性能问题分析
isCircle
isCircle是判断两个人是否有关的函数。题目中两个人可能直接相关,也可能间接相关,故并不容易判断。
可以考虑采用dfs的方法在熟人列表里进行深度优先搜索,直到搜查到另一位人或搜索完毕位置为止。但是这样的方法复杂度过高,需要O(N+E),且不能存储已经查询到的结果,这意味着每次调用都需要再查一遍。
经过查询后知道了采用并查集的方法,其基本思路为将有关联的人放进同一个集合中,若他们的集合序号相等,则有关。
但是考虑到addRelation后若两个人位于两个组不容易合并,我在互测屋里见到了用hashmap实现并查集的方式,可以用<id,根节点id>的方式构建Hashmap,若根节点相同,则属于同一集合。
@Override
public boolean isCircle(int id1, int id2) throws PersonIdNotFoundException {
ArrayList<Integer> acquire = new ArrayList<>();
acquire.add(id1);
if (contains(id1) && contains(id2)) {
if (id1 == id2) {
return true;
} else {
return getDfs(id1) == getDfs(id2);//更新根节点
}
} else if (!contains(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else if (!contains(id2)) {
throw new MyPersonIdNotFoundException(id2);
}
return false;
}
public int getDfs(int id) {
int dfs;
if (unionFind.get(id) != id) {//判断是否为根节点
dfs = getDfs(unionFind.get(id));
unionFind.put(id, dfs);//寻找上一个根节点
}
return unionFind.get(id);
}
queryBlockSum
这个函数的优化比较简单,就是设置qbs的变量,把所有计算都放入可能令qbs值改变的函数,而不是qbs本身。
只需要在addperson中令qbs+1。在addrelation中若两个人在一个block中,不变;若两个人不在一个block中,则qbs-1。这样做的好处已经在上面说明。
agemean
与qbs的思想一样,设定变量,将运算放到add\del person中,调用函数时只返回变量。
sendIndirectMessage
经过阅读JML,可以确定这个函数的行为是找到Message的两个person间最短路径。这是图中寻找最短路径的算法,经典的有两种,Floyd和Dijkstra,其中dijkstra的时间复杂度更低,为O(n2),而Floyd为O(n3)。
但是这种图算法与我们需要的算法有一定的区别,Dijkstra求出的是起始点到周围所有点的最短距离,而我们所需要的仅是它对于确定点的最短距离,由于算法限定,最短路径小于该点的点的计算无法省略,但我们可以将结束标准定为“已找到到达确定点的最短路径”,可以减少一定的时间。
这样做还是不能将作业优化标准要求,故可以采取堆优化的方法。由于每次寻找下一个最短路径时,需要遍历顶点的相邻节点,取得与顶点相邻的最短路径,此时需要依靠遍历实现,这会减慢速度。堆优化方法就是改用set小顶堆来达成减少寻找最短路径的时间,最短的顶点位于最上面,时间变为O(1)。再算上更新堆需要的时间,总时间复杂度变为O(nlogn)。
五、自己的构架
其中MyPerson类是最基础的一个结构,其他类都可以把他当作属性。MyGroup是Person的一个集合,即在一个群里面有很多个人。
MyMessage分为4个类别,同时也分群发消息和私聊消息,故Person和Group都是信息接受双方的属性。
MyNetwork用于侦察社交网络图的一些属性,并且接受指令执行,是最顶层且与外界指令互动的类。
感想
总的来说,这次作业应该是最简单的,但是bug却异常多,还是由于不太细心,过于依赖中测,且没有好好找最简洁的方法所致。