一、各种表达式的含义与操作
请看下面链接里面的博客吧,这是一位大佬写的,里面的图很是不错,可以看看。
二、题目
给定一个表达式,其中运算符仅包含 +,-,*,/
(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
注意:
- 数据保证给定的表达式合法。
- 题目保证符号
-
只作为减号出现,不会作为负号出现,例如,-1+2
,(2+2)*(-(1+1)+2)
之类表达式均不会出现。 - 题目保证表达式中所有数字均为正整数。
- 题目保证表达式在中间计算过程以及结果中,均不超过 。
- 题目中的整除是指向 0取整,也就是说对于大于 0的结果向下取整,例如 5/3=1,对于小于 0的结果向上取整,例如 5/(1−4)=−1。
- C++和Java中的整除默认是向零取整;Python中的整除
//
默认向下取整,因此Python的eval()
函数中的整除也是向下取整,在本题中不能直接使用。
输入格式
共一行,为给定表达式。
输出格式
共一行,为表达式的结果。
数据范围
表达式的长度不超过 。
输入样例:
(2+2)*(1+1)
输出样例:
8
/*这个题是中缀表达式的存储与计算,至于什么是中缀表达式,前缀表达式,后缀表达式,就看下面链接里面的
博客吧。
*/
#include<iostream>
#include<stack>
#include<algorithm>
#include<unordered_map>//要使用哈希表,就要加这个。
using namespace std;
stack<int> num;
stack<char> op;
//建立两个栈,一个是专门存数字字符的,一个是专门存储符号的。
void eval(){
auto b=num.top(); num.pop();
auto a=num.top(); num.pop();
auto c=op.top(); op.pop();
int x;
if(c=='+') x=a+b;
if(c=='-') x=a-b;
if(c=='*') x=a*b;
if(c=='/') x=a/b;
/*这个eval()函数是执行的弹出栈并执行计算的操作,为什么要先b再a呢,这是因为栈的原因,先进后出的
性质,导致了这样,至于具体是怎么样,可以看下面链接里面的那篇博客的图解,那是一位大佬分析的,很是
不错的。*/
num.push(x);
/*每一步都能得到一个结果,将得到的结果压入num栈中。*/
}
int main(){
unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};
//用哈希表来定义各个运算符的优先级,这里要记得在开头调用有这个函数的开头。
string str;
cin>>str;
for(int i=0;i<str.size();i++){
auto c=str[i];//遍历这个字符串里面的每一个字符
if(isdigit(c)){//注意这里的isdigit(),这个是判断一个字符是不是数字字符的一个函数吧。
int x=0,j=i;
while(j<str.size()&&isdigit(str[j]))
x=x*10+str[j++]-'0';
/*这里是容易疏忽的一点,虽然样例输入的都是一位数的运算,但很有可能还有多位数,所以
这个x其实就是求连续的数字字符的大小,获得其值*/
i=j-1;//这个i=j-1很是重要,对我来说的话,下面会有详细的解释的。
num.push(x);//得到了连续数字字符的结果后就入栈num。
}else if(c=='(') op.push(c);//如果检测到了左括号直接入op栈就可以。
else if(c==')'){
while(op.top()!='(') eval();
/*如果检测到了是右括号的话,那右括号不用入栈,直接在现在的op栈里面操作,一直输出和计算,
直到找到左括号为止,就停止操作了。*/
op.pop();/*这个pop是操作到了左括号,表示左右括号之间的运算符已经全部操作完,这个时候直接
将op栈里面的左括号弹出栈就行,因为这个时候已经没用了。*/
}else{
while(op.size()&&op.top()!='('&&pr[c]<=pr[op.top()])
eval();
/*这是另一种情况,当栈里面没有任何的括号时,就只是那几个操作符的话,那么就是判断优先级,
只要这个op栈不为空且里面没有括号且此时c的符号优先级比之前的栈顶的符号优先级低的话,那么
就会先把原来栈里面的运算符一直出栈并运算,直到找到比现在c这个字符优先级高的为止,
加入说都是同级的,那么就是把栈里面所有的运算的字符全部都出栈并操作,栈空以后才将c这个
字符运算符给插进去,就很完美了,至于这个为什么优先级这么想,具体的表现的话,我会在下面
画一张图,看了应该就能理解。*/
op.push(c);
/*这个op的入栈就是最后我说的那个了,要么都是同级的运算符,栈空入栈;要么就是c的优先级本来
就比原来栈顶的优先级高,直接入栈,这是这句代码的含义,要注意。*/
}
}
while(op.size()) eval();
/*如果说因为各种各样的操作以后,op符号栈里面还有运算符,那就把他们全部都弹出并计算,直至栈空。*/
cout<<num.top()<<endl;
/*最后各种各样的操作完成后,最后得到的num栈里面就只有一个元素了,也就是那个最后的结果,所以直接
输出num.top()就行。*/
return 0;
}
代码详细解释
1. 引入头文件
#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <unordered_map>
#include <iostream>
: 引入输入输出流库,用于标准输入输出。#include <cstring>
: 引入C风格字符串处理库,这里其实不需要用到。#include <algorithm>
: 引入算法库,这里也没有使用。#include <stack>
: 引入栈的实现库。#include <unordered_map>
: 引入无序映射(哈希表)库,用于存储操作符优先级。
2. 定义栈
stack<int> num;
stack<char> op;
stack<int> num;
: 整数栈,用于存储操作数。stack<char> op;
: 字符栈,用于存储操作符和括号。
3. eval
函数
void eval()
{
auto b = num.top(); num.pop();
auto a = num.top(); num.pop();
auto c = op.top(); op.pop();
int x;
if (c == '+') x = a + b;
else if (c == '-') x = a - b;
else if (c == '*') x = a * b;
else x = a / b;
num.push(x);
}
auto b = num.top(); num.pop();
: 从num
栈中取出顶层元素(右操作数)并移除它。auto a = num.top(); num.pop();
: 从num
栈中取出顶层元素(左操作数)并移除它。auto c = op.top(); op.pop();
: 从op
栈中取出顶层操作符并移除它。int x;
: 定义一个变量x
用于存储计算结果。if (c == '+') x = a + b;
: 根据操作符c
执行相应的计算,并将结果存入x
。num.push(x);
: 将计算结果x
压入num
栈中。
4. main
函数
int main()
{
unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
string str;
cin >> str;
for (int i = 0; i < str.size(); i ++ )
{
auto c = str[i];
if (isdigit(c))
{
int x = 0, j = i;
while (j < str.size() && isdigit(str[j]))
x = x * 10 + str[j ++ ] - '0';
i = j - 1;
num.push(x);
}
else if (c == '(') op.push(c);
else if (c == ')')
{
while (op.top() != '(') eval();
op.pop();
}
else
{
while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
op.push(c);
}
}
while (op.size()) eval();
cout << num.top() << endl;
return 0;
}
unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
: 创建一个哈希表pr
用于存储操作符的优先级,*
和/
的优先级高于+
和-
。string str; cin >> str;
: 从标准输入读取一个字符串str
。for (int i = 0; i < str.size(); i++ )
: 遍历字符串中的每个字符。auto c = str[i];
: 获取当前字符。if (isdigit(c))
: 如果当前字符是数字:int x = 0, j = i;
: 定义一个变量x
用于构建完整的数字,并初始化j
为当前索引。while (j < str.size() && isdigit(str[j]))
: 读取完整的数字,直到字符不是数字为止。x = x * 10 + str[j++] - '0';
: 逐步构建数字。i = j - 1;
: 更新i
以跳过已读取的数字部分。num.push(x);
: 将读取到的数字压入num
栈中。
else if (c == '(') op.push(c);
: 如果当前字符是左括号,将其压入op
栈。else if (c == ')')
: 如果当前字符是右括号:while (op.top() != '(') eval();
: 不断执行eval
直到遇到左括号。op.pop();
: 弹出左括号。
else
: 如果当前字符是操作符:while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
: 根据操作符优先级执行eval
,直到栈顶的操作符优先级低于当前操作符。op.push(c);
: 将当前操作符压入op
栈中。
while (op.size()) eval();
: 执行所有剩余的操作。cout << num.top() << endl;
: 输出结果。
实际例子
假设输入字符串是 "3+(2*2)"
:
-
初始状态:
num
: 空op
: 空
-
读取 '3':
num
:3
op
: 空
-
读取 '+':
num
:3
op
:+
-
读取 '(':
num
:3
op
:+ (
-
读取 '2':
num
:3 2
op
:+ (
-
读取 '*':
num
:3 2
op
:+ ( *
-
读取 '2':
num
:3 2 2
op
:+ ( *
-
读取 ')':
- 计算
2 * 2
:num
:3 4
op
:+
- 计算
3 + 4
:num
:7
op
: 空
- 计算
最终输出 7
。
三、对于里面i=j-1的理解
代码中的 i = j - 1
代码的关键部分如下:
for (int i = 0; i < str.size(); i++) {
if (isdigit(str[i])) {
int x = 0, j = i;
while (j < str.size() && isdigit(str[j])) {
x = x * 10 + str[j++] - '0';
}
// 跳过已经处理过的数字部分
i = j - 1;
}
// 处理其他字符,如 '+', '*', '(', ')'
}
在这个代码中,i = j - 1
的作用是更新 i
的值,以便在下一次 for
循环时,从处理完数字后的下一个位置继续进行。
去掉 i = j - 1
的影响
如果去掉 i = j - 1
,i
的值不会被更新到数字解析后的下一个位置。这可能导致一些问题:
-
重复处理:
- 当
for
循环进入到i
的位置时,它会重新开始处理已经解析过的部分。这意味着原本已经被处理的部分(比如"37"
)会被重复处理,因为i
的值没有被更新。
- 当
-
无限循环问题:
- 如果
i
从未更新,for
循环会一直从原始位置开始,例如位置 0。因此,如果i
从未跳过已处理的部分,循环会不断重新处理这些部分,从而导致无限循环。
- 如果
去掉i=j-1后的代码
for (int i = 0; i < str.size(); i++) {
if (isdigit(str[i])) {
int x = 0, j = i;
while (j < str.size() && isdigit(str[j])) {
x = x * 10 + str[j++] - '0';
}
// 此处如果没有 i = j - 1,i 将保持原来的位置
}
// 处理其他字符,如 '+', '*', '(', ')'
}
假设我们解析了 "37"
并且 j
在此时是 2
,如果我们没有 i = j - 1
,那么 i
会继续增加而没有跳过已处理的部分:
-
当前
i
的值:i
仍然是循环的当前值,比如0
或1
。
-
下一次迭代:
- 在下一次
for
循环中,i
可能会从未处理的部分开始(如2
),但由于未更新i
,循环可能会再次处理已经解析的部分。
- 在下一次
缺少i=j-1后的代码示例分析
假设你的字符串是 "37+8*(9-4)"
,且初始 i
为 0
:
-
当
i
为0
时,我们解析了"37"
并更新j
到2
,此时没有i = j - 1
,i
继续从0
开始。 -
因此,
for
循环将再次处理0
位置的字符'3'
和'7'
,从而重复解析这些字符。
总结来说,i = j - 1
是用来跳过已经处理过的部分,确保 for
循环从未处理的部分开始。如果没有更新 i
,for
循环会重复处理相同的部分,可能导致无限循环或错误的结果。
这里要注意一点,就是在判断连续的数字字符后,遍历到了运算符字符后,for循环里面的循环体,从内到外也就是while,if等语句都会退出,这个时候虽然不满足循环条件了,要退出当前的for循环,那么for循环最后的那个i++也是要执行的,只不过后面不会再有机会i++了而已,除非又遇到了连续数字字符,详细点来说就是以下的描述:
i++
在 for
循环中确实总是会执行,但如何执行以及最终的效果会受到循环体内其他逻辑的影响。
详细分析
在 for
循环的结构中:
for (initialization; condition; increment) {
// loop body
}
- 初始化:
initialization
只在第一次迭代之前执行一次。 - 条件: 在每次迭代之前评估,如果条件为
true
,则执行循环体。 - 增量: 每次循环体执行完后,
increment
(如i++
)都会被执行。
i++
的执行
即使在循环体内你做了其他操作,i++
这个增量操作在每次循环结束时都会执行。例如:
for (int i = 0; i < str.size(); i++) {
auto c = str[i];
if (isdigit(c)) {
int x = 0, j = i;
while (j < str.size() && isdigit(str[j]))
x = x * 10 + str[j++] - '0';
num.push(x);
i = j - 1; // 更新 i,以跳过已处理的数字
}
// 其他逻辑
}
在这个例子中:
-
如果有
i = j - 1
: 这行代码更新i
的值,使得在下一次循环开始时i
跳过了已经处理过的数字。因此,i++
实际上在这里会使i
增加到j
,但是由于i
被设置为j - 1
,下一次循环开始时i
从j - 1
开始执行,所以i++
使得i
正确地推进到下一个未处理的字符位置。 -
如果没有
i = j - 1
: 在这种情况下,i
会在数字处理完后被默认递增。这样,如果你在处理数字后没有更新i
,下一个迭代会从上一个字符开始,这样可能会导致错误的重复处理。此时,i++
会将i
增加到下一个位置,但由于没有正确跳过已处理的数字,可能会导致重复遍历。
结论
i++
总是会执行,但最终i
的值取决于循环体内的其他逻辑。i = j - 1
: 在这种情况下,i
被设置为处理过的数字的末尾减去 1,确保i
从正确的位置继续遍历,避免了重复处理的问题。
在没有 i = j - 1
的情况下,i++
会导致重复处理字符;而有了 i = j - 1
,可以确保跳过已经处理的部分,防止重复遍历。
四,关于优先级的图
从上面的图就可以看出来,这个题是用了很多知识:栈的存储与应用,二叉树的中序遍历,哈希表的使用,连续数字字符的获取,for循环的使用等等,所以一定要着重的注意这个题,真的很经典也很难。