您现在的位置是:首页 >技术交流 >OO第三单元——无向图总结网站首页技术交流
OO第三单元——无向图总结
OO第三单元——无向图总结
本单元以无向图算法、JML规格和测试为重点,依据给定的接口,实现具体的类。需要按照给定的规格实现具体的功能。其中无向图部分是本单元难点,实现起来不难,但是想要性能更优,运算时间小于15秒实在是需要很多硬核的算法知识!
测试过程
黑盒测试与白盒测试
黑盒测试将程序视为一个黑盒,不关注程序内部具体怎么实现,只关注程序的功能是否满足要求;即给定输入,只关注输出是否与标准的输出一致;
白盒测试关注程序内部具体实现。通过分析程序内部各个节点,检验程序的状态,构造不同的数据,检验每个通路是否能够满足功能,从而完成对程序的检验。
对测试的理解
单元测试
单元测试:完成最小的软件设计单元(模块)的验证工作,目标是确保模块被正确的编码,使用过程设计描述作为指南,对重要的控制路径进行测试以发现模块内的错误。通常情况下是白盒的,对代码风格和规则、程序设计和结构、业务逻辑等进行静态测试,及早的发现和解决不易显现的错误。
功能测试
某个功能或特性完成后,测试人员对这个功能或者特性进行的单独的测试,在这个阶段,一般功能不会相互影响,测试的关注点也比较单一。
集成测试
集成测试:通过测试发现与模块接口有关的问题。目标是把通过了单元测试的模块拿来,构造一个在设计中所描述的程序结构,应当避免一次性的集成(除非软件规模很小),而采用增量集成。
自顶向下集成:模块集成的顺序是首先集成主模块,然后按照控制层次结构向下进行集成,隶属于主模块的模块按照深度优先或广度优先的方式集成到整个结构中去。
自底向上集成:从原子模块开始来进行构造和测试,因为模块是自底向上集成的,集成时要求所有隶属于某个顶层的模块总是存在的,也不再有使用稳定测试桩的必要。
压力测试
软件压力测试是一种基本的质量保证行为,它是每个重要软件测试工作的一部分。软件压力测试的基本思路很简单:不是在常规条件下运行手动或自动测试,而是在计算机数量较少或系统资源匮乏的条件下运行测试。通常要进行软件压力测试的资源包括内部内存、CPU 可用性、磁盘空间和网络带宽。
回归测试
回归测试:回归测试是指在发生修改之后重新测试先前的测试用例以保证修改的正确性。理论上,软件产生新版本,都需要进行回归测试,验证以前发现和修复的错误是否在新软件版本上再次出现。回归测试的目的在于验证以前出现过但已经修复好的缺陷不再重新出现。一般指对某已知修正的缺陷再次围绕它原来出现时的步骤重新测试。
测试工具
测试使用了他人分享的评测机。
数据构造策略
第一个策略是单独检验一个指令的正确性。构造不同情况的图,反复调用一个指令,测试不同环境下指令是否正确,以及程序是否具备良好的性能,不会运算太长时间,内存不会占用过大。
第二个策略是检验复杂指令环境下,程序是否运行正确。具体来说,使指令尽可能地全面、复杂,从而验证每一条指令是否都实现了正确的功能,会不会出现崩溃的情况,异常抛出是否正确。
架构设计
无向图的节点是Person类对象。使用一个ArrayList<Person> people数组存储无向图的所有节点。
在Person类中,有两个ArrayList数组,存储该成员的所有邻接节点以及边的权值。
同时,为了实现Dijkstra算法,需要具有一个int[][]类型的邻接矩阵matrix,记录当前图的边的权值。
动态维护时:
在添加节点的时候,新建Person实例,放入people中;同时扩大邻接矩阵数组范围,并将新节点与其他节点的权值设置为无穷大。
在对v1,v2加边的时候,在v1,v2的两个Person实例中添加对应的acquaintance和value;除此之外,将matrix[v1][v2]和matrix[v2][v1]的值都设置为value。
在修改v1,v2边的权值的时候,若是删除边,则将v1,v2对应的Person实例同时删除对应的acquaintance和value;除此之外,将matrix[v1][v2]和matrix[v2][v1]的值都设置为无穷大。
以上为无向图的实现以及动态维护的方法。
性能问题
第一次作业的性能问题是查找无向图的连通子集个数、两点间连通性判断以及三角形个数。
最开始这些问题我采用了并查集的方法。每个Person增加Ancestor属性,用于标记该节点的祖先;每当添加新结点的时候,就将新Person的祖先设为自己;当添加新的边时,若A,B二者具有的祖先不同,则将B的祖先的祖先设为A的祖先。
有了这个数据结构,三个问题就有了对应的转化。
连通子集个数在添加节点时加一;在添加边时,若两个节点具有共同的祖先,则说明本来就是连通的,连通子集个数不变;若没有则将连通子集个数减一;
两点间连通性,通过两个节点是否具有共同的祖先来判断,若祖先一致,则连通;
三角形个数则是在加边的时候动态维护了。每增加一条边AB,则确定这条新边所增加的新三角形。选择节点A,然后遍历A节点的所有邻接节点C,判断B是否也和各个C相连接。若连接,则三角形个数加一。
通过动态维护,原本复杂的三层嵌套循环就被完美地优化了,复杂度较低。
而第二次作业中,出现了删边的操作,并查集并不好用了。我的替代方案是递归求解。
连通子集个数:初始化各节点都是非visited的状态。遍历各个节点,若是非visited的状态,则连通子集个数加一,将其设为visited,并递归地遍历所有与之相连的节点,将他们也设为visited;若为visited则不进行递归。在每次递归操作时,与首个节点相连接的所有节点都被访问,而这些连接的节点组成一个连通子集,从而可以正确地得到连通子集的个数。
AB间连通性判断:初始化各节点都是非visited的状态。递归遍历A的全部邻接节点,并将遍历到的点设为visited。若当前遍历到的点为B点,则说明AB间连通。为了防止重复查找通路,若下一个要遍历到的点为visited,则不再向下递归。
三角形个数:初始化各节点都是非visited的状态。遍历各个节点,并将该节点设为visited。向下递归邻接节点,递归三层,若起始结点和终止节点相同,如1→2→3→1,则三角形个数加一。最终结果需除以2,因为会出现1→2→3→1和1→3→2→1的重复情况。
除此之外,第二次作业也增加了查询“特别关心”的功能,即两节点的边权值大于等于这两个点的所有边的权值。我的方法是在每加入一条边、修改一条边的权值以及删除一条边时,就更新相关节点的特别关心。这样动态维护就防止了查询时三层循环的超时问题。
第三次作业增加了无向图最小环的功能。我的思路是,建立一张邻接矩阵表matrix,有边的元素记录边的权值,没有边的记为无穷大。当要查询包含A的最小环时,遍历所有它的邻接节点B,将matrix[A][B]和matrix[B][A]设为无穷大,再用Dijkstra算法查询是否二者之间仍然有通路,并返回通路的总值pathValue。若pathValue+value为当前最小,则包含A的最小环的总值为pathValue+value。最终实现起来性能也比较好。
规格和实现分离是特别便捷高效的方法。规格往往只能讲清楚最基本的功能,让设计者明白功能的意义;但是规格不能提供更好的算法来解决问题。这就使得我们需要先明白规格到底讲述了一种什么功能,再用自己的高效的方法加以优化。比如,最基础的一维数组,可以用ArrayList、Hashmap等容器来实现,效率更高;而本次作业中的图的计算,我们也可以用现成的图算法来代替规格中冗长的循环来解决问题。
OK测试方法
OK测试方法能够严格地检验规格,判断函数在第几句规格出现失误。
OK测试方法应该严格按照规格要求的语法来写,不能用自己的算法。因为自己的算法与规格实现存在不同,不能说自身算法判断无误,规格就实现正确了;而是用最基本、最朴实的程序,与规格严格对应,检验程序正确性。
对于检验代码实现和规格一致性,可以对规格逐句地进行检验;若某句规格不符合,则返回该句的序号。这样就可以剖析程序,从内部严格地检验功能实现的正确性,这种检验方法应该是十分有效的。
学习体会
通过本单元实战训练,我学习了规格的作用:规格通过代码语言严谨详细地说明每个类具备的属性、每个方法具备的功能。程序员通过规格的语义理解其功能要求,然后用自己的算法去实现功能。这就是规格在设计程序上的用处。
相比于规格,我认为无向图的算法是本单元的重中之重。首先,本单元实验考察了对于数据结构的熟练掌握程度。比如并查集、优先队列的概念和使用;其次,一些算法也从理论搬上了实际使用,图的深度遍历、Dijkstra求最短路径,也让我们复习了一下数据结构课上的一些知识。
而在规格和算法的转换间,我也明白我们不能完全按照规格直接写代码。我认为本单元作业非常简单,只需要照葫芦画瓢地按规格去一模一样地写代码,也能完成任务。可是这样格局就小了,完全复制规格的做法既在写代码的时候费时费力,又没有高效的时空性能。先理解意思,再动手操作。用巧妙的数据结构、算法去实现最朴素的规格,是这一单元的目的,也是我需要持久地去学习的过程。
图只是抛砖引玉。在我自己编写程序时,往往陷入到一个个困境中,也就是如何用高效而恰当的算法去代替那些循环嵌套、递归,去优化那些尾大不掉的繁琐的程序,去实现那些OJ网站上看似不可能完成的题目。只有规格与算法对接,才是解决这一困境的良方。
对于“规格”,将自然语言转化为我们自己能够理解的“规格”。能不能够通过将题目分为若干个“ ormal_exception”来避免缺少情况的错误?能不能用“ equires”来明确前置条件?能不能在做题前,把题目要求分成具体的“ensures"来明确任务目标?我觉得这也都是可以尝试的部分,毕竟自然语言具有歧义,而翻译为”规格“的过程至少可以保证严谨性,不会出现”啊,我没看着还有这要求“”艹我少写前置条件了“的尴尬情况了。
对于算法,本单元作业我算是“吃百家饭”才拿到了很好的成绩。哎,讨论区里有大佬分享并查集的方法,能让程序复杂度大幅降低,爱了爱了,在自己程序里用一下;又有大佬写出了对拍机,数据构造地也很好哇,既有单条指令的压力测试又有复杂指令的测试,直接拿来用用……以及在与很多同学交流的时候,我觉得自己实现地过于潦草和低能了,于是借鉴了同学的优异算法。
在沾沾自喜之余,我其实发现,自己对于数据结构和算法的掌握程度其实很差。第一个问题,老实说,体现在“不知道”这个算法,完全没有过了解,像是电梯第三次作业中的Floyd算法,简洁、高效而优雅;这次同学提出的“并查集”“优先队列”也让我眼前一亮,于是对其有了初步的认识和学习。我认为算法在于积累,看得多了、用得熟练了,遇到类似的问题也就会突然想到合适的算法。这是我和前面所说的“大佬”的差别:积累的不够!第二,只会用“死的”算法,不会改装算法来适应自己的程序。比如求最小环,我的首要思路永远是递归,直到起始点和当前递归点相同就退出。而同学想到的删边加Dijkstra就非常巧妙。Dijkstra人尽皆知,求最短路径的方法嘛,可是就是不会加一条边,变成求最小环的算法。这就好比去食堂点了一碗粥,而你只会拿双筷子,却不知道拿勺一样不知变通。我觉得这也是做题太少的缘故,熟能生巧才能才思泉涌啊。
遇见了程序AC,
就像跋山涉水遇见了月亮。
以后天黑心伤,
就问那天借一缕月光。