golang泛型实现——skiplist
mhr18 2024-11-25 10:40 22 浏览 0 评论
一、写在前面
skiplist是一种有序的数据结构, 不同于各种平衡树, skiplist看起来就是多层的链表, 具体点每个元素是个数组, 这个元素的数组除了0层是和下个元素直连, 1层和n层之间可能和下个, 或者下下个节点连接起来。
这些skiplist节点的多层结构,构成实施二分搜索的基础, 理论从而达到可观的效率, 开源界大名鼎鼎的redis的zset一部分使用skiplist。
对于这个被吹爆了的数据,下面会使用redis的套路在go里面实现下, 是骡子是马拉出来溜溜压测下性能如何。
二、 一个基本的skiplist的样子
2.1 图示
2.2 代码展示
表头和节点
type Node[T any] struct {
score float64
elem T
// 后退指针
backward *Node[T]
NodeLevel []struct {
// 指向前进节点, 是指向tail的方向
forward *Node[T]
span int
}
}
type SkipList[T any] struct {
head *Node[T]
tail *Node[T]
r *rand.Rand
length int
level int
}
三、查找
3.1 图示
还是以上面的skiplist为例, 下面会画出查找每个元素的搜索路径。
skiplist里面head节点有两个重要的特点:
- head节点的层数等于最大节点和层数, 一般实现中会有MaxLevel记录最大结点层数
- head节点是与数据节点之间有连接关系的
下面的查找路径,
- 实线表示查找的路径,
- 虚线表示节点间的关系
查找的过程就是先从head节点的MaxLevel-1索引查找, 查找的大致走位是向右向下向右向下...就逼近目标位置
- 查找5
- 查找8
- 查找10
3.2 代码展示
// 获取
func (s *SkipList[T]) GetWithErr(score float64) (elem T, err error) {
x := s.head
for i := s.level - 1; i >= 0; i-- {
for x.NodeLevel[i].forward != nil && (x.NodeLevel[i].forward.score < score) {
x = x.NodeLevel[i].forward
}
}
x = x.NodeLevel[0].forward
if x != nil && score == x.score {
return x.elem, nil
}
err = ErrNotFound
return
}
四、新增
新增先使用update[]数组维护每层需要插入节点的位置, 通过抛硬币的函数决定这个新节点的level, 最后修改节点的前后关系, 就是插入链表节点的多层版本, 维护多层信息就是update[]干的事情。
4.1 抛硬币函数
把1当成硬币的正面, 0当作硬币的反面, 直到抛到0时就结束, 这时连续的正面就是skiplist里面需要建的level数。
func (s *SkipList[T]) rand() int {
level := 1
for {
if s.r.Int()%2 == 0 {
break
}
level++
}
if level < SKIPLIST_MAXLEVEL {
return level
}
return SKIPLIST_MAXLEVEL
}
4.2 梳理多层插入节点逻辑
如果在单链表中执行插入操作
- prev是待插入节点的前面一个节点,
- 待插入节点是new, new.next = prev.next,prev.next = new, 即可完成一个节点的插入。
skiplist的插入和单链表相似, 无非是扩展到多层, 使用一个数组记录每一层的prev指针,skiplist的新节点也是数组,这里使用一个for循环遍历层,每个层内的操作与单链表中的操作是一样的。
4.3 图示
插入节点5
4.4 代码展示
func (s *SkipList[T]) InsertInner(score float64, elem T, level int) *SkipList[T] {
var (
update [SKIPLIST_MAXLEVEL]*Node[T]
rank [SKIPLIST_MAXLEVEL]int
)
x := s.head
var x2 *Node[T]
for i := s.level - 1; i >= 0; i-- {
if i == s.level-1 {
rank[i] = 0
} else {
rank[i] = rank[i+1]
}
for x.NodeLevel[i].forward != nil &&
(x.NodeLevel[i].forward.score < score) {
// 暂时不支持重复的key, 后面再说 TODO
//|| x.NodeLevel[i].forward.score == score &&
//s.compare(elem, x.NodeLevel[i].forward.elem) < 0) {
//TODO span的含义是?
rank[i] += x.NodeLevel[i].span
x = x.NodeLevel[i].forward
}
// 保存插入位置的前一个节点, 保存的数量就是level的值
update[i] = x
}
// 这个score已经存在直接返回
x2 = x.NodeLevel[0].forward
if x2 != nil && score == x2.score {
x2.elem = elem
return s
}
if level > s.level {
// 这次新的level与老的level的差值, 给填充head指针
for i := s.level; i < level; i++ {
// TODO rank的含义
rank[i] = 0
update[i] = s.head
update[i].NodeLevel[i].span = s.length
}
s.level = level
}
// 创建新节点
x = newNode(level, score, elem)
for i := 0; i < level; i++ {
// x.NodeLevel[i]的节点假设等于a, 需要插入的节点x在a之后,
// a, x, a.forward三者的关系就是[a, x, a.forward]
// 那就变成了x.forward = a.forward, 修改x.forward的指向
// a.forward = x
// 看如下两行代码
x.NodeLevel[i].forward = update[i].NodeLevel[i].forward
update[i].NodeLevel[i].forward = x
x.NodeLevel[i].span = update[i].NodeLevel[i].span - (rank[0] - rank[i])
update[i].NodeLevel[i].span = rank[0] - rank[i] + 1
}
for i := level; i < s.level; i++ {
update[i].NodeLevel[i].span++
}
if update[0] != s.head {
x.backward = update[0]
}
if x.NodeLevel[0].forward != nil {
x.NodeLevel[0].forward.backward = x
} else {
s.tail = x
}
s.length++
return s
}
五、删除
如果在单链表中执行删除操作, prev是待删除节点的前面一个节点, 如果要删除待删除节点n, 直接prev.next = n.next就完成一个节点的删除, skiplist的删除也和单链表类似。
5.1 图示
删除节点5
5.2 代码展示
func (s *SkipList[T]) removeNode(x *Node[T], update []*Node[T]) {
for i := 0; i < s.level; i++ {
if update[i].NodeLevel[i].forward == x {
update[i].NodeLevel[i].span += x.NodeLevel[i].span - 1
update[i].NodeLevel[i].forward = x.NodeLevel[i].forward
} else {
update[i].NodeLevel[i].span -= 1
}
}
if x.NodeLevel[0].forward != nil {
// 原来右边节点backward指针直接指各左边节点, 现在指向左左节点
x.NodeLevel[0].forward.backward = x.backward
} else {
// 最后一个元素, 修改下尾指针的位置
s.tail = x.backward
}
for s.level > 1 && s.head.NodeLevel[s.level-1].forward == nil {
s.level--
}
s.length--
}
// 根据score删除元素
func (s *SkipList[T]) Remove(score float64) *SkipList[T] {
var update [SKIPLIST_MAXLEVEL]*Node[T]
x := s.head
for i := s.level - 1; i >= 0; i-- {
for x.NodeLevel[i].forward != nil && (x.NodeLevel[i].forward.score < score) {
x = x.NodeLevel[i].forward
}
update[i] = x
}
x = x.NodeLevel[0].forward
if x != nil && score == x.score {
s.removeNode(x, update[:])
return s
}
return s
}
六、压测
从压测上来看, 本文中的skiplist的实现, 相比golang map性能接近,同时保持有序特性,值得王婆卖瓜...
goos: darwin
goarch: amd64
pkg: github.com/guonaihong/gstl/skiplist
cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
BenchmarkGet-8 1000000000 0.7746 ns/op
BenchmarkGetStd-8 1000000000 0.7847 ns/op
PASS
ok github.com/guonaihong/gstl/skiplist 178.377s
七、完整代码
有小伙伴对完整实现感兴趣的可查看如下链接, 除了上面聊过的Get、Set、Remove外, 还有遍历、TopMax、TopMin等操作。
https://github.com/guonaihong/gstl/blob/master/skiplist/skiplist.go
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)