[C++][数据结构][跳表]详细讲解

目录

  • 0.什么是跳表?
  • 1.SkipList的优化思路
  • 2.SkipList的效率如何保证?
  • 3.SkipList实现
  • 4.SkipList VS 平衡搜索树 && Hash


0.什么是跳表?

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

1.SkipList的优化思路

  • 假如每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图b所示

    • 这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半
    • 由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半
  • 以此类推,可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表,这样搜索效率就进一步提高了,如下图c

  • SkipList正是受这种多层链表的想法的启发而设计出来的

  • 实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到 O ( l o g 2 N ) O(log_2N) O(log2N)

  • 但是这个结构在插入删除数据的时候有很大的问题

    • 插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系

    • 如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)

      请添加图片描述

  • SkipList的设计为了避免这种问题,做了一个大胆的处理

    • 不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数

    • 这样每次插入和删除都不需要考虑其他节点的层数, 这样就好处理多了

      请添加图片描述


2.SkipList的效率如何保证?

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

  • 首先要细节分析的是这个随机层数是怎么来的

    • 一般跳表会设计一个最大层数maxLevel的限制
    • 其次会设置一个多增加一层的概率p
    • 计算这个随机层数的伪代码如下图:
      请添加图片描述
  • **参考:**在Redis的SkipList实现中,这两个参数的取值为:

    p = 1/4;
    maxLevel = 32;
    
  • 根据前面randomLevel()的伪代码,产生越高的节点层数,概率越低

    • 节点层数至少为1,而大于1的节点层数,满足一个概率分布
    • 节点层数恰好等于1的概率为 1 − p 1-p 1p
    • 节点层数大于等于2的概率为 p p p,而节点层数恰好等于2的概率为 p ∗ ( 1 − p ) p*(1-p) p(1p)
    • 节点层数大于等于3的概率为 p 2 p^2 p2,而节点层数恰好等于3的概率为 p 2 ∗ ( 1 − p ) p^2*(1-p) p2(1p)
    • 节点层数大于等于4的概率为 p 3 p^3 p3,而节点层数恰好等于4的概率为 p 3 ∗ ( 1 − p ) p^3*(1-p) p3(1p)
  • 因此,一个节点的平均层数(即包含的平均指针数目),计算如下:

    • p = 1 / 2 p=1/2 p=1/2时,每个节点所包含的平均指针数目为2
    • p = 1 / 4 p=1/4 p=1/4时,每个节点所包含的平均指针数目为1.33
      请添加图片描述
  • SkipList的平均时间复杂度为: O ( l o g N ) O(logN) O(logN)


3.SkipList实现

  • 插入结点的关键是找到这个位置的每一层前一个结点
    • 比它小,向下走
    • 比它大,向右走
struct SkipListNode
{
    int _val;
    vector<SkipListNode*> _nextV;

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

class Skiplist
{
    typedef SkipListNode Node;
public:
    Skiplist()
    {
        srand(time(nullptr));

        _head = new Node(-1, 1); // 头节点,层数是1
    }

    bool Search(int target)
    {
        Node* cur = _head;
        int level = _head->_nextV.size() - 1;

        while (level >= 0)
        {

            if (cur->_nextV[level] && target > cur->_nextV[level]->_val)
            {
                // 目标值比下一个结点值大,向右走
                cur = cur->_nextV[level];
            }
            else if (!cur->_nextV[level] || target < cur->_nextV[level]->_val)
            {
                // 下一个结点是空(尾) || 目标值比下一个节点值要小,向下走
                level--;
            }
            else
            {
                return true;
            }
        }

        return false;
    }

    void Add(int num)
    {
        vector<Node*> preV = FindPrevNode(num);

        int n = RandomLevel();
        Node* newnode = new Node(num, n);

        // 如果n超过当前最大的层数,那就升高一下_head的层数
        if (n > _head->_nextV.size())
        {
            _head->_nextV.resize(n, nullptr);
            preV.resize(n, _head);
        }

        // 链接前后结点
        for (size_t i = 0; i < n; i++)
        {
            newnode->_nextV[i] = preV[i]->_nextV[i];
            preV[i]->_nextV[i] = newnode;
        }
    }

    bool Erase(int num)
    {
        vector<Node*> preV = FindPrevNode(num);

        // 第一层下一个不是val,则val不在表中
        if (!preV[0]->_nextV[0] || preV[0]->_nextV[0]->_val != num)
        {
            return false;
        }

        Node* del = preV[0]->_nextV[0];

        // del结点每一层前后指针链接起来
        for (size_t i = 0; i < del->_nextV.size(); i++)
        {
            preV[i]->_nextV[i] = del->_nextV[i];
        }
        delete del;

        // 如果删除最高层结点,把头节点的层数也降一下
        // 可以稍微提高查找效率
        int i = _head->_nextV.size() - 1;
        while (i >= 0)
        {
            if (!_head->_nextV[i])
            {
                i--;
            }
            else
            {
                break;
            }
        }
        _head->_nextV.resize(i + 1);

        return true;
    }

    // SkipList精髓
    vector<Node*> FindPrevNode(int num)
    {
        Node* cur = _head;
        int level = _head->_nextV.size() - 1;

        // 插入位置每一层前一个结点指针
        vector<Node*> preV(level + 1, _head);

        while (level >= 0)
        {
            if (cur->_nextV[level] && num > cur->_nextV[level]->_val)
            {
                // 目标值比下一个结点值大,向右走
                cur = cur->_nextV[level];
            }
            else if (!cur->_nextV[level] || num <= cur->_nextV[level]->_val)
            {
                preV[level--] = cur;
            }
        }

        return preV;
    }

    // v1.0 C
    int RandomLevel()
    {
        size_t level = 1;

        // rand() / RAND_MAX -> [0, 1]
        while (rand() <= RAND_MAX * _p && level <= _maxLevel)
        {
            level++;
        }

        return level;
    }

    // v2.0 C++
    // int RandomLevel()
    // {
    //     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;
    // }
private:
    Node* _head;
    size_t _maxLevel = 32;
    double _p = 0.5;
};

4.SkipList VS 平衡搜索树 && Hash

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

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

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

相关文章

网络安全:Web 安全 面试题.(SQL注入)

网络安全&#xff1a;Web 安全 面试题.&#xff08;SQL注入&#xff09; 网络安全面试是指在招聘过程中,面试官会针对应聘者的网络安全相关知识和技能进行评估和考察。这种面试通常包括以下几个方面&#xff1a; &#xff08;1&#xff09;基础知识:包括网络基础知识、操作系…

php,python aes加密反解

1. python版本 import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpadclass AESUtilCBC:def __init__(self, key, iv):self.key key.encode(utf-8)self.iv iv.encode(utf-8)self.pad_length AES.block_sizedef encrypt(self, data):try…

WPF文本绑定显示格式StringFormat设置-特殊格式时间日期和多数据绑定

WPF文本绑定显示格式StringFormat设置 特殊格式设置日期/时间使用系统默认样式自定义格式&#xff1a; 绑定多个属性&#xff08;多重绑定&#xff09;多重绑定中的特殊字符示例&#xff1a; 特殊格式设置 在Textblock等文本控件中&#xff0c;我们经常要显示一些日期和时间&a…

第3章 小功能大用处-发布订阅

Redis提供了基于“发布/订阅”模式的消息机制&#xff0c;此种模式下&#xff0c;消息发布者和订阅者不进行直接通信&#xff0c;发布者客户端向指定的频道&#xff08;channel&#xff09;发布消息&#xff0c;订阅该频道的每个客户端都可以收到该消息。 命令&#xff1a;Red…

AI落地不容乐观-从神话到现实

开篇 在这儿我不是给大家泼冷水&#xff0c;而是我们一起来看一下从2022年11月左右GPT3.0掀起了一股“AI狂潮”后到现在&#xff0c;AI在商用、工业、军用下到底有没有得到了大规模应用呢&#xff1f; 这个答案每一个参与者其实心里有数那就是&#xff1a;没有。 但是呢它的…

win10修改远程桌面端口号,在Windows 10中修改远程桌面端口号的步骤

在Windows 10中&#xff0c;远程桌面服务&#xff08;Remote Desktop Services, RDS&#xff09;允许用户从远程位置访问和操作计算机。默认情况下&#xff0c;远程桌面协议&#xff08;RDP&#xff09;使用端口3389进行通信。然而&#xff0c;出于安全考虑&#xff0c;管理员可…

时序预测 | Matlab基于CNN-BiLSTM-Attention多变量时间序列多步预测

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab基于CNN-BiLSTM-Attention多变量时间序列多步预测&#xff1b; 2.多变量时间序列数据集&#xff08;负荷数据集&#xff09;&#xff0c;采用前96个时刻预测的特征和负荷数据预测未来96个时刻的负荷数据&…

Mac安装多个jdk环境(jdk8+jdk17)保姆级

Mac安装多个jdk环境&#xff08;jdk8jdk17&#xff09;保姆级 背景&#xff1a;新机安装开发环境发现需要找很多文章&#xff0c;&#xff0c;&#xff0c;&#xff0c;这里一篇文章安装所有环境 文章目录 Mac安装多个jdk环境&#xff08;jdk8jdk17&#xff09;保姆级&#x1f…

异步FIFO

目录 描述 输入描述&#xff1a; 输出描述&#xff1a; 参考代码 描述 请根据题目中给出的双口RAM代码和接口描述&#xff0c;实现异步FIFO&#xff0c;要求FIFO位宽和深度参数化可配置。 电路的接口如下图所示。 双口RAM端口说明&#xff1a; 端口名 I/O 描述 wclk i…

微信小程序简易录音机

首先先创建一个项目&#xff08;想必大家都会啦那就直接开干&#xff09; 首先上html结构 <view class"wx-container"><view id"title">录音机</view><view id"time">{{hours}}:{{minute}}:{{second}}</view>&l…

锐捷统一上网行为管理与审计系统 static_convert.php 前台RCE漏洞复现

0x01 产品简介 锐捷统一上网行为管理与审计RG-UAC系列是星网锐捷网络有限公司自主研发的上网行为管理与审计产品,具备的上网行为日志审计功能,能够全面、准确、细致的审计并记录多种上网行为日志,包括网页、搜索、外发文件、邮件、论坛、IM等等,并对日志数据进行统计分析,…

React的服务器端渲染(SSR)和客户端渲染(CSR)有什么区别?

React的服务器端渲染&#xff08;SSR&#xff09;和客户端渲染&#xff08;CSR&#xff09;是两种不同的页面渲染方式&#xff0c;它们各自有不同的特点和适用场景&#xff1a; 服务器端渲染&#xff08;SSR&#xff09; 页面渲染: 页面在服务器上生成&#xff0c;然后将完整的…

USB2.0网卡安装驱动

有三种安装方式&#xff1a; 驱动精灵驱动总裁USB2.0网卡自带安装程序 前两种很简单&#xff0c;下载驱动精灵或者驱动总裁&#xff0c;然后检测本地硬件&#xff0c;安装相应驱动。 本文重点要介绍的是第三种&#xff0c;利用USB2.0网卡自带的安装程序。有的时候驱动精灵或…

数仓中数据分层的标准流向解读

在大数据开发中&#xff0c;数据分层是一个至关重要的概念。合理的数据分层可以有效地提升数据处理的效率和质量。本文将详细介绍数据分层的标准流向和相关注意事项&#xff0c;并结合实际应用进行说明。 数据分层的标准流向 根据行业标准&#xff0c;数据分层的标准流向如下…

js浅拷贝和深拷贝的区别

JavaScript中的浅拷贝和深拷贝的主要区别在于它们如何处理引用类型的数据。 浅拷贝仅复制对象的引用&#xff0c;而不复制对象本身。这意味着新旧对象共享同一块内存空间。因此&#xff0c;如果修改了原始对象&#xff0c;复制的对象也会相应地改变&#xff0c;因为它们实际上是…

Ant Design Vue Cascader 级联选择 错位问题

当Cascader 多个的时候 对应的下列会错位 如果滚动 他不会跟着元素 而是会跟着屏幕滚动&#xff0c;如下效果 解决方法 在Cascader 标题添加 getPopupContainer 属性监听对应的位置&#xff0c;返回对应的元素 <a-cascader class"smart-width-100 " v-model:…

民宿在线预订系统

摘要 随着国家的发展&#xff0c;人们也更加重视自己的业余时间。在特定的假期或休闲时间外出度假已逐渐成为这个时代的趋势。在我国&#xff0c;随着经济的发展和城市居民物质生活水平的提高&#xff0c;旅游业也呈现出越来越受欢迎的趋势。同时随着人口的快速流动&#xff0…

云计算之CDN

目录 一.什么是CDN&#xff1f; 二.使用CDN的好处&#xff1a; 三.主要特点&#xff1a; 四.关键功能&#xff1a; 一.什么是CDN&#xff1f; 1.CDN的全称是Content Delivery Network&#xff0c;即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和…

R语言——数据与运算

练习基本运算&#xff1a; v <- c(2,4,6,9)t <- c(1,4,7,9)print(v>t)print(v < t)print(v t)print(v!t)print(v>t)print(v<t) v <- c(3,1,TRUE,23i)t <- c(4,1,FALSE,23i)print(v&t)print(v|t)print(!v)v <- c(3,0,TRUE,22i)t <- c(1,3,T…