跳表实质就是一种可以进行二分查找的有序链表,跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。解决了链表查询慢的问题,但会占有更多的内存,是一种以空间换内存的数据结构。
查询任意数据的时间复杂度O(logn)
插入数据的时间复杂度O(logn)
空间复杂度:O(n)
在Server端,对并发和性能有要求的情况下,如何选择合适的数据结构(跳表和红黑树)?
如果单纯比较性能,跳跃表和红黑树可以说相差不大,但是加上并发的环境就不一样了,如果要更新数据,跳表需要更新的部分就比较少,锁的东西也就比较少,所以不同线程争锁的代价就相对少了。而红黑树有个平衡的过程,牵涉到大量的节点,争锁的代价也就相对较高了。性能也就不如前者了。
在并发环境下Skip List有另外一个优势,红黑树在插入和删除的时候可能需要做一些rebalance的操作,这样的操作可能会涉及到整个树的其他部分,而Skip List的操作显然更加局部性一些,锁需要盯住的节点更少,因此在这样的情况下性能好一些。
在Redis中,有序集数据类型(Sorted Set)也是用跳表实现的。
Redis作者描述的使用跳表的原因:
1、跳表的一个缺点是耗内存(因为要重复分层存节点),但是作者也说了,可以调参数来降低内存消耗,和那些平衡树结构达到差不多。
2、redis经查有范围操作,这样利用跳表里面的双向链表,可以方便地操作。另外还有缓存区域化(cache locality)不会比平衡树差。
3、实现简单。zrank操作能够到O(log(N))。
在Java的API中已经有了实现:
import java.util.*;
import java.util.concurrent.*;
/*
* 跳表(SkipList)这种数据结构算是以前比较少听说过,它所实现的功能与红黑树,AVL树都差不太多,说白了就是一种基于排序的索引结构,
* 它的统计效率与红黑树差不多,但是它的原理,实现难度以及编程难度要比红黑树简单。
* 另外它还有一个平衡的树形索引机构没有的好处,这也是引导自己了解跳表这种数据结构的原因,就是在并发环境下其表现很好.
* 这里可以想象,在没有了解SkipList这种数据结构之前,如果要在并发环境下构造基于排序的索引结构,那么也就红黑树是一种比较好的选择了,
* 但是它的平衡操作要求对整个树形结构的锁定,因此在并发环境下性能和伸缩性并不好.
* 在Java中,skiplist提供了两种:
* ConcurrentSkipListMap 和 ConcurrentSkipListSet
* 两者都是按自然排序输出。
*/
public class SkipListDemo {
public static void skipListMapShow(){
Map<Integer,String> map= new ConcurrentSkipListMap<>();
map.put(1, "1");
map.put(23, "23");
map.put(3, "3");
map.put(2, "2");
for(Integer key : map.keySet()){
System.out.println(map.get(key));
}
}
public static void skipListSetShow(){
Set<Integer> mset= new ConcurrentSkipListSet<>();
mset.add(1);
mset.add(21);
mset.add(6);
mset.add(2);
System.out.println("ConcurrentSkipListSet result="+mset);
Set<String> myset = new ConcurrentSkipListSet<>();
System.out.println(myset.add("abc"));
System.out.println(myset.add("fgi"));
System.out.println(myset.add("def"));
System.out.println(myset.add("Abc"));
System.out.println("ConcurrentSkipListSet contains="+myset);
}
}
输出结果:
1
2
3
23
ConcurrentSkipListSet result=[1, 2, 6, 21]
true
true
true
true
ConcurrentSkipListSet contains=[Abc, abc, def, fgi]
ConcurrentSkipListMap和TreeMap类似,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。
ConcurrentSkipListMap的数据结构,如下图所示:
跳表分为许多层(level),每一层都可以看作是数据的索引,这些索引的意义就是加快跳表查找数据速度。每一层的数据都是有序的,上一层数据是下一层数据的子集,并且第一层(level 1)包含了全部的数据;层次越高,跳跃性越大,包含的数据越少。跳表包含一个表头,它查找数据时,是从上往下,从左往右进行查找。
先以数据“7,14,21,32,37,71,85”序列为例,来对跳表进行简单说明。在跳表中查找“32”节点 路径如下图所示:
public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V>
implements ConcurrentNavigableMap<K,V>,
Cloneable,
java.io.Serializable {
//head是跳表的表头
private transient volatile HeadIndex<K,V> head;
}
static class Index<K,V> {
final Node<K,V> node; //哈希表节点node
final Index<K,V> down; //下索引的指针
volatile Index<K,V> right; //右索引的指针
}
static final class HeadIndex<K,V> extends Index<K,V> {
final int level; //节点所属层次
HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
super(node, down, right);
this.level = level;
}
}
static final class Node<K,V> {
final K key;
volatile Object value;
volatile Node<K,V> next;
}
ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它包含一个ConcurrentNavigableMap对象m,而m对象实际上是ConcurrentNavigableMap的实现类ConcurrentSkipListMap,它只用到了ConcurrentSkipListMap中的key,其value是一个空的Object。
public class ConcurrentSkipListSet<E>
extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
private final ConcurrentNavigableMap<E,Object> m;
public ConcurrentSkipListSet() {
m = new ConcurrentSkipListMap<E,Object>();
}
}