目录
前言
链表是一种基本的数据结构,它有许多优缺点。
优点:
- 可以动态增删元素,不需要提前申请内存,不存在内存浪费
- 插入删除元素效率高,不需要移动元素
- 充分利用内存碎片
缺点:
- 不支持随机访问,要找一个元素,只能从头到尾遍历
- 查找元素的时间复杂度为O(n)
- 插入、删除元素后保持有序的时间复杂度为O(n)
因此链表一般是用在需要频繁插入或删除、动态性较强、查找操作较少的地方。但是,有些时候我就想使用链表进行排序,又想提高查找效率怎么办?别着急,让跳表(SkipList)
来帮你。
什么是跳表(SkipList)呢?
我们都知道数组使用二分查找的时间复杂度是O(log(n)),是因为数组支持随机访问的,因此下标可以自由进行跳跃。
而跳表(SkipList)的设计就是让指针支持跳跃,从而实现接近二分查找O(log(n))的效率。(为什么只能是接近,后面会讲到)
让我们用一个例子认识一下什么是跳表(SkipList)。
这里有一些需要进行排序的数字:
10,51,10,20,32
使用链表排序后的数据结构是这样的:
普通链表结构:
跳表(SkipList)结构:
跳表(SkipList)结构解释
上图中的每一行就是一个索引层
上图中的每个圆圈就是一个节点
,每个节点存储的都是元素的引用,并不是实际的数字。
上图中左上角的节点,表示头结点
。
我们先来看一段最简单的跳表节点的java代码:
class Node{
private T data;
private Node[] nextArray;
}
这里需要解释一下nextArray
是用来干嘛的。nextArray
就是用来保存每层索引中这个节点的后一个节点的引用。
比如第一个节点10,它的nextArray
就是下图中标蓝的节点。
next[0]=10
next[1]=20
next[2]=51
看到这里,想必大家基本上懂了跳表是怎么回事儿了吧。如果还没看懂,也别急,接下来的例子保证让大家明白。
查找元素
既然说跳表的查找效率比较高,那么到底高在哪儿呢?
我画个图,大家可能就明白了。
上图是查找元素51
的流程。从头结点开始,访问了10,10小于51,继续访问下一个节点51,51等于51,返回结果。这里一共比较了2
次,如果是普通链表的话,需要比较5
次才能找到结果,效率明显提高了。这里其实是有点类似二分查找的思想,分段查找。
查找元素value
的整个流程是这样的:
- 定义一个变量
level
表示当前所在的索引层级 - 从头结点P开始
- 如果P的nextArray[level-1]小于value,则访问下一个元素
- 否则,检查P的nextArray[level-1]是否等于value,等于则返回结果,不等于则访问P的nextArray[level-2],直到level-2小于0时还没找到就返回null
- 循环3,4过程
理想情况下,如果按照第0层索引的节点数量是第二层的2倍来计算,时间复杂度就能达到O(log(n))。实际情况是,如果想达到O(log(n))的话,代码是比较复杂的,要考虑之前插入节点的索引怎么根据链表节点的数量进行变化,不能只保持最初插入的样子。因此时间复杂度只能接近O(log(n))。
插入元素
插入元素的时候,其实就是在构建跳表的数据结构。
由于想达到O(log(n))的效率,是特别复杂的。因此为了简化代码复杂度,就想到了让每个节点的索引层数进行随机。但是这里的随机也不是乱随机的。
这里的随机生成索引层数采用的是抛硬币的策略,每次都有50%
的机会增加1层。为了防止层数无限增长,定义了一个MAX_LEVEL
用来限制最大索引层数。
插入元素value
的整个流程是这样的:
- 从头结点P开始
- 如果P的nextArray[level-1]不为null,并且P的nextArray[level-1]小于等于value,则访问下一个元素
- 否则,在当前位置插入value节点
- 循环2、3过程,直到level-1等于0
看代码可能更好理解一点:
public void insert(T data){
//创建节点node
int level= randomLevel();
Node<T> node=new Node<>(data,level);
//建立node的索引
//从最上层开始,把第i层中大于这个节点的最小节点,作为这个节点第i层的next节点
Node p=head;
for (int i = level-1; i >=0; i--) {
while(p.getNext(i)!=null&&comparator.compare(p.getNext(i).getData(), data)<=0){
p=p.getNext(i);
}
//在第i层插入节点node
node.setNext(i,p.getNext(i));
p.setNext(i,node);
}
curMaxLevel=Math.max(curMaxLevel,level);
}
JAVA实现
以上就是对跳表(SkipList)的一个原理介绍,应该还是比较通俗易懂。如果还不懂的,可以看一看下面的代码或者私信我,我再一一回复。
我先解释一下,我这段代码。这段代码实现的主要是,跳表(SkipList)的插入、查找、输出,这几个功能。删除我就没在这里写了,只要懂了跳表(SkipList)的原理。自己实现起来也比较容易。我这里是采用泛型实现的,可以支持任意类型的元素。文末附上使用例子。
同时,元素排序的时候肯定存在有多个个元素相同的情况,因此我这里加了一个preArray
引用数组,用来保存每层索引中这个节点的前一个节点的引用。
public class SkipList<T> {
/**
* 用于比较两个节点大小
*/
Comparator comparator = null;
/**
* 最大索引层数(防止建立某个节点的索引时,层数无限增长)
*/
private static final int MAX_LEVEL=8;
/**
* 头结点
*/
private Node<T> head=new Node(null,MAX_LEVEL);
/**
* 当前建立的最大索引层数
*/
private int curMaxLevel=1;
public SkipList(){
this.comparator=Comparator.naturalOrder();
}
public SkipList(Comparator<? super T> comparator){
this.comparator=comparator;
}