一.棧(Stack)的介紹
棧是一個先入後出(FILO:First In Last Out)的有序列表。棧(Stack)是限制線性表中元素的插入和刪除只能在同一端進行的一種特殊線性表。允許插入和刪除的一端,為變化的一端,稱為棧頂(Top),另一端為固定的一端,稱為棧底(Bottom)。根據棧的定義可知,最先放入棧中元素在棧底,最後放入的元素在棧頂而刪除元素剛好相反,最後放入的元素最先刪除,最先放入的元素最後刪除
二、入棧(壓棧)圖解三、出棧(彈棧)圖解四、應用場景1)子程式的呼叫:在跳往子程式前,會先將下個指令的地址存到堆疊中,直到子程式執行完後再將地址取出,以回到原來的程式中。
2)處理遞迴呼叫:和子程式的呼叫類似,只是除了儲存下一個指令的地址外,也將引數、區域變數等資料存入堆疊中。
3)表示式的轉換[中綴表示式轉字尾表示式]與求值(實際解決)。
4)二叉樹的遍歷。
5)圖形的深度優先(depth-first)搜尋法。
五、用陣列模擬棧思路:1)定義一個top來表示棧頂,初始化為-1
2)入棧的操作,當有資料加入到棧時,top++; stack[top] = data;
3)出棧的操作,int value = stack[top]; top--; return value;
程式碼實現://定義一個 ArrayStack 表示棧class ArrayStack { private int maxSize; // 棧的大小 private int[] stack; // 陣列,陣列模擬棧,資料就放在該陣列 private int top = -1;// top表示棧頂,初始化為-1 //構造器 public ArrayStack(int maxSize) { this.maxSize = maxSize; stack = new int[this.maxSize]; } //棧滿 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]); } }}
六、棧實現中綴表示式計算器中綴表示式就是常見的運算表示式,如(3+4)×5-6
public class Calculator { public static void main(String[] args) { //根據前面老師思路,完成表示式的運算 String expression = "7*2*2-5+1-5+3-4"; // 15//如何處理多位數的問題? //建立兩個棧,數棧,一個符號棧 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 = ""; //用於拼接 多位數 //開始while迴圈的掃描expression while(true) { //依次得到expression 的每一個字元 ch = expression.substring(index, index+1).charAt(0); //判斷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); //? "1+3" '1' => 1 //分析思路 //1. 當處理多位數時,不能發現是一個數就立即入棧,因為他可能是多位數 //2. 在處理數,需要向expression的表示式的index 後再看一位,如果是數就進行掃描,如果是符號才入棧 //3. 因此我們需要定義一個變數 字串,用於拼接 //處理多位數 keepNum += ch; //如果ch已經是expression的最後一位,就直接入棧 if (index == expression.length() - 1) { numStack.push(Integer.parseInt(keepNum)); }else{ //判斷下一個字元是不是數字,如果是數字,就繼續掃描,如果是運算子,則入棧 //注意是看後一位,不是index++ if (operStack.isOper(expression.substring(index+1,index+2).charAt(0))) { //如果後一位是運算子,則入棧 keepNum = "1" 或者 "123" numStack.push(Integer.parseInt(keepNum)); //重要的!!!!!!, 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 –
再比如:
1)字尾表示式的計算機求值
從左至右掃描表示式,遇到數字時,將數字壓入堆疊,遇到運算子時,彈出棧頂的兩個數,用運算子對它們做相應的計算(次頂元素 和 棧頂元素),並將結果入棧;重複上述過程直到表示式最右端,最後運算得出的值即為表示式的結果
例如: (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,由此得出最終結果
程式碼實現
//完成對逆波蘭表示式的運算/* * 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());}
2)中綴表示式轉字尾表示式
具體步驟如下:
(1) 初始化兩個棧:運算子棧s1和儲存中間結果的棧s2;
(2) 從左至右掃描中綴表示式;
(3) 遇到運算元時,將其壓s2;
(4) 遇到運算子時,比較其與s1棧頂運算子的優先順序:
(4-1) 如果s1為空,或棧頂運算子為左括號“(”,則直接將此運算子入棧;
(4-2) 否則,若優先順序比棧頂運算子的高,也將運算子壓入s1;
(4-3) 否則,將s1棧頂的運算子彈出並壓入到s2中,再次轉到(4-1)與s1中新的棧頂運算子相比較;
(5) 遇到括號時:
(5-1) 如果是左括號“(”,則直接壓入s1
(5-2) 如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄
(6)重複步驟2至5,直到表示式的最右邊
(7)將s1中剩餘的運算子依次彈出並壓入s2
(8)依次彈出s2中的元素並輸出,結果的逆序即為中綴表示式對應的字尾表示式
舉例說明:將中綴表示式“1+((2+3)×4)-5”轉換為字尾表示式的過程如下:
程式碼實現:
import java.util.ArrayList;import java.util.List;import java.util.Stack;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; }}
結尾本篇章主要是針對棧結構的具體解析,希望對大家有所幫助