跳跃表skipList

130 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情

跳跃表(skiplist)

跳跃表(skiplist)是一种能高效实现插入、删除、查找的内存有序数据结构,它通过在每个节点中维持多个指向其他节点的指针(注:可以理解为维护了多条路径),从而达到快速访问节点的目的。 操作的期望复杂度都是O(logN)

单链表中查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过2分的方式缩减时间复杂度

image.png

特点: (1)跳跃表的每一层都是一条有序的链表。

(2)维护了多条节点路径。

(3)最底层的链表包含所有元素。

(4)跳跃表的空间复杂度为 O(n)。

(5)跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

  在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。 与红黑树以及其他的二分查找树相比,跳跃表的优势在于实现简单,而且在并发场景下加锁粒度更小,从而可以实现更高的并发性。正因为这些优点,跳跃表广泛使用于KV数据库中,诸如Redis、LevelDB、HBase都把跳跃表作为一种维护有序数据集合的基础数据结构。

Redis中跳跃表

  Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。和链表、字典等数据结构被广泛地应用在Redis内部不同,Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构,除此之外,跳跃表在Redis里面没有其他用途。

Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。

image.png

上图所示,展示了一个跳跃表,左侧的是zskipList的结构,该结构的元素如下:

    1、header:指向跳跃表的表头节点

    2、tail:指向跳跃表的表尾节点

    3、level:记录在跳跃表内,层数最大的那个节点的层数(表头的层数不计算在内) 这个主要是用于节省开始层数判断

    4、length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)

    而zskipListNode结构则是包含以下的属性:

    1、level(层):节点中使用L1,L2,L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,一次类推

    2、backword(后退指针):节点中使用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序中从表尾向表头遍历时使用。

    3、score(分值):各个节点中1.0,2.0,3.0是节点保存的分值,在跳跃表中节点按照各自保存的分值从小到大排列。

    4、obj(成员变量):各个节点中的o1,o2和o3是节点保存的成员对象。

    注意表头节点和其他节点的构造是一样的,表头节点也有后退指针,分值和成员对象,不过表头节点的这些属性都不会被用到,因此图中没有展示这些属性。

skipList与平衡树、Hash表的比较:

1、skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。

2、在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。

3、平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。

4、从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

5、查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。

6、从算法实现难度上来比较,skiplist比平衡树要简单得多。