OO第三单元总结
一、架构设计
1.对于社交网络模型的理解
1.1 Person:
person是社交网络模型中最基本的元素,每一个person相当于图中的一个个结点,他们存储了各种相关的信息。person也是我们操作时的基本对象,他们之间的交互、关系的连接、互相发送消息的行为,都是我们用来构成社交网络的基本元素。
1.2 Group:
group相当于是一群person的集合,person可以被加入其中,也可以从中被删去。但Group并非简单的将一群person囊括其中,它可以为person提供新的功能。拿我们平常用的最多的微信举例:用户与用户之间只能够聊天、收发红包、点对点传输文件等,但是我们在微信群中可以发布群通知、发起群收款、共享群文件等,这是group给person带来的行为上的扩展。
对于社交网络来说,群是比person高一级的抽象存在,它由元素形成了集合,并赋予其中的元素更多的性质。另外一点值得注意的是,person可以归属于不同的group,就像我们可以同时存在于OO和OS课程群一样,二者是互不影响的。它很类似于一种性质的概括,person可以有很多不同的性质,比如一个计算机系的同学,既需要上OO课,也需要上OS课,所以他就需要同时加入这两个课程群。
1.3 Network:
Network就是一张图,类似于一个平台,在其中可以存在数量众多的people,不论他们是否属于一个或者几个group,他们都归属于这个Network。继续沿用上述微信的例子,我们定义的Network实际上就类似于微信这个平台本身,他管理着groups和people,并且能从上帝视角来观看整个社交网络本身,我们可以查询某个group中有多少对象、两个person之间究竟能不能通过现有的好友产生联系、整个网络中存在多少个连通图、整个网络的最小连通图是什么等信息。在我们构建Network的时候,实际上站在了一个网站或者平台的构建者角度,来宏观的看我们管理着的一个个元素(person)和集合(group)。
2.图模型及对应的算法分析
2.1 union-find
并查集本质上是个树形结构,但是它比一般的树优越的地方在于其查找根节点的速度要快的多。由于这个优势,它的应用场景主要是:只查询两点间的连通性而不关心两点之间的具体路径。
我的实现方法是先构建一个抽象类,其中定义了并查集的所有方法(find
、union
、isSame
)
public abstract class UnionFind {
private HashMap<Integer, Integer> parents = new HashMap<>(); // key id value parents
public HashMap<Integer, Integer> getParents() {
return parents;
}
public boolean isSame(int v1, int v2) {
return find(v1) == find(v2);
}
//判断
public abstract int find(int v);
//查
public abstract void union(int v1, int v2);
//并
}
然后再建一个类,继承抽象类UnionFind
,并且根据选择的算法实现并查集。
在算法选择这个问题上,我一开始使用了quickfind
,也就是加边的时候,自动将其所有子节点的父节点全部设为自己的父节点,这种方法看似简单且查找复杂度为O(1),但是实际执行起来Union的复杂度太高,而且有些不需要查找的结点,根本没必要压缩路径,所以后来我舍弃了这中方法,转而采用路径压缩算法:
public class UnionFindQF extends UnionFind {
在并操作的时候,只是执行正常的加边,也就是把自己的父节点的父节点设为要并入的结点的父节点;在查操作的时候,再通过路径压缩递归地找到自己的根节点,并将其设为父节点。
2.2 Kruskal
Kruskal算法就是很常规的算法,查找一个最小生成树。在具体实现queryLeastConnection
的过程中,看那个JML感觉是最难的,知道了它要干什么之后反倒难度就不剩下多少了。我的实现方法是先遍历结点,得到存储了与id关联的所有vertex的ArrayList
,然后再读取所有的vertex之间对应的边,放入ArrayList
里,传递给用来实现算法的函数。
private int kruskal(ArrayList<Integer> vertex, ArrayList<Edge> edges) {
UnionFindQF unionFindQF = new UnionFindQF();
int result = 0;
int count = 0;
for (Integer integer : vertex) {
unionFindQF.getParents().put(integer, integer);
}
Collections.sort(edges);
for (int i = 0; i < edges.size() && count < vertex.size() - 1; i++) {
int id1 = edges.get(i).getId1();
int id2 = edges.get(i).getId2();
if (!unionFindQF.isSame(id1, id2)) {
unionFindQF.union(id1, id2);
result += edges.get(i).getValue();
count++;
}
}
return result;
}
由于上面实现了并查集,所以在实现kruskal
的时候很自然就使用了并查集。kruskal
里最复杂的其实就是判断加边后是否会产生圈,而产生圈的条件就是,这两个点在一个连通图中,这就很容易想到要用并查集来判断。向isSame方法传入边的两顶点id,如果说返回true,那么说明加了边会产生圈,因此这条边不能加,反之则可以。
2.3 Dijkstra
这里由于当时完成作业是在仓促,我觉得自己写的方法并不怎么样,是自己看着算法描述随手写的,最后强测的两个点被卡超时1点几秒,可以说是是在不太好。有同学提出可以用tarjan来优化,但是我还没来得及深入研究,之后会尝试去做一下优化。具体代码:
private int dijkstra(int id1, int id2) {
HashMap<Integer, Integer> distance = new HashMap<>();
HashMap<Integer, Boolean> isDetermined = new HashMap<>();
distance.put(id1, 0);
isDetermined.put(id1, true);
HashMap<Integer, Integer> value = ((MyPerson)getPerson(id1)).getValue();
ArrayList<Integer> remain = new ArrayList<>();
for (Integer id : people.keySet()) {
if (id != id1) {
remain.add(id);
}
}
int curPersonId = id1;
int min = 0;
while (isDetermined.get(id2) == null) {
for (int id : remain) {
if (value.get(id) != null) {
if (distance.get(id) == null ||
distance.get(id) > value.get(id) + distance.get(curPersonId)) {
distance.put(id, value.get(id) + distance.get(curPersonId));
}
}
}
min = 0;
for (int id : remain) {
if (distance.get(id) != null) {
if (min == 0) {
min = distance.get(id);
curPersonId = id;
} else if (distance.get(id) < min) {
min = distance.get(id);
curPersonId = id;
}
}
}
remain.remove((Integer) curPersonId);
isDetermined.put(curPersonId, true);
value = ((MyPerson)getPerson(curPersonId)).getValue();
}
return distance.get(id2);
}
二、遇到的问题与修复情况
1.图算法相关问题
1.1 union-find
第一次作业的bug出在union-find上,因为是后来改用的路径压缩算法,最后写的有点仓促,在合并操作的时候出了bug。
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) {
return;
}
getParents().put(p1, p2);
应该是先用find操作找到v1,v2的根结点,然后把其中一个挂到另一个结点上面。但是我当时手残写成了:
getParents().put(v1,v2);
1.2 dijkstra
本次的bug是算法的效率问题,强测有两个点超时了,算法本身应该没出bug。
2.关于JML理解上出现的问题
2.1 getAgeMean & getAgeVar
对于getAgeMean的JML
/*@ ensures \result == (people.length == 0? 0:
@ ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
@*/
漏看了一个括号,我一开始的理解是把每一个person的age先除length,然后再求和。
如果按照这样理解的话,JML规格就变成了:
(\sum int i; 0 <= i && i < people.length; people[i].getAge() / people.length)
而getAgeMean的错误也就导致了getAgeVar的连锁错误。
3.修复情况
3.1 union-find
由于bug很简单,所以看了一下样例后,简单分析了一下,很快就修复了。
3.2 dijkstra
未修复这两个点,暂时没去研究更好的算法,尝试了在本身的基础上改了改,过了几个点,但是最后两个始终过不去。
3.3 getAgeMean & getAgeVar
修复的过程中,我一开始以为是精度的问题,就用了math等等方法,但是始终不行,到最后才想起来看JML,然后发现是自己对于规格的理解出了问题。这次悲催的debug经历也告诉了我规格的重要性,一旦出锅,只要看看JML,再对照JML看自己的设计,只要都正确,那就不会出问题,自己盯着自己的代码死想是很难分析出问题的。JML相当于帮我们翻译了题意,只有按照题意来设计,才能够得到符合要求的程序。
三、测试思路
我的基本测试思路是大量随机数据加少量构造的边界数据。
通过编写程序自动生成随机测试数据,只要代码量够大,指令覆盖够全,对于程序测试就有价值。一般的bug,比如对JML的理解有问题导致程序行为和正确的不符,这样的bug在大量测试数据下是很难逃过去的。正确行为的判断,我选择用对拍的方法,多找几个同学,大家的代码一起跑一跑测试,然后用程序进行对比,如果出现不同结果,就要小心检查对应部分的代码了,肯定是有人有bug的。
构造测试数据针对的是JML规定的边界情况以及题目中预设的一些边界条件。
比如Group中计算平均年龄的时候,出现了Group中没有人的情况。再比如Group添加超过规定了人数上限的情况。
四、额外架构扩展
1. 要求
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
2. JML设计
发广告: Advertiser->Customer 传递的对象是包含产品信息的广告
生产产品:Producer->product 每个生产者产出产品
消费产品:Customer->Advertiser->product 消费者经由Advertiser购买product
每个Producer都拥有一个List,其中存放的元素是Advertiser,表示为其打广告的Advertiser。
相关接口方法
//添加销售员
void addAdvertiser(Person advertiser) throws EqualPersonIdException;
//添加生产商
void addProducer(Person producer) throws EqualPersonIdException;
//添加消费者
void addCustomer(Person customer) throws EqualPersonIdException;
//Advertiser为产品打广告
void advertise(int cusId, int proId) throws PersonIdNotFoundException;
//顾客从Advertiser地方购买产品
void purchase(int cusId, int adId, int toSell) throws PersonIdNotFoundException;
//查询生产商的产品对应的销售额
int querySalesOfProduct(int proId) throws PersonIdNotFoundException;
//查询某生产商的销售路径
List<Advertiser> querySellingPath(int proId) throws PersonIdNotFoundException;
核心业务功能
//Advertiser为产品打广告
/*@public normal_behavior