hello-algo/chapter_array_and_linkedlist/summary.md
2023-07-01 22:39:20 +08:00

81 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
comments: true
---
# 4.4.   小结
- 数组和链表是两种基本数据结构,分别代表数据在计算机内存中的连续空间存储和离散空间存储方式。两者的优缺点呈现出互补的特性。
- 数组支持随机访问、占用内存较少;但插入和删除元素效率低,且初始化后长度不可变。
- 链表通过更改指针实现高效的节点插入与删除,且可以灵活调整长度;但节点访问效率低、占用内存较多。常见的链表类型包括单向链表、循环链表、双向链表。
- 动态数组,又称列表,是基于数组实现的一种数据结构。它保留了数组的优势,同时可以灵活调整长度。列表的出现极大地提高了数组的易用性,但可能导致部分内存空间浪费。
- 下表总结并对比了数组与链表的各项特性与操作效率。
<div class="center-table" markdown>
| | 数组 | 链表 |
| ------------ | ------------------------ | ------------ |
| 存储方式 | 连续内存空间 | 离散内存空间 |
| 数据结构长度 | 长度不可变 | 长度可变 |
| 内存使用率 | 占用内存少、缓存局部性好 | 占用内存多 |
| 优势操作 | 随机访问 | 插入、删除 |
| 访问元素 | $O(1)$ | $O(N)$ |
| 添加元素 | $O(N)$ | $O(1)$ |
| 删除元素 | $O(N)$ | $O(1)$ |
</div>
!!! note "缓存局部性"
在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存”。当我们访问数组元素时计算机不仅会加载它还会缓存其周围的其他数据从而借助高速缓存来提升后续操作的执行速度链表则不然计算机只能挨个地缓存各个节点这样的多次搬运降低了整体效率
## 4.4.1. &nbsp; Q & A
!!! question "数组存储在栈上和存储在堆上对时间效率和空间效率是否有影响"
栈内存分配由编译器自动完成而堆内存由程序员在代码中分配注意这里的栈和堆和数据结构中的栈和堆不是同一概念)。
1. 栈不灵活分配的内存大小不可更改堆相对灵活可以动态分配内存
2. 栈是一块比较小的内存容易出现内存不足堆内存很大但是由于是动态分配容易碎片化管理堆内存的难度更大成本更高
3. 访问栈比访问堆更快因为栈内存较小对缓存友好堆帧分散在很大的空间内会出现更多的缓存未命中
!!! question "为什么数组会强调要求相同类型的元素而在链表中却没有强调同类型呢"
链表由结点组成结点之间由指针连接各个结点可以存储不同类型的数据例如 int, double, string, object
相对地数组元素则必须是相同类型的这样才能通过计算偏移量来获取对应元素位置例如如果数组同时包含 int long 两种类型单个元素分别占用 4 bytes 8 bytes 那么此时就不能用以下公式计算偏移量了因为数组中包含了两种 `elementLength`
```
// 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
elementAddr = firtstElementAddr + elementLength * elementIndex
```
!!! question "删除节点后是否需要把 `P.next` 设为 $\text{None}$ "
不修改 `P.next` 也可以从该链表的角度看从头结点遍历到尾结点已经遇不到 `P` 这意味着结点 `P` 已经从链表中删除了此时结点 `P` 指向哪里都不会对这条链表产生影响了
从垃圾回收的角度看对于 Java, Python, Go 等拥有自动垃圾回收的语言来说节点 `P` 是否被回收取决于是否有仍存在指向它的引用而不是 `P.next` 的值 C, C++ 等语言中我们需要手动释放节点内存
!!! question "在链表中插入和删除操作的时间复杂度是 $O(1)$ 但是增删之前都需要 $O(n)$ 查找元素那为什么时间复杂度不是 $O(n)$ "
如果是先查找元素再删除元素确实是 $O(n)$ 然而链表的 $O(1)$ 增删的优势可以在其他应用上得到体现例如双向队列适合使用链表实现我们维护一个指针变量始终指向头结点尾结点每次插入与删除操作都是 $O(1)$
!!! question "图片链表定义与存储方式浅蓝色的存储结点指针是占用一块内存地址吗还是和结点值各占一半呢"
文中只是一个示意图只是定性表示定量的话需要根据具体情况分析
- 不同类型的结点值占用的空间是不同的比如 int, long, double, 或者是类的实例等等
- 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定大多为 8 字节或 4 字节
!!! question "在列表末尾添加元素是否时时刻刻都为 $O(1)$ "
如果添加元素时超出列表长度则需要先扩容列表再添加系统会申请一块新的内存并将原列表的所有元素搬运过去这时候时间复杂度就会是 $O(n)$
!!! question "“列表的出现大大提升了数组的实用性但副作用是会造成部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量长度扩容倍数所占的内存吗"
这里的空间浪费主要有两方面含义一方面列表都会设定一个初始长度我们不一定需要用这么多另一方面为了防止频繁扩容扩容一般都会乘以一个系数比如 $\times 1.5$ 这样一来也会出现很多空位我们通常不能完全填满它们
!!! question " Python 中初始化 `n = [1, 2, 3]` 3 个元素的地址是相连的但是初始化 `m = [2, 1, 3]` 会发现它们每个元素的 id 并不是连续的而是分别跟 `n` 中的相同这些元素地址不连续那么 `m` 还是数组吗"
假如把列表元素换成链表节点 `n = [n1, n2, n3, n4, n5]` 通常情况下这五个节点对象也是被分散存储在内存各处的然而给定一个列表索引我们仍然可以在 $O(1)$ 时间内获取到节点内存地址从而访问到对应的节点这是因为数组中存储的是节点的引用而非节点本身
与许多语言不同的是 Python 中数字也被包装为对象列表中存储的不是数字本身而是对数字的引用因此我们会发现两个数组中的相同数字拥有同一个 id 并且这些数字的内存地址是无需连续的