跳表:为什么Redis一定要用跳表来实现有序集合?

        对链表稍加改造,就可以支持类似“二分”的查找算法。我们把改造之后的数据结构叫做跳表。它是一个各方面性能比较优秀的动态数据结构,可以支持快速的插入、删除、查找操作。

       Redis中的有序集合(Sorted Set)就是用跳表来实现的。Redis为什么会选择跳表来实现有序集合呢?为什么不用红黑树

1. 理解“跳表”

      链表加上多级索引的结构,就是跳表。

      原始单链表,想要查找某个数据,即使数据是有序的,也得从头开始遍历,所以效率会比较低,时间复杂度会比较高。

      跳表:为什么Redis一定要用跳表来实现有序集合?_第1张图片

        为了提高查找效率,可以像下图这样对链表建立一级“索引”,每两个结点取一个结点到上一级,把抽取出来的那一级叫做索引或是索引层。图中的down表示down指针,指向下一级结点。

      跳表:为什么Redis一定要用跳表来实现有序集合?_第2张图片

     还可以在第一级索引的基础上,再往上提取第二级索引,如下图所示:

     跳表:为什么Redis一定要用跳表来实现有序集合?_第3张图片

       现在,以查找结点16为例,来进行对比分析。

  • 图一的原始单链表,为了查找结点16,需要遍历10个结点
  • 图二的原始链表+第一级索引,为了查找结点16,需要遍历7个结点
  • 图三的原始链表+第一二级索引,为了找到结点16,需要遍历6个结点

      从上面的例子,可以看出,加上索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找的效率提高了。

      前面讲的这种链表加上多级索引的结构,就是跳表。

 

2. 跳表查询的时间复杂度

        在一个单链表中查询某个数据的时间复杂度是O(n),那么在一个具有多级索引的跳表结构中,查询某个数据的时间复杂度是多少呢?假设一个链表有n个结点,那第一级索引的结点个数大约是n/2,第二级索引的结点个数大约是n/4,以此类推,第k级索引的结点个数是第k-1级索引的1/2。那就是n/(2^{k})。

       假设索引有h级,最高级有2个结点,即n/(2^{h})=2,求得h = log_{2}n-1,如果包含原始那一层,那整个跳表的高度就是log_{2}n,如果每一层都要遍历m个结点,那在跳表中查询某一个数据的时间复杂度就是O(m*logn)。由前面那种索引结构,每一级索引最多只需要遍历3个结点,所以m=3。

       所以,在跳表中查询任意数据的时间复杂度是O(logn)。

 

3. 跳表查询的空间复杂度

      对于一个原始链表大小为n的跳表,各级索引的结点个数依次是:n/2,n/4,n/8...4,2。

      跳表:为什么Redis一定要用跳表来实现有序集合?_第4张图片

        n/2+n/4+n/8+...+4+2 = n-2,所以跳表的空间复杂度是O(n)。也就是说,将包含n个结点的单链表构造成跳表,我们需要额外用接近n个结点的存储空间。

       如果每三个结点提取一个结点作为上一级索引,如图所示:

       跳表:为什么Redis一定要用跳表来实现有序集合?_第5张图片

      那此时每一级的结点个数构成的等比数列如下:

      跳表:为什么Redis一定要用跳表来实现有序集合?_第6张图片

       n/3+n/9+n/27+...+3+1 = n/2,尽管空间复杂度仍然是O(n),但是相比上面的两个结点抽一个结点的索引构建方法,这个方法要少一半的存储空间。

 

4. 跳表高效的动态插入和删除

        插入操作:查找需要插入的位置,对于纯粹的单链表来说,需要遍历每个结点,找到插入的位置。但是对于跳表来讲,查找某个数据应该插入的位置与前面讲的查找某个结点的方式类似,时间复杂度也是O(logn)。从下图可以清晰看到插入的过程:

       跳表:为什么Redis一定要用跳表来实现有序集合?_第7张图片

        删除操作:删除操作就是找到要删除的结点,然后找到其前驱结点,然后通过指针操作完成删除。但是如果这个结点在索引中也有出现,那除了删除原始链表中的结点,还需要删除索引中的结点。

 

5. 跳表索引动态更新

        当不停往跳表中添加数据的时候,如果不更新索引,会导致某一级索引的两个结点之间的数据非常多,极端情况下,可能会退化为单链表。

       跳表:为什么Redis一定要用跳表来实现有序集合?_第8张图片

        作为一种动态数据结构,需要维护索引和原始链表大小之间的平衡关系。如果链表中的结点多,索引结点就相应增多,这样可以避免复杂度退化,以及查询、插入、删除操作性能下降。

        当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分的索引层中。通过随机函数生成数k,然后将这个结点添加到第一级到第k级这k级索引当中。

      跳表:为什么Redis一定要用跳表来实现有序集合?_第9张图片

 

6.为什么Redis一定要用跳表来实现有序集合?

     Redis的有序集合支持的核心操作主要有下面这几个:

  • 插入一个数据
  • 删除一个数据
  • 查找一个数据
  • 按照区间查找数据(比如查找值在区间[100,356]的数据)
  • 迭代输出有序序列(不是很懂

        在插入、删除、查找以及迭代输出有序序列这几个操作上,跳表跟红黑树的时间复杂度是一样的,但是在按区间查找数据的操作上,跳表的效率比红黑树更高。

  1. 跳表较红黑树更好实现,意味着可读性好、不易出错。
  2. 跳表更加灵活,可以通过改变索引结构来平衡执行效率和内存消耗之间的关系。

关于红黑树的介绍,参见后面的文章。

 

 

 

 

 

 

 

 

你可能感兴趣的:(学习笔记)