前面一节我们深入学习了stack适配器的基本用法,本节我们将充分利用stack适配器先入后出这种”对称性“,自己可以做一个计算器(支持'+'、'-'、'*'、'/'、'^'、'()')。主要思路是通过satck适配器维护运算栈的单调递增,运算栈的单调递增指的是完全保持从左到右由低到高的运算顺序,维护同级运算符,避免“头重脚轻”,运算错误。
比如我们要计算“1+2-3/4^5-6",从人的视角,我们先给4取指数为“1+2-3/1024-6”,然后由于'/'的优先级比'+'、'-'高,我们后进行除法操作为”1+2-0.0029296875-6“,发现运算栈操作符都是同级,最后计算得到结果”-3.9970703125"。但是从栈的视角来看:我们需要先维护两个栈,一个是数字栈,存数字;一个是运算栈,存操作符。数字栈存入1、2、3后,运算栈先后存入'+'、'-',从第二个运算符'-'开始判断左右两边运算符等级,发现高级'/',于是继续压栈,只要高级,那就压栈,直到发现第二个'-'后,为了维护运算栈的单调递增性,不能压栈了只能弹栈,依此进行合并直到左边运算符等级<=右边运算符等级,周而复始,遍历整个输入表达式。
了解了运算栈原理,我们可以创建一个Calc计算器类,来实现生活中的计算器。思路就是输入一个表达式input,判断表达式有没有非法字符,非法的话就错误输入,然后就是截取数字压进数字栈,配合运算栈遍历表达式,获取结果最后刷新界面。考虑到数字类型的多样性,决定使用double来统一数字,初始化显示输入和显示输出为0,里面有很多辅助函数,需读者自行理解。
#include<iostream>
#include<string>
#include<stack> //双栈维护计算器
#include<map> //运算符等级
#include<cctype> //判断是否为字符
#include<cmath> // pow
using namespace std;
/*设计一个计算机类*/
class Calc
{
public:
/*简单的显示界面*/
void UI()
{
cout << '\n';
cout << "=========================================" << endl;
cout << "| Dotcpp科学计算器 |" << endl;
cout << "=========================================" << endl;
cout << "| |" << endl;
cout << "|输入为:【"<< input << "】";
for(int i = 0; i < 27 - input.size(); ++i) cout << " ";
cout << "|\n";
cout << "|结果为:【"<< output << "】";
for(int i = 0; i < 27 - output.size(); ++i) cout << " ";
cout << "|\n";
cout << "| |" << endl;
cout << "| 0:EXIT|" << endl;
cout << "=========================================" << endl;
}
/*-------------辅助函数-------------begin*/
/*是不是运算符*/
bool is_op(char c)
{
/*string::npos是string的无效位置*/
return oper.find(c) != string::npos;
}
/*获取输入*/
void get_input(const string& s)
{
input = s;
f(); // 获取输入后自动计算
}
/*判断是否应该计算*/
bool is_OK(char op, char stack_op) {
if(stack_op == '(') return false; // 左括号不计算
return mp[op] <= mp[stack_op]; // 当前运算符优先级小于等于栈顶运算符时计算
}
/*执行计算*/
/*保证数字栈至少有两个数&&至少有一个操作符*/
bool compute(stack<double>& s1, stack<char>& s2) {
if(s1.size() < 2 || s2.empty()) {
output = "ERROR: 错误输入";
return false;
}
char op = s2.top();
s2.pop();
double b = s1.top();
s1.pop();
double a = s1.top();
s1.pop();
double result = 0;
switch(op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if(b == 0) {
output = "ERROR: 被除数不能为0";
return false;
}
result = a / b;
break;
case '^': result = pow(a, b); break;
default:
output = "ERROR: 错误输入";
return false;
}
s1.push(result);
return true;
}
/*-------------辅助函数-------------end*/
/*双栈计算主函数*/
void f()
{
stack<double> s1; // 数字栈
stack<char> s2; // 运算符栈
// 验证输入字符是否合法
for(char c : input) {
if(c == ' ') continue;
if(!isdigit(c) && !is_op(c) && c != '.') {
output = "ERROR: 错误输入";
return;
}
}
/*开始遍历输入*/
for(int i = 0; i < input.size(); ++i)
{
char c = input[i];
// 跳过空格
if(c == ' ') continue;
// 如果是数字,读取完整数字
if(isdigit(c) || c == '.')
{
string num_str = "";
while(i < input.size() && (isdigit(input[i]) || input[i] == '.')) {
num_str += input[i];
i++;
}
i--; // 回退一个字符
/*转数字并压栈*/
double num = stod(num_str);
s1.push(num);
}
// 如果是左括号
else if(c == '(')
{
s2.push(c);
}
// 如果是右括号
else if(c == ')')
{
while(!s2.empty() && s2.top() != '(') {
if(!compute(s1, s2)) return; // 计算失败直接返回
}
if(!s2.empty()) s2.pop(); // 弹出左括号
else {
output = "ERROR: 括号不对称";
return;
}
}
// 如果是运算符
else if(is_op(c))
{
while(!s2.empty() && is_OK(c, s2.top())) {
if(!compute(s1, s2)) return; // 计算失败直接返回
}
s2.push(c);
}
}
// 处理剩余的运算符
while(!s2.empty()) {
if(s2.top() == '(') {
output = "ERROR:输入错误";
return;
}
if(!compute(s1, s2)) return; // 计算失败直接返回
}
// 获取结果
if(!s1.empty() && s1.size() == 1) {
ans = s1.top();
output = to_string(ans);
// 移除末尾多余的0
while(output.back() == '0') output.pop_back();
if(output.back() == '.') output.pop_back();
} else {
output = "ERROR:输入错误";
}
}
private:
string input = "0";
double ans = 0;
string output = "0";
string oper = "+-*/^()";
/*对运算符进行分级*/
map<char, int> mp{
{'+', 1}, {'-', 1},
{'*', 2}, {'/', 2},
{'^', 3},
{'(', 0}, {')', 0}//特殊字符
};
};
void test()
{
Calc calc;
string s;//输入
bool key = true;//控制循环
while(key)
{
calc.UI();
getline(cin, s);
if(s == "0") {
key = false;
cout << "欢迎下次使用";
} else {
system("cls");
calc.get_input(s);
}
}
}
int main() {
test();
return 0;
}简单输入几个表达式试试结果:
输入“1+2-3/4^5-6”后:

让我们来检测一下报错情况:
1. 违规字符

2. 不对称括号

3./0情况

4. 残缺表达式

就这样,在充分掌握stack适配器的基础下,我们充分利用栈的”对称性“,实现了一个DIY计算器。读者是否已经按捺不住心中那写码的激情,想要赶快去做一个计算器呢,那快去试试吧!
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程