算法题 逆波兰表达式/计算器

题一:逆波兰表达式(后缀表达式)

什么是逆波兰表达式?

逆波兰表达式(也称为后缀表达式)的核心逻辑是将运算符放在其对应的所有操作数之后从而消除表达式中的括号和运算符优先级歧义,让计算机能通过栈结构高效计算。

逆波兰表达式必须严格遵循"操作数在前、运算符紧跟对应操作数"的顺序 ,否则会导致计算逻辑混乱。数字的顺序与原始中缀表达式中操作数的自然出现顺序保持一致,无需调整数字的先后顺序,只需将运算符按"作用于对应操作数之后"的规则后置即可。

代码实现:

思路:

逆波兰表达式的特点是"运算符位于其对应操作数之后",例如表达式 ["3","4","+"] 对应中缀式 3+4 。代码的核心是:

  1. 遍历表达式的每个元素( tokens 中的字符串);

  2. 遇到数字时,将其转换为整数并压入栈中;

  3. 遇到运算符时,从栈中弹出两个操作数(先弹出的是"右操作数",后弹出的是"左操作数") ,用运算符计算两者的结果,再将结果压回栈中;

  4. 遍历结束后,栈中仅剩的一个元素就是表达式的最终结果。

举个例子(以表达式 ["2","1","+","3","*"] 为例,对应中缀式 (2+1)*3 ):

  1. 遍历到 "2" :压栈 → 栈: [2] ;

  2. 遍历到 "1" :压栈 → 栈: [2,1] ;

  3. 遍历到 "+" :弹出 1 ( right )和 2 ( left ),计算 2+1=3 ,压栈 → 栈: [3] ;

  4. 遍历到 "3" :压栈 → 栈: [3,3] ;

  5. 遍历到 "*" :弹出 3 ( right )和 3 ( left ),计算 3*3=9 ,压栈 → 栈: [9] ;

  6. 返回 st.top() → 结果为 9 。

借助栈的方式,完美契合了逆波兰表达式"先存操作数、后算运算符"的特性

注意点:

1. 函数形参用 vector<string> 而非 string 的简化道理

逆波兰表达式的输入是由多个"原子单元"组成的(每个单元是数字或运算符,如 ["3","+","4"] )。若直接用 string 作为形参,需要手动分割字符串(比如按空格或字符边界拆分出每个单元),这会增加代码复杂度。而 vector<string> 已经将表达式预拆分为一个个独立的"token"(标记),直接遍历每个 string 即可,省去了手动分割字符串的步骤,让核心的"栈计算逻辑"更简洁。
2. if 语句中 == 用 "" (双引号)的原因

本题中 str 被定义为 string 类型(从代码里的判断逻辑 str == "+" 等能看出是字符串比较),而单引号 '+' 表示的是字符( char 类型),字符串( string )和字符( char )是不同的类型,无法直接用 == 进行比较。

  • 字符串( string ):用双引号包裹,string本质是字符数组( char[] ) ,存储时包含字符和结束符 \0 ,例如 "+" 实际存储的是 '+' 和 \0 。
  • 字符( char ):用单引号包裹,是单个字符,例如 '+' 就是一个字符常量。

如果强行用单引号(比如写成 str == '+' ),会因为类型不匹配导致编译错误(编译器会认为你在拿 string 和 char 做比较,这两种类型没有默认的 == 重载)。

3. switch 语句用 str[0] 的原因

str 是表示运算符的 string (如 "+" 、 "-" ),而 switch 语句的条件要求是整型或字符型(C++ 中 switch 不支持 string 类型)。 str[0] 可以获取字符串的第一个字符(如 "+" 的 str[0] 是 '+' , "-" 的 str[0] 是 '-' ),这是一个 char 类型,符合 switch 的语法要求,因此用 str[0] 作为分支判断的条件。

4. 最后用 stoi 的原因

tokens 中的元素是 string 类型(如 "5" 、 "12" ),而栈 st 存储的是 int 类型的操作数。 stoi (string to integer)函数的作用是将字符串形式的数字转换为整数(比如把 "5" 转成 5 ,把 "12" 转成 12 ),这样才能将字符串类型的数字存入整数栈中,供后续的运算使用。


中缀转后缀

为什么要实现中缀转后缀呢?

中缀表达式(如 a+b*c )的运算符位于操作数之间,人类容易理解,但计算机直接解析时,需处理复杂的优先级(如 * 优先于 + )和括号嵌套,极易出错。而逆波兰表达式(如 a b c * + )将运算符放在操作数之后,结构天然适合"遇数入栈、遇运算符出栈计算"的逻辑,因此需要先完成这种转换。

那么中缀表达式怎么转为后缀表达式呢?

cpp 复制代码
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<assert.h>
using namespace std;

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<int> 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] == ')')
			{
				//子表达式结束
				//输出栈里面剩余的运算符
				while (!st.empty())
				{
					v.push_back(string(1, st.top()));
					st.pop();
				}
				++i;
				return;
			}
			else
			{
				//运算符
				if (st.empty() || operatorPrecedence(s[i]) > operatorPrecedence(st.top()))
				{
					st.push(s[i]);
					++i;
				}
				else
				{
					char op = st.top();
					st.pop();

					v.push_back(string(1,op));
				}
			}
		}
		//表达式结束
        //输出栈里面剩余的运算符
		while (!st.empty())
		{
			v.push_back(string(1, st.top()));
			st.pop();
		}
	}
};


int main()
{
	size_t i = 0;
	vector<string> v;
	string str1 = "1+2-3";
	Solution().toRPN(str1, i, v);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	string str2 = "1+2-(3*4+5)-7";
	i = 0;      //重置
	v.clear();   //清空
	Solution().toRPN(str2, i, v);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

思路:

代码通过 toRPN 函数实现转换,接收中缀表达式字符串 s 、当前遍历位置 i (引用传递,确保递归和连续遍历)、结果容器 v (存储后缀表达式的"token",即数字或运算符字符串)。整个转换过程可分为以下关键步骤:

(1)处理数字操作数

当遍历到数字字符时,会连续拼接所有连续的数字字符,形成完整的数字字符串(例如将 '1' 、 '2' 拼接成 "12" ),然后将这个数字字符串直接加入结果容器 v 。这一步确保每个数字作为独立的"token"被正确识别。

(2)处理左括号 ' ( ' :递归深入子表达式

遇到左括号时,说明进入了一个子表达式(如 (3*4+5) )。此时:

  • 先将遍历索引 i 后移(跳过左括号);

  • 递归调用 toRPN 函数,处理子表达式内部的内容;

  • 子表达式处理完成后,右括号会触发递归返回,继续处理外层表达式。

(3)处理右括号 ' ) ' :弹出栈中所有运算符

遇到右括号时,说明子表达式结束,需要将栈中所有未弹出的运算符输出到结果 v 中(这些运算符属于子表达式内部的优先级处理)。例如,子表达式 3*4+5 转换后,栈中可能残留 + ,右括号会触发这些运算符的弹出,然后后移索引 i 并结束当前子表达式的递归处理。

(4)处理运算符(+、-、*、/):基于优先级的栈操作

遇到运算符时,需根据运算符优先级决定是否将栈中已有运算符弹出到结果中:

  • 若栈为空,或当前运算符优先级高于栈顶运算符的优先级(例如 * 优先级高于 + ):将当前运算符压入栈,等待后续处理;

  • 若当前运算符优先级不高于栈顶运算符的优先级(例如 + 优先级不高于 * ):弹出栈顶运算符,加入结果 v ,重复此判断直到满足压栈条件,再将当前运算符压入栈。

这样可以保证运算符按正确的优先级顺序输出到结果中(如 a+b*c 转换为 a b c * + )。
(5)表达式遍历结束:清理栈中剩余运算符

当遍历完整个中缀表达式字符串 s 后,栈中可能仍有未弹出的运算符(如表达式末尾的运算符),需要将它们全部弹出并加入结果 v ,确保所有运算符都被正确转换到后缀表达式中。

(6)运算符优先级的辅助判断

operatorPrecedence 函数用于返回运算符的优先级(乘除 * 、 / 优先级为2,加减 + 、 - 优先级为1),是判断"何时压栈、何时弹栈"的核心依据。通过自定义结构体数组存储"运算符-优先级"的映射关系,实现高效的优先级查找。

最终效果:

在 main 函数中,定义中缀表达式(如 1+2-(3*4+5)-7 ),调用 toRPN 完成转换后,结果容器 v 中会存储对应的后缀表达式token(如 1 2 + 3 4 * 5 + - 7 - ),后续可通过栈结构(如 evalRPN 函数)快速计算出表达式的值。

题二: 基本计算器

下面再来看一道更有难度的OJ题:

从题目要求来看,输入的表达式 s 确实只包含加减、括号和空格,没有乘除运算。但是小编在代码中对乘除( * 、 / )的逻辑进行了额外实现,但这并不影响代码的正确性 , 属于对计算器功能的扩展

代码实现:

cpp 复制代码
class Solution {
public:
 int evalRPN(vector<string>& tokens)
 {
     stack<int> st;
     for (auto& str : tokens)
     {
         if (str == "+" || str == "-" || str == "*" || str == "/")
         {
             //运算符 进行运算 并将运算结果入栈
             int right = st.top();
             st.pop();
             int left = st.top();
             st.pop();

             switch (str[0])
             {
             case '+':
                 st.push(left + right);
                 break;
             case '-':
                 st.push(left - right);
                 break;
             case '*':
                 st.push(left * right);
                 break;
             case '/':
                 st.push(left / right);
                 break;
             }
         }
         else
         {
             st.push(stoi(str));
         }
     }
     return st.top();
 }
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<int> 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] == ')')
		{
			//子表达式结束
			//输出栈里面剩余的运算符
			while (!st.empty())
			{
				v.push_back(string(1, st.top()));
    			st.pop();
			}
			++i;
			return;
		}
		else
		{
			//运算符
			if (st.empty() || operatorPrecedence(s[i]) > operatorPrecedence(st.top()))
			{
				st.push(s[i]);
				++i;
			}
			else
			{
				char op = st.top();
				st.pop();

				v.push_back(string(1,op));
			}
		}
	}
	//表达式结束
     //输出栈里面剩余的运算符
	while (!st.empty())
	{
		v.push_back(string(1, st.top()));
		st.pop();
	}
}
    int calculate(string s) 
    {
        string news;
	    for (auto& ch : s)
	    {
		    if (ch != ' ')
		    {
			    news += ch;
		    }
	    }
	    news.swap(s);
	    news.clear();

    	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];
		    }
	    }
	    cout << news << endl;

        size_t i = 0;
        vector<string> v;
        toRPN(news, i, v);
        return evalRPN(v);

    }
};

思路:

解答这道题就要结合上面的题一和中缀转后缀的代码:

  • 步骤1:输入预处理:过滤空格,并处理负号(如 -1 补全为 0-1 ,避免作为一元运算符的歧义)。
  • 步骤2:中缀转后缀( toRPN 函数):利用栈管理运算符优先级(本题中加减优先级相同),处理括号嵌套,将中缀表达式转换为后缀表达式的 vector<string> 。
  • 步骤3:后缀表达式求值( evalRPN 函数):用栈存储操作数,遇到运算符时弹出栈顶两个数计算,结果再入栈,最终栈顶即为表达式结果。

evalRPN函数 :后缀表达式求值

  • 遍历后缀表达式的每个 token :

  • 若为数字,转整数并入栈;

  • 若为运算符( + 、 - ),弹出栈顶两个数,按运算符计算后将结果入栈。
    operatorPrecedence函数 :运算符优先级(本题简化为加减优先级相同)

由于题目仅涉及 + 、 - ,返回固定优先级(如都为 1 ),确保运算符处理逻辑统一。
toRPN函数 :中缀转后缀

  • 数字处理:连续拼接数字字符,形成完整数字字符串并入结果。

  • 括号处理:遇到 ( 则递归处理子表达式;遇到 ) 则弹出栈中所有运算符直到遇到 ( ,并丢弃 ( 。

  • 运算符处理:由于加减优先级相同,遇到运算符时弹出栈中所有已有运算符,再将当前运算符入栈;遍历结束后弹出栈中剩余运算符。
    calculate函数 :总入口

  • 预处理输入字符串(++过滤空格、补全负号++),调用 toRPN 转后缀,再调用 evalRPN 计算结果并返回。

注意点:

过滤空格

  • 作用:遍历原始字符串 s ,将所有非空格字符拼接至 news ,实现过滤空格的效果(比如输入 "1 + 1" 会被处理为 "1+1" )。
  • news.swap(s) :交换 news 和 s 的内容,此时 s 变为过滤空格后的字符串, news 变为空(为后续补全负号腾出空间)。

补全负号

  • 作用:处理"一元负号"(如 -1 、 -(2+3) ),将其补全为"二元减法形式"(如 0-1 、 0-(2+3) ),避免表达式解析时的歧义。
  • 条件判断 s[i] == '-' && (i == 0 || (!isdigit(s[i - 1]) && s[i - 1] != ')')) :
  • s[i] == '-' :当前字符是负号;
  • i == 0 :负号在字符串开头(如 -123 );
  • !isdigit(s[i - 1]) && s[i - 1] != ')' :负号前不是数字且不是右括号(如 +( -2) 中的负号)。
  • 满足以上条件时,说明是一元负号,需要补全为 0- (将一元运算转为二元减法)。

经过这段预处理后,字符串会被转换为"无空格 + 负号补全"的规范形式,例如:

  • 输入: " -1 + (2-3) "
  • 处理后: "0-1+(2-3)" (过滤空格 + 开头负号补全为 0- )。

为何需要这步处理?在计算器表达式解析中:

  • 空格会干扰字符遍历,必须过滤;
  • 负号既可以是"二元运算符"(如 1-2 ),也可以是"一元运算符"(如 -3 ),补全为 0- 后,能统一按"二元减法"逻辑解析,简化后续中缀转后缀的复杂度。

基本计算器的完整实现

完整实现:

cpp 复制代码
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<assert.h>
using namespace std;

class Solution
{
public:
	//用后缀计算答案
	int evalRPN(vector<string>& tokens)
	{
		stack<int> st;
		for (auto& str : tokens)
		{
			if (str == "+" || str == "-" || str == "*" || str == "/")
			{
				//运算符 进行运算 并将运算结果入栈
				int right = st.top();
				st.pop();
				int left = st.top();
				st.pop();

				switch (str[0])
				{
				case '+':
					st.push(left + right);
					break;
				case '-':
					st.push(left - right);
					break;
				case '*':
					st.push(left * right);
					break;
				case '/':
					st.push(left / right);
					break;
				}
			}
			else
			{
				st.push(stoi(str));
			}
		}
		return st.top();
	}
	//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<int> 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] == ')')
			{
				//子表达式结束
				//输出栈里面剩余的运算符
				while (!st.empty())
				{
					v.push_back(string(1, st.top()));
					st.pop();
				}
				++i;
				return;
			}
			else
			{
				//运算符
				if (st.empty() || operatorPrecedence(s[i]) > operatorPrecedence(st.top()))
				{
					st.push(s[i]);
					++i;
				}
				else
				{
					char op = st.top();
					st.pop();

					v.push_back(string(1, op));
				}
			}
		}
		//表达式结束
		//输出栈里面剩余的运算符
		while (!st.empty())
		{
			v.push_back(string(1, st.top()));
			st.pop();
		}
	}
};


int main()
{

	Solution sol; // 实例化Solution对象,复用成员函数

	string str1 = "1+2-3";
	size_t i = 0;
	vector<string> tokens1;
	sol.toRPN(str1, i, tokens1); // 中缀转后缀
	int result1 = sol.evalRPN(tokens1); // 计算后缀表达式的值
	cout << "中缀表达式 \"" << str1 << "\" 的后缀形式:";
	for (auto& e : tokens1) cout << e << " ";
	cout << ",计算结果:" << result1 << endl;


	string str2 = "1+2-(3*4+5)-7";
	i = 0; // 重置索引i
	vector<string> tokens2;
	sol.toRPN(str2, i, tokens2); // 中缀转后缀
	int result2 = sol.evalRPN(tokens2); // 计算后缀表达式的值
	cout << "中缀表达式 \"" << str2 << "\" 的后缀形式:";
	for (auto& e : tokens2) cout << e << " ";
	cout << ",计算结果:" << result2 << endl;

	return 0;
}

实现结果:

总结:

本文介绍了逆波兰表达式(后缀表达式)及其计算实现方法。逆波兰表达式将运算符置于操作数之后,通过栈结构实现高效计算。主要内容包括:1)逆波兰表达式概念及计算原理,通过栈结构处理数字和运算符;2)中缀表达式转后缀表达式的方法,使用栈管理运算符优先级和括号嵌套;3)计算器实现方案,结合预处理、中缀转后缀和后缀表达式求值三个步骤完成复杂表达式计算。文章通过具体代码示例展示了从表达式转换到最终计算的完整流程,并详细解释了各步骤的实现逻辑和注意事项。

相关推荐
ZhiqianXia2 小时前
C++ 常见代码异味(Code Smells)
c++
编码追梦人3 小时前
基于 STM32 的智能语音唤醒与关键词识别系统设计 —— 从硬件集成到算法实现
stm32·算法·struts
循着风5 小时前
二叉树的多种遍历方式
数据结构·算法
koo3648 小时前
李宏毅机器学习笔记31
1024程序员节
开发者小天8 小时前
调整为 dart-sass 支持的语法,将深度选择器/deep/调整为::v-deep
开发语言·前端·javascript·vue.js·uni-app·sass·1024程序员节
fruge8 小时前
前端Sass完全指南:从入门到精通
1024程序员节
小超爱编程9 小时前
MyBatis 动态 SQL
1024程序员节
老猿讲编程9 小时前
C++中的奇异递归模板模式CRTP
开发语言·c++
Leinwin10 小时前
借助智能 GitHub Copilot 副驾驶 的 Agent Mode 升级 Java 项目
1024程序员节