OO_Unit3_单元总结
OO_Unit3_单元总结
Part1 架构设计和图模型构建
架构分析
本单元三次作业的整体架构和关键方法已经由JML规格给出。我们可以通过课程组给出的JML规格清晰地整理出各个类之间的关系,我们拿第三次作业的JML为例:
//NetWork
/*@ public instance model non_null Person[] people;
@ public instance model non_null Group[] groups;
@ public instance model non_null Message[] messages;
@ public instance model non_null int[] emojiIdList;
@ public instance model non_null int[] emojiHeatList;
@*/
//Person
/*@ public instance model int id;
@ public instance model non_null String name;
@ public instance model int age;
@ public instance model non_null Person[] acquaintance;
@ public instance model non_null int[] value;
@ public instance model int money;
@ public instance model int socialValue;
@ public instance model non_null Message[] messages;
@*/
//Group
/*@ public instance model int id;
@ public instance model non_null Person[] people;
@*/
//Message
/*@ public instance model int id;
@ public instance model int socialValue;
@ public instance model int type;
@ public instance model non_null Person person1;
@ public instance model nullable Person person2;
@ public instance model nullable Group group;
@*/
NetWork
类管理着所有的Person
、Group
、Message
、Emoji
。Group
类中管理着属于自己的Person
。Person
类中则是管理着与自己有直接联系的人也就是acquaintance
和自己接受到的信息messages
。当然JML规格只是一种形式化的表示,具体我们选用哪种数据结构来组织数据是不受约束的。
图模型的构建
在我们实现的社交网络中,NetWork
为图,Person
为节点,relation
为边。方法addPerson
为整个图增加节点,addRelation
为节点间添加带权值的边。
无向图中两节点的可达问题——并查集
在第一次作业中,NetWork
类中的isCircle
方法为给定两个节点判断这两个结点是否可达。在这里使用了并查集的数据结构并进行了rank优化和路径压缩优化。
并查集的思想是用一个数组表示整片森林,树的根节点唯一标识了一个集合,我们只需要找到某个元素的树根就可以确定它在哪个集合中,同处于一个集合中的两个节点是可达的。
//添加节点时,对节点的parent和rank信息进行初始化
public void addPerson(Person person) throws EqualPersonIdException {
if (!people.containsKey(person.getId())) {
people.put(person.getId(), person);
parent.put(person.getId(),person.getId());
rank.put(person.getId(),1);
blockSum++;
}
else {
throw new MyEqualPersonIdException(person.getId());
}
}
//寻找节点的根节点
//寻找的过程中进行路径压缩
private int find(int id) {
int tempId = id;
if (tempId != parent.get(tempId)) {
parent.put(tempId,find(parent.get(tempId)));
}
return parent.get(tempId);
}
//对两个集合进行合并
private void unionElements(int id1,int id2) {
int id1Root = find(id1);
int id2Root = find(id2);
if (id1Root == id2Root) {
return;
}
if (rank.get(id1Root) < rank.get(id2Root)) {
parent.put(id1Root,id2Root);
} else if (rank.get(id1Root) > rank.get(id2Root)) {
parent.put(id2Root,id1Root);
} else {
parent.put(id1Root,id2Root);
int prev = rank.get(id2Root);
rank.put(id2Root,prev + 1);
}
blockSum--;
}
连通图的最小生成树问题——普里姆算法
在第二次作业中,NetWork
类中的queryLeastConnection
方法要求我们找出某一节点所在的连通分支的最小生成树。由于整个图不一定是连通的,所以我采用了Prim算法,在寻找最短边时采用了优先队列的数据结构。
public static int doPrim(HashMap<Integer, Person> people,int id) {
//普里姆算法辅助数据结构
HashSet<Integer> visited = new HashSet<>(); //已经访问过的结点
HashMap<Integer,Integer> distance = new HashMap<>(); //<结点id,距离>
int res = 0;
//Pair<距离,结点id> 二元组
Pair<Integer,Integer> tmp;
//优先队列(小顶堆)
PriorityQueue<Pair<Integer,Integer>> minHeap =
new PriorityQueue<>((pair1,pair2) -> pair1.getKey() - pair2.getKey());
minHeap.add(new Pair<>(0,id));
distance.put(id,0);
while (!minHeap.isEmpty()) {
tmp = minHeap.poll();//弹出距离最小点
if (visited.contains(tmp.getValue())) {
continue;
}
visited.add(tmp.getValue());
res += tmp.getKey();
for (Integer acquaintId :
((MyPerson)people.get(tmp.getValue())).getAcquaintance().keySet()) {
if (!visited.contains(acquaintId)) {
int newDistance =
people.get(tmp.getValue()).queryValue(people.get(acquaintId));
if (!distance.containsKey(acquaintId) ||
(distance.get(acquaintId) > newDistance)) {
distance.put(acquaintId,newDistance);
minHeap.add(new Pair<>(newDistance,acquaintId));
}
}
}
}
return res;
}
求解两可达结点的最短路径——克鲁斯卡尔算法
在第三次作业中,NetWork
类中sendIndirectMessage
方法要求我们找出某两个可达节点间的最短路径。采用Dijkstra算法,具体的实现方法和Prim类似,不同的是在Prim算法中我们的优先队列每次弹出的与已生成的树距离最短的节点,而在Dijkstra算法中优先队列每次弹出的是与起点节点距离最短的结点。
Part2 性能问题及修复状况
在本单元作业中,最容易被Hack的问题就是CPU_TIME_LIMIT_EXCEED
。如果在公测或者其他问题上被Hack往往是因为没有仔细阅读JML规格而产生的。在最初考虑性能问题的时候,我只把目光放在了需要运用图算法的方法上,诸如:isCircle
、queryLeastConnection
、sendIndirectMessage
,而忘记考虑其他算法的时间复杂度导致在第二次作业中被Hack了3次。
bug原因:大量调用qgav导致超时,因为先前在MyGroup类中实现的方法
getAgeVar
和getAgeMean
都是按照规格的算法实现的,时间复杂度都为O(n)
,在getAgeVar
方法中又会调用getAgeMean
方法使时间复杂度变为O(n^2)
,大量调用getAgeMean
导致了超时。
修复方法:在MyGroup类中维护了两个变量ageSum
和ageSquSum
,在往Group中添加人或者删除人的时候更新这两个变量。getAgeVar
和getAgeMean
方法中直接使用这两个变量来得出结果即可。
bug原因:大量调用qgvs导致超时。因为先前在MyGroup类中实现的
getValueSum
方法完全是照抄规格,所以每次调用getValueSum
方法都需要对group中的person进行双重遍历来计算valueSum,时间复杂度为O(n^2)。当数据比较极端的时候,比如给某个group加满1111个人,之后再对这个group调用将近三千次的getValueSum
就会导致超时。
修复方法:在MyGroup类中维护变量valueSum
。当进行往group中添加person、从group中删除person和给person1和person2之间添加关系的时候对valueSum
变量进行维护。当调用getValueSum
方法时直接返回valueSum
值即可。
bug原因 :大量使用qbs指令导致超时。在实现
isCircle
方法的时候使用了并查集,所以在queryBlockSum
方法中遍历所有person来数根结点的个数,时间复杂度为O(n),在MyNetwork中大量添加person并大量调用queryBlockSum
方法会导致超时。
修复方法:在MyNetwork中维护变量blockSum
。在往添加person、合并两个集合的时候更新这个变量。在调用queryBlockSum
方法的时候直返回blockSum
这个变量即可。
Part3 测试数据准备
本单元的测试数据我主要采用随机生成和手动构造的方式。
- 随机生成主要通过编写数据生成器随机生成大量数据,并通过与他人的程序对拍来验证正确性。
- 手动构造则是针对某一特定方法的时间复杂度、边界条件来手动构造数据。测试方法在性能上是否满足要求、边界条件是否考虑完善。
Part4 架构扩展
假设出现了几种不同的Person
Advertiser:持续向外发送产品广告
Producer:产品生产商,通过Advertiser来销售产品
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买
-- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等
请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
- 新增接口
Product
public interface Product {
/*@ public instance model int id;
@ public instance model int cost;
@*/
//@ ensures \result == id;
public /*@ pure @*/ int getId();
//@ ensures \result == cost;
public /*@ pure @*/ int getCost();
}
- 新增接口
Advertiser
、Producer
、Customer
继承自Person
public interface Advertiser extends Person {
}
public interface Producer extends Person {
//@ public instance model non_null Product[] products;
//@ public instance model non_null int[] productsNum;
}
public interface Customer extends Person {
//@ public instance model non_null Product[] preferenceProducts;
//@ public instance model non_null Product[] ownedProducts;
//@ public instance model non_null int[] ownedProductsNum;
}
- 新增接口
AdvertiseMessage
、PurchaseMessage
继承自Messge
public interface AdvertiserMessage extends Message {
//@ public instance model Product advertisedProduct;
//ensures \result == advertisedProduct;
public Product /*@ pure @*/ getAdvertisedProduct();
}
public interface PurchaseMessage extends Message {
//@ public instance model Product purchasedProduct;
//ensures \result == purchasedProduct;
public Product /*@ pure @*/ getPurchasedProduct();
}
NetWork
接口新增属性和接口
public interface NetWork {
/*@...
@public instance model non_null Product[] products;
@public instance model non_null int[] productSaleList;
@*/
//@ ensures \result == (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
public /*@ pure @*/ boolean containsProduct(int id);
//发送广告信息
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id) instanceof AdvertiseMessage &&
@ getMessage(id).getType() == 0 &&
@ getMessage(id).getPerson1() instanceof Advertiser &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages;
@ assignable getMessage(id).getPerson1().socialValue;
@ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue;
@ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
@ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
@ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (NotAdvertiesMessageException e) containsMessage(id) && !(getMessage(id) instanceof AdvertiseMessage );
@ signals (NotAdvertierException e) containsMessage(id) && (getMessage(id) instanceof AdvertiseMessage ) &&
@ getMessage(id).getType() == 0 && !(getMessage(id).getPerson1() instanceof Advertiser);
@ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
@ !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
@*/
public void sendAdvertiseMessage(int id) throws MessageIdNotFoundException, NotAdvertiesMessageException, NotAdvertierException,RelationNotFoundException, PersonIdNotFoundException;
//设置消费者偏好
/*@ public normal_behavior
@ requires contains(id1) && containsProduct(id2) &&
@ getPerson(id1) instanceof Customer &&
@ !((Customer) getPerson(id1)).preferenceProducts.contains(getProduct(id2));
@ assignable ((Customer) getPerson(id1)).preferenceProducts;
@ ensures (\forall int i; 0 <= i && i <= \old(((Customer) getPerson(id1)).preferenceProducts.length);
@ (\exists int j; 0 <= j && j < ((Customer) getPerson(id1)).preferenceProducts.length;
@ ((Customer) getPerson(id1)).preferenceProducts.equals(\old(((Customer) getPerson(id1)).preferenceProducts))));
@ ensures (\exists int i; 0 <= i && i < ((Customer) getPerson(id1)).preferenceProducts.length;
@ ((Customer) getPerson(id1)).preferenceProducts[i] == getProduct(id2));
@ ensures ((Customer) getPerson(id1)).preferenceProducts.length ==
@ \old(((Customer) getPerson(id1)).preferenceProducts.length) + 1;
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id1);
@ signals (ProductIdNotFoundException e) contains(id1) && !containsProduct(id2);
@ signals (EqualProductIdException e) contains(id1) && containsProduct(id2) &&
@ getPerson(id1) instanceof Customer &&
@ ((Customer) getPerson(id1)).preferenceProducts.contains(getProduct(id2));
@*/
public void addPreference(int id1, int id2) throws PersonIdNotFoundException, ProductIdNotFoundException, EqualProductIdException;
//发送购买消息
/*@ public normal_behavior
@ requires contains(id1) && getPerson(id1) instanceof Customer;
@ requires containsMessage(id2) && getMessage(id2) instanceof PurchaseMessage;
@ requires getPerson(id1).containsPreferenceProducts(((PurchaseMessage)getMessage(id2)).purchasedProduct);
@ requires getPerson(id1).hasAdvertisement(((PurchaseMessage)getMessage(id2)).purchasedProduct) &&
@ getPerson(id1).getAdvertisement(((PurchaseMessage)getMessage(id2)).purchasedProduct).getPerson1().equals(getMessage(id2).getPerson1());
@ assignable messages;
@ assignable getMessage(id2).getPerson1().socialValue;
@ assignable getMessage(id2).getPerson2().socialValue;
@ assignable productSaleList;
@ ensures !containsMessage(id2) && messages.length == \old(messages.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
@ ensures \old(getMessage(id2)).getPerson1().getSocialValue() ==
@ \old(getMessage(id2).getPerson1().getSocialValue()) + \old(getMessage(mid)).getSocialValue() &&
@ \old(getMessage(id2)).getPerson2().getSocialValue() ==
@ \old(getMessage(id2).getPerson2().getSocialValue()) + \old(getMessage(mid)).getSocialValue();
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id2).getPerson2().getMessages().size());
@ \old(getMessage(id2)).getPerson2().getMessages().get(i+1) == \old(getMessage(id2).getPerson2().getMessages().get(i)));
@ ensures \old(getMessage(id2)).getPerson2().getMessages().get(0).equals(\old(getMessage(id2)));
@ ensures \old(getMessage(id2)).getPerson2().getMessages().size() == \old(getMessage(id2).getPerson2().getMessages().size()) + 1;
@ ensures (\exist int i; 0 <= i && i < products.length && products[i] == ((PurchaseMessage)getMessage(id2)).purchasedProduct;
@ productSaleList[i] == \old(productSaleList[i]) + ((PurchaseMessage)getMessage(id2)).purchasedProduct.getCost())
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id1);
@ signals (NotCustomerException e) contains(id1) && !(getPerson(id1) instanceof Customer)
@ signals (MessageIdNotFoundException e) !containsMessage(id2);
@ signals (NotPurchaseMessageException e) containsMessage(id2) && !(getMessage(id2) instanceof PurchaseMessage);
@*/
public void sendPurchaseMessage(int id1, int id2) throws MessageIdNotFoundException, NotPurchaseMessageException, PersonIdNotFoundException,NotCustomerException;
Part5 学习体会
本单元的练习要求我们严格遵照JML规格来进行“契约式”编程。在进行方法编写时,只要给出的数据符合前置条件的要求,我们就应给出符合后置条件的结果。这种严谨的编程过程和编程完成后进行的全覆盖单元测试给我以后的编程提供一个良好的范式。
在充分理解规格的同时,如何更便捷的组织数据,降低运算的时间复杂度也是需要我们考虑的地方。在图模型的构建和运算中,运用并查集、优先队列等数据结构可以有效降低时间复杂度,我也由此意识到一个好的数据结构往往可以简化操作提高效率。