分治算法虽然听起来有可能有点陌生,但是实质上绝对是见过。比如在有序顺序表中的二分查找就是分治法的其中一个实例。它的时间复杂度经常性能把一个问题从O(n)降为O(nlogn),虽然它的代码往往涉及到递归调用,看起来非常复杂。
比如以下的一个简单得不能再简单的问题,求数组int a[4]={1,3,2,4}的最大值。第一反应想都不用想就得出如下的代码:
#include<stdio.h> void main(){ int a[4]={1,3,2,4}; int max=a[0]; for(int i=0;i<4;i++){ if(max<a[i]){ max=a[i]; } } printf("%d",max); }
然而,如果用分治法去比较:
不停地将数组拆分成n/2,直到无法再分割,就比如这里4个元素{1,3,2,4},先将其拆分成{1,3},{2,4},已经无法再拆分了,就先从各个小元组中最大值再组成数组{3,4},从而再从这个新数组中,求最大值为4,这里仅比较3次,{1,3},{2,4}的时候2次,{3,4}1次,时间复杂度为O(nlogn),从O(n)将成O(nlogn)是个了不起的成就,然而写成代码却是涉及到递归调用,无比地蛋疼:
#include<stdio.h> int getMax(int array[], int begin, int end){ int Max1 = 0; int Max2 = 0; if (begin == end) {//划分到最后,剩余1个数 return array[begin] = array[end]; } else if(begin+1==end){//划分到最后,剩余2个数 return array[begin]> array[end]?array[begin]: array[end];//谁大返回谁。 } else{//如果剩余的数多于2个,划分成两段,选出这两段的最大值 int mid=(begin+end)/2; Max1=getMax(array,begin,mid); Max2=getMax(array,mid+1,end); return Max1>Max2?Max1:Max2; } }; void main(){ int a[4]={1,3,2,4}; printf("%d",getMax(a,0,3)); }
下面再用一道2014年上半年的软件设计师软考题来说明这个问题,题目是这样的:
采用归并排序对n个元素进行递增排序时,首先将n个元素的数组分成各含n/2个元素的两个子数组,然后用归并排序对两个子数组进行递归排序,最后合并两个已经排好序的子数组得到排序结果,用C语言现实上述递归排序,其中常量和变量规定如下:arr是待排序的数组,一个子数组的位置从p到q,另一个子数组的位置从q+1到r,begin与end是待排序数组的起止位置,left与right是临时存放待合并的两个子数组,n1与n2为两个子数组的长度,i,j,k为循环变量,mid为临时变量。
#include<stdio.h> #include<stdlib.h> #define MAX 65536 void merge(int arr[],int p, int q,int r){ int *left,*right; int n1,n2,i,j,k; //计算左右数组应有的长度 n1=q-p+1; n2=r-q; //开拓左由数组的空间 if((left=(int*)malloc((n1+1)*sizeof(int)))==NULL){ perror("malloc error"); exit(1); } if((right=(int*)malloc((n2+1)*sizeof(int)))==NULL){ perror("malloc error"); exit(1); } //讲待排序数组的平分成两个n/2长的数组,分别存进左右数组 for(i=0;i<n1;i++){ left[i]=arr[p+i]; } left[i]=MAX;//封口 for(i=0;i<n2;i++){ right[i]=arr[(q+1)+i];//q+1是右数组的起始位置,而不是q,注意! } right[i]=MAX;//封口 i=0,j=0; for(k=p;k<=r;k++){//选择左右数组的小者,放入排序后的数组,也就是覆盖原来的数组中存在的内容 if(left[i]>right[j]){ arr[k]=right[j]; j++; }else{ arr[k]=left[i]; i++; } } } void mergeSort(int arr[],int begin,int end){ int mid; if(begin<end){//如果begin=end或者begin>end,证明分无可分,就不分了 mid=(begin+end)/2;//将数组分成2半 mergeSort(arr,begin,mid);//要求对左部分进行排序 mergeSort(arr,mid+1,end);//要求对右部分进行排序 merge(arr,begin,mid,end);//真正的排序算法 } } void main(){ int a[4]={4,2,1,3}; mergeSort(a,0,3); for(int i=0;i<4;i++){ printf("%d",a[i]); } }
归并排序的最好、最坏和平均时间复杂度都是O(nlogn),而空间复杂度是O(n),比较次数介于(nlogn)/2和(nlogn)-n+1,赋值操作的次数是(2nlogn)。因此可以看出,归并排序算法比较占用内存,但却是效率高且稳定的排序算法。这里涉及到对递归式f(n)=2*f(n/2)+O(n)的分析,比较难,记住它就行。
比起最简单的冒泡排序O(n2),时间复杂度降低得非常明显。