diff --git a/chapter_appendix/contribution/index.html b/chapter_appendix/contribution/index.html index eda6944c1..0698f8b38 100644 --- a/chapter_appendix/contribution/index.html +++ b/chapter_appendix/contribution/index.html @@ -3414,7 +3414,7 @@
我们可以通过 Docker 来部署本项目。执行以下脚本,稍等片刻后,即可使用浏览器打开 http://localhost:8000 来访问本项目。
执行以下 Docker 脚本,稍等片刻,即可在网页 http://localhost:8000 访问本项目。
git clone https://github.com/krahets/hello-algo.git
cd hello-algo
docker-compose up -d
diff --git a/chapter_array_and_linkedlist/array/index.html b/chapter_array_and_linkedlist/array/index.html
index 2b9001a8c..7c165cb4a 100644
--- a/chapter_array_and_linkedlist/array/index.html
+++ b/chapter_array_and_linkedlist/array/index.html
@@ -3407,7 +3407,7 @@
Fig. 数组定义与存储方式
数组初始化。通常有无初始值和给定初始值两种方式,我们可根据需求选择合适的方法。在未给定初始值的情况下,数组的所有元素通常会被初始化为默认值 \(0\) 。
-
+
array.java/* 初始化数组 */
@@ -3501,7 +3501,7 @@
然而,从地址计算公式的角度看,索引本质上表示的是内存地址的偏移量。首个元素的地址偏移量是 \(0\) ,因此索引为 \(0\) 也是合理的。
访问元素的高效性带来了诸多便利。例如,我们可以在 \(O(1)\) 时间内随机获取数组中的任意一个元素。
-
+
4.1.2. 数组缺点¶
数组在初始化后长度不可变。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
-
+
array.java/* 扩展数组长度 */
@@ -3822,7 +3822,7 @@

Fig. 数组插入元素
-
+
array.java/* 在数组的索引 index 处插入元素 num */
@@ -3973,7 +3973,7 @@

Fig. 数组删除元素
-
+
array.java/* 删除索引 index 处元素 */
@@ -4106,7 +4106,7 @@
4.1.3. 数组常用操作¶
数组遍历。以下介绍两种常用的遍历方法。
-
+
数组查找。通过遍历数组,查找数组内的指定元素,并输出对应索引。
-
+
array.java/* 在数组中查找指定元素 */
diff --git a/chapter_array_and_linkedlist/linked_list/index.html b/chapter_array_and_linkedlist/linked_list/index.html
index f5623fb29..e84189e68 100644
--- a/chapter_array_and_linkedlist/linked_list/index.html
+++ b/chapter_array_and_linkedlist/linked_list/index.html
@@ -3422,7 +3422,7 @@

Fig. 链表定义与存储方式
-
+
/* 链表节点类 */
@@ -3572,7 +3572,7 @@
在编程语言中,数组整体就是一个变量,例如数组 nums ,包含各个元素 nums[0] , nums[1] 等等。而链表是由多个节点对象组成,我们通常将头节点当作链表的代称,例如头节点 head 和链表 head 实际上是同义的。
链表初始化方法。建立链表分为两步,第一步是初始化各个节点对象,第二步是构建引用指向关系。完成后,即可以从链表的头节点(即首个节点)出发,通过指针 next 依次访问所有节点。
-
+
linked_list.java/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
@@ -3750,7 +3750,7 @@

Fig. 链表插入节点
-
+
linked_list.java/* 在链表的节点 n0 之后插入节点 P */
@@ -3866,7 +3866,7 @@

Fig. 链表删除节点
-
+
4.2.2. 链表缺点¶
链表访问节点效率较低。如上节所述,数组可以在 \(O(1)\) 时间下访问任意元素。然而,链表无法直接访问任意节点,这是因为系统需要从头节点出发,逐个向后遍历直至找到目标节点。例如,若要访问链表索引为 index(即第 index + 1 个)的节点,则需要向后遍历 index 轮。
-
+
linked_list.java/* 访问链表中索引为 index 的节点 */
@@ -4175,7 +4175,7 @@
链表的内存占用较大。链表以节点为单位,每个节点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。
4.2.3. 链表常用操作¶
遍历链表查找。遍历链表,查找链表内值为 target 的节点,输出节点在链表中的索引。
-
+
linked_list.java/* 在链表中查找值为 target 的首个节点 */
@@ -4352,7 +4352,7 @@
单向链表。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 \(\text{None}\) 。
环形链表。如果我们令单向链表的尾节点指向头节点(即首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。
双向链表。与单向链表相比,双向链表记录了两个方向的指针(引用)。双向链表的节点定义同时包含指向后继节点(下一节点)和前驱节点(上一节点)的指针。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。
-
+
/* 双向链表节点类 */
diff --git a/chapter_array_and_linkedlist/list/index.html b/chapter_array_and_linkedlist/list/index.html
index 1dc56f0b1..a1754d2af 100644
--- a/chapter_array_and_linkedlist/list/index.html
+++ b/chapter_array_and_linkedlist/list/index.html
@@ -3378,7 +3378,7 @@
为解决此问题,出现了一种被称为「动态数组 Dynamic Array」的数据结构,即长度可变的数组,也常被称为「列表 List」。列表基于数组实现,继承了数组的优点,并且可以在程序运行过程中动态扩容。在列表中,我们可以自由添加元素,而无需担心超过容量限制。
4.3.1. 列表常用操作¶
初始化列表。通常我们会使用“无初始值”和“有初始值”的两种初始化方法。
-
+
访问与更新元素。由于列表的底层数据结构是数组,因此可以在 \(O(1)\) 时间内访问和更新元素,效率很高。
-
+
在列表中添加、插入、删除元素。相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 \(O(1)\) ,但插入和删除元素的效率仍与数组相同,时间复杂度为 \(O(N)\) 。
-
+
遍历列表。与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。
-
+
拼接两个列表。给定一个新列表 list1 ,我们可以将该列表拼接到原列表的尾部。
-
+
排序列表。排序也是常用的方法之一。完成列表排序后,我们便可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法。
-
+
list.java/* 排序列表 */
@@ -4057,7 +4057,7 @@
扩容机制:插入元素时可能超出列表容量,此时需要扩容列表。扩容方法是根据扩容倍数创建一个更大的数组,并将当前数组的所有元素依次移动至新数组。在本示例中,我们规定每次将数组扩容至之前的 2 倍。
本示例旨在帮助读者直观理解列表的工作机制。实际编程语言中,列表实现更加标准和复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。
-
+
my_list.java/* 列表类简易实现 */
diff --git a/chapter_backtracking/backtracking_algorithm/index.html b/chapter_backtracking/backtracking_algorithm/index.html
index 02a9c7ca1..c5c044c62 100644
--- a/chapter_backtracking/backtracking_algorithm/index.html
+++ b/chapter_backtracking/backtracking_algorithm/index.html
@@ -3437,7 +3437,7 @@
给定一个二叉树,搜索并记录所有值为 \(7\) 的节点,请返回节点列表。
对于此题,我们前序遍历这颗树,并判断当前节点的值是否为 \(7\) ,若是则将该节点的值加入到结果列表 res 之中。
-
+
preorder_traversal_i_compact.java/* 前序遍历:例题一 */
@@ -3487,7 +3487,7 @@
if root == nil {
return
}
- if int(root.Val) == 7 {
+ if (root.Val).(int) == 7 {
// 记录解
*res = append(*res, root)
}
@@ -3599,7 +3599,7 @@
在二叉树中搜索所有值为 \(7\) 的节点,请返回根节点到这些节点的路径。
在例题一代码的基础上,我们需要借助一个列表 path 记录访问过的节点路径。当访问到值为 \(7\) 的节点时,则复制 path 并添加进结果列表 res 。遍历完成后,res 中保存的就是所有的解。
-
+
preorder_traversal_ii_compact.java/* 前序遍历:例题二 */
@@ -3663,7 +3663,7 @@
}
// 尝试
*path = append(*path, root)
- if int(root.Val) == 7 {
+ if root.Val.(int) == 7 {
// 记录解
*res = append(*res, *path)
}
@@ -3835,7 +3835,7 @@
在二叉树中搜索所有值为 \(7\) 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 \(3\) 的节点。
为了满足以上约束条件,我们需要添加剪枝操作:在搜索过程中,若遇到值为 \(3\) 的节点,则提前返回,停止继续搜索。
-
+
preorder_traversal_iii_compact.java/* 前序遍历:例题三 */
@@ -3909,7 +3909,7 @@
}
// 尝试
*path = append(*path, root)
- if int(root.Val) == 7 {
+ if root.Val.(int) == 7 {
// 记录解
*res = append(*res, *path)
*path = (*path)[:len(*path)-1]
@@ -4059,7 +4059,7 @@
13.1.3. 框架代码¶
接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。
在以下框架代码中,state 表示问题的当前状态,choices 表示当前状态下可以做出的选择。
-
+
接下来,我们基于框架代码来解决例题三。状态 state 为节点遍历路径,选择 choices 为当前节点的左子节点和右子节点,结果 res 是路径列表。
-
+
preorder_traversal_iii_template.java/* 判断当前状态是否为解 */
diff --git a/chapter_backtracking/n_queens_problem/index.html b/chapter_backtracking/n_queens_problem/index.html
index 1126707fd..ba53151c6 100644
--- a/chapter_backtracking/n_queens_problem/index.html
+++ b/chapter_backtracking/n_queens_problem/index.html
@@ -3418,7 +3418,7 @@
代码实现¶
请注意,\(n\) 维方阵中 \(row - col\) 的范围是 \([-n + 1, n - 1]\) ,\(row + col\) 的范围是 \([0, 2n - 2]\) ,所以主对角线和次对角线的数量都为 \(2n - 1\) ,即数组 diag1 和 diag2 的长度都为 \(2n - 1\) 。
-
+
n_queens.java/* 回溯算法:N 皇后 */
diff --git a/chapter_backtracking/permutations_problem/index.html b/chapter_backtracking/permutations_problem/index.html
index 4d3af2f3a..3a3c702de 100644
--- a/chapter_backtracking/permutations_problem/index.html
+++ b/chapter_backtracking/permutations_problem/index.html
@@ -3518,7 +3518,7 @@
观察上图发现,该剪枝操作将搜索空间大小从 \(O(n^n)\) 降低至 \(O(n!)\) 。
代码实现¶
想清楚以上信息之后,我们就可以在框架代码中做“完形填空”了。为了缩短代码行数,我们不单独实现框架代码中的各个函数,而是将他们展开在 backtrack() 函数中。
-
+
permutations_i.java/* 回溯算法:全排列 I */
@@ -3861,7 +3861,7 @@
代码实现¶
在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希表 duplicated ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝。
-
+
permutations_ii.java/* 回溯算法:全排列 II */
diff --git a/chapter_backtracking/subset_sum_problem/index.html b/chapter_backtracking/subset_sum_problem/index.html
index 12091ebba..0a81a2193 100644
--- a/chapter_backtracking/subset_sum_problem/index.html
+++ b/chapter_backtracking/subset_sum_problem/index.html
@@ -3481,7 +3481,7 @@
参考全排列解法¶
类似于全排列问题,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 target 时,就将子集记录至结果列表。
而与全排列问题不同的是,本题集合中的元素可以被无限次选取,因此无需借助 selected 布尔列表来记录元素是否已被选择。我们可以对全排列代码进行小幅修改,初步得到解题代码。
-
+
subset_sum_i_naive.java/* 回溯算法:子集和 I */
@@ -3781,7 +3781,7 @@
在开启搜索前,先将数组 nums 排序。在遍历所有选择时,当子集和超过 target 时直接结束循环,因为后边的元素更大,其子集和都一定会超过 target 。
省去元素和变量 total,通过在 target 上执行减法来统计元素和,当 target 等于 \(0\) 时记录解。
-
+
subset_sum_i.java/* 回溯算法:子集和 I */
@@ -4083,7 +4083,7 @@
为解决此问题,我们需要限制相等元素在每一轮中只被选择一次。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。这意味着在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。
与此同时,本题规定中的每个数组元素只能被选择一次。幸运的是,我们也可以利用变量 start 来满足该约束:当做出选择 \(x_{i}\) 后,设定下一轮从索引 \(i + 1\) 开始向后遍历。这样即能去除重复子集,也能避免重复选择元素。
代码实现¶
-
+
subset_sum_ii.java/* 回溯算法:子集和 II */
diff --git a/chapter_computational_complexity/space_complexity/index.html b/chapter_computational_complexity/space_complexity/index.html
index 5cb157e90..e5c82022e 100644
--- a/chapter_computational_complexity/space_complexity/index.html
+++ b/chapter_computational_complexity/space_complexity/index.html
@@ -3503,7 +3503,7 @@

Fig. 算法使用的相关空间
-
+
/* 类 */
@@ -3754,7 +3754,7 @@
以最差输入数据为准。当 \(n < 10\) 时,空间复杂度为 \(O(1)\) ;但当 \(n > 10\) 时,初始化的数组 nums 占用 \(O(n)\) 空间;因此最差空间复杂度为 \(O(n)\) 。
以算法运行过程中的峰值内存为准。例如,程序在执行最后一行之前,占用 \(O(1)\) 空间;当初始化数组 nums 时,程序占用 \(O(n)\) 空间;因此最差空间复杂度为 \(O(n)\) 。
-
+
在递归函数中,需要注意统计栈帧空间。例如,函数 loop() 在循环中调用了 \(n\) 次 function() ,每轮中的 function() 都返回并释放了栈帧空间,因此空间复杂度仍为 \(O(1)\) 。而递归函数 recur() 在运行过程中会同时存在 \(n\) 个未返回的 recur() ,从而占用 \(O(n)\) 的栈帧空间。
-
+
int function() {
@@ -4081,7 +4081,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
常数阶 \(O(1)\)¶
常数阶常见于数量与输入数据大小 \(n\) 无关的常量、变量、对象。
需要注意的是,在循环中初始化变量或调用函数而占用的内存,在进入下一循环后就会被释放,即不会累积占用空间,空间复杂度仍为 \(O(1)\) 。
-
+
space_complexity.java/* 常数阶 */
@@ -4322,7 +4322,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
线性阶 \(O(n)\)¶
线性阶常见于元素数量与 \(n\) 成正比的数组、链表、栈、队列等。
-
+
以下递归函数会同时存在 \(n\) 个未返回的 algorithm() 函数,使用 \(O(n)\) 大小的栈帧空间。
-
+
space_complexity.java/* 线性阶(递归实现) */
@@ -4689,7 +4689,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
平方阶 \(O(n^2)\)¶
平方阶常见于矩阵和图,元素数量与 \(n\) 成平方关系。
-
+
在以下递归函数中,同时存在 \(n\) 个未返回的 algorithm() ,并且每个函数中都初始化了一个数组,长度分别为 \(n, n-1, n-2, ..., 2, 1\) ,平均长度为 \(\frac{n}{2}\) ,因此总体占用 \(O(n^2)\) 空间。
-
+
space_complexity.java/* 平方阶(递归实现) */
@@ -5025,7 +5025,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
指数阶 \(O(2^n)\)¶
指数阶常见于二叉树。高度为 \(n\) 的「满二叉树」的节点数量为 \(2^n - 1\) ,占用 \(O(2^n)\) 空间。
-
+
space_complexity.java/* 指数阶(建立满二叉树) */
diff --git a/chapter_computational_complexity/time_complexity/index.html b/chapter_computational_complexity/time_complexity/index.html
index 6b573e3ce..f3d79e32a 100644
--- a/chapter_computational_complexity/time_complexity/index.html
+++ b/chapter_computational_complexity/time_complexity/index.html
@@ -3591,7 +3591,7 @@
\[
1 + 1 + 10 + (1 + 5) \times n = 6n + 12
\]
-
+
// 在某运行平台下
@@ -3740,7 +3740,7 @@
算法 B 中的打印操作需要循环 \(n\) 次,算法运行时间随着 \(n\) 增大呈线性增长。此算法的时间复杂度被称为「线性阶」。
算法 C 中的打印操作需要循环 \(1000000\) 次,但运行时间仍与输入数据大小 \(n\) 无关。因此 C 的时间复杂度和 A 相同,仍为「常数阶」。
-
+
// 算法 A 时间复杂度:常数阶
@@ -3951,7 +3951,7 @@
\[
T(n) = 3 + 2n
\]
-
+
最终,两者都能推出相同的时间复杂度结果,即 \(O(n^2)\) 。
-
+
void algorithm(int n) {
@@ -4349,7 +4349,7 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!
常数阶 \(O(1)\)¶
常数阶的操作数量与输入数据大小 \(n\) 无关,即不随着 \(n\) 的变化而变化。
对于以下算法,尽管操作数量 size 可能很大,但由于其与数据大小 \(n\) 无关,因此时间复杂度仍为 \(O(1)\) 。
-
+