四則運算表達式求解
這次寫了一個能夠實現簡單四則運算(+,-,*,/,含括號)的小程序。首先分析一下功能需求與限定吧。
需求與限定
- 輸入四則運算表達式,要求用戶輸入其計算的結果,程序能夠判斷用戶輸入是否正確。
- 算式輸入的數據為正整數或者正分數,用戶輸入計算結果為整數或分數(分數以“a/b”的形式表示)。
- 統計用戶輸入正確的答案個數以及錯誤的答案個數。
分析
首先不難想到,程序的整體設計思路分為兩部分,一部分是中綴表達式轉換為后綴表達式,另一部分就是后綴表達式的計算。但在實現的過程中還有一個難點需要注意,就是分數在整個程序運行過程中的存儲以及計算。由於輸入的限定,分數一定是以“a/b”的形式表示的,所以我們可以將這種表示直接看做兩個數相除的形式。由此,解決了輸入是分數的問題。計算過程中的分數存儲以及分數與整數的計算問題,我們可以通過將整數分數進行規格化的存儲來解決,即:我們建立一個表示分數的結構體如下,將整數和分數都以該結構體的形式存儲,其中numerator表示分子,denominator表示分母。對於分數a/b的存儲方式就是numerator = a,denominator = b,整數c的存儲方式就是numerator = c, denominator = 1。通過這樣統一規范的存儲就使得我們在整個計算的過程變得輕松的多了。
1 typedef long long ll; 2 struct num{ 3 ll numerator, denominator; 4 num(){numerator = 0; denominator = 1;} 5 num(int n) {numerator = n; denominator = 1;} 6 num(int n,int d) {numerator = n; denominator = d;} 7 8 void operator = (num x) 9 { 10 numerator = x.numerator; 11 denominator = x.denominator; 12 } 13 };
實現過程
在實現之初,我們先給出需要的變量的定義,如下:
1 #define maxn 1005 2 3 char nifix[maxn], post[maxn]; 4 char ans[maxn]; 5 int cnt_right, cnt_wrong; 6 bool error; 7 num res, rst;
其中,nifix為輸入的算式;
post為轉化后的后綴表達式;
ans表示用戶輸入的結果;
cnt_right, cnt_wrong分別表示用戶回答正確的個數以及回答錯誤的個數;
error標志程序運行過程中是否出錯,如:輸入的表達式不符合規范,出現除零的情況等;
res表示程序計算出的算式答案;
rst表示用戶輸入的答案轉換為規范化的形式的結果。
中綴表達式轉后綴表達式
有了上述一系列的定義,我們就可以開始實現我們的程序了。上面提到程序的實現主要分為兩個部分組成,首先我們實現第一部分中綴表達式轉換為后綴表達式,中綴表達式轉后綴表達式的規則如下:
- 遇到操作數時,直接輸出到后綴表達式。
- 遇到左括號時,直接入棧。
- 遇到右括號時,出棧,將棧頂元素直接添加到后綴表達式后,直到遇到左括號(左括號出棧但不添加到后綴表達式中)。
- 遇到操作符時,若棧為空,則直接入棧;若棧不空,則比較棧頂操作符和該操作符優先級,若棧頂操作符優先級(*,/優先級大於+,-優先級)大於等於該操作符,則出棧並輸出到后綴表達式中。重復操作直至棧空或不符合出棧規則。然后將該操作符入棧。
- 最后將棧中元素依次出棧輸出到后綴表達式中。
上述的規則能夠實現將一個中綴表達式轉化為后綴表達式。但是,我們不能保證用戶輸入的合法性,及用戶可能輸入不符合中綴表達式規則的式子如:12*]-3。所以我們的程序必須實現對錯誤的判斷功能,不能因為錯誤的輸入導致程序崩潰或者計算出錯誤的答案等等。根據我的總結,輸入的中綴表達式的錯誤可以分為幾類:
- 輸入字符串中包含非表達式的字符如:a, b, c, [, &, .等等。
- 輸入的字符串中括號不匹配,包括缺少左括號如:1+2)*3,缺少右括號如:1*(2+3等。
- 連續兩個符號在一塊包括++,+-等但不包括‘(’左邊為‘+’‘-’‘*’‘/’和‘)’右邊為‘+’‘-’‘*’‘/’。
有了以上的分析我們就可以開始具體實現了。首先我們要實現幾個功能性函數,如下:
1 bool isNum(char x) //判斷是否是數字 2 { 3 return (x >= '0' && x <= '9'); 4 } 5 6 bool isOp(char x) //判斷是否是操作符 7 { 8 return (x == '+' || x == '-' || x == '*' || x == '/' || x == '(' || x == ')'); 9 } 10 11 int priority(char x) //返回一個操作符的優先級 12 { 13 if (x == '-' || x == '+') 14 return 1; 15 else if (x == '*' || x == '/') 16 return 2; 17 else if (x == '(') 18 return 0; 19 else 20 return -1; 21 }
然后就是中綴轉后綴的主函數了,實現如下:
1 bool nifix_to_post() 2 { 3 memset(post, 0, sizeof(post)); 4 stack<char> s; //操作符棧,用來壓操作符 5 /* ************************************************************************************************ 6 # 由於操作數是多位的,所以我們逐位做一個累加暫存的工作,當遇到操作符時,代表此操作符前的數暫存完畢, 7 # 需要將其入棧,但也不是所有操作符前都有操作數,如'*('等,所以我們需要一個標志位來表示某個操作符前 8 # 是否進行過暫存操作數的處理。 9 *///************************************************************************************************** 10 bool havenum = false; 11 int tmp = 0, pos = 0; //tmp為暫存多位數的變量,pos為后綴數組存儲位置 12 for (int i = 0; nifix[i] != '\0'; i++) //循環遍歷中綴數組 13 { 14 if (isNum(nifix[i])) //若當前字符是數字,則進行暫存操作。並標識已有操作數 15 { 16 havenum = true; 17 tmp = tmp*10 + (nifix[i]-'0'); 18 } 19 else if (isOp(nifix[i])) //若當前字符是操作符則進行如下操作 20 { 21 //中綴表達式合法性判斷,判斷是否有連續兩個字符相連,若有則代表出錯 22 if (isOp(nifix[i-1]) && nifix[i-1] != ')' && nifix[i] != '(') 23 return true; 24 //如果操作符前有操作數,則將得到的操作數輸出至后綴表達式 25 if (havenum) 26 { 27 havenum = false; 28 post[pos++] = tmp + '0'; 29 tmp = 0; 30 } 31 //如果操作符為右括號,則按規則進行出棧如后綴表達式等操作 32 if (nifix[i] == ')') 33 { 34 if (s.empty()) //中綴表達式合法性判斷,判斷括號匹配 35 return true; 36 while(s.top() != '(') 37 { 38 post[pos++] = s.top(); 39 s.pop(); 40 if (s.empty()) //中綴表達式合法性判斷,判斷括號匹配 41 return true; 42 } 43 s.pop(); 44 } 45 else if (nifix[i] == '(') //如果是左括號則直接入棧 46 s.push(nifix[i]); 47 else //如果是+-*/則按規則操作 48 { 49 while (!s.empty() && priority(nifix[i]) <= priority(s.top())) 50 { 51 post[pos++] = s.top(); 52 s.pop(); 53 } 54 s.push(nifix[i]); 55 } 56 } 57 else //中綴表達式合法性判斷,判斷是否含非法符號 58 return true; 59 } 60 //若有操作數,則將其輸出至后綴表達式 61 if (havenum) 62 { 63 havenum = false; 64 post[pos++] = tmp + '0'; 65 tmp = 0; 66 } 67 //將棧中操作符一次輸出到后綴表達式中 68 while(!s.empty()) 69 { 70 if (s.top() == '(') //中綴表達式合法性判斷,判斷括號匹配 71 return true; 72 post[pos++] = s.top(); 73 s.pop(); 74 } 75 return false; 76 }
至此,中綴表達式轉后綴表達式的算法就完成了,同時在這個過程中還完成了對輸入中綴表達式合法性的判斷。
后綴表達式計算
在這一部分,我們要完成的事情就比較簡單了。唯一要注意的就是對操作數進行分數形式的計算,由於整數和分數都被我們表示為分數的形式,所以對兩個數a/b和c/d,這兩個數的四則運算操作規則如下:
有了運算規則,我再簡要的闡述一下后綴表達式求值的方法:
- 從左到右掃描后綴表達式,遇到運算符就把表達式中該運算符前面兩個操作數取出並運算,然后把結果帶回后綴表達式;繼續掃描直到后綴表達式最后一個表達式。
在后綴表達式計算過程中,我們同樣需要判斷后綴表達式計算的合法性,經過分析,我得到如下幾種引起錯誤的可能:
- 當遇到一個操作符時,棧內沒有足夠的操作數供以運算。
- 當操作符都讀取完畢后,棧內有超過一個的操作數。
- 除數為0。
之后,我們就可以開始編寫后綴表達式求值部分的算法,由上述分數運算規則,並且由於我們所設計的結構體分子分母是分開的,所以還需要解決分子分母約分的問題,所以我們首先需要編寫求分子分母最大公約數的函數,我們用輾轉相除法實現如下(具體算法描述不在此闡述):
1 ll gcd(ll m, ll n) 2 { 3 ll tmp; 4 tmp = m % n; 5 while(tmp) 6 { 7 m = n; 8 n = tmp; 9 tmp = m % n; 10 } 11 return n; 12 }
有了上述函數,就可以開始實現后綴表達式計算的過程了,其函數實現如下:
1 bool cal_result() 2 { 3 stack<num> s; //用來存放操作數的棧 4 for (int i = 0; i < strlen(post); i++) //循環遍歷后綴表達式 5 { 6 if (!isOp(post[i])) //如果是數字則直接入棧 7 { 8 num tmp(post[i]-'0', 1); 9 s.push(tmp); 10 } 11 else 12 { 13 //取出兩個操作數,如果操作數數量不夠,則出錯,返回 14 if (s.empty()) 15 return true; 16 num b = s.top(); s.pop(); 17 if (s.empty()) 18 return true; 19 num a = s.top(); s.pop(); 20 num c; 21 22 if (post[i] == '+') //操作符是'+',進行上述規則運算 23 { 24 c.numerator = a.numerator * b.denominator + b.numerator * a.denominator; 25 c.denominator = a.denominator * b.denominator; 26 } 27 else if (post[i] == '-') //操作符是'-',進行上述規則運算 28 { 29 c.numerator = a.numerator * b.denominator - b.numerator * a.denominator; 30 c.denominator = a.denominator * b.denominator; 31 } 32 else if (post[i] == '*') //操作符是'*',進行上述規則運算 33 { 34 c.numerator = a.numerator * b.numerator; 35 c.denominator = a.denominator * b.denominator; 36 } 37 else if (post[i] == '/') //操作符是'/',進行上述規則運算 38 { 39 if (b.numerator == 0) //如果除的是0,則出現除法錯誤,返回 40 return true; 41 c.numerator = a.numerator * b.denominator; 42 c.denominator = a.denominator * b.numerator; 43 } 44 else //其他情況則出錯 45 return true; 46 if (c.numerator != 0) //若結果不為0,則對分子分母約分 47 { 48 ll div = gcd(c.denominator, c.numerator); 49 c.denominator /= div; 50 c.numerator /= div; 51 } 52 s.push(c); //將約分后結果入棧 53 } 54 } 55 if (s.size() > 1) //如果所有操作符都進行相應操作數后棧內還有多個元素,則說明出錯,返回 56 return true; 57 res = s.top(); //保存結果 58 s.pop(); 59 if (res.denominator < 0) //化簡結果,將分母的負號移到分子上 60 { 61 res.numerator = -res.numerator; 62 res.denominator = -res.denominator; 63 } 64 return false; 65 }
經過上述的操作,我們將一個輸入的中綴表達式成功計算出結果,接下來要做的就是編寫接收用戶輸入並進行判斷了。
用戶輸入結果轉化
用戶輸入的答案理論上講是一個只包含數字和'/'的字符串,對於用戶的輸入,我們只能通過字符串接收,然后將其轉化成我們需要的形式,實現方式也很簡單,當然我們也要對用戶輸入答案的合法性進行判斷,代碼如下:
1 bool trans_ans() 2 { 3 int i = 0; 4 ll tmp = 0; 5 //兩個標志位分別標志用戶輸入的分子分母是否為負數 6 bool num_flag = false, deno_flag = false; 7 //先判斷分子是否為負 8 if (ans[i] == '-') 9 { 10 num_flag = true; 11 i++; 12 } 13 //接收第一個數字 14 while(isNum(ans[i])) 15 { 16 tmp = tmp * 10 + (ans[i] - '0'); 17 i++; 18 } 19 //若第一個數為負數,則將轉化過來的整數取負 20 if (num_flag) 21 tmp = -tmp; 22 //保存分子 23 rst.numerator = tmp; 24 //分母賦初值為1 25 rst.denominator = 1; 26 tmp = 0; 27 //判斷是否是分數 28 if (ans[i] == '/') 29 { 30 //判斷分母是否為負數 31 if (ans[++i] == '-') 32 { 33 deno_flag = true; 34 i++; 35 } 36 //接收第二個數 37 while(isNum(ans[i])) 38 { 39 tmp = tmp * 10 + (ans[i] - '0'); 40 i++; 41 } 42 //若第二個數為負數,則將轉化過來的整數取負 43 if (deno_flag) 44 tmp = -tmp; 45 //保存分母 46 rst.denominator = tmp; 47 } 48 //若分母為0,則用戶輸入的結果是非法的 49 if (rst.denominator == 0) 50 return true; 51 //若此時沒有遍歷所有的字符,則說明輸入是非法的 52 if (i != strlen(ans)) 53 return true; 54 //化簡分母的負號,將其移至分子 55 if (rst.denominator < 0) 56 { 57 rst.numerator = -rst.numerator; 58 rst.denominator = -rst.denominator; 59 } 60 //若用戶輸入的分子分母都是負數,則用戶輸入不符合規范,我們取分母為0(因為計算結果不可能出現分母為0的狀況)標志這種情況的發生 61 if (num_flag && deno_flag) 62 rst.denominator = 0; 63 return false; 64 }
主函數的實現
最后就剩下主函數了,其具體實現如下:
1 int main() 2 { 3 //計數器請0 4 cnt_right = cnt_wrong = 0; 5 cout << "Now test begins..." << endl; 6 while(cin >> nifix) 7 { 8 error = nifix_to_post(); //中綴轉后綴,並接受錯誤信息 9 if (error) //若過程出錯,則輸出錯誤信息 10 { 11 cout << "There is an illegal equation!" << endl; 12 return 0; 13 } 14 15 //test_post(); 16 17 error = cal_result(); //后綴表達式計算,並接受錯誤信息 18 if (error) //若過程出錯,則輸出錯誤信息 19 { 20 cout << "There is something wrong during computing..." << endl; 21 return 0; 22 } 23 24 cout << "Please enter your answer: "; 25 cin >> ans; //接受用戶答案 26 error = trans_ans(); //答案翻譯, 並接受錯誤信息 27 if (error) //若過程出錯,則輸出錯誤信息 28 { 29 cout << "You have inputted an illegal answer!" << endl; 30 return 0; 31 } 32 //若用戶輸入和城西計算答案的分子分母相等或分子均為0,則說明用戶輸入答案正確 33 if ((rst.denominator == res.denominator && rst.numerator == res.numerator) || (rst.numerator == res.numerator && rst.numerator == 0)) 34 { 35 cnt_right++; 36 cout << "Right answer!" << endl; 37 } 38 //否則答案錯誤,程序輸出正確答案 39 else 40 { 41 cnt_wrong++; 42 cout << "You have entered a wrong answer. The right answer is "; 43 if (res.denominator == 1) 44 cout << res.numerator << "." << endl; 45 else 46 cout << res.numerator << "/" << res.denominator << "." << endl; 47 } 48 } 49 cout << "test ends..." << endl; 50 //輸出統計結果 51 cout << "You have answered " << cnt_right+cnt_wrong << " problems in total "; 52 cout << "in which " << cnt_right << " are correct and " << cnt_wrong << " are wrong." << endl; 53 return 0; 54 }
總結
這次寫的這個程序有一定優點,也存在一些缺點。我認為優點就是整個程序的魯棒性很好,不會意外崩潰,對於所有可能出現的錯誤有較全面的考慮,並都對其進行了處理。缺點也有一些,比如程序在輸入時不支持負數的運算。所以整個程序還是有提升的空間的。在整個寫碼的過程中,也學到了很多,也加強了我對算法實現的能力,總體的收獲還是很大的。