当前位置: 首页 > news >正文

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

         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.dtcms.com/a/276819.html

相关文章:

  • 【CMake】CMake构建项目入门
  • 数据标注:AI时代的黄金矿场如何规避法律暗礁
  • C语言习题三
  • HCIA--- OSPF动态路由实验
  • 基于springboot+Vue的二手物品交易的设计与实现(免费分享)
  • 《棒垒球知识科普》技巧赛规则·棒球1号位
  • houdini 用 vellum 制作一个最简单的布料
  • Windows环境下JS计时器精度差异揭秘
  • PyQt5布局管理:QHBoxLayout和QVBoxLayout详解
  • cmd命令之for循环
  • 深入理解-ConcurrentHashMap:JDK-1-7-与-1-8-的演进与实现原理
  • 管理端口: 一个简单的锤子架子
  • JavaSE常用类
  • 《Spring 中上下文传递的那些事儿》Part 11:上下文传递最佳实践总结与架构演进方向
  • Linux反弹shell的几种方式
  • 【leetcode】709. 转换成小写字母
  • 直播录屏技术揭秘:以抖音直播录屏为例
  • 【嘉立创】四层板设计
  • 如何搭建一个高质量的开放接口平台
  • 数据结构与算法之美:线索二叉树
  • 【Scratch】从入门到放弃(四):指令大全-九大类之事件、控制、侦测
  • 解释全连接层的“参数数量”和“计算过程”,保证像看动画片一样直观~
  • c++反射实现
  • # 打开【设备和打印机】菜单时显示成新式【打印机和扫描仪】菜单,怎么才能显示传统带打印机图标菜单?
  • batchnorm类
  • 【DIY小记】逸剑风云决烟尘回响+武家旧事+碧海仙踪DLC攻略整合
  • 哈希扩展 --- 位图
  • 专业硬件检测工具 AIDA64 Extreme V7.70.7500 至尊版
  • Sentry 集成
  • 基于51单片机的超声波智能避障小车仿真