OO-unit3
本单元是基于JML规格来实现对社交关系的模拟和查询,整体架构与设计都是官方给出。在实现过程中,我深感JML是一个非常适合开发的形式化规格语言。尤其对于团队项目的开发,用JML可以清晰规定每一部分的代码要求,程序员在实现时仅需思考在此基础上如何提升性能,而不用为架构设计发愁,体验极佳。
设计策略
由于JML对于设计要求给的已经非常详细了,因此大部分代码仅需将JML语言直接转化为Java。只有小部分的数据结构选择和代码复杂度优化需要自己设计。这里将展示一下需要自行思考的类设计。
Network
isCircle
函数要求判断id1与id2对应的Person是否成环。如果用递归或栈来实现,时间复杂度较高。因此选择用并查集实现, 每次仅需判断一下二者是否有相同的头结点即可判断是否成环。然而这个方法要冒着后续作业增加删除的风险,不过所幸没增加。sendIndirectMessage
则要求求最短路径,首选迪杰斯特拉算法。但是用朴素版的时间复杂度会达到O(n),这里可以使用java自带的数据结构PriorityQueue
来存储节点,时间复杂度减少至O(logn)。queryBlockSum
要求连通分量。并查集的数据结构使得我们之前一直在维护一个连通分量作为属性,此时直接返回即可。
Group
getValueSum
的实现如果按照JML的思路,是个O(n^2)复杂度的二重循环。如果10000条指令频繁操作这个方法,CPU时间恐怕难以保证。因此动态维护一个变量valueSum,当增加关系或将人增加进组时改变其值,最后直接返回。
基于JML规格来设计测试的方法和策略
课程组给出了针对JML规格语言的测试方法,如JUnit。它可以根据JML规格自动建立好测试类,只需要自己向里面填写代码进行测试即可。
然而这种测试方法我三次作业都没用,一是因为它只能验证正确性,但本单元作业对于性能的要求更高。相比于用JUnit,自己构造有针对性的数据来测试更加有意义。
至于评测机,由于本次我认识的大佬们人手一个非常完善的评测机,又人手一个强大的测试数据库,因此在和他们的对拍中走完了三次作业....
对于数据的构造方法,可以用Python针对每个指令建立一个类,来随机生成指令(因为有exception,因此对于数据生成的限制其实没什么)。如果想针对某个点来hack,就需要人为+自动构造数据了。比如之前朱绍铭同学发现我在第二次作业中用了cache机制解决getValueSum
的O(n2)问题,特意构造了一个一条加关系一条查询valueSum的方法,成功hack住。因此后来才被迫选择了动态维护。
容器选择&使用经验
数据结构选择
由于JML在规定类属性时只说明是数组:
/*@ public instance model non_null Person[] people;
@ public instance model non_null Group[] groups;
@ public instance model non_null Message[] messages;
@ public instance model non_null int[] emojiIdList;
@ public instance model non_null int[] emojiHeatList;
@*/
因此选择怎样的容器是很值得思考的问题。根据整体架构设计,我发现本单元作业对于数据结构的查询操作特别多,而HashMap对查询的复杂度是O(1),远好于更接近数组的ArrayList。因此我在能使用HashMap的情况下就不选择ArrayList,这样解决了指令太多查询太慢的问题。
Network
private HashMap<BigInteger, BigInteger> roots;// roots, 用于实现并查集,其中头结点的值为int之外
private HashMap<Integer, Person> people;
private HashMap<Integer, Group> groups;
private HashMap<Integer, Message> messages;
private HashMap<Integer, Integer> emojiList;//用于同时存emojiId和热值
Person
private HashMap<Integer, Person> acquaintance;//用于存储这个该人的亲戚,id->Person
private HashMap<Integer, Integer> value;
然而不能无脑用HashMap,对于遍历操作比较多的结构,我们还是应该用ArrayList实现,抑或是维护两个容器,一个用HashMap存,另一个用ArrayList。例如Group里的people
private HashMap<Integer, Person> people;
private ArrayList<Person> peopleList;
而Person里的messages,由于有顺序要求,且对其查询操作并不多,因此也用ArrayList实现比较方便。
自己的设计为何可以避免性能问题
本单元最反智也是唯一的bug就是没想到各种id可以是负的(主要是没从电梯单元里反应出来,电梯的id是在0-MaxInt),导致我在用并查集时把-1当成头结点,并每次判断是否<0.... 这样只要有负数的personId必错。且,前两次作业无论是强测、互测还是评测机都没有测出来,这么严重的问题第三次才爆出来,当场去世。
但是如果不算上述的失误,对于性能问题应该是没被强测和互测hack住。本质原因是因为我周围有一群热爱优化的大佬同学,他们每天孜孜不倦地寻求最快、更快的方法。于是在学习中,每一次作业我都在精进代码。
如同第一部分阐述那样,对于时间复杂度超过O(n^2)的操作,我都尽可能地降低复杂度到O(logn);对于查询操作较多的容器,我都尽量用HashMap存储将O(n)降低到O(1)。这样下来的复杂度不会高,性能不会太差。
架构设计&图模型构建与维护策略
怎么有种每个问题都差不多的感觉
就像上述分析所言,对于大部分的存储结构,我都采用了
private HashMap<Integer, Person> people;
private HashMap<Integer, Person> acquaintance;
private HashMap<Integer, Integer> value;
针对社交网络的图模型维护,people存放id-person为点集;acquaintance以邻接表的方式存放id-person为边集;value以邻接表的方式存放id-value的权值。如此构造了一个图模型。
维护的核心在于前述的并查集,当增加一个关系时,如果这两个人曾经不成环,则可以合并两人头结点一个,代表可达。并查集可以实现查询&插入接近O(1)复杂度的速度,且维护十分方便。