递归思想在题目中的应用
1、递归问题分为两类:先递归,再处理(代表就是归并排序);先处理,再递归(代表就是快速排序)。
2、递归思想最好处理的问题就是分治问题,即将一个问题分为多个子问题,每一个子问题解决好了,拼合起来整个问题也就解决好了
3、递归算法在计算机中底层是利用栈的结构来实现的
归并排序思想:
1、归并排序的核心是先分解,分解全部完成后再合并
2、合并的方法采用的是双指针归并法(链表合并本质也是这样)(两两比较并合并)
3、分解采用的是严格中间分解
4、排序的整个流程可以认为是先局部有序,再整体有序
图解归并算法:
一、整体思路:
举个例子,我们要对38,27,43,3,9,82,10这个数组进行归并排序。如下图:
可以看到图分为两个部分首先是上面红色到灰色的部分,然后是灰色到绿色的部分。 我们称前者为分解,后者为归并
1、分解:分解顾名思义就是把整个数据给给他按照二等分的方法,不停分开,总共要分解logn次。分解时就是直接分开并不用任何操作
2、归并:归并就是把每组数据合并起来。合并的过程中要保证每组合并之间的数据是有序的。并且每一层合并对象的类型是相同的
例如第一层绿色合并的对象就是中间灰色,每两个灰色进行大小比较然后两个为一组,组内有序的得到第一层绿色;第二层绿色合并的对象就是第一层的绿色的每一组,仍然是两两有序的去合并,然后得到四个元素的,内部有序的两组数据(右边是奇数特例,逻辑上是4个元素)
如此递归不停合并,最后得到的数组整体就是有序的了。
二、归并方法
了解了归并排序整体的思路以后,最关键的地方就在于如何实现每一组之间的归并。
核心的思路就是:双指针。先让两个指针放在数组开头,然后比较两个指针的值,选择较小的放到另外一个暂时存在的数组中,然后让较小值对应的指针指向该数组的下一个元素。
图解如下:
三、时间复杂度分析:
归并的时间复杂度分析:主要是考虑两个函数的时间花销,一、数组划分函数mergeSort();二、有序数组归并函数_mergeSort();
_mergeSort()函数的时间复杂度为O(n),因为代码中有2个长度为n的循环(非嵌套),所以时间复杂度则为O(n);
简单的分析下元素长度为n的归并排序所消耗的时间 T[n]:调用mergeSort()函数划分两部分,那每一小部分排序好所花时间则为 T[n/2],而最后把这两部分有序的数组合并成一个有序的数组_mergeSort()函数所花的时间为 O(n);
公式:T[n] = 2T[n/2] + O(n);
公式就不仔细推导了,可以参考下: 排序算法之快速排序及其时间复杂度和空间复杂度里面时间复杂度的推导;
所以得出的结果为:T[n] = O( nlogn )
因为不管元素在什么情况下都要做这些步骤,所以花销的时间是不变的,所以该算法的最优时间复杂度和最差时间复杂度及平均时间复杂度都是一样的为:O( nlogn );好像有人说最差的时间复杂度不是O(nlogn),我不知道怎么算出来的,知道的麻烦告知下,谢谢;
空间复杂度
归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)
以时间换空间
我看到网上很多blog分享空间复杂度只有O(1)的归并排序法;因为传统的归并排序所消耗的空间主要是在归并函数(把两个有序的函数合并成一个有序的函数),所以如果要让时间复杂度为 O(1) ,那么也只能在归并函数中做文章了。代码就不列出来了,其主要思想就是借助于快速排序(其实就是相当于归并函数被快速排序函数替换了);这样的方法虽然可以减少内存的消耗,但是却会在时间上带来损失,因为这样时间复杂度却变成了 O(n^2) 了;所以这种方法并不是一个两全其美的idea;
总结
归并排序虽然比较稳定,在时间上也是非常有效的(最差时间复杂度和最优时间复杂度都为 O(nlogn) ),但是这种算法很消耗空间,一般来说在内部排序不会用这种方法,而是用快速排序;外部排序才会考虑到使用这种方法;
代码展示
#include<iostream> using namespace std; int q[100001];//暂时用来存放新的有序数组 void merge_sort(int a[],int l,int r){//归并算法输入:待排序数左右端位置、待排数 if(l>=r)return;//递归退出条件 int mid=(l+r)>>1;//分解中间数 merge_sort(a,l,mid);//分别对两端继续递归分解(不停分左端,再分右端,直到两端都分解好) merge_sort(a,mid+1,r); //实现把数完全分解开,下面去慢慢合并,且合并中都保持局部有序 int i=l; int j=mid+1; int k=0; while(i<=mid&&j<=r){ if(a[j]<a[i])q[k++]=a[j++]; else q[k++]=a[i++]; } while(i<=mid) q[k++]=a[i++]; while(j<=r)q[k++]=a[j++]; //到这里q数组中就放着本轮合并后的有序组,且每一轮的分解都对应一组合并,按照栈方式进行 for (i=l, j=0; i<=r; i++, j++ ) a[i]=q[j]; //上面排序的是a的(l,r)这个区间存在q中,所以要把q中的该区间的有序版本存回a中对应位置 //用于下一层递归合并 } int main(){ int a=0; int num[100001]; scanf("%d",&a); for(int i=0;i<a;i++){ cin>>num[i]; } merge_sort(num,0,a-1); for(int i=0;i<a;i++){ printf("%d ", num[i]); } }