build
This commit is contained in:
parent
cf26cd551a
commit
9cd475d8c2
@ -8,6 +8,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 数组定义与存储方式 </p>
|
||||
|
||||
!!! note
|
||||
|
||||
观察上图,我们发现 **数组首元素的索引为 $0$** 。你可能会想,这并不符合日常习惯,首个元素的索引为什么不是 $1$ 呢,这不是更加自然吗?我认同你的想法,但请先记住这个设定,后面讲内存地址计算时,我会尝试解答这个问题。
|
||||
@ -106,6 +108,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 数组元素的内存地址计算 </p>
|
||||
|
||||
```shell
|
||||
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
|
||||
elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
@ -405,6 +409,8 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 数组插入元素 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
@ -527,6 +533,8 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 数组删除元素 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
|
@ -14,6 +14,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 链表定义与存储方式 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
@ -318,6 +320,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 链表插入结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
@ -427,6 +431,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 链表删除结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
@ -1018,3 +1024,5 @@ comments: true
|
||||
```
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 常见链表种类 </p>
|
||||
|
@ -26,6 +26,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 算法使用的相关空间 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
@ -565,6 +567,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 空间复杂度的常见类型 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解空间复杂度含义和推算方法上。
|
||||
@ -1078,6 +1082,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 递归函数产生的线性阶空间复杂度 </p>
|
||||
|
||||
### 平方阶 $O(n^2)$
|
||||
|
||||
平方阶常见于元素数量与 $n$ 成平方关系的矩阵、图。
|
||||
@ -1362,6 +1368,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 递归函数产生的平方阶空间复杂度 </p>
|
||||
|
||||
### 指数阶 $O(2^n)$
|
||||
|
||||
指数阶常见于二叉树。高度为 $n$ 的「满二叉树」的结点数量为 $2^n - 1$ ,使用 $O(2^n)$ 空间。
|
||||
@ -1496,6 +1504,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 满二叉树产生的指数阶空间复杂度 </p>
|
||||
|
||||
### 对数阶 $O(\log n)$
|
||||
|
||||
对数阶常见于分治算法、数据类型转换等。
|
||||
|
@ -371,6 +371,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 算法 A, B, C 的时间增长趋势 </p>
|
||||
|
||||
相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足?
|
||||
|
||||
**时间复杂度可以有效评估算法效率**。算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。
|
||||
@ -538,6 +540,8 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 函数的渐近上界 </p>
|
||||
|
||||
本质上看,计算渐近上界就是在找一个函数 $f(n)$ ,**使得在 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别(仅相差一个常数项 $c$ 的倍数)**。
|
||||
|
||||
!!! tip
|
||||
@ -776,6 +780,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 时间复杂度的常见类型 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
部分示例代码需要一些前置知识,包括数组、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解时间复杂度含义和推算方法上。
|
||||
@ -1328,6 +1334,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 常数阶、线性阶、平方阶的时间复杂度 </p>
|
||||
|
||||
以「冒泡排序」为例,外层循环 $n - 1$ 次,内层循环 $n-1, n-2, \cdots, 2, 1$ 次,平均为 $\frac{n}{2}$ 次,因此时间复杂度为 $O(n^2)$ 。
|
||||
|
||||
$$
|
||||
@ -1733,6 +1741,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 指数阶的时间复杂度 </p>
|
||||
|
||||
在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,分裂 $n$ 次后停止。
|
||||
|
||||
=== "Java"
|
||||
@ -1980,6 +1990,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 对数阶的时间复杂度 </p>
|
||||
|
||||
与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 $\log_2 n$ 的递归树。
|
||||
|
||||
=== "Java"
|
||||
@ -2233,6 +2245,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 线性对数阶的时间复杂度 </p>
|
||||
|
||||
### 阶乘阶 $O(n!)$
|
||||
|
||||
阶乘阶对应数学上的「全排列」。即给定 $n$ 个互不重复的元素,求其所有可能的排列方案,则方案数量为
|
||||
@ -2391,6 +2405,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 阶乘阶的时间复杂度 </p>
|
||||
|
||||
## 2.2.6. 最差、最佳、平均时间复杂度
|
||||
|
||||
**某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**。举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论:
|
||||
|
@ -17,6 +17,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 线性与非线性数据结构 </p>
|
||||
|
||||
## 3.2.2. 物理结构:连续与离散
|
||||
|
||||
!!! note
|
||||
@ -27,6 +29,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 连续空间存储与离散空间存储 </p>
|
||||
|
||||
**所有数据结构都是基于数组、或链表、或两者组合实现的**。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。
|
||||
|
||||
- **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等;
|
||||
|
@ -82,6 +82,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. IEEE 754 标准下的 float 表示方式 </p>
|
||||
|
||||
以上图为例,$\mathrm{S} = 0$ , $\mathrm{E} = 124$ ,$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ ,易得
|
||||
|
||||
$$
|
||||
@ -212,4 +214,6 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 内存条、内存空间、内存地址 </p>
|
||||
|
||||
**内存资源是设计数据结构与算法的重要考虑因素**。内存是所有程序的公共资源,当内存被某程序占用时,不能被其它程序同时使用。我们需要根据剩余内存资源的情况来设计算法。例如,若剩余内存空间有限,则要求算法占用的峰值内存不能超过系统剩余内存;若运行的程序很多、缺少大块连续的内存空间,则要求选取的数据结构必须能够存储在离散的内存空间内。
|
||||
|
@ -16,6 +16,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 链表、树、图之间的关系 </p>
|
||||
|
||||
那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**。
|
||||
|
||||
## 9.1.1. 图常见类型
|
||||
@ -27,6 +29,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 有向图与无向图 </p>
|
||||
|
||||
根据所有顶点是否连通,分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。
|
||||
|
||||
- 对于连通图,从某个顶点出发,可以到达其余任意顶点;
|
||||
@ -34,10 +38,14 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 连通图与非连通图 </p>
|
||||
|
||||
我们可以给边添加“权重”变量,得到「有权图 Weighted Graph」。例如,在王者荣耀等游戏中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以使用有权图来表示。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 有权图与无权图 </p>
|
||||
|
||||
## 9.1.2. 图常用术语
|
||||
|
||||
- 「邻接 Adjacency」:当两顶点之间有边相连时,称此两顶点“邻接”。
|
||||
@ -56,6 +64,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 图的邻接矩阵表示 </p>
|
||||
|
||||
邻接矩阵具有以下性质:
|
||||
|
||||
- 顶点不能与自身相连,因而邻接矩阵主对角线元素没有意义。
|
||||
@ -70,6 +80,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 图的邻接表表示 </p>
|
||||
|
||||
邻接表仅存储存在的边,而边的总数往往远小于 $n^2$ ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。
|
||||
|
||||
观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如,当链表较长时,可以把链表转化为「AVL 树」,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet(即哈希表),将时间复杂度降低至 $O(1)$ 。
|
||||
|
@ -18,6 +18,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 图的广度优先遍历 </p>
|
||||
|
||||
### 算法实现
|
||||
|
||||
BFS 常借助「队列」来实现。队列具有“先入先出”的性质,这与 BFS “由近及远”的思想是异曲同工的。
|
||||
@ -256,6 +258,8 @@ BFS 常借助「队列」来实现。队列具有“先入先出”的性质,
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 图的深度优先遍历 </p>
|
||||
|
||||
### 算法实现
|
||||
|
||||
这种“走到头 + 回溯”的算法形式一般基于递归来实现。与 BFS 类似,在 DFS 中我们也需要借助一个哈希表 `visited` 来记录已被访问的顶点,以避免重复访问顶点。
|
||||
|
@ -26,6 +26,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 链式地址 </p>
|
||||
|
||||
链式地址下,哈希表操作方法为:
|
||||
|
||||
- **查询元素**:先将 key 输入到哈希函数得到桶内索引,即可访问链表头结点,再通过遍历链表查找对应 value 。
|
||||
@ -56,6 +58,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 线性探测 </p>
|
||||
|
||||
线性探测存在以下缺陷:
|
||||
|
||||
- **不能直接删除元素**。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 `2.` 种情况)。因此需要借助一个标志位来标记删除元素。
|
||||
|
@ -10,6 +10,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 哈希表的抽象表示 </p>
|
||||
|
||||
## 6.1.1. 哈希表效率
|
||||
|
||||
除了哈希表之外,还可以使用以下数据结构来实现上述查询功能:
|
||||
@ -408,6 +410,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 简单哈希函数示例 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array_hash_map.java"
|
||||
@ -1273,6 +1277,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 哈希冲突示例 </p>
|
||||
|
||||
综上所述,一个优秀的「哈希函数」应该具备以下特性:
|
||||
|
||||
- 尽量少地发生哈希冲突;
|
||||
|
@ -11,6 +11,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 小顶堆与大顶堆 </p>
|
||||
|
||||
## 8.1.1. 堆术语与性质
|
||||
|
||||
- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。
|
||||
@ -318,6 +320,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 堆的表示与存储 </p>
|
||||
|
||||
我们将索引映射公式封装成函数,以便后续使用。
|
||||
|
||||
=== "Java"
|
||||
@ -1427,6 +1431,8 @@ $$
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完美二叉树的各层结点数量 </p>
|
||||
|
||||
化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得
|
||||
|
||||
$$
|
||||
|
@ -33,6 +33,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 数据结构与算法的关系 </p>
|
||||
|
||||
如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
@ -40,6 +40,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. Hello 算法内容结构 </p>
|
||||
|
||||
### 复杂度分析
|
||||
|
||||
首先介绍数据结构与算法的评价维度、算法效率的评估方法,引出了计算复杂度概念。
|
||||
@ -83,6 +85,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 算法学习路线 </p>
|
||||
|
||||
## 0.1.4. 本书特点
|
||||
|
||||
**以实践为主**。我们知道,学习英语期间光啃书本是远远不够的,需要多听、多说、多写,在实践中培养语感、积累经验。编程语言也是一门语言,因此学习方法也应是类似的,需要多看优秀代码、多敲键盘、多思考代码逻辑。
|
||||
|
@ -20,6 +20,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 页面编辑按键 </p>
|
||||
|
||||
图片无法直接修改,需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述图片问题,我会第一时间重新画图并替换图片。
|
||||
|
||||
## 0.4.2. 内容创作
|
||||
|
@ -154,6 +154,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 动画图解示例 </p>
|
||||
|
||||
## 0.2.3. 在代码实践中加深理解
|
||||
|
||||
本书的配套代码托管在[GitHub 仓库](https://github.com/krahets/hello-algo),**源代码包含详细注释,配有测试样例,可以直接运行**。
|
||||
@ -177,16 +179,22 @@ git clone https://github.com/krahets/hello-algo.git
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 克隆仓库与下载代码 </p>
|
||||
|
||||
### 3) 运行源代码
|
||||
|
||||
若代码块的顶部标有文件名称,则可在仓库 `codes` 文件夹中找到对应的 **源代码文件**。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 代码块与对应的源代码文件 </p>
|
||||
|
||||
源代码文件可以帮助你省去不必要的调试时间,将精力集中在学习内容上。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 运行代码示例 </p>
|
||||
|
||||
## 0.2.4. 在提问讨论中共同成长
|
||||
|
||||
阅读本书时,请不要“惯着”那些弄不明白的知识点。**欢迎在评论区留下你的问题**,小伙伴们和我都会给予解答,您一般 2 日内会得到回复。
|
||||
@ -194,3 +202,5 @@ git clone https://github.com/krahets/hello-algo.git
|
||||
同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家互相学习与进步!
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 评论区示例 </p>
|
||||
|
@ -16,6 +16,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 哈希查找数组索引 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="hashing_search.java"
|
||||
@ -132,6 +134,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 哈希查找链表结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="hashing_search.java"
|
||||
|
@ -12,6 +12,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在数组中线性查找元素 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linear_search.java"
|
||||
|
@ -43,6 +43,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 冒泡排序流程 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="bubble_sort.java"
|
||||
|
@ -12,6 +12,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 单次插入操作 </p>
|
||||
|
||||
## 11.3.1. 算法流程
|
||||
|
||||
1. 第 1 轮先选取数组的 **第 2 个元素** 为 `base` ,执行「插入操作」后,**数组前 2 个元素已完成排序**。
|
||||
@ -20,6 +22,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 插入排序流程 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="insertion_sort.java"
|
||||
|
@ -11,6 +11,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 排序中不同的元素类型和判断规则 </p>
|
||||
|
||||
## 11.1.1. 评价维度
|
||||
|
||||
排序算法主要可根据 **稳定性 、就地性 、自适应性 、比较类** 来分类。
|
||||
|
@ -11,6 +11,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 归并排序的划分与合并阶段 </p>
|
||||
|
||||
## 11.5.1. 算法流程
|
||||
|
||||
**「递归划分」** 从顶至底递归地 **将数组从中点切为两个子数组**,直至长度为 1 ;
|
||||
|
@ -296,6 +296,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 快速排序流程 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="quick_sort.java"
|
||||
|
@ -8,6 +8,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 双向队列的操作 </p>
|
||||
|
||||
## 5.3.1. 双向队列常用操作
|
||||
|
||||
双向队列的常用操作见下表,方法名需根据特定语言来确定。
|
||||
|
@ -10,6 +10,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 队列的先入先出规则 </p>
|
||||
|
||||
## 5.2.1. 队列常用操作
|
||||
|
||||
队列的常用操作见下表,方法名需根据特定语言来确定。
|
||||
|
@ -12,6 +12,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 栈的先入后出规则 </p>
|
||||
|
||||
## 5.1.1. 栈常用操作
|
||||
|
||||
栈的常用操作见下表(方法命名以 Java 为例)。
|
||||
|
@ -10,10 +10,14 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. AVL 树在删除结点后发生退化 </p>
|
||||
|
||||
再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. AVL 树在插入结点后发生退化 </p>
|
||||
|
||||
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除结点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。
|
||||
|
||||
换言之,在频繁增删查改的使用场景中,AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。
|
||||
@ -470,6 +474,8 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 有 grandChild 的右旋操作 </p>
|
||||
|
||||
“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。
|
||||
|
||||
=== "Java"
|
||||
@ -646,10 +652,14 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 左旋操作 </p>
|
||||
|
||||
同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 有 grandChild 的左旋操作 </p>
|
||||
|
||||
观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
|
||||
|
||||
=== "Java"
|
||||
@ -826,18 +836,24 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 先左旋后右旋 </p>
|
||||
|
||||
### Case 4 - 先右后左
|
||||
|
||||
同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 先右旋后左旋 </p>
|
||||
|
||||
### 旋转的选择
|
||||
|
||||
下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. AVL 树的四种旋转情况 </p>
|
||||
|
||||
具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
@ -11,6 +11,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树 </p>
|
||||
|
||||
## 7.3.1. 二叉搜索树的操作
|
||||
|
||||
### 查找结点
|
||||
@ -249,6 +251,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉搜索树中插入结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_search_tree.java"
|
||||
@ -551,10 +555,14 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 0) </p>
|
||||
|
||||
**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 1) </p>
|
||||
|
||||
**当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步:
|
||||
|
||||
1. 找到待删除结点在 **中序遍历序列** 中的下一个结点,记为 `nex` ;
|
||||
@ -1137,6 +1145,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树的中序遍历序列 </p>
|
||||
|
||||
## 7.3.2. 二叉搜索树的效率
|
||||
|
||||
假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:
|
||||
@ -1178,6 +1188,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树的平衡与退化 </p>
|
||||
|
||||
## 7.3.4. 二叉搜索树常见应用
|
||||
|
||||
- 系统中的多级索引,高效查找、插入、删除操作。
|
||||
|
@ -133,6 +133,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 父结点、子结点、子树 </p>
|
||||
|
||||
## 7.1.1. 二叉树常见术语
|
||||
|
||||
二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。
|
||||
@ -148,6 +150,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的常用术语 </p>
|
||||
|
||||
!!! tip "高度与深度的定义"
|
||||
|
||||
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。
|
||||
@ -306,6 +310,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉树中插入与删除结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_tree.java"
|
||||
@ -428,6 +434,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完美二叉树 </p>
|
||||
|
||||
### 完全二叉树
|
||||
|
||||
「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点尽量靠左填充。
|
||||
@ -436,18 +444,24 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完全二叉树 </p>
|
||||
|
||||
### 完满二叉树
|
||||
|
||||
「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完满二叉树 </p>
|
||||
|
||||
### 平衡二叉树
|
||||
|
||||
「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 平衡二叉树 </p>
|
||||
|
||||
## 7.1.4. 二叉树的退化
|
||||
|
||||
当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。
|
||||
@ -457,6 +471,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的最佳与最二叉树的最佳和最差结构差情况 </p>
|
||||
|
||||
如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
@ -480,10 +496,14 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完美二叉树的数组表示 </p>
|
||||
|
||||
然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 给定数组对应多种二叉树可能性 </p>
|
||||
|
||||
为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。
|
||||
|
||||
=== "Java"
|
||||
@ -563,8 +583,12 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
|
||||
|
||||
回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完全二叉树的数组表示 </p>
|
||||
|
||||
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。
|
||||
|
@ -16,6 +16,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的层序遍历 </p>
|
||||
|
||||
### 算法实现
|
||||
|
||||
广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。
|
||||
@ -256,6 +258,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树的前、中、后序遍历 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 位置 | 含义 | 此处访问结点时对应 |
|
||||
|
Loading…
Reference in New Issue
Block a user