算法学习:数组 vs 链表

news2024/11/24 16:00:52

在这里插入图片描述

🔥 个人主页:空白诗

在这里插入图片描述

文章目录

    • 🎯 引言
    • 🛠️ 内存基础
      • 什么是内存❓
      • 内存的工作原理 🎯
    • 📦 数组(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 main

import "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 main

import (
	"fmt"
)

// ListNode 定义链表节点
type ListNode struct {
	Value int
	Next  *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.Head
		for 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.Next
		return
	}
	current := list.Head
	for 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.Head
	for 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://www.coloradmin.cn/o/1651625.html

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

相关文章

网页翻译终极方案:DeepLX

为什么要选择 DeepL 翻译? DeepL 被誉为全世界最精准的机器翻译,比最接近他们的竞争对手准确三倍以上 看看 DeepL 和 微软翻译 的对比 👇👇 三句英文: Walking on eggshells during the software update. Wangs VR game is a…

c#实现音乐的“vip播放功能”

文章目录 前言1. c#窗体2. 功能3. 具体实现3.1 添加文件3.2 音乐播放3.3 其他功能 4. 整体代码和窗口5. 依赖的第三方库 前言 最近在QQ音乐里重温周杰伦的歌,觉得好听到耳朵怀孕,兴起想要下载下来反复听,发现QQ音乐VIP歌曲下载下来的格式居然…

【MsSQL】数据库基础 库的基本操作

目录 一,数据库基础 1,什么是数据库 2,主流的数据库 3,连接服务器 4,服务器,数据库,表关系 5,使用案例 二,库的操作 1,创建数据库 2,创建…

今日早报 每日精选15条新闻简报 每天一分钟 知晓天下事 5月8日,星期三

每天一分钟,知晓天下事! 2024年5月8日 星期三 农历四月初一 1、 我国将对法国等12国免签政策延长至2025年底,旅游平台加码布局入境游。 2、 财政部:下拨1582亿元,提高义务教育阶段家庭经济困难学生补助标准。 3、 4月…

STM32窗口看门狗的操作

STM32的窗口看门狗的主要功能是,程序过早的喂狗还有太晚喂狗,都会触发单片机重启,就是有一个时间段,在这个时间段内喂狗才不会触发单片机重启。 下面我就总结一下窗口看门狗的设置过程: 第一步:开启窗口看…

车辆充电桩|基于Springboot+vue的车辆充电桩管理系统的设计与实现(源码+数据库+文档)

车辆充电桩管理系统 目录 基于Springboot+vue的车辆充电桩管理系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1 前台功能模块 4.1.1 首页功能 4.1.2 用户后台管理 2 后台功能模块 4.2.1 管理员功能 4.2.2 维修员功能 四、数据库设计 五、核…

菜鸡学习netty源码(四)—— EventLoopGroup

1.概述 我们前面进行过分析,channel为netty网络操作的抽象类,EventLoop负责处理注册到其上的Channel处理的I/O事件;EventLoopGroup是一个EventLoop的分组,它可以获取到一个或者多个的EventLoop对象。 2.类关系图 NioEventLoopGroup的类继承图,蓝色部分为对应的java类,绿…

jQuery-1.语法、选择器、节点操作

jQuery jQueryJavaScriptQuery&#xff0c;是一个JavaScript函数库&#xff0c;为编写JavaScript提供了更高效便捷的接口。 jQuery安装 去官网下载jQuery&#xff0c;1.x版本练习就够用 jQuery引用 <script src"lib/jquery-1.11.2.min.js"></script>…

【经验分享】做多年大数据采集的经验分享:关于电商API数据采集接口做电商必备的电商API接口

国内主流电商平台包括&#xff1a; 1. 淘宝&#xff1a;阿里巴巴旗下的电子商务平台&#xff0c;以C2C和B2C交易为主要业务模式。 2. 天猫&#xff1a;阿里巴巴旗下的B2C电子商务平台&#xff0c;为品牌商和零售商提供销售渠道和服务。 3. 京东&#xff1a;一家以B2C为主营业务…

Linux网络部分——部署YUM仓库及NFS共享服务

目录 一、yum仓库服务 1. 软件仓库的提供方式 2.如何构建并使用ftp软件仓库&#xff08;与本地yum源方法一致&#xff09; 3.如何搭建使用yum在线源&#xff1f; 4.yum软件包下载如何保存&#xff1f; 二、NFS共享存储服务 1.存储类型 2.提供共享存储的组合 3.NFS网络…

java+jsp+Oracle+Tomcat 记账管理系统论文(完整版)

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️ ➡️点击免费下载全套资料:源码、数据库、部署教程、论文、答辩ppt一条龙服务 ➡️有部署问题可私信联系 ⬆️⬆️⬆️​​​​​​​⬆️…

Node.js里面 Path 模块的介绍和使用

Node.js path 模块提供了一些用于处理文件路径的小工具&#xff0c;我们可以通过以下方式引入该模块&#xff1a; var path require("path") 方法描述 序号方法 & 描述1path.normalize(p) 规范化路径&#xff0c;注意.. 和 .。2path.join([path1][, path2][,…

安全至上!群晖NAS开启SSH的正确姿势与风险防范(群晖如何开启SSH,解决群晖无法连接SSH的问题)

文章目录 💢 问题 💢🏡 演示环境 🏡💯 解决方案 💯📝 开启SSH服务📝 设置安全策略📝 远程连接SSH📝 常见问题解决⚓️ 相关链接 ⚓️💢 问题 💢 你是否遇到过需要远程管理你的群晖NAS,却发现无法通过SSH连接的窘境?SSH作为一种安全协议,可以让我们安…

C++ | Leetcode C++题解之第76题最小覆盖子串

题目&#xff1a; 题解&#xff1a; class Solution { public:unordered_map <char, int> ori, cnt;bool check() {for (const auto &p: ori) {if (cnt[p.first] < p.second) {return false;}}return true;}string minWindow(string s, string t) {for (const au…

外星人笔记本-记一次电脑发热过热缘由

背景 笔记本进行过大修&#xff0c;电池鼓包&#xff0c;还好没炸&#xff0c;因此替换电池。发现内存&#xff08;SSD&#xff09;不足&#xff0c;又增加了内存。完成后使用还算正常。但是过一段时间后&#xff0c;系统自动更新几次&#xff08;window10系统就是恶心&#x…

C语言 自定义类型——联合体

目录: 一、联合体是&#xff1f;声明计算内存大小 二、联合体的特点例如 三、联合体大小的计算规则&#xff1a; 四、应用习1习2 一、联合体是&#xff1f; 联合体和结构体差不多&#xff0c;但是其最大的区别在于联合体所有的成员共用一块内存空间。所以联合体也叫共用体。联…

平滑 3d 坐标

3d平滑 import torch import torch.nn.functional as F import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3Dclass SmoothOperator:def smooth(self, vertices):# 使用一维平均池化进行平滑vertices_smooth F.avg_pool1d(vertices.p…

第50期|GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

五道数组习题,只过思路

建议先过一遍&#xff1a;保研机试前的最后七道数组题-CSDN博客 第一题&#xff1a; 88. 合并两个有序数组 - 力扣&#xff08;LeetCode&#xff09; ​ 跟合并两个有序链表类似&#xff0c; 快慢指针的用法&#xff0c;新建立一个数组&#xff0c;再将数组赋给nums1。 第…

mysql 不停的重启关闭

早上在使用phpstudy的时候&#xff0c;发现自己的mysql5.7和5.8都出现了问题&#xff0c;就是不停的重启&#xff0c;在梳理了状况之后&#xff0c;可能是硬盘的内存空间不足&#xff0c;或者硬盘出现了问题&#xff1b;于是我将mysql 重新安装了一次&#xff0c;整个问题就解决…