線性表結構:棧

棧簡介

後進者先出,先進者後出,這就是典型的“棧”結構。

img

從棧的操作特性上來看,棧是一種“操作受限”的線性表,只允許在一端插入和刪除數據。

看了上面的介紹,你會發現從功能上來說,數組或鏈表是可以替代棧的。但你要知道,特定的數據結構是對特定場景的抽象,而且,數組或鏈表暴露了太多的操作接口,操作上的確靈活自由,但使用時就比較不可控,自然也就更容易出錯。

當某個數據集合只涉及在一端插入和刪除數據,並且滿足後進先出、先進後出的特性,我們就應該首選“棧”這種數據結構。

手動實現棧結構

從剛纔棧的定義裏,我們可以看出,棧主要包含兩個操作,入棧和出棧,也就是在棧頂插入一個數據和從棧頂刪除一個數據。理解了棧的定義之後,我們來看一看如何用代碼實現一個棧。

實際上,棧既可以用數組來實現,也可以用鏈表來實現。用數組實現的棧,我們叫作順序棧,用鏈表實現的棧,我們叫作鏈式棧。

public class MyStack<E> {

    private int size;
    private Object[] array;

    public MyStack() {
        // 默認創建長度是10的數組
        array = new Object[10];
        size = 0;
    }

    public MyStack(int capacity) {
        // 默認創建長度是10的數組
        array = new Object[capacity];
        size = 0;
    }

    public E pop() {
        if (size == 0) {
            return null;
        } else {
            E item = (E) array[size - 1];
            size--;
            return item;
        }
    }

    public boolean push(E item) {
        int len = array.length;
        if (len == size) {
            Object[] ta = new Object[(int) (len * 1.5)];
            System.arraycopy(array, 0, ta, 0, size);
            array = ta;
        }
        array[size] = item;
        size++;
        return true;
    }

}

上面操作的入棧和出棧操作的時間複雜度都是O(1)。

JDK中的棧結構

在Java中,我們一般用LinkedList來實現棧的功能。

img

LinkedList實現了Deque接口,Deque接口定義了pop和push方法。

public class MyStack<E> {

    private LinkedList<E> list;

    public MyStack() {
        this.list = new LinkedList<>();
    }

    public MyStack(E data){
        Set<E> singleton = Collections.singleton(data);
        this.list = new LinkedList<>(singleton);
    }

    public E pop() {
        return list.pop();
    }

    public void push(E item) {
        list.push(item);
    }
}

還是需要提示的是:LinkedList並不是線程安全的,如果需要做線程安全的棧,需要自己進行手動同步。

使用場景

1. 瀏覽器前進後退

在瀏覽器的同一個“tab”中訪問了多個頁面,我們可以使用瀏覽器的前進和後退功能來進行前後翻頁。

這個功能也可以通過棧來實現。

我們使用兩個棧,X 和 Y,我們把首次瀏覽的頁面依次壓入棧 X,當點擊後退按鈕時,再依次從棧 X 中出棧,並將出棧的數據依次放入棧 Y。當我們點擊前進按鈕時,我們依次從棧 Y 中取出數據,放入棧 X 中。當棧 X 中沒有數據時,那就說明沒有頁面可以繼續後退瀏覽了。當棧 Y 中沒有數據,那就說明沒有頁面可以點擊前進按鈕瀏覽了。

比如你順序查看了 a,b,c 三個頁面,我們就依次把 a,b,c 壓入棧,這個時候,兩個棧的數據就是這個樣子:

當你通過瀏覽器的後退按鈕,從頁面 c 後退到頁面 a 之後,我們就依次把 c 和 b 從棧 X 中彈出,並且依次放入到棧 Y。這個時候,兩個棧的數據就是這個樣子:

這個時候你又想看頁面 b,於是你又點擊前進按鈕回到 b 頁面,我們就把 b 再從棧 Y 中出棧,放入棧 X 中。此時兩個棧的數據是這個樣子:

這個時候,你通過頁面 b 又跳轉到新的頁面 d 了,頁面 c 就無法再通過前進、後退按鈕重複查看了,所以需要清空棧 Y。此時兩個棧的數據這個樣子:

2. 函數調用棧

每個函數調用被封裝成一個“棧幀”,每個“棧幀”就被放入棧中。

3. 表達式求值
爲了方便解釋,我將算術表達式簡化爲只包含加減乘除四則運算,比如:34+13*9+44-12/3。對於這個四則運算,我們人腦可以很快求解出答案,但是對於計算機來說,理解這個表達式本身就是個挺難的事兒。

實際上,編譯器就是通過兩個棧來實現的。其中一個保存操作數的棧,另一個是保存運算符的棧。我們從左向右遍歷表達式,當遇到數字,我們就直接壓入操作數棧;當遇到運算符,就與運算符棧的棧頂元素進行比較。

如果比運算符棧頂元素的優先級高,就將當前運算符壓入棧;如果比運算符棧頂元素的優先級低或者相同,從運算符棧中取棧頂運算符,從操作數棧的棧頂取 2 個操作數,然後進行計算,再把計算完的結果壓入操作數棧,繼續比較。

我將 3+5*8-6 這個表達式的計算過程畫成了一張圖,你可以結合圖來理解我剛講的計算過程。

對於表達式求值,你也可以使用一些求值引擎來實現。比如在Java中,你就可以調用JS引擎來實現表達式的值。

ScriptEngine engine= new ScriptEngineManager().getEngineByName("Nashorn");
String  js="var a=10;var b=20; var c=a+b;c;";
Double  o=  (Double) engine.eval(js);
System.out.println(o);

4. 括號匹配

假設表達式中只包含三種括號,圓括號 ()、方括號[]和花括號{},並且它們可以任意嵌套。比如,{[] ()[{}]}或[{()}([])]等都爲合法格式,而{[}()]或[({)]爲不合法的格式。那我現在給你一個包含三種括號的表達式字符串,如何檢查它是否合法呢?

這裏也可以用棧來解決。我們用棧來保存未匹配的左括號,從左到右依次掃描字符串。當掃描到左括號時,則將其壓入棧中;當掃描到右括號時,從棧頂取出一個左括號。如果能夠匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,則繼續掃描剩下的字符串。如果掃描的過程中,遇到不能配對的右括號,或者棧中沒有數據,則說明爲非法格式。

當所有的括號都掃描完成之後,如果棧爲空,則說明字符串爲合法格式;否則,說明有未匹配的左括號,爲非法格式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章