算法(第四版)C# 習題題解——1.3.49 用 6 個棧實現一個 O(1) 隊列


因為這個解法有點復雜,因此單獨開一貼介紹。

那么這里就使用六個棧來解決這個問題。

這個算法來自於這篇論文

原文里用的是 Pure Lisp,不過語法很簡單,還是很容易看懂的。

先導知識——用兩個棧模擬一個隊列

如何使用兩個棧來模擬一個隊列操作?

這是一道很經典的題目,答案也有很多種,這里只介紹之后會用到的一種方法。

首先我們有兩個棧,H 和 T,分別用作出隊和入隊用。

 幻燈片1

這樣,入隊操作等同於向 T 添加元素,T 的入棧操作只需要 O(1) 時間。

幻燈片2

如果 H 不為空,出隊操作等同於 H 彈棧,H 的彈棧操作也只需要 O(1) 時間。

幻燈片4

但如果 H 為空,則需要將 T 中的元素依次彈出並壓入到 H 中,這是一個 O(n) 的操作。

幻燈片3

顯然,這種方式中,出隊操作的最壞時間復雜度是 O(n),並不滿足題目要求。

分攤 O(n)

那么,怎么解決這個問題呢?

一個很自然的想法是,如果在棧 H 變為空之前,我們就能逐步將棧 T 的內容彈出並壓入到另一個棧 H' 中,等到棧 H 為空時,直接交換 H 和 H' 即可。

假設目前的隊列狀態是這樣,有三個元素等待出隊,還有三個元素等待入隊。

幻燈片5

現在依次讓三個元素出隊,與此同時我們讓棧 T 中的元素依次進入 H' 中。

每一次出隊都執行兩個操作,元素出隊和元素復制(Pop & Push),時間復雜度 O(1) + O(1) + O(1) = O(1)。

第一次操作(出隊)

幻燈片6

第二次操作(出隊)

幻燈片7

第三次操作(出隊)

幻燈片8

現在棧 H 和棧 T 都為空,下一次出隊操作時,我們直接交換棧 H 和棧 H'(由於是交換引用,因此時間復雜度仍為 O(1))。

幻燈片9

之后再進行出隊操作。

這就是這個算法基本想法,在棧 H 變為空之前,分步將棧 T 中的內容分步復制到另一個棧中。

當棧 H 為空時直接用准備好的棧 H' 替代 H,保證時間復雜度為常數。

對復制時 Enqueue 的支持和 T' 的引入

剛才是一種理想情況,顯然我們的隊列在復制時不可能只發生出隊操作,為了增加對入隊操作的支持,我們引入臨時棧 T'。

例如我們有隊列狀態如下,現在啟動復制進程,入隊操作全部由 T' 完成。

幻燈片10

我們進行一次入隊操作和兩次出隊操作,如下組圖所示:

第一次操作(入隊)

幻燈片11

第二次操作(出隊)

幻燈片12

第三次操作(出隊)

幻燈片13

現在 H 和 T 均為空,下一次操作時(不論入隊還是出隊),我們先交換 H 和 H' 以及 T 和 T',同時讓入隊操作控制權回到 T。

0916fix

這樣,我們增加了對復制時入隊操作的支持,但還並不完全,只有在理想情況下才可以做到。

h 與 HR ,對復制時出入隊序列支持的擴展

在之前的例子中,當復制結束時 H 總是為空的,現在我們來討論一下復制結束時 H 不為空的情況。

如果復制結束時 H 不為空,直接交換的結果是我們丟失了原來棧 H 中的數據。

因此,在翻轉 T 的同時,我們還應翻轉 H 到 HR,並在最后將 HR 的內容再度翻轉並添加到 H' 上。

這個過程可以以下圖方式進行:

初始狀態:

 幻燈片15

第一次操作(入隊),H->HR ,T->H',時間復雜度 O(1) + O(1) + O(1) + O(1) + O(1) = O(1)。

幻燈片16

第二次操作(入隊)

幻燈片17

第三次操作(入隊)

幻燈片18

第四次操作(入隊)

幻燈片19

第五次操作(入隊)

幻燈片20

第六次操作(出/入隊執行前)

演示文稿1

這樣我們就解決了 H 復制結束后不為空的問題,代價是引入了兩個額外的問題:

  1. 操作次數增加到了 2k 次,k 代表棧 T 中的元素數量。(如果當 T 中元素數量大於 H 中元素數量時開始復制)
  2. 由於 H 被用於復制進程,我們無法在復制過程中支持出隊操作。

第一個問題解決方案比較簡單,我們可以在每一次出/入隊操作執行時進行兩次的復制步驟(對 T 和 H 進行兩次的 Pop 操作),時間復雜度仍為 O(1)。

第二個問題我們通過引入棧 h 來解決。

h 用於在復制時代替 H 執行出隊功能,它會在復制開始時自動變為棧 H 的一個淺拷貝(也就是說,h 和 H 共用同一片內存空間,但它們用於指示棧頂位置的指針相互獨立)。

現在我們有了全部 6 個棧,它們的功能如下圖所示(為了方便介紹我將一些棧的位置做了調換)。

幻燈片22

由於我們並不能預知接下來會發生的操作,因此當 H 棧中的元素數量第一次小於 T 棧中的元素數量時,我們就必須啟動復制進程了(總是假設接下來全部都是出隊操作)。我們引入一個布爾類型變量 IsCopying 來指示復制進程。

幻燈片23

現在我們進行第一次入隊操作,IsCopying = true,開始復制。

首先 h 變為 H 的淺拷貝,這個過程是 O(1) 的。

幻燈片24

如果在復制過程中有出隊操作,作為 H 的翻轉 HR 中就有一個元素不再需要復制,我們引入一個變量 needcopy 來記錄 HR 中需要復制的元素數量。

幻燈片25

接下來是兩次復制操作,T 和 H 分別有兩個元素進入了 H' 和 HR

幻燈片26

然后是第二次出/入隊操作,這次我們選擇出隊,1 出隊后顯然 HR 中的 1 不再需要復制,needcopy – 1。

幻燈片27

隨后再是兩次復制操作,第一次將 H 中的 3 移到 HR 中,needcopy + 1,T 中的 5 移到 H' 中;第二次只將 T 中的 4 移到 H' 中。

幻燈片28

第三次出/入隊操作我們選擇入隊,8 入隊。隨后 HR 中的兩個元素進入了 H',needcopy – 2。

幻燈片29

由於 needcopy 變成了 0,我們再額外進行一次交換操作,並將 IsCopying 置為 false。

幻燈片30

至此,完整的算法運行完畢。

有關復制開始時機的證明

這里我們選擇了在第 k + 1 個元素入隊時開始復制,現在證明一定能夠在 h 空之前完成復制:

假設復制開始時 H 有 k 個元素,T 有 k + 1個元素。

完成第一輪復制(H->HR , T->H')需要 k + 1 次操作,

完成第二輪復制(H->H')需要 k 次操作,總共需要 2k + 1 次操作才能完成復制。

而 h 的長度為 k,能夠提供 2k 次的操作機會。第 k + 1 個元素入隊時也能提供 2 次操作機會,因此一共是 2k + 2 次操作機會。

由於 2k + 1 < 2k + 2,我們證明了該算法能夠及時完成復制工作。

程序設計

根據之前的內容,我們可以開始設計程序了。主要實現三個功能,Enqueue(), Dequeue() 和 Peek()。

根據算法要求我們添加一個進行復制時操作的函數 OneStep(),用於執行元素的復制,棧交換等操作。

Peek() 只需要根據是否在進行復制選擇棧 h 或棧 H 進行 Peek()。

Enqueue()

  1. 如果不處於復制狀態
    1. 如果 H.Length – T.Length > 0,直接將元素壓入棧 T。
    2. 否則令 IsCopying = true,h 進行淺拷貝,進行兩次的 OneStep。
  2. 如果處於復制狀態,將元素壓入 T',進行兩次的 OneStep。

Dequeue()

  1. 如果不處於復制狀態
    1. 如果 H.Length – T.Length > 0,直接從 H 彈出元素。
    2. 否則從 H 彈出元素,IsCopying = true,h 進行淺拷貝,進行兩次的 OneStep。
  2. 如果處於復制狀態,從 h 彈出元素,needcopy - 1,進行兩次的 OneStep。

OneStep()

  1. 如果不處於復制狀態,什么也不做。
  2. 如果處於復制狀態。
    1. 如果 H 和 T 都不為空,從 H 搬運一個元素至 HR ,從 T 搬運一個元素至 H' ,needcopy + 1。
    2. 如果 H 為空但 T 不為空,從 T 搬運一個元素至 H' 。
    3. 如果 H 和 T 都為空,但 needcopy > 1,從 HR 搬運一個元素至 H' ,needcopy – 1。
    4. 如果 H 和 T 都為空,但 needcopy = 1,從 HR 搬運一個元素至 H' ,needcopy – 1,交換 H 和 H' 以及 T 和 T',其他棧置空,退出復制狀態。
    5. 如果 H 和 T 都為空,但 needcopy = 0,交換 H 和 H' 以及 T 和 T',其他棧置空,退出復制狀態。

 

程序實現(C#)

顯然光演示是不夠的,這里放上我自己寫的 C# 代碼供參考(HH = H', TT = T'):

using Generics;

namespace _1._3._49
{
    class StackQueue<Item>
    {
        Stack<Item> H;
        Stack<Item> T;
        Stack<Item> h;
        Stack<Item> HH;
        Stack<Item> TT;
        Stack<Item> Hr;

        bool isRecopying;
        int nowcopying;

        public StackQueue()
        {
            this.isRecopying = false;
            this.nowcopying = 0;

            this.H = new Stack<Item>();
            this.T = new Stack<Item>();
            this.h = new Stack<Item>();
            this.HH = new Stack<Item>();
            this.TT = new Stack<Item>();
            this.Hr = new Stack<Item>();
        }

        public Item Peek()
        {
            if (this.isRecopying)
            {
                return h.Peek();
            }
            else
            {
                return H.Peek();
            }
        }

        public void Enqueue(Item item)
        {
            if (!this.isRecopying && Lendiff() > 0)
            {
                this.nowcopying = 0;
                this.T.Push(item);
            }
            else if (!this.isRecopying && Lendiff() == 0)
            {
                this.T.Push(item);
                this.isRecopying = true;
                this.h = this.H.Copy();
                OneStep(OneStep(this));
            }
            else if (this.isRecopying)
            {
                this.TT.Push(item);
                OneStep(OneStep(this));
            }
        }

        public int Lendiff()
        {
            return this.H.Size() - this.T.Size();
        }

        public Item Dequeue()
        {
            if (!this.isRecopying && Lendiff() > 0)
            {
                return this.H.Pop();
            }
            else if (!this.isRecopying && Lendiff() == 0)
            {
                Item temp = this.H.Pop();
                this.h = this.H.Copy();
                this.isRecopying = true;
                OneStep(OneStep(this));
                return temp;
            }
            else
            {
                Item temp = this.h.Pop();
                this.nowcopying--;
                OneStep(OneStep(this));
                return temp;
            }
        }

        private static StackQueue<Item> OneStep(StackQueue<Item> q)
        {
            if (q.isRecopying && !q.H.IsEmpty() && !q.T.IsEmpty())
            {
                q.nowcopying++;
                q.HH.Push(q.T.Pop());
                q.Hr.Push(q.H.Pop());
            }
            else if (q.isRecopying && q.H.IsEmpty() && !q.T.IsEmpty())
            {
                q.isRecopying = true;
                q.HH.Push(q.T.Pop());
            }
            else if (q.isRecopying && q.H.IsEmpty() && q.T.IsEmpty() && q.nowcopying > 1)
            {
                q.isRecopying = true;
                q.nowcopying--;
                q.HH.Push(q.Hr.Pop());
            }
            else if (q.isRecopying && q.H.IsEmpty() && q.T.IsEmpty() && q.nowcopying == 1)
            {
                q.isRecopying = false;
                q.nowcopying--;
                q.HH.Push(q.Hr.Pop());
                q.H = q.HH;
                q.T = q.TT;
                q.HH = new Stack<Item>();
                q.TT = new Stack<Item>();
                q.Hr = new Stack<Item>();
                q.h = new Stack<Item>();
            }
            else if (q.isRecopying && q.H.IsEmpty() && q.T.IsEmpty() && q.nowcopying == 0)
            {
                q.isRecopying = false;
                q.H = q.HH;
                q.T = q.TT;
                q.HH = new Stack<Item>();
                q.TT = new Stack<Item>();
                q.Hr = new Stack<Item>();
                q.h = new Stack<Item>();
            }
            return q;
        }
    }
}

 

后記

事實上這道題還沒有結束,在英文版的《算法(第四版)》中,這道題要求的是只使用 3 個棧而不是有限個,具體可以查看 Stackoverflow 上的討論

討論的結果是 6 個棧顯然可行,3 個棧的解決方案用到了懶加載技術,還有一種 3 棧方案太過取巧(3 個“棧的棧“),最后還有人試圖證明 3 棧方案根本不可能。

顯然后來作者意識到了問題因此改成了有限個棧,那么三個棧實現 O(1) 隊列是否可能呢?

反正我是不知道怎么弄,尤其是看完 6 個棧的實現方案之后。 ╮(╯_╰)╭


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM