目录
一、跳表(skip list)简介
跳表使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是 O(logn)。跳表的空间复杂度是 O(n)。不过,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗
1、时间复杂度分析
在单链表中,一旦定好要插入的位置,时间复杂度是很低的为O(1),但是,为了保持原始链表中数据的有序性,通常需要遍历链表找到需要插入的位置,这个查找操作会比较耗时。但是在跳表中,时间复杂度为O(logn),如下图,插入数据 6;在删除操作时,如果删除的节点在索引中也有出现,除了要删除原始链表中的数据外,还要删除索引中的节点
2、跳表索引动态更新
当我们不停地往跳表中插入数据时,如果我们不更新索引,就有可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表还会退化成单链表
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降
而跳表是通过随机函数来维护前面提到的“平衡性”。当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
二、跳表数据存储模型
如果一个节点存在K个向前的指针的话,那么该节点就是K层的节点。跳表的最大层数为节点中所以节点中最大的层数
#define SKIP_LIST_MALLOC(size) rt_malloc(size);
#define SKIP_LIST_CALLOC(n,size) rt_calloc(n,size);
#define SKIP_LIST_FREE(p) rt_free(p);
struct skip_list_node
{
/*key是唯一的*/
int key;
/*存储的内容*/
int value;
/*当前节点最大层数*/
int max_level;
/*柔性数组,根据该节点层数的不同指向大小不同的数组
*next[0]表示该节点的第一层下一节点的索引地址
*next[1]表示该节点的第二层下一节点的索引地址
*next[n]表示该节点的第n层下一节点的索引地址
*/
struct skip_list_node *next[];
};
struct skip_list
{
int level; /*跳表的索引层数*/
int num; /*节点数目*/
struct skip_list_node *head;
};
extern struct skip_list* skip_list_creat(int max_level);
extern int skip_list_insert (struct skip_list *list, int key, int value);
extern int skip_list_delete (struct skip_list *list, int key);
extern int skip_list_modify (struct skip_list *list, int key, int value);
extern int skip_list_search (struct skip_list *list, int key, int *value);
extern int skip_list_destroy(struct skip_list *list);
三、跳表操作
1、初始化
初始化的过程很简单,仅仅是生成下图中红线区域内的部分,也就是跳表的基础结构:
/**
* 动态申请跳表节点.
*
* @return NULL:内存申请失败
* !NULL:节点创建成功
*/
static skip_list_node* skip_list_node_creat(int level, int key, int value)
{
struct skip_list_node *node = NULL;
/* 节点空间大小 为节点数据大小+ level层索引所占用的大小 */
node = SKIP_LIST_MALLOC(sizeof(*node) + level * sizeof(node));
if (node == NULL)
return NULL;
/* 清空申请空间 */
memset(node, 0, sizeof(*node) + level * sizeof(node));
node->key = key;
node->value = value;
node->max_level = level;
return node;
}
/**
* 创建跳表头节点.
*
* @param max_level:跳表最大层数
* @return NULL:创建失败
* !NULL:创建成功
*/
struct skip_list* skip_list_creat(int max_level)
{
struct skip_list *list = NULL;
list = SKIP_LIST_MALLOC(sizeof(*list));
if (list == NULL)
return NULL;
list->level = 1;
list->count = 0;
list->head = skip_list_node_creat(max_level, 0, 0);
if (list->head == NULL)
{
SKIP_LIST_FREE(list);
return NULL;
}
return list;
}
2、插入
由于跳表数据结构整体上是有序的,所以在插入时,需要首先查找到合适的位置,然后就是修改指针(和链表中操作类似),然后更新跳表的level变量
/**
* 随机产生插入元素的索引层数,随机产生的.
*
* @param list:跳表
* @return 节点索引层数
*
*/
static int skip_list_level(struct skip_list *list)
{
int i = 0, level = 1; /*索引层数至少为1,所以从1开始*/
for (i=1; i<list->head->max_level; i++)
{
if ((rand() % 2) == 1)
{
level++;
}
}
return level;
}
/**
* 插入跳表节点.
*
* @param list:跳表
* @param key:
* @param value:
* @return -1:跳表为空
* -2:空间分配失败
* -3:key已经存在
* 0:插入成功
*/
int skip_list_insert(struct skip_list *list, int key, int value)
{
struct skip_list_node **update = NULL; /*用来更新每层索引指针,存放插入位置的前驱各层节点索引*/
struct skip_list_node *cur = NULL;
struct skip_list_node *prev = NULL;
struct skip_list_node *insert = NULL;
int i = 0, level = 0;
if (list == NULL)
return -1;
/*申请update空间用于保存每层的索引指针*/
update = (struct skip_list_node **)SKIP_LIST_MALLOC(sizeof(list->head->max_level * sizeof(struct skip_list_node *)));
if (update == NULL)
return -2;
/*逐层查询,查找插入位置的前驱各层节点索引
*update[0] 存放第一层的插入位置前驱节点,update[0]->next[0]表示插入位置的前驱节点的下一节点(update[0]->next[0])的第一层索引值
*update[1] 存放第二层的插入位置前驱节点,update[1]->next[1]表示插入位置的前驱节点的下一节点(update[1]->next[0])的第二层索引值
*update[n] 存放第一层的插入位置前驱节点,update[n]->next[n]表示插入位置的前驱节点的下一节点(update[n]->next[0])的第n层索引值
*/
prev = list->head; /*从第一个节点开始的最上层开始找*/
i = list->level - 1;
for(; i>=0; i--)
{
/* 各层每个节点的下一个节点不为空 && 下个节点的key小于要插入的key */
while ( ((cur = prev->next[i]) != NULL) && (cur->key < key) )
{
prev = cur; /* 向后移动 */
}
update[i] = prev; /* 各层要插入节点的前驱节点 */
}
/* 当前key已经存在,返回错误 */
if ((cur != NULL) && (cur->key == key))
{
return -3;
}
/*获取插入元素的随机层数,并更新跳表的最大层数*/
level = skip_list_level(list);
/*创建当前节点*/
insert = skip_list_node_creat(level, key, value);
/*根据最大索引层数,更新插入节点的前驱节点,前面已经更新到了[0] - [(list->level-1)]*/
if (level > list->level)
{
for (i=list->level; i<level; i++)
{
update[i] = list->head;/*这部分为多新增的索引层,所以前驱节点默认为头结点*/
}
list->level = level;/*更新跳表的最大索引层数*/
}
/*逐层更新节点的指针*/
for (i=0; i<level; i++)
{
insert->next[i] = update[i]->next[i];
update[i]->next[i] = insert;
}
/*节点数目加1*/
list->num++;
return 0;
}
3、删除
和插入是相同的,首先查找需要删除的节点,如果找到了该节点的话,那么只需要更新指针域,如果跳表的level需要更新的话,进行更新。
/**
* 删除跳表节点.
*
* @param list:跳表
* @param key:
* @param value:
* @return -1:跳表为空 或 跳表节点数量为0
* -2:空间分配失败
* -3:key不存在
* 0:删除成功
*/
int skip_list_delete(struct skip_list *list, int key)
{
struct skip_list_node **update = NULL; /*用来更新每层索引指针,存放插入位置的前驱各层节点索引*/
struct skip_list_node *cur = NULL;
struct skip_list_node *prev = NULL;
int i = 0;
if (list == NULL && list->num == 0)
return -1;
/*申请update空间用于保存每层的节点索引指针*/
update = (struct skip_list_node **)SKIP_LIST_MALLOC(sizeof(list->level * sizeof(struct skip_list_node *)));
if (update == NULL)
return -2;
/*逐层查询,查找删除位置的前驱各层节点索引
*update[0] 存放第一层的删除位置前驱节点,update[0]->next[0]表示删除位置的前驱节点的下一节点(update[0]->next[0])的第一层索引值
*update[1] 存放第二层的删除位置前驱节点,update[1]->next[1]表示删除位置的前驱节点的下一节点(update[1]->next[0])的第二层索引值
*update[n] 存放第一层的删除位置前驱节点,update[n]->next[n]表示删除位置的前驱节点的下一节点(update[n]->next[0])的第n层索引值
*/
prev = list->head; /*从第一个节点开始的最上层开始找*/
i = list->level - 1;
for(; i>=0; i--)
{
/* 各层每个节点的下一个节点不为空 && 下个节点的key小于要插入的key */
while ( ((cur = prev->next[i]) != NULL) && (cur->key < key) )
{
prev = cur; /* 向后移动 */
}
update[i] = prev; /* 各层要删除节点的前驱节点 */
}
/* 当前key存在 */
if ((cur != NULL) && (cur->key == key))
{
/*逐层删除*/
for(i=0; i<list->level; i++)
{
if (update[i]->next[i] == cur)
{
update[i]->next[i] = cur->next[i];
}
}
SKIP_LIST_FREE(cur);
cur = NULL;
/*更新索引的层数*/
for(i=list->level-1; i>=0; i--)
{
/*如果删除节点后,某层的头结点后驱节点为空,则说明该层无索引指针,索引层数需要减1*/
if (list->head->next[i] == NULL)
{
list->level --;
}
else
{
break;
}
}
list->num --; /*节点数减1*/
}
else
{
return -3;
}
return 0;
}
4、修改
/**
* 查询当前key是否在跳表中,存在修改key对于的value数值.
*
* @param list:跳表
* @param key:修改key
* @param value:修改的数据
* @return -1:跳表为空 或 跳表节点数量为0
* -3:key不存在
* 0:修改成功
*/
int skip_list_modify(struct skip_list *list, int key, int value)
{
struct skip_list_node *cur = NULL;
struct skip_list_node *prev = NULL;
int i = 0;
if (list == NULL && list->num == 0)
return -1;
/*逐层查找,查找查询位置原始链表的节点*/
prev = list->head; /*从第一个节点开始的最上层开始找*/
i = list->level - 1;
for(; i>=0; i--)
{
/* 各层每个节点的下一个节点不为空 && 下个节点的key小于要插入的key */
while ( ((cur = prev->next[i]) != NULL) && (cur->key < key) )
{
prev = cur; /* 向后移动 */
}
}
/* 当前key存在 */
if ((cur != NULL) && (cur->key == key))
{
cur->value = value;
}
else
{
return -3;
}
return 0;
}
5、查询
/**
* 查询当前key是否在跳表中,存在返回查询的value数值.
*
* @param list:跳表
* @param key:
* @param value:查询的数据
* @return -1:跳表为空 或 跳表节点数量为0
* -3:key不存在
* 0:查找成功
*/
int skip_list_search(struct skip_list *list, int key, int *value)
{
struct skip_list_node *cur = NULL;
struct skip_list_node *prev = NULL;
int i = 0;
if (list == NULL && value == NULL && list->num == 0)
return -1;
/*逐层查找,查找查询位置原始链表的节点*/
prev = list->head; /*从第一个节点开始的最上层开始找*/
i = list->level - 1;
for(; i>=0; i--)
{
/* 各层每个节点的下一个节点不为空 && 下个节点的key小于要插入的key */
while ( ((cur = prev->next[i]) != NULL) && (cur->key < key) )
{
prev = cur; /* 向后移动 */
}
}
/* 当前key存在 */
if ((cur != NULL) && (cur->key == key))
{
*value = cur->value;
}
else
{
return -3;
}
return 0;
}
5、销毁
/**
* 销毁跳表.
*
* @param list:跳表
* @return -1:跳表为空
* 0:成功
*/
int skip_list_destroy(struct skip_list *list)
{
struct skip_list_node *cur = NULL;
if (list == NULL && list->head == NULL)
return -1;
while((cur = list->head->next[0]) != NULL)
{
list->head->next[0] = cur->next[0];
SKIP_LIST_FREE(cur);
cur = NULL;
}
SKIP_LIST_FREE(list->head);
SKIP_LIST_FREE(list);
return 0;
}
参考链接
https://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html
https://time.geekbang.org/column/intro/126 《数据结构与算法之美--王争》