上图可以看到,一个有序单链表,查找某元素的平均时间复杂度为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
与第一种定义的方法不同的是,这种方法必须要在原始链表中查找元素
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&&p.forward[0].data==val){
return p.forward[0];
}else{
return null;
}
}
结合上图来理解,从头节点指向的最顶层(第l层)结点开始,如果其后继节点p.forward[l]小于val,则继续向右。如果大于val的话,就while循环结束,进入到下一层继续找
一直找到最后一层,直到p的后继节点就是我们要找的value结点。否则返回null
大意是每插入一个新结点,随机生成一个其出现的最高Level,然后只有在小于level的层的链表才能插入这个新结点
public void insert(int val) {
Node p = head;
//如果是第一个插入的结点那肯定是第一层
int level = p.forward[0] == null ? 1 : randomLevel();
//只允许每次新增加一层
if (level > levelCount) {
level = ++levelCount;
}
//最高处于第level层的结点应该创建一个指向大小为level的Node数组引用,用来记录各层的后继节点
Node newNode = new Node(level);
newNode.data = val;
for (int l = levelCount - 1; l >= 0; l--) {
while (p.forward[l] != null && p.forward[l].data < val) {
p = p.forward[l];
}
//只有在level>l的层才能够插入新结点
if (level > l) {
if (p.forward[l] == null) {
p.forward[l] = newNode;
} else {
Node next = p.forward[l];
p.forward[l] = newNode;
newNode.forward[l] = next;
}
}
}
}
大意是开一个数组记录每一层可能将要删除结点的前继结点
然后如果原始链表层上找到了这个要删除的元素
然后遍历所有层,如果数组中该层对应节点的后一个节点就是要删除的元素,就把它删除
public void delete(int val) {
//开一个levelCount大小的数组,记录每一层可能要删除的结点的前一个结点
Node[] toDeleteNode = new Node[levelCount];
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];
}
toDeleteNode[l] = p;
}
//当仅当原始链表上的p的后继节点等于val,即找到之后才返回
if(p.forward[0]!=null&&p.forward[0].data==val){
for (int l = levelCount - 1; l >= 0; l--) {
if (toDeleteNode[l].forward[l] != null && toDeleteNode[l].forward[l].data == val) {
toDeleteNode[l].forward[l] = toDeleteNode[l].forward[l].forward[l];
}
}
}
}
提供了一种线程安全的有序的Map,内部实现是SkipList(跳表)理论上能够实现O(lgn)时间复杂度内完成增删查改操作。
/**
* The comparator used to maintain order in this map, or null if
* using natural ordering. (Non-private to simplify access in
* nested classes.)
* @serial
*/
final Comparator<? super K> comparator; // 外部比较器
/** Lazily initialized topmost index of the skiplist. */
private transient Index<K,V> head; // 跳表索引起点,需要从这里开始搜索元素
/** Lazily initialized element count */
private transient LongAdder adder; // 元素计数
//以下皆是懒加载
/** Lazily initialized key set */
private transient KeySet<K,V> keySet;
/** Lazily initialized values collection */
private transient Values<K,V> values;
/** Lazily initialized entry set */
private transient EntrySet<K,V> entrySet;
/** Lazily initialized descending map */
private transient SubMap<K,V> descendingMap; // 【逆序】Map
通常最后一层的原始链表使用Node来表示的,结点之间以next指针相连
而除了最后一层的链表都是索引层,只有向下和向右结点
// 跳表元素,存储键值对的引用,组成一个单链表
static final class Node<K,V> {
final K key; // currently, never detached
V val;
Node<K,V> next;
Node(K key, V value, Node<K,V> next) {
this.key = key;
this.val = value;
this.next = next;
}
}
// 跳表索引,存储右侧跟下侧的索引,组成一个十字链表
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;
}
}
貌似我看的JDK11代码和网上的源码解读中CSLM的代码增删查改操作都有很大的不同,而删除了HeadIndex这种类型,而且其关于跳表的操作非常非常的复杂…代码很长有兴趣可以自己看看源码
本人水平有限就不贴代码了…但是通篇扫下去,对跳表结构的修改都是用CAS操作来完成,也没有用到synchroinze关键字来保证线程安全。
其实可以趁这个机会学习下lock-free skip list是怎么实现的,这又是一个很大的topic了哎,但关键还是用CAS操作来保证线程安全,以后有时间再看看redis的skipList源码和levelDB的skipList源码。
emmm瞄了一眼
redis中的skipList结点大概是按照本文第二种方法定义的,增删查改也和我写的简单轮子相似,代码量少了很多呢
typedef struct zskiplistNode {
sds ele; //数据域
double score; //分值
struct zskiplistNode *backward; //后向指针,使得跳表第一层组织为双向链表
struct zskiplistLevel { //每一个结点的层级
struct zskiplistNode *forward; //某一层的前向结点
unsigned int span; //某一层距离下一个结点的跨度
} level[]; //level本身是一个柔性数组,最大值为32,由 ZSKIPLIST_MAXLEVEL 定义
最后讲讲varHandle吧
参考 https://blog.csdn.net/sench_z/article/details/79793741
在JDK9之后以及开始逐渐把Unsafe类方法等提供原子操作的方法转换为用varhandle来提供,VarHandle 的出现替代了 java.util.concurrent.atomic 和 sun.misc.Unsafe 的部分操作。并且提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的API。
// VarHandle mechanics
private static final VarHandle HEAD;
private static final VarHandle ADDER;
private static final VarHandle NEXT;
private static final VarHandle VAL;
private static final VarHandle RIGHT;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
HEAD = l.findVarHandle(ConcurrentSkipListMap.class, "head", Index.class);
ADDER = l.findVarHandle(ConcurrentSkipListMap.class, "adder", LongAdder.class);
NEXT = l.findVarHandle(Node.class, "next", Node.class);
VAL = l.findVarHandle(Node.class, "val", Object.class);
RIGHT = l.findVarHandle(Index.class, "right", Index.class);
} catch(ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
在源码中的的CAS方法都是由上面的变量句柄提供如
RIGHT.compareAndSet(q, r, r.right);
除此之外Varhandle方法还会提供内存屏障操作如:
@ForceInline
public static void fullFence() {
UNSAFE.fullFence();
}
@ForceInline
public static void acquireFence() {
UNSAFE.loadFence();
}
内存屏障的核心语义就是禁止指令重排,保证happens before
.
https://segmentfault.com/a/1190000016168566?utm_source=tag-newest
https://blog.csdn.net/sench_z/article/details/79793741