連結串列中的跳錶小結

jackyrongvip發表於2020-12-05
連結串列之所 以訪問中間節點的效率低,就是因為每個節點只儲存了下一個節點的指標,要沿著這個指標
遍歷每個後續節點才能到達中間節點。那如果我們在節點上增加一個指標,指向更遠的節
點,比如說跳過後一個節點,直接指向後面第二個節點,那麼沿著這個指標遍歷,是不是遍
歷速度就翻倍了呢?
同理,如果我們能增加更多的指標,提供不同步長的遍歷能力,比如一次跳過 4 個節點,
甚至一半的節點,那我們是不是就可以更快速地訪問到中間節點了呢?
這當然是可以實現的。我們可以為連結串列的某些節點增加更多的指標。這些指標都指向不同距
離的後續節點。這樣一來,連結串列就具備了更高效的檢索能力。這樣的資料結構就是 跳錶
(Skip List)。
一個理想的跳錶,就是從連結串列頭開始,用多個不同的步長,每隔 2^n 個節點做一次直接鏈
接(n 取值為 0,1,2……)。跳錶中的每個節點都擁有多個不同步長的指標,我們可以在
每個節點裡,用一個陣列 next 來記錄這些指標。next 陣列的大小就是這個節點的層數,
next[0]就是第 0 層的步長為 1 的指標,next[1]就是第 1 層的步長為 2 的指標,next[2]就
是第 2 層的步長為 4 的指標,依此類推。你會發現,不同步長的指標,在連結串列中的分佈是
非常均勻的,這使得整個連結串列具有非常平衡的檢索結構。


 
舉個例子,當我們要檢索 k=a 6 時,從第一個節點 a 1 開始,用最大步長的指標開始遍歷,直
接就可以訪問到中間節點 a 5 。但是,如果沿著這個最大步長指標繼續訪問下去,下一個節
點是大於 k 的 a 9 ,這說明 k 在 a 5 和 a 9 之間。那麼,我們就在 a 5 和 a 9 之間,用小一個級別
的步長繼續查詢。這時候,a 5 的下一個元素是 a 7 ,a 7 依然大於 k 的值,因此,我們會繼續
在 a 5 和 a 7 之間,用再小一個級別的步長查詢,這樣就找到 a 6 了。這個過程其實就是二分查
找。時間代價是 O(log n)。
跳錶的檢索空間平衡方案
不知道你有沒有注意到,我在前面強調了一個詞,那就是“理想的跳錶”。為什麼要叫
它“理想”的跳錶呢?難道在實際情況下,跳錶不是這樣實現的嗎?的確不是。當我們要在
跳錶中插入元素時,節點之間的間隔距離就被改變了。如果要保證理想連結串列的每隔 2^n 個
節點做一次連結的特性,我們就需要重新修改許多節點的後續指標,這會帶來很大的開銷。
所以,在實際情況下,我們會在檢索效能和修改指標代價之間做一個權衡。為了保證檢索性
能,我們不需要保證跳錶是一個“理想”的平衡狀態,只需要保證它在大概率上是平衡的就
可以了。因此,當新節點插入時,我們不去修改已有的全部指標,而是僅針對新加入的節點
為它建立相應的各級別的跳錶指標。具體的操作過程,我們一起來看看。
首先,我們需要確認新加入的節點需要具有幾層的指標。我們通過隨機函式來生成層數,比
如說,我們可以寫一個函式 RandomLevel(),以 (1/2)^n 的概率決定是否生成第 n 層。這
樣,通過簡單的隨機生成指標層數的方式,我們就可以保證指標的分佈,在大概率上是平衡
的。
在確認了新節點的層數 n 以後,接下來,我們需要將新節點和前後的節點連線起來,也就
是為每一層的指標建立前後連線關係。其實每一層的指標連結,你都可以看作是一個獨立的
單連結串列的修改,因此我們只需要用單連結串列插入節點的方式完成指標連線即可。
這麼說,可能你理解起來不是很直觀,接下來,我通過一個具體的例子進一步給你解釋一
下。
我們要在一個最高有 3 層指標的跳錶中插入一個新元素 k,這個跳錶的結構如下圖所示。
 
假設我們通過跳錶的檢索已經確認了,k 應該插入到 a 6 和 a 7 兩個節點之間。那接下來,我
們要先為新節點隨機生成一個層數。假設生成的層數為 2,那我們就要修改第 0 層和第 1
層的指標關係。對於第 0 層的連結串列,k 需要插入到 a 6 和 a 7 之間,我們只需要修改 a 6 和 a 7
的第 0 層指標;對於第 1 層的連結串列,k 需要插入到 a 5 和 a 7 之間,我們只需要修改 a 5 和 a 7
的第 1 層指標。這樣,我們就完成了將 k 插入到跳錶中的動作。
通過這樣一種方式,我們可以大大減少修改指標的代價。當然,由於新加入節點的層數是隨
機生成的,因此在節點數目較少的情況下,如果指標分佈的不合理,檢索效能依然可能不
高。但是當節點數較多的時候,指標會趨向均勻分佈,查詢空間會比較平衡,檢索效能會趨
向於理想跳錶的檢索效率,接近 O(log n)

相關文章