C++實現對數學基本運算表達式的解析

代碼地址如下:<br>http://www.demodashi.com/demo/11078.html

前段時間在LeetCode上刷題,遇到了很多涉及對字符串進行解析的題目。可能是出於這個原因,最近迷戀上了字符串的解析問題。數學基本運算表達式的解析就涉及這類問題。所謂數學基本運算表達式的解析就是指給定一個表達式字符串,如1 + 1,3 * 9,對這個字符串進行解析,從而得到這個表達式的運算結果。(數學基本運算表達式也就是隻用加減乘除進行計算的數學表達式)

其實站在我的角度來看,我覺得對數學基本運算表達式的解析還是有一定難度的。因爲如果一開始沒有正確的思路,我們是很難找到這個問題的着手點的,畢竟解析數學基本運算表達式需要考慮到的問題是有點多的,以下,我把其中主要的問題列舉出來:

  • 乘除法優先計算
  • 括號裏的內容優先計算
  • 表達式中的數字前可能存在正負號

這些問題如果得不到恰當的處理,就會使解析過程失敗。

在實現解析數學基本運算表達式之前,我們首先得弄清楚哪些表達式是合法的,哪些表達式是不合法的。以下我將列舉C/C++等語言中,一些合法與不合法的表達式:

/* 合法 */
7 + 22 + 7 * 27
6 + -13 * 23 / -30 / 35
24 - +34
10 + (3 * 9) / 8

/* 不合法 */
abc + 123  // abc不是數字
1 + 2 *  // *後缺少數字
6 + + 12  // 不能連用加號
8 - - 17  // 不能連用減號
32 + () - 4  // 括號中沒有內容
(0 + 5)) * 7  // 右括號多了一個
((11 + 9) / 2  // 左括號多了一個

不排除有一些編程語言支持上述所說的部分或者全部不合法表達式,但這裏我們先使用C/C++標準。

原理講解

由於實現的過程很“繞圈子”,所以我就先把我的實現思路和原理告訴大家,供大家參考。

(可能網上有大佬提供了更好、更高效的解析數學表達式方法,不過我認爲我的處理方式是非常直白易懂的一種。)

首先,我們來回憶一下我們做數學計算時的情形:

第一步
如果數學運算表達式存在括號的話,我們會率先找到括號裏的內容,並將括號裏的內容當作一個新的數學表達式進行優先運算。將這個新的數學表達式進行運算後,用得到的結果將括號及括號間的內容替換掉。當我們把所有括號裏的內容都用相應的結果替換掉後,就能得到原先數學表達式消去括號後的簡化式子,然後我們再對這個式子進行處理。例如原有的數學式7 (5 + 3),進行消去括號的處理後,就得到了7 8,接下來我們再對這個式子進行解析和計算,就能得到答案。

第二步
消去括號的數學基本運算表達式就只剩加減乘除符號以及數字、小數點(如果有小數的話)了。由於乘法和除法要優先運算,如果式子中有這兩種運算的話,我們就要率先在式子中進行乘法運算和除法運算。運算的過程中,我們採用的做法同樣是把運算得到的結果替換掉原來的運算式子。如下計算過程示例可能會讓你重拾這一過程:

7 + 18 * 5 / 9
  |
  v
7 + 90 / 9
  |
  v
7 + 10

第三步
進行完前兩步的處理,剩下的式子就只有加減符號以及數字、小數點(如果有小數的話)了。用和第二步中類似的做法進行計算,最後,式子就化成了數字,這個數字就是我們的答案。以第二步示例中最後得到的式子爲例,7 + 10運算得到17,17就是最後的答案。

現在,我們回憶完了平日裏我們進行數學計算的步驟,其實這就是一個對表達式逐漸化簡的過程。接下來我們就要從這些步驟中構思我們的解析算法。

在第一步中,我們提到了對括號裏的內容進行優先計算。但是我們可能會遇到括號套括號的問題。但是正如第一步中所說,我們可以把括號裏的式子當作新的一個式子來處理,對於新的式子又可以採用第一步到第三步的方式依次進行處理得到結果,其中的第一步又可以對新式子中的子括號進行處理。這樣一來就形成了一種遞歸關係。所以,我們只用實現如下幾個接口,就能完成對數學基本運算式的解析:

  • 接口A:處理運算式的統一接口
  • 接口B:處理括號的接口
  • 接口C:處理運算符的接口,一次可處理兩種運算符(因爲四則運算符可以分成 乘除一組,加減一組)
  • 接口D:基本運算接口,即針對兩個數字之間四則運算的接口(因爲任意的數學表達式都可以通過兩兩計算進行化簡求值)
  • 接口E:處理字符串的一個接口集合,提供了剔除字符串左右空白或者所有空白的接口、float與std::string相互轉化的接口(爲了方便小數計算,使用float類型)

第一步用接口B來完成,第二、三步用接口C來完成。接口C中使用接口D來進行結果運算,即接口C進行的操作是找到運算符並獲取運算符兩邊的數字,然後交給接口D對這兩個數字按照找到的運算符進行計算,再將得到的計算結果替換掉式子中相應的部分。而接口E爲前幾個接口提供輔助功能。

接口A是調用其他接口的一個入口。接口A~D的僞代碼如下:

/**
 * 接口A
 * @param exp 需要解析的表達式
 */
float getValue(const string& exp)
{
    // 剔除表達式字符串的左右空白
    string __exp = 接口E(exp);

    // 處理括號
    handleBrackets(__exp);

    // 處理乘法除法運算
    handleOperator(__exp, pair<string, string>('*', '/'));
    // 處理加法減法運算
    handleOperator(__exp, pair<string, string>('+', '-'));

    // 將`std::string`轉化爲`float`
    float res = 接口E(__exp);

    // 返回結果
    return res;
}

/**
 * 接口B
 * @param exp 需要解析的表達式
 */
void handleBrackets(string& exp)
{
    for 遍歷`exp` {
        if 遇到括號 {
            string content = 獲取括號內容;

            // 將括號裏的內容作爲新的表達式處理
            int val = getValue(content);
            // 將`float`轉化爲`std::string`
            string valStr = 接口E(val);

            將`exp`括號部分替換爲valStr;
            更新`exp`長度和遍歷的下標位置;
        }
    }
}

/**
 * 接口C
 * @param exp 需要解析的表達式
 * @param operators 需要處理的運算符號(乘除一組,加減一組)
 */
void handleOperator(string& exp, pair<string, string> operators)
{
    for 遍歷`exp` {
        if 遇到operators中的運算符 {
            string strVal1 = 獲取進行計算的數字1(運算符左側數字);
            string strVal2 = 獲取進行計算的數字2(運算符右側數字);

            // 獲取計算結果
            float v = basicCalc(strVal1, 運算符, strVal2);
            // 將`float`轉化爲`std::string`
            string valStr = 接口E(v);

            將`exp`括號部分替換爲valStr;
            更新`exp`長度和遍歷的下標位置;
        }
    }
}

/**
 * 接口D
 * @param s1 用於計算的數字1
 * @param _operator 運算符
 * @param s2 用於計算的數字2
 */
void basicCalc(const string& s1, const string& _operator, const string& s2)
{
    // 將`std::string`轉化爲`float`
    float n1 = 接口E(s1);
    float n2 = 接口E(s2);

    根據運算符_operator進行n1 n2之間的計算;
}

(至於接口E,由於是一個提供輔助功能的接口集,所以上面的僞代碼中沒有給出。具體實現方法請參考給出的源代碼)

文件截圖:

C++實現對數學基本運算表達式的解析
上面的僞代碼還只是一個骨架,沒有血肉,還不能解決一些實際的問題(比如前文提到的如何處理括號,如何解決數字前的正負號等問題)。大家可以參考我給出的完整實現代碼。

當然,各位讀者也可以根據我前面的原理講解自行實現算法。C++實現對數學基本運算表達式的解析

代碼地址如下:<br>http://www.demodashi.com/demo/11078.html

注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章