[高阶数据结构七]跳表的深度剖析

news2024/12/13 5:03:35

1.前言

跳表是一种查找结构,它有着与红黑树、AVL树和哈希表相同的作用,那么已经学习了红黑树和哈希表这种效率高的数据结构,那么为什么还需要学习跳表呢?--请听我娓娓道来。

本章重点:

本章着重讲解跳表的概念,跳表的实现原理,跳表的模拟实现,以及有了红黑树和哈希表之后,为什么还要学习跳表这种数据结构。

2.跳表的概念

跳表是在有序链表的基础上发展而来的。且看下面发展历程。

有序链表的查找和搜索的效率都是O(N)的。

但是0(N)明显时间复杂度较慢,于是乎,就有大佬相出了,那么能不能相邻的节点上升一层,增加一个指针,让指针指向下下个节点?这样当我们查找时,原来需要比较全部的节点,现在由于上升了一层,那么就变成了一半了。

如下图:

依次重复上述步骤,再上升

发现只有两个节点了,那么就不需要上升了。

skiplist 正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方
式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常 类似
二分查找 ,使得查找的时间复杂度就降低到了O (LogN)。
但是这样也会有一个问题, 那么就是在插入和删除时,就会有很大的问题了,由于在升层的时候是严格的按照每相邻两个节点上升一层的,那么上一层的节点的个数和下一层节点的个数比例严格的就是2:1,那么当你插入和删除某一个节点时,也需要改变其他节点的层数,这样时间效率又退化成了O(N),这样是让人无法接受的。
于是这个时候又有大佬相出了一个好的方法: 不再严格要求对应比例关系,而是 插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数, 这样就好处理多了。

 设计一个东西,最终都是要考虑它的时间复杂度的,那么按照上述的设计思路,他的时间复杂度如何呢?他的效率如何呢?

SKipList效率分析

上面我们说到, skiplist 插入一个节点时随机出一个层数,听起来怎么这么随意,如何保证搜索时
的效率呢?
这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数 maxLevel 的限
制,其次会设置一个多增加一层的概率 p 。那么计算这个随机层数的伪代码如下图:

其中p代表效率,maxlevel代表的是层数

根据前面的随机层函数,我们其实可以很清晰的观察出,产生越高的节点层数,概率越低。

具体分析过程如下:

  1. 节点层数至少为1。而大于1的节点层数,满足一个概率分布。
  2. 节点层数恰好等于1的概率为1-p。
  3. 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
  4. 节点层数大于等于3的概率为p^ 2,而节点层数恰好等于3的概率为p^2*(1-p)。
  5. 节点层数大于等于4的概率为p^ 3,而节点层数恰好等于4的概率为p^3*(1-p)。

因此通过综合分析,跳表的平均时间复杂度是0(LogN)的。这个过程比较复杂,需要有一定的数学功底,有兴趣想知道怎么来的老铁,可以阅读以下文章:

ftp://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf

3.跳表的模拟实现

通过了解了跳表的原理,那么就能够很清楚的知道跳表是如何来的了。

那么对于跳表中的层数应该如何把控呢?---用一个vector,只不过我们平常用的vector是水平来看的,现在把vector竖过来看,发现那就是层数了,每一层里面存放的是指向下一个位置的节点。

基本框架:

struct SkiplistNode
{
    int _val;
    vector<SkiplistNode*> _nextV;
    SkiplistNode(int val, int level)
        :_val(val)
        , _nextV(level, nullptr)
    {}
};
class Skiplist {
    typedef SkiplistNode Node;
public:
    Skiplist() {
        //最开始第一个数插入的时候先给1层
        srand((unsigned int)time(0));//生成随机数的种子
        _head = new Node(-1, 1);
    }

private:
    Node* _head;//头结点
    size_t _maxlevel = 32;//最大层数
    double _p = 0.25;//增加一层必须满足的概率
};

这里把最大层数和概率都设置了,如果后续想改成别的,直接修改即可。

跳表需要实现的函数--查找,删除,增加。

搜索函数,即查找:

bool search(int target) {
        Node* cur = _head;
        int level =(int) cur->_nextV.size() - 1;
        while (level >= 0)
        {
            if (cur->_nextV[level] && cur->_nextV[level]->_val < target)
                //向右走
                cur = cur->_nextV[level];
            else if (cur->_nextV[level] == nullptr ||
                cur->_nextV[level]->_val > target)
                //向下走
                level--;
            else return true;//找到了
        }
        return false;//没有这个值
    }

搜索的规则,_val比它(目标值)小,向右走;_val比它(目标值)大,向下走。这里一定要用的是cur->next来进行比较,否则的话,一旦走到了空又要重新走,这就严重影响效率 了。

增加函数:

vector<SkiplistNode*> FindPrev(int num)
    {
        //当cur->nextv[level]->val>num时,
        //那么此时prev[level]=cur->next[level]
        Node* cur = _head;
        int level = (int)cur->_nextV.size() - 1;
        //找到的前节点最大的层数就是level+1,前一个位置的指针放入prevV
        vector<Node*> prevV(level + 1, nullptr);
        while (level >= 0)
        {
            if (cur->_nextV[level] && cur->_nextV[level]->_val < num)
                //向右走
                cur = cur->_nextV[level];
            else if (cur->_nextV[level] == nullptr ||
                cur->_nextV[level]->_val >= num)
                //向下走
            {
                prevV[level] = cur;//记录该层的前一个节点
                level--;
            }
        }
        return prevV;
    }
    void add(int num) {
        //在插入之前应该把这个位置的每前一层的节点都记录一下,方便后续连接
        vector<Node*> prevV = FindPrev(num);
        int level = RandLevel();
        Node* newnode = new Node(num, level);

        //如果插入的值是大于头结点的层数,那么头结点扩容到对应的层数
        if (level > _head->_nextV.size())
        {
            _head->_nextV.resize(level, nullptr);

            //由于之前prevV对应的是没变化之前的头结点的层数,所以这里
            //也需要改变一下
            prevV.resize(level, _head);
        }

        //开始每一层的连接
        for (int i = 0; i < level; i++)
        {
            newnode->_nextV[i] = prevV[i]->_nextV[i];
            prevV[i]->_nextV[i] = newnode;
        }
    }

解释:在插入之前,一定要找到插入这个值所在位置的所有前一层的节点,后续才方便插入。

删除函数:

 bool erase(int num) {
        //在删除之前也需要找到删除位置的值的每一层的前一个结点
        vector<Node*> prevV = FindPrev(num);
        //有可能num不在表中,那么就删除失败
        if (prevV[0]->_nextV[0] == nullptr ||
            prevV[0]->_nextV[0]->_val != num)
        {
            return false;
        }
        //否则就进行删除
        else
        {
            Node* del = prevV[0]->_nextV[0];
            for (int i = 0; i < del->_nextV.size(); i++)
            {
                prevV[i]->_nextV[i] = del->_nextV[i];
            }
            delete del;
            //如果删掉了最高层的话,那么头结点的层数也要下降
            int level = (int)_head->_nextV.size() - 1;
            while (level >= 0)
            {
                if (_head->_nextV[level] == nullptr)
                    level--;
                else break;
            }
            _head->_nextV.resize(level + 1);
            return true;
        }
    }

删除之前也同理,需要找到要删除节点的所有的前一层的节点值是谁,然后把删除值的前一个节点和后一个节点进行链接上即可。

完整代码如下:

SkipList/SkipList · 青酒余成/初识数据结构 - 码云 - 开源中国 (gitee.com)

4.跳表与红黑树、哈希表的对比

首先与红黑树进行对比:

从时间效率上来看:两种都是差不多的。

从空间效率上来看:红黑树要维护三叉链和节点的颜色,相比与跳表有点浪费空间;

从实现角度上来看:跳表的增删查改代码明显比红黑树的增删查改代码要简单的多。

其次与哈希表进行对比:

严格意义上来说,跳表在哈希表面前就有点cuo了。

但是跳表还是有一定的优势的:1.跳表是有序的;2.跳表的空间消耗低,哈希表要存放指针和表空间;3.哈希表在极端情况下,性能会严重退化,可能需要把红黑树挂接到表空间里面。

5.总结

跳表就讲解到这里啦,后续有疑问的小伙伴欢迎后台TT我。

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

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

相关文章

基于MATLAB的信号处理工具:信号分析器

信号&#xff08;或时间序列&#xff09;是与特定时间相关的一系列数字或测量值&#xff0c;不同的行业和学科将这一与时间相关的数字序列称为信号或时间序列。生物医学或电气工程师会将其称为信号&#xff0c;而统计学家或金融定量分析师会使用时间序列这一术语。例如&#xf…

Linux Shell 脚本:一键在 Ubuntu 系统中打开和关闭网络代理

文章目录 shell脚本使用说明验证设置 shell脚本 以下是一个简单的 Shell 脚本&#xff0c;用于在 Ubuntu 系统中打开和关闭网络代理开关 #!/bin/bash# 检查传入的参数 if [ "$#" -ne 1 ]; thenecho "Usage: $0 <1|0>"echo "1: Enable proxy (…

Android ConstraintLayout 约束布局的使用手册

目录 前言 一、ConstraintLayout基本介绍 二、ConstraintLayout使用步骤 1、引入库 2、基本使用&#xff0c;实现按钮居中。相对于父布局的约束。 3、A Button 居中展示&#xff0c;B Button展示在A Button正下方&#xff08;距离A 46dp&#xff09;。相对于兄弟控件的约束…

【AI工具】强大的AI编辑器Cursor详细使用教程

目录 一、下载安装与注册 二、内置模型与配置 三、常用快捷键 四、项目开发与问答 五、注意事项与技巧 参考资料 近日&#xff0c;由四名麻省理工学院&#xff08;MIT&#xff09;本科生共同创立的Anysphere公司宣布&#xff0c;其开发的AI代码编辑器Cursor在成立短短两年…

Linux-GPIO应用编程

本章介绍应用层如何控制 GPIO&#xff0c;譬如控制 GPIO 输出高电平、或输出低电平。 只要是用到GPIO的外设&#xff0c;都有可能用得到这些操作方法。 照理说&#xff0c;GPIO的操作应该是由驱动层去做的&#xff0c;使用寄存器操作或者GPIO子系统之类的框架。 但是&#xff0…

前端开发 之 15个页面加载特效下【附完整源码】

文章目录 十二&#xff1a;铜钱3D圆环加载特效1.效果展示2.HTML完整代码 十三&#xff1a;扇形百分比加载特效1.效果展示2.HTML完整代码 十四&#xff1a;四色圆环显现加载特效1.效果展示2.HTML完整代码 十五&#xff1a;跷跷板加载特效1.效果展示2.HTML完整代码 十二&#xff…

STM32 DMA直接存储器存取原理及DMA转运模板代码

DMA简介&#xff1a; 存储器映像&#xff1a; 注意&#xff1a;FLASH是只读的&#xff0c;DMA不能写入&#xff0c;但是可以读取写到其他存储器里 变量是存在运行内存SRAM里的&#xff0c;常量&#xff08;const&#xff09;是放在程序存储器FLASH里的 DMA框图&#xff1a; …

transformers实现一个检索机器人(一)

简介 检索机器人是一种能够自动搜索和提供信息的系统&#xff0c;它可以帮助我们快速找到需要的信息。这类机器人通常使用自然语言处理&#xff08;NLP&#xff09;技术来理解用户的查询&#xff0c;并利用搜索引擎或数据库来获取相关信息。 那么我们要通过transforme实现什么…

开源ISP介绍(2)————嵌入式Vitis搭建

Vivado搭建参考前一节Vivado基于IP核的视频处理框架搭建&#xff1a; 开源ISP介绍&#xff08;1&#xff09;——开源ISP的Vivado框架搭建-CSDN博客 导出Hardware 在vivado中导出Hardware文件&#xff0c;成功综合—实现—生成比特流后导出硬件.xsa文件。&#xff08;注意导…

力扣-图论-2【算法学习day.52】

前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非…

【PlantUML系列】序列图(二)

目录 一、参与者 二、消息交互顺序 三、其他技巧 3.1 改变参与者的顺序 3.2 使用 as 重命名参与者 3.3 注释 3.4 页眉和页脚 一、参与者 使用 participant、actor、boundary、control、entity 和 database 等关键字来定义不同类型的参与者。例如&#xff1a; Actor&…

如何利用内链策略提升网站的整体权重?

内链是谷歌SEO中常常被低估的部分&#xff0c;实际上&#xff0c;合理的内链策略不仅能帮助提升页面间的关联性&#xff0c;还可以增强网站的整体权重。通过正确的内链布局&#xff0c;用户可以更流畅地浏览你的网站&#xff0c;谷歌爬虫也能更快地抓取到更多页面&#xff0c;有…

zotero中pdf-translate插件和其他插件的安装

1.工具–》插件 2.找插件 3.点击之后看到一堆插件 4.找到需要的&#xff0c;例如pdf-translate 5.点击进入&#xff0c;需要看一下md文档了解下&#xff0c;其实最重要的就是找到特有的(.xpi file) 6.点击刚刚的蓝色链接 7.下载并保存xpi文件 8.回到zotero&#xff0c;安装并使…

Datax遇到的坑

公司数据中台产品&#xff0c;要使用airflow调datax任务实现离线作业的同步。 一、python版本问题 执行python ..datax.py .json时 报错 在运行 Python 脚本时&#xff0c;代码中使用了 Python 2 的 print语法&#xff0c;当前的环境是 Python 3。在 Python 3 中&#xff0…

容易被遗忘的测试用例

网络服务器启动了吗&#xff1f;应用程序服务器启动了吗&#xff1f;数据库上线了吗&#xff1f;测试数据是否预先加载到数据库中&#xff1f;每当我们准备开始测试应用程序时&#xff0c;一切都应该已经准备妥当。 然而&#xff0c;当测试开始后&#xff0c;我们可能会漏掉一些…

机器学习与深度学习-2-Softmax回归从零开始实现

机器学习与深度学习-2-Softmax回归从零开始实现 1 前言 内容来源于沐神的《动手学习深度学习》课程&#xff0c;本篇博客对于Softmax回归从零开始实现进行重述&#xff0c;依旧是根据Python编程的PEP8规范&#xff0c;将沐神的template代码进行简单的修改。近期有点懒散哈哈哈…

文本生成类(机器翻译)系统评估

在机器翻译任务中常用评价指标&#xff1a;BLEU、ROGUE、METEOR、PPL。 这些指标的缺点&#xff1a;只能反应模型输出是否类似于测试文本。 BLUE&#xff08;Bilingual Evaluation Understudy&#xff09;&#xff1a;是用于评估模型生成的句子(candidate)和实际句子(referen…

保护数字资产:iOS 加固在当前安全环境中的重要性

随着互联网和手机的发展&#xff0c;APP在我们的日常生活中已经变得无处不在&#xff0c;各大平台的应用程序成为了黑客攻击的主要目标。尤其在 2024 年&#xff0c;随着数据泄露和隐私侵犯事件的频发&#xff0c;手机应用的安全问题再次成为公众关注的焦点。近期&#xff0c;多…

基于HTML和CSS的校园网页设计与实现

摘要 随着计算机、互联网与通信技术的进步&#xff0c;Internet在人们的学习、工作和生活中的地位也变得越来越高&#xff0c;校园网站已经成为学校与学生&#xff0c;学生与学生之间交流沟通的重要平台&#xff0c;对同学了解学校内发生的各种事情起到了重要的作用。学校网站…

Secured Finance 推出 TVL 激励计划以及基于 FIL 的稳定币

Secured Finance 是新一代 DeFi 2.0 协议&#xff0c;其正在推出基于 FIL 的稳定币、固定收益市场以及具有吸引力的 TVL 激励计划&#xff0c;以助力 Filecoin 构建更强大的去中心化金融生态体系&#xff0c;并为 2025 年初 Secured Finance 协议代币的推出铺平道路。Secure Fi…