GO实现跳跃表

news/2024/4/19 20:59:13/文章来源:https://blog.csdn.net/qq_49723651/article/details/127624075

GO实现跳跃表

文章目录

  • GO实现跳跃表
    • 跳跃表介绍
    • 跳跃表的实现
      • 跳跃表的结构
      • 创建跳跃表
      • 跳跃表的插入和删除
      • 跳跃表的排名操作
      • 跳跃表的区间操作
    • 完整实现

跳跃表介绍

跳跃表(skiplist)是一种有序的数据结构,它通过建立多层"索引",从而达到快速访问节点的目的. 跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

下面是一个跳表结构的示意图,其实跳表就是一个二维链表,只有最底层的链表中存着数据,其他层都是在第一层基础上建立的索引,越靠近上层,节点之间的跨度就越大,跳表的查询范围也越大。依靠着这些索引,跳表可以实现接近二分查找的查找效率。

在这里插入图片描述

跳跃表的实现

跳跃表的结构

跳表的元素

// Element 是一个key-score对组
type Element struct {Member string// 跳跃表节点依照Score升序排序,若一样,则按照Member的字典升序排序Score float64
}

跳表的层结构

// Level 层
type Level struct {// 指向前面一个节点forward *node// 与前一个节点的跨度span int64
}

跳表的节点

跳表的一个节点有三个字段:元素、指向前一个节点的指针和建立在该节点之上的层级。

// node 跳跃表的一个节点
type node struct {Element// 回退指针backward *node// 每个节点有 1~maxLevel 个层级level []*Level
}

跳表的表头结构

// skiplist 跳表结构
type skiplist struct {// 指向表头节点header *node// 指向表尾节点tail *node// 跳跃表的长度(除了第一个节点)length int64// 跳跃表的最大层级(除了第一个节点)level int16
}

创建跳跃表

// makeNode 创建一个跳跃表节点
func makeNode(level int16, score float64, member string) *node {n := &node{Element: Element{Score:  score,Member: member,},level: make([]*Level, level),}for i := range n.level {n.level[i] = new(Level)}return n
}// makeSkiplist 创建一个跳跃表结构
func makeSkiplist() *skiplist {return &skiplist{level:  1,header: makeNode(maxLevel, 0, ""),}
}

跳跃表的插入和删除

在插入跳跃表之前,我们要明确的是新插入的这个节点,我们应该在它之上建立多少层索引呢?我们将通过一个随机算法来计算得到一个随机值,叫做幂次定律

幂次定律的含义是:如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。映射到我们的需求就是一个新插入的节点,生成小数值层数的概率很大,而生成大数值层数的概率很小。

const (maxLevel = 16
)
// randomLevel 随机生成一个新跳跃表节点的层数(1~16)
// 满足幂次定律
func randomLevel() int16 {level := int16(1)for float32(rand.Int31()&0xFFFF) < (0.25 * 0xFFFF) {level++}if level < maxLevel {return level}return maxLevel
}

上述函数计算出来的层数将呈现以下概率:

p = 0.25(1/4)

层数恰好为1的概率(不执行while)为1 - p(3/4).
层数恰好为2的概率(执行 1 次while)为p * (1 - p)(3/16).
层数恰好为3的概率(执行 2 次while)为p ^ 2 * (1 - p)(3/64).
层数恰好为4的概率(执行 3 次while)为p ^ 3 * (1 - p)(3/256).
层数恰好为k(k <= 32)的概率(执行 k - 1 次while)为p ^ (k - 1) * (1 - p).

可以发现生成越高层数的概率会越来越小,而且和上一次呈幂关系递减.

插入操作

插入操作的步骤:

  1. 首先准备两个切片:update(用于保存在每一层,待插入节点的前一个节点)、rank(用于累加每一层的跨度,方便后续待插入节点索引中span字段的计算)。
  2. 从上至下遍历每一层索引,在每一层中寻找待插入节点的位置(如果分数比当前节点小,就往后遍历,比当前节点大就下沉),将待插入节点的前一个节点存到update切片中,然后将待插入节点相对起始点的便宜量粗存到rank切片中。
  3. 找到待插入节点的位置之后,先使用randomLevel函数获取该节点应该建立索引的层数。
  4. 接着构造节点,然后插入到应该插入的位置,首先需要更新每一层索引的状态,新插入节点的forward指针就指向前一个节点的forward指针指向的位置(前一个节点保存在update切片中),新插入节点的索引span字段就是它与前一个节点同层索引的跨度之差(通过rank切片计算得到)。接着因为新插入节点增加了前面节点的跨度,所以需要更新前面一个节点每一层的跨度。
  5. 最后设置新插入节点的backward指针指向,直接指向前一个节点即可(通过update切片来实现)。
// insert 插入元素
func (skiplist *skiplist) insert(member string, score float64) *node {// 保存在每一层,待插入节点的前一个节点update := make([]*node, maxLevel)// 用于累加跨度rank := make([]int64, maxLevel)// 找到待插入的位置node := skiplist.headerfor i := skiplist.level - 1; i >= 0; i-- {if i == skiplist.level-1 {rank[i] = 0} else {// 累加跨度rank[i] = rank[i+1]}if node.level[i] != nil {// 在第i层找待插入的位置for node.level[i].forward != nil &&(node.level[i].forward.Score < score ||(node.level[i].forward.Score == score && node.level[i].forward.Member < member)) { // same score, different key// 累加与前一个节点的跨度rank[i] += node.level[i].span// 前进node = node.level[i].forward}}update[i] = node}// 获得随机层数level := randomLevel()// 如果新插入的节点抽到的层级最大if level > skiplist.level {// 初始化每一层的状态for i := skiplist.level; i < level; i++ {rank[i] = 0update[i] = skiplist.headerupdate[i].level[i].span = skiplist.length}skiplist.level = level}// 构造新节点并插入到跳表node = makeNode(level, score, member)for i := int16(0); i < level; i++ {node.level[i].forward = update[i].level[i].forwardupdate[i].level[i].forward = nodenode.level[i].span = update[i].level[i].span - (rank[0] - rank[i])update[i].level[i].span = (rank[0] - rank[i]) + 1}// 新插入的节点增加了前面节点的跨度for i := level; i < skiplist.level; i++ {update[i].level[i].span++}// 设置回退节点if update[0] == skiplist.header {node.backward = nil} else {node.backward = update[0]}// 设置node前面一个节点的回退节点if node.level[0].forward != nil {node.level[0].forward.backward = node}skiplist.length++return node
}

删除操作

删除操作首先要找到待删除节点的位置,找节点的步骤与插入节点的操作类似的,首先创建一个切片:update(用于保存在每一层,待删除节点的前一个节点)。然后在每一层中进行查找,分数比当前节点小,就往后遍历,比当前节点大就下沉,同时用update切片记录每一层中待删除节点的前一个节点。找到该节点之后,就可以进行删除操作了。

先更新每一层索引的状态:更新待删除节点前一个节点的跨度以及forward指针的指向。
然后更新后面一个节点的回退指针,最后更新跳表中的最大层级即可。

// 寻找待删除的节点
func (skiplist *skiplist) remove(member string, score float64) bool {// 储存待删除节点每一层的上一个节点update := make([]*node, maxLevel)node := skiplist.header// 寻找待删除节点for i := skiplist.level - 1; i >= 0; i-- {for node.level[i].forward != nil &&(node.level[i].forward.Score < score ||(node.level[i].forward.Score == score &&node.level[i].forward.Member < member)) {node = node.level[i].forward}update[i] = node}// node在循环中,一直是待删除节点的前一个节点// 在最底层的索引处向后移动一位,刚好就是待删除节点node = node.level[0].forward// 找到该节点if node != nil && score == node.Score && node.Member == member {skiplist.removeNode(node, update)return true}return false
}
// 删除找到的节点
func (skiplist *skiplist) removeNode(node *node, update []*node) {// 更新每一层的状态for i := int16(0); i < skiplist.level; i++ {if update[i].level[i].forward == node {update[i].level[i].span += node.level[i].span - 1update[i].level[i].forward = node.level[i].forward} else {update[i].level[i].span--}}// 更新后面一个节点的回退指针if node.level[0].forward != nil {node.level[0].forward.backward = node.backward} else {skiplist.tail = node.backward}// 更新跳表中的最大层级for skiplist.level > 1 && skiplist.header.level[skiplist.level-1].forward == nil {skiplist.level--}skiplist.length--
}

跳跃表的排名操作

获取元素的排名

获取元素的排名操作比较简单,首先定义一个rank整型变量,用于在遍历的时候累加跨度。
接着逐层进行查找,在某一层进行查找时,每往前遍历一个元素,就使用rank变量累加上它们索引之间的跨度,当遍历到第0层时,就找到了这个节点,rank变量就是当前节点在整个跳跃表中的排名。

func (skiplist *skiplist) getRank(member string, score float64) int64 {var rank int64 = 0x := skiplist.headerfor i := skiplist.level - 1; i >= 0; i-- {for x.level[i].forward != nil &&(x.level[i].forward.Score < score ||(x.level[i].forward.Score == score &&x.level[i].forward.Member <= member)) {rank += x.level[i].spanx = x.level[i].forward}if x.Member == member {return rank}}return 0
}

通过排名获取元素

首先定义一个变量i用于累加每一层索引的跨度,接着在每一层索引中进行遍历,如果i累加上当前节点层与下一个节点层的跨度值小于rank,就继续往后遍历,否则就下沉。当i等于rank时,就找到了该节点。

func (skiplist *skiplist) getByRank(rank int64) *node {// 记录从头节点开始的跨度var i int64 = 0// 用于遍历节点的指针n := skiplist.header// 从最高层级开始遍历for level := skiplist.level - 1; level >= 0; level-- {for n.level[level].forward != nil && (i+n.level[level].span) <= rank {i += n.level[level].spann = n.level[level].forward}if i == rank {return n}}return nil
}

跳跃表的区间操作

我们创建了一个ScoreBorder结构体用于封装跳表的分数,提供了比较大小以及创建ScoreBorder等API。

const (// 负无穷negativeInf int8 = -1// 正无穷positiveInf int8 = 1
)type ScoreBorder struct {// 标记当前分数是否为无穷Inf int8// 分数值Value float64// 标记两个分数相等时,是否返回trueExclude bool
}func (border *ScoreBorder) greater(value float64) bool {if border.Inf == negativeInf {return false} else if border.Inf == positiveInf {return true}if border.Exclude {return border.Value > value}return border.Value >= value
}func (border *ScoreBorder) less(value float64) bool {if border.Inf == negativeInf {return true} else if border.Inf == positiveInf {return false}if border.Exclude {return border.Value < value}return border.Value <= value
}var positiveInfBorder = &ScoreBorder{Inf: positiveInf,
}var negativeInfBorder = &ScoreBorder{Inf: negativeInf,
}// ParseScoreBorder 根据参数构造并返回ScoreBorder
func ParseScoreBorder(s string) (*ScoreBorder, error) {if s == "inf" || s == "+inf" {return positiveInfBorder, nil}if s == "-inf" {return negativeInfBorder, nil}if s[0] == '(' {value, err := strconv.ParseFloat(s[1:], 64)if err != nil {return nil, errors.New("ERR min or max is not a float")}return &ScoreBorder{Inf:     0,Value:   value,Exclude: true,}, nil}value, err := strconv.ParseFloat(s, 64)if err != nil {return nil, errors.New("ERR min or max is not a float")}return &ScoreBorder{Inf:     0,Value:   value,Exclude: false,}, nil
}

判断[min, max]区间与是否在skiplist的分数区间内(是否有重合)

判断有三个指标:

  1. 判断[min, max]区间本身是否有效。
  2. 判断min是否大于跳表的最大分数值(与表尾元素的分数作比较)。
  3. 判断max是否小于跳表的最小分数值(与表头元素的分数作比较)。
func (skiplist *skiplist) hasInRange(min *ScoreBorder, max *ScoreBorder) bool {// [min, max]无意义或为空if min.Value > max.Value || (min.Value == max.Value && (min.Exclude || max.Exclude)) {return false}// [min, max] > skiplist.tail.Scoren := skiplist.tailif n == nil || !min.less(n.Score) {return false}// [min, max] < skiplist.head.Scoren = skiplist.header.level[0].forwardif n == nil || !max.greater(n.Score) {return false}return true
}

从跳表中找到处于[min, max]区间的最小值

实现思路比较简单,我们找到跳表中分数第一个大于min的节点即可。找到之后我们还需要将该节点的分数与max作比较,如果大于max,则不存在。

func (skiplist *skiplist) getFirstInScoreRange(min *ScoreBorder, max *ScoreBorder) *node {if !skiplist.hasInRange(min, max) {return nil}n := skiplist.header// 找到第一个大于等于min的节点for level := skiplist.level - 1; level >= 0; level-- {for n.level[level].forward != nil && !min.less(n.level[level].forward.Score) {n = n.level[level].forward}}n = n.level[0].forward// n节点的分数在[min, max]区间之外if !max.greater(n.Score) {return nil}return n
}

删除跳表中分数值处在[min, max]区间内的元素,并返回它们的切片

首先遍历跳表,然后找到分数值大于min的第一个节点,从这个节点开始删除,删除一个就继续往后遍历,删除的过程中还得判断,待删除的节点分数是否超出了[min, max]区间。

func (skiplist *skiplist) RemoveRangeByScore(min *ScoreBorder, max *ScoreBorder) (removed []*Element) {// 储存待删除节点每一层的前驱节点update := make([]*node, maxLevel)removed = make([]*Element, 0)// 找到待删除节点每一层的前驱节点node := skiplist.headerfor i := skiplist.level - 1; i >= 0; i-- {for node.level[i].forward != nil {if min.less(node.level[i].forward.Score) {break}node = node.level[i].forward}update[i] = node}node = node.level[0].forward// 开始删除节点for node != nil {// 保证不超出[min, max]区间if !max.greater(node.Score) {break}next := node.level[0].forwardremovedElement := node.Elementremoved = append(removed, &removedElement)skiplist.removeNode(node, update)node = next}return removed
}

删除排名在[start, stop]区间内的元素,并返回它们的切片

首先定义一个i变量,作为删除节点的迭代器,接着找到排名为start的节点,然后从这个节点往后删除即可。

func (skiplist *skiplist) RemoveRangeByRank(start int64, stop int64) (removed []*Element) {// 排名迭代器var i int64 = 0update := make([]*node, maxLevel)removed = make([]*Element, 0)// 找到待删除的第一个节点的前驱节点,并储存在update切片中node := skiplist.headerfor level := skiplist.level - 1; level >= 0; level-- {for node.level[level].forward != nil && (i+node.level[level].span) < start {i += node.level[level].spannode = node.level[level].forward}update[level] = node}i++// 处在区间的第一个节点node = node.level[0].forward// 开始删除节点for node != nil && i < stop {next := node.level[0].forwardremovedElement := node.Elementremoved = append(removed, &removedElement)skiplist.removeNode(node, update)node = nexti++}return removed
}

完整实现

https://github.com/omlight95/GoRedis/blob/master/datastruct/sortedset/skiplist.go

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

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

相关文章

世界城市日|数字城市里看不见的“保安”,真面目竟是…

2022年10月31日&#xff0c;是第8个世界城市日。在数字化浪潮席卷全球的当下&#xff0c;城市发展亦进入新的阶段。建造数字城市&#xff0c;全面推进城市数字化转型成为当前城市建设的热议话题。数字城市、万物互联&#xff0c;与网络空间的融合必不可少。然而系统的复杂度越高…

简单使用gige千兆网口工业相机,国产崛起(二,c#)

发现海康的sdk不错&#xff0c;可以用海康&#xff0c;basler&#xff0c;大华工业相机&#xff0c;估计其他的也可以&#xff0c;有机会试一试&#xff01;国产厉害&#xff0c;崛起了&#xff01;赞一个&#xff0c;热情爆棚&#xff01;且随窃喜&#xff01; 首先下载海康工…

网站SEO标题撰写技巧,做到这些可以提高点击率

搜索引擎认为&#xff0c;一个网站的点击率越高&#xff0c;那么这个网站就越受欢迎&#xff0c;因此就会提高网站的关键词排名。网站的点击率越高&#xff0c;就会获得更多流量。网站标题和点击率息息相关&#xff0c;一个好的网站标题&#xff0c;能够轻松获得流量。那么&…

[carla入门教程]-2 pythonAPI的使用

本专栏教程将记录我从安装carla到调用carla的pythonAPI进行车辆操控的全流程,带领大家从安装carla开始,到最终能够熟练使用carla仿真环境进行传感器数据采集和车辆控制. 第二节 pythonAPI的使用 本小节主要学习使用 pythonAPI来与carla服务器进行交互.包括获取信息,发送信息.…

IDEA热部署插件JRebel使用

JRebel安装与激活 JRebel 使用 此时已经安装好并已激活&#xff0c;我们使用 JRebel debug的时候&#xff0c;修改代码&#xff0c;不能实现热部署&#xff0c;因此还需要设置其他地方 1.项目自动编译 设置 compiler.automake.allow.when.app.running ctrlshiftA 或者 help->…

vue相关原理

vue 原理 面试为什么要考察原理 知其然知其所以然&#xff0c;各行各业通用的道理了解原理才能用的很好&#xff0c;专业性考察&#xff0c;技术的追求竞争激烈&#xff0c;则优录取大厂造轮子&#xff08;业务定制&#xff1a;有些框架不能满足需求&#xff09; 面试中如何…

【Spark NLP】第 19 章:生产化 NLP 应用程序

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

docker下快速部署openldap与PHPLdapAdmin

在一个组织中&#xff0c;为了简化各种内部系统的账号和密码的管理&#xff0c;往往就需要ldap来进行管理了。 对于ldap的实现方式也非常多&#xff0c;但在免费的开源系统中&#xff0c;openldap是ldap的首选系统。 同时&#xff0c;在这一切讲究快速的时代&#xff0c;采用d…

大数据ClickHouse进阶(二十二):ClickHouse优化

文章目录 ClickHouse优化 一、表优化 1、日期字段避免使用String存储 2、Nullable值处理 <

计算机毕业设计(附源码)python音蕾心动

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

云IDE的简单使用、体验与学习

云IDE的简单使用、体验与学习一、简单尝试二、官网展示的特点三、视频用例3.1、用Cloud IDE快速启动开源项目3.2、用Cloud IDE 在线提交PR云IDE产品介绍 云IDE使用教程 免费使用地址&#xff1a;点击【云IDE】&#xff0c;即可开始创建工作空间。 一、简单尝试 快速创建工作空…

学习用Python实现PPT的自动化

前言 在日常工作中&#xff0c;我们总是需要创建或修改PPT。但你也可以用Python来创建或修改PPT文件。本文将告诉你如何使用Python-pptx模块自动或用PPT模板生成ppt&#xff0c;以及如何通过实例修改现有的PPT。 &#xff08;文末送福利&#xff09; 1.Python模块python-ppt…

hbuilderx ios自定义基座真机测试

任务描述&#xff1a; 用uniapp框架写了一个app应用&#xff0c;需要在ios苹果手机上真机运行测试。 hbuilderx不再支持标准基座真机运行了&#xff0c;需要自定义基座运行 制定自定义基座需要准备的材料&#xff1a; ios的appid,profile文件&#xff0c;私钥证书&#xff0…

动视是否磨灭了暴雪的灵魂?

对于成千上万的人&#xff0c;也许是数百万人来说&#xff0c;暴雪是——或者曾经是——一家特殊的公司。 暴雪——游戏开发的典范 对于奇幻世界的关注&#xff0c;暴雪是无与伦比的。如果游戏没有准备好&#xff0c;它就不会发布。1998 年&#xff0c;尽管《魔兽争霸&#xf…

算法复杂度分析

复杂度分析 参考&#xff1a;《算法导论》、复杂度 - OI Wiki (oi-wiki.org)、一文弄懂算法的时间和空间复杂度分析 - 知乎 (zhihu.com)、算法讲解之复杂度分析 - 知乎 (zhihu.com)、算法的时间复杂度和空间复杂度-总结_zolalad的博客-CSDN博客_时间复杂度 算法复杂度分析的阶段…

梦开始的地方 —— C语言数据在内存中的存储(整形+浮点型)

文章目录整形在内存中的存储1. 数值类型的基本分类2. 整形在内存中的存储1. 原码、反码、补码2. 内存中为什么要存放补码&#xff1f;3. 大小端存储4. 无符号有符号数练习5. 有符号数无符号数小结浮点型在内存中的存储IEEE 754整形在内存中的存储 1. 数值类型的基本分类 整形…

AJAX基础+Axios快速入门+JSON使用+综合案例

目录1、 AJAX1.1 概述1.1.1 作用1.1.2 同步和异步1.2 快速入门1.2.1 服务端实现1.2.2 客户端实现1.3 案例1.3.1 需求1.3.2 分析1.3.2 后端实现1.3.3 前端实现2、 Axios异步框架2.1 基本使用2.2 快速入门2.2.1 后端实现2.2.2 前端实现2.3 请求方法别名3、 JSON3.1 概述3.2 JSON基…

GAS技能系统

HUT -》 在\Intermediate\Build\Win64\UE4Editor\Inc\的目录下 找到generated 头文件和cpp文件 里面有HUT根据UCLASS 和 Generate Body 生成的 定义 以及声明宏(UFUNCTION 里的CustomThunk元可以让用户自己手动添加宏定义和宏声明) 将wildcard改为通配符然后手动将自定义的…

Terraform 华为云实践 项目初始化

这个架构就是DNS加上负载均衡加ecs&#xff0c;最后vpc的架构。网络这块是DNS和VPC&#xff0c;对象存储是用来做terraform的后端来配置。 项目的初始化 Terraform Registry 华为云的terraform链接如上所示。 先将项目的目录结构建好&#xff0c;modules是我们的模块&#xf…

来一场关于元宇宙的灵魂辩论|BOOK DAO内容共建招募

「 备选问题 」1. 你认为元宇宙最重要的特点是什么&#xff1f;用一句话描述你理解的 “元宇宙”2. 元宇宙是游戏2.0吗&#xff1f;它与游戏有什么不同&#xff1f;3. 元宇宙是否需要区块链&#xff1f;是否需要NFT&#xff1f;各扮演什么角色&#xff1f;4. 元宇宙是否需要经济…