oo第三单元总结

Posted buaddd

tags:

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

JML单元总结

本单元的重点在于理解规格描述,并在其基础上采用最优的解法尽可能地提高程序的效率,因此本文将从以下四个方面进行总结。

  • 设计策略(思路分析)—— 问题1

  • 容器选择以及性能问题(具体实现)—— 问题3&4

  • 优化结果 —— 问题5

  • 基于JML规格来设计测试的方法和策略 —— 问题2

设计策略

由于三次作业的框架基本一致,因此选取了第三次作业最终的架构。

本单元作业主要分为两部分:异常类包与社交网络实现的包,其中社交网络实现过程中应该根据JML描述在适当的时候抛出异常。

异常类

下图为异常类的结构,三次作业每次增加一部分新出现的异常,但是总体实现方法一致,即自行构建异常类,实现print方法,在MyNetwork类中抛出。在实现过程中,为了实现记录异常发生次数的功能,每个异常类皆拥有一个静态Counter类成员,记录了该类异常发生的次数,以及每个id引起该异常的次数。

社交网络实现

下图为社交网络的实现,主要有三部分内容:Person类、NetWork类以及Message类。Message类又衍生出NoticeMessage、RedEnvelopeMessage与EmojiMessage。

整体来说,Network中记录了社交网络中的人与消息,Person类记录与之相连的其他Person,而Message类在Person之间发送,发送后将从网络中去除。下面将从三个数据管理类的角度来阐述整个社交网络的管理。

Person-Relation

Person-Relation是整个单元作业的基础,也是后续进行各类查询与计算的基础。在实现第一次作业的过程中,由于需要查询person之间是否连通,以及需要求解整个社交网络中连通块的数目,我维护了一个树状结构的关系图来实现并查集。UnionFind类中包含了存储所有父节点的一个HashMap,以及子图数量childNum。

通过findRoot方法可以确定两个人是否有公共根节点,即是否连通;而询问连通块数量时,返回childNum即可。

Group

Group是包含人物的集合,也是第二次作业的重点,其实现相对简单,只需包括人物即可,同时提供了计算年龄均值,方差以及value均值等的方法。为了提高效率,Group内维护了ageMean、ageVar、valSum 分别记录当前组内的年龄均值、年龄方差、关系value总和。

EmojiGroup

由于第三次作业新增了更加细化种类的message,其中EmojiMessage具有独特的EmojiId,当删除EmojiId时,需要连带删除其对应的所有EmojiMessage,为了提升效率,减少遍历,设计了EmojiGroup类用于管理EmojiMessage数据。

该类动态管理属于同一EmojiId的Message,当新增message时需要判断其类型及是否需要加入EmojiGroup,而当发送消息时,需要将已发送消息从此EmojiGroup中去除。当我们需要调用deleteColdEmoji方法时,就可以获取EmojiGroup,进而删除其中对应的消息。

容器选择与性能问题

容器选择

在三次作业中我主要使用了HashMap和ArrayList两种容器。这两类容器都可以动态扩展或者删除,适用于本单元对于数据管理的要求。

  • HashMap:使用场景为需要存储具有id的数据,且经常需要进行单个数据索引。这种场景下使用HashMap进行查找的速度会比ArrayList快很多。例如在NetWork中,JML使用的是数组存储peron和message,这里我使用的都是HashMap,将id与person或message对应起来,这样可以大幅减少查找时间。

  • ArrayList: 使用场景为当前只需存储数据本身,且使用时必须遍历或者获取特定位置的数据(如getReceivedMessage需要获取栈中前四个消息)。这种场景下,ArrayList维护特定的结构比HashMap更加方便。

HashMap的get与containsKey

HashMap中get与containskey方法实现方式其实是相同的,都是调用getNode方法,因此先用containsKey判断有无此元素,再进行索引是没有必要的,直接使用get方法即可,判断其是否为null。

public V get(Object key)
{
	Node<K,V> e;
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}


public boolean containsKey(Object key) 
{
	return getNode(hash(key), key) != null;
}

HashMap遍历

在某些情况下我们需要对HashMap进行遍历,此时有四种方案:

  • for each entrySet

  • for iterator entrySet

  • for each keySet

  • for entrySet=entrySet()

除了遍历keySet再get之外,另外三种复杂度相近,在本单元作业中,我主要采用的是迭代器遍历的方法。

PriorityQueue

在Dijkstra算法中使用了第三种容器优先队列,其内部基于堆排序,可以自定义compareTo方法来实现想要的排序。

性能问题

本单元作业相对于CPU使用时间来说,内存较为充足,因此大量采用了空间换时间的做法。

第一次作业

第一次作业主要为社交网络基础框架的实现与优化,如上文所示,采用了并查集的方法来管理人物间的关系。

并查集在每次新增人物时加入节点,此时新加入的节点为孤立节点。

public void addNode(int id) {
        father.put(id, id);
        childNum++;
    }

当新增关系后,判断二者的根节点是否一致,若不一致则将二者的根节点相连,在此树状关系中Person间的Relation不是很重要,关注的点主要在于它们是否处于一个连通块中。

public void addRelation(int idx, int idy) {
        int root1 = findRoot(idx);
        int root2 = findRoot(idy);
        if (root1 != root2) {
            father.put(root2, root1);
            childNum--;
        }
    }

而在findRoot方法中,每次寻找当前树的根节点时,都将子节点直接连接到根节点上,以便加快下一次的寻找速度。

public int findRoot(int idLeaf) {
    if (father.get(idLeaf) == idLeaf) {
        return idLeaf;
    }
    else {
        int temp = findRoot(father.get(idLeaf));
        father.put(idLeaf, temp);
        return temp;
    }
}

第二次作业

第二次作业主要在新增了Group类管理person,为了减少Group中计算年龄均值与方差以及value和的次数,设置了update函数,在每次新增人物或者删除人物时设置标记,在下次询问这些值时调用update更新,来维护ageMean, ageVar, valSum(但是最后第三次作业强测data4还是超时了)。

		public void update() {
        int result = 0;
        int size = peopleList.size();
        if (size == 0) {
            ageMean = 0;
            ageVar = 0;
            valSum = 0;
            return;
        }
        for (int i = 0; i < size; i++) {
            result += peopleList.get(i).getAge();
        }
        ageMean = result / size;
        result = 0;
        for (int i = 0; i < size; i++) {
            result += ((peopleList.get(i).getAge() - ageMean) *
                    (peopleList.get(i).getAge() - ageMean));
        }
        ageVar = result / size;
        result = 0;
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (peopleList.get(i).isLinked(peopleList.get(j))) {
                    result += peopleList.get(i).queryValue(peopleList.get(j));
                }
            }
        }
        valSum = result;
    }

第三次作业

第三次作业主要新增了不同种类的message,其中涉及EmojiMessage的管理最为复杂,因此我额外增加了一个EmojiGroup类对相同EmojiId的message进行管理。如上文所述,其功能较为简单,只需要维护相同EmojiId的链表。

在deleteColdEmoji时,可以减少遍历中查找的次数。

public int deleteColdEmoji(int limit) {
        ArrayList<Integer> removedId = new ArrayList<>();
        ArrayList<Message> removedMes;
        //遍历确定要去除的EmojiId
        Iterator iter = idHeat.entrySet().iterator();
        while (iter.hasNext()) {
            HashMap.Entry entry = (HashMap.Entry) iter.next();
            Object key = entry.getKey();
            int temp = Integer.parseInt(key.toString());
            if (idHeat.get(temp) < limit) {
                removedId.add(temp);
            }
        }
        //获取要删除的EmojiId对应着的Message,同时删除Emoji列表中的id
        for (int i = 0; i < removedId.size(); i++) {
            removedMes = idEmoji.remove(removedId.get(i)).getList();
            //从message和heat列表中删除这些message
            for (int j = 0; j < removedMes.size(); j++) {
                idMessage.remove(removedMes.get(j).getId());
            }
            idHeat.remove(removedId.get(i));
        }
        return idEmoji.size();
    }

除了管理EmojiId,本次作业另一个难点在于sim,此方法需要找到message发送者和接收者间的最短value距离,两点最短路径算法中,Dijkstra算法最为常见,但是其时间复杂度为O(n^2),因此采用堆优化的Dijkstra算法,其时间复杂度为O(mlogn)。

(关于这一点其实我看到了一篇文章,在稠密图下堆优化的Dijkstra算法复杂度会变为(n2logn),因为堆优化管理的边在稠密图中数量会接近于点数n的平方,大于集合写法管理点的普通Dijkstra算法(此时仍为O(n2))。但是我没有去测试本次作业的图中边密度的大小,盲猜一手边密度不是很大)

采用优先队列进行优化,优先队列中管理的是路径Path类,包含节点id与到此节点的路径长度。Path重写了compareTo方法,进而可以实现最小堆排序。

class Path implements Comparable<Path> {
    private int value;
    private int id;
    @Override
    public int compareTo(Path o) {
        if (o.getValue() < value) {
            return 1;
        }
        else if (o.value > value) {
            return -1;
        }
        else {
            return 0;
        }
    }

优化结果

HashMap的使用使得查找与删除的复杂度由O(n)降为O(1)

高复杂度方法

isCircle

如上文所示采用了并查集,复杂度从O(n^2)降为O(logn)

queryBlockSum

同样采用并查集,计算连通块的操作均摊到每次修改并查集的操作中,时间复杂度为O(1)

queryGroupAgeMean

每次更新的时间复杂度为O(n)

queryGroupAgeVar

每次更新的时间复杂度为O(n)

queryGroupValueSum

每次更新的时间复杂度为O(mn)

deleteColdEmoji

加入了EmojiGroup管理数据,时间复杂度由O(mn)变为O(ma),其中a ≤ n

sendIndirectMessage

采用了堆优化的Dijkstra算法,时间复杂度为O(mlogn)

测试方法和策略

手动构造样例

本单元作业我主要采用手动构造测试样例,因为基本功能的考察较为直观,因此手动构造测试样例基本可以覆盖大部分功能。例如并查集中的不同关系树合并问题,Dijkstra算法中图模型的构建,我都是手动构造样例去测试。

手动构造的优势在于简单直观,但是问题在于没法完全覆盖所有容易出现问题的地方,且无法进行强度测试,当数据量足够大时程序的性能无法考察到,比如第三次作业strong_data_4就对group的方法进行了高强度测试,导致CPU_TLE。

Junit单元测试

Junit可以进行单元测试,自动生产测试类,注解功能如下:

注解 说明
@Test: 标识一条测试用例。 (A) (expected=XXEception.class) (B) (timeout=xxx)
@Ignore: 忽略的测试用例。
@Before: 每一个测试方法之前运行。
@After : 每一个测试方法之后运行。
@BefreClass 所有测试开始之前运行。
@AfterClass 所有测试结果之后运行。

在测试中可采用各类断言来检查程序的正确性

方法 说明
assertArrayEquals(expecteds, actuals) 查看两个数组是否相等。
assertEquals(expected, actual) 查看两个对象是否相等。类似于字符串比较使用的equals()方法。
assertNotEquals(first, second) 查看两个对象是否不相等。
assertNull(object) 查看对象是否为空。
assertNotNull(object) 查看对象是否不为空。
assertSame(expected, actual) 查看两个对象的引用是否相等。类似于使用“==”比较两个对象。
assertNotSame(unexpected, actual) 查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象。
assertTrue(condition) 查看运行结果是否为true。
assertFalse(condition) 查看运行结果是否为false。
assertThat(actual, matcher) 查看实际值是否满足指定的条件。
fail() 让测试失败。

心得与体会

本单元作业的难度相较于前两个单元较为简单,但是对于细节的考察更为注重,由于我们的作业是基于JML描述完成的,所以理解其要求非常重要。在第二次作业时,因为对ageVar和ageMean的计算方式理解错误,导致强测只过了一个点,而在第三次作业中,也因为sim中对于JML描述没有完全实现的问题挂了一些点,在后续的作业中要引以为戒。

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

oo第三单元总结

OO第三单元总结

OO第三单元总结

OO第三单元总结

OO第三单元总结

OO第三单元总结