下面我们以一个例子来讲三种表达式:(3+4)×5-6
前缀表达式又称波兰表达式,操作数都位于运算符之前
将上面的算式转换为前缀表达式为:-×+3456
计算机从右至左扫描算式,遇到数字则压入数栈,遇到运算符,弹出数栈顶两个数,用运算符对他们进行运算,并将结果如数栈,重复上述操作直到算出最终结果
1)从右至左开始扫描:扫描6543并依次入栈
2)继续扫描到+号,栈顶和次栈顶的两个数字3和4出栈运算3+4=7,并入栈
3)继续扫描到×号,栈顶和次栈顶的两个数字7和5出栈运算7×5=35,并入栈
4)继续扫描到-号,栈顶和次栈顶的两个数字35和6出栈运算35-6=29,即为最终结果
如 何 将 中 缀 表 达 式 转 换 为 前 缀 表 达 式 : \color{#9400D3}{如何将中缀表达式转换为前缀表达式:} 如何将中缀表达式转换为前缀表达式:
1)先找出一个算式中最先运算的一个运算符和他前后的数字,这里显然是3+4
2)然后按照符号在前数字在后的顺序写下:+34
3)将已经写过的部分替换成一个整体,这里就用A代替,替换后的式子就是A×5-6
4)继续去找剩余式子中最优先运算的部分,这里就是A×5
5)将运算符写在最左边,数字写在最右边得到:×+345
6)重复上诉替换查找最优先部分的方法即可得到-×+3456
中缀表达式就是人们最常见的算式(3+4)×5-6
中缀表达式的求值是我们人最熟悉的,但对计算机来说并不好操作,例如我们上一篇博客写的综合计算器的例子,就是中缀表达式,我们需要考虑运算符的优先级,然后写一大堆的逻辑在里面,因此在计算时,我们常常将中缀表达式转为其他表达式来操作(一般转为后缀表达式)
前 缀 表 达 式 转 中 缀 表 达 式 : − × + 3456 \color{#9400D3}{前缀表达式转中缀表达式:-×+3456} 前缀表达式转中缀表达式:−×+3456
前缀转中缀我们从中间开始,两边结束
1)先找到最中间的两个数字和一个运算符,转化为中缀表达式的写法3+4
2)剩余部分直接去中间的一个运算符和一个数字,这里注意,如果后面取到的运算符优先级大于之前的运算符,那么要给之前已经转好的部分中缀表达式加上括号
3)剩下的部分继续从中间开始找到×5
4)因为×号的优先级大于+号,所以要给之前得到的中缀表达式加上括号,然后将它直接拼接在后面得到:(3+4)×5
5)继续往后看,因为-号优先级小于×号,所以直接拼接-6即可得到结果(3+4)×5-6
后 缀 表 达 式 转 中 缀 表 达 式 : 34 + 5 × 6 − \color{#9400D3}{后缀表达式转中缀表达式:34+5×6-} 后缀表达式转中缀表达式:34+5×6−
后缀转中缀我们从左到右顺序进行
从左往右顺序读取,将运算符放在数字中间即可,如果发现后面的运算符优先级大于前面的运算符,就将前面运算符的式子用括号括起来
1)将+号放在34中间得到3+4
2)往后看,下一个是5×,按正常来说应该将×号放在5前面拼接上去:写为3+4×5,但是由于后面一个的运算符为×号,优先级大于前面的+号,所以要将前面的3+4括起来,得到(3+4)×5
3)继续往后看,将-号提前到6前面进行拼接即可得到结果(3+4)×5-6
后缀表达式又称逆波兰表达式,操作数位于运算符之后
将上面的算式转换为前缀表达式为:34+5×6-
计算机从左至右扫描算式,,遇到数字时入栈,遇到运算符时,弹出栈顶的两个数,并进行运算,并将结果入栈,重复上述操作直到算出最终结果
1)从左至右开始扫描:扫描34依次入栈
2)继续扫描到+号,栈顶和次栈顶的两个数字3和4出栈运算3+4=7,并入栈
3)继续扫描到5,入栈
4)继续扫描到×号,栈顶和次栈顶的两个数字7和5出栈运算7×5=35,并入栈
5)继续扫描到6,入栈
6)继续扫描到-号,栈顶和次栈顶的两个数字35和6出栈并运算35-6=29,即为最终结果
如 何 将 中 缀 表 达 式 转 换 为 后 缀 表 达 式 : \color{#9400D3}{如何将中缀表达式转换为后缀表达式:} 如何将中缀表达式转换为后缀表达式:
1)先找出一个算式中最先运算的一个运算符和他前后的数字,这里显然是3+4
2)然后按照从左到右先写数字再写运算符的顺序写下:34+
3)将已经写过的部分替换成一个整体,这里就用A代替,替换后的式子就是A×5-6
4)继续去找剩余式子中最优先运算的部分,这里就是A×5
5)接着刚才的式子,按照同样的规则从左到右先写数字再写运算符的顺序写下,也就是在34+后面继续写下5×,得到34+5×
6)重复上诉替换查找最优先部分的方法即可得到34+5×6-
我们上一章的博客中完成的综合计算器里留下了一个括号问题,那么在这一篇博客中我们就利用前缀和后缀表达式将该问题解决
逆波兰计算器,顾名思义就是用后缀表达式去完成一个计算器
要求完成如下任务:
1)输入一个中缀表达式,使用栈(我们这里就用java工具类里提供的栈)计算结果
2)支持小括号和多位整数
这里我们输入的是中缀表达式,那么在计算机里我们首先要去将中缀表达式转换为后缀表达式
上面我们说了用最白的语言去分析如何进行中缀表达式转换后缀表达式,那么这里我们需要用代码去实现,下面我们就来分析一下如何用一个栈去实现上诉的转化问题,并实现一个逆波兰计算器
中 缀 表 达 式 转 后 缀 表 达 式 : \color{#9400D3}{中缀表达式转后缀表达式:} 中缀表达式转后缀表达式:
1)初始化两个栈,一个用来存放运算符(S1),一个用来存放中间结果(数栈S2),接着从左至右开始扫描表达式
2)遇到操作数,直接入S2栈。遇到运算符,如果S1为空或者栈顶运算符为左括号,则直接入栈,否则,将其与S1栈顶的运算符优先级作比较
2.1)如果当前扫描到的运算符优先级大于栈顶的运算符,直接入栈
2.2)如果当前扫描到的运算符优先级小于等于栈顶的运算符,将S1中的运算符出栈,然后入S2栈中。接着再将当前扫描到的运算符与下一个S1栈顶的运算符进行优先级的比较,重复上诉步骤,直到当前的运算符入了S1栈
2.2)如果扫描到的是右括号,那么将S1中的运算符依次弹出入S2栈,直到弹出到一个左括号为止。
3)当中缀表达式扫描完之后,将S1中的运算符依次出栈后入S1栈
4)将S1栈中的元素,顺序出栈后的倒序即为后缀表达式
我们这里以1+((2+3)×4)- 5为例用图解上面的的思路:(从左到右扫描)
1)扫描到1,入S2栈,扫描到+号,S1栈为空,可以直接入栈
2)扫描到左括号(,直接入S1栈
3)又扫描到左括号(,直接入S1栈
4)扫描到2,直接入S2栈
5)扫描到+号,因为S1中栈顶为左括号,所以直接入栈
6)扫描到3,直接入S2栈
7)扫描到右括号(,将S1中的元素逐个出栈入S2栈,直到遇到左括号为止(括号不入S2栈,直接跳过即可)
8)扫描到×号,因为S1栈顶现在为左括号,所以直接入S1栈
9)扫描到4,直接入S2栈
10)扫描到右括号),将S1中的元素逐个出栈入S2栈,直到遇到左括号为止(括号不入S2栈,直接跳过即可)
11)扫描到-号,与S1栈顶元素优先级比较为相等,所以S1运算符出栈,到S2中,然后S1为空,-号可以入S1栈
12)扫描到5,直接入S1栈中
13)中缀表达式扫描完毕,S1栈中元素顺序出站,然后入S1栈中
14)将S2中的元素顺序出栈:-5+×4+321
15)将得到的顺序进行倒序即为后缀表达式:123+4×+5-
小 改 动 : \color{#9400D3}{小改动:} 小改动:
上面的分析我们使用两个栈去实现的,但是在最后我们需要将最后的那个栈中的元素全部倒序出栈,这一点就有点麻烦了。所以我们在实际写代码的时候完全可以用一个ArrayList去存放最终的元素,这样只要顺序输出即可得到中缀转化的后缀表达式,所以下面的代码中我们就用java.util包里的一个Stack和一个ArrayList去实现
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* 逆波兰计算器(结合中缀表达式转后缀表达式),这里以(3+4)×5-6为例
* @author centuowang
*/
public class PolandCal {
public static void main(String[] args) {
//先人为的给一个中缀表达式
String middle_formula = "10+((2+3)*4)-5";//答案应该是:25
/**
* 我们先将表达式进行分割,并将每一个字符放入ArrayList中,然后再遍历ArrayList,用栈进行计算
*/
List<String> splitlist = SplitArrayList(middle_formula);//分割中缀表达式
System.out.println("分割好的中缀表达式" + splitlist);
List<String> back_formula = changeformula(splitlist);//中缀表达式转后缀表达式
System.out.println("转化的后缀表达式" + back_formula);
int result = against_cal(back_formula);
System.out.println("计算机计算的结果为:" + result);
}
/**
* 中缀表达式转后缀表达式
*/
public static List<String> changeformula(List<String> splitlist) {
//创建一个Stack和一个ArrayList
Stack<String> s = new Stack();
List<String> ls = new ArrayList<>();
//开始进行转化
for (String item : splitlist) {//遍历传进来的分解好的中缀表达式
if (item.matches("\\d+")) {
//如果匹配的是多位数,直接加入ArrayList
ls.add(item);
} else if (item.equals("(")) {
//如果是左括号,直接入栈
s.push(item);
} else if (item.equals(")")) {
//如果是右括号,那么将栈中运算符出栈放到ArrayList中,直到出现左括号)为止
while (!s.peek().equals("(")) {
ls.add(s.pop());//出栈符号加入ArrayList中
}
s.pop();//将左括号(丢掉
} else if (s.size() == 0 || s.peek().equals("(")) {
//如果是运算符,且stack为空或者栈顶元素为左括号,直接入栈
s.push(item);
} else if (priority(item) > priority(s.peek())) {
//如果是运算符,且stack不为为空,且该运算符的优先级大于栈顶运算符的优先级,直接入栈
s.push(item);
} else if (priority(item) <= priority(s.peek())) {
//如果是运算符,且stack不为为空,且该运算符的优先级小于等于栈顶运算符的优先级,则将栈中优先级大于该运算符的依次出栈加入到ArrayList中
while (s.size() > 0 && priority(item) <= priority(s.peek())) {
ls.add(s.pop());
}
s.push(item);//最后记得这个运算符还要入栈
}
}
//遍历完了之后,千万要记得将栈中剩余的运算符出栈到ArrayList中
while(s.size() != 0){
ls.add(s.pop());
}
return ls;
}
/**
* 用于分割字符串,将每一个字符放入ArrayList中
*/
public static List<String> SplitArrayList(String middle_formula) {
String str = "";//用于拼接多位数
char c = ' ';//用于存放分割的字符
List<String> list = new ArrayList<>();
for (int i = 0; i < middle_formula.length(); i++) {
c = middle_formula.charAt(i);//获取middle_formula中的第i个字符
if (c < 48 || c > 57 || i + 1 >= middle_formula.length()) {//如果获得的字符不为数字,直接加入list中;或者如果为数字且为最后一个数字的话也直接加入list
//多位数加入list
if (!str.equals("")) {
list.add(str);
}
list.add(c + "");
str = "";//每次不是多位数加入的时候,需要对str做一个清空,以便下一个多位数继续从头开始拼接
} else if (i + 1 < middle_formula.length() && (middle_formula.charAt(i + 1) <= 48 || middle_formula.charAt(i + 1) >= 57)) {
str = str + c;//多位数的数字拼接
}
}
return list;//最后将分割好的List返回回去
}
/**
* 用栈实现的逆波兰的计算方法
*/
public static int against_cal(List<String> splitlist) {
Stack<String> stack = new Stack<>();
//开始遍历分割好的list
for (String c : splitlist) {
if (c.matches("\\d+")) {//用正则表达式匹配多位数
//如果是数字,直接入栈
stack.push(c);
} else {
//如果是运算符,则出栈两个数字后进行计算,再将结果入栈
int num1 = Integer.parseInt(stack.pop());
int num2 = Integer.parseInt(stack.pop());
int rs = 0;
switch (c) {
case "+":
rs = num1 + num2;
break;
case "-":
rs = num2 - num1;//注意减数和被减数的顺序
break;
case "*":
rs = num1 * num2;
break;
case "/":
rs = num2 / num1;//注意除数和被除数的顺序
break;
default:
throw new RuntimeException("运算符有误");
}
//结果入栈
stack.push(rs + "");
}
}
//取最终结果
return Integer.parseInt(stack.pop());
}
/**
* 用于比较运算符优先级的方法
*/
public static int priority(String item) {
int result;
switch (item) {
case "+":
result = 1;
break;
case "-":
result = 1;
break;
case "*":
result = 2;
break;
case "/":
result = 2;
break;
default:
throw new RuntimeException("不存在该运算符");
}
return result;
}
}
当然,我们这个只是个小demo,要细究还有很多可扩展的地方,比如加入小数的运算,对于精度的控制等等,我们的主题是讲数据结构,所以剩余的功能,这里就不做完整的实现,demo跑通,有个样子即可。掌握好数据结构才是我们的关键
下一篇: 从1开始学Java数据结构与算法——递归解决迷宫回溯问题和八皇后问题.