0、引言

正好做LC每日一题要求实现一个跳表,于是学习了redis的扩展skiplist,并使用Go进行复刻学习。学习参考了文章:Redis内部数据结构详解(6)——skiplist - 铁蕾的个人博客

因为作者能力有限,本文只是对跳表的核心功能:创建节点与跳表、插入节点、删除节点、获取节点rank、根据rank获取节点、获取分数区间的ele集合进行复刻,其余的需要自己去实现。

1、跳表核心结构

源码的数据结构定义如下:

#define ZSKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25 /* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode; typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
  • 定义了两个常量,一个是跳表的最大层数ZSKIPLIST_MAXLEVEL,一个是当前节点含有i+1层的概率ZSKIPLIST_P
  • 跳表节点zskiplistNode
    • ele,为string类型,存放的是节点的数据
    • score,存放数据对应的值
    • backward指向前一个跳表节点,只存在第一层链接中
    • level存放多层指向下一个节点的指针forawrd,同时含有一个span用于表示当前指针跨越了多少个节点,用于实现通过排名查询。注意,span是表示当前层,从header到当前节点跨过的指针数,它不包括指针的起始节点,但是包括终点节点。
  • 跳表本身zskiplist
    • headertail,指向跳表首尾的指针
    • length跳表总节点数
    • level跳表当前的层数

复刻:

package goskiplist

const (
SKIPLIST_MAXLEVEL = 32
SKIPLIST_P = 0.25
) type GskiplistLevel struct {
forward *GskiplistNode
span uint64
}
type GskiplistNode struct {
ele string
score float64
backward *GskiplistNode
level []GskiplistLevel
}
type Gskiplist struct {
header *GskiplistNode
tail *GskiplistNode
length uint64
level int
}

2、创建跳表节点与跳表

创建跳表节点源码:

zskiplistNode *zslCreateNode(int level, double score, sds ele) {
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
zn->score = score;
zn->ele = ele;
return zn;
}

复刻:

func createNode(level int, score float64, ele string) *GskiplistNode{
node := &GskiplistNode{
ele: ele,
score: score,
level: make([]GskiplistLevel, level),
backward: nil,
}
return node
}

创建跳表源码:

/* Create a new skiplist. */
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl; zsl = zmalloc(sizeof(*zsl));
zsl->level = 1;
zsl->length = 0;
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
zsl->tail = NULL;
return zsl;
}

初始化设置了跳表的层数为1、节点数为0、初始化头节点指针,分配内存。注意,头节点并不计算在length中。

经过初始化,创建的跳表如下:

3、向跳表插入节点

源码:

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned long rank[ZSKIPLIST_MAXLEVEL];
int i, level; serverAssert(!isnan(score));
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}
/* we assume the element is not already inside, since we allow duplicated
* scores, reinserting the same element should never happen since the
* caller of zslInsert() should test in the hash table if the element is
* already inside or not. */
level = zslRandomLevel();
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level;
}
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
} /* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
} x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
}

zslInsert主要实现了向跳表中插入一个节点,节点的值为ele,分数为score

解析:

(1)创建数组与断言检查

zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned long rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
x = zsl->header;
  • *update[]用于记录每一层插入的位置,update[i]表示节点在第i层,应该插入在update[i]节点之后。
  • rank[]用于记录每一层的跨度,rank[i]表示从第i层,跳到update[i]节点的跨度。使用了前缀和的思想。
  • *x用于节点的遍历
  • serverAssert用于判断数值是否异常

(2)查找插入位置

for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}

i从当前跳表的最高层向下遍历。在每一次遍历中:

  • rank[i]初始赋值上一层的结果,若为最高层则赋值0
  • 若当前层的当前节点存在下一节点,并且分数<新节点分数(从小到大排序)或者分数相同但字典序要小,则累加下一步的跨度,并且移动结点至下一结点。
  • 找到当前层应该插入的位置后,记录这个结点。

加入目前跳表结构如下:

我们想要插入的新节点ele为“e”,score为“75”。那么经过更新后:

  • rank[1] = 3,update[1] = c
  • rank[0] = 3,update[0] = c

(3)设定新节点最大层数

level = zslRandomLevel();
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level;
}

使用zslRandomLevel函数设定新节点的最高层数。如果这个最高层数大于目前跳表的层数,那么就需要设定新高层的rank和update。

zslRandomLevel的实现如下:

int zslRandomLevel(void) {
static const int threshold = ZSKIPLIST_P*RAND_MAX;
int level = 1;
while (random() < threshold)
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

通过将浮点数映射至整数,可以加快运算效率。

假设我们要插入的(“e”,75)节点生成的层数为3,经历上述操作后,跳表结构如下:

(4)插入新节点和更新跨度

x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}

调整每一层的要插入的位置的前一个节点的指针指向,并且更新span。

假设在第i层,我们称update[i]pre,未更新前pre的下一个节点未next,那么因为要在pre和next之间插入新的节点,更新pre的span为pre到next的距离-cur到next的距离。更新cur的span为cur到next的距离。

第二个循环是为了更新当前节点的更高层未更新节点的span值。

经过这一次调整,如图:

这里我画图用于形象的表示span的计算过程,它采用了前缀和的方式:

(5)更新新节点的前指针

x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;

如果update[0]不是头节点,那么它就是x的前一个节点。如果x的后节点存在,则更新x的后节点的前指针指向x,否则x是末尾节点,让tail指向它。

复刻Go源码:

// 向跳表插入一个节点,同时返回插入好的节点。
// ele不能为空串,否则返回nil。
func (this *Gskiplist) Insert(score float64, ele string) *GskiplistNode {
if ele == "" {
return nil
}
update := make([]*GskiplistNode, SKIPLIST_MAXLEVEL)
rank := make([]uint64, SKIPLIST_MAXLEVEL)
var x *GskiplistNode x = this.header
//更新update以及rank
for i := this.level - 1; i >= 0; i-- {
rank[i] = 0
if i != this.level-1 {
rank[i] = rank[i+1]
}
for x.level[i].forward != nil &&
(x.level[i].forward.score < score ||
(x.level[i].forward.score == score && x.level[i].forward.ele < ele)) {
rank[i] += x.level[i].span
x = x.level[i].forward
}
update[i] = x
}
level := this.randomLevel()
//更新最大层数
if level > this.level {
for i := this.level; i < level; i++ {
rank[i] = 0
update[i] = this.header
update[i].level[i].span = this.length
}
this.level = level
}
x = createNode(level, score, ele) //插入操作
for i := 0; i < level; i++ {
x.level[i].forward = update[i].level[i].forward
update[i].level[i].forward = x
//更新x和前一个节点的span
x.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 < this.level; i++ {
update[i].level[i].span++
} //更新前节点指针指向
x.backward = nil
if update[0] != this.header {
x.backward = update[0]
}
if x.level[0].forward != nil {
x.level[0].forward.backward = x
} else {
this.tail = x
}
this.length++
return x
}

4、删除跳表节点

void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {
update[i]->level[i].span -= 1;
}
}
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
zsl->length--;
} int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i; x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
x = x->level[i].forward;
}
update[i] = x;
}
/* We may have multiple elements with the same score, what we need
* is to find the element with both the right score and object. */
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
zslDeleteNode(zsl, x, update);
if (!node)
zslFreeNode(x);
else
*node = x;
return 1;
}
return 0; /* not found */
}

先来看zslDelete:它是删除节点的最上层,update的更新方法与插入一致。接着就是删除score和ele相同的节点,其中node参数用于提供保存删除节点的作用。在Go语言的复刻中,我们可以直接返回node和是否删除成功。

再看zslDeleteNode,它是删除节点的下游具体实现,具体细节如下:

  • 逐层删除x,如果当前层有x,则需要将前一个节点的后指针指向x的后指针,然后更新前一个节点的span;否则只用更新span
  • 如果x的后节点存在,则更新后节点的backward指针,否则修改跳表的tail。
  • 如果存在高层,在删除x后为空层,要修改跳表的层数。
  • 减去一个length

Go复刻如下:

//删除节点,返回这个节点以及是否成功
func (this *Gskiplist) Delete(score float64, ele string) (*GskiplistNode, bool) {
update := make([]*GskiplistNode, SKIPLIST_MAXLEVEL)
var x *GskiplistNode x = this.header
for i := this.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.ele < ele)) {
x = x.level[i].forward
}
update[i] = x
} x = x.level[0].forward
//从底层删除
if x != nil && x.score == score && x.ele == ele {
this.deleteNode(x, update)
return x, true
}
//未找到对应节点
return nil, false
} func (this *Gskiplist) deleteNode(x *GskiplistNode, update []*GskiplistNode) {
for i := 0; i < this.level; i++ {
if update[i].level[i].forward == x {
//在这一层,存在x
update[i].level[i].span += x.level[i].span - 1
update[i].level[i].forward = x.level[i].forward
} else {
//不存在则只更新span
update[i].level[i].span--
}
}
if x.level[0].forward != nil {
x.level[0].forward.backward = x.backward
} else {
this.tail = x.backward
} //若x独占高层,需要逐个清除
for this.level > 1 && this.header.level[this.level-1].forward == nil {
this.level--
}
this.length--
}

5、获取节点的rank

unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
zskiplistNode *x;
unsigned long rank = 0;
int i; x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) <= 0))) {
rank += x->level[i].span;
x = x->level[i].forward;
} /* x might be equal to zsl->header, so test if obj is non-NULL */
if (x->ele && x->score == score && sdscmp(x->ele,ele) == 0) {
return rank;
}
}
return 0;
}

从高层逐个寻找,找到即返回。

6、根据排名获取节点

/* Finds an element by its rank from start node. The rank argument needs to be 1-based. */
zskiplistNode *zslGetElementByRankFromNode(zskiplistNode *start_node, int start_level, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i; x = start_node;
for (i = start_level; i >= 0; i--) {
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].forward;
}
if (traversed == rank) {
return x;
}
}
return NULL;
} /* Finds an element by its rank. The rank argument needs to be 1-based. */
zskiplistNode *zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
return zslGetElementByRankFromNode(zsl->header, zsl->level - 1, rank);
}

Go复刻:

// 根据排名获取节点
func (this *Gskiplist) GetElementByRank(rank uint64) *GskiplistNode {
return this.getElementByRankFromNode(this.header, this.level-1, rank)
}
func (this *Gskiplist) getElementByRankFromNode(startNode *GskiplistNode, startLevel int, rank uint64) *GskiplistNode {
x := startNode
var traversed uint64
for i := startLevel; i >= 0; i-- {
for x.level[i].forward != nil && traversed+x.level[i].span <= rank {
traversed += x.level[i].span
x = x.level[i].forward
}
//遍历完一层,查看是否到达
if traversed == rank {
return x
}
}
return nil
}

7、根据分数区间获取数据集合

现在,我们能很轻易的实现根据分数区间获取数据集合的功能。

// 根据分数区间获取数据集合,返回数据的ele集合
func (this *Gskiplist) GetElementsRangeByScore(low float64, high float64) (ans []string) {
x := this.header
var i int
for i = this.level - 1; i >= 0; i-- {
for x.level[i].forward != nil && x.level[i].forward.score < low {
x = x.level[i].forward
}
}
x = x.level[0].forward
for x != nil && x.score <= high {
ans = append(ans, x.ele)
x = x.level[0].forward
}
return ans
}

到这里为止,对skiplist的核心功能就复刻完成了,剩余的根据需要可以自己探索。

使用Go复刻skiplist核心功能的更多相关文章

  1. Shiro 核心功能案例讲解 基于SpringBoot 有源码

    Shiro 核心功能案例讲解 基于SpringBoot 有源码 从实战中学习Shiro的用法.本章使用SpringBoot快速搭建项目.整合SiteMesh框架布局页面.整合Shiro框架实现用身份认 ...

  2. 复刻smartbits的国产网络测试工具minismb简介

           复刻smartbits的国产网络性能测试工具minismb,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此工具测试任何ip网络设备的端口吞吐率,带 ...

  3. 复刻smartbits的国产网络测试工具minismb功能特点-如何加载、发送PCAP数据包

    复刻smartbits的网络性能测试工具minismb,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此以太网测试工具测试任何ip网络设备的端口吞吐率,带宽,并发 ...

  4. 复刻smartbits的国产网络测试工具minismb-如何测试协议限速

    复刻smartbits的网络性能测试工具MiniSMB,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此工具测试任何ip网络设备的端口吞吐率,带宽,并发连接数和最 ...

  5. 复刻smartbits的国产网络测试工具minismb-网络连接数测试方法

    复刻smartbits的网路性能测试工具MiniSMB,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此工具测试任何ip网络设备的端口吞吐率,带宽,并发连接数和最 ...

  6. 复刻smartbits的国产网络测试工具minismb-如何测试ip限速

    复刻smartbits的网路性能测试工具MiniSMB,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此工具测试任何ip网络设备的端口吞吐率,带宽,并发连接数和最 ...

  7. 161_可视化_Power BI 复刻 GitHub 贡献热力图

    161_可视化_Power BI 复刻 GitHub 贡献热力图 一.背景 在 GitHub 上,有用户的贡献度的热力图如下: Power BI 公共 web 效果:https://demo.jiao ...

  8. 带连接池的netty客户端核心功能实现剖解

    带连接池的netty客户端核心功能实现剖析 带连接池的netty的客户端核心功能实现剖析 本文为原创,转载请注明出处 源码地址: https://github.com/zhangxianwu/ligh ...

  9. Chrome扩展开发之四——核心功能的实现思路

    目录: 0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述 1.Chrome扩展开发之一——Chrome扩展的文件结构 2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制 ...

  10. ES6,ES2105核心功能一览,js新特性详解

    ES6,ES2105核心功能一览,js新特性详解 过去几年 JavaScript 发生了很大的变化.ES6(ECMAScript 6.ES2105)是 JavaScript 语言的新标准,2015 年 ...

随机推荐

  1. P11378[GESP202412 七级]燃烧 题解

    闲话 花了一个小时. 主要原因:条初始值硬控我半小时,题目看错硬控我半小时(悲). 正文 看题目,就是求从哪个点出发所得到的所有单调下降序列的总长度最长(这个描述好奇怪,不过意思是对的). 题目中说的 ...

  2. 编写bash脚本快速kill或启动tomcat

    ​假设tomcat安装路径为 /home/tomcat,示例如下: 1. kill tomcat进程 vim kill-tomcat-force.sh set fileformat=unix path ...

  3. Web components vs. React

    Web components vs. React - LogRocket Blog Web Components + Compose 是一条更好的路线. 当然,像 molecule 一样用 HTML ...

  4. 【PHP】读取本地文件夹中所有图片并显示

    <? //获取文件夹下的所有文件 $dir_str = ''; $imgType = array('gif','png','jpg','jpeg','bmp'); $handle = opend ...

  5. nexus如何上传自己的依赖包

    一.创建第三方包仓库创建第三方jar包的仓库选用hosted 取名为nexus-3rd 然后再public组中加入nexus-3rd,交给public管理 二.创建用户仓库创建完成以后可以创建一个管理 ...

  6. 基于Three.js的大屏3D地图(一)

    依赖安装 yarn add three yarn add @types/three yarn add d3-geo three库安装后在node_modules下其还包含核心three/src和插件t ...

  7. Anaconda下载安装及Spyder使用Python

    第一:选择编程语言, C++ Java Python等,量化交易广泛使用Python开发策略. 第二:选择软件,Anaconda 或者 PyCharm,龙哥选用Anaconda,因为 Anaconda ...

  8. Qt/C++音视频开发65-切换声卡/选择音频输出设备/播放到不同的声音设备/声卡下拉框

    一.前言 近期收到一个用户需求,要求音视频组件能够切换声卡,首先要在vlc上实现,于是马不停蹄的研究起来,马上查阅对应vlc有没有自带的api接口,查看接口前,先打开vlc播放器,看下能不能切换,因为 ...

  9. Qt开源作品24-遮罩层窗体

    一.前言 在有些项目中,需要在弹框的窗体背后遮罩原有主窗体,使得突出显示弹窗窗体,突然想到之前写过一个全局截屏的东东,原理一致,拿来改改.只需要引入一个头文件和实现文件,然后在主窗体中设置下需要遮罩的 ...

  10. Docker安装Redis并设置密码

    1. 镜像下载 docker pull redis 2. 创建目录 mkdir -p /etc/redis/redis.conf mkdir -p /data/redis 3. 启动容器 docker ...