skiplist(高阶数据结构)

news2025/1/17 6:05:50

目录

一、概念

二、实现

三、对比


一、概念

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快,但是哈希表空间消耗略多一点

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

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

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

相关文章

ntp时钟服务安装- 局域网节点时间同步

场景&#xff1a; 一般部署大数据相关应用服务&#xff0c;各个节点之间需要时间同步&#xff1b;内网情况下&#xff0c;很可能各节点之前时间可能不一致&#xff0c;或者过一段时间后 又不一致了 ntp 时钟服务器&#xff1a; 可用于内网各个节点之前得时间同步&#xff0c;安…

C#理论 —— 基础语法、数据类型、变量、常量、运算符、三大结构

文章目录 1. 基础语法1.1 标识符命名规则1.2 C# 关键字1.3 C#注释 2. 数据类型2.1 值类型&#xff08;Value types&#xff09;2.2 引用类型&#xff08;Reference types&#xff09;2.2.1 对象&#xff08;Object&#xff09;类型3.2.2 动态&#xff08;Dynamic&#xff09;类…

ubuntu常见配置

ubuntu各个版本的安装过程大差小不差&#xff0c;可以参考&#xff0c;ubuntu20.04 其它版本换一下镜像版本即可 安装之后需要配置基本的环境&#xff0c;我的话大概就以下内容&#xff0c;后续可能有所删改 sudo apt-get update sudo apt-get install gcc sudo apt-get inst…

【踩坑】PyTorch中指定GPU不生效和GPU编号不一致问题

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 指定GPU不生效问题 解释&#xff1a;就是使用os.environ["CUDA_VISIBLE_DEVICES"] "1"后&#xff0c;后面使用起来仍然是cuda0. 解决&#xff1a;在最开头就使用 import os os.environ[&…

【MySQL | 第一篇】undo log、redo log、bin log三者之间的区分?

undo log、redo log、bin log三者之间的区分&#xff1f; 从 产生的时间点、日志内容、用途 三方面展开论述即可 1.undo log——撤销日志 时间点&#xff1a;事务开始之前产生&#xff0c;根据当前版本的数据生成一个undo log&#xff0c;也保存在事务开始之前 作用&#xf…

springboot+vue网站开发-渲染前端列表页面-缩略图信息

springbootvue网站开发-渲染前端列表页面-缩略图信息&#xff01;内容比较多。这是第一篇&#xff0c;先给大家展示的是&#xff0c;基础的代码封装&#xff0c;vue前端网站模块的代码展示。 我们使用到了pinia-存储我们请求过来的数据&#xff0c;它是一个状态管理&#xff0c…

非线性优化-高斯牛顿法

在SLAM领域&#xff0c;后端多采用基于非线性优化的方法&#xff0c;来优化位姿和地图点&#xff0c;其中高斯牛顿法的使用频率很高。 求解高斯牛顿法的核心公式&#xff1a; 其中 f 是误差函数&#xff0c;J是误差关于待优化变量的雅可比矩阵。 其中H为海森矩阵&#xff08…

RRT算法学习及MATLAB演示

文章目录 1 前言2 算法简介3 MATLAB实现3.1 定义地图3.2 绘制地图3.3 定义参数3.4 绘制起点和终点3.5 RRT算法3.5.1 代码3.5.2 效果3.5.3 代码解读 4 参考5 完整代码 1 前言 RRT&#xff08;Rapid Random Tree&#xff09;算法&#xff0c;即快速随机树算法&#xff0c;是LaVa…

C语言第三十二弹---自定义类型:联合和枚举

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 目录 1、联合体 1.1、联合体类型的声明 1.2、联合体的特点 1.3、相同成员的结构体和联合体对比 1.4、联合体大小的计算 1.5、联合的⼀个练习 2、枚举类型 …

176基于matlab的自适应滤波法预测

基于matlab的自适应滤波法预测&#xff0c;自适应滤波预测实质上是一种加权滑动平均预测&#xff0c;通过迭代得到最佳权值&#xff0c;并给出了相对误差图和预测效果图&#xff0c;程序已调通&#xff0c;可直接运行。 176matlab自适应滤波法预测 时间序列预测 (xiaohongshu.c…

51单片机(6)-----直流电机的介绍与使用(通过独立按键控制电机的运行)

前言&#xff1a;感谢您的关注哦&#xff0c;我会持续更新编程相关知识&#xff0c;愿您在这里有所收获。如果有任何问题&#xff0c;欢迎沟通交流&#xff01;期待与您在学习编程的道路上共同进步。 目录 一. 直流电机模块介绍 1.直流电机介绍 2.电机参数 二. 程序设计…

java线程池原理源码解析,程序员如何技术划水

前言 面试大概九十分钟&#xff0c;问的东西很全面&#xff0c;需要做充足准备&#xff0c;就是除了概念以外问的有点懵逼了。回来之后把这些题目做了一个分类并整理出答案&#xff08;强迫症的我~狂补知识&#xff09;分为MySQLJavaRedis算法网络Linux等六类&#xff0c;接下…

2024-02-28(Kafka,Oozie,Flink)

1.Kafka的数据存储形式 一个主题由多个分区组成 一个分区由多个segment段组成 一个segment段由多个文件组成&#xff08;log&#xff0c;index&#xff08;稀疏索引&#xff09;&#xff0c;timeindex&#xff08;根据时间做的索引&#xff09;&#xff09; 2.读数据的流程 …

Swagger接口文档管理工具

Swagger 1、Swagger1.1 swagger介绍1.2 项目集成swagger流程1.3 项目集成swagger 2、knife4j2.1 knife4j介绍2.2 项目集成knife4j 1、Swagger 1.1 swagger介绍 官网&#xff1a;https://swagger.io/ Swagger 是一个规范和完整的Web API框架&#xff0c;用于生成、描述、调用和…

textbox跨线程写入

实现实例1 实现效果 跨线程实现 // 委托&#xff0c;用于定义在UI线程上执行的方法签名 //public delegate void SetTextCallback(string text);public void textBoxText(string text){// 检查调用线程是否是创建控件的线程 if (textBox1.InvokeRequired){// 如果不是&#…

React UI框架Antd 以及 如何按需引入css样式配置(以及过程中各种错误处理方案)

一、react UI框架Antd使用 1.下载模块 npm install antd -S 2.引入antd的样式 import ../node_modules/antd/dist/reset.css; 3.局部使用antd组件 import {Button, Calendar} from antd; import {PieChartTwoTone} from ant-design/icons; {/* 组件汉化配置 */} import l…

Ant for Blazor做单个表的增删查改

Ant for Blazor做单个表的增删查改 2024年02月27日花了一天时间弄出来了&#xff0c;基本弄好了&#xff0c;vs2022blazor servernet8,引用的AntDesign版本是0.17.4 代码里的model和repository是用自己牛腩代码生成器生成的东西&#xff0c;sqlsugar的&#xff0c;记得在prog…

ROS 2基础概念#1:计算图(Compute Graph)| ROS 2学习笔记

在ROS中&#xff0c;计算图&#xff08;ROS Compute Graph&#xff09;是一个核心概念&#xff0c;它描述了ROS节点之间的数据流动和通信方式。它不仅仅是一个通信网络&#xff0c;它也反映了ROS设计哲学的核心——灵活性、模块化和可重用性。通过细致探讨计算图的高级特性和实…

面试数据库篇(mysql)- 12分库分表

拆分策略 垂直分库 垂直分库:以表为依据,根据业务将不同表拆分到不同库中。 特点: 按业务对数据分级管理、维护、监控、扩展在高并发下,提高磁盘IO和数据量连接数垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中。 特点: 1,冷热数据分离 2,减少IO过渡争…

CSS——PostCSS简介

文章目录 PostCSS是什么postCSS的优点补充&#xff1a;polyfill补充&#xff1a;Stylelint PostCSS架构概述工作流程PostCSS解析方法PostCSS解析流程 PostCSS插件插件的使用控制类插件包类插件未来的CSS语法相关插件后备措施相关插件语言扩展相关插件颜色相关组件图片和字体相关…