目录
(所有举例均为从小到大排序)
基础知识:
1.排序算法的稳定性
定义:在多个记录存在相同的第一关键字时,在排序后第二关键字的相对次序不发生改变的排序算法我们称它为稳定的,否则是不稳定的
例如:我们需要对以下几个坐标按 X 轴从小到大进行排序
(1,1) (2,2) (1,3) (3,1) (4,1)
如果排序后坐标顺序为:(1,1) (1,3) (2,2) (3,1) (4,1)。那么我们称这个排序是稳定的
如果排序后坐标顺序为:(1,3) (1,1) (2,2) (3,1) (4,1)。那么我们称这个排序是不稳定的
2.时间复杂度-空间复杂度-稳定性
排序算法 | 平均时间复杂度 | 空间复杂度 | 稳定性 |
冒泡排序 | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(logn) | 不稳定 |
希尔排序 | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(n) | 稳定 |
堆排序 | O(nlogn) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n+k) | 稳定 |
一:冒泡排序
前置知识
借用网上一张经典的 gif 来展示一下冒泡排序的整个过程
也就是我们进行 n 轮比较,每一次比较相邻的两个数,如果前一个数大于后一个数,那么我们对这两个数进行交换;否则不交换
代码
#include<iostream>
using namespace std;
int a[11] = { 7,5,10,2,9,4,1,6,0,8,3 };
int main() {
for (int i = 0; i <= 9; i++) {
for (int j = 0; j <= 9; j++) {
if (a[j] > a[j + 1]) swap(a[j], a[j + 1]);
}
}
for (int i = 0; i <= 10; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
//输出 0 1 2 3 4 5 6 7 8 9 10
二:选择排序
前置知识
这里也是借用了网上的一张经典 gif 来演示选择排序的整个过程
也是进行 n 轮循环,每次从当前位置的后面选择最小那个数放到当前位置
代码
#include<iostream>
using namespace std;
int a[11] = { 7,5,10,2,9,4,1,6,0,8,3 };
int main() {
for (int i = 0; i <= 9; i++) {
int idx = i;
for (int j = i + 1; j <= 10; j++) {
if (a[j] < a[idx]) idx = j;
}
swap(a[i], a[idx]);
}
for (int i = 0; i <= 10; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
//输出 0 1 2 3 4 5 6 7 8 9 10
三:插入排序
前置知识
首先,我们将数组分为有序和无序两个部分,最开始的时候,有序部分就是第一个数(只有一个数,所以它也是有序的),剩下的为无序部分
接下来,我们从无序部分选择一个数插入到有序部分,插入的时候要有序插入,也就是将这个数插入到正确位置
如:一开始我们的数组为
4 3 6 1 2 5 红色部分为有序部分,黑色部分为无序部分
第一步(对 3 操作):3 < 4,所以 3 前移,4 后移,之后数组为
3 4 6 1 2 5 第二步(对 6 操作):6 > 4,不进行移动,操作结束,之后数组为
3 4 6 1 2 5 第三步(对 1 操作):
- 1 < 6,1 前移,6 后移
- 1 < 4,1 前移,4 后移
- 1 < 3,1 前移,3 后移
之后数组为
1 3 4 6 2 5 ......
全部操作结束后的数组为
1 2 3 4 5 6
代码
#include<iostream>
using namespace std;
int a[11] = { 7,5,10,2,9,4,1,6,0,8,3 };
int main() {
for (int i = 1; i <= 10; i++) {
int pre_idx = i;
for (int j = i - 1; j >= 0; j--) {
if (a[j] > a[pre_idx]) {
swap(a[j], a[pre_idx]);
pre_idx = j;
}
else break;
}
}
for (int i = 0; i <= 10; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
//输出 0 1 2 3 4 5 6 7 8 9 10
全过程:
7 5 10 2 9 4 1 6 0 8 3
5 7 10 2 9 4 1 6 0 8 3
5 7 10 2 9 4 1 6 0 8 3
2 5 7 10 9 4 1 6 0 8 3
2 5 7 9 10 4 1 6 0 8 3
2 4 5 7 9 10 1 6 0 8 3
1 2 4 5 7 9 10 6 0 8 3
1 2 4 5 6 7 9 10 0 8 3
0 1 2 4 5 6 7 9 10 8 3
0 1 2 4 5 6 7 8 9 10 3
0 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9 10
四:快速排序
前置知识
代码
五:希尔排序
前置知识
百度百科对希尔排序的定义:希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
增量gap
需要注意的是:直接插入排序算法是稳定的,但是希尔排序是不稳定的
希尔排序的比较次数和移动次数都要比直接插入排序少,当N越大时,效果越明显。
代码
六:归并排序
前置知识
归并排序就是将两个有序的序列合并为一个序列,那么如果我们要对一个序列排序,我们首先需要将它分为多个序列
我们将一个序列进行无限二分,直到每个部分最多包含一个数(如果一个序列只有一个数,我们认为它是有序的),然后进行两两合并,直到完成所有合并
如下图划分(蓝色部分):
然后进行合并(绿色部分)
合并步骤:
- 设定两个指针 p1 ,p2 ,分别指向两个需要合并的序列的第一个数
- 比较 p1 和 p2 所指向的数的大小关系,如果 p2 指向的数更小,那么我们将 p2 指向的数放入临时数组,同时 p2 后移,p1 不动
- 重复步骤 2 ,直到其中一个序列已经全部比较完
- 将另一个没有移动完的序列里的数全部移动到临时数组里
合并过程:
- 4 < 8,所以 4 在 8 前面;其他同理
- 我们比较 4 和 5 ,4 < 5 ,所以我们将 4 放入临时数组;然后比较 5 和 8 ,5 < 8 ,我们将 5 放入临时数组;然后比较 8 和 7 ,7 < 8 ,我们将 7 放入临时数组;最后将 8 放入临时数组;后面同理
代码
#include<iostream>
using namespace std;
int a[11] = { 7,5,10,2,9,4,1,6,0,8,3 };
int ans = 0;
int tem[110]; //临时数组
void merge_pai(int l, int mid, int r) {
int i = l, j = mid, p = l;
while (i < mid && j <= r) {
if (a[i] < a[j]) tem[p++] = a[i++];
else tem[p++] = a[j++];
}
while (i < mid) tem[p++] = a[i++]; //防止前半部分的数还没有移完
while (j <= r) tem[p++] = a[j++]; //防止后半部分的数还没有移完
for (int i = l; i <= r; i++) a[i] = tem[i]; //移回到原数组
}
void merge_sort(int l, int r) {
if (l < r) {
int mid = (l + r) / 2;
merge_sort(l, mid);
merge_sort(mid + 1, r);
merge_pai(l, mid + 1, r);
}
}
int main() {
merge_sort(0, 10);
for (int i = 0; i <= 10; i++) cout << a[i] << " ";
cout << endl;
}
//输出 0 1 2 3 4 5 6 7 8 9 10
七:堆排序
前置知识
代码
#include<iostream>
using namespace std;
const int N = 100010;
int h[N], mySize;
int n;
void down(int u){
int t = u;
if (2 * u <= mySize && h[t] > h[2 * u])
t = 2 * u;
if (2 * u + 1 <= mySize && h[t] > h[2 * u + 1])
t = 2 * u + 1;
if (u != t){
swap(h[u], h[t]);
down(t);
}
}
int main() {
cin >> n;
mySize = n;
for (int i = 1; i <= n; i++) cin >> h[i];
for (int i = n / 2; i; i--) down(i);
for (int i = 1; i <= n; i++) {
cout << h[1] << " ";
h[1] = h[mySize--];
down(1);
}
return 0;
}
八:计数排序
前置知识
计数排序,顾名思义,我们需要记录每个数出现的次数;那么我们需要一个 count 数组;count 数组的大小至少为序列的最大值 maxn + 1
我们直接来看一下过程吧。
如:我们需要对序列 1 3 3 2 4 3 1 排序
初始化:
索引 idx 0 1 2 3 4 元素个数 0 0 0 0 0 计数第一轮:此时对 1 计数,1 出现一次,所以 count[1] ++
idx 0 1 2 3 4 count[idx] 0 1 0 0 0 计数第二轮:此时对 3 计数,3 出现一次,所以 count[3] ++
idx 0 1 2 3 4 count[idx] 0 1 0 1 0 计数第三轮:此时对 3 计数,3 第二次出现,所以 count[3] ++
idx 0 1 2 3 4 count[idx] 0 1 0 2 0 ......
最后:
idx 0 1 2 3 4 count[idx] 0 2 1 3 1 然后我们遍历 idx 从 0 到 4:
- count[0] = 0,无输出
- count[1] = 2,输出两个 1
- count[2] = 1,输出一个 2
- count[3] = 3,输出三个 3
- count[4] = 1,输出一个 4
代码
#include<iostream>
using namespace std;
int a[7] = { 1,3,3,2,4,3,1 };
int cnt[5];
int main() {
for (int i = 0; i < 7; i++) cnt[a[i]]++;
for (int i = 0; i < 5; i++) {
while (cnt[i]) {
cout << i << " ";
cnt[i]--;
}
}
cout << endl;
}
//输出 1 1 2 3 3 3 4