SkipList(跳表)

article/2025/8/7 21:49:34

SkipList(跳表)

文章目录

  • SkipList(跳表)
    • 参考
    • 前言
    • 跳表的原理
    • 跳表的插入和删除
      • 插入操作
      • 删除操作
    • 跳表的时间空间复杂度分析
      • 时间复杂度
      • 空间复杂度
    • 调表的基本操作
      • 插入数据
      • 删除数据
    • Go 实现
    • 小结

参考

  • https://juejin.cn/post/6844903955831619597#heading-2
  • https://blog.csdn.net/qq_56999918/article/details/122960821
  • 王争老师的SkipList实现

前言

下文介绍一种基于单链表的高级数据结构,跳表。

将单链表先进行排序,然后针对有序链表为了实现高效的查找,可以使用跳表这种数据结构。其根本思想是二分查找的思想。

跳表的前提条件是针对有序的单链表,实现高效地查找,插入、删除。

Redis中的,有序集合sorted set就是用跳表实现的。

跳表的原理

对于单链表,就是是存储的有序数据(即 有序链表上),想在其中查找某个数据,也只能从头到尾遍历,查找效率低,时间复杂度是O(n),如下图所示:

img

为了提高查找效率,并使用二分查找的思想,我们对有序链表建立了一级“索引”。每两个节点提取一个节点到索引层。索引层中的每个节点都包含两个指针,一个指向下一个节点,一个down指针,指向下一级节点。

img

例如我们需要查找图中7 这个节点:

通过有序链表我们需要5次才能查找到,而对于类似上图加上以及索引的结构时我们只需要查找3次就可以找到7这个节点。

那么查找的次数能否再减少呢?我们会很自然想到可以与一级索引类似的再添加一层二级索引,如下图:

img

还是同样去查找7这个节点我们发现,只需遍历两个节点便可以查找到7这个节点了。

通过建立索引的方式,对于数据量越大的有序链表,通过建立多级索引,查找效率提升会非常明显。这种链表加多级索引的结构就是跳表。

对于查找离链首的节点效果可能不会很明显,对于距离链首越远的节点跳表提升的性能会更明显。

跳表的插入和删除

对于链表类数据结构的插入删除操作的时间复杂度为O(log n)(对于链表类的数据结构插入删除的时间复杂度一般与查找的时间复杂度保持一致).

插入操作

为了保证原始链表中的数据的有序性,我们需要先找到新数据应该插入的位置。可以基于多级索引,快速查找到新数据的插入位置,时间复杂度为(log n).

假设插入数据为6的节点,如下图:

img

删除操作

删除原链表中的节点,如果节点存在于索引中,也要删除索引中的节点。因为单链表中的删除需要用到要删除节点的前驱节点。可以像插入操作一样,通过索引逐层向下遍历到原始链表中,要删除的节点,并记录其前驱节点,从而实现删除操作。

跳表的时间空间复杂度分析

时间复杂度

2

在讨论跳表查找的时间复杂度前我们先来讨论一下跳表的索引高度。若按照两个节点会多出一个节点作为上级索引节点的话,不难想到跳表有 h = l o g 2 n h = log_{2}n h=log2n层。

接下来我们跟着上图中用红色加粗的线去看一下一个跳表查询的路径。我们可以发现每层索引最多遍历3个元素。此时我们还知道跳表的高度 h = l o g 2 n h = log_{2}n h=log2n,所以跳表中查找一个元素的时间复杂度为 O ( 3 ∗ l o g n ) O(3 * logn) O(3logn),忽略常数即为: O ( l o g n ) O(logn) O(logn).

空间复杂度

跳表提升元素查找效率的思想就是典型的"空间换时间"的思想。

索引建立的策略仍按照两个节点会多出一个节点作为上级索引节点(前提)。假如原始链表包含n个元素,则一级索引元素个数为 n / 2 n/2 n/2、二级索引元素个数为 n / 4 n/4 n/4依次类推。所以索引节点数就是一个等比数列的求和: n / 2 + n / 4 + . . . . + 2 = n − 2 n/2 + n/4+....+2=n-2 n/2+n/4+....+2=n2

,空间复杂度是O(n).

可以注意到我们在前面计算空间复杂度时是有前提的,如果我们现在按照每三个节点抽一个节点作为索引,计算方式类似的我们可以推出索引节点的总和是: n / 3 + n / 9 + . . . + 3 = n / 2 n/3 + n/9 + ... + 3 = n/2 n/3+n/9+...+3=n/2,减少了一般。所以我们可以通过减少索引来减少空间复杂度,不过与此同时会降低查找的效率。

调表的基本操作

插入数据

我们可以将在跳表中插入数据动作分成两个部分:

  1. 查找到插入的位置

    类似于查找,跳表中数据结构是有序的。这一步中我们要找的就是前一个元素比待插入元素X小后一个元素比待插入元素X大的那个位置。

  2. 更新索引

    层级索引其实是跳表中的核心。如果我们一直往原始列表中插入数据,但是不更新索引,那么会出现两个索引系欸但之间数据非常多的情况,极端情况下跳表将退化为单链表.因此我们需要一个索引更新策略。

    索引更新策略

    假如跳表每一层的晋升概率为1/2,最理想的索引就是在原始链表中每隔一个元素抽取一个元素作为一级索引。那么我们是否可以在原始链表中随机选取n/2个元素作为一级索引是否也能达到一样的效果呢?实际上是可以的,因为好数据结构都是为了应对大数据量的场景,当原始链表中的元素数量足够多,我们得到的索引也会是比较均匀的。因此,我们可以使用这样一个索引策略:随机选取n/2个元素作为一级索引、随机选n/4,以此类推,一直到最顶层的索引。

    我们可以先来看看代码:

    // 向跳表中插入数据
    func (list *SkipListInt) Set(key int64, value interface{}) {list.mutex.Lock()defer list.mutex.Unlock()prev := &list.SkipNodeIntvar next *SkipNodeIntfor i := list.level - 1; i >= 0; i-- {next = prev.next[i]for next != nil && next.key < key {prev = nextnext = prev.next[i]}// 记录查找过来的路径list.update[i] = prev}// 如果key已经存在if next != nil && next.key == key {next.value = valuereturn}// 随机生成新节点的层数level := list.randomLevel()if level > list.level {level = list.level + 1list.level = levellist.update[list.level-1] = &list.SkipNodeInt}// 申请新的节点node := &SkipNodeInt{}node.key = keynode.value = valuenode.next = make([]*SkipNodeInt, level)// 根据前面查找到的这个位置 向不同的层架新增索引for i := 0; i < level; i++ {node.next[i] = list.update[i].next[i]list.update[i].next[i] = node}list.length++
    }
    

    这部分代码就是按照前面提到的两步去插入数据的。其中这个list.randomLevel()的作用就是随机获得要插入数据要更新的索引层级(其概率分布是一级索引1/2, 二级索引1/4 ……)。这里应该可以大致理解维护索引的这个策略了。下面我们通过一个例子更加清晰的去看插入数据的过程。

    例如我们需要在跳表中插入数据6,首先 randomLevel() 返回了3,表示需要建立3级索引。

    3

4

5

6

通过上图我们可以比较清晰地看到整个数据插入、索引更新的过程。绘制的这系列图其实展现了一种实现的思路:

  1. 获得需更新的索引层级
  2. 找到待插入元素的索引区间,就向这个索引区间中插入这个元素

当然我们也可以先找到元素需要插入的位置,在过程中记录查找的路径(最后给出的代码按照这种思路)。

删除数据

跳表删除数据时需要把索引中对应的节点都删掉。如下图中,要删除元素9,我们需要对原始链表一级一级索引中的9都删除掉。

7

这里我不做详解,思路其实与在调表中查找数据的方式一致。不同的是不能在索引层级查找时就退出,而需要继续深入到原始链表。这样子可以找到跳表的所有层级索引中与待删除元素直接相连的前一个元素。

Go 实现

package mainimport ("math/rand""sync""time"
)// 跳表的节点
type SkipNodeInt struct {key   int64value interface{}next  []*SkipNodeInt
}// 跳表的结构
type SkipListInt struct {SkipNodeIntmutex  sync.RWMutexupdate []*SkipNodeIntrand   *rand.Randmaxl   intskip   intlevel  intlength int
}// 初始化一个跳表
func NewSkipListInt(skip ...int) *SkipListInt {list := &SkipListInt{}list.maxl = 32list.skip = 4list.level = 0list.length = 0list.SkipNodeInt.next = make([]*SkipNodeInt, list.maxl)list.update = make([]*SkipNodeInt, list.maxl)list.rand = rand.New(rand.NewSource(time.Now().UnixNano()))if len(skip) == 1 && skip[0] > 1 {list.skip = skip[0]}return list
}// 查找跳表中的元素
func (list *SkipListInt) Get(key int64) interface{} {list.mutex.Lock()defer list.mutex.Unlock()prev := &list.SkipNodeIntvar next *SkipNodeInt// 先从最高层的调表去查找for i := list.level - 1; i >= 0; i-- {next = prev.next[i]// 同级索引查找 如果找到的还是比给定的key小的话就跳到下一个点继续查找for next != nil && next.key < key {prev = nextnext = prev.next[i]}// 找到对应的元素就退出查找if next != nil && next.key == key {return next.value}}return nil
}// 向跳表中插入数据
func (list *SkipListInt) Set(key int64, value interface{}) {list.mutex.Lock()defer list.mutex.Unlock()prev := &list.SkipNodeIntvar next *SkipNodeIntfor i := list.level - 1; i >= 0; i-- {next = prev.next[i]for next != nil && next.key < key {prev = nextnext = prev.next[i]}// 记录查找过来的路径list.update[i] = prev}// 如果key已经存在if next != nil && next.key == key {next.value = valuereturn}// 随机生成新节点的层数level := list.randomLevel()if level > list.level {level = list.level + 1list.level = levellist.update[list.level-1] = &list.SkipNodeInt}// 申请新的节点node := &SkipNodeInt{}node.key = keynode.value = valuenode.next = make([]*SkipNodeInt, level)// 根据前面查找到的这个位置 向不同的层架新增索引for i := 0; i < level; i++ {node.next[i] = list.update[i].next[i]list.update[i].next[i] = node}list.length++
}// 调表中移除某个元素
func (list *SkipListInt) Remove(key int64) interface{} {list.mutex.Lock()defer list.mutex.Unlock()prev := &list.SkipNodeIntvar next *SkipNodeInt// 这种查找方式保证查到路径一定会经过底层的索引,这为后面删除元素时更新索引提供了遍历// 查找时可以找到对应的key时就直接推出for i := list.level - 1; i >= 0; i-- {next = prev.next[i]for next != nil && next.key < key {prev = nextnext = prev.next[i]}list.update[i] = prev}// 节点不存在node := nextif next == nil || next.key != key {return nil}// 调整next的指向for i, v := range node.next {if list.update[i].next[i] == node {list.update[i].next[i] = vif list.SkipNodeInt.next[i] == nil {list.level -= 1}}list.update[i] = nil}list.length--return node.value
}// 获得跳表的长度
func (list *SkipListInt) GetLength() int {list.mutex.Lock()defer list.mutex.Unlock()return list.length
}// 随机生成位于第几层调表
func (list *SkipListInt) randomLevel() int {i := 1for ; i < list.maxl; i++ {if list.rand.Int31()%int32(list.skip) != 0 {break}}return i
}

小结

  • 跳表通过时间换空间的方式实现了可二分查找的有序链表
  • 跳表查询、插入、删除的时间复杂度都为O(log n),与平衡二叉树接近

http://chatgpt.dhexx.cn/article/SacJjJdv.shtml

相关文章

每日一博 - 如何理解跳表(SkipList)

文章目录 什么是跳跃表SkipList跳表关键字Why Skip ListCode跳表-查询跳表-删除跳表-插入 小结完整Code 什么是跳跃表SkipList 跳跃表(简称跳表)由美国计算机科学家William Pugh于1989年发明 论文&#xff1a; Skip lists: a probabilistic alternative to balanced trees 跳…

Skiplist(跳表)实现

前言&#xff1a;跳表本质是一种查找结构&#xff0c;相较于AVL树、红黑树、哈希表&#xff0c;实现起来要更加简单&#xff0c;而且效率也不差。 一.Skiplist概念 跳表本质也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff0c;跟平衡搜索树&#xff08;AVL树、红…

跳表(SkipList)设计与实现(java)

微信搜一搜「bigsai」关注这个有趣的程序员&#xff0c;一起做个朋友 新人原创公众号&#xff0c;求支持一下&#xff01;你的点赞三连肯定对我至关重要&#xff01; 文章已收录在 我的Github bigsai-algorithm 欢迎star 前言 跳表是面试常问的一种数据结构&#xff0c;它在很…

SkipList和java中ConcurrentSkipListMap的实现

文章目录 简介SkipListConcurrentSkipListMapSkipList的实现concurrent的实现 总结 SkipList和java中ConcurrentSkipListMap的实现 简介 一开始听说SkipList我是一脸懵逼的&#xff0c;啥&#xff1f;还有SkipList&#xff1f;这个是什么玩意。 后面经过我的不断搜索和学习&a…

跳表(SkipList)及ConcurrentSkipListMap源码解析

二分查找和AVL树查找 二分查找要求元素可以随机访问&#xff0c;所以决定了需要把元素存储在连续内存。这样查找确实很快&#xff0c;但是插入和删除元素的时候&#xff0c;为了保证元素的有序性&#xff0c;就需要大量的移动元素了。 如果需要的是一个能够进行二分查找&#…

Redis数据结构之——跳表skiplist

写在前面 以下内容是基于Redis 6.2.6 版本整理总结 一、跳表&#xff08;skiplist&#xff09; 如何理解跳表&#xff1f;在了解跳表之前&#xff0c;我们先从普通链表开始&#xff0c;一点点揭开跳表的神秘面纱~ 首先&#xff0c;普通单链表来说&#xff0c;即使链表是有序…

redis中ziplist,skiplist

ziplist压缩表 ziplist主要是为了节约内存&#xff0c;他将元素存储在一块连续的内存空间中&#xff0c;这样在查询数据的时候也可以利用CPU的缓存访问数据&#xff0c;加快查询的效率 相较于数组而言。我们知道,数组要求每个元素的大小都相同,如果我们要存储不同长度的字符串…

跳表-skiplist的简单实现

文章目录 1、什么是跳表-skiplist2、skiplist的效率如何保证&#xff1f;3、skiplist的实现4、skiplist跟平衡搜索树和哈希表的对比 1、什么是跳表-skiplist skiplist本质上也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff0c;跟平衡搜索树和哈希表的价值是一样…

跳表:Skiplist原理介绍和优缺点

skiplist介绍 不要求上下相邻两层链表之间的节点个数有严格的对应关系&#xff0c;而是为每个节点随机出一个层数(level)。比如&#xff0c;一个节点随机出的层数是3&#xff0c;那么就把它链入到第1层到第3层这三层链表中。为了表达清楚&#xff0c;下图展示了如何通过一步步的…

skiplist 跳跃表详解及其编程实现

skiplist介绍 跳表(skip List)是一种随机化的数据结构&#xff0c;基于并联的链表&#xff0c;实现简单&#xff0c;插入、删除、查找的复杂度均为O(logN)。跳表的具体定义&#xff0c; 请参考参考维基百科 点我&#xff0c;中文版。跳表是由William Pugh发明的&#xff0c;这…

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

转载请注明&#xff1a;http://blog.csdn.net/ict2014/article/details/17394259 SkipList在leveldb以及lucence中都广为使用&#xff0c;是比较高效的数据结构。由于它的代码以及原理实现的简单性&#xff0c;更为人们所接受。我们首先看看SkipList的定义&#xff0c;为什么叫…

跳表(skiplist)的理解

听到跳表(skiplist)这个名字,既然是list,那么应该跟链表有关。 跳表是有序链表,但是我们知道,即使对于排过序的链表,我们对于查找还是需要进行通过链表的指针进行遍历的,时间复杂度很高依然是O(n),这个显然是不能接受的。是否可以像数组那样,通过二分法进行查找呢,但…

SkipList详解

本文参考&#xff1a;《大数据日知录》 概念 SkipList是一种用来代替平衡树的数据结构。 虽然在最坏的情况下SkipList的效率要低于平衡树&#xff0c;但是大多数情况下效率仍然非常高&#xff0c;其插入、删除、查找的时间复杂度都是O(log(N))。 除了高效外&#xff0c;其实现…

跳表(skipList)

一、为何有skipList这种数据结构的出现 我们知道二分查找算法之所以能达到 O(logn) 这样高效的一个重要原因在于它所依赖的数据结构是数组&#xff0c;数组支持随机访问一个元素&#xff0c;通过下标很容易定位到中间元素。而链表是不支持随机访问的&#xff0c;只能从头到尾依…

跳表 skiplist 简介

跳表 skiplist 跳表 (Skip List) 是由 William Pugh 在 1990 年发表的文章 Skip Lists: A Probabilistic Alternative toBalanced Trees 中描述的一种查找数据结构&#xff0c;支持对数据的快速查找&#xff0c;插入和删除。 对于 AVL 树、红黑树等平衡树&#xff0c;在插入过…

SkipList(跳跃表)详解

Introduction: skiplist本质上也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff08;Searching&#xff09;&#xff0c;即根据给定的key&#xff0c;快速查到它所在的位置&#xff08;或者对应的value&#xff09; 一般用于解决查找问题的数据结构分为两个大类…

docker中国镜像

一,如果是像redis,mysql等官方的镜像,直接配置阿里云的镜像就可以 1,注册阿里云账号,并登录 2,打开这个网址 https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors 3,页面上会教怎么设置,如下面的截图 二,如果是私有仓库的镜像 非常感谢原作者:https://www.jiansh…

Docker 使用国内镜像仓库

Docker 使用国内镜像仓库 1、问题描述2、总结 1、问题描述 由于某些原因&#xff0c;导致Docker镜像在国内下载速度特别慢。所以为了沉浸式开发。最好切换为国内源。这里以163 的镜像仓库举例。首先修改/etc/docker/daemon.json配置文件。 sudo vi /etc/docker/daemon.json…

MacOS上配置docker国内镜像仓库地址

背景 docker官方镜像仓库网速较差&#xff0c;我们需要设置国内镜像服务 我的MacOS docker版本如下 设置docker国内镜像仓库地址 点击Settings点击Docker Engine修改配置文件&#xff0c;添加registry-mirrors {"builder": {"gc": {"defaultKeepS…

Docker国内镜像加速地址与详细说明

简介 对于动不动就几百M甚至上G的Docker镜像来说&#xff0c;官方镜像总是掉线或速度极慢&#xff0c;为了改善这种情况&#xff0c;建议切换成国内镜像。常用的国内镜像使用阿里云、网易的居多&#xff0c;本篇内容将记录一下Docker的这些国内镜像是怎么使用的。 国内常用的…