多图秒解【跳表】数据结构设计

跳表

想必学后端的都知道Redis的Zset实现了跳表,如果不知道可以看下面描述。

跳表概念

跳表结构可以简单理解为通过在有序单链表添加多级索引结构,以实现类似二分法的操作行为的数据结构,本质也是空间换时间

但这多级的结点究竟是如何维护的呢? 本文参考-宫水三叶的Leetcode题解的描述与代码去探索跳表的初步实现。

设计一个跳表结构之前要考虑的细节

【跳表结点】结构的设计

我们都知道跳表的逻辑结构大概是这样的:

但是,如果按照这个逻辑结构去编写代码,后面的增删查就会难以下手。 因此,实际代码可以把他变成树形结构,如下图:

图中标灰色的为索引指针。 代码实现如下:

java 复制代码
        public static class Node {
            //1.值
            public int val;
            //2.【指针数组】
            //level表示索引限制的层级
            //如上面图,结点30的next数组值为
            //next[0]表示结点40,next[1]表示结点50,next[2]表示结点70 ,next[3->9]没有结点
            public Node[] next = new Node[level];

            public Node(int val) {
                this.val = val;
            }

        }

跳表三个操作的设计与实现

跳表维护分成三个操作:添加、删除、查询。 前面提到,跳表本质是一个添加索引的有序的单链表 ,那么添加、删除 这两个操作也会依赖于查询。 查询操作只需要把索引用起来就好了,而添加和删除操作需要进行对索引的维护。

查询操作

查询操作是最好实现的,特别是只需要判断某个值在这个跳表中存不存在。 然而,考虑到后面的添加和删除操作需要对索引进行维护,我们在查询某个值时,需要把查询这个过程中涉及的索引记录下来。 比如要查询60,我们需要记录下图标红的结点(30、50、50)

java 复制代码
        //设置层级为10层
        private final static int level = 10;
        //头结点指针(链表题目惯有操作,引入虚拟头结点,可以省去一堆if-else的逻辑)
        private Node head = new Node(-1);

        private Node[] find(int target) {
            Node[] ans = new Node[level];
            Node cur = head;
            for (int i = level - 1; i >= 0; i--) {
                //从高层往低层找,找到每一层<target的最后一个结点
                //如果下一个结点不为空且值小于target,当前指针才能向右移动
                while (cur.next[i] != null && cur.next[i].val < target) {
                    cur = cur.next[i];
                }
                ans[i] = cur;
            }
            return ans;
        }

返回值Node[]就是上图标红的结点数组。

  • 要判断跳表中是否存在值,代码如下:
java 复制代码
        public boolean search(int target) {
            Node[] nodes = find(target);
            //如上面图最底层的红色结点50,50的下一个结点就是绿色结点60,就是答案。
            return nodes[0].next[0] != null && nodes[0].next[0].val == target;
        }
添加操作

添加结点操作复用上面find(target)方法,然后从下往上添加索引。 并不是每次添加操作都会添加索引,因此添加跳表索引时机是通过随机数实现的。(每次让其有一半的概率会向上添加索引,越往上添加索引的概率越低)

java 复制代码
        private ThreadLocalRandom random = ThreadLocalRandom.current();

        public void add(int num) {
            Node[] nodes = find(num);
            //创建新结点
            Node newNode = new Node(num);
            for (int i = 0; i < level; i++) {
                //从下往上,将上图红色结点后面插入创建的新结点
                newNode.next[i] = nodes[i].next[i];
                nodes[i].next[i] = newNode;
                //一般的概率向上继续添加索引
                if (random.nextInt(2) == 0) {
                    break;
                }
            }
        }

还是这个例子,find的返回值如上图结点。 插入55,第一次random就跳出循环的结果是:

如果是第二次才跳出循环,结果是:

删除操作

删除操作也一样从下往上删除,代码如下:

java 复制代码
        public boolean erase(int num) {
            Node[] nodes = find(num);
            //不存在num这个数
            if (nodes[0].next[0] == null || nodes[0].next[0].val != num) {
                return false;
            }
            //删除上面红色结点列表的右侧结点值为num的结点
            for (int i = 0; i < level; i++) {
                if (nodes[i].next[i] == null || nodes[i].next[i].val != num) {
                    break;
                }
                //删除结点
                nodes[i].next[i] = nodes[i].next[i].next[i];
            }
            return true;
        }

依然是原来这个例子,不过这次操作是删除结点50,过程如下:

完整代码

java 复制代码
    public class Skiplist {
        public static class Node {
            //1.值
            public int val;
            //2.指针数组
            public Node[] next = new Node[level];

            public Node(int val) {
                this.val = val;
            }

        }

        //设置层级为10层
        private final static int level = 10;
        //头结点指针(链表题目惯有操作,引入虚拟头结点,可以省去一堆if-else的逻辑)
        private Node head = new Node(-1);

        private Node[] find(int target) {
            Node[] ans = new Node[level];
            Node cur = head;
            for (int i = level - 1; i >= 0; i--) {
                //从高层往低层找,找到每一层<target的最后一个结点
                //如果下一个结点不为空且值小于target,当前指针才能向右移动
                while (cur.next[i] != null && cur.next[i].val < target) {
                    cur = cur.next[i];
                }
                ans[i] = cur;
            }
            return ans;
        }

        public boolean search(int target) {
            Node[] nodes = find(target);
            //如上面图最底层的红色结点。
            return nodes[0].next[0] != null && nodes[0].next[0].val == target;
        }

        private ThreadLocalRandom random = ThreadLocalRandom.current();

        public void add(int num) {
            Node[] nodes = find(num);
            //创建新结点
            Node newNode = new Node(num);
            for (int i = 0; i < level; i++) {
                //从下往上,将上图红色结点后面插入创建的新结点
                newNode.next[i] = nodes[i].next[i];
                nodes[i].next[i] = newNode;
                //一般的概率向上继续添加索引
                if (random.nextInt(2) == 0) {
                    break;
                }
            }
        }

        public boolean erase(int num) {
            Node[] nodes = find(num);
            //不存在num这个数
            if (nodes[0].next[0] == null || nodes[0].next[0].val != num) {
                return false;
            }
            //删除上面红色结点列表的右侧结点值为num的结点
            for (int i = 0; i < level; i++) {
                if (nodes[i].next[i] == null || nodes[i].next[i].val != num) {
                    break;
                }
                //删除结点
                nodes[i].next[i] = nodes[i].next[i].next[i];
            }
            return true;
        }
    }

有帮助的话点个赞~,感谢阅读!

相关推荐
知来者逆1 小时前
计算机视觉——速度与精度的完美结合的实时目标检测算法RF-DETR详解
图像处理·人工智能·深度学习·算法·目标检测·计算机视觉·rf-detr
阿让啊1 小时前
C语言中操作字节的某一位
c语言·开发语言·数据结构·单片机·算法
এ᭄画画的北北1 小时前
力扣-160.相交链表
算法·leetcode·链表
爱研究的小陈2 小时前
Day 3:数学基础回顾——线性代数与概率论在AI中的核心作用
算法
渭雨轻尘_学习计算机ing2 小时前
二叉树的最大宽度计算
算法·面试
BB_CC_DD3 小时前
四. 以Annoy算法建树的方式聚类清洗图像数据集,一次建树,无限次聚类搜索,提升聚类搜索效率。(附完整代码)
深度学习·算法·聚类
梁下轻语的秋缘4 小时前
每日c/c++题 备战蓝桥杯 ([洛谷 P1226] 快速幂求模题解)
c++·算法·蓝桥杯
CODE_RabbitV4 小时前
【深度强化学习 DRL 快速实践】逆向强化学习算法 (IRL)
算法
mit6.8245 小时前
[贪心_7] 最优除法 | 跳跃游戏 II | 加油站
数据结构·算法·leetcode
keep intensify5 小时前
通讯录完善版本(详细讲解+源码)
c语言·开发语言·数据结构·算法