面向对象第三单元总结
〇.单元总览
本单元的主要目的是建立一个社交通信网络,代码已由课程组给出,我们只需根据给出的jml
规格实现接口即可,主要包括人,群组,网络和各种消息以及各种可能会产生的异常,部分方法涉及图论的相关知识。
一.容器选择
在jml规格提供的可选属性中,一般提供的都是使用数组类型存储数据,但为了实现更高的查找效率以及将id
和对应数据建立起对应的联系,我最终选择使用HashMap
这一类型。
Group
private HashMap<Integer,Person> people;
//建立id-person间的对应关系
Network
private HashMap<Integer,Person> people;
//建立id-person间的对应关系
private HashMap<Integer,Group> groups;
//建立id-group间的对应关系
private HashMap<Integer,Message> messages;
//建立id-message间的对应关系
private HashMap<Integer,Integer> emojis;
//建立id-emoji间的对应关系
Person
private HashMap<Person, Integer> personMap;
//存储当前person和其他person间的权值
Exception
private static HashMap<Integer, Integer> map = new HashMap<>();
//建立触发异常的id-当前id触发次数间的对应关系
二.性能优化
本单元作业不用在架构设计部分花费太多的功夫(毕竟都已经给出了jml
规格),但在性能优化部分却是有很多东西可以说道。
qgvs指令
qgvs
指令是为了获取一个group
内所有person
两两间的value
总和,若是直接使用二重循环相加,在大规模调用qgvs
指令时会导致程序运行时间过长,产生超时错误。
为了解决这个问题,我们可以在Group
类中维护一个ValueSum
的属性,用来实时更新value
值的总和,需要注意的是这个值需要在加人,删人和添加关系时进行更新。
@Override
public void addPerson(Person person) {
for (Person p : people.values()) {
this.valueSum = this.valueSum + person.queryValue(p) * 2;
}
this.people.put(person.getId(), person);
}
//加人时更新
public void updateValueSum(int value) {
this.valueSum = this.valueSum + value * 2;
}
//添加关系时更新
@Override
public void delPerson(Person person) {
people.remove(person.getId());
for (Person p : people.values()) {
this.valueSum = this.valueSum - person.queryValue(p) * 2;
}
}
//删人时更新
并查集
在作业中有query_circle
和query_block_sum
两种指令:query_circle
询问两个点是否联通,query_block_sum
询问图里的连通块数量。为了提高程序的运行效率,我们可以使用并查集来解决这个问题。
//并查集类
import java.util.HashMap;
public class Ufds {
private HashMap<Integer,Integer> ufds;
public Ufds() {
this.ufds = new HashMap<>();
}
public void add(int son, int father) {
this.ufds.put(son, father);
}
public int getFather(int son) {
return ufds.get(son);
}
public int findFather(int son) {
if (son == ufds.get(son)) {
return son;
} else {
ufds.put(son, findFather(ufds.get(son)));
return ufds.get(son);
}
}
public boolean isCircle(int id1, int id2) {
return findFather(id1) == findFather(id2);
}
}
最小生成树
第十次作业中出现了query_least_connection
这一指令,这个指令是询问图中包含某个点的最小生成树的边权和。
由于之前已经实现了并查集的算法,因此可以使用Kruskal
算法来生成维护最小生成树。
在Network
类中使用ArrayList
记录下所有的点集合和边集合,命名为points
和edges
。在queryLeastConnection
方法中首先挑选出所有与传入id
连通的点,记录下连通点的数目,循环前使用排序算法调整edges
中边的顺序,随后每次在循环中挑选出符合条件的最短边,直到跳出循环。
// Kruskal算法具体实现
@Override
public int queryLeastConnection(int id) throws PersonIdNotFoundException {
if (!contains(id)) {
throw new MyPersonIdNotFoundException(id);
} else {
int cnt = 0;
int sum = 0;
int i = 0;
for (int pointsId : points) {
ufds2.add(pointsId,pointsId);
if (ufds1.isCircle(id, pointsId)) {
cnt++;
}
}
Collections.sort(edges);
while (cnt != 1) {
MyEdge edge = edges.get(i++);
if (!ufds2.isCircle(edge.getId1(),edge.getId2()) &&
ufds1.isCircle(id,edge.getId1()) &&
ufds1.isCircle(id,edge.getId2())) {
ufds2.add(ufds2.findFather(edge.getId2()),
ufds2.findFather(edge.getId1()));
sum = sum + edge.getValue();
cnt--;
}
}
return sum;
}
}
最短路径
最短路径直接使用Dijkstra
算法即可实现,但由于追求更快的运行效率,我们可以稍作处理,使用堆优化的Dijkstra
算法即可,复杂度为O(nlogn)
// 堆优化的Dijkstra算法
public int dijkstra(int id1, int id2) {
HashMap<Integer,Integer> visited = new HashMap<>();
PriorityQueue<Ele> pq = new PriorityQueue<>
(Comparator.comparingInt(Ele::getLength));
pq.offer(new Ele(id1, 0));
for (int id : people.keySet()) {
if (ufds1.isCircle(id1,id)) {
visited.put(id,maxValue);
}
}
visited.put(id1,0);
while (!pq.isEmpty()) {
Ele ele = pq.poll();
int minId = ele.getId();
int minValue = ele.getLength();
if (!visited.containsKey(minId)) {
continue;
}
if (minId == id2) {
return minValue;
}
visited.remove(minId);
for (Person p : ((MyPerson)getPerson(minId)).getPersonMap().keySet()) {
int newValue = minValue + getLength(minId,p.getId());
if (visited.containsKey(p.getId())) {
if (newValue < visited.get(p.getId())) {
visited.put(p.getId(), newValue);
pq.offer(new Ele(p.getId(), newValue));
}
}
}
}
return 0;
}
三.Bug分析
三次作业中存在两处bug
:第一处为qgvs
指令超时错误;第二处为添加新关系时若2人同时在多个群组里时只会找到第一个群组,其他群组会被忽略。
发现其他同学四处bug
:第一处为qgvs
指令超时错误;第二处为分发红包时发红包人扣除money
数错误;第三处为deleteColdEmoji
方法中可能会删除多个message
但只删除了一个message
的错误;第四处为群组里加人的时候上限为 1111,未进行该判断而产生的错误。
由于本单元作业是基于jml
规格进行的设计,因此主要的debug
策略是将代码与规格进行比对,其次便是依靠评测机进行大规模测试。
四.Network 扩展
假设出现了几种不同的Person
Advertiser
:持续向外发送产品广告Producer
:产品生产商,通过Advertiser
来销售产品Customer
:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser
给相应Producer
发一个购买消息Person
:吃瓜群众,不发广告,不买东西,不卖东西
Advertiser
、Producer
、Customer
均可设计为Person的子类;AdvertiseMessage
和BuyMessage
可以设计为 Message
的子类。
发送广告:
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
@ (message instanceof AdvertiseMessage) &&
@ (message.getType() == 0) ==> (message.getPerson1() != message.getPerson2()) ;
@ assignable messages;
@ ensures messages.length == \old(messages.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(messages.length);
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
@ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message));
@ also
@ public exceptional_behavior
@ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length;
@ messages[i].equals(message));
@*/
public void addAdvertiseMessage(Message message) throws EqualMessageIdException;
查询销售额:
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < products.length; products[i].getId() == id)
@ ensures \result==getProduct(id).getSaleNum();
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException)
@ !(\exists int i; 0 <= i && i < products.length; products[i].getId() == id)
@*/
public int querySaleNum(int id) throws ProductIdNotFoundException;
生产商生产产品:
/*@ public normal_behavior
@ requires contains(producerId) && (getPerson(producerId) instanceof Producer) &&
@ getPerson(producerId).containProduct(productId);
@ assignable getPerson(producerId).productNum;
@ ensures getPerson(producerId).getProductNum(productId) ==
@ \old(getPerson(producerId).getProductNum(productId)) + 1;
@ also
@ public normal_behavior
@ requires contains(producerId) && (getPerson(producerId) instanceof Producer) &&
@ !getPerson(producerId).containProduct(productId);
@ assignable getPerson(producerId).productCount, getPerson(producerId).productNum;
@ ensures getPerson(producerId).getProductCount() ==
@ \old(getPerson(producerId).getProductCount()) + 1;
@ ensures getPerson(producerId).containProduct(productId) &&
@ getPerson(producerId).getProductNum(productId) == 1;
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(producerId);
@ signals (NotProducerException e) !(getPerson(producerId) instanceof Producer);
@*/
public void produceProduct(int producerId, int productId) throws
PersonIdNotFoundException, NotProducerException;
五.心得体会
本单元整体难度相较于前两单元难度有所下降,主要考察对jml
的阅读理解,为此我们需要熟悉jml
的相关规模格式。
虽说简单,但也不能够掉以轻心,因为总有一些地方会存在某些坑点,我们需要仔细阅读jml
以确保程序的正确性。当然jml
也仅仅只是一个规范的描述,并不是一门真正的编程语言,我们编写代码只需要符合它的根本要求即可,不需要完全根据它的描述来编写程序,例如本次作业中的最小生成树部分,如果直接按照jml
来编写程序,绝对会导致最后运行超时,我们需要对其实现进行一定的改变。
最后,请让我吐槽一句:jml
好长啊!!!