7种简单排序算法小结
假设所有的排序都是从小到大进行的
问题:给定n个元素,输出这n个元素从小到大的顺序(第一行读入一个n,接下来读入n个元素,每个元素不超过100)(n < 100)
样例输入:
6 21 25 49 28 16 8
样例输出:
8 16 21 25 28 49
稳定性描述: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中, r [ i ] = r [ j ] r[i]=r[j] r[i]=r[j],且 r [ i ] r[i] r[i]在 r [ j ] r[j] r[j]之前,而在排序后的序列中, r [ i ] r[i] r[i]仍在 r [ j ] r[j] r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
基于插入的排序
直接插入排序
- 数列的前半部分为有序,后部分为无需。一开始有序序列只有第一个,默认是有序的,然后将新的一个元素在原来的有序序列中找到正确的位置,然后插入进去。(需要注意的是,将新的元素插入进有序序列的时候,有些类似于冒泡的过程,如果待插入的元素小于有序序列的最大元素,那么将这两个的大小交换一下,再往前进行比较)(数据规模小并且越有序越高效)
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:稳定
将有序序列末的后一个元素插入进有序序列的过程,例如 2 3 5 7 4 2\,3\,5\,7\,4 23574,前 4 4 4个元素有序,将 4 4 4插入到正确的位置,方法是不断用当前最后一个和前一个比较,如果顺序不对就交换,直到不发生交换为止,说明全部都有序了,过程如下图所示
代码
#include <cstdio>
#define N 105
int a[N], n;
void Insert_Sort () {
for (int i = 2; i <= n; i++) {
int temp = a[i];
for (int j = i; j >= 1 && a[j] < a[j - 1]; j --) {
a[j] = a[j - 1];
a[j - 1] = temp;
}
}
}
int main () {
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
Insert_Sort ();
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
希尔排序
- 希尔排序是插入排序的一种。也叫做缩小增量排序,将原来的序列,按照一定的增量进行分组,然后对每个分组进行直接插入排序。接着减少增量,再次分组排序, 直到增量为 1 1 1的时候,整个序列被分为 1 1 1组,排序完成。
- 时间复杂度: O ( n l o g 2 n ) O(nlog\,2n) O(nlog2n)(和增量的选择有关)
- 稳定性:不稳定
- 没有快速排序快,但是实现简单,在最坏情况下和平均情况下效率相差不大,可以多使用,如果性能不行再用快排
假设对序列 9 1 2 5 7 4 8 6 3 5 9\,1\,2\,5\,7\,4\,8\,6\,3\,5 9125748635进行希尔排序,首先分组增量设置为长度的一半,然后一直减半,直到为 1 1 1,过程中对每一组进行直接插入排序,使得每一组都是有序的。如下图
代码
#include <cstdio>
#define N 105
int a[N], n;
void Shell_Sort () {
for (int gap = n / 2; gap >= 1; gap /= 2) { //gap表示增量
for (int i = gap; i <= n; i++) {
int temp = a[i];
for (int j = i; j >= gap && a[j] < a[j - gap]; j -= gap) { //对每一个分组进行排序
a[j] = a[j - gap];
a[j - gap] = temp;
}
}
}
}
int main () {
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
Shell_Sort ();
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
基于选择的排序
直接选择排序
- 从头到尾遍历所有的元素,找到最小的元素,并且与首位置元素进行交换,接着从第二个元素开始遍历,找到最小的元素,并且与第二个位置的元素进行交换, n n n个元素则进行 n − 1 n-1 n−1轮的比较交换,每一轮确定一个最小的元素放在前面。
- 时间复杂度: O ( N 2 ) O(N^2) O(N2)
- 稳定性:不稳定
代码
#include <cstdio>
#define N 105
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
int a[N];
int main () {
int n, mn, loc;
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = 1; i < n; i++) {
mn = a[i], loc = i;
for (int j = i; j <= n; j++)
if (a[j] < mn) {
mn = a[j];
loc = j;
}
swap (a[loc], a[i]);
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
基于交换的排序
冒泡排序
- 每一轮依次比较相邻的两个元素,如果大的在前小的在后,则交换这两个元素,这样每一轮都能确定一个未排序的元素中最大的元素,总共 n n n个元素进行 n − 1 n-1 n−1轮的比较
- 时间复杂度: O ( N 2 ) O(N^2) O(N2)
- 稳定性:稳定
代码
#include <cstdio>
#define N 105
int a[N];
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
int main () {
int n;
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = 1; i < n; i++) {
for (int j = 1; j <= n - i; j++) {
if (a[j] > a[j+1])
swap (a[j], a[j+1]);
}
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
可简单改进 (思路就在在某一轮比较中,如果相邻的两个元素没有发生交换,则说明顺序已经对了,不需要再继续排序下去)
#include <cstdio>
#define N 105
int a[N];
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
int main () {
int n;
bool flag;
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = n; i > 1; i--) {
flag = true;
for (int j = 1; j < i; j++) {
if (a[j] > a[j+1]) {
swap (a[j], a[j+1]);
flag = false;
}
}
if (flag) break;
}
for (int i = 1; i <= n; i ++)
printf("%d ", a[i]);
return 0;
}
快速排序
- 首先找到一个基准数,将基准数左右两边的数字进行交换,使得左边的元素全部比它小,右边的元素全都比它大,然后将左右两边分而治之,同样找基准数,然后交换,不断进行下去,直到子序列只剩下一个元素。每一轮快速排序,总是能将基准数放在正确的位置上。而每次交换比较是跳跃式的,总的比较次数就比冒泡排序少,最坏情况下,仍然是相邻的两个元素进行了交换(基准数的选取很大程度上影响算法的速度,一般选择 序列的第一个元素,或者中间的元素作为基准数)。
- 平均时间复杂度: O ( N l o g ( N ) ) O(Nlog(N)) O(Nlog(N))
- 稳定性:不稳定
以中间的数作为基准数为例,指针 i , j i,j i,j分别从 l e f t , r i g h t left,right left,right开始,为了保证基准数的左边小于它,右边大于它,那么应该在左边找到一个不小于基准数的数,在右边找到一个不大于基准数的数,然后交换这两个数。直到,两个指针相遇。然后从 i , j i,j i,j指针停留的位置,一分为二,进行下一轮的调整(可以从图中发现,实际上快速排序就是在给每一次的基准数找到自己正确的位置)。下图以 1 5 2 3 4 8 1\,5\,2\,3\,4\,8 152348为例,来进行快速排序,下图的橘色为每次的基准数,每次先从右边开始找,然后是左边。如图所示
代码
#include <cstdio>
#define N 105
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
void Quick_Sort1 (int a[], int left, int right) { //选取第一个点作为基准元素
if (left >= right) return; // 一个元素肯定是有序的
int i = left, j = right, key = a[left];
while (i < j) {
while (i < j && a[j] >= key) j --; //必须先从右边开始找
while (i < j && a[i] <= key) i ++;
if (i < j) swap (a[i], a[j]);
}
swap (a[left], a[i]);
Quick_Sort (a, i + 1, right);
Quick_Sort (a, left, i - 1);
}
void Quick_Sort2 (int a[], int left, int right) { //选取中点作为基准元素
if (left >= right) return ;
int i = left, j = right, key = a[(left + right) >> 1];
while (i < j) {
while (a[j] > key) j --;
while (a[i] < key) i ++;
if (i <= j) {
swap (a[i], a[j]);
i ++, j --;
}
}
Quick_Sort (a, left, j);
Quick_Sort (a, i, right);
}
int main () {
int n, a[N] = {0};
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
Quick_Sort2 (a, 1, n);
for (int i = 1; i <= n; i++)
printf("%d ", a[i]);
return 0;
}
桶排序
- 找出待排序元素的最大值 m a x max max,开一个数组大小为 m a x + 1 max+1 max+1,遍历待排序数列,将数列的值最为数组的下标,数组元素自增 1 1 1位。最后从数组的开头进行遍历,如果数组的值不为 0 0 0,则进行输出下标,输出的次数为数组的值的大小。
- 时间复杂度: O ( N + C ) O(N+C) O(N+C)
- 稳定性:稳定排序
代码
#include <cstdio>
#define N 105
int bucket[N];
int main () {
int n, number;
scanf ("%d", &n);
for (int i = 1; i <= n; i++) {
scanf ("%d", &number);
bucket[number] ++;
}
for (int i = 0; i < N; i++) {
if (bucket[i])
for (int j = 1; j <= bucket[i]; j++)
printf("%d ", i);
}
return 0;
}
归并排序
- 归并排序是建立在归并操作上的一种有效排序算法。核心思想在于将两个有序的数列合并成为一个大的有序数列,通过递归的方式层层合并。先将数组不断二分,然后在进行有序序列的合并
- 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
- 稳定性:稳定
下面通过两张图片说明这个过程,例如 有一个初始序列, 2 8 4 6 3 7 2 \,8\, 4\, 6\, 3\, 7 284637,对它进行归并排序,首先将这个序列不断二分,直到子序列的元素个数为 1 1 1( 1 1 1个元素肯定是有序的)。
然后进行归并
不断将归并的序列变成有序,最终的到一个有序的序列 2 3 4 6 7 8 2\,3\,4\,6\,7\,8 234678
那么归并的操作是怎么进行的呢?如何将两个有序的序列合并成一个有序的序列呢?来看下面这张图,假设现在有两个有序子序列分别是 2 4 8 2\,4\,8 248和 3 6 7 3\,6\,7 367(它们都存在于数组 a a a中),用两个指针 i j i\,j ij分别指向这两个序列的开头,然后进行比较,找到小的那个,将它放进辅助数组中,在这个例子中 a [ i ] < a [ j ] a[i] < a[j] a[i]<a[j]因此指针 i i i向后移动一位继续比较。然后比较 4 3 4\,3 43的大小,并继续移动指针,当其中一个指针移动到子序列的末尾就停止移动,然后检查是否 i j i\,j ij都移动到了末尾,如果没有移动到末尾,就将后面的元素依次放进辅助数组中。这样辅助数组中就存放了两个有序子序列合并后的结果了。
需要注意的是,辅助数组中只是暂时存放当前归并的两个子序列的结果,需要将这个辅助数组对应的位置复制到原数组中。#include <cstdio>
#define N 105
int a[N], b[N]; //数组B为辅助数组
void merge_sort (int l, int r) {
if (l >= r) return ;
int mid = (l + r) >> 1;
merge_sort (l, mid); //不断分解
merge_sort (mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r && k <= r) {
if (a[i] <= a[j]) b[k++] = a[i++];
else b[k++] = a[j++];
}
while (i <= mid) b[k++] = a[i++];
while (j <= r) b[k++] = a[j++];
for (int q = l; q <= r; q ++) //将辅助数组中的有序序列赋值回去
a[q] = b[q];
}
int main () {
int n;
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
merge_sort (1, n);
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
由于归并排序的特性,可以衍生出利用归并排序来找逆序对的方法。所谓逆序对就是一对元素,大的在前,小的在后,也就是序列中 a i > a j a_i>a_j ai>aj且 i < j i<j i<j的有序对。例如序列 4 3 6 2 4\,3\,6\,2 4362,其中就有 4 4 4个逆序对。分别是 4 3 、 4 2 、 3 2 、 6 2 4\,3、4\,2、3\,2、6\,2 43、42、32、62
那么怎么找逆序对呢?不妨这样想一想,归并排序在进行合并的时候。指针 i i i是指向左边序列,指针 j j j是指向右边的序列,并且这两个子序列都是从小到大有序增加的,那么如果出现了这样一种情况,即 a [ i ] > a [ j ] a[i] > a[j] a[i]>a[j],那么这个时候肯定会出现逆序对,会出现多少逆序对呢?注意此时的子序列是递增的,那么左边子序列在 i i i右边的肯定比 a [ i ] a[i] a[i]大,这个时候逆序对的个数,就是从 i i i开始到左边子序列结束的个数,用一个累加器加在一起即可。
#include <cstdio>
#define N 500005
#define in(a) a = read ()
int read () {
int x = 0, f = 1; char c = getchar ();
while (c < '0' || c > '9') {if (c == '-') f = - 1; c = getchar ();}
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + c - 48; c = getchar ();}
return f * x;
}
int a[N], b[N];
int ans;
void Merge_Sort (int left, int right) {
if (left >= right) return ;
int mid = (left + right) >> 1;
Merge_Sort (left, mid);
Merge_Sort (mid + 1, right);
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) {
if (a[i] <= a[j]) b[k++] = a[i++];
else {
ans += mid - i + 1; //将所有a[j]前面比它大的数找出来
b[k++] = a[j++];
}
}
while (i <= mid) b[k++] = a[i++];
while (j <= right) b[k++] = a[j++];
for (int q = left; q <= right; q ++) a[q] = b[q];
}
int main () {
int n;
in (n);
for (int i = 1; i <= n; i++)
in (a[i]);
Merge_Sort (1, n);
printf ("%d", ans);
return 0;
}