实验1经验总结
本次实验总共有3个问题,作为Java初学者,遇见了许多问题,写下这篇博客总结一下做实验的经验和教训
1.Eclipse IDE的基本结构
在eclipse下基本结构如下
workplace->project->package
根据实验指导书给出的目录要求,整个实验应该算一个project,里面的3个问题应该算是3个不同的包
2.文件读写问题(P1)
在Java中文件读写有多种方式,在此列出我在实验中所使用的文件读写方式
读取文件
BufferedReader bw = null;
try{
bw = new BufferedReader(new FileReader(fileName));
}catch(Exception ex)
{
ex.printStackTrace();
}finally
{
if (bw != null) {
try {
bw.close();
} catch (IOException e1) {
}
}
写入文件
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter("src/P1/txt/6.txt");
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
Java提供了多种多样的文件读写形式,本文只列出其中一种,各种方式互有利弊,根据需求选择即可
3.Exit Fast原则(P1)
我在实验的时候遇到一个非常奇怪的问题,当五个文本同时在main函数顺序执行,会存在某个实验用例没有输出结果,但是并没有找到为什么,后来对代码进行了调整,即对不符合spec(不满足矩阵要求,没按“\t"分割等),立即退出,而不是带着这种错误继续执行,结果满足了要求
所以,我想说的是,写程序尽量追求Exit Fast的原则,而不是带着这个问题继续执行,否则后续非常容易卡bug,然后难以找到问题所在
4.MagicSquare的生成算法(P1)
实验指导书里面给了一个生成N*N的MagicSquare的算法(要求n为奇数)
代码如下
int magic[][] = new int[n][n];
int row = 0, col = n / 2, i, j, square = n * n;//将1写在第一行的中间
for (i = 1; i <= square; i++) {
magic[row][col] = i;//对幻方进行填数操作
if (i % n == 0)//如果填的数是n的倍数,下一个要填的数位置在下一行
row++;
else { //如果填的数不在第一行或者最后一列,下一个要填的数行数-1 列数+1
if (row == 0)//如果要填的数在第一行或者最后一列,下一个要填的数在最后一行或者第一列
row = n - 1;
else
row--;
if (col == (n - 1))
col = 0;
else
col++;
}
}
这种方法就是比较著名且简单的奇数阶幻方填法–罗伯法:
居上行正中央——数字 1 放在首行最中间的格子中
依次斜填切莫忘——向右上角斜行,依次填入数字
上出框界往下写——如果右上方向出了上边界,就以出框后的虚拟方格位置为基准,将数字竖直降落至底行对应的格子中
右出框时左边放——同上,向右出了边界,就以出框后的虚拟方格位置为基准,将数字平移至最左列对应的格子中
重复便在下格填——如果数字{N} 右上的格子已被其它数字占领,就将{N+1} 填写在{N}下面的格子中
右上重复一个样——如果朝右上角出界,和“重复”的情况做同样处理
实验书里提到的偶数和负数异常的问题原因如下
偶数不成立是因为, (n-1)/2不能整除
负数显然不成立
5.将多维问题转换为一维问题的思想(P1,P3)
P1和P3都存在一个问题,二维的MagicSquare和图的邻接矩阵,但是我们并不知道MagicSquare的阶数和邻接矩阵的大小,但是我们有Java自带的ArrayList数据类型,还有HashMap这种映射。
5.1对MagicSquare进行压缩(P1)
因为采用一行一行进行读取,我们可以获得矩阵的阶数
我们可以对输入的MagicSquare进行压缩,将二维数组压缩成一维数组。
对于二维数组任意元素a[x][y]
在ArrayList中的编号i即为m*x+y
这样我们就成功把二维数组压缩到了一维数组。
而且ArrayList是变长的,避免了初始化不知道N的大小的问题
(采取本方法是因为希望做到读取一行数据之后,直接写入到待处理求和ArrayList,在读取过程中初始化一个数组显然非常繁琐)
5.2图中的压缩问题(P3)
在以往的学习中,图有两种表达方式,邻接矩阵和邻接表
对于邻接表,在Java中非常重要的一个问题是,Java完全面向对象,没有结构体,所以还需要去定义类,对于P3这种非常轻量级别的问题,显然过于繁琐,但是如果用邻接矩阵,那么无法确定邻接矩阵的大小,所以我们依然可以采取编码压缩的方法
同时本问题里面边权值都为1,所以可以直接用ArrayList当作边集,本方法可以看作改良版本的邻接链表
综上,将多维问题压缩成一维问题很好的一点就是采用Java直接提供的一些数据类型去解决,这种编码的思想也是许多算法的来源,不过Java确实可以实现变长二维数组,不过过于繁琐,并不在此赘述
6.计算几何问题选编(P2)
P2的问题基本来自于计算几何中的问题,下面给出解决问题的相关公式
6.1凸正多边形内角大小与边数关系
内角=180-(360/边数)
6.2相对y轴正方向偏转角计算(Java实现)
需要用到Java.Math里面的函数,代码如下
double degree = Math.toDegrees(Math.atan2(targetY - currentY, targetX - currentX));
degree = 90 - degree - currentBearing;
if (degree < 0) {
degree += 360;
}
Java.Math里面的三角函数都是返回弧度制,需要Math.toDegrees转为角度制
6.3凸包问题(CovexHull)
凸包问题的简单概述为
在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,…Xn)的凸组合来构造.
在二维欧几里得空间中,凸包可想象为一条刚好包著所有点的橡皮圈。
用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中所有的点
最简单可行的算法为gift-wrapping algorithm 也被称为Javis March algorithm
算法的时间复杂度为O(nh),n为所有点的个数,h为凸包中的点的个数,本问题采取Javis March是因为其简单可行,而且适用于点的个数比较少的情况
代码如下
public static Set<Point> convexHull(Set<Point> points) {
if (points.size() < 3) {
return points;
}
Set<Point> covexHull = new HashSet<Point>();
List list = new ArrayList(points);
int l = 0;
for (int i = 1; i < points.size(); i++) {
if (((Point) list.get(i)).x() < ((Point) list.get(l)).x()) {
l = i;
}
}
int p = l, q;
do {
covexHull.add((Point) list.get(p));
q = (p + 1) % (points.size());
for (int i = 0; i < points.size(); i++) {
double val = (((Point) list.get(i)).y() - ((Point) list.get(p)).y())
* (((Point) list.get(q)).x() - ((Point) list.get(i)).x())
- (((Point) list.get(i)).x() - ((Point) list.get(p)).x())
* (((Point) list.get(q)).y() - ((Point) list.get(i)).y());
if (val < 0.0) {
q = i;
}
}
p = q;
} while (p != l);
return covexHull;
// throw new RuntimeException("implement me!");
}
这里面的val值是因为用计算叉积,原理如下
叉积的一个非常重要性质是可以通过它的符号判断两矢量相互之间的顺逆时针关系:
若 P × Q > 0 , 则P在Q的顺时针方向。
若 P × Q < 0 , 则P在Q的逆时针方向。
若 P × Q = 0 , 则P与Q共线,但可能同向也可能反向。
三个点A(x1,y1),B(x2,y2),C(x3,y3)
AB与AC的叉积为(x2-x1)(y3-y1)-(y2-y1)(x3-x1)
PS:给出的代码是计算叉积的负值,所以val<0的时候为逆时针
7.JUnit测试相关问题(P2,P3)
P2和P3里面都涉及JUnit测试,这一部分总结一下JUnit测试所遇到的问题与解决方案
7.1测试用例与代码互补的问题(P2)
MIT给的测试用例虽然不多,但是很有代表性和指导性
例如计算角度的部分,存在卡浮点数精度的问题,这一点在写代码的时候要注意,在保证程序正确性的前提下,要关注测试用例的期望结果是什么,尤其是涉及到浮点数精度问题时。
对于凸包问题,MIT给出的测试用例默认如果存在三点共线的情况,选取距离较远的两点加入到凸包中,如果没有注意到这一点,很容易过不去测试用例
在存在测试用例的前提下,一定要兼顾到测试用例的正确性。
(或者叫面向测试用例编程)
7.2编写JUnit测试的常用断言式(P3)
对于问题3,需要自己实现JUnit测试,需要用到非常多的Assert语句,在此列出比较常用的几条语句
assertTrue(graph.nameVertex.contains("Rachel"));
assertEquals(-1, graph.getDistance(rachel, ross));
同时上课提到关于JUnit测试顺序的问题,找到相关资料是这样的
从4.11版本开始,JUnit将默认使用一个确定的,但不可预测的顺序。执行顺序同DefaultOrderTest
7.3JUnit测试与exit(0) (P3)
对于JUnit,如果实验代码中有Exit(0),那么JUnit测试用例执行到了该语句,也会随之退出,不会再执行其他测试用例,这一点是值得注意的,所以JUnit测试不等同于Debug,在编写测试用例的时候要特别注意这一点
8.类设计的简单入门知识(P3)
在本问题中,要自己设计一个Java的类,在此给出设计的基本结构,虽然简单但是起码可以作为一个类设计的基本模板
public class Person {
//field
private String personName;
//constructor
public Person(String name) {
this.personName=name;
}
//method
public String getNameString() {
return this.personName;
}
}
这三个要素是类的三个基本组成之一,本问题设计出来的类模板可以做一个尚可的参考
9.BFS寻找最短路径的算法(P3)
平常提到最短路想到的算法可能是Dijkstra algorithm,不过对于本问题来讲,由于路的权值都为1,可以用BFS的方法去寻找最短路径,以下代码参考的算法导论(第三版)
n为顶点个数(|V|)
q为队列
egdeList存储的是编码后的值,即如果i到j有一条边,那么i*n+j被add到edgeList中
int n = nameVertex.size();
int distance[] = new int[n];
boolean visit[] = new boolean[n];
// BFS找到最短路径 当点的距离被遍历到 直接返回
q.offer(i);
// 根据编码原则,边列表的值除n是起点,%n是终点
while (!q.isEmpty()) {
head = q.poll();
for (int p = 0; p < edgeList.size(); ++p) {
if (edgeList.get(p) / n == head && visit[edgeList.get(p) % n] == false) {
visit[edgeList.get(p) % n] = true;
q.offer(edgeList.get(p) % n);
distance[edgeList.get(p) % n] = distance[head] + 1;
if (edgeList.get(p) % n == j) {
return distance[j];
}
}
}
}
// 没有被搜索到则不可达,返回-1
return -1;