redis跳表的实现

288 阅读2分钟

结构

跳表的本质是一个链表,只不过有的节点有多层。
通过跳表,我们可以实现O(logN)时间复杂度的查询。 结构的具体结构:以三级高度的跳表为例:有5个节点:

  • 最底层lv1中的节点为:1,2,3,4,5
  • 第二层lv2中的节点为:1,3,5
  • 第三层lv3中的节点为:3

各层的节点都有指向下一个本次节点的指针

image.png

跳表节点的定义:

typedef struct zskiplistNode {
    //Zset 对象的元素值
    sds ele;
    //元素权重值
    double score;
    //后向指针
    struct zskiplistNode *backward;
  
    //节点的level数组,保存每层上的前向指针和跨度
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

跳表结构的定义:

typedef struct zskiplist {
//头尾指针
    struct zskiplistNode *header, *tail;
    //跳表长度
    unsigned long length;
    //最高层数
    int level;
} zskiplist;

头节点也是一个跳表节点,只不过后向指针、权重、元素值都没有用到。

查找

跳表的查找是从最高层开始,依据权重和元素内容来判断。判断规则如下:

  • 如果当前的节点权重小于要查找的节点权重,就查询本层的下一个节点。
  • 如果当前节点的权重等于要查找的权重的节点,并且元素的值小于要查找的元素的值,就会访问该层的下一节点

除了上面的情况或者下一节点为空,就跳到下一层继续查找

增加节点

链表是一个有序链表,在插入节点的时候会先查找到该插入的位置。

层数设置

理想的层次结构应该是高一层的节点数是其下一层的1/2,也就是高层:底层=1:2。但为了维持这一比例,在插入和删除节点的时候就会导致结构调整,所以redis使用了另一种方法来实现:redis定义了一个参数:

//跳表加一层索引的概率
var SKIPLIST_P = 0.25

//随机索引的层数

func (list *SkipList) randLevel() int {
	level := 1
	for (rand.Uint32()&0xFFFF) < uint32(0xFFFF*SKIPLIST_P) && level < list.maxLevel {
		level++
	}
	return level
}

他会随机生成一个0-1的值,小于0.25就会再加一层,大于0.25就不再增加层数。

和平衡树比较

  1. 跳表更在空间上更有优势:平衡树的每个节点必定有两个指针,而跳表只在最底层确定有一个只指针,高层的指针个数取决于SKIPLIST_P的值来决定,是人为可修改的,更为灵活。
  2. 范围查找更优秀:在树中,我们找到了小值,再查后面的值就需要使用中序遍历来一个个查找,而跳表找到小值后通过链表就能快速遍历后面的数据。
  3. 跳表比平衡树更容易实现:树的节点插入删除可能会导致树结构的调整,而跳表的增删只要修改指针就可以了。