mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-24 21:16:28 +08:00
finetune
This commit is contained in:
parent
cf0d4b32ec
commit
d387d9d41d
29 changed files with 85 additions and 58 deletions
|
@ -1,11 +1,13 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 数组和链表是两种基本的数据结构,分别代表数据在计算机内存中的两种存储方式:连续空间存储和离散空间存储。两者的特点呈现出互补的特性。
|
||||
- 数组支持随机访问、占用内存较少;但插入和删除元素效率低,且初始化后长度不可变。
|
||||
- 链表通过更改引用(指针)实现高效的节点插入与删除,且可以灵活调整长度;但节点访问效率低、占用内存较多。常见的链表类型包括单向链表、循环链表、双向链表。
|
||||
- 动态数组,又称列表,是基于数组实现的一种数据结构。它保留了数组的优势,同时可以灵活调整长度。列表的出现极大地提高了数组的易用性,但可能导致部分内存空间浪费。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?"
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 回溯算法本质是穷举法,通过对解空间进行深度优先遍历来寻找符合条件的解。在搜索过程中,遇到满足条件的解则记录,直至找到所有解或遍历完成后结束。
|
||||
- 回溯算法的搜索过程包括尝试与回退两个部分。它通过深度优先搜索来尝试各种选择,当遇到不满足约束条件的情况时,则撤销上一步的选择,退回到之前的状态,并继续尝试其他选择。尝试与回退是两个方向相反的操作。
|
||||
- 回溯问题通常包含多个约束条件,它们可用于实现剪枝操作。剪枝可以提前结束不必要的搜索分支,大幅提升搜索效率。
|
||||
|
@ -10,3 +12,12 @@
|
|||
- 对于子集和问题,数组中的相等元素会产生重复集合。我们利用数组已排序的前置条件,通过判断相邻元素是否相等实现剪枝,从而确保相等元素在每轮中只能被选中一次。
|
||||
- $n$ 皇后旨在寻找将 $n$ 个皇后放置到 $n \times n$ 尺寸棋盘上的方案,要求所有皇后两两之间无法攻击对方。该问题的约束条件有行约束、列约束、主对角线和副对角线约束。为满足行约束,我们采用按行放置的策略,保证每一行放置一个皇后。
|
||||
- 列约束和对角线约束的处理方式类似。对于列约束,我们利用一个数组来记录每一列是否有皇后,从而指示选中的格子是否合法。对于对角线约束,我们借助两个数组来分别记录该主、副对角线是否存在皇后;难点在于找处在到同一主(副)对角线上格子满足的行列索引规律。
|
||||
|
||||
### Q & A
|
||||
|
||||
!!! question "怎么理解回溯和递归的关系?"
|
||||
|
||||
总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。
|
||||
|
||||
- 回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。
|
||||
- 递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
**算法效率评估**
|
||||
|
||||
- 时间效率和空间效率是衡量算法优劣的两个主要评价指标。
|
||||
|
@ -22,7 +24,7 @@
|
|||
- 我们通常只关注最差空间复杂度,即统计算法在最差输入数据和最差运行时间点下的空间复杂度。
|
||||
- 常见空间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$ 和 $O(2^n)$ 等。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "尾递归的空间复杂度是 $O(1)$ 吗?"
|
||||
|
||||
|
|
|
@ -44,4 +44,4 @@
|
|||
|
||||
!!! tip
|
||||
|
||||
如若感觉理解物理结构有困难,建议先阅读下一章“数组与链表”,然后再回顾本节内容。
|
||||
如果你感觉物理结构理解起来有困难,建议先阅读下一章“数组与链表”,然后再回顾本节内容。
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 数据结构可以从逻辑结构和物理结构两个角度进行分类。逻辑结构描述了数据元素之间的逻辑关系,而物理结构描述了数据在计算机内存中的存储方式。
|
||||
- 常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性和非线性结构。
|
||||
- 当程序运行时,数据被存储在计算机内存中。每个内存空间都拥有对应的内存地址,程序通过这些内存地址访问数据。
|
||||
|
@ -11,7 +13,7 @@
|
|||
- ASCII 码是最早出现的英文字符集,长度为 1 字节,共收录 127 个字符。GBK 字符集是常用的中文字符集,共收录两万多个汉字。Unicode 致力于提供一个完整的字符集标准,收录世界内各种语言的字符,从而解决由于字符编码方法不一致而导致的乱码问题。
|
||||
- UTF-8 是最受欢迎的 Unicode 编码方法,通用性非常好。它是一种变长的编码方法,具有很好的扩展性,有效提升了存储空间的使用效率。UTF-16 和 UTF-32 是等长的编码方法。在编码中文时,UTF-16 比 UTF-8 的占用空间更小。Java 和 C# 等编程语言默认使用 UTF-16 编码。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "为什么哈希表同时包含线性数据结构和非线性数据结构?"
|
||||
|
||||
|
|
|
@ -210,4 +210,4 @@
|
|||
|
||||
汉诺塔问题源自一种古老的传说故事。在古印度的一个寺庙里,僧侣们有三根高大的钻石柱子,以及 $64$ 个大小不一的金圆盘。僧侣们不断地移动原盘,他们相信在最后一个圆盘被正确放置的那一刻,这个世界就会结束。
|
||||
|
||||
然而根据以上分析,即使僧侣们每秒钟移动一次,总共需要大约 $2^{64} \approx 1.84×10^{19}$ 秒,合约 $5850$ 亿年,远远超过了现在对宇宙年龄的估计。所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。
|
||||
然而,即使僧侣们每秒钟移动一次,总共需要大约 $2^{64} \approx 1.84×10^{19}$ 秒,合约 $5850$ 亿年,远远超过了现在对宇宙年龄的估计。所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
# 图的遍历
|
||||
|
||||
!!! note "图与树的关系"
|
||||
树代表的是“一对多”的关系,而图则具有更高的自由度,可以表示任意的“多对多”关系。因此,我们可以把树看作是图的一种特例。显然,**树的遍历操作也是图的遍历操作的一种特例**。
|
||||
|
||||
树代表的是“一对多”的关系,而图则具有更高的自由度,可以表示任意的“多对多”关系。因此,我们可以把树看作是图的一种特例。显然,**树的遍历操作也是图的遍历操作的一种特例**,建议你在学习本章节时融会贯通两者的概念与实现方法。
|
||||
|
||||
图和树都是非线性数据结构,都需要使用搜索算法来实现遍历操作。
|
||||
|
||||
与树类似,图的遍历方式也可分为两种,即「广度优先遍历 breadth-first traversal」和「深度优先遍历 depth-first traversal」。它们也被称为「广度优先搜索 breadth-first search」和「深度优先搜索 depth-first search」,简称 BFS 和 DFS 。
|
||||
图和树都都需要应用搜索算法来实现遍历操作。图的遍历方式可分为两种:「广度优先遍历 breadth-first traversal」和「深度优先遍历 depth-first traversal」。它们也常被称为「广度优先搜索 breadth-first search」和「深度优先搜索 depth-first search」,简称 BFS 和 DFS 。
|
||||
|
||||
## 广度优先遍历
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 图由顶点和边组成,可以被表示为一组顶点和一组边构成的集合。
|
||||
- 相较于线性关系(链表)和分治关系(树),网络关系(图)具有更高的自由度,因而更为复杂。
|
||||
- 有向图的边具有方向性,连通图中的任意顶点均可达,有权图的每条边都包含权重变量。
|
||||
|
@ -12,7 +14,7 @@
|
|||
- 图的广度优先遍历是一种由近及远、层层扩张的搜索方式,通常借助队列实现。
|
||||
- 图的深度优先遍历是一种优先走到底、无路可走时再回溯的搜索方式,常基于递归来实现。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "路径的定义是顶点序列还是边序列?"
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@
|
|||
|
||||
!!! quote
|
||||
|
||||
有一篇论文专门讨论了该问题。作者给出了一个 $O(n^3)$ 时间复杂度的算法,用于判断一个硬币组合是否可以使用贪心算法找出任何金额的最优解。
|
||||
有一篇论文给出了一个 $O(n^3)$ 时间复杂度的算法,用于判断一个硬币组合是否可以使用贪心算法找出任何金额的最优解。
|
||||
|
||||
Pearson, David. A polynomial-time algorithm for the change-making problem. Operations Research Letters 33.3 (2005): 231-234.
|
||||
|
||||
|
@ -152,9 +152,9 @@
|
|||
|
||||
贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下列举了一些典型的贪心算法问题。
|
||||
|
||||
1. **硬币找零问题**:在某些硬币组合下,贪心算法总是可以得到最优解。
|
||||
2. **区间调度问题**:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
|
||||
3. **分数背包问题**:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解。
|
||||
4. **股票买卖问题**:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
|
||||
5. **霍夫曼编码**:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最小的两个节点合并,最后得到的霍夫曼树的带权路径长度(即编码长度)最小。
|
||||
6. **Dijkstra 算法**:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。
|
||||
- **硬币找零问题**:在某些硬币组合下,贪心算法总是可以得到最优解。
|
||||
- **区间调度问题**:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
|
||||
- **分数背包问题**:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解。
|
||||
- **股票买卖问题**:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
|
||||
- **霍夫曼编码**:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最小的两个节点合并,最后得到的霍夫曼树的带权路径长度(即编码长度)最小。
|
||||
- **Dijkstra 算法**:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。
|
||||
|
|
|
@ -148,6 +148,6 @@ $$
|
|||
|
||||
使用反证法,只分析 $n \geq 3$ 的情况。
|
||||
|
||||
1. **所有因子 $\leq 3$** :假设最优切分方案中存在 $\geq 4$ 的因子 $x$ ,那么一定可以将其继续划分为 $2(x-2)$ ,从而获得更大的乘积。这与假设矛盾。
|
||||
2. **切分方案不包含 $1$** :假设最优切分方案中存在一个因子 $1$ ,那么它一定可以合并入另外一个因子中,以获取更大乘积。这与假设矛盾。
|
||||
1. **所有因子 $\leq 3$** :假设最优切分方案中存在 $\geq 4$ 的因子 $x$ ,那么一定可以将其继续划分为 $2(x-2)$ ,从而获得更大的乘积。这与假设矛盾。
|
||||
2. **切分方案不包含 $1$** :假设最优切分方案中存在一个因子 $1$ ,那么它一定可以合并入另外一个因子中,以获取更大乘积。这与假设矛盾。
|
||||
3. **切分方案最多包含两个 $2$** :假设最优切分方案中包含三个 $2$ ,那么一定可以替换为两个 $3$ ,乘积更大。这与假设矛盾。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
上节提到,**通常情况下哈希函数的输入空间远大于输出空间**,因此理论上哈希冲突是不可避免的。比如,输入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一数组索引。
|
||||
|
||||
哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为解决该问题,我们可以每当遇到哈希冲突时就进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们可以采用以下思路。
|
||||
哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为解决该问题,我们可以每当遇到哈希冲突时就进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们可以采用以下策略。
|
||||
|
||||
1. 改良哈希表数据结构,**使得哈希表可以在存在哈希冲突时正常工作**。
|
||||
2. 仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。
|
||||
|
@ -103,9 +103,7 @@
|
|||
[class]{HashMapChaining}-[func]{}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
当链表很长时,查询效率 $O(n)$ 很差,**此时可以将链表转换为“AVL 树”或“红黑树”**,从而将查询操作的时间复杂度优化至 $O(\log n)$ 。
|
||||
值得注意的是,当链表很长时,查询效率 $O(n)$ 很差。**此时可以将链表转换为“AVL 树”或“红黑树”**,从而将查询操作的时间复杂度优化至 $O(\log n)$ 。
|
||||
|
||||
## 开放寻址
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 输入 `key` ,哈希表能够在 $O(1)$ 时间内查询到 `value` ,效率非常高。
|
||||
- 常见的哈希表操作包括查询、添加键值对、删除键值对和遍历哈希表等。
|
||||
- 哈希函数将 `key` 映射为数组索引,从而访问对应桶并获取 `value` 。
|
||||
|
@ -14,7 +16,7 @@
|
|||
- 常见的哈希算法包括 MD5、SHA-1、SHA-2 和 SHA3 等。MD5 常用于校验文件完整性,SHA-2 常用于安全应用与协议。
|
||||
- 编程语言通常会为数据类型提供内置哈希算法,用于计算哈希表中的桶索引。通常情况下,只有不可变对象是可哈希的。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "哈希表的时间复杂度为什么不是 $O(n)$ ?"
|
||||
|
||||
|
|
|
@ -656,7 +656,7 @@
|
|||
|
||||
### 堆顶元素出堆
|
||||
|
||||
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤。
|
||||
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。
|
||||
|
||||
1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点)。
|
||||
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 堆是一棵完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素是最大(小)的。
|
||||
- 优先队列的定义是具有出队优先级的队列,通常使用堆来实现。
|
||||
- 堆的常用操作及其对应的时间复杂度包括:元素入堆 $O(\log n)$、堆顶元素出堆 $O(\log n)$ 和访问堆顶元素 $O(1)$ 等。
|
||||
|
@ -8,7 +10,7 @@
|
|||
- 输入 $n$ 个元素并建堆的时间复杂度可以优化至 $O(n)$ ,非常高效。
|
||||
- Top-K 是一个经典算法问题,可以使用堆数据结构高效解决,时间复杂度为 $O(n \log k)$ 。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "数据结构的“堆”与内存管理的“堆”是同一个概念吗?"
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
!!! tip
|
||||
|
||||
当 $k = n$ 时,我们可以得到从大到小的序列,等价于“选择排序”算法。
|
||||
当 $k = n$ 时,我们可以得到完整的有序序列,此时等价于“选择排序”算法。
|
||||
|
||||
## 方法二:排序
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
- 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。
|
||||
- 提供简洁的数据表示和逻辑信息,以便使得算法高效运行。
|
||||
|
||||
**数据结构设计是一个充满权衡的过程**。如果想要在某方面取得提升,往往需要在另一方面作出妥协。
|
||||
**数据结构设计是一个充满权衡的过程**。如果想要在某方面取得提升,往往需要在另一方面作出妥协。下面举两个例子。
|
||||
|
||||
- 链表相较于数组,在数据添加和删除操作上更加便捷,但牺牲了数据访问速度。
|
||||
- 图相较于链表,提供了更丰富的逻辑信息,但需要占用更大的内存空间。
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
- 感谢我的女朋友泡泡作为本书的首位读者,从算法小白的角度提出许多宝贵建议,使得本书更适合新手阅读。
|
||||
- 感谢腾宝、琦宝、飞宝为本书起了一个富有创意的名字,唤起大家写下第一行代码 "Hello World!" 的美好回忆。
|
||||
- 感谢苏潼为本书设计了精美的封面和 LOGO,并在我的强迫症下多次耐心修改。
|
||||
- 感谢 @squidfunk 提供的写作排版建议,以及杰出的开源项目 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。
|
||||
- 感谢 @squidfunk 提供的写作排版建议,以及他开发的开源文档主题 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。
|
||||
|
||||
在写作过程中,我阅读了许多关于数据结构与算法的教材和文章。这些作品为本书提供了优秀的范本,确保了本书内容的准确性与品质。在此感谢所有老师和前辈们的杰出贡献!
|
||||
|
||||
|
|
|
@ -111,10 +111,10 @@
|
|||
|
||||
此方法虽然可用,但其包含线性查找,因此时间复杂度为 $O(n)$ 。当数组中存在很多重复的 `target` 时,该方法效率很低。
|
||||
|
||||
现考虑拓展二分查找代码。如下图所示,整体流程保持不变,每轮先计算中点索引 $m$ ,再判断 `target` 和 `nums[m]` 大小关系。
|
||||
现考虑拓展二分查找代码。如下图所示,整体流程保持不变,每轮先计算中点索引 $m$ ,再判断 `target` 和 `nums[m]` 大小关系,分为以下几种情况。
|
||||
|
||||
1. 当 `nums[m] < target` 或 `nums[m] > target` 时,说明还没有找到 `target` ,因此采用普通二分查找的缩小区间操作,**从而使指针 $i$ 和 $j$ 向 `target` 靠近**。
|
||||
2. 当 `nums[m] == target` 时,说明小于 `target` 的元素在区间 $[i, m - 1]$ 中,因此采用 $j = m - 1$ 来缩小区间,**从而使指针 $j$ 向小于 `target` 的元素靠近**。
|
||||
- 当 `nums[m] < target` 或 `nums[m] > target` 时,说明还没有找到 `target` ,因此采用普通二分查找的缩小区间操作,**从而使指针 $i$ 和 $j$ 向 `target` 靠近**。
|
||||
- 当 `nums[m] == target` 时,说明小于 `target` 的元素在区间 $[i, m - 1]$ 中,因此采用 $j = m - 1$ 来缩小区间,**从而使指针 $j$ 向小于 `target` 的元素靠近**。
|
||||
|
||||
循环完成后,$i$ 指向最左边的 `target` ,$j$ 指向首个小于 `target` 的元素,**因此索引 $i$ 就是插入点**。
|
||||
|
||||
|
|
|
@ -86,12 +86,10 @@
|
|||
[class]{}-[func]{bucket_sort}
|
||||
```
|
||||
|
||||
!!! question "桶排序的适用场景是什么?"
|
||||
## 算法特性
|
||||
|
||||
桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。
|
||||
|
||||
## 算法特性
|
||||
|
||||
- **时间复杂度 $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$ 个元素的额外空间。
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
## 算法流程
|
||||
|
||||
如下图所示,“划分阶段”从顶至底递归地将数组从中点切为两个子数组。
|
||||
如下图所示,“划分阶段”从顶至底递归地将数组从中点切分为两个子数组。
|
||||
|
||||
1. 计算数组中点 `mid` ,递归划分左子数组(区间 `[left, mid]` )和右子数组(区间 `[mid + 1, right]` )。
|
||||
2. 递归执行步骤 `1.` ,直至子数组区间长度为 1 时,终止递归划分。
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 冒泡排序通过交换相邻元素来实现排序。通过添加一个标志位来实现提前返回,我们可以将冒泡排序的最佳时间复杂度优化到 $O(n)$ 。
|
||||
- 插入排序每轮将未排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。
|
||||
- 快速排序基于哨兵划分操作实现排序。在哨兵划分中,有可能每次都选取到最差的基准数,导致时间复杂度劣化至 $O(n^2)$ 。引入中位数基准数或随机基准数可以降低这种劣化的概率。尾递归方法可以有效地减少递归深度,将空间复杂度优化到 $O(\log n)$ 。
|
||||
|
@ -12,7 +14,7 @@
|
|||
|
||||
![排序算法对比](summary.assets/sorting_algorithms_comparison.png)
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "排序算法稳定性在什么情况下是必须的?"
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 栈是一种遵循先入后出原则的数据结构,可通过数组或链表来实现。
|
||||
- 从时间效率角度看,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会降低至 $O(n)$ 。相比之下,基于链表实现的栈具有更为稳定的效率表现。
|
||||
- 在空间效率方面,栈的数组实现可能导致一定程度的空间浪费。但需要注意的是,链表节点所占用的内存空间比数组元素更大。
|
||||
- 队列是一种遵循先入先出原则的数据结构,同样可以通过数组或链表来实现。在时间效率和空间效率的对比上,队列的结论与前述栈的结论相似。
|
||||
- 双向队列是一种具有更高自由度的队列,它允许在两端进行元素的添加和删除操作。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "浏览器的前进后退是否是双向链表实现?"
|
||||
|
||||
|
|
|
@ -863,7 +863,4 @@ AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。
|
|||
|
||||
- 组织和存储大型数据,适用于高频查找、低频增删的场景。
|
||||
- 用于构建数据库中的索引系统。
|
||||
|
||||
!!! question "为什么红黑树比 AVL 树更受欢迎?"
|
||||
|
||||
红黑树的平衡条件相对宽松,因此在红黑树中插入与删除节点所需的旋转操作相对较少,在节点增删操作上的平均效率高于 AVL 树。
|
||||
- 红黑树在许多应用中比 AVL 树更受欢迎。这是因为红黑树的平衡条件相对宽松,在红黑树中插入与删除节点所需的旋转操作相对较少,其节点增删操作的平均效率更高。
|
||||
|
|
|
@ -195,10 +195,11 @@
|
|||
|
||||
### 删除节点
|
||||
|
||||
先在二叉树中查找到目标节点,再将其从二叉树中删除。
|
||||
|
||||
与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。
|
||||
|
||||
1. 在二叉树中执行查找操作,获取待删除节点。
|
||||
2. 根据待删除节点的子节点数量(三种情况),执行对应的删除节点操作。
|
||||
因此,我们需要根据目标节点的子节点数量,共分为 0、1 和 2 这三种情况,执行对应的删除节点操作。
|
||||
|
||||
如下图所示,当待删除节点的度为 $0$ 时,表示该节点是叶节点,可以直接删除。
|
||||
|
||||
|
|
|
@ -182,7 +182,7 @@
|
|||
|
||||
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
|
||||
|
||||
!!! tip "高度与深度的定义"
|
||||
!!! tip
|
||||
|
||||
请注意,我们通常将“高度”和“深度”定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png)
|
||||
|
||||
### 代码实现
|
||||
|
||||
广度优先遍历通常借助“队列”来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。
|
||||
|
||||
=== "Java"
|
||||
|
@ -86,9 +88,10 @@
|
|||
[class]{}-[func]{level_order}
|
||||
```
|
||||
|
||||
**时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
|
||||
### 复杂度分析
|
||||
|
||||
**空间复杂度**:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $(n + 1) / 2$ 个节点,占用 $O(n)$ 空间。
|
||||
- **时间复杂度 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
|
||||
- **空间复杂度 $O(n)$** :在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $(n + 1) / 2$ 个节点,占用 $O(n)$ 空间。
|
||||
|
||||
## 前序、中序、后序遍历
|
||||
|
||||
|
@ -98,6 +101,10 @@
|
|||
|
||||
![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png)
|
||||
|
||||
### 代码实现
|
||||
|
||||
深度优先搜索通常基于递归实现:
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_tree_dfs.java"
|
||||
|
@ -218,13 +225,9 @@
|
|||
[class]{}-[func]{post_order}
|
||||
```
|
||||
|
||||
**时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
|
||||
|
||||
**空间复杂度**:在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统占用 $O(n)$ 栈帧空间。
|
||||
|
||||
!!! note
|
||||
|
||||
我们也可以不使用递归,仅基于迭代实现前、中、后序遍历,有兴趣的同学可以自行实现。
|
||||
深度优先搜索也可以基于迭代实现,有兴趣的同学可以自行研究。
|
||||
|
||||
下图展示了前序遍历二叉树的递归过程,其可分为“递”和“归”两个逆向的部分。
|
||||
|
||||
|
@ -263,3 +266,8 @@
|
|||
|
||||
=== "<11>"
|
||||
![preorder_step11](binary_tree_traversal.assets/preorder_step11.png)
|
||||
|
||||
### 复杂度分析
|
||||
|
||||
- **时间复杂度 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间。
|
||||
- **空间复杂度 $O(n)$** :在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统占用 $O(n)$ 栈帧空间。
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# 小结
|
||||
|
||||
### 重点回顾
|
||||
|
||||
- 二叉树是一种非线性数据结构,体现“一分为二”的分治逻辑。每个二叉树节点包含一个值以及两个指针,分别指向其左子节点和右子节点。
|
||||
- 对于二叉树中的某个节点,其左(右)子节点及其以下形成的树被称为该节点的左(右)子树。
|
||||
- 二叉树的相关术语包括根节点、叶节点、层、度、边、高度和深度等。
|
||||
|
@ -12,7 +14,7 @@
|
|||
- AVL 树,也称为平衡二叉搜索树,它通过旋转操作,确保在不断插入和删除节点后,树仍然保持平衡。
|
||||
- AVL 树的旋转操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或删除节点后,AVL 树会从底向顶执行旋转操作,使树重新恢复平衡。
|
||||
|
||||
## Q & A
|
||||
### Q & A
|
||||
|
||||
!!! question "对于只有一个节点的二叉树,树的高度和根节点的深度都是 $0$ 吗?"
|
||||
|
||||
|
|
Loading…
Reference in a new issue