本篇是MathAssist的第三篇,將在上篇所實現的BigNumber基礎上完成具有編譯功能支持無限大數的計算器SuperCalculator。
要想從形如 "(1.23435+sin(0.5*180/PI))*2468.2345" 字符串格式的表達式中求值,需要使用編譯原理的知識,不過在一般的《數據結構》課程中都會講解基礎的表達式求值問題,而本篇也是在數據結構課程的基礎上稍加拓展而實現。
多叉樹的節點類型
node繼承體系
表達式的值,一般將其轉化成二叉樹結構,根節點表示操作符,子節點表示操作符所使用到的分量。
比如上面的表達式表示成二叉樹如圖所示:
如圖所示,加乘除的操作數都是兩個,而sin只需要一個操作數,所以其子節點只有一個。而如果要支持不限參數個數的函數,就必須有兩個以上的節點,所以SuperCalculator中所使用的是多叉數。
先看所有節點類型的根類型Node

- Format表示此節點的字符串表示,如果是sin節點此值即為"sin",乘法節點此值即為"*"
- Index 表示此節點的首字符在字符串表達式中的索引,主要是用在錯誤定位上。
- MinParameterCount 因為現在操作符子節點的個數不定,所以要定義一個最小所需的操作個數,如果小於這個數后就是不合法的表達式
- Nexts 此節點所有的子節點。這個類型是List<Node>就是用於存儲不定個數的操作數。
- Priority 表示節點在優先級,主要用在構建多叉樹時,優先級越高的節點這個值越大。比如數字節點是6,函數節點是5,乘除是4,加減是2
- Value 計算此節點時最后的值。
下面再看Node及其子類的繼承體系。
直接從Node繼承的類有三個:NumberNode(純數字節點), ExpandNode(可拓展節點), CompartNode(間隔節點)
其中CompartNode,表示括號之類節點,只在詞法分析中用到,不會出現在樹形中
ExpandNode的直接子類有三個:FunctionNode(函數節點),ConstantNode(常量節點), OperateNode(操作符節點)
- FunctionNode 所有的函數節點都以此類為父節點,比如sin, max, exp等
- ConstantNode PI, e等常量節點的父節點
- OperateNode 加減乘除等節點的父節點
其中節點中所有的數字類型都是用BigNumber來表示。
用反射機制來查找所有可用的拓展節點
如下所示的代碼,將項目中所有從ConstantNode,FunctionNode,OperateNode中繼承而來的子類,先實例化后再存儲到對應的List<T>中,這樣在構建多叉樹時用這些List<T>中的對象的Format進行字符串查找即可判斷對應的類型。
internal static void Find(List<ConstantNode> constants, List<FunctionNode> functions, List<OperateNode> operates) { Assembly ass = Assembly.GetExecutingAssembly(); Module[] modes = ass.GetModules(); Type[] typs; foreach (Module m in modes) { typs = m.GetTypes(); foreach (Type typ in typs) { if (typ.IsSubclassOf(typeof(ConstantNode))) { constants.Add(ass.CreateInstance(typ.FullName) as ConstantNode); } else if (typ.IsSubclassOf(typeof(FunctionNode))) { functions.Add(ass.CreateInstance(typ.FullName) as FunctionNode); } else if (typ.IsSubclassOf(typeof(OperateNode))) { operates.Add(ass.CreateInstance(typ.FullName) as OperateNode); } } } }
使用反射機制后就非常方便添加新的函數或操作符了。比如現在項目中只實現了Max函數,如果要實現Min函數,只需要添加一個MinNode類,重寫Format屬性返回"min",重寫Value屬性求出List<Node>中的最小值即可。將這個類實現后,反射機制將自動添加min函數了。
語法分析、構建多叉樹
Expression表達式類
Exression 用於接受一個字符串類型的表達式,並計算出值。其結構如圖所示:

從圖中可以看出詞法分析在Lexcial類中,語法分析在Syntax.cs類中。
其中Lexical.Analyse()函數負責將string value格式的字符串表達式轉化成List<Node>格式的表示式。然后Syntax.Analyse(List<Node> nodes)負責將中序表達式轉化成多叉樹,最后只返回一個根節點Node
詞法分析
詞法分析的過程,就是字符串的各種比較。
- 先過濾過空格回車,
- 是否為字母,那么就有可能是常量節點或者函數節點,就在之前用反射獲取的節點List<Node>中查找是否有對應的字符串
- 是否為數字
- 是否為左右括號
- 是否為"," ,函數中的多個參數是用逗號進行分隔。
- 是否為操作符節點
- ...
具體細節見代碼吧
語法分析
從中序從構建多叉樹的經典算法“數據結構”中都應該有講到,即先將中序轉化為后序或前序(本文轉化成后序),然后再將后序轉化成多叉樹。
Syntax.cs中有四個靜態函數:
- Node Analyse(List<Node> nodes); 對外接口,將中序的參數轉化成多叉樹,返回根節點。
- Node CreateSyntaxTree(List<Node> nodes, int first, int end); //nodes全部的中序結構,[first,end]表示要將中序結構中的哪一部分轉化成樹形。
- Node CreateSyntaxTree(List<Node> after) // 這個方法的參數已經是后序形式,沒有括號,函數的參數也合並到函數的子節點后,所以這里就是“數據結構”課程中最經典的,使用一個棧將后序轉化成樹。
- List<Node> FindParameters(List<Node> nodes, int start, ref int end) 在函數的括號中,找到參數,這些參數用逗號分開,返回所以的參數。start指向左括號的位置,end返回函數的右括號。這個函數中可能會遞歸調用到CreateSyntaxTree(),因為參數也有可能是復雜的表示式。
下面詳細介紹 CreateSyntaxTree(List<Node> nodes, int first, int end)——
這個函數中遍歷nodes,對每個節點先判斷
- 是否為數字節點或常量節點,如果是則添加到后序列表中。
- 如果為左括號,則把左括號添加到棧
- 如果為右括號,則一直彈出棧中元素,直到遇到棧中的左括號
- 如果是函數節點,則使用下面的FindParameters函數將所有參數添加到這個節點的子節點中后,再將這個節點添加到后序
- 如果是操作符節點,先看棧中有先元素,如果沒有元素直接把操作符放到棧中,如果有元素,則比較棧最后一個節點和此節點的優先級,如果棧中優先級高則彈出棧節點放到后序,並把此節點放入棧,否則將此節點放入棧。
上面這個過程也是數據結構中的經典算法。
得到正確的樹型后,就只需要簡單地調用Head.Value即可引爆多叉樹的求值過程(四年前本人是使用屬性,其實現在看來用函數更合理一些)。
結束語
最后提供程序的exe和全部源碼。
SuperCalculator_exe http://files.cnblogs.com/files/xiangism/SuperCalculator_exe.rar
SuperCalculator http://files.cnblogs.com/files/xiangism/SuperCalculator.rar
exe中在控制台直接輸入想要計算的表達式,然后回車即可看到結果。
補充:sin,pow之類的函數因為精度的需要,所以在正常函數的基礎上添加了一個表示精度的可選參數。
sin(PI) = 0.0000000000000032 sin(PI, 30) = 0.0000000000000032384627081457275276040217744770360060341499422643 44376687740626754440803176096879519813709774691978013205 pow(2.1, 2.1) = 4.74963807 pow(2.1, 2.1, 30) = 4.7496380917422417156885305942099853613159436030728012667686 29382182863206610681766137388241594625579055187057232218446673

