調度場算法可以把一個中綴表達式轉換為後綴表達式,後綴表達式可以很輕鬆的被轉換為二叉表達樹。
中綴表示法(或中綴記法)是一個通用的 算術 或 邏輯 公式表示方法, 操作符 是以中綴形式處於 操作數 的中間(例:3 + 4)。與 前綴表達式 (例:+ 3 4 )或 後綴表達式 (例:3 4 + )相比,中綴表達式不容易被 電腦 解析,但仍被許多 程式語言 使用,因為它符合人們的普遍用法。
—— 維基百科
算法#
文字版#
- 當還有記號可以讀取時:
- 讀取一個記號。
- 如果這個記號表示一個數字,那麼將其添加到輸出隊列中。
- 如果這個記號表示一個函數,那麼將其壓入棧當中。
- 如果這個記號表示一個函數參數的分隔符(例如,一個半角逗號,):
- 從棧當中不斷地彈出操作符並且放入輸出隊列中去,直到棧頂部的元素為一個左括號為止。如果一直沒有遇到左括號,那麼要麼是分隔符放錯了位置,要麼是括號不匹配。
- 如果這個記號表示一個操作符,記做 o1,那麼:
- 只要存在另一個記為 o2 的操作符位於棧的頂端,並且
如果 o1 是左結合性的並且它的運算符優先級要小於或者等於 o2 的優先級,或者如果 o1 是右結合性的並且它的運算符優先級比 o2 的要低,那麼將 o2 從棧的頂端彈出並且放入輸出隊列中(循環直至以上條件不滿足為止); - 然後,將 o1 壓入棧的頂端。
- 只要存在另一個記為 o2 的操作符位於棧的頂端,並且
- 如果這個記號是一個左括號,那麼就將其壓入棧當中。
- 如果這個記號是一個右括號,那麼:
- 從棧當中不斷地彈出操作符並且放入輸出隊列中,直到棧頂部的元素為左括號為止。
- 將左括號從棧的頂端彈出,但並不放入輸出隊列中去。
- 如果此時位於棧頂端的記號表示一個函數,那麼將其彈出並放入輸出隊列中去。
- 如果在找到一個左括號之前棧就已經彈出了所有元素,那麼就表示在表達式中存在不匹配的括號。
- 當再沒有記號可以讀取時:
- 如果此時在棧當中還有操作符:
- 如果此時位於棧頂端的操作符是一個括號,那麼就表示在表達式中存在不匹配的括號。
- 將操作符逐個彈出並放入輸出隊列中。
- 如果此時在棧當中還有操作符:
- 退出算法。
狀態機#
我把上面的一大串文字梳理一下,其實就是這麼一個狀態機。為了簡單一點,沒有考慮表達式錯誤的情況。
Mermaid Loading...
函數就是 sin(x)
, tan(x)
這種。
引用一下維基百科的例子#
我們輸入一個 3 + 4 * 2 / (1 - 5)^ (2 ^ 3)
輸入 | 動作 | 輸出隊列 | 運算符棧 | 提示 |
---|---|---|---|---|
3 | 將符號加入輸出隊列 | 3 | ||
+ | 將符號壓入操作符堆棧 | 3 | + | |
4 | 將符號加入輸出隊列 | 3 4 | + | |
* | 將符號壓入操作符堆棧 | 3 4 | * + | * 號的優先級高於 + 號 |
2 | 將符號加入輸出隊列 | 3 4 2 | * + | |
/ | 將堆棧中元素彈出,加入輸出隊列 | 3 4 2 * | + | / 號和 * 號優先級相同 |
/ | 將符號壓入操作符堆棧 | 3 4 2 * | / + | / 號的優先級高於 + 號 |
( | 將符號壓入操作符堆棧 | 3 4 2 * | ( / + | |
1 | 將符號加入輸出隊列 | 3 4 2 * 1 | ( / + | |
- | 將符號壓入操作符堆棧 | 3 4 2 * 1 | - ( / + | |
5 | 將符號加入輸出隊列 | 3 4 2 * 1 5 | - ( / + | |
) | 將堆棧中元素彈出,加入輸出隊列 | 3 4 2 * 1 5 − | ( / + | 循環直到找到 (號 |
) | 將堆棧元素彈出 | 3 4 2 * 1 5 − | / + | 括號匹配結束 |
^ | 將符號壓入操作符堆棧 | 3 4 2 * 1 5 − | ^ / + | ^ 號的優先級高於 / 號 |
2 | 將符號加入輸出隊列 | 3 4 2 * 1 5 − 2 | ^ / + | |
^ | 將符號壓入操作符堆棧 | 3 4 2 * 1 5 − 2 | ^ ^ / + | ^ 號為從右至左求值 |
3 | 將符號加入輸出隊列 | 3 4 2 * 1 5 − 2 3 | ^ ^ / + | |
END | 將棧中所有數據加入輸出隊列 | 3 4 2 * 1 5 − 2 3 ^ ^ / + |
CPP 實現#
把上面的算法理解了之後實現應該就非常簡單了。下面這個代碼沒有實現函數,實現了數字和操作符。但理解了結構,實現一個函數應該還是不難。
#include <iostream>
#include <string>
#include <stack>
#include <vector>
#include <stdexcept>
#include <cmath>
#define is_operator(c) (c == ‘+’ || c == ‘-‘ || c == ‘/‘ || c == ‘*’ || c == ‘%’ || c == ‘^’)
#define is_left_operator(c) (c == ‘+’)
#define is_others(c) (c == ‘(‘ || c == ‘)’)
using namespace std;
// 解析樹結構體
struct ParseTree{
string data; // 存的數據
int result = -1; // 子樹計算的結果
// int diff = -1; // 自動求導可能會用上,這裡用不上
struct ParseTree* left = nullptr;
struct ParseTree* right = nullptr;
};
// 操作符棧
stack<char> op_stack;
// 解析樹數組
stack<ParseTree*> parse_stack;
// 輸出隊列
vector<string> o_vector;
// 這裡定義的五種類型,分別是 數字,函數,操作符,默認,其他。
// 函數在這篇裡沒有實現,只實現了操作符。但結構都在這裡了,實現一個函數問題應該不大
enum CharType {Number, Function, Operator, Default, Others};
// 字符轉數字
int char2int(char c){
return c - ‘0’;
}
//初始化全局變量
void initVariables() {
while (!op_stack.empty()) {
op_stack.pop();
}
while (!parse_stack.empty()) {
parse_stack.pop();
}
o_vector.clear();
}
// + == - < * == / == % < ^
// 1 1 2 2 2
// 得到操作符的優先級
int getPriority(char op1) {
if (op1 == ‘(‘ || op1 == ‘)’)
return 0;
if (op1 == ‘+’ || op1 == ‘-‘)
return 1;
if (op1 == ‘*’ || op1 == ‘/‘ || op1 == ‘%’)
return 2;
if (op1 == ‘^’)
return 3;
throw invalid_argument(“ received invalid operator “);
}
//得到字符所屬的類別
CharType getType(char c) {
CharType type = Default;
if (‘0’ <= c && c <= ‘9’) {
type = Number;
}
if (is_operator(c)) {
type = Operator;
}
if (is_others(c)) {
type = Others;
}
return type;
}
//將操作符棧的頂部轉移到輸出隊列
void move2queue() {
if (op_stack.empty()) {
throw invalid_argument(“ cannot pop a empty stack “);
}
char top = op_stack.top();
string temp(1, top);
o_vector.push_back(temp);
op_stack.pop();
}
//將一個數字 push 到輸出隊列
void push_accu(int accu) {
if (accu < 0)
return;
string temp = to_string(accu);
o_vector.push_back(temp);
}
//調度場主要算法
bool shunting_yard(string input){
int length = (int)input.length();
// 這個 Flag 主要用來標記是否是連續的數字,連續的數字要合併為一個數字。
bool flagContinuousNumber = false;
int accumulator = -1;
// Begin Loop
for (int i = 0; i < length; i++){
CharType type = getType(input[i]);
if (type == Default){
flagContinuousNumber = false;
push_accu(accumulator);
accumulator = -1;
continue;
}
if (type == Number) {
if (flagContinuousNumber) { // last one is a number
accumulator = accumulator * 10 + char2int(input[i]);
} else { // last one is not a number, but this one is.
flagContinuousNumber = true;
accumulator = char2int(input[i]);
}
continue;
} else {
flagContinuousNumber = false;
push_accu(accumulator);
accumulator = -1;
}
if (type == Operator) {
while(!op_stack.empty()) {
if (getPriority(input[i]) <= getPriority(op_stack.top())) {
move2queue();
} else {
break;
}
}
op_stack.push(input[I]);
continue;
}
if (type == Others) {
if (input[I] == ‘(‘) {
op_stack.push(input[I]);
}
if (input[I] == ‘)’) {
while (op_stack.top() != ‘(‘) {
move2queue();
}
op_stack.pop();
}
continue;
}
}
if (flagContinuousNumber)
push_accu(accumulator);
while (!op_stack.empty()) {
char top = op_stack.top();
if (top == ‘(‘ || top == ‘)’) {
return false;
}
string temp(1, top);
o_vector.push_back(temp);
op_stack.pop();
}
return true;
}
int main(){
initVariables();
// const string input = "(1+2) * (3 * (4+5))"; // 81
const string input = "4+ 2 ^ 3 ^ 2 + 4 / 2"; // 516
shunting_yard(input);
return 0;
}