OO第三单元总结

OO第三单元总结

一、作业分析

1、第一次作业

(1)作业要求简述

第一次作业要求通过实现官方接口 Person、Network、Group,来实现自己的 MyPerson、MyNetwork、MyGroup类,并最后能实现以下指令:

指令 简写
add_person ap
add_relation ar
query_value qv
query_people_sum qps
query_circle qci
query_block_sum qbs
add_group ag
add_to_group atg
del_from_group dfg

(2)个人实现过程

第一次作业的难点在于query_circle、query_block_sum这两个指令的实现,根本上在于一个问题:判断无向图中两个节点之间是否存在路径

由于我是在这一次作业刚布置就开始写了,所以没有注意到后来的讨论区里各位同学的建议(也没有考虑到强测数据的暴力),直接就采用了大一下学期数据结构中学到的方法:DFS遍历。具体实现在此不赘述,DFS递归实现大家都懂。但是,架构设计的偷懒带来的就是面对大量数据测试时的不堪一击,性能直接炸裂……

之后,我便开始思考怎么进行性能上的优化:DFS肯定不能用。我也想到了,对people新开一个属性动态储存其关联节点,这样只需要在加人、加关系的时候维护一下,而在执行query_circle、query_block_sum这两个指令时直接通过该属性即可迅速完成。

但是,当时的我是开了一个列表,储存了该people的所有直接相关的结点,这就导致在维护时操作极其繁琐,最终性能与DFS相差无几,甚至更加恶劣。

最后,我在了解了并查集后意识到,我仅仅需要储存该people的最高父节点即可完成本次作业的指令的要求:毕竟我只需要判断某两个节点是否有路径即可,至于路径是怎么样的,我不需要管。

2、第二次作业

(1)作业要求简述

第二次作业要求通过实现官方接口 Person、Network、Group、Message,来实现自己的 MyPerson、MyNetwork、MyGroup、MyMessage类,并最后能实现以下指令:

指令 简写
add_person ap
add_relation ar
query_value qv
query_people_sum qps
query_circle qci
query_block_sum qbs
add_group ag
add_to_group atg
del_from_group dfg
query_group_people_sum qgps
query_group_value_sum qgvs
query_group_age_var qgav
add_message am
send_message sm
query_social_value qsv
query_received_messages qrm
query_least_connection qlc

(2)个人实现过程

第二次作业的难点在于2个问题:**最小生成树问题 **以及 如何保证面对大量qgvs、qgav指令时不超时

对于最小生成树问题,由于我第一次作业最终采用了并查集,这对于Prim算法的实现提供了非常良好的基础,因此这一难点不足为惧。

真正恶心的还是性能问题:如果节点、关系足够多,qgvs、qgav指令也足够多,就非常容易超时。

最终,我选择再开两个新属性valueSum、ageSum来储存qgvs、qgav指令应该返回的值,而这两个属性在加人、加关系等操作时动态维护。虽然增加了维护这一成本,但是在执行qgvs、qgav指令时复杂度为O(1)。

3、第三次作业

(1)作业要求简述

第三次作业要求通过实现官方接口 Person、Network、Group、Message、EmojiMessage、NoticeMessage、RedEnvelopeMessage,来实现自己的 MyPerson、MyNetwork、MyGroup、MyMessage、MyEmojiMessage、MyNoticeMessage、MyRedEnvelopeMessage类,并最后能实现以下指令:

指令 简写
add_person ap
add_relation ar
query_value qv
query_people_sum qps
query_circle qci
query_block_sum qbs
add_group ag
add_to_group atg
del_from_group dfg
query_group_people_sum qgps
query_group_value_sum qgvs
query_group_age_var qgav
add_message am
send_message sm
query_social_value qsv
query_received_messages qrm
query_least_connection qlc
add_red_envelope_message arem
add_notice_message anm
clean_notices cn
add_emoji_message aem
store_emoji_id sei
query_popularity qp
delete_cold_emoji dce
query_money qm
send_indirect_message sim

(2)个人实现过程

第三次作业的难点在于一个问题:无向图的最短路径问题

在算法方面,我没有选择迪杰斯特拉算法,而是选择了弗洛伊德算法。

我知道从单次复杂度来看,弗洛伊德比迪杰斯特拉复杂很多,但是,考虑到上次作业的测试点中,很多测试点的结构是“构造图+查询”,对于这种结构,弗洛伊德算法维护的图在面对大量查询时,其优势就体现出来了:虽然从单次求最短路径上来看成本更高,但是一旦操作变多,其边际成本是O(1)的。

4、异常实现

在这里,我对三次作业的异常实现进行统一阐述。

以MyEqualPersonIdException为例:

package com.oocourse.spec1.exceptions;

import java.util.HashMap;

public class MyEqualPersonIdException extends EqualPersonIdException {
    private static int sum = 0;
    private int id;
    private static HashMap<Integer, Integer> map = new HashMap<>();

    public MyEqualPersonIdException(int id) {
        this.id = id;
        if (map.containsKey(id)) {
            map.put(id, map.get(id) + 1);
        }
        else {
            map.put(id, 1);
        }
    }

    @Override
    public void print() {
        sum++;
        System.out.println("epi-" + sum + ", " + id + "-" + map.get(id));
    }
}

我增加了两个静态属性:

静态属性sum存储的值为本类异常出现的总次数;

静态属性map存储的是一个hashmap,键为发生本类异常的id,值为该id发生异常的次数;

每发生一次异常,sum就会自增一次,而MyEqualPersonIdException(int id)这一方法则会查询map中是否包含该id,有则值加一,没有则map增加一个键为id,值为1的元素。

通过以上机制,就实现了计数功能。

二、测试数据准备

由于本单元是根据JML规格完成作业,大多数指令都较为简单,每次作业都只有少数几条指令涉及到算法、可能出错,因此我选择偷点懒:直接到洛谷上找考察相关图论算法的题目,将其测试点构造的图通过我自己写的C语言程序改造成本单元的指令的格式,再手动测试即可。至于改造的C语言程序无非就是做一些字符串变换。

但是这难以对性能进行测试,这就导致了我在性能方面的不足。

三、Network扩展

假设出现了几种不同的Person
  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

Person:作为吃瓜群众,可用原先就已实现了的MyPerson来代替;

Advertiser:实现Person接口,新增1个属性 ArrayList<AdvertiseMessage> advertiseMessages存储产品广告;新增2个方法,分别为向消费者发送广告、向生产商发送购买消息PurchaseMessage2;
Producer:实现Person接口,新增1个方法接收PurchaseMessage并生产和发送产品
Customer:实现Person接口,新增2个属性ArrayList<Interger> hobby存储自己的爱好产品的id,ArrayList<AdvertiseMessage> advertiseMessages存储自己收到的广告;新增3个方法,向广告商发送购买消息PurchaseMessage1,清除不喜欢的广告,购买产品;
AdvertiseMessage:实现Message接口,新增1个属性存储产品id;
PurchaseMessage1:实现Message接口,新增1个属性存储产品id;

PurchaseMessage2:实现Message接口,新增1个属性存储产品id;

NetWork:新增属性存储当前的产品及销售额(可用hashmap)、广告消息、购买消息;新增Producer生产产品的方法,Customer选择产品的方法,查询产品销售额的方法,查询产品销售路径的方法。
Exception:新增ProduceIdNotFoundException,没有相应id的产品时触发;AdvertiseMessageIdNotFoundException,没有相应id的广告时触发;PurchaseMessage1IdNotFoundException、PurchaseMessage2IdNotFoundException,没有相应id的购买消息时触发。

Customer清除不喜欢的产品的广告:

/*@ public normal_behavior
  @ requires contains(personId);
  @ assignable getPerson(personId).advertiseMessages
  @ ensures (\forall int i; 0 <= i && i < \old(getPerson(personId).advertiseMessages.length);
  @          (\old(getPerson(personId).hobby.contains(getPerson(personId).advertiseMessages[i].produceId))
  @           ==> (\exists int j; 0 <= j && j < getPerson(personId).advertiseMessages.length; 
  @           getPerson(personId).advertiseMessages[j] == \old(getPerson(personId).advertiseMessages[i]))));
  @ ensures (\forall int i; 0 <= i && i < getPerson(personId).advertiseMessages.length;
  @          (\exists int j; 0 <= j && j < \old(getPerson(personId).advertiseMessages.length);
  @           getPerson(personId).advertiseMessages[i] == \old(getPerson(personId).advertiseMessages[j])));
  @ ensures getPerson(personId).advertiseMessages.length == 
  @          (\num_of int i; 0 <= i && i < \old(getPerson(personId).advertiseMessages.length); 
  @           getPerson(personId).hobby.contains(getPerson(personId).advertiseMessages[i].produceId));
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !contains(personId);
  @*/
public void deleteHateAdvertisements(int personId) throws PersonIdNotFoundException;

查询产品销售额:

/*@ public normal_behavior
  @ requires containsProduce(id);
  @ ensures (\exists int i; 0 <= i && i < produces.length; produces[i].getId() == id &&
  @         \result == produces[i].getNum());
  @ also
  @ public exceptional_behavior
  @ signals (ProduceIdNotFoundException e) !containsProduce(id);
  @*/
public /*@ pure @*/ Person getProduceNum(int id) throws ProduceIdNotFoundException;

Advertiser向Customer发送广告:

    /*@ public normal_behavior
      @ requires containsAdvertiseMessage(id) &&
      @          containsCustomer(getAdvertiseMessage(id).getPerson2());
      @ assignable getAdvertiseMessage(id).getPerson2().advertiseMessages;
      @ ensures !containsAdvertiseMessage(id) && advertiseMessages.length == \old(advertiseMessages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(advertiseMessages.length) && \old(advertiseMessages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < advertiseMessages.length; advertiseMessages[j].equals(\old(advertiseMessages[i]))));
      @ ensures (\forall int i; 0 <= i && i < \old(getAdvertiseMessage(id).getPerson2().getAdvertiseMessages().size());
      @          \old(getAdvertiseMessage(id)).getPerson2().getAdvertiseMessages().get(i+1) == \old(getAdvertiseMessage(id).getPerson2().getAdvertiseMessages().get(i)));
      @ ensures \old(getAdvertiseMessage(id)).getPerson2().getAdvertiseMessages().get(0).equals(\old(getAdvertiseMessage(id)));
      @ ensures \old(getAdvertiseMessage(id)).getPerson2().getAdvertiseMessages().size() == \old(getAdvertiseMessage(id).getPerson2().getAdvertiseMessages().size()) + 1;
      @ also
      @ public exceptional_behavior
      @ signals (AdvertiseMessageIdNotFoundException e) !containsAdvertiseMessage(id);
      @ signals (PersonIdNotFoundException e) containsAdvertiseMessage(id) &&
      @          !containsCustomer(getAdvertiseMessage(id).getPerson2());
      @*/
    public void sendAdvertise(int id) throws
      AdvertiseMessageIdNotFoundException, PersonIdNotFoundException;

四、单元学习体会

我有以下几点体会:

第一,JML规格对于编写代码有着很好的简化作用。这一单元的作业思维难度比前两个单元低了不少,大多数情况只需要仔细阅读、理解JML规格就可以完成代码的编写,这让我非常直观地感受到JML规格带来的好处。

第二,JML规格写起来有点反人类。JML好用归好用,但是难写是真的难写。有时候用自然语言非常容易描述的东西,用JML却极难描述;除此之外,JML中有一些ensure语句基本上已经是约定了,但是在形式化语言里还是要写,有些冗余。

第三,好的数据结构、好的算法对于功能的实现、性能的优化有着举足轻重的地位。比如第一次作业的并查集,相较于我最初使用的DFS,在性能方面强了无数倍。

我认为,以后在构思算法、结构的时候,应该先明确最终目的是什么、不可缺少的东西是什么,根据这些来构思:就比如isCircle里,我们只需要判断两个节点是否相连,即两个节点是否在一棵树上,而不用关注这两个节点之间是怎么相连的,所以我们就只需要知道所有节点所在树的根结点就可以得出结论,而不用遍历树,而这就是并查集的核心思想。

posted on 2022-06-06 15:48  哇哈哈小太阳  阅读(24)  评论(0编辑  收藏  举报