逆波兰算法
简单问题引入
一个只涉及个位的四则运算表达式,例如:
7 * 2 * 2 - 5 * 1 - 5 + 3 * 4(忽略空格)
现在要求设计一个简单算法,完成计算(肯定不是让你直接输入表达式计算)
先说一下大致思路:
利用两个栈,一个放数字,记为nums—数字栈;一个放计算符号,记为symbol—符号栈
- 扫描表达式,如果是数字则直接入数字栈nums
- 如果是运算符,分以下情况:
1) 如果符号栈为空或者,当前符号优先级>当前栈顶符号优先级:直接push
2)如果当前符号优先级<=当前栈顶符号优先级: 取出nums栈顶两位元素和符号栈栈顶元素进行相应计算 - 最后的情况可能是符号栈内还剩余一个符号,再进行运算即可
- 这时nums内的栈顶元素即为答案
下面看代码:
public class SimpleException {
public static void main(String[] args) {
String exception = "7*2*2-5*1-5+3*4";
System.out.println(exception + " = " + solution(exception));
}
static int solution(String exception){
Stack nums = new Stack();//数字栈
Stack symbol = new Stack();//符号栈
char[] chars = exception.toCharArray();
for (char chr: chars){//扫描表达式每个字符
if (!isSymbol(chr)){//判断是不是运算符
int target = Character.getNumericValue(chr);
nums.push(target);//如果是数字,直接入数字栈
}else {//如果是运算符
if (symbol.isEmpty() || getPriority((char)symbol.peek())<getPriority(chr)){//如果运算符栈顶为空
symbol.push(chr);//直接入栈
}else if (getPriority((char)symbol.peek())>=getPriority(chr)){
/*如果当前符号栈栈顶的优先级大于等于当前扫描符号
*先弹出数字栈两个数字,再弹出符号栈栈顶元素,进行计算,结果再入数字栈,别忘记将当前符号入栈
*/
int result = calculate(nums,symbol);
nums.push(result);
//这一步是为了防止类似-5-5这样的情况,计算的时候会变成5-5.保证符号栈最多只有一个符号
if (!symbol.isEmpty()){
result = calculate(nums,symbol);
nums.push(result);
}
symbol.push(chr);
}
}
}
while(!symbol.isEmpty()){
int result = calculate(nums,symbol);
nums.push(result);
}
return (int)nums.pop();
}
private static int calculate(Stack nums, Stack symbol) {
int num1 = (int)nums.pop();
int num2 = (int)nums.pop();
char sym = (char)symbol.pop();
int result = 0;
switch (sym){
case '+':
result = num1 + num2;
break;
case '-':
result = num2 - num1;
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num2 / num1;
}
return result;
}
static boolean isSymbol(char chr){
if (chr == '+' || chr == '-' || chr == '*' ||chr == '/')
return true;
return false;
}
static int getPriority(char symbol){
if(symbol == '+' || symbol == '-')
return 1;
else return 2;
}
}
这段代码解决不了多位数计算,这里讲一下思路:
- 处理不了多位数主要因为我们是一旦发现不是运算符就直接入栈,所以出错。
- 当发现不是运算符时先不要入栈,先保存(可以用字符串保存,后面发现还是数字就拼接,然后取值用Integer的paseInt方法)
- 继续向后扫描,当发现是运算符时就取出数字然后压入nums
再想想如果假如括号呢?情况就会更复杂一些。
下面介绍一种算法可以比较简单地处理:逆波兰算法
在此之前需要知晓前置知识:
什么是前、中、后缀表达式?
中缀表达式
我们正常使用的表达式,如上面的7 * 2 * 2 - 5 * 1 - 5 + 3 * 4
前缀表达式
又称为波兰表达式,运算符都位于操作数之前,例如(3 + 4) * 5 - 6 的前缀表达式就是 - * + 3 4 5 6
计算机在进行计算式,从右向左扫描,遇到数字就push进数字栈,遇到运算符就进入运算流程
后缀表达式
又称为逆波兰表达式,还是以(3 + 4) * 5 - 6 为例,它的后缀表达式是3 4 + 5 * 6 -
这里多举几个例子:
中缀表达式 | 后缀表达式 |
---|---|
a + b | a b + |
a + ( b - c) | a b c - + |
a + ( b - c) * d | a b c - d * + |
a + d * ( b - c) | a d b c - * + |
a = 1 + 3 | a 1 3 + = |
转换的时候思考计算机要怎么计算(前面的例子)就会好理解很多
计算方式和前置表达式类似,只是从左往右扫描。
看一下具体的计算过程:
public class InversePolish {
public static void main(String[] args) {
//这里偷个懒,用空格分割
String exception = "3 4 + 5 * 6 -";
System.out.println(solution(exception));
}
static int solution(String exception){
Stack<String > stack = new Stack();
String[] strs = exception.split(" ");
for (String str:strs) {
if (str.matches("\\d+")){//匹配多位数
stack.push(str);
}else {
int result = calculate(stack,str);
stack.push(result+"");
}
}
return Integer.parseInt(stack.pop());
}
static int calculate(Stack stack,String str) {
int num1 = Integer.parseInt((String) stack.pop());
int num2 = Integer.parseInt((String) stack.pop());
int result = 0;
switch (str){
case "+":
result = num1 + num2;
break;
case "-":
result = num2 - num1;
break;
case "*":
result = num1 * num2;
break;
case "/":
result = num2 / num1;
}
return result;
}
}
从代码来看,逆波兰的计算过程比中缀表达式要简单,麻烦的过程可能就是怎么转化一个中缀表达式为后缀表达式。
逆波兰表达式转化思路
- 初始化两个栈:中间结果栈s1和运算符号符号栈s2
- 从左往右扫描中缀表达式
- 遇到数值,压入s1
- 遇到运算符
1) 如果s2为空,或者栈顶元素是“( ”,则直接入栈
2)如果优先级比栈顶符号高,也入栈s2
3)否则,将s2的栈顶元素弹出并压入s2中,再次重复步骤4 - 如果遇到括号
1)如果是“( ”,直接入栈s2
2)如果是“ )”,依次弹出s2中元素并压入s1,直到遇到“( ”为止,然后抛弃这对括号 - 重复步骤2-5,知道扫描完毕
- 最后只需要将s1中的结果依次弹出,并逆序保存即为后缀表达式
下面代码实现:
public class InversePolish {
public static void main(String[] args) {
//这里偷个懒,用空格分割
String exception = "( 3 + 4 ) * 5 - 6";
String postE = postFix(exception);//转化为后缀表达式
System.out.println(solution(postE));//计算后缀表达式
}
static Stack<String> s1 = new Stack<>();//存放中间结果
static Stack<String> s2 = new Stack<>();//存放运算符
static String postFix(String exception){
String[] strs = exception.split(" ");
String str = "";
for (int i=0;i<strs.length;++i) {
str = strs[i];
if (str.matches("\\d+")){//匹配多位数
s1.push(str);
}else {
if (str.equals("(") || s2.isEmpty()) {
s2.push(str);
} else if (str.equals(")")){//“)”的判断应该靠前
while (!(s2.peek()).equals("(")){
s1.push(s2.pop());
}
s2.pop();
}else if ((s2.peek()).equals("(") ||
getPriority(str)>=getPriority(s2.peek())){
s2.push(str);
}else if (getPriority(str)<getPriority(s2.peek())){
s1.push(s2.pop());
return postFix(exception.substring(i+i));//和新的栈顶元素对比,重走流程
}
}
}
while(!s2.isEmpty()){//如果最后s2还有剩余符号未添加,则依序添加到s1
s1.push(s2.pop());
}
String inverseResult = "";
while (!s1.isEmpty()){
inverseResult = inverseResult + s1.pop() + " ";
}
String result = "";
for (int i = inverseResult.length()-1; i >= 0 ; i--) {
result = result + inverseResult.substring(i,i+1);
}
return result.substring(1);//最后结果前面或有一个空格
}
static int getPriority(String symbol){
if(symbol.equals("-") || symbol.equals("+"))
return 1;
else if (symbol.equals("*") || symbol.equals("/"))
return 2;
else return 0;
}
static int solution(String exception){
Stack<String > stack = new Stack();
String[] strs = exception.split(" ");
for (String str:strs) {
if (str.matches("\\d+")){//匹配多位数
stack.push(str);
}else {
int result = calculate(stack,str);
stack.push(result+"");
}
}
return Integer.parseInt(stack.pop());
}
static int calculate(Stack stack,String str) {
int num1 = Integer.parseInt((String) stack.pop());
int num2 = Integer.parseInt((String) stack.pop());
int result = 0;
switch (str){
case "+":
result = num1 + num2;
break;
case "-":
result = num2 - num1;
break;
case "*":
result = num1 * num2;
break;
case "/":
result = num2 / num1;
}
return result;
}
}
总结
面试被问到逆波兰算法,只能讲出利用堆栈。复习一下具体流程,欢迎纠错交流。