跳跃表概述:
红黑树实现了一颗高效的二叉查找树,其增删查的时间复杂度为O(log2N),但是其实现起来有点复杂。
跳跃表是一种以更加便捷的方式实现了和红黑树增删查操作时间复杂度一样的数据结构。
JUC并发包中提供了一个ConcurrentSkipLIstSet的实现,是一个线程安全的跳跃表,基于CAS实现的线程安全。
缺点:
跳跃表会浪费大量内存,在海量数据时,其浪费的内存差不多和已存储的数据内存一样大。
跳跃表的几个特性:
- 时间复杂度为O(log2N);
- 在理想条件下,上层元素个数是下层元素个数的一半。
- 相同数据,每次插入跳跃表,层数是会变化的。
跳跃表原理:
学过数据结构的都知道,在单链表中查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过2分的方式缩减时间复杂度。
如上图,我们要查询元素为55的结点,必须从头结点,循环遍历到最后一个节点,不算-INF(负无穷)一共查询8次。那么用什么办法能够用更少的次数访问55呢?最直观的,当然是新开辟一条捷径去访问55。
如上图,我们要查询元素为55的结点,只需要在L2层查找4次即可。在这个结构中,查询结点为46的元素将耗费最多的查询次数5次。即先在L2查询46,查询4次后找到元素55,因为链表是有序的,46一定在55的左边,所以L2层没有元素46。然后我们退回到元素37,到它的下一层即L1层继续搜索46。非常幸运,我们只需要再查询1次就能找到46。这样一共耗费5次查询。
那么,如何才能更快的搜寻55呢?有了上面的经验,我们就很容易想到,再开辟一条捷径。
如上图,我们搜索55只需要2次查找即可。这个结构中,查询元素46仍然是最耗时的,需要查询5次。即首先在L3层查找2次,然后在L2层查找2次,最后在L1层查找1次,共5次。很显然,这种思想和2分非常相似,那么我们最后的结构图就应该如下图。
我们可以看到,最耗时的访问46需要6次查询。即L4访问55,L3访问21、55,L2访问37、55,L1访问46。我们直觉上认为,这样的结构会让查询有序链表的某个元素更快。那么究竟算法复杂度是多少呢?
如果有n个元素,因为是2分,所以层数就应该是log n层 (本文所有log都是以2为底),再加上自身的1层。以上图为例,如果是4个元素,那么分层为L3和L4,再加上本身的L2,一共3层;如果是8个元素,那么就是3+1层。最耗时间的查询自然是访问所有层数,耗时logn+logn,即2logn。为什么是2倍的logn呢?我们以上图中的46为例,查询到46要访问所有的分层,每个分层都要访问2个元素,中间元素和最后一个元素。所以时间复杂度为O(logn)。
至此为止,我们引入了最理想的跳跃表,但是如果想要在上图中插入或者删除一个元素呢?比如我们要插入一个元素22、23、24……,自然在L1层,我们将这些元素插入在元素21后,那么L2层,L3层呢?我们是不是要考虑插入后怎样调整连接,才能维持这个理想的跳跃表结构。我们知道,平衡二叉树的调整是一件令人头痛的事情,左旋右旋左右旋……一般人还真记不住,而调整一个理想的跳跃表将是一个比调整平衡二叉树还复杂的操作。幸运的是,我们并不需要通过复杂的操作调整连接来维护这样完美的跳跃表。有一种基于概率统计的插入算法,也能得到时间复杂度为O(logn)的查询效率,这种跳跃表才是我们真正要实现的。
跳跃表插入:
先讨论插入,我们先看理想的跳跃表结构,L2层的元素个数是L1层元素个数的1/2,L3层的元素个数是L2层的元素个数的1/2,以此类推。从这里,我们可以想到,只要在插入时尽量保证上一层的元素个数是下一层元素的1/2,我们的跳跃表就能成为理想的跳跃表。那么怎么样才能在插入时保证上一层元素个数是下一层元素个数的1/2呢?很简单,抛硬币就能解决了!假设元素X要插入跳跃表,很显然,L1层肯定要插入X。那么L2层要不要插入X呢?我们希望上层元素个数是下层元素个数的1/2,所以我们有1/2的概率希望X插入L2层,那么抛一下硬币吧,正面就插入,反面就不插入。那么L3到底要不要插入X呢?相对于L2层,我们还是希望1/2的概率插入,那么继续抛硬币吧!以此类推,元素X插入第n层的概率是(1/2)的n次。这样,我们能在跳跃表中插入一个元素了。
在此还是以上图为例:跳跃表的初试状态如下图,表中没有一个元素:
如果我们要插入元素2,首先是在底部插入元素2,如下图:
然后我们抛硬币,结果是正面,那么我们要将2插入到L2层,如下图:
继续抛硬币,结果是反面,那么元素2的插入操作就停止了,插入后的表结构就是上图所示。接下来,我们插入元素33,跟元素2的插入一样,现在L1层插入33,如下图:
然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,我们插入元素55,首先在L1插入55,插入后如下图:
然后抛硬币,结果是正面,那么L2层需要插入55,如下图:
继续抛硬币,结果又是正面,那么L3层需要插入55,如下图:
以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。
具体步骤:
- 遍历跳跃表找到要插入节点位置,只有pre下层为空时,才是找到真正要插入的节点位置。
- 抛硬币决定要插入的层数,但是最高只能比head层数高一层。若层数比当前head高,更新head节点。
- 构建新node节点数组,大小为level。并将其上下串起来,形成key串。
- 从最底层key串开始,将节点插入当前层。若当前的pre.up==null,那么向左寻找最近的up节点。
跳跃表删除节点:
删除节点比插入来说更为简单:
- 先进行查找key值节点,找到第一个key就跳出,因此key为key串头节点,未找到直接返回。
- 遍历key串,将每个节点的左右节点进行连接
- 检查head,若head当前层只有head一个节点,取消此层,head下移。
代码实现:
package com.AdvancedDataStructure.SkipList;
/**
* @Created with IntelliJ IDEA
* @Description: 跳跃表(增删查实现)
* @Package: com.AdvancedDataStructure.SkipList
* @author: FLy-Fly-Zhang
* @Date: 2019/7/15
* @Time: 10:24
*/
class Node<T extends Comparable<T>>{
private T data;
private Node<T> up;
private Node<T> down;
private Node<T> left;
private Node<T> right;
public Node(T data,Node<T> up,Node<T> down,Node<T> left,Node<T> right){
this.data=data;
this.up=up;
this.down=down;
this.left=left;
this.right=right;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node<T> getUp() {
return up;
}
public void setUp(Node<T> up) {
this.up = up;
}
public Node<T> getDown() {
return down;
}
public void setDown(Node<T> down) {
this.down = down;
}
public Node<T> getLeft() {
return left;
}
public void setLeft(Node<T> left) {
this.left = left;
}
public Node<T> getRight() {
return right;
}
public void setRight(Node<T> right) {
this.right = right;
}
}
/**
* 跳跃表每一层链表的起始头结点类型,相对于其它类型,添加了一个记录层数的level变量
* @param <T>
*/
class HeadNode<T extends Comparable<T>> extends Node<T>{
private int level;
public HeadNode(T data,Node<T> up,Node<T> down,Node<T> left,Node<T> right,int level){
super(data, up, down, left, right);
this.level=level;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}
public class SkipListDemo <T extends Comparable<T>> {
private HeadNode<T> head;
public SkipListDemo(){
this.head=new HeadNode<>(null,null,null,null,null,1);
}
/**
* 插入元素
* @param key
*/
public void put(T key){
Node<T> pre=this.head;
Node<T> cur=pre.getRight();
for(;;){
if(cur!=null){
//本层,找到cur<key的节点
if(cur.getData().compareTo(key)>0){
pre=cur;
cur=cur.getRight();
continue; //这个一定要加,否则就继续执行下去,找到的点也不准
}else if(cur.getData().compareTo(key)==0){
return;
}
}
//已找到,判断是否有下层,有则继续进行搜索
//需要注意的是,跳跃表的结构,如果不是最底层,每个上层节点不一定有up节点,但是一定有down节点
//这是由跳跃表的性质决定的。
if(pre.getDown()==null){ //已到最底层节点
break;
}
pre=pre.getDown();
cur=pre.getRight();
}
//抛硬币决定key最高放几层。
int level=getInsertLevel();
//构建Node数组,并形成串。
//抛硬币层数高于当前head层数
if(level>this.head.getLevel()){
HeadNode<T> h=new HeadNode<>(null,null,this.head,null,null,level);
this.head.setUp(h);
this.head=h; //更新head节点
}
Node<T>[] nodes=new Node[level];
Node<T> index=null;
for (int i = 0; i < level; i++) {
//创建每层的node节点并将其与上一层的node节点连接起来。
nodes[i]=index=new Node<T>(key,index,null,null,null);
}
//将每层节点与下层连接起来。
for (int i = level-1-1; i >=0 ; i--) {
nodes[i].setDown(nodes[i+1]);
}
//将生成的key串插入到跳跃表中,需要知道的是,当前pre仍在下最底层,最底层node就插入pre的右边。
//若level不为0,那么仍需将nodes中节点与对应层连接起来。
//此时需要对pre进行处理,我们只需要找到pre或者pre左边第一个up不为null的节点,那么它就是上一层要插入的pre节点。
for (int i = level-1;i>=0 ;i--) {
//将节点连接到跳跃表当前层。
//左边互连
pre.setRight(nodes[i]);
nodes[i].setLeft(pre);
//若存在右边节点,右边结点互连
if(cur!=null){
cur.setLeft(nodes[i]);
nodes[i].setRight(cur);
}
//这句是必须要有的,假如最高层只有head和插入节点
//如果不break,pre.getleft会出现空指针异常
//因为不会在有up!=null 的情况了
if(i==0)
break;
while(pre.getUp()==null){
pre=pre.getLeft();
}
pre=pre.getUp();//新的要插入的左结点
cur=pre.getRight();//右节点
}
}
/**
* 抛硬币,得到要插入层数
* @return
*/
private int getInsertLevel(){
int level=1;
while(Math.random()>0.5){
level++;
//这个判断一定要放在这,否则运气不太好的情况下,level可能会大head.level好多。
if(level>head.getLevel())
break;
}
return level;
}
/**
* 条约表的删除。
* @param key
*/
public void remove(T key){
Node<T> pre=this.head;
Node<T>cur=pre.getRight();
//先查找跳跃表中是否有该节点
for(;;){
if(cur!=null){
if(cur.getData().compareTo(key)>0){
pre=cur;
cur=cur.getRight();
continue;
}else if(cur.getData().compareTo(key)==0){
break;
}
}
//已经找到最底层
if(pre.getDown()==null) //最底层也没有找到,直接返回。
return;
//去下层
pre=pre.getDown();
cur=pre.getRight();
}
//跳跃表中未有此数据
if(cur==null)
return;
//删除从cur开始的垂直链表,从下往上删除。
while(cur!=null){
cur.getLeft().setRight(cur.getRight());
if(cur.getRight()!=null){
cur.getRight().setLeft(cur.getLeft());
}
cur=cur.getDown(); //这里是down,因为在上面查找key时,是查找到key串第一个就跳出。因为得到的是key串头节点。
}
Node<T> h=this.head;
//从头开始检查,若当前层只有一个head节点,head节点下移
while(h.getRight()==null&&h.getDown()!=null){
h=h.getDown();
}
this.head=(HeadNode<T>)h;
}
/**
* 查询表中是否有此元素。
* @param key
* @return
*/
public boolean get(T key){
Node<T> pre=this.head;
Node<T> cur=pre.getRight();
for (;;) {
if(cur!=null){
if(cur.getData().compareTo(key)>0){
pre=cur;
cur=cur.getRight();
continue;
}else if(cur.getData().compareTo(key)==0){
return true;
}
}
if(pre.getDown()==null)
return false;
pre=pre.getDown();
cur=pre.getRight();
}
}
/**
* 层序打印跳跃表。
*/
public void show(){
System.out.println("跳跃表共:"+this.head.getLevel()+"层");
Node<T> h=this.head;
while(h!=null){
Node<T> cur=h.getRight();
while(cur!=null){
System.out.print(cur.getData()+" ");
cur=cur.getRight();
}
System.out.println();
h=h.getDown();
}
}
public static void main(String[] args) {
SkipListDemo<Integer> sl = new SkipListDemo<>();
for (int i = 0; i < 10; i++) {
sl.put(i);
}
sl.show();
sl.remove(4);
sl.remove(5);
sl.show();
System.out.println(sl.get(4));
sl.put(4);
sl.show();
System.out.println(sl.get(4));
}
}
测试结果:
结果1结果2用来证明其层数是动态变化的,因为我们用的是随机树。
- 结果1
跳跃表共:3层
6
8 6 5 1 0
9 8 7 6 5 4 3 2 1 0
跳跃表共:3层
6
8 6 1 0
9 8 7 6 3 2 1 0
false
跳跃表共:3层
6
8 6 1 0
9 8 7 6 4 3 2 1 0
true
Process finished with exit code 0
- 结果2
跳跃表共:4层
8
8 6
8 7 6 3 1 0
9 8 7 6 5 4 3 2 1 0
跳跃表共:4层
8
8 6
8 7 6 3 1 0
9 8 7 6 3 2 1 0
false
跳跃表共:4层
8
8 6
8 7 6 3 1 0
9 8 7 6 4 3 2 1 0
true
博客推荐:
前面概念性以及图例引用了这位博主博客:请点击