数据结构与算法(三)
2021/10/11 17:16:03
本文主要是介绍数据结构与算法(三),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
在上一篇文章中,我们完整实现了计算器功能,但是还存在一些问题:
解决不支持多位数
不支持多位数的原因就在于:在扫描表达式时,没有考虑多位数的解析。那么思路是:当我们当前的数据是数字的时候,不能直接添加,而是保存到一个String中,需要判断下一位数据是不是字符或者没有下一位,如果是,将字符串转换成int类型数据添加到数栈中,如果不是直接扫描下一位数据继续判断,直到遇到符号或者结尾才结束,将数添加到数栈。当一个数添加到数栈以后,字符串需要重置,否则会出现错误。
在扫描表达式这一步进行修改支持多位数
//当我们遍历的数据是数字的时候 numCh += ch;//numCh为String类型,我们在遍历循环之前定义 //判断当前遍历的下一位还是不是数字,如果当前遍历元素是最后一位直接结束 if(index == express.length() -1){ numStack.push(Integer.parseInt(numCh)); }else{ //当下一个元素是符号的时候才会添加,numCh进行重载,否则数字保存在numCh中 if(numStack.isOper(express.substring(index+1,index+2).charAt(0))){ numStack.push(Integer.parseInt(numCh)); numCh = ""; } }
三种表达式
前缀表达式(波兰表达式)
前缀表达式又称为 波兰表达式,前缀表达式的 运算符位于操作数之前。
例如:(3+4)x5-6
对应的前缀表达式为:- x + 3 4 5 6
注意:前面这个表达式是一个中缀表达式,对应的是后面的这个前缀表达式。它的符号出现的顺序与中缀的顺序不一致。
前缀表达式中的符号顺序,就是他求值的规定了
前缀表达式求值过程
-
从 右到左 扫描表达式
-
遇到 数字 时,将数字压入堆栈
-
遇到 运算符 时
弹出栈顶的两个数(栈顶和次顶),用运算符对它们做相应的计算,并将结果入栈。
计算顺序是:先 弹出来的 (运算符) 后 弹出来的
然后重复以上步骤,直到表达式的最左端,最后运算出的值则是表达式的值。
看完前缀表达式的计算逻辑,那么你要明白的是,从一个 中缀表达式 转换为 前缀表达式 时,优先级顺序是已经处理好的,因为在求值时,不进行优先级的判定
例如:(3+4)x5-6
对应的前缀表达式为:- x + 3 4 5 6
,前缀表达式求值步骤如下:
-
从右到左扫描,将 6、5、4、3 压入栈
-
遇到
+
运算符时:将弹出 3 和 4(3 为栈顶元素,4 为次顶元素),计算出
3 + 4 = 7
,将结果压入栈 -
遇到
x
运算符时将弹出 7 和 5,计算出
7 x 5 = 35
,将 35 压入栈 -
遇到
-
运算符时将弹出 35 和 6,计算出
35 - 6 = 29
,压入栈 -
扫描结束,栈中留下的唯一一个数字 29 则是表达式的值
中缀表达式
中缀表达式就是 常见的运算表达式,如 (3+4)x5-6
。
中缀表达式的求值是人类最熟悉的,但是对于计算机来说却不好操作:
- 需要计算运算符的优先级
- 对于中括号来说,笔者想不出实现办法
因此,在计算结果时,往往会将 中缀表达式 转成其他表达式,一般转成后缀表达式。
后缀表达式(逆波兰表达式)
后缀表达式 又称为 逆波兰表达式,与前缀表达式类似,只是 运算符 位于 操作数之后。
比如:(3+4)x5-6
对应的后缀表达式 3 4 + 5 x 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 + = |
后缀表达式求值过程
-
从 左到右 扫描表达式
-
遇到 数字 时,将数字压入堆栈
-
遇到 运算符 时
弹出栈顶的两个数(栈顶和次顶),用运算符对它们做相应的计算,并将结果入栈。
计算顺序是:后 弹出来的 (运算符) 先 弹出来的
然后重复以上步骤,直到表达式的最右端,最后运算出的值则是表达式的值。
比如:(3+4)x5-6
对应的后缀表达式 3 4 + 5 x 6 -
-
从左到右扫描,将 3、4 压入堆栈
-
扫描到
+
运算符时将弹出 4 和 3,计算
3 + 4 = 7
,将 7 压入栈 -
将 5 入栈
-
扫描到
x
运算符时将弹出 5 和 7 ,计算
7 x 5 = 35
,将 35 入栈 -
将 6 入栈
-
扫描到
-
运算符时将弹出 6 和 35,计算
35 - 6 = 29
,将 29 压入栈 -
扫描表达式结束,29 是表达式的值
逆波兰计算器
完成一个逆波兰计算器,需求如下:
-
输入一个 逆波兰表达式,使用栈 Stack(JDK 自带),计算器结果
-
支持 小括号 和 多位数
主要这里是讲解数据结构,因此简化为只对整数计算
注意咯:知道逆波兰表达式是什么后,相对来说,实现还是比较容易的,难点其实是在于中缀表达式如何转换为后缀表达式,因为这涉及到运算符的优先级问题。在前面我们实现的时候,其实就是这个运算符优先级问题很刺手。而前缀、后缀表达式里面表达式的优先级在形成表达式时就已经确定了。
public class PolandNotation { public static void main(String[] args) { //使用逆波兰表达式计算结果 String suffixExpress = "4 5 * 8 - 60 + 8 2 / +"; List<String> res = getString(suffixExpress); System.out.println("res = " + res); int result = 0; try { result = calculation(res); } catch (Exception e) { System.out.println(e.getMessage());; } System.out.println("计算结果为:" + result); } //将逆波兰表达式转分割,装入ArrayList集合,方便后面进行运算 public static List<String> getString(String suffix){ String[] splits = suffix.split(" "); List<String> res = new ArrayList<>(); for(String s: splits){ res.add(s); } return res; } //计算结果 public static int calculation(List<String> list){ int result = 0; Stack<String> stack = new Stack<>(); for(String s : list){ //判断是不是一个数字 if(s.matches("\\d+")){ //使用正则表达式判断 stack.push(s);//入栈 }else{ //进行运算 int num2 = Integer.parseInt(stack.pop()); int num1 = Integer.parseInt(stack.pop()); if(s.equals("+")){ result = num1 + num2; }else if(s.equals("-")){ result = num1 - num2; }else if(s.equals("*")){ result = num1 * num2; }else if(s.equals("/")){ result = num1 / num2; }else{ throw new RuntimeException("运算符错误"); } //将结果入栈 stack.push(result + ""); } } //最后在栈中的就是计算结果 return Integer.parseInt(stack.pop()); } }
中缀表达式转后缀表达式
通过前面的逆波兰计算器的代码实现,可以看到:后缀表达式对于计算机来说很方便,但是对于人类来说,后缀表达式却不是那么容器写出来的。
中缀表达式转后缀表达式步骤:
-
初始化两个栈:
- 运算符栈:s1
- 中间结果栈:s2
-
从左到右扫描中缀表达式
-
遇到操作数时,将其压入 s2
-
遇到运算符时
比较 它 与 s1 栈顶运算符的优先级:
- 如果 s1 为空,或则栈顶运算符号为
(
,则将其压入符号栈 s1 - 如果:优先级比栈顶运算符 高,也将其压入符号栈 s1
- 如果:优先级比栈顶运算符 低 或 相等,将 s1 栈顶的运算符 弹出,并压入到 s2 中
再重复第 4.1 步骤,与新的栈顶运算符比较(因为 4.3 将 s1 栈顶运算符弹出了)
这里重复的步骤在实现的时候有点难以理解,下面进行解说:
-
如果 s1 栈顶符号 优先级比 当前符号 高或则等于,那么就将其 弹出,压入 s2 中(循环做,是只要 s1 不为空)
如果栈顶符号为
(
,优先级是 -1,就不会弹出,就跳出循环了 -
跳出循环后,则将当前符号压入 s1
- 如果 s1 为空,或则栈顶运算符号为
-
遇到括号时:
-
如果是左括号
(
:则直接压入 s1 -
如果是右括号
)
:则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到 左括号 为止,此时将这一对括号 丢弃
-
-
重复步骤 2 到 5,直到表达式最右端
-
将 s1 中的运算符依次弹出并压入 s2
-
依次弹出 s2 中的元素并输出,结果的 逆序 即为:中缀表达式转后缀表达式
下面进行举例说明:
将中缀表达式:1+((2+3)*4)-5
转换为后缀表达式
扫描到的元素 | s2 (栈底 -> 栈顶) | s1(栈底 -> 栈顶) | 说明 |
---|---|---|---|
1 | 1 |
空 | 遇到操作数,将其压入 s2 |
+ |
1 |
+ |
s1 栈为空,将其压入 s1 |
( |
1 |
+ ( |
是左括号,压入 s1 |
( |
1 |
+ ( ( |
是左括号,压入 s1 |
2 | 1 2 |
+ ( ( |
遇到操作数,将其压入 s2 |
+ |
1 2 |
+ ( ( + |
遇到操作符:与 s1 栈顶运算符比较,为 ( ,将其压入 s1 |
3 | 1 2 3 |
+ ( ( + |
遇到操作数,将其压入 s2 |
) |
1 2 3 + |
+ ( |
遇到右括号:弹出 s1 中的 + 压入 s2 中,这里去掉这一对小括号 |
* |
1 2 3 + |
+ ( * |
遇到操作符:与 s1 栈顶比较,为 ( ,将其压入 s1 栈 |
4 | 1 2 3 + 4 |
+ ( * |
遇到操作数:将其压入 s2 |
) |
1 2 3 + 4 * |
+ |
遇到右括号:弹出 s 1 中的 * 压入 s2 中,这里去掉这一队小括号 |
- |
1 2 3 + 4 * + |
- |
遇到操作符:与 s1 栈顶比较,优先级一致,将 s1 中的 + 弹出,并压入 s2 中 |
5 | 1 2 3 + 4 * + 5 |
- |
遇到操作数:将其压入 s2 |
1 2 3 + 4 * + 5 - |
空 | 解析完毕,将 s1 中的符号弹出并压入 s2 中 |
由于 s2 是一个栈,弹出是从栈顶弹出,因此逆序后结果就是 1 2 3 + 4 * + 5 -
在学习和使用上有两个层次:
- 应用层次:别人发明出来的东西,你学习、理解它,并灵活运用它
- 自创:你自己发明一个东西出来,并使用它
那么这里的中缀转后缀表达式的思路步骤,则属于第一个层次,相关的计算机专家之类的,发明出来了。我们要理解它并灵活运用它。等你能力达到一定层度时,有可能发明出来一个算法。
再比如:绝世武功 -> 降龙十八掌,别人已经创造出来了,你不去学习理解它,如何加以改进并自创?如果没有人教你,你怎么能学会降龙十八掌?
逆波兰表达式计算器代码实现
public class PolandNotation { public static void main(String[] args) { String str = "1+((2+3)*4)-5"; List<String> list = toInfixExpressionList(str); System.out.println("list = " + list); List<String> result = parseSuffixExpressionList(list); System.out.println("result:" + result); //计算结果 System.out.println(calculation(result)); } //中缀表达式转后缀表达式 public static List<String> parseSuffixExpressionList(List<String> list){ //1.初始化一个栈用来保存符号,直接使用ArrayList来保存中间数据,保存的结果输出就是后缀表达式 Stack<String> operStack = new Stack<>(); List<String> result = new ArrayList<String>(); //2.从左到右遍历中中缀表达式 for(String c : list){ if(c.matches("\\d+")){ //当前表达式是一个数,直接添加到中间数据中 result.add(c); }else{ //当为符号的情况 if(operStack.isEmpty()){ //当符号栈为空的时候,或者遇到“("就直接添加 operStack.push(c); }else if(c.equals("(")){ operStack.push(c); }else if(c.equals(")")){ //当当前符号为)的时候,弹出栈中数据到中间结果中,直到遇到(就结束 while(!operStack.peek().equals("(")){ result.add(operStack.pop()); } //将符号"("弹出 operStack.pop(); }else{ //进行优先级的比较 while(operStack.size() != 0 && Operational.priority(c) <= Operational.priority(operStack.peek())){ //当我们要添加的符号的优先级小于或者等于栈顶元素的时候,就将栈顶元素取出加入到中间结果 result.add(operStack.pop()); } operStack.push(c);//将当前符号加入栈中 } } } //将符号栈中的符号添加至中间结果 while(!operStack.isEmpty()){ result.add(operStack.pop()); } return result; } //将一个中缀表达式分割并存放到一个ArrayList集合中,方便我们后面转换操作 public static List<String> toInfixExpressionList(String str){ List<String> list = new ArrayList<>(); String s = null; int index = 0;//遍历str字符串的索引 char c;//保存每次遍历的数据 do{ c = str.charAt(index); s = "";//重置字符串 //判断这个数据是字符还是数据 if(c >= 48 && c <= 57){ //是数字 s += c; //查看下一个是否是数字,注意要防止最后一个元素是多位数,出现索引越界的情况, // 所以这里只能判断到length-2就进入循环 while(index < (str.length()-1) && (str.charAt(index+1) >= 48) && (str.charAt(index+1) <= 57) ){ //后一个还是数字 index++; s += str.charAt(index); } list.add(s); index++; }else{ //是字符,直接加入 s += c; list.add(s); index++; } }while(index < str.length()); return list; } //将逆波兰表达式转分割,装入ArrayList集合,方便后面进行运算 public static List<String> getString(String suffix){ String[] splits = suffix.split(" "); List<String> res = new ArrayList<>(); for(String s: splits){ res.add(s); } return res; } //计算结果 public static int calculation(List<String> list){ int result = 0; Stack<String> stack = new Stack<>(); for(String s : list){ //判断是不是一个数字 if(s.matches("\\d+")){ //使用正则表达式判断 stack.push(s);//入栈 }else{ //进行运算 int num2 = Integer.parseInt(stack.pop()); int num1 = Integer.parseInt(stack.pop()); if(s.equals("+")){ result = num1 + num2; }else if(s.equals("-")){ result = num1 - num2; }else if(s.equals("*")){ result = num1 * num2; }else if(s.equals("/")){ result = num1 / num2; }else{ throw new RuntimeException("运算符错误"); } //将结果入栈 stack.push(result + ""); } } //最后在栈中的就是计算结果 return Integer.parseInt(stack.pop()); } } //定义一个工具类,主要用于比较运算符的优先级 class Operational{ //定义一个操作符类,用于比较 private static final int ADD = 1; private static final int SUB = 1; private static final int MUL = 2; private static final int DIV = 2; private static final int OPER = 0; public static int priority(String c){ int res = -1; switch(c){ case "+": res = ADD; break; case "-": res = SUB; break; case "*": res = MUL; break; case "/": res = DIV; break; case "(": res = OPER; break; default: System.out.println("操作符非法"); break; } return res; } }
这篇关于数据结构与算法(三)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南