diff --git a/chapter_appendix/contribution.md b/chapter_appendix/contribution.md index 4f49c17bb..adec4cd0d 100644 --- a/chapter_appendix/contribution.md +++ b/chapter_appendix/contribution.md @@ -2,7 +2,7 @@ comments: true --- -# 16.2.   一起参与创作 +# 16.2   一起参与创作 由于作者能力有限,书中难免存在一些遗漏和错误,请您谅解。如果您发现了笔误、失效链接、内容缺失、文字歧义、解释不清晰或行文结构不合理等问题,请协助我们进行修正,以帮助其他读者获得更优质的学习资源。 @@ -14,7 +14,7 @@ comments: true 然而在本开源书中,内容更迭的时间被缩短至数日甚至几个小时。 -## 16.2.1.   内容微调 +## 16.2.1   内容微调 在每个页面的右上角有一个「编辑」图标,您可以按照以下步骤修改文本或代码: @@ -28,7 +28,7 @@ comments: true 图片无法直接修改,需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述问题,我们会尽快重新绘制并替换图片。 -## 16.2.2.   内容创作 +## 16.2.2   内容创作 如果您有兴趣参与此开源项目,包括将代码翻译成其他编程语言、扩展文章内容等,那么需要实施 Pull Request 工作流程: @@ -38,7 +38,7 @@ comments: true 4. 将本地所做更改 Commit ,然后 Push 至远程仓库。 5. 刷新仓库网页,点击“Create pull request”按钮即可发起拉取请求。 -## 16.2.3.   Docker 部署 +## 16.2.3   Docker 部署 执行以下 Docker 脚本,稍等片刻,即可在网页 `http://localhost:8000` 访问本项目。 diff --git a/chapter_appendix/index.md b/chapter_appendix/index.md index f1a482893..f551a5c08 100644 --- a/chapter_appendix/index.md +++ b/chapter_appendix/index.md @@ -3,7 +3,7 @@ comments: true icon: material/help-circle-outline --- -# 16.   附录 +# 第 16 章   附录
diff --git a/chapter_appendix/installation.md b/chapter_appendix/installation.md index 3a70b5670..14614a2a3 100644 --- a/chapter_appendix/installation.md +++ b/chapter_appendix/installation.md @@ -2,58 +2,58 @@ comments: true --- -# 16.1.   编程环境安装 +# 16.1   编程环境安装 -## 16.1.1.   VSCode +## 16.1.1   VSCode 本书推荐使用开源轻量的 VSCode 作为本地 IDE ,下载并安装 [VSCode](https://code.visualstudio.com/) 。 -## 16.1.2.   Java 环境 +## 16.1.2   Java 环境 1. 下载并安装 [OpenJDK](https://jdk.java.net/18/)(版本需满足 > JDK 9)。 2. 在 VSCode 的插件市场中搜索 `java` ,安装 Extension Pack for Java 。 -## 16.1.3.   C/C++ 环境 +## 16.1.3   C/C++ 环境 1. Windows 系统需要安装 [MinGW](https://sourceforge.net/projects/mingw-w64/files/)([配置教程](https://blog.csdn.net/qq_33698226/article/details/129031241)),MacOS 自带 Clang 无需安装。 2. 在 VSCode 的插件市场中搜索 `c++` ,安装 C/C++ Extension Pack 。 3. (可选)打开 Settings 页面,搜索 `Clang_format_fallback Style` 代码格式化选项,设置为 `{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }` 。 -## 16.1.4.   Python 环境 +## 16.1.4   Python 环境 1. 下载并安装 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) 。 2. 在 VSCode 的插件市场中搜索 `python` ,安装 Python Extension Pack 。 3. (可选)在命令行输入 `pip install black` ,安装代码格式化工具。 -## 16.1.5.   Go 环境 +## 16.1.5   Go 环境 1. 下载并安装 [go](https://go.dev/dl/) 。 2. 在 VSCode 的插件市场中搜索 `go` ,安装 Go 。 3. 快捷键 `Ctrl + Shift + P` 呼出命令栏,输入 go ,选择 `Go: Install/Update Tools` ,全部勾选并安装即可。 -## 16.1.6.   JavaScript 环境 +## 16.1.6   JavaScript 环境 1. 下载并安装 [node.js](https://nodejs.org/en/) 。 2. 在 VSCode 的插件市场中搜索 `javascript` ,安装 JavaScript (ES6) code snippets 。 3. (可选)在 VSCode 的插件市场中搜索 `Prettier` ,安装代码格式化工具。 -## 16.1.7.   C# 环境 +## 16.1.7   C# 环境 1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) 。 2. 在 VSCode 的插件市场中搜索 `C# Dev Kit` ,安装 C# Dev Kit ([配置教程](https://code.visualstudio.com/docs/csharp/get-started))。 3. 也可使用 Visual Studio([安装教程](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022))。 -## 16.1.8.   Swift 环境 +## 16.1.8   Swift 环境 1. 下载并安装 [Swift](https://www.swift.org/download/)。 2. 在 VSCode 的插件市场中搜索 `swift` ,安装 [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)。 -## 16.1.9.   Dart 环境 +## 16.1.9   Dart 环境 1. 下载并安装 [Dart](https://dart.dev/get-dart) 。 2. 在 VSCode 的插件市场中搜索 `dart` ,安装 [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code) 。 -## 16.1.10.   Rust 环境 +## 16.1.10   Rust 环境 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/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md index 5063d8fb6..ca7605eff 100755 --- a/chapter_array_and_linkedlist/array.md +++ b/chapter_array_and_linkedlist/array.md @@ -2,7 +2,7 @@ comments: true --- -# 4.1.   数组 +# 4.1   数组 「数组 Array」是一种线性数据结构,其将相同类型元素存储在连续的内存空间中。我们将某个元素在数组中的位置称为该元素的「索引 Index」。 @@ -10,7 +10,7 @@ comments: true

图:数组定义与存储方式

-## 4.1.1.   数组常用操作 +## 4.1.1   数组常用操作 ### 初始化数组 @@ -1204,7 +1204,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` -## 4.1.2.   数组优点与局限性 +## 4.1.2   数组优点与局限性 数组存储在连续的内存空间内,且元素类型相同。这包含丰富的先验信息,系统可以利用这些信息来优化操作和运行效率,包括: @@ -1218,7 +1218,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex - **长度不可变**: 数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。 - **空间浪费**: 如果数组分配的大小超过了实际所需,那么多余的空间就被浪费了。 -## 4.1.3.   数组典型应用 +## 4.1.3   数组典型应用 数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构,主要包括: diff --git a/chapter_array_and_linkedlist/index.md b/chapter_array_and_linkedlist/index.md index 802fab542..6bcc368bb 100644 --- a/chapter_array_and_linkedlist/index.md +++ b/chapter_array_and_linkedlist/index.md @@ -3,7 +3,7 @@ comments: true icon: material/view-list-outline --- -# 4.   数组与链表 +# 第 4 章   数组与链表
diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index 599071800..b947325b0 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -2,7 +2,7 @@ comments: true --- -# 4.2.   链表 +# 4.2   链表 内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此时链表的灵活性优势就体现出来了。 @@ -188,7 +188,7 @@ comments: true } ``` -## 4.2.1.   链表常用操作 +## 4.2.1   链表常用操作 ### 初始化链表 @@ -1098,7 +1098,7 @@ comments: true } ``` -## 4.2.2.   数组 VS 链表 +## 4.2.2   数组 VS 链表 下表总结对比了数组和链表的各项特点与操作效率。由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。 @@ -1118,7 +1118,7 @@ comments: true
-## 4.2.3.   常见链表类型 +## 4.2.3   常见链表类型 **单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 $\text{None}$ 。 @@ -1327,7 +1327,7 @@ comments: true

图:常见链表种类

-## 4.2.4.   链表典型应用 +## 4.2.4   链表典型应用 单向链表通常用于实现栈、队列、散列表和图等数据结构。 diff --git a/chapter_array_and_linkedlist/list.md b/chapter_array_and_linkedlist/list.md index e8400649e..cab351b0f 100755 --- a/chapter_array_and_linkedlist/list.md +++ b/chapter_array_and_linkedlist/list.md @@ -2,13 +2,13 @@ comments: true --- -# 4.3.   列表 +# 4.3   列表 **数组长度不可变导致实用性降低**。在实际中,我们可能事先无法确定需要存储多少数据,这使数组长度的选择变得困难。若长度过小,需要在持续添加数据时频繁扩容数组;若长度过大,则会造成内存空间的浪费。 为解决此问题,出现了一种被称为「动态数组 Dynamic Array」的数据结构,即长度可变的数组,也常被称为「列表 List」。列表基于数组实现,继承了数组的优点,并且可以在程序运行过程中动态扩容。我们可以在列表中自由地添加元素,而无需担心超过容量限制。 -## 4.3.1.   列表常用操作 +## 4.3.1   列表常用操作 ### 初始化列表 @@ -854,7 +854,7 @@ comments: true list.sort(); // 排序后,列表元素从小到大排列 ``` -## 4.3.2.   列表实现 +## 4.3.2   列表实现 许多编程语言都提供内置的列表,例如 Java, C++, Python 等。它们的实现比较复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。 diff --git a/chapter_array_and_linkedlist/summary.md b/chapter_array_and_linkedlist/summary.md index 339f2a6e7..3c2588a03 100644 --- a/chapter_array_and_linkedlist/summary.md +++ b/chapter_array_and_linkedlist/summary.md @@ -2,14 +2,14 @@ comments: true --- -# 4.4.   小结 +# 4.4   小结 - 数组和链表是两种基本的数据结构,分别代表数据在计算机内存中的两种存储方式:连续空间存储和离散空间存储。两者的特点呈现出互补的特性。 - 数组支持随机访问、占用内存较少;但插入和删除元素效率低,且初始化后长度不可变。 - 链表通过更改引用(指针)实现高效的节点插入与删除,且可以灵活调整长度;但节点访问效率低、占用内存较多。常见的链表类型包括单向链表、循环链表、双向链表。 - 动态数组,又称列表,是基于数组实现的一种数据结构。它保留了数组的优势,同时可以灵活调整长度。列表的出现极大地提高了数组的易用性,但可能导致部分内存空间浪费。 -## 4.4.1.   Q & A +## 4.4.1   Q & A !!! question "数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?" diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md index 66c2a7d2a..81cd260f2 100644 --- a/chapter_backtracking/backtracking_algorithm.md +++ b/chapter_backtracking/backtracking_algorithm.md @@ -2,7 +2,7 @@ comments: true --- -# 13.1.   回溯算法 +# 13.1   回溯算法 「回溯算法 Backtracking Algorithm」是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。 @@ -199,7 +199,7 @@ comments: true

图:在前序遍历中搜索节点

-## 13.1.1.   尝试与回退 +## 13.1.1   尝试与回退 **之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略**。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。 @@ -503,7 +503,7 @@ comments: true

图:尝试与回退

-## 13.1.2.   剪枝 +## 13.1.2   剪枝 复杂的回溯问题通常包含一个或多个约束条件,**约束条件通常可用于“剪枝”**。 @@ -801,7 +801,7 @@ comments: true

图:根据约束条件剪枝

-## 13.1.3.   框架代码 +## 13.1.3   框架代码 接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。 @@ -1663,7 +1663,7 @@ comments: true 相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰嗦,但通用性更好。实际上,**许多回溯问题都可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法即可。 -## 13.1.4.   常用术语 +## 13.1.4   常用术语 为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。 @@ -1684,7 +1684,7 @@ comments: true 问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心等算法中都有涉及。 -## 13.1.5.   优势与局限性 +## 13.1.5   优势与局限性 回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优势在于它能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率。 @@ -1698,7 +1698,7 @@ comments: true - **剪枝**:避免搜索那些肯定不会产生解的路径,从而节省时间和空间。 - **启发式搜索**:在搜索过程中引入一些策略或者估计值,从而优先搜索最有可能产生有效解的路径。 -## 13.1.6.   回溯典型例题 +## 13.1.6   回溯典型例题 回溯算法可用于解决许多搜索问题、约束满足问题和组合优化问题。 diff --git a/chapter_backtracking/index.md b/chapter_backtracking/index.md index 522949210..5dabb0892 100644 --- a/chapter_backtracking/index.md +++ b/chapter_backtracking/index.md @@ -3,7 +3,7 @@ comments: true icon: material/map-marker-path --- -# 13.   回溯 +# 第 13 章   回溯
diff --git a/chapter_backtracking/n_queens_problem.md b/chapter_backtracking/n_queens_problem.md index 5e00bc18a..cb2805ace 100644 --- a/chapter_backtracking/n_queens_problem.md +++ b/chapter_backtracking/n_queens_problem.md @@ -2,7 +2,7 @@ comments: true --- -# 13.4.   N 皇后问题 +# 13.4   N 皇后问题 !!! question diff --git a/chapter_backtracking/permutations_problem.md b/chapter_backtracking/permutations_problem.md index 910ab33d9..52ed0519c 100644 --- a/chapter_backtracking/permutations_problem.md +++ b/chapter_backtracking/permutations_problem.md @@ -2,7 +2,7 @@ comments: true --- -# 13.2.   全排列问题 +# 13.2   全排列问题 全排列问题是回溯算法的一个典型应用。它的定义是在给定一个集合(如一个数组或字符串)的情况下,找出这个集合中元素的所有可能的排列。 @@ -20,7 +20,7 @@ comments: true
-## 13.2.1.   无相等元素的情况 +## 13.2.1   无相等元素的情况 !!! question @@ -473,7 +473,7 @@ comments: true } ``` -## 13.2.2.   考虑相等元素的情况 +## 13.2.2   考虑相等元素的情况 !!! question diff --git a/chapter_backtracking/subset_sum_problem.md b/chapter_backtracking/subset_sum_problem.md index fe6385e99..57c1b6d70 100644 --- a/chapter_backtracking/subset_sum_problem.md +++ b/chapter_backtracking/subset_sum_problem.md @@ -2,9 +2,9 @@ comments: true --- -# 13.3.   子集和问题 +# 13.3   子集和问题 -## 13.3.1.   无重复元素的情况 +## 13.3.1   无重复元素的情况 !!! question @@ -918,7 +918,7 @@ comments: true

图:子集和 I 回溯过程

-## 13.3.2.   考虑重复元素的情况 +## 13.3.2   考虑重复元素的情况 !!! question diff --git a/chapter_backtracking/summary.md b/chapter_backtracking/summary.md index f0b774164..3ee7b0e69 100644 --- a/chapter_backtracking/summary.md +++ b/chapter_backtracking/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 13.5.   小结 +# 13.5   小结 - 回溯算法本质是穷举法,通过对解空间进行深度优先遍历来寻找符合条件的解。在搜索过程中,遇到满足条件的解则记录,直至找到所有解或遍历完成后结束。 - 回溯算法的搜索过程包括尝试与回退两个部分。它通过深度优先搜索来尝试各种选择,当遇到不满足约束条件的情况时,则撤销上一步的选择,退回到之前的状态,并继续尝试其他选择。尝试与回退是两个方向相反的操作。 diff --git a/chapter_computational_complexity/index.md b/chapter_computational_complexity/index.md index c9f093318..b6673a306 100644 --- a/chapter_computational_complexity/index.md +++ b/chapter_computational_complexity/index.md @@ -3,7 +3,7 @@ comments: true icon: material/timer-sand --- -# 2.   复杂度 +# 第 2 章   复杂度
diff --git a/chapter_computational_complexity/performance_evaluation.md b/chapter_computational_complexity/performance_evaluation.md index b48d5b309..58ab418ba 100644 --- a/chapter_computational_complexity/performance_evaluation.md +++ b/chapter_computational_complexity/performance_evaluation.md @@ -2,7 +2,7 @@ comments: true --- -# 2.1.   算法效率评估 +# 2.1   算法效率评估 在算法设计中,我们先后追求以下两个层面的目标: @@ -18,7 +18,7 @@ comments: true 效率评估方法主要分为两种:实际测试和理论估算。 -## 2.1.1.   实际测试 +## 2.1.1   实际测试 假设我们现在有算法 `A` 和算法 `B` ,它们都能解决同一问题,现在需要对比这两个算法的效率。最直接的方法是找一台计算机,运行这两个算法,并监控记录它们的运行时间和内存占用情况。这种评估方式能够反映真实情况,但也存在较大局限性。 @@ -26,7 +26,7 @@ comments: true **展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 更少;而输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这样需要耗费大量的计算资源。 -## 2.1.2.   理论估算 +## 2.1.2   理论估算 由于实际测试具有较大的局限性,我们可以考虑仅通过一些计算来评估算法的效率。这种估算方法被称为「渐近复杂度分析 Asymptotic Complexity Analysis」,简称为「复杂度分析」。 @@ -40,7 +40,7 @@ comments: true 如果你对复杂度分析的概念仍感到困惑,无需担心,我们会在后续章节详细介绍。 -## 2.1.3.   复杂度的重要性 +## 2.1.3   复杂度的重要性 复杂度分析为我们提供了一把评估算法效率的“标尺”,帮助我们衡量了执行某个算法所需的时间和空间资源,并使我们能够对比不同算法之间的效率。 diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index 7fc10f037..c8496a2d5 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -2,11 +2,11 @@ comments: true --- -# 2.3.   空间复杂度 +# 2.3   空间复杂度 「空间复杂度 Space Complexity」用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”。 -## 2.3.1.   算法相关空间 +## 2.3.1   算法相关空间 算法运行过程中使用的内存空间主要包括以下几种: @@ -292,7 +292,7 @@ comments: true ``` -## 2.3.2.   推算方法 +## 2.3.2   推算方法 空间复杂度的推算方法与时间复杂度大致相同,只需将统计对象从“计算操作数量”转为“使用空间大小”。 @@ -656,7 +656,7 @@ comments: true ``` -## 2.3.3.   常见类型 +## 2.3.3   常见类型 设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列): @@ -1978,7 +1978,7 @@ $$ 再例如“数字转化为字符串”,输入任意正整数 $n$ ,它的位数为 $\log_{10} n$ ,即对应字符串长度为 $\log_{10} n$ ,因此空间复杂度为 $O(\log_{10} n) = O(\log n)$ 。 -## 2.3.4.   权衡时间与空间 +## 2.3.4   权衡时间与空间 理想情况下,我们希望算法的时间复杂度和空间复杂度都能达到最优。然而在实际情况中,同时优化时间复杂度和空间复杂度通常是非常困难的。 diff --git a/chapter_computational_complexity/summary.md b/chapter_computational_complexity/summary.md index 8f74efa44..e2494b2e0 100644 --- a/chapter_computational_complexity/summary.md +++ b/chapter_computational_complexity/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 2.4.   小结 +# 2.4   小结 **算法效率评估** @@ -26,7 +26,7 @@ comments: true - 我们通常只关注最差空间复杂度,即统计算法在最差输入数据和最差运行时间点下的空间复杂度。 - 常见空间复杂度从小到大排列有 $O(1)$ , $O(\log n)$ , $O(n)$ , $O(n^2)$ , $O(2^n)$ 等。 -## 2.4.1.   Q & A +## 2.4.1   Q & A !!! question "尾递归的空间复杂度是 $O(1)$ 吗?" diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index 82c229598..268660102 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -2,7 +2,7 @@ comments: true --- -# 2.2.   时间复杂度 +# 2.2   时间复杂度 运行时间可以直观且准确地反映算法的效率。如果我们想要准确预估一段代码的运行时间,应该如何操作呢? @@ -187,7 +187,7 @@ $$ 但实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。 -## 2.2.1.   统计时间增长趋势 +## 2.2.1   统计时间增长趋势 「时间复杂度分析」采取了一种不同的方法,其统计的不是算法运行时间,**而是算法运行时间随着数据量变大时的增长趋势**。 @@ -446,7 +446,7 @@ $$ **时间复杂度也存在一定的局限性**。例如,尽管算法 `A` 和 `C` 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 `B` 的时间复杂度比 `C` 高,但在输入数据大小 $n$ 较小时,算法 `B` 明显优于算法 `C` 。在这些情况下,我们很难仅凭时间复杂度判断算法效率高低。当然,尽管存在上述问题,复杂度分析仍然是评判算法效率最有效且常用的方法。 -## 2.2.2.   函数渐近上界 +## 2.2.2   函数渐近上界 给定一个函数 `algorithm()` : @@ -638,7 +638,7 @@ $T(n)$ 是一次函数,说明时间的增长趋势是线性的,因此其时 也就是说,计算渐近上界就是寻找一个函数 $f(n)$ ,使得当 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别,仅相差一个常数项 $c$ 的倍数。 -## 2.2.3.   推算方法 +## 2.2.3   推算方法 渐近上界的数学味儿有点重,如果你感觉没有完全理解,也无需担心。因为在实际使用中,我们只需要掌握推算方法,数学意义可以逐渐领悟。 @@ -894,7 +894,7 @@ $$
-## 2.2.4.   常见类型 +## 2.2.4   常见类型 设输入数据大小为 $n$ ,常见的时间复杂度类型包括(按照从低到高的顺序排列): @@ -2952,7 +2952,7 @@ $$ 请注意,因为 $n! > 2^n$ ,所以阶乘阶比指数阶增长地更快,在 $n$ 较大时也是不可接受的。 -## 2.2.5.   最差、最佳、平均时间复杂度 +## 2.2.5   最差、最佳、平均时间复杂度 **算法的时间效率往往不是固定的,而是与输入数据的分布有关**。假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的,任务目标是返回元素 $1$ 的索引。我们可以得出以下结论: diff --git a/chapter_data_structure/basic_data_types.md b/chapter_data_structure/basic_data_types.md index 0d6d5c906..09214e3ff 100644 --- a/chapter_data_structure/basic_data_types.md +++ b/chapter_data_structure/basic_data_types.md @@ -2,7 +2,7 @@ comments: true --- -# 3.2.   基本数据类型 +# 3.2   基本数据类型 谈及计算机中的数据,我们会想到文本、图片、视频、语音、3D 模型等各种形式。尽管这些数据的组织形式各异,但它们都由各种基本数据类型构成。 diff --git a/chapter_data_structure/character_encoding.md b/chapter_data_structure/character_encoding.md index 89bccd079..711715d37 100644 --- a/chapter_data_structure/character_encoding.md +++ b/chapter_data_structure/character_encoding.md @@ -2,11 +2,11 @@ comments: true --- -# 3.4.   字符编码 * +# 3.4   字符编码 * 在计算机中,所有数据都是以二进制数的形式存储的,字符 `char` 也不例外。为了表示字符,我们需要建立一套“字符集”,规定每个字符和二进制数之间的一一对应关系。有了字符集之后,计算机就可以通过查表完成二进制数到字符的转换。 -## 3.4.1.   ASCII 字符集 +## 3.4.1   ASCII 字符集 「ASCII 码」是最早出现的字符集,全称为“美国标准信息交换代码”。它使用 7 位二进制数(即一个字节的低 7 位)表示一个字符,最多能够表示 128 个不同的字符。这包括英文字母的大小写、数字 0-9 、一些标点符号,以及一些控制字符(如换行符和制表符)。 @@ -18,13 +18,13 @@ comments: true 在世界范围内,陆续出现了一批适用于不同地区的 EASCII 字符集。这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。 -## 3.4.2.   GBK 字符集 +## 3.4.2   GBK 字符集 后来人们发现,**EASCII 码仍然无法满足许多语言的字符数量要求**。比如汉字大约有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了「GB2312」字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。 然而,GB2312 无法处理部分的罕见字和繁体字。「GBK」字符集是在 GB2312 的基础上扩展得到的,它共收录了 21886 个汉字。在 GBK 的编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。 -## 3.4.3.   Unicode 字符集 +## 3.4.3   Unicode 字符集 随着计算机的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作。另一方面,同一种语言也存在多种字符集标准,如果两台电脑安装的是不同的编码标准,则在信息传递时就会出现乱码。 @@ -44,7 +44,7 @@ Unicode 是一种字符集标准,本质上是给每个字符分配一个编号 然而 ASCII 码已经向我们证明,编码英文只需要 1 字节。若采用上述方案,英文文本占用空间的大小将会是 ASCII 编码下大小的两倍,非常浪费内存空间。因此,我们需要一种更加高效的 Unicode 编码方法。 -## 3.4.4.   UTF-8 编码 +## 3.4.4   UTF-8 编码 目前,UTF-8 已成为国际上使用最广泛的 Unicode 编码方法。**它是一种可变长的编码**,使用 1 到 4 个字节来表示一个字符,根据字符的复杂性而变。ASCII 字符只需要 1 个字节,拉丁字母和希腊字母需要 2 个字节,常用的中文字符需要 3 个字节,其他的一些生僻字符需要 4 个字节。 @@ -72,7 +72,7 @@ UTF-8 的编码规则并不复杂,分为两种情况: 从兼容性的角度看,UTF-8 的通用性最佳,许多工具和库都优先支持 UTF-8 。 -## 3.4.5.   编程语言的字符编码 +## 3.4.5   编程语言的字符编码 对于以往的大多数编程语言,程序运行中的字符串都采用 UTF-16 或 UTF-32 这类等长的编码。这是因为在等长编码下,我们可以将字符串看作数组来处理,其优点包括: diff --git a/chapter_data_structure/classification_of_data_structure.md b/chapter_data_structure/classification_of_data_structure.md index 0fda76a14..7d81e96df 100644 --- a/chapter_data_structure/classification_of_data_structure.md +++ b/chapter_data_structure/classification_of_data_structure.md @@ -2,11 +2,11 @@ comments: true --- -# 3.1.   数据结构分类 +# 3.1   数据结构分类 常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从“逻辑结构”和“物理结构”两个维度进行分类。 -## 3.1.1.   逻辑结构:线性与非线性 +## 3.1.1   逻辑结构:线性与非线性 **「逻辑结构」揭示了数据元素之间的逻辑关系**。在数组和链表中,数据按照顺序依次排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出祖先与后代之间的派生关系;图则由节点和边构成,反映了复杂的网络关系。 @@ -25,7 +25,7 @@ comments: true - **树形结构**:树、堆、哈希表,元素之间是一对多的关系。 - **网状结构**:图,元素之间是多对多的关系。 -## 3.1.2.   物理结构:连续与离散 +## 3.1.2   物理结构:连续与离散 在计算机中,内存和硬盘是两种主要的存储硬件设备。硬盘主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。内存用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。 diff --git a/chapter_data_structure/index.md b/chapter_data_structure/index.md index 4b0baa7bf..8637624f5 100644 --- a/chapter_data_structure/index.md +++ b/chapter_data_structure/index.md @@ -3,7 +3,7 @@ comments: true icon: material/shape-outline --- -# 3.   数据结构 +# 第 3 章   数据结构
diff --git a/chapter_data_structure/number_encoding.md b/chapter_data_structure/number_encoding.md index 894443ff8..a85bce265 100644 --- a/chapter_data_structure/number_encoding.md +++ b/chapter_data_structure/number_encoding.md @@ -2,13 +2,13 @@ comments: true --- -# 3.3.   数字编码 * +# 3.3   数字编码 * !!! note 在本书中,标题带有的 * 符号的是选读章节。如果你时间有限或感到理解困难,可以先跳过,等学完必读章节后再单独攻克。 -## 3.3.1.   原码、反码和补码 +## 3.3.1   原码、反码和补码 从上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个。例如,`byte` 的取值范围是 $[-128, 127]$ 。这个现象比较反直觉,它的内在原因涉及到原码、反码、补码的相关知识。 @@ -92,7 +92,7 @@ $$ 补码的设计非常精妙,因篇幅关系我们就先介绍到这里,建议有兴趣的读者进一步深度了解。 -## 3.3.2.   浮点数编码 +## 3.3.2   浮点数编码 细心的你可能会发现:`int` 和 `float` 长度相同,都是 4 bytes,但为什么 `float` 的取值范围远大于 `int` ?这非常反直觉,因为按理说 `float` 需要表示小数,取值范围应该变小才对。 diff --git a/chapter_data_structure/summary.md b/chapter_data_structure/summary.md index 20035f105..e76edfc77 100644 --- a/chapter_data_structure/summary.md +++ b/chapter_data_structure/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 3.5.   小结 +# 3.5   小结 - 数据结构可以从逻辑结构和物理结构两个角度进行分类。逻辑结构描述了数据元素之间的逻辑关系,而物理结构描述了数据在计算机内存中的存储方式。 - 常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性和非线性结构。 @@ -15,7 +15,7 @@ comments: true - ASCII 码是最早出现的英文字符集,长度为 1 字节,共收录 127 个字符。GBK 字符集是常用的中文字符集,共收录两万多个汉字。Unicode 致力于提供一个完整的字符集标准,收录世界内各种语言的字符,从而解决由于字符编码方法不一致而导致的乱码问题。 - UTF-8 是最受欢迎的 Unicode 编码方法,通用性非常好。它是一种变长的编码方法,具有很好的扩展性,有效提升了存储空间的使用效率。UTF-16 和 UTF-32 是等长的编码方法。在编码中文时,UTF-16 比 UTF-8 的占用空间更小。Java, C# 等编程语言默认使用 UTF-16 编码。 -## 3.5.1.   Q & A +## 3.5.1   Q & A !!! question "为什么哈希表同时包含线性数据结构和非线性数据结构?" diff --git a/chapter_divide_and_conquer/binary_search_recur.md b/chapter_divide_and_conquer/binary_search_recur.md index b780260b0..41bd9c443 100644 --- a/chapter_divide_and_conquer/binary_search_recur.md +++ b/chapter_divide_and_conquer/binary_search_recur.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 12.2.   分治搜索策略 +# 12.2   分治搜索策略 我们已经学过,搜索算法分为两大类: diff --git a/chapter_divide_and_conquer/build_binary_tree_problem.md b/chapter_divide_and_conquer/build_binary_tree_problem.md index 574d7a434..b582e9acb 100644 --- a/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 12.3.   构建二叉树问题 +# 12.3   构建二叉树问题 !!! question diff --git a/chapter_divide_and_conquer/divide_and_conquer.md b/chapter_divide_and_conquer/divide_and_conquer.md index 2a9e91391..c317c3cb7 100644 --- a/chapter_divide_and_conquer/divide_and_conquer.md +++ b/chapter_divide_and_conquer/divide_and_conquer.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 12.1.   分治算法 +# 12.1   分治算法 「分治 Divide and Conquer」,全称分而治之,是一种非常重要且常见的算法策略。分治通常基于递归实现,包括“分”和“治”两步: @@ -19,7 +19,7 @@ status: new

图:归并排序的分治策略

-## 12.1.1.   如何判断分治问题 +## 12.1.1   如何判断分治问题 一个问题是否适合使用分治解决,通常可以参考以下几个判断依据: @@ -33,7 +33,7 @@ status: new 2. 每个子数组都可以独立地进行排序(子问题可以独立进行求解)。 3. 两个有序子数组(子问题的解)可以被合并为一个有序数组(原问题的解)。 -## 12.1.2.   通过分治提升效率 +## 12.1.2   通过分治提升效率 分治不仅可以有效地解决算法问题,**往往还可以带来算法效率的提升**。在排序算法中,快速排序、归并排序、堆排序相较于选择、冒泡、插入排序更快,就是因为它们应用了分治策略。 @@ -79,7 +79,7 @@ $$

图:桶排序的并行计算

-## 12.1.3.   分治常见应用 +## 12.1.3   分治常见应用 一方面,分治可以用来解决许多经典算法问题: diff --git a/chapter_divide_and_conquer/hanota_problem.md b/chapter_divide_and_conquer/hanota_problem.md index 8f28c1ef8..5c1844ecc 100644 --- a/chapter_divide_and_conquer/hanota_problem.md +++ b/chapter_divide_and_conquer/hanota_problem.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 12.4.   汉诺塔问题 +# 12.4   汉诺塔问题 在归并排序和构建二叉树中,我们都是将原问题分解为两个规模为原问题一半的子问题。然而对于汉诺塔问题,我们采用不同的分解策略。 diff --git a/chapter_divide_and_conquer/index.md b/chapter_divide_and_conquer/index.md index 6e38102b1..d16351c5d 100644 --- a/chapter_divide_and_conquer/index.md +++ b/chapter_divide_and_conquer/index.md @@ -4,7 +4,7 @@ icon: material/set-split status: new --- -# 12.   分治 +# 第 12 章   分治
diff --git a/chapter_divide_and_conquer/summary.md b/chapter_divide_and_conquer/summary.md index b7a6861d3..c4e2d5444 100644 --- a/chapter_divide_and_conquer/summary.md +++ b/chapter_divide_and_conquer/summary.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 12.5.   小结 +# 12.5   小结 - 分治算法是一种常见的算法设计策略,包括分(划分)和治(合并)两个阶段,通常基于递归实现。 - 判断是否是分治算法问题的依据包括:问题能否被分解、子问题是否独立、子问题是否可以被合并。 diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md index b163f561b..16ac54909 100644 --- a/chapter_dynamic_programming/dp_problem_features.md +++ b/chapter_dynamic_programming/dp_problem_features.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 14.2.   动态规划问题特性 +# 14.2   动态规划问题特性 在上节中,我们学习了动态规划是如何通过子问题分解来求解问题的。实际上,子问题分解是一种通用的算法思路,在分治、动态规划、回溯中的侧重点不同: @@ -13,7 +13,7 @@ status: new 实际上,动态规划常用来求解最优化问题,它们不仅包含重叠子问题,还具有另外两大特性:最优子结构、无后效性。 -## 14.2.1.   最优子结构 +## 14.2.1   最优子结构 我们对爬楼梯问题稍作改动,使之更加适合展示最优子结构概念。 @@ -431,7 +431,7 @@ $$ } ``` -## 14.2.2.   无后效性 +## 14.2.2   无后效性 「无后效性」是动态规划能够有效解决问题的重要特性之一,定义为:**给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关**。 diff --git a/chapter_dynamic_programming/dp_solution_pipeline.md b/chapter_dynamic_programming/dp_solution_pipeline.md index 35c53f86f..d8f61e1f5 100644 --- a/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/chapter_dynamic_programming/dp_solution_pipeline.md @@ -3,14 +3,14 @@ comments: true status: new --- -# 14.3.   动态规划解题思路 +# 14.3   动态规划解题思路 上两节介绍了动态规划问题的主要特征,接下来我们一起探究两个更加实用的问题: 1. 如何判断一个问题是不是动态规划问题? 2. 求解动态规划问题该从何处入手,完整步骤是什么? -## 14.3.1.   问题判断 +## 14.3.1   问题判断 总的来说,如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常就适合用动态规划求解。然而,我们很难从问题描述上直接提取出这些特性。因此我们通常会放宽条件,**先观察问题是否适合使用回溯(穷举)解决**。 @@ -30,7 +30,7 @@ status: new 如果一个问题满足决策树模型,并具有较为明显的“加分项“,我们就可以假设它是一个动态规划问题,并在求解过程中验证它。 -## 14.3.2.   问题求解步骤 +## 14.3.2   问题求解步骤 动态规划的解题流程会因问题的性质和难度而有所不同,但通常遵循以下步骤:描述决策,定义状态,建立 $dp$ 表,推导状态转移方程,确定边界条件等。 diff --git a/chapter_dynamic_programming/edit_distance_problem.md b/chapter_dynamic_programming/edit_distance_problem.md index e7fd47152..dd4d8da27 100644 --- a/chapter_dynamic_programming/edit_distance_problem.md +++ b/chapter_dynamic_programming/edit_distance_problem.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 14.6.   编辑距离问题 +# 14.6   编辑距离问题 编辑距离,也被称为 Levenshtein 距离,指两个字符串之间互相转换的最小修改次数,通常用于在信息检索和自然语言处理中度量两个序列的相似度。 diff --git a/chapter_dynamic_programming/index.md b/chapter_dynamic_programming/index.md index 7e507cdd1..016068a20 100644 --- a/chapter_dynamic_programming/index.md +++ b/chapter_dynamic_programming/index.md @@ -4,7 +4,7 @@ icon: material/table-pivot status: new --- -# 14.   动态规划 +# 第 14 章   动态规划
diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md index 88ee31643..b459a1ddf 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 14.1.   初探动态规划 +# 14.1   初探动态规划 「动态规划 Dynamic Programming」是一个重要的算法范式,它将一个问题分解为一系列更小的子问题,并通过存储子问题的解来避免重复计算,从而大幅提升时间效率。 @@ -360,7 +360,7 @@ status: new } ``` -## 14.1.1.   方法一:暴力搜索 +## 14.1.1   方法一:暴力搜索 回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。 @@ -615,7 +615,7 @@ $$ 以此类推,子问题中包含更小的重叠子问题,子子孙孙无穷尽也。绝大部分计算资源都浪费在这些重叠的问题上。 -## 14.1.2.   方法二:记忆化搜索 +## 14.1.2   方法二:记忆化搜索 为了提升算法效率,**我们希望所有的重叠子问题都只被计算一次**。为此,我们声明一个数组 `mem` 来记录每个子问题的解,并在搜索过程中这样做: @@ -923,7 +923,7 @@ $$

图:记忆化搜索对应递归树

-## 14.1.3.   方法三:动态规划 +## 14.1.3   方法三:动态规划 **记忆化搜索是一种“从顶至底”的方法**:我们从原问题(根节点)开始,递归地将较大子问题分解为较小子问题,直至解已知的最小子问题(叶节点)。之后,通过回溯将子问题的解逐层收集,构建出原问题的解。 @@ -1167,7 +1167,7 @@ $$

图:爬楼梯的动态规划过程

-## 14.1.4.   状态压缩 +## 14.1.4   状态压缩 细心的你可能发现,**由于 $dp[i]$ 只与 $dp[i-1]$ 和 $dp[i-2]$ 有关,因此我们无需使用一个数组 `dp` 来存储所有子问题的解**,而只需两个变量滚动前进即可。 diff --git a/chapter_dynamic_programming/knapsack_problem.md b/chapter_dynamic_programming/knapsack_problem.md index 36f7042b8..4c3f05eed 100644 --- a/chapter_dynamic_programming/knapsack_problem.md +++ b/chapter_dynamic_programming/knapsack_problem.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 14.4.   0-1 背包问题 +# 14.4   0-1 背包问题 背包问题是一个非常好的动态规划入门题目,是动态规划中最常见的问题形式。其具有很多变种,例如 0-1 背包问题、完全背包问题、多重背包问题等。 diff --git a/chapter_dynamic_programming/summary.md b/chapter_dynamic_programming/summary.md index 3aedfc223..c63ebeffc 100644 --- a/chapter_dynamic_programming/summary.md +++ b/chapter_dynamic_programming/summary.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 14.7.   小结 +# 14.7   小结 - 动态规划对问题进行分解,并通过存储子问题的解来规避重复计算,实现高效的计算效率。 - 不考虑时间的前提下,所有动态规划问题都可以用回溯(暴力搜索)进行求解,但递归树中存在大量的重叠子问题,效率极低。通过引入记忆化列表,可以存储所有计算过的子问题的解,从而保证重叠子问题只被计算一次。 diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem.md b/chapter_dynamic_programming/unbounded_knapsack_problem.md index 929c94a8a..f218b656f 100644 --- a/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -3,11 +3,11 @@ comments: true status: new --- -# 14.5.   完全背包问题 +# 14.5   完全背包问题 在本节中,我们先求解另一个常见的背包问题:完全背包,再了解它的一种特例:零钱兑换。 -## 14.5.1.   完全背包 +## 14.5.1   完全背包 !!! question @@ -529,7 +529,7 @@ $$ } ``` -## 14.5.2.   零钱兑换问题 +## 14.5.2   零钱兑换问题 背包问题是一大类动态规划问题的代表,其拥有很多的变种,例如零钱兑换问题。 @@ -1178,7 +1178,7 @@ $$ } ``` -## 14.5.3.   零钱兑换问题 II +## 14.5.3   零钱兑换问题 II !!! question diff --git a/chapter_graph/graph.md b/chapter_graph/graph.md index ffb18b3e9..a66b8ce23 100644 --- a/chapter_graph/graph.md +++ b/chapter_graph/graph.md @@ -2,7 +2,7 @@ comments: true --- -# 9.1.   图 +# 9.1   图 「图 Graph」是一种非线性数据结构,由「顶点 Vertex」和「边 Edge」组成。我们可以将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。以下示例展示了一个包含 5 个顶点和 7 条边的图。 @@ -20,7 +20,7 @@ $$ 那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作节点,把「边」看作连接各个节点的指针,则可将「图」看作是一种从「链表」拓展而来的数据结构。**相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,从而更为复杂**。 -## 9.1.1.   图常见类型 +## 9.1.1   图常见类型 根据边是否具有方向,可分为「无向图 Undirected Graph」和「有向图 Directed Graph」。 @@ -46,13 +46,13 @@ $$

图:有权图与无权图

-## 9.1.2.   图常用术语 +## 9.1.2   图常用术语 - 「邻接 Adjacency」:当两顶点之间存在边相连时,称这两顶点“邻接”。在上图中,顶点 1 的邻接顶点为顶点 2、3、5。 - 「路径 Path」:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。在上图中,边序列 1-5-2-4 是顶点 1 到顶点 4 的一条路径。 - 「度 Degree」表示一个顶点拥有的边数。对于有向图,「入度 In-Degree」表示有多少条边指向该顶点,「出度 Out-Degree」表示有多少条边从该顶点指出。 -## 9.1.3.   图的表示 +## 9.1.3   图的表示 图的常用表示方法包括「邻接矩阵」和「邻接表」。以下使用无向图进行举例。 @@ -86,7 +86,7 @@ $$ 观察上图可发现,**邻接表结构与哈希表中的「链地址法」非常相似,因此我们也可以采用类似方法来优化效率**。例如,当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;此外,还可以将链表转换为哈希表,将时间复杂度降低至 $O(1)$ 。 -## 9.1.4.   图常见应用 +## 9.1.4   图常见应用 实际应用中,许多系统都可以用图来建模,相应的待求解问题也可以约化为图计算问题。

表:现实生活中常见的图

diff --git a/chapter_graph/graph_operations.md b/chapter_graph/graph_operations.md index 40b61fc09..753eaf453 100644 --- a/chapter_graph/graph_operations.md +++ b/chapter_graph/graph_operations.md @@ -2,11 +2,11 @@ comments: true --- -# 9.2.   图基础操作 +# 9.2   图基础操作 图的基础操作可分为对「边」的操作和对「顶点」的操作。在「邻接矩阵」和「邻接表」两种表示方法下,实现方式有所不同。 -## 9.2.1.   基于邻接矩阵的实现 +## 9.2.1   基于邻接矩阵的实现 给定一个顶点数量为 $n$ 的无向图,则有: @@ -1124,7 +1124,7 @@ comments: true } ``` -## 9.2.2.   基于邻接表的实现 +## 9.2.2   基于邻接表的实现 设无向图的顶点总数为 $n$ 、边总数为 $m$ ,则有: @@ -2112,7 +2112,7 @@ comments: true } ``` -## 9.2.3.   效率对比 +## 9.2.3   效率对比 设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。

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

diff --git a/chapter_graph/graph_traversal.md b/chapter_graph/graph_traversal.md index ad86dbf97..e2d48d439 100644 --- a/chapter_graph/graph_traversal.md +++ b/chapter_graph/graph_traversal.md @@ -2,7 +2,7 @@ comments: true --- -# 9.3.   图的遍历 +# 9.3   图的遍历 !!! note "图与树的关系" @@ -12,7 +12,7 @@ comments: true 与树类似,图的遍历方式也可分为两种,即「广度优先遍历 Breadth-First Traversal」和「深度优先遍历 Depth-First Traversal」,也称为「广度优先搜索 Breadth-First Search」和「深度优先搜索 Depth-First Search」,简称 BFS 和 DFS。 -## 9.3.1.   广度优先遍历 +## 9.3.1   广度优先遍历 **广度优先遍历是一种由近及远的遍历方式,从距离最近的顶点开始访问,并一层层向外扩张**。具体来说,从某个顶点出发,先遍历该顶点的所有邻接顶点,然后遍历下一个顶点的所有邻接顶点,以此类推,直至所有顶点访问完毕。 @@ -439,7 +439,7 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 **空间复杂度:** 列表 `res` ,哈希表 `visited` ,队列 `que` 中的顶点数量最多为 $|V|$ ,使用 $O(|V|)$ 空间。 -## 9.3.2.   深度优先遍历 +## 9.3.2   深度优先遍历 **深度优先遍历是一种优先走到底、无路可走再回头的遍历方式**。具体地,从某个顶点出发,访问当前顶点的某个邻接顶点,直到走到尽头时返回,再继续走到尽头并返回,以此类推,直至所有顶点遍历完成。 diff --git a/chapter_graph/index.md b/chapter_graph/index.md index 8c0ad2969..8cfb2957c 100644 --- a/chapter_graph/index.md +++ b/chapter_graph/index.md @@ -3,7 +3,7 @@ comments: true icon: material/graphql --- -# 9.   图 +# 第 9 章   图
diff --git a/chapter_graph/summary.md b/chapter_graph/summary.md index e93b9c883..0baa78055 100644 --- a/chapter_graph/summary.md +++ b/chapter_graph/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 9.4.   小结 +# 9.4   小结 - 图由顶点和边组成,可以被表示为一组顶点和一组边构成的集合。 - 相较于线性关系(链表)和分治关系(树),网络关系(图)具有更高的自由度,因而更为复杂。 @@ -16,7 +16,7 @@ comments: true - 图的广度优先遍历是一种由近及远、层层扩张的搜索方式,通常借助队列实现。 - 图的深度优先遍历是一种优先走到底、无路可走时再回溯的搜索方式,常基于递归来实现。 -## 9.4.1.   Q & A +## 9.4.1   Q & A !!! question "路径的定义是顶点序列还是边序列?" diff --git a/chapter_greedy/fractional_knapsack_problem.md b/chapter_greedy/fractional_knapsack_problem.md index 21ec67058..9112d7d76 100644 --- a/chapter_greedy/fractional_knapsack_problem.md +++ b/chapter_greedy/fractional_knapsack_problem.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 15.2.   分数背包问题 +# 15.2   分数背包问题 分数背包是 0-1 背包的一个变种问题。 diff --git a/chapter_greedy/greedy_algorithm.md b/chapter_greedy/greedy_algorithm.md index 5e52a17a5..baffc4515 100644 --- a/chapter_greedy/greedy_algorithm.md +++ b/chapter_greedy/greedy_algorithm.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 15.1.   贪心算法 +# 15.1   贪心算法 贪心算法是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期望获得全局最优解。贪心算法简洁且高效,在许多实际问题中都有着广泛的应用。 @@ -221,7 +221,7 @@ status: new } ``` -## 15.1.1.   贪心优点与局限性 +## 15.1.1   贪心优点与局限性 **贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times amt)$ 提升了一个数量级。 @@ -242,7 +242,7 @@ status: new 1. **可以保证找到最优解**:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效。 2. **可以找到近似最优解**:贪心算法在这种情况下也是可用的。对于很多复杂问题来说,寻找全局最优解是非常困难的,能以较高效率找到次优解也是非常不错的。 -## 15.1.2.   贪心算法特性 +## 15.1.2   贪心算法特性 那么问题来了,什么样的问题适合用贪心算法求解呢?或者说,贪心算法在什么情况下可以保证找到最优解? @@ -263,7 +263,7 @@ status: new Pearson, David. A polynomial-time algorithm for the change-making problem. Operations Research Letters 33.3 (2005): 231-234. -## 15.1.3.   贪心解题步骤 +## 15.1.3   贪心解题步骤 贪心问题的解决流程大体可分为三步: @@ -280,7 +280,7 @@ status: new 然而,正确性证明也很可能不是一件易事。如若没有头绪,我们通常会选择面向测试用例进行 Debug ,一步步修改与验证贪心策略。 -## 15.1.4.   贪心典型例题 +## 15.1.4   贪心典型例题 贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下是一些典型的贪心算法问题: diff --git a/chapter_greedy/index.md b/chapter_greedy/index.md index 59f9c6c59..504c6f4f2 100644 --- a/chapter_greedy/index.md +++ b/chapter_greedy/index.md @@ -4,7 +4,7 @@ icon: material/head-heart-outline status: new --- -# 15.   贪心 +# 第 15 章   贪心
diff --git a/chapter_greedy/max_capacity_problem.md b/chapter_greedy/max_capacity_problem.md index 15106ed1a..308ca3a6e 100644 --- a/chapter_greedy/max_capacity_problem.md +++ b/chapter_greedy/max_capacity_problem.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 15.3.   最大容量问题 +# 15.3   最大容量问题 !!! question diff --git a/chapter_greedy/max_product_cutting_problem.md b/chapter_greedy/max_product_cutting_problem.md index 6c899559d..382fccc1b 100644 --- a/chapter_greedy/max_product_cutting_problem.md +++ b/chapter_greedy/max_product_cutting_problem.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 15.4.   最大切分乘积问题 +# 15.4   最大切分乘积问题 !!! question diff --git a/chapter_greedy/summary.md b/chapter_greedy/summary.md index ee1736794..ba6564fe6 100644 --- a/chapter_greedy/summary.md +++ b/chapter_greedy/summary.md @@ -3,7 +3,7 @@ comments: true status: new --- -# 15.5.   小结 +# 15.5   小结 - 贪心算法通常用于解决最优化问题,其原理是在每个决策阶段都做出局部最优的决策,以期望获得全局最优解。 - 贪心算法会迭代地做出一个又一个的贪心选择,每轮都将问题转化成一个规模更小的子问题,直到问题被解决。 diff --git a/chapter_hashing/hash_algorithm.md b/chapter_hashing/hash_algorithm.md index db0b4d84a..7bea818d1 100644 --- a/chapter_hashing/hash_algorithm.md +++ b/chapter_hashing/hash_algorithm.md @@ -2,7 +2,7 @@ comments: true --- -# 6.3.   哈希算法 +# 6.3   哈希算法 在上两节中,我们了解了哈希表的工作原理和哈希冲突的处理方法。然而无论是开放寻址还是链地址法,**它们只能保证哈希表可以在发生冲突时正常工作,但无法减少哈希冲突的发生**。 @@ -22,7 +22,7 @@ index = hash(key) % capacity 这意味着,为了减小哈希冲突的发生概率,我们应当将注意力集中在哈希算法 `hash()` 的设计上。 -## 6.3.1.   哈希算法的目标 +## 6.3.1   哈希算法的目标 为了实现“既快又稳”的哈希表数据结构,哈希算法应包含以下特点: @@ -42,7 +42,7 @@ index = hash(key) % capacity 请注意,**“均匀分布”与“抗碰撞性”是两个独立的概念**,满足均匀分布不一定满足抗碰撞性。例如,在随机输入 `key` 下,哈希函数 `key % 100` 可以产生均匀分布的输出。然而该哈希算法过于简单,所有后两位相等的 `key` 的输出都相同,因此我们可以很容易地从哈希值反推出可用的 `key` ,从而破解密码。 -## 6.3.2.   哈希算法的设计 +## 6.3.2   哈希算法的设计 哈希算法的设计是一个复杂且需要考虑许多因素的问题。然而对于简单场景,我们也能设计一些简单的哈希算法。以字符串哈希为例: @@ -520,7 +520,7 @@ $$ 总而言之,我们通常选取质数作为模数,并且这个质数最好足够大,以尽可能消除周期性模式,提升哈希算法的稳健性。 -## 6.3.3.   常见哈希算法 +## 6.3.3   常见哈希算法 不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。 @@ -544,7 +544,7 @@ $$
-## 6.3.4.   数据结构的哈希值 +## 6.3.4   数据结构的哈希值 我们知道,哈希表的 `key` 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法,用于计算哈希表中的桶索引。以 Python 为例,我们可以调用 `hash()` 函数来计算各种数据类型的哈希值,包括: diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 863df4ae2..29a976173 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -2,7 +2,7 @@ comments: true --- -# 6.2.   哈希冲突 +# 6.2   哈希冲突 上节提到,**通常情况下哈希函数的输入空间远大于输出空间**,因此理论上哈希冲突是不可避免的。比如,输入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一数组索引。 @@ -13,7 +13,7 @@ comments: true 哈希表的结构改良方法主要包括链式地址和开放寻址。 -## 6.2.1.   链式地址 +## 6.2.1   链式地址 在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 Separate Chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。 @@ -1152,7 +1152,7 @@ comments: true 当链表很长时,查询效率 $O(n)$ 很差,**此时可以将链表转换为「AVL 树」或「红黑树」**,从而将查询操作的时间复杂度优化至 $O(\log n)$ 。 -## 6.2.2.   开放寻址 +## 6.2.2   开放寻址 「开放寻址 Open Addressing」不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测、多次哈希等。 @@ -2448,7 +2448,7 @@ comments: true 与线性探测相比,多次哈希方法不易产生聚集,但多个哈希函数会增加额外的计算量。 -## 6.2.3.   编程语言的选择 +## 6.2.3   编程语言的选择 Java 采用链式地址。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会被转换为红黑树以提升查找性能。 diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index ec1b44e79..42cc2fc0b 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -2,7 +2,7 @@ comments: true --- -# 6.1.   哈希表 +# 6.1   哈希表 散列表,又称「哈希表 Hash Table」,其通过建立键 `key` 与值 `value` 之间的映射,实现高效的元素查询。具体而言,我们向哈希表输入一个键 `key` ,则可以在 $O(1)$ 时间内获取对应的值 `value` 。 @@ -31,7 +31,7 @@ comments: true 观察发现,**在哈希表中进行增删查改的时间复杂度都是 $O(1)$** ,非常高效。 -## 6.1.1.   哈希表常用操作 +## 6.1.1   哈希表常用操作 哈希表的常见操作包括:初始化、查询操作、添加键值对和删除键值对等。 @@ -444,7 +444,7 @@ comments: true ``` -## 6.1.2.   哈希表简单实现 +## 6.1.2   哈希表简单实现 我们先考虑最简单的情况,**仅用一个数组来实现哈希表**。在哈希表中,我们将数组中的每个空位称为「桶 Bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 `key` 对应的桶,并在桶中获取 `value` 。 @@ -1487,7 +1487,7 @@ index = hash(key) % capacity } ``` -## 6.1.3.   哈希冲突与扩容 +## 6.1.3   哈希冲突与扩容 本质上看,哈希函数的作用是将所有 `key` 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,**理论上一定存在“多个输入对应相同输出”的情况**。 diff --git a/chapter_hashing/index.md b/chapter_hashing/index.md index ed9b6b3b0..a477cd425 100644 --- a/chapter_hashing/index.md +++ b/chapter_hashing/index.md @@ -3,7 +3,7 @@ comments: true icon: material/table-search --- -# 6.   散列表 +# 第 6 章   散列表
diff --git a/chapter_hashing/summary.md b/chapter_hashing/summary.md index 20443c1b0..84fba9af0 100644 --- a/chapter_hashing/summary.md +++ b/chapter_hashing/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 6.4.   小结 +# 6.4   小结 - 输入 `key` ,哈希表能够在 $O(1)$ 时间内查询到 `value` ,效率非常高。 - 常见的哈希表操作包括查询、添加键值对、删除键值对和遍历哈希表等。 @@ -18,7 +18,7 @@ comments: true - 常见的哈希算法包括 MD5, SHA-1, SHA-2, SHA3 等。MD5 常用于校验文件完整性,SHA-2 常用于安全应用与协议。 - 编程语言通常会为数据类型提供内置哈希算法,用于计算哈希表中的桶索引。通常情况下,只有不可变对象是可哈希的。 -## 6.4.1.   Q & A +## 6.4.1   Q & A !!! question "哈希表的时间复杂度为什么不是 $O(n)$ ?" diff --git a/chapter_heap/build_heap.md b/chapter_heap/build_heap.md index ef7f1a3ae..4a432625b 100644 --- a/chapter_heap/build_heap.md +++ b/chapter_heap/build_heap.md @@ -2,17 +2,17 @@ comments: true --- -# 8.2.   建堆操作 +# 8.2   建堆操作 如果我们想要根据输入列表生成一个堆,这个过程被称为「建堆」。 -## 8.2.1.   借助入堆方法实现 +## 8.2.1   借助入堆方法实现 最直接的方法是借助“元素入堆操作”实现,首先创建一个空堆,然后将列表元素依次添加到堆中。 设元素数量为 $n$ ,则最后一个元素入堆的时间复杂度为 $O(\log n)$ 。在依次添加元素时,堆的平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。 -## 8.2.2.   基于堆化操作实现 +## 8.2.2   基于堆化操作实现 有趣的是,存在一种更高效的建堆方法,其时间复杂度仅为 $O(n)$ 。我们先将列表所有元素原封不动添加到堆中,**然后迭代地对各个节点执行“从顶至底堆化”**。当然,**我们不需要对叶节点执行堆化操作**,因为它们没有子节点。 @@ -191,7 +191,7 @@ comments: true } ``` -## 8.2.3.   复杂度分析 +## 8.2.3   复杂度分析 为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。 diff --git a/chapter_heap/heap.md b/chapter_heap/heap.md index 6306c14bd..4d1527395 100644 --- a/chapter_heap/heap.md +++ b/chapter_heap/heap.md @@ -2,7 +2,7 @@ comments: true --- -# 8.1.   堆 +# 8.1   堆 「堆 Heap」是一种满足特定条件的完全二叉树,可分为两种类型: @@ -19,7 +19,7 @@ comments: true - 我们将二叉树的根节点称为「堆顶」,将底层最靠右的节点称为「堆底」。 - 对于大顶堆(小顶堆),堆顶元素(即根节点)的值分别是最大(最小)的。 -## 8.1.1.   堆常用操作 +## 8.1.1   堆常用操作 需要指出的是,许多编程语言提供的是「优先队列 Priority Queue」,这是一种抽象数据结构,定义为具有优先级排序的队列。 @@ -320,7 +320,7 @@ comments: true ``` -## 8.1.2.   堆的实现 +## 8.1.2   堆的实现 下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。 @@ -1565,7 +1565,7 @@ comments: true } ``` -## 8.1.3.   堆常见应用 +## 8.1.3   堆常见应用 - **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。 - **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。然而,我们通常会使用一种更优雅的方式实现堆排序,详见后续的堆排序章节。 diff --git a/chapter_heap/index.md b/chapter_heap/index.md index 84018359d..d42cc1f8b 100644 --- a/chapter_heap/index.md +++ b/chapter_heap/index.md @@ -3,7 +3,7 @@ comments: true icon: material/family-tree --- -# 8.   堆 +# 第 8 章   堆
diff --git a/chapter_heap/summary.md b/chapter_heap/summary.md index 34fd87663..074c26ea6 100644 --- a/chapter_heap/summary.md +++ b/chapter_heap/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 8.4.   小结 +# 8.4   小结 - 堆是一棵完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素是最大(小)的。 - 优先队列的定义是具有出队优先级的队列,通常使用堆来实现。 @@ -12,7 +12,7 @@ comments: true - 输入 $n$ 个元素并建堆的时间复杂度可以优化至 $O(n)$ ,非常高效。 - Top-K 是一个经典算法问题,可以使用堆数据结构高效解决,时间复杂度为 $O(n \log k)$ 。 -## 8.4.1.   Q & A +## 8.4.1   Q & A !!! question "数据结构的“堆”与内存管理的“堆”是同一个概念吗?" diff --git a/chapter_heap/top_k.md b/chapter_heap/top_k.md index 6aa03c934..8ae5a8374 100644 --- a/chapter_heap/top_k.md +++ b/chapter_heap/top_k.md @@ -2,7 +2,7 @@ comments: true --- -# 8.3.   Top-K 问题 +# 8.3   Top-K 问题 !!! question @@ -10,7 +10,7 @@ comments: true 对于该问题,我们先介绍两种思路比较直接的解法,再介绍效率更高的堆解法。 -## 8.3.1.   方法一:遍历选择 +## 8.3.1   方法一:遍历选择 我们可以进行 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\cdots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。 @@ -24,7 +24,7 @@ comments: true 当 $k = n$ 时,我们可以得到从大到小的序列,等价于「选择排序」算法。 -## 8.3.2.   方法二:排序 +## 8.3.2   方法二:排序 我们可以对数组 `nums` 进行排序,并返回最右边的 $k$ 个元素,时间复杂度为 $O(n \log n)$ 。 @@ -34,7 +34,7 @@ comments: true

图:排序寻找最大的 k 个元素

-## 8.3.3.   方法三:堆 +## 8.3.3   方法三:堆 我们可以基于堆更加高效地解决 Top-K 问题,流程如下: diff --git a/chapter_introduction/algorithms_are_everywhere.md b/chapter_introduction/algorithms_are_everywhere.md index d12d5fa90..d81f60445 100644 --- a/chapter_introduction/algorithms_are_everywhere.md +++ b/chapter_introduction/algorithms_are_everywhere.md @@ -2,7 +2,7 @@ comments: true --- -# 1.1.   算法无处不在 +# 1.1   算法无处不在 当我们听到“算法”这个词时,很自然地会想到数学。然而实际上,许多算法并不涉及复杂数学,而是更多地依赖于基本逻辑,这些逻辑在我们的日常生活中处处可见。 diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index ae667187c..2fe164f0d 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -3,7 +3,7 @@ comments: true icon: material/calculator-variant-outline --- -# 1.   初识算法 +# 第 1 章   初识算法
diff --git a/chapter_introduction/summary.md b/chapter_introduction/summary.md index 8ce4d05ef..e936c88f7 100644 --- a/chapter_introduction/summary.md +++ b/chapter_introduction/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 1.3.   小结 +# 1.3   小结 - 算法在日常生活中无处不在,并不是遥不可及的高深知识。实际上,我们已经在不知不觉中学会了许多算法,用以解决生活中的大小问题。 - 查阅字典的原理与二分查找算法相一致。二分查找体现了分而治之的重要算法思想。 diff --git a/chapter_introduction/what_is_dsa.md b/chapter_introduction/what_is_dsa.md index 8c1711033..2e5a70e62 100644 --- a/chapter_introduction/what_is_dsa.md +++ b/chapter_introduction/what_is_dsa.md @@ -2,9 +2,9 @@ comments: true --- -# 1.2.   算法是什么 +# 1.2   算法是什么 -## 1.2.1.   算法定义 +## 1.2.1   算法定义 「算法 Algorithm」是在有限时间内解决特定问题的一组指令或操作步骤。它具有以下特性: @@ -12,7 +12,7 @@ comments: true - 具有可行性,能够在有限步骤、时间和内存空间下完成。 - 各步骤都有确定的含义,相同的输入和运行条件下,输出始终相同。 -## 1.2.2.   数据结构定义 +## 1.2.2   数据结构定义 「数据结构 Data Structure」是计算机中组织和存储数据的方式。它的设计目标包括: @@ -25,7 +25,7 @@ comments: true - 链表相较于数组,在数据添加和删除操作上更加便捷,但牺牲了数据访问速度。 - 图相较于链表,提供了更丰富的逻辑信息,但需要占用更大的内存空间。 -## 1.2.3.   数据结构与算法的关系 +## 1.2.3   数据结构与算法的关系 数据结构与算法高度相关、紧密结合,具体表现在: diff --git a/chapter_preface/about_the_book.md b/chapter_preface/about_the_book.md index bd34348b2..1bcd0dcd0 100644 --- a/chapter_preface/about_the_book.md +++ b/chapter_preface/about_the_book.md @@ -2,7 +2,7 @@ comments: true --- -# 0.1.   关于本书 +# 0.1   关于本书 本项目旨在创建一本开源免费、新手友好的数据结构与算法入门教程。 @@ -10,7 +10,7 @@ comments: true - 算法源代码皆可一键运行,支持 Java, C++, Python, Go, JS, TS, C#, Swift, Zig 等语言。 - 鼓励读者在章节讨论区互帮互助、共同进步,提问与评论通常可在两日内得到回复。 -## 0.1.1.   读者对象 +## 0.1.1   读者对象 若您是算法初学者,从未接触过算法,或者已经有一些刷题经验,对数据结构与算法有模糊的认识,在会与不会之间反复横跳,那么这本书正是为您量身定制! @@ -22,7 +22,7 @@ comments: true 您需要至少具备任一语言的编程基础,能够阅读和编写简单代码。 -## 0.1.2.   内容结构 +## 0.1.2   内容结构 本书主要内容包括: @@ -34,7 +34,7 @@ comments: true

图:Hello 算法内容结构

-## 0.1.3.   致谢 +## 0.1.3   致谢 在本书的创作过程中,我得到了许多人的帮助,包括但不限于: diff --git a/chapter_preface/index.md b/chapter_preface/index.md index 0fca2719f..f45ef77dd 100644 --- a/chapter_preface/index.md +++ b/chapter_preface/index.md @@ -3,7 +3,7 @@ comments: true icon: material/book-open-outline --- -# 0.   前言 +# 第 0 章   前言
diff --git a/chapter_preface/suggestions.md b/chapter_preface/suggestions.md index 0a8fc1149..093006a24 100644 --- a/chapter_preface/suggestions.md +++ b/chapter_preface/suggestions.md @@ -2,13 +2,13 @@ comments: true --- -# 0.2.   如何使用本书 +# 0.2   如何使用本书 !!! tip 为了获得最佳的阅读体验,建议您通读本节内容。 -## 0.2.1.   行文风格约定 +## 0.2.1   行文风格约定 - 标题后标注 `*` 的是选读章节,内容相对困难。如果你的时间有限,建议可以先跳过。 - 文章中的重要名词会用 `「 」` 括号标注,例如 `「数组 Array」` 。请务必记住这些名词,包括英文翻译,以便后续阅读文献时使用。 @@ -164,7 +164,7 @@ comments: true ``` -## 0.2.2.   在动画图解中高效学习 +## 0.2.2   在动画图解中高效学习 相较于文字,视频和图片具有更高的信息密度和结构化程度,更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。 @@ -174,7 +174,7 @@ comments: true

图:动画图解示例

-## 0.2.3.   在代码实践中加深理解 +## 0.2.3   在代码实践中加深理解 本书的配套代码被托管在 [GitHub 仓库](https://github.com/krahets/hello-algo)。**源代码附有测试样例,可一键运行**。 @@ -206,7 +206,7 @@ git clone https://github.com/krahets/hello-algo.git

图:代码块与对应的源代码文件

-## 0.2.4.   在提问讨论中共同成长 +## 0.2.4   在提问讨论中共同成长 阅读本书时,请不要“惯着”那些没学明白的知识点。**欢迎在评论区提出你的问题**,我和其他小伙伴们将竭诚为你解答,一般情况下可在两天内得到回复。 @@ -216,7 +216,7 @@ git clone https://github.com/krahets/hello-algo.git

图:评论区示例

-## 0.2.5.   算法学习路线 +## 0.2.5   算法学习路线 从总体上看,我们可以将学习数据结构与算法的过程划分为三个阶段: diff --git a/chapter_preface/summary.md b/chapter_preface/summary.md index 4e015e6eb..04afa3b69 100644 --- a/chapter_preface/summary.md +++ b/chapter_preface/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 0.3.   小结 +# 0.3   小结 - 本书的主要受众是算法初学者。如果已有一定基础,本书能帮助您系统回顾算法知识,书内源代码也可作为“刷题工具库”使用。 - 书中内容主要包括复杂度分析、数据结构、算法三部分,涵盖了该领域的大部分主题。 diff --git a/chapter_searching/binary_search.md b/chapter_searching/binary_search.md index 506fb2605..9350cd1e2 100755 --- a/chapter_searching/binary_search.md +++ b/chapter_searching/binary_search.md @@ -2,7 +2,7 @@ comments: true --- -# 10.1.   二分查找 +# 10.1   二分查找 「二分查找 Binary Search」是一种基于分治思想的高效搜索算法。它利用数据的有序性,每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。 @@ -336,7 +336,7 @@ comments: true 空间复杂度为 $O(1)$ 。指针 `i` , `j` 使用常数大小空间。 -## 10.1.1.   区间表示方法 +## 10.1.1   区间表示方法 除了上述的双闭区间外,常见的区间表示还有“左闭右开”区间,定义为 $[0, n)$ ,即左边界包含自身,右边界不包含自身。在该表示下,区间 $[i, j]$ 在 $i = j$ 时为空。 @@ -631,7 +631,7 @@ comments: true

图:两种区间定义

-## 10.1.2.   优点与局限性 +## 10.1.2   优点与局限性 二分查找在时间和空间方面都有较好的性能: diff --git a/chapter_searching/binary_search_edge.md b/chapter_searching/binary_search_edge.md index c1bc9ee18..459eca5b3 100644 --- a/chapter_searching/binary_search_edge.md +++ b/chapter_searching/binary_search_edge.md @@ -3,9 +3,9 @@ comments: true status: new --- -# 10.3.   二分查找边界 +# 10.3   二分查找边界 -## 10.3.1.   查找左边界 +## 10.3.1   查找左边界 !!! question @@ -150,7 +150,7 @@ status: new [class]{}-[func]{binary_search_left_edge} ``` -## 10.3.2.   查找右边界 +## 10.3.2   查找右边界 那么如何查找最右一个 `target` 呢?最直接的方式是修改代码,替换在 `nums[m] == target` 情况下的指针收缩操作。代码在此省略,有兴趣的同学可以自行实现。 diff --git a/chapter_searching/binary_search_insertion.md b/chapter_searching/binary_search_insertion.md index db61e915f..723d4bdf1 100644 --- a/chapter_searching/binary_search_insertion.md +++ b/chapter_searching/binary_search_insertion.md @@ -3,11 +3,11 @@ comments: true status: new --- -# 10.2.   二分查找插入点 +# 10.2   二分查找插入点 二分查找不仅可用于搜索目标元素,还具有许多变种问题,比如搜索目标元素的插入位置。 -## 10.2.1.   无重复元素的情况 +## 10.2.1   无重复元素的情况 !!! question @@ -188,7 +188,7 @@ status: new [class]{}-[func]{binary_search_insertion} ``` -## 10.2.2.   存在重复元素的情况 +## 10.2.2   存在重复元素的情况 !!! question diff --git a/chapter_searching/index.md b/chapter_searching/index.md index be0969ac7..2fcb9ee8e 100644 --- a/chapter_searching/index.md +++ b/chapter_searching/index.md @@ -3,7 +3,7 @@ comments: true icon: material/text-search --- -# 10.   搜索 +# 第 10 章   搜索
diff --git a/chapter_searching/replace_linear_by_hashing.md b/chapter_searching/replace_linear_by_hashing.md index 1456b49f0..f05cd19ce 100755 --- a/chapter_searching/replace_linear_by_hashing.md +++ b/chapter_searching/replace_linear_by_hashing.md @@ -2,7 +2,7 @@ comments: true --- -# 10.4.   哈希优化策略 +# 10.4   哈希优化策略 在算法题中,**我们常通过将线性查找替换为哈希查找来降低算法的时间复杂度**。我们借助一个算法题来加深理解。 @@ -10,7 +10,7 @@ comments: true 给定一个整数数组 `nums` 和一个目标元素 `target` ,请在数组中搜索“和”为 `target` 的两个元素,并返回它们的数组索引。返回任意一个解即可。 -## 10.4.1.   线性查找:以时间换空间 +## 10.4.1   线性查找:以时间换空间 考虑直接遍历所有可能的组合。开启一个两层循环,在每轮中判断两个整数的和是否为 `target` ,若是,则返回它们的索引。 @@ -229,7 +229,7 @@ comments: true 此方法的时间复杂度为 $O(n^2)$ ,空间复杂度为 $O(1)$ ,在大数据量下非常耗时。 -## 10.4.2.   哈希查找:以空间换时间 +## 10.4.2   哈希查找:以空间换时间 考虑借助一个哈希表,键值对分别为数组元素和元素索引。循环遍历数组,每轮执行: diff --git a/chapter_searching/searching_algorithm_revisited.md b/chapter_searching/searching_algorithm_revisited.md index aa0054fc8..dad80d4e1 100644 --- a/chapter_searching/searching_algorithm_revisited.md +++ b/chapter_searching/searching_algorithm_revisited.md @@ -2,7 +2,7 @@ comments: true --- -# 10.5.   重识搜索算法 +# 10.5   重识搜索算法 「搜索算法 Searching Algorithm」用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。 @@ -13,7 +13,7 @@ comments: true 不难发现,这些知识点都已在前面的章节中介绍过,因此搜索算法对于我们来说并不陌生。在本节中,我们将从更加系统的视角切入,重新审视搜索算法。 -## 10.5.1.   暴力搜索 +## 10.5.1   暴力搜索 暴力搜索通过遍历数据结构的每个元素来定位目标元素。 @@ -24,7 +24,7 @@ comments: true 然而,**此类算法的时间复杂度为 $O(n)$** ,其中 $n$ 为元素数量,因此在数据量较大的情况下性能较差。 -## 10.5.2.   自适应搜索 +## 10.5.2   自适应搜索 自适应搜索利用数据的特有属性(例如有序性)来优化搜索过程,从而更高效地定位目标元素。 @@ -40,7 +40,7 @@ comments: true 自适应搜索算法常被称为查找算法,**主要关注在特定数据结构中快速检索目标元素**。 -## 10.5.3.   搜索方法选取 +## 10.5.3   搜索方法选取 给定大小为 $n$ 的一组数据,我们可以使用线性搜索、二分查找、树查找、哈希查找等多种方法在该数据中搜索目标元素。各个方法的工作原理如下图所示。 diff --git a/chapter_searching/summary.md b/chapter_searching/summary.md index 4db142ad3..330e94bf5 100644 --- a/chapter_searching/summary.md +++ b/chapter_searching/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 10.6.   小结 +# 10.6   小结 - 二分查找依赖于数据的有序性,通过循环逐步缩减一半搜索区间来实现查找。它要求输入数据有序,且仅适用于数组或基于数组实现的数据结构。 - 暴力搜索通过遍历数据结构来定位数据。线性搜索适用于数组和链表,广度优先搜索和深度优先搜索适用于图和树。此类算法通用性好,无需对数据预处理,但时间复杂度 $O(n)$ 较高。 diff --git a/chapter_sorting/bubble_sort.md b/chapter_sorting/bubble_sort.md index 44a20da18..022af52d7 100755 --- a/chapter_sorting/bubble_sort.md +++ b/chapter_sorting/bubble_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.3.   冒泡排序 +# 11.3   冒泡排序 「冒泡排序 Bubble Sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。 @@ -31,7 +31,7 @@ comments: true

图:利用元素交换操作模拟冒泡

-## 11.3.1.   算法流程 +## 11.3.1   算法流程 设数组的长度为 $n$ ,冒泡排序的步骤为: @@ -277,7 +277,7 @@ comments: true } ``` -## 11.3.2.   效率优化 +## 11.3.2   效率优化 我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。 @@ -559,7 +559,7 @@ comments: true } ``` -## 11.3.3.   算法特性 +## 11.3.3   算法特性 - **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。 - **空间复杂度为 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。 diff --git a/chapter_sorting/bucket_sort.md b/chapter_sorting/bucket_sort.md index a75290b4c..e8b32bff7 100644 --- a/chapter_sorting/bucket_sort.md +++ b/chapter_sorting/bucket_sort.md @@ -2,13 +2,13 @@ comments: true --- -# 11.8.   桶排序 +# 11.8   桶排序 前述的几种排序算法都属于“基于比较的排序算法”,它们通过比较元素间的大小来实现排序。此类排序算法的时间复杂度无法超越 $O(n \log n)$ 。接下来,我们将探讨几种“非比较排序算法”,它们的时间复杂度可以达到线性阶。 「桶排序 Bucket Sort」是分治思想的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。 -## 11.8.1.   算法流程 +## 11.8.1   算法流程 考虑一个长度为 $n$ 的数组,元素是范围 $[0, 1)$ 的浮点数。桶排序的流程如下: @@ -396,14 +396,14 @@ comments: true 桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。 -## 11.8.2.   算法特性 +## 11.8.2   算法特性 - **时间复杂度 $O(n + k)$** :假设元素在各个桶内平均分布,那么每个桶内的元素数量为 $\frac{n}{k}$ 。假设排序单个桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 时间,则排序所有桶使用 $O(n \log\frac{n}{k})$ 时间。**当桶数量 $k$ 比较大时,时间复杂度则趋向于 $O(n)$** 。合并结果时需要遍历所有桶和元素,花费 $O(n + k)$ 时间。 - **自适应排序**:在最坏情况下,所有数据被分配到一个桶中,且排序该桶使用 $O(n^2)$ 时间。 - **空间复杂度 $O(n + k)$ 、非原地排序** :需要借助 $k$ 个桶和总共 $n$ 个元素的额外空间。 - 桶排序是否稳定取决于排序桶内元素的算法是否稳定。 -## 11.8.3.   如何实现平均分配 +## 11.8.3   如何实现平均分配 桶排序的时间复杂度理论上可以达到 $O(n)$ ,**关键在于将元素均匀分配到各个桶中**,因为实际数据往往不是均匀分布的。例如,我们想要将淘宝上的所有商品按价格范围平均分配到 10 个桶中,但商品价格分布不均,低于 100 元的非常多,高于 1000 元的非常少。若将价格区间平均划分为 10 份,各个桶中的商品数量差距会非常大。 diff --git a/chapter_sorting/counting_sort.md b/chapter_sorting/counting_sort.md index 098f714f3..a851295d2 100644 --- a/chapter_sorting/counting_sort.md +++ b/chapter_sorting/counting_sort.md @@ -2,11 +2,11 @@ comments: true --- -# 11.9.   计数排序 +# 11.9   计数排序 「计数排序 Counting Sort」通过统计元素数量来实现排序,通常应用于整数数组。 -## 11.9.1.   简单实现 +## 11.9.1   简单实现 先来看一个简单的例子。给定一个长度为 $n$ 的数组 `nums` ,其中的元素都是“非负整数”。计数排序的整体流程如下: @@ -321,7 +321,7 @@ comments: true 从桶排序的角度看,我们可以将计数排序中的计数数组 `counter` 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。 -## 11.9.2.   完整实现 +## 11.9.2   完整实现 细心的同学可能发现,**如果输入数据是对象,上述步骤 `3.` 就失效了**。例如,输入数据是商品对象,我们想要按照商品价格(类的成员变量)对商品进行排序,而上述算法只能给出价格的排序结果。 @@ -772,13 +772,13 @@ $$ } ``` -## 11.9.3.   算法特性 +## 11.9.3   算法特性 - **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。 - **空间复杂度 $O(n + m)$ 、非原地排序** :借助了长度分别为 $n$ 和 $m$ 的数组 `res` 和 `counter` 。 - **稳定排序**:由于向 `res` 中填充元素的顺序是“从右向左”的,因此倒序遍历 `nums` 可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历 `nums` 也可以得到正确的排序结果,但结果是非稳定的。 -## 11.9.4.   局限性 +## 11.9.4   局限性 看到这里,你也许会觉得计数排序非常巧妙,仅通过统计数量就可以实现高效的排序工作。然而,使用计数排序的前置条件相对较为严格。 diff --git a/chapter_sorting/heap_sort.md b/chapter_sorting/heap_sort.md index bbe36ecbe..c42878094 100644 --- a/chapter_sorting/heap_sort.md +++ b/chapter_sorting/heap_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.7.   堆排序 +# 11.7   堆排序 !!! tip @@ -15,7 +15,7 @@ comments: true 以上方法虽然可行,但需要借助一个额外数组来保存弹出的元素,比较浪费空间。在实际中,我们通常使用一种更加优雅的实现方式。 -## 11.7.1.   算法流程 +## 11.7.1   算法流程 设数组的长度为 $n$ ,堆排序的流程如下: @@ -540,7 +540,7 @@ comments: true } ``` -## 11.7.2.   算法特性 +## 11.7.2   算法特性 - **时间复杂度 $O(n \log n)$ 、非自适应排序** :建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。 - **空间复杂度 $O(1)$ 、原地排序** :几个指针变量使用 $O(1)$ 空间。元素交换和堆化操作都是在原数组上进行的。 diff --git a/chapter_sorting/index.md b/chapter_sorting/index.md index 8bc768aca..6849c6f94 100644 --- a/chapter_sorting/index.md +++ b/chapter_sorting/index.md @@ -3,7 +3,7 @@ comments: true icon: material/sort-ascending --- -# 11.   排序 +# 第 11 章   排序
diff --git a/chapter_sorting/insertion_sort.md b/chapter_sorting/insertion_sort.md index 3ca40a9f4..bd190fbcf 100755 --- a/chapter_sorting/insertion_sort.md +++ b/chapter_sorting/insertion_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.4.   插入排序 +# 11.4   插入排序 「插入排序 Insertion Sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。 @@ -14,7 +14,7 @@ comments: true

图:单次插入操作

-## 11.4.1.   算法流程 +## 11.4.1   算法流程 插入排序的整体流程如下: @@ -248,13 +248,13 @@ comments: true } ``` -## 11.4.2.   算法特性 +## 11.4.2   算法特性 - **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。 - **空间复杂度 $O(1)$ 、原地排序** :指针 $i$ , $j$ 使用常数大小的额外空间。 - **稳定排序**:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。 -## 11.4.3.   插入排序优势 +## 11.4.3   插入排序优势 插入排序的时间复杂度为 $O(n^2)$ ,而我们即将学习的快速排序的时间复杂度为 $O(n \log n)$ 。尽管插入排序的时间复杂度相比快速排序更高,**但在数据量较小的情况下,插入排序通常更快**。 diff --git a/chapter_sorting/merge_sort.md b/chapter_sorting/merge_sort.md index 3a9f553ce..35e4f905e 100755 --- a/chapter_sorting/merge_sort.md +++ b/chapter_sorting/merge_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.6.   归并排序 +# 11.6   归并排序 「归并排序 Merge Sort」基于分治思想实现排序,包含“划分”和“合并”两个阶段: @@ -13,7 +13,7 @@ comments: true

图:归并排序的划分与合并阶段

-## 11.6.1.   算法流程 +## 11.6.1   算法流程 “划分阶段”从顶至底递归地将数组从中点切为两个子数组: @@ -625,13 +625,13 @@ comments: true - **在阅读代码时,需要特别注意各个变量的含义**。`nums` 的待合并区间为 `[left, right]` ,但由于 `tmp` 仅复制了 `nums` 该区间的元素,因此 `tmp` 对应区间为 `[0, right - left]` 。 - 在比较 `tmp[i]` 和 `tmp[j]` 的大小时,**还需考虑子数组遍历完成后的索引越界问题**,即 `i > leftEnd` 和 `j > rightEnd` 的情况。索引越界的优先级是最高的,如果左子数组已经被合并完了,那么不需要继续比较,直接合并右子数组元素即可。 -## 11.6.2.   算法特性 +## 11.6.2   算法特性 - **时间复杂度 $O(n \log n)$ 、非自适应排序** :划分产生高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,因此总体时间复杂度为 $O(n \log n)$ 。 - **空间复杂度 $O(n)$ 、非原地排序** :递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间。合并操作需要借助辅助数组实现,使用 $O(n)$ 大小的额外空间。 - **稳定排序**:在合并过程中,相等元素的次序保持不变。 -## 11.6.3.   链表排序 * +## 11.6.3   链表排序 * 归并排序在排序链表时具有显著优势,空间复杂度可以优化至 $O(1)$ ,原因如下: diff --git a/chapter_sorting/quick_sort.md b/chapter_sorting/quick_sort.md index 15731464b..0fe808f5e 100755 --- a/chapter_sorting/quick_sort.md +++ b/chapter_sorting/quick_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.5.   快速排序 +# 11.5   快速排序 「快速排序 Quick Sort」是一种基于分治思想的排序算法,运行高效,应用广泛。 @@ -360,7 +360,7 @@ comments: true } ``` -## 11.5.1.   算法流程 +## 11.5.1   算法流程 1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组。 2. 然后,对左子数组和右子数组分别递归执行「哨兵划分」。 @@ -586,13 +586,13 @@ comments: true } ``` -## 11.5.2.   算法特性 +## 11.5.2   算法特性 - **时间复杂度 $O(n \log n)$ 、自适应排序** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。 - **空间复杂度 $O(n)$ 、原地排序** :在输入数组完全倒序的情况下,达到最差递归深度 $n$ ,使用 $O(n)$ 栈帧空间。排序操作是在原数组上进行的,未借助额外数组。 - **非稳定排序**:在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。 -## 11.5.3.   快排为什么快? +## 11.5.3   快排为什么快? 从名称上就能看出,快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与「归并排序」和「堆排序」相同,但通常快速排序的效率更高,原因如下: @@ -600,7 +600,7 @@ comments: true - **缓存使用效率高**:在执行哨兵划分操作时,系统可将整个子数组加载到缓存,因此访问元素的效率较高。而像「堆排序」这类算法需要跳跃式访问元素,从而缺乏这一特性。 - **复杂度的常数系数低**:在上述三种算法中,快速排序的比较、赋值、交换等操作的总数量最少。这与「插入排序」比「冒泡排序」更快的原因类似。 -## 11.5.4.   基准数优化 +## 11.5.4   基准数优化 **快速排序在某些输入下的时间效率可能降低**。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 $n - 1$ 、右子数组长度为 $0$ 。如此递归下去,每轮哨兵划分后的右子数组长度都为 $0$ ,分治策略失效,快速排序退化为「冒泡排序」。 @@ -1053,7 +1053,7 @@ comments: true } ``` -## 11.5.5.   尾递归优化 +## 11.5.5   尾递归优化 **在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 diff --git a/chapter_sorting/radix_sort.md b/chapter_sorting/radix_sort.md index cec2175b6..37d5c19e9 100644 --- a/chapter_sorting/radix_sort.md +++ b/chapter_sorting/radix_sort.md @@ -2,13 +2,13 @@ comments: true --- -# 11.10.   基数排序 +# 11.10   基数排序 上一节我们介绍了计数排序,它适用于数据量 $n$ 较大但数据范围 $m$ 较小的情况。假设我们需要对 $n = 10^6$ 个学号进行排序,而学号是一个 $8$ 位数字,这意味着数据范围 $m = 10^8$ 非常大,使用计数排序需要分配大量内存空间,而基数排序可以避免这种情况。 「基数排序 Radix Sort」的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。 -## 11.10.1.   算法流程 +## 11.10.1   算法流程 以学号数据为例,假设数字的最低位是第 $1$ 位,最高位是第 $8$ 位,基数排序的步骤如下: @@ -688,7 +688,7 @@ $$ 在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 $a < b$ ,而第二轮排序结果 $a > b$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。 -## 11.10.2.   算法特性 +## 11.10.2   算法特性 相较于计数排序,基数排序适用于数值范围较大的情况,**但前提是数据必须可以表示为固定位数的格式,且位数不能过大**。例如,浮点数不适合使用基数排序,因为其位数 $k$ 过大,可能导致时间复杂度 $O(nk) \gg O(n^2)$ 。 diff --git a/chapter_sorting/selection_sort.md b/chapter_sorting/selection_sort.md index 7f43411b1..2f202b043 100644 --- a/chapter_sorting/selection_sort.md +++ b/chapter_sorting/selection_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.2.   选择排序 +# 11.2   选择排序 「选择排序 Selection Sort」的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。 @@ -284,7 +284,7 @@ comments: true } ``` -## 11.2.1.   算法特性 +## 11.2.1   算法特性 - **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 - **空间复杂度 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。 diff --git a/chapter_sorting/sorting_algorithm.md b/chapter_sorting/sorting_algorithm.md index b50859f47..e038e5cfe 100644 --- a/chapter_sorting/sorting_algorithm.md +++ b/chapter_sorting/sorting_algorithm.md @@ -2,7 +2,7 @@ comments: true --- -# 11.1.   排序算法 +# 11.1   排序算法 「排序算法 Sorting Algorithm」用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更有效地查找、分析和处理。 @@ -12,7 +12,7 @@ comments: true

图:数据类型和判断规则示例

-## 11.1.1.   评价维度 +## 11.1.1   评价维度 **运行效率**:我们期望排序算法的时间复杂度尽量低,且总体操作数量较少(即时间复杂度中的常数项降低)。对于大数据量情况,运行效率显得尤为重要。 @@ -47,7 +47,7 @@ comments: true **是否基于比较**:「基于比较的排序」依赖于比较运算符($<$ , $=$ , $>$)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 $O(n \log n)$ 。而「非比较排序」不使用比较运算符,时间复杂度可达 $O(n)$ ,但其通用性相对较差。 -## 11.1.2.   理想排序算法 +## 11.1.2   理想排序算法 **运行快、原地、稳定、正向自适应、通用性好**。显然,迄今为止尚未发现兼具以上所有特性的排序算法。因此,在选择排序算法时,需要根据具体的数据特点和问题需求来决定。 diff --git a/chapter_sorting/summary.md b/chapter_sorting/summary.md index f27a1ccf9..664a377f2 100644 --- a/chapter_sorting/summary.md +++ b/chapter_sorting/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 11.11.   小结 +# 11.11   小结 - 冒泡排序通过交换相邻元素来实现排序。通过添加一个标志位来实现提前返回,我们可以将冒泡排序的最佳时间复杂度优化到 $O(n)$ 。 - 插入排序每轮将未排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。 @@ -17,7 +17,7 @@ comments: true

图:排序算法对比

-## 11.11.1.   Q & A +## 11.11.1   Q & A !!! question "排序算法稳定性在什么情况下是必须的?" diff --git a/chapter_stack_and_queue/deque.md b/chapter_stack_and_queue/deque.md index a7f8ebd10..83c644107 100644 --- a/chapter_stack_and_queue/deque.md +++ b/chapter_stack_and_queue/deque.md @@ -2,7 +2,7 @@ comments: true --- -# 5.3.   双向队列 +# 5.3   双向队列 对于队列,我们仅能在头部删除或在尾部添加元素。然而,「双向队列 Deque」提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。 @@ -10,7 +10,7 @@ comments: true

图:双向队列的操作

-## 5.3.1.   双向队列常用操作 +## 5.3.1   双向队列常用操作 双向队列的常用操作如下表所示,具体的方法名称需要根据所使用的编程语言来确定。

表:双向队列操作效率

@@ -325,7 +325,7 @@ comments: true ``` -## 5.3.2.   双向队列实现 * +## 5.3.2   双向队列实现 * 双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。 @@ -3207,7 +3207,7 @@ comments: true } ``` -## 5.3.3.   双向队列应用 +## 5.3.3   双向队列应用 双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。 diff --git a/chapter_stack_and_queue/index.md b/chapter_stack_and_queue/index.md index cda44a9bc..eb8644b95 100644 --- a/chapter_stack_and_queue/index.md +++ b/chapter_stack_and_queue/index.md @@ -3,7 +3,7 @@ comments: true icon: material/stack-overflow --- -# 5.   栈与队列 +# 第 5 章   栈与队列
diff --git a/chapter_stack_and_queue/queue.md b/chapter_stack_and_queue/queue.md index c52094940..32db628a2 100755 --- a/chapter_stack_and_queue/queue.md +++ b/chapter_stack_and_queue/queue.md @@ -2,7 +2,7 @@ comments: true --- -# 5.2.   队列 +# 5.2   队列 「队列 Queue」是一种遵循先入先出(First In, First Out)规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列的尾部,而位于队列头部的人逐个离开。 @@ -12,7 +12,7 @@ comments: true

图:队列的先入先出规则

-## 5.2.1.   队列常用操作 +## 5.2.1   队列常用操作 队列的常见操作如下表所示。需要注意的是,不同编程语言的方法名称可能会有所不同。我们在此采用与栈相同的方法命名。

表:队列操作效率

@@ -292,7 +292,7 @@ comments: true ``` -## 5.2.2.   队列实现 +## 5.2.2   队列实现 为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。 @@ -2106,7 +2106,7 @@ comments: true 两种实现的对比结论与栈一致,在此不再赘述。 -## 5.2.3.   队列典型应用 +## 5.2.3   队列典型应用 - **淘宝订单**。购物者下单后,订单将加入队列中,系统随后会根据顺序依次处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。 - **各类待办事项**。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等。队列在这些场景中可以有效地维护处理顺序。 diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md index 1fa81cb7c..e0222f45b 100755 --- a/chapter_stack_and_queue/stack.md +++ b/chapter_stack_and_queue/stack.md @@ -2,7 +2,7 @@ comments: true --- -# 5.1.   栈 +# 5.1   栈 「栈 Stack」是一种遵循先入后出(First In, Last Out)原则的线性数据结构。 @@ -14,7 +14,7 @@ comments: true

图:栈的先入后出规则

-## 5.1.1.   栈常用操作 +## 5.1.1   栈常用操作 栈的常用操作如下表所示,具体的方法名需要根据所使用的编程语言来确定。在此,我们以常见的 `push()` , `pop()` , `peek()` 命名为例。

表:栈的操作效率

@@ -290,7 +290,7 @@ comments: true ``` -## 5.1.2.   栈的实现 +## 5.1.2   栈的实现 为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。 @@ -1672,7 +1672,7 @@ comments: true } ``` -## 5.1.3.   两种实现对比 +## 5.1.3   两种实现对比 ### 支持操作 @@ -1697,7 +1697,7 @@ comments: true 综上,我们不能简单地确定哪种实现更加节省内存,需要针对具体情况进行分析。 -## 5.1.4.   栈典型应用 +## 5.1.4   栈典型应用 - **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就会将上一个网页执行入栈,这样我们就可以通过「后退」操作回到上一页面。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。 - **程序内存管理**。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会执行出栈操作。 diff --git a/chapter_stack_and_queue/summary.md b/chapter_stack_and_queue/summary.md index 676aaeb28..305209af1 100644 --- a/chapter_stack_and_queue/summary.md +++ b/chapter_stack_and_queue/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 5.4.   小结 +# 5.4   小结 - 栈是一种遵循先入后出原则的数据结构,可通过数组或链表来实现。 - 从时间效率角度看,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会降低至 $O(n)$ 。相比之下,基于链表实现的栈具有更为稳定的效率表现。 @@ -10,7 +10,7 @@ comments: true - 队列是一种遵循先入先出原则的数据结构,同样可以通过数组或链表来实现。在时间效率和空间效率的对比上,队列的结论与前述栈的结论相似。 - 双向队列是一种具有更高自由度的队列,它允许在两端进行元素的添加和删除操作。 -## 5.4.1.   Q & A +## 5.4.1   Q & A !!! question "浏览器的前进后退是否是双向链表实现?" diff --git a/chapter_tree/array_representation_of_tree.md b/chapter_tree/array_representation_of_tree.md index 6a9920be6..55267be04 100644 --- a/chapter_tree/array_representation_of_tree.md +++ b/chapter_tree/array_representation_of_tree.md @@ -2,13 +2,13 @@ comments: true --- -# 7.3.   二叉树数组表示 +# 7.3   二叉树数组表示 在链表表示下,二叉树的存储单元为节点 `TreeNode` ,节点之间通过指针相连接。在上节中,我们学习了在链表表示下的二叉树的各项基本操作。 那么,能否用「数组」来表示二叉树呢?答案是肯定的。 -## 7.3.1.   表示完美二叉树 +## 7.3.1   表示完美二叉树 先分析一个简单案例。给定一个完美二叉树,我们将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。 @@ -20,7 +20,7 @@ comments: true **映射公式的角色相当于链表中的指针**。给定数组中的任意一个节点,我们都可以通过映射公式来访问它的左(右)子节点。 -## 7.3.2.   表示任意二叉树 +## 7.3.2   表示任意二叉树 然而完美二叉树是一个特例,在二叉树的中间层,通常存在许多 $\text{None}$ 。由于层序遍历序列并不包含这些 $\text{None}$ ,因此我们无法仅凭该序列来推测 $\text{None}$ 的数量和分布位置。**这意味着存在多种二叉树结构都符合该层序遍历序列**。显然在这种情况下,上述的数组表示方法已经失效。 @@ -1153,7 +1153,7 @@ comments: true } ``` -## 7.3.3.   优势与局限性 +## 7.3.3   优势与局限性 二叉树的数组表示的优点包括: diff --git a/chapter_tree/avl_tree.md b/chapter_tree/avl_tree.md index c2f2e258d..dcd37963e 100644 --- a/chapter_tree/avl_tree.md +++ b/chapter_tree/avl_tree.md @@ -2,7 +2,7 @@ comments: true --- -# 7.5.   AVL 树 * +# 7.5   AVL 树 * 在二叉搜索树章节中,我们提到了在多次插入和删除操作后,二叉搜索树可能退化为链表。这种情况下,所有操作的时间复杂度将从 $O(\log n)$ 恶化为 $O(n)$。 @@ -20,7 +20,7 @@ comments: true G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 $O(\log n)$ 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。 -## 7.5.1.   AVL 树常见术语 +## 7.5.1   AVL 树常见术语 「AVL 树」既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为「平衡二叉搜索树」。 @@ -580,7 +580,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit 设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。 -## 7.5.2.   AVL 树旋转 +## 7.5.2   AVL 树旋转 AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,**旋转操作既能保持树的「二叉搜索树」属性,也能使树重新变为「平衡二叉树」**。 @@ -1519,7 +1519,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` -## 7.5.3.   AVL 树常用操作 +## 7.5.3   AVL 树常用操作 ### 插入节点 @@ -2436,7 +2436,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。 -## 7.5.4.   AVL 树典型应用 +## 7.5.4   AVL 树典型应用 - 组织和存储大型数据,适用于高频查找、低频增删的场景。 - 用于构建数据库中的索引系统。 diff --git a/chapter_tree/binary_search_tree.md b/chapter_tree/binary_search_tree.md index 68a115552..6d879bba5 100755 --- a/chapter_tree/binary_search_tree.md +++ b/chapter_tree/binary_search_tree.md @@ -2,7 +2,7 @@ comments: true --- -# 7.4.   二叉搜索树 +# 7.4   二叉搜索树 「二叉搜索树 Binary Search Tree」满足以下条件: @@ -13,7 +13,7 @@ comments: true

图:二叉搜索树

-## 7.4.1.   二叉搜索树的操作 +## 7.4.1   二叉搜索树的操作 我们将二叉搜索树封装为一个类 `ArrayBinaryTree` ,并声明一个成员变量 `root` ,指向树的根节点。 @@ -1484,7 +1484,7 @@ comments: true

图:二叉搜索树的中序遍历序列

-## 7.4.2.   二叉搜索树的效率 +## 7.4.2   二叉搜索树的效率 给定一组数据,我们考虑使用数组或二叉搜索树存储。 @@ -1509,7 +1509,7 @@ comments: true

图:二叉搜索树的平衡与退化

-## 7.4.3.   二叉搜索树常见应用 +## 7.4.3   二叉搜索树常见应用 - 用作系统中的多级索引,实现高效的查找、插入、删除操作。 - 作为某些搜索算法的底层数据结构。 diff --git a/chapter_tree/binary_tree.md b/chapter_tree/binary_tree.md index 79942141c..ac19b2a79 100644 --- a/chapter_tree/binary_tree.md +++ b/chapter_tree/binary_tree.md @@ -2,7 +2,7 @@ comments: true --- -# 7.1.   二叉树 +# 7.1   二叉树 「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用。 @@ -173,7 +173,7 @@ comments: true

图:父节点、子节点、子树

-## 7.1.1.   二叉树常见术语 +## 7.1.1   二叉树常见术语 二叉树涉及的术语较多,建议尽量理解并记住。 @@ -194,7 +194,7 @@ comments: true 请注意,我们通常将「高度」和「深度」定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。 -## 7.1.2.   二叉树基本操作 +## 7.1.2   二叉树基本操作 **初始化二叉树**。与链表类似,首先初始化节点,然后构建引用指向(即指针)。 @@ -518,7 +518,7 @@ comments: true 需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。 -## 7.1.3.   常见二叉树类型 +## 7.1.3   常见二叉树类型 ### 完美二叉树 @@ -556,7 +556,7 @@ comments: true

图:平衡二叉树

-## 7.1.4.   二叉树的退化 +## 7.1.4   二叉树的退化 当二叉树的每层节点都被填满时,达到「完美二叉树」;而当所有节点都偏向一侧时,二叉树退化为「链表」。 diff --git a/chapter_tree/binary_tree_traversal.md b/chapter_tree/binary_tree_traversal.md index f2ada62ca..a194669fc 100755 --- a/chapter_tree/binary_tree_traversal.md +++ b/chapter_tree/binary_tree_traversal.md @@ -2,13 +2,13 @@ comments: true --- -# 7.2.   二叉树遍历 +# 7.2   二叉树遍历 从物理结构的角度来看,树是一种基于链表的数据结构,因此其遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现。 二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历和后序遍历等。 -## 7.2.1.   层序遍历 +## 7.2.1   层序遍历 「层序遍历 Level-Order Traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。 @@ -326,7 +326,7 @@ comments: true **空间复杂度**:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,占用 $O(n)$ 空间。 -## 7.2.2.   前序、中序、后序遍历 +## 7.2.2   前序、中序、后序遍历 相应地,前序、中序和后序遍历都属于「深度优先遍历 Depth-First Traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。 diff --git a/chapter_tree/index.md b/chapter_tree/index.md index 1a2ce4290..7d7bc4649 100644 --- a/chapter_tree/index.md +++ b/chapter_tree/index.md @@ -3,7 +3,7 @@ comments: true icon: material/graph-outline --- -# 7.   树 +# 第 7 章   树
diff --git a/chapter_tree/summary.md b/chapter_tree/summary.md index 482becedd..a7bb668f7 100644 --- a/chapter_tree/summary.md +++ b/chapter_tree/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 7.6.   小结 +# 7.6   小结 - 二叉树是一种非线性数据结构,体现“一分为二”的分治逻辑。每个二叉树节点包含一个值以及两个指针,分别指向其左子节点和右子节点。 - 对于二叉树中的某个节点,其左(右)子节点及其以下形成的树被称为该节点的左(右)子树。 @@ -16,7 +16,7 @@ comments: true - AVL 树,也称为平衡二叉搜索树,它通过旋转操作,确保在不断插入和删除节点后,树仍然保持平衡。 - AVL 树的旋转操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或删除节点后,AVL 树会从底向顶执行旋转操作,使树重新恢复平衡。 -## 7.6.1.   Q & A +## 7.6.1   Q & A !!! question "对于只有一个节点的二叉树,树的高度和根节点的深度都是 $0$ 吗?"