跳跃表详解-Java实现

引出跳表

什么是跳表?

跳表本质上是一种有序链表,但是其支持二分查找。

跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。

跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

考虑一个有序链表,我们要查找3、7、17这几个元素,我们只能从头开始遍历链表,直到查找到元素为止。

上述这个链表是有序的,但是不能使用二分查找,又不是数组是吧。

那么,有没有什么方法可以实现有序链表的二分查找呢?

答案是肯定的,那就是我们将要介绍的这种数据结构——跳表。

跳表的功能与红黑树和差不多,效率上也相差不大。但是红黑树代码实现复杂,跳表实现简单,所以再很多时候我们也可以选择使用跳表

跳表的实现

跳表的查询

我们多添加一级索引,形成了下面这样的结构。

现在假设我们想要查找15这个元素是不是就要快很多呢?

我们只要从一级索引往后遍历即可,只需要经过1、6、15、我们就可以找到15了。

那么,我们要查找11这个元素呢?

我们从一级索引的1开始,向右到6,再向右发现是15,它比11大,此路不通,从6往下走,再从下面的6往右走,到7,再到11。

同样地,一级索引也可以往上再提取一层,组成二级索引,如下:

这时候我们再查找17这个元素呢?

只需要经过6、15、17这几个元素就可以找到17了。

这基本上就是跳表的核心思想了,其实这也是一个“空间换时间”的算法,通过向上提取索引增加了查找的效率。

 跳表的插入

比如,我们要向上面这个跳表添加一个元素8。

首先,我们先根据投硬币的方式,决定8这个元素要占据的层数,其实就是一种概率性的问题,这样出来的层数具有其规律性。

这也硬币怎么抛呢?我们设置一个初始值为i=1,代表当前元素所占据的层数为1。我们规定硬币反面的时候,层数i++,正面的时候,当前当前i的值,就是当前这个元素所占的层数,因为层数的值越大的概率越低,所以最终所有元素层数的分布是具有一定的规律性的。大概就是层数每高一层,该层元素的个数就要减少一半。前提是我们是用的概率是0.5。

我们可以利用随机函数生成随机数,如果随机数小于0.5.level++。如果大于0.5,当前的level就是当前插入元素的层数。

假设当前随机出来的层数level=2。

然后,找到8这个元素在下面两层的前置节点。

接着,就是链表的插入元素操作了,比较简单。

最后,就像下面这样:

跳表的删除

删除其实是跟插入类似的

首先,找到各层中包含元素x的节点。

然后,使用标准的链表删除元素的方法删除即可。

比如,要删除17这个元素。

 

跳表的代码实现

public class SkipList<T extends Comparable<T>> {
    // 当前跳表中最高的level,也就是首列的高度。
    private int maxLevel;
    // 首列,其中没有存储数据,其只是作为一个索引列。
    private Node head;
    // 抛硬币的可能性,level增加的可能性
    private static final float threshold = 0.5f;
    private int size;

    // 表示跳表中的每一列,也就是每一个被存储的元素。
    private class Node {
        public T val;

        public List<Node> nodeList;

        public Node(T val) {
            this.val = val;
            this.nodeList = new ArrayList<>();
        }
    }

    public SkipList() {
        head = new Node(null);
        head.nodeList.add(null);
    }

    public boolean add(T t) {
        if (t == null || contain(t)) {
            return false;
        }
        int level = 0;          // 当前柱的高度
        for (; Math.random() < threshold; ) {
            level++;
        }
        // 扩充首列的level,保证其的高度一定是最高之一。
        for (; level > maxLevel; maxLevel++) {
            head.nodeList.add(null);
        }
        Node newNode = new Node(t);
        int curLevel = maxLevel;
        Node curNode = head;
        do {
            // 从curLevel层开始找小于t的元素中最大的元素的位置。findNextLessThan中只会向右移动
            curNode = findNextLessThan(curNode, t, curLevel);
            if (level >= curLevel) {
                newNode.nodeList.add(0, curNode.nodeList.get(curLevel));
                curNode.nodeList.set(curLevel, newNode);
                level--;
            }
        } while (curLevel-- > 0);
        size++;
        return true;
    }


    public boolean delete(T t) {
        if (t == null) {
            return false;
        }
        int curLevel = maxLevel;
        Node curNode = head, next;

        do {
            for (next = curNode.nodeList.get(curLevel); next != null && lessThan(next.val, t);
                 curNode = next, next = curNode.nodeList.get(curLevel)) {
            }
            if (next != null && t.equals(next.val)) {
                for (; curLevel >= 0; curLevel--) {
                    curNode.nodeList.set(curLevel, next.nodeList.get(curLevel));
                }
                size--;
                return true;
            }
        } while (curLevel-- > 0);
        return false;
    }

    public boolean contain(T t) {
        if (t == null) {
            return false;
        }
        int curLevel = maxLevel;
        Node curNode = head;
        do {
            Node next = findNextLessThan(curNode, t, curLevel);
            if (t == next.val || t.equals(next.val)) {
                return true;
            }
        } while (curLevel-- > 0);
        return false;
    }

    public void printf() {
        Node cur = head.nodeList.get(0);
        for (; cur != null; cur = cur.nodeList.get(0)) {
            System.out.print(cur.val + " ");
        }
    }

    public Node findNextLessThan(Node curNode, T t, int level) {
        for (Node next = curNode.nodeList.get(level); next != null; curNode = next, next = curNode.nodeList.get(level)) {
            if (lessThan(t, next.val)) {
                break;
            }
        }
        return curNode;
    }

    public boolean lessThan(T t, T c) {
        return t.compareTo(c) < 0;
    }

    public int size() {
        return size;
    }
}

 

 

 

标准化的跳表

上面举的例子是完全随机的跳表,那么,如果我们每两个元素提取一个元素作为上一级的索引会怎么样呢?

 

这是不是很像平衡二叉树,现在这颗树元素比较少,可能不太明显,我们来看个元素个数多的情况。

 

可以看到,上一级元素的个数是下一级的一半,这样每次减少一半,就很接近平衡二叉树了。

时间复杂度

我们知道单链表查询的时间复杂度为O(n),而插入、删除操作需要先找到对应的位置,所以插入、删除的时间复杂度也是O(n)。

那么,跳表的时间复杂度是多少呢?

如果按照标准的跳表来看的话,每一级索引减少k/2个元素(k为其下面一级索引的个数),那么整个跳表的高度就是(log n)。

学习过平衡二叉树的同学都知道,它的时间复杂度与树的高度成正比,即O(log n)。

所以,这里跳表的时间复杂度也是O(log n)。(这里不一步步推倒了,只要记住,查询时每次减少一半的元素的时间复杂度都是O(log n),比如二叉树的查找、二分法查找、归并排序、快速排序)

空间复杂度

我们还是以标准的跳表来分析,每两个元素向上提取一个元素,那么,最后额外需要的空间就是:

n/2 + (n/2)^2 + (n/2)^3 + ... + 8 + 4 + 2 = n - 2

所以,跳表的空间复杂度是O(n)。

总结

(1)跳表是可以实现二分查找的有序链表;

(2)每个元素插入时随机生成它的level;

(3)最低层包含所有的元素;

(4)如果一个元素出现在level(x),那么它肯定出现在x以下的level中;

(5)每个索引节点包含两个指针,一个向下,一个向右;

(6)跳表查询、插入、删除的时间复杂度为O(log n),与平衡二叉树接近;

彩蛋

为什么Redis选择使用跳表而不是红黑树来实现有序集合?

首先,我们来分析下Redis的有序集合支持的操作:

1)插入元素

2)删除元素

3)查找元素

4)有序输出所有元素

5)查找区间内所有元素

其中,前4项红黑树都可以完成,且时间复杂度与跳表一致。

但是,最后一项,红黑树的效率就没有跳表高了。

在跳表中,要查找区间的元素,我们只要定位到两个区间端点在最低层级的位置,然后按顺序遍历元素就可以了,非常高效。

而红黑树只能定位到端点后,再从首位置开始每次都要查找后继节点,相对来说是比较耗时的。



部分引用自:https://www.jianshu.com/p/dd01e8dc4d1f
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值