mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-25 08:46:28 +08:00
Remove the heading numbers
in all the source docs.
This commit is contained in:
parent
15417d2a95
commit
88b00151b0
46 changed files with 257 additions and 172 deletions
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 4.1. 数组
|
||||
# 数组
|
||||
|
||||
「数组 Array」是一种将 **相同类型元素** 存储在 **连续内存空间** 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。
|
||||
|
||||
|
@ -102,7 +102,7 @@ comments: true
|
|||
var nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
## 4.1.1. 数组优点
|
||||
## 数组优点
|
||||
|
||||
**在数组中访问元素非常高效**。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。
|
||||
|
||||
|
@ -179,7 +179,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
[class]{}-[func]{randomAccess}
|
||||
```
|
||||
|
||||
## 4.1.2. 数组缺点
|
||||
## 数组缺点
|
||||
|
||||
**数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
|
||||
|
||||
|
@ -333,7 +333,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
## 4.1.3. 数组常用操作
|
||||
## 数组常用操作
|
||||
|
||||
**数组遍历**。以下介绍两种常用的遍历方法。
|
||||
|
||||
|
@ -459,7 +459,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## 4.1.4. 数组典型应用
|
||||
## 数组典型应用
|
||||
|
||||
**随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 4.2. 链表
|
||||
# 链表
|
||||
|
||||
!!! note "引言"
|
||||
|
||||
|
@ -314,7 +314,7 @@ comments: true
|
|||
n3.next = &n4;
|
||||
```
|
||||
|
||||
## 4.2.1. 链表优点
|
||||
## 链表优点
|
||||
|
||||
**在链表中,插入与删除结点的操作效率高**。例如,如果想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。
|
||||
|
||||
|
@ -402,7 +402,7 @@ comments: true
|
|||
[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
## 4.2.2. 链表缺点
|
||||
## 链表缺点
|
||||
|
||||
**链表访问结点效率低**。上节提到,数组可以在 $O(1)$ 时间下访问任意元素,但链表无法直接访问任意结点。这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 `index` (即第 `index + 1` 个)的结点,那么需要 `index` 次访问操作。
|
||||
|
||||
|
@ -468,7 +468,7 @@ comments: true
|
|||
|
||||
**链表的内存占用多**。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。
|
||||
|
||||
## 4.2.3. 链表常用操作
|
||||
## 链表常用操作
|
||||
|
||||
**遍历链表查找**。遍历链表,查找链表内值为 `target` 的结点,输出结点在链表中的索引。
|
||||
|
||||
|
@ -532,7 +532,7 @@ comments: true
|
|||
[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## 4.2.4. 常见链表类型
|
||||
## 常见链表类型
|
||||
|
||||
**单向链表**。即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的「指针(引用)」两项数据。我们将首个结点称为头结点,尾结点指向 `null` 。
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 4.3. 列表
|
||||
# 列表
|
||||
|
||||
**由于长度不可变,数组的实用性大大降低**。在很多情况下,我们事先并不知道会输入多少数据,这就为数组长度的选择带来了很大困难。长度选小了,需要在添加数据中频繁地扩容数组;长度选大了,又造成内存空间的浪费。
|
||||
|
||||
为了解决此问题,诞生了一种被称为「列表 List」的数据结构。列表可以被理解为长度可变的数组,因此也常被称为「动态数组 Dynamic Array」。列表基于数组实现,继承了数组的优点,同时还可以在程序运行中实时扩容。在列表中,我们可以自由地添加元素,而不用担心超过容量限制。
|
||||
|
||||
## 4.3.1. 列表常用操作
|
||||
## 列表常用操作
|
||||
|
||||
**初始化列表**。我们通常会使用到“无初始值”和“有初始值”的两种初始化方法。
|
||||
|
||||
|
@ -703,7 +703,7 @@ comments: true
|
|||
std.sort.sort(i32, list.items, {}, comptime std.sort.asc(i32));
|
||||
```
|
||||
|
||||
## 4.3.2. 列表简易实现 *
|
||||
## 列表简易实现 *
|
||||
|
||||
为了帮助加深对列表的理解,我们在此提供一个列表的简易版本的实现。需要关注三个核心点:
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 4.4. 小结
|
||||
# 小结
|
||||
|
||||
- 数组和链表是两种基本数据结构,代表了数据在计算机内存中的两种存储方式,即连续空间存储和离散空间存储。两者的优点与缺点呈现出此消彼长的关系。
|
||||
- 数组支持随机访问、内存空间占用小;但插入与删除元素效率低,且初始化后长度不可变。
|
||||
- 链表可通过更改指针实现高效的结点插入与删除,并且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型有单向链表、循环链表、双向链表。
|
||||
- 列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,且可以灵活改变长度。列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费。
|
||||
|
||||
## 4.4.1. 数组 VS 链表
|
||||
## 数组 VS 链表
|
||||
|
||||
<p align="center"> Table. 数组与链表特点对比 </p>
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 2.1. 算法效率评估
|
||||
# 算法效率评估
|
||||
|
||||
## 2.1.1. 算法评价维度
|
||||
## 算法评价维度
|
||||
|
||||
在开始学习算法之前,我们首先要想清楚算法的设计目标是什么,或者说,如何来评判算法的好与坏。整体上看,我们设计算法时追求两个层面的目标。
|
||||
|
||||
|
@ -18,7 +18,7 @@ comments: true
|
|||
|
||||
数据结构与算法追求“运行速度快、占用内存少”,而如何去评价算法效率则是非常重要的问题,因为只有知道如何评价算法,才能去做算法之间的对比分析,以及优化算法设计。
|
||||
|
||||
## 2.1.2. 效率评估方法
|
||||
## 效率评估方法
|
||||
|
||||
### 实际测试
|
||||
|
||||
|
@ -42,7 +42,7 @@ comments: true
|
|||
|
||||
如果感觉对复杂度分析的概念一知半解,无需担心,后续章节会展开介绍。
|
||||
|
||||
## 2.1.3. 复杂度分析重要性
|
||||
## 复杂度分析重要性
|
||||
|
||||
复杂度分析给出一把评价算法效率的“标尺”,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 2.3. 空间复杂度
|
||||
# 空间复杂度
|
||||
|
||||
「空间复杂度 Space Complexity」统计 **算法使用内存空间随着数据量变大时的增长趋势**。这个概念与时间复杂度很类似。
|
||||
|
||||
## 2.3.1. 算法相关空间
|
||||
## 算法相关空间
|
||||
|
||||
算法运行中,使用的内存空间主要有以下几种:
|
||||
|
||||
|
@ -252,7 +252,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 2.3.2. 推算方法
|
||||
## 推算方法
|
||||
|
||||
空间复杂度的推算方法和时间复杂度总体类似,只是从统计“计算操作数量”变为统计“使用空间大小”。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。
|
||||
|
||||
|
@ -554,7 +554,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 2.3.3. 常见类型
|
||||
## 常见类型
|
||||
|
||||
设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 2.4. 权衡时间与空间
|
||||
# 权衡时间与空间
|
||||
|
||||
理想情况下,我们希望算法的时间复杂度和空间复杂度都能够达到最优,而实际上,同时优化时间复杂度和空间复杂度是非常困难的。
|
||||
|
||||
|
@ -10,7 +10,7 @@ comments: true
|
|||
|
||||
大多数情况下,时间都是比空间更宝贵的,只要空间复杂度不要太离谱、能接受就行,**因此以空间换时间最为常用**。
|
||||
|
||||
## 2.4.1. 示例题目 *
|
||||
## 示例题目 *
|
||||
|
||||
以 LeetCode 全站第一题 [两数之和](https://leetcode.cn/problems/two-sum/) 为例。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 2.5. 小结
|
||||
# 小结
|
||||
|
||||
### 算法效率评估
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 2.2. 时间复杂度
|
||||
# 时间复杂度
|
||||
|
||||
## 2.2.1. 统计算法运行时间
|
||||
## 统计算法运行时间
|
||||
|
||||
运行时间能够直观且准确地体现出算法的效率水平。如果我们想要 **准确预估一段代码的运行时间** ,该如何做呢?
|
||||
|
||||
|
@ -161,7 +161,7 @@ $$
|
|||
|
||||
但实际上, **统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,毕竟算法需要跑在各式各样的平台之上。其次,我们很难获知每一种操作的运行时间,这为预估过程带来了极大的难度。
|
||||
|
||||
## 2.2.2. 统计时间增长趋势
|
||||
## 统计时间增长趋势
|
||||
|
||||
「时间复杂度分析」采取了不同的做法,其统计的不是算法运行时间,而是 **算法运行时间随着数据量变大时的增长趋势** 。
|
||||
|
||||
|
@ -381,7 +381,7 @@ $$
|
|||
|
||||
**时间复杂度也存在一定的局限性**。比如,虽然算法 `A` 和 `C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B` 比 `C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。对于以上情况,我们很难仅凭时间复杂度来判定算法效率高低。然而,即使存在这些问题,计算复杂度仍然是评判算法效率的最有效且常用的方法。
|
||||
|
||||
## 2.2.3. 函数渐近上界
|
||||
## 函数渐近上界
|
||||
|
||||
设算法「计算操作数量」为 $T(n)$ ,其是一个关于输入数据大小 $n$ 的函数。例如,以下算法的操作数量为
|
||||
|
||||
|
@ -548,7 +548,7 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得
|
|||
|
||||
渐近上界的数学味儿有点重,如果你感觉没有完全理解,无需担心,因为在实际使用中我们只需要会推算即可,数学意义可以慢慢领悟。
|
||||
|
||||
## 2.2.4. 推算方法
|
||||
## 推算方法
|
||||
|
||||
推算出 $f(n)$ 后,我们就得到时间复杂度 $O(f(n))$ 。那么,如何来确定渐近上界 $f(n)$ 呢?总体分为两步,首先「统计操作数量」,然后「判断渐近上界」。
|
||||
|
||||
|
@ -767,7 +767,7 @@ $$
|
|||
|
||||
</div>
|
||||
|
||||
## 2.2.5. 常见类型
|
||||
## 常见类型
|
||||
|
||||
设输入数据大小为 $n$ ,常见的时间复杂度类型有(从低到高排列)
|
||||
|
||||
|
@ -1528,7 +1528,7 @@ $$
|
|||
|
||||
<p align="center"> Fig. 阶乘阶的时间复杂度 </p>
|
||||
|
||||
## 2.2.6. 最差、最佳、平均时间复杂度
|
||||
## 最差、最佳、平均时间复杂度
|
||||
|
||||
**某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**。举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论:
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 3.2. 数据结构分类
|
||||
# 数据结构分类
|
||||
|
||||
数据结构主要可根据「逻辑结构」和「物理结构」两种角度进行分类。
|
||||
|
||||
## 3.2.1. 逻辑结构:线性与非线性
|
||||
## 逻辑结构:线性与非线性
|
||||
|
||||
**「逻辑结构」反映了数据之间的逻辑关系**。数组和链表的数据按照顺序依次排列,反映了数据间的线性关系;树从顶至底按层级排列,反映了祖先与后代之间的派生关系;图由结点和边组成,反映了复杂网络关系。
|
||||
|
||||
|
@ -19,7 +19,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 线性与非线性数据结构 </p>
|
||||
|
||||
## 3.2.2. 物理结构:连续与离散
|
||||
## 物理结构:连续与离散
|
||||
|
||||
!!! note
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 3.1. 数据与内存
|
||||
# 数据与内存
|
||||
|
||||
## 3.1.1. 基本数据类型
|
||||
## 基本数据类型
|
||||
|
||||
谈到计算机中的数据,我们能够想到文本、图片、视频、语音、3D 模型等等,这些数据虽然组织形式不同,但是有一个共同点,即都是由各种基本数据类型构成的。
|
||||
|
||||
|
@ -134,7 +134,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 3.1.2. 计算机内存
|
||||
## 计算机内存
|
||||
|
||||
在计算机中,内存和硬盘是两种主要的存储硬件设备。「硬盘」主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。「内存」用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 3.3. 小结
|
||||
# 小结
|
||||
|
||||
- 整数 byte, short, int, long 、浮点数 float, double 、字符 char 、布尔 boolean 是计算机中的基本数据类型,占用空间的大小决定了它们的取值范围。
|
||||
- 在程序运行时,数据存储在计算机的内存中。内存中每块空间都有独立的内存地址,程序是通过内存地址来访问数据的。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 9.1. 图
|
||||
# 图
|
||||
|
||||
「图 Graph」是一种非线性数据结构,由「顶点 Vertex」和「边 Edge」组成。我们可将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。例如,以下表示一个包含 5 个顶点和 7 条边的图
|
||||
|
||||
|
@ -18,7 +18,7 @@ $$
|
|||
|
||||
那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**。
|
||||
|
||||
## 9.1.1. 图常见类型
|
||||
## 图常见类型
|
||||
|
||||
根据边是否有方向,分为「无向图 Undirected Graph」和「有向图 Directed Graph」。
|
||||
|
||||
|
@ -38,13 +38,13 @@ $$
|
|||
|
||||
![weighted_graph](graph.assets/weighted_graph.png)
|
||||
|
||||
## 9.1.2. 图常用术语
|
||||
## 图常用术语
|
||||
|
||||
- 「邻接 Adjacency」:当两顶点之间有边相连时,称此两顶点“邻接”。
|
||||
- 「路径 Path」:从顶点 A 到顶点 B 走过的边构成的序列,被称为从 A 到 B 的“路径”。
|
||||
- 「度 Degree」表示一个顶点具有多少条边。对于有向图,「入度 In-Degree」表示有多少条边指向该顶点,「出度 Out-Degree」表示有多少条边从该顶点指出。
|
||||
|
||||
## 9.1.3. 图的表示
|
||||
## 图的表示
|
||||
|
||||
图的常用表示方法有「邻接矩阵」和「邻接表」。以下使用「无向图」来举例。
|
||||
|
||||
|
@ -74,7 +74,7 @@ $$
|
|||
|
||||
观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如,当链表较长时,可以把链表转化为「AVL 树」,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet(即哈希表),将时间复杂度降低至 $O(1)$ ,。
|
||||
|
||||
## 9.1.4. 图常见应用
|
||||
## 图常见应用
|
||||
|
||||
现实中的许多系统都可以使用图来建模,对应的待求解问题也可以被约化为图计算问题。
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 9.2. 图基础操作
|
||||
# 图基础操作
|
||||
|
||||
图的基础操作分为对「边」的操作和对「顶点」的操作,在「邻接矩阵」和「邻接表」这两种表示下的实现方式不同。
|
||||
|
||||
## 9.2.1. 基于邻接矩阵的实现
|
||||
## 基于邻接矩阵的实现
|
||||
|
||||
设图的顶点总数为 $n$ ,则有:
|
||||
|
||||
|
@ -92,7 +92,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 9.2.2. 基于邻接表的实现
|
||||
## 基于邻接表的实现
|
||||
|
||||
设图的顶点总数为 $n$ 、边总数为 $m$ ,则有:
|
||||
|
||||
|
@ -183,7 +183,7 @@ comments: true
|
|||
[class]{GraphAdjList}-[func]{}
|
||||
```
|
||||
|
||||
## 9.2.3. 效率对比
|
||||
## 效率对比
|
||||
|
||||
设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 9.3. 图的遍历
|
||||
# 图的遍历
|
||||
|
||||
!!! note "图与树的关系"
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
类似地,图的遍历方式也分为两种,即「广度优先遍历 Breadth-First Traversal」和「深度优先遍历 Depth-First Travsersal」,也称「广度优先搜索 Breadth-First Search」和「深度优先搜索 Depth-First Search」,简称为 BFS 和 DFS 。
|
||||
|
||||
## 9.3.1. 广度优先遍历
|
||||
## 广度优先遍历
|
||||
|
||||
**广度优先遍历优是一种由近及远的遍历方式,从距离最近的顶点开始访问,并一层层向外扩张**。具体地,从某个顶点出发,先遍历该顶点的所有邻接顶点,随后遍历下个顶点的所有邻接顶点,以此类推……
|
||||
|
||||
|
@ -133,7 +133,7 @@ BFS 常借助「队列」来实现。队列具有“先入先出”的性质,
|
|||
|
||||
**空间复杂度:** 列表 `res` ,哈希表 `visited` ,队列 `que` 中的顶点数量最多为 $|V|$ ,使用 $O(|V|)$ 空间。
|
||||
|
||||
## 9.3.2. 深度优先遍历
|
||||
## 深度优先遍历
|
||||
|
||||
**深度优先遍历是一种优先走到底、无路可走再回头的遍历方式**。具体地,从某个顶点出发,不断地访问当前结点的某个邻接顶点,直到走到尽头时回溯,再继续走到底 + 回溯,以此类推……直至所有顶点遍历完成时结束。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 6.2. 哈希冲突
|
||||
# 哈希冲突
|
||||
|
||||
理想情况下,哈希函数应该为每个输入产生唯一的输出,使得 key 和 value 一一对应。而实际上,往往存在向哈希函数输入不同的 key 而产生相同输出的情况,这种情况被称为「哈希冲突 Hash Collision」。哈希冲突会导致查询结果错误,从而严重影响哈希表的可用性。
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
另一方面,**考虑通过优化数据结构以缓解哈希冲突**,常见的方法有「链式地址」和「开放寻址」。
|
||||
|
||||
## 6.2.1. 哈希表扩容
|
||||
## 哈希表扩容
|
||||
|
||||
「负载因子 Load Factor」定义为 **哈希表中元素数量除以桶槽数量(即数组大小)**,代表哈希冲突的严重程度。
|
||||
|
||||
|
@ -20,7 +20,7 @@ comments: true
|
|||
|
||||
与数组扩容类似,**哈希表扩容操作的开销很大**,因为需要将所有键值对从原哈希表依次移动至新哈希表。
|
||||
|
||||
## 6.2.2. 链式地址
|
||||
## 链式地址
|
||||
|
||||
在原始哈希表中,桶内的每个地址只能存储一个元素(即键值对)。**考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中**。
|
||||
|
||||
|
@ -39,7 +39,7 @@ comments: true
|
|||
|
||||
为了缓解时间效率问题,**可以把「链表」转化为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。
|
||||
|
||||
## 6.2.3. 开放寻址
|
||||
## 开放寻址
|
||||
|
||||
「开放寻址」不引入额外数据结构,而是通过“多次探测”来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希**。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 6.1. 哈希表
|
||||
# 哈希表
|
||||
|
||||
哈希表通过建立「键 key」和「值 value」之间的映射,实现高效的元素查找。具体地,输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 $O(1)$ 。
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 哈希表抽象表示 </p>
|
||||
|
||||
## 6.1.1. 哈希表效率
|
||||
## 哈希表效率
|
||||
|
||||
除了哈希表之外,还可以使用以下数据结构来实现上述查询功能:
|
||||
|
||||
|
@ -33,7 +33,7 @@ comments: true
|
|||
|
||||
</div>
|
||||
|
||||
## 6.1.2. 哈希表常用操作
|
||||
## 哈希表常用操作
|
||||
|
||||
哈希表的基本操作包括 **初始化、查询操作、添加与删除键值对**。
|
||||
|
||||
|
@ -391,7 +391,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 6.1.3. 哈希函数
|
||||
## 哈希函数
|
||||
|
||||
哈希表中存储元素的数据结构被称为「桶 Bucket」,底层实现可能是数组、链表、二叉树(红黑树),或是它们的组合。
|
||||
|
||||
|
@ -492,7 +492,7 @@ $$
|
|||
[class]{ArrayHashMap}-[func]{}
|
||||
```
|
||||
|
||||
## 6.1.4. 哈希冲突
|
||||
## 哈希冲突
|
||||
|
||||
细心的同学可能会发现,**哈希函数 $f(x) = x \% 100$ 会在某些情况下失效**。具体地,当输入的 key 后两位相同时,哈希函数的计算结果也相同,指向同一个 value 。例如,分别查询两个学号 $12836$ 和 $20336$ ,则有
|
||||
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 6.3. 小结
|
||||
# 小结
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 8.1. 堆
|
||||
# 堆
|
||||
|
||||
「堆 Heap」是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型:
|
||||
|
||||
|
@ -11,13 +11,13 @@ comments: true
|
|||
|
||||
![min_heap_and_max_heap](heap.assets/min_heap_and_max_heap.png)
|
||||
|
||||
## 8.1.1. 堆术语与性质
|
||||
## 堆术语与性质
|
||||
|
||||
- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。
|
||||
- 二叉树中的根结点对应「堆顶」,底层最靠右结点对应「堆底」。
|
||||
- 对于大顶堆 / 小顶堆,其堆顶元素(即根结点)的值最大 / 最小。
|
||||
|
||||
## 8.1.2. 堆常用操作
|
||||
## 堆常用操作
|
||||
|
||||
值得说明的是,多数编程语言提供的是「优先队列 Priority Queue」,其是一种抽象数据结构,**定义为具有出队优先级的队列**。
|
||||
|
||||
|
@ -272,7 +272,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 8.1.3. 堆的实现
|
||||
## 堆的实现
|
||||
|
||||
下文实现的是「大顶堆」,若想转换为「小顶堆」,将所有大小逻辑判断取逆(例如将 $\geq$ 替换为 $\leq$ )即可,有兴趣的同学可自行实现。
|
||||
|
||||
|
@ -780,7 +780,7 @@ $$
|
|||
|
||||
进一步地,高度为 $h$ 的完美二叉树的结点数量为 $n = 2^{h+1} - 1$ ,易得复杂度为 $O(2^h) = O(n)$。以上推算表明,**输入列表并建堆的时间复杂度为 $O(n)$ ,非常高效**。
|
||||
|
||||
## 8.1.4. 堆常见应用
|
||||
## 堆常见应用
|
||||
|
||||
- **优先队列**。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 $O(\log n)$ ,建队操作为 $O(n)$ ,皆非常高效。
|
||||
- **堆排序**。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 1.1. 算法无处不在
|
||||
# 算法无处不在
|
||||
|
||||
听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 1.2. 算法是什么
|
||||
# 算法是什么
|
||||
|
||||
## 1.2.1. 算法定义
|
||||
## 算法定义
|
||||
|
||||
「算法 Algorithm」是在有限时间内解决特定问题的一组指令或操作步骤。算法具有以下特性:
|
||||
|
||||
|
@ -13,7 +13,7 @@ comments: true
|
|||
- 具有可行性,可在有限步骤、有限时间、有限内存空间下完成。
|
||||
- 独立于编程语言,即可用多种语言实现。
|
||||
|
||||
## 1.2.2. 数据结构定义
|
||||
## 数据结构定义
|
||||
|
||||
「数据结构 Data Structure」是在计算机中组织与存储数据的方式。为了提高数据存储和操作性能,数据结构的设计原则有:
|
||||
|
||||
|
@ -23,7 +23,7 @@ comments: true
|
|||
|
||||
数据结构的设计是一个充满权衡的过程,这意味着如果获得某方面的优势,则往往需要在另一方面做出妥协。例如,链表相对于数组,数据添加删除操作更加方便,但牺牲了数据的访问速度;图相对于链表,提供了更多的逻辑信息,但需要占用更多的内存空间。
|
||||
|
||||
## 1.2.3. 数据结构与算法的关系
|
||||
## 数据结构与算法的关系
|
||||
|
||||
「数据结构」与「算法」是高度相关、紧密嵌合的,体现在:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 0.1. 关于本书
|
||||
# 关于本书
|
||||
|
||||
五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出“快速排序”代码,我畏畏缩缩地写了一个“冒泡排序”,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
<h4 align="center"> Hello,算法! </h4>
|
||||
|
||||
## 0.1.1. 读者对象
|
||||
## 读者对象
|
||||
|
||||
!!! success "前置条件"
|
||||
|
||||
|
@ -32,7 +32,7 @@ comments: true
|
|||
|
||||
如果您是 **算法大佬**,请受我膜拜!希望您可以抽时间提出意见建议,或者[一起参与创作](https://www.hello-algo.com/chapter_preface/contribution/),帮助各位同学获取更好的学习内容,感谢!
|
||||
|
||||
## 0.1.2. 内容结构
|
||||
## 内容结构
|
||||
|
||||
本书主要内容分为复杂度分析、数据结构、算法三个部分。
|
||||
|
||||
|
@ -71,7 +71,7 @@ comments: true
|
|||
- 实现方法:完整的算法实现,以及优化措施;
|
||||
- 示例题目:结合例题加深理解;
|
||||
|
||||
## 0.1.3. 配套代码
|
||||
## 配套代码
|
||||
|
||||
完整代码托管在 [GitHub 仓库](https://github.com/krahets/hello-algo) ,皆可一键运行。
|
||||
|
||||
|
@ -80,7 +80,7 @@ comments: true
|
|||
1. [编程环境安装](https://www.hello-algo.com/chapter_preface/installation/) ,若有请跳过
|
||||
2. 代码下载与使用方法请见 [如何使用本书](https://www.hello-algo.com/chapter_preface/suggestions/#_4)
|
||||
|
||||
## 0.1.4. 风格约定
|
||||
## 风格约定
|
||||
|
||||
- 标题后标注 * 符号的是选读章节,如果你的时间有限,可以先跳过这些章节。
|
||||
- 文章中的重要名词会用「」符号标注,例如「数组 Array」。名词混淆会导致不必要的歧义,因此最好可以记住这类名词(包括中文和英文),以便后续阅读文献时使用。
|
||||
|
@ -216,7 +216,7 @@ comments: true
|
|||
// 注释
|
||||
```
|
||||
|
||||
## 0.1.5. 本书特点 *
|
||||
## 本书特点 *
|
||||
|
||||
??? abstract "默认折叠,可以跳过"
|
||||
|
||||
|
@ -238,7 +238,7 @@ comments: true
|
|||
|
||||
敲代码如同写字,“美”是统一的追求。本书力求美观的代码,保证规范的变量命名、统一的空格与换行、对齐的缩进、整齐的注释等。
|
||||
|
||||
## 0.1.6. 致谢
|
||||
## 致谢
|
||||
|
||||
本书的成书过程中,我获得了许多人的帮助,包括但不限于:
|
||||
|
||||
|
@ -254,7 +254,7 @@ comments: true
|
|||
|
||||
感谢父母,你们一贯的支持与鼓励给了我自由度来做这些有趣的事。
|
||||
|
||||
## 0.1.7. 作者简介
|
||||
## 作者简介
|
||||
|
||||
![profile](about_the_book.assets/profile.png){: .center}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 0.4. 一起参与创作
|
||||
# 一起参与创作
|
||||
|
||||
!!! success "开源的魅力"
|
||||
|
||||
|
@ -10,7 +10,7 @@ comments: true
|
|||
|
||||
由于作者水平有限,书中内容难免疏漏谬误,请您谅解。此外,期待您可以一同参与本书的创作。如果发现笔误、无效链接、内容缺失、文字歧义、解释不清晰、行文结构不合理等问题,烦请您修正内容,以帮助其他读者获取更优质的学习内容。所有 [撰稿人](https://github.com/krahets/hello-algo/graphs/contributors) 将被展示在仓库主页,以感谢您对开源社区的无私奉献。
|
||||
|
||||
## 0.4.1. 修改文字与代码
|
||||
## 修改文字与代码
|
||||
|
||||
每个页面的右上角都有一个「编辑」按钮,你可以按照以下步骤修改文章:
|
||||
|
||||
|
@ -21,7 +21,7 @@ comments: true
|
|||
|
||||
![edit_markdown](contribution.assets/edit_markdown.png)
|
||||
|
||||
## 0.4.2. 修改图片与动画
|
||||
## 修改图片与动画
|
||||
|
||||
书中的配图无法直接修改,需要通过以下途径提出修改意见:
|
||||
|
||||
|
@ -29,7 +29,7 @@ comments: true
|
|||
2. 描述图片问题,应如何修改;
|
||||
3. 提交 Issue 即可,我会第一时间重新画图并替换图片。
|
||||
|
||||
## 0.4.3. 创作新内容
|
||||
## 创作新内容
|
||||
|
||||
如果您想要创作新内容,例如 **重写章节、新增章节、修改代码、翻译代码至其他编程语言** 等,那么需要实施 Pull Request 工作流程:
|
||||
|
||||
|
@ -41,7 +41,7 @@ comments: true
|
|||
|
||||
非常欢迎您和我一同来创作本书!
|
||||
|
||||
## 0.4.4. 本地部署 hello-algo
|
||||
## 本地部署 hello-algo
|
||||
|
||||
### Docker
|
||||
|
||||
|
|
|
@ -2,51 +2,51 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 0.3. 编程环境安装
|
||||
# 编程环境安装
|
||||
|
||||
(TODO 视频教程)
|
||||
|
||||
## 0.3.1. 安装 VSCode
|
||||
## 安装 VSCode
|
||||
|
||||
本书推荐使用开源轻量的 VSCode 作为本地 IDE ,下载并安装 [VSCode](https://code.visualstudio.com/) 。
|
||||
|
||||
## 0.3.2. Java 环境
|
||||
## Java 环境
|
||||
|
||||
1. 下载并安装 [OpenJDK](https://jdk.java.net/18/)(版本需满足 > JDK 9)。
|
||||
2. 在 VSCode 的插件市场中搜索 `java` ,安装 Java Extension Pack 。
|
||||
|
||||
## 0.3.3. C/C++ 环境
|
||||
## 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 。
|
||||
|
||||
## 0.3.4. Python 环境
|
||||
## Python 环境
|
||||
|
||||
1. 下载并安装 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) 。
|
||||
2. 在 VSCode 的插件市场中搜索 `python` ,安装 Python Extension Pack 。
|
||||
|
||||
## 0.3.5. Go 环境
|
||||
## Go 环境
|
||||
|
||||
1. 下载并安装 [go](https://go.dev/dl/) 。
|
||||
2. 在 VSCode 的插件市场中搜索 `go` ,安装 Go 。
|
||||
3. 快捷键 `Ctrl + Shift + P` 呼出命令栏,输入 go ,选择 `Go: Install/Update Tools` ,全部勾选并安装即可。
|
||||
|
||||
## 0.3.6. JavaScript 环境
|
||||
## JavaScript 环境
|
||||
|
||||
1. 下载并安装 [node.js](https://nodejs.org/en/) 。
|
||||
2. 在 VSCode 的插件市场中搜索 `javascript` ,安装 JavaScript (ES6) code snippets 。
|
||||
|
||||
## 0.3.7. C# 环境
|
||||
## C# 环境
|
||||
|
||||
1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) ;
|
||||
2. 在 VSCode 的插件市场中搜索 `c#` ,安装 c# 。
|
||||
|
||||
## 0.3.8. Swift 环境
|
||||
## 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)。
|
||||
|
||||
## 0.3.9. Rust 环境
|
||||
## 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,9 +2,9 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 0.2. 如何使用本书
|
||||
# 如何使用本书
|
||||
|
||||
## 0.2.1. 图文搭配学
|
||||
## 图文搭配学
|
||||
|
||||
视频和图片相比于文字的信息密度和结构化程度更高,更容易让人理解。在本书中,重点和难点知识会主要以动画、图解的形式呈现,而文字的作用则是作为动画和图的解释与补充。
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
![animation](suggestions.assets/animation.gif)
|
||||
|
||||
## 0.2.2. 代码实践学
|
||||
## 代码实践学
|
||||
|
||||
!!! tip "前置工作"
|
||||
|
||||
|
@ -44,7 +44,7 @@ git clone https://github.com/krahets/hello-algo.git
|
|||
|
||||
若学习时间紧张,**请至少将所有代码通读并运行一遍**。若时间允许,**强烈建议对照着代码自己敲一遍**,逐渐锻炼肌肉记忆。相比于读代码,写代码的过程往往能带来新的收获。
|
||||
|
||||
## 0.2.3. 提问讨论学
|
||||
## 提问讨论学
|
||||
|
||||
阅读本书时,请不要“惯着”那些弄不明白的知识点。如果有任何疑惑,**可以在评论区留下你的问题**,小伙伴们和我都会给予解答(您一般 3 天内会得到回复)。
|
||||
|
||||
|
@ -52,7 +52,7 @@ git clone https://github.com/krahets/hello-algo.git
|
|||
|
||||
![comment](suggestions.assets/comment.gif)
|
||||
|
||||
## 0.2.4. 算法学习“三步走”
|
||||
## 算法学习“三步走”
|
||||
|
||||
**第一阶段,算法入门,也正是本书的定位**。熟悉各种数据结构的特点、用法,学习各种算法的工作原理、用途、效率等。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.2. 二分查找
|
||||
# 二分查找
|
||||
|
||||
「二分查找 Binary Search」利用数据的有序性,通过每轮缩小一半搜索区间来查找目标元素。
|
||||
|
||||
|
@ -11,7 +11,7 @@ comments: true
|
|||
- **要求输入数据是有序的**,这样才能通过判断大小关系来排除一半的搜索区间;
|
||||
- **二分查找仅适用于数组**,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。
|
||||
|
||||
## 10.2.1. 算法实现
|
||||
## 算法实现
|
||||
|
||||
给定一个长度为 $n$ 的排序数组 `nums` ,元素从小到大排列。数组的索引取值范围为
|
||||
|
||||
|
@ -276,13 +276,13 @@ $$
|
|||
|
||||
```
|
||||
|
||||
## 10.2.2. 复杂度分析
|
||||
## 复杂度分析
|
||||
|
||||
**时间复杂度 $O(\log n)$** :其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。
|
||||
|
||||
**空间复杂度 $O(1)$** :指针 `i` , `j` 使用常数大小空间。
|
||||
|
||||
## 10.2.3. 优点与缺点
|
||||
## 优点与缺点
|
||||
|
||||
二分查找效率很高,体现在:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.3. 哈希查找
|
||||
# 哈希查找
|
||||
|
||||
!!! question
|
||||
|
||||
|
@ -10,7 +10,7 @@ comments: true
|
|||
|
||||
「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」,我们可以在 $O(1)$ 时间下实现“键 $\rightarrow$ 值”映射查找,体现着“以空间换时间”的算法思想。
|
||||
|
||||
## 10.3.1. 算法实现
|
||||
## 算法实现
|
||||
|
||||
如果我们想要给定数组中的一个目标元素 `target` ,获取该元素的索引,那么可以借助一个哈希表实现查找。
|
||||
|
||||
|
@ -140,13 +140,13 @@ comments: true
|
|||
[class]{}-[func]{hashingSearchLinkedList}
|
||||
```
|
||||
|
||||
## 10.3.2. 复杂度分析
|
||||
## 复杂度分析
|
||||
|
||||
**时间复杂度 $O(1)$** :哈希表的查找操作使用 $O(1)$ 时间。
|
||||
|
||||
**空间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。
|
||||
|
||||
## 10.3.3. 优点与缺点
|
||||
## 优点与缺点
|
||||
|
||||
在哈希表中,**查找、插入、删除操作的平均时间复杂度都为 $O(1)$** ,这意味着无论是高频增删还是高频查找场景,哈希查找的性能表现都非常好。当然,一切的前提是保证哈希表未退化。
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.1. 线性查找
|
||||
# 线性查找
|
||||
|
||||
「线性查找 Linear Search」是一种最基础的查找方法,其从数据结构的一端开始,依次访问每个元素,直到另一端后停止。
|
||||
|
||||
## 10.1.1. 算法实现
|
||||
## 算法实现
|
||||
|
||||
线性查找实质上就是遍历数据结构 + 判断条件。比如,我们想要在数组 `nums` 中查找目标元素 `target` 的对应索引,那么可以在数组中进行线性查找。
|
||||
|
||||
|
@ -134,13 +134,13 @@ comments: true
|
|||
[class]{}-[func]{linearSearchLinkedList}
|
||||
```
|
||||
|
||||
## 10.1.2. 复杂度分析
|
||||
## 复杂度分析
|
||||
|
||||
**时间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。
|
||||
|
||||
**空间复杂度 $O(1)$** :无需使用额外空间。
|
||||
|
||||
## 10.1.3. 优点与缺点
|
||||
## 优点与缺点
|
||||
|
||||
**线性查找的通用性极佳**。由于线性查找是依次访问元素的,即没有跳跃访问元素,因此数组或链表皆适用。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 10.4. 小结
|
||||
# 小结
|
||||
|
||||
- 线性查找是一种最基础的查找方法,通过遍历数据结构 + 判断条件实现查找。
|
||||
- 二分查找利用数据的有序性,通过循环不断缩小一半搜索区间来实现查找,其要求输入数据是有序的,并且仅适用于数组或基于数组实现的数据结构。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 11.2. 冒泡排序
|
||||
# 冒泡排序
|
||||
|
||||
「冒泡排序 Bubble Sort」是一种最基础的排序算法,非常适合作为第一个学习的排序算法。顾名思义,「冒泡」是该算法的核心操作。
|
||||
|
||||
|
@ -37,7 +37,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 冒泡操作 </p>
|
||||
|
||||
## 11.2.1. 算法流程
|
||||
## 算法流程
|
||||
|
||||
1. 设数组长度为 $n$ ,完成第一轮「冒泡」后,数组最大元素已在正确位置,接下来只需排序剩余 $n - 1$ 个元素。
|
||||
2. 同理,对剩余 $n - 1$ 个元素执行「冒泡」,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个。
|
||||
|
@ -107,7 +107,7 @@ comments: true
|
|||
[class]{}-[func]{bubbleSort}
|
||||
```
|
||||
|
||||
## 11.2.2. 算法特性
|
||||
## 算法特性
|
||||
|
||||
**时间复杂度 $O(n^2)$** :各轮「冒泡」遍历的数组长度为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。
|
||||
|
||||
|
@ -119,7 +119,7 @@ comments: true
|
|||
|
||||
**自适应排序**:引入 `flag` 优化后(见下文),最佳时间复杂度为 $O(N)$ 。
|
||||
|
||||
## 11.2.3. 效率优化
|
||||
## 效率优化
|
||||
|
||||
我们发现,若在某轮「冒泡」中未执行任何交换操作,则说明数组已经完成排序,可直接返回结果。考虑可以增加一个标志位 `flag` 来监听该情况,若出现则直接返回。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 11.3. 插入排序
|
||||
# 插入排序
|
||||
|
||||
「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。
|
||||
|
||||
|
@ -14,7 +14,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 插入操作 </p>
|
||||
|
||||
## 11.3.1. 算法流程
|
||||
## 算法流程
|
||||
|
||||
1. 第 1 轮先选取数组的 **第 2 个元素** 为 `base` ,执行「插入操作」后,**数组前 2 个元素已完成排序**。
|
||||
2. 第 2 轮选取 **第 3 个元素** 为 `base` ,执行「插入操作」后,**数组前 3 个元素已完成排序**。
|
||||
|
@ -84,7 +84,7 @@ comments: true
|
|||
[class]{}-[func]{insertionSort}
|
||||
```
|
||||
|
||||
## 11.3.2. 算法特性
|
||||
## 算法特性
|
||||
|
||||
**时间复杂度 $O(n^2)$** :最差情况下,各轮插入操作循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,使用 $O(n^2)$ 时间。
|
||||
|
||||
|
@ -96,7 +96,7 @@ comments: true
|
|||
|
||||
**自适应排序**:最佳情况下,时间复杂度为 $O(n)$ 。
|
||||
|
||||
## 11.3.3. 插入排序 vs 冒泡排序
|
||||
## 插入排序 vs 冒泡排序
|
||||
|
||||
!!! question
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 11.1. 排序简介
|
||||
# 排序简介
|
||||
|
||||
「排序算法 Sorting Algorithm」使得列表中的所有元素按照从小到大的顺序排列。
|
||||
|
||||
|
@ -13,7 +13,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 排序中的不同元素类型和判断规则 </p>
|
||||
|
||||
## 11.1.1. 评价维度
|
||||
## 评价维度
|
||||
|
||||
排序算法主要可根据 **稳定性 、就地性 、自适应性 、比较类** 来分类。
|
||||
|
||||
|
@ -64,7 +64,7 @@ comments: true
|
|||
|
||||
「比较类排序」的时间复杂度最优为 $O(n \log n)$ ;而「非比较类排序」可以达到 $O(n)$ 的时间复杂度,但通用性较差。
|
||||
|
||||
## 11.1.2. 理想排序算法
|
||||
## 理想排序算法
|
||||
|
||||
- **运行快**,即时间复杂度低;
|
||||
- **稳定排序**,即排序后相等元素的相对位置不变化;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 11.5. 归并排序
|
||||
# 归并排序
|
||||
|
||||
「归并排序 Merge Sort」是算法中“分治思想”的典型体现,其有「划分」和「合并」两个阶段:
|
||||
|
||||
|
@ -13,7 +13,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 归并排序两阶段:划分与合并 </p>
|
||||
|
||||
## 11.5.1. 算法流程
|
||||
## 算法流程
|
||||
|
||||
**「递归划分」** 从顶至底递归地 **将数组从中点切为两个子数组**,直至长度为 1 ;
|
||||
|
||||
|
@ -341,7 +341,7 @@ comments: true
|
|||
- `nums` 的待合并区间为 `[left, right]` ,而因为 `tmp` 只复制了 `nums` 该区间元素,所以 `tmp` 对应区间为 `[0, right - left]` ,**需要特别注意代码中各个变量的含义**。
|
||||
- 判断 `tmp[i]` 和 `tmp[j]` 的大小的操作中,还 **需考虑当子数组遍历完成后的索引越界问题**,即 `i > leftEnd` 和 `j > rightEnd` 的情况,索引越界的优先级是最高的,例如如果左子数组已经被合并完了,那么不用继续判断,直接合并右子数组元素即可。
|
||||
|
||||
## 11.5.2. 算法特性
|
||||
## 算法特性
|
||||
|
||||
- **时间复杂度 $O(n \log n)$** :划分形成高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,总体使用 $O(n \log n)$ 时间。
|
||||
- **空间复杂度 $O(n)$** :需借助辅助数组实现合并,使用 $O(n)$ 大小的额外空间;递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间。
|
||||
|
@ -349,7 +349,7 @@ comments: true
|
|||
- **稳定排序**:在合并时可保证相等元素的相对位置不变。
|
||||
- **非自适应排序**:对于任意输入数据,归并排序的时间复杂度皆相同。
|
||||
|
||||
## 11.5.3. 链表排序 *
|
||||
## 链表排序 *
|
||||
|
||||
归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 11.4. 快速排序
|
||||
# 快速排序
|
||||
|
||||
「快速排序 Quick Sort」是一种基于“分治思想”的排序算法,速度很快、应用很广。
|
||||
|
||||
|
@ -121,7 +121,7 @@ comments: true
|
|||
|
||||
哨兵划分的实质是将 **一个长数组的排序问题** 简化为 **两个短数组的排序问题**。
|
||||
|
||||
## 11.4.1. 算法流程
|
||||
## 算法流程
|
||||
|
||||
1. 首先,对数组执行一次「哨兵划分」,得到待排序的 **左子数组** 和 **右子数组**;
|
||||
2. 接下来,对 **左子数组** 和 **右子数组** 分别 **递归执行**「哨兵划分」……
|
||||
|
@ -193,7 +193,7 @@ comments: true
|
|||
[class]{QuickSort}-[func]{quickSort}
|
||||
```
|
||||
|
||||
## 11.4.2. 算法特性
|
||||
## 算法特性
|
||||
|
||||
**平均时间复杂度 $O(n \log n)$** :平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。
|
||||
|
||||
|
@ -207,7 +207,7 @@ comments: true
|
|||
|
||||
**自适应排序**:最差情况下,时间复杂度劣化至 $O(n^2)$ 。
|
||||
|
||||
## 11.4.3. 快排为什么快?
|
||||
## 快排为什么快?
|
||||
|
||||
从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高**,这是因为:
|
||||
|
||||
|
@ -215,7 +215,7 @@ comments: true
|
|||
- **缓存使用效率高**:哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。
|
||||
- **复杂度的常数系数低**:在提及的三种算法中,快速排序的 **比较**、**赋值**、**交换** 三种操作的总体数量最少(类似于「插入排序」快于「冒泡排序」的原因)。
|
||||
|
||||
## 11.4.4. 基准数优化
|
||||
## 基准数优化
|
||||
|
||||
**普通快速排序在某些输入下的时间效率变差**。举个极端例子,假设输入数组是完全倒序的,由于我们选取最左端元素为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,从而 **左子数组长度为 $n - 1$、右子数组长度为 $0$** 。这样进一步递归下去,**每轮哨兵划分后的右子数组长度都为 $0$** ,分治策略失效,快速排序退化为「冒泡排序」了。
|
||||
|
||||
|
@ -303,7 +303,7 @@ comments: true
|
|||
[class]{QuickSortMedian}-[func]{partition}
|
||||
```
|
||||
|
||||
## 11.4.5. 尾递归优化
|
||||
## 尾递归优化
|
||||
|
||||
**普通快速排序在某些输入下的空间效率变差**。仍然以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 0 ,那么将形成一个高度为 $n - 1$ 的递归树,此时使用的栈帧空间大小劣化至 $O(n)$ 。
|
||||
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 11.6. 小结
|
||||
# 小结
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 5.3. 双向队列
|
||||
# 双向队列
|
||||
|
||||
对于队列,我们只能在头部删除或在尾部添加元素,而「双向队列 Deque」更加灵活,在其头部和尾部都能执行元素添加或删除操作。
|
||||
|
||||
|
@ -10,7 +10,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 双向队列的操作 </p>
|
||||
|
||||
## 5.3.1. 双向队列常用操作
|
||||
## 双向队列常用操作
|
||||
|
||||
双向队列的常用操作见下表,方法名需根据特定语言来确定。
|
||||
|
||||
|
@ -293,7 +293,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 5.3.2. 双向队列实现 *
|
||||
## 双向队列实现 *
|
||||
|
||||
与队列类似,双向队列同样可以使用链表或数组来实现。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 5.2. 队列
|
||||
# 队列
|
||||
|
||||
「队列 Queue」是一种遵循「先入先出 first in, first out」数据操作规则的线性数据结构。顾名思义,队列模拟的是排队现象,即外面的人不断加入队列尾部,而处于队列头部的人不断地离开。
|
||||
|
||||
|
@ -12,7 +12,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 队列的先入先出特性 </p>
|
||||
|
||||
## 5.2.1. 队列常用操作
|
||||
## 队列常用操作
|
||||
|
||||
队列的常用操作见下表,方法名需根据特定语言来确定。
|
||||
|
||||
|
@ -262,7 +262,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 5.2.2. 队列实现
|
||||
## 队列实现
|
||||
|
||||
队列需要一种可以在一端添加,并在另一端删除的数据结构,也可以使用链表或数组来实现。
|
||||
|
||||
|
@ -429,11 +429,11 @@ comments: true
|
|||
|
||||
以上实现的队列仍存在局限性,即长度不可变。不过这个问题很容易解决,我们可以将数组替换为列表(即动态数组),从而引入扩容机制。有兴趣的同学可以尝试自行实现。
|
||||
|
||||
## 5.2.3. 两种实现对比
|
||||
## 两种实现对比
|
||||
|
||||
与栈的结论一致,在此不再赘述。
|
||||
|
||||
## 5.2.4. 队列典型应用
|
||||
## 队列典型应用
|
||||
|
||||
- **淘宝订单**。购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。
|
||||
- **各种待办事项**。任何需要实现“先来后到”的功能,例如打印机的任务队列、餐厅的出餐队列等等。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 5.1. 栈
|
||||
# 栈
|
||||
|
||||
「栈 Stack」是一种遵循「先入后出 first in, last out」数据操作规则的线性数据结构。我们可以将栈类比为放在桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。
|
||||
|
||||
|
@ -14,7 +14,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 栈的先入后出特性 </p>
|
||||
|
||||
## 5.1.1. 栈常用操作
|
||||
## 栈常用操作
|
||||
|
||||
栈的常用操作见下表(方法命名以 Java 为例)。
|
||||
|
||||
|
@ -261,7 +261,7 @@ comments: true
|
|||
|
||||
```
|
||||
|
||||
## 5.1.2. 栈的实现
|
||||
## 栈的实现
|
||||
|
||||
为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。
|
||||
|
||||
|
@ -419,7 +419,7 @@ comments: true
|
|||
[class]{ArrayStack}-[func]{}
|
||||
```
|
||||
|
||||
## 5.1.3. 两种实现对比
|
||||
## 两种实现对比
|
||||
|
||||
### 支持操作
|
||||
|
||||
|
@ -444,7 +444,7 @@ comments: true
|
|||
|
||||
综上,我们不能简单地确定哪种实现更加省内存,需要 case-by-case 地分析。
|
||||
|
||||
## 5.1.4. 栈典型应用
|
||||
## 栈典型应用
|
||||
|
||||
- **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就将上一个网页执行入栈,这样我们就可以通过「后退」操作来回到上一页面,后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么则需要两个栈来配合实现。
|
||||
- **程序内存管理**。每当调用函数时,系统就会在栈顶添加一个栈帧,用来记录函数的上下文信息。在递归函数中,向下递推会不断执行入栈,向上回溯阶段时出栈。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 5.4. 小结
|
||||
# 小结
|
||||
|
||||
- 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。
|
||||
- 在时间效率方面,栈的数组实现具有更好的平均效率,但扩容时会导致单次入栈操作的时间复杂度劣化至 $O(n)$ 。相对地,栈的链表实现具有更加稳定的效率表现。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.4. AVL 树 *
|
||||
# AVL 树 *
|
||||
|
||||
在「二叉搜索树」章节中提到,在进行多次插入与删除操作后,二叉搜索树可能会退化为链表。此时所有操作的时间复杂度都会由 $O(\log n)$ 劣化至 $O(n)$ 。
|
||||
|
||||
|
@ -18,7 +18,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
|
|||
|
||||
换言之,在频繁增删查改的使用场景中,AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。
|
||||
|
||||
## 7.4.1. AVL 树常见术语
|
||||
## AVL 树常见术语
|
||||
|
||||
「AVL 树」既是「二叉搜索树」又是「平衡二叉树」,同时满足这两种二叉树的所有性质,因此又被称为「平衡二叉搜索树」。
|
||||
|
||||
|
@ -303,7 +303,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
|
|||
|
||||
设平衡因子为 $f$ ,则一棵 AVL 树的任意结点的平衡因子皆满足 $-1 \le f \le 1$ 。
|
||||
|
||||
## 7.4.2. AVL 树旋转
|
||||
## AVL 树旋转
|
||||
|
||||
AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影响二叉树中序遍历序列的前提下,使失衡结点重新恢复平衡**。换言之,旋转操作既可以使树保持为「二叉搜索树」,也可以使树重新恢复为「平衡二叉树」。
|
||||
|
||||
|
@ -556,7 +556,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
|||
[class]{AVLTree}-[func]{rotate}
|
||||
```
|
||||
|
||||
## 7.4.3. AVL 树常用操作
|
||||
## AVL 树常用操作
|
||||
|
||||
### 插入结点
|
||||
|
||||
|
@ -750,7 +750,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
|||
|
||||
「AVL 树」的结点查找操作与「二叉搜索树」一致,在此不再赘述。
|
||||
|
||||
## 7.4.4. AVL 树典型应用
|
||||
## AVL 树典型应用
|
||||
|
||||
- 组织存储大型数据,适用于高频查找、低频增删场景;
|
||||
- 用于建立数据库中的索引系统;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.3. 二叉搜索树
|
||||
# 二叉搜索树
|
||||
|
||||
「二叉搜索树 Binary Search Tree」满足以下条件:
|
||||
|
||||
|
@ -11,7 +11,7 @@ comments: true
|
|||
|
||||
![binary_search_tree](binary_search_tree.assets/binary_search_tree.png)
|
||||
|
||||
## 7.3.1. 二叉搜索树的操作
|
||||
## 二叉搜索树的操作
|
||||
|
||||
### 查找结点
|
||||
|
||||
|
@ -290,7 +290,7 @@ comments: true
|
|||
|
||||
![bst_inorder_traversal](binary_search_tree.assets/bst_inorder_traversal.png)
|
||||
|
||||
## 7.3.2. 二叉搜索树的效率
|
||||
## 二叉搜索树的效率
|
||||
|
||||
假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:
|
||||
|
||||
|
@ -319,7 +319,7 @@ comments: true
|
|||
|
||||
</div>
|
||||
|
||||
## 7.3.3. 二叉搜索树的退化
|
||||
## 二叉搜索树的退化
|
||||
|
||||
理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。
|
||||
|
||||
|
@ -331,7 +331,7 @@ comments: true
|
|||
|
||||
![bst_degradation](binary_search_tree.assets/bst_degradation.png)
|
||||
|
||||
## 7.3.4. 二叉搜索树常见应用
|
||||
## 二叉搜索树常见应用
|
||||
|
||||
- 系统中的多级索引,高效查找、插入、删除操作。
|
||||
- 各种搜索算法的底层数据结构。
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.1. 二叉树
|
||||
# 二叉树
|
||||
|
||||
「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含「值」和两个「指针」。
|
||||
|
||||
|
@ -135,7 +135,7 @@ comments: true
|
|||
|
||||
<p align="center"> Fig. 子结点与子树 </p>
|
||||
|
||||
## 7.1.1. 二叉树常见术语
|
||||
## 二叉树常见术语
|
||||
|
||||
二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。
|
||||
|
||||
|
@ -156,7 +156,7 @@ comments: true
|
|||
|
||||
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。
|
||||
|
||||
## 7.1.2. 二叉树基本操作
|
||||
## 二叉树基本操作
|
||||
|
||||
**初始化二叉树**。与链表类似,先初始化结点,再构建引用指向(即指针)。
|
||||
|
||||
|
@ -422,7 +422,7 @@ comments: true
|
|||
|
||||
插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。
|
||||
|
||||
## 7.1.3. 常见二叉树类型
|
||||
## 常见二叉树类型
|
||||
|
||||
### 完美二叉树
|
||||
|
||||
|
@ -454,7 +454,7 @@ comments: true
|
|||
|
||||
![balanced_binary_tree](binary_tree.assets/balanced_binary_tree.png)
|
||||
|
||||
## 7.1.4. 二叉树的退化
|
||||
## 二叉树的退化
|
||||
|
||||
当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。
|
||||
|
||||
|
@ -478,7 +478,7 @@ comments: true
|
|||
|
||||
</div>
|
||||
|
||||
## 7.1.5. 二叉树表示方式 *
|
||||
## 二叉树表示方式 *
|
||||
|
||||
我们一般使用二叉树的「链表表示」,即存储单位为结点 `TreeNode` ,结点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.2. 二叉树遍历
|
||||
# 二叉树遍历
|
||||
|
||||
从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历结点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。
|
||||
|
||||
常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。
|
||||
|
||||
## 7.2.1. 层序遍历
|
||||
## 层序遍历
|
||||
|
||||
「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。
|
||||
|
||||
|
@ -88,7 +88,7 @@ comments: true
|
|||
|
||||
**空间复杂度**:当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个结点,使用 $O(n)$ 空间。
|
||||
|
||||
## 7.2.2. 前序、中序、后序遍历
|
||||
## 前序、中序、后序遍历
|
||||
|
||||
相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
comments: true
|
||||
---
|
||||
|
||||
# 7.5. 小结
|
||||
# 小结
|
||||
|
||||
- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。
|
||||
- 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。
|
||||
|
|
|
@ -10,6 +10,7 @@ sys.path.append(osp.dirname(osp.dirname(osp.dirname(osp.abspath(__file__)))))
|
|||
import re
|
||||
import glob
|
||||
import shutil
|
||||
from docs.utils.number_headings import number_headings
|
||||
from docs.utils.extract_code_python import ExtractCodeBlocksPython
|
||||
from docs.utils.extract_code_java import ExtractCodeBlocksJava
|
||||
from docs.utils.extract_code_cpp import ExtractCodeBlocksCpp
|
||||
|
@ -20,7 +21,7 @@ from docs.utils.extract_code_go import ExtractCodeBlocksGo
|
|||
from docs.utils.extract_code_zig import ExtractCodeBlocksZig
|
||||
|
||||
|
||||
def build_markdown(md_path):
|
||||
def build_code_blocks(md_path):
|
||||
with open(md_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
|
@ -114,6 +115,9 @@ if __name__ == "__main__":
|
|||
shutil.copytree("docs", "build", dirs_exist_ok=True)
|
||||
shutil.rmtree("build/utils")
|
||||
|
||||
# Build docs
|
||||
# Build code blocks
|
||||
for md_path in glob.glob("docs/chapter_*/*.md"):
|
||||
build_markdown(md_path)
|
||||
build_code_blocks(md_path)
|
||||
|
||||
# Build headings
|
||||
number_headings("mkdocs.yml", "build")
|
||||
|
|
81
docs/utils/number_headings.py
Executable file
81
docs/utils/number_headings.py
Executable file
|
@ -0,0 +1,81 @@
|
|||
import re
|
||||
|
||||
def get_heading_info_from_nav(mkdocs_path):
|
||||
"""
|
||||
Get heading info from mkdocs navigation
|
||||
"""
|
||||
|
||||
with open(mkdocs_path) as f:
|
||||
lines = f.readlines()[125:]
|
||||
# Get nav lines
|
||||
for i, line in enumerate(lines):
|
||||
if "nav:" in line:
|
||||
break
|
||||
lines = lines[i:]
|
||||
|
||||
# Search articles
|
||||
articles = []
|
||||
for line in lines:
|
||||
level = 0
|
||||
level_re = None
|
||||
while level_re is None and level < 3:
|
||||
level += 1
|
||||
level_pat = level * " "
|
||||
level_pat = f"^{level_pat}- \d"
|
||||
level_re = re.search(level_pat, line)
|
||||
# Only add articles with heading level 2
|
||||
if level != 2:
|
||||
continue
|
||||
|
||||
number_pat = level * "\d+."
|
||||
number_re = re.search(number_pat, line)
|
||||
number = re.search(number_pat, line).group(0) if number_re else None
|
||||
file_path = re.search("\S+\/\S+\.md", line).group(0)
|
||||
|
||||
article = {
|
||||
"level": level,
|
||||
"number": number,
|
||||
"file_path": file_path
|
||||
}
|
||||
articles.append(article)
|
||||
|
||||
print(f"{file_path}, heading number is {number}")
|
||||
|
||||
return articles
|
||||
|
||||
def number_article(article, base_dir="build"):
|
||||
"""
|
||||
Number a doc
|
||||
"""
|
||||
|
||||
with open(f"{base_dir}/{article['file_path']}", "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Add h1, h2 heading numbers
|
||||
h2_count = 1
|
||||
for i, line in enumerate(lines):
|
||||
h1_re = re.search("^(#)\s+\S+", line)
|
||||
if h1_re is not None:
|
||||
h1 = h1_re.group(1)
|
||||
lines[i] = line.replace(h1, f"# {article['number']}")
|
||||
continue
|
||||
|
||||
h2_re = re.search("^(##)\s+\S+", line)
|
||||
if h2_re is not None:
|
||||
h2 = h2_re.group(1)
|
||||
lines[i] = line.replace(h2, f"## {article['number']}{h2_count}.")
|
||||
h2_count += 1
|
||||
|
||||
with open(f"{base_dir}/{article['file_path']}", "w") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
|
||||
def number_headings(mkdocs_path, build_dir):
|
||||
"""
|
||||
Build headings
|
||||
"""
|
||||
|
||||
articles = get_heading_info_from_nav(mkdocs_path)
|
||||
|
||||
for article in articles:
|
||||
number_article(article, base_dir=build_dir)
|
Loading…
Reference in a new issue