C++常用的排序算法【选择排序】【swap函数】【冒泡排序】 【插入排序】【快速排序】【归并排序】【sort函数】【stable_sort函数】【计数排序】

目录

一、选择排序

 介绍一下swap()函数

二、冒泡排序

三、插入排序

四、快速排序

【分治法】

【复杂度分析】

五、sort函数的使用

六、归并排序

七、stable_sort函数 

八、计数排序


一、选择排序

1.基本思路:

(1)在寻找第 i 位置的元素时,求出第 i 位到第 n 位的最小值,并记录其位置为 k

(2)交换 i 位元素和第 k 位元素。

2.代码实现: 

#include<iostream>
#include<algorithm>
using namespace std;
#define N 2000

int n;
int a[N];

int main()
{
	cin >> n;
	for (int i = 0; i < n; i++)
		cin >> a[i];
	for (int i = 0; i < n-1; i++)
	{
		int min_pos = i;
		for (int j = i+1; j < n; j++)
		{
			if (a[j] < a[min_pos])
				min_pos = j;
		}
		swap(a[i], a[min_pos]);
	}
	for (int i = 0; i < n; i++)
		cout << a[i] << ' ';
	return 0;
}

  时间复杂度为:O(n^{2})

【注意】

        代码段第14行

for (int i = 0; i < n-1; i++)

        这里 i 的循环条件为0~n-1,之所以不选择 n 是因为,当前 n-1 位已经确定之后,第 n 位也自动确定了。

 3.介绍一下swap()函数:

        因为排序算法经常用到交换操作,每一次都写一个交换操作会很麻烦,所以这里采用C++一个内置的交换函数swap()函数,主要作用就是交换两个参数的值。

        有关于swap()更详细的介绍放在下面

变量值互换自定义函数swap()的使用_Apollon_krj的博客-CSDN博客https://blog.csdn.net/apollon_krj/article/details/51445885【C++ primer阅读记录】内存管理之swap交换函数与std::move - LeeSCUT - 博客园 (cnblogs.com)https://www.cnblogs.com/leeinSCUT/p/14087987.html

二、冒泡排序

总体思路为:

        第 1 个阶段,通过冒泡,将 n 个元素中的最大值移动到序列的最后一位

        第 i 个阶段,通过冒泡,将前 n- i +1个元素中的最大值移动到最后一位

冒泡过程:

        从左到右,依次比较相邻的两个元素,利用如下代码,将两者中更大的元素移到右边(即交换操作):

if(a[i]>a[i+1])
    swap(a[i],a[i+1]);

采用代码如下:

#include<iostream>
#include<algorithm>
#define N 1000
using namespace std;

int a[N];
int n;
int main()
{
	cin >> n;
	for (int i = 0; i < n; i++)
		cin >> a[i];
	for (int i = 0; i < n-1; i++)
	{
		for (int j = 0; j < n-1-i; j++)
		{
			if (a[j] > a[j + 1])
				swap(a[j], a[j + 1]);
		}
	}
	for (int i = 0; i < n; i++)
		cout << a[i] << ' ';
	return 0;
}

时间复杂度为:O(n^{2})

【注意】

代码段的第13行i 的循环条件是0~n-1,一共进行n-1次冒泡排序。

三、插入排序

基本思路:

(1)假设1~i-1已经有序,从 i 到 1 枚举分界线的下标 j

(2)如果分界线前面的元素a[ j-1 ] > x ,那么说明a[ j-1 ]应该在 x 的后面,所以将a[ j-1 ]移动到x的后面,分界线前移,变为 j-1;

(3)如果分界线前面没有元素,就将 x 放在数组的第一位;如果碰到一个 a[ j-1 ] < x ,说明分界线正确,就将x插入到 j 位。

for (j = i; j > 0 && a[j - 1] > x; j--)    //其中j>0
			a[j] = a[j - 1];
		a[j] = x;

时间复杂度为:O(n^{2}

采用代码如下:

#include<iostream>
#define N 1000
using namespace std;

int a[N];
int n;
int main()
{
	cin >> n;
	for (int i = 0; i < n; i++)
		cin >> a[i];
	for (int i = 1; i < n; i++)
	{
		int j;
		int x = a[i];
		for (j = i; j > 0 && a[j - 1] > x; j--)
			a[j] = a[j - 1];
		a[j] = x;
	}
	for (int i = 0; i < n; i++)
		cout << a[i] << ' ';
}

四、快速排序

【分治法】

        快速排序用到了一种分治的思想,就是将一个复杂的问题不断分解为规模更小,更容易解决的问题,从而提升解决效率

基本思路:

(1)假设我们要对数组a[1..n]排序。初始化区间[1..n]

(2)令 l 和 r 分别为当前区间的左右端点。下面假设我们对 l到 r子段内的数字进行划分。取pivot = a[l]为分界线,将小于pivot的数字移到左边大于pivot的数字移到右边,然后将pivot放在中间。假设pivot的位置是 k 

(3)如果左边区间[l..k-1]长度大于1,则对于新的区间[l..k-1],重复调用上面的过程。

(4)如果右边区间[k+1..r]长度大于1,则设置新的区间[k+1, r],重复调用上面的过程。

(5)当整个过程结束以后,整个序列排序完毕。

【注意】对于(2)~(4)的操作可以用递归来实现。

分段代码解析如下:

  设置quick_sort递归函数:

void quick_sort(int l,int r)    //参数分别为当前排序子段在原序列中左右端点的位置

模拟(3)(4)操作的递归:

//k为当前分界线的位置
if(k-1>l) quick_sort(l,k-1);//如果序列的分界线左边的子段长度>1,排序
if(k+1<r) quick_sort(k+1,r);//如果序列的分界线右边的子段长度>1,排序

模拟(2)操作的移动:

int pivot=a[r];//设置最右边的数为分界线的值
int k=l;//用来记录数字已经被用来交换的位置,此时小于k的部分一定是小于pivot的部分
for(int j=l;j<r;j++) //枚举l~r内的所有数字,将小于pivot的数字依次与最前面的位置交换
{
    if(a[j]<pivot)
        swap(a[j],a[k++]);
}
swap(a[k],a[r]);//最后将右侧端点放到分界线的位置(利用右侧端点作为分界线)

完整代码如下:

#include<iostream>
#define N 10000
using namespace std;
int a[N];
int n;

void quick_sort(int l, int r)
{
	int pivot = a[r];
	int k = l;
	for (int i = l; i < r; i++)
	{
		if (a[i] < pivot)
			swap(a[i], a[k++]);
	}
	swap(a[r], a[k]);//此时的k为分界线位置
	if (k - 1 > l)
		quick_sort(l, k - 1);
	if (k + 1 < r)
		quick_sort(k + 1, r);
}
int main()
{
	cin >> n;
	for (int i = 0; i < n; i++)
		cin >> a[i];
	quick_sort(0, n-1);
	for (int i = 0; i < n; i++)
		cout << a[i] << ' ';
	return 0;
}

【复杂度分析】

        空间复杂度:O(n)

        整个排序的过程中,元素都在原数组中移动,是一种原地排序算法,占用的内存空间一直都是原数组的大小,即为O(n)

        时间复杂度:

        当我们将每个子段的中间元素作为分界线的话,一共会被划分为log n层每一层对每一个子段扫描的合并时间复杂度为O(n),那么在这种情况下总体的时间复杂度为:O(n*log n)

        下面对log n层进行简单解释:

        若假设一共有n个元素,那么:

        但是对于任意n个数的排序,每次划分情况取决于分界线情况,如果每次分界线刚好取最大值或者最小值,会导致划分时所有元素到另一边,此时的时间复杂度最坏,为:O(n^{2}),所以要尽量避免这样的情况发生。

        有如下两种避免上述情况的办法:

(1)在排序之前,随机打乱数组元素的顺序

(2)在选取分界线时,与之前选取固定位置作为分界线的方法相比,我们可以随机选取位置作为分界线


五、sort函数的使用

        C++标准模板库(STL)提供了一种排序函数sort,因为在很多场景下都需要用到排序的操作,但是一些平方级别的排序算法不够高效,所以我们还可以选择sort函数进行排序。

        sort函数基本用法:

#include<algorithm>
using namespace std;
sort(头指针,尾指针,比较函数cmp);//前两个参数必须有,比较函数可以没有,
                                //若没有,则按照递增进行排序

        若实现从大到小的排序,可以自行定义cmp函数

bool cmp(int a,int b)
{
    return a>b;
}
sort(a,a+n,cmp);

        有关于sort函数更详细的用法放在了下面

sort()函数详解_辉小歌的博客-CSDN博客_sort函数https://blog.csdn.net/qq_46527915/article/details/114597901

 六、归并排序

【基本思路】

        把整个序列分成大小相等的两个序列,不设置分界线分别进行排序,然后合并两个序列。所用思想依然是分治法,归并排序是一种自底向上的逻辑,它把序列一步一步分解为最小有序子段(即长度为1),然后对相邻两个有序子段进行排序、合并,同样的方法依次使用,直到归并为整个序列。

【算法过程】

        (1)假设我们要对数组a[1..n]排序。初始化左端点l=1右端点r=n

        (2)下面假设我们对 l到 r 子段内的数字进行划分。取 l 和 r 的中点 mid,将 l到 mid的元素看成第一个子段的部分,将 mid+1到 r的部分看成第二个子段的部分。两边分别进入下一层,重复调用上面的过程。直到子段长度为1返回上一层

        (3)当算法阶段返回到当前层时,使用归并操作合并下一层的左右两个有序序列形成本层的有序序列,继续返回上一层。此时借助一个辅助数组b,现将归并排序后的数组复制到b中用k来枚举原序列 l 到 r 的位置,依次从b数组中挑选元素,填入到k所在位置。

       设置两个指针 i ,j,分别指向两个子段的最小元素,如果:j 已经移出子段末尾或者 i 和 j 仍指向当前子段且 i 指向元素比 j 小,那么:将 i 指向元素填入到 k 所在位置,并将 i 后移否则,将 j 指向元素填入到 k 所在位置

        (4)当整个过程结束以后,整个序列排序完毕。

【模拟算法】

模拟(1)(2)归并排序的算法操作:

void merge_sort(int l,int r)
{
    if(l>=r)
        return;//如果子段为空,或者长度为1,则说明该子段有序,退出该函数
    int mid=(l+r)>>1;//利用右移一位的操作来得出中间位置
    merge_sort(l,mid);
    merge_sort(mid+1,r);

    /*省略了合并的过程*/
}

模拟(3)归并的算法操作:

void merge(int l, int r) 
{
    for (int i = l; i <= r; i++) b[i] = a[i]; // 将a数组对应位置复制进辅助数组
    
    int mid = l + r >> 1;           
    int i = l, j = mid + 1;         // 初始化i和j两个指针分别指向两个子段的首位
    for (int k = l; k <= r; ++k)  // 枚举原数组的对应位置
    { 
        if (j > r || i <= mid && b[i] < b[j]) 
            a[k] = b[i++]; 
        else 
            a[k] = b[j++];
    }
}

完整代码如下:

#include<iostream>
using namespace std;
#define N 1000
int a[N], b[N];
int n;

void merge(int l, int r)
{
	for (int i = l; i <= r; i++)
		b[i] = a[i];
	int mid = (l + r) >> 1;
	int i = l, j = mid + 1;
	for (int k = l; k <= r; k++)
	{
		if (j > r || i <= mid && b[i] < b[j])
			a[k] = b[i++];
		else
			a[k] = b[j++];
	}
}
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);
	merge(l, r);
}
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	merge_sort(1, n);
	for (int i = 1; i <= n; i++)
		cout << a[i] << ' ';
}

【对比】

  与快速排序对比:

(1)归并排序没有设置分界线快速排序的时间复杂度高度依赖于分界线的选取

(2)归并排序增加了一个辅助数组,是一种非原地排序算法快速排序是一种原地排序算法

(3)归并排序是稳定排序快速排序不是稳定排序;(稳定排序就是指对于有重复元素的序列,在排序前后,重复元素的相对位置不改变)

(4)二者都使用了递归操作。

【复杂度分析】

空间复杂度:O(n)

时间复杂度:O(nlogn),分析方法和快速排序类似

七、stable_sort函数 

        STL库中有对归并排序的优化实现,即为stable_sort()函数,用法与sort()函数相同。

 八、计数排序

【基本思路】

        计数排序是一种对已知数量范围的数组进行排序的算法。给定长度为 n 的序列,假设已知序列元素的范围都是 [ 0……k ]中的整数,并且 k 的范围比较小,则可以使用计数排序的方法。如果元素的范围不是 [ 0……k ],可以转换到这个区间进行排序,或者进行扩大或缩放到这个范围。

【算法描述】

(1)使用数组 cnt 统计 [ 0……k ]范围内所有数字在序列中出现的个数

(2)使用 i 从0~k 进行枚举,如果出现 cnt [ i ] 次 i ,那么就在序列末尾增加cnt [ i ] 个 i

(3)利用 i 从0~k 进行枚举,利用 j 作为答案数组所在位置,将原数组中的数字序列对应到新数组中,最后将新数组输出。

采用代码如下:

#include<iostream>
using namespace std;
#define N 1000005
#define K 1000000
int a[N], b[N],cnt[K];
int n;
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		++cnt[a[i]];    //这里通过cnt数组来维护每个数字出现的次数
	}
	for (int i = 0, j = 0; i < K; i++)
	{
		for (int m = 1; m <= cnt[i]; m++)
			b[++j] = i;
	}
	for (int i = 1; i <= n; i++)
		cout << b[i] << ' ';
	return 0;
}

【复杂度分析】

空间复杂度:

        上述代码一共开设了三个数组,对于a和b为O(N),对于cnt为O(K),总体空间复杂度为O(N+K)

时间复杂度:

        输入和输出的时间复杂度为:O(n)内层循环为O(n),维护有序序列的最外层循环为O(K),总体时间复杂度为:O(n+K)。之所以计数排序可以达到比O(nlogn)更好的时间复杂度,就是因为它并不是基于比较的排序

【另一种算法——对应原数组当中的位置】

大体思路如下:

(1)(2)与上述算法相同;

(3)设置一个前缀和数组sum[ ],来记录cnt[ ]各项的前缀和,利用sum[ x ]即为x 出现的最后的那一个位置,设置idx[ ]数组来与原数组元素在新数组中的位置进行对应

(4)将idx[ ] 中的位置信息和a[ ] 进行对应,并存储到b[ ]中。

 采用代码如下:

#include<iostream>
#define N 100005
#define K 100000
using namespace std;
int a[N], b[N];
int cnt[K],sum[K];
int idx[N];
int n;
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		++cnt[a[i]];
	}
	sum[0] = cnt[0];
	for (int i = 1; i < K; i++)
		sum[i] = sum[i - 1] + cnt[i];

	for (int i = n; i >= 1; i--)//之所以倒着循环是因为,对于相等的元素,
                                //我们是从后向前分配位置,保证排序的稳定性
		idx[i] = sum[a[i]]--;

	for (int i = 1; i <= n; i++)
		b[idx[i]] = a[i];

	for (int i = 1; i <= n; i++)
		cout << b[i] << ' ';
}

有关于计数排序的拓展:基数排序和桶排序,详细的算法描述放在了下面

1.10 基数排序 | 菜鸟教程 (runoob.com)https://www.runoob.com/w3cnote/radix-sort.html1.9 桶排序 | 菜鸟教程 (runoob.com)https://www.runoob.com/w3cnote/bucket-sort.html完结撒花!这里只介绍了常用的几种排序算法,目前我就先学了这几种哈哈哈,等以后拓展一些之后,还会不断更新的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值