一、回顾
链表属于链式存储,用一组任意的存储单元来存储,并不要求物理上相邻。它主要通过在各个节点上维护一个指针地址,来关联数据元素的前后关系。由于存储位置是无顺序的,所以链表的查询时间复杂度为O(n)。而插入和删除的时间复杂度,若是找到目标之后,它的执行时间复杂度是O(1),如果算上查询目标位置的时间,它的时间复杂度是O(n)。下图是一个单链表,删除元素的案例示意图。
其实,数据结构之间并没有优劣之分。而是结合实际场景,合理的运用每一种数据结构才是最优的方案。
二、二分查找
举个例子,如下图是一个从小到大的有序集合列表。
若想要查询【 45、70 】两个元素。将会对有序集合进行二次轮询,第一次轮询到45时已经比较了4次,第二次轮询到70时比较了6次。所以两次轮询总共比较了10次。那如果数据比较大,轮询时间也会更久。
通过二分查找的方式,提取一部分元素节点,作为索引。如下图所示。
通过将 【10、30、58、80 】抽出作为上一级索引节点,通过上一级索引的比较之后再进行原链表元素的比较,那时间会不会缩短呢?
想要查询数据为70的元素,首先从上一级索引开始查找,有以下查询流程。
将70和10比较,比10大,则向后继续比较
将70和30比较,比30大,则向后继续比较
将70和58比较,比58大,则向后继续比较
将70和80比较,没有80大,则走向元素为58所指向的原链表元素地址58
将70和原链表地址的58所指向的后继元素70进行比较,咦,相同则查询完成
如果数据元素相对较多,则查询的速度越明显快速。那是否还可以继续优化一下呢?
三、跳跃表
typedef struct zskiplist{
# 表头节点和表尾节点
struct skiplistNode *header, *tail;
# 表中节点的数量
unsigned long length;
# 表中层数最大的节点层数
int level;
}zskiplist;
2)上图的右侧部分,是跳跃表zskilistNode结构模型,定义如下
typedef struct zskiplistNode{struct zskiplistNode *backward;# 后退指;
double score; # 分值
object *obj; # 成员对象
# 层
struct zskiplistLevel {
# 前进指针
struct zsklistNode *forward;
# 跨度
unsigned int span;
}level[]
}zskiplistNode;
3.1 跳跃表的查询
3.2 随机层数
# Redis 中随机算法,ZSKIPLIST_P == 0.25 的概率
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
3.3 跳跃表的插入
如上图所示,插入一个元素值为76。
1)随机算法运行 【 高度等于2 = ( 高度+1 )+ (高度+1 ) 】
2)查找插入的位置:通过随机算法获得,该元素最高层数是L2
3)插入对应的元素:则从高层往底层处增加该元素的节点
如果通过随机算法获得的层高,高于已有的层数,则添加新的层。如下图所示。
如上图所示,插入一个元素值为89。
1)随机算法运行的状况 【 高度等于4 = ( 高度+1) + (高度+1 ) + (高度+1)+ (高度+1) 】
2)通过随机算法获得,该元素最高层数是L4
3)则从高层往底层处增加该元素的节点
3.4 跳跃表的删除
如上图所示,将元素89的值删除掉的效果图。
1)删除掉跳跃表中的某一元素,首先找到需要删除的元素,如果没有找到则直接退出
2)如果找到了需要删除的元素,则从高层到低层删该元素,并将多余的 “空链” 删除
四、总结
通过对跳跃表的初步了解,我们目前可以知道。Redis中的跳跃表,是基于单链表抽象出元素作为索引以优化查询效率的。这种方式类似于(二分查找法)其中索引的层高是由随机算法计算所得,Redis中的层高范围是1-32之间的随机数。跳跃表采用了空间换时间的做法,解决了单链表查询慢的缺陷。它每个节点上都有维护一个bw指针,通过该指针可以实现双向链表的反查询效果。在跳跃表中,多个节点的分值是可以重复的,但是每个节点的对象值必须唯一。
五、参考文献
《Redis 设计与实现 》