前面讲的冒泡,选择,插入排序还是太慢了,时间复杂度达到了n2,下面介绍三个快速的排序
快速排序:
快速的思想是每次排一个数,这个数是末尾的数,通过排序交换,让这个数左边的数都小与它,右边的数都大于它,我们把排好的这个点叫做隔断点,这个点将数组分成了两部分,然后对这两部分再找隔断点,这个过程将数组不断切割,直到数组长度变为1,开始返回,这时就会发现,在不断找间断点的过程中,就已经将数组排好
快速排序的递归思想:
不难发现上述过程其实就是递归的过程,我们将排序看成一个大问题,然后进行子问题拆解,想让这个数组有序,我先让一个数有序,即让这个数左边都小与它,右边都大于它,然后对这个点即隔断点左右拆解,分为两个部分,又找到了两个隔断点,这时我们已经让三个点有序,并将数组拆成了四个部分,注意在找隔断点的过程中让这些数组有了大小顺序,即前面的数组的所有值一定小于后面的数组任意值,即整体呈上升趋势
下面是代码
int partition(int a[], int l, int r)//partition隔断点,找到隔断的数的角标并返回
//假设为x,那么x左边都小于等于x,右边都大于等于x
{
int pivot = a[r], i = l, j = r;
while (i<j)
{
while (i < j && a[i] <= pivot)i++;//向前扫,直到找到可交换的点
while (i < j && a[j] >= pivot)j--;//向后扫,直到找到可交换的点
if (i < j)swap(a[i], a[j]);//没相撞就交换
else swap(a[i], a[r]);//相撞挪移x
}
return i;//返回x的角标
}
void Quicksort(int a[], int l, int r)//直到分到数组长度为1,l=r,结束递归
{
if (l < r)
{
int mid = partition(a, l, r);//总分
Quicksort(a, l, mid - 1);//前面小于x的分二
Quicksort(a, mid + 1, r);//后面大于x的分二
}
}
给一组样例可以结合代码理解
8 7 4 6 5 7 3 4(随手写的,可能不太好,感兴趣可自己写一组推一下)
一开始找的是4,即4为隔断点
i从前向后扫,发现8>4可交换
j从后向前扫,发现3<4可交换
交换后i与j未相撞,继续扫,
i发现7可交换,而j扫到7,i与j相撞,4与7交换
3 4 4 6 5 7 8 7
第二个4为间断点,左边长度为1,不必考虑,对右边数组重复上述过程
到有序的过程是
3 4 4 6 5 7 7 8
3 4 4 6 5 7 7 8
3 4 4 5 6 7 7 8
到这就可以写蓝桥3226宝藏排序II啦,以下AC代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 5;
int partition(int a[], int l, int r)//partition隔断点,找到隔断的数的角标并返回
//假设为x,那么x左边都小于等于x,右边都大于等于x
{
int pivot = a[r], i = l, j = r;
while (i<j)
{
while (i < j && a[i] <= pivot)i++;//向前扫,直到找到可交换的点
while (i < j && a[j] >= pivot)j--;//向后扫,直到找到可交换的点
if (i < j)swap(a[i], a[j]);//没相撞就交换
else swap(a[i], a[r]);//相撞挪移x
}
return i;//返回x的角标
}
void Quicksort(int a[], int l, int r)//直到分到数组长度为1,l=r,结束递归
{
if (l < r)
{
int mid = partition(a, l, r);//总分
Quicksort(a, l, mid - 1);//前面小于x的分二
Quicksort(a, mid + 1, r);//后面大于x的分二
}
}
int a[N];
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n; cin >> n;
for (int i = 1; i <= n; i++)cin >> a[i];
Quicksort(a, 1, n);
for (int i = 1; i <= n; i++)cout << a[i] << " ";
return 0;
}
时间复杂度为n*logn
接下来是归并排序:
归并排序也是递归的思想,不同的是需要一个辅助数组,并且是在递归返回的时候,逐渐让数组有序,对于整个数组,我是否可将数组拆分为两个部分,让左边的数组右序,右边的数组有序,并且取两个指针分别指向两个数组的开头,每次我都取更小的数放入辅助数组,将两个数组放完后,辅助数组就是我们要求的数组,所以将辅助数组赋回原数组即可(看不懂可以先看样例)
那么问题就转变为如何将数组一分为2,左边有序,右边有序呢?以递归的思想来看,我们可以将数组不断拆分,16变8,8变4,4变2,2变1,不可再分,长度为1的数组肯定有序,再2变1的过程中,两个长度为1的数组取更小放入辅助数组,再还原给原数组,那么长度为16的数组可以看为8段有序数组组合而成,2再还原到4,4再还原到8,8再还原到16的过程就是上述过程
总结:将数组不断切割,在不断合并的过程中将原数组变得逐渐有序的过程就是归并排序啦
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int a[N];
int b[N];
int n;
void mergesort(int l, int r)
{
if (l == r)return;
int mid = (l + r)>>1;
mergesort(l, mid);
mergesort(mid + 1, r);
int p1 = l, p2 = mid + 1;
int pos = l;
while (p1 <= mid&&p2 <= r)
{
if (a[p1] <= a[p2])b[pos++] = a[p1++];
else b[pos++] = a[p2++];
}
while (p1 <= mid)b[pos++] = a[p1++];
while (p2 <= r)b[pos++] = a[p2++];
for (int i = l; i <= r; i++)
a[i] = b[i];
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)cin >> a[i];
mergesort(1, n);
for(int i=1;i<=n;i++)cout<<a[i]<<" ";
return 0;
}
时间复杂度为n*logn
这个代码也可通过上述题目
给一组样例方便理解
8 3 7 6 4 2 5 2
进行归并排序切割到长度为1,开始返回
用|表示被切割的数组
8|3|7|6|4|2|5|2
3 8|6 7|2 4|2 5
3 6 7 8|2 2 4 5
2 2 3 4 5 6 7 8
快速排序,归并排序的递归还是太吃操作了,有没有更简单更快速的排序?有的有的,那就是计数排序,时间复杂的仅为O(n+m),就是太吃空间了
计数排序的思想是统计数组中每个数出现的次数,最后遍历数组中元素的范围,对有元素出现的数直接输出
以蓝桥1314计数排序为例
#include<iostream>
using namespace std;
const int N = 5e5 + 5;
const int M = 5e5+1;
int n, m=M;
int a[N], c[M];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <=n; i++)
{
cin >> a[i];
c[a[i]]++;
}
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= c[i]; j++)
{
cout << i << " ";
}
}
cout << endl;
return 0;
}
c数组是为了统计每个数出现的次数,m是数组中元素能达到的最大值,从前向后遍历所有可能出现的值,依次输出。
到这可能有疑问,既然要存储次数用map不是更好吗?确实可以使用map来存储和遍历每个元素,而且map在遍历键的时候是默认从小到大,过滤掉了不必要的元素,会不会更快呢?答案是否定的map由于自身结构和操作的时间复杂度反而会让程序更慢,c数组是对map结构模仿
以下是用map解决计数排序的代码,有兴趣的可以检测一下哪个更快
#include<iostream>
#include<map>
using namespace std;
const int N = 5e5 + 5;
int n;
int a[N], c[N];
map<int,int> m;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <=n; i++)
{
cin >> a[i];
m[a[i]]++;
}
for (auto &p:m)
{
for (int j = 1; j <=p.second; j++)
{
cout << p.first<< " ";
}
}
cout << endl;
return 0;
}
计数排序还有什么用呢?其实可以用计数排序求每个元素在排序后处于数组中的哪个位置,我们用r数组来存位置,而且这也是用map写计数排序做不到的地方,因为我们要对c数组做一个前缀和,做完前最缀和的c[i]表示的是a数组中小于等i的数量,然后从后向前遍历a数组,从最后一个开始遍历,确定每一个排完序后处于哪个位置(这样做的目的是让相同的元素按照序号排序,即先出现的在前面)
假设取得得a[i]数为4,那么c[4]表示的是a数组中小于等于4的数量,假设数量为2,那么这个4在的位置就是第2个,因为小与等于它的用2个也包括它自己,所以之后要c[i]--
代码如下
#include<iostream>
using namespace std;
const int N = 1e5 + 5;
const int M = 5e5+1;
int n, m=M;
int a[N], c[M], r[N];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <=n; i++)
{
cin >> a[i];
c[a[i]]++;
}
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= c[i]; j++)
{
cout << i << " ";
}
}
cout << endl;
for (int i = 1; i <= m; i++)c[i] += c[i - 1];
for (int i = n; i>=1; i--)
{
r[i] = c[a[i]]--;
}
for (int i = 1; i <= n; i++)cout << r[i] << " ";
return 0;
}
运行一个示例
5
3 9 5 3 2
排完序后
2 3 3 5 9
排序后每个数应在的位置,如第一个3,序号在前,所以在第二位
2 5 4 3 1
最后排序一般都可以用库函数来排,如algorithm中的sort,还可以排结构体,重要的是理解思想