结构
跳表的本质是一个链表,只不过有的节点有多层。
通过跳表,我们可以实现O(logN)时间复杂度的查询。
结构的具体结构:以三级高度的跳表为例:有5个节点:
- 最底层lv1中的节点为:1,2,3,4,5
- 第二层lv2中的节点为:1,3,5
- 第三层lv3中的节点为:3
各层的节点都有指向下一个本次节点的指针
跳表节点的定义:
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就不再增加层数。
和平衡树比较
- 跳表更在空间上更有优势:平衡树的每个节点必定有两个指针,而跳表只在最底层确定有一个只指针,高层的指针个数取决于SKIPLIST_P的值来决定,是人为可修改的,更为灵活。
- 范围查找更优秀:在树中,我们找到了小值,再查后面的值就需要使用中序遍历来一个个查找,而跳表找到小值后通过链表就能快速遍历后面的数据。
- 跳表比平衡树更容易实现:树的节点插入删除可能会导致树结构的调整,而跳表的增删只要修改指针就可以了。