[数据结构]表达式求值
一、问题描述
表达式求值是数学中的一个基本问题,也是程序设计中的一个简单问题。我们所涉及的表达式中包含数字和符号,本实验中处理的符号包括‘+’、‘-’、‘*’、‘/’、‘(’和‘)’,要求按照我们所习惯的计算顺序,正确计算出表达式的值,并输出至屏幕上。
本实验采用的数据结构是栈,表达式求值的问题用栈来实现是十分合适的。本实验用两个栈分别保存表达式中的数字和符号,以确定每对符号相遇时的优先级来决定当前应该进行什么操作。符号栈的操作分为三种:一是直接入栈;一是直接出栈;一是将当前栈顶符号出栈并计算,然后根据新的栈顶符号与当前符号的优先级关系重复操作类型的判断。
根据栈的特点以及实验操作的要求,本实验采用的是顺序栈实现。相比链栈来说,顺序栈的访问更方便,但其本质是没有差别的。
二、数据结构——栈
栈的本质是一个仅在队尾进行操作的线性表。相比于一般的线性表来说,栈的操作是受限制的。一般的栈仅有四个基本操作:初始化(InitStack())、取栈顶元素(GetTop())、入栈(Push())和出栈(Pop()),本实验多定义了一个操作函数ChangeTop(),用于更改栈顶元素的值,当然这个操作也等价于先出栈后入栈一个新的值。
栈的顺序表示类似于线性表的顺序表示,用一段地址连续的区域存储一个顺序栈,用物理存储位置的连表示栈的逻辑关系。与顺序表示的线性表类似,这也要求元素之间维持严格的物理位置关系,但正如上文所说,栈的操作都是限制在栈顶元素处,所以修改操作的有限突出了访问的便捷,因此栈的实现通常使用顺序表示而不是链式表示。
具体实现时,先申请一段连续的空间,当空间不够时,需要新申请一段更大的空间,把之前的所有数据移到新的位置。这样的操作确实十分耗时且无法避免,所以需要在实现之前更好地估计程序所需要的空间大小,减少此类操作的次数,同时最大化地利用空间。
三、算法的设计和实现
1、建立两个栈:一个是数字栈,一个是算符栈,首先向算符栈内添加一个符号‘#’。对于具体的符号栈的实现,可以通过编号的形式使用数字编号,也可以直接保存char类型的字符。
2、读取一个字符,自动滤掉空格、换行符等无关字符,当出现非法字符时结束程序并输出“表达式包含未被识别的符号。”
3、如果当前字符c是数字字符,即c满足条件“c>=’0’&&c<=’9’”,则继续读取,并由一个新的变量Num保存这个数字的真实值,具体实现是Num的初值为0,然后每次执行语句Num=Num*10+c-’0’,直到读取到非数字字符为止;如果当前字符c不是数字字符,调用函数OptrType(char c)得到该符号的编号。
4、当读到一个算符,考虑其入栈时,有三种情况:
(1)算符栈的栈顶元素优先级低于当前算符时,当前算符入栈;
(2)当两者优先级相同时,当前算符不入栈,算符栈的栈顶元素弹出;
(3)算符栈的栈顶元素优先级高于当前算符时,算符栈的栈顶元素弹出,数字栈弹出两个元素,按照顺序进行弹出的符号所对应的运算执行运算操作,将得到的结果压入数字栈。再将待入栈元素继续进行步骤4的判断。
5、当‘#’都弹出之后,计算结束。如果算式没有错误,则数字栈只剩一个元素,该元素就是算式的计算结果,输出即可。
6、清栈。
注:步骤4中提到的优先级是相对的,本实验中的实现方式是编号之后使用一个二维数组保存一个序对的关系的。比如Cmp[‘+’][‘-’]=1,而同样的有Cmp[‘-’][‘+’]=1。这样的规定方式是为了让四则运算中同级的运算能够先出现的先计算,从而避免了错误。具体关系见下表,其中值为-2的位置表示表达式有错误。
+ | - | * | / | ( | ) | # | |
+ | 1 | 1 | -1 | -1 | -1 | 1 | 1 |
- | 1 | 1 | -1 | -1 | -1 | 1 | 1 |
* | 1 | 1 | 1 | 1 | -1 | 1 | 1 |
/ | 1 | 1 | 1 | 1 | -1 | 1 | 1 |
( | -1 | -1 | -1 | -1 | -1 | 0 | -2 |
) | 1 | 1 | 1 | 1 | -2 | 1 | 1 |
# | -1 | -1 | -1 | -1 | -1 | -2 | 0 |
四、预期结果和实验中的问题
1、预期结果是程序可以正确计算出一个表达式的值,并判断出表达式的错误,包含“表达式中包含无法识别的字符”和“表达式输入错误”两种错误。比如输入的表达式为“2*(3+5)”,则能够得到的输出为“原式=16”;如果输入为“2x+3”,则输出为“表达式包含未被识别的符号”;如果输入为“1 1+1”,则输出为“表达式输出有误”。
2、实验中的问题及解决方案:
(1)map或者set解决算符映射问题
对于所出现的算符,我都编了一个数字号码以便访问。这样有一个弊端在于,当符号变多的时候,这样的没有特殊意义的数字编码映射显得十分累赘且不方便,即使每次访问一个专门编码、解码的函数,也显得很笨拙。
所以我想到的解决方法是,利用c++的STL标准库中的map或者set进行映射,这样在编码的时候可以直接以char类型的数据作为下标进行访问,十分方便。map和set这两个容器的原理都是基于红黑树,红黑树简单地说是一种二叉平衡查找树,具体维护操作这里不再赘述。
(2)template解决通用程序设计问题
本实验中涉及到的两个栈,一个是数字栈,一个是算符栈。正如上文所说,我将算符编号了,相当于说算符栈是一个维护int型元素的栈,而且其中的元素需要用作下标访问Cmp[][]数组以得到两个算符的优先级;而由于计算的必要,数字栈需要是一个维护double型元素的栈。一种暴力有效的方式是,建立两个几乎一模一样的栈,一个维护int型的元素,另一个维护double型的元素。当然我不愿意这样暴力,所以我使用了c++中的模板的程序设计,也就是template,简单地说这是c++程序设计语言中采用类型作为参数的程序设计,支持通用程序设计。于是这个问题就完美解决了。
(3)关于链栈
关于链栈的实现,我认为实现很容易。正如上文所说,栈可以看作一个有特殊操作限制的线性表,所以实现几乎与链栈无异。个人觉得栈这种数据结构没有必要使用链式表示,频繁的申请空间的时间且多出的链域空间都是不必要的浪费。
附:c++源代码:
1 /* 2 项目:顺序栈实现表达式求值 3 作者:张译尹 4 */ 5 #include <iostream> 6 #include <cstdio> 7 8 using namespace std; 9 10 #define STACK_INIT_SIZE 100 //栈存储空间的初始分配量 11 #define STACKINCREMENT 10 //栈存储空间的分配增量 12 13 #define MaxLen 120 //表达式长度 14 15 template <class T> class MyStack 16 { 17 private: 18 T *base; //栈底指针,构造之前和销毁之后为NULL 19 T *top; //栈顶指针 20 int StackSize; //当前栈分配的空间大小 21 22 public: 23 void InitStack() //构造一个空的栈 24 { 25 base = new T(STACK_INIT_SIZE); 26 top = base; 27 StackSize = STACK_INIT_SIZE; 28 } 29 void DestroyStack() //销毁栈 30 { 31 delete base; 32 StackSize = 0; 33 } 34 void ClearStack() //清空栈 35 { 36 top = base; 37 } 38 bool StackEmpty() //判断栈空 39 { 40 return (base == top); 41 } 42 int StackLength() //返回当前栈内元素个数 43 { 44 return (top - base); 45 } 46 bool GetTop(T &Elem) //取栈顶元素,用Elem返回,失败返回true 47 { 48 if(StackEmpty()) 49 return true; 50 Elem = *(top - 1); 51 return false; 52 } 53 void Push(T Elem) //将Elem加入栈顶 54 { 55 int Len = StackLength(); 56 if(Len + 1 > StackSize) //当前栈的空间不够 57 { 58 T *NewBase = new T(StackSize + STACKINCREMENT); 59 int i; 60 for(i = 0; i < Len; i++) 61 NewBase[i] = *(base + i); 62 delete base; 63 base = NewBase; 64 StackSize += STACKINCREMENT; 65 } 66 *top = Elem; 67 top++; 68 } 69 bool Pop(T &Elem) //出栈栈顶元素,用Elem返回,失败时返回true 70 { 71 if(StackEmpty()) 72 return true; 73 top--; 74 Elem = *top; 75 return false; 76 } 77 bool ChangeTop(T Elem) //将栈顶元素的值改为Elem,失败时返回true 78 { 79 if(StackEmpty()) 80 return true; 81 *(top - 1) = Elem; 82 return false; 83 } 84 }; 85 86 int Cmp[10][10] = {{}, 87 {0, 1, 1, -1, -1, -1, 1, 1}, 88 {0, 1, 1, -1, -1, -1, 1, 1}, 89 {0, 1, 1, 1, 1, -1, 1, 1}, 90 {0, 1, 1, 1, 1, -1, 1, 1}, 91 {0,-1, -1, -1, -1, -1, 0, -2}, 92 {0, 1, 1, 1, 1, -2, 1, 1}, 93 {0,-1, -1, -1, -1, -1, -2, 0}}; 94 //保存运算符之间的优先关系 95 96 void Check() 97 { 98 MyStack<int> s; 99 s.InitStack(); 100 int n; 101 int tmp; 102 scanf("%d", &n); 103 for(int i = 1; i <= n; i++) 104 { 105 scanf("%d", &tmp); 106 s.Push(tmp); 107 } 108 printf("Length = %d\n", s.StackLength()); 109 printf("====================================\n"); 110 while(!s.StackEmpty()) 111 { 112 s.GetTop(tmp); 113 printf("%d\t", tmp); 114 s.Pop(tmp); 115 printf("%d\n", tmp); 116 } 117 printf("====================================\n"); 118 } 119 120 inline int OptrType(char ch) //返回运算符编号 121 { 122 switch(ch) 123 { 124 case ' ': case '\n': return 0; 125 case '+': return 1; 126 case '-': return 2; 127 case '*': return 3; 128 case '/': return 4; 129 case '(': return 5; 130 case ')': return 6; 131 case '#': return 7; 132 case '.': return 8; 133 } 134 return -1; 135 } 136 137 double Cal(double x, double y, int Op) //x Op y 138 { 139 switch(Op) 140 { 141 case 1: return (x + y); 142 case 2: return (x - y); 143 case 3: return (x * y); 144 case 4: return (x / y); 145 } 146 return 0x3f; 147 } 148 149 void EvaluateExpression() 150 { 151 char c; 152 int cnt, t, tmp, pElem; 153 double x, y; //用于计算的数,x符号y 154 double Num; 155 bool OK = false; //表示算式是否完成计算,完成为true 156 157 //+:1 -:2 *:3 /:4 (:5 ):6 #:7 158 MyStack<int> Optr; //运算符栈 159 MyStack<double> Opnd; //运算数栈 160 Optr.InitStack(); 161 Opnd.InitStack(); 162 163 Optr.Push(7); //在起初压入'#'以便最后的清栈 164 165 printf("请输入一个算式,以\'#\'结束。\n"); 166 167 while(c = getchar()) 168 { 169 if((c >= '0' && c <= '9') || c == '.') 170 { 171 Num = 0.0; 172 while(c >= '0' && c <= '9') 173 { 174 Num = Num * 10 + c - '0'; 175 c = getchar(); 176 } 177 if(c == '.') 178 { 179 cnt = 0; 180 c = getchar(); 181 while(c >= '0' && c <= '9') 182 { 183 Num = Num * 10 + c - '0'; 184 c = getchar(); 185 cnt++; 186 } 187 while(cnt--) 188 Num /= 10.0; 189 } 190 Opnd.Push(Num); 191 } 192 t = OptrType(c); 193 if(!t) //空格符、换行符 194 continue; 195 if(t == -1) //其他符号 196 { 197 printf("表达式包含未被识别的符号。\n"); 198 return ; 199 } 200 for(;;) 201 { 202 Optr.GetTop(tmp); 203 if(Cmp[tmp][t] == -1) 204 { 205 Optr.Push(t); 206 break; 207 } 208 if(Cmp[tmp][t] == 0) 209 { 210 if(t == 7) //'#'表示结束 211 { 212 Optr.Pop(pElem); 213 OK = true; 214 } 215 else //遇到左右括号配对了 216 { 217 Optr.Pop(pElem); 218 } 219 break; 220 } 221 //Cmp[tmp][t] == 1 222 Optr.Pop(pElem); 223 Opnd.Pop(y); 224 Opnd.GetTop(x); 225 x = Cal(x, y, pElem); 226 Opnd.ChangeTop(x); 227 Optr.GetTop(tmp); 228 } 229 if(OK) 230 break; 231 } 232 tmp = Opnd.StackLength(); 233 if(tmp != 1) 234 { 235 printf("表达式输入有误。\n"); 236 return ; 237 } 238 Opnd.GetTop(x); 239 printf("原式 = %lf\n", x); 240 } 241 242 int main() 243 { 244 //Check(); 245 EvaluateExpression(); 246 return 0; 247 }