Skip to content

Commit

Permalink
Add summary for the chapters of introduction, hashing, heap, graph, s…
Browse files Browse the repository at this point in the history
…orting
  • Loading branch information
krahets committed Feb 26, 2023
1 parent 1a49631 commit c2d6415
Show file tree
Hide file tree
Showing 15 changed files with 78 additions and 19 deletions.
9 changes: 5 additions & 4 deletions docs/chapter_array_and_linkedlist/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
- 数组支持随机访问、内存空间占用小;但插入与删除元素效率低,且初始化后长度不可变。
- 链表可通过更改指针实现高效的结点插入与删除,并且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型有单向链表、循环链表、双向链表。
- 列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,且可以灵活改变长度。列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费。

## 数组 VS 链表
- 下表总结对比了数组与链表的各项特性。

<div class="center-table" markdown>

Expand All @@ -18,9 +17,11 @@

</div>

!!! tip
!!! question "缓存局部性的简单解释"

在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存”。当我们访问数组元素时,计算机不仅会加载它,还会缓存其周围的其它数据,从而借助高速缓存来提升后续操作的执行速度。链表则不然,计算机只能挨个地缓存各个结点,这样的多次“搬运”降低了整体效率。

「缓存局部性(Cache locality)」涉及到了计算机操作系统,在本书不做展开介绍,建议有兴趣的同学 Google / Baidu 一下
- 下表对比了数组与链表的各种操作效率

<div class="center-table" markdown>

Expand Down
8 changes: 4 additions & 4 deletions docs/chapter_graph/graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -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」表示有多少条边从该顶点指出。

## 图的表示
Expand All @@ -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)$ 。

## 图常见应用

Expand Down
13 changes: 13 additions & 0 deletions docs/chapter_graph/summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 小结

- 图由顶点和边组成,可以表示为一组顶点和一组边构成的集合。
- 相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂。
- 有向图的边存在方向,连通图中的任意顶点都可达,有权图的每条边都包含权重变量。
- 邻接矩阵使用方阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,使用 $1$ 或 $0$ 来表示两个顶点之间有边或无边。邻接矩阵的增删查操作效率很高,但占用空间大。
- 邻接表使用多个链表来表示图,第 $i$ 条链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点。邻接表相对邻接矩阵更加节省空间,但由于需要通过遍历链表来查找边,因此时间效率较低。
- 当邻接表中的链表过长时,可以将其转化为红黑树或哈希表,从而提升查询效率。
- 从算法思想角度分析,邻接矩阵体现“以空间换时间”,邻接表体现“以时间换空间”
- 图可以用于建模各类现实系统,例如社交网络、地铁线路等。
- 树是图的一种特例,树的遍历也是图的遍历的一种特例。
- 图的广度优先遍历是一种由近及远、层层扩张的搜索方式,常借助队列实现。
- 图的深度优先遍历是一种优先走到底、无路可走再回头的搜索方式,常基于递归来实现。
6 changes: 3 additions & 3 deletions docs/chapter_hashing/hash_collision.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

那么,为什么会出现哈希冲突呢?本质上看,**由于哈希函数的输入空间往往远大于输出空间**,因此不可避免地会出现多个输入产生相同输出的情况,即为哈希冲突。比如,输入空间是全体整数,输出空间是一个固定大小的桶(数组)的索引范围,那么必定会有多个整数同时映射到一个桶索引。

为了缓解哈希冲突,一方面,我们可以通过「哈希表扩容」来减小冲突概率。极端情况下,当输入空间和输出空间大小相等时,哈希表就等价于数组了,可谓“大力出奇迹”。
为了缓解哈希冲突,一方面,**我们可以通过哈希表扩容来减小冲突概率**。极端情况下,当输入空间和输出空间大小相等时,哈希表就等价于数组了,可谓“大力出奇迹”。

另一方面,**考虑通过优化数据结构以缓解哈希冲突**,常见的方法有「链式地址」和「开放寻址」。
另一方面,**考虑通过优化哈希表的表示方式以缓解哈希冲突**,常见的方法有「链式地址」和「开放寻址」。

## 哈希表扩容

Expand All @@ -33,7 +33,7 @@
- **占用空间变大**,因为链表或二叉树包含结点指针,相比于数组更加耗费内存空间;
- **查询效率降低**,因为需要线性遍历链表来查找对应元素;

为了缓解时间效率问题**可以把「链表」转化为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。
为了提升操作效率**可以把「链表」转化为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。

## 开放寻址

Expand Down
10 changes: 10 additions & 0 deletions docs/chapter_hashing/summary.md
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# 小结

- 向哈希表中输入一个键 key ,查询到值 value 的时间复杂度为 $O(1)$ ,非常高效。
- 哈希表的常用操作包括查询、添加与删除键值对、遍历键值对等。
- 哈希函数将 key 映射到桶(数组)索引,从而访问到对应的值 value 。
- 两个不同的 key 经过哈希函数可能得到相同的桶索引,进而发生哈希冲突,导致查询错误。
- 缓解哈希冲突的途径有两种:哈希表扩容、优化哈希表的表示方式。
- 负载因子定义为哈希表中元素数量除以桶槽数量,体现哈希冲突的严重程度,常用作哈希表扩容的触发条件。与数组扩容的原理类似,哈希表扩容操作开销也很大。
- 链式地址考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中,从而解决哈希冲突。而为了提升查询效率,可以把链表转化为 AVL 树或红黑树,
- 开放寻址通过多次探测来解决哈希冲突。线性探测使用固定步长,缺点是不能删除元素且容易产生聚集。多次哈希使用多个哈希函数进行探测,相对线性探测不容易产生聚集,代价是多个哈希函数增加了计算量。
- 在工业界中,Java 的 HashMap 采用链式地址、Python 的 Dict 采用开放寻址。
4 changes: 2 additions & 2 deletions docs/chapter_heap/build_heap.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

### 基于堆化操作实现

然而,**存在一种更加高效的建堆方法**设结点数量为 $n$ ,我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。
然而,**存在一种更加高效的建堆方法**设元素数量为 $n$ ,我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。

=== "Java"

Expand Down Expand Up @@ -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$ ,易得

Expand Down
8 changes: 8 additions & 0 deletions docs/chapter_heap/summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 小结

- 堆是一棵限定条件下的完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素最大(小)。
- 优先队列定义为一种具有出队优先级的队列。堆是实现优先队列的最常用数据结构。
- 堆的常用操作和对应时间复杂度为元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 、访问堆顶元素 $O(1)$ 等。
- 完全二叉树非常适合用数组来表示,因此我们一般用数组来存储堆。
- 堆化操作用于修复堆的特性,在入堆和出堆操作中都会使用到。
- 输入 $n$ 个元素并建堆的时间复杂度可以被优化至 $O(n)$ ,非常高效。
7 changes: 7 additions & 0 deletions docs/chapter_introduction/summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 小结

- 算法在生活中随处可见,并不高深莫测。我们已经不知不觉地学习到许多“算法”,用于解决生活中大大小小的问题。
- “查字典”的原理和二分查找算法一致。二分体现分而治之的重要算法思想。
- 算法是在有限时间内解决特定问题的一组指令或操作步骤,数据结构是在计算机中组织与存储数据的方式。
- 数据结构与算法两者紧密联系。数据结构是算法的底座,算法是发挥数据结构的舞台。
- 乐高积木对应数据,积木形状和连接形式对应数据结构,拼装积木的流程步骤对应算法。
12 changes: 6 additions & 6 deletions docs/chapter_introduction/what_is_dsa.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
「数据结构」与「算法」是高度相关、紧密嵌合的,体现在:

- 数据结构是算法的底座。数据结构为算法提供结构化存储的数据,以及操作数据的对应方法。
- 算法是发挥数据结构优势的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。
- 算法是数据结构发挥的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。
- 算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。

![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png)
Expand All @@ -33,12 +33,12 @@

<div class="center-table" markdown>

| 数据结构与算法 | LEGO 乐高 |
| -------------- | ---------------------------------------- |
| 输入数据 | 未拼装的积木 |
| 数据结构与算法 | LEGO 乐高 |
| -------------- | ------------------------------- |
| 输入数据 | 未拼装的积木 |
| 数据结构 | 积木组织形式,包括形状、大小、连接方式等 |
| 算法 | 把积木拼成目标形态的一系列操作步骤 |
| 输出数据 | 积木模型 |
| 算法 | 把积木拼成目标形态的一系列操作步骤 |
| 输出数据 | 积木模型 |

</div>

Expand Down
1 change: 1 addition & 0 deletions docs/chapter_searching/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- 线性查找是一种最基础的查找方法,通过遍历数据结构 + 判断条件实现查找。
- 二分查找利用数据的有序性,通过循环不断缩小一半搜索区间来实现查找,其要求输入数据是有序的,并且仅适用于数组或基于数组实现的数据结构。
- 哈希查找借助哈希表来实现常数阶时间复杂度的查找操作,体现以空间换时间的算法思想。
- 下表总结对比了查找算法的各种特性和时间复杂度。

<div class="center-table" markdown>

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions docs/chapter_sorting/summary.md
Original file line number Diff line number Diff line change
@@ -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)

- 总体来看,我们追求运行快、稳定、原地、正向自适应性的排序。显然,如同其它数据结构与算法一样,同时满足这些条件的排序算法并不存在,我们需要根据问题特点来选择排序算法。
7 changes: 7 additions & 0 deletions docs/chapter_tree/summary.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
# 小结

### 二叉树

- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。
- 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。
- 二叉树的术语较多,包括根结点、叶结点、层、度、边、高度、深度等。
- 二叉树的初始化、结点插入、结点删除操作与链表的操作方法类似。
- 常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态。
- 二叉树可以使用数组表示,具体做法是将结点值和空位按照层序遍历的顺序排列,并基于父结点和子结点之间的索引映射公式实现指针。

### 二叉树遍历

- 二叉树层序遍历是一种广度优先搜索,体现着“一圈一圈向外”的层进式遍历方式,通常借助队列来实现。
- 前序、中序、后序遍历是深度优先搜索,体现着“走到头、再回头继续”的回溯遍历方式,通常使用递归实现。

### 二叉搜索树

- 二叉搜索树是一种高效的元素查找数据结构,查找、插入、删除操作的时间复杂度皆为 $O(\log n)$ 。二叉搜索树退化为链表后,各项时间复杂度劣化至 $O(n)$ ,因此如何避免退化是非常重要的课题。
- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除结点后,仍然可以保持二叉树的平衡(不退化)。
- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除结点后,AVL 树会从底至顶地执行旋转操作,使树恢复平衡。
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ nav:
- 1. &nbsp; &nbsp; 引言:
- 1.1. &nbsp; 算法无处不在: chapter_introduction/algorithms_are_everywhere.md
- 1.2. &nbsp; 算法是什么: chapter_introduction/what_is_dsa.md
- 1.3. &nbsp; 小结: chapter_introduction/summary.md
- 2. &nbsp; &nbsp; 计算复杂度:
- 2.1. &nbsp; 算法效率评估: chapter_computational_complexity/performance_evaluation.md
- 2.2. &nbsp; 时间复杂度: chapter_computational_complexity/time_complexity.md
Expand Down Expand Up @@ -165,10 +166,12 @@ nav:
- 8. &nbsp; &nbsp; 堆:
- 8.1. &nbsp; 堆(Heap): chapter_heap/heap.md
- 8.2. &nbsp; 建堆操作 *: chapter_heap/build_heap.md
- 8.3. &nbsp; 小结: chapter_heap/summary.md
- 9. &nbsp; &nbsp; 图:
- 9.1. &nbsp; 图(Graph): chapter_graph/graph.md
- 9.2. &nbsp; 图基础操作: chapter_graph/graph_operations.md
- 9.3. &nbsp; 图的遍历: chapter_graph/graph_traversal.md
- 9.4. &nbsp; 小结: chapter_graph/summary.md
- 10. &nbsp; &nbsp; 查找算法:
- 10.1. &nbsp; 线性查找: chapter_searching/linear_search.md
- 10.2. &nbsp; 二分查找: chapter_searching/binary_search.md
Expand Down

0 comments on commit c2d6415

Please sign in to comment.