SkipList(跳表)

news/2024/4/29 10:56:47/文章来源:https://blog.csdn.net/qq_52785898/article/details/127102103

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=log2nh = log_{2}nh=log2n层。

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

空间复杂度

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

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

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

可以注意到我们在前面计算空间复杂度时是有前提的,如果我们现在按照每三个节点抽一个节点作为索引,计算方式类似的我们可以推出索引节点的总和是:n/3+n/9+...+3=n/2n/3 + n/9 + ... + 3 = n/2n/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://www.luyixian.cn/news_show_16016.aspx

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

相关文章

C# ZBar解码测试(QRCode、一维码条码)并记录里面隐藏的坑

实现效果(文中含源码): C# ZBar解码演示 使用提醒(解码能力下降,甚至解错) C# ZXing解码介绍请查看下面文章: C# ZXing.net解码测试(QRCode、DataMatrix、1D-Barcode一维码条码)_Color Space的博客-CSDN博客 实现步骤: 1、需要的包及对应版本: ① ZXing.Net v0.16.8…

基于html+JavaScript+css的飞机射击小游戏网页设计与实现

目录 一、概述 1 1.1项目内容 1 1.2项目开发 5 1.3项目组员 5 1.4组员分工 5 1.5验收标准 5 二、项目介绍 5 2.1 目标 5 2.2 用户的特点 6 2.3 假定和约束 6 2.3.1 开发期限&#xff1a; 6 2.3.2 技术约束&#xff1a; 6 2.4 确定系统运行的要求 6 2.5飞机大作战的整理思路 6 三…

MySQL三种存储引擎的区别

文章目录一、引言二、三个存储引擎的介绍1&#xff0c;InnoDB介绍特点&#xff08;1&#xff09;事务处理、回滚、崩溃修复能力和多版本并发控制的事务安全&#xff08;2&#xff09;支持主键自增&#xff08;3&#xff09;支持外键&#xff08;4&#xff09;支持事务和事务相关…

jar包运行报错jar中没有主清单属性、springGateway访问接口报错302,跳转login接口

此处记录三个错误&#xff1a; 一、jar中没有主清单属性&#xff0c;打包以后运行报错&#xff1a; 这是一个gateway模块&#xff0c;包含了启动类&#xff0c;因为在pom文件中没有指定&#xff0c;所以报错&#xff1a;在该模块的pom文件中加入如下代码&#xff1a;com.xxxx…

Spring中IOC容器的基本配置使用

实例化bean对象 为外部定义的bean起别名 基于set方法的依赖注入 使用构造函数进行依赖注入 使用内部注入的方式注入bean对象 使用list集合依赖注入和使用map集合依赖注入 测试类 使用depends-on控制bean的加载顺序 使用懒加载lazy-init 默认为false 使用单例或者多例(原型)…

C#中对象与JSON字符串互相转换的三种方式

JSON(JavaScriptObject Notation, JS 对象标记) 是一种轻量级的数据交换格式。 关于内存对象和JSON字符串的相互转换,在实际项目中应比较广泛,经过一番搜索,找到如下三种方法来解决此问题 分别是使用Newtonsoft.Json.dll、DataContractJsonSerializer、JavaScriptSerializer…

【老板要你啥都会系列】| 前端晋升全栈--项目日志

目录 1.开篇 2.nodejs文件操作 3.stream 4.stream演示 5.写日志 6.拆分日志 7.分析日志介绍 8.readline 1.开篇 日志包含什么访问人数啊、峰值啊、bug 啊什么的&#xff0c;如果没有日志那么很容易失控。 访问日志可以参考我们 http-server&#xff0c;每次访问都会有这…

谐振波导光栅的严格分析

摘要 谐振波导光栅(RWG)由于其在波长、相位和偏振等方面的可调谐性&#xff0c;在研究和工业中有着广泛的应用。RWG的结构包含一个薄的高折射率波导薄膜&#xff0c;该薄膜与光栅接触。波导支持多种导模&#xff0c;并且根据厚度的不同&#xff0c;模式的数量也不同。在这个…

javaFx打包exe程序

文章目录将代码打成jar包准备工作下载exe4j定制jre检测jar包用到jre的哪些jmods生成jre准备exe图标使用exe4j将jar转换成exe程序将代码打成jar包 打jar之前&#xff0c;把那些用不到的依赖统统删除&#xff0c;以免包含一些无用的内容&#xff0c;比如用不上的一些依赖jar&…

特征工程之特征降维-特征选择-PCA/LDA

特征降维之特征选择 特征选择是建模中常用的降维手段&#xff0c;比较暴力&#xff0c;直接将不重要特征删除。 缺点&#xff1a;造成信息丢失&#xff0c;不利于模型精度。【与之形成对比的是PCA、LDA等降维方式。】 主要标准有两个&#xff1a; 特征是否发散。特征与目标的…

简单组件讲解

在编程阶段,会遇到有些页面的某一区域的布局或数据显示类似;那么我们就可以复用这一段代码;在使用原生JS编程时,我们习惯是将代码抽出来自成一个文件,需要时引入即可。而在vue中也存在这样一个模块,可以简便的将可复用的代码抽成一个模块,这个就是组件。在很多人看来,组…

产品-如何让用户“更愿意“付费

学院课程 文章目录学院课程前言如何提高用户体验,吸引用户付费关于程序员等级的划分基础免费试看优质博客内容转化为视频用户关于短视频内容的生成关于利用用户的碎片化时间怎么差异化竞品&#xff1f;短视频赛道理解学院现状分析总结前言 学院地址 今天下午公司组织了一场需求…

线性回归

线性回归 导入库 import numpy as np import pandas as pd import matplotlib.pyplot as plt人工数据集n = 100 true_theta = np.array([[1], [1]]) X = np.insert(np.random.normal(5, 1, size=(n, 1)), 0, 1, 1) y = X @ true_theta + np.random.normal(0, 0.04, size=(n, …

Azure | AZ-204 认证之旅-应用服务(二)

theme: orange 我正在参加「掘金启航计划」 Web应用是构建在PaaS层的服务&#xff0c;它是支持托管的&#xff0c;并且是可缩放的&#xff0c;极大提高了工程效率&#xff0c;并且减少了在运营的层面的消耗&#xff0c;这篇文章介绍如何创建应用程序服务。 创建Web应用程序服务…

pytorch深度学习训练模板(分类、回归)

前言 LeNet-AlexNet-ZFNet: LeNet-AlexNet-ZFNet一维复现pytorch VGG: VGG一维复现pytorch GoogLeNet: GoogLeNet一维复现pytorch ResNet: ResNet残差网络一维复现pytorch-含残差块复现思路分析 DenseNet: DenseNet一维复现pytorch 包含所有一维经典模型的代码可随意切换训练 …

云原生技术 --- k8s节点组件之kube-proxy的学习与理解

k8s 网络代理(kube-proxy)在每个节点上运行。网络代理反映了每个节点上 Kubernetes API 中定义的服务&#xff0c;并且可以执行简单的 TCP、UDP 和 SCTP 流转发&#xff0c;或者在一组后端进行 循环 TCP、UDP 和 SCTP 转发。但是&#xff0c;必须要有一个插件&#xff0c;才可以…

毕业设计- 基于单片机与GPS+GSM的车辆定位跟踪系统

文章目录0 前言1 简介2 主要器件3 实现效果4 硬件设计Maduino Zero A9G GPRS/GPSk开发板这款低功耗A9G使用SIM800/900和NEO-6M GPS模块的基于Arduino的GPS跟踪系统5 软件说明使用Arduino的基于GPSGSM的车辆跟踪系统GPSGSM的车辆跟踪系统的代码6 最后0 前言 &#x1f525; 这两…

IDEA中使用Git

目录 一、IDEA中使用Git 配置Git settings ——>Version Control——>Git 点击Test测试版本号 下载gitee插件 配置账户 第一种方式&#xff1a;账号密码 第二种方式&#xff1a;通过Token令牌 分享单个项目 组员需要拿到项目的SSH地址 二、总结 一、IDEA中使用G…

万物互联时代到来,猿代码领衔先计算机赋数据化转型

社会经济的高速发展推动着各行各业进行转型升级&#xff0c;而数字经济则是当前社会经济发展的强大驱动。根据相关报道显示&#xff0c;早在多年前我国数字经济规模总量便达到万亿元规模&#xff0c;占2016年全年GDP比重的30&#xff05;。数字经济的飞速发张需要强劲算力的支撑…

wxpython设计GUI:grid控件实现显示表单数据功能,同时实现界面的上下翻页以及跳转功能

grid控件实现显示表单数据功能&#xff0c;同时实现界面的上下翻页以及跳转功能。 1. 效果展示 2. 代码实现 #!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Logintern09########################################################################### ## Python …