diff --git a/README.md b/README.md index 30a9af2a1..bd9b46c0b 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ 我们正在加速更新本书,欢迎您通过提交 Pull Request 来[参与本项目](https://www.hello-algo.com/chapter_appendix/contribution/),以帮助其他读者获取更优质的学习内容。 -- 若您发现语法错误、内容缺失、文字歧义、无效链接、解释不清晰等问题,请协助修正或在评论区指出; -- 期待您参与 C++, Python, Go, JavaScript, TypeScript, C, C#, Swift, Zig, Rust, Dart 等语言的[代码翻译](https://github.com/krahets/hello-algo/issues/15); -- 欢迎您为本书内容提出宝贵意见和建议,如有任何问题请提交 Issues 或微信联系 krahets-jyd ; +- 若您发现语法错误、内容缺失、文字歧义、无效链接、解释不清晰等问题,请协助修正或在评论区指出。 +- 期待您参与 C++, Python, Go, JavaScript, TypeScript, C, C#, Swift, Zig, Rust, Dart 等语言的[代码翻译](https://github.com/krahets/hello-algo/issues/15)。 +- 欢迎您为本书内容提出宝贵意见和建议,如有任何问题请提交 Issues 或微信联系 krahets-jyd 。 感谢本开源书的每一位撰稿人,是他们的无私奉献让这本书变得更好,他们是: diff --git a/docs/chapter_appendix/contribution.md b/docs/chapter_appendix/contribution.md index 2f1a64d49..d05e3f3da 100644 --- a/docs/chapter_appendix/contribution.md +++ b/docs/chapter_appendix/contribution.md @@ -10,23 +10,23 @@ 在每个页面的右上角有一个「编辑」图标,您可以按照以下步骤修改文本或代码: -1. 点击编辑按钮,如果遇到“需要 Fork 此仓库”的提示,请同意该操作; -2. 修改 Markdown 源文件内容,并确保内容正确,同时尽量保持排版格式的统一; +1. 点击编辑按钮,如果遇到“需要 Fork 此仓库”的提示,请同意该操作。 +2. 修改 Markdown 源文件内容,并确保内容正确,同时尽量保持排版格式的统一。 3. 在页面底部填写修改说明,然后点击“Propose file change”按钮;页面跳转后,点击“Create pull request”按钮即可发起拉取请求。 ![页面编辑按键](contribution.assets/edit_markdown.png) -由于图片无法直接修改,因此需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述图片问题,我们会尽快重新绘制并替换图片。 +由于图片无法直接修改,因此需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述问题,我们会尽快重新绘制并替换图片。 ## 内容创作 如果您有兴趣参与此开源项目,包括将代码翻译成其他编程语言、扩展文章内容等,那么需要实施 Pull Request 工作流程: -1. 登录 GitHub ,将[本仓库](https://github.com/krahets/hello-algo) Fork 到个人账号下; -2. 进入您的 Fork 仓库网页,使用 git clone 命令将仓库克隆至本地; -3. 在本地进行内容创作,并通过运行测试以验证代码的正确性; -4. 将本地所做更改 Commit ,然后 Push 至远程仓库; -5. 刷新仓库网页,点击“Create pull request”按钮即可发起拉取请求; +1. 登录 GitHub ,将[本仓库](https://github.com/krahets/hello-algo) Fork 到个人账号下。 +2. 进入您的 Fork 仓库网页,使用 git clone 命令将仓库克隆至本地。 +3. 在本地进行内容创作,并通过运行测试以验证代码的正确性。 +4. 将本地所做更改 Commit ,然后 Push 至远程仓库。 +5. 刷新仓库网页,点击“Create pull request”按钮即可发起拉取请求。 ## Docker 部署 diff --git a/docs/chapter_appendix/installation.md b/docs/chapter_appendix/installation.md index ec348e913..37b2088f3 100644 --- a/docs/chapter_appendix/installation.md +++ b/docs/chapter_appendix/installation.md @@ -35,15 +35,15 @@ ## C# 环境 -1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) ; +1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) 。 2. 在 VSCode 的插件市场中搜索 `c#` ,安装 c# 。 ## Swift 环境 -1. 下载并安装 [Swift](https://www.swift.org/download/); +1. 下载并安装 [Swift](https://www.swift.org/download/)。 2. 在 VSCode 的插件市场中搜索 `swift` ,安装 [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)。 ## Rust 环境 -1. 下载并安装 [Rust](https://www.rust-lang.org/tools/install); +1. 下载并安装 [Rust](https://www.rust-lang.org/tools/install)。 2. 在 VSCode 的插件市场中搜索 `rust` ,安装 [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)。 diff --git a/docs/chapter_array_and_linkedlist/summary.md b/docs/chapter_array_and_linkedlist/summary.md index b211db77c..0d16699f0 100644 --- a/docs/chapter_array_and_linkedlist/summary.md +++ b/docs/chapter_array_and_linkedlist/summary.md @@ -30,9 +30,9 @@ 栈内存分配由编译器自动完成,而堆内存由程序员在代码中分配(注意,这里的栈和堆和数据结构中的栈和堆不是同一概念)。 - 1. 栈不灵活,分配的内存大小不可更改;堆相对灵活,可以动态分配内存; - 2. 栈是一块比较小的内存,容易出现内存不足;堆内存很大,但是由于是动态分配,容易碎片化,管理堆内存的难度更大、成本更高; - 3. 访问栈比访问堆更快,因为栈内存较小、对缓存友好,堆帧分散在很大的空间内,会出现更多的缓存未命中; + 1. 栈不灵活,分配的内存大小不可更改;堆相对灵活,可以动态分配内存。 + 2. 栈是一块比较小的内存,容易出现内存不足;堆内存很大,但是由于是动态分配,容易碎片化,管理堆内存的难度更大、成本更高。 + 3. 访问栈比访问堆更快,因为栈内存较小、对缓存友好,堆帧分散在很大的空间内,会出现更多的缓存未命中。 !!! question "为什么数组会强调要求相同类型的元素,而在链表中却没有强调同类型呢?" diff --git a/docs/chapter_backtracking/backtracking_algorithm.md b/docs/chapter_backtracking/backtracking_algorithm.md index f28e941d0..5d68aa183 100644 --- a/docs/chapter_backtracking/backtracking_algorithm.md +++ b/docs/chapter_backtracking/backtracking_algorithm.md @@ -782,6 +782,6 @@ 请注意,对于许多组合优化问题,回溯都不是最优解决方案,例如: -- 0-1 背包问题通常使用动态规划解决,以达到更高的时间效率; -- 旅行商是一个著名的 NP-Hard 问题,常用解法有遗传算法和蚁群算法等; -- 最大团问题是图论中的一个经典问题,可用贪心等启发式算法来解决; +- 0-1 背包问题通常使用动态规划解决,以达到更高的时间效率。 +- 旅行商是一个著名的 NP-Hard 问题,常用解法有遗传算法和蚁群算法等。 +- 最大团问题是图论中的一个经典问题,可用贪心等启发式算法来解决。 diff --git a/docs/chapter_backtracking/subset_sum_problem.md b/docs/chapter_backtracking/subset_sum_problem.md index 30cf5ce17..589e2e954 100644 --- a/docs/chapter_backtracking/subset_sum_problem.md +++ b/docs/chapter_backtracking/subset_sum_problem.md @@ -125,8 +125,8 @@ 分支越靠右,需要排除的分支也越多,例如: -1. 前两轮选择 $3$ , $5$ ,生成子集 $[3, 5, \cdots]$ ; -2. 前两轮选择 $4$ , $5$ ,生成子集 $[4, 5, \cdots]$ ; +1. 前两轮选择 $3$ , $5$ ,生成子集 $[3, 5, \cdots]$ 。 +2. 前两轮选择 $4$ , $5$ ,生成子集 $[4, 5, \cdots]$ 。 3. 若第一轮选择 $5$ ,**则第二轮应该跳过 $3$ 和 $4$** ,因为子集 $[5, 3, \cdots]$ 和子集 $[5, 4, \cdots]$ 和 `1.` , `2.` 中生成的子集完全重复。 ![不同选择顺序导致的重复子集](subset_sum_problem.assets/subset_sum_i_pruning.png) diff --git a/docs/chapter_computational_complexity/performance_evaluation.md b/docs/chapter_computational_complexity/performance_evaluation.md index 561205b5d..de1d76625 100644 --- a/docs/chapter_computational_complexity/performance_evaluation.md +++ b/docs/chapter_computational_complexity/performance_evaluation.md @@ -30,9 +30,9 @@ **复杂度分析评估的是算法运行效率随着输入数据量增多时的增长趋势**。这个定义有些拗口,我们可以将其分为三个重点来理解: -- “算法运行效率”可分为“运行时间”和“占用空间”,因此我们可以将复杂度分为「时间复杂度 Time Complexity」和「空间复杂度 Space Complexity」; -- “随着输入数据量增多时”表示复杂度与输入数据量有关,反映了算法运行效率与输入数据量之间的关系; -- “增长趋势”表示复杂度分析关注的是算法时间与空间的增长趋势,而非具体的运行时间或占用空间; +- “算法运行效率”可分为“运行时间”和“占用空间”,因此我们可以将复杂度分为「时间复杂度 Time Complexity」和「空间复杂度 Space Complexity」。 +- “随着输入数据量增多时”表示复杂度与输入数据量有关,反映了算法运行效率与输入数据量之间的关系。 +- “增长趋势”表示复杂度分析关注的是算法时间与空间的增长趋势,而非具体的运行时间或占用空间。 **复杂度分析克服了实际测试方法的弊端**。首先,它独立于测试环境,因此分析结果适用于所有运行平台。其次,它可以体现不同数据量下的算法效率,尤其是在大数据量下的算法性能。 diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index fae47b095..a637b1371 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -6,9 +6,9 @@ 算法运行过程中使用的内存空间主要包括以下几种: -- 「输入空间」用于存储算法的输入数据; -- 「暂存空间」用于存储算法运行过程中的变量、对象、函数上下文等数据; -- 「输出空间」用于存储算法的输出数据; +- 「输入空间」用于存储算法的输入数据。 +- 「暂存空间」用于存储算法运行过程中的变量、对象、函数上下文等数据。 +- 「输出空间」用于存储算法的输出数据。 通常情况下,空间复杂度统计范围是「暂存空间」+「输出空间」。 @@ -286,8 +286,8 @@ **最差空间复杂度中的“最差”有两层含义**,分别是输入数据的最差分布和算法运行过程中的最差时间点。 -- **以最差输入数据为准**。当 $n < 10$ 时,空间复杂度为 $O(1)$ ;但当 $n > 10$ 时,初始化的数组 `nums` 占用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; -- **以算法运行过程中的峰值内存为准**。例如,程序在执行最后一行之前,占用 $O(1)$ 空间;当初始化数组 `nums` 时,程序占用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; +- **以最差输入数据为准**。当 $n < 10$ 时,空间复杂度为 $O(1)$ ;但当 $n > 10$ 时,初始化的数组 `nums` 占用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ 。 +- **以算法运行过程中的峰值内存为准**。例如,程序在执行最后一行之前,占用 $O(1)$ 空间;当初始化数组 `nums` 时,程序占用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ 。 === "Java" diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index 122587bc9..ba9e5cf9c 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -1632,8 +1632,8 @@ $$ **某些算法的时间复杂度不是固定的,而是与输入数据的分布有关**。例如,假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论: -- 当 `nums = [?, ?, ..., 1]` ,即当末尾元素是 $1$ 时,需要完整遍历数组,此时达到 **最差时间复杂度 $O(n)$**; -- 当 `nums = [1, ?, ?, ...]` ,即当首个数字为 $1$ 时,无论数组多长都不需要继续遍历,此时达到 **最佳时间复杂度 $\Omega(1)$**; +- 当 `nums = [?, ?, ..., 1]` ,即当末尾元素是 $1$ 时,需要完整遍历数组,此时达到 **最差时间复杂度 $O(n)$** 。 +- 当 `nums = [1, ?, ?, ...]` ,即当首个数字为 $1$ 时,无论数组多长都不需要继续遍历,此时达到 **最佳时间复杂度 $\Omega(1)$** 。 “函数渐近上界”使用大 $O$ 记号表示,代表「最差时间复杂度」。相应地,“函数渐近下界”用 $\Omega$ 记号来表示,代表「最佳时间复杂度」。 diff --git a/docs/chapter_data_structure/basic_data_types.md b/docs/chapter_data_structure/basic_data_types.md index 0546a2e87..96b4567ab 100644 --- a/docs/chapter_data_structure/basic_data_types.md +++ b/docs/chapter_data_structure/basic_data_types.md @@ -4,15 +4,15 @@ **基本数据类型是 CPU 可以直接进行运算的类型,在算法中直接被使用**。它包括: -- 整数类型 `byte` , `short` , `int` , `long` ; -- 浮点数类型 `float` , `double` ,用于表示小数; -- 字符类型 `char` ,用于表示各种语言的字母、标点符号、甚至表情符号等; -- 布尔类型 `bool` ,用于表示“是”与“否”判断; +- 整数类型 `byte` , `short` , `int` , `long` 。 +- 浮点数类型 `float` , `double` ,用于表示小数。 +- 字符类型 `char` ,用于表示各种语言的字母、标点符号、甚至表情符号等。 +- 布尔类型 `bool` ,用于表示“是”与“否”判断。 **所有基本数据类型都以二进制的形式存储在计算机中**。在计算机中,我们将 $1$ 个二进制位称为 $1$ 比特,并规定 $1$ 字节(byte)由 $8$ 比特(bits)组成。基本数据类型的取值范围取决于其占用的空间大小,例如: -- 整数类型 `byte` 占用 $1$ byte = $8$ bits ,可以表示 $2^{8}$ 个不同的数字; -- 整数类型 `int` 占用 $4$ bytes = $32$ bits ,可以表示 $2^{32}$ 个数字; +- 整数类型 `byte` 占用 $1$ byte = $8$ bits ,可以表示 $2^{8}$ 个不同的数字。 +- 整数类型 `int` 占用 $4$ bytes = $32$ bits ,可以表示 $2^{32}$ 个数字。 下表列举了各种基本数据类型的占用空间、取值范围和默认值。此表格无需硬背,大致理解即可,需要时可以通过查表来回忆。 diff --git a/docs/chapter_data_structure/classification_of_data_structure.md b/docs/chapter_data_structure/classification_of_data_structure.md index d49d03f60..aaa037985 100644 --- a/docs/chapter_data_structure/classification_of_data_structure.md +++ b/docs/chapter_data_structure/classification_of_data_structure.md @@ -8,16 +8,16 @@ 逻辑结构通常分为“线性”和“非线性”两类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。 -- **线性数据结构**:数组、链表、栈、队列、哈希表; -- **非线性数据结构**:树、堆、图、哈希表; +- **线性数据结构**:数组、链表、栈、队列、哈希表。 +- **非线性数据结构**:树、堆、图、哈希表。 ![线性与非线性数据结构](classification_of_data_structure.assets/classification_logic_structure.png) 非线性数据结构可以进一步被划分为树形结构和网状结构。 -- **线性结构**:数组、链表、队列、栈、哈希表,元素存在一对一的顺序关系; -- **树形结构**:树、堆、哈希表,元素存在一对多的关系; -- **网状结构**:图,元素存在多对多的关系; +- **线性结构**:数组、链表、队列、栈、哈希表,元素存在一对一的顺序关系。 +- **树形结构**:树、堆、哈希表,元素存在一对多的关系。 +- **网状结构**:图,元素存在多对多的关系。 ## 物理结构:连续与离散 @@ -37,8 +37,8 @@ **所有数据结构都是基于数组、链表或二者的组合实现的**。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。 -- **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等; -- **基于链表可实现**:栈、队列、哈希表、树、堆、图等; +- **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等。 +- **基于链表可实现**:栈、队列、哈希表、树、堆、图等。 基于数组实现的数据结构也被称为“静态数据结构”,这意味着此类数据结构在初始化后长度不可变。相对应地,基于链表实现的数据结构被称为“动态数据结构”,这类数据结构在初始化后,仍可以在程序运行过程中对其长度进行调整。 diff --git a/docs/chapter_data_structure/number_encoding.md b/docs/chapter_data_structure/number_encoding.md index 877838eb9..360f5360c 100644 --- a/docs/chapter_data_structure/number_encoding.md +++ b/docs/chapter_data_structure/number_encoding.md @@ -90,9 +90,9 @@ $$ 实际上,这是因为浮点数 `float` 采用了不同的表示方式。根据 IEEE 754 标准,32-bit 长度的 `float` 由以下部分构成: -- 符号位 $\mathrm{S}$ :占 1 bit ; -- 指数位 $\mathrm{E}$ :占 8 bits ; -- 分数位 $\mathrm{N}$ :占 24 bits ,其中 23 位显式存储; +- 符号位 $\mathrm{S}$ :占 1 bit 。 +- 指数位 $\mathrm{E}$ :占 8 bits 。 +- 分数位 $\mathrm{N}$ :占 24 bits ,其中 23 位显式存储。 设 32-bit 二进制数的第 $i$ 位为 $b_i$ ,则 `float` 值的计算方法定义为: @@ -141,7 +141,7 @@ $$ 特别地,次正规数显著提升了浮点数的精度,这是因为: -- 最小正正规数为 $2^{-126} \approx 1.18 \times 10^{-38}$ ; -- 最小正次正规数为 $2^{-126} \times 2^{-23} \approx 1.4 \times 10^{-45}$ ; +- 最小正正规数为 $2^{-126} \approx 1.18 \times 10^{-38}$ 。 +- 最小正次正规数为 $2^{-126} \times 2^{-23} \approx 1.4 \times 10^{-45}$ 。 双精度 `double` 也采用类似 `float` 的表示方法,此处不再详述。 diff --git a/docs/chapter_divide_and_conquer/binary_search_recur.md b/docs/chapter_divide_and_conquer/binary_search_recur.md index d45a39e39..1a27e2ca1 100644 --- a/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -30,8 +30,8 @@ 从原问题 $f(0, n-1)$ 为起始点,二分查找的分治步骤为: -1. 计算搜索区间 $[i, j]$ 的中点 $m$ ,根据它排除一半搜索区间; -2. 递归求解规模减小一半的子问题,可能为 $f(i, m-1)$ 或 $f(m+1, j)$ ; +1. 计算搜索区间 $[i, j]$ 的中点 $m$ ,根据它排除一半搜索区间。 +2. 递归求解规模减小一半的子问题,可能为 $f(i, m-1)$ 或 $f(m+1, j)$ 。 3. 循环第 `1.` , `2.` 步,直至找到 `target` 或区间为空时返回。 下图展示了在数组中二分查找元素 $6$ 的分治过程。 diff --git a/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index 6044ae370..4fd1b6e13 100644 --- a/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -20,14 +20,14 @@ 根据定义,`preorder` 和 `inorder` 都可以被划分为三个部分: -- 前序遍历:`[ 根节点 | 左子树 | 右子树 ]` ,例如上图 `[ 3 | 9 | 2 1 7 ]` ; -- 中序遍历:`[ 左子树 | 根节点 | 右子树 ]` ,例如上图 `[ 9 | 3 | 1 2 7 ]` ; +- 前序遍历:`[ 根节点 | 左子树 | 右子树 ]` ,例如上图 `[ 3 | 9 | 2 1 7 ]` 。 +- 中序遍历:`[ 左子树 | 根节点 | 右子树 ]` ,例如上图 `[ 9 | 3 | 1 2 7 ]` 。 以上图数据为例,我们可以通过以下步骤得到上述的划分结果: -1. 前序遍历的首元素 3 是根节点的值; -2. 查找根节点 3 在 `inorder` 中的索引,利用该索引可将 `inorder` 划分为 `[ 9 | 3 | 1 2 7 ]` ; -3. 根据 `inorder` 划分结果,易得左子树和右子树的节点数量分别为 1 和 3 ,从而可将 `preorder` 划分为 `[ 3 | 9 | 2 1 7 ]` ; +1. 前序遍历的首元素 3 是根节点的值。 +2. 查找根节点 3 在 `inorder` 中的索引,利用该索引可将 `inorder` 划分为 `[ 9 | 3 | 1 2 7 ]` 。 +3. 根据 `inorder` 划分结果,易得左子树和右子树的节点数量分别为 1 和 3 ,从而可将 `preorder` 划分为 `[ 3 | 9 | 2 1 7 ]` 。 ![在前序和中序遍历中划分子树](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png) @@ -35,9 +35,9 @@ 根据以上划分方法,**我们已经得到根节点、左子树、右子树在 `preorder` 和 `inorder` 中的索引区间**。而为了描述这些索引区间,我们需要借助几个指针变量: -- 将当前树的根节点在 `preorder` 中的索引记为 $i$ ; -- 将当前树的根节点在 `inorder` 中的索引记为 $m$ ; -- 将当前树在 `inorder` 中的索引区间记为 $[l, r]$ ; +- 将当前树的根节点在 `preorder` 中的索引记为 $i$ 。 +- 将当前树的根节点在 `inorder` 中的索引记为 $m$ 。 +- 将当前树在 `inorder` 中的索引区间记为 $[l, r]$ 。 如下表所示,通过以上变量即可表示根节点在 `preorder` 中的索引,以及子树在 `inorder` 中的索引区间。 diff --git a/docs/chapter_divide_and_conquer/divide_and_conquer.md b/docs/chapter_divide_and_conquer/divide_and_conquer.md index 28795b4b5..f2a194250 100644 --- a/docs/chapter_divide_and_conquer/divide_and_conquer.md +++ b/docs/chapter_divide_and_conquer/divide_and_conquer.md @@ -2,8 +2,8 @@ 「分治 Divide and Conquer」,全称分而治之,是一种非常重要且常见的算法策略。分治通常基于递归实现,包括“分”和“治”两步: -1. **分(划分阶段)**:递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止; -2. **治(合并阶段)**:从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题的解; +1. **分(划分阶段)**:递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止。 +2. **治(合并阶段)**:从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题的解。 已介绍过的「归并排序」是分治策略的典型应用之一,它的分治策略为: @@ -22,9 +22,9 @@ 显然归并排序,满足以上三条判断依据: -1. 递归地将数组(原问题)划分为两个子数组(子问题); -2. 每个子数组都可以独立地进行排序(子问题可以独立进行求解); -3. 两个有序子数组(子问题的解)可以被合并为一个有序数组(原问题的解); +1. 递归地将数组(原问题)划分为两个子数组(子问题)。 +2. 每个子数组都可以独立地进行排序(子问题可以独立进行求解)。 +3. 两个有序子数组(子问题的解)可以被合并为一个有序数组(原问题的解)。 ## 通过分治提升效率 diff --git a/docs/chapter_divide_and_conquer/hanota_problem.md b/docs/chapter_divide_and_conquer/hanota_problem.md index ec3f50a68..ceac40105 100644 --- a/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/docs/chapter_divide_and_conquer/hanota_problem.md @@ -6,9 +6,9 @@ 给定三根柱子,记为 `A` , `B` , `C` 。起始状态下,柱子 `A` 上套着 $n$ 个圆盘,它们从上到下按照从小到大的顺序排列。我们的任务是要把这 $n$ 个圆盘移到柱子 `C` 上,并保持它们的原有顺序不变。在移动圆盘的过程中,需要遵守以下规则: - 1. 圆盘只能从一个柱子顶部拿出,从另一个柱子顶部放入; - 2. 每次只能移动一个圆盘; - 3. 小圆盘必须时刻位于大圆盘之上; + 1. 圆盘只能从一个柱子顶部拿出,从另一个柱子顶部放入。 + 2. 每次只能移动一个圆盘。 + 3. 小圆盘必须时刻位于大圆盘之上。 ![汉诺塔问题示例](hanota_problem.assets/hanota_example.png) @@ -26,9 +26,9 @@ 对于问题 $f(2)$ ,即当有两个圆盘时,**由于要时刻满足小圆盘在大圆盘之上,因此需要借助 `B` 来完成移动**,包括三步: -1. 先将上面的小圆盘从 `A` 移至 `B` ; -2. 再将大圆盘从 `A` 移至 `C` ; -3. 最后将小圆盘从 `B` 移至 `C` ; +1. 先将上面的小圆盘从 `A` 移至 `B` 。 +2. 再将大圆盘从 `A` 移至 `C` 。 +3. 最后将小圆盘从 `B` 移至 `C` 。 解决问题 $f(2)$ 的过程可总结为:**将两个圆盘借助 `B` 从 `A` 移至 `C`** 。其中,`C` 称为目标柱、`B` 称为缓冲柱。 @@ -48,9 +48,9 @@ 对于问题 $f(3)$ ,即当有三个圆盘时,情况变得稍微复杂了一些。由于已知 $f(1)$ 和 $f(2)$ 的解,因此可从分治角度思考,**将 `A` 顶部的两个圆盘看做一个整体**,执行以下步骤: -1. 令 `B` 为目标柱、`C` 为缓冲柱,将两个圆盘从 `A` 移动至 `B` ; -2. 将 `A` 中剩余的一个圆盘从 `A` 直接移动至 `C` ; -3. 令 `C` 为目标柱、`A` 为缓冲柱,将两个圆盘从 `B` 移动至 `C` ; +1. 令 `B` 为目标柱、`C` 为缓冲柱,将两个圆盘从 `A` 移动至 `B` 。 +2. 将 `A` 中剩余的一个圆盘从 `A` 直接移动至 `C` 。 +3. 令 `C` 为目标柱、`A` 为缓冲柱,将两个圆盘从 `B` 移动至 `C` 。 这样三个圆盘就被顺利地从 `A` 移动至 `C` 了。 @@ -70,9 +70,9 @@ 至此,我们可总结出汉诺塔问题的分治策略:将原问题 $f(n)$ 划分为两个子问题 $f(n-1)$ 和一个子问题 $f(1)$ 。子问题的解决顺序为: -1. 将 $n-1$ 个圆盘借助 `C` 从 `A` 移至 `B` ; -2. 将剩余 $1$ 个圆盘从 `A` 直接移至 `C` ; -3. 将 $n-1$ 个圆盘借助 `A` 从 `B` 移至 `C` ; +1. 将 $n-1$ 个圆盘借助 `C` 从 `A` 移至 `B` 。 +2. 将剩余 $1$ 个圆盘从 `A` 直接移至 `C` 。 +3. 将 $n-1$ 个圆盘借助 `A` 从 `B` 移至 `C` 。 对于这两个子问题 $f(n-1)$ ,**可以通过相同的方式进行递归划分**,直至达到最小子问题 $f(1)$ 。而 $f(1)$ 的解是已知的,只需一次移动操作即可。 diff --git a/docs/chapter_dynamic_programming/dp_problem_features.md b/docs/chapter_dynamic_programming/dp_problem_features.md index f40c4de6d..ac19dada1 100644 --- a/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/docs/chapter_dynamic_programming/dp_problem_features.md @@ -192,8 +192,8 @@ $$ 为此,我们需要扩展状态定义:**状态 $[i, j]$ 表示处在第 $i$ 阶、并且上一轮跳了 $j$ 阶**,其中 $j \in \{1, 2\}$ 。此状态定义有效地区分了上一轮跳了 $1$ 阶还是 $2$ 阶,我们可以据此来决定下一步该怎么跳: -- 当 $j$ 等于 $1$ ,即上一轮跳了 $1$ 阶时,这一轮只能选择跳 $2$ 阶; -- 当 $j$ 等于 $2$ ,即上一轮跳了 $2$ 阶时,这一轮可选择跳 $1$ 阶或跳 $2$ 阶; +- 当 $j$ 等于 $1$ ,即上一轮跳了 $1$ 阶时,这一轮只能选择跳 $2$ 阶。 +- 当 $j$ 等于 $2$ ,即上一轮跳了 $2$ 阶时,这一轮可选择跳 $1$ 阶或跳 $2$ 阶。 在该定义下,$dp[i, j]$ 表示状态 $[i, j]$ 对应的方案数。在该定义下的状态转移方程为: diff --git a/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/docs/chapter_dynamic_programming/dp_solution_pipeline.md index cdfac9bcf..e71df5044 100644 --- a/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -93,10 +93,10 @@ $$ 从状态 $[i, j]$ 开始搜索,不断分解为更小的状态 $[i-1, j]$ 和 $[i, j-1]$ ,包括以下递归要素: -- **递归参数**:状态 $[i, j]$ ; -- **返回值**:从 $[0, 0]$ 到 $[i, j]$ 的最小路径和 $dp[i, j]$ ; -- **终止条件**:当 $i = 0$ 且 $j = 0$ 时,返回代价 $grid[0, 0]$ ; -- **剪枝**:当 $i < 0$ 时或 $j < 0$ 时索引越界,此时返回代价 $+\infty$ ,代表不可行; +- **递归参数**:状态 $[i, j]$ 。 +- **返回值**:从 $[0, 0]$ 到 $[i, j]$ 的最小路径和 $dp[i, j]$ 。 +- **终止条件**:当 $i = 0$ 且 $j = 0$ 时,返回代价 $grid[0, 0]$ 。 +- **剪枝**:当 $i < 0$ 时或 $j < 0$ 时索引越界,此时返回代价 $+\infty$ ,代表不可行。 === "Java" diff --git a/docs/chapter_dynamic_programming/edit_distance_problem.md b/docs/chapter_dynamic_programming/edit_distance_problem.md index df0ff96a6..ef5768768 100644 --- a/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -26,8 +26,8 @@ 我们希望在编辑操作的过程中,问题的规模逐渐缩小,这样才能构建子问题。设字符串 $s$ 和 $t$ 的长度分别为 $n$ 和 $m$ ,我们先考虑两字符串尾部的字符 $s[n-1]$ 和 $t[m-1]$ : -- 若 $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[n-2]$ 和 $t[m-2]$ 。 +- 若 $s[n-1]$ 和 $t[m-1]$ 不同,我们需要对 $s$ 进行一次编辑(插入、删除、替换),使得两字符串尾部的字符相同,从而可以跳过它们,考虑规模更小的问题。 也就是说,我们在字符串 $s$ 中进行的每一轮决策(编辑操作),都会使得 $s$ 和 $t$ 中剩余的待匹配字符发生变化。因此,状态为当前在 $s$ , $t$ 中考虑的第 $i$ , $j$ 个字符,记为 $[i, j]$ 。 @@ -39,9 +39,9 @@ 考虑子问题 $dp[i, j]$ ,其对应的两个字符串的尾部字符为 $s[i-1]$ 和 $t[j-1]$ ,可根据不同编辑操作分为三种情况: -1. 在 $s[i-1]$ 之后添加 $t[j-1]$ ,则剩余子问题 $dp[i, j-1]$ ; -2. 删除 $s[i-1]$ ,则剩余子问题 $dp[i-1, j]$ ; -3. 将 $s[i-1]$ 替换为 $t[j-1]$ ,则剩余子问题 $dp[i-1, j-1]$ ; +1. 在 $s[i-1]$ 之后添加 $t[j-1]$ ,则剩余子问题 $dp[i, j-1]$ 。 +2. 删除 $s[i-1]$ ,则剩余子问题 $dp[i-1, j]$ 。 +3. 将 $s[i-1]$ 替换为 $t[j-1]$ ,则剩余子问题 $dp[i-1, j-1]$ 。 ![编辑距离的状态转移](edit_distance_problem.assets/edit_distance_state_transfer.png) diff --git a/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index 7513e451d..e3c73ca81 100644 --- a/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -231,8 +231,8 @@ $$ 为了提升算法效率,**我们希望所有的重叠子问题都只被计算一次**。为此,我们声明一个数组 `mem` 来记录每个子问题的解,并在搜索过程中这样做: -1. 当首次计算 $dp[i]$ 时,我们将其记录至 `mem[i]` ,以便之后使用; -2. 当再次需要计算 $dp[i]$ 时,我们便可直接从 `mem[i]` 中获取结果,从而将重叠子问题剪枝; +1. 当首次计算 $dp[i]$ 时,我们将其记录至 `mem[i]` ,以便之后使用。 +2. 当再次需要计算 $dp[i]$ 时,我们便可直接从 `mem[i]` 中获取结果,从而将重叠子问题剪枝。 === "Java" @@ -404,9 +404,9 @@ $$ 总结以上,动态规划的常用术语包括: -- 将数组 `dp` 称为「$dp$ 表」,$dp[i]$ 表示状态 $i$ 对应子问题的解; -- 将最小子问题对应的状态(即第 $1$ , $2$ 阶楼梯)称为「初始状态」; -- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」; +- 将数组 `dp` 称为「$dp$ 表」,$dp[i]$ 表示状态 $i$ 对应子问题的解。 +- 将最小子问题对应的状态(即第 $1$ , $2$ 阶楼梯)称为「初始状态」。 +- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」。 ![爬楼梯的动态规划过程](intro_to_dynamic_programming.assets/climbing_stairs_dp.png) diff --git a/docs/chapter_dynamic_programming/knapsack_problem.md b/docs/chapter_dynamic_programming/knapsack_problem.md index 5a24cfeb7..d74b2d409 100644 --- a/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/docs/chapter_dynamic_programming/knapsack_problem.md @@ -28,8 +28,8 @@ 当我们做出物品 $i$ 的决策后,剩余的是前 $i-1$ 个物品的决策。因此,状态转移分为两种情况: -- **不放入物品 $i$** :背包容量不变,状态转移至 $[i-1, c]$ ; -- **放入物品 $i$** :背包容量减小 $wgt[i-1]$ ,价值增加 $val[i-1]$ ,状态转移至 $[i-1, c-wgt[i-1]]$ ; +- **不放入物品 $i$** :背包容量不变,状态转移至 $[i-1, c]$ 。 +- **放入物品 $i$** :背包容量减小 $wgt[i-1]$ ,价值增加 $val[i-1]$ ,状态转移至 $[i-1, c-wgt[i-1]]$ 。 上述的状态转移向我们揭示了本题的最优子结构:**最大价值 $dp[i, c]$ 等于不放入物品 $i$ 和放入物品 $i$ 两种方案中的价值更大的那一个**。由此可推出状态转移方程: @@ -51,10 +51,10 @@ $$ 搜索代码包含以下要素: -- **递归参数**:状态 $[i, c]$ ; -- **返回值**:子问题的解 $dp[i, c]$ ; -- **终止条件**:当物品编号越界 $i = 0$ 或背包剩余容量为 $0$ 时,终止递归并返回价值 $0$ ; -- **剪枝**:若当前物品重量超出背包剩余容量,则只能不放入背包; +- **递归参数**:状态 $[i, c]$ 。 +- **返回值**:子问题的解 $dp[i, c]$ 。 +- **终止条件**:当物品编号越界 $i = 0$ 或背包剩余容量为 $0$ 时,终止递归并返回价值 $0$ 。 +- **剪枝**:若当前物品重量超出背包剩余容量,则只能不放入背包。 === "Java" diff --git a/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index 4812e8a0d..e2da6919b 100644 --- a/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -12,13 +12,13 @@ 完全背包和 0-1 背包问题非常相似,**区别仅在于不限制物品的选择次数**。 -- 在 0-1 背包中,每个物品只有一个,因此将物品 $i$ 放入背包后,只能从前 $i-1$ 个物品中选择; -- 在完全背包中,每个物品有无数个,因此将物品 $i$ 放入背包后,**仍可以从前 $i$ 个物品中选择**; +- 在 0-1 背包中,每个物品只有一个,因此将物品 $i$ 放入背包后,只能从前 $i-1$ 个物品中选择。 +- 在完全背包中,每个物品有无数个,因此将物品 $i$ 放入背包后,**仍可以从前 $i$ 个物品中选择**。 这就导致了状态转移的变化,对于状态 $[i, c]$ 有: -- **不放入物品 $i$** :与 0-1 背包相同,转移至 $[i-1, c]$ ; -- **放入物品 $i$** :与 0-1 背包不同,转移至 $[i, c-wgt[i-1]]$ ; +- **不放入物品 $i$** :与 0-1 背包相同,转移至 $[i-1, c]$ 。 +- **放入物品 $i$** :与 0-1 背包不同,转移至 $[i, c-wgt[i-1]]$ 。 从而状态转移方程变为: @@ -200,9 +200,9 @@ $$ **零钱兑换可以看作是完全背包的一种特殊情况**,两者具有以下联系与不同点: -- 两道题可以相互转换,“物品”对应于“硬币”、“物品重量”对应于“硬币面值”、“背包容量”对应于“目标金额”; -- 优化目标相反,背包问题是要最大化物品价值,零钱兑换问题是要最小化硬币数量; -- 背包问题是求“不超过”背包容量下的解,零钱兑换是求“恰好”凑到目标金额的解; +- 两道题可以相互转换,“物品”对应于“硬币”、“物品重量”对应于“硬币面值”、“背包容量”对应于“目标金额”。 +- 优化目标相反,背包问题是要最大化物品价值,零钱兑换问题是要最小化硬币数量。 +- 背包问题是求“不超过”背包容量下的解,零钱兑换是求“恰好”凑到目标金额的解。 **第一步:思考每轮的决策,定义状态,从而得到 $dp$ 表** @@ -214,8 +214,8 @@ $$ 与完全背包的状态转移方程基本相同,不同点在于: -- 本题要求最小值,因此需将运算符 $\max()$ 更改为 $\min()$ ; -- 优化主体是硬币数量而非商品价值,因此在选中硬币时执行 $+1$ 即可; +- 本题要求最小值,因此需将运算符 $\max()$ 更改为 $\min()$ 。 +- 优化主体是硬币数量而非商品价值,因此在选中硬币时执行 $+1$ 即可。 $$ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) diff --git a/docs/chapter_graph/graph.md b/docs/chapter_graph/graph.md index 3cd0759f6..66176c8f9 100644 --- a/docs/chapter_graph/graph.md +++ b/docs/chapter_graph/graph.md @@ -18,15 +18,15 @@ $$ 根据边是否具有方向,可分为「无向图 Undirected Graph」和「有向图 Directed Graph」。 -- 在无向图中,边表示两顶点之间的“双向”连接关系,例如微信或 QQ 中的“好友关系”; -- 在有向图中,边具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系; +- 在无向图中,边表示两顶点之间的“双向”连接关系,例如微信或 QQ 中的“好友关系”。 +- 在有向图中,边具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系。 ![有向图与无向图](graph.assets/directed_graph.png) 根据所有顶点是否连通,可分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。 -- 对于连通图,从某个顶点出发,可以到达其余任意顶点; -- 对于非连通图,从某个顶点出发,至少有一个顶点无法到达; +- 对于连通图,从某个顶点出发,可以到达其余任意顶点。 +- 对于非连通图,从某个顶点出发,至少有一个顶点无法到达。 ![连通图与非连通图](graph.assets/connected_graph.png) diff --git a/docs/chapter_graph/graph_traversal.md b/docs/chapter_graph/graph_traversal.md index a02dfabf1..4358005cd 100644 --- a/docs/chapter_graph/graph_traversal.md +++ b/docs/chapter_graph/graph_traversal.md @@ -18,9 +18,9 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质,这与 BFS 的“由近及远”的思想异曲同工。 -1. 将遍历起始顶点 `startVet` 加入队列,并开启循环; -2. 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部; -3. 循环步骤 `2.` ,直到所有顶点被访问完成后结束; +1. 将遍历起始顶点 `startVet` 加入队列,并开启循环。 +2. 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部。 +3. 循环步骤 `2.` ,直到所有顶点被访问完成后结束。 为了防止重复遍历顶点,我们需要借助一个哈希表 `visited` 来记录哪些节点已被访问。 @@ -235,8 +235,8 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 深度优先遍历的算法流程如下图所示,其中: -- **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点; -- **曲虚线代表向上回溯**,表示此递归方法已经返回,回溯到了开启此递归方法的位置; +- **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点。 +- **曲虚线代表向上回溯**,表示此递归方法已经返回,回溯到了开启此递归方法的位置。 为了加深理解,建议将图示与代码结合起来,在脑中(或者用笔画下来)模拟整个 DFS 过程,包括每个递归方法何时开启、何时返回。 diff --git a/docs/chapter_greedy/fractional_knapsack_problem.md b/docs/chapter_greedy/fractional_knapsack_problem.md index effc33d0b..2d6b732e9 100644 --- a/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/docs/chapter_greedy/fractional_knapsack_problem.md @@ -12,8 +12,8 @@ 不同点在于,本题允许只选择物品的一部分,**这意味着可以对物品任意地进行切分,并按照重量比例来计算物品价值**,因此有: -1. 对于物品 $i$ ,它在单位重量下的价值为 $val[i-1] / wgt[i-1]$ ,简称为单位价值; -2. 假设放入一部分物品 $i$ ,重量为 $w$ ,则背包增加的价值为 $w \times val[i-1] / wgt[i-1]$ ; +1. 对于物品 $i$ ,它在单位重量下的价值为 $val[i-1] / wgt[i-1]$ ,简称为单位价值。 +2. 假设放入一部分物品 $i$ ,重量为 $w$ ,则背包增加的价值为 $w \times val[i-1] / wgt[i-1]$ 。 ![物品在单位重量下的价值](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png) diff --git a/docs/chapter_greedy/max_capacity_problem.md b/docs/chapter_greedy/max_capacity_problem.md index 700d529d7..7850f00d1 100644 --- a/docs/chapter_greedy/max_capacity_problem.md +++ b/docs/chapter_greedy/max_capacity_problem.md @@ -28,8 +28,8 @@ $$ 我们发现,**如果此时将长板 $j$ 向短板 $i$ 靠近,则容量一定变小**。这是因为在移动长板 $j$ 后: -- 宽度 $j-i$ 肯定变小; -- 高度由短板决定,因此高度只可能不变( $i$ 仍为短板)或变小(移动后的 $j$ 成为短板); +- 宽度 $j-i$ 肯定变小。 +- 高度由短板决定,因此高度只可能不变( $i$ 仍为短板)或变小(移动后的 $j$ 成为短板)。 ![向内移动长板后的状态](max_capacity_problem.assets/max_capacity_moving_long_board.png) diff --git a/docs/chapter_greedy/max_product_cutting_problem.md b/docs/chapter_greedy/max_product_cutting_problem.md index ba8aa850b..97a1609b8 100644 --- a/docs/chapter_greedy/max_product_cutting_problem.md +++ b/docs/chapter_greedy/max_product_cutting_problem.md @@ -133,7 +133,7 @@ $$ **时间复杂度取决于编程语言的幂运算的实现方法**。以 Python 为例,常用的幂计算函数有三种: -- 运算符 `**` 和函数 `pow()` 的时间复杂度均为 $O(\log a)$ ; +- 运算符 `**` 和函数 `pow()` 的时间复杂度均为 $O(\log a)$ 。 - 函数 `math.pow()` 内部调用 C 语言库的 `pow()` 函数,其执行浮点取幂,时间复杂度为 $O(1)$ 。 变量 $a$ , $b$ 使用常数大小的额外空间,**因此空间复杂度为 $O(1)$** 。 diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index b1ffa9951..8fa2cef15 100755 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -8,9 +8,9 @@ 除哈希表外,我们还可以使用数组或链表实现查询功能。若将学生数据看作数组(链表)元素,则有: -- **添加元素**:仅需将元素添加至数组(链表)的尾部即可,使用 $O(1)$ 时间; -- **查询元素**:由于数组(链表)是乱序的,因此需要遍历其中的所有元素,使用 $O(n)$ 时间; -- **删除元素**:需要先查询到元素,再从数组中删除,使用 $O(n)$ 时间; +- **添加元素**:仅需将元素添加至数组(链表)的尾部即可,使用 $O(1)$ 时间。 +- **查询元素**:由于数组(链表)是乱序的,因此需要遍历其中的所有元素,使用 $O(n)$ 时间。 +- **删除元素**:需要先查询到元素,再从数组中删除,使用 $O(n)$ 时间。