diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index 0ff66d22a..599071800 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -1102,6 +1102,8 @@ comments: true 下表总结对比了数组和链表的各项特点与操作效率。由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。 +

表:数组与链表的效率对比

+
| | 数组 | 链表 | diff --git a/chapter_array_and_linkedlist/summary.md b/chapter_array_and_linkedlist/summary.md index e274e9795..339f2a6e7 100644 --- a/chapter_array_and_linkedlist/summary.md +++ b/chapter_array_and_linkedlist/summary.md @@ -60,3 +60,12 @@ comments: true 假如把列表元素换成链表节点 `n = [n1, n2, n3, n4, n5]` ,通常情况下这五个节点对象也是被分散存储在内存各处的。然而,给定一个列表索引,我们仍然可以在 $O(1)$ 时间内获取到节点内存地址,从而访问到对应的节点。这是因为数组中存储的是节点的引用,而非节点本身。 与许多语言不同的是,在 Python 中数字也被包装为对象,列表中存储的不是数字本身,而是对数字的引用。因此,我们会发现两个数组中的相同数字拥有同一个 id ,并且这些数字的内存地址是无需连续的。 + +!!! question "C++ STL 里面的 std::list 已经实现了双向链表,但好像一些算法的书上都不怎么直接用这个,是不是有什么局限性呢?" + + 一方面,我们往往更青睐使用数组实现算法,而只有在必要时才使用链表。这是因为: + + 1. 空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 `std::list` 通常比 `std::vector` 更占用空间。 + 2. 缓存不友好:由于数据不是连续存放的,`std::list` 对缓存的利用率较低。一般情况下,`std::vector` 的性能会更好。 + + 另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 `stack` 和 `queue` ,而非链表。 diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md index 56a3f9e2d..66c2a7d2a 100644 --- a/chapter_backtracking/backtracking_algorithm.md +++ b/chapter_backtracking/backtracking_algorithm.md @@ -1667,6 +1667,8 @@ comments: true 为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。 +
+ | 名词 | 定义 | 例题三 | | ------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | | 解 Solution | 解是满足问题特定条件的答案,可能有一个或多个 | 根节点到节点 $7$ 的满足约束条件的所有路径 | @@ -1676,6 +1678,8 @@ comments: true | 回退 Backtracking | 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶结点、结束结点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 | | 剪枝 Pruning | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则终止继续搜索 | +
+ !!! tip 问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心等算法中都有涉及。 diff --git a/chapter_backtracking/permutations_problem.md b/chapter_backtracking/permutations_problem.md index 24ed13041..910ab33d9 100644 --- a/chapter_backtracking/permutations_problem.md +++ b/chapter_backtracking/permutations_problem.md @@ -8,6 +8,8 @@ comments: true 下表列举了几个示例数据,包括输入数组和对应的所有排列。 +

表:数组与链表的效率对比

+
| 输入数组 | 所有排列 | diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index ed752e870..82c229598 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -880,6 +880,7 @@ $$ **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。 以下表格展示了一些例子,其中一些夸张的值是为了强调“系数无法撼动阶数”这一结论。当 $n$ 趋于无穷大时,这些常数变得无足轻重。 +

表:多项式时间复杂度示例

diff --git a/chapter_data_structure/basic_data_types.md b/chapter_data_structure/basic_data_types.md index 4a171a981..0d6d5c906 100644 --- a/chapter_data_structure/basic_data_types.md +++ b/chapter_data_structure/basic_data_types.md @@ -21,6 +21,7 @@ comments: true - 整数类型 `int` 占用 $4$ bytes = $32$ bits ,可以表示 $2^{32}$ 个数字。 下表列举了各种基本数据类型的占用空间、取值范围和默认值。此表格无需硬背,大致理解即可,需要时可以通过查表来回忆。 +

表:基本数据类型的占用空间和取值范围

diff --git a/chapter_data_structure/number_encoding.md b/chapter_data_structure/number_encoding.md index 01871430b..894443ff8 100644 --- a/chapter_data_structure/number_encoding.md +++ b/chapter_data_structure/number_encoding.md @@ -144,6 +144,7 @@ $$ **尽管浮点数 `float` 扩展了取值范围,但其副作用是牺牲了精度**。整数类型 `int` 将全部 32 位用于表示数字,数字是均匀分布的;而由于指数位的存在,浮点数 `float` 的数值越大,相邻两个数字之间的差值就会趋向越大。 进一步地,指数位 $E = 0$ 和 $E = 255$ 具有特殊含义,**用于表示零、无穷大、$\mathrm{NaN}$ 等**。 +

表:指数位含义

diff --git a/chapter_divide_and_conquer/build_binary_tree_problem.md b/chapter_divide_and_conquer/build_binary_tree_problem.md index 0651860f1..574d7a434 100644 --- a/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -49,6 +49,7 @@ status: new - 将当前树在 `inorder` 中的索引区间记为 $[l, r]$ 。 如下表所示,通过以上变量即可表示根节点在 `preorder` 中的索引,以及子树在 `inorder` 中的索引区间。 +

表:根节点和子树在前序和中序遍历中的索引

diff --git a/chapter_graph/graph.md b/chapter_graph/graph.md index 5e8106a3e..ffb18b3e9 100644 --- a/chapter_graph/graph.md +++ b/chapter_graph/graph.md @@ -89,6 +89,7 @@ $$ ## 9.1.4.   图常见应用 实际应用中,许多系统都可以用图来建模,相应的待求解问题也可以约化为图计算问题。 +

表:现实生活中常见的图

diff --git a/chapter_graph/graph_operations.md b/chapter_graph/graph_operations.md index c8131e3f5..40b61fc09 100644 --- a/chapter_graph/graph_operations.md +++ b/chapter_graph/graph_operations.md @@ -2115,6 +2115,7 @@ comments: true ## 9.2.3.   效率对比 设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 +

表:邻接矩阵与邻接表对比

diff --git a/chapter_hashing/hash_algorithm.md b/chapter_hashing/hash_algorithm.md index 749eb2de1..db0b4d84a 100644 --- a/chapter_hashing/hash_algorithm.md +++ b/chapter_hashing/hash_algorithm.md @@ -532,6 +532,8 @@ $$ - SHA-2 系列中的 SHA-256 是最安全的哈希算法之一,仍未出现成功的攻击案例,因此常被用在各类安全应用与协议中。 - SHA-3 相较 SHA-2 的实现开销更低、计算效率更高,但目前使用覆盖度不如 SHA-2 系列。 +
+ | | MD5 | SHA-1 | SHA-2 | SHA-3 | | -------- | ------------------------------ | ---------------- | ---------------------------- | -------------------- | | 推出时间 | 1992 | 1995 | 2002 | 2008 | @@ -540,6 +542,8 @@ $$ | 安全等级 | 低,已被成功攻击 | 低,已被成功攻击 | 高 | 高 | | 应用 | 已被弃用,仍用于数据完整性检查 | 已被弃用 | 加密货币交易验证、数字签名等 | 可用于替代 SHA-2 | +
+ ## 6.3.4.   数据结构的哈希值 我们知道,哈希表的 `key` 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法,用于计算哈希表中的桶索引。以 Python 为例,我们可以调用 `hash()` 函数来计算各种数据类型的哈希值,包括: diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index 23a823843..ec1b44e79 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -17,6 +17,7 @@ comments: true - **添加元素**:仅需将元素添加至数组(链表)的尾部即可,使用 $O(1)$ 时间。 - **查询元素**:由于数组(链表)是乱序的,因此需要遍历其中的所有元素,使用 $O(n)$ 时间。 - **删除元素**:需要先查询到元素,再从数组中删除,使用 $O(n)$ 时间。 +

表:元素查询效率对比

diff --git a/chapter_heap/heap.md b/chapter_heap/heap.md index a017fa414..6306c14bd 100644 --- a/chapter_heap/heap.md +++ b/chapter_heap/heap.md @@ -26,6 +26,7 @@ comments: true 实际上,**堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将「优先队列」和「堆」看作等价的数据结构。因此,本书对两者不做特别区分,统一使用「堆」来命名。 堆的常用操作见下表,方法名需要根据编程语言来确定。 +

表:堆的操作效率

diff --git a/chapter_introduction/what_is_dsa.md b/chapter_introduction/what_is_dsa.md index 8b4a82dc6..8c1711033 100644 --- a/chapter_introduction/what_is_dsa.md +++ b/chapter_introduction/what_is_dsa.md @@ -44,10 +44,11 @@ comments: true

图:拼装积木

两者的详细对应关系如下表所示。 +

表:将数据结构与算法类比为积木

-| 数据结构与算法 | LEGO 乐高 | +| 数据结构与算法 | 积木 | | -------------- | ---------------------------------------- | | 输入数据 | 未拼装的积木 | | 数据结构 | 积木组织形式,包括形状、大小、连接方式等 | diff --git a/chapter_searching/searching_algorithm_revisited.md b/chapter_searching/searching_algorithm_revisited.md index fab62356a..aa0054fc8 100644 --- a/chapter_searching/searching_algorithm_revisited.md +++ b/chapter_searching/searching_algorithm_revisited.md @@ -49,6 +49,7 @@ comments: true

图:多种搜索策略

上述几种方法的操作效率与特性如下表所示。 +

表:查找算法效率对比

diff --git a/chapter_stack_and_queue/deque.md b/chapter_stack_and_queue/deque.md index b7b79a47d..a7f8ebd10 100644 --- a/chapter_stack_and_queue/deque.md +++ b/chapter_stack_and_queue/deque.md @@ -13,6 +13,7 @@ comments: true ## 5.3.1.   双向队列常用操作 双向队列的常用操作如下表所示,具体的方法名称需要根据所使用的编程语言来确定。 +

表:双向队列操作效率

diff --git a/chapter_stack_and_queue/queue.md b/chapter_stack_and_queue/queue.md index a2e6dcb7e..c52094940 100755 --- a/chapter_stack_and_queue/queue.md +++ b/chapter_stack_and_queue/queue.md @@ -15,6 +15,7 @@ comments: true ## 5.2.1.   队列常用操作 队列的常见操作如下表所示。需要注意的是,不同编程语言的方法名称可能会有所不同。我们在此采用与栈相同的方法命名。 +

表:队列操作效率

diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md index 78be8362b..1fa81cb7c 100755 --- a/chapter_stack_and_queue/stack.md +++ b/chapter_stack_and_queue/stack.md @@ -17,6 +17,7 @@ comments: true ## 5.1.1.   栈常用操作 栈的常用操作如下表所示,具体的方法名需要根据所使用的编程语言来确定。在此,我们以常见的 `push()` , `pop()` , `peek()` 命名为例。 +

表:栈的操作效率

diff --git a/chapter_tree/avl_tree.md b/chapter_tree/avl_tree.md index c5879a5e8..c2f2e258d 100644 --- a/chapter_tree/avl_tree.md +++ b/chapter_tree/avl_tree.md @@ -1095,15 +1095,16 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉

图:AVL 树的四种旋转情况

在代码中,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于上图中的哪种情况。 +

表:四种旋转情况的选择条件

| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 | | ---------------- | ---------------- | ---------------- | -| $>1$ (即左偏树) | $\geq 0$ | 右旋 | -| $>1$ (即左偏树) | $<0$ | 先左旋后右旋 | -| $<-1$ (即右偏树) | $\leq 0$ | 左旋 | -| $<-1$ (即右偏树) | $>0$ | 先右旋后左旋 | +| $> 1$ (即左偏树) | $\geq 0$ | 右旋 | +| $> 1$ (即左偏树) | $<0$ | 先左旋后右旋 | +| $< -1$ (即右偏树) | $\leq 0$ | 左旋 | +| $< -1$ (即右偏树) | $>0$ | 先右旋后左旋 |
diff --git a/chapter_tree/binary_search_tree.md b/chapter_tree/binary_search_tree.md index 659b31da6..68a115552 100755 --- a/chapter_tree/binary_search_tree.md +++ b/chapter_tree/binary_search_tree.md @@ -1489,6 +1489,7 @@ comments: true 给定一组数据,我们考虑使用数组或二叉搜索树存储。 观察可知,二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能表现。只有在高频添加、低频查找删除的数据适用场景下,数组比二叉搜索树的效率更高。 +

表:数组与搜索树的效率对比

diff --git a/chapter_tree/binary_tree.md b/chapter_tree/binary_tree.md index df581105d..79942141c 100644 --- a/chapter_tree/binary_tree.md +++ b/chapter_tree/binary_tree.md @@ -568,14 +568,15 @@ comments: true

图:二叉树的最佳与最差结构

如下表所示,在最佳和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大或极小值。 +

表:二叉树的最佳与最差情况

| | 完美二叉树 | 链表 | | ----------------------------- | ---------- | ---------- | | 第 $i$ 层的节点数量 | $2^{i-1}$ | $1$ | -| 树的高度为 $h$ 时的叶节点数量 | $2^h$ | $1$ | -| 树的高度为 $h$ 时的节点总数 | $2^{h+1} - 1$ | $h + 1$ | -| 树的节点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ | +| 高度 $h$ 树的叶节点数量 | $2^h$ | $1$ | +| 高度 $h$ 树的节点总数 | $2^{h+1} - 1$ | $h + 1$ | +| 节点总数 $n$ 树的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
diff --git a/chapter_tree/summary.md b/chapter_tree/summary.md index 8cdf13405..482becedd 100644 --- a/chapter_tree/summary.md +++ b/chapter_tree/summary.md @@ -50,3 +50,7 @@ comments: true - `equals()`:用来对比两个对象的值是否相等。 因此如果要对比值,我们通常会用 `equals()` 。然而,通过 `String a = "hi"; String b = "hi";` 初始化的字符串都存储在字符串常量池中,它们指向同一个对象,因此也可以用 `a == b` 来比较两个字符串的内容。 + +!!! question "广度优先遍历到最底层之前,队列中的节点数量是 $2^h$ 吗?" + + 是的,例如高度 $h = 2$ 的满二叉树,其节点总数 $n = 7$ ,则底层节点数量 $4 = 2^h = (n + 1) / 2$ 。