看了redis的跳表的源码,感觉代码的实现非常短小精悍。
redis中跳表主要实现:增删改查, 除了这几个常规功能之外,还有一个很牛逼且很实用的功能:获取节点的rank排名、或者获取指定范围rank的节点。
redis中跳表的实现
typedef struct zskiplistNode {
robj *obj;
double score; // 内部节点排序的依据,如果值相等根据obj对象排序。
struct zskiplistNode *backward; //后退指针。每次只能后退一个节点
struct zskiplistLevel {
struct zskiplistNode *forward; //前进指针。一次可能跳过多个节点
unsigned int span; //跨度。 表示当前节点在当前层到达下一个节点的距离。后续查找节点的时候,每层会获取一个节点,这些节点的span加起来,就是节点的rank。
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //头结点和尾节点。tail是为了实现反向查找,即根据score从大到小的 顺序查找。注意header中的第一个节点是空节点。如果当前层的链为null,span表示链表的长度。header->forward节点的backward指向null。
unsigned long length; //节点的数量
int level; //跳跃表中节点的最大层数。根据实际需要增加或者减小。因此每个节点level不同,分配的空间大小也不同。
} zskiplist;
主要函数:
zslInsert函数:添加一个节点
update[ZSKIPLIST_MAXLEVEL]变量:每层待插入节点的上一个节点。
rank[ZSKIPLIST_MAXLEVEL]变量:update[i]节点的rank,即链表头到update[i]节点的距离。
函数主要逻辑:
1.计算每层要插入节点的前一个节点,以及该节点的rank,这段代码实现的很精简,而且后续增删改查都用到。
2.根据zslRandomLevel函数获取新结点的level,如果大于当前的最大level,新的level上第一个节点初始化。注意新的level上由于第一个节点的后继节点是NULL,即链表结尾,因此第一个节点的跨度为链表长度。
3.创建新节点,更新每层中的链表关系和span。主要分2部分:一部分是底层要插入节点的level,这部分要更改update[i]节点在第i层和新插入节点的链表指针、span;第二部分由于没有插入节点,只要更改update[i]->level[i].span++
4.如果新插入的节点是头节点或者尾节点,修改zskiplist
zslDelete函数:删除一个节点
1.找到每层中要处理节点的前一个节点,保存在update[i]
2.如果链表中存在该节点,检查每层中update[i]的下一个节点是不是要删除的节点,如果是则删除,并且update[i]对应的第i层的span要和下一个节点的span合并。如果不是,update[i]第i层的span-1
其他功能的实现和上面两个函数的基本一致,这里不在赘述。
redis中跳表相比红黑树、hash的优点:
1.跳表和红黑树可以实现有序排列,即可以进行范围查询,hash只支持单值查询。
2.跳表的修改和删除的复杂度为O(n),redis中采用zslRandomLevel计算不同点的层次,不用严格维护上次节点是下层节点一半数量,将复杂度降低为O(logn)。虽然和红黑树的时间复杂度相等,但是实际操作中不需要考虑树的维护,只要处理相邻的节点指针,操作简单快速。
3.算法的远比红黑树简单的多。
4.redis中获取某段范围的节点,红黑树根据中序遍历,跳表只需要根据链表指针顺序读取,执行速度和实现复杂度都优于红黑树
5.redi的跳表跳表平均每个节点有1.33个指针(根据zslRandomLevel计算平均指针数量),红黑树至少有2个,占用空间少一些。
6.redis的跳表维护了span字段,可以快速计算出节点的rank或者获取指定rank的节点