pull/944/head
krahets 2 years ago
parent cf26cd551a
commit 9cd475d8c2

@ -8,6 +8,8 @@ comments: true
![数组定义与存储方式](array.assets/array_definition.png)
<p align="center"> Fig. 数组定义与存储方式 </p>
!!! note
观察上图,我们发现 **数组首元素的索引为 $0$** 。你可能会想,这并不符合日常习惯,首个元素的索引为什么不是 $1$ 呢,这不是更加自然吗?我认同你的想法,但请先记住这个设定,后面讲内存地址计算时,我会尝试解答这个问题。
@ -106,6 +108,8 @@ comments: true
![数组元素的内存地址计算](array.assets/array_memory_location_calculation.png)
<p align="center"> Fig. 数组元素的内存地址计算 </p>
```shell
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
elementAddr = firtstElementAddr + elementLength * elementIndex
@ -405,6 +409,8 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
![数组插入元素](array.assets/array_insert_element.png)
<p align="center"> Fig. 数组插入元素 </p>
=== "Java"
```java title="array.java"
@ -527,6 +533,8 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
![数组删除元素](array.assets/array_remove_element.png)
<p align="center"> Fig. 数组删除元素 </p>
=== "Java"
```java title="array.java"

@ -14,6 +14,8 @@ comments: true
![链表定义与存储方式](linked_list.assets/linkedlist_definition.png)
<p align="center"> Fig. 链表定义与存储方式 </p>
=== "Java"
```java title=""
@ -318,6 +320,8 @@ comments: true
![链表插入结点](linked_list.assets/linkedlist_insert_node.png)
<p align="center"> Fig. 链表插入结点 </p>
=== "Java"
```java title="linked_list.java"
@ -427,6 +431,8 @@ comments: true
![链表删除结点](linked_list.assets/linkedlist_remove_node.png)
<p align="center"> Fig. 链表删除结点 </p>
=== "Java"
```java title="linked_list.java"
@ -1018,3 +1024,5 @@ comments: true
```
![常见链表种类](linked_list.assets/linkedlist_common_types.png)
<p align="center"> Fig. 常见链表种类 </p>

@ -26,6 +26,8 @@ comments: true
![算法使用的相关空间](space_complexity.assets/space_types.png)
<p align="center"> Fig. 算法使用的相关空间 </p>
=== "Java"
```java title=""
@ -565,6 +567,8 @@ $$
![空间复杂度的常见类型](space_complexity.assets/space_complexity_common_types.png)
<p align="center"> Fig. 空间复杂度的常见类型 </p>
!!! tip
部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解空间复杂度含义和推算方法上。
@ -1078,6 +1082,8 @@ $$
![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png)
<p align="center"> Fig. 递归函数产生的线性阶空间复杂度 </p>
### 平方阶 $O(n^2)$
平方阶常见于元素数量与 $n$ 成平方关系的矩阵、图。
@ -1362,6 +1368,8 @@ $$
![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png)
<p align="center"> Fig. 递归函数产生的平方阶空间复杂度 </p>
### 指数阶 $O(2^n)$
指数阶常见于二叉树。高度为 $n$ 的「满二叉树」的结点数量为 $2^n - 1$ ,使用 $O(2^n)$ 空间。
@ -1496,6 +1504,8 @@ $$
![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png)
<p align="center"> Fig. 满二叉树产生的指数阶空间复杂度 </p>
### 对数阶 $O(\log n)$
对数阶常见于分治算法、数据类型转换等。

@ -371,6 +371,8 @@ $$
![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png)
<p align="center"> Fig. 算法 A, B, C 的时间增长趋势 </p>
相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足?
**时间复杂度可以有效评估算法效率**。算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。
@ -538,6 +540,8 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得
![函数的渐近上界](time_complexity.assets/asymptotic_upper_bound.png)
<p align="center"> Fig. 函数的渐近上界 </p>
本质上看,计算渐近上界就是在找一个函数 $f(n)$ **使得在 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别(仅相差一个常数项 $c$ 的倍数)**。
!!! tip
@ -776,6 +780,8 @@ $$
![时间复杂度的常见类型](time_complexity.assets/time_complexity_common_types.png)
<p align="center"> Fig. 时间复杂度的常见类型 </p>
!!! tip
部分示例代码需要一些前置知识,包括数组、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解时间复杂度含义和推算方法上。
@ -1328,6 +1334,8 @@ $$
![常数阶、线性阶、平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png)
<p align="center"> Fig. 常数阶、线性阶、平方阶的时间复杂度 </p>
以「冒泡排序」为例,外层循环 $n - 1$ 次,内层循环 $n-1, n-2, \cdots, 2, 1$ 次,平均为 $\frac{n}{2}$ 次,因此时间复杂度为 $O(n^2)$ 。
$$
@ -1733,6 +1741,8 @@ $$
![指数阶的时间复杂度](time_complexity.assets/time_complexity_exponential.png)
<p align="center"> Fig. 指数阶的时间复杂度 </p>
在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,分裂 $n$ 次后停止。
=== "Java"
@ -1980,6 +1990,8 @@ $$
![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png)
<p align="center"> Fig. 对数阶的时间复杂度 </p>
与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 $\log_2 n$ 的递归树。
=== "Java"
@ -2233,6 +2245,8 @@ $$
![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png)
<p align="center"> Fig. 线性对数阶的时间复杂度 </p>
### 阶乘阶 $O(n!)$
阶乘阶对应数学上的「全排列」。即给定 $n$ 个互不重复的元素,求其所有可能的排列方案,则方案数量为
@ -2391,6 +2405,8 @@ $$
![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png)
<p align="center"> Fig. 阶乘阶的时间复杂度 </p>
## 2.2.6. &nbsp; 最差、最佳、平均时间复杂度
**某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**。举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论:

@ -17,6 +17,8 @@ comments: true
![线性与非线性数据结构](classification_of_data_structure.assets/classification_logic_structure.png)
<p align="center"> Fig. 线性与非线性数据结构 </p>
## 3.2.2. &nbsp; 物理结构:连续与离散
!!! note
@ -27,6 +29,8 @@ comments: true
![连续空间存储与离散空间存储](classification_of_data_structure.assets/classification_phisical_structure.png)
<p align="center"> Fig. 连续空间存储与离散空间存储 </p>
**所有数据结构都是基于数组、或链表、或两者组合实现的**。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。
- **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等;

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

@ -16,6 +16,8 @@ $$
![链表、树、图之间的关系](graph.assets/linkedlist_tree_graph.png)
<p align="center"> Fig. 链表、树、图之间的关系 </p>
那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**。
## 9.1.1. &nbsp; 图常见类型
@ -27,6 +29,8 @@ $$
![有向图与无向图](graph.assets/directed_graph.png)
<p align="center"> Fig. 有向图与无向图 </p>
根据所有顶点是否连通,分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。
- 对于连通图,从某个顶点出发,可以到达其余任意顶点;
@ -34,10 +38,14 @@ $$
![连通图与非连通图](graph.assets/connected_graph.png)
<p align="center"> Fig. 连通图与非连通图 </p>
我们可以给边添加“权重”变量,得到「有权图 Weighted Graph」。例如在王者荣耀等游戏中系统会根据共同游戏时间来计算玩家之间的“亲密度”这种亲密度网络就可以使用有权图来表示。
![有权图与无权图](graph.assets/weighted_graph.png)
<p align="center"> Fig. 有权图与无权图 </p>
## 9.1.2. &nbsp; 图常用术语
- 「邻接 Adjacency」当两顶点之间有边相连时称此两顶点“邻接”。
@ -56,6 +64,8 @@ $$
![图的邻接矩阵表示](graph.assets/adjacency_matrix.png)
<p align="center"> Fig. 图的邻接矩阵表示 </p>
邻接矩阵具有以下性质:
- 顶点不能与自身相连,因而邻接矩阵主对角线元素没有意义。
@ -70,6 +80,8 @@ $$
![图的邻接表表示](graph.assets/adjacency_list.png)
<p align="center"> Fig. 图的邻接表表示 </p>
邻接表仅存储存在的边,而边的总数往往远小于 $n^2$ ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。
观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如当链表较长时可以把链表转化为「AVL 树」,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet即哈希表将时间复杂度降低至 $O(1)$ 。

@ -18,6 +18,8 @@ comments: true
![图的广度优先遍历](graph_traversal.assets/graph_bfs.png)
<p align="center"> Fig. 图的广度优先遍历 </p>
### 算法实现
BFS 常借助「队列」来实现。队列具有“先入先出”的性质,这与 BFS “由近及远”的思想是异曲同工的。
@ -256,6 +258,8 @@ BFS 常借助「队列」来实现。队列具有“先入先出”的性质,
![图的深度优先遍历](graph_traversal.assets/graph_dfs.png)
<p align="center"> Fig. 图的深度优先遍历 </p>
### 算法实现
这种“走到头 + 回溯”的算法形式一般基于递归来实现。与 BFS 类似,在 DFS 中我们也需要借助一个哈希表 `visited` 来记录已被访问的顶点,以避免重复访问顶点。

@ -26,6 +26,8 @@ comments: true
![链式地址](hash_collision.assets/hash_collision_chaining.png)
<p align="center"> Fig. 链式地址 </p>
链式地址下,哈希表操作方法为:
- **查询元素**:先将 key 输入到哈希函数得到桶内索引,即可访问链表头结点,再通过遍历链表查找对应 value 。
@ -56,6 +58,8 @@ comments: true
![线性探测](hash_collision.assets/hash_collision_linear_probing.png)
<p align="center"> Fig. 线性探测 </p>
线性探测存在以下缺陷:
- **不能直接删除元素**。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 `2.` 种情况)。因此需要借助一个标志位来标记删除元素。

@ -10,6 +10,8 @@ comments: true
![哈希表的抽象表示](hash_map.assets/hash_map.png)
<p align="center"> Fig. 哈希表的抽象表示 </p>
## 6.1.1. &nbsp; 哈希表效率
除了哈希表之外,还可以使用以下数据结构来实现上述查询功能:
@ -408,6 +410,8 @@ $$
![简单哈希函数示例](hash_map.assets/hash_function.png)
<p align="center"> Fig. 简单哈希函数示例 </p>
=== "Java"
```java title="array_hash_map.java"
@ -1273,6 +1277,8 @@ $$
![哈希冲突示例](hash_map.assets/hash_collision.png)
<p align="center"> Fig. 哈希冲突示例 </p>
综上所述,一个优秀的「哈希函数」应该具备以下特性:
- 尽量少地发生哈希冲突;

@ -11,6 +11,8 @@ comments: true
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
<p align="center"> Fig. 小顶堆与大顶堆 </p>
## 8.1.1. &nbsp; 堆术语与性质
- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。
@ -318,6 +320,8 @@ comments: true
![堆的表示与存储](heap.assets/representation_of_heap.png)
<p align="center"> Fig. 堆的表示与存储 </p>
我们将索引映射公式封装成函数,以便后续使用。
=== "Java"
@ -1427,6 +1431,8 @@ $$
![完美二叉树的各层结点数量](heap.assets/heapify_operations_count.png)
<p align="center"> Fig. 完美二叉树的各层结点数量 </p>
化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得
$$

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

@ -40,6 +40,8 @@ comments: true
![Hello 算法内容结构](about_the_book.assets/hello_algo_mindmap.png)
<p align="center"> Fig. Hello 算法内容结构 </p>
### 复杂度分析
首先介绍数据结构与算法的评价维度、算法效率的评估方法,引出了计算复杂度概念。
@ -83,6 +85,8 @@ comments: true
![算法学习路线](suggestions.assets/learning_route.png)
<p align="center"> Fig. 算法学习路线 </p>
## 0.1.4. &nbsp; 本书特点
**以实践为主**。我们知道,学习英语期间光啃书本是远远不够的,需要多听、多说、多写,在实践中培养语感、积累经验。编程语言也是一门语言,因此学习方法也应是类似的,需要多看优秀代码、多敲键盘、多思考代码逻辑。

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

@ -154,6 +154,8 @@ comments: true
![动画图解示例](suggestions.assets/animation.gif)
<p align="center"> Fig. 动画图解示例 </p>
## 0.2.3. &nbsp; 在代码实践中加深理解
本书的配套代码托管在[GitHub 仓库](https://github.com/krahets/hello-algo)**源代码包含详细注释,配有测试样例,可以直接运行**。
@ -177,16 +179,22 @@ git clone https://github.com/krahets/hello-algo.git
![克隆仓库与下载代码](suggestions.assets/download_code.png)
<p align="center"> Fig. 克隆仓库与下载代码 </p>
### 3) 运行源代码
若代码块的顶部标有文件名称,则可在仓库 `codes` 文件夹中找到对应的 **源代码文件**。
![代码块与对应的源代码文件](suggestions.assets/code_md_to_repo.png)
<p align="center"> Fig. 代码块与对应的源代码文件 </p>
源代码文件可以帮助你省去不必要的调试时间,将精力集中在学习内容上。
![运行代码示例](suggestions.assets/running_code.gif)
<p align="center"> Fig. 运行代码示例 </p>
## 0.2.4. &nbsp; 在提问讨论中共同成长
阅读本书时,请不要“惯着”那些弄不明白的知识点。**欢迎在评论区留下你的问题**,小伙伴们和我都会给予解答,您一般 2 日内会得到回复。
@ -194,3 +202,5 @@ git clone https://github.com/krahets/hello-algo.git
同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家互相学习与进步!
![评论区示例](suggestions.assets/comment.gif)
<p align="center"> Fig. 评论区示例 </p>

@ -16,6 +16,8 @@ comments: true
![哈希查找数组索引](hashing_search.assets/hash_search_index.png)
<p align="center"> Fig. 哈希查找数组索引 </p>
=== "Java"
```java title="hashing_search.java"
@ -132,6 +134,8 @@ comments: true
![哈希查找链表结点](hashing_search.assets/hash_search_listnode.png)
<p align="center"> Fig. 哈希查找链表结点 </p>
=== "Java"
```java title="hashing_search.java"

@ -12,6 +12,8 @@ comments: true
![在数组中线性查找元素](linear_search.assets/linear_search.png)
<p align="center"> Fig. 在数组中线性查找元素 </p>
=== "Java"
```java title="linear_search.java"

@ -43,6 +43,8 @@ comments: true
![冒泡排序流程](bubble_sort.assets/bubble_sort_overview.png)
<p align="center"> Fig. 冒泡排序流程 </p>
=== "Java"
```java title="bubble_sort.java"

@ -12,6 +12,8 @@ comments: true
![单次插入操作](insertion_sort.assets/insertion_operation.png)
<p align="center"> Fig. 单次插入操作 </p>
## 11.3.1. &nbsp; 算法流程
1. 第 1 轮先选取数组的 **第 2 个元素** 为 `base` ,执行「插入操作」后,**数组前 2 个元素已完成排序**。
@ -20,6 +22,8 @@ comments: true
![插入排序流程](insertion_sort.assets/insertion_sort_overview.png)
<p align="center"> Fig. 插入排序流程 </p>
=== "Java"
```java title="insertion_sort.java"

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

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

@ -296,6 +296,8 @@ comments: true
![快速排序流程](quick_sort.assets/quick_sort_overview.png)
<p align="center"> Fig. 快速排序流程 </p>
=== "Java"
```java title="quick_sort.java"

@ -8,6 +8,8 @@ comments: true
![双向队列的操作](deque.assets/deque_operations.png)
<p align="center"> Fig. 双向队列的操作 </p>
## 5.3.1. &nbsp; 双向队列常用操作
双向队列的常用操作见下表,方法名需根据特定语言来确定。

@ -10,6 +10,8 @@ comments: true
![队列的先入先出规则](queue.assets/queue_operations.png)
<p align="center"> Fig. 队列的先入先出规则 </p>
## 5.2.1. &nbsp; 队列常用操作
队列的常用操作见下表,方法名需根据特定语言来确定。

@ -12,6 +12,8 @@ comments: true
![栈的先入后出规则](stack.assets/stack_operations.png)
<p align="center"> Fig. 栈的先入后出规则 </p>
## 5.1.1. &nbsp; 栈常用操作
栈的常用操作见下表(方法命名以 Java 为例)。

@ -10,10 +10,14 @@ comments: true
![AVL 树在删除结点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png)
<p align="center"> Fig. AVL 树在删除结点后发生退化 </p>
再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
![AVL 树在插入结点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png)
<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」的操作其可 **在不影
![有 grandChild 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
<p align="center"> Fig. 有 grandChild 的右旋操作 </p>
“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。
=== "Java"
@ -646,10 +652,14 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
![左旋操作](avl_tree.assets/avltree_left_rotate.png)
<p align="center"> Fig. 左旋操作 </p>
同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。
![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
<p align="center"> Fig. 有 grandChild 的左旋操作 </p>
观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
=== "Java"
@ -826,18 +836,24 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png)
<p align="center"> Fig. 先左旋后右旋 </p>
### Case 4 - 先右后左
同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
![先右旋后左旋](avl_tree.assets/avltree_right_left_rotate.png)
<p align="center"> Fig. 先右旋后左旋 </p>
### 旋转的选择
下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。
![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png)
<p align="center"> Fig. AVL 树的四种旋转情况 </p>
具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。
<div class="center-table" markdown>

@ -11,6 +11,8 @@ comments: true
![二叉搜索树](binary_search_tree.assets/binary_search_tree.png)
<p align="center"> Fig. 二叉搜索树 </p>
## 7.3.1. &nbsp; 二叉搜索树的操作
### 查找结点
@ -249,6 +251,8 @@ comments: true
![在二叉搜索树中插入结点](binary_search_tree.assets/bst_insert.png)
<p align="center"> Fig. 在二叉搜索树中插入结点 </p>
=== "Java"
```java title="binary_search_tree.java"
@ -551,10 +555,14 @@ comments: true
![在二叉搜索树中删除结点(度为 0](binary_search_tree.assets/bst_remove_case1.png)
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 0 </p>
**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。
![在二叉搜索树中删除结点(度为 1](binary_search_tree.assets/bst_remove_case2.png)
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 1 </p>
**当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步:
1. 找到待删除结点在 **中序遍历序列** 中的下一个结点,记为 `nex`
@ -1137,6 +1145,8 @@ comments: true
![二叉搜索树的中序遍历序列](binary_search_tree.assets/bst_inorder_traversal.png)
<p align="center"> Fig. 二叉搜索树的中序遍历序列 </p>
## 7.3.2. &nbsp; 二叉搜索树的效率
假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:
@ -1178,6 +1188,8 @@ comments: true
![二叉搜索树的平衡与退化](binary_search_tree.assets/bst_degradation.png)
<p align="center"> Fig. 二叉搜索树的平衡与退化 </p>
## 7.3.4. &nbsp; 二叉搜索树常见应用
- 系统中的多级索引,高效查找、插入、删除操作。

@ -133,6 +133,8 @@ comments: true
![父结点、子结点、子树](binary_tree.assets/binary_tree_definition.png)
<p align="center"> Fig. 父结点、子结点、子树 </p>
## 7.1.1. &nbsp; 二叉树常见术语
二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。
@ -148,6 +150,8 @@ comments: true
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
<p align="center"> Fig. 二叉树的常用术语 </p>
!!! tip "高度与深度的定义"
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。
@ -306,6 +310,8 @@ comments: true
![在二叉树中插入与删除结点](binary_tree.assets/binary_tree_add_remove.png)
<p align="center"> Fig. 在二叉树中插入与删除结点 </p>
=== "Java"
```java title="binary_tree.java"
@ -428,6 +434,8 @@ comments: true
![完美二叉树](binary_tree.assets/perfect_binary_tree.png)
<p align="center"> Fig. 完美二叉树 </p>
### 完全二叉树
「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满且最底层结点尽量靠左填充。
@ -436,18 +444,24 @@ comments: true
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
<p align="center"> Fig. 完全二叉树 </p>
### 完满二叉树
「完满二叉树 Full Binary Tree」除了叶结点之外其余所有结点都有两个子结点。
![完满二叉树](binary_tree.assets/full_binary_tree.png)
<p align="center"> Fig. 完满二叉树 </p>
### 平衡二叉树
「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
<p align="center"> Fig. 平衡二叉树 </p>
## 7.1.4. &nbsp; 二叉树的退化
当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。
@ -457,6 +471,8 @@ comments: true
![二叉树的最佳与最二叉树的最佳和最差结构差情况](binary_tree.assets/binary_tree_corner_cases.png)
<p align="center"> Fig. 二叉树的最佳与最二叉树的最佳和最差结构差情况 </p>
如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。
<div class="center-table" markdown>
@ -480,10 +496,14 @@ comments: true
![完美二叉树的数组表示](binary_tree.assets/array_representation_mapping.png)
<p align="center"> Fig. 完美二叉树的数组表示 </p>
然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
![给定数组对应多种二叉树可能性](binary_tree.assets/array_representation_without_empty.png)
<p align="center"> Fig. 给定数组对应多种二叉树可能性 </p>
为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。
=== "Java"
@ -563,8 +583,12 @@ comments: true
![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png)
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。
![完全二叉树的数组表示](binary_tree.assets/array_representation_complete_binary_tree.png)
<p align="center"> Fig. 完全二叉树的数组表示 </p>
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。

@ -16,6 +16,8 @@ comments: true
![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png)
<p align="center"> Fig. 二叉树的层序遍历 </p>
### 算法实现
广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。
@ -256,6 +258,8 @@ comments: true
![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png)
<p align="center"> Fig. 二叉搜索树的前、中、后序遍历 </p>
<div class="center-table" markdown>
| 位置 | 含义 | 此处访问结点时对应 |

Loading…
Cancel
Save