验训练抽象数据类型(ADT)的设计、规约、测试,并使用面向对象编程(OOP)技术实现 ADT。
- 实验环境配置
环境:IntelliJ IDEA,jdk21,Junit4
在这里给出你的GitHub Lab2仓库的URL地址(HIT-Lab2-学号)。
https://github.com/ComputerScienceHIT/HIT-Lab2-2022112818.git
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
完成Graph接口的两实现个类ConcreteEdgesGraph,ConcreteVerticesGraph ,实现Graph数据类型,同时可实现泛型化Graph<L>。其中需要在边与定点中实现add(添加新节点),set(添加新边),remove(移除节点),vertices(获得所有的节点集合),sources获得以target为目标节点的边的起始节点,targets获得以source为起始节点的边的目标节点。
打开实验要求的github仓库,获取URL后通过git clone克隆到本地的git仓库中。
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
Add:添加重复与不重复的点
Set:添加新边,修改边值,用Add验证边的两点是否添加成功
Remove:删除存在或不存在的点,利用Set验证与点相连的边是否存在
Vertices:验证输出点集是否相同
Sources/Targets:测试起始点/终点是否与图中一致
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
Edge中
字段
private final L source;:边的起点,类型为泛型L,并且使用final修饰
private final L target;:边的终点,类型也为泛型L,同样使用final修饰。
private final int weight;:边的权重,类型为整型,并使用final修饰。
构造函数
Edge(L source, L target, int weight):这是Edge类的构造函数,它接受三个参数来初始化source、target和weight字段。
Getter 方法
public L getSource():返回边的起点。
public L getTarget():返回边的终点。
public int getWeight():返回边的权重。
private void checkRep():这个方法是一个辅助方法,用于检查类的表示不变量是否满足。
public String toString():这个方法重写了Object类的toString()方法,返回边的字符串表示形式。
ConcreteEdgesGraph中
- add方法
加入一个新的顶点。
直接调用add函数。
② Set方法
输入source,target,weight,建立一条有向边。
具体做法:如weight>0,移去可能已经存在的相同起始点和终点的边,然后加入新的边,如weight=0,寻找可能已经存在的相同起始点的边,删去,如weight<0,提示输入非法。
③ remove方法
从vertices中删去给定的vertex点,遍历edges,寻找该vertex是否为某条边的起点或者终点,删去相应的边。在使用迭代器遍历时使用iterator.remove方法保证安全性。
④ vertices方法
返回vertices集合。
⑤ sources方法
根据传入的target参数寻找以target为终点的边。返回一个键值对为(点,权重)的map。
实现:建立一个map,利用迭代器遍历edges,如果某个edge的target和传入参数target相等,则将该边的source和weight存入map中。
target与source类似,这里不再赘述。
测试结果
Vertex中
字段
private L name;:顶点的名字,类型为泛型L。
private Map<L, Integer> sources = new HashMap<>();:指向该顶点的边的映射,键为边的起点,值为边的权重。
private Set<L> targets = new HashSet<>();:该顶点发出的边所指向的顶点集合。
构造函数
public Vertex(L name):构造函数,接受一个类型为L的参数作为顶点的名字,并初始化sources和targets字段。
方法
public int set(L source, int weight):设置从source指向当前顶点的边的权重。如果之前存在这样的边,则返回之前的权重;否则返回0。如果source为null或weight不大于0,则不执行任何操作。
public L getName():返回顶点的名字。
public Map<L, Integer> getSources():返回指向当前顶点的边的映射。
public void setSources(Map<L, Integer> sources):设置指向当前顶点的边的映射。public Set<L> getTargets():返回当前顶点发出的边所指向的顶点集合。
public void addTargets(L target):向当前顶点发出的边所指向的顶点集合中添加一个顶点。
public void removeTarget(L target):从当前顶点发出的边所指向的顶点集合中移除一个顶点。
public String toString():这个方法重写了Object类的toString()方法,返回边的字符串表示形式。
ConcreteVerticesGraph中
- add方法
加入一个新的顶点。
检查点集中没有相同点,若有返回false,若无将点加入点集后返回true。
② Set方法
输入source,target,weight,建立一条有向边。
具体做法:如weight>0,移去可能已经存在的相同起始点和终点的边,然后加入新的边,如weight=0,寻找可能已经存在的相同起始点的边,删去,如weight<0,提示输入非法。
③ remove方法
从vertices中删去给定的vertex点,遍历sources 和 targets,寻找该vertex是否为某条边的起点或者终点,删去相应的边。
④ vertices方法
返回vertices集合。
⑤ sources方法
根据传入的target参数寻找以target为终点的边。返回一个键值对为(点,权重)的map。
实现:建立一个map,利用迭代器遍历edges,如果某个edge的target和传入参数target相等,则将该边的source和weight存入map中。
target与source类似,这里不再赘述。
测试结果
将具体类的声明更改为:
public class ConcreteEdgesGraph<L> implements Graph<L> { ... }
class Edge<L> { ... }
和
public class ConcreteVerticesGraph<L> implements Graph<L> { ... }
class Vertex<L> { ... }
更新两个实现以支持任何类型的顶点标签,使用占位符L代替String。
选择ConcreteEdgesGraph,新建一个空Graph
分别测试空文档,单个单词与多个单词
GraphPoet读取文件并根据文件生成图。
利用BufferedReader.readLine方法读取全部输入后用string.split以空格划分,保存在数组中,随后每次取相邻元素,在图中新增边。
poem根据输入字符串和之前构造的图(graph)来生成一首诗
利用相同方法分割输入字符串,声明一个StringBuilder保存返回结果。每次读取一个词,然后以当前词为source,下一个词为target,在graph中寻找符合此条件的边,记录权值,结束后选择权值最大的,利用append方法,将节点名字加入字符串。
toString输出图结构
调用ConcreteEdgesGraph的toString方法,输出图结构
请按照http://web.mit.edu/6.031/www/sp17/psets/ps2/#before_youre_done的说明,检查你的程序。
如何通过Git提交当前版本到GitHub上你的Lab2仓库。
git add .
git commit -m "P1 Finished"
git push -u origin master
基于在Poetic Walks中定义的Graph及其两种实现,重新实现Lab1中的 FriendshipGraph类。
继承ConcreteEdgesGraph<Person>,构造一个ArrayList类型的变量personList存储顶点列表。
addVertex把参数添加到图中,作为图的一个顶点,直接调用父类的this.add()即可。
addEdge构建图的要素,在图中添加边。先调用 this.vertices().contains()方法来判断所添加边的顶点是否存在,再判断两顶点之间是否已有边连接,若条件满足,则调用this.set()方法设置边,权重初始化为1并返回true,其余情况返回false。
getDistance获取两个顶点之间距离的函数,采用广度遍历的方式。
Person类根据FriendshipGraph类的需求编写的。它用于描述每个成员的性质,主要是实例化姓名的构造方法。
直接复制实验要求的代码。
注释掉rachel -> ross后的测试结果
与lab1测试思路相同。
如何通过Git提交当前版本到GitHub上你的Lab3仓库。
git add .
git commit -m "P1 Finished"
git push -u origin master
在这里给出你的项目的目录结构树状示意图。
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
4.13 | 15:00-21:00 | 完成P1的GraphStaticTest类测试 | 按计划完成 |
4.14 | 全天 | 完成P1的ConcreteEdgesGraph类编写 | 按计划完成 |
4.15 | 15:00-21:00 | 完成P1的ConcreteVerticesGraph类 | 按计划完成 |
15:00-21:00 | 实现泛型Graph<L>的转换 | 按计划完成 | |
4.17 | 15:00-21:00 | 实现Poetic Walks | 未完成 |
4.20 | 全天 | 完成Poetic Walks | 按计划完成 |
4.21 | 全天 | 完成P2的Social Network的改写 | 完成 |
- 实验过程中遇到的困难与解决途径
遇到的难点 | 解决途径 |
对面向test的编程思想理解不够深入 | 查阅相关资料,阅读其它相关代码 |
对规约的要求不够理解 | 通过学习模仿,尝试自己编写相应规约并实现之。 |
目录结构调整有误 | 目前未解决 |
- 实验过程中收获的经验、教训、感想
- 实验过程中收获的经验和教训(必答)
经验:
加深了自己对于泛型的理解和认识,提高了代码编写、ADT设计的能力。编写test测试文件时,有些方法的测试也能覆盖到其他的方法,避免重复测试增加工作量。
教训:
在设计多个类并使之互相配合的方面做得不好,编写代码的逻辑性有待提高。
-
- 针对以下方面的感受(必答)
- 面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
前者代码复用性较好,避免大量重复工作,灵活性高,唯一劣势是ADT设计较难。直接面向应用场景逻辑更清晰,但是代码复用率低,重复工作多
- 使用泛型和不使用泛型的编程,对你来说有何差异?
可读性与可维护性更强,也更适合面向test编程
- 在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
优势是不考虑代码的内部实现,只需考虑是否完成了规约中指定的功能。难以适应这种方式。
- P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
减少重复代码
- 为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
提高程序的安全性与可读性,便于其他人阅读与维护,及客户端代码使用。虽然愿意但很多时候想不明白
- 关于本实验的工作量、难度、deadline。
工作量适中但是难度较大,如果ddl不延期又做不完
- 《软件构造》课程进展到目前,你对该课程有何收获和建议?
放慢上课节奏,下调实验难度,或者多来点学时,实验学时可以少一点,反正代码都是课下完成