數據結構篇——棧擴展(計算器,前中後綴表達式)

上一篇博客我們講了隊列的擴展應用,本篇我們再來分析一下棧的一些應用。

計算器

說到棧的應用,最經典的就是計算器的實現了。對於給定的一個算數表達式,我們需要輸出它的最終結果。
例如:對於輸入(10+2-5)*(30/6) 我們需要輸出35

分析過程

  1. 對於一個計算表達式,我們需要考慮運算符的優先級,還要考慮括號。按照我們人類的計算思路,會把要先運算的算好再與其他繼續運算。
    例如:
  1+2*(4-3)
=>1+2*1
=>1+2
=>3
  1. 一個表達式中有兩種數據:數字,運算符號(括號也算運算符號)。我們需要考慮的是使用一個棧還是使用兩個棧?如果使用一個棧,當往棧裏面放數字我們需要做什麼處理?放運算符號呢?
  2. 我們仔細觀察表達式的話,會發現一個運算符加上左右兩側的數字能夠構成一個獨立的表達式(不考慮括號的場景);如果有括號的話,左括號和右括號中間也是一個獨立的表達式;
  3. 表達式經過運算就是一個數字;
  4. 使用棧計算表達式必然需要兩個過程:入棧,出棧。對於出棧,我們一定希望先取出來的是優先級比較高的表達式,後取出來的是優先級比較低的表達式。所以就需要我們在表達式入棧的時候進行一波整理。把所有的運算邏輯按運算優先級調整入棧順序。加法和減法相對於乘法除法必定位於棧底部,括號運算由於過於複雜,在入棧的時候我們就把括號裏面的表達式變成一個數字

雙棧實現

/**
 * 用兩個棧實現的計算器
 */
public class StackCalculator {

    /**
     * 表達式
     */
    private String expr;

    /**
     * 操作數棧
     */
    private Stack<Number> numStack;

    /**
     * 操作符棧
     */
    private Stack<Character> operStack;

    public StackCalculator(String expr) {
        //去除表達式中的空格
        this.expr = expr.replace(" ", "");
        numStack = new Stack<>();
        operStack = new Stack<>();
    }

    /**
     * 計算表達式的值
     */
    public Number calculate() {
        char[] chars = expr.toCharArray();

        //入棧
        for (int i = 0; i < chars.length; i++) {
            //如果是數字則直接入棧
            if (Character.isDigit(chars[i])) {
                String numString = String.valueOf(chars[i]);
                //此處考慮多位數和小數的情況 如果下一位還是數字或者小數點則繼續向下取值
                while (Character.isDigit(chars[i + 1]) || chars[i + 1] == '.') {
                    i++;
                    numString += chars[i];
                }
                numStack.push(new BigDecimal(numString));
                continue;
            }

            //如果是運算符號 我們就需要在入棧的時候檢查優先級順序
            //1、如果operStack裏面沒有數據則直接入棧
            //2、如果operStack裏面有數據,則需要比較棧頂運算符與本次運算符的優先級
            //2.1、如果本次優先級比較大,則放入棧
            //2.2、如果棧裏面的優先級比較大,則從operStack取出棧頂運算符,從numStack取出兩個數字進行運算,再把運算結果放回numStack
            //3、如果本次運算符是左括號,則放入棧
            //4、如果本次運算符是右括號,則不停地從operStack彈出運算符直到彈出左括號,把這兩個括號中間的表達式算出結果,放入numStack
            if (operStack.size() == 0) {
                operStack.push(chars[i]);
                continue;
            }

            if (chars[i] == '(') {
                operStack.push(chars[i]);
                continue;
            }
            if (chars[i] == ')') {
                while (operStack.top() != '(') {
                    calResult();
                }
                operStack.pop();
                continue;
            }
            if (compareOper(chars[i], operStack.top())) {
                operStack.push(chars[i]);
            } else {
                calResult();
                operStack.push(chars[i]);
            }
        }

        //出棧得出最後計算結果

        while (operStack.size() != 0) {
            calResult();
        }
        return numStack.pop();
    }

    /**
     * 這個方法比較重要,巧妙的設計能夠簡便我們的運算
     * 當oper1和oper2的優先級一樣的時候返回false,這樣子就能保證在不考慮括號的情況下 operStack裏面的數據量不超過2個
     * <p>
     * 1、stackOper是乘除法 返回false
     * 2、putOper和stackOper同爲加減法 返回false
     * 3、stackOper是左括號 返回true
     * 4、其他情況(putOper是乘除法  stackChar是加減法) 返回true
     */
    private boolean compareOper(char putOper, char stackOper) {
        if (stackOper == '*' || stackOper == '/') {
            return false;
        }
        if ((putOper == '+' || putOper == '-') && (stackOper == '+' || stackOper == '-')) {
            return false;
        }
        if (stackOper == '(') {
            return true;
        }
        return true;
    }

    /**
     * 計算一次值 operStack取出一個值 numStack取出兩個值 算好以後放進numStack
     */
    private void calResult() {
        Character oper = operStack.pop();
        //要注意第二次取出來的是 第一個操作數
        Number num2 = numStack.pop();
        Number num1 = numStack.pop();
        BigDecimal result = null;
        if (oper == '+') {
            result = new BigDecimal(String.valueOf(num1)).add(new BigDecimal(String.valueOf(num2)));
        } else if (oper == '-') {
            result = new BigDecimal(String.valueOf(num1)).subtract(new BigDecimal(String.valueOf(num2)));
        } else if (oper == '*') {
            result = new BigDecimal(String.valueOf(num1)).multiply(new BigDecimal(String.valueOf(num2)));
        } else if (oper == '/') {
            result = new BigDecimal(String.valueOf(num1)).divide(new BigDecimal(String.valueOf(num2)), BigDecimal.ROUND_HALF_UP);
        }
        numStack.push(result);
    }

    public static void main(String[] args) {
        StackCalculator stackCalculator = new StackCalculator("(10.5+2.5)*(30/6+12.5/2.5*2)");
        System.out.println(stackCalculator.calculate());
    }
}

這裏在原先的Stack中增加了一個top()方法

    /**
     * 查看棧頂的值 不取出
     */
    public T top() {
        //如果棧空了則返回null
        if (isEmpty()) {
            System.out.println("棧空了");
            return null;
        }
        return first.item;
    }

具體的實現細節,都很詳細的寫在了註釋中。上述代碼是使用了雙棧,分別存放操作數和操作符。如果使用一個棧,當然也可以。每次計算的時候固定從stack中取三個元素。

前綴,中綴,後綴表達式

這幾種表達式的不同之處僅僅在於操作數與操作符的相對位置不同。前中後是針對操作符而言的。以一個簡單的數學表達式1+2*3舉例
前綴表達式:+ 1 * 2 3
中綴表達式:1 + 2 * 3
後綴表達式:1 2 3 * +

表達式之間的相互轉換思路:
1、如果大家能夠很好的理解上面計算器的實現方式,對於表達式的轉換應該也很容易就能想到;
2、利用棧FIFO的特性,一開始在表達式入棧的時候按照一定規律整理好。把不按照規律的表達式,在入棧時直接計算出一個臨時結果放入棧中,最後按次序出棧得到最後的結果。

中綴轉前綴

    /**
     * 在原來的計算器類中新加入操作符棧
     */
    private Stack<Character> operStack;
    
 	/**
     * 中綴表達式轉前綴表達式
     */
    public String infix2Prefix() {
        char[] chars = expr.toCharArray();
        //入棧
        for (int i = 0; i < chars.length; i++) {
            //如果是數字則直接入棧
            if (Character.isDigit(chars[i])) {
                String numString = String.valueOf(chars[i]);
                //此處考慮多位數和小數的情況 如果下一位還是數字或者小數點則繼續向下取值
                while (Character.isDigit(chars[i + 1]) || chars[i + 1] == '.') {
                    i++;
                    numString += chars[i];
                }
                exprStack.push(numString);
                continue;
            }
            //如果是運算符號 我們就需要在入棧的時候檢查優先級順序
            //1、如果operStack裏面沒有數據則直接入棧
            //2、如果operStack裏面有數據,則需要比較棧頂運算符與本次運算符的優先級
            //2.1、如果本次優先級比較大,則放入棧
            //2.2、如果棧裏面的優先級比較大,則從operStack取出棧頂運算符,從exprStack取出兩個表達式進行拼接,再把運算結果放回exprStack
            //3、如果本次運算符是左括號,則放入棧
            //4、如果本次運算符是右括號,則不停地從operStack彈出運算符直到彈出左括號,把這兩個括號中間的表達式算出結果,放入exprStack
            if (operStack.size() == 0) {
                operStack.push(chars[i]);
                continue;
            }

            if (chars[i] == '(') {
                operStack.push(chars[i]);
                continue;
            }
            if (chars[i] == ')') {
                while (operStack.top() != '(') {
                    getPrefix();
                }
                operStack.pop();
                continue;
            }
            if (compareOper(chars[i], operStack.top())) {
                operStack.push(chars[i]);
            } else {
                getPrefix();
                operStack.push(chars[i]);
            }
        }

        //出棧得出最後計算結果

        while (operStack.size() != 0) {
            getPrefix();
        }
        return exprStack.pop();
    }

    private void getPrefix() {
        String expr2 = exprStack.pop();
        String expr1 = exprStack.pop();
        Character oper = operStack.pop();
        //前綴表達式 符號在前
        String tempResult = oper + " " + expr1 + " " + expr2;
        exprStack.push(tempResult);
    }

    public static void main(String[] args) {
        StackCalculator stackCalculator = new StackCalculator("(10.5+2.5)*(30/6+12.5/2.5*2)");
//        System.out.println(stackCalculator.calculate());
        System.out.println(stackCalculator.infix2Prefix());
    }

我們可以看到其實總的流程跟計算器是一模一樣的,修改的點只是把原來的calResult方法改爲getPrefix。相信大家應該也很容易理解。

中綴轉後綴

中綴轉後綴的思路與中綴轉前綴的思路一樣,只是需要把getPrefix的實現改爲getSuffix。

    private void getSuffix() {
        String expr2 = exprStack.pop();
        String expr1 = exprStack.pop();
        Character oper = operStack.pop();
        //後綴表達式 符號在前
        String tempResult = expr1 + " " + expr2 + " " + oper;
        exprStack.push(tempResult);
    }

前綴轉後綴

這個留給大家自己實現吧~
其實百分之95的代碼跟中綴轉後綴一樣。只不過需要在過程加一點判斷,防止一些運行時異常發生,如數組越界異常。然後對於小數和多位數的處理也略有不同。

總結

爲什麼我們在實現計算器的時候要使用棧,而不是隊列?
對於3個值A,B,C。設入棧(隊列)的順序爲ABC,現在A已經放入了棧(隊列),正要把B也按照一定邏輯放入棧(隊列)中,現在需要特定來源的數據來給我們的邏輯提供支撐。
如果使用隊列,對於B的處理邏輯,我們需要依靠C,也就是說我們需要依靠未來的值來處理現在的邏輯
如果使用棧,對於B的處理邏輯,我們需要依靠A,也就是說我們可以依靠過去的值來處理現在的邏輯
這是我理解中棧與隊列的選型最大的差別。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章