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

跳表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

  • 12
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
凸优化是一种数学优化问题的处理方法,它的目的是找到一个凸函数的全局最小值或最大值,同时满足一些约束条件。下面以一个简单的例子来说明凸优化的应用,并给出C++程序实现。 假设有一个二次函数 $f(x)=x^2-4x+6$,我们希望在 $x\in [1,5]$ 的范围内找到它的最小值。这是一个无约束的优化问题,可以使用梯度下降算法来求解。具体实现如下: ```c++ #include <iostream> #include <cmath> using namespace std; double f(double x) { return x * x - 4 * x + 6; } double df(double x) { return 2 * x - 4; } void gradient_descent(double x0, double lr, int n_iter) { double x = x0; for (int i = 0; i < n_iter; i++) { double dx = df(x); x -= lr * dx; } cout << "Minimum value of f(x) = " << f(x) << " at x = " << x << endl; } int main() { gradient_descent(0, 0.1, 1000); return 0; } ``` 上述程序中,`f(x)`和`df(x)`分别表示函数 $f(x)$ 和其导数 $f'(x)$。`gradient_descent`函数使用梯度下降算法来求解 $f(x)$ 的最小值。`x0`是起始点,`lr`是学习率,`n_iter`是迭代次数。运行程序,可以得到如下结果: ``` Minimum value of f(x) = 4 at x = 2 ``` 因为 $f(x)$ 是一个凸函数,所以梯度下降算法能够找到全局最小值。 以上是一个简单的凸优化实例,可以看出,凸优化在实际问题中的应用非常广泛。需要注意的是,实际问题可能会有更多的约束条件,此时可以使用线性规划或二次规划等方法来求解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值