# 順序棧與鏈式棧的圖解與實現
- 棧是一種特殊的線性表,它與線性表的區別體現在增刪操作上
- 棧的特點是先進後出,後進先出,也就是說棧的資料操作只能發生在末端,而不允許在中間節點進行操作
- 如上圖所示,對棧的增刪操作都只能在末端也就是棧頂操作,
- 棧既然是線性表那麼就存在表頭和表尾,不過在棧結構中,對其都進行限制改造,表尾用來輸入資料也叫做棧頂(
top
),相應的 表頭就是棧底(bottom
),棧頂和棧頂是兩個指標用來表示這個棧 - 與線性表類似,棧也是又順序表示和鏈式表示,分別稱作順序棧和鏈棧
棧的基本操作
- 如何通過棧這個後進先出的線性表,來實現增刪查呢?
- 初始時,棧內沒有資料,即空棧。此時棧頂就是棧底。
- 當存入資料時,最先放入的資料會進入棧底。接著加入的資料都會放入到棧頂的位置。
- 如果要刪除資料,也只能通過訪問棧頂的資料並刪除。對於棧的新增操作,通常也叫作
push
或壓棧。 - 對於棧的刪除操作,通常也叫作
pop
或出棧。對於壓棧和出棧,我們分別基於順序棧和鏈棧來分析
順序棧
- 順序棧即就是順序儲存元素的,通常順序棧我們可以通過陣列來實現,將陣列的首元素放在棧底,最後一個元素放在棧頂,之後指定一個
top
指標指向棧頂元素的位置 - 當棧中只有一個元素是,此時
top=0
,一般以top
是否為-1
來判定是否為空棧,當定義了棧的最大容量時,則棧頂top
必須小於最大容量值 - 下面我們通過
Java
程式碼實現一個順序棧,非常簡單如下:
/**
* @url: i-code.online
* @author: 雲棲簡碼
* @time: 2020/12/8 16:48
*/
public class Stack<T> {
private Object[] stack;
private int stackSize;
private int top = -1;
public Stack(int size){
stackSize = size;
stack = new Object[size];
}
public void push(T value){
if (top < stackSize-1){
top++;
stack[top] = value;
return;
}
throw new ArrayIndexOutOfBoundsException(top +"越界");
}
public T pop(){
if (top > -1){
top--;
return (T) stack[top+1];
}
throw new ArrayIndexOutOfBoundsException(top +"越界");
}
public boolean empty(){
return top == -1;
}
}
- 當需要新增資料元素,即入棧操作時,就需要將新插入元素放在棧頂,並將棧頂指標增加 1。如下圖所示:
- 刪除資料元素,即出棧操作,只需要
top-1
就可以了。
對於查詢操作,棧沒有額外的改變,跟線性表一樣,它也需要遍歷整個棧來完成基於某些條件的數值查詢,上述程式碼中並未去實現該功能
鏈棧
- 關於鏈式棧,就是用連結串列的方式對棧的表示。通常,可以把棧頂放在單連結串列的頭部,如下圖所示。由於鏈棧的後進先出,原來的頭指標就顯得毫無作用了。因此,對於鏈棧來說,是不需要頭指標的。相反,它需要增加指向棧頂的
top
指標,這是壓棧和出棧操作的重要支援。
- 對於連結串列我們新增都是在其後追加,但是對於鏈棧,新增資料的壓棧操作需要額外處理的,就是棧的
top
指標。如下圖所示,插入新的資料放在頭部,則需要讓新的結點指向原棧頂,即top
指標指向的物件,再讓top
指標指向新的結點。
- 在鏈式棧中進行刪除操作時,只能在棧頂進行操作。因此,將棧頂的
top
指標指向棧頂元素的next
指標即可完成刪除。對於鏈式棧來說,新增刪除資料元素沒有任何迴圈操作,其時間複雜度均為O(1)
。 - 通過程式碼簡單實現棧的操作,如下:
/**
* @url: i-code.online
* @author: 雲棲簡碼
* @time: 2020/12/8 20:57
*/
public class LinkedList<E> {
private Node<E> top = new Node<>(null,null);
public void push(E e){
Node<E> node = new Node<>(e,top.next);
top.next = node;
}
public E pop(){
if (top.next == null){
throw new NoSuchElementException();
}
final Node<E> next = top.next;
top.next = next.next;
return next.item;
}
private static class Node<E>{
E item;
Node<E> next;
public Node(E item, Node<E> next){
this.item = item;
this.next = next;
}
}
}
對於查詢操作,相對連結串列而言,鏈棧沒有額外的改變,它也需要遍歷整個棧來完成基於某些條件的數值查詢。
- 不管是順序棧還是鏈棧,資料的新增、刪除、查詢與線性表的操作原理極為相似,時間複雜度完全一樣,都依賴當前位置的指標來進行資料物件的操作。區別僅僅在於新增和刪除的物件,只能是棧頂的資料結點。
棧的案例
- 我們可以通過一個案例來看棧的具體使用,這裡選取
leetcode
上的案例來練習,如下
有效括號
- 給定一個只包括
'(',')','{','}','[',']'
的字串,判斷字串是否有效。有效字串需滿足:左括號必須與相同型別的右括號匹配,左括號必須以正確的順序匹配。例如,{ [ ( ) ( ) ] }
是合法的,而{ ( [ ) ] }
是非法的。 - 這個問題很適合採用棧來處理。原因是,在匹配括號是否合法時,左括號是從左到右依次出現,而右括號則需要按照“後進先出”的順序依次與左括號匹配。因此,實現方案就是通過棧的進出來完成。
- 具體的實現思路,我們可以遍歷字串從左起,當遇到左括號時進行壓榨操作,而到遇到右括號時則繼續出棧,判斷出棧的括號是否與當前的右括號是一對,如果不是則非法,如果一致則繼續遍歷直到結束
- 程式碼如下:
public boolean isValid(String s) {
Stack stack = new Stack();
for(int i =0;i<s.length();i++){
char curr = s.charAt(i);
if (isLeft(curr)) {
stack.push(curr);
}else {
if (stack.empty())
return false;
if (!isPair(curr,(char)stack.pop())){
return false;
}
}
}
if (stack.empty()){
return true;
}else {
return false;
}
}
public boolean isPair(char curr,char expt){
if ((expt == '[' && curr == ']') || (expt == '{' && curr == '}') || (expt == '(' && curr == ')'))
return true;
return false;
}
public boolean isLeft(char c){
if (c == '{' || c == '[' || c == '(')
return true;
return false;
}
總結
- 棧繼承了線性表特性,是一個特殊的線性表
- 棧只允許資料從棧頂進出,即棧的特性先進後出
- 不管是順序棧還是鏈式棧,它們對於資料的新增操作和刪除操作的時間複雜度都是
O(1)
。而在查詢操作中,棧和線性表一樣只能通過全域性遍歷的方式進行,也就是需要O(n)
的時間複雜度 - 當我們面臨頻繁增刪節點,同時資料順序有後來居上的特點時棧就是個不錯的選擇。例如,瀏覽器的前進和後退,括號匹配等問題
推薦閱讀
- 《Java併發程式設計-執行緒基礎》
- 《總算把執行緒六種狀態的轉換說清楚了!》
- 《[高頻面試]解釋執行緒池的各個引數含義》
- 《知道執行緒池的四種拒絕策略嗎?》
- 《java中常見的六種執行緒池詳解》
- 《基於synchronized的鎖的深度解析》?推薦
- 《JAVA中常見的阻塞佇列詳解》
- 《優雅關閉執行緒池的方案》
本文由AnonyStar 釋出,可轉載但需宣告原文出處。
歡迎關注微信公賬號 :雲棲簡碼 獲取更多優質文章
更多文章關注筆者部落格 :雲棲簡碼 i-code.online