BUAAOO第三单元总结

第三单元总结博客

设计策略

​ 第九次作业实现的东西并不复杂,只是简单的提供了一个社交系统的必要组成部分,包括Person类和支持简单操作的Network类。考虑到person的id是唯一的,因此在本次架构中大多数的容器都采用了以id作为hash值索引的存储方式,主要应用的是HashTable<>()

​ 后两次作业在架构上没什么可说的,仍然是增加了一些社交系统的组成部分,对于Message类和Group类我们很容易想到继续采用HashTable<>()进行存储,重点在于如何实现对应的功能和提高性能,这两部分在后面会阐述。

​ 在本单元测试环节,我尝试了Junit方法测试规范性,但很快还是认为对拍方式和鲁棒性测试效率更高,后来的两次作业都采用了后者进行测试。

容器选择

  • 由于Person、Message、EmojiMessage、Group的id都是唯一确定的,因此为了提高性能,有关上述对象的存储都使用HashTable容器
  • 对于Person.messages,通过阅读JML规格看出sendMessage方法和getRecievedMessage方法是依赖于收发顺序的,因此采用了Stack作为容器进行存储

性能优化

第九次作业

​ 本次作业中最大的优化点应该就是判断两点是否连通的isCircle方法和计算连通分支数的quaryBlockSum方法。考虑到本单元作业是一个动态变化的图结构,我选择了一个辅助类作为性能优化的工具,与堆优化类似,但可以随着作业迭代增加新的功能。

public class Blocks {
    private Hashtable<Integer, Integer> index = new Hashtable<>();
    private Hashtable<Integer, LinkedList<Integer>> blocks = new Hashtable<>();
	//blocks: <街区号, 街区<person.id>>
    //index:<person.id, 所在街区>,用于对id索引查表
    
    //增加新的Person, 需要增加新的街区,同时在index表中增加新的key值,由于街区号其实意义并不太大,只要不重复就行,因此依然使用了person.id
    public void addPerson(int pid) {
        blocks.put(pid, new LinkedList<>());
        blocks.get(pid).add(pid);
        index.put(pid, pid);
    }
	
    //核心方法之一,返回一个personId对应的街区号
    public int pid2index(int pid) {
        return index.get(pid);
    }

    //当增加新的关系时,若两人不在同一街区,将街区合并,并更新索引表
    public void addRelation(int id1, int id2) {
        if (pid2index(id1) == pid2index(id2)) {
            return;
        } else {
            int tmp = pid2index(id2);
            int des = pid2index(id1);
            for (int i : blocks.get(tmp)) {
                blocks.get(des).add(i);
                index.put(i, des);
            }
            blocks.remove(tmp);
        }
    }

    //返回街区总数
    public int getSize() {
        return blocks.size();
    }
}

​ 这是第九次和第十次作业使用的Block类,第十一次作业对其进行了重构。该类的核心功能是按照person1和person2之间是否认识划分街区,通过判断person1和person2是否在同一街区判断是否连通,通过街区总数计算BlockSum,具体方法含义见代码区。

第十次作业

​ 主要性能损失在Group类的几个计算方法中,解决方法很简单,在addPerson的时候就进行运算,减少每次的遍历开销,这里用到了概率统计的公式,但是利用简单的因式分解也能写出来。

private int agesum = 0;
    private long agesquaresum = 0;
    private int valuesum = 0;

    @Override
    public void addPerson(Person person) {
        int age = person.getAge();
        people.put(person.getId(), (MyPerson) person);
        agesum += age;
        agesquaresum += age * age;
        for (Integer i : people.keySet()) {
            if (people.get(i).isLinked(person)) {
                addValue(people.get(i).queryValue(person));
            }
        }
    }

    @Override
    public void delPerson(Person person) {
        int age = person.getAge();
        for (Integer i : people.keySet()) {
            if (people.get(i).isLinked(person)) {
                valuesum -= people.get(i).queryValue(person) * 2;
            }
        }
        people.remove(person.getId());
        agesum -= age;
        agesquaresum -= age * age;
    }
	
	/*
	...
	*/

@Override
    public int getAgeMean() {
        int size = getSize();
        if (size == 0) {
            return 0;
        }
        return agesum / size;
    }

    @Override
    public int getAgeVar() {
        int size = getSize();
        if (size == 0) {
            return 0;
        }
        int ageMean = getAgeMean();
        return (int) ((agesquaresum + size * ageMean * ageMean - 2 * ageMean * agesum) / size);
    }

第十一次作业

​ 显而易见,主要的性能损失在求解最短加权路径上,这里考虑到图的结构经常变化,因此采用了迪杰斯特拉算法,因为其复杂度比较低。同时为了充分利用我们之前设立的Blocks类,显然在计算是否存在最短加权路径时,首先要考虑是否两个人在相同街区,然后对相同街区内的人进行遍历即可。为此的话对之前的Blocks类进行了修改。

public class Blocks {
    //构造了一个内部类,用于封装前两次作业中的单个街区
    private class Block {
        private HashSet<Integer> pidlist = new HashSet<>();//街区中person的id集合
        private Hashtable<Integer, Hashtable<Integer, Integer>> distancemap = new Hashtable<>();//街区中任意两点之间的最短路径

        public void addpid(int pid) {
            pidlist.add(pid);
        }
		
        public HashSet<Integer> getPidlist() {
            return pidlist;
        }

        //查询是否存在id1到id2的最短路径的缓存,默认返回-1
        public int qds(int id1, int id2) {
            if (!distancemap.containsKey(id1)) {
                return -1;
            }
            if (!distancemap.get(id1).containsKey(id2)) {
                return -1;
            }
            return distancemap.get(id1).get(id2);
        }

        //重置整个map,每当有新人加入街区时都重置该街区的map
        public void resetDistance() {
            if (!distancemap.isEmpty()) {
                distancemap.clear();
            }
        }
		
        //写最短路径,path(id1->id2)=dis
        public void setDistance(int id1, int id2, int dis) {
            if (!distancemap.containsKey(id1)) {
                distancemap.put(id1, new Hashtable<>());
            }
            distancemap.get(id1).put(id2, dis);
        }
    }

    private Hashtable<Integer, Integer> index = new Hashtable<>();
    private Hashtable<Integer, Block> blocks = new Hashtable<>();

    public void addPerson(int pid) {
        blocks.put(pid, new Block());
        blocks.get(pid).addpid(pid);
        index.put(pid, pid);
    }

    public int pid2index(int pid) {
        return index.get(pid);
    }

    public void addRelation(int id1, int id2) {
        if (pid2index(id1) == pid2index(id2)) {
            blocks.get(pid2index(id1)).resetDistance();
            return;
        } else {
            int tmp = pid2index(id2);
            int des = pid2index(id1);
            for (int i : blocks.get(tmp).getPidlist()) {
                blocks.get(des).addpid(i);
                index.put(i, des);
            }
            blocks.remove(tmp);
            blocks.get(des).resetDistance();
        }
    }

    public boolean iscircle(int id1, int id2) {
        return pid2index(id1) == pid2index(id2);
    }

    public int getSize() {
        return blocks.size();
    }
	
    //查询是否存在从id1到id2的最短路径缓存
    public int qbs(int id1, int id2) {
        int index = pid2index(id1);
        return blocks.get(index).qds(id1, id2);
    }

    public void setBlockDistance(int id1, int id2, int distance) {
        blocks.get(pid2index(id1)).setDistance(id1, id2, distance);
    }
	
    //返回person.id对应的街区内的personList
    public HashSet<Integer> getBlock(int pid) {
        return blocks.get(pid2index(pid)).getPidlist();
    }
}

感想

​ 相较于前两单元的作业来说,本次作业可以说是非常简单了,只要先宏观理解JML规格以及最终要实现的功能,然后根据经验选择合适的容器及算法就好,适当的使用缓存模式进行优化。但是要注意在细节上要符合JML规格,如涉及到计算的方法要注意JML的计算过程,防止出现误差;涉及到复杂逻辑的方法要注意JML的判断顺序,防止出现漏判误判。

​ 对于异常类的处理实际比较简单,只需要按照要求在每个类内增加一个计数器就好。本次作业我没有单独增加计数器类,是因为觉得对于简单的异常类,直接复制粘贴建类也很方便,但是随着异常类的增多,管理起来也比较复杂,手动debug输出信息时也比较难找。在第二次作业中就出现了因为在第一次作业debug过程中留下的System.out语句没删,导致评测始终不通过的尴尬场景,这也提醒我们良好的架构不仅有利于读懂代码,还有利于后期的修改和维护,这在以后的代码协作过程中十分重要。

上一篇:「考试题」选课


下一篇:2020面向对象设计与构造 第三单元 博客总结