跳表

简介: 跳表通过多层指针实现二分查找式访问,每层以不同步长连接节点,高层加速遍历,低层精确定位。查找时从高层开始,逐步降层逼近目标,时间复杂度O(log n)。为避免频繁调整结构,插入时用随机函数决定新节点层数,保证概率上的平衡性,兼顾效率与实现简便性。

跳表是如何进行二分查找的?

除了二叉检索树,有序链表还有其他快速访问中间节点的改造方案吗?我们知道,链表之所以访问中间节点的效率低,就是因为每个节点只存储了下一个节点的指针,要沿着这个指针遍历每个后续节点才能到达中间节点。那如果我们在节点上增加一个指针,指向更远的节点,比如说跳过后一个节点,直接指向后面第二个节点,那么沿着这个指针遍历,是不是遍历速度就翻倍了呢?

同理,如果我们能增加更多的指针,提供不同步长的遍历能力,比如一次跳过 4 个节点,甚至一半的节点,那我们是不是就可以更快速地访问到中间节点了呢?

这当然是可以实现的。我们可以为链表的某些节点增加更多的指针。这些指针都指向不同距离的后续节点。这样一来,链表就具备了更高效的检索能力。这样的数据结构就是 跳表(Skip List)。

一个理想的跳表,就是从链表头开始,用多个不同的步长,每隔 2^n 个节点做一次直接链接(n 取值为 0,1,2……)。跳表中的每个节点都拥有多个不同步长的指针,我们可以在每个节点里,用一个数组 next 来记录这些指针。next 数组的大小就是这个节点的层数,next[0]就是第 0 层的步长为 1 的指针,next[1]就是第 1 层的步长为 2 的指针,next[2]就是第 2 层的步长为 4 的指针,依此类推。你会发现,不同步长的指针,在链表中的分布是非常均匀的,这使得整个链表具有非常平衡的检索结构。

a7a1asa6a8a9a4a2a3

理想的跳表

举个例子,当我们要检索 k=a6 时,从第一个节点 a1 开始,用最大步长的指针开始遍历,直接就可以访问到中间节点 a5。但是,如果沿着这个最大步长指针继续访问下去,下一个节点是大于 k 的 a9,这说明 k 在 a5 和 a9 之间。那么,我们就在 a5 和 a9 之间,用小一个级别的步长继续查询。这时候,a5的下一个元素是 a7,a7 依然大于 k 的值,因此,我们会继续在 a5 和 a7 之间,用再小一个级别的步长查找,这样就找到 a6 了。这个过程其实就是二分查找。时间代价是 O(log n)。

跳表的检索空间平衡方案

不知道你有没有注意到,我在前面强调了一个词,那就是 「理想的跳表」。为什么要叫它「理想」的跳表呢?难道在实际情况下,跳表不是这样实现的吗?的确不是。当我们要在跳表中插入元素时,节点之间的间隔距离就被改变了。如果要保证理想链表的每隔 2^n 个节点做一次链接的特性,我们就需要重新修改许多节点的后续指针,这会带来很大的开销。

所以,在实际情况下,我们会在检索性能和修改指针代价之间做一个权衡。为了保证检索性能,我们不需要保证跳表是一个理想的平衡状态,只需要保证它在大概率上是平衡的就可以了。因此,当新节点插入时,我们不去修改已有的全部指针,而是仅针对新加入的节点为它建立相应的各级别的跳表指针。具体的操作过程,我们一起来看看。

首先,我们需要确认新加入的节点需要具有几层的指针。我们通过随机函数来生成层数,比如说,我们可以写一个函数 RandomLevel() ,以 (1/2)^n 的概率决定是否生成第 n 层。这样,通过简单的随机生成指针层数的方式,我们就可以保证指针的分布,在大概率上是平衡的。

在确认了新节点的层数 n 以后,接下来,我们需要将新节点和前后的节点连接起来,也就是为每一层的指针建立前后连接关系。其实每一层的指针链接,你都可以看作是一个独立的单链表的修改,因此我们只需要用单链表插入节点的方式完成指针连接即可。

这么说,可能你理解起来不是很直观,接下来,我通过一个具体的例子进一步给你解释一下。

相关文章
|
1天前
|
存储 Serverless
哈希冲突
哈希冲突可通过优化哈希函数或采用冲突解决策略应对。开放寻址法通过线性、二次探查或双散列寻找空位,但易导致聚集,影响效率;链表法则在冲突位置构建链表,避免抢占,更适应动态数据,是常用方案之一。
|
1天前
|
NoSQL 索引
SSTable 的分层管理设计
SSTable分层管理通过将文件按层级组织,逐层合并,控制每层容量上限,减少多路归并规模,避免全量重叠,提升查询效率与系统性能,是LevelDB高效读写的核心设计。
|
1天前
|
存储 负载均衡 搜索推荐
大规模检索系统
本讲介绍大规模检索系统如何通过分布式技术加速检索。通过索引拆分,将倒排索引分散到多台服务器内存中,减少单机数据规模和磁盘访问,从而提升单次查询效率。结合分发服务器与负载均衡,实现高吞吐、低延迟的分布式检索架构。
|
1天前
|
存储 自然语言处理 分布式计算
索引构建
搜索引擎如何为万亿网页构建索引?通过分治与多路归并,将文档拆分为小集合,在内存中生成倒排索引后写入磁盘,再合并多个有序临时文件,最终生成全局倒排文件。词典可加载至内存或用B+树管理,实现高效检索。该过程类似MapReduce,支持分布式扩展。
|
1天前
|
存储 搜索推荐 索引
跳表法加速倒排索引
跳表、哈希表与位图法可加速倒排索引。跳表通过多层链表实现快速跳转,将归并查找时间降至O(log n);哈希表适用于小集合查大集合,查询可达O(1);位图则利用位运算高效求交集,适合短posting list场景,显著提升检索效率。
|
1天前
|
存储 算法 搜索推荐
数组的检索效率
二分查找通过将有序数组不断折半,每次比较中间值与目标值,缩小搜索范围至一半,实现O(log n)高效检索,显著优于遍历的O(n),适用于大规模有序数据查询。
|
1天前
|
存储 API 索引
数据结构的存储方式
数据结构底层存储只有数组和链表两种,其他如栈、队列、树、图等均为其衍生。数组支持随机访问但扩容困难,链表灵活增删但无法随机访问。所有数据结构的操作本质为“增删查改”,遍历方式分为线性迭代与非线性递归。理解二者差异,是掌握各类高级数据结构的基础。(238字)
|
1天前
|
存储 Java API
数组(顺序存储)基本原理
本章讲解数组的底层原理,区分静态与动态数组。通过静态数组实现动态数组的增删查改,揭示随机访问O(1)的成因与连续内存的利弊,助你理解数据结构本质。
|
1天前
|
存储 算法 Java
基础语法与面向对象
classDiagram class Collection {<<interface>>} class List {<<interface>>} class Set {<<interface>>} class Map { <<interface>> entrySet()* keySet()* values()* } Collection <|-- List Collection <|-- Set List <|.. ArrayList List <|.. LinkedList List <|.. Vector Set <|.. HashSet Map <|.. HashMap Map <|..
|
1天前
|
存储 缓存 NoSQL
查找对应的 SSTable 文件
通过分层结构与二分查找快速定位SSTable,结合BloomFilter过滤和索引区加速查询。利用table cache与block cache缓存机制,减少磁盘IO,提升检索效率。整个过程高效有序,适用于大规模数据检索场景。(238字)