跳跃表(skiplist )详解及其C++编程实现

跳跃表(skiplist )详解及其C++编程实现跳表SkipList跳表SkipList1、背景2、定义2.1、SkipList基本数据结构及其实现3、实现4、使用方法4.1、跳表的创建4.2、跳表插入操作参考跳表SkipList1、背景为什么选择跳表?目前经常使用的平衡数据结构有:B树,红黑树,AVL树,SplayTree,Treep等。跳表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。用跳表吧,跳表是一种随机化的数据结构,目前开源软件

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

跳表SkipList

1、背景

为什么选择跳表?

目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。跳表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。

用跳表吧,跳表是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,它的效率和红黑树以及 AVL 树不相上下,但跳表的原理相当简单,只要你能熟练操作链表,就能轻松实现一个 SkipList。

Redis为什么用跳表而不用平衡树?

2、定义

漫画:什么是跳表?

跳表(Skiplist)是一个特殊的链表,相比一般的链表,有更高的查找效率,可比拟二叉查找树,平均期望的查找、插入、删除时间复杂度都是O(logn),许多知名的开源软件(库)中的数据结构均采用了跳表这种数据结构。

  • Redis中的有序集合zset
  • LevelDB、RocksDB、HBase中Memtable
  • ApacheLucene中的TermDictionary、Posting List

我们先来看一下单向链表如何实现查找
在这里插入图片描述
假设我们有个有序链表,可知其查询和插入复杂度都为 O(n)。相比数组,链表不能进行二分查找的原因在于,不能用下标索引进行常数复杂度数据访问,从而不能每次每次快速的筛掉现有规模的一半。那么如何改造一下链表使之可以进行二分?

利用 map 构造一个下标到节点的映射?这样虽然可以进行二分查询了,但是每次插入都会引起后面所有元素的下标变动,从而需要在 map 中进行 O(n) 的更新。

增加指针使得从任何一个节点都能直接跳到其他节点?那得构造一个全连接图,指针耗费太多空间不说,每次插入指针的更新仍是 O(n) 的。

跳表给出了一种思路,跳步采样,构建索引,逐层递减。怎么提高查询效率呢?如果我们给该单链表加一级索引,将会改善查询效率。
在这里插入图片描述
如图所示,当我们每隔一个节点就提取出来一个元素到上一层,把这一层称作索引,其中的down指针指向原始链表。

当我们查找元素16的时候,单链表需要比较10次,而加过索引的两级链表只需要比较7次。当数据量增大到一定程度的时候,效率将会有显著的提升。

如果我们再加多几级索引的话,效率将会进一步提升。这种链表加多级索引的结构,就叫做跳表。

在这里插入图片描述

这基本上就是跳表的核心思想,其实是一种通过“空间来换取时间”的一个算法,通过在每个节点中增加了向前的指针(即层),从而提升查找的效率。

  • 跳跃列表是按层建造的。
  • 底层是一个普通的有序链表。每个更高层都充当下面列表的「快速跑道」,这里在层 i 中的元素按某个固定的概率 p
    (通常为0.5或0.25)出现在层 i+1 中。
  • 平均起来,每个元素都在 1/(1-p) 个列表中出现, 而最高层的元素(通常是在跳跃列表前端的一个特殊的头元素),在 O(log1/p n) 个列表中出现。

2.1、SkipList基本数据结构及其实现

一个跳表,应该具有以下特征:

  • 1,一个跳表应该有几个层(level)组成;
  • 2,跳表的第一层包含所有的元素;
  • 3,每一层都是一个有序的链表;
  • 4,如果元素x出现在第i层,则所有比i小的层都包含x;
  • 5,每个节点包含key及其对应的value和一个指向同一层链表的下个节点的指针数组

跳表可视为水平排列(Level)垂直排列(Tower)的位置(Position,对Entry的访问抽象)的二维集合。每个Level是一个列表Si每个Tower包含存储连续列表中相同Entry的位置,跳表的各个位置可以通过如下方式进行遍历。

  • After(P):返回和P在同一Level的后面的一个位置,若不存在则返回NULL。

  • Before(P):返回和P在同一Level的前面的一个位置,若不存在则返回NULL。

  • Below(P):返回和P在同一Tower的下面的一个位置,若不存在则返回NULL。

  • Above(P):返回和P在同一Tower的上面的一个位置,若不存在则返回NULL。

    在这里插入图片描述
    有顺序关系的多个Entry(K,V)集合M可以由跳表实现,跳表S由一系列列表{S0,S1,…,Sh}组成,

  • 其中h代表跳表的高度

  • 每个列表Si按Key顺序存储M项的子集,并额外增加两个特殊键,分别是-∞和+∞,其中-∞小于M中的每个键+∞大于M中的每个键

此外,S中的列表满足以下要求(不同实现版本要求会有不同)。

  • 列表S0包含集合M的每个Entry(加上带有键-∞和+∞的特殊Entry)。
  • 对于i=1,…,h−1,列表Si包含(包括-∞和+∞)列表Si−1中Entry的随机生成子集。
  • 列表Sh只包含-∞和+∞

Si中的Entry是从Si-1中的Entry集合中随机选择的,**对于Si-1中的每个Entry**,以1/2概率来决定是否需要拷贝到Si中,我们期望S1有大约n/2个Entry,S2有大约n/4个Entry,Si 有n/2 ^i

跳表的高度h大约是logn。从一个列表到下一个列表的Entry数减半并不是跳表的强制性要求,相反借助随机函数来达成。

3、实现

跳表基本数据结构

定义跳表数据类型:

//跳表结构
typedef struct skip_list
{ 
   
    int level;// 层数
    Node *head;//指向头结点
} skip_list;

其中level是当前跳表最大层数,head是指向跳表的头节。

跳表的每个节点的数据结构:

typedef struct node
{ 
   
    keyType key;// key值
    valueType value;// value值
    struct node *next[1];// 后继指针数组,柔性数组 可实现结构体的变长
} Node;

在这里插入图片描述
上面的每个结构体对应着图中的每个节点,如果一个节点是一层的节点的话(如7,12等节点),那么对应的next将指向一个只含一个元素的数组,以此类推。

对于这个结构体重点说说,struct node *next[1] 其实它是个柔性数组,主要用于使结构体包含可变长字段。我们可以通过如下方法得到包含可变

层数(n)的Node *类型的内存空间:

#define new_node(n)((Node*)malloc(sizeof(Node)+n*sizeof(Node*)))

通过上面我们可以根据层数n来申请指定大小的内存,从而节省了不必要的内存空间(比如固定大小的next数组就会浪费大量的内存空间)。

4、使用方法

4.1、跳表的创建

// 创建节点
Node *create_node(int level, keyType key, valueType val)
{ 
   
    Node *p=new_node(level);
    if(!p)
        return NULL;
    p->key=key;
    p->value=val;
    return p;
}

跳表的创建

列表的初始化需要初始化头部,并使头部每层(根据事先定义的MAX_LEVEL)指向末尾(NULL)

//创建跳跃表
skip_list *create_sl()
{ 
   
    skip_list *sl=(skip_list*)malloc(sizeof(skip_list));//申请跳表结构内存
    if(NULL==sl)
        return NULL;
 
    sl->level=0;// 设置跳表的层level,初始的层为0层(数组从0开始)
 
    Node *h=create_node(MAX_L-1, 0, 0);//创建头结点
    if(h==NULL)
    { 
   
        free(sl);
        return NULL;
    }
    sl->head = h;
    int i;
	 // 将header的next数组清空
    for(i=0; i<MAX_L; ++i)
    { 
   
        h->next[i] = NULL;
    }
	srand(time(0));
    return sl;
}

4.2、跳表查找操作

目的:在跳跃表中查找一个元素x
在跳跃表中查找一个元素x,按照如下几个步骤进行:

  • 1、从最上层的链(Sh)的开头开始

  • 2、假设当前位置为p,它向右指向的节点为q(p与q不一定相邻),且q的值为y。将y与x作比较

       (1) x=y  输出查询成功及相关信息
       (2) x>y  从p向右移动到q的位置
       (3) x<y  从p向下移动一格
    
  • 3、如果当前位置在最底层的链中(S0),且还要往下移动的话,则输出查询失败

valueType *search(skip_list *sl, keyType key)
{ 
   
    Node *q,*p=sl->head;
	q=NULL;
    int i=sl->level-1;
    for(; i>=0; --i)
    { 
   
        while((q=p->next[i]) && q->key<key)
        { 
   
            p=q;
        }
        if(q && key==q->key)
            return &(q->value);
    }
    return NULL;
}

4.3、跳表插入操作

1、目的:向跳跃表中插入一个元素x

2、首先明确,向跳跃表中插入一个元素,相当于在表中插入一列从S0中某一位置出发向上的连续一段元素。有两个参数需要确定,即插入列的位置以及它的“高度”。

3、 关于插入的位置,我们先利用跳跃表的查找功能,找到比x小的最大的数y。根据跳跃表中所有链均是递增序列的原则,x必然就插在y的后面。

4、跳表是一种随机化数据结构,其随机化体现在插入元素的时候元素所占有的层数完全是随机的。而插入列的“高度”较前者来说显得更加重要。为了使插入数据之后,保持该数据结构进行各种操作均为O(logn)复杂度的性质,我们引入随机化算法(Randomized Algorithms)。

  • 相当与做一次丢硬币的实验,如果遇到正面(rand产生奇数),继续丢,遇到反面,则停止,用实验中丢硬币的次数level作为元素占有的层数。
  • 显然随机变量 level 满足参数为 p = 1/2 的几何分布level 的期望值 E[level] = 1/p = 2.
    就是说,各个元素的层数,期望值是 2 层。

随机决策过程:

  • 1、产生一个0到1的随机数r , r ← random()

  • 2、如果r小于一个常数p,则执行方案A, if r<p then do A,否则,执行方案B else do B

  • 3、初始时列高为1。插入元素时,不停地执行随机决策模块。如果要求执行的是A操作,则将列的高度加1,并且继续反复执行随机决策模块。直到第i次,模块要求执行的是B操作,我们结束决策,并向跳跃表中插入一个高度为i的列。

//插入元素的时候元素所占有的层数完全是随机算法
int randomLevel()
{ 
   
  int level=1;
  while (rand()%2)
      level++;
  level=(MAX_L>level)? level:MAX_L;
  return level;
}

由于跳表数据结构整体上是有序的,所以在插入时,需要首先查找到合适的位置,然后就是修改指针(和链表中操作类似),然后更新跳表的level变量。 跳表的插入总结起来需要三步:

  • 1:查找到待插入位置, 每层跟新update数组;
  • 2:需要随机产生一个层数;
  • 3:从高层至下插入,与普通链表的插入完全相同;

比如插入key为25的节点,如下图

  • 对于步骤1,我们需要对于每一层进行遍历并保存这一层中下降的节点(其后继节点为NULL或者后继节点的key大于等于要插入的key),如下图,节点中有白色星花标识的节点保存到update数组。
  • 对于步骤2我们上面已经说明了是通过一个随机算法产生一个随机的层数,但是当这个随机产生的层数level大于当前跳表的最大层数时,我们此时需要更新当前跳表最大层数到level之间的update内容,这时应该更新其内容为跳表的头节点head。然后就是更新跳表的最大层数。

在这里插入图片描述

  • 对于步骤3就和普通链表插入一样了,只不过现在是对每一层链表进行插入节点操作。最终的插入结果如图所示,因为新插入key为25的节点level随机数值为4,大于插入前的最大层数,所以此时跳表的层数为4。
    在这里插入图片描述

实现代码如下:

//跳表的插入需要三个步骤,第一步需要查找到在每层待插入位置,然后需要随机产生一个层数,最后就是从高层至
//下插入,插入时算法和普通链表的插入完全相同。 
bool insert(skip_list *sl, keyType key, valueType val)
{ 

Node *update[MAX_L];
Node *q=NULL,*p=sl->head;//q,p初始化
int i=sl->level-1;
/******************step1*******************/
//从最高层往下查找需要插入的位置,并更新update
//即把降层节点指针保存到update数组
for( ; i>=0; --i)
{ 

while((q=p->next[i])&& q->key<key)
p=q;
update[i]=p;
}
if(q && q->key == key)//key已经存在的情况下
{ 

q->value = val;
return true;
}
/******************step2*******************/
//产生一个随机层数level
int level = randomLevel();
//如果新生成的层数比跳表的层数大
if(level>sl->level)
{ 

//在update数组中将新添加的层指向header
for(i=sl->level; i<level; ++i)
{ 

update[i]=sl->head;
}
sl->level=level;
}
//printf("%d\n", sizeof(Node)+level*sizeof(Node*));
/******************step3*******************/
//新建一个待插入节点,一层一层插入
q=create_node(level, key, val);
if(!q)
return false;
//逐层更新节点的指针,和普通链表插入一样
for(i=level-1; i>=0; --i)
{ 

q->next[i]=update[i]->next[i];
update[i]->next[i]=q;
}
return true;
}

4.4、跳表 删除操作

删除节点操作和插入差不多,找到每层需要删除的位置,删除时和操作普通链表完全一样。不过需要注意的是,如果该节点的level是最大的,则需要更新跳表的level。

删除操作分为以下三个步骤:

  • 在跳跃表中查找到这个元素的位置,如果未找到,则退出
  • 将该元素所在整列从表中删除
  • 将多余的“空链”删除
    在这里插入图片描述
    删除节点操作和插入差不多,找到每层需要删除的位置,删除时和操作普通链表完全一样。不过需要注意的是,如果该节点的level是最大的,则需要更新跳表的level。
bool erase(skip_list *sl, keyType key)
{ 

Node *update[MAX_L];
Node *q=NULL, *p=sl->head;
int i = sl->level-1;
for(; i>=0; --i)
{ 

while((q=p->next[i]) && q->key < key)
{ 

p=q;
}
update[i]=p;
}
//判断是否为待删除的key
if(!q || (q&&q->key != key))
return false;
//逐层删除与普通链表删除一样
for(i=sl->level-1; i>=0; --i)
{ 

if(update[i]->next[i]==q)//删除节点
{ 

update[i]->next[i]=q->next[i];
//如果删除的是最高层的节点,则level--
if(sl->head->next[i]==NULL)
sl->level--;
}
}
free(q);
q=NULL;
return true;
}

4.5、跳表的销毁

上面分别介绍了跳表的创建、节点插入、节点删除,其中涉及了内存的动态分配,在使用完跳表后别忘了释放所申请的内存,不然会内存泄露的。

// 释放跳跃表
void sl_free(skip_list *sl)
{ 

if(!sl)
return;
Node *q=sl->head;
Node *next;
while(q)
{ 

next=q->next[0];
free(q);
q=next;
}
free(sl);
}

4.6、完整代码

skiplist.h

//参考:Skip lists: a probabilistic alternative to balanced trees 
#ifndef _SKIPLIST_H_
#define _SKIPLIST_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define MAX_L 16 //最大层数 
//new_node生成一个Node结构体,同时生成包含n个Node *元素的数组
#define new_node(n) ((Node*)malloc(sizeof(Node)+n*sizeof(Node*)))
//定义key和value的类型 
typedef int keyType;
typedef int valueType;
//每个节点的数据结构
typedef struct node
{ 

keyType key;// key值
valueType value;// value值
struct node *next[1];// 后继指针数组,柔性数组 可实现结构体的变长
} Node;
//跳表结构
typedef struct skip_list
{ 

int level;// 最大层数
Node *head;//指向头结点
} skip_list;
/* 创建节点,成功返回Node*类型指针,否则返回NULL level 节点层数 key 节点关键字 vlal 节点的值 */
Node *create_node(int level, keyType key, valueType val);
/* 创建跳跃表,成功返回skip_list*类型指针,否则返回NULL */
skip_list *create_sl();
/* 插入元素的时候元素所占有的层数完全是随机算法,返回随机层数 */
int randomLevel();
/* 插入节点,插入成功返回true,否则返回false sl 跳表指针 key 节点关键字 vlal 节点的值 */
bool insert(skip_list *sl, keyType key, valueType val);
/* 删除节点,成功返回true,否则返回false sl 跳表指针 key 节点关键字 */
bool erase(skip_list *sl, keyType key);
/* 查找节点,成功返回valueT*类型的指针,否则返回NULL sl 跳表指针 key 节点关键字 */
valueType *search(skip_list *sl, keyType key);
/* 从最高层开始逐层打印 sl 跳表指针 */
void print_sl(skip_list *sl);
/* 释放跳跃表 sl 跳表指针 */
void sl_free(skip_list *sl);
#endif

skiplist.cpp

#include "skiplist.h"
// 创建节点
Node *create_node(int level, keyType key, valueType val)
{ 

Node *p=new_node(level);
if(!p)
return NULL;
p->key=key;
p->value=val;
return p;
}
//创建跳跃表
skip_list *create_sl()
{ 

skip_list *sl=(skip_list*)malloc(sizeof(skip_list));//申请跳表结构内存
if(NULL==sl)
return NULL;
sl->level=0;// 设置跳表的层level,初始的层为0层(数组从0开始)
Node *h=create_node(MAX_L-1, 0, 0);//创建头结点
if(h==NULL)
{ 

free(sl);
return NULL;
}
sl->head = h;
int i;
// 将header的next数组清空
for(i=0; i<MAX_L; ++i)
{ 

h->next[i] = NULL;
}
srand(time(0));
return sl;
}
//插入元素的时候元素所占有的层数完全是随机算法
int randomLevel()
{ 

int level=1;
while (rand()%2)
level++;
level=(MAX_L>level)? level:MAX_L;
return level;
}
/* step1:查找到在每层待插入位置,跟新update数组 step2:需要随机产生一个层数 step3:从高层至下插入,与普通链表的插入完全相同。 */
bool insert(skip_list *sl, keyType key, valueType val)
{ 

Node *update[MAX_L];
Node *q=NULL,*p=sl->head;//q,p初始化
int i=sl->level-1;
/******************step1*******************/
//从最高层往下查找需要插入的位置,并更新update
//即把降层节点指针保存到update数组
for( ; i>=0; --i)
{ 

while((q=p->next[i])&& q->key<key)
p=q;
update[i]=p;
}
if(q && q->key == key)//key已经存在的情况下
{ 

q->value = val;
return true;
}
/******************step2*******************/
//产生一个随机层数level
int level = randomLevel();
//如果新生成的层数比跳表的层数大
if(level>sl->level)
{ 

//在update数组中将新添加的层指向header
for(i=sl->level; i<level; ++i)
{ 

update[i]=sl->head;
}
sl->level=level;
}
//printf("%d\n", sizeof(Node)+level*sizeof(Node*));
/******************step3*******************/
//新建一个待插入节点,一层一层插入
q=create_node(level, key, val);
if(!q)
return false;
//逐层更新节点的指针,和普通链表插入一样
for(i=level-1; i>=0; --i)
{ 

q->next[i]=update[i]->next[i];
update[i]->next[i]=q;
}
return true;
}
// 删除节点
bool erase(skip_list *sl, keyType key)
{ 

Node *update[MAX_L];
Node *q=NULL, *p=sl->head;
int i = sl->level-1;
for(; i>=0; --i)
{ 

while((q=p->next[i]) && q->key < key)
{ 

p=q;
}
update[i]=p;
}
//判断是否为待删除的key
if(!q || (q&&q->key != key))
return false;
//逐层删除与普通链表删除一样
for(i=sl->level-1; i>=0; --i)
{ 

if(update[i]->next[i]==q)//删除节点
{ 

update[i]->next[i]=q->next[i];
//如果删除的是最高层的节点,则level--
if(sl->head->next[i]==NULL)
sl->level--;
}
}
free(q);
q=NULL;
return true;
}
// 查找
valueType *search(skip_list *sl, keyType key)
{ 

Node *q,*p=sl->head;
q=NULL;
int i=sl->level-1;
for(; i>=0; --i)
{ 

while((q=p->next[i]) && q->key<key)
{ 

p=q;
}
if(q && key==q->key)
return &(q->value);
}
return NULL;
}
//从最高层开始逐层打印
void print_sl(skip_list *sl)
{ 

Node *q;
int i=sl->level-1;
for(; i>=0; --i)
{ 

q=sl->head->next[i];
printf("level %d:\n", i+1);
while(q)
{ 

printf("key:%d val:%d\t", q->key, q->value);
q=q->next[i];
}
printf("\n");
}
}
// 释放跳跃表
void sl_free(skip_list *sl)
{ 

if(!sl)
return;
Node *q=sl->head;
Node *next;
while(q)
{ 

next=q->next[0];
free(q);
q=next;
}
free(sl);
}

在这里插入图片描述
test.cpp

#include "skiplist.h"
int main()
{ 

skip_list *sl=create_sl();
int i=1;
for(;i<11111; ++i)
{ 

insert(sl, i, i);
}
for(i=11; i<11111; ++i)
{ 

if(!erase(sl, i))
printf("No!\n");
}
print_sl(sl);
int *p=search(sl, 10);
if(p)
printf("value of key 10 is %d\n", *p);
sl_free(sl);
return 0;
}

参考

1、https://blog.csdn.net/daniel_ustc/article/details/20218489
2、https://www.zhihu.com/search?type=content&q=%E8%B7%B3%E8%A1%A8
3、https://zhuanlan.zhihu.com/p/113227225
4、http://www.cppblog.com/mysileng/archive/2013/04/06/199159.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/180086.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • Python学习—面向对象学习下

    Python学习—面向对象学习下

  • BPTT

    BPTTRNN的BP——BackPropagationThroughTime.参考:零基础入门深度学习(5)-循环神经网络。知乎。1   defbackward(self,sensitivity_array,2activator):3”’4实现BPTT…

  • 概念结构设计[通俗易懂]

    概念结构设计[通俗易懂]

  • goland激活码 2021_通用破解码

    goland激活码 2021_通用破解码,https://javaforall.cn/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

  • hi3516dv300芯片手册_hi3518ev300

    hi3516dv300芯片手册_hi3518ev300基于Hi3516DV300的嵌入式入门演练(上)基于Hi3516DV300的嵌入式入门演练(下)文章目录信息5常见外设操作5.1USB无线网卡5.1.1在内核中开启驱动支持5.1.2准备驱动需使用到的固件文件5.1.3使用wpa_supplicant连接到热点5.1.4使用hostapd将网卡作为AP5.2TF卡的挂载5.2.1手动挂载5.2.2使用mdev自动挂载设备5.2.3使用udev自动挂载设备6扩展演练6.1使用Buildroot构建根文件系统6.2理解设备树6.3

  • 图解 AD9364模块 TDD与FDD

    图解 AD9364模块 TDD与FDD转载自:http://iphonebbs.cnmo.com/thread-14714263-1-1.html如图和明显TDD是这一秒上行,下一秒下行FDD是两个通道再详细点就是TDD就是这一个时段进,下一个时段出,所以叫做时分双工,速度越快,衰落变换频率越高,衰落深度越深,因此必须要求移动速度不能太高。而FDD是双向通道,是两个频段,所以叫做频分双工,FDD模式的特点是在分离(上下行频率间隔…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号