Skip Lists跳表及Java实现

简介: Skip Lists 跳表 Java 实现

1、源码

源码地址: GITHUB
https://github.com/fofcn/operation-system/blob/main/%E5%AE%9E%E8%B7%B5/os/src/main/java/lang/skiplists/SkipLists.java

2、概念

跳跃表命名的原因是因为作者认为链表结构使用了额外的节点之间的指针。
跳跃表是一种可能的平衡树的替换方法。跳跃表是通过查询一个随机数生成器来保持平衡。尽管跳跃表在最坏情况性能特别差,但是在平均情况下跳跃表的性能很好。

跳跃表的关键概念是有序,索引节点有序,数据节点有序,这样层层查找才能进行二分查找。
下图为跳跃表的模型图:
1.png

3、跳跃表

通常列表的搜索需要列表链表中的每一个节点是否和搜索内容一致。如果列表是有序的,并且列表中每两个节点都有一个指向该节点的指针,那么我们检查的次数不会超过n/2 + 1次。如果每四个节点都有一个指向该节点的指针,那么需要不超过n/4 + 2次检查就可以找到元素。如果每个2^i个节点都有一个节点指向它,那么检查次数可以减少到log(n)次。跳跃表可以用来快速搜索,但是插入和删除的运行时间会比较长。

4、跳跃表算法

这部分给出跳跃表在字典或符号表应用中的搜索、插入和删除算法。

4.1、搜索

搜索操作返回key关联的value值,如果失败则返回不存在。下图给出了搜索路径(图片来自原)
2.png

4.2、插入

插入操作将一个key对应的值插入到跳跃表中,如果插入的key已经存在则更新值。
3.png

4.3、删除

删除操作来删除指定的key。
其他操作也很容易支持,比如:查找最大或最小的key。
删除完成后需要检查是否需要减少索引层数。

5、算法实现

5.1、搜索

/**
     * 根据key从跳跃表中获取一个值
     * @param key key
     * @return 存在则返回对应的value,不存在则返回null
     */
    public Value get(Key key) {
        // 参数检查
        if (key == null) {
            throw new IllegalArgumentException();
        }

        // 找到key对应的前驱节点
        Node<Key, Value> predecessor = findPredecessor(key);
        // 从前驱节点开始遍历查找key对应的节点
        for (Node<Key, Value> next = predecessor.next;;) {
            if (next == null) {
                break;
            }

            // key与节点的Key比较
            // 如果相等就找到了直接返回值
            // 如果key大于节点的key,继续向后查找
            // 如果key小于节点的key,那么没找到,退出循环
            int cmp = key.compareTo(next.key);
            if (cmp == 0) {
                return next.value;
            } else if (cmp > 0) {
                next = next.next;
            } else {
                break;
            }
        }

        return null;
    }

5.2、插入

/**
     * 向跳跃表添加一个key-value对
     * @param key key
     * @param value value
     */
    public void put(Key key, Value value) {
        if (key == null) {
            throw new NullPointerException();
        }

        Node<Key, Value> newNode = null;
        // 找到最底层插入节点的前驱节点
        Node<Key, Value> predecessor = findPredecessor(key);

        // 找到索引节点对应的数据节点以后,开始查找插入数据前驱节点
        for (Node<Key, Value> b = predecessor, next = b.next;;) {
            if (next != null) {
                // 如果插入的key大于当前数据节点,那么继续查找下一个
                int cmp = key.compareTo(next.key);
                if (cmp > 0) {
                    b = next;
                    next = next.next;
                    continue;
                } else if (cmp == 0) {
                    next.value = value;
                    break;
                }
            }

            // 新建节点并更改前驱节点的下一个节点为新节点
            newNode = new Node<>(key, value, next);
            b.next = newNode;
            break;
        }

        // 是否要为数据节点添加索引层
        int level = randomLevel();

        // 需要加层
        if (level > header.level) {
            int oldLevel = header.level;
            int newLevel = level;

            // 为新节点创建索引节点
            Index<Key, Value>[] newNodeIndexes = createNewNodeIndex(newNode, newLevel);
            // 为头结点增量补充索引节点,并将头结点的索引节点指向新节点的索引节点
            header = incrHeaderIdxes(newLevel, newNodeIndexes);

            // 根据老层更新新节点的数据
            updateIndex(key, newNodeIndexes[oldLevel], oldLevel, newLevel);
        } else {
            // 如果节点索引层大于1就需要为节点新建索引层
            if (level > 1) {
                // 根据新节点索引层新建节点索引
                Index<Key, Value>[] newNodeIndexes = createNewNodeIndex(newNode, level);
                // 更新索引
                // 在没有新建层时,为新节点新建层传入的新层参数是头索引的层数,因为每次都从头索引开始查找,
                // 需要将头索引直接下降到对应的层后开始修改关系
                updateIndex(key, newNodeIndexes[level], level, header.level);
            }
        }
    }

5.3、删除

/**
     * 根据key删除一个节点
     * 注意删除节点可能需要减层
     * @param key 要删除的关键字
     * @return key对应的value值,如果没有找到value就返回null
     */
    public Value delete(Key key) {
        if (key == null) {
            throw new NullPointerException();
        }

        Value val = null;

        // 找到最底层插入节点的前驱节点
        Node<Key, Value> predecessor = findPredecessor(key);
        for (Node<Key, Value> b = predecessor, next = b.next;;) {
            if (next != null) {
                // 如果插入的key大于当前数据节点,那么继续查找下一个
                int cmp = key.compareTo(next.key);
                if (cmp > 0) {
                    b = next;
                    next = next.next;
                    continue;
                } else if (cmp < 0) {
                    break;
                } else {
                    // 相等就将节点元素设置为空
                    val = next.value;
                    next.value = null;
                    break;
                }
            }

            break;
        }

        // 通过查找前驱索引节点删除可能需要删除的索引
        // 删除索引的标记信息就是node.value==null
        findPredecessor(key);
        // 删除层
        // 如果头索引的右侧索引已经被删除就减层
        while (header.right == null && header.level > 1) {
            header = (HeaderIndex<Key, Value>) header.down;
        }

        return val;
    }

5.4、其他关键实现

5.4.1、查找key对应的前驱索引

/**
     * 查找key对应的前驱索引
     * @param key key
     * @return 前驱索引
     */
    private Index<Key, Value> findIndex(Key key) {
        for (Index<Key, Value> cur = header, right = cur.right, down;;) {
            // 如果索引的右侧不为空
            // 用搜索的key对数据节点的key进行比较
            // 如果搜索的key大于索引节点的key,那么继续向右进行搜索
            if (right != null) {
                Node<Key, Value> n = right.node;
                Key k = n.key;
                // value为空代表节点的值已经被删除
                // 删除节点对应的索引
                if (n.value == null) {
                    // 将当前所有的右侧索引更新为右侧的右侧
                    cur.right = right.right;
                    // 更新right索引变量为当前索引的右侧
                    right = cur.right;
                    continue;
                }

                // 如果搜索的key大于右侧节点指向的key,那么继续向右查找
                if (key.compareTo(k) > 0) {
                    cur = right;
                    right = right.right;
                    continue;
                }
            }

            // 如果索引节点的右侧节点大于key,那么向下放索引查找
            if ((down = cur.down) != null) {
                // 将当前节点指向下方索引节点
                // 右侧节点指针指向下方索引的右侧
                cur = down;
                right = down.right;
            } else {
                return cur;
            }
        }
    }

5.4.2、随机生成节点层

/**
     * 随机生成节点的层,但是不超过32
     * @return 新节点层数
     */
    private int randomLevel() {
        int level = 1;
        int rnd = ThreadLocalRandom.current().nextInt(1, 101);
        while (rnd > PROBABILITY && level <= MAX_LEVEL) {
            // 根据随机数来生成索引层数
            ++level;
            rnd = ThreadLocalRandom.current().nextInt(1, 101);
        }
        return level;
    }

5.4.3、更新索引

/**
     * 更新老层的索引
     * @param key 关键字
     * @param newNodeOldIdx 新节点索引
     * @param oldLevel 老层数
     * @param newLevel 新层数
     */
    private void updateIndex(Key key, Index<Key, Value> newNodeOldIdx, int oldLevel, int newLevel) {
        Index<Key, Value> newNodeIdx = newNodeOldIdx;
        Index<Key, Value> precursorIdx = header;

        // 跳过新索引层,因为已经做了关联
        for (int i = oldLevel + 1; i <= newLevel; i++) {
            precursorIdx = precursorIdx.down;
        }
        Index<Key, Value> right = precursorIdx.right;

        // 找到对应的层之后,我们开始向右继续查找前驱索引节点
        while (true) {
            if (right != null && right.node.value != null) {
                int cmp = key.compareTo(right.node.key);
                if (cmp > 0) {
                    precursorIdx = right;
                    right = right.right;
                    continue;
                }
            }

            // 找到需要更新的索引之后,重建索引
            // 前驱索引节点的右侧设置新的索引
            precursorIdx.right = newNodeIdx;
            // 新索引有右侧设置为老索引的右侧节点
            newNodeIdx.right = right;
            // 新节点索引向下
            newNodeIdx = newNodeIdx.down;
            // 老索引向下
            precursorIdx = precursorIdx.down;
            if (precursorIdx == null) {
                break;
            }
        }
    }

6、总结

  1. 跳跃表特点就是通过跳跃的方式组织数据,相当于给数据建了一个多级索引,思想有点像二分查找
  2. 跳跃表更容易实现并且运行效率也很高
  3. 跳跃表使用了空间换取时间的设计思想

7、参考

  1. https://www.cl.cam.ac.uk/teaching/0506/Algorithms/skiplists.pdf
  2. JDK1.8 ConcurrentSkipListMap源码
目录
相关文章
|
Java
Java 实现汉字按照首字母分组排序
Java 实现汉字按照首字母分组排序
561 0
|
分布式计算 Java Hadoop
Java实现单词计数MapReduce
本文分享实现单词计数MapReduce的方法
301 0
|
Java 数据安全/隐私保护
JAVA 实现上传图片添加水印(详细版)(上)
JAVA 实现上传图片添加水印(详细版)
933 0
JAVA 实现上传图片添加水印(详细版)(上)
|
存储 Java
Java实现图书管理系统
本篇文章是对目前Java专栏已有内容的一个总结练习,希望各位小主们在学习完面向对象的知识后,可以阅览本篇文章后,自己也动手实现一个这样的demo来加深总结应用已经学到知识并进行巩固。
374 0
Java实现图书管理系统
|
Java Windows Spring
java实现spring boot项目启动时,重启Windows进程
java实现spring boot项目启动时,重启Windows进程
475 0
|
数据可视化 Java
Java实现拼图小游戏(1)—— JFrame的认识及界面搭建
如果要在某一个界面里面添加功能的话,都在一个类中,会显得代码难以阅读,而且修改起来也会很困难,所以我们将游戏主界面、登录界面、以及注册界面都单独编成一个类,每一个类都继承JFrame父类,并且在类中创建方法来来实现页面
458 0
Java实现拼图小游戏(1)—— JFrame的认识及界面搭建
|
网络协议 Java
Java网络编程:UDP/TCP实现实时聊天、上传图片、下载资源等
ip地址的分类: 1、ipv4、ipv6 127.0.0.1:4个字节组成,0-255,42亿;30亿都在北美,亚洲就只有4亿 2011年就用尽了。
Java网络编程:UDP/TCP实现实时聊天、上传图片、下载资源等
|
数据可视化 Java 容器
Java实现拼图小游戏(7)—— 计步功能及菜单业务的实现
注意由于我们计步功能的步数要在重写方法中用到,所以不能将初始化语句写在方法体内,而是要写在成员位置。在其名字的时候也要做到“见名知意”,所以我们给它起名字为step
266 0
Java实现拼图小游戏(7)—— 计步功能及菜单业务的实现
|
Java
Java实现拼图小游戏(7)—— 作弊码和判断胜利
当我们好不容易把拼图复原了,但是一点提示也没有,完全看不出来是成功了,那么我们就需要有判断胜利的功能去弹出“成功”类的图片,以便于玩家选择是重新开始还是退出小游戏
239 0
Java实现拼图小游戏(7)—— 作弊码和判断胜利
|
Java
Java实现拼图小游戏(7)——查看完整图片(键盘监听实例2)
由于在移动和图片中我们已经添加了键盘监听,也继承了键盘监听的接口,那么我们只需要在重写方法内输入我们的代码即可
174 0