在讲述算法之前,我们需要先学习几个概念。
中缀表示法
中缀表示法就是我们人书写表达式的方法,如8/4+3*(6-2)。
后缀表示法
后缀表示法是从中缀表示法转化过来的,它满足以下条件:
(1)操作数的顺序与中缀表达式一致。
(2)没有括号。
(3)操作符没有优先级之分。
例如上面的表达式,其后缀形式是:84/362-*+
后缀表达式的特点对计算机计算非常有利。
二元运算符
需要两个操作数的运算符,例如是加法、减法、乘法、乘方、大于、等于等。
一元运算符
只需要一个操作数的运算符,例如是负数、逻辑非、正弦、平方根等。
我们下面介绍的算法,将支持以下运算:
数值运算 | 逻辑运算 | |
---|---|---|
二元 | + - * / %(求余) ^(乘方) | and or & | |
e(科学计数法) | < > = != <= >= | |
一元 | + - | ! |
sin cos tan asin acos atan | ||
sqrt lg |
利用这些运算,如果希望增加其他运算,会是很容易的。
一、表达式预处理
预处理包括去除空白字符、转成小写、判断括号是否匹配,以及加入结束符。
private static string PreTreat(string exp)
{
if (string.IsNullOrEmpty(exp))
{
throw new Exception("表达式为空");
}
exp = exp.Replace(" ", "").Replace("\t", "").Replace("\r", "").Replace("\n", "");
if (string.IsNullOrEmpty(exp))
{
throw new Exception("表达式为空");
}
if (!IsBracketMatch(exp))
{
throw new Exception("左右括号不匹配");
}
exp = exp.ToLower();
exp = exp + "@";
return exp;
}
结束符的作用是不需要每次扫描都判断是否越界。假如我们的表达式是8.1*sqrt(4)/-4+3*(16-23),经过预处理后,就变成了8.1*sqrt(4)/-4+3*(16-23)@。
二、元素切割
我们的表达式是一个字符串,由一个一个的字符组成,字符与字符之间并没有联系。我们需要把上面的字符串,变成以下的一个操作数和运算符分隔的列表:8.1,*,sqrt,(,4,),/,-,4,+,3,(,16,-,23,),@
切割的代码如下:
private static List<string> Segment(string exp)
{
List<string> segments = new List<string>();
int current = 0;
int pre_type = 1;//上一个类型,1符号,2字母,3数字
while (current < exp.Length)
{
if (SPLIT_SYMBOL.Contains(exp[current]))
{
segments.Add(exp[current].ToString());
switch (exp[current])
{
case '<':
switch (exp[current + 1])
{
case '>':
case '=':
segments[segments.Count - 1] += exp[current + 1];
current++;
break;
}
break;
case '>':
case '!':
switch (exp[current + 1])
{
case '=':
segments[segments.Count - 1] += exp[current + 1];
current++;
break;
}
break;
}
pre_type = 1;
}
else
{
int type = 3;
if (LETTER.IsMatch(exp[current].ToString()))
{
type = 2;
}
if (pre_type != type)
{
segments.Add(exp[current].ToString());
}
else
{
segments[segments.Count - 1] += exp[current];
}
pre_type = type;
}
current++;
}
return segments;
}
三、转成后缀表示法
转换的方法如下:
(1)初始化一个堆栈和一个队列,堆栈存放中间过程,队列存放结果。(结果也可以用堆栈存放,不过要反过来装载一遍)
(2)从左到右读入中缀表达式,每次一个元素。
(3)如果元素是操作数,将它添加到结果队列。
(4)如果元素是个运算符,弹出运算符,直至遇见左括号、优先级较低的运算符。把这个运算符压入堆栈。
(5)如果元素是个左括号,把它压入堆栈。
(6)如果元素是个右括号,在遇见左括号前,弹出所有运算符,然后把它们添加到结果队列。
(7)如果到达输入字符串的末尾,弹出所有运算符并添加到结果队列。
代码如下:
private static Stack<object> Parse(List<string> segments)
{
Stack<object> syntax = new Stack<object>();//语法单元堆栈
Stack<object> operands = new Stack<object>();//操作数堆栈
Stack<Operator> operators = new Stack<Operator>();//运算符堆栈
bool pre_opt = true;//前一个符号是运算符
for (int i = 0; i < segments.Count; i++)
{
string ele = segments[i];
if (ALL_OPERATOR.Contains(ele))//运算符
{
Operator opt = new Operator(ele, !pre_opt);
pre_opt = true;
//若当前运算符为结束运算符,则停止循环
if (opt.Type == OperatorType.END)
{
break;
}
//若当前运算符为左括号,则直接存入堆栈。
if (opt.Type == OperatorType.LB)
{
operators.Push(new Operator(OperatorType.LB));
continue;
}
//若当前运算符为右括号,则依次弹出运算符堆栈中的运算符并存入到操作数堆栈,直到遇到左括号为止,此时抛弃该左括号.
if (opt.Type == OperatorType.RB)
{
while (operators.Count > 0)
{
if (operators.Peek().Type != OperatorType.LB)
{
operands.Push(operators.Pop());
}
else
{
operators.Pop();
break;
}
}
pre_opt = false;
continue;
}
//若运算符堆栈为空,或者若运算符堆栈栈顶为左括号,则将当前运算符直接存入运算符堆栈.
if (operators.Count == 0 || operators.Peek().Type == OperatorType.LB)
{
operators.Push(opt);
continue;
}
//若当前运算符优先级大于运算符栈顶的运算符,则将当前运算符直接存入运算符堆栈.
if (opt.CompareTo(operators.Peek()) > 0)
{
operators.Push(opt);
}
else
{
//若当前运算符若比运算符堆栈栈顶的运算符优先级低或相等,则输出栈顶运算符到操作数堆栈,直至运算符栈栈顶运算符低于(不包括等于)该运算符优先级,
//或运算符栈栈顶运算符为左括号
//并将当前运算符压入运算符堆栈。
while (operators.Count > 0)
{
if (opt.CompareTo(operators.Peek()) <= 0 && operators.Peek().Type != OperatorType.LB)
{
operands.Push(operators.Pop());
if (operators.Count == 0)
{
operators.Push(opt);
break;
}
}
else
{
operators.Push(opt);
break;
}
}
}
}
else//操作数
{
operands.Push(ele);
pre_opt = false;
}
}
//转换完成,若运算符堆栈中尚有运算符时,
//则依序取出运算符到操作数堆栈,直到运算符堆栈为空
while (operators.Count > 0)
{
operands.Push(operators.Pop());
}
//调整操作数栈中对象的顺序并输出到最终栈
while (operands.Count > 0)
{
syntax.Push(operands.Pop());
}
return syntax;
}
对于+、-符号的判断,我们采用以下方法:
(1)如果符号前是非右括号的运算符,则判定为正号、负号。
(2)除此,判定为加号、减号。
经过这一步骤后,上述的表达式转成如下的后缀表达式:
8.1,4,sqrt,*,4,-(负号),/,3,16,23,-,*,+
四、对后缀表达式进行计算
后缀表达式的计算还是比较简单的,方法如下:
(1)初始化一个空堆栈。
(2)从左到右读入后缀表达式。
(3)如果字符是一个操作数,把它压入堆栈。
(4)如果字符是个运算符,弹出两个操作数,执行恰当操作,然后把结果压入堆栈。
(5)到后缀表达式末尾,从堆栈中弹出结果。
实现代码:
private static string Evaluate(Stack<object> syntax)
{
if (syntax.Count == 0)
{
return null;
}
Stack<string> opds = new Stack<string>();
Stack<object> pars = new Stack<object>();
string opdA, opdB;
double numA = 0, numB = 0;
string strA = "", strB = "";
foreach (object item in syntax)
{
if (item is string)
{
opds.Push((string)item);
}
else
{
switch (((Operator)item).Type)
{
//二元
case OperatorType.POW://乘方
case OperatorType.SCI://科学记数法
case OperatorType.FIX://定点
case OperatorType.MUL://乘
case OperatorType.DIV://除
case OperatorType.MOD://余
case OperatorType.ADD://加
case OperatorType.SUB://减
case OperatorType.LT://小于
case OperatorType.GT://大于
case OperatorType.LE://小于等于
case OperatorType.GE://大于等于
opdA = opds.Pop();
opdB = opds.Pop();
if (!double.TryParse(opdA.ToString(), out numA) || !double.TryParse(opdB.ToString(), out numB))
{
throw new Exception("操作数必须均为数字");
}
break;
case OperatorType.AND://与
case OperatorType.OR://或
opdA = opds.Pop();
opdB = opds.Pop();
strA = opdA.ToString().ToLower();
strB = opdB.ToString().ToLower();
if (strA == "1" || strA == "true")
{
strA = "1";
}
else if (strA == "0" || strA == "false")
{
strA = "0";
}
else
{
throw new Exception("操作数必须均为逻辑类型");
}
if (strB == "1" || strB == "true")
{
strB = "1";
}
else if (strB == "0" || strB == "false")
{
strB = "0";
}
else
{
throw new Exception("操作数必须均为逻辑类型");
}
break;
case OperatorType.ET://等于
case OperatorType.UT://不等于
opdA = opds.Pop();
opdB = opds.Pop();
strA = opdA.ToString().ToLower();
strB = opdB.ToString().ToLower();
if (strA == "1" || strA == "true")
{
strA = "1";
}
else if (strA == "0" || strA == "false")
{
strA = "0";
}
if (strB == "1" || strB == "true")
{
strB = "1";
}
else if (strB == "0" || strB == "false")
{
strB = "0";
}
break;
//一元
case OperatorType.PS://正
case OperatorType.NS://负
case OperatorType.TAN://正切
case OperatorType.ATAN://反正切
case OperatorType.SIN://正弦
case OperatorType.ASIN://反正弦
case OperatorType.COS://余弦
case OperatorType.ACOS://反余弦
case OperatorType.SQRT://开平方
case OperatorType.LG://对数
opdA = opds.Pop();
if (!double.TryParse(opdA.ToString(), out numA))
{
throw new Exception("操作数必须为数字");
}
break;
case OperatorType.NOT://非
opdA = opds.Pop();
strA = opdA.ToString().ToLower();
if (strA == "1" || strA == "true")
{
strA = "1";
}
else if (strA == "0" || strA == "false")
{
strA = "0";
}
else
{
throw new Exception("操作数必须为逻辑类型");
}
break;
}
switch (((Operator)item).Type)
{
//二元
case OperatorType.POW://乘方
opds.Push(Math.Pow(numB, numA).ToString());
break;
case OperatorType.SCI://科学记数法
opds.Push((numB * Math.Pow(10, numA)).ToString());
break;
case OperatorType.FIX://定点
opds.Push(numB.ToString("F" + numA));
break;
case OperatorType.MUL://乘
opds.Push((numB * numA).ToString());
break;
case OperatorType.DIV://除
opds.Push((numB / numA).ToString());
break;
case OperatorType.MOD://余
opds.Push((numB % numA).ToString());
break;
case OperatorType.ADD://加
opds.Push((numB + numA).ToString());
break;
case OperatorType.SUB://减
opds.Push((numB - numA).ToString());
break;
case OperatorType.AND://与
if (strA == "1" && strB == "1")
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
case OperatorType.OR://或
if (strA == "1" || strB == "1")
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
case OperatorType.ET://等于
if (strA == strB)
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
case OperatorType.UT://不等于
if (strA != strB)
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
case OperatorType.LT://小于
if (numB < numA)
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
case OperatorType.GT://大于
if (numB > numA)
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
case OperatorType.LE://小于等于
if (numB <= numA)
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
case OperatorType.GE://大于等于
if (numB >= numA)
{
opds.Push("1");
}
else
{
opds.Push("0");
}
break;
//一元
case OperatorType.PS://正
opds.Push(numA.ToString());
break;
case OperatorType.NS://负
opds.Push((-numA).ToString());
break;
case OperatorType.TAN://正切
opds.Push(Math.Tan(numA).ToString());
break;
case OperatorType.ATAN://反正切
opds.Push(Math.Atan(numA).ToString());
break;
case OperatorType.SIN://正弦
opds.Push(Math.Sin(numA).ToString());
break;
case OperatorType.ASIN://反正弦
opds.Push(Math.Asin(numA).ToString());
break;
case OperatorType.COS://余弦
opds.Push(Math.Cos(numA).ToString());
break;
case OperatorType.ACOS://反余弦
opds.Push(Math.Acos(numA).ToString());
break;
case OperatorType.SQRT://开平方
opds.Push(Math.Sqrt(numA).ToString());
break;
case OperatorType.LG://对数
opds.Push(Math.Log10(numA).ToString());
break;
case OperatorType.NOT://非
if (strA == "1")
{
opds.Push("0");
}
else
{
opds.Push("1");
}
break;
}
}
}
if (opds.Count == 1)
{
return opds.Pop();
}
return null;
}
至此,表达式的求值完成。