前言
★ 这里是小冷的博客
✓ 优质技术好文见专栏
个人公众号,分享一些技术上的文章,以及遇到的坑
当前系列:数据结构系列
源代码 git 仓库 ‘
数据结构代码地址 代码Git 仓库地址
认识表达式与 逆波兰计算器实现
什么是前缀,什么中缀,什么是后缀?(理论加举例)
前缀
前缀表达式是一种没有括号的算术表达式,与中缀表达式不同的是,其将运算符写在前面,操作数写在后面。为纪念其发明者波兰数学家Jan Lukasiewicz,前缀表达式也称为“波兰式”。例如,- 1 + 2 3,它等价于1-(2+3)。
我们完成一个逆波兰计算器,要求完成如下任务:
-
输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果
-
支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
中缀
(或中缀记法)是一个通用的算术或逻辑公式表示方法, 操作符是以中缀形式处于操作数的中间(例:3 + 4),中缀表达式是人们常用的算术表示方法。
与前缀表达式(例:+ 3 4)或后缀表达式(例:3 4 +)相比,中缀表达式不容易被计算机解析,但仍被许多程序语言使用,因为它符合人们的普遍用法。
与前缀或后缀记法不同的是,中缀记法中括号是必需的。计算过程中必须用括号将操作符和对应的操作数括起来,用于指示运算的次序。
例:
(1)8+4-6*2用后缀表达式表示为:
8 4+6 2*-
(2)2*(3+5)+7/1-4用后缀表达式表示为**:**
235+*71/+4-
后缀
这种表示方式把运算符写在运算对象的后面,例如,把a+b写成ab+,所以也称为后缀式。这种表示法的优点是根据运算对象和算符的出现次序进行计算,不需要使用括号,也便于用械实现求值。对于表达式x:=(a+b)(c+d),其后缀式为xab+cd+:=。
原表达式:a*(b*(c+d/e)-f)# /* # 为表达式结束符号*/
后缀式:abcde/+f-#
为运算符定义优先级:# ( + - * / **
-1 0 1 1 2 2 3
从原表达式求后缀式的规则为:
思路分析
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
1.从左至右扫描,将 3 和 4 压入堆栈;
2.遇到+运算符,因此弹出 4 和 3(4 为栈顶元素,3 为次顶元素),计算出 3+4 的值,得 7,再将 7 入栈;
3.将 5 入栈;
4.接下来是×运算符,因此弹出 5 和 7,计算出 7×5=35,将 35 入栈;
5.将 6 入栈;
6.最后是-运算符,计算出 35-6 的值,即 29,由此得出最终结果
代码实现
public static void main(String[] args) {
//定义逆波兰表达式
// 测试
String suffixExpression = "3 4 + 5 * 6 -";
// 思路
// 1. 先将 3 4 + 5 * 6 - 方法哦 ArrayList中
// 2. 将 Arraylist 传递一个方法 遍历ArrayList 配合栈完成计算
List<String> list = getListString(suffixExpression);
System.out.println("rpnlist = " + list);
int res = calculate(list);
System.out.println("计算的结果是 = " + res);
}
/**
* @author 冷环渊 Doomwatcher
* @context: 将一个逆波兰表达式的数据和运算符 依次放入Arraylist中
* @date: 2021/12/25 18:09
* @param suffixExpression
* @return: java.util.List<java.lang.String> 将分割好的字符List 返回
*/
public static List<String> getListString(String suffixExpression) {
// 分割字符串 将后缀表达式的每一个字符取出
String[] split = suffixExpression.split(" ");
// 我们用list 来存储分割出来的表达式,这样之后我们就需要遍历即可
List<String> list = new ArrayList<>();
for (String ele : split) {
list.add(ele);
}
return list;
}
/**
* @author 冷环渊 Doomwatcher
* @context: 这个方法就是 逆波兰表达式的运算处理方法
* * 实现思路 : 我们以 (3+4)*4-6 对应的后缀表达式 => 3 4 + 5 * 6 -
* * 1) 从左至右扫描,将3和4压入栈
* * 2) 遇到+运算符号 因此弹出 4 和 3 (4为栈顶元素 3 为次顶元素) 计算出3+4的值,将计算的结果加入栈
* * 3) 将5 入栈
* * 4) 接下来将 * 是运算符 弹出 5和7 计算出 7*5 = 35 将结果入栈
* * 5) 将6 入栈
* @date: 2021/12/25 18:14
* @param ls
* @return: int 返回的是一个运算结果
*/
public static int calculate(List<String> ls) {
//创建栈 只需要一个栈即可
Stack<String> stack = new Stack<>();
// 遍历处理
for (String item : ls) {
// 这里我们用正则表达式来取出数字 和符号 如果是数字进行处理,如果符号做另外的处理
if (item.matches("\\d+")) {
//匹配的是数字
stack.push(item);
} else {
// 如果是符号就 pop出两个数 运算之后结果入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if ("+".equals(item)) {
res = num1 + num2;
} else if ("-".equals(item)) {
res = num1 - num2;
} else if ("*".equals(item)) {
res = num1 * num2;
} else if ("/".equals(item)) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
// 把 res 入栈
stack.push("" + res);
}
}
//最后留在stack 中的数据是运算结果
return Integer.parseInt(stack.pop());
}
这里我们用list加栈的方式实现了表达式的运算
需要注意的几点:
- 从中体会 后缀表达式和中缀表达式的转换
- 计算上,后缀与中缀的区别
中缀转后缀表达式
大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发 中,我们需要将 中缀表达式转成后缀表达式。 所以我们需要用代码将中缀表达式处理成后缀表达式之后运算一劳永逸
思路
1.初始化两个栈 元素安抚栈 s1 和存储中间结果的栈2
2.从左至右扫描中缀表达式
3.遇到操作数时 将其压入s2
4.遇到运算符时 比较其与s1栈顶的运算符优先级别:
1。如果s1为空 或者栈顶运算符为左括号( 则直接将此运算符压入栈
2.否则,若优先级别比栈顶运算符的高,也将运算符压入s1
3.否则 将s1栈顶的于是暖夫弹出并且压入s2栈内 再次转到 4.1与s1新的扎你当运算符比较
5.遇到括号时:
1.如果是左括号( ,则直接压入s1
2.如果是右括号),则一次弹出s1栈顶的运算符,并且压入s2,直到遇到左括号位置,此时将一对括号丢弃
6.重复步骤 2-5 直到表达式最右边
7.将s1中剩余的运算符一次弹出并压入s2
8.依次弹出s2中的元素并且输出,结果逆序就是中缀表达式对应的后缀的表达式
思路举例
将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下
因此结果为 :“1 2 3 + 4 × + 5 –”
编码
跟着思路来实现代码
代码实现
package com.hyc.DataStructure.Stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* @projectName: DataStructure
* @package: com.hyc.DataStructure.Stack
* @className: PolandExpression
* @author: 冷环渊 doomwatcher
* @description: TODO
* 实现思路 : 我们以 (3+4)*4-6 对应的后缀表达式 => 3 4 + 5 * 6 -
* 1) 从左至右扫描,将3和4压入栈
* 2) 遇到+运算符号 因此弹出 4 和 3 (4为栈顶元素 3 为次顶元素) 计算出3+4的值,将计算的结果加入栈
* 3) 将5 入栈
* 4) 接下来将 * 是运算符 弹出 5和7 计算出 7*5 = 35 将结果入栈
* 5) 将6 入栈
* 最后 是 - 运算符 计算出 35-6 = 29 由此得出最后的结果
*
* 使用技术 这里我们用 java给我们提供的 Stack 和 ArrayList组合来实现计算器
* @date: 2021/12/25 18:00
* @version: 1.0
*/
public class PolandExpression {
public static void main(String[] args) {
//定义中缀表达式 ,转成后置表达式之后运算
String expression = "1+((2+3)*4)-5";
//拆成链表
List<String> infixExpressionList = toinfixExpression(expression);
System.out.println("中缀表达式对应的list=" + infixExpressionList);
List<String> suffixexpression = parseSuffixExpressionList(infixExpressionList);
System.out.println("后缀表达式对应的List" + suffixexpression);
System.out.printf("expression=%d", calculate(suffixexpression));
//定义逆波兰表达式
// 测试
String suffixExpression = "3 4 + 5 * 6 -";
// 思路
// 1. 先将 3 4 + 5 * 6 - 方法哦 ArrayList中
// 2. 将 Arraylist 传递一个方法 遍历ArrayList 配合栈完成计算
List<String> list = getListString(suffixExpression);
System.out.println("rpnlist = " + list);
int res = calculate(list);
System.out.println("计算的结果是 = " + res);
}
/**
* @author 冷环渊 Doomwatcher
* @context: 将 Arraylist[1,+,(,(,2,+,3,),*,4,),-,5] 转化成 Arraylist[1,2,3,+,4,*,5,-]
* 转换思路:
* 1. 初始化两个栈 元素安抚栈 s1 和存储中间结果的栈2
* 2.从左至右扫描中缀表达式
* 3.遇到操作数时 将其压入s2
* 4.遇到运算符时 比较其与s1栈顶的运算符优先级别:
* 1。如果s1为空 或者栈顶运算符为左括号( 则直接将此运算符压入栈
* 2.否则,若优先级别比栈顶运算符的高,也将运算符压入s1
* 3.否则 将s1栈顶的于是暖夫弹出并且压入s2栈内 再次转到 4.1与s1新的扎你当运算符比较
* 5.遇到括号时:
* 1.如果是左括号( ,则直接压入s1
* 2.如果是右括号),则一次弹出s1栈顶的运算符,并且压入s2,直到遇到左括号位置,此时将一对括号丢弃
* 6.重复步骤 2-5 直到表达式最右边
* 7.将s1中剩余的运算符一次弹出并压入s2
* 8.依次弹出s2中的元素并且输出,结果逆序就是中缀表达式对应的后缀的表达式
*
* @date: 2021/12/27 17:48
* @param
* @return: java.util.List<java.lang.String>
*/
public static List<String> parseSuffixExpressionList(List<String> ls) {
// 定义两个栈
/*符号栈*/
Stack<String> s1 = new Stack<>();
/* S2 思路说明
* 这里我们使用栈 在转换过程中我们不需要pop操作,而且这个s2 之后我们需要逆序输出操作很麻烦
* 于是这里使用List来当作 s2 的数据存储结构
* */
List<String> s2 = new ArrayList<>();
// 遍历 ls
for (String item : ls) {
// 如果是一个数字 那么就加入 S2(这里我们字符串加入 正则表达式来判断是不是一个数字)
if (item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
s1.push(item);
} else if (item.equals(")")) {
// 如果是 )右括号 则一次弹出 s1栈顶的运算符 并且压入 s2 知道遇到左括号为止,完成前置的一系列操作之后将这一对括号丢弃
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
// 弹出( 消除掉一组括号
s1.pop();
} else {
/* 当 item 优先级小于等于s1 栈顶运算符的时候 将s1栈顶的运算符淡出并且加入到s2 中,再次重复思路中的第四个环节,与s1中的新栈顶运算符比较
*问题 我们缺少一个比较优先级高低的方法
* */
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
// 之后将 itea 压入栈
s1.push(item);
}
}
// 将s1中剩余的运算符一次弹出加入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
// 注意应为存放的是list 这里我们顺序不需要改变 直接输出就是对应的后缀表达式
return s2;
}
/**
* @author 冷环渊 Doomwatcher
* @context: 将中缀表达式 转换成对应的list
* @date: 2021/12/27 17:32
* @param s
* @return: java.util.List<java.lang.String>
*/
public static List<String> toinfixExpression(String s) {
/*定义一个List 存放中缀表达式的对应内容*/
List<String> ls = new ArrayList<>();
//定义一个指针 用于遍历 中缀表达式的字符串
int i = 0;
// 用于拼接字符串
String str;
//用于存放遍历的字符
char c;
do {
// 如果c是一个非数字 那么我们需要加入到 ls 中去 ,这里的判断条件是 ASCLL值
if ((c = s.charAt(i)) < 48 || ((c = s.charAt(i)) > 57)) {
ls.add("" + c);
// 指针后移
i++;
} else {
/*
* 如果是一个数字 那么我们还需要考虑是不是多位数
* 这里我们将 str 置空
* */
str = "";
while (i < s.length() && (c = s.charAt(i)) >= 48 && ((c = s.charAt(i)) <= 57)) {
//拼接数字字符串
str += c;
i++;
}
ls.add(str);
}
} while (i < s.length());
return ls;
}
/**
* @author 冷环渊 Doomwatcher
* @context: 将一个逆波兰表达式的数据和运算符 依次放入Arraylist中
* @date: 2021/12/25 18:09
* @param suffixExpression
* @return: java.util.List<java.lang.String> 将分割好的字符List 返回
*/
public static List<String> getListString(String suffixExpression) {
// 分割字符串 将后缀表达式的每一个字符取出
String[] split = suffixExpression.split(" ");
// 我们用list 来存储分割出来的表达式,这样之后我们就需要遍历即可
List<String> list = new ArrayList<>();
for (String ele : split) {
list.add(ele);
}
return list;
}
/**
* @author 冷环渊 Doomwatcher
* @context: 这个方法就是 逆波兰表达式的运算处理方法
* * 实现思路 : 我们以 (3+4)*4-6 对应的后缀表达式 => 3 4 + 5 * 6 -
* * 1) 从左至右扫描,将3和4压入栈
* * 2) 遇到+运算符号 因此弹出 4 和 3 (4为栈顶元素 3 为次顶元素) 计算出3+4的值,将计算的结果加入栈
* * 3) 将5 入栈
* * 4) 接下来将 * 是运算符 弹出 5和7 计算出 7*5 = 35 将结果入栈
* * 5) 将6 入栈
* @date: 2021/12/25 18:14
* @param ls
* @return: int 返回的是一个运算结果
*/
public static int calculate(List<String> ls) {
//创建栈 只需要一个栈即可
Stack<String> stack = new Stack<>();
// 遍历处理
for (String item : ls) {
// 这里我们用正则表达式来取出数字 和符号 如果是数字进行处理,如果符号做另外的处理
if (item.matches("\\d+")) {
//匹配的是数字
stack.push(item);
} else {
// 如果是符号就 pop出两个数 运算之后结果入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if ("+".equals(item)) {
res = num1 + num2;
} else if ("-".equals(item)) {
res = num1 - num2;
} else if ("*".equals(item)) {
res = num1 * num2;
} else if ("/".equals(item)) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
// 把 res 入栈
stack.push("" + res);
}
}
//最后留在stack 中的数据是运算结果
return Integer.parseInt(stack.pop());
}
}
class Operation {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
// 写一个方法 返回对应的数字
public static int getValue(String operation) {
int result = 0;
switch (operation) {
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
System.out.println("不存在该运算符");
break;
}
return result;
}
}
总结
- 计算的思路和我们先前写的逆波兰计算器事一样的
- 这里我们考虑中缀转后缀的几个要点,运算符优先级和运算符的位置
- 动手之后,debug看看栈和List的变化加深理解