ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

数据结构与算法(三)

2021-10-11 15:32:37  阅读:122  来源: 互联网

标签:中缀 压入 s1 运算符 算法 result 数据结构 表达式


在上一篇文章中,我们完整实现了计算器功能,但是还存在一些问题:
解决不支持多位数

不支持多位数的原因就在于:在扫描表达式时,没有考虑多位数的解析。那么思路是:当我们当前的数据是数字的时候,不能直接添加,而是保存到一个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

注意:前面这个表达式是一个中缀表达式,对应的是后面的这个前缀表达式。它的符号出现的顺序与中缀的顺序不一致。

前缀表达式中的符号顺序,就是他求值的规定了

前缀表达式求值过程
  1. 右到左 扫描表达式

  2. 遇到 数字 时,将数字压入堆栈

  3. 遇到 运算符

    弹出栈顶的两个数(栈顶和次顶),用运算符对它们做相应的计算,并将结果入栈。

    计算顺序是: 弹出来的 (运算符) 弹出来的

然后重复以上步骤,直到表达式的最左端,最后运算出的值则是表达式的值。

看完前缀表达式的计算逻辑,那么你要明白的是,从一个 中缀表达式 转换为 前缀表达式 时,优先级顺序是已经处理好的,因为在求值时,不进行优先级的判定

例如:(3+4)x5-6 对应的前缀表达式为:- x + 3 4 5 6,前缀表达式求值步骤如下:

  1. 从右到左扫描,将 6、5、4、3 压入栈

  2. 遇到 + 运算符时:

    将弹出 3 和 4(3 为栈顶元素,4 为次顶元素),计算出 3 + 4 = 7,将结果压入栈

  3. 遇到 x 运算符时

    将弹出 7 和 5,计算出 7 x 5 = 35,将 35 压入栈

  4. 遇到 - 运算符时

    将弹出 35 和 6,计算出 35 - 6 = 29,压入栈

  5. 扫描结束,栈中留下的唯一一个数字 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 + =
后缀表达式求值过程
  1. 左到右 扫描表达式

  2. 遇到 数字 时,将数字压入堆栈

  3. 遇到 运算符

    弹出栈顶的两个数(栈顶和次顶),用运算符对它们做相应的计算,并将结果入栈。

    计算顺序是: 弹出来的 (运算符) 弹出来的

然后重复以上步骤,直到表达式的最右端,最后运算出的值则是表达式的值。

比如:(3+4)x5-6 对应的后缀表达式 3 4 + 5 x 6 -

  1. 从左到右扫描,将 3、4 压入堆栈

  2. 扫描到 + 运算符时

    将弹出 4 和 3,计算 3 + 4 = 7,将 7 压入栈

  3. 将 5 入栈

  4. 扫描到 x 运算符时

    将弹出 5 和 7 ,计算 7 x 5 = 35,将 35 入栈

  5. 将 6 入栈

  6. 扫描到 - 运算符时

    将弹出 6 和 35,计算 35 - 6 = 29,将 29 压入栈

  7. 扫描表达式结束,29 是表达式的值

逆波兰计算器

完成一个逆波兰计算器,需求如下:

  1. 输入一个 逆波兰表达式,使用栈 Stack(JDK 自带),计算器结果

  2. 支持 小括号多位数

    主要这里是讲解数据结构,因此简化为只对整数计算

注意咯:知道逆波兰表达式是什么后,相对来说,实现还是比较容易的,难点其实是在于中缀表达式如何转换为后缀表达式,因为这涉及到运算符的优先级问题。在前面我们实现的时候,其实就是这个运算符优先级问题很刺手。而前缀、后缀表达式里面表达式的优先级在形成表达式时就已经确定了。

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());
    }
}

中缀表达式转后缀表达式

通过前面的逆波兰计算器的代码实现,可以看到:后缀表达式对于计算机来说很方便,但是对于人类来说,后缀表达式却不是那么容器写出来的。

中缀表达式转后缀表达式步骤:

  1. 初始化两个栈:

    • 运算符栈:s1
    • 中间结果栈:s2
  2. 从左到右扫描中缀表达式

  3. 遇到操作数时,将其压入 s2

  4. 遇到运算符时

    比较 它 与 s1 栈顶运算符的优先级:

    1. 如果 s1 为空,或则栈顶运算符号为 ( ,则将其压入符号栈 s1
    2. 如果:优先级比栈顶运算符 ,也将其压入符号栈 s1
    3. 如果:优先级比栈顶运算符 低 或 相等,将 s1 栈顶的运算符 弹出,并压入到 s2 中

    再重复第 4.1 步骤,与新的栈顶运算符比较(因为 4.3 将 s1 栈顶运算符弹出了)

    这里重复的步骤在实现的时候有点难以理解,下面进行解说:

    1. 如果 s1 栈顶符号 优先级比 当前符号 高或则等于,那么就将其 弹出,压入 s2 中(循环做,是只要 s1 不为空)

      如果栈顶符号为 (,优先级是 -1,就不会弹出,就跳出循环了

    2. 跳出循环后,则将当前符号压入 s1

  5. 遇到括号时:

    1. 如果是左括号 ( :则直接压入 s1

    2. 如果是右括号 )

      则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到 左括号 为止,此时将这一对括号 丢弃

  6. 重复步骤 2 到 5,直到表达式最右端

  7. 将 s1 中的运算符依次弹出并压入 s2

  8. 依次弹出 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 -

在学习和使用上有两个层次:

  1. 应用层次:别人发明出来的东西,你学习、理解它,并灵活运用它
  2. 自创:你自己发明一个东西出来,并使用它

那么这里的中缀转后缀表达式的思路步骤,则属于第一个层次,相关的计算机专家之类的,发明出来了。我们要理解它并灵活运用它。等你能力达到一定层度时,有可能发明出来一个算法。

再比如:绝世武功 -> 降龙十八掌,别人已经创造出来了,你不去学习理解它,如何加以改进并自创?如果没有人教你,你怎么能学会降龙十八掌?

逆波兰表达式计算器代码实现
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;
    }
}

标签:中缀,压入,s1,运算符,算法,result,数据结构,表达式
来源: https://www.cnblogs.com/wyzstudy/p/15393531.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有