資料結構與演算法整理總結---跳錶

lxzoliver發表於2020-04-13

如何理解“跳錶”?

對於⼀個單連結串列來講,即便連結串列中儲存的資料是有序的,如果我們要想在其中查詢某個資料,也只能從頭到尾遍歷連結串列。這樣查詢效率就會很低,時間複雜度會很⾼,是O(n)。

資料結構與演算法整理總結---跳錶

那怎麼來提⾼查詢效率呢?如果像圖中那樣,對連結串列建⽴⼀級“索引”,查詢起來是不是就會更快⼀些呢?每兩個結點提取⼀個結點到上⼀級,我們把抽出來的那⼀級叫作索引或索引層。

資料結構與演算法整理總結---跳錶

如果我們現在要查詢某個結點,⽐如16。我們可以先在索引層遍歷,當遍歷到索引層中值為13的結點時,我們發現下⼀個結點是17,那要查詢的結點16肯定就在這兩個結點之間。然後我們通過索引層結點的down指標,下降到原始連結串列這⼀層,繼續遍歷。這個時候,我們只需要再遍歷2個結點,就可以找到值等於16的這個結點了。這樣,原來如果要查詢16,需要遍歷10個結點,現在只需要遍歷7個結點。

跟前⾯建⽴第⼀級索引的⽅式相似,我們在第⼀級索引的基礎之上,每兩個結點就抽出⼀個結點到第⼆級索引。現在我們再來查詢16,只需要遍歷6個結點了,需要遍歷的結點數量⼜減少了。

資料結構與演算法整理總結---跳錶

我舉的例⼦資料量不⼤,所以即便加了兩級索引,查詢效率的提升也並不明顯。為了讓你能真切地感受索引提升查詢效率。我畫了⼀個包含64個結點的連結串列,按照前⾯講的這種思路,建⽴了五級索引。

資料結構與演算法整理總結---跳錶

這種連結串列加多級索引的結構,就是跳錶

跳錶的查詢效率

演算法的執⾏效率可以通過時間複雜度來度量,這⾥依舊可以⽤。我們知道,在⼀個單連結串列中查詢某個資料的時間複雜度是O(n)。那在⼀個具有多級索引的跳錶中,查詢某個資料的時間複雜度是多少呢?

按照我們剛才講的,每兩個結點會抽出⼀個結點作為上⼀級索引的結點,那第⼀級索引的結點個數⼤約就是n/2,第⼆級索引的結點個數⼤約就是n/4,第三級索引的結點個數⼤約就是n/8,依次類推,也就是說,第k級索引的結點個數是第k-1級索引的結點個數的1/2,那第k級索引結點的個數就是n/(2k)。

假設索引有h級,最⾼級的索引有2個結點。通過上⾯的公式,我們可以得到n/(2h)=2,從⽽求得h=log2n-1。如果包含原始連結串列這⼀層,整個跳錶的⾼度就是log2n。我們在跳錶中查詢某個資料的時候,如果每⼀層都要遍歷m個結點,那在跳錶中查詢⼀個資料的時間複雜度就是O(m*logn)。

假設我們要查詢的資料是x,在第k級索引中,我們遍歷到y結點之後,發現x⼤於y,⼩於後⾯的結點z,所以我們通過y的down指標,從第k級索引下降到第k-1級索引。在第k-1級索引中,y和z之間只有3個結點(包含y和z),所以,我們在K-1級索引中最多隻需要遍歷3個結點,依次類推,每⼀級索引都最多隻需要遍歷3個結點。

資料結構與演算法整理總結---跳錶

通過上⾯的分析,我們得到m=3,所以在跳錶中查詢任意資料的時間複雜度就是O(logn)。這個查詢的時間複雜度跟⼆分查詢是⼀樣的。換句話說,我們其實是基於單連結串列實現了⼆分查詢。

跳錶的空間複雜度

假設原始連結串列⼤⼩為n,那第⼀級索引⼤約有n/2個結點,第⼆級索引⼤約有n/4個結點,以此類推,每上升⼀級就減少⼀半,直到剩下2個結點。如果我們把每層索引的結點數寫出來,就是⼀個等⽐數列。

資料結構與演算法整理總結---跳錶

這⼏級索引的結點總和就是n/2+n/4+n/8…+8+4+2=n-2。所以,跳錶的空間複雜度是O(n)。

⾼效的動態插⼊和刪除

跳錶這個動態資料結構,不僅⽀持查詢操作,還⽀持動態的插⼊、刪除操作,⽽且插⼊、刪除操作的時間複雜度也是O(logn)。

在單連結串列中,⼀旦定位好要插⼊的位置,插⼊結點的時間複雜度是很低的,就是O(1)。但是,這⾥為了保證原始連結串列中資料的有序性,我們需要先找到要插⼊的位置,這個查詢操作就會⽐較耗時。
對於純粹的單連結串列,需要遍歷每個結點,來找到插⼊的位置。但是,對於跳錶來說,我們講過查詢某個結點的的時間複雜度是O(logn),所以這⾥查詢某個資料應該插⼊的位置,⽅法也是類似的,時間複雜度也是O(logn)。
資料結構與演算法整理總結---跳錶

Redis中的有序集合是通過跳錶來實現的,嚴格點講,其實還⽤到了雜湊表。

Redis中的有序集合⽀持的核⼼操作主要有下⾯這⼏個:

插⼊⼀個資料;刪除⼀個資料;查詢⼀個資料;按照區間查詢資料(⽐如查詢值在[100, 356]之間的資料);迭代輸出有序序列。

其中,插⼊、刪除、查詢以及迭代輸出有序序列這⼏個操作,紅⿊樹也可以完成,時間複雜度跟跳錶是⼀樣的。但是,按照區間來查詢資料這個操作,紅⿊樹的效率沒有跳錶⾼。 對於按照區間查詢資料這個操作,跳錶可以做到O(logn)的時間複雜度定位區間的起點,然後在原始連結串列中順序往後遍歷就可以了。

當然,Redis之所以⽤跳錶來實現有序集合,還有其他原因,⽐如,跳錶更容易程式碼實現。雖然跳錶的實現也不簡單,但⽐起紅⿊樹來說還是好懂、好寫多了,⽽簡單就意味著可讀性好,不容易出錯。還有,跳錶更加靈活,它可以通過改變索引構建策略,有效平衡執⾏效率和記憶體消耗。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章