資料結構與演算法——棧(五)中綴表示式轉字尾表示式

天然呆dull發表於2021-08-28

通過資料結構與演算法——棧(四)逆波蘭計算器-字尾表示式的程式碼實現,可以看到:字尾表示式對於計算機來說很方便,但是對於我們人來說,字尾表示式卻不是那麼容易寫出來的。

所以本篇就是來講解怎麼實現中綴表示式轉換成字尾表示式,以及完成完整版的逆波蘭計算器。

* 中綴表示式轉字尾表示式步驟

  1. 初始化兩個棧:

    • 運算子棧:s1
    • 中間結果棧:s2
  2. 從左到右掃描中綴表示式

  3. 遇到運算元時,將其壓入 s2

  4. 遇到運算子時

    比較 它 與 s1 棧頂運算子的優先順序:

    ​ (1)如果 s1 為空,或則棧頂運算子號為 ( ,則將其壓入符號棧 s1

    ​ (2)否則,若優先順序比棧頂運算子 ,也將其壓入符號棧 s1

    ​ (3)否則,若優先順序比棧頂運算子 低 或 相等,將 s1 棧頂的運算子 彈出,並壓入到 s2 中

    再重複第 4.1 步驟,與新的棧頂運算子比較(因為 4.3 將 s1 棧頂運算子彈出了)

    這裡重複的步驟在實現的時候有點難以理解,下面進行解說:

    ​ 如果 s1 棧頂符號 優先順序比 當前符號 高或則等於,那麼就將其 彈出,壓入 s2 中(迴圈做,是隻要 s1 不為空),如果棧頂符號為 (,這裡不把(當作運算子,所以碰到了就不用作比較了,(也不用彈出,直接 把當前運算子壓入即可。但如果當前運算子為左括號 ( 或者 右括號 ) 呢?那就看下面的 第5點

  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 * + 遇到右括號:彈出運算子直至遇到左括號,這裡彈出 s1 中的 * 壓入 s2 中,這裡去掉這一對小括號
- 1 2 3 + 4 * + - 遇到操作符:與 s1 棧頂比較,優先順序一致,將 s1 中的 + 彈出,並壓入 s2 中,再將-壓入s1
5 1 2 3 + 4 * + 5 - 遇到運算元:將其壓入 s2
到達最右端 1 2 3 + 4 * + 5 - 解析完畢,將 s1 中的符號彈出並壓入 s2 中

由於 s2 是一個棧,彈出是從棧頂彈出,因此逆序後結果就是 1 2 3 + 4 * + 5 -

我的疑問

你怎麼知道這個中綴表示式轉字尾表示式的思路是這樣的?

在學習和使用上有兩個層次:

  1. 應用層次:別人發明出來的東西,你學習、理解它,並靈活運用它
  2. 自創:你自己發明一個東西出來,並使用它

那麼這裡的中綴轉字尾表示式的思路步驟,則屬於第一個層次,相關的計算機專家之類的,發明出來了。我們要理解它並靈活運用它。等你能力達到一定層度時,有可能發明出來一個演算法。

再比如:絕世武功 -> 降龍十八掌,別人已經創造出來了,你不去學習理解它,如何加以改進並自創?如果沒有人教你,你怎麼能學會降龍十八掌?

程式碼實現

/**
 * 中綴表示式轉字尾表示式
 */
public class InfixToSuffix {
    public static void main(String[] args) {
        InfixToSuffix infixToSuffix = new InfixToSuffix();
        // 目標:1+((2+3)*4)-5  轉為 1 2 3 + 4 * + 5 -
        // 1. 將中綴表示式轉成 List,方便在後續操作中獲取資料
        String infixExpression = "1+((2+3)*4)-5";
        List<String> infixList = infixToSuffix.toInfixExpressionList(infixExpression);
        System.out.println(infixList); // [1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
        // 2. 將中綴表示式轉成字尾表示式
        List<String> suffixList = infixToSuffix.parseSuffixExpreesionList(infixList);
        System.out.println(suffixList); // [1, 2, 3, +, 4, *, +, 5, -]
    }


    /**
     * 將中綴表示式解析成單個元素的 List,
     *
     * @param s
     * @return 1+((2+3)*4)-5 -> [1,+,(,(,2,+,3,),*,4,),5]
     */
    //方法:將 中綴表示式轉成對應的List
    //  s="1+((2+3)×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是一個非數字,我需要加入到ls
            if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
                ls.add("" + c);
                i++; //i需要後移
            } else { //如果是一個數,需要考慮多位數
                str = ""; //先將str 置成"" '0'[48]->'9'[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;//返回
    }

    /**
     * 中綴表示式 List 轉為字尾表示式 List
     *
     * @param ls
     * @return
     */
    //即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]  =》 ArrayList [1,2,3,+,4,*,+,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

        //遍歷ls
        for (String item : ls) {
            //如果是一個數,加入s2
            if (item.matches("\\d+")) {
                s2.add(item);
            } else if (item.equals("(")) {
                s1.push(item);
            } else if (item.equals(")")) {
                //如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄
                while (!s1.peek().equals("(")) {
                    s2.add(s1.pop());
                }
                s1.pop();//!!! 將 ( 彈出 s1棧, 消除小括號
            } else {
            //當item的優先順序小於等於s1棧頂運算子, 將s1棧頂的運算子彈出並加入到s2中,再次轉到(4.1)與s1中新的棧頂運算子相比較
            //問題:我們缺少一個比較優先順序高低的方法。  下面建立了priority類,用來比較優先順序
                //這裡比較許可權的時候,可能在比較過程中取到括號,但是問題不大,因為方法規定了,不符合運算子的預設優先順序為0
                while (s1.size() != 0 && priority.getValue(s1.peek()) >= priority.getValue(item)) {
                    s2.add(s1.pop());
                }
                //還需要將item壓入棧
                s1.push(item);
            }
        }

        //將s1中剩餘的運算子依次彈出並加入s2
        while (s1.size() != 0) {
            s2.add(s1.pop());
        }

        return s2; //注意因為是存放到List, 因此按順序輸出就是對應的字尾表示式對應的List

    }
}

    /**
     * 計算操作符號優先順序,暫時只支援 + - * /
     *
     * @return 優先順序越高,數值越大
     */
    //編寫一個類 priority 可以返回一個運算子 對應的優先順序
    class priority {
        private static int ADD = 1;//加
        private static int SUB = 1;//減
        private static int MUL = 2;//乘
        private static int DIV = 2;//除

        //寫一個靜態方法,返回對應的優先順序數字
        public static int getValue(String operation) {
            int result = 0;
            switch (operation) {
                case "+":
                    result = ADD;
                    break;
                case "-":
                    result = SUB;
                    break;
                case "*":
                    result = MUL;
                    break;
                case "/":
                    result = DIV;
                    break;
                default:
                    System.out.println("不存在該運算子" + operation);
                    break;
            }
            return result;
        }
    }

測試輸出

[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
不存在該運算子(
不存在該運算子(
[1, 2, 3, +, 4, *, +, 5, -] 

可以看到,已經變成字尾表示式的順序了。下面結合前面實現的逆波蘭計算器整合中綴表示式轉字尾表示式。

完整版逆波蘭計算器

public class PolandNotation {

    public static void main(String[] args) {


        //完成將一箇中綴表示式轉成字尾表示式的功能
        //說明
        //1. 1+((2+3)×4)-5 => 轉成  1 2 3 + 4 × + 5 –
        //2. 因為直接對str 進行操作,不方便,因此 先將  "1+((2+3)×4)-5" =》 中綴的表示式對應的List
        //   即 "1+((2+3)×4)-5" => ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
        //3. 將得到的中綴表示式對應的List => 字尾表示式對應的List
        //   即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]  =》 ArrayList [1,2,3,+,4,*,+,5,–]

        String expression = "1+((2+3)*4)-5";//注意表示式
        List<String> infixExpressionList = toInfixExpressionList(expression);
        System.out.println("中綴表示式對應的List=" + infixExpressionList); // ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
        List<String> suffixExpreesionList = parseSuffixExpreesionList(infixExpressionList);
        System.out.println("字尾表示式對應的List" + suffixExpreesionList); //ArrayList [1,2,3,+,4,*,+,5,–]

        System.out.printf("expression=%d", calculate(suffixExpreesionList)); // ?
    }


    //即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]  =》 ArrayList [1,2,3,+,4,*,+,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

        //遍歷ls
        for (String item : ls) {
            //如果是一個數,加入s2
            if (item.matches("\\d+")) {
                s2.add(item);
            } else if (item.equals("(")) {
                s1.push(item);
            } else if (item.equals(")")) {
                //如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄
                while (!s1.peek().equals("(")) {
                    s2.add(s1.pop());
                }
                s1.pop();//!!! 將 ( 彈出 s1棧, 消除小括號
            } else {
            //當item的優先順序小於等於s1棧頂運算子, 將s1棧頂的運算子彈出並加入到s2中,再次轉到(4.1)與s1中新的棧頂運算子相比較
            //問題:我們缺少一個比較優先順序高低的方法
                while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
                    s2.add(s1.pop());
                }
                //還需要將item壓入棧
                s1.push(item);
            }
        }

        //將s1中剩餘的運算子依次彈出並加入s2
        while (s1.size() != 0) {
            s2.add(s1.pop());
        }

        return s2; //注意因為是存放到List, 因此按順序輸出就是對應的字尾表示式對應的List

    }

    //方法:將 中綴表示式轉成對應的List
    //  s="1+((2+3)×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是一個非數字,我需要加入到ls
            if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
                ls.add("" + c);
                i++; //i需要後移
            } else { //如果是一個數,需要考慮多位數
                str = ""; //先將str 置成"" '0'[48]->'9'[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)從左至右掃描,將3和4壓入堆疊;
		2)遇到+運算子,因此彈出4和3(4為棧頂元素,3為次頂元素),計算出3+4的值,得7,再將7入棧;
		3)將5入棧;
		4)接下來是×運算子,因此彈出5和7,計算出7×5=35,將35入棧;
		5)將6入棧;
		6)最後是-運算子,計算出35-6的值,即29,由此得出最終結果
	 */

    public static int calculate(List<String> ls) {
        // 建立給棧, 只需要一個棧即可
        Stack<String> stack = new Stack<String>();
        // 遍歷 ls
        for (String item : ls) {
            // 這裡使用正規表示式來取出數
            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("*")) {
                    res = num1 * num2;
                } else if (item.equals("/")) {
                    res = num1 / num2;
                } else {
                    throw new RuntimeException("運算子有誤");
                }
                //把res 入棧
                stack.push("" + res);
            }

        }
        //最後留在stack中的資料是運算結果
        return Integer.parseInt(stack.pop());
    }

}

//編寫一個類 Operation 可以返回一個運算子 對應的優先順序
class Operation {
    private static int ADD = 1;
    private static int SUB = 1;
    private static int MUL = 2;
    private static int DIV = 2;

    //寫一個方法,返回對應的優先順序數字
    public static int getValue(String operation) {
        int result = 0;
        switch (operation) {
            case "+":
                result = ADD;
                break;
            case "-":
                result = SUB;
                break;
            case "*":
                result = MUL;
                break;
            case "/":
                result = DIV;
                break;
            default:
                System.out.println("不存在該運算子" + operation);
                break;
        }
        return result;
    }

}

測試輸出

中綴表示式對應的List=[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
不存在該運算子(
不存在該運算子(
字尾表示式對應的List[1, 2, 3, +, 4, *, +, 5, -]
expression=16

相關文章