Fix all the ** (bolded symbols).

This commit is contained in:
Yudong Jin 2023-01-09 22:39:30 +08:00
parent 97ee638d31
commit aaa2ff29f9
20 changed files with 93 additions and 93 deletions

View file

@ -356,9 +356,9 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
**数组中插入或删除元素效率低下**。假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点:
- **时间复杂度高** 数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。
- **丢失元素** 由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。
- **内存浪费** 我们一般会初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。
- **时间复杂度高**数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。
- **丢失元素**由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。
- **内存浪费**我们一般会初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。
![array_insert_remove_element](array.assets/array_insert_remove_element.png)

View file

@ -599,9 +599,9 @@ comments: true
为了帮助加深对列表的理解,我们在此提供一个列表的简易版本的实现。需要关注三个核心点:
- **初始容量** 选取一个合理的数组的初始容量 `initialCapacity` 。在本示例中,我们选择 10 作为初始容量。
- **数量记录** 需要声明一个变量 `size` ,用来记录列表当前有多少个元素,并随着元素插入与删除实时更新。根据此变量,可以定位列表的尾部,以及判断是否需要扩容。
- **扩容机制** 插入元素有可能导致超出列表容量,此时需要扩容列表,方法是建立一个更大的数组来替换当前数组。需要给定一个扩容倍数 `extendRatio` ,在本示例中,我们规定每次将数组扩容至之前的 2 倍。
- **初始容量**选取一个合理的数组的初始容量 `initialCapacity` 。在本示例中,我们选择 10 作为初始容量。
- **数量记录**需要声明一个变量 `size` ,用来记录列表当前有多少个元素,并随着元素插入与删除实时更新。根据此变量,可以定位列表的尾部,以及判断是否需要扩容。
- **扩容机制**插入元素有可能导致超出列表容量,此时需要扩容列表,方法是建立一个更大的数组来替换当前数组。需要给定一个扩容倍数 `extendRatio` ,在本示例中,我们规定每次将数组扩容至之前的 2 倍。
本示例是为了帮助读者对如何实现列表产生直观的认识。实际编程语言中,列表的实现远比以下代码复杂且标准,感兴趣的读者可以查阅源码学习。

View file

@ -12,8 +12,8 @@ comments: true
我们一般将逻辑结构分为「线性」和「非线性」两种。“线性”这个概念很直观,即表明数据在逻辑关系上是排成一条线的;而如果数据之间的逻辑关系是非线形的(例如是网状或树状的),那么就是非线性数据结构。
- **线性数据结构** 数组、链表、栈、队列、哈希表;
- **非线性数据结构** 树、图、堆、哈希表;
- **线性数据结构**数组、链表、栈、队列、哈希表;
- **非线性数据结构**树、图、堆、哈希表;
![classification_logic_structure](classification_of_data_structure.assets/classification_logic_structure.png)
@ -33,8 +33,8 @@ comments: true
**所有数据结构都是基于数组、或链表、或两者组合实现的**。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。
- **基于数组可实现** 栈、队列、堆、哈希表、矩阵、张量(维度 $\geq 3$ 的数组)等;
- **基于链表可实现** 栈、队列、堆、哈希表、树、图等;
- **基于数组可实现**栈、队列、堆、哈希表、矩阵、张量(维度 $\geq 3$ 的数组)等;
- **基于链表可实现**栈、队列、堆、哈希表、树、图等;
基于数组实现的数据结构也被称为「静态数据结构」,这意味着该数据结构在在被初始化后,长度不可变。相反地,基于链表实现的数据结构被称为「动态数据结构」,该数据结构在被初始化后,我们也可以在程序运行中修改其长度。

View file

@ -24,9 +24,9 @@ comments: true
在原始哈希表中,一个桶地址只能存储一个元素(即键值对)。**考虑将桶地址内的单个元素转变成一个链表,将所有冲突元素都存储在一个链表中**,此时哈希表操作方法为:
- **查询元素** 先将 key 输入到哈希函数得到桶地址(即访问链表头部),再遍历链表来确定对应的 value 。
- **添加元素** 先通过哈希函数访问链表头部,再将元素直接添加到链表头部即可。
- **删除元素** 同样先访问链表头部,再遍历链表查找对应元素,删除之即可。
- **查询元素**先将 key 输入到哈希函数得到桶地址(即访问链表头部),再遍历链表来确定对应的 value 。
- **添加元素**先通过哈希函数访问链表头部,再将元素直接添加到链表头部即可。
- **删除元素**同样先访问链表头部,再遍历链表查找对应元素,删除之即可。
(图)
@ -46,9 +46,9 @@ comments: true
「线性探测」使用固定步长的线性查找来解决哈希冲突。
**插入元素** 如果出现哈希冲突,则从冲突位置向后线性遍历(步长一般取 1 ),直到找到一个空位,则将元素插入到该空位中。
**插入元素**如果出现哈希冲突,则从冲突位置向后线性遍历(步长一般取 1 ),直到找到一个空位,则将元素插入到该空位中。
**查找元素** 若出现哈希冲突,则使用相同步长执行线性查找,会遇到两种情况:
**查找元素**若出现哈希冲突,则使用相同步长执行线性查找,会遇到两种情况:
1. 找到对应元素,返回 value 即可;
2. 若遇到空位,则说明查找键值对不在哈希表中;
@ -64,9 +64,9 @@ comments: true
顾名思义,「多次哈希」的思路是基于多个哈希函数 $f_1(x)$ , $f_2(x)$ , $f_3(x)$ , $\cdots$ 进行探测。
**插入元素** 若哈希函数 $f_1(x)$ 出现冲突,则尝试 $f_2(x)$ ,以此类推……直到找到空位后插入元素。
**插入元素**若哈希函数 $f_1(x)$ 出现冲突,则尝试 $f_2(x)$ ,以此类推……直到找到空位后插入元素。
**查找元素** 以相同的哈希函数顺序查找,存在两种情况:
**查找元素**以相同的哈希函数顺序查找,存在两种情况:
1. 找到目标元素,则返回之;
2. 到空位或已尝试所有哈希函数,说明哈希表中无此元素;

View file

@ -16,10 +16,10 @@ comments: true
除了哈希表之外,还可以使用以下数据结构来实现上述查询功能:
1. **无序数组** 每个元素为 `[学号, 姓名]`
2. **有序数组** `1.` 中的数组按照学号从小到大排序;
3. **链表** 每个结点的值为 `[学号, 姓名]`
4. **二叉搜索树** 每个结点的值为 `[学号, 姓名]` ,根据学号大小来构建树;
1. **无序数组**每个元素为 `[学号, 姓名]`
2. **有序数组**`1.` 中的数组按照学号从小到大排序;
3. **链表**每个结点的值为 `[学号, 姓名]`
4. **二叉搜索树**每个结点的值为 `[学号, 姓名]` ,根据学号大小来构建树;
使用上述方法,各项操作的时间复杂度如下表所示(在此不做赘述,详解可见 [二叉搜索树章节](https://www.hello-algo.com/chapter_tree/binary_search_tree/#_6))。无论是查找元素、还是增删元素,哈希表的时间复杂度都是 $O(1)$ ,全面胜出!

View file

@ -480,9 +480,9 @@ $$
## 复杂度分析
**时间复杂度 $O(\log n)$ ** 其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。
**时间复杂度 $O(\log n)$** 其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。
**空间复杂度 $O(1)$ ** 指针 `i` , `j` 使用常数大小空间。
**空间复杂度 $O(1)$** 指针 `i` , `j` 使用常数大小空间。
## 优点与缺点

View file

@ -193,9 +193,9 @@ comments: true
## 复杂度分析
**时间复杂度** $O(1)$ 哈希表的查找操作使用 $O(1)$ 时间。
**时间复杂度 $O(1)$** 哈希表的查找操作使用 $O(1)$ 时间。
**空间复杂度** $O(n)$ 其中 $n$ 为数组或链表长度。
**空间复杂度 $O(n)$** 其中 $n$ 为数组或链表长度。
## 优点与缺点

View file

@ -252,9 +252,9 @@ comments: true
## 复杂度分析
**时间复杂度 $O(n)$ ** 其中 $n$ 为数组或链表长度。
**时间复杂度 $O(n)$** 其中 $n$ 为数组或链表长度。
**空间复杂度 $O(1)$ ** 无需使用额外空间。
**空间复杂度 $O(1)$** 无需使用额外空间。
## 优点与缺点

View file

@ -220,15 +220,15 @@ comments: true
## 算法特性
**时间复杂度 $O(n^2)$ ** 各轮「冒泡」遍历的数组长度为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。
**时间复杂度 $O(n^2)$** 各轮「冒泡」遍历的数组长度为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。
**空间复杂度 $O(1)$ ** 指针 $i$ , $j$ 使用常数大小的额外空间。
**空间复杂度 $O(1)$** 指针 $i$ , $j$ 使用常数大小的额外空间。
**原地排序** 指针变量仅使用常数大小额外空间。
**原地排序**指针变量仅使用常数大小额外空间。
**稳定排序** 不交换相等元素。
**稳定排序**不交换相等元素。
**自适排序** 引入 `flag` 优化后(见下文),最佳时间复杂度为 $O(N)$ 。
**自适排序**引入 `flag` 优化后(见下文),最佳时间复杂度为 $O(N)$ 。
## 效率优化

View file

@ -183,15 +183,15 @@ comments: true
## 算法特性
**时间复杂度 $O(n^2)$ ** 最差情况下,各轮插入操作循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,使用 $O(n^2)$ 时间。
**时间复杂度 $O(n^2)$** 最差情况下,各轮插入操作循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,使用 $O(n^2)$ 时间。
**空间复杂度 $O(1)$ ** 指针 $i$ , $j$ 使用常数大小的额外空间。
**空间复杂度 $O(1)$** 指针 $i$ , $j$ 使用常数大小的额外空间。
**原地排序** 指针变量仅使用常数大小额外空间。
**原地排序**指针变量仅使用常数大小额外空间。
**稳定排序** 不交换相等元素。
**稳定排序**不交换相等元素。
**自适应排序** 最佳情况下,时间复杂度为 $O(n)$ 。
**自适应排序**最佳情况下,时间复杂度为 $O(n)$ 。
## 插入排序 vs 冒泡排序

View file

@ -6,8 +6,8 @@ comments: true
「归并排序 Merge Sort」是算法中“分治思想”的典型体现其有「划分」和「合并」两个阶段
1. **划分阶段** 通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;
2. **合并阶段** 划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;
1. **划分阶段**通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;
2. **合并阶段**划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;
![merge_sort_preview](merge_sort.assets/merge_sort_preview.png)
@ -56,8 +56,8 @@ comments: true
观察发现,归并排序的递归顺序就是二叉树的「后序遍历」。
- **后序遍历** 先递归左子树、再递归右子树、最后处理根结点。
- **归并排序** 先递归左子树、再递归右子树、最后处理合并。
- **后序遍历**先递归左子树、再递归右子树、最后处理根结点。
- **归并排序**先递归左子树、再递归右子树、最后处理合并。
=== "Java"
@ -406,11 +406,11 @@ comments: true
## 算法特性
- **时间复杂度 $O(n \log n)$ ** 划分形成高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,总体使用 $O(n \log n)$ 时间。
- **空间复杂度 $O(n)$ ** 需借助辅助数组实现合并,使用 $O(n)$ 大小的额外空间;递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间。
- **非原地排序** 辅助数组需要使用 $O(n)$ 额外空间。
- **稳定排序** 在合并时可保证相等元素的相对位置不变。
- **非自适应排序** 对于任意输入数据,归并排序的时间复杂度皆相同。
- **时间复杂度 $O(n \log n)$** 划分形成高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,总体使用 $O(n \log n)$ 时间。
- **空间复杂度 $O(n)$** 需借助辅助数组实现合并,使用 $O(n)$ 大小的额外空间;递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间。
- **非原地排序**辅助数组需要使用 $O(n)$ 额外空间。
- **稳定排序**在合并时可保证相等元素的相对位置不变。
- **非自适应排序**对于任意输入数据,归并排序的时间复杂度皆相同。
## 链表排序 *

View file

@ -235,9 +235,9 @@ comments: true
## 算法流程
1. 首先,对数组执行一次「哨兵划分」,得到待排序的 **左子数组****右子数组**
1. 首先,对数组执行一次「哨兵划分」,得到待排序的 **左子数组****右子数组**
2. 接下来,对 **左子数组****右子数组** 分别 **递归执行**「哨兵划分」……
3. 直至子数组长度为 1 时 **终止递归** ,即可完成对整个数组的排序。
3. 直至子数组长度为 1 时 **终止递归**,即可完成对整个数组的排序;
观察发现,快速排序和「二分查找」的原理类似,都是以对数阶的时间复杂度来缩小处理区间。
@ -373,25 +373,25 @@ comments: true
## 算法特性
**平均时间复杂度 $O(n \log n)$ ** 平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。
**平均时间复杂度 $O(n \log n)$** 平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。
**最差时间复杂度 $O(n^2)$ ** 最差情况下,哨兵划分操作将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。
**最差时间复杂度 $O(n^2)$** 最差情况下,哨兵划分操作将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。
**空间复杂度 $O(n)$ ** 输入数组完全倒序下,达到最差递归深度 $n$ 。
**空间复杂度 $O(n)$** 输入数组完全倒序下,达到最差递归深度 $n$ 。
**原地排序** 只在递归中使用 $O(\log n)$ 大小的栈帧空间。
**原地排序**只在递归中使用 $O(\log n)$ 大小的栈帧空间。
**非稳定排序** 哨兵划分操作可能改变相等元素的相对位置。
**非稳定排序**哨兵划分操作可能改变相等元素的相对位置。
**自适应排序** 最差情况下,时间复杂度劣化至 $O(n^2)$ 。
**自适应排序**最差情况下,时间复杂度劣化至 $O(n^2)$ 。
## 快排为什么快?
从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高**,这是因为:
- **出现最差情况的概率很低** 虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。
- **缓存使用效率高** 哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。
- **复杂度的常数系数低** 在提及的三种算法中,快速排序的 **比较**、**赋值**、**交换** 三种操作的总体数量最少(类似于「插入排序」快于「冒泡排序」的原因)。
- **出现最差情况的概率很低**虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。
- **缓存使用效率高**哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。
- **复杂度的常数系数低**在提及的三种算法中,快速排序的 **比较**、**赋值**、**交换** 三种操作的总体数量最少(类似于「插入排序」快于「冒泡排序」的原因)。
## 基准数优化

View file

@ -202,8 +202,8 @@ comments: true
给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根结点 < 右子树的性质插入操作分为两步
1. **查找插入位置** 与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环;
2. **在该位置插入结点** 初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置
1. **查找插入位置**与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环;
2. **在该位置插入结点**初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置
二叉搜索树不允许存在重复结点,否则将会违背其定义。因此若待插入结点在树中已经存在,则不执行插入,直接返回即可。
@ -442,15 +442,15 @@ comments: true
与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根结点 < 右子树的性质首先我们需要在二叉树中执行查找操作获取待删除结点接下来根据待删除结点的子结点数量删除操作需要分为三种情况
**待删除结点的子结点数量 $= 0$ **表明待删除结点是叶结点,直接删除即可。
**当待删除结点的子结点数量 $= 0$ 时**表明待删除结点是叶结点,直接删除即可。
![bst_remove_case1](binary_search_tree.assets/bst_remove_case1.png)
**待删除结点的子结点数量 $= 1$ **。将待删除结点替换为其子结点
**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可
![bst_remove_case2](binary_search_tree.assets/bst_remove_case2.png)
**待删除结点的子结点数量 $= 2$ **删除操作分为三步:
**当待删除结点的子结点数量 $= 2$ 时**删除操作分为三步:
1. 找到待删除结点在 **中序遍历序列** 中的下一个结点,记为 `nex`
2. 在树中递归删除结点 `nex`
@ -830,17 +830,17 @@ comments: true
假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:
- **查找元素** 由于数组是无序的,因此需要遍历数组来确定,使用 $O(n)$ 时间;
- **插入元素** 只需将元素添加至数组尾部即可,使用 $O(1)$ 时间;
- **删除元素** 先查找元素,使用 $O(n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间;
- **获取最小 / 最大元素** 需要遍历数组来确定,使用 $O(n)$ 时间;
- **查找元素**由于数组是无序的,因此需要遍历数组来确定,使用 $O(n)$ 时间;
- **插入元素**只需将元素添加至数组尾部即可,使用 $O(1)$ 时间;
- **删除元素**先查找元素,使用 $O(n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间;
- **获取最小 / 最大元素**需要遍历数组来确定,使用 $O(n)$ 时间;
为了得到先验信息,我们也可以预先将数组元素进行排序,得到一个「排序数组」,此时操作效率为:
- **查找元素** 由于数组已排序,可以使用二分查找,平均使用 $O(\log n)$ 时间;
- **插入元素** 先查找插入位置,使用 $O(\log n)$ 时间,再插入到指定位置,使用 $O(n)$ 时间;
- **删除元素** 先查找元素,使用 $O(\log n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间;
- **获取最小 / 最大元素** 数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间;
- **查找元素**由于数组已排序,可以使用二分查找,平均使用 $O(\log n)$ 时间;
- **插入元素**先查找插入位置,使用 $O(\log n)$ 时间,再插入到指定位置,使用 $O(n)$ 时间;
- **删除元素**先查找元素,使用 $O(\log n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间;
- **获取最小 / 最大元素**数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间;
观察发现,无序数组和有序数组中的各项操作的时间复杂度是“偏科”的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。