OO第三单元总结

1 JML概述

1.1 JML语言理论基础梳理

先上一张思维导图,这是JML level 0下主要的语法

由于JML手册中已经给出了详尽的规范,所以此处只叙述一下个人的理解:

一般而言看JML可以先看他的类型规格,尤其是所属的类有什么成员变量(容器不需要严格实现,但是成员的分布需要了解),不变式或者状态约束可以暂时跳过;然后针对每个方法,可以先看其正常状况下的ensures,因为这个是方法的主要功能!然后通篇读完所有的类与方法之后,再开始从较为底层的方法开始严格按照JML写!

1.2 JML工具链

①Junit4 手动单元测试

②OpenJML 检查JML的语法

③SMT Solver 检查JML与代码的符合性

④JMLUnitNG 根据JML生成测试样例并进行检验

总的来讲后三个工具链报错巨多,而且并不是很智能,详尽分析请看下文

2 JML验证工具

2.1 openJML

openJML用于对JML语法进行检查

指令:java -jar <openjml.jar路径> -check -dir <JML代码所在源代码路径>

注意-check是针对某一个java文件进行检查,而-dir则会连带其整个目录进行检查

仅对Group.java的检查

 

可以发现找到的错误均与Person有关,于是我们连带整个目录进行检查

就没有任何错误

2.2 SMT Solver

指令:java -jar <openjml.jar路径> -exec <Solver路径> -esc <JML代码所在源代码路径>

SMT Solver已经包含在下载的openJML里面

分析两个报错:

①发现其对@Override的标签也会报错,目测是由于无法识别@Override的含义

②在ensure语句中不该出现私有变量

2.3 JMLUnitNG

指令:java -jar <JMLUniting的路径> <被测试源文件路径> <目标路径\*.java>

注意需要将诸如new HashMap<>()中<>内的内容补全

生成的测试样例截图:

可以看到生成的测试样例取得都是极端值、0、null等等,并且不能准确识别requires

所以简而言之就是,这款工具还相当的简陋,离实际应用相距甚远

4 模型构建策略

写在前面:一定要弄明白模型的整体目的 再仔细看JML!!!

由于三次作业在迭代过程中,仅增加了一些函数,所以直接从第三次作业分析

4.1 数据结构的选择

首先需要明确:JML类型规格中给出的容器,仅供参考,不一定需要实现

本次作业中由于Person和Group都有独一无二的id,所以选择HashMap作为People和Groups的容器是非常合适的,ArrayList的底层实现是数组,查找效率不高,而HashMap作为散列表查找的时间复杂度近乎o(1)

在和同学交流过程中,得知:

① 有人会根据函数需要遍历还是查找来选择ArrayList和HashMap,个人认为如果确实有这样的区分度的话,是一个很不错的想法,但是我应该所有的容器都采用HashMap,因为我也观察过大部分函数都是需要查找,这样不会超时也较为简单

② 有人全部使用ArrayList导致强测TLE,后来我与其共同分析,在queryNameRank()中时间复杂度过高!

4.2 算法选择

4.2.1 缓存法

针对查询年龄平均、方差等等此类功能,采用缓存法需要在add和delete时做出极为小心的改动,并且认真阅读JML,因为部分数据与直觉不符,但是可以极大地节约时间

然而,第二次作业中一开始,我非常轻率的直接严格按照JML用二重循环写,直到在测试过程中发现会超时,几乎全部重构的修改了函数,才避免了TLE

4.2.2 并查集

iscircle()和queryBlockSum()都可以用并查集非常轻松地解决,其中iscircle()时间复杂度为o(1),queryBlockSum()时间复杂度为o(n)

此外并查集有两个可以进行优化的地方:

① 路径压缩

由于使用树来存储一个集合内的所有元素,而树根则是该集合的代表,如果不路径压缩,时间复杂度为o(height),而路径压缩是一种启发式的优化手段,因为事实上可以把所有的元素(除树根)直接连到树根上,从而实现o(1)的查询时间复杂度

P.S. 图片取自《算法导论》(原书第3版)作者: Thomas H.Cormen / Charles E.Leiserson / Ronald L.Rivest / Clifford Stein

② 按秩分配

注意到并查集在合并两个集合时,将一棵树的树根挂到另一棵树根上,为了使得树高(height)尽量低,一般选择将低一些的树挂到另一棵树上

此算法我并未实现,(实际上实现起来相当简单),但是考虑到使用并查集+路径压缩已经可以实现相当高的查找效率,就不增加代码的复杂性

4.2.3 Dijkstra算法&Tarjan算法

针对queryMinPath()采用Dijkstra算法,由于不是算法课不详尽介绍了,但是注意求最小的路径估计时,可以采用java自带的PriorityQueue,其底层采用堆实现,可以显著降低时间复杂度

针对queryStrongLinked(),采用Tarjan算法,其本质是求点双联通分量,但是要注意:离散数学中两点一线也算点双联通,但是根据JML此处不算,所以采用Tarjan算法之后要加以特判排除两点一线情况

5 bug与修复情况

5.1 自己的bug:

仅第二次作业互测中被发现1个bug

原因分析:

浅层原因:

第二次作业中iscircle()函数可以通过并查集快速实现,并查集主要有3个操作构成:

新建集合:MAKE-SET(v),

寻找给定元素所在集合的根:FIND-SET(v),

合并两个集合:UNION(v, u)

而对于两个集合的合并应当写成:

UNION( FIND-SET(v), FIND-SET(u) )

即寻找两个集合的根节点并且使得这两个根节点成为父子节点,而因为第一次写并查集误写成UNION(v, FIND-SET(u)),从而导致部分连接信息丢失,但是强测中并未被hack,可能是由于强测中图的连通性过高,部分信息的缺失不足以导致iscircle()判断的

深层原因

本次作业依旧采用自动测评的方式进行测试,方法如下:

①利用python脚本实现一个生成器,随机输出指令并且维护指令计数器与已给出id的列表

②利用git diff功能与同学代码对拍

本次作业由于对于一组给定的指令,结果必然完全一致,所以对拍即可实现比对,比前两次作业进行逻辑验证方便许多

然而,问题即发生在python脚本的生成器,由于版本管理失误,我使用了旧版的生成器,其不会生成iscircle()指令,从而导致代码测试的覆盖不全

 

注:

a.并查集的三个操作引用 《算法导论》(原书第3版)作者: Thomas H.Cormen / Charles E.Leiserson / Ronald L.Rivest / Clifford Stein

b.感谢hjw同学分享的生成器以及他在OO课程上对我的其他帮助

 

5.2 互测发现的bug

第1、3次互测整个room均未发现任何bug

第2次互测中发现三个bug,均为WRONG ANSWER

原因分析:

对于queryAgeMean()和queryAgeVar()这两个函数,JML中均给出关于整除的严格说明,但是有同学忽略了这一点,以至于在特定数据下会出现相差1的错误

6 心得体会

a. 一定要注意JML中,方法规格的要求是强制性的,但是对于类的成员数据类型要求却是非强制的,意味着:JML规格中可能给出的静态数组,而你可以用任意容易诸如HashMap去实现,以提高性能

b. JML规格由于处于注释中,idea无法提供任何颜色提示,很容易搞混括号匹配,一定要弄清楚否则容易出现理解错误

posted @ 2020-05-23 19:56  18231094  阅读(165)  评论(0编辑  收藏  举报