pull/944/head
krahets 2 years ago
parent 8f74aa1fad
commit ed43cdf179

@ -2,7 +2,7 @@
comments: true
---
# 4.1. 数组
# 4.1.   数组
「数组 Array」是一种将 **相同类型元素** 存储在 **连续内存空间** 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。
@ -102,7 +102,7 @@ comments: true
var nums = [_]i32{ 1, 3, 2, 5, 4 };
```
## 4.1.1. 数组优点
## 4.1.1.   数组优点
**在数组中访问元素非常高效**。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。
@ -244,7 +244,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
}
```
## 4.1.2. 数组缺点
## 4.1.2.   数组缺点
**数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
@ -624,7 +624,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
}
```
## 4.1.3. 数组常用操作
## 4.1.3.   数组常用操作
**数组遍历**。以下介绍两种常用的遍历方法。
@ -914,7 +914,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
}
```
## 4.1.4. 数组典型应用
## 4.1.4.   数组典型应用
**随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。

@ -2,7 +2,7 @@
comments: true
---
# 4.2. 链表
# 4.2.   链表
!!! note "引言"
@ -314,7 +314,7 @@ comments: true
n3.next = &n4;
```
## 4.2.1. 链表优点
## 4.2.1.   链表优点
**在链表中,插入与删除结点的操作效率高**。例如,如果想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。
@ -524,7 +524,7 @@ comments: true
}
```
## 4.2.2. 链表缺点
## 4.2.2.   链表缺点
**链表访问结点效率低**。上节提到,数组可以在 $O(1)$ 时间下访问任意元素,但链表无法直接访问任意结点。这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 `index` (即第 `index + 1` 个)的结点,那么需要 `index` 次访问操作。
@ -668,7 +668,7 @@ comments: true
**链表的内存占用多**。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。
## 4.2.3. 链表常用操作
## 4.2.3.   链表常用操作
**遍历链表查找**。遍历链表,查找链表内值为 `target` 的结点,输出结点在链表中的索引。
@ -827,7 +827,7 @@ comments: true
}
```
## 4.2.4. 常见链表类型
## 4.2.4.   常见链表类型
**单向链表**。即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的「指针(引用)」两项数据。我们将首个结点称为头结点,尾结点指向 `null`

@ -2,13 +2,13 @@
comments: true
---
# 4.3. 列表
# 4.3.   列表
**由于长度不可变,数组的实用性大大降低**。在很多情况下,我们事先并不知道会输入多少数据,这就为数组长度的选择带来了很大困难。长度选小了,需要在添加数据中频繁地扩容数组;长度选大了,又造成内存空间的浪费。
为了解决此问题,诞生了一种被称为「列表 List」的数据结构。列表可以被理解为长度可变的数组因此也常被称为「动态数组 Dynamic Array」。列表基于数组实现继承了数组的优点同时还可以在程序运行中实时扩容。在列表中我们可以自由地添加元素而不用担心超过容量限制。
## 4.3.1. 列表常用操作
## 4.3.1.   列表常用操作
**初始化列表**。我们通常会使用到“无初始值”和“有初始值”的两种初始化方法。
@ -703,7 +703,7 @@ comments: true
std.sort.sort(i32, list.items, {}, comptime std.sort.asc(i32));
```
## 4.3.2. 列表简易实现 *
## 4.3.2.   列表简易实现 *
为了帮助加深对列表的理解,我们在此提供一个列表的简易版本的实现。需要关注三个核心点:

@ -2,14 +2,14 @@
comments: true
---
# 4.4. 小结
# 4.4.   小结
- 数组和链表是两种基本数据结构,代表了数据在计算机内存中的两种存储方式,即连续空间存储和离散空间存储。两者的优点与缺点呈现出此消彼长的关系。
- 数组支持随机访问、内存空间占用小;但插入与删除元素效率低,且初始化后长度不可变。
- 链表可通过更改指针实现高效的结点插入与删除,并且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型有单向链表、循环链表、双向链表。
- 列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,且可以灵活改变长度。列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费。
## 4.4.1. 数组 VS 链表
## 4.4.1.   数组 VS 链表
<p align="center"> Table. 数组与链表特点对比 </p>

@ -2,9 +2,9 @@
comments: true
---
# 2.1. 算法效率评估
# 2.1. &nbsp; 算法效率评估
## 2.1.1. 算法评价维度
## 2.1.1. &nbsp; 算法评价维度
在开始学习算法之前,我们首先要想清楚算法的设计目标是什么,或者说,如何来评判算法的好与坏。整体上看,我们设计算法时追求两个层面的目标。
@ -18,7 +18,7 @@ comments: true
数据结构与算法追求“运行速度快、占用内存少”,而如何去评价算法效率则是非常重要的问题,因为只有知道如何评价算法,才能去做算法之间的对比分析,以及优化算法设计。
## 2.1.2. 效率评估方法
## 2.1.2. &nbsp; 效率评估方法
### 实际测试
@ -42,7 +42,7 @@ comments: true
如果感觉对复杂度分析的概念一知半解,无需担心,后续章节会展开介绍。
## 2.1.3. 复杂度分析重要性
## 2.1.3. &nbsp; 复杂度分析重要性
复杂度分析给出一把评价算法效率的“标尺”,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。

@ -2,11 +2,11 @@
comments: true
---
# 2.3. 空间复杂度
# 2.3. &nbsp; 空间复杂度
「空间复杂度 Space Complexity」统计 **算法使用内存空间随着数据量变大时的增长趋势**。这个概念与时间复杂度很类似。
## 2.3.1. 算法相关空间
## 2.3.1. &nbsp; 算法相关空间
算法运行中,使用的内存空间主要有以下几种:
@ -252,7 +252,7 @@ comments: true
```
## 2.3.2. 推算方法
## 2.3.2. &nbsp; 推算方法
空间复杂度的推算方法和时间复杂度总体类似,只是从统计“计算操作数量”变为统计“使用空间大小”。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。
@ -554,7 +554,7 @@ comments: true
```
## 2.3.3. 常见类型
## 2.3.3. &nbsp; 常见类型
设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列)

@ -2,7 +2,7 @@
comments: true
---
# 2.4. 权衡时间与空间
# 2.4. &nbsp; 权衡时间与空间
理想情况下,我们希望算法的时间复杂度和空间复杂度都能够达到最优,而实际上,同时优化时间复杂度和空间复杂度是非常困难的。
@ -10,7 +10,7 @@ comments: true
大多数情况下,时间都是比空间更宝贵的,只要空间复杂度不要太离谱、能接受就行,**因此以空间换时间最为常用**。
## 2.4.1. 示例题目 *
## 2.4.1. &nbsp; 示例题目 *
以 LeetCode 全站第一题 [两数之和](https://leetcode.cn/problems/two-sum/) 为例。

@ -2,7 +2,7 @@
comments: true
---
# 2.5. 小结
# 2.5. &nbsp; 小结
### 算法效率评估

@ -2,9 +2,9 @@
comments: true
---
# 2.2. 时间复杂度
# 2.2. &nbsp; 时间复杂度
## 2.2.1. 统计算法运行时间
## 2.2.1. &nbsp; 统计算法运行时间
运行时间能够直观且准确地体现出算法的效率水平。如果我们想要 **准确预估一段代码的运行时间** ,该如何做呢?
@ -161,7 +161,7 @@ $$
但实际上, **统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,毕竟算法需要跑在各式各样的平台之上。其次,我们很难获知每一种操作的运行时间,这为预估过程带来了极大的难度。
## 2.2.2. 统计时间增长趋势
## 2.2.2. &nbsp; 统计时间增长趋势
「时间复杂度分析」采取了不同的做法,其统计的不是算法运行时间,而是 **算法运行时间随着数据量变大时的增长趋势** 。
@ -381,7 +381,7 @@ $$
**时间复杂度也存在一定的局限性**。比如,虽然算法 `A``C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B``C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。对于以上情况,我们很难仅凭时间复杂度来判定算法效率高低。然而,即使存在这些问题,计算复杂度仍然是评判算法效率的最有效且常用的方法。
## 2.2.3. 函数渐近上界
## 2.2.3. &nbsp; 函数渐近上界
设算法「计算操作数量」为 $T(n)$ ,其是一个关于输入数据大小 $n$ 的函数。例如,以下算法的操作数量为
@ -548,7 +548,7 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得
渐近上界的数学味儿有点重,如果你感觉没有完全理解,无需担心,因为在实际使用中我们只需要会推算即可,数学意义可以慢慢领悟。
## 2.2.4. 推算方法
## 2.2.4. &nbsp; 推算方法
推算出 $f(n)$ 后,我们就得到时间复杂度 $O(f(n))$ 。那么,如何来确定渐近上界 $f(n)$ 呢?总体分为两步,首先「统计操作数量」,然后「判断渐近上界」。
@ -767,7 +767,7 @@ $$
</div>
## 2.2.5. 常见类型
## 2.2.5. &nbsp; 常见类型
设输入数据大小为 $n$ ,常见的时间复杂度类型有(从低到高排列)
@ -2407,7 +2407,7 @@ $$
<p align="center"> Fig. 阶乘阶的时间复杂度 </p>
## 2.2.6. 最差、最佳、平均时间复杂度
## 2.2.6. &nbsp; 最差、最佳、平均时间复杂度
**某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**。举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论:

@ -2,11 +2,11 @@
comments: true
---
# 3.2. 数据结构分类
# 3.2. &nbsp; 数据结构分类
数据结构主要可根据「逻辑结构」和「物理结构」两种角度进行分类。
## 3.2.1. 逻辑结构:线性与非线性
## 3.2.1. &nbsp; 逻辑结构:线性与非线性
**「逻辑结构」反映了数据之间的逻辑关系**。数组和链表的数据按照顺序依次排列,反映了数据间的线性关系;树从顶至底按层级排列,反映了祖先与后代之间的派生关系;图由结点和边组成,反映了复杂网络关系。
@ -19,7 +19,7 @@ comments: true
<p align="center"> Fig. 线性与非线性数据结构 </p>
## 3.2.2. 物理结构:连续与离散
## 3.2.2. &nbsp; 物理结构:连续与离散
!!! note

@ -2,9 +2,9 @@
comments: true
---
# 3.1. 数据与内存
# 3.1. &nbsp; 数据与内存
## 3.1.1. 基本数据类型
## 3.1.1. &nbsp; 基本数据类型
谈到计算机中的数据我们能够想到文本、图片、视频、语音、3D 模型等等,这些数据虽然组织形式不同,但都是由各种基本数据类型构成的。
@ -202,7 +202,7 @@ $$
```
## 3.1.2. 计算机内存
## 3.1.2. &nbsp; 计算机内存
在计算机中,内存和硬盘是两种主要的存储硬件设备。「硬盘」主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。「内存」用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。

@ -2,7 +2,7 @@
comments: true
---
# 3.3. 小结
# 3.3. &nbsp; 小结
- 整数 byte, short, int, long 、浮点数 float, double 、字符 char 、布尔 boolean 是计算机中的基本数据类型,占用空间的大小决定了它们的取值范围。
- 在程序运行时,数据存储在计算机的内存中。内存中每块空间都有独立的内存地址,程序是通过内存地址来访问数据的。

@ -2,7 +2,7 @@
comments: true
---
# 9.1. 图
# 9.1. &nbsp;
「图 Graph」是一种非线性数据结构由「顶点 Vertex」和「边 Edge」组成。我们可将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。例如,以下表示一个包含 5 个顶点和 7 条边的图
@ -18,7 +18,7 @@ $$
那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**。
## 9.1.1. 图常见类型
## 9.1.1. &nbsp; 图常见类型
根据边是否有方向,分为「无向图 Undirected Graph」和「有向图 Directed Graph」。
@ -38,13 +38,13 @@ $$
![weighted_graph](graph.assets/weighted_graph.png)
## 9.1.2. 图常用术语
## 9.1.2. &nbsp; 图常用术语
- 「邻接 Adjacency」当两顶点之间有边相连时称此两顶点“邻接”。
- 「路径 Path」从顶点 A 到顶点 B 走过的边构成的序列,被称为从 A 到 B 的“路径”。
- 「度 Degree」表示一个顶点具有多少条边。对于有向图「入度 In-Degree」表示有多少条边指向该顶点「出度 Out-Degree」表示有多少条边从该顶点指出。
## 9.1.3. 图的表示
## 9.1.3. &nbsp; 图的表示
图的常用表示方法有「邻接矩阵」和「邻接表」。以下使用「无向图」来举例。
@ -74,7 +74,7 @@ $$
观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如当链表较长时可以把链表转化为「AVL 树」,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet即哈希表将时间复杂度降低至 $O(1)$ 。
## 9.1.4. 图常见应用
## 9.1.4. &nbsp; 图常见应用
现实中的许多系统都可以使用图来建模,对应的待求解问题也可以被约化为图计算问题。

@ -2,11 +2,11 @@
comments: true
---
# 9.2. 图基础操作
# 9.2. &nbsp; 图基础操作
图的基础操作分为对「边」的操作和对「顶点」的操作,在「邻接矩阵」和「邻接表」这两种表示下的实现方式不同。
## 9.2.1. 基于邻接矩阵的实现
## 9.2.1. &nbsp; 基于邻接矩阵的实现
设图的顶点总数为 $n$ ,则有:
@ -775,7 +775,7 @@ comments: true
```
## 9.2.2. 基于邻接表的实现
## 9.2.2. &nbsp; 基于邻接表的实现
设图的顶点总数为 $n$ 、边总数为 $m$ ,则有:
@ -1451,7 +1451,7 @@ comments: true
[class]{GraphAdjList}-[func]{}
```
## 9.2.3. 效率对比
## 9.2.3. &nbsp; 效率对比
设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。

@ -2,7 +2,7 @@
comments: true
---
# 9.3. 图的遍历
# 9.3. &nbsp; 图的遍历
!!! note "图与树的关系"
@ -12,7 +12,7 @@ comments: true
类似地,图的遍历方式也分为两种,即「广度优先遍历 Breadth-First Traversal」和「深度优先遍历 Depth-First Travsersal」也称「广度优先搜索 Breadth-First Search」和「深度优先搜索 Depth-First Search」简称为 BFS 和 DFS 。
## 9.3.1. 广度优先遍历
## 9.3.1. &nbsp; 广度优先遍历
**广度优先遍历优是一种由近及远的遍历方式,从距离最近的顶点开始访问,并一层层向外扩张**。具体地,从某个顶点出发,先遍历该顶点的所有邻接顶点,随后遍历下个顶点的所有邻接顶点,以此类推……
@ -200,7 +200,7 @@ BFS 常借助「队列」来实现。队列具有“先入先出”的性质,
**空间复杂度:** 列表 `res` ,哈希表 `visited` ,队列 `que` 中的顶点数量最多为 $|V|$ ,使用 $O(|V|)$ 空间。
## 9.3.2. 深度优先遍历
## 9.3.2. &nbsp; 深度优先遍历
**深度优先遍历是一种优先走到底、无路可走再回头的遍历方式**。具体地,从某个顶点出发,不断地访问当前结点的某个邻接顶点,直到走到尽头时回溯,再继续走到底 + 回溯,以此类推……直至所有顶点遍历完成时结束。

@ -2,7 +2,7 @@
comments: true
---
# 6.2. 哈希冲突
# 6.2. &nbsp; 哈希冲突
理想情况下,哈希函数应该为每个输入产生唯一的输出,使得 key 和 value 一一对应。而实际上,往往存在向哈希函数输入不同的 key 而产生相同输出的情况,这种情况被称为「哈希冲突 Hash Collision」。哈希冲突会导致查询结果错误从而严重影响哈希表的可用性。
@ -12,7 +12,7 @@ comments: true
另一方面,**考虑通过优化数据结构以缓解哈希冲突**,常见的方法有「链式地址」和「开放寻址」。
## 6.2.1. 哈希表扩容
## 6.2.1. &nbsp; 哈希表扩容
「负载因子 Load Factor」定义为 **哈希表中元素数量除以桶槽数量(即数组大小)**,代表哈希冲突的严重程度。
@ -20,7 +20,7 @@ comments: true
与数组扩容类似,**哈希表扩容操作的开销很大**,因为需要将所有键值对从原哈希表依次移动至新哈希表。
## 6.2.2. 链式地址
## 6.2.2. &nbsp; 链式地址
在原始哈希表中,桶内的每个地址只能存储一个元素(即键值对)。**考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中**。
@ -39,7 +39,7 @@ comments: true
为了缓解时间效率问题,**可以把「链表」转化为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。
## 6.2.3. 开放寻址
## 6.2.3. &nbsp; 开放寻址
「开放寻址」不引入额外数据结构,而是通过“多次探测”来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希**。

@ -2,7 +2,7 @@
comments: true
---
# 6.1. 哈希表
# 6.1. &nbsp; 哈希表
哈希表通过建立「键 key」和「值 value」之间的映射实现高效的元素查找。具体地输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 $O(1)$ 。
@ -12,7 +12,7 @@ comments: true
<p align="center"> Fig. 哈希表抽象表示 </p>
## 6.1.1. 哈希表效率
## 6.1.1. &nbsp; 哈希表效率
除了哈希表之外,还可以使用以下数据结构来实现上述查询功能:
@ -33,7 +33,7 @@ comments: true
</div>
## 6.1.2. 哈希表常用操作
## 6.1.2. &nbsp; 哈希表常用操作
哈希表的基本操作包括 **初始化、查询操作、添加与删除键值对**。
@ -391,7 +391,7 @@ comments: true
```
## 6.1.3. 哈希函数
## 6.1.3. &nbsp; 哈希函数
哈希表中存储元素的数据结构被称为「桶 Bucket」底层实现可能是数组、链表、二叉树红黑树或是它们的组合。
@ -1265,7 +1265,7 @@ $$
}
```
## 6.1.4. 哈希冲突
## 6.1.4. &nbsp; 哈希冲突
细心的同学可能会发现,**哈希函数 $f(x) = x \% 100$ 会在某些情况下失效**。具体地,当输入的 key 后两位相同时,哈希函数的计算结果也相同,指向同一个 value 。例如,分别查询两个学号 $12836$ 和 $20336$ ,则有

@ -2,4 +2,4 @@
comments: true
---
# 6.3. 小结
# 6.3. &nbsp; 小结

@ -2,7 +2,7 @@
comments: true
---
# 8.1. 堆
# 8.1. &nbsp;
「堆 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.1. &nbsp; 堆术语与性质
- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。
- 二叉树中的根结点对应「堆顶」,底层最靠右结点对应「堆底」。
- 对于大顶堆 / 小顶堆,其堆顶元素(即根结点)的值最大 / 最小。
## 8.1.2. 堆常用操作
## 8.1.2. &nbsp; 堆常用操作
值得说明的是,多数编程语言提供的是「优先队列 Priority Queue」其是一种抽象数据结构**定义为具有出队优先级的队列**。
@ -306,7 +306,7 @@ comments: true
```
## 8.1.3. 堆的实现
## 8.1.3. &nbsp; 堆的实现
下文实现的是「大顶堆」,若想转换为「小顶堆」,将所有大小逻辑判断取逆(例如将 $\geq$ 替换为 $\leq$ )即可,有兴趣的同学可自行实现。
@ -1456,7 +1456,7 @@ $$
进一步地,高度为 $h$ 的完美二叉树的结点数量为 $n = 2^{h+1} - 1$ ,易得复杂度为 $O(2^h) = O(n)$。以上推算表明,**输入列表并建堆的时间复杂度为 $O(n)$ ,非常高效**。
## 8.1.4. 堆常见应用
## 8.1.4. &nbsp; 堆常见应用
- **优先队列**。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 $O(\log n)$ ,建队操作为 $O(n)$ ,皆非常高效。
- **堆排序**。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可。

@ -2,7 +2,7 @@
comments: true
---
# 1.1. 算法无处不在
# 1.1. &nbsp; 算法无处不在
听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。

@ -2,9 +2,9 @@
comments: true
---
# 1.2. 算法是什么
# 1.2. &nbsp; 算法是什么
## 1.2.1. 算法定义
## 1.2.1. &nbsp; 算法定义
「算法 Algorithm」是在有限时间内解决特定问题的一组指令或操作步骤。算法具有以下特性
@ -13,7 +13,7 @@ comments: true
- 具有可行性,可在有限步骤、有限时间、有限内存空间下完成。
- 独立于编程语言,即可用多种语言实现。
## 1.2.2. 数据结构定义
## 1.2.2. &nbsp; 数据结构定义
「数据结构 Data Structure」是在计算机中组织与存储数据的方式。为了提高数据存储和操作性能数据结构的设计原则有
@ -23,7 +23,7 @@ comments: true
数据结构的设计是一个充满权衡的过程,这意味着如果获得某方面的优势,则往往需要在另一方面做出妥协。例如,链表相对于数组,数据添加删除操作更加方便,但牺牲了数据的访问速度;图相对于链表,提供了更多的逻辑信息,但需要占用更多的内存空间。
## 1.2.3. 数据结构与算法的关系
## 1.2.3. &nbsp; 数据结构与算法的关系
「数据结构」与「算法」是高度相关、紧密嵌合的,体现在:

@ -2,7 +2,7 @@
comments: true
---
# 0.1. 关于本书
# 0.1. &nbsp; 关于本书
五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出“快速排序”代码,我畏畏缩缩地写了一个“冒泡排序”,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。
@ -12,7 +12,7 @@ comments: true
<h4 align="center"> Hello算法 </h4>
## 0.1.1. 读者对象
## 0.1.1. &nbsp; 读者对象
!!! success "前置条件"
@ -32,7 +32,7 @@ comments: true
如果您是 **算法大佬**,请受我膜拜!希望您可以抽时间提出意见建议,或者[一起参与创作](https://www.hello-algo.com/chapter_preface/contribution/),帮助各位同学获取更好的学习内容,感谢!
## 0.1.2. 内容结构
## 0.1.2. &nbsp; 内容结构
本书主要内容分为复杂度分析、数据结构、算法三个部分。
@ -71,7 +71,7 @@ comments: true
- 实现方法:完整的算法实现,以及优化措施;
- 示例题目:结合例题加深理解;
## 0.1.3. 配套代码
## 0.1.3. &nbsp; 配套代码
完整代码托管在 [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. 风格约定
## 0.1.4. &nbsp; 风格约定
- 标题后标注 * 符号的是选读章节,如果你的时间有限,可以先跳过这些章节。
- 文章中的重要名词会用「」符号标注,例如「数组 Array」。名词混淆会导致不必要的歧义因此最好可以记住这类名词包括中文和英文以便后续阅读文献时使用。
@ -216,7 +216,7 @@ comments: true
// 注释
```
## 0.1.5. 本书特点 *
## 0.1.5. &nbsp; 本书特点 *
??? abstract "默认折叠,可以跳过"
@ -238,7 +238,7 @@ comments: true
敲代码如同写字,“美”是统一的追求。本书力求美观的代码,保证规范的变量命名、统一的空格与换行、对齐的缩进、整齐的注释等。
## 0.1.6. 致谢
## 0.1.6. &nbsp; 致谢
本书的成书过程中,我获得了许多人的帮助,包括但不限于:
@ -254,7 +254,7 @@ comments: true
感谢父母,你们一贯的支持与鼓励给了我自由度来做这些有趣的事。
## 0.1.7. 作者简介
## 0.1.7. &nbsp; 作者简介
![profile](about_the_book.assets/profile.png){: .center}

@ -2,7 +2,7 @@
comments: true
---
# 0.4. 一起参与创作
# 0.4. &nbsp; 一起参与创作
!!! success "开源的魅力"
@ -10,7 +10,7 @@ comments: true
由于作者水平有限,书中内容难免疏漏谬误,请您谅解。如果发现笔误、无效链接、内容缺失、文字歧义、解释不清晰、行文结构不合理等问题,请您帮忙修正,以帮助其他读者获取更优质的学习内容。所有[撰稿人](https://github.com/krahets/hello-algo/graphs/contributors)将被展示在仓库与网站主页,以感谢他们对开源社区的无私奉献!
## 0.4.1. 内容微调
## 0.4.1. &nbsp; 内容微调
每个页面的右上角都有一个「编辑」图标,你可以按照以下步骤修改文字或代码:
@ -22,7 +22,7 @@ comments: true
图片无法直接修改,需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述图片问题,我会第一时间重新画图并替换图片。
## 0.4.2. 内容创作
## 0.4.2. &nbsp; 内容创作
如果您想要参与本开源项目,包括翻译代码至其他编程语言、拓展文章内容等,那么需要实施 Pull Request 工作流程:
@ -32,7 +32,7 @@ comments: true
4. 将本地更改 Commit ,并 Push 至远程仓库;
5. 刷新仓库网页点击“Create pull request”按钮发起拉取请求即可
## 0.4.3. Docker 部署
## 0.4.3. &nbsp; Docker 部署
你可以使用 Docker 来部署本项目。

@ -2,51 +2,51 @@
comments: true
---
# 0.3. 编程环境安装
# 0.3. &nbsp; 编程环境安装
TODO 视频教程)
## 0.3.1. 安装 VSCode
## 0.3.1. &nbsp; 安装 VSCode
本书推荐使用开源轻量的 VSCode 作为本地 IDE ,下载并安装 [VSCode](https://code.visualstudio.com/) 。
## 0.3.2. Java 环境
## 0.3.2. &nbsp; Java 环境
1. 下载并安装 [OpenJDK](https://jdk.java.net/18/)(版本需满足 > JDK 9
2. 在 VSCode 的插件市场中搜索 `java` ,安装 Java Extension Pack 。
## 0.3.3. C/C++ 环境
## 0.3.3. &nbsp; 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 环境
## 0.3.4. &nbsp; Python 环境
1. 下载并安装 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) 。
2. 在 VSCode 的插件市场中搜索 `python` ,安装 Python Extension Pack 。
## 0.3.5. Go 环境
## 0.3.5. &nbsp; Go 环境
1. 下载并安装 [go](https://go.dev/dl/) 。
2. 在 VSCode 的插件市场中搜索 `go` ,安装 Go 。
3. 快捷键 `Ctrl + Shift + P` 呼出命令栏,输入 go ,选择 `Go: Install/Update Tools` ,全部勾选并安装即可。
## 0.3.6. JavaScript 环境
## 0.3.6. &nbsp; JavaScript 环境
1. 下载并安装 [node.js](https://nodejs.org/en/) 。
2. 在 VSCode 的插件市场中搜索 `javascript` ,安装 JavaScript (ES6) code snippets 。
## 0.3.7. C# 环境
## 0.3.7. &nbsp; C# 环境
1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download)
2. 在 VSCode 的插件市场中搜索 `c#` ,安装 c# 。
## 0.3.8. Swift 环境
## 0.3.8. &nbsp; 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 环境
## 0.3.9. &nbsp; 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. &nbsp; 如何使用本书
## 0.2.1. 图文搭配学
## 0.2.1. &nbsp; 图文搭配学
视频和图片相比于文字的信息密度和结构化程度更高,更容易让人理解。在本书中,重点和难点知识会主要以动画、图解的形式呈现,而文字的作用则是作为动画和图的解释与补充。
@ -12,7 +12,7 @@ comments: true
![animation](suggestions.assets/animation.gif)
## 0.2.2. 代码实践学
## 0.2.2. &nbsp; 代码实践学
!!! tip "前置工作"
@ -44,7 +44,7 @@ git clone https://github.com/krahets/hello-algo.git
若学习时间紧张,**请至少将所有代码通读并运行一遍**。若时间允许,**强烈建议对照着代码自己敲一遍**,逐渐锻炼肌肉记忆。相比于读代码,写代码的过程往往能带来新的收获。
## 0.2.3. 提问讨论学
## 0.2.3. &nbsp; 提问讨论学
阅读本书时,请不要“惯着”那些弄不明白的知识点。如果有任何疑惑,**可以在评论区留下你的问题**,小伙伴们和我都会给予解答(您一般 3 天内会得到回复)。
@ -52,7 +52,7 @@ git clone https://github.com/krahets/hello-algo.git
![comment](suggestions.assets/comment.gif)
## 0.2.4. 算法学习“三步走”
## 0.2.4. &nbsp; 算法学习“三步走”
**第一阶段,算法入门,也正是本书的定位**。熟悉各种数据结构的特点、用法,学习各种算法的工作原理、用途、效率等。

@ -2,7 +2,7 @@
comments: true
---
# 10.2. 二分查找
# 10.2. &nbsp; 二分查找
「二分查找 Binary Search」利用数据的有序性通过每轮缩小一半搜索区间来查找目标元素。
@ -11,7 +11,7 @@ comments: true
- **要求输入数据是有序的**,这样才能通过判断大小关系来排除一半的搜索区间;
- **二分查找仅适用于数组**,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。
## 10.2.1. 算法实现
## 10.2.1. &nbsp; 算法实现
给定一个长度为 $n$ 的排序数组 `nums` ,元素从小到大排列。数组的索引取值范围为
@ -571,13 +571,13 @@ $$
```
## 10.2.2. 复杂度分析
## 10.2.2. &nbsp; 复杂度分析
**时间复杂度 $O(\log n)$** :其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。
**空间复杂度 $O(1)$** :指针 `i` , `j` 使用常数大小空间。
## 10.2.3. 优点与缺点
## 10.2.3. &nbsp; 优点与缺点
二分查找效率很高,体现在:

@ -2,7 +2,7 @@
comments: true
---
# 10.3. 哈希查找
# 10.3. &nbsp; 哈希查找
!!! question
@ -10,7 +10,7 @@ comments: true
「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」我们可以在 $O(1)$ 时间下实现“键 $\rightarrow$ 值”映射查找,体现着“以空间换时间”的算法思想。
## 10.3.1. 算法实现
## 10.3.1. &nbsp; 算法实现
如果我们想要给定数组中的一个目标元素 `target` ,获取该元素的索引,那么可以借助一个哈希表实现查找。
@ -245,13 +245,13 @@ comments: true
}
```
## 10.3.2. 复杂度分析
## 10.3.2. &nbsp; 复杂度分析
**时间复杂度 $O(1)$** :哈希表的查找操作使用 $O(1)$ 时间。
**空间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。
## 10.3.3. 优点与缺点
## 10.3.3. &nbsp; 优点与缺点
在哈希表中,**查找、插入、删除操作的平均时间复杂度都为 $O(1)$** ,这意味着无论是高频增删还是高频查找场景,哈希查找的性能表现都非常好。当然,一切的前提是保证哈希表未退化。

@ -2,11 +2,11 @@
comments: true
---
# 10.1. 线性查找
# 10.1. &nbsp; 线性查找
「线性查找 Linear Search」是一种最基础的查找方法其从数据结构的一端开始依次访问每个元素直到另一端后停止。
## 10.1.1. 算法实现
## 10.1.1. &nbsp; 算法实现
线性查找实质上就是遍历数据结构 + 判断条件。比如,我们想要在数组 `nums` 中查找目标元素 `target` 的对应索引,那么可以在数组中进行线性查找。
@ -328,13 +328,13 @@ comments: true
}
```
## 10.1.2. 复杂度分析
## 10.1.2. &nbsp; 复杂度分析
**时间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。
**空间复杂度 $O(1)$** :无需使用额外空间。
## 10.1.3. 优点与缺点
## 10.1.3. &nbsp; 优点与缺点
**线性查找的通用性极佳**。由于线性查找是依次访问元素的,即没有跳跃访问元素,因此数组或链表皆适用。

@ -2,7 +2,7 @@
comments: true
---
# 10.4. 小结
# 10.4. &nbsp; 小结
- 线性查找是一种最基础的查找方法,通过遍历数据结构 + 判断条件实现查找。
- 二分查找利用数据的有序性,通过循环不断缩小一半搜索区间来实现查找,其要求输入数据是有序的,并且仅适用于数组或基于数组实现的数据结构。

@ -2,7 +2,7 @@
comments: true
---
# 11.2. 冒泡排序
# 11.2. &nbsp; 冒泡排序
「冒泡排序 Bubble Sort」是一种最基础的排序算法非常适合作为第一个学习的排序算法。顾名思义「冒泡」是该算法的核心操作。
@ -37,7 +37,7 @@ comments: true
<p align="center"> Fig. 冒泡操作 </p>
## 11.2.1. 算法流程
## 11.2.1. &nbsp; 算法流程
1. 设数组长度为 $n$ ,完成第一轮「冒泡」后,数组最大元素已在正确位置,接下来只需排序剩余 $n - 1$ 个元素。
2. 同理,对剩余 $n - 1$ 个元素执行「冒泡」,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个。
@ -231,7 +231,7 @@ comments: true
}
```
## 11.2.2. 算法特性
## 11.2.2. &nbsp; 算法特性
**时间复杂度 $O(n^2)$** :各轮「冒泡」遍历的数组长度为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。
@ -243,7 +243,7 @@ comments: true
**自适应排序**:引入 `flag` 优化后(见下文),最佳时间复杂度为 $O(N)$ 。
## 11.2.3. 效率优化
## 11.2.3. &nbsp; 效率优化
我们发现,若在某轮「冒泡」中未执行任何交换操作,则说明数组已经完成排序,可直接返回结果。考虑可以增加一个标志位 `flag` 来监听该情况,若出现则直接返回。

@ -2,7 +2,7 @@
comments: true
---
# 11.3. 插入排序
# 11.3. &nbsp; 插入排序
「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。
@ -14,7 +14,7 @@ comments: true
<p align="center"> Fig. 插入操作 </p>
## 11.3.1. 算法流程
## 11.3.1. &nbsp; 算法流程
1. 第 1 轮先选取数组的 **第 2 个元素** 为 `base` ,执行「插入操作」后,**数组前 2 个元素已完成排序**。
2. 第 2 轮选取 **第 3 个元素** 为 `base` ,执行「插入操作」后,**数组前 3 个元素已完成排序**。
@ -197,7 +197,7 @@ comments: true
}
```
## 11.3.2. 算法特性
## 11.3.2. &nbsp; 算法特性
**时间复杂度 $O(n^2)$** :最差情况下,各轮插入操作循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,使用 $O(n^2)$ 时间。
@ -209,7 +209,7 @@ comments: true
**自适应排序**:最佳情况下,时间复杂度为 $O(n)$ 。
## 11.3.3. 插入排序 vs 冒泡排序
## 11.3.3. &nbsp; 插入排序 vs 冒泡排序
!!! question

@ -2,7 +2,7 @@
comments: true
---
# 11.1. 排序简介
# 11.1. &nbsp; 排序简介
「排序算法 Sorting Algorithm」使得列表中的所有元素按照从小到大的顺序排列。
@ -13,7 +13,7 @@ comments: true
<p align="center"> Fig. 排序中的不同元素类型和判断规则 </p>
## 11.1.1. 评价维度
## 11.1.1. &nbsp; 评价维度
排序算法主要可根据 **稳定性 、就地性 、自适应性 、比较类** 来分类。
@ -64,7 +64,7 @@ comments: true
「比较类排序」的时间复杂度最优为 $O(n \log n)$ ;而「非比较类排序」可以达到 $O(n)$ 的时间复杂度,但通用性较差。
## 11.1.2. 理想排序算法
## 11.1.2. &nbsp; 理想排序算法
- **运行快**,即时间复杂度低;
- **稳定排序**,即排序后相等元素的相对位置不变化;

@ -2,7 +2,7 @@
comments: true
---
# 11.5. 归并排序
# 11.5. &nbsp; 归并排序
「归并排序 Merge Sort」是算法中“分治思想”的典型体现其有「划分」和「合并」两个阶段
@ -13,7 +13,7 @@ comments: true
<p align="center"> Fig. 归并排序两阶段:划分与合并 </p>
## 11.5.1. 算法流程
## 11.5.1. &nbsp; 算法流程
**「递归划分」** 从顶至底递归地 **将数组从中点切为两个子数组**,直至长度为 1
@ -447,7 +447,7 @@ comments: true
- `nums` 的待合并区间为 `[left, right]` ,而因为 `tmp` 只复制了 `nums` 该区间元素,所以 `tmp` 对应区间为 `[0, right - left]` **需要特别注意代码中各个变量的含义**。
- 判断 `tmp[i]``tmp[j]` 的大小的操作中,还 **需考虑当子数组遍历完成后的索引越界问题**,即 `i > leftEnd``j > rightEnd` 的情况,索引越界的优先级是最高的,例如如果左子数组已经被合并完了,那么不用继续判断,直接合并右子数组元素即可。
## 11.5.2. 算法特性
## 11.5.2. &nbsp; 算法特性
- **时间复杂度 $O(n \log n)$** :划分形成高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,总体使用 $O(n \log n)$ 时间。
- **空间复杂度 $O(n)$** :需借助辅助数组实现合并,使用 $O(n)$ 大小的额外空间;递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间。
@ -455,7 +455,7 @@ comments: true
- **稳定排序**:在合并时可保证相等元素的相对位置不变。
- **非自适应排序**:对于任意输入数据,归并排序的时间复杂度皆相同。
## 11.5.3. 链表排序 *
## 11.5.3. &nbsp; 链表排序 *
归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为:

@ -2,7 +2,7 @@
comments: true
---
# 11.4. 快速排序
# 11.4. &nbsp; 快速排序
「快速排序 Quick Sort」是一种基于“分治思想”的排序算法速度很快、应用很广。
@ -288,7 +288,7 @@ comments: true
哨兵划分的实质是将 **一个长数组的排序问题** 简化为 **两个短数组的排序问题**。
## 11.4.1. 算法流程
## 11.4.1. &nbsp; 算法流程
1. 首先,对数组执行一次「哨兵划分」,得到待排序的 **左子数组** 和 **右子数组**
2. 接下来,对 **左子数组** 和 **右子数组** 分别 **递归执行**「哨兵划分」……
@ -451,7 +451,7 @@ comments: true
}
```
## 11.4.2. 算法特性
## 11.4.2. &nbsp; 算法特性
**平均时间复杂度 $O(n \log n)$** :平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。
@ -465,7 +465,7 @@ comments: true
**自适应排序**:最差情况下,时间复杂度劣化至 $O(n^2)$ 。
## 11.4.3. 快排为什么快?
## 11.4.3. &nbsp; 快排为什么快?
从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高**,这是因为:
@ -473,7 +473,7 @@ comments: true
- **缓存使用效率高**:哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。
- **复杂度的常数系数低**:在提及的三种算法中,快速排序的 **比较**、**赋值**、**交换** 三种操作的总体数量最少(类似于「插入排序」快于「冒泡排序」的原因)。
## 11.4.4. 基准数优化
## 11.4.4. &nbsp; 基准数优化
**普通快速排序在某些输入下的时间效率变差**。举个极端例子,假设输入数组是完全倒序的,由于我们选取最左端元素为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,从而 **左子数组长度为 $n - 1$、右子数组长度为 $0$** 。这样进一步递归下去,**每轮哨兵划分后的右子数组长度都为 $0$** ,分治策略失效,快速排序退化为「冒泡排序」了。
@ -796,7 +796,7 @@ comments: true
}
```
## 11.4.5. 尾递归优化
## 11.4.5. &nbsp; 尾递归优化
**普通快速排序在某些输入下的空间效率变差**。仍然以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 0 ,那么将形成一个高度为 $n - 1$ 的递归树,此时使用的栈帧空间大小劣化至 $O(n)$ 。

@ -2,5 +2,5 @@
comments: true
---
# 11.6. 小结
# 11.6. &nbsp; 小结

@ -2,7 +2,7 @@
comments: true
---
# 5.3. 双向队列
# 5.3. &nbsp; 双向队列
对于队列,我们只能在头部删除或在尾部添加元素,而「双向队列 Deque」更加灵活在其头部和尾部都能执行元素添加或删除操作。
@ -10,7 +10,7 @@ comments: true
<p align="center"> Fig. 双向队列的操作 </p>
## 5.3.1. 双向队列常用操作
## 5.3.1. &nbsp; 双向队列常用操作
双向队列的常用操作见下表,方法名需根据特定语言来确定。
@ -293,7 +293,7 @@ comments: true
```
## 5.3.2. 双向队列实现 *
## 5.3.2. &nbsp; 双向队列实现 *
与队列类似,双向队列同样可以使用链表或数组来实现。

@ -2,7 +2,7 @@
comments: true
---
# 5.2. 队列
# 5.2. &nbsp; 队列
「队列 Queue」是一种遵循「先入先出 first in, first out」数据操作规则的线性数据结构。顾名思义队列模拟的是排队现象即外面的人不断加入队列尾部而处于队列头部的人不断地离开。
@ -12,7 +12,7 @@ comments: true
<p align="center"> Fig. 队列的先入先出特性 </p>
## 5.2.1. 队列常用操作
## 5.2.1. &nbsp; 队列常用操作
队列的常用操作见下表,方法名需根据特定语言来确定。
@ -262,7 +262,7 @@ comments: true
```
## 5.2.2. 队列实现
## 5.2.2. &nbsp; 队列实现
队列需要一种可以在一端添加,并在另一端删除的数据结构,也可以使用链表或数组来实现。
@ -1628,11 +1628,11 @@ comments: true
以上实现的队列仍存在局限性,即长度不可变。不过这个问题很容易解决,我们可以将数组替换为列表(即动态数组),从而引入扩容机制。有兴趣的同学可以尝试自行实现。
## 5.2.3. 两种实现对比
## 5.2.3. &nbsp; 两种实现对比
与栈的结论一致,在此不再赘述。
## 5.2.4. 队列典型应用
## 5.2.4. &nbsp; 队列典型应用
- **淘宝订单**。购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。
- **各种待办事项**。任何需要实现“先来后到”的功能,例如打印机的任务队列、餐厅的出餐队列等等。

@ -2,7 +2,7 @@
comments: true
---
# 5.1. 栈
# 5.1. &nbsp;
「栈 Stack」是一种遵循「先入后出 first in, last out」数据操作规则的线性数据结构。我们可以将栈类比为放在桌面上的一摞盘子如果需要拿出底部的盘子则需要先将上面的盘子依次取出。
@ -14,7 +14,7 @@ comments: true
<p align="center"> Fig. 栈的先入后出特性 </p>
## 5.1.1. 栈常用操作
## 5.1.1. &nbsp; 栈常用操作
栈的常用操作见下表(方法命名以 Java 为例)。
@ -261,7 +261,7 @@ comments: true
```
## 5.1.2. 栈的实现
## 5.1.2. &nbsp; 栈的实现
为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。
@ -1305,7 +1305,7 @@ comments: true
}
```
## 5.1.3. 两种实现对比
## 5.1.3. &nbsp; 两种实现对比
### 支持操作
@ -1330,7 +1330,7 @@ comments: true
综上,我们不能简单地确定哪种实现更加省内存,需要 case-by-case 地分析。
## 5.1.4. 栈典型应用
## 5.1.4. &nbsp; 栈典型应用
- **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就将上一个网页执行入栈,这样我们就可以通过「后退」操作来回到上一页面,后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么则需要两个栈来配合实现。
- **程序内存管理**。每当调用函数时,系统就会在栈顶添加一个栈帧,用来记录函数的上下文信息。在递归函数中,向下递推会不断执行入栈,向上回溯阶段时出栈。

@ -2,7 +2,7 @@
comments: true
---
# 5.4. 小结
# 5.4. &nbsp; 小结
- 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。
- 在时间效率方面,栈的数组实现具有更好的平均效率,但扩容时会导致单次入栈操作的时间复杂度劣化至 $O(n)$ 。相对地,栈的链表实现具有更加稳定的效率表现。

@ -2,7 +2,7 @@
comments: true
---
# 7.4. AVL 树 *
# 7.4. &nbsp; 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 树常见术语
## 7.4.1. &nbsp; AVL 树常见术语
「AVL 树」既是「二叉搜索树」又是「平衡二叉树」,同时满足这两种二叉树的所有性质,因此又被称为「平衡二叉搜索树」。
@ -444,7 +444,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
设平衡因子为 $f$ ,则一棵 AVL 树的任意结点的平衡因子皆满足 $-1 \le f \le 1$ 。
## 7.4.2. AVL 树旋转
## 7.4.2. &nbsp; AVL 树旋转
AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影响二叉树中序遍历序列的前提下,使失衡结点重新恢复平衡**。换言之,旋转操作既可以使树保持为「二叉搜索树」,也可以使树重新恢复为「平衡二叉树」。
@ -1170,7 +1170,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
}
```
## 7.4.3. AVL 树常用操作
## 7.4.3. &nbsp; AVL 树常用操作
### 插入结点
@ -1944,7 +1944,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
「AVL 树」的结点查找操作与「二叉搜索树」一致,在此不再赘述。
## 7.4.4. AVL 树典型应用
## 7.4.4. &nbsp; AVL 树典型应用
- 组织存储大型数据,适用于高频查找、低频增删场景;
- 用于建立数据库中的索引系统;

@ -2,7 +2,7 @@
comments: true
---
# 7.3. 二叉搜索树
# 7.3. &nbsp; 二叉搜索树
「二叉搜索树 Binary Search Tree」满足以下条件
@ -11,7 +11,7 @@ comments: true
![binary_search_tree](binary_search_tree.assets/binary_search_tree.png)
## 7.3.1. 二叉搜索树的操作
## 7.3.1. &nbsp; 二叉搜索树的操作
### 查找结点
@ -1137,7 +1137,7 @@ comments: true
![bst_inorder_traversal](binary_search_tree.assets/bst_inorder_traversal.png)
## 7.3.2. 二叉搜索树的效率
## 7.3.2. &nbsp; 二叉搜索树的效率
假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:
@ -1166,7 +1166,7 @@ comments: true
</div>
## 7.3.3. 二叉搜索树的退化
## 7.3.3. &nbsp; 二叉搜索树的退化
理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。
@ -1178,7 +1178,7 @@ comments: true
![bst_degradation](binary_search_tree.assets/bst_degradation.png)
## 7.3.4. 二叉搜索树常见应用
## 7.3.4. &nbsp; 二叉搜索树常见应用
- 系统中的多级索引,高效查找、插入、删除操作。
- 各种搜索算法的底层数据结构。

@ -2,7 +2,7 @@
comments: true
---
# 7.1. 二叉树
# 7.1. &nbsp; 二叉树
「二叉树 Binary Tree」是一种非线性数据结构代表着祖先与后代之间的派生关系体现着“一分为二”的分治逻辑。类似于链表二叉树也是以结点为单位存储的结点包含「值」和两个「指针」。
@ -135,7 +135,7 @@ comments: true
<p align="center"> Fig. 子结点与子树 </p>
## 7.1.1. 二叉树常见术语
## 7.1.1. &nbsp; 二叉树常见术语
二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。
@ -156,7 +156,7 @@ comments: true
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。
## 7.1.2. 二叉树基本操作
## 7.1.2. &nbsp; 二叉树基本操作
**初始化二叉树**。与链表类似,先初始化结点,再构建引用指向(即指针)。
@ -422,7 +422,7 @@ comments: true
插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。
## 7.1.3. 常见二叉树类型
## 7.1.3. &nbsp; 常见二叉树类型
### 完美二叉树
@ -454,7 +454,7 @@ comments: true
![balanced_binary_tree](binary_tree.assets/balanced_binary_tree.png)
## 7.1.4. 二叉树的退化
## 7.1.4. &nbsp; 二叉树的退化
当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。
@ -478,7 +478,7 @@ comments: true
</div>
## 7.1.5. 二叉树表示方式 *
## 7.1.5. &nbsp; 二叉树表示方式 *
我们一般使用二叉树的「链表表示」,即存储单位为结点 `TreeNode` ,结点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。

@ -2,13 +2,13 @@
comments: true
---
# 7.2. 二叉树遍历
# 7.2. &nbsp; 二叉树遍历
从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历结点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。
常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。
## 7.2.1. 层序遍历
## 7.2.1. &nbsp; 层序遍历
「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树并在每层中按照从左到右的顺序访问结点。
@ -250,7 +250,7 @@ comments: true
**空间复杂度**:当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个结点,使用 $O(n)$ 空间。
## 7.2.2. 前序、中序、后序遍历
## 7.2.2. &nbsp; 前序、中序、后序遍历
相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」其体现着一种“先走到尽头再回头继续”的回溯遍历方式。

@ -2,7 +2,7 @@
comments: true
---
# 7.5. 小结
# 7.5. &nbsp; 小结
- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。
- 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。

Loading…
Cancel
Save