js 建立一條通用連結串列

發表於2017-10-16

什麼是「連結串列科普」?

連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。

什麼是「順序儲存結構科普」?

在計算機中用一組地址連續的儲存單元依次儲存線性表的各個資料元素,稱作線性表的順序儲存結構。

多數高階語言的「陣列」使用「順序儲存結構」,不過早期的 javascript 引擎用了「鏈式儲存結構」。Chrome 的 V8 的陣列使用了「順序儲存結構」與「鏈式儲存結構」混合模式;大多數情況下,V8 下的陣列是「順序儲存結構」,所以我們就假裝 V8 的陣列使用的是「順序儲存結構」吧!(-_-! 心虛)

javascript 開發需要「連結串列」嗎?自問自答
大多數情況下 javascript 開發關心的是「資料的邏輯結構」而非「資料的儲存結構」,似乎「連結串列」跟 javascript 開發沒什麼關係。But…「連結串列」在一些情況下能有效提升程式碼的效能,特別是在H5遊戲的過程中。

假設有一個業務需要高頻率地向一張「線性表科普」插入或刪除節點。通常筆者會用陣列表示「線性表」,因為 javascript 的陣列有一系列成熟好用的 APIs (如:unshift / push / shift / pop / splice 等)可以完成插入與刪除節點的操作。但是陣列(順序儲存結構)的 unshift & shift & splice 的演算法時間複雜度是 O(n) ,這情況可能「連結串列」是更好的選擇。

圖解連結串列

先看一下最簡單的單向連結串列:

單連結串列結構圖

往連結串列裡插入一個節點:
插入節點

剔除連結串列裡的節點:
剔除節點

往連結串列裡插入一條連結串列:
插入連結串列

剪除連結串列的一段切片:
剪除切片

通過上面的圖示,可以很清晰地瞭解到單連結串列的優勢:插入節點或連結串列片段的演算法時間複雜度為O(2);刪除節點或連結串列片段的演算法時間複雜度為O(1)

實現雙向連結串列

「單向連結串列」效率雖然高,不過侷限性比較大。所以筆者想實現的是「雙向連結串列」。雙向連結串列插入節點或連結串列的演算法時間複雜度為 O(4),刪除節點或連結串列片段的演算法時間複雜度為O(2)
雙向連結串列的結構如下:

  • 節點指標 ——「前驅」與「後繼」
  • 連結串列指標 —— 「頭指標」、「尾指標」和「遊標指標」

雙向連結串列

用一個匿名物件作為連結串列上的節點,如下虛擬碼:

宣告變數 HEAD, TAIL, POINTER & length 分別指代「頭指標」,「尾指標」,「遊標指標」和 「連結串列長度」,那麼構建一個雙向連結串列如下虛擬碼:

連結串列結構本身是個很簡單的結構,20行左右程式碼可以完成雙向連結串列資料結構的構建。

定製 APIs

上一節雖然實現了「雙向連結串列」的資料結構,但連結串列還處在很原始的狀態,操作起來比較麻煩,為了方便操作連結串列得為連結串列量身定做一套 APIs。陣列有一系列成熟且好用的 APIs,筆者想借鑑陣列的 APIs 為連結串列定製以下的 APIs:

Name Detail Name Detail Name Detail Name Detail
shift 參見陣列 unshift 參見陣列 pop 參見陣列 push 參見陣列
slice 參見陣列 splice 參見陣列 concat 參見陣列 reverse 參見陣列
sort 參見陣列 indexOf 參見陣列 length[屬性] 參見陣列
at 指標定位 prev 指標前移 next 指標後退 curr 當前指標
first 頭節點 last 尾節點 remove 刪除節點 clone 克隆連結串列
insertAfter 插入節點 insertBefore 插入節點 insertChainAfter 插入連結串列 insertChainBefore 插入連結串列
HEAD[屬性] 頭指標 TAIL[屬性] 尾指標 setHead 重置頭指標 setTail 重置尾指標
POINTER[屬性] 遊標指標(當前位置) setPointer 設定當前指標

上表與陣列同名的 APIs,表示用法與功能與陣列一樣。

為了突顯「連結串列性」筆者新增了四個 insert*insert* 的作用是向主連結串列指定位置插入節點或連結串列。APIs 不小心被筆者寫多了,這裡就不展開介紹它們的實現過程了。有興趣的同學可以移步:https://github.com/leeenx/es6-utils/blob/master/modules/Chain_v2.js

迴圈連結串列

筆者以往都是用陣列來模擬迴圈連結串列,如下:

有了 Chain 類後,可以直接這樣寫:

結語

近期有些同學在問筆者,使用 Chain 類真的可以提升效能嗎?這個需要分情況,如果是比較長的「線性表」做高頻的「刪除」或「插入」操作,自然是使用 Chain 有演算法上的優勢。但是,對於短的「線性表」來說,使用陣列更快一些,因為 V8 的陣列效能相當高,筆者認為小於 200 的「線性表」都可以直接使用陣列。

本文實現的 Chain 類託管在:https://github.com/leeenx/es6-utils/blob/master/modules/Chain_v2.js

感謝耐心閱讀完本文章的讀者。本文僅代表筆者的個人觀點,如有不妥之處請不吝賜教。

相關文章