diff --git a/docs/chapter_array_and_linkedlist/summary.md b/docs/chapter_array_and_linkedlist/summary.md index efde57b4a..280ba75b3 100644 --- a/docs/chapter_array_and_linkedlist/summary.md +++ b/docs/chapter_array_and_linkedlist/summary.md @@ -4,8 +4,7 @@ - 数组支持随机访问、内存空间占用小;但插入与删除元素效率低,且初始化后长度不可变。 - 链表可通过更改指针实现高效的结点插入与删除,并且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型有单向链表、循环链表、双向链表。 - 列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,且可以灵活改变长度。列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费。 - -## 数组 VS 链表 +- 下表总结对比了数组与链表的各项特性。
@@ -18,9 +17,11 @@
-!!! tip +!!! question "缓存局部性的简单解释" + + 在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存”。当我们访问数组元素时,计算机不仅会加载它,还会缓存其周围的其它数据,从而借助高速缓存来提升后续操作的执行速度。链表则不然,计算机只能挨个地缓存各个结点,这样的多次“搬运”降低了整体效率。 - 「缓存局部性(Cache locality)」涉及到了计算机操作系统,在本书不做展开介绍,建议有兴趣的同学 Google / Baidu 一下。 +- 下表对比了数组与链表的各种操作效率。
diff --git a/docs/chapter_graph/graph.md b/docs/chapter_graph/graph.md index 71e0496e9..64191c65e 100644 --- a/docs/chapter_graph/graph.md +++ b/docs/chapter_graph/graph.md @@ -36,8 +36,8 @@ $$ ## 图常用术语 -- 「邻接 Adjacency」:当两顶点之间有边相连时,称此两顶点“邻接”。 -- 「路径 Path」:从顶点 A 到顶点 B 走过的边构成的序列,被称为从 A 到 B 的“路径”。 +- 「邻接 Adjacency」:当两顶点之间有边相连时,称此两顶点“邻接”。例如,上图中顶点 1 的邻接顶点为顶点 2, 3, 5 。 +- 「路径 Path」:从顶点 A 到顶点 B 走过的边构成的序列,被称为从 A 到 B 的“路径”。例如,上图中 1, 5, 2, 4 是顶点 1 到顶点 4 的一个路径。 - 「度 Degree」表示一个顶点具有多少条边。对于有向图,「入度 In-Degree」表示有多少条边指向该顶点,「出度 Out-Degree」表示有多少条边从该顶点指出。 ## 图的表示 @@ -62,13 +62,13 @@ $$ ### 邻接表 -「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表结点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了所有与该顶点相连的顶点。 +「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表结点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点(即与该顶点相连的顶点)。 ![图的邻接表表示](graph.assets/adjacency_list.png) 邻接表仅存储存在的边,而边的总数往往远小于 $n^2$ ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。 -观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如,当链表较长时,可以把链表转化为「AVL 树」,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet(即哈希表),将时间复杂度降低至 $O(1)$ 。 +观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如,当链表较长时,可以把链表转化为 AVL 树或红黑树,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为哈希表,将时间复杂度降低至 $O(1)$ 。 ## 图常见应用 diff --git a/docs/chapter_graph/summary.md b/docs/chapter_graph/summary.md new file mode 100644 index 000000000..a715f38f1 --- /dev/null +++ b/docs/chapter_graph/summary.md @@ -0,0 +1,13 @@ +# 小结 + +- 图由顶点和边组成,可以表示为一组顶点和一组边构成的集合。 +- 相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂。 +- 有向图的边存在方向,连通图中的任意顶点都可达,有权图的每条边都包含权重变量。 +- 邻接矩阵使用方阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,使用 $1$ 或 $0$ 来表示两个顶点之间有边或无边。邻接矩阵的增删查操作效率很高,但占用空间大。 +- 邻接表使用多个链表来表示图,第 $i$ 条链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点。邻接表相对邻接矩阵更加节省空间,但由于需要通过遍历链表来查找边,因此时间效率较低。 +- 当邻接表中的链表过长时,可以将其转化为红黑树或哈希表,从而提升查询效率。 +- 从算法思想角度分析,邻接矩阵体现“以空间换时间”,邻接表体现“以时间换空间” +- 图可以用于建模各类现实系统,例如社交网络、地铁线路等。 +- 树是图的一种特例,树的遍历也是图的遍历的一种特例。 +- 图的广度优先遍历是一种由近及远、层层扩张的搜索方式,常借助队列实现。 +- 图的深度优先遍历是一种优先走到底、无路可走再回头的搜索方式,常基于递归来实现。 diff --git a/docs/chapter_hashing/hash_collision.md b/docs/chapter_hashing/hash_collision.md index 8e43b2631..a7c5236b3 100644 --- a/docs/chapter_hashing/hash_collision.md +++ b/docs/chapter_hashing/hash_collision.md @@ -4,9 +4,9 @@ 那么,为什么会出现哈希冲突呢?本质上看,**由于哈希函数的输入空间往往远大于输出空间**,因此不可避免地会出现多个输入产生相同输出的情况,即为哈希冲突。比如,输入空间是全体整数,输出空间是一个固定大小的桶(数组)的索引范围,那么必定会有多个整数同时映射到一个桶索引。 -为了缓解哈希冲突,一方面,我们可以通过「哈希表扩容」来减小冲突概率。极端情况下,当输入空间和输出空间大小相等时,哈希表就等价于数组了,可谓“大力出奇迹”。 +为了缓解哈希冲突,一方面,**我们可以通过哈希表扩容来减小冲突概率**。极端情况下,当输入空间和输出空间大小相等时,哈希表就等价于数组了,可谓“大力出奇迹”。 -另一方面,**考虑通过优化数据结构以缓解哈希冲突**,常见的方法有「链式地址」和「开放寻址」。 +另一方面,**考虑通过优化哈希表的表示方式以缓解哈希冲突**,常见的方法有「链式地址」和「开放寻址」。 ## 哈希表扩容 @@ -33,7 +33,7 @@ - **占用空间变大**,因为链表或二叉树包含结点指针,相比于数组更加耗费内存空间; - **查询效率降低**,因为需要线性遍历链表来查找对应元素; -为了缓解时间效率问题,**可以把「链表」转化为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。 +为了提升操作效率,**可以把「链表」转化为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。 ## 开放寻址 diff --git a/docs/chapter_hashing/summary.md b/docs/chapter_hashing/summary.md index 2e79e520a..cd61196cf 100644 --- a/docs/chapter_hashing/summary.md +++ b/docs/chapter_hashing/summary.md @@ -1 +1,11 @@ # 小结 + +- 向哈希表中输入一个键 key ,查询到值 value 的时间复杂度为 $O(1)$ ,非常高效。 +- 哈希表的常用操作包括查询、添加与删除键值对、遍历键值对等。 +- 哈希函数将 key 映射到桶(数组)索引,从而访问到对应的值 value 。 +- 两个不同的 key 经过哈希函数可能得到相同的桶索引,进而发生哈希冲突,导致查询错误。 +- 缓解哈希冲突的途径有两种:哈希表扩容、优化哈希表的表示方式。 +- 负载因子定义为哈希表中元素数量除以桶槽数量,体现哈希冲突的严重程度,常用作哈希表扩容的触发条件。与数组扩容的原理类似,哈希表扩容操作开销也很大。 +- 链式地址考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中,从而解决哈希冲突。而为了提升查询效率,可以把链表转化为 AVL 树或红黑树, +- 开放寻址通过多次探测来解决哈希冲突。线性探测使用固定步长,缺点是不能删除元素且容易产生聚集。多次哈希使用多个哈希函数进行探测,相对线性探测不容易产生聚集,代价是多个哈希函数增加了计算量。 +- 在工业界中,Java 的 HashMap 采用链式地址、Python 的 Dict 采用开放寻址。 diff --git a/docs/chapter_heap/heap.assets/heapify_operations_count.png b/docs/chapter_heap/build_heap.assets/heapify_operations_count.png similarity index 100% rename from docs/chapter_heap/heap.assets/heapify_operations_count.png rename to docs/chapter_heap/build_heap.assets/heapify_operations_count.png diff --git a/docs/chapter_heap/build_heap.md b/docs/chapter_heap/build_heap.md index 87ab47c14..defb518bf 100644 --- a/docs/chapter_heap/build_heap.md +++ b/docs/chapter_heap/build_heap.md @@ -10,7 +10,7 @@ ### 基于堆化操作实现 -然而,**存在一种更加高效的建堆方法**。设结点数量为 $n$ ,我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。 +然而,**存在一种更加高效的建堆方法**。设元素数量为 $n$ ,我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。 === "Java" @@ -89,7 +89,7 @@ $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1 $$ -![完美二叉树的各层结点数量](heap.assets/heapify_operations_count.png) +![完美二叉树的各层结点数量](build_heap.assets/heapify_operations_count.png) 化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得 diff --git a/docs/chapter_heap/summary.md b/docs/chapter_heap/summary.md new file mode 100644 index 000000000..c8773d28c --- /dev/null +++ b/docs/chapter_heap/summary.md @@ -0,0 +1,8 @@ +# 小结 + +- 堆是一棵限定条件下的完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素最大(小)。 +- 优先队列定义为一种具有出队优先级的队列。堆是实现优先队列的最常用数据结构。 +- 堆的常用操作和对应时间复杂度为元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 、访问堆顶元素 $O(1)$ 等。 +- 完全二叉树非常适合用数组来表示,因此我们一般用数组来存储堆。 +- 堆化操作用于修复堆的特性,在入堆和出堆操作中都会使用到。 +- 输入 $n$ 个元素并建堆的时间复杂度可以被优化至 $O(n)$ ,非常高效。 diff --git a/docs/chapter_introduction/summary.md b/docs/chapter_introduction/summary.md new file mode 100644 index 000000000..d449ed00a --- /dev/null +++ b/docs/chapter_introduction/summary.md @@ -0,0 +1,7 @@ +# 小结 + +- 算法在生活中随处可见,并不高深莫测。我们已经不知不觉地学习到许多“算法”,用于解决生活中大大小小的问题。 +- “查字典”的原理和二分查找算法一致。二分体现分而治之的重要算法思想。 +- 算法是在有限时间内解决特定问题的一组指令或操作步骤,数据结构是在计算机中组织与存储数据的方式。 +- 数据结构与算法两者紧密联系。数据结构是算法的底座,算法是发挥数据结构的舞台。 +- 乐高积木对应数据,积木形状和连接形式对应数据结构,拼装积木的流程步骤对应算法。 diff --git a/docs/chapter_introduction/what_is_dsa.md b/docs/chapter_introduction/what_is_dsa.md index c24d7959b..03196eabe 100644 --- a/docs/chapter_introduction/what_is_dsa.md +++ b/docs/chapter_introduction/what_is_dsa.md @@ -24,7 +24,7 @@ 「数据结构」与「算法」是高度相关、紧密嵌合的,体现在: - 数据结构是算法的底座。数据结构为算法提供结构化存储的数据,以及操作数据的对应方法。 -- 算法是发挥数据结构优势的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。 +- 算法是数据结构发挥的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。 - 算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。 ![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png) @@ -33,12 +33,12 @@
-| 数据结构与算法 | LEGO 乐高 | -| -------------- | ---------------------------------------- | -| 输入数据 | 未拼装的积木 | +| 数据结构与算法 | LEGO 乐高 | +| -------------- | ------------------------------- | +| 输入数据 | 未拼装的积木 | | 数据结构 | 积木组织形式,包括形状、大小、连接方式等 | -| 算法 | 把积木拼成目标形态的一系列操作步骤 | -| 输出数据 | 积木模型 | +| 算法 | 把积木拼成目标形态的一系列操作步骤 | +| 输出数据 | 积木模型 |
diff --git a/docs/chapter_searching/summary.md b/docs/chapter_searching/summary.md index 9b20e7102..755e536f2 100644 --- a/docs/chapter_searching/summary.md +++ b/docs/chapter_searching/summary.md @@ -3,6 +3,7 @@ - 线性查找是一种最基础的查找方法,通过遍历数据结构 + 判断条件实现查找。 - 二分查找利用数据的有序性,通过循环不断缩小一半搜索区间来实现查找,其要求输入数据是有序的,并且仅适用于数组或基于数组实现的数据结构。 - 哈希查找借助哈希表来实现常数阶时间复杂度的查找操作,体现以空间换时间的算法思想。 +- 下表总结对比了查找算法的各种特性和时间复杂度。
diff --git a/docs/chapter_sorting/summary.assets/sorting_algorithms_comparison.png b/docs/chapter_sorting/summary.assets/sorting_algorithms_comparison.png new file mode 100644 index 000000000..d5e92db40 Binary files /dev/null and b/docs/chapter_sorting/summary.assets/sorting_algorithms_comparison.png differ diff --git a/docs/chapter_sorting/summary.md b/docs/chapter_sorting/summary.md index b65d06938..5a29d1a0c 100644 --- a/docs/chapter_sorting/summary.md +++ b/docs/chapter_sorting/summary.md @@ -1,2 +1,11 @@ # 小结 +- 冒泡排序通过交换相邻元素来实现排序。通过增加标志位实现提前返回,我们可将冒泡排序的最佳时间复杂度优化至 $O(N)$ 。 +- 插入排序每轮将待排序区间内元素插入至已排序区间的正确位置,从而实现排序。插入排序的时间复杂度虽为 $O(N^2)$ ,但因为总体操作少而很受欢迎,一般用于小数据量的排序工作。 +- 快速排序基于哨兵划分操作实现排序。在哨兵划分中,有可能每次都选取到最差的基准数,从而导致时间复杂度劣化至 $O(N^2)$ ,通过引入中位数基准数或随机基准数可大大降低劣化概率。尾递归方法可以有效减小递归深度,将空间复杂度优化至 $O(\log N)$ 。 +- 归并排序包含划分和合并两个阶段,是分而治之的标准体现。对于归并排序,排序数组需要借助辅助数组,空间复杂度为 $O(N)$ ;而排序链表的空间复杂度可以被优化至 $O(1)$ 。 +- 下图总结对比了各个排序算法的运行效率与特性。其中,桶排序中 $k$ 为桶的数量;基数排序仅适用于正整数、字符串、特定格式的浮点数,$k$ 为最大数字的位数。 + +![排序算法对比](summary.assets/sorting_algorithms_comparison.png) + +- 总体来看,我们追求运行快、稳定、原地、正向自适应性的排序。显然,如同其它数据结构与算法一样,同时满足这些条件的排序算法并不存在,我们需要根据问题特点来选择排序算法。 diff --git a/docs/chapter_tree/summary.md b/docs/chapter_tree/summary.md index c9d9f6f89..c2a777690 100644 --- a/docs/chapter_tree/summary.md +++ b/docs/chapter_tree/summary.md @@ -1,5 +1,7 @@ # 小结 +### 二叉树 + - 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。 - 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。 - 二叉树的术语较多,包括根结点、叶结点、层、度、边、高度、深度等。 @@ -7,8 +9,13 @@ - 常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态。 - 二叉树可以使用数组表示,具体做法是将结点值和空位按照层序遍历的顺序排列,并基于父结点和子结点之间的索引映射公式实现指针。 +### 二叉树遍历 + - 二叉树层序遍历是一种广度优先搜索,体现着“一圈一圈向外”的层进式遍历方式,通常借助队列来实现。 - 前序、中序、后序遍历是深度优先搜索,体现着“走到头、再回头继续”的回溯遍历方式,通常使用递归实现。 + +### 二叉搜索树 + - 二叉搜索树是一种高效的元素查找数据结构,查找、插入、删除操作的时间复杂度皆为 $O(\log n)$ 。二叉搜索树退化为链表后,各项时间复杂度劣化至 $O(n)$ ,因此如何避免退化是非常重要的课题。 - AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除结点后,仍然可以保持二叉树的平衡(不退化)。 - AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除结点后,AVL 树会从底至顶地执行旋转操作,使树恢复平衡。 diff --git a/mkdocs.yml b/mkdocs.yml index b912ed6b0..7c24f4caf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -132,6 +132,7 @@ nav: - 1.     引言: - 1.1.   算法无处不在: chapter_introduction/algorithms_are_everywhere.md - 1.2.   算法是什么: chapter_introduction/what_is_dsa.md + - 1.3.   小结: chapter_introduction/summary.md - 2.     计算复杂度: - 2.1.   算法效率评估: chapter_computational_complexity/performance_evaluation.md - 2.2.   时间复杂度: chapter_computational_complexity/time_complexity.md @@ -165,10 +166,12 @@ nav: - 8.     堆: - 8.1.   堆(Heap): chapter_heap/heap.md - 8.2.   建堆操作 *: chapter_heap/build_heap.md + - 8.3.   小结: chapter_heap/summary.md - 9.     图: - 9.1.   图(Graph): chapter_graph/graph.md - 9.2.   图基础操作: chapter_graph/graph_operations.md - 9.3.   图的遍历: chapter_graph/graph_traversal.md + - 9.4.   小结: chapter_graph/summary.md - 10.     查找算法: - 10.1.   线性查找: chapter_searching/linear_search.md - 10.2.   二分查找: chapter_searching/binary_search.md