学校数据结构课程实验之一。
用到的数据结构:栈
基本功能:
- 输入中缀的运算表达式(即运算符在操作数中间,符合日常的书写习惯),将其转为逆波兰表达式(后缀表达式,适于机器运算);
- 对输入的表达式进行括号匹配检查,若不符合规则,报错;对于符合规则的表达式,计算结果并输出;
- 过滤输入的非法字符(字母)。
概要设计:
主函数代码(main.cpp):
1 #include <iostream> 2 #include "MyStack.cpp" 3 #include "Calculator.h" 4 using namespace std; 5 int main() 6 { 7 Calculator calc1;//创建计算器对象 8 do 9 { 10 calc1.clear(); 11 cout << "Please input an expression, ended with a '#'" << endl; 12 calc1.evaluate();//计算表达式并显示结果 13 cout << endl << "Would you like to continue? [y/n]" << endl; 14 fflush(stdin); 15 } while (cin.get() == 'y'); 16 17 return 0; 18 }
计算器类的定义(Calculator.h):
1 using namespace std; 2 class Calculator 3 { 4 private://私有成员(数据成员) 5 MyStack<char> optr;//操作符栈 6 MyStack<double> opnd;//操作数栈 7 MyStack<char> bracket;//用于存放左括号的栈 8 public://公有成员(供外界调用的成员函数,即方法) 9 Calculator()//构造函数 10 { 11 optr.setnull(); 12 opnd.setnull(); 13 bracket.setnull(); 14 optr.push('#');//‘#’作为栈底哨兵 15 } 16 void evaluate();//利用栈结构将中缀表达式转为PRN来计算,并检查输入的合法性 17 void clear()//清空计算器 18 { 19 optr.setnull(); 20 optr.setnull(); 21 bracket.setnull(); 22 optr.push('#');//与构造的实现是一致的 23 } 24 25 private://私有成员(对外隐藏的成员函数,只供内部调用) 26 int lp(char op);//返回左端操作符的优先级 27 int rp(char op);//返回右端操作符的优先级 28 double operate(char theta, double a, double b);//进行后缀表达式的运算,返回运算结果 29 };
栈模板类的定义(MyStack.h)
1 const int maxstack = 10;//栈内元素个数的最大值 2 enum Error_code { success, underflow, overflow };//函数运行成功与否的内部标签 3 template <class Stack_entry>//类模板,数据类型叫Stack_entry 4 class MyStack 5 { 6 public: //共有成员(类的所有成员函数) 7 MyStack(); 8 bool empty() const; 9 Error_code push(const Stack_entry &item); 10 Error_code pop(); 11 Error_code top(Stack_entry &item) const; 12 Stack_entry top()const; //重载top函数以适应不同的需要 13 void setnull();//清空栈 14 private: //私有成员(数据成员) 15 int count; //计数器 16 Stack_entry entry[maxstack];//用数组实现存储 17 };
具体实现:
计算器类的实现:
1 #include <iostream> 2 #include "MyStack.cpp" 3 #include "Calculator.h" 4 #include <ctype.h>//调用了isdigit和isalpha函数 5 using namespace std; 6 void Calculator::evaluate()//利用栈结构将键盘输入的中缀表达式转为RPN计算,并检查输入的正确性 7 { 8 char ch, op, theta;//当前读入字符、操作符、运算符 9 double val, a, b;//数值、左操作数、右操作数 10 cin >> ch; 11 op = '#'; 12 bool is_matched = true;//标记左右括号是否匹配 13 do 14 { 15 while (isalpha(ch)) { cin >> ch; }//过滤非法字符输入(英文字母) 16 is_matched = true; 17 18 switch (ch)//左右括号匹配检查 19 { 20 case '(': bracket.push(ch); break;//左括号入栈 21 case ')': if (bracket.empty()) 22 //右括号没有已入栈的左括号来匹配时,输入非法 23 is_matched = false; 24 else 25 { 26 bracket.pop(); is_matched = true; 27 }//匹配时,弹出一个左括号 28 break; 29 default: break;//非括号的字符一律过滤掉 30 } 31 while (is_matched)//右括号匹配时,执行中缀表达式运算 32 { 33 if (isdigit(ch))//当前字符为数字时,将它放入操作数栈 34 {//isdigit遇到多位数会自动识别吗? 35 cin.putback(ch);//答:当然不能。因为isdigt的参数是char型的,它只能判断一位字符 36 cin >> val;//这也是为什么要cin.putback(ch)再cin>>out了。cin的功能很强大,自动匹配数据类型,而不用scanf的各种格式化输入符 37 opnd.push(val); 38 cin >> ch; break; 39 } 40 else if (lp(op) < rp(ch)) 41 {//当前操作符优先级高于栈顶操作符时,入栈 42 optr.push(ch); 43 op = optr.top(); 44 cin >> ch; break; 45 } 46 else if (lp(op) == rp(ch)) 47 {//当前操作符优先级等于栈顶操作符时,当前栈顶操作符出栈 48 optr.pop(); 49 op = optr.top(); 50 cin >> ch; break; 51 } 52 else if (lp(op) > rp(ch)) 53 {//当前操作符优先级低于栈顶,将栈顶取出进行运算,再将结果放回操作数栈 54 theta = op; optr.pop(); 55 b = opnd.top(); opnd.pop(); 56 a = opnd.top(); opnd.pop();//注意两个操作数的出栈顺序 57 opnd.push(operate(theta, a, b)); 58 op = optr.top(); 59 if ((ch == ')') && (op == '('))//解决左右括号匹配后的问题 60 { 61 optr.pop(); op = optr.top();//左括号出栈 62 cin >> ch;//刷新当前字符(避免对右括号二次判断) 63 if (ch == '(') is_matched = false;//右括号紧邻左括号,报错 64 } 65 break; 66 } 67 } 68 if (is_matched == false || (op == '(' && ch == '#')) 69 {//最后还剩没配上对的左括号,或者右括号没有左括号来匹配,则输入非法 70 cout << "Bad match." << endl; 71 ch = op = '#'; 72 is_matched = false; 73 } 74 } while (ch != '#' || op != '#'); 75 76 if (is_matched == true) cout << '=' << opnd.top();//循环结束后,结果在操作数栈顶 77 else { 78 cout << "sorry, cannot compute this expression." << endl; 79 } 80 } 81 int Calculator::lp(char op)//返回左端操作符的优先级 82 { 83 int prior;//优先级 84 switch (op) 85 { 86 case '+'://将优先级表的内容写入 87 case '-': prior = 3; break; 88 case '*': 89 case '/': prior = 5; break; 90 case '(': prior = 1; break; 91 case ')': prior = 6; break; 92 case '#': prior = 0; break; 93 } 94 return prior; 95 } 96 int Calculator::rp(char op)//返回右端操作符的优先级 97 { 98 int prior;//优先级 99 switch (op) 100 { 101 case '+': 102 case '-': prior = 2; break; 103 case '*': 104 case '/': prior = 4; break; 105 case '(': prior = 6; break; 106 case ')': prior = 1; break; 107 case '#': prior = 0; break; 108 } 109 return prior; 110 } 111 double Calculator::operate(char theta, double a, double b) 112 //进行后缀表达式的运算,返回运算结果 113 { 114 switch (theta) 115 { 116 case '+': return a + b; 117 case '-': return a - b; 118 case '*': return a*b; 119 case '/': return a / b; 120 default: return 0; 121 } 122 }
栈模板类的实现(同样参考了经典教材"Data Structures and Program Design in C++" Robert L. Kruse, Alexander J. Ryba 高等教育出版社-影印版):
1 #include <iostream> 2 #include "MyStack.h" 3 using namespace std; 4 template <class Stack_entry> 5 MyStack<Stack_entry>::MyStack() 6 /*Pre: None. 7 Post: The stack is initialized to be empty.*/ 8 { 9 count = 0; 10 } 11 template <class Stack_entry> 12 bool MyStack<Stack_entry>::empty() const 13 /*Pre: None. 14 Post: if the stack is empty, true is returned. Ohterwise 15 false is returned.*/ 16 { 17 bool outcome = true; 18 if (count > 0) outcome = false; 19 return outcome; 20 } 21 template <class Stack_entry> 22 Error_code MyStack<Stack_entry>::push(const Stack_entry &item) 23 /*Pre: None. 24 Post: if the stack is not full, item is added to the top 25 of the stack. Ohterwise an error code of overflow is returned 26 and the stack is left unchanged.*/ 27 { 28 Error_code outcome = success; 29 if (count >= maxstack) outcome = overflow; 30 else entry[count++] = item; 31 return outcome; 32 } 33 template <class Stack_entry> 34 Error_code MyStack<Stack_entry>::pop() 35 /*Pre: None. 36 Post: If the stack is not empty, the top of the stack is 37 removed. If the stack is empty, an Error code of underflow 38 is returned.*/ 39 { 40 Error_code outcome = success; 41 if (count == 0) outcome = underflow; 42 else --count; 43 return outcome; 44 } 45 template <class Stack_entry> 46 Error_code MyStack<Stack_entry>::top(Stack_entry &item) const 47 /*Pre: None. 48 Post: if the stack is not empty, the top of the stack 49 is returned in item. if the stack is empty, an error code 50 of underflow is returned.*/ 51 { 52 Error_code outcome = success; 53 if (count == 0) outcome = underflow; 54 else item = entry[count - 1]; 55 return outcome; 56 } 57 template <class Stack_entry> 58 Stack_entry MyStack<Stack_entry>::top()const//重载top函数以适应不同的需要 59 { 60 if (count == 0) underflow; 61 return entry[count - 1]; 62 } 63 template <class Stack_entry> 64 void MyStack<Stack_entry>::setnull() 65 { 66 count = 0;//与构造实现是一致的 67 }
调试分析:
(1)我将“括号匹配检查”和“转RPN”放在一个函数(evaluate())里了,所以编好“转RPN”后再增补“括号匹配检查”时,就觉得“牵一发而动全身”了。大概50%的调试修改都花费在这里。调试过程中遇到的问题众多,比较突出的有:
1 switch (ch)//左右括号匹配检查 2 { 3 case '(': bracket.push(ch); break;//左括号入栈 4 case ')': if (bracket.empty()) 5 //右括号没有已入栈的左括号来匹配时,输入非法 6 is_matched = false; 7 else 8 { 9 bracket.pop(); is_matched = true; 10 }//匹配时,弹出一个左括号 11 break; 12 default: break;//非括号的字符一律过滤掉 13 } 14 while (is_matched)//右括号匹配时,执行中缀表达式运算 15 {后略。。。
开始我并未定义is_matched变量,当前字符为右括号时,若查到栈内有左括号,则直接将左括号抛栈。接着,当前的右括号还要执行while(is_matched)循环内的“转RPN”操作,注意若它的优先级低,是不会从输入流读入下一个字符来刷新当前字符的,这就意味着右括号还要进行一次“括号匹配检查”,而此时本应和它匹配的左括号已在上一次检查中出栈,那么这对右括号的二次检查必然会输出“不匹配”,这与实际结果显然不符。
为了解决此问题,我增加了函数全局布尔型变量is_matched,用来标记当前括号是否真的匹配。初始化为true,当右括号初次检查就不匹配或表达式结束后栈中仍有未被匹配的左括号,将is_matched标记为false,进入报错处理部分。针对刚刚提出的bug,只需在每次有左括号被抛出时,将is_matched标记为true,然后在右括号优先级低的分支下,加入以下代码:
1 if ((ch == ')') && (op == '('))//解决左右括号匹配后的问题 2 { 3 optr.pop(); op = optr.top();//左括号出栈 4 cin >> ch;//刷新当前字符(避免对右括号二次判断) 5 if (ch == '(') is_matched = false;//右括号紧邻左括号,报错 6 } 7 break;
在确认做左右括号确实刚刚匹配过的情况下将已比较过的右括号丢弃,从输入流读入新字符,从而与其他两种分支产生同样的完成结果,给主函数提供统一的状态,问题即可解决。
(2)多种“括号不匹配”情况的归并
这段代码写在evaluate函数的do-while循环末尾:
1 if (is_matched == false || (op == '(' && ch == '#')) 2 {//最后还剩没配上对的左括号,或者右括号没有左括号来匹配,则输入非法 3 cout << "Bad match." << endl; 4 ch = op = '#'; 5 is_matched = false; 6 } 7 } while (ch != '#' || op != '#');
括号不匹配的情况的组合有多种,但总结起来无非三种:
① 右括号没有左括号来配
② 左括号没有右括号来配
③ 右括号后紧接左括号(两对括号之间缺少运算符)
对于第一种,在右括号出入时进行switch检查并标记is_matched为false即可;对于第二种,往往在表达式结束后(ch=’#’)才能看出来(栈中还剩一个’(‘);于是在循环的末尾,将这两种情况归并,统一标记is_matched为false,为后续报错提供统一接口。
而对于第三种情况,由于只在前一对完整括号结束后才发生,且右括号作为有操作符的优先级仅高于’#’,故只需在转RPN的最后一个分支中加上一个条件判断(见上一段代码)即可。
使用说明:
(1)运行程序,会看到输出的提示信息"Please input an expression, ended with a '#'",输入要运算的表达式后回车
(2)若表达式合法,则会显示运算结果;若包含字母,则会自动过滤掉后计算;(3)若括号不匹配,则会输出提示信息"Bad match.""sorry, cannot compute this expression."和"Would you like to continue? [y/n]",按y可重新输入,按其他键退出程序。
我的测试数据为:
(56-23)/8-4# 期望结果:0.125
34+p(u89-12.3)k/3# 期望结果:59.5667
89.5*749+25)# 期望结果:输入有误
(8*(7-4)# 期望结果:输入有误
65*(72+98)(70-45) # 期望结果:输入有误
6*# 期望结果:输入有误(这一条未实现)
)5+3(# 期望结果:输入有误
测试截图:
心得体会:
为了参照讲义利用cin的强大功能,我将“括号匹配检查”和“转RPN”放在一个函数(evaluate())里了,这本身通常不是个明智的选择,我也确实在后续的调试过程中体会到了它的弊端。因为这样造成了这两个任务的“高耦合、低内聚”,没有遵守模块化程序设计的原则(高内聚、低耦合)。在以后的程序设计中要注意这点,尽量把不相干的功能设计成不同的函数,这样写的程序的可读性、可维护性、可重用性都会比较高。