diff --git a/chapter_appendix/contribution.md b/chapter_appendix/contribution.md index adec4cd0d..a2c7198a3 100644 --- a/chapter_appendix/contribution.md +++ b/chapter_appendix/contribution.md @@ -16,9 +16,9 @@ comments: true ## 16.2.1 内容微调 -在每个页面的右上角有一个「编辑」图标,您可以按照以下步骤修改文本或代码: +您可以按照以下步骤修改文本或代码: -1. 点击编辑按钮,如果遇到“需要 Fork 此仓库”的提示,请同意该操作。 +1. 点击页面的右上角的“编辑图标”,如果遇到“需要 Fork 此仓库”的提示,请同意该操作。 2. 修改 Markdown 源文件内容,检查内容的正确性,并尽量保持排版格式的统一。 3. 在页面底部填写修改说明,然后点击“Propose file change”按钮。页面跳转后,点击“Create pull request”按钮即可发起拉取请求。 diff --git a/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md index f11af3992..3bdd08a9c 100755 --- a/chapter_array_and_linkedlist/array.md +++ b/chapter_array_and_linkedlist/array.md @@ -4,7 +4,7 @@ comments: true # 4.1 数组 -「数组 Array」是一种线性数据结构,其将相同类型元素存储在连续的内存空间中。我们将某个元素在数组中的位置称为该元素的「索引 Index」。 +「数组 array」是一种线性数据结构,其将相同类型元素存储在连续的内存空间中。我们将某个元素在数组中的位置称为该元素的「索引 index」。  @@ -840,7 +840,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex 在数组中查找指定元素需要遍历数组,每轮判断元素值是否匹配,若匹配则输出对应索引。 -因为数组是线性数据结构,所以上述查找操作被称为「线性查找」。 +因为数组是线性数据结构,所以上述查找操作被称为“线性查找”。 === "Java" diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index c1fb9cd8e..63eddd32f 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -6,19 +6,19 @@ comments: true 内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此时链表的灵活性优势就体现出来了。 -「链表 Linked List」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,我们可以通过它从当前节点访问到下一个节点。这意味着链表的各个节点可以被分散存储在内存各处,它们的内存地址是无须连续的。 +「链表 linked list」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,我们可以通过它从当前节点访问到下一个节点。这意味着链表的各个节点可以被分散存储在内存各处,它们的内存地址是无须连续的。 
图:链表定义与存储方式
-观察上图,链表中的每个「节点 Node」对象都包含两项数据:节点的“值”、指向下一节点的“引用”。 +观察上图,链表的组成单位是「节点 node」对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。 - 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。 - 尾节点指向的是“空”,它在 Java, C++, Python 中分别被记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。 - 在 C, C++, Go, Rust 等支持指针的语言中,上述的“引用”应被替换为“指针”。 -如以下代码所示,链表以节点对象 `ListNode` 为单位,每个节点除了包含值,还需额外保存下一节点的引用(指针)。因此在相同数据量下,**链表通常比数组占用更多的内存空间**。 +链表节点 `ListNode` 如以下代码所示。每个节点除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,**链表比数组占用更多的内存空间**。 === "Java" @@ -903,7 +903,7 @@ comments: true ### 5. 查找节点 -遍历链表,查找链表内值为 `target` 的节点,输出节点在链表中的索引。此过程也属于「线性查找」。 +遍历链表,查找链表内值为 `target` 的节点,输出节点在链表中的索引。此过程也属于线性查找。 === "Java" diff --git a/chapter_array_and_linkedlist/list.md b/chapter_array_and_linkedlist/list.md index 0fee90312..d80ed3019 100755 --- a/chapter_array_and_linkedlist/list.md +++ b/chapter_array_and_linkedlist/list.md @@ -6,7 +6,7 @@ comments: true **数组长度不可变导致实用性降低**。在实际中,我们可能事先无法确定需要存储多少数据,这使数组长度的选择变得困难。若长度过小,需要在持续添加数据时频繁扩容数组;若长度过大,则会造成内存空间的浪费。 -为解决此问题,出现了一种被称为「动态数组 Dynamic Array」的数据结构,即长度可变的数组,也常被称为「列表 List」。列表基于数组实现,继承了数组的优点,并且可以在程序运行过程中动态扩容。我们可以在列表中自由地添加元素,而无须担心超过容量限制。 +为解决此问题,出现了一种被称为「动态数组 dynamic array」的数据结构,即长度可变的数组,也常被称为「列表 list」。列表基于数组实现,继承了数组的优点,并且可以在程序运行过程中动态扩容。我们可以在列表中自由地添加元素,而无须担心超过容量限制。 ## 4.3.1 列表常用操作 diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md index 81cd260f2..1d4b9d3fa 100644 --- a/chapter_backtracking/backtracking_algorithm.md +++ b/chapter_backtracking/backtracking_algorithm.md @@ -4,9 +4,9 @@ comments: true # 13.1 回溯算法 -「回溯算法 Backtracking Algorithm」是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。 +「回溯算法 backtracking algorithm」是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。 -回溯算法通常采用「深度优先搜索」来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。 +回溯算法通常采用“深度优先搜索”来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。 !!! question "例题一" diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index 4ab71a894..e3481087b 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -296,7 +296,7 @@ comments: true 空间复杂度的推算方法与时间复杂度大致相同,只需将统计对象从“操作数量”转为“使用空间大小”。 -而与时间复杂度不同的是,**我们通常只关注「最差空间复杂度」**。这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。 +而与时间复杂度不同的是,**我们通常只关注最差空间复杂度**。这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。 观察以下代码,最差空间复杂度中的“最差”有两层含义。 @@ -1800,7 +1800,7 @@ $$ ### 4. 指数阶 $O(2^n)$ -指数阶常见于二叉树。高度为 $n$ 的「满二叉树」的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间: +指数阶常见于二叉树。高度为 $n$ 的“满二叉树”的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间: === "Java" diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index 20ac78e7e..eddf50b01 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -191,7 +191,7 @@ $$ ## 2.2.1 统计时间增长趋势 -「时间复杂度分析」采取了一种不同的方法,其统计的不是算法运行时间,**而是算法运行时间随着数据量变大时的增长趋势**。 +时间复杂度分析统计的不是算法运行时间,**而是算法运行时间随着数据量变大时的增长趋势**。 “时间增长趋势”这个概念比较抽象,我们通过一个例子来加以理解。假设输入数据大小为 $n$ ,给定三个算法函数 `A` 、 `B` 和 `C` : @@ -430,11 +430,11 @@ $$ } ``` -算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为「常数阶」。 +算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为“常数阶”。 -算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大呈线性增长。此算法的时间复杂度被称为「线性阶」。 +算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大呈线性增长。此算法的时间复杂度被称为“线性阶”。 -算法 `C` 中的打印操作需要循环 $1000000$ 次,虽然运行时间很长,但它与输入数据大小 $n$ 无关。因此 `C` 的时间复杂度和 `A` 相同,仍为「常数阶」。 +算法 `C` 中的打印操作需要循环 $1000000$ 次,虽然运行时间很长,但它与输入数据大小 $n$ 无关。因此 `C` 的时间复杂度和 `A` 相同,仍为“常数阶”。  @@ -2247,7 +2247,7 @@ $$ } ``` -指数阶增长非常迅速,在穷举法(暴力搜索、回溯等)中比较常见。对于数据规模较大的问题,指数阶是不可接受的,通常需要使用「动态规划」或「贪心」等算法来解决。 +指数阶增长非常迅速,在穷举法(暴力搜索、回溯等)中比较常见。对于数据规模较大的问题,指数阶是不可接受的,通常需要使用动态规划或贪心等算法来解决。 ### 5. 对数阶 $O(\log n)$ @@ -2962,7 +2962,7 @@ $$ - 当 `nums = [?, ?, ..., 1]` ,即当末尾元素是 $1$ 时,需要完整遍历数组,**达到最差时间复杂度 $O(n)$** 。 - 当 `nums = [1, ?, ?, ...]` ,即当首个元素为 $1$ 时,无论数组多长都不需要继续遍历,**达到最佳时间复杂度 $\Omega(1)$** 。 -「最差时间复杂度」对应函数渐近上界,使用大 $O$ 记号表示。相应地,「最佳时间复杂度」对应函数渐近下界,用 $\Omega$ 记号表示: +“最差时间复杂度”对应函数渐近上界,使用大 $O$ 记号表示。相应地,“最佳时间复杂度”对应函数渐近下界,用 $\Omega$ 记号表示: === "Java" @@ -3314,9 +3314,9 @@ $$ } ``` -值得说明的是,我们在实际中很少使用「最佳时间复杂度」,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。**而「最差时间复杂度」更为实用,因为它给出了一个效率安全值**,让我们可以放心地使用算法。 +值得说明的是,我们在实际中很少使用最佳时间复杂度,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。**而最差时间复杂度更为实用,因为它给出了一个效率安全值**,让我们可以放心地使用算法。 -从上述示例可以看出,最差或最佳时间复杂度只出现于“特殊的数据分布”,这些情况的出现概率可能很小,并不能真实地反映算法运行效率。相比之下,**「平均时间复杂度」可以体现算法在随机输入数据下的运行效率**,用 $\Theta$ 记号来表示。 +从上述示例可以看出,最差或最佳时间复杂度只出现于“特殊的数据分布”,这些情况的出现概率可能很小,并不能真实地反映算法运行效率。相比之下,**平均时间复杂度可以体现算法在随机输入数据下的运行效率**,用 $\Theta$ 记号来表示。 对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数就是数组长度的一半 $\frac{n}{2}$ ,平均时间复杂度为 $\Theta(\frac{n}{2}) = \Theta(n)$ 。 diff --git a/chapter_data_structure/basic_data_types.md b/chapter_data_structure/basic_data_types.md index 2fe8e17d1..b4b25ecda 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}$ 个数字。 下表列举了各种基本数据类型的占用空间、取值范围和默认值。此表格无须硬背,大致理解即可,需要时可以通过查表来回忆。 +表:基本数据类型的占用空间和取值范围
图:原码、反码与补码之间的相互转换
-显然「原码」最为直观。但实际上,**数字是以「补码」的形式存储在计算机中的**。这是因为原码存在一些局限性。 - -一方面,**负数的原码不能直接用于运算**。例如,我们在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。 +「原码 true form」虽然最直观,但存在一些局限性。一方面,**负数的原码不能直接用于运算**。例如在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。 $$ \begin{aligned} @@ -35,7 +33,7 @@ $$ \end{aligned} $$ -为了解决此问题,计算机引入了「反码」。如果我们先将原码转换为反码,并在反码下计算 $1 + (-2)$ ,最后将结果从反码转化回原码,则可得到正确结果 $-1$ 。 +为了解决此问题,计算机引入了「反码 1's complement code」。如果我们先将原码转换为反码,并在反码下计算 $1 + (-2)$ ,最后将结果从反码转化回原码,则可得到正确结果 $-1$ 。 $$ \begin{aligned} @@ -57,7 +55,7 @@ $$ \end{aligned} $$ -与原码一样,反码也存在正负零歧义问题,因此计算机进一步引入了「补码」。我们先来观察一下负零的原码、反码、补码的转换过程: +与原码一样,反码也存在正负零歧义问题,因此计算机进一步引入了「补码 2's complement code」。我们先来观察一下负零的原码、反码、补码的转换过程: $$ \begin{aligned} @@ -144,6 +142,7 @@ $$ **尽管浮点数 `float` 扩展了取值范围,但其副作用是牺牲了精度**。整数类型 `int` 将全部 32 位用于表示数字,数字是均匀分布的;而由于指数位的存在,浮点数 `float` 的数值越大,相邻两个数字之间的差值就会趋向越大。 进一步地,指数位 $E = 0$ 和 $E = 255$ 具有特殊含义,**用于表示零、无穷大、$\mathrm{NaN}$ 等**。 +表:指数位含义
表:根节点和子树在前序和中序遍历中的索引
图:爬楼梯对应递归树
-观察上图发现,**指数阶的时间复杂度是由于「重叠子问题」导致的**。例如:$dp[9]$ 被分解为 $dp[8]$ 和 $dp[7]$ ,$dp[8]$ 被分解为 $dp[7]$ 和 $dp[6]$ ,两者都包含子问题 $dp[7]$ 。 +观察上图发现,**指数阶的时间复杂度是由于“重叠子问题”导致的**。例如:$dp[9]$ 被分解为 $dp[8]$ 和 $dp[7]$ ,$dp[8]$ 被分解为 $dp[7]$ 和 $dp[6]$ ,两者都包含子问题 $dp[7]$ 。 以此类推,子问题中包含更小的重叠子问题,子子孙孙无穷尽也。绝大部分计算资源都浪费在这些重叠的问题上。 diff --git a/chapter_graph/graph.md b/chapter_graph/graph.md index 4672471f6..e29a90751 100644 --- a/chapter_graph/graph.md +++ b/chapter_graph/graph.md @@ -4,7 +4,7 @@ comments: true # 9.1 图 -「图 Graph」是一种非线性数据结构,由「顶点 Vertex」和「边 Edge」组成。我们可以将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。以下示例展示了一个包含 5 个顶点和 7 条边的图。 +「图 graph」是一种非线性数据结构,由「顶点 vertex」和「边 edge」组成。我们可以将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。以下示例展示了一个包含 5 个顶点和 7 条边的图。 $$ \begin{aligned} @@ -18,11 +18,11 @@ $$图:链表、树、图之间的关系
-那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作节点,把「边」看作连接各个节点的指针,则可将「图」看作是一种从「链表」拓展而来的数据结构。**相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,从而更为复杂**。 +那么,图与其他数据结构的关系是什么?如果我们把顶点看作节点,把边看作连接各个节点的指针,则可将图看作是一种从链表拓展而来的数据结构。**相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,从而更为复杂**。 ## 9.1.1 图常见类型 -根据边是否具有方向,可分为「无向图 Undirected Graph」和「有向图 Directed Graph」。 +根据边是否具有方向,可分为「无向图 undirected graph」和「有向图 directed graph」。 - 在无向图中,边表示两顶点之间的“双向”连接关系,例如微信或 QQ 中的“好友关系”。 - 在有向图中,边具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系。 @@ -31,7 +31,7 @@ $$图:有向图与无向图
-根据所有顶点是否连通,可分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。 +根据所有顶点是否连通,可分为「连通图 connected graph」和「非连通图 disconnected graph」。 - 对于连通图,从某个顶点出发,可以到达其余任意顶点。 - 对于非连通图,从某个顶点出发,至少有一个顶点无法到达。 @@ -40,7 +40,7 @@ $$图:连通图与非连通图
-我们还可以为边添加“权重”变量,从而得到「有权图 Weighted Graph」。例如,在王者荣耀等手游中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以用有权图来表示。 +我们还可以为边添加“权重”变量,从而得到「有权图 weighted graph」。例如,在王者荣耀等手游中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以用有权图来表示。  @@ -48,19 +48,19 @@ $$ ## 9.1.2 图常用术语 -- 「邻接 Adjacency」:当两顶点之间存在边相连时,称这两顶点“邻接”。在上图中,顶点 1 的邻接顶点为顶点 2、3、5。 -- 「路径 Path」:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。在上图中,边序列 1-5-2-4 是顶点 1 到顶点 4 的一条路径。 -- 「度 Degree」表示一个顶点拥有的边数。对于有向图,「入度 In-Degree」表示有多少条边指向该顶点,「出度 Out-Degree」表示有多少条边从该顶点指出。 +- 「邻接 adjacency」:当两顶点之间存在边相连时,称这两顶点“邻接”。在上图中,顶点 1 的邻接顶点为顶点 2、3、5。 +- 「路径 path」:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。在上图中,边序列 1-5-2-4 是顶点 1 到顶点 4 的一条路径。 +- 「度 degree」:一个顶点拥有的边数。对于有向图,「入度 In-Degree」表示有多少条边指向该顶点,「出度 Out-Degree」表示有多少条边从该顶点指出。 ## 9.1.3 图的表示 -图的常用表示方法包括「邻接矩阵」和「邻接表」。以下使用无向图进行举例。 +图的常用表示方法包括“邻接矩阵”和“邻接表”。以下使用无向图进行举例。 ### 1. 邻接矩阵 -设图的顶点数量为 $n$ ,「邻接矩阵 Adjacency Matrix」使用一个 $n \times n$ 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 $1$ 或 $0$ 表示两个顶点之间是否存在边。 +设图的顶点数量为 $n$ ,「邻接矩阵 adjacency matrix」使用一个 $n \times n$ 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 $1$ 或 $0$ 表示两个顶点之间是否存在边。 -如下图所示,设邻接矩阵为 $M$ 、顶点列表为 $V$ ,那么矩阵元素 $M[i][j] = 1$ 表示顶点 $V[i]$ 到顶点 $V[j]$ 之间存在边,反之 $M[i][j] = 0$ 表示两顶点之间无边。 +如下图所示,设邻接矩阵为 $M$ 、顶点列表为 $V$ ,那么矩阵元素 $M[i, j] = 1$ 表示顶点 $V[i]$ 到顶点 $V[j]$ 之间存在边,反之 $M[i, j] = 0$ 表示两顶点之间无边。  @@ -76,7 +76,7 @@ $$ ### 2. 邻接表 -「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表节点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点(即与该顶点相连的顶点)。 +「邻接表 adjacency list」使用 $n$ 个链表来表示图,链表节点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点(即与该顶点相连的顶点)。  @@ -84,11 +84,12 @@ $$ 邻接表仅存储实际存在的边,而边的总数通常远小于 $n^2$ ,因此它更加节省空间。然而,在邻接表中需要通过遍历链表来查找边,因此其时间效率不如邻接矩阵。 -观察上图可发现,**邻接表结构与哈希表中的「链地址法」非常相似,因此我们也可以采用类似方法来优化效率**。例如,当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;此外,还可以将链表转换为哈希表,将时间复杂度降低至 $O(1)$ 。 +观察上图,**邻接表结构与哈希表中的“链式地址”非常相似,因此我们也可以采用类似方法来优化效率**。比如当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ;还可以把链表转换为哈希表,从而将时间复杂度降低至 $O(1)$ 。 ## 9.1.4 图常见应用 实际应用中,许多系统都可以用图来建模,相应的待求解问题也可以约化为图计算问题。 +表:现实生活中常见的图
表:邻接矩阵与邻接表对比
表:元素查询效率对比
表:堆的操作效率
图:查字典步骤
-查阅字典这个小学生必备技能,实际上就是著名的二分查找算法。从数据结构的角度,我们可以把字典视为一个已排序的「数组」;从算法的角度,我们可以将上述查字典的一系列操作看作是「二分查找」。 +查阅字典这个小学生必备技能,实际上就是著名的二分查找算法。从数据结构的角度,我们可以把字典视为一个已排序的“数组”;从算法的角度,我们可以将上述查字典的一系列操作看作是“二分查找”。 **例二:整理扑克**。我们在打牌时,每局都需要整理扑克牌,使其从小到大排列,实现流程如下图所示。 @@ -43,7 +43,7 @@ comments: true图:扑克排序步骤
-上述整理扑克牌的方法本质上是「插入排序」算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都存在插入排序的身影。 +上述整理扑克牌的方法本质上是“插入排序”算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都存在插入排序的身影。 **例三:货币找零**。假设我们在超市购买了 $69$ 元的商品,给收银员付了 $100$ 元,则收银员需要找我们 $31$ 元。他会很自然地完成如下图所示的思考。 @@ -57,7 +57,7 @@ comments: true图:货币找零过程
-在以上步骤中,我们每一步都采取当前看来最好的选择(尽可能用大面额的货币),最终得到了可行的找零方案。从数据结构与算法的角度看,这种方法本质上是「贪心算法」。 +在以上步骤中,我们每一步都采取当前看来最好的选择(尽可能用大面额的货币),最终得到了可行的找零方案。从数据结构与算法的角度看,这种方法本质上是“贪心”算法。 小到烹饪一道菜,大到星际航行,几乎所有问题的解决都离不开算法。计算机的出现使我们能够通过编程将数据结构存储在内存中,同时编写代码调用 CPU 和 GPU 执行算法。这样一来,我们就能把生活中的问题转移到计算机上,以更高效的方式解决各种复杂问题。 diff --git a/chapter_introduction/what_is_dsa.md b/chapter_introduction/what_is_dsa.md index 143658191..a1d80e524 100644 --- a/chapter_introduction/what_is_dsa.md +++ b/chapter_introduction/what_is_dsa.md @@ -62,4 +62,4 @@ comments: true !!! tip "约定俗成的简称" - 在实际讨论时,我们通常会将「数据结构与算法」简称为「算法」。比如众所周知的 LeetCode 算法题目,实际上同时考察了数据结构和算法两方面的知识。 + 在实际讨论时,我们通常会将“数据结构与算法”简称为“算法”。比如众所周知的 LeetCode 算法题目,实际上同时考察了数据结构和算法两方面的知识。 diff --git a/chapter_preface/suggestions.md b/chapter_preface/suggestions.md index 093006a24..fe199e114 100644 --- a/chapter_preface/suggestions.md +++ b/chapter_preface/suggestions.md @@ -11,10 +11,10 @@ comments: true ## 0.2.1 行文风格约定 - 标题后标注 `*` 的是选读章节,内容相对困难。如果你的时间有限,建议可以先跳过。 -- 文章中的重要名词会用 `「 」` 括号标注,例如 `「数组 Array」` 。请务必记住这些名词,包括英文翻译,以便后续阅读文献时使用。 -- **加粗的文字** 表示重点内容或总结性语句,这类文字值得特别关注。 - 专有名词和有特指含义的词句会使用 `“双引号”` 标注,以避免歧义。 -- 涉及到编程语言之间不一致的名词,本书均以 Python 为准,例如使用 $\text{None}$ 来表示“空”。 +- 重要专有名词及其英文翻译会用 `「 」` 括号标注,例如 `「数组 array」` 。建议记住它们,以便阅读文献。 +- **加粗的文字** 表示重点内容或总结性语句,这类文字值得特别关注。 +- 当涉及到编程语言之间不一致的名词时,本书均以 Python 为准,例如使用 $\text{None}$ 来表示“空”。 - 本书部分放弃了编程语言的注释规范,以换取更加紧凑的内容排版。注释主要分为三种类型:标题注释、内容注释、多行注释。 === "Java" diff --git a/chapter_searching/binary_search.md b/chapter_searching/binary_search.md index 1d52839f3..874a27b6c 100755 --- a/chapter_searching/binary_search.md +++ b/chapter_searching/binary_search.md @@ -4,7 +4,7 @@ comments: true # 10.1 二分查找 -「二分查找 Binary Search」是一种基于分治思想的高效搜索算法。它利用数据的有序性,每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。 +「二分查找 binary search」是一种基于分治思想的高效搜索算法。它利用数据的有序性,每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。 !!! question diff --git a/chapter_searching/searching_algorithm_revisited.md b/chapter_searching/searching_algorithm_revisited.md index 11578a422..6ff0919d8 100644 --- a/chapter_searching/searching_algorithm_revisited.md +++ b/chapter_searching/searching_algorithm_revisited.md @@ -4,7 +4,7 @@ comments: true # 10.5 重识搜索算法 -「搜索算法 Searching Algorithm」用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。 +「搜索算法 searching algorithm」用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。 根据实现思路,搜索算法总体可分为两种: @@ -17,8 +17,8 @@ comments: true 暴力搜索通过遍历数据结构的每个元素来定位目标元素。 -- 「线性搜索」适用于数组和链表等线性数据结构。它从数据结构的一端开始,逐个访问元素,直到找到目标元素或到达另一端仍没有找到目标元素为止。 -- 「广度优先搜索」和「深度优先搜索」是图和树的两种遍历策略。广度优先搜索从初始节点开始逐层搜索,由近及远地访问各个节点。深度优先搜索是从初始节点开始,沿着一条路径走到头为止,再回溯并尝试其他路径,直到遍历完整个数据结构。 +- “线性搜索”适用于数组和链表等线性数据结构。它从数据结构的一端开始,逐个访问元素,直到找到目标元素或到达另一端仍没有找到目标元素为止。 +- “广度优先搜索”和“深度优先搜索”是图和树的两种遍历策略。广度优先搜索从初始节点开始逐层搜索,由近及远地访问各个节点。深度优先搜索是从初始节点开始,沿着一条路径走到头为止,再回溯并尝试其他路径,直到遍历完整个数据结构。 暴力搜索的优点是简单且通用性好,**无须对数据做预处理和借助额外的数据结构**。 @@ -28,9 +28,9 @@ comments: true 自适应搜索利用数据的特有属性(例如有序性)来优化搜索过程,从而更高效地定位目标元素。 -- 「二分查找」利用数据的有序性实现高效查找,仅适用于数组。 -- 「哈希查找」利用哈希表将搜索数据和目标数据建立为键值对映射,从而实现查询操作。 -- 「树查找」在特定的树结构(例如二叉搜索树)中,基于比较节点值来快速排除节点,从而定位目标元素。 +- “二分查找”利用数据的有序性实现高效查找,仅适用于数组。 +- “哈希查找”利用哈希表将搜索数据和目标数据建立为键值对映射,从而实现查询操作。 +- “树查找”在特定的树结构(例如二叉搜索树)中,基于比较节点值来快速排除节点,从而定位目标元素。 此类算法的优点是效率高,**时间复杂度可达到 $O(\log n)$ 甚至 $O(1)$** 。 @@ -49,6 +49,7 @@ comments: true图:多种搜索策略
上述几种方法的操作效率与特性如下表所示。 +表:查找算法效率对比
表:双向队列操作效率
表:队列操作效率
图:基于数组实现队列的入队出队操作
-你可能会发现一个问题:在不断进行入队和出队的过程中,`front` 和 `rear` 都在向右移动,**当它们到达数组尾部时就无法继续移动了**。为解决此问题,我们可以将数组视为首尾相接的「环形数组」。 +你可能会发现一个问题:在不断进行入队和出队的过程中,`front` 和 `rear` 都在向右移动,**当它们到达数组尾部时就无法继续移动了**。为解决此问题,我们可以将数组视为首尾相接的“环形数组”。 对于环形数组,我们需要让 `front` 或 `rear` 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示。 diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md index 43ef30b5f..1ae2986f1 100755 --- a/chapter_stack_and_queue/stack.md +++ b/chapter_stack_and_queue/stack.md @@ -4,11 +4,11 @@ comments: true # 5.1 栈 -「栈 Stack」是一种遵循先入后出(First In, Last Out)原则的线性数据结构。 +「栈 stack」是一种遵循先入后出的逻辑的线性数据结构。 我们可以将栈类比为桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。我们将盘子替换为各种类型的元素(如整数、字符、对象等),就得到了栈数据结构。 -在栈中,我们把堆叠元素的顶部称为「栈顶」,底部称为「栈底」。将把元素添加到栈顶的操作叫做「入栈」,而删除栈顶元素的操作叫做「出栈」。 +在栈中,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫做“入栈”,而删除栈顶元素的操作叫做“出栈”。  @@ -17,6 +17,7 @@ comments: true ## 5.1.1 栈常用操作 栈的常用操作如下表所示,具体的方法名需要根据所使用的编程语言来确定。在此,我们以常见的 `push()` , `pop()` , `peek()` 命名为例。 +表:栈的操作效率
图:右旋操作步骤
-此外,如果节点 `child` 本身有右子节点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子节点。 +此外,如果节点 `child` 本身有右子节点(记为 `grandChild` ),则需要在右旋中添加一步:将 `grandChild` 作为 `node` 的左子节点。  @@ -835,19 +835,19 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 ### 2. 左旋 -相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。 +相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行“左旋”操作。 图:左旋操作
-同理,若节点 `child` 本身有左子节点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子节点。 +同理,若节点 `child` 本身有左子节点(记为 `grandChild` ),则需要在左旋中添加一步:将 `grandChild` 作为 `node` 的右子节点。 图:有 grandChild 的左旋操作
-可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们可以轻松地从右旋的代码推导出左旋的代码。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` ,将所有的 `right` 替换为 `left` ,即可得到「左旋」代码。 +可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们只需将右旋的实现代码中的所有的 `left` 替换为 `right` ,将所有的 `right` 替换为 `left` ,即可得到左旋的实现代码。 === "Java" @@ -1072,7 +1072,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 ### 3. 先左旋后右旋 -对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 +对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行“左旋”,再对 `node` 执行“右旋”。  @@ -1080,7 +1080,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 ### 4. 先右旋后左旋 -同理,对于上述失衡二叉树的镜像情况,需要先右旋后左旋,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。 +同理,对于上述失衡二叉树的镜像情况,需要先右旋后左旋,即先对 `child` 执行“右旋”,然后对 `node` 执行“左旋”。  @@ -1095,6 +1095,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉图:AVL 树的四种旋转情况
在代码中,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于上图中的哪种情况。 +表:四种旋转情况的选择条件
表:数组与搜索树的效率对比
图:二叉树的最佳与最差结构
如下表所示,在最佳和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大或极小值。 +表:二叉树的最佳与最差情况
图:二叉树的层序遍历
-广度优先遍历通常借助「队列」来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。 +广度优先遍历通常借助“队列”来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。 === "Java" @@ -328,7 +328,7 @@ comments: true ## 7.2.2 前序、中序、后序遍历 -相应地,前序、中序和后序遍历都属于「深度优先遍历 Depth-First Traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。 +相应地,前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。 如下图所示,左侧是深度优先遍历的示意图,右上方是对应的递归代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,在这个过程中,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。