数据结构与算法之美:跳表

         Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》

欢迎点赞,关注!

目录

1、跳表引入

        1.1、背景

        1.2、跳表的基本原理

2、跳表模拟实现

        2.1、跳表节点

        2.2、跳表框架

        2.3、 查询

        2.4、 查找前置节点

        2.5、添加节点

        2.6、删除节点

3、跳表的性能分析


1、跳表引入

        1.1、背景

        跳表(Skip List)由美国计算机科学家威廉·普(William Pugh)于1989年提出,目的是解决有序链表查询效率低的问题。传统链表查询时间复杂度为O(n),而跳表通过引入多层索引结构,将查询、插入和删除操作的时间复杂度优化至O(log n),同时保持了较高的空间效率。普的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》奠定了跳表在数据结构领域的地位,成为平衡树轻量级替代方案


        1.2、跳表的基本原理

        跳表的核心思想是通过概率性构建多层索引加速查找。每一层都是有序链表,底层包含所有元素,上层逐步稀疏。稀疏与稠密是通过设置概率不同有所差别。每一个跳表的分布都是随机生成的。查找时从最高层开始,若当前节点的下一个节点值小于目标值,则向右移动;否则向下移动到下一层继续搜索。插入和删除操作通过调整相邻节点的指针实现,并动态维护索引层数。


2、跳表模拟实现

        2.1、跳表节点

        _val存储每个节点存储的值,_nextV指向下一个节点。

struct SkiplistNode
{int _val;vector<SkiplistNode*> _nextV;SkiplistNode(int val, int level):_val(val), _nextV(level, nullptr){}
};

        2.2、跳表框架

        对于跳表类的私有成员变量,有三个,_head是跳表的头,这个节点不存储值。_maxlevel存储最高层高,_p是晋升概率,也就是层高加一的概率。后面会详细说。

class Skiplist
{typedef SkiplistNode Node;
public:Skiplist(){srand(time(0));_head = new Node(-1, 1);}private:Node* _head;size_t _maxLevel = 32;double _p = 0.5;
};

        2.3、 查询

        我们先来说查找,一会在介绍怎么添加节点、

        对于查找的过程来说,我们从头结点开始查找,如果下一个节点的值小于我们想要查找的target,我们就直接跳到下一个节点那。如果下一个节点的值大于我们想要查找的taget,我们就得下降层数。如果层数下降到-1(0层是最低层)也没有找到target值,就跳出循环返回false。

        我们拿一个跳表模拟一下查找的过程:

bool search(int target)
{Node* cur = _head;int level = _head->_nextV.size() - 1;while (level >= 0){if (cur->_nextV[level] && cur->_nextV[level]->_val < target){cur = cur->_nextV[level];}//        走到最后一个节点else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val > target){--level;}else{return true;}}return false;
}

        2.4、 查找前置节点

        首先我们先新建一个前置节点,然后把他初始化为level层,其实就是让他的层数和跳表中最高节点的层数相等。我们把这个前置节点的存储的值都初始化为_head。因为如果我们要查找的某个值都比跳表中的所有值都小,那么他的前置节点就是head。

        在循环的过程中,每次调整level我们就记录一下前置节点:

        后序删除和添加节点的时候我们都得先找到前置节点。

vector<Node*> FindPrevNode(int num)
{Node* cur = _head;int level = _head->_nextV.size() - 1;vector<Node*> prevV(level + 1, _head);while (level >= 0){if (cur->_nextV[level] && cur->_nextV[level]->_val < num){cur = cur->_nextV[level];}else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val >= num){prevV[level] = cur;--level;}}return prevV;
}

        2.5、添加节点

         再添加节点的操作中,因为我们要控制每个跳表节点的高度都是随机的,所以说我们得先说一下生成随机层高的函数:

        随机数生成部分:

  generator是静态的默认随机数引擎

       用当前系统时间作为种子(time_since_epoch().count()),确保每次程序运行时种子不同static关键字保证在整个程序运行期间只初始化一次

   distribution:静态的均匀实数分布,范围[0.0, 1.0)用于生成0到1之间的随机浮点数。

        p是晋升概率,也就是我们层数++的概率。

        节点层数至少为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)。

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;
}

        接下来我们看添加节点操作。首先我们找到应该插入位置的前置节点,然后依次设置每一层的节点链接。

void add(int num)
{vector<Node*> prevV = FindPrevNode(num);int n = RandomLevel();Node* newnode = new Node(num, n);if (n > _head->_nextV.size()){_head->_nextV.resize(n, nullptr);prevV.resize(n, _head);}for (size_t i = 0;i < n;i++){newnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode;}
}

        2.6、删除节点

        最后需要注意我们得调整一下头结点的层数。

bool erase(int num)
{vector<Node*> prevV = FindPrevNode(num);if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num){//该节点不存在return false;}else{Node* del = prevV[0]->_nextV[0];for (size_t i = 0;i < del->_nextV.size();i++){prevV[i]->_nextV[i] = del->_nextV[i];}delete del;//如果删除了最高层节点,就把头结点层数降一下int i = _head->_nextV.size() - 1;while (i >= 0){if (_head->_nextV[i] == nullptr)--i;elsebreak;}_head->_nextV.resize(i+1);return true;}
}

 

3、跳表的性能分析

        时间复杂度查询、插入、删除均为O(log n),依赖随机层数分布。

        空间复杂度平均O(n),索引节点数量约为n/2 + n/4 +... ≈ n。

        时间复杂度都是由查询操作卡着,而查询操作其实是从上到下遍历一遍。跳表的高度通常为O(log n),其中n是节点数量。

        优势实现简单,无需复杂平衡操作;适合高并发场景(如Redis的有序集合)。

        跳表作为一种高效动态数据结构,兼具实用性与灵活性,是算法设计中的经典案例。

        好了,今天的内容就分享到这,我们下期再见! 

 

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

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

相关文章

从0设计一个短链接服务:如何实现尽可能短、可变长的短网址系统?

从 0 设计一个短链接服务&#xff1a;如何实现尽可能短、可变长的短网址系统&#xff1f; 在日常生活中&#xff0c;我们经常在短信、微博、广告营销中看到“短链接”&#xff0c;如&#xff1a; https://t.cn/EXaQ4xY https://bit.ly/3Yp9zJk相比冗长复杂的原始 URL&#xff0…

Microsoft Word 中 .doc 和 .docx 的区别

Microsoft Word 中 .doc 和 .docx 的区别 解释 Microsoft Word 中 .doc 和 .docx 文件格式的区别。这些格式都是 Word 处理文档的标准&#xff0c;但它们在结构、兼容性和功能上存在显著差异。下面我将详细说明。 1. 基本定义 .doc&#xff1a;这是 Microsoft Word 的旧格式&am…

Springboot aop面向切面编程

aop:面向切面编程&#xff0c;理解在一个流程中插入一个切面&#xff0c;这样切面方法会在指定位置执行能无影响的在某些方法前或者后插入一些动作springboot使用1.引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>sprin…

手机识别数据集,2628张原始图片,支持yolo,coco json,pasical voc xml等格式的标注

本文提供手机识别数据集&#xff0c;2628张原始图片&#xff0c;支持yolo&#xff0c;coco json,pasical voc xml等格式的标注的数据集下载&#xff0c;下载地址在文末手机识别数据集简介手机识别数据集通常用于训练和评估机器学习模型&#xff0c;以识别不同手机品牌、型号或功…

ollama - sqlcoder模型:面向提示词编程(根据用户信息生成sql语句并执行返回结果)

https://ollama.ac.cn/library/sqlcoderhttps://blog.csdn.net/hzether/article/details/143816042import ollama import sqlite3 import json from contextlib import closingdef generate_and_execute_sql(question: str, db_path: str) -> dict:# 1. 生成 SQL 查询语句pr…

C语言,结构体指针案例

案例一&#xff1a; #include <stdio.h> #include <stdbool.h> #include <string.h> // 添加string.h头文件用于strcpy //结构体指针//方式 1 : 先定义结构体 struct Dog {char *name;int age;char weight; };//方式 1 : char *get_dog_info(struct Dog do…

Vue 3 中父子组件双向绑定的 4 种方式

&#x1f501; Vue 3 中父子组件双向绑定的 4 种方式 整理不易&#xff0c;点赞 收藏 关注&#xff0c;助你组件通信不再混乱&#xff01;✅ 场景说明 父组件希望将某个值传递给子组件&#xff0c;同时希望子组件能够修改这个值&#xff08;实现“绑定 反向更新”&#xff0…

阻有形,容无声——STA 签核之RC Corner

RC corner&#xff0c;RC指的是gate跟network的寄生参数&#xff0c;寄生参数抽取工具&#xff08;比如Starrc&#xff09;根据电路的物理信息&#xff0c;抽取出电路的电阻电容值&#xff0c;再以寄生参数文件&#xff08;Spef&#xff09;输入给STA工具&#xff08;PT&#x…

多代理系统(multi-agent)框架深度解析:架构、特性与未来

在人工智能技术迭代的浪潮中&#xff0c;多代理系统&#xff08;Multi-Agent System&#xff09;正从实验室走向产业应用的核心舞台。这一技术范式的崛起源于三大驱动力&#xff1a;大模型能力的指数级提升、复杂任务分解的需求爆发&#xff0c;以及传统单体智能架构的局限性日…

【Redis】黑马点评笔记:使用redis解决各种分布式/并发问题

1、系统架构2、基于session登录用户的 session 是由服务器&#xff08;如 Tomcat&#xff09;自动管理和维护的&#xff0c;每个用户在访问 Web 应用时都会拥有一个独立的 session 对象。这个对象是通过浏览器和服务器之间的 HTTP 协议自动绑定的。1. 如何区分不同用户的 Sessi…

Javaweb- 11 MVC架构模式

MVC&#xff08;Model View Controller&#xff09; 是软件工程中一种软件架构模式&#xff0c;它把软件系统分为模型&#xff0c;视图&#xff0c;控制器&#xff0c;三个基本部分。用一种业务逻辑&#xff0c;数据&#xff0c;界面显示分离的方法组织代码&#xff0c;将业务逻…

【电脑】主板的基础知识

主板&#xff08;Motherboard&#xff09;是计算机的核心组件之一&#xff0c;它将所有其他硬件部件连接在一起并协调它们的工作。以下是关于主板的详细知识&#xff1a;1. 架构组成一个典型的主板通常由以下几个主要部分构成&#xff1a;芯片组&#xff08;Chipset&#xff09…

【飞算JavaAI】一站式智能开发,驱动Java开发全流程革新

【作者主页】Francek Chen 【专栏介绍】⌈⌈⌈人工智能与大模型应用⌋⌋⌋ 人工智能&#xff08;AI&#xff09;通过算法模拟人类智能&#xff0c;利用机器学习、深度学习等技术驱动医疗、金融等领域的智能化。大模型是千亿参数的深度神经网络&#xff08;如ChatGPT&#xff09…

STM32中的RTC(实时时钟)详解

前言&#xff1a;为什么需要RTC&#xff1f; 在嵌入式系统中&#xff0c;时间记录是一项基础且关键的功能。想象一下&#xff1a;智能家居设备需要按时间触发开关灯&#xff0c;工业仪表需要记录传感器数据的采集时刻&#xff0c;物联网终端需要同步服务器时间戳……这些场景都…

Python技巧记录

空格拼接数组格式化显示 一维数组 arr [1, 2, 3, 4, 5] print( .join(map(str, arr))) # 直接转换并连接二维数组 for row in arr:print( .join(map(str, row)))for row in arr: 此循环会遍历矩阵arr中的每一行。这里的arr是一个二维列表&#xff0c;每一行代表一个子列表。m…

next.js打包后的前端资源如何进行部署和访问,为什么没有index.html

在 Next.js 项目中&#xff0c;打包后的部署方式和传统单页应用&#xff08;SPA&#xff09;有所不同&#xff0c;尤其是没有直接生成 index.html 这一点。以下是详细解释和部署指南&#xff1a;为什么没有 index.html 文件&#xff1f; Next.js 采用 混合渲染策略&#xff0c;…

Qt+FFmpeg网络视频流播放

init 函数用于初始化 FFmpeg&#xff0c;包括设置参数、打开输入、初始化视频和音频等。initOption 函数用于设置 FFmpeg 的参数选项。bool FFmpegThread::init() {if (url.isEmpty()) {return false;}//判断该摄像机是否能联通if (checkConn && isRtsp) {if (!checkUr…

【SpringBoot】Spring Boot 高并发优化终极指南,涵盖线程模型、JVM 调优、数据库访问、缓存策略等 15+ 核心模块

Spring Boot 高并发优化终极指南&#xff0c;涵盖线程模型、JVM 调优、数据库访问、缓存策略等 15 核心模块一、线程模型深度调优&#xff08;核心瓶颈突破&#xff09;1. Tomcat 线程池原子级配置2. 异步任务线程池隔离策略二、JVM 层终极调参&#xff08;G1GC 深度优化&#…

linux(CentOS-7-x86_64:NAT模式下解决yum无法使用:更新yum源的详细操作步骤2025)

目录 一、CentOS-7-x86_64的NAT模式下解决yum无法使用。&#xff08;更新可用的yum&#xff09; &#xff08;1&#xff09;首先保证能够ping通&#xff0c;也就是NAT模式下虚拟机有网络。 &#xff08;2&#xff09;错误&#xff1a;无法使用yum。比如我现在无法yum search if…

C++11的整理笔记

Lambda 表达式Lambda 表达式是 C11 引入的一种强大的功能&#xff0c;它允许你在代码中直接定义匿名函数对象。Lambda 表达式可以捕获上下文中的变量&#xff0c;并在需要时使用它们。它们通常用于简化代码&#xff0c;尤其是那些需要传递函数对象作为参数的场景&#xff08;如…