返回顶部
首页 > 资讯 > 后端开发 > GO >GO实现跳跃表的示例详解
  • 902
分享到

GO实现跳跃表的示例详解

GoLang跳跃表GO跳跃表 2022-12-19 12:12:22 902人浏览 安东尼
摘要

目录跳跃表介绍跳跃表的实现跳跃表的结构创建跳跃表跳跃表的插入和删除跳跃表的排名操作跳跃表的区间操作完整实现跳跃表介绍 跳跃表(skiplist)是一种有序的数据结构,它通过建立多层&

跳跃表介绍

跳跃表(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).

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

插入操作

插入操作的步骤:

  • 首先准备两个切片:update(用于保存在每一层,待插入节点的前一个节点)、rank(用于累加每一层的跨度,方便后续待插入节点索引中span字段的计算)。
  • 从上至下遍历每一层索引,在每一层中寻找待插入节点的位置(如果分数比当前节点小,就往后遍历,比当前节点大就下沉),将待插入节点的前一个节点存到update切片中,然后将待插入节点相对起始点的便宜量粗存到rank切片中。
  • 找到待插入节点的位置之后,先使用randomLevel函数获取该节点应该建立索引的层数。
  • 接着构造节点,然后插入到应该插入的位置,首先需要更新每一层索引的状态,新插入节点的forward指针就指向前一个节点的forward指针指向的位置(前一个节点保存在update切片中),新插入节点的索引span字段就是它与前一个节点同层索引的跨度之差(通过rank切片计算得到)。接着因为新插入节点增加了前面节点的跨度,所以需要更新前面一个节点每一层的跨度。
  • 最后设置新插入节点的backward指针指向,直接指向前一个节点即可(通过update切片来实现)。
// insert 插入元素
func (skiplist *skiplist) insert(member string, score float64) *node {
	// 保存在每一层,待插入节点的前一个节点
	update := make([]*node, maxLevel)
	// 用于累加跨度
	rank := make([]int64, maxLevel)
	// 找到待插入的位置
	node := skiplist.header
	for 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] = 0
			update[i] = skiplist.header
			update[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].forward
		update[i].level[i].forward = node
		node.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 - 1
			update[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 = 0
	x := skiplist.header
	for 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].span
			x = 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].span
			n = n.level[level].forward
		}
		if i == rank {
			return n
		}
	}
	return nil
}

跳跃表的区间操作

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

const (
	// 负无穷
	negativeInf int8 = -1
	// 正无穷
	positiveInf int8 = 1
)
type ScoreBorder struct {
	// 标记当前分数是否为无穷
	Inf int8
	// 分数值
	Value float64
	// 标记两个分数相等时,是否返回true
	Exclude 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的分数区间内(是否有重合)

判断有三个指标:

  • 判断[min, max]区间本身是否有效。
  • 判断min是否大于跳表的最大分数值(与表尾元素的分数作比较)。
  • 判断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.Score
	n := skiplist.tail
	if n == nil || !min.less(n.Score) {
		return false
	}
	// [min, max] < skiplist.head.Score
	n = skiplist.header.level[0].forward
	if 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.header
	for 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].forward
		removedElement := node.Element
		removed = 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 = 0
	update := make([]*node, maxLevel)
	removed = make([]*Element, 0)
	// 找到待删除的第一个节点的前驱节点,并储存在update切片中
	node := skiplist.header
	for level := skiplist.level - 1; level >= 0; level-- {
		for node.level[level].forward != nil && (i+node.level[level].span) < start {
			i += node.level[level].span
			node = node.level[level].forward
		}
		update[level] = node
	}
	i++
	// 处在区间的第一个节点
	node = node.level[0].forward
	// 开始删除节点
	for node != nil && i < stop {
		next := node.level[0].forward
		removedElement := node.Element
		removed = append(removed, &removedElement)
		skiplist.removeNode(node, update)
		node = next
		i++
	}
	return removed
}

完整实现

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

到此这篇关于GO实现跳跃表的示例详解的文章就介绍到这了,更多相关GO跳跃表内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: GO实现跳跃表的示例详解

本文链接: https://lsjlt.com/news/175240.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • GO实现跳跃表的示例详解
    目录跳跃表介绍跳跃表的实现跳跃表的结构创建跳跃表跳跃表的插入和删除跳跃表的排名操作跳跃表的区间操作完整实现跳跃表介绍 跳跃表(skiplist)是一种有序的数据结构,它通过建立多层&...
    99+
    2022-12-19
    GoLang跳跃表 GO跳跃表
  • Java实现跳跃表的示例详解
    跳表全称叫做跳跃表,简称跳表,是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序列表上面增加多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同...
    99+
    2024-04-02
  • 详解Java中跳跃表的原理和实现
    目录一、跳跃表的引入二、算法分析1.时间复杂度2.空间复杂度三、跳跃表介绍四、跳跃表的实现1.数据结构定义2.查找3.插入4.删除五、实战1.代码2.测试结果一、跳跃表的引入 对有序...
    99+
    2022-12-27
    Java实现跳跃表 Java跳跃表原理 Java跳跃表
  • Java实现跳跃表(skiplist)的简单实例
    跳跃链表是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好。基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以...
    99+
    2023-05-31
    java 跳跃表 skiplist
  • Java跳跃游戏实例真题解决思路详解
    目录变式题—跳跃游戏 I一、题目描述二、思路三、代码变式题—跳跃游戏 II一、题目描述二、思路三、代码变式题—跳跃游戏 I 一、题目描述 给定一个...
    99+
    2022-11-13
    Java跳跃游戏 Java跳跃游戏实例
  • Redis跳跃表的基本原理和实现
    目录一、概述二、跳跃表的实现2.1 跳跃表节点的zskiplisNode结构定义2.2 zskiplist结构的定义三、结束一、概述 跳跃表(skiplist)是一种有序数...
    99+
    2024-04-02
  • python实现跳表SkipList的示例代码
    跳表 跳表,又叫做跳跃表、跳跃列表,在有序链表的基础上增加了“跳跃”的功能,由William Pugh于1990年发布,设计的初衷是为了取代平衡树(比如红黑树)。 Redis、LevelDB 都是著名的 Key-Va...
    99+
    2022-06-02
    python 跳表SkipList python 跳表
  • go语言心跳超时的实现示例
    目录一、背景二、心跳超时的实现2.1 通过select case (设计概念比较多)2.2 通过time.sleep(简单有效)三、个人的实现观感一、背景 本文描述的是客户端接收心跳...
    99+
    2024-04-02
  • Java利用跳跃表解决双重队列问题详解
    目录一 问题描述二 输入三 输出四 输入和输出样例五 分析和设计六 代码七 测试一 问题描述 银行的每个客户都有一个正整数标识 K,到银行请求服务时将收到一个正整数的优先级...
    99+
    2022-12-28
    Java跳跃表解决双重队列 Java跳跃表 Java 双重队列
  • vue实现列表展示示例详解
    目录Vue 的CSS之deep语法::v-deepclassPrefix 前缀给元素绑定class总结Object.freeze关于Vue和ts的配合问题ISO8601和dayjs库...
    99+
    2024-04-02
  • JavaWeb实现表单提交的示例详解
    目录register.htmlRegisterServlet.java修改web.xml,添加如下code重新配置服务器先点击左侧图标再点击Redeploy,重新部署Tomcat服务...
    99+
    2024-04-02
  • go实现Redis读写分离示例详解
    目录我们为什么需要了解RESP协议?什么是RESP协议RESP协议规范如何使用该协议请求Redis使用go编写Redis中间件实现读写分离总结我们为什么需要了解RESP协议? 本篇文章目的为探究RESP协议,而非编写读写...
    99+
    2024-04-02
  • go smtp实现邮件发送示例详解
    目录smtp指令go demosdk中SendMail方法DialAndSend实现smtp指令 书接上文邮件实现详解,这里我们及我们简单复习一下smtp的指令如下: telnet ...
    99+
    2024-04-02
  • GO语言中Chan实现原理的示例详解
    目录GO 中 Chan 实现原理分享chan 是什么GO 中 Chan 的底层数据结构咱们来画个图看看dataqsiz 对应的环形队列是啥样的写 sendq和 读 recvq 等待队...
    99+
    2023-02-24
    GO语言Chan实现原理 GO语言Chan原理 GO语言Chan
  • GO语言中defer实现原理的示例详解
    目录GO 中 defer的实现原理defer 是什么defer 实现原理GO 中 defer 的规则第一点咱们来写个小DEMO第三点也来一个DEMO总结GO 中 defer的实现原理...
    99+
    2023-02-24
    GO语言defer实现原理 GO语言defer原理 GO defer
  • vue中详情跳转至列表页实现列表页缓存的示例分析
    小编给大家分享一下vue中详情跳转至列表页实现列表页缓存的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!提了一个需求,希...
    99+
    2024-04-02
  • 利用CSS3实现闪烁跳跃的进度条
    本篇内容主要讲解“利用CSS3实现闪烁跳跃的进度条”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“利用CSS3实现闪烁跳跃的进度条”吧!今天为大家带来一款具个性化...
    99+
    2024-04-02
  • Go语言实现彩色输出示例详解
    目录简介说明支持Linux彩色输出支持Windows彩色输出Golang IDE输出是不支持的使用CODE DEMO小结简介 在逛github时发现一个好玩的Go项目,彩色输出文本 ...
    99+
    2024-04-02
  • Go底层channel实现原理及示例详解
    目录概念:使用场景:底层数据结构:操作:创建发送接收关闭案例分析:概念: Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来...
    99+
    2024-04-02
  • Go定时器的三种实现方式示例详解
    目录SleepTimerTicker小结Sleep 很多时候需要周期性的执行某些操作,就需要用到定时器。定时器有三种思路。 本节源码位置 github.com/golang-min...
    99+
    2022-12-20
    Go定时器实现方式 Go定时器
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作