skiplist(高阶数据结构)

目录

一、概念

二、实现

三、对比


一、概念

skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》

skiplist本质上是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。skiplist是一个list,是在有序链表的基础上发展起来的。若是一个有序的链表,查找数据的时间复杂度是 O(N)

William Pugh开始的优化思路

1. 假如每相邻两个结点升高一层,增加一个指针,让指针指向下下个结点,这样所有新增加的指针连成了一个新的链表,但包含的结点个数只有原来的一半。由于新增加的指针,不再需要与链表中每个结点逐个进行比较了,需要比较的结点数大概只有原来的一半

2. 以此类推,可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表,这样搜索效率就进一步提高了

3. skiplist正是受这种多层链表的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的结点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(logN)。但是这个结构在插入删除数据的时候有很大的问题,插入或者删除一个结点后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。若要维持这种对应关系,就必须把新插入结点后面的所有结点(也包括新插入的结点)重新进行调整,这会让时间复杂度重新退化成 O(N)

4. skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个结点时随机出一个层数。这样每次插入和删除都不需要考虑其他结点的层数

skiplist的效率如何保证?

skiplist插入一个节点时随机出一个层数,听起来如此随意,如何保证搜索时的效率呢?

一般跳表会设计最大层数maxLevel的限制,其次会设置一个多增加一层的概率p,伪代码如下:

在Redis的skiplist实现中,这两个参数的取值为:maxLevel = 32p = 1/4

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

一个结点的平均层数(即包含的平均指针数目) 

现在可以计算出:

  • 当p = 1/2时,每个结点所包含的平均指针数目为2
  • 当p = 1/4时,每个结点所包含的平均指针数目为1.33

跳表的平均时间复杂度为O(logN),推导过程较为复杂,需要一定数学功底,有兴趣可以参考以下大佬文章中的讲解

Redis内部数据结构详解(6)——skiplist - 铁蕾的个人博客 Redis内部数据结构详解(6)——skiplist - 铁蕾的个人博客 - 作者:张铁蕾icon-default.png?t=N7T8http://zhangtielei.com/posts/blog-redis-skiplist.html

二、实现

https://leetcode.cn/problems/design-skiplist/description/

结点设计

struct SkiplistNode
{
    SkiplistNode(int value, int level)
        :_value(value) ,_nextVector(level, nullptr) {}
    int _value;
    vector<SkiplistNode*> _nextVector;
};

Skiplist成员变量和构造函数

class Skiplist
{
    typedef SkiplistNode Node;
public:
    Skiplist()
    {
        srand(time(0)); //设置随机数种子,后续随机生成结点层数时使用
        _head = new SkiplistNode(-1, 1);//头结点初始层数为1
    }

private:
    Node* _head;
    size_t maxLevel = 32; //最大层数限制
    double _p = 0.25; //多增加一层的概率
};

search函数                      

  • 记录当前所在结点以及所在结点的层数
  • 只有level >=0 时查找才有效,否则返回false
  • 若当前结点的值 > 目标值则向右走
  • 若当前结点的值 < 目标值 或者 同一层下一个结点为空(下标--)  ,则向下走
bool search(int target)
{
    Node* current = _head;
    int levelIndex = _head->_nextVector.size() - 1;
    while (levelIndex >= 0)
    {
        //目标值比下一个结点的值大
        if (current->_nextVector[levelIndex] != nullptr && current->_nextVector[levelIndex]->_value < target)
            current = current->_nextVector[levelIndex]; //向右走
        //下一个结点是空(尾)
        //目标值比下一个结点要小
        else if (current->_nextVector[levelIndex] == nullptr || current->_nextVector[levelIndex]->_value > target)
            --levelIndex; //向下走
        else
            return true; //找到了
    }
    return false;
}

FindPrevNode函数

设计该函数的目的:

  • add函数添加新结点要找到新结点每一层的前一个结点进行连接 
  • erase函数删除结点要找到该结点每一层前一个结点 与 每一层的后一个结点进行连接
  • 使代码简洁,设计了FindPrevNode函数,实现代码的复用
vector<Node*> FindPrevNode(int number)
{
    Node* current = _head;
    int levelIndex = _head->_nextVector.size() - 1;

    //待插入结点或待删除结点 的每一层的前一个结点的指针
    vector<Node*> prevVector(levelIndex + 1, _head);

    while (levelIndex >= 0)
    {
        if (current->_nextVector[levelIndex] != nullptr && current->_nextVector[levelIndex]->_value < number)
            current = current->_nextVector[levelIndex];
        else if (current->_nextVector[levelIndex] == nullptr || current->_nextVector[levelIndex]->_value >= number)
        {
            prevVector[levelIndex] = current;
            --levelIndex;
        }
    }
    return prevVector;
}

该函数基本与search函数相同,需要注意的是:当current->_nextVector[levelIndex]->_value >= number时,记录current结点。比search多一个等于,因为最低层的指针也需要修改链接

add函数

  • 获取要添加结点的前一个Node的集合
  • 随机获取层数,构建新结点并初始化
  • 若随机获取的层数超过当前最大的层数,那就升高一下_head的层数
  • 利用前一个Node集合 prevVector 和 当前结点的每一层建立连接关系
void add(int number)
{
    //获取要添加数据的前一个Node的集合
    vector<Node*> prevVector = FindPrevNode(number);

    //随机获取层数,构建新结点并初始化
    int level = RandomLevel();
    Node* newNode = new Node(number, level);

    //若随机获取的层数超过当前最大的层数,那就升高一下_head的层数
    if (level > _head->_nextVector.size()) {
        _head->_nextVector.resize(level, nullptr);
        prevVector.resize(level, _head);
    }

    //链接前后结点
    for (int i = 0; i < level; ++i) {
        newNode->_nextVector[i] = prevVector[i]->_nextVector[i];
        prevVector[i]->_nextVector[i] = newNode;
    }
}

为什么不一开始就将_head头结点的层数设为最高呢?

一开始设为最高,后序查找有很多是无用的,所以不直接将_head设为最高,且利用一个变量记录最高层,当新插入数据的层数 > 最高层时才增加层数

erase函数

  • 获取要删除结点的前一个Node的集合prevVector
  • 若prevVector[0]->_nextVector[0] == nullptr || prevVector[0]->_nextVector[0]->_value != number 即未找到该数据,返回false
  • 否则记录要删除的Node ,去除前后连接关系,然后delete释放资源
  • 若删除的是最高层节点,重新调整头结点层数,下次查找时就不会从无用的最高层开始查找 (这个过程做不做都行,提升不太大)
bool erase(int number)
{
    //获取要删除结点的前一个Node的集合
    vector<Node*> prevVector = FindPrevNode(number);

    if (prevVector[0]->_nextVector[0] == nullptr || prevVector[0]->_nextVector[0]->_value != number)
        return false;
    else
    {
        Node* deleteNode = prevVector[0]->_nextVector[0];
        // deleteNode结点每一层的前后指针链接起来
        for (int i = 0; i < deleteNode->_nextVector.size(); ++i)
            prevVector[i]->_nextVector[i] = deleteNode->_nextVector[i];
        delete deleteNode;

        //若删除的是最高层节点,重新调整头结点层数
        int headLevel = _head->_nextVector.size() - 1;
        while (headLevel >= 0)
        {
            if (_head->_nextVector[headLevel] == nullptr)
                --headLevel;
            else break;
        }
        _head->_nextVector.resize(headLevel + 1);
    }
    return true;
}

获取随机数

方法一:C语言

int RandomLevel()
{
    size_t level = 1;
    // rand() ->[0, RAND_MAX]之间,将[0,RAND_MAX]看作为[0,1]
    while (rand() <= RAND_MAX * _p && level < _maxLevel)
        ++level;
    return level;
}

方法二:C++

std::uniform_real_distribution<double> distribution(0.0, 1.0) ,随机生成0.0  -  1.0的数,生成的数是均匀分布的

int RandomLevelCPP()
{
    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) <= _p && level < _maxLevel)
        ++level;
    return level;
}

三、对比

skiplist与红黑树、AVL树对比

  • skiplist和平衡搜索树(AVL树和红黑树)都可以做到遍历数据有序,时间复杂度也差不多
  • 但skiplist实现简单,容易控制。平衡树增删查改遍历都更复杂.
  • 并且skiplist的额外空间消耗更低。平衡树结点存储每个值有三叉链,平衡因子/颜色等消耗。skiplist中 p=1/2 时,每个结点所包含的平均指针数目为2;skiplist中 p=1/4 时,每个结点所包含的平均指针数目为1.33

skiplist与哈希表对比

skiplist与哈希表对比,就没有那么大的优势了。哈希表平均时间复杂度是 O(1),比skiplist快,但是哈希表空间消耗略多一点

  • 哈希表扩容有性能损耗
  • 哈希表在极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(k8s中)docker netty OOM问题记录

1、首先查看docker的内存占用情况&#xff1a; docker top 容器名 -u 查看内存cpu占用率&#xff08;容器名来自kubectl describe pod xxx中信息&#xff09; 可以看出内存一直增长&#xff0c;作为IO代理这是不正常的。 2、修改启动参数和配置文件 需要注意的是为了安全考…

BOOT电路

本质&#xff1a;BOOT电路本质上是单片机的引脚 作用&#xff1a;BOOT电路的作用是用于确定单片机的启动模式 使用方法&#xff1a;在单片机上电或者复位时给BOOT管脚设置为指定电平即可将单片机设置为指定启动模式。 原理&#xff1a;单片机上电或复位后会先启动内部晶振&a…

Vue的生命周期函数

今天我们来讲一讲Vue中的生命周期函数 每个Vue实例在其生命周期中都会经历多个关键阶段&#xff0c;这些阶段包括数据监听设置、模板编译、实例DOM挂载以及数据变化时的DOM更新等。同时&#xff0c;Vue提供了一系列生命周期钩子函数&#xff0c;允许开发者在这些重要阶段插入自…

EMR StarRocks实战——猿辅导的OLAP演进之路

目录 一、数据需求产生 二、OLAP选型 2.1 需求 2.2 调研 2.3 对比 三、StarRocks的优势 四、业务场景和技术方案 4.1 整体的数据架构 4.2 BI自助/报表/多维分析 4.3 实时事件分析 4.5 直播教室引擎性能监控 4.4 B端业务后台—斑马 4.5 学校端数据产品—飞象星球 4…

js 手写深拷贝方法

文章目录 一、深拷贝实现代码二、代码讲解2.1 obj.constructor(obj)2.2 防止循环引用 手写一个深拷贝是我们常见的面试题&#xff0c;在实现过程中我们需要考虑的类型很多&#xff0c;包括对象、数组、函数、日期等。以下就是深拷贝实现逻辑 一、深拷贝实现代码 const origin…

(转载)SpringCloud 微服务(三)-Seata解决分布式事务问题

ps:这个原文写的很好&#xff0c;怕后续这个地址失效&#xff0c;备份一个留着自己学习 转自&#xff1a;SpringCloud 微服务&#xff08;三&#xff09;-Seata解决分布式事务问题_seata 黑马 代码-CSDN博客 看完了黑马程序员的免费课程&#xff0c;感觉受益匪浅&#xff0c;…

基于springboot实现旅游路线规划系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现旅游路线规划系统演示 摘要 随着互联网的飞速发展以及旅游产业的逐渐升温&#xff0c;越来越多人通过互联网获取更多的旅游信息&#xff0c;包括参考旅游文纪等内容。通过参考旅游博主推荐的旅游景点和规划线路&#xff0c;参考计划着自己的旅行&#xff0c…

智慧应急与物联网相结合:物联网技术如何提升智慧应急响应能力

目录 一、引言 二、智慧应急与物联网技术的结合 三、物联网技术提升智慧应急响应能力的途径 四、物联网技术在智慧应急中的应用案例 五、物联网技术在智慧应急中面临的挑战与解决方案 挑战一&#xff1a;技术标准与规范不统一 解决方案&#xff1a; 挑战二&#xff1a;…

双流机场到天府机场ADS-B数据导入MATLAB

MATLAB导入数据 导入的数据Excel部分截图&#xff1a; 一些处理 % 导入外部轨迹数据并转成标准形式 clear;clc; %% 导入&预处理 [NUM,TXT,RAW]xlsread(2021年10月31日CTU-TFU); time_cell RAW(3:end,1); %拉取时间数据&#xff08;cell&#xff09; time_char char(t…

MySQL里的两个“二次”

文章中所有图片均来自网络 一、double write 第一个二次是mysql一个崩溃恢复很重要的特性-重复写入。 doublewrite缓冲区是位于系统表空间中的存储区域&#xff0c;在该区域中&#xff0c;InnoDB会在将页面写入数据文件中的适当位置之前&#xff0c;从InnoDB缓冲池中刷新这些页…

串联所有单词的子串

题目链接 串联所有单词的子串 题目描述 注意点 words[i] 和 s 由小写英文字母组成1 < words.length < 5000可以以 任意顺序 返回答案words中所有字符串长度相同 解答思路 根据滑动窗口哈希表解决本题&#xff0c;哈希表存储words中所有的单词及单词的出现次数&#…

K8s Pod资源管理组件

目录 Pod基础概念 在Kubrenetes集群中Pod有如下两种使用方式 pause容器使得Pod中的所有容器可以共享两种资源 网络 存储 总结 kubernetes中的pause容器主要为每个容器提供功能 Kubernetes设计这样的Pod概念和特殊组成结构的用意 通常把Pod分为以下几类 自主式Pod 控…

高温应用中GaN HEMT大信号建模的ASM-HEMT

来源&#xff1a;An ASM-HEMT for Large-Signal Modeling of GaN HEMTs in High-Temperature Applications&#xff08;JEDS 23年&#xff09; 摘要 本文报道了一种用于模拟高温环境下氮化镓高电子迁移率晶体管&#xff08;GaN HEMT&#xff09;的温度依赖性ASM-HEMT模型。我…

算法沉淀——动态规划之子序列问题(下)(leetcode真题剖析)

算法沉淀——动态规划之子序列问题 01.最长定差子序列02.最长的斐波那契子序列的长度03.最长等差数列04.等差数列划分 II - 子序列 01.最长定差子序列 题目链接&#xff1a;https://leetcode.cn/problems/longest-arithmetic-subsequence-of-given-difference/ 给你一个整数数…

springboot+maven项目导入本地jar包,以有打包错误问题

1 本地jar包放置路径为&#xff1a; 2添加Modules File->project settings–>Modules–>Dependencies–>–>, 3 添加 Libraies 至此 项目即可成功运行。 mvn 打包错误&#xff0c;需要 运行以下命令 mvn install:install-file -Dfile${project.basedir}/s…

Python进阶学习:Numpy--ndim、shape、dtype、astype的用法说明

Python进阶学习&#xff1a;Numpy–ndim、shape、dtype、astype的用法说明 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448…

拥有美国洛杉矶RAKsmart云服务器:探索无限可能

随着信息技术的飞速发展&#xff0c;云服务器已成为企业和个人用户不可或缺的重要工具。美国洛杉矶的RAKsmart云服务器&#xff0c;凭借其卓越的性能、稳定的网络环境和高级的安全性&#xff0c;为用户提供了无尽的便利和可能性。那么&#xff0c;拥有这样一台云服务器&#xf…

【Java程序设计】【C00338】基于Springboot的银行客户管理系统(有论文)

基于Springboot的银行客户管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的银行客户管理系统&#xff0c;本系统有管理员、员工以及用户二种角色&#xff1b; 管理员&#xff1a;个人中心、管理员管理、客…

LabVIEW水下温盐深数据一体化采集与分析

LabVIEW水下温盐深数据一体化采集与分析 开发一个基于LabVIEW的水下温盐深数据一体化采集与分析系统&#xff0c;实现海洋环境监测的自动化和精确化。通过集成温度、盐度和深度传感器&#xff0c;结合USB数据采集卡&#xff0c;利用LabVIEW软件开发的图形化界面&#xff0c;实…

Java Web(十一)--JSON Ajax

JSON JSon在线文档&#xff1a; JSON 简介 JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。轻量级指的是跟xml做比较。数据交换指的是客户端和服务器之间业务数据的传递格式。 它基于 ECMAScript (W3C制定的JS规范)的一个子集&#xff0c;采…