跳表

上图可以看到,一个有序单链表,查找某元素的平均时间复杂度为O(n)
跳表本质上是在有序链表上建立多层索引,以实现二分查找。以空间换时间的思想,实现增删查改平均时间复杂度为O(lgn)
而skipList的结构可能有2种:
第一种是每个结点会指向向右和向下的结点,像ConcurrentSkipListMap就是这么设计的
/**
* Index nodes represent the levels of the skip list.
*/
// 跳表索引,存储右侧跟下侧的索引,组成一个十字链表
static final class Index<K,V> {
final Node<K,V> node; // currently, never detached
final Index<K,V> down;
Index<K,V> right;
Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
this.node = node;
this.down = down;
this.right = right;
}
}
下图就是一个好例子:

我在面试阿里云的时候笔试题就是手写跳表,当时我也打算是这么设计结点的结构。但面试官提到,这么做的话其实会非常浪费空间。首先最底层的原始链表都是没有向下结点,最后一个结点也没有向右结点,虽然next和right都是引用,但是未免还是有点浪费…更重要的是,假设跳表有10层,那么假设要插入一个新的节点,可能要在10层(不一定10层,这个是随机的)中都添加这个节点,虽然每一层next和right可能不同,但还是会造成数据冗余对吧。
因此另外一种定义结点的方式是这样的:
class Node{
int data;
Node[] forward; //forward[i]表示处于第i层的结点Node的下一个结点
//随机高度数 k决定节点的高度 h,节点的高度 h决定节点中forword的长度;}
每一个结点有一个结点数组,可以指向其每一层指向的结点。假设这个Node分布在10层中,那么forward长度就是10,forward[0]是其在第一层的后继节点…以此类推
那么每次插入一个新节点只需要new 一个Node,forward本质上也是一个数组的引用。那么总比上面那种可能要new 10次要强吧?
那我们以第二种定义方式继续讨论:

假设要找元素15
1.从head结点开始,从最顶层的1开始找,下一个结点为8,跳到8
2.下一个节点为18,从8的下一层开始寻找
3.在最底层依次经过8,10,13最后找到15
可以把除了最底层链表的结点都看作是索引层,通过额外引入索引节点,达到查询时候忽略某些节点的效果,本质上是以空间换时间
这里参考跳表实现尝试以这种定义方式手写一个跳表轮子加深理解,源代码在这skipList
- find
与第一种定义的方法不同的是,这种方法必须要在原始链表中查找元素
public Node find(int val) {
//begin with head node
Node p = head;
for (int l = levelCount - 1; l >= 0; l--) {
while (p.forward[l] != null && p.forward[l].data < val) {
p = p.forward[l];
}
}
if(p.forward[0]!=null

本文介绍了跳表的概念,通过对比两种跳表结点设计,探讨了空间效率,并以第二种方式手写了跳表实现。接着详细讲解了ConcurrentSkipListMap的查找、插入和删除操作,指出其线程安全特性源于CAS操作和VarHandle的使用,最后提到了Redis的跳表实现作为对比。
最低0.47元/天 解锁文章
23万+

被折叠的 条评论
为什么被折叠?



