Remove the spaces between “ ” and 中文 aside.

This commit is contained in:
Yudong Jin
2022-12-21 01:37:58 +08:00
parent 7283bbaf6f
commit f3ef226874
31 changed files with 126 additions and 108 deletions

View File

@@ -6,7 +6,7 @@ comments: true
「冒泡排序 Bubble Sort」是一种最基础的排序算法非常适合作为第一个学习的排序算法。顾名思义「冒泡」是该算法的核心操作。
!!! question "为什么叫 “冒泡”"
!!! question "为什么叫“冒泡”"
在水中,越大的泡泡浮力越大,所以最大的泡泡会最先浮到水面。

View File

@@ -6,7 +6,7 @@ comments: true
「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。
「插入操作」的思想:选定数组的某个元素为基准数 `base` ,将 `base` 与其左边的元素依次对比大小,并 “插入” 到正确位置。
「插入操作」的思想:选定数组的某个元素为基准数 `base` ,将 `base` 与其左边的元素依次对比大小,并“插入”到正确位置。
然而,由于数组在内存中的存储方式是连续的,我们无法直接把 `base` 插入到目标位置,而是需要将从目标位置到 `base` 之间的所有元素向右移动一位(本质上是一次数组插入操作)。

View File

@@ -4,7 +4,7 @@ comments: true
# 归并排序
「归并排序 Merge Sort」是算法中 “分治思想” 的典型体现,其有「划分」和「合并」两个阶段:
「归并排序 Merge Sort」是算法中“分治思想”的典型体现其有「划分」和「合并」两个阶段
1. **划分阶段:** 通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;
2. **合并阶段:** 划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;
@@ -78,13 +78,13 @@ comments: true
int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
// 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd)
nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
// 否则,若“右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
// 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else
nums[k] = tmp[j++];
}
@@ -122,13 +122,13 @@ comments: true
int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
// 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd)
nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
// 否则,若“右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
// 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else
nums[k] = tmp[j++];
}
@@ -166,15 +166,15 @@ comments: true
i, j = left_start, right_start
# 通过覆盖原数组 nums 来合并左子数组和右子数组
for k in range(left, right + 1):
# 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
# 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if i > left_end:
nums[k] = tmp[j]
j += 1
# 否则,若 “右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
# 否则,若“右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
elif j > right_end or tmp[i] <= tmp[j]:
nums[k] = tmp[i]
i += 1
# 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
# 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else:
nums[k] = tmp[j]
j += 1
@@ -214,15 +214,15 @@ comments: true
i, j := left_start, right_start
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for k := left; k <= right; k++ {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
// 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if i > left_end {
nums[k] = tmp[j]
j++
// 否则,若 “右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
// 否则,若“右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
} else if j > right_end || tmp[i] <= tmp[j] {
nums[k] = tmp[i]
i++
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
// 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else {
nums[k] = tmp[j]
j++
@@ -264,13 +264,13 @@ comments: true
let i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (let k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
// 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) {
nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
// 否则,若“右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
} else if (j > rightEnd || tmp[i] <= tmp[j]) {
nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
// 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else {
nums[k] = tmp[j++];
}
@@ -309,13 +309,13 @@ comments: true
let i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (let k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
// 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) {
nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
// 否则,若“右子数组已全部合并完”“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
} else if (j > rightEnd || tmp[i] <= tmp[j]) {
nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
// 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else {
nums[k] = tmp[j++];
}
@@ -370,7 +370,7 @@ comments: true
归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为:
- 由于链表可仅通过改变指针来实现结点增删,因此 “将两个短有序链表合并为一个长有序链表” 无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp`
- 由于链表可仅通过改变指针来实现结点增删,因此“将两个短有序链表合并为一个长有序链表”无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp`
- 通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间;
> 详情参考:[148. 排序链表](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/)

View File

@@ -4,7 +4,7 @@ comments: true
# 快速排序
「快速排序 Quick Sort」是一种基于 “分治思想” 的排序算法,速度很快、应用很广。
「快速排序 Quick Sort」是一种基于“分治思想”的排序算法速度很快、应用很广。
快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数** ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为:
@@ -339,7 +339,7 @@ comments: true
## 快排为什么快?
从命名能够看出,快速排序在效率方面一定 “有两把刷子” 。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为:
从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为:
- **出现最差情况的概率很低:** 虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。
- **缓存使用效率高:** 哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。
@@ -351,7 +351,7 @@ comments: true
为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数** 。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。
进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数 “既不大也不小” 的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。
进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数“既不大也不小”的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。
=== "Java"