对于一个表达式.我们通常的讲包含两个部分,运算数(数字)和运算符
如: 3 + 5 * 7
其中运算符为 +,*
,数字为3,5,7
;
如: 0&1&(1|0), 其中运算符从左往右依次为:&&(|)
,运算数为0110
而,在求解表达式的值的过程中,算法往往采用两个栈来维护.一个用来存储运算数的num
栈,另一个用来存储运算符的op
栈.
第一步读入数据
表达式读入.通常以字符串存储.
再尔遍历字符串, 对字符串的各个字符进行操作
这里不考虑表达式的合法性.因为若是求表达式的值,考题的重点在于求值的逻辑,而不在于表达式的合法性,99%的情况下,表达式都是合理的 表达式是否合理, 主要判断是括号的匹配, 以及双目运算符的左右是否各自都是数值.
第二步得到数字和运算符
得到数字
在遍历的过程中, 如果遇到了数字,记得连续选取.这里面先介绍两个工具.
double stod(string s)
将字符串转成浮点数
C++
cout << stod("123.5") << endl;//123.5 得到浮点数123.5
cout << stod("000.02") << endl;//0.02 去掉前导0
cout << stod("0.23dd") << endl;//0.23 遇到不符合浮点数的字符自动结束
int stoi(string s)
将字符串转成整型
C++
cout << stoi("123.5") << endl;//123
cout << stoi("002") << endl;//2
cout << stoi("327FD32") << endl;//327
string to_string(int/double/long long)
将数字转成字符串
c
cout <<to_string(3.24) << endl;//3.240000 这里要注意浮点数的精度后面的0会被转成小数
cout <<to_string(1233) << endl;//1233
通过这数字转成字符串和字符串转成数字, 可以更方便的一次性提取字符串中数字的部分.
运算符
运算符通常一个字符.但一个运算符,若占用字符粗中的多个字符,也如同选择运算数一样 ,一并全部提取出来,直接放入op
栈中.另外放入栈中用一个简写的字母代替即可.因为op
栈中的每个元素都是一个字符例如, 运算符为MAX
那么栈中就用一个A
来表示该运算.
第三步遍历逻辑
-
当遇到数字的时候, 将数字如栈
num
. -
当遇到运算符的时候,要开始做运算,
-
当运算符栈中没有其他元素,即该运算栈为空, 此运算符入栈.
例如表达式为
3+4*5-7
遍历到
+
时,栈中的情况是这样的 -
那么此时+
只能入栈.形成
当遇到数字4的时候
注意遇到数字不做运算,因为你现在得到的是3+4
但4
后面如题意要去*5
所以无论何时遇到数字都不做运算, 是因为无法保证数字后面是否还有运算符,又或者后面的运算符优先级更高.
遍历到*
的时候,这时候因为*
的运算符比栈顶的元素要高,所*
入栈,因为最终是*
的两个因子,左边的是4
,右边的还没有输入,所以到这一步*
不能操作. 那这里为什么不清栈顶元素呢. 因为如果清了栈顶的元素, 那么会先运算3 + 4
, 但实际上我们知道要先算4*5
.那什么时候可以清,不急 我们继续往下看..所以这一笔也只是*
先入栈如下:
遇到数字5
继续入栈;依然不参与运算
这时候有的同学会思考, 咦不是5*4
都出现了吗.这里面我们要考虑代码逻辑的普遍性, 如果每次数字出现了都要去特判,那么逻辑会变得很复杂.
接下来遇到-
号:
这时候,核心的逻辑来了因为运算符栈里面有元素, 并且是*
,那么因为*
号运算符的优先级比-
高,而-
的出现, 代表着前面的*
的两个乘数都出现了.那么这时候,就可以处理*
了,于是乎处理如下:
注意这里第一个出栈5
是第二个乘数,第二个出栈的4
是第一个乘数 .这个顺序是不能乱的, 因为如果是÷,-
,
a-b
和b-a
可是完全不一样的结果.
这时候op
栈中的元素是+
,因为+
运算符优先级不会低于-
所以,现在的可以把op
运算符中的+
清掉,逻辑和上一次清栈中*
逻辑是一致的.
此时,运算符栈中已经没有元素了.所以-
入栈.
再由数字7
入栈, 此时,字符串遍历结束.剩余的工作便是清栈了.
再次做出总结
-
当遇到数字的时候, 将数字如栈
num
. -
当遇到运算符的时候,要开始做运算,
- 当
op
栈为空的时候或者栈顶运算符的优先级低于 遍历的运算符时,直接入栈 - 当
op
栈顶的运算符优先级高于等于遍历的运算符时,则一直清栈,直到上述情况.此时再入栈.
最后清栈,
num
栈顶元素为答案.核心代码框架
- 当
scss
string s;
cin >> s;
stack<运算数> num;
stack<char> op;
for(auto &ch : s){//遍历字符串
if(ch是个数字){
num.push(ch->转成数字);
}else if(ch 是运算符){
while(!op.empty() && ch的优先级 <= op.top()的优先级){
清栈操作();
}
op.push(ch运算符)
}
}
清栈操作();
cout<< num.top() << endl;
上面的清栈操作, 代码中只做一次运算,根据不同的题意,和不同的运算符,使用对应的逻辑. 运算符的优先级,也看题意,大多数符合正常的运算规则,但不排除题目中有一些特定的运算符或者函数(函数也当做运算符)
括号的处理
括号实际上不算是运算符.他的存在只是帮助我们更好的切割一个整体.在这里我们把左括号和右括号分开处理.
左括号
当做运算符来处理,并且他的优先级是最低的.并且入op
栈中,用来挡住清栈运算.如案例`3 * (8-4) + 2:
当遍历到-
的时候,因为(
的优先级是最低的,所以这时候-
不做任何操作直接入栈, 避免清栈去运算3*8
;
右括号
当右括号出现, 说明表达式一个整体的部分到此结束. 那么我们一直清栈,直到遇到(
为止.然后在单独的把(
出栈.
)
不用入op
栈.
完善整体的代码逻辑如下:
C++
string s;
cin >> s;
stack<运算数> num;
stack<char> op;
for(auto &ch : s){//遍历字符串
if(ch是个数字){
num.push(ch->转成数字);
}else if(ch =='('){
'('->入op栈;
}else if(ch == ')'{
while(op.empty() != '('){
清栈操作();
}
op.pop(); //把'('弹掉
}else if(ch 是运算符){
while(!op.empty() && ch的优先级 <= op.top()的优先级){
清栈操作();
}
op.push(ch运算符)
}
}
清栈操作();
cout<< num.top() << endl;
举例一
路飞想做一个计算器,但是,他要做的不仅仅是一计算一个A+B 的计算器,他想实现随便输入一个表达式都能求出它的值的计算器,现在请你帮助他来实现这个计算器吧。
比如输入:"1+2/4="
,程序就输出 1.50(结果保留两位小数)
输入格式
第一行输入一个整数 n,共有 n 组测试数据(n≤20)。
每组测试数据只有一行,是一个长度不超过 1000的字符串,表示这个运算式,每个运算式都是以 "="
结束。这个表达式里只包含 +-*/
与小括号这几种符号。其中小括号可以嵌套使用。数据保证输入的操作数中不会出现负数。
数据保证除数不会为0
输出格式
每组都输出该组运算式的运算结果,输出结果保留两位小数。
输出时每行末尾的多余空格,不影响答案正确性
样例输入 1
ini
2
1.000+2/4=
((1+2)*5+1)/4=
样例输出 1
1.50
4.00
C++
#include <iostream>
#include <stack>
#include <iomanip>
using namespace std;
stack<char>op;
stack<double> num;
double yunsuan(double x, double y, double op){//运算规则
// cout << "x = " << x << " y = " << y << endl;
if(op == '+') return x + y;
if(op == '-') return y - x;
if(op == '*') return x * y;
if(op == '/') return y / x;
}
bool isOp(char ch){
return ch == '+' || ch == '-' || ch == '*' || ch == '/';
}
int opLe(char ch){//定义运算符的优先级
if(ch == '(') return 0;
if(ch == '+' || ch == '-') return 1;
if(ch == '*' || ch == '/') return 2;
}
void opOnce(){//清栈操作 清栈一次
double d1 = num.top();
num.pop();
double d2 = num.top();
num.pop();
char oo = op.top();
op.pop();
// cout << d1 << ' ' << d2 << ' ' << oo << endl;
num.push(yunsuan(d1,d2,oo));
}
double solve(string s){
for(int i = 0; i < s.size() - 1; i ++){
if(s[i] >='0' && s[i] <= '9'){
string t = s.substr(i);
double f = stod(t);
num.push(f);
while(s[i] >='0' && s[i] <= '9' || s[i] == '.'){
i++;
}
i -= 1;
}else if(s[i] == '('){
op.push('(');
}else if(s[i] == ')'){
while(op.top() != '('){
opOnce();//做一次运算
}
op.pop();
}else{
while(!op.empty() && opLe(op.top()) >= opLe(s[i])){
opOnce();//做一次运算
}
op.push(s[i]);
}
}
while(!op.empty()){
opOnce();//做一次运算
}
return num.top();
}
int main(){
int t;
cin >> t;
cout << fixed << setprecision(2);
while(t--){
string s;
cin >> s;
while(!op.empty()) op.pop();
while(!num.empty()) num.pop();
// verify(s);
cout << solve(s) << endl;
}
return 0;
}
举例二
假设表达式定义为:
- 一个十进制的正整数 X 是一个表达式。
- 如果 X 和 Y 是 表达式,则 X+Y, X*Y 也是表达式; *优先级高于+.
- 如果 X 和 Y 是 表达式,则 函数 Smax(X,Y)也是表达式,其值为:先分别求出 X ,Y 值的各位数字之和,再从中选最大数。
- 如果 X 是 表达式,则 (X)也是表达式。
例如:
表达式 12*(2+3)+Smax(333,220+280)
的值为 69。
请你编程,对给定的表达式,输出其值。
输入格式
第一行: T 表示要计算的表达式个数 (1≤T≤10)
接下来有 T 行, 每行是一个字符串,表示待求的表达式,长度≤1000
输出格式
对于每个表达式,输出一行,表示对应表达式的值。
数据范围
1≤n≤1000
ops[i] 为 "C"
、"D"
、"+"
,或者一个表示整数的字符串。整数范围是 [−3×10^4,3×10^4]
对于 "+"
操作,题目数据保证记录此操作时前面总是存在两个有效的分数
对于 "C"
和 "D"
操作,题目数据保证记录此操作时前面总是存在一个有效的分数
输出时每行末尾的多余空格,不影响答案正确性
样例输入 1
scss
3
12+2*3
12*(2+3)
12*(2+3)+Smax(333,220+280)
样例输出 1
18
60
69
C++
#include <iostream>
#include <stack>
using namespace std;
stack<char> op;
stack<int> num;
int opLe(char ch){//定义运算符的优先级
if(ch == '(') return 0;
if(ch == ',') return 1;
if(ch == '+' || ch == '-') return 2;
if(ch == '*' || ch == '/') return 3;
// if(ch == 'S') return 4;
}
int getSum(int x){//获得一个数的各个位的和
int ans = 0;
while(x){
ans += x % 10;
x /= 10;
}
return ans;
}
void opOnce(){//清栈运算一
char oo = op.top();
int y = num.top();
num.pop();
int x = num.top();
num.pop();
if(oo == '+'){
num.push(x + y);
}else if(oo == '-'){
num.push(x - y);
}else if(oo == '*'){
num.push(x * y);
}else if(oo == '/'){
num.push(x / y);
}else if(oo == ','){
int tx = getSum(x);
int ty = getSum(y);
num.push(max(tx,ty));
}
op.pop();
}
int solve(string s){//主体框架
for(int i = 0; i <s.size(); i ++){
if(s[i] >= '0' && s[i] <= '9'){
string t = s.substr(i);
int n = stoi(t);
num.push(n);
int jump = to_string(n).size();
//1234+
i += jump - 1;
}else if(s[i] == '('){
op.push('(');
}else if(s[i] == ')'){
while(op.top() != '('){
opOnce();
}
op.pop();
}else if(s[i] == 'S'){
i+=3;
}else{//+-*/,
while(! op.empty() && opLe(op.top()) >= opLe(s[i])){
opOnce();
}
op.push(s[i]);
}
}
while(!op.empty()){
opOnce();
}
return num.top();
}
int main(){
int n;
cin >> n;
while(n--){
string s;
cin >> s;
while(!op.empty()) op.pop();
while(!num.empty()) num.pop();
cout << solve(s) << endl;
}
return 0;
}
举例三
逻辑表达式是计算机科学中的重要概念和工具,包含逻辑值、逻辑运算、逻辑运算优先级等内容。
在一个逻辑表达式中,元素的值只有两种可能:00(表示假)和 1 (表示真)。元素之间有多种可能的逻辑运算,本题中只需考虑如下两种:"与"(符号为&)和"或"(符号为|)。其运算规则如下:
0&0=0 ,0&1 = 0, 1&0 = 0, 1&1 = 1
0|0 = 0 , 0|1 = 1, 1|0 = 1 , 1|1 = 1;
在一个逻辑表达式中还可能有括号。规定在运算时,括号内的部分先运算;两种运算并列时,& 运算优先于 | 运算;同种运算并列时,从左向右运算。
比如,表达式 0∣1&00∣1&0 的运算顺序等同于 0∣(1&0)0∣(1&0) ;表达式 0&1&0∣10&1&0∣1 的运算顺序等同于 ((0&1)&0)∣1((0&1)&0)∣1。
此外,在 C++ 等语言的有些编译器中,对逻辑表达式的计算会采用一种"短路"的策略。:在形如 a&b 的逻辑表达式中,会先计算a 部分的值,如果 a=0,那么整个逻辑表达式的值就一定为 0,故无需再计算 b 部分的值;同理,在形如a∣b 的逻辑表达式中,会先计算 a 部分的值,如果a=1,那么整个逻辑表达式的值就一定为 1,无需再计算 b 部分的值。
输入格式
输入共一行,一个非空字符串 s 表示待计算的逻辑表达式。
输出格式
输出共两行,第一行输出一个字符 0 或 1,表示这个逻辑表达式的值;第二行输出两个非负整数,分别表示计算上述逻辑表达式的过程中,形如 a&b 和 a∣b 的"短路"各出现了多少次。
数据范围
设 ∣s∣ 为字符串 s 的长度。
对于所有数据,1≤∣s∣≤10^6。保证 s 中仅含有字符 0
、1
、&
、|
、(
、)
且是一个符合规范的逻辑表达式。保证输入字符串的开头、中间和结尾均无额外的空格。保证 s 中没有重复的括号嵌套(即没有形如 ((a)) 形式的子串,其中 a 是符合规范的逻辑表达式)。
测试点编号 | s≤ | 特殊性质 |
---|---|---|
1∼2 | 3 | 无 |
3∼4 | 5 | 无 |
5 | 2000 | 1 |
6 | 2000 | 2 |
7 | 2000 | 3 |
8∼10 | 2000 | 无 |
11∼12 | 10^6 | 1 |
13∼14 | 10^6 | 2 |
15∼16 | 10^6 | 3 |
18∼20 | 10^6 | 无 |
其中:
特殊性质 1 为:保证 s 中没有字符 &。
特殊性质 2 为:保证 s 中没有字符 ∣。
特殊性质 3为:保证 s 中没有字符 ( 和 )。
提示
以下给出一个"符合规范的逻辑表达式"的形式化定义:
- 字符串 0 和 1 是符合规范的;
- 如果字符串 s 是符合规范的,且 s 不是形如 (t) 的字符串(其中t是符合规范的),那么字符串 (s) 也是符合规范的;
- 如果字符串 a 和 b 均是符合规范的,那么字符串 a&b、a|b 均是符合规范的;
- 所有符合规范的逻辑表达式均可由以上方法生成。
输出时每行末尾的多余空格,不影响答案正确性
样例输入 1
scss
0&(1|0)|(1|1|1&0)
样例输出 1
1
1 2
样例解释 1
该逻辑表达式的计算过程如下,每一行的注释表示上一行计算的过程:
scss
0&(1|0)|(1|1|1&0)
=(0&(1|0))|((1|1)|(1&0)) //用括号标明计算顺序
=0|((1|1)|(1&0)) //先计算最左侧的&,是一次形如a&b的"短路"
=0|(1|(1&0)) //再计算中间的|,是一次形如a|b的"短路"
=0|1 //再计算中间的|,是一次形如a|b的"短路"
=1
样例输入 2
scss
(0|1&0|1|1|(1|1))&(0&1&(1|0)|0|1|0)&0
样例输出 2
0
2 3
C++
#include <bits/stdc++.h>
using namespace std;
stack<char> op;
string s;
struct Node{//本身的值v, yu->与短路次数, huo->或短路次数
int yu = 0,huo = 0;
int v;
};
stack<Node> num;// 此题的运算数 是一个 Node结构体,
int opLevel(char ch){//定义各个运算符的优先级
if(ch == '(') return 0;
if(ch == '&') return 2;
if(ch == '|') return 1;
}
void opOnce(int i){//清栈运算一次
Node y = num.top();
num.pop();
Node x = num.top();
num.pop();
Node temp={0,0,0};
char oo = op.top();
op.pop();
if(oo == '&'){
if(x.v == 1){
temp.yu = x.yu + y.yu;
temp.huo = x.huo + y.huo;
}else if(x.v == 0){
temp.yu = x.yu + 1;
temp.huo = x.huo;
}
temp.v = x.v & y.v;
}else if(oo == '|'){
if(x.v == 1){
temp.yu = x.yu;
temp.huo = x.huo + 1;
}else if(x.v == 0){
temp.yu = x.yu + y.yu ;
temp.huo = x.huo + y.huo;
}
temp.v = x.v | y.v;
}
num.push(temp);
}
void solve(){//表达式整体框架逻辑
for(int i = 0; i < s.size();i ++){
char ch = s[i];
if(ch == '0' || ch == '1'){
num.push({0,0,ch-'0'});
}else if(ch == '('){
op.push('(');
}else if(ch == ')'){
while(op.top() != '('){
opOnce();
}
op.pop();
}else{
while(!op.empty() && opLevel(op.top()) >= opLevel(ch)){
opOnce();
}
op.push(ch);
}
}
while(!op.empty()){
opOnce();
}
auto tp = num.top();
cout << tp.v << endl;
cout << tp.yu << ' ' <<tp.huo;
}
int main(){
cin >> s;
solve();
return 0;
}
总结
可以看到三个例子, 在求表达式的值的时候, 题意会有稍许的改变, 但是整体的框架是不变的. 变的是运算数的内容, 清栈一次的规则.以及各个题目中对于运算符的优先级的定义.
这样所有关于表达式的值的问题都能在这个框架上解决.而且各个题目的清栈方式,优先级的定义都封装成函数.使得结构更加清晰明了.