数据结构之跳跃表


跳跃链表又称“跳表”,是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好。在 redis的底层实现中就采用了跳跃表

引入

在单链表中查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过二分的方式缩减时间复杂度。 如下图:
在这里插入图片描述
但是如果我们在有序的单链表上加入多层索引,如下图,此时它的查找效率就类似于二分查找
在这里插入图片描述
访问46需要6次查询。即L4访问55,L3访问21、55,L2访问37、55,L1访问46。此时时间复杂度为O(logn)。

性质

跳表具有如下性质:

(1) 由很多层结构组成

(2) 每一层都是一个有序的链表

(3) 最底层(Level 1)的链表包含所有元素

(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。

(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

实现原理

  1. 上面引入的是最理想的跳跃表,但是如果想要在上图中插入或者删除一个元素呢?比如我们要插入一个元素22、23、24……,自然在L1层,我们将这些元素插入在元素21后,那么L2层,L3层呢?我们是不是要考虑插入后怎样调整连接,才能维持这个理想的跳跃表结构。我们知道,平衡二叉树的调整是一件令人头痛的事情,左旋右旋左右旋……一般人还真记不住,而调整一个理想的跳跃表将是一个比调整平衡二叉树还复杂的操作。幸运的是,我们并不需要通过复杂的操作调整连接来维护这样完美的跳跃表。有一种基于概率统计的插入算法,也能得到时间复杂度为O(logn)的查询效率,这种跳跃表才是我们真正要实现的。

  2. 先讨论插入,我们先看理想的跳跃表结构,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次。这样,我们能在跳跃表中插入一个元素了。 当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。

代码实现

  1. 定义
import java.util.Random;

public class SkipList {
    // height
    public int h;
    // 表头
    private SkipListEntry head;
    // 表尾
    private SkipListEntry tail;
    // 生成randomLevel用到的概率值
    private Random r;

    public SkipList() {
        //链表头节点
        head = new SkipListEntry(Integer.MIN_VALUE, Integer.MIN_VALUE);
        //链表尾节点
        tail = new SkipListEntry(Integer.MAX_VALUE, Integer.MAX_VALUE);
        head.right =tail;
        tail.left = head;
        h = 0;
        r = new Random();
    }

    /**
     * 跳表节点
     */
    static class SkipListEntry {
        Integer key;
        Integer value;
        SkipListEntry right;
        SkipListEntry left;
        SkipListEntry down;
        SkipListEntry up;
        public SkipListEntry(Integer key, Integer value) {
            this.key = key;
            this.value = value;
        }
    }
}
  1. 查找
 	/**
     * 查找
     * @param key
     * @return
     */
    public Integer get(int key) {

        SkipListEntry p;

        p = findEntry(key);

        if(p.key ==key) {
            return p.value;
        } else {
            return null;
        }

    }

    /**
     * 根据key查找结点
     * @param key
     * @return 返回key相等的节点或插入的前驱
     */
    public SkipListEntry findEntry(Integer key)
    {
        SkipListEntry p;
        p = head;
        /**
         * 基本逻辑是从head(跳表的最高层链表的头结点)开始自右开始查找,
         * 当找到该层链表的最接近且小于指定key的节点时,往下开始查找,
         * 最终找到最底层的那个节点
         */
        while ( true )
        {
            while ( p.right.key != Integer.MAX_VALUE && p.right.key< key )
            {
                p = p.right;
            }
            if ( p.down != null )
            {
                p = p.down;
            }
            else
                break;
        }
        return(p);         // p.key <= key
    }

  1. 插入
  /**
     * 插入
     * @param key
     * @param value
     * @return 如果跳跃表中存在含有key值的节点,则替换新值返回旧值
     */
 public Integer put(int key, int value) {
        SkipListEntry p, q;
        int i = 0;
        // 查找适合插入的位子
        p = findEntry(key);
        // 如果跳跃表中存在含有key值的节点,则进行value的修改操作即可完成
        if(p.key ==key) {
            Integer oldValue = p.value;
            p.value = value;
            return oldValue;
        }
        // 如果跳跃表中不存在含有key值的节点,则进行新增操作
        q = new SkipListEntry(key, value);
        q.left = p;
        q.right = p.right;
        p.right.left = q;
        p.right = q;

        //本层操作完毕,看更高层操作
        //抛硬币随机决定是否上层插入
        while ( r.nextDouble() < 0.5 )
        {
            if ( i >= h )
            {
                //如果i大于最大层次,则创建新的层
                addEmptyLevel();
            }
            //找到第一个上层索引
            while ( p.up == null )
            {
                p = p.left;
            }
            p = p.up;
            SkipListEntry e;
            // 创建上层索引节点,这里需要注意的是除底层节点之外的节点对象是不需要value值的
            e = new SkipListEntry(key, null);
            e.left = p;
            e.right = p.right;
            e.down = q;
	        //把e插入到上层
            p.right.left = e;
            p.right = e;
            q.up = e;

            //把e当作新插入的节点,递归执行
            q = e;
            // level增加
            i = i + 1;


        }
        return null;
    }


    /**
     * 创建新的空索引层
     */
    private void addEmptyLevel() {

        SkipListEntry p1, p2;

        p1 = new SkipListEntry(Integer.MIN_VALUE, null);
        p2 = new SkipListEntry(Integer.MAX_VALUE, null);

        p1.right = p2;
        p1.down = head;

        p2.left = p1;
        p2.down = tail;

        head.up = p1;
        tail.up = p2;

        head = p1;
        tail = p2;

        h = h + 1;
    }
  1. 删除
	/**
     * 删除
     * @param key
     * @return 返回删除的值
     */
    public Integer remove(int key) {

        SkipListEntry p, q;
        //查找到包含key值的节点
        p = findEntry(key);

        if(!p.key.equals(key)) {
            return null;
        }

        Integer oldValue = p.value;
        //将节点p从链表中移除,接着如果有更高level的节点,则重复此操作。
        while(p != null) {
            q = p.up;
            //断链
            p.left.right = p.right;
            p.right.left = p.left;
            //递归
            p = q;
        }

        return oldValue;
    }

参考链接:
https://blog.csdn.net/qpzkobe/article/details/80056807
https://blog.csdn.net/bohu83/article/details/83654524

发布了15 篇原创文章 · 获赞 4 · 访问量 1074

猜你喜欢

转载自blog.csdn.net/a15723207292/article/details/98173581