Skip to content

Commit

Permalink
Update the format of the Q&As (krahets#1031)
Browse files Browse the repository at this point in the history
  • Loading branch information
krahets authored Jan 8, 2024
1 parent e7c39dd commit c4e4a60
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 134 deletions.
74 changes: 37 additions & 37 deletions docs/chapter_array_and_linkedlist/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,68 +13,68 @@

### Q & A

!!! question "数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?"
**Q**数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?

存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。
存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。

1. 分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。
2. 大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。
3. 灵活性:栈上的数组的大小需要在编译时确定,而堆上的数组的大小可以在运行时动态确定。
1. 分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。
2. 大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。
3. 灵活性:栈上的数组的大小需要在编译时确定,而堆上的数组的大小可以在运行时动态确定。

!!! question "为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?"
**Q**为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?

链表由节点组成,节点之间通过引用(指针)连接,各个节点可以存储不同类型的数据,例如 `int`、`double`、`string`、`object` 等。
链表由节点组成,节点之间通过引用(指针)连接,各个节点可以存储不同类型的数据,例如 `int``double``string``object` 等。

相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,数组同时包含 `int` 和 `long` 两种类型,单个元素分别占用 4 字节 和 8 字节 ,此时就不能用以下公式计算偏移量了,因为数组中包含了两种“元素长度”。
相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,数组同时包含 `int``long` 两种类型,单个元素分别占用 4 字节 和 8 字节 ,此时就不能用以下公式计算偏移量了,因为数组中包含了两种“元素长度”。

```shell
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
```
```shell
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
```

!!! question "删除节点后,是否需要把 `P.next` 设为 `None` 呢?"
**Q**删除节点后,是否需要把 `P.next` 设为 `None` 呢?

不修改 `P.next` 也可以。从该链表的角度看,从头节点遍历到尾节点已经不会遇到 `P` 了。这意味着节点 `P` 已经从链表中删除了,此时节点 `P` 指向哪里都不会对该链表产生影响。
不修改 `P.next` 也可以。从该链表的角度看,从头节点遍历到尾节点已经不会遇到 `P` 了。这意味着节点 `P` 已经从链表中删除了,此时节点 `P` 指向哪里都不会对该链表产生影响。

从垃圾回收的角度看,对于 Java、Python、Go 等拥有自动垃圾回收机制的语言来说,节点 `P` 是否被回收取决于是否仍存在指向它的引用,而不是 `P.next` 的值。在 C 和 C++ 等语言中,我们需要手动释放节点内存。
从垃圾回收的角度看,对于 Java、Python、Go 等拥有自动垃圾回收机制的语言来说,节点 `P` 是否被回收取决于是否仍存在指向它的引用,而不是 `P.next` 的值。在 C 和 C++ 等语言中,我们需要手动释放节点内存。

!!! question "在链表中插入和删除操作的时间复杂度是 $O(1)$ 。但是增删之前都需要 $O(n)$ 的时间查找元素,那为什么时间复杂度不是 $O(n)$ 呢?"
**Q**在链表中插入和删除操作的时间复杂度是 $O(1)$ 。但是增删之前都需要 $O(n)$ 的时间查找元素,那为什么时间复杂度不是 $O(n)$ 呢?

如果是先查找元素、再删除元素,时间复杂度确实是 $O(n)$ 。然而,链表的 $O(1)$ 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头节点、尾节点,每次插入与删除操作都是 $O(1)$ 。
如果是先查找元素、再删除元素,时间复杂度确实是 $O(n)$ 。然而,链表的 $O(1)$ 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头节点、尾节点,每次插入与删除操作都是 $O(1)$ 。

!!! question "图“链表定义与存储方式”中,浅蓝色的存储节点指针是占用一块内存地址吗?还是和节点值各占一半呢?"
**Q**图“链表定义与存储方式”中,浅蓝色的存储节点指针是占用一块内存地址吗?还是和节点值各占一半呢?

该示意图只是定性表示,定量表示需要根据具体情况进行分析。
该示意图只是定性表示,定量表示需要根据具体情况进行分析。

- 不同类型的节点值占用的空间是不同的,比如 `int`、`long`、`double` 和实例对象等。
- 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。
- 不同类型的节点值占用的空间是不同的,比如 `int``long``double` 和实例对象等。
- 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。

!!! question "在列表末尾添加元素是否时时刻刻都为 $O(1)$ ?"
**Q**在列表末尾添加元素是否时时刻刻都为 $O(1)$ ?

如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 $O(n)$ 。
如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 $O(n)$ 。

!!! question "“列表的出现极大地提高了数组的实用性,但可能导致部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?"
**Q**“列表的出现极大地提高了数组的实用性,但可能导致部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?

这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多;另一方面,为了防止频繁扩容,扩容一般会乘以一个系数,比如 $\times 1.5$ 。这样一来,也会出现很多空位,我们通常不能完全填满它们。
这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多;另一方面,为了防止频繁扩容,扩容一般会乘以一个系数,比如 $\times 1.5$ 。这样一来,也会出现很多空位,我们通常不能完全填满它们。

!!! question "在 Python 中初始化 `n = [1, 2, 3]` 后,这 3 个元素的地址是相连的,但是初始化 `m = [2, 1, 3]` 会发现它们每个元素的 id 并不是连续的,而是分别跟 `n` 中的相同。这些元素的地址不连续,那么 `m` 还是数组吗?"
**Q**在 Python 中初始化 `n = [1, 2, 3]` 后,这 3 个元素的地址是相连的,但是初始化 `m = [2, 1, 3]` 会发现它们每个元素的 id 并不是连续的,而是分别跟 `n` 中的相同。这些元素的地址不连续,那么 `m` 还是数组吗?

假如把列表元素换成链表节点 `n = [n1, n2, n3, n4, n5]` ,通常情况下这 5 个节点对象也分散存储在内存各处。然而,给定一个列表索引,我们仍然可以在 $O(1)$ 时间内获取节点内存地址,从而访问到对应的节点。这是因为数组中存储的是节点的引用,而非节点本身。
假如把列表元素换成链表节点 `n = [n1, n2, n3, n4, n5]` ,通常情况下这 5 个节点对象也分散存储在内存各处。然而,给定一个列表索引,我们仍然可以在 $O(1)$ 时间内获取节点内存地址,从而访问到对应的节点。这是因为数组中存储的是节点的引用,而非节点本身。

与许多语言不同,Python 中的数字也被包装为对象,列表中存储的不是数字本身,而是对数字的引用。因此,我们会发现两个数组中的相同数字拥有同一个 id ,并且这些数字的内存地址无须连续。
与许多语言不同,Python 中的数字也被包装为对象,列表中存储的不是数字本身,而是对数字的引用。因此,我们会发现两个数组中的相同数字拥有同一个 id ,并且这些数字的内存地址无须连续。

!!! question "C++ STL 里面的 `std::list` 已经实现了双向链表,但好像一些算法书上不怎么直接使用它,是不是因为有什么局限性呢?"
**Q**C++ STL 里面的 `std::list` 已经实现了双向链表,但好像一些算法书上不怎么直接使用它,是不是因为有什么局限性呢?

一方面,我们往往更青睐使用数组实现算法,而只在必要时才使用链表,主要有两个原因。

- 空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 `std::list` 通常比 `std::vector` 更占用空间。
- 缓存不友好:由于数据不是连续存放的,因此 `std::list` 对缓存的利用率较低。一般情况下,`std::vector` 的性能会更好。
一方面,我们往往更青睐使用数组实现算法,而只在必要时才使用链表,主要有两个原因。

另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 `stack` 和 `queue` ,而非链表。
- 空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 `std::list` 通常比 `std::vector` 更占用空间。
- 缓存不友好:由于数据不是连续存放的,因此 `std::list` 对缓存的利用率较低。一般情况下,`std::vector` 的性能会更好。

!!! question "初始化列表 `res = [0] * self.size()` 操作,会导致 `res` 的每个元素引用相同的地址吗?"
另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 `stack``queue` ,而非链表。

不会。但二维数组会有这个问题,例如初始化二维列表 `res = [[0] * self.size()]` ,则多次引用了同一个列表 `[0]` 。
**Q**:初始化列表 `res = [0] * self.size()` 操作,会导致 `res` 的每个元素引用相同的地址吗?

!!! question "在删除节点中,需要断开该节点与其后继节点之间的引用指向吗?"
不会。但二维数组会有这个问题,例如初始化二维列表 `res = [[0] * self.size()]` ,则多次引用了同一个列表 `[0]`

从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。
**Q**:在删除节点中,需要断开该节点与其后继节点之间的引用指向吗?

从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。
8 changes: 4 additions & 4 deletions docs/chapter_backtracking/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

### Q & A

!!! question "怎么理解回溯和递归的关系?"
**Q**怎么理解回溯和递归的关系?

总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。
总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。

- 回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。
- 递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。
- 回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。
- 递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。
26 changes: 13 additions & 13 deletions docs/chapter_computational_complexity/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,24 @@

### Q & A

!!! question "尾递归的空间复杂度是 $O(1)$ 吗?"
**Q**尾递归的空间复杂度是 $O(1)$ 吗?

理论上,尾递归函数的空间复杂度可以优化至 $O(1)$ 。不过绝大多数编程语言(例如 Java、Python、C++、Go、C# 等)不支持自动优化尾递归,因此通常认为空间复杂度是 $O(n)$ 。
理论上,尾递归函数的空间复杂度可以优化至 $O(1)$ 。不过绝大多数编程语言(例如 Java、Python、C++、Go、C# 等)不支持自动优化尾递归,因此通常认为空间复杂度是 $O(n)$ 。

!!! question "函数和方法这两个术语的区别是什么?"
**Q**函数和方法这两个术语的区别是什么?

「函数 function」可以被独立执行,所有参数都以显式传递。「方法 method」与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。
「函数 function」可以被独立执行,所有参数都以显式传递。「方法 method」与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。

下面以几种常见的编程语言为例来说明。
下面以几种常见的编程语言为例来说明。

- C 语言是过程式编程语言,没有面向对象的概念,所以只有函数。但我们可以通过创建结构体(struct)来模拟面向对象编程,与结构体相关联的函数就相当于其他编程语言中的方法。
- Java 和 C# 是面向对象的编程语言,代码块(方法)通常作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。
- C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。
- C 语言是过程式编程语言,没有面向对象的概念,所以只有函数。但我们可以通过创建结构体(struct)来模拟面向对象编程,与结构体相关联的函数就相当于其他编程语言中的方法。
- Java 和 C# 是面向对象的编程语言,代码块(方法)通常作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。
- C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。

!!! question "图解“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?"
**Q**图解“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?

不是,该图展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。

假设取 $n = 8$ ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。
不是,该图展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。

在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 $n = 8$ 之下的最优解法。但对于 $n = 8^5$ 就很好选了,这时增长趋势已经占主导了。
假设取 $n = 8$ ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。

在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 $n = 8$ 之下的最优解法。但对于 $n = 8^5$ 就很好选了,这时增长趋势已经占主导了。
Loading

0 comments on commit c4e4a60

Please sign in to comment.