二、数组和链表
本章中的数组和列表,都属于线性表的顺序存储。
数组和链表是两种基本的数据结构,分别代表数据在计算机内存中的两种存储方式:连续空间存储和分散空间存储。两者的特点呈现出互补的特性。
数组
数组(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.常见链表类型
链表可以分为带头节点(也称哨兵节点或哑节点)和不带头节点两种形式,它们在实现和操作上有一些关键的区别:
- 不带头节点的链表:在这种链表中,链表的第一个元素直接由头指针指向。这意味着,如果链表为空,头指针将指向
NULL。不带头节点的链表在插入和删除操作上可能稍微复杂一些,因为每次操作都需要特殊处理边界条件,比如当操作发生在链表头部时。 - 带头节点的链表:带有一个额外的头节点的链表,这个头节点通常不存储任何数据,只作为一个标记存在。头节点的存在简化了链表的插入和删除操作,因为它提供了一个统一的起点,使得所有插入和删除操作都变得相似,不需要特别考虑链表是否为空或者操作发生在链表头部的情况。
常见链表类型
-
单向链表:即前面介绍的普通链表。
-
双向链表:与单向链表相比,双向链表记录了两个方向的引用。
-
环形链表:如果我们令单向链表的尾节点指向头节点(首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。
- 循环单链表:很多时候对链表的操作都是在头部或尾部,指针指向链表尾部。
- 循环双链表
-
静态链表:用数组的方式实现的链表。适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表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 采用链式地址来解决哈希冲突,而当链表过长时,系统会将其转化为红黑树,以提升查询效率。
习题
回答问题要有逻辑
问题:请描述顺序表和链表,实现线性表时,用顺序表还是链表好?
顺序表和链表的逻辑结构都是线性结构,都属于线性表。
但是二者的存储结构不同,顺序表采用顺序存储(特点,带来的优点缺点);链表采用链式存储(特点、导致的优缺点)。
由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元素时.;当删除一个数据元素时..;当查找一个数据元素时…