前言
相比较实验一,实验二真是好几个实验一的工作量啊。害,这头发是保不住的了。但是实验二比实验一有趣太多了,是的,实验二更有一种创造的乐趣,从P1到P3,一步一步,学习如何设计ADT,再到实际应用,一度有一种自己已经是个软件工程师的感觉(累傻了出现幻觉了)。这次先分享P1和P2,因为这个两个任务其实是有关联的,对于P3我想单独写,我也需要好好消化一下最近学习ADT的成果。
Graph
P1的难度已经比实验一难了,在刚开始实验的时候,我对接口和泛型真是一窍不通,然后刚开始看了两天书和查了两天资料才知道原来Graph定义的是接口,然后实验一开始就让我们先写test??虽然有spec但还是不太习惯,但其实最后发现先把test的写了或许能让我们更能理解spec,就像是把预期的结果确定下来,按着这个结果去实现就保证没错,更像是对我们写的代码的兼容性的考验。
测试策略 测试策略主要分两种,一种是所有值最少一次覆盖策略,另一种是笛卡尔乘积全覆盖策略。在我们这个实验中第一种也是可以的。首先就是划分等价类,通过spec,进行边界划分,然后组合。
接下里就是正式开始实现接口方法。首先我们拿到的源代码中,Graph中第一个方法是长下面这样的:
这里面的L就是泛型,然后显然这是一个静态工厂方法,也就是通过静态方法获得实例,也就不需要constructor来获得实例。实验要求我们先用String来实现,那其实都差不多,这里就直接用泛型实现。我们要先修改这个静态工厂方法,来获得一个实例,做如下变动,返回一个实例就行。
public static <L> Graph<L> empty() {
Graph<L> graph = new ConcreteEdgesGraph<L>();
return graph;
}
ConcreteEdgesGraph
设计Edge
从名字上很容易就能理解,就是需要用边来实现图的保存,然后根据实验手册的提示,我们需要设计一个关于边的ADT—Egde。
图肯定是和点边关系密切相关的,既然我们需要通过边来表示图,那当然不能落下点了,那点和边是什么关系呢?显然,一条边,或者说线段,是两点间的连线,再结合题目要求我们的图是加权有向图,所以Edge这个类里面包含的属性就有起点source和终点target,以及边权值weight。
// TODO fields
private final L source, target;
private final int weight;
接下来就是一个很重要的要求,实验手册要求我们的Edge类必须是immutable,也就是不可变的,大家可能会好奇为什么我的属性为什么都是加上了final呢,这就是因为这个类是不可变的,那么也就是说rep是不能被修改的,所以加上final可以保证变量引用不变性。特别是针对基础数据类型,加上了final基本就是不可变了。
那么根据不可变要求,这个累就不需要setter方法,只需要gettter方法了。
getter和setter方法可以直接由eclipse生成,右键source->Generate Getter and Setter就可以自动生成。当然我还是推荐大家自己写比较好,反正也不多嘛(其实是我一开始不知道还能自动生成所以是自己写了)。
做完这些后,其实应该是在这些之前,要先把AF,RI,还有Safety from rep exposure写了。然后根据RI编写checkRep方法,来确保RI的正确性,还需要注意的一点就是请将所有有返回值的方法加上防御式拷贝,自动生成的我不知道有没有,不过一定要有防御式拷贝,因为如果返回是一个mutable类型的数据,那有可能在外部被客户端修改,所以必须使用防御式拷贝。
实现接口方法
完成了Edge设计后,就可以根据接口的spec来重写我们的接口函数,那个@override就是重写的意思。同样的也是得先把AF,RI,Safety from rep exposure写了,然后再把checkRep写了,要养成良好的编程习惯(虽然我自己也是后写的,但我会改的)。还有防御式拷贝,嗯,这几个是基本意识。下面是我的checkRep方法,第一个是边实现的,第二个是点实现的。
// Representation invariant:
// vertices contains no repeated point
// edges contains the directed edges whose weight must be positive
// the size of the vertices n and the size of the edges m must conform m <= (n-1)n or n >= Math.ceil()
private void checkRep() {
for ( int i = 0 ; i < this.edges.size() ; i++) {
assert edges.get(i).getWeight() > 0 : " weight < 0";
}
final int n = this.vertices.size();
final int m = this.edges.size() ;
int N = n*(n-1) ;
assert N >= m : " this is not a Graph";
}
// Representation invariant:
// vertex in the list can only appear once
public void checkRep() {
for ( int i = 0 ; i < this.vertices.size(); i++) {
L src = this.vertices.get(i).getVertex();
for (int j = i+1 ; j < this.vertices.size(); j++)
assert !src.equals(this.vertices.get(j).getVertex());
}
}
按照spec重写接口函数应该不算太难。需要注意的是返回值还是需要防御式拷贝,并且在所有的方法中都必须调用checkRep()方法进行检查。
这里我分享一下关于遍历的方法。在C语言里面,我们遍历就是三种,for,while,do while。这里介绍一下for的一种特别的写法,至于这种写法有什么特别的好处可以移步到其他博主那去,这里就暂时不做讨论了。
for ( Vertex<L> vert : this.vertices)
那还有一种针对列表遍历的,Iterator。例如下面这段代码
Iterator<Edge<L>> iedge = this.edges.iterator();
while (iedge.hasNext()) {
Edge<L> e = iedge.next();
if (e.getSource().equals(vertex) || e.getTarget().equals(vertex)) {
iedge.remove();
}
}
我们可以利用迭代器可以更好的访问链式结构,因为迭代器里面有next()和pre()方法来定位,适用于无序的结构遍历。而且虽然迭代器是拷贝,但也只是引用的拷贝,实际上对于可变数据类型,所指向的内容是一样的。所以在迭代器中对内容的改变是可以反映到数据类型上的。
P1中可能会遇到的问题
相信大家肯定有用collection类吧,也有用collection中的.contains()方法吧,这里有个很大的陷阱,就是.contains方法可能会一直返回false,哪怕你的元素明明在集合里面,关于这个原因呢我单独写在我的另一篇博客里了,叫为什么要重写equals()和hashcode(),同学们可以移步去看看。
Poet诗意的漫步
这个名字就特别好听,带有一种诗意的感觉。这部分我们需要完成的问题以及要求,其实在GraphPoet.java文件中开头那一长串都说明了,甚至还有实例,可惜是英文的哈哈哈哈额鹅鹅鹅,我就不多说了。
那么很明显,我们需要通过Graph来完成,我们实现Graph接口就是为了这一时刻和P2,那么其中主要涉及到的操作就是拆解字符串了,这个实验一大家应该都做了,忘了的同学请回去再看一遍或者看我的实验一分享。这些都是小问题。
难点
有一个地方可能容易被忽视的,就是出现重边的情况,也就是这两个词的关系构成的边已经在图中有了,实验要求我们每出现一次都将这条边的权值加一。
//设置点边关系,根据spec,每个相邻的单词间的权值为1,每重复一次的相邻关系,则权值加1
for (int i = 0; i < array.length - 1; i++) {
if (graph.vertices().contains(array[i]) && graph.targets(array[i]).containsKey(array[i + 1])) {
//若这条边已经存在,权值加1.
int weight = graph.targets(array[i]).get(array[i+1])+1;
graph.set(array[i], array[i + 1],weight );
}
else
graph.set(array[i], array[i + 1], 1);
}
稍微麻烦一点的地方是怎么找呢,怎么根据输入的字符串找呢对吧。根据要求和实例,我们主要将其诗意化的方法就是看原话中的两个单词在我们构建的图中是否相连,或者是否中间有一个过渡词,我们的任务就是如果有过度词,请将这个过渡词加进去。实验手册中的示例也很清楚了,那怎么找这个过渡词呢。显然要有点边关系。
比如:如果a,b中在图中有个过渡词c,也就是a->c->b的情况,那么a的终点集合b的起点集必有交集,且交集不为空。我们就要找到这个交集,并且找到其中权值最大的那个。
取两个集合的交集的方法是.retainAll(),当然还有取并集的,想了解的同学可以网上查,有十分详细的讲解。
for (int i = 0; i < words.length - 1; i++) {
Map<String, Integer> sources = new HashMap<>();
Map<String, Integer> targets = new HashMap<>();
sources = graph.sources(words[i + 1]);
targets = graph.targets(words[i]);
Set<String> s = new HashSet<>();
Set<String> t = new HashSet<>();
if (sources != null)
s = sources.keySet();
if (targets != null)
t = targets.keySet();
//下面这行是取交集,就是这两个单词之间的交集
s.retainAll(t);
int weight = 0;
String bridge = null;
//判断是否有交集
if (!s.isEmpty())
//有交集,就需要选取交集中权值最大的那个点作为添加的单词。若权值都相同,则选取第一个元素。
for (String str : s) {
if (weight < targets.get(str)) {
weight = targets.get(str);
bridge = new String(str);
}
}
//判断是否需要添加过渡词
if (bridge != null)
result = result + " " + bridge + " " + words[i + 1];
else
result = result + " " + words[i + 1];
}
P2 FriendshipGraph
和实验一差不多,只不过实验二要求我们以实验一的方式来实现,并完成getdistance,其实是差不多的,按照实验一中的稍微改改就差不多了,大家可以选择继承Graph接口,而憨憨的我却理解成实现就行,所以我直接在里面用Graph.empty()返回了个实例。。。也不知道算不算完成了要求。
有了Graph接口,就很容易写加边和加点都很容易实现。
需要注意的地方:Person类记得重写equals,这样你可以直接用contains而不用遍历了。
可能有人在getdisance那块出问题,想用广搜的同学可以移步我的lab1中有详细的提到。
结尾
总体来说P1和P2较为简单,特别是P2,如果实验一做的好那就是拿来就用,稍微改改就好,但是P3才能让你感受到真正的快乐,不一样的滋味,也会让你对ADT加深了解。