1008:棧
一、什麼是棧?
1.後進者先出,先進者後出,這就是典型的“棧”結構。
2.從棧的操作特性來看,是一種“操作受限”的線性表,只允許在端插入和刪除資料。
二、為什麼需要棧?
1.棧是一種操作受限的資料結構,其操作特性用陣列和連結串列均可實現。
2.但,任何資料結構都是對特定應用場景的抽象,陣列和連結串列雖然使用起來更加靈活,但卻暴露了幾乎所有的操作,難免會引發錯誤操作的風險。
3.所以,當某個資料集合只涉及在某端插入和刪除資料,且滿足後進者先出,先進者後出的操作特性時,我們應該首選棧這種資料結構。
三、如何實現棧?
1.棧的API
public class Stack<Item> { public void push(Item item){}//壓棧 public Item pop(){}//彈棧 public boolean isEmpty(){} //是否為空 public int size(){}//棧中資料的數量 public Item peek(){} //返回棧中最近新增的元素而不刪除它 }
2.陣列實現(自動擴容)
時間複雜度分析:根據均攤複雜度的定義,可以得陣列實現(自動擴容)符合大多數情況是O(1)級別複雜度,個別情況是O(n)級別複雜度,比如自動擴容時,會進行完整資料的拷貝。
空間複雜度分析:在入棧和出棧的過程中,只需要一兩個臨時變數儲存空間,所以O(1)級別。我們說空間複雜度的時候,是指除了原本的資料儲存空間外,演算法執行還需要額外的儲存空間。
實現程式碼:(棧的陣列實現)
public class StackOfArray<Item> implements Iterable<Item>{ //儲存資料的陣列 Item[] a = (Item[])new Object[1]; //記錄元素個數N int N = 0; //構造器 public StackOfArray(){} //新增元素 public void push(Item item){ //自動擴容 if (N == a.length ) resize(2*a.length ); a[N++] = item; } //刪除元素 public Item pop(){ Item item = a[--N]; a[N] = null; if (N > 0 && N == a.length / 4) resize(a.length / 2); return item; } //是否為空 public boolean isEmpty(){ return N == 0; } //元素個數 public int size(){ return N; } //改變陣列容量 private void resize(int length) { Item[] temp = (Item[])new Object[length]; for (int i = 0; i < N; i++) { temp[i] = a[i]; } a = temp; } //返回棧中最近新增的元素而不刪除它 public Item peek(){ return a[N-1]; } @Override public Iterator<Item> iterator() { return new ArrayIterator(); } //內部類 class ArrayIterator implements Iterator{ //控制迭代數量 int i = N; @Override public boolean hasNext() { return i > 0; } @Override public Item next() { return a[--i]; } } }
3.連結串列實現
時間複雜度分析:壓棧和彈棧的時間複雜度均為O(1)級別,因為只需更改單個節點的索引即可。
空間複雜度分析:在入棧和出棧的過程中,只需要一兩個臨時變數儲存空間,所以O(1)級別。我們說空間複雜度的時候,是指除了原本的資料儲存空間外,演算法執行還需要額外的儲存空間。
實現程式碼:(棧的連結串列實現)
public class StackOfLinked<Item> implements Iterable<Item> { //定義一個內部類,就可以直接使用型別引數 private class Node{ Item item; Node next; } private Node first; private int N; //構造器 public StackOfLinked(){} //新增 public void push(Item item){ Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; N++; } //刪除 public Item pop(){ Item item = first.item; first = first.next; N--; return item; } //是否為空 public boolean isEmpty(){ return N == 0; } //元素數量 public int size(){ return N; } //返回棧中最近新增的元素而不刪除它 public Item peek(){ return first.item; } @Override public Iterator<Item> iterator() { return new LinkedIterator(); } //內部類:迭代器 class LinkedIterator implements Iterator{ int i = N; Node t = first; @Override public boolean hasNext() { return i > 0; } @Override public Item next() { Item item = (Item) t.item; t = t.next; i--; return item; } } }
四、棧的應用
1.棧在函式呼叫中的應用
作業系統給每個執行緒分配了一塊獨立的記憶體空間,這塊記憶體被組織成“棧”這種結構,用來儲存函式呼叫時的臨時變數。每進入一個函式,就會將其中的臨時變數作為棧幀入棧,當被呼叫函式執行完成,返回之後,將這個函式對應的棧幀出棧。
2.棧在表示式求值中的應用(比如:34+13*9+44-12/3)
利用兩個棧,其中一個用來儲存運算元,另一個用來儲存運算子。我們從左向右遍歷表示式,當遇到數字,我們就直接壓入運算元棧;當遇到運算子,就與運算子棧的棧頂元素進行比較,若比運算子棧頂元素優先順序高,就將當前運算子壓入棧,若比運算子棧頂元素的優先順序低或者相同,從運算子棧中取出棧頂運算子,從運算元棧頂取出2個運算元,然後進行計算,把計算完的結果壓入運算元棧,繼續比較。
3.棧在括號匹配中的應用(比如:{}{[()]()})
用棧儲存為匹配的左括號,從左到右一次掃描字串,當掃描到左括號時,則將其壓入棧中;當掃描到右括號時,從棧頂取出一個左括號,如果能匹配上,則繼續掃描剩下的字串。如果掃描過程中,遇到不能配對的右括號,或者棧中沒有資料,則說明為非法格式。
當所有的括號都掃描完成之後,如果棧為空,則說明字串為合法格式;否則,說明未匹配的左括號為非法格式。
4.如何實現瀏覽器的前進後退功能?
我們使用兩個棧X和Y,我們把首次瀏覽的頁面依次壓如棧X,當點選後退按鈕時,再依次從棧X中出棧,並將出棧的資料一次放入Y棧。當點選前進按鈕時,我們依次從棧Y中取出資料,放入棧X中。當棧X中沒有資料時,說明沒有頁面可以繼續後退瀏覽了。當Y棧沒有資料,那就說明沒有頁面可以點選前進瀏覽了。
五、思考
1. 我們在講棧的應用時,講到用函式呼叫棧來儲存臨時變數,為什麼函式呼叫要用“棧”來儲存臨時變數呢?用其他資料結構不行嗎?
答:因為函式呼叫的執行順序符合後進者先出,先進者後出的特點。比如函式中的區域性變數的生命週期的長短是先定義的生命週期長,後定義的生命週期短;還有函式中呼叫函式也是這樣,先開始執行的函式只有等到內部呼叫的其他函式執行完畢,該函式才能執行結束。
正是由於函式呼叫的這些特點,根據資料結構是特定應用場景的抽象的原則,我們優先考慮棧結構。
2.我們都知道,JVM 記憶體管理中有個“堆疊”的概念。棧記憶體用來儲存區域性變數和方法呼叫,堆記憶體用來儲存 Java 中的物件。那 JVM 裡面的“棧”跟我們這裡說的“棧”是不是一回事呢?如果不是,那它為什麼又叫作“棧”呢?
答:JVM裡面的棧和我們這裡說的是一回事,被稱為方法棧。和前面函式呼叫的作用是一致的,用來儲存方法中的區域性變數。