跳跃表(skiplist)详解及代码实现(C/C++)

概念介绍

跳跃表,其实是一种可以跳跃着进行查询的链表,其的本质仍然是链表,因此要想掌握跳跃表首先需要较好的理解链表这个基础的数据结构。

以下是一个跳跃表的例子,图源博主DanielWang_

在这里插入图片描述

从功能角度来看,跳跃表就像在链表之上架起了查询的“高速公路”,可以在普通链表上快速的查找到所要的元素。在查找元素时,跳表能够提供 O ( l o g n ) O(logn) O(logn)的时间复杂度。

从结构角度来看,跳跃表就像一个不完整的十字链表,如果比较熟悉十字链表那么跳跃表就很好理解了。
(10.13更新:想了一下,这个地方和十字链表还是有一定差别,这里的纵向是类似数组的结构而横向是链表结构)

算法原理

对于普通的链表我们知道,如果想要查询链表中的一个元素,必须从头向后依次扫描,直到找到该元素或者扫描完整个链表。现在考虑有序链表,有序链表的搜索和普通链表并无明显区别,但是如果我们将其中一些结点作为索引提取出来就可以更快速的找到所要的元素,例如下图(图源博主DanielWang_):

在这里插入图片描述

正常情况下我们想要搜索元素39需要经过结点3,7,13,18,但是在这个链表中我们将3,18,77作为索引提取出来,此时如果我们再想搜索元素39就可以先在上层链表查询,3<39,18<39,77>39,因此确定查询的结点在18和77中间,从而可以减少查询的次数,虽然在本例中不是很明显,但是在规模较大的链表中就会有很好的效果,同理由于索引本身也是链表,我们还可以给索引建立索引,依次类推形成了最终的跳跃表。

那么这就会引出一个新的问题:那就是究竟选哪些结点作为索引以及一共要搭建几层索引。
这里我们首先引入完全跳跃表的概念,也就是对于一个长度为n的有序链表,可以通过构建索引,使得每一个k级结点有k+1个指针,分别跳过 2 k − 1 2^k-1 2k1 2 k − 1 − 1 2^{k-1}-1 2k11 ⋯ \cdots 2 0 − 1 2^0-1 201个结点,这样就可以在时间 O ( l o g n ) O(log n) O(logn)内完成集合成员的搜索运算。(完全跳跃表示例如下)

在这里插入图片描述

但是这只适用于静态的情况,一旦进行结点的插入操作就会导致结构变化从而打乱原有平衡,为了在动态变化中维持跳跃表中索引结点的平衡性,必须使跳跃表中 k 级结点数维持在总结点数的一定比例范围内。因此我们可以事先确定一个实数 0 < p < 1 0<p<1 0<p<1,并要求在跳跃表中具有k级指针的结点在同时具有k+1级指针的结点所占比例约为p。这里我们不妨令 p = 1 2 p=\frac{1}{2} p=21,则我们很容易得到,在这个跳跃表中50%的指针是0级指针,25%的指针是1级指针, … …, 100 2 k + 1 \frac{100}{2^{k+1}} 2k+1100 %的指针是 k级指针。这样我们就可以通过概率来解决插入时破坏平衡的问题,也就是插入新结点时这个结点有50%的概率是0级结点,25%的概率是1级结点, 100 2 k + 1 \frac{100}{2^{k+1}} 2k+1100 %的概率是k级结点,这样就维持了跳跃表的平衡。

代码实现(C/C++)
跳跃表和结点的数据结构
//结点结构
typedef struct node
{
    int key;//键值
    struct node *next[1];//多层链表结点
} Node;

//跳跃表结构
typedef struct skiplist
{
    int level;//最大层数
    Node *head;//表头结点
} Skiplist;
创建结点
//创建结点
Node *create_node(int level, int key)
{
    Node *p = (Node*)malloc(sizeof(Node)+level*sizeof(Node*));//分配对应层次的结点
    if(!p)
        return NULL;
    p->key = key;
    return p;
}

这里要注意给结点分配空间的时候大小需要仔细考虑。

创建跳跃表
//创建跳跃表
Skiplist *create_sl()
{
    Skiplist *sl = (Skiplist*)malloc(sizeof(Skiplist));
    if(!sl)
        return NULL;
    sl->level = 0;//初始化跳跃表层数为0层
    Node *h = create_node(MAX_L-1, 0);//创建跳跃表头结点
    if(!h)
    {
        free(sl);
        return NULL;
    }
    sl->head = h;
    for(int i=0; i<MAX_L; i++)
        h->next[i] = NULL;//初始化数组
    return sl;
}

这里的MAX_L是人为规定的以避免过多的空间浪费,取MAX_L=16,也就是最多可以有15层索引。

搜索结点

本来这部分应该是插入,但是插入比较复杂而且插入过程中要用到搜索这部分的思想,所以把搜索写在前面,这里我们假设跳跃表已经建好了。

//查找跳跃表中元素
Node *search(Skiplist *sl, int key)
{
    Node *q=NULL, *p=sl->head;
    for(int i=sl->level-1; i>=0; i--)
    {
        while((q=p->next[i]) && q->key<key)
            p=q;
        if(q && q->key==key)
            return q;
    }
    return NULL;
}

由于跳跃表是有序的(本题升序)所以查找并不难理解。

插入新结点

这里是跳跃表中最复杂的部分,我们分为三个部分:找到插入位置、确定新结点层数、逐层插入。

找到插入位置:

//step1:查找到在每层待插入位置,更新update数组
Node *update[MAX_L];
Node *q=NULL, *p=sl->head;
//找到每一层插入前的一个结点,放在update数组中
for(int i=sl->level-1; i>=0; i--)
{
    while((q=p->next[i]) && q->key<key)
        p=q;
    update[i]=p;
}
if(q && q->key == key)//如果插入的结点已经存在
{
    return true;
}

update数组是一个储存插入位置的数组,里面储存着每一层该插入位置的前一个结点信息,这一步和上面的搜索操作有很多相似之处。

确定新结点层数:

//step2:随机产生一个层数
int level = level_rand();//随机产生新结点的层数
//如果新生成的结点层数比原跳跃表大
if(level > sl->level)
{
    for(int i=sl->level; i<level; i++)
    {
        update[i] = sl->head;//多出的层数讲跳跃表头结点放入update数组
    }
    sl->level = level;//更新跳跃表的层数
}

关于随机层数的生成,根据前面的分析,我们在插入一个新结点时,先将其结点级别初始化为0,然后用随机数生成器反复地产生一个[0,1]间的随机实数q。如果q<p,则使新结点级别增加1,直至 q ≥ p q\geq p qp。由此产生新结点级别的过程可知,所产生的新结点的级别为0的概率为1-p,级别为1的概率为p(1-p),…,级别为k的概率为 p k ( 1 − p ) p^k(1- p) pk(1p).
代码如下:

double random()//生成0~1的随机数
{
    double q = rand()/(double)RAND_MAX;
    return q;
}

int level_rand()//生成新结点的级数
{
    int level = 1;
    while(random() <= prob)
        level++;
    return (level<=MAX_L)?level:MAX_L;
}
逐层插入

这部分只需要根据update数组中的结点把新结点的每一层插在对应结点的后面即可,插入操作和链表完全相同。

//step3:从高层至下插入
q = create_node(level, key);//创建新结点
if(!q)
    return false;
//根据update数组在每一层中插入新结点
for(int i=level-1; i>=0; i--)
{
    q->next[i] = update[i]->next[i];
    update[i]->next[i] = q;
}
return true;
删除结点

删除操作相当于在插入操作中省略了计算层数这一步,其余部分基本相同。

完整代码(C/C++)
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

using namespace std;
#define MAX_L 16 //最大层数  
#define prob 0.5 //有i+1级指针的结点占有i级指针的结点的比例

//结点结构
typedef struct node
{
    int key;//键值
    struct node *next[1];//多层链表结点
} Node;

//跳跃表结构
typedef struct skiplist
{
    int level;//最大层数
    Node *head;//表头结点
} Skiplist;

//创建结点
Node *create_node(int level, int key)
{
    Node *p = (Node*)malloc(sizeof(Node)+level*sizeof(Node*));//分配对应层次的结点
    if(!p)
        return NULL;
    p->key = key;
    return p;
}

//创建跳跃表
Skiplist *create_sl()
{
    Skiplist *sl = (Skiplist*)malloc(sizeof(Skiplist));
    if(!sl)
        return NULL;
    sl->level = 0;//初始化跳跃表层数为0层
    Node *h = create_node(MAX_L-1, 0);//创建跳跃表头结点
    if(!h)
    {
        free(sl);
        return NULL;
    }
    sl->head = h;
    for(int i=0; i<MAX_L; i++)
        h->next[i] = NULL;//初始化数组
    return sl;
}

double random()//生成0~1的随机数
{
    double q = rand()/(double)RAND_MAX;
    return q;
}

int level_rand()//生成新结点的级数
{
    int level = 1;
    while(random() <= prob)
        level++;
    return (level<=MAX_L)?level:MAX_L;
}

//向跳跃表中插入元素
bool insert(Skiplist *sl, int key)
{
    //step1:查找到在每层待插入位置,更新update数组
    Node *update[MAX_L];
    Node *q=NULL, *p=sl->head;
    //找到每一层插入前的一个结点,放在update数组中
    for(int i=sl->level-1; i>=0; i--)
    {
        while((q=p->next[i]) && q->key<key)
            p=q;
        update[i]=p;
    }
    if(q && q->key == key)//如果插入的结点已经存在
    {
        return true;
    }
    /**************************************/
    //step2:随机产生一个层数
    int level = level_rand();//随机产生新结点的层数
    //如果新生成的结点层数比原跳跃表大
    if(level > sl->level)
    {
        for(int i=sl->level; i<level; i++)
        {
            update[i] = sl->head;//多出的层数讲跳跃表头结点放入update数组
        }
        sl->level = level;//更新跳跃表的层数
    }
    /****************************************/
    //step3:从高层至下插入
    q = create_node(level, key);//创建新结点
    if(!q)
        return false;
    //根据update数组在每一层中插入新结点
    for(int i=level-1; i>=0; i--)
    {
        q->next[i] = update[i]->next[i];
        update[i]->next[i] = q;
    }
    return true;
}

//删除跳跃表中的元素
bool erase(Skiplist *sl, int key)
{
    Node *update[MAX_L];
    Node *q=NULL, *p=sl->head;
    //找到每一层待删除前的一个结点,放在update数组中
    for(int i=sl->level-1; i>=0; i--)
    {
        while((q=p->next[i]) && q->key<key)
            p=q;
        update[i]=p;
    }
    //判断q是否为待删除的结点
    if(!q || (q && q->key!=key))
        return false;
    //从最高层开始逐层删除结点
    for(int i=sl->level-1; i>=0; i--)
    {
        if(update[i]->next[i] == q)
        {
            update[i]->next[i]=q->next[i];
            //如果删除了最高层唯一的结点,则层数减一
            if(sl->head->next[i] == NULL)
                sl->level--;
        }
    }
    free(q);
    return true;
}

//查找跳跃表中元素
Node *search(Skiplist *sl, int key)
{
    Node *q=NULL, *p=sl->head;
    for(int i=sl->level-1; i>=0; i--)
    {
        while((q=p->next[i]) && q->key<key)
            p=q;
        if(q && q->key==key)
            return q;
    }
    return NULL;
}

//从最高层开始逐层打印
void print(Skiplist *sl)
{
    Node *q;
    for(int i=sl->level-1; i>=0; i--)
    {
        q=sl->head->next[i];
        printf("level %d:\n", i+1);
		while(q)
        {
            printf("key:%d\t", q->key);
            q = q->next[i];
        }
        printf("\n");
    }
}

//释放跳跃表
void free_sl(Skiplist *sl)
{
    if(!sl)
        return;
    Node *q = sl->head;
    Node *next;
    while(q)
    {
        next = q->next[0];
        free(q);
        q = next;
    }
    free(sl);
}

int main()
{
    srand((int)time(0));//随机数种子
    Skiplist *sl=create_sl();
	for(int i=1; i<20; i++)
	{
		insert(sl, i);
	}
    print(sl);
    cout << "******************************" << endl;
	for(int i=11; i<20; i++)
	{
		if(!erase(sl, i))
			printf("No!\n");
	}
	print(sl);
	free_sl(sl);
    return 0;
}

参考链接:https://blog.csdn.net/daniel_ustc/article/details/20218489

写在后面

有没有好心人推荐一款画图软件呀,太痛苦辽…

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: memset()函数是C/C++语言中的一个库函数,可以用于将一段内存空间的内容设置为指定的值。 memset()函数的原型为: ```c void *memset(void *s, int c, size_t n); ``` 其中,s是指向要设置的内存的指针;c是要设置的值,通常是一个无符号字符或者零;n是要设置的内存空间的大小。 memset()函数的作用是将一段内存空间的每个字节都设置为相同的值。可以用于初始化内存,或者将内存中的内容清零。 使用memset()函数需要注意以下几点: 1. memset()函数只能用于字符数据类型或者无符号整型数据类型,即只能设置1字节大小的值。 2. 使用memset()函数时,需要知道要设置的内存空间的大小,以防止超出边界进行内存越界操作。 3. memset()函数是按字节进行设置的,所以对于非字符类型数据(如整数或浮点数),可能造成数据不符合预期。 示例代码: ```c #include <string.h> int main() { int arr[5]; memset(arr, 0, 5 * sizeof(int)); // 将arr内存空间设置为0 char str[20]; memset(str, 'A', 19); // 将str内存空间设置为'A' str[19] = '\0'; // 末尾添加'\0',形成一个字符串 return 0; } ``` 总之,memset()函数是一个能快速设置内存空间内容的函数,可以方便地进行内存初始化和内存清零操作。 ### 回答2: memset()函数是C语言中的一个库函数,其原型如下: void *memset(void *ptr, int value, size_t num); memset()函数的作用是将指定内存空间的值设置为指定的值。其中,ptr表示要设置的内存空间的起始地址,value表示要设置的值,num表示要设置的字节数。 memset()函数的返回值为void指针类型,即可以接受任何类型的指针。 使用memset()函数可以在一次调用中批量设置内存空间的值,提高效率和代码的简洁度。 例如,下面的代码片段就是使用memset()函数将数组arr中的所有元素设置为0: int arr[10]; memset(arr, 0, sizeof(arr)); 由于memset()函数设置的是字节数据,所以在设置非字符类型数据时需要小心。以一个int型数组arr为例,使用memset()函数将其所有元素设置为-1可能会出现错误。因为在某些机器上,-1的二进制表示并不是所有字节全为1,这会导致memset()函数设置的结果并非预期。 对于字符数组或字符串,可以使用memset()函数设置为0,即'\0',也可以使用strcpy()函数进行单个字符赋值,这样更为安全可靠。 总之,memset()函数是一个实用的函数,可以批量设置内存空间的值,提高代码的执行效率和简洁度。在使用时,需要注意数据类型和数据源的合法性,以避免出现错误。 ### 回答3: memset()函数是C/C++语言中的一个库函数,主要用来对一段指定内存空间进行初始化。 其函数原型为: void* memset(void* ptr, int value, size_t num); 第一个参数ptr是一个指向某一块内存区域的指针,用来指定待初始化的内存空间。 第二个参数value是一个整数值,用来指定待初始化的值,其中最常用的是0。 第三个参数num是一个整数值,用来指定待初始化的内存空间的字节数。 memset()函数的作用是将ptr指向的内存空间中的每个字节都设置为value指定的值。一般来说,value为0时,可以用来将内存空间清零。 memset()函数通常用于对数组、字符串或结构体等数据类型的初始化。例如,当我们声明一个数组或字符串后,需要将其所有元素或字符都初始化为0,可以使用memset()函数。 以下是一个使用memset()函数进行数组初始化的示例: int num[5]; memset(num, 0, sizeof(num)); 以上代码将会把num数组的所有元素都初始化为0。 需要注意的是,memset()函数只能用来设置每个字节的值,并不能对较大的数据块进行初始化。此外,在使用memset()函数时,需要确保待初始化的内存空间不受限制并且是可访问的,否则可能会引发错误。 总结起来,memset()函数是C/C++中常用的一个函数,主要用来对一段指定的内存空间进行初始化,提高程序的可读性和可维护性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值