软件工程实践2017第二次作业
(1)作业地址以及github地址
(2)估计将在程序的各个模块的开发上耗费的时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | |
· Estimate | · 估计这个任务需要多少时间 | 30 | |
Development | 开发 | 500 | |
· Analysis | · 需求分析 (包括学习新技术) | 180 | |
· Design Spec | · 生成设计文档 | - | - |
· Design Review | · 设计复审 (和同事审核设计文档) | - | - |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | |
· Design | · 具体设计 | 100 | |
· Coding | · 具体编码 | 60 | |
· Code Review | · 代码复审 | 30 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | |
Reporting | 报告 | 140 | |
· Test Report | · 测试报告 | 60 | |
· Size Measurement | · 计算工作量 | 20 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | |
合计 | 670 |
(3)解题思路描述
题目描述
数独是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。
数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。
心路历程
- 阶段一:一开始拿到题目时想的是将顺序的将81空填完,即每填一空用rand()函数取数,然后判断此数是否在行中、列中、宫中都唯一,不唯一则重新取数。后面写完程序运行后发现出现死循环(即停在某一空中无限循环),查看了输出结果发现此方法会导致从第二行后开始后的空有可能出现没有数可以填入的情况(无保证行、列、宫中唯一的数)。该想法就暂时搁置。
- 阶段二:后面想到了一种解法(类似魔方,不知道比喻是否恰当,后面看了其它同学博客发现他们有的也是这么想的。。。),即先给定程序一个写好的数独盘,然后通过行列变换来实现随机性,为保证首字母不变,第一行与第一列不参与行列交换,且只能每一个要交换的行、列只能与与它在同一宫中的其他行、列进行交换(ps:交换一整行、列而不是只有宫中的三个数),否则会打乱数独盘导致宫中数字不唯一。后面计算了此方法能生成的数独个数为(266)(266)=5184种,不符合题目要求的在100w数目中不出现重复。暂时搁置。
- 阶段三:参考了一下几位同学的做法,他们的方法是先生成一个宫,然后将该宫中的行或列按照一定的规则填到其它宫中去。后面觉得这种解法生成的数独盘有点太套路,故放弃。
- 阶段四:后面还思考了一些想法发现以自己目前码力实现起来比较困难,重新思考了阶段二中的想法,发现如果先交换数独中不同的两个数字,然后再去行列变换会大大提高随机性,足以满足题目要求,后面按照此思路编写代码。
(4)设计实现过程
编写了6个函数,以下为函数间依赖关系及相关说明
- main函数
- sudoku_create函数(生成一个数独盘)
- value_swap(原数独盘中两个不同值交换在数组中的位置)
- 执行一定次数(为保证随机性,要执行一定次数的行列操作)
- radom_born函数(此函数通过传入一个待交换的行列值已生成另一个待交换的的行列值)
- row_swap函数或column_swap函数(传入两个待交换行列值执行交换操作)
- result_output函数(输出函数)
- sudoku_create函数(生成一个数独盘)
(5)关键代码说明(代码中已包括相关注释)
- 列交换函数
void row_swap(int row1,int row2) {
//进行行交换
for (int j = 0; j < 9; j++) {
int temp = sudoku[row1][j];
sudoku[row1][j] = sudoku[row2][j];
sudoku[row2][j] = temp;
}
}
-
行交换函数与列类似(故未贴出)
-
值交换函数
void value_swap(int value1,int value2) {
//用随机数得所要交换的两个值
for (int i = 0; i<9; i++)
for (int j = 0; j < 9; j++) {
if (sudoku[i][j] == value1) {
sudoku[i][j] = value2;
}
else if (sudoku[i][j] == value2) {
sudoku[i][j] = value1;
}
}
}
- 此函数为已知列或行情况下,生成待交换的列或行
int random_born(int x){
int y;
int rannum = rand() % 2 + 1;
if (x == 1)//因为第0行不可交换,故只能与第二行交换,以下同理
y = 2;
else if (x == 2)
y = 1;
else {
switch (x % 3) {//得到row1在它所在宫中的第几行
case 0:
y = x + rannum; break;//在它所在宫中第一行则加上1或2得到所要交换的第二行
case 1:
if (rannum == 1) {
y = x - 1; break;//在第二行,为1则减1得到第二行
}
y = x + 1; break;//为2则加1得到第二行
case 2:
y = x - rannum;//在第三行则减去1或2的随机数得到第二行
}
}
return y;
}
- 数独生成函数
void sudoku_create() {
int value1 = rand() % 9 + 1;
int value2 = rand() % 9 + 1;
while (value1 == 7) {//所要交换的值不能为7,否则会改变首字母
value1 = rand() % 9 + 1;
}
while (value2 == 7 || value2 == value1) {//所要交换的两个值不能相同,否则无意义
value2 = rand() % 9 + 1;
}
value_swap(value1,value2);
for (int count = 0; count < 10; count++) {
int rannum = rand() % 2;//获取随机数,0对行执行操作,1对列执行操作
if (rannum == 0) {
int row1 = rand() % 8 + 1;//得到一个随机数作为要交换的行1,第0行不能作为交换行,因为交换会改变首数字
int row2 = random_born(row1);
row_swap(row1,row2);//行交换函数
}
else {
int column1 = rand() % 8 + 1;//得到一个随机数作为要交换的列1,第0列不能作为交换列,因为交换会改变首数字
int column2=random_born(column1);
column_swap(column1,column2);//列交换函数
}
}
}
- main函数
int main(int argc,char* argv[])
{
//判断输入的命令行是否符合要求
if (argc != 3 || strcmp(argv[1],"-c") != 0) {
printf("您所输入的字符串格式有误\n");
return 0;
}
//判断输入的数字是否为整数
for (int i = 0; i < strlen(argv[2]); i++)
{
if (argv[2][i]<'0' || argv[2][i]>'9') {
printf("请确保您所输入的欲生成数独个数为有效数字\n");
return 0;
}
else
continue;
}
int n=atoi(argv[2]);//将字符串转换为整数
FILE *stream;
freopen_s(&stream,"sudoku.txt", "w", stdout);//新建输出流,输出文件为当前路径下的sudoku.txt
scanf_s("%d", &n);
srand((unsigned)time(NULL));//以当前时间作为随机数初始种子
for (int i = 0; i < n; i++) {
sudoku_create();//生成数独
result_output();//打印结果
}
return 0;
}
(6)单元测试
对使用到的部分函数进行了单元测试,因为使用的为VS 2017 Community版,用了几个插件都没办法支持显示代码覆盖率,所以以下没有覆盖率贴图
以下为测试结果截图
(7)测试运行结果
- 对于不合法输入输出错误信息
- 输入正确命令后在sudoku.txt中查看结果
(8)性能分析与改进
-
对输出的性能优化
- 一开始测试数目为100w,最后跑了6分多钟,发现主要耗时是输出函数,此时输出使用了c++库函数ofstream
- 网上查找资料发现使用c++的io速度较慢,故使用c语言库函数freopen进行优化,可以看出性能优化显而易见,但仍可看出限制程序性能的主要为io操作
- 一开始测试数目为100w,最后跑了6分多钟,发现主要耗时是输出函数,此时输出使用了c++库函数ofstream
-
可对sudoku_create函数进行优化
- 可通过减小其中行列交换的次数来提高性能,但可能会降低数独生成的随机性
(9)完成项目实际所用时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 500 | 940 |
· Analysis | · 需求分析 (包括学习新技术) | 180 | 360 |
· Design Spec | · 生成设计文档 | - | - |
· Design Review | · 设计复审 (和同事审核设计文档) | - | - |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 100 | 120 |
· Coding | · 具体编码 | 60 | 120 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 300 |
Reporting | 报告 | 140 | 210 |
· Test Report | · 测试报告 | 60 | 120 |
· Size Measurement | · 计算工作量 | 20 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 670 | 1180 |
(9)完成本次作业的收获
- 点亮的技能
- git基本的使用操作(之前有使用过但只是一知半解)
- 使用VS进行单元测试
- 使用VS进行性能分析
- 感想
- 学习的过程是有趣的,但是也是不断产生问题的过程。一直反复于产生问题与查找解决问题的方法的过程中,这导致了实际使用时间与预估时间相差甚远
- 了解了单元测试的作用,我们目前写的sudoku小程序可能无法体现单元测试的强大,在一些比较大的项目中单元测试会更加有用。
- 一开始写出来的代码都是比较low的,要通过改善代码结构来使代码变得更优雅,并通过性能优化来改善代码的执行效率。