每日一博 - 如何理解跳表(SkipList)

article/2025/8/7 21:51:53

文章目录

  • 什么是跳跃表SkipList
  • 跳表关键字
  • Why Skip List
  • Code
    • 跳表-查询
    • 跳表-删除
    • 跳表-插入
  • 小结
  • 完整Code

在这里插入图片描述


什么是跳跃表SkipList

跳跃表(简称跳表)由美国计算机科学家William Pugh于1989年发明

论文: Skip lists: a probabilistic alternative to balanced trees

跳表(SkipList,全称跳跃表)是用于有序元素序列快速搜索查找的一个数据结构,跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表

跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。

跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,实现也比红黑树简单很多。

在这里插入图片描述


跳表关键字

  • 随机化
  • 有序链表
  • 索引
  • 二分查找
    在这里插入图片描述

Why Skip List

地球人都知道的事儿:

  • 顺序表(数组)是内存上一块连续的区域,基于下标,查找速度快。
  • 链表: 内存上不连续,通过指针相连, 插入和删除动作效率特别高,但是查询呢,时间复杂度o(n)

在这里插入图片描述

那么链表上查询的时间复杂度能优化一下吗?

在这里插入图片描述

我们知道有很多算法有个思想: 空间换时间 。 如果在链表的上面加一层索引,让部分节点在上层能够直接定位到,这样链表的查询时间近乎减少一半 。

在这里插入图片描述

那查询的时候,就会发生某些变化 -----------> 如果要查找某个节点, 首先需要从上一层快速定位到节点所在的一个范围 ,如果向下查找的有个向下的指针指向真实的数据,那理论上,以前有n, 现在就是 n/2 .

当然了,如果节点数据量超巨,一样很慢,可能就损耗在了,一层一层的查找上。 我们知道二分查找每次都能折半的去压缩查找范围, 那用上这个二分查找是不是就会快很多????

在这里插入图片描述

事实上,跳表就能让链表拥有近乎的接近二分查找的效率的一种数据结构,其原理依然是给上面加若干层索引,优化查找速度。

通过上图我们可以知道,这样的一个数据结构对有序链表进行查找都能近乎二分的性能。

究其原因就是在上面维护了多层的索引

首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。

由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。

对于理想的跳表,每向上一层索引节点数量都是下一层的1/2.那么如果n个节点增加的节点数量(1/2+1/4+…)<n。并且层数较低,对查找效果影响不大。

但是对于这么一个结构,你可能会疑惑,这样完美的结构真的存在吗?大概率不存在的,因为作为一个链表,少不了增删该查的一些操作。而删除和插入可能会改变整个结构,所以上面的这些都是理想的结构,在插入的时候是否添加上层索引是个概率问题(1/2的概率)。


Code

在实现本跳表的过程为了便于操作,我们将跳表的头结点(head)的key设为int的最小值(一定满足左小右大方便比较)。

对于每个节点的设置,设置成SkipNode类,为了防止初学者将next向下还是向右搞混,直接设置right,down两个指针。

class SkipNode<T>
{int key;T value;SkipNode right,down;//右下个方向的指针public SkipNode (int key,T value) {this.key=key;this.value=value;}
}

跳表的结构和初始化, 其主要参数和初始化方法为:

public class SkipList <T> {SkipNode headNode;//头节点,入口int highLevel;//当前跳表索引层数Random random;// 用于投掷硬币final int MAX_LEVEL = 32;//最大的层SkipList(){random=new Random();headNode=new SkipNode(Integer.MIN_VALUE,null);highLevel=0;}//其他方法
}

在这里插入图片描述


跳表-查询

很多时候链表也可能这样相连仅仅是某个元素或者key作为有序的标准。所以有可能链表内部存在一些value。不过修改和查询其实都是一个操作,找到关键数字(key)。并且查找的流程也很简单,设置一个临时节点team=head。当team不为null其流程大致如下:

  • (1) 从team节点出发,如果当前节点的key与查询的key相等,那么返回当前节点(如果是修改操作那么一直向下进行修改值即可)。

  • (2) 如果key不相等,且右侧为null,那么证明只能向下(结果可能出现在下右方向),此时team=team.down

  • (3) 如果key不相等,且右侧不为null,且右侧节点key小于待查询的key。那么说明同级还可向右,此时team=team.right

  • (4)(否则的情况)如果key不相等,且右侧不为null,且右侧节点key大于待查询的key 。那么说明如果有结果的话就在这个索引和下个索引之间,此时team=team.down。

最终将按照这个步骤返回正确的节点或者null(说明没查到)。

在这里插入图片描述
例如上图查询12节点.

  • 第一步从head出发发现右侧不为空,且7<12,向右;
  • 第二步右侧为null向下;
  • 第三步节点7的右侧10<12继续向右;
  • 第四步10右侧为null向下;
  • 第五步右侧12小于等于向右。
  • 第六步起始发现相等返回节点结束。

代码如下

public SkipNode search(int key) {SkipNode team=headNode;while (team!=null) {if(team.key==key){return  team;}else if(team.right==null)//右侧没有了,只能下降{team=team.down;}else if(team.right.key>key)//需要下降去寻找{team=team.down;}else //右侧比较小向右{team=team.right;}}return null;
}

在这里插入图片描述


跳表-删除

删除操作比起查询稍微复杂一丢丢,但是比插入简单。删除需要改变链表结构所以需要处理好节点之间的联系。对于删除操作需要谨记以下几点:

  • (1)删除当前节点和这个节点的前后节点都有关系

  • (2)删除当前层节点之后,下一层该key的节点也要删除,一直删除到最底层

根据这两点分析一下:如果找到当前节点了,它的前面一个节点怎么查找呢?这个总不能再遍历一遍吧!有的使用四个方向的指针(上下左右)用来找到左侧节点。是可以的,但是这里可以特殊处理一下 ,不直接判断和操作节点,先找到待删除节点的左侧节点。通过这个节点即可完成删除,然后这个节点直接向下去找下一层待删除的左侧节点。

设置一个临时节点team=head,当team不为null具体循环流程为:

  • (1)如果team右侧为null,那么team=team.down(之所以敢直接这么判断是因为左侧有头结点在左侧,不用担心特殊情况)

  • (2)如果team右侧不 为null,并且右侧的key等于待删除的key,那么先删除节点,再team向下team=team.down为了删除下层节点。

  • (3)如果team右侧不 为null,并且右侧key小于待删除的key,那么team向右team=team.right。

  • (4)如果team右侧不 为null,并且右侧key大于待删除的key,那么team向下team=team.down,在下层继续查找删除节点。

在这里插入图片描述

例如上图删除10节点,

  • 首先team=head从team出发,7<10向右(team=team.right后面省略);
  • 第二步右侧为null只能向下;
  • 第三部右侧为10在当前层删除10节点然后向下继续查找下一层10节点;
  • 第四步8<10向右;
  • 第五步右侧为10删除该节点并且team向下。
  • team为null说明删除完毕退出循环。
public void delete(int key)//删除不需要考虑层数
{SkipNode team=headNode;while (team!=null) {if (team.right == null) {//右侧没有了,说明这一层找到,没有只能下降team=team.down;}else if(team.right.key==key)//找到节点,右侧即为待删除节点{team.right=team.right.right;//删除右侧节点team=team.down;//向下继续查找删除}else if(team.right.key>key)//右侧已经不可能了,向下{team=team.down;}else { //节点还在右侧team=team.right;}}
}

在这里插入图片描述


跳表-插入

插入操作在实现起来是最麻烦的,需要的考虑的东西最多。

查询,不需要动索引;
删除,每层索引如果有删除就是了。

插入不一样了,插入需要考虑是否插入索引,插入几层等问题。

由于需要插入删除所以我们肯定无法维护一个完全理想的索引结构,因为它耗费的代价太高。但我们使用随机化的方法去判断是否向上层插入索引

即产生一个[0-1]的随机数如果小于0.5就向上插入索引,插入完毕后再次使用随机数判断是否向上插入索引。运气好这个值可能是多层索引,运气不好只插入最底层(这是100%插入的)。但是索引也不能不限制高度,我们一般会设置索引最高值如果大于这个值就不往上继续添加索引了。

其流程为

  • (1)首先通过上面查找的方式,找到待插入的左节点。插入的话最底层肯定是需要插入的,所以通过链表插入节点(需要考虑是否为末尾节点)

  • (2)插入完这一层,需要考虑上一层是否插入,首先判断当前索引层级,如果大于最大值那么就停止(比如已经到最高索引层了)。否则设置一个随机数1/2的概率向上插入一层索引(因为理想状态下的就是每2个向上建一个索引节点)。

  • (3)继续(2)的操作,直到概率退出或者索引层数大于最大索引层。

具体向上插入的时候,实质上还有非常重要的细节需要考虑。首先如何找到上层的待插入节点 ?

这个各个实现方法可能不同,如果有左、上指向的指针那么可以向左向上找到上层需要插入的节点,但是如果只有右指向和下指向的我们也可以巧妙的借助查询过程中记录下降的节点。因为曾经下降的节点倒序就是需要插入的节点,最底层也不例外(因为没有匹配值会下降为null结束循环)。在这里我使用栈这个数据结构进行存储,当然使用List也可以。

下图就是给了一个插入示意图。

在这里插入图片描述

其次如果该层是目前的最高层索引,需要继续向上建立索引应该怎么办?

首先跳表最初肯定是没索引的,然后慢慢添加节点才有一层、二层索引,但是如果这个节点添加的索引突破当前最高层,该怎么办呢?

这时候需要注意了,跳表的head需要改变了,新建一个ListNode节点作为新的head,将它的down指向老head,将这个head节点加入栈中(也就是这个节点作为下次后面要插入的节点),就比如上面的9节点如果运气够好再往上建立一层节点,会是这样的。

在这里插入图片描述
插入上层的时候注意所有节点要新建(拷贝),除了right的指向down的指向也不能忘记,down指向上一个节点可以用一个临时节点作为前驱节点。如果层数突破当前最高层,头head节点(入口)需要改变。

代码如下

public void add(SkipNode node)
{int key=node.key;SkipNode findNode=search(key);if(findNode!=null)//如果存在这个key的节点{findNode.value=node.value;return;}Stack<SkipNode>stack=new Stack<SkipNode>();//存储向下的节点,这些节点可能在右侧插入节点SkipNode team=headNode;//查找待插入的节点   找到最底层的哪个节点。while (team!=null) {//进行查找操作 if(team.right==null)//右侧没有了,只能下降{stack.add(team);//将曾经向下的节点记录一下team=team.down;}else if(team.right.key>key)//需要下降去寻找{stack.add(team);//将曾经向下的节点记录一下team=team.down;}else //向右{team=team.right;}}int level=1;//当前层数,从第一层添加(第一层必须添加,先添加再判断)SkipNode downNode=null;//保持前驱节点(即down的指向,初始为null)while (!stack.isEmpty()) {//在该层插入nodeteam=stack.pop();//抛出待插入的左侧节点SkipNode nodeTeam=new SkipNode(node.key, node.value);//节点需要重新创建nodeTeam.down=downNode;//处理竖方向downNode=nodeTeam;//标记新的节点下次使用if(team.right==null) {//右侧为null 说明插入在末尾team.right=nodeTeam;}//水平方向处理else {//右侧还有节点,插入在两者之间nodeTeam.right=team.right;team.right=nodeTeam;}//考虑是否需要向上if(level>MAX_LEVEL)//已经到达最高级的节点啦break;double num=random.nextDouble();//[0-1]随机数if(num>0.5)//运气不好结束break;level++;if(level>highLevel)//比当前最大高度要高但是依然在允许范围内 需要改变head节点{highLevel=level;//需要创建一个新的节点SkipNode highHeadNode=new SkipNode(Integer.MIN_VALUE, null);highHeadNode.down=headNode;headNode=highHeadNode;//改变headstack.add(headNode);//下次抛出head}}
}

在这里插入图片描述


小结

对于上面,跳表完整分析就结束啦,当然,你可能看到不同品种跳表的实现,还有的用数组方式表示上下层的关系这样也可以,但本文只定义right和down两个方向的链表更纯正化的讲解跳表。

对于跳表以及跳表的同类竞争产品:红黑树,为啥Redis的有序集合(zset) 使用跳表呢?因为跳表除了查找插入维护和红黑树有着差不多的效率,它是个链表,能确定范围区间,而区间问题在树上可能就没那么方便查询啦。

而JDK中跳跃表ConcurrentSkipListSet和ConcurrentSkipListMap。


完整Code

import java.util.Random;
import java.util.Stack;
class SkipNode<T>
{int key;T value;SkipNode right,down;//左右上下四个方向的指针public SkipNode (int key,T value) {this.key=key;this.value=value;}}
public class SkipList <T> {SkipNode headNode;//头节点,入口int highLevel;//层数Random random;// 用于投掷硬币final int MAX_LEVEL = 32;//最大的层SkipList(){random=new Random();headNode=new SkipNode(Integer.MIN_VALUE,null);highLevel=0;}public SkipNode search(int key) {SkipNode team=headNode;while (team!=null) {if(team.key==key){return  team;}else if(team.right==null)//右侧没有了,只能下降{team=team.down;}else if(team.right.key>key)//需要下降去寻找{team=team.down;}else //右侧比较小向右{team=team.right;}}return null;}public void delete(int key)//删除不需要考虑层数{SkipNode team=headNode;while (team!=null) {if (team.right == null) {//右侧没有了,说明这一层找到,没有只能下降team=team.down;}else if(team.right.key==key)//找到节点,右侧即为待删除节点{team.right=team.right.right;//删除右侧节点team=team.down;//向下继续查找删除}else if(team.right.key>key)//右侧已经不可能了,向下{team=team.down;}else { //节点还在右侧team=team.right;}}}public void add(SkipNode node){int key=node.key;SkipNode findNode=search(key);if(findNode!=null)//如果存在这个key的节点{findNode.value=node.value;return;}Stack<SkipNode>stack=new Stack<SkipNode>();//存储向下的节点,这些节点可能在右侧插入节点SkipNode team=headNode;//查找待插入的节点   找到最底层的哪个节点。while (team!=null) {//进行查找操作if(team.right==null)//右侧没有了,只能下降{stack.add(team);//将曾经向下的节点记录一下team=team.down;}else if(team.right.key>key)//需要下降去寻找{stack.add(team);//将曾经向下的节点记录一下team=team.down;}else //向右{team=team.right;}}int level=1;//当前层数,从第一层添加(第一层必须添加,先添加再判断)SkipNode downNode=null;//保持前驱节点(即down的指向,初始为null)while (!stack.isEmpty()) {//在该层插入nodeteam=stack.pop();//抛出待插入的左侧节点SkipNode nodeTeam=new SkipNode(node.key, node.value);//节点需要重新创建nodeTeam.down=downNode;//处理竖方向downNode=nodeTeam;//标记新的节点下次使用if(team.right==null) {//右侧为null 说明插入在末尾team.right=nodeTeam;}//水平方向处理else {//右侧还有节点,插入在两者之间nodeTeam.right=team.right;team.right=nodeTeam;}//考虑是否需要向上if(level>MAX_LEVEL)//已经到达最高级的节点啦break;double num=random.nextDouble();//[0-1]随机数if(num>0.5)//运气不好结束break;level++;if(level>highLevel)//比当前最大高度要高但是依然在允许范围内 需要改变head节点{highLevel=level;//需要创建一个新的节点SkipNode highHeadNode=new SkipNode(Integer.MIN_VALUE, null);highHeadNode.down=headNode;headNode=highHeadNode;//改变headstack.add(headNode);//下次抛出head}}}public void printList() {SkipNode teamNode=headNode;int index=1;SkipNode last=teamNode;while (last.down!=null){last=last.down;}while (teamNode!=null) {SkipNode enumNode=teamNode.right;SkipNode enumLast=last.right;System.out.printf("%-8s","head->");while (enumLast!=null&&enumNode!=null) {if(enumLast.key==enumNode.key){System.out.printf("%-5s",enumLast.key+"->");enumLast=enumLast.right;enumNode=enumNode.right;}else{enumLast=enumLast.right;System.out.printf("%-5s","");}}teamNode=teamNode.down;index++;System.out.println();}}public static void main(String[] args) {SkipList<Integer>list=new SkipList<Integer>();for(int i=1;i<20;i++){list.add(new SkipNode(i,666));}list.printList();list.delete(4);list.delete(8);list.printList();}
}

参考: https://www.toutiao.com/a6910597347328426503


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

相关文章

Skiplist(跳表)实现

前言&#xff1a;跳表本质是一种查找结构&#xff0c;相较于AVL树、红黑树、哈希表&#xff0c;实现起来要更加简单&#xff0c;而且效率也不差。 一.Skiplist概念 跳表本质也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff0c;跟平衡搜索树&#xff08;AVL树、红…

跳表(SkipList)设计与实现(java)

微信搜一搜「bigsai」关注这个有趣的程序员&#xff0c;一起做个朋友 新人原创公众号&#xff0c;求支持一下&#xff01;你的点赞三连肯定对我至关重要&#xff01; 文章已收录在 我的Github bigsai-algorithm 欢迎star 前言 跳表是面试常问的一种数据结构&#xff0c;它在很…

SkipList和java中ConcurrentSkipListMap的实现

文章目录 简介SkipListConcurrentSkipListMapSkipList的实现concurrent的实现 总结 SkipList和java中ConcurrentSkipListMap的实现 简介 一开始听说SkipList我是一脸懵逼的&#xff0c;啥&#xff1f;还有SkipList&#xff1f;这个是什么玩意。 后面经过我的不断搜索和学习&a…

跳表(SkipList)及ConcurrentSkipListMap源码解析

二分查找和AVL树查找 二分查找要求元素可以随机访问&#xff0c;所以决定了需要把元素存储在连续内存。这样查找确实很快&#xff0c;但是插入和删除元素的时候&#xff0c;为了保证元素的有序性&#xff0c;就需要大量的移动元素了。 如果需要的是一个能够进行二分查找&#…

Redis数据结构之——跳表skiplist

写在前面 以下内容是基于Redis 6.2.6 版本整理总结 一、跳表&#xff08;skiplist&#xff09; 如何理解跳表&#xff1f;在了解跳表之前&#xff0c;我们先从普通链表开始&#xff0c;一点点揭开跳表的神秘面纱~ 首先&#xff0c;普通单链表来说&#xff0c;即使链表是有序…

redis中ziplist,skiplist

ziplist压缩表 ziplist主要是为了节约内存&#xff0c;他将元素存储在一块连续的内存空间中&#xff0c;这样在查询数据的时候也可以利用CPU的缓存访问数据&#xff0c;加快查询的效率 相较于数组而言。我们知道,数组要求每个元素的大小都相同,如果我们要存储不同长度的字符串…

跳表-skiplist的简单实现

文章目录 1、什么是跳表-skiplist2、skiplist的效率如何保证&#xff1f;3、skiplist的实现4、skiplist跟平衡搜索树和哈希表的对比 1、什么是跳表-skiplist skiplist本质上也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff0c;跟平衡搜索树和哈希表的价值是一样…

跳表: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…