Update the captions of all the figures.

This commit is contained in:
krahets 2023-02-26 18:18:34 +08:00
parent 85d04b30fb
commit 9e99ac06ce
31 changed files with 99 additions and 175 deletions

View file

@ -2,9 +2,7 @@
「数组 Array」是一种将 **相同类型元素** 存储在 **连续内存空间** 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。
![array_definition](array.assets/array_definition.png)
<p align="center"> Fig. 数组定义与存储方式 </p>
![数组定义与存储方式](array.assets/array_definition.png)
!!! note
@ -102,9 +100,7 @@
**在数组中访问元素非常高效**。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。
![array_memory_location_calculation](array.assets/array_memory_location_calculation.png)
<p align="center"> Fig. 数组元素的内存地址计算 </p>
![数组元素的内存地址计算](array.assets/array_memory_location_calculation.png)
```shell
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
@ -241,7 +237,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
**数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。
![array_insert_element](array.assets/array_insert_element.png)
![数组插入元素](array.assets/array_insert_element.png)
=== "Java"
@ -299,7 +295,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
删除元素也是类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。
![array_remove_element](array.assets/array_remove_element.png)
![数组删除元素](array.assets/array_remove_element.png)
=== "Java"

View file

@ -8,9 +8,7 @@
链表的「结点 Node」包含两项数据一是结点「值 Value」二是指向下一结点的「指针 Pointer」或称「引用 Reference」
![linkedlist_definition](linked_list.assets/linkedlist_definition.png)
<p align="center"> Fig. 链表定义与存储方式 </p>
![链表定义与存储方式](linked_list.assets/linkedlist_definition.png)
=== "Java"
@ -314,7 +312,7 @@
**在链表中,插入与删除结点的操作效率高**。比如,如果我们想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。
![linkedlist_insert_node](linked_list.assets/linkedlist_insert_node.png)
![链表插入结点](linked_list.assets/linkedlist_insert_node.png)
=== "Java"
@ -378,7 +376,7 @@
在链表中删除结点也很方便,只需要改变一个结点指针即可。如下图所示,虽然在完成删除后结点 `P` 仍然指向 `n2` ,但实际上 `P` 已经不属于此链表了,因为遍历此链表是无法访问到 `P` 的。
![linkedlist_remove_node](linked_list.assets/linkedlist_remove_node.png)
![链表删除结点](linked_list.assets/linkedlist_remove_node.png)
=== "Java"
@ -720,6 +718,6 @@
}
```
![linkedlist_common_types](linked_list.assets/linkedlist_common_types.png)
![常见链表种类](linked_list.assets/linkedlist_common_types.png)
<p align="center"> Fig. 常见链表类型 </p>

View file

@ -20,9 +20,7 @@
- 「栈帧空间」用于保存调用函数的上下文数据。系统每次调用函数都会在栈的顶部创建一个栈帧,函数返回时,栈帧空间会被释放。
- 「指令空间」用于保存编译后的程序指令,**在实际统计中一般忽略不计**。
![space_types](space_complexity.assets/space_types.png)
<p align="center"> Fig. 算法使用的相关空间 </p>
![算法使用的相关空间](space_complexity.assets/space_types.png)
=== "Java"
@ -561,9 +559,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
\end{aligned}
$$
![space_complexity_common_types](space_complexity.assets/space_complexity_common_types.png)
<p align="center"> Fig. 空间复杂度的常见类型 </p>
![空间复杂度的常见类型](space_complexity.assets/space_complexity_common_types.png)
!!! tip
@ -761,9 +757,7 @@ $$
[class]{}-[func]{linearRecur}
```
![space_complexity_recursive_linear](space_complexity.assets/space_complexity_recursive_linear.png)
<p align="center"> Fig. 递归函数产生的线性阶空间复杂度 </p>
![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png)
### 平方阶 $O(n^2)$
@ -891,9 +885,7 @@ $$
[class]{}-[func]{quadraticRecur}
```
![space_complexity_recursive_quadratic](space_complexity.assets/space_complexity_recursive_quadratic.png)
<p align="center"> Fig. 递归函数产生的平方阶空间复杂度 </p>
![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png)
### 指数阶 $O(2^n)$
@ -959,9 +951,7 @@ $$
[class]{}-[func]{buildTree}
```
![space_complexity_exponential](space_complexity.assets/space_complexity_exponential.png)
<p align="center"> Fig. 满二叉树下的指数阶空间复杂度 </p>
![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png)
### 对数阶 $O(\log n)$

View file

@ -365,9 +365,7 @@ $$
```
![time_complexity_simple_example](time_complexity.assets/time_complexity_simple_example.png)
<p align="center"> Fig. 算法 A, B, C 的时间增长趋势 </p>
![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png)
相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足?
@ -534,9 +532,7 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得
T(n) = O(f(n))
$$
![asymptotic_upper_bound](time_complexity.assets/asymptotic_upper_bound.png)
<p align="center"> Fig. 函数的渐近上界 </p>
![函数的渐近上界](time_complexity.assets/asymptotic_upper_bound.png)
本质上看,计算渐近上界就是在找一个函数 $f(n)$ **使得在 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别(仅相差一个常数项 $c$ 的倍数)**。
@ -774,9 +770,7 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline
\end{aligned}
$$
![time_complexity_common_types](time_complexity.assets/time_complexity_common_types.png)
<p align="center"> Fig. 时间复杂度的常见类型 </p>
![时间复杂度的常见类型](time_complexity.assets/time_complexity_common_types.png)
!!! tip
@ -1042,9 +1036,7 @@ $$
[class]{}-[func]{quadratic}
```
![time_complexity_constant_linear_quadratic](time_complexity.assets/time_complexity_constant_linear_quadratic.png)
<p align="center"> Fig. 常数阶、线性阶、平方阶的时间复杂度 </p>
![常数阶、线性阶、平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png)
以「冒泡排序」为例,外层循环 $n - 1$ 次,内层循环 $n-1, n-2, \cdots, 2, 1$ 次,平均为 $\frac{n}{2}$ 次,因此时间复杂度为 $O(n^2)$ 。
@ -1180,9 +1172,7 @@ $$
[class]{}-[func]{exponential}
```
![time_complexity_exponential](time_complexity.assets/time_complexity_exponential.png)
<p align="center"> Fig. 指数阶的时间复杂度 </p>
![指数阶的时间复杂度](time_complexity.assets/time_complexity_exponential.png)
在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,分裂 $n$ 次后停止。
@ -1314,9 +1304,7 @@ $$
[class]{}-[func]{logarithmic}
```
![time_complexity_logarithmic](time_complexity.assets/time_complexity_logarithmic.png)
<p align="center"> Fig. 对数阶的时间复杂度 </p>
![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png)
与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 $\log_2 n$ 的递归树。
@ -1446,9 +1434,7 @@ $$
[class]{}-[func]{linearLogRecur}
```
![time_complexity_logarithmic_linear](time_complexity.assets/time_complexity_logarithmic_linear.png)
<p align="center"> Fig. 线性对数阶的时间复杂度 </p>
![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png)
### 阶乘阶 $O(n!)$
@ -1520,9 +1506,7 @@ $$
[class]{}-[func]{factorialRecur}
```
![time_complexity_factorial](time_complexity.assets/time_complexity_factorial.png)
<p align="center"> Fig. 阶乘阶的时间复杂度 </p>
![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png)
## 最差、最佳、平均时间复杂度

View file

@ -11,9 +11,7 @@
- **线性数据结构**:数组、链表、栈、队列、哈希表;
- **非线性数据结构**:树、图、堆、哈希表;
![classification_logic_structure](classification_of_data_structure.assets/classification_logic_structure.png)
<p align="center"> Fig. 线性与非线性数据结构 </p>
![线性与非线性数据结构](classification_of_data_structure.assets/classification_logic_structure.png)
## 物理结构:连续与离散
@ -23,9 +21,7 @@
**「物理结构」反映了数据在计算机内存中的存储方式**。从本质上看,分别是 **数组的连续空间存储****链表的离散空间存储**。物理结构从底层上决定了数据的访问、更新、增删等操作方法,在时间效率和空间效率方面呈现出此消彼长的特性。
![classification_phisical_structure](classification_of_data_structure.assets/classification_phisical_structure.png)
<p align="center"> Fig. 连续空间存储与离散空间存储 </p>
![连续空间存储与离散空间存储](classification_of_data_structure.assets/classification_phisical_structure.png)
**所有数据结构都是基于数组、或链表、或两者组合实现的**。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。

View file

@ -76,7 +76,7 @@ $$
\end{aligned}
$$
![ieee_754_float](data_and_memory.assets/ieee_754_float.png)
![IEEE 754 标准下的 float 表示方式](data_and_memory.assets/ieee_754_float.png)
以上图为例,$\mathrm{S} = 0$ $\mathrm{E} = 124$ $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ ,易得
@ -206,8 +206,6 @@ $$
**系统通过「内存地址 Memory Location」来访问目标内存位置的数据**。计算机根据特定规则给表格中每个单元格编号,保证每块内存空间都有独立的内存地址。自此,程序便通过这些地址,访问内存中的数据。
![computer_memory_location](data_and_memory.assets/computer_memory_location.png)
<p align="center"> Fig. 内存条、内存空间、内存地址 </p>
![内存条、内存空间、内存地址](data_and_memory.assets/computer_memory_location.png)
**内存资源是设计数据结构与算法的重要考虑因素**。内存是所有程序的公共资源,当内存被某程序占用时,不能被其它程序同时使用。我们需要根据剩余内存资源的情况来设计算法。例如,若剩余内存空间有限,则要求算法占用的峰值内存不能超过系统剩余内存;若运行的程序很多、缺少大块连续的内存空间,则要求选取的数据结构必须能够存储在离散的内存空间内。

View file

@ -10,7 +10,7 @@ G & = \{ V, E \} \newline
\end{aligned}
$$
![linkedlist_tree_graph](graph.assets/linkedlist_tree_graph.png)
![链表、树、图之间的关系](graph.assets/linkedlist_tree_graph.png)
那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**。
@ -21,18 +21,18 @@ $$
- 在无向图中,边表示两顶点之间“双向”的连接关系,例如微信或 QQ 中的“好友关系”;
- 在有向图中,边是有方向的,即 $A \rightarrow B$ 和 $A \leftarrow B$ 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系;
![directed_graph](graph.assets/directed_graph.png)
![有向图与无向图](graph.assets/directed_graph.png)
根据所有顶点是否连通,分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。
- 对于连通图,从某个顶点出发,可以到达其余任意顶点;
- 对于非连通图,从某个顶点出发,至少有一个顶点无法到达;
![connected_graph](graph.assets/connected_graph.png)
![连通图与非连通图](graph.assets/connected_graph.png)
我们可以给边添加“权重”变量,得到「有权图 Weighted Graph」。例如在王者荣耀等游戏中系统会根据共同游戏时间来计算玩家之间的“亲密度”这种亲密度网络就可以使用有权图来表示。
![weighted_graph](graph.assets/weighted_graph.png)
![有权图与无权图](graph.assets/weighted_graph.png)
## 图常用术语
@ -50,7 +50,7 @@ $$
如下图所示,记邻接矩阵为 $M$ 、顶点列表为 $V$ ,则矩阵元素 $M[i][j] = 1$ 代表着顶点 $V[i]$ 到顶点 $V[j]$ 之间有边,相反地 $M[i][j] = 0$ 代表两顶点之间无边。
![adjacency_matrix](graph.assets/adjacency_matrix.png)
![图的邻接矩阵表示](graph.assets/adjacency_matrix.png)
邻接矩阵具有以下性质:
@ -64,7 +64,7 @@ $$
「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表结点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了所有与该顶点相连的顶点。
![adjacency_list](graph.assets/adjacency_list.png)
![图的邻接表表示](graph.assets/adjacency_list.png)
邻接表仅存储存在的边,而边的总数往往远小于 $n^2$ ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。

View file

@ -12,7 +12,7 @@
**广度优先遍历优是一种由近及远的遍历方式,从距离最近的顶点开始访问,并一层层向外扩张**。具体地,从某个顶点出发,先遍历该顶点的所有邻接顶点,随后遍历下个顶点的所有邻接顶点,以此类推……
![graph_bfs](graph_traversal.assets/graph_bfs.png)
![图的广度优先遍历](graph_traversal.assets/graph_bfs.png)
### 算法实现
@ -133,7 +133,7 @@ BFS 常借助「队列」来实现。队列具有“先入先出”的性质,
**深度优先遍历是一种优先走到底、无路可走再回头的遍历方式**。具体地,从某个顶点出发,不断地访问当前结点的某个邻接顶点,直到走到尽头时回溯,再继续走到底 + 回溯,以此类推……直至所有顶点遍历完成时结束。
![graph_dfs](graph_traversal.assets/graph_dfs.png)
![图的深度优先遍历](graph_traversal.assets/graph_dfs.png)
### 算法实现

View file

@ -20,7 +20,7 @@
在原始哈希表中,桶内的每个地址只能存储一个元素(即键值对)。**考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中**。
![hash_collision_chaining](hash_collision.assets/hash_collision_chaining.png)
![链式地址](hash_collision.assets/hash_collision_chaining.png)
链式地址下,哈希表操作方法为:
@ -50,7 +50,7 @@
1. 找到对应元素,返回 value 即可;
2. 若遇到空位,则说明查找键值对不在哈希表中;
![hash_collision_linear_probing](hash_collision.assets/hash_collision_linear_probing.png)
![线性探测](hash_collision.assets/hash_collision_linear_probing.png)
线性探测存在以下缺陷:

View file

@ -4,9 +4,7 @@
例如,给定一个包含 $n$ 个学生的数据库,每个学生有“姓名 `name` ”和“学号 `id` ”两项数据,希望实现一个查询功能:**输入一个学号,返回对应的姓名**,则可以使用哈希表实现。
![hash_map](hash_map.assets/hash_map.png)
<p align="center"> Fig. 哈希表抽象表示 </p>
![哈希表的抽象表示](hash_map.assets/hash_map.png)
## 哈希表效率
@ -404,9 +402,7 @@ $$
f(x) = x \% 100
$$
![hash_function](hash_map.assets/hash_function.png)
<p align="center"> Fig. 哈希函数 </p>
![简单哈希函数示例](hash_map.assets/hash_function.png)
=== "Java"
@ -498,9 +494,7 @@ $$
两个学号指向了同一个姓名,这明显是不对的,我们将这种现象称为「哈希冲突 Hash Collision」。如何避免哈希冲突的问题将被留在下章讨论。
![hash_collision](hash_map.assets/hash_collision.png)
<p align="center"> Fig. 哈希冲突 </p>
![哈希冲突示例](hash_map.assets/hash_collision.png)
综上所述,一个优秀的「哈希函数」应该具备以下特性:

View file

@ -5,7 +5,7 @@
- 「大顶堆 Max Heap」任意结点的值 $\geq$ 其子结点的值;
- 「小顶堆 Min Heap」任意结点的值 $\leq$ 其子结点的值;
![min_heap_and_max_heap](heap.assets/min_heap_and_max_heap.png)
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
## 堆术语与性质
@ -314,7 +314,7 @@
具体地,给定索引 $i$ ,那么其左子结点索引为 $2i + 1$ 、右子结点索引为 $2i + 2$ 、父结点索引为 $(i - 1) / 2$ (向下整除)。当索引越界时,代表空结点或结点不存在。
![representation_of_heap](heap.assets/representation_of_heap.png)
![堆的表示与存储](heap.assets/representation_of_heap.png)
我们将索引映射公式封装成函数,以便后续使用。
@ -789,7 +789,7 @@ $$
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1
$$
![heapify_operations_count](heap.assets/heapify_operations_count.png)
![完美二叉树的各层结点数量](heap.assets/heapify_operations_count.png)
化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得

View file

@ -27,9 +27,7 @@
- 算法是发挥数据结构优势的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。
- 算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。
![relationship_between_data_structure_and_algorithm](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png)
<p align="center"> Fig. 数据结构与算法的关系 </p>
![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png)
如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系。

View file

@ -34,7 +34,7 @@
本书主要内容分为复杂度分析、数据结构、算法三个部分。
![hello_algo_mindmap](about_the_book.assets/hello_algo_mindmap.png)
![Hello 算法内容结构](about_the_book.assets/hello_algo_mindmap.png)
### 复杂度分析
@ -75,9 +75,9 @@
- **第二阶段,刷算法题**。可以先从热门题单开刷,推荐[剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)、[LeetCode Hot 100](https://leetcode.cn/problem-list/2cktkvj/),先积累至少 100 道题量,熟悉大多数的算法问题。刚开始刷题时,“遗忘”是最大的困扰点,但这是很正常的,请不要担心。学习中有一种概念叫“周期性回顾”,同一道题隔段时间做一次,在重复 3 轮以上后,往往就能牢记于心了。
- **第三阶段,搭建知识体系**。在学习方面,可以阅读算法专栏文章、解题框架、算法教材,不断地丰富知识体系。在刷题方面,可以开始采用进阶刷题方案,例如按专题分类、一题多解、一解多题等,相应刷题计划与心得可以在社区中找到,在此不做赘述。
作为入门教程,**本书主要对应「第一阶段」的学习内容**,致力于使读者更高质量高效地开展第二、三阶段的学习。
根据观察,很多同学都是从“第二阶段”开始学习算法的。而作为入门教程,**本书内容主要对应“第一阶段”**,致力于帮助读者更高效地开展第二、三阶段的学习。
![learning_route](suggestions.assets/learning_route.png)
![算法学习路线](suggestions.assets/learning_route.png)
## 本书特点
@ -111,6 +111,6 @@
本书鼓励“手脑并用”的学习方式,在这点上受到了《动手学深度学习》很大影响,也在此向各位同学强烈推荐这本著作,包括[中文版](https://github.com/d2l-ai/d2l-zh)、[英文版](https://github.com/d2l-ai/d2l-en)、[李沐老师 bilibili 主页](https://space.bilibili.com/1567748478)。
在写作过程中,我阅读了许多数据结构与算法的书籍与教材,这些著作为本书作出了很好的榜样,保证了本书内容的正确性与质量,感谢前辈们的精彩创作!
在写作过程中,我阅读了许多数据结构与算法的教材与文章,这些著作为本书作出了很好的榜样,保证了本书内容的正确性与质量,感谢前辈们的精彩创作!
感谢父母,你们一贯的支持与鼓励给了我自由度来做这些有趣的事。

View file

@ -14,7 +14,7 @@
2. 修改 Markdown 源文件内容,并检查内容正确性,尽量保持排版格式统一;
3. 在页面底部填写更改说明然后单击“Propose file change”按钮页面跳转后点击“Create pull request”按钮发起拉取请求即可。
![edit_markdown](contribution.assets/edit_markdown.png)
![页面编辑按键](contribution.assets/edit_markdown.png)
图片无法直接修改,需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述图片问题,我会第一时间重新画图并替换图片。

View file

@ -1,7 +1,5 @@
# 编程环境安装
TODO 视频教程)
## 安装 VSCode
本书推荐使用开源轻量的 VSCode 作为本地 IDE ,下载并安装 [VSCode](https://code.visualstudio.com/) 。

View file

@ -6,11 +6,11 @@
标题后标注 `*` 的是选读章节,内容相对较难。如果你的时间有限,建议可以先跳过。
文章中的重要名词会用 `「」` 括号标注,例如 `「数组 Array」` 。建议记住这些名词,包括英文翻译,以便后续阅读文献时使用。
文章中的重要名词会用 `「括号」` 标注,例如 `「数组 Array」` 。建议记住这些名词,包括英文翻译,以便后续阅读文献时使用。
重点内容、总起句、总结句会被 **加粗** ,此类文字值得特别关注。
专有名词和有特指含义的词句会使用 `“ ”` 双引号标注,以避免歧义。
专有名词和有特指含义的词句会使用 `“双引号”` 标注,以避免歧义。
本书部分放弃了编程语言的注释规范,以换取更加紧凑的内容排版。注释主要分为三种类型:标题注释、内容注释、多行注释。
@ -148,7 +148,7 @@
阅读本书时,若发现某段内容提供了动画或图解,**建议你以图为主线**,将文字内容(一般在图的上方)对齐到图中内容,综合来理解。
![animation](suggestions.assets/animation.gif)
![动画图解示例](suggestions.assets/animation.gif)
## 在代码实践中加深理解
@ -171,17 +171,17 @@ git clone https://github.com/krahets/hello-algo.git
当然你也可以点击“Download ZIP”直接下载代码压缩包解压即可。
![download_code](suggestions.assets/download_code.png)
![克隆仓库与下载代码](suggestions.assets/download_code.png)
### 3) 运行源代码
若代码块的顶部标有文件名称,则可在仓库 `codes` 文件夹中找到对应的 **源代码文件**
![code_md_to_repo](suggestions.assets/code_md_to_repo.png)
![代码块与对应的源代码文件](suggestions.assets/code_md_to_repo.png)
源代码文件可以帮助你省去不必要的调试时间,将精力集中在学习内容上。
![running_code](suggestions.assets/running_code.gif)
![运行代码示例](suggestions.assets/running_code.gif)
## 在提问讨论中共同成长
@ -189,4 +189,4 @@ git clone https://github.com/krahets/hello-algo.git
同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家互相学习与进步!
![comment](suggestions.assets/comment.gif)
![评论区示例](suggestions.assets/comment.gif)

View file

@ -4,14 +4,14 @@
[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).
[3] 程杰. 大话数据结构.
[3] 严蔚敏. 数据结构( C 语言版).
[4] 王争. 数据结构与算法之美.
[4] 邓俊辉. 数据结构( C++ 语言版,第三版).
[5] 严蔚敏. 数据结构( C 语言版).
[5] 马克·艾伦·维斯著,陈越译. 数据结构与算法分析Java语言描述第三版).
[6] 邓俊辉. 数据结构( C++ 语言版,第三版).
[6] 程杰. 大话数据结构.
[7] 马克·艾伦·维斯著,陈越译. 数据结构与算法分析Java语言描述第三版.
[7] 王争. 数据结构与算法之美.
[8] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

View file

@ -10,7 +10,7 @@
如果我们想要给定数组中的一个目标元素 `target` ,获取该元素的索引,那么可以借助一个哈希表实现查找。
![hash_search_index](hashing_search.assets/hash_search_index.png)
![哈希查找数组索引](hashing_search.assets/hash_search_index.png)
=== "Java"
@ -74,7 +74,7 @@
再比如,如果我们想要给定一个目标结点值 `target` ,获取对应的链表结点对象,那么也可以使用哈希查找实现。
![hash_search_listnode](hashing_search.assets/hash_search_listnode.png)
![哈希查找链表结点](hashing_search.assets/hash_search_listnode.png)
=== "Java"

View file

@ -6,7 +6,7 @@
线性查找实质上就是遍历数据结构 + 判断条件。比如,我们想要在数组 `nums` 中查找目标元素 `target` 的对应索引,那么可以在数组中进行线性查找。
![linear_search](linear_search.assets/linear_search.png)
![在数组中线性查找元素](linear_search.assets/linear_search.png)
=== "Java"

View file

@ -39,9 +39,7 @@
2. 同理,对剩余 $n - 1$ 个元素执行「冒泡」,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个。
3. 以此类推…… **循环 $n - 1$ 轮「冒泡」,即可完成整个数组的排序**
![bubble_sort_overview](bubble_sort.assets/bubble_sort_overview.png)
<p align="center"> Fig. 冒泡排序流程 </p>
![冒泡排序流程](bubble_sort.assets/bubble_sort_overview.png)
=== "Java"

View file

@ -6,9 +6,7 @@
回忆数组插入操作,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。
![insertion_operation](insertion_sort.assets/insertion_operation.png)
<p align="center"> Fig. 插入操作 </p>
![单次插入操作](insertion_sort.assets/insertion_operation.png)
## 算法流程
@ -16,9 +14,7 @@
2. 第 2 轮选取 **第 3 个元素**`base` ,执行「插入操作」后,**数组前 3 个元素已完成排序**。
3. 以此类推……最后一轮选取 **数组尾元素**`base` ,执行「插入操作」后,**所有元素已完成排序**。
![insertion_sort_overview](insertion_sort.assets/insertion_sort_overview.png)
<p align="center"> Fig. 插入排序流程 </p>
![插入排序流程](insertion_sort.assets/insertion_sort_overview.png)
=== "Java"

View file

@ -5,9 +5,7 @@
- 待排序的列表的 **元素类型** 可以是整数、浮点数、字符、或字符串;
- 排序算法可以根据需要设定 **判断规则**,例如数字大小、字符 ASCII 码顺序、自定义规则;
![sorting_examples](intro_to_sort.assets/sorting_examples.png)
<p align="center"> Fig. 排序中的不同元素类型和判断规则 </p>
![排序中不同的元素类型和判断规则](intro_to_sort.assets/sorting_examples.png)
## 评价维度

View file

@ -5,9 +5,7 @@
1. **划分阶段**:通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;
2. **合并阶段**:划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;
![merge_sort_overview](merge_sort.assets/merge_sort_overview.png)
<p align="center"> Fig. 归并排序两阶段:划分与合并 </p>
![归并排序的划分与合并阶段](merge_sort.assets/merge_sort_overview.png)
## 算法流程

View file

@ -37,8 +37,6 @@
=== "<9>"
![pivot_division_step9](quick_sort.assets/pivot_division_step9.png)
<p align="center"> Fig. 哨兵划分 </p>
=== "Java"
```java title="quick_sort.java"
@ -125,9 +123,7 @@
观察发现,快速排序和「二分查找」的原理类似,都是以对数阶的时间复杂度来缩小处理区间。
![quick_sort_overview](quick_sort.assets/quick_sort_overview.png)
<p align="center"> Fig. 快速排序流程 </p>
![快速排序流程](quick_sort.assets/quick_sort_overview.png)
=== "Java"

View file

@ -2,9 +2,7 @@
对于队列,我们只能在头部删除或在尾部添加元素,而「双向队列 Deque」更加灵活在其头部和尾部都能执行元素添加或删除操作。
![deque_operations](deque.assets/deque_operations.png)
<p align="center"> Fig. 双向队列的操作 </p>
![双向队列的操作](deque.assets/deque_operations.png)
## 双向队列常用操作

View file

@ -4,9 +4,7 @@
我们将队列头部称为「队首」,队列尾部称为「队尾」,将把元素加入队尾的操作称为「入队」,删除队首元素的操作称为「出队」。
![queue_operations](queue.assets/queue_operations.png)
<p align="center"> Fig. 队列的先入先出特性 </p>
![队列的先入先出规则](queue.assets/queue_operations.png)
## 队列常用操作

View file

@ -6,9 +6,7 @@
我们将这一摞元素的顶部称为「栈顶」,将底部称为「栈底」,将把元素添加到栈顶的操作称为「入栈」,将删除栈顶元素的操作称为「出栈」。
![stack_operations](stack.assets/stack_operations.png)
<p align="center"> Fig. 栈的先入后出特性 </p>
![栈的先入后出规则](stack.assets/stack_operations.png)
## 栈常用操作

View file

@ -4,11 +4,11 @@
如下图所示,执行两步删除结点后,该二叉搜索树就会退化为链表。
![avltree_degradation_from_removing_node](avl_tree.assets/avltree_degradation_from_removing_node.png)
![AVL 树在删除结点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png)
再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
![avltree_degradation_from_inserting_node](avl_tree.assets/avltree_degradation_from_inserting_node.png)
![AVL 树在插入结点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png)
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作使得在不断添加与删除结点后AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。
@ -323,7 +323,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
进而,如果结点 `child` 本身有右子结点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子结点。
![avltree_right_rotate_with_grandchild](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
![有 grandChild 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。
@ -391,11 +391,11 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。
![avltree_left_rotate](avl_tree.assets/avltree_left_rotate.png)
![左旋操作](avl_tree.assets/avltree_left_rotate.png)
同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。
![avltree_left_rotate_with_grandchild](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
@ -463,19 +463,19 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
对于下图的失衡结点 3 **单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
![avltree_left_right_rotate](avl_tree.assets/avltree_left_right_rotate.png)
![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png)
### Case 4 - 先右后左
同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
![avltree_right_left_rotate](avl_tree.assets/avltree_right_left_rotate.png)
![先右旋后左旋](avl_tree.assets/avltree_right_left_rotate.png)
### 旋转的选择
下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。
![avltree_rotation_cases](avl_tree.assets/avltree_rotation_cases.png)
![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png)
具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。

View file

@ -5,7 +5,7 @@
1. 对于根结点,左子树中所有结点的值 $<$ 根结点的值 $<$ 右子树中所有结点的值;
2. 任意结点的左子树和右子树也是二叉搜索树,即也满足条件 `1.`
![binary_search_tree](binary_search_tree.assets/binary_search_tree.png)
![二叉搜索树](binary_search_tree.assets/binary_search_tree.png)
## 二叉搜索树的操作
@ -100,7 +100,7 @@
二叉搜索树不允许存在重复结点,否则将会违背其定义。因此若待插入结点在树中已经存在,则不执行插入,直接返回即可。
![bst_insert](binary_search_tree.assets/bst_insert.png)
![在二叉搜索树中插入结点](binary_search_tree.assets/bst_insert.png)
=== "Java"
@ -172,11 +172,11 @@
**当待删除结点的子结点数量 $= 0$ 时**,表明待删除结点是叶结点,直接删除即可。
![bst_remove_case1](binary_search_tree.assets/bst_remove_case1.png)
![在二叉搜索树中删除结点(度为 0](binary_search_tree.assets/bst_remove_case1.png)
**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。
![bst_remove_case2](binary_search_tree.assets/bst_remove_case2.png)
![在二叉搜索树中删除结点(度为 1](binary_search_tree.assets/bst_remove_case2.png)
**当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步:
@ -284,7 +284,7 @@
借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,而无需额外排序,非常高效。
![bst_inorder_traversal](binary_search_tree.assets/bst_inorder_traversal.png)
![二叉搜索树的中序遍历序列](binary_search_tree.assets/bst_inorder_traversal.png)
## 二叉搜索树的效率
@ -325,7 +325,7 @@
在实际应用中,如何保持二叉搜索树的平衡,也是一个需要重要考虑的问题。
![bst_degradation](binary_search_tree.assets/bst_degradation.png)
![二叉搜索树的平衡与退化](binary_search_tree.assets/bst_degradation.png)
## 二叉搜索树常见应用

View file

@ -127,9 +127,7 @@
除了叶结点外,每个结点都有子结点和子树。例如,若将下图的「结点 2」看作父结点那么其左子结点和右子结点分别为「结点 4」和「结点 5」左子树和右子树分别为「结点 4 及其以下结点形成的树」和「结点 5 及其以下结点形成的树」。
![binary_tree_definition](binary_tree.assets/binary_tree_definition.png)
<p align="center"> Fig. 子结点与子树 </p>
![父结点、子结点、子树](binary_tree.assets/binary_tree_definition.png)
## 二叉树常见术语
@ -144,9 +142,7 @@
- 结点「深度 Depth」 :根结点到该结点走过边的数量;
- 结点「高度 Height」最远叶结点到该结点走过边的数量
![binary_tree_terminology](binary_tree.assets/binary_tree_terminology.png)
<p align="center"> Fig. 二叉树的常见术语 </p>
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
!!! tip "高度与深度的定义"
@ -304,9 +300,7 @@
**插入与删除结点**。与链表类似,插入与删除结点都可以通过修改指针实现。
![binary_tree_add_remove](binary_tree.assets/binary_tree_add_remove.png)
<p align="center"> Fig. 在二叉树中插入与删除结点 </p>
![在二叉树中插入与删除结点](binary_tree.assets/binary_tree_add_remove.png)
=== "Java"
@ -428,7 +422,7 @@
在中文社区中,完美二叉树常被称为「满二叉树」,请注意与完满二叉树区分。
![perfect_binary_tree](binary_tree.assets/perfect_binary_tree.png)
![完美二叉树](binary_tree.assets/perfect_binary_tree.png)
### 完全二叉树
@ -436,19 +430,19 @@
**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空结点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。
![complete_binary_tree](binary_tree.assets/complete_binary_tree.png)
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
### 完满二叉树
「完满二叉树 Full Binary Tree」除了叶结点之外其余所有结点都有两个子结点。
![full_binary_tree](binary_tree.assets/full_binary_tree.png)
![完满二叉树](binary_tree.assets/full_binary_tree.png)
### 平衡二叉树
「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。
![balanced_binary_tree](binary_tree.assets/balanced_binary_tree.png)
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
## 二叉树的退化
@ -457,9 +451,7 @@
- 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势;
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$
![binary_tree_corner_cases](binary_tree.assets/binary_tree_corner_cases.png)
<p align="center"> Fig. 二叉树的最佳和最差结构 </p>
![二叉树的最佳与最二叉树的最佳和最差结构差情况](binary_tree.assets/binary_tree_corner_cases.png)
如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。
@ -482,11 +474,11 @@
**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意结点,我们都可以使用映射公式来访问子结点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。
![array_representation_mapping](binary_tree.assets/array_representation_mapping.png)
![完美二叉树的数组表示](binary_tree.assets/array_representation_mapping.png)
然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
![array_representation_without_empty](binary_tree.assets/array_representation_without_empty.png)
![给定数组对应多种二叉树可能性](binary_tree.assets/array_representation_without_empty.png)
为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。
@ -565,10 +557,10 @@
```
![array_representation_with_empty](binary_tree.assets/array_representation_with_empty.png)
![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png)
回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。
![array_representation_complete_binary_tree](binary_tree.assets/array_representation_complete_binary_tree.png)
![完全二叉树的数组表示](binary_tree.assets/array_representation_complete_binary_tree.png)
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。

View file

@ -10,7 +10,7 @@
层序遍历本质上是「广度优先搜索 Breadth-First Traversal」其体现着一种“一圈一圈向外”的层进遍历方式。
![binary_tree_bfs](binary_tree_traversal.assets/binary_tree_bfs.png)
![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png)
<p align="center"> Fig. 二叉树的层序遍历 </p>
@ -90,7 +90,7 @@
如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。
![binary_tree_dfs](binary_tree_traversal.assets/binary_tree_dfs.png)
![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png)
<p align="center"> Fig. 二叉树的前 / 中 / 后序遍历 </p>