大家好,我是被白菜拱的猪。
一个热爱学习废寝忘食头悬梁锥刺股,痴迷于girl的潇洒从容淡然coding handsome boy。
文章目录
一、写在前言
紧接上一篇文章,下面介绍栈的另一种场景,了解我们手中的计算器是如何进行运算的。
二、栈
(一)前缀表达式
1、前缀表达式求值
下面仅仅简单的叙述的一些前缀表达式,重要的是后缀表达式。
前缀表达式(波兰表达式)
- 前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
- 举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6
前缀表达式的计算机求值
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
- 从右至左扫描,将6、5、4、3压入堆栈
- 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
- 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
(二)逆波兰表达式计算
1、运算思路
相较于中缀表达式,后缀表达式计算相较于简单,只借用一个栈,遇到数字则入栈,遇到符号,则弹出两个数进行运算,然后将运算结果压入栈中。
例如:(3+4)x5-6对应的后缀表达式为3 4 + 5 x 6 -
- 从左到右扫描,将3,4压入栈中
- 遇到+,则弹出4,3,计算3+4=7,得到7,再将7入栈,5入栈
- 遇到x,则弹出5,7,计算5x7=35,将35压入栈中,6入栈
- 遇到 - ,弹出35,6,先入栈的是被减数,所以35-6,而不是6-35,最后求得结果29.
2、代码实现
package com.codingboy.stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* @author: ljl
* @date: 2020/8/8 10:46
* @description: 逆波兰表达式求值
*/
public class PolandNotation {
public static void main(String[] args) {
//先定义一个逆波兰表达式
//(3+4)X5-6 => 3 4 + 5 x 6 -
//为了符号,逆波兰表达式数字与符号用空格隔开
String suffixExpression = "3 4 + 5 * 6 - ";
List<String> rpnList = reverseList(suffixExpression);
System.out.println(suffixExpression + "=" + calculateList(rpnList) );
}
//将字符串表达式转化为list
public static List<String> reverseList(String suffixExpression) {
List<String> list = new ArrayList<>();
String[] elements = suffixExpression.split(" ");
for(String element : elements) {
list.add(element);
}
return list;
}
//这里计算逆波兰表达式的值
public static int calculateList(List<String> list) {
Stack<String> stack = new Stack<>();
int num1 = 0; //先弹出的数字
int num2 = 0; //后弹出的数字
int res = 0; //运算结果
for(String item : list) {
if(item.matches("\\d+")) { //使用正则表达式匹配多位数
stack.push(item);
}else { //符号
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = cal(num1, num2, item);
//结果在压入栈中
stack.push(""+res);
}
}
//遍历结束,栈中的唯一的值就是结果
return Integer.parseInt(stack.pop());
}
//进行运算,num1代表先出栈的元素,num2代表后出栈的元素,oper是操作符
public static int cal(int num1, int num2, String oper) {
int res = 0;
switch (oper) {
case "+":
res = num1 + num2;
break;
case "-":
res = num2 - num1; //注意这里后出栈的是被除数
break;
case "*":
res = num2 * num1;
break;
case "/":
res = num2 / num1;
break;
}
return res;
}
}
(三)中缀表达式转为后缀表达式
我们看到使用后缀表达式计算对于计算机来说非常的容易,但是我们人却很难直接写出后缀表达式,尤其是表达式很长的情况下,所以我们需要将中缀表达式转化为后缀表达式。
1、思路分析
1)初始化两个栈:运算符栈s1和储存中间结果的栈s2
2)从左到右扫描中缀表达式
3)遇到操作数,直接压入s2
4)遇到操作符
- 如果s1为空,或栈顶为“(” 直接入栈,这样就避免了考虑括号的优先级问题。
- 如果不为空,比较栈顶操作符优先级,括号不当做操作符
若优先级比栈顶元素高,则入栈
若优先级小于或等于栈顶元素,则将栈顶元素出栈入s2,然后在与栈顶元素优先级比较,直到改元素能够成功入栈
5)遇到括号
- 如果是左括号,则直接入栈s1
- 如果是右括号,则依次弹出s1栈顶运算符,并压入s2,直到遇到左括号位置,右括号与左括号抵消。
6)重复2-5,直到扫描结束
7)将s1剩余运算符依次弹出并压入s2
8)依次弹出s2,最后结果的逆序为中缀表达式对应的后缀表达式
2、思路总结
思路看着很复杂,其实非常的清晰,脑子搭建好框架,首先扫描,看扫描的东西是什么分为三种情况,操作符,数字,括号,然后一一判断对应的情况该怎么处理,数字直接入栈,操作符看栈内是否为空,是否是左括号,是的话直接入栈,不是就比较优先级,优先级大的怎么办,小的怎么办。(这里是大的直接入栈,与之前中缀表达式不同,中缀表达式是大于等于就可以入了,这里大的才能入,就是说打你,完完全全的打得过你才去打)最后扫描结束,在进行相应的操作。这一套下来入行云流水一般,非常的流畅。
在学习的过程中,一定要把知识的框架体系搭建起来,然后在去填东西。
3、代码示范
package com.codingboy.stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* @author: ljl
* @date: 2020/8/8 10:46
* @description: 逆波兰表达式求值
*/
public class PolandNotation {
public static void main(String[] args) {
//先定义一个逆波兰表达式
//(3+4)X5-6 => 3 4 + 5 x 6 -
//为了符号,逆波兰表达式数字与符号用空格隔开
// String suffixExpression = "3 4 + 5 * 6 - ";
// List<String> rpnList = reverseList(suffixExpression);
// System.out.println(suffixExpression + "=" + calculateList(rpnList) );
//完成将一个中缀表达式转为后缀表达式
//1. 1+((2+3)*4)-5 => 1 2 3 + 4 * + 5 –
//2. 因为对str直接操作不方便,所以将1+((2+3)×4)-5转化为list
//所以分为三大步,前缀转集合,集合转后缀,后缀进行运算
//1.前缀转为集合
String expression = "1+((2+3)*4)-5";
System.out.println("转化前:" + expression);
List<String> afterToInfixList = toInfixList(expression);
System.out.println("转化为集合后:" + afterToInfixList);
//2.集合转化为后缀
List<String> afterToSubfixList = toSubfixList(afterToInfixList);
System.out.println("转换为后缀后:" + afterToSubfixList);
//3.计算后缀表达式
int result = calculateList(afterToSubfixList);
System.out.println("计算结果为:" +result);
}
//1.这与之前转的不一样,之前是有空格,直接split分一下就好
public static List<String> toInfixList(String expression) {
List<String> list = new ArrayList<>();
char[] chars = expression.toCharArray();
//判断是数字还是符号
String str = "";
for (int i = 0; i < chars.length; i++) {
if(chars[i] < 48 || chars[i] > 57) { //是符号,直接加进去
list.add("" + chars[i]);
}else {
str = "";
while(i < chars.length && chars[i] >= 48 && chars[i] <= 57) {
str +=chars[i];
i++;
}
i--;
list.add(str);
}
}
return list;
}
//2.infixList转化为后缀
public static List<String> toSubfixList(List<String> infixList) {
//这里相当于s1
Stack<String> stack = new Stack<>();
//这里相当于s2栈,但是s2一直push,top一直++,没有出栈的操作,所以这里用list代替,方便我们遍历
List<String> list = new ArrayList<>();
//遍历集合
for(String item : infixList) {
//判断是数字,操作符,还是括号
if(item.matches("\\d+")) { //使用正则表达式判断是否为多位数,多位数直接入栈
list.add(item);
}else if(item.equals("(")) { //右括号直接入栈
stack.push(item);
}else if(item.equals(")")) { //左括号弹出来直到碰见右括号
while(!stack.peek().equals("(")) {
list.add(stack.pop());
}
//最后把左括号pop出来
stack.pop();
}else { //操作符假如栈为空或者为右括号则直接进入
if (!stack.isEmpty() && !stack.peek().equals("(")) { //既不空也不是左括号比较优先级,同时也要考虑崩出来之后栈顶是左括号的情况
while (!stack.isEmpty() && priority(item) <= priority(stack.peek()) && !stack.peek().equals("(")) {
list.add(stack.pop());
}
}
//最后就想把他压进去
stack.push(item);
}
}
//遍历结束之后,把stack中的全部添加到list中
while(!stack.isEmpty()) {
list.add(stack.pop());
}
return list;
}
//定义优先级
public static int priority(String oper) {
//这里除了四则运算,还带有括号,高级一些
if (oper.equals("*")|| oper.equals("/")) {
return 2;
} else if (oper.equals("+")|| oper.equals("-")) {
return 1;
} else {
return -1;
}
}
//将前缀字符串表达式转化为list
public static List<String> reverseList(String suffixExpression) {
List<String> list = new ArrayList<>();
String[] elements = suffixExpression.split(" ");
for(String element : elements) {
list.add(element);
}
return list;
}
//这里计算逆波兰表达式的值
public static int calculateList(List<String> list) {
Stack<String> stack = new Stack<>();
int num1 = 0; //先弹出的数字
int num2 = 0; //后弹出的数字
int res = 0; //运算结果
for(String item : list) {
if(item.matches("\\d+")) { //使用正则表达式匹配多位数
stack.push(item);
}else { //符号
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = cal(num1, num2, item);
//结果在压入栈中
stack.push(""+res);
}
}
//遍历结束,栈中的唯一的值就是结果
return Integer.parseInt(stack.pop());
}
//进行运算,num1代表先出栈的元素,num2代表后出栈的元素,oper是操作符
public static int cal(int num1, int num2, String oper) {
int res = 0;
switch (oper) {
case "+":
res = num1 + num2;
break;
case "-":
res = num2 - num1; //注意这里后出栈的是被除数
break;
case "*":
res = num2 * num1;
break;
case "/":
res = num2 / num1;
break;
}
return res;
}
}
4、运行结果及总结
我们求算术表达式一般经历三个步骤,先将中缀表达式转化为集合,然后将集合转化为后缀表达式,在求后缀表达式的值。
三、结束语
不知不觉又敲了一个下午,算法重要的是记住思路,但是了解思路之后,又面对了一个巨大的问题,就是如何将其转化为代码呢?在刚开始我们可能无从下手,这时候我推荐去参考别人的代码,看看他们是如何转化的,最后你会发现其实敲代码就是按照思路一步步来的。然后自己实践一遍,敲多了也就知道怎么敲了,保不住自己还会举一反三。
另外在敲的过程中,你会发现要考虑很多东西,比如中缀转化为后缀的时候,遍历到操作符是要比较优先级,假如优先级大的蹦跶出来之后,剩下的还是比你大呢,是不是要继续出栈,而且出栈的时候还要考虑栈是否为空,这就要有个判空,另外假如是左括号呢?前面是直接判断是不是左括号,但是优先级大的出来之后就保不住不是左括号了,所以这里又要有一个是不是左括号的条件,所以不敲不知道,一敲吓一跳,看着比人敲的很轻松不会出错,即便自己一个一个照着敲,冷不丁的兴许哪里就会出错。这就是差距,多年敲代码积累的经验,所以加油吧,都是一步一个脚印来的。
自己又想了一下,在只有加减乘除的场景下,其实没有必要在考虑蹦跶出来一个之后在考虑下一个优先级大的情况,因为在添加的时候就保证了外面大于里面的才进去,而且乘除不可能挨在一块,因为他们优先级一样,里面的优先级肯定是一个越来越大,但是判空还是需要的,你写了也不能算法,因为你这是你经过考虑后,你写了从一方面说明你思考过,虽然不需要,但值得鼓励。
另外在相似的算法中找不同也非常重要,在昨天写的求中缀表达式值的时候,我说了实力相当或者大于的才进去打架,而这里中缀变为后缀,是直接大于,明摆的欺软怕硬,才进去打。就这么简单的一个比较优先级用的等号,有没有都会出错。
算法真的很考验一个人的思维,考虑的是否全面,没有事情是一蹴而就的,遇到错误不可怕,出错了改不就完事了嘛。