Skip to content

Commit

Permalink
Add merge sort, and sorting algorithm.
Browse files Browse the repository at this point in the history
  • Loading branch information
krahets committed Nov 23, 2022
1 parent 4290026 commit 0a52e53
Show file tree
Hide file tree
Showing 20 changed files with 155 additions and 79 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
# mkdocs files
site/
.cache/
codes/python
codes/cpp
codes/scripts
docs/overrides/
12 changes: 8 additions & 4 deletions codes/java/chapter_sorting/merge_sort.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ public class merge_sort {
* 右子数组区间 [mid + 1, right]
*/
static void merge(int[] nums, int left, int mid, int right) {
int[] tmp = Arrays.copyOfRange(nums, left, right + 1); // 初始化辅助数组
int leftStart = left - left, leftEnd = mid - left, // 左子数组的起始索引和结束索引
rightStart = mid + 1 - left, rightEnd = right - left; // 右子数组的起始索引和结束索引
int i = leftStart, j = rightStart; // i,j 分别指向左子数组、右子数组的首元素
// 初始化辅助数组
int[] tmp = Arrays.copyOfRange(nums, left, right + 1);
// 左子数组的起始索引和结束索引
int leftStart = left - left, leftEnd = mid - left;
// 右子数组的起始索引和结束索引
int rightStart = mid + 1 - left, rightEnd = right - left;
// i, j 分别指向左子数组、右子数组的首元素
int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions docs/chapter_sorting/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
comments: true
---

# 排序算法

「排序算法 Sorting Algorithm」使得列表中的所有元素按照从小到大的顺序排列。

- 待排序的列表的 **元素类型** 可以是整数、浮点数、字符、或字符串;
- 排序算法可以根据需要设定 **判断规则** ,例如数字大小、字符 ASCII 码顺序、自定义规则;

![sorting_examples](index.assets/sorting_examples.png)

<p align="center"> Fig. 排序中的不同元素类型和判断规则 </p>

## 评价维度

排序算法主要可根据 **稳定性 、就地性 、自适应性 、比较类** 来分类。

### 稳定性

- 「稳定排序」在完成排序后,**不改变** 相等元素在数组中的相对顺序。
- 「非稳定排序」在完成排序后,相等素在数组中的相对位置 **可能被改变**

假设我们有一个存储学生信息当表格,第 1, 2 列粉笔是姓名和年龄。那么在以下示例中,「非稳定排序」会导致输入数据的有序性丢失。因此「稳定排序」是很好的特性,**在多级排序中是必须的**

```shell
# 输入数据是按照姓名排序好的
# (name, age)
('A', 19)
('B', 18)
('C', 21)
('D', 19)
('E', 23)

# 假设使用非稳定排序算法按年龄排序列表,
# 结果中 ('D', 19) 和 ('A', 19) 的相对位置改变,
# 输入数据按姓名排序的性质丢失
('B', 18)
('D', 19)
('A', 19)
('C', 21)
('E', 23)
```

### 就地性

- 「原地排序」无需辅助数据,不使用额外空间;
- 「非原地排序」需要借助辅助数据,使用额外空间;

「原地排序」不使用额外空间,可以节约内存;并且一般情况下,由于数据操作减少,原地排序的运行效率也更高。

### 自适应性

- 「自适应排序」的时间复杂度受输入数据影响,即最佳 / 最差 / 平均时间复杂度不相等。
- 「非自适应排序」的时间复杂度恒定,与输入数据无关。

我们希望 **最差 = 平均** ,即不希望排序算法的运行效率在某些输入数据下发生劣化。

### 比较类

- 「比较类排序」基于元素之间的比较算子(小于、相等、大于)来决定元素的相对顺序。
- 「非比较类排序」不基于元素之间的比较算子来决定元素的相对顺序。

「比较类排序」的时间复杂度最优为 $O(n \log n)$ ;而「非比较类排序」可以达到 $O(n)$ 的时间复杂度,但通用性较差。

## 理想排序算法

- **运行地快**,即时间复杂度低;
- **稳定排序**,即排序后相等元素的相对位置不变化;
- **原地排序**,即运行中不使用额外的辅助空间;
- **正向自适应性**,即算法的运行效率不会在某些输入数据下发生劣化;

然而,**没有排序算法同时具备以上所有特性**。排序算法的选型使用取决于具体的列表类型、列表长度、元素分布等因素。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 60 additions & 20 deletions docs/chapter_sorting/merge_sort.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,59 @@ comments: true

「归并排序 Merge Sort」是算法中 “分治思想” 的典型体现,其有「划分」和「合并」两个阶段:

1. **划分** 不断递归地 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;
1. **划分阶段** 通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;

2. **合并** 划分到子数组长度为 1 时,开始向上合并,不断将 ** / 右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;
2. **合并阶段** 划分到子数组长度为 1 时,开始向上合并,不断将 **右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;

(图)
![merge_sort_preview](merge_sort.assets/merge_sort_preview.png)

<p align="center"> Fig. 归并排序两阶段:划分与合并 </p>

## 算法流程

**递归划分** 从顶至底递归地 **将数组从中点切为两个子数组** ,直至长度为 1 ;
**递归划分** 从顶至底递归地 **将数组从中点切为两个子数组** ,直至长度为 1 ;

1. 计算数组中点 `mid` ,递归划分左子数组(区间 `[left, mid]` )和右子数组(区间 `[mid + 1, right]` );
2. 递归执行 `1.` 步骤,直至子数组区间长度为 1 时,终止递归划分;

**回溯合并** 从底至顶将左子数组和右子数组合并为一个 **有序数组** ;由于是从长度为 1 的子数组开始合并的,因此 **每个子数组也是有序的** ,因此合并任务本质是要 **将两个有序子数组合并为一个有序数组**
**回溯合并** 从底至顶地将左子数组和右子数组合并为一个 **有序数组**

1. 初始化一个辅助数组 `tmp` 暂存待合并区间 `[left, right]` 内的元素,后序通过覆盖原数组 `nums` 的元素来实现合并;
2. 初始化指针 `i` , `j` , `k` 分别指向左子数组、右子数组、原数组的首元素;
3. 循环判断 `tmp[i]``tmp[j]` 的大小,将较小的先覆盖至 `nums[k]` ,指针 `i` , `j` 根据判断结果交替前进(指针 `k` 也前进),直至两个子数组都遍历完,即可完成合并。
需要注意,由于从长度为 1 的子数组开始合并,所以 **每个子数组都是有序的** 。因此,合并任务本质是要 **将两个有序子数组合并为一个有序数组**

合并代码的实现主要难点:
=== "Step1"
![merge_sort_step1](merge_sort.assets/merge_sort_step1.png)

- **`nums` 的待合并区间为 `[left, right]`** ,而由于 `tmp` 只复制了 `nums` 该区间元素,因此 **`tmp` 对应区间为 `[0, right - left]`** 。以下代码中的 `leftStart` , `leftEnd` , `rightStart` , `rightEnd` , `i` , `j` 都是根据 `tmp` 定义的,而 `k` 是根据 `nums` 定义的。
- 判断 `tmp[i]``tmp[j]` 的大小的操作中,还 **需考虑当子数组遍历完成后的索引越界问题**,即 `i > leftEnd``j > rightEnd` 的情况,索引越界的优先级是最高的,例如如果左子数组已经被合并完了,那么不用继续判断,直接合并右子数组元素即可。
=== "Step2"
![merge_sort_step2](merge_sort.assets/merge_sort_step2.png)

=== "Step3"
![merge_sort_step3](merge_sort.assets/merge_sort_step3.png)

=== "Step4"
![merge_sort_step4](merge_sort.assets/merge_sort_step4.png)

=== "Step5"
![merge_sort_step5](merge_sort.assets/merge_sort_step5.png)

=== "Step6"
![merge_sort_step6](merge_sort.assets/merge_sort_step6.png)

=== "Step7"
![merge_sort_step7](merge_sort.assets/merge_sort_step7.png)

=== "Step8"
![merge_sort_step8](merge_sort.assets/merge_sort_step8.png)

=== "Step9"
![merge_sort_step9](merge_sort.assets/merge_sort_step9.png)

(动画)
=== "Step10"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png)

观察发现,归并排序的递归顺序就是二叉树的「后序遍历」。

- **后序遍历:** 先递归左子树、再递归右子树、最后处理根结点。
- **归并排序:** 先递归左子树、再递归右子树、最后处理合并。

=== "Java"

Expand All @@ -41,10 +69,14 @@ comments: true
* 右子数组区间 [mid + 1, right]
*/
void merge(int[] nums, int left, int mid, int right) {
int[] tmp = Arrays.copyOfRange(nums, left, right + 1); // 初始化辅助数组
int leftStart = left - left, leftEnd = mid - left, // 左子数组的起始索引和结束索引
rightStart = mid + 1 - left, rightEnd = right - left; // 右子数组的起始索引和结束索引
int i = leftStart, j = rightStart; // i,j 分别指向左子数组、右子数组的首元素
// 初始化辅助数组
int[] tmp = Arrays.copyOfRange(nums, left, right + 1);
// 左子数组的起始索引和结束索引
int leftStart = left - left, leftEnd = mid - left;
// 右子数组的起始索引和结束索引
int rightStart = mid + 1 - left, rightEnd = right - left;
// i, j 分别指向左子数组、右子数组的首元素
int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++
Expand Down Expand Up @@ -72,6 +104,17 @@ comments: true
}
```

下面重点解释一下合并方法 `merge()` 的流程:

1. 初始化一个辅助数组 `tmp` 暂存待合并区间 `[left, right]` 内的元素,后续通过覆盖原数组 `nums` 的元素来实现合并;
2. 初始化指针 `i` , `j` , `k` 分别指向左子数组、右子数组、原数组的首元素;
3. 循环判断 `tmp[i]``tmp[j]` 的大小,将较小的先覆盖至 `nums[k]` ,指针 `i` , `j` 根据判断结果交替前进(指针 `k` 也前进),直至两个子数组都遍历完,即可完成合并。

合并方法 `merge()` 代码中的主要难点:

- `nums` 的待合并区间为 `[left, right]` ,而因为 `tmp` 只复制了 `nums` 该区间元素,所以 `tmp` 对应区间为 `[0, right - left]`**需要特别注意代码中各个变量的含义**
- 判断 `tmp[i]``tmp[j]` 的大小的操作中,还 **需考虑当子数组遍历完成后的索引越界问题**,即 `i > leftEnd``j > rightEnd` 的情况,索引越界的优先级是最高的,例如如果左子数组已经被合并完了,那么不用继续判断,直接合并右子数组元素即可。

## 算法特性

- **时间复杂度 $O(n \log n)$ :** 划分形成高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,总体使用 $O(n \log n)$ 时间。
Expand All @@ -87,7 +130,4 @@ comments: true
- 由于链表可仅通过改变指针来实现结点增删,因此 “将两个短有序链表合并为一个长有序链表” 无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp`
- 通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间;

!!! quote

详情参考:[148. 排序链表](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/)

> 详情参考:[<u>148. 排序链表</u>](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/)
6 changes: 6 additions & 0 deletions docs/chapter_sorting/summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
comments: true
---

# 小结

50 changes: 0 additions & 50 deletions docs/overrides/partials/comments.html

This file was deleted.

4 changes: 2 additions & 2 deletions docs/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
--md-accent-fg-color: #999;

--md-typeset-color: #1D1D20;
--md-typeset-a-color: #2CA44F;
--md-typeset-a-color: #2AA996;
}

[data-md-color-scheme="slate"] {
Expand All @@ -19,7 +19,7 @@
--md-accent-fg-color: #999;

--md-typeset-color: #FEFEFE;
--md-typeset-a-color: #31BC5A;
--md-typeset-a-color: #21C8B8;
}

/* Center Markdown Tables (requires md_in_html extension) */
Expand Down
4 changes: 3 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ markdown_extensions:
- pymdownx.caret
- pymdownx.details
# - pymdownx.emoji:
# emoji_generator: !!python/name:materialx.emoji.to_svg
# emoji_index: !!python/name:materialx.emoji.twemoji
# emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
Expand Down Expand Up @@ -156,9 +156,11 @@ nav:
- 哈希查找: chapter_searching/hashing_search.md
- 小结: chapter_searching/summary.md
- 排序算法:
- chapter_sorting/index.md
- 冒泡排序: chapter_sorting/bubble_sort.md
- 插入排序: chapter_sorting/insertion_sort.md
- 快速排序: chapter_sorting/quick_sort.md
- 归并排序: chapter_sorting/merge_sort.md
- 小结: chapter_sorting/summary.md
- 参考文献:
- chapter_reference/index.md

0 comments on commit 0a52e53

Please sign in to comment.