数组和链表

144 阅读7分钟

二、数组和链表

本章中的数组和列表,都属于线性表的顺序存储。

数组和链表是两种基本的数据结构,分别代表数据在计算机内存中的两种存储方式:连续空间存储和分散空间存储。两者的特点呈现出互补的特性。

数组

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

数组内存地址(首元素内存地址)。

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

数组的有效长度。

数组中存储的是节点的引用,而非节点本身。

数组的存储空间是连续指的是在虚拟内存上,然而,在物理内存中,数组可能并不连续。虚拟内存到物理内存的映射涉及到分页(paging)技术。

内存管理中的堆和栈,与数据结构的堆和栈,不是一个概念。

1.数组基本操作

初始化数组

插入元素

平均时间复杂度O(n)

删除元素

平均时间复杂度O(n)

查找元素

按位查找

按值查找

  • 平均时间复杂度O(n)
  • 若表内元素有序,可在O(logzn) 时间内找到。

扩容数组

如果我们希望扩容数组,则需重新建立一个更大的数组,然后把原数组元素依次复制到新数组。这是一个 O(n) 的操作,在数组很大的情况下非常耗时。

2.数组的优点与局限性

优点:

  • 数组存储在连续的内存空间内,且元素类型相同。
  • 缓存局部性:当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。
  • 数组具有更高的缓存命中率,因此它在操作效率上通常优于链表。

局限性:

  • 长度不可变。

3.应用

数组典型应用

  • 数据结构实现。

链表

链表(linked list): 链表是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。

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

1.链表基本操作

初始化链表

建立链表分为两步,第一步是初始化各个节点对象,第二步是构建节点之间的引用关系。

通常将头节点当作链表的代称。

由于已经没有任何变量引用P ,因此P会被 GC 回收。

链表的插入和删除确实是O(1) 但是查找既然是需要遍历。那应该是O(n)不是么?

插入节点

插入和删除时,修改指针时要注意顺序。

按位序插入

  • 不带头结点
  • 带头结点,推荐

尾插法建立单链表

头插法建立单链表

  • 头插法的重要应用:链表的逆置。

删除节点

删除指定节点:O(1),复制要删除的节点的后继结点数据域和指针域到要删除的节点。

查询节点

O(n)

2. 数组 vs 链表

3.常见链表类型

链表可以分为带头节点(也称哨兵节点或哑节点)和不带头节点两种形式,它们在实现和操作上有一些关键的区别:

  1. 不带头节点的链表:在这种链表中,链表的第一个元素直接由头指针指向。这意味着,如果链表为空,头指针将指向NULL。不带头节点的链表在插入和删除操作上可能稍微复杂一些,因为每次操作都需要特殊处理边界条件,比如当操作发生在链表头部时。
  2. 带头节点的链表:带有一个额外的头节点的链表,这个头节点通常不存储任何数据,只作为一个标记存在。头节点的存在简化了链表的插入和删除操作,因为它提供了一个统一的起点,使得所有插入和删除操作都变得相似,不需要特别考虑链表是否为空或者操作发生在链表头部的情况。

常见链表类型

  1. 单向链表:即前面介绍的普通链表。

  2. 双向链表:与单向链表相比,双向链表记录了两个方向的引用。

  3. 环形链表:如果我们令单向链表的尾节点指向头节点(首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。

    • 循环单链表:很多时候对链表的操作都是在头部或尾部,指针指向链表尾部。
    • 循环双链表
  4. 静态链表:用数组的方式实现的链表。适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

4.应用

高级数据结构:比如在红黑树、B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指向父节点的引用来实现,类似于双向链表。

必要使用链表的情况主要是二叉树和图。

列表

列表(list): 列表可以基于链表或数组实现。

动态数组(dynamic array)

  • 使用动态数组(dynamic array)来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。
  • 列表是一种抽象数据结构,可以基于数组或链表实现。在各编程语言中,列表通常基于数组实现。因此在本书中,除特别说明外,可以将“列表”和“动态数组”看作同一概念。
  • 动态数组是增加了扩容操作的数组。在容量没用完的情况下,“尾部添加元素”实际上只是在修改数组中某个元素的值,因此时间复杂度为 O(1) 。

1.列表基本操作

初始化列表

插入与删除元素

在列表末尾添加元素是不是也不严格为O(1),比如某些情况下列表正好需要先扩容再添加,这时候时间复杂度就会是O(N)。请问Python中的list是否也会存在这种问题?

拼接列表

2.列表实现

初始容量

数量记录

扩容机制

Java 的 ArrayList 也不会自动缩容,但可以通过调用 ArrayList 的 trimToSize() 方法来实现缩容。

在 Java 中,ArrayList 的底层始终是数组,与容量无关。HashMap 采用链式地址来解决哈希冲突,而当链表过长时,系统会将其转化为红黑树,以提升查询效率。

习题

回答问题要有逻辑

问题:请描述顺序表和链表,实现线性表时,用顺序表还是链表好?

顺序表和链表的逻辑结构都是线性结构,都属于线性表。

但是二者的存储结构不同,顺序表采用顺序存储(特点,带来的优点缺点);链表采用链式存储(特点、导致的优缺点)。

由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元素时.;当删除一个数据元素时..;当查找一个数据元素时…