一、排序的运用
生活中排序随处可见,比如我们高考时的排名,大学学校水平的排名等,打开京东,可以发现每样商品按照不同的方式排序,比如综合,销量,价格。其内部需要排序代码来完成。
二、常见的排序算法
一、交换排序
一、冒泡排序
冒泡排序是一种最容易想到的排序,但是其效率不高,没有实践意义。冒泡排序的原理是把一个数组内的元素两两相比,以排升序为例,若前一个比后一个大,不符合升序要求,便将这两个元素交换。以下是冒泡排序的原理图:
代码的实现如下:
时间复杂度:O(N^2),效率十分底下,因此冒泡排序并没有实践意义,但是对初学者提高算法能力具有教学意义,初学者通过对冒泡排序的编写可以很好的提高代码能力。
二、快速排序
一、递归版本
图:
原理:把key放左边,right先走,找比key小的值;然后left再走,找比key大的值;left和right的值交换;然后right再走,再找比key小的值;left再走,再找比key大的值,再交换;直到left和right相遇,然后把key和相遇点(称为meet)的值交换,完成一趟。然后递归完成[begin,meet-1]和[meet+1,end]的排序。
代码实现:
时间复杂度:O(N*logN)
两种提高效率的优化:
1、三数取中:如果排的数据已经有序,那么次算法的时间复杂度会退化到O(N^2),其次,每一趟排好的数据接近中间时,这个排序算法的效率会比较高。因此,有人想出了一个三数取中的办法对快速排序进行优化,其原理是把left,right,以及(left+right)/2进行比较,选出中间大小那个,把中间大小那个与left交换,这样能提高key那个数据大小在中等大小的概率。
2、小区间优化:如果数据量比较小(小于10)时,快排的速度并不比O(N^2)的排序算法快,所以在数据量小于10时,选择使用O(N^2)中最快的排序方法插入排序来排,可以有效的提高效率。
优化后的代码:
递归缺陷:只要是递归就会有栈溢出的风险。
二、非递归版本
上述递归方法的单趟不易理解,还容易错,于是有人便提出了一种虽然没有提升效率但是更容易让人理解,也更不易出错的办法,前后指针(这俩种方法与递归非递归无关,递归和非递归的单趟都可以用)。
图:
原理:key在左边,cur(初始值为key+1)往后遍历,若找到小prev(初始值为key)++,然后与cur交换,cur++;若找到大,cur++;cur超过right后,把prev和key交换。
非递归原理:用栈模拟递归。把left和right压入栈中,排完一次后把有效的左右区间的left和right存入栈中。直到栈为空。
代码实现:
二、插入排序
一、插入排序
原理:就像打斗地主摸牌插入一样,把摸到的牌放到指定位置。以排升序为例:把[0,end]视为有序,先把end+1下标的数据放到一个临时变量tmp中,若tmp小于end下标对应的数据,把end位置的数据放到end+1位置,end--;若tmp大于end下标的数据,就把tmp插入到end+1位置。看图:
代码实现:
这里有一个需要注意的点:while循环不一定是break出去的,也有可能tmp比所有数据都要小end==-1出去的,所以a[end + 1] = tmp如果写在else里面的话,外面要加一层判断。
时间复杂度:显然,是O(N^2),虽然与冒泡属于同一个量级,但其效率比同量级的冒泡高出许多。
二、希尔排序
上文所述的插入排序,时间复杂度为O(N^2),效率不高,但是有一个叫希尔(D.L.Shell)的人,他很牛逼,认为插入排序这种思想很好,于是设计出了一套算法对插入排序进行了优化。先说结论,优化过后希尔排序的时间复杂度大约为O(N^1.3)(具体是多少目前众说纷纭,但是每种说法的大小都在O(N^1.3)附近),优化过后可以和时间复杂度为O(N^logN)排序方法的相提并论了。
(希尔的照片)
原理:
希尔当时想,插入排序在数据接近有序的时候效率很高,那么能不能先进行预排序,让数据接近有序,再使用插入排序把数据排成有序的,这不就很快了吗?那么怎么预排序能让数据一步步接近有序呢?
希尔想了一个办法(不知道他怎么想出来的),把n个数据分成gap组,每组组内进行插入排序,这样每排完一次数据就会更接近有序,每排完一次把gap的值进行缩小,这样就会越来越接近有序,到最后gap为1时恰好就是一次插入排序,排完直接有序。看图:
(图片来源:希尔排序 | 菜鸟教程 (runoob.com))
有专家研究过,(研究过程对我们使用者来说意义不大)gap每次除3的效率优于每次除2的效率,因此每次除3好一点,但是除3会有一个问题,就是最后一次gap的值不一定是1,这样的话要在外面额外多写一次插入排序,为了代码的简洁性,每次除3后加1便可以保证最后一次gap一定是1。
代码实现:
时间复杂度:O(N^1.3),计算很复杂,只需要记住O(N^1.3)这个结论就好。
三、选择排序
选择排序的原理是把最大的选出来放最后,最小的选出来放最前面。通过遍历找最大最小的数的方法便是直接选择排序,通过建堆选出最大的数的方法便是堆排序。
一、直接选择排序
原理:遍历一次在[begin,end]的范围内找最大的和最小的数,找到以后记录最大最小数据的下标,然后把最小的放左边,最大的放右边,begin++,end--,继续遍历。
代码:
直接这样写的话会出bug,因为如果maxi==begin的话这样换会把最小值换到最后,所以如果maxi==begin要先把两句Swap换一下位置才能正确;可是如果mini==end的话上图中Swap的顺序是对的,所以要进行分类讨论,注意上述两种情况可能同时发生,也可能同时不发生,所以一共四类情况,同时不发生的情况与有且仅有一种发生的情况可以使用一样的代码实现,所以,下图我分了三种情况:
时间复杂度:两个循环嵌套,O(N^2),因此基本不用。
二、堆排序
原理:升序建大堆,降序建小堆,每次把堆顶数据与堆的最后一个数据交换,然后再把数据向下调整为堆,循环到堆内只剩一个数据结束。
代码:
时间复杂度:O(N*logN)。
四、归并排序
一、归并排序
一、递归版本
图:
原理:如果左半边的数组有序且右半边的数组也有序,那么可以通过创建一个新的等大的数组,左右的数据比较,哪个小哪个就尾插到新数组中,然后该指针++。最后把数组拷贝回去就可以了。要让左右都有序可以使用递归。
时间复杂度:O(N*logN)。
二、循环版本
原理:先两两归并,再四四归并,再八八归并,依此类推。
还要处理一下越界
两个区间为 [begin1,end1] [begin2,end2],end2>begin2>end1>begin1,越界有三种情况:
1、只有end2越
2、end2和begin2越
3、end1和begin2和end2都越
23种情况直接不用归了,break出去
第1种情况只要把end2改成n-1就可以了
时间复杂度:O(N*logN)。
标签:end,递归,复杂度,C语言,key,排序,插入排序 From: https://blog.csdn.net/2401_83318090/article/details/139282616