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

来源:http://www.cnblogs.com/ikesnowy/archive/2017/07/12/7157813.html
-Advertisement-
Play Games

因為這個解法有點複雜,因此單獨開一貼介紹。《演算法(第四版)》中的題目是這樣的:1.3.49棧與隊列。用有限個棧實現一個隊列,保證每個隊列操作(在最壞情況下)都只需要常數次的棧操作。那麼這裡就使用六個棧來解決這個問題。這個演算法來自於這篇論文。原文里用的是 Pure Lisp,不過語法很簡單,還是很容易... ...


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

《演算法(第四版)》中的題目是這樣的:

1.3.49

棧與隊列。
用有限個棧實現一個隊列,
保證每個隊列操作(在最壞情況下)都只需要常數次的棧操作。

那麼這裡就使用六個棧來解決這個問題。

這個演算法來自於這篇論文

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

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

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

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

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

 幻燈片1

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

幻燈片2

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

演示文稿1

但如果 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。

演示文稿1

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

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)。

幻燈片16

第二次操作(入隊)

幻燈片17

第三次操作(入隊)

幻燈片18

第四次操作(入隊)

幻燈片19

第五次操作(入隊)

幻燈片20

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

幻燈片21

這樣我們就解決了 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.HH = this.H;
                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 個棧的實現方案之後。 ╮(╯_╰)╭


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這個不是nginx的問題,也不是dotnet core的問題,也不是mvc的問題,更不是防火牆的問題! 原因在於這個SeLinux 把它關了就可以了 感謝這個文章的作者! http://www.cnblogs.com/hager/p/5689493.html ...
  • 前言 使用Web頁面配置ESP8266的參數相對於使用串口AT指令配置更加直觀和簡單。與配置路由器方式類似。 基本思路 基本思路是ESP8266工作AP模式下,作為TCP Server監聽TCP Client的連接。因為網頁HTTP預設的埠是80,所以ESP8266作為TCP Server的埠需 ...
  • LINUX是開源的,這也是最主要的原因,想學Windows,Unix對不起,沒有源代碼。也正是因為這樣,LINUX才能夠像雪球一樣越滾越大,發展到現在這種規模。今天將為大家帶來關於Linux主流框架運維工作剖析,大家一定要認真閱讀哦~ 隨著IT運維的不斷發展,尤其的Linux的飛速發展,越來越多的企 ...
  • Win7 U盤安裝Ubuntu16.04 雙系統詳細教程 安裝主要分為以下幾步: 一. 下載Ubuntu 16.04鏡像軟體; 二. 製作U盤啟動盤使用ultraISO; 三. 安裝Ubuntu系統; 四. 用EasyBCD 創建啟動系統啟動引導; (根據個人情況,選擇性的安裝) 五. 開啟系統; ...
  • 5. 獲取進程名、進程號以及用戶 ID 查看埠和連接的信息時,能查看到它們對應的進程名和進程號對系統管理員來說是非常有幫助的。舉個慄子,Apache 的 httpd 服務開啟80埠,如果你要查看 http 服務是否已經啟動,或者 http 服務是由 apache 還是 nginx 啟動的,這時候 ...
  • 介紹什麼的就免了.直接進入正題 平臺: Windows 10 IDE : Visual studio 2017 首先從官網下載最新的SDK,https://sciter.com/download/ 創建流程. https://sciter.com/forums/topic/simple-questi ...
  • 解決網卡無法自動獲取IP址的方法 為了省錢或者一戶多機,很多人都購買寬頻路由器共用上網。在架設路由上網的時候,有些“師傅”可能不懂或是偷懶,開啟了寬頻路由器的DHCP( Dynamic Host Configuration Protocol(動態主機分配協議))功能,這樣,其他機子只要設置“自動獲取 ...
  • 背水一戰 Windows 10 之 控制項(集合類 - ListViewBase): ListView, GridView ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...