當前位置: 華文星空 > 知識

用連結串列的目的是什麽?省空間還是省時間?

2015-10-06知識

這是一個典型的錯誤思考方向。

錯誤的根源在於,你把連結串列當成了一種整體的、不可分割不可更改的完整概念——然後,就著這個概念,考慮它的用途它的優點它的弱點,總結出一二三四然後背誦……完了。

完蛋。

這叫買櫝還珠。

實際上,講連結串列是為了給你引出「借助後向指標(next)組織數據」這麽一個設計思路;同時借助這個思路完成一個典型的套用案例、學著分析它的空間/時間復雜度……

然後,馬上領著你變換它、變形它、改進它……

比如,加上一個前向的prev指標,把單向連結串列改成雙向連結串列;或者把next指標去掉、換成left/right指標,把它改成二叉樹,等等。

這個過程中,真正想教給你的,是因地制宜的客製各種數據結構、分析其時空復雜度,為自己未來設計自己的演算法/數據結構鋪路。

因此,不要問「用連結串列的目的是什麽」,而是反過來問:「連結串列是為了解決什麽問題而發明的」、「有沒有更優方案」、「如何找出更優方案」、「如何證明方案更優」……終至於「當我遇到某個沒有先例的難題時,該如何優雅的解決它?」

當你這樣問時,問題就很簡單了。

1、連結串列是為了解決什麽問題而發明的?

為了解決動態數量的數據儲存。

比如說,我們要管理一堆票據,可能有一張,但也可能有一億張。

怎麽辦呢?申請50G的大陣列等著?萬一使用者只有一百張票據要處理呢?記憶體小於64G拒絕執行?可申請少了又不夠用啊……

而且,用陣列的話,刪除然後添加票據,是每次刪除讓後面五百萬張往前移一格呢、還是每次添加從頭搜尋空閑格子?如何區分空閑格子和值為0的數據?搞區分的話是多占用空間呢還是占用數據值域?占用了值域會不會使得數據處理變得格外復雜?會不會一不小心就和正常數據混淆?萬一拿來區分的欄位在某個版本後廢棄不用、或者擴充值域了呢?

你看,滿是棘手的問題。

那麽,連結串列這種東西就是個很有效的數據結構,可以很有效的管理這類不定量的數據。

2、有沒有更優方案?

有。

時間上,連結串列無法支持搜尋,想找到特定數據只能遍歷。

空間上,連結串列每個數據要額外占用一個指標的空間;對於int等基本數據類別,數據量暴增一倍(單連結串列)甚至兩倍。

那麽,為了在時間上最佳化它,我們可以搞成二叉樹;然後透過先序/後序/中序遍歷取得按一定規律排布的數據;也可以透過和根節點比較來快速確定數據在排序二叉樹的左還是右子樹上——這就得到了O(logN)的查詢效率。

但「隨隨便便插入數據」的二叉樹很可能「偏」的非常厲害;極端情況下就退化成了空間消耗更高但效率沒有絲毫提升的連結串列(絕大多數的左或右指標空著)。因此我們需要研究怎麽樣的二叉樹才能有最好的查詢效率、怎樣才能保持二叉樹的良好效能——於是就有了滿二叉樹、平衡樹、紅黑樹等概念/演算法。

但這樣空間占用就比單連結串列更多了。怎麽辦呢?

堆是一種最佳化到極致的二叉樹;它實際上就是一個陣列,左右節點對應的陣列下標可以直接計算出來——這就省掉了指向子節點的指標。

不過,指標沒了,靈活管理不定量數據、低消耗的插入刪除等好處也沒了。

靈活管理不定量數據這個需求容易滿足:我們把陣列分成若幹段、然後調整一下計算出來的下標就行了。

比如,陣列1一共256個元素,用滿後不得不又申請了一個1024元素的陣列2;那麽對於下標1000,我們就不能存取陣列1下標1000的元素,而應該去陣列2尋找;並且在陣列2中,它的下標應該是1000-256——你看,一旦內部做了這樣一個自動調整,我們就又把「按需申請空間」能力找回來了。

只不過,這個方案裏,插入/刪除會變得特別麻煩——堆排序本身已經夠燒腦了,結果算出來的下標還要根據配置截成很多段、還要在每段裏重新計算……

即便這個演算法我們可以輕易hold住,但每次插入/刪除造成海量元素位置移動,這個消耗也太可怕了——O(N)的消耗!

另一個折衷方案就是B樹(以及B+樹)——說白了,把節點做大一些,多儲存一些數據,換句話說就是「讓若幹數據共用一組指標」、或者說是一種「半堆半樹/堆樹結合」的數據結構:用更少的指標得到差不多的效能,這就把空間占用問題和高消耗的插入刪除問題給解決了,同時尋找效率仍然保持在O(log N)。

順帶的,這也避免了需要連續讀取數據時不停的順著指標跳轉的問題,因此是一種非常適合磁盤儲存的數據結構。

所以你說「用連結串列的目的是什麽」?

沒目的。

或者說,目的是讓你學會因地制宜的、靈活的組織數據——而且隨便你搞出多麽奇怪的數據結構、多麽復雜的數據組織形式,你都能清晰的給出它(對某個特定任務)的時間/空間復雜度。

當你能掌握到這個程度時,你完全可以把完全二叉樹、滿二叉樹、紅黑樹、B樹、B+樹的定義統統忘掉;但只要有需要,你隨時隨地都能把你面對的數據整進一個結合了二叉樹和佇列優點的、不知道該叫什麽的數據結構裏——從而以最高效率完成你面對的任務。

換句話說,不要浮在表面、只看到連結串列二叉樹之類東西;而是要深入進去,把它們統統拆散了、揉碎了、忘記了——你要做它們的發明者,不要做它們的使用者。

你要學的,是最高效率把玩海量數據的思路;你不僅要能因地制宜的給出解決方案、還要有能力給出綜合最優的方案(並作出證明)——停留在連結串列這個表面上,那是連門在哪都沒摸到,談何入門。

拿程式設計語言的術語來說,連結串列的定義並不是final class List<T>,而是abstract class List<T>——前者是「這是一個現成的完美品」「用就對了別管它怎麽實作也別試圖改進它」;後者是「這並不是一個完美的現成品」「你必須徹底搞明白它的設計思路」「你必須自己改進它」……

可以說,本科的演算法與數據結構這本書,其中一大半的篇幅都在教這個改進過程/思路——只不過絕大多數的師生乃至寫教材的老師都沒意識到這點,反而習慣性的分章節摘開、把明顯具有演進關系的概念割裂成一個個final class講解/學習,再加上考試指揮棒的作用,這才使得絕大多數人被誤導到了錯誤的方向。