详解十大排序算法!

以下所有排序算法的示例代码皆为:

引言

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。

十个排序算法的特性 - 1

对上面图中的部分参数进行一个讲解:

参数 说明
$n$ 数据的规模
$k$ “桶”的个数
$in-place$ 占用的常数内存,不占用额外内存
$out-place$ 占用的额外内存
稳定性 排序后的 2 个相等键值的顺序和排序之前它们的顺序相同
十个排序算法的特性 - 2

冒泡排序

冒泡排序是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较相邻的两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

算法步骤

比较相邻的元素。如果第一个比第二个大,就交换他们两个。

对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

冒泡排序 动图演示

最坏时间复杂度情况

输入数据为反序的时候。时间复杂度为 $O(n^2)$(但是和平均时间复杂度一样)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Bubble Sort
class Solution {
public:
/**
* 将给定数组排序
* @param arr int整型vector 待排序的数组
* @return int整型vector
*/
vector<int> MyBubbleSort(vector<int>& arr) {
int len = arr.size();
for (int i = 0; i < len - 1; i++) {
for (int j = i; j < len; j++) {
if (arr[i] > arr[j]) {
swap(arr[i], arr[j]);
}
}
}
return arr;
}
};

选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 $O(n^2)$ 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

算法步骤

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

选择排序 动图演示

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Selection Sort
class Solution {
public:
/**
* 将给定数组排序
* @param arr int整型vector 待排序的数组
* @return int整型vector
*/
vector<int> MySelectionSort(vector<int>& arr) {
int len = arr.size(), min_value_position = 0;
for (int i = 0; i < len - 1; i++) {
min_value_position = i;
for (int j = i + 1; j < len; j++) {
// 寻找未排序部分中最小值所在的位置
if (arr[min_value_position] > arr[j])
min_value_position = j;
}
swap(arr[i], arr[min_value_position]);
}
return arr;
}
};

插入排序

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入

算法步骤

初始状态(即未开始排序的时候)下,将第一待排序序列第一个元素看做一个有序序列,第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)。

插入排序 动图演示

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Insertion Sort

class Solution {
public:
/**
* 将给定数组排序
* @param arr int整型vector 待排序的数组
* @return int整型vector
*/
vector<int> MyInsertionSort(vector<int>& arr) {
int len = arr.size();

for (int i = 1; i < len; i++) {
int current_value = arr[i], cmp_index = i - 1;
while (cmp_index >= 0 && arr[cmp_index] > current_value) {
arr[cmp_index + 1] = arr[cmp_index];
cmp_index--;
}
arr[cmp_index + 1] = current_value;
}

return arr;
}
};

希尔排序

希尔排序的实质就是分组插入排序,该方法又称递减增量排序算法。希尔排序是非稳定的排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  2. 插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

算法步骤

先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序。

待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

这里需要注意的是,我们是将所有间隔为一个“增量”的元素归并为一个子序列,而非是连续长度为“间隔”的数字归并为子序列。

假设有一组 ${9, 1, 2, 5, 7, 4, 8, 6, 3, 5}$ 无序序列:

希尔排序 示例

这里对上面的示例进行一个讲解:

  • 第一趟排序

    设 $gap1 = N / 2 = 5$,即相隔距离为 $5$ 的元素组成一组,可以分为 $5$ 组。接下来,按照直接插入排序的方法对每个组进行排序。

  • 第二趟排序
    将上次的 $gap$ 缩小一半,即 $gap2 = gap1 / 2 = 2$ (取整数)。这样每相隔距离为 $2$ 的元素组成一组,可以分为 $2$ 组。按照直接插入排序的方法对每个组进行排序。

  • 第三趟排序
    再次把 $gap$ 缩小一半,即 $gap3 = gap2 / 2 = 1$。 这样相隔距离为 $1$ 的元素组成一组,即只有一组。按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。

注:需要注意一下的是,图中有两个相等数值的元素 $5$ 和 $5$。我们可以清楚的看到,在排序过程中,两个元素位置交换了。也即希尔排序是不稳定的排序算法。

时间复杂度

  • 平均时间复杂度:$O(nlog_2n)$
  • 最坏时间复杂度:当序列为最终排序结果的反序时,时间复杂度则会是 $O(n^2)$

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
/**
* 将给定数组排序
* @param arr int整型vector 待排序的数组
* @return int整型vector
*/
vector<int> MyShellSort(vector<int>& arr) {
int len = arr.size();
// 计算 gap,每次都是前一次的 1/2 倍
for (int gap = len >> 1; gap > 0; gap >>= 1) {
for (int i = gap; i < len; i++) {
// 代码参考插入排序,需要注意 cmp_index 的取值变化
int current_value = arr[i], cmp_index = i - gap;
while (cmp_index >= 0 && arr[cmp_index] > current_value) {
arr[cmp_index + gap] = arr[cmp_index];
cmp_index -= gap;
}
arr[cmp_index + gap] = current_value;
}
}
return arr;
}
};

归并排序

归并排序

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <vector>
class Solution {
public:
/**
* 将给定数组排序
* @param arr int整型vector 待排序的数组
* @return int整型vector
*/
vector<int> MySort(vector<int>& arr) {
// 调用归并排序算法对数组进行排序
mergeSort(arr, 0, arr.size() - 1);
return arr;
}

void mergeSort(vector<int>& arr, int left, int right) {
// 如果左边界大于等于右边界,说明已经分到最小区间,直接返回
if (left >= right) return;
// 计算中间位置,将数组分为两部分
int mid = left + (right - left) / 2;
// 对左半部分进行归并排序
mergeSort(arr, left, mid);
// 对右半部分进行归并排序
mergeSort(arr, mid + 1, right);
// 合并两个有序的子区间
mergeIntervals(arr, left, mid, right);
}

void mergeIntervals(vector<int>& arr, int left, int mid, int right) {
// 创建一个临时数组,用于存放合并后的有序元素
vector<int> tempArr(right - left + 2);
// 定义两个指针,分别指向左右两个子区间的起始位置
int low = left, height = mid + 1, count = 0;
// 当两个指针都没有越界时,比较它们所指向的元素,将较小的元素放入临时数组,并移动相应的指针
while (low <= mid && height <= right) {
if (arr[low] < arr[height]) {
tempArr[++count] = arr[low];
low++;
} else {
tempArr[++count] = arr[height];
height++;
}
}

// 当左边的指针越界时,将右边剩余的元素放入临时数组
while (low <= mid) {
tempArr[++count] = arr[low];
low++;
}

// 当右边的指针越界时,将左边剩余的元素放入临时数组
while (height <= right) {
tempArr[++count] = arr[height];
height++;
}

// 将临时数组中的元素复制回原数组对应的位置
for (int i = left; i <= right; i++) {
arr[i] = tempArr[i - left + 1];
}
}
};

参考资料


详解十大排序算法!
https://luoyuy.top/posts/69698757e59a/
作者
LuoYu-Ying
发布于
2023年3月13日
许可协议