C++で作る基本電卓 III ― 括弧と四則演算を含む数式を評価するアルゴリズム
問題概要
数式を表す文字列が与えられ、その式を評価する基本的な電卓を実装することを考えます。式の文字列には、開き括弧と閉じ括弧、加算「+」や減算「-」の記号、非負の整数、そして空白が含まれる可能性があります。つまり、式には非負の整数と「+」「-」「*」「/」の各演算子、開き括弧・閉じ括弧、空白のみが登場します。整数同士の除算では、結果をゼロ方向へ切り捨てるものとします。
例として、入力が「6-4 / 2」であれば、出力は 4 になります。
アルゴリズムの考え方
この問題は、スタックを活用して優先順位の異なる2種類の演算子(加減算レベルと乗除算レベル)を管理することで解けます。使用する変数の役割は次のとおりです。
- l1: これまでに確定した部分和
- o1: 直前の加減算演算子(+ なら 1、- なら -1)
- l2: 現在処理中の項の値
- o2: 直前の乗除算演算子(* なら 1、/ なら -1)
具体的な手順は以下のとおりです。
- l1 := 0、l2 := 1 と初期化する
- o1 := 1、o2 := 1 と初期化する
- スタック st を1つ定義する
- n := 文字列 s の長さとする
- i := 0 から始め、i < n の間、i を1ずつ増やしながら以下を繰り返す
- x := s[i] とする
- x が '0' 以上 '9' 以下(数字)の場合
- num := x - '0' とする
- (i + 1 < n かつ s[i + 1] が数字)である間、次を繰り返す
- i を1増やす
- num := (num * 10) + (s[i] - '0')
- l2 := (o2 が 1 ならば l2 * num、そうでなければ l2 / num)
- そうでなく x が '(' の場合
- st に l1、o1 の順で挿入する
- st に l2、o2 の順で挿入する
- l1 := 0、o2 := 1 とする
- o1 := 1、l2 := 1 とする
- そうでなく x が ')' の場合
- temp := l1 + o1 * l2 とする
- o2 := st の先頭要素を取得し、st から削除
- l2 := st の先頭要素を取得し、st から削除
- o1 := st の先頭要素を取得し、st から削除
- l1 := st の先頭要素を取得し、st から削除
- l2 := (o2 が 1 ならば l2 * temp、そうでなければ l2 / temp)
- そうでなく x が '*' または '/' の場合
- o2 := (x が '*' なら 1、そうでなければ -1)
- そうでなく x が '+' または '-' の場合
- x が '-' であり、かつ(i == 0 または (i - 1 >= 0 かつ s[i - 1] == '('))の場合
- o1 := -1 とする
- 以降の処理をスキップして次の反復へ進む
- l1 := l1 + o1 * l2
- o1 := (x が '+' なら 1、そうでなければ -1)
- l2 := 1、o2 := 1 とする
- x が '-' であり、かつ(i == 0 または (i - 1 >= 0 かつ s[i - 1] == '('))の場合
- 最後に l1 + o1 * l2 を返す
理解を深めるために、以下の実装例を見てみましょう。
実装例(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long int lli;
class Solution {
public:
int calculate(string s) {
lli l1 = 0;
lli l2 = 1;
lli o1 = 1;
lli o2 = 1;
stack<lli> st;
lli n = s.size();
for (lli i = 0; i < n; i++) {
char x = s[i];
if (x >= '0' && x <= '9') {
lli num = x - '0';
while (i + 1 < n && s[i + 1] >= '0' && s[i + 1] <= '9') {
i++;
num = (num * 10) + (s[i] - '0');
}
l2 = (o2 == 1) ? l2 * num : l2 / num;
}
else if (x == '(') {
st.push(l1);
st.push(o1);
st.push(l2);
st.push(o2);
l1 = 0;
o2 = 1;
o1 = 1;
l2 = 1;
}
else if (x == ')') {
lli temp = l1 + o1 * l2;
o2 = st.top();
st.pop();
l2 = st.top();
st.pop();
o1 = st.top();
st.pop();
l1 = st.top();
st.pop();
l2 = (o2 == 1) ? l2 * temp : l2 / temp;
}
else if (x == '*' || x == '/') {
o2 = (x == '*') ? 1 : -1;
}
else if (x == '+' || x == '-') {
if (x == '-' && (i == 0 || (i - 1 >= 0 && s[i - 1] == '('))) {
o1 = -1;
continue;
}
l1 += o1 * l2;
o1 = (x == '+') ? 1 : -1;
l2 = 1;
o2 = 1;
}
}
return l1 + o1 * l2;
}
};
main(){
Solution ob;
cout << (ob.calculate("(5+9*3)/8"));
}
入力
"(5+9*3)/8"
出力
4
動作のポイント
このアルゴリズムでは、開き括弧「(」が出現した時点で現在の状態(l1、o1、l2、o2)をスタックに退避し、括弧内の計算をまっさらな状態で開始します。閉じ括弧「)」が出現すると、括弧内の計算結果 temp を確定させ、スタックから状態を復元した上で、temp を直前の乗除算演算子に応じて外側の式へ組み込みます。この仕組みにより、任意の深さに入れ子になった括弧にも正しく対応できます。
また、式の先頭や開き括弧の直後に現れる「-」は単項マイナス(符号)として扱う必要があるため、特別な判定を設けています。これにより「-(2+3)」のような式も正しく評価できます。
計算量については、文字列を一度だけ走査すればよいため時間計算量は O(n)、スタックには括弧のネストの深さに比例したデータが積まれるため空間計算量も O(n) となります。
-
C++で解くパス合計III:DFSで二分木のルートからリーフまでの経路を探索
整数のキーを持つノードで構成される二分木が与えられたとき、合計値が指定した値と一致する「ルートからリーフ(葉)までの経路」をすべて見つける問題を考えます。経路は必ず根から始まり、葉で終わる必要があります。 問題の例 たとえば、次のような二分木 [5,4,8,11,null,13,4,7,2,null,null,5,1] があり、目標の合計値が 22 だとします。 このとき、条件を満たす経路は次の 2 本です。 [[5, 4, 11, 2], [5, 8, 4, 5]] 解法のアプローチ:DFS(深さ優先探索)+バックトラック この問題は、少し手を加えた DFS(深さ優先探索)関数で効率的に解
-
C++で学ぶ基本のグラフィックプログラミング入門
C++は汎用性の高いプログラミング言語です。C++を使えば、基本的な図形の描画、スタイリッシュなフォントによる文字表示、色付けといった初歩的なグラフィックスも作成できます。グラフィックプログラミングは、ターミナルやコマンドプロンプトから直接行うこともできますし、DevC++コンパイラをダウンロードして開発環境を整える方法もあります。graphics.hライブラリのインストールターミナルでグラフィックプログラミングを行う場合は、GCCコンパイラにgraphics.hライブラリを追加する必要があります。以下のコマンドを順番に入力してください。>sudo apt-get install bui