OO第三单元JML总结

OO第三单元JML作业总结

一、实现规格所采取的设计策略

本单元的作业旨在实现一个社交关系模拟系统。

  • 首先,由于异常类的功能基本相同,所以先完成几个异常类。
  • 然后,结合官方包代码中的类名和各个类的代码规格可以确定MyNetwork类是代码量最大,功能最复杂的类,是最上层的类,所以先完成除MyNetwork类的其他几个类,最后实现MyNetwork类。
  • 在实现各个类的过程中,先结合类名和方法名猜各个方法的实际意义,然后结合给出的JML规格进一步理解并且实现。如果JML规格非常复杂,比如sendIndirectMessage方法的规格有40+行,就把规格分成几部分,分别考虑其实际意义然后实现,保证完全按照JML的规格实现。
  • 另外,在实现JML规格的过程中,较多地使用了HashMap容器,并且采用并查集堆优化Dijkstra等算法尽量减小方法的时间复杂度。

二、基于JML规格来设计测试的方法和策略

本单元虽然推荐我们使用Junit进行测试,但是在本单元第一次作业使用了Junit后,感觉并不是很好用(应该还是太弱了不会用),而且第二次第三次作业很多方法的规格非常复杂,就不想搞Junit测试了Orz。

主要的测试还是以黑盒对拍测试为主。随机生成数据为主,主要对于程序整体的功能正确性进行测试,手工构造数据为辅,主要对于JML规格中时间复杂度较高的方法进行测试,卡时间。

虽然前两次作业并没有出问题,但是第三次作业的评测机数据生成写的不太好,导致有一个异常类的一处小笔误没有检查出来导致强测爆炸了QAQ,还是有些后悔没有用Junit全面测试一下的。

三、容器的选择和使用

在本单元的三次作业中主要采用了HashMapArrayList容器,在堆优化Dijkstra算法中还使用了PriorityQueue容器。

  • MyPerson

    • HashMap<Person, Integer> friends // <acquaintance, value>:由于acquaintancevalue是一一对应的,所以使用HashMap来存储。
    • ArrayList<Message> messages:考虑到向MyPerson添加Message时需要考虑插入顺序的问题(需要头插),getReceivedMessages方法也与插入的顺序有关,所以采用ArrayList来存储,添加时使用arraylist.add(int index,E element)方法即可。
  • MyGroup

    • ArrayList<Person> people:最初考虑使用HashMap来存储,但是考虑到MyGroup类中的方法基本不涉及到对这个容器中Person的查询操作,都是遍历,不需要以键值对的形式进行存储,所以直接使用了ArrayList
  • MyNetwork

    • 由于PersonGroupMessageEmoji都具有一个独一无二的id属性,并且MyNetwork类中增删改查的操作很多,所以都采用HashMap进行存储。

    • HashMap<Integer, Person> people; //<person.id, person>

    • HashMap<Integer, Group> groups; //<group.id, group>

    • HashMap<Integer, Message> messages; //<message.id, message>

    • HashMap<Integer, Integer> emojis; //<emoji.id, emoji.heat>

    • 另外由于采用了并查集进行优化,所以还有两个容器。

    • HashMap<Integer, Integer> father; //<person.id, fatherId>:把每个人的id和其父结点的id作为键值对进行存储。

    • HashMap<Integer, Integer> rank; //<person.id, rank>:把每个人的id和其对应的rank作为键值对进行存储。

四、性能问题

出现性能问题的原因是算法的时间复杂度过高,在本单元的三次作业中有四个方法有可能会出现性能问题:MyNetwork类中的queryBlockSum方法和sendIndirectMessage方法,这两个方法分别对应图论中的求连通块数和求最短路问题;MyGroup类中的getValueSum方法和getAgeVar方法。

  • queryBlockSum方法(求图的连通块数):最初这个方法是用BFS算法实现的,功能上确实没什么问题,但是和同学对拍的过程中就发现程序运行时间很长,并且如果手工构造数据,时间会特别长QAQ。后来使用并查集进行优化,实现并查集的过程中还实现了路径压缩按秩合并进行进一步的优化。这样一来,只需要在addRelation方法中进行操作,queryBlockSum方法的复杂度就降低到了O(n),只需要遍历所有的结点,父结点的个数也就是连通块数:

    @Override
    public int queryBlockSum() {
        int count = 0;
        for (int eachId : people.keySet()) { //单重循环,看一共有几个父结点
            if (father.get(eachId) == eachId) {
                count++;
            }
        }
        return count;
    }
    

    啰嗦一句,并查集的实现有递归和非递归两种实现方法,为了防止爆栈,建议使用非递归实现。

    //递归实现
    private int find(int id) {
    	if (id == father.get(id)) {
    		return id;
    	}
    	else {
    		father.replace(id, find(father.get(id)));
    		return father.get(id);
    	}
    }
    //非递归实现
    private int find(int id) {
    	while (id != father.get(id)) {
    		father.replace(id, father.get(father.get(id)));
    		id = father.get(id);
    	}
    	return id;
    }
    //并没有压缩到最优
    
  • sendIndirectMessage方法(求图中两结点的最短路):由于两个人之间的value值只能为非负值,所以这个方法可以使用Dijkstra算法实现,而朴素的Dijkstra算法的时间复杂度是O(n2),考虑到数据最长可以达到10000行,所以采取堆优化,可以把时间复杂度降低到O(mlogn),避免出现性能问题。并且由于java内置了优先队列PriorityQueue容器,所以堆优化的实现非常简单,只需要重写EdgecompareTo方法。

    PriorityQueue<Edge> queue = new PriorityQueue<>();
    
    public class Edge implements Comparable<Edge> {
        private Person to;
        private int value;
    
        /*————————————————*/
    
        @Override
        public int compareTo(Edge o) {
            return value - o.getValue();
        }
    }
    
  • getValueSum方法和getAgeVar方法:这两个方法的处理就不像前两个方法那样涉及算法的问题了。只需要维护两个变量valueSumageVar即可,初始化为0,在向Group加人和删人的时候进行修改即可(我还设置了ageSum变量,便于求年龄平均值):

    private int valueSum;
    private int ageSum;
    private int ageVar;
    
    @Override
    public void addPerson(Person person) {
        ageSum += person.getAge();
        people.add(person);
        ageVar = 0;
        int ageAverage = getAgeMean();
        for (Person eachPerson : people) {
            valueSum += 2 * person.queryValue(eachPerson);
            ageVar += (eachPerson.getAge() - ageAverage) * (eachPerson.getAge() - ageAverage);
        }
    }
    

五、作业架构设计

  • 本单元的架构设计已经由官方包代码给出,我们只需要按照JML规格实现即可,不需要考虑太多,除了要求实现的类,仅在第三次作业中为了实现堆优化Dijkstra算法添加了Edge类。

  • 图模型构建与维护策略:

    • 构建:其实图模型的构建JML规格已经为我们实现了,Network就是一张图,每个Person就是一个结点,为两个Person添加关系也就是连接两个结点,两个人之间的value值就是边的权值,Personacquaintance其实就是邻接表。在完成作业的过程中,除实现了JML规格规定的图模型以外,因为使用了并查集,所以构建了一个新的图,新图的结点与Network的结点相同。

    • 维护:对于已有的图模型,要做的就是严格按照给出的JML规格实现各个类及其方法即可。对于新构建的图,具体操作如下:

      //addPerson方法中
      father.put(person.getId(), person.getId()); //将父节点设置为自己
      //addRelation方法中
      //对加关系的两个Person进行合并操作(Merge)
      int father1 = find(id1);
      int father2 = find(id2);
      if (rank.get(father1) <= rank.get(father2)) {
      	father.replace(father1, father2);
      }
      else {
      	father.replace(father2, father1);
      }
      if (rank.get(father1).equals(rank.get(father2)) && father1 != father2) {
      	rank.replace(father2, rank.get(father2) + 1);
      }
      //find方法(为实现并查集)中
      //路径压缩,具体实现见性能问题部分
      
posted @ 2021-05-28 16:17  神樂坂清清  阅读(113)  评论(0)    收藏  举报