跳表

倍增设计技术(指针跳跃技术)——表序问题——求森林的根 倍增技术也叫指针跳跃技术,因为涉及到指针,所以特别适合处理链表或有向有根树之类的数据结构,在图论和涉及链表的算法中有广泛的应用。 每当递归调用倍增技术时,要处理的数据之间的距离就会逐步加倍,经过k次迭代就可以完成2k数据的计算。 下面以经典的表序问题来说明此技术。 问题描述 给定一个包含 n 个元素的链表,每个元素 i 到 i 所指元素的距离用数组 d [ i ] 表示,每个元素指向的下一个元素用数组 p [ i ] 表示,初始化链表从表头指向表尾,规定表尾元素指向自己,距离为 0,d [ n ] = 0; 阅读详情

跳表

针对链表这种数据数据结构,捎加改造,可以支持类似“二分”的查找算法,这就是跳表。

如何理解跳表

对于单链表来说,即使存储的数据是有序的,如果在链表中查找某个数据,也只能从头到尾依次遍历,时间复杂度会很高,是O(n)。
在这里插入图片描述

对于单链表如何提高查找效率呢?可以类似图中如下做一级“索引”,这样子查询起来就会快很多,每两个节点抽取一个结点到上一级,可以把抽取出来的叫作索引。图中down表示down指针,指向下一级。
在这里插入图片描述

以查找6为例,首先可以遍历一级索引,当遍历到节点为5时,索引的下一个节点为7,大于需查找到6,则通过down指针向下一级,在原始链表中查找,现在再通过以down指针指向的5为起点,向后遍历,但遍历一次,就找到了我们需要查找的6。相比于原始链表的遍历,通过一级索引的方式,我们将原始需要6次的遍历,减少到了5次。

那如果再加一级索引呢?
在这里插入图片描述

以查找8为例,首先在二级索引遍历,当遍历到节点为5时,二级索引的下一个节点为9,大于查找的8,则通过二级索引的5的down指针向下一级,在一级索引中查找。一级索引以5为起来,向后遍务,找到7时,一级索引的7的下一个节点为9,大于查找的8,通过一级索引的7的down指针向下一级,原始链表则以7为起点,向后遍历,即可得到8,相比原始链接,通过增加两级索引,将链表中的遍历次数从8次减少到6次。

在例子中,数据量不大,仅几个数据,所以即便是增加两级索引,效果也不明显。那来个数据大点的直观显示吧!

这里我画了7个顶点,建立了5级索引。
在这里插入图片描述

图示为数据为1至72,查找70,仅需9次,即可找到70,效率提升是非常明显的。

跳表的时间复杂度?

单链的时间复杂度是O(n),那跳表的时间复杂度呢?是什么呢?

假设一个链表中有n个节点,每两个节点向上抽出一个节点作为上一级的索引节点,那上一级节点个数大约为n/2,那第二级的节点个数大约就是n/4,第三级节点的个数大约就是n/8,依次类推至,第K级索引结点个数就是K-1级索引结点个数的二分之一,那K级结点的个数就是n/(2的k次方)

假设有s级,最高级的索引有2个结点,通过公式 ,可得到n/(2的s次方)=2,那s=log2n-1,那跳表的高度就是log2n,在跳表中查询时,如果每一层都要遍历m个节点,那在跳表中查询一个数据的时间复杂度就是O(m*logn)

跳表的空间复杂度

比起单纯的单链表,跳表需要存储多级索引,肯定要消耗更多的存储空间。那需要消耗多少呢?

假设原始链表大小为n,第一级大约有n/2个结点,第二级大约有n/4个顶点,第三级大约有n/8个顶点,依次类推,每上升一级就减少一半,直到剩下2个结点,如果把每一层写出不就是一个等比数列:

n/2,n/4,n/8,…8,4,2

这几级索引节点和就是n/2+n/4+n/8…+8+4+2=n-2

所以跳表的空间复杂度为O(n)

跳表的代码实现

跳表是一种动态数据结构,不仅支持查找操作,还支持动态的插入、删除操作,而且插入、删除操作的时间复杂度为O(logn)

跳表作为一种动态数据结构,需要一种手段来维护索引与原始链表大小之间的平衡,也就是说,当链表中的结构多,索引中的节点就应该增加一些,避免复杂度退化,以及查找、插入、删除性能的下降。跳表是通过随机函数来维护这一“平衡性”。通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如索引值K,那就将这个结点插入到第一级至第K级索引中。

随机函数的选择很有讲究,从概率上来讲,能够保证跳表的索引大小和数据大小平衡性,不至于性能过度退化。

开始跳表的代码吧。就从添加开始吧。

public class SkipList {

  /** 当前最大层级 */
  private final int maxLevel;

  /** 随机函数 */
  private Random rand = new Random();

  /** 根节点 */
  private Node root;

  /** 当前的层级数 */
  private int levelNum;

  public SkipList(int maxLevel) {
    this.maxLevel = maxLevel;
    this.root = new Node();
  }

  /**
   * 跳表插入节点
   *
   * @param value 数据
   */
  public void add(int value) {
    // 1,通过随机函数决定叶子节点数
    int level = this.randomLevel();
    // 节点信息
    Node tempNode = new Node();
    // 保存值
    tempNode.data = value;
    // 最大层级
    tempNode.nodeMaxLevel = level;
    // 跳表的前指针位置记录
    Node[] nodeForward = new Node[level];
    // 1,将跳表都指向头节点
    for (int i = 0; i < level; i++) {
      nodeForward[i] = root;
    }
    // 进行跳表的查找,找到每一层该插入的位置。记录到nodeForward中
    Node nodeTmp = root;
    for (int i = level - 1; i >= 0; i--) {
      // 从最顶层的节点开始
      while (nodeTmp.forward[i] != null && nodeTmp.forward[i].data < value) {
        nodeTmp = nodeTmp.forward[i];
      }
      // 设置当前跳表层的位置。
      nodeForward[i] = nodeTmp;
    }

    // 按索引层的插入点位置插入节点
    for (int i = 0; i < level; i++) {
      tempNode.forward[i] = nodeForward[i].forward[i];
      nodeForward[i].forward[i] = tempNode;
    }

    // 设置当前跳表的层级
    if (levelNum < level) {
      this.levelNum = level;
    }
  }
    
  /**
   * 随机生成跳表的层级
   *
   * <p>跳表使用随机函数,做到大致分布均匀。
   *
   * @return 跳表的层级
   */
  private int randomLevel() {
    int level = 1;
    for (int i = 1; i < maxLevel; i++) {
      if (rand.nextInt() % 2 == 0) {
        level++;
      }
    }
    return level;
  }   
    
 /**
   * 检查一个数是否存在于跳表中
   *
   * @param value 当前待查找的值
   * @return true 存在,false 不存在
   */
  public boolean find(int value) {
    Node node = root;
    // 按层级向下进行索引搜索
    for (int i = levelNum - 1; i >= 0; i--) {
      if (node.forward[i] != null && node.forward[i].data < value) {
        node = node.forward[i];
      }
    }

    if (node.forward[0] != null && node.forward[0].data == value) {
      return true;
    } else {
      return false;
    }
  }
    
    
    
  /**
   * 执行删除操作
   *
   * @param targetValue 目标值
   */
  public void delete(int targetValue) {
    // 1,找到每一层中的删除的前一个顶点
    Node[] levelNodes = new Node[maxLevel];
    Node tempNode = root;
    for (int i = maxLevel - 1; i >= 0; i--) {
      while (tempNode.forward[i] != null && tempNode.forward[i].data < targetValue) {
        tempNode = tempNode.forward[i];
      }
      levelNodes[i] = tempNode;
    }

    // 将每层中的结果进行移除操作
    if (tempNode.forward[0] != null && tempNode.forward[0].data == targetValue) {
      for (int i = maxLevel - 1; i >= 0; i--) {
        if (levelNodes[i].forward[i] != null && levelNodes[i].forward[i].data == targetValue) {
          levelNodes[i].forward[i] = levelNodes[i].forward[i].forward[i];
        }
      }
    }
  }    
    
    
    
    
  /** 跳表的存储节点信息 */
  class Node {
    /** 用于存储数据,默认为-1 */
    private int data = -1;
    /** 跳表的节点 */
    private Node[] forward = new Node[maxLevel];
    /** 当前节点的最大层级 */
    private int nodeMaxLevel;

    @Override
    public String toString() {
      final StringBuilder sb = new StringBuilder("Node{");
      sb.append("data=").append(data);
      sb.append(", forward=").append(Arrays.toString(forward));
      sb.append(", nodeMaxLevel=").append(nodeMaxLevel);
      sb.append('}');
      return sb.toString();
    }
  }
}

插入操作

以插入来讲解吧,如果明白了插入,其他都是操作都是类似的。

还是来举个例子吧,假设已经存在如下图所示的跳表,插入16,这个数据。
在这里插入图片描述

这个节点的插入分为这样几步:

  1. 构建16这个数据项的结点,生成随机的索引层数。示例生成为3层
    在这里插入图片描述

  2. 根据随机函数生成的结果3,找到这3层中16这个结点的插入位置。将位置的结果记录到nodeForward中。

        // 跳表的前指针位置记录
        Node[] nodeForward = new Node[level];
        // 1,将跳表都指向头节点
        for (int i = 0; i < level; i++) {
          nodeForward[i] = root;
        }
        // 进行跳表的查找,找到每一层该插入的位置。记录到nodeForward中
        Node nodeTmp = root;
        for (int i = level - 1; i >= 0; i--) {
          // 从最顶层的节点开始
          while (nodeTmp.forward[i] != null && nodeTmp.forward[i].data < value) {
            nodeTmp = nodeTmp.forward[i];
          }
          // 设置当前跳表层的位置。
          nodeForward[i] = nodeTmp;
        }
    

    在这里插入图片描述

    按层搜索的起点定为root,从root节点开始,将索引层找到的离插入点最近的结点记录到nodeForward中。

    1. 将构建的16这个数据及索引插入到跳表中,通过之前找到的位置nodeForward,将索引项的每层都插入指定层中,插入完成后,nodeForward变成了如下所示:

      	// 按索引层的插入点位置插入节点
          for (int i = 0; i < level; i++) {
            tempNode.forward[i] = nodeForward[i].forward[i];
            nodeForward[i].forward[i] = tempNode;
          }
      

      在这里插入图片描述

      由于nodeForward数组是跳到中的一部分节点,当数据插入后,整个跳表就变成了如下所示:
      在这里插入图片描述

      就这样将数据插入到了跳表中。

    查询操作

    还是以刚刚的跳表为例,来查找28这个数据吧,搜索相对于添加来说就简单多了,直接使用一个指针按层向下搜索即可

    1. 使用一个指针搜索向下搜索。

          Node node = root;
          // 按层级向下进行索引搜索
          for (int i = levelNum - 1; i >= 0; i--) {
            if (node.forward[i] != null && node.forward[i].data < value) {
              node = node.forward[i];
            }
          }
      

      每一个层找仅查找比当前目标数小的数据,通过“i”(可以理解为down指针)即向下搜索。当搜索完毕,即找到了离28最近的值,26这个顶点。
      在这里插入图片描述

      1. 在原始链表那层向后检查下是否为目标值,即可确认是否为搜索目标值。

            if (node.forward[0] != null && node.forward[0].data == value) {
              return true;
            } else {
              return false;
            }
        

      搜索的原理也解释完毕了。

    总结

    跳表使用空间换时间的设计思路,通过构建多级索引来提高查询效率。实现了基于链表的“二分查找”,跳表是一种动态数据结构,可以快速的插入、删除、查询,而且时间复杂度都为O(logn),跳表的空间复杂度为O(N).跳表相比对其他动态数据结构,链表支持区间搜索的时间复杂度也是O(logn),这也可能是redis使用跳表来实现有序集合的一个原因,另一个原因,可能就是跳表实现起来更容易一点吧。

Redis-跳表 跳表(skiplist)是一种有序数据结构,它通过在每个节点维持多个指向其他节点的指针来达到快速访问的目的。Redis使用跳表作为有序表的底层实现。 阅读详情

相关推荐

用豆包+Skill从零制作AI漫剧:分镜、画面、配音到成片全流程

AI漫剧作为一种新兴内容形态,结合AI绘画与AI视频生成技术,正在改变短视频创作方式。其核心原理是利用提示词工程,将创意转化为结构化分镜脚本,再通过豆包等AI助手的Skill能力,将重复的脚本生成和分镜拆解流程固化为可复用的指令模板,从而大幅降低从构思到成品的生产门槛。这种模式无需剧组、演员和实景拍摄,让个人创作者也能借助图像生成、TTS配音与剪辑合成工具,批量化地生产剧情短片,适用于短视频平台连载、商业定制与个人IP打造。围绕豆包+Skill这条主线,从选题、剧本写作、分镜表设计,到画面提示词生成、视频片

weixin_29048309的博客 1477

Redis数据结构(四)-跳跃表、跳跃表节点(前进指针、跨度等)

跳跃表概述跳跃表的实现跳跃表节点层(level)前进指针(forward)跨度(span)后退指针(backward)分值(score) 和 成员(obj)跳跃表跳跃表API跳跃表的查询跳跃表的插入跳跃表的删除 概述 跳跃表(skiplist) 是一种 有序数据结构,它通过 在每个节点中 维持 多个 指向其他节点的指针,从而达到 快速访问节点 的 目的 跳跃表 支持 平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过 顺序性操作 来批量 处理节点 在大部分情况下,跳跃表的效率可以和平衡树相媲美,并

星光之子的博客 893

OV5640调试大坑,关于binning、RES分辨率与EV曝光

OV5640在使用binning模式时,必须考虑使用场景,分辨率下降叠加动态范围压缩,会使目标对象细节在高光环境下大量丢失,2x2的binning模式,分辨率会远低于感光面积的1/4。

chongyun_的专栏 1824

一文详解跳表的原理及实现

跳表可以达到和红黑树一样的时间复杂度O(logN),且实现简单,Redis中的有序集合对象的底层数据结构就使用了跳表。本篇文章将对跳表的实现进行学习。跳表的时间复杂度与AVL树和红黑树相同,可以达到O(logN),但是AVL树要维持高度的平衡,红黑树要维持高度的近似平衡,这都会导致插入或者删除节点时的一些时间开销,所以跳表相较于AVL树和红黑树来说,省去了维持高度的平衡的时间开销,但是相应的也付出了更多的空间来存储多个层的节点,所以跳表是用空间换时间的数据结构。

Java技术攻略的博客 1655

Oracle OCP 19c 认证1Z0-083考试大纲(Oracle官方版)

Oracle Database: Managing Multitenant Architecture Ed 1 course

#真题在线# 1588

45、使用 sqlread 函数将数据从数据库表导入 MATLAB

本文详细介绍了如何使用 MATLAB 中的 `sqlread` 函数从不同类型的数据库(如 Microsoft SQL Server 和 SQLite)中导入数据。文章涵盖了 `sqlread` 的基本语法、输入输出参数、使用示例以及注意事项,并通过多个实例演示了自定义导入选项、元信息检索和数据类型转换等内容。此外,还总结了导入数据库数据的通用流程及最佳实践建议,帮助用户更高效地进行数据分析和处理工作。

bash7scripter的博客 133

跳表:高效索引的神奇算法

本文主要介绍跳表的基本原理、特点及实现细节,包括随机性选层、插入、删除和查找的技术细节,和Java实现源码

若明天不见 1228

Selenium|通过更改亚马逊amazon的配送地获取当地的cookie

如果我们需要拿到国外配送地的商品信息,这个时候就需要用到selenium或者pypputeer自动化的方法去更改配送地,拿到当地的cookie信息,之后,携带当地的cookie使用requests发送请求,就可以拿到国外配送地的商品数据信息。一般来说,我们访问亚马逊,亚马逊的配送地一般显示的是国内,但是国内的商品信息和国外的商品信息其实是存在很大差别的,比如你在搜索框输入某个关键词,看到的

Python进阶专栏《爬虫实战进阶》,《Pyppeteer从入门到精通》原创作者 991

跳表(Skip List)

跳表SkipList一. 跳表的定义 一. 跳表的定义 跳表(SkipList):增加了向前指针的链表叫做指针。跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质是一种可以进行二分查找的有序链表。跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。 跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树、AVL树不相上下,但是跳表的原理非常简单,目前在Redis和LevelDB中都有用到。 ...

3万+

实现基于 Python 和 xterm.js 的 Web 交互终端demo

前端 (HTML):使用 xterm.js 作为前端终端模拟器库。创建了一个 WebSocket 连接到 ws://localhost:8765,这与后端服务器的地址和端口匹配。监听 WebSocket 消息,并将数据写入终端。同时监听用户输入并发送到服务器。后端 (Python):使用 websockets 库来创建 WebSocket 服务器,监听端口 8765。使用 paramiko 库来创建 SSH 客户端并连接到指定的远程服务器。

u010674101的专栏 876

华为OD机试真题 新系统 2026-05-13 Python&JS语言 实现【查找能被整除的最大整数】

给定一个字符串和一个正整数,字符串由大小写字母和数字组成,要求从字符串中找出最大且能被给定正整数整除的数。

MISAYAONE的博客 224

Redis中的跳表是怎么回事

引子 早些年笔者一次真实的美团面试经历。 面试官:请问Redis有哪些数据结构? 我:string、hash、list、set、zset、 bitmaps、hyperloglogs、geospatial、布隆过滤器。 面试官:zset主要用什么应用场景? 我:zset主要是一种有权重的set集合,因为每一个元素都可以有一个“分数”,redis提供了根据这个分数操作的api,比如取分数前几的啊,所以我们可以用作一些类似于排行榜的东西,比如点赞次数前几名的啦,,叭叭叭。 面试官:嗯,,,那它的底层的数据结构

程序员大航子 4756

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

什么是跳表

储存数据时,数组容量有限且有序数组增加元素时效率比较低,所以存储数据时常选用链表。但是链表的查询效率O(n)。 有没有可以提高链表查找效率的方式呢?跳表就是这样一种结构。利用空间换时间的方式,提高查询效率。

每天进步一点 1万+

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

什么是跳表,Java如何实现跳表?

如图:每个链表存在一个指向下一节点的指针,如果我们要对其任一节点进行增删改,都需要先使用迭代器进行查询,找到指定节点进行修改,复杂度较高。

benaso的大学学习历程 1667

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

跳表skiplist

跳表可以说是平衡树的一种替代品。它也是为了解决的的问题。那这个问题,hash 表解决的很好,插入和查找都是 O(1) 的时间复杂度。但若呢?这个时候 hash 表就不行了,二叉查找树可以解决这个问题。但是由于二叉查找树在按大小顺序进行插入的时候,就会退化为链表。所以又出现了平衡二叉树,而根据算法不同,又分为AVL树、B-Tree、B+Tree、红黑树等。(先不用管这些数据结构的实现,其是复杂的)。而跳表的出现就是为了解决平衡二叉树复杂的问题,它以一种。

m0_57408211的博客 977

【数据结构】一文解析跳表

跳表是一种 的数据结构,随机性体现在跳表的层数是不固定的。跳表基于有序链表,可以在原始链表基础上创建多层索引架构,采用这种随机技术,跳表中的搜索、插入、删除操作的时间均为 。 上面就是跳表的图示,注意 h 节点是头结点,n 节点是尾结点,从图片来看本质就是一个链表加了 n 层来进行索引,跳表的查询流程等后面会逐步介绍的。大家在看到上面图片的时候也能想象,如果去掉 、、,其实跳表也就是一个链表,或者说是一个数组,但是加上之后,在做增删改查的时候就可以从上往下逐层查询查询,性能大大提高,所以查询性能才比较高

Jwasx的博客 1712

跳表——C++

skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。那么相比而言它的优势是什么的呢?那么等我们学习完它的细节实现,我们再来对比。skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。对细节感兴趣的同学可以下载论文原文来阅读。

sqm_C的博客 1409

跳表的理解以及使用

跳表(SkipList),是一种随机化的数据结构,可以看作是一种可以进行二分查找的有序链表。跳表通过增加多级索引(即“跳跃”的层)来优化链表的查找、插入和删除操作,使得这些操作的时间复杂度降低到O(logn)。跳表在Redis和LevelDB等系统中都有应用,其实现和维护相对简单,但性能上却与红黑树、AVL树等平衡树不相上下。

zhangdonghuirjdd的博客 1258

Redis 跳表

文章目录基本概念动态查找的数据结构数组结构链表平衡树跳表的雏形跳跃链表的实现原理简单索引多级索引索引层数和索引节点密度跳表的复杂度分析时间复杂度分析跳表的空间存储跳表在 Redis 中的应用跳表随机层数的实现原理Redis 关于跳表的源码参考 基本概念 允许快速查询一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n)。 动态查找的数据结构 可以作为查找数据结构的包括: 线性结构:数组、链表 非线性结构:平衡树 数组结构 缺点: 移动和删除时需要整体移

Powerstot的博客 3871

从链表到跳表——跳表是什么?

跳表是一个面试和笔试中虽然不会考到,但会经常被提到的一个数据结构,其最典型的应用就是在Redis中。那么跳表是怎么提出的呢,它又解决了什么问题呢?本文试图从数组到链表再到跳表,分析现有数据结构的局限性,从而引出跳表的定义,并对跳表进行了一个全面的基本介绍

zhuiyisinian的博客 1276

跳表 SkipList

考虑到数据结构的可扩展性,结点的 Key 与 Value 均定义为泛型。其中 T key 继承Comparable接口,保证 key 可以用于比较排序,E Value 则用于存储结点的值。每个结点包含一个 forward 数组来存储索引中下一个索引结点的引用。该数组在初始化时,大小被设定为该结点的索引层数,以减少空间浪费。

SolitudeCoding的博客 1373

跳表的插入

跳表的插入

做一个善良的人,做好自己的事,如果愿意,再发一份光。 1106

C++【跳表】

skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A对细节感兴趣的同学可以下载论文原文来阅读。skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。如果是一个有序的链表,查找数据的时间复杂度是O(N)。假如我们每相邻两个节点升高一层,增加一个指针。

芊樱烛渊的博客 1678
上一篇: 重构实例-消息发送-使用策略进行重构-枚举方式-7
下一篇: 快速排序
null_null_bug
博客等级 码龄7年 72粉丝 71原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值