数据结构的运用:用栈结合逆波兰表达式实现一个计算器

数据结构的运用:用栈结合逆波兰表达式实现一个计算器

  • 需要用到栈中的方法有
  • 什么是逆波兰表达式?
    • 前缀表达式
    • 中缀表达式
    • 后缀表达式
  • 计算机是怎么操作后缀表达式的呢?
    • 思路分析
    • 代码实现
  • 如何将中缀表达式转化为后缀表达式
    • 思路分析
    • 代码实现
  • 完整代码
    • 计算逆波兰表达式的完整代码
    • 如何将中缀表达式转化为后缀表达式完整代码

需要用到栈中的方法有

  1. pop(); //出栈
  2. push(); //入栈
  3. peek(); //查看栈顶元素

什么是逆波兰表达式?

逆波兰表达式,又名后缀表达式。与之对应的还有前缀表达式,中缀表达式。

前缀表达式

前缀表达式在本案例中无应用,所以就稍微带过一下。
前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
举例说明:(3+4)*5-6对应的前缀表达式:-+3456;

中缀表达式

中缀表达式是我们日常生活中用的最多的表达式。
(3+4)*5-6就是一个中缀表达式。中缀表达式适合人们阅读求解,但这并不适合计算机操作,因此诞生了后缀表达式。

后缀表达式

就像标题写的,后缀表达式是实现这个计算器功能的核心。
后缀表达式的特点为运算符在操作数的后面(注意:括号不算运算符,在后面会讲到)
例如:(3+4)* 5-6的后缀表达式是:3 4 + 5 * 6 -

计算机是怎么操作后缀表达式的呢?

在这里先给出一个逆波兰表达式用于举例:3 4 + 5 * 6 -
实现这个功能需要一个栈,我们用Java中提供的Stack创建

//这里stack的泛型采用String类型,是因为提供的是表达式为String类型:"3 4 + 5 * 6 -"
Stack<String> stack = new Stack<>();

思路分析

  1. 计算机从左自有扫描逆波兰表达式,依次得到[3, 4, +, …]
  2. 遇到数字时,依次将数字压入栈
  3. 遇到运算符时,依次弹出栈中的两个元素,记为栈顶元素num1,次顶元素num2,并执行如num2 + num1的操作
  4. 将num2 + num1算出的答案重新压入栈中
  5. 重复1 → 4的操作,直至表达式扫描结束,最后的栈顶元素就是该逆波兰表达式计算所得到的答案

注意,3中运算顺序必须是:次顶元素 运算符 栈顶元素。 因为当运算符为"-“或者”/"时,由于表达式的扫描方向的原因,被减(除)数总是在减(除)数的下方,即栈顶元素总是减(除)数,次顶元素总是被减(除)数。另外,加法和乘法不受此影响,但为了方便记忆,统一使用该运算顺序。

代码实现

Tips:本处代码由于方法不在同一片代码片中,代码结构在阅读起来也许有些许乱,因此在文末提供了整合过后的完整代码。

本例采用Java语言,但其思想是一样的。

首先我们需要一个方法,用于将原字符串转换为一个存储了所有元素的集合

public static ArrayList<String> getList(String notation) {
    //定义一个集合来存放rpn中的各个元素,采用split方法分割字符串
    ArrayList<String> list = new ArrayList<>();
    String[] splitArray = notation.split(" ");
    //使用for循环,依次将splitArray中的元素添加至集合
    for(String element : splitArray) {
        list.add(element);
    }
    return list;
}

我们还需要一个方法,用于计算逆波兰表达式,其逻辑关系就是上面的思路分析

public static int calculate(ArrayList<String> list) {
    //创建一个栈
    Stack<String> stack = new Stack<>();
    //遍历list集合
    for(String item : list) {
        //采用正则表达式来判断item为多位数还是符号
        if (item.matches(("\\d+"))) {
            //item为多位数,入栈
            stack.push(item);
        } else {
            //item是运算符,执行思路分析中2 -> 4的操作
            int num1 = Integer.parseInt(stack.pop());
            int num2 = Integer.parseInt(stack.pop());
            int res = 0;
            switch (item){
                case "+":
                    res = num1 + num2;
                    break;
                case "-":
                    res = num2 - num1;
                    break;
                case "*":
                    res = num2 * num1;
                    break;
                case "/":
                    res = num2 / num1;
                    break;
                default:
                    throw new RuntimeException("运算符错误");
            }
            //将结果入栈
            stack.push("" + res);
        }
    }
    //for循环执行完后,stack栈栈顶就是答案
    return Integer.parseInt(stack.pop());
}

有了这两个方法,就可以完成对逆波兰表达式的计算
让我们来写一下主函数

变量命名需遵守命名规范。英语不好的同学在阅读复杂的变量名时会产生遗忘、混淆。
因此在此处提供变量名命名注解,供英语不好的同学使用。
rpn:Reverse Polish Notation 逆波兰表达式

public static void main(String[] args) {
    //提供一个逆波兰表达式。为了方便计算机扫描。在每个元素中间插入空格
    String rpn = "3 4 + 5 * 6 -";
    //分割rpn字符串
    ArrayList list = getList(rpn);
    //计算集合list,得到结果
    int result = calculate(list);
    //输出验证
    System.out.println(rpn + "的结果是:" + result);
}

如何将中缀表达式转化为后缀表达式

虽然后缀表达式是最适合计算机求解的表达式,但它不适合人们阅读,人们更喜欢中缀表达式,因此,需要有一些方法能够将中缀表达式转化为后缀表达式。

思路分析

和上面一样,默认给出一个中缀表达式:(3+4)* 5-6

  1. 需要两个栈,第一个栈是用于存储运算符的栈1,第二个栈是用于中间结果的栈2
  2. 从左自右扫描表达式
  3. 遇到操作数时,压入栈2
  4. 遇到操作符时:
    4.1 如果栈1空或者栈1的栈顶元素为"(",则直接入栈
    4.2 否则,如果操作符优先级比栈顶元素优先级高,也直接入栈
    4.3 否则,弹出栈1的栈顶元素,压入栈2,回到4-1继续与栈1的栈顶元素比较
  5. 遇到括号时:
    5.1 如果是"(",则直接入栈1
    5.2 如果是")",则依次弹出栈1栈顶元素并压入栈2,直至遇到"("为止。此时这对括号将被丢弃。重复2 -> 5的操作,直至表达式最右边
  6. 依次将栈1剩余的元素压入栈2
  7. 将栈2逆序输出即为该中缀表达式对应的后缀表达式

这里将括号丢弃的原因,就是在上文中提到的,括号不算运算符

对上述逻辑思路进行进一步分析,发现用于存储中间结果的栈2从始至终没有进行pop操作,并且在最后输出时,由于栈结构的特点,必须采用逆序输出,这很不方便。因此,将栈2用ArrayList来代替,即解决了逆序输出困难的特点,又实现了原来的要求。

代码实现

同操作后缀表达式一样,我们在一开始需要一个中缀表达式,并且中间用空格隔开,用于简化操作(当然,不隔开也可以,添加一个辅助索引index来逐步判断是否为多位数数字)。

infixNotation:中缀表达式
suffixNotaion: 后缀表达式
parse: 转换

//提供一个中缀表达式
String infixNotation = "1 + ( ( 2 + 3 ) * 4 ) - 5";
//利用上面计算逆波兰表达式中构写的getList方法分割字符串
ArrayList<String> list = getList(infixNotation);

在获取到相应的list集合后,我们需要一个方法来解决如何将中缀表达式转化为后缀表达式的问题。
由于4.2,4.3的原因,在写上述方法之前我们需要另一个方法来比较运算符的优先级大小

public static int getPriority(String operation) {
    //数字越大,运算符优先级越高
    switch(operation) {
        case "+":
            return 1;
        case "-":
            return 1;
        case "*":
            return 2;
        case "/":
            return 2;
        default:
            throw new RuntimeException("不存在该运算符");
    }
}

拥有这个可以获取优先级的方法之后,我们就可以开始构写转换表达式的方法了

public static ArrayList<String> parseSuffixNotationList(ArrayList<String> list) {
    //需要一个栈,一个集合
    Stack<String> stack = new Stack<>();
    ArrayList<String> helperList = new ArrayList<>();
    //增强for循环遍历list集合
    for(String item : list) {
        //利用正则表达式判断item是否为操作数
        if(item.matches("\\d+")) {
            //是操作数,则入中间结果集合helperList
            helperList.add(item);
        } else if(item.equals("(") || stack.size() == 0){
            //如果栈1空或者栈1的栈顶元素为"(",则直接入栈
            stack.push(item);
        } else if(item.equals(")")){
            //遇到右括号则依次将运算符弹出栈,直至遇到左括号为止,并丢弃这对括号
            while(!stack.peek().equals("(")) {
                helperList.add(stack.pop());
            }
            //丢弃左括号操作
            stack.pop();
        } else {
            //遇到操作符时
            if(stack.peek().equals("(") || getPriority(item) > getPriority(stack.peek())) {
                //如果操作符优先级比栈顶元素优先级高,也直接入栈
                stack.push(item);
            } else {
                while (stack.size() != 0 && getPriority(stack.peek()) >= getPriority(item)) {
                    //利用while循环,实现弹出栈1的栈顶元素,压入栈2,回到4-1继续与栈1的栈顶元素比较
                    helperList.add(stack.pop());
                }
                stack.push(item);
            }
        }
    }
    //最后将stack栈中剩余的运算符依次添加到helperList中
    while(stack.size() != 0) {
        helperList.add(stack.pop());
    }
    return helperList;
}

到此为止方法写完了,让我们进主方法测试一下

public static void main(String[] args) {
    //提供一个中缀表达式
    String infixNotation = "1 + ( ( 2 + 3 ) * 4 ) - 5";
    //利用上面计算逆波兰表达式中构写的getList方法分割字符串
    ArrayList<String> infixList = getList(infixNotation);
    //获取逆波兰表达式
    ArrayList<String> suffixList = parseSuffixNotationList(infixList);
    //计算逆波兰表达式
    int result = calculate(suffixList);
    //输出验证
    System.out.println(infixNotation + "的结果是" + result);
}

完整代码

计算逆波兰表达式的完整代码

import java.util.ArrayList;
import java.util.Stack;

public class Main {

    public static void main(String[] args) {
        //提供一个逆波兰表达式。为了方便计算机扫描。在每个元素中间插入空格
        String rpn = "3 4 + 5 * 6 -";
        //分割rpn字符串
        ArrayList list = getList(rpn);
        //计算集合list,得到结果
        int result = calculate(list);
        //输出验证
        System.out.println(rpn + "的结果是:" + result);
    }

    public static ArrayList<String> getList(String notation) {
        //定义一个集合来存放rpn中的各个元素,采用split方法分割字符串
        ArrayList<String> list = new ArrayList<>();
        String[] splitArray = notation.split(" ");
        //使用for循环,依次将splitArray中的元素添加至集合
        for(String element : splitArray) {
            list.add(element);
        }
        return list;
    }

    public static int calculate(ArrayList<String> list) {
        //创建一个栈
        Stack<String> stack = new Stack<>();
        //遍历list集合
        for(String item : list) {
            //采用正则表达式来判断item为多位数还是符号
            if (item.matches(("\\d+"))) {
                //item为多位数,入栈
                stack.push(item);
            } else {
                //item是运算符,执行思路分析中2 -> 4的操作
                int num1 = Integer.parseInt(stack.pop());
                int num2 = Integer.parseInt(stack.pop());
                int res = 0;
                switch (item){
                    case "+":
                        res = num1 + num2;
                        break;
                    case "-":
                        res = num2 - num1;
                        break;
                    case "*":
                        res = num2 * num1;
                        break;
                    case "/":
                        res = num2 / num1;
                        break;
                    default:
                        throw new RuntimeException("运算符错误");
                }
                //将结果入栈
                stack.push("" + res);
            }
        }
        //for循环执行完后,stack栈栈顶就是答案
        return Integer.parseInt(stack.pop());
    }

}

如何将中缀表达式转化为后缀表达式完整代码

import java.util.ArrayList;
import java.util.Stack;

public class Main {

    public static void main(String[] args) {
        //提供一个中缀表达式
        String infixNotation = "1 + ( ( 2 + 3 ) * 4 ) - 5";
        //利用上面计算逆波兰表达式中构写的getList方法分割字符串
        ArrayList<String> infixList = getList(infixNotation);
        //获取逆波兰表达式
        ArrayList<String> suffixList = parseSuffixNotationList(infixList);
        //计算逆波兰表达式
        int result = calculate(suffixList);
        //输出验证
        System.out.println(infixNotation + "的结果是" + result);
    }

    public static int getPriority(String operation) {
        //数字越大,运算符优先级越高
        switch(operation) {
            case "+":
                return 1;
            case "-":
                return 1;
            case "*":
                return 2;
            case "/":
                return 2;
            default:
                throw new RuntimeException("不存在该运算符");
        }
    }

    public static ArrayList<String> parseSuffixNotationList(ArrayList<String> list) {
        //需要一个栈,一个集合
        Stack<String> stack = new Stack<>();
        ArrayList<String> helperList = new ArrayList<>();
        //增强for循环遍历list集合
        for(String item : list) {
            //利用正则表达式判断item是否为操作数
            if(item.matches("\\d+")) {
                //是操作数,则入中间结果集合helperList
                helperList.add(item);
            } else if(item.equals("(") || stack.size() == 0){
                //如果栈1空或者栈1的栈顶元素为"(",则直接入栈
                stack.push(item);
            } else if(item.equals(")")){
                //遇到右括号则依次将运算符弹出栈,直至遇到左括号为止,并丢弃这对括号
                while(!stack.peek().equals("(")) {
                    helperList.add(stack.pop());
                }
                //丢弃左括号操作
                stack.pop();
            } else {
                //遇到操作符时
                if(stack.peek().equals("(") || getPriority(item) > getPriority(stack.peek())) {
                    //如果操作符优先级比栈顶元素优先级高,也直接入栈
                    stack.push(item);
                } else {
                    while (stack.size() != 0 && getPriority(stack.peek()) >= getPriority(item)) {
                        //利用while循环,实现弹出栈1的栈顶元素,压入栈2,回到4-1继续与栈1的栈顶元素比较
                        helperList.add(stack.pop());
                    }
                    stack.push(item);
                }
            }
        }
        //最后将stack栈中剩余的运算符依次添加到helperList中
        while(stack.size() != 0) {
            helperList.add(stack.pop());
        }
        return helperList;
    }

    public static ArrayList<String> getList(String notation) {
        //定义一个集合来存放rpn中的各个元素,采用split方法分割字符串
        ArrayList<String> list = new ArrayList<>();
        String[] splitArray = notation.split(" ");
        //使用for循环,依次将splitArray中的元素添加至集合
        for(String element : splitArray) {
            list.add(element);
        }
        return list;
    }

    public static int calculate(ArrayList<String> list) {
        //创建一个栈
        Stack<String> stack = new Stack<>();
        //遍历list集合
        for(String item : list) {
            //采用正则表达式来判断item为多位数还是符号
            if (item.matches(("\\d+"))) {
                //item为多位数,入栈
                stack.push(item);
            } else {
                //item是运算符,执行思路分析中2 -> 4的操作
                int num1 = Integer.parseInt(stack.pop());
                int num2 = Integer.parseInt(stack.pop());
                int res = 0;
                switch (item){
                    case "+":
                        res = num1 + num2;
                        break;
                    case "-":
                        res = num2 - num1;
                        break;
                    case "*":
                        res = num2 * num1;
                        break;
                    case "/":
                        res = num2 / num1;
                        break;
                    default:
                        throw new RuntimeException("运算符错误");
                }
                //将结果入栈
                stack.push("" + res);
            }
        }
        //for循环执行完后,stack栈栈顶就是答案
        return Integer.parseInt(stack.pop());
    }
}

部分代码结构可使用idea进一步优化。

你可能感兴趣的:(数据结构)