java数独流程图_软件工程个人项目总结-数独
Github項目地址
包含博客
這篇博客將之前的每周所發整合成了一篇
PSP表格
PSP2.1
Personal Software Process Stages
預估耗時(分鐘)
實際耗時(分鐘)
Planning
計劃
60
60
· Estimate
· 估計這個任務需要多少時間
60
60
Development
開發
870
1710
· Analysis
· 需求分析 (包括學習新技術)
120
120
· Design Spec
· 生成設計文檔
120
160
· Design Review
· 設計復審 (和同事審核設計文檔)
60
30
· Coding Standard
· 代碼規范 (為目前的開發制定合適的規范)
120
60
· Design
· 具體設計
60
120
· Coding
· 具體編碼
240
500
· Code Review
· 代碼復審
30
120
· Test
· 測試(自我測試,修改代碼,提交修改)
120
600
Reporting
報告
150
240
· Test Report
· 測試報告
60
120
· Size Measurement
· 計算工作量
30
30
· Postmortem & Process Improvement Plan
· 事后總結, 并提出過程改進計劃
60
90
合計
1080
2010
需求分析
需求
運行一個命令行程序,程序能:
1. 生成不重復的數獨終局至文件。
2. 讀取文件內的數獨問題,求解并將結果輸出到文件。
數據建模
將數獨分成9個宮進行求解,ER表示如下,因為數獨左上角第一塊確定,所以將其看作數獨的屬性。
功能建模
數據源:用戶
數據終點:文件
主要數據流:生成指令、求解指令、數獨(待求解),以及終局
主要支持文件:待求解數獨文件
主要處理過程:生成終局、求解數獨
0層圖
對系統進行求精,劃分系統的子系統。
行為建模
解題思路
在最開始拿到題目的時候,想起上學期算法課程學過的回溯算法,決定用會回溯算法進行實現,所以翻看了算法設計的課本和ppt,對回溯算法重新進行了學習。在生成n個不同局面和求解給定數獨的思路大致一致,但有在細節處的處理有些不同。首先是生成n個不同的局面,按行進行循環,有一個二維數組保存每個位置的可能數字,從可能數字中隨機選一個放在該位置,若該位置找不到可能的位置,則回到上一個,修改上一個位置的解。不斷進行回溯,直到所有的位置都合法。若是對給定局面進行求解,則將空白位置用數組記錄下來,只對空白位置進行回溯即可。
但在該回溯算法中,搜索時盲目的,效率較低,最差的實現對每個空方格試探所有可能的數字,有大量的時間浪費。并且如果需要生成多個終局時,每一個終局都是通過回溯實現,且生成不同的終局由Random()進行數字選擇實現,但實際上Random生成偽隨機數,在一定時間會生成相同隨機數,無法保證在生成終局數目足夠大時,不會產生兩個相同的終局。
因為算法效率有很大的改進空間,以及無法保證在n足夠大時,所有終局都相異,所以在對代碼重構的過程中,引入了排列組合的算法,對原有進行優化。將數獨分成9個3X3的小方塊,每個小方塊是一個宮。
參考維基百科提供的一個生成終局思路,由第一宮生成其余八個宮。對第一宮中的塊進行排列組合即可,因為生成終局時,要求左上角的第一個數為:(學號后兩位相加)%9+1,(1+3)%9+1=5,所以左上角為5,且不允許改變。即在第一宮內只有八個塊可以移動,有8!=40320種情況,并且在每一宮內(除了第一宮)進行列列變換有3!×3!×3!×3!=1296,第二宮和第三宮可以交換,第四宮和第七宮可以交換有2!×2!=4,總共可以生成209,018,880個終局(大于1,000,000),此方案可行。
對于各種全排列,使用遞歸的方式實現。
設計實現過程
???編譯器:Intellij IDEA 2019.2
???語言:java
???運行環境:JDK 1.8
活動圖
順序圖
下面是生成終局的順序圖
下面是求解數獨的順序圖
類圖
???共有三個類,Main用于判斷輸入是否合法、將終局寫入文件、判斷輸入命令是否合法,Generate類用于生成終局,Solve類用于求解數獨。
具體代碼說明
Generate中主要函數:
?generateSudoku 外部調用的接口,傳入一個n(n<=1,000,000),即可以生成n個終局。
createSeed 將第一宮作為種子,進行交換操作,得到不同的種子。
createMap 通過種子的變換,得到該種子生成的第一個終局。
changeIndex 對于第一個終局進行行列變換,生成不同的終局。
writeToOutput 將生成的int[ ] [ ]的終局,按照輸出格式要求,存放在一個char[]中 。
changeIndex流程圖如下
具體實現代碼如下所示,在goalNum=1時,不用進行變換,在>1開始,先交換index[16]和index[17],即先在第三大列內進行列變換,可以有6中不同的終局,接著在第二大列內進行交換,依次類推,到行進行交換,可以快速產生大量不相同的終局。
/**
* @Title: changeIndex
* @Description: 對宮內的行列進行全排列
* @param a
* @param start
* @param end
* @return void
* @throws
*/
public void changeIndex( int start, int end) {
int i;
//分段進行全排列
if (start == end) {
if (end == 5) {
changeIndex( 6, 8);
}else if(end==8){
changeIndex( 12, 14);
}else if(end==14){
changeIndex( 15, 17);
}
else {
writeToOutput();
}
}
else {
for (i = start; i <= end; i++) {
swap(index, start, i);
changeIndex( start + 1, end);
if (nowNum == goalNum) break;
swap(index, start, i);
}
}
}
Solve中主要函數
findSolution外部調用的接口,傳入一個txt的絕對路徑,即可對該txt中的數獨進行求解。
dealFile將txt中的所有數獨,存在一個int[]中,并得到要求解的數獨數目。
initCriterion將Criterion(表示行列宮沒有用過的數)初始化,并將數獨中出現過的數在Criterion進行標記。
chooseCriterion遞歸對數獨進行求解。
chooseCriterion流程圖
chooseCriteron和dealWithCriterion是求解數獨的主要兩個函數。chooseCriterion中state表示當前是否能找到可行解,找到返回true,沒找返回false。先循環1-9九個數,找到一個所在行、列、宮都沒有使用過的數放入,然后調用dealWithCriterion函數,若后面的位置沒有待求解位置,則返回true,此時state為true,結束求解。若后面仍有待求解位置(a,b),則dealWithCriterion函數返回chooseCriterion(a, b)進行求解,若可以找到可行解,則前面所求解保留,若找不到,則修改前面找到的可行解。本質是利用回溯算法進行求解。
起初兩個函數寫在一起,但在代碼質量檢查時,該函數復雜度太高,所以拆分成兩個函數
/**
* @Title: chooseCriterion
* @Description: 回溯選擇合適的數
* @param row
* @param col
* @return boolean
* @throws
*/
private boolean chooseCriterion(int row, int col) {
int value;
boolean state = false;
for (int k = 0; k < 9; k++) {
if(!state) {
value = k + 1;
if (!fill(row, col, value)) {
continue;
}
map[row][col] = value;
usedNum(row, col, value);
state = dealWithCriterion(row, col);
if (!state) {
releaseNum(row, col, value);
map[row][col] = 0;
}
}
}
return state;
}
/**
* @Title: dealWithCriterion
* @Description: 查找是否有空位要求解
* @param row
* @param col
* @return boolean
* @throws
*/
public boolean dealWithCriterion( int row, int col) {
//沒有空位
boolean state = false;
for (; row < 9; row++) {
for (; col < 9; col++) {
if (map[row][col] == 0) {
state = true;
break;
}
}
if (state)
break;
col = 0;
}
//沒有空位
if (!state) {
return true;
}
return chooseCriterion(row, col);
}
Main中主要函數
stringToFile傳入一個char[],將其寫入可執行文件同級文件夾中的layout.txt(沒有會創建)。
isNumeric,whetherSolve,whetherGenerate都是判斷輸入是否合法。
測試
?使用Junit5對三個類分別進行單元測試,單元測試的編寫思路是黑盒測試,運用了路徑覆蓋和邊界覆蓋。對接口功能、邊界條件等進行測試,并使測試覆蓋率盡可能,測試用例盡可能全面。
?MainTest用于測試輸入是否合法,以及是否可以順利寫入文件,
?GenerateTest測試其中的函數是否正常,并進行了重復性檢測,檢測器生成的1000個終局中是否有相同的情況。
?SolveTest對Solve中部分主要函數進行了檢測,并對求解結果進行了有效性檢測,判斷是否是否求出了正確的數獨解。
?單元測試覆蓋率
Main部分測試設計(期待狀態=實際狀態,即為成功)
編號
內容
狀態
1
args[0]=11,args[1]=null
成功
2
args[0]="-c", args[1]="0"
成功
3
args[0]="-c", args[1]="1_000_001"
成功
4
args[0]="-c", args[1]="1_000_000"
成功
5
args[0]="-c", args[1]="D:/1.txt"
成功
6
args[0]="-s", args[1]="D:/1.txt"
成功
7
args[0]="-s", args[1]="D:/app/"
成功
Generate部分測試用例
函數
內容
狀態
createSeed
seed={1,2,...9},goalNum=1
成功
createSeed
seed={1,2,...9},goalNum=100
成功
createMap
int[][] temp={{5,1,2,7,8....},goal=1
成功
createMap
int[][] temp={{5,1,2,7,8....},goal=1
成功
swap
交換兩個數
成功
generateSudoku
count=1000(檢測生成的1000個終局是否重復)
成功
generateSudoku
count=1,000,000(檢測生成的1,000,000個終局是否重復)
成功
Solve部分測試用例
函數
內容
狀態
findSolution
PATH=txt路徑,里面有1000個數獨,檢查找到的解是否正確
成功
findSolution
PATH=txt路徑,里面有100個高難數獨,檢查找到的解是否正確
成功
fill
Criterion[0]=510,Criterion[9]=510,Criterion[18]=510,row=0,col=0,value=5;
成功
releaseNum
PATH=txt路徑,row=0,col=0,value=4;
成功
getBlock
row=0,col=0
成功
getBlock
row=3,col=4
成功
通過命令行執行程序,對得到結果進程測試
編號
命令行參數
結果
1
-c 0
能生成的終局在1-1,000,000之間
2
-c -1
能生成的終局在1-1,000,000之間
3
-c 1000
運行成功
4
-c 1000000
運行成功
5
-c 1000001
能生成的終局在1-1,000,000之間
6
-c
生成終局命令為:java sudoku -c 阿拉伯數字
7
-a
您的輸入不正確
生成終局命令為:java sudoku -c 阿拉伯數字
求解數獨命令為:java sudoku -s puzzle.txt的絕對路徑
8
-s D:\app\
路徑需指向一個txt文件
9
-s D:\app\unexisted.txt
系統找不到指定的文件
10
-s puzzle.txt
運行成功
代碼質量分析
?用SonarLint插件對每個java文件進行代碼質量優化
?分析過后,除了Main類中因為需要打印命令錯誤提示,仍存在一些warning,其余文件中的warning全部消除。
性能改進
用Intellij中插件JProfiler進行程序性能分析
最初的程序每生成一個終局,直接寫入文件,且進行回溯時,多次進行值的交換。為了保證隨機性,每一次將所有可能值先放入鏈表,然后將隨機選取一個放入該空位。在求解數獨時,先讀取一個數獨,求解后寫入文件,再繼續讀取下一個數據。這種算法會頻繁創建很多對象,且讀寫會花費大量的時間。
改進后,時間上有了非常大的提升,且在回溯,用一個27位數組Criterion,前九位表示每一行可能解,中間九位表示每一列可能解,最后九位表示每一個宮的可能解,且每一個數可以表示成一個九位的二進制,從右到左依次表示1-9,若Criterion[0]=000_000_001,則表示第一行只有1沒有用過,該位置的可行解只有1。選擇一個數時只需將該數與對應的下標的Criterion進行與運算,即可將該數標記為使用,這樣減少了反復復制的時間。且改進后不再隨機選取可行解,按順序進行選擇。
并且改進后每生成一個終局,先放在一個char數組中,生成所有的終局,統一寫入文件;在求解數獨時,一次讀取出所有的待求解數獨,每個分別求解,再統一寫入。
下圖為Jprofiler中提供的CallTree,通過樹形圖體現了方法間的層次調用關系,并按照執行總時間從小到大排列。
改進之前,用FileWriter寫入文件,生成1000個終局需要12s左右。每生成一個終局馬上寫入文件。下圖看到關閉流花費了大量的時間。
改進后,用BufferedWrite寫入文件,由于含有緩沖區,所以效率要比FileWriter高,但內部仍用FileWrite進行讀寫。在生成1000個終局時,總耗時為0.034s。由下表寫入文件所消耗的時間遠大于生成終局。
以后討論情況均為改進以后
在生成1,000,000個終局時,總耗時0.805。stringToFile,即將終局寫入文件所占比例最大,即耗時最長,但生成終局的方法generateSudoku執行耗時雖然不是最長,但仍占有較大的比例。
綜合生成1000和1,000,000個終局可得,在生成終局的情況下,stringToFile時消耗最多的函數。
在求解數獨時,首先讀取1000個數獨,進行求解,總耗時0.325s。此時findSolution函數耗時最多,但在這個函數中會調用其他函數,其中dealFile即讀取文件耗時最多。
求解10,000個數獨時,findSolution耗時最多,在該函數調用的函數中,SolveSudoku求解數獨的函數耗時超過了讀取文件的耗時。在求解數目較小,讀取文件的時間相對較長,在求解數獨變大以后,求解數獨的時間要更長。
GUI界面開發
開發運行環境
運行環境:JDK1.8
運行命令:java -jar sudokuGUI.jar
語言:java
開發環境:Intellij IDEA 2019.2,JavaFX Scene Builder 8.3.0
項目結構和說明
為了利用之前的源代碼,UI界面采用javafx進行開發,使用MVC框架來設計整個應用,使用了數據綁定,通過構建容器組件,添加menu、監聽器等實現圖形化界面功能。
軟件功能說明
主要頁面如下所示,一打開應用自動進行計時。
點擊菜單中的開始,可以開始新游戲、提交當前頁面,查看答案,退出游戲。
點擊提交時,計時器停止,若有空位置沒有填,會提示填滿再提交,若有錯誤會彈窗會提示,完全正確時會彈窗恭喜用戶。
面向對象分析設計
用例圖
類圖
總共有五個類
Main用于顯示UI界面,界面用xml編寫,在sample.xml中。
Controller監聽圖形頁面、鼠標、鍵盤等。
Solve驗證用戶提交的數獨是否正確。
Generate用于生成終局,對終局隨機挖空,形成數獨,顯示在UI中。
AlterInfo用于彈窗提示。
SudokuCell表示數獨中的每一個小塊,控制是否可以輸入,顯示數字等。
狀態圖
設計思路
GUI中的主體代碼和命令行部分幾乎一致,最開始選擇生成數獨的回溯算法。最初實現UI界面,用了優化以后利用排列組合快速生成終局的算法,但因為終局是由變換第一宮形成的,所以存在規律性,降低數獨的可玩性。所以采用了最初生成終局的回溯算法。
實現思路是先通過回溯算法生成一個終局,因為要求最少挖30個空,最多挖60個空,每個宮中最少有兩個。當每個最少挖4個,4×9=36符合要求,每個宮最多挖6個,6×9=54個符合要求。所以循環九個宮,每個宮產生一個隨機數n(4<=n<=6),然后在1-9中生成n個不同的隨機數,在該宮中將n個隨機數所在的位置挖掉,便生成了一個數獨。
在編寫代碼的過程中,卡殼了很久一直在思考如何保證數獨解的唯一性,但因為自己對這部分算法的理解并不是很深入,所以放棄了保證了數獨解的唯一性。用戶提交數獨時,不與最初生成的終局比較,而是利用循環檢查用戶提交的答案中是否有錯誤,沒有錯誤即為正確答案。若用戶選擇查看答案,則提供最初的終局。
自定義了一個數據類型SudokuCell,來存放數獨中的每一個小塊,其中設置一個屬性write保證可以多次修改答案,若數獨中該位置需要填寫,則將write設置為true,若不需要則設置為false,并且可以對每一個SudokuCelle用css進行美化,使數獨的外觀更加美觀。
具體代碼實現
對用戶提交數獨進行檢查,正確時返回true,錯誤時返回false,這個方法在命令行中用于測試,檢查對txt數獨中求解時,是否有錯誤。
/**
* @Title: checkSolution
* @Description: 檢查結果
* @param data
* @return boolean
* @throws
*/
boolean checkSolution(int[] data)
{
int index=0;
int[] criterion=new int[27];
Solve s=new Solve();
for (int j = 0; j < 27; j++) {
criterion[j] = 511;
}
s.setCriterion(criterion);
if(!checkSudoku(data,index,s))
return false;
return true;
}
/**
* @Title: checkSudoku
* @Description: 檢查數獨答案是否正確
* @param data
* @param index
* @param s
* @return boolean
* @throws
*/
private boolean checkSudoku(int[] data,int index,Solve s) {
for (int j = 0; j < 9; j++) {
for (int k = 0; k < 9; k++) {
while (data[index]>9 || data[index]<1)
{
index++;
}
int temp = data[index++];
if (!s.fill(j, k, temp)) {
Logger logger=Logger.getLogger("SolveTest");
logger.setLevel(Level.SEVERE);
String msg="row:"+(j+1)+"clo:"+(k+1)+"value:"+temp;
logger.severe(msg);
return false;
}else{
s.usedNum(j,k,temp);
}
}
}
return true;
}
測試
因為GUI項目中,除了UI部分代碼,都是在命令行中經過測試的代碼,所以只進行簡單的系統測試,檢測頁面的響應、監聽等是否正常。
編號
操作
預期結果
實際結果
狀態
1
頁面有空位,點擊提交
未完成彈窗
未完成彈窗
通過
2
頁面沒有空位,存在錯誤,點擊提交
錯誤彈窗
錯誤彈窗
通過
3
正確完成數獨,點擊提交
正確彈窗
計時停止
正確彈窗
計時停止
通過
4
點擊查看答案
顯示答案
計時停止
顯示答案
計時停止
通過
5
點擊退出
退出程序
退出程序
通過
6
點擊新游戲
開始新游戲
開始新游戲
通過
7
在空位輸入非數字字符
輸不進去
輸不進去
通過
8
在空位輸入數字字符
空位顯示該數字
空位顯示該數字
通過
9
點擊查看答案以后,點擊提交
正確彈窗
正確彈窗
通過
10
點擊關于
信息彈窗
信息彈窗
通過
11
點擊幫助
幫助彈窗
幫助彈窗
通過
簡要開發過程
時間
內容
2019年12月19日 - 2019年12月20日
需求分析、概要設計
2019年12月26日 - 2020年1月1日
基本完成命令行功能
2019年12月26日 - 2020年1月1日
開始單元測試
2020年1月2日 - 2020年1月3日
修改算法、對代碼結構進行優化,完成測試
2020年1月4日 - 2020年1月16日
代碼質量檢測、UI編寫
2020年1月17日 - 2020年1月18日
博客優化
總結
以上是生活随笔為你收集整理的java数独流程图_软件工程个人项目总结-数独的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle数据库视图有红叉,oracl
- 下一篇: jquery不同版本冲突导致低版本功能不