目录
(所有举例均为从小到大排序)
基础知识:
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;
const int N = 10010;
int a[N];
int main() {
int n; cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = n - 1; i >= 1; i--) {
bool fl = true;
for(int j = 1; j <= i; j++) {
if(a[j] > a[j + 1]) {
swap(a[j], a[j + 1]);
fl = false;
}
}
if(fl) break;
}
for(int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
二:选择排序
前置知识
这里也是借用了网上的一张经典 gif 来演示选择排序的整个过程
也是进行 n 轮循环,每次从当前位置的后面选择最小那个数放到当前位置
代码
#include<iostream>
using namespace std;
const int N = 10010;
int a[N];
int main() {
int n; cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n - 1; i++) {
int idx = i;
for(int j = i + 1; j <= n; j++)
if(a[idx] > a[j]) idx = j;
swap(a[idx], a[i]);
}
for(int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
三:插入排序
前置知识
首先,我们将数组分为有序和无序两个部分,最开始的时候,有序部分就是第一个数(只有一个数,所以它也是有序的),剩下的为无序部分
接下来,我们从无序部分选择一个数插入到有序部分,插入的时候要有序插入,也就是将这个数插入到正确位置
如:一开始我们的数组为
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
四:快速排序
前置知识
快速排序(Quick sort)是对冒泡排序的一种改进。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
1、首先设定一个分界值,通过该分界值将数组分成左右两部分。
2、将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
3、然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4、重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
如:一开始我们的数组为
选取基准值为 base = a[1] = 3,i = left = 1, j = right = 6
3 4 6 1 5 2
第一轮:a[j] < base j 不变
a[i] <= base i++ --------------- a[i = 2] > base -------swap(a[i], a[j])
此时数组如下,j = 6,i = 2:
3 2 6 1 5 4 第二轮:a[j] >= base j-- ---------------- a[j] >= base j-- ------------a[j = 4] < base j = 4
a[i] <= base i++ ---------------- a[i = 3] >= base swap(a[i], a[j])
此时数组如下,j = 4, i = 3:
3 2 1 6 5 4 第三轮:a[j] >= base j-- ------------------a[j = 3] < base j = 3
a[i] <= base but i == j ---------- end
END:基准值归位,swap(a[left], a[i])
此时数组如下:
1 2 3 6 5 4 至此,基准值左边的数全部小于基准值,右边的数全部小于基准值,如此分治递归,就是快速排序
代码
#include<iostream>
using namespace std;
const int N = 10010;
int a[N], n;
void quickSort(int left, int right) {
if(left >= right) return ;
int i = left, j = right, base = a[left];
while(i < j) {
while(a[j] >= base && i < j) j--;
while(a[i] <= base && i < j) i++;
if(i < j) swap(a[i], a[j]);
}
swap(a[left], a[i]);
quickSort(left, i - 1);
quickSort(i + 1, right);
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
quickSort(1, n);
for(int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
五:希尔排序
前置知识
百度百科对希尔排序的定义:希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
增量gap
需要注意的是:直接插入排序算法是稳定的,但是希尔排序是不稳定的
希尔排序的比较次数和移动次数都要比直接插入排序少,当N越大时,效果越明显。
代码
#include<iostream>
using namespace std;
const int N = 10010;
int a[N], n;
void shellSort() {
if(n <= 1) return ;
int gap = n / 2;
while(gap >= 1) {
for(int i = gap + 1; i <= n; i++) {
if(a[i] < a[i - gap]) {
int temp = a[i], j;
for(j = i - gap; j >= 1 && a[j] > temp; j -= gap)
a[j + gap] = a[j];
a[j + gap] = temp;
}
}
gap /= 2;
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
shellSort();
for(int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
六:归并排序
前置知识
归并排序就是将两个有序的序列合并为一个序列,那么如果我们要对一个序列排序,我们首先需要将它分为多个序列
我们将一个序列进行无限二分,直到每个部分最多包含一个数(如果一个序列只有一个数,我们认为它是有序的),然后进行两两合并,直到完成所有合并
如下图划分(蓝色部分):
然后进行合并(绿色部分)
合并步骤:
- 设定两个指针 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
九:桶排序
前置知识
桶排序是计数排序的升级版,也是分治算法。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。简言之,将值为 val 的元素放入 val 号桶,最后依次把桶里的元素倒出来。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 10010, bucketN = 10;
int a[N], n;
void bucketSort() {
int maxn = -1;
for(int i = 1; i <= n; i++) maxn = max(maxn, a[i]);
vector<int> buckets[bucketN];
int bucketSize = 1;
while(maxn) maxn /= 10, bucketSize *= 10;
bucketSize /= 10;
for(int i = 1; i <= n; i++) {
int idx = a[i] / bucketSize;
buckets[idx].push_back(a[i]);
// bucket with insertsort
auto &temp = buckets[idx];
int len = buckets[idx].size();
for(int j = len - 1; j >= 1; j--)
for(int k = 0; k <= j - 1; k++)
if(temp[k] >= temp[k + 1]) swap(temp[k], temp[k + 1]);
}
for(int i = 0, k = 1; i < bucketN; i++)
for(int j = 0; j < buckets[i].size(); j++, k++)
a[k] = buckets[i][j];
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
bucketSort();
for(int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
十:基数排序
前置知识
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
- 取得数组中的最大数,并取得位数
- a 为原始数组,从最低位开始取每个位组成 radix 数组
- 对 radix 进行计数排序(利用计数排序适用于小范围数的特点)
代码
#include<iostream>
using namespace std;
const int N = 10010;
int a[N], n;
int maxbit = -1;
int cnt[10], temp[N];
int maxb() {
int maxn = -1;
for(int i = 1; i <= n; i++) maxn = max(maxn, a[i]);
int ans = 0;
while(maxn) maxn /= 10, ans++;
return ans;
}
void radixSort() {
maxbit = maxb();
int radix = 1;
while(maxbit--) {
for(int i = 0; i <= 9; i++) cnt[i] = 0;
for(int i = 1; i <= n; i++) {
int k = (a[i] / radix) % 10;
cnt[k]++;
}
for(int i = 1; i <= 9; i++) cnt[i] += cnt[i - 1];
for(int i = n; i >= 1; i--) {
int k = (a[i] / radix) % 10;
temp[cnt[k] - 1] = a[i];
cnt[k]--;
}
for(int i = 1; i <= n; i++) a[i] = temp[i - 1];
radix *= 10;
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
radixSort();
for(int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}