首先,我想到写这个算法的时候,是因为我在想数据结构中二叉树的时候想到的。
没一本数据结构都应该有一个很经典的二叉树应用举例,就是算式的拆分。
这样经过二叉树拆分的式子在感觉上很直观,而且一旦这个树能构造出来,那么计算起来用函数递归的方式很快就能算出来了。
不过,我用的并不是这种方法。
因为在对于将一个包含括号的四则运算字符型表达式差分到一个二叉树中,我当时没想到甚么好思路。
所以,很干脆的,我采用了操作符优先级的判定来实现四则运算的方法。
这个是我代码中主要的三个buffer和全部的成员函数:
1 private:
2 vector<float> fArray;
3 vector<char> cOprt;
4 vector<int> iPriority;
5
6 bool IsBracketLegal(char * Buf);
7 void PrioritySet(char * Buf);
8 int GetMaxPriority();
9 bool HaveHOprt(int MaxPri);
10 bool Display();
11 int GetTen(int n);
12 bool Analysis(char * Buf);
13 float Calculate();
14 public:
15 CCalculate(char * Buf = NULL);
16 float DoIt(char * BUf);
在接下去说之前,我要感谢STL的发明者。让我在写代码的时候能够很方便的用到如此稳定可靠地容器模板。
不然我还要自己去设计两个容器模板,虽然自己有写过的,但是毫无疑问,其中会有不少的bug。
言归正传。
这三个容器的用途分别是:
fArray存储用于计算的数据。
cOprt存储字符型操作符
iPriority用来存储操作符的计算优先级。
在这里你能看出来我的思路吗?
思路是这样的:我们在算数的时候,总是先算括号最里面的算式,然后再算括号外面的。同时需要注意的就是同优先级下,要先乘除,后加减。
所以我做了三个容器,分别根据优先级和操作符来计算fArray中的数据。
接下来思路就比较简单了。但是在程序设计中,还有一些需要处理的细节问题:
1 从控制台输入的是一个字符串,我需要将字符串拆解分析,根据字符串的信息来填充这三个容器。
2 括号不匹配的检测
3 计算过程中,对容器的更新。
4 如果这个输入算是是非法的,如何判定。
这里我处理4没做之外,上面的三个全都做了。
不过这个以后是必须要做的,一个合格的算式处理程序怎么能不检查算式的非法性……
但是因为我当时比较侧重的是算法的代码实现,这一块,就是顺手把括号的合法性做了,因为括号检测很简单~
首先说一下算式的拆分方法:
1 bool CCalculate::Analysis(char * Buf)
2 {
3 if(!IsBracketLegal(Buf))
4 return false;
5
6 stack<int> iTemp;
7 int valtemp = 0;
8
9 for(int i = 0;Buf[i] != NULL;i ++)
10 {
11 if(Buf[i] >= '0' && Buf[i] <= '9')
12 {
13 iTemp.push(int(Buf[i] - '0'));
14 continue;
15 }
16 switch(Buf[i])
17 {
18 case '+':
19 case '-':
20 case '*':
21 case '/':
22 for(int k = 0; !iTemp.empty(); k ++)
23 {
24 valtemp += iTemp.top() * GetTen(k);
25 iTemp.pop();
26 }
27 fArray.push_back(float(valtemp));
28 valtemp = 0;
29 cOprt.push_back(Buf[i]);
30 break;
31 }
32 }
33 for(int k = 0; !iTemp.empty(); k ++)
34 {
35 valtemp += iTemp.top() * GetTen(k);
36 iTemp.pop();
37 }
38 fArray.push_back(float(valtemp));
39 return true;
40 }
先不用看那个括号检测,直接跳来。
我并没有在检测完括号后删掉括号,因为一个是括号对于数字和操作符的分析不影响,而且操作符的优先级设定还依赖于括号。
首先是检测数字。一个多位数如何从字符的形式向整数转变?其实控制台是有这个能力的。如果我投机取巧,未尝不可。
但是,这个活不费劲,就自己直接来了。
我对于检测到的满足'0'-'9'的字符全部入栈,然后continue,进入下一个数字的检测。此时只要是数字,就会入栈。
如果遇到了操作符,就是+ - * /。先把这个操作符添加到cOprt容器中,然后再对栈中的数据进行处理。
我在遇到数字的时候,把最高位入到栈底,最低位在栈顶。
这样只要一个for循环,不断的出栈,每个出栈的权值就是10^i,i为循环的次数。将其相加,当栈为空的时候,这个数据就出来了。
数据出来之后,直接加到fArray容器中。
就是这段代码:
1 for(int k = 0; !iTemp.empty(); k ++)
2 {
3 valtemp += iTemp.top() * GetTen(k);
4 iTemp.pop();
5 }
6 fArray.push_back(float(valtemp));
7 valtemp = 0;
记得最后要把valtemp初始化为零。而栈就不用了,因为在循环完成后,这个栈已经是空的了。
你应该能注意到,我并没有对小数点做任何处理,也就是这段代码还不支持小数。
这里就是我有点懒啦……以后应该会考虑做的。
同时需要注意的是,一个正常的算是后面的应该是数字而不是操作符。
所以在函数最后,还要将最后的一个数字提取出来。
这个就是在函数最后面又加了一段循环的原因了。
刚才去休息一会……回来接着写
现在Analysis函数执行后,就完成了数据的分拣。
上面这些说起来挺容易的,不过实现的时候也确实不费劲,因为这个算法在上课的时候设计的~~~
到此第一个问题已经解决。
另外,在设计有点层次的算法时,确实很有必要一层一层的做。不然写了一大串函数,拼装后问题一大堆,是哪里出bug了都不知道。
接下来是括号检测问题。
括号检测,我觉得只有一点很需要注意。就是在栈的top函数返回值上。
如果栈是空的,那么top函数全完不能拿来比较。
比较了,就是进程越界内存访问,程序直接abort掉。
我没想清楚为什么STL的设计者在这没适当的优化下,我貌似是第二次吃亏了。
所以我的比较代码是这样的:
1 bool CCalculate::IsBracketLegal(char * Buf)
2 {
3 stack<char> Temp;
4 for(int i = 0;Buf[i] != NULL; i++)
5 {
6 if(Buf[i] == '(')
7 {
8 Temp.push(Buf[i]);
9 continue;
10 }
11 if(Buf[i] == ')')
12 {
13 if(Temp.empty())
14 return false;
15 else
16 {
17 Temp.pop();
18 continue;
19 }
20 }
21 }
22 if(Temp.empty())
23 return true;
24 return false;
25 }
当遇到')'的时候,先看栈是不是空的,空的直接返回false,不是空的就直接出栈。
因为我在设计的时候对于括号只支持(),没做别的类型括号。像这种东西,除非是做产品,否则真的没必要在这浪费太多精力。
毕竟这个不是技术活。
而且这段代码看起来挺长,但是,每次的执行效率我觉得还是凑合的。因为条件判断跳转不少的指令。
然后接下来,就是计算工作了。
先举个例子:
1+2*3
数据是1 2 3
操作符是+ *
优先级暂且不说。这样第一遍计算的结果就是:
数据 1 6
操作符 +
然后在下次的计算中就会很容易的得到结果7。
从上面的简单的演示能够知道,先算乘除,再算加减。并且在算完后,删除所有参与了运算的数据和操作符。
并且在数据的位置上插入新的计算结果。
如果包含了括号,比如这样:
(1+2)*3
数据和操作符和之前一样,但是在优先级上就应该出现变化。
操作符+ *
优先级1 0
也就是优先级高的要先进行运算,优先级低的进行运算。
我在这里对优先级进行处理是单独使用了一个函数的:
1 void CCalculate::PrioritySet(char * Buf)
2 {
3 int Priority = 0;
4 for(int i = 0;Buf[i] != NULL; i++)
5 {
6 if(Buf[i] == '(')
7 {
8 Priority++;
9 continue;
10 }
11 else if(Buf[i] == ')')
12 {
13 Priority--;
14 continue;
15 }
16 switch(Buf[i])
17 {
18 case '+':
19 case '-':
20 case '*':
21 case '/':
22 iPriority.push_back(Priority);
23 break;
24 }
25 }
26 }
我这么做的理由就是利用操作符的等效映射,只是我这次在检测到操作符后,直接做的就是向iPriority中添加数据。
优先级如何判定?做法很简单,就是遇到左括号加1,遇到右括号减1.
很简单有木有?
然后这样就完成了三个初始化工作。
突然发现这个函数搬到第一个数据分拣中说好了……
然后来看一下Calculate函数:
1 float CCalculate::Calculate()
2 {
3 vector<float>::iterator vfIt = fArray.begin();
4 vector<char>::iterator vcIt = cOprt.begin();
5 vector<int>::iterator viIt = iPriority.begin();
6
7 float iTempVal;
8 int iSize = cOprt.size();
9 int iMaxPri;
10 while(!cOprt.empty())
11 {
12 iMaxPri = GetMaxPriority();
13 if(HaveHOprt(iMaxPri))
14 {
15 for(int i = 0;i < iSize; i++)
16 {
17 if(cOprt[i] == '*' && iPriority[i] == iMaxPri)
18 iTempVal = fArray[i] * fArray[i + 1];
19 else if(cOprt[i] == '/' && iPriority[i] == iMaxPri)
20 iTempVal = fArray[i] / fArray[i + 1];
21 else
22 continue;
23
24 iPriority.erase(viIt + i);//删除当前对应的最高级数据
25 fArray.erase(vfIt + i);
26 fArray.erase(vfIt + i);
27 fArray.insert(vfIt + i, iTempVal);
28 //这里的erase函数只会删除当前的迭代器指向的那个点元素
29 cOprt.erase(vcIt + i);
30 i --;
31 iSize = cOprt.size();
32 Display();
33 }
34 }
35 else
36 {
37
38 for(int i = 0;i < iSize; i++)
39 {
40 if(cOprt[i] == '+' && iPriority[i] == iMaxPri)
41 iTempVal = fArray[i] + fArray[i + 1];
42 else if(cOprt[i] == '-' && iPriority[i] == iMaxPri)
43 iTempVal = fArray[i] - fArray[i + 1];
44 else
45 continue;
46
47 iPriority.erase(viIt + i);
48 fArray.erase(vfIt + i);
49 fArray.erase(vfIt + i);
50 fArray.insert(vfIt + i, iTempVal);
51 //这里的erase函数只会删除当前的迭代器指向的那个点元素
52 cOprt.erase(vcIt + i);
53 i --;
54 iSize = cOprt.size();
55 Display();
56 }
57 }
58 }
59 return iTempVal;
60 }
在这里你会发现其实我对代码的简洁性并未做什么优化,有多处的代码出现了重复。这个,暂时算作我比较懒吧~前面的几个函数也是可以合并进去的。
比如那个数字的循环检测。
在这个函数中,我上来就做了三个迭代器,因为涉及到数据的删除和插入。
在这改编一句台词:有的迭代器,编程更美好~
这个函数中我还用了两个函数,分别是:
int GetMaxPriority();
bool HaveHOprt(int MaxPri);
名字里面能把意思表示的都很清楚了。第一个,获取当前操作符的最高优先级。
第二个则是检测当前优先级下是否存在乘除运算符。
这么做,就是为了实现要先算括号里面的,再算括号外面的,并且保证同级别下,先乘除,后加减。
所以每次循环上来,先得到运算符的最大优先级。再检测是否存在程序运算。
接下来就是处理简单的两个数,一个运算符的简单问题了。
另外提示一点的就是,每次运算符使用后,其对应的优先级也需要从容器中删除掉,否则,计算会混乱。
整个运算的结束与否是根据cOprt是否为空判断的。只要操作符为空,那么数据肯定就只剩下一个了。
就是最后的运算结果。
下面来张程序实现的效果图:
效果还可以吧。这个算法我已经封装成类。将char变量类型改成WCHAR就能移植到MFC中了。
其中我应该还是遗漏了一些细节的。
不过我觉得算法设计和程序设计都是按层次来比较好。
我目前见过的层次设计有
计算机网络协议。
ZigBee协议栈
Windows的硬件抽象层。
层次设计还是很实用的,因为我们很难直接处理复杂的事情。
化繁为简,逐层叠加。
好方法。