数据结构之跳表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://xiahunao.cn/news/3247813.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

2-37 基于matlab的IMU姿态解算

基于matlab的IMU姿态解算,姿态类型为四元数&#xff1b;角速度和线加速度的类型为三维向量。IMU全称是惯性导航系统&#xff0c;主要元件有陀螺仪、加速度计和磁力计。其中陀螺仪可以得到各个轴的加速度&#xff0c;而加速度计能得到x&#xff0c;y&#xff0c;z方向的加速度&a…

云计算数据中心(三)

目录 四、自动化管理&#xff08;一&#xff09;自动化管理的特征&#xff08;二&#xff09;自动化管理实现阶段&#xff08;三&#xff09;Facebook自动化管理 五、容灾备份&#xff08;一&#xff09;容灾系统的等级标准&#xff08;二&#xff09;容灾备份的关键技术&#…

NXP i.MX8系列平台开发讲解 - 3.19 Linux TTY子系统(二)

专栏文章目录传送门&#xff1a;返回专栏目录 Hi, 我是你们的老朋友&#xff0c;主要专注于嵌入式软件开发&#xff0c;有兴趣不要忘记点击关注【码思途远】 目录 1. Linux 串口驱动 1.1 Uart 驱动注册流程 1.2 uart 操作函数 1.3 line discipline 2. Linux tty应用层使用…

Windows安装部署MySQL8.0

1.版本及下载 1.版本介绍&#xff1a; Alpha 版&#xff1a;开发版&#xff0c;公司内部使用 Beta 版&#xff1a;完成开发后&#xff0c;用户体验版 RC 版&#xff1a;生产环境发布之前的一个小版本或称候选版 GA 版&#xff1a;正式发布版本&#xff08;咱们要用的&…

代码随想录算法训练营Day26 | 491.递增子序列 | 46.全排列 | 47.全排列 II | 332.重新安排行程 | 51.N皇后 | 37.解数独

今日任务 491.递增子序列 题目链接&#xff1a; https://leetcode.cn/problems/non-decreasing-subsequences/description/题目描述&#xff1a; Code class Solution { public:vector<vector<int>> findSubsequences(vector<int>& nums) {vector&l…

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

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

leetcode145. 二叉树的后序遍历,递归法+迭代法,全过程图解+步步解析,一点点教会你迭代法后序遍历

leetcode145. 二叉树的后序遍历&#xff0c;递归法迭代法 给你一棵二叉树的根节点 root &#xff0c;返回其节点值的 后序遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[3,2,1] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#…

vue、js截取视频任意一帧图片

html有本地上传替换部分&#xff0c;可以不看 原理&#xff1a;通过video标签对视频进行加载&#xff0c;随后使用canvas对截取的视频帧生成需要的图片 <template> <el-row :gutter"18" class"preview-video"><h4>视频预览<span&…

LabVIEW电路产品功能自动检测系统

开发基于LabVIEW的电路产品功能自动检测系统。该系统通过整合先进的硬件和软件技术&#xff0c;实现了电路产品的自动化测试&#xff0c;显著提高了测试效率和准确性&#xff0c;对于提升电子产品的可靠性和工作效率具有重要意义。 项目背景 在电子制造业中&#xff0c;电路产…

PyCharm查看文件或代码变更记录

背景&#xff1a; Mac笔记本上有一个截图的定时任务在运行&#xff0c;本地Python使用的是PyCharm IDE&#xff0c;负责的同事休假&#xff0c;然后定时任务运行的结果不符合预期&#xff0c;一下子不知道问题出现在哪里。 定位思路&#xff1a; 1、先确认网络、账号等基本的…

git使用以及理解

git练习网站 Learn Git Branching git操作大全Oh Shit, Git!?! git commit git branch name git merge bugFix 合并俩个分支 git rebase main git checkout headgit switch head 会导致HEAD分离 &#xff0c;就是指head->HEAD->c1 相对引用 ------------------- …

测试面试宝典(十四)—— 你觉得软件测试的核心竞争力是什么?

回答一&#xff1a; 软件测试的核心竞争力在于其能够保障软件产品的质量和可靠性。 首先&#xff0c;测试人员需要具备敏锐的观察力和细致入微的分析能力&#xff0c;能够在复杂的系统中发现潜在的缺陷和问题。例如&#xff0c;在测试一款电商平台时&#xff0c;不仅要关注订…

Apache AGE的MATCH子句

MATCH子句允许您在数据库中指定查询将搜索的模式。这是检索数据以在查询中使用的主要方法。 通常在MATCH子句之后会跟随一个WHERE子句&#xff0c;以添加用户定义的限制条件到匹配的模式中&#xff0c;以操纵返回的数据集。谓词是模式描述的一部分&#xff0c;不应被视为仅在匹…

【TDA4板端部署】基于 Pytorch 训练并部署 ONNX 模型在 TDA4

1 将torch模型转onnx模型 Ti转换工具只支持以下格式&#xff1a; Caffe - 0.17 (caffe-jacinto in gitHub) Tensorflow - 1.12 ONNX - 1.3.0 (opset 9 and 11) TFLite - Tensorflow 2.0-Alpha 基于 Tensorflow、Pytorch、Caffe 等训练框架&#xff0c;训练模型&#xff1a;选择…

多多OJ评测系统 前端项目环境初始化 安装Vue脚手架 引入Arco Design组件

目录 确定环境 命令行输入 装一下脚手架 监测一下是否安装成功 创建一个项目 选择一系列的配置后 我们打开webStorm 配置脚手架后我们先运行 我们这边能获取到网址 其实我们脚手架已经帮我们做到了 接下来要引入相关的组件 选择用npm进行安装 我们建议的是完整引入…

姓名配对测试源码

源码简介 姓名配对测试源码&#xff0c;输入两人姓名即可测试缘分&#xff0c;可查看朋友到底喜欢谁的趣味源码。 自己手动在数据库里修改数据&#xff0c;数据库里有就会优先查询数据库的信息&#xff0c; 没设置的话第一次查询缘分都是非常好的 95-99&#xff0c;第二次查…

Spring Web MVC(常用的注解@RequestMapping,@RequestParam,@RequestBody等)

一、Spring MVC spring的启动类 启动类是看这个 SpringBootApplication 注解&#xff0c;而不是 类的名字 这个注解在哪&#xff0c;哪个类就是启动类 1.MVC思想 举例 二、Spring MVC mvc 是一种思想&#xff0c;而spring mvc是对mvc思想的一种实现。全称是 spring web mvc…

pytorch学习(四)绘制loss和correct曲线

这一次学习的时候静态绘制loss和correct曲线&#xff0c;也就是在模型训练完成后&#xff0c;对统计的数据进行绘制。 以minist数据训练为例子 import torch from torch import nn from torch.utils.data import DataLoader from torchvision import datasets from torchvisi…

Prometheus智能化监控介绍

Prometheus智能化监控介绍 官方网站特点&#xff1a;样本 Prometheus组件Prometheus工作流程Prometheus和zabbix对比分析Prometheus的几种部署模式Prometheus的四种数据类型CounterGaugehistogram为什需要用histogram柱状图&#xff1f; summary Prometheus对kubernetes的监控介…

文献阅读:tidyomics 生态系统:增强组学数据分析

文献介绍 文献题目&#xff1a; The tidyomics ecosystem: enhancing omic data analyses 研究团队&#xff1a; Stefano Mangiola&#xff08;澳大利亚沃尔特和伊丽莎霍尔医学研究所&#xff09;、Michael I. Love&#xff08;美国北卡罗来纳大学教堂山分校&#xff09;、Ant…