面向对象第三单元总结
Posted DDoSolitary
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象第三单元总结相关的知识,希望对你有一定的参考价值。
本文为面向对象课程第三单元“社交网络”作业的总结。
设计策略分析
我在本单元作业中采用的设计策略基本上分为三个步骤。
-
首先通读整个程序的 JML 规格,在总体上把握程序的架构,了解各个类的大致功能和互相的调用关系。
例如在本单元作业中,Person
类表示一个人,提供与这个人的各种属性以及与他人的关系;Message
类及其子类表示一条消息,提供消息相关的信息;Group
类表示一组人,里面包含Person
类的对象作为成员,提供增删成员和查询统计信息的接口;Network
类表示整个社交网络,其中包含Person
、Group
、Message
类,提供对它们进行各种操作的接口。 -
然后根据各个类的调用关系,按自底向上的顺序实现各个类。
因为有些类的规格中提供的方法有时不足以实现依赖它的类,所以如果先实现被依赖的类,在之后按需要添加方法时就可以参照已经完成的部分了。如本单元中我是按照Message
,Group
,Person
,Network
的顺序来实现各个类的。 -
实现每个类时首先仔细阅读每个方法的 JML 规格,综合考虑各个方法的功能,确定组织类内数据使用的数据结构和实现各个方法所需的算法,然后再同样按照自底向上的顺序实现类内的各个方法。
测试方法和策略
本单元作业除了像前几单元需要测试程序的正确性以外,由于涉及了一些需要算法优化的图上的操作,还需要进行性能测试。
正确性测试
对于正确性测试,本单元我沿用了前两单元使用的模糊测试的方法,通过编写 Python 脚本按照输入要求生成大量随机数据,自动调用程序检查输出的正确性。在确定生成的输入数据对应的正确输出时,则是自己编写 Python 代码进行外围的细节处理,然后调用了一个名为 NetworkX 的图论库实现图结构的维护和图上的各种运算操作。
不过这个方法在本单元的效果并不是很理想。一方面由于时间关系我把连通性判断完全交给了 NetworkX 提供的 has_path
函数,但它内部只是简单地调用了计算最短路的 shortest_path
方法,性能上不是很理想,导致在后两次作业最大请求数增加到 5000 后速度完全无法接受,只能继续生成 1000 个请求的数据。另一方面我实在编写完作业本身后再开始编写测试程序的,因此如果在阅读 JML 时理解出现了偏差,编写测试程序时就会因为先入为主的印象导致无法测试出程序的错误。在第二次作业中我就因为这个原因没有测试出 sendMessage
方法的 bug ,导致在强测中失分。
性能测试
对于性能测试,我则是针对几种容易出现性能问题的请求,专门编写脚本生成运算量尽可能大的输入数据,再使用 shell 提供的 time
命令运行程序,确保其运行时间没有超出限制。
容器的选择和使用
本单元中需要使用容器存储的主要是 Person
、Message
、Group
这几个类,而它们都使用 int
类型的标识符(ID)来唯一确定,因此在存储这些类时主要使用了 HashMap
和 HashSet
这两个基于哈希表容器,这样就可以实现时间复杂度为 \\(O(1)\\) 的增删查改操作。
此外对于一些有特殊需求的场合也是用了一些其他的容器,例如在使用 Dijkstra 算法计算最短路时使用了 PriorityQueue
类作为优先队列,在 Person
类内存储接收到的消息时则是由于需要按顺序在头部插入而选用了 LinkedList
类,在处理 queryNameRank
请求则是使用了 TreeMap
类维护有序的姓名数据。
性能问题分析
本单元易出现性能问题的地方主要集中在图论算法上。
首先是查询两点间连通性的 isCircle
方法以及计算连通块数目的 queryBlockSum
方法,如果使用简单的 DFS 或 BFS 算法复杂度为 \\(O(n)\\),很容易超过时间限制,因此我使用了可以对集合进行近似 \\(O(1)\\) 的合并和查询的并查集算法。不过遗憾的是由于我偷懒仅实现了路径压缩而没有实现按秩合并,在第三次作业中的有一组强测数据中因为输入的图是一条链而导致并查集维护的树也退化为了链,最终在递归查询时栈溢出了。
然后在第三次作业中加入的 sendIndirectMessage
方法需要计算计算两点间的最短路,如果使用简单的搜索算法进行枚举或是复杂度高达 \\(O(n^3)\\) 的 Floyd 算法的话就会出现性能问题,这里我使用了标准的 Dijkstra 算法,顺利地通过了测试。
除了上述几个会造成超时的地方以外,还有一些方法的实现尽管使用单纯的暴力算法不会超时,但仍有优化空间。例如 Group
类中计算统计信息的 getAgeSum
和 getValueSum
可以通过在添加点或边时顺便维护相应的数据使查询的的时间复杂度降到 \\(O(1)\\)。而对于 queryNameRank
请求,如果在 addPerson
时使用 TreeMap
类来维护有序的姓名序列,尽管会导致后者的时间复杂度从 \\(O(1)\\) 增加到 \\(O(\\log n)\\),但在处理前者时就可以直接按顺序遍历,时间复杂度从 \\(O(n\\log n)\\) 优化到 \\(O(n)\\),对于 queryNameRank
请求较多的输入数据会有更好的性能表现。
架构设计
本次作业的总体架构设计十分简单,课程已经提供了各个类的接口定义,只需按照要求实现即可,因此这里主要简单说明一下各个算法和数据结构的组织方式。
Person
类中以邻接表(实际上是哈希表)的形式存储了社交网络的图结构本身,它也负责维护这个人相关的各种属性。
Group
类中则是用哈希表存储了其所有的成员,并维护了一些统计信息以避免查询时再计算。
Message
类基本上只负责承载消息本身的数据并提供属性接口。
Network
类负责管理管理整个社交网络,其中使用哈希表分别存储了网络中的所有 Person
、Group
和 Message
对象,并提供各种修改和查询的接口。处理复杂查询所需的算法也都在 Network
类中实现,例如其中维护了表示节点连通性关系的并查集以支持 isCircle
等连通性相关的查询,还用哈希表维护了 emojiId
对应的热度信息以支持 deleteColdEmoji
请求,计算最短路的 Dijkstra 算法则是直接实现在唯一用到它的 sendIndirectMessage
方法中。
以上是关于面向对象第三单元总结的主要内容,如果未能解决你的问题,请参考以下文章