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

 

有序表的搜索

比如一个有序表:

redis中跳跃表的权重 redis跳跃表实现_链表

 

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

redis中跳跃表的权重 redis跳跃表实现_跳跃表_02

这里我们把 < 14, 34, 50, 72 > 提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。

我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:  

redis中跳跃表的权重 redis跳跃表实现_跳跃表_03

 

跳跃表

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

如上图所示,跳跃表有以下性质:

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

我们看下跳跃表的定义:

redis中跳跃表的权重 redis跳跃表实现_跳跃表_04

可以看到一个跳跃表包含一个头、尾指针(header/tail),以及一个length长度字段,一个跳表层数字段。

那么跳跃表包含多个层级,每个层级包含多个节点。从图中我们看到,一个几点包含一个ele字段,表示成员对象,一个score字段表示该成员的得分,一个backward后退指针,一个层级结构包含后退指针和跨度。

另外说一个题外话,图中最后一个结构即有序集合的定义,我们可以看到是由一个字典 和 一个跳跃表实现的,今天我们主要介绍跳跃表实现的细节,关于字典的方方面面可以查阅公众号之前的一片文章 Redis源码阅读 - 哈希表的设计与实现

节点插入

节点插入需要先确定该元素要占据的层数 K(一般采用丢硬币的方式)然后在 Level 1 ... Level K 各个层的链表都插入元素。我们先看下K的随机策略实现:

redis中跳跃表的权重 redis跳跃表实现_链表_05

 

我们知道(random()&0xFFFF 都会将高位清零,得到 <= 0xFFFF的随机数,这个随机数比ZSKIPLIST_P * 0xFFFF小的概率为ZSKIPLIST_P。看源码定义,是0.25,即1/4的概率会+1。最后还有一个限制,层数不能大于最大层级限制常量ZSKIPLIST_MAXLEVEL,目前这个常量的默认值是32层。

下面我们看下 skiplist 数据插入算法的 具体的实现:

redis中跳跃表的权重 redis跳跃表实现_Redis_06

 

我们可以看到整个插入过程包含以下几个重要阶段:

  • 遍历 skiplist 中所有的层,通过score定位ele应该所处的位置,并保存在 update 中
  • random一个level的K值,是按我们上文介绍的方法随机出来的
  • 如果random出来的level比跳表原有level大,则增加跳表的level
  • 把新节点插入到update[i]的后面
  • 调整新节点的前驱指针、调整 skiplist 的长度

 

节点删除
有数据插入就有数据删除,跳表的删除算和插入算法步骤类似:找出每一层需删除数据的前驱并保存;接着调整指针,在 redis 中还会调整 span。我们看下源码实现:

redis中跳跃表的权重 redis跳跃表实现_链表_07

 

我们可以看到,跳表节点删除大致的过程如下:

  • x是需要删除的节点ele,先遍历找到节点x,update是每一个层x的前驱数组
  • 调整span和forward指针
  • 调整后驱指针
  • 删除某一个节点后,层数 level 可能降低,调整 level,调整跳表的长度

总结

  • 跳跃表是一种随机化数据结构,查找、添加、删除操作都可以在对数期望时间下完成。
  • 跳跃表目前在 Redis 的唯一作用,就是作为有序集类型的底层数据结构(之一,另一个构成有序集的结构是字典)。
  • 为了满足自身的需求,Redis 基于 William Pugh 论文中描述的跳跃表进行了修改。
    包括:score 值可重复;对比一个元素需要同时检查它的 score 和 memeber ;每个节点带有高度为 1 层的后退指针,用于从表尾方向向表头方向迭代。

自此我们学习完 skiplist 的方方面面,如有其他疑问欢迎留言探讨。

 

redis中跳跃表的权重 redis跳跃表实现_redis中跳跃表的权重_08