一、数组
1.概念:
「数组 array」是一种线性数据结构,其将相同类型元素存储在连续的内存空间中。我们将元素在数组中的位 置称为该元素的「索引 index」。
问题:为什么数组的下标是从0开始,为什么只能存储相同类型的元素
先看一张图片:
在计算机存储中,数据是按照一定的地址存储的,而数组是一种线性结构,其元素在内存中是依次排列的(在内存中,线性结构通常以连续的一段地址存储)。使用从0开始的下标,有助于简化对数组元素在内存中的计算,避免了每次计算时都需要将索引值-1。
关于为什么只能存储相同的数据类型是因为 相同的数据类型的元素长度是一致的,占据相同大小的内存空间。如果数组中可以存储不同类型的元素,就无法确定每个元素所占的内存大小,也就无法通过下标直接访问特定位置的元素。
2.插入元素
数组元素在内存中是“紧挨着的”,它们之间没有空间再存放任何数据,如果想要在数组中间
插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。
值得注意的是,由于数组的长度是固定的,在中间插入一个,末尾的元素就会丢失
3.删除元素
若想要删除索引
𝑖
处的元素,则需要把索引
𝑖
之后的元素都向前移动一位。
![](https://img-blog.csdnimg.cn/68de60b3ab6e45c79e80233234794e10.png)
总的来看,数组的插入与删除操作有以下缺点。
‧
时间复杂度高
:数组的插入和删除的平均时间复杂度均为
𝑂(𝑛)
,其中
𝑛
为数组长度。
‧
丢失元素
:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。
‧
内存浪费
:我们可以初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素
都是“无意义”的,
4遍历
for循环
5.查找
在数组中查找指定元素需要遍历数组,每轮判断元素值是否匹配,若匹配则输出对应索引。
因为数组是线性数据结构,所以上述查找操作被称为“线性查找”。
/* 在数组中查找指定元素 */
int find(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target)
return i;
}
return -1;
}
6.优点缺点
数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来
优化数据结构的操作效率。
‧
空间效率高
: 数组为数据分配了连续的内存块,无须额外的结构开销。
‧
支持随机访问
: 数组允许在
𝑂(1)
时间内访问任何元素。
‧
缓存局部性
: 当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓
存来提升后续操作的执行速度。
连续空间存储是一把双刃剑,其存在以下缺点。
‧
插入与删除效率低
: 当数组中元素较多时,插入与删除操作需要移动大量的元素。
‧
长度不可变
: 数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
‧
空间浪费
: 如果数组分配的大小超过了实际所需,那么多余的空间就被浪费了。
7.数组的典型应用
数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构。
‧
随机访问
:如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实 现样本的随机抽取。
‧
排序和搜索
:数组是排序和搜索算法最常用的数据结构。快速排序、归并排序、二分查找等都主要在数 组上进行。
‧
查找表
:当我们需要快速查找一个元素或者需要查找一个元素的对应关系时,可以使用数组作为查找 表。假如我们想要实现字符到 ASCII 码的映射,则可以将字符的 ASCII 码值作为索引,对应的元素存 放在数组中的对应位置。
‧
机器学习
:神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式 构建的。数组是神经网络编程中最常使用的数据结构。
‧
数据结构实现
:数组可以用于实现栈、队列、哈希表、堆、图等数据结构。例如,图的邻接矩阵表示实 际上是一个二维数组。
二、链表
内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我 们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此 时链表的灵活性优势就体现出来了。
1.概念
「链表 linked list」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。
引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。
链表的设计使得各个节点可以被分散存储在内存各处,它们的内存地址是无须连续的。
![](https://img-blog.csdnimg.cn/e2259fa29aa542c3a59166c7363ed35f.png)
链表的组成单位是「节点 node」对象。每个节点都包含两项数据:当前节点的“值”和指向下一节
点的“引用”。
‧ 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
‧ 尾节点指向的是“空”,它在 Java、C++ 和 Python 中分别被记为 null、nullptr 和 None 。
‧ 在 C、C++、Go 和 Rust 等支持指针的语言中,上述的“引用”应被替换为“指针”。
链表类示例:
/* 链表节点类 */
class ListNode {
int val; // 节点值
ListNode next; // 指向下一节点的引用
ListNode(int x) { val = x; } // 构造函数
}
2.初始化 插入 删除
初始化
建立链表分为两步,第一步是初始化各个节点对象,第二步是构建引用指向关系。初始化完成后,我们就可
以从链表的头节点出发,通过引用指向
next
依次访问所有节点。
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */// 初始化各个节点ListNode n0 = new ListNode ( 1 );ListNode n1 = new ListNode ( 3 );ListNode n2 = new ListNode ( 2 );ListNode n3 = new ListNode ( 5 );ListNode n4 = new ListNode ( 4 );// 构建引用指向n0 . next = n1 ;n1 . next = n2 ;n2 . next = n3 ;n3 . next = n4 ;
插入
在链表中插入节点非常容易。假设我们想在相邻的两个节点 n0 和 n1 之间插入一个新节点 P ,
则只需要改变两个节点引用(指针)即可
,时间复杂度为
𝑂(1)
。 相比之下,在数组中插入元素的时间复杂度为 𝑂(𝑛)
,在大数据量下的效率较低。
![](https://img-blog.csdnimg.cn/efdb36cc75aa4e4c9544c23827ad96b8.png)
/* 在链表的节点 n0 之后插入节点 P */void insert ( ListNode n0 , ListNode P ) {ListNode n1 = n0 . next ;P . next = n1 ;n0 . next = P ;}
删除
在链表中删除节点也非常方便,
只需改变一个节点的引用(指针)即可
。 请注意,尽管在删除操作完成后节点 P
仍然指向
n1
,但实际上遍历此链表已经无法访问到
P
,这意味着
P
已 经不再属于该链表了。
![](https://img-blog.csdnimg.cn/d6871041db51437082dba1e21bf51671.png)
/* 删除链表的节点 n0 之后的首个节点 */void remove ( ListNode n0 ) {if ( n0 . next == null) return;// n0 -> P -> n1ListNode P = n0 . next ;ListNode n1 = P . next ;n0 . next = n1 ;}
3. 访问节点
在链表访问节点的效率较低
。如上节所述,我们可以在
𝑂(1)
时间下访问数组中的任意元素。链表则不然,程 序需要从头节点出发,逐个向后遍历,直至找到目标节点。也就是说,访问链表的第 𝑖
个节点需要循环
𝑖 − 1 轮,时间复杂度为 𝑂(𝑛)
。
/* 访问链表中索引为 index 的节点 */ListNode access ( ListNode head , int index ) {for ( int i = 0 ; i < index ; i ++) {if ( head == null) return null;head = head . next ;}return head ;}
4.数组vs链表
![](https://img-blog.csdnimg.cn/8462453ec90446d8ab3b0ba24e0b4c80.png)
5.典型的链表应用
单向链表通常用于实现栈、队列、哈希表和图等数据结构。
‧
栈与队列
:当插入和删除操作都在链表的一端进行时,它表现出先进后出的的特性,对应栈;当插入操
作在链表的一端进行,删除操作在链表的另一端进行,它表现出先进先出的特性,对应队列。
‧
哈希表
:链地址法是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表
中。
‧
图
:邻接表是表示图的一种常用方式,在其中,图的每个顶点都与一个链表相关联,链表中的每个元素
都代表与该顶点相连的其他顶点。
双向链表常被用于需要快速查找前一个和下一个元素的场景。
‧
高级数据结构
:比如在红黑树、B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指
向父节点的引用来实现,类似于双向链表。
‧
浏览器历史
:在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和
后一个网页。双向链表的特性使得这种操作变得简单。
‧
LRU 算法
:在缓存淘汰算法(LRU)中,我们需要快速找到最近最少使用的数据,以及支持快速地添
加和删除节点。这时候使用双向链表就非常合适。
循环链表常被用于需要周期性操作的场景,比如操作系统的资源调度。
‧
时间片轮转调度算法
:在操作系统中,时间片轮转调度算法是一种常见的 CPU 调度算法,它需要对一
组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU 将切换到下一个进程。这种循
环的操作就可以通过循环链表来实现。
‧
数据缓冲区
:在某些数据缓冲区的实现中,也可能会使用到循环链表。比如在音频、视频播放器中,数
据流可能会被分成多个缓冲块并放入一个循环链表,以便实现无缝播放。