高阶数据结构跳表

news2025/2/25 18:40:09

"想象为翼,起飞~"


跳表简介?

        skiplist本质上是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是 一样的,可以作为key或者key/value的查找模型。

跳表由来        

        skiplist是由美国计算机科学家William Pugh于1989年发明,skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。我们知道在对一个有序链表进行查找,它的时间复杂度为O(N)。

William Pugh开始了他的优化思路:

● 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图所示:

 这样新增的一层指针通过连接可以形成新的链表,它包含了整个链表节点的一半,由此需要在这一层进行比较、筛除的个数也就降低了一半。

        以此类推,继续增加一层指针,新链表的节点数下降,查找的效率自然而然也就提高了。按照上述每增加一层,节点数就少一半,其查找的过程类似于二分查找,使得查找的时间复杂度可以降低到O(LogN)。

        当然上述查找的前提是一个有序的链表。无论你是对其中的链表新增节点,还是删除节点,都可能打乱原有维持的指针连接,从而导致跳表失效。。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也 包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。

● 随机层数: 为了避免这种情况,skiplist的设计不再严格要求对应比例关系,而是,插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数。

 

skiplist如何保证效率?

        那么skiplist在引入随机层数后,如何保证其查找效率呢?首先,这个随机层数会有一个限制,这里把它叫做maxlevel,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:

        我们最终可以得到这样一个数学式,用于计算一个节点的平均层数:

有了这个公式,我们可以很容易计算出:

当 p =  1/2 时: 每个节点所包含的平均指针数目为2。

当 p =  1/4 时: 每个节点所包含的平均指针数目为1.33。                 

        至于跳表的平均时间复杂度为O(logN),这个推导的过程较为复杂,愚钝的我也就不在此摆弄文墨,下面的两篇中英文章可以给你提供你要的答案:
铁蕾大佬的博客:http://zhangtielei.com/posts/blog-redis-skiplist.html.

William_Pugh大佬的论文: http://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf.


跳表实现:

        leetcode上有一道实现跳表的题,你可以在这上面完成跳表的测试: https://leetcode.cn/problems/design-skiplist/
        当然讲了这么多,还是没具体说说到底跳表是如何进行查找的,所以我们要实现的第一个函数接口就是跳表元素查找:

skipList初始化:

// 跳表不仅仅是要存储数据 _data
// 还需要有next指针,当然这些next指针也不止一个
// 这取决于 当前节点的层数
typedef struct SkiplistNode
{
    int _val;                       // 节点值
    vector<SkiplistNode*> _nextV;   // 节点连接的其他表项

    SkiplistNode(int val, int level)
        :_val(val), _nextV(level, nullptr)
    {}
}Node;

class Skiplist {
public:
    Skiplist() {
        // 初始化 _headList
        // 默认给一层
        _head = new SkiplistNode(-1, 1);
    }
private:
    Node* _head;                // 头节点
    double _prate = 0.25;       // 新增层概率
    int _MaxLevel;              // 最大层数
};

 

Search:

    bool search(int target) 
    {
        // 1.从头节点查
        Node* cur = _head;
        // 记录的层数
        // 0~n-1的下标
        int level = _head->_nextV.size() - 1;
        while (level >= 0)
        {
            // target 大于 下一个节点的val cur向右移动
            if (cur->_nextV[level] && target > cur->_nextV[level]->_val)
            {
                cur = cur->_nextV[level];
            }                           // 因为支持数据冗余 所以如果出现一样的就把新节点插在它后面即可
            else if(cur->_nextV[level]==nullptr || target < cur->_nextV[level]->_val)
            {
                // target 小于 下一个节点的val --level || next节点为空
                --level;
            }
            else
            {
                // 找到了
                return true;
            }
        }
        return false;
    }

 

Add:

    vector<Node*> FindPath(int num)
    {
        // 从头节点查起
        Node* cur = _head;
        int level = _head->_nextV.size()-1;

        // 开和level一样的大小
        vector<Node*> prevV(level+1,_head);
            
        while (level >= 0)
        {
            if (cur->_nextV[level] && num > cur->_nextV[level]->_val)
            {
                // 只管移动
                cur = cur->_nextV[level];
            } // 因为支持数据冗余 所以如果出现一样的就把新节点插在它后面即可
            else if (cur->_nextV[level] == nullptr || 
                num <= cur->_nextV[level]->_val)
            {
                // 记录该层num的前一个节点
                prevV[level] = cur;
                // 向下更新
                --level;
            }
        }
        return prevV;
    }


    int RandomLevel()
    {
        int level = 1;
        while (rand() <= _prate * RAND_MAX && level < _MaxLevel)
        {
            ++level;
        }
        return level;
    }

    void add(int num)
    {
        // 前驱节点
        vector<Node*> prevV = FindPath(num);

        // 创建节点
        int n = RandomLevel();
        Node* newnode = new Node(num, n);

        // 可能创建节点层数 > _head
        if (n > _head->_nextV.size())
        {
            // 进行扩容
            _head->_nextV.resize(n,nullptr);
            // prevV也许跟着扩容
            // 这里的新增前驱节点为什么初始化为 _head?
            // 新增节点一定是连接在 prevV里的节点之后的
            prevV.resize(n, _head);
        }

        // 前后连接节点
        for (int i = 0;i < n;++i)
        {
            // 可以理解为:newnode->next = prev->next->next
            newnode->_nextV[i] = prevV[i]->_nextV[i];
            prevV[i]->_nextV[i] = newnode; // 连接回来
        }
    }

        这里的randomLevel()是以一种巧妙的方式完成的:

         通过p可以控制最终值产生范围的概率。

        不过,C++有专门的随机数生成的库,比这个rand功能更加强大,所以我们可以将那个RandomLevel()改成这样:

    int RandomLevel()
    {
        // 随机数种子
        static static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());
        // 生成随机数范围
        static std::uniform_real_distribution<double> distribution(0.0,1.0);
        size_t level = 1;
        while (distribution(generator) <= _prate && level < _MaxLevel)
        {
            ++level;
        }
        return level;
    }

Erase:

    bool erase(int num)
    {
        vector<Node*> prevV = FindPath(num);
        
        // 这里可能找不到
        if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num) return false;

        // 我们根据prevV的最底层节点 就可以找到del节点
        Node* del = prevV[0]->_nextV[0];
            
        // 根据该节点的层数 更新prevV 和 nextV
        // 进行连接
        for (int i = 0;i < del->_nextV.size();++i)
        {
            prevV[i]->_nextV[i] = del->_nextV[i];
        }
           
        // 删除节点
        // 这里记录level
        int level = del->_nextV.size();
        delete del;

        // 如果删除的节点是 最高层呢? 并且是唯一呢?
        // 这种优化可以不做 但你也可以做
        // 就是重新定义_head的高度
        // 向下遍历 只要遇到不为空的最高 就break
        int i = _head->_nextV.size() - 1; 
        while (i >= 0)
        {
            if (_head->_nextV[i] == nullptr)
            {
                --i;
            }
            else
            {
                break;
            }
        }
        _head->_nextV.resize(i + 1);
        return true;
    }

 

        最后我们可以通过leetcode提供的测试用例,来测试测试咱们写的跳表。 

跳表vs平衡搜索树和哈希表的对比

        最后一个话题:

        skiplist相比平衡搜索树(AVL树和红黑树)对比都可以做到遍历数据有序,时间复杂度也差不多。不过skiplist与平衡搜索树的最大优势在于:

● skiplist实现简单,容易控制。平衡树增删查改遍历都更复杂.

● skiplist的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗。可是skiplist可以通过p来调整每个节点的指针个数,那是个可接受的数量。

        skiplist相比哈希表而言,在查找上就没有那么大的优势了。

● 哈希表平均时间复杂度是O(1),比skiplist快。

        相反skiplist在这些方面胜过哈希表:
● 遍历数据有序

● skiplist空间消耗略小一点,哈希表存在链接指针和表空间消耗

● 哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力
 


本篇到此结束,感谢你的阅读。

祝你好运,向阳而生~

 

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

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

相关文章

【SCSS变量】$ | | var | @for | @include | @function | @each 等常用方法使用

SCSS优点&#xff1a;编写清晰、无冗余、语义化的CSS&#xff0c;减少不必要的重复工作 1、变量声明&#xff08;$&#xff09;和使用2、使用 & 代替父元素3、在HTML中使用 :style{--name: 动态值}自定义属性&#xff0c;在SCSS中用var(--name)函数绑定动态变量值&#xff…

一文搞懂数据中心ip和住宅ip

我们在购买到一个代理后&#xff0c;通过检测网址会看到检测类型会出现有hosting或者具体的某个运营商&#xff0c;代表这是两种不同的代理&#xff1a;数据中心代理以及isp住宅代理。 1、什么是isp住宅代理 ISP 全称为 Internet Service Provider&#xff08;互联网服务提供…

C语言之feof与fgetc应用实例(八十一)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

【FreeRTOS】【STM32】中断详细介绍

文章目录 一、三种优先级的概念辨析1. 先理清楚两个概念&#xff1a;CPU 和 MPU2. Cortex-M3 内核与 STM32F1XX 控制器有什么关系3. 优先级的概念辨析① Cortex-M3 内核和 STM32F1XX 的中断优先级② FreeRTOS 的任务的优先级 二、 Cortex-M3 内核的中断优先级1. 中断编号2. 优先…

XD转Sketch完美实现,这款神器助你轻松转换设计文件

Adobe XD和Sketch作为主流设计软件,却存在文件格式不兼容的痛点。设计师经常需要在两款软件之间互相转换设计稿件,头疼不已。那么有没有一种简单快捷的方法实现XD到Sketch的格式转换呢?答案是有的!今天就来看看这个神奇的在线互转工具。 XD转Sketch&#xff0c;在线免费转 这…

栈和队列在数据结构中的应用

文章目录 理解栈和队列的概念及其特点栈的应用和操作队列的应用和操作结论 &#x1f389;欢迎来到数据结构学习专栏~探索栈和队列在数据结构中的应用 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;IT陈寒的博客&#x1f388;该系列文章专栏&#xff1a;…

基于YOLOV8模型和Kitti数据集的人工智能驾驶目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOV8模型和Kitti数据集的人工智能驾驶目标检测系统可用于日常生活中检测与定位车辆、汽车等目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用…

Linux之基础IO文件系统讲解

基础IO文件系统讲解 回顾C语言读写文件读文件操作写文件操作输出信息到显示器的方法stdin & stdout & stderr总结 系统文件IOIO接口介绍文件描述符fd文件描述符的分配规则C标准库文件操作函数简易模拟实现重定向dup2 系统调用在minishell中添加重定向功能 FILE文件系统…

【项目管理】PMP考试总结

2023年08月19日考完了PMP&#xff0c;总结一下子 1、花费费用 先算下花费及购置的材料&#xff1a; 5月14日&#xff1a;书-拼多多 PMBOK指南第七版&#xff0c;19.8 5月28日&#xff1a;书-淘宝&#xff1a; 敏捷实践指南&#xff0c;30.49&#xff0c; PMBOK指南第6版&…

将一个树形结构的数据平铺成一个一维数组(vue3)

一、需求描述 由于自带组件库没有具体完善,无法实现像element-ui这种可以多选选择任意一级的选项,也就是说,选择父级的时候不会联动选择子级的全部 例如: 所以,才会出现【二、案例场景】类似的场景,可以用来多选 ,并可以实现单选父级而不关联子级,选择了将树状数据进…

浅谈 Linux 下 vim 的使用

Vim 是从 vi 发展出来的一个文本编辑器&#xff0c;其代码补全、编译及错误跳转等方便编程的功能特别丰富&#xff0c;在程序员中被广泛使用。 Vi 是老式的字处理器&#xff0c;功能虽然已经很齐全了&#xff0c;但还有可以进步的地方。Vim 可以说是程序开发者的一项很好用的工…

AutoSAR配置与实践(基础篇)3.6 BSW的WatchDog功能

3.6 BSW的WatchDog功能 一、WatchDog功能介绍1.1 WatchDog 模块组成1.2 内外部看门狗区别和原理1.3 常见看门狗校验方式一、WatchDog功能介绍 1.1 WatchDog 模块组成 WatchDog 即看门狗功能。这个看门狗不是真正看家的狗,而是软件的一个模块,但是因为功能类似故以此起名。主…

LeetCodeHot100python版本:单调栈,栈,队列,堆

单调栈 739. 每日温度 42. 接雨水 双指针 单调栈(横向求解) ​​​​​​84. 柱状图中最大的矩形 栈和队列 队列:先入先出 栈:先入后出 两个栈 模拟 队列 一个队列 可以模拟 栈 20. 有效的括号 ​​​​​​155. 最小栈 394. 字符串解码 堆 215. 数组中的第K个最大元素 3…

嵌入式Linux开发实操(十二):PWM接口开发

# 前言 使用pwm实现LED点灯,可以说是嵌入式系统的一个基本案例。那么嵌入式linux系统下又如何实现pwm点led灯呢? # PWM在嵌入式linux下的操作指令 实际使用效果如下,可以通过shell指令将开发板对应的LED灯点亮。 点亮3个LED,则分别使用pwm1、pwm2和pwm3。 # PWM引脚的硬…

拆解1000篇爆文!揭秘种草爆文四大万能公式

2023年上半场已收官&#xff0c;小红书用户青睐什么内容&#xff1f; 千瓜调研2023上半年的1000篇商业笔记爆文&#xff0c;从笔记类型和内容特征两大层面总结以下四大内容种草爆文公式&#xff0c;快来围观&#xff01; 突破同质化 爆款内容创新风向 笔记类型角度 千瓜调…

2022年度瞪羚培育企业名单公布,科东软件上榜

8月23日&#xff0c;广州市黄埔区、广州开发区2022年度瞪羚企业和瞪羚培育企业名单公布。科东软件凭借国产化技术创新优势、成熟的数字化转型方案和强劲的经营成长韧性&#xff0c;入选广州开发区2022年度瞪羚培育企业。 瞪羚培育企业是指未来在科技创新或商业模式创新方面有…

Navicat安装教程

众所周知&#xff0c; Navicat是一款轻量级的用于MySQL连接和管理的工具&#xff0c;非常好用&#xff0c;使用起来方便快捷&#xff0c;简洁。下面我会简单的讲一下其安装以及使用的方法。并且会附带相关的永久安装教程。 简介 一般我们在开发过程中是离不开数据库的&#xf…

win11 设置小任务栏

设置后效果 以下两种工具均可 1、StartAllBack 2、Start11

安全防护产品对接流程讲解

服务器被攻击了&#xff0c;怎么对接高防产品呢&#xff0c;需要提供什么&#xff1f; 1、配置转发规则&#xff1a;提供域名、IP、端口&#xff0c;由专业技术人员为您配置转发协议/转发端口/源站IP等转发规则&#xff0c;平台会分配该线路独享高防IP。 2、修改DNS解析&…

2023年高教社杯数学建模思路 - 复盘:校园消费行为分析

文章目录 0 赛题思路1 赛题背景2 分析目标3 数据说明4 数据预处理5 数据分析5.1 食堂就餐行为分析5.2 学生消费行为分析 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 赛题背景 校园一卡通是集…