面向对象课程第三单元总结

全伟业-22371393 学生 2024-05-16 11:41:50

面向对象课程第三单元总结

前言

本单元的主要内容是要求完成JML规格实现,我们需要理解体会规格语言对程序开发带来的帮助。此外,我们还需要根据时间复杂度的要求进一步优化实现方法,体会规格和实现上的区别

测试

黑箱测试与白箱测试

  • 黑箱测试也称功能测试、数据驱动测试或基于规格说明的测试。测试者只知道程序的输入、输出和系统的功能,这是从使用者的角度针对软件的接口、功能及外部结构进行的测试,不考虑程序内部实现逻辑。
  • 白箱测试也称结构测试、逻辑驱动测试或基于程序本身的测试,测试程序内部结构或运行。在白箱测试时,从程序设计语言的角度来设计测试样例。测试者输入数据并验证数据在程序中的流动路径,并确定适当的输出,类似测试电路中的节点。

具体测试方法

  • 单元测试:针对软件的最小功能模块进行测试
  • 功能测试:对整个软件系统的功能进行测试
  • 集成测试:在单元测试之后,将各个功能模块组装在一起进行测试
  • 压力测试:对软件系统在高负载和大并发情况下进行测试
  • 回归测试:在软件系统进行修改或升级后,重新运行之前的测试用例以验证修改是否引入新的错误或导致原有功能出现问题的方法

数据构造策略

  • 构造更多的稠密图
  • 手动构造一些极端数据
  • 增大数据规模

架构设计

总体架构设计就是依照JML规格进行设计的,以下具体讲讲实现上的优化

异常类的统计

专门设置一个统计异常的类Exceptioncounter,对于每一种异常都有一个变量存储总发生次数,都有一个容器存储对应id发生异常的次数,在每个异常的接口实现的类中直接进行对该类的统计方法的调用输出即可

图的设计

并查集

本次第一次作业中用到了并查集,用于解决查询Person之间是否在一个关系圈,共有多少关系圈等方法。通过利用路径压缩来节省时间复杂度。在我的作业实现中在Edge类实现并查集的操作和查询。
其中难点在于并查集的动态维护,当遇到删边操作时,如果选择重建并查集无疑是一种暴力且耗时的行为,这里我们采用深搜,假设我们要删的边的两个节点为A、B,根据对并查集的定义,这两个节点必然拥有同一个father(路径压缩),或者说有同一个根,此时我们进行删边,不妨将A,B的father分别设置成自己(对于同一个圈子内的Person,father可以是他们中的任意一个人)。然后以A结点开始dfs,遍历所有从A结点出发可以遍历到的点,将他们的father设置为A结点,然后深搜后判断B结点的father是否为A结点,如果是,那么说明即使删了边,AB依然在同一个block中,反之则不在。

public void modifyEdge(int x, int y) {
    HashSet<Integer> con = map.get(x);
    con.remove(y);
    map.replace(x, con);
    con = map.get(y);
    con.remove(x);
    map.replace(y, con);
    father.replace(y, y);
    visit = new HashSet<>();
    dfs(x, x);
    if (father.get(y) != x) {
        blockSum++;
    }
    visit = new HashSet<>();
    dfs(y, y);
    for (int i : map.get(x)) {
        if (map.get(i).contains(y)) {
            tripleSum--;
        }
    }
}

public void dfs(int x, int rt) {
    visit.add(x);
    father.put(x, rt);
    for (int i : map.get(x)) {
        if (!visit.contains(i)) {
            dfs(i, rt);
        }
    }
}

双向bfs的应用

在第二次作业中,需要我们查询两个节点间的最短路径,条件是边权为1,那么在边权为1的情况下,dijsktra算法其实就不是很优的考虑了,这里我们直接考虑广度优先搜索,其实双向bfs和bfs之间的差距并不是很大,这里我直接实现了双向bfs,至于时间上的差别并不大

public int queryShortestPath(int id1,int id2)
        throws PersonIdNotFoundException, PathNotFoundException {
    if (!containsPerson(id1)) { throw new MyPersonIdNotFoundException(id1); }
    else if (!containsPerson(id2)) { throw new MyPersonIdNotFoundException(id2); }
    else if (!edge.isCircle(id1, id2)) { throw new MyPathNotFoundException(id1, id2); }
    else {
        if (id1 == id2 || getPerson(id1).isLinked(getPerson(id2))) { return 0; }
        Queue<Person> startQueue = new LinkedList<>();
        Queue<Person> endQueue = new LinkedList<>();
        HashSet<Integer> startVisited = new HashSet<>();
        startQueue.offer(getPerson(id1));
        HashSet<Integer> endVisited = new HashSet<>();
        endQueue.offer(getPerson(id2));
        startVisited.add(id1);
        endVisited.add(id2);
        int diss = 0;
        int dise = 0;
        int isPath = 0;
        while (!startQueue.isEmpty() && !endQueue.isEmpty()) {
            diss++;
            isPath = bfs(startQueue, startVisited, endVisited);
            if (isPath == 1) { return diss + dise - 1; }
            dise++;
            isPath = bfs(endQueue, endVisited, startVisited);
            if (isPath == 1) { return dise + diss - 1; }
        }
        return 0;
    }
}

public int bfs(Queue<Person> queue, HashSet<Integer> visited, HashSet<Integer> other) {
    int si = queue.size();
    for (int i = 0; i < si; ++i) {
        MyPerson cur = (MyPerson) queue.poll();
        for (Person j : cur.getAcquaintance().keySet()) {
            if (!visited.contains(j.getId())) {
                if (other.contains(j.getId())) { return 1; }
                visited.add(j.getId());
                queue.add(j);
            }
        }
    }
    return 0;
}

优先队列的应用

对于查询BestAcquaintance的操作,最清晰的方法便是维护每一个Person的优先队列,维护队首为最大值,每次查询返回队首即可,这里由于排序有两个优先级,第一优先级是value,第二优先级是id,因此要自定义cmp,这里容器我并没有采用优先队列(因为java没用过不知道咋用),我直接采用了TreeMap容器来维护
注意这里不要采用return o1.getId()-o2.getId()的形式,如果注意了数据范围,就会发现会爆

Comparator<PersonCmp> cmp = (o1, o2) -> {
        if (o1.getValue() < o2.getValue()) {
            return 1;
        } else if (o1.getValue() > o2.getValue()) {
            return -1;
        } else {
            return Integer.compare(o1.getId(), o2.getId());
        }
    };
    priQue = new TreeMap<>(cmp);

动态维护

有一些方法不需要图论的知识或是算法上的优化,只是需要动态维护修改和查询即可,这里便不细说了,目的是将查询的复杂度降为O(1),但是修改的复杂度从O(1)变成了O(n),但整体来说依然是优化

Junit测试

数据构造

数据构造方面前两次作业是雷同的,只需要去构造一张图,尽可能的保证数据生成的图既有稠密图也有稀疏图,保证覆盖率
第三次作业的数据构造要考虑message,对比前两次需要考虑message的构造次数,在这里我采用了构造一定数量的各种类型的message,再进行delete操作,保证数据的普适性

断言

这里需要根据JML规格上的要求进行全方位的断言,除了方法调用结果外,调用后状态是否改变啊等等都需要去断言,这里我采用的是影子,除了本身的network,在生成数据时同时深克隆一个network0作为影子,届时直接比较调用后的network的属性和network0即可

bug分析

这三次作业强测均为满分通过且互测没有被hack,在hack别人时通过自己构造的极端数据或是自己在写代码时评测机测出的错误数据均有收获

学习心得体会

JML规格只是一个框架和要求,只要满足该要求,如何实现都可以,在此基础上自然是时间越优的实现越能得到正收益。同时JML规格也有效的规定了我们能做的和不能做的,是个很有效的工具。

...全文
31 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧