最近开始看Redis设计原理,碰到一个从未遇见的数据结构:跳跃表(skiplist)。于是花时间学习了跳表的原理,并用java对其实现。

介绍

跳跃表是一种有序数据结构,它通过每个结点中维持多个指向其它结点的指针,从而达到快速访问结点的目的。

我们平时熟知的链表,查找效率为O(N)。跳表在链表的基础上,每个结点中维护了很多指向其它结点的指针,大大缩短时间复杂度。可以实现时间复杂度平均O(logN),最坏O(N)。后文会有具体的分析和计算。

一个跳跃表示意图:



由左至右依次是,跳跃表结构结点(存储跳表信息)、头结点、连续的跳表结点。

最外层的跳表字段结构如下所示:

  1. public class SkipList<T extends Comparable<? super T>> {
  2. //首尾结点的指针
  3. private SkipListNode<T> header;
  4. private SkipListNode<T> tail;
  5. //记录跳表中结点数量
  6. private long length;
  7. //最大结点的层数
  8. private int level;
  9. //...
  10. }

跳表节点

跳表节点记为SkipListNode,内部字段结构如下:

  1. class SkipListNode <T> {
  2. //索引层
  3. private SkipListLevel[] level;
  4. //后退指针
  5. private SkipListNode<T> backword;
  6. //分值
  7. private double score;
  8. //成员对象
  9. private T obj;
  10. //......
  11. }
  • 索引层数组:多个索引层组成的数组,每个元素包含一个指向其它节点的指针。通过这些指针的访问来加快查找速度。
  • 后退指针:指向前一个节点;
  • 分值:是一个浮点数,跳表中所有节点都按照分值从小到大来排序;
  • 成员对象:即指向具体的数据对象。

索引层

索引层SkipListLevel的结构如下:

  1. class SkipListLevel{
  2. //前进指针
  3. private SkipListNode forward;
  4. //跨度
  5. private int span;
  6. //......
  7. }
  • 前进指针:指向后续节点;
  • 跨度:与指向的节点之间的距离。譬如,相邻节点距离就是1。

到这里,我们对跳表的基本结构有了一个清晰的认识。

理想的跳表

这里想先讲讲理想状态的跳表,不然无法理解实际跳表为什么可以缩减时间复杂度。

跳表节点间的关联方式:(索引层中的前向指针)第一层逐个链接,第二层每隔t个节点进行链接,第三层每隔2*t个节点进行链接,不断迭代。这里取t=2,画出每个节点的索引层之间的关联关系,得到如下图形式的链式结构:

有点像完全二叉树的结构。因此很容易理解:节点总数为N时,层最大高度为1+logN。例如图中有8个节点,最大层高为4。

搜索规则:从头结点的索引层的末端开始向下遍历。如果第K层的下一节点小于target,则移到该节点;若不小于,则下移到第K-1层。

按照此搜索规则,假设需要查找的target为7a,则搜索路径为0d--8d--0c--4c--4b--6b--6a--7a,如下图所示:

上述过程中,分别在8d、4c、6b、7a处进行比较。可见每一层都比较了一次,所以比较次数等于层数,为logN+1。所以时间复杂度为O(logN)。

如果实际的跳表按照这种形式进行设计,每次插入节点时,需要对很多结点的索引层进行调整,节点的插入删除将成为极其复杂的工作。因此,实际的跳表使用一种基于概率统计的算法,简化插入删除带来的调整工作,同时也能得到O(logN)的时间复杂度。

实际的跳表

每当需要新增一个节点时,需要考虑如何确定该节点的索引层层数,即SkipListLevel[]数组的长度。

如何确定“层”的高度?

在redis中,每次创建一个节点,都会根据幂次定律随机生成一个介于1和32之间的值作为索引层的高度。问题是,这个随机的过程如何设计?

我们观察理想状态跳表,可以发现,不算头节点总共8个节点,其中4个节点拥有2层索引,2个节点拥有3层索引,1个节点拥有4层索引。

可以近似看作满足这样的规律:节点索引层高度为 j 的概率为 1/2^j。因此每次生成新节点时,通过这样的概率计算可以得到索引层层数。代码如下所示:

  1. /**
  2. * 获取随机的层高度
  3. * @return
  4. */
  5. private int getRandomHeight() {
  6. Random random = new Random();
  7. int i = 1;
  8. for (; i < 32; ++i) {
  9. if (random.nextInt(2) == 0) {
  10. break;
  11. }
  12. }
  13. return i;
  14. }

注意:在redis中最大索引高度不超过32

为什么时间复杂度平均O(logN),最坏O(N)?

当节点数量足够多时,这种方式得到的跳跃表形态可以逼近理想的跳表的。很惭愧我不知道怎么证明,学过概率统计的同学一定很容易理解。它的时间复杂度就是近似为 O(logN) 。当然也有不理想的情况,当跳表中每一个节点随机得到的层高度都是 1 时,跳表就是一个普通双向链表,时间复杂度为 O(N) 。因此,时间复杂度平均O(logN)、最坏O(N),这种说法是比较严谨的。

节点的分值

这个分值 score 很容易与节点的“跨度”混淆。跨度其实就是节点在跳表中的排位,或者说序号。而分值是一个节点属性。节点按照分值大小由小到大排列,不同节点的分值可以相等。如果分值相等,对象较大的会排在后面(靠近表尾方向)。

在实际API应用中,需要以分值和obj成员对象作为target进行查询、插入等操作。

功能实现

跳跃表的初始化-代码实现

class SkipList:

  1. //构造方法初始化SkipList
  2. public SkipList() {
  3. SkipListNode<T> node = new SkipListNode<>(null);
  4. this.header = node;
  5. this.tail = node;
  6. this.length = 0;
  7. this.maxLevelHeight = 0;
  8. }

class SkipListNode:

  1. //初始化头结点
  2. SkipListNode(T obj){
  3. this.obj = obj;
  4. this.level = new SkipListLevel[32];
  5. initLevel(this.level,32);
  6. this.score = 0;
  7. }
  8. //根据"层高"和"分值",新建一个节点
  9. SkipListNode(T obj, int levelHeight,double score){
  10. this.obj = obj;
  11. this.level = new SkipListLevel[levelHeight];
  12. initLevel(this.level,levelHeight);
  13. this.score = score;
  14. }
  15. private void initLevel(SkipListLevel[] level, int height){
  16. for(int i=0;i<height;++i){
  17. level[i] = new SkipListLevel();
  18. }
  19. }

跳跃表的插入-代码实现

流程如下:

  • 按照幂次定律获取随机数,作为索引层的高度levelHeight,实例化新节点target;
  • 设置一个SkipListNode类型的数组,update[](记录所有需要进行调整的前置位节点,包括需要调整forword、或者只需要修改span值的节点),update[]的大小为max(levelHeight,maxLevelHeight);
  • 设置int数组rank[],记录update[]数组中各个对应节点的排位
  • 遍历 update[] 进行插入和更新操作;根据update[]获取插入位置节点,进行插入;根据rank[]来辅助更新跨度值span。

实际代码比上述流程要复杂很多,levelHeight与maxLevelHeight的大小关系不能确定,根据不同的情况要对update[]进行不同的处理。

跳跃表插入的代码如下所示:

注意:是依据score大小和obj的大小来决定插入顺序

  1. public SkipListNode slInsert(double score, T obj) {
  2. int levelHeight = getRandomHeight();
  3. SkipListNode<T> target = new SkipListNode<>(obj, levelHeight, score);
  4. // update[i] 记录所有需要进行调整的前置位节点
  5. SkipListNode[] update = new SkipListNode[Math.max(levelHeight, maxLevel)];
  6. int[] rank = new int[update.length];//记录每一个update节点的排位
  7. int i = update.length - 1;
  8. if (levelHeight > maxLevel) {
  9. for (; i >= maxLevel; --i) {
  10. update[i] = header;
  11. rank[i] = 0;
  12. }
  13. maxLevel = levelHeight;
  14. }
  15. for (; i >= 0; --i) {
  16. SkipListNode<T> node = header;
  17. SkipListNode<T> next = node.getLevel()[i].getForward();
  18. rank[i] = 0;
  19. //遍历得到与target最接近的节点(左侧)
  20. while (next != null && (score > next.getScore() || score == next.getScore() && next.getObj().compareTo(obj) < 0)) {
  21. rank[i] += node.getLevel()[i].getSpan();
  22. node = next;
  23. next = node.getLevel()[i].getForward();
  24. }
  25. update[i] = node;
  26. }
  27. //当maxLevel>levelHeight,前面部分节点的span值加1,因为该节点与forword指向节点之间将要 多出来一个新节点
  28. for (i = update.length - 1; i >= levelHeight; --i) {
  29. int span = update[i].getLevel()[i].getSpan();
  30. update[i].getLevel()[i].setSpan(++span);
  31. }
  32. //遍历 update[] 进行插入和更新操作
  33. for (; i >= 0; --i) {
  34. SkipListLevel pre = update[i].getLevel()[i];
  35. //将target节点插入update[i]和temp之间
  36. SkipListNode<T> temp = pre.getForward();
  37. int span = pre.getSpan();
  38. pre.setForward(target);
  39. pre.setSpan(rank[0] + 1 - rank[i]);
  40. target.getLevel()[i].setSpan(span > 0 ? (span - rank[0] + rank[i]) : 0);
  41. target.getLevel()[i].setForward(temp);
  42. //设置后退指针
  43. if (temp == null) {
  44. target.setBackword(header);
  45. } else {
  46. target.setBackword(temp.getBackword());
  47. temp.setBackword(target);
  48. }
  49. }
  50. if (tail.getLevel()[0].getForward() != null) {
  51. tail = target;
  52. }
  53. length++;
  54. return target;
  55. }

跳跃表的节点删除-代码实现

根据分值和成员对象来删除跳表中对应节点

  1. /**
  2. * 删除节点
  3. * @param obj
  4. * @return 删除的节点(若节点不存在则返回null)
  5. */
  6. public SkipListNode zslDelete(double score, T obj) {
  7. SkipListNode[] update = new SkipListNode[maxLevelHeight];
  8. SkipListNode<T> node = header;
  9. for (int i = maxLevelHeight - 1; i >= 0; --i) {
  10. SkipListNode<T> next = node.getLevel()[i].getForward();
  11. //遍历得到与target最接近的节点
  12. while (next != null && (score > next.getScore() || score == next.getScore() && next.getObj().compareTo(obj) < 0)) {
  13. node = next;
  14. next = node.getLevel()[i].getForward();
  15. }
  16. update[i] = node;
  17. }
  18. //待删除的目标节点
  19. SkipListNode<T> target = update[0].getLevel()[0].getForward();
  20. if(target==null) return null;
  21. for (int i = maxLevelHeight - 1; i >= 0; --i) {
  22. SkipListLevel current = update[i].getLevel()[i];
  23. SkipListNode<T> next = current.getForward();
  24. if (next == null) continue;
  25. if (next != target) {
  26. current.modifySpan(-1);
  27. continue;
  28. }
  29. current.setForward(target.getLevel()[i].getForward());
  30. if(current.getForward()!=null)
  31. current.modifySpan(target.getLevel()[i].getSpan() - 1);
  32. else
  33. current.setSpan(0);
  34. }
  35. length--;
  36. while(header.getLevel()[maxLevelHeight-1].getSpan()==0){
  37. maxLevelHeight--;
  38. }
  39. return target;
  40. }

跳跃表的节点查询-代码实现

  1. 根据分值范围 fromScore~toScore,返回第一个符合范围的节点
  • 参数 node 是开始查询的位置,调用时传入header , 递归过程会发生变化;
  • k 是当前层数,从最高层开始递归遍历;
  1. public SkipListNode<T> zslFirstInRange(double fromScore, double toScore, SkipListNode<T> node, int k) {
  2. if (!zslIsInRange(fromScore, toScore)) {
  3. return null;
  4. }
  5. SkipListNode<T> next = node.getLevel()[k].getForward();
  6. if (next == null || next.getScore() >= fromScore) {
  7. if (k == 0) return next != null && next.getScore() > toScore ? null : next;
  8. return zslFirstInRange(fromScore, toScore, node, k - 1);
  9. }
  10. return zslFirstInRange(fromScore, toScore, next, k);
  11. }
  1. 根据分值范围,返回最后一个符合范围的节点
  1. public SkipListNode<T> zslLastInRange(double fromScore, double toScore, SkipListNode<T> node, int k) {
  2. if (!zslIsInRange(fromScore, toScore)) {
  3. return null;
  4. }
  5. SkipListNode<T> next = node.getLevel()[k].getForward();
  6. if (next == null || next.getScore() > toScore) {
  7. if (k == 0) return next != null && next.getScore() < fromScore ? null : node;
  8. return zslLastInRange(fromScore, toScore, node, k - 1);
  9. }
  10. return zslLastInRange(fromScore, toScore, next, k);
  11. }

本篇博客介绍了跳跃表基本原理,并使用java完成了基本数据结构的封装,实现了节点“插入”、“删除”、“搜索”等核心功能的代码实现。

【Redis】跳跃表原理分析与基本代码实现(java)的更多相关文章

  1. 跳跃表-原理及Java实现

    跳跃表-原理及Java实现 引言: 上周现场面试阿里巴巴研发工程师终面,被问到如何让链表的元素查询接近线性时间.笔者苦思良久,缴械投降.面试官告知回去可以看一下跳跃表,遂出此文. 跳跃表的引入 我们知 ...

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

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

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

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

  4. 《闲扯Redis十》Redis 跳跃表的结构实现

    一.前言 Redis 提供了5种数据类型:String(字符串).Hash(哈希).List(列表).Set(集合).Zset(有序集合),理解每种数据类型的特点对于redis的开发和运维非常重要. ...

  5. Redis sentinel & cluster 原理分析

    1. Redis集群实现分析 1.1  sentinel 1.   功能 Sentinel实现如下功能: (1)monitoring--redis实例是否正常运行. (2)notification-- ...

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

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

  7. redis跳跃表

    最近在阅读redis设计与实现,关于redis数据结构zset的一种底层实现跳跃表一直没有太理解,所以在搜了一下资料,终于搞懂了它的设计思路,记录一下. 参考链接:https://mp.weixin. ...

  8. 基于跳跃表的 ConcurrentSkipListMap 内部实现(Java 8)

    我们知道 HashMap 是一种键值对形式的数据存储容器,但是它有一个缺点是,元素内部无序.由于它内部根据键的 hash 值取模表容量来得到元素的存储位置,所以整体上说 HashMap 是无序的一种容 ...

  9. 数据结构HashMap哈希表原理分析

    先看看定义:“散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度. 哈希 ...

随机推荐

  1. 听说你想要部署 Octopress?满足你

    Octopress 是一个面向开发者的博客系统,广受程序员的喜爱.既然大家有需求,那么 Octopress 也要安排上~ 云开发(CloudBase)是一款云端一体化的产品方案 ,采用 serverl ...

  2. [译]HAL-超文本应用语言

    [译]HAL-超文本应用语言 精益超媒体类型 总结 HAL 是一种简单的格式,它提供了一种一致且简便的方法在 API 的资源之间进行超链接. 采用 HAL 将使您的 API 易于探索,并且其文档很容易 ...

  3. Centos7 编译安装 Libmcrypt 库

    0x00 先下载 libmcrypt 库源码 libmcrypt-2.5.8.tar.gz 或者去这里 libmcrypt 下载你需要的版本. 0x01 将下载的源码解压到文件夹 tar -zxvf ...

  4. 事务的传播属性及隔离级别 Spring

    事务的传播属性(Propagation) REQUIRED ,这个是默认的属性 Support a current transaction, create a new one if none exis ...

  5. Pormetheus(一)

    (1)Prometheus由来普罗米修斯的灵感来自于谷歌的Borgmon.它最初是由马特·t·普劳德(Matt T. Proud)作为一个研究项目开发的,普劳德曾是谷歌(google)的一名雇员.在普 ...

  6. 基于my-DAQ的温室迷你温室设计

    这是一个小项目,采用NI的my-DAQ做数据采集,需要采集的数据有温度(LM35),气体(MQ2),需要控制的设备有风扇.加热棒,另外还有光照亮度调节. 一.数据采集 1.LM35 LM35是模拟输出 ...

  7. Docker搭建Nessus pro笔记

    0x01 准备Docker环境 拉取镜像: docker pull ubuntu 创建容器: docker run -p 9922:22 -p 8834:8834 --name nessus -it ...

  8. Daily Scrum 1/11/2016

    Zhaoyang & Minlong: Took and edited the video which introduced our APP. Yandong: Summarized bugs ...

  9. 漫画:工作这么多年,你居然不知道 Maven 中 Optional 和 Exclusions 的区别?

    欢迎关注笔者的公众号: 小哈学Java, 专注于推送 Java 领域优质干货文章!! Maven 依赖排除(Exclusions) 因为 Maven 构建项目具有依赖可传递的特性,当你在 pom.xm ...

  10. 感受python之美,python简单易懂的小例子

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 1 简洁之美 通过一行代码,体会Python语言简洁之美 2 Python ...