【数据结构与算法】跳表

news2024/10/7 6:49:25

目录

一、什么是跳表

二、跳表的效率验证

三、跳表的实现

1、search

2、add

3、erase

四、跳表与其它搜索结构对比

总结


一、什么是跳表

跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,目前在Redis和LeveIDB中都有用到。

它采用随机技术决定链表中哪些节点应增加向前指针以及在该节点中应增加多少个指针。跳表结构的头节点需有足够的指针域,以满足可能构造最大级数的需要,而尾节点不需要指针域。

采用这种随机技术,跳表中的搜索、插入、删除操作的时间均为O(logn),然而,最坏情况下时间复杂性却变成O(n)。相比之下,在一个有序数组或链表中进行插入/删除操作的时间为O(n),最坏情况下为O(n)。

跳表是由skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A

Probabilistic Alternative to Balanced Trees skiplists (cmu.edu)

跳表的本质还是一个链表,它在一定程度上改善了链表的查询效率,相邻结点的高度可能是不相同的 ,上图是一种比较理想的情况,它的每个节点高度都是不相同且呈现一定的规律

不过这种情况太过理想,实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)。但是这个结构在插入删除数据的时候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。

skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数,这样就好处理多了。这样就相当于将每个节点独立出来,只需要考虑当前节点的层数

我们以跳表c来举例,在c中查询17这个节点

它的具体过程是:

 首先有一个cur指针指向头节点的最高层,从头节点的最高层开始,如果同层的下一个节点的值比它小,就向右走:9比17小,cur指向9

如果同层的下一个节点的值比它大,就向下走:21比17大,cur走到9的第二层

同层的下一个节点的值与它相等,就找到了

如果走到了尾(nullptr)或者走到了-1层,那么就没有找到

 

二、跳表的效率验证

根据上面查询的过程我们可以大概估计它的时间复杂度是O(logN)

同时William Pugh给出了节点高度随机值的范围,如果没有限定范围随机出了10W,难道要建立10W层吗?

 P:增加一层节点的概率

MaxLevel:节点层数的最大值

一般而言:P定为0.25,MaxLevel定为32层

我们可以使用加权平均值计算出跳表的平均高度

第一层的概率 1 - P

第二层的概率(1 - P) * P

第三层的概率(1 - P) * P * P 

第n层的概率 (1 - P) * P ^(n - 1)

 

 当P = 0.5时 H = 2

    P = 0.25时 H = 1.3333 

也就是说跳表节点高度不会太高

三、跳表的实现

1206. 设计跳表 - 力扣(LeetCode)

我们以这道题举例并且验证跳表的正确性

首先是跳表的节点

struct SkiplistNode
{
    int _val;
    std::vector<SkiplistNode*> _nextV;

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

};

跳表实际上是多维链表,节点的高度用一个vector来表示

 跳表的基本框架

class Skiplist {
    typedef SkiplistNode Node;
public:
    Skiplist() {
        _head = new Node(-1, 1);//创建一个头节点
    }
    

private:
    Node* _head;
    size_t _maxLevel = 32;
    double _p = 0.25;
};

1、search

首先search  19这个能够找到的节点

cur首先走到6,6的下一个是空,cur走到6的下一层,6的下一个是25,25大于19,cur再向下走一层,cur的下一个是9,19大于9,cur走到9,9的下一个是25,25大于19,cur走到9的下一层,

9的下一个是12,12小于19,cur走到12,12的下一个是19,找到了

然后是找17这个节点

前面走到12的过程完全一样,现在走到12,12的下一个是19,比17大,cur走到12的下一层,12已经走到了最后一层,它的下一层是-1,结束,没有找到

还有一种情况是找28,走到26,26的下一个是空,结束,没有找到

    bool search(int target) {
        Node* cur = _head;
        int level = _head->_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;
    }

2、add

add与链表类似,首先是找到要插入节点位置的前一个节点

找到插入节点的前一个节点的过程与search类似

使用一个vector来存储search过程中的prev节点

注意:这里只记录当向下移动时的节点,因为我们所谓的前一个节点指的是寻找一组target节点之前的每一层的前一个节点,只有在向下移动时才找到了当前层的target之前的节点

    std::vector<Node*> FindPrevNode(int num)
    {
        Node* cur = _head;
        int level = _head->_nextV.size() - 1;
        std::vector<Node*> prevV(_maxLevel, _head);

        while(level >= 0)
        {
            if(cur->_nextV[level] && cur->_nextV[level]->_val < num)
            {
                cur = cur->_nextV[level];
            }
            else
            {
                prevV[level] = cur;
                level--;
            }
        }

        return prevV;
    }

我们接收返回值,获取插入节点之前的每一层的prev节点

然后对于插入节点确定高度

这里使用C语言风格的随机值

    size_t RandomLevel()
    {
        size_t level = 1;
        while(rand() <= RAND_MAX * _p && level < _maxLevel)
        {
            level++;
        }

        return level;
    }

之后就是普通的链表插入节点的过程

    void add(int num) {
        std::vector<Node*> prevV = FindPrevNode(num);

        int n = RandomLevel();
        Node* newNode = new Node(num, n);
        if(n > _head->_nextV.size())
        {
            _head->_nextV.resize(n, nullptr);
        }

        for(size_t i = 0; i < n; i++)
        {
            newNode->_nextV[i] = prevV[i]->_nextV[i];
            prevV[i]->_nextV[i] = newNode;
        }
    }

3、erase

erase之前也需要我们获取它的前一个节点,我们还是查找它的前一个节点,然后手动判断一下,它的下一个节点的值是否是我们想要删除的节点的值,如果不是则证明该节点不存在

反之存在,就删除它,删除的过程也就不必多说

    bool erase(int num) {
        std::vector<Node*> prevV = FindPrevNode(num);

        if(prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)
        {
            return false;
        }

        Node* del = prevV[0]->_nextV[0];
        
        for(size_t i = 0; i < del->_nextV.size(); i++)
        {
            prevV[i]->_nextV[i] = del->_nextV[i];
        }
        delete del;

        return true;
    }

同时这里还有一个优化的空间,如果删除的节点是跳表的最高节点,那么可以考虑降低头节点的高度

       //压缩高度
        int hight = _head->_nextV.size() - 1;
        while(hight >= 0)
        {
            if(_head->_nextV[hight] == nullptr)
            {
                hight--;
            }
            else
            {
                break;
            }
        }

        _head->_nextV.resize(hight + 1);

erase完整代码

    bool erase(int num) {
        std::vector<Node*> prevV = FindPrevNode(num);

        if(prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)
        {
            return false;
        }

        Node* del = prevV[0]->_nextV[0];
        
        for(size_t i = 0; i < del->_nextV.size(); i++)
        {
            prevV[i]->_nextV[i] = del->_nextV[i];
        }
        delete del;

        //压缩高度
        int hight = _head->_nextV.size() - 1;
        while(hight >= 0)
        {
            if(_head->_nextV[hight] == nullptr)
            {
                hight--;
            }
            else
            {
                break;
            }
        }

        _head->_nextV.resize(hight + 1);

        return true;
    }

完整代码

struct SkiplistNode
{
    int _val;
    std::vector<SkiplistNode*> _nextV;

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

};

class Skiplist {
    typedef SkiplistNode Node;
public:
    Skiplist() {
        srand(time(0));
        _head = new Node(-1, 1);
    }
    
    bool search(int target) {
        Node* cur = _head;
        int level = _head->_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;
    }

    size_t RandomLevel()
    {
        size_t level = 1;
        while(rand() <= RAND_MAX * _p && level < _maxLevel)
        {
            level++;
        }

        return level;
    }

    std::vector<Node*> FindPrevNode(int num)
    {
        Node* cur = _head;
        int level = _head->_nextV.size() - 1;
        std::vector<Node*> prevV(_maxLevel, _head);

        while(level >= 0)
        {
            if(cur->_nextV[level] && cur->_nextV[level]->_val < num)
            {
                cur = cur->_nextV[level];
            }
            else
            {
                prevV[level] = cur;
                level--;
            }
        }

        return prevV;
    }
    
    void add(int num) {
        std::vector<Node*> prevV = FindPrevNode(num);

        int n = RandomLevel();
        Node* newNode = new Node(num, n);
        if(n > _head->_nextV.size())
        {
            _head->_nextV.resize(n, nullptr);
        }

        for(size_t i = 0; i < n; i++)
        {
            newNode->_nextV[i] = prevV[i]->_nextV[i];
            prevV[i]->_nextV[i] = newNode;
        }
    }
    
    bool erase(int num) {
        std::vector<Node*> prevV = FindPrevNode(num);

        if(prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)
        {
            return false;
        }

        Node* del = prevV[0]->_nextV[0];
        
        for(size_t i = 0; i < del->_nextV.size(); i++)
        {
            prevV[i]->_nextV[i] = del->_nextV[i];
        }
        delete del;

        //压缩高度
        int hight = _head->_nextV.size() - 1;
        while(hight >= 0)
        {
            if(_head->_nextV[hight] == nullptr)
            {
                hight--;
            }
            else
            {
                break;
            }
        }

        _head->_nextV.resize(hight + 1);

        return true;
    }
private:
    Node* _head;
    size_t _maxLevel = 32;
    double _p = 0.25;
};

四、跳表与其它搜索结构对比

1. skiplist 相比平衡搜索树 (AVL 树和红黑树 ) 对比,都可以做到遍历数据有序,时间复杂度也差不多。skiplist 的优势是: a skiplist 实现简单,容易控制。平衡树增删查改遍历都更复杂。b、skiplist 的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子 / 颜色等消耗。skiplist中 p=1/2 时,每个节点所包含的平均指针数目为 2 skiplist p=1/4 时,每个节点所包
含的平均指针数目为 1.33
2. skiplist 相比哈希表而言,就没有那么大的优势了。相比而言 a 、哈希表平均时间复杂度是O(1),比 skiplist 快。 b 、哈希表空间消耗略多一点。 skiplist 优势如下: a 、遍历数据有序b、 skiplist 空间消耗略小一点,哈希表存在链接指针和表空间消耗。 c 、哈希表扩容有性能损耗。d 、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力。


总结


以上就是今天要讲的内容,本文仅仅简单介绍了跳表

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

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

相关文章

【高精度定位】RTK定位与RTD定位知识科普

高精度定位一般指亚米级别或厘米级别的定位&#xff0c;常见的室内有蓝牙AoA和UWB两种技术&#xff0c;室外有北斗地基增强技术&#xff0c;这些技术都是采用算法进行定位。 工业4.0时代&#xff0c;在资源和环境约束不断强化的背景下&#xff0c;创新驱动传统制造向智能制造转…

【MAUI】条形码,二维码扫描功能

前言 本系列文章面向移动开发小白&#xff0c;从零开始进行平台相关功能开发&#xff0c;演示如何参考平台的官方文档使用MAUI技术来开发相应功能。 介绍 移动端的扫描条形码、二维码的功能已经随处可见&#xff0c;已经很难找到一个不支持扫描的App了&#xff0c;但是微软的…

sync fsync fdatasync 三者的区别

传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存&#xff0c;大多数磁盘I/O都通过缓区进行。当我们向文件写入数据时&#xff0c;内核通常先将数据复制到缓冲区中&#xff0c;然后排入队列&#xff0c;晚些时候再入磁盘。这种方式被称为延迟写 (delayed wrie)(Bach[…

最惨面试季:“这么简单的9道题,我刷掉了90%的测试员。”

人往高处走水往低处流&#xff0c;十二月将至&#xff0c;“金三银四”招聘季还会远吗&#xff1f; 远观2022年的招聘季&#xff0c;在资本寒冬的映照下&#xff0c;的确萧条了不少&#xff0c;裁员、取消年终奖、末尾淘汰制等热门制度&#xff0c;让很多人陷入了“工作难保&a…

【自然语言处理】隐马尔科夫模型【Ⅴ】解码问题

有任何的书写错误、排版错误、概念错误等&#xff0c;希望大家包含指正。 由于字数限制&#xff0c;分成六篇博客。 【自然语言处理】隐马尔可夫模型【Ⅰ】马尔可夫模型 【自然语言处理】隐马尔科夫模型【Ⅱ】隐马尔科夫模型概述 【自然语言处理】隐马尔科夫模型【Ⅲ】估计问题…

基于安卓的校园订餐系统开发设计

目 录 Abstract 6 1 绪 论 1 1.1 研究背景 1 1.2 研究意义 1 1.3 国内外研究现状 1 1.4 研究的过程和结果 2 1.5 论文主要组织结构 3 2 设计原理和方法 4 2.2 关键技术简介 4 2.3 开发工具 5 2.4 应用平台 6 3 需求分析 8 3.1 功能性需求 8 3.2 非功能需求 17 4 系统概要设计 1…

JS文件操作介绍

JS文件操作介绍 本文将介绍前端浏览器支持的JS文件操作技术。相关权威技术资料 带有 type"file" 的 <input> 元素允许用户可以从他们的设备中选择一个或多个文件。<input type"file"> - HTML&#xff08;超文本标记语言&#xff09; | MDN …

HDFS的API操作

目录 依赖环境 AIP操作hdfs基本流程 创建目录 文件上传 参数优先级 文件下载 文件删除 文件更名与移动 查看HDFS文件的详情信息 文件和文件夹的判断 依赖环境 hadoop依赖&#xff1a; Maven Repository&#xff08;Maven仓库&#xff09;- https://mvnrepository.com/…

中小型企业 CRM 系统有哪些好的推荐?

不同行业的中小型企业在资源、需求、抗风险能力、业务成熟度等方面存在显著差异&#xff0c;因此对CRM的应用需求有不同侧重。 图源&#xff1a;艾瑞咨询我专门研究了一些CRM的行业报告&#xff0c;比较了一下当下几款比较热门的CRM&#xff0c;希望能够帮助大家找到自己的“心…

【能效管理】AcrelEMS-IDC数据中心综合能效管理系统应用分析

概述 安科瑞电气紧跟数据中心发展形式&#xff0c;推出AcrelEMS-IDC数据中心综合能效管理解决方案&#xff0c;包含有电力监控、动环监控、消防监控、能耗统计分析、智能照明控制以及新能源监测几个子系统。集成了变配电监测、电源备自投、电气接点测温、智能照明控制、电能质…

PD芯片在OTG设备的角色应用及OTG发展历史

OTG是On-The-Go的缩写&#xff0c;是近年发展起来的技术。2001年12月18日由USB标准化组织公布&#xff0c;主要应用于不同的设备或移动设备间的联接&#xff0c;进行数据交换。 USB技术的发展&#xff0c;使得PC和周边设备能够通过简单的方式、适度的制造成本&#xff0c;将各…

渗透测试sec123笔记

1.外网打点 1.1端口探测 └─$ sudo masscan 192.168.1.103 -p 1-65535 --rate100 masscan扫描结果 └─$ nmap -sV 192.168.1.103 -sC -p 8088,5985,49154,49173,80,8878,47001,7000,49153,49157,3389,49155,49152,8899,49161 -oN sec-ports …

开发转测试:从零开始的6年自动化之路,太迷茫...

自动化初识 作为一个测试人&#xff0c;我们或多或少都听过或用过自动化&#xff0c;我们都曾在初入测试行业时&#xff0c;满怀期待的以为测试的尽头是不用测试员点点了&#xff0c;项目一提测&#xff0c;小手点下自动化的开关&#xff0c;瞬间测试的工作就完成了。 这就是…

4-Arm PEG-Biotin,4-Arm peg-Biotin,四臂-聚乙二醇-生物素科研试剂供应

一&#xff1a;产品描述 1、名称 英文&#xff1a;4-Arm PEG-Biotin&#xff0c;4-Arm peg-Biotin 中文&#xff1a;四臂-聚乙二醇-生物素 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Biotin PEG Multi-arm PEGs 4、分子量&#xff1a;可定制&#xff0c;四臂-PE…

BundleTrack自用配置笔记

0、下载代码 git clone https://github.com/wenbowen123/BundleTrack.git 或者选择Download ZIP 1、按照Ubuntu版本安装docker 查看docker版本 $ docker version 在1080显卡上测试时出现bug&#xff0c;找到一个解决的文章&#xff0c;本人适用 docker: Error response…

微信小程序模板与配置

文章目录1. 数据绑定2. 事件绑定3. 事件传参4. 网络请求1. 数据绑定 在data中定义数据&#xff1a;在页面对应的js文件中&#xff0c;把数据定义到data对象中即可 // index.js Page({// string 类型info:init data,// 数组 类型msgList:[{msg:hello},{msg:world}] })在WXML中使…

测试人员面试需要掌握的内容,软件测试面试题答案

1、在公司的测试流程是什么&#xff1f; 产品经理确认本次版本的需求&#xff0c;召开需求评审会&#xff0c;进行估时排期&#xff0c;需求和时间都确定之后&#xff0c;UI出设计图&#xff0c;开发人员进行开发&#xff0c;测试人员编写测试用例&#xff0c;召开用例评审会议…

12.13

AJAX 视频 1-11 1.1 AJAX 简介 AJAX 全称为 Asynchronous JavaScript And XML&#xff0c;就是异步的 JS 和 XML。 通过 AJAX 可以在浏览器中向服务器发送异步请求&#xff0c;最大的优势&#xff1a;无刷新获取数据。 AJAX 不是新的编程语言&#xff0c;而是一种将现有…

909. 蛇梯棋-广度优先遍历

909. 蛇梯棋-广度优先遍历 给你一个大小为 n x n 的整数矩阵 board &#xff0c;方格按从 1 到 n2 编号&#xff0c;编号遵循 转行交替方式 &#xff0c;从左下角开始 &#xff08;即&#xff0c;从 board[n - 1][0] 开始&#xff09;每一行交替方向。 玩家从棋盘上的方格 1 …

高压开关电源3D PCB绘制教程:SOLIDWORKS绘制瓷片电容教程

本教程配有配套视频教程&#xff0c;读者可以配合配套的视频教程学习&#xff0c;下载本课的对应课件和源文件&#xff0c;更多课程及材料&#xff0c;敬请关注凡亿教育&#xff1a;《高压开关电源3D PCB绘制教程》。绘制指导&#xff1a;家介绍瓷片电容的绘制方法&#xff0c;…