跳表-skiplist的简单实现

article/2025/8/8 0:29:04

文章目录

  • 1、什么是跳表-skiplist
  • 2、skiplist的效率如何保证?
  • 3、skiplist的实现
  • 4、skiplist跟平衡搜索树和哈希表的对比

1、什么是跳表-skiplist

skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。那么相比而言它的优势是什么的呢?这么等我们学习完它的细节实现,我们再来对比。

skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: AProbabilistic Alternative to Balanced Trees》。对细节感兴趣的同学可以下载论文原文来阅读。

skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。如果是一个有序的链表,查找数据的时间复杂度是O(N)。

William Pugh开始的优化思路:

  1. 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图b所示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。
  2. 以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。
  3. skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到 O ( l o g 2 n ) O(log_2n) O(log2n)。但是这个结构在插入删除数据的时候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。

在这里插入图片描述

  1. skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数,这样就好处理多了。细节过程入下图:
    在这里插入图片描述

模拟查找过程:
例如需要查找17

17比6大,向右走,跳跃到6
6的下一个结点是nullptr,向下走
6的下一个结点是25,比17大,向下走
6的下一个结点是9,向右走,跳跃到9
9的下一个结点是17,正是要查找的值,跳跃到17


2、skiplist的效率如何保证?

上面我们说到,skiplist插入一个节点时随机出一个层数,听起来怎么这么随意,如何保证搜索时的效率呢?
这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:
在这里插入图片描述
在Redis的skiplist实现中,这两个参数的取值为:

p = 1/4
maxLevel = 32

根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析
如下:

  • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。
  • 节点层数恰好等于1的概率为1-p。
  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
  • 节点层数大于等于3的概率为p2,而节点层数恰好等于3的概率为p2 *(1-p)。
  • 节点层数大于等于4的概率为p3,而节点层数恰好等于4的概率为p3 *(1-p)。

因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:
在这里插入图片描述
现在很容易计算出:

当p=1/2时,每个节点所包含的平均指针数目为2;
当p=1/4时,每个节点所包含的平均指针数目为1.33。

跳表的平均时间复杂度为O(logN),这个推导的过程较为复杂,需要有一定的数学功底,有兴趣的
老铁,可以参考以下文章中的讲解

铁蕾大佬的博客:http://zhangtielei.com/posts/blog-redis-skiplist.html
William_Pugh大佬的论文:ftp://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf


3、skiplist的实现

#define _CRT_SECURE_NO_WARNINGS 1#include <iostream>
#include <vector>
#include <time.h>
#include <random>
#include <chrono>
using namespace std;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(0));// 头节点,层数是1_head = new SkiplistNode(-1, 1);}bool search(int target) {Node* cur = _head;//从最高层开始查找int level = _head->_nextV.size() - 1;//到第0层如果还找不到数据,就说明不存在while (level >= 0){// 下一个结点不为nullptr并且目标值比下一个节点值要大,向右走if (cur->_nextV[level] != nullptr && 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;}//获取插入结点的每一层的前一层结点指针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){// 更新level层前一个prevV[level] = cur;// 向下走--level;}}return prevV;}void add(int num) {vector<Node*> prevV = FindPrevNode(num);int n = RandomLevel();Node* newnode = new Node(num, n);// 如果n超过当前最大的层数,那就升高一下_head的层数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;}}bool erase(int num) {vector<Node*> prevV = FindPrevNode(num);// 第一层下一个不是val,val不在表中if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num){return false;}else{Node* del = prevV[0]->_nextV[0];// del节点每一层的前后指针链接起来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;}}//C语言产生随机数//int RandomLevel()//{//	size_t level = 1;//	// rand() ->[0, RAND_MAX]之间//	while (rand() <= RAND_MAX*_p && level < _maxLevel)//	{//		++level;//	}//	return level;//}//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;//出现一层的概率是1/2,出现两层的概率是1/4,出现三层的概率是1/8,以此类推	double _p = 0.5;
};

测试层数是否跟前面的概率分布大致相同:

int main()
{vector<int> v(32, 0);Skiplist sl;//int max = 0;//测试1千万次for (size_t i = 0; i < 100000000; ++i){v[sl.RandomLevel() - 1]++;for (int i = 0; i < v.size(); ++i){cout << "第" << i + 1 << "层: " << v[i] << endl;}cout << endl;return 0;
}

在这里插入图片描述
从测试结果来看,每一层和下一层的比例大概是2:1,和我们给出的概率是相同的。


4、skiplist跟平衡搜索树和哈希表的对比

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

http://chatgpt.dhexx.cn/article/3HmBsQq4.shtml

相关文章

跳表:Skiplist原理介绍和优缺点

skiplist介绍 不要求上下相邻两层链表之间的节点个数有严格的对应关系&#xff0c;而是为每个节点随机出一个层数(level)。比如&#xff0c;一个节点随机出的层数是3&#xff0c;那么就把它链入到第1层到第3层这三层链表中。为了表达清楚&#xff0c;下图展示了如何通过一步步的…

skiplist 跳跃表详解及其编程实现

skiplist介绍 跳表(skip List)是一种随机化的数据结构&#xff0c;基于并联的链表&#xff0c;实现简单&#xff0c;插入、删除、查找的复杂度均为O(logN)。跳表的具体定义&#xff0c; 请参考参考维基百科 点我&#xff0c;中文版。跳表是由William Pugh发明的&#xff0c;这…

浅析SkipList跳跃表原理及代码实现

转载请注明&#xff1a;http://blog.csdn.net/ict2014/article/details/17394259 SkipList在leveldb以及lucence中都广为使用&#xff0c;是比较高效的数据结构。由于它的代码以及原理实现的简单性&#xff0c;更为人们所接受。我们首先看看SkipList的定义&#xff0c;为什么叫…

跳表(skiplist)的理解

听到跳表(skiplist)这个名字,既然是list,那么应该跟链表有关。 跳表是有序链表,但是我们知道,即使对于排过序的链表,我们对于查找还是需要进行通过链表的指针进行遍历的,时间复杂度很高依然是O(n),这个显然是不能接受的。是否可以像数组那样,通过二分法进行查找呢,但…

SkipList详解

本文参考&#xff1a;《大数据日知录》 概念 SkipList是一种用来代替平衡树的数据结构。 虽然在最坏的情况下SkipList的效率要低于平衡树&#xff0c;但是大多数情况下效率仍然非常高&#xff0c;其插入、删除、查找的时间复杂度都是O(log(N))。 除了高效外&#xff0c;其实现…

跳表(skipList)

一、为何有skipList这种数据结构的出现 我们知道二分查找算法之所以能达到 O(logn) 这样高效的一个重要原因在于它所依赖的数据结构是数组&#xff0c;数组支持随机访问一个元素&#xff0c;通过下标很容易定位到中间元素。而链表是不支持随机访问的&#xff0c;只能从头到尾依…

跳表 skiplist 简介

跳表 skiplist 跳表 (Skip List) 是由 William Pugh 在 1990 年发表的文章 Skip Lists: A Probabilistic Alternative toBalanced Trees 中描述的一种查找数据结构&#xff0c;支持对数据的快速查找&#xff0c;插入和删除。 对于 AVL 树、红黑树等平衡树&#xff0c;在插入过…

SkipList(跳跃表)详解

Introduction: skiplist本质上也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff08;Searching&#xff09;&#xff0c;即根据给定的key&#xff0c;快速查到它所在的位置&#xff08;或者对应的value&#xff09; 一般用于解决查找问题的数据结构分为两个大类…

docker中国镜像

一,如果是像redis,mysql等官方的镜像,直接配置阿里云的镜像就可以 1,注册阿里云账号,并登录 2,打开这个网址 https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors 3,页面上会教怎么设置,如下面的截图 二,如果是私有仓库的镜像 非常感谢原作者:https://www.jiansh…

Docker 使用国内镜像仓库

Docker 使用国内镜像仓库 1、问题描述2、总结 1、问题描述 由于某些原因&#xff0c;导致Docker镜像在国内下载速度特别慢。所以为了沉浸式开发。最好切换为国内源。这里以163 的镜像仓库举例。首先修改/etc/docker/daemon.json配置文件。 sudo vi /etc/docker/daemon.json…

MacOS上配置docker国内镜像仓库地址

背景 docker官方镜像仓库网速较差&#xff0c;我们需要设置国内镜像服务 我的MacOS docker版本如下 设置docker国内镜像仓库地址 点击Settings点击Docker Engine修改配置文件&#xff0c;添加registry-mirrors {"builder": {"gc": {"defaultKeepS…

Docker国内镜像加速地址与详细说明

简介 对于动不动就几百M甚至上G的Docker镜像来说&#xff0c;官方镜像总是掉线或速度极慢&#xff0c;为了改善这种情况&#xff0c;建议切换成国内镜像。常用的国内镜像使用阿里云、网易的居多&#xff0c;本篇内容将记录一下Docker的这些国内镜像是怎么使用的。 国内常用的…

Docker国内镜像

1、下面这些镜像实测基本都用不了。 网易镜像中心&#xff1a;http://hub-mirror.c.163.com daocloud镜像市场&#xff1a;https://hub.daocloud.io 七牛云&#xff1a;https://reg-mirror.qiniu.com Azure&#xff1a;https://dockerhub.azk8s.cn 中科大: https://docke…

docker 国内镜像配置

一、常用镜像地址 1、中国科技大学:https://docker.mirrors.ustc.edu.cn 2、阿里云:https://cr.console.aliyun.com/ 3、Docker中国区官方镜像:https://registry.docker-cn.com 4、网易:http://hub-mirror.c.163.com 5、ustc:https://docker.mirrors.ustc.edu.cn 6、daoclo…

window docker国内镜像设置

刚开始使用的时候&#xff0c;发现因为网络的问题&#xff0c;经常出现镜像下载失败的情况。如下图所示。 这是应为docker服务在国外,直接访问会因为网络原因失败或者特别慢,因此我们可以将将镜像源设置为国内的。 一. 在桌面右下角出有一个docker的图标,鼠标右键点击setings,…

【Docker】Docker 设置国内镜像源_docker国内镜像库

文章目录 1. Docker阿里云镜像加速2. 参考资料 点击跳转&#xff1a;Docker安装MySQL、Redis、RabbitMQ、Elasticsearch、Nacos等常见服务全套&#xff08;质量有保证&#xff0c;内容详情&#xff09; 1. Docker阿里云镜像加速 在国内&#xff0c;从官方的Docker Hub仓库拉取…

docker构建国内镜像服务

在国内想要下载镜像比较困难&#xff0c;因此很多公司都构建自己的私有仓库。如何搭建私有仓库&#xff0c;请参考《docker私有仓库从无到有》。然而即使私有仓库服务构建完成&#xff0c;但是里面没有镜像&#xff0c;一样很苦恼。今天介绍一下如何利用国内云服务商提供的镜像…

电网电压的三相静止对称坐标系和三相电网电压的相量表示法

电网电压的空间电压矢量和电网电压的相量表示这两个概念需要区分清楚。分别参考邱关源的《电路》和张兴的《PWM整流》相关章节。 图2 三相电网电压的相量表示法 电网电压的相量表示&#xff0c;三相相差120度&#xff0c;整体逆时针50HZ旋转&#xff0c;这里的120度是指三分之一…

交流电中为什么要用相量法?

上两节课,电工学了电流和电压的相量表示法,对于复数的引入感觉稀里糊涂的,于是去搜了知乎,一篇文章让我恍然大悟,如果也有不理解的小伙伴可以复制这个,去知乎看详细解答嗷~ https://www.zhihu.com/question/347763932/answer/1103938667 下面👇是我的理解➕概括总结:…