本章我们主要介绍以下跳跃表的实现。
1. Skip List介绍
skip list在wiki中介绍如下:在计算机科学中,skip list是一种允许对有序序列进行快速查找的数据结构。通过维持一个链式层级结构,每一个后继序列都会跳过比上层更少的元素,这样就使得快速查找成为可能,请参看下图。查找一般会从最顶层的稀疏序列开始,直到找到两个元素,其中一个小于等于所查找元素,另一个大于等于所查找元素。
skip list是一种随机化的数据结构,其效率可比拟于二叉查找树(对于大多数操作需要O(logn)平均时间)。跳跃表是在很多应用中有可能替代平衡树而作为实现方法的一种数据结构。跳跃表的算法有同平衡树一样的渐进的预期时间边界,并且更简单、更快速和使用更少的空间。目前开源的Redis和LevelDB都有用到它,它的效率和红黑树以及AVL树不相上下,但是跳跃表的原理相当简单,只要能熟练操作链表,就能轻松实现一个Skip List。跳跃表是有Pugh在1990年提出的。
2. skip list定义及构造步骤
2.1 跳跃表的性质
首先应该了解跳跃表的性质:
-
一个跳跃表应该由多个层(level)组成;
-
每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil
节点;
-
最底层(level 1)的链表包含了所有元素;
-
如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现;
-
每个节点包含两个指针,一个指向同一层链表中的下一个元素,另一个指向下面一层具有相同值的元素;
参照上面所示的跳跃表,可以看到总共有4层,最上面一层是最高层(Level4),最下面一层是最底层(level1),然后每一列中的链表节点的值都相同,用指针来链接着。跳跃表的层数跟结构中最高节点的高度相同。理想情况下,跳跃表结构中第一层存在所有节点,第二层只有一半的节点,而且是均匀间隔的,第三层则存在1/4
的节点,并且是均匀间隔的,以此类推,这样理想的层数就是logN
。
2.2 跳跃表数据结构
下面先给出跳跃表数据结构的一个示意图:
下面我们分别讲述一下上面两个数据结构各字段含义:
1) struct skiplist数据结构
skiplist
是表示跳跃表的数据结构,其各属性字段含义如下:
在skiplist
结构中,obj按score有序。
2) struct skiplist_node数据结构
skiplist_node
代表跳跃表的一个节点,其各属性的含义如下:
-
levels: 用于保存节点的层数。比如上边score为1.0的节点,层数为4; score为2.0的节点,层数为2; score为3.0的节点,层数为5.
-
level: 代表着各层,节点中L1、L2、L3等字样标记节点的各个层。L1代表第一层, L2代表第二层,以此类推。每个层有一个前进指针和跨度(注:上面数据结构中我们注释了span,暂未使用)。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。注意这里level
定义的是一个零长度数组,因此需要放在结构体的最后。实际上,我们可以定义为level[1]
,因为对于一个节点至少有一个前进指针,这样我们就可以把该字段存放在任何位置了。
-
backward指针: 节点中用BW
字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
-
score: 各个节点中的1.0
、2.0
和3.0
是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
-
obj: 各个节点中的obj1
、obj2
和obj3
是节点所保存的成员对象。
注意表头节点和其他节点的构造是一样的:表头节点也有后退指针、分值和成员对象,不过表头节点的这些属性都不会被用到,所以上图省略了这些部分,只显示了表头节点的各个层。
2.3 跳跃表的初始化
如下图所示是一个初始化的空跳跃表:
下面给出相应的代码实现:
2.4 跳跃表的搜索
在跳跃表中查找一个元素x
,按照如下几个步骤进行:
参看下图所示:
下面我们给出跳跃表搜索的代码(注: 当score相等时,如下搜索代码其实存在一些问题):
2.5 跳跃表的插入
在插入时首先确定要占据的层数K,这里通常会用到如下两种方法:
- 抛硬币方式:即采用类似于抛硬币的方式,只要是正面就累加,直到遇到反面才停止,最后记录正面的次数并将其作为要添加新元素的层。
- 统计概率方式: 先给定一个概率
p
,产生一个0到1之间的随机数,如果这个随机数小于p
,则将高度加1,直到产生的随机数大于概率p才停止,根据给出的结论,当概率为1/2或者1/4的时候,整体的性能会比较好(其实当p=1/2时,也就是抛硬币的方法)
确定好层数K
之后,然后在level 1...level K
各个层的链表都插入元素。参看如下示例:
1) 插入119, K=2
下图显示在插入119, K=2时的情形:
2) 插入119, K=4
下图显示在插入119, K=2时的情形:
下面我们给出跳跃表
插入算法:
2.6 跳跃表的删除
在各个层中找到包含x的节点,使用标准的delete from list
方法删除该节点。请参看下图删除71:
下面给出相应的代码实现:
2.7 跳跃表的销毁
如下给出跳跃表销毁代码:
3. 跳跃表完整实现
下面我们给出跳跃表的完整实现代码。分为如下三个部分:
-
头文件(skiplist.h)
-
源文件(skiplist.c)
-
测试文件(skiplist_test.c)
3.1 跳跃表实现头文件
如下是头文件skiplist.h:
3.2 跳跃表实现源文件
如下是头文件skiplist.c:
3.3 跳跃表测试
如下是跳跃表测试代码(注: 当前在score相同时,跳跃表插入、搜索等操作有些问题):
编译运行:
# gcc -o skiplist_test *.c
# ./skiplist_test
search...
matches=1
id: 1
remove 5 elements
print the first 5 elements
0.000000
1.000000
2.000000
3.000000
4.000000
print the last 5 element
19.000000
18.000000
17.000000
16.000000
15.000000
skiplist: length=15, max_level=6
node[1], score=0.000000, level=2
node[2], score=1.000000, level=1
node[3], score=2.000000, level=1
node[4], score=3.000000, level=1
node[5], score=4.000000, level=2
node[6], score=10.000000, level=1
node[7], score=11.000000, level=1
node[8], score=12.000000, level=1
node[9], score=13.000000, level=6
node[10], score=14.000000, level=1
node[11], score=15.000000, level=1
node[12], score=16.000000, level=2
node[13], score=17.000000, level=1
node[14], score=18.000000, level=2
node[15], score=19.000000, level=2
-
Skip List(跳跃表)原理详解与实现
-
跳跃表的实现
-
跳跃表实现(github)
-
跳跃表的实现
-
跳跃表的原理及实现
-
跳跃表 SkipList【数据结构】原理及实现
-
深夜学算法之SkipList:让链表飞
-
跳跃表的实现