資料結構與演算法-連結串列(下)

daqianmen發表於2021-09-09

承接上文,解決普通連結串列查詢的問題。首先分析問題的瓶頸,對於查詢,自然是從頭開始順序查詢到尾部,那麼怎麼才能更快查詢到目標元素呢?將連結串列中的元素排序可以加速查詢過程,但仍需要順序查詢。因此,連結串列最好允許跳過某些節點,以避免順序處理。基於以上思路,提出跳躍連結串列的概念。跳躍連結串列是有序連結串列的一個有趣變種,可以進行非順序查詢,演算法複雜度是O(lgn),相比於O(n)下降了一個量級。

首先使用自然語言概述跳躍連結串列。假設我們已有N個元素組成的有序單向連結串列,從中取出n1個元素組成新的單向連結串列,我們稱之為二層連結串列,然後從二層連結串列中繼續取出n2個元素組成三層連結串列,最後以此迴圈,可以有m層連結串列,m隨意定義。這就是跳躍連結串列。就像蓋高樓一樣,普通連結串列只有一層,查詢某個元素只能從頭到尾,但是如果我們組織多層連結串列,就可以從高層到低層逐層查詢,在這個過程中可以跳過很多元素,達到快速查詢的目的。

下面是數學定義:

位於2^(k-1)*i的節點指向位於2^(k-1)*(i+1)的節點,約束條件:i>=1,1<=k<=lgn,lgn向下取整複製程式碼

跳躍連結串列:

資料結構與演算法-連結串列(下)

這意味著第2個節點指向前面距離2個單位的節點,第4個節點指向前面距離4個單位的節點,以此類推。為此,要在連結串列中包含不同數目的指標:半數節點只有一個指標,1/4節點有兩個指標,1/8節點有3個指標,以此類推。

概述下查詢的邏輯:

假設查詢元素el,應該從最高層上的指標開始,找到該元素就成功的結束查詢。如果到達該連結串列的末尾,或者遇到大於元素el的某個元素key,就從包含key的那個節點的前一個節點重新開始查詢,但是這次查詢是從比前面低一層的指標開始的。直到找到el,或者沿著第一層的指標到達連結串列的末尾,或者找到一個大於el的元素,查詢才會停止。

舉個例子,比如在上面的跳躍連結串列中查詢5,那麼首先嚐試的是第四層,這一層第一個節點是8,查詢不成功,接著回退8節點的上一個節點。找到root指標,查詢root第3層,首先找到4,然後找到8,查詢不成功,回退8節點的上一個節點。找到節點4,查詢節點4第2層,首先找到6,查詢不成功,回退6節點的上一個節點。找到4節點,查詢節點4第1層,首先找到5,查詢成功。

動態圖如下:

資料結構與演算法-連結串列(下)

跳躍連結串列的查詢複雜度只有O(lgn),這是很不錯的效率了,但是連結串列的設計使得插入和刪除效率很低。為了插入一個新的元素,必須重新構建新節點之後的所有節點,這當然是不能容忍的。為了保持跳躍連結串列在查詢方面的優點,同時避免在插入和刪除節點時重新構造連結串列,我們可以放棄對不同層上節點的位置要求,僅保留不同層上節點的數目要求即可。通過計算可以知道不同層級之間節點數目比例如下:

假設層級為n,那麼從n層到1層的節點數目比例為:2^0 : 2^1 : ... : 2^(n-1)

比如上面跳躍連結串列是4層,那麼比例就是1:2:4:8。這裡有一點需要注意,因為我們放棄了位置要求,那麼連結串列層級是可以隨意指定的,一般是4層,即使節點數目有10000個,只要層級之間節點數目比例保持不變,就不會影響查詢效率。

使用上述方法,新增和刪除的演算法複雜度也保持在O(lgn),這是因為新增和刪除也依賴查詢,跳躍連結串列是順序表,新增或刪除時首先依賴查詢確定節點位置,才能執行後續操作。

新增的操作就不多說了,邏輯上和普通連結串列差不多,無非是普通連結串列只是插入一層,而跳躍連結串列插入首先確定新插入節點的層級m(可以使用隨機數確定,保持層級節點比例即可),然後將節點插入到每一層連結串列即可。

總結:

與更高階的資料結構相比,例如自適應樹或者AVL樹,跳躍連結串列的效率相當不錯,因此,用跳躍連結串列來代替這些資料結構是可行的的。

  • 自組織連結串列

連結串列的資料結構探討已經差不多了,事實上,我們上述探討的是連結串列的靜態結構,研究的是基於已經組織好的連結串列的演算法。我們還可以使用某種方法動態地組織連結串列,從而提高查詢效率。通過動態方式組織的連結串列,可以稱之為自組織連結串列。

這裡給出4種動態組織連結串列的方式:

  1. 前移法。在找到需要的元素之後,把它放到連結串列的開頭。
  2. 換位法。在找到需要的元素之後,只要它不在連結串列的開頭,就與其前驅交換位置。
  3. 計數法。根據元素被訪問的次數,對連結串列進行排序。
  4. 排序法。根據節點元素的屬性,對連結串列進行排序。

我們通過一組表格來說明這4中方法:

假設現在有一個空連結串列以及一段資料流-ACBCDADACACCEE,我們通過上面4中方式,將資料流動態的組織到連結串列中。

資料結構與演算法-連結串列(下)

為了測試這些方法的效率,可以將實際比較次數與可能的最大比較次數相對比。可能的最大比較次數是將連結串列在處理每個元素時的長度相加得到的。比如在上面表格中,資料體包含14個字母,在處理每個字母前,將連結串列的長度記錄下來,其結果是0+1+2+3+3+4+4+4+4+4+4+4+4+5=46,這個總長度用來與所做的比較次數相對比。對於所有的方法來說,這個總長度是相同的,而且是最大的,那麼就可以用來做對比了。

我們通過將不同文字中的單片語織成連結串列來測試上述方法的查詢效率:

資料結構與演算法-連結串列(下)

我們將普通法、前移法、換位法通過兩種不同的插入方式進行比較。通過以上資料總結出以下特徵:

  • 隨著資料量的提升,所有方法的效率都有所提高
  • 在尾部插入資料總是比在前端插入資料高效
  • 跳躍連結串列>計數法>前移法>換位法

可以看到跳躍連結串列的查詢效率相比於其他方法有著巨大的優勢,緊隨其後的是計數法,然後是前移法以及換位法都比字母順序以及普通法表現優異。

並不是說查詢效率越高的就越好,跳躍連結串列的組織方式相比於其他方法要複雜很多。事實上,計數法在軟體開發中使用的很多,我們平常見到的‘熱資料’本質上屬於計數法。這些方法組織連結串列的方式簡單、高效,往往會得到大量應用。

這裡有一點需要注意,在上述表格中只包含了資料的比較,沒有包含執行這些方法所需要的其他操作,如果包含這些資訊,這些方法之間的差異可能就不會那麼顯著了。

這裡簡單總結一下,對於中等數量的資料,使用連結串列就足夠了。隨著資料量的增長和訪問頻率的提高需要使用更復雜的方法和資料結構。

到此為止,連結串列的探討就結束了,更多的收穫就需要在實踐中摸索了。

資料結構與演算法-棧與佇列


相關文章