301
社区成员
发帖
与我相关
我的任务
分享本单元作业的重点为代码效率的优化,在本次作业中主要体现在以下三个方面:
本次作业中所有对Person进行的查找,都可以转化为对唯一的PrimaryKey: id进行的查找。因此采用HashMap容器存储Person,其中HashMap的key代表Person的id,value代表Person,可以保证所有的查找时间复杂度均为O(1),避免每次查找时都对数组进行遍历。
本次作业中的isCircle()函数要求查询两个Person之间是否连通,可以采用并查集的方式进行优化。并查集用于管理一些不相交的集合,主要思想为用集合中的一个元素代表一个集合。对于每一个节点,都有一个父节点用于代表它。如果两个节点的代表节点相同,则说明它们属于同一个集合。这样两个Person的isCircle判断就可以被转化成对它们的代表节点是否相同的判断。
将并查集封装为如下的类:
public class DisjointSet {
private final HashMap<Integer, Integer> pre;
private final HashMap<Integer, Integer> rank;
public DisjointSet() {
pre = new HashMap<>();
rank = new HashMap<>();
}
public void add(int id) {
if (!pre.containsKey(id)) {
pre.put(id, id);
rank.put(id, 0);
}
}
public int find(int id) {...}
public void merge(int id1, int id2) {...}
}
pre存储一个节点和代表它的节点,key为节点编号,value为该节点的代表节点编号。rank存储一个节点的秩,初始秩为0,表示一个节点一开始的代表节点是它自己。
查找一个节点的代表节点时,可以将查找路径上的所有节点的直接代表节点都设置为最终查找到的代表元。在查找过程中进行路径压缩的具体方式如下:
public int find(int id) {
int represent = id;
if (!pre.containsKey(id)) {
add(id);
}
while (represent != pre.get(represent)) {
represent = pre.get(represent);
}
int current = id;
while (current != represent) {
int father = pre.get(current);
pre.put(current, represent);
current = father;
}
return represent;
}
如果将find写为递归函数会导致爆栈。
每个节点的秩代表从该节点找到代表节点需要经历的步数。将两个节点合并时,需要使合并后的 秩尽量低,因此合并时将秩小的节点的代表节点设置为秩大的节点。具体内容如下:
public void merge(int id1, int id2) {
int father1 = find(id1);
int father2 = find(id2);
if (father1 == father2) {
return;
}
int rank1 = rank.get(father1);
int rank2 = rank.get(father2);
if (rank1 < rank2) {
pre.put(father1, father2);
} else if (rank1 == rank2) {
pre.put(father2, father1);
rank.put(father1, rank1 + 1);
} else {
pre.put(father2, father1);
}
}
本次作业中queryBlockSum和queryTripleSum需要计算network中连通块和三角形的数量,如果每次调用时计算会产生超时,需要在增删Person和Relation时就进行动态维护。
link,则三角形数量+1本次作业编写了queryTripleSum方法的Junit测试,该测试主要有两个要点:
Person数组的所有内容均不产生改变首先生成测试数据,为了保证生成稠密的图,每次添加关系时的两人中有一人为上一次添加的关系中的一方。之后创建两个network,一个用来调用方法,另一个用来存储调用前的数据。
检验结果时,按照JML规格中的表述进行复刻即可,之后通过assertEquals()断言对结果的正确性和Person的属性进行判断。
本次作业的优化主要体现在求两点之间不带权最短路径的算法,以及新添加的Tag类中的计算类方法。
queryShortestPath()要求求出两点之间的不带权最短路径,可以采用Dijkstra算法求出以某节点为源点到所有点的路径长度,存储在一个HashMap中,之后查找目的节点对应的值即可求得最短路径长度。需要注意的是相连的两个节点距离是0而不是1,因此需要将结果-1;但这样会导致节点和自己的距离变为-1,因此需要特殊讨论。
本次作业中主要的计算类方法在于Tag中添加的对年龄的平均值和方差以及对queryValue求和的计算。为了避免每次查询时都重新遍历求和,在向Tag添加或删除Person时,维护age和age^2的值。当添加或删除Relation时,对queryValue的值进行维护。
每次增加或删除关系时,都对bestAcquaintance的值进行更新,如果需要删除的Person为bestAcquaintance,则重新寻找新的bestAcquaintance。需要注意可能出现删除关系后不存在acquaintance的情况,具体代码如下:
public void delAcquaintance(int personId) {
acquaintance.remove(personId);
value.remove(personId);
if (personId == bestId) {
maxValue = 0;
bestId = 2147483647;
for (Integer key : value.keySet()) {
if (value.get(key) > maxValue) {
bestId = key;
maxValue = value.get(key);
} else if (value.get(key) == maxValue) {
if (key < bestId) {
bestId = key;
}
}
}
if (bestId == 2147483647) {
acqNum = 0;
}
}
}
本次作业编写了queryCoupleSum方法的Junit测试,该测试主要有两个要点:
Person数组的所有内容均不产生改变本次作业的JunitTest与上次类似,此处不再赘述。
本次作业没有新增太多复杂的算法,但是需要注意在delNoticeMessages中,如果一边遍历一边删除,会出现RunTimeError。需要保证遍历的容器不做更改。
本次作业编写了deleteColdEmojiTest方法的Junit测试,该测试主要要点如下:
构造测试样例的方式与之前类似。本人在构造数据时出现的一个错误是,由于MessageId是随机生成的,EmojiMessage的Id有可能与其他类型Message的Id相同,导致后构造的其他类型Message覆盖了之前构造的EmojiMessage,从而产生错误的EmojiIdList和EmojiHeatList。
JunitTest中使用这种方式构造大量数据,以检测方法实现的正确性。JunitTest对方法进行测试。本人在进行测试时,采用黑箱测试与白箱测试相结合的方式。既生成大量随机数据检测正确性,还针对特殊情况构造数据测试代码中的逻辑问题。例如对于Tag相关内容,在增删人和关系时,容易忽略删除关系后双方不在对方的Tag里这种情况。通过构造类似的测试数据,不仅发现了自己代码中存在的漏洞,还找出了同学的bug。
针对每次作业的优化均在前文提及,但本人在强测中还是出现了超时现象。原因是并查集按秩合并时,将find(id)误写为pre.get(id),导致代表节点相互嵌套,产生死循环。
JML规格语言为类和方法定义了清晰的规格,将规格定义与代码实现分离,主要有以下几个优势:
通过本单元的学习,我掌握了JML规格语言的读写和使用,初步体会到契约式编程的优势。同时也实践了许多图论算法,掌握了不少优化性能的方法,进一步提升了编程能力。