看跳表(SkipList)如何把链表插入、查找元素的时间复杂度提高到接近log(n)

前言

链表是一种基本的数据结构,它有许多优缺点。
优点:

  • 可以动态增删元素,不需要提前申请内存,不存在内存浪费
  • 插入删除元素效率高,不需要移动元素
  • 充分利用内存碎片

缺点:

  • 不支持随机访问,要找一个元素,只能从头到尾遍历
  • 查找元素的时间复杂度为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的整个流程是这样的:

  1. 定义一个变量level表示当前所在的索引层级
  2. 从头结点P开始
  3. 如果P的nextArray[level-1]小于value,则访问下一个元素
  4. 否则,检查P的nextArray[level-1]是否等于value,等于则返回结果,不等于则访问P的nextArray[level-2],直到level-2小于0时还没找到就返回null
  5. 循环3,4过程

理想情况下,如果按照第0层索引的节点数量是第二层的2倍来计算,时间复杂度就能达到O(log(n))。实际情况是,如果想达到O(log(n))的话,代码是比较复杂的,要考虑之前插入节点的索引怎么根据链表节点的数量进行变化,不能只保持最初插入的样子。因此时间复杂度只能接近O(log(n))。

插入元素

插入元素的时候,其实就是在构建跳表的数据结构。
由于想达到O(log(n))的效率,是特别复杂的。因此为了简化代码复杂度,就想到了让每个节点的索引层数进行随机。但是这里的随机也不是乱随机的。
这里的随机生成索引层数采用的是抛硬币的策略,每次都有50%的机会增加1层。为了防止层数无限增长,定义了一个MAX_LEVEL用来限制最大索引层数。
插入元素value的整个流程是这样的:

  1. 从头结点P开始
  2. 如果P的nextArray[level-1]不为null,并且P的nextArray[level-1]小于等于value,则访问下一个元素
  3. 否则,在当前位置插入value节点
  4. 循环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;
    }

    
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值