3.2.1 Problem 1: Clone and import
3.2.2 Problem 3: Turtle graphics and drawSquare
3.2.3 Problem 5: Drawing polygons
本次实验通过求解三个问题,训练基本 Java编程技能,能够利用Java OO开发基本的功能模块,能够阅读理解已有代码框架并根据功能需求补全代码,能够为所开发的代码编写基本的测试程序并完成测试,初步保证所开发代码的正确性。另一方面,利用Git作为代码配置管理的工具,学会Git的基本使用方法。
理解:1、幻方是一个有n*n个不同数字、且每行、每列和斜线上都有相同的和的方形结构。要求写出程序判断输入一个矩阵是否是幻方,并且构造幻方。
2、主函数首先要判断5个txt文件中的矩阵是否为幻方(需要读取各个文件中的数据,然后进行识别),然后要运行函数构造一个幻方存入6.txt文件之中。
过程:设计思路:
- 首先要读取矩阵,要考虑如何设计矩阵的变量类型,以及读取矩阵中可能发生的一些错误,需要识别并抛出。
- 判断读取的矩阵是否符合方形矩阵的定义,即行数和列数是否相等、每一行的数据数是否相等,若不满足,返回false。
- 判断读取的数是否为正数字,通过和0比较判断正负,通过正则表达式判断是否为数字(不是则由题意说明分隔符不为\t),通过浮点型和整型之间的类型转换判断是否有小数,以上有一个不满足,返回false
- 判断各行、各列、主次对角线的数字和是否相同,若不同,返回false。
- 若以上判断都通过,判定矩阵为幻方,返回true。
过程:按思路构造对应函数。
关键代码:
//设定矩阵阶数最大值N
final static int N = 400;
//主函数
public static void main(String[] args) throws IOException {
for (int i = 1; i <= 5; i++) {
if (isLegalMagicSquare(i + ".txt")) {
System.out.println(i + ".txt 中存的数据是幻方");
} else {
System.out.println("因此" + i + ".txt 中存的数据不是幻方");
}
System.out.println("");
}
int n;
Scanner sr = new Scanner(System.in);
n = sr.nextInt();
boolean flag = generateMagicSquare(n);
if(flag)
{
System.out.println("6.txt幻方写入成功");
}
else{
System.out.println("6.txt幻方写入失败");
}
}
//判断是否是幻方函数
static boolean isLegalMagicSquare(String fileName) throws FileNotFoundException {
File f = new File("src\\P1\\txt\\" + fileName);
BufferedReader br = new BufferedReader(new FileReader(f));
String[][] line = new String[N][N]; //用来存储读取的每一行
int len = 0; //记录读取到的幻方行数
int[] wid = new int[N]; //记录读取到的幻方每行的列数
String str = null; //用来临时存储读取到的行
String[][] square = new String[N][N];//创建二维字符数组装矩阵
int[][] int_square = new int[N][N];//创建二维整型数组装矩阵
try {
while ((str = br.readLine()) != null) {
line[len] = str.split("\t");
wid[len] = line[len].length;
square[len] = line[len];
len++;
}
} catch (IOException e) {
System.out.println("读取阻塞");
} finally {
try {
br.close();
} catch (IOException e) {
System.out.println("文件关闭失败");
}
}
//判断行列数是否满足方型矩阵定义
int tmp = wid[0];
for (int i = 1; i < len; i++) {
if (tmp != wid[i]) {
System.out.println("输入的数据每行的个数不相等,不是矩阵");
return false;
}
}
if (tmp != len) {
System.out.println("输入的矩阵行列数不相等");
return false;
}
//判断输入的每个数是否是正整数
float z = 0;
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
if (!isNumber(square[i][j])) {
System.out.println("数字之间并非使用\\t 分割");
return false;
}
z = Float.parseFloat(square[i][j]);
if (z <= 0.0) {
System.out.println("输入的数据含非正整数");
return false;
} else if ((float) ((int) z) != z) {
System.out.println("输入的数据有小数");
return false;
}
int_square[i][j] = Integer.valueOf(square[i][j]);
}
}
int add = 0; //保存第一行数字的和
for (int i = 0; i < len; i++) {
add += int_square[0][i];
}
//判断每行数之和是否相等
tmp = 0; //保存各行的和
for (int i = 1; i < len; i++) {
for (int j = 0; j < len; j++) {
tmp += int_square[i][j];
}
if (add != tmp) {
System.out.println("存在某行的数字之和与其他不等");
return false;
}
tmp = 0;
}
//判断每列数之和是否相等
tmp = 0; //保存各列的和
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
tmp += int_square[j][i];
}
if (add != tmp) {
System.out.println("存在某列的数字之和与其他不等");
return false;
}
tmp = 0;
}
//判断主对角线之和是否相等
tmp = 0; //保存主对角线的和
for (int i = 0; i < len; i++) {
tmp += int_square[i][i];
}
if (add != tmp) {
System.out.println("主对角线的数字之和与其他不等");
return false;
}
//判断次对角线之和是否相等
tmp = 0; //保存次对角线的和
for (int i = 0; i < len; i++) {
tmp += int_square[i][len - i - 1];
}
if (add != tmp) {
System.out.println("次对角线的数字之和与其他不等");
return false;
}
return true;
}
//正则表达式判断字符串是否为数字
public static boolean isNumber(String string) {
if (string == null)
return false;
Pattern pattern = Pattern.compile("^(\\-|\\+)?\\d+(\\.\\d+)?$");
return pattern.matcher(string).matches();
}
函数创建幻方原理:
1、首先创建一个n*n的方阵空间。
2、把“1”放在中间一列最上边的方格中,从它开始,按对角线方向(从左下往右上)顺次把由小到大的各数放入方格中。
3、如果碰到顶,则折向底,如果到达右侧,则折向左边。
4、如果进行中轮到方格中已经有数或者到达右上角则退至前一格的下方。
当n*n个数均填完时,幻方创建完毕。
思路:
- 将函数写入文件中,并在主函数中完成相关调用
- 调用函数运行程序,并查看偶数、负数的情况,并分析原因
3、扩展函数:
1)首先完成文件写入操作,通过字符缓冲输出流将矩阵写入文件
2)完成对输入偶数、负数情况的识别,使程序能够在该情况下优雅退出
代码:
public static boolean generateMagicSquare(int n) throws IOException {
if(n <= 0)
{
System.out.println("输入的数字不是正整数,不合法");
return false;
}
else if(n%2 == 0)
{
System.out.println("输入的数字不是奇数,不合法");
return false;
}
int magic[][] = new int[n][n]; //初始化二维数组保存矩阵值
int row = 0, col = n / 2, i, j, square = n * n;//从第一行的中央开始填数
//连摆法构造幻方,循环填入n*n个数,填完后幻方构造完成
for (i = 1; i <= square; i++) {
magic[row][col] = i;
if (i % n == 0) //数整除n时,下移一行
row++;
else {
if (row == 0)//数填到了最上面第一行时,返回到最下面一行开始填数
row = n - 1;
else //否则,上移一行
row--;
if (col == (n - 1)) //数填到了最右边一列时,返回到最左边开始填数
col = 0;
else//否则,右移一列
col++;
}
}
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++)
System.out.print(magic[i][j] + "\t");
System.out.println();
}
File f = new File("src\\P1\\txt\\6.txt"); //创建文件对象
BufferedWriter bw = new BufferedWriter(new FileWriter(f)); //创建字符缓冲输出流
//通过循环将矩阵写入文件
for (i = 0; i < n; i++) {
for ( j = 0; j < n; j++) {
bw.write(magic[i][j] + "\t");
bw.flush(); //清空缓冲区
}
bw.newLine(); //换行
}
bw.close();//关闭字符缓冲输出流
return true;
}
分析:
- 输入偶数出现异常:
异常意义:数组下标越界异常
出现原因:输入的数是偶数时,由于下面一行必有一个数可以整除n,根据代码会使坐标下移一行,导致数组越界。
- 输入负数出现异常:
异常意义:数组长度异常
出现原因:输入的n是作为定义数组时数组的行数和列数的,由于n为负数,而数组的长度不能为负数,因此出现异常。
该实验要求我们先从对应的github上把代码clone下来,然后完成TurtleSoup中的各个方法,并运用该方法完成图形的绘制,并且最后完成个人艺术绘图。
1、在对应的文件夹内右键打开 Git Bash
2、通过输入指令 git clone https://github.com/ForestBagpipes/Spring2022_HITCS_SC_Lab1.git 从而将代码克隆到当前的文件夹内
然后将需要的文件移动到P2即可
首先:由于Turtle被放入了P2包内部,所以在文件开头:package turtle 要改为:
package P2.turtle。
然后是画一个正方形:实现只需要每次走sidelength长,右旋90°,完成4次即可。
代码:
//根据边长画一个正方形
public static void drawSquare(P2.turtle.Turtle turtle, int sideLength) {
for (int i = 0; i < 4; i++) {
turtle.forward(sideLength);
turtle.turn(90);
}
}
Problem 5: Drawing polygons
画正多边形需要完成3个函数内容,并通过JUnit测试。
分别为:
calculateRegularPolygonAngle:根据多边形边数计算内角,实现只需运用几何知识简单计算即可。
calculatePolygonSidesFromAngle:根据多边形内角计算多边形边数,实现需要运用几何知识,由于涉及除法,需要进行四舍五入,并且需要转换结果为整型输出。
drawRegularPolygon:画正多边形,原理同画正方形,需要调用calculateRegularPolygonAngle,实现简单。
代码:
//已知边数,返回多边形每个角的度数
public static double calculateRegularPolygonAngle(int sides) {
return (double) 180.0 * (1.0 - 2.0 / sides);
}
//已知多边形的一个内角度数,求多边形的边数
public static int calculatePolygonSidesFromAngle(double angle) {
return (int) Math.round(360 / ((double) 180.0 - angle)); //调用函数进行四舍五入
}
//根据给出的边数和边长,从(0,0)出发画一个正多边形
public static void drawRegularPolygon(P2.turtle.Turtle turtle, int sides, int sideLength) {
for (int i = 0; i < sides; i++) {
turtle.forward(sideLength);
double ang = calculateRegularPolygonAngle(sides);
turtle.turn(180.0 - ang);
}
}
实现方法calculateBearingToPoint和calculateBearings,并通过JUnit测试。
public static double calculateBearingToPoint(double currentBearing, int currentX, int currentY,
int targetX, int targetY) {
double x = targetX - currentX; //相对横坐标长度
double y = targetY - currentY; //相对纵坐标长度
double len = Math.sqrt(x * x + y * y); //求出当前点和目标点之间的距离
//相同点,则不用旋转角度
if (len == 0) {
return 0;
}
double bearingAngle = 180.0 * Math.atan2(y, x) / Math.PI; //求出两点形成的直线与水平正半轴之间的夹角
if (bearingAngle >= -180.0 && bearingAngle <= 90.0) {
bearingAngle = 90.0 - bearingAngle;
} else {
bearingAngle = 450.0 - bearingAngle;
}
bearingAngle = bearingAngle - currentBearing;
if (bearingAngle < 0)//度数区间必须在0到360直接,因此小于0的度数要加360以此满足需求
{
bearingAngle = bearingAngle + 360;
}
return bearingAngle;
}
//根据得到的一系列x,y坐标,算出每次的偏转角并保存到集合中返回
public static List<Double> calculateBearings(List<Integer> xCoords, List<Integer> yCoords) {
List<Double> ang = new ArrayList<Double>();
int currentX = xCoords.get(0);
int currentY = yCoords.get(0);
int targetX = 0;
int targetY = 0;
double bearingAngle = 0;
for (int i = 1; i < xCoords.size(); i++) {
targetX = xCoords.get(i);
targetY = yCoords.get(i);
bearingAngle = calculateBearingToPoint(bearingAngle, currentX, currentY, targetX, targetY);
ang.add(bearingAngle);
currentX = targetX;
currentY = targetY;
}
return ang;
}
思路:
calculateBearingToPoint:首先根据点间距离是否为0判断两点是不是同一个,然后通过几何知识,调用Math.atan2()方法求出两点形成的直线与水平正半轴之间的夹角,从而得到其和竖直正半轴直接的夹角。以此角减去当前的夹角,若为负数加上360度,最终得到结果。
calculateBearings:根据得到的一系列x,y坐标,算出每次的偏转角并保存到集合中返回。
求解凸包问题,并通过Junit测试
思路:首先找到整个点集中位于最左下角的点,即x最小的点中,y也最小的点,作为P1,然后遍历点击,找到右旋角度最小的点P2,P2显然在凸包上,然后从P2出发再依次找下去,直到最后,就能得到整个凸包。
关键代码:
//凸包问题
public static Set<Point> convexHull(Set<Point> points) {
//当点个数小于等于三个,大于等于0个的时候,该点集即凸包
if (points.size() <= 3 && points.size() >= 0) {
return points;
}
Set<Point> resultPoints = new HashSet<Point>();//保存凸包上的点集
Point p = new Point(Double.MAX_VALUE, Double.MAX_VALUE); //用于保存左下角的点
for (Point i : points) {
if (i.x() < p.x() || (i.x() == p.x()) && i.y() < p.y()) {
p = i;
}
}
Point curPoint = p; //保存当前点
Point minPoint = null;//保存右旋角最小的点
double x1 = 0.0, y1 = 1.0; //y轴正方向向量
do {
resultPoints.add(curPoint);
Double minAngle = Double.MAX_VALUE, x2 = 0.0, y2 = 0.0;
for (Point i : points) {
if ((!resultPoints.contains(i) || i == p) ) {
double x3 = i.x() - curPoint.x(), y3 = i.y() - curPoint.y(); //当前凸包点到点i的向量坐标
double angle = Math.acos((x1 * x3 + y1 * y3) / (Math.sqrt(x1 * x1 + y1 * y1) * Math.sqrt(x3 * x3 + y3 * y3)));//用向量积的方式求夹角
//当右旋角度相同时,选择距离较远的点
if (angle < minAngle || (angle == minAngle && x3 * x3 + y3 * y3 > Math.pow(i.x() - minPoint.x(), 2) + Math.pow(i.y() - minPoint.y(), 2))) {
minPoint = i;
minAngle = angle;
x2 = x3;
y2 = y3;
}
}
}
x1 = x2;
y1 = y2;
curPoint = minPoint;
} while (curPoint != p); //结束条件为当前的点和最初的点p共用同一个地址
return resultPoints;
}
方法:旋转50次,每次画一个正五边形,边长每次加5,每次旋转角度为7.2°,奇数次颜色为红,偶数次颜色为蓝色,即可。
public static void drawPersonalArt(P2.turtle.Turtle turtle) {
int len = 10;
for (int i = 0; i < 50; i++) {
if(i%2==0){
turtle.color(PenColor.BLUE);
}
else {
turtle.color(PenColor.RED);
}
for (int j = 0; j < 5; j++) {
drawRegularPolygon(turtle,5,len);
}
len += 5;
turtle.turn(360.0/50.0);
}
}
在该文件夹下打开GitBash,使用git add指令,git commit指令添加到本地,最后使用git push<仓库地址> master 将当前的文件上传到远程仓库里去。
要求我们设计一个社交网络,网络为无向图,可以连接人与人,能够完成添加人结点,添加连接,计算两个人之间的最短社交距离,判断人是否重名等功能。
-
-
- 设计/实现FriendshipGraph类
-
思路:用HashMap的形式保存图,键为Person ,值为HashSet(Person)。
addVertex: 向HashMap中添加键为Person,值为空HashSet的元素
addEdge: 两个人向对方键的集合中添加自己。
getDistance:运用广度优先搜索,遍历点集即可。
关键代码:
public class FriendshipGraph {
private HashMap<Person, HashSet<Person>> friendship; //用HashMap保存关系网
public FriendshipGraph() {
friendship = new HashMap<Person, HashSet<Person>>();
}
public HashMap<Person, HashSet<Person>> getFriend() {
HashMap<Person, HashSet<Person>> f = friendship;
return f;
}
public void addVertex(Person p) {
if (!friendship.containsKey(p)) {
friendship.put(p, new HashSet<Person>());
} else {
System.out.println("该人物在关系网中已经存在");
System.exit(0);
}
}
public void addEdge(Person A, Person B) {
if (!friendship.containsKey(A)) {
System.out.println("该人物" + A + "还未加入社交网络");
System.exit(0);
return;
}
if (!friendship.containsKey(B)) {
System.out.println("该人物" + B + "还未加入社交网络");
System.exit(0);
return;
}
friendship.get(A).add(B);
}
public int getDistance(Person p1, Person p2) {
if (!friendship.containsKey(p1)) {
System.out.println("该人物" + p1 + "还未加入社交网络");
System.exit(0);
return -1;
}
if (!friendship.containsKey(p2)) {
System.out.println("该人物" + p2 + "还未加入社交网络");
System.exit(0);
return -2;
}
int distance = 0; //距离
HashSet<Person> visited = new HashSet<>(); //记录访问过的顶点
HashMap<Integer, HashSet<Person>> map = new HashMap<>(); //记录距离p1一定距离的顶点集
map.put(0, new HashSet<Person>());
map.get(0).add(p1);
do {
if (map.get(distance).contains(p2)) {
return distance;
} else {
visited.addAll(map.get(distance));
map.put(distance + 1, new HashSet<Person>());
for (Person i : map.get(distance)) {
if (friendship.get(i).size() != 0) {
for (Person p : friendship.get(i)) {
if (!visited.contains(p)) {
map.get(distance + 1).add(p);
}
}
}
}
}
distance++;
} while (map.get(distance).size() != 0);
return -1;
}
public static void main(String[] args) {
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));
//should print 1
System.out.println(graph.getDistance(rachel, ben));
//should print 2
System.out.println(graph.getDistance(rachel, rachel));
//should print 0
System.out.println(graph.getDistance(rachel, kramer));
//should print -1
}
}
-
-
- 设计/实现Person类
-
思路:Person类私有属性只有Name。其他只要添加类中的构造方法即可。
package P3;
public class Person {
private String Name;
public Person(String Name) {
this.Name = Name;
}
public String getName() {
return Name;
}
}
-
-
- 设计/实现客户端代码main()
-
思路:添加人名、人与人之间的关系,并调用方法计算距离,并与预期进行对比,看结果是否相同,主函数代码已经提供。
如果将主函数代码的第 10 行注释掉,我的主程序第14-17行应该分别返回:-1,-1,0,-1。
如果将主程序第 3 行引号中的“Ross”替换为“Rachel”,我的程序会在终端中打印“”该人物在关系网中已经存在”,并结束程序。
实现思路:
addVertex:向关系网中添加节点,检测是否添加成功
addEdge:给其中两人添加关系,然后判断其中一个的朋友列表中的最后一人是否是另外一个人。
getDistance:设计测试用例检测人与人之间的距离与期望是否相等。
package P3;
import junit.framework.TestCase;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
public class FriendshipGraphTest {
@Test
public void addVertex() {
FriendshipGraph graph = new FriendshipGraph();
Person rachel = new Person("Rachel");
Person ross = new Person("Ross");
graph.addVertex(rachel);
assertEquals(true,graph.getFriend().containsKey(rachel));
assertEquals(false,graph.getFriend().containsKey(ross));
}
@Test
public void addEdge() {
FriendshipGraph graph = new FriendshipGraph();
Person rachel = new Person("Rachel");
Person ross = new Person("Ross");
graph.addVertex(rachel);
graph.addVertex(ross);
graph.addEdge(rachel, ross);
graph.addEdge(ross, rachel);
assertEquals(true,graph.getFriend().get(rachel).contains(ross));
assertEquals(true,graph.getFriend().get(ross).contains(rachel));
}
@Test
public void getDistance() {
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);
assertEquals(1,graph.getDistance(rachel, ross));
assertEquals(2,graph.getDistance(rachel, ben));
assertEquals(-1,graph.getDistance(ben, kramer));
assertEquals(0,graph.getDistance(rachel, rachel));
}
}
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 任务 | 实际完成情况 |
2023-03-07 | 20:00-22:00 | 编写问题1的isLegalMagicSquare函数并进行测试 | 按计划完成 |
2023-03-08 | 19:00-22:00 | 编写问题1的generateMagicSquare()并完成问题1的相关报告 | 按计划完成 |
2023-03-09 | 20:00-22:00 | 完成问题2的克隆代码环节,补全TurtleSoup.java中从drawSquare到calculateBearings的函数的代码 | 按计划完成 |
2023-03-13 | 20:00-22:00 | 完成问题2到calculateBearings的相关报告和代码的junit测试,对凸包问题代码进行编写 | 按计划完成 |
2023-3-14 | 20:20-22:13 | 完成问题3的Friendshipgragh的编写以及class的编写 | 按计划完成 |
2023-3-15 | 19:30-21:30 | 完成问题3的test文件并完成报告 | 按计划完成 |
遇到的困难 | 解决途径 |
计算凸包问题,没有接触过 | 上网查找资料,借鉴思路,看视频,看各种解决方法并总结 |
社交关系网络不知道用什么数据结构保存 | 上网查找资料,询问同学,看教学视频 |
Java的一些语法操作、git操作不会 | 上网观看教学视频,去csdn等论坛看博客 |
经验教训:通过这次实验,我发现我在真正动手写代码的时候,还是十分生疏,对于编译环境的使用也很不熟悉,在进行种种操作时往往要从网上进行搜索,严重降低了自己的任务完成的速度。还有在拿到代码题目的时候,往往没有什么思路,手足无措。
是。优势:相比我熟悉的C语言,java的语言十分的简洁优美,能更加直观地让人看懂。不足:Java程序的运行依赖于 Java虚拟机,所以相对于其他语言(比如C)编写的程序慢。
- 关于Eclipse或IntelliJ IDEA,它们作为IDE的优势和不足;
优势:IDEA的功能十分强大,操作方便,有许许多多的快捷键和扩展功能能让你轻松实现代码。不足:强大的功能性容易造成巨大的依赖性,让人脱离它之后写Java代码变得困难。
- 关于Git和GitHub,是否感受到了它在版本控制方面的价值;
是。Git和GitHub 在版本控制和开发方面十分方便,能更好地让我们控制、保存电脑上的文件和完成文件的开发
- 关于CMU和MIT的作业,你有何感受;
他们的作业考虑到了程序开发的方方面面,注重对一个人的全方面培养。需要的知识面十分广,对人的锻炼作用巨大。
- 关于本实验的工作量、难度、deadline;
本实验的工作量较大,难度适中,deadline较紧
- 关于初接触“软件构造”课程;
软件构造相对于其他课对于实践更加重视,在理论之外注重我们的编程能力的培养,能够潜移默化地培养一个人的思维方式和习惯,影响深远,作用巨大。