OO 第三单元总结
测试数据
本单元的测试主要采用了同学的评测机,仅自己手动构造了一些数据。可以根据JML中的各种正常和异常情况的前置条件分别构造数据,要特别注意一些边界情况,如Group的人数上限为1111。只要能按照JML中对所有情况的描述严格地进行测试,理论上就不会出现问题。这样直接根据JML构造测试数据需要对JML有正确的理解。JML虽然能够准确地表达规格,但同时也导致阅读起来非常复杂,容易导致对JML产生错误的理解。所以,还是通过与同学对拍这种方式实际效果更好。
架构设计
容器选择
第九次作业中,对Group类中的Person、Network类中的Person和Group使用HashSet来保存,但由于从中取出Person或Group时是按照其id查询,这样保存不能体现HashSet的优势,时间复杂度为O(n)。在后续作业中,将这些信息改为用以其id为key的HashMap保存,时间复杂度为O(1)。
后两次作业中Person类中messages需要保证有序存储,且插入时插入到首项。向首项插入时LinkedList的时间复杂度为O(1),而ArrayList为O(n),所以选择使用LinkedList存储。
Person类的acquaintance和value、Network类中的emojiIdList和emojiHeatList有明显的一一对应关系,采用HashMap存储。
图的维护
第九次作业中,在没有学习并查集之前,我使用了一个HashMap<Integer, HashSet<Integer>> blocks来保存连通块,其中key为连通块的id,value中保存一个连通块中所有人的id。另外有一个HashMap<Integer, Integer> personInBlock来保存所有人所在的连通块id。当新增人时,为其分配一个新的连通块id,当新增关系时,将两个连通块合并为一个,并修改其中一个连通块中所有人的所在连通块。
private final HashMap<Integer, Integer> personInBlock;
private final HashMap<Integer, ArrayList<Person>> blocks;
public void addRelation(int id1, int id2, int value)
throws PersonIdNotFoundException, EqualRelationException {
......
int block1num = personInBlock.get(person1.getId());
int block2num = personInBlock.get(person2.getId());
if (block1num != block2num) {
ArrayList<Person> block1 = blocks.get(block1num);
ArrayList<Person> block2 = blocks.get(block2num);
while (block2.size() > 0) {
Person person = block2.remove(0);
block1.add(person);
personInBlock.put(person.getId(), block1num);
}
blocks.remove(block2num);
}
从实际效果上来看,personInBlock这个HashMap比较像一个路径压缩过的并查集,但这样做会导致addRelation方法的时间复杂度增加。在学习了并查集之后最终改为使用并查集。
后两次作业中求最小生成树和最短路径都是在连通块中求,所以又改回了直接存储连通块的方式,为连通块建立了新类,其中存储一个id、处于同一连通块中的所有人和这些人之间的关系,连通块类中求最小生成树和最短通路分别使用克鲁斯卡尔算法和堆优化的迪杰斯特拉算法。
private final int id;
private final HashSet<Integer> people;
private final ArrayList<Relation> relations;
在MyNetwork类中仍然维护了连通块和人所在连通块的信息。
private final HashMap<Integer, Block> blocks;
private final HashMap<Integer, Integer> personInBlock;
求最小生成树和最短通路都直接调用Block类中的相关方法进行,这样做的想法是可以避免求最小生成树和最短通路时需要先找出所有与给定的人联通的人和这些人中所有的关系,但由于连通块中有存储了关系,连通块合并时除了复制所有的人还要复制关系,addRealtion更加耗时。在与同学对拍时发现这样做不如将Relation存储在Network中快。
代码性能问题和修复
首先是存储Group类中的Person、Network类中的Person和Group使用HashSet,最后改为了HashMap,时间复杂度由O(n)变为O(1),已经在上面说明过。
Group中的getValueSum方法中需要两层循环,外层循环是对Group中所有人的循环,此时如果内层循环也遍历Group中所有人,在Group中人数较多(如达到上限1111)且输入数据中有大量qgvs会导致超时,可以改为内层循环遍历Group中的所有人和人的好友中人数较少的一个。
public int getValueSum() {
int sum = 0;
for (Person person: people.values()) {
HashMap<Integer, Integer> acquaintance = ((MyPerson) person).getAcquaintance();
if (people.size() < acquaintance.size()) {
for (Person person1: people.values()) {
sum += person.queryValue(person1);
}
}
else {
for (Integer person1: acquaintance.keySet()) {
if (people.containsKey(person1)) {
sum += acquaintance.get(person1);
}
}
}
}
return sum;
}
由于输入条数的限制,社交网络中人数较多时关系数很少,是一个稀疏图。在这种情况下使用堆优化的迪杰斯特拉算法比普通迪杰斯特拉算法快很多。向堆中压入节点和取出堆顶节点的时间复杂度均为O(logn)。堆优化的迪杰斯特拉算法每次从堆中取出距离最小的点对与该点连接的点进行优化,并将这些点中可以再次优化的点放入堆中,而不需要普通迪杰斯特拉算法中遍历所有点找出距离最小的点。堆优化的迪杰斯特拉算法时间复杂度为O(mlogm)。
public int getShortestPath(int start, int end) {
Person endPerson = people.get(end);
PriorityQueue<PersonVertex> vertexes = new PriorityQueue<>();
HashSet<Person> arrived = new HashSet<>();
HashMap<Person, Integer> minDist = new HashMap<>();
for (Person person: people.values()) {
minDist.put(person, 2147483647);
}
vertexes.offer(new PersonVertex(people.get(start), 0));
while (!vertexes.isEmpty()) {
PersonVertex vertex = vertexes.poll();
if (vertex.getPerson() == endPerson) {
return vertex.getMinValue();
}
if (!arrived.contains(vertex.getPerson())) {
arrived.add(vertex.getPerson());
Set<Integer> acquaintance =
((MyPerson) vertex.getPerson()).getAcquaintance().keySet();
for (Integer personId: acquaintance) {
Person person = people.get(personId);
if (!arrived.contains(person)) {
int newDist = vertex.getMinValue() + person.queryValue(vertex.getPerson());
if (newDist < minDist.get(person)) {
minDist.put(person, newDist);
vertexes.offer(new PersonVertex(person, newDist));
}
}
}
}
}
return -1;
}
其他的一些琐碎的性能提升包括在Group中维护一个ageSum,降低getAgeMean和getAgeVar的复杂度等。
扩展
产品类,属性包括产品id、价格和生产商。
public interface Product {
/*@ public instance model int id;
@ public instance model int price;
@ public instance model Producer producer;
@*/
//@ ensures \result == id;
public /*@ pure @*/ Product getId();
//@ ensures \result == price;
public /*@ pure @*/ int getPrice();
//@ ensures \result == price;
public /*@ pure @*/ Producer getProducer();
}
广告消息为消息的子类,新增属性包括产品和广告费。
public interface AdvertisementMessage extends Message {
/*@ public instance model Product product;
@ public instance model int advertisingFee;
@*/
//@ ensures \result == product;
public /*@ pure @*/ Product getProduct();
//@ ensures \result == advertisingFee;
public /*@ pure @*/ int getAdvertisingFee();
}
Network中增加对产品信息的数组和一些辅助方法。
/*@ public instance model Product[] products;
@ public instance model int[] productAmount;
@*/
//@ ensures \result == (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
public boolean containsProduct(int id);
/*@ public normal_behavior
@ requires containsProduct(id);
@ ensures (\exists int i; 0 <= i && i < products.length; products[i].getId() == id &&
@ \result == products[i]);
@ also
@ public normal_behavior
@ requires !containsProduct(id);
@ ensures \result == null;
@*/
public Product getProduct(int id);
新增三种异常,ProductIdNotFoundException用于产品id不存在的情况,WrongPersonTypeException用于试图使错误种类的人进行某些操作的情况,如使非消费者的人购买物品,ProductSoldOutException用于试图在库存不足的情况下购买产品的情况。
public abstract class ProductIdNotFoundException extends Exception {
public abstract void print();
}
public abstract class WrongPersonTypeException extends Exception {
public abstract void print();
}
public abstract class ProductSoldOutException extends Exception {
public abstract void print();
}
addProductToProducer()方法用于增加产品数量,当不存在待加入产品时加入新产品。
/*@ public normal_behavior
@ requires containsProduct(product.get(Id));
@ assignable productAmount;
@ ensures products.length == \old(products.length) && productAmount.length == products.length;
@ ensures (\forall int i; 0 <= i && i < \old(products.length) && product != products[i];
@ \old(products[i]) == products[i] && \old(productAmount[i]) == amount[i]);
@ ensures (\exists int i; 0 <= i && i < \old(products.length) && product == products[i];
@ \old(productAmount[i]) == productAmount[i] - amount);
@ also
@ public normal_behavior
@ requires !containsProduct(product.get(Id));
@ assignable products, productAmount;
@ ensures products.length == \old(products.length) + 1 && productAmount.length == products.length;
@ ensures (\forall int i; 0 <= i && i < \old(products.length) && product != products[i];
@ \old(products[i]) == products[i] && \old(productAmount[i]) == amount[i]);
@ ensures products[\old(products.length)] == product && productAmount[\old(products.length)] == amount;
@*/
public void addProduct(Product product, int amount);
AdvertisementMessage为Message的子类,AdvertisementMessage的添加与发送可以合并到addMessage和sendMessage中。可以直接扩展addMessage和sendMessage方法的JML,addMessage中主要加入新的异常,sendMessage中加入Producer向Advertiser支付广告费。
//@ (message instanceof AdvertisementMessage) ==> message.getPerson1() instanceof Advertiser &&
addMessage的exceptional_behavior中加入:
/*@ signals (PersonWrongTypeException e) !(\exists int i; 0 <= i && i < messages.length;
@ messages[i].equals(message)) &&
@ ((message instanceof AdvertisementMessage) ==>
@ !message.getPerson1() instanceof Advertiser;
@*/
sendMessage的两个normal_behavior中加入:
/*@ ensures (\old(getMessage(id)) instanceof AdvertisementMessage) ==>
@ (\old(getMessage(id)).getPerson1().getMoney() == \old(getMessage(id).getPerson1().getMoney()) +
@ ((AdvertisementMessage)(\old(getMessage(id)))).getAdvertisingFee()) &&
@ (((AdvertisementMessage)\old(getMessage(id))).getProduct().getProducer().getMoney() ==
@ \old(((AdvertisementMessage)getMessage(id)).getProduct().getProducer().getMoney()) -
@ ((AdvertisementMessage)(\old(getMessage(id)))).getAdvertisingFee());
@*/
buyProduct()方法用于消费者购买产品。当库存不足时抛出异常。
/*@ public normal_behavior
@ requires contains(customerId) && contains(customerId) &&
@ getPerson(customerId) instanceof Customer &&
@ containsProduct(productId) &&
@ (\exists int i;
@ 0 <= i && i < products.length && products[i].getId() == productId;
@ productAmount[i] > 0);
@ assignable productAmount, people[*].money;
@ ensures (\forall int i; 0 <= i && i < products.length && products[i].getId() != productId;
@ \not_assigned(productAmount[i]));
@ ensures (\exists int i; 0 <= i && i < products.length && products[i].getId() == productId;
@ productAmount[i] == \old(productAmount[i]) - 1);
@ ensures (\forall int i; 0 <= i && i < people.length && people[i].getId() != customerId &&
@ people[i].getId() != getProduct(productId).getProducer().getId();
@ people[i].getMoney() == \old(people[i].getMoney()));
@ ensures (\exists int i; 0 <= i && i < people.length && people[i].getId() == customerId;
@ people[i].getMoney() == \old(people[i].getMoney()) - getProduct(productId).getMoney());
@ ensures (\exists int i; 0 <= i && i < people.length &&
@ people[i].getId() == getProduct(productId).getProducer().getId();
@ people[i].getMoney() == \old(people[i].getMoney()) + getProduct(productId).getMoney());
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(customerId);
@ signals (WrongPersonTypeException e) contains(customerId) &&
@ !getPerson(customerId) instanceof Customer;
@ signals (ProductIdNotFoundException e) contains(customerId) &&
@ getPerson(customerId) instanceof Customer &&
@ !containsProduct(productId);
@ signals (ProductSoldOutException e) contains(customerId) &&
@ getPerson(customerId) instanceof Customer &&
@ containsProduct(productId) &&
@ (\exists int i;
@ 0 <= i && i < products.length && products[i].getId() == productId;
@ productAmount[i] == 0);
@*/
public void buyProduct(int customerId, int productId) throws PersonIdNotFoundException
, WrongPersonTypeException, ProductIdNotFoundException, ProductSoldOutException;
学习体会
一些情况下,选择何种容器来存储信息对运行时间的影响很大,这就需要对不同容器的实现机制、适合用于何种情况和相关方法的时间复杂度有一定的了解。