BUAA OO-Blogs Unit3
OO-Blogs Unit3
单元简介
通过实现官方提供的JML规格,实现了一个对社交关系的模拟。
官方包已经提供了每个类每个方法的JML规格,因此无需做全局性的架构设计,只要着眼于方法的实现即可。因此,由于很多方法的内容十分简单(如get,set等),本文只选择一些稍具规模的方法说明。
HW1
由于提供了JML规格,设计的效果集中体现在两个方面,一是正确性,二是性能。
正确性没有什么特殊之处,认真阅读规格、做比较充分的测试就问题不大,因此也不细谈,后面会说一下测试。
性能大致就是查看数据规模->选择能支持此规模的算法设计实现这样一个步骤。当然,OO用到的算法都是基本的板子,也无需做过多设计。
那么回到第一次作业,数据规模\(n=1000\),显然\(n^2\)问题不大,所以每次操作摊上\(O(n)\)是没有问题的。
首先,那些contains的操作用hash就可以做到平均\(O(1)\)。
其次值得关注的就是isCircle这个方法,是查询两点的可达性。由于没有删点删边的操作,故只要用并查集维护就可以,加点加边查询的复杂度都是\(O(1)\)。相关的还有queryBlockSum这个方法,是查询连通块个数。只需要在并查集加点和合并的过程中维护一下即可,也是\(O(1)\)实现。
还有复杂度稍高的是queryNameRank这个方法,查询某名字的排名,其实照抄JML的方法也是\(O(n)\),可以接受。如果希望进一步优化的话,也可以做到平均\(O(logn)\)。用一颗二叉查找树存储所有的名字。每次插入更新沿途所有子树的大小,每次查询统计所查结点左边的所有子树大小即可得到排名。一点细节是,由于查询的是排名,所以在查找的过程中应优先搜左子树。虽然普通的二叉查找树会退化,但最差\(O(n)\)罢了,又何妨。
封装
除了分析复杂度,还想聊一聊封装的问题。(要不然真没的写了)
不管是并查集还是上面说的这个二叉树解法,都会在各种方法里有维护的操作,如果遍地都是维护,算法的描述就非常分散,可读性非常差,修理起来也很不方便。这就体现了封装的优势,可以把高逻辑强度的代码放在一起处理,对外部隐藏。
但还有一个问题,我们传入封装好的容器的参数应当是Person这样具有现实意义的对象,但写算法不想管这些,只想开一个array每个0-(N-1)的下标代表一个元素就开始搞。因此,现实含义和算法需要分离。其实也很简单,建立一个映射就好,map可以\(O(1)\)的做到这一点,时间开销也可以忽略。那么如何用map实现这两者的联系呢?方法自然很多,我用了一个自认为比较优雅的方案。
外部是一个泛型类,用来将对象映射到下标,不涉及算法。内部类接受下标,真正实现算法,不涉及元素的实际意义。对外只保留外部类的方法。大致框架如下
public class DisjointSetUnion<T> {
private final DisjointSetUnionInIndex dsu = new DisjointSetUnionInIndex();
public void union(T t1, T t2){
dsu.union(indexMap.get(t1), indexMap.get(t2));
}
private static class DisjointSetUnionInIndex {
private final ArrayList<Integer> fathers = new ArrayList<>();
public void union(int a, int b){
fathers.set(find(a), find(b));
}
}
}
评测
测试方法在最后讨论。
自己的程序没有出锅。互测房有三位同学queryBlockSum用了\(O(n^3)\)的暴力,比较平均的随机数据都能T掉,我非常感动...
HW2
第二次作业新增了两个类,但需要关注的只有Group类。除此之外,数据规模提到了n=1e4,但group大小m=1111,因此\(O(mn)\)也差不多可以过。
这次作业这几个方法需要在线维护,getValueSum、getAgeMean、getAgeVar,含义分别是查询组内边数、查询组内平均年龄、查询组内平均方差。
getValueSum暴力是\(O(m^2)\)的,但只要在加人减人以及加边时进行维护就均摊到\(O(m)\)。
getAgeMean不解释,getAgeVar其实懒得话做\(O(m)\)也行,但是也很容易做到\(O(1)\),见下。
两者分别维护即可。
顺便一提,Person内的message list的存取不需随机访问,只有头插,因此LinkedList会好过ArrayList.
评测
自己的程序没有出锅。互测房有两位同学getValueSum用了\(O(n^2)\)的暴力。
HW3
这一次加入了继承关系,新增了几个Message的具体种类,分别实现对应的操作。但也没有什么新的问题,照JML抄就可以。
数据范围还是n=1e4,但时间开到了6s,应该\(O(n^2)\)差不多,也不用过多担心常数。
除此之外,值得一提的就只有两个方法deleteColdEmoji和sendIndirectMessage。前者是删除冷门表情,后者是发送间接消息(请原谅我拙劣的翻译)。
deleteColdEmoji是要将emoji list里小于某阈值的表情删掉,同时维护message list使得message中含的表情都存在于emoji list中。所以就先删emoji list,然后把message中表情不在emoji list里的删掉即可。由于这里emoji list用HashMap存,所以contains方法是\(O(1)\),总体是\(O(1\cdot n)=O(n)\)。
sendIndirectMessage核心是最短路,无负边权。因此用一下Dijkstra+堆优化做到\(O(MlogN)\),大概算一下应该够用了。同样的,这个图容器也采用了HW1中提到的方式进行了封装,内部用邻接表存储。
评测
自己的程序没有出锅。互测房有一次hack,我没有读代码,所以也不知道发生了什么...
测试
这一单元采用对拍进行测试。
对拍的有效性是建立在这样的基础上:两份代码在同一个地方出一样的错的概率非常小。因此,为了保证对拍的有效性,减少讨论是一件很有必要的事情,事实也证明效果不错,通过两个人的对拍就解决了(已知的)所有bug。
生成部分采用随机生成,没什么新意,但是大量随机数据的覆盖性还是可以的。
多人对拍
当然,对拍还是参与者越多错误率越低,多人对拍可以用下面这样一个平台实现。
建立一个远端的rep,每个同学都可以执行两种操作。
一是上传数据,操作上只需把数据点.txt文件放入某文件夹,运行出正确结果后push即可上传新数据。
二是拉取数据,只需pull后运行即可。本地程序保存一个当前评测最后一个SAME的指针,从这个指针开始评测,免去重复工作。
当本地对拍不一致,只需联系数据上传者即可。
这样隔离了每个同学的代码,每人都仅持有自己的代码,同时实现了多人对拍的效果,效率也有保证。
JUnit测试
JUnit测试我自己只是试了试,可以用,但效果很难和上面说的随机数据+对拍的方式媲美。
这很大程度是因为需要手动编写数据,手动编写的量有限,覆盖性不高;而且手动编写的针对性测试往往针对的点可以静态的分析代码得到结果,所以测试的意义也不大。
但在没有对拍的基础时,JUnit之类的单元测试就有了用武之地。
关于JML
三次作业写下来,也读了不少JML了。其严谨性值得认可,但弊端也不能忽视。
其一是JML有时候略显冗长,有些方法的JML可以长达几十行,比我写的代码都长。这还只是一个简单的作业,要放到工程上,一个方法实现非常复杂的功能,JML不知道要写多少才能描述完整。事实上,规格这个东西实现了这样一个过程设计师设计架构 --(将所想翻译为JML)-> 程序员拿到JML --(将JML翻译为所想)-> 程序员编写代码,核心是消息的无损传递。但这个过程中,这两个括号的复杂度也不得不加入考虑,这中间或许需要一些平衡。提高一些规格表述的抽象性或许是一种选择。
其次,JML貌似利用其形式化的便利开发了一些自动测试工具。我没有配置,但想想就局限性很大,动辄一个\exist()就要\(O(n!)\)验证,很难想象如何在有限时间内生成结果。因此我认为很难广泛应用,真正能测试的可能也就是那些没有逻辑含量的小代码段。
心得
JML单元体会的最深的就是设计和实现的分离,在这一单元里同学们充当的是实现者,但转行做设计应该也问题不大。
同时没有了性能分,事情变得简单了许多,可以更多的关注代码质量、性能等指标。

浙公网安备 33010602011771号