面向对象第三单元总结

王思月-21373349 2024-05-16 23:43:03

面向对象第三单元总结

第一次作业分析

代码架构分析

本单元作业的重点为代码效率的优化,在本次作业中主要体现在以下三个方面:

容器的选择

本次作业中所有对Person进行的查找,都可以转化为对唯一的PrimaryKey: id进行的查找。因此采用HashMap容器存储Person,其中HashMapkey代表Personidvalue代表Person,可以保证所有的查找时间复杂度均为O(1),避免每次查找时都对数组进行遍历。

并查集优化

本次作业中的isCircle()函数要求查询两个Person之间是否连通,可以采用并查集的方式进行优化。并查集用于管理一些不相交的集合,主要思想为用集合中的一个元素代表一个集合。对于每一个节点,都有一个父节点用于代表它。如果两个节点的代表节点相同,则说明它们属于同一个集合。这样两个PersonisCircle判断就可以被转化成对它们的代表节点是否相同的判断

并查集类的封装

将并查集封装为如下的类:

public class DisjointSet {
    private final HashMap<Integer, Integer> pre;
    private final HashMap<Integer, Integer> rank;

    public DisjointSet() {
        pre = new HashMap<>();
        rank = new HashMap<>();
    }

    public void add(int id) {
        if (!pre.containsKey(id)) {
            pre.put(id, id);
            rank.put(id, 0);
        }
    }

    public int find(int id) {...}

    public void merge(int id1, int id2) {...}
}

pre存储一个节点和代表它的节点,key为节点编号,value为该节点的代表节点编号。
rank存储一个节点的秩,初始秩为0,表示一个节点一开始的代表节点是它自己。

路径压缩

查找一个节点的代表节点时,可以将查找路径上的所有节点的直接代表节点都设置为最终查找到的代表元。在查找过程中进行路径压缩的具体方式如下:

public int find(int id) {
    int represent = id;
    if (!pre.containsKey(id)) {
        add(id);
    }
    while (represent != pre.get(represent)) {
        represent = pre.get(represent);
    }
    int current = id;
    while (current != represent) {
        int father = pre.get(current);
        pre.put(current, represent);
        current = father;
    }
    return represent;
}

如果将find写为递归函数会导致爆栈。

按秩合并

每个节点的秩代表从该节点找到代表节点需要经历的步数。将两个节点合并时,需要使合并后的 秩尽量低,因此合并时将秩小的节点的代表节点设置为秩大的节点。具体内容如下:

public void merge(int id1, int id2) {
    int father1 = find(id1);
    int father2 = find(id2);
    if (father1 == father2) {
        return;
    }
    int rank1 = rank.get(father1);
    int rank2 = rank.get(father2);
    if (rank1 < rank2) {
        pre.put(father1, father2);
    } else if (rank1 == rank2) {
        pre.put(father2, father1);
        rank.put(father1, rank1 + 1);
    } else {
        pre.put(father2, father1);
    }
}

动态维护变量

本次作业中queryBlockSumqueryTripleSum需要计算network中连通块和三角形的数量,如果每次调用时计算会产生超时,需要在增删PersonRelation时就进行动态维护。

  • 增加Person时 新增加的人必然不和任何其他人连通,因此连通块数量+1;增加人不会对三角形数量产生影响
  • 增加Relation时 如果新增关系前两个人不在同一连通块内,则新增关系后两个连通块会合并为一个,此时连通块数量-1;新增关系后,如果其余人中有人和关系的双方link,则三角形数量+1
  • 删除Relation时 如果删除该关系后两人不在同一连通块内,则连通块数量+1;删除关系后,原先因为此关系而产生的三角形消失

Junit测试

本次作业编写了queryTripleSum方法的Junit测试,该测试主要有两个要点:

  • 保证pure 即方法调用前后Person数组的所有内容均不产生改变
  • 验证结果正确性

首先生成测试数据,为了保证生成稠密的图,每次添加关系时的两人中有一人为上一次添加的关系中的一方。之后创建两个network,一个用来调用方法,另一个用来存储调用前的数据。

检验结果时,按照JML规格中的表述进行复刻即可,之后通过assertEquals()断言对结果的正确性和Person的属性进行判断。

第二次作业分析

代码架构分析

本次作业的优化主要体现在求两点之间不带权最短路径的算法,以及新添加的Tag类中的计算类方法。

最短路径算法

queryShortestPath()要求求出两点之间的不带权最短路径,可以采用Dijkstra算法求出以某节点为源点到所有点的路径长度,存储在一个HashMap中,之后查找目的节点对应的值即可求得最短路径长度。需要注意的是相连的两个节点距离是0而不是1,因此需要将结果-1;但这样会导致节点和自己的距离变为-1,因此需要特殊讨论。

计算类方法的优化

本次作业中主要的计算类方法在于Tag中添加的对年龄的平均值和方差以及对queryValue求和的计算。为了避免每次查询时都重新遍历求和,在向Tag添加或删除Person时,维护ageage^2的值。当添加或删除Relation时,对queryValue的值进行维护。

静态维护bestAcquaintance

每次增加或删除关系时,都对bestAcquaintance的值进行更新,如果需要删除的PersonbestAcquaintance,则重新寻找新的bestAcquaintance。需要注意可能出现删除关系后不存在acquaintance的情况,具体代码如下:

public void delAcquaintance(int personId) {
    acquaintance.remove(personId);
    value.remove(personId);
    if (personId == bestId) {
        maxValue = 0;
        bestId = 2147483647;
        for (Integer key : value.keySet()) {
            if (value.get(key) > maxValue) {
                bestId = key;
                maxValue = value.get(key);
            } else if (value.get(key) == maxValue) {
                if (key < bestId) {
                    bestId = key;
                }
            }
        }
        if (bestId == 2147483647) {
            acqNum = 0;
        }
    }
}

Junit测试

本次作业编写了queryCoupleSum方法的Junit测试,该测试主要有两个要点:

  • 保证pure 即方法调用前后Person数组的所有内容均不产生改变
  • 验证结果正确性

本次作业的JunitTest与上次类似,此处不再赘述。

第三次作业分析

代码架构分析

本次作业没有新增太多复杂的算法,但是需要注意在delNoticeMessages中,如果一边遍历一边删除,会出现RunTimeError。需要保证遍历的容器不做更改。

Junit测试

本次作业编写了deleteColdEmojiTest方法的Junit测试,该测试主要要点如下:

  • 保证方法调用后只有heat < limit的EmojiMessage被删除
  • 保证方法调用前后其他Message的内容没有被改变
  • 保证方法调用后EmojiId与EmojiHeat的数量一致
  • 验证结果正确性

构造测试样例的方式与之前类似。本人在构造数据时出现的一个错误是,由于MessageId是随机生成的,EmojiMessageId有可能与其他类型MessageId相同,导致后构造的其他类型Message覆盖了之前构造的EmojiMessage,从而产生错误的EmojiIdListEmojiHeatList

测试过程分析

黑箱测试和白箱测试

  • 黑箱测试:随机生成测试数据,不考虑程序内部逻辑。本人主要在JunitTest中使用这种方式构造大量数据,以检测方法实现的正确性。
  • 白箱测试根据程序内部逻辑针对性构造数据。本人主要在互测过程中使用这种方式对代码的逻辑正确性进行测试。

测试规格分析

  • 单元测试:主要关注独立单元(例如一个函数、方法或类)的行为,例如通过编写JunitTest对方法进行测试。
  • 功能测试:主要关注程序的功能需求是否得到满足。
  • 集成测试:主要关注不同单之间的交互和协作是否正确。
  • 压力测试:通过大量数据测试程序在极端情况下的性能,例如强测中较大规模的数据。
  • 回归测试:修改程序后重新运行之前的测试用例,以确保该次修改没有引入新的错误或导致原有功能失效。例如在第三次作业时测试前两次作业的代码,以确保新添加的代码不影响之前的正确性。

数据构造策略

本人在进行测试时,采用黑箱测试与白箱测试相结合的方式。既生成大量随机数据检测正确性,还针对特殊情况构造数据测试代码中的逻辑问题。例如对于Tag相关内容,在增删人和关系时,容易忽略删除关系后双方不在对方的Tag这种情况。通过构造类似的测试数据,不仅发现了自己代码中存在的漏洞,还找出了同学的bug。

性能问题分析

优化方式

针对每次作业的优化均在前文提及,但本人在强测中还是出现了超时现象。原因是并查集按秩合并时,将find(id)误写为pre.get(id),导致代表节点相互嵌套,产生死循环。

规格与实现分离

JML规格语言为类和方法定义了清晰的规格,将规格定义与代码实现分离,主要有以下几个优势:

  • 提高代码可维护性 JML规格语言为类和方法定义了清晰的规格,便于对程序进行维护。
  • 模块化开发 每个模块只需关注自身的规格,并提供清晰的接口给其他模块使用。符合面向对象高内聚低耦合的思想。
  • 提高代码可扩展性 JML仅定义规格,并未约束具体实现方式,便于后续迭代开发。

心得体会

通过本单元的学习,我掌握了JML规格语言的读写和使用,初步体会到契约式编程的优势。同时也实践了许多图论算法,掌握了不少优化性能的方法,进一步提升了编程能力。

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

301

社区成员

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

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