跳表的原理與實現 Golang 版

2021-10-10 17:07:02 字數 4138 閱讀 9819

跳表的原理與實現 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=log⁡2n−1h = \log_2 n -1,如果再加上原始鍊錶一層,那麼就是 log⁡2n\log_2 n 。在查詢過程中,如果每一層都需要查詢 mm 個節點,那麼時間複雜度就是 o(m∗log⁡2n)o(m * \log_2 n) ,因為每一層 mm 的值有乙個最大的限制。時間複雜度為 o(log⁡2n)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 操作強制使樹結構保持平衡來保證節點搜尋的效率。在資料為隨機插入的情況下,平衡樹效能表現良好 但資料為順序插入或者需要刪除節點的情況下,平衡樹的效能就會有...