排序合集

排序是算法中很基础的部分,必须要掌握。很多人也做过排序合集并且总结归纳了代码和各个排序方法的思路,看过不少之后我觉得终究还是要靠自己理解消化思想后独立地敲一遍,才能确保真正化为自己知识的一部分。
所以就有了这个合集,记录我自己的学习进度。
总计九个排序:
①选择排序
②插入排序
③冒泡排序
④快速排序
⑤归并排序
⑥shell排序(希尔排序)
⑦堆排序
⑧基数排序
⑨计数排序
代码实现均为升序排序
其中,基数排序和计数排序属于线性时间的排序算法

先放张总结图(来自其他博客)
在这里插入图片描述

①选择排序:
基本思想: 最外层循环每次都会按顺序选定一个数作为“基准”,内层循环则负责从基准数后一位开始,遍历整个数组寻找比基准数小的值,若找到则将该数与基准数交换(此时基准数就被更新成了我们找到的该数)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

void SelectSort(int a[],int l)  
{
	for (int i=0;i<l-1;i++) //外层循环执行到最后一个数据时,整个数组必然已处理完毕,故处理到倒数第二个即可
	{
		for (int j=i+1;j<l;j++)
		{
			if (a[i]>a[j]) swap(a[i],a[j]);
		}
	}
}

int main()
{
	int t[]={10,9,1,3,0,0,7,2,8,22,0};
	int n;
	n=sizeof(t)/sizeof(t[0]);
	SelectSort(t,n);
	for (int i=0;i<n;i++) printf("%d ",t[i]);
	return 0;
}

②插入排序:
基本思想: 将数组分为两部分看待:有序区与无序区,每趟都会从无序区选取第一个数据作为待插入数,然后从有序区末尾开始向前遍历整个有序区寻找插入位置,在插入过程中需要把有序区的数据后移,为待插入数腾出位置。
最初,有序区只有一个数据a[0],单个数据显然是有序的,那么插入数据过程直接从a[1]开始

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

void InsertSort(int a[],int l)  
{
	int wait,j;
	for (int i=1;i<l;i++) 
	{ 
		wait=a[i]; // 获取待插入数据
		j=i;  // j为插入位置,此处初始化
		
		//若待插入数刚好大于已有序序列末尾的数的话,直接插入到其后即可
		if (wait<a[i-1]) //否则就要在已有有序序列里从后往前遍历寻找插入位置 
		{
			while(j>0 && wait<a[j-1])
			{
				a[j]=a[j-1]; // 后移腾空间
				j--; // 继续向前寻找插入位置
			}	
		}
		a[j]=wait; // 插入数据	
	}
}

int main()
{
	int t[]={10,9,1,3,0,0,7,2,8,22,0};
	int n;
	n=sizeof(t)/sizeof(t[0]);
	InsertSort(t,n);
	for (int i=0;i<n;i++) printf("%d ",t[i]);
	return 0;
}

③冒泡排序
基本思想: 就像水中气泡上升,或者"石沉大海"一样,每一趟排序会逐个两两比较数据,大的往后面丢。第一趟将整个数组中最大的值移动到了数组末尾,第二趟将第二大的移动到了数组的倒数第二位,第三趟…以此类推,每一趟都会令一个当前趟数最大的数据沉底,已经"沉底"的就不需要再管它了,因此,遍历趟数越多,遍历数据次数越少。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

void BubbleSort(int a[],int l)  
{
	for (int i=0;i<=l-1;i++)
	{
		for (int j=0;j<=l-i-1;j++)
		{
			if (a[j]>a[j+1]) swap(a[j],a[j+1]);
		}
	}
}

int main()
{
	int t[]={10,9,1,3,0,0,7,2,8,22,0};
	int n;
	n=sizeof(t)/sizeof(t[0]);
	BubbleSort(t,n);
	for (int i=0;i<n;i++) printf("%d ",t[i]);
	return 0;
}

④快速排序
基本思想:基于"分而治之"思想,在数据中选取一个基准数(一般来说是数组最左边的数),保证让基准数左边的数据都比基准数小,右边的都比基准数大,然后将整个数据分成两份,递归,再度重复以上过程
这里的讲解很不错,对理解很有帮助 http://developer.51cto.com/art/201403/430986.htm
每次敲快排时脑子里就浮现出左右哨兵移动的过程…然后自然而然就敲完了

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

void QuickSort(int a[],int l,int r)  
{
	if (l<r)
	{
		int i=l,j=r; //左哨兵和右哨兵
		int key=l; //基准数下标 
		while (i!=j)
		{
			//基准数选在最左边,则右哨兵先行动,选在最右边,则左哨兵先行动
			while (i<j && a[j]>=a[key])	 j--;  
			while (i<j && a[i]<=a[key])  i++;
			if (i<j) swap(a[i],a[j]);
		} 
		swap(a[key],a[i]); //此时左右哨兵已经"碰头"了
		QuickSort(a,l,i-1);
		QuickSort(a,i+1,r);	
	} 
}

int main()
{
	int t[]={10,9,1,3,0,0,7,2,8,22,0};
	int n;
	n=sizeof(t)/sizeof(t[0]);
	QuickSort(t,0,n-1);
	for (int i=0;i<n;i++) printf("%d ",t[i]);
	return 0;
}

⑤归并排序
基本思想:依旧是“分而治之”,通过递归将原序列不断分成左右两个小序列,直到小序列里只有一个数据时,显然是有序的;每次递归回溯过程都会将左右有序小序列合并为一个有序序列,直到还原出一个已经呈现有序的原序列。
代码实现分为两个部分:分割序列和归并序列
在这里插入图片描述

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

void Merge(int a[],int l,int mid,int r) // 合并已经各自排好序的两个序列 
{
	// 在该函数处理前,序列已经是"预合并"状态,即它的长度已经是两个序列合并后的长度了,但当然还不是合并后的有序状态 
	int len=r-l+1; // 两个序列合并后的长度 
	int *tmp=new int[len],cnt=0; // 分配临时数组 
	int i=l,j=mid+1; // 分别为两个序列的起始处理点
	while (i<=mid && j<=r)
	{
		tmp[cnt++]=a[i]<=a[j]?a[i++]:a[j++]; //升序排序,把较小的放入临时数组	
	}
	while (i<=mid)	tmp[cnt++]=a[i++];
	while (j<=r)  tmp[cnt++]=a[j++];	  
	for (int k=0;k<len;k++)	a[l++]=tmp[k]; // 借助临时数组,让序列从"预合并"状态过渡到真正合并 
}

void MergeSort(int a[],int l,int r)
{
	if (l==r) return; // 递归到序列里只有一个数字了,显然有序 
	int mid=(l+r)>>1; 
	MergeSort(a,l,mid);
	MergeSort(a,mid+1,r);
	Merge(a,l,mid,r);
}

int main()
{
	int t[]={10,9,1,3,0,0,7,2,8,22,0};
	int n;
	n=sizeof(t)/sizeof(t[0]);
	
	MergeSort(t,0,n-1);
	
	for (int i=0;i<n;i++) printf("%d ",t[i]);
	return 0;
}

⑥shell排序(希尔排序)
基本思想:希尔排序又称为"缩小增量排序",可以认为是插入排序的升级版。
回忆插入排序,我们在插入数据的过程中是按部就班地,一个一个向前遍历数据来寻找插入位置,但这样很慢:比如对初始序列进行升序排序时,若整个序列里的最小值最初位置在序列末尾,那么它也经过一个一个查找插入位置的过程才能回到它该待的“第一位”这个位置。为什么我们不能试试“跳跃式”地查找插入位置呢?也就是每隔一定间隔来查找插入位置,并且将相同间隔的数据称为“组”。
我们肯定明白只靠一次“跳跃式”是无法让整个序列有序的,那么我们就让每次“跳跃量”都减小,直到最后一次跳跃式查找过程时,间隔变为1,也就变成了普通插入排序的方法,但此时经过我们前几趟的操作,序列已经基本有序了,这时花费的时间不会再像刚拿到初始序列就进行直接插入排序那样多。
关于最完美的“跳跃量”的选取,在数学上是一个难题,我们在这里仅用最常用的“跳跃量”,也就是希尔增量(尽管它不是最优的)来实现代码,即初始gap=整个序列数据数量/2,之后继续为gap=gap/2这种形式。
在这里插入图片描述

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

void ShellSort(int a[],int l,int r) // 代码和上面的讲解图是吻合的
{
	int gap=(r-l+1)>>1; // gap即为增量,此处为初始化
	while(gap)
	{
		for (int i=l;i<=l+gap-1;i++) //控制当前在处理的是哪一“组”
		//这里的“组”指的就是上面讲解图里颜色相同的数据的集合,也就是说图里一种颜色就代表一个“组”
		{
			for (int j=i;j<=r;j+=gap) // 处理该组里的数据,它们都以gap为间隔
			{
				int now=j;
				while (now-gap>=l && a[now]<a[now-gap]) //就像插入排序里的插入过程一样的意义,但我们在这里用的是交换
				{
					swap(a[now],a[now-gap]); 
					now-=gap;
				}
			}
		}
		gap/=2;
	}
}

int main()
{
	int t[]={10,9,1,3,0,0,7,2,8,22,0};
	int n;
	n=sizeof(t)/sizeof(t[0]);
	
	ShellSort(t,0,n-1);
	
	for (int i=0;i<n;i++) printf("%d ",t[i]);
	return 0;
}

⑦堆排序
这是一个要求前置知识点比较多才能完全理解的排序
讲堆排序之前必须讲清楚这些概念:二叉树、完全二叉树(近似满二叉树)、堆

  1. 二叉树:每个结点至多有2个儿子结点,并且明确地分成了左儿子右儿子
    二叉树具有的重要性质:
    ①高度为h的二叉树至少得有h+1个结点
    ②高度为h的二叉树至多有2^(h+1)-1个结点
    ③含有n>=1个结点的二叉树高度至多为n-1(这种情况就像一条线…)
    ④含有n>=1个结点的二叉树高度至少为⌊logn⌋(⌊⌋代表向下取整,且log底数为2)
    满二叉树是高度为h,结点有2^(h+1)-1个的二叉树(简而言之就是“放满了”)
  2. 完全二叉树(近似满二叉树):如下图
    在这里插入图片描述
    用最简单的讲法就是满足2个性质:
    ①除了最底端的那层之外,其他层全部放满了
    ②最底端的元素都必须从左边开始放,优先放左边,然后再是右边,绝对不允许出现左儿子结点还没放,右儿子结点却放好的状态
    另外,满二叉树是属于近似满二叉树的。
  3. 堆:具有优先级性质的完全二叉树(在这里,你可以把优先级理解成具体数值的大小)
    设二叉树的一个结点存储一个元素
    极大化堆(大根堆或者大顶堆):任一结点所存元素的优先级不小于其儿子结点所存元素的优先级
    极小化堆(小根堆或者小顶堆):任一结点所存元素的优先级不大于其儿子结点所存元素的优先级
    在这里插入图片描述
    (a)为极大化堆, (b)为极小化堆
    简单来说就是:极大化堆的根节点优先级最大,离根节点越远的结点优先级越小;极小化堆的根节点优先级最小,离根节点越远的结点优先级越大

堆排序基本思想:输入一个无序序列,第一步将其整理成大根堆(想要降序排序就整理成小根堆),然后将根节点存储的元素(已经是序列元素中的最大值)和序列末尾的元素交换,此时已经找到了有序序列的最大值,故将这个最大值从堆中“剔除”,再通过第二次整理,令堆中剩下的元素重新满足大根堆定义,此时根节点存储的元素就是序列元素中的次大值,再把它和序列末尾的元素交换,将次大值从堆中“剔除”,再开始下一次整理重新满足大根堆…重复以上过程直到序列有序。

举例说明: 初始无序序列为arr,表示成二叉树形式如下
在这里插入图片描述
整理成大根堆的过程:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述至此第一次整理完成,大根堆已构建
找到序列最大值,和arr序列末尾元素交换后“剔除”
在这里插入图片描述
第二次整理,重新构建大根堆,交换并“剔除“序列次大值
在这里插入图片描述
在这里插入图片描述
以此类推直到序列有序。
在这里插入图片描述
算法思想参考自博客白话讲排序系列(六) 堆排序

有缘一定补上(指期末考后十几天内)

⑧基数排序
基本思想:大白话:对输入的无序序列的元素,对其个位数排序→十位数排序→百位数排序→…有多少位就排序到哪位,并且必须注意每一位排序都必须建立在前一位排序结果的基础上。
举例说明: 设无序序列为:73 22 93 43 55 14 28 65 39 81

对个位数排序:
在这里插入图片描述
对十位数排序:(建立在个位数排序结果的基础上)
在这里插入图片描述
得到有序序列:14 22 28 39 43 55 65 73 81 93

有缘一定补上(指期末考后十几天内)

⑨计数排序
基本思想:输入一个无序序列,对序列中的每个元素,如果我们已经明白了有多少个元素比它小,那么是不是就能够直接将它放到最终输出的有序序列的正确位置上?例如对一个元素x,如果输入序列里有17个元素小于x,那么x就应该放在第18个输出位置上,实现这个可以用一个计数用数组c[],并且以无序序列中的最大值作为它的容量。但是若序列里有多个相同的元素时,当然不能把它们都放在同一个输出位置上,必须作相应处理。

举例说明:输入无序序列a[]={1,5,3,7,6,2,8,9,4,3,3}

a[]15376289433
下标012345678910

由于序列中的最大值为9,我们可以确定c[]的容量为9,在计数前我们得初始化c[]

c[9]00000000000
下标012345678910

用c[]记录每一个元素在无序序列里出现的次数(1出现了1次,3出现了3次,c[1]=1,c[3]=3,以此类推)

c[9]0113111111
下标0123456789

为了得到一个元素在最终输出的有序序列上的位置,c[]必须以一种方式自我更新:即c[i]+=c[i-1]
此时c[i]中的值的意思是,a[]中小于或等于i的元素个数
如小于或等于1的元素只有1个,小于或等于3的元素有5个(1,2,3,3,3)

c[9]012567891011
下标0123456789

最后再依照c[]输出最终的有序序列b[]

时间复杂度分析:(因为总结图里没有计数排序)
首先,对c[]数组的初始化需要O(m)的时间,m为无序序列的最大值
顺序检查a[n]中每个元素,记录每个元素出现的次数需要O(n)的时间
c[]数组自我更新需要O(m)的时间
将无序序列元素整合成有序并输出到b[]需要O(n)时间
综上所述,计数排序的时间复杂度为O(m+n)

#include<cstdio>
#include<algorithm>
using namespace std;
int maxval=-2147483647,n;
int *a,*b,*c; 

void CountSort(int a[],int b[],int n)
{
	for (int i=0;i<=maxval;i++) c[i]=0;
	for (int i=0;i<n;i++) c[a[i]]++;
	for (int i=1;i<=maxval;i++) c[i]+=c[i-1];
	for (int i=n;i>0;i--)  //一定要反向遍历,否则不稳定
	{
		b[c[a[i-1]]-1]=a[i-1];
		c[a[i-1]]--;
	}
}

int main()
{
	scanf("%d",&n);
	a=new int[n];b=new int[n];
	for (int i=0;i<n;i++)
	{
		scanf("%d",&a[i]);
		if (a[i]>maxval) maxval=a[i];
	}
	c=new int[maxval+1];
	
	CountSort(a,b,n);
	
	for (int i=0;i<n;i++) printf("%d ",b[i]);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值