四則運算
功能
- [完成] 使用 -n 參數控制生成題目的個數
- [完成] 使用 -r 參數控制題目中數值的范圍, 。該參數可以設置為1或其他自然數。
- [完成] 生成的題目中計算過程不能產生負數
- [完成] 生成的題目中如果存在形如e1 ÷ e2的子表達式,那么其結果應是真分數。
- [完成] 程序一次運行生成的題目不能重復,生成的題目存入執行程序的當前目錄下的Exercises.txt文件
- [完成] 每道題目中出現的運算符個數不超過3個
- [完成] 在生成題目的同時,計算出所有題目的答案,並存入執行程序的當前目錄下的Answers.txt文件
- [完成] 程序應能支持一萬道題目的生成。
- [完成] 程序支持對給定的題目文件和答案文件,判定答案中的對錯並進行數量統計
設計
表達式:
我們平常見到的表達式:1+2×3÷(4-5)+6是一種中綴表達式,轉化為后綴表達式就是123×45-÷+6+,還有一種前綴表達式就詳細說了。一條表達式就可以用一顆二叉樹來表示,這個種表達式樹定義以下性質
- 非葉子節點就是算術符號,葉子節點就是數
- 非葉子節點的左孩子和右孩子非空
無論前綴,中綴,還是后綴,只是訪問的時機不一樣,上面表達式轉化為二叉樹就是

綠色為符號節點,紅色為數字節點,所以如果限定符號節點數不超過3個,那就是用隨機函數,隨機生成一個數,在構建一棵子樹的時候應該算出子樹的結果和樹高
負數:
負數的產生是因為減法運算,在上面的算數中4-5會產生負數,在計算符號節點的結果的時候,需要判斷一下結果是否是負數,如果是負數,就取絕對,就是將左右子樹互換就行

表達式1+2×3÷(4-5)+6在計算4-5的時候發現是-1,然后對調左右子樹,就相當於取了絕對值
分數,整數的表示
我的思路就是將整數,也當做分數來計算,然后在輸出的時候將分數轉化為整數,或者分數的形式,然后定義一個類來表示分數,這個類應該有加減乘除的方法,並且在運算的過程中需要保證最簡
判斷是否重復
遞歸判斷兩棵表達式樹k1,k2, 如果k1->left == k2->left && k1->right == k2->right,然后就可以判定k1,k2是兩棵相同的樹,也就是表達式一樣,如果不相同並且符號是+或者×的時候,就判斷k1->left == k2->right && k1->right == k2->left ,如果滿足,也是相同的一個表達式樹,其他都是不相同的樹,比如

1+2×3÷(5-4)+6 跟 6+3×2÷(5-4)+1 就是一樣的表達式
代碼
分數:Fraction
定義一個分數類,里面應該需要加減乘除
/**
* IntelliJ IDEA 18
* Created by Pramy on 2018/9/16.
*/
public class Fraction {
/**
* 分子
*/
private int a;
/**
* 分母
*/
private int b;
public Fraction(String string) {
string = string.trim();
int a, b;
int cc = string.indexOf("'");
int bb = string.indexOf("/");
if (cc != -1) {
int c = Integer.valueOf(string.substring(0, cc));
b = Integer.valueOf(string.substring(bb + 1));
a = c * b + Integer.valueOf(string.substring(cc + 1, bb));
} else if (bb != -1) {
b = Integer.valueOf(string.substring(bb + 1));
a = Integer.valueOf(string.substring(0, bb));
} else {
a = Integer.valueOf(string);
b = 1;
}
adjust(a,b);
}
public Fraction(int a, int b) {
adjust(a,b);
}
private void adjust(int a, int b) {
if (b == 0) {
throw new RuntimeException("分母不能為0");
}
//記錄負數的標志
int isNegative = (a ^ b) >>> 31 == 1 ? -1 : 1;
a = Math.abs(a);
b = Math.abs(b);
int c = gcd(a, b);
//保證只有a才會小於0
this.a = a / c * isNegative;
this.b = b / c;
}
/**
* 加法 a + b
* @param fraction b
* @return a - b
*/
public Fraction add(Fraction fraction) {
return new Fraction(this.a * fraction.b + fraction.a * this.b,
this.b * fraction.b);
}
/**
* 減法 a - b
* @param fraction b
* @return a - b
*/
public Fraction subtract(Fraction fraction) {
return new Fraction(this.a * fraction.b - fraction.a * this.b,
this.b * fraction.b);
}
/** 乘法 a x b
* @param fraction b
* @return a x b
*/
public Fraction multiply(Fraction fraction) {
return new Fraction(this.a * fraction.a,
this.b * fraction.b);
}
/** 除法 a / b
* @param fraction b
* @return a / b
*/
public Fraction divide(Fraction fraction) {
return new Fraction(this.a * fraction.b, b * fraction.a);
}
/**
* 絕對值
*/
public void abs() {
this.a = Math.abs(this.a);
this.b = Math.abs(this.b);
}
/**是否是負數
* @return a < 0
*/
public boolean isNegative() {
return a < 0;
}
private int gcd(int a, int b) {
int mod = a % b;
return mod == 0 ? b : gcd(b, mod);
}
}
表達式:Expression
表達式里面有兩個內部類來表示節點
葉子節點是分數節點:Node
static class Node implements Cloneable {
//表達式結果
Fraction result;
Node right;
Node left;
int high;
//```以下省略
}
非葉子節點就是符號節點:SymbolNode
static class SymbolNode extends Node {
//符號
String symbol;
}
構建表達式
第一種構建方式就是給定非葉子結點的數量,隨機生成
/**
* 根據符號數來隨機構造表達樹
* @param sum 符號數
* @return node
*/
private Node build(int sum) {
//如果是0就構造葉子節點
if (sum == 0) {
return new Node(createFraction(bound), null, null, 1);
}
ThreadLocalRandom random = ThreadLocalRandom.current();
//1.否則就是構造符號節點
final SymbolNode parent = new SymbolNode(null, null, SYMBOLS[random.nextInt(4)]);
int left = random.nextInt(sum);
//2.遞歸下去構造左孩子和右孩子
parent.left = build(left);
parent.right = build(sum - left - 1);
//3.然后計算結果
Fraction result = calculate(parent.symbol, parent.left.result, parent.right.result);
//4.如果是負數就取絕對值,然后交換左右孩子
if (result.isNegative()) {
Node tmp = parent.left;
parent.left = parent.right;
parent.right = tmp;
result.abs();
}
parent.result = result;
//5.計算樹高
parent.high = Math.max(parent.left.high, parent.right.high) + 1;
return parent;
}
第二種就是給定特定的中綴表達式來構建
/** 根據 string 表達式構建樹
* @param expression 表達式
* @return node
*/
private Node build(String expression) {
String[] strings = expression.split(" ");
Stack<Node> nodeStack = new Stack<>();
Stack<String> symbolStack = new Stack<>();
for (String string : strings) {
//1.如果是數字就構建葉子節點並且進棧
if (!isSymbol(string)) {
nodeStack.push(new Node(new Fraction(string), null, null, 1));
} else {
//比較符號棧中的頂層符號如果需要出棧
while (!symbolStack.isEmpty() && !tryPush(string, symbolStack.peek())) {
String symbol = symbolStack.pop();
if (symbol.equals(LEFT_BRACKETS) && string.equals(RIGHT_BRACKETS)) {
break;
}
push(symbol, nodeStack);
}
//如果符號不是")"就進棧
if (!string.equals(RIGHT_BRACKETS)) {
symbolStack.push(string);
}
}
}
//剩下的符號都推進棧里面
while (!symbolStack.isEmpty()) {
push(symbolStack.pop(), nodeStack);
}
return nodeStack.pop();
}
/**構造一個符號node推入棧
* @param symbol 符號
* @param nodeStack 棧
*/
private void push(String symbol, Stack<Node> nodeStack) {
if (!symbol.equals(LEFT_BRACKETS)) {
Node right = nodeStack.pop();
Node left = nodeStack.pop();
SymbolNode node = new SymbolNode(right, left, symbol);
node.result = calculate(symbol, left.result, right.result);
node.high = Math.max(left.high, right.high) + 1;
nodeStack.push(node);
}
}
/**
* 是否可以入棧
* @param s 准備入棧的復發
* @param target 棧頂符號元素
* @return true 能入棧 ,false 不能入棧
*/
private boolean tryPush(String s, String target) {
return s.equals(LEFT_BRACKETS) || (isTwo(s) && isOne(target)) ||
(!s.equals(RIGHT_BRACKETS) && target.equals(LEFT_BRACKETS));
}
/**
* 是否是符號
* @param s s
* @return boolean
*/
private boolean isSymbol(String s) {
return s.equals(ADD) || s.equals(SUBTRACT) || s.equals(MULTIPLY) || s.equals(DIVIDE)
|| s.equals(LEFT_BRACKETS) || s.equals(RIGHT_BRACKETS);
}
功能:Function
輸出表達式到Exercises.txt和輸出答案到Answers.txt
/**
* @param sum 符號數量
* @param bound 范圍
*/
public void outputExercises(int sum, int bound) {
if (bound <= 0 || sum <= 0) {
throw new RuntimeException("bound or sun must greater than 0");
}
Set<Expression> set = new HashSet<>();
try (BufferedWriter exercisesWriter = new BufferedWriter(new FileWriter("Exercises.txt"));
BufferedWriter answerWriter = new BufferedWriter(new FileWriter("Answers.txt"))
) {
for (int i = 1; set.size()< sum;) {
try {
//因為在運算的過程中會出現n÷0的情況,這時候就會拋異常
Expression expression = new Expression(3, bound);
if (!set.contains(expression)) {
exercisesWriter.write(i + "." + expression.toString() + "\n");
answerWriter.write(i + "." + expression.getResult() + "\n");
set.add(expression);
i++;
}
} catch (RuntimeException ignored) {
}
}
exercisesWriter.flush();
answerWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
根據給定的表達式和結果輸出正確的題數和錯誤的題數
/**
* 輸出結果
* @param exercisePath 表達式文件路徑
* @param answerPath 結果文件路徑
* @param gradePath 輸出結果文件路徑
*/
public void outputGrade(String exercisePath, String answerPath, String gradePath) {
try (BufferedReader exReader = new BufferedReader(new FileReader(exercisePath));
BufferedReader anReader = new BufferedReader(new FileReader(answerPath));
BufferedWriter gradeWriter = new BufferedWriter(new FileWriter(gradePath))
) {
String ex, an;
int c = 0, w = 0;
StringBuilder correct = new StringBuilder("Correct: %d (");
StringBuilder wrong = new StringBuilder("Wrong: %d (");
while ((ex = exReader.readLine()) != null && (an = anReader.readLine()) != null) {
int exPoint = ex.indexOf(".");
int anPoint = an.indexOf(".");
if (exPoint != -1 && anPoint != -1) {
int i = Integer.valueOf(ex.substring(0,exPoint).trim());
Expression expression = new Expression(ex.substring(exPoint + 1));
Fraction answer = new Fraction(an.substring(anPoint + 1));
if (expression.getResult().equals(answer.toString())) {
c++;
correct.append(" ").append(i);
if (c % 20 == 0) {
correct.append("\n");
}
} else {
w++;
wrong.append(" ").append(i);
if (w % 20 == 0) {
wrong.append("\n");
}
}
}
}
gradeWriter.write(String.format(correct.append(" )\n").toString(),c));
gradeWriter.write(String.format(wrong.append(" )\n").toString(),w));
gradeWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
測試
表達式的輸出
隨機輸出10條20范圍以內的表達式和答案
private Function function = new Function();
@Test
public void outputExercises() {
function.outputExercises(10,20);
}
得到測試結果:
表達式:
1.2'13/17 × ( 10 + 11 )
2.16 - 6
3.( 8 - 1'5/9 ) ÷ ( 16 ÷ 5'9/17 )
4.1'5/14 ÷ ( 1/6 × 3 )
5.4'1/9 ÷ 15
6.11 - 6/19 - 4'1/3 ÷ 1
7.9 - 4
8.1 ÷ ( ( 8/19 - 0 ) ÷ 2 )
9.12 - 9 ÷ ( 1'1/15 + 5 )
10.9 ÷ ( 15 - 9 - 11/15 )
結果:
1.58'1/17
2.10
3.2'139/612
4.2'5/7
5.37/135
6.6'20/57
7.5
8.4'3/4
9.10'47/91
10.1'56/79
隨機輸出10000條20范圍以內的表達式和答案:
由於太多,給個連接打開
結果的對比
private Function function = new Function();
@Test
public void outputGrade() {
function.outputGrade("Exercises.txt","Answers.txt","Grade.txt");
}
對比上面輸出的10條表達式和答案,結果
Correct: 10 ( 1 2 3 4 5 6 7 8 9 10 )
Wrong: 0 ( )
然后故意修改第2道題的結果為100,和第7道題為50,然后再重新運行一遍,結果如下
Correct: 8 ( 1 3 4 5 6 8 9 10 )
Wrong: 2 ( 2 7 )
然后對比10000道題的結果,其中我在答案文件中,隨機改變了正確的數值,得到以下對比結果
PSP
| PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
|---|---|---|---|
| Planning | 計划 | 30 | 60 |
| · Estimate | · 估計這個任務需要多少時間 | 30 | 60 |
| Development | 開發 | 870 | 1415 |
| · Analysis | · 需求分析 (包括學習新技術) | 30 | 60 |
| · Design Spec | · 生成設計文檔 | 30 | 150 |
| · Design Review | · 設計復審 (和同事審核設計文檔) | 120 | 120 |
| · Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 30 | 20 |
| · Design | · 具體設計 | 30 | 120 |
| · Coding | · 具體編碼 | 360 | 1200 |
| · Code Review | · 代碼復審 | 30 | 45 |
| · Test | · 測試(自我測試,修改代碼,提交修改) | 240 | 300 |
| Reporting | 報告 | 80 | 80 |
| · Test Report | · 測試報告 | 20 | 40 |
| · Size Measurement | · 計算工作量 | 30 | 20 |
| · Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 30 | 20 |
| 合計 | 980 | 2155 |
總結
與柯文朗同學一起討論編寫程序,我主要負責寫代碼,然后我們一起討論,有時候自己卡住的時候,別人的建議會讓自己思維變得更加清晰,在寫代碼的過程中,兩個人的思維碰撞會產生意外的結果,有一些細節的地方自己沒有辦法想到,就可以由另外一個人填補,兩個人提高了程序的設計與編寫的速度,我們在討論如果碰到負數的時候應該怎么辦的時候,我的想法本來是將減號換為加號,雖然可行,但是在一定程度上干擾了隨機生成符號的這設計理念,在我猶豫的過程中,柯文朗同學提出取絕對值得想法,將左右兩個表達式互換,最終商討覺得他的想法比較好,然后采納了他的想法
