数据结构--数组、链表

一、数组

1.概念:

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

 问题:为什么数组的下标是从0开始,为什么只能存储相同类型的元素

先看一张图片:

在计算机存储中,数据是按照一定的地址存储的,而数组是一种线性结构,其元素在内存中是依次排列的(在内存中,线性结构通常以连续的一段地址存储)。使用从0开始的下标,有助于简化对数组元素在内存中的计算,避免了每次计算时都需要将索引值-1。

关于为什么只能存储相同的数据类型是因为 相同的数据类型的元素长度是一致的,占据相同大小的内存空间。如果数组中可以存储不同类型的元素,就无法确定每个元素所占的内存大小,也就无法通过下标直接访问特定位置的元素。

2.插入元素

数组元素在内存中是“紧挨着的”,它们之间没有空间再存放任何数据,如果想要在数组中间
插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。

值得注意的是,由于数组的长度是固定的,在中间插入一个,末尾的元素就会丢失 

3.删除元素

若想要删除索引 𝑖 处的元素,则需要把索引 𝑖 之后的元素都向前移动一位。

总的来看,数组的插入与删除操作有以下缺点。
时间复杂度高 :数组的插入和删除的平均时间复杂度均为 𝑂(𝑛) ,其中 𝑛 为数组长度。
丢失元素 :由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。
内存浪费 :我们可以初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素
都是“无意义”的,

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」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。
引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。
链表的设计使得各个节点可以被分散存储在内存各处,它们的内存地址是无须连续的。
链表的组成单位是「节点 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) 。 相比之下,在数组中插入元素的时间复杂度为 𝑂(𝑛) ,在大数据量下的效率较低。
/* 在链表的节点 n0 之后插入节点 P */
void insert ( ListNode n0 , ListNode P ) {
        ListNode n1 = n0 . next ;
        P . next = n1 ;
        n0 . next = P ;
}
删除
在链表中删除节点也非常方便, 只需改变一个节点的引用(指针)即可 。 请注意,尽管在删除操作完成后节点 P 仍然指向 n1 ,但实际上遍历此链表已经无法访问到 P ,这意味着 P 已 经不再属于该链表了。
/* 删除链表的节点 n0 之后的首个节点 */
void remove ( ListNode n0 ) {
        if ( n0 . next == null) return;
        // n0 -> P -> n1
        ListNode 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链表

5.典型的链表应用

单向链表通常用于实现栈、队列、哈希表和图等数据结构。
栈与队列 :当插入和删除操作都在链表的一端进行时,它表现出先进后出的的特性,对应栈;当插入操
作在链表的一端进行,删除操作在链表的另一端进行,它表现出先进先出的特性,对应队列。
哈希表 :链地址法是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表
中。
:邻接表是表示图的一种常用方式,在其中,图的每个顶点都与一个链表相关联,链表中的每个元素
都代表与该顶点相连的其他顶点。
双向链表常被用于需要快速查找前一个和下一个元素的场景。
高级数据结构 :比如在红黑树、B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指
向父节点的引用来实现,类似于双向链表。
浏览器历史 :在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和
后一个网页。双向链表的特性使得这种操作变得简单。
LRU 算法 :在缓存淘汰算法(LRU)中,我们需要快速找到最近最少使用的数据,以及支持快速地添
加和删除节点。这时候使用双向链表就非常合适。
循环链表常被用于需要周期性操作的场景,比如操作系统的资源调度。
时间片轮转调度算法 :在操作系统中,时间片轮转调度算法是一种常见的 CPU 调度算法,它需要对一
组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU 将切换到下一个进程。这种循
环的操作就可以通过循环链表来实现。
数据缓冲区 :在某些数据缓冲区的实现中,也可能会使用到循环链表。比如在音频、视频播放器中,数
据流可能会被分成多个缓冲块并放入一个循环链表,以便实现无缝播放。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值