算法学习:数组 vs 链表

在这里插入图片描述

🔥 个人主页:空白诗

在这里插入图片描述

文章目录

    • 🎯 引言
    • 🛠️ 内存基础
      • 什么是内存❓
      • 内存的工作原理 🎯
    • 📦 数组(Array)
      • 📖 什么是数组
      • 🌀 数组的存储
      • 📝 示例代码(go语言)
        • 🎯 执行增加操作
        • 🎯 执行删除操作
      • 📊 优缺点分析
    • 🔗 链表(Linked List)
      • 📖 什么是链表
      • 🌀 链表的存储
      • 📝 示例代码(go语言)
      • 📊 优缺点分析
    • 📊 数组与链表的对比
      • 🎯 访问速度
      • 🎯 插入与删除效率
      • 🎯 空间利用效率
      • 🎯 应用场景
    • 📚 小结


🎯 引言

在编程的奇妙世界里,数组和链表作为两种基础且重要的数据结构,各自扮演着不可替代的角色。它们在存储和管理数据方面展现出了不同的优势和局限。本文将带领你深入了解数组(Array)链表(Linked List)的奥秘🚀


🛠️ 内存基础

什么是内存❓

内存,尤其是随机存取存储器(RAM),是计算机中用于临时存储数据和程序指令的部分。与硬盘相比,内存访问速度快,但信息非持久保存。

想象一下,当你在解决一个复杂的算法问题时,那些数字、字符,乃至复杂的数据结构,都需要一个地方暂时停留和操作——这个地方就是内存

内存的工作原理 🎯

内存由一系列连续或非连续的存储单元组成,每个单元都有一个独一无二的地址。通过地址,CPU(中央处理器)可以迅速找到所需的数据。

就好比内存是一个储物柜,你将东西放进去后会给你一个号码,通过号码你可以快速找到你存储物品的柜子。

在这里插入图片描述

需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址。需要存储多项数据时,有两种基本方式——数组和链表


📦 数组(Array)

📖 什么是数组

数组是一种线性数据结构它将元素按照一定的顺序存储在一块连续的内存区域中。每个元素都有一个索引(从0开始),通过索引可以快速访问数组中的任意元素。但是对于插入和删除,特别是当位置不在末尾时,可能需要移动后续的所有元素,以保持连续性,导致最坏情况下的时间复杂度为O(n)。

+---+---+---+---+
| 1 | 2 | 3 | 4 |
+---+---+---+---+^           ^|           |索引0       索引3

🌀 数组的存储

数组在创建时会一次性申请足够的内存空间进行存储。这意味着数组的大小是固定的,一旦声明,不能轻易改变。

如果需要在数组中添加新元素很麻烦,因为数组必须是连续的。比如你和3个朋友一起去看电影,已经选好了连坐的4个位置并且付款。此时又有一个朋友要与你们一起看,你们想要连排座,但是你们已经选好的这4个位置旁边没有了空位,所以你们只能放弃这4个位置进行退款,然后重新选择有5个连坐的位置。如果又来了一位朋友,而当前坐的地方也没有空位,你们就得再次转移!真是太麻烦了。如果没有了空间,就得移到内存的其他地方,因此添加新元素的速度会很慢。

而此时如果中间某个人不看电影了,那么后面的人就需要向前靠拢和大家坐在一起,即在数组中删除元素,就需要移动后面的所有元素。

在这里插入图片描述
如上图,此时如果再来一个人,只能舍弃原本的四个位置,去重新找有五个连续的位置。如果此时’王五‘不看电影了,那右边的赵六就需要向左边坐一个位置与大家靠拢。

📝 示例代码(go语言)

package mainimport "fmt"func main() {// 定义一个整型切片arr := []int{1, 2, 3, 4, 5}temp := []int{10, 11}// 遍历切片并打印每个元素及其地址for i := range arr {fmt.Printf("Element: %d, Address: %p\n", arr[i], &arr[i])}for i := range temp {fmt.Printf("temp: %d, Address: %p\n", temp[i], &temp[i])}// 添加一个元素arr = append(arr, 6)// 删除索引为3的元素(值为4)//arr = append(arr[:3], arr[4:]...)// 	遍历切片并打印每个元素及其地址fmt.Printf("-----\n")for i := range arr {fmt.Printf("Element: %d, Address: %p\n", arr[i], &arr[i])}for i := range temp {fmt.Printf("temp: %d, Address: %p\n", temp[i], &temp[i])}
}

在Go语言中,数组和切片的处理方式有所不同。首先,理解一下基本概念:

  • 数组:固定大小的元素序列,分配一块连续的内存。
  • 切片:是对数组的一个引用,包含指向底层数组的指针、长度和容量信息。切片本身是轻量级的,修改切片(如追加、删除)操作可能引起底层数组的重新分配。

这段代码中,arr 是一个切片,但它的初始化方式 [1, 2, 3, 4, 5] 实际上创建了一个底层数组,并用这个数组来初始化切片。而 temp 同样是一个基于数组初始化的切片。

假设初始时,arr 的底层数组在内存中的布局如下(简化表示):

| arr[0]: 1 | arr[1]: 2 | arr[2]: 3 | arr[3]: 4 | arr[4]: 5 |

每个元素旁边标注的是其值和大致的内存地址(实际地址会更复杂,但这里为了简化说明)。注意,&arr[i] 获取的是元素的地址,对于切片中的元素,这实际上是底层数组中相应元素的地址。

🎯 执行增加操作
Element: 1, Address: 0x14000016180
Element: 2, Address: 0x14000016188
Element: 3, Address: 0x14000016190
Element: 4, Address: 0x14000016198
Element: 5, Address: 0x140000161a0
temp: 10, Address: 0x1400000e0a0
temp: 11, Address: 0x1400000e0a8
-----
Element: 1, Address: 0x1400001c140
Element: 2, Address: 0x1400001c148
Element: 3, Address: 0x1400001c150
Element: 4, Address: 0x1400001c158
Element: 5, Address: 0x1400001c160
Element: 6, Address: 0x1400001c168
temp: 10, Address: 0x1400000e0a0
temp: 11, Address: 0x1400000e0a8

如上输出结果所示,增加元素时数组的地址全部发生了变化。

在Go语言中,当你对切片(slice)执行append操作时,如果切片的容量(cap)不足以容纳新的元素,Go会执行以下步骤:

  1. 检查容量: 首先,Go检查切片的当前容量是否足够容纳新元素。如果足够,切片会在原地扩展,也就是直接在现有底层数组的末尾添加新元素,此时原有元素的地址不会改变。

  2. 容量不足时的处理: 如果当前切片的容量不足以容纳新元素,Go会创建一个新的、容量更大的底层数组。然后,它会将原切片中的所有元素复制到新数组中,再在新数组的末尾追加新元素。这意味着所有元素都会被移动到新的内存位置,因此它们的地址会改变。

在代码示例中,由于初始时没有明确指定切片的容量,切片会有一个默认的容量。当你调用append添加第六个元素时,如果这个操作导致需要更多空间超出了切片的当前容量,Go就会执行上述的第二步,即创建新的底层数组并复制元素。因此,追加元素后你会观察到每个元素的地址都发生了变化因为它们都被移到了新的内存位置上

总结来说,切片追加元素后地址变化的原因在于添加操作导致了底层数组的重新分配,从而引发了元素地址的更新。

🎯 执行删除操作
Element: 1, Address: 0x140000b6030
Element: 2, Address: 0x140000b6038
Element: 3, Address: 0x140000b6040
Element: 4, Address: 0x140000b6048
Element: 5, Address: 0x140000b6050
temp: 10, Address: 0x140000a4020
temp: 11, Address: 0x140000a4028
-----
Element: 1, Address: 0x140000b6030
Element: 2, Address: 0x140000b6038
Element: 3, Address: 0x140000b6040
Element: 5, Address: 0x140000b6048
temp: 10, Address: 0x140000a4020
temp: 11, Address: 0x140000a4028

如上输出结果所示,当删除数组一个元素时(此时删除了索引为3的元素),后续数组的所有元素都向前移动。

当执行 arr = append(arr[:3], arr[4:]...) 这行代码时,Go的切片操作实际上做了以下几步:

  1. 切片操作:首先,它创建了两个新的切片,一个包含从开始到索引3(不包括3)的元素,另一个包含从索引4开始到最后的元素。
  2. 合并与重新分配:然后,使用 append 函数将这两个切片的内容合并。由于原切片的连续性被打破(需要“跳过”索引3的元素),append 可能会检查当前切片的容量是否足够存放新数据。如果不够,它可能会分配一个新的足够大的底层数组来存储合并后的结果;如果当前切片的剩余容量足够,则直接在原有底层数组的基础上进行操作。

删除元素并重新分配内存后,arr 中剩余元素的地址发生了改变,因为它们现在位于一个全新的、连续的内存区域。当打印出每个元素的地址时,你会发现从原来索引3之后的所有元素的地址相比之前都“向前移动”了,这是因为它们现在位于一个起始位置更早的连续块中。

而对于 temp 切片,因为它没有进行任何删除或添加操作,所以其元素的地址保持不变。每次打印 temp 的元素地址时,你会看到相同的地址输出,因为这部分内存没有被重新分配。

总之,删除切片中的元素并导致元素地址“向前移动”的根本原因,在于append操作可能触发的底层数组的重新分配和数据复制到新位置的过程,以维持切片元素的连续性。

📊 优缺点分析

  • 优点:

    • 随机访问: 直接通过索引访问,时间复杂度为O(1)。
    • 简单易用: 大多数编程语言内置支持,易于理解和实现。
  • 缺点:

    • 插入与删除: 在数组中插入或删除元素需要移动元素,最坏情况下时间复杂度为O(n)。
    • 固定大小限制: 传统数组大小固定,动态数组虽然可以自动扩容,但在扩容时可能会导致性能开销。

🔗 链表(Linked List)

📖 什么是链表

链表也是一种线性数据结构,但与数组不同,链表中的元素在内存中并不是顺序存放的,而是通过存在元素中的指针链接起来。每个链表节点包含两个部分:数据域指针域

链表访问某个元素需要从头节点开始,沿着指针一步步遍历,最坏情况下时间复杂度为O(n),意味着数据越大,查找越慢。但是在插入和删除操作上链表表现出色,特别是在链表的头部或尾部进行时,只需调整相邻节点的指针即可,时间复杂度为O(1),即使在中间操作,也仅需改动少量指针,避免了大量数据移动。

🌀 链表的存储

链表中的元素可存储在内存的任何地方,因为链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。

在这里插入图片描述

这犹如寻宝游戏。你前往第一个地址,那里有一张纸条写着“下一个元素的地址为 123”。因此,你前往地址 123,那里又有一张纸条,写着“下一个元素的地址为 847”,以此类推。在链表中添加元素很容易:只需将其放入内存,并将其地址存储到前一个元素中。

因此使用链表时,根本就不需要移动元素,只要有足够的内存空间,就能为链表分配内存。

📝 示例代码(go语言)

package mainimport ("fmt"
)// ListNode 定义链表节点
type ListNode struct {Value intNext  *ListNode
}// LinkedList 定义链表结构
type LinkedList struct {Head *ListNode
}// NewListNode 创建新节点
func NewListNode(value int) *ListNode {return &ListNode{Value: value}
}// Append 向链表末尾追加节点
func (list *LinkedList) Append(value int) {newNode := NewListNode(value)if list.Head == nil {list.Head = newNode} else {current := list.Headfor current.Next != nil {current = current.Next}current.Next = newNode}
}// Delete 删除链表中第一个匹配值的节点
func (list *LinkedList) Delete(value int) {if list.Head == nil {return}if list.Head.Value == value {list.Head = list.Head.Nextreturn}current := list.Headfor current.Next != nil && current.Next.Value != value {current = current.Next}if current.Next != nil {current.Next = current.Next.Next}
}// PrintListWithAddresses 打印链表节点值和地址
func (list *LinkedList) PrintListWithAddresses() {current := list.Headfor current != nil {fmt.Printf("Value: %d, Address: %p -> ", current.Value, current)current = current.Next}fmt.Println("nil")
}func main() {// 创建链表实例linkedList := &LinkedList{}// 增加节点linkedList.Append(1)linkedList.Append(2)linkedList.Append(3)fmt.Println("增加前:")linkedList.PrintListWithAddresses()// 删除索引为1的元素(值为2)linkedList.Delete(2)fmt.Println("删除后:")linkedList.PrintListWithAddresses()// 再次增加节点linkedList.Append(4)fmt.Println("增加后:")linkedList.PrintListWithAddresses()
}
增加前:
Value: 1, Address: 0x1400010a240 -> Value: 2, Address: 0x1400010a250 -> Value: 3, Address: 0x1400010a260 -> nil
删除后:
Value: 1, Address: 0x1400010a240 -> Value: 3, Address: 0x1400010a260 -> nil
增加后:
Value: 1, Address: 0x1400010a240 -> Value: 3, Address: 0x1400010a260 -> Value: 4, Address: 0x1400010a270 -> nil

这是一个简单的Go语言示例,模拟演示了链表的创建、增加节点、删除节点以及输出节点值和地址的操作。从上述输出结果可以看见,不管是增加还是删除,改变的只有元素的指向,并没有修改其内存地址,删除也没有移动其他元素的内存地址。

📊 优缺点分析

  • 优点:

    • 动态大小: 链表的长度可以在运行时动态改变,无需担心预先分配内存的问题。
    • 高效插入删除: 在链表中插入或删除元素只需要修改相邻节点的指针,时间复杂度为O(1)(在有指针的情况下)。
  • 缺点:

    • 访问速度: 不能直接通过索引访问,需要从头节点开始遍历,时间复杂度为O(n)。
    • 额外空间开销: 每个节点除了存储数据,还需要存储指向下一个节点的指针。

📊 数组与链表的对比

在这里插入图片描述

🎯 访问速度

  • 数组: 🏆 胜出在于其提供了常数时间O(1)的访问速度。由于元素在内存中连续存储,给定索引后,计算元素地址简单直接,瞬间定位。
  • 链表: 访问某个元素需要从头节点开始,沿着指针一步步遍历,最坏情况下时间复杂度为O(n),意味着数据越大,查找越慢。

🎯 插入与删除效率

  • 链表: 🏆 在插入和删除操作上表现出色,特别是在链表的头部或尾部进行时,只需调整相邻节点的指针即可,时间复杂度为O(1)。即使在中间操作,也仅需改动少量指针,避免了大量数据移动。
  • 数组: 对于插入和删除,特别是当位置不在末尾时,可能需要移动后续的所有元素,以保持连续性,导致最坏情况下的时间复杂度为O(n)。

🎯 空间利用效率

  • 数组: 可能导致内存浪费。预先分配固定大小的内存空间,如果未填满,则有未使用的空间。不过,对于确切知道大小的数据集,这不构成太大问题。
  • 链表: 每个节点除了存储数据外,还需要额外的内存来存储指向下一个节点的指针,这构成了空间上的开销。然而,链表能够根据需要动态调整大小,避免了预分配过大内存的问题。

🎯 应用场景

  • 数组: 非常适合于需要快速随机访问数据的场景,例如图像处理、音频数据、大规模科学计算等,其中数据一旦加载,就频繁查询而很少修改。
  • 链表: 在频繁进行插入和删除操作的场景中大放异彩,如实现动态数据结构(如队列、栈)、构建更复杂的数据结构(如哈希表的链地址法解决冲突、图的邻接表表示)或者处理不确定长度的数据流。

📚 小结

数组与链表各有千秋,选择合适的工具对于提高程序性能至关重要。理解它们的底层原理,能帮助我们在面对具体问题时做出明智的选择。希望这篇学习笔记能加深你对这两种基础数据结构的理解,为你的编程之旅增添一份助力!✨

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

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

相关文章

运放的同相与反相放大

反相放大器 同相端接地,电压为 0,反相端和同相端虚短,因此也是 0 V 的电压,同时由于虚断,几乎没有电流注入,所以R 1 和R 2 相当于串联,电阻上的电流相等 因此可以求出输入输出关系式为 V o u t…

excel如何将多列数据转换为一列?

这个数据整理借用数据透视表也可以做到: 1.先将数据源的表头补齐,“姓名” 2.点击插入选项卡,数据透视表,在弹出对话框中,数据透视位置选择 现有工作表,(实际使用时新建也没有问题)…

免费分享一套微信小程序在线订餐(点餐)配送系统(SpringBoot+Vue),帅呆了~~

大家好,我是java1234_小锋老师,看到一个不错的微信小程序在线订餐(点餐)配送系统(SpringBootVue),分享下哈。 项目视频演示 【免费】微信小程序在线订餐(点餐)配送系统(SpringBootVue) Java毕业设计_哔哩哔哩_bilibili【免费】微信小程序在…

每日OJ题_贪心算法三⑥_力扣738. 单调递增的数字

目录 力扣738. 单调递增的数字 解析代码 力扣738. 单调递增的数字 738. 单调递增的数字 难度 中等 当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xff0…

集成学习案例-幸福感预测

集成学习案例一 &#xff08;幸福感预测&#xff09; 背景介绍 此案例是一个数据挖掘类型的比赛——幸福感预测的baseline。比赛的数据使用的是官方的《中国综合社会调查&#xff08;CGSS&#xff09;》文件中的调查结果中的数据&#xff0c;其共包含有139个维度的特征&#xf…

哪款充电宝质量和口碑比较好?适合入手充电宝有哪些?

充电宝这么好用的移动电源就不用我说了吧&#xff0c;平时不出门还好&#xff0c;家里有插座可以充电&#xff0c;但是但凡出门了&#xff0c;手机电量一般是不能够支撑到&#xff0c;像我这种手机重度使用者&#xff0c;出门在外手机快没电了我就非常焦虑了&#xff0c;有一款…

五一 大项目

Docker 中的 Nginx 服务为什么要启用 HTTPS 一安装容器 1 安装docker-20.10.17 2 安装所需的依赖 sudo yum install -y yum-utils device-mapper-persistent-data lvm23 添加Docker官方仓库 sudo yum-config-manager --add-repo https://download.docker.com/linux/centos…

Python pypdf库:PDF文档处理的利器

更多Python学习内容&#xff1a;ipengtao.com PDF&#xff08;Portable Document Format&#xff09;是一种常见的文档格式&#xff0c;广泛应用于各种场景&#xff0c;包括文档编辑、电子书籍、报告等。Python作为一种强大的编程语言&#xff0c;在处理PDF文档方面也有着丰富的…

怎样通过PHP语言实现远程控制一路开关

怎样通过PHP语言实现远程控制一路开关呢&#xff1f; 本文描述了使用PHP语言调用HTTP接口&#xff0c;实现控制一路开关&#xff0c;一路开关可控制一路照明、排风扇等电器。 可选用产品&#xff1a;可根据实际场景需求&#xff0c;选择对应的规格 序号设备名称厂商1智能WiFi…

【JavaWeb】网上蛋糕项目商城-我的订单,退出功能

概念 上一文中&#xff0c;我们实现了注册&#xff0c;登录&#xff0c;提交订单以及修改个人信息等功能。本文在登录的状态下&#xff0c;实现订单列表以及退出登录功能等。 我的订单 在head.jsp头部页面中&#xff0c;当用户处于登录状态&#xff0c;则会显示“我的订单”…

面试分享——订单超30分钟未支付自动取消用什么实现?如何使用Redis实现延迟队列?

目录 1.订单超时未支付自动取消&#xff0c;这个你用什么方案实现&#xff1f; 2.如何使用Redis实现延迟队列 2.1实验步骤 2.2实现生产可用的延迟队列还需关注什么 3.总结 电商场景中的问题向来很受面试官的青睐&#xff0c;因为业务场景大家都相对更熟悉&#xff0c;相关…

Java | Leetcode Java题解之第75题颜色分类

题目&#xff1a; 题解&#xff1a; class Solution {public void sortColors(int[] nums) {int n nums.length;int p0 0, p2 n - 1;for (int i 0; i < p2; i) {while (i < p2 && nums[i] 2) {int temp nums[i];nums[i] nums[p2];nums[p2] temp;--p2;}i…

Pytorch入门—Tensors张量的学习

Tensors张量的学习 张量是一种特殊的数据结构&#xff0c;与数组和矩阵非常相似。在PyTorch中&#xff0c;我们使用张量来编码模型的输入和输出&#xff0c;以及模型的参数。 张量类似于NumPy的ndarrays&#xff0c;只是张量可以在GPU或其他硬件加速器上运行。事实上&#xf…

Flink 部署模式

目录 概述 部署模式 会话模式&#xff08;Session Mode&#xff09; 单作业模式(Per-Job Mode) 应用模式(Application Mode) 运行模式&#xff08;资源管理模式&#xff09; Standalone运行模式 会话模式部署 应用模式部署 Yarn运行模式 会话模式部署 单作业模式部…

LeetCode70:爬楼梯

题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 解题思想 1.确定dp数组以及下标的含义 dp[i]&#xff1a; 爬到第i层楼梯&#xff0c;有dp[i]种方法 2.确定递推公式 从dp[i]的定义可以…

Git系列:git merge 使用技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

基于springboot实现体育馆管理系统项目【项目源码+论文说明】

基于springboot实现体育馆管理系统演示 摘要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本体育馆管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理…

2024高安全个人密码本程序源码,贴身密码管家-随机密码备忘录二代密码

项目概述&#xff1a; 在这个网络高度发展的时代&#xff0c;每个人都需要上网&#xff0c;而上网就不可避免地需要使用账号和密码。 在众多账号的情况下&#xff0c;你是否还在为复杂难记的密码感到烦恼&#xff1f;现在只需要记录一次&#xff0c; 就可以随时查看你的密码…

代码随想录算法训练营第二十天:二叉树成长

代码随想录算法训练营第二十天&#xff1a;二叉树成长 110.平衡二叉树 力扣题目链接(opens new window) 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a;一个二叉树每个节点 的左右两个子树的高度差的绝…

提升Go语言数学运算能力:math包使用指南

提升Go语言数学运算能力&#xff1a;math包使用指南 介绍数学函数的使用基本数学运算幂和根的计算三角函数对数计算 特殊数学常数和函数数学常数超越数学函数错误处理和精度问题 高级应用实例统计数据的标准偏差计算利用三角函数解决实际问题 性能优化技巧避免不必要的函数调用…