一、堆棧的基本概念:
堆棧(也簡稱作棧)是一種特殊的線性表,堆棧的數據元素以及數據元素間的邏輯關系和線性表完全相同,其差別是線性表允許在任意位置進行插入和刪除操作,而堆棧只允許在固定一端進行插入和刪除操作。
先進后出:堆棧中允許進行插入和刪除操作的一端稱為棧頂,另一端稱為棧底。堆棧的插入和刪除操作通常稱為進棧或入棧,堆棧的刪除操作通常稱為出棧或退棧。
備注:棧本身就是一個線性表,所以我們之前討論過線性表的順序存儲和鏈式存儲,對於棧來說,同樣適用。
二、堆棧的抽象數據類型:
數據集合:
堆棧的數據集合可以表示為a0,a1,…,an-1,每個數據元素的數據類型可以是任意的類類型。
操作集合:
(1)入棧push(obj):把數據元素obj插入堆棧。
(2)出棧pop():出棧, 刪除的數據元素由函數返回。
(3)取棧頂數據元素getTop():取堆棧當前棧頂的數據元素並由函數返回。
(4)非空否notEmpty():若堆棧非空則函數返回true,否則函數返回false。
三、順序棧:
順序存儲結構的堆棧稱作順序堆棧。其存儲結構示意圖如下圖所示:
1、順序棧的實現:
(1)設計Stack接口
(2)實現SequenceStack類
注:棧是線性表的特例,線性表本身就是用數組來實現的。於是,順序棧也是用數組實現的。
代碼實現:
(1)Stack.java:(Stack接口)
1 public interface Stack { 2 3 //入棧 4 public void push(Object obj) throws Exception; 5 6 //出棧 7 public Object pop() throws Exception; 8 9 //獲取棧頂元素 10 public Object getTop() throws Exception; 11 12 //判斷棧是否為空 13 public boolean isEmpty(); 14 }
(2)SequenceStack.java:
1 //順序棧 2 public class SequenceStack implements Stack { 3 4 Object[] stack; //對象數組(棧用數組來實現) 5 final int defaultSize = 10; //默認最大長度 6 int top; //棧頂位置(的一個下標):其實可以理解成棧的實際長度 7 int maxSize; //最大長度 8 9 //如果用無參構造的話,就設置默認長度 10 public SequenceStack() { 11 init(defaultSize); 12 } 13 14 //如果使用帶參構造的話,就調用指定的最大長度 15 public SequenceStack(int size) { 16 init(size); 17 } 18 19 public void init(int size) { 20 this.maxSize = size; 21 top = 0; 22 stack = new Object[size]; 23 } 24 25 //獲取棧頂元素 26 @Override 27 public Object getTop() throws Exception { 28 // TODO Auto-generated method stub 29 if (isEmpty()) { 30 throw new Exception("堆棧為空!"); 31 } 32 33 return stack[top - 1]; 34 } 35 36 //判斷棧是否為空 37 @Override 38 public boolean isEmpty() { 39 // TODO Auto-generated method stub 40 return top == 0; 41 } 42 43 //出棧操作 44 @Override 45 public Object pop() throws Exception { 46 // TODO Auto-generated method stub 47 if (isEmpty()) { 48 throw new Exception("堆棧為空!"); 49 } 50 top--; 51 52 return stack[top]; 53 } 54 55 //入棧操作 56 @Override 57 public void push(Object obj) throws Exception { 58 // TODO Auto-generated method stub 59 //首先判斷棧是否已滿 60 if (top == maxSize) { 61 throw new Exception("堆棧已滿!"); 62 } 63 stack[top] = obj; 64 top++; 65 } 66 }
2、測試類:
設計一個順序棧,從鍵盤輸入十個整數壓進棧,然后再彈出棧,並打印出棧序列。
代碼實現:
(3)Test.java:
1 import java.util.Scanner; 2 3 public class Test { 4 public static void main(String[] args) throws Exception { 5 SequenceStack stack = new SequenceStack(10); 6 7 Scanner in = new Scanner(System.in); 8 int temp; 9 for (int i = 0; i < 10; i++) { 10 System.out.println("請輸入第" + (i + 1) + "個整數:"); 11 temp = in.nextInt(); 12 stack.push(temp); 13 } 14 15 //遍歷輸出 16 while (!stack.isEmpty()) { 17 System.out.println(stack.pop()); 18 } 19 } 20 }
運行效果:
四、Java中棧與堆的區別:
棧(stack):(線程私有)
是一個先進后出的數據結構,通常用於保存方法(函數)中的參數,局部變量。在java中,所有基本類型和引用類型的引用都在棧中存儲。棧中數據的生存空間一般在當前scopes內(就是由{...}括起來的區域)。
堆(heap):(線程共享)
是一個可動態申請的內存空間(其記錄空閑內存空間的鏈表由操作系統維護),C中的malloc語句所產生的內存空間就在堆中。在java中,所有使用new xxx()構造出來的對象都在堆中存儲,當垃圾回收器檢測到某對象未被引用,則自動銷毀該對象。所以,理論上說java中對象的生存空間是沒有限制的,只要有引用類型指向它,則它就可以在任意地方被使用。
五、hashCode與對象之間的關系:
如果兩個對象的hashCode不相同,那么這兩個對象肯定也不同。
如果兩個對象的hashCode相同,那么這兩個對象有可能相同,也有可能不同。
總結一句:不同的對象可能會有相同的hashCode;但是如果hashCode不同,那肯定不是同一個對象。
代碼舉例:
1 public class StringTest { 2 3 public static void main(String[] args) { 4 5 //s1 和 s2 其實是同一個對象。對象的引用存放在棧中,對象存放在方法區的字符串常量池 6 String s1 = "china"; 7 String s2 = "china"; 8 9 //凡是用new關鍵創建的對象,都是在堆內存中分配空間。 10 String s3 = new String("china"); 11 12 //凡是new出來的對象,絕對是不同的兩個對象。 13 String s4 = new String("china"); 14 15 System.out.println(s1 == s2); //true 16 System.out.println(s1 == s3); 17 System.out.println(s3 == s4); 18 System.out.println(s3.equals(s4)); 19 20 System.out.println("\n-----------------\n"); 21 /*String很特殊,重寫從父類繼承過來的hashCode方法,使得兩個 22 *如果字符串里面的內容相等,那么hashCode也相等。 23 **/ 24 25 //hashCode相同 26 System.out.println(s3.hashCode()); //hashCode為94631255 27 System.out.println(s4.hashCode()); //hashCode為94631255 28 29 //identityHashCode方法用於獲取原始的hashCode 30 //如果原始的hashCode不同,表明確實是不同的對象 31 32 //原始hashCode不同 33 System.out.println(System.identityHashCode(s3)); //2104928456 34 System.out.println(System.identityHashCode(s4)); //2034442961 35 36 System.out.println("\n-----------------\n"); 37 38 //hashCode相同 39 System.out.println(s1.hashCode()); //94631255 40 System.out.println(s2.hashCode()); //94631255 41 42 //原始hashCode相同:表明確實是同一個對象 43 System.out.println(System.identityHashCode(s1)); //648217993 44 System.out.println(System.identityHashCode(s2)); //648217993 45 } 46 }
上面的代碼中,注釋已經標明了運行的結果。通過運行結果我們可以看到,s3和s4的字符串內容相同,但他們是兩個不同的對象,由於String類重寫了hashCode方法,他們的hashCode相同,但原始的hashCode是不同的。
六、鏈式堆棧:
鏈式存儲結構的堆棧稱作鏈式堆棧。
與單鏈表相同,鏈式堆棧也是由一個個結點組成的,每個結點由兩個域組成,一個是存放數據元素的數據元素域data,另一個是存放指向下一個結點的對象引用(即指針)域next。
堆棧有兩端,插入數據元素和刪除數據元素的一端為棧頂,另一端為棧底。鏈式堆棧都設計成把靠近堆棧頭head的一端定義為棧頂。
依次向鏈式堆棧入棧數據元素a0, a1, a2, ..., an-1后,鏈式堆棧的示意圖如下圖所示:
1、設計鏈式堆棧:
(1)Node.java:結點類
1 //結點類 2 public class Node { 3 4 Object element; //數據域 5 Node next; //指針域 6 7 //頭結點的構造方法 8 public Node(Node nextval) { 9 this.next = nextval; 10 } 11 12 //非頭結點的構造方法 13 public Node(Object obj, Node nextval) { 14 this.element = obj; 15 this.next = nextval; 16 } 17 18 //獲得當前結點的后繼結點 19 public Node getNext() { 20 return this.next; 21 } 22 23 //獲得當前的數據域的值 24 public Object getElement() { 25 return this.element; 26 } 27 28 //設置當前結點的指針域 29 public void setNext(Node nextval) { 30 this.next = nextval; 31 } 32 33 //設置當前結點的數據域 34 public void setElement(Object obj) { 35 this.element = obj; 36 } 37 38 public String toString() { 39 return this.element.toString(); 40 } 41 }
(2)Stack.java:
1 //棧接口 2 public interface Stack { 3 4 //入棧 5 public void push(Object obj) throws Exception; 6 7 //出棧 8 public Object pop() throws Exception; 9 10 //獲得棧頂元素 11 public Object getTop() throws Exception; 12 13 //判斷棧是否為空 14 public boolean isEmpty(); 15 }
(3)LinkStack.java:
1 public class LinkStack implements Stack { 2 3 Node head; //棧頂指針 4 int size; //結點的個數 5 6 public LinkStack() { 7 head = null; 8 size = 0; 9 } 10 11 @Override 12 public Object getTop() throws Exception { 13 // TODO Auto-generated method stub 14 return head.getElement(); 15 } 16 17 @Override 18 public boolean isEmpty() { 19 // TODO Auto-generated method stub 20 return head == null; 21 } 22 23 @Override 24 public Object pop() throws Exception { 25 // TODO Auto-generated method stub 26 if (isEmpty()) { 27 throw new Exception("棧為空!"); 28 } 29 Object obj = head.getElement(); 30 head = head.getNext(); 31 size--; 32 return obj; 33 } 34 35 @Override 36 public void push(Object obj) throws Exception { 37 // TODO Auto-generated method stub 38 head = new Node(obj, head); 39 size++; 40 }
(4)Test.java:測試類
1 import java.util.Scanner; 2 3 public class Test { 4 5 public static void main(String[] args) throws Exception { 6 //SequenceStack stack = new SequenceStack(10); 7 LinkStack stack = new LinkStack(); 8 Scanner in = new Scanner(System.in); 9 int temp; 10 for (int i = 0; i < 10; i++) { 11 System.out.println("請輸入第" + (i + 1) + "個整數:"); 12 temp = in.nextInt(); 13 stack.push(temp); 14 } 15 //遍歷輸出 16 while (!stack.isEmpty()) { 17 System.out.println(stack.pop()); 18 } 19 } 20 }
運行效果:
七、堆棧的應用:
堆棧是各種軟件系統中應用最廣泛的數據結構之一。括號匹配和表達式計算是編譯軟件中的基本問題,其軟件設計中都需要使用堆棧。
- 括號匹配問題
- 表達式計算
1、括號匹配問題:
假設算術表達式中包含圓括號,方括號,和花括號三種類型。使用棧數據結構編寫一個算法判斷表達式中括號是否正確匹配,並設計一個主函數測試。
比如:
{a+[b+(c*a)/(d-e)]} 正確
([a+b)-(c*e)]+{a+b} 錯誤,中括號的次序不對
括號匹配有四種情況:
1.左右括號匹配次序不正確
2.右括號多於左括號
3.左括號多於右括號
4.匹配正確
下面我們就通過代碼把這四種情況列舉出來。
代碼實現:
1 public class Test { 2 3 //方法:將字符串轉化為字符串數組 4 public static String[] expToStringArray(String exp) { 5 int n = exp.length(); 6 String[] arr = new String[n]; 7 for (int i = 0; i < arr.length; i++) { 8 arr[i] = exp.substring(i, i + 1); 9 } 10 return arr; 11 } 12 13 //方法:括號匹配問題的檢測 14 public static void signCheck(String exp) throws Exception { 15 SequenceStack stack = new SequenceStack(); 16 String[] arr = Test.expToStringArray(exp); 17 for (int i = 0; i < arr.length; i++) { 18 if (arr[i].equals("(") || arr[i].equals("[") || arr[i].equals("{")) { //當碰到都是左邊的括號的時候,統統壓進棧 19 stack.push(arr[i]); 20 } else if (arr[i].equals(")") && !stack.isEmpty() && stack.getTop().equals("(")) { //當碰到了右小括號時,如果匹配正確,就將左小括號出棧 21 stack.pop(); 22 } else if (arr[i].equals(")") && !stack.isEmpty() && !stack.getTop().equals("(")) { 23 System.out.println("左右括號匹配次序不正確!"); 24 return; 25 } else if (arr[i].equals("]") && !stack.isEmpty() && stack.getTop().equals("[")) { 26 stack.pop(); 27 } else if (arr[i].equals("]") && !stack.isEmpty() && !stack.getTop().equals("[")) { 28 System.out.println("左右括號匹配次序不正確!"); 29 return; 30 } else if (arr[i].equals("}") && !stack.isEmpty() && stack.getTop().equals("{")) { 31 stack.pop(); 32 } else if (arr[i].equals("}") && !stack.isEmpty() && !stack.getTop().equals("{")) { 33 System.out.println("左右括號匹配次序不正確!"); 34 return; 35 } else if (arr[i].equals(")") || arr[i].equals("]") || arr[i].equals("}") && stack.isEmpty()) { 36 System.out.println("右括號多於左括號!"); 37 return; 38 } 39 } 40 if (!stack.isEmpty()) { 41 System.out.println("左括號多於右括號!"); 42 } else { 43 System.out.println("括號匹配正確!"); 44 } 45 } 46 47 48 public static void main(String[] args) throws Exception { 49 50 String str = "([(a+b)-(c*e)]+{a+b}"; 51 //括號匹配的檢測 52 Test.signCheck(str); 53 } 54 }
運行效果:
上方代碼中,第50行是一個錯誤的括號表達式,於是運行結果也很明顯了。
2、表達式計算:
比如:
3+(6-4/2)*5=23
其后綴表達式為:3642/-5*+# (#符號為結束符)
現在要做的是:
使用鏈式堆棧,設計一個算法計算表達式,當我們輸入后綴表達式后,能輸出運行結果。
代碼實現:
1 public class Test { 2 3 //方法:使用鏈式堆棧,設計一個算法計算表達式 4 public static void expCaculate(LinkStack stack) throws Exception { 5 char ch; //掃描每次輸入的字符。 6 int x1, x2, b = 0; //x1,x2:兩個操作數 ,b字符的ASCII碼 7 System.out.println("輸入后綴表達式並以#符號結束:"); 8 while ((ch = (char) (b = System.in.read())) != '#') { 9 //如果是數字,說明是操作數則壓入堆棧 10 if (Character.isDigit(ch)) { 11 stack.push(new Integer(Character.toString(ch))); 12 } 13 //如果不是數字,說明是運算符 14 else { 15 x2 = ((Integer) stack.pop()).intValue(); 16 x1 = ((Integer) stack.pop()).intValue(); 17 switch (ch) { 18 case '+': 19 x1 += x2; 20 break; 21 case '-': 22 x1 -= x2; 23 break; 24 case '*': 25 x1 *= x2; 26 break; 27 case '/': 28 if (x2 == 0) { 29 throw new Exception("分母不能為零!"); 30 } else { 31 x1 /= x2; 32 } 33 break; 34 } 35 stack.push(new Integer(x1)); 36 } 37 } 38 System.out.println("后綴表達式計算結果是:" + stack.getTop()); 39 } 40 41 public static void main(String[] args) throws Exception { 42 LinkStack stack = new LinkStack(); 43 //(2+3)*(3-1)/2=5的后綴表達式為:23+31-*2/ 44 //方法:鍵盤輸入后綴表達式,輸出的得到計算結果 45 Test.expCaculate(stack); 46 47 } 48 }
運行效果:






