作業 20180925-6 四則運算試題生成
作業要求:【https://edu.cnblogs.com/campus/nenu/2018fall/homework/2148】
作業地址:【https://git.coding.net/zhangjy982/ArithMetic.git】
要求1:
一、重點和難點
1.功能1的重點和難點
(1).隨機出題
題目要求出題隨機,其中包括操作數隨機和操作符隨機,隨機產生這兩項之後再組成用於計算的式子。下面是我取隨機操作數和操作符的代碼:
1 public List<string[]> getRandomNum() // 獲取4個1-20之間的隨機操作數 2 {3 List<string[]> randomNums = new List<string[]>(); 4 string[] randomFractions = new string[4];// 保持分數的形式方便展示 5 string[] randomRealNum = new string[4]; // 把分數轉化為小數方便計算(功能4中用得到) 6 7 string number1 = ""; 8 string number2 = ""; 9 for (int i = 0; i < 4; i++) 10 { 11 int flag = rm.Next(1, 11); 12 switch (flag % 2) 13 { 14 case 0: 15 number1 = rm.Next(1, 20).ToString(); 16 randomFractions[i] = number1;17 randomRealNum[i] = number1; 18 break; 19 case 1: 20 number1 = rm.Next(1, 20).ToString(); 21 number2 = rm.Next(1, 20).ToString(); 22 randomFractions[i] = number1 + "/" + number2; 23 randomRealNum[i] = (Convert.ToDecimal(number1)/Convert.ToDecimal(number2)).ToString(); 24 break; 25 } 26 } 27 randomNums.Add(randomFractions); 28 randomNums.Add(randomRealNum); 29 return randomNums; 30 }
1 public string[] getRandomOperator() // 獲取3個隨機操作符符 2 { 3 string[] operators = new string[4]; 4 string[] randomOperators = new string[3]; 5 operators[0] = "+"; 6 operators[1] = "-"; 7 operators[2] = "*"; 8 operators[3] = "/"; 9 for (int i = 0; i < 3; i++) 10 { 11 randomOperators[i] = operators[rm.Next(0, 4)]; 12 } 13 return randomOperators; 14 }
功能1的運行截圖:(寫博客已完成4個功能,所以這裏顯示有分數表示,因為分數的“/“號和運算符“/”除號容易使公式不明晰,所以對象之間加了空格)
2.功能1的編程收獲
因為測試和熟練度的問題,和結對夥伴商量之後從Python語言轉到了C#,對C#比Python要熟悉很多,所以剛開始做功能1比較得心應手,之前用Python的時候少了很多面向對象的思想和方法,把功能都寫在了一起。現在開始結對編程對代碼的可讀性要求非常高,因此在功能1中就開始註意一些代碼的復用和重用,把功能盡量細化,每一個小功能對應一個方法,類似的幾個方法寫成一個類,盡量做到高內聚、低耦合,對以後程序的進一步開發也有很多好處;
3.功能2的重點和難點
(1).生成括號的位置
因為括號的生成位置不固定,一個式子中可能有1個或2個括號,這些括號的位置也各不相同,但是操作數只有4個,我們討論之後決定把這些括號的可能位置枚舉出來,情況總共有10種,我們使用switch case將這些情況都枚舉出來,隨機生成1~100之內的整數,然後用整數%10可以得到10個case,進而完成括號位置的生成;
(2).括號的匹配及運算的優先級
除了添加括號過程之外,括號的引入主要是改變了方程的算術優先級,看到這我首先想到的是數據結構中棧的應用這塊,所以我們馬上找到了數據結構的書和王道,又參考了網上博客的內容,制定了解決方案:先把運算符的優先級制定好,然後把中綴表達式轉化為後綴表達式,也就是逆波蘭的相關內容,最後通過棧的性質依次去除操作數和操作符進行運算並輸出結果的過程,代碼如下:
定義運算符優先級:
1 public static int GetOperationLevel(string c) // 定義運算符優先級 2 { 3 switch (c) 4 { 5 case "+": return 1; 6 case "-": return 1; 7 case "*": return 2; 8 case "/": return 2; 9 case "#": return -1; 10 case "(": return -1; 11 case ")": return -1; 12 default: return 0; 13 } 14 }
轉化為後綴表達式:
1 public Stack<string> getReversePolish(string equation) 2 { 3 //equation = "(1+2)*3"; 4 Stack<string> opStack = new Stack<string>(); // 定義運算符棧 5 opStack.Push("#"); 6 Stack<string> numStack = new Stack<string>(); // 定義操作數棧 7 for(int i = 0; i < equation.Length;) 8 { 9 int opNum = GetOperationLevel(equation[i].ToString()); 10 if (opNum == 0) 11 { 12 int index = GetCompleteValue(equation.Substring(i, equation.Length - i)); 13 numStack.Push(equation.Substring(i, index)); 14 i = (i + index); 15 } 16 else 17 { 18 if (equation[i] == ‘(‘) 19 { 20 opStack.Push(equation[i].ToString()); 21 } 22 else if (equation[i] == ‘)‘) 23 { 24 MoveOperator(opStack, numStack); 25 } 26 else 27 { 28 if (opStack.Peek() == "(") 29 { 30 opStack.Push(equation[i].ToString()); 31 } 32 else 33 { 34 JudgeOperator(opStack, numStack, equation[i].ToString()); 35 } 36 } 37 i++; 38 } 39 } 40 if (opStack.Count != 0) 41 { 42 while (opStack.Count != 0 && opStack.Peek() != "#") 43 { 44 numStack.Push(opStack.Pop()); 45 } 46 } 47 return numStack; 48 }
計算表達式的值:
1 public Stack<string> getRpnEquation(Stack<string> numStack) 2 { 3 Stack<string> rpnEquation = new Stack<string>(); // 逆波蘭 4 foreach (string s in numStack) 5 { 6 rpnEquation.Push(s); 7 } 8 return rpnEquation; 9 } 10 11 public string CalcRPNFormula(Stack<string> rpnFormula) 12 { 13 string result = ""; 14 Stack<string> resultStack = new Stack<string>(); 15 foreach (string s in rpnFormula) 16 { 17 int num = GetOperationLevel(s); 18 if (num == 0) 19 { 20 resultStack.Push(s); 21 } 22 else 23 { 24 CalcResult(resultStack, s); 25 } 26 } 27 result = resultStack.Pop(); 28 //Console.WriteLine(result); 29 return result; 30 }
功能2運行截圖:
4.功能2的編程收獲
功能2主要是逆波蘭的使用和棧的使用,最明顯的收獲當然是對棧的理解加深了很多,對C#中棧的使用更加熟悉,以後遇到問題用棧解決成為了一個非常值得嘗試的選擇;其次就是更加加深了對數據結構這門專業基礎課的認識,越來越覺得數據結構在編程中的重要作用,這次用到了棧,下次可能還會遇到隊列等數據結構,所以以後要更加強對各種數據結構的了解,爭取用語言去實現各種數據結構和算法,偽代碼雖然看起來非常便捷,但是對於編程能力非常一般的我來說偽代碼到語言實現還是有一定的距離的,所以要更加加強算法代碼實現這一塊內容;
5.功能3的重點和難點
(1).命令行參數
功能3再次出現了命令行參數,經過了之前的訓練,這一塊已經不是難點,但是還是此功能的一個重點,C#和Python在命令行參數之中的區別在於Python是把程序的名字當做第一個命令行參數,C#是傳進去的是什麽那麽使用中就是什麽,沒有多余的一項命令行參數;
(2).限定題目數量,區分命令行參數是否為正整數
第二個命令行參數可能是字符串或者小數等,這樣就沒辦法完成限定題目數量的問題,所以首先要判斷第二個命令行參數是否為正整數,我們采用的是正則表達式的方式,正則代碼如下:
1 static bool isNumeric(string value) // 判斷第二個命令行參數是否為正整數 2 { 3 return Regex.IsMatch(value, @"^[+]?\d+$"); 4 }
(3).精美打印輸出
因為括號數量的不一樣,所以生成的方程式長短不一,我們采用的方法是通過String.PadRight(length,string)把方程後面補上空格組成長短一致的字符串;另外功能還要求輸出到txt文件中,這裏用到了C#的輸入輸出流,使用StreamWriter對象對文件進行寫入操作,由於每次寫入會覆蓋原來的文件,所以我們先把所有的題目都生成保存在List<string>中,最後統一打印輸出,打印代碼如下:
1 public void produceFiles(string filename,List<string> equations) // 傳入參數為文件名和寫入內容 2 { 3 StreamWriter streamWriter = new StreamWriter(filename, false, Encoding.Default); 4 for(int i = 0; i < equations.Count;i++) // 按條將題目打印到文件 5 { 6 streamWriter.WriteLine(equations[i]); 7 } 8 streamWriter.Flush(); 9 streamWriter.Close(); 10 }
(4).去除重復
這塊我感覺是整個項目中最難處理的一部分,在網上搜了很多方法,比如樹的最小表示法區分同構的樹等等,我們嘗試了很多但是沒有達到預想的效果,所以最後我們兩個人經過討論決定使用這樣一種判斷:如果兩條題目的結果相同、操作數和操作符相同(順序可能不同,經過排序後比較是否相同),雖然沒有非常精確地算法,但是輸出的同樣是沒有重復的題目,同樣完成了功能,因為結果不同的題目構成肯定不相同。缺憾就是生成過程中會多丟棄幾個題目,但是對整體影響非常小,我們也對自己的想法比較滿意;
功能3的運行效果截圖:
控制臺的輸出效果:
生成的文件內容:
6.功能3的編程收獲
功能3最大的難點在於去重,我們最後也沒有找到最優解,但是我們經過討論和分析制定了一個不是最優解但是相差不大的解決方案,在同等情況下,我可能實現最優解需要用8個小時,但是我的非最優解只需要花費我3個小時的時間,而在性能上非最優解比最優解差的性能不足1%,我覺得這是非常可以接受的。在工程上可能以後會遇到更多這種情況,當然最後的辦法還是找打最優解,但是如果時間和收獲比差太大的話選擇非最優解也是非常好的選擇。所以完成這個功能給我帶來最大的收獲不是技術層面的,而是編程思想方面的,有時候不一定非要去找到最優解,可能有些東西根本就不存在最優的情況呢;
7.功能4的重點和難點
(1).帶分數的出題
較之前的3個功能,最明顯的區別就是操作數變了,操作數從整數擴充到了分數,出題的那一部分代碼就要發生相應的變化,所以我在生成隨機操作數的時候讓操作數可以是"5/2"的形式,分數和整數的出現同樣是隨機的,這樣就保證了出的題目中既包括整數也有分數;
(2).帶分數題目的計算
帶分數題目計算最大的一個難點在於除法是不滿足交換律的,也就是"a/b/c"和"a/(b/c)"的結果是不一樣的,而分數的表示又和除號是一樣的,如果直接按照之前的算法是沒有辦法完成正確的運算的,需要把分數(a/b)看成是一個數,也就是"a/ b/c"的運算順序是"a/(b/c)",這一點看起來容易但實現起來卻很困難,反正我倆真的想了好久該怎麽解決這一問題。最後我們的解決方案是:把運算數保存到2個數組裏,也就是在生成題目的時候,1個數組裏的題目是專門用於輸出到文件的,就是單純的字符串形式,另一個數組裏的題目是用於計算的,二者的操作數和操作符完全一致,這樣就可以實現把分數先計算當做一個數,先完成分數到小數的轉換,再重用之前的代碼就可以解決,關鍵代碼如下:
1 number1 = rm.Next(1, 20).ToString(); // 隨機生成的分子 2 number2 = rm.Next(1, 20).ToString(); // 隨機生成的分母 3 randomFractions[i] = number1 + "/" + number2; // 用於展示的分數字符串 4 randomRealNum[i] = (Convert.ToDecimal(number1)/Convert.ToDecimal(number2)).ToString(); // 用於計算的操作數
產生題目也產生2個,一個用於輸出,一個用於計算:
1 equation = "(" + " " + nums[0] + " "+ operators[0] + " " + nums[1] + " " + operators[1] + " " + nums[2] + " " + ")" + " " + operators[2] + " " + nums[3] + " " ; // 用於顯示 2 equationCal = "(" + nums1[0] + operators[0] + nums1[1] + operators[1] + nums1[2] + ")" + operators[2] + nums1[3]; // 用於計算
(3).結果分數約分輸出
經過前2個步驟的處理,得到的結果result是string類型的,轉化為decimal類型的話,有整數也有小數,對於整數就不用處理可以直接輸出,但對於小數就要變成真分數並約分。我們的解決方案是:先正則表達式匹配判斷是否為整數,如果為整數則直接可以輸出,如果是小數必須經過轉化為分數並約分操作。這一步驟是首先把小數的整數部分和小數部分分開,整數部分直接是答案前面的整數數字,小數部分的處理(這裏假設是2.5)是根據小數的長度(len =1)讓小數除以math.pow(10,len),也就是10,即得到5和10,然後根據輾轉相除法求出這兩個數的最大公約數,兩個數分別除以最大公約數再按照固定格式輸出就可以達到功能預期效果,這部分代碼如下:
1 namespace f4 2 { 3 class DecimalToFraction 4 { 5 public string decimalTranverse(string value) // 小數轉化為分數 6 { 7 string result = ""; 8 string[] str = value.Split(‘.‘); 9 int decimalLen = str[1].Length; 10 if (Regex.IsMatch(str[1], @"^[0]*$")) //如果小數部分全為0則直接返回整數部分 11 { 12 return str[0]; 13 } 14 long weitght = Convert.ToInt32(Math.Pow(10, decimalLen)); 15 long num = Convert.ToInt32(str[1]); 16 long gcd = gCD(num, weitght); 17 if (Regex.IsMatch(str[0], @"^[+-]?[0]*$")) // 如果整數部分為0則不用輸出整數部分,直接輸出分數 18 { 19 result = String.Format("{0}{1}{2}", num / gcd, "/", weitght / gcd); 20 } 21 else 22 { 23 result = String.Format("{0}{1}{2}{3}{4}", str[0], " ", num / gcd, "/", weitght / gcd); 24 25 } 26 27 return result; 28 } 29 30 public long gCD(long m, long n) //求最大公約數 31 { 32 long r, t; 33 if (m < n) 34 { 35 t = n; 36 n = m; 37 m = t; 38 } 39 while (n != 0) 40 { 41 r = m % n; 42 m = n; 43 n = r; 44 45 } 46 return (m); 47 } 48 49 } 50 }
8.功能4的編程收獲
不要被嚇到,看到題目中的要求發現時選做題就覺得這題肯定很難,剛開始真的都很想放棄這個功能了,程序員要女生的青睞反正也沒啥用,但是仔細看看好像又沒有那麽難,所以我們就試著做了一下,結果發現真的沒有想象的那麽難,跟功能3用的時間根本沒法比,所以,做功能4給我最大的收獲還是不要被嚇到,把功能拆分成幾部分然後重用之前的代碼也沒那麽難;還有一個就是我之前一直以為除法是滿足結合律的,開始做這個功能的時候就完全沒考慮這個事,幸虧隊友及時提醒,也讓我把最基礎的數學鞏固了一下;
9.功能5
功能5是為以後做準備,我覺得我們的類和方法重用性都很高,以後會很方便整成接口;
二、結對編程的體會
首先,結對編程可以讓人自覺地規範自己的代碼風格,我之前的代碼命名特別隨便,有時候可能還能根據變量要表示的內容給變量起名字,更多情況就是abcd這樣起,時不時的還整個拼音上去,所有的函數都寫在主函數下面,基本不寫註釋,導致整個項目亂七八糟,過很短的時間就連自己的代碼都看不懂了。結對編程的時候,有個人在你旁邊看著剛開始還蠻緊張的,後來反正也是沒有自己編的時候那麽自由,要命名啥東西被強迫著寫註釋,寫很多個類,類裏面再寫方法,對編程習慣的改善非常的大;
其次,我是個很粗心的人,有時候沒看清要幹嘛就開始整了,整出來明明是錯的自己也發現不了。結對編程過程中有個人在旁邊提醒著就非常有必要,就比如除法那個結合律,要是我自己整肯定就是按照除法遵循結合律做了,但是有隊友提醒就能好很多,有些錯誤自己發現不了,隊友很容易就發現了;
第三,可以被督促著幹活。平常的話國慶假期可能每天都得睡到10點鐘,慢慢悠悠的,但是前一天約好了第二天見面的時間就強行讓自己睡不了懶覺,也提高了很多效率;
最後,遇到問題可以隨時討論研究,自己很容易鉆到牛角尖裏,真正體會到才發現多一條思路是多麽重要;
三、花費時間較長,給我較大收獲的事件
1.git的使用
之前都是一個項目只有自己提交代碼,也很少有沖突,但是結對過程中,有時候用我的電腦,有時候用隊友的,經常出現各種提交錯誤和沖突,有一回還把項目整的打不開了。所以在git的使用上我們多了很多百度的時間,百度報的各種錯,最後對git的了解加深了很多,命令也不再局限於add commit push這幾個,對報的各種錯都有了了解,基本的git問題都能解決;
2.括號匹配
剛開始看到括號匹配的時候我倆主要看的嚴蔚敏的那版數據結構教材,書上都是偽代碼看的頭大,王道上更簡略,搞清整個算法的流程就花費了非常多的時間(最起碼得150分鐘),搞清流程後,結合博客轉化為C#代碼又花費了很多時間(60分鐘左右),所幸最後寫出來了。這個給我的收獲就是弄懂了一個經典算法;
3.單元測試
之前我自己做的測試頂多也就是新建一個工程把一些不太確定的方法在工程裏跑一下,這是第一次整正兒八經的單元測試,照著書上和博客上一點一點的弄,雖然不復雜,但是剛開始的時候也是容易丟三落四,忘了很多測試的東西,後來才慢慢好起來,這個測試的入門花了挺多時間,但收獲還是蠻大的,終於算是入門測試這一技術了;
4.功能3去重
問了一個比較厲害的同學他說這個功能可以用樹的最小生成法來做,所以我們就專門研究樹的最小生成法,找書、找博客等各種方法,最後雖然花了非常多的時間(180分鐘左右)沒整出來就換了個思路,但是在這個過程中,我們還是明白了什麽是樹的最小生成法,樹的同構是怎麽回事,也明白了算法的基本原理,甚至去Poj看了題目並提交ac了,也是十分有收獲的,也希望今後有時間能繼續研究研究這個事;
5.判斷一個結果相同的題目操作數和操作符是否相等
因為兩個題目結果相同的話這兩個題目還不一定是連在一起的,所以需要把所有的題目、題目結果都保存起來,每一個題目的操作數都是數組,操作符也是數組,題目的數量是根據用戶輸入來的,不知道有多少,所以需要有一個非常好的數據結構來保存這些數據,最開始打算用二維數組,但是二維數組空間分配等有很麻煩,最後用了List<string[]>這一泛型存儲數據,非常簡便。這讓我對泛型的使用和理解又加深了很多,給了我非常大的收獲;
要求二:
編程地點:信息科學與技術學院232機房
照片:
要求三:
按照老師要求push代碼;
作業地址:【https://git.coding.net/zhangjy982/ArithMetic.git】
作業 20180925-6 四則運算試題生成