栈的运用--普通计算器和逆波兰计算器
用栈实现计算器,这个比较难。有兴趣和热情的朋友,你可以看看。没有的,建议你看下一个数据结构。
逆波兰计算器是我们的终极目标,在实现目标之前,需要不断积累,所以我们一步步来:
普通单位加减乘除计算器-->普通多位加减乘除计算器-->前中后缀表达式规则-->逆波兰计算器
单位加减乘除实现:输入一个字符串“2+2*5-2”,通过栈计算出答案
算法原理:
栈结构,我选取上面实现的链表模拟栈;在原有基础上,新增 isEmpty,getTop(获取栈顶),priority(获取操作符的优先级),isOper(判断是否为运算符),cal(输入两个数字与一个运算符进行数学运算)方法,具体实现见代码;
创建numStick用于存储数字,创建operStick用于存储运算符,对字符串进行逐项扫描,每扫描一个字符判断其是否为运算符(+-*/)
是运算符时,就判断operStick空吗(isEmpty实现)?空就直接入栈,不空就要进行当前运算符与栈顶运算符(用getTop获取)进行优先级的比较(priority实现)
当前<=top,则将numStick中的两个栈点进行出栈,将operStick的栈顶进行出栈,这些出栈数据统统装入cal方法进行运算,运算结果放入numStick中,当前运算符入栈。当前>top,当前运算符直接入栈
不是运算符时,就直接入numStick栈;在上述判断执行完后,继续循环执行,直到字符串中所有字符都被扫描即可;
扫描完成后,所有数据被列入栈中,上述操作的目的旨在将高级运算在入栈时,运算干净,这样栈里都是同级的运算
下面对栈中的同级运算数据进行运算,运算过程就是numStick出两个,operStick出一个,进入cal方法中运算,结果继续入numStick栈,然后接着重复计算,直到operStack中没有运算符残留,就结束。
那么numStack中一定只剩下一个数字,而它就是我们苦苦寻找的结果!
package cn.dataStructure.demo.stack.calculator;
class DoubleNode{
public int data;
public DoubleNode next;//保存下一个结点
public DoubleNode pre;//保存上一个结点
public DoubleNode(int data){
this.data =data;
}
@Override
public String toString() {
return "["+this.data +"]";
}
}
class DoubleLinkedListStack{
private DoubleNode root=new DoubleNode(0);//创建有头根节点
public DoubleNode getRoot(){
return this.root;
}
public void push(int data){//普通add:不按序号存储
DoubleNode temp=this.root;//root不能动,需要辅助结点
while (true){//查找链表最后结点
if (temp.next==null){
break;
}
temp=temp.next;//向后一位
}
DoubleNode newNode=new DoubleNode(data);
temp.next=newNode;//尾结点双向添加
newNode.pre=temp;
}
public int pop(){
if (root.next==null){//判断是否为空链表
throw new RuntimeException("空栈");
}
DoubleNode temp=root;
while (temp.next!=null){//遍历到链表最后
temp=temp.next;
}
int value=temp.data;
temp.pre.next=null;//自我删除
return value;
}
public void show(){//逆向打印
if (root.next==null){//判断是否为空链表
System.out.println("空栈");
return;
}
DoubleNode temp=root;
while (temp.next!=null){//遍历到链表最后
temp=temp.next;
}
while (temp!=root){//逆向打印
System.out.println(temp);
temp=temp.pre;
}
}
//判断栈空
public boolean isEmpty(){
return root.next==null;
}
//返回栈顶的值
public int getTop(){
if (root.next==null){//判断是否为空链表
throw new RuntimeException("空栈");
}
DoubleNode temp=root;
while (temp.next!=null){//遍历到链表最后
temp=temp.next;
}
return temp.data;
}
//返回运算符优先级,正比
public int prioprity(int oper){
if (oper=='*'||oper=='/'){
return 1;
}else if (oper=='+'||oper=='-'){
return 0;
}else {
return -1;//暂且支持加减乘除
}
}
//判断是否为加减乘除
public boolean isOper(int oper){
return oper=='+'||oper=='-'||oper=='/'||oper=='*';
}
//对数据进行计算
public int cal(int num1,int num2,int oper){
int result=0;//存储结果
switch (oper){
case '+':
result=num1+num2;
break;
case '-'://栈先入后出,所以2-1
result=num2-num1;
break;
case '*':
result=num1*num2;
break;
case '/'://栈先入后出,所以2-1
result=num2/num1;
break;
}
return result;
}
}
public class 栈实现计算器 {
public static void main(String[] args) {
DoubleLinkedListStack numStack=new DoubleLinkedListStack();//数字栈
DoubleLinkedListStack operStack=new DoubleLinkedListStack();//运算符栈
String str="2+5*2-2";//10
int index=0;//对str进行扫描的索引
char ch=' ';//保存每次扫描到的字符
int num1;//存储要计算的数字
int num2;
int value;//用于承接临时计算数据
//对字符串进行扫描,将所有数据录入栈中
while (true){
ch=str.substring(index,index+1).charAt(0);
if (operStack.isOper(ch)){//判断是否为操作符
if (operStack.isEmpty()){//操作符栈空时,扫描到的操作符直接入栈
operStack.push(ch);
}else if (operStack.prioprity(ch)<=operStack.prioprity(operStack.getTop())){//判断当前操作符的优先级<=栈顶操作符的优先级
num1=numStack.pop();
num2=numStack.pop();
value=operStack.cal(num1,num2,operStack.pop());
numStack.push(value);
operStack.push(ch);
}else{//当前操作符的优先级>栈顶操作符的优先级
operStack.push(ch);
}
}else {
numStack.push(ch-48);//数字是按char接收的,所以要减去48,从char恢复到int
}
index++;
if (index==str.length()){//此时字符串中所有数据被扫描完
break;
}
}
//对栈中数据进行计算
while (true){
if (operStack.isEmpty()){//当操作符栈空,数据计算完成
break;
}
num1=numStack.pop();
num2=numStack.pop();
value=operStack.cal(num1,num2,operStack.pop());
numStack.push(value);
}
System.out.printf("%s=%d",str,numStack.pop());
}
}
2+5*2-2=10
开心了吗?其实还是有点不开心,因为上面这个只能是一位数字运算,多位数程序就崩了,原因在于我们在数字入栈前没有判断下一个字符是不是数字。那么我们开始着手升级
多位加减乘除实现:输入一个字符串“2+2*5-20”,通过栈计算出答案
算法分析:
修改字符串
String str="2+5*2-20";//-8
新增number用于处理多位计算,将多次读入的字符合并
String number="";//用于处理多位计算,将多次读入的字符合并
对数字入栈进行升级
//对字符串进行扫描,将所有数据录入栈中
while (true){
ch=str.substring(index,index+1).charAt(0);
if (operStack.isOper(ch)){//判断是否为操作符
if (operStack.isEmpty()){//操作符栈空时,扫描到的操作符直接入栈
operStack.push(ch);
}else if (operStack.prioprity(ch)<=operStack.prioprity(operStack.getTop())){//判断当前操作符的优先级<=栈顶操作符的优先级
num1=numStack.pop();
num2=numStack.pop();
value=operStack.cal(num1,num2,operStack.pop());
numStack.push(value);
operStack.push(ch);
}else{//当前操作符的优先级>栈顶操作符的优先级
operStack.push(ch);
}
}else {
//对下面这句话进行修改,实现多位运算支持
//numStack.push(ch-48);//数字是按char接收的,所以要减去48,从char恢复到int
number+=ch;//合并所有扫描到的数字
if (index==str.length()-1){//若该数字是最后一位,无需判断直接入栈
numStack.push(Integer.parseInt(number));
}else {//判断下一个字符是数字吗?不是则入栈
if (operStack.isOper(str.substring(index+1,index+2).charAt(0))){
numStack.push(Integer.parseInt(number));
number="";//清空number
}
}
}
效果如下
2+5*2-20=-8
上面的计算器实现最麻烦的是,要将人看的懂的表达式转化为机器能看懂的表达式!解决这个问题的办法----波兰表达式。
前中后缀表达式规则
前缀即波兰表达式,中缀即人看的表达式,后缀即逆波兰表达式,上面我们实现的计算机属于中缀计算器。其中逆波兰表达式,计算机最懂,所以我以逆波兰为主,制作一个逆波兰计算器
逆波兰计算器实现
算法分析:
为了简化代码复杂度,使用系统提供的类集框架代替自己写的链表与栈
将逆波兰表达式分割为一个个单独的字符串数组,通过逆波兰表达式阅读规则:遇到运算符就出栈两个数字,编写计算方法
package cn.dataStructure.demo.stack.calculator;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class 逆波兰计算器 {
public static void main(String[] args) {
String str="2 5 * 8 2 / + 1 -";//(2*5)+8/2-1-->2 5 * 8 2 / + 1 -;13
List<String> list=getListString(str);
System.out.printf("(2*5)+8/2-1=%d",calculate(list));
}
//分割逆波兰表达式入list存储
public static List<String> getListString(String str){
String split[]=str.split(" ");
List <String> list=new ArrayList<>();
for (String temp:split) {
list.add(temp);
}
return list;
}
//逆波兰表达式计算
public static int calculate(List<String> list){
Stack<String> stack=new Stack<>();
for (String temp:list){
if (temp.matches("\\d+")){//正则匹配多位数字
stack.push(temp);
}else {
int num1= Integer.parseInt(stack.pop());
int num2= Integer.parseInt(stack.pop());
int value=0;
switch (temp){
case "+":
value=num2+num1;
break;
case "-":
value=num2-num1;
break;
case "*":
value=num2*num1;
break;
case "/":
value=num2/num1;
break;
default:
throw new RuntimeException("运算符有误");
}
stack.push(""+value);//利用+将int转为string
}
}
return Integer.parseInt(stack.pop());
}
}
2*5+8/2-1=13
你有没有一个疑问,有没有可以把中缀表达式自动转化为逆波兰表达式的算法?有,如下:
中缀-->逆波兰
举个例子:“1+((2+3)*4)-5”-->“1 2 3 + 4 * + 5 -”
在整个过程中,s1不断进行入栈,出栈操作,所以s1一定为栈结构,但s2只有入栈,没有出栈,且若s2以栈结构时,最后输出逆波兰表达式时,需要逆向输出。所以我们使用ArrayList来代替栈结构。
在上面的代码中我们已经实现了逆波兰表达式计算,下面实现中缀表达式转逆波兰。算法流程和上面图片中描述的一样
算法分析:
1,对getListString方法进行改造,使之可以对中缀表达式字符串进行分割,该算法支持多位整数。
public static List<String> getListString(String str){
List<String> list=new ArrayList();
int index=0;//字符串索引标记
char ch;//拆分的字符
String string;//拼接的字符串
do {
ch=str.charAt(index);
if (ch<48||ch>57){//非数字
list.add(""+ch);
index++;
}else {//是数字,考虑多位问题
string="";
//index不能越界,且下一位仍然为数字
while (index<str.length()&&(ch=str.charAt(index))>=48&&(ch=str.charAt(index))<=57){
string+=ch;
index++;
}
list.add(string);
}
}while (index<str.length());
return list;
}
2,新增parseSuffix方法,用于将拆分好的中缀表达式转化为逆波兰表达式。
这里有个坑:进行字符串比较的时候要用equals方法,不要用==比较。这里牵扯到Java字符串底层机制(入池与不入池),详见我的Java SE博客:【String类-字符串比较】
//中缀表达式转逆波兰表达式的方法
public static List<String> parseSuffix(List<String> list){
Stack<String> s1=new Stack<>();//s1用于存储运算符
List<String > s2=new ArrayList();//s2用于存储结果
for (String temp:list){
if (temp.matches("\\d+")){//是数字,入s2栈
s2.add(temp);
}else {//非数字(运算符,括号)
//是括号
if (temp.equals("(")){//(,直接入栈
s1.push(temp);
}else if (temp.equals(")")){//),不断s1弹出运算符入s2,直到遇到s1的(为至,并将这一对括号丢弃
while (!s1.peek().equals("(")){
s2.add(s1.pop());
}
s1.pop();//抛弃括号
}else if (s1.isEmpty()){//s1空,直接入栈
s1.push(temp);
}else if (s1.peek().equals("(")){//s1的栈顶为(,直接入栈
s1.push(temp);
}else if (getPriority(temp)>getPriority(s1.peek())){//当前运算符优先级>栈顶优先级,直接入栈
s1.push(temp);
}else {
while (s1.size()!=0&&getPriority(temp)<=getPriority(s1.peek())){
s2.add(s1.pop());
}
s1.push(temp);
}
}
}
//将s1剩余字符串依次加入s2
while (s1.size()!=0){
s2.add(s1.pop());
}
return s2;
}
3,parseSuffix方法需要getPriority方法(获取操作符优先级)支持
//获取优先级
public static int getPriority(String oper){
int value=0;
switch (oper){
case "+":
value=1;
break;
case "-":
value=1;
break;
case "*":
value=2;
break;
case "/":
value=2;
break;
}
return value;
}
4,在主方法编辑测试代码
String str="1+((2+3)*4)-5";//1 2 3 + 4 * + 5 -
List<String> list=getListString(str);//拆分字符串
List<String> suffix=parseSuffix(list);//获取逆波兰表达式
System.out.println(str+"="+calculate(suffix));
1+((2+3)*4)-5=16
【数据结构与算法整理总结目录 :>】<-- 宝藏在此(doge)