机制

链表中查询的效率的复杂度是O(n), 有没有办法提升这个查询复杂度呢? 最简单的想法就是在原始的链表上构建多层索引.

在level 1(最底层为0), 每2位插入一个索引, 查询复杂度便是 O(N/2 + 1)

在level 2, 每四位插入一个索引, 查询复杂度便是 O(N/4 + 2)

那么推广开来, 如果我们有这样的一组链表, 在level i, 每间隔第 元素就有一个链接 在level 1, 每一个节点之间有一个链接 在level 2, 每两个节点之间有一个链接 在level 3, 每四个节点之间有一个链接 在level 4, 每八个节点之间有一个链接. 这样我们可以看到, 每向上一层, 数据量就减少了 1/2, 所以查询的过程就近似变成了2分查找, 查询性能就变成了稳定的O(logN). 索引的存储空间为 其中

两式相减得到 所以 所以 因此这样的数据结构总的空间复杂度为 2n - 2.

但是这样的数据结构存在一个问题, 严格要求每一层按照 的间隔链接很难在持续插入的过程中维护.

当插入一个新元素的时候, 需要为他分配一个新的节点, 此时我们需要决定该节点是多少阶的. 通过观察 Figure 10.60 可以发现, 有1/2的元素是1阶的, 有 1/4 的元素是2阶的, 所以大约 的节点是第 i 阶的. 那么根据这个性质, 我们就可以通过随机统计的方式来判断新元素应该插入的阶数. 最容易得做法就是抛一枚硬币直到正面出现并把抛硬币的总次数用作该节点的阶数.

连续抛i次才出现正面的概率是 , 而的节点是属于第 i 阶的.

通常的计算阶数的方法

/**
* 这个函数返回的是levelCount, 最小为1, 表示不构建索引.
*
* <p>
* <li>1/2 概率返回1 表示不用构建索引
* <li>1/2 概率返回2 表示构建一级索引
* <li>1/4 概率返回3 表示构建二级索引
* <li>1/8 概率返回4 表示构建三级索引
*
* @return
*/
private int randomLevel() {
   int level = 1;
   while (Math.random() < SKIPLIST_P && level < MAX_LEVEL) {
       level++;
  }
   return level;
}

通常p取值为 1/2 或者 1/4 表示两层之间的数据分布概率, Math.random()随机返回一个0-1之间的数, 这个就是模拟不断抛硬币的过程, height 为累计的抛硬币的次数.

因此跳表的实现, 是利用了随机化算法来计算新插入节点的阶数, 而这个阶数的数学期望能保证每一层数据能随机化的递减 1/2, 通过这样来保证最终插入和查找复杂度的期望都为 O(logN).

相比于红黑树 优势

  1. 插入 查找 删除的复杂度和红黑树一样

  2. 区间查找的效率更高

  3. 代码实现更简单

  4. 并且可以通过插入节点阶数生成的策略来平衡时间和空间复杂度的不同需求. 比如我们可以让每一层的数据为下一层的1/3. 这种情况下索引存储量 为 n/3 + n/9 + n/27 + ... 2 = n / 2 空间占用就缩小一半.

劣势

  1. 跳表的内存占用相比会大一点, 不过因为索引其实可以只存储key和指针, 实际的空间开销往往没有那么大

实现

查找

例如查找16

  1. 从head处开始查找, 同一层中遍历向前直到next节点为空或者next节点的value大于当前值

  2. 跳转到level + 1的索引处, 继续上述流程

  3. 最后一定会到达level 0. 指针非空则找到了相应的value

插入

  1. 通过随机函数生成此value插入的高度为2.

  2. 同查找一样的遍历流程, 找到比6大的那个位置. 在此过程中, 每一次向下迭代需要记录转折点如图中的1和4.这两个节点会作为6的前置节点

  3. 将新value的next指向原来前置节点的next, 将原来前置节点的next指向新的节点

  4. 同时新生成的level是可能高于原始高度的

删除

删除相比插入更简单, 在遍历每一层的时候不需要单独去记录前置节点了. 虽然以下实现是记录了前置节点后统一更新的, 但我感觉是没有必要的, delete可以改成 https://github.com/wangzheng0822/algo/blob/master/java/17_skiplist/SkipList.java

public void delete2(int value) {
   Node p = head;
   // 找到前置节点
   for (int i = levelCount - 1; i >= 0; i--) {
       while (p.forwards[i] != null && p.forwards[i].value < value) {
           p = p.forwards[i];
      }
       if (p.forwards[i] != null && p.forwards[i].value == value) {
           p.forwards[i] = p.forwards[i].forwards[i];
      }
  }

   // head 指向为空的节点都剔除.
   while (levelCount > 1 && head.forwards[levelCount] == null) {
       levelCount--;
  }
}

工业实现

redis的sorted set hbase中内存的有序集合 java ConcurrentSkipListSet ConcurrentSkipListMap

参考

<数据结构与算法分析 Java描述> 10.4.2

<HBase原理与实践> 2.1 跳跃表

王争数据结构与算法之美#17

Skip List--跳表(全网最详细的跳表文章没有之一)

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

SkipList原理与实现的更多相关文章

  1. 跳表(SkipList)原理篇

    1.什么是跳表? 维基百科:跳表是一种数据结构.它使得包含n个元素的有序序列的查找和插入操作的平均时间复杂度都是 O(logn),优于数组的 O(n)复杂度.快速的查询效果是通过维护一个多层次的链表实 ...

  2. 浅析SkipList跳跃表原理及代码实现

    本文将总结一种数据结构:跳跃表.前半部分跳跃表性质和操作的介绍直接摘自<让算法的效率跳起来--浅谈“跳跃表”的相关操作及其应用>上海市华东师范大学第二附属中学 魏冉.之后将附上跳跃表的源代 ...

  3. 【转】浅析SkipList跳跃表原理及代码实现

    SkipList在Leveldb以及lucence中都广为使用,是比较高效的数据结构.由于它的代码以及原理实现的简单性,更为人们所接受.首先看看SkipList的定义,为什么叫跳跃表? "S ...

  4. 算法: skiplist 跳跃表代码实现和原理

    SkipList在leveldb以及lucence中都广为使用,是比较高效的数据结构.由于它的代码以及原理实现的简单性,更为人们所接受. 所有操作均从上向下逐层查找,越上层一次next操作跨度越大.其 ...

  5. 跳跃表 SkipList【数据结构】原理及实现

    为什么选择跳表 目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等. 想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树出来吗? ...

  6. skiplist(跳表)的原理及JAVA实现

    前记 最近在看Redis,之间就尝试用sortedSet用在实现排行榜的项目,那么sortedSet底层是什么结构呢? "Redis sorted set的内部使用HashMap和跳跃表(S ...

  7. JAVA SkipList 跳表 的原理和使用例子

    跳跃表是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好. 关于跳跃表的具体介绍可以参考MIT的公开课:跳跃表 跳跃表的应 ...

  8. skip-list(跳表)原理及C++代码实现

    跳表是一个很有意思的数据结构,它实现简单,但是性能又可以和平衡二叉搜索树差不多. 据MIT公开课上教授的讲解,它的想法和纽约地铁有异曲同工之妙,简而言之就是不断地增加“快线”,从而降低时间复杂度. 当 ...

  9. 跳跃表Skip List的原理和实现

    >>二分查找和AVL树查找 二分查找要求元素可以随机访问,所以决定了需要把元素存储在连续内存.这样查找确实很快,但是插入和删除元素的时候,为了保证元素的有序性,就需要大量的移动元素了.如果 ...

  10. skiplist 跳表(2)-----细心学习

    快速了解skiplist请看:skiplist 跳表(1) http://blog.sina.com.cn/s/blog_693f08470101n2lv.html 本周我要介绍的数据结构,是我非常非 ...

随机推荐

  1. js-函数记忆

    函数记忆: 指将上次的(计算结果)缓存起来,当下次调用时,如果遇到相同的(参数),就直接返回(缓存中的数据). 实现原理:将参数和对应的结果保存在对象中,再次调用时,判断对象 key 是否存在,存在返 ...

  2. Array.prototype.at。Arrat和 String 中的 at 方法

    一篇有关新 js 特性 at 方法的思考 入参只能是number 类型,允许入参有小数(按照 chrome DevTools Console 测试确实可以带小数) 有返回值,如果对应下标在实例中存在, ...

  3. 【kafka】-分区-消费端负载均衡

    一.为什么kafka要做分区? 因为当一台机器有可能扛不住(类比:就像redis集群中的redis-cluster一样,一个master抗不住写,那么就多个master去抗写),把一个队列的单一mas ...

  4. RTSP Server(LIVE555)源码分析(五)-PLAY信令

    主要分析RTSPServer::RTSPClientSession针对客户端PLAY事件处理 一. PLAY信令,handleCmd_withinSession源码解析 1)步骤1.03,当RTSP客 ...

  5. sklearn数据预处理中fit(),transform()与fit_transform()的区别

    1 概述 注意这是数据预处理中的方法: Fit(): Method calculates the parameters μ and σ and saves them as internal objec ...

  6. 关于java中的super

    首当其冲先说一下super的用途和含义.他是用于调用一些被重写的方法. 这里还可以复习一下子这个重写:重写是把新的方法放在被重写的方法前面.在被重写的子类中,优先调用重写后的方法.但是如果想要调用原本 ...

  7. Windows亚克力特效代码实现(Dev c++可以编译通过)

    #include <windows.h> #include <dwmapi.h> // 定义一个枚举类型,表示不同的窗口组合状态 enum AccentState { ACCE ...

  8. 2022-11-19:第二高的薪水。表结构和数据的sql语句如下,输出200,因为200是第二大的。请问sql语句如何写? DROP TABLE IF EXISTS `employee`; CREAT

    2022-11-19:第二高的薪水.表结构和数据的sql语句如下,输出200,因为200是第二大的.请问sql语句如何写? DROP TABLE IF EXISTS `employee`; CREAT ...

  9. 2021-09-25:给定一个字符串数组,将字母异位词组合在一起。可以按任意顺序返回结果列表。字母异位词指字母相同,但排列不同的字符串。示例 1:输入: strs = [“eat“, “tea“, “

    2021-09-25:给定一个字符串数组,将字母异位词组合在一起.可以按任意顺序返回结果列表.字母异位词指字母相同,但排列不同的字符串.示例 1:输入: strs = ["eat" ...

  10. 解决pytest+allure报告中文乱码

    在conftest文件里添加钩子函数: def pytest_collection_modifyitems(items): """ 测试用例收集完成时,将收集到的item ...