第三次
软工实践第三次作业
软件工程 | https://edu.cnblogs.com/campus/zswxy/software-engineering-2017-1 |
---|---|
作业要求 | https://edu.cnblogs.com/campus/zswxy/software-engineering-2017-1/homework/10408 |
作业目标 | 个人编程完成数独 |
作业正文 | 见下文 |
其他参考文献 | 百度,博客园,CSDN |
.Github地址
github打不开根本,挂了VPN一样打不开
1.PSP表格
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | 30 | 20 |
估计这个任务需要多少时间 | 10 | 10 |
开发 | 200 | 150 |
需求分析 (包括学习新技术) | 60 | 40 |
生成设计文档 | 60 | 60 |
设计复审 | 30 | 20 |
代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
具体设计 | 40 | 30 |
具体编码 | 120 | 180 |
代码复审 | 30 | 20 |
测试(自我测试,修改代码,提交修改) | 400 | 180 |
报告 | 60 | 30 |
测试报告 | 60 | 40 |
计算工作量 | 30 | 15 |
事后总结, 并提出过程改进计划 | 60 | 40 |
合计 | 1300 | 845 |
2.解题思路
看到题目为解数独,且最大宫数为9时我便决定用深度优先搜索直接搜索出解。
3.设计过程
由于本题只是一道算法题,于是我便决定只设计一个类来求解,该类主要由main函数、输入输出函数、判断函数、深度优先搜索函数组成。
整体思路如图
dfs函数思路
3.0输入输出函数
读入参数n,m判断有几个盘面需要输入,每个盘面输入后直接调用解数独函数求解并输出
public static void inAndOut(int n,String in,String out,int m) throws IOException {
File input = new File(in);
File output = new File(out);
BufferedReader reader = null;
BufferedWriter writer = null;
reader = new BufferedReader(new FileReader(input));
writer = new BufferedWriter(new FileWriter(output));
for(int l=0;l<n;l++){//l表示当前读到第几盘
for(int i=0;i<m;i++){
String s = reader.readLine();//读入该行
String str = s.replace(" ","");//删除空格
for(int j=0;j<m;j++){
char [] chars = str.toCharArray();//char转为int
array[i][j] = chars[j]-48;
}
}
if(dfs(0,m)){
for(int i=0;i<m;i++){
for(int j=0;j<m;j++){
writer.write(array[i][j]+" ");
}
writer.newLine();//写入新的一行
}
}else {
writer.write("no result");
}
writer.newLine();
String s = reader.readLine();
}
writer.close();
}
3.1判断函数
依据要填入的空格的位置,把即将填入的数字与该空的对应的行列和宫上的数字进行比较,如果相同则不能填入该空
private static boolean isIn(int n,int x,int y,int m){
for(int i=0;i<m;i++){
if(array[x][i]==n||array[i][y]==n)//判断与该行该列的数字是否相同
return false;
}
if(m==9) {//宫数为9时判断与该宫内的数字是否相同
for (int i = (x / 3) * 3; i < (x / 3 + 1) * 3; i++) {
for (int j = (y / 3) * 3; j < (y / 3 + 1) * 3; j++) {
if (array[i][j] == n)
return false;
}
}
}
if (m==4){//宫数为4时判断与该宫内的数字是否相同
for (int i = (x / 2) * 2; i < (x / 2 + 1) * 2; i++) {
for (int j = (y / 2) * 2; j < (y / 2 + 1) * 2; j++) {
if (array[i][j] == n)
return false;
}
}
}
if(m==6) {//宫数为6时判断与该宫内的数字是否相同
for (int i = (x / 2) * 2; i < (x / 2 + 1) * 2; i++) {
for (int j = (y / 3) * 3; j < (y / 3 + 1) * 3; j++) {
if (array[i][j] == n)
return false;
}
}
}
if(m==8) {//宫数为8时判断与该宫内的数字是否相同
for (int i = (x / 4) * 4; i < (x / 4 + 1) * 4; i++) {
for (int j = (y / 2) * 2; j < (y / 2 + 1) * 2; j++) {
if (array[i][j] == n)
return false;
}
}
}
return true;
}
3.2深度优先搜索函数
从第一行第一列的空格开始,按顺序进行搜索,如空格上以有数字,跳过该空,否则从1~9中按顺序用检验函数进行检验,如都不符合,则回到上一空,直到填满81空,结束递归。我一开始写dfs函数时定义的函数为void类型,代码如下
public static void dfs(int x,int m){
if (x == (m*m)) {//如果以填完最后一空返回
return;
}
int i = x / m;
int j = x % m;
if (array[i][j] == 0) {
for (int k = 1; k <= m; k++) {
if (isIn(n,i,j,m) {//该数字没有重复,可以填入
array[i][j] = k;
dfs(x + 1,m);//继续填下一空
}
array[i][j] = 0;//清除掉刚填过的数字,将盘面退回原样
}
} else {
dfs(x + 1,m);//继续填下一空
}
}
这段代码运行完后的输出文件与输入文件完全一样,后来我上博客查看别人的代码发现有一个逻辑和我一样但是在深度优先搜索达到最后一空时便把结果输出,可以得到正确结果,与上面的代码差异如下
public static void dfs(int k,int m,String outputFilename) throws IOException {
if (k == (m*m)) {
try{//直接将结果输出
File output = new File(outputFilename);
BufferedWriter writer = null;
writer = new BufferedWriter(new FileWriter(output));
for(int i=0;i<m;i++){
for(int j=0;j<m;j++){
writer.write(table[i][j]+" ");
}
writer.newLine();
}
writer.newLine();
writer.close();
}
catch (Exception e) {
e.printStackTrace();
}
return;
}
int x = k / m;
int y = k % m;
if (table[x][y] == 0) {
for (int i = 1; i <= m; i++) {
table[x][y] = i;
if (judge(table,x, y, i,m)) {
dfs(k + 1,m,outputFilename);
}
}
table[x][y] = 0;
} else {
dfs(k + 1,m,outputFilename);
}
}
于是我怀疑是因为没有及时输出,在到达第81空后,递归函数又一直回退到之前的状态,导致已经填好的空被消除掉,于是我增加了一个数组来存放盘面防止已填好的空被清空,代码如下
public static int [][] res = new int[9][9];
public static void dfs(int num,int m)
{
if(num==m*m){//将数组保存到另一个新数组,并返回
for(int i=0; i<m; i++){
for(int j=0; j<m; j++)
res[i][j]=array[i][j];
}
return ;
}
int i=num/m,j=num%m;
if(array[i][j]!=0) dfs(num+1,m);
else{
for(int k=1; k<=m; k++)
{
if(isIn(k,i,j,m))
{
array[i][j]=k;
dfs(num+1,m);
}
}
array[i][j]=0;
}
return ;
}
此时已经可以正确解出数独,但在我测试的时候我发现如果数独有多解且解的个数较大时,该代码需要跑很长的时间才能出结果,因为该函数会一直遍历完所有的情况才会得出结果,且需要的内存空间较大,于是我便将dfs函数改成在得出第一个解时就能直接返回的形式,代码如下
public static boolean dfs(int x,int m){
int i,j;
i = x/m;j = x%m;
if(x==m*m){//如过已填完最后一空则返回true
return true;
}
if(array[i][j]!=0) return dfs(x+1,m);//继续填下一空
for(int n=1;n<=m;n++){
if(isIn(n,i,j,m)){
array[i][j] = n;
if(dfs(x+1,m)) return true;//如果下一空结果正确则返回true;
array[i][j] = 0;//如果下一空没有可以填入的数字则应清除该空以填入的数据,填入下一个可用数字
}
}
return false;//该空找不到可以填入的数字,返回false以返回到上一空
}
将函数返回值设置成boolean便可依据返回值判断是否要回到之前的状态,在第一次达到最后一空时便可以把结果输出。
4.改进思路
主要思路都以体现在上一部分的3.2深度优先搜索函数上了。
5.单元测试设计
单元测试我主要分为三个步骤进行
第一个步骤是输入输出部分的测试,代码如下
public class IOtest{
public static int [][] array = new int[9][9];
public static void inAndOut(int n,String in,String out,int m) throws IOException {
File input = new File(in);
File output = new File(out);
BufferedReader reader = null;
BufferedWriter writer = null;
reader = new BufferedReader(new FileReader(input));
writer = new BufferedWriter(new FileWriter(output));
for(int l=0;l<n;l++){
for(int i=0;i<m;i++){
String s = reader.readLine();//整行读入
String str = s.replace(" ","");//替换空格
for(int j=0;j<m;j++){
char [] chars = str.toCharArray();//转化为char类型
array[i][j] = chars[j]-48;//转化为int类型
}
}
for(int i=0;i<m;i++){
for(int j=0;j<m;j++){
writer.write(array[i][j]+" ");
}
writer.newLine();//从新的一行开始写起
}
writer.newLine();//每个盘面间间隔一行
String s = reader.readLine();
}
writer.close();
}
public static void main(String [] args) throws IOException{
//从控制台获取参数
char[] ch = args[1].toCharArray();
int m = ch[0]-48;
char[] chars = args[3].toCharArray();
int n = chars[0] - 48;
//调用输入输出函数
inAndOut(n,args[5],args[7],m);
}
}
第二个步骤是数独求解部分的测试,由于对c++比较熟悉,我一开始是用的用c++实现算法,之后才移植为java;
#include <cstdio>
using namespace std;
int array[9][9];
bool isIn(int n,int x,int y,int m){
for(int i=0;i<m;i++){
if(array[x][i]==n||array[i][y]==n)
return false;
}
if(m==9) {
for (int i = (x / 3) * 3; i < (x / 3 + 1) * 3; i++) {
for (int j = (y / 3) * 3; j < (y / 3 + 1) * 3; j++) {
if (array[i][j] == n)
return false;
}
}
}
if (m==4){
for (int i = (x / 2) * 2; i < (x / 2 + 1) * 2; i++) {
for (int j = (y / 2) * 2; j < (y / 2 + 1) * 2; j++) {
if (array[i][j] == n)
return false;
}
}
}
if(m==6) {
for (int i = (x / 2) * 2; i < (x / 2 + 1) * 2; i++) {
for (int j = (y / 3) * 3; j < (y / 3 + 1) * 3; j++) {
if (array[i][j] == n)
return false;
}
}
}
if(m==8) {
for (int i = (x / 4) * 4; i < (x / 4 + 1) * 4; i++) {
for (int j = (y / 2) * 2; j < (y / 2 + 1) * 2; j++) {
if (array[i][j] == n)
return false;
}
}
}
return true;
}
bool dfs(int x,int m){
int i,j;
i = x/m;j = x%m;
if(x==m*m){
return true;
}
if(array[i][j]!=0) return dfs(x+1,m);
for(int n=1;n<=m;n++){
if(isIn(n,i,j,m)){
array[i][j] = n;
if(dfs(x+1,m)) return true;
array[i][j] = 0;
}
}
return false;
}
int main(){
int m = 6;//指定宫数
for(int i=0; i<m; i++)
for(int j=0; j<m; j++)
scanf("%d",&array[i][j]);
dfs(0,m);
for(int i=0; i<m; i++){
for(int j=0; j<m; j++)
printf("%d ",array[i][j]);
printf("\n");
}
return 0;
}
第三个步骤便是将之前两部分整合起来,对完整的代码进行测试。
6.Code Quality Analysis工具的分析结果
采用jprofiler分析,结果如下
占用cpu运行时间最长的为我在输入时使用过的String.replace函数,接下来就是dfs函数和判断函数,运行次数最多的为判断函数,判断函数主要是被dfs函数所调用,如果想进行优化的话最好还是对dfs函数进行优化。
下面试代码规范分析的截图,主要问题在于一些变量名和定义变量时不够规范
7.心路历程与收获
本次作业还是比较简单的,对我来说主要难点就是深度优先搜索函数的设计,本次作业加深了我对深度优先搜索和递归的理解,但对于如何构建软件并没有很大的帮助。
自评表
学号 | 姓名 | 作业头 | GitHub项目地址 | 代码要求经过凑得工具的分析消除警告 | PSP表格 | 解题思路描述 | 代码如何组织 | 关键函数流程图 | 单元测试的设计 | 找出性能瓶颈 | 改进 | 显示关键代码 | 解释思路与注释说明 | 心路历程和感想 | 总分 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2分 | 1分 | 2分 | 1分 | 1分 | 0.5分 | 1分 | 0.5分 | 0.5分 | 0.5分 | 0.5分 | 0.5分 | 1分 | 12分 | ||
20177599 | 胡宇 | 2 | 0 | 1 | 1 | 1 | 0.2 | 0.5 | 0.2 | 0.2 | 0.2 | 0.5 | 0.2 | 1 | 8 |