zoukankan      html  css  js  c++  java
  • Skip List(跳表)

     Skip List(跳表)

     

        跳表(Skip List),也称为跳跃表,是一种不怎么熟悉,但是听起来很腻害的数据结构,Redis有序集合的实现好像跟这个家伙也有关系呢,今天我们就来一起学习下吧。

        先举个例子,看看在实际场景中,哪些地方用得到它。

        一、需求:道具拍卖系统

        1、几个要点

        1)道具

        用来查阅和出售游戏中的道具

        2)支持四种排序方式

        按价格、等级、剩余时间、出售者ID排序 

        3)时间性能要求

        要尽可能快地展现给玩家

        查询:支持输入道具名称的精确查询和不输入名称的全量查询

        4)数据量

       拍卖行的商品总数量有几十万件,对应数据库商品表的几十万条记录

    • 按照商品名称精确查询,可以直接从数据库查出来,最多也就上百条记录
    • 不输入名称的全量查询

       实现:提前在内存中存储有序的全量商品集合,每一种排序方式都保存成独立的集合,每次请求的时候按照请求的排序种类,返回对应的集合

       比如按照价格字段排序的集合

       比如按照等级字段排序的集合

       需要注意的是,当时还没有Redis这样的内存数据库,所以只能自己实现一套合适的数据结构来存储。 

       二、选择什么样的数据结构?

       比如插入商品等级

       1.  考虑用数组用来实现

       数组:查找时间复杂度logN(二分查找),插入时间复杂度为0(N)

       总体时间复杂度为O(N)

       2. 考虑用链表来实现

       链表:查找时间复杂度O(N)(无法使用二分查找,只能和原链表中的节点逐一比较大小来确定位置),插入时间复杂度为O(1)

       总体时间复杂度为O(N)

     

       总结:这对于拥有几十万商品的集合来说,这两种方法显然都太慢了。

       三、使用跳表来实现

       1.  什么是跳表?

       跳跃表(skiplist)是一种基于有序链表的扩展,简称跳表。

       思考:怎样能更快查找到一个有序链表的某一节点呢?

       可以利用类似索引的思想,提取出链表中的部分关键节点。 

       提取的极限:同一层只有两个节点的时候,因为一个节点没有比较多意义。这样的多层链表结构,就是所谓的【跳跃表】。

     

       2. 理解跳表,从单链表开始说起

          1)单链表

         单链表的特性就是每个元素存放下一个元素的引用

          2)场景

         这里有一个原始链表:1->3->4->5->7->8->10->13->16->17->18

         现在有这样一个场景:需要从上面链表中快速找到10这个元素 

         分析:我们知道,在线性表的顺序存储结构中,我们要计算任意一个元素的存储位置是很容易的。但是在单链表中,由于第 i 个元素到底在哪?没办法一开始就知道,必须得从头开始找,直到找到我们需要找点元素。

         因此,这里查找路径为:1->3->4->5->7->8->10

         3)那么,如何提高链表的查找速度呢?

         我们从链表中每两个元素抽出来,加一级索引,一级索引指向了原始链表,如下图:

         第一级索引 1->4->7->9->13->17

         查找顺序:先在索引中遍历查找,1->4->7->9,发现 9 的后继节点是 13,比 10 大,于是不往后找了,而是通过 9 找到原始链表的 9,然后再往后遍历找到了我们要找的 10,遍历结束。

         优点:加了一级索引之后,查找路径为:1->4->7->9->10,查找节点需要遍历的元素相对少了,不需要对10之前对所有数据都遍历,查找的效率提升了。

         那如果加二级索引呢?

         第二级索引1->7->13

         查找路径:1->7->9->10 这样一来查找效率更高了。

         分析:有了二级索引之后,新的节点可以看和二级索引比较,确定大体范围,然后再和一级索引比较,最后在回到原链表,找到并插入对应位置。当节点很多的时候,比较次数会减少到原来的四分之一!近似于二分查找。

         这就是跳表的思想,用空间换时间,通过给链表建立索引,提高了查找的效率。 

         3. 如何应对大量新节点插入到原链表的场景?

         1)可能会产生的问题

         当大量的新节点通过逐层比较,最终插入到原链表之后,但是不更新索引,就可能出现两个索引节点之间数据非常多的情况,极端情况就是跳表退化为单链表,从而使得查找效率从O(logn)退化为O(n)。

         2)如何解决?

         我们需要在插入数据的时候,索引节点也需要相应的增加、或者重建索引,来避免查找效率的退化。这时候需要从新节点当中选取一部分提到上一层。可是究竟应该提拔谁,忽略谁呢?该如何去维护这个索引呢?

         比较容易理解的做法就是完全重建索引,我们每次插入数据后,都把这个跳表的索引删掉全部重建,重建索引的时间复杂度是多少呢?因为索引的空间复杂度是 O(n),即:索引节点的个数是 O(n) 级别,每次完全重新建一个 O(n) 级别的索引,时间复杂度也是 O(n) 。造成的后果是:为了维护索引,导致每次插入数据的时间复杂度变成了 O(n)。所以这种办法实际中不可取。

         3)解决办法:抛硬币

         跳跃表的设计者采用了一种有趣的办法:【抛硬币】。也就是随机决定新节点是否提拔,每向上提拔一层的几率是50%

         采用抛硬币这个办法的理由:

         1)因为跳跃表删除和添加的节点是不可预测的,很难用一种有效的算法来保证跳表的索引分布始终均匀。

         2)随机抛硬币的方法虽然不能保证索引绝对均匀分布,却可以让大体趋于均匀

        四、跳表的进一步认识

        从上面的描述可以看出,跳表是可以实现二分查找的有序链表。下面来进一步认识跳表。

        1.  查找的时间复杂度

        查找元素的过程是从最高级索引开始,一层一层遍历最后下沉到原始链表。所以,时间复杂度 = 索引的高度 * 每层索引遍历元素的个数。

        推算

        原始链表有n个元素,则一级索引有n/2个元素,二级索引有n/4个元素...k级索引就有n/(2k次方)个元素。

        最高级索引一般有2个元素,即:最高级索引h满足2=n/(2h次方),即h=log2n-1

        最高级索引h为索引层的高度,加上原始数据一层,跳表的总高度为log2n

        跳表的索引高度 h = log2n,且每层索引最多遍历 3 个元素。所以跳表中查找一个元素的时间复杂度为 O(3*logn),省略常数即:O(logn)

        2.  空间复杂度

        跳表通过建立索引,来提高查找元素的效率,就是典型的空间换时间的思想,所以在空间上做了一些牺牲,那空间复杂度到底是多少呢? O(n) 

        3.  跳跃表的添加操作

        1)新节点和各层索引节点逐一比较,确定原链表的插入位置。OlogN

        2)把索引插入到原链表。O1

        3)利用抛硬币的随机方式,决定新节点是否提升为上一级索引。结果为则提升并继续抛硬币,结果为则停止。OlogN 

       总体上,跳跃表插入操作的时间复杂度是OlogN),而这种数据结构所占空间是2N,既空间复杂度是 ON)。

       4.  跳跃表的删除操作

       删除操作比较简单,只要在索引层找到要删除的节点,然后顺藤摸瓜,删除每一层的相同节点即可。

       如果某一层索引在删除后只剩下一个节点,那么整个一层就可以干掉了。还用原来的例子,如果要删除的节点值是5

       总结:跳跃表删除节点的操作

       1)自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。OlogN

       2)删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。OlogN

      总体上,跳跃表删除操作的时间复杂度是OlogN

       五、跳跃表和二叉查找树的区别是什么?

       跳跃表的优点是维持结构平衡的成本比较低,完全依靠随机。而二叉查找树在多次插入删除后,需要Rebalance来重新调整结构平衡。所以说没有绝对优劣的数据结构,关键还要看应用场景。

       小灰和大黄并不知道,他们的这一解决方案和若干年后Redis当中的Sorted-set不谋而合。而Sorted-set这种有序集合,正是对于跳跃表的改进和应用。

     

      

    参考链接:

    https://zhuanlan.zhihu.com/p/53975333

    https://www.jianshu.com/p/9d8296562806

  • 相关阅读:
    php js表单登陆验证
    jQuery Ajax 简单的实现跨域请求
    常见http代码错误原因及处理
    使用git做服务器端代码的部署
    mysql之消息队列
    MySQL触发器使用详解
    mysql之触发器before和after的区别
    mysql之触发器trigger
    手把手教你使用Git
    xcode: {} 花括号缩进一个空格
  • 原文地址:https://www.cnblogs.com/hld123/p/15760929.html
Copyright © 2011-2022 走看看