数式の処理
逆ポーランド記法 (1)
我々が日々格闘している相手であるコンピュータは, 電子計算機の呼称が示すとおり, いわば高級な電卓である. コンピュータのプロとしては, 電卓程度のアプリケーションくらいサラッと書く素養を持つべきである.
そのような信念の元, 魂の電卓シリーズ第1弾として, 今回は逆ポーランド記法について書いてみる.
逆ポーランド記法 (RPN) というのは, 数式を表現する方法の一つである. 通常我々が使用している“2+3*5-7”のような記法は, 演算の対象の間に演算子を挟み込んで記述するため「中置記法」と呼ばれることがある. これに対して逆ポーランド記法は「後置記法」である. “2+3*5-7”を逆ポーランド記法で表現すると“2 3 5 * + 7 -”となる.
通常の数式を逆ポーランド記法に変換するには, 次の単純なアルゴリズムがある.
- 数式を構成する各数値や演算子を左から右へ向かって処理していく. 各構成要素に対して 2, 3 の操作を繰り返す.
- 注目している要素が演算子であれば, 演算子スタックのトップが現在の演算子より優先順位が高い間繰返してポップして出力し, その後現在の演算子を演算子スタックへプッシュする.
- 注目している要素が数値であれば, 単純に出力する.
- すべての構成要素を 2, 3 の操作に基づいて処理し終えた後, 残っている演算子スタックの内容をすべてポップして出力する.
まずは逆ポーランド記法を利用して数式の内容を保持する Expr クラスを作ってみた. 関数機能やメモリ機能はおろか, エラー処理もないし, 括弧も単項演算子も使えないヘボ電卓であるが, これを徐々に改良して行くことで, 新たな話題を考えるのをサボろうという寸法である.
おっと, 実装言語は C# を使ってみた.
using System;
using System.Collections.Generic;
using System.Text;
namespace expr {
public enum TokenType
{
AdditiveOperator, MultiplicativeOperator, UnaryOperator,
Number, EndOfLine
}
public class InvalidSyntaxException: Exception
{
public InvalidSyntaxException() { }
public InvalidSyntaxException(string message): base(message) { }
}
public class Token
{
private TokenType type;
public Token(TokenType type) { this.type = type; }
public TokenType Type { get { return type; } }
public virtual string TextValue { get { return null; } }
public virtual int IntValue { get { return 0; } }
public override string ToString() { return "="; }
}
public class TextToken: Token
{
private string value;
public TextToken(TokenType type, string value): base(type)
{ this.value = value; }
public override string TextValue { get { return value; } }
public override string ToString() { return value; }
}
public class NumberToken: Token
{
private int value;
public NumberToken(int value): base(TokenType.Number)
{ this.value = value; }
public override int IntValue { get { return value; } }
public override string ToString() { return value.ToString(); }
}
public class Tokenizer
{
private string text;
private int position;
public Tokenizer(string text)
{
this.text = text;
this.position = 0;
}
public Token next()
{
char c;
do
{
if (position >= text.Length)
return new Token(TokenType.EndOfLine);
c = text[position++];
} while (Char.IsWhiteSpace(c));
int startPos = position - 1;
switch (c)
{
case '+': case '-':
return new TextToken(
TokenType.AdditiveOperator,
text.Substring(startPos, position - startPos));
case '*': case '/':
return new TextToken(
TokenType.MultiplicativeOperator,
text.Substring(startPos, position - startPos));
default:
if (Char.IsDigit(c))
{
StringBuilder sb = new StringBuilder();
for (; ; )
{
sb.Append(c);
if (position >= text.Length)
break;
if (!Char.IsDigit(c = text[position++]))
{
--position;
break;
}
}
return new NumberToken(Int32.Parse(sb.ToString()));
}
break;
}
throw new InvalidSyntaxException();
}
}
public class Expr
{
private List<Token> rpn;
public Expr(string text)
{
rpn = new List<Token>();
Tokenizer tokenizer = new Tokenizer(text);
Stack<Token> operators = new Stack<Token>();
Token token;
while ((token = tokenizer.next()).Type != TokenType.EndOfLine)
{
if (token.Type < TokenType.Number)
{
while (operators.Count > 0 &&
operators.Peek().Type >= token.Type)
rpn.Add(operators.Pop());
operators.Push(token);
}
else
rpn.Add(token);
}
while (operators.Count > 0)
rpn.Add(operators.Pop());
}
public int Evaluate()
{
Stack<int> operands = new Stack<int>();
foreach (Token token in rpn)
{
switch (token.Type)
{
case TokenType.AdditiveOperator:
case TokenType.MultiplicativeOperator:
int rhs = operands.Pop();
if (token.TextValue == "+")
operands.Push(operands.Pop() + rhs);
else if (token.TextValue == "-")
operands.Push(operands.Pop() - rhs);
else if (token.TextValue == "*")
operands.Push(operands.Pop() * rhs);
else // if (token.TextValue == "/")
operands.Push(operands.Pop() / rhs);
break;
case TokenType.Number:
operands.Push(token.IntValue);
break;
}
}
return operands.Pop();
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (Token token in rpn)
{
if (sb.Length > 0)
sb.Append(' ');
sb.Append(token);
}
return sb.ToString();
}
}
}
|