什么是跳表
我们知道二叉搜索算法能够高效的查询数据,但是需要一块连续的内存,而且增删改效率很低。
跳表,是基于链表实现的一种类似“二分”的算法。它可以快速的实现 增,删,改,查 操作。
链表,相信大家都不陌生,维护一个有序的链表是一件非常简单的事情,我们都知道,在一个有序的链表里面,查询跟插入的算法复杂度都是O(n)。
我们能不能进行优化呢,比如我们一次比较两个呢?那样不就可以把时间缩小一半?
同理,如果我们4个4个比,那不就更快了?
就好像火车,有快车有慢车;快车停得站少,慢车停得多。所以,从一个地方到另一个地方,我们需要先乘坐快车,之后换乘慢车。
跳表就是这样的一种数据结构(利用空间换时间的方式,提高查询效率 ),结点是跳过一部分的,从而加快了查询的速度。
跳表跟红黑树和AVL树又有什么差别呢?
既然 两者的算法复杂度差不多 ,为什么Redis要使用跳表而不使用红黑树呢?跳表相对于红黑树,主要有这几个优点:
- 代码相对简单,手写个跳表还有可能,手写个红黑树试试?
- 如果我们要查询一个区间里面的值,用平衡树可能会麻烦。这里的麻烦指的是实现和理解上,平衡二叉树查询一段区间也是可以做到的。
- 删除一段区间,这个如果是平衡二叉树,就会相当困难,毕竟设计到树的平衡问题,而跳表则没有这种烦恼。
- 空间复杂度 O(n):
对于每层的期待:第一层n,第二层n/2,第三层n/22 ,…,直到 n/2logn=1。所以,总空间需求:
S = n + n/2 + n/22 + … + n/2log n < n(1 + 1/2 + 1/22 + … + 1/2∞) =2n
然而原始链表包含 n 个元素,索引节点的总和是 n,因此他的空间复杂度为 O(n)
- Skip List高度:
对每层来说,它会向上增长的概率为1/2,则第m层向上增长的概率为1/2m;n个元素,则在m层元素数目的期待为Em = n/2m;当Em = 1,m = log2n即为层数的期待。故其高度期待为 Eh = O(log n)。
跳表的性质:
- 每条链必须包含两个特殊元素:+∞ 和 -∞
- S0包含所有的元素,并且所有链中的元素按照升序排列。
- 每条链中的元素集合必须包含于序数较小的链的元素集合
查询
假如我们要查询11,那么我们从最上层出发,发现下一个是5,再下一个是13,已经大于11,所以进入下一层,下一层的一个是9,查找下一个,下一个又是13,再次进入下一层。最终找到11。
是不是非常的简单?我们可以把查找的过程总结为一条二元表达式(下一个是否大于结果?下一个:下一层)。理解跳表的查询过程非常重要,试试看查询其他数字,只要你理解了查询,后面两种都非常简单。
/* 如果存在 x, 返回 x 所在的节点,
否则返回 x 的后继节点 */
find(x) {
p = top;
while (1) {
while (p->next->key < x) {
p = p->next;
}
if(p->next->key == x){
return p->next;
}
else if (p->down == NULL) {
return p->next;
}
else
p = p->down;
}
}
插入
插入数据看起来也很简单,跳表的原始链表需要保持有序,所以我们会向查找元素一样,找到元素应该插入的位置。如下图所示,要插入数据6,整个过程类似于查找6,整个的查找路径为 1、1、1、4、4、5。查找到第底层原始链表的元素 5 时,发现 5 小于 6 但是后继节点 7 大于 6,所以应该把 6 插入到 5 之后 7 之前。整个时间复杂度为查找元素的时间复杂度 O(logn)。
如下图所示,假如一直往原始列表中添加数据,但是不更新索引,就可能出现两个索引节点之间数据非常多的情况,极端情况,跳表退化为单链表,从而使得查找效率从 O(logn) 退化为 O(n)。那这种问题该怎么解决呢?我们需要在插入数据的时候,索引节点也需要相应的增加、或者重建索引,来避免查找效率的退化。那我们该如何去维护这个索引呢?
首先要从最底层开始,插入被插入的元素。然后看看从下而上,是否需要逐层插入。可是到底要不要插入上一层呢?我们都知道,我们想每层的跳跃都非常高效,越是平衡就越好(第一层1级跳,第二层2级跳,第3层4级跳,第4层8级跳)。但是用算法实现起来,确实非常地复杂的,并且要严格地按照2地指数次幂,我们还要对原有地结构进行调整。所以跳表的思路是抛硬币,听天由命,产生一个随机数,50%概率再向上扩展,否则就结束。这样子,每一个元素能够有X层的概率为0.5^(X-1)次方。
删除
同插入一样,删除也是先查找,查找到了之后,再从下往上逐个删除。比较简单,就不再赘叙。