Coding.net源碼倉庫地址:https://git.coding.net/wanghz499/2016012032week2-2.git
測試步驟:
1.進入src文件夾
2.在命令行輸入javac -encoding utf-8 Main.java
3.回車再輸入java Main 20
4.回車,將會在根目錄下(與src同級)產生result.txt
一、需求分析
通過對題目要求的分析,我共提取出以下7個需求(實現帶括號和真分數的附加功能):
1.程序可從命令行接收一個輸入參數n,然后隨機產生n道加減乘除練習題。
2.每個數字在 0 和 100 之間,運算符在3個到5個之間。
3.每個練習題至少要包含2種運算符。
4.所出的練習題在運算過程中不得出現負數與非整數。
5.將學號與生成的n道練習題及其對應的正確答案輸出到文件“result.txt”中。
6.支持有括號的運算式,包括出題與求解正確答案。算式中存在的括號必須大於2個,且不得超過運算符的個數。
7.支持真分數的加減法,並且每個分數都化到最簡
二、功能設計
能夠根據用戶輸入的參數n隨機產生n道符合要求的練習題,自動算出答案,並將式子與答案以文檔的形式呈現。並實現附加功能:支持有括號的運算、支持真分數的加減運算。
三、設計實現
我共設計了5個類,如圖:
Creat類:負責隨機產生一條帶括號的至少2種運算符四則運算的式子,且有3-5個運算符
Calculator類:負責篩選運算過程中不產生負數和小數的式子,並計算答案
MakeFile類:負責產生result.txt文件,並將學號和產生的練習題寫入文件
properFraction類:負責產生真分數式子並計算答案
Main類:主類,負責接收命令行的參數並啟動程序
5個類的相互調用關系為:
比較重要的函數:
Creat類:creatProblem():隨機產生一條帶括號的含3-5個運算符四則運算式子,
若式子不符合條件,會遞歸直到產生符合條件的式子。
index(int n):產生運算符下標數組,並保證至少有2個不同的運算符。
Calculator類:
algorithm(String s):結合了調度場算法和逆波蘭表達式的求值,計算出式子的答案。
calculate(int a,int b,String stmp):計算式子每一部分的運算,排除運算過程中出現小數和負數的式子。
ProperFraction類:
createProblem():隨機產生一條含3-5個運算符的真分數加減運算式子,並計算出結果。
greatFactor(int x,int y):求最大公因數,用於化簡
函數間的邏輯關系:creatProblem()調用index(int n)和algorithm(String s),algorithm(String s)調用calculate(int a,int b,String stmp),還有生成文件相關的方法就不列舉了。
四、算法詳解
本項目的關鍵在於Calculator類的計算,結合了調度場算法和逆波蘭表達式(即后綴表達式)的求值,一步實現計算四則運算式子。
關於調度場算法和逆波蘭表達式求值,我花費了大量時間瀏覽博客理解它的實現過程,現總結如下:
調度場算法的作用是將中綴表達式變為后綴表達式,它需要一個隊列來裝后綴表達式和一個棧來裝符號。先從左到右遍歷中綴表達式的每個符號和數字,若是數字就入隊;若是符號,則判斷其與棧頂符號的優先級,若該符號是右括號或其優先級低於或等於棧頂符號,則棧頂元素依次出棧並輸出進入隊列,並將當前符號進棧,一直到最終輸出后綴表達式。
逆波蘭表達式求值步驟(只需要一個棧):
1.先初始化一個空棧,開始遍歷后綴表達式。
2.如果字符是一個操作數,則令其入棧。
3.如果字符是個運算符,則彈出棧里的兩個操作數(一定會有兩個數在棧里,因為是后綴表達式),進行運算,再把結果入棧。
4.到后綴表達式末尾,從棧中彈出結果。
理解了這兩個算法后,就可將兩個算法結合,只需兩個棧就可一次性求出答案:
1.初始化兩個棧,分別是數字棧和符號棧。
2.遍歷中綴表達式,如果是數字,則入數字棧。
3.如果是符號,則判斷其與符號棧的棧頂符號的優先級。若當前符號是右括號或其優先級低於或等於棧頂符號,則棧頂元素依次出符號棧,並在數字棧彈出兩個數進行相應運算,再使結果入數字棧,當前符號也入符號棧。
4.當遇到等號,則將符號棧里的符號依次出棧,從數字棧彈出兩個數進行相應運算,再把結果入數字棧,直到最后一個符號出棧。把數字棧的數字彈出,即為結果。
至於符號的優先級,則使用Hashmap<String,int>建立多組鍵值對,使每個符號對應一個數值,數值越高說明優先級越高。
優先級從小到大:)小於 + - 小於 ×÷ 小於 (
五、測試運行
進入src文件夾,在命令行輸入javac -encoding utf-8 Main.java 將類編譯成class文件,再輸入java Main 20 運行class文件,這里先做一個非法輸入和越界測試,如輸入java Main e或java Main 1200:
再正常輸入如java Main 20,將會在根目錄下(與src同級)產生result.txt文件:
測試完成!
六、代碼展示
產生整數式子的方法:
public static String createProblem(){ Random random = new Random(); String[] operator = {"+","-","×","÷"}; int operatorCount = 3+random.nextInt(3); //操作符的個數3-5 int[] num = new int[operatorCount+1]; //操作數的個數比操作符多1 int[] index = index(operatorCount); //操作符的下標 String s = new String(); for(int j=0;j<operatorCount+1;j++){ num[j] = random.nextInt(101); //產生0-100范圍的操作數,random.nextInt(n)的取值范圍是[0,n) } int choose = random.nextInt(2); //選擇式子括號形態 switch (operatorCount){ case 3:{ if(choose==0){ s=num[0]+operator[index[0]]+"("+"("+num[1]+operator[index[1]]+num[2]+")"+operator[index[2]]+num[3]+")";//1+((2×3)-4)型 }else s="("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+"("+num[2]+operator[index[2]]+num[3]+")";//(1+2)×(3+4)型 break; } case 4:{ if(choose==0){ s="("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+num[4]+operator[index[3]]+"("+num[2]+operator[index[2]]+num[3]+")";//(1+2)×3÷(4-1)型 }else s=num[4]+operator[index[3]]+"("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+"("+num[2]+operator[index[2]]+num[3]+")";//3×(1+2)+(4÷2)型 break; } case 5:{ if(choose==0){ s="("+num[0]+operator[index[0]]+num[1]+operator[index[4]]+num[5]+")"+operator[index[1]]+"("+num[4]+operator[index[3]]+num[2]+")"+operator[index[2]]+num[3];//(6+2×3)-(1+2)×3型 }else s="("+num[0]+operator[index[0]]+"("+num[1]+operator[index[1]]+num[2]+operator[index[2]]+num[3]+")"+")"+operator[index[3]]+"("+num[4]+operator[index[4]]+num[5]+")";//(1+(2×3+4))-(6÷3)型 break; } } s+="="; //給式子加上等號 int answer = Calculator.calculate(s); if(answer>=0){ //判斷式子是否符合要求,凡是返回負數的就是不合格的 s+=answer; }else { return createProblem(); //遞歸,直到產生合格的式子 } return s; }
保證式子里至少有2個不同操作符的方法:
private static int[] index(int n,int m){ //產生操作符的下標數組 Random random = new Random(); int similar=0; int[] a = new int[n]; for(int j=0;j<n;j++){ a[j] = random.nextInt(m); } for(int j=1;j<n;j++){ if(a[0]==a[j]) similar++; } if(similar==n-1) return index(n); //保證一個式子里至少有2個不同的操作符,若所有操作符下標都一樣,則重新產生操作符下標 else { return a; } }
產生真分數式子並計算的方法:
public String createProblem(){ Random random = new Random(); String[] operator = {"+","-"}; int operatorCount = 3+random.nextInt(3); //操作符的個數3-5 Create create = new Create(); int[] index = create.index(operatorCount,2); //操作符的下標 int sumx = 1+random.nextInt(10); //第一個數的分子1-10 int sumy = 1+random.nextInt(10);//第一個數的分母1-10 int greatFactor = greatFactor(sumx,sumy); sumx/=greatFactor; //化簡 sumy/=greatFactor; while (sumx>=sumy){ sumx = 1+random.nextInt(10); sumy = 1+random.nextInt(10); greatFactor = greatFactor(sumx,sumy); sumx/=greatFactor; sumy/=greatFactor; } String s=sumx+"/"+sumy; //第一個數 for(int i=0;i<operatorCount;i++){ int numx = random.nextInt(25); //分子分母不宜過大 int numy = 1+random.nextInt(25); //否則通分可能會產生很大的數導致溢出 String currentOpreator = operator[index[i]]; while (numx>=numy){ //當分子大於分母,即假分數,則重新生成 numx = random.nextInt(25); numy = 1+random.nextInt(25); greatFactor = greatFactor(numx,numy); numx/=greatFactor; numy/=greatFactor; } if(currentOpreator.equals("+")){ //加法 while(sumx*numy+sumy*numx>sumy*numy) //和為假分數 { numx=random.nextInt(25); numy=1+random.nextInt(25); greatFactor=greatFactor(numx,numy); numx/=greatFactor; numy/=greatFactor; } sumx=sumx*numy+sumy*numx; sumy=sumy*numy; } else { //減法 while(sumx*numy-sumy*numx<0) //差為負數 { numx=random.nextInt(25); numy=1+random.nextInt(25); greatFactor=greatFactor(numx,numy); numx/=greatFactor; numy/=greatFactor; } sumx=sumx*numy-sumy*numx; sumy=sumy*numy; } s+=currentOpreator+numx+"/"+numy; } greatFactor = greatFactor(sumx,sumy); sumx/=greatFactor; //最終結果化簡 sumy/=greatFactor; if(sumx==0) s+="="+sumx; else if(sumx==1&&sumy==1) s+="="+sumx; else s+="="+sumx+"/"+sumy; return s; }
判斷非法輸入和越界輸入的方法(主方法):
public static void main(String[] args) { int n = 0; try { n = Integer.parseInt(args[0]); if(n>1000||n<1){ System.out.println("對不起,只允許輸入1-1000的數字!"); return; //結束運行 } }catch (NumberFormatException e){ //輸入非數字字符等 System.out.println("對不起,只允許輸入1-1000的數字!"); return; //結束運行 } MakeFile.creatFile(n); }
其他代碼請見coding.net,就不一一展示了。
六、PSP
SP2.1 |
任務內容 |
計划共完成需要的時間(h) |
實際完成需要的時間(h) |
Planning |
計划 |
26 |
47 |
· Estimate |
· 估計這個任務需要多少時間,並規划大致工作步驟 |
26 |
47 |
Development |
開發 |
20 |
40 |
· Analysis |
· 需求分析 (包括學習新技術) |
3 |
5 |
· Design Spec |
· 生成設計文檔 |
0 |
0 |
· Design Review |
· 設計復審 (和同事審核設計文檔) |
0 |
0 |
· Coding Standard |
· 代碼規范 (為目前的開發制定合適的規范) |
0 |
0 |
· Design |
· 具體設計 |
3 |
5 |
· Coding |
· 具體編碼 |
10 |
15 |
· Code Review |
· 代碼復審 |
2 |
5 |
· Test |
· 測試(自我測試,修改代碼,提交修改) |
2 |
10 |
Reporting |
報告 |
6 |
7 |
· Test Report |
· 測試報告 |
5 |
6 |
· Size Measurement |
· 計算工作量 |
0.5 |
0.5 |
· Postmortem & Process Improvement Plan |
· 事后總結, 並提出過程改進計划 |
0.5 |
0.5 |
七、總結
這次項目比我想象中的要難,原以為一兩天就可以搞定,實際上花了整整4天時間在圖書館鑽研。其實做這個項目我並沒有完整地按照軟件開發的步驟,也沒有提前設計代碼,想到哪就寫到哪,導致敲代碼的過程中遇到很多小問題,一遇到問題就得停下來去找相應的解決方法,經常是代碼寫了又刪,刪了又寫,效率比較低,下次一定要事先設計,想好整個流程再寫代碼。此外,通過這次作業,我感受到了算法在項目中的重要性,算法是一個項目的靈魂。就如調度場算法和逆波蘭表達式求值算法是這次作業的核心,我再也不敢說類似於不知算法有什么用之類的話了。
原本我是沒有實現分數加減的附加功能的,因為我潛意識里覺得它很難,所以壓根沒想過也不敢做這個附加功能。但是看了其他同學的博客后,發現很多同學都實現了,我分析他們的代碼,突然覺得也不是很難了,幾經思考后我最終也實現了分數的功能,開心不已。我想寫博客的意義就在此吧,互相分享自己的學習成果,共同進步。很感謝那些願意寫博客分享技術的人,從他們的博客中真的可以學到很多東西!
這4天,看了一篇又一篇博客,改了一段又一段代碼,很疲憊卻也很充實,讓我感受到了全身心投入一件事情的快樂,專注與鑽研,我喜歡這樣的感覺。同時,也讓我意識到自己的水平遠比想象中的低,做一個四則運算就讓我費了這么大的勁,說明我的水平真的還不夠,我要好好努力。最后,還是忍不住分享獨自完成一個小項目的喜悅,真的很開心,也給了我很大的鼓勵!專注的感覺真好!