2020年春季学期计算机学院《软件构造》课程Lab1实验报告
- Software Construction Lab-1
- Fundamental Java Programming and Testing
- 1183710109郭茁宁
1 实验目标概述
本次实验通过求解三个问题,训练基本 Java 编程技能,能够利用 Java OO 开发基本的功能模块,能够阅读理解已有代码框架并根据功能需求补全代码,能够为所开发的代码编写基本的测试程序并完成测试,初步保证所开发代码的正确性。另一方面,利用 Git 作为代码配置管理的工具,学会 Git 的基本使用方法。
基本的 Java OO 编程
基于 Eclipse IDE 进行 Java 编程
基于 JUnit 的测试
基于 Git 的代码配置管理
2 实验环境配置
2.1 Java & Eclipse & Maven 使用配置方法
2.2 JUnit
3 实验过程
3.1 Magic Squares
- 幻方是一个有n*n个不同数字、且每行、每列和斜线上都有相同的和的方形结构。要求写出程序判断输入一个矩阵是否是幻方,并且构造幻方。
- Main函数有两个部分,分别是:读取5个矩阵并判断、生成一个矩阵判断后输出到文件中。在读取时,用for循环分别将字符1到5拼入地址中,输出同理。其中在生成幻方之前,判断n的合法性。
static final int N = 200;
public static int[][] square = new int[N][N];
public static boolean[] vis = new boolean[N * N + 1];
public static void main(String[] args) throws IOException {
for (char i = '1'; i <= '5'; i++) {
System.out.println(i + " " + String.valueOf(isLegalMagicSquare("src/P1/txt/" + i + ".txt")));
}
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
while (n <= 0 || n % 2 == 0) {
System.out.println("Input Wrong");
n = sc.nextInt();
}
generateMagicSquare(n);
System.out.println("6" + " " + String.valueOf(isLegalMagicSquare("src/P1/txt/" + "6" + ".txt")));
return;
}
3.1.1 isLegalMagicSquare()
该函数要实现判断一个矩阵是否为幻方。
- 读入文件
a) 创建FileReader、BufferReader、StringBuilder对象
b) 初始化line - 逐行将字符串转换为整型矩阵存储
a) 将非空readline的字符串分割
b) 去除头尾空格后转换为数字存储到二维数组中
c) 存储时判断该数字是否出现过
i. 出现过则报错
ii. 否则用Boolean表标记
d) 判断行列长度是否相等 - 计算两条斜线的和并比较
a) 分别得出主对角线和次对角线的和
b) 比较
i. 若相等则记录,作为基准值
ii. 若不相等则报错 - 计算每条纵线和横线和和并比较
a) 分别计算第i条横线和纵线的和
b) 与基准值比较
i. 不相等则报错 - 确定是否为幻方
public static boolean isLegalMagicSquare(String fileName) throws IOException {
File file = new File(fileName);
FileReader reader = new FileReader(file);
BufferedReader bReader = new BufferedReader(reader);
StringBuilder sb = new StringBuilder();
String line = "";
int n = 0, m = 0;
Arrays.fill(vis, false);
while ((line = bReader.readLine()) != null) {
String[] l = line.split("\t");
m = l.length;
for (int i = 0; i < m; i++) {
square[n][i] = Integer.valueOf(l[i].trim());
if (square[n][i] <= 0 || vis[square[n][i]])
return false;
else
vis[square[n][i]] = true;
}
n++;
}
bReader.close();
if (n != m)
return false;
int s1 = 0, s2 = 0, s = 0;
for (int i = 0; i < n; i++) {
s1 += square[i][i];
s2 += square[n - i - 1][i];
}
if (s1 == s2)
s = s1;
else
return false;
for (int i = 0; i < n; i++) {
s1 = s2 = 0;
for (int j = 0; j < n; j++) {
s1 += square[i][j];
s2 += square[j][i];
}
if (s1 != s || s2 != s)
return false;
}
return true;
}
3.1.2 generateMagicSquare()
该函数要实现生成一个边长为奇数的幻方。
- 初始化
i. 生成空矩阵
ii. Row=0,Col=n/2 - 循环n*n次填充矩阵
i. 将矩阵的[row, col]位置填充为i
ii. 在保证坐标在矩阵范围内的情况下使Row–,Col++ - 打开文件,打印结果
public static boolean generateMagicSquare(int n) throws IOException {
int magic[][] = new int[n][n];
int row = 0, col = n / 2, i, j, square = n * n;
for (i = 1; i <= square; i++) {
magic[row][col] = i;
if (i % n == 0)
row++;
else {
if (row == 0)
row = n - 1;
else
row--;
if (col == (n - 1))
col = 0;
else
col++;
}
}
File file = new File("src/P1/txt/6.txt");
PrintWriter output = new PrintWriter(file);
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++)
output.print(magic[i][j] + "\t");
output.println();
}
output.close();
return true;
}
3.2 Turtle Graphics
- 该任务需要我们clone已有的程序后,利用turtle按照要求画图,其中需要利用几何知识设计一些函数简化编程,最后可以发挥想象力进行Personal Art。首先分析turtle的package组成,了解类成员。
3.2.1 Problem 1: Clone and import
- 打开目标存储文件夹 右键点击Git Bash
- 输入git clone https://github.com/ComputerScienceHIT/Lab1-1183710109.git
3.2.2 Problem 3: Turtle graphics and drawSquare
- 该函数需要实现:已知边长,画出边长为指定数值的正方形。参数是海龟对象turtle和编程sidelength。
- 首先将海龟画笔设置为黑色。然后执行4次的前进sidelength长度、转完90度,即可完成一个边长为sidelength的正方形。下图是边长为200的正方形:
public static void drawSquare(Turtle turtle, int sideLength) {
turtle.color(PenColor.BLACK);
for (int i = 0; i < 4; i++) {
turtle.forward(sideLength);
turtle.turn(90);
}
}
3.2.3 Problem 5: Drawing polygons
- 该问题首先希望已知正多边形边数的情况下计算正多边形的内角度。根据几何知识可以推导得公式:
- 使用该公式,实现calculateRegularPolygonAngle,通过运行TurtleSoupTest中的Junit测试得:
public static double calculateRegularPolygonAngle(int sides) {
return (double) 180.0 - (double) 360.0 / sides;
}
- 该问题还希望已知正多变型得边数和边长画出一个正多边形。参照画正方形的方法,可以先前进sidelength,再使海龟旋转一个角度,执行“边数”次。其中,这个角度是正多边形内角的补角,利用calculateRegularPolygonAngle的功能计算出多边形内角,再用180°减去这个值即可。边长为100的正六边形效果如下:
public static void drawRegularPolygon(Turtle turtle, int sides, int sideLength) {
turtle.color(PenColor.BLACK);
for (int i = 0; i < sides; i++) {
turtle.forward(sideLength);
turtle.turn((double) 180.0 - calculateRegularPolygonAngle(sides));
}
}
3.2.4 Problem 6: Calculating Bearings
该问题首先希望解决,已知起点和当前朝向角度,想知道到终点需要转动的角度。例如,如果海龟在(0,1)朝向 30 度,并且必须到达(0,0)它必须再转动 150 度。
- 首先使用Math.atan2函数计算两点之间的边在坐标系的角度,减去当前朝向的角度;
- 然后取相反数(海龟旋转的方向是顺时针,坐标轴角度的旋转角度的逆时针);
- 再减去90°(海龟的0°线是向上,坐标轴的0°线是向右,向右到向上要逆时针旋转90°);
- 最后调整为0-360°之间(可能大于360°或小于0°)。
public static double calculateBearingToPoint(double currentBearing, int currentX, int currentY, int targetX,
int targetY) {
double angle = Math.atan2(targetY - currentY, targetX - currentX) * 180.0 / Math.PI;
if (angle < 0)
angle += 360.0;
double bearing = (360 - angle + 90 >= 360 ? 90 - angle : 360 - angle + 90) - currentBearing;
return bearing < 0 ? 360.0 + bearing : bearing;
}
- 基于上一个问题,此时有若干个点,想知道从第一个点开始到第二个点,再从第二个点到第三个点……以此类推每次转向的角度。
- 将“起点”选为第一个点(坐标为(xCoords.get(0),yCoords.get(0)));
- 循环n-1次(n为点的个数)
- 每次将第i+1号点设置为“终点”,通过上一个函数计算旋转角度并存储到List中;
- 将下一次的“起点”用当前“终点”更新,继续循环;
- 退出循环后返回List。
public static List<Double> calculateBearings(List<Integer> xCoords, List<Integer> yCoords) {
double currentBearing = 0.0;
int currentX = xCoords.get(0), currentY = yCoords.get(0), targetX, targetY;
int length = xCoords.size();
List<Double> ans = new ArrayList<>();
for (int i = 1; i < length; i++) {
targetX = xCoords.get(i);
targetY = yCoords.get(i);
ans.add(calculateBearingToPoint(currentBearing, currentX, currentY, targetX, targetY));
currentBearing = ans.get(i - 1);
currentX = targetX;
currentY = targetY;
}
return ans;
}
3.2.5 Problem 7: Convex Hulls
3.2.5.1 凸包问题
3.2.5.2 算法描述
3.2.5.3 JUnit测试结果
3.2.6 Problem 8: Personal art
3.2.7 Submitting
3.3 Social Network
- 该任务要求设计一张社交网络图,基于连接人与人,并且能计算任意两人之间的联系情况。网络图基于两个类,分别是FriendshipGraph类和Person类。
3.3.1 设计/实现FriendshipGraph类
- 该类的实际意义是一张社交网络图,包括了代表每个Person的点、代表每两个Person之间联系的边、以及建立点和联系和计算距离的方法。
3.3.1.1 邻接表存储结构
- 在存储社交网络时,我使用了邻接表。所有的Node被连在一起,方便查找,并补充了一个head变量用来标记首个Node。假定一个社交网络为
- 则该图转换为邻接表的示意图为:
3.3.1.2 Class Node
- Node类要实现的是将一个Person转换为邻接表里的点,所以一个Node有邻接表中点的重要成员变量:下一个Node为next,对应的Person对象person,直接连接的边lastEdge,以及实现邻接表的相关方法。此外,为了能实现方法getDistance,我另外增设了vis和dis两个变量用来记录是否访问过以及与当前起点的最近距离。
- Class Node.Edge
该类是邻接表中的边,每个Edge对象存储了邻接表中的下一条边,以及对应的边的两个Person所对应的Node。 - Method Node.LoadData
该方法将Person对象导入到Node中进行存储,需要的时候可以直接调用。 - Method Node.addNode
该方法将相应的Node加入到Node的链表中(即邻接表图中的纵向链表)。 - Method Node.addNodeEdge
该方法将新的边加入到对应的Node中,更新每个Node后的Edge链表。
/**
* @Class Node : A Node is linked with a particular Person based on graph
* theory. TODO Build nodes and Linking persons
* @param next the person joins in the graph after him/her
* @param person class Person matched with
* @param vis visited mark for BFS
* @param dis lowest distance from a chose person
* @param lastEdge first edge in First-Search
* @method LoadData record which person THIS belongs to
* @method addNode add a new Node for a person
* @method addNodeEdge add a edge between 2 Nodes
*/
public class Node {
private Node next = null;
Person person;
private boolean vis;
private int dis = Integer.MAX_VALUE;
private Edge lastEdge = null;
public void LoadData(Person person) {
this.person = person;
person.node = this;
}
public void addNode(Node nextVertex) {
nextVertex.next = this.next;
this.next = nextVertex;
}
/**
* @Class Edge A edge between 2 Nodes
* @param origin one Node
* @param terminal another Node
* @param nextEdge the next edge which has the same ORIGIN with THIS
*/
public class Edge {
public Node origin = null, terminal = null;
public Edge nextEdge = null;
}
public void addNodeEdge(Node toVertex) {
Edge newEdge = new Edge();
newEdge.origin = this;
newEdge.terminal = toVertex;
newEdge.nextEdge = this.lastEdge;
this.lastEdge = newEdge;
}
}
3.3.1.3 Method addVertex()
- 该方法目标是在社交网络图中增加一个新的节点,参数是要加入的Person类。首先,方法要对Person的名字进行判重:用哈希集合HashSet记录下已加入的所有Person的名字,每当新加入一个Person则进行判断是否在集合中;然后则新建一个Node类,使每一个Person与一个Node对应起来。
/**
* @method addVertex add new vertex if the person's name is unduplicated
* @param newPerson adding person
* @param head head of Linked List of persons
* @param NameSet set of persons' names, used for removing duplication
*/
private Node head = null;
private HashSet<String> NameSet = new HashSet<>();
public void addVertex(Person newPerson) {
if (NameSet.contains(newPerson.Name)) {
System.out.println("Person " + newPerson.Name + " already existed.");
System.exit(0);
}
NameSet.add(newPerson.Name);
Node NewVertex = new Node();
newPerson.node = NewVertex;
NewVertex.LoadData(newPerson);
if (head == null)
head = NewVertex;
else
head.addNode(NewVertex);
return;
}
3.3.1.4 Method addEdge()
- 该方法目标是将两个Person之间进行联系,在邻接表中,用有向边来代表“有社交关系”,由于题目设定是社交默认为双向,则需要在函数中两次调用Node中的addNodeEdge方法加两个方向的边。考虑到可扩展性和可复用性,程序考虑到了“单向社交的情况”,仅需将双向加边中的“B->A”删除即可。
/**
* @method addEdge add edges of double directions
* @param a,b 2 persons being linking with an edge
* @param A,B 2 nodes of the 2 persons
*/
public void addEdge(Person a, Person b) {
if (a == b) {
System.out.println("They are the same one.");
System.exit(0);
}
Node A = a.node, B = b.node;
A.addNodeEdge(B);
B.addNodeEdge(A);
return;
}
3.3.1.5 Method getDistance()
- 该方法要计算任意两个Person之间的“距离”,若没有任何社交关系则输出“-1”。两个Person之间计算使用BFS,默认边权为1,则在搜索到边时加1即可,搜索到目标点退出;特殊情况根据要求输出0或-1。
/**
* @method getDistance
* @param sta path starting person
* @param end path ending person
* @return distance between 2 persons or -1 when unlinked
*/
public int getDistance(Person sta, Person end) {
if (sta == end)
return 0;
Queue<Person> qu = new LinkedList<Person>();
for (Node p = head; p != null; p = p.next) {
p.vis = false;
p.dis = 0;
}
sta.node.vis = true;
for (qu.offer(sta); !qu.isEmpty();) {
Person p = qu.poll();
for (Node.Edge e = p.node.lastEdge; e != null; e = e.nextEdge) {
if (!e.terminal.vis) {
qu.offer(e.terminal.person);
e.terminal.vis = true;
e.terminal.dis = p.node.dis + 1;
if (e.terminal.person == end)
return end.node.dis;
}
}
}
return -1;
}
3.3.2 设计/实现Person类
- 该类的目标是将每一个人对应到一个Person对象,并存储名字的信息。此外,在我的设计中,为了方便起见,我将每个Person对象在FriendshipGraph中对应的Node存储在对应Person的成员变量中。
public class Person {
public String Name;
public FriendshipGraph.Node node = null;
public Person(String PersonName) {
Name = PersonName;
}
}
3.3.3 设计/实现客户端代码main()
3.3.3.1 重复名字错误测试
/**
* TODO to test program exit when graph has the same names.
*/
@Test
void ExceptionProcess() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("a");
graph.addVertex(a);
Person b = new Person("b");
graph.addVertex(b);
// Person c = new Person("a");
// graph.addVertex(c);
}
3.3.3.2 简单图测试
/**
* Basic Network Test
*/
@Test
void GraphTest1() {
FriendshipGraph graph = new FriendshipGraph();
Person rachel = new Person("Rachel");
Person ross = new Person("Ross");
Person ben = new Person("Ben");
Person kramer = new Person("Kramer");
graph.addVertex(rachel);
graph.addVertex(ross);
graph.addVertex(ben);
graph.addVertex(kramer);
graph.addEdge(rachel, ross);
graph.addEdge(ross, rachel);
graph.addEdge(ross, ben);
graph.addEdge(ben, ross);
/*
* System.out.println(graph.getDistance(rachel, ross));// 1
* System.out.println(graph.getDistance(rachel, ben));// 2
* System.out.println(graph.getDistance(rachel, rachel));// 0
* System.out.println(graph.getDistance(rachel, kramer));// -1
*/
assertEquals(1, graph.getDistance(rachel, ross));
assertEquals(2, graph.getDistance(rachel, ben));
assertEquals(0, graph.getDistance(rachel, rachel));
assertEquals(-1, graph.getDistance(rachel, kramer));
}
3.3.3.3 复杂图测试
/**
* Further Test
*/
@Test
void GrpahTest2() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("A");
Person b = new Person("B");
Person c = new Person("C");
Person d = new Person("D");
Person e = new Person("E");
Person f = new Person("F");
Person g = new Person("G");
Person h = new Person("H");
Person i = new Person("I");
Person j = new Person("J");
graph.addVertex(a);
graph.addVertex(b);
graph.addVertex(c);
graph.addVertex(d);
graph.addVertex(e);
graph.addVertex(f);
graph.addVertex(g);
graph.addVertex(h);
graph.addVertex(i);
graph.addVertex(j);
graph.addEdge(a, b);
graph.addEdge(a, d);
graph.addEdge(b, d);
graph.addEdge(c, d);
graph.addEdge(d, e);
graph.addEdge(c, f);
graph.addEdge(e, g);
graph.addEdge(f, g);
graph.addEdge(h, i);
graph.addEdge(i, j);
assertEquals(2, graph.getDistance(a, e));
assertEquals(1, graph.getDistance(a, d));
assertEquals(3, graph.getDistance(a, g));
assertEquals(3, graph.getDistance(b, f));
assertEquals(2, graph.getDistance(d, f));
assertEquals(2, graph.getDistance(h, j));
assertEquals(0, graph.getDistance(i, i));
assertEquals(-1, graph.getDistance(d, j));
assertEquals(-1, graph.getDistance(c, i));
assertEquals(-1, graph.getDistance(f, h));
}
3.3.4 设计/实现测试用例
3.3.4.1 重复名字错误测试
- 测试当有重复名字“a”时,程序是否能终止
Person a = new Person("a");
graph.addVertex(a);
Person b = new Person("b");
graph.addVertex(b);
Person c = new Person("a");
graph.addVertex(c);
3.3.4.2 简单图测试
- 根据题目中的社交网络图:
- 分别测试:
- Rachel和Ross距离是1,Rachel和Ben距离是2
- Rachel和Rachel距离是0
- Rachel和Kramer距离是-1
3.3.4.3 复杂图测试
- 设计10个点、10条边的社交网络图:
- 分别测试:
- AE距离2,AD距离1,AG距离3,BF距离3,DF距离2,HJ距离2
- II距离0
- DJ距离-1,CI距离-1,FH距离-1
3.3.4.4 Junit测试结果
4 实验进度记录
- 略
5 实验过程中遇到的困难与解决途径
6 实验过程中收获的经验、教训、感想
- 略