目录

跳跃表的原理

跳跃表的实现步骤分析

代码实现

跳跃表的应用


跳跃表的原理

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

如上图,我们要查询元素为55的结点,必须从头结点,循环遍历到最后一个节点,不算-INF(负无穷)一共查询8次。那么用什么办法能够用更少的次数访问55呢?最直观的,当然是新开辟一条捷径去访问55。

如上图,我们要查询元素为55的结点,只需要在L2层查找4次即可。在这个结构中,查询结点为46的元素将耗费最多的查询次数5次。即先在L2查询46,查询4次后找到元素55,因为链表是有序的,46一定在55的左边,所以L2层没有元素46。然后我们退回到元素37,到它的下一层即L1层继续搜索46。非常幸运,我们只需要再查询1次就能找到46。这样一共耗费5次查询。

那么,如何才能更快的搜寻55呢?有了上面的经验,我们就很容易想到,再开辟一条捷径。

如上图,我们搜索55只需要2次查找即可。这个结构中,查询元素46仍然是最耗时的,需要查询5次。即首先在L3层查找2次,然后在L2层查找2次,最后在L1层查找1次,共5次。很显然,这种思想和2分非常相似,那么我们最后的结构图就应该如下图。

我们可以看到,最耗时的访问46需要6次查询。即L4访问55,L3访问21、55,L2访问37、55,L1访问46。我们直觉上认为,这样的结构会让查询有序链表的某个元素更快。那么究竟算法复杂度是多少呢?

如果有n个元素,因为是2分,所以层数就应该是log n层 (本文所有log都是以2为底),再加上自身的1层。以上图为例,如果是4个元素,那么分层为L3和L4,再加上本身的L2,一共3层;如果是8个元素,那么就是3+1层。最耗时间的查询自然是访问所有层数,耗时logn+logn,即2logn。为什么是2倍的logn呢?我们以上图中的46为例,查询到46要访问所有的分层,每个分层都要访问2个元素,中间元素和最后一个元素。所以时间复杂度为O(logn)。

至此为止,我们引入了最理想的跳跃表,但是如果想要在上图中插入或者删除一个元素呢?比如我们要插入一个元素22、23、24……,自然在L1层,我们将这些元素插入在元素21后,那么L2层,L3层呢?我们是不是要考虑插入后怎样调整连接,才能维持这个理想的跳跃表结构。我们知道,平衡二叉树的调整是一件令人头痛的事情,左旋右旋左右旋……一般人还真记不住,而调整一个理想的跳跃表将是一个比调整平衡二叉树还复杂的操作。幸运的是,我们并不需要通过复杂的操作调整连接来维护这样完美的跳跃表。有一种基于概率统计的插入算法,也能得到时间复杂度为O(logn)的查询效率,这种跳跃表才是我们真正要实现的。

跳跃表的实现步骤分析

先讨论插入,我们先看理想的跳跃表结构,L2层的元素个数是L1层元素个数的1/2,L3层的元素个数是L2层的元素个数的1/2,以此类推。从这里,我们可以想到,只要在插入时尽量保证上一层的元素个数是下一层元素的1/2,我们的跳跃表就能成为理想的跳跃表。那么怎么样才能在插入时保证上一层元素个数是下一层元素个数的1/2呢?很简单,抛硬币就能解决了!假设元素X要插入跳跃表,很显然,L1层肯定要插入X。那么L2层要不要插入X呢?我们希望上层元素个数是下层元素个数的1/2,所以我们有1/2的概率希望X插入L2层,那么抛一下硬币吧,正面就插入,反面就不插入。那么L3到底要不要插入X呢?相对于L2层,我们还是希望1/2的概率插入,那么继续抛硬币吧!以此类推,元素X插入第n层的概率是(1/2)的n次。这样,我们能在跳跃表中插入一个元素了。

在此还是以上图为例:跳跃表的初试状态如下图,表中没有一个元素:

如果我们要插入元素2,首先是在底部插入元素2,如下图:

然后我们抛硬币,结果是正面,那么我们要将2插入到L2层,如下图:

继续抛硬币,结果是反面,那么元素2的插入操作就停止了,插入后的表结构就是上图所示。接下来,我们插入元素33,跟元素2的插入一样,现在L1层插入33,如下图:

然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,我们插入元素55,首先在L1插入55,插入后如下图:

然后抛硬币,结果是正面,那么L2层需要插入55,如下图:

继续抛硬币,结果又是正面,那么L3层需要插入55,如下图:

以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。

当然,这样的分析在感性上是很直接的,但是时间复杂度的证明实在复杂,在此我就不深究了,感兴趣的可以去看关于跳跃表的paper。再讨论删除,删除操作没什么讲的,直接删除元素,然后调整一下删除元素后的指针即可。跟普通的链表删除操作完全一样。再来讨论一下时间复杂度,插入和删除的时间复杂度就是查询元素插入位置的时间复杂度,这不难理解,所以是O(logn)。

代码实现

在章节2中,我们采用抛硬币的方式来决定新元素插入的最高层数,这当然不能在程序中实现。代码中,我们采用随机数生成的方式来获取新元素插入的最高层数。我们先估摸一下n的规模,然后定义跳跃表的最大层数maxLevel,那么底层,也就是第0层,元素是一定要插入的,概率为1;最高层,也就是maxLevel层,元素插入的概率为1/2^maxLevel。

我们先随机生成一个范围为0~2^maxLevel-1的一个整数r。那么元素r小于2^(maxLevel-1)的概率为1/2,r小于2^(maxLevel-2)的概率为1/4,……,r小于2的概率为1/2^(maxLevel-1),r小于1的概率为1/2^maxLevel。

举例,假设maxLevel为4,那么r的范围为0~15,则r小于8的概率为1/2,r小于4的概率为1/4,r小于2的概率为1/8,r小于1的概率为1/16。1/16正好是maxLevel层插入元素的概率,1/8正好是maxLevel层插入的概率,以此类推。

通过这样的分析,我们可以先比较r和1,如果r<1,那么元素就要插入到maxLevel层以下;否则再比较r和2,如果r<2,那么元素就要插入到maxLevel-1层以下;再比较r和4,如果r<4,那么元素就要插入到maxLevel-2层以下……如果r>2^(maxLevel - 1),那么元素就只要插入在底层即可。

以上分析是随机数算法的关键。算法跟实现跟语言无关,但是Java程序员还是更容易看明白Java代码实现的跳跃表,以下贴一下别人的java代码实现。

/***************************  SkipList.java  *********************/import java.util.Random;public class SkipList<T extends Comparable<? super T>> {private int maxLevel;private SkipListNode<T>[] root;private int[] powers;private Random rd = new Random();SkipList() {this(4);}SkipList(int i) {maxLevel = i;root = new SkipListNode[maxLevel];powers = new int[maxLevel];for (int j = 0; j < maxLevel; j++)root[j] = null;choosePowers();}public boolean isEmpty() {return root[0] == null;}public void choosePowers() {powers[maxLevel-1] = (2 << (maxLevel-1)) - 1;    // 2^maxLevel - 1for (int i = maxLevel - 2, j = 0; i >= 0; i--, j++)powers[i] = powers[i+1] - (2 << j);           // 2^(j+1)}public int chooseLevel() {int i, r = Math.abs(rd.nextInt()) % powers[maxLevel-1] + 1;for (i = 1; i < maxLevel; i++)if (r < powers[i])return i-1; // return a level < the highest level;return i-1;         // return the highest level;}// make sure (with isEmpty()) that search() is called for a nonempty list;public T search(T key) { int lvl;SkipListNode<T> prev, curr;            // find the highest nonnullfor (lvl = maxLevel-1; lvl >= 0 && root[lvl] == null; lvl--); // level;prev = curr = root[lvl];while (true) {if (key.equals(curr.key))          // success if equal;return curr.key;else if (key.compareTo(curr.key) < 0) { // if smaller, go down,if (lvl == 0)                 // if possiblereturn null;      else if (curr == root[lvl])   // by one levelcurr = root[--lvl];      // starting from theelse curr = prev.next[--lvl]; // predecessor which}                                  // can be the root;else {                             // if greater,prev = curr;                  // go to the nextif (curr.next[lvl] != null)   // non-null nodecurr = curr.next[lvl];   // on the same levelelse {                        // or to a list on a lower level;for (lvl--; lvl >= 0 && curr.next[lvl] == null; lvl--);if (lvl >= 0)curr = curr.next[lvl];else return null;}}}}public void insert(T key) {SkipListNode<T>[] curr = new SkipListNode[maxLevel];SkipListNode<T>[] prev = new SkipListNode[maxLevel];SkipListNode<T> newNode;int lvl, i;curr[maxLevel-1] = root[maxLevel-1];prev[maxLevel-1] = null;for (lvl = maxLevel - 1; lvl >= 0; lvl--) {while (curr[lvl] != null && curr[lvl].key.compareTo(key) < 0) { prev[lvl] = curr[lvl];           // go to the nextcurr[lvl] = curr[lvl].next[lvl]; // if smaller;}if (curr[lvl] != null && key.equals(curr[lvl].key)) // don't return;                          // include duplicates;if (lvl > 0)                         // go one level downif (prev[lvl] == null) {         // if not the lowestcurr[lvl-1] = root[lvl-1]; // level, using a linkprev[lvl-1] = null;        // either from the root}else {                           // or from the predecessor;curr[lvl-1] = prev[lvl].next[lvl-1];prev[lvl-1] = prev[lvl];}}lvl = chooseLevel();                // generate randomly level newNode = new SkipListNode<T>(key,lvl+1); // for newNode;for (i = 0; i <= lvl; i++) {        // initialize next fields ofnewNode.next[i] = curr[i];      // newNode and reset to newNodeif (prev[i] == null)            // either fields of the rootroot[i] = newNode;         // or next fields of newNode'selse prev[i].next[i] = newNode; // predecessors;}}
}

跳跃表的应用

我们都知道 Redis 的有序集合底层使用的就是跳跃表。为什么 Redis 要用跳表来实现有序集合,而不是红黑树

Redis 中的有序集合是通过跳表来实现的,严格点讲,其实还用到了散列表。Redis 中的有序集合支持的核心操作主要有下面这几个:

  • 插入一个数据;
  • 删除一个数据;
  • 查找一个数据;
  • 按照区间查找数据;
  • 迭代输出有序序列。

选择有序集合,而不是红黑树有如下几个原因:

  • 其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。对于按照区间查找数据这个操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。
  • 跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。
  • 跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。

不过,跳表也不能完全替代红黑树。因为红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现的。

我们做业务开发的时候,直接拿来用就可以了,不用费劲自己去实现一个红黑树,但是跳表并没有一个现成的实现,所以在开发中,如果你想使用跳表,必须要自己实现。

跳跃表的原理和实现以及应用相关推荐

  1. 用Python深入理解跳跃表原理及实现

    最近看 Redis 的实现原理,其中讲到 Redis 中的有序数据结构是通过跳跃表来进行实现的.第一次听说跳跃表的概念,感到比较新奇,所以查了不少资料.其中,网上有部分文章是按照如下方式描述跳跃表的: ...

  2. Redis 为什么这么快? Redis 的有序集合 zset 的底层实现原理是什么? —— 跳跃表 skiplist

    Redis有序集合 zset 的底层实现--跳跃表skiplist Redis简介 Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库.缓存和消息中间件. 它支持多种类型的数据结构,如 ...

  3. 常见索引结构—跳跃表

    原文作者:JeemyJohn 原文地址:跳跃表的原理及实现 推荐阅读:漫画算法:什么是跳跃表? 1. 跳跃表的原理 学过数据结构的都知道,在单链表中查询一个元素的时间复杂度为O(n),即使该单链表是有 ...

  4. Redis跳跃表(SkipList)

    什么是跳跃表 跳跃表(skiplist)是一种有序且随机化的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的. 跳跃表的用处 有序集合(zset)的底层可以采用数组. ...

  5. Redis - 跳跃表

    一.跳跃表简介 跳跃表(skiplist)是一种随机化的数据结构,由 William Pugh 在论文<Skip lists: a probabilistic alternative to ba ...

  6. Redis面试题-Redis跳跃表

    本文参考 嗨客网 Redis面试题 Redis跳跃表 什么是跳跃表 Redis 中的跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的. 为什么使用跳 ...

  7. Redis面试题系列:跳跃表

    简介 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.在大部分情况下,跳跃表的效率可以和平衡树相媲美,而且实现比平衡树更加简单. ...

  8. 跳跃表(Skip list)原理与java实现

    转载自 [算法导论33]跳跃表(Skip list)原理与java实现 Skip list是一个用于有序元素序列快速搜索的数据结构,由美国计算机科学家William Pugh发明于1989年.它的效率 ...

  9. 跳跃表 skipList 跳表的原理以及golang实现

    跳跃表 skipList 调表的原理以及golang实现 调表skiplist 是一个特殊的链表,相比一般的链表有更高的查找效率,跳跃表的查找,插入,删除的时间复杂度O(logN) Redis中的有序 ...

最新文章

  1. Moss/Sharepoint 一些很重要的API备忘
  2. Mac OS X 下mysql配置备忘
  3. 经济学人: 低调应用, 高调回报, 亚马逊才是AI技术最大受益企业
  4. idea 设置光标回到上一次位置的快捷键
  5. JS三大对象中常用方法集锦
  6. 2020年春季学期信号与系统课程作业参考答案-第十四次作业
  7. java动态代理--代理接口无实现类
  8. 频谱扩展 matlab,语音信号频谱扩展
  9. linux-shell入门-shell两种使用方式-shell的基本特性
  10. java跳出循环break;return;continue使用
  11. 天池学习赛:工业蒸汽量预测2——特征工程
  12. 做了MVC模式一年,可不要把传统模式忘记呀!ashx配合aspx实现AJAX加载JSON数据...
  13. 51单片机跑马灯程序
  14. 计算机专业英语1700+
  15. 学校固定资产管理系统由谁来做,云呐RFID固定资产管理系统
  16. 采用java编写一个软件,100以内的口算题【软件构造大作业】
  17. U盘里面的文件夹变成文件也打不开文件的寻回方法
  18. 利用Spire复制Excel工作表(worksheet)
  19. linux 网站服务器优化 web server优化
  20. 【原型模式】原型模式深入分析

热门文章

  1. 计算机科学拔尖计划毕业生,【关注】中国科学院大学“拔尖计划”首届本科毕业生深造率达85.1%...
  2. Hive SQL优化思路分享
  3. 简单读懂人工智能:机器学习与深度学习是什么关系
  4. librosa 音频分析
  5. 前端网址转二维码实现
  6. jieba分词textrank算法
  7. java代码自动对齐_java程序实现编写代码时变量名垂直对齐的风格
  8. I.MX6ULL裸机LED驱动实验过程
  9. 电子科技大学2011陕西计算机,西安电子科技大学 2011-2014 陕西理科一本分专业录取分数和位次...
  10. 正则验证密码格式(密码必须包含字母大小写、数字、特殊字符,且不能少于8位)