OO2022第三单元作业总结
面向对象第三次单元总结
前言
本单元学习了JML相关内容。本篇文章讲解了三次作业中如何选择正确的容器,如何选择正确的算法解决可能出现的性能问题。同时也阐释了本次作业的测试方法与Hack策略。最后完成了对于Network的一个拓展,实现了一个买卖广告系统,完成了对三个核心方法的JML的撰写。如有错误,谢谢指正!
阅读JML和实现JML的策略
在本单元的代码实现过程中,由于已经用JML为我们定义了方法的规格,所以只需要将JML翻译成代码语言即可。在翻译的过程中需要确保实现的逻辑严谨以及严格符合JML的规格。
在我看来,在本单元中认真阅读JML和掌握JML各项含义,实现正确的架构基本没有太大的问题,但前提是要认真阅读JML并提取出其全部信息,否则正确性肯定难以保证。
-
第一个策略:在本次中我认为更快实现JML的一个策略就是按一定顺序,从底到上的实现JML,按照这样实现的顺序逻辑会更加通畅并且架构更加清晰。我的设计策略是首先实现异常类,然后分别实现不同的类。从类的包含关系来看,首先实现较为简单的Person类和Mesage类,接着实现包含这两个类的Gruop类,最后实现较为复杂的Network类。
-
第二个策略:另一个实现JML的策略便是容器的选择,在本单元中我第一次见到了HashMap的 威力,在之前单元我一直采取的都是常用的ArrayList动态方法来进行存储。在本单元首次使用HashMap这类的数据结构才知道真香啊😍。Hashmap可以实现更快的查找,但也要存储更多的信息,是一种以空间换时间的策略,但我们的作业几乎不用考虑空间不够的问题,考虑时间才是第一要务,因此本单元我几乎所有的数据存储结构全部采用HashMap来实现。此外在本单元中我还学习到了更多的容器,例如HashSet集合,优先队列PriorityList,链表LinkedList,这些容器都视不同的需求而使用,慎重、机智的选择不同的容器也是本单元的一大重点。
三次作业分析
如我刚刚所述,只要JML认真阅读,实现的正确性有很好的保证,因此接下来的分析主要围绕容器的选择与性能问题的解决。这里不再分三次作业逐步叙述,而是整体进行叙述,因为三次作业没有明显的迭代关系,更像是一个整体被分为三个部分来完成。
1.1 不同类中容器的选择
在本次作业中自己主要采用了 HashMap
,HashSet
以及 LinkedList
容器。下面具体列出实现中采用的容器以及如此选择容器的依据。
Person
private HashMap<Person, Integer> acquaintance
用来储存一个人的有关系的人以及这些关系的对应value值,因为我们在person类中只需要使用Person和value的映射关系,因此我们不需要将二者分开存储,而是将其统一存放在Hashmap中,既可以减小空间的占用,又能很好的实现功能的查找,总而言之这是一个技巧,类似的映射关系都可以采用Hashmap这种容器来存储。
private LinkedList<Message> messages;
储存一个人接收到的 Message。由于 getReceivedMessages
方法以及 Network
中 sendMessage
方法的规格对 messages
的顺序有要求,故采用 List
进行存储;又由于 sendMessage
对 messages
进行的是头插操作,且没有涉及到对这个 List
的按下标访问,故相较于 ArrayList
而言选用了更适合头插与顺序遍历的 链表LinkedList
Group
HashMap: <Person.id, Person>
该容器储存该 group 内含有的人。这里选用了 HashMap
是出于考虑到我们实现的 Network
需要大量的根据 id
查询到某个Person的操作,所以将 id 和实体建立 HashMap
以便于查询。
Network
HashMap: <Person.id, Person>
用于实现id-->person的快速查找。
HashMap: <Message.id, Message>
用于实现id-->message的快速查找。
HashMap: <Group.id, Group>
用于实现id-->group的快速查找。
HashMap: <Emoji.id, Emoji.heat>
用于存储该 Network 中含有的表情以及表情对应的热度。
HashMap<Person.id, Person.id> father;//id-father
用于实现并查集的基本结构,并查集可以用于查找是否连通isCircle()和查询最小生成树queryLeastConnection()
HashMap<emoji.id, heat> // emoji id-heat
用于实现emoji.id--->heat的快速查找。
1.2 性能问题的解决
在本部分我将围绕三次作业中可能出现性能问题的几个函数进行阐述,并解释我如何解决这些性能问题。在三次作业中我们需要根据JMl实现许多函数功能,一旦这些函数设计到O(n^2)及以上的复杂度,我们就要慎重的考虑其实现方式,尽可能的降低其时间复杂度。
1.2.1 getValueSum (qgvs) ----维护方式实现
求一个 Group 中所有边的边权之和。对于该方法如果直接按照规格去对组内的 Person 进行二重循环遍历并两两判断是否有边,则单次执行该方法时间复杂度为 O(n^2),如果强测数据中有大量的 qgvs 指令便可能会超时 。我的方式是采取维护的方式实现,实现方法是采用一个变量来记录当前 Group 中所有边的边权之和,在对该组 addPerson
与 delPerson
,以及对组内的人进行 addRelation
时对边权和进行维护,对value值进行更新,使得 getValueSum
方法本身的复杂度为 O(1) ,可以满足性能要求。
1.2.2 getAgeMean与getAgeVar ----维护方式实现
求一个用户组内所有用户的年龄平均值与年龄方差,如果按照JML方式来实现,是一个O(n)的一重循环,虽然数据少时不会产生性能问题,但是多次查询时,每次都要重新计算,难免会降低性能。因此同样采用维护的方式实现,在此我们引入年龄平方和ageSqrSum,ageSum在addPerson
和delPerson
时对ageAqrSum,ageSum进行更新(相应的加减操作),getAgeMean与getAgeVar可以用公式
求出,注意公式的计算一定要按照JML展开且不能随意化简,否则会出现精度问题。
1.2.3 isCircle ---- 并查集实现
该方法实际意义为判断无向图上的两个节点是否连通,可采用并查集来实现。并查集 相关资料在网上很多,阅读之后很快就可以理解,其精华在于记录每个节点所在的组号(这里的组不是我们实现的Group的意思,而是这个节点所在的子图),边查找边更新。用并查集的方法,两个节点连通只要查找其组号是否相同。
1.2.4 queryBlockSum ---- 维护实现
该方法求无向图中连通块的数量,引入变量blockSum初始化为0。addPeson时,该Person与任何人都无关系,blockSum肯定加1,在addRelation时,如果联通的两人不属于同一个子图,联通二者以后blockSum减1。
1.2.5 sendIndirectMessage ---- 堆优化的Dijkstra 算法
该方法的返回值是两个人之间的最短路,需采用 Dijkstra 算法对无向图中的单源最短路进行求解。注意使用 Dijkstra 时需要采用堆优化。
1.2.6 queryLeastConnection ---- 并查集+kruskal算法实现最小生成树
这个函数要求我们找到某一节点所在的子图的最小生成树。首先我们利用之前实现的并查集可以找到该节点所在的子图,接下来使之前学过的的Kruskal算法实现最小生成树即可(当然使用其他的最小生成树算法例如Prim也可以)。
2.1 Hack策略与测试方法
2.1.2 测试方法
本单元中再进行测试时,我一开始尝试学习了Junit方法,但是感觉作用不大,不如和伙伴们一起对拍来的快且高效。下面是编写的一个简单的数据自动生成程序,总体上采用随机生成的策略,先加入必要的元素,再加入大量的query查询方法。用这种方法我和伙伴们也确实有效快速地找到了一些Bug,因此本单元强测互测都没有出现Bug。总而言之,这个单元与前两个单元相比,生成数据的方法更加简单,同时输出的结果也是一定的,采用对拍的方式对于第三单元来说非常有效。
#数据自动生成器
import os
import random
# 1000人 id 0 ---- 4000
numof_person = 1000
# 25组 id 0 ---- 10
numof_group = 10
# 3000条关系
numof_relation = 3000
# 500 for 3 kinds of message
numof_red_envelop_message = 250 # 250 type0,250type1 total 500
numof_notice_message = 250 # 250 type0,250type1 total 500
numof_emoji_message = 250 # 250 type0,250type1 total 500
numof_message = 250 # 250 type0,250type1 total 500
# 实现messeageid独一无二
def spawn():
total = 0
f = open("testPoints.txt", "w")
cnt = []
for i in range(0, 18):
cnt.append(0)
i = 0
# ag 10
for j in range(numof_group):
f.write("ag " + str(j) + "\n")
# ap 1000 times and add to random group
for j in range(numof_person):
f.write("ap " + str(j) +
" testname" + " " +
str(random.randint(0, 200)) + " \n")
f.write("atg " + str(j) + " " +
str(random.randint(0, numof_group)) + "\n")
# ar 3000
for j in range(numof_relation):
f.write("ar " + str(random.randint(0, numof_person)) + " " +
str(random.randint(0, numof_person)) + " " +
str(random.randint(0, 1000)) + "\n")
# am 250type1 250 type0
for j in range(numof_message):
m0 = "am " + str(j) + " " + \
str(random.randint(-1000, 1000)) + " " + \
str(0) + " " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_person)) + "\n"
m1 = "am " + str(numof_message + j) + " " + \
str(random.randint(-1000, 1000)) + " " + \
str(1) + " " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_group)) + "\n"
f.write(m0)
f.write(m1)
total += 2 * numof_message
# arem add red envelop
for j in range(numof_red_envelop_message):
m0 = "arem " + str(total + j) + " " + \
str(random.randint(0, 200)) + " " + str(0) + \
" " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_person)) + "\n"
m1 = "arem " + str(total + numof_red_envelop_message + j) + " " + \
str(random.randint(0, 200)) + " " + str(1) + \
" " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_group)) + "\n"
f.write(m0)
f.write(m1)
total += 2 * numof_red_envelop_message
# add notice
for j in range(numof_notice_message):
m0 = "anm " + str(total + j) + " " + \
"goodluck" + " " + str(0) + \
" " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_person)) + "\n"
m1 = "anm " + str(total + numof_notice_message + j) + " " + \
"goodluck" + " " + str(1) + \
" " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_group)) + "\n"
f.write(m0)
f.write(m1)
total += 2 * numof_notice_message
# store some emoji
for j in range(200):
f.write("sei " + str(j) + "\n")
f.write("sei " + str(numof_emoji_message + j) + "\n")
# add emoji
for j in range(numof_emoji_message):
m0 = "aem " + str(total + j) + " " + \
str(j) + " " + str(0) + \
" " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_person)) + "\n"
m1 = "aem " + str(total + numof_emoji_message + j) + " " + \
str(numof_emoji_message + j) + " " + str(1) + \
" " + str(random.randint(0, numof_person)) + " " + \
str(random.randint(0, numof_group)) + "\n"
f.write(m0)
f.write(m1)
total += 2 * numof_emoji_message
##########################################
# now we start query!!!!!!!!!!!!
##########################################
while i < 10000:
number = random.randint(0, 15)
if number == 0:
f.write("qci " + str(random.randint(0, numof_person)) + " " +
str(random.randint(0, numof_person)) + "\n")
i += 1
elif number == 1:
f.write("qbs\n")
i += 1
elif number == 2:
f.write("qgps " + str(random.randint(0, numof_group)) + "\n")
i += 1
elif number == 3:
f.write("qgvs " + str(random.randint(0, numof_group)) + "\n")
i += 1
elif number == 4:
f.write("qgav " + str(random.randint(0, numof_group)) + "\n")
i += 1
elif number == 5:
f.write("sm " + str(random.randint(0, total)) + "\n")
i += 1
elif number == 6:
f.write("qsv " + str(random.randint(0, numof_person)) + "\n")
i += 1
elif number == 7:
f.write("qrm " + str(random.randint(0, numof_person)) + "\n")
i += 1
elif number == 8:
f.write("qlc " + str(random.randint(0, numof_person)) + "\n")
i += 1
elif number == 9:
f.write(
"dfg " + str(random.randint(0, numof_person)) + " " + str(random.randint(0, numof_group)) + "\n")
i += 1
elif number == 10:
f.write("cn " + str(random.randint(0, numof_person)) + "\n")
i += 1
elif number == 11:
f.write("sei " + str(random.randint(0, 2 * numof_emoji_message)) + "\n")
i += 1
elif number == 12: # qp
f.write("qp " + str(random.randint(0, 2 * numof_emoji_message)) + "\n")
i += 1
elif number == 13: # qm
f.write("qm " + str(random.randint(0, numof_person)) + "\n")
i += 1
elif number == 14: #
f.write("sim " + str(random.randint(0, total)) + "\n")
if random.randint(0, 10000) < 10: # dce 不能加太多
f.write("dce " + str(random.randint(0, 10)) + "\n")
f.close()
if __name__ == "__main__":
spawn()
2.1.3 Hack策略
在本单元Hack时,大家出问题的地方往往是没有认真仔细阅读JML,比如组内人数不能超过1111等等。另一个就是性能问题,我在Hack时采用的是阅读代码,针对容易出问题的地方比如说1111这类的问题,直接在代码里看有没有出问题。再一个就是针对每次作业中容易出问题的性能问题的方法比如qgvs,qgbs,qic等等方法,构造大量的查询query数据来看其是否超时。这两个策略都比较简单而且有效。
3.1 Network图模型架构的扩展
3.1.1 需求分析
假设出现了几种不同的Person
Advertiser
:持续向外发送产品广告Producer
:产品生产商,通过Advertiser来销售产品Customer
:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser
给相应Producer
发一个购买消息Person
:吃瓜群众,不发广告,不买东西,不卖东西
- NetWork可以支持市场营销,并能查询某种商品的销售额和销售路径
- 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格
3.1.2 设想架构
Advertiser
,Producer
,Customer
本质上都是Person,可以让他们均继承Person类- 广告本质上是一条Message,新增广告类Advertisement继承 Message
- 此外我们还需要商品类Product,新增商品类Product
一下是上述类内部的大致属性与方法(一些不重要的方法类似getter,setter没有列出)
//Product
private int id; //商品id独一无二
private int price; //商品价格
private int type ;//商品的种类,这里用int来代表
//Advertisement
private int id ;//独一无二的id
private Product product;//该广告所对应的商品
private Person advertiser;//该广告的源头广告商
private Group group; //一般广告不止发给某一个人,在这里我们默认直接发往某一组的所有人
//Adevertiser
private ArrayList<Advertisement> ads; //待发送的广告
public void sendAd(Advertisement ad);//发一条广告,要想实现“Advertiser持续向外发送产品广告”可以将Adevertiser实现为进程然后不断操作该方法
public void addAd(Product product,Person person,Group group);//ads加入一条关于product的广告
public void sendBuymessage(Advertisement ad);//给Producer发送一条购买消息
//Producer
private Advertiser advertiser;//对接的广告商,发送广告时通过他
private ArrayList<Product> products;//已生产的商品
private HashMap<Integer,Integer> sales;//type类型商品对应销售额
public void sell(Product product,Group group)//售卖product,通过广告商发一条广告给group里的所有人
public void produce(Product product);//生产一个product
public int querySales(int type) ;//查询某一产品销售额
public int getSalesAmount();//得到HashMap对应type种类商品的销售额
//Customer
private int id;
private HashMap<Integer,Integer> like; //type的商品对应的喜爱值 type - likevalue
private ArrayList<Advertisement> recieveAds;//目前为止接受到的广告
public void buy();//从目前的收到的广告里挑一个最爱的type类型的商品购买,如果有多个最喜爱的商品的广告,只卖最早接收到的那个,一次只买一个
public void addReceiveAds(Advertisement);//收到一条广告
3.1.3 核心类JML撰写
下面挑选了三个核心方法的JML类进行撰写:
//Producer
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < products.length; @ products[i].equals(product));
@ assignable products;
@ ensures products.length == \old(products.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(products.length);
@ (\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i]))));
@ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product);
@ also
@ public exceptional_behavior
@ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
@ products[i].equals(product));
@*/
public void produce(Product product) throws EqualProductIdException;
//Advertiser
/*@ public normal_behavior
@ requires containsAd(ad)
@ assignable ad.group.people[*].recieveAds,ads;
@ ensures !containsAd(id) && ads.length == \old(ads.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(ads.length) && \old(ads[i].getId()) != id;
@ (\exists int j; 0 <= j && j < ads.length; ads[j].equals(\old(ads[i]))));
@ ensures (\forall Person p; ad.getGroup().hasPerson(p) && (p instance of Customer);
@ p.receiveAds.length == \old(p.receiveAds.length) + 1;
@ && (\forall int i; 0 <= i && i < \old(p.receiveAds.length);
@ (\exists int j; 0 <= j && j < p.receiveAds.length; @ p.receiveAds[j].equals(\old(p.receiveAds[i]))));
@ && (\exists int i; 0 <= i && i < p.receiveAds.length; p.receiveAds[i].equals(ad));
@ also
@ public exceptional_behavior
@ signals (AdNotFoundException e) !containsAd(ad);
@*/
public void sendAd(Advertisement ad);
//Producer
/*@ public normal_behavior
@ requires containsType(type);
@ ensures \result == getSalesAmount();
@ also
@ public exceptional_behavior
@ signals (TypeNotFoundException e) !containsType(type);
@*/
public int querySales(int type) ;
心得体会
本单元难度不大,主要是在学习一些JML的功能,同时在这一单元也学习到了一些算法的内容类似并查集,堆优化的迪杰斯特拉算法等等。
在这里有一个小建议,如果这个单元主要内容是让大家学会JML的阅读和理解的话,我觉得可能不需要三次作业,1~2次作业我觉得已经足够,第三次作业的话感觉就是在学习写更多的算法,其中JML的东西还是第1,2次作业那些东西,没有学到什么新的JML的知识,感觉没有必要。