1. 程式人生 > >棧(stack)及其儲存結構和特點詳解

棧(stack)及其儲存結構和特點詳解

棧是一個有著特殊規則的資料結構。我們熟悉漢諾塔遊戲(如圖 1 所示),這裡有一個明確的規則,即每次只能移動頂端的一個圓盤。

圖 1 漢諾塔遊戲
棧也有這個特點。我們可以將棧視為漢諾塔中的一個柱子,我們往這個柱子上放置圓盤,先放下去的一定是最後才能拿出來的,而最後放下去的一定是最先拿出來的。這也是棧的最重要一個特點——後進先出(LIFO,Last In First Out),也可以說是先進後出(FILO,First In Last Out),我們無論如何只能從一端去操作元素。

棧又叫作堆疊(Stack),這裡說明一下不要將它和堆混淆。實際上堆和棧是兩個不同的概念,棧是一種只能在一端進行插入和刪除的線性資料結構。

一般來說,棧主要有兩個操作:一個是進棧(PUSH),又叫作入棧、壓棧;另一個是出棧(POP),或者叫作退棧。


其實棧是一種比較簡單的資料結構,但是由於其特性,又衍生了不少的相關演算法。

棧的儲存結構

棧一般使用一段連續的空間進行儲存,通常預先分配一個長度,可以簡單地使用陣列去實現,具體的儲存結構如圖 2 所示。


圖 2 棧的儲存結構
通過圖 2 可以清晰地看到,只有一個方向可以對棧內的元素進行操作,而棧中最下面的一個元素成為棧底,一般是陣列的第 0 個元素,而棧頂是棧內最後放入的元素。

一般而言,定義一個棧需要有一個初始的大小,這就是棧的初始容量。當需要放入的元素大於這個容量時,就需要進行擴容。

棧出入元素的操作如下。例如我們初始化一個長度為 10 的陣列,並向其中放入元素,根據棧的定義,只能從陣列的一端放入元素,我們設定這一端為陣列中較大下標的方向。我們放入第 1 個元素,由於棧內沒有元素,於是第 1 個元素就落到了陣列的第 0 個下標的位置上;接著放入第 2 個元素,第 2 個元素該放入下標為 1 的位置上;以此類推,當放入了 5 個元素時,第 5 個入棧的元素應該在陣列的第 4 個下標的位置上。

現在我們要進行出棧操作,出棧只能從一端操作,我們之前設定只能從陣列下標較大的方向操作,因此需要確定陣列中下標最大的一個方向中存在棧元素的位置下標是多少。我們一般會在棧中做個計數器來記錄這個值。現在棧中有 5 個元素,所以將陣列中的第 5 個位置也就是下標為 4 的元素出棧。此時陣列中只剩下 4 個元素了。

下面是棧的實現程式碼,這裡以整型元素為例,在 Java 類的高階語言中,資料型別可以換成物件。
package me.irfen.algorithm.ch02;
import java.util.Arrays;
public class Stack {
    private int size = 0;
    private int[] array;
    public Stack() {
        this(10);
    }

    public Stack(int init) {
        if (init <= 0) {
            init = 10;
        }
        array = new int[init];
    }

    /**
      * 入棧
      * @param item 入棧元素的值
    */
    public void push(int item) {
        if (size == array.length) {
            array = Arrays.copyOf(array, size * 2);
        }
        array[size++] = item;
    }

    /**
      * 獲取棧頂元素,但是沒有出棧
      * @return
      */
    public int peek() {
        if (size == 0) {
            throw new IndexOutOfBoundsException("棧裡已經空啦");
        }
        return array[size - 1];
    }

    /**
      * 出棧,同時獲取棧頂元素
      * @return
      */
    public int pop() {
        int item = peek();
        size --; // 直接使元素個數減1,不需要真的清除元素,下次入棧會覆蓋舊元素值
        return item;
    }

    /**
      * 棧是否滿了
      * @return
      */
    public boolean isFull() {
        return size == array.length;
    }

    /**
      * 棧是否為空棧
      * @return
      */
    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }
}
下面是測試程式碼:
package me.irfen.algorithm.ch02;
public class StackTest {
public static void main(String[] args) {
    Stack stack = new Stack(1); // 為了方便看出效果,設定初始陣列長度為1
    stack.push(1);
    stack.push(2);
    System.out.println(stack.size()); // 棧內元素個數為2,當前陣列長度也為2
    stack.push(3);
    System.out.println(stack.size()); // 棧內元素個數為3,當前陣列長度為4
    System.out.println(stack.peek()); // 獲取棧頂元素,為3,但是沒有出棧
    System.out.println(stack.size()); // 由於上面一行沒有出棧,所以元素個數還是3
    System.out.println(stack.pop()); // 棧頂元素出棧,返回3
    System.out.println(stack.pop()); // 棧頂元素出站,返回2
    System.out.println(stack.size()); // 出了兩次棧,當前元素個數為1
}
}

棧的特點

棧的特點是顯而易見的,只能在一端進行操作,遵循先進後出或者後進先出的原則。

棧的適用場景

什麼時使用棧?根據棧先進先出且只能在一端操作的特點,一般有下面幾個應用。

1) 逆序輸出

由於棧具有先進後出的特點,所以逆序輸出是其中一個非常簡單的應用。首先把所有元素按順序入棧,然後把所有元素出棧並輸出,輕鬆實現逆序輸出。

2) 語法檢查,符號成對出現

在程式語言中,一般括號都是成對出現的,比如“[”和“]”“{”和“}”“(”和“)”“<”和“>”(這裡的“<”和“>”排除了作為大於小於號的情況)。

凡是遇到括號的前半部分,即為入棧符號(PUSH);凡是遇到括號的後半部分,就比對是否與棧頂元素相匹配(PEEK),如果相匹配,則出棧(POP),否則就是匹配出錯。

3) 數制轉換(將十進位制的數轉換為2~9的任意進位制的數)

另一個應用就是用於實現十進位制與其他進位制的轉換規則。

通過求餘法,可以將十進位制數轉換為其他進位制,比如要轉為八進位制,則將原十進位制數除以 8,記錄餘數,然後繼續將商除以 8,一直到商等於 0 為止,最後將餘數倒著寫出來就行了。

依照這個原理,當我們要將 100 轉為八進位制數時,先將 100 除以 8,商 12 餘 4,第 1 個餘數 4 入棧;之後繼續用 12 除以 8,商 1 餘 4,第 2 個餘數 4 入棧;接著用 1 除以 8,商 0 餘 1,第 3 個餘數 1 入棧。最後將三個餘數全部出棧,就得到了 100 的八進位制數 144。當然,棧的應用不僅有這些,其他應用還有很多。比如常聽到的程式語言呼叫中的“函式棧”,就是我們在呼叫方法時計算機會執行 PUSH 方法,記錄呼叫,在 return 也就是方法結束之後,執行 POP 方法,完成前後對應。