BUAA_OO_第三单元总结
自测过程中如何利用JML规格来准备测试数据的分析
本单元完全是依靠同学的测试数据进行测试的,自己没有准备测试数据。我猜测应该主要是根据JML规格的requires部分和signal的布尔表达式部分进行测试数据的生成。
本单元的架构设计,图模型构建和维护策略分析
架构设计
本单元的架构设计,官方包基本已经建好了,我所做的能够算做架构设计的就是选择了一下数据结构。最大的特点就是采用了较多的HashMap。
MyNetwork
public class MyNetwork implements Network {
private HashMap<Integer,Person> people = new HashMap<>();
private ArrayList<Group> groups = new ArrayList<>();
private HashMap<Integer,Message> messages = new HashMap<>();
private HashMap<Integer,Integer> emojiIdheat = new HashMap<>();
MyPerson
public class MyPerson implements Person {
private int id = 0;
private String name = "";
private int age = 0;
private HashMap<Person,Integer> acquainvalue = new HashMap<>();
private int money = 0;
private int socialValue = 0;
private LinkedList<Message> messages = new LinkedList<>();
MyGroup
public class MyGroup implements Group {
private int id = 0;
private HashSet<Person> people = new HashSet<>();
private int valuesum = 0;
Hash结构提高contains类方法,get类方法的效率到O(1)。
图模型建构
-
第一次作业求连通分量采用了并查集
实际上一开始也尝试过DFS,然后和同学对拍发现时间过长,就改为了并查集,并查集真是效率又高又好写,缺点可能就是不好应付删边的情况。并查集的实现基本如下。
private HashMap<Integer,Integer> faid = new HashMap<>(); private int find(int id);//查找根节点 private void merge(int id1,int id2);//合并两颗树
-
第二次作业求最小生成树的权值。
这次作业我没有采用贪心算法和Prim算法,而是收到上一单元的启发,我想要去维护一个最小生成树,再每次加边的时候维护一下这条边的所在的连通分量的最小生成树。基本思路是
(i)假设加边之后恰好连通两个连通分量,那么两颗最小生成树的边加上新加的这条边就是两个连通分量合并以后对应的连通分量的最小生成树。
(ii)假设加的边的两个端点都在一个连通分量内,就需要更新一下所在的连通的分量对应的最小生成树,假设两个端点名字是A,B。更新方法就是,先从原来的最小生成树中找到A到B的路径,显然最小生成树中只有一条这样的路径。然后将这条路径里面最长的边,也就是权值最大的边,称为m,与新加的边(称为new)的权值进行比较,如果m的权值比new的大,那么将m从最小生成树中剔除,把new加入,如果m的权值比new小或等于,就维持最小生成树的边集不变。
这个更新的算法其实还是利用了贪心算法的思想。采用这种方式qlc的复杂度可以降到O(1)(只需要维护最小生成树的同时顺便维护一下最小生成树的边权值和)。但是addRelation的复杂度就增大了,因为在最小生成树中搜索路径需要花费O(n)时间(n为最小生成树节点数)
-
第三次作业求最短路径,采用的是Dijistra算法。
这次作业用PriorityQueue进行了维护。
Queue<Edge> edges = new PriorityQueue<>(); public class Edge implements Comparable<Edge> { private int to; private int cost;
edge存的是节点id,和到这个节点的路径长度开销
edges.add(new Edge(tempto,tempcost)); Edge tempedge = edges.poll();
edges会维护保持路径开销最小的edge,在堆顶,可以通过poll方法弹出获得堆顶的edge。就方便Djistra扩展点集(指已经发现了最短路径的点的集合)
维护策略
第一次作业
主要是在addPerson和并查集merge时维护了最小联通分量的个数
private int blocks = 0;
public void addPerson(Person person) throws EqualPersonIdException {
if (!contains(person.getId())) {
.......
blocks++;
........
} else {
throw new MyEqualPersonIdException(person.getId());
}
}
private void merge(int id1,int id2) {
......
if (rootid1 != rootid2) {
blocks--;
...........
} else {
............
}
}
public int queryBlockSum() {
return blocks;
}
第二、三次作业
主要维护了qgvs的结果,在addrealtion和addPersontoGroup和delPersontoGroup时维护一下。
private int valuesum = 0;
public void addPerson(Person person) {
if (!hasPerson(person)) {
for (Person person1 : people) {
if (person1.isLinked(person)) {
valuesum += (2 * person.queryValue(person1));
}
}
people.add(person);
}
}
public void delPerson(Person person) {
if (hasPerson(person)) {
people.remove(person);
for (Person person1 : people) {
if (person1.isLinked(person)) {
valuesum -= (2 * person.queryValue(person1));
}
}
}
}
public void addrelation(Person person1,Person person2) {
if (hasPerson(person1) && hasPerson(person2)) {
valuesum += (2 * person1.queryValue(person2));
}
}
代码实现出现的性能问题和修复情况
这三次作业的强测都没有出现问题
只在互测中出现了一次问题
在于getAgeVar方法的复杂度
\\bug version
public int getAgeVar() {
if (people.size() == 0) {
return 0;
} else {
int sum = 0;
for (Person person : people) {
sum += (person.getAge() - getAgeMean()) * (person.getAge() - getAgeMean());
}
return sum / people.size();
}
}
\\fix bug version
public int getAgeVar() {
if (people.size() == 0) {
return 0;
} else {
int sum = 0;
int mean = getAgeMean();
for (Person person : people) {
sum += (person.getAge() - mean) * (person.getAge() - mean);
}
return sum / people.size();
}
}
问题就是我在for循环中访问了getAgeMean()这个O(n)的方法,使getAgeVar的复杂度达到了O(n^2)。实际上只需要开始的时候访问一次存下来就可以。这是照抄JML规格导致的问题。
Network扩展及相应的JML规格
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买
-- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息 - Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等
请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
MyNetwork:
增加广告的方法:
广告视为一种Message,只需要加一个Exception,略去部分和第十一次作业的规格相同,广告应该具getProductId()方法,也就是获取所广告的产品的id。
/*@ public normal_behavior
............
@ also
@ public exceptional_behavior
...........
@ signals (EqualPersonIdException e) ........
@ signals (ProductIdNotFoundException e) !(\exists int i; 0 <= i && i <
@ messages.length; messages[i].equals(message)) &&
@ ((message instanceof EmojiMessage) ==>
@ containsEmojiId(((EmojiMessage) message).getEmojiId())) &&
@ ((message.getType() == 0) ==> (message.getPerson1() !=
@ message.getPerson2())) && (message instanceof AdvertisementMessage) &&
@ !countanisProductId(message.getProductId);
@*/
public void addMessage(Message message) throws
EqualMessageIdException, EmojiIdNotFoundException,
EqualPersonIdException, ProductIdNotFoundException;
购买方法:
因为购买就是通过Advertiser给相应的Producer发一个购买消息。所以改写sendMessage方法。发送这个消息不只是改变了消息列表,还应该应该改变了钱数。只要修改一下sendMessage的assures部分。
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages, emojiHeatList;
@ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
@ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money;
@ assignable getMessage(id).getCustomer().money;
......................
@ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
@ (\old(getMessage(id)).getCustomer().getMoney() ==
@ \old(getMessage(id).getCustomer().getMoney()) - ((PurchaseMessage)\old(getMessage(id))).getPrice() &&
@ \old(getMessage(id)).getPerson2().getMoney() ==
@ \old(getMessage(id).getPerson2().getMoney()) + ((PurchaseMessage)\old(getMessage(id))).getPrice());
@ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage) || (\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(people[*].money));
..................
public void sendMessage(int id) throws RelationNotFoundException,
MessageIdNotFoundException, PersonIdNotFoundException;
关注广告的方法:
Customer应该有一个Advertisement[] intersted;属性
/*@ public normal_behavior
@ requires containsPerson(personId) && containsAdvertisement(advertisementId)
@ assignable getPerson(personId).intersted;
@ ensures (\forall int i; 0 <= i && i <
@ \old(getPerson(personId).getIntersted().size());
@ getPerson(personId).getIntersted().get(i+1) ==
@ \old(getPerson(personId).getIntersted().get(i)));
@ ensures
@ getPerson(personId).getIntersted().get(0).equals(getMessage(advertisementId));
@ ensures getPerson(personId).getIntersted().size() ==
@ \old(getPerson(personId).getIntersted().size()) + 1;
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(advertisementId);
@ signals (PersonIdNotFoundException e) containsMessage(id) &&
@ !containsPerson(personId)
public void focusad(int personId,int advertisementId) throws
MessageIdNotFoundException, PersonIdNotFoundException;
本单元学习体会
本单元的体会就是本单元的作业还是比较轻松的,因为之前的作业,我还需要花很多时间去进行一些设计,这次作业相当于已经把结构都设计好了,每个JML规格相当于又替我们设计好了方法,所以省去了许多设计费脑的时间,图论算法什么的上网查查也很容易模仿学会。可能时间花最多还是看JML规格。
这个单元主要还是学习到了写JML应该注意的一些地方,JML的requires,assignable,assures,assures必须把确定好assignable的变量的变化,就是确保程序员不能乱改assignable的变量。然后就是signal的每个情况都是互斥的。