LeetCode: 基本计算器详解

文章目录

  • [1. 计算器通解](#1. 计算器通解)
    • [1.1 逆波兰表达式](#1.1 逆波兰表达式)
    • [1.2 逆波兰表达式计算](#1.2 逆波兰表达式计算)
    • [1.3 合并双栈写法](#1.3 合并双栈写法)
  • [2. LeetCode 题目优化](#2. LeetCode 题目优化)
    • [2.1 基本计算器](#2.1 基本计算器)
    • [2.2 基本计算器 II](#2.2 基本计算器 II)

1. 计算器通解

1.1 逆波兰表达式

计算器实现,关键在于处理各类运算符之间的优先级关系 ,而传统的数学表达式,是中缀表达式,并不能很好表达这种优先级关系。
逆波兰表达式 ,即后缀表达式,该表达式本身已经将运算符优先关系处理好,运算是非常简单的。

因此,计算器通解 ,或者说解决最复杂的计算器问题,本质都是两步:逆波兰表达式转换 + 计算

逆波兰表达式核心处理逻辑:

  1. 用一个栈存储运算符。
  2. 遇到运算符时,如果栈为空,或者当前运算符优先级大于等于栈顶运算符,那么入栈。
  3. 遇到运算符时,如果当前运算符优先级低于栈顶运算符,那么需要出栈顶运算符加入最终的逆波兰表达式中,然后重复该逻辑,直至栈为空,或当前运算符优先级大于等于栈顶运算符,即情况2。
  4. 遇到数字,将数字直接加入逆波兰表达式中。
  5. 整个循环结束,将栈中剩余的运算符依次弹出,加入最终表达式中,
  6. 括号情况。括号内的表达式本质是相对独立的,同样需要独立考虑计算。本质就是以左括号为开始,右括号为结束,递归上述过程。

非常重要的一点,上述逆波兰表达式处理,必须针对合法的表达式,因此需要预处理原中缀表达式,去掉空格,同时保证一个运算符,一定严格有两个运算数与之匹配,即+X或-X情况,必须处理为0+X或0-X情况。

以下,以加减乘除为例的逆波兰表达式转换代码。

cpp 复制代码
int Hash(char ch) {
	switch(ch) {
		case '+':
			return 1;
		case '-':
			return 1;
		case '*':
			return 2;
		case '/':
			return 2;
	}
	return -1;
}
void myToRPN(const string& s, size_t& i, vector<string>& v) {
    stack<char> st;
    int sz = s.size();
    while (i < sz) {
        if (isdigit(s[i])) {
            string num;
            while (isdigit(s[i])) {
                num += s[i++];
            }
            v.push_back(num);
        }

        else if (s[i] == '(') {
            i++;
            myToRPN(s, i, v);
        }

        else if (s[i] == ')') {
            while (!st.empty()) {
                v.push_back(string(1, st.top()));
                st.pop();
            }
            i++;
            return;
        }

        else {
            if (st.empty() ||
                Hash(s[i]) > Hash(st.top())) {
                st.push(s[i++]);
            } else {
                char ch = st.top();
                st.pop();
                v.push_back(string(1, ch));
            }
        }
    }
    while (!st.empty()) {
        char ch = st.top();
        st.pop();
        v.push_back(string(1, ch));
    }
    return;
}

1.2 逆波兰表达式计算

得到逆波兰表达式后,计算逻辑相对而言是简单的。

具体规则:

  1. 如果是数字,那么放入栈中。
  2. 如果运算符,那么出栈中两个元素,进行运算。需注意,先出栈的数字,为运算符右侧元素,后出栈的数字,为运算符左侧元素。 运算后的结果,重新加入栈中。
  3. 遍历完整个逆波兰表达式后,若为合法逆波兰表达式,那么此时栈中仅会剩下一个元素,即为最终结果,返回栈顶元素即可。

LeetCode链接

以下同样是加减乘除版本的计算代码示例

cpp 复制代码
int evalRPN(vector<string>& tokens) {
    stack<int> st;
    int sz = tokens.size();
    for (int i = 0; i < sz; i++) {
        const string& tmp = tokens[i];
        if (tmp == "+" || tmp == "-" || tmp == "*" || tmp == "/") {
            int val2 = st.top();
            st.pop();
            int val1 = st.top();
            st.pop();

            switch (tmp[0]) {
            case '+':
                st.push(val1 + val2);
                break;
            case '-':
                st.push(val1 - val2);
                break;
            case '*':
                st.push(val1 * val2);
                break;
            case '/':
                st.push(val1 / val2);
                break;
            }
        } else {
            st.push(stoi(tmp));
        }
    }
    return st.top();
}

1.3 合并双栈写法

在上述逆波兰表达式转换 + 计算 两个过程中,各自用到了一个栈结构 ,因此我们可以使用双栈结构,将两个过程合并为一个过程,即一边进行逆波兰表达式转换,一边进行计算,以此简化整体代码。

牛客题目链接

加减乘除版本的示例代码如下:

cpp 复制代码
#include <cctype>
class Solution {
public:
    int Hash(char ch) {
        if(ch == '+')
            return 1;
        else if(ch == '-') 
            return 1;
        else if(ch == '*')
            return 2;   
        return -1;
    }
    long long cal(stack<char>& op,stack<long long>& st) {
        char ch = op.top();
        op.pop();
        long long n1 = st.top();
        st.pop();
        long long n2 = st.top();
        st.pop();
        switch(ch) {
            case '+' :
                return n1 + n2;
            case '-':
                return n2 - n1;
            case '*':
                return n1 * n2;
        }
        return -1;
    }
    int ToRPN(const string& str,stack<long long>& st,int& i) {
        stack<char> op;
        int sz = str.size();
        while(i < sz) {
            char ch = str[i];
            if(isdigit(ch)) {
                int j = i + 1;
                long long num = ch - '0';
                while(j < sz && isdigit(str[j]))
                    num = num * 10 + str[j++] - '0';
                i = j;
                st.push(num);
            }
            else if(ch == '+' || ch == '-' || ch == '*') {
                 if(op.empty() || Hash(op.top()) < Hash(ch)) {
                    op.push(ch);
                    i++;
                 } 
                 else {
                    // 此处需要循环比较,直至进入上面的逻辑
                    st.push(cal(op,st));              
                 }
            }
            else {
                if(ch == '(')
                    ToRPN(str,st,++i);
                else {
                    i++;
                    while(!op.empty())
                    st.push(cal(op,st));
                    return st.top();
                }
            }   
        }
        while(!op.empty())
            st.push(cal(op,st));
        return st.top();
    }

    int solve(string s) {
        string init;
        int n = s.size();
        for(int i = 0; i < n; i++) {
            if(isspace(s[i]))
                continue;
            else if(s[i] == '-' && (i == 0 || s[i - 1] == '('))
                init += "0-";
            else
                init += s[i];
        }
        int i = 0;
        stack<long long> st; // 记录数字
        int ret = ToRPN(init,st,i);
        return ret;        
    }
};

2. LeetCode 题目优化

上述逆波兰表达式转换 + 计算的解法 ,本质是应对最复杂计算器情况的通解,代码也是最复杂的,但实际上,针对具体的计算器题目,由于具体要求不同,可能不需要使用上述做法,而使用更贴近题目本身的优化解法

2.1 基本计算器

LeetCode 链接

这道计算器题目特殊在:没有乘除运算符,只有加减运算和括号。
对于只包含加减运算的表达式,可以通过去括号,使得表达式最终仅仅为简单的加减运算。而去括号逻辑本质就是正负符号的叠加运算,这一点,我们可以借助栈实现。

具体思路:

  1. 维护一个栈和一个变量。栈用来记录一对括号之前的符号情况,变量用于记录前一个符号情况。
  2. 栈的维护。遍历到左括号,就将变量记录的符号与栈顶符号相乘,然后结果入栈;遍历到右括号,就将栈顶元素出栈。
  3. 变量维护。每遇到一个符号,动态更新符号情况即可。
  4. 栈和变量初始化。为了不对栈空和变量初始情况做额外判断,统一初始化为正,不影响结果,同时简化代码特别注意,维护栈时,遍历到左括号,变量与栈顶符号相乘,结果入栈后,需重新将变量初始化为1,避免括号内第一个数字为正数,却不带正号的特殊情况。

代码示例如下:

cpp 复制代码
class Solution {
public:
    int calculate(string s) {
        stack<int> st;
        st.push(1);
        int pre = 1;
        int i = 0, n = s.size();
        long long ret = 0;
        while(i < n) {
            if(isspace(s[i]));
            else if(s[i] == '+')
                pre = 1;
            else if(s[i] == '-')
                pre = -1;
            else if(isdigit(s[i])) {
                int j = i + 1;
                long long sum = s[i] - '0';
                while(j < n && isdigit(s[j]))
                    sum = sum * 10 + s[j++] - '0';
                ret += st.top() * pre * sum;
                i = j;
                continue;
            }
            else {
                if(s[i] == '(') {
                    st.push(st.top() * pre);
                    pre = 1; // reset
                }
                else {
                    st.pop();
                }
            }
            i++;
        }
        return ret;
    }
};

2.2 基本计算器 II

LeetCode 链接

这道计算器题目,特殊之处在于:有加减乘除四种运算符,但是没有括号,所以优先级的处理是明确的------一定先处理乘除,最后处理加减。

那么根据上述思路,可得如下算法规则:

  1. 维护三个变量,ret记录结果,pre记录前一个符号,pre_num记录前一个数字。
  2. pre维护。遇到符号就更新。
  3. pre_numret维护。遇到数字,判断pre符号。如果是加减符号,ret += pre_numpre_num 动态更新成当前数字(如果是减号,那么更新成相反数);如果是乘除符号,那么pre_num *= 当前数pre_num /= 当签数
  4. 细节问题。retpre_num初始化为 0 , pre 初始化为正号情况。

具体代码如下:

cpp 复制代码
class Solution {
public:
    int calculate(string s) {
        long long ret = 0;
        long long pre_num = 0;
        int i = 0, n = s.size();
        char pre = '+';
        while(i < n) {
            if(isspace(s[i]))
                i++;
            else if(isdigit(s[i])) {
                int j = i + 1;
                long long sum = s[i] - '0';
                while(j < n && isdigit(s[j]))
                    sum = sum * 10 + s[j++] - '0';              
                switch(pre) {
                    case '+':
                        ret += pre_num;
                        pre_num = sum;      
                        break;
                    case '-':
                        ret += pre_num;
                        pre_num = -sum;
                        break;
                    case '*':
                        pre_num *= sum;
                        break;
                    case '/':
                        pre_num /= sum;
                        break;
                }
                i = j;
            }
            else
                pre = s[i++];
        }
        ret += pre_num;
        return ret;
    }
};
相关推荐
haing20192 小时前
卡尔曼滤波(Kalman Filter)原理
线性代数·算法·机器学习
Dev7z2 小时前
基于深度学习的泳池溺水行为检测算法设计
人工智能·深度学习·算法
Pluchon2 小时前
硅基计划4.0 算法 优先级队列
数据结构·算法·排序算法
Swift社区2 小时前
LeetCode 375 - 猜数字大小 II
算法·leetcode·swift
sunfove2 小时前
从“锯齿”到“光滑”:相位解包裹 (Phase Unwrapping) 算法深度解析
算法
DuHz2 小时前
自动驾驶雷达干扰缓解:探索主动策略论文精读
论文阅读·人工智能·算法·机器学习·自动驾驶·汽车·信号处理
漫随流水2 小时前
leetcode算法(257.二叉树的所有路径)
数据结构·算法·leetcode·二叉树
liu****2 小时前
神经网络基础
人工智能·深度学习·神经网络·算法·数据挖掘·回归
有一个好名字2 小时前
力扣-二叉树的最大深度
算法·leetcode·深度优先