mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-25 09:46:29 +08:00
Update punctuation
This commit is contained in:
parent
c5a7323817
commit
57851ab11e
50 changed files with 100 additions and 100 deletions
|
@ -11,8 +11,8 @@
|
||||||
观察上图,链表的组成单位是「节点 node」对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。
|
观察上图,链表的组成单位是「节点 node」对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。
|
||||||
|
|
||||||
- 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
|
- 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
|
||||||
- 尾节点指向的是“空”,它在 Java, C++, Python 中分别被记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。
|
- 尾节点指向的是“空”,它在 Java、C++ 和 Python 中分别被记为 $\text{null}$、$\text{nullptr}$ 和 $\text{None}$ 。
|
||||||
- 在 C, C++, Go, Rust 等支持指针的语言中,上述的“引用”应被替换为“指针”。
|
- 在 C、C++、Go 和 Rust 等支持指针的语言中,上述的“引用”应被替换为“指针”。
|
||||||
|
|
||||||
如以下代码所示,链表节点 `ListNode` 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,**链表比数组占用更多的内存空间**。
|
如以下代码所示,链表节点 `ListNode` 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,**链表比数组占用更多的内存空间**。
|
||||||
|
|
||||||
|
@ -395,11 +395,11 @@
|
||||||
n3.borrow_mut().next = Some(n4.clone());
|
n3.borrow_mut().next = Some(n4.clone());
|
||||||
```
|
```
|
||||||
|
|
||||||
数组整体是一个变量,比如数组 `nums` 包含元素 `nums[0]` , `nums[1]` 等,而链表是由多个独立的节点对象组成的。**我们通常将头节点当作链表的代称**,比如以上代码中的链表可被记做链表 `n0` 。
|
数组整体是一个变量,比如数组 `nums` 包含元素 `nums[0]` 和 `nums[1]` 等,而链表是由多个独立的节点对象组成的。**我们通常将头节点当作链表的代称**,比如以上代码中的链表可被记做链表 `n0` 。
|
||||||
|
|
||||||
### 插入节点
|
### 插入节点
|
||||||
|
|
||||||
在链表中插入节点非常容易。如下图所示,假设我们想在相邻的两个节点 `n0` , `n1` 之间插入一个新节点 `P` ,**则只需要改变两个节点引用(指针)即可**,时间复杂度为 $O(1)$ 。
|
在链表中插入节点非常容易。如下图所示,假设我们想在相邻的两个节点 `n0` 和 `n1` 之间插入一个新节点 `P` ,**则只需要改变两个节点引用(指针)即可**,时间复杂度为 $O(1)$ 。
|
||||||
|
|
||||||
相比之下,在数组中插入元素的时间复杂度为 $O(n)$ ,在大数据量下的效率较低。
|
相比之下,在数组中插入元素的时间复杂度为 $O(n)$ ,在大数据量下的效率较低。
|
||||||
|
|
||||||
|
|
|
@ -852,7 +852,7 @@
|
||||||
|
|
||||||
## 列表实现
|
## 列表实现
|
||||||
|
|
||||||
许多编程语言都提供内置的列表,例如 Java, C++, Python 等。它们的实现比较复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。
|
许多编程语言都提供内置的列表,例如 Java、C++、Python 等。它们的实现比较复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。
|
||||||
|
|
||||||
为了加深对列表工作原理的理解,我们尝试实现一个简易版列表,包括以下三个重点设计。
|
为了加深对列表工作原理的理解,我们尝试实现一个简易版列表,包括以下三个重点设计。
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
!!! question "为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?"
|
!!! question "为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?"
|
||||||
|
|
||||||
链表由结点组成,结点之间通过引用(指针)连接,各个结点可以存储不同类型的数据,例如 int, double, string, object 等。
|
链表由结点组成,结点之间通过引用(指针)连接,各个结点可以存储不同类型的数据,例如 int、double、string、object 等。
|
||||||
|
|
||||||
相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,如果数组同时包含 int 和 long 两种类型,单个元素分别占用 4 bytes 和 8 bytes ,那么此时就不能用以下公式计算偏移量了,因为数组中包含了两种 `elementLength` 。
|
相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,如果数组同时包含 int 和 long 两种类型,单个元素分别占用 4 bytes 和 8 bytes ,那么此时就不能用以下公式计算偏移量了,因为数组中包含了两种 `elementLength` 。
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
不修改 `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)$ 呢?"
|
!!! question "在链表中插入和删除操作的时间复杂度是 $O(1)$ 。但是增删之前都需要 $O(n)$ 查找元素,那为什么时间复杂度不是 $O(n)$ 呢?"
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
文中的示意图只是定性表示,定量表示需要根据具体情况进行分析。
|
文中的示意图只是定性表示,定量表示需要根据具体情况进行分析。
|
||||||
|
|
||||||
- 不同类型的结点值占用的空间是不同的,比如 int, long, double, 或者是类的实例等等。
|
- 不同类型的结点值占用的空间是不同的,比如 int、long、double 和实例对象等。
|
||||||
- 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。
|
- 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。
|
||||||
|
|
||||||
!!! question "在列表末尾添加元素是否时时刻刻都为 $O(1)$ ?"
|
!!! question "在列表末尾添加元素是否时时刻刻都为 $O(1)$ ?"
|
||||||
|
|
|
@ -136,6 +136,6 @@
|
||||||
[class]{}-[func]{n_queens}
|
[class]{}-[func]{n_queens}
|
||||||
```
|
```
|
||||||
|
|
||||||
逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n, n-1, \dots, 2, 1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。
|
逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n$、$n-1$、$\dots$、$2$、$1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。
|
||||||
|
|
||||||
数组 `state` 使用 $O(n^2)$ 空间,数组 `cols` , `diags1` , `diags2` 皆使用 $O(n)$ 空间。最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。因此,**空间复杂度为 $O(n^2)$** 。
|
数组 `state` 使用 $O(n^2)$ 空间,数组 `cols`、`diags1` 和 `diags2` 皆使用 $O(n)$ 空间。最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。因此,**空间复杂度为 $O(n^2)$** 。
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
- 在做出选择 `choice[i]` 后,我们就将 `selected[i]` 赋值为 $\text{True}$ ,代表它已被选择。
|
- 在做出选择 `choice[i]` 后,我们就将 `selected[i]` 赋值为 $\text{True}$ ,代表它已被选择。
|
||||||
- 遍历选择列表 `choices` 时,跳过所有已被选择过的节点,即剪枝。
|
- 遍历选择列表 `choices` 时,跳过所有已被选择过的节点,即剪枝。
|
||||||
|
|
||||||
如下图所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1, 3 的分支。
|
如下图所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1 和元素 3 的分支。
|
||||||
|
|
||||||
![全排列剪枝示例](permutations_problem.assets/permutations_i_pruning.png)
|
![全排列剪枝示例](permutations_problem.assets/permutations_i_pruning.png)
|
||||||
|
|
||||||
|
|
|
@ -128,14 +128,14 @@
|
||||||
|
|
||||||
**我们考虑在搜索过程中通过剪枝进行去重**。观察下图,重复子集是在以不同顺序选择数组元素时产生的,例如以下情况。
|
**我们考虑在搜索过程中通过剪枝进行去重**。观察下图,重复子集是在以不同顺序选择数组元素时产生的,例如以下情况。
|
||||||
|
|
||||||
1. 当第一轮和第二轮分别选择 $3$ , $4$ 时,会生成包含这两个元素的所有子集,记为 $[3, 4, \dots]$ 。
|
1. 当第一轮和第二轮分别选择 $3$ 和 $4$ 时,会生成包含这两个元素的所有子集,记为 $[3, 4, \dots]$ 。
|
||||||
2. 之后,当第一轮选择 $4$ 时,**则第二轮应该跳过 $3$** ,因为该选择产生的子集 $[4, 3, \dots]$ 和 `1.` 中生成的子集完全重复。
|
2. 之后,当第一轮选择 $4$ 时,**则第二轮应该跳过 $3$** ,因为该选择产生的子集 $[4, 3, \dots]$ 和 `1.` 中生成的子集完全重复。
|
||||||
|
|
||||||
在搜索中,每一层的选择都是从左到右被逐个尝试的,因此越靠右的分支被剪掉的越多。
|
在搜索中,每一层的选择都是从左到右被逐个尝试的,因此越靠右的分支被剪掉的越多。
|
||||||
|
|
||||||
1. 前两轮选择 $3$ , $5$ ,生成子集 $[3, 5, \dots]$ 。
|
1. 前两轮选择 $3$ 和 $5$ ,生成子集 $[3, 5, \dots]$ 。
|
||||||
2. 前两轮选择 $4$ , $5$ ,生成子集 $[4, 5, \dots]$ 。
|
2. 前两轮选择 $4$ 和 $5$ ,生成子集 $[4, 5, \dots]$ 。
|
||||||
3. 若第一轮选择 $5$ ,**则第二轮应该跳过 $3$ 和 $4$** ,因为子集 $[5, 3, \dots]$ 和子集 $[5, 4, \dots]$ 和 `1.` , `2.` 中描述的子集完全重复。
|
3. 若第一轮选择 $5$ ,**则第二轮应该跳过 $3$ 和 $4$** ,因为子集 $[5, 3, \dots]$ 和 $[5, 4, \dots]$ 与第 `1.` 和 `2.` 步中描述的子集完全重复。
|
||||||
|
|
||||||
![不同选择顺序导致的重复子集](subset_sum_problem.assets/subset_sum_i_pruning.png)
|
![不同选择顺序导致的重复子集](subset_sum_problem.assets/subset_sum_i_pruning.png)
|
||||||
|
|
||||||
|
|
|
@ -999,7 +999,7 @@ $$
|
||||||
[class]{}-[func]{quadratic}
|
[class]{}-[func]{quadratic}
|
||||||
```
|
```
|
||||||
|
|
||||||
如下图所示,该函数的递归深度为 $n$ ,在每个递归函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $n / 2$ ,因此总体占用 $O(n^2)$ 空间:
|
如下图所示,该函数的递归深度为 $n$ ,在每个递归函数中都初始化了一个数组,长度分别为 $n$、$n-1$、$\dots$、$2$、$1$ ,平均长度为 $n / 2$ ,因此总体占用 $O(n^2)$ 空间:
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
|
|
@ -1210,7 +1210,7 @@ $$
|
||||||
|
|
||||||
![常数阶、线性阶和平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png)
|
![常数阶、线性阶和平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png)
|
||||||
|
|
||||||
以冒泡排序为例,外层循环执行 $n - 1$ 次,内层循环执行 $n-1, n-2, \dots, 2, 1$ 次,平均为 $n / 2$ 次,因此时间复杂度为 $O((n - 1) n / 2) = O(n^2)$ 。
|
以冒泡排序为例,外层循环执行 $n - 1$ 次,内层循环执行 $n-1$、$n-2$、$\dots$、$2$、$1$ 次,平均为 $n / 2$ 次,因此时间复杂度为 $O((n - 1) n / 2) = O(n^2)$ 。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
**基本数据类型是 CPU 可以直接进行运算的类型**,在算法中直接被使用,主要包括以下几种类型。
|
**基本数据类型是 CPU 可以直接进行运算的类型**,在算法中直接被使用,主要包括以下几种类型。
|
||||||
|
|
||||||
- 整数类型 `byte` , `short` , `int` , `long` 。
|
- 整数类型 `byte`、`short`、`int`、`long` 。
|
||||||
- 浮点数类型 `float` , `double` ,用于表示小数。
|
- 浮点数类型 `float`、`double` ,用于表示小数。
|
||||||
- 字符类型 `char` ,用于表示各种语言的字母、标点符号、甚至表情符号等。
|
- 字符类型 `char` ,用于表示各种语言的字母、标点符号、甚至表情符号等。
|
||||||
- 布尔类型 `bool` ,用于表示“是”与“否”判断。
|
- 布尔类型 `bool` ,用于表示“是”与“否”判断。
|
||||||
|
|
||||||
|
@ -33,15 +33,15 @@
|
||||||
|
|
||||||
对于上表,需要注意以下几点。
|
对于上表,需要注意以下几点。
|
||||||
|
|
||||||
- C, C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 LP64 [数据模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用于 Unix 64 位操作系统(例如 Linux , macOS)。
|
- C 和 C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 LP64 [数据模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用于包括 Linux 和 macOS 在内的 Unix 64 位操作系统。
|
||||||
- 字符 `char` 的大小在 C, C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见“字符编码”章节。
|
- 字符 `char` 的大小在 C 和 C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见“字符编码”章节。
|
||||||
- 即使表示布尔量仅需 1 位($0$ 或 $1$),它在内存中通常被存储为 1 字节。这是因为现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。
|
- 即使表示布尔量仅需 1 位($0$ 或 $1$),它在内存中通常被存储为 1 字节。这是因为现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。
|
||||||
|
|
||||||
那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。它的主语是“结构”而非“数据”。
|
那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。它的主语是“结构”而非“数据”。
|
||||||
|
|
||||||
如果想要表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 `int` 、小数 `float` 、还是字符 `char` ,则与“数据结构”无关。
|
如果想要表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 `int`、小数 `float`或是字符 `char` ,则与“数据结构”无关。
|
||||||
|
|
||||||
换句话说,**基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”**。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型(`int` , `float` , `char`, `bool`)。
|
换句话说,**基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”**。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型,包括 `int`、`float`、`char`、`bool` 等。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
## ASCII 字符集
|
## ASCII 字符集
|
||||||
|
|
||||||
「ASCII 码」是最早出现的字符集,全称为“美国标准信息交换代码”。它使用 7 位二进制数(即一个字节的低 7 位)表示一个字符,最多能够表示 128 个不同的字符。如下图所示,ASCII 码包括英文字母的大小写、数字 0-9 、一些标点符号,以及一些控制字符(如换行符和制表符)。
|
「ASCII 码」是最早出现的字符集,全称为“美国标准信息交换代码”。它使用 7 位二进制数(即一个字节的低 7 位)表示一个字符,最多能够表示 128 个不同的字符。如下图所示,ASCII 码包括英文字母的大小写、数字 0 ~ 9、一些标点符号,以及一些控制字符(如换行符和制表符)。
|
||||||
|
|
||||||
![ASCII 码](character_encoding.assets/ascii_table.png)
|
![ASCII 码](character_encoding.assets/ascii_table.png)
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
- 常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性和非线性结构。
|
- 常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性和非线性结构。
|
||||||
- 当程序运行时,数据被存储在计算机内存中。每个内存空间都拥有对应的内存地址,程序通过这些内存地址访问数据。
|
- 当程序运行时,数据被存储在计算机内存中。每个内存空间都拥有对应的内存地址,程序通过这些内存地址访问数据。
|
||||||
- 物理结构主要分为连续空间存储(数组)和离散空间存储(链表)。所有数据结构都是由数组、链表或两者的组合实现的。
|
- 物理结构主要分为连续空间存储(数组)和离散空间存储(链表)。所有数据结构都是由数组、链表或两者的组合实现的。
|
||||||
- 计算机中的基本数据类型包括整数 `byte` , `short` , `int` , `long` 、浮点数 `float` , `double` 、字符 `char` 和布尔 `boolean` 。它们的取值范围取决于占用空间大小和表示方式。
|
- 计算机中的基本数据类型包括整数 `byte`、`short`、`int`、`long` ,浮点数 `float`、`double` ,字符 `char` 和布尔 `boolean` 。它们的取值范围取决于占用空间大小和表示方式。
|
||||||
- 原码、反码和补码是在计算机中编码数字的三种方法,它们之间是可以相互转换的。整数的原码的最高位是符号位,其余位是数字的值。
|
- 原码、反码和补码是在计算机中编码数字的三种方法,它们之间是可以相互转换的。整数的原码的最高位是符号位,其余位是数字的值。
|
||||||
- 整数在计算机中是以补码的形式存储的。在补码表示下,计算机可以对正数和负数的加法一视同仁,不需要为减法操作单独设计特殊的硬件电路,并且不存在正负零歧义的问题。
|
- 整数在计算机中是以补码的形式存储的。在补码表示下,计算机可以对正数和负数的加法一视同仁,不需要为减法操作单独设计特殊的硬件电路,并且不存在正负零歧义的问题。
|
||||||
- 浮点数的编码由 1 位符号位、8 位指数位和 23 位分数位构成。由于存在指数位,浮点数的取值范围远大于整数,代价是牺牲了精度。
|
- 浮点数的编码由 1 位符号位、8 位指数位和 23 位分数位构成。由于存在指数位,浮点数的取值范围远大于整数,代价是牺牲了精度。
|
||||||
- ASCII 码是最早出现的英文字符集,长度为 1 字节,共收录 127 个字符。GBK 字符集是常用的中文字符集,共收录两万多个汉字。Unicode 致力于提供一个完整的字符集标准,收录世界内各种语言的字符,从而解决由于字符编码方法不一致而导致的乱码问题。
|
- ASCII 码是最早出现的英文字符集,长度为 1 字节,共收录 127 个字符。GBK 字符集是常用的中文字符集,共收录两万多个汉字。Unicode 致力于提供一个完整的字符集标准,收录世界内各种语言的字符,从而解决由于字符编码方法不一致而导致的乱码问题。
|
||||||
- UTF-8 是最受欢迎的 Unicode 编码方法,通用性非常好。它是一种变长的编码方法,具有很好的扩展性,有效提升了存储空间的使用效率。UTF-16 和 UTF-32 是等长的编码方法。在编码中文时,UTF-16 比 UTF-8 的占用空间更小。Java, C# 等编程语言默认使用 UTF-16 编码。
|
- UTF-8 是最受欢迎的 Unicode 编码方法,通用性非常好。它是一种变长的编码方法,具有很好的扩展性,有效提升了存储空间的使用效率。UTF-16 和 UTF-32 是等长的编码方法。在编码中文时,UTF-16 比 UTF-8 的占用空间更小。Java 和 C# 等编程语言默认使用 UTF-16 编码。
|
||||||
|
|
||||||
## Q & A
|
## Q & A
|
||||||
|
|
||||||
|
@ -19,4 +19,4 @@
|
||||||
|
|
||||||
!!! question "`char` 类型的长度是 1 byte 吗?"
|
!!! question "`char` 类型的长度是 1 byte 吗?"
|
||||||
|
|
||||||
`char` 类型的长度由编程语言采用的编码方法决定。例如,Java, JS, TS, C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。
|
`char` 类型的长度由编程语言采用的编码方法决定。例如,Java、JS、TS、C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
1. 计算搜索区间 $[i, j]$ 的中点 $m$ ,根据它排除一半搜索区间。
|
1. 计算搜索区间 $[i, j]$ 的中点 $m$ ,根据它排除一半搜索区间。
|
||||||
2. 递归求解规模减小一半的子问题,可能为 $f(i, m-1)$ 或 $f(m+1, j)$ 。
|
2. 递归求解规模减小一半的子问题,可能为 $f(i, m-1)$ 或 $f(m+1, j)$ 。
|
||||||
3. 循环第 `1.` , `2.` 步,直至找到 `target` 或区间为空时返回。
|
3. 循环第 `1.` 和 `2.` 步,直至找到 `target` 或区间为空时返回。
|
||||||
|
|
||||||
下图展示了在数组中二分查找元素 $6$ 的分治过程。
|
下图展示了在数组中二分查找元素 $6$ 的分治过程。
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
!!! question
|
!!! question
|
||||||
|
|
||||||
给定三根柱子,记为 `A` , `B` , `C` 。起始状态下,柱子 `A` 上套着 $n$ 个圆盘,它们从上到下按照从小到大的顺序排列。我们的任务是要把这 $n$ 个圆盘移到柱子 `C` 上,并保持它们的原有顺序不变。在移动圆盘的过程中,需要遵守以下规则。
|
给定三根柱子,记为 `A`、`B` 和 `C` 。起始状态下,柱子 `A` 上套着 $n$ 个圆盘,它们从上到下按照从小到大的顺序排列。我们的任务是要把这 $n$ 个圆盘移到柱子 `C` 上,并保持它们的原有顺序不变。在移动圆盘的过程中,需要遵守以下规则。
|
||||||
|
|
||||||
1. 圆盘只能从一个柱子顶部拿出,从另一个柱子顶部放入。
|
1. 圆盘只能从一个柱子顶部拿出,从另一个柱子顶部放入。
|
||||||
2. 每次只能移动一个圆盘。
|
2. 每次只能移动一个圆盘。
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
给定一个楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,每一阶楼梯上都贴有一个非负整数,表示你在该台阶所需要付出的代价。给定一个非负整数数组 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 个台阶需要付出的代价,$cost[0]$ 为地面起始点。请计算最少需要付出多少代价才能到达顶部?
|
给定一个楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,每一阶楼梯上都贴有一个非负整数,表示你在该台阶所需要付出的代价。给定一个非负整数数组 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 个台阶需要付出的代价,$cost[0]$ 为地面起始点。请计算最少需要付出多少代价才能到达顶部?
|
||||||
|
|
||||||
如下图所示,若第 $1$ , $2$ , $3$ 阶的代价分别为 $1$ , $10$ , $1$ ,则从地面爬到第 $3$ 阶的最小代价为 $2$ 。
|
如下图所示,若第 $1$、$2$、$3$ 阶的代价分别为 $1$、$10$、$1$ ,则从地面爬到第 $3$ 阶的最小代价为 $2$ 。
|
||||||
|
|
||||||
![爬到第 3 阶的最小代价](dp_problem_features.assets/min_cost_cs_example.png)
|
![爬到第 3 阶的最小代价](dp_problem_features.assets/min_cost_cs_example.png)
|
||||||
|
|
||||||
|
@ -28,11 +28,11 @@ $$
|
||||||
|
|
||||||
这便可以引出最优子结构的含义:**原问题的最优解是从子问题的最优解构建得来的**。
|
这便可以引出最优子结构的含义:**原问题的最优解是从子问题的最优解构建得来的**。
|
||||||
|
|
||||||
本题显然具有最优子结构:我们从两个子问题最优解 $dp[i-1]$ , $dp[i-2]$ 中挑选出较优的那一个,并用它构建出原问题 $dp[i]$ 的最优解。
|
本题显然具有最优子结构:我们从两个子问题最优解 $dp[i-1]$ 和 $dp[i-2]$ 中挑选出较优的那一个,并用它构建出原问题 $dp[i]$ 的最优解。
|
||||||
|
|
||||||
那么,上节的爬楼梯题目有没有最优子结构呢?它的目标是求解方案数量,看似是一个计数问题,但如果换一种问法:“求解最大方案数量”。我们意外地发现,**虽然题目修改前后是等价的,但最优子结构浮现出来了**:第 $n$ 阶最大方案数量等于第 $n-1$ 阶和第 $n-2$ 阶最大方案数量之和。所以说,最优子结构的解释方式比较灵活,在不同问题中会有不同的含义。
|
那么,上节的爬楼梯题目有没有最优子结构呢?它的目标是求解方案数量,看似是一个计数问题,但如果换一种问法:“求解最大方案数量”。我们意外地发现,**虽然题目修改前后是等价的,但最优子结构浮现出来了**:第 $n$ 阶最大方案数量等于第 $n-1$ 阶和第 $n-2$ 阶最大方案数量之和。所以说,最优子结构的解释方式比较灵活,在不同问题中会有不同的含义。
|
||||||
|
|
||||||
根据状态转移方程,以及初始状态 $dp[1] = cost[1]$ , $dp[2] = cost[2]$ ,我们就可以得到动态规划代码。
|
根据状态转移方程,以及初始状态 $dp[1] = cost[1]$ 和 $dp[2] = cost[2]$ ,我们就可以得到动态规划代码。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -298,7 +298,7 @@ $$
|
||||||
|
|
||||||
!!! question "爬楼梯与障碍生成"
|
!!! question "爬楼梯与障碍生成"
|
||||||
|
|
||||||
给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶。**规定当爬到第 $i$ 阶时,系统自动会给第 $2i$ 阶上放上障碍物,之后所有轮都不允许跳到第 $2i$ 阶上**。例如,前两轮分别跳到了第 $2, 3$ 阶上,则之后就不能跳到第 $4, 6$ 阶上。请问有多少种方案可以爬到楼顶。
|
给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶。**规定当爬到第 $i$ 阶时,系统自动会给第 $2i$ 阶上放上障碍物,之后所有轮都不允许跳到第 $2i$ 阶上**。例如,前两轮分别跳到了第 $2$、$3$ 阶上,则之后就不能跳到第 $4$、$6$ 阶上。请问有多少种方案可以爬到楼顶。
|
||||||
|
|
||||||
在这个问题中,下次跳跃依赖于过去所有的状态,因为每一次跳跃都会在更高的阶梯上设置障碍,并影响未来的跳跃。对于这类问题,动态规划往往难以解决。
|
在这个问题中,下次跳跃依赖于过去所有的状态,因为每一次跳跃都会在更高的阶梯上设置障碍,并影响未来的跳跃。对于这类问题,动态规划往往难以解决。
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
- 若 $s[n-1]$ 和 $t[m-1]$ 相同,我们可以跳过它们,直接考虑 $s[n-2]$ 和 $t[m-2]$ 。
|
- 若 $s[n-1]$ 和 $t[m-1]$ 相同,我们可以跳过它们,直接考虑 $s[n-2]$ 和 $t[m-2]$ 。
|
||||||
- 若 $s[n-1]$ 和 $t[m-1]$ 不同,我们需要对 $s$ 进行一次编辑(插入、删除、替换),使得两字符串尾部的字符相同,从而可以跳过它们,考虑规模更小的问题。
|
- 若 $s[n-1]$ 和 $t[m-1]$ 不同,我们需要对 $s$ 进行一次编辑(插入、删除、替换),使得两字符串尾部的字符相同,从而可以跳过它们,考虑规模更小的问题。
|
||||||
|
|
||||||
也就是说,我们在字符串 $s$ 中进行的每一轮决策(编辑操作),都会使得 $s$ 和 $t$ 中剩余的待匹配字符发生变化。因此,状态为当前在 $s$ , $t$ 中考虑的第 $i$ , $j$ 个字符,记为 $[i, j]$ 。
|
也就是说,我们在字符串 $s$ 中进行的每一轮决策(编辑操作),都会使得 $s$ 和 $t$ 中剩余的待匹配字符发生变化。因此,状态为当前在 $s$ 和 $t$ 中考虑的第 $i$ 和 $j$ 个字符,记为 $[i, j]$ 。
|
||||||
|
|
||||||
状态 $[i, j]$ 对应的子问题:**将 $s$ 的前 $i$ 个字符更改为 $t$ 的前 $j$ 个字符所需的最少编辑步数**。
|
状态 $[i, j]$ 对应的子问题:**将 $s$ 的前 $i$ 个字符更改为 $t$ 的前 $j$ 个字符所需的最少编辑步数**。
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
|
|
||||||
![编辑距离的状态转移](edit_distance_problem.assets/edit_distance_state_transfer.png)
|
![编辑距离的状态转移](edit_distance_problem.assets/edit_distance_state_transfer.png)
|
||||||
|
|
||||||
根据以上分析,可得最优子结构:$dp[i, j]$ 的最少编辑步数等于 $dp[i, j-1]$ , $dp[i-1, j]$ , $dp[i-1, j-1]$ 三者中的最少编辑步数,再加上本次的编辑步数 $1$ 。对应的状态转移方程为:
|
根据以上分析,可得最优子结构:$dp[i, j]$ 的最少编辑步数等于 $dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ 三者中的最少编辑步数,再加上本次的编辑步数 $1$ 。对应的状态转移方程为:
|
||||||
|
|
||||||
$$
|
$$
|
||||||
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
|
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
|
||||||
|
|
|
@ -132,7 +132,7 @@ $$
|
||||||
|
|
||||||
![方案数量递推关系](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png)
|
![方案数量递推关系](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png)
|
||||||
|
|
||||||
我们可以根据递推公式得到暴力搜索解法。以 $dp[n]$ 为起始点,**递归地将一个较大问题拆解为两个较小问题的和**,直至到达最小子问题 $dp[1]$ 和 $dp[2]$ 时返回。其中,最小子问题的解是已知的,即 $dp[1] = 1$ , $dp[2] = 2$ ,表示爬到第 $1$ , $2$ 阶分别有 $1$ , $2$ 种方案。
|
我们可以根据递推公式得到暴力搜索解法。以 $dp[n]$ 为起始点,**递归地将一个较大问题拆解为两个较小问题的和**,直至到达最小子问题 $dp[1]$ 和 $dp[2]$ 时返回。其中,最小子问题的解是已知的,即 $dp[1] = 1$、$dp[2] = 2$ ,表示爬到第 $1$、$2$ 阶分别有 $1$、$2$ 种方案。
|
||||||
|
|
||||||
观察以下代码,它和标准回溯代码都属于深度优先搜索,但更加简洁。
|
观察以下代码,它和标准回溯代码都属于深度优先搜索,但更加简洁。
|
||||||
|
|
||||||
|
@ -436,7 +436,7 @@ $$
|
||||||
根据以上内容,我们可以总结出动态规划的常用术语。
|
根据以上内容,我们可以总结出动态规划的常用术语。
|
||||||
|
|
||||||
- 将数组 `dp` 称为「$dp$ 表」,$dp[i]$ 表示状态 $i$ 对应子问题的解。
|
- 将数组 `dp` 称为「$dp$ 表」,$dp[i]$ 表示状态 $i$ 对应子问题的解。
|
||||||
- 将最小子问题对应的状态(即第 $1$ , $2$ 阶楼梯)称为「初始状态」。
|
- 将最小子问题对应的状态(即第 $1$ 和 $2$ 阶楼梯)称为「初始状态」。
|
||||||
- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」。
|
- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」。
|
||||||
|
|
||||||
## 空间优化
|
## 空间优化
|
||||||
|
|
|
@ -56,7 +56,7 @@ $$
|
||||||
|
|
||||||
- 顶点不能与自身相连,因此邻接矩阵主对角线元素没有意义。
|
- 顶点不能与自身相连,因此邻接矩阵主对角线元素没有意义。
|
||||||
- 对于无向图,两个方向的边等价,此时邻接矩阵关于主对角线对称。
|
- 对于无向图,两个方向的边等价,此时邻接矩阵关于主对角线对称。
|
||||||
- 将邻接矩阵的元素从 $1$ , $0$ 替换为权重,则可表示有权图。
|
- 将邻接矩阵的元素从 $1$ 和 $0$ 替换为权重,则可表示有权图。
|
||||||
|
|
||||||
使用邻接矩阵表示图时,我们可以直接访问矩阵元素以获取边,因此增删查操作的效率很高,时间复杂度均为 $O(1)$ 。然而,矩阵的空间复杂度为 $O(n^2)$ ,内存占用较多。
|
使用邻接矩阵表示图时,我们可以直接访问矩阵元素以获取边,因此增删查操作的效率很高,时间复杂度均为 $O(1)$ 。然而,矩阵的空间复杂度为 $O(n^2)$ ,内存占用较多。
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ BFS 通常借助队列来实现。队列具有“先入先出”的性质,这
|
||||||
|
|
||||||
!!! question "广度优先遍历的序列是否唯一?"
|
!!! question "广度优先遍历的序列是否唯一?"
|
||||||
|
|
||||||
不唯一。广度优先遍历只要求按“由近及远”的顺序遍历,**而多个相同距离的顶点的遍历顺序是允许被任意打乱的**。以上图为例,顶点 $1$ , $3$ 的访问顺序可以交换、顶点 $2$ , $4$ , $6$ 的访问顺序也可以任意交换。
|
不唯一。广度优先遍历只要求按“由近及远”的顺序遍历,**而多个相同距离的顶点的遍历顺序是允许被任意打乱的**。以上图为例,顶点 $1$、$3$ 的访问顺序可以交换、顶点 $2$、$4$、$6$ 的访问顺序也可以任意交换。
|
||||||
|
|
||||||
### 复杂度分析
|
### 复杂度分析
|
||||||
|
|
||||||
|
|
|
@ -40,10 +40,10 @@ $$
|
||||||
|
|
||||||
下图展示了贪心策略的执行过程。
|
下图展示了贪心策略的执行过程。
|
||||||
|
|
||||||
1. 初始状态下,指针 $i$ , $j$ 分列与数组两端。
|
1. 初始状态下,指针 $i$ 和 $j$ 分列与数组两端。
|
||||||
2. 计算当前状态的容量 $cap[i, j]$ ,并更新最大容量。
|
2. 计算当前状态的容量 $cap[i, j]$ ,并更新最大容量。
|
||||||
3. 比较板 $i$ 和 板 $j$ 的高度,并将短板向内移动一格。
|
3. 比较板 $i$ 和 板 $j$ 的高度,并将短板向内移动一格。
|
||||||
4. 循环执行第 `2.` , `3.` 步,直至 $i$ 和 $j$ 相遇时结束。
|
4. 循环执行第 `2.` 和 `3.` 步,直至 $i$ 和 $j$ 相遇时结束。
|
||||||
|
|
||||||
=== "<1>"
|
=== "<1>"
|
||||||
![最大容量问题的贪心过程](max_capacity_problem.assets/max_capacity_greedy_step1.png)
|
![最大容量问题的贪心过程](max_capacity_problem.assets/max_capacity_greedy_step1.png)
|
||||||
|
@ -76,7 +76,7 @@ $$
|
||||||
|
|
||||||
代码循环最多 $n$ 轮,**因此时间复杂度为 $O(n)$** 。
|
代码循环最多 $n$ 轮,**因此时间复杂度为 $O(n)$** 。
|
||||||
|
|
||||||
变量 $i$ , $j$ , $res$ 使用常数大小额外空间,**因此空间复杂度为 $O(1)$** 。
|
变量 $i$、$j$、$res$ 使用常数大小额外空间,**因此空间复杂度为 $O(1)$** 。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,11 @@ $$
|
||||||
|
|
||||||
如下图所示,当 $n \geq 4$ 时,切分出一个 $2$ 后乘积会变大,**这说明大于等于 $4$ 的整数都应该被切分**。
|
如下图所示,当 $n \geq 4$ 时,切分出一个 $2$ 后乘积会变大,**这说明大于等于 $4$ 的整数都应该被切分**。
|
||||||
|
|
||||||
**贪心策略一**:如果切分方案中包含 $\geq 4$ 的因子,那么它就应该被继续切分。最终的切分方案只应出现 $1$ , $2$ , $3$ 这三种因子。
|
**贪心策略一**:如果切分方案中包含 $\geq 4$ 的因子,那么它就应该被继续切分。最终的切分方案只应出现 $1$、$2$、$3$ 这三种因子。
|
||||||
|
|
||||||
![切分导致乘积变大](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png)
|
![切分导致乘积变大](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png)
|
||||||
|
|
||||||
接下来思考哪个因子是最优的。在 $1$ , $2$ , $3$ 这三个因子中,显然 $1$ 是最差的,因为 $1 \times (n-1) < n$ 恒成立,即切分出 $1$ 反而会导致乘积减小。
|
接下来思考哪个因子是最优的。在 $1$、$2$、$3$ 这三个因子中,显然 $1$ 是最差的,因为 $1 \times (n-1) < n$ 恒成立,即切分出 $1$ 反而会导致乘积减小。
|
||||||
|
|
||||||
如下图所示,当 $n = 6$ 时,有 $3 \times 3 > 2 \times 2 \times 2$ 。**这意味着切分出 $3$ 比切分出 $2$ 更优**。
|
如下图所示,当 $n = 6$ 时,有 $3 \times 3 > 2 \times 2 \times 2$ 。**这意味着切分出 $3$ 比切分出 $2$ 更优**。
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ $$
|
||||||
|
|
||||||
总结以上,可推出以下贪心策略。
|
总结以上,可推出以下贪心策略。
|
||||||
|
|
||||||
1. 输入整数 $n$ ,从其不断地切分出因子 $3$ ,直至余数为 $0$ , $1$ , $2$ 。
|
1. 输入整数 $n$ ,从其不断地切分出因子 $3$ ,直至余数为 $0$、$1$、$2$ 。
|
||||||
2. 当余数为 $0$ 时,代表 $n$ 是 $3$ 的倍数,因此不做任何处理。
|
2. 当余数为 $0$ 时,代表 $n$ 是 $3$ 的倍数,因此不做任何处理。
|
||||||
3. 当余数为 $2$ 时,不继续划分,保留之。
|
3. 当余数为 $2$ 时,不继续划分,保留之。
|
||||||
4. 当余数为 $1$ 时,由于 $2 \times 2 > 1 \times 3$ ,因此应将最后一个 $3$ 替换为 $2$ 。
|
4. 当余数为 $1$ 时,由于 $2 \times 2 > 1 \times 3$ ,因此应将最后一个 $3$ 替换为 $2$ 。
|
||||||
|
@ -142,7 +142,7 @@ $$
|
||||||
- 运算符 `**` 和函数 `pow()` 的时间复杂度均为 $O(\log a)$ 。
|
- 运算符 `**` 和函数 `pow()` 的时间复杂度均为 $O(\log a)$ 。
|
||||||
- 函数 `math.pow()` 内部调用 C 语言库的 `pow()` 函数,其执行浮点取幂,时间复杂度为 $O(1)$ 。
|
- 函数 `math.pow()` 内部调用 C 语言库的 `pow()` 函数,其执行浮点取幂,时间复杂度为 $O(1)$ 。
|
||||||
|
|
||||||
变量 $a$ , $b$ 使用常数大小的额外空间,**因此空间复杂度为 $O(1)$** 。
|
变量 $a$ 和 $b$ 使用常数大小的额外空间,**因此空间复杂度为 $O(1)$** 。
|
||||||
|
|
||||||
### 正确性证明
|
### 正确性证明
|
||||||
|
|
||||||
|
|
|
@ -193,7 +193,7 @@ index = hash(key) % capacity
|
||||||
|
|
||||||
先抛出结论:**当我们使用大质数作为模数时,可以最大化地保证哈希值的均匀分布**。因为质数不会与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。
|
先抛出结论:**当我们使用大质数作为模数时,可以最大化地保证哈希值的均匀分布**。因为质数不会与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。
|
||||||
|
|
||||||
举个例子,假设我们选择合数 $9$ 作为模数,它可以被 $3$ 整除。那么所有可以被 $3$ 整除的 `key` 都会被映射到 $0$ , $3$ , $6$ 这三个哈希值。
|
举个例子,假设我们选择合数 $9$ 作为模数,它可以被 $3$ 整除。那么所有可以被 $3$ 整除的 `key` 都会被映射到 $0$、$3$、$6$ 这三个哈希值。
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
|
@ -221,7 +221,7 @@ $$
|
||||||
|
|
||||||
不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。
|
不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。
|
||||||
|
|
||||||
在实际中,我们通常会用一些标准哈希算法,例如 MD5 , SHA-1 , SHA-2 , SHA3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。
|
在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2、SHA3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。
|
||||||
|
|
||||||
近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。
|
近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。
|
||||||
|
|
||||||
|
|
|
@ -206,7 +206,7 @@
|
||||||
|
|
||||||
### 多次哈希
|
### 多次哈希
|
||||||
|
|
||||||
顾名思义,多次哈希方法是使用多个哈希函数 $f_1(x)$ , $f_2(x)$ , $f_3(x)$ , $\dots$ 进行探测。
|
顾名思义,多次哈希方法是使用多个哈希函数 $f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$ 进行探测。
|
||||||
|
|
||||||
- **插入元素**:若哈希函数 $f_1(x)$ 出现冲突,则尝试 $f_2(x)$ ,以此类推,直到找到空位后插入元素。
|
- **插入元素**:若哈希函数 $f_1(x)$ 出现冲突,则尝试 $f_2(x)$ ,以此类推,直到找到空位后插入元素。
|
||||||
- **查找元素**:在相同的哈希函数顺序下进行查找,直到找到目标元素时返回;或遇到空位或已尝试所有哈希函数,说明哈希表中不存在该元素,则返回 $\text{None}$ 。
|
- **查找元素**:在相同的哈希函数顺序下进行查找,直到找到目标元素时返回;或遇到空位或已尝试所有哈希函数,说明哈希表中不存在该元素,则返回 $\text{None}$ 。
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
- 不同编程语言采取了不同的哈希表实现。例如,Java 的 `HashMap` 使用链式地址,而 Python 的 `Dict` 采用开放寻址。
|
- 不同编程语言采取了不同的哈希表实现。例如,Java 的 `HashMap` 使用链式地址,而 Python 的 `Dict` 采用开放寻址。
|
||||||
- 在哈希表中,我们希望哈希算法具有确定性、高效率和均匀分布的特点。在密码学中,哈希算法还应该具备抗碰撞性和雪崩效应。
|
- 在哈希表中,我们希望哈希算法具有确定性、高效率和均匀分布的特点。在密码学中,哈希算法还应该具备抗碰撞性和雪崩效应。
|
||||||
- 哈希算法通常采用大质数作为模数,以最大化地保证哈希值的均匀分布,减少哈希冲突。
|
- 哈希算法通常采用大质数作为模数,以最大化地保证哈希值的均匀分布,减少哈希冲突。
|
||||||
- 常见的哈希算法包括 MD5, SHA-1, SHA-2, SHA3 等。MD5 常用于校验文件完整性,SHA-2 常用于安全应用与协议。
|
- 常见的哈希算法包括 MD5、SHA-1、SHA-2 和 SHA3 等。MD5 常用于校验文件完整性,SHA-2 常用于安全应用与协议。
|
||||||
- 编程语言通常会为数据类型提供内置哈希算法,用于计算哈希表中的桶索引。通常情况下,只有不可变对象是可哈希的。
|
- 编程语言通常会为数据类型提供内置哈希算法,用于计算哈希表中的桶索引。通常情况下,只有不可变对象是可哈希的。
|
||||||
|
|
||||||
## Q & A
|
## Q & A
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
## 方法一:遍历选择
|
## 方法一:遍历选择
|
||||||
|
|
||||||
我们可以进行下图所示的 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\dots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。
|
我们可以进行下图所示的 $k$ 轮遍历,分别在每轮中提取第 $1$、$2$、$\dots$、$k$ 大的元素,时间复杂度为 $O(nk)$ 。
|
||||||
|
|
||||||
此方法只适用于 $k \ll n$ 的情况,因为当 $k$ 与 $n$ 比较接近时,其时间复杂度趋向于 $O(n^2)$ ,非常耗时。
|
此方法只适用于 $k \ll n$ 的情况,因为当 $k$ 与 $n$ 比较接近时,其时间复杂度趋向于 $O(n^2)$ ,非常耗时。
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
本项目旨在创建一本开源免费、新手友好的数据结构与算法入门教程。
|
本项目旨在创建一本开源免费、新手友好的数据结构与算法入门教程。
|
||||||
|
|
||||||
- 全书采用动画图解,结构化地讲解数据结构与算法知识,内容清晰易懂、学习曲线平滑。
|
- 全书采用动画图解,结构化地讲解数据结构与算法知识,内容清晰易懂、学习曲线平滑。
|
||||||
- 算法源代码皆可一键运行,支持 Java, C++, Python, Go, JS, TS, C#, Swift, Zig 等语言。
|
- 算法源代码皆可一键运行,支持 Java、C++、Python、Go、JS、TS、C#、Swift、Rust、Dart、Zig 等语言。
|
||||||
- 鼓励读者在章节讨论区互帮互助、共同进步,提问与评论通常可在两日内得到回复。
|
- 鼓励读者在章节讨论区互帮互助、共同进步,提问与评论通常可在两日内得到回复。
|
||||||
|
|
||||||
## 读者对象
|
## 读者对象
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
|
|
||||||
时间复杂度为 $O(\log n)$ 。每轮缩小一半区间,因此二分循环次数为 $\log_2 n$ 。
|
时间复杂度为 $O(\log n)$ 。每轮缩小一半区间,因此二分循环次数为 $\log_2 n$ 。
|
||||||
|
|
||||||
空间复杂度为 $O(1)$ 。指针 `i` , `j` 使用常数大小空间。
|
空间复杂度为 $O(1)$ 。指针 $i$ 和 $j$ 使用常数大小空间。
|
||||||
|
|
||||||
## 区间表示方法
|
## 区间表示方法
|
||||||
|
|
||||||
|
|
|
@ -177,7 +177,7 @@
|
||||||
|
|
||||||
### 转化为查找元素
|
### 转化为查找元素
|
||||||
|
|
||||||
我们知道,当数组不包含 `target` 时,最后 $i$ , $j$ 会分别指向首个大于、小于 `target` 的元素。
|
我们知道,当数组不包含 `target` 时,最终 $i$ 和 $j$ 会分别指向首个大于、小于 `target` 的元素。
|
||||||
|
|
||||||
因此,如下图所示,我们可以构造一个数组中不存在的元素,用于查找左右边界。
|
因此,如下图所示,我们可以构造一个数组中不存在的元素,用于查找左右边界。
|
||||||
|
|
||||||
|
|
|
@ -222,6 +222,6 @@
|
||||||
|
|
||||||
本节的代码都是“双闭区间”写法。有兴趣的读者可以自行实现“左闭右开”写法。
|
本节的代码都是“双闭区间”写法。有兴趣的读者可以自行实现“左闭右开”写法。
|
||||||
|
|
||||||
总的来看,二分查找无非就是给指针 $i$ , $j$ 分别设定搜索目标,目标可能是一个具体的元素(例如 `target` ),也可能是一个元素范围(例如小于 `target` 的元素)。
|
总的来看,二分查找无非就是给指针 $i$ 和 $j$ 分别设定搜索目标,目标可能是一个具体的元素(例如 `target` ),也可能是一个元素范围(例如小于 `target` 的元素)。
|
||||||
|
|
||||||
在不断的循环二分中,指针 $i$ , $j$ 都逐渐逼近预先设定的目标。最终,它们或是成功找到答案,或是越过边界后停止。
|
在不断的循环二分中,指针 $i$ 和 $j$ 都逐渐逼近预先设定的目标。最终,它们或是成功找到答案,或是越过边界后停止。
|
||||||
|
|
|
@ -188,6 +188,6 @@
|
||||||
|
|
||||||
## 算法特性
|
## 算法特性
|
||||||
|
|
||||||
- **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\dots$ , $2$ , $1$ ,总和为 $(n - 1) n / 2$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。
|
- **时间复杂度为 $O(n^2)$、自适应排序**:各轮“冒泡”遍历的数组长度依次为 $n - 1$、$n - 2$、$\dots$、$2$、$1$ ,总和为 $(n - 1) n / 2$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。
|
||||||
- **空间复杂度为 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。
|
- **空间复杂度为 $O(1)$、原地排序**:指针 $i$ 和 $j$ 使用常数大小的额外空间。
|
||||||
- **稳定排序**:由于在“冒泡”中遇到相等元素不交换。
|
- **稳定排序**:由于在“冒泡”中遇到相等元素不交换。
|
||||||
|
|
|
@ -93,8 +93,8 @@
|
||||||
|
|
||||||
## 算法特性
|
## 算法特性
|
||||||
|
|
||||||
- **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\dots$ , $2$ , $1$ 次,求和得到 $(n - 1) n / 2$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。
|
- **时间复杂度 $O(n^2)$、自适应排序**:最差情况下,每次插入操作分别需要循环 $n - 1$、$n-2$、$\dots$、$2$、$1$ 次,求和得到 $(n - 1) n / 2$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。
|
||||||
- **空间复杂度 $O(1)$ 、原地排序** :指针 $i$ , $j$ 使用常数大小的额外空间。
|
- **空间复杂度 $O(1)$、原地排序**:指针 $i$ 和 $j$ 使用常数大小的额外空间。
|
||||||
- **稳定排序**:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。
|
- **稳定排序**:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。
|
||||||
|
|
||||||
## 插入排序优势
|
## 插入排序优势
|
||||||
|
|
|
@ -119,8 +119,8 @@
|
||||||
|
|
||||||
## 算法特性
|
## 算法特性
|
||||||
|
|
||||||
- **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\dots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。
|
- **时间复杂度为 $O(n^2)$、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$、$n - 1$、$\dots$、$3$、$2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。
|
||||||
- **空间复杂度 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。
|
- **空间复杂度 $O(1)$、原地排序**:指针 $i$ 和 $j$ 使用常数大小的额外空间。
|
||||||
- **非稳定排序**:如下图所示,元素 `nums[i]` 有可能被交换至与其相等的元素的右边,导致两者相对顺序发生改变。
|
- **非稳定排序**:如下图所示,元素 `nums[i]` 有可能被交换至与其相等的元素的右边,导致两者相对顺序发生改变。
|
||||||
|
|
||||||
![选择排序非稳定示例](selection_sort.assets/selection_sort_instability.png)
|
![选择排序非稳定示例](selection_sort.assets/selection_sort_instability.png)
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
自适应性需要根据具体情况来评估。如果最差时间复杂度差于平均时间复杂度,说明排序算法在某些数据下性能可能劣化,因此被视为负面属性;而如果最佳时间复杂度优于平均时间复杂度,则被视为正面属性。
|
自适应性需要根据具体情况来评估。如果最差时间复杂度差于平均时间复杂度,说明排序算法在某些数据下性能可能劣化,因此被视为负面属性;而如果最佳时间复杂度优于平均时间复杂度,则被视为正面属性。
|
||||||
|
|
||||||
**是否基于比较**:「基于比较的排序」依赖于比较运算符($<$ , $=$ , $>$)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 $O(n \log n)$ 。而「非比较排序」不使用比较运算符,时间复杂度可达 $O(n)$ ,但其通用性相对较差。
|
**是否基于比较**:「基于比较的排序」依赖于比较运算符($<$、$=$、$>$)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 $O(n \log n)$ 。而「非比较排序」不使用比较运算符,时间复杂度可达 $O(n)$ ,但其通用性相对较差。
|
||||||
|
|
||||||
## 理想排序算法
|
## 理想排序算法
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在尾递归优化后,向下递归的子数组长度最大为原数组的一半长度。假设最差情况,一直为一半长度,那么最终的递归深度就是 $\log n$ 。
|
递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在尾递归优化后,向下递归的子数组长度最大为原数组的一半长度。假设最差情况,一直为一半长度,那么最终的递归深度就是 $\log n$ 。
|
||||||
|
|
||||||
回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 $n, n - 1, n - 2, ..., 2, 1$ ,从而递归深度为 $n$ 。尾递归优化可以避免这种情况的出现。
|
回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 $n$、$n - 1$、$\dots$、$2$、$1$ ,递归深度为 $n$ 。尾递归优化可以避免这种情况的出现。
|
||||||
|
|
||||||
!!! question "当数组中所有元素都相等时,快速排序的时间复杂度是 $O(n^2)$ 吗?该如何处理这种退化情况?"
|
!!! question "当数组中所有元素都相等时,快速排序的时间复杂度是 $O(n^2)$ 吗?该如何处理这种退化情况?"
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
## 栈常用操作
|
## 栈常用操作
|
||||||
|
|
||||||
栈的常用操作如下表所示,具体的方法名需要根据所使用的编程语言来确定。在此,我们以常见的 `push()` , `pop()` , `peek()` 命名为例。
|
栈的常用操作如下表所示,具体的方法名需要根据所使用的编程语言来确定。在此,我们以常见的 `push()`、`pop()`、`peek()` 命名为例。
|
||||||
|
|
||||||
<p align="center"> 表 <id> 栈的操作效率 </p>
|
<p align="center"> 表 <id> 栈的操作效率 </p>
|
||||||
|
|
||||||
|
@ -475,7 +475,7 @@
|
||||||
|
|
||||||
在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。
|
在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。
|
||||||
|
|
||||||
综上所述,当入栈与出栈操作的元素是基本数据类型(如 `int` , `double` )时,我们可以得出以下结论。
|
综上所述,当入栈与出栈操作的元素是基本数据类型时,例如 `int` 或 `double` ,我们可以得出以下结论。
|
||||||
|
|
||||||
- 基于数组实现的栈在触发扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。
|
- 基于数组实现的栈在触发扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。
|
||||||
- 基于链表实现的栈可以提供更加稳定的效率表现。
|
- 基于链表实现的栈可以提供更加稳定的效率表现。
|
||||||
|
|
|
@ -175,7 +175,7 @@
|
||||||
- 「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 $\text{None}$ 。
|
- 「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 $\text{None}$ 。
|
||||||
- 「边 edge」:连接两个节点的线段,即节点引用(指针)。
|
- 「边 edge」:连接两个节点的线段,即节点引用(指针)。
|
||||||
- 节点所在的「层 level」:从顶至底递增,根节点所在层为 1 。
|
- 节点所在的「层 level」:从顶至底递增,根节点所在层为 1 。
|
||||||
- 节点的「度 degree」:节点的子节点的数量。在二叉树中,度的取值范围是 0, 1, 2 。
|
- 节点的「度 degree」:节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
|
||||||
- 二叉树的「高度 height」:从根节点到最远叶节点所经过的边的数量。
|
- 二叉树的「高度 height」:从根节点到最远叶节点所经过的边的数量。
|
||||||
- 节点的「深度 depth」:从根节点到该节点所经过的边的数量。
|
- 节点的「深度 depth」:从根节点到该节点所经过的边的数量。
|
||||||
- 节点的「高度 height」:从最远叶节点到该节点所经过的边的数量。
|
- 节点的「高度 height」:从最远叶节点到该节点所经过的边的数量。
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
DFS 的前、中、后序遍历和访问数组的顺序类似,是遍历二叉树的基本方法,利用这三种遍历方法,我们可以得到一个特定顺序的遍历结果。例如在二叉搜索树中,由于结点大小满足 `左子结点值 < 根结点值 < 右子结点值` ,因此我们只要按照 `左->根->右` 的优先级遍历树,就可以获得有序的节点序列。
|
DFS 的前、中、后序遍历和访问数组的顺序类似,是遍历二叉树的基本方法,利用这三种遍历方法,我们可以得到一个特定顺序的遍历结果。例如在二叉搜索树中,由于结点大小满足 `左子结点值 < 根结点值 < 右子结点值` ,因此我们只要按照 `左->根->右` 的优先级遍历树,就可以获得有序的节点序列。
|
||||||
|
|
||||||
!!! question "右旋操作是处理失衡节点 `node` , `child` , `grand_child` 之间的关系,那 `node` 的父节点和 `node` 原来的连接不需要维护吗?右旋操作后岂不是断掉了?"
|
!!! question "右旋操作是处理失衡节点 `node`、`child`、`grand_child` 之间的关系,那 `node` 的父节点和 `node` 原来的连接不需要维护吗?右旋操作后岂不是断掉了?"
|
||||||
|
|
||||||
我们需要从递归的视角来看这个问题。右旋操作 `right_rotate(root)` 传入的是子树的根节点,最终 `return child` 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。
|
我们需要从递归的视角来看这个问题。右旋操作 `right_rotate(root)` 传入的是子树的根节点,最终 `return child` 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue