這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在[先前的博文](https://studygolang.com/articles/12686)中,我們探討了鏈表以及如何將它應用於實際應用。在這篇文章中,我們將繼續探討兩個相似且功能強大的資料結構。## 建模操作和曆史讓我們看看 Excel 或 Google 文檔,他們是人類發明的最普遍的構成檔案的應用程式。我們都使用過它們。 正如你可能知道的,這些應用程式有各種各樣對文本的操作。比如在文本中添加顏色、底線、各種字型和大小,或者在表格中組織內容。這個列表很長,我們期望從這些工具中得到一個普遍的功能 —— “撤銷”和“重做”已經執行了的操作的能力。你是否考慮過讓你做,你將如何規劃這樣的功能?下面讓我們探索一個可以協助我們完成這樣一項任務的資料結構。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/data-structures/excel.png)讓我們試著想想如何為這些應用程式的操作建模。此外,稍後我們將看到如何儲存動作的曆史並“撤銷”它們。一個簡單的 `Action struct` 看起來像這樣 :```gotype Action struct {name stringmetadata Meta// Probably some other data here...// 這裡或許還有一些其他資料}// 譯者注 :Action 結構體需要添加一個 next 欄位,否則下面代碼的部分操作不成立type Action struct {name stringmetadata Meta// 這裡或許還有一些其他資料next *Action}```我們現在只儲存名稱,而相對來說實際應用中會有更多元素。現在,當編輯器的使用者將一個函數應用到一堆文本中時,我們希望將該操作儲存在某個集合中,以便稍後可以“撤消”它。```gotype ActionHistory struct {top *Actionsize int}```這個 `ActionHistory` 資料結構,我們儲存一個在堆棧頂部指向 `Action` 的指標以及堆棧的大小。每當一個動作被執行,我們將把它連結到 `top` 的操作上。因此,當對文檔應用一個操作時,這是可以在後台啟動並執行。```gofunc (history *ActionHistory) Apply(newAction *Action) {if history.top != nil {oldTop := history.topnewAction.next = oldTop}history.top = newActionhistory.size++}````Add` 函數(譯者註:此處應為上文中的 `Apply`)會將最新的 `Action` 添加到 `ActionHistory` 的頂部。如果曆史結構在頂部有一個動作,它將通過將它與新的動作聯絡起來,將它往下壓。否則,它將把新操作附加到列表的頂部。現在,如果您知道鏈表(或者閱讀我最近的一篇關於鏈表的文章),你可能會發現他們的相似之處。到目前為止,我們在這裡使用的基本上依然是一個鏈表。那麼撤銷操作會是怎樣的呢?如下是一個 `Undo` 函數的實現:```gofunc (history *ActionHistory) Undo() *Action {topAction := history.topif topAction != nil {history.top = topAction.next} else if topAction.next == nil {history.top = nil}historyAction.size--//historyAction 沒有定義,應該是作者筆誤return topAction}//--------------------//譯者註:此處代碼存在問題,建議修改如下。``````gofunc (history *ActionHistory) Undo() *Action {topAction := new(Action)if history.size > 0 {topAction = history.tophistory.top = topAction.nexthistory.size--}return topAction}```感謝 [無聞](https://github.com/Unknwon) 的建議。如果你仔細觀擦,你會注意到這與從鏈表中刪除一個節點有一點不同。由於一個 `ActionHistory` 的性質,我們希望最後一個被執行了的動作是最先被撤銷的,這才是我們所希望實現的。這是堆棧的基本行為。堆棧是一種資料結構,您只能在堆棧頂部插入或刪除元素。把它想象成一堆檔案,或者你廚房抽屜裡的一堆盤子。如果你想從那一堆盤子取出最下面的盤子,那是挺難的。但是拿最上面的那個是簡單的。堆棧也被認為是 `LIFO` 結構 —— 意思是後進先出,我們前面解釋過那是為什麼。這基本上就是我們的 `Undo` 函數所處理的。如果堆棧(或者說`ActionHistory`)有多個 `Action` ,它將為第二項設定頂部連結。否則,它將清空 `ActionHistory`,將 `top` 元素設定為 `nil`。從 `Big-O` 標記法來看,在堆棧中搜尋的複雜度是 `O(n)`,但是在堆棧中插入和刪除是非常快的複雜度是 `O(1)`。這是因為遍曆整個堆棧,在最壞的情況下,仍然會在其中執行所有的 `n` 項,而插入和刪除元素的時間複雜度是常量時間,因為我們總是從堆棧的頂部插入和刪除。你可以在[*這裡*](https://play.golang.org/p/Eu8_-HTDBY_A) 使用該代碼的工作版本。## 行李控制我們大多數人都是坐飛機旅行的,而且知道所有人員都必須通過安檢才能上飛機。當然,這是為了我們的安全,但有時進行全部的掃描、檢查和測試是不必要的。機場安檢點的一個常見情境是安檢人員排起長龍,行李放在 x 光機的帶子上,而人們則通過金屬探測器門。也許我們對這些不甚瞭解,但是讓我們關注一下掃描我們的袋子的 x 光機。你有沒有想過,你會如何類比這台機器上發生的相互作用?當然,這些至少是看得見的。讓我們來探討一下這個想法。我們必須以某種方式將行李上的行李作為物品的集合,而 x 光機一次掃描一件行李。`Luggage` 結構體如下 :```gotype Luggage struct {weight intpassenger string}```與此同時,我們為 `Luggage` 類型添加簡單的建構函式:```gofunc NewLuggage(weight int, passenger string) *Luggage {l := Luggage{weight: weight,passenger: passenger, // just as an identifier}return &l}```接著,我們建立一個 `Belt` (流水線),讓 `Luggage` 放到上面並通過 X 光的檢測。```gotype Belt []*Luggage```不是你想要的?我們所建立的是一個 `Belt` 類型,實際上是 `Luggage` 指標的一部分。這就是所謂的傳送帶 —— 僅僅是一堆被逐一掃描的袋子。所以現在我們需要添加一個知道如何將 `Luggage` 添加到 `Belt` 的函數:```gofunc (belt *Belt) Add(newLuggage *Luggage) {*belt = append(*belt, newLuggage)}```既然 `Belt` 實際上是一個切片,那麼我們就可以用 Go 語言內建函數 `append` 將 `newLuggage` 添加到 `Belt` 上。這個實現很奇妙的部分是時間複雜度 -- 因為我們使用了 `append` 這個內建函數,所以插入操作的時間複雜度是 O(1)。當然,這有一定的控間浪費,這是一位 go 語言切片的工作原理造成的。當 `Belt` 開始運動並且將 `Luggage` 帶到 X 光機上,我們需要將行李拿下來並且裝進機器進行檢查。鑒於 `Belt` 的自然屬性,第一個放到傳送帶上面的行李是第一個被掃描監測的。自然地,最後一個放到傳送帶上的是最後一個被掃描的。所以我們可以說 `Belt` 是一個 FIFO(先進先出)的資料結構體。請留意上述的細節並看看如下 `Take` 函數的實現:```gofunc (belt *Belt) Take() *Luggage {first, rest := (*belt)[0], (*belt)[1:]*belt = restreturn first}```這個函數它取走了第一個元素並且將其返回,並且它會把集合中的其他東西都分配到它的開頭,所以它的第二個元素就會變成第一個,以此類推。你會發現,從隊列中取走第一個元素的時間複雜度是 `O(1)`。使用我們新的類型和函數能夠進行以下操作:```gofunc main() {belt := &Belt{}belt.Add(NewLuggage(3, "Elmer Fudd"))belt.Add(NewLuggage(5, "Sylvester"))belt.Add(NewLuggage(2, "Yosemite Sam"))belt.Add(NewLuggage(10, "Daffy Duck"))belt.Add(NewLuggage(1, "Bugs Bunny"))fmt.Println("Belt:", belt, "Length:", len(*belt))first := belt.Take()fmt.Println("First luggage:", first)fmt.Println("Belt:", belt, "Length:", len(*belt))}````main` 函數的輸出大致如下:```Belt: &[0x1040a0c0 0x1040a0d0 0x1040a0e0 0x1040a100 0x1040a110] Length: 5First luggage: &{3 Elmer Fudd}Belt: &[0x1040a0d0 0x1040a0e0 0x1040a100 0x1040a110] Length: 4```基本上,我們在 `Belt` 上加了5個不同的 `Luggage`,然後我們取出第一個元素,它在螢幕的第二行輸出顯示了。你可以在[*這裡*](https://play.golang.org/p/DTFUkWeZ4H8)使用執行個體代碼。## 頭等艙的乘客 ?恩,沒錯,是他們。是的,他們呢?我的意思是,他們已經花了那麼多錢買機票,他們在經濟艙的排隊中去等行李是不合理的。那麼,我們該如何優先考慮這些乘客呢?如果他們的行李有某種優先權,優先順序越高,他們通過隊列越快?讓我們對 `Luggage` 結構體進行修改,如下:```gotype Luggage struct {weight intpriority intpassenger string}```當然,我們使用 `newLuggage` 函數建立 `luggage` 的時候會加入 `priority` 作為參數。```gofunc NewLuggage(weight int, priority int, passenger string) *Luggage {l := Luggage{weight: weight,priority: priority,passenger: passenger,}return &l}```讓我們再想想。基本上,當一個新的 `Luggage` 被放在 `Belt` 上時,我們需要檢測它的 `priority`,並根據 `priority` 把它放在 `Belt`的最前面。我們在修改一下 `Add` 函數:```gofunc (belt *Belt) Add(newLuggage *Luggage) {if len(*belt) == 0 {*belt = append(*belt, newLuggage)} else {added := falsefor i, placedLuggage := range *belt {if newLuggage.priority > placedLuggage.priority {*belt = append((*belt)[:i], append(Belt{newLuggage}, (*belt)[i:]...)...)added = truebreak}}if !added {*belt = append(*belt, newLuggage)}}}```與之前的實現相比,這是相當複雜的。這裡要處理多種情況,第一種情況相對是簡單的。如果皮帶是空的,我們就把新的行李放在傳送帶上就可以了。 `Belt` 上只有一件東西,那第一個拿走就行了。第二種情況是在 `Belt` 上有不知一個元素,我們要遍曆 `Belt` 上的所有行李並且與將要加進來的行李進行優先順序比較。當找到一個優先順序比它小的行李的時候,那麼就會繞過這個優先順序小的行李,並且把新的行李放到它的前面。這就意味著優先順序越高的行李,將會在 `Belt` 的越靠前位置。當然,如果遍曆沒有找到這樣的行李,它會把它附加到 `Belt` 的末端。我們新的 `Add` 函數的時間複雜度是 `O(N)`,這是因為在最壞的情況下,我們往 `Luggage` 結構插入一個新的元素可能要遍曆整個切片。從本質上說,搜尋和訪問隊列中的任何項都是相同的複雜度 `O(n)`。為了示範新的添加功能,我們可以運行以下代碼:```gofunc main() {belt := make(Belt, 0)belt.Add(NewLuggage(3, 1, "Elmer Fudd"))belt.Add(NewLuggage(3, 1, "Sylvester"))belt.Add(NewLuggage(3, 1, "Yosemite Sam"))belt.Inspect()belt.Add(NewLuggage(3, 2, "Daffy Duck"))belt.Inspect()belt.Add(NewLuggage(3, 3, "Bugs Bunny"))belt.Inspect()belt.Add(NewLuggage(100, 2, "Wile E. Coyote"))belt.Inspect()}```首先我們建立一個有三個 `Luggage` 的 `Belt` ,這些 `Luggage` 的優先順序都是 1 :```0. &{3 1 Elmer Fudd}1. &{3 1 Sylvester}2. &{3 1 Yosemite Sam}```然後我們添加一個 優先順序為 2 的 `Luggage`:```0. &{3 2 Daffy Duck}1. &{3 1 Elmer Fudd}2. &{3 1 Sylvester}3. &{3 1 Yosemite Sam}```你看,帶著最高優先順序的新行李被提升到 `Belt` 上的第一個位置。接下來,我們再添加一個具有更高優先順序(3)的新元素:```0. &{3 3 Bugs Bunny}1. &{3 2 Daffy Duck}2. &{3 1 Elmer Fudd}3. &{3 1 Sylvester}4. &{3 1 Yosemite Sam}```正如預期的那樣,優先順序最高的那一個被放在了 `Belt` 的第一個位置。最後,我們再加一件行李,它的優先順序為 2 :```0. &{3 3 Bugs Bunny}1. &{3 2 Daffy Duck}2. &{100 2 Wile E. Coyote}3. &{3 1 Elmer Fudd}4. &{3 1 Sylvester}5. &{3 1 Yosemite Sam}```新的 `Luggage` 會被添加到優先順序相同的 `Luggage` 的後面,當然,不是 `Belt` 的開始位置。總的來說,當我們往 `Belt` 上添加 `Luggage` 都會被排序。如果你對隊列有一定的瞭解,那麼你可能會認為這些並不是實現優先順序隊列最有效方法,你是完全正確的。實現優先順序隊列可以更高效地使用堆,我們將在另一個博文中進行談論。我們可以探索更多關於優先隊列有趣的知識。你可以查看優先隊列的 [Wiki](https://en.wikipedia.org/wiki/Priority_queue) 頁面。如果你對隊列有一定的瞭解,那麼你可能會認為這些並不是實現優先順序隊列最有效方法,你是完全正確的。實現優先順序隊列可以更高效地使用堆,我們將在另一篇文章中對此進行介紹,特別是“實現”部分。你可以在[這裡](https://play.golang.org/p/Eu8_-HTDBY_A)查看並使用範例程式碼。
via: https://ieftimov.com/golang-datastructures-stacks-queues
作者:Ilija Eftimov 譯者:SergeyChang 校對:Unknwon
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
642 次點擊 ∙ 1 贊