目錄 Welcome to YARP - 1.認識YARP並搭建反向代理服務 Welcome to YARP - 2.配置功能 2.1 - 配置文件(Configuration Files) 2.2 - 配置提供者(Configuration Providers) 2.3 - 配置過濾器(Confi ...
在11月的下雪天,小悅身處於溫暖的辦公室中,窗外的雪花在燈光下翩翩起舞。她盯著電腦屏幕,不經意間,一個熟悉的身影從辦公室門口處經過,吸引了她的目光。那個人看上去很像是一個女孩,名叫蘇菲,是她在大學時期遇到的國外交換生。
小悅的心跳加速,她有些不敢相信自己的眼睛。在她的記憶中,蘇菲是一個溫柔、聰明且樂於助人的女孩。她們曾經一起上過電腦科學課,蘇菲對數學和編程的熱愛給小悅留下了深刻的印象。在課程中,蘇菲表現出了非凡的編程天賦和扎實的技術功底,她的編程能力讓小悅敬佩不已。
小悅忍不住站起來,快步走向那個人。她輕輕地拍了拍她的肩膀,問道:“蘇菲?”
那個女孩轉過身來,露出了一張熟悉的面孔。她的眼睛閃爍著聰慧的光芒,嘴角掛著一抹微笑。她驚喜地喊道:“小悅?”
兩人緊緊地擁抱在一起,重逢的喜悅讓她們感到溫暖。她們來到樓下的咖啡廳,開始回憶起大學時期的時光,談論著彼此的生活和工作。
小悅感到非常驚喜,沒想到蘇菲會來到她的辦公樓。蘇菲也並不知道小悅在這裡工作。或許是命運的安排,讓她們再次相遇。小悅心想,一定要好好和蘇菲聊聊天,暢談彼此的近況,重溫那段美好的大學時光。
小悅想起了那次電腦科學課的上機考試。她需要實現一個計算器表達式演算法。小悅在考試前嘗試著自己實現了一個多項式解析式演算法,但是效果並不甚理想。這個問題讓她感到很困惑,不知道如何著手。就在這時,蘇菲主動幫助了她。
蘇菲耐心地解釋瞭如何分析問題、拆分令牌、使用逆波蘭表達式演算法進行計算,以及如何使用棧來處理尾碼表達式。她的思路清晰、有條理,讓小悅瞬間明白了問題的解決方法。
在蘇菲的幫助下,小悅成功地解決了問題,並且在考試中取得了優異的成績。她一直感激蘇菲的幫助和鼓勵。
回憶起那段經歷,小悅不禁感慨萬分。她對蘇菲說:“謝謝你,蘇菲。你的幫助和支持讓我變得更加自信和勇敢。”
蘇菲微笑著說:“我們是朋友,小悅。無論何時何地,我都會儘力幫助你。”
她們聊了很久,談論著過去的點點滴滴和現在的變化。畢業後,蘇菲選擇繼續深造,攻讀電腦科學碩博學位。在國外留學期間,她積累了豐富的經驗和技能,志向是成為一名電腦科學家。然而,儘管小悅和蘇菲在大學時期形影不離,但畢業後兩人便各奔東西,相距太遠,漸漸失去了聯繫。她們還計划著再次回到校園,去湖心島看看那個曾經給她們帶來無限回憶的地方。
在分別前,小悅緊緊地擁抱了蘇菲一次。她心裡默默地想:謝謝你,蘇菲,謝謝你陪我度過了那段美好的時光。我會永遠珍惜這份友誼。
隨著時間的推移,雖然小悅和蘇菲的聯繫越來越少。但是她們的友誼和回憶永遠留在彼此的心中。在這個下雪的日子里,小悅重新與蘇菲相遇,讓她回憶起了那些青蔥校園歲月和湖心島的甜蜜時光。她感到無比幸福和感激,因為她知道這些美好的回憶將永遠伴隨著她走過人生的旅程。即使未來還有更多的挑戰等待著她,小悅也相信自己的能力能夠剋服一切困難。
當時小悅和蘇菲面對的考試題目為:要求計算一個包含數字和運算符的字元串的結果,包括加減乘除和冪運算,還有括弧。數字可以是整數或小數,字元串中可以有空格。要求處理字元串中的所有運算符和數字,並計算出最終的結果。
示例:3 * (4 + (2 / 3) * 6 - 5)=9;
123 -( 4^ ( 3 - 1) * 8 - 8 /( 1 + 1 ) *(3 -1) )=3;
演算法實現1:
1 using System; 2 using System.Collections.Generic; 3 4 public static class Edm { 5 6 public static double calculate(string input) 7 { 8 // 從輸入中移除空格 9 input = input.Replace(" ", ""); 10 11 // 將輸入轉換為令牌列表 12 List<string> tokens = new List<string>(); 13 string currentToken = ""; 14 foreach (char c in input) { 15 if (char.IsDigit(c) || c == '.') { // 如果字元是數字或小數點 16 currentToken += c; // 將字元添加到當前令牌中 17 } else { 18 if (currentToken != "") { // 如果當前令牌不為空 19 tokens.Add(currentToken); // 將當前令牌添加到令牌列表中 20 currentToken = ""; // 重置當前令牌 21 } 22 tokens.Add(c.ToString()); // 將字元轉換為字元串並添加到令牌列表中 23 } 24 } 25 if (currentToken != "") { // 如果當前令牌不為空 26 tokens.Add(currentToken); // 將當前令牌添加到令牌列表中 27 } 28 29 // 使用逆波蘭演算法評估令牌 30 Stack<string> operatorStack = new Stack<string>(); // 操作符棧 31 Queue<string> outputQueue = new Queue<string>(); // 輸出隊列 32 foreach (string token in tokens) { 33 if (double.TryParse(token, out double num)) { // 如果令牌可以轉換為雙精度浮點數 34 outputQueue.Enqueue(num.ToString()); // 將數字轉換為字元串並添加到輸出隊列中 35 } else if (IsOperator(token)) { // 如果令牌是操作符 36 while (operatorStack.Count > 0 && IsOperator(operatorStack.Peek()) && GetPrecedence(token) <= GetPrecedence(operatorStack.Peek())) { 37 outputQueue.Enqueue(operatorStack.Pop()); // 將操作符棧中的操作符彈出並添加到輸出隊列中 38 } 39 operatorStack.Push(token); // 將操作符添加到操作符棧中 40 } else if (token == "(") { // 如果令牌是左括弧 41 operatorStack.Push(token); // 將左括弧添加到操作符棧中 42 } else if (token == ")") { // 如果令牌是右括弧 43 while (operatorStack.Count > 0 && operatorStack.Peek() != "(") { 44 outputQueue.Enqueue(operatorStack.Pop()); // 將操作符棧中的操作符彈出並添加到輸出隊列中 45 } 46 if (operatorStack.Count == 0) { 47 throw new Exception("Mismatched parentheses"); // 拋出異常,提示括弧不匹配 48 } 49 operatorStack.Pop(); // 彈出左括弧 50 } else { 51 throw new Exception("Invalid token: " + token); // 拋出異常,提示令牌無效 52 } 53 } 54 while (operatorStack.Count > 0) { 55 if (operatorStack.Peek() == "(") { 56 throw new Exception("Mismatched parentheses"); // 拋出異常,提示括弧不匹配 57 } 58 outputQueue.Enqueue(operatorStack.Pop()); // 將操作符棧中的操作符彈出並添加到輸出隊列中 59 } 60 61 // 評估尾碼表達式 62 Stack<double> operandStack = new Stack<double>(); // 操作數棧 63 foreach (string token in outputQueue) { 64 if (double.TryParse(token, out double num)) { // 如果令牌可以轉換為雙精度浮點數 65 operandStack.Push(num); // 將數字添加到操作數棧中 66 } else if (IsOperator(token)) { // 如果令牌是操作符 67 double b = operandStack.Pop(); 68 double a = operandStack.Pop(); 69 double result = EvaluateOperator(token, a, b); // 計算操作符對應的結果 70 operandStack.Push(result); // 將計算結果壓入操作數棧中 71 } else { 72 throw new Exception("Invalid token: " + token); // 拋出異常,提示令牌無效 73 } 74 } 75 if (operandStack.Count != 1) { 76 throw new Exception("Invalid expression"); // 拋出異常,提示表達式無效 77 } 78 return operandStack.Pop(); // 返回操作數棧中唯一的元素作為計算結果 79 } 80 81 static bool IsOperator(string token) { 82 return token == "+" || token == "-" || token == "*" || token == "/" || token == "^"; // 判斷一個字元串是否為操作符(+、-、*、/、^) 83 } 84 85 static int GetPrecedence(string op) {// 獲取操作符優先順序 86 if (op == "^") { 87 return 3; 88 } else if (op == "*" || op == "/") { 89 return 2; 90 } else if (op == "+" || op == "-") { 91 return 1; 92 } else { 93 return 0; 94 } 95 } 96 97 // 計算兩個操作數和一個操作符的結果 98 static double EvaluateOperator(string op, double a, double b) { 99 if (op == "+") { 100 return a + b; 101 } else if (op == "-") { 102 return a - b; 103 } else if (op == "*") { 104 return a * b; 105 } else if (op == "/") { 106 return a / b; 107 } else if (op == "^") { 108 return Math.Pow(a, b); 109 } else { 110 throw new Exception("Invalid operator: " + op); 111 } 112 } 113 }
這段代碼實現了一個表達式計算器,能夠接受包含基本數學運算的字元串表達式,並返回計算結果。
-
導入了System和System.Collections.Generic命名空間,用於使用標準的數據結構和功能。
-
定義了一個靜態類Edm,其中包含一個名為calculate的靜態方法,該方法接受一個字元串輸入並返回一個雙精度浮點數。
-
calculate方法首先去除輸入中的空格,然後將輸入轉換為一個令牌列表(tokens)。
-
使用逆波蘭演算法(shunting yard algorithm)對令牌進行評估。它使用了操作符棧(operatorStack)和輸出隊列(outputQueue)來對錶達式進行處理。
-
在這段代碼中,我們使用了操作符棧和輸出隊列來處理給定的令牌(tokens)。
- 首先,我們遍歷令牌數組,對每個令牌進行處理。
- 如果令牌是一個數字,我們將其轉換為字元串並添加到輸出隊列中。
- 如果令牌是一個操作符,我們需要根據操作符的優先順序來決定其在輸出隊列中的位置。我們將當前操作符與操作符棧頂的操作符進行比較,如果當前操作符的優先順序小於或等於棧頂操作符的優先順序,則將棧頂操作符彈出並添加到輸出隊列中,直到滿足條件為止,然後將當前操作符壓入操作符棧中。
- 如果令牌是左括弧,我們直接將其壓入操作符棧中。
- 如果令牌是右括弧,我們需要將操作符棧中的操作符彈出並添加到輸出隊列中,直到遇到左括弧為止。如果在彈出操作符時,操作符棧為空,或者沒有遇到左括弧,就會拋出異常提示括弧不匹配。
- 如果令牌不是數字、操作符、左括弧或右括弧,則拋出異常提示令牌無效。
- 最後,處理完所有令牌後,將操作符棧中剩餘的操作符依次彈出並添加到輸出隊列中。如果在此過程中遇到左括弧,也會拋出異常提示括弧不匹配。
這樣,我們可以將中綴表達式轉換為尾碼表達式,並且在這個過程中處理了操作符的優先順序和括弧的匹配關係。
-
對尾碼表達式進行評估。這部分代碼使用了操作數棧(operandStack)來計算尾碼表達式的值。
-
評估尾碼表達式的詳細解釋:
- 創建一個空的操作數棧,用於存儲操作數。
- 遍歷尾碼表達式中的每個令牌。
- 如果令牌可以轉換為雙精度浮點數,則將其壓入操作數棧中。
- 如果令牌是操作符,則從操作數棧中彈出兩個操作數(假設為a和b),然後根據該操作符對這兩個操作數進行計算,得到結果。
- 將計算得到的結果壓入操作數棧中。
- 如果令牌不是數字也不是操作符,則拋出異常提示令牌無效。
- 完成對所有令牌的處理後,操作數棧中應該只剩下一個元素,即為最終的計算結果。如果操作數棧中的元素不止一個,說明表達式無效,拋出異常。
-
IsOperator方法用於檢查一個字元串是否為操作符(+、-、*、/、^)。
-
GetPrecedence方法用於獲取操作符的優先順序。
-
EvaluateOperator方法用於計算兩個操作數和一個操作符的結果。
逆波蘭表達式演算法由荷蘭電腦科學家Edsger Dijkstra在1960年代實現,它也是ALGOL60語言的基礎演算法,因為在ALGOL60上做出原理性貢獻,Dijkstra獲得了1972年的圖靈獎。而該演算法的名稱來源於其發明者,波蘭數學家Jan Łukasiewicz由1929年提出此數學方法。
逆波蘭表達式演算法的數學原理可以通過棧的數據結構來解釋。我們以中綴表達式 "3 + 4 * 2" 為例進行解釋。
- 遍歷中綴表達式中的每個令牌(數字、操作符)。
- 如果令牌是數字,則直接將其添加到輸出隊列中。
- 如果令牌是操作符: a. 如果操作符棧為空,或者操作符棧頂的操作符優先順序小於當前操作符,則將當前操作符壓入操作符棧中。 b. 如果操作符棧頂的操作符優先順序大於等於當前操作符,則將操作符棧頂的操作符彈出並添加到輸出隊列中,直到滿足條件為止,然後將當前操作符壓入操作符棧中。
- 處理完所有令牌後,將操作符棧中剩餘的操作符依次彈出並添加到輸出隊列中。
以中綴表達式 "3 + 4 * 2" 為例,通過上述步驟,我們可以將中綴表達式轉換為尾碼表達式 "3 4 2 * +"。
中綴表達式轉換為尾碼表達式的過程是為了更方便地讓電腦進行數學表達式的計算。尾碼表達式(也稱為逆波蘭表達式)具有以下優點:
-
沒有括弧:尾碼表達式不需要括弧來表示運算的優先順序,因此避免了括弧所帶來的歧義和複雜性。
-
易於計算:尾碼表達式的計算可以通過簡單的迭代演算法來實現,不需要遞歸或者棧來保存中間結果,這使得計算更加高效。
對於尾碼表達式 "3 4 2 * +",我們可以按照以下步驟進行計算:
- 從左到右掃描尾碼表達式,遇到操作數就將其壓入棧中。
- 遇到操作符時,從棧中彈出相應數量的操作數進行計算,並將計算結果壓入棧中。
- 最終棧中剩下的元素就是整個表達式的計算結果。
對於尾碼表達式 "3 4 2 * +",計算過程如下:
- 遇到 "3",壓入棧中
- 遇到 "4",壓入棧中
- 遇到 "2",壓入棧中
- 遇到 "*",彈出棧頂的兩個元素(4和2),計算結果為8,將結果壓入棧中
- 遇到 "+",彈出棧頂的兩個元素(3和8),計算結果為11,將結果壓入棧中
- 最終棧中剩下的唯一元素就是整個表達式的計算結果,即11。
因此,尾碼表達式的轉換和計算可以使數學表達式在電腦中的處理更加簡單和高效。
逆波蘭表達式演算法之所以受到廣泛應用,是因為它能夠有效地解決中綴表達式計算的問題。中綴表達式需要使用括弧來確定操作符的優先順序,而逆波蘭表達式則不需要,因為它使用尾碼表示法,操作符的優先順序可以通過操作符的順序來確定。
逆波蘭表達式演算法在計算器、編譯器、解釋器等領域都有廣泛應用。在計算器中,用戶輸入的數學算式,即中綴表達式通常需要轉換為逆波蘭表達式才能進行計算。在編譯器和sql解釋器中,逆波蘭表達式演算法可以用於計算sql表達式的值,以及將sql表達式轉換為可執行代碼。
演算法實現2:
1 public static double calculate(string s){ 2 s=s.Replace(" ",""); 3 if (Regex.Match(s,"^[(][\\d\\.\\+\\-\\*/\\^]+[)]$").Success) s=Regex.Replace(s,"^[(]|[)]$",""); 4 if (Regex.Match(s,"^-?[\\d\\.]+$").Success) return double.Parse(s);//檢測到單個的正負數字,直接返回值 5 if (Regex.Match(s,"^\\d[\\d+-\\.]+$").Success) return Regex.Matches(s,"-[\\d\\.]+|(?<=^|[+])[\\d\\.]+").OfType<Match>().Sum(x=>double.Parse(x.Value)); //檢測到只包含連續+-符號的算式,返回其運算值 6 if (Regex.Match(s,"^[\\d\\.]+[*/\\^][\\d\\.]+$").Success) { //檢測到兩個數的乘除次方,返回其運算值 7 var tmp=Regex.Split(s,"[*/\\^]").Select(double.Parse).ToArray(); 8 return s.Contains('*') ? tmp[0]*tmp[1] : s.Contains('/') ? tmp[0]/tmp[1] : Math.Pow(tmp[0],tmp[1]); 9 } 10 if (s.Contains('(')) { //檢測到括弧,先運算一個括弧單元,然後遞歸 11 var tmp=Regex.Match(s,"[(][\\d\\.\\+\\-\\*/\\^]+[)]").Value; 12 return calculate(s.Substring(0,s.IndexOf(tmp))+calculate(tmp)+s.Substring(s.IndexOf(tmp)+tmp.Length)); 13 } 14 if (s.Contains('^')) { //檢測次方符號,優先運算 15 var tmp=Regex.Match(s,"[\\d\\.]+[\\^][\\d\\.]+").Value; 16 return calculate(s.Substring(0,s.IndexOf(tmp))+calculate(tmp)+s.Substring(s.IndexOf(tmp)+tmp.Length)); 17 } 18 if (s.Contains('*')||s.Contains('/')) { //檢測乘除符號,優先運算 19 var tmp=Regex.Match(s,"[\\d\\.]+[\\*/][\\d\\.]+").Value; 20 return calculate(s.Substring(0,s.IndexOf(tmp))+calculate(tmp)+s.Substring(s.IndexOf(tmp)+tmp.Length)); 21 } 22 return 0; 23 }
演算法2是一個用於計算數學表達式的演算法,使用了正則表達式和遞歸的思想,以及一些基本的操作符處理和字元串處理方法。這種類型的演算法通常用於電腦科學和編程中,以便對數學表達式進行求值。
在工作之餘,小悅回想起大學時期的那次考試。當時,她運用逆波蘭表達式演算法成功解決了算式計算器問題。而現在,憑藉著工作經驗的積累,她採用自己設計的方法——正則表達式和遞歸演算法。
小悅運用正則表達式的強大功能,將輸入的算式進行精準的解析和拆分。她巧妙地運用正則表達式的分組和捕獲功能,將算式拆分為一個個獨立的子表達式。然後,她利用遞歸演算法的特性,對這些子表達式進行遞歸計算。這種方法的優點在於,它能夠處理包含括弧、運算符優先順序等複雜元素在內的算式,同時還能保證計算過程的靈活性和高效性。
但演算法2對於更複雜的腳本語言解釋器來說,對於更複雜的表達式,比如函數調用、變數賦值等,就無法處理,可能需要考慮更多的功能和錯誤處理機制。如果需要實現一個更完整的腳本解釋器,還是需要通過演算法1進行擴展。
演算法1和演算法2的比較如下:
演算法1優點:
- 逆波蘭表達式演算法不需要使用括弧,因此可以減少括弧匹配的複雜性。
- 逆波蘭表達式演算法可以直接利用棧結構進行計算,簡化了中綴表達式轉換為尾碼表達式的過程。
- 逆波蘭表達式演算法可以用於解釋各種類型的語言和數據格式,從而實現各種解釋器。這些解釋器可以用於實現各種應用,例如命令行工具、頁面腳本語言、配置文件解析器、sql資料庫查詢解析器等。
演算法1缺點:
- 中綴表達式轉換為尾碼表達式的過程可能較為複雜,需要額外的轉換步驟。
- 逆波蘭表達式演算法需要額外的數據結構(棧)來進行計算,可能需要更多的記憶體空間。
演算法2優點:
- 演算法2可以直接對中綴表達式進行遞歸計算,不需要額外的轉換步驟。
- 演算法2可以直接處理括弧、乘除次方等運算符,邏輯相對清晰。
演算法2缺點:
- 演算法2中使用了正則表達式進行匹配,可能在處理複雜表達式時效率較低。
- 演算法2在處理嵌套括弧的情況時可能需要多次遞歸計算,效率較低。
- 演算法2對於更複雜的腳本語言解釋器來說,不容易擴展和優化。
綜合來看,演算法1在處理簡單表達式時可能更加高效,而演算法2在處理複雜數學表達式時可能更加直觀和易於理解。具體選擇哪種演算法取決於實際需求和對演算法的偏好。
測試用例:
1 using NUnit.Framework; 2 using System; 3 [TestFixture] 4 public class CalculatorTest 5 { 6 public bool close(double a, double b) 7 { 8 if (Math.Abs(a-b)<0.000000001) return true; 9 return false; 10 } 11 [Test] 12 public void EasyTests() 13 { 14 Assert.AreEqual(true, close(Edm.calculate("3 + 5"), 8)); 15 Assert.AreEqual(true, close(Edm.calculate("5 + 41"), 46)); 16 Assert.AreEqual(true, close(Edm.calculate("5 - 3"), 2)); 17 Assert.AreEqual(true, close(Edm.calculate("5 - 5"), 0)); 18 Assert.AreEqual(true, close(Edm.calculate("3 * 5"), 15)); 19 Assert.AreEqual(true, close(Edm.calculate("2 * 23"), 46)); 20 Assert.AreEqual(true, close(Edm.calculate("123 / 3"), 41)); 21 Assert.AreEqual(true, close(Edm.calculate("22 / 1"), 22)); 22 } 23 24 [Test] 25 public void MediumTests() 26 { 27 Assert.AreEqual(true, close(Edm.calculate("3 + 5 * 2"), 13)); 28 Assert.AreEqual(true, close(Edm.calculate("5 - 3 * 8 / 8"), 2)); 29 Assert.AreEqual(true, close(Edm.calculate("6*(2 + 3)"), 30)); 30 Assert.AreEqual(true, close(Edm.calculate("2 ^ 5"), 32)); 31 Assert.AreEqual(true, close(Edm.calculate("5 ^0"), 1)); 32 Assert.AreEqual(true, close(Edm.calculate("23.2- 15.2"), 8)); 33 Assert.AreEqual(true, close(Edm.calculate("22 / 5"), 22.0/5)); 34 } 35 36 [Test] 37 public void HardTests() 38 { 39 Assert.AreEqual(true, close(Edm.calculate("3 * (4 + (2 / 3) * 6 - 5)"), 9)); 40 Assert.AreEqual(true, close(Edm.calculate("123 -( 4^ ( 3 - 1) * 8 - 8 /( 1 + 1 ) *(3 -1) )"), 3)); 41 Assert.AreEqual(true, close(Edm.calculate("4 + 2 * ( (226 - (5 * 3) ^ 2) ^ 2 + (10.7 - 7.4) ^ 2 - 6.89)"),14)); 42 Assert.AreEqual(true, close(Edm.calculate(" (226 - (5 * 3) ^ 2) ^ 2"),1)); 43 } 44 45 [Test] 46 public void RandomTest() //some really simple random tests just to get this older kata out of beta (@smile67, 26.10.2017) 47 { 48 string [] f=new string []{"a * b -c","a + b/ (b + c)","(a + c+ b) * b *a"}; 49 Random rnd = new Random(); 50 for (int i=1;i<51;i++) { 51 int r= rnd.Next(0,3), a= rnd.Next(0,100), b= rnd.Next(0,100), c= rnd.Next(0,100); 52 string t=f[r].Replace("a",a+"").Replace("b",b+"").Replace("c",c+""); 53 double s1=Edm.calculate(t), s2=calculate2(t); 54 Console.WriteLine(i+". Tested for: