算法与数据结构-跳表

news2024/9/28 15:22:11

文章目录

  • 什么是跳表
  • 跳表的时间复杂度
  • 跳表的空间复杂度
  • 如何高效的插入和删除
  • 跳表索引动态更新
  • 代码示例


什么是跳表

对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是 O(n)。
在这里插入图片描述
那怎么来提高查找效率呢?如果像图中那样,对链表建立一级“索引”,查找起来是不是就会更快一些呢?每两个结点提取一个结点到上一级,我们把抽出来的那一级叫做索引或索引层。你可以看我画的图。图中的 down 表示 down 指针,指向下一级结点。
在这里插入图片描述
如果我们现在要查找某个结点,比如 16。我们可以先在索引层遍历,当遍历到索引层中值为 13 的结点时,我们发现下一个结点是 17,那要查找的结点 16 肯定就在这两个结点之间。然后我们通过索引层结点的 down 指针,下降到原始链表这一层,继续遍历。这个时候,我们只需要再遍历 2 个结点,就可以找到值等于 16 的这个结点了。这样,原来如果要查找 16,需要遍历 10 个结点,现在只需要遍历 7 个结点。

从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。那如果我们再加一级索引呢?效率会不会提升更多呢?

跟前面建立第一级索引的方式相似,我们在第一级索引的基础之上,每两个结点就抽出一个结点到第二级索引。现在我们再来查找 16,只需要遍历 6 个结点了,需要遍历的结点数量又减少了。
在这里插入图片描述
这种链表加多级索引的结构,就是跳表。

跳表的时间复杂度

按照我们刚才讲的,每两个结点会抽出一个结点作为上一级索引的结点,那第一级索引的结点个数大约就是 n/2,第二级索引的结点个数大约就是 n/4,第三级索引的结点个数大约就是 n/8,依次类推,也就是说,第 k 级索引的结点个数是第 k-1 级索引的结点个数的 1/2,那第 k 级索引结点的个数就是 n/(2k)。

假设索引有 h 级,最高级的索引有 2 个结点。通过上面的公式,我们可以得到 n/(2h)=2,从而求得 h=log2n-1。如果包含原始链表这一层,整个跳表的高度就是 log2n。我们在跳表中查询某个数据的时候,如果每一层都要遍历 m 个结点,那在跳表中查询一个数据的时间复杂度就是 O(m*logn)。

那这个 m 的值是多少呢?按照前面这种索引结构,我们每一级索引都最多只需要遍历 3 个结点,也就是说 m=3,为什么是 3 呢?我来解释一下。

假设我们要查找的数据是 x,在第 k 级索引中,我们遍历到 y 结点之后,发现 x 大于 y,小于后面的结点 z,所以我们通过 y 的 down 指针,从第 k 级索引下降到第 k-1 级索引。在第 k-1 级索引中,y 和 z 之间只有 3 个结点(包含 y 和 z),所以,我们在 K-1 级索引中最多只需要遍历 3 个结点,依次类推,每一级索引都最多只需要遍历 3 个结点。

在这里插入图片描述

通过上面的分析,我们得到 m=3,所以在跳表中查询任意数据的时间复杂度就是 O(logn)。这个查找的时间复杂度跟二分查找是一样的。换句话说,我们其实是基于单链表实现了二分查找.

跳表的空间复杂度

通过上面的分析,我们得到 m=3,所以在跳表中查询任意数据的时间复杂度就是 O(logn)。这个查找的时间复杂度跟二分查找是一样的。换句话说,我们其实是基于单链表实现了二分查找

在这里插入图片描述
这几级索引的结点总和就是 n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是 O(n)。也就是说,如果将包含 n 个结点的单链表构造成跳表,我们需要额外再用接近 n 个结点的存储空间。

如何高效的插入和删除

我们知道,在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是 O(1)。但是,这里为了保证原始链表中数据的有序性,我们需要先找到要插入的位置,这个查找操作就会比较耗时。

对于纯粹的单链表,需要遍历每个结点,来找到插入的位置。但是,对于跳表来说,我们讲过查找某个结点的时间复杂度是 O(logn),所以这里查找某个数据应该插入的位置,方法也是类似的,时间复杂度也是 O(logn)。我画了一张图,你可以很清晰地看到插入的过程。

在这里插入图片描述

我们再看下删除操作。 如果这个结点在索引中也有出现,我们除了要删除原始链表中的结点,还要删除索引中的。因为单链表中的删除操作需要拿到要删除结点的前驱结点,然后通过指针操作完成删除。所以在查找要删除的结点的时候,一定要获取前驱结点。当然,如果我们用的是双向链表,就不需要考虑这个问题了。

跳表索引动态更新

当我们不停地往跳表中插入数据时,如果我们不更新索引,就有可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表还会退化成单链表。
在这里插入图片描述
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。

当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?

我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。

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

代码示例

public class SkipList {

    private static final float SKIPLIST_P = 0.5f;
    private static final int MAX_LEVEL = 16;

    private int levelCount = 1;

    private Node head = new Node();  // 带头链表

    public Node find(int value) {
        Node p = head;
        for (int i = levelCount - 1; i >= 0; --i) {
            while (p.forwards[i] != null && p.forwards[i].data < value) {
                p = p.forwards[i];
            }
        }

        if (p.forwards[0] != null && p.forwards[0].data == value) {
            return p.forwards[0];
        } else {
            return null;
        }
    }

    public void insert(int value) {
        // 随机索引层数
        int level = randomLevel();

        // 定义新节点
        Node newNode = new Node();
        newNode.data = value;

        //
        Node update[] = new Node[level];
        for (int i = 0; i < level; ++i) {
            update[i] = head;
        }

        // record every level largest value which smaller than insert value in update[]
        Node p = head;
        for (int i = level - 1; i >= 0; --i) {
            while (p.forwards[i] != null && p.forwards[i].data < value) {
                p = p.forwards[i];
            }
            update[i] = p;// use update save node in search path
        }

        // in search path node next node become new node forwords(next)
        for (int i = 0; i < level; ++i) {
            newNode.forwards[i] = update[i].forwards[i];
            update[i].forwards[i] = newNode;
        }

        // update node hight
        if (levelCount < level) levelCount = level;
    }

    public void delete(int value) {
        Node[] update = new Node[levelCount];
        Node p = head;
        for (int i = levelCount - 1; i >= 0; --i) {
            while (p.forwards[i] != null && p.forwards[i].data < value) {
                p = p.forwards[i];
            }
            update[i] = p;
        }

        if (p.forwards[0] != null && p.forwards[0].data == value) {
            for (int i = levelCount - 1; i >= 0; --i) {
                if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {
                    update[i].forwards[i] = update[i].forwards[i].forwards[i];
                }
            }
        }

        while (levelCount > 1 && head.forwards[levelCount] == null) {
            levelCount--;
        }

    }

    // 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。
    // 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。
    // 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :
    //        50%的概率返回 1
    //        25%的概率返回 2
    //      12.5%的概率返回 3 ...
    private int randomLevel() {
        int level = 1;

        while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)
            level += 1;
        return level;
    }

    public void printAll() {
        Node p = head;
        while (p.forwards[0] != null) {
            System.out.print(p.forwards[0] + " ");
            p = p.forwards[0];
        }
        System.out.println();
    }

    public class Node {
        private int data = -1;
        private Node forwards[] = new Node[MAX_LEVEL];
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/837933.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OceanBase上的泡泡玛特抽盒机,轻松应对百倍流量峰值

8月3日晚10点&#xff0c;近百万年轻人再次同时涌入泡泡玛特的抽盒机小程序&#xff0c;参加抢抽盲盒新品的狂欢。 每周四的这个时刻&#xff0c;都是对抽盒机系统的一次技术大考。这个考验不但影响着用户体验&#xff0c;也直接影响着泡泡玛特的业绩。据2022年年度财报&#…

向你推荐这5个好用的UI设计软件,快来收藏

好用的UI设计工具将助力设计师实现高效创作&#xff0c;今天本文将为大家推荐5个好用的UI设计工具&#xff0c;一起来看看吧&#xff01; 1、即时设计 即时设计是国产的UI设计工具&#xff0c;它不仅能为设计师提供UI设计上的帮助&#xff0c;还可以助力设计团队实现一体化协…

[CKA]考试之检查可用节点数量

由于最新的CKA考试改版&#xff0c;不允许存储书签&#xff0c;本博客致力怎么一步步从官网把答案找到&#xff0c;如何修改把题做对&#xff0c;下面开始我们的 CKA之旅 题目为&#xff1a; Task 检查集群中有多少节点为Ready状态&#xff08;不包括被打上 Taint&#xff1…

java中使用Jsoup和Itext实现将html转换为PDF

1.在build.gradle中安装所需依赖&#xff1a; implementation group: com.itextpdf, name: itextpdf, version: 5.5.13 implementation group: com.itextpdf.tool, name: xmlworker, version: 5.5.13 implementation group: org.jsoup, name: jsoup, version: 1.15.32.创建工具…

Java多线程(1)---多线程认识、四种创建方式以及线程状态

目录 前言 一.Java的多线程 1.1多线程的认识 1.2Java多线程的创建方式 1.3Java多线程的生命周期 1.4Java多线程的执行机制 二.创建多线程的四种方式 2.1继承Thread类 ⭐创建线程 ⭐Thread的构造方法和常见属性 2.2.实现Runnable接口 ⭐创建线程 ⭐使用lambda表达…

大一新生必读:如何选择适合自己的笔记本电脑?

大家好&#xff0c;这里是程序员晚枫&#xff0c; 今天给大家推荐5个适合大学生&#xff0c;尤其是大一新生使用的笔记本电脑。都是大品牌&#xff0c;而且价格实惠&#xff0c;性能优秀&#xff01; 偷偷说一句&#xff0c;点击本文链接有大额优惠券哟~ 01 推荐电脑 以下是…

【IC设计】ICC workshop Lab1 数据准备基本流程 【脚本总结】

Task 1 Create a Milkyway library 先进入lab1_data_setup目录&#xff0c;打开icc_shell&#xff0c;创建项目 create_mw_lib -technology $tech_file -mw_reference_library "$mw_path/sc $mw_path/io $mw_path/ram16x128" -bus_naming_style {[%d]} -open $my_m…

100道Python练习题

100道Python练习题&#xff0c;希望对你提升有所帮助&#xff01; 编写一个程序&#xff0c;输入两个数并计算它们的和。编写一个程序&#xff0c;输入一个字符串&#xff0c;并倒序输出该字符串。编写一个程序&#xff0c;判断一个数是否为质数。编写一个程序&#xff0c;计…

“Why Should I Trust You?” Explaining the Predictions of Any Classifier阅读笔记

“Why Should I Trust You?” Explaining the Predictions of Any Classifier阅读笔记 1. 论文贡献2. 背景 [ 1 ] ^{[1]} [1]3. LIME解释单个样本3.1 总体思想3.2 构建可解释的数据表示 [ 1 ] ^{[1]} [1]3.3 可解释性和忠实度的权衡3.4 局部采样3.5 稀疏线性解释3.6 使用SVM进…

C++ 一行代码删除string字符串中的“\n“、“\r“、“\t“ 和 所有空白字符

这篇博客记录如何删除C字符串中的回车、换行、制表符和所有的空白字符&#xff01; 方式一 示例&#xff1a; std::string str "\n\r\t abc \n\t\r cba \r\t\n"; std::cout << str << std::endl; 运行截图&#xff1a; 使用remove_if进行移除…

当我用Python采集全国加盟品牌详情信息,并进行可视化分析后发现了这些

表弟找我说想开个加盟店&#xff0c; 不知道什么品牌好&#xff0c;让我帮他参谋参谋。 还好我会Python&#xff0c;分分钟就获取到了全国加盟品牌信息&#xff0c;稍加分析就筛选出了最适合他的品牌。 话不多说&#xff0c;咱们直接分享干货&#xff01; 准备工作 开发环境…

第三章 API基础

3-1 String类 1、API概述-帮助文档的使用 【1】API概述 【2】如何使用帮助文档 2、键盘录入字符串案例 【1】需求 需求&#xff1a;按照帮助文档的使用步骤学习Scanner类的使用&#xff0c;并实现键盘录入一个字符串&#xff0c;最后输出在控制台 【2】实现 &#xff08;…

SuperNova论文赏析

1. 引言 前序博客有&#xff1a; Nova: Recursive Zero-Knowledge Arguments from Folding Schemes学习笔记 卡内基梅隆大学 Abhiram Kothapalli 和 微软研究中心 Srinath Setty 2022年论文《SuperNova: Proving universal machine executions without universal circuits》…

36.利用解fgoalattain 有约束多元变量多目标规划问题求解(matlab程序)

1.简述 多目标规划的一种求解方法是加权系数法&#xff0c;即为每一个目标赋值一个权系数&#xff0c;把多目标模型转化为一个单目标模型。MATLAB的fgoalattain()函数可以用于求解多目标规划。 基本语法 fgoalattain()函数的用法&#xff1a; x fgoalattain(fun,x0,goal,weig…

计蒜客T1116——验证子串

C实现验证子串的功能:今天复习了一下数据结构的串部分的内容&#xff0c;突然想起来子串匹配的实现&#xff0c;于是计蒜客随便找一道题写一下&#xff0c;核心的代码为裁剪子串和字符串比较两个内容&#xff0c;建议理解背诵&#xff0c;考研大概率会考。 子串裁剪 string Sf…

让这款音频转文字免费软件帮你解放双手吧

嘿&#xff0c;朋友们!你是不是经常遇到这样的场景——你听到一段精彩的演讲、音乐或者语音笔记&#xff0c;却又不方便手动输入文字&#xff1f;别担心&#xff0c;音频转文字技术将解救你出困境&#xff01;说到这&#xff0c;你是不是也对这项技术动心了呢&#xff1f;想知道…

dbfirst下让efcore生成的model继承于一个公共的类

&#xff09;1 Make entities inherit from a base class in Entity Framework Core DB First https://davecallan.com/make-entities-inherit-base-class-in-entity-framework-core-db-first/ 通过EF Core Power Tools vs扩展插件实现 1.1&#xff09;安装扩展Install the…

组织门户支持成员自主公开,快速搭建内容|ModelWhale 版本更新

大暑三秋近&#xff0c;林钟九夏移&#xff0c;ModelWhale 迎来新一轮的版本更新&#xff0c;期待为你带来更好的使用体验。 本次更新中&#xff0c;ModelWhale 主要进行了以下功能迭代&#xff1a; • 新增 门户内容支持成员自主公开&#xff08;团队版✓ &#xff09; • …

uni、css——制作表格样式的模型

案例展示 这里以5列做展示&#xff08;可随意调节&#xff09; 案例代码 <view class"list"><view class"item" v-for"(item,index) in list" :key"index">1</view> <!-- 有内容 --><view clas…

Git 主要命令和操作流程(来自B站黑马)

其中 git 查看日志有好些参数&#xff0c;黑马总结了下&#xff0c;这里记录一下 git 1og--prettyoneline --a11 --graph --abbrev-commit oneline 就是在同一行显示&#xff0c;graph 是以层次关系显示, --abbrev-commit 是查看唯一标识符。那么这么长的命令&#xff0c;每次…