【学习笔记】数据结构与算法02:数组与链表

知识出处:Hello算法:https://www.hello-algo.com/

二、 数据结构

物理结构反映了数据在计算机内存中的存储方式,可分为连续空间存储(数组)和分散空间存储(链表)。所有数据结构都是基于数组、链表或二者的组合实现的

  • “静态数据结构”是基于数组实现的数据结构。(栈、队列、哈希表、树、堆、图、矩阵、张量(维度 ≥3 的数组)等。
    • 此类数据结构在初始化后长度不可变。
    • 空间上连续存储。
  • “动态数据结构”是基于链表实现的数据结构(栈、队列、哈希表、树、堆、图等)
    • 数据结构在初始化后,仍可以在程序运行过程中对其长度进行调整。
    • 空间上分散存储

连续空间存储与分散空间存储

2.0 基础数据类型

基本数据类型是 CPU 可以直接进行运算的类型(所以十分重要),在算法中直接被使用,主要包括以下几种。

  • 整数类型 byteshortintlong
  • 浮点数类型 floatdouble ,用于表示小数。
  • 字符类型 char ,用于表示各种语言的字母、标点符号甚至表情符号等。
  • 布尔类型 bool ,用于表示“是”与“否”判断。

基本数据类型以二进制的形式存储在计算机中。一个二进制位即为 1 比特。在绝大多数现代操作系统中,1 字节(byte)由 8 比特(bit)组成。

在Java中的基础数据类型如下:

image-20240221111225851

请注意,表 3-1 针对的是 Java 的基本数据类型的情况。每种编程语言都有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。

  • 在 Python 中,整数类型 int 可以是任意大小,只受限于可用内存;浮点数 float 是双精度 64 位;没有 char 类型,单个字符实际上是长度为 1 的字符串 str
  • C 和 C++ 未明确规定基本数据类型的大小,而因实现和平台各异。表 3-1 遵循 LP64 数据模型,其用于包括 Linux 和 macOS 在内的 Unix 64 位操作系统。
  • 字符 char 的大小在 C 和 C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见“字符编码”章节。
  • 即使表示布尔量仅需 1 位(0 或 1),它在内存中通常也存储为 1 字节。这是因为现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。

2.1 数组与链表

2.1.1 数组

  • 「数组 array」是一种线性数据结构,其将相同类型的元素存储在连续的内存空间中。
  • 元素在数组中的位置称为该元素的「索引 index」。
  • 数组的元素类型相同。

数组定义与存储方式

2.1.1.1 常用操作(不涉及代码

初始化

两种初始化方式:无初始值、给定初始值

在未指定初始值的情况下,大多数编程语言会将数组元素初始化为 0

访问元素

  • 数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。

  • 通过某个元素的索引,得到该元素的内存地址,从而直接访问该元素。

  • 索引本质上是内存地址的偏移量,首个元素的地址偏移量是 0 ,因此它的索引为 0 是合理的。

  • 访问元素非常高效,我们可以在 O(1) 时间内随机访问数组中的任意一个元素。

插入元素

  • 如果想在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。
  • 由于数组的长度是固定的,因此插入一个元素必定会导致数组尾部元素“丢失”

删除元素

  • 删除索引 i 处的元素,则需要把索引 i 之后的元素都向前移动一位
  • 删除元素完成后,原先末尾的元素变得“无意义”了,所以我们无须特意去修改它。

总的来看,数组的插入与删除操作有以下缺点。

  • 时间复杂度高:数组的插入和删除的平均时间复杂度均为 O(n) ,其中 n 为数组长度。
  • 丢失元素:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。
  • 内存浪费:我们可以初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是“无意义”的,但这样做会造成部分内存空间浪费。

遍历数组

既可以通过索引遍历数组,也可以直接遍历获取数组中的每个元素

查找元素

  • 在数组中查找指定元素需要**遍历数组,每轮判断元素值是否匹配,**若匹配则输出对应索引。

  • 因为数组是线性数据结构,所以上述查找操作被称为“线性查找”

扩容数组
  • 在复杂的系统环境中,程序难以保证数组之后的内存空间是可用的,从而无法安全地扩展数组容量。因此在大多数编程语言中,数组的长度是不可变的
  • 如果需要扩容数组,则需要建立一个更大的数组,将元素依次复制到新数组。这是一个 O(n) 的操作,在数组很大的情况下非常耗时
2.1.1.2 数组的优点与局限性

得益于数组存储于连续的空间,且元素相同,具有丰富的先验信息,所以数组具有以下优点:

  • **空间效率高,**只占用一篇连续的内存空间
  • 支持随机访问,可以通过索引快速访问数组中的任意元素
  • 局部缓存性,先验性,系统可以在加载某个元素时,提前缓存周围的数据,从而提高后续操作的速度。

但是,连续空间存储是一把双刃剑,其存在以下局限性

  • 插入和删除效率低下:需要操作大量元素
  • 长度固定:实际情况更希望数组的长度可变,所以更多会使用列表
  • 空间浪费:数组分配的大小超过实际所需,那么多余的空间就被浪费了

2.2 栈与队列

2.1.1.3 数组的典型应用
  • 随机访问:如果我们想随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现随机抽样。
  • 排序和搜索:数组是排序和搜索算法最常用的数据结构。快速排序、归并排序、二分查找等都主要在数组上进行。
  • 查找表:当需要快速查找一个元素或其对应关系时,可以使用数组作为查找表。假如我们想实现字符到 ASCII 码的映射,则可以将字符的 ASCII 码值作为索引,对应的元素存放在数组中的对应位置。
  • 机器学习:神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。
  • 数据结构实现:数组可以用于实现栈、队列、哈希表、堆、图等数据结构。例如,图的邻接矩阵表示实际上是一个二维数组。

2.1.2 链表

「链表 linked list」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。但是,相同数据量下,链表比数组占用更多的内存空间

链表的组成单位是「节点 node」对象。

  • 每个节点都包含两项数据:

    • 节点的“值”

    • 指向下一节点的“引用”。

  • 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。

  • 尾节点指向的是“空”,在 Java、C++ 和 Python 中分别被记为 nullnullptrNone

链表定义与存储方式

2.1.2.1 链表常用操作

初始化

建立链表分为两步,第一步是初始化各个节点对象,第二步是构建节点之间的引用关系。通常将头节点当作链表的代称(不同于数组,数组整体是一个变量,比如数组 nums 包含元素 nums[0]nums[1] 等)

插入节点

在相邻的两个节点 n0n1 之间插入一个新节点 P则只需改变两个节点引用(指针)即可,时间复杂度为 O(1) 。

删除节点

在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可

注意,在实际操作中,只需要改变前节点n0的引用,虽然后节点P依然指向n1,但实际上此链表已经无法抵达节点P了,这意味着P已经不属于该链表了

访问节点

在链表中访问节点的效率较低,需要逐个遍历,时间复杂度为 O(n) 。

查找节点

遍历链表,查找其中值为 target 的节点,输出该节点在链表中的索引。也属于线性查找

2.1.2.2 链表和数组对比

由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。

  1. 数组和链表最大的不同源于他们的存储方式上。
  2. 由此可以延伸出扩容和内存占用的特点
  3. 插入、删除、访问、查询分别对应增删改查操作,两者各有优劣,需要根据实际情况来选择。
    • 数组的优势在于查询和访问特点元素
    • 链表的优势在于快速插入和删除元素

image-20240221114357561

2.1.2.3 链表的常见类型

常见链表种类

2.1.2.4 链表典型应用

单向链表通常用于实现栈、队列、哈希表和图等数据结构。

  • 栈与队列:单向链表可以很好的体现先进后出、或者先进先出的特征。
  • 哈希表解决哈希冲突的主流方案之一
  • :邻接表是表示图的一种常用方式,链表中的每个元素都代表与该顶点相连的其他顶点。

双向链表常用于需要快速查找前一个和后一个元素的场景。

  • 高级数据结构:比如在红黑树、B 树,需要同时保存父节点和子节点
  • 浏览器历史:浏览器需要知道用户访问过的前一个和后一个网页。双向链表的特性使得这种操作变得简单。
  • LRU 算法:在缓存淘汰(LRU)算法中,我们需要快速找到最近最少使用的数据,以及支持快速添加和删除节点。这时候使用双向链表就非常合适。

环形链表常用于需要周期性(or循环)操作的场景,比如操作系统的资源调度。

  • 轮询算法就可以通过环形链表实现
  • 时间片轮转调度算法:在操作系统中,时间片轮转调度算法是一种常见的 CPU 调度算法,它需要对一组进程进行循环。
  • 数据缓冲区:在某些数据缓冲区的实现中,也可能会使用环形链表。比如播放器循环播放列表的实现

2.1.3 列表

「列表 list」是一个抽象的数据结构概念,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。

  • 链表天然可以看作一个列表
  • 数组由于其长度不可变,所以一般使用「动态数组 dynamic array」来实现列表,它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容

实际上,许多编程语言中的标准库提供的列表是基于动态数组实现的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。

2.1.3.1 列表的常用操作
初始化、访问、插入、删除、遍历
  • 使用“无初始值”和“有初始值”这两种初始化方法
  • 列表本质上是数组,因此可以在 O(1) 时间内访问和更新元素,效率很高
  • 列表可以自由地添加与删除元素。
    • 列表尾部添加删除元素的时间复杂度为 O(1)
    • 插入和删除元素的效率仍与数组相同,时间复杂度为 O(n) 。
  • 与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。
拼接列表、排序列表
  • 由于长度不固定,列表支持将另一个列表拼接到原列表的尾部。
  • 在代码实现中,可以使用列表内置的方式快速实现排序。排序后,可以使用在数组类算法题中经常考查的“二分查找”和“双指针”算法。
2.3.1.3 列表的实现

一个简易版列表,包括以下三个重点设计

  • 初始容量,选取一个合理的数组初始容量
  • 数量记录,声明一个变量用于记录列表中实际元素的数量size,并随着元素插入和删除实时更新。
  • 扩容机制,若插入元素时列表容量已满,则需要进行扩容。需要考虑扩容的时机和扩容的倍数

  • 16
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xcong_Zhu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值