跳表(SkipList)这种数据结构算是以前比较少听说过,它所实现的功能与红黑树,AVL树都差不太多,说白了就是一种基于排序的索引结构,它的统计效率与红黑树差不多,但是它的原理,实现难度以及编程难度要比红黑树简单。
另外它还有一个平衡的树形索引机构没有的好处,这也是引导自己了解跳表这种数据结构的原因,就是在并发环境下其表现很好。。。
这里可以想象,在没有了解SkipList这种数据结构之前,如果要在并发环境下构造基于排序的索引结构,那么也就红黑树是一种比较好的选择了,但是它的平衡操作要求对整个树形结构的锁定,因此在并发环境下性能和伸缩性并不好。。。
好啦,接下来就开始SkipList了吧,最开始它的作者提出它的目的好像就是为了替代基于平衡树形结构的索引数据结构,也就是前面提到的红黑树,AVL树啥的。。。。
首先,SkipList是基于链表的数据结构,从它的名字就可以体现出这种特点,其次它又是一种多层次的链表结构。。。
我们先看如下的一个链表:
如上图,这是一个十分普通的链表,而且它的元素都已经排好序了,这里如果我们要查找5这个元素,那么只需要进行遍历就好了(嗯,这里是链表,不是数组,所以没法二分搜索),那么可以知道查找的时间复杂度是O(N),
但是当我们对当前的链表结构进行一定的改装,变成如下的样子:
这里及可以看成对链表进行了层次扩展,变成了3层,
最上层是:1-》9
中间一层是:1-》5-》9
最下层是:1-》3-》5-》9
由此可以看出上层链表其实是下层链表的子集,那么对于改装之后的结构,我们进行搜索应该是怎么进行的呢,这里有个关键点:按层次进行
加入我们还是索引5,那么首先从最上层开始索引,因为最上层,1过了之后就是9,比5大,所以这里讲到第二层进行索引,第二层,1过了之后就是5,那么就直接就是5了。。。。
上述的过程其实就是SkipList的索引算法,通过不断的降低链表的层次,从而不断的扩大索引的范围。。。从而降低整个索引时间消耗,当达到了一定的数据规模之后,它的效率与红黑树差不多。。。
看了上面对SkipList的索引过程,可以初步看出整个SkipList确实简单,那么接下来来看看它的节点的擦入过程。。
还是拿上面的链表来举例子,假如我们要擦入7,那么利用上面的索引算法,我们可以快速的找到比7小的最大的元素是5,那么就可以确定我们需要将7擦入到5这个节点后面。。。如果只是擦入节点,那么链表将会变成如下的样子:
那么这里就会想到如果仅仅是这样的话,那么随着链表元素的增多,那么整个链表的索引其实最终又变成了线性的,没啥改进。。。。
这个时候就有了另外一个附加的过程,那就是确定7的层次,这个层次的确定,嗯,有一定的说法,甚至一般情况下都是按照随机的方法来确定的,例如加入7的层次是3,那么整个链表就变成了如下的样子:
因此这里可以知道,擦入元素的层次的确定是很关键的,其实一般情况下这里层次的确定都是一种类似于随机的方法来确定,当层次确定之后,再重新调整不同层次的链表。。。
通过这里也就知道为啥SkipList的分类是概率数据结构。这名字够神奇的。。。。
到这里,SkipList也就算有了大致的了解,这里也就知道在并发环境下为啥SkipList会比红黑树有更好的表现了,因为是基于链表的结构,所以在并发环境下构建SkipList就会有两种不同的选择:
(1)基于锁来构建,这里可以通过提升锁的粒度来让表现更好。
(2)基于CAS来构建。。。。嗯,无锁链表嘛。。。
好啦,接下来来看看在java中使用SkipList。。。
嗯,java的类库中没有普通的SkipList,但是在concurrent库中可以看到这两个个类型:ConcurrentSkipListMap和ConcurrentSkipListSet,其实他们底层都是使用的基于CAS来构建的SkipList。。。。
嗯,这里来做个比较有意思的事情吧,写两段程序来对比一下ConcurrentHashMap和ConcurrentSkipList。。。。
代码如下:
import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; public class Fjs { public static List<Thread> threads = new ArrayList<Thread>(); public static void doIt(final Map<String, String> map) { threads.clear(); map.put("fjs", "fjs"); for (int i = 0; i < 2; i++) { threads.add(new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 1000000; i++) { String str = UUID.randomUUID().toString(); map.put(str, str); } } })); threads.get(i).start(); } for (Thread t : threads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String args[]) { long s1 = System.currentTimeMillis(); Map<String, String> hmap = new ConcurrentHashMap<String, String>(); Fjs.doIt(hmap); System.out.println(hmap.get("fjs")); long e1 = System.currentTimeMillis(); System.out.println((e1 - s1) / 1000); long s2 = System.currentTimeMillis(); Map<String, String> smap = new ConcurrentSkipListMap<String, String>(); Fjs.doIt(smap); System.out.println(smap.get("fjs")); long e2 = System.currentTimeMillis(); System.out.println((e2 - s2) / 1000); } }
最后比下来,还是ConcurrentHashMap更好一些,不过由于无法测试大规模的并发,所以也没啥代表性。。
另外毕竟一种是基于Hash的索引结构,一种是基于排序的索引结构,他们分别有不同的应用场景。。。