2022-面向对象设计与构造-第三单元总结
2022-面向对象设计与构造-第三单元总结
架构设计
第九次作业
本次作业中图模型相关的方法主要是isCircle
和queryBlockSum
,二者都是依赖并查集优化。在并查集的实现上,我采用了路径压缩和按秩合并的策略,有效得提高了其查找的性能,尤其是对环形图的查找,可以从变为。同时为了避免MyNetwork
复杂度过高,我单独建立了一个UnionFindSet
类和Unit
类来封装并查集,其中Unit
类很简单,就是将节点的父亲和秩封装在了一起;UnionFindSet
通过一个HashMap<Integer, Unit>
将id映射到对应的Unit
节点来维护必要的信息,同时其还有一个blockSize
字段,用来给出queryBlock
的结果,这个字段在新加入元素时加一,在执行union
时若两个元素原来不是一组则减一。
在维护策略上,主要是有以下几个方法做了优化
isCircle
和queryBlockSum
:采用路径压缩的并查集维护,时间复杂度(路径压缩会导致少量的非情形)hasPerson, contains, getPerson, getGroup, queryValue
等:采用hashMap
存储相应的数据,使得能在时间内完成getValueSum
:在addPerson
和delPerson
是遍历当前的所有组员,维护一个valueSum
,使得getValueSum
时间复杂度从变为,但代价addPerson
和delPerson
时间复杂度从变为,这是值得的getAgeMean
:维护一个ageSum
,调用该方法时根据 在内给出结果getAgeVar
:维护一个年龄平方和ageSquareSum
,调用该方法时根据 在内给出结果
第十次作业
本次作业中主要增加了一个最小生成树的方法queryLeastConnection
,我是使用的最小生成树算法是Kruskal
算法,主要是因为已经实现了路径压缩的并查集,而且java标准库中有PriorityQueue
,实现一个堆优化的Kruskal
算法很简单,时间复杂度为,效率很高。通过一个打竞赛的室友得知可以动态维护最小生成树,这样可以在查询很多时进一步提高小效率,但是考虑到作业中对查询次数限制较严格,但是对修改次数限制宽松了很多,这样不仅会大大增加作业难度,而且还可能导致效率不增反降,因此没有采用这个方法。同时还注意到可以通过设计标志位记录一些已经算出过的数据,这样能在连续询问同一联通分支时只计算一次,大大提高效率,但问题是我们可以在每次查询中插入修改来使该方法完全失效,同时不使用该方法手动构造的用时最长的数据CPU时间也不超过2s,感觉没必要,也没有采用这个方法。在没有遇到性能问题时最好不要去考虑性能问题,优先去保证其功能的正确性。
此外,我认为官方包中给的Person
可以用来当做图中的点和边,但是由于没有方便的接口进行操作,如果新增方法的话又需要向上转型,因此我新建了Edge
类用于存放边,主要用于最小生成树方法。同时,本次作业中我对并查集做了优化,通过阅读别人的代码,我发现我第九次作业的并查集结构结构比较复杂,
同时还有一个小细节,在Person
中有对Message
的相关操作,仔细观察可以发现所有的操作都是在头尾进行的,不存在向中间的操作,因此一个好的做法是使用LinkedList
,这样可以在常数时间内完成对头尾的插入删除。
第十一次作业
本次作业中主要增加了一个计算最短路径的方法sendIndirectMessage
,我是使用的是PriorityQueue
优化的Dijkstra
算法,时间复杂度为。和上次作业类似,我没有用官方包中的Person
对图进行建模,而是使用了自己定义的Node
和Dot
对图进行建模,其中Node
是一个点,拥有一个HashMap<Integer, Integer>
存放与其相邻的点和对应边的权值;Dot
是也是一个点,主要用于在Dijkstra
算法中记录当前点的distance
,之所以用两个类是因为Node
是用来永久存放图信息并会更新的;Dot
只是在计算最短路径时使用,为不可变对象。同上次作业一样,我想到了记录已经算出来的最短路径来提高连续询问时的性能,但是没有遇到性能问题,因此没有实现该功能。
性能问题和修复情况
三次作业中我都没有遇到性能问题,但是这里我想说一下我第一次作业中遇到的一个和性能相关的小bug。
对于方法getValueSum
,最简单的做法是两遍循环,时间复杂度为,可能TLE。为了减低时间复杂度,我最开始的维护策略是给每个人维护一个valueSum
,然后再Group
的getValueSum
中,对所有人的valueSum
求和,因此gteValueSum
的复杂度是,同时Group
中的addPerson
和dePerson
方法的复杂度是,这样的复杂度肯定不会TLE。但是后来发现JML规格中要求对组内所有的边求和,我这种方式求的还有组外的,这就是我测试过程中发现的一个小bug。思考组内的VlaueSum
何时会改变,发现只有在addRelation
和Group.delPerson, Group.addPerson
中会改变,我就改成了组内维护一个ValuseSum
,在addRelation
时遍历一遍groups
,看这两个人是否都在这一组,如果都在,就增加改组的ValueSum
;在Group.addPerson, Group.delPerson
时遍历一遍组内当前的人数,看是否有和新增/删的人相连的人,如果有的话就对应地维护ValueSum
。这样getValueSum
的复杂度就是,而Group.addPerson, Group.delPerson
的复杂度是,addRelation
复杂度也有所增加,但是考虑到实际上group
可能不多,可以忽略。
测试设计
初次尝试Junit
本单元我首次尝试了Junit
测试,但是感觉用在本单元挺鸡肋的,主要是我个人觉得(限于我对Junit
的了解)它和测评机比唯二的优点是不调试就知道是哪个方法出了问题和能统计分支覆盖情况,但是在这个单元中每条指令几乎和每个方法时一一对应的本身就可以快速定位到出问题的方法,而分支覆盖问题我用测评机也能较好地解决,这就导致使用测评机相对使用Junit
是几乎是完全占优策,如果放到第一、四单元Junit
应该能突出其优势。因此本单元我主要的测试方案还是测评机自动测试,正确性判定是通过多人对拍。
测评机数据生成策略
本次作业自动生成数据较为简单,用随机数按照格式生成就好了。但是如果完全随机地生成会产生很多无效的id,导致正常分支覆盖率低,但是正常分支往往又是bug出现较为集中的地方。未解决这个问题,提高正常分支的覆盖率,我的室友陈正昊提出了一个比较好的方案:就是在数据生成器中记录一些必要的信息,在生成id时,根据id可能的关系进行分类,然后依概率进入每种类型,这样就能很大程度上控制数据进入正常分支还是异常分支,具体的做法如下
在生成id时,我们可能遇到需要单个id的情况,这时我们就看是需要personId
还是groupId
,我们以一定的概率从当前的personId
或groupId
集合中取一个id,这样取出的id就是有效的,能够进入正常分支。我们还可能遇到需要取两个id的情形,这又分两种情况,一种情况是两个personId
和一个personId
一个groupId
,这两种情况中又有如下的子情况
在取出两个personId
时(记为这两个id对应的对象为person1
和person2
),有四种子情况
person1
和person2
都不存在person1
和person2
只有一个存在person1
和person2
都存在,且不邻接person1
和person2
都存在,且邻接
在取出一个personId
和一个groupId
时(记两个id对应的对象为person
和group
),有五种子情况
person
不存在,group
不存在person
不存在,group
存在person
存在,group
不存在person
存在,group
存在,person
在group
里面person
存在,group
存在,person
不在在group
里面
在这些子情况基本能覆盖所有的分支(正常分支和异常分支),两种大情况的后两种子情况对应的是正常分支,我们可以通过随机数控制进入每种分支的概率,然后根据维护的信息生成对应子情况的数据。
完成了这样的数据生成器后,我通过在方法的不同分支中增加特定输出,简单地测试了一下各个分支的覆盖率,当每次数据量为10万条时,每次都能覆盖到所有的分支,说明该策略有效。
针对新的需求给出JML
规格
新需求
出现了几种不同的Person
Advertiser
:持续向外发送产品广告Producer
:产品生产商,通过Advertiser来销售产品Customer
:消费者,会关注广告并选择和自己偏好匹配的产品来购买——所谓购买,就是直接通过Advertiser
给相应Producer
发一个购买消息Person
:吃瓜群众,不发广告,不买东西,不卖东西
如此Network
可以支持市场营销,并能查询某种商品的销售额和销售路径等
对新需求的讨论
首先,我们对新增的三类Person
建模,他们都继承自Person
类,新增内容如下:
Advertiser
:新增producer
字段,记录和他合作的Producer
,以便发送相应的广告并给Producer
发送购买消息;新增salesVolume
字段,记录销售额Producer
:新增produceSize
字段,记录当前产品个数;新增productType
字段,用来表示生产的产品的种类,并记录产品的价格、成本等信息Custom
:新增preference
字段,用来表示偏好的产品;新增product
字段,用来表示购买的产品
同时,我们还应新增Advertisement
类并继承自Message
,用来表示一个广告消息,要有包含相应的advertise, producer, custon, money, product, selelCount
等(其中saleCount
字段表示有多少人通过该广告购买了该产品);需要新增Product
类,用来表示产品。
我认为这是最基础的情况,我们还可以进一步考虑复杂情况,如:Advertise
可以根据不同的Custom
(比如老主顾可以给点儿优惠啥的)和不同的产品给出不同的价格,Custom
可以选择从更便宜的Advertise
处购买,Producer
可以给出对Advertiser
激励方案(博弈与社会乱入),来点儿618促销等等。
方法先关的,首先Advertiser
有addAdvertisement
方法用于投放广告,有cooperate
方法用于和特定的Producer
绑定合作关系,有querySaleVolume
获取销售额;Producer
有saleProduct
方法用于销售产品,有cooperate
方法用于和特定的Advertiser
绑定合作关系;Custom
有delUselessAdvertisement
方法删除收到的广告中无关自己偏好商品的广告,有buy
方法选择特定的广告购买商品等等。
部分方法的JML
规格
Advertiser
会根据产品和生产商投放相应的广告,对应的方法addAdvertisement
方法规格如下
/*
@ public normal_behavior
@ requires contains(advertiserId) && contains(producerId)
@ && containsProduct(productId) && !containsMessage(advertisementId) &&
@ isCooperated(advertiserId, producerId) &&
@ !(\exists int i; 0 <= i && i < producers.length; producers[i].getId() == producerId &&
@ !producers[i].haveProduct(productId));
@ assignable messages;
@ ensures messages.length == \old(messages.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(messages.length);
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
@ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].getAdvertise.getId() == advertiserId
@ && messages[i].getProducer.getId() == producerId && messages[i].getProduct.getId() == productId
@ && messages[i].geMoney() == money && messages[i].getSaleCount() == 0));
@ ensures
@ public exceptional_behavior
@ signals (PersonNotFoundException e) !contains(advertiserId) || !contains(producerId);
@ also
@ public exceptional_behavior
@ signals (ProductNotFoundExcrption e) contains(advertiserId) && contains(producerId) &&
@ !containsProduct(productId);
@ also
@ public exceptional_behavior
@ signals (EqualMessageIdException e) contains(advertiserId) && contains(producerId) &&
@ containsProduct(productId) && containsMessage(advertisementId);
@ also
@ public exceptional_behavior
@ signals (NotCooperatedException e) contains(advertiserId) && contains(producerId) &&
@ containsProduct(productId) && !containsMessage(advertisementId) &&
@ !isCooperated(advertiserId, producerId);
@ also
@ public exceptional_behavior
@ signals (ProducerNotHaveProductException e) contains(advertiserId) && contains(producerId)
@ && containsProduct(productId) && !containsMessage(advertisementId) &&
@ isCooperated(advertiserId, producerId) &&
@ (\exists int i; 0 <= i && i < producers.length; producers[i].getId() == producerId &&
@ !producers[i].haveProduct(productId));
*/
public void addAdvertisement(int advertiserId, int producerId, int productId, int advertisementId, int money) throw PersonNotFoundException, ProductNotFoundExcrption, NotCooperatedException, ProducerNotHaveProductException, EqualMessageIdException;
Advertiser
有对应的销售额,可以通过querySalesVolume
方法询问其销售额,规格如下
/*
@ public normal_behavior
@ requires contains(advertiserId);
@ assignable \nothing;
@ ensures (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && \results = people[i].getSalesVolume());
@ public exceptional_behavior
@ signals (PersonNotFoundException e) !contains(advertiserId);
*/
public /*@ pure @*/ int querySalesVolume(int advertiserId) throw PersonNotFoundException;
Customer
可以根据自己的偏好过滤掉自己不感兴趣的广告,对应方法delUselessAdvertisement
规格如下
/*
@ public normal_behavior
@ requires contains(customerId);
@ assignable getPerson(customerId).messages;
@ ensures (\exists int i; 0 <= i && i < getPerson(customerId).messages.length;
@ getPerson(customerId).messages[i] instanceof Advertisement &&
@ getPerson(customerId).isInterestedIn(\old(getPerson(customerId).messages[i]).getProduct()));
@ ensures (\exists int i; 0 <= i && i < getPerson(customerId).messages.length;
@ !(getPerson(customerId).messages[i] instanceof Advertisement));
@ ensures !(\exists int i; 0 <= i && i < getPerson(customerId).messages.length;
@ getPerson(customerId).messages[i] instanceof Advertisement &&
@ !getPerson(customerId).isInterestedIn(\old(getPerson(customerId).messages[i]).getProduct()));
@ public exceptional_behavior
@ signals (PersonNotFoundException e) !contains(customerId);
*/
public void delUselessAdvertisement(int customerId) throw PersonNotFoundException;
心得体会
- 通过本单元,我学会了理解基础的
JML
语言,并能根据规范写出符合要求的代码。我认为用JML
语言来描述方法的规格优点在于严谨,不会出现谜语人的问题(OS上机就经常谜语人),但是缺点在于相对于自然语言要更难阅读和理解,又时会遇到自然语言一句话能说清的问题,JML
要写很多很多。同时JML
的另一大优点在于便于进行单元测试和形式化验证、机器检验,现在也有一些能够根据JML
规格自动测试方法的工具,但是很不成熟,用起来很鸡肋。现在随着程序越来越大,对正确性要求越来越高,我认为未来JML
语言会不断发展完善,希望它能成为一个为程序正确性保驾护航的工具,而不是成为一个华而不实,给程序员增加负担的语言。 - 本单元我回顾了一些在大一学习的算法和数据结构,如并查集、最小生成树算法、最短路径算法等,领略了算法和数据结构的魅力。同时,通过使用java自带的工具类,我很方便地完成了对部分算法的优化,熟悉了一些新的工具类,惊叹于面向对象、泛型等机制的精妙、方便。
- 本单元对程序的性能做了限制,促使我去思考如何动态维护一些值来加速特定的查询过程,其实动态维护的方法并不难,但是如果没有这个限制我很可能直接按照
JML
描述暴力求解,由此我认识到不应该直接按照JML
去写方法,而是建立在理解规格的含义之后再去写方法。写程序也是,不能想到什么些什么,要先有一个整体的把控,想好了在深入到细节去具体实现,这样往往能起到事半功倍的效果(这样的工程化方法从计算机组成课程就开始强调了)。 - 本单元重点训练的是根据
JML
写出符合规范的代码,但是对编写JML
训练很少,我认为JML
的这种思维很重要:通过前置条件、后置条件、约束条件等编写描述方法,在测试时可以直接根据这些前置条件、后置条件、约束条件进行检查,十分有利于单元测试。我们在进行单元测试时,也可以考虑方法的这些写出条件,然后给出相应的检查函数用来检查。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架