什么是跳表?
跳表是运用了 二分查找的规律,的一种“特殊的链表”。
(如下图所示)
我们可以看到,我们搜寻从 header 开始,然后往后搜寻,过程中我们通过 1 级,2级…n 级索引大大的减少了遍历次数。是二分搜索思想的一种体现.
相比红黑树,SkipList 不光能实现其大多数功能,并且能在 区间遍历 这一功能上的效率上要 优于红黑树。
但是在 REDIS 的源码中不是这样的实现的,而是类似于下图:
一. SkipList 的源码实现
三个主要组成部分:
- SkipList 结构体
- SkipList->Head 指向的 头节点
- 其余 成员节点
typedef struct SkipListNode Node;
typedef struct SkipListNode* node_pointer;
typedef double value_type;
typedef size_t size_type;
struct SkipListNode //一个节点为一列
{
node_pointer prev;//后退指针
value_type score;//节点排序的重要参考
struct LevelNode
{
node_pointer next;//横向、下一个元素
size_type span;//节点在该层与前向节点的 距离
} level[]; //没有 down指针,用结构体数组来代替
};
typedef struct
{
node_pointer Head;//指向头节点,头结点为一个 32 层的列
node_pointer Tail;//指向尾部节点
size_type length;//节点数量、一个节点为一列
size_type level;//默认层数为32
} SkipList;
二. SkipList 的插入
一. 需要用到辅助数组:
- update[i] 的作用:
- rank[i] 的作用: 相应update[i]指向的节点 距离 Head节点 的距离
- span 的作用: 前一个节点 与本节点 的排位差
Node *update[MAX_LEVEL];
Node *x;
unsigned int rank[MAX_LEVEL];//rank[i] 记录的是相应 update[i] 距离 左边一个节点的距离
int i, level;
二. 总步骤:
- 找到 Update[i] 和 Rank[i]
//每层结点找到插入位置前一结点,记录在 update[level] 中
//每层结点找到插入位置前一结点,记录在 update[level] 中
x = sklist->Head;
for (i = sklist->level - 1; i >= 0; i--)//遍历一个节点的每个level
{
//如果level[i]的下一个指针指向 不为NULL
//且
//下一个分值
rank[i] = i == (sklist->level - 1) ? 0 : rank[i + 1];//本层到头结点的距离与上一层的相同, 也就是都初始化为0
while( x->level[i].next
&& x->level[i].next->score <= score)
{
rank[i] += x->level[i].span;//从最左边一次加到最右边
x = x->level[i].next;
}
update[i] = x;
}
//执行完上述步骤,update[i]存储的都是: 要插入的节点的每个level的将要插入位置的 前继节点。
//这段主要是为了更新 level
//随机插入结点的层级
level = sklistRandomLevel();
//将新层级header补充到update[],update[i]保存了i层级插入位置前一结点
if (level > sklist->level)
{
for (i = sklist->level; i < level; i++)
{
rank[i] = 0;
update[i] = sklist->Head;
update[i]->level[i].span = sklist->length;
}
sklist->level = level;
}
- 插入节点的 其余层
//创建结点并插入相应的位置,其实就是链表插入结点的操作
//只不过这里是,节点的每个 level 都要指向其后 的 update[i]->next
x = create_node(level, score);
for (i = 0; i < level; i++)
{
x->level[i].next = update[i]->level[i].next;
update[i]->level[i].next = x;
/* update span covered by update[i] as x is inserted here */
/*
rank[0]表示的是update[0]指向的节点的排位,该节点肯定是新插入节点的后向节点*/
/*rank[i]表示的是update[i]指向的节点的排位,该节点的前向节点的level[i]指向的就是新节点*/
/*rank[0]-rank[i] 就是排位之差。插入新节点前update[i]->level[i].span是其和update[i]->level[i]->forward之间的排位差*/
/*那么可知update[i]->level[i]->forward的排位应该是rank[i]+update[i]->level[i].span,当插入了新节点,将变成update[i]->level[i] ->x(新节点)->update[i]->level[i]->forward,*/
/*update[i]->level[i]->forward 的排位将变成 rank[i]+update[i]->level[i].span+1 == rank[0]+1+x->level[i].span*/
/*所以可知x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i])*/
//这个式子是 数学推过来的
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
for (i = level; i < sklist->level; i++)
{
update[i]->level[i].span++;
}
- 插入节点的 底层
//由于第一层是双向链表,
//所以要对其特殊处理
x->prev = (update[0] == sklist->Head) ? NULL : update[0];//判断是否为第一个节点
if (x->level[0].next)//说明 x 不是最后一个节点, 也说明了节点存在
x->level[0].next->prev = x;//只有最底层才有 prev 指针, 这里逻辑为让 x右边的节点的 prev 指向新节点
else
sklist->Tail = x;
sklist->length++;
return x;
三.SkipList 的删除
//由于第一层是双向链表,
//所以要对其特殊处理
x->prev = (update[0] == sklist->Head) ? NULL : update[0];//判断是否为第一个节点
if (x->level[0].next)//说明 x 不是最后一个节点, 也说明了节点存在
x->level[0].next->prev = x;//只有最底层才有 prev 指针, 这里逻辑为让 x右边的节点的 prev 指向新节点
else
sklist->Tail = x;
sklist->length++;
return x;
四.RandomLevel 函数的实现
在redis中,返回一个随机层数值,随机算法所使用的幂次定律。
含义是:如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。
表现是:少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。
int sklistRandomLevel()
{
int level = 1;
while (
( random() & 0xFFFF) < (0.5 * 0xFFFF)
)
level += 1;
return (level < MAX_LEVEL) ? level : MAX_LEVEL;
}
五. 最大优势: 查找快
区间遍历快, 红黑树的遍历是非线性的,而跳表可做到区间遍历线性.
具体做法为通过 get_element_by_rank 得到相应排位的节点的地址
/* Finds an element by its rank. The rank argument needs to be 1-based. */
node_pointer get_element_by_rank(SkipList *skiplist, unsigned long rank)
{
node_pointer x;
unsigned long traversed = 0;
int i;
x = skiplist->Head;
for (i = skiplist->level - 1; i >= 0; i--)
{
while (x->level[i].next && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].next;
}
if (traversed == rank)
{
return x;
}
}
return NULL;
}
//遍历代码:
for(
node_pointer i = get_element_by_rank(&skiplist, 1);
i != get_element_by_rank(&skiplist, 9);
i = pos_plus(i)
)
{
printf("%lf ", i->score);
}
完整代码:
#include <stdio.h>
#include <malloc.h>
#include <stddef.h>
#include <assert.h>
#include <stdlib.h>
#define MAX_LEVEL 32
//在 Redis 源码中, 可以逻辑上将整个跳表想象成一个链表,
//一. 每个节点分为:
//1.prev 指向上一个节点
//2.score 为排序的重要权重
//3. 一个 level 结构体数组, 每个结构体的组成是:
// 1. next 指向下一个 Node 中的level中的 节点
// 2. span 记录了 本节点 到上个节点的距离
//其中头结点是一个有 32 层 level 的节点
// 二. 整个跳表的结构为:
// 1. 节点个数 length
// 2. 最大层数
// 3. 头指针、尾指针
typedef struct SkipListNode Node;
typedef struct SkipListNode* node_pointer;
typedef double value_type;
typedef size_t size_type;
struct SkipListNode //一个节点为一列
{
node_pointer prev;//后退指针
value_type score;//节点排序的重要参考
struct LevelNode
{
node_pointer next;//横向、下一个元素
size_type span;//节点在该层与前向节点的 距离
} level[]; //没有 down指针,用结构体数组来代替
};
typedef struct
{
node_pointer Head;//指向头节点,头结点为一个 32 层的列
node_pointer Tail;//指向尾部节点
size_type length;//节点数量、一个节点为一列
size_type level;//默认层数为32
} SkipList;
node_pointer create_node(size_type level, value_type score);
void free_node(Node* this_node);
void init_skiplist(SkipList* sklist);
void destory_skiplist(SkipList* sklist);
size_t sklistGetRank(SkipList *sklist, value_type score);
int delete(SkipList *sklist, value_type score);
void deleteNode(SkipList *skiplist, node_pointer x, Node **update);
node_pointer create_node(size_type level, value_type score)
{
node_pointer tmp =
(node_pointer)
malloc
(sizeof(struct LevelNode) * level
+ sizeof(Node) );//申请一个节点(为一列)的内存
assert(tmp != (void*)0);
tmp->score = score;
return tmp;
}
void free_node(Node* this_node)
{
free(this_node);
this_node = (void*)0;
}
void init_skiplist(SkipList* sklist)
{
sklist->Head = create_node(32, 0);
for(int i = 0; i < MAX_LEVEL; ++i)
{
sklist->Head->level[i].span = 0;
sklist->Head->level[i].next = NULL;
}
sklist->Head->prev = NULL;
sklist->Tail = NULL;
sklist->length = 0;
sklist->level = 1;
}
void destory_skiplist(SkipList* sklist)
{
if(sklist->Head)
{
for(node_pointer pos = sklist->Head->level[0].next;
pos->level[0].next != (void*)0 ;
++pos)
{
free(pos->prev);
}
return;
}
free(sklist->Head);
sklist->Head = (void*)0;
}
size_t sklistGetRank(SkipList *sklist, value_type score)
{
return 0;
}
//找到并删除返回1,否则返回 0
int delete(SkipList *sklist, value_type score)
{
node_pointer update[MAX_LEVEL];
node_pointer x = sklist->Head;
int i;
for(i = sklist->level - 1; i >= 0; ++i)
{
while(x->level[i].next
&& score <= x->score)
{
x = x->level[i].next;
}
update[i] = x;
}
if(x && x->score == score)
{
deleteNode(sklist, x, update);
free_node(x);
return 1;
}
return 0;
}
void deleteNode(SkipList *skiplist, node_pointer x, Node **update)
{
int i;
for(i = 0; i < skiplist->level; ++i)
{
if (update[i]->level[i].next == x)
{
update[i]->level[i].span += x->level[i].span - 1;//更新span
update[i]->level[i].next = x->level[i].next;
}
else
{
update[i]->level[i].span -= 1;
}
}
if(x->level[0].next)
{
x->level[0].next->prev = x->prev;
}
else
{
skiplist->Tail = x->prev;
}
while(skiplist->level > 1 && skiplist->Head->level[skiplist->level - 1].next == NULL)
skiplist->level--;
skiplist->length--;
}
int sklistRandomLevel()
{
int level = 1;
while (
( random() & 0xFFFF) < (0.5 * 0xFFFF)
)
level += 1;
return (level < MAX_LEVEL) ? level : MAX_LEVEL;
}
node_pointer insert(SkipList* sklist, value_type score)
{
//总步骤:
//1. 找到 Update[i] 和 Rank[i]
//2. 插入节点的 其余层
//3. 插入节点的 底层
//问题:
//1. update[i] 的作用:
//2. rank[i] 的作用: 相应update[i]指向的节点 距离 Head节点 的距离
//3. span 的作用: 前一个节点 与本节点 的排位差
Node *update[MAX_LEVEL];
Node *x;
unsigned int rank[MAX_LEVEL];//rank[i] 记录的是相应 update[i] 距离 左边一个节点的距离
int i, level;
//每层结点找到插入位置前一结点,记录在 update[level] 中
x = sklist->Head;
for (i = sklist->level - 1; i >= 0; i--)//遍历一个节点的每个level
{
//如果level[i]的下一个指针指向 不为NULL
//且
//下一个分值
rank[i] = i == (sklist->level - 1) ? 0 : rank[i + 1];//本层到头结点的距离与上一层的相同, 也就是都初始化为0
while( x->level[i].next
&& x->level[i].next->score <= score)
{
rank[i] += x->level[i].span;//从最左边一次加到最右边
x = x->level[i].next;
}
update[i] = x;
}
//执行完上述步骤,update[i]存储的都是: 要插入的节点的每个level的将要插入位置的 前继节点。
//这段主要是为了更新 level
//随机插入结点的层级
level = sklistRandomLevel();
//将新层级header补充到update[],update[i]保存了i层级插入位置前一结点
if (level > sklist->level)
{
for (i = sklist->level; i < level; i++)
{
rank[i] = 0;
update[i] = sklist->Head;
update[i]->level[i].span = sklist->length;
}
sklist->level = level;
}
//创建结点并插入相应的位置,其实就是链表插入结点的操作
//只不过这里是,节点的每个 level 都要指向其后 的 update[i]->next
x = create_node(level, score);
for (i = 0; i < level; i++)
{
x->level[i].next = update[i]->level[i].next;
update[i]->level[i].next = x;
/* update span covered by update[i] as x is inserted here */
/*
rank[0]表示的是update[0]指向的节点的排位,该节点肯定是新插入节点的后向节点*/
/*rank[i]表示的是update[i]指向的节点的排位,该节点的前向节点的level[i]指向的就是新节点*/
/*rank[0]-rank[i] 就是排位之差。插入新节点前update[i]->level[i].span是其和update[i]->level[i]->forward之间的排位差*/
/*那么可知update[i]->level[i]->forward的排位应该是rank[i]+update[i]->level[i].span,当插入了新节点,将变成update[i]->level[i] ->x(新节点)->update[i]->level[i]->forward,*/
/*update[i]->level[i]->forward 的排位将变成 rank[i]+update[i]->level[i].span+1 == rank[0]+1+x->level[i].span*/
/*所以可知x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i])*/
//这个式子是 数学推过来的
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
for (i = level; i < sklist->level; i++)
{
update[i]->level[i].span++;
}
//由于第一层是双向链表,
//所以要对其特殊处理
x->prev = (update[0] == sklist->Head) ? NULL : update[0];//判断是否为第一个节点
if (x->level[0].next)//说明 x 不是最后一个节点, 也说明了节点存在
x->level[0].next->prev = x;//只有最底层才有 prev 指针, 这里逻辑为让 x右边的节点的 prev 指向新节点
else
sklist->Tail = x;
sklist->length++;
return x;
}
/* Finds an element by its rank. The rank argument needs to be 1-based. */
node_pointer get_element_by_rank(SkipList *skiplist, unsigned long rank)
{
node_pointer x;
unsigned long traversed = 0;
int i;
x = skiplist->Head;
for (i = skiplist->level - 1; i >= 0; i--)
{
while (x->level[i].next && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].next;
}
if (traversed == rank)
{
return x;
}
}
return NULL;
}
node_pointer pos_plus(node_pointer x)
{
return x = x->level[0].next;
}
int main()
{
SkipList skiplist;
init_skiplist(&skiplist);
insert(&skiplist, 3.0);
insert(&skiplist, 2.0);
insert(&skiplist, 7.0);
insert(&skiplist, 4.0);
insert(&skiplist, 2.0);
insert(&skiplist, 6.0);
insert(&skiplist, 9.0);
insert(&skiplist, -6.0);
//由于 SkipList 的优势在于 读取数据
//且相比平衡二叉树的优势在于 区间遍历时, 到达下一个节点只需 ->next 即可,红黑树却有非常复杂的算法
//所以侧重与实现 Serach 和 区间遍历
for(
node_pointer i = get_element_by_rank(&skiplist, 1);
i != get_element_by_rank(&skiplist, 9);
i = pos_plus(i)
)
{
printf("%lf ", i->score);
}
destory_skiplist(&skiplist);
return 0;
}
参考文献:
- https://blog.csdn.net/men_wen/article/details/70040026