之所以会有这篇文章,是因为我在学习Go语言跳表代码实现的过程中,产生过一些困惑,但网上的大家都不喜欢写注释- -

我的代码注释一向是写的很全的,所以发出来供后来者学习参考。

本文假设你已经理解了跳表的原理,不再做细节阐述。(可能会考虑以后补充)

代码实现参考了 https://github.com/wangzheng0822/algo/blob/master/go/17_skiplist/skiplist.go

但以上项目实现是有问题的(截至2020/06/14 14:18),Find 方法有概率查不到 score 重复的不同元素,Delete 方法也有bug,有可能会出现空指针,代码写的略奇怪,我直接重写了。

我还没去提pull request,因为测试用例我完全没写。有空补充一下。

本文的跳跃表参考了Redis跳表实现,加入了score属性区分优先级,跳表元素不能重复,允许score重复。

定义数据结构如下:

type SkipList struct {
Head *SkipListNode
LevelN int // 包含原始链表的层数
Length int // 长度
}
type SkipListNode struct {
Val interface{}
Level int // 该节点的最高索引层是多少
Score int
Next []*SkipListNode // key是层高
}

提供了以下方法:

  • func (sl *SkipList) Insert(val interface{}, score int) (bool, error)
  • func (sl *SkipList) Find(v interface{}, score int) *skipListNode
  • func (sl *SkipList) Delete(v interface{}, score int) bool

此外,可以使用func (sl *SkipList) printSkipList()方法打印跳跃表的索引结构,加深理解。

整体代码如下:

const MAX_LEVEL = 16 // 最大索引层级限制

// 不支持重复元素
// 支持相同的score
type SkipList struct {
Head *skipListNode
LevelN int // 包含原始链表的层数
Length int // 长度
}
type skipListNode struct {
Val interface{}
Level int // 该节点的最高索引层是多少
Score int
Next []*skipListNode // key是层高
} func NewSkipList() *SkipList {
return &SkipList{
Head: newSkipListNode(nil, math.MinInt64, MAX_LEVEL),
LevelN: 1,
Length: 0,
}
}
func newSkipListNode(val interface{}, score, level int) *skipListNode {
return &skipListNode{
Val: val,
Level: level,
Score: score,
Next: make([]*skipListNode, level, level),
}
}
func (sl *SkipList) Insert(val interface{}, score int) (bool, error) {
if val == nil {
return false, errors.New("can't insert nil value to skiplist")
} cur := sl.Head
update := [MAX_LEVEL]*skipListNode{} // 记录在每一层的插入位置,value保存哨兵结点
k := MAX_LEVEL - 1
// 从最高层的索引开始查找插入位置,逐级向下比较,最后插入到原始链表也就是第0级
for ; k >= 0; k-- {
for cur.Next[k] != nil {
if cur.Next[k].Val == val {
return false, errors.New("can't insert repeatable value to skiplist")
}
if cur.Next[k].Score > score {
update[k] = cur
break
}
cur = cur.Next[k]
}
// 如果待插入元素的优先级最大,哨兵节点就是最后一个元素
if cur.Next[k] == nil {
update[k] = cur
}
} randomLevel := sl.getRandomLevel()
newNode := newSkipListNode(val, score, randomLevel) // 插入元素
for i := randomLevel - 1; i >= 0; i-- {
newNode.Next[i] = update[i].Next[i]
update[i].Next[i] = newNode
}
if randomLevel > sl.LevelN {
sl.LevelN = randomLevel
}
sl.Length++ return true, nil
} // skiplist在插入元素时需要维护索引,生成一个随机值,将元素插入到第1-k级索引中
func (sl *SkipList) getRandomLevel() int {
level := 1
for i := 1; i < MAX_LEVEL; i++ {
if rand.Int31()%7 == 1 {
level++
}
}
return level
} func (sl *SkipList) Find(v interface{}, score int) *skipListNode {
if v == nil || sl.Length == 0 {
return nil
}
cur := sl.Head
for i := sl.LevelN - 1; i >= 0; i-- {
if cur.Next[i] != nil {
if cur.Next[i].Val == v && cur.Next[i].Score == score {
return cur.Next[i]
} else if cur.Next[i].Score >= score {
continue
}
cur = cur.Next[i]
}
}
// 如果没有找到该元素,这时cur是原始链表中,score相同的第一个元素,向后查找
for cur.Next[0].Score <= score {
if cur.Next[0].Val == v && cur.Next[0].Score == score {
return cur.Next[0]
}
cur = cur.Next[0]
} return nil
}
func (sl *SkipList) Delete(v interface{}, score int) bool {
if v == nil {
return false
}
cur := sl.Head
// 记录每一层待删除数据的前驱结点
// 如果某些层没有待删除数据,那么update[i]为空
// 如果待删除数据不存在,那么update[i]也为空
update := [MAX_LEVEL]*skipListNode{}
for i := sl.LevelN - 1; i >= 0; i-- {
for cur.Next[i] != nil && cur.Next[i].Score <= score {
if cur.Next[i].Score == score && cur.Next[i].Val == v {
update[i] = cur
break
}
cur = cur.Next[i]
}
}
// 删除节点
for i := sl.LevelN - 1; i >= 0; i-- {
if update[i] == nil {
continue
}
// 如果该层中,删除节点是第一个节点且没有下一个节点,直接降低索引层(只有最高层会出现这种情况)
if update[i] == sl.Head && update[i].Next[i].Next[i] == nil {
sl.LevelN = i
continue
}
update[i].Next[i] = update[i].Next[i].Next[i]
} sl.Length-- return true
} func (sl *SkipList) printSkipList() {
if sl.Length > 0 {
for i := 0; i < sl.LevelN; i++ {
cur := sl.Head
output := fmt.Sprintf("The %dth skipList is: ", i)
for cur.Next[i] != nil && cur.Next[i].Val != nil {
// value(score)
output += fmt.Sprintf("-%v(%d)-", cur.Next[i].Val, cur.Next[i].Score)
cur = cur.Next[i]
}
fmt.Println(output)
}
}
}

之后有空可能会补充一些API,不过最关键的就是这些了。

如有疏漏请联系我,感谢!

Go语言的跳跃表(SkipList)实现的更多相关文章

  1. Redis数据结构之跳跃表-skiplist

    在Redis中,zset是一个复合结构: 使用hash来存储value和score的映射关系 使用跳跃表来提供按照score进行排序的功能,同时可以指定score范围来获取value列表 结构 zse ...

  2. redis 5.0.7 源码阅读——跳跃表skiplist

    redis中并没有专门给跳跃表两个文件.在5.0.7的版本中,结构体的声明与定义.接口的声明在server.h中,接口的定义在t_zset.c中,所有开头为zsl的函数. 一.数据结构 单个节点: t ...

  3. 跳跃表 SkipList【数据结构】原理及实现

    为什么选择跳表 目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等. 想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树出来吗? ...

  4. 探索c#之跳跃表(SkipList)

    阅读目录: 基本介绍 算法思想 演化步骤 实现细节 总结 基本介绍 SkipList是William Pugh在1990年提出的,它是一种可替代平衡树的数据结构. SkipList在实现上相对比较简单 ...

  5. [Redis]Redis的设计与实现-链表/字典/跳跃表

    redis的设计与实现:1.假如有一个用户关系模块,要实现一个共同关注功能,计算出两个用户关注了哪些相同的用户,本质上是计算两个用户关注集合的交集,如果使用关系数据库,需要对两个数据表执行join操作 ...

  6. Redis底层探秘(二):链表和跳跃表

    链表简介 链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地跳转链表的长度. 作为一种常用数据结构,链表内置在很多高级的编程语言里面,因为Redis使用C语言并没有内 ...

  7. redis 系列7 数据结构之跳跃表

    一.概述 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.在大部分情况下,跳跃表的效率可以和平衡树(关系型数据库的索引就是平衡树 ...

  8. Redis(2)——跳跃表

    一.跳跃表简介 跳跃表(skiplist)是一种随机化的数据结构,由 William Pugh 在论文<Skip lists: a probabilistic alternative to ba ...

  9. 【Redis】跳跃表原理分析与基本代码实现(java)

    最近开始看Redis设计原理,碰到一个从未遇见的数据结构:跳跃表(skiplist).于是花时间学习了跳表的原理,并用java对其实现. 主要参考以下两本书: <Redis设计与实现>跳表 ...

随机推荐

  1. 入门大数据---Spark_Streaming基本操作

    一.案例引入 这里先引入一个基本的案例来演示流的创建:获取指定端口上的数据并进行词频统计.项目依赖和代码实现如下: <dependency> <groupId>org.apac ...

  2. JDK8--06:Stream流

    一.描述 Stream流提供了筛选与切片.映射.排序.匹配与查找.归约.收集等功能 筛选与切片: filter:接收lambda,从流中排除某些元素 limit(n):截断流,使其元素不超过n ski ...

  3. SpringBoot--使用redis实现分布式限流

    1.引入依赖 <!-- 默认就内嵌了Tomcat 容器,如需要更换容器也极其简单--> <dependency> <groupId>org.springframew ...

  4. SpringBoot--swagger搭建、配置及使用

    一. 作用: 1. 接口的文档在线自动生成. 2. 接口测试. 二.模块介绍 Swagger是一组开源项目,其中主要要项目及功能如下: 1.Swagger Codegen: 通过Codegen 可以将 ...

  5. vue全家桶(2.3)

    3.4.嵌套路由 实际生活中的应用界面,通常由多层嵌套的组件组合而成.同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如: 再来看看下面这种更直观的嵌套图: 接下来我们需要实现下面这种 ...

  6. 基于小程序请求接口 wx.request 封装的类 axios 请求

    基于小程序请求接口 wx.request 封装的类 axios 请求 Introduction wx.request 的配置.axios 的调用方式 源码戳我 feature 支持 wx.reques ...

  7. 简单几步让CentOS系统时间同步

    在使用CentOS系统的时候,我们可能会遇到时间不准的问题,那我们如何解决这个我问题呢,下面就来教大家一个CentOS系统时间同步的方法,希望大家可以解决自己所存在的疑问. CentOS系统时间同步的 ...

  8. js修改函数内部的this指向(bind,call,apply)

    js修改函数内部的this指向 在调用函数的时候偶尔在函数内部会使用到this,在使用this的时候发现并不是我们想要指向的对象.可以通过bind,call,apply来修改函数内部的this指向. ...

  9. h5页面自动播放视频、音频_关于媒体文件自动全屏播放的实现方式

    在移动端(ios和android)播放视频的时候,我们即使定义了autoplay属性,仍然不能自动播放.这是由于手机浏览器为了防止浪费用户的网络流量,在默认情况下是不允许媒体文件自动播放的,除非用户自 ...

  10. centos7安装Mysql爬坑记录

    centos7安装Mysql爬坑记录   查看是否已安装 使用下列命令查看是否已经安装过mysql/mariadb/PostgreSQL 如果未安装,不返回任何结果(ECS的centos镜像默认未安装 ...