diff --git a/chapter_array_and_linkedlist/array/index.html b/chapter_array_and_linkedlist/array/index.html index 7253484af..bf910710d 100644 --- a/chapter_array_and_linkedlist/array/index.html +++ b/chapter_array_and_linkedlist/array/index.html @@ -4737,8 +4737,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

4.1.2   数组的优点与局限性

数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。

diff --git a/chapter_array_and_linkedlist/linked_list/index.html b/chapter_array_and_linkedlist/linked_list/index.html index 0cfbf5c05..bc89819a3 100644 --- a/chapter_array_and_linkedlist/linked_list/index.html +++ b/chapter_array_and_linkedlist/linked_list/index.html @@ -4122,8 +4122,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

3.   删除节点

如图 4-7 所示,在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可

@@ -4288,8 +4288,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

4.   访问节点

在链表中访问节点的效率较低。如上一节所述,我们可以在 \(O(1)\) 时间下访问数组中的任意元素。链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点。也就是说,访问链表的第 \(i\) 个节点需要循环 \(i - 1\) 轮,时间复杂度为 \(O(n)\)

@@ -4445,8 +4445,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

5.   查找节点

遍历链表,查找其中值为 target 的节点,输出该节点在链表中的索引。此过程也属于线性查找。代码如下所示:

@@ -4625,8 +4625,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

4.2.2   数组 vs. 链表

表 4-1 总结了数组和链表的各项特点并对比了操作效率。由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。

diff --git a/chapter_backtracking/backtracking_algorithm/index.html b/chapter_backtracking/backtracking_algorithm/index.html index 4b2798187..27d117ad5 100644 --- a/chapter_backtracking/backtracking_algorithm/index.html +++ b/chapter_backtracking/backtracking_algorithm/index.html @@ -5137,6 +5137,11 @@ +
+可视化运行 +

+全屏观看 >

+

根据题意,我们在找到值为 \(7\) 的节点后应该继续搜索,因此需要将记录解之后的 return 语句删除。图 13-4 对比了保留或删除 return 语句的搜索过程。

保留与删除 return 的搜索过程对比

图 13-4   保留与删除 return 的搜索过程对比

diff --git a/chapter_backtracking/subset_sum_problem/index.html b/chapter_backtracking/subset_sum_problem/index.html index ac1ce07fd..30d3933bc 100644 --- a/chapter_backtracking/subset_sum_problem/index.html +++ b/chapter_backtracking/subset_sum_problem/index.html @@ -4031,8 +4031,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

向以上代码输入数组 \([3, 4, 5]\) 和目标元素 \(9\) ,输出结果为 \([3, 3, 3], [4, 5], [5, 4]\)虽然成功找出了所有和为 \(9\) 的子集,但其中存在重复的子集 \([4, 5]\)\([5, 4]\)

这是因为搜索过程是区分选择顺序的,然而子集不区分选择顺序。如图 13-10 所示,先选 \(4\) 后选 \(5\) 与先选 \(5\) 后选 \(4\) 是不同的分支,但对应同一个子集。

@@ -4486,8 +4486,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

图 13-12 所示为将数组 \([3, 4, 5]\) 和目标元素 \(9\) 输入以上代码后的整体回溯过程。

子集和 I 回溯过程

@@ -4980,8 +4980,8 @@
可视化运行 -

-全屏观看 >

+

+全屏观看 >

图 13-14 展示了数组 \([4, 4, 5]\) 和目标元素 \(9\) 的回溯过程,共包含四种剪枝操作。请你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。

子集和 II 回溯过程

diff --git a/chapter_computational_complexity/iteration_and_recursion/index.html b/chapter_computational_complexity/iteration_and_recursion/index.html index d3ba4056c..fb8a3db00 100644 --- a/chapter_computational_complexity/iteration_and_recursion/index.html +++ b/chapter_computational_complexity/iteration_and_recursion/index.html @@ -5142,12 +5142,12 @@ const stack = []; let res = 0; // 递:递归调用 - for (let i = 1; i <= n; i++) { + for (let i = n; i > 0; i--) { // 通过“入栈操作”模拟“递” stack.push(i); } // 归:返回结果 - while (stack.length) { + while (stack.length) { // 通过“出栈操作”模拟“归” res += stack.pop(); } @@ -5163,12 +5163,12 @@ const stack: number[] = []; let res: number = 0; // 递:递归调用 - for (let i = 1; i <= n; i++) { + for (let i = n; i > 0; i--) { // 通过“入栈操作”模拟“递” stack.push(i); } // 归:返回结果 - while (stack.length) { + while (stack.length) { // 通过“出栈操作”模拟“归” res += stack.pop(); } diff --git a/chapter_data_structure/basic_data_types/index.html b/chapter_data_structure/basic_data_types/index.html index 610739179..52fbd6fc9 100644 --- a/chapter_data_structure/basic_data_types/index.html +++ b/chapter_data_structure/basic_data_types/index.html @@ -3624,7 +3624,7 @@
// 使用多种基本数据类型来初始化数组
 let numbers: Vec<i32> = vec![0; 5];
-let decimals: Vec<f32> = vec![0.0, 5];
+let decimals: Vec<f32> = vec![0.0; 5];
 let characters: Vec<char> = vec!['0'; 5];
 let bools: Vec<bool> = vec![false; 5];
 
diff --git a/chapter_divide_and_conquer/binary_search_recur/index.html b/chapter_divide_and_conquer/binary_search_recur/index.html index 346fd2c1f..bc172de59 100644 --- a/chapter_divide_and_conquer/binary_search_recur/index.html +++ b/chapter_divide_and_conquer/binary_search_recur/index.html @@ -3856,6 +3856,11 @@
+
+可视化运行 +

+全屏观看 >

+
diff --git a/chapter_divide_and_conquer/build_binary_tree_problem/index.html b/chapter_divide_and_conquer/build_binary_tree_problem/index.html index 425f620a2..fa2af328b 100644 --- a/chapter_divide_and_conquer/build_binary_tree_problem/index.html +++ b/chapter_divide_and_conquer/build_binary_tree_problem/index.html @@ -3979,6 +3979,11 @@ +
+可视化运行 +

+全屏观看 >

+

图 12-8 展示了构建二叉树的递归过程,各个节点是在向下“递”的过程中建立的,而各条边(引用)是在向上“归”的过程中建立的。

diff --git a/chapter_divide_and_conquer/hanota_problem/index.html b/chapter_divide_and_conquer/hanota_problem/index.html index 95e06a9ff..f19465af9 100644 --- a/chapter_divide_and_conquer/hanota_problem/index.html +++ b/chapter_divide_and_conquer/hanota_problem/index.html @@ -3992,6 +3992,11 @@
+
+可视化运行 +

+全屏观看 >

+

如图 12-15 所示,汉诺塔问题形成一棵高度为 \(n\) 的递归树,每个节点代表一个子问题,对应一个开启的 dfs() 函数,因此时间复杂度为 \(O(2^n)\) ,空间复杂度为 \(O(n)\)

汉诺塔问题的递归树

图 12-15   汉诺塔问题的递归树

diff --git a/chapter_dynamic_programming/dp_problem_features/index.html b/chapter_dynamic_programming/dp_problem_features/index.html index 65dfb3605..ba007e6a5 100644 --- a/chapter_dynamic_programming/dp_problem_features/index.html +++ b/chapter_dynamic_programming/dp_problem_features/index.html @@ -3781,6 +3781,11 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i] +
+可视化运行 +

+全屏观看 >

+

图 14-7 展示了以上代码的动态规划过程。

爬楼梯最小代价的动态规划过程

图 14-7   爬楼梯最小代价的动态规划过程

@@ -3991,6 +3996,11 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i] +
+可视化运行 +

+全屏观看 >

+

14.2.2   无后效性

无后效性是动态规划能够有效解决问题的重要特性之一,其定义为:给定一个确定的状态,它的未来发展只与当前状态有关,而与过去经历的所有状态无关

以爬楼梯问题为例,给定状态 \(i\) ,它会发展出状态 \(i+1\) 和状态 \(i+2\) ,分别对应跳 \(1\) 步和跳 \(2\) 步。在做出这两种选择时,我们无须考虑状态 \(i\) 之前的状态,它们对状态 \(i\) 的未来没有影响。

@@ -4291,6 +4301,11 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] +
+可视化运行 +

+全屏观看 >

+

在上面的案例中,由于仅需多考虑前面一个状态,因此我们仍然可以通过扩展状态定义,使得问题重新满足无后效性。然而,某些问题具有非常严重的“有后效性”。

爬楼梯与障碍生成

diff --git a/chapter_dynamic_programming/dp_solution_pipeline/index.html b/chapter_dynamic_programming/dp_solution_pipeline/index.html index 913d56d2d..94c1f633c 100644 --- a/chapter_dynamic_programming/dp_solution_pipeline/index.html +++ b/chapter_dynamic_programming/dp_solution_pipeline/index.html @@ -3911,6 +3911,11 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
+
+可视化运行 +

+全屏观看 >

+

图 14-14 给出了以 \(dp[2, 1]\) 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 grid 的尺寸变大而急剧增多。

从本质上看,造成重叠子问题的原因为:存在多条路径可以从左上角到达某一单元格

暴力搜索递归树

@@ -4216,6 +4221,11 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j] +
+可视化运行 +

+全屏观看 >

+

如图 14-15 所示,在引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 \(O(nm)\)

记忆化搜索递归树

图 14-15   记忆化搜索递归树

@@ -4539,6 +4549,11 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j] +
+可视化运行 +

+全屏观看 >

+

图 14-16 展示了最小路径和的状态转移过程,其遍历了整个网格,因此时间复杂度为 \(O(nm)\)

数组 dp 大小为 \(n \times m\)因此空间复杂度为 \(O(nm)\)

@@ -4878,6 +4893,11 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
+
+可视化运行 +

+全屏观看 >

+
diff --git a/chapter_dynamic_programming/edit_distance_problem/index.html b/chapter_dynamic_programming/edit_distance_problem/index.html index 1012817f8..b55aa5678 100644 --- a/chapter_dynamic_programming/edit_distance_problem/index.html +++ b/chapter_dynamic_programming/edit_distance_problem/index.html @@ -3935,6 +3935,11 @@ dp[i, j] = dp[i-1, j-1] +
+可视化运行 +

+全屏观看 >

+

如图 14-30 所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作填写一个二维网格的过程。

@@ -4368,6 +4373,11 @@ dp[i, j] = dp[i-1, j-1]
+
+可视化运行 +

+全屏观看 >

+
diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming/index.html b/chapter_dynamic_programming/intro_to_dynamic_programming/index.html index c9d3320dd..ccc002ea8 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming/index.html +++ b/chapter_dynamic_programming/intro_to_dynamic_programming/index.html @@ -3907,6 +3907,11 @@ +
+可视化运行 +

+全屏观看 >

+

14.1.1   方法一:暴力搜索

回溯算法通常并不显式地对问题进行拆解,而是将求解问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。

我们可以尝试从问题分解的角度分析这道题。设爬到第 \(i\) 阶共有 \(dp[i]\) 种方案,那么 \(dp[i]\) 就是原问题,其子问题包括:

@@ -4129,6 +4134,11 @@ dp[i] = dp[i-1] + dp[i-2] +
+可视化运行 +

+全屏观看 >

+

图 14-3 展示了暴力搜索形成的递归树。对于问题 \(dp[n]\) ,其递归树的深度为 \(n\) ,时间复杂度为 \(O(2^n)\) 。指数阶属于爆炸式增长,如果我们输入一个比较大的 \(n\) ,则会陷入漫长的等待之中。

爬楼梯对应递归树

图 14-3   爬楼梯对应递归树

@@ -4440,6 +4450,11 @@ dp[i] = dp[i-1] + dp[i-2] +
+可视化运行 +

+全屏观看 >

+

观察图 14-4 ,经过记忆化处理后,所有重叠子问题都只需计算一次,时间复杂度优化至 \(O(n)\) ,这是一个巨大的飞跃。

记忆化搜索对应递归树

图 14-4   记忆化搜索对应递归树

@@ -4668,6 +4683,11 @@ dp[i] = dp[i-1] + dp[i-2] +
+可视化运行 +

+全屏观看 >

+

图 14-5 模拟了以上代码的执行过程。

爬楼梯的动态规划过程

图 14-5   爬楼梯的动态规划过程

@@ -4861,6 +4881,11 @@ dp[i] = dp[i-1] + dp[i-2] +
+可视化运行 +

+全屏观看 >

+

观察以上代码,由于省去了数组 dp 占用的空间,因此空间复杂度从 \(O(n)\) 降至 \(O(1)\)

在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时我们可以只保留必要的状态,通过“降维”来节省内存空间。这种空间优化技巧被称为“滚动变量”或“滚动数组”

diff --git a/chapter_dynamic_programming/knapsack_problem/index.html b/chapter_dynamic_programming/knapsack_problem/index.html index 5819acc5a..060b41eea 100644 --- a/chapter_dynamic_programming/knapsack_problem/index.html +++ b/chapter_dynamic_programming/knapsack_problem/index.html @@ -3824,6 +3824,11 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) +
+可视化运行 +

+全屏观看 >

+

如图 14-18 所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 \(O(2^n)\)

观察递归树,容易发现其中存在重叠子问题,例如 \(dp[1, 10]\) 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。

0-1 背包问题的暴力搜索递归树

@@ -4136,6 +4141,11 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) +
+可视化运行 +

+全屏观看 >

+

图 14-19 展示了在记忆化搜索中被剪掉的搜索分支。

0-1 背包问题的记忆化搜索递归树

图 14-19   0-1 背包问题的记忆化搜索递归树

@@ -4431,6 +4441,11 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) +
+可视化运行 +

+全屏观看 >

+

如图 14-20 所示,时间复杂度和空间复杂度都由数组 dp 大小决定,即 \(O(n \times cap)\)

@@ -4767,6 +4782,11 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
+
+可视化运行 +

+全屏观看 >

+
diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem/index.html b/chapter_dynamic_programming/unbounded_knapsack_problem/index.html index aa815c1fe..bbe797fc5 100644 --- a/chapter_dynamic_programming/unbounded_knapsack_problem/index.html +++ b/chapter_dynamic_programming/unbounded_knapsack_problem/index.html @@ -4046,6 +4046,11 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) +
+可视化运行 +

+全屏观看 >

+

3.   空间优化

由于当前状态是从左边和上边的状态转移而来的,因此空间优化后应该对 \(dp\) 表中的每一行进行正序遍历

这个遍历顺序与 0-1 背包正好相反。请借助图 14-23 来理解两者的区别。

@@ -4346,6 +4351,11 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) +
+可视化运行 +

+全屏观看 >

+

14.5.2   零钱兑换问题

背包问题是一大类动态规划问题的代表,其拥有很多变种,例如零钱兑换问题。

@@ -4726,6 +4736,11 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
+
+可视化运行 +

+全屏观看 >

+

图 14-25 展示了零钱兑换的动态规划过程,和完全背包问题非常相似。

@@ -5086,6 +5101,11 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
+
+可视化运行 +

+全屏观看 >

+

14.5.3   零钱兑换问题 II

Question

@@ -5357,7 +5377,7 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] // 若超过目标金额,则不选硬币 i dp[i][a] = dp[i - 1][a]; } else { - // 不选和选硬币 i 这两种方案的较小值 + // 不选和选硬币 i 这两种方案之和 dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize]; } } @@ -5429,6 +5449,11 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
+
+可视化运行 +

+全屏观看 >

+

3.   空间优化

空间优化处理方式相同,删除硬币维度即可:

@@ -5652,7 +5677,7 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] // 若超过目标金额,则不选硬币 i dp[a] = dp[a]; } else { - // 不选和选硬币 i 这两种方案的较小值 + // 不选和选硬币 i 这两种方案之和 dp[a] = dp[a] + dp[a - coins[i - 1] as usize]; } } @@ -5712,6 +5737,11 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
+
+可视化运行 +

+全屏观看 >

+
diff --git a/chapter_graph/graph_operations/index.html b/chapter_graph/graph_operations/index.html index eda5e265b..2759dd8aa 100644 --- a/chapter_graph/graph_operations/index.html +++ b/chapter_graph/graph_operations/index.html @@ -4555,6 +4555,11 @@ +
+可视化运行 +

+全屏观看 >

+

9.2.2   基于邻接表的实现

设无向图的顶点总数为 \(n\)、边总数为 \(m\) ,则可根据图 9-8 所示的方法实现各种操作。