常见的排序

Posted 语风之

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常见的排序相关的知识,希望对你有一定的参考价值。

文章目录

常见的排序算法

插入排序

基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

1. 直接插入排序

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。(默认数组第一个元素已经完成元素插入)

例:插入1,1<=3,3后移;1<=2,2后移;1放入2的位置。
tip:
(记得用临时变量保存待插入元素的值,否则会在元素后移时被覆盖)

算法分析:

应用场景:数据接近有序或数据量比较少
平均时间复杂度:O(N^2),标准的内外两层循环
最好的时间复杂度:O(N),若有序,则每个元素不需要再找待插入元素的位置,只需要遍历1遍。
最坏的时间复杂度:O(N^2)
空间复杂度:O(1),需要1个临时变量保存待插入元素的值
稳定性:稳定

代码:

void InsertSort(int* a, int n)
	//对剩余的n-1个元素进行插入排序
	for (int i = 1; i < n; i++)
		//用temp记录待插入的元素,待会儿会被覆盖
		int temp = a[i];
		//end记录已排好序的最后的一个元素
		int end = i - 1;
		//找待插入元素的位置
		while (end >= 0 && a[end]>temp)
			a[end + 1] = a[end];
			end--;
		
		//插入元素
		a[end + 1] = temp;
	

2. 希尔排序

算法思想:
考虑对数据进行分组,将数据进行一定的间隔分组,每个数组应用插入排序。

先选定一个整数gap,把所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。重复上述分组和排序的工作。当gap到达=1时,所有记录在统一组内排好序。

代码:

//在插入排序的基础上实现的
void ShellSort(int* a, int n)
	assert(a);
	int gap = n;
	//当gap到达=1时,所有记录在统一组内排好序。
	while (gap > 1)
		gap = gap / 3 + 1;
		//分组+对每组进行插入排序
		for (int i = gap; i < n; i++)
			int temp = a[i];
			int end = i - gap;
			while (end >= 0 && a[end]>temp)
				a[end + gap] = a[end];
				end-=gap;
			
			a[end + gap] = temp;
		
	

算法分析:

平均时间复杂度:大量实验的基础上得到约为O(N^1.3), 而 Knuth提出在O(N^1.25)和 O(1.6*N^1.25)之间。
最好的时间复杂度:和增量序列的选取有关。
最坏的时间复杂度:O(N^2)
空间复杂度:O(1)
应用场景:数据量非常大 并且 数据非常杂乱
稳定性:不稳定

选择排序

基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

1. 直接选择排序

在元素集合 a[0]–a[n-1] 中选出选出最小的排在最前面,最大的排在最后面。在剩余的元素集合 a[1]–a[n-2] 中再分别选出次最大、最小,重复操作到只剩1个元素或没有元素了。

代码:

void SelectSort(int* a, int left, int right)
	//[begin,end],待选择最小,最大值的数组范围
	int begin = left,end = right - 1;
	while (begin < end)
		int max = begin;
		int min = begin;
		//1、选出最大的、最小的
		//tip:更新以后选择次大,次小的,范围要随着end,begin变化
		for (int i = begin; i <= end; i++)
			if (a[max] < a[i])
				max = i;
			
			if (a[min]>a[i])
				min = i;
			
		
		//2、最小的排在最前面,最大的排在最后面
		swap(&a[min],&a[begin]);
		//如果max在begin,此时begin中的元素已交换到a[min],更新max
		if (max == begin)
			max = min;
		
		swap(&a[max],&a[end]);
		//3、更新begin,end,即,循环选出次大的,次小的
		++begin;
		--end;
	


算法分析:

平均时间复杂度:O(N^2),双层循环
空间复杂度:O(1),创建了begin,end,max,min临时变量
应用场景:数据量非常大 并且 数据非常杂乱
稳定性:不稳定。(7) 2 5 9 3 4 [7] 1 当我们利用直接选择排序算法进行排序时候,(7)和1调换,(7)就跑到了[7]的后面了,原来的次序改变了,这样就不稳定了。
缺陷:直接选择排序会将前面比较过的元素重复比较

2. 堆排序

建小堆,将根节点元素与最后1个节点的元素交换,再对剩下的元素进行堆向下调整。重复操作至只剩下一个元素,得到降序序列。解决了直接排序的元素重复比较问题。

代码:

void HeapSort(int* a, int n)//堆排序
	//1、建小堆,从最后1个非叶子节点开始
	for (int i = n / 2 - 1; i >= 0; i--)
		AdjustDown(a,n,i);
	
	//2、堆排序
	//end为数组元素个数
	int end = n ;
	//调整到只剩一个元素
	while (end>1)
		//交换队尾和队头元素
		swap(&a[0], &a[--end]);
		//从队头开始向下调整end个元素
		AdjustDown(a,end,0);
	

算法分析:

建堆平均时间复杂度:O( N )
堆排序平均时间复杂度:O( N logN )
空间复杂度:O(1)
稳定性:不稳定。因为交换时是隔着元素进行交换。

交换排序

基本思想:
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置

1. 冒泡排序

比较 n-1 圈,每一圈将更大值往后交换,最后将元素集合 a[0]–a[n-1] 中的最大值交换到最后一个元素处

代码:

void BubbleSort(int * a, int n)
	assert(a);
	int end = n;
	    //1、比较n-1圈
	while (end-1)
		//设置一个标志位,若每回比较时有交换数据,则flag=1,表示当前数据序列仍无序;若flag==0,则当前序列已经有序。
		int flag = 0;
		int i = 0;
		//2、每圈比较end-1次,将更大值往后交换。最大值交换到最后1个元素处
		for (i = 1; i<end; i++)
			if (a[i - 1]>a[i])
				swap(&a[i - 1], &a[i]);
				flag = 1;
			
		
		if (0 == flag)
			break;
		
		end--;
	

算法分析:

平均时间复杂度:O( N^2 ),嵌套双循环
最好时间复杂度:O( N ),元素有序,只循环1次退出
空间复杂度:O(1)
稳定性:稳定。

2. 快速排序

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于等于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

快速排序框架:

void quikSort(int* arry, int left, int right)
	if (right - left <= 1)
		return;
	
	//a[flag]为基准值
	int flag = Partion(arry, left, right);
	//元素均小于等于基准值的左子序列
	quikSort(arry, left, flag);
	//元素均大于基准值的右子序列
	quikSort(arry, flag + 1, right);

算法分析:

单个分割算法时间复杂度:O( N )
快速排序平均时间复杂度:O( NlogN )
稳定性:不稳定。元素间间隔着距离交换

1. hoare法

分割前选最左侧元组作为基准值key,end从后往前找,找到小于等于key的值;begin从前往后找大于key的值。交换begin和end位置的元素。重复以上操作直到begin和end相遇,在该位置放入基准值的值。

tip:
a[left]为基准值时,要先从右往左找,这样在最后一次查找时指针相遇的位置为比key小的值,end若未找到比key小的值而与begin相遇,这里的值小于基准值,可以交换a[left],a[begin];若end找到比key小的值,而begin没有找到比key大的值而相遇,此时,这里的值还是小于key,可以交换a[left],a[begin]。若先从左往右找,则最后一次相遇位置的值可能大于基准值a[left],不能交换
*/

代码:

//hoare版本,返回之类型为int,返回基准值的下标
int Partion1(int* a, int left, int right)
	assert(a);
	int begin = left;
	int end = right - 1;
	int key = a[begin];//基准值,从a[begin],a[end]中选一个
	while (begin<end)
		//1、从右往左找,找到小于等于key的值
		while (begin<end && a[end]>key)
			end--;
		
		//2、从左往右找,找到大于key的值
		while (begin<end && a[begin] <= key)
			begin++;
		
		//3、若此时位置的值不等于基准值,则交换值
		if (begin<end)
			swap(&a[begin], &a[end]);
		
	
	//4、将基准值放入刚才的位置
	if (key != a[begin])
		swap(&a[left], &a[begin]);
	
	//5、返回基准值的新位置
	return begin;

2. 挖坑法

a[begin]为基准值key时,end从后往前找大于等于key的值,找到就停下,将该元素放入begin,end此时成为新的坑位。begin从前往后找小于key的值,找到就停下,将该元素放入end,begin此时成为新的坑位。重复以上操作直至begin与end相遇,该位置放入基准值key。

代码:

//挖坑法
int Partion2(int* a, int left, int right)
	assert(a);
	int begin = left;
	int end = right - 1;
	int key = a[begin];
	while (begin<end)
		//1、从后往前找小于等于key的值,放入a[begin],a[end]为新的坑位
		while (begin<end && a[end] > key)
			end--;
		
		if (begin<end)
			a[begin] = a[end];
			begin++;
		
		//2、从前往后找大于key的值,放入a[end],a[begin]成为新的坑位
		while (begin<end && a[begin]<=key)
			begin++;
		
		if (begin<end)
			a[end] = a[begin];
			end++;
		
	
	//3、用基准值填最后end(或begin)的位置的值
	a[begin] = key;
	//4、返回基准值的位置
	return begin;

3. 前后指针法

以a[right-1]作为基准值key,设置前后指针prev,cur,cur在前,prev在后。当cur、prev一前一后时,表明cur,prev所指向的元素都小于等于基准值;若cur,prev之间有距离,(prev,cur)中保存的都是大于基准值的值;若a[cur]<=key并且cur,prev之间有距离时,++prev,交换a[prev],a[cur],使(prev,cur)中保存的都是大于基准值的值。

代码:

int Partion3(int* a, int left, int right)
	int cur = left;
	int prev = cur - 1;
	int key = a[right - 1];
	while (cur<right)
		//prev与cur之间保存大于基准值的值
		if (a[cur] <= key && ++prev != cur)
			swap(&a[prev], &a[cur]);
		
		cur++;
	
	/*不需要这一步,因为在最后一次交换中,若cur,prev之间有距离,则a[right-1]已经被交换到++prev处;若cur,prev之间没有距离,则prev直接标记a[right-1]。
	if(++prev!=right-1)
	swap(&a[prev],&a[right-1]);
	*/
	//返回基准值的位置
	return prev;

归并排序

基本思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

划分子序列到每个子序列最多有1个元素,采用递归处理左侧,递归处理右侧的方法。将左侧与右侧子序列合并,按升序放入temp。在合并之后及时将temp中的元素放回原数组a.

代码:

//左闭右开区间,[left,right)
void MergeSort(int* a, int left, int right, int* temp)
	//递归出口:当至多有1个元素时,返回
	if (right -left<= 1)
		return;
	
	int mid = left + ((right- left) >> 1);
	//2、递归处理左侧,递归处理右侧,此时[left,mid),[mid,right),mid在左侧无法取到,在右侧可以取到
	MergeSort(a,left,mid,temp);
	MergeSort(a,mid,right,temp);
	//3、合并,[begin1,end1),[begin2,end2)
	int begin1 = left,end1=mid;
	int begin2 = mid,end2=right;
	int index = left;
	//a.将左侧与右侧合并,按升序放入temp
	while (begin1 < end1 && begin2 < end2)
		if (a[begin1] <= a[begin2])
			temp[index++] = a[begin1++];
		
		else
			temp[index++] = a[begin2++];
		
	
	//b.将左侧或右侧中剩下的元素放入temp
	while (begin1 < end1)
		temp[index++] = a[begin1++];
	
	while (begin2 < end2)
		temp[index++] = a[begin2++];
	
	//4、将temp中的元素及时放回数组a,[left,right),(right-left)为right与left之间的有效元素个数
	memcpy(a+left,temp+left,(right-left)*sizeof(int));


算法分析:

平均时间复杂度:O( NlogN )
空间复杂度:O(N),需要辅助空间临时存储元素
稳定性:稳定。

代码链接:

点击获取源码。

以上是关于常见的排序的主要内容,如果未能解决你的问题,请参考以下文章

常见排序算法——快速排序

几个常见的排序算法

算法学习——递归之快速排序

快速排序-Python

Java中常见的排序方式-快速排序(升序)

数据结构&算法-快速排序