数据结构:跳表实现(C++)

news/2024/11/7 11:21:08/

在这里插入图片描述

个人主页 : 个人主页
个人专栏 : 《数据结构》 《C语言》《C++》《Linux》《网络》 《redis学习笔记》

文章目录

  • 前言
  • 跳表
    • 跳表的优化思路
    • skiplist,平衡搜索树,哈希表的对比
  • 实现思路
    • SkiplistNode
    • search 搜索
    • add 增加
    • earse 删除
  • 整体代码
  • 总结


前言

本文使用C++实现跳表的增删查。

铁蕾大佬关于跳表的博客


跳表

跳表(SkipList)是一种用于有序数据的高效数据结构,由 William Pugh 在1989年提出。
跳表本质是一个查找结构,通过在原有的有序链表上面增加多级索引来实现快速查找;跳表可以看作是一种可以进行二分查找的有序链表

跳表的优化思路

  • 有序链表的问题:有序链表虽然保持了数据的顺序,但在查找特定元素时,需要从头节点开始顺序遍历,时间复杂度为O(N),其中N为链表中元素数量。这在数据量较大时,会导致查找效率较低

跳表的优化思路
假如我们每相邻两个节点增加一个指针,让这个指针指向下下个节点。
在这里插入图片描述

这样所有新增加的指针连成一个新链表,但它包含的节点个数只有原来的一半。此时,我们再查找特定元素时,需要比较的节点数量大概只有原来的一半。
跳表就是采用多层链表的思路。
在这里插入图片描述

在这里插入图片描述

skiplist,平衡搜索树,哈希表的对比

SkipList(跳表):
查找效率:跳表的平均时间复杂度为O(logN),其中N是节点的数量。这是因为跳表通过多层链表结构,使得查找过程能够跳过部分节点,从而加快查找速度;
特点:跳表是一种概率平衡的数据结构,其实现相对简单,且支持快速的插入,删除和查找操作。同时,跳表在内存中的占用也相对较低

平衡搜索树
查找效率:平衡搜索树的查找时间复杂度通常我O(logN),其中N是节点的数量。这是因为平衡搜索树通过旋转,分裂等操作来保持树的平衡性,从而确保每次查找都能快速定位到目标节点
特定:平衡搜索树具有严格的平衡性要求,使其插入,删除和查找操作都能保持较高的效率。同时,平衡搜索树还支持范围查询等复杂操作。然而,其实现相对复杂,且需要额外的空间来维护树的平衡性

哈希表
查找效率:哈希表的查找效率时间复杂度平均为O(1),但在最坏情况下可能退化为O(N)。因此,哈希表的查找效率取决于哈希函数的优劣和装填因子的设置
特点:哈希表具有极高的查找效率,特别适用于需要频繁查找的场景。同时,哈希表的插入和删除操作也相对简单。然而,哈希表不支持范围查询等复杂操作,且哈希冲突较多时,其性能可能会受到影响。此外,哈希表还需要额外的空间来存储哈希函数和链表等结构

实现思路

SkiplistNode

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

search 搜索

在这里插入图片描述
假如我们要查找 17 这个节点

先定义 cur 指向头节点,从最顶层开始查找;发现 9 小于 17(要查找元素在该元素后面),改变cur指向,使cur 指向 9节点。
在这里插入图片描述
再从9节点开始查找,发现 21 大于 17(要查找元素在该元素前面),去下面一层查找,发现 17 == 17,找到要查找的元素。
在这里插入图片描述

    bool search(int target) {int level = _head._nexts.size() - 1;SkiplistNode* cur = &_head;while (level >= 0) {if (cur->_nexts[level] && cur->_nexts[level]->_val < target) {// 向右走,更新 cur 和 level,target 只能在 cur->_nexts[level]->_val 后面cur = cur->_nexts[level];}else if (!cur->_nexts[level] || cur->_nexts[level]->_val > target) {// 向下走,target 只能在 cur->_nexts[level]->_val 前面level--;}else {// 找到了return true;}}return false;}

add 增加

在这里插入图片描述
假如我们要增加 18 。
那我们需要分三步完成,1.寻找到新增节点要插入的位置。2.构造新节点。3.链接节点;其实与单链表的新增节点相似,只不过寻找到新增节点要插入的位置由一点不同

寻找新增节点要插入的位置
我们需要一个数组(大小为最大层数),来记录新节点前面的节点。此时 cur 指向头结点,从顶层开始查找,发现 6 < 18(新增节点在6的后面),改变cur,使cur指向6
在这里插入图片描述
cur指向6,从顶层查找,发现6指向nullptr,向下走,prevs在第4层记录6(如果新增节点有4层,6可能是新增节点的前一个节点)。发现 25 > 18(新增节点在25之前),向下走,prevs在第三层记录6(如果新增节点有3层,6可能是新增节点的前一个节点)。发现 9 < 18(新增节点在9后面),cur 指向9。
在这里插入图片描述
cur指向9,从第二层查找,发现 17 < 18(新增节点在17之后),cur指向17。 在这里插入图片描述
cur指向17,从第二层查找,发现 25 > 18(新增节点在25前面),向下走,prevs在第二层记录17(如果新增节点有2层,17可能是新增节点的前一个节点)。从第一次查找,发现 19 > 18(新增节点在19前面),向下走,prevs在第一层记录19(如果新增节点有1层,19可能是新增节点的前一个节点)。此时层数 小于 0,查找结束。17的后面就是新增节点的位置。
在这里插入图片描述

在这里插入图片描述

    vector<SkiplistNode*> searchPrevs(int num) {vector<SkiplistNode*> prevs(_maxLevel, &_head);int level = _head._nexts.size() - 1;SkiplistNode* cur = &_head;while (level >= 0) {if (!cur->_nexts[level] || cur->_nexts[level]->_val >= num) {// 要插入节点,一定在cur->_nexts[level]的前面// 向下走,并更新节点prevs[level] = cur;level--;}else if (cur->_nexts[level] && cur->_nexts[level]->_val < num) {// 要插入节点,一定在cur->_nexts[level]的后面// 向右走cur = cur->_nexts[level];}}return prevs;}void add(int num) {// 寻找插入位置,并记录前面的节点vector<SkiplistNode*> prevs = searchPrevs(num);// 构造新节点,随机层数int newlevel = RandomLevel();SkiplistNode* newnode = new SkiplistNode(num, newlevel);// 连接节点newlevel--;while (newlevel >= 0) {newnode->_nexts[newlevel] = prevs[newlevel]->_nexts[newlevel];prevs[newlevel]->_nexts[newlevel] = newnode;newlevel--;}}

earse 删除

在这里插入图片描述
假如我们要删除 19。
我们需要4步完成,1.寻找删除节点,并记录前面节点。2.判断是否存在要删除的节点。3.链接节点。4.删除节点
与新增节点操作类似。

经过第一步寻找删除节点,并记录前面节点后,我们只需判断prevs数组中第0层节点的第0层指向的节点是否等于要删除节点即可。

在这里插入图片描述

    vector<SkiplistNode*> searchPrevs(int num) {vector<SkiplistNode*> prevs(_maxLevel, &_head);int level = _head._nexts.size() - 1;SkiplistNode* cur = &_head;while (level >= 0) {if (!cur->_nexts[level] || cur->_nexts[level]->_val >= num) {// 要插入节点,一定在cur->_nexts[level]的前面// 向下走,并更新节点prevs[level] = cur;level--;}else if (cur->_nexts[level] && cur->_nexts[level]->_val < num) {// 要插入节点,一定在cur->_nexts[level]的后面// 向右走cur = cur->_nexts[level];}}return prevs;}bool erase(int num) {// 寻找删除节点,并记录前面的节点vector<SkiplistNode*> prevs = searchPrevs(num);// 判断是否存在要删除的节点if (!prevs[0]->_nexts[0] || prevs[0]->_nexts[0]->_val != num)return false;// 连接节点SkiplistNode* del = prevs[0]->_nexts[0];int level = del->_nexts.size() - 1;while (level >= 0) {prevs[level]->_nexts[level] = del->_nexts[level];level--;}// 删除节点delete del;return true;}

整体代码

leetcode上设计跳表的题
1206.设计跳表

在这里插入图片描述

struct SkiplistNode {int _val;vector<SkiplistNode*> _nexts;SkiplistNode(int val, int level) :_val(val), _nexts(level, nullptr) {}
};class Skiplist {// 本质是一个有序链表
public:Skiplist() :_head(-1, _maxLevel) {srand((unsigned int)time(nullptr));}bool search(int target) {int level = _head._nexts.size() - 1;SkiplistNode* cur = &_head;while (level >= 0) {if (cur->_nexts[level] && cur->_nexts[level]->_val < target) {// 向右走,更新 cur 和 level,target 只能在 cur->_nexts[level]->_val 后面cur = cur->_nexts[level];}else if (!cur->_nexts[level] || cur->_nexts[level]->_val > target) {// 向下走,target 只能在 cur->_nexts[level]->_val 前面level--;}else {// 找到了return true;}}return false;}void add(int num) {// 寻找插入位置,并记录前面的节点vector<SkiplistNode*> prevs = searchPrevs(num);// 构造新节点,随机层数int newlevel = RandomLevel();SkiplistNode* newnode = new SkiplistNode(num, newlevel);// 连接节点newlevel--;while (newlevel >= 0) {newnode->_nexts[newlevel] = prevs[newlevel]->_nexts[newlevel];prevs[newlevel]->_nexts[newlevel] = newnode;newlevel--;}}bool erase(int num) {// 寻找删除节点,并记录前面的节点vector<SkiplistNode*> prevs = searchPrevs(num);// 判断是否存在要删除的节点if (!prevs[0]->_nexts[0] || prevs[0]->_nexts[0]->_val != num)return false;// 连接节点SkiplistNode* del = prevs[0]->_nexts[0];int level = del->_nexts.size() - 1;while (level >= 0) {prevs[level]->_nexts[level] = del->_nexts[level];level--;}// 删除节点delete del;return true;}private:int RandomLevel() {int level = 1;while (rand() <= RAND_MAX * _p && level < _maxLevel)level++;return level;}vector<SkiplistNode*> searchPrevs(int num) {vector<SkiplistNode*> prevs(_maxLevel, &_head);int level = _head._nexts.size() - 1;SkiplistNode* cur = &_head;while (level >= 0) {if (!cur->_nexts[level] || cur->_nexts[level]->_val >= num) {// 要插入节点,一定在cur->_nexts[level]的前面// 向下走,并更新节点prevs[level] = cur;level--;}else if (cur->_nexts[level] && cur->_nexts[level]->_val < num) {// 要插入节点,一定在cur->_nexts[level]的后面// 向右走cur = cur->_nexts[level];}}return prevs;}private:double _p = 0.5;int _maxLevel = 32;SkiplistNode _head;
};

总结

以上就是跳表的实现

在这里插入图片描述


http://www.ppmy.cn/news/1545041.html

相关文章

8. 数据结构——邻接表、邻接矩阵的基本操作

一、邻接表 1. 内容 2. 实现代码(直接可以复制使用) //邻接表的相关操作 #include<bits/stdc.h> #define MVnum 100 #define OK 1 #define ERROR -1 using namespace std;typedef int Status; typedef char VerTexType; //假设顶点的数据类型为char typedef int ArcT…

Python 三维图表绘制指南

Python 三维图表绘制指南 在数据可视化中&#xff0c;三维图表可以更直观地展示数据之间的关系&#xff0c;尤其是当数据具有多个维度时。Python 提供了多个库来绘制三维图表&#xff0c;其中最常用的就是 Matplotlib。本文将介绍如何使用 Matplotlib 绘制三维图表&#xff0c…

pycharm中的服务是什么?

在PyCharm中&#xff0c;服务是指允许在PyCharm中运行的一种功能或插件。服务可以是内置的&#xff0c;也可以是通过插件安装的。 一些常见的PyCharm服务包括&#xff1a; 调试服务&#xff1a;PyCharm提供了全功能的调试工具&#xff0c;可以帮助开发人员通过设置断点、监视变…

JavaScript中的变量作用域

写在前面 在JavaScript中&#xff0c;变量作用域是指变量在代码中可见的范围。理解变量作用域对于编写高效、可维护的JavaScript代码至关重要。本文将深入探讨JavaScript中的变量作用域&#xff0c;包括全局作用域、函数作用域和块级作用域。 全局作用域 在JavaScript中&…

猫用空气净化器哪个牌子好?求除毛好、噪音小的宠物空气净化器!

换毛季家里孩子不省心&#xff0c;疯狂掉落的猫毛和空气中乱飞的浮毛可把我折磨死了。每天下班都要抽出时间来清理&#xff0c;不然这个家就不能要了。猫毛靠我自己可以打扫&#xff0c;浮毛还得借助宠物空气净化器这种专业工具。所以我最近着手做功课&#xff0c;打算入手一台…

梧桐数据库-使用Python和梧桐数据库进行多维数据分析分享

在数据驱动的商业决策中&#xff0c;多维数据分析&#xff08;MDA&#xff09;是一种强大的工具&#xff0c;它允许我们从多个角度探索数据&#xff0c;揭示潜在的趋势和模式。本文将介绍如何使用Python结合梧桐数据库来执行多维数据分析&#xff0c;并通过一个实际的例子来展示…

dockerfile 和 docker compose

目录 1.dockerfile和docker compose区别 主要区别 目的&#xff1a; 格式&#xff1a; 使用场景&#xff1a; 2.Dockerfile 2.1基本格式 2.2模块解析 2.3例子 3.docker compose 3.1安装 3.2格式 3.3执行 1.dockerfile和docker compose区别 Dockerfile 和…

wifiTrackerlib源码解读

1. 监听wifi相关的Broadcast 1.1 根据布局找到wifi显示用到的方法 首先研究原生carSetting的代码布局---找到wifi_list_fragment.xml&#xff0c;可以知道这里是wifi显示界面的xml然后是找到wifi对应的布局部分: <com.android.car.ui.preference.CarUiPreferenceandroid:…