【数据结构】跳表Skiplist

news2024/11/19 15:11:37

文章目录

  • 跳表--skiplist
    • skiplist的概念
    • skilplist的原理
    • skilplist的实现
      • 随机值函数
      • 跳表节点
    • 跳表框架
      • 查找函数
      • 寻找前置节点
      • 添加元素
      • 删除元素
      • 打印链表
    • 测试结果
    • Skiplist与其他Key-Value结构的比较

跳表–skiplist

skiplist的概念

skiplist本质上也是一种查找结构,用于解决算法中的查找问题。skiplist,顾名思义,首先它是一个list。它是在有序链表的基础上发展起来的。有序链表的的查找数据的时间复杂度是O(n);

skilplist相对于list做出了优化,类似于二分查找,查找的时间复杂度是log(n);

  • 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。
  • 以此类推,在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一
    个指针,从而产生第三层链表。
  • 这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)。但是在插入或者删除一个节点时,就打乱了上下的层次关系。**为了维持2:1的对应关系,需要调整整个链表,时间复杂度就又变成了O(n)。 **

image-20221210230152671

在设计跳表时,设计师对Skiplist作出了大胆的优化。不再严格要求对于的比例关系,而是插入一个节点时,随机赋予一个层数。当然这里的层数是在一个范围内的随机值

image-20221210230214786

skilplist的原理

层数的随机值

  • 最大层数maxLevel
  • p:增加一层的概率

image-20221210235753818

在Redis中的skiplist,maxlevel取值为32,p为1/4

数学原理

P表示新增加一层的概率,节点层数至少为1。而大于1的节点层数,满足一个概率分布。

  • 节点层数恰好等于1的概率为1-p。
  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
  • 节点层数大于等于3的概率为p2,而节点层数恰好等于3的概率为p2*(1-p) 。
  • 节点层数大于等于4的概率为p3,而节点层数恰好等于4的概率为p3*(1-p) 。

通过错位相减,可以得到平均的层数:

image-20221211000452257

skilplist的实现

跳表OJ:

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

随机值函数

因为插入一个节点时,层数是随机的,所以需要设计一个随机值函数。

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;
    // _p是增加一层的概率,_maxLevel是最大的层数
	while (distribution(generator) <= _p && level < _maxLevel)
	{
		++level;
	}
	return level;
}

C语言的写法

int RandomLevel()
{
	size_t level = 1;
	// RAND_MAX是一个宏,即最大的随机值
	while (rand() < RADN_MAX *_p && level < _maxLevel)
	{
		level++;
	}
	return level;
}

跳表节点

template<class K,class V>
struct skiplistnode{
    //存储的键值对
    K _key;
    V _value;
    vector<skiplistnode*> _nextV;
    //创建一个节点
    skiplistnode(K key,V value,int level)
    :_key(key),_value(value),_nextV(level,nullptr)
    {}
};

跳表框架

跳表的主要功能有增加元素,删除元素,操作元素等。跳表是一个Key-Value的查找结构,所以需要实现一个模板。

template <class K, class V>
class skiplist
{
    typedef skiplistnode<K, V> Node;
public:
    // 构造函数
    skiplist()
    {
        srand(time(0));
        _head = new Node(K(), V(), 1);
    }
    
    // 查找函数
    pair<V, bool> search(K target)
    {}
    
	//查找前置结点,返回一个vector,包含target每一层的前置节点
    vector<Node *> Findprev(K target)
    {}
    
    
	//添加元素函数
    void add(K key, V value)
    {}
    
	//删除函数
    bool erase(K target)
    {}
    
    
	//打印链表,每一层为一个链表
    void print()
    {}
    
	//随机值生成函数
    int RandomLevel()
    {}
private:
    double _p = 0.25;
    int _maxLevel = 32;
    Node *_head;
};

查找函数

查找关键字Key,返回对应的值。

// 查找函数
pair<V, bool> search(K target)
{
    Node *cur = _head;
    int level = _head->_nextV.size() - 1;
    while (level >= 0)
    {
        if (cur->_nextV[level] && target > cur->_nextV[level]->_key)
        {
            cur = cur->_nextV[level];
        }
        else if (cur->_nextV[level] == nullptr || target < cur->_nextV[level]->_key)
        {
            level--;
        }
        else
        {
            return make_pair(cur->_nextV[level]->_value, true);
        }
    }
    return make_pair(V(), false);
}

寻找前置节点

查找前置结点,返回一个vector,包含关键字target每一层的前置节点指针。

vector<Node *> Findprev(K target)
{
    int level = _head->_nextV.size() - 1;
    vector<Node *> prevnode(level + 1, _head);
    // 寻找前置节点
    Node *cur = _head;
    while (level >= 0)
    {
        if (cur->_nextV[level] && target > cur->_nextV[level]->_key)
        {
            cur = cur->_nextV[level];
        }
        else if (cur->_nextV[level] == nullptr || target <= cur->_nextV[level]->_key)
        {
            prevnode[level] = cur;
            level--;
        }
    }
    return prevnode;
}

添加元素

向链表这插入一个键值对

void add(K key, V value)
{
    // 先获取插入点的层数
    int newlevel = RandomLevel();
    Node *newnode = new Node(key, value, newlevel);
    // 创建一个数组用于获取每一层的前置结点
    vector<Node *> prev = Findprev(key);
    // 如果层数比哨兵位节点还要大,则需要调整大小哨兵位节点的层数
    if (newlevel > _head->_nextV.size())
    {
        _head->_nextV.resize(newlevel, nullptr);
        prev.resize(newlevel, _head);
    }

    // 连接结点
    for (size_t i = 0; i < newlevel; i++)
    {
        newnode->_nextV[i] = prev[i]->_nextV[i];
        prev[i]->_nextV[i] = newnode;
    }
}

删除元素

输入一个关键字Key,删除链表中第一个Key元素

bool erase(K target)
{
    vector<Node *> prev = Findprev(target);
    // 判断是否有当前的值,如果没有返回false
    if (prev[0]->_nextV[0] == nullptr || prev[0]->_nextV[0]->_value != target)
    {
        return false;
    }
    else
    {
        // 删除结点
        Node *del = prev[0]->_nextV[0];
        int level = del->_nextV.size();
        for (size_t i = 0; i < level; i++){
            prev[i]->_nextV[i] = del->_nextV[i];
        }
        delete del;
        // 判断是否要调整哨兵位的层数
        int n = _head->_nextV.size() - 1;
        while (n >= 0)
        {
            if (_head->_nextV[n]){
                break;
            }
            else{
                n--;
            }
        }
        _head->_nextV.resize(n + 1);
    }
    return true;
}

打印链表

打印链表,每一层为一个链表。

void print()
{
    int level = _head->_nextV.size() - 1;
    while (level--)
    {
        Node *cur = _head;
        while (cur)
        {
            if (cur->_nextV[level] != nullptr){
                cout << cur->_nextV[level]->_value << " ";
            }
            cur = cur->_nextV[level];
        }
        cout << endl;
    }
}

测试结果

#include"Skilplist.hpp"
#include<iostream>

int main(){
    skiplist<int,int> sklist;
    sklist.add(3,3);
    sklist.add(6,6);
    sklist.add(9,9);
    sklist.add(11,11);
    sklist.add(7,7);
    sklist.add(5,5);
    sklist.add(11,11);
    sklist.add(13,13);
    sklist.add(1,1);
    sklist.add(2,2);
    sklist.add(15,15);
    cout<<"del before.........."<<endl;
    sklist.print();
    sklist.erase(7);
    sklist.erase(8);
    sklist.erase(11);
    sklist.erase(13);
    cout<<"del after..........."<<endl;
    sklist.print();
    auto it1=sklist.search(9);
    cout<<"search 9:  "<<it1.first<<"; bool: "<<it1.second<<endl;
    auto it2=sklist.search(13);
    cout<<"search 13:  "<<it2.first<<"; bool: "<<it2.second<<endl;
    return 0;
}

image-20221212113307323

Skiplist与其他Key-Value结构的比较

  1. skiplist相比平衡搜索树(AVL树和红黑树)对比,都可以做到遍历数据有序,时间复杂度相差不大。skiplist的优势是:
  • skiplist实现更简单,容易控制。平衡树增删查改遍历都更复杂。
  • skiplist的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗。
  1. skiplist相比哈希表而言 ,优势不大。
  • 哈希表平均时间复杂度是O(1),而跳表的时间复杂度是O(log n)
  • 哈希表空间复杂度更高
  • 相当于哈希表,跳表遍历数据有序
  • 哈希表扩容有性能损耗
  • 哈希表再极端场景下哈希冲突高,效率下降厉害,需要挂载红黑树

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

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

相关文章

产品待办列表梳理(PBR)是什么?

产品待办列表(PBL)是Scrum框架下最重要的一个工件(Artifact)&#xff0c;产品待办列表的梳理(Product backlog Refinement-PBR)也是一个重要的活动&#xff0c;它不同于Scrum的3-3-5-5。仔细阅读Scrum指南&#xff0c;对产品待办列表梳理活动的描述是有限的&#xff1a; “只有…

R语言基于树的方法:决策树,随机森林,Bagging,增强树

概观 本文是有关 基于树的 回归和分类方法的。最近我们被客户要求撰写关于决策树的研究报告&#xff0c;包括一些图形和统计输出。 视频&#xff1a;从决策树到随机森林&#xff1a;R语言信用卡违约分析信贷数据实例 从决策树到随机森林&#xff1a;R语言信用卡违约分析信贷…

『微信小程序从0到1』视图与逻辑

&#x1f431;‍&#x1f409;&#x1f431;‍&#x1f409;&#x1f431;‍&#x1f409; 请乘理想之马&#xff0c;挥鞭从此起程&#xff0c;路上春色正好&#xff0c;天上太阳正晴&#x1f43e;&#x1f43e;&#x1f43e;/font> &#x1f308;博客主页&#x1f449;白小…

【SQL】主从复制

主从复制主从复制的作用主从复制的原理一主一从架构主从配置文件1.主机配置2.从机配置3.主机建立账户并授权4.从机&#xff1a;配置需要复制的主机5.测试6.停止主从同步binlog_format三种格式双主双从架构如何提升数据库并发能力&#xff1a;在实际工作中&#xff0c;我们常常将…

B站运营,B站游戏数据如何分析?

B站聚集了大部分年轻人&#xff0c;用户量和黏性一直持续增长&#xff0c;在B站&#xff0c;游戏也是重点发展的一个分区&#xff0c;而对于运营者想做好游戏运营也并非易事&#xff0c;想做好运营掌握游戏数据必不可少&#xff0c;那么应该如何查看游戏数据呢&#xff1f; 一…

童年 高尔基

每周读一本经典&#xff0c;大概从2022年11月开始了&#xff0c;目前已经读了4本&#xff0c;csdn平台更适合技术分享&#xff0c;知乎比较适合书籍分享 《童年》是高尔基三部曲中的第一部。坐在柳东图书馆像海水浸过的沙子般座椅&#xff0c;周末的下午沉浸在高尔基先生奇特的…

线程池应用(四)

线程池应用线程池线程池应用多线程应用同步和异步1. 需要等待结果1. join 实现&#xff08;同步&#xff09;2. Future 实现&#xff08;同步&#xff09;3.CompletableFuture 实现&#xff08;异步&#xff09;4. BlockingQueue 实现&#xff08;异步&#xff09;2. 不需等待结…

5分钟搞定,实现 定时任务 的五种方案!

我们在实际开发中&#xff0c;多多少少都会用到定时任务来处理一些问题。 比如金融项目中的对账&#xff0c;每天定时对昨天的账务进行核对&#xff0c;每个月初对上个月的账务进行核对等。 还比如&#xff0c;我们需要处理一些老数据迁移&#xff0c;修复一些新项目和老项目…

基于java+springboot+mybatis+vue+mysql的财务管理系统

项目介绍 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时…

CI24R1/SI24R1 2.4G无线传输技术--无线门铃

无线门铃是一种基于2.4G无线传输技术的智能家居产品&#xff0c;主要用于亲朋好友探访的语音提醒功能。此次方案采用2.4G无线通信设计&#xff0c;室内和室外子母机组网&#xff0c;在智能家居及办公场所等方面得到广泛的应用。 一、应用场景 适用于居民社区、家庭、公寓、酒店…

2-2-3-5-5、Disruptor详解

目录简介juc包下的队列存在的问题设计方案RingBuffer数据结构数据存取方案常用等待策略写数据流程单线程&#xff08;一个生产者&#xff09;多线程&#xff08;多个生产者&#xff09;多个消费者读数据多个生产者写数据核心概念使用构造器引入依赖单生产者单消费者模式创建Eve…

微信公众号开发——接收用户消息(图文、语言、上报位置、关注、取消关注)及自动回复

&#x1f60a; 作者&#xff1a; 一恍过去&#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390&#x1f38a; 社区&#xff1a; Java技术栈交流&#x1f389; 主题&#xff1a; 微信公众号开发——接收用户消息(图文、语言、上报位置、关注、取消关注)及…

推动MRO工业品数字化基建升级,数商云采购系统赋能企业采购数字化管理

MRO工业品是工业生产中的重要组成部分&#xff0c;经历了十余年的发展成长&#xff0c;市场规模持续增长&#xff0c;然而据数据显示&#xff0c;MRO工业品市场的线上渗透率仍停留在个位数&#xff0c;这意味着MRO工业品数字化采购仍有巨大的发展空间。 MRO工业品行业发展受困…

非零基础自学Golang 第1章 走进Go 1.1 Go编程语言概述 1.1.1 Go 的历史

非零基础自学Golang 文章目录非零基础自学Golang第1章 走进Go1.1 Go编程语言概述1.1.1 Go 的历史第1章 走进Go 1.1 Go编程语言概述 Go语言也叫Golang&#xff0c;是由谷歌&#xff08;Google&#xff09;公司在2007年推出的一款静态编译型语言。Go语言高效、简洁、容易上手&a…

上海诺基亚贝尔-S-010W-AV2B卡刷固件包

上海诺基亚贝尔-S-010W-AV2B卡刷固件包 固件特点&#xff1a; 1、修改dns&#xff0c;三网通用&#xff1b; 2、开放原厂固件屏蔽的市场安装和u盘安装apk&#xff1b; 3、无开机广告&#xff0c;无系统更新&#xff0c;不在被强制升级&#xff1b; 4、大量精简内置的没用的…

最新版Crack:MailBee.NET 2022最后版

MailBee.NET Items Package 包括 SMTP、POP3、IMAP、EWS、Security、Antispam、Outlook Converter、Address Validator、PDF 部件&#xff0c;以及作为免费功能的 BounceMail、HTML、MIME、ICalVCard 部件。MailBee.NET Objects是一组功能强大且功能丰富的 .NET 元素&#xff0…

基于nodejs如何爬取csdn上自己的文章

当你想把自己在csdn上写的文章转为hexo上可以发布的文章或是将文章写入自己的数据库时,可以用到 将所有博客数据写入数据库 获取你的文章的分页接口: 在浏览自己的所有文章时,我们不难发现,文章的数据是往下滑动到底时,才会刷新出新的数据, 那么此时肯定是发送了一个请求来获…

一篇文章教你实战Docker容器数据卷

在上一篇中&#xff0c;咱们对Docker中的容器数据卷做了介绍。已经知道了容器数据卷是什么&#xff1f;能干什么用。那么本篇咱们就来实战容器数据卷&#xff0c;Docker容器数据卷案例主要做以下三个案例 1&#xff1a;宿主机(也就是Docker所安装的机器)与容器之间的映射-让Do…

LeetCode 538. 把二叉搜索树转换为累加树(C++)

标签&#xff1a;二叉树搜索 深度优先遍历 二叉树 思路一&#xff1a;递归实现反向中序遍历&#xff0c;并累加递归过程中的根的值 思路二&#xff1a;使用迭代&#xff0c;给每个根节点添加一个反向中序遍历的前驱节点。 原题链接&#xff1a;https://leetcode.cn/problems/co…

数据分析业务场景 | CTR预估

一.概况 定义 是推荐中最核心的算法之一 对每次广告的点击情况做出预测&#xff0c;预测用户是点击还是不点击 就是预测点击与否的而分类算法&#xff0c;成功的关键之一就是样本的准确性 对于正样本&#xff0c;一般可发挥的空间不是很大&#xff0c;最多就是卡一个停留时…