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

@@ -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 冒泡排序
@@ -199,7 +199,7 @@ comments: true
虽然「插入排序」和「冒泡排序」的时间复杂度皆为 $O(n^2)$ ,但实际运行速度却有很大差别,这是为什么呢?
回顾复杂度分析,两个方法的循环次数都是 $\frac{(n - 1) n}{2}$ 。但不同的是,「冒泡操作」是在做 **元素交换** ,需要借助一个临时变量实现,共 3 个单元操作;而「插入操作」是在做 **赋值** ,只需 1 个单元操作;因此,可以粗略估计出冒泡排序的计算开销约为插入排序的 3 倍。
回顾复杂度分析,两个方法的循环次数都是 $\frac{(n - 1) n}{2}$ 。但不同的是,「冒泡操作」是在做 **元素交换**,需要借助一个临时变量实现,共 3 个单元操作;而「插入操作」是在做 **赋值**,只需 1 个单元操作;因此,可以粗略估计出冒泡排序的计算开销约为插入排序的 3 倍。
插入排序运行速度快,并且具有原地、稳定、自适应的优点,因此很受欢迎。实际上,包括 Java 在内的许多编程语言的排序库函数的实现都用到了插入排序。库函数的大致思路:

View File

@@ -7,7 +7,7 @@ comments: true
「排序算法 Sorting Algorithm」使得列表中的所有元素按照从小到大的顺序排列。
- 待排序的列表的 **元素类型** 可以是整数、浮点数、字符、或字符串;
- 排序算法可以根据需要设定 **判断规则** ,例如数字大小、字符 ASCII 码顺序、自定义规则;
- 排序算法可以根据需要设定 **判断规则**,例如数字大小、字符 ASCII 码顺序、自定义规则;
![sorting_examples](intro_to_sort.assets/sorting_examples.png)
@@ -55,7 +55,7 @@ comments: true
- 「自适应排序」的时间复杂度受输入数据影响,即最佳 / 最差 / 平均时间复杂度不相等。
- 「非自适应排序」的时间复杂度恒定,与输入数据无关。
我们希望 **最差 = 平均** ,即不希望排序算法的运行效率在某些输入数据下发生劣化。
我们希望 **最差 = 平均**,即不希望排序算法的运行效率在某些输入数据下发生劣化。
### 比较类

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)
@@ -15,14 +15,14 @@ comments: true
## 算法流程
**「递归划分」** 从顶至底递归地 **将数组从中点切为两个子数组** ,直至长度为 1
**「递归划分」** 从顶至底递归地 **将数组从中点切为两个子数组**,直至长度为 1
1. 计算数组中点 `mid` ,递归划分左子数组(区间 `[left, mid]` )和右子数组(区间 `[mid + 1, right]`
2. 递归执行 `1.` 步骤,直至子数组区间长度为 1 时,终止递归划分;
**「回溯合并」** 从底至顶地将左子数组和右子数组合并为一个 **有序数组**
需要注意,由于从长度为 1 的子数组开始合并,所以 **每个子数组都是有序的** 。因此,合并任务本质是要 **将两个有序子数组合并为一个有序数组**
需要注意,由于从长度为 1 的子数组开始合并,所以 **每个子数组都是有序的**。因此,合并任务本质是要 **将两个有序子数组合并为一个有序数组**
=== "Step1"
![merge_sort_step1](merge_sort.assets/merge_sort_step1.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

@@ -6,13 +6,13 @@ comments: true
「快速排序 Quick Sort」是一种基于“分治思想”的排序算法速度很快、应用很广。
快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数** ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为:
快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数**,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为:
1. 以数组最左端元素作为基准数,初始化两个指针 `i` , `j` 指向数组两端;
2. 设置一个循环,每轮中使用 `i` / `j` 分别寻找首个比基准数大 / 小的元素,并交换此两元素;
3. 不断循环步骤 `2.` ,直至 `i` , `j` 相遇时跳出,最终把基准数交换至两个子数组的分界线;
「哨兵划分」执行完毕后,原数组被划分成两个部分,即 **左子数组****右子数组** ,且满足 **左子数组任意元素 < 基准数 < 右子数组任意元素**。因此,接下来我们只需要排序两个子数组即可。
「哨兵划分」执行完毕后,原数组被划分成两个部分,即 **左子数组****右子数组**,且满足 **左子数组任意元素 < 基准数 < 右子数组任意元素**。因此,接下来我们只需要排序两个子数组即可。
=== "Step 1"
![pivot_division_step1](quick_sort.assets/pivot_division_step1.png)
@@ -235,9 +235,9 @@ comments: true
## 算法流程
1. 首先,对数组执行一次「哨兵划分」,得到待排序的 **左子数组** 和 **右子数组**
1. 首先,对数组执行一次「哨兵划分」,得到待排序的 **左子数组** 和 **右子数组**
2. 接下来,对 **左子数组** 和 **右子数组** 分别 **递归执行**「哨兵划分」……
3. 直至子数组长度为 1 时 **终止递归** ,即可完成对整个数组的排序
3. 直至子数组长度为 1 时 **终止递归**,即可完成对整个数组的排序
观察发现,快速排序和「二分查找」的原理类似,都是以对数阶的时间复杂度来缩小处理区间。
@@ -373,31 +373,31 @@ 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)$ 的复杂度。
- **缓存使用效率高**哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。
- **复杂度的常数系数低**在提及的三种算法中,快速排序的 **比较**、**赋值**、**交换** 三种操作的总体数量最少(类似于「插入排序」快于「冒泡排序」的原因)。
## 基准数优化
**普通快速排序在某些输入下的时间效率变差**。举个极端例子,假设输入数组是完全倒序的,由于我们选取最左端元素为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,从而 **左子数组长度为 $n - 1$ 、右子数组长度为 $0$** 。这样进一步递归下去,**每轮哨兵划分后的右子数组长度都为 $0$** ,分治策略失效,快速排序退化为「冒泡排序」了。
**普通快速排序在某些输入下的时间效率变差**。举个极端例子,假设输入数组是完全倒序的,由于我们选取最左端元素为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,从而 **左子数组长度为 $n - 1$、右子数组长度为 $0$** 。这样进一步递归下去,**每轮哨兵划分后的右子数组长度都为 $0$** ,分治策略失效,快速排序退化为「冒泡排序」了。
为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数** 。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。
为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数**。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。
进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数“既不大也不小”的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。