mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-26 23:46:29 +08:00
build
This commit is contained in:
parent
0c64fc7315
commit
5a68f683b9
26 changed files with 93 additions and 93 deletions
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 14.2. 一起参与创作
|
||||
# 13.2. 一起参与创作
|
||||
|
||||
!!! success "开源的魅力"
|
||||
|
||||
|
@ -10,7 +10,7 @@ comments: true
|
|||
|
||||
由于作者能力有限,书中难免存在一些遗漏和错误,请您谅解。如果您发现了笔误、失效链接、内容缺失、文字歧义、解释不清晰或行文结构不合理等问题,请协助我们进行修正,以帮助其他读者获得更优质的学习资源。所有[撰稿人](https://github.com/krahets/hello-algo/graphs/contributors)将在仓库和网站主页上展示,以感谢他们对开源社区的无私奉献!
|
||||
|
||||
## 14.2.1. 内容微调
|
||||
## 13.2.1. 内容微调
|
||||
|
||||
在每个页面的右上角有一个「编辑」图标,您可以按照以下步骤修改文本或代码:
|
||||
|
||||
|
@ -24,7 +24,7 @@ comments: true
|
|||
|
||||
由于图片无法直接修改,因此需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述图片问题,我们会尽快重新绘制并替换图片。
|
||||
|
||||
## 14.2.2. 内容创作
|
||||
## 13.2.2. 内容创作
|
||||
|
||||
如果您有兴趣参与此开源项目,包括将代码翻译成其他编程语言、扩展文章内容等,那么需要实施 Pull Request 工作流程:
|
||||
|
||||
|
@ -34,7 +34,7 @@ comments: true
|
|||
4. 将本地所做更改 Commit ,然后 Push 至远程仓库;
|
||||
5. 刷新仓库网页,点击“Create pull request”按钮即可发起拉取请求;
|
||||
|
||||
## 14.2.3. Docker 部署
|
||||
## 13.2.3. Docker 部署
|
||||
|
||||
我们可以通过 Docker 来部署本项目。执行以下脚本,稍等片刻后,即可使用浏览器打开 `http://localhost:8000` 来访问本项目。
|
||||
|
||||
|
|
|
@ -2,52 +2,52 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 14.1. 编程环境安装
|
||||
# 13.1. 编程环境安装
|
||||
|
||||
## 14.1.1. VSCode
|
||||
## 13.1.1. VSCode
|
||||
|
||||
本书推荐使用开源轻量的 VSCode 作为本地 IDE ,下载并安装 [VSCode](https://code.visualstudio.com/) 。
|
||||
|
||||
## 14.1.2. Java 环境
|
||||
## 13.1.2. Java 环境
|
||||
|
||||
1. 下载并安装 [OpenJDK](https://jdk.java.net/18/)(版本需满足 > JDK 9)。
|
||||
2. 在 VSCode 的插件市场中搜索 `java` ,安装 Extension Pack for Java 。
|
||||
|
||||
## 14.1.3. C/C++ 环境
|
||||
## 13.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 }` 。
|
||||
|
||||
## 14.1.4. Python 环境
|
||||
## 13.1.4. Python 环境
|
||||
|
||||
1. 下载并安装 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) 。
|
||||
2. 在 VSCode 的插件市场中搜索 `python` ,安装 Python Extension Pack 。
|
||||
3. (可选)在命令行输入 `pip install black` ,安装代码格式化工具。
|
||||
|
||||
## 14.1.5. Go 环境
|
||||
## 13.1.5. Go 环境
|
||||
|
||||
1. 下载并安装 [go](https://go.dev/dl/) 。
|
||||
2. 在 VSCode 的插件市场中搜索 `go` ,安装 Go 。
|
||||
3. 快捷键 `Ctrl + Shift + P` 呼出命令栏,输入 go ,选择 `Go: Install/Update Tools` ,全部勾选并安装即可。
|
||||
|
||||
## 14.1.6. JavaScript 环境
|
||||
## 13.1.6. JavaScript 环境
|
||||
|
||||
1. 下载并安装 [node.js](https://nodejs.org/en/) 。
|
||||
2. 在 VSCode 的插件市场中搜索 `javascript` ,安装 JavaScript (ES6) code snippets 。
|
||||
3. (可选)在 VSCode 的插件市场中搜索 `Prettier` ,安装代码格式化工具。
|
||||
|
||||
## 14.1.7. C# 环境
|
||||
## 13.1.7. C# 环境
|
||||
|
||||
1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) ;
|
||||
2. 在 VSCode 的插件市场中搜索 `c#` ,安装 c# 。
|
||||
|
||||
## 14.1.8. Swift 环境
|
||||
## 13.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)。
|
||||
|
||||
## 14.1.9. Rust 环境
|
||||
## 13.1.9. 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)。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 13.1. 回溯算法
|
||||
# 12.1. 回溯算法
|
||||
|
||||
「回溯算法 Backtracking Algorithm」是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。
|
||||
|
||||
|
@ -161,7 +161,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 在前序遍历中搜索节点 </p>
|
||||
|
||||
## 13.1.1. 尝试与回退
|
||||
## 12.1.1. 尝试与回退
|
||||
|
||||
**之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略**。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。
|
||||
|
||||
|
@ -389,7 +389,7 @@ comments: true
|
|||
=== "<11>"
|
||||
![preorder_find_paths_step11](backtracking_algorithm.assets/preorder_find_paths_step11.png)
|
||||
|
||||
## 13.1.2. 剪枝
|
||||
## 12.1.2. 剪枝
|
||||
|
||||
复杂的回溯问题通常包含一个或多个约束条件,**约束条件通常可用于“剪枝”**。
|
||||
|
||||
|
@ -592,7 +592,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 根据约束条件剪枝 </p>
|
||||
|
||||
## 13.1.3. 常用术语
|
||||
## 12.1.3. 常用术语
|
||||
|
||||
为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。
|
||||
|
||||
|
@ -609,7 +609,7 @@ comments: true
|
|||
|
||||
解、状态、约束条件等术语是通用的,适用于回溯算法、动态规划、贪心算法等。
|
||||
|
||||
## 13.1.4. 框架代码
|
||||
## 12.1.4. 框架代码
|
||||
|
||||
回溯算法可用于解决许多搜索问题、约束满足问题和组合优化问题。为提升代码通用性,我们希望将回溯算法的“尝试、回退、剪枝”的主体框架提炼出来。
|
||||
|
||||
|
@ -1283,7 +1283,7 @@ comments: true
|
|||
|
||||
相较于基于前序遍历的实现代码,基于回溯算法框架的实现代码虽然显得啰嗦,但通用性更好。实际上,**所有回溯问题都可以在该框架下解决**。我们需要根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法。
|
||||
|
||||
## 13.1.5. 典型例题
|
||||
## 12.1.5. 典型例题
|
||||
|
||||
**搜索问题**:这类问题的目标是找到满足特定条件的解决方案。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 13.3. N 皇后问题
|
||||
# 12.3. N 皇后问题
|
||||
|
||||
!!! question "根据国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。给定 $n$ 个皇后和一个 $n \times n$ 大小的棋盘,寻找使得所有皇后之间无法相互攻击的摆放方案。"
|
||||
|
||||
|
@ -486,7 +486,7 @@ comments: true
|
|||
[class]{}-[func]{nQueens}
|
||||
```
|
||||
|
||||
## 13.3.1. 复杂度分析
|
||||
## 12.3.1. 复杂度分析
|
||||
|
||||
逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n, n-1, \cdots, 2, 1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 13.2. 全排列问题
|
||||
# 12.2. 全排列问题
|
||||
|
||||
全排列问题是回溯算法的一个典型应用。它的定义是在给定一个集合(如一个数组或字符串)的情况下,找出这个集合中元素的所有可能的排列。
|
||||
|
||||
|
@ -18,7 +18,7 @@ comments: true
|
|||
|
||||
</div>
|
||||
|
||||
## 13.2.1. 无重复的情况
|
||||
## 12.2.1. 无重复的情况
|
||||
|
||||
!!! question "输入一个整数数组,数组中不包含重复元素,返回所有可能的排列。"
|
||||
|
||||
|
@ -342,7 +342,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 全排列剪枝示例 </p>
|
||||
|
||||
## 13.2.2. 考虑重复的情况
|
||||
## 12.2.2. 考虑重复的情况
|
||||
|
||||
!!! question "输入一个整数数组,**数组中可能包含重复元素**,返回所有不重复的排列。"
|
||||
|
||||
|
@ -690,7 +690,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 两种剪枝条件的作用范围 </p>
|
||||
|
||||
## 13.2.3. 复杂度分析
|
||||
## 12.2.3. 复杂度分析
|
||||
|
||||
假设元素两两之间互不相同,则 $n$ 个元素共有 $n!$ 种排列(阶乘);在记录结果时,需要复制长度为 $n$ 的列表,使用 $O(n)$ 时间。因此,**时间复杂度为 $O(n!n)$** 。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.1. 图
|
||||
# 9.1. 图
|
||||
|
||||
「图 Graph」是一种非线性数据结构,由「顶点 Vertex」和「边 Edge」组成。我们可以将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。以下示例展示了一个包含 5 个顶点和 7 条边的图。
|
||||
|
||||
|
@ -20,7 +20,7 @@ $$
|
|||
|
||||
那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作节点,把「边」看作连接各个节点的指针,则可将「图」看作是一种从「链表」拓展而来的数据结构。**相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,从而更为复杂**。
|
||||
|
||||
## 10.1.1. 图常见类型
|
||||
## 9.1.1. 图常见类型
|
||||
|
||||
根据边是否具有方向,可分为「无向图 Undirected Graph」和「有向图 Directed Graph」。
|
||||
|
||||
|
@ -46,13 +46,13 @@ $$
|
|||
|
||||
<p align="center"> Fig. 有权图与无权图 </p>
|
||||
|
||||
## 10.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」表示有多少条边从该顶点指出。
|
||||
|
||||
## 10.1.3. 图的表示
|
||||
## 9.1.3. 图的表示
|
||||
|
||||
图的常用表示方法包括「邻接矩阵」和「邻接表」。以下使用无向图进行举例。
|
||||
|
||||
|
@ -86,7 +86,7 @@ $$
|
|||
|
||||
观察上图可发现,**邻接表结构与哈希表中的「链地址法」非常相似,因此我们也可以采用类似方法来优化效率**。例如,当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;此外,还可以将链表转换为哈希表,将时间复杂度降低至 $O(1)$ 。
|
||||
|
||||
## 10.1.4. 图常见应用
|
||||
## 9.1.4. 图常见应用
|
||||
|
||||
实际应用中,许多系统都可以用图来建模,相应的待求解问题也可以约化为图计算问题。
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.2. 图基础操作
|
||||
# 9.2. 图基础操作
|
||||
|
||||
图的基础操作可分为对「边」的操作和对「顶点」的操作。在「邻接矩阵」和「邻接表」两种表示方法下,实现方式有所不同。
|
||||
|
||||
## 10.2.1. 基于邻接矩阵的实现
|
||||
## 9.2.1. 基于邻接矩阵的实现
|
||||
|
||||
给定一个顶点数量为 $n$ 的无向图,则有:
|
||||
|
||||
|
@ -762,7 +762,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 10.2.2. 基于邻接表的实现
|
||||
## 9.2.2. 基于邻接表的实现
|
||||
|
||||
设无向图的顶点总数为 $n$ 、边总数为 $m$ ,则有:
|
||||
|
||||
|
@ -1451,7 +1451,7 @@ comments: true
|
|||
[class]{GraphAdjList}-[func]{}
|
||||
```
|
||||
|
||||
## 10.2.3. 效率对比
|
||||
## 9.2.3. 效率对比
|
||||
|
||||
设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.3. 图的遍历
|
||||
# 9.3. 图的遍历
|
||||
|
||||
!!! note "图与树的关系"
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
与树类似,图的遍历方式也可分为两种,即「广度优先遍历 Breadth-First Traversal」和「深度优先遍历 Depth-First Traversal」,也称为「广度优先搜索 Breadth-First Search」和「深度优先搜索 Depth-First Search」,简称 BFS 和 DFS。
|
||||
|
||||
## 10.3.1. 广度优先遍历
|
||||
## 9.3.1. 广度优先遍历
|
||||
|
||||
**广度优先遍历是一种由近及远的遍历方式,从距离最近的顶点开始访问,并一层层向外扩张**。具体来说,从某个顶点出发,先遍历该顶点的所有邻接顶点,然后遍历下一个顶点的所有邻接顶点,以此类推,直至所有顶点访问完毕。
|
||||
|
||||
|
@ -332,7 +332,7 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质
|
|||
|
||||
**空间复杂度:** 列表 `res` ,哈希表 `visited` ,队列 `que` 中的顶点数量最多为 $|V|$ ,使用 $O(|V|)$ 空间。
|
||||
|
||||
## 10.3.2. 深度优先遍历
|
||||
## 9.3.2. 深度优先遍历
|
||||
|
||||
**深度优先遍历是一种优先走到底、无路可走再回头的遍历方式**。具体地,从某个顶点出发,访问当前顶点的某个邻接顶点,直到走到尽头时返回,再继续走到尽头并返回,以此类推,直至所有顶点遍历完成。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.4. 小结
|
||||
# 9.4. 小结
|
||||
|
||||
- 图由顶点和边组成,可以被表示为一组顶点和一组边构成的集合。
|
||||
- 相较于线性关系(链表)和分治关系(树),网络关系(图)具有更高的自由度,因而更为复杂。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.2. 哈希冲突
|
||||
# 6.2. 哈希冲突
|
||||
|
||||
在理想情况下,哈希函数应为每个输入生成唯一的输出,实现 key 和 value 的一一对应。然而实际上,向哈希函数输入不同的 key 却产生相同输出的情况是存在的,这种现象被称为「哈希冲突 Hash Collision」。哈希冲突可能导致查询结果错误,从而严重影响哈希表的可用性。
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
另一方面,**可以考虑优化哈希表的表示以缓解哈希冲突**,常用方法包括「链式地址 Separate Chaining」和「开放寻址 Open Addressing」。
|
||||
|
||||
## 7.2.1. 哈希表扩容
|
||||
## 6.2.1. 哈希表扩容
|
||||
|
||||
哈希函数的最后一步通常是对桶数量 $n$ 取余,作用是将哈希值映射到桶索引范围,从而将 key 放入对应的桶中。当哈希表容量越大(即 $n$ 越大)时,多个 key 被分配到同一个桶中的概率就越低,冲突就越少。
|
||||
|
||||
|
@ -20,7 +20,7 @@ comments: true
|
|||
|
||||
编程语言通常使用「负载因子 Load Factor」来衡量哈希冲突的严重程度,**定义为哈希表中元素数量除以桶数量**,常作为哈希表扩容的触发条件。在 Java 中,当负载因子 $> 0.75$ 时,系统会将 HashMap 容量扩展为原先的 $2$ 倍。
|
||||
|
||||
## 7.2.2. 链式地址
|
||||
## 6.2.2. 链式地址
|
||||
|
||||
在原始哈希表中,每个桶仅能存储一个键值对。**链式地址将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中**。
|
||||
|
||||
|
@ -41,7 +41,7 @@ comments: true
|
|||
|
||||
为了提高操作效率,**可以将链表转换为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。
|
||||
|
||||
## 7.2.3. 开放寻址
|
||||
## 6.2.3. 开放寻址
|
||||
|
||||
「开放寻址」方法不引入额外的数据结构,而是通过“多次探测”来解决哈希冲突,**探测方主要包括线性探测、平方探测、多次哈希**。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.1. 哈希表
|
||||
# 6.1. 哈希表
|
||||
|
||||
哈希表通过建立「键 key」与「值 value」之间的映射,实现高效的元素查询。具体而言,我们向哈希表输入一个 key,则可以在 $O(1)$ 时间内获取对应的 value 。
|
||||
|
||||
|
@ -26,7 +26,7 @@ comments: true
|
|||
|
||||
</div>
|
||||
|
||||
## 7.1.1. 哈希表常用操作
|
||||
## 6.1.1. 哈希表常用操作
|
||||
|
||||
哈希表的基本操作包括 **初始化、查询操作、添加与删除键值对**。
|
||||
|
||||
|
@ -387,7 +387,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 7.1.2. 哈希函数
|
||||
## 6.1.2. 哈希函数
|
||||
|
||||
哈希表的底层实现为数组,同时可能包含链表、二叉树(红黑树)等数据结构,以提高查询性能(将在下节讨论)。
|
||||
|
||||
|
@ -1263,7 +1263,7 @@ $$
|
|||
}
|
||||
```
|
||||
|
||||
## 7.1.3. 哈希冲突
|
||||
## 6.1.3. 哈希冲突
|
||||
|
||||
细心的你可能已经注意到,**在某些情况下,哈希函数 $f(x) = x \bmod 100$ 可能无法正常工作**。具体来说,当输入的 key 后两位相同时,哈希函数的计算结果也会相同,从而指向同一个 value 。例如,查询学号为 $12836$ 和 $20336$ 的两个学生时,我们得到:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.3. 小结
|
||||
# 6.3. 小结
|
||||
|
||||
- 哈希表能够在 $O(1)$ 时间内将键 key 映射到值 value,效率非常高。
|
||||
- 常见的哈希表操作包括查询、添加与删除键值对、遍历键值对等。
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 9.2. 建堆操作 *
|
||||
# 8.2. 建堆操作 *
|
||||
|
||||
如果我们想要根据输入列表生成一个堆,这个过程被称为「建堆」。
|
||||
|
||||
## 9.2.1. 借助入堆方法实现
|
||||
## 8.2.1. 借助入堆方法实现
|
||||
|
||||
最直接的方法是借助“元素入堆操作”实现,首先创建一个空堆,然后将列表元素依次添加到堆中。
|
||||
|
||||
设元素数量为 $n$ ,则最后一个元素入堆的时间复杂度为 $O(\log n)$ 。在依次添加元素时,堆的平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。
|
||||
|
||||
## 9.2.2. 基于堆化操作实现
|
||||
## 8.2.2. 基于堆化操作实现
|
||||
|
||||
有趣的是,存在一种更高效的建堆方法,其时间复杂度仅为 $O(n)$ 。我们先将列表所有元素原封不动添加到堆中,**然后迭代地对各个节点执行“从顶至底堆化”**。当然,**我们不需要对叶节点执行堆化操作**,因为它们没有子节点。
|
||||
|
||||
|
@ -162,7 +162,7 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
## 9.2.3. 复杂度分析
|
||||
## 8.2.3. 复杂度分析
|
||||
|
||||
为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 9.1. 堆
|
||||
# 8.1. 堆
|
||||
|
||||
「堆 Heap」是一种满足特定条件的完全二叉树,可分为两种类型:
|
||||
|
||||
|
@ -19,7 +19,7 @@ comments: true
|
|||
- 我们将二叉树的根节点称为「堆顶」,将底层最靠右的节点称为「堆底」。
|
||||
- 对于大顶堆(小顶堆),堆顶元素(即根节点)的值分别是最大(最小)的。
|
||||
|
||||
## 9.1.1. 堆常用操作
|
||||
## 8.1.1. 堆常用操作
|
||||
|
||||
需要指出的是,许多编程语言提供的是「优先队列 Priority Queue」,这是一种抽象数据结构,定义为具有优先级排序的队列。
|
||||
|
||||
|
@ -307,7 +307,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 9.1.2. 堆的实现
|
||||
## 8.1.2. 堆的实现
|
||||
|
||||
下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。
|
||||
|
||||
|
@ -1345,7 +1345,7 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
## 9.1.3. 堆常见应用
|
||||
## 8.1.3. 堆常见应用
|
||||
|
||||
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。
|
||||
- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后依次将所有元素弹出,从而得到一个有序序列。当然,堆排序的实现方法并不需要弹出元素,而是每轮将堆顶元素交换至数组尾部并缩小堆的长度。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 9.3. 小结
|
||||
# 8.3. 小结
|
||||
|
||||
- 堆是一棵完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素是最大(小)的。
|
||||
- 优先队列的定义是具有出队优先级的队列,通常使用堆来实现。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 6.1. 二分查找
|
||||
# 10.1. 二分查找
|
||||
|
||||
「二分查找 Binary Search」是一种基于分治思想的高效搜索算法。它利用数据的有序性,每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。
|
||||
|
||||
|
@ -285,7 +285,7 @@ comments: true
|
|||
|
||||
空间复杂度为 $O(1)$ 。指针 `i` , `j` 使用常数大小空间。
|
||||
|
||||
## 6.1.1. 区间表示方法
|
||||
## 10.1.1. 区间表示方法
|
||||
|
||||
除了上述的双闭区间外,常见的区间表示还有“左闭右开”区间,定义为 $[0, n)$ ,即左边界包含自身,右边界不包含自身。在该表示下,区间 $[i, j]$ 在 $i = j$ 时为空。
|
||||
|
||||
|
@ -530,7 +530,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 两种区间定义 </p>
|
||||
|
||||
## 6.1.2. 优点与局限性
|
||||
## 10.1.2. 优点与局限性
|
||||
|
||||
二分查找在时间和空间方面都有较好的性能:
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 6.2. 二分查找边界
|
||||
# 10.2. 二分查找边界
|
||||
|
||||
上一节规定目标元素在数组中是唯一的。如果目标元素在数组中多次出现,上节介绍的方法只能保证返回其中一个目标元素的索引,**而无法确定该索引的左边和右边还有多少目标元素**。
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 线性查找最左元素 </p>
|
||||
|
||||
## 6.2.1. 查找最左一个元素
|
||||
## 10.2.1. 查找最左一个元素
|
||||
|
||||
!!! question "查找并返回元素 `target` 在有序数组 `nums` 中首次出现的索引。若数组中不包含该元素,则返回 $-1$ 。数组可能包含重复元素。"
|
||||
|
||||
|
@ -156,7 +156,7 @@ comments: true
|
|||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
```
|
||||
|
||||
## 6.2.2. 查找最右一个元素
|
||||
## 10.2.2. 查找最右一个元素
|
||||
|
||||
类似地,我们也可以二分查找最右一个元素。设首个大于 `target` 的元素为 `rightarget` 。
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 12.2. 哈希优化策略
|
||||
# 10.3. 哈希优化策略
|
||||
|
||||
在算法题中,**我们常通过将线性查找替换为哈希查找来降低算法的时间复杂度**。我们借助一个算法题来加深理解。
|
||||
|
||||
|
@ -10,7 +10,7 @@ comments: true
|
|||
|
||||
给定一个整数数组 `nums` 和一个整数目标值 `target` ,请在数组中搜索“和”为目标值 `target` 的两个整数,并返回他们在数组中的索引。注意,数组中同一个元素在答案里不能重复出现。返回任意一个解即可。
|
||||
|
||||
## 12.2.1. 线性查找:以时间换空间
|
||||
## 10.3.1. 线性查找:以时间换空间
|
||||
|
||||
考虑直接遍历所有可能的组合。开启一个两层循环,在每轮中判断两个整数的和是否为 `target` ,若是,则返回它们的索引。
|
||||
|
||||
|
@ -195,7 +195,7 @@ comments: true
|
|||
|
||||
此方法的时间复杂度为 $O(n^2)$ ,空间复杂度为 $O(1)$ ,在大数据量下非常耗时。
|
||||
|
||||
## 12.2.2. 哈希查找:以空间换时间
|
||||
## 10.3.2. 哈希查找:以空间换时间
|
||||
|
||||
考虑借助一个哈希表,键值对分别为数组元素和元素索引。循环遍历数组,每轮执行:
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 12.1. 搜索算法
|
||||
# 10.4. 搜索算法
|
||||
|
||||
「搜索算法 Searching Algorithm」用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。
|
||||
|
||||
我们已经学过数组、链表、树和图的遍历方法,也学过哈希表、二叉搜索树等可用于实现查询的复杂数据结构。因此,搜索算法对于我们来说并不陌生。在本节,我们将从更加系统的视角切入,重新审视搜索算法。
|
||||
|
||||
## 12.1.1. 暴力搜索
|
||||
## 10.4.1. 暴力搜索
|
||||
|
||||
暴力搜索通过遍历数据结构的每个元素来定位目标元素。
|
||||
|
||||
|
@ -19,7 +19,7 @@ comments: true
|
|||
|
||||
然而,**此类算法的时间复杂度为 $O(n)$** ,其中 $n$ 为元素数量,因此在数据量较大的情况下性能较差。
|
||||
|
||||
## 12.1.2. 自适应搜索
|
||||
## 10.4.2. 自适应搜索
|
||||
|
||||
自适应搜索利用数据的特有属性(例如有序性)来优化搜索过程,从而更高效地定位目标元素。
|
||||
|
||||
|
@ -35,7 +35,7 @@ comments: true
|
|||
|
||||
自适应搜索算法常被称为查找算法,**主要关注在特定数据结构中快速检索目标元素**。
|
||||
|
||||
## 12.1.3. 搜索方法选取
|
||||
## 10.4.3. 搜索方法选取
|
||||
|
||||
给定大小为 $n$ 的一组数据,我们可以使用线性搜索、二分查找、树查找、哈希查找等多种方法在该数据中搜索目标元素。各个方法的工作原理如下图所示。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 12.3. 小结
|
||||
# 10.5. 小结
|
||||
|
||||
- 二分查找依赖于数据的有序性,通过循环逐步缩减一半搜索区间来实现查找。它要求输入数据有序,且仅适用于数组或基于数组实现的数据结构。
|
||||
- 暴力搜索通过遍历数据结构来定位数据。线性搜索适用于数组和链表,广度优先搜索和深度优先搜索适用于图和树。此类算法通用性好,无需对数据预处理,但时间复杂度 $O(n)$ 较高。
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 8.3. 二叉树数组表示
|
||||
# 7.3. 二叉树数组表示
|
||||
|
||||
在链表表示下,二叉树的存储单元为节点 `TreeNode` ,节点之间通过指针相连接。在上节中,我们学习了在链表表示下的二叉树的各项基本操作。
|
||||
|
||||
那么,能否用「数组」来表示二叉树呢?答案是肯定的。
|
||||
|
||||
## 8.3.1. 表示完美二叉树
|
||||
## 7.3.1. 表示完美二叉树
|
||||
|
||||
先分析一个简单案例,给定一个完美二叉树,我们将节点按照层序遍历的顺序编号(从 $0$ 开始),此时每个节点都对应唯一的索引。
|
||||
|
||||
|
@ -20,7 +20,7 @@ comments: true
|
|||
|
||||
**映射公式的作用相当于链表中的指针**。如果我们将节点按照层序遍历的顺序存储在一个数组中,那么对于数组中的任意节点,我们都可以通过映射公式来访问其子节点。
|
||||
|
||||
## 8.3.2. 表示任意二叉树
|
||||
## 7.3.2. 表示任意二叉树
|
||||
|
||||
然而,完美二叉树只是一个特例。在二叉树的中间层,通常存在许多 $\text{null}$ ,而层序遍历序列并不包含这些 $\text{null}$ 。我们无法仅凭该序列来推测 $\text{null}$ 的数量和分布位置,**这意味着存在多种二叉树结构都符合该层序遍历序列**。显然在这种情况下,上述的数组表示方法已经失效。
|
||||
|
||||
|
@ -112,7 +112,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
|
||||
|
||||
## 8.3.3. 优势与局限性
|
||||
## 7.3.3. 优势与局限性
|
||||
|
||||
二叉树的数组表示存在以下优点:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 8.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 树能始终保持高效的数据操作性能,具有很好的应用价值。
|
||||
|
||||
## 8.5.1. AVL 树常见术语
|
||||
## 7.5.1. AVL 树常见术语
|
||||
|
||||
「AVL 树」既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为「平衡二叉搜索树」。
|
||||
|
||||
|
@ -494,7 +494,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
|
|||
|
||||
设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。
|
||||
|
||||
## 8.5.2. AVL 树旋转
|
||||
## 7.5.2. AVL 树旋转
|
||||
|
||||
AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,**旋转操作既能保持树的「二叉搜索树」属性,也能使树重新变为「平衡二叉树」**。
|
||||
|
||||
|
@ -1275,7 +1275,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
}
|
||||
```
|
||||
|
||||
## 8.5.3. AVL 树常用操作
|
||||
## 7.5.3. AVL 树常用操作
|
||||
|
||||
### 插入节点
|
||||
|
||||
|
@ -2025,7 +2025,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
|
||||
AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。
|
||||
|
||||
## 8.5.4. AVL 树典型应用
|
||||
## 7.5.4. AVL 树典型应用
|
||||
|
||||
- 组织和存储大型数据,适用于高频查找、低频增删的场景;
|
||||
- 用于构建数据库中的索引系统;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 8.4. 二叉搜索树
|
||||
# 7.4. 二叉搜索树
|
||||
|
||||
「二叉搜索树 Binary Search Tree」满足以下条件:
|
||||
|
||||
|
@ -13,7 +13,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 二叉搜索树 </p>
|
||||
|
||||
## 8.4.1. 二叉搜索树的操作
|
||||
## 7.4.1. 二叉搜索树的操作
|
||||
|
||||
### 查找节点
|
||||
|
||||
|
@ -1159,7 +1159,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 二叉搜索树的中序遍历序列 </p>
|
||||
|
||||
## 8.4.2. 二叉搜索树的效率
|
||||
## 7.4.2. 二叉搜索树的效率
|
||||
|
||||
给定一组数据,我们考虑使用数组或二叉搜索树存储。
|
||||
|
||||
|
@ -1183,7 +1183,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 二叉搜索树的平衡与退化 </p>
|
||||
|
||||
## 8.4.3. 二叉搜索树常见应用
|
||||
## 7.4.3. 二叉搜索树常见应用
|
||||
|
||||
- 用作系统中的多级索引,实现高效的查找、插入、删除操作。
|
||||
- 作为某些搜索算法的底层数据结构。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 8.1. 二叉树
|
||||
# 7.1. 二叉树
|
||||
|
||||
「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含一个「值」和两个「指针」。
|
||||
|
||||
|
@ -155,7 +155,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 父节点、子节点、子树 </p>
|
||||
|
||||
## 8.1.1. 二叉树常见术语
|
||||
## 7.1.1. 二叉树常见术语
|
||||
|
||||
二叉树涉及的术语较多,建议尽量理解并记住。
|
||||
|
||||
|
@ -176,7 +176,7 @@ comments: true
|
|||
|
||||
请注意,我们通常将「高度」和「深度」定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。
|
||||
|
||||
## 8.1.2. 二叉树基本操作
|
||||
## 7.1.2. 二叉树基本操作
|
||||
|
||||
**初始化二叉树**。与链表类似,首先初始化节点,然后构建引用指向(即指针)。
|
||||
|
||||
|
@ -459,7 +459,7 @@ comments: true
|
|||
|
||||
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。
|
||||
|
||||
## 8.1.3. 常见二叉树类型
|
||||
## 7.1.3. 常见二叉树类型
|
||||
|
||||
### 完美二叉树
|
||||
|
||||
|
@ -497,7 +497,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 平衡二叉树 </p>
|
||||
|
||||
## 8.1.4. 二叉树的退化
|
||||
## 7.1.4. 二叉树的退化
|
||||
|
||||
当二叉树的每层节点都被填满时,达到「完美二叉树」;而当所有节点都偏向一侧时,二叉树退化为「链表」。
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 8.2. 二叉树遍历
|
||||
# 7.2. 二叉树遍历
|
||||
|
||||
从物理结构的角度来看,树是一种基于链表的数据结构,因此其遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现。
|
||||
|
||||
二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历和后序遍历等。
|
||||
|
||||
## 8.2.1. 层序遍历
|
||||
## 7.2.1. 层序遍历
|
||||
|
||||
「层序遍历 Level-Order Traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
|
||||
|
||||
|
@ -285,7 +285,7 @@ comments: true
|
|||
|
||||
**空间复杂度**:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,占用 $O(n)$ 空间。
|
||||
|
||||
## 8.2.2. 前序、中序、后序遍历
|
||||
## 7.2.2. 前序、中序、后序遍历
|
||||
|
||||
相应地,前序、中序和后序遍历都属于「深度优先遍历 Depth-First Traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 8.6. 小结
|
||||
# 7.6. 小结
|
||||
|
||||
- 二叉树是一种非线性数据结构,体现“一分为二”的分治逻辑。每个二叉树节点包含一个值以及两个指针,分别指向其左子节点和右子节点。
|
||||
- 对于二叉树中的某个节点,其左(右)子节点及其以下形成的树被称为该节点的左(右)子树。
|
||||
|
|
Loading…
Reference in a new issue