各种排序算法详解集合(时间复杂度、空间复杂度、稳定性分析)

动图来源:https://blog.csdn.net/weixin_41190227/article/details/86600821

目录

一、冒泡排序

二、选择排序

三、插入排序

四、希尔排序

五、归并排序

六、快速排序

七、堆排序

八、计数排序

九、桶排序

十、基数排序


一、冒泡排序

image

冒泡排序的名字是根据排序算法的特性得出的,每一个元素,像一个气泡,从最初的起始位置,一步步冒到最终位置。

冒泡每次交换相邻的两个元素,(假设是从小到大排序),每一轮冒泡,起码可以把最大的元素,从最初位置置换到最后位置,一路上还顺带着交换了一些不满足从小到大的元素,所以每个元素并不都是在一轮冒泡中到位的,也可能是经过了几轮的铺垫,在第K轮冒泡过程,元素中第K大的数才就位。

时间复杂度:

最差的情况下,元素是从大到小的,而目标是从小到大,所以需要O(n^2)复杂度。

最优的情况下,元素本身就是有序的,但是我们仍需冒一次泡判断出这个序列本身是有序的,时间复杂度O(n)

    具体操作:加一个标记变量表示是否产生了交换操作,若产生交换操作,则说明此时序列未达到有序,则需继续执行冒泡,若无交    换操作说明有序,提前结束。我们在执行每一轮冒泡时,都检查下这个标记变量,即可提前退出。 若元素初始有序,则一次冒泡就可判断出这个序列有序,直接退出,时间复杂度O(n).

平均复杂度;O(n^2)。

空间复杂度:

冒泡排序的空间复杂度就是在交换元素时那个临时变量所占的内存空间,只需交换两个元素,所以为O(1)

稳定性:

交换操作执行时并不会交换相邻的相等元素,所以是稳定的。

代码:

	for (int i=0; i<n-1; i++){	 //一共需要冒泡n-1次
		for (int j=0; j<n-i-1; j++){  //每次都会将最大(小)的元素到最后
			if (a[j]>a[j+1]) swap(a[j],a[j+1]);
		}
	}

 

二、选择排序

image

每次从未排序序列中找到最小的,把它和未排序序列的第一个元素交换,执行N-1次。

如何每次从未排序序列中找到最小的?

假设未排序的第一个元素是我们要找的最小值,用它和未排序序列的每一个进行比较,若某个元素比它更小,则用此元素替换,产生新的最小值。

时间复杂度:

因为选择是没有顺序的,所以选择排序不能提前退出,不管是什么序列,都需要执行完整的排序过程。

最优:O(n^2)

最差:O(n^2)

平均:O(n^2)

空间复杂度:

不需要额外的空间占用,所以为O(1)

稳定性:

每次找到的最小值,要和未排序的序列的第一个元素交换,这个操作是不稳定的。

例子:2 4 2 1 3

在进行第一次选择排序操作时,红色的2被假定为最小值,之后的比较操作发现1才是最小值,所以红2和1交换,此时红2和蓝2的相对位置发生改变,所以选择排序是不稳定的。

代码:

   for (int i=0; i<n-1; i++){ //0~i是有序的,i+1~n-1是未排序的 
        int min=i; //假设i是最小值 
        for (int j=i+1; j<n; j++) //从未排序的元素中找到最小的 
        	if (a[j]<a[min]) min=j;    
        swap(a[min], a[i]); //交换 
    }

 

三、插入排序

image

构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序是一种内排序,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序对于基本有序数据较少的序列很高效。

时间复杂度:

最优:O(n)    【假如原来就是有序的,每个元素只需和排好序的最后一个元素比一下即可放置,进行下一个数的比较】

最差:O(n^2)

平均:O(n^2)

空间复杂度:

不需要额外的空间占用,所以为O(1)

稳定性:

如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

代码:

//插入排序 
void InsertSort(int a[],int l,int r){
	for (int i=l+1; i<=r; i++){
		int num=a[i];
		int j=i;
		while (j-1>=l && num<a[j-1]) {
			a[j]=a[j-1];
			j--;		
		}
		a[j]=num;
	}
}

 

四、希尔排序

首先,我们要知道,插入排序的数据规模越小,或者越接近有序,他的效率是越高的。

基于这个结论,希尔改进了插入排序,创造了希尔排序。【希尔排序是插入排序的一种,也叫做缩小增量排序】

具体改进如下: 先将序列在逻辑上分组,分组靠的是增量。因为分组导致每个小组的数据规模较小,所以插入排序效率高;当此次分组的排序结束后,减小增量,此时每个组的数据规模会增加,但是因为有小分组排序的铺垫,这新的分组有序性会很高,插入排序效率也很高。所以完美的利用分组把插入排序的优势展现出来,当增量变为1的时候,数组就排好序了。

image

 

时间复杂度:

最坏的情况下是O(n^2),一般情况下是O(nlogn)~O(n^2)之间。【不太确定】

空间复杂度:

不需要额外的空间占用,所以为O(1)

稳定性:

插入排序是稳定的,但是分组之后再插入排序的希尔排序是不稳定。

 

五、归并排序

image

归并排序是一种基于分治思想的排序。

先将序列分为长度相等的两个子序列,对子序列排好序以后,再合并。

对子序列的排序,则调用递归实现。

 

时间复杂度:

最优:O(nlogn)    

最差:O(nlogn)

平均:O(nlogn)

归并排序的时间复杂度是稳定的,不管数据类型如何,对于n个数据,需要分成logn层,而合并每一层的数据,都需要O(n)的时间,所以时间复杂度稳定为O(nlogn)。

空间复杂度:

归并排序需要额外的空间占用,也算是他的缺点吧,空间占用为O(n)

稳定性:

归并排序的出口只有1个元素,或者2个元素的时候,我们都可以在这里保证他们的稳定性,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。

代码实现:

最简练写法实现归并排序【C++代码】

 

六、快速排序

image

首先挑选一个基准,通过一趟排序,把小于基准的数据放到前面,大于基准的数据放到后面,这样就把数据分成了以基准为界的两个子序列,对两个子序列再进行相同操作。

时间复杂度:

最优:O(nlogn)

最差:O(n^2)  【在挑选标准时,恰好每次抽中的都是最大值或者最小值,快速排序则退化为选择排序这种,最差为n^2】

平均:O(nlogn) 【数据分组要进行logn次,并且每次都需要对序列遍历一遍以确定是否需要调位置,所以时间复杂度为nlogn】

空间复杂度:

     首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;

     最优的情况下空间复杂度为:O(logn)  ;每一次都平分数组的情况

     最差的情况下空间复杂度为:O( n )      ;退化为选择排序的情况

稳定性:

如待排序的序列为[....5(a),5(b),......,4,....]。当我们选到4为基准值,那么前面的两个5最终是要到4后面的,并且一定是5(a)先到,因为 i 是从小到大变化的,j 是从大到小变化的,必然导致两个5的相对顺序改变。

代码:

快速排序的代码实现【较为容易理解的实现方法】

 

七、堆排序

image

步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
步骤2:将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
步骤3:由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

时间复杂度:

因为选择是没有顺序的,所以选择排序不能提前退出,不管是什么序列,都需要执行完整的排序过程。

最优:O(nlogn)

最差:O(nlogn)

平均:O(nlogn)

空间复杂度:

不需要额外的空间占用,所以为O(1)

稳定性:

堆的结构是节点i的孩子为 2i 和 2i+1 节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程,首先要根据floyd算法建堆,因此要从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2,...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
       eg:{5A,6,5B,7,8} --> {8,7,5B,5A,6} ,两个5的顺序颠倒了。

代码:

堆的性质、堆的实现、堆排序

 

八、计数排序

image

通过另外一个数组的地址表示输入元素的值,数组的值表示元素个数的方法来进行统计,从而达到排序效果。

计数排序 的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

计数排序(Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。

时间复杂度:

最优时间复杂度、最差时间复杂度、平均时间复杂度都是O(n+k)   k是数据范围。

当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序。

空间复杂度:

O(Max-Min+1)

稳定性:

       应该不稳定,同一个数字进入同一个数组空间,再次分离的时候没有了先后顺序。

 

九、桶排序

 

image

 桶排序 是计数排序的升级版。

桶排序 (Bucket sort)的工作的原理:
假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序。

桶排序,在大数据量的情况下排序,比快速排序还要快。若待排序的数据元素个数比较少,桶排序的优势就不是那么明显了,因为桶排序就是基于分而治之的策略,可以将数据进行分布式排序,充分发挥并行计算的优势。

时间复杂度:

平均时间复杂度是O(N+N*logM),其中,N表示桶的个数,M表示桶内元素的个数(这里,M取的是一个大概的平均数,这也说明,为何桶内的元素尽量不要出现有的很多,有的很少这种分布不均的事情,分布不均的话,算法的性能优势就不能最大发挥)。

空间复杂度:

O((max-min)/arr.length+1)

稳定性:

      桶内实现稳定排序,桶排序就是稳定的。

 

十、基数排序

image

 

多关键字的思想:给定一组数据,我可以先按个位的大小对所有数进行排序,然后再按十位进行排序,一直到最高位,这样就可以使整组数据变得有效,这样从最低位开始的方法称为最低位优先。

基数排序需要两个辅助空间,一个是0~9号桶,另一个是计算定位的数组,定位数组是干什么的呢?就是记录每个桶中的数据待会要放回原数组的哪个位置。

时间复杂度:

基数排序的时间复杂度可以理解为O(d*n),d为序列中最大的位数,适用于n值很大,但是关键字较小的序列。

空间复杂度:

O(n+10+10)

稳定性:

        基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

  • 5
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值