1.概述
跳表(跳跃表)是一种数据结构,改进自链表,用于存储有序的数据,跳跃表通过空间换时间的方法来提高数据检索的速度。
需要排序的东西不需要持久化或者为了更高的效率,是直接在内存中进行排序,那么使用到的数据结构就是jdk中的ConcurrentSkipListMap
(支持同步)或者TreeMap
(不支持同步,性能更好)
2.场景
通过上面的介绍,我们了解到跳跃表的应用场景大概是这样的:
- 有序
- 频繁插入删除
- 频繁的查找
对于有序来说,数组和普通链表都可以通过排序算法来实现,在排序复杂度上不相上下。链表在插入和删除上性能较好,数组在查找上性能较好,所以都不能满足我们的要求。跳跃表则是在插入删除和查找的性能上做了折中,复杂度为log(n)。
跳表结构如下所示:
为了更好的支持插入和删除,所以采用链表的形式,可以看到图片中最下面一行是一个有序的链表。但如果只是一个单一的链表,查找时复杂度为O(n),性能太差。如何优化呢?
3.节点
在有序数组中,我们查找时用的是二分查找,一次查找可以排除一半元素的遍历。在数组中之所以可以用二分查找,是因为我们能够快速的以O(1)的复杂度定位到中间的位置,但是链表只能是O(N)。所以跳跃表采取空间换时间的方式,既然你找不到中间点,或者三分之一点等中间位置,那么我可以通过多增加一个节点来指向中间位置,这样你也能够快速的定位到中间的位置,然后一定程度的减少你遍历元素的个数,提高效率。图中有多层,相邻的两层,采用的都是这样的思想。
3.节点
通过上图,可以发现有两种节点类型,第一种是最下层的,与普通链表相似,第二种是除了最后一层以外的其他索引层节点,有两个指针。
但是在jdk源码中还存在一种节点,是索引层的头节点,还维护了其层数信息
但是在jdk源码中还存在一种节点,是索引层的头节点,还维护了其层数信息
// Node是最底层的链表的节点,包括键值对和指向下一个节点的指针 static final class Node<K,V> { final K key; volatile Object value; volatile Node<K,V> next; // 至于为什么需要两个构造函数,后面源码会有解释 Node(K key, Object value, Node<K,V> next) { this.key = key; this.value = value; this.next = next; } Node(Node<K,V> next) { this.key = null; this.value = this; this.next = next; } // ...配套method } // 索引节点结构 // 存储了两个指针,分别指向右边和下边的节点 // 索引节点的value为链表节点 static class Index<K,V> { final Node<K,V> node; final Index<K,V> down; volatile 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; } // ...配套method } // 索引层的头节点结构 // 在索引节点的基础上添加了表示层数的level变量 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; } }
4.构造函数
// Compartor接口用来指定key的排序规则 public ConcurrentSkipListMap() { this.comparator = null; initialize(); } public ConcurrentSkipListMap(Comparator<? super K> comparator) { this.comparator = comparator; initialize(); } // 还有两个传入map和sortedMap的构造函数 private void initialize() { keySet = null;// 内部类 entrySet = null;// 内部类 values = null;// 内部类 descendingMap = null;// 内部类 // private static final Object BASE_HEADER = new Object(); // 从注释给出的图来看,这个head应该是一直处于第一层的头节点 head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null), null, null, 1); }