Java中不可變的佇列如何實現? - XP123

banq發表於2021-11-18

函式式語言通常具有不可變的資料型別——一旦建立了物件,您就無法更改它,儘管您可以將其作為其他物件的一部分包含在內。不可變物件更容易推理,並防止程式碼的兩部分認為它們具有更改資料的獨佔訪問許可權的錯誤。我們將研究一種使用兩個Stack以不可變方式實現佇列Queue的方法。
 

不可變的Stack
傳統的堆疊Stack實現通常使用一個陣列加上一個到當前頂部的偏移量,或者一個單連結串列,其中指向列表的指標指向頂部。
從不變性的角度來看,陣列不是很好,因為它是一個旨在讀取和更新的資料塊。
基於列表的堆疊是更自然的選擇。堆疊有兩種可能性:

  1. 空堆疊,由 null 表示,-或-
  2. 包含內容的堆疊,由對包含值的單元格的引用和對堆疊其餘部分的“下一個”引用表示。

要將物件壓入堆疊,請為新單元分配資料,並將其指向當前的“頂部”單元。這不會影響現有堆疊。然後將頂部更改為指向剛剛分配的單元格。
要移除物件,請將頂部指標移至當前單元格中的“下一個”指標。這隻會改變“頂部”。
因此,我們需要有一個堆疊,它佔用的空間與專案的數量成正比,並且需要 O(1) 時間。(O(1) 意味著它使用固定的最大步數,並且永遠不必迴圈其內容。)

 

傳統的佇列Queue
與堆疊Stack一樣,傳統佇列有兩種常見的實現方式:陣列版本和列表版本。標記。我意識到這聽起來很混亂。在一個真實的例子上追蹤它,你就會明白它為什麼有效。(有關更深入的討論,請參閱參考資料中的“程式設計:使用隱藏的內容”。)
從不變性的角度來看,這兩種方法都有缺陷:我們要麼更新陣列的內容,要麼在現有單元格中處理指標。兩者都不是一成不變的。
兩種方法都需要 O(1) 來新增或刪除,但都使用可變性。
 

從堆疊Stack派生除的佇列Queue
我們想要一個不變的佇列,它佔用的空間與佇列中的單元格數量成正比,新增或刪除專案的時間複雜度為 O(1)。下面的兩棧演算法很接近。(我的靈感來自參考文獻中Haskell 書中的版本。)
這個想法是你有兩個堆疊:ins和outs兩個Stack。每當您新增新專案時,就將其新增到ins堆疊中。每當您移除它時,您就將其從outs堆疊中移除。
刪除顯然是一個棘手的問題——物件如何進入outs堆疊以將其刪除?訣竅是:如果你想刪除,而outs堆疊中沒有任何東西,那麼首先從insto彈出所有內容outs,然後從 中刪除outs。
這是 Java 中的一個實現。它使用 Java Stack 類,它實際上是一個基於陣列的實現,但這對我們來說並不重要。我們只關心它是一個 O(1) 堆疊。

public class Queue<T> {
    private Stack<T> ins = new Stack<>();
    private Stack<T> outs = new Stack<>();

    public boolean isEmpty() {
        return ins.isEmpty() && outs.empty();
    }

    public void insert(T item) {
        ins.push(item);
    }

    public T remove() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        if (outs.isEmpty()) {
           Queue.emptyStackTo(ins, outs);
        }
        return outs.pop();
    }

    private static <T> void emptyStackTo(
            Stack<T> ins, Stack<T> outs) {
        while (!ins.isEmpty()) {
            outs.push(ins.pop());
        }
    }
}

我們要求佇列不變地工作。在我們的程式碼中,我們持有兩個堆疊。這些都是不可變的,所以我們的佇列也是不可變的。
大小應該與專案的數量成正比。堆疊告訴我們:每條資料都在兩個堆疊之一中。所以使用的空間量是兩個堆疊空間的總和。
最後,我們想要 O(1) 的效能。堆疊為我們提供了它們push()和pop()操作。我們add()只是呼叫了一個堆疊操作,所以它是 O(1)。不過,remove()比較麻煩。
Remove()在呼叫堆疊的 O(1) 之前,可能必須將一個堆疊清空到另一個堆疊中pop()。但是我們的複製操作可能必須在某個時候複製一整堆資料。這個副本與使用的空間成正比——我們稱之為 O(n),其中 n 代表專案的數量。這使得我們的操作 O(n)。
我發現這個演算法在幾個層面上很有趣:
  • 我喜歡看到以不同方式實現的佇列,沒有陣列或棘手的連結串列。
  • 看看不變性如何改變資料結構很有趣。隨著我們使用更多的函式式語言和更多的多執行緒系統,這樣的技術將變得更加重要。(見Okasaki在參考文獻)。
  • 這是攤銷時間界限的一個很好的例子。(這讓我想知道我們是否可以以某種方式降低 O(n)。)

相關文章