一、GitHub连接
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 60 | 30 |
Estimate | 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 240 | 300 |
Analysis | 需求分析 (包括学习新技术) | 120 | 180 |
Design Spec | 生成设计文档 | 60 | 30 |
Design Review | 设计复审 | 30 | 20 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
Design | 具体设计 | 60 | 60 |
Coding | 具体编码 | 240 | 180 |
Code Review | 代码复审 | 180 | 240 |
Test | 测试(自我测试,修改代码,提交修改) | 180 | 240 |
Reporting | 报告 | 60 | 60 |
Test Repor | 测试报告 | 20 | 20 |
Size Measurement | 计算工作量 | 10 | 10 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1370 | 1450 |
三、思路描述
一开始看到数独,并没有考虑很多,用着半生不熟的JAVA语言直接上手。因为题目要求是三到九宫格,就打算以不变应万变,从最麻烦的九宫格入手,其他的就在九宫格的基础上改改。结果一个宫格一个类,一共写了八个类!自己看着代码都头昏。但是自己思考一个快捷的算法又有点困难。于是我就想,数独算法应该比较成熟,网上或许有很多资料可以查。然鹅,数独的生成算法确实挺多,求解算法看起来很高端,模拟人类求解数独,采用区块摒除法、数组法、四角对角线、唯一矩形、全双值坟墓、单数链、异数链及其他数链的高级技巧等等。我百度完这些方法后,满脑子都是大写的懵逼。万分绝望下我拿起数据结构的书翻了翻,入眼就是“回溯法解决八皇后问题”,而里面的第一行介绍就是,可以利用回溯法解决数独问题。我仔细看完八皇后的求解,总觉得思路和一开始写的有一点点相似,都是设置一个A数组,初始化为-1,再一一检测后置0,但是到了结尾,文章提供了递归法检验数组求解是否正确的方法,我就寻思着,能不能把它改改,编写成求解数组的方法呢?于是就有了现在的程序。具体思路在代码说明部分。
再写完程序后,我又找到一个关键词,舞蹈链,据说这是个更快捷的方法求解数独,但是苦于没有时间研究,只能作罢。
四、单元测试
1、测试读取文件方法
BufferedReader br = new BufferedReader(new FileReader("D:\\test.txt"));//构造一个BufferedReader类来读取文件
String s = null;
int[][] chess = new int[9][9];
for(int k = 0; k < chess.length ;k++ )
for(int j =0; j < chess[k].length; j ++)
{
chess[k][j] = 0;
}
int i = 0;
while((s = br.readLine())!=null){//使用readLine方法,一次读一行
String[] temp = s.split(" ");
System.out.println(temp[2]);
for(int j=0;j<temp.length;j++) {
System.out.println(temp[j]);
chess[i][j]=Integer.parseInt(temp[j]);
}i++;
}
System.out.println(chess.toString());
for(int k = 0; k < chess.length ;k++ ){
for(int j =0; j < chess[k].length; j ++)
{
System.out.print(chess[k][j]+ " ");
}
System.out.println();
}
2、测试递归方法solve()
@Test
public boolean solve(int[][] chess, int i, int j, int m) { //同一行按列搜,若j=9,则跳到下一行搜,若宫格填完返回true
if (j == m) {
if (i == m - 1)
return true;
i++;
j = 0;
}
if (chess[i][j] != 0) {
return solve(chess, i, j + 1, m);
}
for (char k = 1; k <= m; k++) {
if (isValid(chess, i, j, k, m)) {
chess[i][j] = k;
if (solve(chess, i, j + 1, m))
return true;
else
chess[i][j] = 0;
}
}
return false;
}
3、测试写入文本方法printFuntxt()
@Test
private static void printFuntxt(int[][] totalchess, int n, int m, String fileout) {
File file=new File(fileout);
try (PrintWriter output = new PrintWriter(file)) {
for (int i = 0; i < m * n; i++) {
if(i % m == 0 && i != 0)
output.println();
for (int j = 0; j < m; j++) {
if (totalchess[i][j] == 0) {
System.out.println("无解");
return;
}
if (j != m-1)
output.print(totalchess[i][j] + " ");
else if(i!= m * n - 1)
output.print(totalchess[i][j] + "\n");
else
output.print(totalchess[i][j]);
}
}
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
}
测试结果
·正面测试
·边界测试
五、性能改进
虽然看不太懂性能分析图,但是还是可以看得出,开销最大的是读取文件的函数。第一个版本经过改进后,Classes的开支明显减少。
六、代码说明
主要思路流程如下:
其中最核心的就是递归和判断宫格是否可以填充。
程序使用solve()方法来进行递归。
先从第i行开始,检测第j个位置是否可以填k值,若不行,则j+1,按照j=0,1……m-1的次序。
·如果可以填,则将这个格子置为k,判断下一个格子。如果j=m-1,则跳到下一行继续判断。
·如果不可以填,则将这个位置置0。
·如果所有格子都填完了,则返回true。
public boolean solve(int[][] chess, int i, int j, int m) { //同一行按列搜,若j=9,则跳到下一行搜,若宫格填完返回true
if (j == m) {
if (i == m - 1)
return true;
i++;
j = 0;
}
if (chess[i][j] != 0) {
return solve(chess, i, j + 1, m);
}
for (char k = 1; k <= m; k++) {
if (isValid(chess, i, j, k, m)) {
chess[i][j] = k;
if (solve(chess, i, j + 1, m))
return true;
else
chess[i][j] = 0;
}
}
return false;
}
接着用isvalid()方法判断这个位置是否可以填值。
在检测第j个位置是否可以填k值时,分别按行、列、某些阶数的宫图还需要按宫进行判断,若行数不为零且已经填了k值时,则返回false,k值加1,进行下一轮的判断。
public boolean isValid(int[][] chess, int i, int j, char c,int m) { //有效空格
for (int k = 0; k < m; k++) {
if (chess[i][k] != 0 && chess[i][k] == c) //按行搜
return false;
if (chess[k][j] != 0 && chess[k][j] == c) //按列搜
return false;
if (m == 4) { //按宫搜
if (chess[i / 2 * 2 + k / 2][j / 2 * 2 + k % 2] != 0
&& chess[i / 2 * 2 + k / 2][j / 2 * 2 + k % 2] == c)
return false;
} else if(m == 6) {
if (chess[i / 2 * 2 + k / 3][j / 3 * 3 + k % 3] != 0
&& chess[i / 2 * 2 + k / 3][j / 3 * 3 + k % 3] == c)
return false;
} else if(m == 8) {
if (chess[i / 4 * 4 + k / 2][j / 2 * 2 + k % 2] != 0
&& chess[i / 4 * 4 + k / 2][j / 2 * 2 + k % 2] == c)
return false;
} else if(m == 9) {
if (chess[i / 3 * 3 + k / 3][j / 3 * 3 + k % 3] != 0
&& chess[i / 3 * 3 + k / 3][j / 3 * 3 + k % 3] == c)
return false;
}
}
return true;
}
关于文件的读取,是将文件按行读取,存入一个叫totalchess的二维数组中。
public static int[][] readFile(File file, int[][] totalchess) { //把文件读入一个大数组
String s = null;
for (int k = 0; k < totalchess.length; k++) {
for (int j = 0; j < totalchess[k].length; j++) {
totalchess[k][j] = 0;
}
}
try {
int i = 0;
BufferedReader br = new BufferedReader(new FileReader(file)); //构造一个BufferedReader类来读取文件
while ((s = br.readLine()) != null) { //使用readLine方法,一次读一行
if (s.equals(""));
else {
String[] temp = s.split(" ");
for (int j = 0; j < temp.length; j++) {
totalchess[i][j] = Integer.parseInt(temp[j]);
}
i++;
}
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
return totalchess;
}
而在solveSudoku方法中,如果solve方法返回值为true,即宫格已经填写完毕,将填写完成的chess数组存入totalchess数组中一边最后一次性写入文本。
public void solveSudoku(int[][] chess, int m, int[][]totalchess) {
if (chess == null || chess.length != m || chess[0].length != m) {
System.out.println("error!");
return;
}
if (solve(chess, 0, 0, m)) { // 打印结果
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
totalchess[y][j] = chess[i][j];
//System.out .print(totalchess[y][j]);
}
//System.out .println();
y++;
}
}
}
最后是把totalchess数组写入文本。
private static void printFuntxt(int[][] totalchess, int n, int m, String fileout) {
File file=new File(fileout);
try (PrintWriter output = new PrintWriter(file)) {
for (int i = 0; i < m * n; i++) {
if(i % m == 0 && i != 0)
output.println();
for (int j = 0; j < m; j++) {
if (totalchess[i][j] == 0) {
System.out.println("无解");
return;
}
if (j != m-1)
output.print(totalchess[i][j] + " ");
else if(i!= m * n - 1)
output.print(totalchess[i][j] + "\n");
else
output.print(totalchess[i][j]);
}
}
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
}
这与第一个版本相比,最大的优点就是能将七个阶级的宫根据if语句在一个方法中进行实现,而不需要创建七个类,代码冗长繁琐。
七、异常处理
·命令行输入格式不正确
if (args.length != 8) {
System.out.println("Invalid input.");
System.exit(0);
}
·文本输入数组越界
try {
int i = 0;
BufferedReader br = new BufferedReader(new FileReader(file)); //构造一个BufferedReader类来读取文件
while ((s = br.readLine()) != null) { //使用readLine方法,一次读一行
if (s.equals(""));
else {
String[] temp = s.split(" ");
for (int j = 0; j < temp.length; j++) {
totalchess[i][j] = Integer.parseInt(temp[j]);
}
i++;
}
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
·输入文本为空
if ((filein == null) || !filein.exists() || filein.length() == 0) {
System.out.println("输入文件不存在");
System.exit(1);
}
八、心得体会
这次作业让我学到了很多,首先是文本的读写与命令行的使用,在这块上我耗了很长的时间,其次是一些插件的使用,比如jprofile、checkstyle和juint,关于单元检测里juint的使用,还不是很熟练,但相信以后会慢慢熟悉的。在单元检测这块内容时候,也懵了很久,自己的方法都是相互调用的,怎么进行检测?在仔细看完构建之法后,终于知道单元检测就是把模块提取出来,可以自己加上输入输出语句,进行检测,然后在保证某些模块接口稳定的情况下,借助它对其他接口模块进行单元测试。总之就是,累与学无止境。