OO_Unit3总结

Posted MOC8

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OO_Unit3总结相关的知识,希望对你有一定的参考价值。

OO_Unit3总结

本单元的作业是基于JML规格来进行社交网络的模拟,考察的重点一是对JML规格的理解,学会读懂各个方法的规格来完成相应的方法设计。而是对一些基本算法的掌握,学会使用时间复杂度较低的算法来避免程序运行超时。因为不涉及架构设计,只需按照JML编写已设计好的结构,所以总体难度和复杂度来讲要低于前面两个单元,并且本次作业是一个严格的迭代开发模型,一般来说不会进行重构。


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

根据规格写代码时,第一步是要将规格中的各种情况抽取出来。例如哪些是正常情况,哪些是异常情况,根据这些情况对自己实现的这个方法设计相应的分支结构。在每个分支中,要理解清楚当执行完这个方法后,要返回什么,对现有的变量造成什么影响,已经何处要抛异常等操作,例如以下这个方法addToGroup

    /*@ public normal_behavior
      @ requires (\\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id2) &&
      @           (\\exists int i; 0 <= i && i < people.length; people[i].getId() == id1) &&
      @            getGroup(id2).hasPerson(getPerson(id1)) == false &&
      @             getGroup(id2).people.length < 1111;
      @ assignable groups;
      @ ensures (\\forall int i; 0 <= i < groups.length; \\not_assigned(groups[i]));
      @ ensures (\\forall Person i; \\old(getGroup(id2).hasPerson(i));
      @          getGroup(id2).hasPerson(i));
      @ ensures \\old(getGroup(id2).people.length) == getGroup(id2).people.length - 1;
      @ ensures getGroup(id2).hasPerson(getPerson(id1));
      @ also
      @ public normal_behavior
      @ requires (\\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id2) &&
      @           (\\exists int i; 0 <= i && i < people.length; people[i].getId() == id1) &&
      @            getGroup(id2).hasPerson(getPerson(id1)) == false && 
      @             getGroup(id2).people.length >= 1111;
      @ assignable \\nothing
      @ also
      @ public exceptional_behavior
      @ signals (GroupIdNotFoundException e) !(\\exists int i; 0 <= i && i < groups.length;
      @          groups[i].getId() == id2);
      @ signals (PersonIdNotFoundException e) (\\exists int i; 0 <= i && i < groups.length;
      @          groups[i].getId() == id2) && !(\\exists int i; 0 <= i && i < people.length;
      @           people[i].getId() == id1);
      @ signals (EqualPersonIdException e) (\\exists int i; 0 <= i && i < groups.length;
      @          groups[i].getId() == id2) && (\\exists int i; 0 <= i && i < people.length;
      @           people[i].getId() == id1) && getGroup(id2).hasPerson(getPerson(id1));
      @*/
    public void addToGroup(int id1, int id2) throws GroupIdNotFoundException,
            PersonIdNotFoundException, EqualPersonIdException;

该方法有两个正常情况和三个异常情况,要求满足人与组都要存在在这个network中,且人本身不在组中,根据组中人数是否大于1111来决定是否把这个人加入到组中。当人或组不在network中或是人已在组中时抛出相应异常。综上,实现的代码如下:

	@Override
    public void addMessage(Message message) throws EqualMessageIdException,
            EmojiIdNotFoundException, EqualPersonIdException {
        if (messages.containsKey(message.getId())) {
            throw new MyEqualMessageIdException(message.getId());
        } else {
            if (message instanceof EmojiMessage &&
                    !containsEmojiId(((EmojiMessage) message).getEmojiId())) {
                throw new MyEmojiIdNotFoundException(((EmojiMessage) message).getEmojiId());
            } else if (message.getType() == 0) {
                if (message.getPerson1().equals(message.getPerson2())) {
                    throw new MyEqualPersonIdException(message.getPerson1().getId());
                } else {
                    this.messages.put(message.getId(), message);
                }
            } else {
                this.messages.put(message.getId(), message);
            }
        }
    }

一般而言,实现的代码长度比JML规格的长度要短,这是影响JML将所有的效果都用最基本的语言进行描述,确保没有二义,这也是JML能作为java开发中常用的形式化描述的原因之一。


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

在本单元的作业中,我设计测试时仍然还是像前两个单元一样使用数据生成+测评机的方式来进行测试,而没有使用Junit工具来编写单元测试。但是我在设计数据生成的时候,有意的不让数据太随机,而是采用模拟简易network的方式去针对每种方法的每种情况进行测试,从而达到基于规格测试的效果。

一般的数据生成器:几乎所有的数据完全随机,导致在测评时真正有效的信息很少,降低了测评效率,输出的结果基本上全是各种异常输出一层一层往上叠。

基于模拟network的生成器:能够从特定的数据集取数据来模拟各种方法的正常,异常情况,从而生成能够测试各种方法的程序。

class Network:
    def __init__(self):
        self.people = []
        self.groups = []
        self.noemptygroups = []
        self.groupsinside = {}
        self.messages = []
        self.relations = []
        self.emojis = []
        self.deledmes = []

以上是建议社交网络的元素,list中只存id信息,用于作为数据集提取。

生成某单条指令的方法也是基于此网络构造的,例如:

#########################
def atg_normal(mynetwork):
    id2 = random.choice(mynetwork.groups)
    id1 = random.choice(list(set(mynetwork.people).difference(set(mynetwork.groupsinside[id2]))))
    if id1 not in mynetwork.groupsinside[id2]:
        mynetwork.groupsinside[id2].append(id1)
        if id2 not in mynetwork.noemptygroups:
            mynetwork.noemptygroups.append(id2)
    cmd = "atg " + str(id1) + " " + str(id2)
    return cmd

def atg_pnf(mynetwork):
    id1 = random.randint(1, intrange)
    id2 = random.choice(mynetwork.groups)
    if (id1 in mynetwork.people) and (id1 not in mynetwork.groupsinside[id2]):
        mynetwork.groupsinside[id2].append(id1)
        if id2 not in mynetwork.noemptygroups:
            mynetwork.noemptygroups.append(id2)
    cmd = "atg " + str(id1) + " " + str(id2)
    return cmd

def atg_gnf(mynetwork):
    id1 = random.choice(mynetwork.people)
    id2 = random.randint(1, intrange)
    if (id2 in mynetwork.groups) and (id1 not in mynetwork.groupsinside[id2]):
        mynetwork.groupsinside[id2].append(id1)
        if id2 not in mynetwork.noemptygroups:
            mynetwork.noemptygroups.append(id2)
    cmd = "atg " + str(id1) + " " + str(id2)
    return cmd

def atg_epe(mynetwork):
    id2 = random.choice(mynetwork.noemptygroups)
    id1 = random.choice(mynetwork.groupsinside[id2])
    cmd = "atg " + str(id1) + " " + str(id2)
    return cmd

在这些原子操作都执行完毕后,就可以开始进行最后的数据生成,将原子操作拼接成数据,只要按照一定顺序将各种指令拼接,即可形成相应的结构。

class Datastruct_undifined:
    def __init__(self):
        self.total = 0
        self.ap_normal = 0        #其中插入qps
        self.ap_epe = 0
        self.ar_normal = 0        #其中插入qbs
        self.ar_pnf1 = 0
        self.ar_pnf2 = 0
        self.ar_ere = 0
        self.qv_normal = 0
        self.qv_pnf1 = 0
        self.qv_pnf2 = 0
        self.qv_rnf = 0
        self.cn_normal = 0
        self.cn_pnf1 = 0
        self.cn_pnf2 = 0
...

以下是几种具有不同特点的结构(篇幅有限,不展示全):

image

从左至右分别是:

  • 对所有方法进行测试的数据点。
  • 对最短路径算法进行专项测试的数据点。
  • 对第三次作业新增方法的重点测试的数据点。

要生成具有不同倾向的测试点时,只需对数据结构体进行修改即可。

这样生成的数据集,优点在于能仅用数据生成器来模拟单元测试,并且可以快速的对所有指令的功能进行全覆盖无死角的评测,一般来说有功能错误的程序在我这里一个点都过不去。但与此同时,该测试集的缺点在于生成的数据随机性太弱,各种指令的出现顺序是一定的,虽然在数据结构中对某些可能具有牵连效应的指令多次出现,但还是无法避免测不出某些十分十分不常见的bug。

我们的数据生成器都是经过专业训练的,无论多少功能Bug呢,我们都能测出来

——除非太奇葩。

不过我在搭建测评机的时候是和其他几个同学共同完成的,因此结合其他人的生成器能一定程度上避免这个问题。

测试集和作业代码一样,在这个单元都是进行迭代开发实现的,最后一次的代码量达到了将近1000行,比作业本身的代码量还要大。虽然其中大多是机械的步骤,但真正写起来还是相当锻炼人的。


3、容器选择和使用的经验

在本单元作业中,一共使用率4种Java容器,分别为:

  • ArrayList
  • HashSet
  • HashMap
  • PriorityQueue

ArrayList

ArrayList是java中的可变数组,可以实现动态的增加,删减数据。由于该容器在许多查询操作上效率较低,因此在本单元中采用的很少。唯一用到的地方是在Person的消息列表中,由于部分指令要求取出靠前的几项,所以具有索引功能的ArrayList成为了这一部分的最佳选择。

HashSet

HashSet是基于哈希表的集合类,要求其中的元素不重复,且具有较高的查询效率,在本单元中一般作为id集合,所用到的位置有Network中的EmojiId集合以及在迪杰斯特拉算法中的已到达节点id集合。

HashMap

HashMap是java中的哈希表键值对,要求其中的Key不能重复,在本次作业中,涉及id这一不能有重复值的属性,且如PersonGroup等,一个id对应一个类实例,因此本单元作业中HashMap的使用最为频繁。

private HashMap<Integer, Person> people;
private HashMap<Integer, Group> groups;
private HashMap<Integer, Message> messages;
private HashSet<Integer> emojiIdList;
private HashMap<Integer, Integer> emojiHeatList;
private HashMap<Integer, Integer> father;

在使用HashMap时,要注意慎用containsvalue算法,不同于containskey基于哈希值的O(1)查找,containsvalue在查找时的复杂度是O(n)。因此在使用时要注意此复杂度会不会导致超时。

PriorityQueue

PriorityQueue是java中的优先队列容器,也就是数据结构中的小顶堆,在最短路径的迪杰斯特拉算法中,可以起到堆优化的作用。因此,在本次作业的sim方法中,要计算最短路径时,采用该容器可以直接实现堆优化的迪杰斯特拉算法,十分方便,只要每次将相应节点加入堆中,从堆顶推出的节点就是能用最短路径到达的点。


使用经验

在容器的选择中,大致的经验为:

  • 如果要存储的节点是键值对,则优先使用Map型容器,一般来说使用HashMap,当需要对容器中的节点进行排序操作时,可以使用基于红黑树实现的TreeMap
  • 如果要存储的节点两两不同,可采用Set即集合类容器,一般使用HashSet,具有较高的查询效率。
  • 在其中的节点有明显的基于加入顺序等动态因素的索引需求时,采用ArrayList,用于根据索引确定节点,此外ArrayList也可用于排序。

4、有关性能问题的分析

第一次作业

本次作业中可能涉及性能的方法主要是isCircle,此方法的功能是判断network中的两个person是否通过关系网连通。一般来说,判断连通时可以采用dfs或是bfs的搜索方法,但由于本次作业中(三次作业都是)只有addRelation没有delRelation,因此可以用并查集算法,在每次添加关系的时候来维护两点之间的关系。利用并查集算法,并采用路径压缩算法,使得查询时的时间复杂度达到O(1),大大节省时间。

对于query_block_sum方法,需要计算的是连通分量的个数,如果不进行维护,采用的最简算法是用并查集所维护的祖先列表,当某个person的祖先是他自己时,就可以用其代表一个联通块。

然而在第一次作业中我们组还是出现了一位同学用了O(n4)算法,采用多次遍历计算联通块个数,所以我用750个ap,250个qbs的数据点进行自动测试时,这个同学跑了十几分钟的时间……

但是,其实可以用一个简单的维护就能让qbs的查询变为O(1),即在network中加入人时将qbs+1,当出现并查集合并时将qbs-1。即可直接用一个变量将其记录。

private int qbs;
public void join(int id1, int id2) {
    int ori1 = find(id1);
    int ori2 = find(id2);
    if (ori1 != ori2) {
        father.remove(ori1);
        father.put(ori1, ori2);
        qbs--;
    }
}
public void addPerson(Person person) throws EqualPersonIdException {
    if (people.containsKey(person.getId())) {
        throw new MyEqualPersonIdException(person.getId());
    } else {
        people.put(person.getId(), person);
        father.put(person.getId(), person.getId());
        qbs++;
    }
}

第二次作业

第二次作业中不涉及复杂的算法,主要是Group中一些变量的维护,包括valuesumvalue的和),agesumage的和),age2sumage的平方和)。

  • valuesum:因为此变量涉及到组内两个人的关系,若要保证不超时,该变量一定要维护,否则会出现双重循环的O(n2)的复杂度导致超时。其中需要注意的是:
    1. valuesum对于两个人之间的关系是要加两次,不能只加一次。
    2. 需要维护的方法不仅有atgdfg,还有ar,在同组的两个人加上关系时,需要将这个组的valuesum进行相应的修改。
  • agesum:此变量用于计算年龄平均数,在atgdfg时进行维护。
  • age2sum:同样在atgdfg中维护,用于计算方差。

在计算方差时,可以采用公式:

\\[Var(x) = (S(x) - 2 * S(x) * E(x) + N * E(x) * E(x)) \\over N \\]

注意,有关精度的计算应当与JML规格中保持一致,因此不能直接用E(X2)E(x)2Var(X)的关系来计算。


第三次作业

第三次作业的CPU时间上限变为6s,因此之前两次作业中O(n2)的算法不再容易被时间复杂度卡掉,因此有关性能的问题主要体现在了sim算法,即最短路径计算上。

在最短路径的计算中,我采用的是最流行的迪杰斯特拉算法,并用Java自带的优先队列容器来对其进行堆优化以降低其时间复杂度从O(n2)O(nlogn),并且在其中进行了局部优化(例如对于以更新过的废弃节点不进入空转而是在判断后直接移除),从而让自己的此算法在与身边同学的比较中也较快。


在三次作业中,因为对每个方法的时间复杂度都进行了分析,并测试了对各种方法较为特化的测试点,所以都顺利通过了对时间性能测试的相应测试点。


5、架构设计

本次作业由于主体方法都是基于课程组给定的JML规格,因此在架构设计层面的难度相较于前两次作业较低,只要按照JML所描述的规格,合理选用算法及维护策略,就能使架构较为清晰。

图模型

有关图的建构,我基本上就是按照JML中已有的容器将图和图中有关节点的信息进行存储,比如说network中的people表,每一个personacquaintance表,利用这些构成一个关系网络图。其中附加的因子有并查集查询中会用到的father表以及GroupMessage等不以节点的形式存在于图中的因子等等。

在本次作业中,并未涉及较多的有关图论的知识,仅有联通块和最短路径两种需要考虑算法的地方。因此用以上的结构对图进行建构是可行的。

维护策略

为便于查询,在编写代码过程中将部分量进行维护是很好的策略。

在本单元作业中,我维护的量在前面的小节已经说明,即第一次作业的qbs及第二次作业的valuesumagesumage2sum。在判断一个变量是否需要维护时,我认为应该遵循以下的原则。

  • 需要维护的变量应该是满足“维护复杂度明显小于不维护查询复杂度”这一条件的,例如qbs,维护和维护后查询的复杂度均为O(1),不维护查询的复杂度至少为O(n)
  • 为使代码尽量简介,一些维护后复杂度无明显变化的变量无需优化,例如在作业中出现的各种size
  • 有些变量在进行许多操作时都会产生巨大的变化,此时进行维护反而是作茧自缚,例如第三次作业中的最短路径,如果想在每次添加关系和添加人时都维护,其复杂度会无比巨大,远远大于在查询时看似笨重的O(nlogn)

6、感想

本次作业需要动脑设计的部分不多,算是让自己的脑子从前两单元的繁忙中歇了歇。但这一个单元作业做下来也让我取得的不少收获。

  1. 首先,JML的学习让我知道了怎么去根据已有规格来编写代码,让我明白学计算机不是一味地学编程,埋头写代码,而是要仔细的思考一个优秀的代码是什么样的。根据JML写代码极大地培养了我的代码习惯,让我学会在每写一个方法前去思考有哪些可能情况,会造成哪些可能影响等等。
  2. 其次,这单元作业让我意识到学习算法的重要性,尽管本次作业涉及的主要算法只有两种,但还是暴露出我在算法掌握方面存在很大的进步空间。对于一个上学期数据结构课从第七节翘课翘到最后一节的人[逃]来说,直接在作业中写并查集和迪杰斯特拉算法确实是十分艰难的过程。查资料,找板子,寻找优化算法花了很长的时间,才算是把算法掌握透。在今后的程序编写中,对于算法的要求会更高,所以对于算法的学习要从平时就开始。
  3. 再者,这次的作业让我们明白简单的事物往往暗藏杀机。这单元作业虽然“杀气”比不过前两单元,但这单元的第二次作业竟然成为了这学期OO课程“死伤最惨重”的一次,有相当一部分同学直接爆零。可见我们不能被看似简单的作业蒙蔽了双眼,对于每一项任务都要认真地去完成。
  4. 最后,这单元让我明白做好测试的重要性。对于这个单元,做好测试是取得好成绩的重中之重,这单元的三次作业堪称“测评机大作战”,有一个好的测试数据生成及便能成功一半。因此要学会写测试,学会对自己的代码进行测试。

以上是关于OO_Unit3总结的主要内容,如果未能解决你的问题,请参考以下文章

OO_Unit3——JML契约式编程

OO_Unit4 UML模型化设计总结

OO_Unit3_JML规格模式

OO_Unit3_Summary

OO_Unit2——电梯模拟

OO_Unit2 关于性能优化与测试的那些事