1.介绍

    今天写了一个关于计算器的算法,主要注重的是简单高效和通俗易懂,并且是层层解释,下面我就谈谈一下这个算法,该算法可以对数字式子进行最终值的计算,给一段字符串式子(可以包含括号,正负整数,小数的加减乘除),那么就可以计算出结果.

2.涉及到的技术点(分两部分):

1.第一阶段,没有括号时的计算:

1.用ArrayList容器即可解决
2.简单的正则表达式
3.for和while循环

2.第二阶段,计算有括号时的匹配和计算问题:

1.把字符串存入集合,一个一个字符的存,为了后续的"读取和替换"
2.采用"堆栈"来获取"括号"的内容
3.然后计算得到的第一个"完整括号内容",再到第二个,大化小,因为是左右匹配,所以每次都能计算一个"小括号"里的式子.
4.前期加上了"( )",最后计算出来的就是结果.

用这些基础的技术点就能轻松完成计算器算法


3.逻辑过程讲解:

第一阶段:常规计算

这里写图片描述
给定一个字符串,比如: String str = “-3+5.0*-4-9/-3”;

    1.把"数字"放入一个容器: -3,5.0,-4,9,-3;
    2.把"运算符号"放入一个容器:+,*,-,/
    3.先计算 *和/
    4.再计算 +和-
    5.先遍历符号集合,读到*或者/的时候,获取并移除该符号(后面元素自动往前移),然后同样从数字集合中获取并移除相同角标的数字和连着的数字(后面自动前移);
    6.求出计算结果,然后在放入同样的位置
    7.计算完*和/后集合只剩+,-,所以都是按顺序,就可以无限循环获取并移除0位置的符号,并获取和移除0位置的数字,当符号集合没有符号即停止,得出结果.

以上所讲的就是第一步常规计算的核心思想,大家要注意的还有一点,就是怎么获取数字,而且是包括”负号”的数字.

    所以优先的就是处理负号,要处理的符号就是首位负号,以及*或者/后面的负号,其它不好当作运算符处理,处理的办法就是把两个位置的负号变成@代替:-3+5.0*-4-9/-3变成:@3+5.0*@4-9/@3;
    1.所以就可以通过正则获取数字字符数组:str.split("[0123456789\\.@]");0-9加小数点
    2.获取符号,放集合:str.split("[\\+\\-\\*/]"),加减乘都是正则表达式正规字符
    3.先遍历*和/
        1.get(i) =='*'||'/',就移除符号
        2.从数字容器取两数字运算
         ch = ops.remove(i);
         d1 = numbers.remove(i);//自动往前; 
         d2 = numbers.remove(i);
         if op ==*  d1*d2   =='/' d1/d2
        numbers.add(i,d1)//又放入

4.再变量+-;
    while(ops不为空) 一直取,因为+-都是取第0个
    ops.remove(0);
    d1 = remove(0);
    d2 = remove(0);
    如果是加号: d1+d2;
    如果是减号: d1-d2;
第二阶段:括号的问题计算

这里写图片描述
得出的结论:

1.式子就是一个集合包含着的"单个"字符

2.每次计算后,集合都会改变(涉及到第三步)
    注意:
        1.这里关键是判断集合的移除长度的问题(移除(2-8) 是5个长度)
        2.但是计算到后面,前面原本的括号变成了"33.0000",他只占集合的1位,所以移除的时候,应该按角标来计算才是准确的,而不是"(2-8)".length,"真正的lenght"是最括号到右括号,即:右括号-左括号;然后移动左括号角标位置共"lenght次"

3.把结果放入
    放入问题(即使放入88.00000或更长,那么还是占1个长度而已)
4.追踪括号里面就剩唯一的式子就得到结果了

以上的第2和第3步是解决问题的重点,同样是最容易出问题的地方



4.详细代码过程

第一部分:基础计算,括号在第二部分

/**
 * 计算器算法
 *
 * 1.用ArrayList集合
 *
 * 2.正则表达获取数字集合注意有小数Double
 *
 * 3.获取符号集合,注意符号先转成@,再转成-
 *
 * 4.获取* / 注意符号的0,就获取数字的01 ,1就获取12,再放入
 *
 * 5.获取+- 获取完* / +-只用while寻0即可
 *
 */

public class Demo11_Calc {
    public static void main(String[] args) {
        String str = "-3+5.0*-4-9/-3";
        Result(str);
    }

    private static void Result(String str) {
        ArrayList<String> ops = getOps(str);
        ArrayList<Double> num = getNum(str);
        // 先乘除再加减
        for (int i = 0; i < ops.size(); i++) {
            if (ops.get(i).contains("*") || ops.get(i).contains("/")) {
                String op = ops.remove(i);
                if (op.equals("*")) {
                    // 从数字集合取对应和后面一位数字
                    double d1 = num.remove(i);
                    double d2 = num.remove(i);

                    double number = d1*d2;
                    //再加上
                    num.add(i,number);
                }
                if (op.equals("/")) {
                    double d1 = num.remove(i);
                    double d2 = num.remove(i);
                    double number = d1/d2;
                    num.add(i, number);
                }
                i--;    //刚刚移掉两个,却又刚加上一个新数,所以i要--,因为i++,所以才能取到,如果不加那么虽然貌似正常,但是如果如8*3/3,*/连在一起就报错了;因为连着的两个if;
            }
        }
        //到+-,按顺序的所以就用while()了
        while (ops.size() != 0) {
            String op = ops.remove(0);
            double d1 = num.remove(0);
            double d2 = num.remove(0);

            if (op.equals("+")) {
                double number = d1+d2;
                //再加入
                num.add(0, number);
            }
            if (op.equals("-")) {
                double number = d1-d2;
                num.add(0, number);
            }
        }
        System.out.println(num);
    }

    /**
     * 获取符号 1.首位 和 * /后面 的-变成@,其他的-不用
     */
    private static ArrayList getNum(String str) {
        // -变成@
        str = change(str);
        ArrayList<Double> list = new ArrayList();

        String[] split = str.split("[\\+\\-\\*/]");
        for (int i = 0; i < split.length; i++) { // @3,5,@4,9,@3
            String s = split[i];
            // 再把@变成-
            if (s.contains("@")) {
                s = '-' + s.substring(1);
            }
            list.add(Double.parseDouble(s));
        }

        return list;
    }

    private static String change(String str) {
        char[] chars = str.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            // @3+5*-4-9/-3
            if (i == 0 && chars[i] == '-') {
                str = '@' + str.substring(i + 1);
            }
            // @3+5*@4-9/@3
            if (chars[i] == '*' && chars[i + 1] == '-' || chars[i] == '/' && chars[i + 1] == '-') {
                str = str.substring(0, i + 1) + '@' + str.substring(i + 2);
            }
        }
        return str;
    }

    // 获取符号
    private static ArrayList getOps(String str) {
        ArrayList<String> list = new ArrayList();
        // @变-
        str = change(str);
        // @3+5*@4-9/@3
        String[] split = str.split("[0-9\\.@]");// 表示0-9包括小数和@
        for (int i = 0; i < split.length; i++) {
            if (split[i].contains("+") || split[i].contains("-") || split[i].contains("*") || split[i].contains("/")) {
                list.add(split[i]);
            }
        }
        return list;
    }
}

以上就是第一步,解决了没有括号时的计算的问题,下面是升级到能加括号计算

第二步骤:加括号算法:

public class Demo10_test {
    private static Object calc;

    public static void main(String[] args) {
                calcDemo();
    }

    private static void calcDemo() {
        String str = "3+12/(2-8)+7*((55+1)/2+0.2*(9-1))/2+10";
        // 括弧:先判断左括号和右括号是否相等,再判断括号是否左右是否匹配
        if (!isPiPei(str)) {
            return;
        }
        if (str.contains("()")) {
            System.out.println("包含了空的括号,不符合,请检查重新输入");
            return;
        }
        /*--------------集合存单字符,用于随时移除和添加--------------*/
        // 加上括号,这样就能当作最终的表达式并判断,最终求出结果
        str = "(" + str + ")";

        ArrayList str_list = new ArrayList();

        for (int i = 0; i < str.length(); i++) {
            str_list.add(str.charAt(i));
        }
        /*--------------获取所有的单位括号内容--------------*/
        //关键是获取每一次“对称的括号”,并逐个计算,小到大,所以用栈是最好的方法。
        Stack<Integer> stack = new Stack();
        for (int i = 0; i < str_list.size(); i++) {
            if (str_list.get(i) == '(') {
                stack.add(i);
            }
            if (str_list.get(i) == ')') {
                // 移除栈记录的内容角标
                int s = stack.pop();

                // 获取式子内容
                StringBuilder sb = new StringBuilder();
                for (int j = s; j <= i; j++) {
                    sb.append(str_list.get(j));
                }

                int sbLength = i - s + 1;// 重点(这部把是最容易出错的了,(sb.Length()按照的是字符串,看似满足其实不满足list,因为8123488或者再长的小树,在list只占一位,这里用i-s+1才满足所有))

                for (int k = 0; k < sbLength; k++) {
                    str_list.remove(s);// 移除这个位置长度次
                }
                // 获取括号中的式子,计算成结果,在放入集合变成新的式子
                String strCalc = sb.substring(1, sb.length() - 1);
                double num = Demo11_Calc.Result(strCalc);
                str_list.add(s, num);

                System.out.println(str_list);
                i = i - sbLength + 1; // 移掉后,占一位,++后要能获取"本来i位置"的下一位
                if (i < -1) {// 防越界
                    break;
                }
            }
        }
    }

    /**
     * 判断字符串里的"左和右"括号是否"相等"
     */
    private static boolean isCountEqual(String str) {
        char[] chars = str.toCharArray();
        int left = 0;
        int right = 0;
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == '(') {
                left++;
            }
        }
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == ')') {
                right++;
            }
        }
        if (left == right) {

        } else {
            System.out.println("表达式括号不匹配");
        }
        return left == right;
    }

    /**
     * 判断字符串里的"左和右"括号是否"匹配"
     */
    private static boolean isPiPei(String str) {
        boolean isPiPei = false;
        //先判断是否相等
        if (!isCountEqual(str)) {
            return false;
        }
        //定义栈记录左
        Stack<Character> stack = new Stack<>();
        char pop;
        char[] chs = str.toCharArray();
        fo:
        for (int i = 0; i < chs.length; i++) {
            switch (chs[i]) {
                case '(':
                    stack.add(chs[i]);// 放在前面
                    break;
                case ')':
                    pop = stack.pop();// 获取并移除
                    if (pop == '(') {
                        isPiPei = true;
                    } else {
                        isPiPei = false;
                        // 并停止所有
                        break fo;
                    }
                    break;
            }
        }
        return isPiPei;
    }
}

5.总结:

注意重点的步骤即可:
第一部分:
    第一个和*/后面的负号现变成@,截取后再变回来
    1.数字集合
    2.符号集合
    3.获取并移除符号集合元素和数字元素,再添加在原位
    4.追踪符号集合为空即可求出结果;
第二部分:
    括号的概念就是:获取一个就计算一个,并把计算后的值放入到原来的位置替换
    1.移除的特点:移除左括号位置:(右括号-左括号+1次)
    2.添加结果在左括号位置
    3.为了方便,初始也加上(),让方法自动算出最后的结果。
    4.额外的注意点就是前期的判断括号是否匹配问题。
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐