第三单元总结
写在前面
本单元主要是对JML规格的学习,要求能够简单书写JML,并能够根据JML完成代码的编写。在本单元中,我们通过JML规格构造了一个简单的人际关系网络。相较于前两单元来说,本单元的任务就比较容易了,按照规格所规定的行为去做,只要足够细心,就能够保证程序的正确性。所以在本单元,对于复杂度的考虑与实现性能的优化比实现功能需要花费更多的精力,总的来讲,还使能够让我比较轻松的完成本单元的作业。
实现规格所采取的设计策略
-
在实现规格所对应的功能时,我选择首先根据方法名称及给出的JML来通读理解整个作业想要实现的功能,并将具有大量JML描述的方法用自然语言概括其内容。
-
通读之后,考虑整个作业中需要用到的变量,以及需要用何种容器来实现相应的操作。
-
在写代码时,先补充功能较少的异常处理类,再完成person,network等内容较多的类,对于方法的实现由简单到复杂。
-
在每个方法中,先对异常情况进行异常处理,在排除异常情况后再实现方法功能。
基于JML规格测试的策略
首先在测试工具方面,根据课程组所给的教程,简单学习了Junit单元测试的方法,在第一次作业中也编写了较为简单的的test函数进行测试,由于对Junit的掌握不熟练,感觉有些繁琐,在后续作业中还是选择用python编程方式实现数据构造来进行测试。
对于每次作业的测试,我都大致分为以下几个部分进行:
-
功能测试
由于规格的存在,每个方法有哪几种处理方式都是固定的。只需要根据requires提供相应的输入,然后检查得到的结果是否符合预期,便可以完成基本的功能测试。为了使功能测试具有说服力,应当在样例构造时考虑到各种情形。如对于isCircle的判断可以构造连通与不连通的样例,sendMessage中则可以向同一人发送多条消息,检验消息的输出顺序等,尽量将所有结果的可能场景均进行测试。尤其在最短路径的测试中,应当考虑各种可能存在问题的场景。
-
异常测试
该部分专门针对每次作业中的异常情况进行测试,检验是否符合作业要求的异常输出,尤其是对于错误id的输出是否正确,本部分也可以根据JML的规格中异常行为来进行针对性测试。
-
性能测试
这部分测试主要是保证在上万条数据的情况下能够在2s规定时间内跑完,主要针对一些复杂度较高的方法,如第一次作业中的qci,qbs,第二次作业的qsv,第三次作业的sim等,在未进行优化前,这些方法的复杂度都在甚至更高,为了进行性能的改善,性能测试在几次作业以来一直是最值得关注的问题。
除此之外,还通过与其他同学进行随机测试数据的对拍,对程序进行了更全面的检测。
容器选择和使用的经验
-
在本单元的作业中,大部分情况下选择了HashMap作为数据存储的容器。HashMap在查找方面能够进行很大的性能优化,其按key查找的平均复杂度为,且get方法的复杂度也为,这对于各种遍历操作起到了极好的简化作用。可以用于id和person,message,group的对应关系,以及emoji及其热度的对应关系。
-
对于person的messages的存储时则使用了ArrayList的方式,因为方法中要求对收到的信息进行排序,且输出排序为前四条的消息,ArrayList的存储方式很好的解决了排序问题。
-
在实现最短路径时,使用了优先队列PriorityQueue,优先队列中加入元素的复杂度为,且能够完成距离从小到大的排列,从而优化了整体的性能。
性能问题分析
第一次作业
第一次作业中复杂度较高的函数为isCircle和queryBlockSum,在处理isCircle时,我采用了广度优先搜索,同时在queryBlockSum中我也应用了广度优先遍历,所以二者的复杂度都还勉强合格。在互测过程中,发现有部分同学在queryBlockSum中调用了isCircle函数,直接导致双重遍历+BFS/DFS,在面对上千条数据时存在严重超时现象。
通过向其他同学学习,在互测结束后,第二次作业开始前,我对于自己的架构进行了改善。采用了并查集的形式,利用ArrayList<HashMap<Integer, MyPerson>>来存储多个人形成的联系网,isCircle只需要判断两个人是否在同一HashMap中,而queryBlockSum则仅需要获取ArrayList的size,复杂度大大降低。
第二次作业
第二次作业中复杂度容易出现问题的函数为getValueSum和getAgeVar,如果处理不当,每次调用getValueSum函数均会造成对group中元素的二次遍历,在第二次互测变为5000条的限制下的复杂度一定会超时,而getAgeVar如果在循环中调用了求平均数的方法,也会导致复杂度变为,导致超时。
解决方案是每在新添加一个person到group时,算出增加的valuesum以及年龄总和等,在添加关系时对group的valuesum进行维护,这样在getValueSum时直接返回值即可。
第三次作业
第三次作业中复杂度问题主要在于最短路径的求解上。如果采取一般的Dijkstra算法,那么其复杂度为,虽然限制时间为6秒,但这样复杂度仍然存在风险。采用优先队列进行优化,可以将复杂度降低到。
作业架构设计与维护策略
三次作业基本与JML规格的接口保持一致,严格按照JML规格进行实现。
在第一次作业中,进行优化后加入了一个DisjointSet类,此类主要针对isCircle与queryBlockSum方法,降低其复杂度,每当有新的节点加入时,建立一个新的HashMap,每当加入新的关系时,将两个节点所在HashMap的内容合并。由此在查询两个节点是否有联系时,只需要判断二者是否在同一个HashMap中,在查询有多少个块时,只需要查询HashMap的数量。
第二次作业中,主要对Group类中的属性进行了维护,用valueSum来记录组中的value总和,用ageSum来记录组中的年龄总和。需要注意的是,再添加关系时,也需要重新计算valueSum,这个值可以在getValueSum中直接返回,而ageSum为更方便的计算均值与方差起到作用。
第三次作业中,新建了一个Path类,用以记录初始节点、到达节点与两节点之间的距离,此类的建立是为优先队列及最短路径的计算提供服务,在设计中并未对最短距离进行保存和维护,因为考虑到添加关系后的改动复杂度应该会更高,所以选择每次重新计算最短路径的方式。
除此之外,其他类都按照接口的规范实现对应的方法,不多介绍。

浙公网安备 33010602011771号