【追求卓越10】算法--跳表

引导

        在上一节中,我们学习到二分查找,惊叹于它超高的效率(时间复杂度为O(logn))。但是二分查找有一个局限性就是依赖于数组,这就导致它应用并不广泛。

        那么适用链表是否可以做到呢?答案是可以的。只不过要复杂一点,算法中称为跳表。应用很广泛。

跳表

        跳表的实质就是通过多级索引结构加快查找速度。这么说可能不理解,我们通过下面一系列图来进行理解。

        我们在链表中查找一个元素的时间复杂度时O(n),这个我们在链表章节已经说明了。但是如何能够提高查找的效率呢?如果加上索引是否会快一些。

        如上图所示,我们每两个节点添加一个索引。通过这样的结构,查找13这个元素。原先需要9次比较操作,现在只需要5次。是不是减少了很多?但是当数据量n很大时,每两个节点引出一个索引,那么第一级索引就有n/2个节点。这样在第一级索引中也会消耗很多时间。

有什么好的解决方式吗?--多级索引

        通过添加多级索引,能够加快我们的查找速度,这就是跳表。

跳表的效率

        我们通过上面的图,了解到跳表的确能够提高我们的查找速度,但是它的时间复杂度时多少呢?我们继续上面的例子来分析。

        假设我们原始链表的长度为n,那么第一即的索引个数就是n/2,第二级索引个数就是n/4,...第k级索引个数就是n/2^k。

每层索引最多查找3次。故该跳表的最差情况的时间复杂度是O(3*logn)=O(logn)。

其实我们知道这也是典型的空间换时间的方式。这么多的索引岂不是很浪费内存?

跳表浪费内存吗?

答案是肯定的,这么多的索引节点,当然会浪费内存。根据上面的跳表,我们也可以计算出空间复杂度为O(n-2)。但是这种空间的浪费重要吗?或者说有什么方式可以缩减这个空间消耗呢?

  1. 通过跳表原理的介绍,我们知道索引节点只需要存储几个指针和关键值。相对于原始数据中可能是很大的对象,这个就可以忽略了。
  2. 我们也可以增加节点间隔来产生索引,比如10个节点产生一个索引。你可以尝试计算一下空间复杂度

插入删除

        我们知道链表的插入删除操作的时间复杂度是O(1),但无奈于插入删除之前有查找操作,这就导致实际中我们想要删除或插入一个节点的时间复杂度是O(n)。现在跳表查找的时间复杂度是O(logn),那么我们插入删除的炒作也就是O(logn)。

插入操作:

插入一个数据变得更加高效了。如果只在原始链表中进行数据的插入,不对索引进行更新,就会出现下面的问题:

这样复杂度容易退化O(n)。因此跳表的删除,插入操作一定要更新索引表,用来保持这种平衡:当原始数据多了,就适当增加索引的节点。避免复杂度退化。

跳表平衡的维护

一般这种平衡性是通过随机函数来维持的。一般常用的方式是抛硬币法。我将会在代码中进行解析。

#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define MAXLEVEL 15
typedef struct node{
    int val;
    struct node * level[MAXLEVEL];
}Node;
int initslist(Node* head)
{
    head->val = -1;
    memset(head->level,0,sizeof(Node*)*MAXLEVEL);
    return 0;
}
int random_level(void)
{
    int i, level = 1;
    for (i = 1; i < MAXLEVEL; i++)
            if (random() % 2 == 1)
                    level++;
    return level;
}
insertslist(Node* head,int val)
{
    int level = random_level();
    Node* new = (Node*)malloc(sizeof(Node));
    new->val = val;
    memset(new->level,0,sizeof(Node*)*MAXLEVEL);
    Node* p = head;
    Node* update[MAXLEVEL] = {0};

    int i = 0;
    for(i = level - 1 ; i >= 0 ; i--)
    {
        while(p->level[i] && p->level[i]->val < val)
            p = p->level[i];
        update[i] = p;
    }
    for(i = 0 ; i < level ; i++)
    {
        new->level[i] = update[i]->level[i];
        update[i]->level[i] = new;
    }
}
Node * findslist(Node* head,int target)
{
    Node * p = head;
    int i = MAXLEVEL - 1;
    for( ; i >= 0 ; i--)
    {
            while(p->level[i] && p->level[i]->val < target)
            {
                    p = p->level[i];
            }
    }
    if(p->level[0] && p->level[0]->val == target)
        return p->level[0];
    else
    return NULL;
}
int deleteslist(Node* head, int target)
{
    Node * p = head;
    int i = MAXLEVEL - 1;
    Node* update[MAXLEVEL] = {0};
    for(; i >= 0 ; i--)
    {
        while(p->level[i] && p->level[i]->val < target)
            p = p->level[i];
       update[i] = p;
    }
    if(update[0]->level[0] == NULL || update[0]->level[0]->val != target)
        return -1;

    for(i = MAXLEVEL -1  ; i >= 0 ; i--)
    {
        if(update[i]->level[i] && update[i]->level[i]->val == target)
          {
                update[i]->level[i] = update[i]->level[i]->level[i];
          }
    }
   

    return 0;
}
int printslist(Node* head)
{
    Node * p = head;
    int i = MAXLEVEL - 1;
    for(; i >= 0 ; i -- )
    {
        p = head;
        printf("Level[%02d]",i);
        while(p->level[i])
        {
            printf(" %4d (%p)",p->level[i]->val,p->level[i]);
            p = p->level[i];
        }
        printf("\n");
    }
    printf("\n");
}
int main()
{
    Node head;
    initslist(&head);
    srandom(time(NULL));
    int i =0;
    for(i = 20 ; i < 30 ; i++)
    insertslist(&head,i);
    printslist(&head);
    Node* temp = findslist(&head,25);
    printf("temp = %p\n",temp);
    deleteslist(&head,25);
    printslist(&head);
    return 0;
}

总结

        本节我们接触到了跳表这种数据结构。它的查找,删除,插入操作时间复杂度都是O(logn),并且不依赖于数组,因而它的应用场景更加广泛。

跳表是利用空间换时间的方式得到更高的效率。

        跳表在插入删除操作的时候不仅仅要对原始链表进行处理,也要更新索引结构,以保持两者的平衡,避免复杂度退化。保持平衡的方法一般用“抛硬币”方式。

        跳表的实现并不简单,需要好好琢磨并练习。

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

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

相关文章

【程序员的自我修养03】深入了解ELF文件格式

绪论 大家好&#xff0c;欢迎来到【程序员的自我修养】专栏。正如其专栏名&#xff0c;本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度&#xff0c;我会尽自己最大的努力&#xff0c;争取…

嵌入式Linux:ARM驱动+QT应用+OpenCV人脸识别项目实现

一、前言&#xff1a; 这个项目主要分为两部分&#xff0c;客户端&#xff08;ARM板端&#xff09;负责利用OpenCV采集人脸数据&#xff0c;利用TCP将人脸数据发送给服务器&#xff0c;然后服务器根据人脸数据进行人脸识别&#xff0c;将识别后的结果返还给客户端&#xff0c;客…

请大数据把奥威BI分析工具推给每一个财务!

这个财务指标怎么算&#xff1f;那些数据什么时候能拿到&#xff1f;看完报表&#xff0c;发现某部门上个月的支出涨幅过大&#xff0c;想了解原因怎么办&#xff1f;……财务人&#xff0c;你是不是每个月都把时间消耗在这些事情上了&#xff1f;那你可得快接住这个BI大数据分…

网站文章采集软件大盘点

在信息时代&#xff0c;随着互联网的不断发展和普及&#xff0c;获取、整理和利用海量信息成为各行业的共同挑战。在这个背景下&#xff0c;网站文章采集技术应运而生&#xff0c;成为满足信息需求的重要工具。本文将对网站文章采集及其相关软件进行深入探讨&#xff0c;为读者…

Golang语言基础之切片

概述 数组的长度是固定的并且数组长度属于类型的一部分&#xff0c;所以数组有很多的局限性 func arraySum(x [3]int) int{sum : 0for _, v : range x{sum sum v}return sum } 这个求和函数只能接受 [3]int 类型&#xff0c;其他的都不支持。 切片 切片&#xff08;Slic…

virustotal的使用

www.virustotal.com是一个恶意代码扫描网站&#xff0c;提交时需要验证码。 该网站有近百个病毒引擎的支持。 该网站最有用的地方在于&#xff0c;这是一个交互式的恶意代码检测网站&#xff0c;这样的模式有一个隐形的福利&#xff0c;那就是为病毒木马爱好者提供了攻防一体…

市面上这么多SD-WAN服务商,究竟有何不同?

随着数字化浪潮的不断发展&#xff0c;企业网络已经成为了现代企业中不可缺少的一部分。而提供企业组网服务的SD-WAN服务商也呈现出快速增长的趋势。但是&#xff0c;市场上有这么多SD-WAN服务商&#xff0c;各个服务商技术实现方案非常相似&#xff0c;那么这些服务商之间到底…

人工智能驱动的医疗辅助:陪诊系统的技术原理与应用

随着人工智能技术的不断发展&#xff0c;医疗领域也迎来了新的可能性。本文将深入探讨陪诊系统的技术原理及其在医疗领域中的应用。我们将重点关注人工智能的核心概念&#xff0c;如自然语言处理、机器学习和语音识别&#xff0c;以解释陪诊系统是如何在医疗环境中发挥作用的。…

html5各行各业官网模板源码下载(1)

文章目录 1.来源2.源码模板2.1 HTML5白色简洁设计师网站模板 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/134682321 html5各行各业官网模板源码下载&#xff0c;这个主题覆盖各行业的html官网模板&#xff0c;效果模…

图解Redis适用场景

Redis以其速度而闻名。 1 业务数据缓存 1.1 通用数据缓存 string&#xff0c;int&#xff0c;list&#xff0c;map。Redis 最常见的用例是缓存对象以加速 Web 应用程序。 此用例中&#xff0c;Redis 将频繁请求的数据存储在内存。允许 Web 服务器快速返回频繁访问的数据。这…

Make sure bypassing Vue built-in sanitization is safe here.

一、问题描述 二、问题分析 XSS(跨站脚本攻击) XSS攻击通常指的是通过利用网页开发时留下的漏洞&#xff0c;通过巧妙的方法注入恶意指令代码到网页&#xff0c;使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript&#xff0c;但实际上也可以包括J…

【注册表】Sublime Text添加到右键菜单

官网下载 windows下地地址: http://www.sublimetext.com/download_thanks?targetwin-x64设置右键菜单和菜单小图标 win R打开运行&#xff0c;并输入regedit打开注册表编辑器依次找到HKEY_CLASSESS_ROOT -> * -> Shell&#xff0c;下面新建项&#xff0c; 这个项的名…

【TinyALSA全解析(三)】tinyplay、tincap、pcm_open源码解析

tinyplay、tincap、pcm_open源码解析 一、本文的目的二、tinyplay.c源码分析三、tinycap.c源码分析四、pcm.c如何调度到Linux Kernel4.1 pcm_open解析4.1.1 pcm_open的主要流程4.1.2 流程说明4.1.3 调用方法 4.2 pcm_write解析 /*********************************************…

图解系列--HTTPS,认证

确保 Web 安全的HTTPS 1.HTTP 的缺点 1.1.通信使用明文可能会被窃听 加密处理防止被窃听 加密的对象可以有这么几个。 (1).通信的加密 HTTP 协议中没有加密机制&#xff0c;但可以通过和 SSL&#xff08;Secure Socket Layer&#xff0c;安全套接层&#xff09;或TLS&#xff…

猜-MISC-bugku-解题步骤

——CTF解题专栏—— 题目信息&#xff1a; 题目&#xff1a;猜 作者&#xff1a;harry 提示&#xff1a; 解题附件&#xff1a;flag格式key{图中人物名字全拼} 解题思路&#xff1a; 这......头都没有&#xff0c;让我guess&#xff1f;&#xff1f;&#xff1f;详细信息看…

NASM安装和结合nodepad++进行编译的过程

mov ax,0x30 mov bx,0xc0 add ax,bx times 502 db 0 db 0x55 db 0xAA nasm安装地址: https://www.nasm.us/ 下载exe安装 在命令行提示符输入nasm编译命令 nasm exam.asm -f bin -o exam.bin 此时输入回车将会执行编译过程。 1&#xff0c;启动NotePad&#xff0c;在菜单上选…

web前端tips:js继承——寄生组合式继承

上篇文章给大家分享了 js继承中的 寄生式继承 web前端tips&#xff1a;js继承——寄生式继承 今天给大家分享一下 js 继承中的 寄生组合式继承 寄生组合式继承 寄生组合式继承是一种结合了寄生式继承和组合式继承的方式&#xff0c;它的目标是减少组合式继承中多余的调用父…

【Java 并发编程】进程线程、lock、设计模式、线程池...

博主&#xff1a;_LJaXi Or 東方幻想郷 专栏&#xff1a; Java | 从入门到入坟 Java 并发编程 并发编程多线程的入门类和接口线程组和线程优先级线程的状态及主要转化方法线程间的通信重排序和 happens-beforevolatilesynchronized 与锁CAS 与原子操作AQS计划任务Stream 并行计…

开放式耳机哪个品牌好用?超好用的耳机推荐,新手小白必看

在当今数不胜数的音频品牌中&#xff0c;寻找一款优秀的开放式耳机成为了许多音乐爱好者和新手小白的共同关注点&#xff0c;开放式耳机以其通透的音质和舒适的佩戴感受受到了广泛好评&#xff0c;但市场上的选择却让人眼花缭乱&#xff0c; 为了帮助新手小白在这个耳机的海洋…

从零开始:打造自己的抖音核销工具小程序

对于商家而言&#xff0c;如何高效核销活动中的抖音优惠券成为一项挑战。在这篇文章中&#xff0c;我们将探讨如何从零开始&#xff0c;打造一个个性化、高效的抖音核销工具小程序。 第一步&#xff1a;明确需求和目标 在动手之前&#xff0c;我们需要明确自己的需求和目标。…