LeetCode 1206. 设计跳表

news2025/1/19 3:00:37

LeetCode 1206. 设计跳表

难度: h a r d \color{red}{hard} hard


题目描述

不使用任何库函数,设计一个 跳表

跳表 是在 O ( l o g ( n ) ) O(log(n)) O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度相较下更短,其设计思想与链表相似。

例如,一个跳表包含 [ 30 , 40 , 50 , 60 , 70 , 90 ] [30, 40, 50, 60, 70, 90] [30,40,50,60,70,90] ,然后增加 80 80 80 45 45 45 到跳表中,以下图的方式操作:


Artyom Kalinin [CC BY-SA 3.0], via Wikimedia Commons

跳表中有很多层,每一层是一个短的链表。在第一层的作用下,增加、删除和搜索操作的时间复杂度不超过 O ( n ) O(n) O(n)。跳表的每一个操作的平均时间复杂度是 O ( l o g ( n ) ) O(log(n)) O(log(n)),空间复杂度是 O ( n ) O(n) O(n)

了解更多 : https://en.wikipedia.org/wiki/Skip_list

在本题中,你的设计应该要包含这些函数:

  • b o o l s e a r c h ( i n t t a r g e t ) bool search(int target) boolsearch(inttarget) : 返回target是否存在于跳表中。
  • v o i d a d d ( i n t n u m ) void add(int num) voidadd(intnum): 插入一个元素到跳表。
  • b o o l e r a s e ( i n t n u m ) bool erase(int num) boolerase(intnum): 在跳表中删除一个值,如果 n u m num num 不存在,直接返回false. 如果存在多个 n u m num num ,删除其中任意一个即可。

注意,跳表中可能存在多个相同的值,你的代码需要处理这种情况。

示例 1:

输入
["Skiplist", "add", "add", "add", "search", "add", "search", "erase", "erase", "search"]
[[], [1], [2], [3], [0], [4], [1], [0], [1], [1]]
输出
[null, null, null, null, false, null, true, false, true, false]

解释
Skiplist skiplist = new Skiplist();
skiplist.add(1);
skiplist.add(2);
skiplist.add(3);
skiplist.search(0);   // 返回 false
skiplist.add(4);
skiplist.search(1);   // 返回 true
skiplist.erase(0);    // 返回 false,0 不在跳表中
skiplist.erase(1);    // 返回 true
skiplist.search(1);   // 返回 false,1 已被擦除

提示:

  • 0 < = n u m , t a r g e t < = 2 ∗ 1 0 4 0 <= num, target <= 2 * 10^{4} 0<=num,target<=2104
  • 调用 s e a r c h search search, a d d add add, e r a s e erase erase操作次数不大于 5 ∗ 1 0 4 5 * 10^{4} 5104

算法

(数据结构-单链表)

write here...
首先需要定义链表的最大高度 level,这里取一个经验值 level=8Redis 中设置是 32

我们可以看到 head 节点在每一层都会连接一个链表,由上图引出跳表节点的结构:

  • 节点值 val

  • 存储当前节点在每一层的 next 指针,方便我们操作(为了方便理解我们可以把图中每个节点的高度都看成 level,没在图中画出来的就是指向 NULL,而它们的值都是相同的 val

节点结构代码实现:

// 定义跳表节点
struct Node {
    int val; // 节点值
    vector<Node*> next; // 记录节点在每一层的 next,next[i] 表示当前节点第 i 层的 next

    Node(int _val) : val(_val) { // 构造函数
        next.resize(level, NULL); // 初始化 next 数组的大小和层数 level 相同,初始值都指向 NULL
    }
}*head; // 定义头节点 head

搞清楚节点结构之后,下面就好办了,不管是查找、插入、删除都需要先找到目标值或者找到目标值的前一个节点,那么这里我们统一设计一个辅助函数 find():找到小于目标值的最大的节点,由于跳表是多层链表结构,所以要找的不是一层而是每一层。

find() 函数代码实现:

// 辅助函数:找到每一层 i 小于目标值 target 的最大节点 pre[i],最后 pre 中存的就是每一层小于 target 的最大节点
    void find(int target, vector<Node*>& pre) {
        auto p = head; // 从头节点开始遍历每一层
        for (int i = level - 1; i >= 0; i -- ) { // 从上层往下层找
            while (p->next[i] && p->next[i]->val < target) p = p->next[i]; // 如果当前层 i 的 next 不为空,且它的值小于 target,则 p 往后走指向这一层 p 的 next
            pre[i] = p; // 退出 while 时说明找到了第 i 层小于 target 的最大节点就是 p
        }
    }

有了辅助函数之后,如何实现查找、插入、删除操作呢?不管是哪种操作首先调用 find() 得到每一层小于目标值的最大节点数组 pre

1、查找 search():由于第 0 层的数据是最全的,所以只需要在第 0 层查找是否存在即可,代码如下:

// 从跳表中查找 target
bool search(int target) {
    vector<Node*> pre(level);
    find(target, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]

    auto p = pre[0]->next[0]; // 因为最下层【0】的节点是全的,所以只需要判断 target 是否在第 0 层即可,而 pre[0] 正好就是小于 target 的最大节点,如果 pre[0]->next[0] 的值不是 target 说明没有这个元素
    return p && p->val == target;
}

2.、插入 insert():新建要插入的节点,从第 0 层开始插入,往上每层 50% 的插入,50% 的概率不插入,相当于两个点中有一个在上层插入(当然这并不一定),只不过这样较好实现,理论上是一样的。具体插入操作就是单链表插入,代码如下:

// 向跳表中插入元素 num
void add(int num) {
    vector<Node*> pre(level);
    find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]

    auto p = new Node(num); // 创建要插入的新节点
    for (int i = 0; i < level; i ++ ) { // 遍历每一层,从下往上插入新节点
        p->next[i] = pre[i]->next[i]; // 这两步就是单链表的插入
        pre[i]->next[i] = p;
        if (rand() % 2) break; // 每一层有 50% 的概率不插入新节点
    }
}

3、删除 erase():因为是多层结构,所以需要把每一层等于目标值的节点都删除,具体删除操作就是单链表删除,代码如下:

// 从跳表中删除 num
bool erase(int num) {
    vector<Node*> pre(level);
    find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]

    // 先判断 num 是否存在,不存在直接返回 false
    // 第 0 层存储的是全部节点,所以只需要判断 pre[0]->next[0](第 0 层小于 num 的最大节点的在第 0 层的 next) 是不是 num 即可
    auto p = pre[0]->next[0];
    if (!p || p->val != num) return false;

    // 否则删除每一层的 num,如果 pre[i]->next[i] == p 说明第 i 层存在 p
    for (int i = 0; i < level && pre[i]->next[i] == p; i ++ ) {
        pre[i]->next[i] = p->next[i]; // 单链表删除
    }

    delete p; // 删除节点 p,防止内存泄漏

    return true;
}

复杂度分析

  • 时间复杂度:查询、删除、插入的时间复杂度近似 O ( l o g n ) O(logn) O(logn)

C++ 代码

class Skiplist {
public:
    static const int level = 8; // 层数,经验值 8,太大浪费空间,因为每一个节点都要存在每一层的 next,层数越多节点数越多

    // 定义跳表节点
    struct Node {
        int val; // 节点值
        vector<Node*> next; // 记录节点在每一层的 next,next[i] 表示当前节点第 i 层的 next

        Node(int _val) : val(_val) { // 构造函数
            next.resize(level, NULL); // 初始化 next 数组的大小和层数 level 相同,初始值都指向 NULL
        }
    }*head; // 定义头节点 head

    Skiplist() {
        head = new Node(-1); // 初始化一个不存在的节点值 -1
    }

    ~Skiplist() {
        delete head; // 析构函数删除 head
    }

    // 辅助函数:找到每一层 i 小于目标值 target 的最大节点 pre[i],最后 pre 中存的就是每一层小于 target 的最大节点
    void find(int target, vector<Node*>& pre) {
        auto p = head; // 从头节点开始遍历每一层
        for (int i = level - 1; i >= 0; i -- ) { // 从上层往下层找
            while (p->next[i] && p->next[i]->val < target) p = p->next[i]; // 如果当前层 i 的 next 不为空,且它的值小于 target,则 p 往后走指向这一层 p 的 next
            pre[i] = p; // 退出 while 时说明找到了第 i 层小于 target 的最大节点就是 p
        }
    }

    // 从跳表中查找 target
    bool search(int target) {
        vector<Node*> pre(level);
        find(target, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]

        auto p = pre[0]->next[0]; // 因为最下层【0】的节点是全的,所以只需要判断 target 是否在第 0 层即可,而 pre[0] 正好就是小于 target 的最大节点,如果 pre[0]->next[0] 的值不是 target 说明没有这个元素
        return p && p->val == target;
    }

    // 向跳表中插入元素 num
    void add(int num) {
        vector<Node*> pre(level);
        find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]

        auto p = new Node(num); // 创建要插入的新节点
        for (int i = 0; i < level; i ++ ) { // 遍历每一层,从下往上插入新节点
            p->next[i] = pre[i]->next[i]; // 这两步就是单链表的插入
            pre[i]->next[i] = p;
            if (rand() % 2) break; // 每一层有 50% 的概率不插入新节点
        }
    }

    // 从跳表中删除 num
    bool erase(int num) {
        vector<Node*> pre(level);
        find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]

        // 先判断 num 是否存在,不存在直接返回 false
        // 第 0 层存储的是全部节点,所以只需要判断 pre[0]->next[0](第 0 层小于 num 的最大节点的在第 0 层的 next) 是不是 num 即可
        auto p = pre[0]->next[0];
        if (!p || p->val != num) return false;

        // 否则删除每一层的 num,如果 pre[i]->next[i] == p 说明第 i 层存在 p
        for (int i = 0; i < level && pre[i]->next[i] == p; i ++ ) {
            pre[i]->next[i] = p->next[i]; // 单链表删除
        }

        delete p; // 删除节点 p,防止内存泄漏

        return true;
    }
};

/**
 * Your Skiplist object will be instantiated and called as such:
 * Skiplist* obj = new Skiplist();
 * bool param_1 = obj->search(target);
 * obj->add(num);
 * bool param_3 = obj->erase(num);
 */

备注

转载自 LeetCode 1206. 设计跳表

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

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

相关文章

【区块链】【FISCO】WeIdentity

什么是 WeIdentity&#xff1f; 官方的说法&#xff1a;去中心化身份标识解决方案。其实说白了就是互联网上每个人都拥有自己数字身份&#xff0c;并且这个身份是唯一且不可篡改的。 WeIdentity要解决的问题就是用来解决数字身份验证的问题。传统互联网身份验证的方式通常用账…

将ip地址中的每一个字符串按照分隔符提取

1、算法思想 该题采用 c 中的 string 完成比较方便 对于字符串 string str1“hehehe:hahaha:xixixi:lalala” 定义 int pos 0&#xff0c;记录子串的初始位置 在循环语句中重复执行以下操作&#xff1a; &#xff08;1&#xff09;、定义 int ret str1.find(":",…

OpenAI-ChatGPT最新官方接口《聊天交互多轮对话》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(二)(附源码)

目录Chat completions Beta 聊天交互前言Introduction 导言Response format 提示格式Managing tokensCounting tokens for chat API calls 为聊天API调用标记计数Instructing chat models 指导聊天模型Chat vs Completions 聊天与完成FAQ 问与答其它资料下载Chat completions B…

27.Linux网络编程socket变成 tcp 高并发 线程池 udp

好&#xff0c;咱们开始上课了&#xff0c;从今天开始咱们连续讲 8 天的&#xff0c;网络编程这个还是在linux环境下去讲&#xff0c;咱们先看一下咱们这 8 天都讲什么东西&#xff0c;跟大家一块来梳理一下&#xff0c;你先有个大概的印象&#xff0c;这些你也不要记&#xff…

什么是以太坊

以太网是“世界的计算机”&#xff0c;这是以太坊平台的一种常见描述。这是什么意思呢&#xff1f;让我们首先从关注计算机科学的描述开始&#xff0c;然后对以太坊的功能和特性进行更实际的解读&#xff0c;并将其与比特币和其他分布式账本技术&#xff08;简单起见&#xff0…

【学习笔记】unity脚本学习(三)(向量 Vector3)

目录向量复习高中向量基础【数学】向量的四则运算、点积、叉积、正交基叉乘公式叉乘运算定理向量、坐标系点积叉积Vector3 三维向量静态变量变量变量normalized 与 Normalize() 方法静态方法ClampMagnitudeCrossDistanceDotMoveTowards其他变换类似Lerp 在两个点之间进行线性插…

走出至暗时刻,手机“冲高”仍有新故事

随着数十年的发展变迁&#xff0c;智能手机行业已进入平稳发展期&#xff0c;在格局重塑的同时&#xff0c;也引来外界的质疑&#xff1a;出货量下滑&#xff0c;是否意味着行业开始进入至暗时刻&#xff1f; 事实上&#xff0c;这种质疑只看到表层的数据变化&#xff0c;没有…

[Java]Cookie机制

1.Session机制&#xff1a; Session机制https://blog.csdn.net/m0_71229255/article/details/130138826?spm1001.2014.3001.5501 2. 什么是cookie HTTP协议本身是无状态的。什么是无状态呢&#xff0c;即服务器无法判断用户身份。Cookie实际上是一小段的文本信息&#xff0…

优维可观测轴心产品大观:HyperInsight超融合持续可观测解决方案

随着Kubernetes得到越来越广泛的采用&#xff0c;企业软件系统正在向复杂的云原生架构进行革命性转变。应用形式呈现有Web、APP、小程序等多种形式&#xff0c;访问的网络有4G、5G、Wi-Fi等。企业用云也从单一云时代&#xff0c;逐渐来到混合多云时代。在这些庞大复杂的多云环境…

【接口测试】从0不到1的心路历程

我是一名做了三年测试的tester&#xff0c;2020年以功能测试工程师的身份入职北京一家医疗培训公司&#xff0c;入职后为了提高测试效率&#xff0c;接触到接口测试&#xff0c;以下是从零到现在 (还有很大完善的空间&#xff0c;所以不能算是1) 的一些心路历程。 做接口测试的…

李宏毅教程系列——增强学习

目录 0. 强化学习wiki 1. 介绍 2. Exploration vs Exploitation 探索与开发 3. 各类最优化方法 3.1 Brute force猛兽蛮力法&#xff08;暴力搜索&#xff09; 3.2 Value function estimation&#xff08;价值函数估计&#xff09; 3.2.1 Monte Carlo methods 蒙特卡洛方…

linux安装南大通用数据库

linux安装南大通用数据库1、操作系统、数据库2、下载链接3、安装文档4、安装前准备4.1、以root用户创建 gbasedbt 组和用户4.2、创建 GBase 8s 数据库安装目录4.3、上传并解压安装包5、安装5.1、执行安装程序5.2、回车继续 直到接受许可条款5.3、输入安装目录绝对路径5.4、选择…

腾讯音乐笔试0414

介绍一 Triplet Loss的原理&#xff0c; 其中的样本分为哪几类?可以用于哪些场景? Triplet Loss是一种用于训练神经网络的损失函数&#xff0c;主要用于学习映射函数&#xff0c;将样本映射到低维空间中&#xff0c;使得同一类别的样本距离尽可能近&#xff0c;不同类别的样…

开发钉钉和企业微信微应用

钉钉应用开发流程&#xff1a; 1、登录钉钉后台管理 -- 应用管理 -- 工作台 -- 自建应用 2、上传内部应用logo和名字。注意需要添加可访问域名的配置。 3、配置首页可访问地址&#xff1a;打包到线上的路径&#xff08;注意配置正式环境和本地环境&#xff09; 4、在所在公司…

早有尔闻 | 低碳赋能,创新发展

01 2023中国管理科学大会 发布创新奖榜单 海尔位列第一 4月15日&#xff0c;2023中国管理科学大会暨第八届“管理科学奖”颁奖典礼在北京举行。大会发布了第八届中国管理科学学会“管理科学奖”获奖名单&#xff0c;海尔集团“基于用户端低碳升级的智慧能源管理体系建设”项…

[CVE漏洞复现系列]CVE2017_0147:永恒之蓝

Hi~ o(&#xffe3;▽&#xffe3;)ブ 文章目录前言一、永恒之蓝是什么&#xff1f;1.SMB协议介绍。二、准备工作1.Windows7 and kali linux2.テストを開始总结前言 这是新的系列&#xff0c;我能力有限有的漏洞实现不了&#xff0c;我尽力吧 &#x1f968;&#x1f968;&…

Deep Glow(AE辉光特效插件)中文版安装教程

deepglow比AE自带的辉光效果好很多&#xff0c;基于GPU运算&#xff0c;同时控制调节发光效果&#xff0c;有了这款插件&#xff0c;我们就可以非常轻松的模拟出非常真实非常漂亮的物理发光特效&#xff0c;支持各种参数的自定义&#xff0c;喜欢的欢迎下载使用。 安装教程 1…

营收、净利创新高,股价却“跌跌不休”,紫光国微怎么了?

‍数据智能产业创新服务媒体——聚焦数智 改变商业要问当前科技圈处于“风口浪尖”的&#xff0c;除了ChatGPT就应该是半导体了。近日&#xff0c;紫光国微发布2022年年报&#xff0c;实现营收和净利双创新高。作为一家在集成电路设计领域深耕二十余年的企业&#xff0c;紫光国…

快速精简软件,如何让软件缩小到原来的5%大小,从删除文件入手,到修改C++引用库,合规解决存储问题

Hi~大家好&#xff0c;今天制作一个简单的精简软件的教学~ 事先说明下&#xff0c;精简软件并不违反任何规定&#xff0c;尤其是开源软件&#xff0c;这里也仅讨论开源软件的修改&#xff0c;根据几乎所有开源软件的开源规则&#xff0c;精简软件&#xff0c;本质也就是修改软件…

如何通过python实现一个web自动化测试框架?

要实现一个web自动化测试框架&#xff0c;可以使用Python中的Selenium库&#xff0c;它是最流行的Web应用程序测试框架之一。以下是一个基本的PythonSelenium测试框架的示例&#xff1a; 如果你想学习更详细的web自动化测试教程&#xff0c;我这边给你推荐一个详细的视频教程 …