SkipList是一种有序的数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。

    跳跃表支持平均O(logN),最坏O(N)负责度的节点查找。还可以通过顺序性操作来批量处理节点,在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且跳跃表的实现比平衡树来的更为简单,所以有不少程序使用跳跃表来代替平衡树。

    Redis中的有序集合键使用跳跃表作为底层的实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis会使用跳跃表来作为有序集合的底层实现。

    Redis中只有两个地方使用到了跳跃表,一个是有序集合,一个是集群节点中作为内部数据结构。

    跳跃表的原理:

        性质:

            1、跳跃表由很多层结构组成

            2、每一层都是一个有序的集合,排列顺序由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点。

            3、最底层的链表包含了所有的元素

            4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也都会出现(上一层的元素是当前层的元素的子集)

            5、链表中的每个节点都至少包含两个指针,一个指向同一层的下一个链表节点,一个指向下一层的同一个链表节点

    上面的性质看的可能有些绕,我们先来看下跳跃表的大致模样。

Redis中的skipList

如图,我们可以理解跳跃表类似于游戏中的跳跃,在不同楼层之间跳跃,用于寻找那个家的位置。

    下面我们来理解下跳跃表的性质

        性质1:这个略过

        性质2:每一层都是一个有序的集合,排列顺序由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点。这个我们从图上面可以看出,由于不同层,组成了不同密度的链表,可以理解为不同的层次之间有不同的联系。组成了不同层级之间链表。

        性质3:这个略过

        性质4:如果一个元素出现在某一层的链表中,那么在该层之下的链表也都会出现(上一层的元素是当前层的元素的子集)这个也比较好理解,如果元素是3,那么最底层的链表肯定包含了3,同样的3对应的第2层也包含了3,第2层很明显是底层的一个子集。

        性质5:链表中的每个节点都至少包含两个指针,一个指向同一层的下一个链表节点,一个指向下一层的同一个链表节点 

        性质5有点绕口,同一层的下一个链表节点和下一层的同一个链表节点。理解这个,需要我们清楚一个前提,skipList本身是具有顺序的链表结构,因此在查找的时候,会充分利用这个特性。首先,我们寻找是自上而下开始寻找的,由于最高层的链表的节点数量是最少的,因此我们可以通过最高层之前的有序性,来确定目标节点的最高层的范围,

例如对节点5的寻找,首先寻找到了4节点的最高层,比较了与4节点相同高度的只有6节点,因此比较6节点的最高层的值,发现5节点的score(这边用score来表示有序性的***)在4节点和6节点之间,因此寻找4节点的次高层,向下延伸,发现4节点次高层的同一高度最近的节点还是6节点的次高层,再次递归比较,发现6节点的次高层的score比5节点的要大,因此继续从4节点向下延伸,到最底层发现5节点的是4节点的最底层的下一个节点,继续比较score的值,发现5是符合条件的,继续比较5节点的内容和寻找的是否一致,确定节点。这样我们可以看出来平均的复杂度是O(logN)

 

    明白了跳跃表的原理,我们来看Redis中的SortSet的实现。

   Redis中sortSet的实现:

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

Redis中的skipList

如上图所示,展示了一个跳跃表,左侧的是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是节点保存的成员对象。

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

 以下是跳跃表的数据结构:

Redis中的skipListRedis中的skipList

跳跃表的API:

Redis中的skipList

使用跳跃表,我们可以很方便的查找一个范围区间的数据。

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比平衡树要简单得多

转载于:https://my.oschina.net/guanhe/blog/2253672

相关文章:

  • 2022-12-23
  • 2021-12-29
  • 2022-12-23
  • 2022-12-23
  • 2021-09-08
  • 2021-07-02
  • 2022-01-13
  • 2021-06-30
猜你喜欢
  • 2021-12-03
  • 2021-08-25
  • 2021-06-29
  • 2022-01-13
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
相关资源
相似解决方案