BUAAOO Unit3博客作业

OO第三单元总结

撰写技术博客:

(1)总结分析自己实现规格所采取的设计策略

(2)结合课程内容,整理基于JML规格来设计测试的方法和策略

(3)总结分析容器选择和使用的经验

(4)针对本单元容易出现的性能问题,总结分析原因如果自己作业没有出 现,分析自己的设计为何可以避免

(5)梳理自己的作业架构设计,特别是图模型构建与维护策略

实现规格采取的设计策略

  • 首先通读接口中规格的描述,根据方法规格分析类中属性应该用哪种容器实现,属性之间的关系能否综合起来(如以键值对的形式),确定类内数据管理结构
  • 实现所有的接口方法,阅读方法规格,将规格种每一个语句转换成自然语言,理解方法的作用并实现
  • 对于类内的方法,综合利用构建中间层次、采用复杂度低的算法的方法来进行方法的优化,降低时间复杂度
  • 在实现的过程中先从基础的类入手(异常类、Person类),逐步向上层实现,根据需要增加类内方法来方便进行数据管理和查询。

在三次作业中我们只需要根据规格的表述来进行实现,但是这并不是一个简单的事情,我们需要理解规格描述的语义,并且尽可能一次性理解清楚,尽量减少修改,否则很容易因为修改过程中思虑不周导致很多方法中遗留bug,说多了都是泪啊😢

基于JML规格的测试方法

Junit是专门用来进行单元测试的框架

在理解方法规格的基础上,根据规格构造测试数据,并利用Junit插件编写测试类进行单元测试

Junit单元测试代码编写小知识:

  1. 几个注解的语义:
    • @Test:测试方法,方法执行结果无论是正常执行和抛异常都与预期一致才通过,并且如果用timeout限制时间的话,只有执行时间在限制之内才能pass
    • @Before:在每一个测试方法之前运行的代码段,所有测试方法的首部公共代码段
    • @After:在每一个测试方法之后运行,同为所有测试方法的尾部公共代码段
    • @BeforeClass:方法为静态方法(static 声明),在所有测试开始之前运行,与before不同,该方法只在所有测试方法运行之前运行一次
    • @AfterClass:方法同为静态方法(static 声明),在所有测试结束之后运行,与after不同
  2. 检验结果的正确性采用assert断言来判断

本单元才开始采用单元测试的方法,最开始还以为Junit插件能根据规格自动生成测试用例来进行测试,一度认为插件安的有问题,咋还得我填。。阿巴阿巴。。偷懒未遂。。

除Junit单元测试之外,就是黑盒测试了。通过构造简单的测试用例,来检查调用过程中是否出现了问题,并通过跑从同学那里搜刮来的数据来进行正确性判断(跪谢)。

容器使用

  • HashMap

    本单元使用的最多的就是HashMap了,Map由于是以键值对的方式进行数据存储,所以方便进行数据的映射和数据查找,并且HashMap对自由增删查改数据很友好,所以像Person类中的values,经常会根据personId来进行数值查找,所以采用HashMap<personId,value>来进行存储会更加方便。而且使用HashMap也能够方便地进行数据遍历,使用.keySet()即可获得键的集合,使用.values()即可获得值的集合。

  • ArrayList

    ArrayList适合利用数组下标来进行访问的数据。在本单元作业中ArrayList的使用很少,我在Person类中采用ArrayList来存储messages,由于我们需要根据插入顺序来对数据排序,所以采用ArrayList能够自然地对数据按照插入顺序排列,而且也可以避免多余内存占用。

    private ArrayList<Message> messages;
    
    public void addMessage(Message message) {
            messages.add(0, message);
    }
    
    public List<Message> getReceivedMessages() {
            ArrayList<Message> temp = new ArrayList<>();
            if (messages.size() <= 4) {
                temp.addAll(messages);
            } else {
                temp.add(0, messages.get(0));
                temp.add(1, messages.get(1));
                temp.add(2, messages.get(2));
                temp.add(3, messages.get(3));
            }
            return temp;
    }
    
  • Queue

    第三次作业使用堆优化的迪杰斯特拉算法的时候用到了PriorityQueue容器,它是基于优先级的优先级队列,根据构造队列时提供的Comparator进行排序。第一次使用这个容器,简单学习一下用法:

    • add() / offer():向队列中插入元素
    • poll():从队列中弹出队首元素,默认最小元素出列

性能问题分析

本单元后两次作业都出现了性能问题。

第一次作业考虑到isCircle()和queryBlockSum()可能会产生性能问题,所以为每一个Person节点增加了一个可达集属性:Reachable的list,这样就使得每一次add relation都会产生O(n)的复杂度,但是在方法isCircle()就只有O(1)的复杂度,queryBlockSum()方法有O(n)的复杂度。其实复杂度还是很高的,只是第一次作业房间里的xdm手下留情,没有hack我。。。

第二次作业由于没有采用并查集而是沿用第一次作业的方式,被卡了好几个样例;除此之外,还有Group中getAgeMean()方法为O(n)的复杂度,牵连getAgeVar()方法也是O(n)的复杂度,共同导致CPU TIME LIMIT ERROR。后将结构改为并查集实现,并为Group设置AgeSum属性,完成了bug修复。

第三次作业我吸取前两次的教训,请教了别人该用什么算法,所以使用了堆优化的迪杰斯特拉算法,复杂度为O((m+n)*logn),没有在这个点上被卡,如果采用最朴素的算法,复杂度可能达到O(n3),采用朴素迪杰斯特拉算法复杂度为O(n2)。但是第二次侥幸逃过的问题在这一次被疯狂攻击,qgvs使用了最简单的算法,有O(n^2)的复杂度,被多次hack。当时好像还注释了这里要改,忘了,害。

第三单元作业感觉上最简单,但是实际上做得最差,因为需要考虑算法上的问题,而这个是之前一直所忽略的,没有意识到算法的重要性,轻视了作业难度。同时这个单元也暴露了我在算法这个方面的问题,真的涉猎太少,并查集和堆优化的迪杰斯特拉都是第一次听说和使用,真的该去看看算法书了。

架构设计

第一次作业

这次作业没有复杂的层次结构,大体直接按照规格说明设计。值得一提的就是并查集结构了,虽然我在第二次作业中bug修复才更正。并查集对于查看节点的连通性有很大的遍历,在对节点添加关系的时候直接构建树结构来构建连通图,在插入节点时只需建立两个节点之间的关系就可以,如果这个树的父子关系确定,则在查询任意两节点是否可达时的复杂度为O(logn),而在查询的时候我们可以顺便进行路径压缩,将这个过程中查询到的节点都与这个树的根节点建立父子关系,这样查询就能够在原复杂度的基础上继续降低复杂度。

第二次作业

第二次作业沿用第一次的图架构,并在此基础上增添了Group类,其中有一些查询操作,所以这些查询操作的方法设计就是至关重要的,ageMean如果每次都从头用公式计算一次的话,则为O(n)复杂度,这自然不如每次增删Person时进行数据计算来得简单,还有ValueSum的计算,直接计算就是O(n^2)的复杂度,但是采用在线计算的方式,就可以降低,在增删人员、人员间关系的方法里会有O(n)的复杂度,但在查询时只有O(1)复杂度,所以在计算指令密集的之后就能够尽量避免超时的问题。Message类较为简单,没有复杂的结构,且对于Network中的Group和Message我都采用HashMap来进行管理。

第三次作业

第三次作业使用了层次化设计,对于Message有多种类别,建立层次化继承关系就是最简单的方法,这在规格中也有明显的提示。这次作业中我收获最大的就是堆优化的迪杰斯特拉算法了,我采用构建一个中间层次来进行最短加权路径的计算。这个图的结构直接利用Person的属性作为一张图来进行计算,在增删人员、人组关系时候的维护复杂度为O(1),在查询的时候需要动态构建一个优先队列。这里记录一下该算法的核心流程:

public int dijkstra(int id1, int id2) {
    HashMap<Integer, Boolean> visit = new HashMap<>();
    PriorityQueue<Node> pq = new PriorityQueue<>(new Node());
    pq.add(new Node(id1, 0));
    while (!pq.isEmpty()) {
        Node t = pq.poll();
        int id = t.getId();
        int valueSum = t.getValueSum();
        if (id == id2) {
            return valueSum;
        }
        if (visit.get(id) == null) {
            visit.put(id, true);
            Person person = getPerson(id);
            HashMap<Integer, Integer> value = ((MyPerson) person).getValues();
            for (int i : value.keySet()) {
                if (visit.get(i) == null) {
                    pq.add(new Node(i, valueSum + value.get(i)));
                }
            }
        }
    }
    return -1;
}

class Node implements Comparator<Node> {
    private int id;
    private int valueSum;
    public Node() {}
    public Node(int node, int cost) {
        this.id = node;
        this.valueSum = cost;
    }
    @Override
    public int compare(Node node1, Node node2) {
        return node1.valueSum - node2.valueSum;
    }
    public int getId() { return id; }
    public int getValueSum() { return valueSum; }
}
posted @ 2021-05-30 11:46  Yeeman_Z  阅读(67)  评论(0)    收藏  举报