OO第三单元作业总结

OO第三单元作业总结

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

在实现JML规格的过程中,我采取的策略是:

  1. 首先宏观阅读指导书,同时简要阅读代码,将各个类的承接关系理清,从比较小的、没有使用其他类的类,如Person,开始编写代码,进而查看比较大的、使用了其他类的类,如Network
  2. 在实现方法的过程中,首先将规格中要求的各种正常情况和异常情况抽离出来,同时检查规格中规定的各种情况是否完备
  3. 按照规格中异常情况的顺序(这一步很重要!),先编写异常情况,保证处理正常情况时数据一定是合乎前置条件的
  4. 按照规格所规定的的各种正常情况进行代码编写,在此过程中进一步考虑容器的选择和性能的优化

总体来说,大部分方法都是比较简单的方法,基本上直接按照规格来编写即可,但是由于CPU时间的限制,在编写较复杂的函数时还需要考虑时间复杂度的限制。这也就是说,在按照规格编写方法时,如果规格中出现O(n2)复杂度的代码,就需要加以分析,想想如何降低时间复杂度,而不能无脑抄规格。也就是说,当写完代码回头来看时,如果出现了O(n2)复杂度的代码块,是需要高度警惕的,这一部分代码很有可能让CPU时间超时。

而对于需要自己改写的方法,不仅要注意其时间复杂度的控制(尤其是要注意JAVA自带容器方法的复杂度),更应该谨慎地编写以保证改写的方法与规格是完全等价的

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

关于JUNIT和openjml的使用

在本单元的对程序的测试中,我并没有使用JUNIT和openjml。一个原因是虽然课程组在大力推荐,但是往年博客上的学长们都对其并不看好,许多博客中都提到JUNIT只能对如2147483647等“平凡”的极端样例进行测试,但是在实际情况中极端样例需要根据不同的代码来设计,JUNIT在这一需求上并不智能,最后极端样例还是只能自己手动构造。另一原因是单元测试在现行条件下基本无用。虽然课程组已经清晰地说明了单元测试在实际生产中的重要性,即测试工程师会专门编写单元测试对别人的代码进行测试,但是在OO的自己编写、自己测试的生产模式下,单元测试就基本失去了其意义。

基于对拍机的测试样例设计策略

虽然没有单元测试,但是对于程序的测试还是必不可少的。由于本单元的性质与前两单元不同,即正确输出结果唯一,使得简单的对拍机成为测试程序的首选。拥有了对拍机之后,唯一需要考虑的就是如何设计测试样例了。

我编写的测试样例以检测正确性为主,从其实质来说,与单元测试有些相似,都是基于JML进行功能的测试,因此样例的强度稍弱。但与单元测试的不同之处在于,整体的黑盒测试更有利于指令和指令之间的交互,因而能够更好地保证实现的正确性。

总体来说,我对于指令的测试仍然是从简单到复杂。也就是说,我会首先对apar等基本指令进行全面地测试,进而再测试各种有关于“写”的指令,例如agatgamsm,最终会对各种查询指令进行检测,如qbsqci等指令。

具体来说,我将指令大致分为几类,每一类都是一条修改指令加上几条查询指令,对其进行功能性测试

# 如下是ap、qps、qv
def ap():
    data = []
    for i in range(2000):
        id = i // 2
        name = "".join(random.sample(STR, random.randint(1, 10)))
        ins1 = "ap " + str(id) + " " + name + " " + str(random.randint(0,200)) + "\n"
        ins2 = "qps\n"
        ins3 = "qv " + str(random.randint(0, i // 2)) + " " + str(random.randint(0, i // 2)) + "\n"
        data.append(ins1)
        data.append(ins2)
        data.append(ins3)
        data.append(ins1)
        data.append(ins2)
    return data
# 如下是ar、qv
def ar():
    data = []
    for i in range(100):
        id = i
        name = "".join(random.sample(STR, random.randint(1, 10)))
        ins = "ap " + str(id) + " " + name + " " + str(random.randint(0,200)) + "\n"
        data.append(ins)
    for i in range(120):
        for j in range(i):
            id1 = i
            id2 = j
            check1 = "qv " + str(id1) + " " + str(id2) + "\n"
            check2 = "qv " + str(id2) + " " + str(id1) + "\n"
            ins = "ar " + str(id1) + " " + str(id2) + " " + str(random.randint(0, i + j)) + "\n"
            data.append(ins)
            data.append(check1)
            data.append(check2)
            ins = "ar " + str(id1) + " " + str(id2) + " " + str(random.randint(0, i + j)) + "\n"
            data.append(ins)
            data.append(check1)
            data.append(check2)
            ins = "ar " + str(id1) + " " + str(id2) + " " + str(random.randint(0, i + j)) + "\n"
            data.append(ins)
            data.append(check1)
            data.append(check2)
            data.append("qps\n")
    return data

容器选择和使用的经验

  • HashMap

在本单元作业中,我使用了许多HashMap来代替规格中要求的数组,原因在于每一个Person都有一个独一无二的id,这很符合HashMap的设计思想,可以通过id进行实例的查找。

@ public instance model non_null Person[] acquaintance;
@ public instance model non_null int[] value;

例如,在上述规格中描述的两个数组,完全可以使用HashMap<Integer, Integer>来进行代替,而HashMapcontainsKey方法查找开销为O(1),与数组的查找开销相同,并没有因为容器的更换而增大时间复杂度。

所有使用的HashMap如下

Person.java
	private HashMap<Integer, Integer> acquaintance; /* HashMap<personId,value> */
Group.java
	private HashMap<Integer, Person> people; /* HashMap<personId,person> */
Network.java
	private HashMap<Integer, Person> people; /* HashMap<personId,person> */
    private HashMap<Integer, Group> groups; /* HashMap<groupId,group> */
    private HashMap<Integer, Message> messages; /* HashMap<messageId,message> */
    private HashMap<Integer, Integer> emojis; /* HashMap<emojiId, emojiHeat> */
    private HashMap<Integer, Integer> tree; /* HashMap<id, parent> */
    /* <结点id, 结点的父节点id> */
    private HashMap<Integer, Integer> size; /* HashMap<id, size> */
    /* <结点id, 以本节点为祖先结点的结点个数> */
  • ArrayList

由于指令smqrm都对于message的顺序有要求,因此使用了ArrayList作为规格中数组的代替,并且使用头插来进行指令的实现。

  • PriorityQueue

这一容器主要用于获得一个小顶堆,从而能够对dijkstra算法进行堆优化。在Java的各种容器中,具有小顶堆性质的不仅仅有PriorityQueue,例如TreeSetTreeMap都是可以实现小顶堆的。我最终选择PriorityQueue的原因在于,首先我在堆优化中只需要弹出堆的第一个元素,因此查找是不必要的,排除TreeMap;其次TreeSet可以对集合中的元素进行排序,是一个有序的集合,但是算法中只需要小顶堆性质,将元素全部排序可能带来额外的时间开销。基于上述两点,我最终选择了PriorityQueue

常见的性能问题和避免方法

  • qciqbs指令

    按照最简单的想法,qci可以使用深度优先遍历算法,这样的时间复杂度为O(n),而qbs如果按照规格直译,其时间复杂度会达到O(n2),因此需要使用并查集算法进行优化,使用两个HashMap进行路径压缩+size优化,指令的复杂度可以达到O(1);当然,在使用并查集时如果不使用按秩压缩或者按大小压缩,有爆栈的危险

  • qgamqgav指令

    直接按照规格计算是可行的,两条指令都可以控制在O(n)复杂度。如果通过缓存ageMeanageSquare可以将复杂度控制到O(1)级别,但是需要注意维护缓存,尤其是在加边时,如果涉及到的两个人在同一个群中,需要对缓存进行更新;除此之外,需要注意的是整除精度的问题,如果轻易对规格中的公式进行变换,可能出现精度不一致而出错的情况

  • sim指令

    按规格直译,指令的复杂度可以达到O(n2),这是不能接受的,由于没有负权边,因此目前最快的算法是堆优化dijkstra算法,可以使用JAVA中现有的容器PriorityQueue模拟小顶堆,这样可以将复杂度降到O(nlogn);需要特别注意的是容器本身的时间复杂度,小心如ArrayList的查找和删除,这些都是O(n)复杂度,如果被放在了循环里,那基本就凉了

架构设计

图模型建构

图的模型建构以Person为结点,Relation的存储采用acquaintance带来的天然的邻接表,value作为边的权重

但是,如果仅仅使用一种图的组织结构不足以应对多样的查询指令,因此可以考虑同时采用多种图的组织结构,并同时维护;在本单元中,我还采用了下述两种结构

  • 对于查询连通块指令,采取并查集算法,破坏图的点与点之间的连接关系,而只保留点与点之间的连通关系

  • 对于查询最短路径长指令,采取堆优化的dijkstra算法,增加一个Vertex类来存储personId和当前距起点距离distance

维护策略

维护策略主要集中在arqgav

  • ar中,需要注意对于各个GroupvalueSum进行维护

  • qgav中,需要在Group加人和删除人时都要进行维护

除此之外,还需要考虑对于多种图的组织结构的维护,如下是并查集的组织和维护

// 并查集组织形式
private HashMap<Integer, Integer> tree; /* HashMap<id, parent> */
private HashMap<Integer, Integer> size; /* HashMap<id, size> */
// 并查集在增加人时的维护,采取size优化
int root1 = getRootParent(tree, id1);
int root2 = getRootParent(tree, id2);
if (size.get(id1) >= size.get(id2)) {
    tree.put(root2, root1);
    size.put(root1, size.get(root1) + size.get(root2));
} else {
    tree.put(root1, root2);
    size.put(root2, size.get(root1) + size.get(root2));
}
// 并查集中查的部分,采取路径压缩算法
private int getRootParent(HashMap<Integer, Integer> tree, int personId) {
    int parent = tree.get(personId);
    if (parent != personId) {
        tree.put(personId, getRootParent(tree, parent));
    }
    return tree.get(personId);
}
posted @ 2021-05-31 19:57  ArSpi  阅读(55)  评论(0编辑  收藏  举报