【数据结构与算法】用动图解说数组、链表、跳表原理与实现

2024-04-22 04:18

本文主要是介绍【数据结构与算法】用动图解说数组、链表、跳表原理与实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

「初」前言

在学习数据结构与算法的过程中,感觉真的是一入算法深似海,但是越学越觉得有趣。不过我们会发现在终身学习的过程中,我们都是越学越多,不知的也越来越多,但是更渴望认知更多的知识,越是对知识感兴趣。

本期讲说最常见的数据结构类型分别有数组、链表、跳表。这一期我们一起来了解它们的原理与实现。

「一」数组 Array

  • Java, C++: int a[100]
  • Python: list = []
  • JavaScript: let x = [1, 2, 3]

当今的高级数据语言中,对于数组里面的类型没有严格要求,相对来说比较多元化。

在语言下有一个标准的叫法叫做泛型,也就说任何一个单元类型都可以放入数组。

数组的原理

  • 数组底层的硬件实现是有一个内存管理器的;
  • 当我们向计算机申请数组时,实际上计算机是在内存中给我们开辟了一段连续的地址
  • 每一个地址都是可以通过内存管理进行访问;
  • 无论我们是访问第一个值,还是里面其中一个值,时间复杂度都是常数O(1)
  • 并且可以随意访问任何一个元素,所以它的访问速度非常的快,也是数组的特性之一;

数组的缺陷

数组的问题关键是在增加与删除元素的时候。

数组插入操作

假设现在我们定义了一个[A, B, C, E, F, G]的数组,然后我们要插入一个D到这个数组里面。现在假设我们要把D插入到指针3的位置,我们要怎么实现呢?

  1. 首先我们需要把EFG都挪动到各自的下一个指针;
  2. 然后加入D到指针3上;

详细实现效果请查看下效果图:

因为插入操作的时候,我们需要挪动平均一半的元素(N/2),所以数组每次插入元素时,平均就是O(n)的时间复杂度。

数组删除操作

删除元素也是同理的,假设我们现在有[A, B, C, Z, D, E, F]的一个数组,我们现在需要把Z从这个数组中移除。实现逻辑如下:

  1. 首先把指针3的值置空;
  2. 然后把DEF三个值往上移动一个位置;
  3. 最后在例如Java的数组语言中,我们需要把数组的长度减一即可;

具体的实现效果看下图:

因为删除操作的时候,也是需要挪动平均一半的元素(N/2),所以数组每次删除元素时,平均就是O(n)的时间复杂度。

数组时间复杂度

操作类型时间复杂度
查询上一个 (prepend)O(1)
查询下一个 (append)O(1)
查询某一个元素 (lookup)O(1)
新增结点 (insert)O(N)
删除结点 (delete)O(N)

「二」 链表 Linked List

下来我们一起来看看另外一个数据结构链表。链表的诞生就是为了解决数组的缺点

链表的特性:

  • 每一个元素有两个成员变量valuenext指针(指向下一个元素);
  • 每一个元素串在一起后与数组是非常相似的结构;
  • 与数组不一样的就是每一个元素一般都要定义一个Class):一般都叫一个Node
  • 单链表:只有一个next指针;
  • 双向链表:拥有一个prev或者previous指针指向前一个元素;
  • 头指针用Head来表示,尾指针用Tail来表示;
  • 尾部指针的next指针都会指向一个None(空);
  • 循环链表:尾指针的next指针指向头指针;

链表添加结点

下来我们一起来看看一个链表新添加一个元素的原理:

  1. 首先为新的元素创建一个结点(Node);
  2. 然后我们需要把这个新元素插入到连个元素之间;
  3. 把前一个元素的next指针指向新的Node
  4. 把新元素的next指针指向后一个元素;

具体实现效果看下图:

链表的插入操作总共是2次,但是常数次的,所以时间复杂度为 O(1)

链表删除结点

接下来我们一起来看看删除结点的原理,删除与新增大致上是一样的,是

  1. 在需要把删除的结点的前一个nodenext,改为删除结点的下一个node

具体的实效效果看下图:

链表的删除操作只需要一次,所以时间复杂也是O(1)

链表时间复杂度

通过分析链表的新增和删除操作,我们发现链表中并没有像数组一样需要挪动一半或者多个的元素的位置和复制元素等。也是因为这样它的移动和修改操作的效率非常高为O(1)。 但是在查询的时候,当我们需要访问链表中某一个值的时候,就相对变得复杂了,为O(N)

我们来看看一下的链表时间复杂度:

操作类型时间复杂度
查询上一个 (prepend)O(1)
查询下一个 (append)O(1)
查询某一个元素 (lookup)O(N)
新增结点 (insert)O(1)
删除结点 (delete)O(1)

看完ArrayLinked List的两种数据结构的特性后,我们可以发现是没有完美的数据结构的。如果有完美的那就不需要Array或者Linked List并存了。所以我们需要看场景来决定我们需要用那种数据结构。

「三」跳表 Skip List

后续有技术科学家对链表进行了优化,诞生出第三个数据结构叫做跳表(Skip List)。跳表可能有些小伙伴没有怎么接触过,但是其实它一直都在我们身边的应用中使用。在Redis里面就使用了跳表。不过面试过程中并不会给大家出跳表的题目来写程序,所以我们只需要理解它的原理即可。

跳表的核心是为了优化链表元素随机访问时间复杂度过高的问题 (O(n))。

这个优化的中心思想其实是贯穿于整个算法数据结构,甚至也贯穿于整个数学与物理的世界。那就是升维思想 / 空间换时间 - 顾名思义就是在原有的链表中添加第二维的链表叫第一级索引

添加第一级索引

我们看看下面图什么是一级索引

  • 首先索引的第一个索引指向头 (head),也就是第一个元素 (1)
  • 然后索引的下一个元素指向的就是next + 1,也就是第三个元素 (4)
  • 换句话来说,就是第一级索引的元素比原始链表走快2倍的速度

假设现在我们需要访问结点7,添加了这个索引后,是怎么提高了访问速度呢?我们来看看下面的图:

  • 首先从第一级索引中走到索引7;
  • 然后从索引7下来找到第7个结点;
  • 这里总共的步数4步降到2步就能找到第7个结点;

虽然说速度是快了,但是能不能更快呢?可以的,只需要我们再叠加维度,用空间换时间的中心思想即可

添加第二级索引

第二级索引比第一级的索引再走快一步,那就是每次走两步,也就是next+2。这样访问结点的时候就更快了。首先我们来看看加入第二级索引后的结构图:

  • 同理二级索引的第一个是指向一级索引的第一个,最终指向的是头 (head);
  • 二级索引的第个人结点指向的就是结点7,因为二级索引是next+2每次跳3步的进行步伐;

加入了二级索引后,我们访问结点7的时候是怎么样的呢?

  • 维度升级到第二级时,只需要1步就能到达结点7的索引;
  • 加入二级索引后,我们从4步降到1步完成结点7的访问;

所以清晰看到,当我们升级多一层的维度后,链表的访问速度也会相对应的提升。也就是说,在一个非常长的链表中,我们可以加入N级索引,也就是提高N层的维度就可以提高这个链表访问的速度。总体来说我们就是需要添加log2n个级索引,来达到最高级索引维度。

跳表查询的时间复杂度分析

  • 首先每一级索引我们提升了2倍的跨度,那就是减少了2倍的步数,所以是n/2、n/4、n/8以此类推
  • 第 k 级索引结点的个数就是 n/(2^k)
  • 假设索引有 h 级, 最高的索引有2个结点;
  • n/(2^h) = 2, 从这个公式我们可以求得 h = log2(n)-1
  • 所以最后得出跳表的时间复杂度是O(log n)

跳表查询的空间复杂度分析

  • 首先原始链表长度为n
  • 如果索引是每2个结点有一个索引结点,每层索引的结点数:n/2, n/4, n/8 … , 8, 4, 2 以此类推;
  • 或者所以是每3个结点有一个索引结点,每层索引的结点数:n/3, n/9, n/27 … , 9, 3, 1 以此类推;
  • 所以空间复杂度是O(n)

跳表现实中的形态

来源于覃超老师的PPT
  • 在现实使用中,链表的索引并不是那么整齐和有规则的;
  • 这个是因为在元素增加与删除的过程中会有所变化
  • 最后经过多次改动之后,有一些索引会跨步多几步或者少哭跨几步
  • 而且维护成本相对要高 - 新增或者删除时需要把所有索引都更新一遍;
  • 最后在新增和删除的过程中的更新,时间复杂度也是O(log n)

升维思想和空间换时间的思维,我们一定要记下来,并且融会贯通。后面在解决相应的面试题的时候我们会经常用到这种思维。比如:树,二叉搜索树等经常用高级数据库结构。

「四」工程中的应用

链表在日常工程中其实应用是很多的,但是因为这些都属于高级的数据结构了,无论是Java也好、C++、JavaScript还是Go语言,这些语言里面都提供了封装好的数据结构,我们只需要直接使用就可以了

链表的应用

链表最常见的一个应用就是LRU Cache,没有接触过的小伙伴,可以百度一下深挖一下。然后这里附上一道Leetcode的题目[面试题 16.25. LRU缓存,这道题的话使用双链表就可以实现。有兴趣的小伙伴可以尝试实现。

跳表的应用

跳表的话在Redis中就有应用到。 想了解更多的小伙伴可以搜索Redis的跳跃表进深挖。

「终」总结

  • 数据结构
    • 数组:随机查询快 O(1),但是删除与插入较慢 O(n)
    • 链表:删除与插入快 O(1),但是随机查询慢 O(n)
    • 跳表:为了提高链表的随机查询而生的,随机查询能提升到 O(log n),但是维护成本高
  • 思维重点
    • 升维思想 + 空间换时间
  • 应用
    • 链表:LRU Cache
    • 跳表:Redis

我是三钻,一个在技术银河中等和你们一起来终身漂泊学习。
点赞是力量,关注是认可,评论是关爱!下期再见 👋!

公众号《技术银河》回复"算法资料",可以获得这个系列文章的PDF版其他资料

推荐专栏

小伙伴们可以查看或者订阅相关的专栏,从而集中阅读相关知识的文章哦。

  • 📖 《数据结构与算法》 — 到了如今,如果想成为一个高级开发工程师或者进入大厂,不论岗位是前端、后端还是AI,算法都是重中之重。也无论我们需要进入的公司的岗位是否最后是做算法工程师,前提面试就需要考算法。

  • 📖 《FCC前端集训营》 — 根据FreeCodeCamp的学习课程,一起深入浅出学习前端。稳固前端知识,一起在FreeCodeCamp获得证书

  • 📖 《前端星球》 — 以实战为线索,深入浅出前端多维度的知识点。内含有多方面的前端知识文章,带领不懂前端的童鞋一起学习前端,在前端开发路上童鞋一起燃起心中那团火🔥

推荐课程

数据结构核心原理与算法应用

这篇关于【数据结构与算法】用动图解说数组、链表、跳表原理与实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/924801

相关文章

Vue中动态权限到按钮的完整实现方案详解

《Vue中动态权限到按钮的完整实现方案详解》这篇文章主要为大家详细介绍了Vue如何在现有方案的基础上加入对路由的增、删、改、查权限控制,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、数据库设计扩展1.1 修改路由表(routes)1.2 修改角色与路由权限表(role_routes)二、后端接口设计

C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)

《C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)》本文主要介绍了C#集成DeepSeek模型实现AI私有化的方法,包括搭建基础环境,如安装Ollama和下载DeepS... 目录前言搭建基础环境1、安装 Ollama2、下载 DeepSeek R1 模型客户端 ChatBo

Spring Cloud Hystrix原理与注意事项小结

《SpringCloudHystrix原理与注意事项小结》本文介绍了Hystrix的基本概念、工作原理以及其在实际开发中的应用方式,通过对Hystrix的深入学习,开发者可以在分布式系统中实现精细... 目录一、Spring Cloud Hystrix概述和设计目标(一)Spring Cloud Hystr

Qt实现发送HTTP请求的示例详解

《Qt实现发送HTTP请求的示例详解》这篇文章主要为大家详细介绍了如何通过Qt实现发送HTTP请求,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、添加network模块2、包含改头文件3、创建网络访问管理器4、创建接口5、创建网络请求对象6、创建一个回复对

C++实现回文串判断的两种高效方法

《C++实现回文串判断的两种高效方法》文章介绍了两种判断回文串的方法:解法一通过创建新字符串来处理,解法二在原字符串上直接筛选判断,两种方法都使用了双指针法,文中通过代码示例讲解的非常详细,需要的朋友... 目录一、问题描述示例二、解法一:将字母数字连接到新的 string思路代码实现代码解释复杂度分析三、

grom设置全局日志实现执行并打印sql语句

《grom设置全局日志实现执行并打印sql语句》本文主要介绍了grom设置全局日志实现执行并打印sql语句,包括设置日志级别、实现自定义Logger接口以及如何使用GORM的默认logger,通过这些... 目录gorm中的自定义日志gorm中日志的其他操作日志级别Debug自定义 Loggergorm中的

Spring Boot整合消息队列RabbitMQ的实现示例

《SpringBoot整合消息队列RabbitMQ的实现示例》本文主要介绍了SpringBoot整合消息队列RabbitMQ的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录RabbitMQ 简介与安装1. RabbitMQ 简介2. RabbitMQ 安装Spring

Gin框架中的GET和POST表单处理的实现

《Gin框架中的GET和POST表单处理的实现》Gin框架提供了简单而强大的机制来处理GET和POST表单提交的数据,通过c.Query、c.PostForm、c.Bind和c.Request.For... 目录一、GET表单处理二、POST表单处理1. 使用c.PostForm获取表单字段:2. 绑定到结

springMVC返回Http响应的实现

《springMVC返回Http响应的实现》本文主要介绍了在SpringBoot中使用@Controller、@ResponseBody和@RestController注解进行HTTP响应返回的方法,... 目录一、返回页面二、@Controller和@ResponseBody与RestController

nginx中重定向的实现

《nginx中重定向的实现》本文主要介绍了Nginx中location匹配和rewrite重定向的规则与应用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下... 目录一、location1、 location匹配2、 location匹配的分类2.1 精确匹配2