项目描述:输入需要计算的表达式字符串,若表达式不符合运算规则输出error描述信息,否则输出运算结果:
如表达式-0.5 + 4 * (5 + 6) - -5是可计算的,-0.5 + 4 (5 + 6)) - -5则有误
计算器的实现虽然是个小项目,但若不进行整体框架的构思,也会遇到很多的细节问题难以描述。因此,首先需要对项目进行分析,将其拆分为多个待解决问题的集合,然后逐个解决之,是为有效大体上可将计算器的实现分解为如下三个部分:
1. 将原始字符串按照不同类型分解为独立的字符串单元
2. 将分解所得的中缀字符串序列转换为后缀字符串序列
3. 计算转化后的后缀表达式
步骤一的分解实现是整个项目的基础,其难点在于如何将操作数准确分解?例如表达式 4 - -0.35 中-0.35的分离,关键在于如何区分运算符'+''-'与数值符号'+''-',从而实现数值符号'+''-'与数值结合为一个整体
步骤二中的中缀表达式是人们常见的形式,便于理解,但它并不是计算机所擅长处理的任务。由于中缀表达式涉及了诸如运算符的优先级计算以及括号的优先计算等问题,不利于计算机处理。而后缀表达式的优势在于消除了括号的同时又保留了中缀表达式的计算优先级,便于计算机处理。因此,转换之
步骤三的处理相较于步骤一二要简单些,只需要将后缀表达式中的数值字符串转化为浮点数,同时确定待运算的两个操作数以及对应的操作符即可
以上步骤一二三均涉及了队列的运用,而步骤二三同时涉及到了栈的运用,其实现思路很是巧妙,值得思考学习。
下面分别介绍三个步骤的具体实现过程:
1. 将原始字符串按照不同类型分解为独立的字符串单元
所需要处理的独立字符串单元类型包括:操作数、运算符以及括号。为了使字符串分离操作易于实现,可将问题进一步拆分。针对三种不同字符串单元的分离首先应设计对应的检测模块,如下:
bool isDotOrNum(const string & str); //是否为数值
bool isSymbol(const string & str); //是否为非数值符号
bool isOperator(const string & str); //是否为运算符
bool isSign(const string & str); //是否为数值符号
bool isBracketL(const string & str); //是否为左括号
bool isBracketR(const string & str); //是否为有括号
检测模块详细代码如下:
/*
* NOTE: 不要试图改变"+" == str -> '+' == str[0],会出现各种问题,这种设计是基于全局考虑的
* 修改后不受用影像的函数:isDotOrNum,isBracketL,isBracketR,isSymble
* So,为了风格统一,使用字符串比较的方式
*/
bool isDotOrNum(const string &str)
{
return '.' == str[0] || (str[0] >= '0' && str[0] <= '9');
}
bool isSign(const string & str)
{
/*必须用字符串整体比较,否则可能造成符号与数字的分离(如:+5)*/
return "+" == str || "-" == str;
}
bool isOperator(const string & str)
{
return "+" == str || "-" == str || "*" == str || "/" == str;
}
bool isBracketL(const string & str)
{
return "(" == str;
}
bool isBracketR(const string & str)
{
return ")" == str;
}
bool isSymble(const string & str)
{
return "(" == str || ")" == str || isOperator(str);
}
分离程序代码:
bool splitExpStr(const string &src, queue &dstExp)
{
string prev = "";
string toStore = "";
string strTmp = "";
for (const auto c : src)
{
strTmp = c;
if (isDotOrNum(strTmp))
{
toStore += strTmp; //若为数字则合并为一个整体
}
else if (isSymble(strTmp))
{
if (!toStore.empty())
{
dstExp.push(toStore);
toStore.clear(); //每次存储后清零toStore
}
toStore = strTmp;
/*
*以下两种情况下,将符号单独存储;否则,加至数字前作为一个整体
*/
if (!isSign(strTmp) || /*1. 非'+'or'-'符号*/
(isSign(strTmp) && (isBracketR(prev) || isDotOrNum(prev)))) /*2. 该符号前一字符是')'或数字*/
{
dstExp.push(toStore);
toStore.clear();
}
}
else
return false; //非法字符
prev = strTmp;
}
if (!toStore.empty())
dstExp.push(toStore);
return true;
}
2. 将分解所得的中缀字符串序列转换为后缀字符串序列
转换之前,需要对中缀表达式的合法性做检测,代码如下:
/*
* 若出现如下情况之一,则表达式不合法:
* 1. 运算符连续出现两次
* 2. 数字 + (
* 3. ) + 数字
* 4. 左右括号步匹配
*/
bool isMach(queue src)
{
bool ret = true;
stack stk;
string curr = "";
string prev = "";
while (!src.empty())
{
curr = src.front();
/*
* 1. 运算符是否连续出现两次?
* 2. 数字 + ( ?
* 3. ) + 数字 ?
* isDotOrNum(curr) || curr.size() > 1成立则curr为数字(curr.size() > 1是针对像-6这种有符号数情况考虑的)
* NOTE: 针对1,若修改isOperator程序的判断条件:
* "+" == str -> '+' == str[0],则会导致bug(5 + +5,该表达式判断为不匹配)产生
*/
if ( (isOperator(curr) && isOperator(prev))
|| (isBracketL(curr) && (isDotOrNum(prev) || prev.size() > 1))
|| ((isDotOrNum(curr) || curr.size() > 1) && isBracketR(prev)))
{
ret = false;
cout << "Exp not match: 1 or 2 or 3!" << endl;
break;
}
//4. 左右括号是否匹配?
if (isBracketL(curr))
stk.push(curr);
else if (isBracketR(curr))
{
/*first to check is stk empty?*/
if (!stk.empty() && isBracketL(stk.top()))
stk.pop();
else
{
ret = false;
cout << "Exp not match: Bracket not match!" << endl;
break;
}
}
prev = curr;
src.pop();
}
if (!stk.empty())
ret = false;
return ret;
}
中缀表达式序列转换为后缀表达式序列,可有效消除括号同时保留原表达式的计算优先级。因此,需要根据实际情况设置操作符以及'('的优先级:
/*设置操作符以及'('优先级*/
int getPriority(const string &str)
{
int ret = 0; //'('优先级设为0
if ("+" == str || "-" == str)
ret = 1; //'+'、'-'优先级设为1
else if("*" == str || "/" == str)
ret = 2; //'*'、'/'优先级设为2
return ret;
}
中缀表达式序列转后缀表达式序列的过程,可以这样描述:
(1)将输入队列中的数值直接载入输出队列;
(2)将操作符按优先级从低到高依次压入栈中;
(3)若遇到不低于栈顶符号优先级的操作符,则依次将栈中的操作符载入输出队列;
(4)若遇到'(',则直接将其压入栈中,并重复1,2,3直至遇到')'。再将栈中的操作符依次载入输出队列,直至遇到‘(’,并将其删除
(5)输入队列元素全部出队列后,将栈中可能存在的元素依次载入输出队列
该实现过程主要是巧借栈的结构特点,完成了对操作符按优先级的准确排列,转换代码如下:
/*中缀表达式转后缀表达式*/
bool transformExp(queue &srcExp, queue &dstExp)
{
bool ret = isMach(srcExp);
if (true == ret)
{
stack stk;
while (!srcExp.empty())
{
string curr = srcExp.front();
if (isOperator(curr))
{
while (!stk.empty() && getPriority(curr) <= getPriority(stk.top()))
{
dstExp.push(stk.top());
stk.pop();
}
stk.push(curr);
}
else if (isBracketR(curr))
{
//运算符的优先级均高于'(',保证了仅遇到')'时,方可弹出'('
//stk.empty非空前面isMach已经保证,此处亦可去除
while (!stk.empty() && !isBracketL(stk.top()))
{
dstExp.push(stk.top());
stk.pop();
}
stk.pop(); //弹出与当前')'匹配的'('
}
else if (isBracketL(curr)) //左括号直接入栈
{
stk.push(curr);
}
else
dstExp.push(curr); //操作数直接输出
srcExp.pop();
}
while (!stk.empty()) //输出stk中剩余的运算符
{
dstExp.push(stk.top());
stk.pop();
}
}
return ret;
}
3. 计算转换后的后缀表达式
该实现过程同样是借助于栈实现了对表达式的有序计算过程,不同的是,这次栈的存储对象为操作数。对后缀表达式计算过程的代码如下:
double calculate(double opdL, string opr, double opdR)
{
double ret = 0.0;
if ("+" == opr)
ret = opdL + opdR;
else if ("-" == opr)
ret = opdL - opdR;
else if ("*" == opr)
ret = opdL * opdR;
else
ret = opdL / opdR;
return ret;
}
double calculate(queue &srcExp)
{
double ret = 0.0;
double opdL = 0.0;
double opdR = 0.0;
stack stk;
while (!srcExp.empty())
{
string curr = srcExp.front();
if (isOperator(curr))
{
opdR = stk.top();
stk.pop();
opdL = stk.top();
stk.pop();
ret = calculate(opdL, curr, opdR);
stk.push(ret);
}
else
{
stk.push( stod(curr) );
}
srcExp.pop();
}
ret = stk.top();
return ret;
}
以上,通过完成三个部分的处理,便实现了所述简单计算器的功能。测试主程序如下:
int main()
{
string srcStr;
queue expTmp1;
queue expTmp2;
while (cout << "Expression: ", cin >> srcStr)
{
//分离字符串表达式
bool ret = splitExpStr(srcStr, expTmp1);
if (true == ret)
{
//中缀表达式转后缀表达式
ret = transformExp(expTmp1, expTmp2);
//输出计算结果
if (true == ret)
cout << "result: " << calculate(expTmp2) << endl;
}
}
return 0;
}