作者前言

大家好,我是阿濠,今篇内容跟大家分享的是数据结构之栈,很高兴分享到segmentfault与大家一起学习交流,初次见面请大家多多关照,一起学习进步.

一、什么是栈

首先输入一个表达式:[7*2*2-5+1-5+3-3]

图片.png

请问:结果是多少?

请问:计算机底层是如何运算得到结果的?(注意不是简单的把算式列出运算)

因为我们看这个算式7*2*2-5+1-5+3-3,但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串)

这就需要涉及到

栈的介绍

举个例子:就像叠盘子 一样,后放的盘子总是在上面,拿盘子时是从上面拿,也就是先拿后来放上面的盘子,最后的盘子是最早放的。

1.栈的英文为stack
2.栈是一个先入后出(FILO-First-In-Last-Out)的有序列表
3.栈是限制线性表中,元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top)另一端为固定的一端,称为栈底(Bottom)
4.根据栈的定义可知,最先放入栈中元素在栈底最后放入的元素在栈项,而删除元素刚好相反最后放入的元素最先删除最先放入的元素最后删除
5.出栈(pop)入栈(push)的概念(示意图)

图片.png

图片.png

栈的应用场景

1) 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
2)处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数区域变量等数据存入堆栈中。
3)表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
4)二叉树遍历
5)图形的深度优先(depth- first)搜索法

栈的插入和删除操作

因为栈的本质是线性表,线性表有两种存储方式,所以栈也分两种存储方式分别为栈的顺序存储结构、栈的链式存储结构

栈的插入操作(Push)叫做进栈,也称为入栈,对于顺序栈的进栈操作只需将新的数据元素存入栈内,然后让记录栈内元素个数的变量加1,程序即可再次通过arr\[size-1\]重新访问新的栈顶元素。进栈操作示意图如下:

由于顺序栈底层通常会采用数组来保存数据元素,因此可能出现的情况是:当程序试图让一个数据元素进栈时,底层数据已满,那么就必须扩充底层数组的长度来容纳新进栈的数据元素。

栈的删除操作(Pop)叫做出栈,对于顺序栈的出栈操作而言,需要将栈顶元素弹出栈,程序要做两件事。

  • 让记录栈内元素个数的变量减1.
  • 释放数组对栈顶元素的引用。

对于删除操作来说,只要让记录栈内元素个数的size减1,程序即可通过arr\[size-1\]`访问新的栈顶元素。但不要忘记释放`原来栈顶的数组引用,否则会引起内存泄漏

栈比普通线性表的功能更弱,栈是一种被限制过的线性表,只能从栈顶插入,删除数据元素

栈的链式存储结构及实现

可以采用单链表来保存栈中所有元素,这种链式结构的栈也被称为栈链。对于栈链而言,栈顶元素不断地改变,程序只要使用一个top引用来记录当前的栈顶元素即可。

链式的进栈操作,只需要做几件件事:

  • 让top引用指向新元素添加的元素
  • 新元素的next引用指向原来的栈顶元素。
  • 让记录栈内元素个数的size变量加1.

链式的出栈操作,只需要做几件件事:

  • 释放原来的栈顶元素
  • 让top引用指向原栈顶元素的下一个元素
  • 让记录栈内元素个数的size变量减1.

从空间利用率的角度说,链栈的空间利用率比顺序栈的空间利用率要高一些。

二、栈的快速入门

使用数组模拟栈(示意图)

图片.png

思路分析:
1.使用数组ArrayList模拟栈操作
2.定义Top 指示为栈顶,初始化为-1
3.入栈的操作:当有数据加入到栈时,top++; stack[top]= data;
4.出栈的操作,int value = stack[top]; top--, return valuel
5.栈满:top == maxSize-1
6.栈空:top == -1

使用ArrayStack表示栈 代码如下:

class ArrayStack{

    private int maxSize;//栈的大小
    private int[] stack;//数组,使用数组模拟栈,数据放入数组
    private int top=-1;//top表示栈顶 初始话为-1

    public ArrayStack(int maxSize) {
        this.maxSize = maxSize;
        stack =new int[this.maxSize];
        this.top = -1;
    }

    //栈满
    public boolean isFu1l() {
        return top == maxSize - 1;
    }
    //栈空
    public boolean isEmpty(){
        return top == -1;
    }
    //入栈
    public void push(int value) {
        //先判断栈是否满
        if(isFu1l()) {
            System.out.println("栈满");
            return;
        }
            //栈顶++指向新数据
            stack[++top] = value;
    }
    //出栈 将栈顶数据返回
    public int pop() {
        //先判断栈是否空
        if(isEmpty()) {
            //抛出异常
            throw new RuntimeException("栈空, 没有数据~");
        }
        //临时保存栈顶数据
        int value = stack[top];
        top--;//栈顶--指向原栈顶后一元素
        return value;//返回临时保存栈顶的数据
    }
    //显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据
    public void list() {
        //判断栈是否为空,为空无法遍历
        if(isEmpty()) {
            System .out.println("栈空,没有数据~~");
            return;
        }
        //需要从栈顶开始显示数据
        for(int i=top;i>=0;i--){
            System.out .printf("stack[%d]=%d\n", i, stack[i]);
        }
    }
}

添加数据测试ArrayStack方法操作

 //先创建-个ArrayStack对象->表示栈
ArrayStack stack = new ArrayStack(4);
System.out.println("入栈数据:10、4、3、2");
stack.push(10);
stack.push(4);
stack.push(3);
stack.push(2);
System.out.println("当前栈的数据-----------");
stack.list();
System.out.println("栈顶出栈、当前栈的数据-----------");
stack.pop();
stack.list();


运行结果如下:
入栈数据:10、4、3、2
当前栈的数据-----------
stack[3]=2
stack[2]=3
stack[1]=4
stack[0]=10
栈顶出栈、当前栈的数据-----------
stack[2]=3
stack[1]=4
stack[0]=10

栈的经典使用场景浏览器的前进、后退功能

  • 使用两个栈X和Y,首次浏览的界面入X栈。
  • 后退时,依次从X中出栈,并将出栈的数据依次放入Y栈
  • 前进时,从Y栈中出栈,在依次入X栈。
  • 当X栈中没有数据时,说明不能后退了;
  • 当Y栈中没有数据时,说明不能前进了。

比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈

这个时候,两个栈的数据就是这个样子:

当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。

这个时候,两个栈的数据就是这个样子:

这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。

此时两个栈的数据是这个样子:

这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。

此时两个栈的数据这个样子:

三、使用栈实现综合计算器(中缀表达式)

使用栈完成表达式计算:7*2*2-5+1-5+3-3

提示:使用两个栈完成计算
图片.png

思路分析:
1.通过值index(索引)遍历表达式字符串
2.若扫描到是数字,则直接放入数栈numStack
3.若扫描到是符号分以下情况:
符号栈为空则直接入栈
➢如果符号栈有操作符,就进行比较,如果当前的操作符优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,并从符号栈中pop出一个符号进行运算,将得到结果放入数栈,然后将当前的操作符入符号栈
➢如果当前的操作符优先级大于栈中的操作符,就直接入符号栈
4.当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行.
5.最后在数栈只有一个数字,就是表达式的结果

问题简单化

一、计算表达式:3+2*6-2

根据思路放入扫描并将数字与符号放入栈中

图片.png

此时下一扫描到的符号是 * ,它比 + 高,按照思路当前符号优先级高于栈中操作符,直接入栈

图片.png

此时下一扫描到的符号是 - ,它比 * 低,按照思路当前符号优先级小于或者等于栈中的操作符,需要从数栈中pop出两个数,并从符号栈中pop出一个符号进行运算,将得到结果放入数栈,然后将当前的操作符入符号栈

图片.png

此时下一位扫到的是数字 2 将它直接放入数栈,此时表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号
图片.png
图片.png

我们根据之前数组模拟栈的代码进行修改实现思路图,编码如下:

class ArrayStack2{

    private int maxSize;//栈的大小
    private int[] stack;//数组,使用数组模拟栈,数据放入数组
    private int top=-1;//top表示栈顶 初始话为-1

    public ArrayStack2(int maxSize) {
        this.maxSize = maxSize;
        stack =new int[this.maxSize];
        this.top = -1;
    }
    //直接返回栈顶数据
    public int peek(){
        return stack[top];
    }
    //栈满
    public boolean isFu1l() {
        return top == maxSize - 1;
    }
    //栈空
    public boolean isEmpty(){
        return top == -1;
    }
    //入栈
    public void push(int value) {
        //先判断栈是否满
        if(isFu1l()) {
            System.out.println("栈满");
            return;
        }
        //栈顶++指向新数据
        stack[++top] = value;
    }
    //出栈 将栈顶数据返回
    public int pop() {
        //先判断栈是否空
        if(isEmpty()) {
            //抛出异常
            throw new RuntimeException("栈空, 没有数据~");
        }
        //临时保存栈顶数据
        int value = stack[top];
        top--;//栈顶--指向原栈顶后一元素
        return value;//返回临时保存栈顶的数据
    }
    //显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据
    public void list() {
        //判断栈是否为空,为空无法遍历
        if(isEmpty()) {
            System .out.println("栈空,没有数据~~");
            return;
        }
        //需要从栈顶开始显示数据
        for(int i=top;i>=0;i--){
            System.out .printf("stack[%d]=%d\n", i, stack[i]);
        }
    }
}

并可以进行自定义运算符与自定义优先级,编码如下:

//返回运算符的优先级,优先级是程序员来确定,优先级使用数字表示
//数字越大,则优先级就越高.
public int priority(int oper) {
    if(oper == '*' || oper == '/'){
        return 1;
    } else if (oper == '+'| oper == '-') {
        return 0;
    } else {
        return -1; //假定目前的表达式只有+,- ,*,/
    }
}
//判断是不是一-个运算符
public boolean is0per(char val) {
    return val == '+'|| val == '-' || val == '*' || val == '/';
}
//计算方法
public int cal(int num1, int num2 , int oper) {
    int res = 0; //res 用于存放计算的结果
    switch (oper) {
        case '+':
            res = num1 + num2;
            break;
        case '-':
            res = num2 - num1;//注意顺序
            break;
        case '*':
            res = num1 * num2;
            break;
        case '/':
            res = num2 / num1;
            break;
        default:
            break;
    }
    return res;
}

进行实验添加测试数据看看效果

//根据前面思路,完成表达式的运算
String expression = "3+2*6-2";

//创建两个栈,数栈,一个符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);

//定义需要的相关变量
int index = 0;//用于扫描表达式里的每一位字符
int num1 = 0;
int num2 = 0;
int oper = 0;//用于存放运算符级别
int res = 0;//用于存放表达式结果
char ch = ' '; //将每次扫描得到char保存到ch

//开始while循环的扫描expression
while (true) {
    //依次得到expression的每一个字符
    //比如说获取表达式'3' 是字符串'3' 将它转换成char
    ch = expression.substring(index, index + 1).charAt(0);
    //判断ch是什么,然后做相应的处理
    //如果是运算符
    if (operStack.is0per(ch)) {
        //断当前的符号栈是否为空 如果符号栈有操作符,就进行比较
        if( !operStack.isEmpty()) {
            //如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,
            //在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈
            //operStack. priority(ch) 获取运算符的优先级别 0/1
            if(operStack. priority(ch) <= operStack. priority(operStack . peek())) {
                //从数栈中pop出两个数
                num1 = numStack.pop();
                num2 = numStack.pop();
                //从符号栈中pop出一个符号
                oper = operStack. pop();
                //进行运算
                res = numStack.cal(num1, num2, oper);
                //把运算的结果入数栈
                numStack. push(res);
                //然后将当前的操作符入符号栈
                operStack. push(ch);
            } else {
                //如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈。
                operStack. push(ch);
            }
        }else {
            //如果符号栈没有操作符直接入栈
            operStack.push(ch);
        }
    }else{
        //如果是数字 放入数栈
        numStack.push(ch - 48 );
    }
    //让index + 1,并判断是否扫描到expression最后.
    index++;
    if (index >= expression.length()) {
        //证明已扫描完毕退出循环结束
        break;
    }
}
//当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行。
while(true) {
    //如果符号栈为空,则计算到最后的结果,数栈中只有一个数字[结果]
    if(operStack. isEmpty()) {
        break;
    }
    num1 = numStack. pop();
    num2 = numStack. pop();
    oper = operStack. pop();
    //计算最终结果
    res = numStack.cal(num1, num2, oper);
    numStack.push(res);//将结果放入数栈中
}
System. out. printf("表达式%s = %d", expression, numStack . pop());

有人就会好奇数字放入数栈时push( ch - 48 ),为什么-48?
因为此时的ch = expression.substring( index , index + 1).chatAt(0)
表达式里的1 是'1' 而对应的ASCII码是49,刚好相差48

图片.png

那么运行起来的结果如下:

表达式 3 + 2*6 - 2 = 13

那么我们使用中缀表达式计算时还是有些问题的,比如说无法进行表达式:70-2*6+1

问题分析并优化

1.多位数无法进行计算
2.逻辑判断时只要是一个数字就入栈,如果是多位数则无法完整入栈
3.index处理时需要判断当前数字后面是数字还是符号
4.定义变量用于拼接当前数字后面的数字,入栈后清空
5.若index是最后一位则无需操作

//如果是数字 放入数栈
//numStack.push(ch - 48 );
//while循环外声明拼接变量
String keepNum="";

//分析思路
//1.当处理多位数时,不能发现是一个数就立即入栈,因为他可能是多位数
//2.在处理数,需要向expression的表达式的index后再看一位,如果是数就进行扫描,如果是符号才入栈
//3.因此我们需要定义一个变量字符串,用于拼接
//处理多位数
keepNum += ch;

//如果ch已经是expression的最后一位,就直接入栈
if (index == expression.length() - 1) {
    numStack.push(Integer .parseInt(keepNum));
}else {
    //判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
    //注意是看后一位,不是index++
    if (operStack.is0per(expression.substring(index + 1, index + 2).charAt(0))) {
        //如果后一位是运算符,则入栈keepNum = "1"或者"123"
        numStack.push(Integer.parseInt(keepNum));
        //重要的!!!!!,keepNum清空
        keepNum = "";
    }
}
表达式 30+ 2*6 - 2 = 40
表达式 7*2*2-5+1-5+3-3 =18

四、前缀、中缀、后缀表达式(逆波兰表达式)

前缀表达式(波兰表达式)

1)前缀表达式又称波兰表达式前缀表达式运算符位于操作数之前
2)举例说明: (3+4)* 5 - 6 对应的前缀表达式就是 - x + 3 4 5 6

前缀表达式的计算机求值

从右至左扫描表达式,遇到数字时,将数字压入堆栈
➢遇到运算符时,弹出栈顶两个数,用运算符对它们做相应的计算(栈顶元素和次项元素),并将结果入栈
重复上述过程直到表达式最左端最后运算得出的即为表达式的结果

比如计算:(3+4)x 5 - 6 对应的表达式: - x + 3 4 5 6 计算机求值步骤:
1.从右至左扫描,将6、5、4、3压入堆栈
2.遇到+运算符,因此弹出3和4 (3为栈顶元素,4为次项元素),计算出3+4的得7,再将7入栈
3.接下来是 * 运算符,因此弹出7和5, 计算出7 x 5=35,将35入栈
4.最后是 - 运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式(常见表达式)

1)中缀表达式就是常见运算表达式,如:(3+4)X5-6
2)中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作比如说有小括号先计算,没有按优先级计算
3)因此在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)

后缀表达式(逆波兰表达式)

1)后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
2)中缀表达式举例说明: (3+4) x 5 - 6 对应的后缀表达式就是 34 + 5 x 6 -

图片.png

后缀表达式的计算机求值

从左至右扫描表达式,遇到数字时,将数字压入堆栈
➢遇到运算符时,弹出栈项两个数,用运算符对它们做相应的计算(次顶元素和栈项元素),并将结果入栈
重复上述过程直到表达式最右端最后运算得出的即为表达式的结果

例如: (3+4)* 5-6 对应的后缀表达式就是 3 4 + 5 x 6 -,计算机求值步骤:
1.从左至右扫描,将3和4压入堆栈;
2.遇到+运算符, 因此弹出4和3 (4为栈项元素,3为次顶元素),计算出3+4的得7,再将7入栈、5入栈;
3.接下来是*运算符,因此弹出5和7,计算出7x5=35,将35入栈、6入栈;
4.最后是 -运算符,计算出35-6的值, 即29, 由此得出最终结果

对比前缀表表达式与后缀表达式,前缀表达式最后-运算35是栈顶(上边),后缀表达式35是栈底(下边),不同表达式减数与被减数顺序不一样,需要小心使用

五、使用栈实现综合计算器(后缀表达式)

完成一个逆波兰计算器 要求如下:
1)输入一个逆波兰表达式(后缀表达式),使用栈(Stack),计算其结果
2)支持小括号和多位数整数

(讲的是数据结构,因此计算器进行简化,只支持对整数的计算。)

代码如下:

//先定义给逆波兰表达式
//(3+4)*5-6 => 34 + 5 x 6 -
//说明为了方便,逆波兰表达式的数字和符号使用空格隔开
String suffixExpression ="3 4 + 5 x 6 -";

波兰计算器代码如下:

public class PolandNotation {
   
    //将一个逆波兰表达式,依次将数据和运算符放入到ArrayList中
    public static List<String> getListString(String suffixExpression) {
        //将suffixExpression分割空格
        String[] split = suffixExpression.split(" ");
        List<String> list = new ArrayList<String>();
        for (String ele : split) {
            list.add(ele);
        }
        return list;
    }
    
    //完成对逆波兰表达式的运算
    /**
     * 1)从左至右扫描,将3和4压入堆栈;
     * 2)遇到+运算符,因此弹出4和3 (4为栈顶元素, 3为次顶元素)
     * 3)计算出3+4的值,得7,再将7入栈;将5入栈;
     * 4)接下来是x运算符,因此弹出5和7,
     * 5)计算出7x5=35,将35入栈;将6入栈;
     * 6)最后是-运算符,计算出35-6的值,即29,由此得出最终结果
     * */
    public static int calculate(List<String> is) {
        //创建给栈,只需要一个栈即可
        Stack<String> stack = new Stack<String>();
        //遍历is
        for(String item: is) {
            //这里使用正则表达式来取出数
            //匹配的是多位数
            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 (item.equals("+")) {
                    res = num1 + num2;
                } else if (item.equals("-")) {
                    res = num1 - num2;
                } else if (item. equals("x")) {
                    res = num1 *
                            num2;
                } else if (item. equals("/")) {
                    res = num1 / num2;
                } else {
                    throw new RuntimeException( "运算符有误" );
                }
                //把res入栈
                stack.push("" + res);

            }
        }
        //最后栈中留下的是最后的结果 直接返回栈顶数据
        return Integer.parseInt(stack.pop());
    }
}

运行操作如下:

//1. 先将"34+5x6-"=>放到ArrayList中
//2.将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
//将字符串放入ArrayList直接遍历ArrayList即可
//以前是扫描字符串,现在直接遍历ArrayList即可
List<String> list = getListString(suffixExpression);
System .out. println("rpnList=" + list);
int res = calculate(list);
System.out.print("结果="+res);

运行结果如下:
rpnList=[3, 4, +, 5, x, 6, -]
结果=29

我们发现后缀表达式更于简单些,那么如何将我们人平常使用的中缀表达式转为后缀表达式呢?

六、将中缀表达式转后缀表达式

具体步骤如下:
➢初始化两个栈:运算符栈s1和储存中间结果的栈s2;
从左至右扫描中缀表达式;
操作数时,将其压s2;
运算符时,比较其与s1栈项运算符优先级:
1.如果s1为空或栈顶运算符为左括号“(", 则直接将此运算符入栈;
2.否则,若优先级栈顶运算符,也将运算符压入s1;
3.否则,将s1栈项的运算符弹出压入s2中,再次转到第一点s1新的栈项运算符比较;
➢遇到括号时:
1.如果是左括号(”, 则直接压入s1
2.如果是右括号)”, 则依次弹出s1栈项的运算符,并压入s2直到遇到左括号为止,此时将这一对括号丢弃
重复步骤直到表达式最右边
➢将s1中剩余的运算符依次弹出并压入s2
依次弹出s2中的元素并输出结果逆序即为中缀表达式对应的后缀表达式

举例说明:
将中缀表达式“1+((2X3)X4)-5”转换为后缀表达式"123+4 X + 5 -"

按照思路进行示意图解

1.初始化两个栈运算符栈s1和储存中间结果的栈s2;
图片.png
2.从左至右扫描中缀表达式;操作数时,将其压s2;运算符时,比较其与s1栈项运算符优先级:
图片.png
3.遇到括号时:如果是左括号(”, 则直接压入s1,如果是右括号)”, 则依次弹出s1栈项的运算符,并压入s2直到遇到左括号为止
图片.png
4.重复步骤直到表达式最右边,此时遇到运算符+,比较优先级,如果s1为空或栈顶运算符为左括号“(", 则直接将此运算符入栈;
图片.png
5.重复步骤直到表达式最右边,此时遇到运算符),依次弹出s1栈项的运算符压入s2,直到遇到左括号为止,此时将这一对括号丢弃,相当于要把一对()消除掉
图片.png
6.重复步骤直到表达式最右边,此时遇到运算符*比较其与s1栈项运算符优先级,如果s1为空或栈顶运算符为左括号“(", 则直接将此运算符入栈;
图片.png
7.重复步骤直到表达式最右边,此时遇到运算符),依次弹出s1栈项的运算符压入s2,直到遇到左括号为止,此时将这一对括号丢弃,相当于要把一对()消除掉
图片.png
8.重复步骤直到表达式最右边,此时遇到运算符-,此时操作符 + 与操作符 - 一个级别,满足将s1栈项的运算符弹出压入s2中,再次转到s1新的栈项运算符比较;
图片.png
9.此时回到如果s1为空或栈顶运算符为左括号“(", 则直接将此运算符入栈;
图片.png
10.重复步骤直到表达式最右边,此时遇到运算符5将其压s2;
图片.png
11.将s1中剩余的运算符依次弹出并压入s2
图片.png
12.依次弹出s2中的元素并输出结果逆序即为中缀表达式对应的后缀表达式

将S2出栈:- 5 + * 4 + 3 2 1 逆序即为中缀表达式对应的后缀表达式所以结果=1 2 3 + 4 * + 5 -

图片.png

那么我们是怎么知道中缀表达式是使用这个思路呢?

其实这个思路是相对应的设计者与相关的专家告诉我们的,我们进行运用,但是我们能不能自己设计一个思路或算法呢?

举个例子:
有一门武功降龙十八掌秘籍,你是不是可以
1.按照秘籍与心法,进行学习运用
2.创造属于自己的降龙十八掌秘籍

如果是创造属于自己的降龙十八掌那么这个难度是不言而喻的,算法其实也是相对应的道理。

学习算法:
1.理解算法,进行灵活运用解决问题
2.设计算法,解决问题

设计成熟的算法需要很强的数学功底与计算机理解深度,但是运用前人的经验就是我们目前需要做的事情。

代码实现中缀表达式转后缀表达式

方法1:将中缀表达式转成对应的List
即"1+((2+3)x4)-5"转换成ArrayList[1,+,((,2,+,3,),x,4,),-,5];

public static List<String> toInfixExpressionList(String s) {
    //定义一个List,存放中缀表达式对应的内容
    List<String> ls = new ArrayList<String>();
    int i = 0; //这时是一个指针,用于遍历中缀表达式字符串
    String str; //对多位数的拼接
    char c; //每遍历到一个字符,就放入到c
    do {
        //如果c是-个非数字,我需要加入到1s
        //数字0-9 的ASCII码是 48 - 57
        //大于则不是数字,将他放入1s
        if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
            ls.add("" + c);
            i++; //i需要 后移
        } else {
            //如果是一个数,需要考虑多位数
            str = ""; //先将str. 置成""
            //'0'[48]->'9'[57] 大于并小于57 是数字
            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;
}

//中缀表达式转后缀表达式
//完成将一个中缀表达式转成后缀表达式的功能
//说明
//目标:1+((2+3)x4)-5 =>转成1 2 3 + 4 x + 5 -
//1.因为直接对str进行操作,不方便,因此先将“1+((2+3)x4)-5" =》中缀的表达式对应的List
//即"1+((2+3)x4)-5" => ArrayList [1,+,(,(,2,+,3,),x,4,),-,5]
String str ="1+((2+3)x4)-5";
List<String> list=toInfixExpressionList(str);
System.out.println("中缀表达式转后缀表达式:"+list);

运行结果如下:
中缀表达式转后缀表达式:[1, +, (, (, 2, +, 3, ), x, 4, ), -, 5]

方法2:将ArrayList [1,+,(,(,2,+,3,),x,4,),-,5]转成1 2 3 + 4 x + 5 -
按照思路开始定义两个栈,但是s2这个栈因为在整个转换过程,没有pop操作,而最后步骤需要逆序输出比较麻烦,所以直接使用ArrayList也可以

//编写一个类Operation可以返回-一个运算符对应的优先级
class Operation {

    private static final int ADD = 1;//加
    private static final int SUB = 1;//减
    private static final int MUL = 2;//乘
    private static final int DIV = 2;//除

    //写一个方法,返回对应的优先级数字
    public static int getValue(String operation) {
        int result = 0;
        if ("+".equals(operation)) {
            result = ADD;

        } else if ("-".equals(operation)) {
            result = SUB;

        } else if ("x".equals(operation)) {
            result = MUL;

        } else if ("/".equals(operation)) {
            result = DIV;

        } else {
            System.out.println("不存在该运算符");

        }
        return result;
    }
}

中缀表达式转后缀表达式代码如下:

//即ArrayList [1,+,(,(,2,+,3,),x,4,),-,5] => ArrayList [1,2,3,+,4,x,+,5,-]
//方法:将得到的中缀表达式对应的List =>后缀表达式对应的List
public static List<String> parseSuffixExpreesionList(List<String> ls) {
    //定义两个栈
    Stack<String> s1 = new Stack<String>(); // 符号栈
    //说明:因为s2这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
    //因此比较麻烦,这里我们就不用Stack<String>直接使用List<String> s2
    //Stack<String> s2 = new Stack<String>(); //储存中间结果的栈s2
    List<String> s2 = new ArrayList<String>(); //储存中间结果的Lists2
    //遍历1s
    for (String item : ls) {
        // 按照思路分析
        // 如果是一个数,加入s2
        if (item.matches("\\d+")) {
            s2.add(item);
        } else if (item.equals("(")) {
            //遇到括号时:如果是左括号“(”, 则直接压入s1
            s1.push(item);
        } else if (item.equals(")")) {
            //如果是右括号“)”
            //直到遇到左括号为止,此时将这一对括号丢弃
            while (!s1.peek().equals("(")) {
                //依次弹出s1栈项的运算符,并压入s2
                s2.add(s1.pop());
            }
            //将小括号 ( 弹出 此时将这一对括号丢弃
            s1.pop();
        } else {
            //遇到运算符时,比较其与s1栈项运算符的优先级:
            //1.如果s1为空,或栈顶运算符为左括号“(", 则直接将此运算符入栈;
            //2.否则,若优先级比栈顶运算符的高,也将运算符压入s1;
            //3.否则,将s1栈项的运算符弹出并压入到s2中,再次转到第一点与s1中新的栈项运算符相比较;
            while( s1.size()!=0 && !s1.peek().equals("(") && Operation.getValue(s1.peek())>=Operation.getValue(item)){
                //将s1栈项的运算符弹出并压入到s2中
                s2.add(s1.pop());
            }
            s1.push(item);
        }
    }
    //将S1中剩余得运算符依次弹出降入S2
    while(s1.size()!=0){
        s2.add(s1.pop());
    }
    return s2;//注意因为是存放ArrayList中,正常输出则是逆波兰表达式
}

我们加上使用后缀表达式计算器的方法测试一下数据

String str ="1+((2+3)x4)-5";
List<String> infixExpressionList=toInfixExpressionList(str);
System.out.println("中缀表达式对应的List:"+infixExpressionList);
List<String> suffixExpreesionList=parseSuffixExpreesionList(infixExpressionList);
System.out.println("后缀表达式对应的List:"+suffixExpreesionList);
System.out.printf("expression=%d",calculate(suffixExpreesionList));

运算结果如下:
中缀表达式对应的List:[1, +, (, (, 2, +, 3, ), x, 4, ), -, 5]
后缀表达式对应的List:[1, 2, 3, +, 4, x, +, 5, -]
expression=16

28640
116 声望25 粉丝

心有多大,舞台就有多大