This commit is contained in:
krahets
2023-08-27 23:40:56 +08:00
parent 48980ddf28
commit df0f7d3be1
64 changed files with 255 additions and 261 deletions

View File

@@ -623,7 +623,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
}
```
总的来看,数组的插入与删除操作有以下缺点
总的来看,数组的插入与删除操作有以下缺点
- **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(n)$ ,其中 $n$ 为数组长度。
- **丢失元素**:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。
@@ -1206,13 +1206,13 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
## 4.1.2   数组优点与局限性
数组存储在连续的内存空间内,且元素类型相同。这包含丰富的先验信息,系统可以利用这些信息来优化操作和运行效率,包括:
数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。
- **空间效率高**: 数组为数据分配了连续的内存块,无须额外的结构开销。
- **支持随机访问**: 数组允许在 $O(1)$ 时间内访问任何元素。
- **缓存局部性**: 当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。
连续空间存储是一把双刃剑,它导致的缺点有:
连续空间存储是一把双刃剑,其存在以下缺点。
- **插入与删除效率低**:当数组中元素较多时,插入与删除操作需要移动大量的元素。
- **长度不可变**: 数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
@@ -1220,7 +1220,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
## 4.1.3   数组典型应用
数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构,主要包括:
数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构
- **随机访问**:如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。
- **排序和搜索**:数组是排序和搜索算法最常用的数据结构。快速排序、归并排序、二分查找等都主要在数组上进行。

View File

@@ -17,8 +17,8 @@ comments: true
观察图 4-5 ,链表的组成单位是「节点 node」对象。每个节点都包含两项数据节点的“值”和指向下一节点的“引用”。
- 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
- 尾节点指向的是“空”,它在 Java, C++, Python 中分别被记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。
- 在 C, C++, Go, Rust 等支持指针的语言中,上述的“引用”应被替换为“指针”。
- 尾节点指向的是“空”,它在 JavaC++ Python 中分别被记为 $\text{null}$$\text{nullptr}$ $\text{None}$ 。
- 在 CC++、Go 和 Rust 等支持指针的语言中,上述的“引用”应被替换为“指针”。
如以下代码所示,链表节点 `ListNode` 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,**链表比数组占用更多的内存空间**。
@@ -401,11 +401,11 @@ comments: true
n3.borrow_mut().next = Some(n4.clone());
```
数组整体是一个变量,比如数组 `nums` 包含元素 `nums[0]` , `nums[1]` 等,而链表是由多个独立的节点对象组成的。**我们通常将头节点当作链表的代称**,比如以上代码中的链表可被记做链表 `n0` 。
数组整体是一个变量,比如数组 `nums` 包含元素 `nums[0]` `nums[1]` 等,而链表是由多个独立的节点对象组成的。**我们通常将头节点当作链表的代称**,比如以上代码中的链表可被记做链表 `n0` 。
### 2.   插入节点
在链表中插入节点非常容易。如图 4-6 所示,假设我们想在相邻的两个节点 `n0` , `n1` 之间插入一个新节点 `P` **则只需要改变两个节点引用(指针)即可**,时间复杂度为 $O(1)$ 。
在链表中插入节点非常容易。如图 4-6 所示,假设我们想在相邻的两个节点 `n0` `n1` 之间插入一个新节点 `P` **则只需要改变两个节点引用(指针)即可**,时间复杂度为 $O(1)$ 。
相比之下,在数组中插入元素的时间复杂度为 $O(n)$ ,在大数据量下的效率较低。

View File

@@ -856,9 +856,9 @@ comments: true
## 4.3.2   列表实现
许多编程语言都提供内置的列表,例如 Java, C++, Python 等。它们的实现比较复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。
许多编程语言都提供内置的列表,例如 JavaC++Python 等。它们的实现比较复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。
为了帮助你理解列表工作原理,我们在此提供一个简易版列表实现,重点包括:
为了加深对列表工作原理的理解,我们尝试实现一个简易版列表,包括以下三个重点设计。
- **初始容量**:选取一个合理的数组初始容量。在本示例中,我们选择 10 作为初始容量。
- **数量记录**:声明一个变量 size用于记录列表当前元素数量并随着元素插入和删除实时更新。根据此变量我们可以定位列表尾部以及判断是否需要扩容。

View File

@@ -21,7 +21,7 @@ comments: true
!!! question "为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?"
链表由结点组成,结点之间通过引用(指针)连接,各个结点可以存储不同类型的数据,例如 int, double, string, object 等。
链表由结点组成,结点之间通过引用(指针)连接,各个结点可以存储不同类型的数据,例如 intdoublestringobject 等。
相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,如果数组同时包含 int 和 long 两种类型,单个元素分别占用 4 bytes 和 8 bytes ,那么此时就不能用以下公式计算偏移量了,因为数组中包含了两种 `elementLength`
@@ -34,7 +34,7 @@ comments: true
不修改 `P.next` 也可以。从该链表的角度看,从头结点遍历到尾结点已经遇不到 `P` 了。这意味着结点 `P` 已经从链表中删除了,此时结点 `P` 指向哪里都不会对这条链表产生影响了。
从垃圾回收的角度看,对于 Java, Python, Go 等拥有自动垃圾回收的语言来说,节点 `P` 是否被回收取决于是否有仍存在指向它的引用,而不是 `P.next` 的值。在 C, C++ 等语言中,我们需要手动释放节点内存。
从垃圾回收的角度看,对于 JavaPythonGo 等拥有自动垃圾回收的语言来说,节点 `P` 是否被回收取决于是否有仍存在指向它的引用,而不是 `P.next` 的值。在 C C++ 等语言中,我们需要手动释放节点内存。
!!! question "在链表中插入和删除操作的时间复杂度是 $O(1)$ 。但是增删之前都需要 $O(n)$ 查找元素,那为什么时间复杂度不是 $O(n)$ 呢?"
@@ -42,9 +42,9 @@ comments: true
!!! question "图片“链表定义与存储方式”中,浅蓝色的存储结点指针是占用一块内存地址吗?还是和结点值各占一半呢?"
文中只是一个示意图只是定性表示定量的话需要根据具体情况分析
文中示意图只是定性表示定量表示需要根据具体情况进行分析
- 不同类型的结点值占用的空间是不同的,比如 int, long, double, 或者是类的实例等等。
- 不同类型的结点值占用的空间是不同的,比如 intlongdouble 和实例对象等。
- 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。
!!! question "在列表末尾添加元素是否时时刻刻都为 $O(1)$ "
@@ -63,9 +63,9 @@ comments: true
!!! question "C++ STL 里面的 std::list 已经实现了双向链表,但好像一些算法的书上都不怎么直接用这个,是不是有什么局限性呢?"
一方面,我们往往更青睐使用数组实现算法,而只有在必要时才使用链表。这是因为:
一方面,我们往往更青睐使用数组实现算法,而只有在必要时才使用链表,主要有两个原因
1. 空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 `std::list` 通常比 `std::vector` 更占用空间。
2. 缓存不友好:由于数据不是连续存放的,`std::list` 对缓存的利用率较低。一般情况下,`std::vector` 的性能会更好。
- 空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 `std::list` 通常比 `std::vector` 更占用空间。
- 缓存不友好:由于数据不是连续存放的,`std::list` 对缓存的利用率较低。一般情况下,`std::vector` 的性能会更好。
另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 `stack` 和 `queue` ,而非链表。