调度场算法可以把一个中缀表达式转换为后缀表达式,后缀表达式可以很轻松的被转换为二叉表达树。
中缀表示法(或中缀记法)是一个通用的 算术 或 逻辑 公式表示方法, 操作符 是以中缀形式处于 操作数 的中间(例: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;
}