面向对象程序设计-第三单元博客
作者:互联网
第三单元博客
第三单元是对JML规格的初步接触和练习。其初衷是为了以严格的语言来规范代码的书写,避免二义性,传达清晰的信息。总的来说,本单元作业难度不算特别大,但需要对JML的描述进行转化避免性能过差。因此在JML的规格约束下设计合适的架构,进行合理的选择是整个作业的关键,也是使用形式化语言的关键。
从JML得到代码的设计策略
首先要说的是,我是如何一般性地从JML得到一份代码,效率高而又不出现问题。首先遵循通读原则和从简到繁的原则,拿到JML应该先通读一下属性规格,方法名,从而大致了解一下其功能。然后再读相对简单的规格并进行代码补全,例如get
,set
方法等。最后再综合考虑先前理解和复杂规格将复杂的方法解决。这样既能让人大致了解要完成什么任务,又能将困难的任务集中在理解建立之后以进行正确的编写和充分的优化。
结合课程内容,整理基于JML规格来设计测试的方法和策略
基于JML规格来进行测试,一般推荐Junit,其可以自行设计测试点来对代码进行测试。通过学习,我发现Junit的测试很想我们的操作系统课程的测试方式,即生成一个测试函数写满断言来对各个部分的正确行为进行评估,而操作系统实验的基本任务是补全代码,很类似于我们这里根据JML对代码进行填充。因此模仿我们操作系统实验的测试方法就成了我设计测试策略的重要手段。
大概来说,基于JML规格用Junit来对代码正确性进行测试,可以通过根据JML的几个\requires
语句提供的前置条件,利用\ensures
语句给出的后置条件来对类的行为进行基本测试,具体而言就是利用assertEquals
来对行为设定预期结果进行判定,可以生成类实例,调用方法,塞入合适的参数来进行测试。
而对于异常类的测试,由于涉及到标准输出,因此可以将输出流重定位,例如重定位至ByteArrayOutputStream
,将其缓存在这个字节缓存中,然后使用try-fail-catch
语句捕获异常并进行print
来assertEuqals
进行比对。
容器的选择经验
在设计中,很重要的一点便是如何选择容器。对于规格中的数组,我基本选择的是HashMap
,但并不以累加的下标作key,而是以各自的唯一的id等作key,充分利用hash查找的O(1)
的复杂度进行优化。实例有MyNetwork
中的people
数组/Hashmap
和value
数组。people
数组即以Person
的id做索引。value
数组/Hashmap
则为了简化调用,将Person
作为key而未将其id作为key。但本质上都是利用hash的好处。
而对实在没有能逻辑上构成对的数据,但数据之间不会equals
时,则选择HashSet
进行存储,这样仍然便于检索查找;而对可重复数据则在ArrayList
和LinkList
中根据查找,插入和删除的使用频率进行选择,如果查找较多或加入删除集中于尾部则使用ArrayList
,如果插入删除较多则使用LinkList
,例如MyPerson
中的messages
数组,就是因为收到的消息可能相同,但是仅仅起到记录作用而采用了ArrayList
。
而对总是需要排序的数据结构,就需要使用TreeMap
或者PriorityQueue
(堆实现),或者手动实现Comparable
接口进行Collection.sort
排序(但是较慢,不如红黑树实现的TreeMap
)。例如第三次作业中的Dijkstra
算法中寻找当前最短路径使用TreeMap
和PriorityQueue
均可以,我采用的是PriorityQueue
,因为堆可以较快的维护得到最小值或最大值而又不需要保证完全有序。
针对本单元容易出现的性能问题,总结分析原因如果自己作业没有出现,分析自己的设计为何可以避免
-
第一次作业
第一次作业容易出现性能问题的方法是queryNameRank
,isCircle
,queryBlockSum
。其中queryNameRank
相对不容易出问题,因为最慢也是完整扫描一遍people
数组,达到O(n)
的复杂度(我即是如此做的)。isCircle
关键在于设计是否合理,最朴素的做法就是dfs或bfs算法进行搜索,acquantance
数组相当于邻接表,这样每次遍历复杂度为O(n)
,如果查询过多,性能会较差,也不便于queryBlockSum
和后续作业顺带对于人与人之间是否有关系的判断。因此从这里开始就可以考虑使用一种新的数据结构进行优化——并查集。因为考虑到并查集主要用来判断元素是否共集合,其满足且恰好满足无向图的连通记录,复杂度低,且寻找父节点时压缩结构可以使插入复杂度为O(1)
。我在此处采用的是以HashMap
tree
保存上级结构,用循环方式查找父节点并压缩,没有递归的复杂调用但是树结构优化不彻底,但是足够使用。而在queryBlockSum
中只需要对tree
进行一次遍历,记录根的个数即可得到连通分量数,复杂度为O(n)
。当然这还可以进一步优化,彻底贯彻修改即更新的原则,每次加入新节点或关系就对连通分量数进行更新,可以使得queryBlockSum
复杂度为O(1)
,但是操作相对复杂分散。
-
第二次作业
第二次作业的关键函数为MyNetwork.queryGroupValueSum <-> MyGroup.getValueSum
,MyNetwork.queryGroupAgeMean <-> MyGroup.getAgeMean
和MyNetwork.queryGroupAgeVar <-> MyGroup.getAgeVar
。这三组函数都涉及到如何快速求和。一般来说,朴素的办法就是查询才计算,每次都要遍历O(n)
,特别是value
还需要判断是否isLinked
,容易变成两重循环O(n^2)
,查询指令一多就容易超时。而较好的办法同样是每次插入与删除便更新,查询只是查变量,那么就需要提防在那些地方存在对Group内属性的修改。对于value
,存在两种情况,一是两个人已经在某些组中而新建立了关系,需要对这些组进行更新,更新的位置在MyNetwork.addRelation
;二是两个已有关系的人被加入某些组,也需要对这些组中进行更新,更新位置在MyGroup.addPerson,MyGroup.delPerson
。而对于age
,需要对age的和与平方和进行维护,维护的地方在MyGroup.addPerson,MyGroup.delPerson
。在这些地方更新完毕后,查询就是返回这些对应的和。值得一提的是,getAgeVar
所返回的值需要利用数学方法分析得到表达式,即
其中要注意的是涉及到的除法是整除,不能够直接使用乘法分配律;不能够拆出去,避免前面出现负数然后合并出现问题(Java整除是去掉小数点而非向下取整)。
-
第三次作业
第三次作业貌似没有强连通分量的求取,但是仍然有拉性能的新函数——sendIndirectMessage
和deleteColdEmoji
。其中sendIndirectMessage
是寻求最短路,而deleteColdEmoji
则是迭代删除。前者比较明显,朴素的做法就是使用Dijkstra
算法,复杂度为O(V^2)
,性能已经较为崩坏,而我采用的是加上堆优化的方法,让复杂度变为Elog(E)
,当边较少时不会退化为V^2log(V^2)
,题目限制输入已经保证了该点。deleteColdEmoji
朴素方法就是遍历寻找热度小于limit的emojiId,在找到时便同时遍历messages
数组将相关的消息全部删除,这样复杂度为O(n^2)
,复杂度较高。有许多优化方法,可以采用TreeSet
与新建节点类对热度进行排序,简化遍历流程,同时将id和与其相关的消息用数组关联,删除时根据这个关联数组从messages
数组中删除。但我选择的方法是相对简单的先遍历messages
数组查询对应emoji热度进行迭代删除,再删除emoji,可以让复杂度退化为O(n)
。
梳理自己的作业架构设计,特别是图模型构建与维护策略
我这一单元的作业架构设计并没有太出奇的地方,只是简单地按照对课程组提供的接口的实现来构建代码框架。在此之外,仅仅增加了并查集,Dijkstra
算法使用优先队列的Node
节点等结构。这样来看,如果不做出合理的设计,一旦涉及到增删指令,操作就会很分散和繁琐,一旦代码量大了,很容易出现遗漏的地方,例如在实时更新valueSum
时就需要在三个地方进行更新操作的代码书写来进行操作选择,这样就可以在包装函数中统一进行某些增删控制操作。这里的Runner
写死了,也就不太方便。
附三次作业UML图:(不包括异常类)
-
第一次作业
第三单元博客
第三单元是对JML规格的初步接触和练习。其初衷是为了以严格的语言来规范代码的书写,避免二义性,传达清晰的信息。总的来说,本单元作业难度不算特别大,但需要对JML的描述进行转化避免性能过差。因此在JML的规格约束下设计合适的架构,进行合理的选择是整个作业的关键,也是使用形式化语言的关键。
从JML得到代码的设计策略
首先要说的是,我是如何一般性地从JML得到一份代码,效率高而又不出现问题。首先遵循通读原则和从简到繁的原则,拿到JML应该先通读一下属性规格,方法名,从而大致了解一下其功能。然后再读相对简单的规格并进行代码补全,例如get
,set
方法等。最后再综合考虑先前理解和复杂规格将复杂的方法解决。这样既能让人大致了解要完成什么任务,又能将困难的任务集中在理解建立之后以进行正确的编写和充分的优化。
结合课程内容,整理基于JML规格来设计测试的方法和策略
基于JML规格来进行测试,一般推荐Junit,其可以自行设计测试点来对代码进行测试。通过学习,我发现Junit的测试很想我们的操作系统课程的测试方式,即生成一个测试函数写满断言来对各个部分的正确行为进行评估,而操作系统实验的基本任务是补全代码,很类似于我们这里根据JML对代码进行填充。因此模仿我们操作系统实验的测试方法就成了我设计测试策略的重要手段。
大概来说,基于JML规格用Junit来对代码正确性进行测试,可以通过根据JML的几个\requires
语句提供的前置条件,利用\ensures
语句给出的后置条件来对类的行为进行基本测试,具体而言就是利用assertEquals
来对行为设定预期结果进行判定,可以生成类实例,调用方法,塞入合适的参数来进行测试。
而对于异常类的测试,由于涉及到标准输出,因此可以将输出流重定位,例如重定位至ByteArrayOutputStream
,将其缓存在这个字节缓存中,然后使用try-fail-catch
语句捕获异常并进行print
来assertEuqals
进行比对。
容器的选择经验
在设计中,很重要的一点便是如何选择容器。对于规格中的数组,我基本选择的是HashMap
,但并不以累加的下标作key,而是以各自的唯一的id等作key,充分利用hash查找的O(1)
的复杂度进行优化。实例有MyNetwork
中的people
数组/Hashmap
和value
数组。people
数组即以Person
的id做索引。value
数组/Hashmap
则为了简化调用,将Person
作为key而未将其id作为key。但本质上都是利用hash的好处。
而对实在没有能逻辑上构成对的数据,但数据之间不会equals
时,则选择HashSet
进行存储,这样仍然便于检索查找;而对可重复数据则在ArrayList
和LinkList
中根据查找,插入和删除的使用频率进行选择,如果查找较多或加入删除集中于尾部则使用ArrayList
,如果插入删除较多则使用LinkList
,例如MyPerson
中的messages
数组,就是因为收到的消息可能相同,但是仅仅起到记录作用而采用了ArrayList
。
而对总是需要排序的数据结构,就需要使用TreeMap
或者PriorityQueue
(堆实现),或者手动实现Comparable
接口进行Collection.sort
排序(但是较慢,不如红黑树实现的TreeMap
)。例如第三次作业中的Dijkstra
算法中寻找当前最短路径使用TreeMap
和PriorityQueue
均可以,我采用的是PriorityQueue
,因为堆可以较快的维护得到最小值或最大值而又不需要保证完全有序。
针对本单元容易出现的性能问题,总结分析原因如果自己作业没有出现,分析自己的设计为何可以避免
-
第一次作业
第一次作业容易出现性能问题的方法是queryNameRank
,isCircle
,queryBlockSum
。其中queryNameRank
相对不容易出问题,因为最慢也是完整扫描一遍people
数组,达到O(n)
的复杂度(我即是如此做的)。isCircle
关键在于设计是否合理,最朴素的做法就是dfs或bfs算法进行搜索,acquantance
数组相当于邻接表,这样每次遍历复杂度为O(n)
,如果查询过多,性能会较差,也不便于queryBlockSum
和后续作业顺带对于人与人之间是否有关系的判断。因此从这里开始就可以考虑使用一种新的数据结构进行优化——并查集。因为考虑到并查集主要用来判断元素是否共集合,其满足且恰好满足无向图的连通记录,复杂度低,且寻找父节点时压缩结构可以使插入复杂度为O(1)
。我在此处采用的是以HashMap
tree
保存上级结构,用循环方式查找父节点并压缩,没有递归的复杂调用但是树结构优化不彻底,但是足够使用。而在queryBlockSum
中只需要对tree
进行一次遍历,记录根的个数即可得到连通分量数,复杂度为O(n)
。当然这还可以进一步优化,彻底贯彻修改即更新的原则,每次加入新节点或关系就对连通分量数进行更新,可以使得queryBlockSum
复杂度为O(1)
,但是操作相对复杂分散。
-
第二次作业
第二次作业的关键函数为MyNetwork.queryGroupValueSum <-> MyGroup.getValueSum
,MyNetwork.queryGroupAgeMean <-> MyGroup.getAgeMean
和MyNetwork.queryGroupAgeVar <-> MyGroup.getAgeVar
。这三组函数都涉及到如何快速求和。一般来说,朴素的办法就是查询才计算,每次都要遍历O(n)
,特别是value
还需要判断是否isLinked
,容易变成两重循环O(n^2)
,查询指令一多就容易超时。而较好的办法同样是每次插入与删除便更新,查询只是查变量,那么就需要提防在那些地方存在对Group内属性的修改。对于value
,存在两种情况,一是两个人已经在某些组中而新建立了关系,需要对这些组进行更新,更新的位置在MyNetwork.addRelation
;二是两个已有关系的人被加入某些组,也需要对这些组中进行更新,更新位置在MyGroup.addPerson,MyGroup.delPerson
。而对于age
,需要对age的和与平方和进行维护,维护的地方在MyGroup.addPerson,MyGroup.delPerson
。在这些地方更新完毕后,查询就是返回这些对应的和。值得一提的是,getAgeVar
所返回的值需要利用数学方法分析得到表达式,即
其中要注意的是涉及到的除法是整除,不能够直接使用乘法分配律;不能够拆出去,避免前面出现负数然后合并出现问题(Java整除是去掉小数点而非向下取整)。
-
第三次作业
第三次作业貌似没有强连通分量的求取,但是仍然有拉性能的新函数——sendIndirectMessage
和deleteColdEmoji
。其中sendIndirectMessage
是寻求最短路,而deleteColdEmoji
则是迭代删除。前者比较明显,朴素的做法就是使用Dijkstra
算法,复杂度为O(V^2)
,性能已经较为崩坏,而我采用的是加上堆优化的方法,让复杂度变为Elog(E)
,当边较少时不会退化为V^2log(V^2)
,题目限制输入已经保证了该点。deleteColdEmoji
朴素方法就是遍历寻找热度小于limit的emojiId,在找到时便同时遍历messages
数组将相关的消息全部删除,这样复杂度为O(n^2)
,复杂度较高。有许多优化方法,可以采用TreeSet
与新建节点类对热度进行排序,简化遍历流程,同时将id和与其相关的消息用数组关联,删除时根据这个关联数组从messages
数组中删除。但我选择的方法是相对简单的先遍历messages
数组查询对应emoji热度进行迭代删除,再删除emoji,可以让复杂度退化为O(n)
。
梳理自己的作业架构设计,特别是图模型构建与维护策略
我这一单元的作业架构设计并没有太出奇的地方,只是简单地按照对课程组提供的接口的实现来构建代码框架。在此之外,仅仅增加了并查集,Dijkstra
算法使用优先队列的Node
节点等结构。这样来看,如果不做出合理的设计,一旦涉及到增删指令,操作就会很分散和繁琐,一旦代码量大了,很容易出现遗漏的地方,例如在实时更新valueSum
时就需要在三个地方进行更新操作的代码书写来进行操作选择,这样就可以在包装函数中统一进行某些增删控制操作。这里的Runner
写死了,也就不太方便。
附三次作业UML图:(不包括异常类)
-
第一次作业
第三单元博客
第三单元是对JML规格的初步接触和练习。其初衷是为了以严格的语言来规范代码的书写,避免二义性,传达清晰的信息。总的来说,本单元作业难度不算特别大,但需要对JML的描述进行转化避免性能过差。因此在JML的规格约束下设计合适的架构,进行合理的选择是整个作业的关键,也是使用形式化语言的关键。
从JML得到代码的设计策略
首先要说的是,我是如何一般性地从JML得到一份代码,效率高而又不出现问题。首先遵循通读原则和从简到繁的原则,拿到JML应该先通读一下属性规格,方法名,从而大致了解一下其功能。然后再读相对简单的规格并进行代码补全,例如get
,set
方法等。最后再综合考虑先前理解和复杂规格将复杂的方法解决。这样既能让人大致了解要完成什么任务,又能将困难的任务集中在理解建立之后以进行正确的编写和充分的优化。
结合课程内容,整理基于JML规格来设计测试的方法和策略
基于JML规格来进行测试,一般推荐Junit,其可以自行设计测试点来对代码进行测试。通过学习,我发现Junit的测试很想我们的操作系统课程的测试方式,即生成一个测试函数写满断言来对各个部分的正确行为进行评估,而操作系统实验的基本任务是补全代码,很类似于我们这里根据JML对代码进行填充。因此模仿我们操作系统实验的测试方法就成了我设计测试策略的重要手段。
大概来说,基于JML规格用Junit来对代码正确性进行测试,可以通过根据JML的几个\requires
语句提供的前置条件,利用\ensures
语句给出的后置条件来对类的行为进行基本测试,具体而言就是利用assertEquals
来对行为设定预期结果进行判定,可以生成类实例,调用方法,塞入合适的参数来进行测试。
而对于异常类的测试,由于涉及到标准输出,因此可以将输出流重定位,例如重定位至ByteArrayOutputStream
,将其缓存在这个字节缓存中,然后使用try-fail-catch
语句捕获异常并进行print
来assertEuqals
进行比对。
容器的选择经验
在设计中,很重要的一点便是如何选择容器。对于规格中的数组,我基本选择的是HashMap
,但并不以累加的下标作key,而是以各自的唯一的id等作key,充分利用hash查找的O(1)
的复杂度进行优化。实例有MyNetwork
中的people
数组/Hashmap
和value
数组。people
数组即以Person
的id做索引。value
数组/Hashmap
则为了简化调用,将Person
作为key而未将其id作为key。但本质上都是利用hash的好处。
而对实在没有能逻辑上构成对的数据,但数据之间不会equals
时,则选择HashSet
进行存储,这样仍然便于检索查找;而对可重复数据则在ArrayList
和LinkList
中根据查找,插入和删除的使用频率进行选择,如果查找较多或加入删除集中于尾部则使用ArrayList
,如果插入删除较多则使用LinkList
,例如MyPerson
中的messages
数组,就是因为收到的消息可能相同,但是仅仅起到记录作用而采用了ArrayList
。
而对总是需要排序的数据结构,就需要使用TreeMap
或者PriorityQueue
(堆实现),或者手动实现Comparable
接口进行Collection.sort
排序(但是较慢,不如红黑树实现的TreeMap
)。例如第三次作业中的Dijkstra
算法中寻找当前最短路径使用TreeMap
和PriorityQueue
均可以,我采用的是PriorityQueue
,因为堆可以较快的维护得到最小值或最大值而又不需要保证完全有序。
针对本单元容易出现的性能问题,总结分析原因如果自己作业没有出现,分析自己的设计为何可以避免
-
第一次作业
第一次作业容易出现性能问题的方法是queryNameRank
,isCircle
,queryBlockSum
。其中queryNameRank
相对不容易出问题,因为最慢也是完整扫描一遍people
数组,达到O(n)
的复杂度(我即是如此做的)。isCircle
关键在于设计是否合理,最朴素的做法就是dfs或bfs算法进行搜索,acquantance
数组相当于邻接表,这样每次遍历复杂度为O(n)
,如果查询过多,性能会较差,也不便于queryBlockSum
和后续作业顺带对于人与人之间是否有关系的判断。因此从这里开始就可以考虑使用一种新的数据结构进行优化——并查集。因为考虑到并查集主要用来判断元素是否共集合,其满足且恰好满足无向图的连通记录,复杂度低,且寻找父节点时压缩结构可以使插入复杂度为O(1)
。我在此处采用的是以HashMap
tree
保存上级结构,用循环方式查找父节点并压缩,没有递归的复杂调用但是树结构优化不彻底,但是足够使用。而在queryBlockSum
中只需要对tree
进行一次遍历,记录根的个数即可得到连通分量数,复杂度为O(n)
。当然这还可以进一步优化,彻底贯彻修改即更新的原则,每次加入新节点或关系就对连通分量数进行更新,可以使得queryBlockSum
复杂度为O(1)
,但是操作相对复杂分散。
-
第二次作业
第二次作业的关键函数为MyNetwork.queryGroupValueSum <-> MyGroup.getValueSum
,MyNetwork.queryGroupAgeMean <-> MyGroup.getAgeMean
和MyNetwork.queryGroupAgeVar <-> MyGroup.getAgeVar
。这三组函数都涉及到如何快速求和。一般来说,朴素的办法就是查询才计算,每次都要遍历O(n)
,特别是value
还需要判断是否isLinked
,容易变成两重循环O(n^2)
,查询指令一多就容易超时。而较好的办法同样是每次插入与删除便更新,查询只是查变量,那么就需要提防在那些地方存在对Group内属性的修改。对于value
,存在两种情况,一是两个人已经在某些组中而新建立了关系,需要对这些组进行更新,更新的位置在MyNetwork.addRelation
;二是两个已有关系的人被加入某些组,也需要对这些组中进行更新,更新位置在MyGroup.addPerson,MyGroup.delPerson
。而对于age
,需要对age的和与平方和进行维护,维护的地方在MyGroup.addPerson,MyGroup.delPerson
。在这些地方更新完毕后,查询就是返回这些对应的和。值得一提的是,getAgeVar
所返回的值需要利用数学方法分析得到表达式,即
其中要注意的是涉及到的除法是整除,不能够直接使用乘法分配律;不能够拆出去,避免前面出现负数然后合并出现问题(Java整除是去掉小数点而非向下取整)。
-
第三次作业
第三次作业貌似没有强连通分量的求取,但是仍然有拉性能的新函数——sendIndirectMessage
和deleteColdEmoji
。其中sendIndirectMessage
是寻求最短路,而deleteColdEmoji
则是迭代删除。前者比较明显,朴素的做法就是使用Dijkstra
算法,复杂度为O(V^2)
,性能已经较为崩坏,而我采用的是加上堆优化的方法,让复杂度变为Elog(E)
,当边较少时不会退化为V^2log(V^2)
,题目限制输入已经保证了该点。deleteColdEmoji
朴素方法就是遍历寻找热度小于limit的emojiId,在找到时便同时遍历messages
数组将相关的消息全部删除,这样复杂度为O(n^2)
,复杂度较高。有许多优化方法,可以采用TreeSet
与新建节点类对热度进行排序,简化遍历流程,同时将id和与其相关的消息用数组关联,删除时根据这个关联数组从messages
数组中删除。但我选择的方法是相对简单的先遍历messages
数组查询对应emoji热度进行迭代删除,再删除emoji,可以让复杂度退化为O(n)
。
梳理自己的作业架构设计,特别是图模型构建与维护策略
我这一单元的作业架构设计并没有太出奇的地方,只是简单地按照对课程组提供的接口的实现来构建代码框架。在此之外,仅仅增加了并查集,Dijkstra
算法使用优先队列的Node
节点等结构。这样来看,如果不做出合理的设计,一旦涉及到增删指令,操作就会很分散和繁琐,一旦代码量大了,很容易出现遗漏的地方,例如在实时更新valueSum
时就需要在三个地方进行更新操作的代码书写来进行操作选择,这样就可以在包装函数中统一进行某些增删控制操作。这里的Runner
写死了,也就不太方便。
附三次作业UML图:(不包括异常类)
-
第一次作业
-
第二次作业
-
第三次作业
总结
总体来说,本章作业难度不大,关键是让我们体会一下形式化语言对信息传达的严谨性和形式化测试的完备性,以及复习一下图算法(x)和优化。期待最后一单元作业!
标签:数组,MyGroup,复杂度,作业,博客,面向对象,JML,程序设计,进行 来源: https://www.cnblogs.com/earsaxcs/p/14828772.html