数据结构之跳表SkipList、ConcurrentSkipListMap

概述

SkipList,跳表,跳跃表,在LevelDB和Lucene中都广为使用。跳表被广泛地运用到各种缓存实现当中,跳跃表使用概率均衡技术而不是使用强制性均衡,因此对于插入和删除结点比传统上的平衡树算法更为简洁高效。

Skip lists are data structures that use probabilistic balancing rather than strictly enforced balancing. As a result, the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.

传统意义的单链表是一个线性结构,在一个有序链表里,查询、插入、删除一个结点的算法时间复杂度都是O(n)

跳表示意图
在这里插入图片描述
跳表是在链表之上加上多层索引构成的:

  • 表头(head):负责维护跳跃表的结点指针
  • 跳跃表结点:保存着元素值,以及多个层
  • 层:保存着指向其他元素的指针,这个层数是随机的

每一个结点不单单只包含指向下一个结点的指针,可能包含很多个指向后续结点的指针,这样就可以跳过一些不必要的结点,从而加快查找、删除等操作。对于一个链表内每一个结点包含多少个指向后续元素的指针,这个过程是通过一个随机函数生成器得到,这样子就构成一个跳跃表。通过随机生成一个结点中指向后续结点的指针数目。所有操作都以对数随机化的时间进行。

优点,跟红黑树、AVL等平衡树一样,做到比较稳定地插入、查询与删除,支持顺序操作。插入查询删除的算法时间复杂度理论值为O(logn),最坏情况下O(n)

跳表性质:

  1. 由很多层结构组成,每一层都是一个有序的链表
  2. 最底层(Level 1)的链表包含所有元素,最底层数据结构退化为一个普通的有序链表
  3. 如果一个元素出现在Level i的链表中,则它在Level i之下的链表也都会出现
  4. 每个结点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
  5. 搜索过程是逐层进行,不能越两级搜索
  6. 在每一层中,-1和1两个元素都出现(分别表示INT_MIN和INT_MAX)
  7. Top指针指向最高层的第一个元素
  8. 跳表是一种以牺牲更多的存储空间换取查找速度,即空间换时间

Skip List构造步骤

  • 给定一个有序的链表
  • 选择链表中最大和最小的元素,然后从其他元素中按照一定算法随机选出一些元素,将这些元素组成有序链表。这个新的链表称为一层,原链表称为其下一层
  • 为刚选出的每个元素添加一个指针域,这个指针指向下一层中值同自己相等的元素。Top指针指向该层首元素
  • 重复2、3步,直到不再能选择出除最大最小元素以外的元素

跳表的插入
先确定该元素要占据的层数K(随机),然后在Level 1…Level K各个层的链表都插入元素。K大于链表层数,则需要添加新层。跳表的插入需要三个步骤:

  • 需要查找到在每层待插入位置
  • 随机产生一个层数
  • 从高层至下插入,插入时算法和普通链表的插入完全相同

删除结点操作和插入差不多,找到每层需要删除的位置,删除时和操作普通链表完全一样。如果该结点的level是最大的,则需要更新跳表的level。

理论

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

跳表 vs B+树

相同:都是用空间来换取时间,用额外的空间来保存链表或者目录页,来提升查询性能

区别:

  • 层高:B+树三层就能支持千万级别的数据,但跳表存储相同的数据量需要更高的层级。InnoDB索引用B+树而不用跳表的原因,InnoDB强依赖于磁盘IO,层级越高,IO次数也就越多;Redis的zset是用的跳表,因为Redis是基于内存操作,没有磁盘IO概念,跳表更简单
  • 操作数据:跳表比B+树快,B+树在数据操作时需要维护B+树,所以会有树的分裂与合并;跳表是随机一个层次,实现相对简单

跳表 vs 平衡树

类似于平衡树,用来快速查找。区别是平衡树的插入和删除可能需要一次需要全局调整,而跳表只需对整个数据结构进行局部操作。所以在高并发下,需要对平衡树进行全局锁,而跳表只需部分加锁。
本质是维护多个分层的链表,最底层的链表维护表内所有元素,每上面一层是下面一层的子集。表内所有元素的链表都是排序的。查找时,先从最顶层开始查找,当发现查找元素大于链表中取值就进入下一行,用空间换时间。

插入
插入时,先查询,然后从最底层开始,插入被插入的元素。然后看看从下而上,是否需要逐层插入。可是到底要不要插入上一层呢?想每层的跳跃都非常高效,越是平衡就越好(第一层1级跳,第二层2级跳,第3层4级跳,第4层8级跳)。但是用算法实现起来,确实非常地复杂的,并且要严格地按照2地指数次幂,我们还要对原有地结构进行调整。所以跳表的思路是抛硬币,听天由命,产生一个随机数,50%概率再向上扩展,否则就结束。这样子,每一个元素能够有X层的概率为0.5^(X-1)次方。反过来,第X层有多少个元素的数学期望大家也可以算一下。

删除
同插入一样,删除也是先查找,查找到之后,再从下往上逐个删除。

跳表 vs 红黑树

为什么Redis要使用跳表而不使用红黑树呢?跳表相对于红黑树的优点:

  1. 代码相对简单
  2. 如果要查询一个区间里面的值,用平衡树在实现和理解上可能会麻烦些,虽然可以实现
  3. 删除一段区间,用平衡二叉树则涉及到树的平衡问题而相当困难,跳表没有这个问题

应用

JDK

在JDK里也有跳表的实现,如ConcurrentSkipListMap和ConcurrentSkipListSet。

ConcurrentSkipListMap

JDK22版本下,ConcurrentSkipListMap属性如下:

/*** 指定全局比较器,用于比较两个元素的关键字大小并进行排序,如果在构造器中没有显式传入指定比较器,则默认对key按照自然顺序排序*/
@SuppressWarnings("serial") // Conditionally serializable
final Comparator<? super K> comparator;
/** 最上层索引链表的头结点,延迟加载(包括下面几个属性),即在使用时才会初始化 */
private transient Index<K,V> head;
/** 元素计数器 */
private transient LongAdder adder;
/** 保存key的set集合 */
private transient KeySet<K,V> keySet;
/** 保存value的集合 */
private transient Values<K,V> values;
/**  保存key-value的EntrySet集合 */
private transient EntrySet<K,V> entrySet;
/** 保存key-value结点的逆序排序的Map集合 */
private transient SubMap<K,V> descendingMap;

内部类有Node、Index、,省略构造方法(下同):

static final class Node<K,V> {final K key; // currently, never detachedV val;Node<K,V> next;
}

Node表示链表结点,用于保存数据,包括三个属性:key-键、volatile的value-值、volatile的next-后继结点。

static final class Index<K,V> {final Node<K,V> node;  // currently, never detachedfinal Index<K,V> down;Index<K,V> right;
}

Index表示基于链表的索引结点,用于保存索引关系和索引相关操作。包括三个属性:指向的链表数据结点node,指向下一层索引链表的索引结点down,指向同一层索引链表的当前结点的后继索引结点right。

抽象内部类Iter,见名知意,用于迭代:

abstract class Iter<T> implements Iterator<T> {/** next()方法返回的最后一个节点 */Node<K,V> lastReturned;/** next()方法返回的下一个节点 */Node<K,V> next;/** 缓存下一个值字段以保持弱一致性 */V nextValue;/** 初始化整个范围的升序迭代器 */Iter() {advance(baseHead());}public final boolean hasNext() {return next != null;}/** Advances next to higher entry. */final void advance(Node<K,V> b) {Node<K,V> n = null;V v = null;if ((lastReturned = b) != null) {while ((n = b.next) != null && (v = n.val) == null)b = n;}nextValue = v;next = n;}public final void remove() {Node<K,V> n; K k;if ((n = lastReturned) == null || (k = n.key) == null)throw new IllegalStateException();// It would not be worth all of the overhead to directly// unlink from here. Using remove is fast enough.ConcurrentSkipListMap.this.remove(k);lastReturned = null;}
}

基于Iter抽象类,有3个实现类分别用于Key、Value、Key和Value的遍历,即KeyIterator、ValueIterator、EntryIterator这3个内部类。

核心方法

  • put:插入结点,调用doPut方法,使用到VarHandle的acquireFence、compareAndSet两个方法,以及ThreadLocalRandom.nextSecondarySeed()方法,源码还是挺复杂的
  • remove:删除结点,有多个重载方法,最后调用doRemove方法
  • get:查找结点,调用doGet方法,也是使用到VarHandle的acquireFence、compareAndSet两个方法,和双层循环。基于doGet方法,还提供有用的getOrDefault方法
  • replace:有两个方法
    • public V replace(K key, V value),如果指定key对应的结点存在,那么使用指定value替换旧value。返回以前与指定键关联的值;如果没有该键的映射关系,则返回null
    • public boolean replace(K key, V oldValue, V newValue):如果指定key-value对应的结点存在,则使用newValue替换oldValue。如果该值被替换成功,则返回true。
  • contains:来自Map的方法,用于判断是否包括某个Key或Value,包括:
    • containsKey:直接使用doGet来判断即可
    • containsValue:通过一层循环来遍历
  • size:判断大小
  • isEmpty:判断是否为空,判断头结点是否为空即可:return findFirst() == null;
  • clear:清空

doRemove方法使用两层嵌套循环,默认情况下使用break关键词只会跳出一层循环体。为了实现一次性跳出两层(多层也可以)循环,在最外层定义一个outer:,注意冒号不能省略,然后使用break outer实现:

final V doRemove(Object key, Object value) {if (key == null)throw new NullPointerException();Comparator<? super K> cmp = comparator;V result = null;Node<K,V> b;outer: while ((b = findPredecessor(key, cmp)) != null &&  result == null) {for (;;) {Node<K,V> n; K k; V v; int c;if ((n = b.next) == null)break outer;else if ((k = n.key) == null)break;else if ((v = n.val) == null)unlinkNode(b, n);else if ((c = cpr(cmp, key, k)) > 0)b = n;else if (c < 0)break outer;else if (value != null && !value.equals(v))break outer;else if (VAL.compareAndSet(n, v, null)) {result = v;unlinkNode(b, n);break; // loop to clean up}}}if (result != null) {tryReduceLevel();addCount(-1L);}return result;
}

另外outer标志字段可以使用其他非Java保留关键词都行,如flag

有2个参数的replace方法源码:

public V replace(K key, V value) {if (key == null || value == null)throw new NullPointerException();for (;;) {Node<K,V> n; V v;if ((n = findNode(key)) == null)return null;if ((v = n.val) != null && VAL.compareAndSet(n, v, value))return v;}
}

有3个参数的replace方法源码:

public boolean replace(K key, V oldValue, V newValue) {if (key == null || oldValue == null || newValue == null)throw new NullPointerException();for (;;) {Node<K,V> n; V v;if ((n = findNode(key)) == null)return false;if ((v = n.val) != null) {if (!oldValue.equals(v))return false;if (VAL.compareAndSet(n, v, newValue))return true;}}
}

用于判断Value是否存在的containsValue方法:

public boolean containsValue(Object value) {if (value == null)throw new NullPointerException();Node<K,V> b, n; V v;if ((b = baseHead()) != null) {while ((n = b.next) != null) {if ((v = n.val) != null && value.equals(v))return true;elseb = n;}}return false;
}

size方法最大为Integer.MAX_VALUE

public int size() {long c;return ((baseHead() == null) ? 0 : ((c = getAdderCount()) >= Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) c);
}

getAdderCount方法如下:

final long getAdderCount() {LongAdder a; long c;do {} while ((a = adder) == null && !ADDER.compareAndSet(this, null, a = new LongAdder()));return ((c = a.sum()) <= 0L) ? 0L : c; // ignore transient negatives
}

VarHandle

JDK 9引入的概念。TODO。

Kafka

Kafka的每个日志对象中使用ConcurrentSkipListMap来保存各个日志分段,每个日志分段的baseOffset作为key,这样可以根据指定偏移量来快速定位到消息所在的日志分段。

LevelDB

memtable用于存储在内存中还未落盘到sstable中的数据,这部分使用跳表做为底层的数据结构。

Lucene

占用内存小,且可调,但是对模糊查询支持不好。Lucene3.0之前使用的也是跳跃表结构,后换成FST,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。

基于lucene-core-9.10.0版本,可以看到两个抽象类MultiLevelSkipListReader和MultiLevelSkipListWriter。前面的分析讲过,普通的快表只能从最上层往下一层层搜索,不能越两级搜索,因为没有维护越级的指针。

以MultiLevelSkipListReader为例,看看其属性有哪些:

public abstract class MultiLevelSkipListReader implements Closeable {/** the maximum number of skip levels possible for this index */protected int maxNumberOfSkipLevels;/** number of levels in this skip list */protected int numberOfSkipLevels;private int docCount;/** skipStream for each level. */private IndexInput[] skipStream;/** The start pointer of each skip level. */private long[] skipPointer;/** skipInterval of each level. */private int[] skipInterval;/*** Number of docs skipped per level. It's possible for some values to overflow a signed int, but this has been accounted for.*/private int[] numSkipped;/** Doc id of current skip entry per level. */protected int[] skipDoc;/** Doc id of last read skip entry with docId &lt;= target. */private int lastDoc;/** Child pointer of current skip entry per level. */private long[] childPointer;/** childPointer of last read skip entry with docId &lt;= target. */private long lastChildPointer;private final int skipMultiplier;
}

TODO

Redis

zset数据结构,由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾结点、长度等),后者用于表示跳跃表结点

参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/47487.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AQS详解(详细图文)

目录 AQS详解1、AQS简介AbstractQueuedSynchronizer的继承结构和类属性AQS的静态内部类Node总结AQS的实现思想总结AQS的实现原理AQS和锁的关系 2、AQS的核心方法AQS管理共享资源的方式独占方式下&#xff0c;AQS获取资源的流程详解独占方式下&#xff0c;AQS释放资源的流程详解…

如何通过DBC文件看懂CAN通信矩阵

实现汽车CAN通信开发&#xff0c;必不可少要用到DBC文件和CAN通信矩阵。 CAN通信矩阵是指用于描述 CAN 网络中各个节点之间通信关系的表格或矩阵。它通常记录了每个节点能够发送和接收的消息标识符&#xff08;ID&#xff09;以及与其他节点之间的通信权限。 通信矩阵在 CAN 网…

利用Msfvenom获取WindowsShell

一、在kali主机上利用msfvenom生成windows端的安装程序(exe文件),程序名最好取一个大家经常安装的程序,如腾讯视频、爱奇艺等。 (1)由于生成的程序可能会被杀毒软件识别,我们比较一下使用单个编码器生成的程序与用两个编码器生成的程序,哪个更容易被识别。 利用单个编码…

SSE(Server Sent Event)实战(2)- Spring MVC 实现

一、服务端实现 使用 RestController 注解创建一个控制器类&#xff08;Controller&#xff09; 创建一个方法来创建一个客户端连接&#xff0c;它返回一个 SseEmitter&#xff0c;处理 GET 请求并产生&#xff08;produces&#xff09;文本/事件流 (text/event-stream) 创建…

如何使用Milvus Cloud进行稀疏向量搜索

如何使用Milvus Cloud进行向量搜索Milvus Cloud 是一款高度可扩展、性能出色的开源向量数据库。在最新的 2.4 版本中,Milvus Cloud 支持了稀疏和稠密向量(公测中)。本文将利用 Milvus Cloud 2.4 来存储数据集并执行向量搜索。 接下来,我们将演示如何利用 Milvus Cloud 在 M…

[GXYCTF2019]Ping Ping Ping1

打开靶机 结合题目名称&#xff0c;考虑是命令注入&#xff0c;试试ls 结果应该就在flag.php。尝试构造命令注入载荷。 cat flag.php 可以看到过滤了空格,用 $IFS$1替换空格 还过滤了flag&#xff0c;我们用字符拼接的方式看能否绕过,ag;cat$IFS$1fla$a.php。注意这里用分号间隔…

睡前故事—绿色科技的未来:可持续发展的梦幻故事

欢迎来到《Bedtime Stories Time》。这是一个我们倾听、放松、并逐渐入睡的播客。感谢你收听并支持我们&#xff0c;希望你能将这个播客作为你睡前例行活动的一部分。今晚我们将讲述绿色科技的未来&#xff1a;可持续发展的梦幻故事的故事。一个宁静的夜晚&#xff0c;希望你现…

0602STM32定时器输出比较

STM32定时器输出比较 PWM简介 主要用来输出PWM波形&#xff0c;PWM波形又是驱动电机的必要条件&#xff0c;所以如果想用STM32做一些有电机的项目&#xff0c;比如智能车&#xff0c;机器人等。那输出比较功能就要认真掌握 1.PWM驱动LED呼吸灯 2.PWM驱动舵机 3.PWM驱动直流电机…

搜维尔科技:【研究】触觉技术将在5年内以8种方式改变人们的世界

触觉技术在过去几年中发展迅猛&#xff0c;大大提高了反馈的精确度和真实度。其应用产生了真正的影响&#xff0c;数百家公司和企业都集成了触觉技术来增强培训和研究模拟。 虽然触觉技术主要用于 B2B 层面&#xff0c;但触觉技术可能会彻底改变我们的生活&#xff0c;尤其是通…

视频共享融合赋能平台LntonCVS视频监控业务平台技术方案详细介绍

LntonCVS国标视频综合管理平台是一款智慧物联应用平台&#xff0c;核心技术基于视频流媒体&#xff0c;采用分布式和负载均衡技术开发&#xff0c;提供广泛兼容、安全可靠、开放共享的视频综合服务。该平台功能丰富&#xff0c;包括视频直播、录像、回放、检索、云存储、告警上…

【数据结构】详解堆

一、堆的概念 堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵 完全二叉树的 数组对象。 堆是非线性数据结构&#xff0c;相当于一维数组&#xff0c;有两个直接后继。 如果有一个关键码的集合K { k₀&#xff0c;k₁&#xff0c;k₂ &#xff0…

数据结构(双向链表)

链表的分类 链表的结构⾮常多样&#xff0c;以下情况组合起来就有8种&#xff08;2 x 2 x 2&#xff09;链表结构&#xff1a; 虽然有这么多的链表的结构&#xff0c;但是我们实际中最常⽤还是两种结构&#xff1a;单链表和双向带头循环链表 1.⽆头单向⾮循环链表&#xff1a…

第十课:telnet(远程登入)

如何远程管理网络设备&#xff1f; 只要保证PC和路由器的ip是互通的&#xff0c;那么PC就可以远程管理路由器&#xff08;用telnet技术管理&#xff09;。 我们搭建一个下面这样的简单的拓扑图进行介绍 首先我们点击云&#xff0c;把云打开&#xff0c;点击增加 我们绑定vmn…

【面试题】Redo log和Undo log

Redo log 介绍Redo log之前我们需要了解一下&#xff0c;mysql数据操作的流程&#xff1a; 上述就是数据操作的流程图&#xff0c;可以发现sql语句并不是直接操作的磁盘而是通过操作内存&#xff0c;然后进行内存到磁盘的一个同步。这里我们必须要了解一些区域&#xff1a; 缓…

华为HCIP Datacom H12-821 卷42

42.填空题 如图所示&#xff0c;MSTP网络中SW1为总根&#xff0c;请将以下交换机与IST域根和主桥配对。 参考答案&#xff1a;主桥1468 既是IST域根又是主桥468 既不是又不是就是25 解析&#xff1a; 主桥1468 既是IST域根又是主桥468 既不是又不是就是25 43.填空题 网络有…

[日进斗金系列]用码上飞解决企微开发维修管理系统的需求

前言&#xff1a; 今天跟大家唠唠如何用小money生 大money的方法&#xff0c;首先我们需要准备一个工具。 这个工具叫码上飞CodeFlying&#xff0c;它是目前国内首发的L4级自动化智能软件开发平台。 它可以在短时间内&#xff0c;与AI进行几轮对话就能开发出一个可以解决实际…

WEB前端06-BOM对象

BOM浏览器对象模型 浏览器对象模型&#xff1a;将浏览器的各个组成部分封装成对象。是用于描述浏览器中对象与对象之间层次关系的模型&#xff0c;提供了独立于页面内容、并能够与浏览器窗口进行交互的对象结构。 组成部分 Window&#xff1a;浏览器窗口对象 Navigator&…

区块链资料

Quantstamp - Public Security Assessments smart-contract-sanctuary-bsc/contracts/mainnet at master tintinweb/smart-contract-sanctuary-bsc GitHub https://github.com/slowmist/Cryptocurrency-Security-Audit-Guide/blob/main/README_CN.md sFuzz: 高效自适应的智…

Spring3(代理模式 Spring1案例补充 Aop 面试题)

目录 一、代理模式 介绍 意图 主要解决的问题 使用场景 实现方式 关键代码 应用实例 优点 缺点 使用建议 注意事项 结构 什么是代理模式&#xff1f; 为什么要用代理模式&#xff1f; 有哪几种代理模式&#xff1f; 1. 静态代理 实现 2. 基于接口的动态代理…

《昇思25天学习打卡营第20天|GAN图像生成》

生成对抗网络&#xff08;GAN&#xff09;是一种深度学习模型&#xff0c;用于生成逼真的图像。在手写数字识别的任务中&#xff0c;GAN 可以用来生成与真实手写数字相似的图像&#xff0c;以增强模型的训练数据集。GAN 主要由两个部分组成&#xff1a;生成器&#xff08;Gener…