Jml规格化设计——oo第三单元总结

Jml规格化设计


——oo第三单元总结

第一章 基本架构

第一次作业

overview

第一次作业比较简单,除了6个异常类,我们只需要实现MyPerson,MyGroup,MyNetwork三个类,支持大约10条指令。这些指令大部分比较简单,能通过HashMap查询快速解决,比较麻烦的指令是query_circle和query_block_sum,前者考察图两点的连通性,后者考察图的连通分支数量。如果暴力dfs或者bfs的话会tle。

异常类的实现比较简单,实现好单个id的异常类与两个id的异常类后,其他异常类就可以复制粘贴了。

架构图

第一次作业的架构图如下(省略官方接口以及异常类):

第一次作业架构

这里除了官方接口要求实现的类外,我还实现了MyBlock,MyBlocks两个类。二者都是为降低时间复杂而引入的辅助类。

整体上看,架构的关联线(UMLAssociation,UMLAggregation)没有穿过类所在的矩形区域,说明架构的耦合程度还可以接受。

数据结构

本次作业的一大特点是查询指令多,且同类对象的id唯一,这一特点决定了我的架构采用HashMap作为主要容器,id是HashMap的key,对象引用时HashMap的value。具体来说,Network中People,Groups,Group中People,People中acquaintance均用HashMap存储。

本次作业的另一大特点时重点考察图的连通性。为了避免每次查询时都遍历一遍图,我采用了并查集这一数据结构。

并查集分为两种,第一种是深度可以无限延伸的树,每次合并时需要将一棵深度较低的树的根节点作为深度较高的树根节点的子节点。这样一个节点最多被合并$log_2n$次,树的深度最多为$log_2n$,因此查询的时间复杂度为O(log_2n),合并的时间复杂度为O(1)。

第二种并查集是一个深度为2的树(俗称菊花图),这种并查集的根节点不是具体元素,而是代表连通分支。每次合并时需要节点较少的树的所有节点加入到节点较大的树的节点元素中。同样,每个节点最多被合并$log_2n$次,因此合并的最大次数为$\frac{n}{2}\cdot log_2n$,单次合并的时间复杂度为O(n)。由于树的高度为2,因此查询的时间复杂度为O(1)。

由此可见,不同算法在不同输入数据下性能不一样,还要具体情况具体分析。

我最终选择了第二种并查集(真实情况是我原本不知道并查集算法,第二种方法是我直接想出来的,后面经过查阅资料才知道这也是并查集),对于代表连通分支的根节点,我单开了MyBlock这个类,用于存储连通分支中所有MyPerson,这样一是为了连通分支的合并,而是为日后可能出现的最小生成树,最短路径做准备。同时,每个MyPerson都有属性用于记录所在连通分支,这是为了查询两个MyPerson是否连通。

为方便管理各连通分支,我还定义了MyBlocks类,这一类中存储了MyBlock的HashMap,这样当合并两个MyBlock时就可以调用MyBlocks中的方法,而不是让一个MyBlock中存放有另一个MyBlock的信息,从而减少类之间的耦合度。

架构实现

query_group_value_sum:(后面才知道这是第二次作业的指令,但毕竟第一次作业提出了group的getValueSum()方法,因此就在第一次作业的架构实现中分析):

这一方法查询group内所有人连接形成的边的权值和。可以暴力遍历group中的人,这样时间复杂度为O($n^2$)。这次作业指令最多1000条,不会tle,但为了避免日后作业tle,我开辟了冗余存储空间维护各group的valueSum。当执行add_relation,add_to_group,del_from_group三条指令时修改valueSum,执行其余指令时valueSum保持不变。具体来说,执行add_relation时,如果两个MyPeron在统一group内,则valueSum增加add_relation添加的value的两倍;执行add_to_group时,先遍历group中已经存在的每个MyPerson,如果新加入的MyPerson与其中的某个MyPerson间存在权值为value的边,则valueSum增加两倍value,然后再加入MyPerson;同理执行del_from_group时,首先将MyPerson从group中删除,然后遍历group中剩余的每个MyPerson,如果删除的MyPerson与其中的某个MyPerson间存在权值为value的边,则valueSum减少两倍value。

query_circle:

如上所述,菊花图并查集O(1)搞定。但是在add_relation是需要维护并查集,具体来说,如果add_relation的两个MyPerson本就在一个MyBlock中则不用合并并查集;否则将所含元素较少的MyBlock元素投入到所含元素较多的MyBlock(这样可以减少时间开销),在此之前需要将所含元素较少的MyBlock中所有MyPerson的MyBlock字段更新为所含元素较多的MyBlock。此外,还需要将元素个数较少的MyBlock从MyBlocks中移除。

query_block_sum:

直接返回MyBlocks中MyBlock集合的元素个数,O(1)搞定。

对于其他指令,除了MyBlock、valueSum等必要的维护外要实现的功能比较简单,基本两三行代码就可以搞定,因此不再赘述。

第二次作业

overview

第二次作业增加了Message类与MyPerson的socialValue,需要实现的功能增加了Message的添加,发送,查询。除此之外,还增加了group中MyPerson的age方差,连通分支最小生成树权值和,MyPerson的socialValue的查询。其中关于Message的指令实现较为简单,考察重点在于读题的仔细程度;age方差的查询考察冗余存储减小时间复杂度;连通分支最小生成树考察prim算法的堆优化或者kruskal算法。

此外,第二次作业新增了关于Message的几个异常,但同第一次作业一样,这些异常只用根据引发异常的id是一个还是两个复制粘贴不同id个数的已实现异常,然后改一改异常输出名称就可以了,不值一提。

架构图

以下是第二次作业架构图:

第二次作业架构图

这次作业新增的类除了官方要求的Message外,我还开了MyHeap类,用于prim算法的堆优化。

从架构图上看,本次作业的UML关联仍然没有横穿类所在矩形区域,说明架构耦合度可以接受。

数据结构

为实现prim算法中的节点距离数组,我在MyPerson中定义了minDistance属性,为了在堆优化的prim算法中知道MyPerson元素的下标,我在MyPerson中定义了indexInHeap属性。

尽管JDK有自带的PriorityQueue,但为了知道MyPerson在堆中的下标,我不得不自己定义MyHeap这一个堆,这个堆在调整MyPerson在堆中的位置后会更新MyPeron的indexInHeap属性。

PS.后来我看了大佬的实现才知道可以用HashMap避免知道MyPerson在堆中的效标,从而使用PriorityQueue解决问题。不过手搓MyHeap让我加深了对堆的理解,还是挺不错的。

MyHeap类的数据结构是一颗完全二叉树,用数组ArrayList实现,包含五个方法:

  • private int parent(int index)。该方法返回index位置的父节点,若该节点已经是根节点则返回-1,表示没有父节点。

  • private int child(int index, int leftRight)。该方法返回index位置的子节点,当leftRight为0是返回左孩子,当leftRight为1时返回右孩子。若子节点不存在(子节点索引值大于等于数组的size),方法返回-1。

  • public void set(int index, value)。该方法将index的位置的MyPerson的minDistance值设置为value,并调整堆元素的位置,使数组仍然满足堆的定义。需要注意的是,该方法的前置条件是设置value前,存在合适的值i,当index处元素的minDistance为i时,ArrayList符合堆的定义。换言之,前置条件为:只要index处元素具有合适的值,ArrayList就符合堆的定义。

  • public MyPerson poll()。该方法将堆的第零个元素返回,删除堆的第一个元素,调整堆元素的位置,使数组仍然满足堆的定义。在具体实现中,该方法将堆的最后一个元素移动到ArrayList的第零个位置,覆盖堆的第零个元素,然后调用set()方法,将堆的第零个位置的MyPerson的minDistance设置为该MyPerson的minDistance(此举是为了通过set()触发堆的维护)。对于被删除的第零个元素,将其indexInHeap属性设置为-1,表示元素不在堆中。需要注意的是,删除堆的第零个元素不能通过ArrayList的remove()方法,否则ArrayList将自动地把后面的元素前移,这将导致堆的形态被破坏,set()方法的前置条件不能被满足。因此,只能用最后一个元素覆盖第零个元素来达到删除的目的。在这个操作之前,需要用ArrayList的remove方法将最后一个元素删除(删除最后一个元素不会导致ArrayList其他元素的位置改变),以免堆中在第零个位置与最后一个位置有相同的MyPerson。

  • public void add(MyPerson person)。该方法将person加入到堆中,并且调整堆元素的位置以维护堆。在具体实现中,将person加入到堆的最后一个位置,然后调用set()方法,将堆的最后一个元素的minDistance设置为该元素原本的minDistance,从而触发堆的自动维护。

架构实现

query_group_age_var:

为实现查询的O(1)复杂度,我在group中维护了age的平方和ageSquSum以及age的和ageSum,当执行add_to_group以及del_from_group时更新这两个值。这样根据方差的计算公式便可以一步计算出ageVar。

但这里有个小细节,就是根据Jml定义,方差是先求和,再除以总人数,这就会带来除法运算取整的问题。Jml的具体描述是这样的:

/*@ ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length; 
  @                     (people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) / 
  @                      people.length));
  @*/

当人数大于0时,将这一公式展开,可得:

$$
\begin{aligned}
var &= \frac {\sum age_i^2 - 2 \cdot \overline {age} \cdot \sum {age_i} + n \cdot \overline {age}^2}{n} \
&= \frac {\sum age_i^2 - 2 \cdot \overline {age} \cdot \sum {age_i}}{n} + \overline {age}^2
\end{aligned}
$$

第二部减少了一次在数学上不必要的乘法,但结果是不对的。为什么呢?这就与整数除法的取整机制有关了,对于正整数,除法向上取整。以C++程序运行结果为例进行说明:

整数除法取整

可见,对于(-7)/3这样的负整数相除,如果向下取整的话应该为-3,但是负数采用了向上取整,因此为-2。这样有个好处,就是除法结果的绝对值与正负号无关,看起来对称。

回到本次作业中,如果按照第二步推导编写代码,则第一部分的分子极有可能是负数,因此除法将向上取整,这样算出的结果比正确答案大1。

query_received_messages:

这条指令不难,但是题目很坑,只取前4条Message,读题要小心。

query_least_connection:

这条指令是本次作业的难点,涉及最小生成树算法。我采用的方式是prim+堆优化,时间复杂度为$O(elog_2n)$,比传统的prim算法$O(n^2)$好了不少。前面我已经详细讲述了MyHeap的实现,对于prim+堆优化的具体实现我不想细讲,但我要讲一讲为什么一定要知道MyPerson在堆中的位置。

prim+堆优化算法中,每次将距离已有集合S最近的节点p加入到S中后,需要用p与其邻接节点的距离更新S到S外节点的距离。由于S外节点处于堆中,因此更新距离后需要维护堆。PriorityQueue不支持直接调整堆中元素的值后触发堆的自动调整,因此没有被我采用;MyHeap则通过set()方法实现了这一机制。但是如果仅仅传入对象让MyHeap自行搜索对象的索引将造成O(n)的复杂度(ArrayList的int indexOf(Object o)方法通过顺序查找获得索引值),这样的候多时prim+堆优化算法的时间复杂度变为了$O(nelog_2n)$,甚至比简单的prim算法更糟糕。因此有必要开辟单独的数组反映图的各个MyPerson节点在堆中的位置。

但都到了面向对象的时代,直接把MyPerson元素与它在堆中的位置绑定可能更好。这样有两种做法,一是新定义一个类,其中包括MyPerson与indexInHeap两个字段,这样的缺点是为了组合MyPerson与indexInHeap两个信息单开了一个类,显得冗余,并且每次执行prim+堆优化算法时都需要new许多的该类对象,开销较大。第二种做法是在MyPerson中定义indexInHeap字段,这样的缺点是MyPerson中多了一个冗余属性,因为这一属性只在最小生成树,最短路径问题中才被用到。回想我们学习过的知识,第一种做法类似于大一下学期数据结构中的链表,第二种做法类似于本学期操作系统中采用的侵入式链表。最终为减少类的数量,降低时间复杂度,我采用了第二种做法,即在MyPerson中定义indexInHeap字段

由此可见,为减少时间复杂度,常常需要开辟冗余存储空间。时间空间,鱼与熊掌不可得兼。

这里讲一下最小生成树权值和的维护方法。我在MyBlock中定义了int minDistacne属性,用于维护最小生成树的权值和;定义boolean addedRelation属性,用于判定在上一次更新minDistance后是否为这一连通分支中的MyPeron执行过add_relation指令。对某个MyBlock,起初addedRelation为false,因为最初每个人头上都顶着一个MyBlock,其中的minDistacne为0。执行add_relation指令时,分三种情况:

  • 当add_relation涉及到的两个人都不在该MyBlock中时,不用修改minDistacne以及addedRelation。

  • 当add_relation涉及到的一个人在该MyBlock中,另一个人在另一个MyBlock中时,只要其中一个MyBlock的addedRelation为true,则合并后的MyBlock的addedRelation为true,此时的minDistacne不能代表新的MyBlock最小生成树权值和,没有意义;当两个MyBlock的addedRelation均为false时,合并后的MyBlock的addedRelation为false,且minDistacne为两个MyBlock的的minDistacne之和加上add_relation指令中的value(新的最小生成树一定会包含add_relation引入的边)。

  • 当add_relation涉及到的两个人都在该MyBlock中,则addedRelation为true,minDistacne没有意义,因此不用改变。

当执行query_least_connection指令时,如果addedRelation为false,则直接返回MyBlock的minDistance;否则执行prim+堆优化算法,更新minDistance并且将addedRelation置为false,表示minDistance已经被更新为正确值。

到此为止,相信聪明的读者都看懂了吧!

本次作业的其他指令都很简单,不再赘述。

第三次作业

overview

本次作业增加了三个有特定含义的Message类,MyPerson的money属性,以及emoji。指令上将第二次作业的addMessage(),sendMessage()方法具体化,要求支持有特定含义的Message;新增MyPerson的money,emoji火热程度查询功能;增加了删除不火热的emoji,删除MyPerson收到的NoticeMessage指令;增加了最短路径查询指令。这些指令中,最短路径是难点,其他都是纸老虎。

异常上,本次作业新增了关于emoji的异常,由于复制粘贴比较简单,不再赘述。

架构图

以下是本次作业的架构图:

第三次作业架构图

本次作业除了官方接口要求增加了类,我新增加了MyEmoji类,用于管理一个emoji对应的多个EmojiMessage以及emoji的heat。

经过我精心谋篇布局,本次作业架构图仍然做到了UML关联不穿过类所在矩形区域。说明架构耦合程度可以接受。

数据结构

MyEmoji类中有类的heat,emoji对应的MyEmojiMessage编号的ArrayList两个属性。提供了三个方法:

  • public boolean isCold(int limit):

该方法根据传入的limit值判断该emoji是否冷门,是删除emoji以及相应的MyEmojiMessage的依据。

  • public void addOneHeat():

该方法让heat值加一,可以在发送emojiMessage时让对应的emoji的heat值加一。

  • public Iterator iterator():

该方法返回该emoji对应的MyEmojiMessage的编号迭代器,外部(MyNetwork)可以通过该迭代器获得MyEmojiMessage编号,进而可以执行删除MyEmojiMessage的操作。

在Network中,定义数据结构HashMap<Integer, MyEmonji> emojis来管理emoji。

架构实现

delete_cold_emoji:

遍历emojis容器,若某个emoji是冷的(isCold()返回true),则调用该emoji的iterator()方法,获得所属的MyEmojiMessage的id容器迭代器,通过这些id将Network中相应的MyEmojiMessage删除。

这样的数据结构与架构实现的好处是当确定要删除某个emoji时,不用遍历整个Messsage容器,而是可以拿到要删除的MyEmojiMessage编号,针对性地删除。(后来我估算了10000条指令的极端情况,发现即使暴力也不会超时,>_<)

事后,我发现MyEmoji中用于存放MyEmojiMessage的容器用HashSet<Integer>比用ArrayList<Integer>更好,原因是MyEmojiMessage的id不讲求顺序,具有唯一性,并且有可能被删除,因此当需要删除某个id时,用HashSet可以减少元素定位时间(hash VS 顺序查找),以及元素删除时间(红黑树 VS 连续数组)。

send_indirect_message:

这是本次作业的难点——最短路径。对此我采用的是dijkstra+堆优化。

这一算法与prim+堆优化类似,在不用HashMap的前提下需要知道元素在堆中的位置,因此我沿用了上一次实现的MyHeap。算法具体实现不再赘述。

与上一次作业的query_least_connection不同的是,这一次的最小路径通过冗余存储维护,因此每次执行send_indirect_message指令时都需要执行dijkstra+堆优化算法。

其余指令以及异常类都很简单,按下不表。

第二章 错误总结

本单元我的正确率较高,强测与互测都只在第三次作业中因为dijkstra算法出了错,但是在与别人对拍的过程中我还是发现了好一些错误。

错误1:group的ageVar取整问题,详见这里

点评:掌握了整数相除细节,避免以后出现类似错误。

错误2:复制粘贴异常后忘记修改System.out.println()中的异常名称。

点评:该挨板子。

错误3:query_received_messages中忽略了Jml要求仅仅取出前四条指令。

点评:该挨板子。

错误4:dijkstra算法与prim算法中忽略了MyPerson的query_value()方法在不连通时返回0,自己与自己相连,并且图中存在权值为0的边。

解决方法:先用isLinked()方法判定是否连通,不连通则赋予权值99999,连通时才用query_value()方法获取权值。

错误5:dijkstra算法与prim算法中忽略了源点MyPerson的indexInHeap值应设置为-1,表示该MyPerson不再MyHeap中。

错误6:dijkstra算法与prim算法中忽略了源点MyPerson的minDistance值应设置为0,分别表示源点通过仅含有源点的集合到源点的最短距离是0,仅含有源点的集合到源点的最短距离是0。

错误7:dijkstra算法中无穷大设置为了99999。在第三次作业强测中,第六个6测试点构造了一字长蛇阵图,这样两端点之间的权值和超过了99999,出现错误。

解决办法:将无穷大设置为0x7fffffff。

点评:

  • 无穷大$\iff$99999是极其不专业的做法。

  • 当0x7fffffff也无法解决问题时,可能需要考虑用负数权值代表无穷大,因为dijkstra算法不允许负数权值。

  • 随机数据生成器很难构造链状的图,日后关于图论的算法要单独构造链状图测试点。

错误8:发送MyEmojiMessage后没有删除其在对应emoji的MyEmojiMessage集合中的编号。导致当同样编号的Message被加入Network后delete_cold_emoji指令会将其删除。

解决办法:在delete_cold_message时判定Message为MyEmojiMessage并且对应的emoji编号为要删除的emoji编号时才将Message删除。这个做法是我在截止提交前1.5小时想出来的,尽管看上去并不优雅,但能解决问题。在写博客时,我想到其实可以在发送MyEmojiMessage时就将它的编号从对应emoji的MyEmojiMessage集合中删除。因此我在这里详细分析了如何降低删除的时间复杂度。

点评:

这一错误没有被我及时发现的原因是我的评测机为每个Message设置了唯一的id,这个id即使在Message被发出后也不会被后来的Message利用。之所以这样设计是为了降低评测机编写的难度,只需要定义一个message_id_count的全局变量,每增加一条Message将其加一就可以实现。反观大佬的评测机,只见一开始便开辟好了Message的id集合,每当新建Message时便从集合中随机取出一个编号,编号不一定从零开始,并且很有可能会重复,极好地覆盖了Message编号重复的异常与我的bug。学到了!

错误9:ErroNotFoundException

第三章 测试方法

评测机暴力

生成数据与对拍的逻辑不再赘述。这里提几个亮点。

  • 模块化测试,本次作业的数据生成程序分为几个模块,分别测试不同功能。这样构造的数据具有针对性,而不是漫天撒网,难以用有限的指令找到bug。具体来说,本次作业分为了group,qlc,sim,msg四个模块,其中group主打query_group_age_var,query_group_value_sum指令;qlc主打query_least_connection指令;sim主打send_indirect_message指令;msg模块涵盖了全部指令与异常,用于最后的综合测试。各模块测试重点明确。

  • 集中程度高,本次作业我全程使用命令行调用上述数据生成模块与数据对拍模块check,实现了数据生成-结果比较一体化。

  • 异常全覆盖,三次作业异常众多,为尽可能覆盖所有异常,我编写程序exception_check遍历输出,列出测试数据没有覆盖到的异常,从而便于我调整测试数据,实现异常全覆盖。

下图展示了我的全部测试模块:

测试模块

Junit单元测试

本次作业对于group单元,我部署了Junit测试工具。

初始化

在所有测试方法执行前,执行初始化setup()方法,因此用@Before修饰setup()方法。

@Before
public void setup() throws Exception {
    System.out.println("setup!");
    people.add(new MyPerson(0, "o", 100));
    people.add(new MyPerson(1, "o", 100));
    people.add(new MyPerson(2, "o", 100));
    people.add(new MyPerson(3, "o", 0));
    network.addPerson(people.get(0));
    network.addPerson(people.get(1));
    network.addPerson(people.get(2));
    network.addPerson(people.get(3));
    network.addRelation(0, 1, 100);
    network.addGroup(group);
}

需要注意的是一旦用@Before修饰,就必须设置方法可见性为public。

随后是一系列测试方法,均用@Test修饰。

@Test
void getAgeMean() throws Exception {
    setup();
    assertEquals(0, group.getAgeMean());
}

@Test
void getAgeVar() throws Exception {
    setup();

    int var = group.getValueSum();
    assertEquals(0, var);

    network.addToGroup(0, 0);
    var = group.getAgeVar();
    assertEquals(0, var);

    network.addToGroup(1, 0);
    network.addToGroup(2, 0);
    var = group.getAgeVar();
    assertEquals(0, var);

    network.addToGroup(3, 0);
    var = group.getAgeVar();
    assertEquals(12 * 25 * 25 / 4, var);
}

@Test
void addPerson() throws Exception {
    setup();
}

@Test
void hasPerson() throws Exception {
    setup();
    assertFalse(group.hasPerson(people.get(0)));
    network.addToGroup(0, 0);
    assertTrue(group.hasPerson(people.get(0)));
    assertFalse(group.hasPerson(new MyPerson(1000, "ddd", 999)));
}

@Test
void getValueSum() throws Exception {
    setup();
    assertEquals(0, group.getValueSum());
    network.addToGroup(0, 0);
    network.addToGroup(1, 0);
    network.addToGroup(2, 0);
    network.addToGroup(3, 0);
    assertEquals(200, group.getValueSum());
    network.addRelation(1, 2, 1000);
    assertEquals(2200, group.getValueSum());
    network.addPerson(new MyPerson(9, "9", 9));
    network.addRelation(0, 9, 100000);
    assertEquals(2200, group.getValueSum());
    network.delFromGroup(1, 0);
    assertEquals(0, group.getValueSum());
}

@Test
void delPerson() throws Exception {
    setup();
}

@Test
void getSize() throws Exception {
    setup();
    assertEquals(0, group.getSize());
    network.addToGroup(0, 0);
    assertEquals(1, group.getSize());
}

@Test
void equals() throws Exception {
    setup();
    assertEquals(group, group);
    assertEquals(group, new MyGroup(0));
    assertNotEquals(group, new MyGroup(1));
    network.addToGroup(0, 0);
    assertEquals(group, new MyGroup(0));
    network.delFromGroup(0, 0);
    assertEquals(group, new MyGroup(0));
    assertNotEquals(group, null);
    assertNotEquals(group, new MyGroup(9999));
}

尽管equals()方法没有被我显式调用,但管理group的HashMap会使用equals()方法,因此equals()方法仍需要检查。

以下是测试结果:

Junit测试结果

以下是测试覆盖率分析:

Junit测试覆盖率分析

由此可见,上述针对group的单元测试达到了100%的覆盖率。

第四章 性能问题

本单元在强测与互测中我都没有出现性能问题。以下是提升性能的方法总结,其中大部分方法都在前面提到了:

  • 维护最小生成树权值总和,详见这里

  • prim与dijkstra算法均采用堆优化。

  • 为group维护valueSum,详见这里

  • 为group维护ageSum与ageSquSum,以实现query_group_age_var的O(1)查询,详见这里

  • 采用并查集算法,实现query_block_sum与querry_circle指令O(1)查询,详见这里

  • 采用专门的容器将管理同一个emoji下的MyEmojiMessage,避免删除MyEmojiMessage时遍历位于Network中的Message集合,详见这里

  • 初始化容器大小,减少扩容的时间开销。

  • 多使用临时变量存储中间计算结果,避免循环中的重复计算。例如在维护group的valueSum时,不优化的代码如下:

for (Person person1 : people.values()) {
    valueSum += (person.queryValue(person1) << 1);
}

优化后的代码如下:

int tmpValue = 0;
for (Person person1 : people.values()) {
    tmpValue += person.queryValue(person1);
}
valueSum += (tmpValue << 1);

可见优化后避免了循环中重复的移位运算,并且在循环中只需要访问局部变量,时间开销上比访问全局变量(属性变量)小。

第五章 扩展分析

Network发送广告:

假设发送广告后,发出Advertisement的Producer的money会减少Advertisement的cost(广告费),这一部分money会给Advertiser,想要买Advertisement的product的Consumer的money会减少Advertisement的price(商品价值),这一部分money会给Producer,并且该Producer的sales会增加购买的用户数量。

/*@ public normal_behavior
  @ requires contains(ads.getProducer()) && contains(ads.getAdvertiser());
  @ assignable people[*].money, ((Producer) people[*]).sales;
  @ ensures (\forall int i;0 <= i && i < people.length;
  @          (people[i] instanceof Producer && ads.getProducer().getId == people[i].getId()) ==>
  @           people[i].money = \old(people[i].getMoney()) - (ads.getCost()) +
  @           (\sum int j;0 <= j && j < people.length && people[j] instanceof Consumer && ((Consumer) people[j]).wantsToBuy(ads.getProduct());
  @            ads.getPrice())));
  @ ensures (\forall int i;0 <= i && i < people.length;
  @          (people[i] instanceof Producer && ads.getProducer().getId == people[i].getId()) ==>
  @           ((Producer) people[i]).sales = \old(((Producer) people[i]).getSales()) + 
  @           (\sum int j;0 <= j && j < people.length && people[j] instanceof Consumer && ((Consumer) people[j]).wantsToBuy(ads.getProduct());
  @            1)));
  @ ensures (\forall int i;0 <= i && i < people.length;
  @          (people[i] instanceof Advertiser && ads.getAdvertiser().getId() == people[i].getId()) ==>
  @           people[i].money = \old(people[i].getMoney()) + ads.getCost());
  @ ensures (\forall int i;0 <= i && i < people.length;
  @          (people[i] instanceof Consumer && people[i].wantsToBuy(ads.getProduct())) ==>
  @           people[i].money = \old(people[i].getMoney()) - ads.getPrice());
  @ ensures (\forall int i;0 <= i && i < people.lenght;
  @          !(people[i] instanceof Producer || people[i] instanceof Consumer || people[i] instanceof Advertiser) ==>
  @           \not_assigned(people[i].money));
  @ also
  @ public exceptional_behavior
  @ signals (ProducerNotFoundException e) !contains(ads.getProducer()) ||
  @                                       (contains(ads.getProducer()) && !(getPerson(ads.getProducer()) instacnceof Producer));
  @ also
  @ public exceptional_behavior
  @ signals (AdvertiserNotFoundException e) contains(ads.getProducer()) && getPerson(ads.getProducer() instacnceof Producer)) &&
  @                                         (!contains(ads.getProducer()) ||
  @                                          (contains(ads.getProducer()) && !(getPerson(ads.getProducer()) instacnceof Producer)));
  @*/
public void sendAdvertisement(Advertisement ads);

Network查询某个Producer的销量:

/*@ public normal_behavior
  @ requires contains(id) && getPerson(id) instanceof Producer;
  @ ensures \result == ((Producer) getPerson(id)).getSales();
  @ also
  @ public exceptional_behavior
  @ signals (ProducerNotFoundException e) !contains(ads.getProducer()) ||
  @                                       (contains(ads.getProducer()) && !(getPerson(ads.getProducer()) instacnceof Producer));
  @*/
public int querySales(int id);

Network查询某个Product的生产厂家:

/*@ public normal_behavior
  @ requires product != null;
  @ ensures (\forall int i;0 <= i && i < \result.size();
  @          contains(\result.get(i)) && \result.get(i) instanceof Producer && ((Producer) \result.get(i)).isProduce(product));
  @ ensures (\forall int i;0 <= i && i < people.length && people[i] instanceof Producer && ((Producer) people[i]).isProduce(product);
  @          (\exist int j;0 <= j && j < \result.size();\result.get(j).getId() == people[i].getId()));
  @ also
  @ public excetional_behavior
  @ signals (NullProductException e) product == null;
  @*/
public List<Producer> queryProducers(Product product);

第六章 总结

本章的学习让我有以下收获:

  • 重温了堆这一数据结构,prim算法与dijkstra算法(包括二者的证明过程),并写出了两个算法的堆优化版本,在大一下学期数据结构课上有了提升。

  • 学会了模块化构造数据,测试具有针对性。

  • 学会了定义局部变量的技巧,包括延迟变量定义时间,用局部变量存放中间结果。

  • 学会了命令行处理的高级技巧,在第二单元的命令行知识基础上有了提升。

  • 学会了使用Java的Runtime类获取程序运行内存消耗。

  • 学会了初始化Java容器的大小,减小扩容的时间开销。

  • 学会了Jml这一规格化设计语言。

posted @ 2022-06-01 18:59  Combinatorics  阅读(50)  评论(1编辑  收藏  举报