跳表SkipList介绍与实现

news2025/1/12 21:47:16

目录

一.跳表介绍

二.实现思路

(一).结点结构

(二).检索

(三).插入

(四).删除

三.实现代码


一.跳表介绍

跳表是一种随机化数据结构,主要用于快速检索数据。实质上是一种可以进行二分查找的有序链表。时间复杂度可以达到O(log^n)。在性能上与红黑树、AVL树相当。当然因为结构具有随机性,最坏情况下时间复杂度为O(n)。

跳表结构如下图:

与普通链表相比,跳表每个结点有不止一个指向后续的指针,具体数量是随机出来的。这些指针结构上从低到高排列指向后面与自己同层的指针所在的结点

检索数据时,从head结点开始,按指针从高到低的所指元素大小进行比较,直到找到或走到结尾。因为层数越高代表跳过的元素数量越多,因此理论上可以类比二分查找。

查找时可能找到也可能找不到元素:

如果找到元素,以上图为例,假如要检索的是9,那么顺序如下:

 

 

如果是未找到元素,以6为例:


 

 

 

 

二.实现思路

(一).结点结构

结点通过结构体封装即可,内部是保存的结点元素值和可变数组,数组每一个元素是指向结点的指针。代码如下:

struct SkiplistNode {//结点
    int _val;//元素值,没有使用模板,可以自定义模板
    vector<SkiplistNode*> _skipPoints;//结点指针数组
    SkiplistNode(int val, int n = 1)//n:指针层数,默认1层
        :_val(val)
        , _skipPoints(n, nullptr)
    {}
    ~SkiplistNode()
    {
        for (auto* p : _skipPoints) p = nullptr;
    }
};

(二).检索

按照上述检索思路,检索失败的标准是走到结点指针的-1层。每一次检索时判断此时同层的指针所指后续元素大小,大于就走到该后续元素,小于就走到低一层的指针。

代码结构如下:

bool search(int target) {
    Node* cur = _head;//记录当前结点位置
    int sub = cur->_skipPoints.size() - 1;//从最高层开始,head层数即最高层数
    while (sub >= 0) {
    if (没有走到null && 大于后续结点值)
    {
        cur = cur->_skipPoints[sub];
    }
    else if (走到null || 小于后续结点值)
    {
        sub--;
    }
    else 找到结点
    }

    没找到结点
}

(三).插入

插入元素前,需要确定在哪个结点后插入,但基于跳表结点多层指针结构,每一层指针的前序指针可能不同,因此需要先检索一遍,确定每一层的前序元素结点

以8为例,插入后,每一层的前序指针不同。

 通过数组记录每一层的前序结点,在插入时按照链表的插入方式插入即可。

插入代码结构如下:

//获取前序结点数组,结构与search相似
vector<Node*> getPrev(int target) {
    vector<Node*> prev(_head->_skipPoints.size(), nullptr);
    Node* cur = _head;
    int sub = _head->_skipPoints.size() - 1;
    while (层数 >= 0) {
        if (大于后续结点) {
            cur = cur->_skipPoints[sub];
        }
        else if (小于等于后续结点) {
            prev[sub] = cur;//记录前序结点
            sub--;//向下走一层
        }
    }
    return prev;
}

void add(int num) {
    vector<Node*> prevPoints = getPrev(num);//专门记录插入结点的前序指针的数组
    if (prevPoints[0]记录下一个结点元素与插入值相同) return;//重复添加
    int i = getLevel();//获取随机层数
    Node* cur = new Node(num, i);
    if (随机层数比现有要高) {
        _head和prevPoints都要增加至i层
    }
    for (i -= 1; i >= 0; i--) {
        按普通链表插入即可
    }
}

同时,因为跳表每个结点有多少层指针是随机的,因此需要写一个随机函数确定层数:
结点每增加一层的概率为p,同时设定最大层数值。

层数概率
1 - p
p * (1 - p)
p * p * (1 - p)

根据表格可知,p越小结点增加层数的概率越低。

随机函数可以使用C++随机数库实现:

int getLevel() {
    //使用随机数库
    static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());
    //随机数范围0 - 1,类型是double
    static std::uniform_real_distribution<double> distribution(0.0, 1.0);

    int level = 1;
    //如果随机数小于_p同时没达到最大层数,层数++
    while (distribution(generator) <= _p && level < _maxLevel)
    {
        ++level;
    }

    return level;
}

(四).删除

删除元素同样要先找到每一层的前序结点。

之后删除时按照普通链表的方式删除即可。

同时如果删除的结点拥有唯一最高层,那么需要更新_head结点层数。

代码结构如下:

bool erase(int num) {
    //获取各层前序结点
    vector<Node*> prevPoints = getPrev(num);
    //没有该节点
    if (前序指针指向空 || 前序指向元素不是目标删除元素) {
        return false;
    }
    //获取待删除元素的结点,一层层删除
    for () {
        //...
    }
    //更新高度
    return true;
}

三.实现代码

元素以int为例,可以使用template变成模板类。

struct SkiplistNode {
    int _val;
    vector<SkiplistNode*> _skipPoints;
    SkiplistNode(int val, int n = 1)
        :_val(val)
        , _skipPoints(n, nullptr)
    {}
    ~SkiplistNode()
    {
        for (auto* p : _skipPoints) p = nullptr;
    }
};

class Skiplist {
    typedef SkiplistNode Node;

    vector<Node*> getPrev(int target) {
        vector<Node*> prev(_head->_skipPoints.size(), nullptr);
        Node* cur = _head;
        int sub = _head->_skipPoints.size() - 1;
        while (sub >= 0) {
            if (cur->_skipPoints[sub] && target > cur->_skipPoints[sub]->_val) {
                cur = cur->_skipPoints[sub];
            }
            else if (!cur->_skipPoints[sub] || target <= cur->_skipPoints[sub]->_val) {
                prev[sub] = cur;
                sub--;
            }
        }
        return prev;
    }
    int getLevel() {
        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);

        int level = 1;
        while (distribution(generator) <= _p && level < _maxLevel)
        {
            ++level;
        }

        return level;
    }
public:
    Skiplist() {
        srand(time(NULL));
        _head = new Node(-1);
    }

    bool search(int target) {
        Node* cur = _head;
        int sub = cur->_skipPoints.size() - 1;
        while (sub >= 0) {
            if (cur->_skipPoints[sub] && target > cur->_skipPoints[sub]->_val)//target大于下一个值, 继续向后
            {
                cur = cur->_skipPoints[sub];
            }
            else if (!cur->_skipPoints[sub] || target < cur->_skipPoints[sub]->_val)//target小于, 向下
            {
                sub--;
            }
            else return true;
        }
        return false;
    }

    void add(int num) {
        vector<Node*> prevPoints = getPrev(num);//专门记录插入结点的前序指针的数组
        //if (prevPoints[0]->_skipPoints[0] && prevPoints[0]->_skipPoints[0]->_val == num) return;//重复添加
        int i = getLevel();
        Node* cur = new Node(num, i);
        if (i > _head->_skipPoints.size()) {//随机层数比现有要高
            _head->_skipPoints.resize(i, nullptr);
            prevPoints.resize(i, _head);//让前序指针数组高出的指向_head,这样能将_head与结点相连
        }
        for (i -= 1; i >= 0; i--) {
            cur->_skipPoints[i] = prevPoints[i]->_skipPoints[i];
            prevPoints[i]->_skipPoints[i] = cur;
        }
    }

    bool erase(int num) {
        vector<Node*> prevPoints = getPrev(num);
        //如果前序为空(num大于所有节点值)或 前序下一个不是num(因为getPrev函数获得的是<=num)
        if (!prevPoints[0]->_skipPoints[0] || prevPoints[0]->_skipPoints[0]->_val != num) {
            return false;
        }
        //获取待删除元素的结点
        Node* cur = prevPoints[0]->_skipPoints[0];
        //一层层删除
        for (int i = cur->_skipPoints.size() - 1; i >= 0; i--) {
            prevPoints[i]->_skipPoints[i] = cur->_skipPoints[i];
        }
        delete cur;
        int n = _head->_skipPoints.size() - 1;
        while (n >= 0) {
            if (_head->_skipPoints[n] == nullptr) n--;
            else break;
        }
        _head->_skipPoints.resize(n + 1);
        return true;
    }
private:
    Node* _head;
    size_t _maxLevel = 32;
    double _p = 0.25;
};

信念和目标,必须永远洋溢在程序员内心——未名


如有错误,敬请斧正 

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

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

相关文章

JavaScript 函数

文章目录JavaScript 函数JavaScript 函数语法调用带参数的函数带有返回值的函数局部 JavaScript 变量全局 JavaScript 变量JavaScript 变量的生存期向未声明的 JavaScript 变量分配值笔记列表JavaScript 函数 函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块。 实…

Go语言之容器总结

目录 1.值类型 1.1. 数组Array 数组遍历 数组初始化 值拷贝 内置函数len、cap 2. 引用数据类型 2.1. 切片slice 切片初始化 切片的内存布局 通过slice修改struct array值 用append内置函数操作切片&#xff08;切片追加&#xff09; slice自动扩容 slice中cap重新…

基于yolov5算法的安全帽头盔检测源码+模型,Pytorch开发,智能工地安全领域中头盔目标检测的应用

基于yolov5算法的安全帽头盔检测|Pytorch开发源码模型 本期给大家打开的是YOLOv5在智能工地安全领域中头盔目标检测的应用。 完整代码下载地址&#xff1a;基于yolov5算法的安全帽头盔检测源码模型 可视化界面演示&#xff1a; &#x1f4a5;&#x1f4a5;&#x1f4a5;新增…

opencv c++ Mat CUDA的编译与使用

Mat 构造函数 cv::Mat img ; //默认 定义了一个Mat img cv::imread("image.jpg");//除了直接读取&#xff0c;还有通过大小构造等cv::Mat img cv::imread("image.png", IMREAD_GRAYSCALE); cv::Mat img_novel img;转换 Mat::convertTo(Mat& m, in…

【自学Java】Java方法

Java方法 Java方法教程 在 Java 语言 中&#xff0c;方法就是一段可重复调用的代码段。在平时开发直接交流中&#xff0c;也有一些同学喜欢把方法叫做函数&#xff0c;这两个其实是一个概念。 Java语言方法详解 语法 public void fun(Object param1,...){//do something }…

多线程与高并发(四)

【Exchanger】&#xff1a; package Ten_Class.t04.no139;import java.util.concurrent.Exchanger;public class T12_TestExchanger {static Exchanger<String> exchanger new Exchanger<>();public static void main(String[] args) {new Thread(() -> {Stri…

实验二十四 策略路由配置

实验二十四 策略路由配置实验要求&#xff1a; 某企业通过路由器AR1连接互联网&#xff0c;由于业务儒要&#xff0c;与两家运营商ISPA和ISPB相连。 企业网内的数据流从业务类型上可以分为两类&#xff0c; 一类来自于网络172.16.0.0/16&#xff0c;另 一类 来自于网络172.17.0…

百趣代谢组学分享:黑木耳多糖对小鼠肠道微生物及代谢表型的影响

文章标题&#xff1a;Effects of Auricularia auricula Polysaccharides on Gut Microbiota and Metabolic Phenotype in Mice 发表期刊&#xff1a;Foods 影响因子&#xff1a;5.561 作者单位&#xff1a;西北大学 百趣提供服务&#xff1a;发现代谢组学Standard-亲水版、1…

dataCompare大数据对比之异源数据对比

在从0到1介绍一下开源大数据比对平台dataCompare 已经详细介绍了dataCompare 的功能&#xff0c;目前dataCompare 已经实现同源数据的对比 一、dataCompare 现有核心功能如下&#xff1a; (1)数量级对比 (2)一致性对比 (3)差异case 自动发现 (4)定时调度自动对比数据 二、…

【个人解答版】笔试题-2023禾赛-FPGA

题目背景 笔试时间&#xff1a;2022.06.22应聘岗位&#xff1a;FPGA开发工程师 题目评价 难易程度&#xff1a;★★☆☆☆知识覆盖&#xff1a;★☆☆☆☆超纲范围&#xff1a;☆☆☆☆☆值得一刷&#xff1a;★☆☆☆☆ 文章目录1. 使用最少的电路实现二分频&#xff0c;给出…

《机器学习实战》chap1 机器学习概览

《机器学习实战》chap1 机器学习概览 Chap1 The Machine Learning Landscape 这本书第三版也已经出版了:https://github.com/ageron/handson-ml3 Hands-on Machine Learning with Scikit-Learn,Keras & TensorFlow 引入 很早的应用&#xff1a;光学字符识别(OCR&#xff0…

远程办公之怎样在外网登录在线答题网站

很多学校或企业因为教学、测试需要&#xff0c;为学生或员工提供了在线答题平台网站&#xff0c;但弊端是这种在线答题平台只能在校内或在企业内网访问使用&#xff0c;在外网是无法登录访问的。在无公网Ip服务器上部署的web&#xff0c;默认情况下只能内网访问&#xff0c;公网…

TLE4943C/CH505C轮速传感器芯片的输出协议介绍

Infineon公司的TLE4943是一款集成式有源磁场传感器&#xff0c;适用于基于霍尔技术的车轮速度应用。它的基本功能是测量磁极轮或铁磁齿轮的速度。它具有使用AK协议进行通信的两线电流接口。该协议除了提供速度信号外&#xff0c;还提供其他信息&#xff0c;如车轮旋转方向和气隙…

java安装教程-windows

检查是否已经安装过jav打开cmd命令窗口 输入 java -v下载java安装包网址&#xff1a;https://www.oracle.com/java/technologies/downloads/安装java双击运行程序jdk-19_windows-x64_bin.exe&#xff0c;点击下一步进行安装可以更改安装路径&#xff0c;注意安装路径不能有中文…

【TypeScript】TS类型守卫(六)

&#x1f431;个人主页&#xff1a;不叫猫先生 &#x1f64b;‍♂️作者简介&#xff1a;前端领域新星创作者、华为云享专家、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &#x1f4ab;系列专栏&#xff…

独立开发变现周刊(第86期):月收入4000美元的日程规划器

分享独立开发、产品变现相关内容&#xff0c;每周五发布。目录1、NotionReads: 在Notion中管理你的阅读书籍2、Zaap.ai: 面向创作者的一站式工具3、microfeed: 开源的可自我托管的轻量级内容管理系统(CMS)4、Reactive Resume&#xff1a;一个免费的开源简历生成器5、一个月收入…

2019年1月政企终端安全态势分析报告

声明 本文是学习2019年1月政企终端安全态势分析报告. 下载地址 http://github5.com/view/55037而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 漏洞利用病毒攻击政企分析 奇安信终端安全实验室监测数据显示&#xff0c;2019年4月&#xff0c;有6.7%的…

JavaScript中的元编程

紧接上回&#xff0c;伴随着Reflect&#xff0c;Proxy降世&#xff0c;为js带来了更便捷的元编程&#xff01; 什么是元编程&#xff1f;这词第一次听&#xff0c;有点懵&#xff0c;好像有点高级&#xff0c;这不得学一下装…进自己的知识库 概念 元编程是一种编程技术&…

【数据结构与算法】Collection接口迭代器

Java合集框架 数据结构是以某种形式将数据组织在一起的合集&#xff08;collection&#xff09;。数据结构不仅存储数据&#xff0c;还支持访问和处理数据的操作 在面向对象的思想里&#xff0c;一种数据结构也被认为是一个容器&#xff08;container&#xff09;或者容器对象…

【MySQL】MySQL表的七大约束

序号系列文章1【MySQL】MySQL介绍及安装2【MySQL】MySQL基本操作详解3【MySQL】MySQL基本数据类型4【MySQL】MySQL表的七大约束文章目录MySQL表的约束1&#xff0c;默认约束2&#xff0c;非空约束3&#xff0c;唯一约束4&#xff0c;主键约束5&#xff0c;自增约束6&#xff0c…