diff --git a/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md index efeac6cd9..a5cd5a3fb 100755 --- a/chapter_array_and_linkedlist/array.md +++ b/chapter_array_and_linkedlist/array.md @@ -8,6 +8,8 @@ comments: true ![数组定义与存储方式](array.assets/array_definition.png) +
Fig. 数组定义与存储方式
+ !!! note 观察上图,我们发现 **数组首元素的索引为 $0$** 。你可能会想,这并不符合日常习惯,首个元素的索引为什么不是 $1$ 呢,这不是更加自然吗?我认同你的想法,但请先记住这个设定,后面讲内存地址计算时,我会尝试解答这个问题。 @@ -106,6 +108,8 @@ comments: true ![数组元素的内存地址计算](array.assets/array_memory_location_calculation.png) +Fig. 数组元素的内存地址计算
+ ```shell # 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引 elementAddr = firtstElementAddr + elementLength * elementIndex @@ -405,6 +409,8 @@ elementAddr = firtstElementAddr + elementLength * elementIndex ![数组插入元素](array.assets/array_insert_element.png) +Fig. 数组插入元素
+ === "Java" ```java title="array.java" @@ -527,6 +533,8 @@ elementAddr = firtstElementAddr + elementLength * elementIndex ![数组删除元素](array.assets/array_remove_element.png) +Fig. 数组删除元素
+ === "Java" ```java title="array.java" diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index 7bc57460d..8a5130309 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -14,6 +14,8 @@ comments: true ![链表定义与存储方式](linked_list.assets/linkedlist_definition.png) +Fig. 链表定义与存储方式
+ === "Java" ```java title="" @@ -318,6 +320,8 @@ comments: true ![链表插入结点](linked_list.assets/linkedlist_insert_node.png) +Fig. 链表插入结点
+ === "Java" ```java title="linked_list.java" @@ -427,6 +431,8 @@ comments: true ![链表删除结点](linked_list.assets/linkedlist_remove_node.png) +Fig. 链表删除结点
+ === "Java" ```java title="linked_list.java" @@ -1018,3 +1024,5 @@ comments: true ``` ![常见链表种类](linked_list.assets/linkedlist_common_types.png) + +Fig. 常见链表种类
diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index 506ab37cd..375671e4a 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -26,6 +26,8 @@ comments: true ![算法使用的相关空间](space_complexity.assets/space_types.png) +Fig. 算法使用的相关空间
+ === "Java" ```java title="" @@ -565,6 +567,8 @@ $$ ![空间复杂度的常见类型](space_complexity.assets/space_complexity_common_types.png) +Fig. 空间复杂度的常见类型
+ !!! tip 部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解空间复杂度含义和推算方法上。 @@ -1078,6 +1082,8 @@ $$ ![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png) +Fig. 递归函数产生的线性阶空间复杂度
+ ### 平方阶 $O(n^2)$ 平方阶常见于元素数量与 $n$ 成平方关系的矩阵、图。 @@ -1362,6 +1368,8 @@ $$ ![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png) +Fig. 递归函数产生的平方阶空间复杂度
+ ### 指数阶 $O(2^n)$ 指数阶常见于二叉树。高度为 $n$ 的「满二叉树」的结点数量为 $2^n - 1$ ,使用 $O(2^n)$ 空间。 @@ -1496,6 +1504,8 @@ $$ ![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png) +Fig. 满二叉树产生的指数阶空间复杂度
+ ### 对数阶 $O(\log n)$ 对数阶常见于分治算法、数据类型转换等。 diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index 68b60975d..02e92e98a 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -371,6 +371,8 @@ $$ ![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png) +Fig. 算法 A, B, C 的时间增长趋势
+ 相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足? **时间复杂度可以有效评估算法效率**。算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。 @@ -538,6 +540,8 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得 ![函数的渐近上界](time_complexity.assets/asymptotic_upper_bound.png) +Fig. 函数的渐近上界
+ 本质上看,计算渐近上界就是在找一个函数 $f(n)$ ,**使得在 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别(仅相差一个常数项 $c$ 的倍数)**。 !!! tip @@ -776,6 +780,8 @@ $$ ![时间复杂度的常见类型](time_complexity.assets/time_complexity_common_types.png) +Fig. 时间复杂度的常见类型
+ !!! tip 部分示例代码需要一些前置知识,包括数组、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解时间复杂度含义和推算方法上。 @@ -1328,6 +1334,8 @@ $$ ![常数阶、线性阶、平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png) +Fig. 常数阶、线性阶、平方阶的时间复杂度
+ 以「冒泡排序」为例,外层循环 $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) +Fig. 指数阶的时间复杂度
+ 在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,分裂 $n$ 次后停止。 === "Java" @@ -1980,6 +1990,8 @@ $$ ![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png) +Fig. 对数阶的时间复杂度
+ 与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 $\log_2 n$ 的递归树。 === "Java" @@ -2233,6 +2245,8 @@ $$ ![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png) +Fig. 线性对数阶的时间复杂度
+ ### 阶乘阶 $O(n!)$ 阶乘阶对应数学上的「全排列」。即给定 $n$ 个互不重复的元素,求其所有可能的排列方案,则方案数量为 @@ -2391,6 +2405,8 @@ $$ ![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png) +Fig. 阶乘阶的时间复杂度
+ ## 2.2.6. 最差、最佳、平均时间复杂度 **某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**。举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论: diff --git a/chapter_data_structure/classification_of_data_structure.md b/chapter_data_structure/classification_of_data_structure.md index 587dfc4e9..566e763c9 100644 --- a/chapter_data_structure/classification_of_data_structure.md +++ b/chapter_data_structure/classification_of_data_structure.md @@ -17,6 +17,8 @@ comments: true ![线性与非线性数据结构](classification_of_data_structure.assets/classification_logic_structure.png) +Fig. 线性与非线性数据结构
+ ## 3.2.2. 物理结构:连续与离散 !!! note @@ -27,6 +29,8 @@ comments: true ![连续空间存储与离散空间存储](classification_of_data_structure.assets/classification_phisical_structure.png) +Fig. 连续空间存储与离散空间存储
+ **所有数据结构都是基于数组、或链表、或两者组合实现的**。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。 - **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等; diff --git a/chapter_data_structure/data_and_memory.md b/chapter_data_structure/data_and_memory.md index bdea7ff44..6fef7d01b 100644 --- a/chapter_data_structure/data_and_memory.md +++ b/chapter_data_structure/data_and_memory.md @@ -82,6 +82,8 @@ $$ ![IEEE 754 标准下的 float 表示方式](data_and_memory.assets/ieee_754_float.png) +Fig. IEEE 754 标准下的 float 表示方式
+ 以上图为例,$\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) +Fig. 内存条、内存空间、内存地址
+ **内存资源是设计数据结构与算法的重要考虑因素**。内存是所有程序的公共资源,当内存被某程序占用时,不能被其它程序同时使用。我们需要根据剩余内存资源的情况来设计算法。例如,若剩余内存空间有限,则要求算法占用的峰值内存不能超过系统剩余内存;若运行的程序很多、缺少大块连续的内存空间,则要求选取的数据结构必须能够存储在离散的内存空间内。 diff --git a/chapter_graph/graph.md b/chapter_graph/graph.md index d1b1c6031..2645b638a 100644 --- a/chapter_graph/graph.md +++ b/chapter_graph/graph.md @@ -16,6 +16,8 @@ $$ ![链表、树、图之间的关系](graph.assets/linkedlist_tree_graph.png) +Fig. 链表、树、图之间的关系
+ 那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**。 ## 9.1.1. 图常见类型 @@ -27,6 +29,8 @@ $$ ![有向图与无向图](graph.assets/directed_graph.png) +Fig. 有向图与无向图
+ 根据所有顶点是否连通,分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。 - 对于连通图,从某个顶点出发,可以到达其余任意顶点; @@ -34,10 +38,14 @@ $$ ![连通图与非连通图](graph.assets/connected_graph.png) +Fig. 连通图与非连通图
+ 我们可以给边添加“权重”变量,得到「有权图 Weighted Graph」。例如,在王者荣耀等游戏中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以使用有权图来表示。 ![有权图与无权图](graph.assets/weighted_graph.png) +Fig. 有权图与无权图
+ ## 9.1.2. 图常用术语 - 「邻接 Adjacency」:当两顶点之间有边相连时,称此两顶点“邻接”。 @@ -56,6 +64,8 @@ $$ ![图的邻接矩阵表示](graph.assets/adjacency_matrix.png) +Fig. 图的邻接矩阵表示
+ 邻接矩阵具有以下性质: - 顶点不能与自身相连,因而邻接矩阵主对角线元素没有意义。 @@ -70,6 +80,8 @@ $$ ![图的邻接表表示](graph.assets/adjacency_list.png) +Fig. 图的邻接表表示
+ 邻接表仅存储存在的边,而边的总数往往远小于 $n^2$ ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。 观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如,当链表较长时,可以把链表转化为「AVL 树」,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet(即哈希表),将时间复杂度降低至 $O(1)$ 。 diff --git a/chapter_graph/graph_traversal.md b/chapter_graph/graph_traversal.md index 94eb09995..41e0a650e 100644 --- a/chapter_graph/graph_traversal.md +++ b/chapter_graph/graph_traversal.md @@ -18,6 +18,8 @@ comments: true ![图的广度优先遍历](graph_traversal.assets/graph_bfs.png) +Fig. 图的广度优先遍历
+ ### 算法实现 BFS 常借助「队列」来实现。队列具有“先入先出”的性质,这与 BFS “由近及远”的思想是异曲同工的。 @@ -256,6 +258,8 @@ BFS 常借助「队列」来实现。队列具有“先入先出”的性质, ![图的深度优先遍历](graph_traversal.assets/graph_dfs.png) +Fig. 图的深度优先遍历
+ ### 算法实现 这种“走到头 + 回溯”的算法形式一般基于递归来实现。与 BFS 类似,在 DFS 中我们也需要借助一个哈希表 `visited` 来记录已被访问的顶点,以避免重复访问顶点。 diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 4b518c6e1..553bc98ee 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -26,6 +26,8 @@ comments: true ![链式地址](hash_collision.assets/hash_collision_chaining.png) +Fig. 链式地址
+ 链式地址下,哈希表操作方法为: - **查询元素**:先将 key 输入到哈希函数得到桶内索引,即可访问链表头结点,再通过遍历链表查找对应 value 。 @@ -56,6 +58,8 @@ comments: true ![线性探测](hash_collision.assets/hash_collision_linear_probing.png) +Fig. 线性探测
+ 线性探测存在以下缺陷: - **不能直接删除元素**。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 `2.` 种情况)。因此需要借助一个标志位来标记删除元素。 diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index 130faac0a..eaba30d6f 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -10,6 +10,8 @@ comments: true ![哈希表的抽象表示](hash_map.assets/hash_map.png) +Fig. 哈希表的抽象表示
+ ## 6.1.1. 哈希表效率 除了哈希表之外,还可以使用以下数据结构来实现上述查询功能: @@ -408,6 +410,8 @@ $$ ![简单哈希函数示例](hash_map.assets/hash_function.png) +Fig. 简单哈希函数示例
+ === "Java" ```java title="array_hash_map.java" @@ -1273,6 +1277,8 @@ $$ ![哈希冲突示例](hash_map.assets/hash_collision.png) +Fig. 哈希冲突示例
+ 综上所述,一个优秀的「哈希函数」应该具备以下特性: - 尽量少地发生哈希冲突; diff --git a/chapter_heap/heap.md b/chapter_heap/heap.md index 95163580d..1862a09d0 100644 --- a/chapter_heap/heap.md +++ b/chapter_heap/heap.md @@ -11,6 +11,8 @@ comments: true ![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png) +Fig. 小顶堆与大顶堆
+ ## 8.1.1. 堆术语与性质 - 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。 @@ -318,6 +320,8 @@ comments: true ![堆的表示与存储](heap.assets/representation_of_heap.png) +Fig. 堆的表示与存储
+ 我们将索引映射公式封装成函数,以便后续使用。 === "Java" @@ -1427,6 +1431,8 @@ $$ ![完美二叉树的各层结点数量](heap.assets/heapify_operations_count.png) +Fig. 完美二叉树的各层结点数量
+ 化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得 $$ diff --git a/chapter_introduction/what_is_dsa.md b/chapter_introduction/what_is_dsa.md index 6046f3e3a..0ef291127 100644 --- a/chapter_introduction/what_is_dsa.md +++ b/chapter_introduction/what_is_dsa.md @@ -33,6 +33,8 @@ comments: true ![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png) +Fig. 数据结构与算法的关系
+ 如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系。Fig. Hello 算法内容结构
+ ### 复杂度分析 首先介绍数据结构与算法的评价维度、算法效率的评估方法,引出了计算复杂度概念。 @@ -83,6 +85,8 @@ comments: true ![算法学习路线](suggestions.assets/learning_route.png) +Fig. 算法学习路线
+ ## 0.1.4. 本书特点 **以实践为主**。我们知道,学习英语期间光啃书本是远远不够的,需要多听、多说、多写,在实践中培养语感、积累经验。编程语言也是一门语言,因此学习方法也应是类似的,需要多看优秀代码、多敲键盘、多思考代码逻辑。 diff --git a/chapter_preface/contribution.md b/chapter_preface/contribution.md index d47ad86ad..26073669f 100644 --- a/chapter_preface/contribution.md +++ b/chapter_preface/contribution.md @@ -20,6 +20,8 @@ comments: true ![页面编辑按键](contribution.assets/edit_markdown.png) +Fig. 页面编辑按键
+ 图片无法直接修改,需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述图片问题,我会第一时间重新画图并替换图片。 ## 0.4.2. 内容创作 diff --git a/chapter_preface/suggestions.md b/chapter_preface/suggestions.md index 5d46b56c7..c1efe4e13 100644 --- a/chapter_preface/suggestions.md +++ b/chapter_preface/suggestions.md @@ -154,6 +154,8 @@ comments: true ![动画图解示例](suggestions.assets/animation.gif) +Fig. 动画图解示例
+ ## 0.2.3. 在代码实践中加深理解 本书的配套代码托管在[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) +Fig. 克隆仓库与下载代码
+ ### 3) 运行源代码 若代码块的顶部标有文件名称,则可在仓库 `codes` 文件夹中找到对应的 **源代码文件**。 ![代码块与对应的源代码文件](suggestions.assets/code_md_to_repo.png) +Fig. 代码块与对应的源代码文件
+ 源代码文件可以帮助你省去不必要的调试时间,将精力集中在学习内容上。 ![运行代码示例](suggestions.assets/running_code.gif) +Fig. 运行代码示例
+ ## 0.2.4. 在提问讨论中共同成长 阅读本书时,请不要“惯着”那些弄不明白的知识点。**欢迎在评论区留下你的问题**,小伙伴们和我都会给予解答,您一般 2 日内会得到回复。 @@ -194,3 +202,5 @@ git clone https://github.com/krahets/hello-algo.git 同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家互相学习与进步! ![评论区示例](suggestions.assets/comment.gif) + +Fig. 评论区示例
diff --git a/chapter_searching/hashing_search.md b/chapter_searching/hashing_search.md index 5fd86ecc5..301a3e829 100755 --- a/chapter_searching/hashing_search.md +++ b/chapter_searching/hashing_search.md @@ -16,6 +16,8 @@ comments: true ![哈希查找数组索引](hashing_search.assets/hash_search_index.png) +Fig. 哈希查找数组索引
+ === "Java" ```java title="hashing_search.java" @@ -132,6 +134,8 @@ comments: true ![哈希查找链表结点](hashing_search.assets/hash_search_listnode.png) +Fig. 哈希查找链表结点
+ === "Java" ```java title="hashing_search.java" diff --git a/chapter_searching/linear_search.md b/chapter_searching/linear_search.md index 0d997d786..506fe4020 100755 --- a/chapter_searching/linear_search.md +++ b/chapter_searching/linear_search.md @@ -12,6 +12,8 @@ comments: true ![在数组中线性查找元素](linear_search.assets/linear_search.png) +Fig. 在数组中线性查找元素
+ === "Java" ```java title="linear_search.java" diff --git a/chapter_sorting/bubble_sort.md b/chapter_sorting/bubble_sort.md index afd7c84b6..620518cf5 100755 --- a/chapter_sorting/bubble_sort.md +++ b/chapter_sorting/bubble_sort.md @@ -43,6 +43,8 @@ comments: true ![冒泡排序流程](bubble_sort.assets/bubble_sort_overview.png) +Fig. 冒泡排序流程
+ === "Java" ```java title="bubble_sort.java" diff --git a/chapter_sorting/insertion_sort.md b/chapter_sorting/insertion_sort.md index 3f32894cb..4e54732ae 100755 --- a/chapter_sorting/insertion_sort.md +++ b/chapter_sorting/insertion_sort.md @@ -12,6 +12,8 @@ comments: true ![单次插入操作](insertion_sort.assets/insertion_operation.png) +Fig. 单次插入操作
+ ## 11.3.1. 算法流程 1. 第 1 轮先选取数组的 **第 2 个元素** 为 `base` ,执行「插入操作」后,**数组前 2 个元素已完成排序**。 @@ -20,6 +22,8 @@ comments: true ![插入排序流程](insertion_sort.assets/insertion_sort_overview.png) +Fig. 插入排序流程
+ === "Java" ```java title="insertion_sort.java" diff --git a/chapter_sorting/intro_to_sort.md b/chapter_sorting/intro_to_sort.md index c36d633c8..5a4e85480 100644 --- a/chapter_sorting/intro_to_sort.md +++ b/chapter_sorting/intro_to_sort.md @@ -11,6 +11,8 @@ comments: true ![排序中不同的元素类型和判断规则](intro_to_sort.assets/sorting_examples.png) +Fig. 排序中不同的元素类型和判断规则
+ ## 11.1.1. 评价维度 排序算法主要可根据 **稳定性 、就地性 、自适应性 、比较类** 来分类。 diff --git a/chapter_sorting/merge_sort.md b/chapter_sorting/merge_sort.md index eaa88cb32..8007cc934 100755 --- a/chapter_sorting/merge_sort.md +++ b/chapter_sorting/merge_sort.md @@ -11,6 +11,8 @@ comments: true ![归并排序的划分与合并阶段](merge_sort.assets/merge_sort_overview.png) +Fig. 归并排序的划分与合并阶段
+ ## 11.5.1. 算法流程 **「递归划分」** 从顶至底递归地 **将数组从中点切为两个子数组**,直至长度为 1 ; diff --git a/chapter_sorting/quick_sort.md b/chapter_sorting/quick_sort.md index 1fd0d5d18..d193e1b01 100755 --- a/chapter_sorting/quick_sort.md +++ b/chapter_sorting/quick_sort.md @@ -296,6 +296,8 @@ comments: true ![快速排序流程](quick_sort.assets/quick_sort_overview.png) +Fig. 快速排序流程
+ === "Java" ```java title="quick_sort.java" diff --git a/chapter_stack_and_queue/deque.md b/chapter_stack_and_queue/deque.md index ed0c61251..653adea3b 100644 --- a/chapter_stack_and_queue/deque.md +++ b/chapter_stack_and_queue/deque.md @@ -8,6 +8,8 @@ comments: true ![双向队列的操作](deque.assets/deque_operations.png) +Fig. 双向队列的操作
+ ## 5.3.1. 双向队列常用操作 双向队列的常用操作见下表,方法名需根据特定语言来确定。 diff --git a/chapter_stack_and_queue/queue.md b/chapter_stack_and_queue/queue.md index 52fe933e3..3c5f5cc3a 100755 --- a/chapter_stack_and_queue/queue.md +++ b/chapter_stack_and_queue/queue.md @@ -10,6 +10,8 @@ comments: true ![队列的先入先出规则](queue.assets/queue_operations.png) +Fig. 队列的先入先出规则
+ ## 5.2.1. 队列常用操作 队列的常用操作见下表,方法名需根据特定语言来确定。 diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md index cd07e7bc7..86cf65648 100755 --- a/chapter_stack_and_queue/stack.md +++ b/chapter_stack_and_queue/stack.md @@ -12,6 +12,8 @@ comments: true ![栈的先入后出规则](stack.assets/stack_operations.png) +Fig. 栈的先入后出规则
+ ## 5.1.1. 栈常用操作 栈的常用操作见下表(方法命名以 Java 为例)。 diff --git a/chapter_tree/avl_tree.md b/chapter_tree/avl_tree.md index cee11d176..1efe0fc74 100644 --- a/chapter_tree/avl_tree.md +++ b/chapter_tree/avl_tree.md @@ -10,10 +10,14 @@ comments: true ![AVL 树在删除结点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png) +Fig. AVL 树在删除结点后发生退化
+ 再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。 ![AVL 树在插入结点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png) +Fig. AVL 树在插入结点后发生退化
+ 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) +Fig. 有 grandChild 的右旋操作
+ “向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 === "Java" @@ -646,10 +652,14 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ![左旋操作](avl_tree.assets/avltree_left_rotate.png) +Fig. 左旋操作
+ 同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。 ![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png) +Fig. 有 grandChild 的左旋操作
+ 观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。 === "Java" @@ -826,18 +836,24 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png) +Fig. 先左旋后右旋
+ ### Case 4 - 先右后左 同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。 ![先右旋后左旋](avl_tree.assets/avltree_right_left_rotate.png) +Fig. 先右旋后左旋
+ ### 旋转的选择 下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。 ![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png) +Fig. AVL 树的四种旋转情况
+ 具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。Fig. 二叉搜索树
+ ## 7.3.1. 二叉搜索树的操作 ### 查找结点 @@ -249,6 +251,8 @@ comments: true ![在二叉搜索树中插入结点](binary_search_tree.assets/bst_insert.png) +Fig. 在二叉搜索树中插入结点
+ === "Java" ```java title="binary_search_tree.java" @@ -551,10 +555,14 @@ comments: true ![在二叉搜索树中删除结点(度为 0)](binary_search_tree.assets/bst_remove_case1.png) +Fig. 在二叉搜索树中删除结点(度为 0)
+ **当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。 ![在二叉搜索树中删除结点(度为 1)](binary_search_tree.assets/bst_remove_case2.png) +Fig. 在二叉搜索树中删除结点(度为 1)
+ **当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步: 1. 找到待删除结点在 **中序遍历序列** 中的下一个结点,记为 `nex` ; @@ -1137,6 +1145,8 @@ comments: true ![二叉搜索树的中序遍历序列](binary_search_tree.assets/bst_inorder_traversal.png) +Fig. 二叉搜索树的中序遍历序列
+ ## 7.3.2. 二叉搜索树的效率 假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为: @@ -1178,6 +1188,8 @@ comments: true ![二叉搜索树的平衡与退化](binary_search_tree.assets/bst_degradation.png) +Fig. 二叉搜索树的平衡与退化
+ ## 7.3.4. 二叉搜索树常见应用 - 系统中的多级索引,高效查找、插入、删除操作。 diff --git a/chapter_tree/binary_tree.md b/chapter_tree/binary_tree.md index cf2c7b286..e2b21d778 100644 --- a/chapter_tree/binary_tree.md +++ b/chapter_tree/binary_tree.md @@ -133,6 +133,8 @@ comments: true ![父结点、子结点、子树](binary_tree.assets/binary_tree_definition.png) +Fig. 父结点、子结点、子树
+ ## 7.1.1. 二叉树常见术语 二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。 @@ -148,6 +150,8 @@ comments: true ![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png) +Fig. 二叉树的常用术语
+ !!! tip "高度与深度的定义" 值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。 @@ -306,6 +310,8 @@ comments: true ![在二叉树中插入与删除结点](binary_tree.assets/binary_tree_add_remove.png) +Fig. 在二叉树中插入与删除结点
+ === "Java" ```java title="binary_tree.java" @@ -428,6 +434,8 @@ comments: true ![完美二叉树](binary_tree.assets/perfect_binary_tree.png) +Fig. 完美二叉树
+ ### 完全二叉树 「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点尽量靠左填充。 @@ -436,18 +444,24 @@ comments: true ![完全二叉树](binary_tree.assets/complete_binary_tree.png) +Fig. 完全二叉树
+ ### 完满二叉树 「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。 ![完满二叉树](binary_tree.assets/full_binary_tree.png) +Fig. 完满二叉树
+ ### 平衡二叉树 「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。 ![平衡二叉树](binary_tree.assets/balanced_binary_tree.png) +Fig. 平衡二叉树
+ ## 7.1.4. 二叉树的退化 当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。 @@ -457,6 +471,8 @@ comments: true ![二叉树的最佳与最二叉树的最佳和最差结构差情况](binary_tree.assets/binary_tree_corner_cases.png) +Fig. 二叉树的最佳与最二叉树的最佳和最差结构差情况
+ 如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。Fig. 完美二叉树的数组表示
+ 然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。 ![给定数组对应多种二叉树可能性](binary_tree.assets/array_representation_without_empty.png) +Fig. 给定数组对应多种二叉树可能性
+ 为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。 === "Java" @@ -563,8 +583,12 @@ comments: true ![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png) +Fig. 任意类型二叉树的数组表示
+ 回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。 ![完全二叉树的数组表示](binary_tree.assets/array_representation_complete_binary_tree.png) +Fig. 完全二叉树的数组表示
+ 数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。 diff --git a/chapter_tree/binary_tree_traversal.md b/chapter_tree/binary_tree_traversal.md index 28144247d..15ca018af 100755 --- a/chapter_tree/binary_tree_traversal.md +++ b/chapter_tree/binary_tree_traversal.md @@ -16,6 +16,8 @@ comments: true ![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png) +Fig. 二叉树的层序遍历
+ ### 算法实现 广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。 @@ -256,6 +258,8 @@ comments: true ![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png) +Fig. 二叉搜索树的前、中、后序遍历
+