第三单元总结博客
设计策略
第九次作业实现的东西并不复杂,只是简单的提供了一个社交系统的必要组成部分,包括Person类和支持简单操作的Network类。考虑到person的id是唯一的,因此在本次架构中大多数的容器都采用了以id作为hash值索引的存储方式,主要应用的是HashTable<>()
后两次作业在架构上没什么可说的,仍然是增加了一些社交系统的组成部分,对于Message类和Group类我们很容易想到继续采用HashTable<>()进行存储,重点在于如何实现对应的功能和提高性能,这两部分在后面会阐述。
在本单元测试环节,我尝试了Junit方法测试规范性,但很快还是认为对拍方式和鲁棒性测试效率更高,后来的两次作业都采用了后者进行测试。
容器选择
- 由于Person、Message、EmojiMessage、Group的id都是唯一确定的,因此为了提高性能,有关上述对象的存储都使用HashTable容器
- 对于Person.messages,通过阅读JML规格看出sendMessage方法和getRecievedMessage方法是依赖于收发顺序的,因此采用了Stack作为容器进行存储
性能优化
第九次作业
本次作业中最大的优化点应该就是判断两点是否连通的isCircle方法和计算连通分支数的quaryBlockSum方法。考虑到本单元作业是一个动态变化的图结构,我选择了一个辅助类作为性能优化的工具,与堆优化类似,但可以随着作业迭代增加新的功能。
public class Blocks {
private Hashtable<Integer, Integer> index = new Hashtable<>();
private Hashtable<Integer, LinkedList<Integer>> blocks = new Hashtable<>();
//blocks: <街区号, 街区<person.id>>
//index:<person.id, 所在街区>,用于对id索引查表
//增加新的Person, 需要增加新的街区,同时在index表中增加新的key值,由于街区号其实意义并不太大,只要不重复就行,因此依然使用了person.id
public void addPerson(int pid) {
blocks.put(pid, new LinkedList<>());
blocks.get(pid).add(pid);
index.put(pid, pid);
}
//核心方法之一,返回一个personId对应的街区号
public int pid2index(int pid) {
return index.get(pid);
}
//当增加新的关系时,若两人不在同一街区,将街区合并,并更新索引表
public void addRelation(int id1, int id2) {
if (pid2index(id1) == pid2index(id2)) {
return;
} else {
int tmp = pid2index(id2);
int des = pid2index(id1);
for (int i : blocks.get(tmp)) {
blocks.get(des).add(i);
index.put(i, des);
}
blocks.remove(tmp);
}
}
//返回街区总数
public int getSize() {
return blocks.size();
}
}
这是第九次和第十次作业使用的Block类,第十一次作业对其进行了重构。该类的核心功能是按照person1和person2之间是否认识划分街区,通过判断person1和person2是否在同一街区判断是否连通,通过街区总数计算BlockSum,具体方法含义见代码区。
第十次作业
主要性能损失在Group类的几个计算方法中,解决方法很简单,在addPerson的时候就进行运算,减少每次的遍历开销,这里用到了概率统计的公式,但是利用简单的因式分解也能写出来。
private int agesum = 0;
private long agesquaresum = 0;
private int valuesum = 0;
@Override
public void addPerson(Person person) {
int age = person.getAge();
people.put(person.getId(), (MyPerson) person);
agesum += age;
agesquaresum += age * age;
for (Integer i : people.keySet()) {
if (people.get(i).isLinked(person)) {
addValue(people.get(i).queryValue(person));
}
}
}
@Override
public void delPerson(Person person) {
int age = person.getAge();
for (Integer i : people.keySet()) {
if (people.get(i).isLinked(person)) {
valuesum -= people.get(i).queryValue(person) * 2;
}
}
people.remove(person.getId());
agesum -= age;
agesquaresum -= age * age;
}
/*
...
*/
@Override
public int getAgeMean() {
int size = getSize();
if (size == 0) {
return 0;
}
return agesum / size;
}
@Override
public int getAgeVar() {
int size = getSize();
if (size == 0) {
return 0;
}
int ageMean = getAgeMean();
return (int) ((agesquaresum + size * ageMean * ageMean - 2 * ageMean * agesum) / size);
}
第十一次作业
显而易见,主要的性能损失在求解最短加权路径上,这里考虑到图的结构经常变化,因此采用了迪杰斯特拉算法,因为其复杂度比较低。同时为了充分利用我们之前设立的Blocks类,显然在计算是否存在最短加权路径时,首先要考虑是否两个人在相同街区,然后对相同街区内的人进行遍历即可。为此的话对之前的Blocks类进行了修改。
public class Blocks {
//构造了一个内部类,用于封装前两次作业中的单个街区
private class Block {
private HashSet<Integer> pidlist = new HashSet<>();//街区中person的id集合
private Hashtable<Integer, Hashtable<Integer, Integer>> distancemap = new Hashtable<>();//街区中任意两点之间的最短路径
public void addpid(int pid) {
pidlist.add(pid);
}
public HashSet<Integer> getPidlist() {
return pidlist;
}
//查询是否存在id1到id2的最短路径的缓存,默认返回-1
public int qds(int id1, int id2) {
if (!distancemap.containsKey(id1)) {
return -1;
}
if (!distancemap.get(id1).containsKey(id2)) {
return -1;
}
return distancemap.get(id1).get(id2);
}
//重置整个map,每当有新人加入街区时都重置该街区的map
public void resetDistance() {
if (!distancemap.isEmpty()) {
distancemap.clear();
}
}
//写最短路径,path(id1->id2)=dis
public void setDistance(int id1, int id2, int dis) {
if (!distancemap.containsKey(id1)) {
distancemap.put(id1, new Hashtable<>());
}
distancemap.get(id1).put(id2, dis);
}
}
private Hashtable<Integer, Integer> index = new Hashtable<>();
private Hashtable<Integer, Block> blocks = new Hashtable<>();
public void addPerson(int pid) {
blocks.put(pid, new Block());
blocks.get(pid).addpid(pid);
index.put(pid, pid);
}
public int pid2index(int pid) {
return index.get(pid);
}
public void addRelation(int id1, int id2) {
if (pid2index(id1) == pid2index(id2)) {
blocks.get(pid2index(id1)).resetDistance();
return;
} else {
int tmp = pid2index(id2);
int des = pid2index(id1);
for (int i : blocks.get(tmp).getPidlist()) {
blocks.get(des).addpid(i);
index.put(i, des);
}
blocks.remove(tmp);
blocks.get(des).resetDistance();
}
}
public boolean iscircle(int id1, int id2) {
return pid2index(id1) == pid2index(id2);
}
public int getSize() {
return blocks.size();
}
//查询是否存在从id1到id2的最短路径缓存
public int qbs(int id1, int id2) {
int index = pid2index(id1);
return blocks.get(index).qds(id1, id2);
}
public void setBlockDistance(int id1, int id2, int distance) {
blocks.get(pid2index(id1)).setDistance(id1, id2, distance);
}
//返回person.id对应的街区内的personList
public HashSet<Integer> getBlock(int pid) {
return blocks.get(pid2index(pid)).getPidlist();
}
}
感想
相较于前两单元的作业来说,本次作业可以说是非常简单了,只要先宏观理解JML规格以及最终要实现的功能,然后根据经验选择合适的容器及算法就好,适当的使用缓存模式进行优化。但是要注意在细节上要符合JML规格,如涉及到计算的方法要注意JML的计算过程,防止出现误差;涉及到复杂逻辑的方法要注意JML的判断顺序,防止出现漏判误判。
对于异常类的处理实际比较简单,只需要按照要求在每个类内增加一个计数器就好。本次作业我没有单独增加计数器类,是因为觉得对于简单的异常类,直接复制粘贴建类也很方便,但是随着异常类的增多,管理起来也比较复杂,手动debug输出信息时也比较难找。在第二次作业中就出现了因为在第一次作业debug过程中留下的System.out语句没删,导致评测始终不通过的尴尬场景,这也提醒我们良好的架构不仅有利于读懂代码,还有利于后期的修改和维护,这在以后的代码协作过程中十分重要。