SkipList源码实现的 拙劣剖析

什么是跳表?

跳表是运用了 二分查找的规律,的一种“特殊的链表”。
(如下图所示)

跳表的逻辑结构
 我们可以看到,我们搜寻从 header 开始,然后往后搜寻,过程中我们通过 1 级,2级…n 级索引大大的减少了遍历次数。是二分搜索思想的一种体现.
 相比红黑树,SkipList 不光能实现其大多数功能,并且能在 区间遍历 这一功能上的效率上要 优于红黑树
但是在 REDIS 的源码中不是这样的实现的,而是类似于下图:
在这里插入图片描述

一. SkipList 的源码实现

 三个主要组成部分:

  1. SkipList 结构体
  2. SkipList->Head 指向的 头节点
  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;

二. SkipList 的插入

一. 需要用到辅助数组:

  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;

二. 总步骤:

  1. 找到 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;
  }
  1. 插入节点的 其余层
    //创建结点并插入相应的位置,其实就是链表插入结点的操作
 //只不过这里是,节点的每个 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++;
 }
  1. 插入节点的 底层
    //由于第一层是双向链表,
 //所以要对其特殊处理
 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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值