[資料結構與演算法] 邂逅棧

時間靜止不是簡史發表於2020-02-05

在邂逅了完線性結構的陣列和佇列後, 我們便偶遇了棧這個東東, 他到底是個啥?
就讓我們慢慢揭開它的神祕面紗吧~~~

需求介紹

在這裡插入圖片描述

棧的介紹

棧的英文為(stack): 又名堆疊,它是一種運算受限的線性表。限定僅在表尾(棧頂)進行插入和刪除操作的線性表

  • 棧是一個先入後出(FILO-First In Last Out)的有序列表。
  • 棧(stack)是限制線性表中元素的插入和刪除只能線上性表的同一端進行的一種特殊線性表。允許插入和刪除的一端,為變化的一端,稱為棧頂(Top),另一端為固定的一端,稱為棧底(Bottom)。
  • 根據棧的定義可知,最先放入棧中元素在棧底,最後放入的元素在棧頂,而刪除元素剛好相反,最後放入的元素最先刪除,最先放入的元素最後刪除

入棧圖示
在這裡插入圖片描述
出棧圖示
在這裡插入圖片描述

棧的應用場景

  • 子程式的呼叫:在跳往子程式前,會先將下個指令的地址存到堆疊中,直到子程式執行完後再將地址取出,以回到原來的程式中。
  • 處理遞迴呼叫:和子程式的呼叫類似,只是除了儲存下一個指令的地址外,也將引數、區域變數等資料存入堆疊中。
  • 表示式的轉換[中綴表示式轉字尾表示式] 與求值 (實際解決)。
  • 二叉樹的遍歷
  • 圖形的深度優先(depth一first)搜尋法。

利用陣列實現棧

思路圖

在這裡插入圖片描述
程式碼實現

package ah.sz.tp.algorithm1;

/**
 * 單向迴圈連結串列學習
 *
 * @author TimePause
 * @create 2020-01-11 20:30
 */
public class SingleCircleLinkedListDemo {
    public static void main(String[] args) {
        SingleCircleLinkedListDemo demo = new SingleCircleLinkedListDemo();
        // 建立了內部類, 所以使用這種方式建立內部類物件
        SingleCircleLinkedList list = demo.new SingleCircleLinkedList();
        // 呼叫新增方法
        list.add(5);
        // 呼叫遍歷顯示方法
        //list.showSingleCircleLinkedList();
        list.countBoy(1, 2, 5);

    }

    /**
     * 建立單向迴圈連結串列類
     */
    class SingleCircleLinkedList {
        // 建立第一個節點
        private Boy first = null;
        // 建立臨時指標/變數
        private Boy curBoy = null;

        /**
         * 根據使用者輸入, 計算小孩出圈的順序
         *
         * @param startNo  表示從第幾個小孩開始數數
         * @param countNum 表示數幾下
         * @param nums     表示圈中最初有多少個小孩
         */
        public void countBoy(int startNo, int countNum, int nums) {
            // 1.對引數進行校驗
            if (startNo < 1 || countNum > nums || startNo > nums) {
                System.out.println("引數設定有誤,請重新設定!!!");
            }
            //建立輔助指標, 幫助小孩節點出圈(迴圈連結串列)
            Boy helper = first;
            //2. 讓輔助變數helper首先指向這個圈的最後一個節點
            while (true) {
                if (helper.getNext() == first) {
                    break;
                }
                // 指標後移
                helper = helper.getNext(); //or helper.next
            }
            //3. 小孩報數前,先讓first和helper移動k-1(startNo-1)次=>從指定地方報數
            for (int i = 0; i < startNo - 1; i++) {
                //同時移動first和helper指標
                first = first.getNext();
                helper = helper.getNext();
            }
            //4.小孩報數時, 讓first和helper的指標同時移動m-1(countMun-1),然後出圈
            while (true) {
                if (helper == first) {//說明圈中只剩下一個元素
                    break;
                }
                // 讓first和helper的指標同時移動m-1(countMun-1)
                for (int j = 0; j < countNum - 1; j++) {
                    first = first.getNext();
                    helper = helper.getNext();
                }
                //這時first要指向的節點, 就是小孩要出圈的節點
                System.out.printf("小孩節點%d 要出圈\n", first.getNo());
                //5. 將first指向的小孩節點出圈
                //思路:結合圖知,首先將first移動到要出圈的節點的下一個節點後,讓輔助指標去指向下一個節點,這樣要出圈的節點就會被GC
                first = first.getNext();
                helper.setNext(first);
            }
            System.out.printf("最後留在圈中的小孩的編號為%d \n", first.no);

        }

        /**
         * 新增方法
         *
         * @param num 新增幾個節點
         */
        public void add(int num) {
            // 對引數進行校驗
            if (num < 1) {
                System.out.println("新增節點個數有誤,請重新新增~~~");
                return;
            }

            //使用for迴圈來建立我們的環形連結串列
            for (int i = 1; i <= num; i++) {
                //建立下一個節點/子節點
                Boy boy = new Boy(i);
                // 如果新增一個節點
                if (i == 1) {
                    //1.將第一個節點boy作為first(相當於其他連結串列中的頭節點)
                    first = boy;
                    //2.將boy下一個節點指向first(形成環)
                    boy.next = first;//也可以寫成 boy.setNext(first); 同理,下面都可以
                    //3. 將指標移動到first(頭節點)
                    curBoy = first;
                } else {
                    // 如果新增1個以上節點
                    //1. 將輔助變數指向下一個節點boy 2.將boy指向第一個節點(形成環) 3.一陣移動到一下個節點
                    curBoy.next = boy;
                    boy.next = first;
                    curBoy = boy;
                }
            }

        }

        /**
         * 遍歷當前的單向迴圈連結串列
         */
        public void showSingleCircleLinkedList() {
            // 判斷連結串列是否非空
            if (first == null) {
                System.out.println("當前迴圈連結串列為空, 無法遍歷哦~~~");
                return;
            }
            // 呼叫輔助指標完成遍歷
            curBoy = first;
            while (true) {
                System.out.printf("孩子節點的編號%d \n", curBoy.no);
                System.out.println(curBoy.toString());
                if (curBoy.next == first) {
                    break;
                }
                curBoy = curBoy.next;
            }

        }

    }


    /**
     * 建立Boy類,用於存放節點資訊
     */
    class Boy {
        //屬性設定成私有,如果是內部類,可以直接使用; 如果是外部類,需要通過get(),set()方法, 或將屬性設定成public
        private int no;//編號
        private Boy next;//代表指向下一個節點

        //帶參構造
        public Boy(int no) {
            this.no = no;
        }

        public int getNo() {
            return no;
        }

        public Boy getNext() {
            return next;
        }

        public void setNo(int no) {
            this.no = no;
        }

        public void setNext(Boy next) {
            this.next = next;
        }

    }
}

棧實現綜合計算器

邏輯圖
在這裡插入圖片描述

實現程式碼

public class Calculator {

    public static void main(String[] args) {
        //根據前面思路,完成表示式的運算
        String expression = "7*2*2-5+1-5+3-4"; //如何處理多位數的問題?
        //建立兩個棧,一個數棧,一個符號棧
        ArrayStack2 numStack = new ArrayStack2(10);
        ArrayStack2 operStack = new ArrayStack2(10);
        //定義需要的相關變數
        int index = 0;//用於掃描表示式
        int num1 = 0;
        int num2 = 0;
        int oper = 0;
        int res = 0;
        char ch = ' '; //將每次掃描得到char儲存到ch
        String keepNum = ""; //==>b.用於拼接 多位數
        //開始while迴圈的掃描expression
        while (true) {
            //依次得到expression 的每一個字元==>這裡只能處理一位數
            ch = expression.substring(index, index + 1).charAt(0);//substring處理得到的是一個string型別, 需要使用charAt轉換成char型別
            //判斷ch是什麼,然後做相應的處理
            if (operStack.isOper(ch)) {//如果是運算子
                //判斷當前的符號棧是否為空
                if (!operStack.isEmpty()) {
                    //如果符號棧有操作符,就進行比較,如果當前的操作符的優先順序小於或者等於棧中的操作符,就需要從數棧中pop出兩個數,
                    //在從符號棧中pop出一個符號,進行運算,將得到結果,入數棧,然後將當前的操作符入符號棧
                    if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
                        num1 = numStack.pop();
                        num2 = numStack.pop();
                        oper = operStack.pop();
                        res = numStack.cal(num1, num2, oper);
                        //把運算的結果入數棧
                        numStack.push(res);
                        //然後將當前的操作符入符號棧
                        operStack.push(ch);
                    } else {
                        //如果當前的操作符的優先順序大於棧中的操作符, 就直接入符號棧.
                        operStack.push(ch);
                    }
                } else {
                    //如果為空直接入符號棧..
                    operStack.push(ch); // 1 + 3
                }
            } else { //如果是數,則直接入數棧
                //numStack.push(ch - 48); //? eg: 我們掃描的 "1+3" 中的 '1' 代表的是ascall是49 而不是數字 1

                //a.分析思路
                //1. 當處理多位數時,不能發現是一個數就立即入棧,因為他可能是多位數
                //2. 在處理數,需要向expression的表示式的index 後再看一位,如果是數就進行掃描,如果是符號才入棧
                //3. 因此我們需要定義一個變數 字串,用於拼接

                //c.處理多位數
                keepNum += ch;

                //e.如果ch已經是expression的最後一位,就直接入棧
                if (index == expression.length() - 1) {
                    numStack.push(Integer.parseInt(keepNum));
                } else {

                    //d.判斷下一個字元是不是數字,如果是數字,就繼續掃描,如果是運算子,則入棧
                    //注意是看後一位,不是index++
                    if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) {
                        //如果後一位是運算子,則入棧 keepNum = "1" 或者 "123"
                        numStack.push(Integer.parseInt(keepNum));//將String的substring轉換成int型別
                        //重要的!!!!!!, keepNum清空
                        keepNum = "";
                    }
                }
            }
            //讓index + 1, 並判斷是否掃描到expression最後.
            index++;
            if (index >= expression.length()) {
                break;
            }
        }

        //當表示式掃描完畢,就順序的從 數棧和符號棧中pop出相應的數和符號,並執行.
        while (true) {
            //如果符號棧為空,則計算到最後的結果, 數棧中只有一個數字【結果】
            if (operStack.isEmpty()) {
                break;
            }
            num1 = numStack.pop();
            num2 = numStack.pop();
            oper = operStack.pop();
            res = numStack.cal(num1, num2, oper);
            numStack.push(res);//將運算好的資料入棧
        }
        //將數棧的最後數,pop出,就是結果
        int res2 = numStack.pop();
        System.out.printf("表示式 %s = %d", expression, res2);
    }

}

//先建立一個棧,直接使用前面建立好
//定義一個 ArrayStack2 表示棧, 需要擴充套件功能
class ArrayStack2 {
    private int maxSize; // 棧的大小
    private int[] stack; // 陣列,陣列模擬棧,資料就放在該陣列
    private int top = -1;// top表示棧頂,初始化為-1

    //構造器
    public ArrayStack2(int maxSize) {
        this.maxSize = maxSize;
        stack = new int[this.maxSize];
    }

    //增加一個方法,可以返回當前棧頂的值, 但是不是真正的pop
    public int peek() {
        return stack[top];
    }

    //棧滿
    public boolean isFull() {
        return top == maxSize - 1;
    }

    //棧空
    public boolean isEmpty() {
        return top == -1;
    }

    //入棧-push
    public void push(int value) {
        //先判斷棧是否滿
        if (isFull()) {
            System.out.println("棧滿");
            return;
        }
        top++;
        stack[top] = value;
    }

    //出棧-pop, 將棧頂的資料返回
    public int pop() {
        //先判斷棧是否空
        if (isEmpty()) {
            //丟擲異常
            throw new RuntimeException("棧空,沒有資料~");
        }
        int value = stack[top];
        top--;
        return value;
    }

    //顯示棧的情況[遍歷棧], 遍歷時,需要從棧頂開始顯示資料
    public void list() {
        if (isEmpty()) {
            System.out.println("棧空,沒有資料~~");
            return;
        }
        //需要從棧頂開始顯示資料
        for (int i = top; i >= 0; i--) {
            System.out.printf("stack[%d]=%d\n", i, stack[i]);
        }
    }

    //返回運算子的優先順序,優先順序是程式設計師來確定, 優先順序使用數字表示
    //數字越大,則優先順序就越高.
    public int priority(int oper) {
        if (oper == '*' || oper == '/') {
            return 1;
        } else if (oper == '+' || oper == '-') {
            return 0;
        } else {
            return -1; // 假定目前的表示式只有 +, - , * , /
        }
    }

    //判斷是不是一個運算子
    public boolean isOper(char val) {
        return val == '+' || val == '-' || val == '*' || val == '/';
    }

    //計算方法
    public int cal(int num1, int num2, int oper) {
        int res = 0; // res 用於存放計算的結果
        switch (oper) {
            case '+':
                res = num1 + num2;
                break;
            case '-':
                res = num2 - num1;// 注意順序
                break;
            case '*':
                res = num1 * num2;
                break;
            case '/':
                res = num2 / num1;
                break;
            default:
                break;
        }
        return res;
    }

}

測試結果

在這裡插入圖片描述

在這裡插入圖片描述

字首表示式(波蘭表示式)

  • 字首表示式又稱波蘭式,字首表示式的運算子位於運算元之前
  • 舉例說明: (3+4)×5-6 對應的字首表示式就是 - × + 3 4 5 6

字首表示式的計算機求值

從右至左掃描表示式,遇到數字時,將數字壓入堆疊,遇到運算子時,彈出棧頂的兩個數,用運算子對它們做相應的計算(棧頂元素 和 次頂元素),並將結果入棧;重複上述過程直到表示式最左端,最後運算得出的值即為表示式的結果

例如: (3+4)×5-6 對應的字首表示式就是 - × + 3 4 5 6 , 針對字首表示式求值步驟如下:

  1. 從右至左掃描,將6、5、4、3壓入堆疊
  2. 遇到+運算子,因此彈出3和4(3為棧頂元素,4為次頂元素),計算出3+4的值,得7,再將7入棧
  3. 接下來是×運算子,因此彈出7和5,計算出7×5=35,將35入棧
  4. 最後是-運算子,計算出35-6的值,即29,由此得出最終結果

中綴表示式

  • 中綴表示式就是常見的運算表示式,如(3+4)×5-6

  • 中綴表示式的求值是我們人最熟悉的,但是對計算機來說卻不好操作(前面我們講的案例就能看的這個問題),因此,在計算結果時,往往會將中綴表示式轉成其它表示式來操作(一般轉成字尾表示式.)

字尾表示式

  • 字尾表示式又稱逆波蘭表示式,與字首表示式相似,只是運算子位於運算元之後

  • 舉例說明: (3+4)×5-6 對應的字尾表示式就是 3 4 + 5 × 6 –

在這裡插入圖片描述

中綴轉字尾表示式的計算機求值

從左至右掃描表示式,遇到數字時,將數字壓入堆疊,遇到運算子時,彈出棧頂的兩個數,用運算子對它們做相應的計算(次頂元素 和 棧頂元素),並將結果入棧;重複上述過程直到表示式最右端,最後運算得出的值即為表示式的結果

例如: (3+4)×5-6 對應的字尾表示式就是 3 4 + 5 × 6 - , 針對字尾表示式求值步驟如下:

  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,由此得出最終結果

我們完成一個逆波蘭計算器,要求完成如下任務:

  • 輸入一個逆波蘭表示式(字尾表示式),使用棧(Stack), 計算其結果
  • 支援小括號和多位數整數,因為這裡我們主要講的是資料結構,因此計算器進行簡化,只支援對整數的計算。
  • 思路分析
  • 程式碼完成

中綴轉字尾

大家看到,字尾表示式適合計算式進行運算,但是人卻不太容易寫出來,尤其是表示式很長的情況下,因此在開發中,我們需要利用字尾表示式計算器將中綴表示式轉成字尾表示式。

具體步驟如下:

  • 初始化兩個棧:運算子棧s1和儲存中間結果的棧s2;
  • 從左至右掃描中綴表示式;
  • 遇到運算元時,將其壓s2;
  • 遇到運算子時,比較其與s1棧頂運算子的優先順序:
    如果s1為空,或棧頂運算子為左括號“(”,則直接將此運算子入棧;
    否則,若優先順序比棧頂運算子的高,也將運算子壓入s1;
    否則,將s1棧頂的運算子彈出並壓入到s2中,再次轉到(4-1)與s1中新的棧頂運算子相比較;
  • 遇到括號時:
    (1) 如果是左括號“(”,則直接壓入s1
    (2) 如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄
  • 重複步驟2至5,直到表示式的最右邊
  • 將s1中剩餘的運算子依次彈出並壓入s2
  • 依次彈出s2中的元素並輸出,結果的逆序即為中綴表示式對應的字尾表示式

在這裡插入圖片描述

程式碼實現

package ah.sz.tp.algorithm1.stack;

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

/**
 *中綴轉字尾表示式
 * 
 * @author TimePause
 * @create 2020-01-14 19:07
 */
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)); // ?



		/*

		//先定義給逆波蘭表示式
		//(30+4)×5-6  => 30 4 + 5 × 6 - => 164
		// 4 * 5 - 8 + 60 + 8 / 2 => 4 5 * 8 - 60 + 8 2 / +
		//測試
		//說明為了方便,逆波蘭表示式 的數字和符號使用空格隔開
		//String suffixExpression = "30 4 + 5 * 6 -";
		String suffixExpression = "4 5 * 8 - 60 + 8 2 / +"; // 76
		//思路
		//1. 先將 "3 4 + 5 × 6 - " => 放到ArrayList中
		//2. 將 ArrayList 傳遞給一個方法,遍歷 ArrayList 配合棧 完成計算

		List<String> list = getListString(suffixExpression);
		System.out.println("rpnList=" + list);
		int res = calculate(list);
		System.out.println("計算的結果是=" + res);

		*/
	}



	//即 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;//返回
	}

	//將一個逆波蘭表示式, 依次將資料和運算子 放入到 ArrayList中
	public static List<String> getListString(String suffixExpression) {
		//將 suffixExpression 分割
		String[] split = suffixExpression.split(" ");
		List<String> list = new ArrayList<String>();
		for(String ele: split) {
			list.add(ele);
		}
		return list;

	}

	//完成對逆波蘭表示式的運算
	/*
	 * 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;
	}

}

結果展示
在這裡插入圖片描述

完整版的逆波蘭計算器,功能包括

  • 支援 + - * / ( )
  • 多位數,支援小數,
  • 相容處理, 過濾任何空白字元,包括空格、製表符、換頁符

逆波蘭計算器完整版考慮的因素較多,下面給出完整版程式碼供同學們學習,其基本思路和前面一樣,也是使用到:中綴表示式轉字尾表示式。

public class ReversePolishMultiCalc {

    /**
     * 匹配 + - * / ( ) 運算子
     */
    static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";

    static final String LEFT = "(";
    static final String RIGHT = ")";
    static final String ADD = "+";
    static final String MINUS= "-";
    static final String TIMES = "*";
    static final String DIVISION = "/";

    /**
     * 加減 + -
     */
    static final int LEVEL_01 = 1;
    /**
     * 乘除 * /
     */
    static final int LEVEL_02 = 2;

    /**
     * 括號
     */
    static final int LEVEL_HIGH = Integer.MAX_VALUE;


    static Stack<String> stack = new Stack<>();
    static List<String> data = Collections.synchronizedList(new ArrayList<String>());

    /**
     * 去除所有空白符
     * @param s
     * @return
     */
    public static String replaceAllBlank(String s ){
        // \\s+ 匹配任何空白字元,包括空格、製表符、換頁符等等, 等價於[ \f\n\r\t\v]
        return s.replaceAll("\\s+","");
    }

    /**
     * 判斷是不是數字 int double long float
     * @param s
     * @return
     */
    public static boolean isNumber(String s){
        Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
        return pattern.matcher(s).matches();
    }

    /**
     * 判斷是不是運算子
     * @param s
     * @return
     */
    public static boolean isSymbol(String s){
        return s.matches(SYMBOL);
    }

    /**
     * 匹配運算等級
     * @param s
     * @return
     */
    public static int calcLevel(String s){
        if("+".equals(s) || "-".equals(s)){
            return LEVEL_01;
        } else if("*".equals(s) || "/".equals(s)){
            return LEVEL_02;
        }
        return LEVEL_HIGH;
    }

    /**
     * 匹配
     * @param s
     * @throws Exception
     */
    public static List<String> doMatch (String s) throws Exception{
        if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
        if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");

        s = replaceAllBlank(s);

        String each;
        int start = 0;

        for (int i = 0; i < s.length(); i++) {
            if(isSymbol(s.charAt(i)+"")){
                each = s.charAt(i)+"";
                //棧為空,(操作符,或者 操作符優先順序大於棧頂優先順序 && 操作符優先順序不是( )的優先順序 及是 ) 不能直接入棧
                if(stack.isEmpty() || LEFT.equals(each)
                        || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
                    stack.push(each);
                }else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
                    //棧非空,操作符優先順序小於等於棧頂優先順序時出棧入列,直到棧為空,或者遇到了(,最後操作符入棧
                    while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
                        if(calcLevel(stack.peek()) == LEVEL_HIGH){
                            break;
                        }
                        data.add(stack.pop());
                    }
                    stack.push(each);
                }else if(RIGHT.equals(each)){
                    // ) 操作符,依次出棧入列直到空棧或者遇到了第一個)操作符,此時)出棧
                    while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
                        if(LEVEL_HIGH == calcLevel(stack.peek())){
                            stack.pop();
                            break;
                        }
                        data.add(stack.pop());
                    }
                }
                start = i ;    //前一個運算子的位置
            }else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
                each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
                if(isNumber(each)) {
                    data.add(each);
                    continue;
                }
                throw new RuntimeException("data not match number");
            }
        }
        //如果棧裡還有元素,此時元素需要依次出棧入列,可以想象棧裡剩下棧頂為/,棧底為+,應該依次出棧入列,可以直接翻轉整個stack 新增到佇列
        Collections.reverse(stack);
        data.addAll(new ArrayList<>(stack));

        System.out.println(data);
        return data;
    }

    /**
     * 算出結果
     * @param list
     * @return
     */
    public static Double doCalc(List<String> list){
        Double d = 0d;
        if(list == null || list.isEmpty()){
            return null;
        }
        if (list.size() == 1){
            System.out.println(list);
            d = Double.valueOf(list.get(0));
            return d;
        }
        ArrayList<String> list1 = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            list1.add(list.get(i));
            if(isSymbol(list.get(i))){
                Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
                list1.remove(i);
                list1.remove(i-1);
                list1.set(i-2,d1+"");
                list1.addAll(list.subList(i+1,list.size()));
                break;
            }
        }
        doCalc(list1);
        return d;
    }

    /**
     * 運算
     * @param s1
     * @param s2
     * @param symbol
     * @return
     */
    public static Double doTheMath(String s1,String s2,String symbol){
        Double result ;
        switch (symbol){
            case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
            case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
            case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
            case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
            default : result = null;
        }
        return result;

    }

    public static void main(String[] args) {
        //String math = "9+(3-1)*3+10/2";
        String math = "12.8 + (2 - 3.55)*4+10/5.0";
        try {
            doCalc(doMatch(math));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

執行結果
在這裡插入圖片描述

遞迴

遞迴就是方法自己呼叫自己, 且每次呼叫時傳入不同變數
遞迴的出現是幫助解決複雜問題, 簡化程式碼

遞迴程式碼舉例

public static int factorial(int n) {
	if (n == 1) {
		return 1;
	} else {
		return factorial(n - 1) * n;
}}

遞迴使用的場景

  • 如階乘, 複製整個資料夾的實現, 以及商城專案中商品分類樹的新增和刪除邏輯

  • 各種數學問題如: 8皇后問題 , 漢諾塔, 階乘問題, 迷宮問題, 球和籃子的問題(google程式設計大賽)

  • 各種演算法中也會使用到遞迴,比如快排,歸併排序,二分查詢,分治演算法等.

  • 將用棧解決的問題–>第歸程式碼比較簡潔

遞迴原則

  • 執行一個方法時,就建立一個新的受保護的獨立空間(棧空間)

  • 方法的區域性變數是獨立的,不會相互影響, 比如n變數

  • 如果方法中使用的是引用型別變數(比如陣列),就會共享該引用型別的資料.

  • 遞迴必須向退出遞迴的條件逼近,否則就是無限遞迴,出現StackOverflowError,死龜了:)

  • 當一個方法執行完畢,或者遇到return,就會返回,遵守誰呼叫,就將結果返回給誰,同時當方法執行完畢或者返回時,該方法也就執行完畢。

遞迴實現迷宮問題

在這裡插入圖片描述
思路分析

  • 使用二維陣列模擬迷宮,8行7列.
  • 使用1代表擋板,不能行動, 2代表可以移動, 3代表是死路
  • 設定座標(1,1)為起點,(6,5)為終點
  • 方法一採取下右上左的策略, 方法二採取上右下左
  • 在沒有使用演算法的情況下如果尋找最小路徑?
    將所有策略寫成方法, 然後將路徑放入集合中, 統計集合的大小並比較即可得到最短路徑

程式碼實現

/**
 * 迷宮回溯問題
 *
 * @author TimePause
 * @create 2020-01-26 21:26
 */
public class MiGong {

    public static void main(String[] args) {
        // 先建立一個二維陣列(8行7列),模擬迷宮
        // 地圖
        int[][] map = new int[8][7];
        // 使用1 表示牆
        // 上下全部置為1(第0行-第6行的列)
        for (int i = 0; i < 7; i++) {
            map[0][i] = 1;
            map[7][i] = 1;
        }

        // 左右全部置為1(第0列-第7列的行)
        for (int i = 0; i < 8; i++) {
            map[i][0] = 1;
            map[i][6] = 1;
        }
        //設定擋板, 1 表示
        map[3][1] = 1;
        map[3][2] = 1;
//		map[1][2] = 1;
//		map[2][2] = 1;

        // 輸出地圖
        System.out.println("地圖的情況");
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }

        //使用遞迴回溯給小球找路
        //setWay(map, 1, 1);
        setWay2(map, 1, 1);

        //輸出新的地圖, 小球走過,並標識過的遞迴
        System.out.println("小球走過,並標識過的 地圖的情況");
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }

    }

    //使用遞迴回溯來給小球找路
    //說明
    //1. map 表示地圖
    //2. i,j 表示從地圖的哪個位置開始出發 (1,1)
    //3. 如果小球能到 map[6][5] 位置,則說明通路找到.
    //4. 約定: 當map[i][j] 為 0 表示該點沒有走過 當為 1 表示牆  ; 2 表示通路可以走 ; 3 表示該點已經走過,但是走不通
    //5. 在走迷宮時,需要確定一個策略(方法) 下->右->上->左 , 如果該點走不通,再回溯
    /**
     *
     * @param map 表示地圖
     * @param i 從哪個位置開始找
     * @param j
     * @return 如果找到通路,就返回true, 否則返回false
     */
    public static boolean setWay(int[][] map, int i, int j) {
        if(map[6][5] == 2) { // 通路已經找到ok
            return true;
        } else {
            if(map[i][j] == 0) { //如果當前這個點還沒有走過
                //按照策略 下->右->上->左  走
                map[i][j] = 2; // 假定該點是可以走通.
                if(setWay(map, i+1, j)) {//向下走
                    return true;
                } else if (setWay(map, i, j+1)) { //向右走
                    return true;
                } else if (setWay(map, i-1, j)) { //向上
                    return true;
                } else if (setWay(map, i, j-1)){ // 向左走
                    return true;
                } else {
                    //說明該點是走不通,是死路
                    map[i][j] = 3;
                    return false;
                }
            } else { // 如果map[i][j] != 0 , 可能是 1, 2, 3
                return false;
            }
        }
    }

    //修改找路的策略,改成 上->右->下->左
    public static boolean setWay2(int[][] map, int i, int j) {
        if(map[6][5] == 2) { // 通路已經找到ok
            return true;
        } else {
            if(map[i][j] == 0) { //如果當前這個點還沒有走過
                //按照策略 上->右->下->左
                map[i][j] = 2; // 假定該點是可以走通.
                if(setWay2(map, i-1, j)) {//向上走
                    return true;
                } else if (setWay2(map, i, j+1)) { //向右走
                    return true;
                } else if (setWay2(map, i+1, j)) { //向下
                    return true;
                } else if (setWay2(map, i, j-1)){ // 向左走
                    return true;
                } else {
                    //說明該點是走不通,是死路
                    map[i][j] = 3;
                    return false;
                }
            } else { // 如果map[i][j] != 0 , 可能是 1, 2, 3
                return false;
            }
        }
    }

}


採取第一種策略(下右上左)
在這裡插入圖片描述

採取第二種策略(上右下左)
在這裡插入圖片描述

8皇后問題

8皇后問題, 作為回溯演算法的經典案例, 於1848年提出: 在8x8 的國際象棋上擺放著八個皇后, 使其不能相互攻擊, 即: 在同一列或同一斜線上, 有多少種擺法.(92)

思路圖解
在這裡插入圖片描述
具體思路
在這裡插入圖片描述
實現程式碼

public class Queue8 {

	//1.定義一個max表示共有多少個皇后
	int max = 8;
	//2.定義陣列array, 儲存皇后放置位置的結果,比如 arr = {0 , 4, 7, 5, 2, 6, 1, 3}
	int[] array = new int[max];
	static int count = 0;
	static int judgeCount = 0;
	public static void main(String[] args) {
		//測試一把 , 8皇后是否正確
		Queue8 queue8 = new Queue8();
		queue8.check(0);
		System.out.printf("一共有%d解法", count);
		System.out.printf("一共判斷衝突的次數%d次", judgeCount); // 1.5w

	}



	//5.編寫一個方法,放置第n個皇后
	//特別注意: check 是每一次遞迴時,進入到check中都有  for(int i = 0; i < max; i++),因此會有回溯
	private void check(int n) {
		if(n == max) {  //n = 8 , 其實8個皇后就既然放好
			print();
			return;
		}

		//6.依次放入皇后,並判斷是否衝突
		for(int i = 0; i < max; i++) {
			//先把當前這個皇后 n , 放到該行的第1列
			array[n] = i;
			//判斷當放置第n個皇后到i列時,是否衝突
			if(judge(n)) { // 不衝突
				//接著放n+1個皇后,即開始遞迴
				check(n+1); //
			}
			//如果衝突,就繼續執行 array[n] = i; 即將第n個皇后,放置在本行得 後移的一個位置
		}
	}

	//4.檢視當我們放置第n個皇后, 就去檢測該皇后是否和前面已經擺放的皇后衝突
	/**
	 *
	 * @param n 表示第n個皇后
	 * @return
	 */
	private boolean judge(int n) {
		judgeCount++;
		for(int i = 0; i < n; i++) {
			// 說明
			//1. array[i] == array[n]  表示判斷 第n個皇后是否和前面的n-1個皇后在同一列
			//2*. Math.abs(n-i) == Math.abs(array[n] - array[i]) 表示判斷第n個皇后是否和第i皇后是否在同一斜線
			// n = 1  放置第 2列 1 n = 1 array[1] = 1
			// Math.abs(1-0) == 1  Math.abs(array[n] - array[i]) = Math.abs(1-0) = 1
			//3. 判斷是否在同一行, 沒有必要,n 每次都在遞增
			if(array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n] - array[i]) ) {
				return false;
			}
		}
		return true;
	}

	//3.寫一個方法,可以將皇后擺放的位置輸出
	private void print() {
		count++;
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i] + " ");
		}
		System.out.println();
	}

}

相關文章