diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index a19369d84..f11d586dd 100755 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -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. 数组典型应用 +## 数组典型应用 **随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 diff --git a/docs/chapter_array_and_linkedlist/linked_list.md b/docs/chapter_array_and_linkedlist/linked_list.md index f2df041fd..834be28e1 100755 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -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` 。 diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index a1edf9f44..a9285c215 100755 --- a/docs/chapter_array_and_linkedlist/list.md +++ b/docs/chapter_array_and_linkedlist/list.md @@ -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. 列表简易实现 * +## 列表简易实现 * 为了帮助加深对列表的理解,我们在此提供一个列表的简易版本的实现。需要关注三个核心点: diff --git a/docs/chapter_array_and_linkedlist/summary.md b/docs/chapter_array_and_linkedlist/summary.md index 3b6ac2cb0..60fe604f0 100644 --- a/docs/chapter_array_and_linkedlist/summary.md +++ b/docs/chapter_array_and_linkedlist/summary.md @@ -2,14 +2,14 @@ comments: true --- -# 4.4. 小结 +# 小结 - 数组和链表是两种基本数据结构,代表了数据在计算机内存中的两种存储方式,即连续空间存储和离散空间存储。两者的优点与缺点呈现出此消彼长的关系。 - 数组支持随机访问、内存空间占用小;但插入与删除元素效率低,且初始化后长度不可变。 - 链表可通过更改指针实现高效的结点插入与删除,并且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型有单向链表、循环链表、双向链表。 - 列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,且可以灵活改变长度。列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费。 -## 4.4.1. 数组 VS 链表 +## 数组 VS 链表

Table. 数组与链表特点对比

diff --git a/docs/chapter_computational_complexity/performance_evaluation.md b/docs/chapter_computational_complexity/performance_evaluation.md index 45288720f..449c6bfed 100644 --- a/docs/chapter_computational_complexity/performance_evaluation.md +++ b/docs/chapter_computational_complexity/performance_evaluation.md @@ -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. 复杂度分析重要性 +## 复杂度分析重要性 复杂度分析给出一把评价算法效率的“标尺”,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。 diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index bf350a3c1..e0225e939 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -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$ ,常见的空间复杂度类型有(从低到高排列) diff --git a/docs/chapter_computational_complexity/space_time_tradeoff.md b/docs/chapter_computational_complexity/space_time_tradeoff.md index 15aef9907..1c4827c05 100755 --- a/docs/chapter_computational_complexity/space_time_tradeoff.md +++ b/docs/chapter_computational_complexity/space_time_tradeoff.md @@ -2,7 +2,7 @@ comments: true --- -# 2.4. 权衡时间与空间 +# 权衡时间与空间 理想情况下,我们希望算法的时间复杂度和空间复杂度都能够达到最优,而实际上,同时优化时间复杂度和空间复杂度是非常困难的。 @@ -10,7 +10,7 @@ comments: true 大多数情况下,时间都是比空间更宝贵的,只要空间复杂度不要太离谱、能接受就行,**因此以空间换时间最为常用**。 -## 2.4.1. 示例题目 * +## 示例题目 * 以 LeetCode 全站第一题 [两数之和](https://leetcode.cn/problems/two-sum/) 为例。 diff --git a/docs/chapter_computational_complexity/summary.md b/docs/chapter_computational_complexity/summary.md index acd745f9f..0a1ee80be 100644 --- a/docs/chapter_computational_complexity/summary.md +++ b/docs/chapter_computational_complexity/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 2.5. 小结 +# 小结 ### 算法效率评估 diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index bdb55b4b6..f4c410e22 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -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 @@ $$ -## 2.2.5. 常见类型 +## 常见类型 设输入数据大小为 $n$ ,常见的时间复杂度类型有(从低到高排列) @@ -1528,7 +1528,7 @@ $$

Fig. 阶乘阶的时间复杂度

-## 2.2.6. 最差、最佳、平均时间复杂度 +## 最差、最佳、平均时间复杂度 **某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**。举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论: diff --git a/docs/chapter_data_structure/classification_of_data_structure.md b/docs/chapter_data_structure/classification_of_data_structure.md index 1b8d7b739..a2f5221b8 100644 --- a/docs/chapter_data_structure/classification_of_data_structure.md +++ b/docs/chapter_data_structure/classification_of_data_structure.md @@ -2,11 +2,11 @@ comments: true --- -# 3.2. 数据结构分类 +# 数据结构分类 数据结构主要可根据「逻辑结构」和「物理结构」两种角度进行分类。 -## 3.2.1. 逻辑结构:线性与非线性 +## 逻辑结构:线性与非线性 **「逻辑结构」反映了数据之间的逻辑关系**。数组和链表的数据按照顺序依次排列,反映了数据间的线性关系;树从顶至底按层级排列,反映了祖先与后代之间的派生关系;图由结点和边组成,反映了复杂网络关系。 @@ -19,7 +19,7 @@ comments: true

Fig. 线性与非线性数据结构

-## 3.2.2. 物理结构:连续与离散 +## 物理结构:连续与离散 !!! note diff --git a/docs/chapter_data_structure/data_and_memory.md b/docs/chapter_data_structure/data_and_memory.md index e2e1084d0..43cdbefea 100644 --- a/docs/chapter_data_structure/data_and_memory.md +++ b/docs/chapter_data_structure/data_and_memory.md @@ -2,9 +2,9 @@ comments: true --- -# 3.1. 数据与内存 +# 数据与内存 -## 3.1.1. 基本数据类型 +## 基本数据类型 谈到计算机中的数据,我们能够想到文本、图片、视频、语音、3D 模型等等,这些数据虽然组织形式不同,但是有一个共同点,即都是由各种基本数据类型构成的。 @@ -134,7 +134,7 @@ comments: true ``` -## 3.1.2. 计算机内存 +## 计算机内存 在计算机中,内存和硬盘是两种主要的存储硬件设备。「硬盘」主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。「内存」用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。 diff --git a/docs/chapter_data_structure/summary.md b/docs/chapter_data_structure/summary.md index 5340746a3..e51e4dc5b 100644 --- a/docs/chapter_data_structure/summary.md +++ b/docs/chapter_data_structure/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 3.3. 小结 +# 小结 - 整数 byte, short, int, long 、浮点数 float, double 、字符 char 、布尔 boolean 是计算机中的基本数据类型,占用空间的大小决定了它们的取值范围。 - 在程序运行时,数据存储在计算机的内存中。内存中每块空间都有独立的内存地址,程序是通过内存地址来访问数据的。 diff --git a/docs/chapter_graph/graph.md b/docs/chapter_graph/graph.md index 1de9f230d..43993d657 100644 --- a/docs/chapter_graph/graph.md +++ b/docs/chapter_graph/graph.md @@ -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. 图常见应用 +## 图常见应用 现实中的许多系统都可以使用图来建模,对应的待求解问题也可以被约化为图计算问题。 diff --git a/docs/chapter_graph/graph_operations.md b/docs/chapter_graph/graph_operations.md index e99eac913..933e9b843 100644 --- a/docs/chapter_graph/graph_operations.md +++ b/docs/chapter_graph/graph_operations.md @@ -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$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 diff --git a/docs/chapter_graph/graph_traversal.md b/docs/chapter_graph/graph_traversal.md index 1d7529534..3af3d720e 100644 --- a/docs/chapter_graph/graph_traversal.md +++ b/docs/chapter_graph/graph_traversal.md @@ -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. 深度优先遍历 +## 深度优先遍历 **深度优先遍历是一种优先走到底、无路可走再回头的遍历方式**。具体地,从某个顶点出发,不断地访问当前结点的某个邻接顶点,直到走到尽头时回溯,再继续走到底 + 回溯,以此类推……直至所有顶点遍历完成时结束。 diff --git a/docs/chapter_hashing/hash_collision.md b/docs/chapter_hashing/hash_collision.md index 269254c6c..d17bb5911 100644 --- a/docs/chapter_hashing/hash_collision.md +++ b/docs/chapter_hashing/hash_collision.md @@ -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. 开放寻址 +## 开放寻址 「开放寻址」不引入额外数据结构,而是通过“多次探测”来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希**。 diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index 0985a8fa2..3ad00dd58 100755 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -2,7 +2,7 @@ comments: true --- -# 6.1. 哈希表 +# 哈希表 哈希表通过建立「键 key」和「值 value」之间的映射,实现高效的元素查找。具体地,输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 $O(1)$ 。 @@ -12,7 +12,7 @@ comments: true

Fig. 哈希表抽象表示

-## 6.1.1. 哈希表效率 +## 哈希表效率 除了哈希表之外,还可以使用以下数据结构来实现上述查询功能: @@ -33,7 +33,7 @@ comments: true -## 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$ ,则有 diff --git a/docs/chapter_hashing/summary.md b/docs/chapter_hashing/summary.md index a66b041a3..5f58b7594 100644 --- a/docs/chapter_hashing/summary.md +++ b/docs/chapter_hashing/summary.md @@ -2,4 +2,4 @@ comments: true --- -# 6.3. 小结 +# 小结 diff --git a/docs/chapter_heap/heap.md b/docs/chapter_heap/heap.md index c52c2e7c9..ed7dbaacf 100644 --- a/docs/chapter_heap/heap.md +++ b/docs/chapter_heap/heap.md @@ -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)$ ,皆非常高效。 - **堆排序**。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可。 diff --git a/docs/chapter_introduction/algorithms_are_everywhere.md b/docs/chapter_introduction/algorithms_are_everywhere.md index 980fba81e..5d80f9c92 100644 --- a/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/docs/chapter_introduction/algorithms_are_everywhere.md @@ -2,7 +2,7 @@ comments: true --- -# 1.1. 算法无处不在 +# 算法无处不在 听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。 diff --git a/docs/chapter_introduction/what_is_dsa.md b/docs/chapter_introduction/what_is_dsa.md index d104e4852..10ea25133 100644 --- a/docs/chapter_introduction/what_is_dsa.md +++ b/docs/chapter_introduction/what_is_dsa.md @@ -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. 数据结构与算法的关系 +## 数据结构与算法的关系 「数据结构」与「算法」是高度相关、紧密嵌合的,体现在: diff --git a/docs/chapter_preface/about_the_book.md b/docs/chapter_preface/about_the_book.md index 67a26b465..62b1eb435 100644 --- a/docs/chapter_preface/about_the_book.md +++ b/docs/chapter_preface/about_the_book.md @@ -2,7 +2,7 @@ comments: true --- -# 0.1. 关于本书 +# 关于本书 五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出“快速排序”代码,我畏畏缩缩地写了一个“冒泡排序”,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。 @@ -12,7 +12,7 @@ comments: true

Hello,算法!

-## 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} diff --git a/docs/chapter_preface/contribution.md b/docs/chapter_preface/contribution.md index 073c3f5f4..498002c72 100644 --- a/docs/chapter_preface/contribution.md +++ b/docs/chapter_preface/contribution.md @@ -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 diff --git a/docs/chapter_preface/installation.md b/docs/chapter_preface/installation.md index b700649ad..b74125bbd 100644 --- a/docs/chapter_preface/installation.md +++ b/docs/chapter_preface/installation.md @@ -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)。 diff --git a/docs/chapter_preface/suggestions.md b/docs/chapter_preface/suggestions.md index 7907db787..4c7743564 100644 --- a/docs/chapter_preface/suggestions.md +++ b/docs/chapter_preface/suggestions.md @@ -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. 算法学习“三步走” +## 算法学习“三步走” **第一阶段,算法入门,也正是本书的定位**。熟悉各种数据结构的特点、用法,学习各种算法的工作原理、用途、效率等。 diff --git a/docs/chapter_searching/binary_search.md b/docs/chapter_searching/binary_search.md index eb359fff2..652ca6298 100755 --- a/docs/chapter_searching/binary_search.md +++ b/docs/chapter_searching/binary_search.md @@ -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. 优点与缺点 +## 优点与缺点 二分查找效率很高,体现在: diff --git a/docs/chapter_searching/hashing_search.md b/docs/chapter_searching/hashing_search.md index 007846d0d..8ee456d7a 100755 --- a/docs/chapter_searching/hashing_search.md +++ b/docs/chapter_searching/hashing_search.md @@ -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)$** ,这意味着无论是高频增删还是高频查找场景,哈希查找的性能表现都非常好。当然,一切的前提是保证哈希表未退化。 diff --git a/docs/chapter_searching/linear_search.md b/docs/chapter_searching/linear_search.md index 1a0b00275..4c079794f 100755 --- a/docs/chapter_searching/linear_search.md +++ b/docs/chapter_searching/linear_search.md @@ -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. 优点与缺点 +## 优点与缺点 **线性查找的通用性极佳**。由于线性查找是依次访问元素的,即没有跳跃访问元素,因此数组或链表皆适用。 diff --git a/docs/chapter_searching/summary.md b/docs/chapter_searching/summary.md index 738255889..066d09af5 100644 --- a/docs/chapter_searching/summary.md +++ b/docs/chapter_searching/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 10.4. 小结 +# 小结 - 线性查找是一种最基础的查找方法,通过遍历数据结构 + 判断条件实现查找。 - 二分查找利用数据的有序性,通过循环不断缩小一半搜索区间来实现查找,其要求输入数据是有序的,并且仅适用于数组或基于数组实现的数据结构。 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index d3a5e8f6d..b2c396d2f 100755 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.2. 冒泡排序 +# 冒泡排序 「冒泡排序 Bubble Sort」是一种最基础的排序算法,非常适合作为第一个学习的排序算法。顾名思义,「冒泡」是该算法的核心操作。 @@ -37,7 +37,7 @@ comments: true

Fig. 冒泡操作

-## 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` 来监听该情况,若出现则直接返回。 diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index b5562024d..4e3dd2b6a 100755 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.3. 插入排序 +# 插入排序 「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。 @@ -14,7 +14,7 @@ comments: true

Fig. 插入操作

-## 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 diff --git a/docs/chapter_sorting/intro_to_sort.md b/docs/chapter_sorting/intro_to_sort.md index e4e1552ac..0b717159d 100644 --- a/docs/chapter_sorting/intro_to_sort.md +++ b/docs/chapter_sorting/intro_to_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.1. 排序简介 +# 排序简介 「排序算法 Sorting Algorithm」使得列表中的所有元素按照从小到大的顺序排列。 @@ -13,7 +13,7 @@ comments: true

Fig. 排序中的不同元素类型和判断规则

-## 11.1.1. 评价维度 +## 评价维度 排序算法主要可根据 **稳定性 、就地性 、自适应性 、比较类** 来分类。 @@ -64,7 +64,7 @@ comments: true 「比较类排序」的时间复杂度最优为 $O(n \log n)$ ;而「非比较类排序」可以达到 $O(n)$ 的时间复杂度,但通用性较差。 -## 11.1.2. 理想排序算法 +## 理想排序算法 - **运行快**,即时间复杂度低; - **稳定排序**,即排序后相等元素的相对位置不变化; diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index e794996e5..82d2fd845 100755 --- a/docs/chapter_sorting/merge_sort.md +++ b/docs/chapter_sorting/merge_sort.md @@ -2,7 +2,7 @@ comments: true --- -# 11.5. 归并排序 +# 归并排序 「归并排序 Merge Sort」是算法中“分治思想”的典型体现,其有「划分」和「合并」两个阶段: @@ -13,7 +13,7 @@ comments: true

Fig. 归并排序两阶段:划分与合并

-## 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)$** ,这是因为: diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index 6037e81f2..42e8e4416 100755 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -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)$ 。 diff --git a/docs/chapter_sorting/summary.md b/docs/chapter_sorting/summary.md index 71cc82609..df0b7a700 100644 --- a/docs/chapter_sorting/summary.md +++ b/docs/chapter_sorting/summary.md @@ -2,5 +2,5 @@ comments: true --- -# 11.6. 小结 +# 小结 diff --git a/docs/chapter_stack_and_queue/deque.md b/docs/chapter_stack_and_queue/deque.md index 7e85c6ce5..000f1d17e 100644 --- a/docs/chapter_stack_and_queue/deque.md +++ b/docs/chapter_stack_and_queue/deque.md @@ -2,7 +2,7 @@ comments: true --- -# 5.3. 双向队列 +# 双向队列 对于队列,我们只能在头部删除或在尾部添加元素,而「双向队列 Deque」更加灵活,在其头部和尾部都能执行元素添加或删除操作。 @@ -10,7 +10,7 @@ comments: true

Fig. 双向队列的操作

-## 5.3.1. 双向队列常用操作 +## 双向队列常用操作 双向队列的常用操作见下表,方法名需根据特定语言来确定。 @@ -293,7 +293,7 @@ comments: true ``` -## 5.3.2. 双向队列实现 * +## 双向队列实现 * 与队列类似,双向队列同样可以使用链表或数组来实现。 diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index 0e8855d52..5f7912d6c 100755 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -2,7 +2,7 @@ comments: true --- -# 5.2. 队列 +# 队列 「队列 Queue」是一种遵循「先入先出 first in, first out」数据操作规则的线性数据结构。顾名思义,队列模拟的是排队现象,即外面的人不断加入队列尾部,而处于队列头部的人不断地离开。 @@ -12,7 +12,7 @@ comments: true

Fig. 队列的先入先出特性

-## 5.2.1. 队列常用操作 +## 队列常用操作 队列的常用操作见下表,方法名需根据特定语言来确定。 @@ -262,7 +262,7 @@ comments: true ``` -## 5.2.2. 队列实现 +## 队列实现 队列需要一种可以在一端添加,并在另一端删除的数据结构,也可以使用链表或数组来实现。 @@ -429,11 +429,11 @@ comments: true 以上实现的队列仍存在局限性,即长度不可变。不过这个问题很容易解决,我们可以将数组替换为列表(即动态数组),从而引入扩容机制。有兴趣的同学可以尝试自行实现。 -## 5.2.3. 两种实现对比 +## 两种实现对比 与栈的结论一致,在此不再赘述。 -## 5.2.4. 队列典型应用 +## 队列典型应用 - **淘宝订单**。购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。 - **各种待办事项**。任何需要实现“先来后到”的功能,例如打印机的任务队列、餐厅的出餐队列等等。 diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index bc51d0c7f..14f065385 100755 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -2,7 +2,7 @@ comments: true --- -# 5.1. 栈 +# 栈 「栈 Stack」是一种遵循「先入后出 first in, last out」数据操作规则的线性数据结构。我们可以将栈类比为放在桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。 @@ -14,7 +14,7 @@ comments: true

Fig. 栈的先入后出特性

-## 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. 栈典型应用 +## 栈典型应用 - **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就将上一个网页执行入栈,这样我们就可以通过「后退」操作来回到上一页面,后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么则需要两个栈来配合实现。 - **程序内存管理**。每当调用函数时,系统就会在栈顶添加一个栈帧,用来记录函数的上下文信息。在递归函数中,向下递推会不断执行入栈,向上回溯阶段时出栈。 diff --git a/docs/chapter_stack_and_queue/summary.md b/docs/chapter_stack_and_queue/summary.md index 19cf6c8c3..d6066f0f1 100644 --- a/docs/chapter_stack_and_queue/summary.md +++ b/docs/chapter_stack_and_queue/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 5.4. 小结 +# 小结 - 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。 - 在时间效率方面,栈的数组实现具有更好的平均效率,但扩容时会导致单次入栈操作的时间复杂度劣化至 $O(n)$ 。相对地,栈的链表实现具有更加稳定的效率表现。 diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index 2c1ee4b0e..41a35ba72 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -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 树典型应用 - 组织存储大型数据,适用于高频查找、低频增删场景; - 用于建立数据库中的索引系统; diff --git a/docs/chapter_tree/binary_search_tree.md b/docs/chapter_tree/binary_search_tree.md index 7a1890a9c..e17daee00 100755 --- a/docs/chapter_tree/binary_search_tree.md +++ b/docs/chapter_tree/binary_search_tree.md @@ -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 -## 7.3.3. 二叉搜索树的退化 +## 二叉搜索树的退化 理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 @@ -331,7 +331,7 @@ comments: true ![bst_degradation](binary_search_tree.assets/bst_degradation.png) -## 7.3.4. 二叉搜索树常见应用 +## 二叉搜索树常见应用 - 系统中的多级索引,高效查找、插入、删除操作。 - 各种搜索算法的底层数据结构。 diff --git a/docs/chapter_tree/binary_tree.md b/docs/chapter_tree/binary_tree.md index a6a71577a..0fcd32222 100644 --- a/docs/chapter_tree/binary_tree.md +++ b/docs/chapter_tree/binary_tree.md @@ -2,7 +2,7 @@ comments: true --- -# 7.1. 二叉树 +# 二叉树 「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含「值」和两个「指针」。 @@ -135,7 +135,7 @@ comments: true

Fig. 子结点与子树

-## 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 -## 7.1.5. 二叉树表示方式 * +## 二叉树表示方式 * 我们一般使用二叉树的「链表表示」,即存储单位为结点 `TreeNode` ,结点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。 diff --git a/docs/chapter_tree/binary_tree_traversal.md b/docs/chapter_tree/binary_tree_traversal.md index 97bf3f68c..9a03c59f7 100755 --- a/docs/chapter_tree/binary_tree_traversal.md +++ b/docs/chapter_tree/binary_tree_traversal.md @@ -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」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。 diff --git a/docs/chapter_tree/summary.md b/docs/chapter_tree/summary.md index 8566eaa8e..15dc2f6d3 100644 --- a/docs/chapter_tree/summary.md +++ b/docs/chapter_tree/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 7.5. 小结 +# 小结 - 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。 - 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。 diff --git a/docs/utils/build_markdown.py b/docs/utils/build_markdown.py index 5af0194f8..92aa59824 100755 --- a/docs/utils/build_markdown.py +++ b/docs/utils/build_markdown.py @@ -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") diff --git a/docs/utils/number_headings.py b/docs/utils/number_headings.py new file mode 100755 index 000000000..8d53dd5e6 --- /dev/null +++ b/docs/utils/number_headings.py @@ -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)