北航20级oo课程第三单元总结
OO第三单元总结
架构设计:
本单元我们的主要任务是按照jml规格实现接口,主体代码已由课程组撰写完成。UML类图中展示的是第三次作业中自己实现的类以及它们之间的联系。前两次作业的架构大同小异,这里不多做展示。
架构分析:
本次作业涉及到基本的图论知识,在算法实现时,需要格外注意时间的复杂度,这个在后面的性能分析中会给出具体解决方案,这里我们先着重于代码的基本逻辑结构。通过JML的描述,我们可以知道,本次作业中,我们实现了一个基础的社交网络结构,以person作为网络节点,构建了一个无向图。依次为基础,我们需要求解最小生成树,连通无向图数,以及最短路径等问题。为了降低复杂度,在代码中,需要维护并查集relations和领接表raid。
private HashMap<Integer, Integer> relations; private HashMap<Integer, HashMap<Integer, Integer>> raid;
性能分析:
1.并查集:
在isCircle函数中,为了降低复杂度,实现了一个并查集,并查集的维护工作放在addRelation函数中,在添加关系时,就修改relations:
int f1 = find(id1); int f2 = find(id2); relations.put(f1, f2); relations.put(id1, f2); relations.put(id2, f2);
其中,f1、f2为id1与id2原本的源点,find为一个递归查找的函数。
2.Prim算法及其优化:
在求最小生成树(queryLeastConnection函数)中,我采用了prim算法来实现,但是普通的prim算法并不能满足复杂度的需求,因此在bug修复环节,我对prim算法进行了优化。
在普通的prim中,我们每次都需要从与生成树相连的顶点中找出一个权值最小的加入生成树,这个过程中遍历排序需要花费大量时间,因此,在MyNetwork中,我定义了一个内部类Line记录边长与端点,在一开始,我就对连通无向图中的所有边创建了一个Line实例并加入列表lines,之后对整个列表进行排序。每次选择顶点时,只需要从列表第一项开始遍历,满足Line的两个端点一个在树中,一个不在树中即可。在后续的数据构造中,发现这个算法仍然有rtle的风险,因此,在后续优化的过程中,增加了一个删除操作,若遇到一个Line其两端点都在生成树中,则意味着后续遍历中不需要再次遍历这个点,将其从列表中移除。
while (count < n) { for (int i = 0; i < lines.size(); i++) { Line l = lines.get(i); if ((treenodes.get(l.p1) && !treenodes.get(l.p2))) { treenodes.put(l.p2, true); sum += l.val; lines.remove(l); count++; break; } else if (!treenodes.get(l.p1) && treenodes.get(l.p2)) { treenodes.put(l.p1, true); sum += l.val; lines.remove(l); count++; break; } else if (treenodes.get(l.p1) && treenodes.get(l.p2)) { lines.remove(l); i--; } } }
以上是我对prim的最终优化结果。lines为Line的列表(已排序),treenodes为是否加入生成树的hashmap,key值代表节点编号,value值代表是否已加入生成树。
后来我在与同学的交流中,得知的prim还有一种堆优化的算法,很遗憾没能在作业中具体实现。(自己优化算法费力不讨好)
3.dijkstra算法:
我所采用的dijkstra算法就是最普通的那种,没有进行相应的优化,也是在后来知道prim的堆优化后,才知道dijkstra也能采用类似的方法进行优化。
数据构造:
本单元第一次作业我没有关注数据的问题,结果是造成了大片的rtle,后来,在构造数据时,我往往是从前一次bug中挑出一个复杂样例,并在此基础上进行修改。在构造过程中,我很少用到junit,这工具我只是用来验证JML实现的正确性(txt文件里直接添删指令多方便),构造极端数据时有两种策略,一种是在所有的ap,ar指令后集中查询;一种是每次ar后查询一次。
//思路一 ap ap ······ ar ar ······ qlc qlc qlc ······ //思路二 ap ap ······ ar qlc ar qlc ·····
在本次作业中,不乏有同学通过缓存的方式来缩短集中查询的时间,但是这种方法在第二种数据面前就不太好用了,本人也是由于bug修复时用了缓存被助教打回过,后来自己测试时也确实发现第二种数据虽然前几次查询的时间并不长,但是随着数据量的增大,还是会出现rtle的情况。
Bug分析:
本单元作业中,最常见的bug就是rtle了,无论是自己的代码,还是别人的代码,主要的错误都集中在rtle层面,而解决这类bug的方法,其实在上面的性能分析中已经给出。追根究底,我认为还是对JML的认识有了问题,才导致了这种现象。第一次作业,我就是老老实实照着JML来写代码,完全不考虑性能问题,才出现了大规模的rtle,第二次作业,我虽然有意识地注意复杂度的问题,但由于第一次作业的遗留问题,还是出现了rtle的情况,直到第三次作业,rtle才完全消失。JML只是功能的描述,并不能约束我们的具体实现方法,我们可以新定义类和方法,可以通过高性能的算法来优化,我觉得这是本单元bug给我的最深刻的教训。
Network扩展:
首先,这次扩展中,额外需要添加Advertiser,Producer,Customer 这三个继承Person的接口,Customer还得定义一个代表其偏好的成员preference;然后是Message接口得新增两个子接口AdvertiseMessage(type = 2),SaleMessage(type = 3)分别代表广告信息,以及销售(购买)信息,AdvertiseMessage内定义一个新成员prefertype。
Network新增四个方法:addAdvertiseMessage,sendAdvertiseMessage,addSaleMessage,sendSaleMessage。
/*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && @ (message.getType() == 0) ==> (message.getPerson1() != message.getPerson2()) && @ (message instanceof AdvertiseMessage); @ 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].equals(message)); @ also @ public normal_behavior @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && @ (message instanceof EmojiMessage) ==> containsEmojiId(((EmojiMessage) message).getEmojiId()) && @ (message.getType() == 0) ==> (message.getPerson1() != message.getPerson2()) && @ !(message instanceof AdvertiseMessage); @ assignable nothing; @ also @ public exceptional_behavior @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)); @*/ public void addAdvertiseMessage(Message message) throws EqualMessageIdException; /*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && @ (message.getType() == 0) ==> (message.getPerson1() != message.getPerson2()) && @ (message instanceof SaleMessage); @ 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].equals(message)); @ also @ public normal_behavior @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && @ (message instanceof EmojiMessage) ==> containsEmojiId(((EmojiMessage) message).getEmojiId()) && @ (message.getType() == 0) ==> (message.getPerson1() != message.getPerson2()) && @ !(message instanceof SaleMessage); @ assignable nothing; @ also @ public exceptional_behavior @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)); @*/ public void addSaleMessage(Message message) throws EqualMessageIdException; /*@ public normal_behavior @ requires containsMessage(id) && getMessage(id).getType() == 2 && @ getMessage(id).getPerson1() != getMessage(id).getPerson2() &&
@ getMessage(id).getPerson1() instanceof Advertiser &&
@ getMessage(id).getPerson2() instanceof Customer && @ ((Customer) getMessage(id).getPerson1()).getPreference == prefertype; @ assignable messages; @ assignable getMessage(id).getPerson2().messages; @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 && @ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id; @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size()); @ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i))); @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id))); @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1; @ also @ public exceptional_behavior @ signals (MessageIdNotFoundException e) !containsMessage(id); @*/ public void sendAdvertiseMessage(int id) throws MessageIdNotFoundException; /*@ public normal_behavior @ requires containsMessage(id) && getMessage(id).getType() == 3 && @ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() instanceof Advertiser &&
@ getMessage(id).getPerson2() instanceof Producer && @ getMessage(id).getPerson1() != getMessage(id).getPerson2(); @ assignable messages; @ assignable getMessage(id).getPerson2().messages; @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 && @ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id; @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size()); @ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i))); @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id))); @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1; @ also @ public exceptional_behavior @ signals (MessageIdNotFoundException e) !containsMessage(id); @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 3 && @ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2())); @*/ public void sendSaleMessage(int id) throws RelationNotFoundException, MessageIdNotFoundException;
心得体会:
本单元课程虽然相较于前面课程任务量较少,实现难度较低(优化难度较高),但是存在很多细节上的问题。实际上,就我个人感觉,JML规格虽然能够准确描述期望的功能,但是阅读以及编写起来过于困难,一个简单的需求需要多行JML来描述,不够直观,也容易出现理解上的错误。可以说阅读JML并实现相对应的接口对我来说是一个十分痛苦的过程,不过也确确实实锻炼了我的相关能力。希望接下来最后一单元的学习能够顺利结束。