OO_Unit3总结
Posted MOC8
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OO_Unit3总结相关的知识,希望对你有一定的参考价值。
OO_Unit3总结
本单元的作业是基于JML规格来进行社交网络的模拟,考察的重点一是对JML规格的理解,学会读懂各个方法的规格来完成相应的方法设计。而是对一些基本算法的掌握,学会使用时间复杂度较低的算法来避免程序运行超时。因为不涉及架构设计,只需按照JML编写已设计好的结构,所以总体难度和复杂度来讲要低于前面两个单元,并且本次作业是一个严格的迭代开发模型,一般来说不会进行重构。
1、实现规格所采取的设计策略
根据规格写代码时,第一步是要将规格中的各种情况抽取出来。例如哪些是正常情况,哪些是异常情况,根据这些情况对自己实现的这个方法设计相应的分支结构。在每个分支中,要理解清楚当执行完这个方法后,要返回什么,对现有的变量造成什么影响,已经何处要抛异常等操作,例如以下这个方法addToGroup
:
/*@ public normal_behavior
@ requires (\\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id2) &&
@ (\\exists int i; 0 <= i && i < people.length; people[i].getId() == id1) &&
@ getGroup(id2).hasPerson(getPerson(id1)) == false &&
@ getGroup(id2).people.length < 1111;
@ assignable groups;
@ ensures (\\forall int i; 0 <= i < groups.length; \\not_assigned(groups[i]));
@ ensures (\\forall Person i; \\old(getGroup(id2).hasPerson(i));
@ getGroup(id2).hasPerson(i));
@ ensures \\old(getGroup(id2).people.length) == getGroup(id2).people.length - 1;
@ ensures getGroup(id2).hasPerson(getPerson(id1));
@ also
@ public normal_behavior
@ requires (\\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id2) &&
@ (\\exists int i; 0 <= i && i < people.length; people[i].getId() == id1) &&
@ getGroup(id2).hasPerson(getPerson(id1)) == false &&
@ getGroup(id2).people.length >= 1111;
@ assignable \\nothing
@ also
@ public exceptional_behavior
@ signals (GroupIdNotFoundException e) !(\\exists int i; 0 <= i && i < groups.length;
@ groups[i].getId() == id2);
@ signals (PersonIdNotFoundException e) (\\exists int i; 0 <= i && i < groups.length;
@ groups[i].getId() == id2) && !(\\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id1);
@ signals (EqualPersonIdException e) (\\exists int i; 0 <= i && i < groups.length;
@ groups[i].getId() == id2) && (\\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id1) && getGroup(id2).hasPerson(getPerson(id1));
@*/
public void addToGroup(int id1, int id2) throws GroupIdNotFoundException,
PersonIdNotFoundException, EqualPersonIdException;
该方法有两个正常情况和三个异常情况,要求满足人与组都要存在在这个network
中,且人本身不在组中,根据组中人数是否大于1111
来决定是否把这个人加入到组中。当人或组不在network
中或是人已在组中时抛出相应异常。综上,实现的代码如下:
@Override
public void addMessage(Message message) throws EqualMessageIdException,
EmojiIdNotFoundException, EqualPersonIdException {
if (messages.containsKey(message.getId())) {
throw new MyEqualMessageIdException(message.getId());
} else {
if (message instanceof EmojiMessage &&
!containsEmojiId(((EmojiMessage) message).getEmojiId())) {
throw new MyEmojiIdNotFoundException(((EmojiMessage) message).getEmojiId());
} else if (message.getType() == 0) {
if (message.getPerson1().equals(message.getPerson2())) {
throw new MyEqualPersonIdException(message.getPerson1().getId());
} else {
this.messages.put(message.getId(), message);
}
} else {
this.messages.put(message.getId(), message);
}
}
}
一般而言,实现的代码长度比JML规格的长度要短,这是影响JML将所有的效果都用最基本的语言进行描述,确保没有二义,这也是JML能作为java开发中常用的形式化描述的原因之一。
2、基于JML规格来设计测试的方法和策略
在本单元的作业中,我设计测试时仍然还是像前两个单元一样使用数据生成+测评机的方式来进行测试,而没有使用Junit工具来编写单元测试。但是我在设计数据生成的时候,有意的不让数据太随机,而是采用模拟简易network
的方式去针对每种方法的每种情况进行测试,从而达到基于规格测试的效果。
一般的数据生成器:几乎所有的数据完全随机,导致在测评时真正有效的信息很少,降低了测评效率,输出的结果基本上全是各种异常输出一层一层往上叠。
基于模拟network
的生成器:能够从特定的数据集取数据来模拟各种方法的正常,异常情况,从而生成能够测试各种方法的程序。
class Network:
def __init__(self):
self.people = []
self.groups = []
self.noemptygroups = []
self.groupsinside = {}
self.messages = []
self.relations = []
self.emojis = []
self.deledmes = []
以上是建议社交网络的元素,list
中只存id
信息,用于作为数据集提取。
生成某单条指令的方法也是基于此网络构造的,例如:
#########################
def atg_normal(mynetwork):
id2 = random.choice(mynetwork.groups)
id1 = random.choice(list(set(mynetwork.people).difference(set(mynetwork.groupsinside[id2]))))
if id1 not in mynetwork.groupsinside[id2]:
mynetwork.groupsinside[id2].append(id1)
if id2 not in mynetwork.noemptygroups:
mynetwork.noemptygroups.append(id2)
cmd = "atg " + str(id1) + " " + str(id2)
return cmd
def atg_pnf(mynetwork):
id1 = random.randint(1, intrange)
id2 = random.choice(mynetwork.groups)
if (id1 in mynetwork.people) and (id1 not in mynetwork.groupsinside[id2]):
mynetwork.groupsinside[id2].append(id1)
if id2 not in mynetwork.noemptygroups:
mynetwork.noemptygroups.append(id2)
cmd = "atg " + str(id1) + " " + str(id2)
return cmd
def atg_gnf(mynetwork):
id1 = random.choice(mynetwork.people)
id2 = random.randint(1, intrange)
if (id2 in mynetwork.groups) and (id1 not in mynetwork.groupsinside[id2]):
mynetwork.groupsinside[id2].append(id1)
if id2 not in mynetwork.noemptygroups:
mynetwork.noemptygroups.append(id2)
cmd = "atg " + str(id1) + " " + str(id2)
return cmd
def atg_epe(mynetwork):
id2 = random.choice(mynetwork.noemptygroups)
id1 = random.choice(mynetwork.groupsinside[id2])
cmd = "atg " + str(id1) + " " + str(id2)
return cmd
在这些原子操作都执行完毕后,就可以开始进行最后的数据生成,将原子操作拼接成数据,只要按照一定顺序将各种指令拼接,即可形成相应的结构。
class Datastruct_undifined:
def __init__(self):
self.total = 0
self.ap_normal = 0 #其中插入qps
self.ap_epe = 0
self.ar_normal = 0 #其中插入qbs
self.ar_pnf1 = 0
self.ar_pnf2 = 0
self.ar_ere = 0
self.qv_normal = 0
self.qv_pnf1 = 0
self.qv_pnf2 = 0
self.qv_rnf = 0
self.cn_normal = 0
self.cn_pnf1 = 0
self.cn_pnf2 = 0
...
以下是几种具有不同特点的结构(篇幅有限,不展示全):
从左至右分别是:
- 对所有方法进行测试的数据点。
- 对最短路径算法进行专项测试的数据点。
- 对第三次作业新增方法的重点测试的数据点。
要生成具有不同倾向的测试点时,只需对数据结构体进行修改即可。
这样生成的数据集,优点在于能仅用数据生成器来模拟单元测试,并且可以快速的对所有指令的功能进行全覆盖无死角的评测,一般来说有功能错误的程序在我这里一个点都过不去。但与此同时,该测试集的缺点在于生成的数据随机性太弱,各种指令的出现顺序是一定的,虽然在数据结构中对某些可能具有牵连效应的指令多次出现,但还是无法避免测不出某些十分十分不常见的bug。
我们的数据生成器都是经过专业训练的,无论多少功能Bug呢,我们都能测出来
——除非太奇葩。
不过我在搭建测评机的时候是和其他几个同学共同完成的,因此结合其他人的生成器能一定程度上避免这个问题。
测试集和作业代码一样,在这个单元都是进行迭代开发实现的,最后一次的代码量达到了将近1000行,比作业本身的代码量还要大。虽然其中大多是机械的步骤,但真正写起来还是相当锻炼人的。
3、容器选择和使用的经验
在本单元作业中,一共使用率4种Java容器,分别为:
ArrayList
HashSet
HashMap
PriorityQueue
ArrayList
ArrayList
是java中的可变数组,可以实现动态的增加,删减数据。由于该容器在许多查询操作上效率较低,因此在本单元中采用的很少。唯一用到的地方是在Person
的消息列表中,由于部分指令要求取出靠前的几项,所以具有索引功能的ArrayList
成为了这一部分的最佳选择。
HashSet
HashSet
是基于哈希表的集合类,要求其中的元素不重复,且具有较高的查询效率,在本单元中一般作为id
集合,所用到的位置有Network
中的EmojiId
集合以及在迪杰斯特拉算法中的已到达节点id
集合。
HashMap
HashMap
是java中的哈希表键值对,要求其中的Key不能重复,在本次作业中,涉及id这一不能有重复值的属性,且如Person
,Group
等,一个id对应一个类实例,因此本单元作业中HashMap
的使用最为频繁。
private HashMap<Integer, Person> people;
private HashMap<Integer, Group> groups;
private HashMap<Integer, Message> messages;
private HashSet<Integer> emojiIdList;
private HashMap<Integer, Integer> emojiHeatList;
private HashMap<Integer, Integer> father;
在使用HashMap
时,要注意慎用containsvalue
算法,不同于containskey
基于哈希值的O(1)
查找,containsvalue
在查找时的复杂度是O(n)
。因此在使用时要注意此复杂度会不会导致超时。
PriorityQueue
PriorityQueue
是java中的优先队列容器,也就是数据结构中的小顶堆,在最短路径的迪杰斯特拉算法中,可以起到堆优化的作用。因此,在本次作业的sim
方法中,要计算最短路径时,采用该容器可以直接实现堆优化的迪杰斯特拉算法,十分方便,只要每次将相应节点加入堆中,从堆顶推出的节点就是能用最短路径到达的点。
使用经验
在容器的选择中,大致的经验为:
- 如果要存储的节点是键值对,则优先使用
Map
型容器,一般来说使用HashMap
,当需要对容器中的节点进行排序操作时,可以使用基于红黑树实现的TreeMap
。 - 如果要存储的节点两两不同,可采用
Set
即集合类容器,一般使用HashSet
,具有较高的查询效率。 - 在其中的节点有明显的基于加入顺序等动态因素的索引需求时,采用
ArrayList
,用于根据索引确定节点,此外ArrayList
也可用于排序。
4、有关性能问题的分析
第一次作业
本次作业中可能涉及性能的方法主要是isCircle
,此方法的功能是判断network
中的两个person
是否通过关系网连通。一般来说,判断连通时可以采用dfs
或是bfs
的搜索方法,但由于本次作业中(三次作业都是)只有addRelation
没有delRelation
,因此可以用并查集算法,在每次添加关系的时候来维护两点之间的关系。利用并查集算法,并采用路径压缩算法,使得查询时的时间复杂度达到O(1)
,大大节省时间。
对于query_block_sum
方法,需要计算的是连通分量的个数,如果不进行维护,采用的最简算法是用并查集所维护的祖先列表,当某个person
的祖先是他自己时,就可以用其代表一个联通块。
然而在第一次作业中我们组还是出现了一位同学用了O(n4)
算法,采用多次遍历计算联通块个数,所以我用750个ap
,250个qbs
的数据点进行自动测试时,这个同学跑了十几分钟的时间……
但是,其实可以用一个简单的维护就能让qbs
的查询变为O(1)
,即在network
中加入人时将qbs+1
,当出现并查集合并时将qbs-1
。即可直接用一个变量将其记录。
private int qbs;
public void join(int id1, int id2) {
int ori1 = find(id1);
int ori2 = find(id2);
if (ori1 != ori2) {
father.remove(ori1);
father.put(ori1, ori2);
qbs--;
}
}
public void addPerson(Person person) throws EqualPersonIdException {
if (people.containsKey(person.getId())) {
throw new MyEqualPersonIdException(person.getId());
} else {
people.put(person.getId(), person);
father.put(person.getId(), person.getId());
qbs++;
}
}
第二次作业
第二次作业中不涉及复杂的算法,主要是Group
中一些变量的维护,包括valuesum
(value
的和),agesum
(age
的和),age2sum
(age
的平方和)。
valuesum
:因为此变量涉及到组内两个人的关系,若要保证不超时,该变量一定要维护,否则会出现双重循环的O(n2)
的复杂度导致超时。其中需要注意的是:valuesum
对于两个人之间的关系是要加两次,不能只加一次。- 需要维护的方法不仅有
atg
和dfg
,还有ar
,在同组的两个人加上关系时,需要将这个组的valuesum
进行相应的修改。
agesum
:此变量用于计算年龄平均数,在atg
与dfg
时进行维护。age2sum
:同样在atg
与dfg
中维护,用于计算方差。
在计算方差时,可以采用公式:
注意,有关精度的计算应当与JML规格中保持一致,因此不能直接用E(X2)
,E(x)2
和Var(X)
的关系来计算。
第三次作业
第三次作业的CPU时间上限变为6s,因此之前两次作业中O(n2)
的算法不再容易被时间复杂度卡掉,因此有关性能的问题主要体现在了sim
算法,即最短路径计算上。
在最短路径的计算中,我采用的是最流行的迪杰斯特拉算法,并用Java自带的优先队列容器来对其进行堆优化以降低其时间复杂度从O(n2)
到O(nlogn)
,并且在其中进行了局部优化(例如对于以更新过的废弃节点不进入空转而是在判断后直接移除),从而让自己的此算法在与身边同学的比较中也较快。
在三次作业中,因为对每个方法的时间复杂度都进行了分析,并测试了对各种方法较为特化的测试点,所以都顺利通过了对时间性能测试的相应测试点。
5、架构设计
本次作业由于主体方法都是基于课程组给定的JML规格,因此在架构设计层面的难度相较于前两次作业较低,只要按照JML所描述的规格,合理选用算法及维护策略,就能使架构较为清晰。
图模型
有关图的建构,我基本上就是按照JML中已有的容器将图和图中有关节点的信息进行存储,比如说network
中的people
表,每一个person
的acquaintance
表,利用这些构成一个关系网络图。其中附加的因子有并查集查询中会用到的father
表以及Group
,Message
等不以节点的形式存在于图中的因子等等。
在本次作业中,并未涉及较多的有关图论的知识,仅有联通块和最短路径两种需要考虑算法的地方。因此用以上的结构对图进行建构是可行的。
维护策略
为便于查询,在编写代码过程中将部分量进行维护是很好的策略。
在本单元作业中,我维护的量在前面的小节已经说明,即第一次作业的qbs
及第二次作业的valuesum
,agesum
和age2sum
。在判断一个变量是否需要维护时,我认为应该遵循以下的原则。
- 需要维护的变量应该是满足“维护复杂度明显小于不维护查询复杂度”这一条件的,例如
qbs
,维护和维护后查询的复杂度均为O(1)
,不维护查询的复杂度至少为O(n)
。 - 为使代码尽量简介,一些维护后复杂度无明显变化的变量无需优化,例如在作业中出现的各种
size
。 - 有些变量在进行许多操作时都会产生巨大的变化,此时进行维护反而是作茧自缚,例如第三次作业中的最短路径,如果想在每次添加关系和添加人时都维护,其复杂度会无比巨大,远远大于在查询时看似笨重的
O(nlogn)
。
6、感想
本次作业需要动脑设计的部分不多,算是让自己的脑子从前两单元的繁忙中歇了歇。但这一个单元作业做下来也让我取得的不少收获。
- 首先,JML的学习让我知道了怎么去根据已有规格来编写代码,让我明白学计算机不是一味地学编程,埋头写代码,而是要仔细的思考一个优秀的代码是什么样的。根据JML写代码极大地培养了我的代码习惯,让我学会在每写一个方法前去思考有哪些可能情况,会造成哪些可能影响等等。
- 其次,这单元作业让我意识到学习算法的重要性,尽管本次作业涉及的主要算法只有两种,但还是暴露出我在算法掌握方面存在很大的进步空间。对于一个上学期数据结构课从第七节翘课翘到最后一节的人[逃]来说,直接在作业中写并查集和迪杰斯特拉算法确实是十分艰难的过程。查资料,找板子,寻找优化算法花了很长的时间,才算是把算法掌握透。在今后的程序编写中,对于算法的要求会更高,所以对于算法的学习要从平时就开始。
- 再者,这次的作业让我们明白简单的事物往往暗藏杀机。这单元作业虽然“杀气”比不过前两单元,但这单元的第二次作业竟然成为了这学期OO课程“死伤最惨重”的一次,有相当一部分同学直接爆零。可见我们不能被看似简单的作业蒙蔽了双眼,对于每一项任务都要认真地去完成。
- 最后,这单元让我明白做好测试的重要性。对于这个单元,做好测试是取得好成绩的重中之重,这单元的三次作业堪称“测评机大作战”,有一个好的测试数据生成及便能成功一半。因此要学会写测试,学会对自己的代码进行测试。
以上是关于OO_Unit3总结的主要内容,如果未能解决你的问题,请参考以下文章