深入了解Java中的跳表

什么是跳表?

        跳表是一种数据结构,用于在有序链表中进行高效的查找和插入操作。它通过在原有有序链表的基础上添加多层索引,从而减少了查找的时间复杂度。跳表的设计思想是通过“跳跃”式的方式迅速定位到目标位置。

跳表的结构

        跳表包含多层,每一层都是一个有序的链表。每一层的节点都包含一个指向下一层的指针。最底层包含所有元素,而每一层的元素都是前一层的子集。

特点:

  • 由很多层结构组成
  • 每一层都是一个有序的链表
  • 最底层(Level 1)的链表包含所有元素
  • 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
  • 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

时间复杂度

        底层包含所有元素(n个),即 2^(h +1)=n ,索引层数 h=logn-1。搜索时,首先在顶层索引中进行查找,然后二分搜索,最多从顶层搜索到底层,最多 O(logn) 层,因此查找的时间复杂度为 O(logn)。

跳表的索引生成

        跳表索引最理想的生成方式是每层抽出一半作为索引,但是在涉及到插入,删除时候要平凡的改动索引,会造成很大的时间浪费,可用性不高。所以退而求其次,采用随机的方式的生成索引。虽然是随机,但是数据量越大,索引结构就会越接近理想索引。

Java中的实现

public class Node<T> {
    
    /**
     * 在跳表中排序的关键字
     */
    public int key;
    
    /**
     * 在跳表中存储的值,每个key对应一个value(也可以不放value,这样就相当于从treemap变成treeset)
     * 注意:在跳表中,只有最底层的node才有真正的value,其它层的node的value都为null,查找时会到最底层得到value
     */
    public T value;
    
    /**
     * 左右节点
     */
    public Node<T> pre,next;
    
    /**上下节点
     * 
     */
    public Node<T> up,down;
    
    /**
     * 头结点被设置成int的min
     */
    public static int HEAD_KEY=Integer.MIN_VALUE;
    
    /**尾节点被设置成int的max
     * 
     */
    public static int TAIL_KEY=Integer.MAX_VALUE;
 
    /**Node的构造函数
     * @param key  在跳表中排序的关键字
     * @param value  在跳表中存储的值,每个key对应一个value
     */
    public Node(int key, T value) {
        this.key = key;
        this.value = value;
    }
 
    public int getKey() {
        return key;
    }
 
    public void setKey(int key) {
        this.key = key;
    }
 
    public T getValue() {
        return value;
    }
 
    public void setValue(T value) {
        this.value = value;
    }
 
    @Override
    public String toString() {
        return "Node [key=" + key + ", value=" + value + "]";
    }
                
}


public class SkipList<T> {
    
    /**
     * 跳表的头尾
     */
    public Node<T> head,tail;
    
    /**
     * 跳表所含的关键字的个数
     */
    public int size;
    
    /**跳表的层数
     * 
     */
    public int level;
 
    /**随机数发生器
     * 
     */
    public Random random;
 
    /**
     * 向上一层的概率
     */
    public static final double PROBABILITY=0.5;
    
    /**
     * 跳表的构造函数
     */
    public SkipList(){
        //初始化头尾节点及两者的关系
        head=new Node<>(Node.HEAD_KEY, null);
        tail=new Node<>(Node.TAIL_KEY, null);
        head.next=tail;
        tail.pre=head;
        //初始化大小,层,随机
        size=0;
        level=0;
        random=new Random();
    }

}

跳表的操作

查找

/**根据key,返回最底层对应的node,如果不存在这个key,则返回null
 * @param key
 * @return
 */
public Node<T> get(int key){
    //返回跳表最底层中,最接近这个key的node
    Node<T> p=findNearestNode(key);
    //如果key相同,返回这个node
    if(p.key==key){
        return p;
    }
    //如果不相同,返回null
    return null;
}

/**返回跳表最底层中,最接近这个key的node<br>
 * 如果跳表中有这个key,则返回最底层中,对应这个key的node<br>
 * 如果没有这个key,则返回最底层中,小于这个key,并且最接近这个key的node
 * @param key
 * @return
 */
private Node<T> findNearestNode(int key){
    Node<T> p=head;
    Node<T> next;
    Node<T> down;
    while (true) {
        next=p.next;
        //p先前进到这一层中(一开始在最顶层),p的next>key的位置,然后再考虑向下
        if(next!=null&&next.key<=key){
            p=next;
            continue;
        }
        down=p.down;
        //走到这一步,p在这一层已经无法前进了,就下降一层,在那层进入循环,再往前走
        if(down!=null){
            p=down;
            continue;
        }
        //到这里,说明已经无法再向下(已经到最底层),无法再向前(当前的p<=key,next>key),最接近key,跳出循环
        break;          
    }               
    return p;
}

插入

/**1. 如果put的key,在跳表中不存在,则在跳表中加入,并由random产生的随机数决定生成几层,返回一个null
 * 2. 如果put的key存在,则在跳表中对应的node修改它的value,并返回过去的value
 * @param key
 * @param value
 * @return
 */
public T put(int key,T value){
    //首先得到跳表最底层中,最接近这个key的node
    Node<T> p=findNearestNode(key);
    if(p.key==key){
        //如果跳表中有这个key,则返回最底层中,对应这个key的node
        //在跳表中,只有最底层的node才有真正的value,只需修改最底层的value就行了
        T old=p.value;
        p.value=value;
        return old;
    }
    
    //如果跳表中,没有这个key,那么p就是最底层中,小于这个key,并且最接近这个key的node
    //q为新建的最底层的node
    Node<T> q=new Node<>(key, value);
    //在最底层,p的后面插入q
    insertNodeHorizontally(p, q);
    
    //当前level(要与总层数Level进行比较)
    int currentLevel=0;
    
    //PROBABILITY为进入下一层的可能性,nextDouble()返回[0,1)
    //小于PROBABILITY,说明可能性到达(比如说假设为0.7,小于它的概率就是0.7),进入循环,加入上层节点
    while (random.nextDouble()<PROBABILITY) {
        //最底层为0,到这里currentlevel为当前指针所在的层,被插入的镜像变量在currentlevel+1层
        //所以currentLevel=level时,要在level+1层插入,就要加入一层
        if(currentLevel>=level){
            addEmptyLevel();
        }
        //找到q的左边(也就是<=p)第一个能够up的节点,并上浮,赋予p
        while (p.up==null) {
            p=p.pre;
        }
        p=p.up;
        
        //创建q的镜像变量z,只有key,没有value,插入在现在的p的后面
        Node<T> z=new Node<>(key, null);
        insertNodeHorizontally(p, z);
        
        //设置z与q的上下关系
        z.down=q;
        q.up=z;
        
        //指针上浮
        q=z;
        currentLevel++;
        
    }
    //插入完毕后,size扩大,返回null
    size++;     
    return null;
}


/**返回跳表最底层中,最接近这个key的node<br>
 * 如果跳表中有这个key,则返回最底层中,对应这个key的node<br>
 * 如果没有这个key,则返回最底层中,小于这个key,并且最接近这个key的node
 * @param key
 * @return
 */
private Node<T> findNearestNode(int key){
    Node<T> p=head;
    Node<T> next;
    Node<T> down;
    while (true) {
        next=p.next;
        //p先前进到这一层中(一开始在最顶层),p的next>key的位置,然后再考虑向下
        if(next!=null&&next.key<=key){
            p=next;
            continue;
        }
        down=p.down;
        //走到这一步,p在这一层已经无法前进了,就下降一层,在那层进入循环,再往前走
        if(down!=null){
            p=down;
            continue;
        }
        //到这里,说明已经无法再向下(已经到最底层),无法再向前(当前的p<=key,next>key),最接近key,跳出循环
        break;          
    }               
    return p;
}

/** 在同一层,水平地,在pre的后面,插入节点now
 * @param pre 插入节点前面的节点,应该pre.key小于now.key小于pre.next.key
 * @param now 插入的节点
 */
private void insertNodeHorizontally(Node<T> pre,Node<T> now){
    //先考虑now
    now.next=pre.next;
    now.pre=pre;
    //再考虑pre的next节点
    pre.next.pre=now;
    //最后考虑pre
    pre.next=now;               
}

/**
 * 在现在的最顶层之上新加一层,并更新head和tail为新加的一层的head和tail,然后更新总level数(字段level)
 */
private void addEmptyLevel(){
    Node<T> p1=new Node<T>(Node.HEAD_KEY, null);
    Node<T> p2=new Node<T>(Node.TAIL_KEY, null);
    //设置p1的下右
    p1.next = p2;
    p1.down = head;
    //设置p2的下左
    p2.pre = p1;
    p2.down = tail;
    //设置之前头尾的上
    head.up = p1;
    tail.up = p2;
    //设置现在的头尾
    head=p1;
    tail=p2;
    //更新level
    level++;        
}

删除

/**在跳表中删除这个key对应的所有节点(包括底层节点和镜像节点)<br>
 * 如果确实有这个key,返回这个key对应的value<br>
 * 如果没有这个key,返回null
 * @param key
 * @return
 */
public T remove(int key){
    //在底层找到对应这个key的节点
    Node<T> p=get(key);
    if(p==null){
        //如果没有这个key,返回null
        return null;
    }
    
    T oldValue=p.value;
    Node<T> next;
    while (p!=null) {
        next=p.next;
        //在这一行中,将p删除
        //设置p的next和pre的指针
        next.pre=p.pre;
        p.pre.next=next;
        
        //p上移,继续删除上一行的p
        p=p.up;         
    }
    
    //更新size,返回旧值
    size--;
    return oldValue;
}   

打印整个跳表

public void printSkipList(){
    System.out.println("跳表打印开始,总层数为"+level+",节点个数为"+size);
    Node<T> first=head;
    Node<T> p=first;
    int currentLevel=level;
    while(first!=null){
        //循环first这一层
        System.out.print("打印第"+currentLevel+"层:  ");
        p=first;
        while (p!=null) {
            System.out.print(p+"  ");
            p=p.next;
        }
        first=first.down;
        currentLevel--;
        System.out.println();
    }
    System.out.println();
}

测试

package datastructure.link.skiplist;
 
public class Main {
 
    public static void main(String[] args) {
        SkipList<Double> list=new SkipList<>();
        list.printSkipList();
        list.put(1, 2.0);
        list.printSkipList();
        list.put(3, 4.0);
        list.printSkipList();
        list.put(2, 3.0);
        list.printSkipList();
        
        System.out.println(list.get(2));
        System.out.println(list.get(4));
        
        list.remove(2);
        list.printSkipList();
    }
 
}

跳表的优势和应用

  • 平均查找时间复杂度低: 在有序链表上进行二分查找的平均时间复杂度为O(log n),而跳表的平均查找时间复杂度也是O(log n)。
  • 插入和删除操作高效: 由于跳表的结构,插入和删除操作相对简单,不涉及元素的移动。
  • 适用于高并发环境: 跳表的操作相对简单,支持高并发操作,适用于一些需要快速插入和删除的场景。

总结

        跳表是一种高效的数据结构,通过层级索引的方式在有序链表上实现了快速的查找和插入操作。虽然在Java标准库中并没有直接提供跳表的实现,但你可以通过自己实现或使用第三方库来享受跳表的优势。

参考文献:
[1]: 跳表(跳跃表,skipList)总结-java版_java跳表-CSDN博客

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值