Add sorting.md

This commit is contained in:
Sainnhe Park 2023-02-28 17:10:38 +08:00
parent 4d93e5a150
commit 5c03c9e3f2
2 changed files with 193 additions and 2 deletions

View File

@ -3,14 +3,14 @@
# 数组
- [总结](./array.md)
- [二分查找](./bin_search.md)
- [二分查找](./bin_search.md)
- [移除元素](./remove_elements.md)
- [长度最小的子数组](./minimum_size_subarray_sum.md)
- [三数相加](./three_sum.md)
# 链表
- [总结](./linked_list.md)
- [总结](./linked_list.md)
- [环形链表](./linked_list_cycle.md)
# 哈希表
@ -70,4 +70,5 @@
# 经典代码
- [排序算法](./sorting.md)
- [合并两个有序链表](./merge_two_sorted_linked_lists.md)

190
notes/src/sorting.md Normal file
View File

@ -0,0 +1,190 @@
# 排序算法
在这里规定:
- 数组是 `arr[0...k]`
- 排序是指从左到右从小到大排列
- 有序区是指数组中的一个子数组(子数组要求连续,子序列不要求连续),该子数组已排好序
- 无序区是指数组中的一个子数组,该子数组未排好序
## 插入排序
`arr[0...i]` 为有序区,`arr[i+1...k]` 为无序区,有序区长度一开始为 1。
每次排序将 `arr[i+1]` 插入到有序区中的合适位置。
插入到合适位置是指,插入进去后有序区后面部分的元素后移。
```cpp
void insertSort(std::vector<int> &v) {
int len = v.size();
if (len == 1) return;
// 有序区 [0...i],无序区 [i+1...len-1]
for (int i = 0; i < len - 1; ++i) {
// 找到 v[i+1] 在有序区中的位置
// 从后往前遍历有序区
int j = i;
while (v[i + 1] < v[j]) {
--j;
}
// v[i+1] 应该插入到 v[j+1] 的位置
// 先把原本的 v[j+1...i] 往后挪一位
int tmp = v[i + 1];
for (int x = i; j + 1 <= x; --x) {
v[x + 1] = v[x];
}
// 插入 v[i+1]
v[j + 1] = tmp;
}
}
```
时间复杂度:双重循环,所以为 **O(N^2)**
空间复杂度:占用内存随着排序总数 n 的增大而等比增大,所以为 **O(1)**
稳定性:不改变其它元素间的相对位置,所以**稳定**
## 冒泡排序
`arr[0...i]` 为无序区,`arr[i+1...k]` 为有序区,有序区长度一开始为 0。
每一次遍历无序区中的元素,将最大值放到无序区末尾。
具体是怎么放到末尾的呢?从后往前遍历无序区,比较相邻的两个元素,将大的那个元素往后挪。
```cpp
void bubbleSort(std::vector<int> &v) {
int len = v.size();
if (len == 1) return;
// 无序区 [0...i],有序区 [i+1...len-1]
for (int i = len - 1; i >= 0; --i) {
// 从前往后遍历无序区
for (int j = 0; j < i; ++j) {
// 对比相邻的两个元素,把大的那个往后移
if (v[j] > v[j + 1]) {
v[j] = v[j] ^ v[j + 1];
v[j + 1] = v[j] ^ v[j + 1];
v[j] = v[j] ^ v[j + 1];
}
}
}
}
```
时间复杂度:双重循环,所以为 **O(N^2)**
空间复杂度:占用内存随着排序总数 n 的增大而等比增大,所以为 **O(1)**
稳定性:不改变其它元素间的相对位置,所以**稳定**
## 选择排序
`arr[0...i]` 为无序区,`arr[i+1...k]` 为有序区,有序区长度一开始为 0。
每一次遍历无序区中的元素,将最大值放到无序区末尾。
具体是怎么放到末尾的呢?从后往前遍历无序区,找到最大值的下标,然后将这个值和无序区末尾元素交换。
```cpp
void selectionSort(std::vector<int> &v) {
int len = v.size();
if (len == 1) return;
int maxIndex = 0;
int tmp = v[0];
// 无序区 [0...i],有序区 [i+1...len-1]
for (int i = len - 1; i >= 0; --i) {
maxIndex = 0;
// 从前往后遍历无序区
for (int j = 0; j <= i; ++j) {
// 找到最大值的索引
if (v[j] > v[maxIndex]) maxIndex = j;
}
// 交换最大值和无序区末尾元素
tmp = v[i];
v[i] = v[maxIndex];
v[maxIndex] = tmp;
}
}
```
时间复杂度:双重循环,所以为 **O(N^2)**
空间复杂度:占用内存随着排序总数 n 的增大而等比增大,所以为 **O(1)**
稳定性:因为会与有序区末尾元素进行交换,改变了元素间的相对位置,所以**不稳定**
## 快速排序
选择一个“基准” (pivot),一般选择最左侧元素,比如 `pivot = arr[0]`
现在我们要做的是,将比 `pivot` 小的元素放到它的左侧,将比它大的元素放在右侧,排序完后就成了这样:
`arr[0...x]`, `pivot`, `arr[y...k]`
其中 `arr[0...x]` 中的每一个元素都比 `pivot` 小,`arr[y...k]` 中的每个元素都比 `pivot` 大。
但是 `arr[0...x]``arr[y...k]` 是无序的,怎么办呢?
递归调用快排,对 `arr[0...x]``arr[y...k]` 排序。
```cpp
void quickSort(std::vector<int> &v, int left, int right) {
if (left >= right) return;
int pivot = v[left];
int tmp = 0;
// 双指针
// i = left 从左到右
// j = right 从右到左
int i = left;
int j = right;
while (i < j) {
// 移动 j
// 从右往左找到第一个比 pivot 小的 v[j]
// 此时 v[j+1...k] 都比 pivot 大
while (i < j && v[j] >= pivot) --j;
// 把这个数覆盖到左侧 v[i]
if (i < j) {
v[i] = v[j];
++i;
}
// 移动 i
// 从左往右找到第一个比 pivot 大的 v[i]
// 此时 v[0...i-1] 都比 pivot 小
while (i < j && v[i] <= pivot) ++i;
// 把这个数覆盖到右侧 v[j]
if (i < j) {
v[j] = v[i];
--j;
}
}
// 出循环后 i == j
// 此时左侧和右侧都已经处理完毕
// 接下来把 pivot 放到 v[i] 即可
v[i] = pivot;
// 递归处理左侧
quickSort(v, left, i - 1);
// 递归处理右侧
quickSort(v, i + 1, right);
}
```
时间复杂度:最坏情况下为**O(N^2)**,平均时间复杂度为**O(N·logN)**
空间复杂度: 和时间复杂度相关,每次递归需要的空间是固定的,总体空间复杂度即为递归层数,因此平均/最好空间复杂度为 **O(logN)**,最坏空间复杂度为 **O(N)**
稳定性:**不稳定**
## 桶排序
[一看就懂的原理](https://www.cs.usfca.edu/~galles/visualization/BucketSort.html)
时间复杂度:取决于桶数量和数据分散程度,最好情况下可以达到**O(N)**,最坏情况下为**O(N^2)**,平均**O(N+K)**
空间复杂度:最坏**O(N·K)**
稳定性:**稳定**