表达式求值(综合应用的难题)

一、各种表达式的含义与操作

请看下面链接里面的博客吧,这是一位大佬写的,里面的图很是不错,可以看看。

各种表达式的概念与操作

二、题目

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

  • 数据保证给定的表达式合法。
  • 题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
  • 题目保证表达式中所有数字均为正整数。
  • 题目保证表达式在中间计算过程以及结果中,均不超过 2^{31}-1
  • 题目中的整除是指向 0取整,也就是说对于大于 0的结果向下取整,例如 5/3=1,对于小于 0的结果向上取整,例如 5/(1−4)=−1。
  • C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。

输入格式

共一行,为给定表达式。

输出格式

共一行,为表达式的结果。

数据范围

表达式的长度不超过 10^{5}

输入样例:

(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)"

  1. 初始状态:

    • num: 空
    • op: 空
  2. 读取 '3':

    • num3
    • op: 空
  3. 读取 '+':

    • num3
    • op+
  4. 读取 '(':

    • num3
    • op+ (
  5. 读取 '2':

    • num3 2
    • op+ (
  6. 读取 '*':

    • num3 2
    • op+ ( *
  7. 读取 '2':

    • num3 2 2
    • op+ ( *
  8. 读取 ')':

    • 计算 2 * 2
      • num3 4
      • op+
    • 计算 3 + 4
      • num7
      • 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 - 1i 的值不会被更新到数字解析后的下一个位置。这可能导致一些问题:

  1. 重复处理

    • 当 for 循环进入到 i 的位置时,它会重新开始处理已经解析过的部分。这意味着原本已经被处理的部分(比如 "37")会被重复处理,因为 i 的值没有被更新。
  2. 无限循环问题

    • 如果 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 会继续增加而没有跳过已处理的部分:

  1. 当前 i 的值

    • i 仍然是循环的当前值,比如 0 或 1
  2. 下一次迭代

    • 在下一次 for 循环中,i 可能会从未处理的部分开始(如 2),但由于未更新 i,循环可能会再次处理已经解析的部分。

缺少i=j-1后的代码示例分析

假设你的字符串是 "37+8*(9-4)",且初始 i0

  1. i0 时,我们解析了 "37" 并更新 j2,此时没有 i = j - 1i 继续从 0 开始。

  2. 因此,for 循环将再次处理 0 位置的字符 '3''7',从而重复解析这些字符。

总结来说,i = j - 1 是用来跳过已经处理过的部分,确保 for 循环从未处理的部分开始。如果没有更新 ifor 循环会重复处理相同的部分,可能导致无限循环或错误的结果。

这里要注意一点,就是在判断连续的数字字符后,遍历到了运算符字符后,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,下一次循环开始时 ij - 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循环的使用等等,所以一定要着重的注意这个题,真的很经典也很难。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值