九、跳表(Skip List)
一、概述
跳表是一種在各個(gè)方面都比較優(yōu)秀的動態(tài)數(shù)據(jù)結(jié)構(gòu),可支持快速的插入、刪除、查找操作,甚至可以替代紅黑樹(Red-black Tree)
應(yīng)用:Redis 中的有序集合(sorted set)是用跳表實(shí)現(xiàn)的。
基本思想
結(jié)合 鏈表 和 二分法 的特點(diǎn),將鏈表進(jìn)行加工,創(chuàng)造一個(gè)二者的結(jié)合體:
- 鏈表從頭節(jié)點(diǎn)到尾節(jié)點(diǎn)是有序的
- 可以進(jìn)行跳躍查找(形如二分法)
例如:我們建立兩層索引(第三層和第二層),若要查找某個(gè)結(jié)點(diǎn),比如d。可以先在第三層進(jìn)行遍歷,當(dāng)遍歷的時(shí)候,發(fā)現(xiàn)結(jié)點(diǎn)d在結(jié)點(diǎn)a和節(jié)點(diǎn)e之間,則通過第三層結(jié)點(diǎn)a的指針,下降到第二層,繼續(xù)遍歷==》結(jié)點(diǎn)c和節(jié)點(diǎn)e之間==》結(jié)點(diǎn)c的指針到第一層 ==》繼續(xù)遍歷,找到結(jié)點(diǎn)d
當(dāng)鏈表的長度 n 比較大時(shí),比如 1000、10000 的時(shí)候,在構(gòu)建索引之后,查找效率的提升就會非常明顯。
二、分析
1、時(shí)間復(fù)雜度分析
如果鏈表里有 n 個(gè)結(jié)點(diǎn),會有多少級索引呢?
每兩個(gè)結(jié)點(diǎn)會抽取一個(gè)作為上一級索引的結(jié)點(diǎn),所以第 k 級索引的結(jié)點(diǎn)個(gè)數(shù)是第 k-1 級索引的結(jié)點(diǎn)個(gè)數(shù)的 1/2,那第 k級索引結(jié)點(diǎn)的個(gè)數(shù)就是 n/(2k)。
==》
假設(shè)索引有 h 級,最高級的索引有 2 個(gè)結(jié)點(diǎn)。通過上面的公式,我們可以得到 n/(2h)=2,從而求得 h=log2n-1。如果包含原始鏈表這一層,整個(gè)跳表的高度就是log2n。
==》
在跳表中查詢某個(gè)數(shù)據(jù)的時(shí)候,如果每一層都要遍歷 m 個(gè)結(jié)點(diǎn),那在跳表中查詢一個(gè)數(shù)據(jù)的時(shí)間復(fù)雜度就是 O(m*logn)。其中 m=3(二分思想)
==》時(shí)間復(fù)雜度:O(logn)
2、空間復(fù)雜度分析
原始鏈表大小為n,每2個(gè)結(jié)點(diǎn)抽1個(gè),每層索引的結(jié)點(diǎn)數(shù)為:
n/2, n/4, n/8, ... , 8 , 4 , 2結(jié)點(diǎn)總和為 n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n - 2
==》空間復(fù)雜度為O(n)
3、變形
每兩個(gè)結(jié)點(diǎn)抽一個(gè)結(jié)點(diǎn)到上級索引 ==》每三個(gè)結(jié)點(diǎn)或五個(gè)結(jié)點(diǎn),抽一個(gè)結(jié)點(diǎn)到上級索引
則,第一級索引需要大約 n/3 個(gè)結(jié)點(diǎn),第二級索引需要大約 n/9 個(gè)結(jié)點(diǎn)。每往上一級,索引結(jié)點(diǎn)個(gè)數(shù)都除以 3。通過等比數(shù)列求和公式,總的索引結(jié)點(diǎn)大約就是 n/3+n/9+…
+9+3+1=n/2。
==》雖然空間復(fù)雜度仍為O(n),但是減少了一半索引結(jié)點(diǎn)存儲空間
但是實(shí)際應(yīng)用中,不必太在意索引占用的額外空間。
三、操作
1、動態(tài)插入和刪除
- 插入、刪除操作的時(shí)間復(fù)雜度為 O(logn)
為了保證原始鏈表中數(shù)據(jù)的有序性,需要先找到要插入的位置(時(shí)間復(fù)雜度為O(logn)),然后執(zhí)行插入操作。
刪除操作:若該結(jié)點(diǎn)在索引中也有出現(xiàn),除了要刪除原始鏈表中的結(jié)點(diǎn)外,還要刪除索引中的。需要獲得要刪除結(jié)點(diǎn)的前驅(qū)結(jié)點(diǎn)。
2、索引動態(tài)更新
當(dāng)不停地插入向跳表中插入數(shù)據(jù)時(shí),如果不更新索引,可能導(dǎo)致某兩個(gè)索引結(jié)點(diǎn)之間的數(shù)據(jù)非常多的情況 ==》退化為單鏈表
解決方法:通過隨機(jī)函數(shù)來維護(hù)索引與原始鏈表大小之間的 “平衡性”。也就是說,如果鏈表中結(jié)點(diǎn)多了,索引結(jié)點(diǎn)就相應(yīng)地增加一些,避免復(fù)雜度退化,以及查找、插入、刪除操作性能下降。
通過隨機(jī)函數(shù)來決定該結(jié)點(diǎn)插入到哪一層索引中,比如隨機(jī)函數(shù)生成了 K,則將該結(jié)點(diǎn)添加到第一級到第K級這K級索引中。
四、應(yīng)用
Redis 使用跳表實(shí)現(xiàn)有序集合(其實(shí)也包括散列表,此處先忽略掉),而非紅黑樹?
Redis中有序結(jié)合支持的核心操作主要是:
- 插入一個(gè)數(shù)據(jù);
- 刪除一個(gè)數(shù)據(jù);
- 查找一個(gè)數(shù)據(jù);
- 按照區(qū)間查找數(shù)據(jù)(eg: 查找值在[155, 296]區(qū)間內(nèi))==》時(shí)間復(fù)雜度:O(logn)
- 迭代輸出有序序列
跳表更加靈活,它可以通過改變索引構(gòu)建策略,有效平衡執(zhí)行效率和內(nèi)存消耗。
總結(jié)
以上是生活随笔為你收集整理的九、跳表(Skip List)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 八、二分查找(Binary Search
- 下一篇: 十、散列表(Hash Table)