排序算法

喜马拉雅山 -- 中国

排序算法

1. 冒泡排序

比较相邻元素,第一个比第二个大,就交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>

using namespace std;
template<typename T>
void bubble_sort(vector<T>& vec) {
int n = vec.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (vec[j] > vec[j + 1]) {
swap(vec[j], vec[j + 1]);
}
}
}
}
int main() {
vector<int> vec{1,2,5,8,9,6,4,5,2,1,4,4,1,2};
for (const auto& v : vec) cout << v << " ";
cout << endl;
bubble_sort(vec);
for (const auto& v : vec) cout << v << " ";
cout << endl;
return 0;
}

在 Linux 下编译的时候需要添加编译选项 -std=c++11

最优的情况下也就是输入有序:$O(n)$

最差的情况下也就是输入逆序:$O(n^2)$

无额外空间需求:$O(1)$

稳定

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
25
26
27
28
#include <iostream>
#include <vector>

using namespace std;

template <typename T>
void selectionSort(vector<T>& vec) {
int n = vec.size();
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (vec[j] < vec[minIdx]) {
minIdx = j;
}
}
swap(vec[minIdx], vec[i]);
}
}

int main() {
vector<int> vec{1,2,4,5,3,6,2,8,1,9};
for (const auto& v : vec) cout << v << " ";
cout << endl;
selectionSort(vec);
for (const auto& v : vec) cout << v << " ";
cout << endl;
return 0;
}

每次操作 $n - 1 + n - 2 + … + 1 = ((1 + n - 1) * n - 1) / 2$

时间复杂度:$O(n^2)$

空间复杂度:$O (1)$

不稳定:例子: $80_1, 80_2, 70 --> 70, 80_2, 80_1$

3. 插入排序

算法描述:斗地主摸牌插入手牌

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

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

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
#include <iostream>
#include <vector>
using namespace std;

template<typename T>
void insertSort(vector<T>& vec) {
int n = vec.size();
for (int i = 1; i < n; i++) {
T key = vec[i];
int j = i;
while (j > 0 && vec[j - 1] > key) {
vec[j] = vec[j - 1];
j--;
}
vec[j] = key;
}
}
int main() {
vector<int> vec{1,5,6,2,10,8,1,6,2,7};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
insertSort(vec);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

最优的情况下也就是输入有序:$O(n)$

最差的情况下也就是输入逆序:$O(n^2)$

无额外空间需求:$O(1)$

稳定

4. 希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

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

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

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

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
#include <iostream>
#include <vector>
using namespace std;

template<typename T>
void shellSort(vector<T>& vec) {
int n = vec.size();
for (int step = n / 2; step > 0; step /= 2) {
for (int i = step; i < n; i++) {
T key = vec[i];
int j = i;
while (j >= step && vec[j - step] > key) {
vec[j] = vec[j - step];
j -= step;
}
vec[j] = key;
}
}
}
int main() {
vector<int> vec{1,2,5,3,6,2,1,4,2,8,9};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
shellSort(vec);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

时间复杂度:$O(nlog_2n)$,看 step

空间复杂度:$O(1)$

不稳定:例子: $80_1, 80_2, 80_3, 70 --> 70, 80_1, 80_3, 80_2$

5. 归并排序

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。
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
#include <iostream>
#include <vector>
using namespace std;
// 声明
template <typename T>
void mergeSortRecur(vector<T>& vec, vector<T>& temp, int left, int right);

template <typename T>
void mergeSort(vector<T>& vec) {
int n = vec.size();
vector<T>temp(n);
mergeSortRecur(vec, temp, 0, n - 1);
}
template <typename T>
void mergeSortRecur(vector<T>& vec, vector<T>& temp, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSortRecur(vec, temp, left, mid);
mergeSortRecur(vec, temp, mid + 1, right);
int i = left;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
temp[k++] = vec[i] <= vec[j] ? vec[i++] : vec[j++];
}
while (i <= mid) temp[k++] = vec[i++];
while (j <= right) temp[k++] = vec[j++];
for (int l = 0; l < k; l++) vec[left + l] = temp[l];
}
}
int main() {
vector<int> vec{1,5,2,3,8,4,3,9,1,2,6};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
mergeSort(vec);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

时间复杂度:最好,最坏,平均:$O(nlog_2n)$

空间复杂度:$O(n)$

稳定

6. 快速排序

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
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
#include<iostream>
#include<vector>
using namespace std;

template<typename T>
int partition(vector<T>& vec, int left, int right);

template<typename T>
void quickSort(vector<T>& vec, int left, int right) {
if (left >= right) return;
int pivot = partition(vec, left, right);
quickSort(vec, left, pivot - 1);
quickSort(vec, pivot + 1, right);
}
template<typename T>
int partition(vector<T>& vec, int left, int right) {
// 基准为最左侧值
T pivot = vec[left];
while (left < right) {
while (left < right && vec[right] >= pivot) --right;
vec[left] = vec[right];
while (left < right && vec[left] <= pivot) ++left;
vec[right] = vec[left];
}
vec[left] = pivot;
return left;
}
int main() {
vector<int> vec{1,5,2,3,8,4,3,9,1,2,6};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
int right = vec.size() - 1;
quickSort(vec, 0, right);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

时间复杂度:最坏情况:,有序情况,$O(n^2)$;平摊期望时间:$O(nlog_2n)$ 且隐含的常数因子很小,对于绝大多数顺序性较弱的随机序列,优于归并排序。

空间复杂度:每次递归传参 left,和 right,平均递归次数是 $o(log_2n)$ 次,所以平均空间复杂度是$o(log_2n)$ ,递归栈的深度。最坏的情况是$o(n)$ (初始是逆序的情况)

不稳定:例子 $80_1, 80_2, 80_3, 70 – > 70, 80_2, 80_3, 80_1$ (基准最左边)

快速排序的优化

快速排序的5种优化方法

7. 堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 $O(nlog_2n)$。

算法步骤:

  1. 创建一个堆 $H[0……n-1]$;
  2. 把堆首(最大值)和堆尾互换;
  3. 把堆的尺寸缩小 1,并调用 shift_down(0)​ ,目的是把新的数组顶端数据调整到相应位置;
  4. 重复步骤 2,直到堆的尺寸为 1
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
#include <iostream>
#include <vector>

using namespace std;

template <typename T>
void max_heap(vector<T>& vec, int start, int end) {
// 记录父节点索引
int parent = start;
// 记录子节点索引开始位置
int son = 2 * parent + 1;
while (son <= end) {
// 选择两个孩子节点中,最大的一个孩子,记录其索引
if (son + 1 <= end && vec[son] < vec[son + 1]) {
son++;
}
if (vec[parent] >= vec[son]) {
// 如果父节点已经大于所有子节点了,则直接返回
return ;
} else {
// 否则交换父子节点
swap(vec[parent], vec[son]);
// 父亲索引改为其子节点索引
parent = son;
// 其子节点索引在指向孙节点继续相同的比较
son = parent * 2 + 1;
}

}
}
template <typename T>
void heapSort(vector<T>& vec) {
int len = vec.size();
// 初始化调整:从最后一个父节点找起
for (int i = len / 2 - 1; i >= 0; i--) {
max_heap(vec, i, len - 1);
}// 获得最大堆

// 先将第一个元素和已经排好的元素前一位做交换,
// 再重新调整(刚调整的元素之前的元素),知道排序完毕
// 注:vec[0] 为堆顶,即为最大值

for (int i = len - 1; i > 0; i--) {
swap(vec[0], vec[i]);
max_heap(vec, 0, i - 1);
}
}

int main() {
vector<int> vec{3, 5, 8, 6, 1, 6, 4, 9, 4, 7, 8, 9, 7, 3, 5, 7, 4, 0, 2, 6};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
int right = vec.size() - 1;
heapSort(vec);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

时间复杂度:$O(nlog_2n)$

空间复杂度:$O(1)$

不稳定:例子:$80_1, 80_2, 70 --> 70, 80_2, 80_1$

8. 计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

计数排序的特征

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 $O(n + k)$。计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。

算法的步骤如下:

  • (1)找出待排序的数组中最大和最小的元素
  • (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
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
#include <iostream>
#include <vector>
#include <climits>
using namespace std;

pair<int, int> findMaxMinOfVec(const vector<int>& vec) {
int min = INT_MAX;
int max = INT_MIN;
for (const auto& v : vec) {
if (v > max) max = v;
if (v < min) min = v;
}
return {max, min};
}


void countSort(vector<int>& vec) {
int n = vec.size();
pair<int, int> pa = findMaxMinOfVec(vec);
int maxN = pa.first;
int minN = pa.second;
vector<int> freq(maxN - minN + 1);
for (const auto& v : vec) {
freq[v - minN]++;
}
int end = n - 1;
for (int i = maxN - minN; i >= 0; i--) {
while(freq[i] > 0) {
vec[end--] = i + minN;
freq[i]--;
}
}
}

int main() {
vector<int> vec{1,2,5,5,6,3,2,1,1,2,7,8,8,9,5,9};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
countSort(vec);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

时间复杂度:$O(n + k)$

空间复杂度:$O(k)$ 其中 k 为最大最小值差值 + 1

稳定:需要反向填充数组

9. 桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

最快:当输入的数据可以均匀的分配到每一个桶中。

最慢:当输入的数据被分配到了同一个桶中。

示意图

元素分布在桶中:

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
62
#include <iostream>
#include <vector>
#include <iterator>
using namespace std;
const int BUCKET_NUM = 5;
struct ListNode
{
ListNode* next;
int val;
explicit ListNode(int i = 0) : val(i), next(nullptr) {};
};

// 插入操作,插入有序链表
ListNode* insert(ListNode* head, int val) {
ListNode* dummyNode = new ListNode(0);
dummyNode->next = head;
ListNode* newNode = new ListNode(val);
ListNode* pre, *cur;
pre = dummyNode;
cur = head;
while (cur && cur->val <= val) {
pre = cur;
cur = cur->next;
}
newNode->next = cur;
pre->next = newNode;
return dummyNode->next;
}

// 桶排序
void bucketSort(vector<int>& vec) {
vector<ListNode*> buckets(BUCKET_NUM, (ListNode*)(0));
int n = vec.size();
for (int i = 0; i < n; i++) {
// 桶编号
int idx = vec[i] / BUCKET_NUM;
ListNode *head = buckets.at(idx);
buckets.at(idx) = insert(head, vec[i]);
}

int k = 0;
// 按照桶编号,依次输出结果即可
for (int i = 0; i < BUCKET_NUM; i++) {
if (buckets[i]) {
ListNode* cur = buckets[i];
while (cur) {
vec[k++] = cur->val;
cur = cur->next;
}
}
}
}

int main() {
vector<int> vec{1,2,3,5,14,3,2,17,2,8,6,9,4,12};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
bucketSort(vec);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

时间复杂度:$O(n + k)$,最坏情况下 $O(n^2)$

空间复杂度:$O(n + k)$

稳定,但是确切说应该取决于桶中排序算法。

10. 基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

基数排序有两种方法:

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值
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
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

int maxbit(const vector<int>& vec) {
int n = vec.size();
int maxLen = 0;
for (const auto& v : vec) {
int len = to_string(v).size();
if (len > maxLen) maxLen = len;
}
return maxLen;
}
void radixSort(vector<int>& vec) {
int n = vec.size();
int d = maxbit(vec);
vector<int> temp(n);
int count[10]; // 统计被分配到 0 1 2 ... 9 的次数
int radix = 1;
for (int i = 1; i <= d; i++) {
memset(count, 0, sizeof(count));
for (int j = 0; j < n; j++) {
int k = (vec[j] / radix) % 10;
count[k]++;
}
// 根据每个桶的大小,分配区间位置,从小到大分配
for (int j = 1; j < 10; j++) {
count[j] = count[j - 1] + count[j];
}
for (int j = n - 1; j >= 0; j--) {
int k = (vec[j] / radix) % 10;
// 反向插入
temp[count[k] - 1] = vec[j];
count[k]--; // 向前移动
}
vec = temp; // 赋值
radix *= 10;
}
}
int main() {
vector<int> vec{1,20,51,6,78,9,30,41,2,3,54,12,13,18,81,75,94,9,64,82,37,42};
for (const auto& v : vec) cout << v << ' ';
cout << endl;
radixSort(vec);
for (const auto& v : vec) cout << v << ' ';
cout << endl;
return 0;
}

时间复杂度:$O(n * k)$

空间复杂度:$O(n + k)$

稳定

排序总结

参考资料

1.0 十大经典排序算法 | 菜鸟教程 (runoob.com)



----------- 本文结束 -----------




0%