Skip to content

Commit

Permalink
Update the captions of all the figures.
Browse files Browse the repository at this point in the history
  • Loading branch information
krahets committed Feb 26, 2023
1 parent 85d04b3 commit 9e99ac0
Show file tree
Hide file tree
Showing 31 changed files with 99 additions and 175 deletions.
12 changes: 4 additions & 8 deletions docs/chapter_array_and_linkedlist/array.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

「数组 Array」是一种将 **相同类型元素** 存储在 **连续内存空间** 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。

![array_definition](array.assets/array_definition.png)

<p align="center"> Fig. 数组定义与存储方式 </p>
![数组定义与存储方式](array.assets/array_definition.png)

!!! note

Expand Down Expand Up @@ -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
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
Expand Down Expand Up @@ -241,7 +237,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex

**数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。

![array_insert_element](array.assets/array_insert_element.png)
![数组插入元素](array.assets/array_insert_element.png)

=== "Java"

Expand Down Expand Up @@ -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"

Expand Down
10 changes: 4 additions & 6 deletions docs/chapter_array_and_linkedlist/linked_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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>
20 changes: 5 additions & 15 deletions docs/chapter_computational_complexity/space_complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
- 「栈帧空间」用于保存调用函数的上下文数据。系统每次调用函数都会在栈的顶部创建一个栈帧,函数返回时,栈帧空间会被释放。
- 「指令空间」用于保存编译后的程序指令,**在实际统计中一般忽略不计**

![space_types](space_complexity.assets/space_types.png)

<p align="center"> Fig. 算法使用的相关空间 </p>
![算法使用的相关空间](space_complexity.assets/space_types.png)

=== "Java"

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)$

Expand Down Expand Up @@ -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)$

Expand Down Expand Up @@ -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)$

Expand Down
32 changes: 8 additions & 24 deletions docs/chapter_computational_complexity/time_complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足?

Expand Down Expand Up @@ -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$ 的倍数)**

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)$ 。

Expand Down Expand Up @@ -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$ 次后停止。

Expand Down Expand Up @@ -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$ 的递归树。

Expand Down Expand Up @@ -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!)$

Expand Down Expand Up @@ -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)

## 最差、最佳、平均时间复杂度

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

## 物理结构:连续与离散

Expand All @@ -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)

**所有数据结构都是基于数组、或链表、或两者组合实现的**。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。

Expand Down
6 changes: 2 additions & 4 deletions docs/chapter_data_structure/data_and_memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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$ ,易得

Expand Down Expand Up @@ -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)

**内存资源是设计数据结构与算法的重要考虑因素**。内存是所有程序的公共资源,当内存被某程序占用时,不能被其它程序同时使用。我们需要根据剩余内存资源的情况来设计算法。例如,若剩余内存空间有限,则要求算法占用的峰值内存不能超过系统剩余内存;若运行的程序很多、缺少大块连续的内存空间,则要求选取的数据结构必须能够存储在离散的内存空间内。
12 changes: 6 additions & 6 deletions docs/chapter_graph/graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**

Expand All @@ -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)

## 图常用术语

Expand All @@ -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)

邻接矩阵具有以下性质:

Expand All @@ -64,7 +64,7 @@ $$

「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表结点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了所有与该顶点相连的顶点。

![adjacency_list](graph.assets/adjacency_list.png)
![图的邻接表表示](graph.assets/adjacency_list.png)

邻接表仅存储存在的边,而边的总数往往远小于 $n^2$ ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。

Expand Down
4 changes: 2 additions & 2 deletions docs/chapter_graph/graph_traversal.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

**广度优先遍历优是一种由近及远的遍历方式,从距离最近的顶点开始访问,并一层层向外扩张**。具体地,从某个顶点出发,先遍历该顶点的所有邻接顶点,随后遍历下个顶点的所有邻接顶点,以此类推……

![graph_bfs](graph_traversal.assets/graph_bfs.png)
![图的广度优先遍历](graph_traversal.assets/graph_bfs.png)

### 算法实现

Expand Down Expand Up @@ -133,7 +133,7 @@ BFS 常借助「队列」来实现。队列具有“先入先出”的性质,

**深度优先遍历是一种优先走到底、无路可走再回头的遍历方式**。具体地,从某个顶点出发,不断地访问当前结点的某个邻接顶点,直到走到尽头时回溯,再继续走到底 + 回溯,以此类推……直至所有顶点遍历完成时结束。

![graph_dfs](graph_traversal.assets/graph_dfs.png)
![图的深度优先遍历](graph_traversal.assets/graph_dfs.png)

### 算法实现

Expand Down
4 changes: 2 additions & 2 deletions docs/chapter_hashing/hash_collision.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

在原始哈希表中,桶内的每个地址只能存储一个元素(即键值对)。**考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中**

![hash_collision_chaining](hash_collision.assets/hash_collision_chaining.png)
![链式地址](hash_collision.assets/hash_collision_chaining.png)

链式地址下,哈希表操作方法为:

Expand Down Expand Up @@ -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)

线性探测存在以下缺陷:

Expand Down
12 changes: 3 additions & 9 deletions docs/chapter_hashing/hash_map.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

## 哈希表效率

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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)

综上所述,一个优秀的「哈希函数」应该具备以下特性:

Expand Down
Loading

0 comments on commit 9e99ac0

Please sign in to comment.