這個作業屬於哪個課程 | 班級的連結 |
---|---|
這個作業要求在哪裡 | 作業要求的連結 |
這個作業的目標 | 實現生成和計算四則運算題的程式,並學會與他人合作做專案,積累實踐經驗 |
一、姓名、學號以及Github地址
姓名 | 黃銘濤 | 曾琳備 |
---|---|---|
學號 | 3122004393 | |
Github地址 | Github |
二、psp表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 30 | 35 |
· Estimate | · 估計這個任務需要多少時間 | 20 | 15 |
Development | 開發 | 480 | 600 |
· Analysis | · 需求分析 (包括學習新技術) | 360 | 180 |
· Design Spec | · 生成設計文件 | 0 | 0 |
· Design Review | · 設計複審 | 60 | 45 |
· Coding Standard | · 程式碼規範 (為目前的開發制定合適的規範) | 60 | 60 |
· Design | · 具體設計 | 60 | 80 |
· Coding | · 具體編碼 | 300 | 960 |
· Code Review | · 程式碼複審 | 30 | 60 |
· Test | · 測試(自我測試,修改程式碼,提交修改) | 60 | 80 |
Reporting | 報告 | 0 | 0 |
· Test Repor | · 測試報告 | 0 | 0 |
· Size Measurement | · 計算工作量 | 20 | 10 |
· Postmortem & Process Improvement Plan | · 事後總結, 並提出過程改進計劃 | 30 | 25 |
· 合計 | 1510 | 2150 |
三、效能分析
1、在改程序序效能上花費了多少時間
兩個小時
2、改進的思路
(1)建立變數isexist來記錄生成的重複四則運算題個數,當isexist大於10000時跳出迴圈並提升使用者無法生成給定數量的四則運算題,以此避免無限迴圈。
(2)在生成運算題的同時計算答案,若計算過程中出現小於0的數立刻退出,避免造成不必要的運算。
(3)效能分析圖
(4)消耗最大的函式:main方法
public class main {
public static void main(String[] args) throws IOException {//主函式接收命令列
getParameter(args);//從命令列接收引數並執行程式
}
四、設計實現過程
1、包含幾個類
一共有五個類,每個類中有多個方法,都由建構函式呼叫。五個類分別為:
Number//儲存運算數
Expression//儲存四則運算算式
makeExpression//生成四則運算算式
Correct//根據Exercises.txt檔案和Answers.txt檔案來統計對題和錯題
main//程式執行入口
2、儲存方式
(1)Number類:
Number類用於儲存運算數,裡面包含int型別的numerator(分子)和denominator(分母)
public class Number {//運算數
int numerator;//分子
int denominator=1;//分母
public Number(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
public Number() {
}
}
(2)Expression類:
Expression用來生成四則運算表示式,裡面有運算數集合、符號集合、括號集合等。
public class Expression {//算術表示式
Random R=new Random();
boolean islegal=true;//判斷算式是否合法(不產生負數)
int countNumber=R.nextInt(3)+2;//運算數個數
ArrayList<Number> number2=new ArrayList<>();//複製運算數
ArrayList<Number> number=new ArrayList<>();//運算數
ArrayList<Integer>bracket=new ArrayList<>();//括號集合,表示括號位置,位於相同索引的運算數的左邊
static String Symbol="+-×÷";//所有運算子
ArrayList<Character> symbol=new ArrayList<>();//儲存運算子
ArrayList<Character> symbol2=new ArrayList<>();//複製運算子
Number answer=new Number();//運算結果
public Expression(int r) {
makenumber(r);//生成運算數集合
makeSymbol();//生成運算子集合
makeBracket();//生成括號陣列
answer=getAnswer(0,symbol.size());
if(answer.denominator==0){
islegal=false;
}
}
3、程式執行主要流程
五、程式碼說明
關鍵演算法:
1、Expression類下的getAnswer方法(根據儲存在集合中的運算數以及運算子和括號計算出答案)
(1)思路:
計算四則運算時採用人類的思維,即先算括號裡的算式或乘除運算子,再將算出來的結果與還未計算的運算數進行計算。
方法執行過程:
根據括號位置以及符號位置來優先計算算式中的一個符號兩邊的運算數,將計算過的兩個運算數移出運算數集合,再把運算結果放入集合中兩個運算數被移出之前的位置,最後將計算過的運算子移出運算子集合,迴圈往復,直到運算子集合為空或運算數只剩一個。
(2)程式碼:
public Number getAnswer(int begin, int end){
if(bracket.size()==0)//沒有括號時
return countExpression(begin,end);//直接計算
else{
//有括號時,先算括號
if(bracket.size()==2){//一個括號
countExpression(bracket.get(0),bracket.get(1)-1);//先算括號
return countExpression(0,symbol.size());//再算外面
}
else{
//判斷兩個括號是否有交集
if(bracket.get(0)<=bracket.get(2)&&bracket.get(1)>= bracket.get(3))//一個括號包另一個括號
{
countExpression(bracket.get(2),bracket.get(3)-1);//先算內括號
countExpression(bracket.get(0),bracket.get(1)-2);//再算外括號
return countExpression(0,symbol.size());
}
else{//無交集
countExpression(bracket.get(0),bracket.get(1)-1);//先算左括號
countExpression(bracket.get(2)-1,bracket.get(3)-2);//再算右括號
return countExpression(0,symbol.size());
}
}
}
}
private Number countExpression(int begin,int end) {//根據傳來的begin和end索引計算該索引內的算式,並將運算數替換為答案
Number sum = new Number(0, 1);
for (int i = begin; i < symbol.size()&&i<end; i++) {//第一遍遍歷先算乘除
switch (symbol.get(i)) {
case '×':
case '÷':
sum = count(number.get(i), number.get(i + 1), symbol.get(i));
number.set(i, sum);//替換算過的運算數
number.remove(i + 1);//移除另一個算過的運算數
symbol.remove(i);//移除算過的運算子
end--;//由於移除了運算子,end要減1
i--;//移除了運算數,i也要減1
if(sum.numerator<0||sum.denominator<=0){//運算過程中出現小於0的結果
islegal=false;//將標誌變數置為false
return sum;//直接停止計算
}
}
}
for (int i = begin; i < symbol.size()&&i<end; i++) {//順序計算加減
sum=count(number.get(i),number.get(i+1),symbol.get(i));
if(sum.denominator<=0||sum.numerator<0){//分母小於等於0或分子小於0直接退出
islegal=false;
return sum;
}
number.set(i, sum);
number.remove(i + 1);
symbol.remove(i);
i--;
end--;//同上,替換、移除運算數以及運算子
}
}
public static Number count(Number n1,Number n2,char c)//根據運算數和運算子計算結果
{
switch(c){
case '÷':
if(n2.numerator==0){//分母為0直接退出
Number nl=new Number(-1,-1);
return nl;
}
n1.numerator*=n2.denominator;
n1.denominator*=n2.numerator;
break;
case '×':
n1.denominator*=n2.denominator;
n1.numerator*=n2.numerator;
break;
case '+':
n1.numerator*=n2.denominator;
n2.numerator*=n1.denominator;//通分
n1.denominator*=n2.denominator;
n1.numerator+=n2.numerator;
break;
case '-':
n1.numerator*=n2.denominator;
n2.numerator*=n1.denominator;//通分
n1.denominator*=n2.denominator;
n1.numerator-=n2.numerator;
break;
}
n1=simplify(n1.numerator,n1.denominator);
return n1;
}
2、Correct類下的Correct方法(根據算式檔案和答案檔案統計對錯)
(1)思路:
用String接受檔案中的算式,再將算式裡的數字和運算子存入Expression類中,呼叫Expression類的getAnswer方法計算答案,最後與答案檔案中的答案進行比較。
(2)程式碼:
public Correct(String exerciseFile,String answerFile) throws IOException {
BufferedReader exf=new BufferedReader(new FileReader(exerciseFile));
BufferedReader ans=new BufferedReader(new FileReader(answerFile));//開啟檔案
int questonNumber=1;//題號
int point = 0;//用來跳過小數點
while (true) {
String expression=exf.readLine();//讀取表示式於expression中
String answer=ans.readLine();//讀答案
if(expression==null||answer==null)
break;
int i=0;
int j=0;
for(i=0;i<expression.length();i++){//跳過題號
if(expression.charAt(i)=='.')
break;
}
for(j=0;j<answer.length();j++){
if(answer.charAt(j)=='.')
break;
}
i++;跳過小數點
while(j<answer.length()&&(answer.charAt(j)>'9'||answer.charAt(j)<'0'))跳到答案字串數字初始斷
j++;
int begin=j;//記錄初始斷
while(j<answer.length()&&(answer.charAt(j)<='9'&&answer.charAt(j)>='0'||
answer.charAt(j)=='’'||answer.charAt(j)=='/'))//跳到答案陣列尾端
j++;
Number rightAnswer=creatExpression(expression.substring(i));//建立Expression類並計算正確答案
Number pathAnswer=getNumber(answer.substring(begin,j));//得到檔案中的答案
if(isright(rightAnswer,pathAnswer)==true){//對比答案
cor++;//
correctQuestion.add(questonNumber);
}
else
{
wro++;
wrongQuestion.add(questonNumber);
}
questonNumber++;
}
exf.close();
ans.close();
writeResult(cor,wro);
}
六、測試執行
1、測試答案的正確性
由於該程式用java語言實現,全部採用自己建立的類來儲存資料,要計算時才將資料從集合中拿出來計算,並用多層if和switch語句來判斷運算子和運算數的索引來進行計算,答案難免會有差錯,這使得測試答案是否正確成了難題,好在合作伙伴用python實現了測試答案正確性的功能。
原理如下:
讀取題目檔案,對題目表示式的字串進行分割去掉序號,再將表示式中如"×"、"÷"的符號替換為語言能識別的運算子號。然後讀取答案檔案,對答案字串進行相同的操作。建立正確和錯誤的列表以統計正誤題目,表長就為正誤題目數。最後將兩個列表輸出到新文件中,在控制檯上列印檢查完畢提示答案比對完成。
python測試程式碼:
def check(exercisefile,answerfile):
with open(f'{exercisefile}','r',encoding='utf-8') as eF:
exercise=eF.readlines()
with open(f'{answerfile}','r',encoding='utf-8') as aF:
answer=aF.readlines()
correctList=[]
wrongList=[]
for index,(exercise,answer) in enumerate(zip(exercise,answer),start=1):
split_exercises=exercise.split('.')
split_ans = answer.split('.')
exp = split_exercises[1].replace('’','+').replace('÷','/').replace('×','*').replace('=','')
ans = split_ans[1].replace('’','+')
# if split_exercises[1]==split_ans[1]:
if format(eval(exp),'.10f')==format(eval(ans),'.10f'):
correctList.append(index)
else:
wrongList.append(index)
with open('Grade.txt','w')as gF:
gF.write(f'Correct:{len(correctList)},({','.join(map(str,correctList))})\n')
gF.write(f'Wrong:{len(wrongList)},({','.join(map(str,wrongList))})')
print('檢查完畢')
進過多輪測試,四則運算生成的答案均全對,以此確定我的程式是正確的
2、測試用例
測試用例1:生成10000道10以內的四則運算題,即n=10000,r=10
測試用例2:生成10000道30以內的四則運算題,即n=10000,r=30
測試用例3:生成100道2以內的四則運算題,即n=100,r=2
測試用例4:生成50道1以內的運算題,即n=50,r=1
測試用例5:將測試1生成的10000道練習題以及答案檔案傳入命令列,統計對錯數量
測試用例6:將測試用例5的Answers.txt檔案裡的內容稍作修改,統計對錯數量
測試用例7:生成100道題目,並將生成的Exercises.txt檔案與空白文件比較,統計對錯數目
測試用例8:生成100道題目,並將生成的Answers.txt檔案與空白文件比較,統計對錯數目
測試用例9: 輸入錯誤引數
測試用例10: 輸入不存在的檔案
七、專案小結
1、本次專案實現了作業要求的所有功能。
2、結對開發時缺少交流,未確定最終思路,導致開發效率較低。
3、寫程式碼時要儘量多測試,邊開發邊寫測試程式碼,測試要儘量全面,否則後面程式碼堆起來容易出大問題。
4、結對做專案比單獨做專案效率有了顯著的提升,在開發遇到瓶頸時合作伙伴可以提供新的思路。同時兩個人可以關注到更多的問題,時程式設計的更為全面,在測試程式方面也更加細膩。
5、本次結對兩人都忠於開發,但缺少交流,建議多多加強交流,提高開發效率。