算法分析之分治法

本文详细介绍了分治法在排序(如快速排序、归并排序)、查找(查找最大次大元素、折半查找、第k小元素及中位数)和组合问题(最大子序列和、棋盘覆盖与日程安排)中的应用。涉及算法实现和复杂度分析,展示了分治策略在解决大规模问题中的效率提升。
摘要由CSDN通过智能技术生成


注:下文中的log n均表示以2为底的对数

一、概述

1、设计思想

对一个规模为n的问题,将其分解为k个规模较小的子问题,这些子问题相互独立且与原问题形式相同,递归解决这些子问题,再合并它们的解得到原问题。

2、求解过程

分解(找到基线条件)——求解子问题——合并

  • 【算法实现】
divide-and-conquer(P)
{   
    if |P|≤n0 return adhoc(P);
    将P分解为较小的子问题 P1,P2,…,Pk;
    for(i=1;i<=k;i++) //循环处理k次
    yi=divide-and-conquer(Pi); //递归解决Pi
    return merge(y1,y2,…,yk); //合并子问题
}
//k=1,减治法
//k=2,二分法

二、排序问题

1、快速排序(quick sort)

  • 【基本思想】

①取基准:任取待排序(n个元素)序列中的一个元素作为基准,一般取端点值
②分区:小于基准值的所有数字组成的子数组+基准值+大于基准值的所有数字组成的子数组
③递归(重复):对两个子数组重复以上过程直至每个子序列长度为1或0

  • 【快速排序的分治策略】

①分解:将原序列a[s…t]分解成两个子序列a[s…i-1]和a[i+1…t],其中i为划分的基准位置
②求解子问题:若子序列的长度为0或为1,则它是有序的,直接返回;否则递归地求解各个子问题
③合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作

  • 【算法实现】
int Partition(int a[]int s,int t) //划分算法
{ 
	int i=s,j=t;
	int tmp=a[s]; //用序列的第1个记录作为基准
	while (i!=j) //从序列两端交替向中间扫描,直至i=j为止
	{ 	
		while (j>i && a[j]>=tmp) 
			j--; //从右向左扫描,找第1个关键字小于tmp的a[j]
		a[i]=a[j]; //将a[j]前移到a[i]的位置
		
		while (i<j && a[i]<=tmp) 
			i++; //从左向右扫描,找第1个关键字大于tmp的a[i]
		a[j]=a[i]; //将a[i]后移到a[j]的位置
	}
	a[i]=tmp;
	return i;
}

void QuickSort(int a[]int s,int t)    //对a[s..t]元素序列进行递增排序
{	 
	if (s<t)       //序列内至少存在2个元素的情况
	{	 
		int i=Partition(a,s,t);
		QuickSort(a,s,i-1);   //对左子序列递归排序
		QuickSort(a,i+1,t);   //对右子序列递归排序
	}
}
  • 【算法分析】

快速排序算法的平均时间复杂度(最佳情况)是O(n log2n) ,但最糟时其运行时间为O(n²)。

2、归并排序(Merge sort)

  • 【基本思想】

①将a[0…n-1]看成是n个长度为1的有序表,将相邻的k(k≥2)个有序子表成对归并,得到n/k个长度为k的有序子表
②将得到的有序子表继续归并,得到 n/k² 个长度为 k² 的有序子表
③重复上述方法直至得到长度为n的有序表

  • 【二路归并排序的分治策略】

循环log n(上取整,log为以2为底的对数,下同)次,length依次取1、2、…、log n。每次执行以下步骤:
① 分解:将原序列分解成length长度的若干子序列。
② 求解子问题:将相邻的两个子序列调用Merge算法合并成一个有序子序列。
③ 合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作。

【自底向上的二路归并排序算法】

自单个元素开始向上成对归并

void MergeSort(int a[]int n) //二路归并算法
{ 
	int length;
 	for (length=1;length<n;length=2*length)
	MergePass(a,length,n);
}
  • 【算法分析】

对于上述二路归并排序算法,当有n个元素时,需要log n趟归并,每一趟归并,其元素比较次数不超过n-1,元素移动次数都是n,因此归并排序的时间复杂度为O(n log n)

【自顶向下的二路归并排序算法】

自整序列成对分解成单个序列再归并

  • 【分治策略】

设归并排序的当前区间是a[low…high],则递归归并的两个步骤如下:
① 分解:将序列a[low…high]一分为二,即求mid=(low+high)/2;递归地对两个子序列a[low…mid]和a[mid+1…high]进行继续分解。其终结条件是子序列长度为1(因为一个元素的子表一定是有序表)
② 合并:与分解过程相反,将已排序的两个子序列a[low…mid]和a[mid+1…high]归并为一个有序序列a[low…high]

  • 【算法实现】
void MergeSort(int a[]int low,int high)  //二路归并算法
{ 	int mid;
	if (low<high) //子序列有两个或以上元素
	{ 	
		mid=(low+high)/2; //取中间位置
		MergeSort(a,low,mid); //对a[low..mid]子序列排序
		MergeSort(a,mid+1,high); //对a[mid+1..high]子序列排序
		Merge(a,low,mid,high); //将两子序列合并,见前面的算法
	}
} //递归出口为序列长度为1或0
  • 【算法分析】

设MergeSort(a,0,n-1)算法的执行时间为T(n),显然Merge(a,0,n/2,n-1)的执行时间为O(n),所以得到以下递推式:
T(n)=1 当n=1
T(n)=2T(n/2)+O(n) 当n>1
容易推出,T(n)=O(n log n)

三、求解查找问题

1、查找最大和次大元素

  • 【基本思路】

对无序序列a[low.high]中,采用D&C求最大元素M1和次大元素M2
①a[low.high]中只有一个元素:
M1=a[low]
M2=-INF(-∞)(要求它们是不同的元素)
②a[low.high]中只有两个元素:
M1=MAX{a[low],a[high]}
M2=MIN{a[low],a[high]}
③a[low.high]中有两个以上元素:
按中间位置mid=(low+high)/2划分为a[low…mid]和a[mid+1…high]左右两个区间
求出左区间最大元素LM1和次大元素LM2,求出右区间最大元素RM1和次大元素RM2。
合并:
若LM1>RM1,
则M1=LM1,M2=MAX{LM2,RM1};
否则M1=RM1,M2=MAX{LM1,RM2}

  • 【算法实现】
void solve(int a[],int low,int high,int &M1,int &M2)
{ 	if (low==high) //区间只有一个元素
	{ 	
		M1=a[low]; 
		M2=-INF; 
	}
	
	else if (low==high-1) //区间只有两个元素
	{
		M1=max(a[low],a[high]);
		M2=min(a[low],a[high]);
	 }
	 
	else //区间有两个以上元素
	{ 	
		int mid=(low+high)/2;
		int LM1,LM2;
		solve(a,low,mid,LM1,LM2); //左区间求LM1和LM2
		int RM1,RM2;
		solve(a,mid+1,high,RM1,RM2); //右区间求RM1和RM2
		if (LM1>RM1)
		{ 	
			M1=LM1;
			M2=max(LM2,LM1); //LM2,RM1中求次大元素
		}
		else
		{ 
			M1=RM1;
			M2=max(LM1,RM2); //LM1,RM2中求次大元素
		}
	}
}

  • 【算法分析】

对于solve(a,0,n-1,M1,M2)调用,
其比较次数的递推式为:
T(1)=T(2)=1
T(n)=2T(n/2)+1 //合并的时间为O(1)
可以推导出T(n)=O(n)

2、折半查找

  • 【基本思路】

设a[low…high]是当前的查找区间,首先确定该区
间的中点位置mid=(low+high)/2(下取整);然后将待查的k值与a[mid]比较:
① 若k==a[mid],则查找成功并返回该元素的物理下标;
② 若k<a[mid],则由表的有序性可知a[mid…high]均大于k,因此若表中存在关键字等于k的元素,则该元素必定位于左子表a[low…mid-1]中,故新的查找区间是左子表a[low…mid-1];
③ 若k>a[mid],则要查找的k必在位于右子表a[mid+1…high]中,即新的查找区间是右子表a[mid+1…high]。
下一次查找是针对新的查找区间进行的

  • 【算法实现】
int BinSearch(int a[]int low,int high,int k)    //拆半查找算法
{ 
	int mid;
	if (low<=high) //当前区间存在元素时
	{ 
		mid=(low+high)/2; //求查找区间的中间位置
		if (a[mid]==k) //找到后返回其物理下标mid
			return mid;
		if (a[mid]>k) //当a[mid]>k时
			return BinSearch(a,low,mid-1,k);
		else //当a[mid]<k时
			return BinSearch(a,mid+1,high,k);
	}
	else return -1; //若当前查找区间没有元素时返回-1
}
  • 【算法分析】

折半查找算法的主要时间花费在元素比较上,对于含有n个元素的有序表,采用折半查找时最坏情况下的元素比较次数为C(n),则有:
C(n)=1 ——n=1
C(n)≤1+C(n/2) ——n≥2 (其中(n/2)下取整)
由此得到:C(n)≤log n+1 (log n是底为2下取整)
折半查找的主要时间花在元素比较上,所以算法的时间复杂度为O(log n)

  • 【问题】
    存在相同元素时怎么办?

3、查一个序列中的第k小元素

  • 【基本思路】

给定的含有n元素的无序序列,求这个序列中
第k(1≤k≤n)小的元素:
设无序序列存放在a[0…n-1]中,若将a递增排序,则第k小的元素为a[k-1]。

  • 【算法实现】
int QuickSelect(int a[]int s,int t,int k)   //在a[s..t]序列中找第k小的元素
{ 
	int i=s,j=t,tmp;
	if (s<t)
	{ 
		tmp=a[s];
		while (i!=j) //从区间两端交替向中间扫描,直至i=j为止
		{ 
			while (j>i && a[j]>=tmp) j--;
			a[i]=a[j]; //将a[j]前移到a[i]的位置
			while (i<j && a[i]<=tmp) i++;
			a[j]=a[i]; //将a[i]后移到a[j]的位置
		}
		a[i]=tmp;
		
		if (k-1==i) 
			return a[i];
		else if (k-1<i) 
			return QuickSelect(a,s,i-1,k);  //在左区间中递归查找
		else return QuickSelect(a,i+1,t,k);  //在右区间中递归查找
	}
	else if (s==t && s==k-1) //区间内只有一个元素且为a[k-1]
		return a[k-1];
}

  • 【算法分析】

对于QuickSelect(a,s,t,k)算法,设序列a中含
有n个元素,其比较次数的递推式为:
T(n)=T(n/2)+O(n)
可以推导出T(n)=O(n),这是最好的情况,即每次划分的基准恰好是中位数
将一个序列划分为长度大致相等的两个子序列。
在最坏情况下,每次划分的基准恰好是序列中的最大值或最小值,则处理区间只比上一次减少1个元素,此时比较次数为O(n²)。
平均情况下该算法的时间复杂度为O(n)

4、查两个等长有序序列的中位数

  • 【基本思路】

两个等长有序序列的中位数是含它们所有元素的有序序列的中位数,为了方便,只讨论下取整。
用二分法求含n个有序元素的序列a、b的中位数:
分别求出a、b的中位数a[m1]和b[m2]:
① 若a[m1]=b[m2],则a[m1]或b[m2]即为所求中位数,算法结束。
② 若a[m1]<b[m2],则舍弃序列a中前半部分(较小的一半),同时舍弃序列b中后半部分(较大的一半)要求舍弃的长度相等。
③ 若a[m1]>b[m2],则舍弃序列a中后半部分(较大的一半),同时舍弃序列b中前半部分(较小的一半),要求舍弃的长度相等。

  • 【算法实现】
int midnum(int a[]int s1,int t1,int b[]int s2,int t2)
{ 
	//求两个有序序列a[s1..t1]和b[s2..t2]的中位数
	int m1,m2;
	if (s1==t1 && s2==t2) //两序列只有一个元素时返回较小者
		return a[s1]<b[s2]?a[s1]:b[s2];
	else
	{ 
		m1=(s1+t1)/2; //求a的中位数
		m2=(s2+t2)/2; //求b的中位数
		if (a[m1]==b[m2]) //两中位数相等时返回该中位数
			return a[m1];
		if (a[m1]<b[m2]) //当a[m1]<b[m2]时
		{ 
			postpart(s1,t1); //a取后半部分
			prepart(s2,t2); //b取前半部分
			return midnum(a,s1,t1,b,s2,t2);
		}
		else //当a[m1]>b[m2]时
		{ 
			prepart(s1,t1); //a取前半部分
			postpart(s2,t2); //b取后半部分
			return midnum(a,s1,t1,b,s2,t2);
		}
	}
}

  • 【算法分析】

对于含有n个元素的有序序列a和b,设调用
midnum(a,0,n-1,b,0,n-1)求中位数的执行时间为T(n),显然有以下递归式:
T(n)=1 当n=1
T(n)=2T(n/2)+1 当n>1
容易推出,T(n)=O(log n)

四、求解组合问题

1、求解最大连续子序列和问题

  • 【基本思路】

对于含有n个整数的序列a[0…n-1],若n=1,表示该序列仅含一个元素,如果该元素大于0,则返回该元素;否则返回0。
若n>1,采用分治法求解最大连续子序列时,取其中间位置mid=(n-1)/2(下取整),该子序列只可能出现3个地方。
①该子序列完全落在左半部即a[0…mid]中。采用递归求出其最大连续子序列和maxLeftSum。该子序列完全落在右半部即a[mid+1…n-1]中。采用递归求出其最大连续子序列和maxRightSum。
②该子序列完全落在右半部即a[mid+1…n-1]中。采用递归求出其最大连续子序列和maxRightSum。
③该子序列跨越序列a的中部而占据左右两部分。max3( maxLeftSum,
maxRightSum,
maxLeftBorderSum+maxRightBorderSum )

  • 【算法实现】
long maxSubSum(int a[]int left,int right)
//求a[left..high]序列中最大连续子序列和
{ 
	int i,j;
	long maxLeftSum,maxRightSum;
	long maxLeftBorderSum,leftBorderSum;
	long maxRightBorderSum,rightBorderSum;
	
	if (left==right) //子序列只有一个元素时
	{ 
		if (a[left]>0) //该元素大于0时返回它
			return a[left];
		else //该元素小于或等于0时返回0
			return 0; 
	} 
	
	int mid=(left+right)/2; //求中间位置
	maxLeftSum=maxSubSum(a,left,mid); //求左边
	maxRightSum=maxSubSum(a,mid+1,right); //求右边
	maxLeftBorderSum=0,leftBorderSum=0;
	
	for (i=mid;i>=left;i--) //求出以左边加上a[mid]元素
	{ 
		leftBorderSum+=a[i]; //构成的序列的最大和
		if (leftBorderSum>maxLeftBorderSum)
		maxLeftBorderSum=leftBorderSum;
	}
	
	maxRightBorderSum=0,rightBorderSum=0;
	
	for (j=mid+1;j<=right;j++) //求出a[mid]右边元素
	{ 
		rightBorderSum+=a[j]; //构成的序列的最大和
		if (rightBorderSum>maxRightBorderSum)
			maxRightBorderSum=rightBorderSum;
	}
	return max3(maxLeftSum,maxRightSum,
					maxLeftBorderSum+maxRightBorderSum); 
} 

  • 【算法分析】

设求解序列a[0…n-1]最大连续子序列和的执行时间
为T(n),第(1)、(2)两种情况的执行时间为T(n/2),第(3)种情
况的执行时间为O(n),所以得到以下递推式:
T(n)=1 当n=1
T(n)=2T(n/2)+n 当n>1
容易推出,T(n)=O(n log n)

2、求解棋盘覆盖问题

3、求解循环日程安排问题

五、求解大整数乘法和矩阵乘法问题

1、求解大整数乘法问题

2、求解矩阵乘法问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

憨憨憨羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值