大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE稳定放心使用
跳表SkipList
跳表SkipList
1、背景
为什么选择跳表?
目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。跳表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。
用跳表吧,跳表是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,它的效率和红黑树以及 AVL 树不相上下,但跳表的原理相当简单,只要你能熟练操作链表,就能轻松实现一个 SkipList。
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
,否则,执行方案Belse 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账号...