Golang數據結構與算法系列:Skiplist跳躍表實現

跳躍表是一種有效的數據結構,它可以在n個元素的有序序列中實現O(log n)搜索複雜度和O(log n)插入複雜度。所以,它既是最佳的數組(便於搜索),同時也維護了一個相似鏈表的結構。經過維護子序列的連接層次結構,使得快速搜索成爲可能,每一個後續子序列跳過的元素比前一個子序列少(見下圖)。node

搜索一般從最稀疏的子序列開始(通常是自頂向下),直到找到兩個連續的元素,一個小於或等於搜索目標的元素,另外一個大於或等於搜索目標的元素。經過連接的層次結構,連接到這兩個元素間的下一層子序列,而後在該子序列中繼續搜索,直到最終完成整個序列的檢索。
Golang數據結構與算法系列:Skiplist跳躍表實現
圖1-跳躍表數據結構的示意圖redis

每一個帶箭頭的框表示一個指針,每一行是一個鏈表,包含一個稀疏子序列;底部的編號框(黃色)表示有序的數據序列。查詢集合中的元素時,通常從頂部最稀疏的子序列向下執行,直到找到可能包含目標元素的連續元素區間。算法

跳躍列表是分層構建的,底層是一個普通的有序鏈表。每一個高層則充當「快車道」的角色。對目標元素的搜索從頂部列表中的head元素開始,水平地進行,直到當前元素大於或等於目標元素。若是當前元素等於目標,則說明已經找到它。數組

若是當前元素大於目標元素,或者搜索到達鏈表的末尾,此時則返回前一個元素並轉向下一個較低層級的列表後重復該過程。每一個鏈表中預期的步驟數最多爲1/p,經過選擇不一樣的p值,能夠用搜索成原本交換存儲成本。數據結構

跳躍列表不能提供與傳統的平衡樹數據結構相同的針對最壞狀況的性能保證,但在實際應用中工做得很好(好比Redis和LevelDB底層存儲都是基於跳躍表),並且層數隨機算法中的隨機平衡方案通常比平衡二叉搜索樹中使用的肯定性平衡方案更容易實現。跳躍列表在並行計算中也頗有用,在某些場景下,能夠並行地在跳躍列表的不一樣部分執行插入,而無需對數據結構進行任何全局的從新平衡。
Golang數據結構與算法系列:Skiplist跳躍表實現
圖2 插入要跳轉列表的元素併發

基於Golang的實現細節

首先定義鏈表及節點的基本數據結構以下:
type SkipListNode struct { //跳躍表節點定義
key int
value interface{}
next []*SkipListNode
}dom

type SkipList struct { // 跳躍表結構定義
head,tail SkipListNode
length,level int
mut
sync.RWMutex
rand *rand.Rand
}ide

結構體中元素說明:性能

  • key/value爲跳躍表中每一個節點的鍵值對。
  • next數組爲指向多層鏈表節點的數組。
  • head/tail指向跳躍表的起始節點指針地址。
  • length/level分別爲跳躍表長度和層數。
  • mutex用於控制集合的併發訪問。
  • rand爲內部保留的隨機數。

隨機算法:如何能保證O(logN)的複雜度?假設K層節點的數量是K+1層節點的P倍,那麼這個跳躍表能夠當作是一棵平衡P叉樹,從最頂層開始查找某個節點須要的時間是O(logpN)(其中P是常量)。測試

const P uint32 = 4
func (list *SkipList) random() int {
//當新增節點時隨機生成層數,定義一個平衡P叉樹
//有多種實現算法,redis及leveldb中通常採用P=4的平衡四叉樹
level:= 1
for level < list.length && ((list.rand.Uint32() % P) == 0) {
level++
}
if level < list.level {
return level
} else {
return list.level
}
}

插入節點算法簡單描述以下:
func (list SkipList) AddNode(key int, value interface{}) {
list.mut.Lock()
defer list.mut.Unlock()
//生成隨機層數
level:= list.random()
// 定位新元素的插入點
update:= make([]
SkipListNode, level)
node:= list.head
//從高到低逐層進行定位(此處默認0爲最底層)
for index := level - 1; index >= 0; index-- {
for {
nextNode:= node.next[index]
// n最開始指向最高層的head的next節點,從head開始逐個比較
if nextNode== list.tail || nextNode.key > key {
update[index]= node
break
} else if nextNode.key == key {
//此處簡化算法,若是key值相同則覆蓋,即僅保留惟一的key值節點
nextNode.value= value
return
} else {
//如未達到隊尾且新增元素key值大於當前位置節點的key值,則繼續遍歷列表
node= nextNode
}
}
}
//生成並初始化新節點
newNode:= &SkipListNode{key, value, make([]*SkipListNode, level)}
for index, node := range update {
node.next[index], newNode.next[index] = newNode, node.next[index]
}
list.length++
}
// 其餘主要方法算法邏輯相似,再也不贅述

對於以上代碼測試以下:func main() {//跳錶使用演示//工廠方法用於生成空列表,層數設爲3list:= NewSkipList(3) list.AddNode(3,"jack")list.AddNode(33,"hulu")list.AddNode(97,"sig")list.AddNode(9,"james")fmt.Println(list)}// OUTPUT:level-2: taillevel-1: <9, james> --> taillevel-0: <3, jack> --> <9, james> --><33, hulu> --> <97, sig> --> tail

相關文章
相關標籤/搜索