文章目录
-
-
- 一、项目地址
- 二、各模块开发时间预估
- 三、学习过程、解题思路
-
- 3.1 开发语言及运行环境
- 3.2 项目要求分析
-
-
- 3.2.1 需求建模
- 3.2.2 数据流设计方法
-
- 3.3 解题思路
-
-
- 3.3.1 指令校验模块
- 3.3.2 生成数独终局(组)模块
- 3.3.3 残局校验模块
- 3.3.4 求解数独残局(组)模块
-
- 四、设计实现过程
-
- 4.1 程序流程图
- 4.2 主要函数接口设计
- 4.3 各函数之间的关系
- 五、程序性能分析及改进(测试均为1e6数据规模)
-
- 5.1数独终局生成模块
- 5.2 数独残局求解模块
- 六、代码说明
-
- 6.1 数独终局生成模块
-
-
- 6.1.1 首行全排列模块
- 6.1.2 数独终局生成模块函数
-
- 6.2 数独残局求解模块
- 七、单元测试
-
- 7.1 指令校验模块
- 7.2 求解数独残局中的DFS模块
- 八、各模块实际开发时间及与预期对照
- 九、个人总结
-
- 9.1 个人能力的提升
-
-
- 9.1.1 培养结构化设计程序的思维
- 9.1.2 掌握更高效的编程技巧
- 9.1.3 模仿与自学能力
-
- 9.2 不足之处
-
一、项目地址
github地址:https://github.com/ZJT1024/Sudoku
二、各模块开发时间预估
注:实际耗时在结尾处给出。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 15 |
Estimatie | 估计这个任务需要多少时间 | 20 |
Development | 开发 | 240 |
Analysis | 需求分析(包括学习新技术) | 30 |
Design Spec | 生成设计文档 | 60 |
Design Review | 实际复审(和同事审核设计文档) | 120 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 60 |
Design | 具体设计 | 90 |
Coding | 具体编码 | 360 |
Code Review | 代码复审 | 90 |
Test | 测试(自我测试,修改代码,提交修改) | 300 |
Reporting | 报告 | 90 |
Test Report | 测试报告 | 20 |
Size Measurement | 计算工作量 | 60 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程修改计划 | 30 |
合计 | 1585 |
三、学习过程、解题思路
3.1 开发语言及运行环境
考虑到不同语言的程序运行速度问题,根据题目要求及个人对所要求语言的熟悉程度,本次项目采用C++进行开发,运行环境为64bit Windows10。
3.2 项目要求分析
该项目的主要目的是实现一个能够生成数独终局并能求解数独的控制台程序,此外项目还需包括代码分析、性能测试。由于本次项目需要按照软件工程开发的一般流程进行,所以,除了核心代码之外,代码分析和性能测试就尤为重要。由于该项目具有一定特殊性,即项目需求明确且固定,为方便之后进行单元测试,项目选用增量模型进行开发,每个需求之间采用瀑布模型,设计方法采用结构化的设计方法,尽量做到函数模块之间高内聚低耦合。
3.2.1 需求建模
由题意可知,程序需要能够判断用户输入命令,对于不同命令执行不同子程序,并给出反馈,其中,子程序包括生成数独终局模块和求解数独残局模块。
-
数据建模——ER图描述
通过对题目进行分析,进筛选得到如下实体和实体属性:
指令:输入文件名*、操作指令*、参数*
文件:文件名*、输入输出类型*
数独局:行数据、列数据、宫数据
数独终局生成及残局求解 - 功能建模——数据流图(DFD)
-
- 顶层图(第0层图): 项目主要功能是根据用户提供的合法指令完成数独终局的生成或数独残局的求解,将结果写入指定文件并能在程序出现异常时给用户相应的反馈信息。
数独终局生成及残局求解
- 顶层图(第0层图):
-
- 一层图: 整个项目采用结构化设计,将主要过程进行模块化封装,做到高内聚低耦合。通过对题目的分析,本次项目开发将大致分为四部分,分别为:指令校验模块、生成数独终局(组)模块、残局校验模块、求解数独残局(组)模块。其中,生成数独终局(组)对应数独终局生成功能,残局校验模块和求解数独残局模块对应残局求解功能。
数独终局生成及残局求解
- 一层图:
-
-
二层图:
(指令校验模块)
数独终局生成及残局求解 在指令校验模块中,将指令拆分为三部分分别进行校验,并在校验结束后提取出合法操作符和参数,如果不合法则对用户进行提示。
(生成数独终局(组))
数独终局生成及残局求解 在生成数独终局(组)模块中,程序根据终局需求数生成终局,每生成一个新终局就输出一个并计数,节约内存。
(残局校验模块)
数独终局生成及残局求解 在残局校验模块,程序根据从文件中独入的数独残局的数字进行校验,检查是否有重复数字,若没有则对完整性进行校验,检查残局是否满足9行9列,若都满足则输出合法残局,否则则想用户输出非法数独残局的反馈信息。
(求解数独残局(组))
在求解数独残局(组)模块,程序先统计合法残局中的空位及它周围的数据,之后在对每个空位进行求解,因为合法的残局一定有一个解,所以程序一定能找到一个完整的数独解。数独终局生成及残局求解
-
-
行为建模——状态转换图
下图展示了数独终局生成和数独残局求解程序的运行过程。
数独终局生成及残局求解
3.2.2 数据流设计方法
-
复审并精华数据流图
进过对数据流图的进一步分析,在“生成数独终局(组)”模块和“求解数独残局(组)”模块之后各增加一个输出模块,将原来的按字符输出转化为按块输出,提高输出效率。得到的数据流简化图如下(其中,模块5与模块6为增加的输出部分):
数独终局生成及残局求解 -
划分自动化边界,确定数据流的特征为变换流
自动化边界的划分如上图虚线所示,数据流图中没有明显的事物处理中心,将其视为变换流。
-
划分数据输入、输出边界,分离出处理部分
输入输出边界的划分如上图大括号所示,其中输入部分包括指令输入与校验和数独残局的输入与校验,变换部分包括数独终局生成和数独残局的求解,输出部分为将对应的数独终局输出到指定文件中。
-
执行“一级分解”
系统的一级分解图表现了系统高层的组织结构和高层模块之间的数据流向,其一级分解图如下图所示:
数独终局生成及残局求解 -
执行“二级分解”
二级分解细化了一级分解的结构组织,下图为系统的二级分解图:
数独终局生成及残局求解
3.3 解题思路
整个项目大致分为四个模块,根据题意,程序在指令模块需要能够判断输入指令是否合法,若合法再进行相应操作;生成数独终局(组)模块程序需要在竟可能短的时间内生成最多不超过1000000个不重复的数独终局;残局校验模块要能够对用户输入的残局进行校验,当残局合法时才进行残局求解计算;求解数独残局(组)模块需要在竟可能短的时间内对最多1000000个合法残局进行求解。
3.3.1 指令校验模块
由题意可知,合法指令有如下两种格式:
sudoku.exe -c 20 // 执行sudoku.exe程序 输入指令-c 20
sudoku.exe -s absolute_path_of_puzzlefile // 执行sudoku.exe程序 输入指令-s absolute_path_of_puzzlefile
所以指令校验模块的任务应该是检验操作符是否为“-c”或“-s”,操作符后的参数是否符合要求,所以该模块只需对指令两个部分分别检验即可。因为除了检验指令合法性,该模块还需要对合法指令进行操作符合参数的提取,所以不放接口参数直接用引用类型,将参数直接赋值,而操作符用整型(1/0)或bool型返回。
3.3.2 生成数独终局(组)模块
-
暴力枚举
对于少量的数独终局,我最先想到的是暴力枚举,即每个位随机取[0,9]的整型数字,之后判断是否合法,直到填满81个空格,在生成一个数独总局之后进行查重…显然,这是一种非常费时的算法,就算进行适当优化,在不考虑输出的情况下,假设每个位置的数字均能一次随机出合法数字,则每个数字的合法性的检验需要O(n)的复杂度,生成一个数独终局需要O(n)的复杂度。之后是查重,对n个二维数组查重需要O(n^3)的复杂度。
所以,在特别理想的情况下,该算法的时间复杂度为O(n^3),显然不能满足1000000数量的求解问题…
-
全排列算法
通过简单观察以及查阅资料得知,全排列是生成数独终局的有效算法之一。以第一行为下列数据为例:
当第二行向左(或右)平移(n % 9)位,且(n % 9 != 0)时,该两行中的任意两列元素一定不同。同理,之后的7行也做类似处理,这样就能初步保证9行中任意两列没有重复的元素,因为每一行是由平移得到的,所以只要保证了第一行没有重复元素,之后的9行中任意一行都不会有重复元素。之后就是保证每个3X3的宫内元素不重复。
通过尝试发现,当从第二行开始,每行依次向一个方向平移3、6、1、4、7、2、5、8个单位时,每个宫内的元素不重复。至此一个有效的数独终局便已形成。该算法的优势在于,由于平移的原理,在首行无重复元素的情况下,生成的数独终局一定合法,不需要再对每个元素进行合法性检验,时间复杂度为O(1);其次,只要首行不同,生成的终局一定不同(至少首行不同),所以只要保证首行不同,就不用进行查重,时间复杂度为O(n)。
现在问题就有求解一个数独终局转化成了求解8个数(第一位固定为8)的全排列问题。由于8个数规模不大,只需使用简单的递归便能实现8个数的全排列,复杂度为O(1)。
参考文章:笔试面试算法经典–全排列算法-递归&字典序实现(Java)
通过简单计算发现,此时的可以生成的数独终局的总数为:8! = 40320种,不足1000000,因此再进行优化。继续观察发现,位于同一小九宫格中的数字,两行间整行交换或两列间整列交换不影响结果,但是如果是两个小九宫格中的各一行(或各一列)进行整体交换,则可能引起交换之后小九宫格中出现重复元素。由于第一行第一个元素固定,所以,前三行(第一行九宫格)中,第2行和第3行可以进行交换,中间三行(第二行九宫格)中,三行间可两两交换,最后三行(第三行九宫格)也可以三行间两两交换。这样,若不考虑列交换,则现在能够生成的数独终局数量为:8! X ( 2! X 3! X 3! ) = 2903040,大于1000000。所以,只需考虑首行全排列和行排列(或列排列)即可。
综上所述,使用全排列算法的时间复杂度为O(n)。
3.3.3 残局校验模块
残局校验模块需要对目标文件中的数独残局题目的合法性进行检测,主要检测题目中的元素是否都是[0,9]的整型数字、题目行数(列数)是否为9、题目给出的已知数字之间有没有矛盾等。残局检验模块只需对每个残局进行扫描,并对已知数字进行行列检测即可。该模块相对简单,且题目中没有明确说明数独残局题目有出错的情况,所以在此不多做分析。
3.3.4 求解数独残局(组)模块
该模块需要对一个合法的数独残局进行求解,对于每道题,给出一个可行解即可。根据以往做题经验,看到题目后,我尝试使用回溯进行求解,用适当的剪枝策略进行优化。
有上一个模块的处理,该模块的到的数独残局一定是合法的,由于数独的特性,该残局一定有至少一个可行解。于是,在读取残局的过程中,将空位(为0的位)放入记录的数组中,当扫描结束后,直接按照记录空位数组中的坐标(行号,列号)进行试探。试探过程采用深搜策略(DFS),从第一个空位开始,填入[1,9]中的一个整型数字,之后对该数字的有效性进行判断,如果有效,则进行下一个空位的试探,如果该位置9个数字都不满足要求,则返回上一个空位,换另一个数字进行试探。当所有空位试探完毕后,便得到一个可行的数独解。
该算法的复杂度为O(n^m),m为空位个数,可见深搜其本质是枚举,所以复杂度很高,通过查阅资料,程序将在深搜的基础上进一步优化。
参考文章:暴力算法之美:如何在1毫秒内解决数独问题?| 暴力枚举法+深度优先搜索
受文章启发,反思原有算法发现,程序的时间开销除了在试探每一个数以及回溯上,还主要在检验当前位置数字的有效性上,为了降低时间复杂度,在此用牺牲空间换区时间效益的方式,增加三个记录表,分别用于记录某个数字在某行是否出现,某个数字子在某列是否出现,以及某个数字在每个小九宫格中是否出现。这样在判断当前位置数字的有效性的时候,时间复杂度就从O(n)降为了O(1)。
除此之外,由于只需要输出一个可行解,根据启发式规则,如果当前空位能填的有效数字数量为x,下一空位能填的有效数字的数量为y,当x>y时,先试探有效数字数量少的空位能够在一定程度上降低时间消耗(如:当前空位可填的有效数字数量为4,下一空位可填的有效数字数量为1,则先填充下一空位能够有效减少程序回溯的次数)。所以,在统计完所有空位之后对所有空位按照其能填的有效数字的数量先进行预处理(排序),将一定程度上减少回溯的次数,且空位最多为81位,排序的时间开销可以忽略。
这样一来,怎样提高回溯效率的问题就转化成了怎么计算当前空位可填的有效数字数量的问题。根据启发式规则,如果一个空位周围(所在行、所在列、所在小九宫格)的已知数字越多,那么该空位能填的有效数字应该相对就越少。因此只要在扫描的过程中记录下每行、每列、每个小九宫格中已知的数字的数量,便可作为之后对空位排序的依据。
四、设计实现过程
4.1 程序流程图
4.2 主要函数接口设计
该程序中,比较重要的函数有:全排列函数,数独终局生成函数,递归求解数独残局函数。
首行全排列函数:由上述全排列算法可知,每一种首行排列能够产生2! X 3! X 3! = 72个不同的数独终局,所以用一个max_num记录当前需求数(-c 当前需求数) 所需的最多首行排列数,用一个每行有8个元素的二维数组target[Maxn][8]记录不同排列,每行为一种排列,最多有max_num种。
数独终局生成函数:用一个8元素的一维数组记录从第二行到第九行的平移偏移量(例:move_step[0][8] = {3, 6, 1, 4, 7, 2, 5, 8},一个下标表示第n + 1中平移偏移量),与首行全排列类似,也可通过递归的方式得到足够的平移偏移量序列组。现在已经有了首行的全排列序列和足够的平移偏移量序列,因此在数独终局生成函数中,将两个排列组传入函数体,然后在函数内部件两组序列按上述全排列算法中的方式进行组合,最后按要求输出。
递归求解数独残局函数:按上述回溯剪枝的递归求解数独残局算法,在执行该模块前,程序因通过对合法残局的扫描已经得到各空位组成的数组,各行各列各小九宫格的数据信息。因此,在该函数模块中,按照空位数组(预处理后)顺序进行深搜(DFS)
4.3 各函数之间的关系
由于在编写项目时,VS不是企业版,无法自动生成各函数之间的调用关系,因此,下图为手绘函数调用关系图:
五、程序性能分析及改进(测试均为1e6数据规模)
5.1数独终局生成模块
由上述分析报告可知,数独生成模块的瓶颈主要在最后的输出方式上,因此对函数模块的输出方式进行优化。考虑到原来的方式为单个数字输出,系统的读写速度与CPU运算速度相比要慢得多,因此因减少读写次数,所以考虑将一个数独终局中的所有元素转换成一个字符串,用puts()函数一次性输出。一下为优化之后的代码分析报告:
可见,减少了读写的次数后,程序质量有了大幅度的提高,解决1e6规模的问题耗时不到10s。
5.2 数独残局求解模块
吸取数独终局生成模块的教训,数独残局求解中已经优化了读写模块。
根据分析结果,考虑到在使用类时(特别是数组时),每个类对象需要执行构造函数和析构函数,再加上私有类成员不能直接访问,导致程序运行速度下降。于是考虑用结构体代替类,得到下图:
效果不是很明显…这说明两点:1.类的构造函数基本不会占用太多时间;2.深搜的本质是枚举,枚举真的很慢…(也许是剪枝剪得不够)
六、代码说明
该部分主要对程序的核心代码:数独终局生成模块、数独残局求解模块,进行必要说明。
6.1 数独终局生成模块
6.1.1 首行全排列模块
void Permutate_for_permutation(int source[], int start, int end, int target[Maxn][Maxm], int& line, int max_num)
/*****************************************************************************
参数:source[]:初始的排列(后续生成的所有排列通过该排列变换,相当于种子
start:需要排列的序列起点,用于递归
end:需要排列的序列终点,用于递归
target[Maxn][Maxm]:每一行记录一种排列
line:记录当前生成的排列是第几种
max_num:需要最多的首行排列数
作用:该函数能更具max_num和source[],递归调用自身,完成对初始序列的全排序,将排序结果
放在target数组中,每一行放一种排序,最多有max_num行
******************************************************************************/
{
if (start == end) // 终止条件
{
for (int i = 0; i <= end; i++)
{
target[line][i] = source[i];
}
line++;
}
else
{
for (int i = start; i <= end; i++)
{
if (line >= max_num) // 当前全排序还没有生成结束,但是应为当前的终局生成需求数不需要那么多,所以强制返回
{
return;
}
Swap(source[i], source[start]); // 交换两个元素位置
Permutate_for_permutation(source, start + 1, end, target, line, max_num);
Swap(source[i], source[start]);
}
}
}
需要注意的是,之前提过,每一种首行排列能够产生 2! x 3! x 3! = 72个数独终局,所以不需要每次求解都产生首行的说有排列,只需(当前终局需求数 / 72)下取整即可。上述函数模块的本质是一个 排列通过有限次两两元素交换能得到另一个排列。
6.1.2 数独终局生成模块函数
void FillTheBlock(int cnt, int move_step[80][Maxm], int permutation[Maxn][Maxm])
/*****************************************************************************
参数:cnt:指令中-c的参数,即需要的数独生成终局的数量
move_step[80][Maxm]:第2至9行的行平移偏移量,每一行为中排序,每行的第i个元素对应第i + 1行的平移偏移量
permutation[Maxn][Maxm]:首行排序,每一行对应一种排序
作用:该函数将首行全排列和2只9行每行平移偏移量结合起来,生成数独终局,一个首行全排列和一个平移偏移量排列即
可组成一个数独终局
******************************************************************************/
{
... // 第一行处理(因为第一行不用平移,所以单独处理)
// temp为函数内的局部变量,是一个字符串,记录一整个数独终局,temp_site是对应的脚标
for (int i = 1; i < 9; i++) // 输出 2 ~ 9 行
{
for (int j = 0; j < 9; j++)
{
int site = (j + move_step[ml][i - 1]) % 9; // 对整行进行平移(向左)
temp[temp_site] = permutation[pl][site] + '0';
temp_site++;
if (j == 8)
{
if (i == 8)
{
temp[temp_site] = '\0';
temp_site++;
}
else
{
temp[temp_site] = '\n';
temp_site++;
}
}
else
{
temp[temp_site] = ' ';
temp_site++;
}
}
}
... // 输出
}
该部分代码的实质其实就是将从两个集合里分别选一个元素进行组合,其中需要注意的是,每次要对平移后的脚本模9,保证脚本不超过[0,8]的范围。
6.2 数独残局求解模块
bool DFS(Point p[], const int& num, int rm[Maxm][Maxm], int cm[Maxm][Maxm], int bm[Maxm][Maxm], int step, int block[Maxm][Maxm])
/*****************************************************************************
参数:p[]:空位数组,在扫描之后,记录个空位的坐标(行,列)等有关信息
num:空位数量,记录空位总数,作为递归重点的依据
rm[Maxm][Maxm]:行元素记录表,rm[x][y] == 1 表示x行包含元素y
cm[Maxm][Maxm]:列元素记录表,cm[x][y] == 1 表示x列包含元素y
bm[Maxm][Maxm]:块元素记录表,bm[x][y] == 1 表示小九宫盒x包含元素y,小九宫格顺序为从0到8,一行一行编码
step:表示当前在试探的空位在空位数组中的脚标,用于递归
block[Maxm][Maxm]:记录一个数独残局
返回值:bool型,返回1表示递归找到了可行解,否则表示没有找到
作用:通过递归调用自身,按照空位数组p[]中的顺序对每个空位进行试探,当填满所有空位时,递归结束,找到一个可行解
******************************************************************************/
{
if (step == num)
{
return true;
}
for (int i = 1; i <= 9; i++) // 对于每个空位,从1到9依次试探
{
int r = p[step].row, c = p[step].col;
if (CheckNum(rm[r][i], cm[c][i], bm[GetBlockNum(r, c)][i])) // 检查在空位(r,c)上填数字i是否合适
{
/* 打表记录 */
SetMark(rm, r, i, 1);
SetMark(cm, c, i, 1);
SetMark(bm, GetBlockNum(r, c), i, 1);
block[r][c] = i;
/* 结束 */
if (DFS(p, num, rm, cm, bm, step + 1, block)) //搜索下一个空位
{
return true; // 递归找到了一个可行解
}
/* 递归没有找到可行解,当前位置不能填数字i,恢复之前打表的数据 */
SetMark(rm, r, i, 0);
SetMark(cm, c, i, 0);
SetMark(bm, GetBlockNum(r, c), i, 0);
block[r][c] = 0;
/* 结束 */
}
}
return false;
}
此处列出了该函数模块的核心部分,回溯搜索及剪枝,其中剪枝的思想体现在扫描过程(DFS()之前的预处理)中记录空位数组并根据相关信息对数组进行排序,其次还体现在打表记录每行每列每小九宫格中某个数字是否存在,方便快速验证试探数字的有效性。至于是否需要再进行预处理筛选出每个空位可以填的候选数字,我认为没有必要,应为筛选候选数字需要在整个数独残局扫描结束之后才能进行,上述代码块相当于是在找候选数字的同时直接对候选数字进行试探,理论上效率更高。
七、单元测试
由于本次开发以结构化设计开发方式进行,以增量模型开发,每个部分为小的瀑布模型,因此整个程序模块话程度较高,基本做到高内聚,低耦合。因此,很容易做到对每个模块进行单元测试。测试主要以白盒测试为主,每个单元测试偏向于对判断部分的路径测试,测试用例在代码库里,在此仅展示测试结果图。
7.1 指令校验模块
7.2 求解数独残局中的DFS模块
因为有效的数独问题一定至少有一个可行解,所以DFS函数模块正常情况下一定能返回true,所以该模块的单元测试主要偏向于测试生成的数独残局的可行解是否正确(即,每行每列每小九宫格没有重复元素)。另外,因为之前以及默认DFS的起点是空位数组的第一个位置,所以忽略了起点的边界值等问题,经过单元测试,将起点的边界值判断也归入其中。
八、各模块实际开发时间及与预期对照
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 30 |
Estimatie | 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | 240 | 300 |
Analysis | 需求分析(包括学习新技术) | 30 | 90 |
Design Spec | 生成设计文档 | 60 | 90 |
Design Review | 实际复审(和同事审核设计文档) | 120 | 90 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 60 | 60 |
Design | 具体设计 | 90 | 120 |
Coding | 具体编码 | 360 | 480 |
Code Review | 代码复审 | 90 | 120 |
Test | 测试(自我测试,修改代码,提交修改) | 300 | 360 |
Reporting | 报告 | 90 | 90 |
Test Report | 测试报告 | 20 | 60 |
Size Measurement | 计算工作量 | 60 | 30 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程修改计划 | 30 | 30 |
合计 | 1585 | 1970 |
九、个人总结
9.1 个人能力的提升
9.1.1 培养结构化设计程序的思维
这是我第一次以工程化的角度编写C++程序,与以往做题不同,以往做题,通常所有文件都只用放到一个main.cpp中,只要最后OJ系统判断正确,就万事大吉,而以工程化方式编写程序则更像是一步一个脚印的成长,让自己的程序有规律的健壮。在结构化程序设计过程中,需要先确定需求,认真进行需求分析,弄清各数据流在程序模块之间的转化,真正做到条例清晰。另外,工程化编程和做题的显著区别在于,bug的隐蔽性更高,当然对于培养个人改进程序能力而言,这是一件好事。
9.1.2 掌握更高效的编程技巧
由于工程编程需要对代码进行分析,对模块进行单元测试,在完成每个小任务的过程中,我见识到了新的编程技巧,打破了一贯的只会盲目输入用例进行调试,优化代码得通篇细看的习惯。在VS的代码分析工具的帮助下,我能很快定位程序中的瓶颈,根据二八定律,我便能有针对性的对程序进行优化,而且效果显著。在VS单元测试功能的帮助下,我掌握了对单个模块进行批量测试的方法,不用再像以往一样通篇盲目调试,这样一来,我定位bug的能力又上了一个台阶。总的来说,学无止境,只有不断开阔自己的眼界,才能真正使自己便利。
9.1.3 模仿与自学能力
在编写项目和优化过程中,有很多功能是我第一次接触,这对我的自学能力是一个很大的挑战,好在如今网络便利,加上教程丰富,让我能够很快的上手使用有关功能。仔细想想,从事有关计算机方面的事情,要是没有一定的学习热情和自学能力,真的很快就会被淘汰。
9.2 不足之处
由于计算机发展迅速,所以很多新技术、新资料通常都是英文版的,在这次项目实践过程中,我深切感受到,若不能让英语成为自己的强项,那它终将成为自己的绊脚石。这次项目编写让我真切的认识到了自己的不足,也让我有了强烈的危机感,相信在今后的学习生活中,我会铭记现在的这样迫切想要变得更加优秀的心情,一直努力。