搜索、添加、删除均为O(logn)的数据结构——跳表

news2024/9/21 22:45:28

文章目录

  • 有序数组和链表的对比
    • 有序数组
    • 有序链表
  • 跳表
    • 跳表的搜索
    • 跳表的插入
    • 跳表的删除
  • 跳表完整Java实现代码(包含上面介绍的所有功能模块)

有序数组和链表的对比

有序数组

有序数组支持高效随机访问,可以使用二分查找使得查找的时间复杂度为O(lgn),但插入和删除的时间复杂度为O(n)。
如图:先通过二分查找锁定插入位置,再移动该位置后面元素腾出空间,最后插入值
在这里插入图片描述

有序链表

有序链表在找到位置后,插入和删除快,但链表没有数组那样的高效随机访问,无法使用二分查找,只能一个节点一个节点遍历,所以链表查找、插入、删除的时间复杂度均为O(n)。
如图:先顺序遍历找到指定位置,插入即可
在这里插入图片描述
可以看出,有序数组和有序链表各有优势,有序数组查找快但插入慢,有序链表插入块但查找慢,有没有什么办法让有序链表搜索、添加、删除的平均时间复杂度降低至O(logn)?

跳表

有,一条有序链表不够,可以多加几条。(空间换时间)
如图:
在这里插入图片描述

跳表的搜索

1、从顶层链表的首元素开始,从左往右搜索,直至找到一个大于或等于目标的元素,或者到达当前层链表的尾部
2、如果该元素等于目标元素,则表明该元素已被找到
3、如果该元素大于目标元素或已到达链表的尾部,则退回到当前层的前一个元素,然后转入下一层进行搜索
如图:
在这里插入图片描述
寻找值为19的结点,先在最上面层查找到第一个大于19的结点21,记录它前面的结点17,再从下一层的17结点开始寻找,能快速找到19结点。

跳表的插入

1、在插入节点之前,我们需要搜索得到插入的位置
2、插入元素时,需要随机决定新添加元素的层数
如图所示:
在这里插入图片描述
插入17需要找到所有有序链表的17节点前一个结点。

跳表的删除

如果要删除节点,则把节点和对应的所有索引节点全部删除即可。当然,要删除节点时需要先搜索得到该节点,搜索过程中可以把路径记录下来,这样删除索引层节点的时候就不需要多次搜索了。
删除一个元素后,整个跳表的层数可能会降低。
如图所示:
在这里插入图片描述

跳表完整Java实现代码(包含上面介绍的所有功能模块)

/**
 * @Author hepingfu
 * @Date 2023/05/09/16:03
 * @Version 1.0
 */
public class SkipList {
    /**
     * 最高层数16层
     */
    private static final int MAX_LEVEL = 16;

    /**
     * 每层上升概率0.5,新节点的高度是随机的,这里给定概率0.5
     */
    private static final double P = 0.5;

    /**
     * 当前有效层数
     */
    private int level;

    /**
     * 伪首节点,不存放任何键值对
     */
    private Node first;

    /**
     * 默认构造器创建伪首节点
     */
    public SkipList() {
        first = new Node(null, null, MAX_LEVEL);
    }

    /**
     * 节点类
     */
    private static class Node {
        Integer key;
        Integer value;
        Node[] nexts;

        public Node(Integer key, Integer value, int level) {
            this.key = key;
            this.value = value;
            nexts = new Node[level];
        }
        @Override
        public String toString() {
            return key + ":" + value + "_" + nexts.length;
        }
    }

    /**
     * 查找
     * @param key
     * @return
     */
    public Integer get(Integer key) {
        keyCheck(key);

        Node node = first;
        for (int i = level - 1; i >= 0; i--) {
            /**
             * 当下一个节点非空,
             * 且当前键小于下一个节点的键时,
             * while循环继续
             */
            while (node.nexts[i] != null
                    && key < node.nexts[i].key) {
                node = node.nexts[i];
            }

            //相等直接返回,否则i减1,走下面一层
            if (key == node.nexts[i].key) return node.nexts[i].value;
        }

        // for循环结束出来,就是没找到,返回空
        return null;
    }

    /**
     * 插入节点
     * @param key
     * @param value
     * @return
     */
    public Integer put(Integer key, Integer value) {
        keyCheck(key);
        Node node = first;
        Node[] prevs = new Node[level];  // 记录每层前面的结点
        for (int i = level - 1; i >= 0; i--) {
            /**
             * 当下一个节点非空,
             * 且当前键小于下一个节点的键时,
             * while循环继续
             */
            while (node.nexts[i] != null
                    &&  key < node.nexts[i].key) {
                node = node.nexts[i];
            }
            if (key == node.nexts[i].key) { // 节点是存在的
                Integer oldValue = node.nexts[i].value;
                node.nexts[i].value = value;
                return oldValue;
            }
            //prevs用来记录每一层要插入前的节点
            prevs[i] = node;
        }

        // 新节点的层数,随机产生
        int newLevel = randomLevel();
        // 添加新节点
        Node newNode = new Node(key, value, newLevel);
        // 设置前驱和后继
        for (int i = 0; i < newLevel; i++) {
            if (i >= level) {
                first.nexts[i] = newNode;
            } else {
                newNode.nexts[i] = prevs[i].nexts[i];
                prevs[i].nexts[i] = newNode;
            }
        }

        // 计算跳表的最终层数
        level = Math.max(level, newLevel);
        return null;
    }

    public Node remove(Integer key) {
        keyCheck(key);

        Node node = first;
        Node[] prevs = new Node[level];
        boolean exist = false;
        for (int i = level - 1; i >= 0; i--) {
            /**
             * 当下一个节点非空,
             * 且当前键小于下一个节点的键时,
             * while循环继续
             */
            while (node.nexts[i] != null
                    && key < node.nexts[i].key) {
                node = node.nexts[i];
            }
            prevs[i] = node;
            if (key == node.nexts[i].key) exist = true;
        }
        if (!exist) return null;

        // 需要被删除的节点
        Node removedNode = node.nexts[0];

        // 设置后继
        for (int i = 0; i < removedNode.nexts.length; i++) {
            prevs[i].nexts[i] = removedNode.nexts[i];
        }

        // 删除后更新跳表的层数
        int newLevel = level;
        while (--newLevel >= 0 && first.nexts[newLevel] == null) {
            level = newLevel;
        }

        return removedNode;
    }

    /**
     * 插入时level变化
     * @return
     */
    private int randomLevel() {
        int level = 1;
        while (Math.random() < P && level < MAX_LEVEL) {
            level++;
        }
        return level;
    }

    /**
     * 判断key是否违规
     * @param key
     */
    private void keyCheck(Integer key) {
        if (key == null) {
            throw new IllegalArgumentException("key must not be null.");
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("一共" + level + "层").append("\n");
        for (int i = level - 1; i >= 0; i--) {
            Node node = first;
            while (node.nexts[i] != null) {
                sb.append(node.nexts[i]);
                sb.append(" ");
                node = node.nexts[i];
            }
            sb.append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        SkipList skipList = new SkipList();

        for (int i = 0; i < 20; i++) {
            skipList.put(i, i + 10);
        }

        System.out.println(skipList.toString());
    }

}

ps:计划每日更新一篇博客,今日2023-05-07,日更第二十一天。(9号补更)
昨日更新:

leetcode 104——二叉树的最大深度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/506088.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SEO外包是为了省钱还是为了更好的效果呢?我们的探讨!

SEO是指优化网站以获得更高的自然搜索引擎排名的一系列技术和方法&#xff0c;它可以提高网站的可见性、流量和收入。随着互联网的不断发展&#xff0c;越来越多的企业意识到了SEO的重要性&#xff0c;但是&#xff0c;由于技术难度和人力成本的限制&#xff0c;许多企业选择将…

TikTok选品思路你真的知道吗

TikTok是一款非常流行的短视频社交应用程序&#xff0c;用户群体广泛&#xff0c;主要集中在年轻人之间&#xff0c;越来越多跨境商家布局TikTok。那么在TikTok上进行选品时&#xff0c;要采用什么思路和方法才能让自己的产品比较出众吸睛并且让自己财源滚滚呢&#xff1f; 一、…

阿里云服务器镜像是什么?镜像系统怎么选择?

阿里云服务器镜像是云服务器的装机盘&#xff0c;镜像是为云服务器安装操作系统的。云服务器镜像系统怎么选择&#xff1f;云服务器操作系统镜像分为Linux和Windows两大类&#xff0c;Linux可以选择Alibaba Cloud Linux&#xff0c;Windows可以选择Windows Server 2022数据中心…

BI 数字化转型对央企和中小企业的利好

数字化产品和服务的大规模应用也让数字经济、数据价值得到了众多企业的重视。先说数字经济&#xff0c;据统计&#xff0c;2020年全球数字经济规模达到32.61万亿美元&#xff0c;与GDP总量比例为43.7%&#xff0c;其中中国数字经济规模达到了5.4万亿美元&#xff0c;并维持着9.…

手麻系统源码,PHP手术麻醉临床信息系统源码,手术前管理模块功能

手麻系统源码&#xff0c;PHP手术麻醉临床信息系统源码&#xff0c;手术前管理模块功能 术前管理模块主要有手术排班、手术申请单、手术通知单、手术知情同意书、输血血液同意书、术前查房记录、术前访视、风险评估、手术计划等功能。 功能&#xff1a; 手术排班&#xff1a;…

免费换电池还倒塞用户200,iPhone惨遭“耐用”反噬

多年来 iPhone 最为诟病的几个缺点之一肯定少不了电池。 在 Android 已经普及4、5000mAh 大电池加快充组合的情况下&#xff0c;iPhone 仅有 Pro Max&#xff08;Plus&#xff09;勉强及格。 更难受的是今年3月&#xff0c;官方换电池还迎来了幅度不小的涨价。 苹果这是在建议我…

STM32F4_RTC实时时钟

目录 1. RTC实时时钟简介 2. RTC框图 3. RTC初始化和配置 3.1 RTC和低功耗模式 3.2 RTC中断 4. RTC相关寄存器 4.1 时间寄存器&#xff1a;RTC_TR 4.2 日期寄存器&#xff1a;RTC_DR 4.3 亚秒寄存器&#xff1a;RTC_SSR 4.4 控制寄存器&#xff1a;RTC_CR 4.5 RTC初…

【LoadRunner】教你快速编写一个性能测试脚本 demo

目录 LoadRunner 工具介绍 UI性能测试步骤 编写性能测试脚本&#xff08;VUG&#xff09; 创建测试场景&#xff08;Controller&#xff09; 生成测试报告&#xff08;Analysis&#xff09; LoadRunner 工具介绍 我们使用以上三个工具针对我们的项目进行性能测试。 a&am…

3U VPX 总线架构+ 2片国防科大银河飞腾 FT-M6678 多核浮点运算 DSP 设计资料--VPX303

板卡概述 VPX303 是一款基于 3U VPX 总线架构的高性能信号处理板&#xff0c;板载 2 片国防科大银河飞腾 FT-M6678 多核浮点运算 DSP&#xff0c;可以实现各 种实时性要求较高的信号处理算法。 板卡每个 DSP 均支持 5 片 DDR3 SDRAM 实现数据缓存&#xff0c;两片 DSP 之…

( 位运算 ) 268. 丢失的数字 ——【Leetcode每日一题】

❓268. 丢失的数字 难度&#xff1a;简单 给定一个包含 [0, n] 中 n 个数的数组 nums &#xff0c;找出 [0, n] 这个范围内没有出现在数组中的那个数。 示例 1&#xff1a; 输入&#xff1a;nums [3,0,1] 输出&#xff1a;2 解释&#xff1a;n 3&#xff0c;因为有 3 个数…

正则表达式 - 匹配 Unicode 和其他字符

目录 一、匹配 Unicode 字符 1. 匹配 emoji 符号 &#xff08;1&#xff09;确定 emoji 符号的 Unicode 范围 &#xff08;2&#xff09;emoji 符号的存储 &#xff08;3&#xff09;正则表达式匹配 2. 匹配中文 &#xff08;1&#xff09;确定中文的 Unicode 范围 &am…

PostgreSQL-HA 高可用集群在 Rainbond 上的部署方案

PostgreSQL 是一种流行的开源关系型数据库管理系统。它提供了标准的SQL语言接口用于操作数据库。 repmgr 是一个用于 PostgreSQL 数据库复制管理的开源工具。它提供了自动化的复制管理&#xff0c;包括&#xff1a; 故障检测和自动故障切换&#xff1a;repmgr 可以检测到主服…

二叉搜索树中的插入操作

1题目 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和要插入树中的值 value &#xff0c;将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 &#xff0c;新值和原始二叉搜索树中的任意节点值都不同。 注意&#xff0c;可能存在多种有效的插入…

YOLOv3论文解读/总结

本章论文&#xff1a; YOLOv3论文&#xff08;YOLOv3: An Incremental Improvement&#xff09;&#xff08;原文&#xff0b;解读/总结&#xff0b;翻译&#xff09; 系列论文&#xff1a; YOLOv1论文解读/总结_yolo论文原文_耿鬼喝椰汁的博客-CSDN博客 YOLOv2论文解读/总…

try(){}用法try-with-resources、try-catch-finally

属于Java7的新特性。 经常会用try-catch来捕获有可能抛出异常的代码。如果其中还涉及到资源的使用的话&#xff0c;最后在finally块中显示的释放掉有可能被占用的资源。 但是如果资源类已经实现了AutoCloseable这个接口的话&#xff0c;可以在try()括号中可以写操作资源的语句(…

cocos 项目实践总结

文章目录 组件文档的理解&#xff1a;开发过程顺序问题&#xff1a;1、获取锚点坐标2、事件监听3、批量生成选词按钮4、Button可以自定义边界、边界颜色、及弧度一些问题记录1、button点击过后重新恢复页面渲染&#xff0c;button的状态偶发还是点击态而非正常态2、偶发事件绑定…

【自动驾驶基础入门】SLAM应该怎么学习?

1、SLAM是什么&#xff1f; SLAM是Simultaneous Localization and Mapping的缩写&#xff0c;即同时定位与地图构建。也称为自主机器人导航或者无人车等领域的基本任务之一。 简单来说&#xff0c;SLAM是指在未知环境中&#xff0c;通过移动机器人并利用其搭载的各种传感器数据…

Yolov5训练日记~如何用Yolov5训练识别自己想要的模型~

目录 一.数据集准备 二.标签设置 三.模型训练 四.模型测试 最近尝试了Yolov5训练识别人体&#xff0c;用的是自己尝试做的训练集。见识到Yolo的强大后&#xff0c;决定分享给大家。 一.数据集准备 数据集是从百度图片上下载的&#xff0c;我当然不可能一个一个下载&#xff…

还没用过这12款建筑设计软件?你OUT了

每个建筑设计软件都针对不同的需求。选择最好的一个取决于许多因素&#xff0c;例如成本、与其他程序的兼容性以及您愿意花在绘图过程上的时间。它还取决于您在设计过程中所处的位置——我们可能都开始在纸上画草图&#xff0c;然后转向建筑软件。我们甚至需要图形设计软件来说…

Haoop集群的搭建(小白教学)

搭建hadoop集群我们必须拥有自己的虚拟机&#xff0c;下列我会给大家奉上超详细的集群搭建以及我在搭建的时候碰到的问题以及对应解决办法&#xff0c;正所谓自己走过的错路是曲折的&#xff0c;也是防止大家做弯路&#xff0c;不仅浪费时间还心态爆炸&#xff0c;下面带走入ha…