調度場アルゴリズムは、中置式を後置式に変換することができ、後置式は二分木に簡単に変換できます。
中置記法(または中置表記)は、一般的な算術または論理式の表現方法であり、演算子はオペランドの間に中置形式で存在します(例:3 + 4)。前置式(例:+ 3 4)や後置式(例:3 4 +)と比較して、中置式はコンピュータによって解析されるのが難しいですが、多くのプログラミング言語で使用されています。なぜなら、それは人々の一般的な使用法に合致しているからです。
—— ウィキペディア
アルゴリズム#
テキスト版#
- 読み取るトークンがある限り:
- トークンを 1 つ読み取る。
- このトークンが数字を表す場合、それを出力キューに追加する。
- このトークンが関数を表す場合、それをスタックにプッシュする。
- このトークンが関数の引数の区切り文字(例えば、半角カンマ,)を表す場合:
- スタックから演算子をポップし続けて出力キューに入れ、スタックのトップの要素が左括弧になるまで続けます。左括弧に出会わない場合は、区切り文字の位置が間違っているか、括弧が不一致であることを示します。
- このトークンが演算子を表す場合、これを o1 とします:
- スタックのトップに o2 という別の演算子が存在し、かつ
もし o1 が左結合であり、その演算子の優先度が o2 の優先度以下であるか、または o1 が右結合であり、その演算子の優先度が o2 より低い場合、o2 をスタックのトップからポップして出力キューに入れます(上記の条件が満たされなくなるまでループします); - その後、o1 をスタックのトップにプッシュします。
- スタックのトップに o2 という別の演算子が存在し、かつ
- このトークンが左括弧である場合、それをスタックにプッシュします。
- このトークンが右括弧である場合:
- スタックから演算子をポップし続けて出力キューに入れ、スタックのトップの要素が左括弧になるまで続けます。
- 左括弧をスタックのトップからポップしますが、出力キューには入れません。
- この時、スタックのトップにあるトークンが関数を表す場合、それをポップして出力キューに入れます。
- 左括弧を見つける前にスタックがすべての要素をポップしてしまった場合、式に不一致の括弧が存在することを示します。
- 読み取るトークンがもうない場合:
- スタックに演算子がまだある場合:
- スタックのトップの演算子が括弧である場合、式に不一致の括弧が存在することを示します。
- 演算子を 1 つずつポップして出力キューに入れます。
- スタックに演算子がまだある場合:
- アルゴリズムを終了します。
状態機械#
上記の長い文章を整理すると、実際にはこのような状態機械になります。簡単にするために、式のエラーのケースは考慮していません。
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;
// ここで定義された5種類のタイプは、それぞれ数字、関数、演算子、デフォルト、その他です。
// この文では関数は実装されていませんが、演算子は実装されています。構造はここにあるので、関数を実装するのは問題ないでしょう。
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(“ 無効な演算子を受け取りました “);
}
//文字のタイプを取得
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(“ 空のスタックからポップできません “);
}
char top = op_stack.top();
string temp(1, top);
o_vector.push_back(temp);
op_stack.pop();
}
//数字を出力キューにプッシュ
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();
// このフラグは連続した数字を示すためのもので、連続した数字は1つの数字に統合されます。
bool flagContinuousNumber = false;
int accumulator = -1;
// ループ開始
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) { // 最後のものは数字
accumulator = accumulator * 10 + char2int(input[i]);
} else { // 最後のものは数字でなく、これは数字です。
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;
}