BUAA OO 2021 Unit 3 总结
设计策略
本单元由于直接给出了 JML 的规格说明,因此只要理解了每个方法所需要实现的功能、需要满足的前置条件与后置条件、需要抛出的异常方法,那么根据规格去分立地完成方法是较为容易的。
另外,指导书中明确指出,只要代码按照规格实现,即可保证正确性,因此诸如 querySum
等方法用 JML 给出的 \(\mathcal{O}(n^2)\) 做法也能顺利通过,故不做过多优化(当然强测能过不意味着互测不会被 hack,最后还是因为被 hack 在第三次作业优化了)。
测试方法与策略
在三次实验中,我所采取的测试方法为整体黑盒测试,数据为随机 + 构造生成。虽然本单元中可以用 JUnit 测试,但是最终我并没有采用这样的方案,原因如下:
- 虽然能进行单元测试,但是单个方法对应的测试方法同样需要编写大量代码,且方法总数较多;
- 并非所有的方法都是严格解耦的:由于 JML 提供的仅仅是一个规格,故实现多种多样,实现过程可能是耦合的;
- 某些方法在调用之前可能需要做大量的准备条件,才能进行一次测试(如查询最短路)。
综上,我并没有选择 JUnit 进行测试。不过由于项目是迭代开发的,总是能够用上一次的测试来对新的扩展进行回归测试,因此也获得了较好的效果。
容器选择与使用经验
在本次实验中,我进行了多种容器的选择,其优势与使用场景如下:
HashMap
:高效键值对查询。由于经常需要将id
映射到具体的对象身上,因此<Integer, Object>
的映射关系存储容器是十分有用的;LinkedList
:高效头部元素插入。由于需要按照新到旧的顺序对每个人收到的消息进行插入与查询,因此如果采用ArrayList
,则插入的效率将是很糟糕的。在这里使用链表实现的LinkedList
能提高效率;HashSet
:同HashMap
,不过在不需要查询值时,HashSet
将比HashMap
更方便;PriorityQueue
:高效最小值查询。为了优化 Dijkstra,需要一个堆的数据结构进行动态维护,这里PriorityQueue
就是不错的选择。支持插入一个元素、取出最小值等(可以实现Comparable
接口并重载compareTo()
以自定义比较)。
性能分析
在三次作业中指令数 \(n\) 均不超过 \(10^4\),故每种操作复杂度只要是 \(O(n)\) 级别或以下的,基本都能保证时间复杂度。这里列举一些超过 \(O(n)\) 的方法及其改进方法:
queryCircle
与queryBlockSum
:采用并查集维护连通块,插入和查询时间复杂度降至 \(\mathcal{O}(\alpha(n))\);querySum
:不采用原有 \(\mathcal{O}(n^2)\) 的做法,而是在每次向群组添加人与移除人时,动态维护该群组的边权之和,进而使查询操作降为 \(\mathcal{O}(1)\);sendIndirectMessage
:采用堆优化的 Dijkstra 寻找最短路,时间复杂度 \(\mathcal{O}(n \log n)\)。
经过上述优化,基本已经能保证在公测与互测的范围内能够在给定时空限制内完成运行。
图模型构建与维护策略
本次作业中,社交网络关系图实际上是这样的图论模型:
- 会动态增删点
- 会动态在两点间增加带权无向边
- 不会有重边和自环
需要做的事情是:
- 查询两点是否连通
- 查询连通块数量
- 查询特定点集的边权和
- 查询两点最短路
实际上,由于并不存在删除边的操作,因此通过并查集与最短路算法完全足以解决该图论问题。如果涉及到动态删边,那么则需要更复杂的算法来维护连通性等。
心得体会
本单元以 JML 规格化描述为核心,旨在让同学们了解规格化描述的方法,以及掌握根据规格进行方法编写的能力,最后按照规格的要求进行单元测试。
JML 规格有其优势之处:可以形式化地、严谨地描述方法,可以将架构和代码进行分离。同时,其劣势也是很明显的:代码不一定完全解耦,并且编写规格可能比方法本身还要冗长。因此设计 JML 规格虽然是一种分离设计和代码编写的手段,但是却难以应用在实际工程中。