我们知道Redis、LevelDB 都是著名的 Key-Value 数据库,Redis中 的 SortedSet以及LevelDB 中的 MemTable 都用到了跳表,那么什么是跳表呢?跳表又是如何实现的呢?
1、有序链表
说跳表之前,先说说有序链表:
一个有序链表搜索、添加、删除的平均时间复杂度是都是O(n)。有序数组的随机访问时间复杂度为O(1),在查询某个特定的元素的时候能够进行二分搜索优化,时间复杂度为O(logn)。因此有序链表的访问效率低于有序数据。那么就需要有某种方法来让有序链表搜索、添加、删除的平均时间复杂度降低至 O(logn),跳表这种方法就出现了。
2、什么是跳表
跳表,又叫做跳跃表、跳跃列表,是在有序链表的基础上增加了“跳跃”的功能,它是由William Pugh于1990年发布的,设计的初衷是为了取代平衡树(比如红黑树)。
跳表的结构如下:
从图中可以看到, 跳跃表主要由以下部分构成:
- 表头(first):负责维护跳跃表的节点指针。
- 跳跃表节点:保存着元素值,以及多个层。
- 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
- 表尾:全部由
NULL
组成,表示跳跃表的末尾。
对比于平衡树,跳表有以下优点:
-
跳表的实现和维护更加简单。
-
跳表的搜索、添加、删除的平均时间复杂度为 O(logn)。
-
跳表在新增、删除节点时不需要复杂的旋转。
3、跳表的搜索
① 从收割head节点的有效层数的最高层开始,从左往右搜索,直至找到一个大于或等于目标的元素,或者到达当前层链表的尾部。
② 如果该元素等于目标元素,则表明该元素已被找到。
③ 如果该元素大于目标元素或已到达链表的尾部,则退回到当前层的前一个元素,然后转入下一层进行搜索。
比如要查找17这个数,先从头结点的顶层开始找,找到21,大于17,则返回头结点转入下一层,找到9,小于17,则从9向右查找到21,大于17,则返回上一节点9,转入下一层,开始搜索,发现9的下一个是17,说明被找到。
4、跳表的添加
① 根据跳表搜索的方式确定节点添加的位置
- a:如果已经存在这个节点,则覆盖
- b: 如果不存在,则应该找到第一个大于这个需要新添加的节点的位置
② 随机决定新添加元素的层数
例如,我们需要添加15这个元素:
5、跳表的删除
① 根据跳表搜索的方式确定节点删除的位置,如果已经存在这个节点,则删除
② 删除一个元素后,这个元素的所有前驱节点指向这个节点的所有后继节点
③ 删除一个元素后,整个跳表的层数可能会降低
例如,我们需要添加9这个元素:
6、代码实现
public class SkipList<K, V> {
//链表长度
private int size;
//虚拟节点 null
private Node<K, V> first;
//默认为32层
private static final int MAX_LEVEL = 32;
//redis中的层数因子
private static final double P = 0.25;
//有效层数
private int level;
private Comparator<K> comparator;
public SkipList() {
this(null);
}
public SkipList(Comparator<K> comparator) {
this.comparator = comparator;
first = new Node<K, V>(null, null, MAX_LEVEL);
}
public int size() {
return size;
}
//返回旧的值
public V put(K k, V v) {
checkKey(k);
Node<K, V> node = this.first;
//放置前驱节点
Node<K,V>[] pres=new Node[level];
int comp = -1