前言
上篇文章我们研究了Redis 压缩列表ziplist数据结构的实现原理, 并深入分析了其针对性的优化手段。本篇我们研究一下另一个数据结构跳跃表, 即skiplist。关于跳跃表原理实现相对比较简单, 想必大家都有所了解。本篇我们简要分析Redis中的调表实现, 重点自己实现一版跳跃表数据结构, 以此加深对于跳跃表的认知。
skiplist简介
跳跃表是一种随机的数据结构, 相比于普通单链表通过增加层级的指针指向, 能够更加快速的定位元素。核心思想类似于二分查找。查找和插入操作都可以在对数阶复杂度下完成。
Redis跳跃表实现
核心定义
数据定义在src/server.h
中, 定义如下:
节点定义如下:
可以看到节点定义中增加了后退节点的指针, 相比于典型的跳跃表定义, Redis实现做了有如下改动:
- 允许
score
重复, 即多个不同的member
的score
值可以相同。进行比对操作时, 需要检查score
和member
域。 - 每个节点增加一个
backward
指针, 层高为1, 当执行ZREVRANGE
和ZREVRANGEBYSCORE
需要逆序处理的指令是会使用到相关的字段。
核心api
api定义实现在src/t_zset.c
下。相关api实现以及时间复杂度如下:
函数 | 作用 | 复杂度 |
---|---|---|
zslCreateNode | 创建并返回一个新的跳跃表节点 | 最坏 O(1) |
zslFreeNode | 释放给定的跳跃表节点 | 最坏 O(1) |
zslCreate | 创建并初始化一个新的跳跃表 | 最坏 O(1) |
zslFree | 释放给定的跳跃表 | 最坏 O(N) |
zslInsert | 将一个包含给定 score 和 member 的新节点添加到跳跃表中 | 最坏 O(N)平均 O(logN) |
zslDeleteNode | 删除给定的跳跃表节点 | 最坏 O(N) |
zslDelete | 删除匹配给定 member 和 score 的元素 | 最坏 O(N) 平均 O(logN) |
zslFirstInRange | 找到跳跃表中第一个符合给定范围的元素 | 最坏 O(N) 平均 O(logN) |
zslLastInRange | 找到跳跃表中最后一个符合给定范围的元素 | 最坏 O(N) 平均 O(logN) |
zslDeleteRangeByScore | 删除 score 值在给定范围内的所有节点 | 最坏 O(N^2) |
zslDeleteRangeByRank | 删除给定排序范围内的所有节点 | 最坏 O(N^2) |
zslGetRank | 返回目标元素在有序集中的排位 | 最坏 O(N)平均 O(logN) |
zslGetElementByRank | 根据给定排位,返回该排位上的元素节点 | 最坏 O(N)平均 O(logN) |
golang版本实现
结构定义
const (
maxLevel = 8 // 最大层高
levelProbability = 0.5 // 新增层高概率定义
)
// 元素
type Ele struct {
Score float64
Value interface{}
NextEleList []*Ele
}
type SkipList struct {
header *Ele
len int64 // 元素长度
level int64 // 跳表的层高
}
查找元素
// 查找
func (spl *SkipList) SearchByScore(score float64) *Ele {
// 查找当前score前一个节点元素
preEle := spl.header
for i := spl.level - 1; i >= 0; i-- {
for preEle.NextEleList[i] != nil && preEle.NextEleList[i].Score < score {
preEle = preEle.NextEleList[i]
}
}
// 当前节点 = 前一个节点的第一层指向
curEle := preEle.NextEleList[0]
if curEle != nil && curEle.Score == score {
return curEle
}
return nil
}
新增元素
// 插入
func (spl *SkipList) Insert(score float64, value interface{}) *Ele {
// 查找当前score的前一个节点
TraversedEle := make([]*Ele, maxLevel)
preEle := spl.header
for i := spl.level - 1; i >= 0; i-- {
for preEle.NextEleList[i] != nil && preEle.NextEleList[i].Score < score {
preEle = preEle.NextEleList[i]
}
TraversedEle[i] = preEle // 记录遍历过的节点
}
preEle = preEle.NextEleList[0] // 最近前一个节点的下一个节点
// 如果当前节点已经相同, 则更新值
if preEle != nil && preEle.Score == score {
preEle.Value = value
return preEle
}
// 维护层高
level := randLevel()
if level > spl.level {
level = spl.level + 1 // 超出层高需要
TraversedEle[spl.level] = spl.header
spl.level = level
}
e := NewEle(score, value, level)
for i := 0; int64(i) < level; i++ {
// 新元素的 i 层指向上一个节点的 i 层
e.NextEleList[i] = TraversedEle[i].NextEleList[i]
// 上一个节点的 i 层 指向当前元素
TraversedEle[i].NextEleList[i] = e
}
spl.len++
return e
}
删除元素
// 删除
func (spl *SkipList) Remove(ele *Ele) bool {
// 查找当前节点最近一个前节点
travedEle := make([]*Ele, maxLevel)
preEle := spl.header
for i := spl.level - 1; i >= 0; i-- {
for preEle.NextEleList[i] != nil && preEle.NextEleList[i].Score < ele.Score {
preEle = preEle.NextEleList[i]
}
travedEle[i] = preEle
}
// 当前节点 = 最近一个节点的0层指向
curEle := preEle.NextEleList[0]
if curEle != nil && curEle.Score == ele.Score {
for i := 0; int64(i) < spl.level; i++ {
if travedEle[i].NextEleList[i] != curEle {
return false
}
// 前一个节点指向当前节点的下一个元素
travedEle[i].NextEleList[i] = curEle.NextEleList[i]
}
spl.len--
}
return true
}
总结
本节我们复习了跳跃表的实现, 跳跃表在数据库的实现中应用比较广泛, 如Redis, HBase都有使用,理解上相对也比较简单, 希望看到的小伙伴们可以点个赞 :)