跳跃表(skip-list)

前言

最近看了一种数据结构叫做skipList,redis和levelDB都是用了它。Skip List是在有序链表的基础上进行了扩展,解决了有序链表结构查找特定值困难的问题,查找特定值的时间复杂度为O(logn),他是一种可以代替平衡树的数据结构。

为什么使用跳跃表

目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树出来吗? 很难吧,这需要时间,要考虑很多细节,要参考一堆算法与数据结构之类的树,还要参考网上的代码,相当麻烦。

用跳表吧,跳表是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,它的效率和红黑树以及 AVL 树不相上下,但跳表的原理相当简单,只要你能熟练操作链表,就能轻松实现一个 SkipList。

有序表的搜索

考虑一个有序表:

从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 < 2, 4, 6 >,总共比较的次数为 2 + 4 + 6 = 12 次。有没有优化的算法吗? 链表是有序的,但不能使用二分查找。类似二叉搜索树,我们把一些节点提取出来,作为索引。得到如下结构:

这里我们把 < 14, 34, 50, 72 > 提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。 我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:

这里元素不多,体现不出优势,如果元素足够多,这种索引结构就能体现出优势来了。

跳跃表

下面的结构是就是跳表:
其中 -1 表示 INT_MIN, 链表的最小值,1 表示 INT_MAX,链表的最大值。

跳表具有如下性质:
(1) 由很多层结构组成
(2) 每一层都是一个有序的链表
(3) 最底层(Level 1)的链表包含所有元素
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素

跳跃表搜索

例子:查找元素 117
(1) 比较 21, 比 21 大,往后面找
(2) 比较 37, 比 37大,比链表最大值小,从 37 的下面一层开始找
(3) 比较 71, 比 71 大,比链表最大值小,从 71 的下面一层开始找
(4) 比较 85, 比 85 大,从后面找
(5) 比较 117, 等于 117, 找到了节点。

跳跃表插入

先确定该元素要占据的层数 K(采用丢硬币的方式,这完全是随机的)
然后在 Level 1 … Level K 各个层的链表都插入元素。
例子:插入 119, K = 2

如果 K 大于链表的层数,则要添加新的层。
例子:插入 119, K = 4

丢硬币决定K值

插入元素的时候,元素所占有的层数完全是随机的,通过一下随机算法产生:
相当与做一次丢硬币的实验,如果遇到正面,继续丢,遇到反面,则停止,
用实验中丢硬币的次数 K 作为元素占有的层数。显然随机变量 K 满足参数为 p = 1/2 的几何分布,
K 的期望值 E[K] = 1/p = 2. 就是说,各个元素的层数,期望值是 2 层。

跳跃表删除

在各个层中找到包含 x 的节点,使用标准的 delete from list 方法删除该节点。
例子:删除 71

代码实现(Scala)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package lishijia.algorithm.skiplist
import scala.util.Random
/**
* 跳跃链表:是一种有序的链表扩展
* 增加节点
* 新节点和各层索引节点逐一比较,确定原链表的插入位置
* 把索引插入到原链表
* 利用抛硬币的随机方式,决定新节点是否提升为上一级索引。结果为“正”则提升并继续抛硬币,结果为“负”则停止
* 总体上,跳跃表插入操作的时间复杂度是O(logN),而这种数据结构所占空间是2N,既空间复杂度是 O(N)。
*
* 删除节点
* 自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。O(logN)
* 删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。O(logN)
* 总体上,跳跃表删除操作的时间复杂度是O(logN)。
*
* 目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。
*/
object SkipList {
def main(args: Array[String]): Unit = {
var r = new Random() // Coin toss
println(r.nextDouble())
println(r.nextDouble())
println(r.nextDouble())
println(r.nextDouble())
println(r.nextDouble())

val skipList = new SkipList(1)
skipList.put("1", 1)
skipList.put("2", 2)
skipList.put("3", 3)
println(skipList.get("2"))
}
}

class SkipList{
// special// special
val negInf = "-infinity"
val posInf = "+infinity"
var head:SkipListEntry = null // First element of the top level
var tail:SkipListEntry = null // Last element of the top level
var n = 0 // number of entries in the Skip List
var h = 0 // Height
var r = new Random() // Coin toss

// constructor
def this(capcity:Int){
this()
var p1: SkipListEntry = null
var p2: SkipListEntry = null

// 创建一个 -oo 和一个 +oo 对象
p1 = new SkipListEntry(negInf, null)
p2 = new SkipListEntry(posInf, null)

// 将 -oo 和 +oo 相互连接
p1.right = p2
p2.left = p1

// 给 head 和 tail 初始化
head = p1
tail = p2

n = 0
h = 0
}

private def findEntry(key: String) : SkipListEntry ={
var p:SkipListEntry = null
// 从head头节点开始查找
p = head
while (true){
// 从左向右查找,直到右节点的key值大于要查找的key值
while ((p.right.key ne posInf) && p.right.key.compareTo(key) <= 0) {
p = p.right
}
// 如果有更低层的节点,则向低层移动
if (p.down != null) {
p = p.down
}else{
return p
}
//todo: break is not supported
}
// 返回p,!注意这里p的key值是小于等于传入key的值的(p.key <= key)
return p
}

def put(key: String, value: Integer): Integer = {
var p:SkipListEntry = null
var q:SkipListEntry = null
var i = 0
// 查找适合插入的位子
p = findEntry(key)
// 如果跳跃表中存在含有key值的节点,则进行value的修改操作即可完成
if (p.key.equals(key)) {
val oldValue = p.value
p.value = value
return oldValue
}
// 如果跳跃表中不存在含有key值的节点,则进行新增操作
q = new SkipListEntry(key, value)
q.left = p
q.right = p.right
p.right.left = q
p.right = q
// 再使用随机数决定是否要向更高level攀升
while (r.nextDouble < 0.5) {
// 如果新元素的级别已经达到跳跃表的最大高度,则新建空白层
if (i >= h) {
addEmptyLevel()
}
// 从p向左扫描含有高层节点的节点
while (p.up == null) {
p = p.left
}
p = p.up
// 新增和q指针指向的节点含有相同key值的节点对象
// 这里需要注意的是除底层节点之外的节点对象是不需要value值的
val z = new SkipListEntry(key, null)
z.left = p
z.right = p.right
p.right.left = z
p.right = z
z.down = q
q.up = z
q = z
i = i + 1
}
n = n + 1
// 返回null,没有旧节点的value值
null
}

private def addEmptyLevel(): Unit = {
val p1 = new SkipListEntry(negInf, null)
val p2 = new SkipListEntry(posInf, null)
p1.right = p2
p1.down = head
p2.left = p1
p2.down = tail
head.up = p1
tail.up = p2
head = p1
tail = p2
h = h + 1
}

def get(key: String): Integer = {
var p = findEntry(key)
if (p.key.equals(key)) p.value
else null
}
}


package lishijia.algorithm.skiplist
/**
* skipList数据存储结构
*/
class SkipListEntry{
// data
var key:String = null
var value:Integer = null
// links
var up:SkipListEntry = null
var down:SkipListEntry = null
var left:SkipListEntry = null
var right:SkipListEntry = null
def this(key: String, value: Integer){
this()
this.key=key
this.value=value
}
}

代码:https://github.com/lishijia/algorithm/tree/master/src/main/scala/lishijia/algorithm/skiplist

分享到 评论