跳表的原理與實現 golang 版
有時候,我們會說,在計算機世界裡,其實只有兩種資料結構,乙個是陣列乙個是鍊錶。原因是其他的資料結構都是基於這兩種資料結構做的擴充套件。
陣列和鍊錶的優缺點實在是非常的明顯。陣列可以高效查詢,按照下標索引,但是很難進行高效的刪除和擴容。鍊錶的優缺點正好相反。很多時候,我們不用鍊錶的原因就是因為它沒有辦法快速查詢和插入。但是,如果我們可以對它進行改造,讓它可以使用類似二分查詢的方法就很棒了。這個改造的結果就是 skiplist。
跳表的原理
skiplist 的原理是給鍊錶中的某一些元素新增索引,然後建立多級索引達到效果。
每隔幾個節點,從鍊錶中提取出乙個節點作為一級索引。然後再從一級索引中,每隔幾個節點,再提取乙個節點作為二級索引。如下圖所示:
當我們查詢時,在最高端索引中查詢,依次向下。假如我們要查詢數字 18。我們從最高端索引中查詢,找到 5,然後找到 27 時,發現它大於 18,我們到下一級查詢。在下一級中找到 15,然後 27,發現 27 比 18 大,然後到最低階中查詢,15 的後乙個就是 18。
可以看到,即便是在元素很少的時候,這個查詢路徑也要比從頭開始查詢要少乙個元素。如果想象整個鍊錶的資料是 1000 個,或者 10000 個,那麼查詢效率會大大提公升。
跳表的空間時間占用
但是,skiplist 有乙個明顯的缺點,就是空間占用變多,假設每兩個元素提出乙個索引時,空間占用就是:n2,n4,n8,…,8,4,2\frac n 2, \frac n 4, \frac n 8, …, 8, 4, 2 ,求和之後得到 n−2n-2。實際上就是 o(n)o(n) 的時間複雜度。但是實際上,索引儲存的只是實際資料的指標,即便是多級索引,也是多了幾個索引而已。並不是將原始資料 copy 一遍。所以,實際使用的過程中,這個空間並不會達到 o(n)o(n) 這麼誇張。
但是,時間複雜度的降低可不是一點點。假設我們在任何一級中,都保持每兩個元素提出乙個索引。那麼一級索引就是 n2\frac n 2 ,二級索引就是 n4\frac n 4 ,以此類推,可以得到第 kk 級索引的節點個數就是 n2k\frac n 。加入索引有 hh 級,有兩個索引節點,那麼可得 n2h=2\frac n = 2 ,那麼可以得到 h=log2n−1h = \log_2 n -1,如果再加上原始鍊錶一層,那麼就是 log2n\log_2 n 。在查詢過程中,如果每一層都需要查詢 mm 個節點,那麼時間複雜度就是 o(m∗log2n)o(m * \log_2 n) ,因為每一層 mm 的值有乙個最大的限制。時間複雜度為 o(log2n)o(\log_2n)。
跳表的索引生成
當我們向乙個鍊錶中新增乙個節點後,索引之間的節點數就會增多,如果增加太多的話,就會導致跳表的查詢效率急劇退化。所以,當我們向跳表中新增乙個元素之後,我們就要決定是否要對它生成索引,生成到幾級。所以,我們也像紅黑樹一樣,需要有一種手段來維護整個跳表。
如果節點增多了,那麼相應的索引就增多,避免效能退化。紅黑樹通過左右旋轉來達到這個要求。跳表一般使用乙個隨機函式來決定將這個結點插入到哪幾級索引中,比如隨機函式生成了值 x,那我們就將這個結點新增到第一級到第 x 級的索引中。
跳表的實現
那麼下面我們來實現乙個 skiplist。下面的跳表實現了乙個 sortedset,乙個有序的不能重複的跳表結構。
跳表定義
const max_level = 16
const level_factor = 0.5
const (
ok = iota + 1
duplicated
not_exist
not_init
)type inte***ce inte***ce
type fakenode struct
func (f *fakenode) less(p inte***ce) bool
func (f *fakenode) equal(p inte***ce) bool
複製**
上面定義了一些後面會用到的 struct 和 inte***ce 。最主要的就是 inte***ce ,他用來描述乙個實際儲存的物件。
fakenode 是用來放在鍊錶的頭部,方便後面的節點操作。這是資料結構中一種常用的哨兵方法。
type node struct
type skiplist struct
func newnode
(p inte***ce, l int)
*node
}func newskiplist()
*skiplist
,max_level),
0,1}
}
下面是具體的方法實現,主要實現了三個 add 、 delete 和 search。
跳表新增元素
func
(sl *skiplist)
randomlevel
() int
if sl.level+
1< l
return l
}func
(sl *skiplist)
add(p inte***ce) int
cur :
= sl.head
update :=[
max_level
]*node
i :=max_level-1
for; i >=
0; i--
if!cur.forwards[i]
.data.
less
(p)
cur = cur.forwards[i]
}if nil == cur.forwards[i]
}
sl.length++
l := sl.
randomlevel()
n :=newnode
(p, l)
for i :=0
; i < n.level; i++
if n.level > sl.level
return
ok}
跳表刪除元素
func
(sl *skiplist)
delete
(p inte***ce) int
for i :
= sl.level -
1; i >=
0; i--
cur = cur.forwards[i]}}
cur = update[0]
.forwards[0]
if cur == nil
for i :
= cur.level -
1; i >=
0; i--
if nil != update[i]
.forwards[i]
} sl.length--
return
ok}
跳表查詢元素
func
(sl *skiplist)
search
(p inte***ce)
*node
i :=max_level-1
for; i >=
0; i--
if!cur.forwards[i]
.data.
less
(p) cur = cur.forwards[i]
}if nil == cur.forwards[i]
}return nil
}
其他方法
func
(sl *skiplist)
print()
fmt.
println(""
) cur = sl.head
}}func
(sl *skiplist)
length
() uint32
func
(sl *skiplist)
level
() int
尾聲
跳表的使用其實比較廣泛,在某些場景下,可以替換紅黑樹,而且比紅黑樹實現要簡單得多。在 redis 的 sortedset 中,就用了跳表來實現這一資料結構。
熔斷原理與實現Golang版
熔斷 熔斷機制其實是參考了我們日常生活中的保險絲的保護機制,當電路超負荷執行時,保險絲會自動的斷開,從而保證電路中的電器不受損害。而服務治理中的熔斷機制,指的是在發起服務呼叫的時候,如果被呼叫方返回的錯誤率超過一定的閾值,那麼後續的請求將不會真正發起請求,而是在呼叫方直接返回錯誤 在這種模式下,服務...
跳表的原理及其實現
作用 目的 跳表作為一種資料結構通常用於取代平衡樹。起因平衡樹可以用於表示抽象的資料型別如字典和有序鍊錶,它通過樹旋轉 tree rotation 操作強制使樹結構保持平衡來保證節點搜尋的效率。在資料為隨機插入的情況下,平衡樹效能表現良好 但資料為順序插入或者需要刪除節點的情況下,平衡樹的效能就會有...
跳表的介紹與實現
作用 目的 跳表作為一種資料結構通常用於取代平衡樹。起因平衡樹可以用於表示抽象的資料型別如字典和有序鍊錶,它通過樹旋轉 tree rotation 操作強制使樹結構保持平衡來保證節點搜尋的效率。在資料為隨機插入的情況下,平衡樹效能表現良好 但資料為順序插入或者需要刪除節點的情況下,平衡樹的效能就會有...