一、跳表的基本概念

1、跳表的定义

  跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质是一种可以进行二分查找的有序链表。跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

  跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树、AVL树不相上下,但是跳表的原理非常简单,目前在Redis和LevelDB中都有用到。

2、跳表的详解

                  说明:本文中的图片均来自极客时间《数据结构与算法之美专栏》

  对于一个单链表来说,即使链表中的数据是有序的,如果我们想要查找某个数据,也必须从头到尾的遍历链表,很显然这种查找效率是十分低效的,时间复杂度为O(n)。

  那么我们如何提高查找效率呢?我们可以对链表建立一级“索引”,每两个结点提取一个结点到上一级,我们把抽取出来的那一级叫做索引或者索引层,如下图所示,down表示down指针。

  假设我们现在要查找值为16的这个结点。我们可以先在索引层遍历,当遍历索引层中值为13的时候,通过值为13的结点的指针域发现下一个结点值为17,因为链表本身有序,所以值为16的结点肯定在13和17这两个结点之间。然后我们通过索引层结点的down指针,下降到原始链表这一层,继续往后遍历查找。这个时候我们只需要遍历2个结点(值为13和16的结点),就可以找到值等于16的这个结点了。如果使用原来的链表方式进行查找值为16的结点,则需要遍历10个结点才能找到,而现在只需要遍历7个结点即可,从而提高了查找效率。

  那么我们可以由此得到启发,和上面建立第一级索引的方式相似,在第一级索引的基础上,每两个一级索引结点就抽到一个结点到第二级索引中。再来查找值为16的结点,只需要遍历6个结点即可,从而进一步提高了查找效率。

  上面举得例子中的数据量不大,所以即便加了两级索引,查找的效率提升的也不是很明显,下面通过一个64结点的链表来更加直观的感受下索引提升查找效率,如图所示,建立了五级索引。

  从图中我们可以看出来,原来没有索引的时候,查找62需要遍历62个结点,现在只需要遍历11个结点即可,速度提高了很多。那么,如果当链表的长度为10000、10000000时,通过构件索引之后,查找的效率就会提升的非常明显。

3、跳表的时间复杂度

  单链表的查找时间复杂度为:O(n),下面分析下跳表这种数据结构的查找时间复杂度:

  我们首先考虑这样一个问题,如果链表里有n个结点,那么会有多少级索引呢?按照上面讲的,每两个结点都会抽出一个结点作为上一级索引的结点。那么第一级索引的个数大约就是n/2,第二级的索引大约就是n/4,第三级的索引就是n/8,依次类推,也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那么第k级的索引结点个数为:n/2^k。

  假设索引有h级,最高级的索引有2个结点,通过上面的公式,我们可以得到n/(2^h) = 2,从而可得:h = logn - 1。如果包含原始链表这一层,整个跳表的高度就是logn。我们在跳表中查找某个数据的时候,如果每一层都要遍历m个结点,那么在跳表中查询一个数据的时间复杂度就为:O(m*logn)。

  其实根据前面的分析,我们不难得出m=3,即每一级索引都最多只需要遍历3个结点,分析如下:

  因此,m=3,所以跳表查找任意数据的时间复杂度为O(logn),这个查找的时间复杂度和二分查找是一样的,但是我们却是基于单链表这种数据结构实现的。不过,天下没有免费的午餐,这种查找效率的提升是建立在很多级索引之上的,即空间换时间的思想。其具体空间复杂度见下文详解。

4、跳表的空间复杂度

  比起单纯的单链表,跳表就需要额外的存储空间去存储多级索引。假设原始链表的大小为n,那么第一级索引大约有n/2个结点,第二级索引大约有n/4个结点,依次类推,每上升一级索引结点的个数就减少一半,直到剩下最后2个结点,如下图所示,其实就是一个等比数列。

  这几级索引结点总和为:n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n - 2。所以跳表的空间复杂度为O(n)。也就是说如果将包含n个结点的单链表构造成跳表,我们需要额外再用接近n个结点的存储空间。

  其实从上面的分析,我们利用空间换时间的思想,已经把时间压缩到了极致,因为每一级每两个索引结点就有一个会被抽到上一级的索引结点中,所以此时跳表所需要的额外内存空间最多,即空间复杂度最高。其实我们可以通过改变抽取结点的间距来降低跳表的空间复杂度,在其时间复杂度和空间复杂度方面取一个综合性能,当然也要看具体情况,如果内存空间足够,那就可以选择最小的结点间距,即每两个索引结点抽取一个结点到上一级索引中。如果想降低跳表的空间复杂度,则可以选择每三个或者每五个结点,抽取一个结点到上级索引中。

5、跳表的插入

  跳表插入的时间复杂度为:O(logn),支持高效的动态插入。

  在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是O(1)。但是为了保证原始链表中数据的有序性,我们需要先找到要插入的位置,这个查找的操作就会比较耗时。

  对于纯粹的单链表,需要遍历每个结点,来找到插入的位置。但是对于跳表来说,查找的时间复杂度为O(logn),所以这里查找某个数据应该插入的位置的时间复杂度也是O(logn),如下图所示:

6、跳表的删除

  跳表的删除操作时间复杂度为:O(logn),支持动态的删除。

  在跳表中删除某个结点时,如果这个结点在索引中也出现了,我们除了要删除原始链表中的结点,还要删除索引中的。因为单链表中的删除操作需要拿到删除结点的前驱结点,然后再通过指针操作完成删除。所以在查找要删除的结点的时候,一定要获取前驱结点(双向链表除外)。因此跳表的删除操作时间复杂度即为O(logn)。

7、跳表索引动态更新

  当我们不断地往跳表中插入数据时,我们如果不更新索引,就有可能出现某2个索引节点之间的数据非常多的情况,在极端情况下,跳表还会退化成单链表,如下图所示:

  作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中的结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入和删除操作性能的下降。

  如果你了解红黑树、AVL树这样的平衡二叉树,你就会知道它们是通过左右旋的方式保持左右子树的大小平衡,而跳表是通过随机函数来维护“平衡性”。

  当我们往跳表中插入数据的时候,我们可以通过一个随机函数,来决定这个结点插入到哪几级索引层中,比如随机函数生成了值K,那我们就将这个结点添加到第一级到第K级这个K级索引中。如下图中要插入数据为6,K=2的例子:

  随机函数的选择是非常有讲究的,从概率上讲,能够保证跳表的索引大小和数据大小平衡性,不至于性能的过度退化。至于随机函数的选择,见下面的代码实现过程,而且实现过程并不是重点,掌握思想即可。

8、跳表的性质

(1) 由很多层结构组成,level是通过一定的概率随机产生的;
(2) 每一层都是一个有序的链表,默认是升序 ;
(3) 最底层(Level 1)的链表包含所有元素;
(4) 如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现;
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

二、跳表的Java代码实现

说明:跳表的代码实现并不重要,只是为了帮助理解它的查找过程。

// 跳表中存储的是正整数,并且存储的数据是不重复的public class SkipList {private static final int MAX_LEVEL = 16;    // 结点的个数private int levelCount = 1;   // 索引的层级数private Node head = new Node();    // 头结点private Random random = new Random();// 查找操作public Node find(int value){Node p = head;for(int i = levelCount - 1; i >= 0; --i){while(p.next[i] != null && p.next[i].data < value){p = p.next[i];}}if(p.next[0] != null && p.next[0].data == value){return p.next[0];    // 找到,则返回原始链表中的结点}else{return null;}}// 插入操作public void insert(int value){int level = randomLevel();Node newNode = new Node();newNode.data = value;newNode.maxLevel = level;   // 通过随机函数改变索引层的结点布置Node update[] = new Node[level];for(int i = 0; i < level; ++i){update[i] = head;}Node p = head;for(int i = level - 1; i >= 0; --i){while(p.next[i] != null && p.next[i].data < value){p = p.next[i];}update[i] = p;}for(int i = 0; i < level; ++i){newNode.next[i] = update[i].next[i];update[i].next[i] = newNode;}if(levelCount < level){levelCount = level;}}// 删除操作public void delete(int value){Node[] update = new Node[levelCount];Node p = head;for(int i = levelCount - 1; i >= 0; --i){while(p.next[i] != null && p.next[i].data < value){p = p.next[i];}update[i] = p;}if(p.next[0] != null && p.next[0].data == value){for(int i = levelCount - 1; i >= 0; --i){if(update[i].next[i] != null && update[i].next[i].data == value){update[i].next[i] = update[i].next[i].next[i];}}}}// 随机函数private int randomLevel(){int level = 1;for(int i = 1; i < MAX_LEVEL; ++i){if(random.nextInt() % 2 == 1){level++;}}return level;}// Node内部类public class Node{private int data = -1;private Node next[] = new Node[MAX_LEVEL];private int maxLevel = 0;// 重写toString方法@Overridepublic String toString(){StringBuilder builder = new StringBuilder();builder.append("{data:");builder.append(data);builder.append("; leves: ");builder.append(maxLevel);builder.append(" }");return builder.toString();}}// 显示跳表中的结点public void display(){Node p = head;while(p.next[0] != null){System.out.println(p.next[0] + " ");p = p.next[0];}System.out.println();}}

三、跳表的C++代码实现

https://blog.csdn.net/xp178171640/article/details/103014864

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

  1. 跳跃表的原理和实现以及应用

    目录 跳跃表的原理 跳跃表的实现步骤分析 代码实现 跳跃表的应用 跳跃表的原理 学过数据结构的都知道,在单链表中查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过2分的方式缩减时 ...

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

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

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

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

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

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

  5. Redis跳跃表(SkipList)

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

  6. Redis - 跳跃表

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

  7. Redis面试题-Redis跳跃表

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

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

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

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

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

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

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

最新文章

  1. ValueError: not enough values to unpack (expected 2, got 1)
  2. linux如何运行windows游戏,可运行在Linux下最好Windows软件和游戏
  3. 如何使用jQuery获取焦点元素?
  4. 基于python技术的自动化运维是干嘛的_如何理解Python与自动化运维的关系。?
  5. cms安装教程Linux,DoraCMS安装教程(linux)
  6. freecodecamp_我在1个月内完成了整个freeCodeCamp课程(并记录了所有内容)
  7. 如何更快获取想要的设计资源?
  8. android屏幕适配教程,Android屏幕适配方案,android屏幕适配
  9. Vue成大学核心课程
  10. 阿里云 Code Pipeline 体验
  11. matplotlib——饼状图pie函数
  12. 跨部门的bug的沟通
  13. 大一java题库及答案_2016最新java考试题库及答案
  14. SQL SERVER 数据库日志已满,清理数据库日志的方法
  15. 2.过滤函数-filter/filter-out
  16. 在SQL Server 2008上遇到了删除作业失败的问题。 547错误
  17. BugKu -- AWD --S1排位赛-4
  18. 开源工具,又是免费的神器 ,有点意思
  19. pipioj 1028
  20. java转义字符 \\

热门文章

  1. ubuntu 16.04安装网易云音乐,没声音?
  2. 微型计算机主频一般为,【单选题】目前使用的微型计算机的主频一般为________。 A. 2.6GHz B. 256MHz C. 2.3THz D. 900Hz...
  3. HTML星星组成的平行四边形,用一个程序打印菱形,平行四边形星星图
  4. 饥荒服务器访问令牌文档,《饥荒》联机版构建专属服务器方法步骤图文详解
  5. 模式识别——第3章 判别函数法
  6. Java面试题大全(2021版)
  7. Hadoop详解(七)——Hive的原理和安装配置和UDF,flume的安装和配置以及简单使用,flume+hive+Hadoop进行日志处理
  8. Lecture06:市场出清问题的鲁棒方法
  9. 2020国内可用的android镜像网站
  10. 照片识别年龄 php,用OpenCV和深度学习进行年龄识别