后綴表達式簡介
后綴表達式,簡單地說,就是一種運算符在操作數后面的表達式,后綴表達式有個很重要的特點就是可以去掉中綴表達式的括號但是又保留運算的優先級,這樣便於計算機計算表達式。而我們數學上使用的是中綴表達式,(表達式不包括雙引號)
例如“1+2*(-5)”,把這個表達式轉成后綴表達式就是“1 2 -5 * +”。
手動將中綴表達式轉后綴表達式的方法
手動將中綴轉后綴的方法這里也說一下,以上面的這個“1+2*(-5)”表達式為例,我們先根據運算的順序為表達式加上括號,處理成這樣“(1+(2*(-5)))”,然后我們根據運算的先后順序,逐步執行“把運算符放到兩個操作數后面的右括號后面,並去掉相應的括號”的操作。按照運算順序,我們應該先處理表達式最里層的“(2*(-5))”,那么處理后就變成這樣“ 2 -5 * ”,此時整個表達式是這樣“ (1 + 2 -5 *) ”,(這里其實邏輯上是這樣的“ (1 + (2 -5 *)) ”,表示“(2 -5 *)”被當做一個操作數,但是我們不加這個里層的括號也行),我們繼續按規則處理,然后變成這樣“ 1 2 -5 * + ” ,那么到這里我們就手動把一個中綴表達式轉成后綴表達式了。理解手動轉換的過程有利於我們后面理解程序的轉換邏輯。
實現思路以及代碼
先貼一下我實現的ExpParser的代碼:https://github.com/Melonl/ExpParser
源碼中我簡單封裝了一個ExpParser類,使用時直接在ExpParser對象上調用parser()方法,將要處理的數學表達式傳進去即可得到返回的double類型的結果。源碼中的Array和ArrayStack都是我為了練手手寫的簡陋的動態數組和基於動態數組的棧。下面解釋一下相關函數的實現思路和原理。
首先是一些細節的處理函數:
bool isOp(string s)//判斷是否是操作符
bool isNumStr(string s)//判斷是否是數字
bool checkInput(string &str)//檢查輸入的字符是否合法,包括括號的匹配
double s2d(string s)//string to double,將一個小數字符串轉為double
int getPriority(string opstr)//獲取傳入的操作符的優先級,返回值越低優先級越高
下面說說核心函數:
splitExp(string s)
首先是將整個表達式string按是否是操作符分割的函數splitExp(),返回分割后的字符串數組,容器為自己實現的Array動態數組
Array<string> splitExp(string s)
{
Array<string> tmp;
for (int i = 0; i < s.size(); i++)
{
string num;
while (isdigit(s[i]) || s[i] == '.')
{
num += s[i];
i++;
}
if (num.size())
{
tmp.addLast(num);
}
if (isOp(s[i]))
{
string ss;
ss += s[i];
tmp.addLast(ss);
}
}
//處理減號
for (int i = 0; i < tmp.getSize(); i++)
{
if (i == 0 && tmp.getSize() > 1 && tmp[i] == "-" && isdigit(tmp[i + 1][0]))
{
tmp[i + 1].insert(0, "-");
tmp.remove(i);
}
else if (i == 0 && tmp.getSize() > 1 && tmp[i] == "-" && tmp[i + 1][0] == '(')
{
string s = "0";
tmp.addFirst(s);
}
else if (tmp[i] == "-" && i != 0 && !isdigit(tmp[i - 1].back()) && i != tmp.getSize() - 1 && isdigit(tmp[i + 1][0]))
{
if (isOp(tmp[i - 1]))
{
tmp[i + 1].insert(0, "-");
tmp.remove(i);
}
}
}
return tmp;
}
這個函數的實現思路是:首先根據運算符和左右括號分割字符串,將運算符和字符串分開,注意,此時不認為小數中的點是運算符,但是負號按照減號處理。然后掃描整個分割好的字符串數組處理負號,邏輯如下:
1.如果“-”出現在表達式首位並且后面緊跟數字,那么此時為負號,合並到后面的數字字符串里去
2.如果“-”出現在表達式首位並且后面緊跟左括號,那么此時為負號,在整個字符串數組首位插個0處理成 “ 0 - (xxx)”的形式
3.如果“-”出現在左括號后面,那么此時為負號,合並到后面的數字字符串中
因為輸入的是一個規整的數學表達式,所以我覺得這個邏輯不會有太大的問題。
toSuffixExp(Array<string> &expstr)
這個函數把分割好的字符串數組轉成后綴表達式,依舊返回一個字符串數組。
關於中綴轉后綴的代碼寫法可以參考這篇博客:https://www.cnblogs.com/whlook/p/7143327.html
這篇博客講解得非常詳細,我自己講估計講不了這么清楚,所以還是請各位去看這篇博客吧。我的代碼也是參考這篇博客的講解實現的,貼下我的實現代碼:
Array<string> toSuffixExp(Array<string> &expstr)
{
ArrayStack<string> opstk;
ArrayStack<string> outstk;
for (int i = 0; i < expstr.getSize(); i++)
{
if (isNumStr(expstr[i]))
{
outstk.push(expstr[i]);
continue;
}
else
{
if (expstr[i][0] == '(')
{
opstk.push(expstr[i]);
continue;
}
else if (expstr[i][0] == ')')
{
while (1)
{
if (opstk.top()[0] == '(')
{
opstk.pop();
break;
}
else
{
outstk.push(opstk.pop());
}
}
continue;
}
else if (opstk.size() == 0 || getPriority(expstr[i]) < getPriority(opstk.top()) || opstk.top() == "(")
{
opstk.push(expstr[i]);
continue;
}
else if (isOp(expstr[i]) && getPriority(expstr[i]) >= getPriority(opstk.top()))
{
while (opstk.size() != 0 && getPriority(expstr[i]) >= getPriority(opstk.top()))
{
outstk.push(opstk.pop());
}
opstk.push(expstr[i]);
}
}
}
int tmpsz = opstk.size();
for (int i = 0; i < tmpsz; i++)
{
outstk.push(opstk.pop());
}
ArrayStack<string> tmpstk;
Array<string> res;
int size = outstk.size();
for (int i = 0; i < size; i++)
{
tmpstk.push(outstk.pop());
}
while (tmpstk.size() != 0)
{
res.addLast(tmpstk.pop());
}
return res;
}
cal(string &num2, string &num1, string &op)
這個函數實現了對后綴表達式的計算,具體的邏輯還是很簡單的,要注意下參數的num2才是第一個操作數,num2是第二個操作數。代碼如下
string cal(string &num2, string &num1, string &op)
{
//cout << "cal:" << num1 << "," << num2 << endl;
double n1, n2, res;
if (op.size() != 1)
{
cout << "cal error: " << op << endl;
return "";
}
switch (op[0])
{
case '+':
res = s2d(num1) + s2d(num2);
break;
case '-':
res = s2d(num1) - s2d(num2);
break;
case '*':
res = s2d(num1) * s2d(num2);
break;
case '/':
res = s2d(num1) / s2d(num2);
break;
case '^':
res = pow(s2d(num1), s2d(num2));
break;
default:
cout << "cal error: not find operator for " << op[0] << endl;
break;
}
return to_string(res);
}
總結
總的來說后綴表達式的計算還是比較簡單的,只是邏輯稍微繁瑣了點 ,耐心理清步驟就可以實現出來,關於ExpParser這個項目,后期我可能會嘗試加入對 對數、根號、求余、階乘等運算的支持,感覺有幫助的話不妨在github star一下我的項目,感謝閱讀。