• 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无法提供任何颜色提示,很容易搞混括号匹配,一定要弄清楚否则容易出现理解错误

  • 相关阅读:
    三种按键处理函数
    enum与typedef enum的用法
    PIC18F中断定时器
    .net core Ocelot+Consul实现网关及服务注册和服务发现
    wpf的优点
    ASP.NET Core Web API下事件驱动型架构的实现
    2020个人计划
    图解C#的值类型,引用类型,栈,堆,ref,out
    .NET Core中的一个接口多种实现的依赖注入与动态选择
    redis分布式锁深度剖析
  • 原文地址:https://www.cnblogs.com/huxueyu/p/12944167.html
Copyright © 2020-2023  润新知