C++加餐课-stack_queue:计算器-逆波兰表达式

1. 计算器实现思路

150. 逆波兰表达式求值 - 力扣(LeetCode)

224. 基本计算器 - 力扣(LeetCode)

• 我们日常写的计算表达式都是中缀表达式,也就是运算符在中间,运算数在两边,但是直接读取无法马上进行运算------因为一个计算表达式还涉及运算符优先级问题。

如: 1-2*(3-4)+5 中遇到-和*都无法运算,因为后面还有括号优先级更高。

• 所以其中一种实现思路是把中缀表达式转换为后缀表达式,也就是说分析计算表达式的优先级,将运算数放到前面,运算符放到运算数的后面,然后我们依次读取后缀表达式,遇到运算符就可以进行运算了。

后缀表达式也就做逆波兰表达式(Reverse Polish Notation,RPN),这种表示法由波兰逻辑学家J·卢卡西维兹于1929年提出,后来被广泛应用于计算机科学中。

【结论】计算器实现的时候,给到任意一个四则运算表达式(带括号),使用后缀表达式更方便于计算。

2. 后缀表达式的运算方式

后缀表达式因为已经确定好优先级,运算方式非常简单,就是:

遇到运算符时,取前面的两个运算数进行运算。

因为经过中缀转后缀优先级已经确定好了,先遇到的运算符一定就是需要先运算的。

【具体步骤】

  • 建立一个栈存储运算数,读取后缀表达式,遇到运算数入栈,遇到运算符,出栈顶的两个数据进行运算。
  • 运算后将结果作为一个运算数入栈,继续参与下一次的运算。

读取表达式结束后,最后栈里面的值就是运算结果。

cpp 复制代码
class Solution {
public:
	int evalRPN(const vector<string>& tokens) 
    {
        //定义栈
		stack<int> s;

        //遍历后缀表达式,操作数入栈
	    //for (size_t i = 0; i < tokens.size(); ++i)
        for (auto& str : tokens)
		{
			//const string& str = tokens[i];

			// 如果str为运算数
			if (!("+" == str || "-" == str || "*" == str || "/" == str))
			{
				s.push(stoi(str));
			}
            // 如果str为运算符
			else
			{
                //取左右运算数
				int right = s.top();
				s.pop();
				int left = s.top();
				s.pop();

                //匹配运算符、进行相应运算
				switch (str[0])
				{
				case '+':
					s.push(left + right);
					break;
				case '-':
					s.push(left - right);
					break;
				case '*':
					s.push(left * right);
					break;
				case '/':
					s.push(left / right);
					break;
				}
			}
		}
        //遍历完毕,返回栈顶元素
		return s.top();
	}
};

【课堂演示】


示例分析:

先出栈的(最靠近运算符的)运算数是右操作数------加、乘不论顺序,减、除要论顺序。

借助栈来实现------特点:后进先出------能取到最靠近运算符的两个运算数。


3. 中缀表达式转后缀表达式

3.1 转换思路

  • 依次读取计算表达式中的值,遇到运算数直接输出。
  • 建立一个栈存储运算符,利用栈后进先出性质,遇到后面运算符,出栈里面存的前面运算符进行比较,确定优先级。
  • 遇到运算符,如果栈为空或者栈不为空且当前运算符比栈顶运算符优先级高,则当前运算符入栈。 因为如果栈里面存储的是前一个运算符,当前运算符比前一个优先级高,说明前一个不能运算,当前运算符也不能运算,因为后面可能还有更高优先级的运算符。
  • 遇到运算符,如果栈不为为空且当前运算符比栈顶运算符优先级低或相等,说明栈顶的运算符可以运算了,则输出栈顶运算符,当前运算符继续走前面遇到运算符的逻辑。
  • 如果遇到(),则把括号的计算表达式当成一个子表达式,跟上思路类似,进行递归处理子表达式,处理后转换出的后缀表达式加在前面表达式的后面即可。
  • 计算表达式或者()中子表达式结束值,输出栈中所有运算符。

【课堂演示】

【中缀表达式转后缀表达式分析】

  • 首先不改变所有操作数的相对顺序;
  • 关键是操作符的顺序,要根据它的优先级来排列,并且要紧跟着它的两个操作数;

转换思路:

【1】

  • 依次读取计算表达式中的值,遇到运算数直接输出。
    (输出:打印出来、或者存到容器内,这里由于要实现计算器,所以选择存到容器内)
  • 建立一个栈存储运算符,利用栈后进先出性质,遇到后面运算符,出栈里面存的前面运算符进行比较,确定优先级。

【2·核心逻辑】

  • 遇到运算符,如果栈为空或者栈不为空且当前运算符比栈顶运算符优先级高,则当前运算符入栈。
    • 因为如果栈里面存储的是前一个运算符,当前运算符比前一个优先级高,说明前一个不能运算。
    • 同时当前运算符也不能运算,因为后面可能还有更高优先级的运算符。
  • 遇到运算符,如果栈不为空且当前运算符比栈顶运算符优先级低或相等,说明栈顶的运算符可以运算了,则输出栈顶运算符,当前运算符继续走前面遇到运算符的逻辑。

【3】

  • 如果遇到(),则把括号的计算表达式当成一个子表达式,跟上思路类似,进行递归处理子表达式,处理后转换出的后缀表达式加在前面表达式的后面即可。
  • 计算表达式或者()中子表达式结束值,输出栈中所有运算符。

【1】

因为操作数的相对顺序不会改变,所以可以直接输出。

这里也需要借助一个栈,实现运算符的优先顺序的调整。

l1表示第一个左操作数,这里的关键就是看第一个右操作数(第二个左操作数),到底是先和它左边的操作符结合,还是先和它右边的操作符结合。


【2】

栈为空,遇到加号不能直接运算,因为后面可能有比加号优先级更高的运算符。

所以只能先把加号放到栈里面存起来。

栈顶存储的是前一个运算符:

  • 后一个运算符优先级更高,则前一个运算符不可以运算,后一个也不能直接运算。
    • 后一个运算符入栈成为新的"前一个运算符",继续看新的"后一个运算符"
  • 后一个运算符优先级不更高,则前一个运算符可以运算,直接输出。
    • 后一个运算符入栈成为新的"前一个运算符",继续看新的"后一个运算符"
    • 需要继续走遇到运算符的逻辑,栈为空才能入栈。

后一个运算符优先级不更高(相等或更低),则前一个运算符可以运算,直接输出,哪怕后面还有更高优先级的运算符,也不用管。因为运算符是通过相邻的运算符来确定优先级,并不会跨着去比较优先级。

比如:1+2+3×4,乘号不会影响第一个加号的优先级,影响的是第二个加号的优先级。

结果:1 2 + 3 4 × +


【例1】只有+、-


【例2】还有*、()

【注意】

  • 遇到(,建立一个新栈,递归处理。
  • 遇到),递归结束,弹出新栈的所有运算符,新栈销毁,看回旧栈。
  • 中缀表达式继续向后走。

最后一排跳了一个步骤,下图是完整步骤。


【减号的处理】

  • 减号前面是操作数,正常运算;
  • 加号前面是操作符,把减号当作"0-"

3.2 代码实现

cpp 复制代码
class Solution
{
public:
	//map<char, int> _operatorPrecedence = { { '+', 1 }, { '-', 1 }, { '*', 2 }, { '/', 2 } };
	int operatorPrecedence(char ch)
	{
		struct opPD
		{
			char _op;
			int _pd;
		};
		static opPD arr[] = { {'+', 1},{'-', 1},{'*', 2},{'/', 2} };
		for (auto& e : arr)
		{
			if (e._op == ch)
			{
				return e._pd;
			}
		}
		assert(false);
		return -1;
	}
	void toRPN(const string& s, size_t& i, vector<string>& v)
	{
		stack<char> st;
		while (i < s.size())
		{
			if (isdigit(s[i]))
			{
				// 操作数输出
				string num;
				while (i < s.size() && isdigit(s[i]))
				{
					num += s[i];
					++i;
				}
				v.push_back(num);
			}
			else
			{
				if (s[i] == '(')
				{
					// 递归⽅式处理括号中的⼦表达式
					++i;
					toRPN(s, i, v);
				}
				else if (s[i] == ')')
				{
					++i;
					// 栈中的运算符全部输出
					while (!st.empty())
					{
						v.push_back(string(1, st.top()));
						st.pop();
					}
					// 结束递归
					return;
				}
				else
				{
					// 运算符
					// 1、如果栈为空或者栈不为空且当前运算符⽐栈顶运算符优先级⾼,则当前运算符⼊栈
						// 2、如果栈不为为空且⽐栈顶运算符优先级低或相等,说明栈顶的运算符可以运算了,
						// 输出栈顶运算符,当前运算符继续⾛前⾯遇到运算符的逻辑
					if (st.empty() || operatorPrecedence(s[i]) >
						operatorPrecedence(st.top()))
					{
						st.push(s[i]);
						++i;
					}
					else
					{
						v.push_back(string(1, st.top()));
						st.pop();
					}
				}
			}
		}
		// 栈中的运算符全部输出
		while (!st.empty())
		{
			v.push_back(string(1, st.top()));
			st.pop();
		}
	}
};
int main()
{
	size_t i = 0;
	vector<string> v;
	//string str = "1+2-3";
	string str = "1+2-(3*4+5)-7";
	Solution().toRPN(str, i, v);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

【课堂演示】

计算器的实现,拿到的参数是一个字符串表达式,逆波兰表达式的计算拿到的是一个字符串数组。

所以"中缀转后缀函数"的参数就需要:

  • string:接收计算器接收到的表达式;(const string&)
  • vextor<string>:给到后缀表达式计算函数;(vector<string>&)(输出型参数)
  • size_t& i:遍历中缀表达式的下标,遇到括号进行递归,里面++i外面也会跟着走。

把每个字符输出成一个字符串,存储到vector<string>&,是为了跟前面的接口契合。


【注意】多位数就只能用string存储,所以才必须使用vector<string>。



基本框架:

<string>里面包含了<cctype>。



是操作数,则直接输出。

问题在于可能后一个字符也是操作数------它们一起组成一个多位操作数。

操作数不一定是只有个位,可能是一个多位数。

所以遇到操作数不能直接插入vector<string>,需要先存储到一个临时string。

往后遍历,遇到连续操作数就都尾插给string,直到不是操作数,再把string输出。

遇到运算符,就需要借助辅助栈。

问题在于运算符的优先级不能使用比较运算符来判定。

四则运算符的ASCII码值和他们的运算优先级没关联。

所以需要建立运算符和优先级之间的关联:

  • 没学map:使用函数判定;
  • 学了map:使用map判定;

输出运算符的时候,不能直接输出char op,因为string是没有直接支持使用char构造的。

string支持的是使用n个char去构造,这里可以push一个op构造的匿名对象。


还需要处理括号的问题,插入遇到的运算符是括号,需要建立新的辅助栈,处理子表达式。


当i == s.size(),表达式结束,把辅助栈里面的运算符全部输出。


【测试1】

【测试2】


4. 计算器实现

有了上面两个部分的基础,实现一个计算器的OJ题的大部分问题就解决了。

但是这里还有一些问题需要处理:

  • OJ中给的中缀表达式是字符串,字符串中包含空格,需要去掉空格。
  • 其次就是负数和减号,要进行区分,将所有的负数-x转换为0-x。
cpp 复制代码
class Solution
{
public:
	//map<char, int> _operatorPrecedence = { { '+', 1 }, { '-', 1 }, { '*', 2 }, { '/', 2 } };
	int operatorPrecedence(char ch)
	{
		struct opPD
		{
			char _op;
			int _pd;
		};
		static opPD arr[] = { {'+', 1},{'-', 1},{'*', 2},{'/', 2} };
		for (auto& e : arr)
		{
			if (e._op == ch)
			{
				return e._pd;
			}
		}
		assert(false);
		return -1;
	}
	void toRPN(const string& s, size_t& i, vector<string>& v)
	{
		stack<char> st;
		while (i < s.size())
		{
			if (isdigit(s[i]))
			{
				// 运算数输出
				string num;
				while (i < s.size() && isdigit(s[i]))
				{
					num += s[i];
					++i;
				}
				v.push_back(num);
			}
			else
			{
				if (s[i] == '(')
				{
					// 递归⽅式处理括号中的⼦表达式
					++i;
					toRPN(s, i, v);
				}
				else if (s[i] == ')')
				{
					++i;
					// 栈中的运算符全部输出
					while (!st.empty())
					{
						v.push_back(string(1, st.top()));
						st.pop();
					}
					// 结束递归
					return;
				}
				else
				{
					// 运算符
					// 1、如果栈为空或者栈不为空且当前运算符⽐栈顶运算符优先级⾼,则当前运算符⼊栈
						// 2、如果栈不为为空且⽐栈顶运算符优先级低或相等,说明栈顶的运算符可以运算了,
						// 输出栈顶运算符,当前运算符继续⾛前⾯遇到运算符的逻辑
					if (st.empty() || operatorPrecedence(s[i]) >
						operatorPrecedence(st.top()))
					{
						st.push(s[i]);
						++i;
					}
					else
					{
						v.push_back(string(1, st.top()));
						st.pop();
					}
				}
			}
		}
		// 栈中的运算符全部输出
		while (!st.empty())
		{
			v.push_back(string(1, st.top()));
			st.pop();
		}
	}
	int evalRPN(const vector<string>& tokens) {
		stack<int> s;
		for (size_t i = 0; i < tokens.size(); ++i)
		{
			const string& str = tokens[i];
			// str为数字
			if (!("+" == str || "-" == str || "*" == str || "/" == str))
			{
				s.push(stoi(str));
			}
			else
			{
				// str为操作符
				int right = s.top();
				s.pop();
				int left = s.top();
				s.pop();
				switch (str[0])
				{
				case '+':
					s.push(left + right);
					break;
				case '-':
					s.push(left - right);
					break;
				case '*':
					s.push(left * right);
					break;
				case '/':
					s.push(left / right);
					break;
				}
			}
		}
		return s.top();
	}
	int calculate(string s)
	{
		// 1、去除所有空格,否则下⾯的⼀些逻辑没办法处理
		std::string news;
		news.reserve(s.size());
		for (auto ch : s)
		{
			if (ch != ' ')
				news += ch;
		}
		s.swap(news);
		news.clear();
		// 2、将所有的负数-x转换为0-x
		for (size_t i = 0; i < s.size(); ++i)
		{
			if (s[i] == '-' && (i == 0 || (!isdigit(s[i - 1]) && s[i - 1] !=
				')')))
				news += "0-";
			else
				news += s[i];
		}
		// 中缀表达式转成后缀表达式
		size_t i = 0;
		vector<string> v;
		toRPN(news, i, v);

		// 后缀表达式进⾏运算
		return evalRPN(v);
	}
};

【课堂演示】

不能直接给原始的中缀表达式s,而是需要给处理过后的中缀表达式news。

原始的中缀表达式s转换成处理过后的中缀表达式news,需要经过两个处理:

  • OJ中给的中缀表达式是字符串,字符串中包含空格,需要去掉空格。
  • 其次就是负数和减号,要进行区分,将所有的负数-x转换为0-x。

去除空格的方式:

  1. find(" "),然后erase;
  2. 用原始的中缀表达式s,构建一个新的中缀表达式news,跳过所有的空格;

显然方法1的效率太低了,这里使用方法2。


负号的处理:

  • 减号:前面是操作数;
  • 负号:前面是操作符,需要转换成"0-"

这里if条件判断的情况不容易想到,对于不报测试用例的OJ题,就很难做出来。

负号前面是右括号,那实质上是一个操作数------括号的子表达式当作一个操作数看待,那这个符号实际上是减号。


【扩展】

上面的解法写了一百多行代码,其实还有更简洁的代码逻辑。

这个题解是针对于:只有+、-、()这几个操作符,没有乘除,所以能写得这么简单。


那所有的加减运算都是平级,不会有优先级的问题,需要做的就是去除括号。

  • 加号后面的括号:直接去除括号;
  • 减号后面的括号:将括号内部的所有符号变号,然后去除括号;

如果还有乘除号,也可以采用去除括号的思路,加号后面的括号直接去掉,减号后面的括号需要先把括号内的加减号变号,再去掉括号。

嵌套的括号就需要一些标记来记录变号的情况。


去除完括号,得到new_s表达式。

有乘除号的表达式还是有优先级的问题,不转后缀表达式的话,可以先遍历一遍表达式,先把乘除法运算了,得到new_new_s表达式。

最后只剩加减,再依次计算就可以了。


这个题就是没有括号,但是有加减乘除。


这就是不用后缀表达式的计算器的另一种实现方式:先去除括号(变号),再优先计算乘除。


实现后缀表达式的计算器,意义在于这是很多数据结构教科书里面的标准解法,同时练习了对于栈这个数据结构的使用。

相关推荐
DeepModel2 小时前
通俗易懂讲透 Mini-Batch K-means
开发语言·人工智能·机器学习·kmeans·batch
happy_baymax2 小时前
基于正弦波直接移相的PSFB控制方法
开发语言
傻啦嘿哟2 小时前
如何用 Python 拆分 Word 文件:高效分割大型文档的完整指南
开发语言·c#
高斯林.神犇2 小时前
五、注解方式管理bean
java·开发语言
hoiii1872 小时前
C# 读取 CSV/Excel 文件数据至 DataGridView
开发语言·c#·excel
xiaotao1312 小时前
01-编程基础与数学基石: 常用内置库
开发语言·人工智能·python
SilentSamsara2 小时前
Linux 管道与重定向:命令行精髓的结构性解析
linux·运维·服务器·c++·云原生
一只大袋鼠3 小时前
MySQL 进阶:聚集函数、分组、约束、多表查询
开发语言·数据库·mysql
lclin_202010 小时前
VS2010兼容|C++系统全能监控工具(彩色界面+日志带单位+完整版)
c++·windows·系统监控·vs2010·编程实战