BUAA_OO 第三单元总结
一、架构设计
本单元的三次作业核心是增量开发一个多人聊天系统,由于是迭代开发,所以在此处主要就本单元第三次作业进行分析。
容器选择
为了降低整体的时间复杂度,且本次作业拥有互不相同的Person
、Group
等对象拥有互不相同的id的特点,所以在用到容器时许多我都是优先用Hashmap
,这样使我每次查找操作都快了很多。例如:
private HashMap<Integer, Person> people = new HashMap<>();
private HashMap<Integer, Group> groups = new HashMap<>();
private HashMap<Integer, Message> messages = new HashMap<>();
private HashMap<Integer, Integer> emo = new HashMap<>();
private HashMap<Person, Integer> acquaintance = new HashMap<>();
private HashMap<Integer, Integer> value = new HashMap<>();
有一次特殊的地方就是MyPerson
类中我存储message用了 LinkedList
容器,这是因为要求将新来的消息存到容器的头部,如果选择其它容器,一般都会插到尾部或位置不定,所以我选用了 LinkedList
容器。
private LinkedList<Message> messages = new LinkedList<>();
在sendIndirectMessage
函数中,为了更好应用dijkstra
算法,我又用了优先队列 PriorityQueue
来存储距离,用 HashSet
来存储已经访问过的点。
private PriorityQueue<Distance> pq = new PriorityQueue<>();
private HashSet<Integer> visit = new HashSet<>();
时间复杂度较高方法的总结
在本单元作业中,大部分方法的复杂度都不高,简单实现JML规格即可,再加上选择了效率高的容器,所以可以有很好的性能。但如果仅仅如此,还有以下方法的时间复杂度很高,需要用算法进行优化。它们是:
MyNetwork类中的:
-
boolean isCircle(int id1, int id2)
-
int queryBlockSum()
-
int queryLeastConnectionid)
: -
int sendIndirectMessage(int id)
MyNetwork类中的:
-
int getAgeVar()
-
int getValueSum()
对应优化策略
对于boolean isCircle(int id1, int id2)
我采用了并查集算法。用了private HashMap<Integer, Integer> parent = new HashMap<>();
和private HashMap<Integer, Integer> rank = new HashMap<>();
两个容器。在每进入一个人时,将它加入parent中,每添加关系时,将两个人进行merge,最后通过find函数判定是否二者处于同一连通分支(即是否isCircle)。函数具体实现如下:
private int find(int p) {
int tmp = p;
while (tmp != parent.get(tmp)) {
tmp = parent.get(tmp);
}
return tmp;
}
public void merge(int p, int q) {
int proot = find(p);
int qroot = find(q);
if (proot == qroot) {
return;
}
blockSum--;
if (rank.get(proot) < rank.get(qroot)) {
parent.put(proot, qroot);
} else if (rank.get(proot) > rank.get(qroot)) {
parent.put(qroot, proot);
} else {
parent.put(proot, qroot);
int tmp = rank.get(qroot);
rank.put(qroot, tmp + 1);
}
}
细心的同学可能已经注意到在上面的merge
函数中我同时维护了blocksum
这一变量,这就是为了int queryBlockSum()
函数,该函数就是查询连通分支的个数,只需在在每进入一个人时将其加一,在merge
时将其减一,即可使其变为O(1)
复杂度。
对于int queryLeastConnectionid)
:我采用了利用kruskal算法实现最小生成树的查询。具体实现如下:
public /*@ pure @*/ int queryLeastConnection(int id) throws PersonIdNotFoundException {
if (!contains(id)) {
throw new MyPersonIdNotFoundException(id);
} else {
int sum = 0;
ArrayList<Edge> tmpEdges = new ArrayList<>();
HashSet<Integer> tmpSet = new HashSet<>();
for (int i = 0; i < edges.size(); i++) {
if (find(edges.get(i).getId1()) == find(id)) {
tmpEdges.add(edges.get(i));
tmpSet.add(edges.get(i).getId1());
tmpSet.add(edges.get(i).getId2());
}
}
for (Integer key : tmpSet) {
parent2.put(key, key);
rank2.put(key, 1);
}
Collections.sort(tmpEdges);
int j = 0;
for (int i = 0; i < tmpEdges.size(); i++) {
if (find2(tmpEdges.get(i).getId1()) != find2(tmpEdges.get(i).getId2())) {
merge2(tmpEdges.get(i).getId1(), tmpEdges.get(i).getId2());
sum += tmpEdges.get(i).getValue();
j++;
}
if (j == (tmpSet.size() - 1)) {
break;
}
}
return sum;
}
}
对于int sendIndirectMessage(int id)
函数,我采用了迪杰斯特拉算法+堆优化,通过java提供的PriorityQueue
实现堆优化。具体实现如下:
private int dijkstra(Person person1, Person person2) {
pq.clear();
minDis.clear();
visit.clear();
minDis.put(person1.getId(), 0);
pq.add(new Distance(0, person1));
while (!pq.isEmpty()) {
Distance dis = pq.poll();
Person person = dis.getPerson();
int distance = dis.getDistance();
if (person.equals(person2)) {
return distance;
}
if (visit.contains(person.getId())) {
continue;
}
visit.add(person.getId());
HashMap<Person, Integer> acquaintance = ((MyPerson) person).getAcquaintance();
for (Person key : acquaintance.keySet()) {
if (minDis.get(key.getId()) == null) {
minDis.put(key.getId(), INF);
}
int oriDistance = minDis.get(key.getId());
if (oriDistance > distance + person.queryValue(key)) {
int newDistance = distance + person.queryValue(key);
minDis.put(key.getId(), newDistance);
pq.add(new Distance(newDistance, key));
}
}
}
return -1;
}
对于int getAgeVar()
和int getValueSum()
其优化的方法就是维护变量。
前者我维护了 ageSum
和ageVar
两个变量,最后运用 (ageVar - 2 * getAgeMean() * ageSum + people.size()*getAgeMean() * getAgeMean()) / people.size()
式子进行计算。
后者我维护了valueSum
这一变量。
异常处理
这里我并没有实现一个计数类,因为我觉得使用一个静态变量sum
进行记录即可。
二、测试数据准备
-
利用JML规格来准备测试数据:
我主要利用了JML规格中的
requires
语句和signals
语句来生成测试数据,例如下面的addGroup方法,我就会让生成的数据覆盖groups中有当前有加的group和没有当前有加的group两种情况。/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < groups.length; groups[i].equals(group));
@ assignable groups;
@ ensures groups.length == \old(groups.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(groups.length);
@ (\exists int j; 0 <= j && j < groups.length; groups[j] == (\old(groups[i]))));
@ ensures (\exists int i; 0 <= i && i < groups.length; groups[i] == group);
@ also
@ public exceptional_behavior
@ signals (EqualGroupIdException e) (\exists int i; 0 <= i && i < groups.length;
@ groups[i].equals(group));
@*/
public void addGroup(/*@ non_null @*/Group group) throws EqualGroupIdException;
此外我还利用了JML规格中的后置语句ensure
作为测试结果正不正确的依据。
-
防止公测部分发生bug,我采用了对自己的代码运用随机生成的测试数据进行测试,同时与小伙伴们进行对拍的方式。
三、BUG分析
本单元的三次作业由于运用了上文所述的策略后在公测和互测中均没有出现bug,并且运行时间都远远小于公测测试点要求的最大运行时间。
四、对NetWork进行扩展
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
答:
可以新增三个Person的子类Advertiser、Producer和Customer,新增两个Message的子类Advertisement和Product,并增加几个异常类
public class Advertiser extends MyPerson;
public class Producer extends MyPerson;
public class Customer extends MyPerson;
public class Advertisement extends Message
public class Product extends Message
核心业务功能的接口方法:
-
发送广告:
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser && people[i].containsAdvertisement(advertisementId));
@ assignable people[*].messages
@ ensures (\forall int i; 0 <= i && i < people.length; (getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length) + 1 && people[i].messages[0] == \old(getPerson(id).getAdvertisement(advertisementId)) && (\forall int j; 1 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j - 1]))));
@ ensures (\forall int i; 0 <= i && i < people.length; !(getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length && (\forall int j; 0 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j]))));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) (\forall int i; 0 <= i && i < people.length; people[i].getId() != id || (people[i].getId() == id && !people[i] instanceof Advertiser));
@ signals (AdvertisementIdNotFoundException e) (\exists int i; 0 <= i && i < people.length && people[i].getId() == id && people[i] instanceof Advertiser; !people[i].containsAdvertisement(advertisementId));
@*/
public void sendAdvertisement(int id, int advertisementId) throws PersonIdNotFoundException, AdvertisementIdNotFoundException; -
查询商品价格
/*@ public normal_behavior
@ requires containsProduct(id);
@ ensures \result == getProduct(id).getPrice();
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(id);
@*/
public /*@ pure @*/ int queryProductPrice(int id) throws ProductIdNotFoundException; -
增加商品
/* @ public normal_behavior
@ assignable products
@ requires !containsProduct(id);
@ ensures products.length == \old(products.length) + 1;
@ ensures containsProduct(id);
@ ensures (\forall int i; 0 <= i && i < \old(products.length)
(\exists int j; 0 <= j && j < products.length; products[j] == \old(productList[i])));
@ also
@ public exceptional_behavior
@ signals (EqualProductIdException e) containsProduct(id);
public void addProduct(Product product) throws EqualProductIdException;
五、学习体会
经过这一单元的学习,我对JML规格有了一个较好的掌握,知道了它存在的意义,也知道了JML规格并不是死板的要求而是有很大的灵活性的,例如规格中出现了person[i]
这种文字,并不是要求一定要用数组来存储person,而是可以随意选择容器。除了对JML规格的掌握之外,我又对数据结构的图的算法有了更好的理解,新学习了并查集算法,并巩固学习了用kruskal算法实现最小生成树,还在之前学的迪杰斯特拉算法基础上进一步学习了它的堆优化处理,对我算法能力的加强有了很大帮助。此外我还掌握了更多测试的方法与技巧,可谓是收获满满。在此非常感谢为这一单元备课、出题呕心沥血的老师和助教们!同时这一单元三次作业的零失误,也标志着经过我的不断努力终于弥补了假期没能预习OO所带来的缺陷,我感到很开心,但也更加深刻提醒了我,假期真的不能一点也不学习,以前这样可以侥幸混过,这学期的OO确实给了我当头一棒,让我在学期快要过半的时候才回归正轨,以后不能这么干了!