快速排序,归并排序,计数排序

前面讲的冒泡,选择,插入排序还是太慢了,时间复杂度达到了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,还可以排结构体,重要的是理解思想 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值