BUAA_OO Unit 3 Summary

OO Unit 3 Summary

OO第三单元的训练目标是规格化程序设计。

简而言之,课程组首先使用JML(Java Modeling Language)代替自然语言进行需求描述,随后我严格依据规格的各种条件以及副作用要求,在此基础上加以算法和数据结构层面的优化。如何实现符合规格的高性能程序,是本单元重点关注的问题。

架构设计

本单元的训练目标是依照规格进行程序设计,课程组通过接口和抽象类的形式,给出待实现的方法,并使用JML进行规格描述。而我们的任务是创建新的类来实现接口或继承抽象类,完成其中的方法。

  • 对于接口,使用implements关键字: public class MyPerson implements Person
  • 对于抽象类,使用extends关键字: public class EqualRelation extends EqualRelationException

Emoji类需要实现课程组给出的接口EmojiMessage中的方法,而EmojiMessage又继承了Message接口,因此Emoji类需要实现Message接口中的方法。我使Emoji类继承由我实现的MyMessge类,节省了大量的代码量。

public interface EmojiMessage extends Message          // 课程组官方包
public class MyMessage implements Message
public class Emoji extends MyMessage implements EmojiMessage

我建立了一个异常计数器类Counter,用于统计与特定人关联的异常次数。在异常类中定义静态属性计数器: private static final Counter counter = new Counter();,可以在每次新建异常对象时,正确存储并获取历史异常次数

public class Counter {
    private final HashMap<Integer, Integer> exceptionTimes;

    public Counter() {
        exceptionTimes = new HashMap<>();
    }

    public int getTimes(int id) {
        if (exceptionTimes.computeIfPresent(id, (key, value) -> value + 1) == null) {
            exceptionTimes.put(id, 1);
        }
        return exceptionTimes.get(id);
    }
}

由于需要实现的类以及它们之间的层次关系已经确定,在整体架构上没有太大发挥的空间,因此我们的工作重点在于如何优化方法的实现。其中的一个关键是如何构建并维护社交网络这一图的各种信息。

以下分别从各次作业的需求入手,介绍图的优化思路。

Hw9

在Network类中,JML给出了类的属性Person[] people,即社交网络中人的集合,若不假思索翻译规格,我们可以使用容器ArrayList进行存储。

但实际实现中,我发现经常需要通过person的id寻找特定的人,使用ArrayList时只能通过遍历对比id的方式进行查找,时间复杂度为 O ( n ) O(n) O(n)。因此,我使用HashMap来存储人的信息,key为id,value为Person对象,通过id可以直接找到对应的Person对象。由于Network类中的方法普遍基于Person的id进行操作,可以节省大量时间,并且十分简洁。

    private final HashMap<Integer, Person> people;

    public boolean contains(int id) {
        return people.containsKey(id);
    }

    public Person getPerson(int id) {
        return people.getOrDefault(id, null);
    }

contains()和getPerson()方法的时间复杂度均为 O ( 1 ) O(1) O(1)

并查集

并查集是一种树形的数据结构,用于处理不相交集合的合并问题。并查集维护了一个由多个不相交集合组成的集合族,每个集合通过一个代表元素来标识。它的基本操作有合并查找

  • 查找
    用于确定元素所属的集合,可通过根节点判断两个元素是否属于同一个集合,即它们之间的连通性。通常使用递归或迭代的方式实现,时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)
    这里使用了路径压缩,即在查找的过程中,将路径上的所有节点直接连接到根节点上,从而减少树的高度。
    public int find(int id) {
        int root = id;
        Stack<Integer> stack = new Stack<>();
        while (root != parent.get(root)) {
            stack.push(root);
            root = parent.get(root);
        }

        while (!stack.isEmpty()) {
            parent.put(stack.pop(), root);  // 路径压缩
        }
        return root;
    }
  • 合并
    将两个不相交的集合合并为一个集合。按秩合并是指将高度较小的树连接到高度较大的树上,避免了树的深度过大。这里使用了一个HashMap height来存储每个根节点的高度,高度为0表示该节点为叶子节点。
    public void union(int id1, int id2) {
        int root1 = find(id1);
        int root2 = find(id2);
        if (root1 == root2) {
            return;
        }

        int height1 = height.get(root1);
        int height2 = height.get(root2);
        if (height1 < height2) {           // 按秩合并
            parent.put(root1, root2);
            height.remove(root1);
        } else {
            if (height1 == height2) {
                height.put(root1, height1 + 1);
            }
            parent.put(root2, root1);
            height.remove(root2);
        }
    }

在每次建立关系时维护并查集后,可以迅速解决查询连通性的isCircle()方法,查询连通块的数量的queryBlockSum()方法。可见,并查集在解决图的连通性问题上十分优雅。

    public boolean isCircle(int id1, int id2) {
        return disjointSet.find(id1) == disjointSet.find(id2);
    }

    public int queryBlockSum() {
        return disjointSet.getHeight().size();
    }
TripleSum

TripleSum是指社交网络中存在三个人,它们两两之间认识,统计这样三人组的数量。一种暴力搜索的方法是遍历每个人的朋友,再遍历其所有的朋友,若其与最初的人认识,则三人组的数量加一。但这样的实现时间复杂度为 O ( n 3 ) O(n^3) O(n3),显然不符合性能要求。

因此我采用了动态维护TripleSum的方法。在addRelation()方法中,当a和b建立关系时,遍历a的所有朋友,若其与b认识,则三人组的数量加一。这样,在每次建立关系时动态维护,可以将时间复杂度降为 O ( n ) O(n) O(n)

    HashMap<Integer, Integer> acq1 = getPerson(id1).getAcquaintances();
    for (int id : acq1.keySet()) {
        HashMap<Integer, Integer> acq = getPerson(id).getAcquaintances();
        if (acq.containsKey(id2)) {
            tripleSum++;
        }
    }

Hw10

本次作业中新增了群组功能,与people同理,使用HashMap存储群组信息,可通过group的id快速查找到对应的群组: private final HashMap<Integer, Group> groups;

modifyRelation()

本次作业出现了社交网络中关系变动的需求,涉及到关系亲密度的变化和删除关系,而删除关系是并查集极难处理的问题。

因此我把每条边的信息备份,每次建立关系时将其加入,删除关系时将其移除。这样,每次删除关系后,只需要遍历备份中的所有边,重新建立并查集即可。

    public void rebuild(int id1, int id2) {
        height.clear();
        for (int id : parent.keySet()) {
            parent.put(id, id);
            height.put(id, 0);
        }

        backup.remove(new Pair<>(id1, id2));
        for (Pair<Integer, Integer> pair : backup) {
            union(false, pair.getKey(), pair.getValue());
        }
    }

删除关系后同样需要维护TripleSum,遍历时将三人组的数量减一即可。

CoupleSum

CoupleSum是指社交网络中存在两个人,他们互为最好朋友,统计这样的人的数量。由于关系在不断变化,动态维护社交网络的该信息较为复杂,且维护时的遍历操作无法避免。因此我采用了JML的朴素写法,遍历每个人,若其与其最好朋友互为最好朋友,则数量加一。时间复杂度为 O ( n 2 ) O(n^2) O(n2)

    public int queryCoupleSum() {
        int ans = 0;
        for (int id : people.keySet()) {
            Person person = getPerson(id);
            if (person.getAcquaintances().isEmpty()) {
                continue;
            }
            int acq = person.getBestAcq();
            if (getPerson(acq).getBestAcq() == id) {
                ans++;
            }
        }
        return ans / 2;
    }

虽然CoupleSum没有动态维护,但每个人的最好朋友信息仍可以在关系变动时动态维护,逻辑较为清晰。

BestAcquaintance

在Person类中,我新增了一个属性bestAcq,用于存储该人的最好朋友的id,便于Network类对CoupleSum的查询。

  • 关系变好
    适用于新建立关系或关系的亲密度增加。
    若变动关系的亲密度大于原有的最好朋友关系的亲密度,则更新该人的最好朋友信息
  • 关系变坏
    有删除关系或关系的亲密度减少两种可能。
    若变动的关系为该人的最好朋友关系,则重新遍历所有朋友,找到亲密度最大的关系,更新最好朋友信息。若不为该人的最好朋友关系,则不需要维护
    public void findBestAcq(int type, int id, int value) {
        if (type == 1) {            // 关系变好
            if (value > bestValue || (value == bestValue && id < bestAcq)) {
                bestAcq = id;
                bestValue = value;
            }
        } else {                    // 关系变坏
            if (id == bestAcq) {
                int max = 0;
                int minId = 0;
                for (int key : acquaintances.keySet()) {
                    int now = acquaintances.get(key);
                    if (now > max || (now == max && key < minId)) {
                        minId = key;
                        max = now;
                    }
                }
                bestAcq = minId;
                bestValue = max;
            }
        }
    }

Hw11

使用HashMap存储表情包信息,键值对为<id, heat>,可通过表情包的id快速查找到对应的表情包的使用次数: private final HashMap<Integer, Integer> emojis;

LeastMoments

求社交网络中经过某个人的最小环,边的权值即为关系的亲密度。我使用SPFA算法求出该源点到其余点的最短路,时间复杂度为 O ( n 2 ) O(n^2) O(n2),符合性能要求。

一个环需要三条不同的边,因此可分两种情况由最短路求出最小环:

  • 额外边包含源点,最小环由额外边和一条长度≥2的最短路构成
  • 额外边不包含源点,最小环由额外边和两条最短路构成
    public int leastMoments(int id) {
        int ans = inf;
        int index = match.get(id);
        spfa(index);

        for (int i : matrix.get(index).keySet()) {  // 额外边包含源点
            if (parent[i] != i) {
                ans = Math.min(ans, getValue(index, i) + dis[i]);
            }
        }
        for (int i = 0; i < num; i++) {             // 额外边不包含源点
            for (int j : matrix.get(i).keySet()) {
                if (i != index && j != index && findRoot(i) != findRoot(j)) {
                    ans = Math.min(ans, dis[i] + dis[j] + getValue(i, j));
                }
            }
        }
        return ans;
    }

性能优化

JML规格只给出了方法的基本限制,其常使用循环等写法来约束方法前后的变化,若直接照搬,会导致程序性能较差。因此,需要依据规格的各种条件以及副作用要求,在此基础上加以算法和数据结构层面的优化。

  • 数据结构优化
    我常用的方法是优化数据的存储结构,选用容器HashMap替代JML中的数组写法,设置key为id,使得数据的查找、插入、删除等操作更加高效。
  • 算法优化
    JML中涉及到图的方法,通常使用多层遍历的暴力搜索方法,只保证了正确性,牺牲了性能,也不够简洁。因此,我的优化思路是将静态算法变为动态维护,或优化静态算法的时间复杂度。具体来说我使用了SPFA算法求最短路,并查集求连通块,动态维护等方法,使得程序更加高效。

因此,我们应该认识到,JML不代表具体的实现方式,在实现时不能机械地翻译JML,而是将其作为需求的一种描述,在符合其约束的条件下进行高性能的实现。

OKTest

OK测试方法对于检验代码实现与规格的一致性起着重要的作用。通过使用OK测试方法,开发人员可以更加可靠地确保代码与规格的一致性,提高软件质量。

由于OKTest的方法不需使用Network类中的属性,我将其与Network类分离,单独新建一个OK测试类,作为静态方法存放。

qts

全称为queryTripleSumOKTest,对应查询三人组数量的queryTripleSum()方法。

由于原方法是pure方法,不改变类的任何属性,因此首先检查方法前后是否产生副作用。随后严格根据规格写法,采用JML中时间复杂度为 O ( n 3 ) O(n^3) O(n3)的暴力搜索方法,与正确输出进行比较。

mr

全称为modifyRelationOKTest,对应变动关系的modifyRelation()方法。

首先检查方法是否需要抛出异常但并未抛出,正常完成了方法。随后检查方法是否满足规格要求,即检查JML的后置条件ensure,按JML代码的行进行检查,若ensure条件不满足,返回最先不满足的行数,若满足,返回0。

dce

全称为deleteColdEmojiOKTest,对应删除冷门表情的deleteColdEmoji()方法。

该方法无需抛出异常,其余部分与mr同理,按行检查JML并返回错误行数即可。

心得体会

JML和使用自然语言的需求描述区别较大,通过本单元的大量练习,我培养了阅读JML的能力,能够快速理解JML的含义,在此过程中主动学习了一些图论中的算法,受益匪浅。

同时,我也学会了如何使用JML来描述需求,如何使用JML来检验代码实现与规格的一致性,对软件工程的需求分析和测试方法有了更深刻的认识。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值