BUAA OO 第三单元总结

第一次作业

本单元主要用来熟悉JML,实现难度不大。需要注意设计的点在于isCircle函数。

首先当然是用HashMap来保存id与Person类之间的对应关系(O1的复杂度谁不喜欢呢)。我将某个Person与其“邻居”的关系存在了MyPerson类中(HashMap映射Personvalue),将一个复杂的二维图,一维存在MyNetWork类中,另一维存在MyPerson类中,避免MyNetWork类中的容器过于复杂(HashMap嵌套HashMap),分层设计,分散复杂度。

并查集

查询两点是否连通的较快方法自然是并查集(实现isCircle方法和queryBlockSum方法),复杂度不到On。并查集一般有两种优化,路径压缩和按秩合并,这两种优化都比较简单。
仍然是用HashMap记录id与其根节点id的关系、记录id所在连通块的秩。
只需使用简单的递归即可实现路径压缩,在沿路径向上查询的过程中,把路径上节点的父节点都修改为根节点。

public int find(int id) {
        if (id == father.get(id)) {
            return id;
        } else {
            father.put(id, find(father.get(id)));
            return father.get(id);
        }
    }

但现在看来,我犯了一个很严重的问题,路径压缩后再根据深度去合并树,这是无效的。因为当查询次数较多后,在连通块合并前,每个连通块都是“菊花图”,深度都是1,这时按深度合并没有意义,就是完全随机合并了。若根据根节点下的节点数量合并,将节点数少的根接到节点数多的根上,这样在之后查询时更多的节点只需向上查询一次即可,少部分需要向上查询两次。所以这个“按秩合并”的秩,指的是根下面的节点数量,而不是根下面树的深度。
对于查询连通块个数,只需设置计数变量,在添加Person时+1,在合并树时-1即可。

第二次作业

MyNetWorkMyGroup类中的各种映射关系,比如message的id到message对象的映射等等,均用HashMap存储。

异常类

注意到每个异常类都含有计数部分,若不将这部分提取出来,每加一个异常类就需要复制一份计数代码,还要针对不同类型的异常做一些修改(异常类的构造函数参数数量不同),若计数部分逻辑出现错误,则需要修改所有异常类中的逻辑,实在太过复杂且不优雅,故我从所有异常类中,将计数器作为一个类抽离出来,异常类中只需构造一个计数器的静态对象即可。于是一个异常类就能简化成下图这个样子。计数器的add函数可完成相应id的计数增加功能,且借助方法重载,既可传1个id也可传2个id,解决了不同种类的异常类的计数需求。当然计数器中也是用HashMap存储id和异常触发次数的映射关系。

public class MyEqualRelationException extends EqualRelationException {

    private static Counter counter = new Counter();

    private int id1;
    private int id2;

    public MyEqualRelationException(int inputId1, int inputId2) {
        id1 = inputId1;
        id2 = inputId2;
        if (id1 > id2) {
            int tmp = id1;
            id1 = id2;
            id2 = tmp;
        }
        counter.add(id1,id2);
    }

    public void print() {
        ...
    }
}

Group类

  • getAgeMean直接在加减人的时候相应通过加减维护ageMean变量即可,O1的维护成本。
  • valueSum变量则需要在加减人时候遍历组内的所有人来维护,复杂度由直接计算的On²变为On。但同时需要在addRelation操作时,也要遍历包括这俩Person的所有Group,给这些GroupvalueSum加上2倍的value。这需要在MyPerson类中添加存放其所在Group的容器,并且在addToGroupdeleteFromGroup时维护这个容器,成本On,一定程度上增加了各个类之间的耦合度。
  • ageMean变量缓存的情况下,ageVar复杂度降为On,我通过添加脏位,只有当加减人之后getAgeVar才会重新计算方差,这样可改善连续查询时的性能。

第三次作业

容器的使用

虽说HashMap有着O1的查询时间复杂度,使用方便快捷。但实际生产中,大量的使用HashMap对内存显然非常不友好(尤其是HashMap嵌套),显然不能按照我这种HashMap遍地都有的方式去写项目,本次强测中,有一个测试点的内存使用高达164M,这就太夸张了。
来自我同学,一个减少HashMap的使用方法:可以发现,在我的代码中众多的HashMap中,很多都是Person的id对某一对象的映射,也就是说id在不同的地方映射了很多不同的东西,而且,很显然用ArrayList存这些被映射的对象肯定是比HashMap节省不少内存的。那么就可以按照addPerson的顺序,用一个HashMapPerson的id映射到0,1,2......即数组的下标。这样就可以用一些ArrayList存被id映射的对象了。每得到一个id,只需经过“一个”HashMap的映射为数组下标,再去到相应的ArrayList里寻找需要的相应对象,查找时间复杂度也是O1。

三种Message

题目中要求的三种message显然和原先的普通Message太像了,只是加了个额外属性,所以这三类直接继承自己写的MyMessage类即可,构造函数也先调用父类的,再赋值自己特有的变量,需要注意设置传给父类构造函数的socialValue

delete_cold_emoji

有个我没有采用的,比较简单的办法。在遍历emoji容器时,发现冷门emoji后,直接remove掉相应的映射,遍历完成后,再遍历存Message的容器,判断每一个message对象是否为emoji message,若是,则判断这个message的emojiId在存emoji的容器中是否存在,若不存在则删除该message。这么干的时间复杂度显然On,但肯定不会超时。

我采用的方法是:建立一个从emojiId到存message Id的set的映射,如下

private final HashMap<Integer, HashSet<Integer>> emojiIdToMessage = new HashMap<>();

在添加emoji时会在该map中建立新的映射,添加emojiMessage时会向该emojiIdToMessage对应映射的set中添加message的id。遍历emoji容器时,发现冷门emoji后,仍是直接remove掉相应的映射,但会在emojiIdToMessage找到对应的set容器,遍历该set,根据其中存在message id,在存message的map中也一一对应删除message,最后再移除emojiIdToMessage中相应的映射。这样的时间复杂度是O1,但空间复杂度就比较高,尤其是我这种HashMap嵌套HashSet

在互测后看了同学的代码,发现我这一方法在细节上还可以优化,虽然很微不足道。我是在store emoji时就在emojiIdToMessage添加了相应的映射,但假如长时间没有添加对应的emojiMessage,那这个映射就一直闲置了,甚至可能在删除冷门表情时被删掉,这就是不必要的占用内存了。而我的同学,是在addMessage时候,才在emojiIdToMessage中添加相应映射(如果是emojiMessage),这就保证了建立的映射一定不会被闲置浪费。

最短路径

我和很多人一样,也用的堆优化的Dijkstra,并且将Dijkstra单独分出去了一个类,最短路径的缓存也放在了这个类中,实现了与MyNetWork类的解耦。

针对连续查询,我没有在查到终点id时break,而是一次把一个点到其他所有点的最短路径都计算完,并用二维的HashMap做了缓存。

private final HashMap<Integer, HashMap<Integer, Integer>> cache = new HashMap<>();

public void clearCache() {
    cache.clear();
}

public int find(HashMap<Integer, Person> people, int startId, int endId) {
    HashMap<Integer, Integer> distance = cache.get(startId);
    if (distance != null) {
        return distance.get(endId);
    } else {
        dijkstraWithHeap(people, startId);
        return cache.get(startId).get(endId);
    }
}

dijkstraWithHeap方法就是具体的堆优化Dijkstra
find方法在sendIndirectMessage方法中被调用,返回起点和终点的最短路径长度。
clearCache方法在MyNetWork类中的addRelation方法中被调用,用以清空所有缓存。

我第三次强测有一个点被hack。在Dijkstra中,我之前认为,每更新一次距离就要重新new一个新node再add进优先队列,如此频繁的new实在不太优雅,于是我就想办法在更新距离时,修改原先在堆中的节点中存的distance值,然后把原节点再add进堆。这么做会破坏堆的“上小下大”特点,导致之后add时出现堆顶元素不是最小节点的情况。所以显然这么修改是错误的。

还有一个可能的优化,由于时间限制我没加上。普通Dijkstra的复杂度为V²+E,堆优化的Dijkstra的复杂度为E*logV,当图是稠密图时,普通的Dijkstra反而比堆优化更快。应该加一个特判,以选择是否启用堆优化。

细节问题

研讨课上,助教提醒了HashMap的get方法和contains方法的注意事项,若原本是“contains+get”的逻辑,可用“get+存入中间变量+判断等于null+使用中间变量”来实现,这样可减少底层函数的调用次数。如下面两图所示,前者为我修改前的sendMessage函数,看上去非常的丑啊,而且多次重复调用contains和get函数,冗余操作很多,后者为修改后的,不仅简洁美观、可读性强,而且性能较好

posted @ 2021-05-30 19:46  Running-Noob  阅读(88)  评论(0)    收藏  举报